feat: add validation and toc pipeline upgrades

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
sladro 2025-11-19 10:11:21 +08:00
parent e028e4fa96
commit c1292fcacc
44 changed files with 1411 additions and 1418 deletions

View File

@ -1,501 +0,0 @@
{
"project_name": "111",
"source_file": "C:\\Users\\sladr\\Downloads\\test2.docx",
"success": true,
"technical_count": 6,
"commercial_count": 3,
"deviation_count": 0,
"chapter_count": 7,
"execution_time": 415.01412439346313,
"warnings": [
"AI审查: 5条优化建议"
],
"bid_structure": {
"scoring_criteria": [
{
"item_name": "质保期",
"max_score": 3.0,
"description": "售后质保期2年得1分。售后质保期延长至3年得2分。售后质保期延长至4年得3分。",
"category": "after_sales",
"chapter_id": "chapter_02"
},
{
"item_name": "硬件配置",
"max_score": 8.0,
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分扣完为止。招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供否则评委不予采信",
"category": "equipment_spec",
"chapter_id": "chapter_04"
},
{
"item_name": "软件功能",
"max_score": 8.0,
"description": "应按照磋商文件功能要求实现各项软件功能完成率软件功能按每1项不满足扣1分扣完为止。",
"category": "technical_solution",
"chapter_id": "chapter_05"
},
{
"item_name": "系统架构",
"max_score": 5.0,
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明缺少1项扣1分扣完为止。",
"category": "technical_solution",
"chapter_id": "chapter_06"
},
{
"item_name": "网路安全防护",
"max_score": 5.0,
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况可以综合评估组织的网络安全防护水平缺少1项扣1分扣完为止。",
"category": "quality_safety",
"chapter_id": "chapter_07"
},
{
"item_name": "技术实力",
"max_score": 3.0,
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利每提供一个得1分共3分。提供证书原件的扫描件并加盖公章",
"category": "compliance",
"chapter_id": "chapter_08"
}
],
"deviation_items": [],
"chapters": [
{
"id": "chapter_1",
"title": "评标索引表(技术评分完全对应)",
"level": 1,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2",
"title": "设备规格 (8.0分)",
"level": 1,
"score": 8.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_2_1",
"title": "硬件配置方案",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_2_1_1",
"title": "核心硬件技术参数",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2_1_2",
"title": "设备性能指标说明",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2_1_3",
"title": "硬件兼容性分析",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2_1_4",
"title": "配置方案优势阐述",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_3",
"title": "技术方案 (13.0分)",
"level": 1,
"score": 13.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_3_1",
"title": "软件功能设计",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_3_1_1",
"title": "核心功能模块设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_1_2",
"title": "用户交互界面设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_1_3",
"title": "数据处理与分析功能",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_1_4",
"title": "系统集成与接口设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_3_2",
"title": "系统架构设计",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_3_2_1",
"title": "总体架构设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_2_2",
"title": "技术选型与框架设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_2_3",
"title": "性能与扩展性设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_4",
"title": "质量安全 (5.0分)",
"level": 1,
"score": 5.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_4_1",
"title": "网络安全防护体系",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_4_1_1",
"title": "网络安全架构设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_2",
"title": "网络安全防护措施",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_3",
"title": "网络安全监控与预警",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_4",
"title": "网络安全应急响应",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_5",
"title": "网络安全管理制度",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_5",
"title": "合规响应 (3.0分)",
"level": 1,
"score": 3.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_5_1",
"title": "技术实力",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_5_1_1",
"title": "核心技术团队介绍",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_2",
"title": "技术研发能力展示",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_3",
"title": "技术设备与工具配置",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_4",
"title": "技术成果与专利情况",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_5",
"title": "技术质量管理体系",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_6",
"title": "培训服务",
"level": 1,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_6_1",
"title": "培训计划",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_6_2",
"title": "培训内容",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_6_3",
"title": "培训方式",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7",
"title": "售后服务 (3.0分)",
"level": 1,
"score": 3.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_1",
"title": "质保期服务承诺",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_1_1",
"title": "质保期限与范围",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_1_2",
"title": "质保期内服务内容",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_1_3",
"title": "质保期响应机制",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_1_4",
"title": "质保期满后服务延续方案",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7_2",
"title": "技术支持服务",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_2_1",
"title": "技术支持团队配置",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_2_2",
"title": "技术支持响应时间",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_2_3",
"title": "技术支持服务方式",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7_3",
"title": "维护服务",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_3_1",
"title": "定期维护计划",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_3_2",
"title": "故障处理流程",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_3_3",
"title": "备品备件保障",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7_4",
"title": "客户服务保障",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_4_1",
"title": "客户服务热线",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_4_2",
"title": "客户满意度调查",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_4_3",
"title": "客户投诉处理机制",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
}
]
}
}

View File

@ -1,68 +0,0 @@
[
{
"id": 1,
"title": "质保期",
"chapter_id": "chapter_02",
"score": 3.0,
"description": "售后质保期2年得1分。售后质保期延长至3年得2分。售后质保期延长至4年得3分。",
"category": "after_sales",
"status": "pending",
"content": "",
"placeholder": "{{chapter_02_content}}"
},
{
"id": 2,
"title": "硬件配置",
"chapter_id": "chapter_04",
"score": 8.0,
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分扣完为止。招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供否则评委不予采信",
"category": "equipment_spec",
"status": "pending",
"content": "",
"placeholder": "{{chapter_04_content}}"
},
{
"id": 3,
"title": "软件功能",
"chapter_id": "chapter_05",
"score": 8.0,
"description": "应按照磋商文件功能要求实现各项软件功能完成率软件功能按每1项不满足扣1分扣完为止。",
"category": "technical_solution",
"status": "pending",
"content": "",
"placeholder": "{{chapter_05_content}}"
},
{
"id": 4,
"title": "系统架构",
"chapter_id": "chapter_06",
"score": 5.0,
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明缺少1项扣1分扣完为止。",
"category": "technical_solution",
"status": "pending",
"content": "",
"placeholder": "{{chapter_06_content}}"
},
{
"id": 5,
"title": "网路安全防护",
"chapter_id": "chapter_07",
"score": 5.0,
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况可以综合评估组织的网络安全防护水平缺少1项扣1分扣完为止。",
"category": "quality_safety",
"status": "pending",
"content": "",
"placeholder": "{{chapter_07_content}}"
},
{
"id": 6,
"title": "技术实力",
"chapter_id": "chapter_08",
"score": 3.0,
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利每提供一个得1分共3分。提供证书原件的扫描件并加盖公章",
"category": "compliance",
"status": "pending",
"content": "",
"placeholder": "{{chapter_08_content}}"
}
]

View File

@ -1,501 +0,0 @@
{
"project_name": "111",
"source_file": "C:\\Users\\sladr\\Downloads\\test2.docx",
"success": true,
"technical_count": 6,
"commercial_count": 3,
"deviation_count": 0,
"chapter_count": 7,
"execution_time": 415.01412439346313,
"warnings": [
"AI审查: 5条优化建议"
],
"bid_structure": {
"scoring_criteria": [
{
"item_name": "质保期",
"max_score": 3.0,
"description": "售后质保期2年得1分。售后质保期延长至3年得2分。售后质保期延长至4年得3分。",
"category": "after_sales",
"chapter_id": "chapter_02"
},
{
"item_name": "硬件配置",
"max_score": 8.0,
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分扣完为止。招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供否则评委不予采信",
"category": "equipment_spec",
"chapter_id": "chapter_04"
},
{
"item_name": "软件功能",
"max_score": 8.0,
"description": "应按照磋商文件功能要求实现各项软件功能完成率软件功能按每1项不满足扣1分扣完为止。",
"category": "technical_solution",
"chapter_id": "chapter_05"
},
{
"item_name": "系统架构",
"max_score": 5.0,
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明缺少1项扣1分扣完为止。",
"category": "technical_solution",
"chapter_id": "chapter_06"
},
{
"item_name": "网路安全防护",
"max_score": 5.0,
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况可以综合评估组织的网络安全防护水平缺少1项扣1分扣完为止。",
"category": "quality_safety",
"chapter_id": "chapter_07"
},
{
"item_name": "技术实力",
"max_score": 3.0,
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利每提供一个得1分共3分。提供证书原件的扫描件并加盖公章",
"category": "compliance",
"chapter_id": "chapter_08"
}
],
"deviation_items": [],
"chapters": [
{
"id": "chapter_1",
"title": "评标索引表(技术评分完全对应)",
"level": 1,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2",
"title": "设备规格 (8.0分)",
"level": 1,
"score": 8.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_2_1",
"title": "硬件配置方案",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_2_1_1",
"title": "核心硬件技术参数",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2_1_2",
"title": "设备性能指标说明",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2_1_3",
"title": "硬件兼容性分析",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_2_1_4",
"title": "配置方案优势阐述",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_3",
"title": "技术方案 (13.0分)",
"level": 1,
"score": 13.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_3_1",
"title": "软件功能设计",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_3_1_1",
"title": "核心功能模块设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_1_2",
"title": "用户交互界面设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_1_3",
"title": "数据处理与分析功能",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_1_4",
"title": "系统集成与接口设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_3_2",
"title": "系统架构设计",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_3_2_1",
"title": "总体架构设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_2_2",
"title": "技术选型与框架设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_3_2_3",
"title": "性能与扩展性设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_4",
"title": "质量安全 (5.0分)",
"level": 1,
"score": 5.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_4_1",
"title": "网络安全防护体系",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_4_1_1",
"title": "网络安全架构设计",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_2",
"title": "网络安全防护措施",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_3",
"title": "网络安全监控与预警",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_4",
"title": "网络安全应急响应",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_4_1_5",
"title": "网络安全管理制度",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_5",
"title": "合规响应 (3.0分)",
"level": 1,
"score": 3.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_5_1",
"title": "技术实力",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_5_1_1",
"title": "核心技术团队介绍",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_2",
"title": "技术研发能力展示",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_3",
"title": "技术设备与工具配置",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_4",
"title": "技术成果与专利情况",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_5_1_5",
"title": "技术质量管理体系",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
},
{
"id": "chapter_6",
"title": "培训服务",
"level": 1,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_6_1",
"title": "培训计划",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_6_2",
"title": "培训内容",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_6_3",
"title": "培训方式",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7",
"title": "售后服务 (3.0分)",
"level": 1,
"score": 3.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_1",
"title": "质保期服务承诺",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_1_1",
"title": "质保期限与范围",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_1_2",
"title": "质保期内服务内容",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_1_3",
"title": "质保期响应机制",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_1_4",
"title": "质保期满后服务延续方案",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7_2",
"title": "技术支持服务",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_2_1",
"title": "技术支持团队配置",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_2_2",
"title": "技术支持响应时间",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_2_3",
"title": "技术支持服务方式",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7_3",
"title": "维护服务",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_3_1",
"title": "定期维护计划",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_3_2",
"title": "故障处理流程",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_3_3",
"title": "备品备件保障",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
},
{
"id": "chapter_7_4",
"title": "客户服务保障",
"level": 2,
"score": 0.0,
"template_placeholder": null,
"children": [
{
"id": "chapter_7_4_1",
"title": "客户服务热线",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_4_2",
"title": "客户满意度调查",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
},
{
"id": "chapter_7_4_3",
"title": "客户投诉处理机制",
"level": 3,
"score": 0.0,
"template_placeholder": null,
"children": []
}
]
}
]
}
]
}
}

