Commit Graph
139 Commits
Author SHA1 Message Date
lzf_0626 ae7a89f33d 投顾工作台:修推荐结果渲染(幂等响应形状归一)+ 对齐门户版式规范
投顾页「生成推荐方案」拿不到产品的根因:该端点标了 idempotent,浏览器必带
Idempotency-Key,而后端此时返回的是 {data:{content_id, status, plan:{...}}, meta}
—— 真正的文档嵌在 data.plan 里。此前前端只处理了不带键时的裸文档形状。

- api-client:ADVISOR_RECOMMEND 撤掉 raw(带幂等键时确为信封,需正常解包);
  ADVISOR_ALLOCATION / ADVISOR_ANALYSIS 保留 raw(不标幂等,返回裸文档)
- actions-module:新增 normalizeRecommend(),把「信封 + plan 嵌套」与「裸文档」
  两种响应归一成一种形状;结果卡片显示方案编号与待审核状态
- 投顾页:hero 大图 + 4 张概览指标卡 + 左栏/主区两栏构图
- 投顾页:页头常显「退出登录」按钮;新增风评预警数、快捷问句按钮
- 投顾页:动作名统一为「生成推荐方案」(对齐软件需求文档 5.4 ⑦)
- 管理台「待审投顾内容」空态文案同步改名
- 前端模块相对 import 加 ?v= 版本号:改文件内容而 URL 不变会被浏览器缓存挡住
2026-09-14 22:17:39 +08:00
lzf_0626 1d6c32f6b3 场外核对:桥接"单据账户标识 → 平台交易账号";保存识别字段后自动重跑核对;运营角色补推介材料权限
## 一、根因:核对查不到不是"缺数据",是**账户口径没桥接**

实测(客户 10002 的单据):

```
生成的 SQL: JOIN fin_holding h ... WHERE h.trade_account = '10002'   ← 单据上的账户标识
结果:       0 行 → 「申请前持有份额」= null → 规则判「无法判断」→ 单据状态 query_failed
```

而库里 `fin_holding.trade_account` 存的是**平台交易账号** `FSA{客户号:06d}`
(与 `fin_sim_account.account_no` 同值,见 `tools/seed_sim_account_demo.py:226`),
客户 10002 **一直持有 15911 共 1000 份**。所以页面上"单据核对与运营动作"是空的,
看起来像缺数据,实为**单据印的账户标识与平台账号是两个口径,中间少了一步换算**。

修法:新增 `OffsiteFundService._platform_account()`,在**单据字段落库的两个入口**
(`_create_document` 新建、`_apply_recognition_fields` 保存/重试)做换算,规则:

1. 已是库里的 `account_no` → 原样;
2. 纯数字且命中 `customer_id` → 用该客户的 `account_no`;
3. 其余原样返回并留 warning —— **失败关闭,不猜不造**(不凭空生成 `FSA099999`)。

一处修好,后端查询、NL2SQL 页面的自然语言、页面展示三处口径一致。
附件上的 OCR **原始值不受影响**(`extracted_fields` 仍是单据上印的 `10002`)。

端到端证据(同一张单据,只走服务层):

| 阶段 | document.account_identifier | 规则数据 | 单据状态 |
|---|---|---|---|
| 修复前 | `10002` | `{}` → 无法判断 | `query_failed` |
| 保存识别字段后 | **`FSA010002`** | — | `query_failed` |
| 触发核对后 | `FSA010002` | **申请前持有份额 1000.0000** → **正常** | **planned** |

## 二、前端:保存识别字段后自动重跑核对

`employee-operations/offsite/offsite.js`:原先 `save-ocr` 只保存 + 重载页面,
**不触发核对**,于是运营改完字段点保存,那两个区块要么停留在上一次核对的状态、
要么整块是空的,必须再手动点一次"重新核对并判定规则"——看起来像"保存没生效"。

现在:保存 == 运营已人工确认该单据内容,因此保存成功后**自动对该附件关联的单据**
执行「触发 NL2SQL → 拉取返回字段 → 重新判定规则」,并在提示里区分"已重新核对"
与"核对未全部成功"。

顺带把这段逻辑抽成 `recalculateDocument(taskId)`,与面板上的"重新核对并判定规则"
按钮**走同一条路径** —— 两处各写一份正是"保存后不刷新"这类不一致的来源。

## 三、运营(operator)角色权限

`tools/grant_operator_role.py` 的授权清单补齐:`promotion:write/read/review/deliver`
(`promotion_material_service.py` 的八处 `_require` 恰好只用这四个码,缺任一都会 403,
例如只给 write 会在查看详情 read 那一步被拒)+ `agent:run`。

已实际执行并**从身份侧验证**(`IdentityRepository.load_context`):
9005 / 9006 现在各 7 项权限,四个推介材料码齐全。

- NL2SQL 全库与它相关的权限码**只有 `financial:nl2sql:read`**(`offsite:nl2sql` 在
  `sys_permission` 里并不存在,是 `offsite_fund_service` 里 any-of 校验的死值),
  该码运营早已有,本次无需新增。
- 可持续性已核实:`sys_role_permission` / `sys_user_role` **没有外键**,
  重跑 `seed_test_rbac.py`(DELETE 重建 9001-9099 号段权限)**不会**删掉运营的绑定;
  且本工具按**权限码查 id**、不写死 id,天然抗号段变动。

## 四、10001 / 10002 的 15911 持仓

复核结论:**各 1000 份,且三处口径一致**(`fin_holding.market_value` = 数量 × 最新净值、
净值历史 120 条、`fin_product.current_nav` 与净值最新一条一致、账户可用资金正常)。
`fin_holding` 的唯一键是 `(customer_id, product_id)`,所以**不能**再插一行
`trade_account='10002'` 的"同一个持仓"—— 那会把持仓重复计数,是错的。
需要改数量就用 `python tools/seed_custom_holdings.py --quantity N`(默认就是这两个客户 + 15911)。

## 五、验证与回归

- 新增 `tests/unit/service/test_offsite_account_bridge.py`(5 条:账号原样 / 客户号换算 /
  认不出原样返回 / 不凭空造账号 / 空值不查库);
- `pytest tests/unit/service -k offsite` → 34 passed;
- 场外集成测试 4 个文件 → 27 passed;
- `ruff` 干净;`mypy app` 仍只有组员新代码里那 3 个既有错(与本次无关);
- 前端 `node --check offsite.js` 通过。
2026-09-14 22:16:29 +08:00
lzf_0626 292acd2e2c 新增「补客户归属」工具,并订正一处因迁移而过期的注释
## 新增 tools/assign_customer_scope.py

`sys_customer_assignment` 是**全平台**的"谁负责哪个客户"口径,不是记忆私有:
记忆召回(app/core/memory_scope.py)、`memory:read:customer` /
`investment-goal:*:customer` 的 `own_customers` 授权判定、投顾"已发布方案"的可见客户
都读它。所以"给某人补归属"是一句**授权动作**,不该用一次性 SQL 随手写进库。

本工具把它做成幂等、可复跑、默认只预览(--apply 才写),并**当场用真实身份链路回验**
(IdentityRepository.load_context → customer_memory_scope),还会在目标账号缺少
`memory:read:customer` 时明确提示"归属关系不等于授权"。

内置两个已踩过的坑:
1. `assigned_at` 不能写"当前时间" —— MySQL DATETIME(0) 四舍五入到秒可能落进未来,
   于是 `assigned_at <= now` 判定"尚未生效",归属拿不到**且不报错**;统一往前留 5 秒;
2. `employee_role` 是 NOT NULL 且参与唯一键 `(customer_id, employee_role)`,
   故从 RBAC 反查员工真实角色码,不靠手填。

`id` 在 9901+ 段分配(避开业务号段)。另外修掉一个写入期 bug:校验查询已经开了隐式事务,
再 `async with session.begin()` 会抛 `InvalidRequestError: A transaction is already begun`。

用法:
    python tools/assign_customer_scope.py                 # 预览:9002(风控) ← 9001
    python tools/assign_customer_scope.py --apply
    python tools/assign_customer_scope.py --show          # 只读:每个员工的实际可见范围

## 订正 tools/seed_advisor_demo.py 的过期注释

该文件原写"`sys_customer_assignment.id` 没有自增(EXTRA 为空),必须自己给" ——
这条**已被 20260914_baseline_auto_increment 迁移改变**(实测 EXTRA 现为 auto_increment)。
注释改为记录现状,并说明仍然显式给 id 的理由(保持号段约定 + 让种子可复现,
自增会让同一份种子在不同环境落到不同 id)。

## 本轮实际执行

- 补入一行归属:员工 9002(risk_operator)→ 客户 9001(id=9902,幂等复跑正确跳过);
- 回验:风控 9002 的记忆可读范围从 `()` 变为 `(9001,)`,召回从 0 条变为 2 条;
  投顾 9020 仍 2 条;
- 该动作**只影响记忆召回**:9002 其余权限都是 `data_scope='all'`,
  `scope_from_context()` 见 all 直接放行、不看 customer_ids。
2026-09-14 21:46:29 +08:00
lzf_0626 2c5fe9cdef 把风控扫描演示数据接进一键准备(第 12 步),并让编排支持给步骤传参
## 改动(`tools/seed_demo_data.py`)

1. **`STEPS` 增加第四个字段「额外命令行参数」**。这不是可有可无的装饰:
   `seed_risk_demo_data.py` **不带参数时只打印计划、不连数据库**(默认是干跑模式),
   不给它传 `--apply` 的话,接进编排只会打印一句"确认无误后使用 --apply 正式写入"
   然后退出码 0 —— **看起来像成功,实际什么都没准备**。这类"假成功"最难查,
   所以参数必须能传得进去。
