feat: 实现标书内容填写完整流程并修复关键bug

新增功能:
- 实现ContentWriterAgent内容填写协调器
- 实现SingleChapterAgent单章节处理流程
- 新增write命令组支持标书内容自动填写
- 支持深度优先遍历章节队列
- 实现每章节生成后立即保存到Word

核心修复:
1. 修复章节排序bug:改用数字排序避免 chapter_1_10 排在 chapter_1_2 前面
2. 修复占位符替换丢失格式:保留段落格式而非直接替换paragraph.text
3. 修复占位符缺失问题:添加降级策略,无占位符时自动在标题后插入
4. 实现Markdown到Word格式智能转换:
   - 根据章节层级自动映射样式
   - ## 标题 → 3级内容用加粗段落,2级内容用Heading 4
   - 有序列表 → 1)2)3)编号格式
   - 无序列表 → • 实心圆点
   - 自动应用正文格式:首行缩进、1.5倍行距、宋体12磅

技术改进:
- 使用python-docx底层XML API精确控制段落插入位置
- 实现增量保存避免内容丢失
- 添加详细的段落格式配置

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-10-01 16:53:44 +08:00
parent 5d16d8badc
commit 5d319fd2a6
19 changed files with 2238 additions and 25 deletions

View File

@ -0,0 +1,501 @@
{
"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": []
}
]
}
]
}
]
}
}

68
111 - 副本/tasks.json Normal file
View File

