Commit Graph
202 Commits
Author SHA1 Message Date
lzf_0626 e0468a9d9f docs: 记录 PR #7 合并门禁结果、两处顺手修正与权限号段冲突的来龙去脉 2026-09-12 12:10:03 +08:00
lzf_0626 cdb2de3e45 chore: 权限号段根治——9041-9046 并进种子;修正 grant 脚本与种子 9020-9034 的 id 映射冲突 2026-09-12 12:09:55 +08:00
lzf_0626 2bb056e516 fix: 修正合并进来的 test_security.py 两处密钥路径漂移(漏 dev/,标准布局下必红 2 个用例) 2026-09-12 12:09:55 +08:00
lzf_0626 441364437c merge: 合并 ZSY 的客服 Agent 接入(访客身份、画像候选、转人工工单)—— PR #7 2026-09-12 12:05:25 +08:00
lzf_0626 c7226d0b69 回复 ZSY 的处置回执 v3:核实两处修正;更正端点编号计数(62 非 55);预检合并树并定位 test_security.py 两处密钥路径漂移 2026-09-12 12:04:18 +08:00
张胜宇 f68b052a69 fix: 端点编号去重与 milvus-lite 降为可选依赖
按 qyqy 在 PR #7 评审中的要求处理两项:

- docs/05-接口文档.md §19:客服画像候选的两个端点由 A034/A035 改为 A039/A040。
  原编号与 qyqy 侧登录 / RBAC 只读接口(A034-A038)重复;docs 守卫脚本只校验
  文档文件名编号、不校验 §19 端点编号,因此该重复会静默遗留。改后全表 55 个
  端点编号唯一。
- pyproject.toml / requirements.txt:milvus-lite 由主 dependencies 挪到
  [project.optional-dependencies] dev;requirements.txt 只保留说明性注释,
  不再作为生效依赖。理由:它仅用于本地开发(Docker Milvus 未运行时的本地
  持久化向量库),进主依赖会让生产环境多背一个包。

验证:pytest tests/unit tests/contract -> 1275 passed, 2 skipped, 0 failed;
ruff check app tests tools alembic 通过;mypy app 通过(244 个源文件)。
2026-09-12 11:53:35 +08:00
lzf_0626 ade5e0c085 回复 ZSY 的底座扩展确认 v2:逐行核验两句确认;指出 docs/05 端点编号撞车、三个新权限未随代码合并;补权限种子脚本 2026-09-12 11:46:47 +08:00
lzf_0626 244f03917b 回复 ZSY 的底座扩展确认;登记 docs/00 的一处已知偏差;更新 AGENTS.md 过期口径
## 对 ZSY 三件事的核实与裁决(docs/33-ZSY底座扩展确认-回复.md)

1. **访客身份**:`data_scope="public"` 我扫了全平台 26 个消费点,对未知值的行为一致
   fail closed(6 处 `== "all"` 判假、1 处 `!= "all"` 会要求 customer_ids 非空否则 403、
   1 处显式白名单直接拒),**安全,不用改**。但有一条硬约束必须先确认:全平台有
   **72 处 `int(context.user_id)`,只有 3 处做了防御**——若访客的 sub 不是纯数字,
   其中"权限被拒时要写审计"的路径会把本该 403 的情况变成 500。已要求他确认
   sub 为纯十进制数字、且 visitor 分支复用同一套 `jwt.decode`(含 isdecimal 校验),
   而不是另写第二个鉴权入口。

2. **客服 Agent**:`allowed_roles` 扩集合与确定性安全路由可接受;`recalls_customer_memory`
   是新声明字段,已要求**默认值必须是 True**(否则会静默关掉所有既有 Agent 的记忆召回,
   界面看不出来、只表现为回答变差)。品牌名(南方科技 → 奶龙基金责任有限公司)属业务
   口径,已上报项目方定,不由技术侧拍板。

3. **current_customer_id**:**他的判断正确**。我独立实测 information_schema:
   `EXTRA=''` 且 `GENERATION_EXPRESSION=''`,配合 baseline_generated.sql 无 GENERATED
   子句、seed_profile_demo.py 的注释、profile_repository.py 用原生 SQL 显式写入,
   四处一致 ⇒ `docs/00` 第 783 行"生成列"的描述是错的。处置:按真实 schema 映射成
   普通可空列、**不改 docs/00**(规则 1)、**不补迁移**(DDL 本身正确,为让文档成真而加
   生成列会改变既有列语义,违反规则 4)、但**必须登记这处偏差**。

另:他删了两个死代码文件(含一处硬编码 Milvus 字段名,违反 AGENTS.md §E),方向认同,
但要求他在 PR 里附上两条 git grep 的实际输出以证明"全仓唯一引用是它自己的单测"。

## 落实我在回复里承诺的两件事

- `docs/08-数据库结构审计基线.md` 新增"六、已知文档偏差",逐条登记上述偏差(含四处
  证据与处置口径),并注明发现方式;
- `AGENTS.md` 环境口径新增 `MILVUS_LOCAL_URI` 的坑:配了会让健康检查与部分检索指向
  本地 Milvus Lite 文件,出现"健康检查正常、实际查的是另一个库";并注明 `milvus-lite`
  属本地开发依赖,应放 `optional-dependencies` 而非主依赖。

## 顺带更新 AGENTS.md 的过期口径

- 表数 68 → **89**(场内 51 + 场外/推广 17 + 投顾 21),并注明投顾那 21 张的登记文档待补;
- 测试基线从"1034 passed / 1 failed"改为实测值:ruff 干净 / mypy 228 文件 0 错 /
  1207 passed 0 failed / integration 99 passed,并指向 docs/32;