2. **新增第 12 步「风控扫描演示数据」**,带 `--apply --dry-run-scan`:先写入上游
   业务数据,再跑一次规则干跑并把结果回滚。净效果是「写入上游数据 + 顺带验证
   5 个场景都真的命中预期规则」。
3. `run_step` / `--list` / 主循环相应支持第四项;执行时把参数**打印出来**,
   这样手跑时一眼能看出这一步到底会不会落库。

## 为什么放第 12 步,而不是插在「风控预警样本」旁边

避免**改动后续步骤的编号**。文档与肌肉记忆里已经有 `--only 4,10`、`--from 4`
这类用法,插队会让它们全部错位。而且它本身是独立号段(客户 12001-12005、
产品 159991-159995),放最后不影响任何既有步骤。

## 第 6 步与第 12 步不是重复(docstring 已写明)

- 第 6 步 `seed_risk_alert_demo_data.py`:**直接摆好**三条现成预警,
  保证风控页面一打开就有东西可看;
- 第 12 步 `seed_risk_demo_data.py`:**只造上游**,刻意不写 `fin_risk_alert`,
  让风控扫描器按规则生成 —— 演示覆盖的是**规则 → 证据 → 通知**完整链路,
  而不是"预先摆好的假预警"。

## 实测

- `--list` -> **12 步**,第 12 步末尾显示 `[--apply --dry-run-scan]`
- `--only 12 --dry-run` -> 只打印、不执行(并且打印出了参数)
- `--only 12` -> **真跑通**,退出码 0:

      [12/12] 风控扫描演示数据  用时 3.8s  退出码 0
      [命中] 客户 12001:RW-003 (高)
      [命中] 客户 12002:RW-007 (高)
      [命中] 客户 12003:RW-007 (中)
      [命中] 客户 12004:RW-012 (高)
      [命中] 客户 12005:RW-015、RW-018 (低)

- `ruff check tools/seed_demo_data.py` -> All checks passed
2026-09-14 20:50:55 +08:00
lzf_0626 e00c4ad51d 风控演示数据脚本:造上游业务数据,让风控扫描器自己生成预警
## 来源

组员通过腾讯会议发来的 `seed_risk_demo_data.py`,按约定放入 `tools/`。
**脚本内容是组员写的**,我只做了两件事:放进仓库、把 docstring 里的用法示例
统一成实际文件名(组员原文写的是 `seed_risk_scan_demo_data.py`,与文件名不符,
已改成 `tools/seed_risk_demo_data.py`),**未改动任何逻辑**。

## 它做什么

固定号段:客户 **12001–12005**、产品 **159991–159995**(独立号段,不覆盖人工数据
与其他演示数据)。写入客户 / 画像 / 产品 / 账户 / 持仓 / 交易 / 资金流水 / 登录 /
工单,**刻意不写 `fin_risk_alert` 与 `fin_risk_notification`** —— 预警必须由风控
扫描器按规则生成,这样演示才能真实覆盖规则、证据、通知三条链路。

五个场景各自瞄准一条规则:

| 客户 | 场景 | 预期规则 |
|---|---|---|
| 12001 | 大额快进快出(3 天内入金 80 万后赎回 75 万,比例 93.75%) | RW-003 |
| 12002 | 高风险适当性错配(缺风险揭示 + 二次确认) | RW-007 |
| 12003 | 中风险适当性错配(等级差 1 且缺风险揭示) | RW-007 |
| 12004 | 72 岁客户赎回 75 万,达历史均值 3 倍且用非常用设备 | RW-012 |
| 12005 | 凌晨 2:15 小额自动定投(关联有效工单,预期合并预警) | RW-015、RW-018 |

模式:`--check`(只读核对)、默认 `--dry-run`(只打印计划、**不连数据库**)、
`--apply`(写入)、`--dry-run-scan`(跑规则引擎后回滚)。

## 我跑了什么、核到什么

按约定的命令 `--apply --dry-run-scan`:

    [命中] 客户 12001:RW-003 (高)
    [命中] 客户 12002:RW-007 (高)
    [命中] 客户 12003:RW-007 (中)
    [命中] 客户 12004:RW-012 (高)
    [命中] 客户 12005:RW-015、RW-018 (低)
    规则干跑完成,共命中 5 条演示预警,已回滚,不会写库。

**"已回滚"这句话我没有只信它的打印**,而是直接查了库:

- 上游数据确实写入:客户 5 / 产品 5 / 账户 5 / 持仓 5 / 委托 6 / 成交 6 /
  资金流水 1 / 登录 1 / 工单 3,演示产品行情 5 行(`source=custom_seed`);
- **`fin_risk_alert` = 0、`fin_risk_notification` = 0** —— 回滚确实生效。

跑之前还先确认了它承诺回滚是**成立**的:`RiskRuleEngine.refresh_alerts()`
(`risk_scan_service.py:101`)到 565 行之间**没有 `commit()`**,全文件仅有的
`commit/rollback` 在 `:566-568`,属于另一个方法(正式扫描 `scan()`)。
否则 `dry_run_scan` 的 `session.rollback()` 根本回滚不掉,那就是"假干跑"。

## 复用与兼容

脚本 import 了 `tools/seed_custom_holdings.py` 的 `ensure_user` / `ensure_account` /
`ensure_holding` / `ensure_product` / `ensure_nav_history` / `ensure_market_price`,
我逐个核对了签名,**全部匹配**(组员看过那个脚本)。

它自己的 `_next_id` 用 `SELECT MAX(id)+1` 发号:单线程脚本里没问题,且
`sys_user` 与 `fin_product` 本来就没有 AUTO_INCREMENT(被外键引用,迁移
`20260914_baseline_auto_increment` 已说明为何排除)。**这不是缺陷**,不改。

## 实测

- `ruff check tools/seed_risk_demo_data.py` -> All checks passed
- `--apply --dry-run-scan` -> 5/5 场景命中预期规则,退出码 0
- `pytest tests/unit tests/contract` -> **1428 passed, 2 skipped, 1 failed**
  (那个失败是投顾工作台页面被替换所致,与本次无关)
2026-09-14 20:48:18 +08:00
lzf_0626 c0e5c80929 记忆系统:recall 结果接入 prompt + 可观测性(既有改动,代为提交)
## 说明

**这批改动不是本次会话写的**,它们在会话开始前就已在工作区里、一直未提交。
我做的是**验证**它确实成立,然后按你的指示代为提交。

出处:`docs/演示用/记忆系统排查报告-2026-09-14.md` 与同目录
`记忆系统修复文档-2026-09-14.md`(两份都在本次一并入库)。
排查报告的结论是「记忆系统没有坏」——库里有真实数据、170 条抽取事件全部消费成功;
真正的问题是「观测不到」+「召回结果没人消费」。

## 改动内容(按两份文档的编号)

- **F1 `RecalledMemory.content` 断头路**:`base.py` 新增 `memory_context_text()`,
  `risk_agent._agent_system_prompt` 接收并注入记忆段。无记忆时返回空串,
  因此 prompt 逐字不变 —— 这也是它能安全接线的理由。
- **F3 `governance.recall` 员工身份恒空**:补一条明确的语义日志。
  员工身份下召回的是"该用户自身作为客户"的记忆,恒为空属预期,
  但此前没有任何提示,运维看到 `count=0` 只会以为记忆坏了。
- **F4 可观测性**:`GET /api/v1/users/me/memories`(`stored` / `recalled` /
  `downstream` / `pending_events` 四段)+ 抽取与召回的 6 处日志 +
  三个只读探针 `tools/probe_memory_state.py`、`probe_memory_detail.py`、
  `probe_agent_types.py`。

**未实施**(文档明确留作待决,我也不代为决定):F2 `known` 引用校验永不触发
(需架构确认 memory 类 `source_references` 由业务填还是底座统一附加)、
F5 客服是否读写长期记忆(涉脱敏与复核,需产品+合规)。

## 我做的验证(会话内实测,非照录文档)

- 新接口 `GET /users/me/memories` 以 `cust_t` 调用 -> **HTTP 200**:

      stored:    total=2, by_status={'active': 2}
      recalled:  count=2, degraded=False
      两条记忆:preference:horizon='约三年'(0.95)、preference:risk_level='稳健型'(0.98)

  与排查报告 §〇 列出的那两条**完全吻合**。
- `pytest tests/unit tests/contract` 全绿(这批改动没有破坏既有测试)。

## 未验证的部分

`memory_context_text()` 接进 prompt 后的**端到端效果没有实测** —— 文档自己说明了
原因:当前 `risk` Agent 的召回恒空(员工身份不是客户),所以接线后行为不变,
要用测试替身才能验证注入。我没有为此编造证据。
2026-09-14 20:35:46 +08:00
lzf_0626 c868f01e67 修复 §19 端点编号检查的误报:按表格块识别,而不是"整章所有表格行"
## 问题

`test_real_document_has_no_duplicate_endpoint_ids` 失败,报 7 处"首列无法识别":
`**场外基金**`、`**场外运营**`、`**客户入驻**`、`**推广材料**`、`**访客令牌**`、
`**账户与交易**`、`段`。

查下去发现**文档没错、工具错了**:§19 里除端点总目录外,还有一张
「段 / 前缀 / 挂载来源 / 权限口径」的**说明表**(`docs/05` 行 1207 起)。它的首列是
分组名、表头是 `段` 而不是 `编号`。工具早期实现把**整章所有表格行**都当端点表扫,
于是那张说明表的 6 个分组名 + 1 个表头被报成结构异常。

**误报比漏报更伤**:这个脚本存在的意义是抓"编号被复用"(2026-09-12 真的发生过
`A034`/`A035` 各出现两次),可一旦它平时就在报噪声,真出问题时会被当成噪声忽略。

