ZSY_merge_bot
4c651ccce1
merge: integrate latest origin/qyqy_develop (frontend acceptance checklist + portal flow wizard)
2026-09-12 15:58:08 +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
2525fbdad9
feat(portal): 推广材料补成完整流程向导,并改为按 body.code 判成败
...
- 投顾视图:创建 -> 补结构化输入(含七项费率) -> 生成 -> 投递 -> 查询
生成后自动把 material_version_id 填进投递框
- 管理员视图:新增推广材料审核(promotion:review 只给管理员,职责分离)
- 统一结果渲染改看 body.code:本平台业务失败也返回 HTTP 200
(生成失败是 HTTP 200 + code=422 + '材料内容未通过合规校验')
- 输入骨架刻意不预填业绩(performance_info 有 show_* 开关可关),
费率填占位文本以便跑通流程;真实材料必须换成真实值
2026-09-12 15:45:09 +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
bfd622b592
feat: 统一登录门户(按角色分流五套工作台)+ 补齐权限覆盖缺口
...
- tools/portal.py:客户/风控/运营/管理员/投顾五个工作台,走真实登录与真实接口
- 新增 6 个此前从未建过的权限码(promotion:* 4 个 / financial:nl2sql:read / probe:read),
它们让推广材料与金融 NL2SQL 两条线对所有角色都是 403
- 投顾权限 10 → 25 项:补投资目标创建确认、agent:run、行情、知识检索、客户画像、推广材料
- 运营补 financial:nl2sql:read;create_test_user.py 不再硬编码角色 id(改按 role_code 查库)
- portal 的写请求补 Idempotency-Key 头(漏了会被 AGENT_INPUT_INVALID 拒)
- 新增 tools/check_permission_coverage.py 做权限对账
2026-09-12 15:21:34 +08:00
lzf_0626
c8d5fe11dc
feat: 新增平台功能测试台(自动覆盖 docs/05 §19 全部端点 + 一键只读冒烟 + 按身份测权限)
2026-09-12 14:57:17 +08:00
zhangshy
5634fdc023
完善风控登录权限和模型能力配置
2026-09-12 14:43:05 +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
lzf_0626
ffbcc229c2
fix: 删掉 NL 合并后残留的未使用变量 role_ids(ruff F841)
2026-09-12 14:17:42 +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
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
lzf_0626
4cc0ecdea9
fix: 种子改用 UPSERT 建用户且不覆盖密码,解除投顾 FK 导致的跑不通;记录 RBAC 对齐最终状态
2026-09-12 12:22:32 +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
cdb2de3e45
chore: 权限号段根治——9041-9046 并进种子;修正 grant 脚本与种子 9020-9034 的 id 映射冲突
2026-09-12 12:09:55 +08:00
lzf_0626
441364437c
merge: 合并 ZSY 的客服 Agent 接入(访客身份、画像候选、转人工工单)—— PR #7
2026-09-12 12:05:25 +08:00
lzf_0626
ade5e0c085
回复 ZSY 的底座扩展确认 v2:逐行核验两句确认;指出 docs/05 端点编号撞车、三个新权限未随代码合并;补权限种子脚本
2026-09-12 11:46:47 +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
lzf_0626
0358ea519f
投顾演示数据准备脚本:配好测评与归属,方案那步如实报告卡在证据来源
...
投顾线合并后 21 张 advisor_* 表全空,导致 published 返回 []、投顾 customer_ids 为空、
客户画像因缺测评返回 403。本脚本按依赖顺序补能自己造的那几样:
1. 产品目录:调 tools/import_hq_test_products.py 取真实行情(实测 19 个 ETF/LOF);
2. 客户风险测评:给 9001 补一条 C5、有效期一年(answers 里注明是演示数据);
3. 客户-投顾归属:9001 → 9020(assigned_at 往前留 5 秒,避开 DATETIME(0) 舍入陷阱)。
**方案那步不配** —— 缺的是证据来源不是技术:authoritative_tradable_products 是
fail closed 的(Missing or unverified evidence excludes a product),而造
advisor_product_suitability_reference 必须带 source_url 与 document_sha256,也就是真实的
销售适当性披露 / 基金合同文件(import_product_governance_reference.py 的两个 CSV,
不在仓库里,属环境数据)。脚本**不伪造这两个字段**(证据链红线),只检查并打印真正的解法。
实测:cust_t 的 /users/me/memory-profile 由 403 变 200(测评生效);admin 视角看 9020 的
customer_ids = ["9001"](归属生效);advisor 的 published 仍为 [](符合预期,尚无方案)。
⚠️ 顺带发现一处口径不一致:画像 fin_customer_profile.investor_type 是 C2,而新补的测评是
C5 —— 前者决定画像展示、后者决定适当性裁决,两者会同时出现在界面上。演示前需要统一
(要么把测评改成 C2,要么把画像也改成 C5)。
2026-09-12 10:34:28 +08:00
张胜宇
ef701c844c
merge: integrate ZSY customer service and profile capabilities
2026-09-11 22:31:51 +08:00
lzf_0626
306a514316
修正登录测试台的探针:投顾那个按钮其实该是客户视角
...
/api/v1/advisor/recommendations/published 的语义是"**我(客户)自己**已发布的方案" ——
服务端按 `customer_id == 调用者 user_id` 过滤(权限码 `product-recommendation:read:self`
的 `:self` 正对应这一点),所以**投顾调它必然为空**:筛的是 customer_id = 投顾自己的 id。
把它标成 customer 角色,免得再用投顾账号点它、然后怀疑权限。
查证过程中确认了三处数据空缺(投顾合并后那 21 张 advisor_* 表是新建的):
- client_facing_content = 0 行(没有任何投顾方案)
- sys_customer_assignment = 0 行(投顾没绑定任何客户,故 customer_ids 为空)
- fin_risk_assessment 里没有 9001(客户画像的前置"完成开户风险测评问卷"未满足)
2026-09-11 22:28:32 +08:00
lzf_0626
636892c8b1
登录测试台支持投顾:快捷按钮 + 只读探针
...
- 快捷填充加 advisor_t / abc12345;
- 探针加「已发布投顾方案」(GET /api/v1/advisor/recommendations/published),按 advisor 角色启用。
实测同一接口的对照:投顾令牌 200(data: []),客户令牌 403 —— 权限边界正确。
2026-09-11 21:50:39 +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
e29fce4012
新增登录测试台:一个记事本级别的前端,用来在浏览器里验证登录
...
tools/login_console.py —— 浏览器打开 http://127.0.0.1:8099 即可测。
与 chat_console.py 的关键差别:**它走真实登录接口**(POST /api/v1/auth/tokens)拿令牌,
而不是自己签。chat_console 是当初还没有登录接口时的权宜做法,这个测的是真链路。
页面能做的:
- 三个快捷填充按钮(客户 cust_t/123456、员工 risk_t/666666、管理员 admin_t/88888888);
- 登录后显示用户、角色标签、data_scope、令牌有效期(令牌只显示前 24 字符);
- 按角色列出可调的**只读**接口按钮(我的画像 / 风控概览 / 预警列表 / 角色清单 /
管理视角看某人的身份),一眼核对"登录给的 roles"与"接口实际放行"是否一致;
- 把真实状态码与响应 JSON 原样摊在页面上,并写明 401 涵盖三种原因、429 是登录限流。
实现上不动 app/ 一个字节:内层用 create_app(),本进程只做两件事 —— 提供静态页、
把 /api/* 同源转发(httpx.ASGITransport 进程内调用,不起第二个服务)。同源转发避开 CORS,
也免得浏览器直连主服务;只监听 127.0.0.1,私钥始终留在服务端。
实测:页面 200;通过代理用 admin_t 登录拿到真实 JWT(sub=9003);错密码 401。
顺带说明一条边界变化:docs/24 当初拒绝"给底座加 dev token 端点",理由是那种端点等于把
**任意身份**开放给任何能访问服务的人。现在有了密码校验,浏览器拿令牌不再等于
"谁都能冒充任何人",那条顾虑已消除 —— 所以这个页面不是绕过安全设计,而是设计补齐后的正常用法。
2026-09-11 21:26:45 +08:00
lzf_0626
edd8c53c3a
修正 create_test_user 里写反的理由:sys_user_role 有唯一键,先清后插是为了支持改角色
2026-09-11 21:17:49 +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
lzf_0626
62501747a6
新增 sys_user 认证现状探查(判断"账号密码登录"可行性)
...
tools/probe_auth_state.py:只读统计 sys_user 的 status / user_type 分布与 password_hash
的格式特征(只输出前缀与长度,不输出完整值)。
结论(docs/evidence/auth-state.json):库里 5 个用户,password_hash 全是占位符 ——
4 个是 'x'(tools/seed_test_rbac.py 写入),1 个是 '!worker-only-no-password-login!'
(alembic 迁移写入的哨兵值,名字本身就表明"不用于密码登录")。
has_real_password_hash = false。
⇒ 在本项目做"账号密码换令牌"不是"加个端点"的事:没有密码可校验,等于要新建一套密码体系
(选算法 + 加依赖 + 定义写入流程 + 改上游数据),而 password_hash 是基线已有字段
(规则 4:不得改变既有业务含义)。docs/05 §11 也已明确该职责属统一身份认证模块。
顺带记录一个数据质量问题:user_type 取值不统一(employee / 员工 两种写法并存)。
2026-09-11 20:30:27 +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
9957ddc191
Merge pull request 'NL_develop 评审答复与画像白名单补发脚本' ( #6 ) from qyqy_develop_1 into qyqy_develop
...
合并 qyqy_develop_1 第四轮工作:NL_develop 评审答复、画像工具白名单补发脚本、docs/08 §8。
2026-09-11 19:58:29 +08:00
Windows
fbb1171a45
feat: add quote sync command
2026-09-11 19:51:51 +08:00
lzf_0626
6c09cdecbd
新增补发画像工具白名单的脚本(dry-run + 合并前防呆)
...
tools/publish_profile_tool_whitelist.py:把 query_customer_profile 加进
customer_service:faq,供 NL_develop 合并后在本环境补发配置。
关键设计都来自这轮评审核实到的事实:
1. 继承走 ConfigReleaseService.effective_snapshot(),一次拿全三张受管表
(platform_config_item / prompt_template_version / model_routing_rule)。
既有的两个 agent_tools 发布脚本都绕开它、自己写 SQL 只查第一张 —— 用它们发版
会把 201 里那条客服闲聊提示词一起清掉,而 Agent 侧有兜底、功能看着正常、没有告警
(这个事故在本项目真实发生过一次)。
2. 同 key 覆盖:继承项里的 customer_service:faq 必须被本次新值覆盖,否则旧值
["search_knowledge"] 会被 admin 端校验拦下、整次发布失败。
3. 提示词搬运重分配 version(唯一键含 version),并按 PromptPayload 的 9 个字段挑字段、
归一化 input_schema/output_schema —— 漏带就是静默丢失。
4. 前置防呆:admin 端校验「配置 ⊆ 代码 allowed_tools」(admin_service.py:219-220),
合并前跑必然 422,而报错只有"配置超出 Agent 工具上限"。脚本先查代码上限:
dry-run 给警告后继续预览,正式跑直接中止。
5. JSON 列归一化:SELECT * 读出来的 JSON 列可能是字符串,不解析会静默走进
"生效版本里没有该 key"的分支(本脚本第一版就是这么错的,诊断打印才定位到)。
实测:--dry-run 打印 9 条配置项 + 1 条提示词(v2→v3 重分配)与唯一变化;
正式跑因代码尚未合并而中止,未产生任何写入。
2026-09-11 19:51:44 +08:00
Windows
b6429e0e9c
feat: add advisory product comparison tool
2026-09-11 19:20:32 +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
Windows
930dd59d67
feat: complete advisory market data refresh pipeline
2026-09-11 18:39:57 +08:00
Windows
95633188f1
test: extend advisor e2e fixtures
2026-09-11 17:59:47 +08:00