张胜宇
e38ece32bf
前端提交
2026-09-13 15:56: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
zhangshy
164d55a05a
修复合规问句被误判为收益承诺
2026-09-12 15:05:03 +08:00
Windows
026fec7514
Merge remote-tracking branch 'origin/qyqy_develop' into lzl_qyqy_integration
2026-09-12 14:41:13 +08:00
Windows
9c6f839a6e
feat: add advisor demo environment bootstrap
2026-09-12 14:40:53 +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
yuancong_0626
fedbf5a3ef
merge: sync latest origin/qyqy_develop before push
2026-09-12 13:16:57 +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
yuancong_0626
a0a489a055
chore: preserve RBAC seed consistency checks
2026-09-12 13:03:16 +08:00
张胜宇
c5adf01603
test: 补端点编号守卫与权限判定的 HTTP 层用例
...
补两道守卫,覆盖 2026-09-12 那次合并评审暴露出来的两个盲区。
一、`docs/05` §19 端点编号唯一性(新增 `tools/check_docs_endpoint_ids.py`)
- 背景:`tools/check_authoritative_docs.py` 只校验 `docs/` 的**文件名编号**,不校验
§19 的**端点编号**。两条线各自新增端点时都占了 `A034`/`A035`,合并后 §19 同时
存在两个 `A034` 与两个 `A035`,而文档守卫照样通过 —— 编号复用会静默遗留。
- 口径要点:**不枚举前缀白名单**,前缀从数据里归纳后连同数量一起输出。同一件事上
还踩过一次"扫描正则写成 `[AMKCS]`,漏掉 `O`/`R` 两段,把 62 个端点报成 55 个"——
漏掉的号段一旦被复用,脚本仍会报"重复 0"。所以覆盖报告会打印
`62 个端点编号 / 6 个号段(A×40、C×7、K×4、M×4、O×3、R×4)`,让漏扫本身可见。
- 只读、不依赖任何环境变量(纯解析文档),可在裸检出环境直接跑。
二、端点权限判定的 HTTP 层用例(新增 `tests/unit/api/test_permission_enforcement.py`)
- 背景:既有用例都通过 `build_request_context` 覆写注入**已经带好权限**的上下文,
只覆盖"有权限能通",覆盖不到"缺权限必须被拒"。而"权限码在库里根本不存在"这类
环境数据问题(本次三个新权限码)恰恰只会在这一层暴露:权限判定发生在身份解析之后,
服务层测试自己构造 `RequestContext`,权限字段由测试塞入,所以全绿也照样漏。
- 覆盖客服二期三个权限码对应的 5 个端点:
`handover:read`(工单列表/详情)、`memory:candidate:review`(候选列表/审核)、
`memory:candidate:confirm`(用户确认)。
- 正反双向断言:缺权限 → 必须 403 且错误码为 `AGENT_PERMISSION_DENIED`;
带权限 → **不能**再是 403(这一条把端点要求的权限码钉住,改动即红)。
- 只替换两处边界:`build_request_context`(跳过 JWT 与身份库)与
`AuthorizationService` 的审计落库(内存替身);真实路由、真实权限判定与真实 403 信封。
- 已做反向验证:给一个不存在的权限码时,5 个端点全部被拒(403),确认正向断言非空过。
验证(隔离 worktree,基线 `origin/qyqy_develop` = 4d8edb4):
pytest tests/unit tests/contract -> 1294 passed, 2 skipped, 0 failed;
ruff check app tests tools alembic 全通过;mypy app 244 源文件 0 错;
check_authoritative_docs(50 份文档无撞号)与本脚本均通过。
2026-09-12 12:57:16 +08:00
yuancong_0626
a501060e50
merge: merge yc into qyqy_develop
2026-09-12 12:50:14 +08:00
lzf_0626
4d8edb4b7f
chore: 号段一致性自检固化为 tools 脚本并纳入门禁;AGENTS.md 更新主分支口径、Agent/工具清单与测试基线
2026-09-12 12:29:20 +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
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
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
张胜宇
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
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
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
张胜宇
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
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
张胜宇
6c605ec3c1
feat: add memory sync outbox worker
2026-09-11 20:07:55 +08:00
张胜宇
f167390a8a
feat: add neo4j profile projection adapter
2026-09-11 19:54:46 +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
8cff6b35f9
fix(tests): tmp_path 落点改到仓库内,消除 33 个与缺陷无关的 setup 失败
...
问题:本机 `%TEMP%\pytest-of-Windows` 被权限更高的会话建过,当前用户无权写入,
于是所有用 `tmp_path` 的用例在 setup 阶段批量 ERROR(WinError 5)——实测 33 个,
散布在 offsite 附件预览、通知发送、promotion、worker 等处。这类噪声会让
"到底哪里坏了"完全看不出来(同事这批新用例首次把它暴露出来)。
修法:在 tests/conftest.py 覆盖 `tmp_path`,把根目录改到仓库内 `.workdir/pytest-tmp`
(已在 .gitignore)。语义不变——每个用例仍拿到一个**新建的空目录**(残留目录会污染断言)。
只覆盖 `tmp_path` 而不动 `tmp_path_factory`:后者是 pytest 私有构造,参数随版本变化
(实测直接实例化 `TempPathFactory(...)` 会 TypeError),而本仓库用例只用 `tmp_path`。
同时把 pyproject.toml 里那条 `basetemp = ...` 删掉:pytest **只在命令行认 basetemp**,
写在 ini 里会被静默忽略(实测无效),留着会让人误以为已配好。改为注释指向 conftest。
效果:不带任何参数 `pytest -q` 从「3 failed + 33 errors」变为「3 failed,0 error」。
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
Windows
930dd59d67
feat: complete advisory market data refresh pipeline
2026-09-11 18:39:57 +08:00