- mypy 那条从"本机 181 错、双方不可比"改成可操作的判据:先对版本,根因是某一侧虚拟环境
  没满足 pyproject 的 sqlalchemy>=2.0,<3 / mypy>=1.14,<2;并写明**不要装 sqlalchemy2-stubs**
  (那是给 1.4 的,2.0 自带 py.typed,装了反而按 1.4 API 报一批新错)。

文档守卫:42 份无编号冲突。
2026-09-12 11:27:28 +08:00
lzf_0626 39f261da14 新增 docs/32-平台侧交接与联调准备.md
把当前状态整理成可交接的一份:分支与门禁、三条线合进来的内容、平台侧补齐的登录与
RBAC 只读接口、修掉的真缺陷、演示账号与环境口径、新增工具索引、联调清单、悬置决策项。

其中三条是这一轮实际踩出来的,值得留档:

1. 环境数据不随代码合并(config_release / sys_role / sys_permission / Milvus schema /
   advisor_product_*)—— 三条组员线各自都在这上面栽过一次;
2. 合并后必须**单独**跑 alembic upgrade head 与 tools/check_authoritative_docs.py:
   三家合并里两次带进文档重号、两次库与代码版本不一致(一次"版本号跑了表没建"、
   一次"表没建版本号也没跑"),两者都靠 upgrade 收敛,重号只有守卫能发现;
3. 投顾演示数据里"方案"那步配不出来,缺的是**证据来源**(source_url / document_sha256),
   必须来自真实的销售适当性披露或基金合同文件;不伪造这两个字段是关键红线。

另记下环境口径:mypy 与测试数必须带环境读,NL 那边 184 错与本机 0 错的差异根因是
对方的 .venv 没满足 pyproject 的 sqlalchemy>=2.0,<3 / mypy>=1.14,<2,不是代码质量问题。

文档守卫:41 份无编号冲突。
2026-09-12 11:23:46 +08:00
张胜宇 9aaacc242f chore: 清理违反底座规则的死代码并修正接口文档编号
- 删除生产死代码 app/service/knowledge_tool_service.py 与
  app/infrastructure/milvus_knowledge_adapter.py:后者硬编码 Milvus 字段名,
  违反 AGENTS.md §E,且仅被前者引用;生产检索链路实际走
  knowledge_search_tool -> KnowledgeSearchService -> knowledge_schema 运行时探测。
- 删除上述两模块的单测,以及依赖 legacy 位置参数构造的
  tests/unit/service/test_knowledge_retrieval.py。
- app/service/knowledge_retrieval_service.py 整文件回退底座版本,
  移除 legacy 双构造与重复检索实现。
- docs/05-接口文档.md:客服画像候选改登记为 §8.5,恢复 §8.2 解析知识引用;
  既有 §8.1-§8.4 编号全部保持,修复此前出现两个 8.3 的问题。
- app/model/profile.py:current_customer_id 改为普通可空列映射,与
  alembic/baseline_generated.sql 及真实库一致;原 Computed 声明会让 ORM 把该列
  从 INSERT 中排除,与「必须显式写入」的实际 schema 不符。
- 新增 docs/客服Agent接入底座扩展说明_v1.md,供集成分支评审逐项确认。

验证:pytest tests/unit tests/contract -> 1275 passed, 2 skipped, 0 failed;
ruff check app tests tools alembic 通过;mypy app 通过(244 个源文件)。
2026-09-12 11:15:24 +08:00
张胜宇 e85989b344 merge: integrate latest qyqy_develop (auth login + RBAC read) into ZSY branch
Incremental merge on top of ef701c8, which already integrated the earlier qyqy base bbf623a. qyqy_develop only added commits on top of bbf623a, so this merge is conflict-free. Incoming: account/password login (POST /api/v1/auth/tokens), RBAC read-only query API, rate limit dependency, login test console and user management tools. Additive changes in app/main.py, requirements.txt and pyproject.toml from both sides are all preserved. ZSY side capabilities (visitor tokens, customer service agent, knowledge retrieval, profile projection) are unchanged.
2026-09-12 10:37:53 +08:00
lzf_0626 0358ea519f 投顾演示数据准备脚本:配好测评与归属,方案那步如实报告卡在证据来源
投顾线合并后 21 张 advisor_* 表全空,导致 published 返回 []、投顾 customer_ids 为空、
客户画像因缺测评返回 403。本脚本按依赖顺序补能自己造的那几样:

1. 产品目录:调 tools/import_hq_test_products.py 取真实行情(实测 19 个 ETF/LOF);
2. 客户风险测评:给 9001 补一条 C5、有效期一年(answers 里注明是演示数据);
3. 客户-投顾归属:9001 → 9020(assigned_at 往前留 5 秒,避开 DATETIME(0) 舍入陷阱)。

**方案那步不配** —— 缺的是证据来源不是技术:authoritative_tradable_products 是
fail closed 的(Missing or unverified evidence excludes a product),而造
advisor_product_suitability_reference 必须带 source_url 与 document_sha256,也就是真实的
销售适当性披露 / 基金合同文件(import_product_governance_reference.py 的两个 CSV,
不在仓库里,属环境数据)。脚本**不伪造这两个字段**(证据链红线),只检查并打印真正的解法。

实测:cust_t 的 /users/me/memory-profile 由 403 变 200(测评生效);admin 视角看 9020 的
customer_ids = ["9001"](归属生效);advisor 的 published 仍为 [](符合预期,尚无方案)。