## 改法

`collect_rows` 改为**按表格块**识别:

- 表格块的第一行是表头;**只有表头首列是 `编号`** 的表格才是端点总目录;
- 其它表格整体跳过并**计数** —— 跳过几张表会打印出来,所以"忽略了什么"是可见的,
  不是静默的;
- 端点表**内部**形状不对的首列**仍然报出来** —— 那才是真正的结构异常。

于是"§19 覆盖"这行现在长这样:

    §19 覆盖:93 个端点编号 / 9 个号段(A×49、AD×11、C×7、K×4、M×4、O×3、P×2、R×4、T×9);跳过 1 张非端点表

跳过的表被显式写出来,覆盖范围仍然可核对 —— 这是原实现"不枚举前缀白名单、
把扫到的号段一并打印"那套自检思路的延续。

## 测试

`test_real_document_actually_scans_enough_endpoints` 因 `collect_rows` 返回值从
2 元组变 3 元组而需要同步;顺便**加了一条断言守住新行为**:
`skipped >= 1`,即"§19 的说明表确实被识别为独立表格块并跳过" —— 否则那几个分组名
会重新变成误报,而这条测试会先失败。

## 实测

- `python tools/check_docs_endpoint_ids.py` -> **exit 0**
  「93 个端点编号 / 9 个号段;跳过 1 张非端点表 / 端点编号无重复」
- `pytest tests/unit tests/contract` -> **1428 passed, 2 skipped, 1 failed**
  (剩下的 1 个是投顾工作台 `index.html` 被整体替换成自包含静态页所致,
  属产品决策,见前述说明)
- `ruff check` -> All checks passed
2026-09-14 20:29:54 +08:00
yuancong_0626 2548c39c6b 袁聪的最后一次完善更新 2026-09-14 18:13:10 +08:00
lzf_0626 eb89ffd0b4 按需造数据:给指定客户建持仓,并保证产品有可查的最新净值
## 需求

产品 15911、客户 10001 / 10002;最终要能查到这两户持有 15911 的持仓,
以及 15911 的最新净值。

## tools/seed_custom_holdings.py(新)

参数化脚本,不写死这次的代码与账号。手工写 SQL 会连踩一串坑,每一条都在本仓库真实发生过:

1. `fin_*` 的 `id` 列**没有 AUTO_INCREMENT**,不显式给 id 就报
   `Field 'id' doesn't have a default value`;
2. `fin_holding.shares` / `current_value` 文档标注为"生成列",实测是普通 NOT NULL 列,
   不写插不进去;
3. 只建 `sys_user` 不绑角色 -> 登录成功但查持仓 403;
4. 只建用户不设密码 -> `password_hash` 是占位符,**根本登不进来**;
5. 净值散在三处(`fin_product.current_nav`、`fin_nav_history` 最新一条、
   `fin_market_price.close_price`),只写一处就会"产品页有净值、持仓页市值为空";
6. 往 `fin_market_price` 写"今天"的编造行情会**盖住真实行情** —— 下单按 `trade_date DESC`
   取,编造行排在前面,过了 `MAX_QUOTE_AGE`(15 分钟)整个产品就一律 503
   (周末尤其明显)。

脚本把这些一次做对,并保证上面第 5 条的三处严格一致(持仓市值恒等于 数量 x 最新净值)。

## 开户风险测评:走接口,不插表

这是这次最关键的发现。数据都建好之后,客户能登录,但访问 T001 / T006 一律 403,
报的是 `AGENT_PERMISSION_DENIED`「请先完成开户风险测评问卷」——
那是合规前置(`app/api/dependencies/auth.py` 的 `OnboardingRequiredError`),
不是 RBAC 没配好,两者表象一模一样,很容易查错方向。

`RiskQuestionnaireService.is_required` 只查 `fin_risk_assessment.valid_until > now`,
所以插一行就能过检查。**没有那样做**:插表不会生成画像快照、画像标签和记忆同步事件,
而投顾分析、适当性检查、Agent 对话都要读那些。改走真实接口
`POST /api/v1/onboarding/risk-questionnaire/submissions`,让业务链路自己把关联数据建全。

内置 13 题答案按 `_SCORE_RULES` 折总分 46 -> C4 成长型(产品是 R3,C4 有余量)。
等级**不从接口读**:提交返回里故意没有它(`_SCORE_RULES` 是服务端机密,注释写明
never included in customer responses),所以从库里读回并打印。实测读到 C4 / 46 分,
与手算完全吻合。

## 实测(接口层,不只是查库)

- P001 访客产品列表:21 个产品,★ 命中 15911,`current_nav=1.000000`
- P002 净值走势:30 个点,最近 3 条 2026-09-10 / 09-11 / 09-14
- 10001 / 10002 登录 -> T001 资产看板 200:总资产 101000.00、总市值 1000.00
- 10001 / 10002 -> T006 持仓 200:15911 数量 1000、最新价 1.000000、市值 1000.00
- 幂等复核:第二次执行不重复建记录、不覆盖持仓数量、不重复提交测评、不重置密码
- `ruff check app tests tools alembic hq.py` -> All checks passed

## 已知限制

产品代码 `15911` 是 **5 位**,而系统现有 20 个产品**全是 6 位**,且下单要求行情落在
`FundQuoteRuntimeConfig.allowed_codes`(六位数字)白名单里。
所以它**能建、能查持仓、能查净值,但不能下单**。
若是笔误,用 `--product-code 159911` 重跑即可。
2026-09-14 10:03:07 +08:00
lzf_0626 e096ffab22 修复投顾工作台白板:补上模块拆分时漏掉的 import
## 问题(合并进来的故障,不是本次会话改坏的)

合并 `origin/qyqy_develop`(f5d1b24 / 3134fe5)后 `pytest` 红了一条:
`test_advisor_dashboard_is_composed_from_feature_modules`。

查下去发现是**拆分做了一半**:

- 新建了 `advisor-config.js` / `actions-module.js` / `published-module.js`
- 把 `CONTENT_TYPE_LABELS`、`actionLabels`、`resultMessages` 从 `dashboard.js` 删掉了
- **但没有在 `dashboard.js` 里 import 它们**,`dashboard.js` 仍是拆分前的内联版本,
  第 39 行还在用 `CONTENT_TYPE_LABELS`

后果不只是测试红:投顾工作台一打开就 `ReferenceError: CONTENT_TYPE_LABELS is not defined`,
页面渲染不出来;同时那两个新模块是**死代码**(没有任何地方 import 它们)。
`index.html` 是单入口(只加载 `dashboard.js`),所以模块必须由它 import。

## 修法:把重构接完,而不是把测试改掉

- `dashboard.js` 变成薄组合层:挂 shell、取 DOM、组合两个模块,其余逻辑不再内联
- `advisor-config.js` 收拢 `GOAL_STATUS_LABELS` / `BOOK_STATUS_LABELS`(原来内联在 dashboard.js)
- `actions-module.js` 接管「目标确认与方案书」
- `published-module.js` 直接可用

⚠️ 关键点:`bind()` 会给**所有** `[data-action]` 按钮挂 `open()`,而 `actions-module.js`
原先不认识 `goal-status` —— 直接接线会让它掉到最后一行的兜底分支、被当成
「资产配置」发出去(点"目标确认与方案书"却收到一份配置建议)。
所以把「目标确认与方案书」一并做进 `open()` 的分支里,并在两处留了注释说明这个约束。

「目标确认与方案书」这条功能本身要保留:此前工作台只有 4 个"生成草案"操作 + 1 个只读列表,
而确认目标与查看方案书这两个端点**有接口没入口**,导致目标永远停在 `pending_confirmation`、
方案书永远停在 `pending`(实测客户 9001 正是如此)。

## 防回归:tools/check_portal_modules.py(新)

上面那个 bug **不能靠现有断言发现** —— 那些测试断言的是"某个字符串在文件里出现",
而这里的问题是"定义搬走了、使用处还在",浏览器里才炸,Python 测试全绿。

新检查做四件事:`node --check` 按 ES module 解析语法、相对 import 的目标文件存在、
import 的名字在目标文件里真有 `export`、**用到的全大写常量必须有来源**。

第 4 条是抓这个 bug 的关键。写的时候踩了两次坑,都已修正并记录在文件里:

1. 第一版用 `(?<![\w.$])` 排除属性访问、却把**模板字符串整体**当字符串剔除了 ——
   而 `CONTENT_TYPE_LABELS[row.content_type]` 恰好写在模板字符串里,
   于是漏报、检查全绿。现在只剔除单双引号字符串,模板字符串保留(`${}` 里是真代码)。
2. 用负面验证确认它真的有效:把 `published-module.js` 的 import 拿掉后,
   检查精确报出 `使用了 'CONTENT_TYPE_LABELS',但既没 import 也没在本文件声明`(exit 1);
   恢复后 exit 0。没有这一步,这个检查就是个摆设。

同时接进测试:`test_portal_feature_modules_have_consistent_imports` 调用它,
保证以后每次 `pytest tests/unit` 都会执行。

## 实测

- `pytest tests/unit tests/contract` -> **1400 passed, 2 skipped, 0 failed**
  (合并后未修时是 1399 passed + 1 failed)
- `pytest tests/unit/api/test_portal_frontend.py` -> 39 passed(38 + 新增 1 条)
- `python tools/check_portal_modules.py` -> 全部通过;负面验证 exit 1
- `ruff check app tests tools alembic hq.py` -> All checks passed
2026-09-14 02:07:44 +08:00
lzf_0626 404f8f6aa5 固化接口体检脚本,并交付双击即用的启动 bat
## tools/portal_api_check.py(新)

