Files
group_fqcd_jr/客服agent/_build/_docbuilder.py
T
张胜宇 bc61d5c579 docs: 入库权威文档目录(客服agent/ 24 份 + 开发文档/ 50 份,替换旧命名的过期副本)
## 为什么做这一步

权威文档 74 份此前**只在本机**,评审者 clone 分支后看不到任何设计文档;而仓库里那两份同名目录
是 **2026-09-16 之前的过期副本,连文件名都是旧的**(无体系编号)。本次按「**权威覆盖过期**」入库。

## 入库内容

| 目录 | 文件数 | 体积 | 说明 |
|---|---|---|---|
| `客服agent/` | 24 | 0.77 MB | `D2.1`~`D2.6` 对外交付四件套 + 演示脚本/答辩报告 + `_build` 构建工具 |
| `开发文档/` | 50 | 2.16 MB | `D1.x` 索引与决策、`D3.x` 方案、`D4.x` 清除与重构留痕、`D5.x` 业务流程、`D6.x` 业务事实基座、`D7.x` 交付物、`D8.x` 规范 |

**旧的过期副本整体移除**(`客服Agent执行Todolist.md` → `D2.1-客服Agent执行Todolist.md` 之类
的改名 + 新增 `D2.5`/`D2.6`),入库后目录内容与权威副本**逐文件一致(零差异,已复核)**。

## 入库前的安全扫描(必须留痕)

- 扫描规则:`sk-` 类密钥 / `Bearer` 长串 / `password=`、`api_key=` 赋值 / 会话中出现过的两把明文 key 片段。
- 结论:**真实密钥只出现在 `.env`**(已被 `.gitignore` 命中,未入库);`.env.example` 与
  `config/risk.env.example` 只有**空占位**。
- 文档内唯一命中是 `D3.1` 里一处**截断的示例 JWT**(`Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`),
  末尾带省略号,是接口文档的示意值,**不是可用凭据**。
2026-09-20 15:03:15 +08:00

