1150 lines
47 KiB
Python
1150 lines
47 KiB
Python
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
import asyncio
|
|||
|
|
import uuid
|
|||
|
|
|
|||
|
|
from docxcompose.composer import Composer
|
|||
|
|
from docx import Document as Document_compose
|
|||
|
|
from docxtpl import DocxTemplate
|
|||
|
|
from typing import List, Optional
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
from datetime import datetime, date
|
|||
|
|
|
|||
|
|
from fastapi import HTTPException
|
|||
|
|
from md2docx.converter import MarkdownToDocx
|
|||
|
|
from sqlalchemy import select, func, update, delete
|
|||
|
|
from agents.ai_agents.task_hub.schemas.task_schema import ProjectTaskRecordCreate, TaskTypeEnum, TaskStatusEnum
|
|||
|
|
from agents.ai_agents.task_hub.services.task_service import async_create_or_update_task_record_service
|
|||
|
|
from agents.config.database import AsyncSessionLocal
|
|||
|
|
from agents.interface.service.project_service import async_get_project_by_id_service
|
|||
|
|
from agents.utils.document_parser import DocumentParser
|
|||
|
|
|
|||
|
|
from agents.ai_agents.document_agent.schemas.document_schema import DocumentGenerationRequest, DocumentCategoryEnum, \
|
|||
|
|
DocumentSSEData,GenerateCheckRoleRequest
|
|||
|
|
from agents.ai_agents.document_agent.services.document_service import DOCUMENT_TYPES
|
|||
|
|
from agents.interface.schemas.project_file_link_schema import FileLinkSchema, CreateProjectFileLinkSchema
|
|||
|
|
from agents.interface.service.project_file_link_service import async_create_file_link
|
|||
|
|
from agents.models.base.project_model import FileEntity
|
|||
|
|
from agents.utils.minio_client import minio_client
|
|||
|
|
from agents.utils.openai_client import get_open_ai_client
|
|||
|
|
from agents.ai_agents.document_agent.services.code_parser_service import CodeParserService
|
|||
|
|
from agents.utils.run_command import run_command
|
|||
|
|
from agents.ai_agents.prompt_template.models.prompt_template_model import ProjectTemplateEntity
|
|||
|
|
from agents.ai_agents.prompt_template.services.project_template_custom_service import ProjectTemplateCustomService
|
|||
|
|
from agents.ai_agents.test_verification_agent.services.task_service import TaskService
|
|||
|
|
from agents.ai_agents.diagram_agent.utils.drawio_utils import replace_drawio_host
|
|||
|
|
|
|||
|
|
|
|||
|
|
_STREAM_END = object()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _next_stream_chunk(iterator):
|
|||
|
|
try:
|
|||
|
|
return next(iterator)
|
|||
|
|
except StopIteration:
|
|||
|
|
return _STREAM_END
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _iterate_chat_completion_stream(openai_client, messages):
|
|||
|
|
"""
|
|||
|
|
The OpenAI wrapper exposes a blocking sync generator. Running next() in a
|
|||
|
|
worker thread keeps concurrent SSE requests from blocking the event loop.
|
|||
|
|
"""
|
|||
|
|
iterator = openai_client.chat_completion_stream([dict(message) for message in messages])
|
|||
|
|
while True:
|
|||
|
|
chunk = await asyncio.to_thread(_next_stream_chunk, iterator)
|
|||
|
|
if chunk is _STREAM_END:
|
|||
|
|
break
|
|||
|
|
yield chunk
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DocumentGenerationService:
|
|||
|
|
"""文档生成服务类"""
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def load_template_prompt(cls, file_name: str) -> str:
|
|||
|
|
"""
|
|||
|
|
加载系统提示词模板
|
|||
|
|
|
|||
|
|
:return: 系统提示词模板内容
|
|||
|
|
"""
|
|||
|
|
template_path = os.path.join(
|
|||
|
|
os.path.dirname(os.path.dirname(__file__)),
|
|||
|
|
"templates",
|
|||
|
|
file_name
|
|||
|
|
)
|
|||
|
|
with open(template_path, 'r', encoding='utf-8') as f:
|
|||
|
|
return f.read()
|
|||
|
|
|
|||
|
|
async def generate_stream_event_check_role(
|
|||
|
|
request: GenerateCheckRoleRequest,
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
根据项目文档模板id,
|
|||
|
|
获取到模板信息,
|
|||
|
|
调用ai生成校验规则文档
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 调用OpenAI流式生成
|
|||
|
|
full_content = ""
|
|||
|
|
try:
|
|||
|
|
project_template = await async_get_project_template_info(request.templateId)
|
|||
|
|
project = await async_get_project_by_id_service(project_template.project_id)
|
|||
|
|
content = project_template.content
|
|||
|
|
name = project_template.name
|
|||
|
|
file_name = name + "校验规则"
|
|||
|
|
prompt = load_check_role_prompt(content, name)
|
|||
|
|
messages = [
|
|||
|
|
{"role": "system", "content": prompt},
|
|||
|
|
{"role": "user", "content": ""}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
start_event = DocumentSSEData(
|
|||
|
|
eventType="start",
|
|||
|
|
message=f"开始生成{file_name}",
|
|||
|
|
documentName=None
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(start_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
openai_client = get_open_ai_client(project.api_key, project.base_url, project.model_name)
|
|||
|
|
async for chunk in _iterate_chat_completion_stream(openai_client, messages):
|
|||
|
|
full_content += chunk
|
|||
|
|
# 发送内容块事件
|
|||
|
|
content_event = DocumentSSEData(
|
|||
|
|
eventType="document_generating",
|
|||
|
|
message=chunk,
|
|||
|
|
documentName=file_name
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(content_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="end",
|
|||
|
|
message="校验规则生成成功",
|
|||
|
|
documentName=file_name
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="error",
|
|||
|
|
message=f"文档生成失败, 异常信息:{str(e)}",
|
|||
|
|
documentName=file_name
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def generate_stream_event_generator(
|
|||
|
|
request: DocumentGenerationRequest,
|
|||
|
|
# db: Session
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
SSE流式事件生成器
|
|||
|
|
|
|||
|
|
:param request: 文档生成请求
|
|||
|
|
:param db: 数据库会话
|
|||
|
|
:return: SSE事件流
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
generation_request_id = uuid.uuid4().hex
|
|||
|
|
project = await async_get_project_by_id_service(request.projectId)
|
|||
|
|
if not project:
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="end",
|
|||
|
|
message="项目不存在",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
return
|
|||
|
|
# 3. 创建任务记录
|
|||
|
|
task_record_data = ProjectTaskRecordCreate(
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.RUNNING,
|
|||
|
|
task_log="开始生成文档"
|
|||
|
|
)
|
|||
|
|
task_record_result = await async_create_or_update_task_record_service(task_record_data)
|
|||
|
|
task_id = task_record_result.data.id
|
|||
|
|
try:
|
|||
|
|
# 发送开始事件
|
|||
|
|
start_event = DocumentSSEData(
|
|||
|
|
eventType="start",
|
|||
|
|
# message=f"开始生成 {len(request.produceDocumentList)} 个文档",
|
|||
|
|
message=f"开始生成 {len(request.projectTemplateIdList)} 个文档",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(start_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
# 获取参考文档内容
|
|||
|
|
reference_content = ""
|
|||
|
|
sys_data = ""
|
|||
|
|
if request.referToDocument is not None and len(request.referToDocument) > 0:
|
|||
|
|
analysis_start_event = DocumentSSEData(
|
|||
|
|
eventType="analysisDocumentStart",
|
|||
|
|
message="开始分析参考文档",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(analysis_start_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
# 使用非流式版本获取参考文档内容
|
|||
|
|
reference_content = await async_handle_refer_document(request, project)
|
|||
|
|
# 输出分析过程中的事件(模拟)
|
|||
|
|
analysis_complete_event = DocumentSSEData(
|
|||
|
|
eventType="analysisDocumentEnd",
|
|||
|
|
message="参考文档分析完成",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(analysis_complete_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
# 逐个生成文档
|
|||
|
|
save_document_list: List[FileLinkSchema] = []
|
|||
|
|
for index, project_template_id in enumerate(request.projectTemplateIdList, 1):
|
|||
|
|
# 获取文档模板信息
|
|||
|
|
project_template = await async_get_project_template_info(project_template_id)
|
|||
|
|
doc_name = project_template.name
|
|||
|
|
check_doc_name = doc_name + "校验报告"
|
|||
|
|
project_check_role = project_template.check_role
|
|||
|
|
project_template_content = project_template.content
|
|||
|
|
custom_template_content = await ProjectTemplateCustomService.get_latest_custom_content(
|
|||
|
|
request.projectId,
|
|||
|
|
project_template_id
|
|||
|
|
)
|
|||
|
|
if custom_template_content is not None:
|
|||
|
|
project_template_content = custom_template_content
|
|||
|
|
category = project_template.category
|
|||
|
|
# 发送文档开始事件
|
|||
|
|
doc_start_event = DocumentSSEData(
|
|||
|
|
eventType="document_start",
|
|||
|
|
message=f"开始生成文档: {doc_name}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(doc_start_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
# 获取编写提示词模板
|
|||
|
|
system_prompt = load_prompt(request, sys_data, doc_name, reference_content, project_template_content)
|
|||
|
|
|
|||
|
|
messages = [
|
|||
|
|
{"role": "system", "content": system_prompt},
|
|||
|
|
{"role": "user", "content": f"{request.userInput}"}
|
|||
|
|
]
|
|||
|
|
# 如果选择了仓库信息,可以将仓库先解析,之后再填充至仓库的解析结果
|
|||
|
|
if request.repoInfo is not None:
|
|||
|
|
if request.repoInfo.username and request.repoInfo.password:
|
|||
|
|
# 在URL中插入用户名和密码
|
|||
|
|
from urllib.parse import urlparse
|
|||
|
|
parsed = urlparse(request.repoInfo.url)
|
|||
|
|
auth_url = (f"{parsed.scheme}://{request.repoInfo.username}"
|
|||
|
|
f":{request.repoInfo.password}@{parsed.netloc}{parsed.path}")
|
|||
|
|
repo_url = auth_url
|
|||
|
|
coed_parser_service = CodeParserService()
|
|||
|
|
parse_result = coed_parser_service.parse_code_repository(
|
|||
|
|
repo_url=repo_url, branch=request.repoInfo.branch, use_doxygen=True)
|
|||
|
|
prompt = (coed_parser_service.build_architecture_prompt
|
|||
|
|
(parse_result=parse_result, project_name=project.name))
|
|||
|
|
messages.append({
|
|||
|
|
"role": "user", "content": f"工程代码信息如下:{prompt}"
|
|||
|
|
})
|
|||
|
|
# 调用OpenAI流式生成
|
|||
|
|
full_content = ""
|
|||
|
|
try:
|
|||
|
|
openai_client = get_open_ai_client(project.api_key, project.base_url, project.model_name)
|
|||
|
|
async for chunk in _iterate_chat_completion_stream(openai_client, messages):
|
|||
|
|
full_content += chunk
|
|||
|
|
# 发送内容块事件
|
|||
|
|
content_event = DocumentSSEData(
|
|||
|
|
eventType="document_generating",
|
|||
|
|
message=chunk,
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(content_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
except Exception as e:
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="error",
|
|||
|
|
message=f"文档生成失败: {doc_name}, 异常信息:{str(e)}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.ERROR,
|
|||
|
|
task_log=f"文档生成失败: {doc_name}, 异常信息:{str(e)}"
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 开始将文档转换为md格式
|
|||
|
|
convert_succeeded = False
|
|||
|
|
convert_failed = False
|
|||
|
|
for event in handle_conver_file(
|
|||
|
|
doc_name,
|
|||
|
|
full_content,
|
|||
|
|
project,
|
|||
|
|
save_document_list,
|
|||
|
|
request,
|
|||
|
|
category,
|
|||
|
|
generation_request_id
|
|||
|
|
):
|
|||
|
|
if event.eventType == "convert_complete":
|
|||
|
|
convert_succeeded = True
|
|||
|
|
elif event.eventType == "convert_error":
|
|||
|
|
convert_failed = True
|
|||
|
|
yield f"data: {json.dumps(event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
if convert_failed or not convert_succeeded:
|
|||
|
|
error_message = f"文档转换失败: {doc_name}"
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.ERROR,
|
|||
|
|
task_log=error_message
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="error",
|
|||
|
|
message=error_message,
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 发送文档完成事件
|
|||
|
|
doc_complete_event = DocumentSSEData(
|
|||
|
|
eventType="document_complete",
|
|||
|
|
message=f"文档生成完成: {doc_name}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.PENDING,
|
|||
|
|
task_log=f"文档生成完成: {doc_name}"
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
yield f"data: {json.dumps(doc_complete_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
# 文档校验,生成校验报告
|
|||
|
|
if request.check:
|
|||
|
|
if not project_check_role:
|
|||
|
|
check_start_error = DocumentSSEData(
|
|||
|
|
eventType="check_start_error",
|
|||
|
|
message=f"文档校验规则不存在: {doc_name}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(check_start_error.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
check_start = DocumentSSEData(
|
|||
|
|
eventType="check_start",
|
|||
|
|
message=f"开始生成校验报告: {check_doc_name}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(check_start.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
check_prompt = load_check_prompt(full_content, project_check_role, doc_name)
|
|||
|
|
check_messages = [
|
|||
|
|
{"role": "system", "content": check_prompt},
|
|||
|
|
{"role": "user", "content": "请按照规则生成校验报告"}
|
|||
|
|
]
|
|||
|
|
check_full_content = ""
|
|||
|
|
try:
|
|||
|
|
openai_client = get_open_ai_client(project.api_key, project.base_url, project.model_name)
|
|||
|
|
async for chunk in _iterate_chat_completion_stream(openai_client, check_messages):
|
|||
|
|
check_full_content += chunk
|
|||
|
|
# 发送内容块事件
|
|||
|
|
content_event = DocumentSSEData(
|
|||
|
|
eventType="document_checking",
|
|||
|
|
message=chunk,
|
|||
|
|
documentName=check_doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(content_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
except Exception as e:
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="error",
|
|||
|
|
message=f"校验报告生成失败: {check_doc_name}, 异常信息:{str(e)}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.ERROR,
|
|||
|
|
task_log=f"校验报告生成失败: {check_doc_name}, 异常信息:{str(e)}"
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
continue
|
|||
|
|
check_convert_succeeded = False
|
|||
|
|
check_convert_failed = False
|
|||
|
|
for event in handle_conver_file(
|
|||
|
|
check_doc_name,
|
|||
|
|
check_full_content,
|
|||
|
|
project,
|
|||
|
|
save_document_list,
|
|||
|
|
request,
|
|||
|
|
category,
|
|||
|
|
generation_request_id
|
|||
|
|
):
|
|||
|
|
if event.eventType == "convert_complete":
|
|||
|
|
check_convert_succeeded = True
|
|||
|
|
elif event.eventType == "convert_error":
|
|||
|
|
check_convert_failed = True
|
|||
|
|
yield f"data: {json.dumps(event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
if check_convert_failed or not check_convert_succeeded:
|
|||
|
|
error_message = f"校验报告转换失败: {check_doc_name}"
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.ERROR,
|
|||
|
|
task_log=error_message
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="error",
|
|||
|
|
message=error_message,
|
|||
|
|
documentName=check_doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
continue
|
|||
|
|
# 发送文档完成事件
|
|||
|
|
doc_complete_event = DocumentSSEData(
|
|||
|
|
eventType="check_document_complete",
|
|||
|
|
message=f"校验报告生成完成: {check_doc_name}",
|
|||
|
|
documentName=check_doc_name,
|
|||
|
|
documentIndex=index
|
|||
|
|
)
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.PENDING,
|
|||
|
|
task_log=f"校验报告生成完成: {check_doc_name}"
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
yield f"data: {json.dumps(doc_complete_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
if save_document_list is not None and len(save_document_list) > 0:
|
|||
|
|
try:
|
|||
|
|
file_link_schema = CreateProjectFileLinkSchema(
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
file_list=save_document_list,
|
|||
|
|
# type=None
|
|||
|
|
)
|
|||
|
|
# 创建附件关联
|
|||
|
|
links = await async_create_file_link(file_link_schema)
|
|||
|
|
|
|||
|
|
# 添加任务文件关联记录
|
|||
|
|
if request.taskId and links:
|
|||
|
|
file_ids = [link.file_id for link in links]
|
|||
|
|
await TaskService.async_add_task_file_links(request.taskId, file_ids)
|
|||
|
|
except Exception as e:
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="error",
|
|||
|
|
message=f"文件上传失败: {e}",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.ERROR,
|
|||
|
|
task_log=f"文件上传失败: {e}"
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
# 发送结束事件
|
|||
|
|
end_event = DocumentSSEData(
|
|||
|
|
eventType="end",
|
|||
|
|
message="",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=len(request.produceDocumentList)
|
|||
|
|
)
|
|||
|
|
update_task_data = ProjectTaskRecordCreate(
|
|||
|
|
id=task_id,
|
|||
|
|
project_id=request.projectId,
|
|||
|
|
task_type=TaskTypeEnum.GENERATE_DOCUMENT,
|
|||
|
|
task_status=TaskStatusEnum.COMPLETED,
|
|||
|
|
task_log=f"文档生成成功"
|
|||
|
|
)
|
|||
|
|
await async_create_or_update_task_record_service(update_task_data)
|
|||
|
|
yield f"data: {json.dumps(end_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
error_event = DocumentSSEData(
|
|||
|
|
eventType="error",
|
|||
|
|
message=f"生成过程出错: {str(e)}",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(error_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
end_event = DocumentSSEData(
|
|||
|
|
eventType="end",
|
|||
|
|
message="",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=len(request.produceDocumentList)
|
|||
|
|
)
|
|||
|
|
yield f"data: {json.dumps(end_event.to_dict(), ensure_ascii=False)}\n\n"
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def async_get_project_template_info(project_template_id) -> ProjectTemplateEntity:
|
|||
|
|
async with AsyncSessionLocal() as db:
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(ProjectTemplateEntity).where(ProjectTemplateEntity.id == project_template_id)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
project_template = result.scalar_one_or_none()
|
|||
|
|
if not project_template:
|
|||
|
|
raise HTTPException(status_code=404, detail=f"项目模板不存在: {project_template_id}")
|
|||
|
|
if not project_template.content:
|
|||
|
|
raise HTTPException(status_code=404, detail=f"项目模板内容不存在: {project_template.name}")
|
|||
|
|
return project_template
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_prompt(request, sys_data, doc_name, reference_content, project_template_content) -> str:
|
|||
|
|
document_template_prompt = ""
|
|||
|
|
# project_template_content 为空就读内置文件
|
|||
|
|
if project_template_content is None or project_template_content == '':
|
|||
|
|
document_template_prompt = DocumentGenerationService.load_template_prompt(file_name=doc_name + ".md")
|
|||
|
|
else:
|
|||
|
|
document_template_prompt = project_template_content
|
|||
|
|
# 构建消息
|
|||
|
|
system_prompt = DocumentGenerationService.load_template_prompt(file_name="document_system.md")
|
|||
|
|
system_prompt = system_prompt.replace("{SYSTEM_DATA}", sys_data)
|
|||
|
|
system_prompt = system_prompt.replace("{USER_SUPPLEMENT_QUANTITY}", request.userInput)
|
|||
|
|
system_prompt = system_prompt.replace("{WRITE_TEMPLATE}", document_template_prompt)
|
|||
|
|
system_prompt = system_prompt.replace("{REFERENCE_CONTENT}", reference_content)
|
|||
|
|
system_prompt = system_prompt.replace("{USER_EXTRA_CONSTRAINT}", "无额外约束")
|
|||
|
|
system_prompt = system_prompt.replace("{OTHER_WRITE_CONSTRAINT}", f"国军标选项: {request.GJBSelectOption}")
|
|||
|
|
system_prompt = system_prompt.replace("{CURRENT_DATE}", f'{date.today().strftime("%Y年%m月%d日")}')
|
|||
|
|
return system_prompt
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_check_prompt(document_content, check_role_connect, doc_name) -> str:
|
|||
|
|
"""构造校验文档的提示词信息"""
|
|||
|
|
check_prompt = DocumentGenerationService.load_template_prompt(file_name="document_check.md")
|
|||
|
|
check_prompt = check_prompt.replace("{AI_GENERATED_DOCUMENT}", document_content)
|
|||
|
|
check_prompt = check_prompt.replace("{CHECK_DOCUMENT}", check_role_connect)
|
|||
|
|
check_prompt = check_prompt.replace("{WRITE_TEMPLATE}", doc_name)
|
|||
|
|
return check_prompt
|
|||
|
|
|
|||
|
|
def load_check_role_prompt(document_content, document_name) -> str:
|
|||
|
|
"""获取生成校验规则文档的提示词信息"""
|
|||
|
|
check_prompt = DocumentGenerationService.load_template_prompt(file_name="generate_check_role.md")
|
|||
|
|
check_prompt = check_prompt.replace("{TEMPLATE_DOCUMENT}", document_content)
|
|||
|
|
check_prompt = check_prompt.replace("{DOCUMENT_NAME}", document_name)
|
|||
|
|
return check_prompt
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def handle_refer_document(request, project) -> str:
|
|||
|
|
"""处理参考文档,返回分析后的内容(不流式输出)"""
|
|||
|
|
content_map = {}
|
|||
|
|
openai_client = get_open_ai_client(project.api_key, project.base_url, project.model_name)
|
|||
|
|
async with AsyncSessionLocal() as db:
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(FileEntity).where(FileEntity.id.in_(request.referToDocument))
|
|||
|
|
)
|
|||
|
|
file_list = result.scalars().all()
|
|||
|
|
|
|||
|
|
for doc in file_list:
|
|||
|
|
if doc:
|
|||
|
|
# 从Minio下载并解析文档
|
|||
|
|
try:
|
|||
|
|
text_content = DocumentParser.parse_all_document(doc, openai_client=openai_client)
|
|||
|
|
content_map[doc.name] = text_content
|
|||
|
|
except Exception as e:
|
|||
|
|
raise Exception("文档分析失败", str(e))
|
|||
|
|
return json.dumps(content_map, ensure_ascii=False)
|
|||
|
|
# 组装提示词
|
|||
|
|
# remote_prompt = DocumentGenerationService.load_template_prompt(file_name="remote_analysis.md")
|
|||
|
|
# remote_prompt = remote_prompt.replace("{ANALYSIS_CONTENT}", json.dumps(content_map))
|
|||
|
|
#
|
|||
|
|
# messages = [
|
|||
|
|
# {"role": "system", "content": remote_prompt},
|
|||
|
|
# {"role": "user", "content": "请你帮我分析以下内容"}
|
|||
|
|
# ]
|
|||
|
|
# try:
|
|||
|
|
# # 使用非流式方式获取完整内容
|
|||
|
|
# full_content = openai_client.chat_completion(messages)
|
|||
|
|
# return full_content
|
|||
|
|
# except Exception as e:
|
|||
|
|
# error_msg = f"生成文档失败: {str(e)}"
|
|||
|
|
# raise Exception("文档分析失败:", error_msg)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def async_handle_refer_document(request, project) -> str:
|
|||
|
|
content_map = {}
|
|||
|
|
openai_client = get_open_ai_client(project.api_key, project.base_url, project.model_name)
|
|||
|
|
async with AsyncSessionLocal() as db:
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(FileEntity).where(FileEntity.id.in_(request.referToDocument))
|
|||
|
|
)
|
|||
|
|
file_list = result.scalars().all()
|
|||
|
|
|
|||
|
|
for doc in file_list:
|
|||
|
|
if doc:
|
|||
|
|
try:
|
|||
|
|
text_content = DocumentParser.parse_all_document(doc, openai_client=openai_client)
|
|||
|
|
content_map[doc.name] = text_content
|
|||
|
|
except Exception as e:
|
|||
|
|
raise Exception("文档分析失败", str(e))
|
|||
|
|
return json.dumps(content_map, ensure_ascii=False)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# draw.io CLI 路径解析
|
|||
|
|
def _resolve_drawio_cli() -> Optional[str]:
|
|||
|
|
"""解析 Draw.io 可执行文件路径(不含 xvfb-run)。"""
|
|||
|
|
import logging
|
|||
|
|
import platform
|
|||
|
|
import shutil
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
# 1. 环境变量 DRAWIO_CLI_PATH
|
|||
|
|
env_path = os.getenv("DRAWIO_CLI_PATH")
|
|||
|
|
if env_path and os.path.isfile(env_path):
|
|||
|
|
logger.info(f"使用环境变量 DRAWIO_CLI_PATH: {env_path}")
|
|||
|
|
return env_path
|
|||
|
|
|
|||
|
|
# 2. 项目内默认路径(与 article_agent 保持一致)
|
|||
|
|
default = os.path.normpath(os.path.join(
|
|||
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|||
|
|
"..", "..", "..", "..", "..", "soft", "drawio", "draw.io", "draw.io.exe",
|
|||
|
|
))
|
|||
|
|
if os.path.isfile(default):
|
|||
|
|
logger.info(f"使用项目默认路径: {default}")
|
|||
|
|
return default
|
|||
|
|
|
|||
|
|
# 3. tool 目录下的 drawio.exe(兼容旧配置)
|
|||
|
|
tool_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "tool/drawio.exe")
|
|||
|
|
if os.path.isfile(tool_path):
|
|||
|
|
logger.info(f"使用 tool 目录: {tool_path}")
|
|||
|
|
return tool_path
|
|||
|
|
|
|||
|
|
# 4. PATH 查找
|
|||
|
|
for name in ("draw.io.exe", "drawio", "draw.io"):
|
|||
|
|
found = shutil.which(name)
|
|||
|
|
if found:
|
|||
|
|
logger.info(f"使用系统 PATH: {found}")
|
|||
|
|
return found
|
|||
|
|
|
|||
|
|
logger.warning("Draw.io 未找到,请安装或设置 DRAWIO_CLI_PATH 环境变量")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
DRAWIO_EXE_PATH = _resolve_drawio_cli()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def extract_drawio_blocks(content: str) -> list:
|
|||
|
|
"""
|
|||
|
|
从Markdown内容中提取draw.io XML代码块
|
|||
|
|
|
|||
|
|
:param content: Markdown内容
|
|||
|
|
:return: draw.io XML列表
|
|||
|
|
"""
|
|||
|
|
import re
|
|||
|
|
pattern = r'```(?:drawio|xml)\n(.*?)\n```'
|
|||
|
|
matches = re.findall(pattern, content, re.DOTALL)
|
|||
|
|
# 替换每个 XML 块中的 host
|
|||
|
|
replaced_matches = [replace_drawio_host(match) for match in matches]
|
|||
|
|
return replaced_matches
|
|||
|
|
|
|||
|
|
|
|||
|
|
def convert_drawio_to_png(drawio_xml: str, output_path: str, output_dir) -> bool:
|
|||
|
|
"""
|
|||
|
|
使用draw.io将XML转换为PNG
|
|||
|
|
|
|||
|
|
:param output_dir: 输出目录
|
|||
|
|
:param drawio_xml: draw.io XML内容
|
|||
|
|
:param output_path: 输出PNG文件路径
|
|||
|
|
:return: 是否转换成功
|
|||
|
|
"""
|
|||
|
|
import tempfile
|
|||
|
|
import logging
|
|||
|
|
import re
|
|||
|
|
import platform
|
|||
|
|
import shutil
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
# 优化 drawio XML 中的画布大小,确保适配内容
|
|||
|
|
page_width_match = re.search(r'pageWidth="(\d+)"', drawio_xml)
|
|||
|
|
page_height_match = re.search(r'pageHeight="(\d+)"', drawio_xml)
|
|||
|
|
|
|||
|
|
if page_width_match and page_height_match:
|
|||
|
|
page_width = int(page_width_match.group(1))
|
|||
|
|
page_height = int(page_height_match.group(1))
|
|||
|
|
# 如果画布过大(超过2000),调整为更合适的尺寸
|
|||
|
|
if page_width > 1200 or page_height > 1000:
|
|||
|
|
new_width = min(800, page_width)
|
|||
|
|
new_height = min(600, page_height)
|
|||
|
|
drawio_xml = drawio_xml.replace(
|
|||
|
|
f'pageWidth="{page_width}"', f'pageWidth="{new_width}"'
|
|||
|
|
).replace(
|
|||
|
|
f'pageHeight="{page_height}"', f'pageHeight="{new_height}"'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.drawio', delete=False, encoding='utf-8') as f:
|
|||
|
|
f.write(drawio_xml)
|
|||
|
|
xml_file = f.name
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 检查draw.io命令是否存在
|
|||
|
|
if os.path.exists(DRAWIO_EXE_PATH):
|
|||
|
|
drawio_cmd = DRAWIO_EXE_PATH
|
|||
|
|
logger.info(f"使用内置draw.io: {DRAWIO_EXE_PATH}")
|
|||
|
|
else:
|
|||
|
|
drawio_cmd = shutil.which("draw.io") or shutil.which("drawio")
|
|||
|
|
if not drawio_cmd:
|
|||
|
|
logger.error("draw.io命令未找到,请确认已安装并添加到PATH")
|
|||
|
|
return False
|
|||
|
|
logger.info(f"使用系统draw.io: {drawio_cmd}")
|
|||
|
|
|
|||
|
|
if platform.system() == "Linux":
|
|||
|
|
drawio_cmd = f"xvfb-run -a {drawio_cmd}"
|
|||
|
|
|
|||
|
|
command = (
|
|||
|
|
f'{drawio_cmd} -x -f png '
|
|||
|
|
f'-o "{output_path}" '
|
|||
|
|
f'-b 0 '
|
|||
|
|
f'--crop '
|
|||
|
|
f'"{xml_file}"'
|
|||
|
|
)
|
|||
|
|
logger.info(f"执行draw.io转换命令: {command}")
|
|||
|
|
run_command(command, output_dir)
|
|||
|
|
|
|||
|
|
# 检查输出文件是否生成
|
|||
|
|
if os.path.exists(output_path):
|
|||
|
|
file_size = os.path.getsize(output_path)
|
|||
|
|
logger.info(f"draw.io转换成功,输出文件: {output_path}, 大小: {file_size} bytes")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
logger.error(f"draw.io转换完成但输出文件不存在: {output_path}")
|
|||
|
|
return False
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"draw.io转换异常: {str(e)}", exc_info=True)
|
|||
|
|
return False
|
|||
|
|
finally:
|
|||
|
|
if os.path.exists(xml_file):
|
|||
|
|
os.unlink(xml_file)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def process_drawio_images(content: str, output_dir: str):
|
|||
|
|
"""
|
|||
|
|
处理Markdown中的draw.io代码块,转换为PNG图片引用
|
|||
|
|
|
|||
|
|
:param content: Markdown内容
|
|||
|
|
:param output_dir: 输出目录
|
|||
|
|
:return: 处理后的Markdown内容,draw.io块被替换为图片引用
|
|||
|
|
"""
|
|||
|
|
import re
|
|||
|
|
import logging
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
pattern = r'```(?:drawio|xml)\n(.*?)\n```'
|
|||
|
|
matches = re.findall(pattern, content, re.DOTALL)
|
|||
|
|
total_images = len(matches)
|
|||
|
|
|
|||
|
|
if total_images == 0:
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message="无draw.io图片需要处理",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
yield content
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"开始处理 {total_images} 个draw.io图片",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parts = []
|
|||
|
|
last_end = 0
|
|||
|
|
image_index = 0
|
|||
|
|
|
|||
|
|
for match in re.finditer(pattern, content, re.DOTALL):
|
|||
|
|
parts.append(content[last_end:match.start()])
|
|||
|
|
last_end = match.end()
|
|||
|
|
drawio_xml = match.group(1).strip()
|
|||
|
|
|
|||
|
|
# 替换 draw.io XML 中的 host
|
|||
|
|
drawio_xml = replace_drawio_host(drawio_xml)
|
|||
|
|
|
|||
|
|
image_index += 1
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"处理图片 {image_index}/{total_images}",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
png_filename = f"diagram_{image_index}.png"
|
|||
|
|
png_path = os.path.join(output_dir, png_filename)
|
|||
|
|
|
|||
|
|
xml_filename = f"diagram_{image_index}.drawio"
|
|||
|
|
xml_path = os.path.join(output_dir, xml_filename)
|
|||
|
|
|
|||
|
|
with open(xml_path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(drawio_xml)
|
|||
|
|
|
|||
|
|
success = convert_drawio_to_png(drawio_xml, png_path, output_dir)
|
|||
|
|
if success:
|
|||
|
|
logger.info(f"draw.io图片生成成功: {png_path}")
|
|||
|
|
# 使用相对路径而不是绝对路径,避免转换问题
|
|||
|
|
parts.append(f"")
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"图片 {image_index}/{total_images} 生成完成",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
logger.warning(f"draw.io图片生成失败: {png_filename},继续处理")
|
|||
|
|
# 即使转换失败,也保留占位符,避免破坏文档结构
|
|||
|
|
parts.append(f"")
|
|||
|
|
# 注意:这里不发送convert_error事件,因为图片转换失败不应影响整体转换结果
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"图片 {image_index}/{total_images} 转换失败(已保留占位符)",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
parts.append(content[last_end:])
|
|||
|
|
processed_content = "".join(parts)
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"draw.io图片处理完成,共处理 {total_images} 张图片",
|
|||
|
|
documentName=None,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
yield processed_content
|
|||
|
|
|
|||
|
|
|
|||
|
|
def sanitize_generated_markdown(content: str) -> str:
|
|||
|
|
"""
|
|||
|
|
Clean model output before Markdown->Word conversion.
|
|||
|
|
|
|||
|
|
Current fixes:
|
|||
|
|
- drop template artifact lines like ``\3.1 接口标识和接口图``
|
|||
|
|
- de-duplicate immediately repeated heading text caused by prompt examples
|
|||
|
|
"""
|
|||
|
|
cleaned_lines = []
|
|||
|
|
previous_non_empty = ""
|
|||
|
|
|
|||
|
|
for raw_line in content.splitlines():
|
|||
|
|
line = raw_line.rstrip()
|
|||
|
|
stripped = line.strip()
|
|||
|
|
|
|||
|
|
if re.match(r"^\\\d+(?:\.\d+)*\s+", stripped):
|
|||
|
|
normalized = stripped[1:].strip()
|
|||
|
|
if normalized == previous_non_empty:
|
|||
|
|
continue
|
|||
|
|
# These are template residue lines, not valid final content.
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if stripped and stripped == previous_non_empty:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
cleaned_lines.append(line)
|
|||
|
|
if stripped:
|
|||
|
|
previous_non_empty = stripped
|
|||
|
|
|
|||
|
|
return "\n".join(cleaned_lines)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|||
|
|
if not os.path.exists(f"{base_path}/workspace"):
|
|||
|
|
os.makedirs(f"{base_path}/workspace")
|
|||
|
|
import uuid
|
|||
|
|
current_temp_path = f"{base_path}/workspace/{uuid.uuid4()}"
|
|||
|
|
os.makedirs(current_temp_path)
|
|||
|
|
template_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates/template.docx")
|
|||
|
|
# mermaid_filter_lua_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "tool/mermaid-filter.lua")
|
|||
|
|
# 使用库来进行文档转换
|
|||
|
|
|
|||
|
|
converter = MarkdownToDocx(template_file, font_name="宋体")
|
|||
|
|
docx_file_path = f"{current_temp_path}/aaa.docx"
|
|||
|
|
with open(
|
|||
|
|
"C:/workspace/ai/DocumentGenerateAgent/agents/ai_agents/workspace/1.md",
|
|||
|
|
'r',
|
|||
|
|
encoding="UTF-8") as f:
|
|||
|
|
txt = f.read()
|
|||
|
|
|
|||
|
|
converter.convert(txt, docx_file_path)
|
|||
|
|
# 转换图表的题注。
|
|||
|
|
from agents.ai_agents.document_agent.utils import docx_util
|
|||
|
|
|
|||
|
|
docx_util.fix_table_borders(docx_file_path)
|
|||
|
|
docx_util.add_captions(docx_file_path)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# for event1 in process_drawio_images(txt, "D:/workspace/agent/DocumentGenerateAgent/agents/workspace/test"):
|
|||
|
|
# if isinstance(event1, str):
|
|||
|
|
# processed_content1 = event1
|
|||
|
|
# else:
|
|||
|
|
# print(f'{event1}')
|
|||
|
|
# convert_drawio_to_png(txt,
|
|||
|
|
# "D:/workspace/agent/DocumentGenerateAgent/agents/workspace/bcb79d47-4b33-44fd-9aa6-7334ee714f57/2.png",
|
|||
|
|
# "D:/workspace/agent/DocumentGenerateAgent/agents/workspace/bcb79d47-4b33-44fd-9aa6-7334ee714f57")
|
|||
|
|
# with open("D:/workspace/agent/DocumentGenerateAgent/agents/ai_agents/document_agent/templates/txt.txt", 'r',
|
|||
|
|
# encoding="UTF-8") as f:
|
|||
|
|
# txt = f.read()
|
|||
|
|
# process_drawio_images(txt,
|
|||
|
|
# "D:/workspace/agent/DocumentGenerateAgent/agents/workspace/3ce4660d-e4f6-4fd3-a5d4-29fb0dd25a22/output")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 处理基于模板转换的文件
|
|||
|
|
def convert_template_docx(docx_file_path, project, doc_name, current_temp_path) -> str:
|
|||
|
|
context_data = {
|
|||
|
|
"projectName": project.name,
|
|||
|
|
"name": doc_name,
|
|||
|
|
}
|
|||
|
|
# 加载模板
|
|||
|
|
template_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates/base_template.docx")
|
|||
|
|
template = DocxTemplate(template_file)
|
|||
|
|
# 执行填充,生成一个填充好的中间文档
|
|||
|
|
template.render(context_data)
|
|||
|
|
# 保存中间文档,此文档包含了填充后的所有内容
|
|||
|
|
filled_file = f'{current_temp_path}/filled_temp.docx'
|
|||
|
|
template.save(filled_file)
|
|||
|
|
|
|||
|
|
# --- 第二阶段:将填充好的文档与其他文档合并 ---
|
|||
|
|
# 创建一个基于填充后文档的合并器
|
|||
|
|
base_doc = Document_compose(filled_file)
|
|||
|
|
composer = Composer(base_doc)
|
|||
|
|
|
|||
|
|
# 添加其他需要合并的文档
|
|||
|
|
other_files = [docx_file_path]
|
|||
|
|
for file in other_files:
|
|||
|
|
if os.path.exists(file):
|
|||
|
|
doc_to_append = Document_compose(file)
|
|||
|
|
composer.append(doc_to_append)
|
|||
|
|
|
|||
|
|
# 保存最终合并的文档
|
|||
|
|
save_path = f'{current_temp_path}/{project.name}-{doc_name}.docx'
|
|||
|
|
composer.save(save_path)
|
|||
|
|
return save_path
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 处理文档转换
|
|||
|
|
def handle_conver_file(doc_name, full_content, project, save_document, request, category, generation_request_id: str):
|
|||
|
|
import logging
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|||
|
|
logger.info(f"base_path: {base_path}")
|
|||
|
|
if not os.path.exists(f"{base_path}/workspace"):
|
|||
|
|
os.makedirs(f"{base_path}/workspace")
|
|||
|
|
import uuid
|
|||
|
|
current_temp_path = f"{base_path}/workspace/{generation_request_id}_{uuid.uuid4().hex}"
|
|||
|
|
os.makedirs(current_temp_path)
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_start",
|
|||
|
|
message=f"开始转换文档: {doc_name}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
processed_content = None
|
|||
|
|
for event in process_drawio_images(full_content, current_temp_path):
|
|||
|
|
if isinstance(event, str):
|
|||
|
|
processed_content = event
|
|||
|
|
else:
|
|||
|
|
yield event
|
|||
|
|
|
|||
|
|
if processed_content is None:
|
|||
|
|
processed_content = full_content
|
|||
|
|
processed_content = sanitize_generated_markdown(processed_content)
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"Markdown文件生成完成",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
md_file_path = f"{current_temp_path}/{doc_name}.md"
|
|||
|
|
with open(md_file_path, mode='w', encoding="utf-8") as file:
|
|||
|
|
file.write(processed_content)
|
|||
|
|
|
|||
|
|
template_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates/template.docx")
|
|||
|
|
# mermaid_filter_lua_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "tool/mermaid-filter.lua")
|
|||
|
|
# 使用库来进行文档转换
|
|||
|
|
|
|||
|
|
# 验证模板文件是否存在
|
|||
|
|
if not os.path.exists(template_file):
|
|||
|
|
logger.error(f"模板文件不存在: {template_file}")
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_error",
|
|||
|
|
message=f"模板文件不存在: {template_file}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
logger.info(f"开始Markdown转Word转换,文档名: {doc_name}, 模板路径: {template_file}")
|
|||
|
|
logger.info(f"Markdown内容长度: {len(processed_content)} 字符")
|
|||
|
|
|
|||
|
|
converter = MarkdownToDocx(template_file, font_name="宋体")
|
|||
|
|
docx_file_path = f"{current_temp_path}/{doc_name}.docx"
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
converter.convert(processed_content, docx_file_path)
|
|||
|
|
logger.info(f"Markdown转Word成功,输出文件: {docx_file_path}")
|
|||
|
|
except Exception as convert_ex:
|
|||
|
|
logger.error(f"Markdown转Word失败: {str(convert_ex)}", exc_info=True)
|
|||
|
|
# 保存原始Markdown以便调试
|
|||
|
|
debug_md_path = f"{current_temp_path}/{doc_name}_debug.md"
|
|||
|
|
with open(debug_md_path, 'w', encoding='utf-8') as debug_f:
|
|||
|
|
debug_f.write(processed_content)
|
|||
|
|
logger.info(f"已保存调试用的Markdown文件: {debug_md_path}")
|
|||
|
|
raise convert_ex
|
|||
|
|
# 转换图表的题注。
|
|||
|
|
from agents.ai_agents.document_agent.utils import docx_util
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
logger.info(f"开始修复表格边框: {docx_file_path}")
|
|||
|
|
docx_util.fix_table_borders(docx_file_path)
|
|||
|
|
logger.info(f"开始添加题注: {docx_file_path}")
|
|||
|
|
docx_util.add_captions(docx_file_path)
|
|||
|
|
logger.info(f"开始修复表格单元格样式: {docx_file_path}")
|
|||
|
|
docx_util.fix_table_cell_styles(docx_file_path)
|
|||
|
|
logger.info(f"文档后处理完成: {docx_file_path}")
|
|||
|
|
except Exception as postprocess_ex:
|
|||
|
|
logger.error(f"文档后处理失败: {str(postprocess_ex)}", exc_info=True)
|
|||
|
|
raise postprocess_ex
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"Word文档生成完成",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# pandoc_path = "pandoc"
|
|||
|
|
# sys_platform = platform.system()
|
|||
|
|
#
|
|||
|
|
# if sys_platform == "Linux":
|
|||
|
|
# pandoc_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "tool/pandoc")
|
|||
|
|
# elif sys_platform == "Windows":
|
|||
|
|
# pandoc_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "tool/pandoc.exe")
|
|||
|
|
# # 执行命令行
|
|||
|
|
# try:
|
|||
|
|
# command = (f'{pandoc_path} "{md_file_path}" -o '
|
|||
|
|
# f'"{current_temp_path}/{doc_name}.docx" --standalone '
|
|||
|
|
# f'--reference-doc "{template_file}"')
|
|||
|
|
# logging.info(f"command: {command}")
|
|||
|
|
# run_command(command, work=current_temp_path)
|
|||
|
|
# except Exception as ex:
|
|||
|
|
# logger.error(f"文档转换失败 ({doc_name}),返回码: {ex}")
|
|||
|
|
# # 检查输出文件是否存在
|
|||
|
|
# docx_file_path = f"{current_temp_path}/{doc_name}.docx"
|
|||
|
|
if not os.path.exists(docx_file_path):
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_error",
|
|||
|
|
message=f"Word文档生成失败",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
# 基于模板再次生成详细的文档
|
|||
|
|
convert_docx_path = convert_template_docx(docx_file_path, project, doc_name, current_temp_path)
|
|||
|
|
docx_util.postprocess_merged_document(convert_docx_path, project_name=project.name, doc_name=doc_name)
|
|||
|
|
docx_util.enable_update_fields_on_open(convert_docx_path)
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_processing",
|
|||
|
|
message=f"模板文档生成完成,开始上传",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|||
|
|
object_name = f'{doc_name}_{timestamp}.docx'
|
|||
|
|
if request.version != '' and request.version is not None:
|
|||
|
|
object_name = f'{doc_name}_{request.version}_{timestamp}.docx'
|
|||
|
|
minio_url = minio_client.upload_file_from_path(
|
|||
|
|
file_path=convert_docx_path,
|
|||
|
|
object_name=object_name,
|
|||
|
|
content_type="application/octet-stream"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 确定文档分类
|
|||
|
|
# category = determine_document_category(doc_name)
|
|||
|
|
|
|||
|
|
# 保存到数据库
|
|||
|
|
# show_name 包含时间戳(不包含UUID),格式如:运行方案说明_20260625_112409_671606.docx
|
|||
|
|
timestamp_without_microseconds = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
|||
|
|
file_name = f'{doc_name}_{timestamp_without_microseconds}.docx'
|
|||
|
|
if request.version != '' and request.version is not None:
|
|||
|
|
file_name = f'{doc_name}_{request.version}_{timestamp_without_microseconds}.docx'
|
|||
|
|
generated_doc = FileLinkSchema(
|
|||
|
|
object_name=object_name,
|
|||
|
|
url=minio_url,
|
|||
|
|
file_name=file_name,
|
|||
|
|
type=category
|
|||
|
|
)
|
|||
|
|
save_document.append(generated_doc)
|
|||
|
|
logger.info(f"文档转换并上传成功: {doc_name}")
|
|||
|
|
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_complete",
|
|||
|
|
message=f"文档转换并上传成功: {doc_name}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
except Exception as ex:
|
|||
|
|
logger.error(f"文档转换失败 ({doc_name}): {str(ex)}", exc_info=True)
|
|||
|
|
yield DocumentSSEData(
|
|||
|
|
eventType="convert_error",
|
|||
|
|
message=f"文档转换失败: {str(ex)}",
|
|||
|
|
documentName=doc_name,
|
|||
|
|
documentIndex=0
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
|
|||
|
|
def determine_document_category(doc_name: str) -> str:
|
|||
|
|
"""
|
|||
|
|
根据文档名称确定分类
|
|||
|
|
|
|||
|
|
:param doc_name: 文档名称
|
|||
|
|
:return: 文档分类
|
|||
|
|
"""
|
|||
|
|
for category, doc_types in DOCUMENT_TYPES.items():
|
|||
|
|
if doc_name in doc_types:
|
|||
|
|
return DocumentCategoryEnum(category).name
|
|||
|
|
|
|||
|
|
# 默认返回需求文档类
|
|||
|
|
return DocumentCategoryEnum.REQUIREMENT.name
|