Commit Graph
22 Commits
Author SHA1 Message Date
张胜宇 c3d57fbf3f feat(cs)+docs: 修掉 P1 错分「风险测评结果」—— 画像问答改由受控工具作答(W15)
背景(用户提问触发):
  §1.2.1「客户能看本人的持仓/交易/账户/画像与风评」与 §1.4.5 P1
  「账户与个人数据(含风险测评结果)Agent 无权限读取」读起来互相矛盾。

实测根因(两处):
  1) route_message() 在画像分支之前,且 P1_KEYWORDS 含裸词「风险测评结果」
     ⇒「我的风险等级是多少」走画像作答,
       「我的风险测评结果是什么」被降级成「无法读取本人账户数据」
       —— 同一诉求两种结论,属「能答而不答」(H-03 同类)。
  2) 画像词在前、账户词在后的混问法漏网
     (「我的风险测评结果和持仓一起给我」落 P3 ⇒ 只答画像、静默忽略账户诉求)。

依据(决定性):D2.2 §1.7 第 21 项「画像问答字段直返」;
  D3.1 §0.3 术语表「画像问答属客服能力,与持仓查询严格区分」。

代码:
  - app/core/customer_service_rules.py:P1_KEYWORDS 移除裸词 + 口径说明;
    P1_PATTERNS 新增混问法守卫(第一人称 + 画像词 + 并列连词 + 账户词)。
  - customer_service.py / profile_projection.py:补「投影层白名单 ⊇ 客服对话
    渲染集」口径(total_asset / behavior_score / risk_tags 刻意不陈述,
    渲染它们等于用画像工具绕过 P1)。

守卫:
  - test_customer_service_rules.py:RT-004 → None;新增 RT-004b → P1;
    SAFETY_CASES 由字面区间改显式名单(RT-004b 字母后缀会落区间外被静默漏掉)。
  - test_customer_service_agent.py:画像端到端 3 条 + 混问法反向守卫。

安全不降:答案只来自 query_customer_profile(self 作用域 + 字段白名单 + 工具
  审计),查不到失败关闭、绝不猜等级;P0/P2 未动、P1 其余字面未动;
  混问法仍走 P1;访客问画像仍引导登录。

文档:D2.2 v2.5→v2.6 / D3.1 v2.4→v2.5 / D2.6 更正 / D4.6 追加 §3(不改正文)
  / D1.1 §24 + 版本位 / D1.6 §4.41 修正误记 + §4.42 / D2.1 v6.29。

实测:pytest 1914 passed / 3 skipped / 0 failed(+5);ruff 20(无新债);
  check_authoritative_docs 54 文档无冲突;_consistency GATE PASS;
  http_probe 11/11;定向真机复验 9/9;portal_api_check 35/0/5;
  e2e_smoke 31/31;fe_boundary 12/12;demo.ps1 五项自检全过。
2026-09-20 16:22:02 +08:00
张胜宇 5d0becb67d 客服 Agent 重构收口:五出口决策链 + 知识库档位隔离 + 前端入参边界(答辩演示版本)
一、客服 Agent 智能增强(正面回应"不智能、动不动就转人工")
- 决策链由 2 个出口扩到 5 个:E1 澄清 / E2 计算型 / E3 知识直返 / E4 证据约束生成 / E5 分级回退
- 转人工从"默认动作"降为最后一档 E5c,只保留 4 类白名单:
  P0 反诈 / P1 账户与个人数据 / P2 写操作与争议 / 用户明确要求人工
- 46 条金标实测(修复前 → 修复后):
  转人工率 43.5% → 10.9%;出口准确率 45.7% → 100%;事实正确率 69.6% → 100%
  禁忌违反 1 → 0;档位越权 / 无出处数字 / 误拒 四项零容忍全 0
- 安全不变量 INV-1~INV-5;零容忍规则未删,改的是挂载点
  (输出侧字面黑名单 → 检索层档位隔离 + 判定层合规词表 + 输出守护)

二、知识库:档位单点化与物理隔离
- 新增 app/core/knowledge_tier.py 作为档位规则唯一落点(G-03),
  knowledge_contracts.py 原定义块改为显式再导出(X as X,非副本)