把 2026-09-13 那轮"照着接口内容把前端测一遍"的验证固化成可重复跑的脚本。
当时靠这套验证查出 6 个真缺陷,但它们只存在于一次会话里,下次改动没人会重跑:

- `K002`/`K003` 是裸信封,前端按 `payload.data` 取 -> 知识库页显示"已入库 0 块 / 为空"
- 委托/成交详情页照着建表字段写,而接口返回视图没带那些列 -> 5 行永远显示 `--`
- 配置项/路由规则的 `PUT` 要 `If-Match`,却没有端点能返回该 digest -> 首次编辑必然 409
- 回复模板 `scene` 只校验长度不校验枚举 -> 非法值撞数据库 CHECK、冒成 500
- 路由规则表单固定 `max_attempts=2` 且 `fallbacks` 为空 -> 后端必然 422
- 模型端点手填 ID -> 未激活的 ID 直接 422

与 `e2e_smoke_test.py` 分工互补:后者测**业务链路通不通**,本脚本测**接口契约对不对**
(状态码、信封形状、字段名与前端期望是否一致)。

三档,默认只跑第一档:
- 默认只读 41 项(不动数据)
- `--write` 加测写操作:配置项 ETag 链路、`max_attempts` 越界、知识库上传+失效、
  回复模板非法枚举、敏感词、推广物料全链路
- `--dangerous` 再加测会改生效配置的操作(激活配置版本会整版本替换,默认不跑,
  脚本内注明了恢复办法)

实测:只读 39/39、写入档 54/54 全绿。

## 桌面一键启动(双击即用)

桌面 `启动金融Agent平台.bat` + 仓库 `启动平台.bat`,由
`tools/make_launcher_bat.py` 生成,只负责"双击"这一层,启动逻辑复用 `start.ps1`
(不重复实现,避免两边漂移)。参数可透传:`-Port 8100` / `-NoBrowser` / `-SkipPriceSync`。

不要手写这个 bat:必须同时满足 **GBK 编码 + CRLF 换行 + 无 BOM**。生成器自己
读回来校验这三条。踩过的坑:用 `write_bytes` 直接落盘时没转 CRLF,cmd 对 LF-only
批处理会行边界错乱,把 `echo` 的说明文字当命令执行(实测报
`AT 命令已弃用`、`']' 不是内部或外部命令`)。

## start.ps1 的三处修复

1. **解释器探测选错环境(真机双击失败的主因)**
   原先只验证 `--version` 成功就选中,结果挑到一个 Python 3.10 环境:
   本项目用了 `datetime.UTC`(3.11+)且依赖 `asyncmy`,启动当场 ImportError ——
   而报错发生在**行情刷新**那一步,看起来像"行情源坏了"。
   现在实测两项:**版本 >= 3.11** + `import fastapi, sqlalchemy, asyncmy, pydantic` 通过,
   并在跳过时打印具体原因。

2. **`Select-Object -First 1` 掐断 native 管道**
   `& $exe -c ... 2>&1 | Select-Object -First 1` 拿到首个对象后停掉上游管道,
   等于把进程掐了、`$LASTEXITCODE` 变脏,于是**每个候选都被误判成"无法执行"**
   (连装好的 jr_py313 也被跳过)。改为先整体接住输出、再在结果上取行。

3. **幂等 + 就绪探测 + Docker 自动拉起**
   重复双击不再起第二个 API/Worker(API 按端口、Worker 按进程判断);
   起完等 `/internal/health/ready` 真正应答才报成功;Milvus 不可达时尝试拉起
   Docker Desktop 并最多等 60 秒(它不常驻,是"知识检索静默降级"最常见的原因)。

## 实测

- `启动平台.bat` 重复执行:解释器正确选中 jr_py313、依赖检查全 OK、行情已刷新、
  两个服务识别为已在跑并跳过、就绪探测 1 秒
- `-Port 8199` 冷启动:新起 API 窗口,8199 的 `/internal/health/ready` 与 `/portal/` 均 200,
  测试后已清理
- `ruff check app tests tools alembic hq.py` -> All checks passed
- `start.ps1` 语法解析通过(BOM 已保留)、bat 三项编码约束校验通过

文档同步:`AGENTS.md`(启动方式、bat 生成器、解释器探测两个坑)、
`docs/44-演示流程.md` §0.3/§0.4(双击启动、两条自检线的分工)。
2026-09-14 01:59:46 +08:00
yuancong_0626 abbfb6eddd 合并yy并同步远程qyqy_develop 2026-09-14 01:11:07 +08:00
yuancong_0626 7a49a1c2c8 袁聪的前端调修 2026-09-14 01:07:43 +08:00
lzf_0626 7208713c17 feat(portal): 接入历史净值,产品详情页恢复真实走势图
`fin_nav_history` 一直是**空表**,所以产品详情页画不出走势图 —— 此前那条曲线是
前端 `mock-data.js` 里编的 12 个点位,接入公开产品接口时把它去掉了
(走势图最容易被当成真实业绩),页面改为显示"尚未接入"。本次补上完整链路。

## 1. 取数(app/infrastructure/fund_market_adapter.py)

新增 `fetch_nav_history()`:东方财富历史净值接口(`api.fund.eastmoney.com/f10/lsjz`)
分页取序列。

- **为什么不复用 `fetch_kline`**:它走 `push2his.eastmoney.com`,
  该域名在本环境实测连接被拒(`RemoteProtocolError`);
- **为什么不复用 `hq.get_southern_fund_nav_history`**:那个函数校验**南方基金白名单**,
  而 `fin_product` 里有非南方基金的产品(510300 是华泰柏瑞的),调它会直接 `ValueError`。

⚠️ 实现里踩到一个坑:接口**会忽略请求中的 `pageSize`**(实测固定每次返回 20 条)。
最初按硬编码的 30 判断"是否最后一页",于是 `len(items) < 30` 永远成立、只取到第一页 ——
走势图看上去"有数据",其实只有最近 20 天,而且毫无报错。
改为按首屏**实际条数** + `TotalCount` 推算页数后,同样区间取到 124 条。

## 2. 落库(tools/sync_nav_history.py,新增)

写入 `fin_nav_history`,按 `(product_id, nav_date)` 幂等 upsert。
实测:20 只产品 / **2513 行** / 2026-03-17 ~ 09-13;重跑**新写入 0 行**。

## 3. 接口(P002)

`GET /api/v1/products/{product_code}/nav-history`,编号 **P002**,已登记 `docs/05` §19。
鉴权口径与 P001 相同(要求有效令牌、不校验权限码,访客令牌可用);`days` 有界 1–365。

**表为空时返回 `count=0` 与空数组,而不是报错** —— 调用方据此显示"尚未接入",
**不得回退到编造曲线**。产品不存在或未上市 → `404`(否则前端分不清"没有数据"
和"没有这只产品")。

## 4. 前端

详情页按序列画 SVG 折线,期数标题改为动态("近 N 个交易日")。
表为空时仍显示"尚未接入"占位,并补上此前缺失的 `.detail-chart__empty` 样式。
`product-detail.js` / `.css` / `index.html` 的缓存版本参数一并 bump 到 `-7`。

## 5. 演示数据

`tools/seed_demo_data.py` 增加第 5 步「历史净值」(现 **11 步**),
否则换台机器演示时走势图又会是空的。

验证:P002 实测 515450 / 510300 各 120 个净值点;ruff 通过;mypy 251 文件 0 错;
unit+contract 1391 passed;integration 108 passed;e2e 冒烟 40/40。
2026-09-13 23:38:13 +08:00
lzf_0626 0b82053ca2 fix(rbac): 交易权限的 data_scope 写成了动作名,导致下单/成交静默 403
`tools/seed_test_rbac.py` 的 `PERMISSIONS` 行形状是
`(id, 权限码, resource, action, data_scope)`,而 T 段那四条写成了
`(9061, "trade:order:create", "trade", "order", "create")` ——
「resource, action」多写了一段,于是 action 落进了 `data_scope` 的位置。

## 后果是静默失效,不是放宽

`IdentityRepository.load_context` 只收集 `data_scope ∈ {self, own_customers, all}`
的权限,其余**整条丢弃**:

    if row["permission_code"] and row["data_scope"] in rank:

于是客户在库里**明明有**这四个权限,`POST /api/v1/users/me/orders`、
`GET /api/v1/users/me/orders`、`GET /api/v1/users/me/transactions` 等 T 段端点
却全部返回 `403 AGENT_PERMISSION_DENIED`「缺少操作权限」。
而同一批里 `account:read:self`、`holding:read:self` 的 scope 是 `self`,照常 200 ——
现象特别像"只有交易坏了",几乎不会有人去怀疑**权限行本身写错了字段**。

因为 seed 是 `DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099` 的**重建**语义,
**每跑一次种子就重现一次**,而 `tools/seed_demo_data.py` 的第 1 步正是它。

## 修复与守卫

- 四条改为 `"self"`,并就地写清第 5 个字段是 `data_scope`、只能取三种值;
- `tools/check_rbac_seed_consistency.py` 增加**第 5 条检查**:每行字段数必须是 5,
  且 `data_scope` 取值合法。已用模拟对象确认它能抓住事故写法
  (非法 scope 报 2 条、字段数不足报 1 条、合法写法通过),
  并由 `tests/unit/tools/test_rbac_seed_consistency.py` 纳入门禁。

修复后实测:库内非法 `data_scope` 归零(`self` 22 → 26);
`/users/me/{account/dashboard,holdings,transactions,orders}` 全部 200;
下单成交价 4.579(真实行情);e2e 冒烟 40/40;integration 106 passed。
2026-09-13 22:57:59 +08:00
lzf_0626 f6bfcc26b3 fix(demo): 把未上市的 160129 换成真实挂牌的 515450,并修掉种子假行情盖住真行情
## 1. 160129 本就不该在场内清单里

