Commit Graph
136 Commits
Author SHA1 Message Date
lzf_0626 f6bfcc26b3 fix(demo): 把未上市的 160129 换成真实挂牌的 515450,并修掉种子假行情盖住真行情
## 1. 160129 本就不该在场内清单里

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

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

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

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

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

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

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

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

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

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

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

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

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

修复后实测:

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

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

门禁:ruff 通过;mypy 250 文件 0 错;unit+contract 1381 passed / 0 failed;
integration 104 passed;e2e 冒烟 40/40。
2026-09-13 22:08:04 +08:00
lzf_0626 06f0dee39f feat(sync): 行情源覆盖不到的产品改用净值源降级(160129 现在也可下单)
160129(南方金利定开债券C)在腾讯行情源上**查不到** —— 返回体只有 1 个字符,
换 sh/无前缀、换代码写法都不行;而它的 A 类 160128 正常返回 88 个字段。
判断它很可能未上市交易。但它在东财历史净值(1.0240 @09-11)与 pingzhongdata
(规模 3.91 亿)里都有数据,所以按"换一个源"处理:

- 新增净值降级路径 _nav_fallback:收盘价取 DWJZ、开高低用同值(该接口只给一个价格)、
  olume/	urnover **留空**(净值源不提供量额,不编数字)、总份额由季度规模推算。
- source 写成 eastmoney_nav_fallback,与行情源明确区分,便于日后核对。
- 降级请求放在**事务外**:最初写在落库循环里,会让网络请求拉长事务。

⚠️ **净值不等于市价**:若该产品确实在交易所交易,用它当收盘价会有折溢价偏差。
这条路径只落在"行情源没有覆盖"的产品上;且 160129 很可能本就不该出现在场内产品
列表里(同基金的 A/C 类只有 A 类上市),值得业务侧复核 —— 但按项目方要求先让它可用。

结果:20 只全部同步上(19 行 tencent_quote + 1 行 eastmoney_nav_fallback),
160129 下单 201 已成交 @1.024。

门禁:ruff 通过 / mypy 250 文件 0 错 / 单元+契约 1381 passed 2 skipped。
2026-09-13 21:32:15 +08:00
lzf_0626 87a753f7b3 feat(sync): 给 fin_market_price 补真实行情同步链路(下单前置)
## 解决的问题

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

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

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

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

## 实现

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

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

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

## 验证

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

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

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

门禁:ruff 通过 / mypy 250 文件 0 错 / 单元+契约 1380 passed 2 skipped。
2026-09-13 21:14:13 +08:00
lzf_0626 e9c148f005 fix(sync): 行情同步改为逐只容错,避免一只坏代码拖垮全量
全流程验收时发现的核心故障:**所有客户都无法下单**(任意产品都返回
503 FUND_QUOTE_UNAVAILABLE),而错误信息只说"某产品行情已过期",看不出根因。

根因链:
1) MarketQuoteSyncService.sync() 把库里 fund_manager='南方基金' 的 20 只产品
   打包交给 hq.py 的 loader;
2) 而 hq.py 的白名单校验是 ny(code not in SOUTHERN_FUND_CODES) -> raise,
   **一个代码不在名单里整批就抛错**;
3) 库里混着 510300 —— 它的真实管理人是华泰柏瑞(前面查费率时确认过),
   不在南方基金白名单里 -> 两个数据源全部 ValueError -> quotes=0;
4) 于是 in_market_price 长期不更新(当时只有 2 行、时间停在 08:19),
   下单的 MAX_QUOTE_AGE 校验全线失败。

修法:整批调用失败时**降级为逐只调用**,保留能取到的行情,把被拒代码记进
source_results(并拼进 error_type,让它在 dvisor_market_quote_source_run
审计行里可见,不必为此改表)。被拒代码通常意味着 und_manager 标注有问题,
值得人工核对。

不动 hq.py 的白名单:那是刻意的防误用设计(不许拿任意代码查南方行情),
该容错的是编排层。

验证:重跑 tools/sync_advisor_market_quotes.py 后不再是全批失败;
配合重跑 tools/seed_sim_account_demo 刷新 fin_market_price,
下单恢复 —— 510500、510300 均 HTTP=201 已成交。

