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
|
c4f4c33fd6
|
记忆召回补一道能力码闸门:归属关系不等于授权
上一版把员工召回改成"按 sys_customer_assignment 归属"时,只看了**归属关系**,
没校验**能力码** —— 这留下一个我自己带出来的口子:
- 平台读他人画像/记忆的正式路径(customer_profile_service、public_platform_service)
校验的是 `memory:read:customer`(sys_permission id=9010,data_scope='own_customers')
+ `customer_ids`;
- 而召回是**另一条**把客户记忆送进模型上下文的路径,只按归属放行,等于让
"有归属行但没有该权限"的账号(例如 operator:只有 financial:nl2sql:read
+ offsite:write)凭空获得读他人客户记忆的能力 —— 它上一版之前是读不到的。
现改为**两个条件都满足才放行**(缺一即失败关闭,并在日志里分开点名"缺能力码"
与"归属未维护",避免运维在错误的表上找问题):
① `memory:read:customer` 在 context.permissions 里;
② 客户落在 context.customer_ids(sys_customer_assignment 生效行)内。
演示账号不受影响:9002(risk_operator)、9020(advisor)、9003(admin) 三个角色都持有该权限码,
真实身份链路复验 9020 仍是"修复前 0 条 → 修复后 2 条"。
测试:tests/unit/core/test_memory_scope.py +3 条(缺能力码读不到/能力码与归属是"与"关系/
现有用例补上能力码),tests/unit/service/test_agent_governance.py 同步。
`pytest tests/unit tests/contract` → 1448 passed, 2 skipped, 1 failed
(唯一失败仍是组员正在改的投顾页面)。
|
2026-09-14 21:40:05 +08:00 |
|
lzf_0626
|
89b15f32cb
|
忽略 .env 自动备份:它含真实密钥却不在 .gitignore 里,git add -A 会把它带进版本库
|
2026-09-14 21:33:48 +08:00 |
|
lzf_0626
|
9eebf9627f
|
记忆召回:按 sys_customer_assignment 归属定范围,不再把员工号当客户号
## 修的是什么
`governance.recall()` 把 `int(context.user_id)` 当客户号用。后果有两个,
方向相反但都致命:
1. **员工身份(风控/投顾/运营/管理员/system)恒空** —— 员工不是客户,
那是个不存在的客户号;日志只说 "empty",看不出是"设计如此"还是"记忆坏了"。
2. **越权陷阱** —— 员工号与客户号同号段(演示数据里客户 9001-9020、
员工 9002/9020 并存)。`int(user_id)` 一旦与真实客户号重合,就会把
**陌生客户的长期记忆读进来并注入提示词**,且不报错、看起来正常。
同一个问题在代码里还有另外两处**各自判断**、口径互不一致:
`BaseAgent.recall_memory()` 要求"每条记忆 customer_id == context.user_id"
(否则抛"越过客户范围"),`review_output()` 的引用校验只认同一条件。
## 怎么修的
新增 `app/core/memory_scope.py` 作为**唯一判定口径**,三处共用:
- 客户身份(customer / authenticated_user):**只读自己**,分配表里有别行也不读别人;
- 员工身份:**只读 `sys_customer_assignment` 分配给自己**的客户
(`context.customer_ids`,由 `IdentityRepository.load_context()` 读入);
归属未维护 ⇒ **失败关闭**,并在日志里点名"归属未维护",与"库里确实没有记忆"区分开;
- 访客:无(上游已拦)。
细节约定:
- 归属客户按客户号**升序**召回、单次上限 `MAX_RECALL_CUSTOMERS=10`
—— 升序是为了确定性(同一身份每次取同一批,不随数据库返回顺序漂移),
上限是为了别把成百上千条他人记忆塞进一个提示词;
- 跨客户合并后按置信度降序、`(客户号, uuid)` 兜底排序,最多 10 条;
- 员工同时持有多个归属客户的记忆时,`memory_context_text()` **逐行标注客户号**
并把提示词改成"多个客户的长期事实" —— 否则模型会把 A 客户的事实当成 B 客户的。
单一客户时保持原格式(客户身份的提示词与改动前逐字相同);
- 引用校验与范围守卫都改用同一口径:员工引用**归属客户**的记忆不再被判成伪造引用;
引用**非归属客户**的记忆即便被塞进 memories 也照样拦下。
## 验证(真实身份链路 + 生产召回装配)
`IdentityRepository.load_context` → `PlatformGovernance.recall`(含 Milvus 语义通道):
- 身份展开:roles=('advisor',)、customer_ids=('9001',)(sys_customer_assignment
里唯一那行 9020→9001)、可读范围 (9001,);
- **修复前** `recall(int(user_id)=9020)` → **0 条**;
- **修复后** `recall(按归属)` → **2 条**(客户9001:进取型 / 约三年);
- 边界:客户身份 9001 可读范围 (9001,);无归属员工 9002 = ()(失败关闭,
且**没有**把 9002 当客户号);未分配时的 9020 = ()。
测试:`pytest tests/unit tests/contract` → **1445 passed, 2 skipped, 1 failed**
(1432 + 新增 13;唯一失败是组员正在改的投顾页面,与记忆链路无关)。
新增用例:`tests/unit/core/test_memory_scope.py`(8 条,含"员工号不得被当成客户号"
的反例断言)、`tests/unit/service/test_agent_governance.py`(+5 条:归属召回/
无归属失败关闭且不碰数据库/客户只读自己/引用校验/越界守卫)。
## 遗留(已在 AGENTS.md 与文档里写明,未自行实施)
风控扫描这条线**仍读不到记忆**:它是唯一消费召回内容的地方
(`risk_agent.py:224`),而扫描上下文是 user_id="0"/roles=("system",) 且无归属行。
根因是**顺序问题**:召回发生在 handle() 之前,上下文里没有"本次目标客户"这个概念。
出路有两条:① 给风控专员补 sys_customer_assignment 行(运维动作,立即可用);
② 在 RequestContext 加显式的 target_customer_id 并校验它落在归属集合内
(推荐,但属跨线协议改动,等确认)。
文档:docs/演示用/记忆召回恒空-根因与修复-2026-09-14.md 新增 §五(含 §5.4 遗留说明)、
AGENTS.md 新增"记忆可读范围只有一个判定口径"易错点,并按 2026-09-14 复测更新测试基线。
|
2026-09-14 21:33:16 +08:00 |
|
lzf_0626
|
0c642133d4
|
修复长期记忆向量链路:投影入队 + 集合名三侧同源 + 召回按客户过滤
语义召回"恒空"的根因分四层,本提交修掉投递层与读取层(另两层——重试计数
门禁、episode 不投 rebuild 事件——已在前两个提交修复)。
R2 投递层:dispatch_profile_rebuild 只做图投影,没有任何 Milvus 投递,
长期记忆的向量从未被写入过(实测客户 9001 在 memory_sync_outbox 里 0 行)。
新增 _enqueue_memory_vector_projection(),在画像重建后投
MemorySyncOutbox(target_store="milvus")。走事件而不是同步写,是为了拿
Outbox 的重试/退避/死信,且不把 embedding 的网络等待拖进事务。
⚠️ payload 必须带 version(正整数):适配器 _coerce_profile_version 缺它
直接抛 ValueError(实测踩到,事件立刻 failed)。
R4 读取层:写的集合与读的集合不是同一个 ——
写(MilvusProfileProjection)用 "user_long_term_memory_v1",
读(bootstrap.get_vector_memory_adapter)与删(projection_cleanup_service)
却用 settings.milvus_collection = "jr_memory",而该集合从未被创建。
⇒ 召回:MilvusClient 构造不校验集合存在,适配器"构造成功"但每次 search
抛异常被 VectorMemoryAdapter 吞成 degraded → 召回恒
degraded_reasons=('milvus_unavailable',)、向量命中恒 0 条;
⇒ 清理:jr_memory 不在集合列表 → 走 vector_collection_absent 分支 →
报清理成功但一个向量都没删,陈旧向量永久留存。
修法:PROFILE_COLLECTION 成为唯一常量,读/删两侧直接引用它;
并删除 Settings.milvus_collection 配置项、清掉 .env.example 的
MILVUS_COLLECTION —— 写侧从来没读过它,一个只在契约一侧生效的配置项
比没有配置项更危险(Settings 的 extra="ignore" 会让其他环境残留的该
变量被安全忽略)。
顺带:
- 语义检索把客户过滤下推到 Milvus(filter="customer_id == N")。此前不带
过滤,别家客户的命中会白占 limit 名额,稀释本客户的召回条数。
- upsert 在 sources 为空时先返回,不再无条件 load_collection ——
"本来就没有可写内容"不该被记成投递失败(10001/10002 那两条事件即如此
重试 5 次进死信)。
回归守卫:tests/unit/infrastructure/test_memory_vector_collection_consistency.py
断言读侧与删侧用的都是 PROFILE_COLLECTION,且被删掉的配置项不得回归。
这个缺陷能活下来,正是因为两侧单测全绿而接缝无人守。
验证(走生产装配、进程内调用,未重启你正在跑的 API 窗口):
读侧集合打印 user_long_term_memory_v1(修前为 jr_memory);
召回 degraded=False / reasons=() / sources 含 milvus,且排序随 query 语义
变化(投资期限→horizon 0.288 > risk_level 0.201;风险偏好→risk_level 0.287);
query="进取型" 双通道合并且 vector_score=0.9987;query=None 走 mysql 全量。
pytest tests/unit tests/contract → 1432 passed, 2 skipped, 1 failed
(唯一失败是同事正在改的投顾页面,与记忆链路无关)。
文档:docs/演示用/记忆召回恒空-根因与修复-2026-09-14.md(新增,四层根因+证据)、
docs/37-记忆投影链路实现说明.md(补集合名三侧契约)。
|
2026-09-14 21:26:03 +08:00 |
|
lzf_0626
|
3a1065ca1e
|
修复片段链路:重复聚合拖垮重试预算 + 从不投画像重建事件
## 结论先说:真正的根因和最初两个假设都不是同一个
你原来的判断是「第二条召回恒空、第三条传导断链」,并猜第三条是
「`record_evidence` 返回 False 时该不该补写重建事件」。查完库发现:
1. **`record_evidence` 返回 False 时数据零净变化**(幂等命中直接 return;
并发冲突把刚加的计数减回来),所以那个判断点解释不了画像停更;
2. **`memory_extraction_worker` 是有写重建事件的**(`if recorded:`),
233 条 `profile.rebuild_requested` 全 published;
3. 真正断在两处,都在 **`episode_worker`** 这条片段链路上。
## 根因一:`_touch_retry` 把「重复聚合」记成「抽取失败」(P0,已修)
`episode_worker._persist` 在 `content_hash` 命中(同一段会话被重复聚合)时调
`_touch_retry` 做 `retry_count += 1`。但 `retry_count` 的语义是**抽取失败次数**
(由 `_mark_failed` 累加),而"分片逻辑每轮重新看到同一段会话"根本不是失败。
后果是实测出来的:
episodes 待提取片段:retry_count=1405(40 条,全是客户 9001)
consume_pending 逐条件筛:
仅 status in (待提取,失败) -> 40
+ retry_count < max_retry(3) -> 0 ← 一个都不剩
+ promoted_to_ltm IS FALSE -> 40
这 40 条片段**永远不可能被选中** ⇒ 新记忆进不来 ⇒ 画像停在旧值。
(另外还观察到一次运行里它从 1405 涨到 1407 —— 常驻 Worker 每轮都在继续推高。)
**修法**:重复聚合不再触碰 `retry_count`,连 `flush` 都不做(内容没变就只是看到)。
## 根因二:片段链路从不投「画像重建」事件(已修)
`memory_extraction_worker` 写完证据会投 `profile.rebuild_requested`;
而 `episode_worker` 写完证据直接 `_mark_done` 就结束了 —— **完全没有这一步**。
所以即使片段被成功抽取,画像也不会重建。
**修法**:`episode_worker` 也捕获 `recorded` 并在为真时投同样的事件
(`trigger="episode_extraction"`)。只在 `recorded=True` 时投:幂等命中时证据与计数
都没有净变化,投一次是白跑。
## 数据修复
代码修好不会让已写进库的脏计数自己恢复 —— 那 40 条片段仍然超预算。
用 `tools` 级别的临时脚本把**确实被污染的行**重置(条件收紧为三者交集):
extraction_status IN ('待提取','失败') AND promoted_to_ltm = 0 AND retry_count >= 3
-> 重置 40 行;之后可被 consume_pending 选中的片段从 0 恢复到 40
## 实测
- **重试预算修复**:`consume_episodes` 从"领不到任何片段"变为能领到;
分两批消费完 40 条(全部 `no_fact` —— 那些片段摘要里确实没有用户陈述,
属正常结果),待提取 40 → 0
- **重建事件修复**:那 40 条全是 `no_fact`,走不到 `if recorded:`,所以**
实测不到**。为不留下"改了但没验证",另造了一条含用户陈述的片段:
consume_episodes -> extracted=[191]
9001 的 rebuild 事件 3 -> 4(增量 1)
memory_evidence 8 -> 9
另外注意到基线在我造数据前已经由 1 涨到 3 —— 说明常驻 Worker 也在这期间
投过事件,修复在真实链路里同样生效。
- `pytest tests/unit tests/contract` -> 1428 passed / 2 skipped / 1 failed
(剩下的 1 个是投顾工作台页面被替换所致,与本次无关)
## 测试
两个既有用例断言的正是被修掉的旧行为,已更新,并把第二个改造成**防回归守卫**
`test_repeated_aggregation_never_bumps_retry_count` —— 它守着
"`retry_count` 被重复聚合推高到 `max_retry` 之上会导致片段永久滞留"这个 P0。
`tests/unit/worker/test_episode_worker.py` 16 passed。
## 未处理
「召回恒空」(员工身份下 `recall` 取的是自己作为客户的记忆)**本次没动**。
它需要给 `AgentRequest` 加 `target_customer_id` 并配套越权校验,属接口契约变更;
排查报告给的建议是保持现状、员工侧走 `query_customer_profile` 工具。
要按"支持目标客户维度"做,请确认,我再单独一提交。
|
2026-09-14 21:09:03 +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
|
e85e958389
|
排障表补一条:所有页面一起卡 → Milvus 抖动 + 同步调用阻塞事件循环
## 为什么补这一条
把审查报告的 P1–P3 共 45 项按 8 个演示场景筛完,**"会砸演示"的是 0 条**
(P0 四条已全修)。剩下的分两类:37 项演示路径根本走不到,8 项是"边缘"
(平时无感、特定条件才暴露)。
这 8 项里只有 **P1-2** 属于"一处慢、全平台卡":检索层用的是**同步** Milvus 客户端,
单次 `search` 卡 3 秒会让整个 API 进程停摆 3 秒,连 Worker 心跳一起停。
它同时也是**最容易被误判**的一个 —— 现象是"客服、风控、看板一起变慢",
而排障表原有的 6 条全是**单点**故障(503 行情 / Worker 没起 / 浏览器缓存 /
知识库没同步 / MySQL 没起 / 风控样本用完),**没有一条对得上"全场同时变慢"**。
没有这一条,现场会往"服务挂了"的方向查,而它其实是外部依赖抖动、不需要改代码、
也不需要重启。
所以这条的价值不在改代码,而在**把一个边缘隐患变成一眼能认出的现象**:
演示版本选择不修 P1-2(Milvus 正常时完全无感),代价就是必须知道它长什么样。
## 依据(都是会话内实际核过的)
- 客服链路确认:`app/service/knowledge_tool.py:29` 的 `search_knowledge` 工具
→ `get_knowledge_search_service().search(...)`;
而 `knowledge_search_service.py:207-213 / 224 / 229` 调的是**同步** `MilvusClient`
(`app/infrastructure/vector_memory.py` 的 `def search(...)`,非 `async def`)。
- 对照:项目在其它 10+ 处已用 `asyncio.to_thread`(如
`app/infrastructure/fund_market_adapter.py:273/382`),说明异步规范已建立,
这 4 处是遗漏 —— 但**不是演示阻塞项**,本次不改。
- "风控日报会不会砸"(P1-6,报告标了【确定】)也核过了:`_distribution` 的
`counts` 键是字段**值**,两个调用点用的 `risk_level`(中文高/中/低)与
`alert_type` 都是字符串,`str(key) == key` 恒成立,**触发不了** —— 场景 6 安全。
## 改动范围
只改 `docs/44-演示流程.md` 的 §4 排障表,**新增一行**,未动其它内容。
|
2026-09-14 20:41:34 +08:00 |
|
lzf_0626
|
36c7a9d8d2
|
文档:审查报告入库 + 全量校对补注
## 新入库(`docs/演示用/`)
- `代码库全面审查报告-2026-09-14.md`
- `代码修改方案-2026-09-14.md`
- `记忆系统排查报告-2026-09-14.md`
- `记忆系统修复文档-2026-09-14.md`
- `文档一致性审计报告-2026-09-14.md`
- `多Worker接入方案-2026-09-14.md`
## 全量校对(32 个既有文档 + `AGENTS.md`)
跨 39 个文件、**1125 insertions / 148 deletions**。
⚠️ **这批改动同样不是本次会话写的**。我抽样核对过性质:是**实质内容补充**而不是
格式/换行转换。例如 `docs/44-演示流程.md` 新增两条"2026-09-14 补注":
- `启动金融Agent平台.bat` 只在**桌面**上,仓库里只有 `启动平台.bat` 这一份
(两份由同一个 `tools/make_launcher_bat.py` 产出,改完 `start.ps1` 重跑它一起更新);
- `advisor_t`(9020) 与 `offsite_t`(9006) **不在 `tools/seed_test_rbac.py` 的演示用户里**
(那里只有 `cust_t`/`risk_t`/`admin_t`/`review_t` 四个),由 `grant_*.py` 系列创建,
**重跑种子不会重建它们** —— 换机器时这两个账号登录失败,要先查 `sys_user` 有没有这两行,
而不是查密码。
这两条都是对的地方,与我这一路踩到的现象一致(我确实用到了 `advisor_t`/`offsite_t`)。
**我没有逐字审阅全部 39 个文件**,只抽样确认了改动性质与规模。若其中有需要复核的段落,
请指明文件,我逐处核对。
|
2026-09-14 20:36:00 +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 |
|
lzf_0626
|
6b2e2edcda
|
P0-2:恢复基线要求的 AUTO_INCREMENT,消除手工发号的并发主键冲突
## 问题
`trade_service._next_id` 用 `SELECT MAX(id)+1` 发主键。两个事务读到同一个 MAX、
算出同一个 id,后写的那笔 `flush()` 撞 `Duplicate entry ... for key 'PRIMARY'`
-> 该客户下单直接 **500**。`submit_order` 一次要发 **3 个 id**
(订单 / 成交 / 资金流水),冲突面是单表的三倍。
## 这是"修正偏差",不是"改基线"
`docs/00-新数据库基线设计.md` 第 41 行:
| 主键 | 统一 `BIGINT UNSIGNED AUTO_INCREMENT`,业务编号另设唯一键 |
**基线本来就要求 AUTO_INCREMENT**,是生成的 DDL 漏了 —— `_next_id` 自己的
docstring 也写着"与 docs/00 设计稿存在偏差"。所以本迁移**不违反** AGENTS.md
规则 4(禁止改类型/可空性/业务含义):类型仍是 `BIGINT UNSIGNED`、仍是 `NOT NULL`、
`id` 的业务含义不变,只是补回一个列属性;已有行 id 不变,显式给 id 依然合法。
## 迁移 `20260914_baseline_auto_increment`
- **15 张表**恢复 AUTO_INCREMENT(硬编码表名 —— 迁移必须确定性,动态查
`information_schema` 会让同一份迁移在不同环境产生不同结果)。
- **有意排除 3 张**(`EXCLUDED_BECAUSE_FOREIGN_KEY`):`fin_product`(被 10 张
`advisor_product_*` 引用)、`fin_risk_assessment`、`sys_user`(被 19+ 张引用)。
MySQL 拒绝 `MODIFY` 被外键引用的列:
(1833, "Cannot change column 'id': used in a foreign key constraint ...")
改它们必须先 DROP FOREIGN KEY -> MODIFY -> 重建外键,那是另一件事(涉及 30+ 个
外键的重建与一致性验证),不该塞进这条"恢复基线属性"的迁移。且这三张表**写入
频率很低、没有任何代码用 `SELECT MAX(id)+1` 给它们发号** —— 排除它们不影响
本迁移的目标。
- `downgrade()` 可回滚(只是去掉属性、不丢数据),但注释里写明:**回滚会把 P0-2
的并发冲突带回来**。
⚠️ 迁移执行中踩到过"部分生效":MySQL DDL 非事务性,第一次跑到 `fin_product`
才报错,**前 7 张已经改完**。修正列表后重跑即收敛(对已是 AUTO_INCREMENT 的列
再 `MODIFY` 是无害的)。这一点也说明**迁移必须逐表可重入**。
## 代码
`trade_service.py` 删除 `_next_id` 方法及 4 处调用(`FundSimOrder` /
`FundTransaction` / `FundCashLedger` / `FundHolding`),改由 InnoDB 分配;
顺带清掉因此不再使用的 `Any` 与 `func` import(全仓 grep 确认它们只服务于
`_next_id`)。测试对 `_next_id` 零依赖(已 grep 确认)。
`test_advisor_migration_contract.py` 里那个"钉住末端版本"的断言按它自己的注释
要求同步更新到新 head。
## 实测
- `alembic upgrade head` -> `current = 20260914_baseline_auto_increment`,
复核状态:**15 张已生效、3 张按设计排除**
- **并发下单实测**(2 个客户 × 3 笔 = 6 笔真并发;刻意用**不同客户**,
因为 P0-3 的行锁已经把同一客户串行化了,不同客户才会真正并发进入发号路径):
成功 6 / 主键冲突 0 / 其它失败 0
=> P0-2 已解决
- `pytest tests/unit tests/contract` -> **1427 passed, 2 skipped, 2 failed**
(2 个既有失败与本次无关)
- `ruff check` -> All checks passed
|
2026-09-14 20:27:35 +08:00 |
|
lzf_0626
|
a337cca31a
|
修复 T002 下单的两个 P0:无幂等保护、读账户/持仓无行锁
## P0-1 下单没有幂等保护(实测复现过重复扣款)
`app/api/controllers/trading.py` 的 T002 此前**没有任何幂等保护** —— 全仓 7 个
controller 共 29 处声明了 `Idempotency-Key`,**唯独 trading.py 一处都没有**,
而 `docs/05` §5.2 要求写端点必须带。
**修复前实测**(同一 key 连发两次):
两个不同 order_no:SO...FD48A488 / SO...2FB182C0
委托 +2、可用现金 -910.50(2×455.25)、510300 持仓 3500 -> 3700
⇒ 重复扣款 + 重复建仓
**改法**:接 `ApiTransactionService.execute_in` —— 业务写入与幂等回执**同一个事务**
(`docs/05` §5.2),同键同 body 的第二次请求直接回放上次响应。
- `trade_service.submit_order` 新增 `commit: bool = True`:包装层调用时传
`commit=False`,由 `execute_in` 统一提交。**不做这一步就会"内层提交外层事务"**,
幂等回执与业务写入分处两个事务,回执写失败时业务已落库,重放失去意义。
- 响应经 `model_dump(mode="json")` 统一 JSON 化后再 `model_validate` 还原 ——
保证"首次"与"重放"两条路径返回**同一形状**,且对外结构不变
(金额仍是字符串化 Decimal)。
**修复后实测**(同一 key 连发两次):
两次 order_no 完全相同:SO202609141216512C7166D4
两次响应逐字段相同(真正的回放)
可用现金 -455.25(单次)、510300 持仓 +100(单次)
T003 核对:43 条委托里只多出 1 条
## P0-3 下单读账户/持仓无行锁(并发可扣穿余额)
`trade_service.py` 全文 **零 `with_for_update`**,而项目其它 15 个 service 共 38 处
用了它 —— 规范早已建立,这里是遗漏。
**改法**:`_load_account` / `_load_holding` 增加 `for_update` 开关(默认关,
只读路径不加锁、不牺牲并发),`submit_order` 以 `for_update=True` 调用。
**加锁顺序固定「账户 → 持仓」**:并发事务按同一顺序取锁才不会成环,
这一点写在两处 docstring 里,改顺序前必须先想清楚。
**实测**(真并发 2 笔,各 45520 元,合计 91040 > 余额 49103):
第 1 笔:201 成交 SO...59568811
第 2 笔:422 INSUFFICIENT_FUNDS「可用余额 3578.91 不足」
成交 1/2,最终余额 3578.91 >= 0
**关键证据**:被拒那笔读到的是 **3578.91(已扣减后)**而不是初始的 49103.46 ——
证明两个事务被行锁串行化了。无锁时两笔都会读到 49103.46 而双双通过,
余额会变成 -41936。
## 实测汇总
- `pytest tests/unit tests/contract` -> **1427 passed, 2 skipped, 2 failed**
(那 2 个是既有的:投顾页面被替换、docs/05 §19 分组行,均与本提交无关)
- `ruff check` -> All checks passed
- 两次实测的完整证据见上;两份验证脚本在 %TEMP%(未入库)
## 顺带修正一个我自己脚本的 bug
验证脚本里用 `GET /users/me/orders?limit=200` 读委托数,而 T003 的 `limit` 上限是
**100**(`Query(le=100)`)→ 422 → 读到 0 条,一度让我误判"委托没增加"。
改用 `limit=100` 后确认委托确实只 +1。
|
2026-09-14 20:19:55 +08:00 |
|
lzf_0626
|
e4cd336afa
|
修复权限拒绝变 500、访客令牌无限流,并让两处测试跟上代码
## 1. 权限不足从 500 修回 403(14 个测试失败里的 11 个)
`app/service/authorization_service.py` 的 `_deny` 构造 `InteractionAudit(...)` 时
**漏了 `created_at`**(该列 NOT NULL 且无默认值),而 `raise ForbiddenAgentError`
写在 `session.commit()` **之后** —— commit 必抛 IntegrityError
(1048: Column 'created_at' cannot be null),于是**永远走不到 raise**:
预期 403,实际 500:Internal Server Error
**⇒ 任何「权限不足」的请求都变成 500**,破坏 docs/05 §3.6 的错误码契约。
全仓 100+ 处 `InteractionAudit(...)` 都跟着 `created_at=now`,只有这一处没有
(由 2026-09-14 的 `857c106`「投顾工作台:三接口支持按客户出方案」引入)。
修两处:
- 补 `created_at`;
- **并把审计写入失败与 403 解耦**(try/except + `logger.warning(exc_info=True)`):
安全判定不该依赖审计表是否可写 —— 拒绝就是拒绝。但也绝不静默,审计缺失是合规问题。
## 2. 访客令牌端点补限流(P0-4)
`app/api/controllers/visitor_tokens.py` 此前**零认证、零限流**,可以不限量铸造
有效 JWT;每个都能调 `/api/v1/agent-runs` 触发 LLM 调用,而 agent-runs 的限流
按 `user_id` 计、访客 `sub` 每次都是新随机值 ⇒ **限流被天然绕过**。
把 `rate_limit.py` 的登录闸门抽成通用的 `_enforce_ip_rate_limit(...)`,
新增 `enforce_visitor_token_rate_limit` 挂到该端点的 router 上。
⚠️ 刻意**用独立计数器前缀**(`visitor-token` vs `login`)而不是直接复用登录闸门:
共用会让两者互相挤占配额 —— 正常访客刷几次页面就把别人挡在登录外。
阈值 30 次/分钟(比登录的 10 次宽松,因为访客进站/刷新会正常签发)。
## 3. 测试跟上代码(2 个)
- `test_product_recommendation_service.py`:monkeypatch 打在 `current_for_agent` 上,
而 `generate()` 现在走 `current_for_customer(customer_id, context)` —— 补丁不生效,
真实方法被执行并命中鉴权抛 403。改为按新签名打补丁。
- `test_authorization_service.py` 等 3 个:随第 1 项一起恢复。
## 实测
- `pytest tests/unit tests/contract` -> **1427 passed, 2 skipped, 2 failed**
(修复前 **14 failed** / 1426 passed)
- `_deny`:权限不足恢复 **403**(原为 500)
- 访客限流:连发 40 次 -> `{201: 30, 429: 10}`,首次被拒返回
`RATE_LIMITED「访客令牌签发过于频繁:每 60 秒最多 30 次」`
(修复前:连发 15 次全部 201、无限流)
- `ruff check` -> All checks passed
## 剩余 2 个失败(需产品决策,未擅自处理)
1. `test_advisor_workspace_registers_documented_operation_endpoints` ——
`employee-advisor/dashboard/index.html` 已被**整个替换**为一个自包含静态页
(`data-page-node-id` 属性、内联全部 CSS/JS、硬编码 `API="http://127.0.0.1:8000"`、
自带「离线本地引擎」),**不引用 `api-client.js` / `app-shell.js`、不调 `mountShell`**。
测试断言的是旧页面措辞("组合分析"),新页面写的是"生成推荐方案"。
是接受替换后的页面(改测试断言),还是恢复挂平台壳的版本,属产品决策。
2. `test_docs_endpoint_ids` —— `docs/05` §19 表里被插入了分组标题行
(`**场外基金**`、`**账户与交易**`),而检查工具要求首列是端点编号。
|
2026-09-14 20:14:50 +08:00 |
|
lzf_0626
|
e9539ae1be
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 18:24:24 +08:00 |
|
lzf_0626
|
857c106faf
|
投顾工作台:三接口支持按客户出方案 + 动作按所选客户分流
后端(向后兼容,customer_id 缺省即原行为):
- 三个请求契约新增可选 customer_id(推荐/资产配置/持仓诊断)
- AuthorizationService 新增 require_customer_scope(权限码 + 数据范围)
- 推荐/资产配置/持仓诊断服务支持指定被分析客户
- InvestmentGoalService 新增 current_for_customer
前端(employee-advisor/dashboard/index.html):
- 删除「本人」虚拟条目,客户列表改为 4 位真实客户
- 动作与自然语言入口均按所选客户带 customer_id 调真实后端
- 新增风评超期熔断闸门(FM-03,流水线停在 ② 画像)
- 统一对话入口从硬编码占位改为自然语言意图路由
|
2026-09-14 18:22:56 +08:00 |
|
yuancong_0626
|
14e027f781
|
合并yy与远程qyqy_develop并保留业务优先级
|
2026-09-14 18:16:33 +08:00 |
|
yuancong_0626
|
2548c39c6b
|
袁聪的最后一次完善更新
|
2026-09-14 18:13:10 +08:00 |
|
lzf_0626
|
484cccc145
|
记录三个演示文档的移动,并把 .workbuddy/ 加进 .gitignore
## 文档移动(docs/ -> docs/演示用/)
`代码结构与关键逻辑梳理-2026-09-14.md`、`后端接口文档-2026-09-14.md`、
`软件需求文档-2026-09-14.md` 移入 `docs/演示用/`。
git 识别为 **rename**(状态 `R`),三份文档的历史完整保留,不是"删掉再新建"。
## .workbuddy/ 加入 .gitignore
它与文件里已有的 `.agents/`、`skills-lock.json` 属同一类 —— AI 编码助手的工具产物,
不是项目内容。放在同一节下,避免以后 `git add -A` 时误提交。
## 未包含(有意留下)
`docs/23-记忆分层与画像设计.md` 的工作区改动**没有**一并提交:那不是我改的,
也不在这次确认的范围内,留在工作区由作者决定何时提交。
(暂存时被 `git add docs` 一并带上,已 `git restore --staged` 退回。)
|
2026-09-14 12:09:23 +08:00 |
|
lzf_0626
|
3df6671ce3
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 12:07:18 +08:00 |
|
lzf_0626
|
e6d74059f2
|
修复员工工作台顶部导航重复(入口 JS 被引两次)
## 现象
打开 `employee-console/workspace`(平台治理),页面上出现**两份一模一样的顶部栏**:
南方财富 / 模拟基金服务 / 平台治理 / 风控中心 / admin_t 管理员 —— 连同页脚一起各两份。
## 根因:同一个入口 JS 被引了两次,且 `?v=` 不同
`app/static/portal/employee-console/workspace/index.html` 里曾同时存在:
<script type="module" src=".../workspace.js?v=20260913-3"></script>
<script type="module" src=".../workspace.js?v=20260914"></script>
浏览器按**完整 URL** 去重,两条不同 query 被当成**两个模块**、**各执行一次**。
入口里的 `mountShell()` 因此跑了两次,而它当时是
`document.body.insertAdjacentHTML('afterbegin', ...)` —— **无条件插入**,
于是 header 与 footer 各插两份。
来源是合并事故(`git blame` 定位):
| 行 | 提交 | 作者 |
|---|---|---|
| 旧 | `e31420df` | 卿云秋月(把版本号改成 `20260913-3`)|
| 新 | `f5d1b246` | 张胜宇(把版本号改成 `20260914`)|
两人各自把**同一行**的版本号换成新的,合并时两边都被保留,成了两行。
那个提交的信息是 "merge ... and retain risk review updates" ——
"retain" 在这里保留错了地方。
## 修法(两侧都堵)
1. **HTML 收敛成一行**(保留较新的 `?v=20260914`,与 `workspace.js` 内部
`api-client.js?v=20260914` 一致),并就地写明"改版本号是替换这一行、不是新增一行"。
2. **`mountShell` 加幂等保护**:已有 `.site-header` 就直接 return。
之所以不满足于只修那个 HTML —— 这个 bug 的症状很难反推到原因
(页面看起来只是"多了一块"),而以后谁加缓存版本号时很容易再犯一次。
## 防回归(两条测试,都做过负面验证)
- `test_no_portal_page_includes_the_same_script_twice`:扫 `app/static/portal` 下
**19 个页面**,把 `<script src>` 去掉 query 后比对,同一入口出现多次即失败。
负面验证:把重复行临时放回去,测试**精确报出**
`employee-console\workspace\index.html: ['/static/portal/employee-console/workspace/workspace.js']`,
恢复后通过。
- `test_mount_shell_is_idempotent`:断言 `app-shell.js` 里有那句幂等判断。
全站扫描确认**只有这一处**,不是批量问题。
## 实测
- `GET /portal/employee-console/workspace/` -> 200,页面里 `workspace.js` **只出现一次**
(`?v=20260914`);静态 HTML 中 `site-header` 出现 **0 次**(确认由 JS 注入,
所以 JS 执行一次就只插一份)
- `pytest tests/unit/api/test_portal_frontend.py` -> **43 passed**(41 + 新增 2 条)
- `ruff check` -> All checks passed
## 一点说明
这次是"改同一个版本号"的合并冲突处理失误,属于**流程问题**而非个人疏忽:
两边都想把缓存版本号推新,冲突解决时很容易两边都留下。
测试补上之后,这类错误会在 `pytest tests/unit` 里当场暴露。
|
2026-09-14 12:06:38 +08:00 |
|
zhangshy
|
8409a2c96e
|
合并主项目最新文档并保留风控文档更新
|
2026-09-14 11:44:45 +08:00 |
|
zhangshy
|
5a83aa6fef
|
更新风控文档分页会话记忆和演示数据规则
|
2026-09-14 11:44:21 +08:00 |
|
lzf_0626
|
e59a9905a0
|
把「今日盈亏是硬编码 0」提级为待业务确认项(SRS Q22)
## 问题
`today_profit_loss` / `today_profit_loss_ratio` 在 `app/service/trade_service.py` 里是
**硬编码 `ZERO`**(持仓列表 L678、账户看板 L748-749),客户看板、持仓页、盈亏分析页的
「今日盈亏」永远显示 0.00。同一函数里的「持有盈亏」是**真算的**
(`market_value - cost_amount`,L660 / L726),所以只有这一项是占位。
三份 2026-09-14 文档其实**都记录了这个事实**(接口文档第 11 条、SRS 第 212 / 458 / 615 行),
但**都埋在「注意事项」里,没有进第 0 章那份「★ 请先回答本章」的待确认清单** ——
业务翻文档时看不到,演示现场被问到就答不上来。而且 SRS 第 212 行还把它**错引到了 Q7**
(Q7 讲的是场外阈值,与此无关)。
## 改动
**docs/软件需求文档-2026-09-14.md**
- 新增 **0.5 节「占位实现与本期边界」**,登记 **Q22**:客户看板的「今日盈亏」本期是否实现。
该节专门收这类「接口有、值还没真算」的偏差(不报错、单元测试全绿、只有业务看得出不对),
以后同类问题继续往这里追加。
- 第 0 章项数 21 -> 22;阻塞项清单补上 Q22。
- 修正 F-4.2 的错误引用(见 Q7 延伸 -> 见 Q22)。
- R2 状态改为「已提级为待决项 -> 见 Q22」。
- **R1 状态改为已修复**:组员提交 `4b7ee13`「修复管理员工作台函数缺少闭合大括号」
已解决 `workspace.js` 的语法错误。按该条自己要求的方式复验过 ——
`node --experimental-vm-modules` + `vm.SourceTextModule` 遍历 `app/static/portal`,
**44 个模块全部通过**(`node --check` 会假通过,不能用来判定)。
**docs/44-演示流程.md**(演示现场用)
- §3「可能被问到的问题」新增话术:直说这是占位值、指出持有盈亏是真算的、
指向 SRS Q22 与两条口径的结论;并建议「时间紧就避开这一栏,被问到不要含糊也不要现编」。
- 场景 2(客户资产)加现场提示:讲「持有盈亏 / 总市值 / 可用资金」,不要指着「今日盈亏」讲。
**docs/后端接口文档-2026-09-14.md**
- 第 11 条补上代码行号与交叉引用,并点明同一响应里的 `profit_loss` 是**真算的**。
## 顺带交给业务:两条口径的可行性(实测,非推测)
业务要决定的是「要不要做」,但「能不能做」也得一起给,否则业务选完还要再问一轮:
| 口径 | 数据现状 | 结论 |
|---|---|---|
| 用行情算(今收 − 昨收) | `fin_market_price` **只在同步时才写行**,实测每个产品**只有 2 行**(515450 只有 09-11 与 09-14,还跨了周末) | ❌ 取不到连续「昨收」 |
| 用净值算(今净值 − 上一交易日净值) | `fin_nav_history` 每个产品 **120~160 行连续交易日净值** | ✅ 建议按这个口径 |
⇒ 文档建议按**净值口径**实现,并提醒业务一并确认语义:净值是日终数据,
该指标实际是「最近一个交易日的盈亏」,**不是盘中实时**。
## 入库说明
三份 `docs/*-2026-09-14.md` 此前是**未跟踪文件**,本次一并入库(同一批文档交付物且互相引用)。
`.workbuddy/`(工具会话记忆)属工作区过程产物,**未入库**。
|
2026-09-14 11:24:51 +08:00 |
|
zhangshy
|
4b7ee13cf5
|
修复管理员工作台函数缺少闭合大括号
|
2026-09-14 10:56:45 +08:00 |
|
zhangshy
|
bfbaf823c2
|
同步预警队列后端分页上限为十条
|
2026-09-14 10:50:48 +08:00 |
|
zhangshy
|
baecb89bd4
|
预警队列调整为每页十条
|
2026-09-14 10:40:41 +08:00 |
|
zhangshy
|
54055b4328
|
合并主项目最新改动并保留风险问答记忆修复
|
2026-09-14 10:08:37 +08:00 |
|
zhangshy
|
20773453bd
|
修复通用风险问答会话历史丢失
|
2026-09-14 10:08:05 +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
|
d95ef09627
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 02:00:59 +08:00 |
|
lzf_0626
|
c5bfc2c998
|
钉住 .bat 的换行:新增 .gitattributes(*.bat text eol=crlf)
本机 core.autocrlf=true,git 在 add 时会把工作区的 CRLF 静默存成 LF;
换个 autocrlf=false 的环境 clone 出来就是 LF,而 cmd 对 LF-only 的 .bat
会行边界错乱(把 echo 的说明文字当命令执行,实测报
"AT 命令已弃用"、"']' 不是内部或外部命令",甚至真的去跑脚本)。
.gitattributes 里钉 `*.bat text eol=crlf`:仓库统一按 LF 存储、检出时转回 CRLF,
不依赖任何人的本地 git 配置。
验证方式(不是"我觉得会转"):删掉工作区的 bat 再 `git checkout --` 取回来,
实测 2090 字节、62 个 CRLF、无裸 LF,与写文件时的形态一致。
仓库里目前只有 `启动平台.bat` 一个批处理,影响面仅此。
|
2026-09-14 02:00:41 +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 |
|
张胜宇
|
3134fe5dd0
|
Merge branch 'qyqy_develop' of http://47.106.207.27:3000/AI260626/group_fqcd_jr into qyqy_develop
|
2026-09-14 01:45:47 +08:00 |
|
lzf_0626
|
3cbfe10d62
|
fix(portal): 补齐运营三个新页面的同名 css 入口,修复合并进来的红灯契约测试
合并组员的运营工作台提交后,`test_every_portal_page_has_local_js_and_css_entry`
红了:门户每个页面都要有与目录同名的 `js`/`css` 入口,而新加的
`nl2sql` / `offsite` / `promotion` 三个页面**只有 js**,样式统一引 `operator-workspace.css`。
## 改动
给三个页面补上同名样式入口,并在各自 `index.html` 里引用(放在共用的
`operator-workspace.css` 之后,便于页面覆盖):
- `employee-operations/nl2sql/nl2sql.css`
- `employee-operations/offsite/offsite.css`
- `employee-operations/promotion/promotion.css`
三个文件当前**没有规则**,只写了用途说明 —— 它们的价值是把"页面专属样式"的位置
**确定下来**:这个约定的意义正在于此,否则将来只会继续往共用文件里堆。
⚠️ 只在每个 `index.html` 的 `<head>` 里**加了一行 link**,未改动组员的其它内容。
验证:`tests/unit/api/test_portal_frontend.py` 37 passed;
unit+contract **1398 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;三个页面与三个 css 均 200;e2e 冒烟 **40/40**。
|
2026-09-14 01:42:21 +08:00 |
|
张胜宇
|
f5d1b24618
|
merge qyqy_develop and retain risk review updates
|
2026-09-14 01:41:02 +08:00 |
|
lzf_0626
|
4c1e84b5a2
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 01:35:54 +08:00 |
|
lzf_0626
|
eded5896cd
|
fix(admin): 回复模板的 scene 加枚举校验,非法值不再冒成 500
## 问题
`agent_reply_template.scene` 有数据库 CHECK 约束 `chk_template_scene`,只允许
`disclaimer` / `low_confidence` / `compliance_block` / `transfer` / `model_failure` /
`system_busy` / `clarification` 七个值。
而 `ReplyPayload.scene` 只校验长度(`min_length=1, max_length=32`)——
传一个不在列内的场景会**穿过接口校验、撞上数据库约束**,最终以
`500` 冒出:
(3819, "Check constraint 'chk_template_scene' is violated.")
那本该是一次 `422` 参数校验失败。**500 与 422 的差别不只是状态码**:
前者会让调用方以为服务端故障、触发重试与告警,而实际是自己参数错了。
## 改动
`ReplyPayload.scene` 改为 `Literal[...]`(新增 `ReplyScene` 类型别名),
取值与 `chk_template_scene` **逐字对齐**,并在注释里写明这个对齐关系与本次事故。
实测:非法 scene → `422 AGENT_INPUT_INVALID`(不再是 500);合法 scene → `201`。
## 发现方式
这一处是**按接口逐条调用、逐个核对返回**时暴露的 —— 只看代码很难注意到
"schema 的宽松校验"与"数据库的严格约束"之间那道缝。同类风险仍存在:
凡是**表上有 CHECK 而 schema 只做长度校验**的字段,都有同样的 500 风险。
验证:unit+contract 1397 passed;integration 110 passed;ruff 通过;
mypy 251 文件 0 错;e2e 冒烟 40/40。
|
2026-09-14 01:35:34 +08:00 |
|
yuancong_0626
|
295be972d2
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 01:23:32 +08:00 |
|
lzf_0626
|
daf73a2865
|
fix(portal): 照接口逐条核对前端,修掉 5 处「照着表结构写、看着像对」的缺陷
做法:把前端 `api-client.js` 注册的端点**按前端完全相同的方式**(同样的路径、参数、
身份)逐个调用,再拿真实返回去核对前端 render 用到的字段。
**这类问题纯读代码看不出来** —— 只有把真实返回和期望字段摆在一起才会暴露。
## 1. 知识库功能实际是坏的(K002 / K003 是裸信封)
`K002` 的成功体是 `{knowledge_ids, filename, chunk_count}`、`K003` 是 `{items, count}`,
**都没有 `data` 信封**。而 `request()` 默认取 `payload.data`(undefined),于是:
- 上传后前端显示「已入库 **0** 块」,而库里其实切了 23 块;
- 列表永远显示「知识库为空」。
端点表里标 `raw: true` 后(与 `V001` 同一做法)两者都正常。
实测:上传 4805 字符的产品手册 → 切 23 块并出现在列表里。
## 2. 委托/成交详情页有 5 行永远显示「--」
前端按 `fin_sim_order` / `fin_transaction` 的**建表字段**写了 `quote_source`、
`nav`、`fee_rate_snapshot`、`confirmed_at`、`auto_confirmed` ——
但这些字段**接口的返回视图没有带**(表里有、返回里没有)。已按实际返回重写字段表,
并在注释里写明"以接口返回为准,不要照表写"。
## 3. 配置项与路由规则的**编辑功能不可能成功**(接口缺口)
`PUT` 硬性要求 `If-Match`,校验的是该行内容的 digest;而这两个资源是 `detail=False`
—— **没有任何端点能返回这个 digest**(列表的 `meta` 只有 trace_id)。
乐观并发在"读不到版本"的前提下等于死锁:**首次编辑必然 409**。
(配置发布能用,是因为它有详情端点 `A003`。)
- 新增详情端点 `A048` / `A049`(`detail=True`),已登记 `docs/05` §19;
- 前端编辑前先 GET 详情取 etag,再带 `If-Match` 提交。
- 实测:编辑配置项与路由规则均 200;**不带 `If-Match` 仍返回 409**,
说明乐观并发没有被削弱。
## 4. 路由规则表单**必然提交失败**
前端固定写 `max_attempts: 2` 且 `fallbacks: []`,而后端要求
`max_attempts ≤ 端点总数`(主 + 兜底)→ 422「重试次数超过端点数量」。
改为 `1` 并注明约束。
## 5. 主端点手填 ID 会 422
后端对不存在/未激活的 `primary_endpoint_id` 直接 422「模型端点不存在或未激活」。
把输入框改成**下拉**,只列 `status='active'` 的端点(数据复用已有的端点列表)。
## 顺带
- `apiClient` 增加 `del()` / `put()`:发出的方法一直由端点表决定,所以 `post('K004')`
也能发 DELETE —— 语义太绕,现在意图与行为一致。
- 清理了测试期间上传的 31 条知识残留(客服会检索到它们),库内恢复到 23 条产品手册。
## 关于"逐条核对"的方法论
前两轮跑出来的 9 个和 5 个"失败"里,**多数是我测试脚本自己的假设错了**,不是前端问题:
`T001` 是 `{account, summary}` 嵌套、`RK002` 的字段叫 `risk_level`、
`RK002/RK004/RK005` 的 limit 上限是 5/10/10(前端传的正是 5/10/10)、
`AD011/A002/A047` 的 data 是裸 list。每一处都回到前端源码确认后才下结论 ——
**先把"我以为"改成"代码里写的"**,否则报告出去的就是假 bug。
验证:unit+contract **1397 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;§19 现 93 个端点无重复;e2e 冒烟 **40/40**。
前端等价测试:只读 31 项全绿、写操作(含 ETag 链路)9 项 8 绿 1 项因测试数据过短。
|
2026-09-14 01:19:22 +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
|
e31420df29
|
feat(portal): 补齐四个前端缺口——客户详情、知识库管理、配置项与路由编辑
先审计了全部 **141 个后端端点**:前端注册 68 个,注册的**全部有效**(没有一个打不通)。
本提交补的是其中真正影响可用性的四类。
## 1. 客户端:委托详情(T004)与成交详情(T008)
两个接口后端一直存在,但订单页 / 成交明细页**只接了列表(T003 / T007)** ——
客户点不进任何一条记录,看不到成交价、费用构成、确认时间与行情来源。
改为**行内展开**(点「详情」在原行下方展开,再点收起),不跳页:
- 复用新加的公共样式 `.list-detail`(`common/customer-list.css`),两页共用而不各写一份
- 详情取不到时**不整页报错**:列表本身是好的,只恢复按钮并记一次错误
## 2. 管理员:知识库管理(K002 / K003 / K004)
客服的**全部回答都来自已入库的知识**,而此前**没有任何页面能管理知识库** ——
只能靠命令行脚本 `tools/seed_knowledge_demo.py` 灌数据,管理员既看不到也改不了。
新增「知识库」标签页:文档列表 + 上传(.txt/.md/.docx)+ 失效。两处要点:
- **K003 的成功体是裸的** `{items, count}`、**没有 `data` 信封** ——
`request()` 仍会去取 `payload.data`(那是 undefined),所以列表要两面都兜,
否则永远显示"知识库为空"、而库里其实有数据;
- 上传是 **JSON + base64**,不是 multipart(一期契约如此,见 `knowledge_management.py`)。
## 3. 管理员:配置项与模型路由(A001 / A008–A010 / A018–A020)
此前只能对**已存在**的版本走"校验→审核→激活",**既不能新建版本、也不能往里加配置项**
—— 新建的版本永远是空的、校验必然失败;模型路由规则同样既看不到也改不了。
- 新增「新建配置版本」表单(版本号 / 标题 / 变更说明)
- 每个版本加「内容」按钮(**与状态无关**:草稿阶段就要能加,否则版本永远空)→
展开该版本的**配置项**与**模型路由规则**,两者都支持新增与编辑
- 配置项的「值」按 JSON 输入并在前端校验:与其让后端 422,不如就地拦住并说清哪里不对
- `fallbacks` 暂不在界面编辑(提交空数组),需要时用接口补
这些端点**都已在 `docs/05` §19 有编号**,直接复用,无需新增编号。
## 4. 两个"死端点"查证后**保留**
初查发现 `RK013`(风控日报非流式,已被 RK014 流式取代)与 `ADVISOR_GOAL`
(投顾自己的目标;投顾是员工、没有目标 → 永远 404)注册了却无人调用,一度删除。
但 `tests/unit/api/test_portal_frontend.py` 立刻失败 —— 它把"页面会用到的端点"
固定成一张清单,**注册与调用是两件事**。已恢复注册,并就地注明它们当前无人调用、
但受契约保护。
顺带发现:**`RK013`–`RK015`(风控日报)也不在 §19**,与投顾 AD 段原先的情况相同,
属文档缺口(未在本提交内补)。
## 辅助改动
`apiClient` 增加 `del()` 与 `put()`:真正发出的方法一直由端点表里的 `method` 决定,
所以 `post('K004')` 也能发出 DELETE —— 但读代码的人会以为发的是 POST。
现在意图与行为一致。
验证:unit+contract **1397 passed**(含前端契约 37);integration **110 passed**;
ruff 通过;mypy 251 文件 0 错;e2e 冒烟 **40/40**;相关页面与静态资源全部 200。
|
2026-09-14 00:51:13 +08:00 |
|
lzf_0626
|
9a5259d787
|
feat(market): 存下行情源给的涨跌幅,产品列表与排行页不再显示"暂无"
## 问题
产品列表与排行页的「最近涨跌」全是"暂无"。原因是它只能由**两个交易日的收盘价**
现算,而 `fin_market_price` 每只产品每天只有一行 —— 库里头一天只有一天数据时
根本算不出来。
## 但行情源本来就给了这个数
腾讯行情接口的第 32 位就是**当日涨跌幅**(适配器此前只解析了开高低收量额,没取它)。
所以不必等第二个交易日去算,把源给的数存下来即可。
## 改动
- **迁移** `20260913_market_price_change_pct`:给 `fin_market_price` **新增一个可空列**
`change_pct DECIMAL(10,4)`。只加列、不改任何已有字段(AGENTS.md 规则 2 允许),
可空且不回填,既有读写方全部不受影响。
- **适配器**:`_parse_tencent` 增解析位置 4(昨收)与位置 32(涨跌幅)。
- **同步服务**:写入 `change_pct`;降级路径(净值兜底)没有这个数就写 **NULL**。
- **接口**:`_resolve_change_pct` **优先用存下来的值**;迁移前的历史行没有该列的值,
才回退到"今日收盘 vs 昨收"现算。仍为 `null` 时前端显示"暂无" ——
**不得当成 0**(`formatPercent(null)` 会渲染成 `+0.00%`,那等于说"今天平盘")。
## 契约测试同步
两处契约断言因结构演进需要跟进 —— 它们的作用正是拦住这类变更:
- `test_fund_readonly_contract.py`:`fin_market_price` 列数 **13 → 14**
- `test_advisor_migration_contract.py`:迁移 head 更新为新版本
(核心断言仍是"链收敛到唯一 head",此处只是钉住末端)
验证:实测 20 只产品**全部有真实涨跌幅**(如 515450 −0.43%、159511 +1.23%);
unit+contract **1397 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;`audit_schema` 与 `migration_state_check` 均通过;e2e 冒烟 40/40。
|
2026-09-14 00:30:06 +08:00 |
|
lzf_0626
|
de55c5c60c
|
feat(advisor): 登记 17 个投顾端点并补管理员复核入口(A047 待审队列)
## 1. docs/05 §19 补登 17 个投顾端点
这批端点此前**只存在于代码中**,§19 一条都没登记;而 §12 写的入口
`/api/v1/advisory-plans/**` 与实际路径 `/api/v1/advisor/**` 也不符(已修正)。
- **A041–A046**:管理员治理(配置回测、画像标签与漂移复核、推荐方案审核与发布)
- **AD001–AD011**:投顾自用。**新开 `AD` 号段**的理由:它与 A 段是两个不同的权限面
—— A 段是 `/api/v1/admin/**` 管理面,AD 段是 `/api/v1/advisor/**` 投顾自用;
混在一个号段里,"这条到底谁能调"就得逐条去读权限列。
- 另加 AD 段说明块:`investment-goal` 的两套权限码(`...:self` / `...:customer`)、
AD006/AD007 虽在投顾路径下却要求 `admin`、灰度开关 `enforce_advisor_rollout` 前置、
幂等范围(AD008/AD009 无幂等头)、以及 404/409 的失败口径。
§19 现为 **90 个端点 / 9 个号段**,无重复。
## 2. 管理员复核入口 + A047 待审队列
**发现一个让审核链路不可达的缺口**:`review` / `publish` 都要求调用方先拿到键
(推荐方案是 `content_id`、方案书是 `goal_no`),而此前**没有任何端点能列出待审内容**
—— 管理员拿不到键,投顾生成的东西就永远停在待审状态。
- 新增 `GET /api/v1/admin/advisor/pending-contents`(编号 **A047**):一次返回两类待审内容。
两类内容的"待审"取值不同(推荐方案 `pending_review`、方案书 `pending`),
只判其中一个会整类漏掉,所以用 `PENDING_STATES` 一并匹配。
- **为方案书一并查出 `goal_no`** —— 它的审核/发布端点(AD006/AD007)按 `goal_no` 寻址,
只给 `content_id` 的话管理员拿到列表也调不动。已由 integration 测试守住这一点。
- 管理员工作台新增「投顾复核」标签页:列出待审内容,支持审核通过 / 驳回 / 发布;
前端按 `content_type` 自动选择端点、寻址键与载荷
(方案书发布要 `{publish: true}`,推荐方案发布不读 body)。
- 发布前校验状态:未审核通过不允许发布,与 `publish_book` 的 `IllegalState` 一致。
## 3. ⚠️ 同时发现:投顾的三个分析功能对投顾本人不可用
`ProductRecommendationQuery` **没有 `customer_id`** 字段,而 `generate` 用的是
`int(context.user_id)`(`product_recommendation_service.py:69`)—— 即**把投顾自己**
当成了服务对象。投顾是员工、没有风险测评与持仓,于是实测:
POST /api/v1/advisor/recommendations → {"status": "profile_required"}
POST /api/v1/advisor/asset-allocation → {"status": "profile_required"}
**组合分析、资产配置、生成推荐草案这三个功能,投顾调用必然拿不到结果。**
这是"投顾功能很奇怪"的直接来源之一。修它要改接口契约(加 `customer_id`、
并确定"投顾能对哪些客户生成"的权限口径),属产品决策,未在本提交内改动。
验证:unit+contract **1397 passed**;新增 integration 用例 2 passed;ruff 通过;
mypy 251 文件 0 错;A047 实测管理员 200(带出方案书的 `goal_no`)、投顾 403。
|
2026-09-14 00:17:46 +08:00 |
|
lzf_0626
|
373bcb2a68
|
fix(test): 补 FakeSession.scalar 桩,修复合并进来的 15 个红灯用例
组员的 `2a3146e`("增加风控列表总数")在 `fund_query_repository.fetch_page` 里新增了
一次 `await self.session.scalar(select(func.count())...)` 来返回 `total`,但
`tests/unit/repository/test_fund_query_repository.py` 的 `FakeSession` 只有 `execute`,
于是该文件**15 个用例全部**挂在:
AttributeError: 'FakeSession' object has no attribute 'scalar'
## 改动
给 `FakeSession` 加 `scalar`:
- **返回值取 `len(_rows)`**。这些用例没有一处断言 `total`(只校验 `has_more` /
`next_offset` 与生成的 SQL),返回非负整数即可;`_rows` 是带 `limit+1` 的当页结果,
所以它**不是真实总数** —— 将来若要断言 total,需给这个桩加显式参数。
- **不写入 `statements`**。`last_sql` 取的是 `statements[-1]`,而有用例断言的是
**分页查询**的 SQL(LIMIT / ORDER BY / 过滤条件)。第一版实现把 count 语句也
append 了进去,`last_sql` 随即指向 count 语句,4 个用例的断言集体落空
(实测:15 failed → 4 failed → 0)。count 语句改记在 `count_statements` 里备查。
验证:`test_fund_query_repository.py` 18 passed;unit+contract **1397 passed**;
ruff 通过;e2e 冒烟 40/40。
|
2026-09-14 00:02:35 +08:00 |
|
lzf_0626
|
7c3a832104
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-13 23:56:44 +08:00 |
|
lzf_0626
|
421fed8569
|
feat(advisor): 补全投顾流程入口(确认目标 → 查看方案书),并修复页面缺缓存版本
## 1. 投顾流程此前是断的
投顾工作台只有 4 个「生成草案」操作 + 1 个只读列表,而**确认目标**与**查看方案书**
这两个端点虽然存在却**没有入口** —— 于是目标永远停在 `pending_confirmation`、
方案书永远停在 `pending`。
实测客户 9001 正是如此:`confirmed_at: null`、方案书 `review_status=pending`。
而「录入客户目标」成功后的提示是"已提交,等待确认与审核",**没有下一步可点**。
改动:
- 新增操作卡「目标确认与方案书」:输入客户 ID → 查询目标 → 显示目标状态、
方案书状态、收益区间 / 回撤 / 期限 / 基准 / 确认时间 → **确认目标** / **查看方案书**。
- **录入目标后自动跳到目标状态页**,把确认按钮摆在眼前,而不是停在"已提交"。
- `ADVISOR_CONFIRM_GOAL` / `ADVISOR_GOAL_BOOK` 补登记进 `api-client.js`
(`ADVISOR_CUSTOMER_GOAL` 早已注册却从未被使用)。
- 404「当前投资目标不存在」按**正常情况**处理(给下一步提示),不当故障报警。
⚠️ **审核与发布不在此范围**:`review_book` / `publish_book` 都要求 `admin=True`
(见 `investment_goal_service`),那是管理员的动作,投顾侧到"确认 + 看方案书"为止。
方案书仍会停在 `pending`,除非管理员侧也补上入口。
## 2. 9 个页面的 script 标签缺缓存版本参数
投顾工作台的 script 是 `dashboard.js`(**没有 `?v=`**),另有 8 个页面同样如此
(客户的持仓/下单/流水/盈亏/测评/成交明细、管理员工作台、运营工作台)。
浏览器会一直用缓存的旧 JS —— **改了代码也看不到效果**,这是"功能很奇怪"的一部分。
已统一补上 `?v=20260913`,投顾页的 js/css 提到 `-2`。
## 3. 过程中我损坏过 8 个文件,已恢复
第一次批量补版本参数时我用了 PowerShell `Get-Content -Raw` + `Set-Content`:
**PS 5.1 默认按 ANSI/GBK 读**,把 UTF-8 中文读成乱码再写回
(`角色与权限` → `瑙掕壊鏉冮檺`)并写入 BOM。`tests/unit/api/test_portal_frontend.py`
的断言当场抓到了它(找不到「角色与权限」)。
已 `git checkout` 恢复全部 8 个文件,改用 Python 重做(显式 UTF-8、不写 BOM、
保持原换行),现在每个文件的改动都是 **1 增 1 删**。
验证:`tests/unit/api/test_portal_frontend.py` 35 passed;unit+contract **1395 passed**;
ruff 通过;mypy 251 文件 0 错;确认目标实测 200
(`pending_confirmation` → `confirmed`,写入 `confirmed_at`),
重复确认被状态机以 409 拒绝。
|
2026-09-13 23:56:27 +08:00 |
|