`160129`(南方金利定开债券C)是 `160128`(金利定开债券 **A** 类)的 C 类份额,
而 C 类份额只在场外销售、**不在交易所挂牌** —— 行情源对它永远返回空,下单只能走
净值降级(`source=eastmoney_nav_fallback`),既掩盖了"该产品并不交易"这一事实,
又让成交价带上折溢价偏差。

选型依据:把南方基金全部 **882 个代码**逐个问过腾讯行情源(该源只对交易所上市证券
返回数据),确认真实上市交易的只有 **109 个**;其中 R1/R2 的场内产品
(159700、160128、511070、511810)**原先已在清单内**,所以替换品只能来自 R3 及以上。
选定 **`515450` 红利低波50ETF南方**:仍是南方基金旗下、成交额约 1.3 亿元流动性充足、
红利低波定位偏稳健,与它替换掉的债券 LOF 定位最接近;客户风险等级已覆盖 R3
(原有 `510300` 即 R3)。

改动:`hq.py` 的 `FUND_TYPE_GROUPS`、`tools/import_hq_test_products.py` 的场内映射,
两处都留了注释防止被加回来;`docs/42` / `docs/43` 的产品表与费率表同步
(净值 1.4027、管理费 0.50%、托管费 0.10%);`docs/42` 另加了一条决策记录。

库内用**复用 `fin_product.id = 9100005`** 的方式替换,使 `fin_holding` /
`fin_sim_order` / `fin_transaction` / `fin_market_price` 的既有引用自动跟随,
不产生孤儿数据。那笔历史成交保留 `quote_source='eastmoney_nav_fallback'` ——
它确实是当时的事实,不该被粉饰。同时清掉了 `fin_market_price` 里那条净值降级行,
全库净值降级行情行数归零。

## 2. 种子假行情会盖住真实行情(更严重,且会反复出现)

`seed_sim_account_demo.py` 原来按"今天"upsert 一条假行情(510300=4.50、
510500=6.20,`source='eastmoney_demo_seed'`),而真实行情来自行情源、日期是
**最近交易日**。下单按 `trade_date DESC` 取行情,这条种子行**永远排在真实行情前面**;
它的 `source_updated_at` 只是 seed 运行时刻,过了 `MAX_QUOTE_AGE`(15 分钟)就让整个
产品变成 `503 行情已过期` —— 而库里明明躺着一条刚同步好的真实行情。周末尤其明显:
`trade_date` 落在**非交易日**,价格还是编的。

实测表现:`tools/seed_demo_data.py` 跑完第 4 步刚同步过真实行情,冒烟脚本的下单仍报
`503`,且本该 4.579 的成交价被查到 4.50。

改为:**已有任何行情行就绝不插手**(真实行情优先,种子不参与竞争);
一条都没有时才补,且 `trade_date` 退到最近交易日。

验证:`tools/e2e_smoke_test.py` → **40/40**(下单成交价 4.579 = 真实行情);
ruff 通过;mypy 250 文件 0 错;unit+contract 1381 passed / 0 failed。
2026-09-13 22:28:42 +08:00
lzf_0626 468ae0bd47 fix(admin): 激活意图不再归档同 Agent 的其他意图(风控 4 意图曾只剩 1 条生效)
两个相关的缺陷,都属于**静默失效**型。

1. `admin_service._transition_intent_config`(真正的 bug)

   激活一个意图时按 `agent_type` 过滤旧 active 版本并归档,但唯一键是生成列
   `active_key = concat(agent_type, ':', intent_code)` —— 同一 `agent_type` 下
   **不同意图码本就允许并存**。于是激活 `general` 会把
   `risk_overview` / `risk_search` / `risk_evidence` 一并归档:风控运行期只剩 1 条
   active 意图,问"查看当前风险概览"被分到 `general`(confidence 0.5),
   而**没有任何报错**。该函数自己的 docstring 写的正是正确行为,实现与它不符。

2. `tools/publish_risk_agent_config.py`(使脚本无法自愈)

   按 `intent_code` 单键建 dict 收集现有意图,而列表**按 id 倒序**返回且同一意图码
   有多个版本,于是**旧版本覆盖新版本**;取到 v1 后再"版本 +1"算出的正是已被占用的
   v2,创建必然 409 IDEMPOTENCY_CONFLICT。现象是 4 条意图全部"创建失败"而库里
   其实都有,`tools/seed_demo_data.py` 第 7 步因此必然失败。

修复后实测:

- 库里 4 个风控意图全部 active;
- 问"查看当前风险概览" → `intent=risk_overview`、`confidence=1.0000`
  (修复前为 `general` / 0.5);
- `tools/seed_demo_data.py` 10/10 步完成、退出码 0(修复前第 7 步退出码 1)。

回归测试:`test_activating_one_intent_does_not_archive_sibling_intents`。
已确认把修复回退后该用例**确实失败**(`['beta'] != ['alpha', 'beta']`),
不是永远通过的空测试 —— 原有用例盖不住这个缺陷,因为它建的第二个意图始终停在 draft、
从未激活过。

门禁:ruff 通过;mypy 250 文件 0 错;unit+contract 1381 passed / 0 failed;
integration 104 passed;e2e 冒烟 40/40。
2026-09-13 22:08:04 +08:00
lzf_0626 bfd3964ef8 feat(demo): 演示数据一键准备、启动脚本与演示流程文档
交付"别人能自己把系统跑起来做演示"所需的四件东西:

- tools/seed_demo_data.py      演示数据一键准备:10 步按依赖排序
  (此前散在 10 个脚本里,没人知道该跑哪些、按什么顺序跑,且知识库那步
  根本没有脚本、靠手工调接口)
- tools/seed_knowledge_demo.py 知识库演示素材灌入,幂等(先删同 source_file 再灌)
- start.ps1                    启动 API + Worker,含依赖检查与**自动刷新行情**
- docs/44-演示流程.md           8 个主线场景的照读流程 + 排障表 + 账号/命令速查

两条硬约束同时写进了脚本和文档:

1. **Worker 必须常驻**:没有它客服对话一直停在 queued(前端只显示"超时")、
   新知识不进 Milvus 且**没有任何报错**。
2. **行情有效期仅 15 分钟**(`trade_service.MAX_QUOTE_AGE`):超时后所有委托
   直接 503「行情已过期」,而系统**没有自动刷新机制**。故 start.ps1 启动时刷一次,
   文档另给"演示中途 503 时补刷、无需重启服务"的处置方法(已实测)。

实测证据:
- start.ps1 在 8099 完整启动,`/docs` 与 `/portal/` 均 HTTP 200(测试进程已清理)
- `tools/e2e_smoke_test.py` → 40/40 通过
- `tools/seed_demo_data.py --dry-run` → 10 步全部正常列出
- 文档中的下单命令实跑:510300 成交价 4.579、金额 457.90、手续费 0.05
2026-09-13 21:56:27 +08:00
lzf_0626 f37fd922a1 fix(seed): 演示预警的状态口径与代码对齐,并补 closed_at
## 结论:risk_action_service 没问题,我先前是误判


esolve(完成结案)写全了 status="已结案" + closed_at + handle_result(risk_action_service.py:105-107),
exclude 同样(:67-71)。我报的"结案没写 closed_at"看到的那条 ALDEMO0003
其实是**种子预置样本**,不是我以为的自己创建的那条 —— 我的冒烟脚本用
ALDEMO{时分秒} 命名,**撞上了种子的 ALDEMO0001-0003**,"已存在则跳过"把创建挡掉,
随后处置全 409,串起来就像"结案没写时间"。

真正的问题在种子数据,两处:

1) **状态口径与代码不一致**:ALDEMO0003 写成 status="已关闭",而
   
isk_action_service 里**没有任何动作会产生这个状态**(状态机只有
   待处理 / 调查中 / 已排除 / 已结案)。演示时会被当成系统行为,排查时又查不到出处。
   按它的 close_reason 语义(低风险 + 已核验为正常交易),它就是**关闭误报** → 改为"已排除"。

2) **终态缺 closed_at**:那条有 close_reason 却没有 closed_at,自相矛盾;
   而且 
isk_repository 按 closed_at 统计"当日结案",缺了它会被漏掉。
   INSERT 补上该列,样本用 closed_at_offset_minutes 给值。

## 顺带修掉冒烟脚本的撞号

预警编号前缀 ALDEMO → E2E,注释里写明原因 —— 免得下次又操作到种子样本、
还把现象当成新 bug。

## 验证

- ALDEMO0003 现为 status=已排除 + closed_at 有值
- 库里所有终态预警(已排除/已结案)都带 closed_at;进行中的(调查中)不带 —— 口径一致
- 自己创建的两条走 resolve 后 status=已结案 且 closed_at/handle_result 都有值
- 冒烟重跑:40/40 通过,新造预警编号 E2E134351,处置闭环三段全 PASS

门禁:ruff 通过 / mypy 250 文件 0 错 / 单元+契约 1381 passed 2 skipped。
2026-09-13 21:44:08 +08:00
lzf_0626 2fcc4939e7 feat(tools): 新增全流程端到端冒烟脚本 e2e_smoke_test.py
把验收清单里"能用接口判定"的部分自动化:6 条线 40 项,打真实 HTTP、逐条打印 PASS/FAIL。
不是 pytest 的替代 —— 单测看代码正确性,它看"部署后底座是不是活的"。