⚠️ 仍未解决(数据供给缺口,需要产品/业务侧决定):
in_market_price 目前**只有演示种子脚本写**,没有生产同步链路。
后果是 20 只产品里只有 2 只(510300/510500)有行情、其余 18 只下单报
"缺少场内行情",而且行情一旦过期就没有任何机制刷新。
2026-09-13 20:39:36 +08:00
lzf_0626 f3af00e645 fix(trade): 补 from None 修掉 ruff B904;附 integration 抢队列的复现结论
两件事,都源自组员 e3c316a(下单授权与适当性校验)合并之后:

1) ruff B904:app/service/trade_service.py 在 except 分支里抛
   SuitabilityMismatchError 时没写 from None/from exc,门禁因此变红。
   这里被捕获的是"风险等级字段解析失败",对上层没有诊断价值,
   用 from None 截断因果链即可。

2) tests/integration 出现 2 个失败,**与代码无关**,是常驻 Agent Worker 抢队列:
   - 带 Worker 跑:102 passed / 2 failed
   - 停掉 Worker 跑:104 passed(已复现两次)
   失败用例是 test_complete_run_rolls_back_every_write_on_outbox_conflict 与
   test_cancel_is_idempotent_and_terminates_original_request,都是 run 队列相关 ——
   Worker 会把测试创建的 run 领走并执行,测试自己的状态机就乱了。
   这正是 docs/20 与 AGENTS.md「跑验收前先停常驻 Worker」那条的实际复现,
   顺手把结论写进 commit 备查:判断 integration 红不红之前,先确认 Worker 在不在跑。
2026-09-13 19:30:59 +08:00
张胜宇 aa4b9bf367 Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop 2026-09-13 19:26:31 +08:00
张胜宇 e3c316aafb Enforce trading authorization and suitability checks 2026-09-13 19:24:59 +08:00
lzf_0626 f72a545c39 refactor: 品牌统一为「南方财富」(项目方定)
背景:品牌名此前三处不一致 —— 后端 Agent 自称「奶龙基金」(customer_service_rules
的防诈骗/转人工话术、risk_agent 与 risk_analysis_service 的系统提示词)、前端全站
「南方财富」、闲聊提示词与知识素材「南方科技」。docs/36 已把它登记为"上报项目方后
待定",现按项目方决定统一为「南方财富」。

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

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

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

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

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

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

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

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

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

其他:投顾页显示交付物类型与客户编号(后端新返回的字段),README 补上投顾页
数据口径、访客/客户两条检索工具的差别,以及"渲染 mock 必须带来源声明"的约定。
2026-09-13 18:54:09 +08:00
张胜宇 e740a58e9d 前端2 2026-09-13 18:31:38 +08:00
张胜宇 e38ece32bf 前端提交 2026-09-13 15:56:54 +08:00
lzf_0626 167a4e7162 Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop 2026-09-12 16:30:56 +08:00
lzf_0626 cb13f9cf45 fix(types): trade_service._next_id 的 model 参数标注改为 Any(mypy: type 无 id 属性) 2026-09-12 16:30:54 +08:00
zhangshy 001ba065df Merge remote-tracking branch 'origin/qyqy_develop' into RM2_develop 2026-09-12 16:12:02 +08:00
zhangshy eb4e895e4e 修复高风险预警邮件通知并补充测试 2026-09-12 15:56:50 +08:00
张胜宇 ebc3fe4cbe feat(§T): 账户看板 + 场内模拟交易 9 端点(用户自助首版)
新增 §T 用户自助段(docs/05 §19 新号段 7 个 = A×40/C×7/K×4/M×4/O×3/R×4/T×9):

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

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

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

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

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

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

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

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

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

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

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

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

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

  9047 account:read:self

  9048 trade:order:create

  9049 trade:order:read

  9050 trade:order:cancel

  9051 holding:read:self

  9052 trade:txn:read

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

  404 ACCOUNT_NOT_FOUND / ORDER_NOT_FOUND

  409 ORDER_NOT_CANCELLABLE

  422 INSUFFICIENT_FUNDS / INSUFFICIENT_HOLDING / HOLDING_RATIO_EXCEEDED / SUITABILITY_MISMATCH / PRODUCT_NOT_TRADABLE

  503 FUND_QUOTE_UNAVAILABLE(可重试)

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

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

