feat:导出面板动态编写一半

This commit is contained in:
liailing1026
2026-03-05 11:00:21 +08:00
parent 7a8acc7375
commit 8cd3152c29
10 changed files with 1960 additions and 43 deletions

View File

@@ -1,4 +1,5 @@
from flask import Flask, request, jsonify, Response, stream_with_context
from flask import Flask, request, jsonify, Response, stream_with_context, send_file
from flask_cors import CORS
from flask_socketio import SocketIO, emit, join_room, leave_room
import json
from DataProcess import Add_Collaboration_Brief_FrontEnd
@@ -27,10 +28,14 @@ from db import (
get_db_context,
MultiAgentTaskCRUD,
UserAgentCRUD,
ExportRecordCRUD,
TaskStatus,
text,
)
# 导出模块导入
from AgentCoord.Export import ExportFactory
# initialize global variables
yaml_file = os.path.join(os.getcwd(), "config", "config.yaml")
try:
@@ -48,8 +53,18 @@ AgentProfile_Dict = {}
Request_Cache: dict[str, str] = {}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'agentcoord-secret-key'
CORS(app) # 启用 CORS 支持
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
# 配置静态文件服务(用于导出文件访问)
EXPORT_DIR = os.path.join(os.getcwd(), "uploads", "exports")
@app.route('/uploads/<path:filename>', methods=['GET'])
def serve_export_file(filename):
"""服务导出文件(静态文件访问)"""
from flask import send_from_directory
return send_from_directory('uploads', filename)
def truncate_rehearsal_log(RehearsalLog: List, restart_from_step_index: int) -> List:
"""
截断 RehearsalLog只保留指定索引之前的步骤结果
@@ -2663,6 +2678,321 @@ def handle_update_assigned_agents(data):
})
# ==================== 导出功能 ====================
# 导出类型配置
EXPORT_TYPE_CONFIG = {
"doc": {"ext": ".docx", "mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
"markdown": {"ext": ".md", "mime": "text/markdown"},
"excel": {"ext": ".xlsx", "mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
"ppt": {"ext": ".pptx", "mime": "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
"mindmap": {"ext": ".md", "mime": "text/markdown"}, # 思维导图先用 markdown
"infographic": {"ext": ".html", "mime": "text/html"}, # 信息图先用 html
}
def ensure_export_dir(task_id: str) -> str:
"""确保导出目录存在"""
task_dir = os.path.join(EXPORT_DIR, task_id)
os.makedirs(task_dir, exist_ok=True)
return task_dir
def generate_export_file_name(task_name: str, export_type: str) -> str:
"""生成导出文件名"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 清理文件名中的非法字符
safe_name = "".join(c for c in task_name if c.isalnum() or c in (' ', '-', '_')).strip()
return f"{safe_name}_{export_type}_{timestamp}"
@socketio.on('export')
def handle_export(data):
"""
WebSocket处理导出请求
请求格式:
{
"id": "request-id",
"action": "export",
"data": {
"task_id": "task-id", // 任务ID
"export_type": "doc", // 导出类型: doc/markdown/excel/ppt/mindmap/infographic
"user_id": "user-id", // 用户ID
}
}
"""
request_id = data.get('id')
incoming_data = data.get('data', {})
task_id = incoming_data.get('task_id')
export_type = incoming_data.get('export_type')
user_id = incoming_data.get('user_id', 'anonymous')
# 参数验证
if not task_id:
emit('response', {
'id': request_id,
'status': 'error',
'error': '缺少 task_id 参数'
})
return
if not export_type or export_type not in EXPORT_TYPE_CONFIG:
emit('response', {
'id': request_id,
'status': 'error',
'error': f'无效的导出类型: {export_type}'
})
return
try:
with get_db_context() as db:
# 获取任务数据
task = MultiAgentTaskCRUD.get_by_id(db, task_id)
if not task:
emit('response', {
'id': request_id,
'status': 'error',
'error': f'任务不存在: {task_id}'
})
return
# 准备导出数据
export_data = {
'task_name': task.query or '未命名任务',
'task_content': task.query or '', # 使用 query 作为任务描述
'task_outline': task.task_outline,
'result': task.result,
'agents_info': task.agents_info,
'assigned_agents': task.assigned_agents,
}
# 生成文件名
file_name_base = generate_export_file_name(export_data['task_name'], export_type)
config = EXPORT_TYPE_CONFIG[export_type]
file_name = file_name_base + config['ext']
file_path = os.path.join(ensure_export_dir(task_id), file_name)
# 生成文件内容
# 使用 ExportFactory 来生成各种格式的文件
try:
success = ExportFactory.export(export_type, export_data, file_path)
if not success:
# 如果导出失败,创建空文件占位
with open(file_path, 'wb') as f:
f.write(b'')
emit('response', {
'id': request_id,
'status': 'error',
'error': f'导出类型 {export_type} 不支持或生成失败'
})
return
except Exception as e:
print(f"导出文件失败: {e}")
import traceback
traceback.print_exc()
# 导出失败时创建空文件
with open(file_path, 'wb') as f:
f.write(b'')
# 获取文件大小
file_size = os.path.getsize(file_path)
# 生成访问URL基于文件路径
# 相对路径用于静态访问
relative_path = os.path.join('uploads', 'exports', task_id, file_name)
file_url = f"/{relative_path.replace(os.sep, '/')}"
# 保存导出记录到数据库
record = ExportRecordCRUD.create(
db=db,
task_id=task_id,
user_id=user_id,
export_type=export_type,
file_name=file_name,
file_path=file_path,
file_url=file_url,
file_size=file_size,
)
emit('response', {
'id': request_id,
'status': 'success',
'data': {
'record_id': record.id,
'file_name': file_name,
'file_url': file_url,
'file_size': file_size,
'export_type': export_type,
}
})
except Exception as e:
import traceback
traceback.print_exc()
emit('response', {
'id': request_id,
'status': 'error',
'error': str(e)
})
@socketio.on('get_export_list')
def handle_get_export_list(data):
"""
WebSocket获取导出记录列表
请求格式:
{
"id": "request-id",
"action": "get_export_list",
"data": {
"task_id": "task-id", // 任务ID
}
}
"""
request_id = data.get('id')
incoming_data = data.get('data', {})
task_id = incoming_data.get('task_id')
if not task_id:
emit('response', {
'id': request_id,
'status': 'error',
'error': '缺少 task_id 参数'
})
return
try:
with get_db_context() as db:
records = ExportRecordCRUD.get_by_task_id(db, task_id)
export_list = [record.to_dict() for record in records]
emit('response', {
'id': request_id,
'status': 'success',
'data': {
'list': export_list,
'total': len(export_list)
}
})
except Exception as e:
emit('response', {
'id': request_id,
'status': 'error',
'error': str(e)
})
# ==================== REST API 接口 ====================
@app.route('/api/export/<int:record_id>/download', methods=['GET'])
def download_export(record_id: int):
"""下载导出文件"""
try:
with get_db_context() as db:
record = ExportRecordCRUD.get_by_id(db, record_id)
if not record:
return jsonify({'error': '导出记录不存在'}), 404
if not os.path.exists(record.file_path):
return jsonify({'error': '文件不存在'}), 404
# 发送文件
config = EXPORT_TYPE_CONFIG.get(record.export_type, {})
mime_type = config.get('mime', 'application/octet-stream')
return send_file(
record.file_path,
mimetype=mime_type,
as_attachment=True,
download_name=record.file_name
)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/export/<int:record_id>/preview', methods=['GET'])
def preview_export(record_id: int):
"""预览导出文件"""
try:
with get_db_context() as db:
record = ExportRecordCRUD.get_by_id(db, record_id)
if not record:
return jsonify({'error': '导出记录不存在'}), 404
if not os.path.exists(record.file_path):
return jsonify({'error': '文件不存在'}), 404
# 根据文件类型返回不同的 Content-Type
config = EXPORT_TYPE_CONFIG.get(record.export_type, {})
mime_type = config.get('mime', 'application/octet-stream')
# 读取文件内容
if record.export_type == 'markdown':
with open(record.file_path, 'r', encoding='utf-8') as f:
content = f.read()
return jsonify({'content': content, 'type': 'markdown'})
else:
# 其他类型返回文件路径,前端自行处理
return jsonify({
'file_url': record.file_url,
'file_name': record.file_name,
'type': record.export_type
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/export/<int:record_id>/share', methods=['GET'])
def share_export(record_id: int):
"""生成分享链接"""
try:
with get_db_context() as db:
record = ExportRecordCRUD.get_by_id(db, record_id)
if not record:
return jsonify({'error': '导出记录不存在'}), 404
# 生成分享Token简化实现直接用记录ID
share_token = f"export_{record.id}_{int(record.created_at.timestamp())}"
share_url = f"/share/{share_token}"
return jsonify({
'share_url': share_url,
'file_name': record.file_name,
'expired_at': None # TODO: 可以添加过期时间
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/export/<int:record_id>', methods=['DELETE'])
def delete_export(record_id: int):
"""删除导出记录"""
try:
with get_db_context() as db:
record = ExportRecordCRUD.get_by_id(db, record_id)
if not record:
return jsonify({'error': '导出记录不存在'}), 404
# 删除物理文件
if os.path.exists(record.file_path):
os.remove(record.file_path)
# 删除数据库记录
ExportRecordCRUD.delete(db, record_id)
return jsonify({'success': True, 'message': '删除成功'})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="start the backend for AgentCoord"