覆盖:
  A 访客  令牌 → 提问 → 收到回答 → 未转人工
  B 客户  登录 → 看板/持仓/流水 → 真实下单成交 → 委托详情 → 会话与转人工
  C 风控  概览/队列/详情/八类证据/通知/扫描 → 处置闭环(确认→调查→结案)
  D 投顾  已发布方案     E 运营  场外邮件与邮箱状态
  F 管理员 角色/权限/身份/配置发布/模型端点/审计/转人工工单/画像候选

两个实现细节值得留意:
- **各端点 data 形状不统一**(角色列表裸数组、持仓在 holdings、成交明细在 transactions、
  资金流水在 entries、知识列表不套 data),所以这里用 listed(payload, *keys) 依次尝试,
  并在注释里写明"别猜字段名"—— 我自己写联调脚本时连续猜错三次。
- 库里没有"待处理"预警时**自动造一条**再验证处置闭环,否则该环节只能跳过
  (实测库里的预警常常都已被处置过,第一次跑就跳过了)。

新增 --read-only:跳过所有写操作,用于生产或不想动数据时。

实测:40/40 通过(含真实下单 201 已成交、风控处置闭环三段全 PASS)。

docs/40 补上"一键冒烟"一节。
2026-09-13 21:39:37 +08:00
lzf_0626 87a753f7b3 feat(sync): 给 fin_market_price 补真实行情同步链路(下单前置)
## 解决的问题

全流程验收时发现:**20 只产品里只有 2 只有行情,其余下单直接 503**;而且行情一旦过期
就**没有任何机制刷新**。根因是这张表此前**只有演示种子脚本写**,而它是下单的硬前置
(TradeService 要求 close_price>0、total_fund_shares>0、source_updated_at 在 MAX_QUOTE_AGE 内)。

## 数据源(踩了两个坑才选对)

1) **东财 push2 / push2his 两个行情域名在本环境一律连不上**
   (RemoteProtocolError: Server disconnected),而它的 api.fund(净值)与
   fundf10(概况/费率)**正常** —— 即东财只有"行情类"接口不可达。
   一开始按 K 线方案写完,20 只全失败;排查时还被我自己的 except 吞掉过异常。
2) 一度怀疑被限流,等 90 秒仍失败;做源可用性对照才确认是**域名级不可达**。

最终选**腾讯行情**(qt.gtimg.cn):一次请求可带多只,给今开/最高/最低/现价/
成交量(手)/成交额(万元)/**总市值(亿元)**,正好够写一行日行情。
K 线能力仍保留在适配器里(别的网络环境可能可达),注释写明本环境不可用。

## 实现

- EastmoneyFundAdapter.fetch_tencent_quotes:解析腾讯的位置约定格式,只取语义明确的
  字段并对价格做合理性校验;成交额万元→元、成交量按"手"(与东财 K 线口径一致,实测同为 9666652)。
- MarketPriceSyncService(新):编排 + 落库。总份额按
  「已有值 → 腾讯总市值推算 → 季度规模兜底」的优先级确定,source 标明是否含推算成分;
  三者都拿不到就**跳过该产品**,不编份额。
- 	ools/sync_market_prices.py(新):CLI,演示/验收前跑一次。

## 另一个坑:非交易日的假行情行

种子脚本用 	rade_date = date.today() 写行情,而 2026-09-13 是**周六**、真实行情时间是
09-11 16:14。TradeService 取 order_by(trade_date.desc()).limit(1),于是那两行假的"今天"
永远排在真实行情前面——表现是"同步成功了、下单还说行情过期"。已删除那两行。

## 验证

- 同步:请求 20 只、落库 19 行(160129 腾讯源没有该代码)
- 下单:510300 @4.579、510500 @7.611、159948 @3.693、511810 @100.012 全部 201 已成交
  —— 用的是**真实行情价**,不再是种子编的 4.5/6.2
- 风控处置闭环:新建 ALDEMO0003 后确认接收→进入调查→到达终态,
  ack_status=已确认、handler_id=9002

## 顺带发现的疑点(未改,留给风控线确认)

ALDEMO0003 到达终态时 status='已关闭' 但 **closed_at 与 handle_result 都是 NULL**;
对照 ALDEMO0001(已排除)则 closed_at 有值。结案时间是否应该写入,需业务侧确认。

门禁:ruff 通过 / mypy 250 文件 0 错 / 单元+契约 1380 passed 2 skipped。
2026-09-13 21:14:13 +08:00
lzf_0626 6f397ab513 feat(data): 510300 回填真实值并在产品页披露;511810 口径查清
按项目方决定处理三件事。

## 510300(沪深300ETF)

决定:保留 fund_manager = "南方基金" 作演示口径,**不**改成"华泰柏瑞"
(改了它会退出 MarketQuoteSyncService 的同步范围,投顾与知识条目也不该再算自家产品),
但净值与费率改成真实值,并在产品页显式披露它不是本公司产品。

- 回填(tools/backfill_product_snapshot.py --codes 510300 --apply --overwrite):
  current_nav 4.500000 → 4.5794、current_nav_at 同步为 2026-09-11 15:00:00、
  management_fee_rate 0.5000 → 0.15、custodian_fee_rate 0.1000 → 0.05
- 产品页披露:mock-data.js 给这只加 product_note,详情页新增提示块渲染它
  ("本产品为同指数参考产品,非本公司发行的基金,仅用于功能演示")。
  同时把 mock 里这只的净值/费率也对齐真实值,避免出现"页面显示 mock 值、
  库里是真实值"两套数字。
- 产品详情页的 css/js 版本参数提到 20260913-5,避免旧缓存。

## 511810(货币ETF南方)

之前说它"三个数量级不一致"是**我比错了接口**:QUOTE_API 返的是**场内交易价格**
(货币 ETF 约 100 元/份),而 current_nav 的口径是 DWJZ 单位净值。
查证结果:库里 0.2661 正是 09-11 的真实 DWJZ,快照时间也对得上 —— 不是脏数据,是旧值。
口径由其余 19 只的一致性确定(current_nav == DWJZ)。
要不要刷新到最新(0.2332)属运维节奏问题,**本次未执行**。

## 最小交易金额

按项目方口径:一律**以产品字段为准**(100.00 元),不用「1 手 × 净值」推算金额 ——
后者会与字段值不一致,客户会看到两套数字。docs/42 的 1.1 节据此重写。

## 工具改名

tools/backfill_product_fees.py → tools/backfill_product_snapshot.py:扩展为
净值 + 费率统一入口,新增 --codes 限定范围(避免顺手把全部快照刷成最新,
全量刷新是 ProductHistorySyncService / MarketQuoteSyncService 的职责)。
安全设计不变:默认 dry-run、默认只补 NULL(净值永不为空,所以刷净值需显式
--overwrite)、写入走单事务。

门禁:ruff 通过 / mypy 249 文件 0 错 / 单元+契约 1377 passed 2 skipped。
2026-09-13 19:47:10 +08:00
lzf_0626 02ba1befc0 feat(data): 取真实费率并回填 fin_product;发现 510300 的身份问题
## 取费率

项目原有的行情适配器只有 names/quotes/history,没有费率。东财也没有可用的 JSON 接口
(`FundArchivesDatas.aspx?type=jjfl` 实测正文只有一句 `var apidata=`),
所以给 EastmoneyFundAdapter 加了 fetch_fees:抓 f10 基金概况页
(fundf10.eastmoney.com/jbgk_{code}.html)去标签后匹配字段名,取管理费/托管费/
销售服务费/申赎费,并顺带带回基金全称。取不到的基金不进返回(而不是给全 None),
免得调用方把"没抓到"当成"该基金确实没有费率"。

实测 20/20 全部取到。

## 回填

新增 tools/backfill_product_fees.py,默认 dry-run、默认只补 NULL:
- 只补空值:写入 38 个字段(19 只 × 管理费/托管费),已执行
- 已有值一律不动 —— `510300` 库里的 0.5000/0.1000 保持原样,要改需显式 --overwrite
- 写入走单事务

回填前 grep 确认:`fin_product.management_fee_rate` / `custodian_fee_rate`
**目前没有任何业务代码读取**(advisor 域那两个 fee 字段属于另一张表
advisor_product_contract),所以这次回填不改变任何现有行为。
要让客户真的看到费率,还需要接口与前端读它 —— 那不在本次范围。

## 两个数据问题(都指向 510300)

1) **它不是南方基金的产品**。数据源返回的基金全称是
   「华泰柏瑞沪深300交易型开放式指数证券投资基金」,而库里 fund_manager 写「南方基金」。
   其余 19 只全称都是"南方…"开头,只有它例外。
   影响面不只是知识库:MarketQuoteSyncService 按 fund_manager='南方基金' 筛选同步对象,
   所以它会被当成自家产品一起同步、一起展示。

2) **它的费率是照抄的**。库里 0.5000/0.1000,真实 0.15/0.05;而 0.50/0.10 恰好是
   159329/159382/159511/588890 这四只的真实费率。结合它的净值快照时间是当天
   (其余 19 只停在 09-11)、净值取整数 4.500000,判断是演示用途的人为设定。

这两条都由用户决定怎么处理,本次只记录、不擅自改(510300 的 fund_manager、净值、费率
三处原值都没动)。

## 其他

- tools/fetch_live_quotes.py 增加费率核对段落与 --no-fees(费率要串行抓 45KB/只,较慢)
- docs/42 用真实费率重写 1.2 节与新增 4.3 真实费率清单,3.1 节标出 510300 的身份问题
- docs/42 的数据缺口表更新:费率不再是缺口

门禁:ruff 通过 / mypy 249 文件 0 错 / 单元+契约 1377 passed 2 skipped。
2026-09-13 19:39:16 +08:00
lzf_0626 131effcfae feat(tools): 新增 fetch_live_quotes 拉真实行情并核对库里快照
项目本来就对接了东方财富真实行情(app/infrastructure/fund_market_adapter.py:
push2.eastmoney.com 的行情接口 + api.fund.eastmoney.com 的历史净值接口),
下单链路取 quote_price/quote_source 走的也是它。之前做知识草稿时用的是
fin_product 表里的快照,没有去核对真实数据源,这次补上。

新增 tools/fetch_live_quotes.py:
- 用项目自带的 EastmoneyFundAdapter 取数,不自己拼 HTTP、不引第三方行情库
- 产品范围与 MarketQuoteSyncService.sync 一致(南方基金 / SSE,SZSE / 上市)
- 逐只对比真实净值与 fin_product.current_nav,只报告差异、**不写库**
  (刷新快照是 ProductHistorySyncService / MarketQuoteSyncService 的职责)

实测结果(2026-09-13,20 只全部取到真实数据):
- 18 只完全一致(库里 6 位小数、数据源 4 位,是同一笔数)
- 2 只不一致:
  · 510300 沪深300ETF:库里 4.500000,真实 4.5794。库里快照时间是今天 08:19:37,
    其余 19 只都停在 09-11 15:00:00 —— 说明这是被人为改过的演示值(4.5 凑"1 手 450 元")。
    需要定:知识用真实值还是演示值;若交易按库里的 4.5 成交而知识说 4.5794,两处口径会打架。
  · 511810 货币ETF南方:库里 0.266100,NAV_API 给 0.2332,而 QUOTE_API(场内实时价)
    给 100.012 —— 差三个数量级,说明两个接口对这只的含义不同,需人工核实口径。

docs/42 随之更新:
- 产品清单改用真实净值,并加上当日涨跌与净值日期,注明取数来源与复现命令
- 3.1 节的沪深300ETF 条目改用真实值 4.5794,并列出两处待确认口径
- 新增 4.2 节记录本次核对结果与两只不一致项的判断依据
2026-09-13 19:27:58 +08:00
lzf_0626 f72a545c39 refactor: 品牌统一为「南方财富」(项目方定)
背景:品牌名此前三处不一致 —— 后端 Agent 自称「奶龙基金」(customer_service_rules
的防诈骗/转人工话术、risk_agent 与 risk_analysis_service 的系统提示词)、前端全站
「南方财富」、闲聊提示词与知识素材「南方科技」。docs/36 已把它登记为"上报项目方后
待定",现按项目方决定统一为「南方财富」。

改动:
- app/core/customer_service_rules.py:P0 防诈骗话术与 P2 转人工话术里的品牌名
- app/service/agent/implementations/customer_service.py:COMPANY 常量,
  以及那处引用实测样本的注释(改为不绑定具体品牌名,免得下次改名又过时)
- app/service/agent/implementations/risk_agent.py:docstring、自我介绍、system prompt
- app/service/risk_analysis_service.py:SYSTEM_PROMPT
- app/worker/risk_scan_scheduler.py:--help 描述
- app/static/index.html(旧联调页 4 处)、portal/employee-risk/dashboard/index.html
- tests/unit/api/test_customer_service_test_page.py:同步断言(它断言的正是页面里的品牌名)
- tools/publish_chitchat_prompt.py:SYSTEM_PROMPT 改品牌;并修掉"存在即跳过"的检查
  —— 原来只判当前版本有没有这一行,于是改了文案也发不出去(脚本打印"无需发布"直接
  退出),没有任何提示。改为比对 system_prompt/user_prompt_template 内容。

闲聊提示词已重发为 release 308 / v5,生效内容为"你是南方财富的智能客服助手…"。

刻意未动:
- fin_product.fund_manager = "南方基金" —— 它被 market_quote_sync_service 与
  product_history_sync_service 当过滤条件使用,改名会让同步链路查不到产品
- knowledge_search_service.py 注释里引用的知识库实际标题「南方科技有限公司…」
- docs/客服docs 下的历史素材与 docs/ 下的过程记录(属历史留痕)

⚠️ Milvus 里的知识条目仍写「奶龙基金」(RAG-*/NF-*)与「南方科技」(PROD-*),
属知识数据,需重灌才能统一;本轮不动。