门禁:pytest tests/unit tests/contract 1313 passed (+19 新增) / ruff all clean / 三道守卫全过
2026-09-12 15:50:37 +08:00
lzf_0626 615032ab00 fix: 修 POST /api/v1/conversations 的 MissingGreenlet(会话建不出来导致转人工 404)
- public_platform_service: 创建 ConversationSession 时显式赋值四个 server_default 时间列,
  否则 flush() 后需回读数据库生成值,在 async session 里以同步属性访问触发
  MissingGreenlet,接口 500,连带转人工一直报会话不存在
- portal: 客服改用 C001 真实建会话;转人工改传 reason_code/reason_detail(原 reason 属额外字段 422)
- portal: 投顾查客户投资目标走 customers/{id} 变体
- seed/grant: 补 investment-goal:{read,write,confirm}:customer 三个动态拼出的权限码
  (data_scope=own_customers,投顾只看名下客户)
2026-09-12 15:37:36 +08:00
lzf_0626 c8cdc06e80 Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop 2026-09-12 14:18:06 +08:00
Windows 050eedac68 Merge remote-tracking branch 'origin/qyqy_develop' into lzl_qyqy_integration 2026-09-12 14:16:27 +08:00
Windows 54fadb3a99 Merge branch 'qyqy_develop' of http://47.106.207.27:3000/AI260626/group_fqcd_jr into lzl_qyqy_integration
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-09-12 14:15:43 +08:00
wangjianlong_0626 58c28ef4a0 fix(memory): 记忆抽取容忍"单元素数组"形状,空结果不再被判为无效 JSON
## 现象(2026-09-12 跑真实 Worker 时发现)

`episode_worker` 反复报 `模型记忆抽取输出不是有效 JSON` 并重试到失败:

```
pydantic_core.ValidationError: Input should be a valid dictionary or instance of _ExtractionPayload
  input_value=[{'memory_key': None, 'value': None, 'memory_type': None, 'confidence': 0}]
  input_type=list
```

## 根因

契约是**对象** `{...}`,而模型在 episode 抽取路径会返回**单元素数组** `[{...}]`。
`_parse` 直接 `json.loads` 后交给 pydantic,数组自然过不了 `model_validate`,
于是被归入"输出不是有效 JSON"这一条 —— 但它其实是**合法的空结果**
(`_validate` 已能把"三字段为 null 且 confidence=0"正确识别为"无持久事实",返回 None)。

后果:这类 episode 白跑一遍模型调用、重试到 `retry_count` 上限后判失败,
**该片段的记忆永远抽不出来**。

## 修法

`_parse` 里只对"**恰好一个对象**的数组"做归一化:

- `[{...}]` → 取 `{...}`(空结果照常返回 None;有事实照常解析)
- 多元素数组、元素非对象、空数组 → **不猜**,仍交给校验失败关闭
  (多元素时无法判断哪个是答案,猜错会把错误记忆写进库,比失败更糟)

## 测试

`tests/unit/service/test_memory_extraction_service.py` 追加 2 个用例:

- `test_single_element_array_is_normalized`:数组包空结果 → None;数组包有事实 → 正常解析
- `test_multi_element_or_non_dict_array_still_fails_closed`:多元素 / 非对象元素 / 空数组 → 仍失败

## 验证

- `pytest tests/unit/service/test_memory_extraction_service.py tests/unit/worker/test_memory_extraction_worker.py` → 28 passed
- `mypy app` → 0 错 / 245 文件