- 档位过滤由 bool 默认值(fail-open)改为 tiers 必填集合(缺参即 TypeError)
- Milvus 侧四集合按 visibility 分区键物理隔离;双 schema 收敛为一套
- 新增 app/core/actor.py:访客三元组与匿名判定的唯一构造/判定点(G-01/G-01b)
- 新增 app/core/fund_fee_rules.py:费率计算纯函数

三、前端入参边界对齐(本轮 W11 新修,4 处"校验宽于存储")
- message 加 max_length=8000(与浮窗 widget.js 的 maxlength 一致)
- session_id 加 1—64;idempotency_key 上限 128 → 64(对齐列宽 String(64))
- feedback_type 加 max_length=32(对齐列宽 String(32))
- 8 条路径参数补 min_length=1 + max_length=64 + 字符集正则
  ({session_id} / {run_id} / {handover_id})
- 改前超限值会落到 MySQL 才失败(500);改后一律 422 AGENT_INPUT_INVALID + 字段级定位
- 新增 tests/unit/api/test_frontend_boundaries.py(33 例),含"端点表 ↔ OpenAPI 全量对照"

四、投顾模块整体清除(D4.4 / D4.5)
- 删除投顾相关 controller / schema / model / repository / service 及门户页面
- tools/portal_api_check.py 同步作废 AD003/AD005/AD011/A047 四条用例与 advisor_t 登录
  (端点与账号均已不存在,此前稳定报 3 条假红)

五、验证(提交前实测)
- pytest -q:1856 passed / 2 skipped / 0 failed
- ruff check app tools tests:19(= 基线);mypy app:2(= 基线)
- 前端接口契约体检 portal_api_check.py:38 项,通过 34,失败 0,跳过 4
- 全链路冒烟 e2e_smoke_test.py --read-only:31/31
- HTTP 全链路探针 http_probe.py:11/11 succeeded
- 跨文档一致性 _consistency.py:GATE PASS
- 真机边界复验 12 条:12/12 符合预期