@ -0,0 +1,68 @@
[
{
"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

@ -3,12 +3,14 @@
from .analysis import AnalysisAgent
from .builders import TocAgentBuilder
from .builders.toc_builder import TocAgent
from .content_writer import ContentWriterAgent
from .interaction import InteractionHandler, InteractionMode
__all__ = [
"AnalysisAgent",
"TocAgentBuilder",
"TocAgent",
"ContentWriterAgent",
"InteractionHandler",
"InteractionMode"
"InteractionMode",
]

View File

@ -3,8 +3,7 @@
包含各种Agent的构建器实现
"""
from .content_builder import ContentWriterAgentBuilder
from .toc_builder import TocAgentBuilder
__all__ = [
"TocAgentBuilder"
]
__all__ = ["TocAgentBuilder", "ContentWriterAgentBuilder"]

View File

@ -0,0 +1,125 @@
"""内容填写Agent构建器
构建内容填写工作流的Agent
"""
import logging
from typing import Any, Dict
from ..base import AgentBuilder
from ...nodes.content import (
InitConfigNode,
PrepareChapterNode,
InteractWithUserNode,
GenerateContentNode,
SaveToWordNode,
CollectResultsNode,
)
logger = logging.getLogger(__name__)
def need_user_interaction(state: Dict[str, Any]) -> str:
"""判断是否需要用户交互
Args:
state: 当前状态
Returns:
路由键"interact""generate"
"""
needs_interaction = state.get("needs_interaction", False)
return "interact" if needs_interaction else "generate"
def should_continue_loop(state: Dict[str, Any]) -> str:
"""判断是否继续循环处理章节
Args:
state: 当前状态
Returns:
路由键"continue""collect"
"""
current_index = state.get("current_chapter_index", 0)
total = len(state.get("chapter_queue", []))
decision = "continue" if current_index < total else "collect"
logger.info(f"循环判断: index={current_index}/{total} -> {decision}")
if current_index < total:
return "continue"
return "collect"
class ContentWriterAgentBuilder(AgentBuilder):
"""内容填写Agent构建器
构建包含以下流程的Agent
1. 初始化配置并生成章节队列
2. 循环处理每个章节
- 准备章节
- 用户交互仅1级标题
- 生成内容
- 保存到Word
3. 收集结果
"""
@classmethod
def create(cls, interaction_handler=None) -> "ContentWriterAgentBuilder":
"""创建预配置的内容填写Agent构建器
Args:
interaction_handler: 交互处理函数
Returns:
配置完成的构建器实例
"""
builder = cls(interaction_handler)
# 添加所有节点
builder.add_node(InitConfigNode()) \
.add_node(PrepareChapterNode()) \
.add_node(InteractWithUserNode()) \
.add_node(GenerateContentNode()) \
.add_node(SaveToWordNode()) \
.add_node(CollectResultsNode())
# 设置入口点
builder.set_entry("init_config")
# 配置工作流
builder._configure_workflow()
logger.info("ContentWriterAgent构建器创建完成")
return builder
def _configure_workflow(self) -> None:
"""配置工作流程"""
# init_config → prepare_chapter
self.add_edge("init_config", "prepare_chapter")
# prepare_chapter → 条件分支(是否需要交互)
self.add_conditional_edge(
"prepare_chapter",
need_user_interaction,
{"interact": "interact_user", "generate": "generate_content"},
)
# interact_user → generate_content
self.add_edge("interact_user", "generate_content")
# generate_content → save_to_word
self.add_edge("generate_content", "save_to_word")
# save_to_word → 条件分支(是否继续循环)
self.add_conditional_edge(
"save_to_word",
should_continue_loop,
{"continue": "prepare_chapter", "collect": "collect_results"},
)
# collect_results → END
self.add_edge("collect_results", "END")
logger.info("ContentWriterAgent工作流配置完成")

View File

@ -0,0 +1,125 @@
"""内容填写Agent
负责标书章节内容的自动填写协调器模式
"""
import logging
from pathlib import Path
from typing import Any, Dict, Optional
from .single_chapter_agent import SingleChapterAgent
logger = logging.getLogger(__name__)
class ContentWriterAgent:
"""内容填写Agent协调器
提供标书章节内容的自动填写功能
1. 读取章节队列
2. 循环调用SingleChapterAgent处理每个章节
3. 汇总结果
"""
def __init__(self, interaction_handler=None):
"""初始化ContentWriterAgent
Args:
interaction_handler: 交互处理函数
"""
self.interaction_handler = interaction_handler
def write_content_sync(self, word_file: str) -> Dict[str, Any]:
"""同步执行内容填写
Args:
word_file: Word文档路径
Returns:
执行结果字典
"""
# 验证参数
word_path = Path(word_file)
if not word_path.exists():
raise FileNotFoundError(f"Word文档不存在: {word_file}")
logger.info(f"开始内容填写流程: {word_file}")
# 初始化章节队列直接从Word读取
from ..nodes.content.init_config import InitConfigNode
from ..nodes.base import NodeContext
init_node = InitConfigNode()
context = NodeContext(self.interaction_handler)
init_state = {
"word_file": word_file,
}
state = init_node.execute(init_state, context)
chapter_queue = state["chapter_queue"]
logger.info(f"{len(chapter_queue)} 个章节待处理")
for i, ch in enumerate(chapter_queue):
logger.info(f" [{i+1}] {ch['id']} - {ch['title']} (level={ch['level']})")
# 循环处理每个章节
state["chapter_configs"] = {}
state["generated_contents"] = {}
state["completed_chapters"] = []
state["interaction_handler"] = self.interaction_handler
for index, chapter in enumerate(chapter_queue):
logger.info(f"处理章节 [{index + 1}/{len(chapter_queue)}]: {chapter['id']} - {chapter['title']}")
state["current_chapter_index"] = index
# 创建子Agent处理单个章节
chapter_agent = SingleChapterAgent(self.interaction_handler)
result_state = chapter_agent.process_chapter(chapter, state)
# 显示生成的内容
chapter_id = chapter['id']
generated_content = result_state.get("generated_contents", {}).get(chapter_id, "")
if generated_content:
print(f"\n{'='*60}")
print(f"✅ 章节 {chapter_id} - {chapter['title']} 生成完成")
print(f"{'='*60}")
print(generated_content)
print(f"{'='*60}\n")
# 更新全局状态
state["chapter_configs"].update(result_state.get("chapter_configs", {}))
state["generated_contents"].update(result_state.get("generated_contents", {}))
state["completed_chapters"] = result_state.get("completed_chapters", [])
# 汇总结果
total_words = sum(len(content) for content in state["generated_contents"].values())
results = {
"success": True,
"total_chapters": len(chapter_queue),
"completed_count": len(state["completed_chapters"]),
"total_words": total_words,
"word_file": word_file,
}
logger.info(
f"内容填写完成: {results['completed_count']}/{results['total_chapters']} 章节, {total_words}"
)
return results
@classmethod
def create_silent(cls):
"""创建静默模式Agent"""
from .interaction import InteractionHandler, InteractionMode
handler = InteractionHandler(mode=InteractionMode.SILENT)
return cls(handler)
@classmethod
def create_interactive(cls):
"""创建交互模式Agent"""
from .interaction import InteractionHandler, InteractionMode
handler = InteractionHandler(mode=InteractionMode.INTERACTIVE)
return cls(handler)

View File

@ -167,9 +167,13 @@ class InteractionHandler:
Returns:
输入的文本
"""
# 先用console.print显示格式化的提示信息
self.console.print(f"\n{prompt}", style="blue")
while True:
try:
result = click.prompt(prompt, default=default, type=str)
# 使用简化的prompt获取输入
result = click.prompt("", default=default or "", type=str, show_default=False)
# 验证输入
if validation:

View File

@ -0,0 +1,88 @@
"""单章节处理Agent
负责处理单个章节的完整流程
"""
import logging
from typing import Any, Dict
from .base import BaseAgent, AgentBuilder
from ..nodes.content import (
InteractWithUserNode,
GenerateContentNode,
SaveToWordNode,
)
logger = logging.getLogger(__name__)
def need_user_interaction(state: Dict[str, Any]) -> str:
"""判断是否需要用户交互"""
needs_interaction = state.get("needs_interaction", False)
return "interact" if needs_interaction else "generate"
def route_start(state: Dict[str, Any]) -> str:
"""入口路由:判断是否需要交互"""
needs_interaction = state.get("needs_interaction", False)
return "interact" if needs_interaction else "generate"
class SingleChapterAgentBuilder(AgentBuilder):
"""单章节Agent构建器"""
@classmethod
def create(cls, interaction_handler=None) -> "SingleChapterAgentBuilder":
"""创建单章节Agent构建器"""
builder = cls(interaction_handler)
# 添加节点去掉PrepareChapterNode因为外层已准备好
builder.add_node(InteractWithUserNode()) \
.add_node(GenerateContentNode()) \
.add_node(SaveToWordNode())
# 设置入口interact_user内部会判断是否真的需要交互
builder.set_entry("interact_user")
# 配置流程:线性流程
builder.add_edge("interact_user", "generate_content")
builder.add_edge("generate_content", "save_to_word")
builder.add_edge("save_to_word", "END")
return builder
class SingleChapterAgent(BaseAgent):
"""单章节处理Agent"""
def __init__(self, interaction_handler=None):
builder = SingleChapterAgentBuilder.create(interaction_handler)
super().__init__(builder)
def process_chapter(self, chapter: Dict[str, Any], state: Dict[str, Any]) -> Dict[str, Any]:
"""处理单个章节
Args:
chapter: 章节信息
state: 全局状态包含word_file等
Returns:
处理结果
"""
# 准备单章节状态
chapter_state = {
"word_file": state["word_file"],
"current_chapter": chapter,
"current_chapter_index": state.get("current_chapter_index", 0),
"chapter_queue": state.get("chapter_queue", []),
"chapter_configs": state.get("chapter_configs", {}),
"generated_contents": state.get("generated_contents", {}),
"completed_chapters": state.get("completed_chapters", []),
"needs_interaction": chapter["level"] == 1,
"interaction_handler": state.get("interaction_handler"),
}
# 执行
result_state = self.execute_sync(chapter_state)
return result_state

View File

@ -7,10 +7,11 @@ import logging
import click
from .assemble import assemble
from .generate import generate
from .kb import kb
from .project import project
from .generate import generate
from .assemble import assemble
from .write import write
@click.group()
@ -31,6 +32,7 @@ def cli():
cli.add_command(kb, name="kb")
cli.add_command(project, name="project")
cli.add_command(generate, name="generate")
cli.add_command(write, name="write")
cli.add_command(assemble, name="assemble")
@ -46,6 +48,9 @@ def info():
print(" status - 查看项目状态")
print(" generate - 内容生成")
print(" task - 生成任务内容")
print(" write - 内容填写")
print(" start - 开始填写标书内容")
print(" chapter - 填写指定章节")
print(" assemble - 合规组装")
print(" tables - 生成表格")
print(" info - 显示系统信息")

181
src/bidmaster/cli/write.py Normal file
View File

@ -0,0 +1,181 @@
"""内容填写命令
提供标书内容自动填写功能
"""
import logging
from pathlib import Path
import click
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from ..agents import ContentWriterAgent
console = Console()
logger = logging.getLogger(__name__)
@click.group()
def write():
"""内容填写命令组"""
pass
@write.command()
@click.argument(
"project_dir",
type=click.Path(exists=True),
default=".",
)
@click.option(
"--mode",
"-m",
type=click.Choice(["interactive", "silent"]),
default="interactive",
help="交互模式interactive(交互)|silent(静默)",
)
def start(project_dir: str, mode: str):
"""开始填写标书内容
PROJECT_DIR: 项目目录路径默认当前目录
示例
bidmaster write start # 在当前目录
bidmaster write start ./my-project # 指定项目目录
bidmaster write start ./my-project -m silent # 静默模式
深度优先遍历章节仅在1级标题时与用户交互
"""
try:
console.print("📝 BidMaster 内容填写工具", style="bold blue")
console.print()
project_path = Path(project_dir)
if not project_path.exists():
console.print(f"❌ 项目目录不存在: {project_dir}", style="red")
return
console.print(f"📁 项目目录: {project_path.absolute()}", style="dim")
# 自动查找Word文件
word_file = _find_word_file(project_path)
if not word_file:
console.print("❌ 未找到Word文档", style="red")
return
console.print(f"📄 找到Word文档: {Path(word_file).name}", style="green")
console.print(f"📋 将直接从Word文档提取章节结构", style="dim")
# 创建Agent
if mode == "silent":
agent = ContentWriterAgent.create_silent()
console.print("📌 使用静默模式(自动选择默认值)", style="dim")
else:
agent = ContentWriterAgent.create_interactive()
console.print("📌 使用交互模式", style="dim")
console.print()
console.print("🚀 开始内容填写流程...", style="blue")
if mode == "interactive":
console.print("💡 提示仅在1级标题时会询问您的意见", style="yellow")
console.print()
# 执行内容填写
# 注意交互模式不使用Progress避免干扰用户输入
if mode == "silent":
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("正在处理章节...", total=None)
result = agent.write_content_sync(word_file=word_file)
progress.update(task, completed=True)
else:
# 交互模式不使用Progress避免覆盖用户输入
result = agent.write_content_sync(word_file=word_file)
# 显示结果
if result.get("success"):
console.print("\n✅ 内容填写完成!", style="bold green")
stats_panel = Panel(
f"📊 总章节数: {result.get('total_chapters', 0)}\n"
f"✓ 完成章节: {result.get('completed_count', 0)}\n"
f"📝 总字数: {result.get('total_words', 0)}\n"
f"📄 输出文件: {result.get('word_file')}",
title="生成统计",
title_align="left",
)
console.print("\n", stats_panel)
console.print(f"\n🎯 下一步:", style="dim")
console.print(f" • 打开 {Path(result['word_file']).name} 查看生成的内容", style="dim")
console.print(f" • 使用 Ctrl+A -> F9 刷新Word中的目录和页码", style="dim")
else:
console.print("\n❌ 内容填写失败", style="red")
except Exception as e:
console.print(f"\n❌ 执行失败: {e}", style="red")
logger.exception("内容填写失败")
@write.command()
@click.argument("chapter_id", type=str)
@click.option(
"--word-file",
"-w",
type=click.Path(exists=True),
help="Word文档路径",
)
@click.option(
"--project-dir",
"-p",
default=".",
help="项目目录路径",
)
def chapter(chapter_id: str, word_file: str, project_dir: str):
"""填写指定章节
示例bidmaster write chapter "1.1"
"""
console.print(f"📝 填写章节: {chapter_id}", style="bold blue")
console.print("\n⚠️ 此功能尚未实现,请使用 'write start' 命令", style="yellow")
console.print(
"提示:'write start' 会遍历所有章节您可以在1级标题时选择性跳过", style="dim"
)
def _find_word_file(project_path: Path) -> str:
"""在项目目录中查找Word文档
Args:
project_path: 项目目录
Returns:
Word文档路径未找到返回None
"""
# 查找.docx文件
docx_files = list(project_path.glob("*.docx"))
# 过滤掉临时文件(~$开头)
docx_files = [f for f in docx_files if not f.name.startswith("~$")]
if len(docx_files) == 1:
return str(docx_files[0])
elif len(docx_files) > 1:
console.print(f"\n⚠️ 找到多个Word文档:", style="yellow")
for i, f in enumerate(docx_files, 1):
console.print(f" {i}. {f.name}")
console.print("\n提示:将只有一个.docx文件放在项目目录中", style="yellow")
return None
else:
return None
if __name__ == "__main__":
write()

View File

@ -0,0 +1,20 @@
"""内容填写节点模块
提供标书内容填写的完整工作流节点
"""
from .init_config import InitConfigNode
from .prepare_chapter import PrepareChapterNode
from .interact_user import InteractWithUserNode
from .generate_content import GenerateContentNode
from .save_to_word import SaveToWordNode
from .collect_results import CollectResultsNode
__all__ = [
"InitConfigNode",
"PrepareChapterNode",
"InteractWithUserNode",
"GenerateContentNode",
"SaveToWordNode",
"CollectResultsNode",
]

View File

@ -0,0 +1,65 @@
"""结果收集节点
汇总所有章节的生成结果和统计信息
"""
import logging
from typing import Any, Dict
from ..base import BaseNode, NodeContext
logger = logging.getLogger(__name__)
class CollectResultsNode(BaseNode):
"""结果收集节点
职责
1. 汇总所有已完成章节
2. 统计生成结果
3. 返回最终状态
"""
@property
def name(self) -> str:
return "collect_results"
@property
def description(self) -> str:
return "收集并汇总生成结果"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行结果收集
Args:
state: 当前状态
context: 执行上下文
Returns:
更新后的状态
"""
completed_chapters = state.get("completed_chapters", [])
chapter_queue = state.get("chapter_queue", [])
generated_contents = state.get("generated_contents", {})
total_chapters = len(chapter_queue)
completed_count = len(completed_chapters)
# 统计字数
total_words = sum(len(content) for content in generated_contents.values())
logger.info(
f"内容生成完成!完成章节: {completed_count}/{total_chapters}, 总字数: {total_words}"
)
# 构建结果摘要
results_summary = {
"success": True,
"total_chapters": total_chapters,
"completed_count": completed_count,
"total_words": total_words,
"completed_chapters": completed_chapters,
"word_file": state.get("word_file"),
}
return self._update_state(state, should_continue=False, results=results_summary)

View File

@ -0,0 +1,211 @@
"""内容生成节点
调用RAG工具生成章节内容
"""
import logging
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext
logger = logging.getLogger(__name__)
class GenerateContentNode(BaseNode):
"""内容生成节点
职责
1. 获取当前章节配置如果是子章节继承父章节配置
2. 调用RAGTool生成内容
3. 保存生成结果到state
"""
@property
def name(self) -> str:
return "generate_content"
@property
def description(self) -> str:
return "生成章节内容"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行内容生成
Args:
state: 当前状态
context: 执行上下文
Returns:
更新后的状态
"""
chapter = state.get("current_chapter", {})
chapter_id = chapter.get("id")
chapter_title = chapter.get("title")
if not chapter_id:
raise ValueError("当前章节信息缺失")
logger.info(f"开始生成章节内容: {chapter_id} - {chapter_title}")
# 获取章节配置(继承父章节)
config = self._get_chapter_config(state, chapter)
# 查找当前章节的所有3级子标题
sub_chapters = self._find_sub_chapters(state, chapter_id, max_level=3)
if sub_chapters:
# 有子标题:逐个生成后拼接
logger.info(f"章节 {chapter_id} 包含 {len(sub_chapters)} 个子标题,逐个生成")
content_parts = []
for sub in sub_chapters:
sub_content = self._generate_with_rag(sub, config, state)
content_parts.append(f"## {sub['title']}\n\n{sub_content}")
content = "\n\n".join(content_parts)
else:
# 无子标题:直接生成
content = self._generate_with_rag(chapter, config, state)
# 保存生成结果
state.setdefault("generated_contents", {})[chapter_id] = content
logger.info(f"章节内容生成完成: {chapter_id}, 长度: {len(content)}")
return state
def _get_chapter_config(
self, state: Dict[str, Any], chapter: Dict[str, Any]
) -> Dict[str, Any]:
"""获取章节配置,子章节继承父章节配置
Args:
state: 当前状态
chapter: 当前章节
Returns:
章节配置
"""
chapter_id = chapter["id"]
chapter_configs = state.get("chapter_configs", {})
# 如果当前章节有配置,直接返回
if chapter_id in chapter_configs:
return chapter_configs[chapter_id]
# 查找父章节配置
parent_id = chapter.get("parent_id")
while parent_id:
if parent_id in chapter_configs:
logger.info(f"章节 {chapter_id} 继承父章节 {parent_id} 的配置")
return chapter_configs[parent_id]
# 继续向上查找
parent_chapter = self._find_chapter(state, parent_id)
parent_id = parent_chapter.get("parent_id") if parent_chapter else None
# 返回默认配置
logger.info(f"章节 {chapter_id} 使用默认配置")
return {"emphasis": "", "rag_enabled": False, "rag_store": None}
def _find_chapter(self, state: Dict[str, Any], chapter_id: str) -> Optional[Dict[str, Any]]:
"""在队列中查找章节
Args:
state: 当前状态
chapter_id: 章节ID
Returns:
章节信息或None
"""
queue = state.get("chapter_queue", [])
for chapter in queue:
if chapter["id"] == chapter_id:
return chapter
return None
def _find_sub_chapters(
self, state: Dict[str, Any], parent_id: str, max_level: int = 3
) -> list:
"""查找指定章节的所有子标题最深到3级
Args:
state: 当前状态
parent_id: 父章节ID
max_level: 最大层级默认3级
Returns:
子章节列表
"""
queue = state.get("chapter_queue", [])
parent = self._find_chapter(state, parent_id)
if not parent:
return []
parent_level = parent.get("level", 1)
target_level = parent_level + 1
# 只查找直接子章节比父章节深1层且不超过max_level
sub_chapters = []
for chapter in queue:
if (
chapter.get("parent_id") == parent_id
and chapter.get("level") == target_level
and chapter.get("level") <= max_level
):
sub_chapters.append(chapter)
return sub_chapters
def _generate_with_rag(
self, chapter: Dict[str, Any], config: Dict[str, Any], state: Dict[str, Any]
) -> str:
"""使用RAG生成内容
Args:
chapter: 章节信息
config: 章节配置
state: 当前状态
Returns:
生成的内容
"""
from ...tools.rag import RAGTool
rag_tool = RAGTool()
# 构建生成上下文
generation_context = {
"title": chapter["title"],
"level": chapter["level"],
"requirements": chapter.get("requirements", ""),
"emphasis": config.get("emphasis", ""),
}
# 如果启用RAG添加上下文信息
if config.get("rag_enabled"):
# 检索相关内容
query = f"{chapter['title']} {config.get('emphasis', '')}"
search_results = rag_tool.search(query, k=3)
if search_results:
relevant_context = "\n\n".join([r["content"] for r in search_results])
generation_context["rag_context"] = relevant_context
logger.info(f"RAG检索到 {len(search_results)} 条相关内容")
else:
logger.warning("RAG未检索到相关内容")
generation_context["rag_context"] = ""
else:
generation_context["rag_context"] = ""
# 添加父章节上下文
parent_context = state.get("last_generated_content", "")
if parent_context and chapter["level"] > 1:
generation_context["parent_context"] = parent_context[:500] # 限制长度
# 调用生成方法
try:
content = rag_tool.generate_content(chapter["id"], generation_context)
return content
except Exception as e:
logger.error(f"内容生成失败: {e}")
# 立即失败,不提供后备方案
raise

View File

@ -0,0 +1,206 @@
"""初始化配置节点
读取Word文档结构并生成深度优先的章节队列
"""
import json
import logging
from pathlib import Path
from typing import Any, Dict, List
from ..base import BaseNode, NodeContext
logger = logging.getLogger(__name__)
class InitConfigNode(BaseNode):
"""初始化配置节点
职责
1. 读取Word文档章节结构从tasks.json或analysis_result.json
2. 深度优先遍历生成章节队列
3. 初始化全局配置
"""
@property
def name(self) -> str:
return "init_config"
@property
def description(self) -> str:
return "初始化配置并生成章节队列"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行初始化配置
Args:
state: 当前状态
context: 执行上下文
Returns:
更新后的状态
"""
word_file = state.get("word_file")
if not word_file:
raise ValueError("缺少word_file参数")
word_path = Path(word_file)
if not word_path.exists():
raise FileNotFoundError(f"Word文档不存在: {word_file}")
# 直接从Word文档读取章节结构
chapters = self._load_from_word_document(word_file)
# 深度优先遍历生成队列
chapter_queue = self._build_depth_first_queue(chapters)
logger.info(f"生成章节队列,共{len(chapter_queue)}个章节")
return self._update_state(
state,
chapter_queue=chapter_queue,
chapter_configs={},
generated_contents={},
completed_chapters=[],
current_chapter_index=0,
)
def _load_from_word_document(self, word_file: str) -> List[Dict[str, Any]]:
"""从Word文档读取章节结构
Args:
word_file: Word文档路径
Returns:
章节列表
"""
from docx import Document
import re
doc = Document(word_file)
chapters = []
parent_stack = [] # 栈存储各层级的父章节ID
for para in doc.paragraphs:
style_name = para.style.name
# 只处理Heading 1/2/3
if not style_name.startswith('Heading'):
continue
# 提取层级
level_match = re.match(r'Heading\s+(\d+)', style_name)
if not level_match:
continue
level = int(level_match.group(1))
if level > 3: # 只处理到3级标题
continue
# 提取标题文本和编号
title_text = para.text.strip()
if not title_text:
continue
# 从标题中提取编号(如"2.1.3 标题" → "2_1_3"
number_match = re.match(r'^([\d\.]+)\s+(.+)$', title_text)
if number_match:
number_str = number_match.group(1).rstrip('.')
title = number_match.group(2)
chapter_id = f"chapter_{number_str.replace('.', '_')}"
else:
# 无编号,使用索引
chapter_id = f"chapter_{len(chapters) + 1}"
title = title_text
# 确定父章节ID
while parent_stack and parent_stack[-1]['level'] >= level:
parent_stack.pop()
parent_id = parent_stack[-1]['id'] if parent_stack else None
# 创建章节信息
chapter_info = {
"id": chapter_id,
"title": title,
"level": level,
"placeholder": f"{{{{{chapter_id}_content}}}}",
"requirements": "",
"score": 0,
"parent_id": parent_id,
}
chapters.append(chapter_info)
# 将当前章节加入栈
parent_stack.append({'id': chapter_id, 'level': level})
logger.info(f"从Word文档读取到 {len(chapters)} 个章节")
return chapters
def _build_depth_first_queue(self, chapters: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""深度优先遍历生成章节队列
Args:
chapters: 章节列表
Returns:
深度优先排序的章节队列
"""
# 构建章节树
chapter_map = {ch["id"]: ch for ch in chapters}
# 找出根节点1级标题
root_chapters = [ch for ch in chapters if ch["level"] == 1]
# 深度优先遍历
queue = []
for root in root_chapters:
self._dfs_traverse(root, chapter_map, queue, chapters)
return queue
def _extract_chapter_numbers(self, chapter_id: str) -> List[int]:
"""从章节ID提取数字编号用于排序
Args:
chapter_id: 章节ID "chapter_1_2_3"
Returns:
数字列表 [1, 2, 3]
"""
import re
# 提取所有数字
numbers = re.findall(r'\d+', chapter_id)
return [int(n) for n in numbers]
def _dfs_traverse(
self,
chapter: Dict[str, Any],
chapter_map: Dict[str, Dict],
queue: List[Dict],
all_chapters: List[Dict],
) -> None:
"""深度优先遍历
Args:
chapter: 当前章节
chapter_map: 章节映射
queue: 结果队列
all_chapters: 所有章节列表
"""
queue.append(chapter)
# 查找子章节
chapter_id = chapter["id"]
children = [ch for ch in all_chapters if ch.get("parent_id") == chapter_id]
# 按数字编号排序确保正确顺序(修复:避免字符串排序导致 1_10 < 1_2
children.sort(key=lambda x: self._extract_chapter_numbers(x["id"]))
# 递归遍历子章节
for child in children:
self._dfs_traverse(child, chapter_map, queue, all_chapters)

View File

@ -0,0 +1,147 @@
"""用户交互节点
与用户交互获取章节填写要求和RAG配置
"""
import logging
from typing import Any, Dict
from ..base import BaseNode, NodeContext
logger = logging.getLogger(__name__)
class InteractWithUserNode(BaseNode):
"""用户交互节点
职责
1. 询问用户该章节需要强调的内容
2. 询问是否使用RAG知识库
3. 如果使用RAG选择具体的知识库
4. 保存配置供后续子章节继承
"""
@property
def name(self) -> str:
return "interact_user"
@property
def description(self) -> str:
return "与用户交互获取章节配置"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行用户交互
Args:
state: 当前状态
context: 执行上下文
Returns:
更新后的状态
"""
chapter = state.get("current_chapter", {})
chapter_id = chapter.get("id")
chapter_title = chapter.get("title")
if not chapter_id:
raise ValueError("当前章节信息缺失")
# 检查是否需要交互
needs_interaction = state.get("needs_interaction", False)
if not needs_interaction:
# 不需要交互,直接返回
return state
# 获取交互处理器
interaction_handler = state.get("interaction_handler")
if not interaction_handler:
# 无交互处理器时使用默认值(静默模式)
logger.info("无交互处理器,使用默认配置")
config = {"emphasis": "", "rag_enabled": False, "rag_store": None}
state.setdefault("chapter_configs", {})[chapter_id] = config
return state
logger.info(f"开始与用户交互,获取章节配置: {chapter_id} - {chapter_title}")
# 1. 询问需要强调的内容
emphasis = interaction_handler(
interaction_type="text",
prompt=f"📝 正在处理章节:{chapter_id}. {chapter_title}\n"
f" 是否有需要特别强调的内容?(直接回车跳过)",
default="",
key=f"emphasis_{chapter_id}",
)
# 2. 询问是否使用RAG
use_rag_response = interaction_handler(
interaction_type="choice",
prompt="是否使用RAG知识库辅助生成内容",
options=["", ""],
default="",
key=f"use_rag_{chapter_id}",
)
use_rag = use_rag_response == ""
rag_store = None
# 3. 如果使用RAG选择知识库
if use_rag:
rag_store = self._select_rag_store(interaction_handler, chapter_id)
# 保存章节配置
config = {
"emphasis": emphasis.strip() if emphasis else "",
"rag_enabled": use_rag,
"rag_store": rag_store,
}
state.setdefault("chapter_configs", {})[chapter_id] = config
logger.info(
f"章节配置已保存: 强调内容={'' if config['emphasis'] else ''}, "
f"RAG={'启用' if use_rag else '禁用'}"
)
return state
def _select_rag_store(self, interaction_handler, chapter_id: str) -> str:
"""选择RAG知识库
Args:
interaction_handler: 交互处理器
chapter_id: 章节ID
Returns:
知识库标识
"""
# 获取可用的知识库列表
from ...tools.rag import RAGTool
try:
rag_tool = RAGTool()
stats = rag_tool.get_stats()
total_chunks = stats.get("total_chunks", 0)
total_files = stats.get("total_files", 0)
# 显示知识库统计信息
store_info = f"默认知识库 ({total_files}个文档, {total_chunks}个文档块)"
choice = interaction_handler(
interaction_type="choice",
prompt=f"选择知识库",
options=[
("default", store_info),
("none", "不使用RAG")
],
default="default",
key=f"rag_store_{chapter_id}",
)
if choice == "default":
return "default"
return None
except Exception as e:
logger.warning(f"获取知识库信息失败: {e},使用默认配置")
return "default"

View File

@ -0,0 +1,60 @@
"""准备章节节点
从队列中取出当前待处理章节并设置上下文
"""
import logging
from typing import Any, Dict
from ..base import BaseNode, NodeContext
logger = logging.getLogger(__name__)
class PrepareChapterNode(BaseNode):
"""准备章节节点
职责
1. 从队列取当前章节
2. 设置当前处理上下文
3. 判断是否需要用户交互仅1级标题需要
"""
@property
def name(self) -> str:
return "prepare_chapter"
@property
def description(self) -> str:
return "准备当前待处理章节"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行准备章节逻辑
Args:
state: 当前状态
context: 执行上下文
Returns:
更新后的状态
"""
index = state.get("current_chapter_index", 0)
queue = state.get("chapter_queue", [])
if index >= len(queue):
logger.info("所有章节已处理完成")
return self._update_state(state, should_continue=False)
current_chapter = queue[index]
logger.info(
f"准备处理章节 [{index + 1}/{len(queue)}]: "
f"{current_chapter['id']} - {current_chapter['title']}"
)
# 判断是否需要用户交互仅1级标题
needs_interaction = current_chapter["level"] == 1
return self._update_state(
state, current_chapter=current_chapter, needs_interaction=needs_interaction
)

View File

@ -0,0 +1,110 @@
"""保存到Word节点
将生成的内容填充到Word文档并保存
"""
import logging
from pathlib import Path
from typing import Any, Dict
from ..base import BaseNode, NodeContext
logger = logging.getLogger(__name__)
class SaveToWordNode(BaseNode):
"""保存到Word节点
职责
1. 将生成的内容填充到Word文档对应占位符
2. 立即保存Word文件
3. 更新处理进度
"""
@property
def name(self) -> str:
return "save_to_word"
@property
def description(self) -> str:
return "保存内容到Word文档"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行保存操作
Args:
state: 当前状态
context: 执行上下文
Returns:
更新后的状态
"""
chapter = state.get("current_chapter", {})
chapter_id = chapter.get("id")
word_file = state.get("word_file")
if not chapter_id or not word_file:
raise ValueError("缺少必要参数: chapter_id或word_file")
# 获取生成的内容
generated_contents = state.get("generated_contents", {})
content = generated_contents.get(chapter_id)
if not content:
raise ValueError(f"章节 {chapter_id} 的内容未生成")
logger.info(f"开始保存章节内容到Word: {chapter_id} - {chapter['title']}")
# 填充到Word文档
self._fill_word_document(word_file, chapter, content)
# 更新状态直接修改state确保LangGraph正确传递
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上更新确保下次循环能读取到最新值
state["completed_chapters"] = completed_chapters
state["current_chapter_index"] = current_index + 1
state["last_generated_content"] = content
state["current_step"] = self.name
return state
def _fill_word_document(
self, word_file: str, chapter: Dict[str, Any], content: str
) -> None:
"""填充Word文档
Args:
word_file: Word文档路径
chapter: 章节信息
content: 生成的内容
"""
from ...tools.word import WordProcessor
word_path = Path(word_file)
if not word_path.exists():
raise FileNotFoundError(f"Word文档不存在: {word_file}")
word_processor = WordProcessor()
# 获取占位符
placeholder = chapter.get("placeholder")
if not placeholder:
# 如果没有占位符使用章节ID构建默认占位符
placeholder = f"{{{{{chapter['id']}_content}}}}"
try:
# 填充占位符
word_processor.fill_placeholder(str(word_path), placeholder, content)
logger.info(f"成功填充占位符: {placeholder}")
except Exception as e:
logger.error(f"填充Word文档失败: {e}")
# 立即失败
raise

View File

@ -192,34 +192,48 @@ class RAGTool:
Returns:
生成的内容
"""
from openai import OpenAI
# 从上下文中提取任务信息
task_title = context.get('title', '任务')
task_requirements = context.get('requirements', '')
emphasis = context.get('emphasis', '')
rag_context = context.get('rag_context', '')
# 搜索相关内容
search_results = self.search(task_requirements or task_title, k=3)
# 构建提示词
emphasis_part = f'\n特别强调:{emphasis}' if emphasis else ''
rag_part = f'\n\n参考资料:\n{rag_context}' if rag_context else ''
# 构建提示
relevant_content = "\n".join([r['content'] for r in search_results])
prompt = f"""你是一个专业的标书撰写助手。请根据以下信息生成标书章节内容:
# 直接生成内容
generated_content = f"""# {task_title}
章节标题{task_title}
评分要求{task_requirements}{emphasis_part}{rag_part}
## 概述
根据招标要求和知识库内容本章节详细阐述实施方案
要求
1. 内容专业详实符合招标文件要求
2. 突出技术优势和实施能力
3. 语言正式逻辑清晰
4. 字数控制在500-800
## 相关参考
{relevant_content[:500] if relevant_content else '暂无相关参考内容'}
请直接输出章节内容不要包含章节标题"""
## 实施方案
1. 技术实现
2. 质量保证
3. 进度安排
4. 资源配置
# 调用DeepSeek生成
client = OpenAI(
api_key=self.settings.api_key,
base_url=self.settings.base_url
)
## 总结
本方案充分考虑了各项要求确保项目顺利实施
"""
response = client.chat.completions.create(
model=self.settings.model_name,
messages=[
{"role": "system", "content": "你是一个专业的标书撰写助手。"},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=2000
)
generated_content = response.choices[0].message.content.strip()
return generated_content

View File

@ -128,6 +128,288 @@ class WordProcessor:
pattern = r'^(\d+(?:\.\d+)*\.?\s+)'
return re.sub(pattern, '', title.strip())
def fill_placeholder(self, doc_path: str, placeholder: str, content: str) -> bool:
"""填充Word文档中的占位符
Args:
doc_path: Word文档路径
placeholder: 占位符文本 {{chapter_01_content}}
content: 要填充的内容
Returns:
是否成功填充
"""
try:
doc = Document(doc_path)
replaced = False
# 策略1: 查找并替换占位符
for paragraph in doc.paragraphs:
if placeholder in paragraph.text:
# 保留段落格式的替换方法
# 保存段落级别的格式属性
para_format = paragraph.paragraph_format
# 清空段落内容但保留段落本身
for run in paragraph.runs:
run.text = ''
# 添加新内容保留第一个run的格式如果有的话
if paragraph.runs:
paragraph.runs[0].text = content
else:
paragraph.add_run(content)
replaced = True
logger.info(f"已替换占位符: {placeholder}")
break
# 策略2: 降级处理 - 如果没有占位符,在对应标题后插入内容
if not replaced:
logger.warning(f"未找到占位符: {placeholder},尝试在对应标题后插入内容")
replaced = self._insert_after_heading(doc, placeholder, content)
# 保存文档
doc.save(doc_path)
return replaced
except Exception as e:
logger.error(f"填充占位符失败: {e}")
raise
def _parse_markdown_to_paragraphs(self, content: str, chapter_level: int) -> list:
"""解析Markdown内容为段落结构列表
Args:
content: Markdown格式的内容
chapter_level: 当前章节层级1/2/3
Returns:
段落结构列表每项包含 {'type': 'heading'|'paragraph'|'list', 'text': str, 'style': str}
"""
import re
paragraphs = []
lines = content.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
if not line:
i += 1
continue
# Markdown标题## 标题
if re.match(r'^#{2,4}\s+', line):
level = len(re.match(r'^(#+)', line).group(1))
title_text = re.sub(r'^#{2,4}\s+', '', line)
# 3级标题内容## → 加粗段落(非标题样式)
if chapter_level == 3:
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
})
# 有序列表1. / 1) / 1
elif re.match(r'^(\d+[\.)]|\d+)\s+', line):
text = re.sub(r'^(\d+[\.)]|\d+)\s+', '', line)
paragraphs.append({
'type': 'ordered_list',
'text': text
})
# 无序列表:- / * / •
elif re.match(r'^[-*•]\s+', line):
text = re.sub(r'^[-*•]\s+', '', line)
paragraphs.append({
'type': 'unordered_list',
'text': text
})
# 普通段落
else:
# 处理行内格式:**粗体**
text = re.sub(r'\*\*(.+?)\*\*', r'\1', line)
# 处理行内格式:*斜体*(单星号,但避免误匹配列表)
if not line.startswith('*'):
text = re.sub(r'\*(.+?)\*', r'\1', text)
paragraphs.append({
'type': 'paragraph',
'text': text
})
i += 1
return paragraphs
def _insert_after_heading(self, doc: Document, placeholder: str, content: str) -> bool:
"""在对应标题后插入内容(降级策略)
Args:
doc: Word文档对象
placeholder: 占位符 {{chapter_2_1_1_content}}
content: 要插入的内容
Returns:
是否成功插入
"""
import re
# 从占位符提取章节编号: {{chapter_2_1_1_content}} -> "2.1.1"
match = re.search(r'chapter_(\d+(?:_\d+)*)', placeholder)
if not match:
logger.error(f"无法从占位符解析章节编号: {placeholder}")
return False
chapter_number = match.group(1).replace('_', '.')
# 查找对应编号的标题
target_paragraph = None
target_index = -1
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}."):
target_paragraph = para
target_index = i
logger.info(f"找到匹配标题: {text}")
break
if target_paragraph is None:
logger.error(f"未找到编号为 {chapter_number} 的标题")
return False
# 获取章节层级(从标题样式推断)
chapter_level = 3 # 默认3级
if target_paragraph.style.name == 'Heading 1':
chapter_level = 1
elif target_paragraph.style.name == 'Heading 2':
chapter_level = 2
elif target_paragraph.style.name == 'Heading 3':
chapter_level = 3
# 解析Markdown内容为段落结构
parsed_paragraphs = self._parse_markdown_to_paragraphs(content, chapter_level)
# 在标题后逐个插入段落
from docx.text.paragraph import Paragraph
from docx.shared import Pt, Cm
from docx.enum.text import WD_LINE_SPACING, WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
current_element = target_paragraph._element
inserted_count = 0
for para_struct in parsed_paragraphs:
para_type = para_struct['type']
text = para_struct['text']
# 创建新段落XML元素
new_p_element = OxmlElement('w:p')
current_element.addnext(new_p_element)
current_element = new_p_element
# 创建Paragraph对象
new_para = Paragraph(new_p_element, doc)
para_format = new_para.paragraph_format
# 根据类型应用不同样式
if para_type == 'heading':
# 子标题使用Heading样式
level = para_struct.get('level', 4)
try:
new_para.style = f'Heading {level}'
except:
run = new_para.add_run(text)
run.bold = True
run.font.size = Pt(13)
else:
new_para.text = text
elif para_type == 'bold_paragraph':
# 加粗段落3级标题内的小标题
try:
new_para.style = 'Normal'
except:
pass
para_format.space_before = Pt(6)
para_format.space_after = Pt(3)
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
run = new_para.add_run(text)
run.bold = True
run.font.name = '黑体'
run.font.size = Pt(12)
elif para_type == 'ordered_list':
# 有序列表123
try:
new_para.style = 'Normal'
except:
pass
para_format.left_indent = Cm(0.74)
para_format.space_after = Pt(3)
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
# 手动添加编号(简化实现)
list_index = sum(1 for p in parsed_paragraphs[:parsed_paragraphs.index(para_struct)] if p['type'] == 'ordered_list') + 1
run = new_para.add_run(f"{list_index}{text}")
run.font.name = '宋体'
run.font.size = Pt(12)
elif para_type == 'unordered_list':
# 无序列表:• 或 ○
try:
new_para.style = 'Normal'
except:
pass
para_format.left_indent = Cm(0.74)
para_format.space_after = Pt(3)
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
run = new_para.add_run(f"{text}")
run.font.name = '宋体'
run.font.size = Pt(12)
else: # paragraph
# 普通段落
try:
new_para.style = 'Normal'
except:
pass
para_format.first_line_indent = Cm(0.74)
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
para_format.space_after = Pt(6)
run = new_para.add_run(text)
run.font.name = '宋体'
run.font.size = Pt(12)
inserted_count += 1
logger.info(f"成功在标题 {chapter_number} 后插入内容(共{inserted_count}个段落)")
return True
def _setup_numbering_styles(self, doc: Document):
"""配置标题的多级编号样式"""
# 确保标题样式存在并配置编号格式