Toggle navigation
Toggle navigation
This project
Loading...
Sign in
万朱浩
/
Venue-Ops
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
马一丁
2025-11-14 19:44:04 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
6d0e8f4b8ce76368a4720dc10593af0f4dc9e31b
6d0e8f4b
1 parent
52eed4d0
Add Comments
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
655 additions
and
61 deletions
ReportEngine/agent.py
ReportEngine/core/chapter_storage.py
ReportEngine/core/stitcher.py
ReportEngine/core/template_parser.py
ReportEngine/flask_interface.py
ReportEngine/llms/base.py
ReportEngine/nodes/chapter_generation_node.py
ReportEngine/nodes/document_layout_node.py
ReportEngine/nodes/template_selection_node.py
ReportEngine/nodes/word_budget_node.py
ReportEngine/renderers/html_renderer.py
ReportEngine/utils/config.py
templates/index.html
ReportEngine/agent.py
View file @
6d0e8f4
...
...
@@ -296,8 +296,19 @@ class ReportAgent:
4. 将章节装订成Document IR,再交给HTML渲染器生成成品;
5. 可选地将HTML/IR/状态落盘,并向外界回传路径信息。
Returns:
dict: HTML内容以及保存的文件路径信息
参数:
query: 最终要生成的报告主题或提问语句。
reports: 来自 Query/Media/Insight 等分析引擎的原始输出,允许传入字符串或更复杂的对象。
forum_logs: 论坛/协同记录,供LLM理解多人讨论上下文。
custom_template: 用户指定的Markdown模板,如为空则交由模板节点自动挑选。
save_report: 是否在生成后自动将HTML、IR与状态写入磁盘。
stream_handler: 可选的流式事件回调,接收阶段标签与payload,用于UI实时展示。
返回:
dict: 包含 `html_content` 以及HTML/IR/状态文件路径的字典;若 `save_report=False` 则仅返回HTML字符串。
异常:
Exception: 任一子节点或渲染阶段失败时抛出,外层调用方负责兜底。
"""
start_time
=
datetime
.
now
()
report_id
=
f
"report-{uuid4().hex[:8]}"
...
...
@@ -538,6 +549,15 @@ class ReportAgent:
优先使用用户指定的模板;否则将查询、三引擎报告与论坛日志
作为上下文交给 TemplateSelectionNode,由 LLM 返回最契合的
模板名称、内容及理由,并自动记录在状态中。
参数:
query: 报告主题,用于提示词聚焦行业/事件。
reports: 多来源报告原文,帮助LLM判断结构复杂度。
forum_logs: 对应论坛或协作讨论的文本,用于补充背景。
custom_template: CLI/前端传入的自定义Markdown模板,非空时直接采用。
返回:
dict: 包含 `template_name`、`template_content` 与 `selection_reason` 的结构化结果,供后续节点消费。
"""
logger
.
info
(
"选择报告模板..."
)
...
...
@@ -584,6 +604,12 @@ class ReportAgent:
委托 `parse_template_sections` 将Markdown标题/编号解析为
`TemplateSection` 列表,确保后续章节生成有稳定的章节ID。
当模板格式异常时,会回退到内置的简单骨架避免崩溃。
参数:
template_markdown: 完整的模板Markdown文本。
返回:
list[TemplateSection]: 解析后的章节序列;如解析失败则返回单章兜底结构。
"""
sections
=
parse_template_sections
(
template_markdown
)
if
sections
:
...
...
@@ -618,6 +644,19 @@ class ReportAgent:
将模板名称、布局设计、主题配色、篇幅规划、论坛日志等
一次性整合为 `generation_context`,后续每章调用 LLM 时
直接复用,确保所有章节共享一致的语调和视觉约束。
参数:
query: 用户查询词。
reports: 归一化后的 query/media/insight 报告映射。
forum_logs: 三引擎讨论记录。
template_result: 模板节点返回的模板元信息。
layout_design: 文档布局节点产出的标题/目录/主题设计。
chapter_directives: 字数规划节点返回的章节指令映射。
word_plan: 篇幅规划原始结果,包含全局字数约束。
template_overview: 模板切片提炼的章节骨架摘要。
返回:
dict: LLM章节生成所需的全集上下文,包含主题色、布局、约束等键。
"""
# 优先使用设计稿定制的主题色,否则退回默认主题
theme_tokens
=
(
...
...
@@ -650,6 +689,12 @@ class ReportAgent:
约定顺序为 Query/Media/Insight,引擎提供的对象可能是
字典或自定义类型,因此统一走 `_stringify` 做容错。
参数:
reports: 任意类型的报告列表,允许缺失或顺序混乱。
返回:
dict: 包含 `query_engine`/`media_engine`/`insight_engine` 三个字符串字段的映射。
"""
keys
=
[
"query_engine"
,
"media_engine"
,
"insight_engine"
]
normalized
:
Dict
[
str
,
str
]
=
{}
...
...
@@ -664,6 +709,12 @@ class ReportAgent:
当检测到供应商返回的错误包含特定关键词时,允许章节生成
重新尝试,以便绕过偶发的内容审查触发。
参数:
error: LLM客户端抛出的异常对象。
返回:
bool: 若匹配到内容审查关键词则返回True,否则为False。
"""
message
=
str
(
error
)
if
error
else
""
if
not
message
:
...
...
@@ -683,6 +734,12 @@ class ReportAgent:
- dict/list 统一序列化为格式化 JSON,便于提示词消费;
- 其他类型走 `str()`,None 则返回空串,避免 None 传播。
参数:
value: 任意Python对象。
返回:
str: 适配提示词/日志的字符串表现。
"""
if
value
is
None
:
return
""
...
...
@@ -700,6 +757,9 @@ class ReportAgent:
构造默认主题变量,供渲染器/LLM共用。
当布局节点未返回专属配色时使用该套色板,保持报告风格统一。
返回:
dict: 包含颜色、字体、间距、布尔开关等渲染参数的主题字典。
"""
return
{
"colors"
:
{
...
...
@@ -735,6 +795,13 @@ class ReportAgent:
提取模板标题与章节骨架,供设计/篇幅规划统一引用。
同时记录章节ID/slug/order等辅助字段,保证多节点对齐。
参数:
template_markdown: 模板原文,用于解析全局标题。
sections: `TemplateSection` 列表,作为章节骨架。
返回:
dict: 包含模板标题与章节元数据的概览结构。
"""
fallback_title
=
sections
[
0
]
.
title
if
sections
else
""
overview
=
{
...
...
@@ -763,6 +830,13 @@ class ReportAgent:
优先返回首个 `#` 语法标题;如果模板首行就是正文,则回退到
第一行非空文本或调用方提供的 fallback。
参数:
template_markdown: 模板原文。
fallback: 备用标题,当文档缺少显式标题时使用。
返回:
str: 解析到的标题文本。
"""
for
line
in
template_markdown
.
splitlines
():
stripped
=
line
.
strip
()
...
...
@@ -834,6 +908,14 @@ class ReportAgent:
生成基于查询和时间戳的易读文件名,同时也把运行态的
`ReportState` 写入 JSON,方便下游排障或断点续跑。
参数:
html_content: 渲染后的HTML正文。
document_ir: Document IR结构化数据。
report_id: 当前任务ID,用于创建独立文件名。
返回:
dict: 记录HTML/IR/State文件的绝对与相对路径信息。
"""
timestamp
=
datetime
.
now
()
.
strftime
(
"
%
Y
%
m
%
d_
%
H
%
M
%
S"
)
query_safe
=
""
.
join
(
...
...
@@ -879,6 +961,14 @@ class ReportAgent:
`Document IR` 与 HTML 解耦保存,便于调试渲染差异以及
在不重新跑 LLM 的情况下再次渲染或导出其他格式。
参数:
document_ir: 整本报告的IR结构。
query_safe: 已清洗的查询短语,用于文件命名。
timestamp: 运行时间戳,保证文件名唯一。
返回:
Path: 指向保存后的IR文件路径。
"""
filename
=
f
"report_ir_{query_safe}_{timestamp}.json"
ir_path
=
Path
(
self
.
config
.
DOCUMENT_IR_OUTPUT_DIR
)
/
filename
...
...
@@ -901,6 +991,12 @@ class ReportAgent:
这些中间件文件(document_layout/word_plan/template_overview)
方便在调试或复盘时快速定位:标题/目录/主题是如何确定的、
字数分配有什么要求,以便后续人工校正。
参数:
run_dir: 章节输出根目录。
layout_design: 文档布局节点的原始输出。
word_plan: 篇幅规划节点输出。
template_overview: 模板概览JSON。
"""
artifacts
=
{
"document_layout"
:
layout_design
,
...
...
ReportEngine/core/chapter_storage.py
View file @
6d0e8f4
...
...
@@ -75,6 +75,13 @@ class ChapterStorage:
为本次报告创建独立的章节输出目录与manifest。
同时把全局metadata写入 `manifest.json`,供渲染/调试查询。
参数:
report_id: 任务ID。
metadata: Report元数据(标题、主题等)。
返回:
Path: 新建的run目录。
"""
run_dir
=
self
.
base_dir
/
report_id
run_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
...
...
@@ -93,6 +100,13 @@ class ChapterStorage:
创建章节子目录并在manifest中标记为streaming状态。
会生成 `order-slug` 风格的子目录,并提前登记 raw 文件路径。
参数:
run_dir: 会话根目录。
chapter_meta: 包含 chapterId/title/slug/order 的元数据。
返回:
Path: 章节目录。
"""
slug_value
=
str
(
chapter_meta
.
get
(
"slug"
)
or
chapter_meta
.
get
(
"chapterId"
)
or
"section"
...
...
@@ -124,6 +138,15 @@ class ChapterStorage:
章节流式生成完毕后写入最终JSON并更新manifest状态。
若校验失败,错误信息会被写入manifest,供前端展示。
参数:
run_dir: 会话根目录。
chapter_meta: 章节元信息。
payload: 校验通过的章节JSON。
errors: 可选的错误列表,用于标记invalid状态。
返回:
Path: 最终的 `chapter.json` 文件路径。
"""
slug_value
=
str
(
chapter_meta
.
get
(
"slug"
)
or
chapter_meta
.
get
(
"chapterId"
)
or
"section"
...
...
@@ -159,6 +182,12 @@ class ChapterStorage:
从指定run目录读取全部chapter.json并按order排序返回。
常用于 DocumentComposer 将多个章节装订成整本IR。
参数:
run_dir: 会话根目录。
返回:
list[dict]: 章节payload列表。
"""
payloads
:
List
[
Dict
[
str
,
object
]]
=
[]
for
child
in
sorted
(
run_dir
.
iterdir
()):
...
...
@@ -183,6 +212,12 @@ class ChapterStorage:
将流式输出实时写入raw文件。
通过 contextmanager 暴露文件句柄,简化章节节点的写入逻辑。
参数:
chapter_dir: 当前章节目录。
返回:
Generator[TextIO]: 作为上下文管理器使用的文件对象。
"""
raw_path
=
self
.
_raw_stream_path
(
chapter_dir
)
raw_path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
...
...
ReportEngine/core/stitcher.py
View file @
6d0e8f4
...
...
@@ -36,6 +36,14 @@ class DocumentComposer:
把所有章节按order排序并注入唯一锚点,形成整本IR。
同时合并 metadata/themeTokens/assets,供渲染器直接消费。
参数:
report_id: 本次报告ID。
metadata: 全局元信息(标题、主题、toc等)。
chapters: 章节payload列表。
返回:
dict: 满足渲染器需求的Document IR。
"""
ordered
=
sorted
(
chapters
,
key
=
lambda
c
:
c
.
get
(
"order"
,
0
))
for
idx
,
chapter
in
enumerate
(
ordered
,
start
=
1
):
...
...
ReportEngine/core/template_parser.py
View file @
6d0e8f4
...
...
@@ -63,6 +63,12 @@ def parse_template_sections(template_md: str) -> List[TemplateSection]:
返回的每个TemplateSection都携带slug/order/章节号,
方便后续分章调用与锚点生成。解析时会同时兼容
“# 标题”“无符号编号”“列表提纲”等不同写法。
参数:
template_md: 模板Markdown全文。
返回:
list[TemplateSection]: 结构化的章节序列。
"""
sections
:
List
[
TemplateSection
]
=
[]
...
...
@@ -113,6 +119,13 @@ def _classify_line(stripped: str, indent: int) -> Optional[dict]:
借助正则判断当前行是章节标题、提纲还是普通列表项,
并衍生 depth/slug/number 等派生信息。
参数:
stripped: 去除前后空格后的原始行。
indent: 行首空格数量,用于区分层级。
返回:
dict | None: 识别后的元数据;无法识别时返回None。
"""
heading_match
=
heading_pattern
.
match
(
stripped
)
...
...
@@ -181,6 +194,12 @@ def _split_number(payload: str) -> dict:
例如 `1.2 市场趋势` 会被拆成 number=1.2、label=市场趋势,
并提供 display 用于回填标题。
参数:
payload: 原始标题字符串。
返回:
dict: 包含 number/title/display。
"""
match
=
number_pattern
.
match
(
payload
)
number
=
match
.
group
(
"num"
)
if
match
else
""
...
...
@@ -196,7 +215,16 @@ def _split_number(payload: str) -> dict:
def
_build_slug
(
number
:
str
,
title
:
str
)
->
str
:
"""根据编号/标题生成锚点,优先复用编号,缺失时对标题slug化。"""
"""
根据编号/标题生成锚点,优先复用编号,缺失时对标题slug化。
参数:
number: 章节编号。
title: 标题文本。
返回:
str: 形如 `section-1-0` 的slug。
"""
if
number
:
token
=
number
.
replace
(
"."
,
"-"
)
else
:
...
...
@@ -223,6 +251,13 @@ def _ensure_unique_slug(slug: str, used: set) -> str:
若slug重复则自动追加序号,直到在used集合中唯一。
通过 `-2/-3...` 的方式保证相同标题不会产生重复锚点。
参数:
slug: 初始slug。
used: 已使用集合。
返回:
str: 去重后的slug。
"""
if
slug
not
in
used
:
used
.
add
(
slug
)
...
...
ReportEngine/flask_interface.py
View file @
6d0e8f4
...
...
@@ -43,6 +43,12 @@ def _register_stream(task_id: str) -> Queue:
为指定任务注册一个事件队列,供SSE监听器消费。
返回的 Queue 会存入 `stream_subscribers`,SSE 生成器将不断读取。
参数:
task_id: 需要监听的任务ID。
返回:
Queue: 线程安全的事件队列。
"""
queue
=
Queue
()
with
stream_lock
:
...
...
@@ -55,6 +61,10 @@ def _unregister_stream(task_id: str, queue: Queue):
安全移除事件队列,避免内存泄漏。
需要在finally中调用,保证异常情况下资源也能释放。
参数:
task_id: 任务ID。
queue: 之前注册的事件队列。
"""
with
stream_lock
:
listeners
=
stream_subscribers
.
get
(
task_id
,
[])
...
...
@@ -69,6 +79,10 @@ def _broadcast_event(task_id: str, event: Dict[str, Any]):
将事件推送给所有监听者,失败时做好异常捕获。
采用浅拷贝监听列表,防止并发移除导致遍历异常。
参数:
task_id: 待推送的任务ID。
event: 结构化事件payload。
"""
with
stream_lock
:
listeners
=
list
(
stream_subscribers
.
get
(
task_id
,
[]))
...
...
@@ -84,6 +98,9 @@ def _prune_task_history_locked():
在task_lock持有期间调用,清理过多的历史任务。
仅保留最近 `MAX_TASK_HISTORY` 个任务,避免长时间运行占用过多内存。
说明:
该函数假设调用方已获取 `task_lock`,否则存在竞态风险。
"""
if
len
(
tasks_registry
)
<=
MAX_TASK_HISTORY
:
return
...
...
@@ -98,6 +115,12 @@ def _get_task(task_id: str) -> Optional['ReportTask']:
统一的任务查找方法,优先返回当前任务。
避免重复写锁逻辑,便于多个API共享。
参数:
task_id: 任务ID。
返回:
ReportTask | None: 命中时返回任务实例,否则为None。
"""
with
task_lock
:
if
current_task
and
current_task
.
task_id
==
task_id
:
...
...
@@ -110,6 +133,12 @@ def _format_sse(event: Dict[str, Any]) -> str:
按SSE协议格式化消息。
输出形如 `id:/event:/data:` 的三段文本,供浏览器端直接消费。
参数:
event: 事件payload,至少包含 id/type。
返回:
str: SSE协议要求的字符串。
"""
payload
=
json
.
dumps
(
event
,
ensure_ascii
=
False
)
event_id
=
event
.
get
(
'id'
,
0
)
...
...
@@ -122,6 +151,9 @@ def initialize_report_engine():
初始化Report Engine。
单例化 ReportAgent,方便 API 启动后直接接收任务。
返回:
bool: 初始化成功返回True,异常时返回False。
"""
global
report_agent
try
:
...
...
@@ -176,6 +208,11 @@ class ReportTask:
更新任务状态并广播事件。
会自动刷新 `updated_at`、错误信息,并触发 `status` 类型的 SSE。
参数:
status: 任务阶段(pending/running/completed/error/cancelled)。
progress: 可选的进度百分比。
error_message: 出错时的人类可读说明。
"""
self
.
status
=
status
if
progress
is
not
None
:
...
...
@@ -214,7 +251,13 @@ class ReportTask:
}
def
publish_event
(
self
,
event_type
:
str
,
payload
:
Dict
[
str
,
Any
])
->
None
:
"""将任意事件放入缓存并广播,所有新增逻辑均配套中文说明。"""
"""
将任意事件放入缓存并广播,所有新增逻辑均配套中文说明。
参数:
event_type: SSE中的event名称。
payload: 实际业务数据。
"""
timestamp
=
datetime
.
utcnow
()
.
isoformat
()
+
'Z'
event
:
Dict
[
str
,
Any
]
=
{
'id'
:
0
,
...
...
@@ -230,7 +273,15 @@ class ReportTask:
_broadcast_event
(
self
.
task_id
,
event
)
def
history_since
(
self
,
last_event_id
:
Optional
[
int
])
->
List
[
Dict
[
str
,
Any
]]:
"""根据Last-Event-ID补发历史事件,确保断线重连无遗漏。"""
"""
根据Last-Event-ID补发历史事件,确保断线重连无遗漏。
参数:
last_event_id: SSE客户端记录的最后一个事件ID。
返回:
list[dict]: 从 last_event_id 之后的事件列表。
"""
with
self
.
_event_lock
:
if
last_event_id
is
None
:
return
list
(
self
.
event_history
)
...
...
@@ -272,6 +323,11 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = "
包括:检查输入→加载文档→调用ReportAgent→持久化输出→
推送阶段性事件。出现错误会自动推送并写状态。
参数:
task: 本次任务对象,内部持有事件队列。
query: 报告主题。
custom_template: 可选的自定义模板字符串。
"""
global
current_task
...
...
@@ -385,7 +441,12 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = "
@report_bp.route
(
'/status'
,
methods
=
[
'GET'
])
def
get_status
():
"""获取Report Engine状态,包括引擎就绪情况与当前任务信息。"""
"""
获取Report Engine状态,包括引擎就绪情况与当前任务信息。
返回:
Response: JSON结构包含initialized/engines_ready/当前任务等。
"""
try
:
engines_status
=
check_engines_ready
()
...
...
@@ -411,6 +472,13 @@ def generate_report():
开始生成报告。
负责排队、创建后台线程、清空日志并返回SSE地址。
请求体:
query: 报告主题(可选)。
custom_template: 自定义模板字符串(可选)。
返回:
Response: JSON,包含 task_id 与 SSE stream url。
"""
global
current_task
...
...
@@ -498,7 +566,15 @@ def generate_report():
@report_bp.route
(
'/progress/<task_id>'
,
methods
=
[
'GET'
])
def
get_progress
(
task_id
:
str
):
"""获取报告生成进度,若任务被清理则返回一个完成态兜底。"""
"""
获取报告生成进度,若任务被清理则返回一个完成态兜底。
参数:
task_id: 任务唯一标识。
返回:
Response: JSON包含任务当前状态。
"""
try
:
task
=
_get_task
(
task_id
)
if
not
task
:
...
...
@@ -540,6 +616,12 @@ def stream_task(task_id: str):
- 自动补发Last-Event-ID之后的历史事件;
- 周期性发送心跳以防代理中断;
- 任务结束后自动注销监听。
参数:
task_id: 任务唯一标识。
返回:
Response: `text/event-stream` 类型响应。
"""
task
=
_get_task
(
task_id
)
if
not
task
:
...
...
@@ -592,7 +674,15 @@ def stream_task(task_id: str):
@report_bp.route
(
'/result/<task_id>'
,
methods
=
[
'GET'
])
def
get_result
(
task_id
:
str
):
"""获取报告生成结果"""
"""
获取报告生成结果。
参数:
task_id: 任务ID。
返回:
Response: JSON,包含HTML预览与文件路径。
"""
try
:
task
=
_get_task
(
task_id
)
if
not
task
:
...
...
@@ -655,7 +745,15 @@ def get_result_json(task_id: str):
@report_bp.route
(
'/download/<task_id>'
,
methods
=
[
'GET'
])
def
download_report
(
task_id
:
str
):
"""下载已生成的报告HTML文件"""
"""
下载已生成的报告HTML文件。
参数:
task_id: 任务ID。
返回:
Response: HTML文件的附件下载响应。
"""
try
:
task
=
_get_task
(
task_id
)
if
not
task
:
...
...
@@ -694,7 +792,15 @@ def download_report(task_id: str):
@report_bp.route
(
'/cancel/<task_id>'
,
methods
=
[
'POST'
])
def
cancel_task
(
task_id
:
str
):
"""取消报告生成任务"""
"""
取消报告生成任务。
参数:
task_id: 需要被取消的任务ID。
返回:
Response: JSON,包含取消结果或错误信息。
"""
global
current_task
try
:
...
...
@@ -735,7 +841,12 @@ def cancel_task(task_id: str):
@report_bp.route
(
'/templates'
,
methods
=
[
'GET'
])
def
get_templates
():
"""获取可用模板列表,便于前端展示可选Markdown骨架。"""
"""
获取可用模板列表,便于前端展示可选Markdown骨架。
返回:
Response: JSON,列出模板名称/描述/大小。
"""
try
:
if
not
report_agent
:
return
jsonify
({
...
...
@@ -799,7 +910,12 @@ def internal_error(error):
def
clear_report_log
():
"""清空report.log文件,方便新任务只查看本次运行日志。"""
"""
清空report.log文件,方便新任务只查看本次运行日志。
返回:
None
"""
try
:
log_file
=
settings
.
LOG_FILE
with
open
(
log_file
,
'w'
,
encoding
=
'utf-8'
)
as
f
:
...
...
@@ -811,7 +927,12 @@ def clear_report_log():
@report_bp.route
(
'/log'
,
methods
=
[
'GET'
])
def
get_report_log
():
"""获取report.log内容,并按行去除空白返回。"""
"""
获取report.log内容,并按行去除空白返回。
返回:
Response: JSON,包含最新日志行数组。
"""
try
:
log_file
=
settings
.
LOG_FILE
...
...
@@ -842,7 +963,12 @@ def get_report_log():
@report_bp.route
(
'/log/clear'
,
methods
=
[
'POST'
])
def
clear_log
():
"""手动清空日志,提供REST入口供前端一键重置。"""
"""
手动清空日志,提供REST入口供前端一键重置。
返回:
Response: JSON,标记是否清理成功。
"""
try
:
clear_report_log
()
return
jsonify
({
...
...
ReportEngine/llms/base.py
View file @
6d0e8f4
...
...
@@ -101,15 +101,15 @@ class LLMClient:
def
stream_invoke
(
self
,
system_prompt
:
str
,
user_prompt
:
str
,
**
kwargs
)
->
Generator
[
str
,
None
,
None
]:
"""
流式调用LLM,逐步返回响应内容
流式调用LLM,逐步返回响应内容
。
Args:
system_prompt: 系统提示词
user_prompt: 用户提示词
**kwargs: 额外参数(temperature, top_p等)
参数:
system_prompt: 系统提示词。
user_prompt: 用户提示词。
**kwargs: 采样参数(temperature、top_p等)。
Yields:
响应文本块(str),调用方可边读边写入磁盘或透传到UI
产出:
str: 每次yield一段delta文本,方便上层实时渲染。
"""
messages
=
[
{
"role"
:
"system"
,
"content"
:
system_prompt
},
...
...
@@ -143,15 +143,15 @@ class LLMClient:
@with_retry
(
LLM_RETRY_CONFIG
)
def
stream_invoke_to_string
(
self
,
system_prompt
:
str
,
user_prompt
:
str
,
**
kwargs
)
->
str
:
"""
流式调用LLM并安全地拼接为完整字符串(避免UTF-8多字节字符截断)
流式调用LLM并安全地拼接为完整字符串(避免UTF-8多字节字符截断)
。
Args:
system_prompt: 系统提示词
user_prompt: 用户提示词
**kwargs: 额外参数(temperature, top_p等)
参数:
system_prompt: 系统提示词。
user_prompt: 用户提示词。
**kwargs: 采样或超时配置。
Returns:
完整的响应字符串
返回:
str: 将所有delta拼接后的完整响应。
"""
# 以字节形式收集所有块
byte_chunks
=
[]
...
...
ReportEngine/nodes/chapter_generation_node.py
View file @
6d0e8f4
...
...
@@ -107,7 +107,23 @@ class ChapterGenerationNode(BaseNode):
stream_callback
:
Optional
[
Callable
[[
str
,
Dict
[
str
,
Any
]],
None
]]
=
None
,
**
kwargs
,
)
->
Dict
[
str
,
Any
]:
"""针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果"""
"""
针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果。
参数:
section: 模板切片生成的章节对象,包含标题/顺序/slug。
context: Agent构造的共享上下文(主题、篇幅、布局等)。
run_dir: 章节存盘目录,由 `ChapterStorage.start_session` 返回。
stream_callback: 可选流式回调,将LLM delta 推送给前端。
**kwargs: 透传温度、top_p等采样参数。
返回:
dict: 通过IR校验的章节JSON。
异常:
ChapterJsonParseError: 多次尝试后仍无法解析合法JSON。
ChapterContentError: 正文密度不足或只有标题,需要触发重试。
"""
chapter_meta
=
{
"chapterId"
:
section
.
chapter_id
,
"slug"
:
section
.
slug
,
...
...
@@ -167,7 +183,16 @@ class ChapterGenerationNode(BaseNode):
# ====== 内部方法 ======
def
_build_payload
(
self
,
section
:
TemplateSection
,
context
:
Dict
[
str
,
Any
])
->
Dict
[
str
,
Any
]:
"""构造LLM输入payload"""
"""
构造LLM输入payload。
参数:
section: 当前要生成的章节,提供标题/编号/提纲。
context: 全局上下文字典,包含主题、三引擎报告、篇幅规划等。
返回:
dict: 可以直接序列化进提示词的payload,兼顾章节信息与全局约束。
"""
reports
=
context
.
get
(
"reports"
,
{})
# 章节篇幅规划(来自WordBudgetNode),用于指导字数与强调点
chapter_plan_map
=
context
.
get
(
"chapter_directives"
,
{})
...
...
@@ -233,7 +258,19 @@ class ChapterGenerationNode(BaseNode):
section_meta
:
Optional
[
Dict
[
str
,
Any
]]
=
None
,
**
kwargs
,
)
->
str
:
"""流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。"""
"""
流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。
参数:
user_message: 拼装好的用户提示词。
chapter_dir: 章节的本地缓存目录,用于存放 stream.raw。
stream_callback: SSE流式推送的回调函数。
section_meta: 附带的章节ID/标题,用于回调payload。
**kwargs: 透传温度、top_p等参数。
返回:
str: 将所有delta拼接后的原始文本。
"""
chunks
:
List
[
str
]
=
[]
with
self
.
storage
.
capture_stream
(
chapter_dir
)
as
stream_fp
:
stream
=
self
.
llm_client
.
stream_invoke
(
...
...
@@ -254,7 +291,18 @@ class ChapterGenerationNode(BaseNode):
return
""
.
join
(
chunks
)
def
_parse_chapter
(
self
,
raw_text
:
str
)
->
Dict
[
str
,
Any
]:
"""清洗LLM输出并解析JSON"""
"""
清洗LLM输出并解析JSON。
参数:
raw_text: LLM原始输出(可能包含```包裹或额外说明)。
返回:
dict: 章节JSON对象,至少包含 chapterId/title/blocks。
异常:
ChapterJsonParseError: 多种修复策略仍无法解析合法JSON。
"""
cleaned
=
raw_text
.
strip
()
if
cleaned
.
startswith
(
"```json"
):
cleaned
=
cleaned
[
7
:]
...
...
@@ -304,7 +352,15 @@ class ChapterGenerationNode(BaseNode):
raise
ValueError
("章节
JSON
缺少
chapter
字段")
def
_repair_llm_json
(
self
,
text
:
str
)
->
str
:
"""处理常见的
LLM
错误(如\":=导致的非法
JSON
)"""
"""
处理常见的
LLM
错误(如":=导致的非法
JSON
)。
参数:
text
:
原始章节
JSON
文本。
返回:
str
:
修复后的文本;若未做改动则返回原内容。
"""
repaired
=
text
mutated
=
False
...
...
@@ -482,7 +538,12 @@ class ChapterGenerationNode(BaseNode):
return
fixed
def
_sanitize_chapter_blocks
(
self
,
chapter
:
Dict
[
str
,
Any
]):
"""修正常见的结构性错误(例如
list.items
嵌套过深)"""
"""
修正常见的结构性错误(例如
list.items
嵌套过深)。
参数:
chapter
:
章节
JSON
对象,会在原地被清理和规整。
"""
def
walk
(
blocks
:
List
[
Dict
[
str
,
Any
]]
|
None
):
"""递归检查并修复嵌套结构,保证每个
block
合法"""
...
...
@@ -527,6 +588,12 @@ class ChapterGenerationNode(BaseNode):
若
blocks
缺失、除标题外无有效区块,或正文字符数低于阈值,
则视为章节内容异常,触发
ChapterContentError
以便上游重试。
参数:
chapter
:
当前章节
JSON
。
异常:
ChapterContentError
:
当正文区块数量或字符数达不到下限时抛出。
"""
blocks
=
chapter.get
("
blocks
")
if
not
isinstance
(
blocks
,
list
)
or
not
blocks
:
...
...
@@ -552,6 +619,12 @@ class ChapterGenerationNode(BaseNode):
-
忽略
heading
/
divider
/
widget
等非正文类型;
-
对
paragraph
/
list
/
table
/
callout
等结构抽取嵌套文本;
-
仅用于粗粒度判断篇幅是否合理。
参数:
blocks
:
章节的
blocks
列表或子树。
返回:
int
:
估算的正文字符数量。
"""
def
walk
(
node
:
Any
)
->
int
:
...
...
ReportEngine/nodes/document_layout_node.py
View file @
6d0e8f4
...
...
@@ -37,7 +37,20 @@ class DocumentLayoutNode(BaseNode):
query
:
str
,
template_overview
:
Dict
[
str
,
Any
]
|
None
=
None
,
)
->
Dict
[
str
,
Any
]:
"""综合模板+多源内容,生成全书的标题、目录结构与主题色板"""
"""
综合模板+多源内容,生成全书的标题、目录结构与主题色板。
参数:
sections: 模板切片后的章节列表。
template_markdown: 模板原文,用于LLM理解上下文。
reports: 三个引擎的内容映射。
forum_logs: 论坛讨论摘要。
query: 用户查询词。
template_overview: 预生成的模板概览,可复用以减少提示词长度。
返回:
dict: 包含 title/subtitle/toc/hero/themeTokens 等设计信息的字典。
"""
# 将模板原文、切片结构与多源报告一并喂给LLM,便于其理解层级与素材
payload
=
{
"query"
:
query
,
...
...
@@ -66,7 +79,18 @@ class DocumentLayoutNode(BaseNode):
return
design
def
_parse_response
(
self
,
raw
:
str
)
->
Dict
[
str
,
Any
]:
"""解析LLM返回的JSON文本,若失败则抛出友好错误"""
"""
解析LLM返回的JSON文本,若失败则抛出友好错误。
参数:
raw: LLM原始返回字符串,允许带```包裹。
返回:
dict: 结构化的设计稿。
异常:
ValueError: 当响应为空或JSON解析失败时抛出。
"""
cleaned
=
raw
.
strip
()
if
cleaned
.
startswith
(
"```json"
):
cleaned
=
cleaned
[
7
:]
...
...
ReportEngine/nodes/template_selection_node.py
View file @
6d0e8f4
...
...
@@ -79,6 +79,15 @@ class TemplateSelectionNode(BaseNode):
构造模板列表与报告摘要 → 调用LLM → 解析JSON →
验证模板是否存在并返回标准结构。
参数:
query: 用户输入的主题词。
reports: 多个分析引擎的报告内容。
forum_logs: 论坛日志,可能为空。
available_templates: 本地可用模板清单。
返回:
dict | None: 若LLM成功返回合法结果则包含模板信息,否则为None。
"""
logger
.
info
(
"尝试使用LLM进行模板选择..."
)
...
...
@@ -166,6 +175,12 @@ class TemplateSelectionNode(BaseNode):
清理LLM响应。
去掉 ```json``` 包裹以及前后空白,方便 `json.loads`。
参数:
response: LLM原始响应。
返回:
str: 适合直接做JSON解析的纯文本。
"""
# 移除可能的markdown代码块标记
if
'```json'
in
response
:
...
...
@@ -183,6 +198,13 @@ class TemplateSelectionNode(BaseNode):
从文本响应中提取模板信息。
当LLM未输出合法JSON时,尝试匹配模板名称关键字做降级。
参数:
response: 非结构化的LLM文本。
available_templates: 可选模板列表。
返回:
dict | None: 匹配成功时返回模板详情,否则为None。
"""
logger
.
info
(
"尝试从文本响应中提取模板信息"
)
...
...
@@ -210,6 +232,9 @@ class TemplateSelectionNode(BaseNode):
获取可用的模板列表。
枚举模板目录下的 `.md` 文件并读取内容与描述字段。
返回:
list[dict]: 每项包含 name/path/content/description。
"""
templates
=
[]
...
...
@@ -259,7 +284,12 @@ class TemplateSelectionNode(BaseNode):
def
_get_fallback_template
(
self
)
->
Dict
[
str
,
Any
]:
"""获取备用默认模板(空模板,让LLM自行发挥)。"""
"""
获取备用默认模板(空模板,让LLM自行发挥)。
返回:
dict: 结构体字段与LLM返回一致,方便直接替换。
"""
logger
.
info
(
"未找到合适模板,使用空模板让LLM自行发挥"
)
return
{
...
...
ReportEngine/nodes/word_budget_node.py
View file @
6d0e8f4
...
...
@@ -37,7 +37,20 @@ class WordBudgetNode(BaseNode):
query
:
str
,
template_overview
:
Dict
[
str
,
Any
]
|
None
=
None
,
)
->
Dict
[
str
,
Any
]:
"""根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标"""
"""
根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标。
参数:
sections: 模板章节列表。
design: 布局节点返回的设计稿(title/toc/hero等)。
reports: 三引擎报告映射。
forum_logs: 论坛日志原文。
query: 用户查询词。
template_overview: 可选的模板概览,含章节元信息。
返回:
dict: 章节篇幅规划结果,包含 `totalWords`、`globalGuidelines` 与逐章 `chapters`。
"""
# 输入中除了章节骨架外,还包含布局节点输出,方便约束篇幅时参考视觉主次
payload
=
{
"query"
:
query
,
...
...
@@ -63,7 +76,18 @@ class WordBudgetNode(BaseNode):
return
plan
def
_parse_response
(
self
,
raw
:
str
)
->
Dict
[
str
,
Any
]:
"""将LLM输出的JSON文本转为字典,失败时提示规划异常"""
"""
将LLM输出的JSON文本转为字典,失败时提示规划异常。
参数:
raw: LLM返回值,可能包含```包裹。
返回:
dict: 合法的篇幅规划JSON。
异常:
ValueError: 当响应为空或JSON解析失败时抛出。
"""
cleaned
=
raw
.
strip
()
if
cleaned
.
startswith
(
"```json"
):
cleaned
=
cleaned
[
7
:]
...
...
ReportEngine/renderers/html_renderer.py
View file @
6d0e8f4
...
...
@@ -62,7 +62,15 @@ class HTMLRenderer:
# ====== 公共入口 ======
def
render
(
self
,
document_ir
:
Dict
[
str
,
Any
])
->
str
:
"""接收Document IR,重置内部状态并输出完整HTML"""
"""
接收Document IR,重置内部状态并输出完整HTML。
参数:
document_ir: 由 DocumentComposer 生成的整本报告数据。
返回:
str: 可直接写入磁盘的完整HTML文档。
"""
self
.
document
=
document_ir
or
{}
self
.
widget_scripts
=
[]
self
.
chart_counter
=
0
...
...
@@ -89,7 +97,16 @@ class HTMLRenderer:
# ====== Head / Body ======
def
_render_head
(
self
,
title
:
str
,
theme_tokens
:
Dict
[
str
,
Any
])
->
str
:
"""渲染<head>部分,加载主题CSS与必要的脚本依赖"""
"""
渲染<head>部分,加载主题CSS与必要的脚本依赖。
参数:
title: 页面title标签内容。
theme_tokens: 主题变量,用于注入CSS。
返回:
str: head片段HTML。
"""
css
=
self
.
_build_css
(
theme_tokens
)
return
f
"""
<head>
...
...
@@ -124,7 +141,12 @@ class HTMLRenderer:
</head>"""
.
strip
()
def
_render_body
(
self
)
->
str
:
"""拼装<body>结构,包含头部、导航、章节和脚本"""
"""
拼装<body>结构,包含头部、导航、章节和脚本。
返回:
str: body片段HTML。
"""
header
=
self
.
_render_header
()
cover
=
self
.
_render_cover
()
hero
=
self
.
_render_hero
()
...
...
@@ -152,7 +174,12 @@ class HTMLRenderer:
# ====== Header / Meta / TOC ======
def
_render_header
(
self
)
->
str
:
"""渲染吸顶头部,包含标题、副标题与功能按钮"""
"""
渲染吸顶头部,包含标题、副标题与功能按钮。
返回:
str: header HTML。
"""
metadata
=
self
.
metadata
title
=
metadata
.
get
(
"title"
)
or
"智能舆情分析报告"
subtitle
=
metadata
.
get
(
"subtitle"
)
or
metadata
.
get
(
"templateName"
)
or
"自动生成"
...
...
@@ -172,14 +199,24 @@ class HTMLRenderer:
"""
.
strip
()
def
_render_tagline
(
self
)
->
str
:
"""渲染标题下方的标语,如无标语则返回空字符串"""
"""
渲染标题下方的标语,如无标语则返回空字符串。
返回:
str: tagline HTML或空串。
"""
tagline
=
self
.
metadata
.
get
(
"tagline"
)
if
not
tagline
:
return
""
return
f
'<p class="tagline">{self._escape_html(tagline)}</p>'
def
_render_cover
(
self
)
->
str
:
"""文章开头的封面区,居中展示标题与“文章总览”提示"""
"""
文章开头的封面区,居中展示标题与“文章总览”提示。
返回:
str: cover section HTML。
"""
title
=
self
.
metadata
.
get
(
"title"
)
or
"智能舆情报告"
subtitle
=
self
.
metadata
.
get
(
"subtitle"
)
or
self
.
metadata
.
get
(
"templateName"
)
or
""
overview_hint
=
"文章总览"
...
...
@@ -192,7 +229,12 @@ class HTMLRenderer:
"""
.
strip
()
def
_render_hero
(
self
)
->
str
:
"""根据layout中的hero字段输出摘要/KPI/亮点区"""
"""
根据layout中的hero字段输出摘要/KPI/亮点区。
返回:
str: hero区HTML,若无数据则为空字符串。
"""
hero
=
self
.
metadata
.
get
(
"hero"
)
or
{}
if
not
hero
:
return
""
...
...
@@ -239,7 +281,12 @@ class HTMLRenderer:
return
""
def
_render_toc_section
(
self
)
->
str
:
"""生成目录模块,如无目录数据则返回空字符串"""
"""
生成目录模块,如无目录数据则返回空字符串。
返回:
str: toc HTML结构。
"""
if
not
self
.
toc_entries
:
return
""
toc_config
=
self
.
metadata
.
get
(
"toc"
)
or
{}
...
...
@@ -258,7 +305,15 @@ class HTMLRenderer:
"""
.
strip
()
def
_collect_toc_entries
(
self
,
chapters
:
List
[
Dict
[
str
,
Any
]])
->
List
[
Dict
[
str
,
Any
]]:
"""根据metadata中的tocPlan或章节heading收集目录项"""
"""
根据metadata中的tocPlan或章节heading收集目录项。
参数:
chapters: Document IR中的章节数组。
返回:
list[dict]: 规范化后的目录条目,包含level/text/anchor。
"""
metadata
=
self
.
metadata
toc_config
=
metadata
.
get
(
"toc"
)
or
{}
custom_entries
=
toc_config
.
get
(
"customEntries"
)
...
...
@@ -296,7 +351,15 @@ class HTMLRenderer:
return
entries
def
_format_toc_entry
(
self
,
entry
:
Dict
[
str
,
Any
])
->
str
:
"""将单个目录项转为带描述的HTML行"""
"""
将单个目录项转为带描述的HTML行。
参数:
entry: 目录条目,需包含 `text` 与 `anchor`。
返回:
str: `<li>` 形式的HTML。
"""
desc
=
entry
.
get
(
"description"
)
desc_html
=
f
'<p class="toc-desc">{self._escape_html(desc)}</p>'
if
desc
else
""
level
=
entry
.
get
(
"level"
,
2
)
...
...
@@ -304,7 +367,15 @@ class HTMLRenderer:
return
f
'<li class="level-{css_level}"><a href="#{self._escape_attr(entry["anchor"])}">{self._escape_html(entry["text"])}</a>{desc_html}</li>'
def
_compute_heading_labels
(
self
,
chapters
:
List
[
Dict
[
str
,
Any
]])
->
Dict
[
str
,
Dict
[
str
,
Any
]]:
"""预计算各级标题的编号(章:一、二;节:1.1;小节:1.1.1)"""
"""
预计算各级标题的编号(章:一、二;节:1.1;小节:1.1.1)。
参数:
chapters: Document IR中的章节数组。
返回:
dict: 锚点到编号/描述的映射,方便TOC与正文引用。
"""
label_map
:
Dict
[
str
,
Dict
[
str
,
Any
]]
=
{}
for
chap_idx
,
chapter
in
enumerate
(
chapters
or
[],
start
=
1
):
...
...
@@ -394,17 +465,41 @@ class HTMLRenderer:
# ====== 章节 & Block 渲染 ======
def
_render_chapter
(
self
,
chapter
:
Dict
[
str
,
Any
])
->
str
:
"""将章节blocks包裹进<section>,便于CSS控制"""
"""
将章节blocks包裹进<section>,便于CSS控制。
参数:
chapter: 单个章节JSON。
返回:
str: section包裹的HTML。
"""
section_id
=
self
.
_escape_attr
(
chapter
.
get
(
"anchor"
)
or
f
"chapter-{chapter.get('chapterId', 'x')}"
)
blocks_html
=
self
.
_render_blocks
(
chapter
.
get
(
"blocks"
,
[]))
return
f
'<section id="{section_id}" class="chapter">
\n
{blocks_html}
\n
</section>'
def
_render_blocks
(
self
,
blocks
:
List
[
Dict
[
str
,
Any
]])
->
str
:
"""顺序渲染章节内所有block"""
"""
顺序渲染章节内所有block。
参数:
blocks: 章节内部的block数组。
返回:
str: 拼接后的HTML。
"""
return
""
.
join
(
self
.
_render_block
(
block
)
for
block
in
blocks
or
[])
def
_render_block
(
self
,
block
:
Dict
[
str
,
Any
])
->
str
:
"""根据block.type分派到不同的渲染函数"""
"""
根据block.type分派到不同的渲染函数。
参数:
block: 单个block对象。
返回:
str: 渲染后的HTML,未知类型会输出JSON调试信息。
"""
block_type
=
block
.
get
(
"type"
)
handlers
=
{
"heading"
:
self
.
_render_heading
,
...
...
@@ -468,7 +563,15 @@ class HTMLRenderer:
return
f
'<{tag}{class_attr}>{items_html}</{tag}>'
def
_render_table
(
self
,
block
:
Dict
[
str
,
Any
])
->
str
:
"""渲染表格,同时保留caption与单元格属性"""
"""
渲染表格,同时保留caption与单元格属性。
参数:
block: table类型的block。
返回:
str: 包含<table>结构的HTML。
"""
rows
=
self
.
_normalize_table_rows
(
block
.
get
(
"rows"
)
or
[])
rows_html
=
""
for
row
in
rows
:
...
...
@@ -491,7 +594,15 @@ class HTMLRenderer:
return
f
'<div class="table-wrap"><table>{caption_html}<tbody>{rows_html}</tbody></table></div>'
def
_normalize_table_rows
(
self
,
rows
:
List
[
Dict
[
str
,
Any
]])
->
List
[
Dict
[
str
,
Any
]]:
"""检测并修正仅有单列的竖排表,转换为标准网格"""
"""
检测并修正仅有单列的竖排表,转换为标准网格。
参数:
rows: 原始表格行。
返回:
list[dict]: 若检测到竖排表则返回转置后的行,否则原样返回。
"""
if
not
rows
:
return
[]
if
not
all
(
len
((
row
.
get
(
"cells"
)
or
[]))
==
1
for
row
in
rows
):
...
...
@@ -611,7 +722,15 @@ class HTMLRenderer:
return
f
'<div class="figure-placeholder">{self._escape_html(caption)}</div>'
def
_render_callout
(
self
,
block
:
Dict
[
str
,
Any
])
->
str
:
"""渲染高亮提示盒,tone决定颜色"""
"""
渲染高亮提示盒,tone决定颜色。
参数:
block: callout类型的block。
返回:
str: callout HTML,若内部包含不允许的块会被拆分。
"""
tone
=
block
.
get
(
"tone"
,
"info"
)
title
=
block
.
get
(
"title"
)
safe_blocks
,
trailing_blocks
=
self
.
_split_callout_content
(
block
.
get
(
"blocks"
))
...
...
@@ -689,7 +808,15 @@ class HTMLRenderer:
return
f
'<div class="kpi-grid">{cards}</div>'
def
_render_widget
(
self
,
block
:
Dict
[
str
,
Any
])
->
str
:
"""渲染Chart.js等交互组件的占位容器,并记录配置JSON"""
"""
渲染Chart.js等交互组件的占位容器,并记录配置JSON。
参数:
block: widget类型的block,包含widgetId/props/data。
返回:
str: 含canvas与配置脚本的HTML。
"""
self
.
chart_counter
+=
1
canvas_id
=
f
"chart-{self.chart_counter}"
config_id
=
f
"chart-config-{self.chart_counter}"
...
...
@@ -830,7 +957,15 @@ class HTMLRenderer:
return
payload
def
_render_inline
(
self
,
run
:
Dict
[
str
,
Any
])
->
str
:
"""渲染单个inline run,支持多种marks叠加"""
"""
渲染单个inline run,支持多种marks叠加。
参数:
run: 含 text 与 marks 的内联节点。
返回:
str: 已包裹标签/样式的HTML片段。
"""
text_value
,
marks
=
self
.
_normalize_inline_payload
(
run
)
math_mark
=
next
((
mark
for
mark
in
marks
if
mark
.
get
(
"type"
)
==
"math"
),
None
)
if
math_mark
:
...
...
ReportEngine/utils/config.py
View file @
6d0e8f4
...
...
@@ -47,7 +47,12 @@ settings = Settings()
def
print_config
(
config
:
Settings
):
"""将当前配置项按人类可读格式输出到日志,方便排障"""
"""
将当前配置项按人类可读格式输出到日志,方便排障。
参数:
config: Settings实例,通常为全局settings。
"""
message
=
""
message
+=
"
\n
=== Report Engine 配置 ===
\n
"
message
+=
f
"LLM 模型: {config.REPORT_ENGINE_MODEL_NAME}
\n
"
...
...
templates/index.html
View file @
6d0e8f4
...
...
@@ -1081,6 +1081,7 @@
</style>
</head>
<body>
<!-- 顶层容器:同时包裹搜索区、双列主工作区与状态栏 -->
<div
class=
"container"
>
<!-- 搜索框区域 -->
<div
class=
"search-section"
>
...
...
@@ -1158,13 +1159,14 @@
</div>
</div>
<!-- 状态栏 -->
<!-- 状态栏
:实时展示WebSocket连接状态与系统时钟
-->
<div
class=
"status-bar"
>
<span
id=
"connectionStatus"
>
连接中...
</span>
<span
id=
"systemTime"
></span>
</div>
</div>
<!-- 配置弹窗:与后端.env互通,允许在线修改LLM参数 -->
<div
class=
"config-modal-overlay"
id=
"configModal"
>
<div
class=
"config-modal"
>
<div
class=
"config-modal-header"
>
...
...
@@ -1187,9 +1189,10 @@
</div>
</div>
<!-- 消息提示 -->
<!-- 消息提示
:右上角滑出式成功/错误提醒
-->
<div
class=
"message"
id=
"message"
></div>
<!-- 前端业务脚本:维护Socket连接、引擎启动状态与Report Engine交互 -->
<script>
// 全局变量
let
socket
;
...
...
Please
register
or
login
to post a comment