⚠️ 顺带发现一处口径不一致:画像 fin_customer_profile.investor_type 是 C2,而新补的测评是
C5 —— 前者决定画像展示、后者决定适当性裁决,两者会同时出现在界面上。演示前需要统一
(要么把测评改成 C2,要么把画像也改成 C5)。
2026-09-12 10:34:28 +08:00
张胜宇 ef701c844c merge: integrate ZSY customer service and profile capabilities 2026-09-11 22:31:51 +08:00
lzf_0626 306a514316 修正登录测试台的探针:投顾那个按钮其实该是客户视角
/api/v1/advisor/recommendations/published 的语义是"**我(客户)自己**已发布的方案" ——
服务端按 `customer_id == 调用者 user_id` 过滤(权限码 `product-recommendation:read:self`
的 `:self` 正对应这一点),所以**投顾调它必然为空**:筛的是 customer_id = 投顾自己的 id。
把它标成 customer 角色,免得再用投顾账号点它、然后怀疑权限。

查证过程中确认了三处数据空缺(投顾合并后那 21 张 advisor_* 表是新建的):
- client_facing_content = 0 行(没有任何投顾方案)
- sys_customer_assignment = 0 行(投顾没绑定任何客户,故 customer_ids 为空)
- fin_risk_assessment 里没有 9001(客户画像的前置"完成开户风险测评问卷"未满足)
2026-09-11 22:28:32 +08:00
lzf_0626 636892c8b1 登录测试台支持投顾:快捷按钮 + 只读探针
- 快捷填充加 advisor_t / abc12345;
- 探针加「已发布投顾方案」(GET /api/v1/advisor/recommendations/published),按 advisor 角色启用。

实测同一接口的对照:投顾令牌 200(data: []),客户令牌 403 —— 权限边界正确。
2026-09-11 21:50:39 +08:00
lzf_0626 5018f11843 增加投顾(advisor)角色;修正登录测试的错误假设;修投顾带入的 2 处文档重号
## 投顾角色

投顾线合并后,bootstrap.py 有 10 处 allowed_roles 引用 advisor,
financial_nl2sql_service.py:272 还硬编码检查 {"advisor","operator","admin","super_admin"},
promotion_material_service.py:164 按 "advisor" in context.roles 走业务分支 ——
但 sys_role 里没有这个角色、sys_permission 里也没有投顾那 16 个权限码(种子只建到 9019)。
表现是所有投顾接口 403,而报错看起来像"权限配错了",实际是角色根本不存在。

- tools/grant_advisor_role.py:建 advisor 角色(id=9004,避开种子的 9001-9003 重建范围)
  + 16 个投顾权限(id 9020-9035)+ 授权(advisor 拿 10 项工作流、admin 补齐 16 项)。
  只增不删、可重复执行、带 --dry-run。
- tools/create_test_user.py:ROLE_IDS 加 advisor。
- 先跑 alembic upgrade head:补 21 张 advisor_* 表,业务表 68 → 89,审计通过。

权限划分:投顾工作流 10 项(read:self / generate:self / review / publish)给 advisor;
治理类 6 项(product-governance:*、profile-governance:*、asset-allocation:backtest)只给 admin。
review/publish 也给 advisor,与既有决策一致(此前已裁定不做双人复核)。

验证:advisor_t 登录 200,roles=['advisor'] data_scope=all 权限 10 项;
用它查 RBAC 清单得 403(没有 audit:read),边界正确。

⚠️ 与种子的冲突:seed_test_rbac.py 是 DELETE 重建语义,其
DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099 会清掉本脚本建的权限。
要把投顾权限固化,应并进 seed_test_rbac.py 的 PERMISSIONS 常量。

## 修正登录测试的一个错误假设

test_issued_token_actually_works_on_a_real_endpoint 原本用客户的
/users/me/memory-profile 验证令牌可用,投顾合并后它返回 403。追下去发现**与令牌无关**:
那个接口对客户有业务前置"请先完成开户风险测评问卷",而演示客户 9001 没有测评记录。
是我的测试选错了验证端点,把"业务前置未满足"误判成"令牌坏了"。

- 改用管理员令牌调 /api/v1/admin/roles(需要 audit:read,走完整鉴权链路),
  并补一条反向对照:不带令牌必须 401,否则那个 200 说明不了令牌有效。
- 把那个业务前置单独写成一个用例,让后来者一眼看到条件,而不是反复怀疑令牌。

过程里我先按控制台乱码猜了两次失败原因,都不对;最后把响应抓成 UTF-8 文件才看到真实
消息。教训记下:不要读乱码猜消息。

## 修投顾带入的 2 处文档重号

21-投顾Agent迁移TODO.md → 30-…、22-投顾Agent灰度与回滚操作手册.md → 31-…
(沿用 NL 那次让号的先例:既有文档更早、引用更多;且这两份新文档没有被任何地方引用。)
文档守卫:40 份无编号冲突。

验证:ruff 干净 / mypy 228 文件 0 错 / 文档守卫 40 份无冲突 /
unit+contract 1207 passed(0 failed)/ integration 99 passed / 业务表 89 张。
2026-09-11 21:49:46 +08:00
lzf_0626 c4a73b735e Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
# Conflicts:
#	app/main.py
2026-09-11 21:41:04 +08:00
lzf_0626 e29fce4012 新增登录测试台:一个记事本级别的前端,用来在浏览器里验证登录
tools/login_console.py —— 浏览器打开 http://127.0.0.1:8099 即可测。

与 chat_console.py 的关键差别:**它走真实登录接口**(POST /api/v1/auth/tokens)拿令牌,
而不是自己签。chat_console 是当初还没有登录接口时的权宜做法,这个测的是真链路。

页面能做的:
- 三个快捷填充按钮(客户 cust_t/123456、员工 risk_t/666666、管理员 admin_t/88888888);
- 登录后显示用户、角色标签、data_scope、令牌有效期(令牌只显示前 24 字符);
- 按角色列出可调的**只读**接口按钮(我的画像 / 风控概览 / 预警列表 / 角色清单 /
  管理视角看某人的身份),一眼核对"登录给的 roles"与"接口实际放行"是否一致;
- 把真实状态码与响应 JSON 原样摊在页面上,并写明 401 涵盖三种原因、429 是登录限流。

