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-27 09:51:42 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
4e882560daf26b0880cb96d190872eb29633520b
4e882560
1 parent
23356631
Add Comments
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
192 additions
and
16 deletions
ReportEngine/nodes/chapter_generation_node.py
ReportEngine/renderers/chart_to_svg.py
ReportEngine/renderers/html_renderer.py
ReportEngine/renderers/pdf_renderer.py
ReportEngine/utils/chart_validator.py
ReportEngine/utils/dependency_check.py
regenerate_latest_html.py
regenerate_latest_pdf.py
ReportEngine/nodes/chapter_generation_node.py
View file @
4e88256
...
...
@@ -64,6 +64,7 @@ class ChapterContentError(ValueError):
narrative_characters
:
int
=
0
,
non_heading_blocks
:
int
=
0
,
):
"""保存本次异常的正文特征,供重试与兜底策略参考。"""
super
()
.
__init__
(
message
)
self
.
chapter_payload
:
Optional
[
Dict
[
str
,
Any
]]
=
chapter
self
.
body_characters
:
int
=
int
(
body_characters
or
0
)
...
...
@@ -1018,6 +1019,7 @@ class ChapterGenerationNode(BaseNode):
"""
def
walk
(
node
:
Any
)
->
int
:
"""递归遍历叙述性节点,忽略图表/目录等非正文结构"""
if
node
is
None
:
return
0
if
isinstance
(
node
,
list
):
...
...
ReportEngine/renderers/chart_to_svg.py
View file @
4e88256
...
...
@@ -797,6 +797,7 @@ class ChartToSVGConverter:
colors
=
self
.
_get_colors
(
datasets
)
def
_safe_radius
(
raw
)
->
float
:
"""将输入半径安全转为浮点并设置最小阈值,避免气泡完全消失"""
try
:
val
=
float
(
raw
)
return
max
(
val
,
0.5
)
...
...
ReportEngine/renderers/html_renderer.py
View file @
4e88256
...
...
@@ -1764,6 +1764,7 @@ class HTMLRenderer:
)
->
str
:
"""为词云提供表格兜底,避免WordCloud渲染失败后页面空白"""
def
_collect_items
(
raw
:
Any
)
->
list
[
dict
]:
"""将多种词云输入格式(数组/对象/元组/纯文本)规整为统一的词条列表"""
collected
:
list
[
dict
]
=
[]
if
isinstance
(
raw
,
list
):
for
item
in
raw
:
...
...
@@ -1812,6 +1813,7 @@ class HTMLRenderer:
return
""
def
_format_weight
(
value
:
Any
)
->
str
:
"""统一格式化权重,支持百分比/数值与字符串回退"""
if
isinstance
(
value
,
(
int
,
float
))
and
not
isinstance
(
value
,
bool
):
if
0
<=
value
<=
1.5
:
return
f
"{value * 100:.1f}
%
"
...
...
ReportEngine/renderers/pdf_renderer.py
View file @
4e88256
...
...
@@ -667,6 +667,7 @@ class PDFRenderer:
fallback_pattern
=
rf
'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
def
_hide_fallback
(
m
:
re
.
Match
)
->
str
:
"""为匹配到的图表fallback添加隐藏类,防止PDF中重复渲染"""
tag
=
m
.
group
(
0
)
if
'svg-hidden'
in
tag
:
return
tag
...
...
@@ -712,6 +713,7 @@ class PDFRenderer:
fallback_pattern
=
rf
'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
def
_hide_fallback
(
m
:
re
.
Match
)
->
str
:
"""匹配词云表格兜底并打上隐藏标记,避免SVG/图片重复显示"""
tag
=
m
.
group
(
0
)
if
'svg-hidden'
in
tag
:
return
tag
...
...
ReportEngine/utils/chart_validator.py
View file @
4e88256
...
...
@@ -87,7 +87,7 @@ class ChartValidator:
}
def
__init__
(
self
):
pass
"""初始化验证器并预留缓存结构,便于后续复用验证/修复结果"""
def
validate
(
self
,
widget_block
:
Dict
[
str
,
Any
])
->
ValidationResult
:
"""
...
...
@@ -136,6 +136,7 @@ class ChartValidator:
# 检测是否使用了{x, y}形式的数据点(通常用于时间轴/散点)
def
contains_object_points
(
ds_list
:
List
[
Any
]
|
None
)
->
bool
:
"""检查数据集中是否包含以x/y键表示的对象点,用于切换验证分支"""
if
not
isinstance
(
ds_list
,
list
):
return
False
for
point
in
ds_list
:
...
...
@@ -432,6 +433,7 @@ class ChartRepairer:
return
copy
.
deepcopy
(
cached
)
def
_cache_and_return
(
res
:
RepairResult
)
->
RepairResult
:
"""写入修复结果缓存并返回,避免重复调用下游修复逻辑"""
try
:
self
.
_result_cache
[
cache_key
]
=
copy
.
deepcopy
(
res
)
except
Exception
:
...
...
ReportEngine/utils/dependency_check.py
View file @
4e88256
...
...
@@ -27,6 +27,7 @@ def _get_platform_specific_instructions():
system
=
platform
.
system
()
def
_box_lines
(
lines
):
"""批量将多行文本包装成带边框的提示块"""
return
""
.
join
(
_box_line
(
line
)
for
line
in
lines
)
if
system
==
"Darwin"
:
# macOS
...
...
@@ -107,6 +108,7 @@ def _ensure_windows_gtk_paths():
seen
=
set
()
def
_add_candidate
(
path_like
):
"""收集可能的GTK安装路径,避免重复并兼容用户自定义目录"""
if
not
path_like
:
return
p
=
Path
(
path_like
)
...
...
regenerate_latest_html.py
View file @
4e88256
...
...
@@ -18,7 +18,19 @@ from ReportEngine.utils.config import settings
def
find_latest_run_dir
(
chapter_root
:
Path
):
"""定位包含 manifest.json 的最新章节输出目录。"""
"""
定位章节根目录下最新一次运行的输出目录。
扫描 `chapter_root` 下所有子目录,筛选出包含 `manifest.json`
的候选,按修改时间倒序取最新一条。若目录不存在或没有有效
manifest,会记录错误并返回 None。
参数:
chapter_root: 章节输出的根目录(通常是 settings.CHAPTER_OUTPUT_DIR)
返回:
Path | None: 最新的 run 目录路径;若未找到则为 None。
"""
if
not
chapter_root
.
exists
():
logger
.
error
(
f
"章节目录不存在: {chapter_root}"
)
return
None
...
...
@@ -41,7 +53,18 @@ def find_latest_run_dir(chapter_root: Path):
def
load_manifest
(
run_dir
:
Path
):
"""读取manifest.json并返回report_id与metadata。"""
"""
读取单次运行目录内的 manifest.json。
成功时返回 reportId 以及元数据字典;读取或解析失败会记录错误
并返回 (None, None),以便上层提前终止流程。
参数:
run_dir: 包含 manifest.json 的章节输出目录
返回:
tuple[str | None, dict | None]: (report_id, metadata)
"""
manifest_path
=
run_dir
/
"manifest.json"
try
:
with
manifest_path
.
open
(
"r"
,
encoding
=
"utf-8"
)
as
f
:
...
...
@@ -58,7 +81,18 @@ def load_manifest(run_dir: Path):
def
load_chapters
(
run_dir
:
Path
):
"""加载章节JSON列表。"""
"""
读取指定 run 目录下的所有章节 JSON。
会复用 ChapterStorage 的 load_chapters 能力,自动按 order 排序。
读取后打印章节数量,便于确认完整性。
参数:
run_dir: 单次报告的章节目录
返回:
list[dict]: 章节 JSON 列表(若目录为空则为空列表)
"""
storage
=
ChapterStorage
(
settings
.
CHAPTER_OUTPUT_DIR
)
chapters
=
storage
.
load_chapters
(
run_dir
)
logger
.
info
(
f
"加载章节数: {len(chapters)}"
)
...
...
@@ -66,7 +100,15 @@ def load_chapters(run_dir: Path):
def
validate_chapters
(
chapters
):
"""使用IRValidator做快速校验,仅记录警告不阻断流程。"""
"""
使用 IRValidator 对章节结构做快速校验。
仅记录未通过的章节及前三条错误,不会中断流程;目的是在
重装订前发现潜在结构问题。
参数:
chapters: 章节 JSON 列表
"""
validator
=
IRValidator
()
invalid
=
[]
for
chapter
in
chapters
:
...
...
@@ -84,7 +126,20 @@ def validate_chapters(chapters):
def
stitch_document
(
report_id
,
metadata
,
chapters
):
"""将章节装订为整本Document IR。"""
"""
将各章节与元数据装订为完整的 Document IR。
使用 DocumentComposer 统一处理章节顺序、全局元数据等,并打印
装订完成的章节与图表数量。
参数:
report_id: 报告 ID(来自 manifest 或目录名)
metadata: manifest 中的全局元数据
chapters: 已加载的章节列表
返回:
dict: 完整的 Document IR 对象
"""
composer
=
DocumentComposer
()
document_ir
=
composer
.
build_document
(
report_id
,
metadata
,
chapters
)
logger
.
info
(
...
...
@@ -95,7 +150,18 @@ def stitch_document(report_id, metadata, chapters):
def
count_charts
(
document_ir
):
"""统计IR中的图表数量。"""
"""
统计整本 Document IR 中的 Chart.js 图表数量。
会遍历每章的 blocks,递归查找 widget 类型中以 `chart.js`
开头的组件,便于快速感知图表规模。
参数:
document_ir: 完整的 Document IR
返回:
int: 图表总数
"""
chart_count
=
0
for
chapter
in
document_ir
.
get
(
"chapters"
,
[]):
blocks
=
chapter
.
get
(
"blocks"
,
[])
...
...
@@ -104,7 +170,17 @@ def count_charts(document_ir):
def
_count_chart_blocks
(
blocks
):
"""递归统计chart.js组件。"""
"""
递归统计 block 列表中的 Chart.js 组件数量。
兼容嵌套的 blocks/list/table 结构,确保所有层级的图表都被计入。
参数:
blocks: 任意层级的 block 列表
返回:
int: 统计到的 chart.js 图表数量
"""
count
=
0
for
block
in
blocks
:
if
not
isinstance
(
block
,
dict
):
...
...
@@ -129,7 +205,20 @@ def _count_chart_blocks(blocks):
def
save_document_ir
(
document_ir
,
base_name
,
timestamp
):
"""将装订好的IR重新落盘,便于后续复用。"""
"""
将重新装订好的整本 Document IR 落盘。
按 `report_ir_{slug}_{timestamp}_regen.json` 命名写入
`settings.DOCUMENT_IR_OUTPUT_DIR`,确保目录存在并返回保存路径。
参数:
document_ir: 已装订完成的整本 IR
base_name: 由主题/标题生成的安全文件名片段
timestamp: 时间戳字符串,用于区分多次重生成
返回:
Path: 保存的 IR 文件路径
"""
output_dir
=
Path
(
settings
.
DOCUMENT_IR_OUTPUT_DIR
)
output_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
ir_filename
=
f
"report_ir_{base_name}_{timestamp}_regen.json"
...
...
@@ -140,7 +229,20 @@ def save_document_ir(document_ir, base_name, timestamp):
def
render_html
(
document_ir
,
base_name
,
timestamp
):
"""使用HTMLRenderer渲染并落盘HTML文件。"""
"""
使用 HTMLRenderer 将 Document IR 渲染为 HTML 并保存。
渲染后落盘到 `final_reports/html`,打印图表验证统计信息,方便
观察 Chart.js 数据的修复/失败情况。
参数:
document_ir: 装订完成的整本 IR
base_name: 文件名片段(来源于报告主题/标题)
timestamp: 时间戳字符串
返回:
Path: 生成的 HTML 文件路径
"""
renderer
=
HTMLRenderer
()
html_content
=
renderer
.
render
(
document_ir
)
...
...
@@ -163,7 +265,18 @@ def render_html(document_ir, base_name, timestamp):
def
build_slug
(
text
):
"""将主题/标题转换为安全的文件名片段。"""
"""
将主题/标题转换为文件系统安全的片段。
仅保留字母/数字/空格/下划线/连字符,空格统一为下划线,并限制
最长 60 字符,避免过长文件名。
参数:
text: 原始主题或标题
返回:
str: 清洗后的安全字符串
"""
text
=
str
(
text
or
"report"
)
sanitized
=
""
.
join
(
c
for
c
in
text
if
c
.
isalnum
()
or
c
in
(
" "
,
"-"
,
"_"
))
.
strip
()
sanitized
=
sanitized
.
replace
(
" "
,
"_"
)
...
...
@@ -171,7 +284,18 @@ def build_slug(text):
def
main
():
"""主入口:装订最新章节并渲染HTML。"""
"""
主入口:读取最新章节、装订 IR 并渲染 HTML。
流程:
1) 找到最新的章节 run 目录并读取 manifest;
2) 加载章节并执行结构校验(仅警告);
3) 装订整本 IR,保存 IR 副本;
4) 渲染 HTML 并输出路径与统计信息。
返回:
int: 0 表示成功,其余表示失败。
"""
logger
.
info
(
"🚀 使用最新的LLM章节重新装订并渲染HTML"
)
chapter_root
=
Path
(
settings
.
CHAPTER_OUTPUT_DIR
)
...
...
regenerate_latest_pdf.py
View file @
4e88256
...
...
@@ -14,7 +14,14 @@ sys.path.insert(0, str(Path(__file__).parent))
from
ReportEngine.renderers
import
PDFRenderer
def
find_latest_report
():
"""找到最新的报告IR文件"""
"""
在 `final_reports/ir` 中查找最新的报告 IR JSON。
按修改时间倒序选择第一条,若目录或文件缺失则记录错误并返回 None。
返回:
Path | None: 最新 IR 文件路径;未找到则为 None。
"""
ir_dir
=
Path
(
"final_reports/ir"
)
if
not
ir_dir
.
exists
():
...
...
@@ -34,7 +41,18 @@ def find_latest_report():
return
latest_file
def
load_document_ir
(
file_path
):
"""加载Document IR"""
"""
读取指定路径的 Document IR JSON,并统计章节/图表数量。
解析失败时返回 None;成功时会打印章节数与图表数,便于确认
输入报告的规模。
参数:
file_path: IR 文件路径
返回:
dict | None: 解析后的 Document IR;失败返回 None。
"""
try
:
with
open
(
file_path
,
'r'
,
encoding
=
'utf-8'
)
as
f
:
document_ir
=
json
.
load
(
f
)
...
...
@@ -46,6 +64,7 @@ def load_document_ir(file_path):
chapters
=
document_ir
.
get
(
'chapters'
,
[])
def
count_charts
(
blocks
):
"""递归统计 block 列表中的 Chart.js 图表数量"""
count
=
0
for
block
in
blocks
:
if
isinstance
(
block
,
dict
):
...
...
@@ -70,7 +89,18 @@ def load_document_ir(file_path):
return
None
def
generate_pdf_with_vector_charts
(
document_ir
,
output_path
):
"""使用SVG矢量图表生成PDF"""
"""
使用 PDFRenderer 将 Document IR 渲染为包含 SVG 矢量图表的 PDF。
启用布局优化,生成后输出文件大小与成功提示;异常时返回 None。
参数:
document_ir: 完整的 Document IR
output_path: 目标 PDF 路径
返回:
Path | None: 成功时返回生成的 PDF 路径,失败返回 None。
"""
try
:
logger
.
info
(
"="
*
60
)
logger
.
info
(
"开始生成PDF(带矢量图表)"
)
...
...
@@ -102,7 +132,18 @@ def generate_pdf_with_vector_charts(document_ir, output_path):
return
None
def
main
():
"""主函数"""
"""
主入口:重新生成最新报告的矢量 PDF。
步骤:
1) 查找最新 IR 文件;
2) 读取并统计报告结构;
3) 构造输出文件名并确保目录存在;
4) 调用渲染函数生成 PDF,输出路径与特性说明。
返回:
int: 0 表示成功,非 0 表示失败。
"""
logger
.
info
(
"🚀 使用SVG矢量图表重新生成最新报告的PDF"
)
logger
.
info
(
""
)
...
...
Please
register
or
login
to post a comment