feat:历史记录分享功能实现

This commit is contained in:
liailing1026
2026-03-12 13:35:04 +08:00
parent 26c42697e8
commit 130f78108f
9 changed files with 1500 additions and 80 deletions

View File

@@ -23,6 +23,7 @@ import uuid
import copy
import base64
from typing import List, Dict, Optional
from datetime import datetime, timezone, timedelta
# 数据库模块导入
from db import (
@@ -30,6 +31,7 @@ from db import (
MultiAgentTaskCRUD,
UserAgentCRUD,
ExportRecordCRUD,
PlanShareCRUD,
TaskStatus,
text,
)
@@ -2239,6 +2241,200 @@ def handle_pin_plan(data):
})
import secrets
@socketio.on('share_plan')
def handle_share_plan(data):
"""
WebSocket版本分享任务
"""
# socketio 包装: data = { id: 'share_plan-xxx', action: 'share_plan', data: { id: 'ws_req_xxx', data: {...} } }
request_id = data.get('id') # socketio 包装的 id
incoming_data = data.get('data', {}).get('data', {}) # 真正的请求数据
plan_id = incoming_data.get('plan_id')
expiration_days = incoming_data.get('expiration_days', 7) # 默认为7天0表示永久
extraction_code = incoming_data.get('extraction_code', '') # 提取码
auto_fill_code = incoming_data.get('auto_fill_code', True) # 是否在链接中自动填充提取码
if not plan_id:
emit('response', {
'id': request_id,
'status': 'error',
'error': '缺少 plan_idtask_id'
})
return
try:
with get_db_context() as db:
# 获取任务详情
task = MultiAgentTaskCRUD.get_by_id(db, plan_id)
if not task:
emit('response', {
'id': request_id,
'status': 'error',
'error': f'任务不存在: {plan_id}'
})
return
# 生成唯一分享 token
share_token = secrets.token_urlsafe(16)
# 设置过期时间
if expiration_days == 0:
# 永久有效
expires_at = None
else:
expires_at = datetime.now(timezone.utc) + timedelta(days=expiration_days)
# 准备分享数据(脱敏处理,移除敏感信息)
task_data = {
"general_goal": task.query,
"task_outline": task.task_outline,
"assigned_agents": task.assigned_agents,
"agent_scores": task.agent_scores,
"agents_info": task.agents_info,
"branches": task.branches,
"result": task.result,
"rehearsal_log": task.rehearsal_log,
"status": task.status.value if task.status else None,
}
# 创建分享记录
share = PlanShareCRUD.create(
db=db,
share_token=share_token,
task_id=plan_id,
task_data=task_data,
expires_at=expires_at,
extraction_code=extraction_code.upper() if extraction_code else None,
)
# 生成分享链接根据auto_fill_code决定是否在URL中携带提取码
if extraction_code and auto_fill_code:
share_url = f"/share/{share_token}?code={extraction_code.upper()}"
else:
share_url = f"/share/{share_token}"
emit('response', {
'id': request_id,
'status': 'success',
'data': {
"share_url": share_url,
"share_token": share_token,
"extraction_code": extraction_code.upper() if extraction_code else None,
"auto_fill_code": auto_fill_code,
"task_id": plan_id,
}
})
except Exception as e:
emit('response', {
'id': request_id,
'status': 'error',
'error': str(e)
})
@socketio.on('import_shared_plan')
def handle_import_shared_plan(data):
"""
WebSocket版本导入分享的任务到自己的历史记录
"""
# socketio 包装: data = { id: 'import_shared_plan-xxx', action: 'import_shared_plan', data: { id: 'ws_req_xxx', data: {...} } }
request_id = data.get('id') # socketio 包装的 id
incoming_data = data.get('data', {}).get('data', {}) # 真正的请求数据
share_token = incoming_data.get('share_token')
user_id = incoming_data.get('user_id')
if not share_token:
emit('response', {
'id': request_id,
'status': 'error',
'error': '缺少 share_token'
})
return
if not user_id:
emit('response', {
'id': request_id,
'status': 'error',
'error': '缺少 user_id'
})
return
try:
with get_db_context() as db:
# 获取分享记录
share = PlanShareCRUD.get_by_token(db, share_token)
if not share:
emit('response', {
'id': request_id,
'status': 'error',
'error': '分享链接无效或已失效'
})
return
# 检查是否过期
if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now():
emit('response', {
'id': request_id,
'status': 'error',
'error': '分享链接已过期'
})
return
# 获取分享的任务数据
task_data = share.task_data
# 生成新的 task_id因为是导入到自己的账号
import uuid
new_task_id = str(uuid.uuid4())
# 创建新的任务记录
task = MultiAgentTaskCRUD.create(
db=db,
task_id=new_task_id,
user_id=user_id,
query=task_data.get("general_goal", ""),
agents_info=task_data.get("agents_info", []),
task_outline=task_data.get("task_outline"),
assigned_agents=task_data.get("assigned_agents"),
agent_scores=task_data.get("agent_scores"),
result=task_data.get("result"),
)
# 如果有分支数据,也保存
if task_data.get("branches"):
MultiAgentTaskCRUD.update_branches(db, new_task_id, task_data["branches"])
# 如果有执行日志,也保存
if task_data.get("rehearsal_log"):
MultiAgentTaskCRUD.update_rehearsal_log(db, new_task_id, task_data["rehearsal_log"])
# 增加分享的查看次数
PlanShareCRUD.increment_view_count(db, share_token)
# 通知所有客户端刷新历史列表
socketio.emit('history_updated', {'user_id': user_id})
emit('response', {
'id': request_id,
'status': 'success',
'data': {
"message": "导入成功",
"task_id": new_task_id,
}
})
except Exception as e:
emit('response', {
'id': request_id,
'status': 'error',
'error': str(e)
})
@socketio.on('save_branches')
def handle_save_branches(data):
"""
@@ -3157,6 +3353,168 @@ def get_share_info(record_id: int):
return jsonify({'error': str(e)}), 500
# ==================== 任务分享页面 ====================
@app.route('/share/<share_token>', methods=['GET'])
def get_shared_plan_page(share_token: str):
"""获取分享任务页面(无需登录验证)"""
try:
with get_db_context() as db:
share = PlanShareCRUD.get_by_token(db, share_token)
if not share:
return jsonify({'error': '分享链接无效或已失效'}), 404
# 检查是否过期
if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now():
return jsonify({'error': '分享链接已过期'}), 404
# 增加查看次数
PlanShareCRUD.increment_view_count(db, share_token)
# 返回分享数据
task_data = share.task_data
return jsonify({
'share_token': share_token,
'task_id': share.task_id,
'task_data': task_data,
'created_at': share.created_at.isoformat() if share.created_at else None,
'view_count': share.view_count,
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/share/<share_token>/check', methods=['GET'])
def check_share_code(share_token: str):
"""检查分享链接是否需要提取码"""
try:
with get_db_context() as db:
share = PlanShareCRUD.get_by_token(db, share_token)
if not share:
return jsonify({'error': '分享链接无效或已失效'}), 404
# 检查是否过期
if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now():
return jsonify({'error': '分享链接已过期'}), 404
# 如果有提取码,则需要提取码
need_code = bool(share.extraction_code)
return jsonify({
'need_code': need_code,
'has_extraction_code': bool(share.extraction_code)
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/share/<share_token>', methods=['GET'])
def get_shared_plan_info(share_token: str):
"""获取分享任务详情API 接口,无需登录验证)"""
# 获取URL参数中的提取码
code = request.args.get('code', '').upper()
try:
with get_db_context() as db:
share = PlanShareCRUD.get_by_token(db, share_token)
if not share:
return jsonify({'error': '分享链接无效或已失效'}), 404
# 检查是否过期
if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now():
return jsonify({'error': '分享链接已过期'}), 404
# 验证提取码
if share.extraction_code:
if not code:
return jsonify({'error': '请输入提取码'}), 403
if code != share.extraction_code:
return jsonify({'error': '提取码错误'}), 403
# 增加查看次数
PlanShareCRUD.increment_view_count(db, share_token)
# 返回分享数据
task_data = share.task_data
return jsonify({
'share_token': share_token,
'task_id': share.task_id,
'task_data': task_data,
'created_at': share.created_at.isoformat() if share.created_at else None,
'expires_at': share.expires_at.isoformat() if share.expires_at else None,
'view_count': share.view_count,
'extraction_code': share.extraction_code,
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/share/import', methods=['POST'])
def import_shared_plan():
"""导入分享的任务到自己的历史记录HTTP API无需 WebSocket"""
try:
data = request.get_json()
share_token = data.get('share_token')
user_id = data.get('user_id')
if not share_token:
return jsonify({'error': '缺少 share_token'}), 400
if not user_id:
return jsonify({'error': '缺少 user_id请先登录'}), 401
with get_db_context() as db:
# 获取分享记录
share = PlanShareCRUD.get_by_token(db, share_token)
if not share:
return jsonify({'error': '分享链接无效或已失效'}), 404
# 检查是否过期
if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now():
return jsonify({'error': '分享链接已过期'}), 404
# 获取分享的任务数据
task_data = share.task_data
# 生成新的 task_id因为是导入到自己的账号
new_task_id = str(uuid.uuid4())
# 创建新的任务记录
task = MultiAgentTaskCRUD.create(
db=db,
task_id=new_task_id,
user_id=user_id,
query=task_data.get("general_goal", ""),
agents_info=task_data.get("agents_info", []),
task_outline=task_data.get("task_outline"),
assigned_agents=task_data.get("assigned_agents"),
agent_scores=task_data.get("agent_scores"),
result=task_data.get("result"),
)
# 如果有分支数据,也保存
if task_data.get("branches"):
MultiAgentTaskCRUD.update_branches(db, new_task_id, task_data["branches"])
# 如果有执行日志,也保存
if task_data.get("rehearsal_log"):
MultiAgentTaskCRUD.update_rehearsal_log(db, new_task_id, task_data["rehearsal_log"])
# 增加分享的查看次数
PlanShareCRUD.increment_view_count(db, share_token)
return jsonify({
'success': True,
'message': '导入成功',
'task_id': new_task_id,
})
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):
"""删除导出记录"""