马一丁

Add Comments

@@ -296,8 +296,19 @@ class ReportAgent: @@ -296,8 +296,19 @@ class ReportAgent:
296 4. 将章节装订成Document IR,再交给HTML渲染器生成成品; 296 4. 将章节装订成Document IR,再交给HTML渲染器生成成品;
297 5. 可选地将HTML/IR/状态落盘,并向外界回传路径信息。 297 5. 可选地将HTML/IR/状态落盘,并向外界回传路径信息。
298 298
299 - Returns:  
300 - dict: HTML内容以及保存的文件路径信息 299 + 参数:
  300 + query: 最终要生成的报告主题或提问语句。
  301 + reports: 来自 Query/Media/Insight 等分析引擎的原始输出,允许传入字符串或更复杂的对象。
  302 + forum_logs: 论坛/协同记录,供LLM理解多人讨论上下文。
  303 + custom_template: 用户指定的Markdown模板,如为空则交由模板节点自动挑选。
  304 + save_report: 是否在生成后自动将HTML、IR与状态写入磁盘。
  305 + stream_handler: 可选的流式事件回调,接收阶段标签与payload,用于UI实时展示。
  306 +
  307 + 返回:
  308 + dict: 包含 `html_content` 以及HTML/IR/状态文件路径的字典;若 `save_report=False` 则仅返回HTML字符串。
  309 +
  310 + 异常:
  311 + Exception: 任一子节点或渲染阶段失败时抛出,外层调用方负责兜底。
301 """ 312 """
302 start_time = datetime.now() 313 start_time = datetime.now()
303 report_id = f"report-{uuid4().hex[:8]}" 314 report_id = f"report-{uuid4().hex[:8]}"
@@ -538,6 +549,15 @@ class ReportAgent: @@ -538,6 +549,15 @@ class ReportAgent:
538 优先使用用户指定的模板;否则将查询、三引擎报告与论坛日志 549 优先使用用户指定的模板;否则将查询、三引擎报告与论坛日志
539 作为上下文交给 TemplateSelectionNode,由 LLM 返回最契合的 550 作为上下文交给 TemplateSelectionNode,由 LLM 返回最契合的
540 模板名称、内容及理由,并自动记录在状态中。 551 模板名称、内容及理由,并自动记录在状态中。
  552 +
  553 + 参数:
  554 + query: 报告主题,用于提示词聚焦行业/事件。
  555 + reports: 多来源报告原文,帮助LLM判断结构复杂度。
  556 + forum_logs: 对应论坛或协作讨论的文本,用于补充背景。
  557 + custom_template: CLI/前端传入的自定义Markdown模板,非空时直接采用。
  558 +
  559 + 返回:
  560 + dict: 包含 `template_name`、`template_content` 与 `selection_reason` 的结构化结果,供后续节点消费。
541 """ 561 """
542 logger.info("选择报告模板...") 562 logger.info("选择报告模板...")
543 563
@@ -584,6 +604,12 @@ class ReportAgent: @@ -584,6 +604,12 @@ class ReportAgent:
584 委托 `parse_template_sections` 将Markdown标题/编号解析为 604 委托 `parse_template_sections` 将Markdown标题/编号解析为
585 `TemplateSection` 列表,确保后续章节生成有稳定的章节ID。 605 `TemplateSection` 列表,确保后续章节生成有稳定的章节ID。
586 当模板格式异常时,会回退到内置的简单骨架避免崩溃。 606 当模板格式异常时,会回退到内置的简单骨架避免崩溃。
  607 +
  608 + 参数:
  609 + template_markdown: 完整的模板Markdown文本。
  610 +
  611 + 返回:
  612 + list[TemplateSection]: 解析后的章节序列;如解析失败则返回单章兜底结构。
587 """ 613 """
588 sections = parse_template_sections(template_markdown) 614 sections = parse_template_sections(template_markdown)
589 if sections: 615 if sections:
@@ -618,6 +644,19 @@ class ReportAgent: @@ -618,6 +644,19 @@ class ReportAgent:
618 将模板名称、布局设计、主题配色、篇幅规划、论坛日志等 644 将模板名称、布局设计、主题配色、篇幅规划、论坛日志等
619 一次性整合为 `generation_context`,后续每章调用 LLM 时 645 一次性整合为 `generation_context`,后续每章调用 LLM 时
620 直接复用,确保所有章节共享一致的语调和视觉约束。 646 直接复用,确保所有章节共享一致的语调和视觉约束。
  647 +
  648 + 参数:
  649 + query: 用户查询词。
  650 + reports: 归一化后的 query/media/insight 报告映射。
  651 + forum_logs: 三引擎讨论记录。
  652 + template_result: 模板节点返回的模板元信息。
  653 + layout_design: 文档布局节点产出的标题/目录/主题设计。
  654 + chapter_directives: 字数规划节点返回的章节指令映射。
  655 + word_plan: 篇幅规划原始结果,包含全局字数约束。
  656 + template_overview: 模板切片提炼的章节骨架摘要。
  657 +
  658 + 返回:
  659 + dict: LLM章节生成所需的全集上下文,包含主题色、布局、约束等键。