View File

@ -1,68 +0,0 @@
[
{
"id": 1,
"title": "质保期",
"chapter_id": "chapter_02",
"score": 3.0,
"description": "售后质保期2年得1分。售后质保期延长至3年得2分。售后质保期延长至4年得3分。",
"category": "after_sales",
"status": "pending",
"content": "",
"placeholder": "{{chapter_02_content}}"
},
{
"id": 2,
"title": "硬件配置",
"chapter_id": "chapter_04",
"score": 8.0,
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分扣完为止。招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供否则评委不予采信",
"category": "equipment_spec",
"status": "pending",
"content": "",
"placeholder": "{{chapter_04_content}}"
},
{
"id": 3,
"title": "软件功能",
"chapter_id": "chapter_05",
"score": 8.0,
"description": "应按照磋商文件功能要求实现各项软件功能完成率软件功能按每1项不满足扣1分扣完为止。",
"category": "technical_solution",
"status": "pending",
"content": "",
"placeholder": "{{chapter_05_content}}"
},
{
"id": 4,
"title": "系统架构",
"chapter_id": "chapter_06",
"score": 5.0,
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明缺少1项扣1分扣完为止。",
"category": "technical_solution",
"status": "pending",
"content": "",
"placeholder": "{{chapter_06_content}}"
},
{
"id": 5,
"title": "网路安全防护",
"chapter_id": "chapter_07",
"score": 5.0,
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况可以综合评估组织的网络安全防护水平缺少1项扣1分扣完为止。",
"category": "quality_safety",
"status": "pending",
"content": "",
"placeholder": "{{chapter_07_content}}"
},
{
"id": 6,
"title": "技术实力",
"chapter_id": "chapter_08",
"score": 3.0,
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利每提供一个得1分共3分。提供证书原件的扫描件并加盖公章",
"category": "compliance",
"status": "pending",
"content": "",
"placeholder": "{{chapter_08_content}}"
}
]

View File

@ -286,6 +286,9 @@ content_prompts:
上下文参考:
{context_summary}
重点提示:
{guidance_part}
评分说明:
{requirements_summary}{emphasis_part}{rag_part}

View File