六、纪律与文档
- 可改文件白名单 A-09(docs/46)与底座会签申请单 A-10(docs/47,组 1—组 4 全部受理)
- 零 DDL:未新增/修改任何表结构,89 张业务表与基线一致
- 证据留痕:docs/evidence/**(含 46 条金标 score、快照、清除与重建记录)
- 未提交(刻意排除,见提交说明):仓库内 客服agent/ 与 开发文档/ 是 2026-09-16 前的
  过期副本(Todolist 440 行 vs 权威 D2.1 1167 行),权威正本在仓库外;
  _chunks_report.txt 是 tools/build_knowledge_chunks.py 生成的本地产物
2026-09-20 14:33:30 +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 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
张胜宇 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
zhangshy 164d55a05a 修复合规问句被误判为收益承诺 2026-09-12 15:05:03 +08:00
wangjianlong_0626 57677f6554 merge: 合并主干 qyqy_develop(PR #7 之后)并对齐两套投影实现
共同祖先 bbf623a;主干 54 个提交、118 个文件;本线 25 个文件;9 个冲突文件。
主干这次把 **ZSY 的整条投影实现合进来了(PR #7)**,而本线此前的提交正是
移植并修正同一套代码 —— 因此冲突的本质是"同一功能两份实现并存",取舍错了会把
已修好的缺陷又带回来。逐项取舍与理由见 `docs/39-主干合并对策记录.md`。

## 取舍(9 个冲突)

取本线:
- `app/infrastructure/milvus_profile_projection.py` —— 主干是 ZSY 原版,含两处必炸点:
  ① `customer_id` 要求 int 而本仓所有生产者都写 `str` ⇒ 每个事件必然失败;
  ② 不可投影的 `memory_key` 直接 raise ⇒ 一条 `constraint:` 记忆毒死整客户整批。
  本线版已放宽为「接受纯数字字符串」与「跳过并留痕」。
- `memory_sync_outbox_worker.py` / `conversation_privacy.py` / `risk_questionnaire.py`
  —— 代码逐行一致,仅注释与说明文字详略不同(`risk_questionnaire.py` 两边**独立做了
  完全相同的修复**,都改成 re-export `app.model.profile`)。
- 两个投影测试文件 —— 本线是他那份的**超集**(4→10、4→5 例,包含他全部用例)。

两边合并:
- `app/worker/runtime.py`:`__init__` 两边各加一个参数,都要。
- `app/service/agent/implementations/customer_service.py`:import 取并集;
  `COMPANY` 取主干的「奶龙基金责任有限公司」("奶龙"是本项目实际品牌名,主干多处出现),
  `HOTLINE`/`SERVICE_HOURS` **取本线的修复**(主干仍是占位符 `400-XXX-XXXX`,
  本线已改为引用 `customer_service_rules` 的唯一来源 —— 这是 A1 缺陷修复,
  否则同一客服给客户两个不同号码)。
- `AGENTS.md`:表数/Agent 清单取主干(90/89、7 个 Agent),本线的
  `-X utf8` 与两条 outbox 易错点保留,测试基线按合并后实测重算。

## 消费端只保留一套(本次最重要的一处)

合并后曾出现**两套消费者读同一个 `memory_sync_outbox`**:`__main__.py`(PR #7)
与 `runtime.consume_profile_projections()`(本线),而**两者的 neo4j handler 不同**
—— 前者用 ZSY 的 `Neo4jProfileProjection`(按客户各建私有节点),
后者用主干 `ProfileGraphProjectionService`(共享 tag 节点、只投影已确认事实)。
同一事件被谁领到结果不定,等于"同一事实在图里有两种说法",正是**方案 A 要避免的状态**。

现只保留 runtime 那一套(带 `memory_sources` 兜底、neo4j 复用主干服务),
删除 `__main__.py` 的重复接线;装配入口职责仍在该文件(注入 `relationships` /
`projection_cleaner`),Milvus 客户端由 `bootstrap` 工厂惰性构造、缺配置时显式降级。

副作用:`app/infrastructure/neo4j_profile_projection.py` 不再被生产代码引用,成为
**死代码**(本线未删,属架构师线,其单测仍在)—— 待架构师决定删或明确分工。

## 顺带修掉的 3 个继承缺陷(主干同样存在,PR #7 后未整套复跑故未发现)

1. `tools/seed_test_rbac.py` **少建 `review_t`(9004)账号** —— 两个集成测试都依赖它
   ("账号存在但无权限应返回 200 空集而非 404"、`PLACEHOLDER_ACCOUNTS`)。
   同时把用户↔角色绑定从 `zip(..., strict=True)` 改为**显式配对表**:原写法隐含
   "USERS 与 ROLES 一一对应",一加不绑角色的账号就 ValueError、整个种子跑不完
   (commit 在最后,外部表现是"什么都没发生")。
2. `CustomerProfileCandidateService._write_profile_snapshot` **漏写 `current_customer_id`**
   —— 该列不是生成列而是普通可空列 + 唯一键 `uk_profile_snapshot_current`,
   不写则唯一键形同虚设(多个 NULL 不冲突),且旧当前版本也没清该列、补写就会撞键。
   现旧值置 None、新值显式写入(与 `ProfileGenerationService._clear_current` 一致)。
3. 集成测试前置未记录 —— 13 个登录/RBAC 用例因 401 而红,实为"测试账号不存在",
   跑 `seed_test_rbac.py` + `set_user_password.py` 后转绿;已在 `AGENTS.md` 记明,
   避免被误判成代码缺陷。

## 文档

- 新增 `docs/39-主干合并对策记录.md`(逐文件取舍 + 理由 + 遗留)
- `docs/37` 订正一处过时说法:曾写 `current_customer_id` 无人使用且故意不映射,
  实际 `app/model/profile.py` 已映射且有人使用(详见该文档 §6.2 的订正块)
- 文档编号:主干已占 29–36,本线两份文档让号至 `docs/37`、`docs/38`

## 验证(合并后实测)

- `pytest tests`(全量)→ `2 failed, 1396 passed, 2 skipped`
- `pytest tests/integration` → `102 passed, 1 skipped`(修上述 1、2 后从 15 failed 归零)
- `mypy app` → `Success: no issues found in 245 source files`
- `tools/audit_schema.py` → 89 张业务表无缺失/意外(未改动任何表结构)
- `tools/check_authoritative_docs.py` → 52 份文档无编号冲突
- `tools/check_rbac_seed_consistency.py` → 通过

那 2 个失败是既有环境项(`test_offsite_document_recognition_adapter.py` 断言请求体
中文原文而 httpx 序列化成 `\uXXXX`),与本次合并无关。
2026-09-12 13:14:57 +08:00
lzf_0626 2bb056e516 fix: 修正合并进来的 test_security.py 两处密钥路径漂移(漏 dev/,标准布局下必红 2 个用例) 2026-09-12 12:09:55 +08:00
张胜宇 ef701c844c merge: integrate ZSY customer service and profile capabilities 2026-09-11 22:31:51 +08:00
qyqy a7e2d3eac0 fix(customer-service): 统一客服热线来源;补 transfer_required 出参(docs/05 §6.3 未兑现的一半)
两个都是"代码里存在但没接对"的缺陷,都不是新功能。

## A1 客服热线在代码里有两个值(一个出口给假号码)

- `app/core/customer_service_rules.py:35` `CONTACT_PHONE = "15936583816"` ← 真号码,安全路由 6 处在用
- `app/service/agent/implementations/customer_service.py:155` `HOTLINE = "400-XXX-XXXX"` ← 占位符,兜底出口在用

后果:**同一个客服给客户两个不同的电话号码**。问"风险等级怎么划分"被安全路由处理时给真号码;
问一个知识库答不了的问题走兜底时给 `400-XXX-XXXX` —— 客户按这个号码永远打不通。

修法:`HOTLINE` / `SERVICE_HOURS` 改为**转发** `customer_service_rules` 的两个常量
(不是"改成相同的值",而是引用同一对象,避免日后再次漂移);工作时间也随之从
"每日 7:00-22:00" 统一为 "工作日 09:00-18:00"(与安全路由出口一致)。
新增守卫测试用 `is` 断言对象同一性 —— 值相等挡不住"两边各写一份恰好相同"的漂移。

## A2 `transfer_required` 既没落库也没出参

`docs/05` §6.3 一直规定 `GET /agent-runs/{run_id}` 的 `result` 里有
`transfer_required` / `transfer_reason`,但实现里两个都没有:前端判断"这轮要不要转人工"
只能靠**猜正文里有没有兜底话术的开头**(`docs/24` 自己把这称为权宜之计)。

- 写入侧:`conversation_message` **没有** `transfer_required` 列,加列要迁移且规则 4 禁止改既有
  字段定义 ⇒ 放进 `tool_calls` 这个现成 JSON 列,作为 `calls` 的兄弟键
  (`{"calls": [...], "transfer_required": bool, "transfer_reason": str|None}`)
- 读取侧:`RunQueryService.get` 取出来放进 `result`;**兼容历史行**(`calls` 裸列表 / None →
  按 False/None 处理,不抛异常、也不凭正文猜)

刻意**没做**的一半:`docs/05` §6.3 的 `result` 里还有 `degraded` / `degradation_reason`,
但 `CoreResult` 里根本没有这两个字段(降级信息目前只在工具出参里)—— 补它要改
`CoreResult` 并让各 Agent 传递降级状态,属另一个改动范围。**已在交付说明里注明这一半仍缺。**

## 真机验证

| 问题 | transfer_required | transfer_reason | 正文电话 |
|---|---|---|---|
「请介绍一下量子纠缠在基金估值中的应用」 | **True** | 置信度不足:score=0.571 gap=0.004 | 15936583816 ✅ |
「请帮我计算一下三体问题的数值解」 | **True** | 置信度不足:score=0.499 gap=0.011 | 15936583816 ✅ |
「基金申购后多久确认」(正常知识直返) | False | — | 无(正确) |
「你们公司明天会下雪吗」(闲聊出口) | False | — | 无(正确) |

(第一次我用"下雪"当兜底用例,结果它被闲聊出口正确接住了 —— 是我的期望值写错,不是代码问题。)

门禁:测试 1223 passed(新增 4 个用例)/ 3 failed(均为已知非代码缺陷)/ mypy 0 错。
2026-09-11 21:17:47 +08:00
qyqy 5f82ac5107 feat(knowledge): 检索字段名改为运行时探测,两套集合 schema 都能跑
评审意见 §1.3 要求的修法。背景(双方实测共同确认):同一批集合名在两个开发环境里是两套不同 schema:
  我方:knowledge_id / snippet(无 visibility),行数 106/177/73
  架构师:doc_id / content / chapter / section / doc_no / visibility,行数 125/297/214
上一轮我把字段名硬编码成我方那套,在架构师环境会让 Milvus 报 field doc_id not exist
→ 三集合全失败 → 客服一律转人工(反向亦然)。硬编码任一套都会打挂另一套。

改法(采纳评审建议):
- 新增 app/core/knowledge_schema.py:describe_collection → 逻辑名到物理名映射,按集合缓存;
  缺必需字段的集合明确判为不可用并如实记 degraded,不静默零召回
- 检索服务改为逐集合探测:output_fields 只请求实际存在的字段;visibility 过滤有该字段才拼
- KnowledgeHit 对外形状不变,检索逻辑(字面召回/父子块/去重/置信判定)一行未改

测试:新增 17 个探测单测;架构师的关键词召回测试参数化为两套 schema 各跑一遍。
2026-09-11 18:42:42 +08:00
张胜宇 ef098e6a4b feat: complete customer service safety and handover flow 2026-09-11 16:11:30 +08:00
qyqy cbd6de2721 Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-latest
# Conflicts:
#	app/service/model_gateway.py
2026-09-11 15:38:11 +08:00
wangjianlong_0626 e4c4099aaa wip: 客服Agent + RAG + 画像收尾(基于 6516ccb) 2026-09-11 14:46:40 +08:00
lzf_0626 b3da1b65ed fix(risk): 修正风控的时区缺陷——凌晨规则与日报日界
docs/25 P1 #6。根因是"库内存 UTC naive"这个约定在**业务判断层**没被遵守,
而展示层其实已经是对的(risk_daily_report_service.py:418 按配置时区转换)。

1. 新增 app/core/timeutil.py 作为统一换算入口:约定库内 UTC naive,提供
   local_hour / local_date / local_day_bounds(后两者用于查库时必须返回 UTC naive,
   否则区间与库内值整体错开 8 小时),带时区的入参按其自身时区解释。
2. risk_scan_service.py:248:0 <= confirmed_at.hour < 6 → local_hour(...)。
   原先 [0,6) UTC 被当成"凌晨",实际是北京时间 08:00-14:00,整条
   「凌晨时段小额操作」规则判的是上午。
3. risk_judgement_service.py:238/243:判断**与展示**都换算。展示不改的话,
   风控专员看到的时刻与直觉差 8 小时,无法与客户核对。
4. risk_daily_report_service.py:89:日界改用 local_day_bounds(按北京时间自然日,
   再折回 UTC naive)。原先按 UTC 日期切日,北京 08:00 前生成的日报统计窗口跨零点。

测试:
- 新增 tests/unit/core/test_timeutil.py(8 条),含"UTC 凌晨 0-6 点不是北京凌晨"
  这一缺陷复现,以及"日界必须返回 UTC naive"。
- 改写 test_risk_scan_service.py::test_night_small_trade_boundaries:它原本就拿
  UTC 小时构造数据(写 0 点/6 点),等于在测北京 08:00/14:00;语义一并修正为
  北京 00:00(含)与 06:00(不含)两个边界,三个边界场景保持不变。

ruff / mypy(135 文件) / 603 unit+contract 全绿。
2026-09-11 12:50:52 +08:00
张胜宇 511a8ca18f feat: add governed public knowledge retrieval 2026-09-10 18:42:44 +08:00
lzf_0626 2c5ef35d18 chore: 删除退役的旧 JWT 密钥,同步过时引用并记录登录接口决策
1. 删除 config/jwt/jwt-private.pem 与 jwt-public.pem(旧密钥,配置已不再指向)。
   删除后复跑全量门禁以确认没有残留依赖:ruff 通过、unit+contract 447 passed、
   acceptance_check --production 7 PASS、demo_agent_e2e 9/9 PASS。
2. 修掉 4 处引用旧密钥路径的地方(它们会在删除密钥后直接失败或误导接入方):
   - tests/unit/core/test_security.py 的三处硬编码路径改为单一常量 DEV_KEY_DIR,
     否则删除旧密钥后该测试会因读不到文件而失败;
   - tools/demo_agent_e2e.py 的 docstring 前置条件;
   - docs/09 的配置示例;docs/19 的手工自签说明(改为指向配置项与
     tools/generate_jwt_keys.py)。
3. docs/21 记录旧密钥已删除,并注明删除后已复跑验证无残留引用。
4. docs/19 未解决项新增第 5 条:无登录接口属于**有意识的推迟**(等业务 Agent 开发阶段
   结束后再补),写明补的时候只需动签发侧、验签侧与身份解析侧都不需要改,
   并附上当前私钥边界的实测结论(无法伪造不存在的用户、无法使用已禁用账号)。
2026-09-10 18:18:00 +08:00
张胜宇 ba22a2220f feat: preserve isolated visitor agent runtime 2026-09-10 17:36:39 +08:00
张胜宇 8e36a9941d merge: preserve existing business features on new foundation 2026-09-10 17:26:52 +08:00
lzf_0626 6516ccb385 feat: 第二版——接口契约对齐 docs/05,修复静默故障与数据库基线
相对第一版 46fc976 的完整变更。组员迁移对照表见 docs/20。

一、对外契约对齐 docs/05(破坏性,共 4 处,组员需按 docs/20 调整)
1) 配置发布端点改为文档规定的复数资源名:submit→validations、
   approve→reviews(需 body decision)、activate→activations、
   rollback→rollbacks;第一版这 4 个动词式路径 docs/05 从未定义过。
2) 错误码由 8 个笼统码改为 15 个具体语义码(FORBIDDEN→AGENT_PERMISSION_DENIED、
   UNAUTHORIZED→AUTHENTICATION_REQUIRED、CONFLICT→RESOURCE_VERSION_CONFLICT、
   RESOURCE_NOT_FOUND→RUN_NOT_FOUND/SESSION_NOT_FOUND 等),
   输入类错误状态码 400→422。
3) POST /api/v1/agent-runs 与 GET /api/v1/agent-runs/{run_id} 统一为
   {data, meta} 信封(data 内字段名与语义未变)。
4) 错误响应体统一为 {error:{code,message,retryable,field_errors}, meta:{trace_id}},
   不再返回 FastAPI 默认的 {"detail": ...}。

二、数据库基线与约束
新增 39 张表的基线迁移(链根)与联合唯一键纠偏(4 张表、删 8 增 4,幂等收敛);
撤下 config_release 的双人复核 CHECK(应用层已允许自审,审核节点保留,
自审如实写入 reviewer_id);记忆 active key 生成列与唯一键;
activate 开始记录 supersedes_release_id 使版本链可追溯。
docs/00 基线未修改,未重命名或删除任何表与字段。

三、修复会静默出错或无报错的缺陷
- 跑完集成测试后平台会静默失去生效配置:清理只删自己创建的版本,却没有恢复被它
  顶成 superseded 的原生效版本,且审计一并删除因而完全无痕,表现为所有工具被拒
  但没有任何报错。已修清理逻辑并加恢复。
- Worker 单轮异常导致进程退出;记忆抽取调用方的“事务已开始”异常;
  召回缓存丢失 degraded 标记;连接时区未生效导致 created_at/updated_at 差 8 小时;
  .env 与 os.getenv 密钥来源分裂导致“没有可用的已批准模型端点”。
- 记忆信号识别漏判与跨键误命中;SSE 未带 Accept 的协商行为。

四、功能补齐
记忆链路 P1/P2/P3(抽取、受控词表、召回与缓存、生命周期级联及投影事件)、
fin_* 场内交易只读 ORM 层、agent_intent_config 状态流转并在运行期真正生效、
限流(Redis 固定窗口、故障一律放行)、游标校验、trace_id 中间件、
示例业务 Agent fund_query_demo 与一键端到端验证脚本,以及审计/指纹/迁移状态工具。

五、文档与验证
新增 docs/19(业务 Agent 接入实操)、docs/20(第一版迁移指南)与 docs/evidence 证据;
docs/01/02/06/08/09/17 同步实现现状。

验证结果:ruff 通过、mypy 103 文件无错、unit+contract 447 passed、
integration 29 passed、acceptance_check --production 7 PASS、
demo_agent_e2e 9/9 PASS(含失败关闭反证)。
2026-09-10 15:55:54 +08:00
张胜宇 ad7172367a feat: add limited visitor tokens 2026-09-10 13:56:08 +08:00
Codex b1497fd2c6 chore: initialize project repository 2026-09-09 21:55:37 +08:00