wangjianlong_0626
|
1f62aca6f7
|
fix(seed): 当日行情重跑必须刷新 source_updated_at(修 503 FUND_QUOTE_UNAVAILABLE)
真机实测到的缺陷:`tools/seed_sim_account_demo.py` 的 `_upsert_market_price()`
在"当天已有行"时直接 `return`,于是同一天重跑种子**不刷新 `source_updated_at`**。
行情是时效数据,过期后 `FundQuoteService` 返回
`503 FUND_QUOTE_UNAVAILABLE:产品 510300 行情已过期`,
连带 `T001` 仪表盘与 `T006` 持仓一起不可用(`T010` 权益不查行情,仍 200)。
表现极具误导性:**刚灌完种子能用,过十几分钟就 503** ——
看起来像行情适配器或缓存故障,实际根因在种子脚本。归因过程:
`memory_sync` / Redis / Milvus 全部正常,`fin_market_price` 里当天那行
`source_updated_at` 停在首次灌入时刻。
修法:把列值抽成共享 `values` 字典,当天已有行时 `update` 刷新全部行情列
(含 `source_updated_at`),不存在才 `insert`。
**幂等的正确含义是"不产生重复行"(`(product_id, trade_date)` 唯一),
不是"不更新值"。** 账户/持仓的"已存在则跳过"保持不变 ——
那是业务数据,不该被种子覆盖。
验证(本机,测试客户 9001 `cust_t`):
- `python -X utf8 -m tools.seed_sim_account_demo --customer-id 9001` 正常
- `GET /api/v1/users/me/account/dashboard` → 200(修前 503)
- `GET /api/v1/users/me/holdings` → 200
- `GET /api/v1/users/me/entitlements` → 200
门禁:ruff `app tests tools alembic` 全过;`mypy app` 0 错(252 文件);
`audit_schema.py` 90 张业务表无差异;文档编号/端点编号/RBAC 种子一致性全过。
|
2026-09-12 17:32:24 +08:00 |
|
wangjianlong_0626
|
4766e3bd98
|
feat(benefit): 客户权益功能(T010)+ 修投顾迁移契约里写死 head 的脆弱断言
## 1. 新增客户权益(用户端)
`GET /api/v1/users/me/entitlements`(T010,权限 `benefit:read:self`):
- **层级**由 `fin_customer_profile.total_asset` **实时判定**
(门槛来自 `knowledge/product/高净值客户服务规范.md`:
金卡 50 万 / 白金 200 万 / 钻石 600 万 / 私行 1000 万;低于 50 万为普通客户);
- **权益按层级累积展开**(文档原文"含全部下级权益,新增以下"):
金卡 9 条 / 白金 20 / 钻石 33 / 私行 54,各档已逐档实测;
- 返回**升级提示**(`next_tier`:下一层级与门槛),前端可直接渲染"再投 X 元升级"。
### 新增表 `fin_customer_benefit`(1 张)
层级 → 权益目录,54 条种子数据(`tools/seed_customer_benefits.py`,按 `benefit_code` 幂等)。
**基线合规证明**(规则 1/3/4):只新增这一张表;**未**重命名/删除任何已有表;
**未**重命名/删除/复用任何已有字段,**未**改任何已有字段的类型、可空性或业务含义;
未改 `docs/00`。
复核:`tools/audit_schema.py` → `90 business tables, no missing or unexpected tables`。
### 两条设计取舍
1. **不落"某客户享有哪些权益"**:层级可算,权益由层级推出,两者都不落库。
与 `docs/00` L159(不保留 `net_worth_flag`,因为可算)同一取向。
2. **权益只存各层新增条目**,累积由服务层 `tier_chain()` 展开 ——
否则改一条权益要改四处,漏一处就出现"白金没有金卡权益"。
### 数据来源与一处刻意省略
逐条照抄知识文档,不新增文档里没有的权益。**私行那条
「7×24小时私人银行专线:400-XXX-XXXX 转 8」不写号码** ——
文档里是占位符,而对客号码的唯一来源是 `customer_service_rules.CONTACT_PHONE`
(本线此前修过"同一客服给客户两个不同号码"的缺陷)。把占位符抄进库等于再造一份假号码。
## 2. 修投顾迁移契约里写死的断言
`tests/unit/test_advisor_migration_contract.py` 原先断言
```python
assert script.get_heads()[0] == "20260911_merge_adv_risk_heads"
```
那是"投顾迁移刚加完那一刻"的快照 —— 本 PR 一新增迁移(`20260912_customer_benefit`)
它就变红,**而红的原因与投顾链的对错无关**:断言测到的是时间,不是契约。
原意是"投顾链接在这条主链上、没另起分支"。改为断言**投顾链尾是当前 head 的祖先**
(链尾从 `ADVISOR_FILES[-1]` 派生,不写死),既保住原意又不受后续迁移影响。
`len(script.get_heads()) == 1`(链不分叉)与"投顾文件首尾相接"两条原样保留。
## 3. 顺带发现的既有缺口(**不在本次改动范围**)
`app/api/controllers/trading.py` 的 **T001–T009 未调用 `AuthorizationService.require`**:
`docs/05` §19 为它们登记了权限码(`account:read:self` / `trade:order:*` / `holding:read:self`),
但代码只做认证 + 开户测评门槛,**没有执行 RBAC 权限检查**。
对照:仓库里 **26 个 service** 都调了 `require`,`trade_service` 不在其中。
本线的 T010 **按正确做法实现**:`CustomerBenefitService.entitlements_for` 先鉴权再读数据,
且**鉴权在读取客户资产之前**(有测试断言"拒绝时未查库")。
T001–T009 如何补,需架构师定口径后另行处理。
## 4. 文档
- 新增 `docs/41-客户权益功能说明.md`:表登记 + 基线合规证明 + 分层口径 + 累积规则 +
数据来源 + 权限 + 与仪表盘的关系 + 上述缺口
- `docs/05` §19 登记 T010,并**单独注明它引入了新表**(避免被误读为
"T 段数据库零变更"的一部分)
- `AGENTS.md` 表数 89 → **90** 张业务表
## 验证
- `pytest tests/unit/service/test_customer_benefit_service.py` → **20 passed**
(含边界:499999.99 不是金卡、500000 整是金卡、1000 万整是私行;累积条数;升级提示;
鉴权先于读数据)
- 全量 `pytest tests` → `2 failed, 1469 passed, 1 skipped`
(2 个失败为既有环境项:httpx 把中文序列化成 `\uXXXX`,非本次引入)
- `ruff check app tests tools alembic` → `All checks passed`
- `mypy app` → **0 错 / 252 文件**
- 真机:`GET /users/me/entitlements` → `200`;各档分层与累积条数逐档实测通过
- `audit_schema.py` → 90 张业务表无缺失/意外;文档守卫 55 份无编号冲突;
端点编号无重复;RBAC 种子一致性通过
|
2026-09-12 17:24:37 +08:00 |
|
wangjianlong_0626
|
37926e1c88
|
Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
|
2026-09-12 16:39:39 +08:00 |
|
wangjianlong_0626
|
db4dae2fe6
|
chore: 追平主干(advisor 演示引导/风控权限配置 3 提交);同步测试基线 1424
主干 `c200a61..5634fdc` 3 个提交(advisor 演示环境引导、风控登录权限与模型能力配置),
与本线 5 个提交**无文件重叠**,合并**无冲突**(`tools/seed_test_rbac.py` 双方都改过,
Git 自动合上;合并后 `check_rbac_seed_consistency.py` 仍通过)。
追平后本线相对主干恢复 **BEHIND 0**,PR #9 可重新 fast-forward 合并。
## 验证(合并后重跑)
- `ruff check app tests tools alembic` → `All checks passed`
- `mypy app` → 0 错 / 245 文件
- `pytest tests`(全量)→ `2 failed, 1424 passed, 1 skipped`
(2 个失败为既有环境项:httpx 把中文序列化成 `\uXXXX`,非本次引入)
- `check_authoritative_docs.py` → 53 份文档无编号冲突
- `check_rbac_seed_consistency.py` → 通过(种子 id 唯一、各 grant 脚本与种子逐条一致)
- `check_docs_endpoint_ids.py` → §19 端点编号无重复
- `audit_schema.py` → 89 张业务表无缺失/意外
|
2026-09-12 14:50:08 +08:00 |
|
wangjianlong_0626
|
aa3f9f9b3d
|
Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
|
2026-09-12 14:48:45 +08:00 |
|
wangjianlong_0626
|
b2a2e4292b
|
docs(37): 补「换到新环境必须先做」的前置清单(建集合/Milvus/embedding 端点/迁移)
|
2026-09-12 14:35:49 +08:00 |
|
wangjianlong_0626
|
c12f836d26
|
Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
|
2026-09-12 14:28:51 +08:00 |
|
wangjianlong_0626
|
86d1cf1ffc
|
docs: 补回合并时丢失的 ruff 基线;合并主干 6 提交(含架构师对我疏漏的修正)
## 1. 补回被我丢失的门禁条目
主干 `AGENTS.md` 原本有一条 `ruff` 干净,但我在解决 `AGENTS.md` 冲突时**只保留了自己的
mypy/pytest 基线,把这条丢了** —— 合并时丢信息,和代码冲突一样是缺陷。
本次补回,并把命令写准(这点很重要):
- `ruff check app tests tools alembic` → **`All checks passed`(0 错)**
- 直接 `ruff check .`(全仓)会报 **40 个错,全部来自仓库根目录的 `hq.py` / `nl2sql_yc.py`**
(袁聪线的演示脚本,不属本项目包结构)
所以"ruff 干净"**必须带范围**,否则会和别人的脚本混在一起、把一个健康状态误报成 40 个错。
## 2. 我的一个真实疏漏(已由架构师修正,本次合并带入)
架构师提交 `ffbcc22`:**`fix: 删掉 NL 合并后残留的未使用变量 role_ids(ruff F841)`**
该疏漏是我引入的:改 `tools/seed_test_rbac.py` 时把
`zip(user_ids, role_ids, strict=True)` 换成显式配对表 `USER_ROLES`,删掉了 `user_ids`
却**没删 `role_ids`**。根因是**我全程没跑过 ruff** —— 项目门禁里有它,
只跑 mypy 和 pytest 是不够的。
已在本机复跑 `ruff check app tests tools alembic` 确认:我改过的文件全部干净
(`role_ids` 已随主干修正进来)。
## 3. 合并主干 6 提交
`fedbf5a..c8cdc06`,含上条修正与投顾线的行情双源、验收归档等,**无冲突**。
## 验证
- `ruff check app tests tools alembic` → `All checks passed`
- `mypy app` → 0 错 / 245 文件
- `pytest tests`(全量)→ `2 failed, 1421 passed, 1 skipped`
(2 个失败为既有环境项:httpx 把中文序列化成 `\uXXXX`,非本次引入)
|
2026-09-12 14:26:43 +08:00 |
|
wangjianlong_0626
|
8cb75992e6
|
Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
|
2026-09-12 14:21:34 +08:00 |
|
wangjianlong_0626
|
451aa4e915
|
fix(profile): ProfileAssemblyService 漏写 current_customer_id(唯一键失效的另一处)
## 现象(2026-09-12 端到端跑通后查数据时发现)
客户 9102 的画像快照状态**彼此矛盾**:
| version | is_current | current_customer_id |
|---|---|---|
| 2 | `0` | `9102` ← 清旧时没清空 |
| 3 | `1` | `NULL` ← 建新时没写入 |
## 根因
`ProfileAssemblyService._write_snapshot` 把 `current_customer_id` **当成了生成列**:
- 方法 docstring 原文写着"唯一键 `uk_profile_snapshot_current` 建立在**生成列** `current_customer_id` 上"
- 因此两处都不赋值(以为 DB 会自动填)
**但该列不是生成列** —— `alembic/baseline_generated.sql` 与真实库都是**普通可空列 + 唯一键**,
`app/model/profile.py` 的模块 docstring 第 2 条已明确:"当前版本必须由写入方**显式写入**客户 ID
(历史版本写 NULL),才能保证「每个客户最多一条当前快照」"。
后果:唯一键**形同虚设**(多个 NULL 不冲突)⇒ 不变式失效;且旧版本残留的值
一旦与新版本补上的值相同,就会**直接撞唯一键**。
> 这与本线先前修的 `CustomerProfileCandidateService._write_profile_snapshot` 是**同一个缺陷的另一处**
> —— 当时只找到一处,这次是靠真实链路跑出数据后核对才暴露出来。
## 改动
`app/service/profile_assembly_service.py`:
- 旧版本:`is_current = False` 的同时 `current_customer_id = None`
- 新版本:`is_current=True` 的同时 `current_customer_id=customer_id`
- 订正方法 docstring 的错误认知("生成列"→ 普通可空列 + 唯一键),并写明后果
## 已有数据订正
新增 `tools/fix_profile_snapshot_current.py`(**默认 dry-run**、幂等、`--apply` 才提交):
1. 先清空 `is_current=0` 却残留值的行
2. 再补写 `is_current=1` 却是 NULL 的行
3. **顺序要紧**:反过来的话第 2 步会与残留值撞唯一键
本机实测:清空 1 行、补写 1 行,复核两类异常均归零。
## 验证
- `mypy app` → 0 错 / 245 文件
- `pytest tests`(全量)→ `2 failed, 1418 passed, 1 skipped`
(2 个失败为既有环境项:httpx 把中文序列化成 `\uXXXX`,非本次引入)
- 端到端:真实对话 → 记忆抽取 → `memory_unit` 落库已实测通过(客户 9102
`preference:risk_level = "低风险"`,候选态)
|
2026-09-12 14:20:38 +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
|
be970aa46a
|
chore: 同步测试基线(1415 passed);合并主干袁聪场外线 4 提交
|
2026-09-12 14:08:50 +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
|
67ba1b8eee
|
docs: 标明 neo4j_profile_projection 当前未被生产装配(方案 A 取舍)及启用前提
该适配器与主干 `ProfileGraphProjectionService` 是同一件事的两套实现,对图的建模不同:
本模块按客户各建**私有** `Preference`/`goal` 节点、数据源是 `memory_unit`;
主干服务写**共享** tag 节点、数据源是 `user_facts` 且只投影已确认事实。
## 它不是"本来就没被装配"
| 提交 | 事件 |
|---|---|
| `f167390`(ZSY) | 新建该适配器 |
| `5e848f5`(ZSY) | `feat: wire neo4j projection into worker` —— 在 `__main__.py` 装配,此后一直是**活的** |
| `4d8edb4`(主干) | PR #7 合并后接线仍在,**仍是活的** |
| `57677f6`(本次合并) | 主动摘掉那段装配 ⇒ 失去生产引用 |
`__main__.py` 在本次合并中并没有冲突(git 自动取的是带接线的主干版本),
是本线解决完冲突后**主动手工删除**的。
## 但根本原因是它与方案 A 互斥
只要落实方案 A,它就必然失去引用 —— "删 `__main__.py` 接线、保留 runtime 那套"与
"保留 `__main__.py` 骨架、把它的 neo4j handler 换成主干服务"两种做法结果相同。
所以这不是方案 A 的副作用,而是"两套图投影本来就只能活一套"。
## 改动
- `app/infrastructure/neo4j_profile_projection.py` 文件头加 `.. warning::`:
写明当前未被生产装配、为什么、其单测保护的是**模块自身契约**而非"已装配",
以及**启用前提** —— 必须先决定"图的节点模型以谁为准",只加回 `__main__.py` 装配
会重新变成两套图投影并存。
- `docs/39-主干合并对策记录.md` §3.3 补完整时间线与上述论证;§6 第 1 条改为准确表述。
- **保留文件**(实现本身完整:`MERGE` 幂等、按 `profile_version` 判重不被旧版本覆盖、
写入前经 `sanitize_customer_service_message` 脱敏),去留待架构师定:
删除 / 保留为参考实现(当前取此)/ 反过来改用它(则方案 A 需重议)。
验证:`mypy app` → 245 文件 0 错;该模块 4 个单测通过;文档守卫 53 份无编号冲突。
|
2026-09-12 13:23:43 +08:00 |
|
wangjianlong_0626
|
91f1efcada
|
chore: 同步测试基线到合并主干后的实测值(1414 passed / mypy 245 文件)
|
2026-09-12 13:18:16 +08:00 |
|
wangjianlong_0626
|
cbca2cccbb
|
Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
|
2026-09-12 13:15:33 +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 |
|
wangjianlong_0626
|
3e081addf0
|
docs: docs/32、docs/33 让号至 37、38(主干续占 32–36);同步引用
主干 `qyqy_develop` 已占用 29–36(29 登录接口 / 30 投顾迁移TODO / 31 投顾灰度 /
32 平台侧交接与联调准备 / 33–35 ZSY 底座扩展确认往来 / 36 PR7 合并记录),
本线原用的 32、33 与之重号。按"以架构师为主线"继续让号。
- `docs/32-记忆投影链路实现说明.md` → `docs/37-记忆投影链路实现说明.md`
- `docs/33-架构对齐-记忆与画像投影链路.md` → `docs/38-架构对齐-记忆与画像投影链路.md`
(两次 `git mv`,保留文件历史)
- 同步更新 `AGENTS.md` 内 2 处引用、`docs/38` 内 2 处交叉引用
- 文档内部无自引用编号;全仓已无其它 `docs/32`、`docs/33` 引用(全量搜索确认)
验证:`tools/check_authoritative_docs.py` → checked 41 documents, no number collision
|
2026-09-12 13:00:30 +08:00 |
|
wangjianlong_0626
|
57f56bbcbe
|
fix(profile): 修 profile_snapshots 重复定义(会打挂 Worker);消费端兜底 memory_sources
## 1. 独立缺陷:`profile_snapshots` 被两个 ORM 类重复映射
排查 `memory_sync_outbox` 中 `target_store='neo4j'` 那行 `last_error='InvalidRequestError'`
时发现,根因不在图库,而在模型层:
- `app/model/profile.py` → `ProfileSnapshot` 映射 `profile_snapshots`
- `app/model/risk_questionnaire.py` → **另一个** `ProfileSnapshot` 也映射 `profile_snapshots`
SQLAlchemy 不允许两个类映射同一张表。实测:
| 场景 | 结果 |
|---|---|
| 单独导入 `app.main` / `app.worker.runtime` | 正常 |
| 单独导入 `profile_assembly_service` / `risk_questionnaire_service` | 正常 |
| **两者同时导入** | `InvalidRequestError: Table 'profile_snapshots' is already defined` |
Worker 在同一进程里既要处理 `profile.rebuild_requested`(走 `app.model.profile`),
又要处理投顾风险问卷(走 `risk_questionnaire.py`)——所以这是**会打挂 Worker 的缺陷**,
不是理论风险。
修法:`app/model/risk_questionnaire.py` 不再重复定义,改为从 `app.model.profile`
转出(re-export),既有 4 处 `from app.model.risk_questionnaire import ProfileSnapshot`
无需改动。原定义多映射的 `current_customer_id` 经全仓核查无人使用,故不保留
(`app.model.profile` 明确注明该列由数据库维护、故意不映射)。
## 2. 消费端兜底 `memory_sources`
`memory_sources` 是本线新增的投影入参,而投顾线两处生产者的 payload
(`{customer_id, profile_uuid, version, profile}`)没有这个键,原样会导致它们每次画像
变更都 `memory_sources is invalid` → 重试至死信。
新增 `WorkerRuntime._with_memory_sources()`:**键缺失或为 None** 时回退查询该客户
`memory_unit` 中 `status='active'` 的记忆,并记 warning(使"谁没提供"保持可见)。
语义成立:长期记忆是**客户级**而非画像版本级的,每条记忆自带 `version`,
适配器按 `memory_uuid + version` 幂等,故"用的是哪一版"仍确定。
**兜底不掩盖真错误**:键**存在但格式不对**时**不兜底**,原样交给适配器失败关闭。
实现上用键存在性判断而非 `isinstance`——后者会把"缺失"与"格式错"混为一谈,
那是初版实现里的一个真 bug,被新测试抓出后修正。
## 3. 补上此前欠缺的消费端全路径验证
此前"整合验证"是直接调适配器,跳过了 outbox 的领取→分派→状态更新。
- **失败分支**(Milvus 断开时实测):行被领取、按 target_store 分派、异常被捕获、
`status`/`retry_count`/`last_error`/`next_retry_at` 正确落库。
- **成功分支**(注入替身向量客户端):outbox 行 → `processed`、`processed_at` 已写;
不可投影的 `constraint:` 被跳过(只写 1 行);维度 1024;字符串客户号转 int;
**手机号脱敏生效**(`稳健型投资者,手机号 [手机号已隐藏] 请勿外泄`)。
- **兜底实证**:历史行 `id=5`(payload 无 `memory_sources`)经兜底后成功投递为 `processed`。
- **修复实证**:`id=6` 的 `last_error` 从 `InvalidRequestError` 变为
`RecoverableAgentError`(图库不可用)——证明重复定义缺陷确已消除,剩下的是环境问题。
## 4. 测试与验证
- 新增 `tests/unit/worker/test_runtime_profile_projection.py`(5 用例:已提供原样透传、
缺失兜底、空记忆给空列表而非删键、无客户号不兜底、格式错不兜底)
- 全量:`2 failed, 1312 passed, 2 skipped`(2 个失败为既有环境项,非本次引入)
- mypy:`Success: no issues found in 227 source files`
- 表结构审计:89 张业务表无缺失/意外(未改动任何表结构)
- 文档守卫:41 份文档无编号冲突
## 5. 文档
`docs/32-记忆投影链路实现说明.md` 增补 §6.1(兜底)、§6.2(重复定义缺陷)、
§7.1(消费端全路径验证)并更新验证表与文件清单;
`AGENTS.md` 新增"一张表只能有一个 ORM 类"易错点、校正测试基线数字。
## 未做
未改 `docs/00` 基线、未动数据库迁移、未改投顾线生产者代码、未启动常驻 Worker。
遗留:投顾线两处生产者的 payload 仍缺 `memory_sources`(已有兜底,不再死信,
但根治应由投顾线确认);Milvus/Neo4j 容器本轮不可用(Docker Desktop 崩溃),
`id=6` 停在 failed 属环境不可用、非代码缺陷。
|
2026-09-12 11:24:50 +08:00 |
|
wangjianlong_0626
|
cf70ac49e9
|
docs: docs/29 让号给架构师线(改号 33);补实施后续说明
架构师线 2026-09-12 推送的 `docs/29-Agent组员登录接口使用说明.md` 占用了 29,
与本线的《架构对齐 · 记忆→画像→图》重号。按"以架构师为主线"的口径,本线让号。
- `git mv docs/29-架构对齐-记忆与画像投影链路.md docs/33-架构对齐-记忆与画像投影链路.md`
(用 mv 而非增删,保留文件历史)
- 该文档开头补"⚠️ 后续(2026-09-12)"提示:结论**已落地实施**,指向
`docs/32-记忆投影链路实现说明.md`;避免接手人按原文的"本文只做核对与建议,
不改任何代码"误判为尚未实施。同时记录让号原因,避免编号来历不明
- 文档内部无自引用编号、全仓无 `docs/29` 引用(已全量搜索确认),故无引用需改
验证
- `tools/check_authoritative_docs.py` → checked 41 documents, no number collision
- docs 编号现为 30(投顾迁移TODO) / 31(投顾灰度) / 32(记忆投影链路) / 33(架构对齐)
说明:架构师分支上 `docs/21`、`docs/22` 各自有两份(投顾线带入的重号),
本线已在 c82e989 改号为 30/31,合并后该重号自动消除,无需额外处理。
|
2026-09-12 10:52:25 +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 |
|
wangjianlong_0626
|
cc6ae87f26
|
Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
|
2026-09-11 21:18:30 +08:00 |
|
wangjianlong_0626
|
e4c4099aaa
|
wip: 客服Agent + RAG + 画像收尾(基于 6516ccb)
|
2026-09-11 14:46:40 +08:00 |
|