@ -1,128 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个基于Three.js的3D模型查看器和船厂三维数据管理展示系统。项目包含两个主要页面
- `index.html`: 主要的3D模型查看器界面支持模型加载、选择、测量、标注等功能
- `1.html`: 船舶建造精度数字化平台的数据分析界面通过iframe嵌入3D查看器
## 技术架构
### 前端技术栈
- **Three.js**: 核心3D渲染引擎
- **原生JavaScript**: ES6+ 模块化架构,无框架依赖
- **HTML5/CSS3**: 响应式布局使用Flexbox和Grid
- **Tailwind CSS**: 样式框架仅在1.html中使用
- **ECharts**: 数据可视化图表仅在1.html中使用
### 核心模块结构
所有JavaScript模块位于 `js/` 目录下,采用模块化设计:
#### 核心管理器
- **ModelViewer** (`main.js`): 主应用入口管理整个3D场景
- **ModelManager** (`ModelManager.js`): 模型管理,处理模型的加载和切换
- **ModelLoader** (`ModelLoader.js`): 模型加载器支持GLTF格式
- **ModelAPI** (`ModelAPI.js`): 模型API接口提供程序化控制
#### 功能管理器
- **SelectionManager** (`SelectionManager.js`): 零件选择和高亮
- **MeasurementManager** (`MeasurementManager.js`): 3D测量工具距离、角度、面积
- **ClippingManager** (`ClippingManager.js`): 切面控制
- **AnnotationManager** (`AnnotationManager.js`): 3D标注功能
- **ModelTreeManager** (`ModelTreeManager.js`): 模型结构树管理
- **AnomalyDisplayManager** (`AnomalyDisplayManager.js`): 异常状态显示
- **LayerDisplayManager** (`LayerDisplayManager.js`): 分层显示管理
#### UI和状态管理
- **UIController** (`UIController.js`): UI控制器管理所有界面交互
- **ObjectStateManager** (`ObjectStateManager.js`): 对象状态管理
- **StorageManager** (`StorageManager.js`): 本地存储管理
- **OutlineManager** (`OutlineManager.js`): 边缘高亮效果
### 资源结构
- **models/**: 3D模型文件.glb格式
- **css/**: 样式文件主要是style.css
- **image/**: UI图标和图片资源
- **three/**: Three.js库文件
## 开发环境
### 启动开发服务器
由于项目使用ES6模块和本地文件引用需要通过HTTP服务器运行
```bash
# 使用Live Server推荐
# VS Code安装Live Server扩展配置端口5501
# 右键点击index.html选择"Open with Live Server"
# 或使用Python内置服务器
python -m http.server 5501
# 或使用Node.js http-server
npx http-server -p 5501
```
### 访问页面
- 3D模型查看器: `http://localhost:5501/index.html`
- 数据分析平台: `http://localhost:5501/1.html`
## 开发约定
### 模型文件管理
- 支持的格式: .glb (推荐)
- 模型文件存放在 `models/` 目录
- 默认加载模型: `CX0856_CB01C.glb`
### 配置和状态管理
- 使用 `StorageManager` 进行本地存储
- 配置项通过 `ObjectStateManager` 管理
- 模型状态通过事件系统同步
### 代码风格
- 使用ES6+语法和模块
- 采用面向对象设计模式
- 每个功能模块独立文件
- 使用驼峰命名法
- 保持代码注释的完整性
### 事件通信
项目使用自定义事件系统进行模块间通信:
- 模型加载完成事件
- 选择状态变化事件
- UI状态更新事件
- 测量结果事件
### 浏览器兼容性
- 支持现代浏览器Chrome 88+, Firefox 85+, Safari 14+
- 依赖ES6模块支持
- 需要WebGL 2.0支持
## 调试和测试
### 控制台调试
- 使用 `window.modelViewer` 访问主应用实例
- 各管理器实例可通过modelViewer实例访问
- 开启浏览器开发者工具查看Three.js性能面板
### 性能优化
- 大模型文件建议压缩优化
- 使用LOD (Level of Detail) 技术
- 合理使用材质和纹理
- 避免过度的实时计算
## 常见问题
### CORS错误
确保通过HTTP服务器访问而不是直接打开HTML文件
### 模型加载失败
检查模型文件路径和格式,确保.glb文件完整性
### Three.js版本兼容
项目基于Three.js r150+更新版本时注意API变更
### 内存泄漏
及时清理不需要的几何体、材质和纹理对象

View File

@ -1,20 +1,18 @@
"""BidMaster-CLI 主包 - 统一导出接口
按照规范要求导出三个核心工具函数
"""
__all__ = ["parse", "generate", "assemble"]
# CLI入口
def main():
"""CLI主入口函数"""
from .cli.main import cli
cli()
"""BidMaster-CLI 主包 - 统一导出接口"""
from .tools.parser import BidParser
from .tools.rag import RAGTool
from .tools.table import TableGenerator
__all__ = ["parse", "generate", "assemble"]
def main():
"""CLI主入口函数"""
from .cli.main import cli
cli()
# 创建工具函数的便捷接口
def parse(file_path: str, **kwargs):
"""解析招标文件

View File

@ -7,6 +7,7 @@
"""
import logging
from enum import Enum
from pathlib import Path
from typing import List, Dict, Any, TypedDict
import asyncio
@ -14,7 +15,15 @@ import asyncio
from langgraph.graph import StateGraph, END
from pydantic import BaseModel, Field
from ..tools.parser import BidParser, BidStructure, ScoringCriteria, DeviationItem, DocumentChapter
from ..tools.parser import (
BidParser,
BidStructure,
ScoringCriteria,
DeviationItem,
DocumentChapter,
TechnicalCategory,
ChapterGuidance,
)
from ..config import get_settings
from ..nodes.toc.base_mixins import WorkflowUtilsMixin
from .base import BaseAgentFactory
@ -39,12 +48,30 @@ def _validate_template_file(file_path: str) -> bool:
return False
class SectionScope(str, Enum):
"""章节范围配置"""
TECHNICAL_ONLY = "technical_only"
FULL = "full"
TECHNICAL_SCOPE_WHITELIST = {
TechnicalCategory.TECHNICAL_SOLUTION,
TechnicalCategory.EQUIPMENT_SPEC,
TechnicalCategory.IMPLEMENTATION,
TechnicalCategory.QUALITY_SAFETY,
TechnicalCategory.AFTER_SALES,
TechnicalCategory.COMPLIANCE,
}
class AnalysisAgentState(TypedDict):
"""Analysis Agent的状态定义"""
# 输入参数
source_file: str
interaction_handler: Any # InteractionHandler实例
section_scope: str # 技术章节范围
# 执行状态
current_step: str
@ -67,6 +94,7 @@ class AnalysisAgentState(TypedDict):
pending_suggestions: List[Dict[str, Any]] # 待处理的AI建议
auto_optimization_rounds: int # 自动优化迭代次数
auto_toc_max_rounds: int # 最大自动优化次数
chapter_guidance_map: Dict[str, ChapterGuidance]
# 最终输出
bid_structure: BidStructure | None
@ -88,6 +116,7 @@ class AnalysisResult(BaseModel):
error_message: str | None = Field(default=None, description="错误信息")
warnings: List[str] = Field(default_factory=list, description="警告信息")
execution_time: float = Field(default=0.0, description="执行时间(秒)")
section_scope: str = Field(default=SectionScope.TECHNICAL_ONLY.value, description="章节范围")
# ========== LangGraph 节点函数 ==========
@ -233,6 +262,10 @@ def parse_content_node(state: AnalysisAgentState) -> AnalysisAgentState:
technical_criteria = []
commercial_criteria = []
deviation_items = []
try:
section_scope = SectionScope(state.get("section_scope", SectionScope.TECHNICAL_ONLY.value))
except ValueError:
section_scope = SectionScope.TECHNICAL_ONLY
# 解析评分表
scoring_tables = state["classified_tables"].get("scoring", [])
@ -258,7 +291,14 @@ def parse_content_node(state: AnalysisAgentState) -> AnalysisAgentState:
logger.info(f"内容解析完成: 技术项{len(technical_criteria)}个, 商务项{len(commercial_criteria)}个, 偏离项{len(deviation_items)}")
state["technical_criteria"] = technical_criteria
if section_scope == SectionScope.FULL:
scoped_criteria = technical_criteria + commercial_criteria
else:
scoped_criteria = [c for c in technical_criteria if c.category in TECHNICAL_SCOPE_WHITELIST]
if not scoped_criteria:
state.setdefault("warnings", []).append("技术范围过滤后未找到可用的评分项")
state["technical_criteria"] = scoped_criteria
state["commercial_criteria"] = commercial_criteria
state["deviation_items"] = deviation_items
state["current_step"] = "parse_content"
@ -401,9 +441,13 @@ def finalize_structure_node(state: AnalysisAgentState) -> AnalysisAgentState:
class AnalysisAgent(BaseAgentFactory):
"""Analysis Agent - 第一阶段分析Agent"""
def __init__(self, interaction_handler=None):
def __init__(self, interaction_handler=None, section_scope: str = SectionScope.TECHNICAL_ONLY.value):
self.settings = get_settings()
self.interaction_handler = interaction_handler
try:
self.section_scope = SectionScope(section_scope).value
except ValueError:
self.section_scope = SectionScope.TECHNICAL_ONLY.value
self.graph = self._build_graph()
def _build_graph(self) -> StateGraph:
@ -482,6 +526,7 @@ class AnalysisAgent(BaseAgentFactory):
initial_state = AnalysisAgentState(
source_file=source_file,
interaction_handler=self.interaction_handler,
section_scope=self.section_scope,
current_step="",
progress=0.0,
should_continue=True,
@ -498,6 +543,7 @@ class AnalysisAgent(BaseAgentFactory):
pending_suggestions=[],
auto_optimization_rounds=0,
auto_toc_max_rounds=self.settings.auto_toc_max_rounds,
chapter_guidance_map={},
bid_structure=None,
error="",
warnings=[]
@ -516,7 +562,8 @@ class AnalysisAgent(BaseAgentFactory):
success=False,
error_message=final_state["error"],
warnings=final_state.get("warnings", []),
execution_time=time.time() - start_time
execution_time=time.time() - start_time,
section_scope=final_state.get("section_scope", self.section_scope)
)
else:
bid_structure = final_state["bid_structure"]
@ -528,7 +575,8 @@ class AnalysisAgent(BaseAgentFactory):
deviation_count=len(final_state.get("deviation_items", [])),
chapter_count=len(bid_structure.chapters) if bid_structure else 0,
warnings=final_state.get("warnings", []),
execution_time=time.time() - start_time
execution_time=time.time() - start_time,
section_scope=final_state.get("section_scope", self.section_scope)
)
logger.info(f"Analysis Agent执行完成耗时{result.execution_time:.2f}")
@ -546,4 +594,25 @@ class AnalysisAgent(BaseAgentFactory):
"""同步执行接口用于CLI调用"""
return asyncio.run(self.execute(source_file))
@classmethod
def create_silent(cls, section_scope: str = SectionScope.TECHNICAL_ONLY.value):
from .interaction import InteractionHandler, InteractionMode
handler = InteractionHandler(mode=InteractionMode.SILENT)
return cls(handler, section_scope=section_scope)
@classmethod
def create_programmatic(cls, presets: Dict[str, Any], section_scope: str = SectionScope.TECHNICAL_ONLY.value):
from .interaction import InteractionHandler, InteractionMode
handler = InteractionHandler(mode=InteractionMode.PROGRAMMATIC, presets=presets)
return cls(handler, section_scope=section_scope)
@classmethod
def create_interactive(cls, section_scope: str = SectionScope.TECHNICAL_ONLY.value):
from .interaction import InteractionHandler, InteractionMode
handler = InteractionHandler(mode=InteractionMode.INTERACTIVE)
return cls(handler, section_scope=section_scope)

View File

@ -4,9 +4,8 @@
"""
import logging
from typing import Dict, Any, Optional, Callable
from typing import Dict, Any, Optional
from langgraph.graph import END
from ..base import AgentBuilder, BaseAgent, BaseAgentFactory
from ...nodes.toc import (

View File

@ -5,7 +5,7 @@
import logging
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any, Dict
from .single_chapter_agent import SingleChapterAgent

View File

@ -283,7 +283,7 @@ class InteractionHandler:
if "pattern" in validation:
import re
if not re.match(validation["pattern"], text):
return f"文本格式不符合要求"
return "文本格式不符合要求"
return None

View File

@ -5,6 +5,7 @@
import logging
from pathlib import Path
from typing import Any, Dict
import click
from rich.console import Console
@ -135,7 +136,11 @@ def parse(scoring_file: str, deviation_file: str | None, template_file: str | No
@click.option("--presets", "-p",
type=str,
help="程序化模式的预设配置JSON字符串")
def new(mode: str, presets: str):
@click.option("--section-scope",
type=click.Choice(['technical_only', 'full']),
default='technical_only',
help="章节范围technical_only只保留技术部分full包含全部评分项")
def new(mode: str, presets: str, section_scope: str):
"""核心命令解析招标文件生成任务清单和Word框架"""
try:
console.print("🎯 BidMaster 标书项目创建向导", style="bold blue")
@ -177,7 +182,7 @@ def new(mode: str, presets: str):
# 根据模式创建AnalysisAgent
if mode == 'silent':
analysis_agent = AnalysisAgent.create_silent()
analysis_agent = AnalysisAgent.create_silent(section_scope=section_scope)
console.print("📌 使用静默模式(自动选择默认值)", style="dim")
elif mode == 'programmatic':
if not presets:
@ -185,13 +190,13 @@ def new(mode: str, presets: str):
return
try:
preset_dict = json.loads(presets)
analysis_agent = AnalysisAgent.create_programmatic(preset_dict)
analysis_agent = AnalysisAgent.create_programmatic(preset_dict, section_scope=section_scope)
console.print(f"📌 使用程序化模式,预设配置: {list(preset_dict.keys())}", style="dim")
except json.JSONDecodeError:
console.print("❌ 预设配置JSON格式错误", style="red")
return
else: # interactive
analysis_agent = AnalysisAgent.create_interactive()
analysis_agent = AnalysisAgent.create_interactive(section_scope=section_scope)
console.print("📌 使用交互模式", style="dim")
# 执行完整的分析流程(包括目录生成)
@ -214,6 +219,7 @@ def new(mode: str, presets: str):
data = {
"project_name": project_name,
"source_file": str(Path(bidding_file).absolute()),
"section_scope": result.section_scope,
"success": result.success,
"technical_count": result.technical_count,
"commercial_count": result.commercial_count,
@ -249,6 +255,17 @@ def new(mode: str, presets: str):
tasks = []
task_id = 1
guidance_map: Dict[str, Any] = {}
def _collect_guidance(chapter_obj):
if chapter_obj.guidance:
guidance_map[chapter_obj.id] = chapter_obj.guidance.dict()
for child in chapter_obj.children:
_collect_guidance(child)
for chapter in bid_structure.chapters:
_collect_guidance(chapter)
# 为每个评分项创建任务
for criteria in bid_structure.scoring_criteria:
tasks.append({
@ -260,7 +277,8 @@ def new(mode: str, presets: str):
"category": criteria.category.value,
"status": "pending",
"content": "",
"placeholder": f"{{{{{criteria.chapter_id}_content}}}}"
"placeholder": f"{{{{{criteria.chapter_id}_content}}}}",
"guidance": guidance_map.get(criteria.chapter_id)
})
task_id += 1
@ -338,10 +356,11 @@ def new(mode: str, presets: str):
console.print(f"📚 生成章节: {result.chapter_count}")
console.print(f"📝 生成任务: {len(tasks)}")
console.print(f"⏱️ 处理耗时: {result.execution_time:.2f}")
console.print(f"⚙️ 目录范围: {result.section_scope}")
# 显示警告信息(如果有)
if result.warnings:
console.print(f"\n⚠️ 警告信息:", style="yellow")
console.print("\n⚠️ 警告信息:", style="yellow")
for warning in result.warnings:
console.print(f"{warning}", style="yellow")
@ -356,9 +375,9 @@ def new(mode: str, presets: str):
console.print("\n📚 项目目录结构:")
_display_chapters_recursive(bid_structure.chapters)
console.print(f"\n🎯 下一步:", style="dim")
console.print("\n🎯 下一步:", style="dim")
console.print(f" • 打开 {template_file.name} 开始编写标书", style="dim")
console.print(f" • 使用 'generate task <id>' 为具体任务生成内容", style="dim")
console.print(" • 使用 'generate task <id>' 为具体任务生成内容", style="dim")
except Exception as e:
console.print(f"❌ 项目创建失败: {e}", style="red")
@ -372,6 +391,7 @@ def _serialize_chapter(chapter):
"level": chapter.level,
"score": chapter.score,
"template_placeholder": chapter.template_placeholder,
"guidance": chapter.guidance.dict() if getattr(chapter, "guidance", None) else None,
"children": [_serialize_chapter(child) for child in chapter.children]
}
@ -411,6 +431,7 @@ def status(project_dir: str):
console.print(f"📋 {analysis_data.get('project_name', '标书项目')}", style="bold blue")
console.print(f"📁 项目路径: {project_path.absolute()}")
console.print(f"📄 源文件: {analysis_data.get('source_file', 'N/A')}")
console.print(f"⚙️ 目录范围: {analysis_data.get('section_scope', 'technical_only')}")
# 统计信息
technical_count = len(analysis_data.get('technical_criteria', []))
@ -418,7 +439,7 @@ def status(project_dir: str):
deviation_count = len(analysis_data.get('deviation_items', []))
total_tasks = len(tasks_data)
console.print(f"\n📊 项目概况:")
console.print("\n📊 项目概况:")
console.print(f" • 技术评分项: {technical_count}")
console.print(f" • 商务评分项: {commercial_count}项(已排除)")
console.print(f" • 偏离项: {deviation_count}")
@ -430,7 +451,7 @@ def status(project_dir: str):
status = task.get('status', 'unknown')
status_count[status] = status_count.get(status, 0) + 1
console.print(f"\n✅ 任务进度:")
console.print("\n✅ 任务进度:")
status_names = {
'pending': '待处理',
'in_progress': '进行中',
@ -443,7 +464,7 @@ def status(project_dir: str):
console.print(f"{name}: {count}")
# 显示任务清单
console.print(f"\n📝 任务清单:")
console.print("\n📝 任务清单:")
task_table = Table(title="项目任务列表")
task_table.add_column("ID", justify="right", style="cyan", no_wrap=True)
task_table.add_column("标题", style="yellow")
@ -451,18 +472,19 @@ def status(project_dir: str):
task_table.add_column("状态", style="green")
for task in tasks_data[:10]: # 只显示前10个任务
status_text = task.get('status', 'pending')
status_style = {
'pending': 'yellow',
'in_progress': 'blue',
'completed': 'green',
'failed': 'red'
}.get(task.get('status'), 'white')
}.get(status_text, 'white')
task_table.add_row(
str(task['id']),
task['title'][:50] + "..." if len(task['title']) > 50 else task['title'],
f"{task['score']}",
task.get('status', 'pending')
f"[{status_style}]{status_text}[/]"
)
console.print(task_table)
@ -470,9 +492,9 @@ def status(project_dir: str):
if len(tasks_data) > 10:
console.print(f" ... 还有{len(tasks_data) - 10}个任务")
console.print(f"\n🎯 下一步操作:", style="dim")
console.print(f" • generate task <id> - 为具体任务生成内容", style="dim")
console.print(f" • generate full - 为所有待处理任务生成内容", style="dim")
console.print("\n🎯 下一步操作:", style="dim")
console.print(" • generate task <id> - 为具体任务生成内容", style="dim")
console.print(" • generate full - 为所有待处理任务生成内容", style="dim")
except Exception as e:
console.print(f"❌ 查看项目状态失败: {e}", style="red")
@ -520,16 +542,17 @@ def smart_parse(word_file: str):
console.print(f"📋 偏离项: {result.deviation_count}")
console.print(f"📚 生成章节: {result.chapter_count}")
console.print(f"⏱️ 处理耗时: {result.execution_time:.2f}")
console.print(f"⚙️ 目录范围: {result.section_scope}")
# 显示警告信息(如果有)
if result.warnings:
console.print(f"\n⚠️ 警告信息:", style="yellow")
console.print("\n⚠️ 警告信息:", style="yellow")
for warning in result.warnings:
console.print(f"{warning}", style="yellow")
# 显示技术评分标准
if bid_structure.scoring_criteria:
console.print(f"\n📊 技术评分标准详情:")
console.print("\n📊 技术评分标准详情:")
table = Table(title="技术评分标准解析结果")
table.add_column("技术类别", style="green")
table.add_column("评分项", style="yellow")

View File

@ -66,7 +66,7 @@ def start(project_dir: str, mode: str):
console.print("❌ 未找到Word文档", style="red")
return
console.print(f"📄 找到Word文档: {Path(word_file).name}", style="green")
console.print(f"📋 将直接从Word文档提取章节结构", style="dim")
console.print("📋 将直接从Word文档提取章节结构", style="dim")
# 创建Agent
if mode == "silent":
@ -111,9 +111,9 @@ def start(project_dir: str, mode: str):
)
console.print("\n", stats_panel)
console.print(f"\n🎯 下一步:", style="dim")
console.print("\n🎯 下一步:", style="dim")
console.print(f" • 打开 {Path(result['word_file']).name} 查看生成的内容", style="dim")
console.print(f" • 使用 Ctrl+A -> F9 刷新Word中的目录和页码", style="dim")
console.print(" • 使用 Ctrl+A -> F9 刷新Word中的目录和页码", style="dim")
else:
console.print("\n❌ 内容填写失败", style="red")
@ -166,7 +166,7 @@ def _find_word_file(project_path: Path) -> str:
if len(docx_files) == 1:
return str(docx_files[0])
elif len(docx_files) > 1:
console.print(f"\n⚠️ 找到多个Word文档:", style="yellow")
console.print("\n⚠️ 找到多个Word文档:", style="yellow")
for i, f in enumerate(docx_files, 1):
console.print(f" {i}. {f.name}")
console.print("\n提示:将只有一个.docx文件放在项目目录中", style="yellow")

View File

@ -7,7 +7,7 @@
"""
from pathlib import Path
from typing import Any, Literal
from typing import Any
from enum import Enum
import yaml

View File

@ -1,8 +1,3 @@
"""Internal!W - …èž° <0C>bèimport
"""Experimental internal utilities module placeholder."""
dîU+èž°Æ «èô¥(
@ lq¥ãÇ
B!W´2
"""
__all__ = [] # üúûU¹
__all__: list[str] = []

View File

@ -5,8 +5,6 @@
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any
from uuid import UUID, uuid4
from pydantic import BaseModel, Field

View File

@ -327,6 +327,7 @@ class InitConfigNode(BaseNode):
return {}
chapter_titles: Dict[str, str] = {}
chapter_guidance: Dict[str, Dict[str, Any]] = {}
def _collect_titles(nodes: List[Dict[str, Any]] | None) -> None:
if not nodes:
@ -336,6 +337,9 @@ class InitConfigNode(BaseNode):
title = node.get("title", "")
if chapter_id and title:
chapter_titles[chapter_id] = title
guidance = node.get("guidance")
if chapter_id and guidance:
chapter_guidance[chapter_id] = guidance
_collect_titles(node.get("children"))
_collect_titles(data.get("chapters", []))
@ -373,6 +377,10 @@ class InitConfigNode(BaseNode):
entry["requirements"].append(description)
entry["rubric_points"].extend(self._split_rubric_points(description))
self._merge_guidance(entry, chapter_guidance.get(chapter_id))
return self._finalize_metadata_entries(metadata)
return self._finalize_metadata_entries(metadata)
def _parse_tasks_metadata(self, file_path: Path) -> Dict[str, Dict[str, Any]]:
@ -408,6 +416,8 @@ class InitConfigNode(BaseNode):
entry["requirements"].append(description)
entry["rubric_points"].extend(self._split_rubric_points(description))
self._merge_guidance(entry, task.get("guidance"))
return self._finalize_metadata_entries(metadata)
def _split_rubric_points(self, text: str) -> List[str]:
@ -418,6 +428,44 @@ class InitConfigNode(BaseNode):
clean_parts = [p.strip().strip(":·•") for p in parts if len(p.strip()) >= 3]
return clean_parts
def _merge_guidance(self,
entry: Dict[str, Any],
guidance: Optional[Dict[str, Any]]) -> None:
if not guidance:
return
target = entry.setdefault(
"guidance",
{
"key_requirements": [],
"suggested_evidence": [],
"context_snippets": [],
}
)
for key in ("key_requirements", "suggested_evidence", "context_snippets"):
values = guidance.get(key) or []
for value in values:
if value and value not in target[key]:
target[key].append(value)
def _normalize_guidance(self, guidance: Optional[Dict[str, Any]]) -> Optional[Dict[str, List[str]]]:
if not guidance:
return None
normalized: Dict[str, List[str]] = {}
for key in ("key_requirements", "suggested_evidence", "context_snippets"):
values = guidance.get(key) or []
dedup: List[str] = []
for value in values:
clean = (value or "").strip()
if clean and clean not in dedup:
dedup.append(clean)
if dedup:
normalized[key] = dedup
return normalized or None
def _finalize_metadata_entries(self, metadata: Dict[str, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
finalized: Dict[str, Dict[str, Any]] = {}
for key, entry in metadata.items():
@ -441,6 +489,7 @@ class InitConfigNode(BaseNode):
"rubric_points": dedup_points,
"source": entry.get("source"),
"chapter_id_source": entry.get("chapter_id_source"),
"guidance": self._normalize_guidance(entry.get("guidance")),
}
return finalized
@ -473,6 +522,7 @@ class InitConfigNode(BaseNode):
"rubric_points": meta.get("rubric_points", []),
"source": meta.get("source"),
"chapter_id_source": meta.get("chapter_id_source"),
"guidance": meta.get("guidance"),
}
chapter["requirements"] = enriched["requirements"]
@ -480,6 +530,8 @@ class InitConfigNode(BaseNode):
chapter["score"] = enriched["score"]
if enriched["category"]:
chapter["category"] = enriched["category"]
if enriched.get("guidance"):
chapter["guidance"] = enriched["guidance"]
chapter_metadata[chapter["id"]] = enriched

View File

@ -128,7 +128,7 @@ class InteractWithUserNode(BaseNode):
choice = interaction_handler(
interaction_type="choice",
prompt=f"选择知识库",
prompt="选择知识库",
options=[
("default", store_info),
("none", "不使用RAG")

View File

@ -8,6 +8,7 @@ from pathlib import Path
from typing import Any, Dict
from ..base import BaseNode, NodeContext
from ...utils.validation import ContentValidationError, ContentValidator
logger = logging.getLogger(__name__)
@ -21,6 +22,9 @@ class SaveToWordNode(BaseNode):
3. 更新处理进度
"""
def __init__(self) -> None:
self._content_validator = ContentValidator()
@property
def name(self) -> str:
return "save_to_word"
@ -53,7 +57,21 @@ class SaveToWordNode(BaseNode):
if not content:
raise ValueError(f"章节 {chapter_id} 的内容未生成")
logger.info(f"开始保存章节内容到Word: {chapter_id} - {chapter['title']}")
current_index = state.get("current_chapter_index", 0)
total_chapters = len(state.get("chapter_queue", [])) or 1
logger.info(
"保存章节内容 [%s/%s]: %s - %s",
current_index + 1,
total_chapters,
chapter_id,
chapter.get("title"),
)
try:
self._content_validator.validate(chapter, content)
except ContentValidationError as exc:
logger.error("章节 %s 内容未通过合规校验: %s", chapter_id, exc)
raise ValueError(f"章节 {chapter_id} 内容未通过合规校验: {exc}") from exc
# 填充到Word文档
self._fill_word_document(word_file, chapter, content)
@ -62,9 +80,6 @@ class SaveToWordNode(BaseNode):
completed_chapters = state.get("completed_chapters", [])
completed_chapters.append(chapter_id)
current_index = state.get("current_chapter_index", 0)
total_chapters = len(state.get("chapter_queue", []))
logger.info(f"章节保存完成 [{current_index + 1}/{total_chapters}]: {chapter_id}")
# 直接在state上更新确保下次循环能读取到最新值
@ -100,8 +115,8 @@ class SaveToWordNode(BaseNode):
placeholder = f"{{{{{chapter['id']}_content}}}}"
try:
# 填充占位符
word_processor.fill_placeholder(word_path, placeholder, content)
# 填充章节内容
word_processor.fill_chapter_content(word_path, chapter, content)
logger.info(f"成功填充占位符: {placeholder}")
except Exception as e:

View File

@ -75,7 +75,7 @@ class ApplyReviewSuggestionsNode(BaseNode, TocNodeBase):
try:
# 展示AI的完整评估结果
assessment_text = f"AI审查评估:\n"
assessment_text = "AI审查评估:\n"
if overall_assessment:
assessment_text += f"总体评价: {overall_assessment}\n"
if optimization_score:

View File

@ -5,7 +5,7 @@
import logging
from abc import ABC
from typing import Dict, Any, Optional, Callable
from typing import Dict, Any, Callable
logger = logging.getLogger(__name__)

View File

@ -4,10 +4,10 @@
"""
import logging
from typing import Dict, List, Any
from typing import Dict, List
from ...tools.parser import ScoringCriteria, DocumentChapter
from .constants import CATEGORY_NAMES, CATEGORY_ORDER
from .constants import CATEGORY_NAMES, CATEGORY_ORDER, CATEGORY_TITLE_HINTS
logger = logging.getLogger(__name__)
@ -103,6 +103,27 @@ class CategoryManager:
return "_".join(parts[2:])
@staticmethod
def infer_category_for_chapter(chapter: DocumentChapter) -> str:
"""通过章节ID或标题推断类别"""
category = CategoryManager.extract_category_from_chapter_id(chapter.id)
if category:
return category
title = (chapter.title or "").lower()
if not title:
return ""
for category_key, hints in CATEGORY_TITLE_HINTS.items():
for hint in hints:
if not hint:
continue
if hint.lower() in title:
return category_key
return ""
@staticmethod
def find_corresponding_criteria(chapter: DocumentChapter,
technical_criteria: List[ScoringCriteria]) -> List[ScoringCriteria]:
@ -115,7 +136,7 @@ class CategoryManager:
Returns:
对应的评分项列表
"""
category = CategoryManager.extract_category_from_chapter_id(chapter.id)
category = CategoryManager.infer_category_for_chapter(chapter)
if not category:
return []

View File

@ -10,7 +10,8 @@ CATEGORY_NAMES = {
"equipment_spec": "设备规格",
"quality_safety": "质量安全",
"after_sales": "售后服务",
"implementation": "实施方案"
"implementation": "实施方案",
"commercial": "商务响应"
}
# 预定义类别顺序
@ -20,9 +21,21 @@ CATEGORY_ORDER = [
"implementation",
"quality_safety",
"after_sales",
"compliance"
"compliance",
"commercial"
]
# 章节标题关键字提示
CATEGORY_TITLE_HINTS = {
"technical_solution": [CATEGORY_NAMES["technical_solution"], "技术部分", "总体要求"],
"equipment_spec": [CATEGORY_NAMES["equipment_spec"], "供货", "设备"],
"implementation": [CATEGORY_NAMES["implementation"], "实施方案", "安装调试"],
"quality_safety": [CATEGORY_NAMES["quality_safety"], "质量", "安全"],
"after_sales": [CATEGORY_NAMES["after_sales"], "售后", "培训"],
"compliance": [CATEGORY_NAMES["compliance"], "合规", "条款"],
"commercial": [CATEGORY_NAMES["commercial"], "商务"],
}
# 默认子章节模板
DEFAULT_SUB_CHAPTERS_TEMPLATE = [
{"title": "方案概述", "level": 2},

View File

@ -4,9 +4,9 @@
"""
import logging
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any
from ...tools.parser import DocumentChapter, ScoringCriteria
from ...tools.parser import DocumentChapter
from .constants import CATEGORY_NAMES
logger = logging.getLogger(__name__)

View File

@ -10,6 +10,7 @@ from ..base import BaseNode, NodeContext
from ...tools.parser import DocumentChapter
from .factories import ChapterFactory
from .base_mixins import TocNodeBase
from .category_manager import CategoryManager
logger = logging.getLogger(__name__)
@ -44,6 +45,8 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase):
# 确保标准章节存在
final_chapters = self._ensure_standard_chapters(final_chapters)
self._apply_saved_guidance(final_chapters, state.get("chapter_guidance_map"))
self.log_step_info("finalize_chapters", f"最终生成{len(final_chapters)}个章节")
return self._update_state(state,
@ -76,4 +79,19 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase):
# 可以在此添加其他必要的标准章节
# 例如:封面、目录、声明等
return final_chapters
return final_chapters
def _apply_saved_guidance(self,
chapters: List[DocumentChapter],
guidance_map: Dict[str, Any] | None) -> None:
if not guidance_map:
return
for chapter in chapters:
category_key = CategoryManager.infer_category_for_chapter(chapter)
guidance = guidance_map.get(category_key)
if guidance:
chapter.guidance = guidance
if chapter.children:
self._apply_saved_guidance(chapter.children, guidance_map)

View File

@ -13,6 +13,7 @@ from .factories import ChapterFactory
from .llm_helper import LLMHelper
from .base_mixins import TocNodeBase
from ...utils.document_context import DocumentContextSearcher
from ...utils.guidance import build_chapter_guidance
logger = logging.getLogger(__name__)
@ -50,6 +51,8 @@ class GenerateSubChaptersNode(BaseNode, TocNodeBase):
except Exception as exc:
self.log_step_info("context_searcher", f"上下文检索器初始化失败: {exc}")
guidance_map = dict(state.get("chapter_guidance_map", {}))
# 为每个章节生成子标题
enhanced_chapters = []
for chapter in preliminary_chapters:
@ -60,9 +63,14 @@ class GenerateSubChaptersNode(BaseNode, TocNodeBase):
)
enhanced_chapters.append(enhanced_chapter)
if getattr(enhanced_chapter, "guidance", None):
category_key = CategoryManager.infer_category_for_chapter(enhanced_chapter)
if category_key:
guidance_map[category_key] = enhanced_chapter.guidance
self.log_step_info("generate_sub_chapters", f"完成{len(enhanced_chapters)}个章节的子标题生成")
return self._update_state(state, preliminary_chapters=enhanced_chapters)
return self._update_state(state, preliminary_chapters=enhanced_chapters, chapter_guidance_map=guidance_map)
def _enhance_chapter_with_subs(self,
chapter: DocumentChapter,
@ -102,6 +110,10 @@ class GenerateSubChaptersNode(BaseNode, TocNodeBase):
self.log_step_info("enhance_chapter", f"章节 {chapter.title} AI生成子标题失败")
raise RuntimeError(error_msg)
guidance = build_chapter_guidance(chapter, corresponding_criteria, context_snippets)
if guidance:
chapter.guidance = guidance
return chapter
def _collect_context_snippets(self,

View File

@ -4,10 +4,9 @@
"""
import logging
from typing import Dict, List, Any
from typing import Dict, Any
from ..base import BaseNode, NodeContext
from ...tools.parser import ScoringCriteria
from .category_manager import CategoryManager
from .base_mixins import TocNodeBase

View File

@ -11,7 +11,6 @@ from ..base import BaseNode, NodeContext
from ...tools.parser import DocumentChapter
from .base_mixins import TocNodeBase
from .llm_helper import LLMHelper
from .factories import ChapterFactory
from ...config.prompt_manager import get_prompt_manager
logger = logging.getLogger(__name__)

View File

@ -4,10 +4,9 @@
"""
import logging
from typing import Dict, List, Any
from typing import Dict, Any
from ..base import BaseNode, NodeContext
from ...tools.parser import ScoringCriteria, DocumentChapter
from .llm_helper import LLMHelper
from .base_mixins import TocNodeBase
from ...utils.document_context import DocumentContextSearcher

View File

@ -11,7 +11,6 @@ from enum import Enum
import pandas as pd
from docx import Document
from openai import OpenAI
from pydantic import BaseModel, Field
from ..config import get_settings
@ -51,6 +50,14 @@ class DeviationItem(BaseModel):
chapter_id: str = Field(..., description="对应章节ID")
class ChapterGuidance(BaseModel):
"""章节写作提示"""
key_requirements: List[str] = Field(default_factory=list, description="关键评分要求")
suggested_evidence: List[str] = Field(default_factory=list, description="建议支撑材料")
context_snippets: List[str] = Field(default_factory=list, description="上下文摘录")
class DocumentChapter(BaseModel):
"""文档章节"""
@ -60,6 +67,7 @@ class DocumentChapter(BaseModel):
score: float | None = Field(default=None, description="评分值")
children: List['DocumentChapter'] = Field(default_factory=list, description="子章节")
template_placeholder: str | None = Field(default=None, description="模板占位符")
guidance: ChapterGuidance | None = Field(default=None, description="章节写作提示")

View File

@ -12,7 +12,6 @@ import chromadb
from chromadb.config import Settings as ChromaSettings
from chromadb.utils import embedding_functions
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
@ -215,6 +214,8 @@ class RAGTool:
# 构建提示词变量
emphasis_part = f'\n特别强调:{emphasis}' if emphasis else ''
rag_part = f'\n\n参考资料:\n{rag_context}' if rag_context else ''
guidance_notes = (context.get('guidance_notes') or '').strip()
guidance_part = guidance_notes or '(暂无重点提示)'
prompt_variables = {
"title": task_title,
@ -226,6 +227,7 @@ class RAGTool:
"consistency_rules": context.get('consistency_rules', '1. 保持章节语气与格式一致'),
"context_summary": context.get('context_summary', '(暂无可引用的上下文)'),
"emphasis_part": emphasis_part,
"guidance_part": guidance_part,
"rag_part": rag_part,
}

View File

@ -5,7 +5,6 @@
import json
from pathlib import Path
from typing import Any
class TableGenerator:

View File

@ -5,17 +5,18 @@
import logging
import re
import unicodedata
from collections import defaultdict
from pathlib import Path
from typing import List
from typing import Any, Dict, List, Optional, Tuple
from docx import Document
from docx.shared import Inches, Pt, Cm
from docx.enum.style import WD_STYLE_TYPE
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.text.paragraph import Paragraph
from .parser import DocumentChapter
from .parser import DocumentChapter, ChapterGuidance
from ..config.settings import get_settings
logger = logging.getLogger(__name__)
@ -35,12 +36,12 @@ NUMBERING_PATTERNS = [
re.compile(r'^([A-Z]\.\s+)'), # A.
]
MARKDOWN_HEADING_PATTERN = re.compile(r'^#{2,4}\s+')
MARKDOWN_HEADING_PATTERN = re.compile(r'^(#{2,})\s+')
ORDERED_LIST_PATTERN = re.compile(r'^(\d+[\.)]|\d+)\s+')
UNORDERED_LIST_PATTERN = re.compile(r'^[-*•]\s+')
BOLD_TEXT_PATTERN = re.compile(r'\*\*(.+?)\*\*')
ITALIC_TEXT_PATTERN = re.compile(r'\*(.+?)\*')
CHAPTER_ID_PATTERN = re.compile(r'chapter_(\d+(?:_\d+)*)')
CHAPTER_ID_PATTERN = re.compile(r'chapter_([a-zA-Z0-9_]+)')
HEADING_LEVEL_PATTERN = re.compile(r'^(#+)')
# 字体常量(非格式参数)
@ -110,6 +111,9 @@ class WordProcessor:
run = para.add_run(title_text)
run.bold = True
if getattr(chapter, "guidance", None):
self._add_guidance_block(doc, chapter.guidance)
# 为有内容的章节添加占位符
if chapter.template_placeholder:
content_para = doc.add_paragraph(f"\n{chapter.template_placeholder}\n")
@ -134,6 +138,25 @@ class WordProcessor:
if chapter.level == PRIMARY_CHAPTER_LEVEL:
doc.add_paragraph() # 添加空行
def _add_guidance_block(self, doc: Document, guidance: ChapterGuidance) -> None:
"""在章节下方插入写作提示"""
lines = ["【重点提示】"]
if guidance.key_requirements:
lines.append("关键要求:")
lines.extend([f"{item}" for item in guidance.key_requirements])
if guidance.suggested_evidence:
lines.append("建议支撑:")
lines.extend([f"{item}" for item in guidance.suggested_evidence])
if guidance.context_snippets:
lines.append("招标摘录:")
lines.extend([f"{item}" for item in guidance.context_snippets])
block_text = "\n".join(lines)
para = doc.add_paragraph(block_text)
for run in para.runs:
run.italic = True
def _check_has_numbering(self, title: str) -> bool:
"""检查标题是否包含编号
@ -154,37 +177,100 @@ class WordProcessor:
return NUMBERING_PATTERNS[0].sub('', title.strip())
def fill_placeholder(self, doc_path: str, placeholder: str, content: str) -> None:
"""填充Word文档:在对应标题后插入内容
"""兼容旧接口, 根据占位符填充Word文档内容"""
Args:
doc_path: Word文档路径
placeholder: 占位符(用于解析章节编号, {{chapter_2_1_1_content}})
content: 要填充的内容
match = CHAPTER_ID_PATTERN.search(placeholder)
if not match:
raise ValueError(f"无法从占位符解析章节编号: {placeholder}")
Raises:
ValueError: 占位符格式错误或未找到对应标题
FileNotFoundError: 文档不存在
"""
doc = Document(doc_path)
chapter_id = match.group(0)
chapter_meta = {
"id": chapter_id,
"placeholder": placeholder,
"title": chapter_id,
"normalized_title": self._normalize_heading_text(chapter_id),
"level": None,
"heading_number": None,
"order_index": None,
}
# 1. 解析占位符提取章节编号
chapter_number = self._parse_chapter_number_from_placeholder(placeholder)
try:
chapter_number = self._parse_chapter_number_from_placeholder(placeholder)
except ValueError:
chapter_number = None
# 2. 查找对应标题
target_paragraph, _ = self._find_heading_by_chapter_id(doc, chapter_number)
if chapter_number:
chapter_meta["level"] = chapter_number.count(".") + 1
# 3. 获取章节层级
chapter_level = self._get_chapter_level_from_heading(target_paragraph)
self.fill_chapter_content(doc_path, chapter_meta, content)
def fill_chapter_content(self, doc_path: str | Path, chapter_meta: Dict[str, Any], content: str) -> None:
"""填充指定章节的内容到Word文档"""
if not Path(doc_path).exists():
raise FileNotFoundError(f"Word文档不存在: {doc_path}")
doc = Document(str(doc_path))
headings_index = self._extract_document_headings(doc)
placeholder = chapter_meta.get("placeholder")
chapter_number = None
if placeholder:
try:
chapter_number = self._parse_chapter_number_from_placeholder(placeholder)
except ValueError:
chapter_number = None
target_paragraph, target_index, matched_strategy = self._locate_heading(
chapter_meta, chapter_number, headings_index
)
# 确定章节层级
chapter_level = chapter_meta.get("level")
if not chapter_level and target_paragraph:
chapter_level = self._get_chapter_level_from_heading(target_paragraph)
chapter_level = chapter_level or DEFAULT_CHAPTER_LEVEL
# 4. 解析Markdown内容为段落结构
parsed_paragraphs = self._parse_markdown_to_paragraphs(content, chapter_level)
# 5. 在标题后插入段落
self._insert_parsed_paragraphs(doc, target_paragraph, parsed_paragraphs)
if matched_strategy == "any_heading_fallback":
notice_text = (
f"【系统提示】章节“{chapter_meta.get('title', chapter_meta.get('id', ''))}”未能精准定位,"
"已将内容插入至文档末尾标题,请检查模板结构。"
)
parsed_paragraphs = [{'type': 'notice', 'text': notice_text}] + parsed_paragraphs
# 6. 保存文档
doc.save(doc_path)
logger.info(f"成功填充内容到文档: {placeholder}")
if target_paragraph is None:
logger.warning(
"章节 %s 未能定位原始标题, 将内容追加至文末", chapter_meta.get("id")
)
self._append_content_to_document_end(doc, chapter_meta, parsed_paragraphs, chapter_level)
else:
if matched_strategy == "any_heading_fallback":
logger.warning(
"章节 %s 未精准匹配标题,已退回到最后一个标题",
chapter_meta.get("id"),
)
elif matched_strategy:
logger.info(
"章节 %s 通过策略 %s 匹配到标题: %s",
chapter_meta.get("id"),
matched_strategy,
target_paragraph.text.strip(),
)
else:
logger.info(
"章节 %s 匹配到标题: %s",
chapter_meta.get("id"),
target_paragraph.text.strip(),
)
self._insert_parsed_paragraphs(doc, target_paragraph, parsed_paragraphs)
doc.save(str(doc_path))
logger.info(
"成功填充内容到文档: %s", chapter_meta.get("placeholder", chapter_meta.get("id"))
)
def _parse_markdown_to_paragraphs(self, content: str, chapter_level: int) -> list:
"""解析Markdown内容为段落结构列表
@ -209,30 +295,14 @@ class WordProcessor:
# Markdown标题## 标题
if MARKDOWN_HEADING_PATTERN.match(line):
level = len(HEADING_LEVEL_PATTERN.match(line).group(1))
title_text = MARKDOWN_HEADING_PATTERN.sub('', line)
# 3级标题内容## → 加粗段落(非标题样式)
if chapter_level == DEFAULT_CHAPTER_LEVEL:
paragraphs.append({
'type': 'bold_paragraph',
'text': title_text,
'indent': False
})
# 2级标题内容## → Heading 4
elif chapter_level == 2:
paragraphs.append({
'type': 'heading',
'text': title_text,
'level': 4
})
# 1级标题内容## → Heading 3
else:
paragraphs.append({
'type': 'heading',
'text': title_text,
'level': 3
})
title_text = MARKDOWN_HEADING_PATTERN.sub('', line).strip()
if not title_text:
i += 1
continue
paragraphs.append({
'type': 'bold_paragraph',
'text': title_text,
})
# 有序列表1. / 1) / 1
elif ORDERED_LIST_PATTERN.match(line):
@ -267,6 +337,212 @@ class WordProcessor:
return paragraphs
def _extract_document_headings(self, doc: Document) -> List[Dict[str, Any]]:
"""提取文档中所有标题段落,包含任意层级"""
headings: List[Dict[str, Any]] = []
level_counters: defaultdict[int, int] = defaultdict(int)
for index, para in enumerate(doc.paragraphs):
style_name = getattr(para.style, 'name', '') or ''
if not style_name.lower().startswith('heading'):
continue
level = self._infer_heading_level(para)
level_key = level if level is not None else 0
level_counters[level_key] += 1
if level is not None:
for deeper_level in list(level_counters.keys()):
if isinstance(deeper_level, int) and deeper_level > level:
level_counters[deeper_level] = 0
headings.append({
'paragraph': para,
'index': index,
'level': level,
'style_name': style_name,
'normalized_text': self._normalize_heading_text(para.text),
'raw_text': para.text.strip(),
'number': self._extract_leading_number(para.text),
'order_index': level_counters[level_key],
})
return headings
def _locate_heading(
self,
chapter_meta: Dict[str, Any],
chapter_number: Optional[str],
headings_index: List[Dict[str, Any]],
) -> Tuple[Optional[Paragraph], int, Optional[str]]:
"""根据章节元数据定位标题段落"""
attempts: List[Tuple[str, callable]] = []
if chapter_number:
attempts.append(
(
"placeholder_number",
lambda: self._match_heading_by_number(headings_index, chapter_number),
)
)
meta_number = chapter_meta.get("heading_number")
if meta_number and meta_number != chapter_number:
attempts.append(
(
"chapter_number",
lambda: self._match_heading_by_number(headings_index, meta_number),
)
)
normalized_title = chapter_meta.get("normalized_title")
if normalized_title:
attempts.append(
(
"normalized_title",
lambda: self._match_heading_by_title(headings_index, normalized_title),
)
)
title = chapter_meta.get("title")
if title and (not normalized_title or self._normalize_heading_text(title) != normalized_title):
attempts.append(
(
"raw_title",
lambda: self._match_heading_by_title(
headings_index, self._normalize_heading_text(title)
),
)
)
level = chapter_meta.get("level")
order_index = chapter_meta.get("order_index")
if isinstance(order_index, int) and order_index > 0:
attempts.append(
(
"sequence_index",
lambda: self._match_heading_by_sequence(headings_index, level, order_index),
)
)
for strategy, matcher in attempts:
paragraph, index = matcher()
if paragraph is not None:
return paragraph, index, strategy
if headings_index:
fallback = headings_index[-1]
return fallback['paragraph'], fallback['index'], "any_heading_fallback"
return None, -1, None
def _match_heading_by_number(
self, headings_index: List[Dict[str, Any]], chapter_number: str
) -> Tuple[Optional[Paragraph], int]:
for heading in headings_index:
text = heading['raw_text']
stored_number = heading.get('number')
if stored_number and stored_number == chapter_number:
return heading['paragraph'], heading['index']
if self._paragraph_matches_number(text, chapter_number):
return heading['paragraph'], heading['index']
return None, -1
def _match_heading_by_title(
self, headings_index: List[Dict[str, Any]], normalized_title: str
) -> Tuple[Optional[Paragraph], int]:
if not normalized_title:
return None, -1
for heading in headings_index:
if heading['normalized_text'] == normalized_title:
return heading['paragraph'], heading['index']
return None, -1
def _match_heading_by_sequence(
self, headings_index: List[Dict[str, Any]], level: Optional[int], order_index: int
) -> Tuple[Optional[Paragraph], int]:
count = 0
for heading in headings_index:
heading_level = heading.get('level')
if level is not None and heading_level != level:
continue
count += 1
if count == order_index:
return heading['paragraph'], heading['index']
return None, -1
@staticmethod
def _paragraph_matches_number(text: str, chapter_number: str) -> bool:
target = text.strip()
candidates = [
f"{chapter_number} ",
f"{chapter_number}.",
f"{chapter_number}",
f"{chapter_number}",
f"{chapter_number})",
f"{chapter_number}",
f"{chapter_number}",
]
return any(target.startswith(candidate) for candidate in candidates)
@staticmethod
def _extract_leading_number(text: str | None) -> Optional[str]:
if not text:
return None
stripped = text.strip()
match = re.match(r'^(\d+(?:\.\d+)*)', stripped)
if match:
return match.group(1).rstrip('.')
return None
@staticmethod
def _infer_heading_level(paragraph: Paragraph) -> Optional[int]:
style_name = paragraph.style.name
match = re.search(r"(\d+)$", style_name)
if match:
return int(match.group(1))
return None
@staticmethod
def _normalize_heading_text(text: str | None) -> str:
if not text:
return ""
normalized = unicodedata.normalize("NFKC", text)
normalized = re.sub(r"\s+", "", normalized)
return normalized.lower()
def _append_content_to_document_end(
self,
doc: Document,
chapter_meta: Dict[str, Any],
parsed_paragraphs: List[dict],
chapter_level: int,
) -> None:
notice_text = (
f"【系统提示】未能定位原始章节“{chapter_meta.get('title', chapter_meta.get('id', ''))}”,"
"以下内容已自动追加,请检查模板标题格式。"
)
notice_para = doc.add_paragraph(notice_text)
for run in notice_para.runs:
run.italic = True
fallback_heading = doc.add_paragraph()
heading_text = chapter_meta.get('title') or chapter_meta.get('id') or "未命名章节"
run = fallback_heading.add_run(heading_text)
run.bold = True
run.font.name = FONT_HEITI
run.font.size = Pt(settings.heading_font_size)
self._insert_parsed_paragraphs(doc, fallback_heading, parsed_paragraphs)
def _find_heading_by_chapter_id(self, doc: Document, chapter_number: str) -> tuple[Paragraph, int]:
"""根据章节编号查找标题段落
@ -282,9 +558,8 @@ class WordProcessor:
"""
for i, para in enumerate(doc.paragraphs):
if para.style.name.startswith('Heading'):
text = para.text.strip()
if text.startswith(f"{chapter_number} ") or text.startswith(f"{chapter_number}."):
logger.info(f"找到匹配标题: {text}")
if self._paragraph_matches_number(para.text, chapter_number):
logger.info(f"找到匹配标题: {para.text.strip()}")
return para, i
raise ValueError(f"未找到编号为 {chapter_number} 的标题")
@ -320,9 +595,16 @@ class WordProcessor:
"""
match = CHAPTER_ID_PATTERN.search(placeholder)
if not match:
# 支持自定义章节ID但无法解析出数字编号时返回None
if 'chapter_' in placeholder:
return None
raise ValueError(f"无法从占位符解析章节编号: {placeholder}")
chapter_number = match.group(1).replace('_', '.')
chapter_body = match.group(1)
if not chapter_body or not re.search(r'\d', chapter_body):
return None
chapter_number = chapter_body.replace('_', '.')
return chapter_number
def _create_paragraph_element(self, doc: Document, current_element, para_struct: dict) -> Paragraph:
@ -347,19 +629,11 @@ class WordProcessor:
para_type = para_struct['type']
text = para_struct['text']
# 根据类型应用不同样式
if para_type == 'heading':
# 子标题使用Heading样式
level = para_struct.get('level', 4)
try:
new_para.style = f'Heading {level}'
new_para.text = text
except KeyError:
run = new_para.add_run(text)
run.bold = True
run.font.size = Pt(settings.heading_font_size)
para_type = 'bold_paragraph'
elif para_type == 'bold_paragraph':
# 根据类型应用不同样式
if para_type == 'bold_paragraph':
# 加粗段落3级标题内的小标题
try:
new_para.style = 'Normal'
@ -404,6 +678,20 @@ class WordProcessor:
run.font.name = FONT_SONGTI
run.font.size = Pt(settings.normal_font_size)
elif para_type == 'notice':
try:
new_para.style = 'Normal'
except KeyError:
pass
para_format.first_line_indent = Cm(0)
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
para_format.space_after = Pt(settings.space_after_small)
run = new_para.add_run(text)
run.italic = True
run.font.name = FONT_SONGTI
run.font.size = Pt(settings.normal_font_size)
else: # paragraph
# 普通段落
try:

View File

@ -5,7 +5,6 @@
import logging
import re
from pathlib import Path
from typing import Dict, Optional
from enum import Enum
@ -84,7 +83,7 @@ class WordFormatter:
logger.warning(f"无法识别格式: {part}")
if not format_dict:
logger.warning(f"未识别到任何格式,使用默认")
logger.warning("未识别到任何格式,使用默认")
return WordFormatter.get_default_format()
return format_dict

View File

@ -0,0 +1,47 @@
"""章节写作提示构建工具。"""
from __future__ import annotations
from typing import Iterable, List, Optional
from ..tools.parser import ChapterGuidance, DocumentChapter, ScoringCriteria
def _format_requirement(criterion: ScoringCriteria) -> str:
base = f"{criterion.item_name}{criterion.max_score:g}分)"
description = (criterion.description or "").strip().replace("\n", " ")
if description:
trimmed = description[:120].rstrip()
if len(description) > 120:
trimmed += "..."
return f"{base}{trimmed}"
return base
def _format_evidence(criterion: ScoringCriteria) -> str:
return f"提供{criterion.item_name}相关的方案、记录或佐证材料"
def build_chapter_guidance(
chapter: DocumentChapter,
criteria: Iterable[ScoringCriteria],
context_snippets: Optional[List[str]] = None,
) -> ChapterGuidance | None:
"""根据评分项和上下文生成章节提示。"""
criteria_list = list(criteria)
if not criteria_list:
return None
key_requirements = [_format_requirement(c) for c in criteria_list[:3]]
suggested_evidence = [_format_evidence(c) for c in criteria_list[:3]]
snippets = (context_snippets or [])[:3]
return ChapterGuidance(
key_requirements=key_requirements,
suggested_evidence=suggested_evidence,
context_snippets=snippets,
)
__all__ = ["build_chapter_guidance"]

View File

@ -0,0 +1,49 @@
"""基础监控与指标采集。"""
from __future__ import annotations
import time
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Dict, List
@dataclass(slots=True)
class MetricRecord:
name: str
duration: float
details: Dict[str, str] = field(default_factory=dict)
class MetricsRecorder:
"""事件耗时记录器。"""
def __init__(self) -> None:
self._records: List[MetricRecord] = []
@contextmanager
def track(self, name: str, **details: str):
start = time.perf_counter()
try:
yield
finally:
duration = time.perf_counter() - start
self._records.append(MetricRecord(name=name, duration=duration, details=details))
def add(self, name: str, duration: float, **details: str) -> None:
self._records.append(MetricRecord(name=name, duration=duration, details=details))
def to_dict(self) -> Dict[str, List[dict]]:
return {
"records": [
{
"name": record.name,
"duration": round(record.duration, 4),
"details": record.details,
}
for record in self._records
]
}
__all__ = ["MetricsRecorder", "MetricRecord"]

View File

@ -46,6 +46,7 @@ class PromptPlanner:
objectives = self._build_objectives(requirements, metadata, emphasis)
consistency_rules = self._build_consistency_rules(chapter_path, parent_context)
guidance_notes = self._format_guidance_notes((metadata or {}).get("guidance") or chapter.get("guidance"))
context_parts: List[str] = []
if parent_context:
@ -68,6 +69,7 @@ class PromptPlanner:
"context_summary": context_summary,
"emphasis": emphasis or "",
"category": (metadata or {}).get("category"),
"guidance_notes": guidance_notes,
}
return spec
@ -175,6 +177,28 @@ class PromptPlanner:
return info
def _format_guidance_notes(self, guidance: Optional[Dict[str, Any]]) -> str:
if not guidance:
return ""
lines: List[str] = []
key_requirements = guidance.get("key_requirements") or []
if key_requirements:
lines.append("【关键要求】")
lines.extend(f"- {item}" for item in key_requirements)
suggested_evidence = guidance.get("suggested_evidence") or []
if suggested_evidence:
lines.append("【建议支撑】")
lines.extend(f"- {item}" for item in suggested_evidence)
context_snippets = guidance.get("context_snippets") or []
if context_snippets:
lines.append("【招标摘录】")
lines.extend(f"- {item}" for item in context_snippets)
return "\n".join(lines).strip()
def _split_requirements(self, requirements: str) -> List[str]:
if not requirements:
return []

View File

@ -0,0 +1,81 @@
"""质量检查工具。"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List
@dataclass(slots=True)
class QualityIssue:
code: str
message: str
severity: str = "warning" # info | warning | error
class QualityChecklist:
"""提供基础质量检查。"""
def __init__(self, *, min_content_len: int = 120) -> None:
self.min_content_len = min_content_len
def check_generation(self, task: Dict[str, Any], content: str) -> List[QualityIssue]:
issues: List[QualityIssue] = []
if not content or len(content.strip()) < self.min_content_len:
issues.append(
QualityIssue(
code="content_too_short",
message=f"生成内容长度不足 {self.min_content_len} 字符",
)
)
required_keywords = task.get("quality_keywords") or []
missing = [kw for kw in required_keywords if kw not in content]
if missing:
issues.append(
QualityIssue(
code="missing_keywords",
message=f"缺少关键字: {', '.join(missing)}",
)
)
return issues
def check_assembly(self, chapter_map: Dict[str, Any], tasks: List[Dict[str, Any]]) -> List[QualityIssue]:
issues: List[QualityIssue] = []
for task in tasks:
chapter_id = task.get("chapter_id")
if not chapter_id:
issues.append(
QualityIssue(
code="missing_chapter",
message=f"任务 {task.get('id')} 缺少章节映射",
severity="error",
)
)
continue
if chapter_id not in chapter_map:
issues.append(
QualityIssue(
code="chapter_not_found",
message=f"章节 {chapter_id} 在模板中未找到",
severity="error",
)
)
status = task.get("status")
if status not in {"completed", "approved"}:
issues.append(
QualityIssue(
code="task_not_completed",
message=f"任务 {task.get('id')} 状态为 {status}",
)
)
return issues
__all__ = ["QualityChecklist", "QualityIssue"]

View File

@ -0,0 +1,62 @@
"""轻量重试策略实现。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable, Iterable, List, Tuple, Type
@dataclass(slots=True)
class RetryAttempt:
"""单次尝试记录。"""
attempt: int
succeeded: bool
error: Exception | None = None
@dataclass(slots=True)
class RetryReport:
"""重试执行结果。"""
attempts: List[RetryAttempt] = field(default_factory=list)
@property
def succeeded(self) -> bool:
return any(item.succeeded for item in self.attempts)
class RetryPolicy:
"""简单重试策略。"""
def __init__(
self,
*,
max_attempts: int = 2,
retry_exceptions: Iterable[Type[Exception]] | None = None,
) -> None:
if max_attempts < 1:
raise ValueError("max_attempts 必须 >= 1")
self.max_attempts = max_attempts
self.retry_exceptions = tuple(retry_exceptions or (Exception,))
def run(self, func: Callable, *args, **kwargs) -> Tuple[RetryReport, object | None]:
report = RetryReport()
last_exception: Exception | None = None
for attempt in range(1, self.max_attempts + 1):
try:
result = func(*args, **kwargs)
report.attempts.append(RetryAttempt(attempt=attempt, succeeded=True))
return report, result
except self.retry_exceptions as exc: # type: ignore[misc]
report.attempts.append(
RetryAttempt(attempt=attempt, succeeded=False, error=exc)
)
last_exception = exc
if last_exception is not None:
raise last_exception
raise RuntimeError("RetryPolicy未捕获到异常但执行失败")
__all__ = ["RetryPolicy", "RetryReport", "RetryAttempt"]

View File

@ -339,8 +339,6 @@ def _interactive_readline_windows(prompt: str, timeout: int) -> Tuple[str, bool]
def _interactive_readline_posix(prompt: str, timeout: int) -> Tuple[str, bool]:
import select
import termios
import tty
fd = sys.stdin.fileno()
if fd < 0:

View File

@ -0,0 +1,254 @@
"""文件校验工具集。
提供对招标文件进行基础校验的能力包括
- 路径有效性与文件大小检查
- 格式支持与转换提示
- 统一的告警与问题描述结构
该模块仅做轻量规则校验不负责实际格式转换
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
SUPPORTED_PRIMARY_SUFFIXES = {".docx"}
SUPPORTED_CONVERTIBLE_SUFFIXES = {".doc", ".pdf"}
@dataclass(slots=True)
class FileValidationIssue:
"""单条校验问题。"""
code: str
message: str
severity: str = "warning" # warning | error
@dataclass(slots=True)
class FileValidationReport:
"""文件校验报告。"""
path: Path
size_mb: float
processable: bool
requires_conversion: bool
issues: List[FileValidationIssue] = field(default_factory=list)
@property
def is_valid(self) -> bool:
"""是否无阻塞问题。"""
return all(issue.severity != "error" for issue in self.issues)
def to_dict(self) -> dict:
"""转换为可序列化字典。"""
return {
"path": str(self.path),
"size_mb": round(self.size_mb, 2),
"processable": self.processable,
"requires_conversion": self.requires_conversion,
"issues": [
{
"code": issue.code,
"message": issue.message,
"severity": issue.severity,
}
for issue in self.issues
],
}
class FileValidator:
"""招标文件校验器。"""
def __init__(self, *, max_size_mb: float = 80.0) -> None:
self._max_size_mb = max_size_mb
def validate(self, file_path: str | Path) -> FileValidationReport:
"""执行校验。
Args:
file_path: 待检验文件路径
Returns:
FileValidationReport
"""
path = Path(file_path).expanduser().resolve()
issues: List[FileValidationIssue] = []
if not path.exists():
issues.append(
FileValidationIssue(
code="file_not_found",
message=f"文件不存在: {path}",
severity="error",
)
)
return FileValidationReport(
path=path,
size_mb=0.0,
processable=False,
requires_conversion=False,
issues=issues,
)
size_mb = path.stat().st_size / (1024 * 1024)
if size_mb == 0:
issues.append(
FileValidationIssue(
code="empty_file",
message="文件大小为0无法解析",
severity="error",
)
)
if size_mb > self._max_size_mb:
issues.append(
FileValidationIssue(
code="file_too_large",
message=f"文件大小 {size_mb:.1f}MB 超出限制({self._max_size_mb}MB)",
)
)
suffix = path.suffix.lower()
processable = suffix in SUPPORTED_PRIMARY_SUFFIXES
requires_conversion = False
if suffix in SUPPORTED_PRIMARY_SUFFIXES:
pass
elif suffix in SUPPORTED_CONVERTIBLE_SUFFIXES:
requires_conversion = True
issues.append(
FileValidationIssue(
code="needs_conversion",
message=f"检测到 {suffix} 文件,建议先转换为 .docx 再解析",
)
)
else:
issues.append(
FileValidationIssue(
code="unsupported_format",
message=f"不支持的文件格式: {suffix}",
severity="error",
)
)
return FileValidationReport(
path=path,
size_mb=size_mb,
processable=processable,
requires_conversion=requires_conversion,
issues=issues,
)
@dataclass(slots=True)
class ContentValidationIssue:
"""章节内容校验问题"""
code: str
message: str
class ContentValidationError(ValueError):
"""章节内容校验异常"""
def __init__(self, issues: List[ContentValidationIssue]):
self.issues = issues
message = "; ".join(issue.message for issue in issues)
super().__init__(message)
class ContentValidator:
"""章节内容合规校验器"""
def __init__(self) -> None:
self._opening_patterns = [
re.compile(r"^第[一二三四五六七八九十百千0-9]+章"),
re.compile(r"^[一二三四五六七八九十]+、"),
]
self._opening_blacklist = (
"商务条款响应情况",
"技术规格响应说明",
"商务和技术方面存在的偏差说明",
)
self._forbidden_phrases = (
"经认真研读招标文件要求",
"现就商务和技术方面存在的偏差说明",
"商务条款响应情况",
"技术规格响应说明",
)
def validate(self, chapter: Dict[str, Any], content: str) -> None:
issues: List[ContentValidationIssue] = []
heading_issue = self.ensure_heading_alignment(chapter, content)
if heading_issue:
issues.append(ContentValidationIssue(code="heading_misaligned", message=heading_issue))
template_issue = self.detect_cross_chapter_signature(content)
if template_issue:
issues.append(ContentValidationIssue(code="template_detected", message=template_issue))
if issues:
raise ContentValidationError(issues)
def ensure_heading_alignment(self, chapter: Dict[str, Any], content: str) -> Optional[str]:
line = self._first_meaningful_line(content)
if not line:
return None
cleaned = self._normalize_line(line)
for pattern in self._opening_patterns:
if pattern.match(cleaned):
return (
f"检测到新的章节标题“{line}”,请直接编写《{chapter.get('title', chapter.get('id'))}》正文内容"
)
for keyword in self._opening_blacklist:
if cleaned.startswith(keyword):
return (
f"检测到与章节无关的套话“{line}”,请按章节《{chapter.get('title', chapter.get('id'))}》展开"
)
return None
def detect_cross_chapter_signature(self, content: str) -> Optional[str]:
if not content:
return None
lowered = content.strip()
for phrase in self._forbidden_phrases:
if phrase and phrase in lowered:
return f"检测到跨章节模板短语“{phrase}”,请使用本章节专属内容"
return None
def _first_meaningful_line(self, content: str) -> Optional[str]:
for raw_line in content.splitlines():
stripped = raw_line.strip()
if stripped:
return stripped
return None
def _normalize_line(self, line: str) -> str:
normalized = line.lstrip('#').strip()
normalized = re.sub(r'^[\d\.\-•\s、()]+', '', normalized)
return normalized
__all__ = [
"FileValidator",
"FileValidationReport",
"FileValidationIssue",
"ContentValidator",
"ContentValidationIssue",
"ContentValidationError",
]

View File

@ -1,7 +1,6 @@
"""timeout_input 实用函数的单元测试"""
import importlib.util
import sys
from pathlib import Path
import pytest

View File

@ -0,0 +1,40 @@
from bidmaster.utils.validation import ContentValidationError, ContentValidator
def test_content_validator_rejects_cross_chapter_phrase() -> None:
validator = ContentValidator()
chapter = {"id": "chapter_1", "title": "服务方案"}
content = (
"经认真研读招标文件要求,结合我司实际情况,现就商务和技术方面存在的偏差说明如下:\n"
"商务条款响应情况\n我司完全响应……"
)
try:
validator.validate(chapter, content)
except ContentValidationError as exc:
assert "跨章节模板" in str(exc)
else:
raise AssertionError("内容校验应当失败")
def test_content_validator_accepts_normal_content() -> None:
validator = ContentValidator()
chapter = {"id": "chapter_2", "title": "技术路线"}
content = (
"我们以矿山智慧化平台为核心,构建“实时感知—风险预警—指挥调度”闭环。\n"
"- 通过多源感知网络实现危险场景秒级采集;\n"
"- 以知识图谱关联隐患并生成操作工序清单。"
)
validator.validate(chapter, content)
def test_content_validator_allows_legitimate_intro() -> None:
validator = ContentValidator()
chapter = {"id": "chapter_a", "title": "商务和技术偏差表"}
content = (
"在本次投标过程中,我们对招标文件进行了全面分析,并结合我司情况编制偏差说明。\n"
"我们将在响应表中逐条列出商务及技术条款的差异与说明。"
)
validator.validate(chapter, content)

View File

@ -1 +1,197 @@
# Word处理器测试
"""Word处理器章节填充逻辑单元测试"""
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from docx import Document
from bidmaster.tools.word import WordProcessor
@pytest.fixture()
def word_processor() -> WordProcessor:
return WordProcessor()
def _create_document(path: Path, headings: list[tuple[str, int]]) -> None:
doc = Document()
for text, level in headings:
doc.add_heading(text, level=level)
doc.save(path)
def _count_heading_paragraphs(doc: Document) -> int:
return sum(1 for para in doc.paragraphs if para.style.name.startswith("Heading"))
def test_fill_chapter_with_numbered_heading(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "numbered.docx"
_create_document(doc_path, [("1 商务和技术偏差表", 1)])
chapter_meta = {
"id": "chapter_1",
"title": "商务和技术偏差表",
"level": 1,
"placeholder": "{{chapter_1_content}}",
"normalized_title": word_processor._normalize_heading_text("商务和技术偏差表"),
"heading_number": "1",
"order_index": 1,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "测试段落内容")
doc = Document(doc_path)
assert any("测试段落内容" in para.text for para in doc.paragraphs)
def test_fill_chapter_without_number_uses_title(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "title_only.docx"
_create_document(doc_path, [("商务和技术偏差表", 1)])
chapter_meta = {
"id": "chapter_1",
"title": "商务和技术偏差表",
"level": 1,
"placeholder": "{{chapter_1_content}}",
"normalized_title": word_processor._normalize_heading_text("商务和技术偏差表"),
"heading_number": None,
"order_index": 1,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "服务方案内容")
doc = Document(doc_path)
assert any("服务方案内容" in para.text for para in doc.paragraphs)
def test_fill_chapter_sequence_fallback(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "sequence.docx"
_create_document(
doc_path,
[
("项目背景", 1),
("售后服务和质保期服务计划", 1),
],
)
chapter_meta = {
"id": "chapter_11",
"title": "售后服务和质保期服务计划",
"level": 1,
"placeholder": "{{chapter_11_content}}",
"normalized_title": word_processor._normalize_heading_text("售后服务和质保期服务计划"),
"heading_number": None,
"order_index": 2,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "售后保障措施")
doc = Document(doc_path)
assert any("售后保障措施" in para.text for para in doc.paragraphs)
def test_fill_chapter_appends_when_not_found(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "fallback.docx"
_create_document(doc_path, [("项目概述", 1)])
chapter_meta = {
"id": "chapter_missing",
"title": "未匹配章节",
"level": 1,
"placeholder": "{{chapter_missing_content}}",
"normalized_title": word_processor._normalize_heading_text("未匹配章节"),
"heading_number": None,
"order_index": 2,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "新增内容")
doc = Document(doc_path)
assert any("【系统提示】" in para.text for para in doc.paragraphs)
assert any("新增内容" in para.text for para in doc.paragraphs)
def test_fill_does_not_create_additional_headings(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "no_new_headings.docx"
_create_document(
doc_path,
[
("1 总体方案", 1),
("1.1 服务范围", 2),
],
)
before = Document(doc_path)
before_count = _count_heading_paragraphs(before)
chapter_meta = {
"id": "chapter_1_1",
"title": "服务范围",
"level": 2,
"placeholder": "{{chapter_1_1_content}}",
"normalized_title": word_processor._normalize_heading_text("服务范围"),
"heading_number": "1.1",
"order_index": 1,
}
content = "## 小节标题\n服务内容描述"
word_processor.fill_chapter_content(doc_path, chapter_meta, content)
after = Document(doc_path)
after_count = _count_heading_paragraphs(after)
assert after_count == before_count
def test_markdown_headings_render_as_bold_paragraphs(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "markdown.docx"
_create_document(doc_path, [("1 技术方案", 1)])
chapter_meta = {
"id": "chapter_1",
"title": "技术方案",
"level": 1,
"placeholder": "{{chapter_1_content}}",
"normalized_title": word_processor._normalize_heading_text("技术方案"),
"heading_number": "1",
"order_index": 1,
}
word_processor.fill_chapter_content(
doc_path,
chapter_meta,
"## 子标题\n段落内容",
)
doc = Document(doc_path)
bold_paras = [para for para in doc.paragraphs if para.text.strip() == "子标题"]
assert bold_paras, "应生成加粗段落"
assert all(not para.style.name.startswith("Heading") for para in bold_paras)
assert any(any(run.bold for run in para.runs) for para in bold_paras)
def test_fill_matches_higher_level_heading(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "deep_heading.docx"
_create_document(doc_path, [("1.1.1.1 深度章节", 4)])
chapter_meta = {
"id": "chapter_1_1_1_1",
"title": "深度章节",
"level": 4,
"placeholder": "{{chapter_1_1_1_1_content}}",
"normalized_title": word_processor._normalize_heading_text("深度章节"),
"heading_number": "1.1.1.1",
"order_index": 1,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "深度内容")
doc = Document(doc_path)
assert any("深度内容" in para.text for para in doc.paragraphs)