115 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""客服 Agent 四份交付文档 · 构建器(JSON 配置驱动)
从既有文档抽取「项目 HTML 版式外壳」,再灌入新正文,保证版式与既有体系逐字一致。
用法:python _docbuilder.py <spec.json>
"""
import json
import re
import sys
from pathlib import Path
SRC = Path(r"D:\桌面\金融\开发文档\D3.1-客服Agent需求开发文档与设计方案.html")
OUT_DIR = Path(r"D:\桌面\金融\客服agent")
text = SRC.read_text(encoding="utf-8")
HEAD_END = '<div id="toc-container">'
SHELL_HEAD = text[: text.index(HEAD_END) + len(HEAD_END)]
# ⚠️ 结束锚点必须取「正文第一段注释」的位置:若取 `</div><!-- end doc-container -->`,
# 那在文件里是**最后一行**,会把原文档整段正文一并带进来(曾因此产出 4920 行的错件)。
BODY_START = '<!-- ==================== 0 ==================== -->'
SHELL_MID = text[text.index(' </div>\n</nav>'): text.index(BODY_START)]
SHELL_TAIL = text[text.index('</div><!-- end doc-container -->'):]
assert 'doc-header' in SHELL_MID and len(SHELL_MID) < 4000, "SHELL_MID 抽取异常"
assert SHELL_TAIL.startswith('</div><!-- end doc-container -->')
# 备份一份外壳,便于后续单独构建
Path(r"D:\桌面\金融\_shell_head.html").write_text(SHELL_HEAD, encoding="utf-8")
Path(r"D:\桌面\金融\_shell_mid.html").write_text(SHELL_MID, encoding="utf-8")
Path(r"D:\桌面\金融\_shell_tail.html").write_text(SHELL_TAIL, encoding="utf-8")
def build(spec: dict) -> str:
head = SHELL_HEAD
head = head.replace(
"<title>XX科技·智能财富管家系统 — 智能客服 Agent 需求开发文档与设计方案</title>",
f"<title>{spec['title']}</title>")
head = head.replace(
'<div class="sidebar-subtitle">客服 Agent 专项方案 v2.3 · 双角色 · 身份分离</div>',
f'<div class="sidebar-subtitle">{spec["subtitle"]}</div>')
parts = []
for idx, grp in enumerate(spec["toc"], start=1):
active = " active" if idx == 1 else ""
arr = " open" if idx == 1 else ""
kids = " open" if idx == 1 else ""
parts.append(f'<div class="toc-group">\n'
f'<button class="toc-toggle{active}" data-group="{idx}">'
f'<span class="arr{arr}">▶</span><span>{grp["g"]}</span></button>\n'
f'<div class="toc-kids{kids}">')
for href, label in grp["items"]:
parts.append(f'<a class="toc-item" href="#{href}">{label}</a>')
parts.append("</div></div>\n")
toc_html = "\n".join(parts)
mid = SHELL_MID
mid = mid.replace(
'<div id="topbar-title">智能财富管家系统 — 智能客服 Agent 需求开发文档与设计方案</div>',
f'<div id="topbar-title">{spec["topbar"]}</div>')
mid = mid.replace('<span id="topbar-badge">v2.3 · 访客 ≠ 角色 · 重构基线</span>',
f'<span id="topbar-badge">{spec["badge"]}</span>')
mid = mid.replace('<div class="doc-eyebrow">专项需求开发文档 · 详细设计方案</div>',
f'<div class="doc-eyebrow">{spec["eyebrow"]}</div>')
mid = mid.replace(
'<h1 class="doc-title">XX科技·智能财富管家系统<br>智能客服 Agent 需求开发文档与设计方案</h1>',
f'<h1 class="doc-title">{spec["doctitle"]}</h1>')
meta_html = "".join(
f' <div class="doc-meta-item">\n'
f' <span class="doc-meta-label">{k}</span>\n'
f' <span class="doc-meta-value">{v}</span>\n'
f' </div>\n' for k, v in spec["meta"])
mid = re.sub(r' <div class="doc-meta">.*?\n </div>\n',
f' <div class="doc-meta">\n{meta_html} </div>\n', mid, flags=re.S)
tail = SHELL_TAIL
tail = tail.replace(
"(<strong>v2.2 双角色 · 重构基线版</strong>)",
f"(<strong>{spec['closing']}</strong>)")
# ⚠️ 版式外壳是从旧版式文档抽取的,会把旧品牌字面一并带进来(曾因此漏改 4 份文档的
# <title>/<h1>/sidebar 品牌)。这里对「外壳」做一次品牌兜底归一,保证任何一次重新生成
# 都不会回退到旧口径。只作用于 head/mid/tail;body 内可能有意保留历史字面(如 P-10 行),不动。
BRAND = (
("XX科技·智能财富管家系统", "南方基金·智能服务系统"),
("XX科技 · 智能金融事业部", "南方基金 · 智能服务事业部"),
("XX科技", "南方基金"),
("智能财富管家系统", "智能服务系统"),
("南方财富管理有限公司", "南方基金管理股份有限公司"),
("南方财富", "南方基金"),
("400-XXX-XXXX", "400-889-8899"),
("400-826-9518", "400-889-8899"),
("nanfangwm.com", "nffund.com"),
)
head, mid, tail = (
__import__("functools").reduce(
lambda s, kv: s.replace(kv[0], kv[1]), BRAND, part)
for part in (head, mid, tail)
)
body = Path(spec["body_file"]).read_text(encoding="utf-8")
return head + "\n" + toc_html + mid + body + tail
def main() -> None:
spec = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
OUT_DIR.mkdir(parents=True, exist_ok=True)
out = OUT_DIR / spec["out_file"]
html = build(spec)
out.write_text(html, encoding="utf-8")
print(f"OK {out} ({html.count(chr(10)) + 1} lines)")
if __name__ == "__main__":
main()