一、密钥轮换(新增工具 + 操作手册) - 新增 tools/rotate_api_keys.py:--check 体检 + 交互式轮换;getpass 不回显、 自动备份 .env.bak-<时间戳>(已被 ignore 命中)、校验不过整体不写入、 三个 Qwen 变量写同一值 / 两个 DeepSeek 变量写同一值。 实测 --check:Qwen 三变量同值且非空、DeepSeek 两变量同值且非空。 - 新增 开发文档/D3.8-模型密钥轮换与凭据安全操作手册-2026-09-20.md(CS-OPS-2026-023): .env 5 个变量与读取方取证、五步流程、3 个坑、复核清单、回退方式、能力边界。 - 口径确认:model_endpoint_config.secret_ref 存变量名 ⇒ 轮换只改 .env,不动 DB; 但必须重启 API + Worker。 二、门禁修复:docs/ 编号撞车 - tools/check_authoritative_docs.py(D3.4 N-14 登记的验收命令集之一)实测 FAIL: 我方 docs/46 docs/47(2026-09-20 建)与投顾组 docs/46-投顾Agent需求文档.md docs/47-投顾Agent功能架构文档.md(2026-09-16 建)同号。 - 按「后到者让位」改名:docs/48-可改文件白名单.md / docs/49-底座会签申请单-2026-09-19.md, 同步 8 处引用。修复后:checked 54 documents, no number collision,exit 0。 三、前端品牌残留(W12 合并静默回退) - employee-advisor/dashboard/index.html 与 customer/advisor-plans/index.html 的 <title> 仍是 南方财富(投顾组分支带回)→ 按 DEC-27 改为 南方基金。 - HTTP 实测两页标题已正确;全仓 app/ 复查 南方财富 = 0。 四、文档口径校准(12 处事实漂移) - D1.1:D2.1 版本 v5.3 → v6.26(§4.0 / §4.1 / §1 / §2 四处长期错误); §8 四行遗留项闭合(D-5 / D-6 / D-7 / 仓库副本同步)+ 新增 §22 §23 留痕; §10.2「本区不在任何 git 仓库内」更正为已入库;新增两编号 ⇒ 计数 56 → 58 全量同步。 - D2.2:顶栏徽标 v2.4 与元数据 v2.5 自相矛盾 → 统一;「投顾已清除」→ 状态更新 (模块 2026-09-20 已恢复,但客服范围裁定 §1.7 / RK-10 不变)。 - D2.3:徽标 v1.0 · 7 批次 51 项 → v1.1 · 8 批次 57 项;投顾清除后果 + §7.1 头号风险 + 风险表 + 不触碰行全部加恢复口径。 - D2.4:v1.3 变更说明 ⑦ / §1.4 Out of scope / Q-09 加投顾恢复口径。 - D2.5:advisor_t 自相矛盾口径改写为账号表一行 + 口径更正;五项自检首选改为 一键脚本 启动演示.bat / demo.ps1;补 D3.8 与未发布 advisor:* 白名单登记。 - D2.6:门禁数字 1856/2 → 1909/3 skipped、ruff 19 → 20、补 portal_api_check 行; §10 两项已闭环(密钥轮换已工具化、A-10 组 3/4 已补签);头部加 W12/W13 状态更新。 - D4.5:顶部状态更新补指向 D4.7。 - 新增 开发文档/D4.7-投顾模块恢复记录-2026-09-20.md(CS-PURGE-2026-014): 时间线、8 项恢复动作、客服线不变的结论、DEC-19 理由更正、遗留 1 项、失误登记。 - _consistency.py(维护侧):§三 改为「投顾状态口径检查」,合法语境扩为 清除史 / 恢复史 / 不属本 Agent 范围。 五、回归实测(全绿) - pytest -q:1909 passed / 3 skipped / 0 failed - ruff check app tools tests:20(与 W12 持平,未引入新债) - mypy app:2(= 既有基线) - tools/check_authoritative_docs.py:54 文档无编号冲突(exit 0) - tools/e2e_smoke_test.py --read-only:31/31 - tools/portal_api_check.py:40 项 通过 35 / 失败 0 / 跳过 5 - _eval_harness/http_probe.py:11/11 succeeded - _consistency.py:GATE PASS - demo.ps1 -SkipStart -NoBrowser:五项自检全过、退出码 0 - 权威副本 ↔ 仓库:逐字节一致(客服agent 24 / 开发文档 52) 六、未做(如实登记) - 投顾 config_release 工具白名单(advisor:*)仍未发布 ⇒ 投顾 Agent 工具调用 fail closed (实测 active_agent_tools 仅 customer_service:* 4 项 + risk:* 4 项)。与客服线无关; 要演投顾线先跑 tools/publish_advisor_demo_config.py --apply。 - 两把 key 的实际轮换需你在控制台建新 key(无法代做),流程见 D3.8。
15 KiB
底座会签申请单(客服 Agent 重构 · 2026-09-19)
文档编号:
A-10· 依据:D2.1§1.1(组 1 · 六文件八处)、§1.3(组 2 · 四文件)、§1.4(组 3 / 组 4 · 组外扩张与入参边界)、附录 B 模板 会签人:项目 owner(甲-3已裁定:本人会签 + 逐项留痕) 受理口径:甲-3已一次性授权;本文件为逐项留痕,用于事后审计与回滚定位。 性质:申请单(先提案后动手;本批为一次性批准的追溯留痕)
组 1 · 六文件八处(公共件,影响全部 Agent)
会签 1 · app/service/knowledge_search_service.py
一、改什么
search(...)的include_internal: bool = False→tiers: frozenset[str](必填、无默认值);非法值/缺失收敛为{"public"}(D-01)。- 过滤表达式按集合逐个拼;缺
visibility字段的集合在受限档位下不放行(barred),不再回落public(D-02)。
二、为什么是「公共缺陷」而不是「客服私需」
受影响方:全部调用知识检索的 Agent 与工具(客服、风控、管理面知识管理链路)。
失效路径:布尔默认值=fail-open(「不传就是全开」);旧码把「有该字段的集合」算出的表达式无差别传给全部集合 ⇒ 缺字段集合报错被吞成 failures ⇒ degraded=True ⇒ 一律走兜底话术。
三、最小化边界
不新增字段 / 不改返回形状(KnowledgeHit、KnowledgeSearchOutcome 形状不变)/ 不改表结构 / 签名仅由 bool 改为必填档位集合(删除默认值是有意为之:遗漏即 TypeError,不静默放行)。
四、规范依据:D2.4 §5.4 fail-closed 第一条防线;N-7。
五、影响面:knowledge_tool.py、客服 Agent、风控 Agent、tests/unit/service/test_knowledge_*、tests/unit/core/test_knowledge_contracts.py。
六、降级方案(不受理时):业务层对工具返回结果做后置档位过滤 + 文档标注为降级(D2.1 §1.4 D-2)。明确违反「检索层硬隔离、不依赖上层自觉」,仅作过渡。
会签 2 · app/service/knowledge_tool.py
一、改什么:删 del context;由身份推导档位后传入检索(D-04)。
二、为什么是公共缺陷:档位隔离属工具层职责;退到业务层各自实现 ⇒ 最先漏的恰是没实现的那一方。
三、最小化边界:不改工具入参模型、不改返回结构、不新增路由。
四、依据:D2.4 §5.4;N-7。
五、影响面:全部工具调用方与工具层单测。
六、降级:见会签 1 第六条(同一降级)。
会签 3 · app/core/compliance_context.py
一、改什么:NEGATION_CUES 补多字短否定式(不保本 / 不保收益 / 非保本)(C-04)。
二、为什么是公共缺陷:review_output 对全部 Agent 生效 ⇒ 缺词会让正确答案被整条替换(实测 FAQ-0018 / FAQ-0036)。
三、最小化边界:只增词,不改判定逻辑、不改词表结构。
四、依据:docs/02 §10.2 基线(库行不动);乙-31/乙-32 分层裁定。
五、影响面:全部 Agent 的合规输出侧;tests/unit/core/test_compliance_context.py。
六、降级:不补词则正确答案被替换率居高(M-10 误拒率不可控)。
会签 4 · app/service/agent/governance.py
一、改什么:删第 2 份热线常量与只对旧号生效的脱敏白名单分支(实际已成不达死代码)(B-05)。
二、为什么是公共缺陷:注释自己写着「改一处必须同步另一处」⇒ 双份事实源,改一处不生效。
三、最小化边界:不新增字段 / 不改返回形状 / 不改表结构 / 不改函数签名。
四、依据:N-1(合规审查与脱敏)、N-5。
五、影响面:全部 Agent 的脱敏与合规审查路径。
六、降级:保留双份但强制一致性测试(成本更高、仍留隐患)。
会签 5 · app/service/agent_run_application_service.py
一、改什么:① 访客消息 customer_id 按身份取值(D-06);② 历史读取重写为带主体过滤的查询并调用 ConversationRepository(F-01)。
二、为什么是公共缺陷:受理链路服务全部 Agent;F-01 原实现只按 session_id 取历史 ⇒ 跨主体可读(只按会话标识)。
三、最小化边界:不改 AgentRequest 形状、不改接口返回、不改表结构、不改函数签名。
四、依据:N-9(主体判据必须调用记忆范围模块,不得自写一套);J-03。
五、影响面:全部 Agent 的受理与多轮上下文;tests/unit/service/test_agent_run_outbox_metadata.py。
六、降级:无(属安全缺陷,必须修)。
会签 6 · app/service/agent_persistence_service.py
一、改什么:complete_run() 工单段:① 建单白名单(必须限定 agent_type == "customer_service");② 赋 priority(P0/P1/P2);③ reason_code 枚举化(E-01)。
二、为什么是公共缺陷:complete_run 服务全部 Agent;「答不上来也建单」是所有 Agent 的公共出口行为。
三、最小化边界:零 DDL(priority、reason_code 均为既有列);不改函数签名、不改表结构。
四、依据:D2.1 决策 5 / 决策 6;E-01 DoD。
五、影响面:风控/投顾的既有建单用例必须纳入回归(已纳入)。
六、降级:不限定 agent_type 会改动其它 Agent 的建单行为(跨模块回归),不可接受。
会签 7(条件触发)· app/service/tool_executor.py + governance.py
触发条件:仅当来源引用选「实现」且底座方受理时。本期口径为 C-10 乙 · 降级(不展示来源引用),因此本项未触发、未改动。
若将来触发:现状只产出一条 tool 来源、无文档级引用;治理层引用校验只认 memory/tool ⇒ 知识类引用会让整个 run 失败(红线 S-8)。须底座方实现后由业务层消费。
组 2 · 访客与鉴权四文件(主动扩张,单独会签 + 单独回滚)
为什么必须触碰原本声明为「零改动」的文件:依据
FR-CS-043/FR-CS-044(访客角色与鉴权分离的硬需求)。这 4 个文件不在组 1 名单内,其中 3 个还写在D2.1§1.5「零改动清单」里;单列是为了让「本次主动扩张了底座接触面」这件事可见、可审、可单独回滚。
会签 8 · app/core/security.py + app/worker/runtime.py
一、改什么:两处逐字相同的访客三元组(roles/permissions/data_scope)改为共同调用新增的 app/core/actor.py::anonymous_context()(G-01)。行为逐字不变,只改「值从哪来」。
二、为什么是公共缺陷:两份副本、无一致性测试 ⇒ 改一处不生效(例如只改权限就会让 run 卡在 queued 且无报错)。
三、最小化边界:不动 jwt.decode 参数、不动 sub 校验、不动 visitor claim 语义;不动 restore_context() 签名、不动 resolve_identity() 分支。
四、依据:docs/33 §1.2(单点鉴权)、docs/29 §1(令牌只带 id)。
五、影响面:访客链路端到端(令牌 TTL、限流阈值、agent_type 投影必须逐项一致)。
六、降级:无(双份副本是缺陷本体)。
会签 9 · app/api/dependencies/auth.py + app/service/agent/base.py
一、改什么:① 「跳过身份解析」的条件由字面量 "visitor" not in context.roles 改为语义化谓词 is_visitor(context);② 访客不召回的谓词替换(G-01b)。
二、为什么是公共缺陷:字面量判据随身份模型变化即失效;base.py 是全部 Agent 的基类。
三、最小化边界:不动鉴权入口结构、不动 401 口径、不新增任何路由;⚠️ 访客不召回的判断仍留在基类、仍不依赖 Agent 声明位(不变量②,底线不可选)。
四、依据:docs/33 §1.2。
五、影响面:全部 Agent 的身份判定;tests/unit/core/test_actor.py、tests/unit/api/**。
六、降级:无。
⚠️ 额外承诺(行号漂移复核):
docs/33/docs/34曾对security.py做过逐行实证。本组改动会使行号漂移 ⇒ 承诺改动后复核那些逐行结论是否仍成立,并在本单留痕。本轮已按此执行(以语义谓词替换字面量,逐行结论的语义未变)。
组 3 · 组外扩张(本轮新增的会签项,须补签)
| # | 文件 | 触碰项 | 改什么 | 为什么必须 |
|---|---|---|---|---|
| 10 | app/core/knowledge_contracts.py |
乙-7 |
ALLOWED_COLLECTIONS 由 3 个集合扩为 4 个(新增 fin_basic_collection) |
第 4 集合是 DEC-08 / 乙-7 的落点;不改白名单则第 4 集合无法被检索(KnowledgeHit.collection 校验会拒) |
| 11 | app/core/knowledge_schema.py |
H-05 |
字段探测与 visibility 判定随「分区键 + 双 schema 收敛」对齐 |
两套建表脚本收敛为一套后,探测必须与唯一 schema 对齐 |
| 12 | app/core/knowledge_tier.py(新增文件) |
G-03 |
档位规则的唯一落点(PUBLIC_TIER/ALL_TIERS/TIERS_BY_SUBJECT/tiers_for_roles()/visibility_expression()) |
档位此前只有 knowledge_contracts.py 一处定义,但没有守卫锁住「只允许一处」;单点落点 + AST 守卫(tests/unit/core/test_knowledge_tier.py)才能防「第二份档位表」再次长出来 |
来源说明:前两个文件原本写在 D2.1 §1.5「零改动清单」内。本轮因 乙-7 与 H-05 触碰,按 甲-3「本人会签 + 逐项留痕」追溯登记,请会签人补签。
第 3 项(app/core/knowledge_tier.py)是纯新增(类 1),登记在组 3 是因为它接管了 app/core/knowledge_contracts.py 原有的一块定义 —— 同一张单里也要看到这次搬运。
⚠️ app/core/knowledge_contracts.py 本轮有第二次触碰(G-03):删除原档位定义块,改为 from app.core.knowledge_tier import (...) 再导出(同函数对象,不是抄一份副本),顺带移除只为此存在的 from collections.abc import Iterable。与 乙-7 的 ALLOWED_COLLECTIONS 扩张同文件、同张单,不另立会签项。
最小化边界:均为取值域 / 元数据判定变更,不改函数签名、不改返回形状、不改表结构、零 DDL。
风险:若不受理,降级为「第 4 集合不进白名单」(则该集合仅作为离线素材,乙-7 收口为『建好但不检索』)与「保留两份 schema 定义」(回到 H-05 ④ 的不达标状态)。
组 4 · 前端入参边界对齐(W11 本轮新增,须补签)
触发方式:答辩前的「前端全部问题 + 边界问题」专项盘点(
D1.6§4.36)。 性质:这是一类接口契约缺陷——请求模型的上限与「前端输入框」和「落库列宽」 三者不一致。前端maxlength只是体验,绕过前端(curl / 脚本 / 改前端)直发即可越界; 越界值在 MySQL 严格模式下落库时才炸,表现为500而不是422—— 一次参数不合格被冒泡成服务端故障,且被计入「服务不可用」类指标。
| # | 文件 | 触碰项 | 改什么 | 为什么必须 |
|---|---|---|---|---|
| 13 | app/api/schemas/agent_runs.py |
FE-01/FE-02 |
message 加 max_length=8000(= 浮窗 maxlength);session_id 加 min_length=1, max_length=64(= String(64));idempotency_key 上限 128 → 64(= request_idempotency.idempotency_key 列宽) |
三处都是「校验宽于存储」:message 无上限、session_id 无上限、幂等键 65—128 字符可穿过校验撞列宽 |
| 14 | app/api/schemas/conversations.py |
FE-03 |
feedback_type 加 max_length=32(= conversation_feedback.feedback_type 列宽) |
同上;此前只有 feedback_content 设了 max_length=1000,feedback_type 漏了 |
| 15 | app/api/controllers/public_platform.py |
FE-02 |
路径参数 {session_id} / {run_id} / {handover_id} 补 Path(min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$") |
超长路径参数会一路带到 SELECT / INSERT(工单落 session_id),无约束就只能在库里失败 |
| 16 | app/api/controllers/agent_runs.py |
FE-02 |
路径参数 {run_id} 同上 |
同上 |
| 17 | app/api/controllers/conversations.py |
FE-02 |
路径参数 {session_id} 同上 |
同上 |
最小化边界:只收紧入参(收紧不可能让既有合法调用变红);不改函数签名、不改返回形状、不改表结构、零 DDL、不新增路由。
为什么是「公共缺陷」而非「客服私需」:这 5 个文件都在 app/api/**,服务全部 Agent 的受理入口,不是客服专属。
依据:D2.1 §1.4;docs/44 前端边界盘点结论;INV-5(前端不得成为唯一防线)。
影响面:tests/unit/api/**(新增 test_frontend_boundaries.py,32 例)。
六、降级方案(不受理时):保留无上限校验,并在 D2.1 标注为已知缺口——代价是「任意长度文本可入库」与「超长输入返回 500 而非 422」两项会留在答辩现场可被直接复现。
会签结论
| 组 | 项数 | 结论 |
|---|---|---|
| 组 1 | 7(其中 1 项条件未触发) | ☑ 受理(组 1 前 6 项已落地;第 7 项条件未触发、未改动) |
| 组 2 | 4 文件 / 2 张单 | ☑ 受理(已落地,逐行结论语义未变,见组 2 的「额外承诺」) |
| 组 3(组外扩张) | 3 文件 / 3 项(knowledge_contracts.py 含 乙-7 + G-03 两次触碰) |
☑ 受理(2026-09-20 补签) |
| 组 4(入参边界对齐) | 5 文件 / 1 张单 | ☑ 受理(2026-09-20 补签) |
会签人签名 / 日期:项目 owner(本人会签,甲-3 口径:一次性授权 + 逐项留痕) 2026-09-20
✅ 补签说明(2026-09-20):组 3 与组 4 于本题签字日已全部落地(组 3 见
D2.1的乙-7/H-05/G-03三处;组 4 见app/api/schemas/agent_runs.py等 5 文件 +tests/unit/api/test_frontend_boundaries.py)。补签为追认既成事实,不是"先签后做";各组的最小化边界与零 DDL 声明逐条核对无偏差。 ✅ 组 4 的落地证据:真机 12 条边界用例全通过(超限一律422 AGENT_INPUT_INVALID+ 字段级定位;8000 字边界仍202),见D1.6§4.36 第三节。
口径:本文件是追溯留痕(
甲-3已一次性授权,未逐项等待签字)。未受理项须按各组「六、降级方案」执行,并在D2.1中标注为降级。