621 """ 660 """
622 # 优先使用设计稿定制的主题色,否则退回默认主题 661 # 优先使用设计稿定制的主题色,否则退回默认主题
623 theme_tokens = ( 662 theme_tokens = (
@@ -650,6 +689,12 @@ class ReportAgent: @@ -650,6 +689,12 @@ class ReportAgent:
650 689
651 约定顺序为 Query/Media/Insight,引擎提供的对象可能是 690 约定顺序为 Query/Media/Insight,引擎提供的对象可能是
652 字典或自定义类型,因此统一走 `_stringify` 做容错。 691 字典或自定义类型,因此统一走 `_stringify` 做容错。
  692 +
  693 + 参数:
  694 + reports: 任意类型的报告列表,允许缺失或顺序混乱。
  695 +
  696 + 返回:
  697 + dict: 包含 `query_engine`/`media_engine`/`insight_engine` 三个字符串字段的映射。
653 """ 698 """
654 keys = ["query_engine", "media_engine", "insight_engine"] 699 keys = ["query_engine", "media_engine", "insight_engine"]
655 normalized: Dict[str, str] = {} 700 normalized: Dict[str, str] = {}
@@ -664,6 +709,12 @@ class ReportAgent: @@ -664,6 +709,12 @@ class ReportAgent:
664 709
665 当检测到供应商返回的错误包含特定关键词时,允许章节生成 710 当检测到供应商返回的错误包含特定关键词时,允许章节生成
666 重新尝试,以便绕过偶发的内容审查触发。 711 重新尝试,以便绕过偶发的内容审查触发。
  712 +
  713 + 参数:
  714 + error: LLM客户端抛出的异常对象。
  715 +
  716 + 返回:
  717 + bool: 若匹配到内容审查关键词则返回True,否则为False。
667 """ 718 """
668 message = str(error) if error else "" 719 message = str(error) if error else ""
669 if not message: 720 if not message:
@@ -683,6 +734,12 @@ class ReportAgent: @@ -683,6 +734,12 @@ class ReportAgent:
683 734
684 - dict/list 统一序列化为格式化 JSON,便于提示词消费; 735 - dict/list 统一序列化为格式化 JSON,便于提示词消费;
685 - 其他类型走 `str()`,None 则返回空串,避免 None 传播。 736 - 其他类型走 `str()`,None 则返回空串,避免 None 传播。
  737 +
  738 + 参数:
  739 + value: 任意Python对象。
  740 +
  741 + 返回:
  742 + str: 适配提示词/日志的字符串表现。
686 """ 743 """
687 if value is None: 744 if value is None:
688 return "" 745 return ""
@@ -700,6 +757,9 @@ class ReportAgent: @@ -700,6 +757,9 @@ class ReportAgent:
700 构造默认主题变量,供渲染器/LLM共用。 757 构造默认主题变量,供渲染器/LLM共用。
701 758
702 当布局节点未返回专属配色时使用该套色板,保持报告风格统一。 759 当布局节点未返回专属配色时使用该套色板,保持报告风格统一。
  760 +
  761 + 返回:
  762 + dict: 包含颜色、字体、间距、布尔开关等渲染参数的主题字典。
703 """ 763 """
704 return { 764 return {
705 "colors": { 765 "colors": {
@@ -735,6 +795,13 @@ class ReportAgent: @@ -735,6 +795,13 @@ class ReportAgent:
735 提取模板标题与章节骨架,供设计/篇幅规划统一引用。 795 提取模板标题与章节骨架,供设计/篇幅规划统一引用。
736 796
737 同时记录章节ID/slug/order等辅助字段,保证多节点对齐。 797 同时记录章节ID/slug/order等辅助字段,保证多节点对齐。
  798 +
  799 + 参数:
  800 + template_markdown: 模板原文,用于解析全局标题。
  801 + sections: `TemplateSection` 列表,作为章节骨架。
  802 +
  803 + 返回:
  804 + dict: 包含模板标题与章节元数据的概览结构。
738 """ 805 """
739 fallback_title = sections[0].title if sections else "" 806 fallback_title = sections[0].title if sections else ""
740 overview = { 807 overview = {
@@ -763,6 +830,13 @@ class ReportAgent: @@ -763,6 +830,13 @@ class ReportAgent:
763 830
764 优先返回首个 `#` 语法标题;如果模板首行就是正文,则回退到 831 优先返回首个 `#` 语法标题;如果模板首行就是正文,则回退到
765 第一行非空文本或调用方提供的 fallback。 832 第一行非空文本或调用方提供的 fallback。
  833 +
  834 + 参数:
  835 + template_markdown: 模板原文。
  836 + fallback: 备用标题,当文档缺少显式标题时使用。
  837 +
  838 + 返回:
  839 + str: 解析到的标题文本。
766 """ 840 """
767 for line in template_markdown.splitlines(): 841 for line in template_markdown.splitlines():
768 stripped = line.strip() 842 stripped = line.strip()
@@ -834,6 +908,14 @@ class ReportAgent: @@ -834,6 +908,14 @@ class ReportAgent:
834 908
835 生成基于查询和时间戳的易读文件名,同时也把运行态的 909 生成基于查询和时间戳的易读文件名,同时也把运行态的
836 `ReportState` 写入 JSON,方便下游排障或断点续跑。 910 `ReportState` 写入 JSON,方便下游排障或断点续跑。
  911 +
  912 + 参数:
  913 + html_content: 渲染后的HTML正文。
  914 + document_ir: Document IR结构化数据。
  915 + report_id: 当前任务ID,用于创建独立文件名。
  916 +
  917 + 返回:
  918 + dict: 记录HTML/IR/State文件的绝对与相对路径信息。
837 """ 919 """
838 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 920 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
839 query_safe = "".join( 921 query_safe = "".join(
@@ -879,6 +961,14 @@ class ReportAgent: @@ -879,6 +961,14 @@ class ReportAgent:
879 961
880 `Document IR` 与 HTML 解耦保存,便于调试渲染差异以及 962 `Document IR` 与 HTML 解耦保存,便于调试渲染差异以及
881 在不重新跑 LLM 的情况下再次渲染或导出其他格式。 963 在不重新跑 LLM 的情况下再次渲染或导出其他格式。
  964 +
  965 + 参数:
  966 + document_ir: 整本报告的IR结构。
  967 + query_safe: 已清洗的查询短语,用于文件命名。
  968 + timestamp: 运行时间戳,保证文件名唯一。
  969 +
  970 + 返回:
  971 + Path: 指向保存后的IR文件路径。
882 """ 972 """
883 filename = f"report_ir_{query_safe}_{timestamp}.json" 973 filename = f"report_ir_{query_safe}_{timestamp}.json"
884 ir_path = Path(self.config.DOCUMENT_IR_OUTPUT_DIR) / filename 974 ir_path = Path(self.config.DOCUMENT_IR_OUTPUT_DIR) / filename
@@ -901,6 +991,12 @@ class ReportAgent: @@ -901,6 +991,12 @@ class ReportAgent:
901 这些中间件文件(document_layout/word_plan/template_overview) 991 这些中间件文件(document_layout/word_plan/template_overview)
902 方便在调试或复盘时快速定位:标题/目录/主题是如何确定的、 992 方便在调试或复盘时快速定位:标题/目录/主题是如何确定的、
903 字数分配有什么要求,以便后续人工校正。 993 字数分配有什么要求,以便后续人工校正。
  994 +
  995 + 参数:
  996 + run_dir: 章节输出根目录。
  997 + layout_design: 文档布局节点的原始输出。
  998 + word_plan: 篇幅规划节点输出。
  999 + template_overview: 模板概览JSON。
904 """ 1000 """
905 artifacts = { 1001 artifacts = {
906 "document_layout": layout_design, 1002 "document_layout": layout_design,
@@ -75,6 +75,13 @@ class ChapterStorage: @@ -75,6 +75,13 @@ class ChapterStorage:
75 为本次报告创建独立的章节输出目录与manifest。 75 为本次报告创建独立的章节输出目录与manifest。
76 76
77 同时把全局metadata写入 `manifest.json`,供渲染/调试查询。 77 同时把全局metadata写入 `manifest.json`,供渲染/调试查询。
  78 +
  79 + 参数:
  80 + report_id: 任务ID。
  81 + metadata: Report元数据(标题、主题等)。
  82 +
  83 + 返回:
  84 + Path: 新建的run目录。
78 """ 85 """
79 run_dir = self.base_dir / report_id 86 run_dir = self.base_dir / report_id
80 run_dir.mkdir(parents=True, exist_ok=True) 87 run_dir.mkdir(parents=True, exist_ok=True)
@@ -93,6 +100,13 @@ class ChapterStorage: @@ -93,6 +100,13 @@ class ChapterStorage:
93 创建章节子目录并在manifest中标记为streaming状态。 100 创建章节子目录并在manifest中标记为streaming状态。
94 101
95 会生成 `order-slug` 风格的子目录,并提前登记 raw 文件路径。 102 会生成 `order-slug` 风格的子目录,并提前登记 raw 文件路径。
  103 +
  104 + 参数:
  105 + run_dir: 会话根目录。
  106 + chapter_meta: 包含 chapterId/title/slug/order 的元数据。
  107 +
  108 + 返回:
  109 + Path: 章节目录。
96 """ 110 """
97 slug_value = str( 111 slug_value = str(
98 chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section" 112 chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section"
@@ -124,6 +138,15 @@ class ChapterStorage: @@ -124,6 +138,15 @@ class ChapterStorage:
124 章节流式生成完毕后写入最终JSON并更新manifest状态。 138 章节流式生成完毕后写入最终JSON并更新manifest状态。
125 139
126 若校验失败,错误信息会被写入manifest,供前端展示。 140 若校验失败,错误信息会被写入manifest,供前端展示。
  141 +
  142 + 参数:
  143 + run_dir: 会话根目录。
  144 + chapter_meta: 章节元信息。
  145 + payload: 校验通过的章节JSON。
  146 + errors: 可选的错误列表,用于标记invalid状态。
  147 +
  148 + 返回:
  149 + Path: 最终的 `chapter.json` 文件路径。
127 """ 150 """
128 slug_value = str( 151 slug_value = str(
129 chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section" 152 chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section"
@@ -159,6 +182,12 @@ class ChapterStorage: @@ -159,6 +182,12 @@ class ChapterStorage:
159 从指定run目录读取全部chapter.json并按order排序返回。 182 从指定run目录读取全部chapter.json并按order排序返回。
160 183
161 常用于 DocumentComposer 将多个章节装订成整本IR。 184 常用于 DocumentComposer 将多个章节装订成整本IR。
  185 +
  186 + 参数:
  187 + run_dir: 会话根目录。
  188 +
  189 + 返回:
  190 + list[dict]: 章节payload列表。
162 """ 191 """
163 payloads: List[Dict[str, object]] = [] 192 payloads: List[Dict[str, object]] = []
164 for child in sorted(run_dir.iterdir()): 193 for child in sorted(run_dir.iterdir()):
@@ -183,6 +212,12 @@ class ChapterStorage: @@ -183,6 +212,12 @@ class ChapterStorage:
183 将流式输出实时写入raw文件。 212 将流式输出实时写入raw文件。
184 213
185 通过 contextmanager 暴露文件句柄,简化章节节点的写入逻辑。 214 通过 contextmanager 暴露文件句柄,简化章节节点的写入逻辑。
  215 +
  216 + 参数:
  217 + chapter_dir: 当前章节目录。
  218 +
  219 + 返回:
  220 + Generator[TextIO]: 作为上下文管理器使用的文件对象。
186 """ 221 """
187 raw_path = self._raw_stream_path(chapter_dir) 222 raw_path = self._raw_stream_path(chapter_dir)
188 raw_path.parent.mkdir(parents=True, exist_ok=True) 223 raw_path.parent.mkdir(parents=True, exist_ok=True)
@@ -36,6 +36,14 @@ class DocumentComposer: @@ -36,6 +36,14 @@ class DocumentComposer:
36 把所有章节按order排序并注入唯一锚点,形成整本IR。 36 把所有章节按order排序并注入唯一锚点,形成整本IR。
37 37
38 同时合并 metadata/themeTokens/assets,供渲染器直接消费。 38 同时合并 metadata/themeTokens/assets,供渲染器直接消费。
  39 +
  40 + 参数:
  41 + report_id: 本次报告ID。
  42 + metadata: 全局元信息(标题、主题、toc等)。
  43 + chapters: 章节payload列表。
  44 +
  45 + 返回:
  46 + dict: 满足渲染器需求的Document IR。
39 """ 47 """
40 ordered = sorted(chapters, key=lambda c: c.get("order", 0)) 48 ordered = sorted(chapters, key=lambda c: c.get("order", 0))
41 for idx, chapter in enumerate(ordered, start=1): 49 for idx, chapter in enumerate(ordered, start=1):
@@ -63,6 +63,12 @@ def parse_template_sections(template_md: str) -> List[TemplateSection]: @@ -63,6 +63,12 @@ def parse_template_sections(template_md: str) -> List[TemplateSection]:
63 返回的每个TemplateSection都携带slug/order/章节号, 63 返回的每个TemplateSection都携带slug/order/章节号,
64 方便后续分章调用与锚点生成。解析时会同时兼容 64 方便后续分章调用与锚点生成。解析时会同时兼容
65 “# 标题”“无符号编号”“列表提纲”等不同写法。 65 “# 标题”“无符号编号”“列表提纲”等不同写法。
  66 +
  67 + 参数:
  68 + template_md: 模板Markdown全文。
  69 +
  70 + 返回:
  71 + list[TemplateSection]: 结构化的章节序列。
66 """ 72 """
67 73
68 sections: List[TemplateSection] = [] 74 sections: List[TemplateSection] = []
@@ -113,6 +119,13 @@ def _classify_line(stripped: str, indent: int) -> Optional[dict]: @@ -113,6 +119,13 @@ def _classify_line(stripped: str, indent: int) -> Optional[dict]:
113 119
114 借助正则判断当前行是章节标题、提纲还是普通列表项, 120 借助正则判断当前行是章节标题、提纲还是普通列表项,
115 并衍生 depth/slug/number 等派生信息。 121 并衍生 depth/slug/number 等派生信息。
  122 +
  123 + 参数:
  124 + stripped: 去除前后空格后的原始行。
  125 + indent: 行首空格数量,用于区分层级。
  126 +
  127 + 返回:
  128 + dict | None: 识别后的元数据;无法识别时返回None。
116 """ 129 """
117 130
118 heading_match = heading_pattern.match(stripped) 131 heading_match = heading_pattern.match(stripped)
@@ -181,6 +194,12 @@ def _split_number(payload: str) -> dict: @@ -181,6 +194,12 @@ def _split_number(payload: str) -> dict:
181 194
182 例如 `1.2 市场趋势` 会被拆成 number=1.2、label=市场趋势, 195 例如 `1.2 市场趋势` 会被拆成 number=1.2、label=市场趋势,
183 并提供 display 用于回填标题。 196 并提供 display 用于回填标题。
  197 +
  198 + 参数:
  199 + payload: 原始标题字符串。
  200 +
  201 + 返回:
  202 + dict: 包含 number/title/display。
184 """ 203 """
185 match = number_pattern.match(payload) 204 match = number_pattern.match(payload)
186 number = match.group("num") if match else "" 205 number = match.group("num") if match else ""
@@ -196,7 +215,16 @@ def _split_number(payload: str) -> dict: @@ -196,7 +215,16 @@ def _split_number(payload: str) -> dict:
196 215
197 216
198 def _build_slug(number: str, title: str) -> str: 217 def _build_slug(number: str, title: str) -> str:
199 - """根据编号/标题生成锚点,优先复用编号,缺失时对标题slug化。""" 218 + """
  219 + 根据编号/标题生成锚点,优先复用编号,缺失时对标题slug化。
  220 +
  221 + 参数:
  222 + number: 章节编号。
  223 + title: 标题文本。
  224 +
  225 + 返回:
  226 + str: 形如 `section-1-0` 的slug。
  227 + """
200 if number: 228 if number:
201 token = number.replace(".", "-") 229 token = number.replace(".", "-")
202 else: 230 else:
@@ -223,6 +251,13 @@ def _ensure_unique_slug(slug: str, used: set) -> str: @@ -223,6 +251,13 @@ def _ensure_unique_slug(slug: str, used: set) -> str:
223 若slug重复则自动追加序号,直到在used集合中唯一。 251 若slug重复则自动追加序号,直到在used集合中唯一。
224 252
225 通过 `-2/-3...` 的方式保证相同标题不会产生重复锚点。 253 通过 `-2/-3...` 的方式保证相同标题不会产生重复锚点。
  254 +
  255 + 参数:
  256 + slug: 初始slug。
  257 + used: 已使用集合。
  258 +
  259 + 返回:
  260 + str: 去重后的slug。
226 """ 261 """
227 if slug not in used: 262 if slug not in used:
228 used.add(slug) 263 used.add(slug)
@@ -43,6 +43,12 @@ def _register_stream(task_id: str) -> Queue: @@ -43,6 +43,12 @@ def _register_stream(task_id: str) -> Queue:
43 为指定任务注册一个事件队列,供SSE监听器消费。 43 为指定任务注册一个事件队列,供SSE监听器消费。
44 44
45 返回的 Queue 会存入 `stream_subscribers`,SSE 生成器将不断读取。 45 返回的 Queue 会存入 `stream_subscribers`,SSE 生成器将不断读取。
  46 +
  47 + 参数:
  48 + task_id: 需要监听的任务ID。
  49 +
  50 + 返回:
  51 + Queue: 线程安全的事件队列。
46 """ 52 """
47 queue = Queue() 53 queue = Queue()
48 with stream_lock: 54 with stream_lock:
@@ -55,6 +61,10 @@ def _unregister_stream(task_id: str, queue: Queue): @@ -55,6 +61,10 @@ def _unregister_stream(task_id: str, queue: Queue):
55 安全移除事件队列,避免内存泄漏。 61 安全移除事件队列,避免内存泄漏。
56 62
57 需要在finally中调用,保证异常情况下资源也能释放。 63 需要在finally中调用,保证异常情况下资源也能释放。
  64 +
  65 + 参数:
  66 + task_id: 任务ID。
  67 + queue: 之前注册的事件队列。
58 """ 68 """
59 with stream_lock: 69 with stream_lock:
60 listeners = stream_subscribers.get(task_id, []) 70 listeners = stream_subscribers.get(task_id, [])
@@ -69,6 +79,10 @@ def _broadcast_event(task_id: str, event: Dict[str, Any]): @@ -69,6 +79,10 @@ def _broadcast_event(task_id: str, event: Dict[str, Any]):
69 将事件推送给所有监听者,失败时做好异常捕获。 79 将事件推送给所有监听者,失败时做好异常捕获。
70 80
71 采用浅拷贝监听列表,防止并发移除导致遍历异常。 81 采用浅拷贝监听列表,防止并发移除导致遍历异常。
  82 +
  83 + 参数:
  84 + task_id: 待推送的任务ID。
  85 + event: 结构化事件payload。
72 """ 86 """
73 with stream_lock: 87 with stream_lock:
74 listeners = list(stream_subscribers.get(task_id, [])) 88 listeners = list(stream_subscribers.get(task_id, []))
@@ -84,6 +98,9 @@ def _prune_task_history_locked(): @@ -84,6 +98,9 @@ def _prune_task_history_locked():
84 在task_lock持有期间调用,清理过多的历史任务。 98 在task_lock持有期间调用,清理过多的历史任务。
85 99
86 仅保留最近 `MAX_TASK_HISTORY` 个任务,避免长时间运行占用过多内存。 100 仅保留最近 `MAX_TASK_HISTORY` 个任务,避免长时间运行占用过多内存。
  101 +
  102 + 说明:
  103 + 该函数假设调用方已获取 `task_lock`,否则存在竞态风险。
87 """ 104 """
88 if len(tasks_registry) <= MAX_TASK_HISTORY: 105 if len(tasks_registry) <= MAX_TASK_HISTORY:
89 return 106 return
@@ -98,6 +115,12 @@ def _get_task(task_id: str) -> Optional['ReportTask']: @@ -98,6 +115,12 @@ def _get_task(task_id: str) -> Optional['ReportTask']:
98 统一的任务查找方法,优先返回当前任务。 115 统一的任务查找方法,优先返回当前任务。
99 116
100 避免重复写锁逻辑,便于多个API共享。 117 避免重复写锁逻辑,便于多个API共享。
  118 +
  119 + 参数:
  120 + task_id: 任务ID。
  121 +
  122 + 返回:
  123 + ReportTask | None: 命中时返回任务实例,否则为None。
101 """ 124 """
102 with task_lock: 125 with task_lock:
103 if current_task and current_task.task_id == task_id: 126 if current_task and current_task.task_id == task_id:
@@ -110,6 +133,12 @@ def _format_sse(event: Dict[str, Any]) -> str: @@ -110,6 +133,12 @@ def _format_sse(event: Dict[str, Any]) -> str:
110 按SSE协议格式化消息。 133 按SSE协议格式化消息。
111 134
112 输出形如 `id:/event:/data:` 的三段文本,供浏览器端直接消费。 135 输出形如 `id:/event:/data:` 的三段文本,供浏览器端直接消费。
  136 +
  137 + 参数:
  138 + event: 事件payload,至少包含 id/type。
  139 +
  140 + 返回:
  141 + str: SSE协议要求的字符串。
113 """ 142 """
114 payload = json.dumps(event, ensure_ascii=False) 143 payload = json.dumps(event, ensure_ascii=False)
115 event_id = event.get('id', 0) 144 event_id = event.get('id', 0)
@@ -122,6 +151,9 @@ def initialize_report_engine(): @@ -122,6 +151,9 @@ def initialize_report_engine():
122 初始化Report Engine。 151 初始化Report Engine。
123 152
124 单例化 ReportAgent,方便 API 启动后直接接收任务。 153 单例化 ReportAgent,方便 API 启动后直接接收任务。
  154 +
  155 + 返回:
  156 + bool: 初始化成功返回True,异常时返回False。
125 """ 157 """
126 global report_agent 158 global report_agent
127 try: 159 try:
@@ -176,6 +208,11 @@ class ReportTask: @@ -176,6 +208,11 @@ class ReportTask:
176 更新任务状态并广播事件。 208 更新任务状态并广播事件。
177 209
178 会自动刷新 `updated_at`、错误信息,并触发 `status` 类型的 SSE。 210 会自动刷新 `updated_at`、错误信息,并触发 `status` 类型的 SSE。
  211 +
  212 + 参数:
  213 + status: 任务阶段(pending/running/completed/error/cancelled)。
  214 + progress: 可选的进度百分比。
  215 + error_message: 出错时的人类可读说明。
179 """ 216 """
180 self.status = status 217 self.status = status
181 if progress is not None: 218 if progress is not None:
@@ -214,7 +251,13 @@ class ReportTask: @@ -214,7 +251,13 @@ class ReportTask:
214 } 251 }
215 252
216 def publish_event(self, event_type: str, payload: Dict[str, Any]) -> None: 253 def publish_event(self, event_type: str, payload: Dict[str, Any]) -> None:
217 - """将任意事件放入缓存并广播,所有新增逻辑均配套中文说明。""" 254 + """
  255 + 将任意事件放入缓存并广播,所有新增逻辑均配套中文说明。
  256 +
  257 + 参数:
  258 + event_type: SSE中的event名称。
  259 + payload: 实际业务数据。
  260 + """
218 timestamp = datetime.utcnow().isoformat() + 'Z' 261 timestamp = datetime.utcnow().isoformat() + 'Z'
219 event: Dict[str, Any] = { 262 event: Dict[str, Any] = {
220 'id': 0, 263 'id': 0,
@@ -230,7 +273,15 @@ class ReportTask: @@ -230,7 +273,15 @@ class ReportTask:
230 _broadcast_event(self.task_id, event) 273 _broadcast_event(self.task_id, event)
231 274
232 def history_since(self, last_event_id: Optional[int]) -> List[Dict[str, Any]]: 275 def history_since(self, last_event_id: Optional[int]) -> List[Dict[str, Any]]:
233 - """根据Last-Event-ID补发历史事件,确保断线重连无遗漏。""" 276 + """
  277 + 根据Last-Event-ID补发历史事件,确保断线重连无遗漏。
  278 +
  279 + 参数:
  280 + last_event_id: SSE客户端记录的最后一个事件ID。
  281 +
  282 + 返回:
  283 + list[dict]: 从 last_event_id 之后的事件列表。
  284 + """
234 with self._event_lock: 285 with self._event_lock:
235 if last_event_id is None: 286 if last_event_id is None:
236 return list(self.event_history) 287 return list(self.event_history)
@@ -272,6 +323,11 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " @@ -272,6 +323,11 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = "
272 323
273 包括:检查输入→加载文档→调用ReportAgent→持久化输出→ 324 包括:检查输入→加载文档→调用ReportAgent→持久化输出→
274 推送阶段性事件。出现错误会自动推送并写状态。 325 推送阶段性事件。出现错误会自动推送并写状态。
  326 +
  327 + 参数:
  328 + task: 本次任务对象,内部持有事件队列。
  329 + query: 报告主题。
  330 + custom_template: 可选的自定义模板字符串。
275 """ 331 """
276 global current_task 332 global current_task
277 333
@@ -385,7 +441,12 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " @@ -385,7 +441,12 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = "
385 441
386 @report_bp.route('/status', methods=['GET']) 442 @report_bp.route('/status', methods=['GET'])
387 def get_status(): 443 def get_status():
388 - """获取Report Engine状态,包括引擎就绪情况与当前任务信息。""" 444 + """
  445 + 获取Report Engine状态,包括引擎就绪情况与当前任务信息。
  446 +
  447 + 返回:
  448 + Response: JSON结构包含initialized/engines_ready/当前任务等。
  449 + """
389 try: 450 try:
390 engines_status = check_engines_ready() 451 engines_status = check_engines_ready()
391 452
@@ -411,6 +472,13 @@ def generate_report(): @@ -411,6 +472,13 @@ def generate_report():
411 开始生成报告。 472 开始生成报告。
412 473
413 负责排队、创建后台线程、清空日志并返回SSE地址。 474 负责排队、创建后台线程、清空日志并返回SSE地址。
  475 +
  476 + 请求体:
  477 + query: 报告主题(可选)。
  478 + custom_template: 自定义模板字符串(可选)。
  479 +
  480 + 返回:
  481 + Response: JSON,包含 task_id 与 SSE stream url。
414 """ 482 """
415 global current_task 483 global current_task
416 484
@@ -498,7 +566,15 @@ def generate_report(): @@ -498,7 +566,15 @@ def generate_report():
498 566
499 @report_bp.route('/progress/<task_id>', methods=['GET']) 567 @report_bp.route('/progress/<task_id>', methods=['GET'])
500 def get_progress(task_id: str): 568 def get_progress(task_id: str):
501 - """获取报告生成进度,若任务被清理则返回一个完成态兜底。""" 569 + """
  570 + 获取报告生成进度,若任务被清理则返回一个完成态兜底。
  571 +
  572 + 参数:
  573 + task_id: 任务唯一标识。
  574 +
  575 + 返回:
  576 + Response: JSON包含任务当前状态。
  577 + """
502 try: 578 try:
503 task = _get_task(task_id) 579 task = _get_task(task_id)
504 if not task: 580 if not task:
@@ -540,6 +616,12 @@ def stream_task(task_id: str): @@ -540,6 +616,12 @@ def stream_task(task_id: str):
540 - 自动补发Last-Event-ID之后的历史事件; 616 - 自动补发Last-Event-ID之后的历史事件;
541 - 周期性发送心跳以防代理中断; 617 - 周期性发送心跳以防代理中断;
542 - 任务结束后自动注销监听。 618 - 任务结束后自动注销监听。
  619 +
  620 + 参数:
  621 + task_id: 任务唯一标识。
  622 +
  623 + 返回:
  624 + Response: `text/event-stream` 类型响应。
543 """ 625 """
544 task = _get_task(task_id) 626 task = _get_task(task_id)
545 if not task: 627 if not task:
@@ -592,7 +674,15 @@ def stream_task(task_id: str): @@ -592,7 +674,15 @@ def stream_task(task_id: str):
592 674
593 @report_bp.route('/result/<task_id>', methods=['GET']) 675 @report_bp.route('/result/<task_id>', methods=['GET'])
594 def get_result(task_id: str): 676 def get_result(task_id: str):
595 - """获取报告生成结果""" 677 + """
  678 + 获取报告生成结果。
  679 +
  680 + 参数:
  681 + task_id: 任务ID。
  682 +
  683 + 返回:
  684 + Response: JSON,包含HTML预览与文件路径。
  685 + """
596 try: 686 try:
597 task = _get_task(task_id) 687 task = _get_task(task_id)
598 if not task: 688 if not task:
@@ -655,7 +745,15 @@ def get_result_json(task_id: str): @@ -655,7 +745,15 @@ def get_result_json(task_id: str):
655 745
656 @report_bp.route('/download/<task_id>', methods=['GET']) 746 @report_bp.route('/download/<task_id>', methods=['GET'])
657 def download_report(task_id: str): 747 def download_report(task_id: str):
658 - """下载已生成的报告HTML文件""" 748 + """
  749 + 下载已生成的报告HTML文件。
  750 +
  751 + 参数:
  752 + task_id: 任务ID。
  753 +
  754 + 返回:
  755 + Response: HTML文件的附件下载响应。
  756 + """
659 try: 757 try:
660 task = _get_task(task_id) 758 task = _get_task(task_id)
661 if not task: 759 if not task:
@@ -694,7 +792,15 @@ def download_report(task_id: str): @@ -694,7 +792,15 @@ def download_report(task_id: str):
694 792
695 @report_bp.route('/cancel/<task_id>', methods=['POST']) 793 @report_bp.route('/cancel/<task_id>', methods=['POST'])
696 def cancel_task(task_id: str): 794 def cancel_task(task_id: str):
697 - """取消报告生成任务""" 795 + """
  796 + 取消报告生成任务。
  797 +
  798 + 参数:
  799 + task_id: 需要被取消的任务ID。
  800 +
  801 + 返回:
  802 + Response: JSON,包含取消结果或错误信息。
  803 + """
698 global current_task 804 global current_task
699 805
700 try: 806 try:
@@ -735,7 +841,12 @@ def cancel_task(task_id: str): @@ -735,7 +841,12 @@ def cancel_task(task_id: str):
735 841
736 @report_bp.route('/templates', methods=['GET']) 842 @report_bp.route('/templates', methods=['GET'])
737 def get_templates(): 843 def get_templates():
738 - """获取可用模板列表,便于前端展示可选Markdown骨架。""" 844 + """
  845 + 获取可用模板列表,便于前端展示可选Markdown骨架。
  846 +
  847 + 返回:
  848 + Response: JSON,列出模板名称/描述/大小。
  849 + """
739 try: 850 try:
740 if not report_agent: 851 if not report_agent:
741 return jsonify({ 852 return jsonify({
@@ -799,7 +910,12 @@ def internal_error(error): @@ -799,7 +910,12 @@ def internal_error(error):
799 910
800 911
801 def clear_report_log(): 912 def clear_report_log():
802 - """清空report.log文件,方便新任务只查看本次运行日志。""" 913 + """
  914 + 清空report.log文件,方便新任务只查看本次运行日志。
  915 +
  916 + 返回:
  917 + None
  918 + """
803 try: 919 try:
804 log_file = settings.LOG_FILE 920 log_file = settings.LOG_FILE
805 with open(log_file, 'w', encoding='utf-8') as f: 921 with open(log_file, 'w', encoding='utf-8') as f:
@@ -811,7 +927,12 @@ def clear_report_log(): @@ -811,7 +927,12 @@ def clear_report_log():
811 927
812 @report_bp.route('/log', methods=['GET']) 928 @report_bp.route('/log', methods=['GET'])
813 def get_report_log(): 929 def get_report_log():
814 - """获取report.log内容,并按行去除空白返回。""" 930 + """
  931 + 获取report.log内容,并按行去除空白返回。
  932 +
  933 + 返回:
  934 + Response: JSON,包含最新日志行数组。
  935 + """
815 try: 936 try:
816 log_file = settings.LOG_FILE 937 log_file = settings.LOG_FILE
817 938
@@ -842,7 +963,12 @@ def get_report_log(): @@ -842,7 +963,12 @@ def get_report_log():
842 963
843 @report_bp.route('/log/clear', methods=['POST']) 964 @report_bp.route('/log/clear', methods=['POST'])
844 def clear_log(): 965 def clear_log():
845 - """手动清空日志,提供REST入口供前端一键重置。""" 966 + """
  967 + 手动清空日志,提供REST入口供前端一键重置。
  968 +
  969 + 返回:
  970 + Response: JSON,标记是否清理成功。
  971 + """
846 try: 972 try:
847 clear_report_log() 973 clear_report_log()
848 return jsonify({ 974 return jsonify({
@@ -101,15 +101,15 @@ class LLMClient: @@ -101,15 +101,15 @@ class LLMClient:
101 101
102 def stream_invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> Generator[str, None, None]: 102 def stream_invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> Generator[str, None, None]:
103 """ 103 """
104 - 流式调用LLM,逐步返回响应内容 104 + 流式调用LLM,逐步返回响应内容
105 105
106 - Args:  
107 - system_prompt: 系统提示词  
108 - user_prompt: 用户提示词  
109 - **kwargs: 额外参数(temperature, top_p等) 106 + 参数:
  107 + system_prompt: 系统提示词。
  108 + user_prompt: 用户提示词。
  109 + **kwargs: 采样参数(temperature、top_p等)。
110 110
111 - Yields:  
112 - 响应文本块(str),调用方可边读边写入磁盘或透传到UI 111 + 产出:
  112 + str: 每次yield一段delta文本,方便上层实时渲染。
113 """ 113 """
114 messages = [ 114 messages = [
115 {"role": "system", "content": system_prompt}, 115 {"role": "system", "content": system_prompt},
@@ -143,15 +143,15 @@ class LLMClient: @@ -143,15 +143,15 @@ class LLMClient:
143 @with_retry(LLM_RETRY_CONFIG) 143 @with_retry(LLM_RETRY_CONFIG)
144 def stream_invoke_to_string(self, system_prompt: str, user_prompt: str, **kwargs) -> str: 144 def stream_invoke_to_string(self, system_prompt: str, user_prompt: str, **kwargs) -> str:
145 """ 145 """
146 - 流式调用LLM并安全地拼接为完整字符串(避免UTF-8多字节字符截断) 146 + 流式调用LLM并安全地拼接为完整字符串(避免UTF-8多字节字符截断)
147 147
148 - Args:  
149 - system_prompt: 系统提示词  
150 - user_prompt: 用户提示词  
151 - **kwargs: 额外参数(temperature, top_p等) 148 + 参数:
  149 + system_prompt: 系统提示词。
  150 + user_prompt: 用户提示词。
  151 + **kwargs: 采样或超时配置。
152 152
153 - Returns:  
154 - 完整的响应字符串 153 + 返回:
  154 + str: 将所有delta拼接后的完整响应。
155 """ 155 """
156 # 以字节形式收集所有块 156 # 以字节形式收集所有块
157 byte_chunks = [] 157 byte_chunks = []
@@ -107,7 +107,23 @@ class ChapterGenerationNode(BaseNode): @@ -107,7 +107,23 @@ class ChapterGenerationNode(BaseNode):
107 stream_callback: Optional[Callable[[str, Dict[str, Any]], None]] = None, 107 stream_callback: Optional[Callable[[str, Dict[str, Any]], None]] = None,
108 **kwargs, 108 **kwargs,
109 ) -> Dict[str, Any]: 109 ) -> Dict[str, Any]:
110 - """针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果""" 110 + """
  111 + 针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果。
  112 +
  113 + 参数:
  114 + section: 模板切片生成的章节对象,包含标题/顺序/slug。
  115 + context: Agent构造的共享上下文(主题、篇幅、布局等)。
  116 + run_dir: 章节存盘目录,由 `ChapterStorage.start_session` 返回。
  117 + stream_callback: 可选流式回调,将LLM delta 推送给前端。
  118 + **kwargs: 透传温度、top_p等采样参数。
  119 +
  120 + 返回:
  121 + dict: 通过IR校验的章节JSON。
  122 +
  123 + 异常:
  124 + ChapterJsonParseError: 多次尝试后仍无法解析合法JSON。
  125 + ChapterContentError: 正文密度不足或只有标题,需要触发重试。
  126 + """
111 chapter_meta = { 127 chapter_meta = {
112 "chapterId": section.chapter_id, 128 "chapterId": section.chapter_id,
113 "slug": section.slug, 129 "slug": section.slug,
@@ -167,7 +183,16 @@ class ChapterGenerationNode(BaseNode): @@ -167,7 +183,16 @@ class ChapterGenerationNode(BaseNode):
167 # ====== 内部方法 ====== 183 # ====== 内部方法 ======
168 184
169 def _build_payload(self, section: TemplateSection, context: Dict[str, Any]) -> Dict[str, Any]: 185 def _build_payload(self, section: TemplateSection, context: Dict[str, Any]) -> Dict[str, Any]:
170 - """构造LLM输入payload""" 186 + """
  187 + 构造LLM输入payload。
  188 +
  189 + 参数:
  190 + section: 当前要生成的章节,提供标题/编号/提纲。
  191 + context: 全局上下文字典,包含主题、三引擎报告、篇幅规划等。
  192 +
  193 + 返回:
  194 + dict: 可以直接序列化进提示词的payload,兼顾章节信息与全局约束。
  195 + """
171 reports = context.get("reports", {}) 196 reports = context.get("reports", {})
172 # 章节篇幅规划(来自WordBudgetNode),用于指导字数与强调点 197 # 章节篇幅规划(来自WordBudgetNode),用于指导字数与强调点
173 chapter_plan_map = context.get("chapter_directives", {}) 198 chapter_plan_map = context.get("chapter_directives", {})
@@ -233,7 +258,19 @@ class ChapterGenerationNode(BaseNode): @@ -233,7 +258,19 @@ class ChapterGenerationNode(BaseNode):
233 section_meta: Optional[Dict[str, Any]] = None, 258 section_meta: Optional[Dict[str, Any]] = None,
234 **kwargs, 259 **kwargs,
235 ) -> str: 260 ) -> str:
236 - """流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。""" 261 + """
  262 + 流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。
  263 +
  264 + 参数:
  265 + user_message: 拼装好的用户提示词。
  266 + chapter_dir: 章节的本地缓存目录,用于存放 stream.raw。
  267 + stream_callback: SSE流式推送的回调函数。
  268 + section_meta: 附带的章节ID/标题,用于回调payload。
  269 + **kwargs: 透传温度、top_p等参数。
  270 +
  271 + 返回:
  272 + str: 将所有delta拼接后的原始文本。
  273 + """
237 chunks: List[str] = [] 274 chunks: List[str] = []
238 with self.storage.capture_stream(chapter_dir) as stream_fp: 275 with self.storage.capture_stream(chapter_dir) as stream_fp:
239 stream = self.llm_client.stream_invoke( 276 stream = self.llm_client.stream_invoke(
@@ -254,7 +291,18 @@ class ChapterGenerationNode(BaseNode): @@ -254,7 +291,18 @@ class ChapterGenerationNode(BaseNode):
254 return "".join(chunks) 291 return "".join(chunks)
255 292
256 def _parse_chapter(self, raw_text: str) -> Dict[str, Any]: 293 def _parse_chapter(self, raw_text: str) -> Dict[str, Any]:
257 - """清洗LLM输出并解析JSON""" 294 + """
  295 + 清洗LLM输出并解析JSON。
  296 +
  297 + 参数:
  298 + raw_text: LLM原始输出(可能包含```包裹或额外说明)。
  299 +
  300 + 返回:
  301 + dict: 章节JSON对象,至少包含 chapterId/title/blocks。
  302 +
  303 + 异常:
  304 + ChapterJsonParseError: 多种修复策略仍无法解析合法JSON。
  305 + """
258 cleaned = raw_text.strip() 306 cleaned = raw_text.strip()
259 if cleaned.startswith("```json"): 307 if cleaned.startswith("```json"):
260 cleaned = cleaned[7:] 308 cleaned = cleaned[7:]
@@ -304,7 +352,15 @@ class ChapterGenerationNode(BaseNode): @@ -304,7 +352,15 @@ class ChapterGenerationNode(BaseNode):
304 raise ValueError("章节JSON缺少chapter字段") 352 raise ValueError("章节JSON缺少chapter字段")
305 353
306 def _repair_llm_json(self, text: str) -> str: 354 def _repair_llm_json(self, text: str) -> str:
307 - """处理常见的LLM错误(如\":=导致的非法JSON)""" 355 + """
  356 + 处理常见的LLM错误(如":=导致的非法JSON)。
  357 +
  358 + 参数:
  359 + text: 原始章节JSON文本。
  360 +
  361 + 返回:
  362 + str: 修复后的文本;若未做改动则返回原内容。
  363 + """
308 repaired = text 364 repaired = text
309 mutated = False 365 mutated = False
310 366
@@ -482,7 +538,12 @@ class ChapterGenerationNode(BaseNode): @@ -482,7 +538,12 @@ class ChapterGenerationNode(BaseNode):
482 return fixed 538 return fixed
483 539
484 def _sanitize_chapter_blocks(self, chapter: Dict[str, Any]): 540 def _sanitize_chapter_blocks(self, chapter: Dict[str, Any]):
485 - """修正常见的结构性错误(例如list.items嵌套过深)""" 541 + """
  542 + 修正常见的结构性错误(例如list.items嵌套过深)。
  543 +
  544 + 参数:
  545 + chapter: 章节JSON对象,会在原地被清理和规整。
  546 + """
486 547
487 def walk(blocks: List[Dict[str, Any]] | None): 548 def walk(blocks: List[Dict[str, Any]] | None):
488 """递归检查并修复嵌套结构,保证每个block合法""" 549 """递归检查并修复嵌套结构,保证每个block合法"""
@@ -527,6 +588,12 @@ class ChapterGenerationNode(BaseNode): @@ -527,6 +588,12 @@ class ChapterGenerationNode(BaseNode):
527 588
528 blocks缺失、除标题外无有效区块,或正文字符数低于阈值, 589 blocks缺失、除标题外无有效区块,或正文字符数低于阈值,
529 则视为章节内容异常,触发ChapterContentError以便上游重试。 590 则视为章节内容异常,触发ChapterContentError以便上游重试。
  591 +
  592 + 参数:
  593 + chapter: 当前章节JSON
  594 +
  595 + 异常:
  596 + ChapterContentError: 当正文区块数量或字符数达不到下限时抛出。
530 """ 597 """
531 blocks = chapter.get("blocks") 598 blocks = chapter.get("blocks")
532 if not isinstance(blocks, list) or not blocks: 599 if not isinstance(blocks, list) or not blocks:
@@ -552,6 +619,12 @@ class ChapterGenerationNode(BaseNode): @@ -552,6 +619,12 @@ class ChapterGenerationNode(BaseNode):
552 - 忽略heading/divider/widget等非正文类型; 619 - 忽略heading/divider/widget等非正文类型;
553 - paragraph/list/table/callout等结构抽取嵌套文本; 620 - paragraph/list/table/callout等结构抽取嵌套文本;
554 - 仅用于粗粒度判断篇幅是否合理。 621 - 仅用于粗粒度判断篇幅是否合理。
  622 +
  623 + 参数:
  624 + blocks: 章节的 blocks 列表或子树。
  625 +
  626 + 返回:
  627 + int: 估算的正文字符数量。
555 """ 628 """
556 629
557 def walk(node: Any) -> int: 630 def walk(node: Any) -> int:
@@ -37,7 +37,20 @@ class DocumentLayoutNode(BaseNode): @@ -37,7 +37,20 @@ class DocumentLayoutNode(BaseNode):
37 query: str, 37 query: str,
38 template_overview: Dict[str, Any] | None = None, 38 template_overview: Dict[str, Any] | None = None,
39 ) -> Dict[str, Any]: 39 ) -> Dict[str, Any]:
40 - """综合模板+多源内容,生成全书的标题、目录结构与主题色板""" 40 + """
  41 + 综合模板+多源内容,生成全书的标题、目录结构与主题色板。
  42 +
  43 + 参数:
  44 + sections: 模板切片后的章节列表。
  45 + template_markdown: 模板原文,用于LLM理解上下文。
  46 + reports: 三个引擎的内容映射。
  47 + forum_logs: 论坛讨论摘要。
  48 + query: 用户查询词。
  49 + template_overview: 预生成的模板概览,可复用以减少提示词长度。
  50 +
  51 + 返回:
  52 + dict: 包含 title/subtitle/toc/hero/themeTokens 等设计信息的字典。
  53 + """
41 # 将模板原文、切片结构与多源报告一并喂给LLM,便于其理解层级与素材 54 # 将模板原文、切片结构与多源报告一并喂给LLM,便于其理解层级与素材
42 payload = { 55 payload = {
43 "query": query, 56 "query": query,
@@ -66,7 +79,18 @@ class DocumentLayoutNode(BaseNode): @@ -66,7 +79,18 @@ class DocumentLayoutNode(BaseNode):
66 return design 79 return design
67 80
68 def _parse_response(self, raw: str) -> Dict[str, Any]: 81 def _parse_response(self, raw: str) -> Dict[str, Any]:
69 - """解析LLM返回的JSON文本,若失败则抛出友好错误""" 82 + """
  83 + 解析LLM返回的JSON文本,若失败则抛出友好错误。
  84 +
  85 + 参数:
  86 + raw: LLM原始返回字符串,允许带```包裹。
  87 +
  88 + 返回:
  89 + dict: 结构化的设计稿。
  90 +
  91 + 异常:
  92 + ValueError: 当响应为空或JSON解析失败时抛出。
  93 + """
70 cleaned = raw.strip() 94 cleaned = raw.strip()
71 if cleaned.startswith("```json"): 95 if cleaned.startswith("```json"):
72 cleaned = cleaned[7:] 96 cleaned = cleaned[7:]
@@ -79,6 +79,15 @@ class TemplateSelectionNode(BaseNode): @@ -79,6 +79,15 @@ class TemplateSelectionNode(BaseNode):
79 79
80 构造模板列表与报告摘要 → 调用LLM → 解析JSON → 80 构造模板列表与报告摘要 → 调用LLM → 解析JSON →
81 验证模板是否存在并返回标准结构。 81 验证模板是否存在并返回标准结构。
  82 +
  83 + 参数:
  84 + query: 用户输入的主题词。
  85 + reports: 多个分析引擎的报告内容。
  86 + forum_logs: 论坛日志,可能为空。
  87 + available_templates: 本地可用模板清单。
  88 +
  89 + 返回:
  90 + dict | None: 若LLM成功返回合法结果则包含模板信息,否则为None。
82 """ 91 """
83 logger.info("尝试使用LLM进行模板选择...") 92 logger.info("尝试使用LLM进行模板选择...")
84 93
@@ -166,6 +175,12 @@ class TemplateSelectionNode(BaseNode): @@ -166,6 +175,12 @@ class TemplateSelectionNode(BaseNode):
166 清理LLM响应。 175 清理LLM响应。
167 176
168 去掉 ```json``` 包裹以及前后空白,方便 `json.loads`。 177 去掉 ```json``` 包裹以及前后空白,方便 `json.loads`。
  178 +
  179 + 参数:
  180 + response: LLM原始响应。
  181 +
  182 + 返回:
  183 + str: 适合直接做JSON解析的纯文本。
169 """ 184 """
170 # 移除可能的markdown代码块标记 185 # 移除可能的markdown代码块标记
171 if '```json' in response: 186 if '```json' in response:
@@ -183,6 +198,13 @@ class TemplateSelectionNode(BaseNode): @@ -183,6 +198,13 @@ class TemplateSelectionNode(BaseNode):
183 从文本响应中提取模板信息。 198 从文本响应中提取模板信息。
184 199
185 当LLM未输出合法JSON时,尝试匹配模板名称关键字做降级。 200 当LLM未输出合法JSON时,尝试匹配模板名称关键字做降级。
  201 +
  202 + 参数:
  203 + response: 非结构化的LLM文本。
  204 + available_templates: 可选模板列表。
  205 +
  206 + 返回:
  207 + dict | None: 匹配成功时返回模板详情,否则为None。
186 """ 208 """
187 logger.info("尝试从文本响应中提取模板信息") 209 logger.info("尝试从文本响应中提取模板信息")
188 210
@@ -210,6 +232,9 @@ class TemplateSelectionNode(BaseNode): @@ -210,6 +232,9 @@ class TemplateSelectionNode(BaseNode):
210 获取可用的模板列表。 232 获取可用的模板列表。
211 233
212 枚举模板目录下的 `.md` 文件并读取内容与描述字段。 234 枚举模板目录下的 `.md` 文件并读取内容与描述字段。
  235 +
  236 + 返回:
  237 + list[dict]: 每项包含 name/path/content/description。
213 """ 238 """
214 templates = [] 239 templates = []
215 240
@@ -259,7 +284,12 @@ class TemplateSelectionNode(BaseNode): @@ -259,7 +284,12 @@ class TemplateSelectionNode(BaseNode):
259 284
260 285
261 def _get_fallback_template(self) -> Dict[str, Any]: 286 def _get_fallback_template(self) -> Dict[str, Any]:
262 - """获取备用默认模板(空模板,让LLM自行发挥)。""" 287 + """
  288 + 获取备用默认模板(空模板,让LLM自行发挥)。
  289 +
  290 + 返回:
  291 + dict: 结构体字段与LLM返回一致,方便直接替换。
  292 + """
263 logger.info("未找到合适模板,使用空模板让LLM自行发挥") 293 logger.info("未找到合适模板,使用空模板让LLM自行发挥")
264 294
265 return { 295 return {
@@ -37,7 +37,20 @@ class WordBudgetNode(BaseNode): @@ -37,7 +37,20 @@ class WordBudgetNode(BaseNode):
37 query: str, 37 query: str,
38 template_overview: Dict[str, Any] | None = None, 38 template_overview: Dict[str, Any] | None = None,
39 ) -> Dict[str, Any]: 39 ) -> Dict[str, Any]:
40 - """根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标""" 40 + """
  41 + 根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标。
  42 +
  43 + 参数:
  44 + sections: 模板章节列表。
  45 + design: 布局节点返回的设计稿(title/toc/hero等)。
  46 + reports: 三引擎报告映射。
  47 + forum_logs: 论坛日志原文。
  48 + query: 用户查询词。
  49 + template_overview: 可选的模板概览,含章节元信息。
  50 +
  51 + 返回:
  52 + dict: 章节篇幅规划结果,包含 `totalWords`、`globalGuidelines` 与逐章 `chapters`。
  53 + """
41 # 输入中除了章节骨架外,还包含布局节点输出,方便约束篇幅时参考视觉主次 54 # 输入中除了章节骨架外,还包含布局节点输出,方便约束篇幅时参考视觉主次
42 payload = { 55 payload = {
43 "query": query, 56 "query": query,
@@ -63,7 +76,18 @@ class WordBudgetNode(BaseNode): @@ -63,7 +76,18 @@ class WordBudgetNode(BaseNode):
63 return plan 76 return plan
64 77
65 def _parse_response(self, raw: str) -> Dict[str, Any]: 78 def _parse_response(self, raw: str) -> Dict[str, Any]:
66 - """将LLM输出的JSON文本转为字典,失败时提示规划异常""" 79 + """
  80 + 将LLM输出的JSON文本转为字典,失败时提示规划异常。
  81 +
  82 + 参数:
  83 + raw: LLM返回值,可能包含```包裹。
  84 +
  85 + 返回:
  86 + dict: 合法的篇幅规划JSON。
  87 +
  88 + 异常:
  89 + ValueError: 当响应为空或JSON解析失败时抛出。
  90 + """
67 cleaned = raw.strip() 91 cleaned = raw.strip()
68 if cleaned.startswith("```json"): 92 if cleaned.startswith("```json"):
69 cleaned = cleaned[7:] 93 cleaned = cleaned[7:]
@@ -62,7 +62,15 @@ class HTMLRenderer: @@ -62,7 +62,15 @@ class HTMLRenderer:
62 # ====== 公共入口 ====== 62 # ====== 公共入口 ======
63 63
64 def render(self, document_ir: Dict[str, Any]) -> str: 64 def render(self, document_ir: Dict[str, Any]) -> str:
65 - """接收Document IR,重置内部状态并输出完整HTML""" 65 + """
  66 + 接收Document IR,重置内部状态并输出完整HTML。
  67 +
  68 + 参数:
  69 + document_ir: 由 DocumentComposer 生成的整本报告数据。
  70 +
  71 + 返回:
  72 + str: 可直接写入磁盘的完整HTML文档。
  73 + """
66 self.document = document_ir or {} 74 self.document = document_ir or {}
67 self.widget_scripts = [] 75 self.widget_scripts = []
68 self.chart_counter = 0 76 self.chart_counter = 0
@@ -89,7 +97,16 @@ class HTMLRenderer: @@ -89,7 +97,16 @@ class HTMLRenderer:
89 # ====== Head / Body ====== 97 # ====== Head / Body ======
90 98
91 def _render_head(self, title: str, theme_tokens: Dict[str, Any]) -> str: 99 def _render_head(self, title: str, theme_tokens: Dict[str, Any]) -> str:
92 - """渲染<head>部分,加载主题CSS与必要的脚本依赖""" 100 + """
  101 + 渲染<head>部分,加载主题CSS与必要的脚本依赖。
  102 +
  103 + 参数:
  104 + title: 页面title标签内容。
  105 + theme_tokens: 主题变量,用于注入CSS。
  106 +
  107 + 返回:
  108 + str: head片段HTML。
  109 + """
93 css = self._build_css(theme_tokens) 110 css = self._build_css(theme_tokens)
94 return f""" 111 return f"""
95 <head> 112 <head>
@@ -124,7 +141,12 @@ class HTMLRenderer: @@ -124,7 +141,12 @@ class HTMLRenderer:
124 </head>""".strip() 141 </head>""".strip()
125 142
126 def _render_body(self) -> str: 143 def _render_body(self) -> str:
127 - """拼装<body>结构,包含头部、导航、章节和脚本""" 144 + """
  145 + 拼装<body>结构,包含头部、导航、章节和脚本。
  146 +
  147 + 返回:
  148 + str: body片段HTML。
  149 + """
128 header = self._render_header() 150 header = self._render_header()
129 cover = self._render_cover() 151 cover = self._render_cover()
130 hero = self._render_hero() 152 hero = self._render_hero()
@@ -152,7 +174,12 @@ class HTMLRenderer: @@ -152,7 +174,12 @@ class HTMLRenderer:
152 # ====== Header / Meta / TOC ====== 174 # ====== Header / Meta / TOC ======
153 175
154 def _render_header(self) -> str: 176 def _render_header(self) -> str:
155 - """渲染吸顶头部,包含标题、副标题与功能按钮""" 177 + """
  178 + 渲染吸顶头部,包含标题、副标题与功能按钮。
  179 +
  180 + 返回:
  181 + str: header HTML。
  182 + """
156 metadata = self.metadata 183 metadata = self.metadata
157 title = metadata.get("title") or "智能舆情分析报告" 184 title = metadata.get("title") or "智能舆情分析报告"
158 subtitle = metadata.get("subtitle") or metadata.get("templateName") or "自动生成" 185 subtitle = metadata.get("subtitle") or metadata.get("templateName") or "自动生成"
@@ -172,14 +199,24 @@ class HTMLRenderer: @@ -172,14 +199,24 @@ class HTMLRenderer:
172 """.strip() 199 """.strip()
173 200
174 def _render_tagline(self) -> str: 201 def _render_tagline(self) -> str:
175 - """渲染标题下方的标语,如无标语则返回空字符串""" 202 + """
  203 + 渲染标题下方的标语,如无标语则返回空字符串。
  204 +
  205 + 返回:
  206 + str: tagline HTML或空串。
  207 + """
176 tagline = self.metadata.get("tagline") 208 tagline = self.metadata.get("tagline")
177 if not tagline: 209 if not tagline:
178 return "" 210 return ""
179 return f'<p class="tagline">{self._escape_html(tagline)}</p>' 211 return f'<p class="tagline">{self._escape_html(tagline)}</p>'
180 212
181 def _render_cover(self) -> str: 213 def _render_cover(self) -> str:
182 - """文章开头的封面区,居中展示标题与“文章总览”提示""" 214 + """
  215 + 文章开头的封面区,居中展示标题与“文章总览”提示。
  216 +
  217 + 返回:
  218 + str: cover section HTML。
  219 + """
183 title = self.metadata.get("title") or "智能舆情报告" 220 title = self.metadata.get("title") or "智能舆情报告"
184 subtitle = self.metadata.get("subtitle") or self.metadata.get("templateName") or "" 221 subtitle = self.metadata.get("subtitle") or self.metadata.get("templateName") or ""
185 overview_hint = "文章总览" 222 overview_hint = "文章总览"
@@ -192,7 +229,12 @@ class HTMLRenderer: @@ -192,7 +229,12 @@ class HTMLRenderer:
192 """.strip() 229 """.strip()
193 230
194 def _render_hero(self) -> str: 231 def _render_hero(self) -> str:
195 - """根据layout中的hero字段输出摘要/KPI/亮点区""" 232 + """
  233 + 根据layout中的hero字段输出摘要/KPI/亮点区。
  234 +
  235 + 返回:
  236 + str: hero区HTML,若无数据则为空字符串。
  237 + """
196 hero = self.metadata.get("hero") or {} 238 hero = self.metadata.get("hero") or {}
197 if not hero: 239 if not hero:
198 return "" 240 return ""
@@ -239,7 +281,12 @@ class HTMLRenderer: @@ -239,7 +281,12 @@ class HTMLRenderer:
239 return "" 281 return ""
240 282
241 def _render_toc_section(self) -> str: 283 def _render_toc_section(self) -> str:
242 - """生成目录模块,如无目录数据则返回空字符串""" 284 + """
  285 + 生成目录模块,如无目录数据则返回空字符串。
  286 +
  287 + 返回:
  288 + str: toc HTML结构。
  289 + """
243 if not self.toc_entries: 290 if not self.toc_entries:
244 return "" 291 return ""
245 toc_config = self.metadata.get("toc") or {} 292 toc_config = self.metadata.get("toc") or {}
@@ -258,7 +305,15 @@ class HTMLRenderer: @@ -258,7 +305,15 @@ class HTMLRenderer:
258 """.strip() 305 """.strip()
259 306
260 def _collect_toc_entries(self, chapters: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 307 def _collect_toc_entries(self, chapters: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
261 - """根据metadata中的tocPlan或章节heading收集目录项""" 308 + """
  309 + 根据metadata中的tocPlan或章节heading收集目录项。
  310 +
  311 + 参数:
  312 + chapters: Document IR中的章节数组。
  313 +
  314 + 返回:
  315 + list[dict]: 规范化后的目录条目,包含level/text/anchor。
  316 + """
262 metadata = self.metadata 317 metadata = self.metadata
263 toc_config = metadata.get("toc") or {} 318 toc_config = metadata.get("toc") or {}
264 custom_entries = toc_config.get("customEntries") 319 custom_entries = toc_config.get("customEntries")
@@ -296,7 +351,15 @@ class HTMLRenderer: @@ -296,7 +351,15 @@ class HTMLRenderer:
296 return entries 351 return entries
297 352
298 def _format_toc_entry(self, entry: Dict[str, Any]) -> str: 353 def _format_toc_entry(self, entry: Dict[str, Any]) -> str:
299 - """将单个目录项转为带描述的HTML行""" 354 + """
  355 + 将单个目录项转为带描述的HTML行。
  356 +
  357 + 参数:
  358 + entry: 目录条目,需包含 `text` 与 `anchor`。
  359 +
  360 + 返回:
  361 + str: `<li>` 形式的HTML。
  362 + """
300 desc = entry.get("description") 363 desc = entry.get("description")
301 desc_html = f'<p class="toc-desc">{self._escape_html(desc)}</p>' if desc else "" 364 desc_html = f'<p class="toc-desc">{self._escape_html(desc)}</p>' if desc else ""
302 level = entry.get("level", 2) 365 level = entry.get("level", 2)
@@ -304,7 +367,15 @@ class HTMLRenderer: @@ -304,7 +367,15 @@ class HTMLRenderer:
304 return f'<li class="level-{css_level}"><a href="#{self._escape_attr(entry["anchor"])}">{self._escape_html(entry["text"])}</a>{desc_html}</li>' 367 return f'<li class="level-{css_level}"><a href="#{self._escape_attr(entry["anchor"])}">{self._escape_html(entry["text"])}</a>{desc_html}</li>'
305 368
306 def _compute_heading_labels(self, chapters: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: 369 def _compute_heading_labels(self, chapters: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
307 - """预计算各级标题的编号(章:一、二;节:1.1;小节:1.1.1)""" 370 + """
  371 + 预计算各级标题的编号(章:一、二;节:1.1;小节:1.1.1)。
  372 +
  373 + 参数:
  374 + chapters: Document IR中的章节数组。
  375 +
  376 + 返回:
  377 + dict: 锚点到编号/描述的映射,方便TOC与正文引用。
  378 + """
308 label_map: Dict[str, Dict[str, Any]] = {} 379 label_map: Dict[str, Dict[str, Any]] = {}
309 380
310 for chap_idx, chapter in enumerate(chapters or [], start=1): 381 for chap_idx, chapter in enumerate(chapters or [], start=1):
@@ -394,17 +465,41 @@ class HTMLRenderer: @@ -394,17 +465,41 @@ class HTMLRenderer:
394 # ====== 章节 & Block 渲染 ====== 465 # ====== 章节 & Block 渲染 ======
395 466
396 def _render_chapter(self, chapter: Dict[str, Any]) -> str: 467 def _render_chapter(self, chapter: Dict[str, Any]) -> str:
397 - """将章节blocks包裹进<section>,便于CSS控制""" 468 + """
  469 + 将章节blocks包裹进<section>,便于CSS控制。
  470 +
  471 + 参数:
  472 + chapter: 单个章节JSON。
  473 +
  474 + 返回:
  475 + str: section包裹的HTML。
  476 + """
398 section_id = self._escape_attr(chapter.get("anchor") or f"chapter-{chapter.get('chapterId', 'x')}") 477 section_id = self._escape_attr(chapter.get("anchor") or f"chapter-{chapter.get('chapterId', 'x')}")
399 blocks_html = self._render_blocks(chapter.get("blocks", [])) 478 blocks_html = self._render_blocks(chapter.get("blocks", []))
400 return f'<section id="{section_id}" class="chapter">\n{blocks_html}\n</section>' 479 return f'<section id="{section_id}" class="chapter">\n{blocks_html}\n</section>'
401 480
402 def _render_blocks(self, blocks: List[Dict[str, Any]]) -> str: 481 def _render_blocks(self, blocks: List[Dict[str, Any]]) -> str:
403 - """顺序渲染章节内所有block""" 482 + """
  483 + 顺序渲染章节内所有block。
  484 +
  485 + 参数:
  486 + blocks: 章节内部的block数组。
  487 +
  488 + 返回:
  489 + str: 拼接后的HTML。
  490 + """
404 return "".join(self._render_block(block) for block in blocks or []) 491 return "".join(self._render_block(block) for block in blocks or [])
405 492
406 def _render_block(self, block: Dict[str, Any]) -> str: 493 def _render_block(self, block: Dict[str, Any]) -> str:
407 - """根据block.type分派到不同的渲染函数""" 494 + """
  495 + 根据block.type分派到不同的渲染函数。
  496 +
  497 + 参数:
  498 + block: 单个block对象。
  499 +
  500 + 返回:
  501 + str: 渲染后的HTML,未知类型会输出JSON调试信息。
  502 + """
408 block_type = block.get("type") 503 block_type = block.get("type")
409 handlers = { 504 handlers = {
410 "heading": self._render_heading, 505 "heading": self._render_heading,
@@ -468,7 +563,15 @@ class HTMLRenderer: @@ -468,7 +563,15 @@ class HTMLRenderer:
468 return f'<{tag}{class_attr}>{items_html}</{tag}>' 563 return f'<{tag}{class_attr}>{items_html}</{tag}>'
469 564
470 def _render_table(self, block: Dict[str, Any]) -> str: 565 def _render_table(self, block: Dict[str, Any]) -> str:
471 - """渲染表格,同时保留caption与单元格属性""" 566 + """
  567 + 渲染表格,同时保留caption与单元格属性。
  568 +
  569 + 参数:
  570 + block: table类型的block。
  571 +
  572 + 返回:
  573 + str: 包含<table>结构的HTML。
  574 + """
472 rows = self._normalize_table_rows(block.get("rows") or []) 575 rows = self._normalize_table_rows(block.get("rows") or [])
473 rows_html = "" 576 rows_html = ""
474 for row in rows: 577 for row in rows:
@@ -491,7 +594,15 @@ class HTMLRenderer: @@ -491,7 +594,15 @@ class HTMLRenderer:
491 return f'<div class="table-wrap"><table>{caption_html}<tbody>{rows_html}</tbody></table></div>' 594 return f'<div class="table-wrap"><table>{caption_html}<tbody>{rows_html}</tbody></table></div>'
492 595
493 def _normalize_table_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 596 def _normalize_table_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
494 - """检测并修正仅有单列的竖排表,转换为标准网格""" 597 + """
  598 + 检测并修正仅有单列的竖排表,转换为标准网格。
  599 +
  600 + 参数:
  601 + rows: 原始表格行。
  602 +
  603 + 返回:
  604 + list[dict]: 若检测到竖排表则返回转置后的行,否则原样返回。
  605 + """
495 if not rows: 606 if not rows:
496 return [] 607 return []
497 if not all(len((row.get("cells") or [])) == 1 for row in rows): 608 if not all(len((row.get("cells") or [])) == 1 for row in rows):
@@ -611,7 +722,15 @@ class HTMLRenderer: @@ -611,7 +722,15 @@ class HTMLRenderer:
611 return f'<div class="figure-placeholder">{self._escape_html(caption)}</div>' 722 return f'<div class="figure-placeholder">{self._escape_html(caption)}</div>'
612 723
613 def _render_callout(self, block: Dict[str, Any]) -> str: 724 def _render_callout(self, block: Dict[str, Any]) -> str:
614 - """渲染高亮提示盒,tone决定颜色""" 725 + """
  726 + 渲染高亮提示盒,tone决定颜色。
  727 +
  728 + 参数:
  729 + block: callout类型的block。
  730 +
  731 + 返回:
  732 + str: callout HTML,若内部包含不允许的块会被拆分。
  733 + """
615 tone = block.get("tone", "info") 734 tone = block.get("tone", "info")
616 title = block.get("title") 735 title = block.get("title")
617 safe_blocks, trailing_blocks = self._split_callout_content(block.get("blocks")) 736 safe_blocks, trailing_blocks = self._split_callout_content(block.get("blocks"))
@@ -689,7 +808,15 @@ class HTMLRenderer: @@ -689,7 +808,15 @@ class HTMLRenderer:
689 return f'<div class="kpi-grid">{cards}</div>' 808 return f'<div class="kpi-grid">{cards}</div>'
690 809
691 def _render_widget(self, block: Dict[str, Any]) -> str: 810 def _render_widget(self, block: Dict[str, Any]) -> str:
692 - """渲染Chart.js等交互组件的占位容器,并记录配置JSON""" 811 + """
  812 + 渲染Chart.js等交互组件的占位容器,并记录配置JSON。
  813 +
  814 + 参数:
  815 + block: widget类型的block,包含widgetId/props/data。
  816 +
  817 + 返回:
  818 + str: 含canvas与配置脚本的HTML。
  819 + """
693 self.chart_counter += 1 820 self.chart_counter += 1
694 canvas_id = f"chart-{self.chart_counter}" 821 canvas_id = f"chart-{self.chart_counter}"
695 config_id = f"chart-config-{self.chart_counter}" 822 config_id = f"chart-config-{self.chart_counter}"
@@ -830,7 +957,15 @@ class HTMLRenderer: @@ -830,7 +957,15 @@ class HTMLRenderer:
830 return payload 957 return payload
831 958
832 def _render_inline(self, run: Dict[str, Any]) -> str: 959 def _render_inline(self, run: Dict[str, Any]) -> str:
833 - """渲染单个inline run,支持多种marks叠加""" 960 + """
  961 + 渲染单个inline run,支持多种marks叠加。
  962 +
  963 + 参数:
  964 + run: 含 text 与 marks 的内联节点。
  965 +
  966 + 返回:
  967 + str: 已包裹标签/样式的HTML片段。
  968 + """
834 text_value, marks = self._normalize_inline_payload(run) 969 text_value, marks = self._normalize_inline_payload(run)
835 math_mark = next((mark for mark in marks if mark.get("type") == "math"), None) 970 math_mark = next((mark for mark in marks if mark.get("type") == "math"), None)
836 if math_mark: 971 if math_mark:
@@ -47,7 +47,12 @@ settings = Settings() @@ -47,7 +47,12 @@ settings = Settings()
47 47
48 48
49 def print_config(config: Settings): 49 def print_config(config: Settings):
50 - """将当前配置项按人类可读格式输出到日志,方便排障""" 50 + """
  51 + 将当前配置项按人类可读格式输出到日志,方便排障。
  52 +
  53 + 参数:
  54 + config: Settings实例,通常为全局settings。
  55 + """
51 message = "" 56 message = ""
52 message += "\n=== Report Engine 配置 ===\n" 57 message += "\n=== Report Engine 配置 ===\n"
53 message += f"LLM 模型: {config.REPORT_ENGINE_MODEL_NAME}\n" 58 message += f"LLM 模型: {config.REPORT_ENGINE_MODEL_NAME}\n"
@@ -1081,6 +1081,7 @@ @@ -1081,6 +1081,7 @@
1081 </style> 1081 </style>
1082 </head> 1082 </head>
1083 <body> 1083 <body>
  1084 + <!-- 顶层容器:同时包裹搜索区、双列主工作区与状态栏 -->
1084 <div class="container"> 1085 <div class="container">
1085 <!-- 搜索框区域 --> 1086 <!-- 搜索框区域 -->
1086 <div class="search-section"> 1087 <div class="search-section">
@@ -1158,13 +1159,14 @@ @@ -1158,13 +1159,14 @@
1158 </div> 1159 </div>
1159 </div> 1160 </div>
1160 1161
1161 - <!-- 状态栏 --> 1162 + <!-- 状态栏:实时展示WebSocket连接状态与系统时钟 -->
1162 <div class="status-bar"> 1163 <div class="status-bar">
1163 <span id="connectionStatus">连接中...</span> 1164 <span id="connectionStatus">连接中...</span>
1164 <span id="systemTime"></span> 1165 <span id="systemTime"></span>
1165 </div> 1166 </div>
1166 </div> 1167 </div>
1167 1168
  1169 + <!-- 配置弹窗:与后端.env互通,允许在线修改LLM参数 -->
1168 <div class="config-modal-overlay" id="configModal"> 1170 <div class="config-modal-overlay" id="configModal">
1169 <div class="config-modal"> 1171 <div class="config-modal">
1170 <div class="config-modal-header"> 1172 <div class="config-modal-header">
@@ -1187,9 +1189,10 @@ @@ -1187,9 +1189,10 @@
1187 </div> 1189 </div>
1188 </div> 1190 </div>
1189 1191
1190 - <!-- 消息提示 --> 1192 + <!-- 消息提示:右上角滑出式成功/错误提醒 -->
1191 <div class="message" id="message"></div> 1193 <div class="message" id="message"></div>
1192 1194
  1195 + <!-- 前端业务脚本:维护Socket连接、引擎启动状态与Report Engine交互 -->
1193 <script> 1196 <script>
1194 // 全局变量 1197 // 全局变量
1195 let socket; 1198 let socket;