一、密钥轮换(新增工具 + 操作手册) - 新增 tools/rotate_api_keys.py:--check 体检 + 交互式轮换;getpass 不回显、 自动备份 .env.bak-<时间戳>(已被 ignore 命中)、校验不过整体不写入、 三个 Qwen 变量写同一值 / 两个 DeepSeek 变量写同一值。 实测 --check:Qwen 三变量同值且非空、DeepSeek 两变量同值且非空。 - 新增 开发文档/D3.8-模型密钥轮换与凭据安全操作手册-2026-09-20.md(CS-OPS-2026-023): .env 5 个变量与读取方取证、五步流程、3 个坑、复核清单、回退方式、能力边界。 - 口径确认:model_endpoint_config.secret_ref 存变量名 ⇒ 轮换只改 .env,不动 DB; 但必须重启 API + Worker。 二、门禁修复:docs/ 编号撞车 - tools/check_authoritative_docs.py(D3.4 N-14 登记的验收命令集之一)实测 FAIL: 我方 docs/46 docs/47(2026-09-20 建)与投顾组 docs/46-投顾Agent需求文档.md docs/47-投顾Agent功能架构文档.md(2026-09-16 建)同号。 - 按「后到者让位」改名:docs/48-可改文件白名单.md / docs/49-底座会签申请单-2026-09-19.md, 同步 8 处引用。修复后:checked 54 documents, no number collision,exit 0。 三、前端品牌残留(W12 合并静默回退) - employee-advisor/dashboard/index.html 与 customer/advisor-plans/index.html 的 <title> 仍是 南方财富(投顾组分支带回)→ 按 DEC-27 改为 南方基金。 - HTTP 实测两页标题已正确;全仓 app/ 复查 南方财富 = 0。 四、文档口径校准(12 处事实漂移) - D1.1:D2.1 版本 v5.3 → v6.26(§4.0 / §4.1 / §1 / §2 四处长期错误); §8 四行遗留项闭合(D-5 / D-6 / D-7 / 仓库副本同步)+ 新增 §22 §23 留痕; §10.2「本区不在任何 git 仓库内」更正为已入库;新增两编号 ⇒ 计数 56 → 58 全量同步。 - D2.2:顶栏徽标 v2.4 与元数据 v2.5 自相矛盾 → 统一;「投顾已清除」→ 状态更新 (模块 2026-09-20 已恢复,但客服范围裁定 §1.7 / RK-10 不变)。 - D2.3:徽标 v1.0 · 7 批次 51 项 → v1.1 · 8 批次 57 项;投顾清除后果 + §7.1 头号风险 + 风险表 + 不触碰行全部加恢复口径。 - D2.4:v1.3 变更说明 ⑦ / §1.4 Out of scope / Q-09 加投顾恢复口径。 - D2.5:advisor_t 自相矛盾口径改写为账号表一行 + 口径更正;五项自检首选改为 一键脚本 启动演示.bat / demo.ps1;补 D3.8 与未发布 advisor:* 白名单登记。 - D2.6:门禁数字 1856/2 → 1909/3 skipped、ruff 19 → 20、补 portal_api_check 行; §10 两项已闭环(密钥轮换已工具化、A-10 组 3/4 已补签);头部加 W12/W13 状态更新。 - D4.5:顶部状态更新补指向 D4.7。 - 新增 开发文档/D4.7-投顾模块恢复记录-2026-09-20.md(CS-PURGE-2026-014): 时间线、8 项恢复动作、客服线不变的结论、DEC-19 理由更正、遗留 1 项、失误登记。 - _consistency.py(维护侧):§三 改为「投顾状态口径检查」,合法语境扩为 清除史 / 恢复史 / 不属本 Agent 范围。 五、回归实测(全绿) - pytest -q:1909 passed / 3 skipped / 0 failed - ruff check app tools tests:20(与 W12 持平,未引入新债) - mypy app:2(= 既有基线) - tools/check_authoritative_docs.py:54 文档无编号冲突(exit 0) - tools/e2e_smoke_test.py --read-only:31/31 - tools/portal_api_check.py:40 项 通过 35 / 失败 0 / 跳过 5 - _eval_harness/http_probe.py:11/11 succeeded - _consistency.py:GATE PASS - demo.ps1 -SkipStart -NoBrowser:五项自检全过、退出码 0 - 权威副本 ↔ 仓库:逐字节一致(客服agent 24 / 开发文档 52) 六、未做(如实登记) - 投顾 config_release 工具白名单(advisor:*)仍未发布 ⇒ 投顾 Agent 工具调用 fail closed (实测 active_agent_tools 仅 customer_service:* 4 项 + risk:* 4 项)。与客服线无关; 要演投顾线先跑 tools/publish_advisor_demo_config.py --apply。 - 两把 key 的实际轮换需你在控制台建新 key(无法代做),流程见 D3.8。
10 KiB
D3.8 · 模型密钥轮换与凭据安全操作手册
体系编号:
D3.8· 域:三、现行权威·完整版与专项 · 编号体系见D1.1§4.0
编号:CS-OPS-2026-023 | 版本:v1.0 | 日期:2026-09-20 | 状态:现行(操作手册,答辩后当轮执行) 性质:操作手册(SOP) —— 回答「两把模型密钥已经在会话/文档里出现过明文,怎么安全地换掉、怎么证明换对了」。 配套:工具
group_fqcd_jr\tools\rotate_api_keys.py(本手册的唯一执行入口);演示脚本D2.5;答辩报告D2.6§10 第 4 项;会话留痕D1.6§4.35 第五节第 2 项与 §4.39。 🔴 铁律:任何文档、任何提交、任何截图都不落 key 值(含掩码全量、含片段)。本手册只写变量名与校验方法。
0. 一句话结论
控制台建新 key(人工,不可自动化)→ 跑一个脚本改 .env → 重启 API + Worker → 复核 → 再吊销旧 key。
核心风险不是「会不会改错一个变量」,而是同一个 key 在 .env 里有多个变量名:漏改一个,就会出现「入库成功但检索失败」这类没有任何报错的症状。
1. 为什么要轮换(触发条件)
| # | 事实 |
|---|---|
| 1 | 本项目的两把模型密钥(DashScope/Qwen 一把、DeepSeek 一把)在对话过程中出现过明文(用户于 2026-09-18 直接粘贴在会话里) |
| 2 | 会话记录(D1.6 等)与全部文档从未写入 key 值(W12 入库前已做过全仓密钥扫描:真实密钥只出现在被 .gitignore 命中的 .env) |
| 3 | 但「明文已在会话里出现过」本身就是凭据泄露事件 ⇒ 按泄露处置:轮换 + 吊销旧 key |
轮换不等于改一行:改完之后系统必须仍然能工作,这要用第 4 步的复核来证明,而不是靠「密码改成功了」的直觉。
2. 轮换前的现状(2026-09-20 实测,只写变量名与长度)
2.1 .env 里的 5 个变量(group_fqcd_jr\.env,已被 .gitignore 命中,不入库)
| 变量名 | 实测长度 | 谁在读它(代码取证) |
|---|---|---|
QWEN_API_KEY |
115 | 🔴 model_endpoint_config.id=1 的 secret_ref = env:QWEN_API_KEY(检索侧向量化);tools\load_knowledge_milvus.py:50(灌库侧) |
QWEN_EMBEDDING_API_KEY |
115 | tools\configure_embedding_endpoint.py:92(仅在端点不存在时用于创建端点) |
DASHSCOPE_API_KEY |
115 | app\service\promotion_image_service.py:28(推广素材配图,缺则降级不报错) |
DEEPSEEK_API_KEY |
35 | 🔴 model_endpoint_config.id=2 的 secret_ref = env:DEEPSEEK_API_KEY(生成/判定);app\service\advisor_reason_service.py:145 |
OFFSITE_DEEPSEEK_API_KEY |
35 | app\service\offsite_document_recognition_adapter.py:610(场外文档识别) |
📌 脚本侧另有一个变量名
DEEPSEEK_API_KEY_NL2SQL(nl2sql_yc.py:156优先取它、缺省回退DEEPSEEK_API_KEY)。当前.env未定义该变量 ⇒ 走回退,无需处理。
2.2 数据库侧:secret_ref 存的是变量名,不是值
| id | endpoint_code | provider | model_name | secret_ref | status |
|---|---|---|---|---|---|
| 1 | knowledge-embedding-qwen-v3 |
qwen | text-embedding-v3 |
env:QWEN_API_KEY |
active |
| 2 | deepseek-flash |
deepseek | deepseek-chat |
env:DEEPSEEK_API_KEY |
active |
⇒ 轮换只改 .env,不需要动 DB。(EnvironmentSecretResolver 在运行时按名字去环境变量取值。)
2.3 键值分组(这就是脚本存在的理由)
| 组 | 变量 | 必须满足 |
|---|---|---|
| Qwen(DashScope) | QWEN_API_KEY / QWEN_EMBEDDING_API_KEY / DASHSCOPE_API_KEY |
三个 = 同一个值(同源同一把千问 key) |
| DeepSeek | DEEPSEEK_API_KEY / OFFSITE_DEEPSEEK_API_KEY |
两个 = 同一个值(场外识别复用同一把) |
3. 五步流程
第 1 步 · 控制台建新 key(人工,不可自动化)
| 服务 | 入口 | 建什么 |
|---|---|---|
| 阿里云 DashScope(百炼) | 控制台 → API-KEY 管理 | 新建一把 Qwen key(形如 sk-…,实测长度 115) |
| DeepSeek 开放平台 | 控制台 → API keys | 新建一把 DeepSeek key(形如 sk-…,实测长度 35) |
⚠️ 先不要吊销旧 key —— 旧的留作回退,直到第 4 步复核通过。
第 2 步 · 体检(不改任何文件,先看现状)
cd D:\桌面\金融\group_fqcd_jr
$env:PYTHONPATH='D:\桌面\金融\group_fqcd_jr'
& '.\.venv\Scripts\python.exe' tools\rotate_api_keys.py --check
期望输出(2026-09-20 实测即为此):
· Qwen(DashScope):
QWEN_API_KEY sk-ws-…(115 位)
QWEN_EMBEDDING_API_KEY sk-ws-…(115 位)
DASHSCOPE_API_KEY sk-ws-…(115 位)
✅ 同值且非空
· DeepSeek:
DEEPSEEK_API_KEY sk-e35…(35 位)
OFFSITE_DEEPSEEK_API_KEY sk-e35…(35 位)
✅ 同值且非空
出现 ❌ 有变量为空 或 ❌ 取值不一致 时先别轮换,先查清为什么(不一致本身就是缺陷)。
第 3 步 · 轮换(交互式,输入不回显)
& '.\.venv\Scripts\python.exe' tools\rotate_api_keys.py
脚本会:① 提示输入新的 Qwen key(getpass,不回显);② 提示输入新的 DeepSeek key;③ 校验格式(sk- 前缀 + 长度下限),不通过则整体不写入;④ 备份 .env.bak-<时间戳>(已被 .gitignore 命中);⑤ 把三个 Qwen 变量写成同一个值、两个 DeepSeek 变量写成同一个值;⑥ 只替换目标行,其余行(含注释、缩进、顺序)原样保留。
第 4 步 · 重启 + 复核(不做这步等于没换)
# 1) 先停 API 与 Worker(进程里还是旧 key)
Get-CimInstance Win32_Process -Filter "Name like '%python%'" | Where-Object { $_.CommandLine -match 'uvicorn app.main:app|app.worker' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
# 2) 再起(日志见 _http_api.log / _http_worker.log)
Start-Process -FilePath 'D:\桌面\金融\group_fqcd_jr\.venv\Scripts\python.exe' -ArgumentList '-m','uvicorn','app.main:app','--host','127.0.0.1','--port','8000','--log-level','warning' -WorkingDirectory 'D:\桌面\金融\group_fqcd_jr' -WindowStyle Hidden
Start-Process -FilePath 'D:\桌面\金融\group_fqcd_jr\.venv\Scripts\python.exe' -ArgumentList '-m','app.worker' -WorkingDirectory 'D:\桌面\金融\group_fqcd_jr' -WindowStyle Hidden
| # | 复核项 | 命令 / 判据 |
|---|---|---|
| 1 | 服务就绪 | GET /internal/health/ready → {"status":"ready", ...} |
| 2 | 全链路探针 | _eval_harness\http_probe.py → 11/11 succeeded(含访客线 + 客户线 + 反诈/账户/代办/画像/费率/多轮) |
| 3 | 一键自检 | demo.ps1 -SkipStart -NoBrowser → 五项自检全过(其中第 2 项「向量库三集合可查」即检索侧向量化的真实证明) |
| 4 | 行情链路 | tools\sync_market_prices.py → 跑通(另证明外部 HTTP 出口正常) |
| 5 | 检索侧单独验(可选) | 发一句知识型问句(如「七日年化是什么意思」)必须走 E3 正常作答;若变成「答不上来」或转人工,就是 key 没配对 |
第 5 步 · 吊销旧 key(只在第 4 步全过之后)
回到两个控制台,把旧 key 删除/禁用。删除后建议再跑一次第 4 步的第 2 项(证明系统确实已不再依赖旧 key)。
4. 三个必须知道的坑
| # | 坑 | 症状 | 为什么 |
|---|---|---|---|
| 1 | 只改了 QWEN_API_KEY,漏改另两个 |
「入库成功 / 检索失败」,或旧 key 吊销后才炸 | 三个变量来自同一把 key 的三个变量名,被三处不同代码读取(§2.1) |
| 2 | 只改了 DEEPSEEK_API_KEY,漏改 OFFSITE_DEEPSEEK_API_KEY |
主链路正常,场外文档识别静默降级 | 场外适配器读的是另一个变量名(§2.1) |
| 3 | 手改 .env 时带了 BOM / 改了编码 / 改了换行 |
第一个变量读不出来(后面的都正常)⇒ 症状诡异、难查 | .env 是纯文本按行解析;脚本改写时保留原编码与原换行,就是为了防这一条 |
📌 补充坑:改完不重启。
UVicorn/Worker进程内的环境变量是启动时读入的,.env改了但进程没重启 ⇒ 仍然用旧 key。
5. 回退方式
| 场景 | 怎么做 |
|---|---|
| 第 3 步写入后想撤回 | 用同目录的 .env.bak-<时间戳> 原样覆盖回去,然后重启 API + Worker |
| 新 key 在控制台建错了 / 想作废 | 在控制台禁用新 key,再用备份回退 |
| 旧 key 已吊销、新 key 又不能用 | 只能回控制台再建一把 —— 所以第 5 步必须排在第 4 步之后 |
6. 与其它文档的关系(避免两处口径漂移)
| 文档 | 关系 |
|---|---|
D2.6 §10 第 4 项「两把 API key 轮换」 |
本手册是它的执行细则;该项状态已从「手工 4 步」改为「跑一个脚本(D3.8)」 |
D2.5 §1 五项自检 |
第 4 步复核复用其中的「向量库三集合可查」 |
D1.6 §4.39 |
本轮(W13)新增工具与本手册的会话留痕 |
group_fqcd_jr\docs\26-JWT密钥管理与轮换.md |
讲的是 JWT 签名密钥,与本手册的模型 API key 是两回事(前者是自签自验,后者是外部服务凭据) |
7. 如实声明(本手册的能力边界)
| 项 | 说明 |
|---|---|
| 不能自动化第 1 步 / 第 5 步 | 控制台建 key 与吊销 key 必须由你本人在浏览器操作(无 API 授权、也不应为此开授权) |
| 本手册不记录任何 key 值 | 连掩码全量都不落 —— 掩码+长度对穷举无用,但片段会缩小搜索空间,故一并省略 |
--check 只能证明「一致且非空」 |
它不联网验证 key 是否有效;key 是否可用由第 4 步的真实链路证明 |
| 未覆盖 | 阿里云/DeepSeek 控制台的子账号与权限模型(本手册只处理主账号 key 轮换);密钥托管系统(KMS/Vault)接入 —— 当前项目用 .env,未上密钥托管 |