同时:
- docs/40 把品牌条目标为已处理,并补上知识库缺口的现状
- 新增 docs/42-场内基金知识条目草稿.md:按 fin_product 的 20 只产品生成,
  含通用交易规则与产品清单;费率等缺失字段一律标"以交易页面为准",未编造数字。
  **该文件是草稿,未入库**,待审核后走 POST /api/v1/knowledge/documents 灌库。
2026-09-13 19:17:56 +08:00
lzf_0626 d4a895c643 fix: 修好访客客服浮窗、投顾工作台数据口径,恢复访客页来源声明
组员这次提交的两套新页面方向都对,但各有一处"接不上"的地方,这里补齐。

1) 访客客服浮窗(此前一问即失败)
   客服 Agent 让访客走 query_knowledge(访客令牌的角色是 visitor、权限只有
   agent:run + knowledge:query),但发布配置里三个知识意图只发了 search_knowledge,
   于是 ToolExecutor 直接抛 ForbiddenAgentError,而客服代码对白名单失败是
   「必须冒泡」的 —— 访客拿不到任何回答,登录客户侧却完全正常。
   发布脚本的知识类意图改为同时发 search_knowledge 与 query_knowledge:
   访客走前者、客户走后者,缺任一条对应人群就失败关闭。
   (suitability_check 不发 query_knowledge:访客意图白名单不含它,访客到不了。)

2) 发布脚本会静默丢提示词
   旧写法只查 platform_config_item 就当作"继承",而 config_release 是整版本替换
   语义,新版本没带上的行等于被删除 —— 实际把 customer_service_chitchat 提示词
   漏在了旧版本里(admin 端只在激活时打一句 stderr 警告)。
   改为走 ConfigReleaseService.effective_snapshot() 读全三张受管表,补上提示词
   搬运(version 重分配、带上 input_schema/output_schema),并在激活后硬校验
   配置项与提示词条数,条数不符即失败退出。
   (model_routing_rule 本环境为空;不为空则直接中止,不假装支持。)
   丢失的那条提示词已按原文恢复,active 版本现为 9 条配置项 + 1 条提示词。

3) 投顾工作台永远为空
   published() 取的是 customer_id == 自己 user_id,而投顾是员工账号、不可能是
   客户;且只认 advisor_recommendation_plan + approved,而投顾交付的主产物是
   investment_goal_book,发布后状态是 published。三重不匹配下页面永远显示空态。
   改为按「本人 + sys_customer_assignment 里名下归属客户」过滤(不用 data_scope:
   投顾因持有 all 级权限会把整个身份的 scope 抬到 all,那会放开到全部客户),
   并覆盖两类 content_type 与两种已发布取值。
   实测:投顾可见归属客户 9001 的方案书,客户仍只见自己的,风控仍 403。

4) 访客页把"演示数据"声明删了但假数据还在
   mock-data.js 的 MOCK_SOURCE_NOTICE 与两个页面的 data-source-notice 区块被删除,
   而 MOCK_PRODUCTS/MOCK_RANKING_CHANGE 仍在渲染(详情页含历史净值曲线)。
   恢复声明常量、页面区块与样式,并给 products/product-detail 的 link 与 script
   加上版本参数 —— 此前没有版本号,浏览器会命中旧缓存,改动看不见。

其他:投顾页显示交付物类型与客户编号(后端新返回的字段),README 补上投顾页
数据口径、访客/客户两条检索工具的差别,以及"渲染 mock 必须带来源声明"的约定。
2026-09-13 18:54:09 +08:00
lzf_0626 5b6eaddc4f docs: AGENTS.md 补正式前端口径与最新测试基线;portal.py 标注为调试工具
- 新增前端一节:app/static/portal/ 挂载在 /portal/,四套页面,端点表在 common/api-client.js,
  启动用 uvicorn app.main:app(模块级变量是 app 不是 application)
- 测试基线更新为 2026-09-13 实测:mypy 249 文件 0 错 / 单元+契约 1376 passed / 集成 104 passed
- tools/portal.py 文件头标注降级为跨角色联调工具
2026-09-13 16:20:39 +08:00
lzf_0626 f4eaa2f631 fix(seed): 客户种子改为已开户;portal.py 标注为调试工具
- seed_test_rbac.py 把 fund_account_status 硬编码成 'closed',与 create_test_user.py
  的 {customer: 已开户, employee: closed} 口径不一致,导致种子建的客户永远未开户
- 影响:GET /users/me/account/dashboard、/users/me/cash-ledger 恒返回 404,
  正式前端 T001/T009 两个页面打不开(端点本身是好的)
- 改为按 user_type 取状态;配合 tools/seed_sim_account_demo.py 开虚拟资金账户后
  T001/T006/T007/T009 全部 200
- tools/portal.py:正式前端已在 app/static/portal/,本工具降级为跨角色联调工具
2026-09-13 16:19:54 +08:00
张胜宇 e38ece32bf 前端提交 2026-09-13 15:56:54 +08:00
zhangshy 4f495c6048 修正风控前端文档重命名后的引用 2026-09-12 16:58:31 +08:00
lzf_0626 bbe575f38f fix(portal): 模态打不开时立刻 resolve,避免 await 永久挂起(表现为点击没反应)
- askConfirm/askText 在 openModal 失败时立即 resolve(false)/resolve(null):
  此前 Promise 永不 resolve,await 会一直挂着,用户看到的就是点了没反应
