lzf_0626
|
e9539ae1be
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 18:24:24 +08:00 |
|
lzf_0626
|
857c106faf
|
投顾工作台:三接口支持按客户出方案 + 动作按所选客户分流
后端(向后兼容,customer_id 缺省即原行为):
- 三个请求契约新增可选 customer_id(推荐/资产配置/持仓诊断)
- AuthorizationService 新增 require_customer_scope(权限码 + 数据范围)
- 推荐/资产配置/持仓诊断服务支持指定被分析客户
- InvestmentGoalService 新增 current_for_customer
前端(employee-advisor/dashboard/index.html):
- 删除「本人」虚拟条目,客户列表改为 4 位真实客户
- 动作与自然语言入口均按所选客户带 customer_id 调真实后端
- 新增风评超期熔断闸门(FM-03,流水线停在 ② 画像)
- 统一对话入口从硬编码占位改为自然语言意图路由
|
2026-09-14 18:22:56 +08:00 |
|
lzf_0626
|
484cccc145
|
记录三个演示文档的移动,并把 .workbuddy/ 加进 .gitignore
## 文档移动(docs/ -> docs/演示用/)
`代码结构与关键逻辑梳理-2026-09-14.md`、`后端接口文档-2026-09-14.md`、
`软件需求文档-2026-09-14.md` 移入 `docs/演示用/`。
git 识别为 **rename**(状态 `R`),三份文档的历史完整保留,不是"删掉再新建"。
## .workbuddy/ 加入 .gitignore
它与文件里已有的 `.agents/`、`skills-lock.json` 属同一类 —— AI 编码助手的工具产物,
不是项目内容。放在同一节下,避免以后 `git add -A` 时误提交。
## 未包含(有意留下)
`docs/23-记忆分层与画像设计.md` 的工作区改动**没有**一并提交:那不是我改的,
也不在这次确认的范围内,留在工作区由作者决定何时提交。
(暂存时被 `git add docs` 一并带上,已 `git restore --staged` 退回。)
|
2026-09-14 12:09:23 +08:00 |
|
lzf_0626
|
3df6671ce3
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 12:07:18 +08:00 |
|
lzf_0626
|
e6d74059f2
|
修复员工工作台顶部导航重复(入口 JS 被引两次)
## 现象
打开 `employee-console/workspace`(平台治理),页面上出现**两份一模一样的顶部栏**:
南方财富 / 模拟基金服务 / 平台治理 / 风控中心 / admin_t 管理员 —— 连同页脚一起各两份。
## 根因:同一个入口 JS 被引了两次,且 `?v=` 不同
`app/static/portal/employee-console/workspace/index.html` 里曾同时存在:
<script type="module" src=".../workspace.js?v=20260913-3"></script>
<script type="module" src=".../workspace.js?v=20260914"></script>
浏览器按**完整 URL** 去重,两条不同 query 被当成**两个模块**、**各执行一次**。
入口里的 `mountShell()` 因此跑了两次,而它当时是
`document.body.insertAdjacentHTML('afterbegin', ...)` —— **无条件插入**,
于是 header 与 footer 各插两份。
来源是合并事故(`git blame` 定位):
| 行 | 提交 | 作者 |
|---|---|---|
| 旧 | `e31420df` | 卿云秋月(把版本号改成 `20260913-3`)|
| 新 | `f5d1b246` | 张胜宇(把版本号改成 `20260914`)|
两人各自把**同一行**的版本号换成新的,合并时两边都被保留,成了两行。
那个提交的信息是 "merge ... and retain risk review updates" ——
"retain" 在这里保留错了地方。
## 修法(两侧都堵)
1. **HTML 收敛成一行**(保留较新的 `?v=20260914`,与 `workspace.js` 内部
`api-client.js?v=20260914` 一致),并就地写明"改版本号是替换这一行、不是新增一行"。
2. **`mountShell` 加幂等保护**:已有 `.site-header` 就直接 return。
之所以不满足于只修那个 HTML —— 这个 bug 的症状很难反推到原因
(页面看起来只是"多了一块"),而以后谁加缓存版本号时很容易再犯一次。
## 防回归(两条测试,都做过负面验证)
- `test_no_portal_page_includes_the_same_script_twice`:扫 `app/static/portal` 下
**19 个页面**,把 `<script src>` 去掉 query 后比对,同一入口出现多次即失败。
负面验证:把重复行临时放回去,测试**精确报出**
`employee-console\workspace\index.html: ['/static/portal/employee-console/workspace/workspace.js']`,
恢复后通过。
- `test_mount_shell_is_idempotent`:断言 `app-shell.js` 里有那句幂等判断。
全站扫描确认**只有这一处**,不是批量问题。
## 实测
- `GET /portal/employee-console/workspace/` -> 200,页面里 `workspace.js` **只出现一次**
(`?v=20260914`);静态 HTML 中 `site-header` 出现 **0 次**(确认由 JS 注入,
所以 JS 执行一次就只插一份)
- `pytest tests/unit/api/test_portal_frontend.py` -> **43 passed**(41 + 新增 2 条)
- `ruff check` -> All checks passed
## 一点说明
这次是"改同一个版本号"的合并冲突处理失误,属于**流程问题**而非个人疏忽:
两边都想把缓存版本号推新,冲突解决时很容易两边都留下。
测试补上之后,这类错误会在 `pytest tests/unit` 里当场暴露。
|
2026-09-14 12:06:38 +08:00 |
|
lzf_0626
|
e59a9905a0
|
把「今日盈亏是硬编码 0」提级为待业务确认项(SRS Q22)
## 问题
`today_profit_loss` / `today_profit_loss_ratio` 在 `app/service/trade_service.py` 里是
**硬编码 `ZERO`**(持仓列表 L678、账户看板 L748-749),客户看板、持仓页、盈亏分析页的
「今日盈亏」永远显示 0.00。同一函数里的「持有盈亏」是**真算的**
(`market_value - cost_amount`,L660 / L726),所以只有这一项是占位。
三份 2026-09-14 文档其实**都记录了这个事实**(接口文档第 11 条、SRS 第 212 / 458 / 615 行),
但**都埋在「注意事项」里,没有进第 0 章那份「★ 请先回答本章」的待确认清单** ——
业务翻文档时看不到,演示现场被问到就答不上来。而且 SRS 第 212 行还把它**错引到了 Q7**
(Q7 讲的是场外阈值,与此无关)。
## 改动
**docs/软件需求文档-2026-09-14.md**
- 新增 **0.5 节「占位实现与本期边界」**,登记 **Q22**:客户看板的「今日盈亏」本期是否实现。
该节专门收这类「接口有、值还没真算」的偏差(不报错、单元测试全绿、只有业务看得出不对),
以后同类问题继续往这里追加。
- 第 0 章项数 21 -> 22;阻塞项清单补上 Q22。
- 修正 F-4.2 的错误引用(见 Q7 延伸 -> 见 Q22)。
- R2 状态改为「已提级为待决项 -> 见 Q22」。
- **R1 状态改为已修复**:组员提交 `4b7ee13`「修复管理员工作台函数缺少闭合大括号」
已解决 `workspace.js` 的语法错误。按该条自己要求的方式复验过 ——
`node --experimental-vm-modules` + `vm.SourceTextModule` 遍历 `app/static/portal`,
**44 个模块全部通过**(`node --check` 会假通过,不能用来判定)。
**docs/44-演示流程.md**(演示现场用)
- §3「可能被问到的问题」新增话术:直说这是占位值、指出持有盈亏是真算的、
指向 SRS Q22 与两条口径的结论;并建议「时间紧就避开这一栏,被问到不要含糊也不要现编」。
- 场景 2(客户资产)加现场提示:讲「持有盈亏 / 总市值 / 可用资金」,不要指着「今日盈亏」讲。
**docs/后端接口文档-2026-09-14.md**
- 第 11 条补上代码行号与交叉引用,并点明同一响应里的 `profit_loss` 是**真算的**。
## 顺带交给业务:两条口径的可行性(实测,非推测)
业务要决定的是「要不要做」,但「能不能做」也得一起给,否则业务选完还要再问一轮:
| 口径 | 数据现状 | 结论 |
|---|---|---|
| 用行情算(今收 − 昨收) | `fin_market_price` **只在同步时才写行**,实测每个产品**只有 2 行**(515450 只有 09-11 与 09-14,还跨了周末) | ❌ 取不到连续「昨收」 |
| 用净值算(今净值 − 上一交易日净值) | `fin_nav_history` 每个产品 **120~160 行连续交易日净值** | ✅ 建议按这个口径 |
⇒ 文档建议按**净值口径**实现,并提醒业务一并确认语义:净值是日终数据,
该指标实际是「最近一个交易日的盈亏」,**不是盘中实时**。
## 入库说明
三份 `docs/*-2026-09-14.md` 此前是**未跟踪文件**,本次一并入库(同一批文档交付物且互相引用)。
`.workbuddy/`(工具会话记忆)属工作区过程产物,**未入库**。
|
2026-09-14 11:24:51 +08:00 |
|
lzf_0626
|
eb89ffd0b4
|
按需造数据:给指定客户建持仓,并保证产品有可查的最新净值
## 需求
产品 15911、客户 10001 / 10002;最终要能查到这两户持有 15911 的持仓,
以及 15911 的最新净值。
## tools/seed_custom_holdings.py(新)
参数化脚本,不写死这次的代码与账号。手工写 SQL 会连踩一串坑,每一条都在本仓库真实发生过:
1. `fin_*` 的 `id` 列**没有 AUTO_INCREMENT**,不显式给 id 就报
`Field 'id' doesn't have a default value`;
2. `fin_holding.shares` / `current_value` 文档标注为"生成列",实测是普通 NOT NULL 列,
不写插不进去;
3. 只建 `sys_user` 不绑角色 -> 登录成功但查持仓 403;
4. 只建用户不设密码 -> `password_hash` 是占位符,**根本登不进来**;
5. 净值散在三处(`fin_product.current_nav`、`fin_nav_history` 最新一条、
`fin_market_price.close_price`),只写一处就会"产品页有净值、持仓页市值为空";
6. 往 `fin_market_price` 写"今天"的编造行情会**盖住真实行情** —— 下单按 `trade_date DESC`
取,编造行排在前面,过了 `MAX_QUOTE_AGE`(15 分钟)整个产品就一律 503
(周末尤其明显)。
脚本把这些一次做对,并保证上面第 5 条的三处严格一致(持仓市值恒等于 数量 x 最新净值)。
## 开户风险测评:走接口,不插表
这是这次最关键的发现。数据都建好之后,客户能登录,但访问 T001 / T006 一律 403,
报的是 `AGENT_PERMISSION_DENIED`「请先完成开户风险测评问卷」——
那是合规前置(`app/api/dependencies/auth.py` 的 `OnboardingRequiredError`),
不是 RBAC 没配好,两者表象一模一样,很容易查错方向。
`RiskQuestionnaireService.is_required` 只查 `fin_risk_assessment.valid_until > now`,
所以插一行就能过检查。**没有那样做**:插表不会生成画像快照、画像标签和记忆同步事件,
而投顾分析、适当性检查、Agent 对话都要读那些。改走真实接口
`POST /api/v1/onboarding/risk-questionnaire/submissions`,让业务链路自己把关联数据建全。
内置 13 题答案按 `_SCORE_RULES` 折总分 46 -> C4 成长型(产品是 R3,C4 有余量)。
等级**不从接口读**:提交返回里故意没有它(`_SCORE_RULES` 是服务端机密,注释写明
never included in customer responses),所以从库里读回并打印。实测读到 C4 / 46 分,
与手算完全吻合。
## 实测(接口层,不只是查库)
- P001 访客产品列表:21 个产品,★ 命中 15911,`current_nav=1.000000`
- P002 净值走势:30 个点,最近 3 条 2026-09-10 / 09-11 / 09-14
- 10001 / 10002 登录 -> T001 资产看板 200:总资产 101000.00、总市值 1000.00
- 10001 / 10002 -> T006 持仓 200:15911 数量 1000、最新价 1.000000、市值 1000.00
- 幂等复核:第二次执行不重复建记录、不覆盖持仓数量、不重复提交测评、不重置密码
- `ruff check app tests tools alembic hq.py` -> All checks passed
## 已知限制
产品代码 `15911` 是 **5 位**,而系统现有 20 个产品**全是 6 位**,且下单要求行情落在
`FundQuoteRuntimeConfig.allowed_codes`(六位数字)白名单里。
所以它**能建、能查持仓、能查净值,但不能下单**。
若是笔误,用 `--product-code 159911` 重跑即可。
|
2026-09-14 10:03:07 +08:00 |
|
lzf_0626
|
e096ffab22
|
修复投顾工作台白板:补上模块拆分时漏掉的 import
## 问题(合并进来的故障,不是本次会话改坏的)
合并 `origin/qyqy_develop`(f5d1b24 / 3134fe5)后 `pytest` 红了一条:
`test_advisor_dashboard_is_composed_from_feature_modules`。
查下去发现是**拆分做了一半**:
- 新建了 `advisor-config.js` / `actions-module.js` / `published-module.js`
- 把 `CONTENT_TYPE_LABELS`、`actionLabels`、`resultMessages` 从 `dashboard.js` 删掉了
- **但没有在 `dashboard.js` 里 import 它们**,`dashboard.js` 仍是拆分前的内联版本,
第 39 行还在用 `CONTENT_TYPE_LABELS`
后果不只是测试红:投顾工作台一打开就 `ReferenceError: CONTENT_TYPE_LABELS is not defined`,
页面渲染不出来;同时那两个新模块是**死代码**(没有任何地方 import 它们)。
`index.html` 是单入口(只加载 `dashboard.js`),所以模块必须由它 import。
## 修法:把重构接完,而不是把测试改掉
- `dashboard.js` 变成薄组合层:挂 shell、取 DOM、组合两个模块,其余逻辑不再内联
- `advisor-config.js` 收拢 `GOAL_STATUS_LABELS` / `BOOK_STATUS_LABELS`(原来内联在 dashboard.js)
- `actions-module.js` 接管「目标确认与方案书」
- `published-module.js` 直接可用
⚠️ 关键点:`bind()` 会给**所有** `[data-action]` 按钮挂 `open()`,而 `actions-module.js`
原先不认识 `goal-status` —— 直接接线会让它掉到最后一行的兜底分支、被当成
「资产配置」发出去(点"目标确认与方案书"却收到一份配置建议)。
所以把「目标确认与方案书」一并做进 `open()` 的分支里,并在两处留了注释说明这个约束。
「目标确认与方案书」这条功能本身要保留:此前工作台只有 4 个"生成草案"操作 + 1 个只读列表,
而确认目标与查看方案书这两个端点**有接口没入口**,导致目标永远停在 `pending_confirmation`、
方案书永远停在 `pending`(实测客户 9001 正是如此)。
## 防回归:tools/check_portal_modules.py(新)
上面那个 bug **不能靠现有断言发现** —— 那些测试断言的是"某个字符串在文件里出现",
而这里的问题是"定义搬走了、使用处还在",浏览器里才炸,Python 测试全绿。
新检查做四件事:`node --check` 按 ES module 解析语法、相对 import 的目标文件存在、
import 的名字在目标文件里真有 `export`、**用到的全大写常量必须有来源**。
第 4 条是抓这个 bug 的关键。写的时候踩了两次坑,都已修正并记录在文件里:
1. 第一版用 `(?<![\w.$])` 排除属性访问、却把**模板字符串整体**当字符串剔除了 ——
而 `CONTENT_TYPE_LABELS[row.content_type]` 恰好写在模板字符串里,
于是漏报、检查全绿。现在只剔除单双引号字符串,模板字符串保留(`${}` 里是真代码)。
2. 用负面验证确认它真的有效:把 `published-module.js` 的 import 拿掉后,
检查精确报出 `使用了 'CONTENT_TYPE_LABELS',但既没 import 也没在本文件声明`(exit 1);
恢复后 exit 0。没有这一步,这个检查就是个摆设。
同时接进测试:`test_portal_feature_modules_have_consistent_imports` 调用它,
保证以后每次 `pytest tests/unit` 都会执行。
## 实测
- `pytest tests/unit tests/contract` -> **1400 passed, 2 skipped, 0 failed**
(合并后未修时是 1399 passed + 1 failed)
- `pytest tests/unit/api/test_portal_frontend.py` -> 39 passed(38 + 新增 1 条)
- `python tools/check_portal_modules.py` -> 全部通过;负面验证 exit 1
- `ruff check app tests tools alembic hq.py` -> All checks passed
|
2026-09-14 02:07:44 +08:00 |
|
lzf_0626
|
d95ef09627
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 02:00:59 +08:00 |
|
lzf_0626
|
c5bfc2c998
|
钉住 .bat 的换行:新增 .gitattributes(*.bat text eol=crlf)
本机 core.autocrlf=true,git 在 add 时会把工作区的 CRLF 静默存成 LF;
换个 autocrlf=false 的环境 clone 出来就是 LF,而 cmd 对 LF-only 的 .bat
会行边界错乱(把 echo 的说明文字当命令执行,实测报
"AT 命令已弃用"、"']' 不是内部或外部命令",甚至真的去跑脚本)。
.gitattributes 里钉 `*.bat text eol=crlf`:仓库统一按 LF 存储、检出时转回 CRLF,
不依赖任何人的本地 git 配置。
验证方式(不是"我觉得会转"):删掉工作区的 bat 再 `git checkout --` 取回来,
实测 2090 字节、62 个 CRLF、无裸 LF,与写文件时的形态一致。
仓库里目前只有 `启动平台.bat` 一个批处理,影响面仅此。
|
2026-09-14 02:00:41 +08:00 |
|
lzf_0626
|
404f8f6aa5
|
固化接口体检脚本,并交付双击即用的启动 bat
## tools/portal_api_check.py(新)
把 2026-09-13 那轮"照着接口内容把前端测一遍"的验证固化成可重复跑的脚本。
当时靠这套验证查出 6 个真缺陷,但它们只存在于一次会话里,下次改动没人会重跑:
- `K002`/`K003` 是裸信封,前端按 `payload.data` 取 -> 知识库页显示"已入库 0 块 / 为空"
- 委托/成交详情页照着建表字段写,而接口返回视图没带那些列 -> 5 行永远显示 `--`
- 配置项/路由规则的 `PUT` 要 `If-Match`,却没有端点能返回该 digest -> 首次编辑必然 409
- 回复模板 `scene` 只校验长度不校验枚举 -> 非法值撞数据库 CHECK、冒成 500
- 路由规则表单固定 `max_attempts=2` 且 `fallbacks` 为空 -> 后端必然 422
- 模型端点手填 ID -> 未激活的 ID 直接 422
与 `e2e_smoke_test.py` 分工互补:后者测**业务链路通不通**,本脚本测**接口契约对不对**
(状态码、信封形状、字段名与前端期望是否一致)。
三档,默认只跑第一档:
- 默认只读 41 项(不动数据)
- `--write` 加测写操作:配置项 ETag 链路、`max_attempts` 越界、知识库上传+失效、
回复模板非法枚举、敏感词、推广物料全链路
- `--dangerous` 再加测会改生效配置的操作(激活配置版本会整版本替换,默认不跑,
脚本内注明了恢复办法)
实测:只读 39/39、写入档 54/54 全绿。
## 桌面一键启动(双击即用)
桌面 `启动金融Agent平台.bat` + 仓库 `启动平台.bat`,由
`tools/make_launcher_bat.py` 生成,只负责"双击"这一层,启动逻辑复用 `start.ps1`
(不重复实现,避免两边漂移)。参数可透传:`-Port 8100` / `-NoBrowser` / `-SkipPriceSync`。
不要手写这个 bat:必须同时满足 **GBK 编码 + CRLF 换行 + 无 BOM**。生成器自己
读回来校验这三条。踩过的坑:用 `write_bytes` 直接落盘时没转 CRLF,cmd 对 LF-only
批处理会行边界错乱,把 `echo` 的说明文字当命令执行(实测报
`AT 命令已弃用`、`']' 不是内部或外部命令`)。
## start.ps1 的三处修复
1. **解释器探测选错环境(真机双击失败的主因)**
原先只验证 `--version` 成功就选中,结果挑到一个 Python 3.10 环境:
本项目用了 `datetime.UTC`(3.11+)且依赖 `asyncmy`,启动当场 ImportError ——
而报错发生在**行情刷新**那一步,看起来像"行情源坏了"。
现在实测两项:**版本 >= 3.11** + `import fastapi, sqlalchemy, asyncmy, pydantic` 通过,
并在跳过时打印具体原因。
2. **`Select-Object -First 1` 掐断 native 管道**
`& $exe -c ... 2>&1 | Select-Object -First 1` 拿到首个对象后停掉上游管道,
等于把进程掐了、`$LASTEXITCODE` 变脏,于是**每个候选都被误判成"无法执行"**
(连装好的 jr_py313 也被跳过)。改为先整体接住输出、再在结果上取行。
3. **幂等 + 就绪探测 + Docker 自动拉起**
重复双击不再起第二个 API/Worker(API 按端口、Worker 按进程判断);
起完等 `/internal/health/ready` 真正应答才报成功;Milvus 不可达时尝试拉起
Docker Desktop 并最多等 60 秒(它不常驻,是"知识检索静默降级"最常见的原因)。
## 实测
- `启动平台.bat` 重复执行:解释器正确选中 jr_py313、依赖检查全 OK、行情已刷新、
两个服务识别为已在跑并跳过、就绪探测 1 秒
- `-Port 8199` 冷启动:新起 API 窗口,8199 的 `/internal/health/ready` 与 `/portal/` 均 200,
测试后已清理
- `ruff check app tests tools alembic hq.py` -> All checks passed
- `start.ps1` 语法解析通过(BOM 已保留)、bat 三项编码约束校验通过
文档同步:`AGENTS.md`(启动方式、bat 生成器、解释器探测两个坑)、
`docs/44-演示流程.md` §0.3/§0.4(双击启动、两条自检线的分工)。
|
2026-09-14 01:59:46 +08:00 |
|
lzf_0626
|
3cbfe10d62
|
fix(portal): 补齐运营三个新页面的同名 css 入口,修复合并进来的红灯契约测试
合并组员的运营工作台提交后,`test_every_portal_page_has_local_js_and_css_entry`
红了:门户每个页面都要有与目录同名的 `js`/`css` 入口,而新加的
`nl2sql` / `offsite` / `promotion` 三个页面**只有 js**,样式统一引 `operator-workspace.css`。
## 改动
给三个页面补上同名样式入口,并在各自 `index.html` 里引用(放在共用的
`operator-workspace.css` 之后,便于页面覆盖):
- `employee-operations/nl2sql/nl2sql.css`
- `employee-operations/offsite/offsite.css`
- `employee-operations/promotion/promotion.css`
三个文件当前**没有规则**,只写了用途说明 —— 它们的价值是把"页面专属样式"的位置
**确定下来**:这个约定的意义正在于此,否则将来只会继续往共用文件里堆。
⚠️ 只在每个 `index.html` 的 `<head>` 里**加了一行 link**,未改动组员的其它内容。
验证:`tests/unit/api/test_portal_frontend.py` 37 passed;
unit+contract **1398 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;三个页面与三个 css 均 200;e2e 冒烟 **40/40**。
|
2026-09-14 01:42:21 +08:00 |
|
lzf_0626
|
4c1e84b5a2
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-14 01:35:54 +08:00 |
|
lzf_0626
|
eded5896cd
|
fix(admin): 回复模板的 scene 加枚举校验,非法值不再冒成 500
## 问题
`agent_reply_template.scene` 有数据库 CHECK 约束 `chk_template_scene`,只允许
`disclaimer` / `low_confidence` / `compliance_block` / `transfer` / `model_failure` /
`system_busy` / `clarification` 七个值。
而 `ReplyPayload.scene` 只校验长度(`min_length=1, max_length=32`)——
传一个不在列内的场景会**穿过接口校验、撞上数据库约束**,最终以
`500` 冒出:
(3819, "Check constraint 'chk_template_scene' is violated.")
那本该是一次 `422` 参数校验失败。**500 与 422 的差别不只是状态码**:
前者会让调用方以为服务端故障、触发重试与告警,而实际是自己参数错了。
## 改动
`ReplyPayload.scene` 改为 `Literal[...]`(新增 `ReplyScene` 类型别名),
取值与 `chk_template_scene` **逐字对齐**,并在注释里写明这个对齐关系与本次事故。
实测:非法 scene → `422 AGENT_INPUT_INVALID`(不再是 500);合法 scene → `201`。
## 发现方式
这一处是**按接口逐条调用、逐个核对返回**时暴露的 —— 只看代码很难注意到
"schema 的宽松校验"与"数据库的严格约束"之间那道缝。同类风险仍存在:
凡是**表上有 CHECK 而 schema 只做长度校验**的字段,都有同样的 500 风险。
验证:unit+contract 1397 passed;integration 110 passed;ruff 通过;
mypy 251 文件 0 错;e2e 冒烟 40/40。
|
2026-09-14 01:35:34 +08:00 |
|
lzf_0626
|
daf73a2865
|
fix(portal): 照接口逐条核对前端,修掉 5 处「照着表结构写、看着像对」的缺陷
做法:把前端 `api-client.js` 注册的端点**按前端完全相同的方式**(同样的路径、参数、
身份)逐个调用,再拿真实返回去核对前端 render 用到的字段。
**这类问题纯读代码看不出来** —— 只有把真实返回和期望字段摆在一起才会暴露。
## 1. 知识库功能实际是坏的(K002 / K003 是裸信封)
`K002` 的成功体是 `{knowledge_ids, filename, chunk_count}`、`K003` 是 `{items, count}`,
**都没有 `data` 信封**。而 `request()` 默认取 `payload.data`(undefined),于是:
- 上传后前端显示「已入库 **0** 块」,而库里其实切了 23 块;
- 列表永远显示「知识库为空」。
端点表里标 `raw: true` 后(与 `V001` 同一做法)两者都正常。
实测:上传 4805 字符的产品手册 → 切 23 块并出现在列表里。
## 2. 委托/成交详情页有 5 行永远显示「--」
前端按 `fin_sim_order` / `fin_transaction` 的**建表字段**写了 `quote_source`、
`nav`、`fee_rate_snapshot`、`confirmed_at`、`auto_confirmed` ——
但这些字段**接口的返回视图没有带**(表里有、返回里没有)。已按实际返回重写字段表,
并在注释里写明"以接口返回为准,不要照表写"。
## 3. 配置项与路由规则的**编辑功能不可能成功**(接口缺口)
`PUT` 硬性要求 `If-Match`,校验的是该行内容的 digest;而这两个资源是 `detail=False`
—— **没有任何端点能返回这个 digest**(列表的 `meta` 只有 trace_id)。
乐观并发在"读不到版本"的前提下等于死锁:**首次编辑必然 409**。
(配置发布能用,是因为它有详情端点 `A003`。)
- 新增详情端点 `A048` / `A049`(`detail=True`),已登记 `docs/05` §19;
- 前端编辑前先 GET 详情取 etag,再带 `If-Match` 提交。
- 实测:编辑配置项与路由规则均 200;**不带 `If-Match` 仍返回 409**,
说明乐观并发没有被削弱。
## 4. 路由规则表单**必然提交失败**
前端固定写 `max_attempts: 2` 且 `fallbacks: []`,而后端要求
`max_attempts ≤ 端点总数`(主 + 兜底)→ 422「重试次数超过端点数量」。
改为 `1` 并注明约束。
## 5. 主端点手填 ID 会 422
后端对不存在/未激活的 `primary_endpoint_id` 直接 422「模型端点不存在或未激活」。
把输入框改成**下拉**,只列 `status='active'` 的端点(数据复用已有的端点列表)。
## 顺带
- `apiClient` 增加 `del()` / `put()`:发出的方法一直由端点表决定,所以 `post('K004')`
也能发 DELETE —— 语义太绕,现在意图与行为一致。
- 清理了测试期间上传的 31 条知识残留(客服会检索到它们),库内恢复到 23 条产品手册。
## 关于"逐条核对"的方法论
前两轮跑出来的 9 个和 5 个"失败"里,**多数是我测试脚本自己的假设错了**,不是前端问题:
`T001` 是 `{account, summary}` 嵌套、`RK002` 的字段叫 `risk_level`、
`RK002/RK004/RK005` 的 limit 上限是 5/10/10(前端传的正是 5/10/10)、
`AD011/A002/A047` 的 data 是裸 list。每一处都回到前端源码确认后才下结论 ——
**先把"我以为"改成"代码里写的"**,否则报告出去的就是假 bug。
验证:unit+contract **1397 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;§19 现 93 个端点无重复;e2e 冒烟 **40/40**。
前端等价测试:只读 31 项全绿、写操作(含 ETag 链路)9 项 8 绿 1 项因测试数据过短。
|
2026-09-14 01:19:22 +08:00 |
|
lzf_0626
|
e31420df29
|
feat(portal): 补齐四个前端缺口——客户详情、知识库管理、配置项与路由编辑
先审计了全部 **141 个后端端点**:前端注册 68 个,注册的**全部有效**(没有一个打不通)。
本提交补的是其中真正影响可用性的四类。
## 1. 客户端:委托详情(T004)与成交详情(T008)
两个接口后端一直存在,但订单页 / 成交明细页**只接了列表(T003 / T007)** ——
客户点不进任何一条记录,看不到成交价、费用构成、确认时间与行情来源。
改为**行内展开**(点「详情」在原行下方展开,再点收起),不跳页:
- 复用新加的公共样式 `.list-detail`(`common/customer-list.css`),两页共用而不各写一份
- 详情取不到时**不整页报错**:列表本身是好的,只恢复按钮并记一次错误
## 2. 管理员:知识库管理(K002 / K003 / K004)
客服的**全部回答都来自已入库的知识**,而此前**没有任何页面能管理知识库** ——
只能靠命令行脚本 `tools/seed_knowledge_demo.py` 灌数据,管理员既看不到也改不了。
新增「知识库」标签页:文档列表 + 上传(.txt/.md/.docx)+ 失效。两处要点:
- **K003 的成功体是裸的** `{items, count}`、**没有 `data` 信封** ——
`request()` 仍会去取 `payload.data`(那是 undefined),所以列表要两面都兜,
否则永远显示"知识库为空"、而库里其实有数据;
- 上传是 **JSON + base64**,不是 multipart(一期契约如此,见 `knowledge_management.py`)。
## 3. 管理员:配置项与模型路由(A001 / A008–A010 / A018–A020)
此前只能对**已存在**的版本走"校验→审核→激活",**既不能新建版本、也不能往里加配置项**
—— 新建的版本永远是空的、校验必然失败;模型路由规则同样既看不到也改不了。
- 新增「新建配置版本」表单(版本号 / 标题 / 变更说明)
- 每个版本加「内容」按钮(**与状态无关**:草稿阶段就要能加,否则版本永远空)→
展开该版本的**配置项**与**模型路由规则**,两者都支持新增与编辑
- 配置项的「值」按 JSON 输入并在前端校验:与其让后端 422,不如就地拦住并说清哪里不对
- `fallbacks` 暂不在界面编辑(提交空数组),需要时用接口补
这些端点**都已在 `docs/05` §19 有编号**,直接复用,无需新增编号。
## 4. 两个"死端点"查证后**保留**
初查发现 `RK013`(风控日报非流式,已被 RK014 流式取代)与 `ADVISOR_GOAL`
(投顾自己的目标;投顾是员工、没有目标 → 永远 404)注册了却无人调用,一度删除。
但 `tests/unit/api/test_portal_frontend.py` 立刻失败 —— 它把"页面会用到的端点"
固定成一张清单,**注册与调用是两件事**。已恢复注册,并就地注明它们当前无人调用、
但受契约保护。
顺带发现:**`RK013`–`RK015`(风控日报)也不在 §19**,与投顾 AD 段原先的情况相同,
属文档缺口(未在本提交内补)。
## 辅助改动
`apiClient` 增加 `del()` 与 `put()`:真正发出的方法一直由端点表里的 `method` 决定,
所以 `post('K004')` 也能发出 DELETE —— 但读代码的人会以为发的是 POST。
现在意图与行为一致。
验证:unit+contract **1397 passed**(含前端契约 37);integration **110 passed**;
ruff 通过;mypy 251 文件 0 错;e2e 冒烟 **40/40**;相关页面与静态资源全部 200。
|
2026-09-14 00:51:13 +08:00 |
|
lzf_0626
|
9a5259d787
|
feat(market): 存下行情源给的涨跌幅,产品列表与排行页不再显示"暂无"
## 问题
产品列表与排行页的「最近涨跌」全是"暂无"。原因是它只能由**两个交易日的收盘价**
现算,而 `fin_market_price` 每只产品每天只有一行 —— 库里头一天只有一天数据时
根本算不出来。
## 但行情源本来就给了这个数
腾讯行情接口的第 32 位就是**当日涨跌幅**(适配器此前只解析了开高低收量额,没取它)。
所以不必等第二个交易日去算,把源给的数存下来即可。
## 改动
- **迁移** `20260913_market_price_change_pct`:给 `fin_market_price` **新增一个可空列**
`change_pct DECIMAL(10,4)`。只加列、不改任何已有字段(AGENTS.md 规则 2 允许),
可空且不回填,既有读写方全部不受影响。
- **适配器**:`_parse_tencent` 增解析位置 4(昨收)与位置 32(涨跌幅)。
- **同步服务**:写入 `change_pct`;降级路径(净值兜底)没有这个数就写 **NULL**。
- **接口**:`_resolve_change_pct` **优先用存下来的值**;迁移前的历史行没有该列的值,
才回退到"今日收盘 vs 昨收"现算。仍为 `null` 时前端显示"暂无" ——
**不得当成 0**(`formatPercent(null)` 会渲染成 `+0.00%`,那等于说"今天平盘")。
## 契约测试同步
两处契约断言因结构演进需要跟进 —— 它们的作用正是拦住这类变更:
- `test_fund_readonly_contract.py`:`fin_market_price` 列数 **13 → 14**
- `test_advisor_migration_contract.py`:迁移 head 更新为新版本
(核心断言仍是"链收敛到唯一 head",此处只是钉住末端)
验证:实测 20 只产品**全部有真实涨跌幅**(如 515450 −0.43%、159511 +1.23%);
unit+contract **1397 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;`audit_schema` 与 `migration_state_check` 均通过;e2e 冒烟 40/40。
|
2026-09-14 00:30:06 +08:00 |
|
lzf_0626
|
de55c5c60c
|
feat(advisor): 登记 17 个投顾端点并补管理员复核入口(A047 待审队列)
## 1. docs/05 §19 补登 17 个投顾端点
这批端点此前**只存在于代码中**,§19 一条都没登记;而 §12 写的入口
`/api/v1/advisory-plans/**` 与实际路径 `/api/v1/advisor/**` 也不符(已修正)。
- **A041–A046**:管理员治理(配置回测、画像标签与漂移复核、推荐方案审核与发布)
- **AD001–AD011**:投顾自用。**新开 `AD` 号段**的理由:它与 A 段是两个不同的权限面
—— A 段是 `/api/v1/admin/**` 管理面,AD 段是 `/api/v1/advisor/**` 投顾自用;
混在一个号段里,"这条到底谁能调"就得逐条去读权限列。
- 另加 AD 段说明块:`investment-goal` 的两套权限码(`...:self` / `...:customer`)、
AD006/AD007 虽在投顾路径下却要求 `admin`、灰度开关 `enforce_advisor_rollout` 前置、
幂等范围(AD008/AD009 无幂等头)、以及 404/409 的失败口径。
§19 现为 **90 个端点 / 9 个号段**,无重复。
## 2. 管理员复核入口 + A047 待审队列
**发现一个让审核链路不可达的缺口**:`review` / `publish` 都要求调用方先拿到键
(推荐方案是 `content_id`、方案书是 `goal_no`),而此前**没有任何端点能列出待审内容**
—— 管理员拿不到键,投顾生成的东西就永远停在待审状态。
- 新增 `GET /api/v1/admin/advisor/pending-contents`(编号 **A047**):一次返回两类待审内容。
两类内容的"待审"取值不同(推荐方案 `pending_review`、方案书 `pending`),
只判其中一个会整类漏掉,所以用 `PENDING_STATES` 一并匹配。
- **为方案书一并查出 `goal_no`** —— 它的审核/发布端点(AD006/AD007)按 `goal_no` 寻址,
只给 `content_id` 的话管理员拿到列表也调不动。已由 integration 测试守住这一点。
- 管理员工作台新增「投顾复核」标签页:列出待审内容,支持审核通过 / 驳回 / 发布;
前端按 `content_type` 自动选择端点、寻址键与载荷
(方案书发布要 `{publish: true}`,推荐方案发布不读 body)。
- 发布前校验状态:未审核通过不允许发布,与 `publish_book` 的 `IllegalState` 一致。
## 3. ⚠️ 同时发现:投顾的三个分析功能对投顾本人不可用
`ProductRecommendationQuery` **没有 `customer_id`** 字段,而 `generate` 用的是
`int(context.user_id)`(`product_recommendation_service.py:69`)—— 即**把投顾自己**
当成了服务对象。投顾是员工、没有风险测评与持仓,于是实测:
POST /api/v1/advisor/recommendations → {"status": "profile_required"}
POST /api/v1/advisor/asset-allocation → {"status": "profile_required"}
**组合分析、资产配置、生成推荐草案这三个功能,投顾调用必然拿不到结果。**
这是"投顾功能很奇怪"的直接来源之一。修它要改接口契约(加 `customer_id`、
并确定"投顾能对哪些客户生成"的权限口径),属产品决策,未在本提交内改动。
验证:unit+contract **1397 passed**;新增 integration 用例 2 passed;ruff 通过;
mypy 251 文件 0 错;A047 实测管理员 200(带出方案书的 `goal_no`)、投顾 403。
|
2026-09-14 00:17:46 +08:00 |
|
lzf_0626
|
373bcb2a68
|
fix(test): 补 FakeSession.scalar 桩,修复合并进来的 15 个红灯用例
组员的 `2a3146e`("增加风控列表总数")在 `fund_query_repository.fetch_page` 里新增了
一次 `await self.session.scalar(select(func.count())...)` 来返回 `total`,但
`tests/unit/repository/test_fund_query_repository.py` 的 `FakeSession` 只有 `execute`,
于是该文件**15 个用例全部**挂在:
AttributeError: 'FakeSession' object has no attribute 'scalar'
## 改动
给 `FakeSession` 加 `scalar`:
- **返回值取 `len(_rows)`**。这些用例没有一处断言 `total`(只校验 `has_more` /
`next_offset` 与生成的 SQL),返回非负整数即可;`_rows` 是带 `limit+1` 的当页结果,
所以它**不是真实总数** —— 将来若要断言 total,需给这个桩加显式参数。
- **不写入 `statements`**。`last_sql` 取的是 `statements[-1]`,而有用例断言的是
**分页查询**的 SQL(LIMIT / ORDER BY / 过滤条件)。第一版实现把 count 语句也
append 了进去,`last_sql` 随即指向 count 语句,4 个用例的断言集体落空
(实测:15 failed → 4 failed → 0)。count 语句改记在 `count_statements` 里备查。
验证:`test_fund_query_repository.py` 18 passed;unit+contract **1397 passed**;
ruff 通过;e2e 冒烟 40/40。
|
2026-09-14 00:02:35 +08:00 |
|
lzf_0626
|
7c3a832104
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-13 23:56:44 +08:00 |
|
lzf_0626
|
421fed8569
|
feat(advisor): 补全投顾流程入口(确认目标 → 查看方案书),并修复页面缺缓存版本
## 1. 投顾流程此前是断的
投顾工作台只有 4 个「生成草案」操作 + 1 个只读列表,而**确认目标**与**查看方案书**
这两个端点虽然存在却**没有入口** —— 于是目标永远停在 `pending_confirmation`、
方案书永远停在 `pending`。
实测客户 9001 正是如此:`confirmed_at: null`、方案书 `review_status=pending`。
而「录入客户目标」成功后的提示是"已提交,等待确认与审核",**没有下一步可点**。
改动:
- 新增操作卡「目标确认与方案书」:输入客户 ID → 查询目标 → 显示目标状态、
方案书状态、收益区间 / 回撤 / 期限 / 基准 / 确认时间 → **确认目标** / **查看方案书**。
- **录入目标后自动跳到目标状态页**,把确认按钮摆在眼前,而不是停在"已提交"。
- `ADVISOR_CONFIRM_GOAL` / `ADVISOR_GOAL_BOOK` 补登记进 `api-client.js`
(`ADVISOR_CUSTOMER_GOAL` 早已注册却从未被使用)。
- 404「当前投资目标不存在」按**正常情况**处理(给下一步提示),不当故障报警。
⚠️ **审核与发布不在此范围**:`review_book` / `publish_book` 都要求 `admin=True`
(见 `investment_goal_service`),那是管理员的动作,投顾侧到"确认 + 看方案书"为止。
方案书仍会停在 `pending`,除非管理员侧也补上入口。
## 2. 9 个页面的 script 标签缺缓存版本参数
投顾工作台的 script 是 `dashboard.js`(**没有 `?v=`**),另有 8 个页面同样如此
(客户的持仓/下单/流水/盈亏/测评/成交明细、管理员工作台、运营工作台)。
浏览器会一直用缓存的旧 JS —— **改了代码也看不到效果**,这是"功能很奇怪"的一部分。
已统一补上 `?v=20260913`,投顾页的 js/css 提到 `-2`。
## 3. 过程中我损坏过 8 个文件,已恢复
第一次批量补版本参数时我用了 PowerShell `Get-Content -Raw` + `Set-Content`:
**PS 5.1 默认按 ANSI/GBK 读**,把 UTF-8 中文读成乱码再写回
(`角色与权限` → `瑙掕壊鏉冮檺`)并写入 BOM。`tests/unit/api/test_portal_frontend.py`
的断言当场抓到了它(找不到「角色与权限」)。
已 `git checkout` 恢复全部 8 个文件,改用 Python 重做(显式 UTF-8、不写 BOM、
保持原换行),现在每个文件的改动都是 **1 增 1 删**。
验证:`tests/unit/api/test_portal_frontend.py` 35 passed;unit+contract **1395 passed**;
ruff 通过;mypy 251 文件 0 错;确认目标实测 200
(`pending_confirmation` → `confirmed`,写入 `confirmed_at`),
重复确认被状态机以 409 拒绝。
|
2026-09-13 23:56:27 +08:00 |
|
lzf_0626
|
3ada1f6c87
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-13 23:38:57 +08:00 |
|
lzf_0626
|
7208713c17
|
feat(portal): 接入历史净值,产品详情页恢复真实走势图
`fin_nav_history` 一直是**空表**,所以产品详情页画不出走势图 —— 此前那条曲线是
前端 `mock-data.js` 里编的 12 个点位,接入公开产品接口时把它去掉了
(走势图最容易被当成真实业绩),页面改为显示"尚未接入"。本次补上完整链路。
## 1. 取数(app/infrastructure/fund_market_adapter.py)
新增 `fetch_nav_history()`:东方财富历史净值接口(`api.fund.eastmoney.com/f10/lsjz`)
分页取序列。
- **为什么不复用 `fetch_kline`**:它走 `push2his.eastmoney.com`,
该域名在本环境实测连接被拒(`RemoteProtocolError`);
- **为什么不复用 `hq.get_southern_fund_nav_history`**:那个函数校验**南方基金白名单**,
而 `fin_product` 里有非南方基金的产品(510300 是华泰柏瑞的),调它会直接 `ValueError`。
⚠️ 实现里踩到一个坑:接口**会忽略请求中的 `pageSize`**(实测固定每次返回 20 条)。
最初按硬编码的 30 判断"是否最后一页",于是 `len(items) < 30` 永远成立、只取到第一页 ——
走势图看上去"有数据",其实只有最近 20 天,而且毫无报错。
改为按首屏**实际条数** + `TotalCount` 推算页数后,同样区间取到 124 条。
## 2. 落库(tools/sync_nav_history.py,新增)
写入 `fin_nav_history`,按 `(product_id, nav_date)` 幂等 upsert。
实测:20 只产品 / **2513 行** / 2026-03-17 ~ 09-13;重跑**新写入 0 行**。
## 3. 接口(P002)
`GET /api/v1/products/{product_code}/nav-history`,编号 **P002**,已登记 `docs/05` §19。
鉴权口径与 P001 相同(要求有效令牌、不校验权限码,访客令牌可用);`days` 有界 1–365。
**表为空时返回 `count=0` 与空数组,而不是报错** —— 调用方据此显示"尚未接入",
**不得回退到编造曲线**。产品不存在或未上市 → `404`(否则前端分不清"没有数据"
和"没有这只产品")。
## 4. 前端
详情页按序列画 SVG 折线,期数标题改为动态("近 N 个交易日")。
表为空时仍显示"尚未接入"占位,并补上此前缺失的 `.detail-chart__empty` 样式。
`product-detail.js` / `.css` / `index.html` 的缓存版本参数一并 bump 到 `-7`。
## 5. 演示数据
`tools/seed_demo_data.py` 增加第 5 步「历史净值」(现 **11 步**),
否则换台机器演示时走势图又会是空的。
验证:P002 实测 515450 / 510300 各 120 个净值点;ruff 通过;mypy 251 文件 0 错;
unit+contract 1391 passed;integration 108 passed;e2e 冒烟 40/40。
|
2026-09-13 23:38:13 +08:00 |
|
lzf_0626
|
0b82053ca2
|
fix(rbac): 交易权限的 data_scope 写成了动作名,导致下单/成交静默 403
`tools/seed_test_rbac.py` 的 `PERMISSIONS` 行形状是
`(id, 权限码, resource, action, data_scope)`,而 T 段那四条写成了
`(9061, "trade:order:create", "trade", "order", "create")` ——
「resource, action」多写了一段,于是 action 落进了 `data_scope` 的位置。
## 后果是静默失效,不是放宽
`IdentityRepository.load_context` 只收集 `data_scope ∈ {self, own_customers, all}`
的权限,其余**整条丢弃**:
if row["permission_code"] and row["data_scope"] in rank:
于是客户在库里**明明有**这四个权限,`POST /api/v1/users/me/orders`、
`GET /api/v1/users/me/orders`、`GET /api/v1/users/me/transactions` 等 T 段端点
却全部返回 `403 AGENT_PERMISSION_DENIED`「缺少操作权限」。
而同一批里 `account:read:self`、`holding:read:self` 的 scope 是 `self`,照常 200 ——
现象特别像"只有交易坏了",几乎不会有人去怀疑**权限行本身写错了字段**。
因为 seed 是 `DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099` 的**重建**语义,
**每跑一次种子就重现一次**,而 `tools/seed_demo_data.py` 的第 1 步正是它。
## 修复与守卫
- 四条改为 `"self"`,并就地写清第 5 个字段是 `data_scope`、只能取三种值;
- `tools/check_rbac_seed_consistency.py` 增加**第 5 条检查**:每行字段数必须是 5,
且 `data_scope` 取值合法。已用模拟对象确认它能抓住事故写法
(非法 scope 报 2 条、字段数不足报 1 条、合法写法通过),
并由 `tests/unit/tools/test_rbac_seed_consistency.py` 纳入门禁。
修复后实测:库内非法 `data_scope` 归零(`self` 22 → 26);
`/users/me/{account/dashboard,holdings,transactions,orders}` 全部 200;
下单成交价 4.579(真实行情);e2e 冒烟 40/40;integration 106 passed。
|
2026-09-13 22:57:59 +08:00 |
|
lzf_0626
|
9cb0f6e474
|
feat(portal): 公开产品接口(P001)落地,访客三页改读真实数据
访客首页推荐、产品列表、产品详情此前读的是前端手写的
`app/static/portal/common/mock-data.js`:只有 8 只,且**其中 6 只根本不在
`fin_product` 里**(159915 / 512100 / 513100 / 511360 / 159645 / 159925),
还把海富通的 `511360` 标成"南方短融ETF"、把 `510500` 净值写成 6.742(真实 7.6027)。
`README.md` 把它记为"公开产品 HTTP 接口尚未实现"的临时方案。
## 接口
新增 `GET /api/v1/products`(编号 **P001**,已在 `docs/05` §19 总目录与 §19 说明中登记):
- **要求有效令牌但不校验权限码**:访客令牌的角色是 `visitor`、不带任何权限,
这与 `/api/v1/agent-runs`、`/api/v1/conversations` 面向访客的口径一致;
产品信息本身是公开信息。数据面只暴露 `fin_product`(`status='上市'`)与
`fin_market_price` 的最新一行,**不含任何账户/客户字段**(由 integration 测试守着)。
- 字段与 `mock-data.js` 对齐,因此前端渲染与筛选逻辑**一行未改**。
- `change_pct` **可能是 `null`**:当日涨跌需要两个交易日的收盘价,行情只同步过一天时
算不出来。前端对 `null` 显示"暂无" —— `formatPercent(null)` 会渲染成 `+0.00%`,
那等于对客户说"今天平盘",是编出来的结论。
## 前端
- 新增 `common/visitor-token.js`:访客令牌的**唯一实现**(这段逻辑原先只写在客服浮窗里,
现在四处要用;复制四份的话存储 key 与过期判断迟早不一致);`widget.js` 改为复用它。
- 新增 `common/product-notes.js`:产品级披露文案。510300"非本公司发行"那条是**合规披露**,
不能随 mock 一起删掉。
- 三个访客页改读接口,并显式区分 loading / error 状态。
- **删除 `common/mock-data.js`**。
- 产品详情页**不再画走势图**:`fin_nav_history` 目前 0 行,此前那条曲线是 mock 里
12 个编造点位 —— 走势图最容易被当成真数据,宁可不画,并显式说明"尚未接入"。
## 顺带修正
`min_amount` 在库里全是 0.00(那本是场外"最小认购金额"的概念,对场内按手交易的 ETF
不适用),页面不再显示"¥0.00"(会被读成零元起购),改为"1 手(100 份)起"。
验证:接口实测 `count=20`;ruff 通过;mypy 251 文件 0 错;unit+contract 1387 passed;
integration 106 passed;e2e 冒烟 40/40。
|
2026-09-13 22:57:48 +08:00 |
|
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
|
4629e7d5dd
|
docs(44): 补一步风控助手演示(意图分派已修复,实测置信度 1.0)
|
2026-09-13 22:08:39 +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
|
760d960762
|
docs(AGENTS): 补一键启动/演示入口,并记下 BOM 与行情 15 分钟两个易错点
|
2026-09-13 21:56:59 +08:00 |
|
lzf_0626
|
bfd3964ef8
|
feat(demo): 演示数据一键准备、启动脚本与演示流程文档
交付"别人能自己把系统跑起来做演示"所需的四件东西:
- tools/seed_demo_data.py 演示数据一键准备:10 步按依赖排序
(此前散在 10 个脚本里,没人知道该跑哪些、按什么顺序跑,且知识库那步
根本没有脚本、靠手工调接口)
- tools/seed_knowledge_demo.py 知识库演示素材灌入,幂等(先删同 source_file 再灌)
- start.ps1 启动 API + Worker,含依赖检查与**自动刷新行情**
- docs/44-演示流程.md 8 个主线场景的照读流程 + 排障表 + 账号/命令速查
两条硬约束同时写进了脚本和文档:
1. **Worker 必须常驻**:没有它客服对话一直停在 queued(前端只显示"超时")、
新知识不进 Milvus 且**没有任何报错**。
2. **行情有效期仅 15 分钟**(`trade_service.MAX_QUOTE_AGE`):超时后所有委托
直接 503「行情已过期」,而系统**没有自动刷新机制**。故 start.ps1 启动时刷一次,
文档另给"演示中途 503 时补刷、无需重启服务"的处置方法(已实测)。
实测证据:
- start.ps1 在 8099 完整启动,`/docs` 与 `/portal/` 均 HTTP 200(测试进程已清理)
- `tools/e2e_smoke_test.py` → 40/40 通过
- `tools/seed_demo_data.py --dry-run` → 10 步全部正常列出
- 文档中的下单命令实跑:510300 成交价 4.579、金额 457.90、手续费 0.05
|
2026-09-13 21:56:27 +08:00 |
|
lzf_0626
|
f37fd922a1
|
fix(seed): 演示预警的状态口径与代码对齐,并补 closed_at
## 结论:risk_action_service 没问题,我先前是误判
esolve(完成结案)写全了 status="已结案" + closed_at + handle_result(risk_action_service.py:105-107),
exclude 同样(:67-71)。我报的"结案没写 closed_at"看到的那条 ALDEMO0003
其实是**种子预置样本**,不是我以为的自己创建的那条 —— 我的冒烟脚本用
ALDEMO{时分秒} 命名,**撞上了种子的 ALDEMO0001-0003**,"已存在则跳过"把创建挡掉,
随后处置全 409,串起来就像"结案没写时间"。
真正的问题在种子数据,两处:
1) **状态口径与代码不一致**:ALDEMO0003 写成 status="已关闭",而
isk_action_service 里**没有任何动作会产生这个状态**(状态机只有
待处理 / 调查中 / 已排除 / 已结案)。演示时会被当成系统行为,排查时又查不到出处。
按它的 close_reason 语义(低风险 + 已核验为正常交易),它就是**关闭误报** → 改为"已排除"。
2) **终态缺 closed_at**:那条有 close_reason 却没有 closed_at,自相矛盾;
而且
isk_repository 按 closed_at 统计"当日结案",缺了它会被漏掉。
INSERT 补上该列,样本用 closed_at_offset_minutes 给值。
## 顺带修掉冒烟脚本的撞号
预警编号前缀 ALDEMO → E2E,注释里写明原因 —— 免得下次又操作到种子样本、
还把现象当成新 bug。
## 验证
- ALDEMO0003 现为 status=已排除 + closed_at 有值
- 库里所有终态预警(已排除/已结案)都带 closed_at;进行中的(调查中)不带 —— 口径一致
- 自己创建的两条走 resolve 后 status=已结案 且 closed_at/handle_result 都有值
- 冒烟重跑:40/40 通过,新造预警编号 E2E134351,处置闭环三段全 PASS
门禁:ruff 通过 / mypy 250 文件 0 错 / 单元+契约 1381 passed 2 skipped。
|
2026-09-13 21:44:08 +08:00 |
|
lzf_0626
|
2fcc4939e7
|
feat(tools): 新增全流程端到端冒烟脚本 e2e_smoke_test.py
把验收清单里"能用接口判定"的部分自动化:6 条线 40 项,打真实 HTTP、逐条打印 PASS/FAIL。
不是 pytest 的替代 —— 单测看代码正确性,它看"部署后底座是不是活的"。
覆盖:
A 访客 令牌 → 提问 → 收到回答 → 未转人工
B 客户 登录 → 看板/持仓/流水 → 真实下单成交 → 委托详情 → 会话与转人工
C 风控 概览/队列/详情/八类证据/通知/扫描 → 处置闭环(确认→调查→结案)
D 投顾 已发布方案 E 运营 场外邮件与邮箱状态
F 管理员 角色/权限/身份/配置发布/模型端点/审计/转人工工单/画像候选
两个实现细节值得留意:
- **各端点 data 形状不统一**(角色列表裸数组、持仓在 holdings、成交明细在 transactions、
资金流水在 entries、知识列表不套 data),所以这里用 listed(payload, *keys) 依次尝试,
并在注释里写明"别猜字段名"—— 我自己写联调脚本时连续猜错三次。
- 库里没有"待处理"预警时**自动造一条**再验证处置闭环,否则该环节只能跳过
(实测库里的预警常常都已被处置过,第一次跑就跳过了)。
新增 --read-only:跳过所有写操作,用于生产或不想动数据时。
实测:40/40 通过(含真实下单 201 已成交、风控处置闭环三段全 PASS)。
docs/40 补上"一键冒烟"一节。
|
2026-09-13 21:39:37 +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
|
b6281ba6bc
|
docs(40): 把行情同步列为下单前必跑的前置
全流程验收时踩到:不跑 tools/sync_market_prices.py 就下单,只有 2 只产品有行情、
其余直接 503,而且错误信息只说"行情已过期",看不出根因。把它写进第 0 节,
并说明数据源(腾讯行情 / 东财只有行情类域名不可达)与总份额口径。
|
2026-09-13 21:15:43 +08:00 |
|
lzf_0626
|
369213f50d
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-13 21:14:22 +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
|
de208a039d
|
docs(40): 补全流程验收实测到的接口形状差异
跑全流程 E2E 时连续踩到(都是调用方视角的真实摩擦点):
- 各端点的 data 形状不统一:账户看板 data.account + data.summary、持仓 data.holdings、
成交明细 data.transactions、资金流水 **data.entries**、角色列表 data(裸数组)、
知识列表 items(不套 data)。我写脚本时连续猜错三个字段名,一度误判"资金流水没记录"。
- 创建类接口返回 201 而非 200:下单 T002、创建会话 C001、访客令牌 V001 都是 201。
两条都写进"通用预期",并说明是实测结论。
|
2026-09-13 20:40:16 +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
|
ff37b0b476
|
docs(40): 记录知识治理端点的两处接口差异
联调时踩到并查明(更正前一版判断:不是接口故障,是调用方解包方式不对):
1) GET /api/v1/knowledge/list **不套 data 信封**,直接返回 {"items": [...], "count": N},
而多数端点返回 {"code":…, "data": {…}}。按 data.items 解包会拿到 0 条,
看着像"库里没数据"——我据此误判过一次,也因此在重灌时没能用列表接口批量删除,
改为按 id 逐个 DELETE。
2) 上传是 POST /api/v1/knowledge/upload(/documents 会 405);
DELETE /api/v1/knowledge/{id} 的语义是标记 expired + 投向量删除事件,不是物理删除。
两条都写进验收清单的"通用预期",避免下一个人重复踩。
|
2026-09-13 20:26:56 +08:00 |
|
lzf_0626
|
7fa4b4c4a5
|
feat(knowledge): 手册补「购买基金需要什么条件」一节并重灌
排查「购买基金需要什么条件」仍转人工的根因:**不是检索缺陷,是知识覆盖不足**。
实测该问法下手册 1.1 节其实排第 1(score=0.5416),但低于 MID_SCORE(0.55),
且与次优间隙只有 0.0195 < MIN_GAP(0.07),所以按置信度规则转人工 —— 规则本身工作正常。
对照:手册里有对应条目的问法分数是 1.0 / 0.7946 / 0.7932,链路健康。
补 1.6 节「购买基金需要什么条件」,只写有依据的事实(开户状态、风险测评与等级匹配、
按手交易的整数倍委托),不编造资金门槛或资质要求。
重灌过程顺带发现:GET /api/v1/knowledge/list 返回 0 块,而 fin_knowledge_meta 当时
有 22 行 active —— 管理端列表与实际入库不一致,所以删除只能按 id 逐个走
DELETE /api/v1/knowledge/{id}。这条接口的问题单独记,不在本次改动范围。
重灌结果:旧 22 块标记过期并投删除事件,新 23 块写入;问「购买基金需要什么条件」
命中 1.6 节 score=0.7793(≥ HIGH_SCORE 0.75),客服从转人工变为直接作答(4.08s)。
|
2026-09-13 20:25:57 +08:00 |
|
lzf_0626
|
fef4c826ff
|
fix(worker): 知识写入 Milvus 必须探测字段名并补齐必填字段
背景:走 `POST /api/v1/knowledge/upload` 灌了 22 块场内基金知识,MySQL 全部写入成功、
向量事件也全部投递,却全部同步失败(重试 3 次进死信),客服检索不到新知识。
逐层定位出三个真问题,都在写入侧:
1) **字段名硬编码**。检索侧早已按 AGENTS.md 改用运行时探测
(`app/core/knowledge_schema.py`:`doc_id`↔`knowledge_id`、`content`↔`snippet`),
写侧却一直硬编码 `knowledge_id` / `snippet`。本机集合实际是
`doc_id`/`content`/`chapter`/`visibility`/…(架构师那套 schema),于是
`Attempt to insert an unexpected field knowledge_id` 整条失败。
现在两侧共用 `resolve_schema` 的同一份映射表,调用方只用**逻辑**字段名;
集合没有的字段(如另一套环境无 `intent`)跳过而不是报错。
2) **非 nullable 必填字段没给**。改完名字后报
`Insert missed an field chapter to collection without set nullable==true`:
集合里存在、调用方没提供的 VARCHAR 字段必须补值。VARCHAR 的判据用
`params.max_length`,不必 import pymilvus 的枚举。
3) **补值不能一律空串**:`visibility` 留空会被检索侧
`visibility == "public"` 的过滤整行排除。这条最隐蔽 —— `query` 查得到、`search`
查不到,表现为「入库成功但客服永远答不出新知识」,比写入直接失败更难定位。
现在按 `FIELD_DEFAULTS` 给有语义的字段默认值。
Worker 侧把 `fields` 的键改成逻辑名(`snippet` → `content`),上限表随之改名。
**测试**:原来的桩没有 `describe_collection`,所以这条路径从未覆盖到真机 schema
(这正是缺陷长期存在的原因)。现在桩提供两套真实 schema 并参数化,另加
「跳过集合没有的字段」「探测不出必要字段必须失败关闭」两个用例。
**验证**:22 块重新同步后 `fin_product_collection` 236 行、`visibility` 全为 `public`;
问「南方沪深300ETF 的起投金额是多少」命中新块 `score=0.7932`(≥ `HIGH_SCORE` 0.75),
客服从「转人工」变为直接作答,端到端 3.34 秒。
**另**:新增 `docs/43-场内基金产品手册(知识库入库版).md` —— 从 `docs/42` 抽出
**客户可见**的纯净内容(去掉全部内部决策备注)供入库;`docs/42` 保留为草稿与决策记录。
|
2026-09-13 20:08:31 +08:00 |
|
lzf_0626
|
6f397ab513
|
feat(data): 510300 回填真实值并在产品页披露;511810 口径查清
按项目方决定处理三件事。
## 510300(沪深300ETF)
决定:保留 fund_manager = "南方基金" 作演示口径,**不**改成"华泰柏瑞"
(改了它会退出 MarketQuoteSyncService 的同步范围,投顾与知识条目也不该再算自家产品),
但净值与费率改成真实值,并在产品页显式披露它不是本公司产品。
- 回填(tools/backfill_product_snapshot.py --codes 510300 --apply --overwrite):
current_nav 4.500000 → 4.5794、current_nav_at 同步为 2026-09-11 15:00:00、
management_fee_rate 0.5000 → 0.15、custodian_fee_rate 0.1000 → 0.05
- 产品页披露:mock-data.js 给这只加 product_note,详情页新增提示块渲染它
("本产品为同指数参考产品,非本公司发行的基金,仅用于功能演示")。
同时把 mock 里这只的净值/费率也对齐真实值,避免出现"页面显示 mock 值、
库里是真实值"两套数字。
- 产品详情页的 css/js 版本参数提到 20260913-5,避免旧缓存。
## 511810(货币ETF南方)
之前说它"三个数量级不一致"是**我比错了接口**:QUOTE_API 返的是**场内交易价格**
(货币 ETF 约 100 元/份),而 current_nav 的口径是 DWJZ 单位净值。
查证结果:库里 0.2661 正是 09-11 的真实 DWJZ,快照时间也对得上 —— 不是脏数据,是旧值。
口径由其余 19 只的一致性确定(current_nav == DWJZ)。
要不要刷新到最新(0.2332)属运维节奏问题,**本次未执行**。
## 最小交易金额
按项目方口径:一律**以产品字段为准**(100.00 元),不用「1 手 × 净值」推算金额 ——
后者会与字段值不一致,客户会看到两套数字。docs/42 的 1.1 节据此重写。
## 工具改名
tools/backfill_product_fees.py → tools/backfill_product_snapshot.py:扩展为
净值 + 费率统一入口,新增 --codes 限定范围(避免顺手把全部快照刷成最新,
全量刷新是 ProductHistorySyncService / MarketQuoteSyncService 的职责)。
安全设计不变:默认 dry-run、默认只补 NULL(净值永不为空,所以刷净值需显式
--overwrite)、写入走单事务。
门禁:ruff 通过 / mypy 249 文件 0 错 / 单元+契约 1377 passed 2 skipped。
|
2026-09-13 19:47:10 +08:00 |
|
lzf_0626
|
02ba1befc0
|
feat(data): 取真实费率并回填 fin_product;发现 510300 的身份问题
## 取费率
项目原有的行情适配器只有 names/quotes/history,没有费率。东财也没有可用的 JSON 接口
(`FundArchivesDatas.aspx?type=jjfl` 实测正文只有一句 `var apidata=`),
所以给 EastmoneyFundAdapter 加了 fetch_fees:抓 f10 基金概况页
(fundf10.eastmoney.com/jbgk_{code}.html)去标签后匹配字段名,取管理费/托管费/
销售服务费/申赎费,并顺带带回基金全称。取不到的基金不进返回(而不是给全 None),
免得调用方把"没抓到"当成"该基金确实没有费率"。
实测 20/20 全部取到。
## 回填
新增 tools/backfill_product_fees.py,默认 dry-run、默认只补 NULL:
- 只补空值:写入 38 个字段(19 只 × 管理费/托管费),已执行
- 已有值一律不动 —— `510300` 库里的 0.5000/0.1000 保持原样,要改需显式 --overwrite
- 写入走单事务
回填前 grep 确认:`fin_product.management_fee_rate` / `custodian_fee_rate`
**目前没有任何业务代码读取**(advisor 域那两个 fee 字段属于另一张表
advisor_product_contract),所以这次回填不改变任何现有行为。
要让客户真的看到费率,还需要接口与前端读它 —— 那不在本次范围。
## 两个数据问题(都指向 510300)
1) **它不是南方基金的产品**。数据源返回的基金全称是
「华泰柏瑞沪深300交易型开放式指数证券投资基金」,而库里 fund_manager 写「南方基金」。
其余 19 只全称都是"南方…"开头,只有它例外。
影响面不只是知识库:MarketQuoteSyncService 按 fund_manager='南方基金' 筛选同步对象,
所以它会被当成自家产品一起同步、一起展示。
2) **它的费率是照抄的**。库里 0.5000/0.1000,真实 0.15/0.05;而 0.50/0.10 恰好是
159329/159382/159511/588890 这四只的真实费率。结合它的净值快照时间是当天
(其余 19 只停在 09-11)、净值取整数 4.500000,判断是演示用途的人为设定。
这两条都由用户决定怎么处理,本次只记录、不擅自改(510300 的 fund_manager、净值、费率
三处原值都没动)。
## 其他
- tools/fetch_live_quotes.py 增加费率核对段落与 --no-fees(费率要串行抓 45KB/只,较慢)
- docs/42 用真实费率重写 1.2 节与新增 4.3 真实费率清单,3.1 节标出 510300 的身份问题
- docs/42 的数据缺口表更新:费率不再是缺口
门禁:ruff 通过 / mypy 249 文件 0 错 / 单元+契约 1377 passed 2 skipped。
|
2026-09-13 19:39:16 +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 |
|
lzf_0626
|
d9d3420cf0
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-13 19:28:09 +08:00 |
|
lzf_0626
|
131effcfae
|
feat(tools): 新增 fetch_live_quotes 拉真实行情并核对库里快照
项目本来就对接了东方财富真实行情(app/infrastructure/fund_market_adapter.py:
push2.eastmoney.com 的行情接口 + api.fund.eastmoney.com 的历史净值接口),
下单链路取 quote_price/quote_source 走的也是它。之前做知识草稿时用的是
fin_product 表里的快照,没有去核对真实数据源,这次补上。
新增 tools/fetch_live_quotes.py:
- 用项目自带的 EastmoneyFundAdapter 取数,不自己拼 HTTP、不引第三方行情库
- 产品范围与 MarketQuoteSyncService.sync 一致(南方基金 / SSE,SZSE / 上市)
- 逐只对比真实净值与 fin_product.current_nav,只报告差异、**不写库**
(刷新快照是 ProductHistorySyncService / MarketQuoteSyncService 的职责)
实测结果(2026-09-13,20 只全部取到真实数据):
- 18 只完全一致(库里 6 位小数、数据源 4 位,是同一笔数)
- 2 只不一致:
· 510300 沪深300ETF:库里 4.500000,真实 4.5794。库里快照时间是今天 08:19:37,
其余 19 只都停在 09-11 15:00:00 —— 说明这是被人为改过的演示值(4.5 凑"1 手 450 元")。
需要定:知识用真实值还是演示值;若交易按库里的 4.5 成交而知识说 4.5794,两处口径会打架。
· 511810 货币ETF南方:库里 0.266100,NAV_API 给 0.2332,而 QUOTE_API(场内实时价)
给 100.012 —— 差三个数量级,说明两个接口对这只的含义不同,需人工核实口径。
docs/42 随之更新:
- 产品清单改用真实净值,并加上当日涨跌与净值日期,注明取数来源与复现命令
- 3.1 节的沪深300ETF 条目改用真实值 4.5794,并列出两处待确认口径
- 新增 4.2 节记录本次核对结果与两只不一致项的判断依据
|
2026-09-13 19:27:58 +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
|
52ae38efd7
|
docs(40): 补齐前端验收清单(投顾/运营页、访客浮窗、Worker 前置)
- 关键修正:第 0 节原先写「停常驻 Worker」,方向是反的 —— 客服对话**必须**有
Worker,否则 agent_run 停在 queued,前端只显示超时。补上排查第一步(查最新
那行的 status)与本机实测延迟(端到端 4.1-4.8 秒,受理只占 0.05 秒)。
- 新增第 6 节投顾工作台:数据口径(本人 + 名下归属客户,不用 data_scope)、
方案书审核与发布必须由管理员做(admin=True)、归属数据不随环境走。
- 新增第 7 节运营工作台,含客户/风控访问被 403 拦的实测。
- 新增 2.5 节访客客服浮窗:访客令牌的角色与权限、访客与客户走不同检索工具名。
- 新增第 9 节「看起来像 bug、其实是设计」:置信度阈值 0.75/0.55/0.07 与三条实测
分数逐条对应(0.8203 直接答;0.6818 但间隙 0.0034 转人工;0.5221 转人工),
说明「总转人工」的根因在知识库内容而不是代码;另两条是投顾接口非越权、
方案书复核环节。
- 第 1 节更新为 18 个页面路径 + 12 个静态资源,全部 200(实测值)。
- 第 10 节补入本轮修的 4 个问题(角色落点、访客白名单、mock 声明、发布脚本丢提示词)。
- 新增第 11 节已知未修:知识库无场内 ETF 内容、fin_knowledge_meta 与 Milvus 不一致
(列表接口实测 count=0)、品牌名三处不一致、ADVISOR_GOAL/ANALYSIS 声明未用等。
- 附录补演示账号,并注明 9006/9020 的口令不在 set_user_password.py 的演示规则里。
|
2026-09-13 19:10:21 +08:00 |
|
lzf_0626
|
680c2a0749
|
docs(AGENTS): 补 Worker 常驻与发布脚本继承范围的踩坑记录
- 新增一条:客服/风控对话必须有常驻 Worker(python -m app.worker)。没有它时
agent_run 停在 status=queued、worker_id 为空,前端只会显示"客服响应超时/繁忙"
—— 看起来像链路慢,实际是没人处理。附排查第一步(查 agent_run 最新那行)
与本机实测延迟(端到端 4.1-4.8 秒,其中受理只占 0.05 秒)。
- 更新发布脚本那条:继承范围必须覆盖全部三张受管表(此前只搬 platform_config_item,
把 customer_service_chitchat 提示词静默漏在旧版本里),并写明知识类意图要同时发
search_knowledge 与 query_knowledge。
|
2026-09-13 19:04:29 +08:00 |
|
lzf_0626
|
2098185477
|
fix(portal): 客服浮窗轮询参数调整,压感知延迟并留足超时预算
现象:访客在公开页问客服,等约 9.5 秒后报「客服响应超时」。
根因不在链路快慢 —— Agent Worker 没在跑,run 一直停在 queued,
前端把「没人处理」呈现成了「超时」。
启动 Worker 后实测同一条链路(受理 0.05s + 意图分类 + 知识检索 + 落库):
4.11s / 4.09s / 4.82s,三条全部 succeeded。模型已经是 deepseek-flash,
延迟主要来自一次意图分类加一次 embedding,不是模型选型问题。
不过原来的轮询参数余量确实偏薄:350ms 后首次、之后每 700ms 一次、共 14 次,
约 9.5 秒封顶,后端稍一抖动就撞上;而且平均要多等半个轮询周期才看到结果。
改为 300ms 后首次、之后每 500ms 一次、共 40 次(约 20 秒):
感知延迟压到半秒内,同时给模型与检索抖动留出余量。
超时文案也从「客服响应超时,请稍后重试」改为「客服繁忙,暂时没能给出答复」,
不再暗示是响应慢。代码注释里写明:若仍然超时,先确认 Agent Worker 已启动。
|
2026-09-13 19:03:49 +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 |
|
lzf_0626
|
4fdf1762e6
|
docs: 验收清单改为面向正式前端 /portal/
|
2026-09-13 16:22:25 +08:00 |
|
lzf_0626
|
5b6eaddc4f
|
docs: AGENTS.md 补正式前端口径与最新测试基线;portal.py 标注为调试工具
- 新增前端一节:app/static/portal/ 挂载在 /portal/,四套页面,端点表在 common/api-client.js,
启动用 uvicorn app.main:app(模块级变量是 app 不是 application)
- 测试基线更新为 2026-09-13 实测:mypy 249 文件 0 错 / 单元+契约 1376 passed / 集成 104 passed
- tools/portal.py 文件头标注降级为跨角色联调工具
|
2026-09-13 16:20:39 +08:00 |
|
lzf_0626
|
f4eaa2f631
|
fix(seed): 客户种子改为已开户;portal.py 标注为调试工具
- seed_test_rbac.py 把 fund_account_status 硬编码成 'closed',与 create_test_user.py
的 {customer: 已开户, employee: closed} 口径不一致,导致种子建的客户永远未开户
- 影响:GET /users/me/account/dashboard、/users/me/cash-ledger 恒返回 404,
正式前端 T001/T009 两个页面打不开(端点本身是好的)
- 改为按 user_type 取状态;配合 tools/seed_sim_account_demo.py 开虚拟资金账户后
T001/T006/T007/T009 全部 200
- tools/portal.py:正式前端已在 app/static/portal/,本工具降级为跨角色联调工具
|
2026-09-13 16:19:54 +08:00 |
|
lzf_0626
|
f7f43742fd
|
chore: 移除误提交的 .agents/skills 与 skills-lock.json(AI 助手配置,与项目无关)
- 这 15 个文件随 e38ece3 前端提交一起进了仓库,内容与本项目无关
- 用 git rm --cached 从索引移除(本地文件保留),并加入 .gitignore
|
2026-09-13 16:17:00 +08:00 |
|
lzf_0626
|
bbe575f38f
|
fix(portal): 模态打不开时立刻 resolve,避免 await 永久挂起(表现为点击没反应)
- askConfirm/askText 在 openModal 失败时立即 resolve(false)/resolve(null):
此前 Promise 永不 resolve,await 会一直挂着,用户看到的就是点了没反应
- toast/openModal 在容器缺失时给出明确提示而不是静默失败
- 顶栏显示页面构建时间(取 portal.py 修改时间),用于一眼确认加载的不是缓存旧页
|
2026-09-12 16:41:26 +08:00 |
|
lzf_0626
|
280c8e5026
|
docs: 清单风控章节按 17- 前端合并约束重写(15 项,含实测证据与三个坑)
|
2026-09-12 16:31:55 +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
|
6155f4589e
|
feat(portal): 按风控前端合并约束重写风控工作台
- 概览五指标(总量/等级/待处理/超时/重点预警)
- 预警队列:每页 5 条、风险等级优先、8 项筛选、游标分页、稳定行高与省略号
- 预警详情弹窗:23 个 alert 字段 + 客户信息 + 五个处置动作
- 处置:确认接收(二次确认)/进入调查/关闭误报(必填理由)/结案(必填结论)/升级(必填原因)
- 八类证据(customers/products/transactions/capital_flows/holdings/login_records/
alerts/notifications)+ 五项筛选;注意是 holdings 不是 positions
- 通知记录、日报 SSE 流式生成 + 内容编辑 + 多邮箱发送
- 新增 Toast + 模态框,替换全部 17 处原生 alert(17- 文档明令禁止原生弹窗)
- 风险等级筛选值改用 高/中/低(预警对象用「高」,概览 levels 用「高风险」,口径不一致)
|
2026-09-12 16:30:54 +08:00 |
|
lzf_0626
|
cb13f9cf45
|
fix(types): trade_service._next_id 的 model 参数标注改为 Any(mypy: type 无 id 属性)
|
2026-09-12 16:30:54 +08:00 |
|
lzf_0626
|
6e916aab03
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-12 16:08:39 +08:00 |
|
lzf_0626
|
76923d7e8b
|
fix(portal): 补齐写操作的必填 body(风控升级/解决、场外六个写接口)
- 风控:escalations 要 reason、resolutions 要 resolution(字段名不同),
且真实顺序是 先确认接收 才能升级/解决(否则 409 请先确认接收预警)
- 场外:六个写接口都必填 operator_id(防伪校验,须等于当前登录用户),
confirmations 还要 decision(中文枚举)、notifications 还要 notification_type、
recalculate 要 fund_code+application_date;门户自动带当前 user_id
- 实测:recalculate 200 code=0;不传 operator_id 必得 422(证明该字段必须)
- 清单 §3/§4 更新为实测结果并列出各写接口的必填字段表
|
2026-09-12 16:08:38 +08:00 |
|
lzf_0626
|
552055ee2d
|
fix(portal): 修风控预警列表 422(limit 上限是 5,改为不传);运营邮件改用 page/page_size;审计 limit 钳制到 1-100
- risk/alerts 传 limit=20 会 422 query.limit<=5,导致整张表格渲染不出来、
行内确认/升级/解决按钮随之消失 —— 这正是验收清单 3-3/3-6~3-9 看不到功能的原因
- 实测列表返回 2 条预警;处置接口有状态前置条件(409 = 当前状态不允许)
- 清单 3-3/3-6 更新为实测结果并写明 limit 上限与状态机约束
|
2026-09-12 16:03:22 +08:00 |
|
lzf_0626
|
49ebda4cb7
|
docs: 新增前端验收清单(64 项,逐项给出预期结果,区分实测与按契约)
|
2026-09-12 15:48:31 +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
|
14f5078491
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-12 15:21: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
|
4c00db5636
|
docs: docs/32 工具索引补上功能测试台与两个新守卫,修正已过期的权限号段口径
|
2026-09-12 14:57:56 +08:00 |
|
lzf_0626
|
c8d5fe11dc
|
feat: 新增平台功能测试台(自动覆盖 docs/05 §19 全部端点 + 一键只读冒烟 + 按身份测权限)
|
2026-09-12 14:57:17 +08:00 |
|
lzf_0626
|
c200a61cd5
|
docs: docs/32 更新到 2026-09-12 状态,补功能性测试的触发条件与外部依赖前置
|
2026-09-12 14:22:34 +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 |
|
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 |
|
lzf_0626
|
39f7b81200
|
docs: 记录本机 RBAC 对齐的实际执行结果,以及 seed_test_rbac.py 被投顾 FK 挡住这一既有破坏
|
2026-09-12 12:16:41 +08:00 |
|
lzf_0626
|
e0468a9d9f
|
docs: 记录 PR #7 合并门禁结果、两处顺手修正与权限号段冲突的来龙去脉
|
2026-09-12 12:10:03 +08:00 |
|
lzf_0626
|
cdb2de3e45
|
chore: 权限号段根治——9041-9046 并进种子;修正 grant 脚本与种子 9020-9034 的 id 映射冲突
|
2026-09-12 12:09:55 +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 |
|
lzf_0626
|
c7226d0b69
|
回复 ZSY 的处置回执 v3:核实两处修正;更正端点编号计数(62 非 55);预检合并树并定位 test_security.py 两处密钥路径漂移
|
2026-09-12 12:04:18 +08:00 |
|
lzf_0626
|
ade5e0c085
|
回复 ZSY 的底座扩展确认 v2:逐行核验两句确认;指出 docs/05 端点编号撞车、三个新权限未随代码合并;补权限种子脚本
|
2026-09-12 11:46:47 +08:00 |
|
lzf_0626
|
244f03917b
|
回复 ZSY 的底座扩展确认;登记 docs/00 的一处已知偏差;更新 AGENTS.md 过期口径
## 对 ZSY 三件事的核实与裁决(docs/33-ZSY底座扩展确认-回复.md)
1. **访客身份**:`data_scope="public"` 我扫了全平台 26 个消费点,对未知值的行为一致
fail closed(6 处 `== "all"` 判假、1 处 `!= "all"` 会要求 customer_ids 非空否则 403、
1 处显式白名单直接拒),**安全,不用改**。但有一条硬约束必须先确认:全平台有
**72 处 `int(context.user_id)`,只有 3 处做了防御**——若访客的 sub 不是纯数字,
其中"权限被拒时要写审计"的路径会把本该 403 的情况变成 500。已要求他确认
sub 为纯十进制数字、且 visitor 分支复用同一套 `jwt.decode`(含 isdecimal 校验),
而不是另写第二个鉴权入口。
2. **客服 Agent**:`allowed_roles` 扩集合与确定性安全路由可接受;`recalls_customer_memory`
是新声明字段,已要求**默认值必须是 True**(否则会静默关掉所有既有 Agent 的记忆召回,
界面看不出来、只表现为回答变差)。品牌名(南方科技 → 奶龙基金责任有限公司)属业务
口径,已上报项目方定,不由技术侧拍板。
3. **current_customer_id**:**他的判断正确**。我独立实测 information_schema:
`EXTRA=''` 且 `GENERATION_EXPRESSION=''`,配合 baseline_generated.sql 无 GENERATED
子句、seed_profile_demo.py 的注释、profile_repository.py 用原生 SQL 显式写入,
四处一致 ⇒ `docs/00` 第 783 行"生成列"的描述是错的。处置:按真实 schema 映射成
普通可空列、**不改 docs/00**(规则 1)、**不补迁移**(DDL 本身正确,为让文档成真而加
生成列会改变既有列语义,违反规则 4)、但**必须登记这处偏差**。
另:他删了两个死代码文件(含一处硬编码 Milvus 字段名,违反 AGENTS.md §E),方向认同,
但要求他在 PR 里附上两条 git grep 的实际输出以证明"全仓唯一引用是它自己的单测"。
## 落实我在回复里承诺的两件事
- `docs/08-数据库结构审计基线.md` 新增"六、已知文档偏差",逐条登记上述偏差(含四处
证据与处置口径),并注明发现方式;
- `AGENTS.md` 环境口径新增 `MILVUS_LOCAL_URI` 的坑:配了会让健康检查与部分检索指向
本地 Milvus Lite 文件,出现"健康检查正常、实际查的是另一个库";并注明 `milvus-lite`
属本地开发依赖,应放 `optional-dependencies` 而非主依赖。
## 顺带更新 AGENTS.md 的过期口径
- 表数 68 → **89**(场内 51 + 场外/推广 17 + 投顾 21),并注明投顾那 21 张的登记文档待补;
- 测试基线从"1034 passed / 1 failed"改为实测值:ruff 干净 / mypy 228 文件 0 错 /
1207 passed 0 failed / integration 99 passed,并指向 docs/32;
- mypy 那条从"本机 181 错、双方不可比"改成可操作的判据:先对版本,根因是某一侧虚拟环境
没满足 pyproject 的 sqlalchemy>=2.0,<3 / mypy>=1.14,<2;并写明**不要装 sqlalchemy2-stubs**
(那是给 1.4 的,2.0 自带 py.typed,装了反而按 1.4 API 报一批新错)。
文档守卫:42 份无编号冲突。
|
2026-09-12 11:27:28 +08:00 |
|
lzf_0626
|
39f261da14
|
新增 docs/32-平台侧交接与联调准备.md
把当前状态整理成可交接的一份:分支与门禁、三条线合进来的内容、平台侧补齐的登录与
RBAC 只读接口、修掉的真缺陷、演示账号与环境口径、新增工具索引、联调清单、悬置决策项。
其中三条是这一轮实际踩出来的,值得留档:
1. 环境数据不随代码合并(config_release / sys_role / sys_permission / Milvus schema /
advisor_product_*)—— 三条组员线各自都在这上面栽过一次;
2. 合并后必须**单独**跑 alembic upgrade head 与 tools/check_authoritative_docs.py:
三家合并里两次带进文档重号、两次库与代码版本不一致(一次"版本号跑了表没建"、
一次"表没建版本号也没跑"),两者都靠 upgrade 收敛,重号只有守卫能发现;
3. 投顾演示数据里"方案"那步配不出来,缺的是**证据来源**(source_url / document_sha256),
必须来自真实的销售适当性披露或基金合同文件;不伪造这两个字段是关键红线。
另记下环境口径:mypy 与测试数必须带环境读,NL 那边 184 错与本机 0 错的差异根因是
对方的 .venv 没满足 pyproject 的 sqlalchemy>=2.0,<3 / mypy>=1.14,<2,不是代码质量问题。
文档守卫:41 份无编号冲突。
|
2026-09-12 11:23:46 +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 |
|
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
|
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 |
|
lzf_0626
|
edd8c53c3a
|
修正 create_test_user 里写反的理由:sys_user_role 有唯一键,先清后插是为了支持改角色
|
2026-09-11 21:17:49 +08:00 |
|
lzf_0626
|
48513f3b97
|
docs/29 补第 0 步:转交前先确认对方环境有角色与密码(环境数据不随代码合并)
|
2026-09-11 20:50:59 +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
|
76e87a33a7
|
更新 AGENTS.md 的表数口径:51 → 68(场内 51 + 场外/推广 17)
合并袁聪那 17 张表后,本文件仍写"52 张含 alembic_version = 51 张业务表",与
tools/audit_schema.py 实测的 68 张对不上。改为写明构成,并指向
docs/28-场外与推广域数据表登记.md —— 否则下一个人看到 68 会误以为是有人绕过基线
偷偷建表(audit_schema 的期望值是从迁移动态推导的,不是手写清单)。
核验命令里的 .\.venv\Scripts\python.exe 一并改成 python,与本节"各用本机可用的那个"
的口径一致(架构师环境是 conda)。
|
2026-09-11 20:24:39 +08:00 |
|
lzf_0626
|
3029d0ca9f
|
更新 release-state 证据:画像工具白名单已补发(release 254 active)
|
2026-09-11 20:24:07 +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 |
|