实现上不动 app/ 一个字节:内层用 create_app(),本进程只做两件事 —— 提供静态页、
把 /api/* 同源转发(httpx.ASGITransport 进程内调用,不起第二个服务)。同源转发避开 CORS,
也免得浏览器直连主服务;只监听 127.0.0.1,私钥始终留在服务端。

实测:页面 200;通过代理用 admin_t 登录拿到真实 JWT(sub=9003);错密码 401。

顺带说明一条边界变化:docs/24 当初拒绝"给底座加 dev token 端点",理由是那种端点等于把
**任意身份**开放给任何能访问服务的人。现在有了密码校验,浏览器拿令牌不再等于
"谁都能冒充任何人",那条顾虑已消除 —— 所以这个页面不是绕过安全设计,而是设计补齐后的正常用法。
2026-09-11 21:26:45 +08:00
lzf_0626 6812fbe317 B1:RBAC 只读查询接口(4 个)+ docs/05 登记 A035-A038
在此之前,权限只能靠脚本改,平台里**没有任何地方能看"谁能访问什么"**。
B1 先把"看得见"做出来:

- GET /api/v1/admin/roles                            角色清单 + 权限数 + 在用人数
- GET /api/v1/admin/roles/{role_code}                角色详情
- GET /api/v1/admin/roles/{role_code}/permissions    权限清单(按权限码排序,
                                                     便于与各 Service 的 require() 对照)
- GET /api/v1/admin/users/{user_id}/roles            某人**实际解析出来**的角色/权限/数据范围

几个刻意的决定:

1. 权限码复用 `audit:read` 而不新增 `rbac:read`:这份清单本身就是审计材料,且复用是零数据
   改动、立刻可用(新增权限码要先改 sys_permission,而它目前由 seed_test_rbac.py 以
   DELETE 重建语义管理)。将来要细分再加,不冲突。
2. `/users/{id}/roles` 直接复用 IdentityService.resolve,不自己拼 SQL —— 那正是请求进来时
   走的链路(status 检查、assigned_at/expires_at 时间窗、data_scope 取最高、客户分配)。
   自己写一遍必然漂移,而"这里查出的权限"与"实际能用的权限"不一致比没有这个接口更糟。
   测试里加了一条交叉验证:该接口的 roles/data_scope 必须与登录响应完全一致。
3. 停用账号返回**空权限集**而不是 404 —— 用户存在但拿不到权限,如实呈现比 404 更利于排障。
4. 角色详情与权限清单分开:权限为空的角色不该被误判成"角色不存在"。
5. 全部只读、不写审计(它们返回的就是审计材料本身),docs/05 §19 登记时审计列标"否"。

权限变更(提权/降权)仍无接口 —— docs/05 §19 已注明那不是遗漏,而是需要单独评审
(审计留痕 + 禁止自我提权 + 保护内置角色三条红线)。

另:确认了 load_context 对 sys_user_role.expires_at 是有过滤的(assigned_at<=now AND
(expires_at IS NULL OR expires_at>now)),我上一轮只读了半段 SQL 差点误报。

验证:ruff 干净 / mypy 185 文件 0 错 / 文档守卫 38 份无编号冲突 /
unit+contract 1140 passed / integration 98 passed(本批新增 8 个)。
2026-09-11 21:22:56 +08:00
lzf_0626 edd8c53c3a 修正 create_test_user 里写反的理由:sys_user_role 有唯一键,先清后插是为了支持改角色 2026-09-11 21:17:49 +08:00
Windows bbf623a464 merge: integrate advisor capabilities on latest qyqy base 2026-09-11 21:03:58 +08:00
lzf_0626 48513f3b97 docs/29 补第 0 步:转交前先确认对方环境有角色与密码(环境数据不随代码合并) 2026-09-11 20:50:59 +08:00
lzf_0626 b1429aa364 登录接口配套:加人工具 + 给组员的转交文档(docs/29)
- tools/create_test_user.py:一条命令建"可登录的测试账号"(用户 + 角色 + bcrypt 密码),
  并用 IdentityService.resolve 打印**真实解析结果**。sys_user / sys_user_role 没有 ORM
  模型、全靠裸 SQL,手写容易漏必填字段;更要紧的是 assigned_at 那个静默陷阱(见下)。
  重复执行同一 --id 是覆盖语义,改角色也用它。
- tools/set_user_password.py:hash_password 改从 auth_service 取,消除第二份实现。
- app/service/auth_service.py:新增 hash_password,与 verify_password 放在一起,
  让"写密码"和"校验密码"永远同一套算法。
- docs/29-Agent组员登录接口使用说明.md:给组员的转交文档(接口契约、加人步骤、
  前端接入示例、常见问题、当前边界)。

文档里专门写清三条最容易踩的:

1. 登录用 username 而不是用户 id —— 演示账号是 cust_t / risk_t / admin_t,
   不是 9001/9002/9003。这条不写明,联调时一定有人按 id 试。
2. sys_user_role.assigned_at 的 DATETIME(0) 毫秒舍入陷阱:落在未来会让账号
   "登录成功但 roles=()",**不报错**。create_test_user 统一往前留 5 秒。
3. roles / data_scope 只用于前端分流界面,不是权限凭证 —— 鉴权每次请求查库解析,
   所以权限变更立即生效,前端也不该拿它们做安全判断。

另:create_test_user 的 ON DUPLICATE KEY UPDATE 用 MySQL 8.0.19+ 的 `AS new` 别名语法,
避开已弃用的 VALUES()(实测本机 8.0.27 会打弃用警告)。

验证:文档守卫 38 份无编号冲突 / ruff 干净 / mypy 183 文件 0 错 /
登录集成测试 10 passed。
2026-09-11 20:50:41 +08:00
张胜宇 cbcf7c7444 feat: add milvus profile projection 2026-09-11 20:45:50 +08:00
lzf_0626 01ee68034d 新增登录接口:账号密码换访问令牌(POST /api/v1/auth/tokens)
背景:客户 / 员工 / 管理员三种身份此前无法区分。但区分逻辑其实早就完备 ——
bootstrap.py 里各 Agent 的 allowed_roles 一直是分开的(CustomerServiceAgent 只要
customer、RiskAgent 要 risk_operator/admin、PlatformProbeAgent 只要 admin),
唯独缺"怎么证明你是谁";sys_user.password_hash 字段也一直存在,只是全是占位符
(种子写 'x'、worker 身份写 !worker-only-no-password-login!),从没写过真实密码。

实现:
- app/service/auth_service.py:bcrypt 校验 + 签发只含 sub 的 JWT + 审计。令牌里只放 sub
  是刻意的:角色/权限/数据范围由 IdentityService 每次请求查库解析
  (identity_repository.load_context),权限变更因此立即生效,现有鉴权链路一行未改。
- app/api/controllers/auth.py + app/api/schemas/auth.py:POST /api/v1/auth/tokens,
  响应含 roles/data_scope 供前端决定进哪个界面(鉴权仍以库里实时数据为准)。
- tools/set_user_password.py:设密码(客户 123456 / 员工 666666 / 管理员 88888888)。
  ⚠️ 脚本与文档均标注"仅限演示环境",这三种弱口令上线前必须更换。
- pyproject / requirements 加 bcrypt(cryptography 只用于 JWT,不提供密码哈希)。

安全约定(逐条有实现与测试):失败不区分原因 —— 用户不存在/密码错/账号停用返回同一条
401,否则接口就成了账号枚举器;用户不存在时也跑一次 bcrypt 以抹掉时序差异;
成功与失败都写 interaction_audit(actor_id 可空正是为失败场景准备的);绝不记录密码。

过程中踩到一个自己挖的坑:给登录路由挂了通用的 enforce_rate_limit,而它声明依赖
build_request_context ⇒ 变成"要登录先登录",所有登录都 401。改为新增
enforce_login_rate_limit:按客户端 IP 独立限流(60 秒 10 次)、不依赖认证上下文。
集成测试据此调整:注入恒放行替身隔离跨用例的计数累积,同时保留一个恒超限用例验证闸门
确实会拦 —— 不能因为加了替身就把这道防线测丢。

接口登记:docs/05 §19 加 A034;并更新 §11 —— 那里原写"JWT 签发、刷新、注销由统一身份
认证模块负责,Agent 平台不重复实现",现注明平台只做登录这一步,刷新/注销仍归该模块。

验证:ruff 干净 / mypy 183 文件 0 错 / 文档守卫 37 份无重号(此前因 docs/21 重号失败)/
unit+contract 1140 passed / integration 90 passed。
2026-09-11 20:43:21 +08:00
lzf_0626 62501747a6 新增 sys_user 认证现状探查(判断"账号密码登录"可行性)
tools/probe_auth_state.py:只读统计 sys_user 的 status / user_type 分布与 password_hash
的格式特征(只输出前缀与长度,不输出完整值)。

结论(docs/evidence/auth-state.json):库里 5 个用户,password_hash 全是占位符 ——
4 个是 'x'(tools/seed_test_rbac.py 写入),1 个是 '!worker-only-no-password-login!'
(alembic 迁移写入的哨兵值,名字本身就表明"不用于密码登录")。
has_real_password_hash = false。

⇒ 在本项目做"账号密码换令牌"不是"加个端点"的事:没有密码可校验,等于要新建一套密码体系
(选算法 + 加依赖 + 定义写入流程 + 改上游数据),而 password_hash 是基线已有字段
(规则 4:不得改变既有业务含义)。docs/05 §11 也已明确该职责属统一身份认证模块。

顺带记录一个数据质量问题:user_type 取值不统一(employee / 员工 两种写法并存)。
2026-09-11 20:30:27 +08:00
Windows 4393355c3a docs: record legacy database migration rejection 2026-09-11 20:30:25 +08:00
Windows 5a69cc266c docs: record advisor rollout preparation 2026-09-11 20:28:49 +08:00
Windows f5dd5b8ba2 feat: add advisor rollout gate and rollback playbook 2026-09-11 20:28:21 +08:00
张胜宇 50a611274a feat: target memory sync event replay 2026-09-11 20:26:05 +08:00
lzf_0626 76e87a33a7 更新 AGENTS.md 的表数口径:51 → 68(场内 51 + 场外/推广 17)
合并袁聪那 17 张表后,本文件仍写"52 张含 alembic_version = 51 张业务表",与
tools/audit_schema.py 实测的 68 张对不上。改为写明构成,并指向
docs/28-场外与推广域数据表登记.md —— 否则下一个人看到 68 会误以为是有人绕过基线
偷偷建表(audit_schema 的期望值是从迁移动态推导的,不是手写清单)。

核验命令里的 .\.venv\Scripts\python.exe 一并改成 python,与本节"各用本机可用的那个"
的口径一致(架构师环境是 conda)。
2026-09-11 20:24:39 +08:00
lzf_0626 3029d0ca9f 更新 release-state 证据:画像工具白名单已补发(release 254 active) 2026-09-11 20:24:07 +08:00
lzf_0626 f09ea9e988 Merge origin/NL_develop:客服画像出口、知识管理三端点、合规语境与知识向量链路
NL 线(含其并入的袁聪场外/推广域)。唯一冲突是 .gitignore —— 双方都往同一区域加了
.workdir/,取对方版本(他的更完整,含 .tmp/ 与说明),顺带修掉我之前用
Add-Content -Encoding utf8 造成的编码混合(read 工具当时报 invalid UTF-8)。

合并后修的问题 —— 都不是"改别人业务逻辑",是让门禁能绿:

1. 缺运行依赖 python-docx。document_parser.py 解析 .docx 用它,但 requirements.txt 与
   pyproject.toml 都没声明 —— 别人环境跑知识入库会直接
   ModuleNotFoundError: No module named 'docx'。已补声明。
2. ruff 7 项:其中 tests/conftest.py 的 F821 Undefined name 'Path'(他的 tmp_path 修复
   写了字符串注解 "Path" 却漏 import,运行时不求值所以没炸,但 mypy/ruff 会抓)、
   tools/publish_customer_service_config.py 的 F841 inherited_keys 死变量(他改同 key
   覆盖、换成 inherited_only 后忘删旧的)、3 处 E501,另 2 项 ruff --fix 自动修复。
3. 合规基线种子未跑:integration 的 test_compliance_seed_mysql 4 个用例要求
   agent_negative_word 有 7 条 active 且已复核、agent_reply_template 覆盖 6 场景。
   跑 tools/seed_compliance_baseline.py(11 条 active 规则 / 6 个场景模板)后 80 passed。

验证:ruff 干净 / mypy 180 文件 0 错 / unit+contract 1140 passed /
integration 80 passed / 表数 68(alembic 已在 20260911_merge_risk_heads)。

唯一失败 tests/unit/repository/test_fund_readonly_contract.py 是双方一致的既有缺陷:
它断言 Base.metadata 里的 fin_* 表集合,而实测为空集 —— 即该测试依赖别的测试先导入模型的
副作用,单独跑必失败。NL 方也明确"不修不报",此处照办,仅记录。
2026-09-11 20:22:59 +08:00
张胜宇 5e848f5690 feat: wire neo4j projection into worker 2026-09-11 20:19:34 +08:00
lzf_0626 b5b068065f 修掉合并带入的 3 个 mypy 错(全在袁聪的场外邮件模块,各一行、不动逻辑)
1. offsite_document_recognition_adapter.py:788 —— 冗余 cast。
   `value in ("summary", ...)` 已经把类型收窄到那个字面量联合,cast 多余,删掉即可。
   该文件另有 13 处 cast,删这一个不影响 import。

2. offsite_fund_service.py:1203 —— dict 不变型。
   字面量里只有一个 bool,mypy 推断成 dict[str, bool | None],而 dict 是**不变型**,
   不是 dict[str, object] 的子类型。运行时本来就是合法值,加一行显式标注即可。

3. offsite_fund_service.py:1240 —— 标注宽于实际。
   `get_content()` 只定义在 EmailMessage 上,而调用方传的是
   BytesParser(policy=policy.default).parsebytes(...) 的返回值(本来就是 EmailMessage)。
   形参从基类 Message 改成 EmailMessage;import 同步替换(Message 仅此一处使用)。

三条都不影响运行:tests/unit/worker/test_offsite_mail_worker.py 的 6 个用例覆盖的正是
这条路径,修前修后都通过。修的意义在于——strict=true 下留着这 3 个错,mypy 在这个文件上
就失去价值,而 offsite_* 是刚合并进来、最需要类型检查兜底的新域。

验证:mypy 163 文件 0 错 / ruff 干净 / unit+contract 810 passed / integration 76 passed。

(另:本机 pytest 默认 basetemp 被权限占住的问题仍在,用重定向 TEMP 绕开,注意先建目录;
NL_develop 的 conftest 修复合并后可根治。)
2026-09-11 20:15:17 +08:00
lzf_0626 f60915b70b 补声明测试依赖 aiosqlite(合并袁聪线后暴露)
合并后 tests/unit/worker/test_offsite_mail_worker.py 的 6 个用例直接
ModuleNotFoundError: No module named 'aiosqlite' —— 它们走 SQLAlchemy 的 SQLite
内存库,而 aiosqlite 既不在 requirements.txt、也不在 pyproject 的 dev 依赖里
(dev 原先只有 pytest / pytest-asyncio / ruff / mypy)。

后果不是"本机缺个包":任何人 clone 下来、按文档装完依赖、跑测试,这 6 个用例必然
失败,而且报错指向 SQLAlchemy 内部(aiosqlite.py:480),看不出是缺依赖。
已加入 dev 依赖并注明用途。

同时记录一个本机环境问题(未改代码):pytest 的默认 basetemp
%TEMP%\pytest-of-Windows 被权限更高的会话占住,make_numbered_dir 扫不动 ⇒ 18 个
setup ERROR。NL_develop 的 conftest 修复(tmp_path 落到仓库内 .workdir/)能根治,
等合并;本次用重定向 $env:TEMP 临时绕开(--basetemp 无效,因为 pytest 仍会扫默认 root)。
另需注意 .workdir 目前不在 .gitignore 里,那套修复合并进来前要补上。

合并后现状:unit+contract 810 passed / integration 76 passed / ruff 干净 /
表数 68(alembic 已 upgrade 到位)/ mypy 3 个错(全在袁聪的 offsite_fund_service.py,
属真实类型问题,未擅自改别人的业务代码)。
2026-09-11 20:08:13 +08:00
张胜宇 6c605ec3c1 feat: add memory sync outbox worker 2026-09-11 20:07:55 +08:00
qyqy 58849addcc docs: 交付说明补第二轮批复(含 mypy 归因纠正);修正残留过期数字
上一轮我只更新了接手文档,**漏了交付说明**——它的末次更新(19:15)早于架构师 19:43 的
第二轮答复,导致它还写着已被推翻的结论。本次补齐:

- 新增 §0-B:架构师第二轮的 9 条批复逐条落地对照(含"mypy 归因纠正""docs/00 不改另立登记"
  "两件事归他""那 2 个失败按新引入对待")
- §5.2 整体重写:**认错并给出四组对照实验**(SQLAlchemy 2.0.34+1.14→173、2.0.52+1.14→6、
  2.0.52+1.20→3),说明真因是 SQLAlchemy **补丁版**而非"缺类型存根";环境已升级到
  2.0.52+1.20.2,mypy 184→3;并列出剩余 3 个错全在同事那条线的文件里
- §5.1 接受架构师口径:那 2 个失败**不是"双方共有的既有失败"**,应作为合并后**新引入的失败**跟踪
- §2 净差异分两段重算:本线增量 16 模块/17 测试/5 工具 vs 同事那条线 334 文件/11 迁移
- §6/§7 更新为第二轮后的状态(哪些已批准、哪些归他、哪些已落地)
- 修正残留过期数字:结构审计 51→68 张表、去掉"缺 SQLAlchemy 2.0 类型信息"的错误论断
- 章节号整理:两个同名 "## 0" → "## 0-A / ## 0-B"

文档守卫 35 份无重号;mypy 3 错;测试 1218 passed。
2026-09-11 20:06:23 +08:00
qyqy f7a666fb5a fix(env)+docs: 按架构师第二轮纠正 mypy 根因(环境版本);补场外/推广域表登记
架构师第二轮答复里有一条技术纠正是对的,我原归因错了:
- 我原判断"184 个 mypy 错是因为缺 SQLAlchemy 2.0 类型信息"——**方向对了一半、结论反了**。
  SQLAlchemy 2.0 自带 py.typed,根本不需要 sqlalchemy2-stubs(那是 1.4 用的)。
- 实测复现矩阵(决定性证据):
    SQLAlchemy 2.0.34 + mypy 1.14.1 → 173 错(我原来的环境)
    SQLAlchemy 2.0.34 + mypy 1.20.2 → 173 错
    SQLAlchemy 2.0.52 + mypy 1.14.1 →   6 错
    SQLAlchemy 2.0.52 + mypy 1.20.2 →   3 错
  ⇒ 主因是 SQLAlchemy 的**补丁版本**(173→6),mypy 版本是次因(6→3)。
- 已把环境升到 SQLAlchemy 2.0.52 + mypy 1.20.2(均在 pyproject 约束内),
  mypy 从 184 降到 **3 个错**(剩下的 3 个全在同事的 offsite 文件里,非本线代码)。
- 没有把 sqlalchemy2-stubs 写进依赖(采纳架构师明确要求)。
- 交接文档已重写该条,并留下"两个反面教训":不要装 1.4 的存根包;补丁版差异会造成量级差异,
  若要门禁稳定需把 SQLAlchemy 钉到具体补丁版(属公共约定,未擅自改)。

按架构师裁决补文档(他裁定:不动 docs/00,另立登记):
- 新增 docs/28-场外与推广域数据表登记.md:17 张表逐表登记(表名/来源迁移/归属域/当前行数),
  并做规则 8 的**两向边界核对**——场内代码零引用这 17 张表(config.py 里的 offsite_ 只是配置项名)、
  场外代码零写场内交易表
- docs/08 审计口径更新为「场内 51 + 场外/推广 17 = 68」,并新增"第四种坏状态"
  (alembic_version 已指向新 revision 但表没建)的处置说明
- 澄清一个易误读点:audit_schema.py 的期望集合是**动态推导**的(读 baseline_generated.sql
  + 扫描 alembic/versions/*.py),表数 51→68 是自动结果,**没有人手工改期望值**;
  代价是只增不减(DROP 表会报 missing 假失败)

测试:3 failed(1 既有 + 2 环境相关)/ 1218 passed;文档守卫 35 份无重号;
mypy 3 错(180 文件);结构审计 68 张表通过。
2026-09-11 20:00:56 +08:00
lzf_0626 9957ddc191 Merge pull request 'NL_develop 评审答复与画像白名单补发脚本' (#6) from qyqy_develop_1 into qyqy_develop
合并 qyqy_develop_1 第四轮工作:NL_develop 评审答复、画像工具白名单补发脚本、docs/08 §8。
2026-09-11 19:58:29 +08:00
张胜宇 f167390a8a feat: add neo4j profile projection adapter 2026-09-11 19:54:46 +08:00
lzf_0626 8268646b0c 答复文档补充 §8:两个既有发布脚本会丢提示词(写补发脚本时发现)
publish_customer_service_config.py 与 publish_risk_agent_config.py 的
active_config_items() 都自己写 SQL、只查 platform_config_item,而
ConfigReleaseService.effective_snapshot() 要读全部三张受管表。用它们发版会把生效版本里的
提示词一起清掉 —— 且没有任何声音:_warn_dropped_items() 只告警不阻断激活,Agent 侧
_chitchat_prompt 有兜底会回落代码默认值,功能看着正常(与 release 174 那次事故同型)。

已在答复文档里建议改用 effective_snapshot(),并列出三个细节:同 key 覆盖(对方已修)、
JSON 列必须归一化(SELECT * 读出来可能是字符串,不解析会静默走进"没有该 key"分支)、
提示词搬家要重分配 version 且按 PromptPayload 字段挑(漏 input_schema/output_schema 即丢)。

另提醒时序:admin 端校验「配置 ⊆ 代码 allowed_tools」,画像工具那一版必须等代码合并后才能发,
否则 422 且报错只有"配置超出 Agent 工具上限"。
2026-09-11 19:52:08 +08:00
Windows fbb1171a45 feat: add quote sync command 2026-09-11 19:51:51 +08:00
lzf_0626 6c09cdecbd 新增补发画像工具白名单的脚本(dry-run + 合并前防呆)
tools/publish_profile_tool_whitelist.py:把 query_customer_profile 加进
customer_service:faq,供 NL_develop 合并后在本环境补发配置。

关键设计都来自这轮评审核实到的事实:

1. 继承走 ConfigReleaseService.effective_snapshot(),一次拿全三张受管表
   (platform_config_item / prompt_template_version / model_routing_rule)。
   既有的两个 agent_tools 发布脚本都绕开它、自己写 SQL 只查第一张 —— 用它们发版
   会把 201 里那条客服闲聊提示词一起清掉,而 Agent 侧有兜底、功能看着正常、没有告警
   (这个事故在本项目真实发生过一次)。
2. 同 key 覆盖:继承项里的 customer_service:faq 必须被本次新值覆盖,否则旧值
   ["search_knowledge"] 会被 admin 端校验拦下、整次发布失败。
3. 提示词搬运重分配 version(唯一键含 version),并按 PromptPayload 的 9 个字段挑字段、
   归一化 input_schema/output_schema —— 漏带就是静默丢失。
4. 前置防呆:admin 端校验「配置 ⊆ 代码 allowed_tools」(admin_service.py:219-220),
   合并前跑必然 422,而报错只有"配置超出 Agent 工具上限"。脚本先查代码上限:
   dry-run 给警告后继续预览,正式跑直接中止。
5. JSON 列归一化:SELECT * 读出来的 JSON 列可能是字符串,不解析会静默走进
   "生效版本里没有该 key"的分支(本脚本第一版就是这么错的,诊断打印才定位到)。

实测:--dry-run 打印 9 条配置项 + 1 条提示词(v2→v3 重分配)与唯一变化;
正式跑因代码尚未合并而中止,未产生任何写入。
2026-09-11 19:51:44 +08:00
Windows 5de5c6c6b5 docs: record dual-source quote acceptance 2026-09-11 19:49:31 +08:00
Windows 2376585898 feat: add dual-source quote health orchestration 2026-09-11 19:47:14 +08:00
lzf_0626 d28ccdd88c 答复 NL_develop 第二轮:批准整改,纠正 mypy 归因,裁定 docs/00 不动
产出 docs/NL_develop评审回复-架构师答复.md(可直接转给对方)。三条核实结论:

1. docs/21 在我这边本来就重号(21-JWT密钥管理与轮换.md + 21-风控业务第二版迁移
   清单.md),tools/check_authoritative_docs.py 当前就是失败的 —— 我此前只跑
   ruff/mypy/pytest,没跑到文档守卫。对方的让号顺手修好了这个既有故障,
   故批准 docs/26 与 docs/27 两处让号。

2. 对方的 mypy 归因需要纠正。他说主因是缺 SQLAlchemy 2.0 类型信息;我这边
   mypy 1.20.2 + SQLAlchemy 2.0.52 + 未装 sqlalchemy2-stubs → 0 错,而
   pyproject.toml 约束是 sqlalchemy>=2.0,<3 / mypy>=1.14,<2。SQLAlchemy 2.0 自带
   py.typed,sqlalchemy2-stubs 是给 1.4 用的,装了反而按 1.4 的 API 报错(他自己也
   观察到"还会换一批新错")。真实根因是他的 .venv 没满足 pyproject 约束。

3. 他的迁移警告对我不适用。我这边 alembic current = heads = 20260911_risk_rule_index
   (单一 head,只有我自己的迁移),schema audit 报 51 business tables —— 即"表没建、
   版本号也没跑",与他那边"版本号跑了、表没建"是两种不同的坏状态。合并后我必须补跑
   alembic upgrade heads。

裁定:
- 补发配置(customer_service:faq 加 query_customer_profile)归我做;
- memory_sync_outbox / GraphProjectionWorker 消费端归我(生产者在他那边);
- 两处让号批准;
- docs/00 不动,另立文档登记场外/推广域 17 张表,并更新 docs/08 审计基线口径
  (依据 AGENTS.md 规则 8:场外基金运营流程独立,不得写入场内交易表)。
2026-09-11 19:40:17 +08:00
zhangshy beb67f0a26 优化奶龙字段语义理解 2026-09-11 19:37:26 +08:00
张胜宇 5777f0f86e feat: include memory sources in profile sync events 2026-09-11 19:37:18 +08:00
qyqy f1b8cb859a docs: 新增《接手文档 · NL_develop 这条线(给架构师)》
面向"接手维护"而非"review"的第三份文档,与另两份分工明确:
- 交付说明 → review 用(我动了你什么、为什么)
- 评审意见回复 → 对照你的评审意见
- **本文** → 接手维护用(起点在哪、有哪些坑、哪些待裁决)
- 给下一位开发者 → docs/superpowers/handoff/…

内容要点:
- §1 三条最要紧的结论:① 两台机器不是同一套环境(附三处实测证据)并由此立下两条硬纪律
  (环境相关结论必须带环境限定、禁止硬编码 Milvus 字段名);② **表数已从 51 变 68**
  (同事 11 个迁移新建 17 张 offsite_*/promotion_* 表),而 docs/00 基线与 docs/02 未更新
  —— 已提示、未擅自改不可变基线;③ 测试 3 failed 均非代码缺陷(附证据)
- §2 环境口径(含 Docker Desktop 不常驻、mypy 不可比的实测根因)
- §3 分支与提交 + 两个可回退的备份分支
- §4 交付清单(8 项,含状态与位置)
- §5 必须知道的 5 个坑(Milvus 读一致性与字节截断、config_release 整版本替换、
  两个 outbox 不能混、审计表无 agent_type 列)
- §6 待你裁决的 3 件事(白名单补发归属、接线归属、两处文档让号)
- §7 建议你拍板的两件事(文档体系口径、docs/00 是否补 17 张新表)
- §8 接手后先跑的 5 条自检命令
- §9 四份文档的分工表(防止读串)
2026-09-11 19:32:19 +08:00