build_ai_phase1_summary_doc.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from docx import Document
  4. from docx.enum.section import WD_SECTION_START
  5. from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
  6. from docx.enum.text import WD_ALIGN_PARAGRAPH
  7. from docx.oxml import OxmlElement
  8. from docx.oxml.ns import qn
  9. from docx.shared import Inches, Pt, RGBColor
  10. ROOT = Path(__file__).resolve().parents[1]
  11. OUT = ROOT / "docs" / "市民请集合AI第一期方案汇总.docx"
  12. BLUE = "1E5B8C"
  13. DARK_BLUE = "123B5D"
  14. INK = "0B2545"
  15. MUTED = "667085"
  16. LIGHT_FILL = "EAF1F7"
  17. CALLOUT_FILL = "EEF5FA"
  18. CAUTION_FILL = "FFF8E8"
  19. BORDER = "D0D5DD"
  20. WHITE = "FFFFFF"
  21. def qn_attr(name: str) -> str:
  22. return qn(name)
  23. def set_cell_shading(cell, fill: str) -> None:
  24. tc_pr = cell._tc.get_or_add_tcPr()
  25. shd = tc_pr.find(qn_attr("w:shd"))
  26. if shd is None:
  27. shd = OxmlElement("w:shd")
  28. tc_pr.append(shd)
  29. shd.set(qn_attr("w:fill"), fill)
  30. def set_cell_margins(cell, top=100, bottom=100, start=140, end=140) -> None:
  31. tc_pr = cell._tc.get_or_add_tcPr()
  32. tc_mar = tc_pr.first_child_found_in("w:tcMar")
  33. if tc_mar is None:
  34. tc_mar = OxmlElement("w:tcMar")
  35. tc_pr.append(tc_mar)
  36. for edge, value in (("top", top), ("bottom", bottom), ("start", start), ("end", end)):
  37. node = tc_mar.find(qn_attr(f"w:{edge}"))
  38. if node is None:
  39. node = OxmlElement(f"w:{edge}")
  40. tc_mar.append(node)
  41. node.set(qn_attr("w:w"), str(value))
  42. node.set(qn_attr("w:type"), "dxa")
  43. def set_table_borders(table, color=BORDER) -> None:
  44. tbl_pr = table._tbl.tblPr
  45. borders = tbl_pr.first_child_found_in("w:tblBorders")
  46. if borders is None:
  47. borders = OxmlElement("w:tblBorders")
  48. tbl_pr.append(borders)
  49. for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
  50. node = borders.find(qn_attr(f"w:{edge}"))
  51. if node is None:
  52. node = OxmlElement(f"w:{edge}")
  53. borders.append(node)
  54. node.set(qn_attr("w:val"), "single")
  55. node.set(qn_attr("w:sz"), "6")
  56. node.set(qn_attr("w:space"), "0")
  57. node.set(qn_attr("w:color"), color)
  58. def set_table_width(table, widths_dxa: list[int]) -> None:
  59. tbl_pr = table._tbl.tblPr
  60. tbl_w = tbl_pr.find(qn_attr("w:tblW"))
  61. if tbl_w is None:
  62. tbl_w = OxmlElement("w:tblW")
  63. tbl_pr.append(tbl_w)
  64. tbl_w.set(qn_attr("w:w"), str(sum(widths_dxa)))
  65. tbl_w.set(qn_attr("w:type"), "dxa")
  66. tbl_ind = tbl_pr.find(qn_attr("w:tblInd"))
  67. if tbl_ind is None:
  68. tbl_ind = OxmlElement("w:tblInd")
  69. tbl_pr.append(tbl_ind)
  70. tbl_ind.set(qn_attr("w:w"), "120")
  71. tbl_ind.set(qn_attr("w:type"), "dxa")
  72. grid = table._tbl.tblGrid
  73. if grid is None:
  74. grid = OxmlElement("w:tblGrid")
  75. table._tbl.insert(0, grid)
  76. for child in list(grid):
  77. grid.remove(child)
  78. for width in widths_dxa:
  79. col = OxmlElement("w:gridCol")
  80. col.set(qn_attr("w:w"), str(width))
  81. grid.append(col)
  82. for row in table.rows:
  83. for cell, width in zip(row.cells, widths_dxa):
  84. tc_pr = cell._tc.get_or_add_tcPr()
  85. tc_w = tc_pr.find(qn_attr("w:tcW"))
  86. if tc_w is None:
  87. tc_w = OxmlElement("w:tcW")
  88. tc_pr.append(tc_w)
  89. tc_w.set(qn_attr("w:w"), str(width))
  90. tc_w.set(qn_attr("w:type"), "dxa")
  91. cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
  92. set_cell_margins(cell)
  93. def set_run_font(run, font_name="Calibri", east_asia="Microsoft YaHei") -> None:
  94. run.font.name = font_name
  95. run._element.rPr.rFonts.set(qn_attr("w:ascii"), font_name)
  96. run._element.rPr.rFonts.set(qn_attr("w:hAnsi"), font_name)
  97. run._element.rPr.rFonts.set(qn_attr("w:eastAsia"), east_asia)
  98. def set_paragraph_border_bottom(paragraph, color=BORDER, size="8") -> None:
  99. p_pr = paragraph._p.get_or_add_pPr()
  100. p_bdr = p_pr.find(qn_attr("w:pBdr"))
  101. if p_bdr is None:
  102. p_bdr = OxmlElement("w:pBdr")
  103. p_pr.append(p_bdr)
  104. bottom = p_bdr.find(qn_attr("w:bottom"))
  105. if bottom is None:
  106. bottom = OxmlElement("w:bottom")
  107. p_bdr.append(bottom)
  108. bottom.set(qn_attr("w:val"), "single")
  109. bottom.set(qn_attr("w:sz"), size)
  110. bottom.set(qn_attr("w:space"), "6")
  111. bottom.set(qn_attr("w:color"), color)
  112. def add_para(
  113. doc,
  114. text: str,
  115. style: str | None = None,
  116. bold_prefix: str | None = None,
  117. color: str | None = None,
  118. italic: bool = False,
  119. ):
  120. p = doc.add_paragraph(style=style)
  121. if bold_prefix and text.startswith(bold_prefix):
  122. r = p.add_run(bold_prefix)
  123. r.bold = True
  124. if color:
  125. r.font.color.rgb = RGBColor.from_string(color)
  126. set_run_font(r)
  127. tail = p.add_run(text[len(bold_prefix):])
  128. tail.italic = italic
  129. if color:
  130. tail.font.color.rgb = RGBColor.from_string(color)
  131. set_run_font(tail)
  132. else:
  133. r = p.add_run(text)
  134. r.italic = italic
  135. if color:
  136. r.font.color.rgb = RGBColor.from_string(color)
  137. set_run_font(r)
  138. return p
  139. def add_bullet(doc, text: str):
  140. p = doc.add_paragraph(style="List Bullet")
  141. r = p.add_run(text)
  142. set_run_font(r)
  143. return p
  144. def add_number(doc, text: str):
  145. p = doc.add_paragraph(style="List Number")
  146. r = p.add_run(text)
  147. set_run_font(r)
  148. return p
  149. def add_heading(doc, text: str, level: int):
  150. p = doc.add_heading(text, level=level)
  151. for run in p.runs:
  152. set_run_font(run)
  153. return p
  154. def add_callout(doc, title: str, body: str, fill: str = CALLOUT_FILL) -> None:
  155. table = doc.add_table(rows=1, cols=1)
  156. table.alignment = WD_TABLE_ALIGNMENT.LEFT
  157. table.style = "Table Grid"
  158. set_table_width(table, [9360])
  159. set_table_borders(table)
  160. cell = table.cell(0, 0)
  161. set_cell_shading(cell, fill)
  162. p = cell.paragraphs[0]
  163. p.paragraph_format.space_after = Pt(4)
  164. r = p.add_run(title)
  165. r.bold = True
  166. r.font.color.rgb = RGBColor.from_string(DARK_BLUE)
  167. set_run_font(r)
  168. p2 = cell.add_paragraph()
  169. p2.paragraph_format.space_after = Pt(0)
  170. r2 = p2.add_run(body)
  171. set_run_font(r2)
  172. doc.add_paragraph()
  173. def add_table(doc, headers: list[str], rows: list[list[str]], widths_dxa: list[int]):
  174. table = doc.add_table(rows=1, cols=len(headers))
  175. table.alignment = WD_TABLE_ALIGNMENT.LEFT
  176. table.style = "Table Grid"
  177. set_table_width(table, widths_dxa)
  178. set_table_borders(table)
  179. for i, text in enumerate(headers):
  180. cell = table.rows[0].cells[i]
  181. set_cell_shading(cell, DARK_BLUE)
  182. p = cell.paragraphs[0]
  183. p.alignment = WD_ALIGN_PARAGRAPH.CENTER
  184. p.paragraph_format.space_after = Pt(0)
  185. r = p.add_run(text)
  186. r.bold = True
  187. r.font.color.rgb = RGBColor.from_string(WHITE)
  188. set_run_font(r)
  189. for row in rows:
  190. cells = table.add_row().cells
  191. for i, text in enumerate(row):
  192. p = cells[i].paragraphs[0]
  193. p.alignment = WD_ALIGN_PARAGRAPH.LEFT
  194. p.paragraph_format.space_after = Pt(0)
  195. r = p.add_run(text)
  196. set_run_font(r)
  197. set_table_width(table, widths_dxa)
  198. doc.add_paragraph()
  199. return table
  200. def add_metadata_rows(doc, rows: list[tuple[str, str]]) -> None:
  201. for label, value in rows:
  202. p = doc.add_paragraph()
  203. p.paragraph_format.space_before = Pt(0)
  204. p.paragraph_format.space_after = Pt(2)
  205. p.paragraph_format.line_spacing = 1.10
  206. r1 = p.add_run(label)
  207. r1.bold = True
  208. set_run_font(r1)
  209. r2 = p.add_run(value)
  210. set_run_font(r2)
  211. def configure_styles(doc: Document) -> None:
  212. section = doc.sections[0]
  213. section.page_width = Inches(8.5)
  214. section.page_height = Inches(11)
  215. for margin in ("top_margin", "bottom_margin", "left_margin", "right_margin"):
  216. setattr(section, margin, Inches(1))
  217. section.header_distance = Inches(0.492)
  218. section.footer_distance = Inches(0.492)
  219. styles = doc.styles
  220. normal = styles["Normal"]
  221. normal.font.name = "Calibri"
  222. normal._element.rPr.rFonts.set(qn_attr("w:eastAsia"), "Microsoft YaHei")
  223. normal.font.size = Pt(11)
  224. normal.paragraph_format.space_before = Pt(0)
  225. normal.paragraph_format.space_after = Pt(6)
  226. normal.paragraph_format.line_spacing = 1.10
  227. title = styles["Title"]
  228. title.font.name = "Calibri"
  229. title._element.rPr.rFonts.set(qn_attr("w:eastAsia"), "Microsoft YaHei")
  230. title.font.size = Pt(24)
  231. title.font.bold = True
  232. title.font.color.rgb = RGBColor.from_string(INK)
  233. title.paragraph_format.space_before = Pt(0)
  234. title.paragraph_format.space_after = Pt(8)
  235. subtitle = styles["Subtitle"]
  236. subtitle.font.name = "Calibri"
  237. subtitle._element.rPr.rFonts.set(qn_attr("w:eastAsia"), "Microsoft YaHei")
  238. subtitle.font.size = Pt(11)
  239. subtitle.font.color.rgb = RGBColor.from_string(MUTED)
  240. subtitle.paragraph_format.space_after = Pt(14)
  241. for name, size, color, before, after in [
  242. ("Heading 1", 16, BLUE, 16, 8),
  243. ("Heading 2", 13, BLUE, 12, 6),
  244. ("Heading 3", 12, DARK_BLUE, 8, 4),
  245. ]:
  246. style = styles[name]
  247. style.font.name = "Calibri"
  248. style._element.rPr.rFonts.set(qn_attr("w:eastAsia"), "Microsoft YaHei")
  249. style.font.size = Pt(size)
  250. style.font.bold = True
  251. style.font.color.rgb = RGBColor.from_string(color)
  252. style.paragraph_format.space_before = Pt(before)
  253. style.paragraph_format.space_after = Pt(after)
  254. for name in ("List Bullet", "List Number"):
  255. style = styles[name]
  256. style.font.name = "Calibri"
  257. style._element.rPr.rFonts.set(qn_attr("w:eastAsia"), "Microsoft YaHei")
  258. style.font.size = Pt(11)
  259. style.paragraph_format.space_after = Pt(8)
  260. style.paragraph_format.line_spacing = 1.167
  261. footer = section.footer.paragraphs[0]
  262. footer.alignment = WD_ALIGN_PARAGRAPH.RIGHT
  263. r = footer.add_run("市民请集合 AI 第一期方案汇总")
  264. r.font.size = Pt(9)
  265. r.font.color.rgb = RGBColor.from_string(MUTED)
  266. set_run_font(r)
  267. def build_doc() -> None:
  268. doc = Document()
  269. configure_styles(doc)
  270. doc.add_paragraph()
  271. p = doc.add_paragraph(style="Title")
  272. r = p.add_run("市民请集合 AI 第一期方案汇总")
  273. set_run_font(r)
  274. p = doc.add_paragraph(style="Subtitle")
  275. r = p.add_run("基于当前 smqjh-agent 系统、第一期业务需求及 4 个 ChatGPT Pro 账号预算整理")
  276. set_run_font(r)
  277. add_metadata_rows(
  278. doc,
  279. [
  280. ("版本日期:", "2026-06-30"),
  281. ("一期预算口径:", "4 个 ChatGPT Pro 账号,USD 200/账号/月,合计 USD 800/月;采购与发票口径同步确认"),
  282. ("资料依据:", "《市民请集合AI第一期需求.docx》、当前 smqjh-agent 代码与设计复盘文档、OpenAI 官方订阅说明(2026-06-30 查询)"),
  283. ],
  284. )
  285. rule = doc.add_paragraph()
  286. set_paragraph_border_bottom(rule)
  287. add_callout(
  288. doc,
  289. "结论先行",
  290. "第一期建议定位为“业务运营 AI 助手试点”:先用当前已具备的 Agent/MCP/只读数据库能力,把查询、报价、对账、充值文件整理等高频人工工作做成可追踪、可导出的辅助流程;ChatGPT 账号用于人员日常方案、表格、文档和复杂问题处理,系统侧继续使用受控 MCP 工具承接内部数据能力。整体不建议一开始承诺全自动写入业务系统,所有涉及金额、上下架、充值、开票和结算确认的动作都保留人工复核。"
  291. )
  292. add_heading(doc, "1. 解决什么问题", 1)
  293. add_para(
  294. doc,
  295. "第一期主要解决运营、商务和财务在报价、商品、充值、对账、查询中的重复性整理和跨系统核对问题。当前痛点不是单点功能缺失,而是信息分散在报价单、供应商资料、后台系统、订单/积分/商品表和人工沟通中,导致每次都要重新查、重新改表、重新核口径。",
  296. )
  297. add_table(
  298. doc,
  299. ["问题类型", "当前表现", "AI 一期要解决的点"],
  300. [
  301. ["报价单", "新企业签约时需要基于星闪豹商品、京东旗舰店原价和企业反馈反复调整报价。", "生成固定格式报价单,辅助补齐商品、规格、价格依据,支持对方新增商品后的快速报价和汇总下载。"],
  302. ["商品上下架", "最终报价单确定后,需要在海博和市民请集合系统做批量或零散上下架。", "解析报价单,生成待上架/下架清单、字段校验和执行建议;高风险写入动作人工确认后再执行。"],
  303. ["商品名称对应", "平台商品名、供应商开票名称和采购单名称不一致,建采购单耗时。", "建立名称/规格/编码匹配表,上传供应商开票图片或清单后给出候选匹配,人工确认后沉淀映射。"],
  304. ["积分充值", "企业给的积分导入名单格式不统一,导入后还要输出成功证明,错误反馈需要排查。", "自动清洗为系统导入格式,按规则检查风险,生成导入文件和成功反馈文件。"],
  305. ["对账结算", "积分过期后需要多方核对、生成对账单、开票明细、盖章和付款跟踪。", "打包对账单和核对结果,计算开票金额/税额关系,标记异常差异,形成可给企业确认的文件包。"],
  306. ["数据查询", "运营需要按企业、时间、状态查询积分充值/消耗/可用、正常订单和售后订单。", "管理员用自然语言查询,系统自动查只读数据库并输出表格、摘要和依据。"],
  307. ],
  308. [1500, 3900, 3960],
  309. )
  310. add_para(
  311. doc,
  312. "从系统建设角度看,第一期还要解决两个底层问题:一是把业务查询从“人找字段、人写 SQL、人导表”改成“Agent 调受控工具”;二是把 AI 的输出从长文本变成表格、文件、待确认项和可审计日志。",
  313. )
  314. add_heading(doc, "2. 能够解决到什么程度", 1)
  315. add_para(
  316. doc,
  317. "当前 smqjh-agent 已有可用底座:Python + LangGraph Web Agent、Java MCP Server、DeepSeek 工具规划、业务 schema、只读数据库查询、商品/订单/月结工具、表格渲染和 CSV 下载。基于这个基础,一期能做到“辅助闭环 + 人工确认”,不宜直接承诺无人值守的全自动业务变更。",
  318. )
  319. add_table(
  320. doc,
  321. ["场景", "一期可交付程度", "预计自动化/减负", "需要人工确认的边界"],
  322. [
  323. ["数据查询", "自然语言查询商品、订单、会员、积分、月结相关数据,并输出表格与依据。", "高,常规查询约 80%-90% 可自动完成。", "字段口径不明确、数据异常、权限不足时确认。"],
  324. ["报价单", "生成统一报价单模板,辅助商品匹配、价格引用、对方新增品报价和汇总下载。", "中高,格式整理和候选匹配约 60%-70% 可自动化。", "最终报价、利润率、外部平台最新价必须人工确认。"],
  325. ["对账结算", "按企业和月份生成导出计划、核对规则、差异清单,进一步可生成对账文件包。", "中高,模板确认后约 70%-80% 工作量可减少。", "差异、税率、开票金额、盖章和付款状态确认。"],
  326. ["积分充值", "清洗企业名单,校验字段、金额、手机号和重复项,生成导入格式和反馈文件。", "中,约 50%-70% 的整理排错可自动化。", "真实导入、批量发放、金额异常必须确认。"],
  327. ["商品上下架", "从报价单生成上下架清单和字段校验,后续对接接口后进入确认执行。", "中,前处理约 50%-60% 可自动化。", "自动写入海博/市民请集合系统前必须审批。"],
  328. ["名称/发票匹配", "建立供应商名称、平台商品、SKU、编码映射候选,支持图片/OCR 后续接入。", "中,样本积累前约 50%-60%;字典完善后可提升。", "同规格多候选、开票名称不规范、编码不唯一。"],
  329. ],
  330. [1300, 3450, 1850, 2760],
  331. )
  332. add_callout(
  333. doc,
  334. "一期能力边界",
  335. "AI 可以先把“查、算、整、比、导、提示风险”做起来;凡是会改变业务系统状态的动作,例如上架、下架、充值导入、发券、结算确认、开票提交,都必须保留人工确认和日志记录。",
  336. fill=CAUTION_FILL,
  337. )
  338. add_heading(doc, "3. 大致时间周期", 1)
  339. add_para(
  340. doc,
  341. "建议一期按 4-6 周推进:4 周形成可演示和小范围试用版本,6 周完成重点场景试运行。账号采购申请和发票确认不等待研发完成,采用同步推进。",
  342. )
  343. add_table(
  344. doc,
  345. ["阶段", "周期", "主要工作", "交付结果"],
  346. [
  347. ["启动与确认", "第 1 周", "确认报价单模板、对账模板、充值导入模板、上下架字段、企业样本和账号采购申请。", "一期范围确认表、样表清单、账号申请单。"],
  348. ["查询与报价 MVP", "第 2 周", "强化智能查询、商品/订单/积分表口径,完成报价单模板生成和新增品候选匹配。", "自然语言查询演示、报价单生成样例。"],
  349. ["对账与充值流程", "第 3-4 周", "打通月结企业、订单/积分/运费导出规则、充值文件清洗和风险提示。", "对账文件包样例、充值导入前检查表。"],
  350. ["上下架与名称匹配试点", "第 5 周", "根据报价单生成上下架清单,沉淀商品名、开票名、SKU 编码映射。", "上下架待确认清单、名称匹配表。"],
  351. ["试运行与培训", "第 6 周", "用真实业务样本试跑,修正提示词、口径、模板和人工确认流程。", "试运行报告、使用说明、下一期清单。"],
  352. ],
  353. [1450, 1200, 4300, 2410],
  354. )
  355. add_para(
  356. doc,
  357. "如果只做“账号采购 + 文档/表格辅助”可以更快,约 1 周即可启用;如果要把报价、对账、充值、上下架真正沉淀到当前系统里,建议按 4-6 周评估。",
  358. bold_prefix="补充说明:",
  359. )
  360. add_heading(doc, "4. 费用的具体数量", 1)
  361. add_para(
  362. doc,
  363. "本次费用按 ChatGPT Pro 账号采购口径测算:4 个账号,每个账号 USD 200/月,合计 USD 800/月。该口径对应一个月试点费用,不与其他订阅档位混合测算。",
  364. )
  365. add_table(
  366. doc,
  367. ["费用项", "数量/单价", "金额", "说明"],
  368. [
  369. ["ChatGPT Pro 账号", "4 个账号 x USD 200/月", "USD 800/月", "用于业务人员日常方案、表格、文档、复杂问题分析和高强度 AI 使用。"],
  370. ["采购申请金额", "按 1 个月试点", "USD 800", "先按一个月费用提交申请,后续是否续费根据试运行效果决定。"],
  371. ["OpenAI API 或其他第三方接口", "未纳入本次 USD 800/月", "另行测算", "ChatGPT Pro 订阅不等同于 API 用量;当前系统侧主要通过 MCP/DeepSeek 跑业务工具。"],
  372. ["内部系统开发/实施", "使用现有 smqjh-agent 底座推进", "如内部研发,不另列外采费用", "若后续外包 OCR、价格接口或专门爬取服务,需要单独报价。"],
  373. ],
  374. [2200, 2450, 1850, 2860],
  375. )
  376. add_callout(
  377. doc,
  378. "采购与发票说明",
  379. "ChatGPT 自助订阅通常可在账户账单里下载 invoice/receipt,但这不等同于国内增值税专票或普票。当前建议先按 USD 800/月提交采购申请,同步让财务确认境外订阅 invoice、付款截图、信用卡流水等材料是否可作为报销/入账依据;如果必须取得国内发票,需要另查代理采购或企业合同路径,预算和周期都会变化。",
  380. fill=CAUTION_FILL,
  381. )
  382. add_heading(doc, "5. 推荐推进方式", 1)
  383. add_number(doc, "先提交 USD 800/月 / 4 个 ChatGPT Pro 账号的试点申请。")
  384. add_number(doc, "账号采购和系统建设同步走,避免等采购流程导致研发试点停滞。")
  385. add_number(doc, "第一条端到端样例建议选择“报价单 + 对账查询”组合:展示价值直接,也最贴合当前需求文档。")
  386. add_number(doc, "所有涉及金额、上下架、充值、开票的结果都输出“待确认清单”,由业务人员确认后再执行。")
  387. add_number(doc, "试运行结束后,用真实样本统计节省时间、错误率、人工确认比例,再决定第二期是否做更深的自动写入和审批流。")
  388. add_heading(doc, "6. 可直接用于内部汇报的版本", 1)
  389. add_callout(
  390. doc,
  391. "汇报口径",
  392. "第一期主要解决报价、商品、充值、对账和查询这几类重复性工作。当前系统已经有 Agent、MCP、只读数据库和表格输出底座,所以不需要从零开始。账号侧先采购 4 个 ChatGPT Pro 账号做业务和文档辅助,预算按 USD 800/月申请;系统侧同步把查询、报价单、对账和充值整理做成可复用流程。预计 4 周能出可演示版本,6 周进入小范围试运行。ChatGPT 自助订阅暂时无法提供国内增值税发票,我们先提交申请,同时让财务确认境外 invoice/付款凭证是否可接受;如果必须走国内发票,再调整采购路径和预算。"
  393. )
  394. add_heading(doc, "参考信息", 1)
  395. add_bullet(doc, "OpenAI Help Center:ChatGPT Pro 档位包含 USD 100/月和 USD 200/月两类,Pro USD 200 对应最高用量档。https://help.openai.com/en/articles/9793128-about-chatgpt-pro-tiers")
  396. add_bullet(doc, "OpenAI Help Center:自助 ChatGPT 客户可在账单入口查看或下载过往 invoices;如果组织要求 invoice billing,需要联系 Sales 讨论 Enterprise/Education 等合同方案。https://help.openai.com/en/articles/12356340-how-can-i-find-my-past-chatgpt-invoices")
  397. add_bullet(doc, "当前项目资料:smqjh-agent-runtime、smqjh-mcp-server、smqjh-admin-agent 以及《市民请集合AI第一期需求.docx》。")
  398. OUT.parent.mkdir(parents=True, exist_ok=True)
  399. doc.save(OUT)
  400. print(OUT)
  401. if __name__ == "__main__":
  402. build_doc()