> 说明:`memory_extraction_service.py` 属主干线代码。此处是从**实际运行日志**里发现的
> 健壮性缺陷,改动限定在输出形状归一化,不改变抽取契约与校验规则。
2026-09-12 14:11:42 +08:00
wangjianlong_0626 f7b35006ff Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague 2026-09-12 14:06:18 +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
yuancong_0626 a501060e50 merge: merge yc into qyqy_develop 2026-09-12 12:50:14 +08:00
yuancong_0626 91f5373cfb 袁聪的第三次提交,API接口完善 2026-09-12 12:20:01 +08:00
Windows 3751891574 Merge branch 'qyqy_develop' of http://47.106.207.27:3000/AI260626/group_fqcd_jr into lzl_qyqy_integration 2026-09-12 12:10:41 +08:00
Windows f094cbeae1 fix: harden advisor market history dual-source sync 2026-09-12 12:10:10 +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
wangjianlong_0626 8a0cbab636 fix(memory-projection): 订正 outbox 取值口径并接通画像投影链路
背景:memory_sync_outbox 这条链此前**完全没有消费者**,且生产端照 docs/00 §6.4.6
写成大写 MILVUS/NEO4J + 中文「待处理」,而消费端按 target_store 的**值**分派 handler、
且只领 status in {pending, failed} —— 两个条件都不满足,事件任何消费者都领不到、
永久滞留且不报错(唯一键 (event_uuid, target_store) 对大小写无约束,MySQL 也不报错)。
根因是代码与测试都硬编码字面量,所以测试跟着一起错、谁也没拦住。

订正
- profile_generation_service:取值改为全仓一致的小写(milvus/neo4j/upsert/pending)
- 测试改为引用常量并断言消费端契约,不再硬编码(硬编码是本次跑偏的直接原因)
- 新增契约回归测试:断言大写值分派不到 handler、会进死信,谁改回大写立刻红
- 新增 tools/normalize_memory_sync_outbox.py:订正历史脏行(默认 dry-run、幂等)

接通投影链路(此前零消费者)
- 新增 Milvus 集合 user_long_term_memory_v1 及建集合工具(幂等、不覆盖已有集合)
- 新增 MilvusProfileProjection / MilvusProfileVectorClient,并修掉移植带来的两处必炸点:
  customer_id 由「必须 int」放宽为接受数字字符串(本仓所有生产者都写 str,
  不放宽则每个事件必然失败);不可投影的 memory_key 由「整批 raise」改为跳过留痕
  (否则一条 constraint: 记忆毒死该客户整批,而受控词表 13 个键里有 7 个不满足前缀)
- 新增 MemorySyncOutboxWorker(领取/指数退避/死信骨架保留原样)并接入 WorkerRuntime
- milvus → 向量投影;neo4j → 复用主干 ProfileGraphProjectionService(方案 A,
  不引入第二套投影,避免同一事实在图中两种说法、违反主干既有的只投影已确认事实的不变式)
- 生产端从 memory_unit(status=active) 组装 memory_sources,随事件带上确定快照
- 前置移植 conversation_privacy:写外部存储前脱敏手机号/证件号/银行卡等

验证
- 新增 17 个单测;全量 2 failed, 1307 passed, 2 skipped
  (2 个失败为既有环境项:断言请求体中文原文而 httpx 序列化成 \uXXXX,非本次引入)
- mypy app → 0 错(227 文件);audit_schema → 89 张业务表无缺失/意外,未改动表结构
- 真机:真实 embedding(1024 维) + 真实 Milvus 写入并回读通过
- 整合链路(测试记忆 → 生产端组装 → outbox → 消费端投递 → Milvus 回读)通过,
  且 MySQL 已回滚、Milvus 无残留

文档
- 新增 docs/32-记忆投影链路实现说明.md:真实口径、根因、契约与验证证据(供接手)
- AGENTS.md:新增该易错点;新增 Windows 中文输出乱码的正确命令(-X utf8);
  校正测试基线与 mypy 文件数

未做:未改 docs/00 基线、未动数据库迁移、未改投顾线代码、未启动常驻 Worker。
遗留:投顾线两处生产者的 payload 缺 memory_sources,会被消费至死信,待架构师确认是否投影。
2026-09-12 10:45:40 +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
张胜宇 ef701c844c merge: integrate ZSY customer service and profile capabilities 2026-09-11 22:31:51 +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 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
wangjianlong_0626 cc6ae87f26 Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague 2026-09-11 21:18:30 +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
Windows bbf623a464 merge: integrate advisor capabilities on latest qyqy base 2026-09-11 21:03:58 +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
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
Windows f5dd5b8ba2 feat: add advisor rollout gate and rollback playbook 2026-09-11 20:28:21 +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
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
Windows 2376585898 feat: add dual-source quote health orchestration 2026-09-11 19:47:14 +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
Windows b6429e0e9c feat: add advisory product comparison tool 2026-09-11 19:20:32 +08:00
qyqy 636dcbbe7e chore: 修我文件里的 mypy 类型错误(8 处)
源自评审 §1.4「数字不可比」引发的核查,结论比预想更有价值:

