feat:导出面板动态编写一半
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user