- toast/openModal 在容器缺失时给出明确提示而不是静默失败
- 顶栏显示页面构建时间(取 portal.py 修改时间),用于一眼确认加载的不是缓存旧页
2026-09-12 16:41:26 +08:00
lzf_0626 6155f4589e feat(portal): 按风控前端合并约束重写风控工作台
- 概览五指标(总量/等级/待处理/超时/重点预警)
- 预警队列:每页 5 条、风险等级优先、8 项筛选、游标分页、稳定行高与省略号
- 预警详情弹窗:23 个 alert 字段 + 客户信息 + 五个处置动作
- 处置:确认接收(二次确认)/进入调查/关闭误报(必填理由)/结案(必填结论)/升级(必填原因)
- 八类证据(customers/products/transactions/capital_flows/holdings/login_records/
  alerts/notifications)+ 五项筛选;注意是 holdings 不是 positions
- 通知记录、日报 SSE 流式生成 + 内容编辑 + 多邮箱发送
- 新增 Toast + 模态框,替换全部 17 处原生 alert(17- 文档明令禁止原生弹窗)
- 风险等级筛选值改用 高/中/低(预警对象用「高」,概览 levels 用「高风险」,口径不一致)
2026-09-12 16:30:54 +08:00
lzf_0626 6e916aab03 Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop 2026-09-12 16:08:39 +08:00
lzf_0626 76923d7e8b fix(portal): 补齐写操作的必填 body(风控升级/解决、场外六个写接口)
- 风控:escalations 要 reason、resolutions 要 resolution(字段名不同),
  且真实顺序是 先确认接收 才能升级/解决(否则 409 请先确认接收预警)
- 场外:六个写接口都必填 operator_id(防伪校验,须等于当前登录用户),
  confirmations 还要 decision(中文枚举)、notifications 还要 notification_type、
  recalculate 要 fund_code+application_date;门户自动带当前 user_id
- 实测:recalculate 200 code=0;不传 operator_id 必得 422(证明该字段必须)
- 清单 §3/§4 更新为实测结果并列出各写接口的必填字段表
2026-09-12 16:08:38 +08:00
ZSY_merge e25296f30a merge: integrate origin/qyqy_develop (552055ee) 2026-09-12 16:05:24 +08:00
lzf_0626 552055ee2d fix(portal): 修风控预警列表 422(limit 上限是 5,改为不传);运营邮件改用 page/page_size;审计 limit 钳制到 1-100
- risk/alerts 传 limit=20 会 422 query.limit<=5,导致整张表格渲染不出来、
  行内确认/升级/解决按钮随之消失 —— 这正是验收清单 3-3/3-6~3-9 看不到功能的原因
- 实测列表返回 2 条预警;处置接口有状态前置条件(409 = 当前状态不允许)
- 清单 3-3/3-6 更新为实测结果并写明 limit 上限与状态机约束
2026-09-12 16:03:22 +08:00
ZSY_merge_bot 4c651ccce1 merge: integrate latest origin/qyqy_develop (frontend acceptance checklist + portal flow wizard) 2026-09-12 15:58:08 +08:00
张胜宇 ebc3fe4cbe feat(§T): 账户看板 + 场内模拟交易 9 端点(用户自助首版)
新增 §T 用户自助段(docs/05 §19 新号段 7 个 = A×40/C×7/K×4/M×4/O×3/R×4/T×9):

- T001 GET /api/v1/users/me/account/dashboard  — 账户/资金/持仓/盈亏汇总

- T002 POST /api/v1/users/me/orders  — 委托提交(首版 market 立即全额成交)

- T003 / T004 / T005  委托列表/详情/撤单

- T006 GET /api/v1/users/me/holdings  — 持仓列表(含市值/盈亏/当日盈亏)

- T007 / T008  成交记录列表/详情

- T009 GET /api/v1/users/me/cash-ledger  — 资金账本

要点(与 docs/00 §6.6 一致):

- 首版市价委托立即全额成交,不实现撮合队列/部分成交;T005 撤单首版对任何在场委托返回 ORDER_NOT_CANCELLABLE (409)

- 价格来源复用 base FundQuoteService;service 层不二次封装(满足 AGENTS 第 2 条)

- 首版风控 3 条硬性:产品可交易、客户适当性、持仓比例上限(fin_market_price 缺失或过期 → 拒绝买入)

- 数据库零修改:10 张 fin_* 表全部 docs/00 既定,本批 PR 改列类型与可空性均 0;底座实际偏差(id 无 AUTO_INCREMENT、所谓'生成列'是普通 NOT NULL)由 service _next_id / 业务派生值补偿

注册 API:9 端点均注册进 app.main;user=9001(cust)'s id 写账

权限码(tools/seed_test_rbac.py 同步登记 + CUSTOMER 全量):

  9047 account:read:self

  9048 trade:order:create

  9049 trade:order:read

  9050 trade:order:cancel

  9051 holding:read:self

  9052 trade:txn:read

错误码(app/core/errors.py + docs/05 §3.6 + tests/unit/core/test_errors.py DOCUMENTED 三方同步):

  404 ACCOUNT_NOT_FOUND / ORDER_NOT_FOUND

  409 ORDER_NOT_CANCELLABLE

  422 INSUFFICIENT_FUNDS / INSUFFICIENT_HOLDING / HOLDING_RATIO_EXCEEDED / SUITABILITY_MISMATCH / PRODUCT_NOT_TRADABLE

  503 FUND_QUOTE_UNAVAILABLE(可重试)

新增:app/api/controllers/trading.py / app/api/schemas/trading.py / app/service/trade_service.py / tools/seed_sim_account_demo.py / tests/unit/service/test_trade_service.py(unit×8) / tests/contract/test_trading_endpoint_contract.py(contract×11)

修改:app/main.py(挂载 controller) / app/core/errors.py(10 新异常类) / tools/seed_test_rbac.py / docs/05-接口文档.md(§19 T001-T009 + §3.6 9 新码) / tests/unit/core/test_errors.py(DOCUMENTED 同步)

门禁:pytest tests/unit tests/contract 1313 passed (+19 新增) / ruff all clean / 三道守卫全过
2026-09-12 15:50:37 +08:00
lzf_0626 2525fbdad9 feat(portal): 推广材料补成完整流程向导,并改为按 body.code 判成败
- 投顾视图:创建 -> 补结构化输入(含七项费率) -> 生成 -> 投递 -> 查询
  生成后自动把 material_version_id 填进投递框
- 管理员视图:新增推广材料审核(promotion:review 只给管理员,职责分离)
- 统一结果渲染改看 body.code:本平台业务失败也返回 HTTP 200
  (生成失败是 HTTP 200 + code=422 + '材料内容未通过合规校验')
- 输入骨架刻意不预填业绩(performance_info 有 show_* 开关可关),
  费率填占位文本以便跑通流程;真实材料必须换成真实值
2026-09-12 15:45:09 +08:00
lzf_0626 615032ab00 fix: 修 POST /api/v1/conversations 的 MissingGreenlet(会话建不出来导致转人工 404)
- public_platform_service: 创建 ConversationSession 时显式赋值四个 server_default 时间列,
  否则 flush() 后需回读数据库生成值,在 async session 里以同步属性访问触发
  MissingGreenlet,接口 500,连带转人工一直报会话不存在
- portal: 客服改用 C001 真实建会话;转人工改传 reason_code/reason_detail(原 reason 属额外字段 422)
- portal: 投顾查客户投资目标走 customers/{id} 变体
- seed/grant: 补 investment-goal:{read,write,confirm}:customer 三个动态拼出的权限码
  (data_scope=own_customers,投顾只看名下客户)
2026-09-12 15:37:36 +08:00
lzf_0626 bfd622b592 feat: 统一登录门户(按角色分流五套工作台)+ 补齐权限覆盖缺口
- tools/portal.py:客户/风控/运营/管理员/投顾五个工作台,走真实登录与真实接口
- 新增 6 个此前从未建过的权限码(promotion:* 4 个 / financial:nl2sql:read / probe:read),
  它们让推广材料与金融 NL2SQL 两条线对所有角色都是 403
- 投顾权限 10 → 25 项:补投资目标创建确认、agent:run、行情、知识检索、客户画像、推广材料
- 运营补 financial:nl2sql:read;create_test_user.py 不再硬编码角色 id(改按 role_code 查库)
- portal 的写请求补 Idempotency-Key 头(漏了会被 AGENT_INPUT_INVALID 拒)
- 新增 tools/check_permission_coverage.py 做权限对账
2026-09-12 15:21:34 +08:00
lzf_0626 c8d5fe11dc feat: 新增平台功能测试台(自动覆盖 docs/05 §19 全部端点 + 一键只读冒烟 + 按身份测权限) 2026-09-12 14:57:17 +08:00
zhangshy 5634fdc023 完善风控登录权限和模型能力配置 2026-09-12 14:43:05 +08:00
Windows 026fec7514 Merge remote-tracking branch 'origin/qyqy_develop' into lzl_qyqy_integration 2026-09-12 14:41:13 +08:00
Windows 9c6f839a6e feat: add advisor demo environment bootstrap 2026-09-12 14:40:53 +08:00
lzf_0626 c8cdc06e80 Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop 2026-09-12 14:18:06 +08:00
lzf_0626 ffbcc229c2 fix: 删掉 NL 合并后残留的未使用变量 role_ids(ruff F841) 2026-09-12 14:17:42 +08:00
Windows 050eedac68 Merge remote-tracking branch 'origin/qyqy_develop' into lzl_qyqy_integration 2026-09-12 14:16:27 +08:00
Windows 54fadb3a99 Merge branch 'qyqy_develop' of http://47.106.207.27:3000/AI260626/group_fqcd_jr into lzl_qyqy_integration
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-09-12 14:15:43 +08:00
wangjianlong_0626 f7b35006ff Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague 2026-09-12 14:06:18 +08:00