**mypy 报错的主因不是代码质量,而是本机缺 SQLAlchemy 2.0 的类型信息。**
装上 `sqlalchemy2-stubs` 后 181 → 43(该类存根是 2.0 之前的旧包,会换一批新错:
`mapped_column`/`DeclarativeBase` 不存在),卸载后回到 184。**本机 mypy 数字不可作为
质量结论,双方也不可比。** 但那 184 里有 8 个是**我文件里的真实错误**,已修:

- `knowledge_retrieval_service`:返回类型 `Mapping` → `dict`(回表后要就地补写
  `score`/`intent`,而 `Mapping` 是只读协议);`ids` 显式标注并过滤 `None`;
  去掉 3 处已失效的 `type: ignore`(strict 下 unused-ignore 本身是错误)
- `knowledge_management`:服务工厂返回类型 `Any` → `KnowledgeManagementService`
  (`TYPE_CHECKING` 期导入,运行时仍惰性,不引入循环依赖),消掉 3 个 `no-any-return`

未动的:`model_gateway` 2 处 `dict-item`(`ModelEndpointConfig` 实际具备协议要求的
全部字段,属 SQLAlchemy `Mapped[T]` 在缺存根时的消解问题,不是真缺陷,不用 cast 掩盖)。
2026-09-11 19:13:43 +08:00
qyqy 7677aeaee1 merge: 并入同事的场外申购/推广/行情/NL2SQL 线(11 提交、334 文件)
冲突仅 3 个文件,全部取并集(双方都没有需要丢弃的改动):
- app/main.py:import 双方路由(我方 knowledge_management + 同事的 offsite_fund/
  promotion_material);include_router 段本已自动合并
- app/service/agent/bootstrap.py:import 与工具注册均取并集
  (query_customer_profile + query_financial_data 都注册)
- tests/integration/test_config_release_mysql.py:outbox 清理同时保留
  架构师的 event_type 限定(防误删其它域 outbox 行)与同事新增的 peer_release_id

同事这轮带入:11 个 alembic 迁移(建 offsite_* / promotion_* 等表)、
场外申购与推广素材 Agent、financial NL2SQL 工具。
注意:本库尚无 offsite_*/promotion_* 表,跑相关测试前需要执行 alembic upgrade。

边界核对:同事的场外代码未写入场内交易表(fin_sim_order/fin_capital_flow/fin_cash_ledger),
符合 AGENTS.md 规则 8。
2026-09-11 18:56:26 +08:00
qyqy 928d0bcea3 chore: 按评审恢复 5 份文档、审计补 agent_type、修订环境口径
评审意见落地(§3.3 驳回删除 / §3.1 审计补充 / §1.4 解释器 / §2 编号):
- docs/04、06、10、13、99 全部恢复(评审:删除收益为零、保留成本同样为零);
  AGENTS.md 改为"保留但仅作历史参考"并列入 D 类,ARCHIVE 归档说明加作废声明
- 治理审计补留痕:interaction_audit.detail 增加 agent_type 与 governance_rewrite
  (治理层会改写对外输出,事后必须能追溯到是哪个 Agent 触发的;不改表结构,detail 是 JSON 列)
  新增 tests/unit/service/test_agent_persistence_audit.py 锁住该契约
- AGENTS.md 修订环境口径:解释器各用本机可用的那个(.venv 被 gitignore、不进仓库);
  config_release 与 Milvus schema 均属环境数据、不随代码合并,相关结论必须带环境限定;
  测试基线 1034;mypy 数字双方不可比(本机未装 sqlalchemy2-stubs,报错集中在模型层)
- docs/26 JWT 文档:因 21 已被风控迁移清单占用而改名,PR 描述里会单独说明
2026-09-11 18:42:46 +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