chore: remove obsolete segmented server snapshot files

This commit is contained in:
JOJO 2026-03-07 18:45:45 +08:00
parent 5c92f93e8c
commit 067500b163
8 changed files with 0 additions and 4891 deletions

View File

@ -1,302 +0,0 @@
@app.route('/admin/monitor')
@login_required
@admin_required
def admin_monitor_page():
"""管理员监控页面入口"""
return send_from_directory(str(ADMIN_ASSET_DIR), 'index.html')
@app.route('/admin/policy')
@login_required
@admin_required
def admin_policy_page():
"""管理员策略配置页面"""
return send_from_directory(Path(app.static_folder) / 'admin_policy', 'index.html')
@app.route('/admin/custom-tools')
@login_required
@admin_required
def admin_custom_tools_page():
"""自定义工具管理页面"""
return send_from_directory(str(ADMIN_CUSTOM_TOOLS_DIR), 'index.html')
@app.route('/api/admin/balance', methods=['GET'])
@login_required
@admin_required
def admin_balance_api():
"""查询第三方账户余额Kimi/DeepSeek/Qwen"""
data = balance_client.fetch_all_balances()
return jsonify({"success": True, "data": data})
@app.route('/admin/assets/<path:filename>')
@login_required
@admin_required
def admin_asset_file(filename: str):
return send_from_directory(str(ADMIN_ASSET_DIR), filename)
@app.route('/user_upload/<path:filename>')
@login_required
def serve_user_upload(filename: str):
"""
直接向前端暴露当前登录用户的上传目录文件用于 <show_image src="/user_upload/..."> 等场景
- 仅登录用户可访问
- 路径穿越校验目标必须位于用户自己的 uploads_dir
"""
user = get_current_user_record()
if not user:
return redirect('/login')
workspace = user_manager.ensure_user_workspace(user.username)
uploads_dir = workspace.uploads_dir.resolve()
target = (uploads_dir / filename).resolve()
try:
target.relative_to(uploads_dir)
except ValueError:
abort(403)
if not target.exists() or not target.is_file():
abort(404)
return send_from_directory(str(uploads_dir), str(target.relative_to(uploads_dir)))
@app.route('/workspace/<path:filename>')
@login_required
def serve_workspace_file(filename: str):
"""
暴露当前登录用户项目目录下的文件主要用于图片展示
- 仅登录用户可访问自己的项目文件
- 路径穿越校验目标必须位于用户自己的 project_path
- 非图片直接拒绝避免误暴露其他文件
"""
user = get_current_user_record()
if not user:
return redirect('/login')
workspace = user_manager.ensure_user_workspace(user.username)
project_root = workspace.project_path.resolve()
target = (project_root / filename).resolve()
try:
target.relative_to(project_root)
except ValueError:
abort(403)
if not target.exists() or not target.is_file():
abort(404)
mime_type, _ = mimetypes.guess_type(str(target))
if not mime_type or not mime_type.startswith("image/"):
abort(415)
return send_from_directory(str(target.parent), target.name)
@app.route('/static/<path:filename>')
def static_files(filename):
"""提供静态文件"""
if filename.startswith('admin_dashboard'):
abort(404)
return send_from_directory('static', filename)
@app.route('/api/status')
@api_login_required
@with_terminal
def get_status(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取系统状态(增强版:包含对话信息)"""
status = terminal.get_status()
# 添加终端状态信息
if terminal.terminal_manager:
terminal_status = terminal.terminal_manager.list_terminals()
status['terminals'] = terminal_status
# 【新增】添加当前对话的详细信息
try:
current_conv = terminal.context_manager.current_conversation_id
status['conversation'] = status.get('conversation', {})
status['conversation']['current_id'] = current_conv
if current_conv and not current_conv.startswith('temp_'):
current_conv_data = terminal.context_manager.conversation_manager.load_conversation(current_conv)
if current_conv_data:
status['conversation']['title'] = current_conv_data.get('title', '未知对话')
status['conversation']['created_at'] = current_conv_data.get('created_at')
status['conversation']['updated_at'] = current_conv_data.get('updated_at')
except Exception as e:
print(f"[Status] 获取当前对话信息失败: {e}")
status['project_path'] = str(workspace.project_path)
try:
status['container'] = container_manager.get_container_status(username)
except Exception as exc:
status['container'] = {"success": False, "error": str(exc)}
status['version'] = AGENT_VERSION
try:
policy = resolve_admin_policy(user_manager.get_user(username))
status['admin_policy'] = {
"ui_blocks": policy.get("ui_blocks") or {},
"disabled_models": policy.get("disabled_models") or [],
"forced_category_states": policy.get("forced_category_states") or {},
"version": policy.get("updated_at"),
}
except Exception as exc:
debug_log(f"[status] 附加管理员策略失败: {exc}")
return jsonify(status)
@app.route('/api/container-status')
@api_login_required
@with_terminal
def get_container_status_api(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""轮询容器状态(供前端用量面板定时刷新)。"""
try:
status = container_manager.get_container_status(username)
return jsonify({"success": True, "data": status})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/project-storage')
@api_login_required
@with_terminal
def get_project_storage(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取项目目录占用情况,供前端轮询。"""
now = time.time()
cache_entry = PROJECT_STORAGE_CACHE.get(username)
if cache_entry and (now - cache_entry.get("ts", 0)) < PROJECT_STORAGE_CACHE_TTL_SECONDS:
return jsonify({"success": True, "data": cache_entry["data"]})
try:
file_manager = getattr(terminal, 'file_manager', None)
if not file_manager:
return jsonify({"success": False, "error": "文件管理器未初始化"}), 500
used_bytes = file_manager._get_project_size()
limit_bytes = PROJECT_MAX_STORAGE_MB * 1024 * 1024 if PROJECT_MAX_STORAGE_MB else None
usage_percent = (used_bytes / limit_bytes * 100) if limit_bytes else None
data = {
"used_bytes": used_bytes,
"limit_bytes": limit_bytes,
"limit_label": f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制",
"usage_percent": usage_percent
}
PROJECT_STORAGE_CACHE[username] = {"ts": now, "data": data}
return jsonify({"success": True, "data": data})
except Exception as exc:
stale = PROJECT_STORAGE_CACHE.get(username)
if stale:
return jsonify({"success": True, "data": stale.get("data"), "stale": True}), 200
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/admin/dashboard')
@api_login_required
@admin_api_required
def admin_dashboard_snapshot_api():
try:
snapshot = build_admin_dashboard_snapshot()
return jsonify({"success": True, "data": snapshot})
except Exception as exc:
logging.exception("Failed to build admin dashboard")
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/admin/policy', methods=['GET', 'POST'])
@api_login_required
@admin_api_required
def admin_policy_api():
if request.method == 'GET':
try:
data = admin_policy_manager.load_policy()
defaults = admin_policy_manager.describe_defaults()
return jsonify({"success": True, "data": data, "defaults": defaults})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
# POST 更新
payload = request.get_json() or {}
target_type = payload.get("target_type")
target_value = payload.get("target_value") or ""
config = payload.get("config") or {}
try:
saved = admin_policy_manager.save_scope_policy(target_type, target_value, config)
return jsonify({"success": True, "data": saved})
except ValueError as exc:
return jsonify({"success": False, "error": str(exc)}), 400
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/admin/custom-tools', methods=['GET', 'POST', 'DELETE'])
@api_login_required
@admin_api_required
def admin_custom_tools_api():
"""自定义工具管理(仅全局管理员)。"""
try:
if request.method == 'GET':
return jsonify({"success": True, "data": custom_tool_registry.list_tools()})
if request.method == 'POST':
payload = request.get_json() or {}
saved = custom_tool_registry.upsert_tool(payload)
return jsonify({"success": True, "data": saved})
# DELETE
tool_id = request.args.get("id") or (request.get_json() or {}).get("id")
if not tool_id:
return jsonify({"success": False, "error": "缺少 id"}), 400
removed = custom_tool_registry.delete_tool(tool_id)
if removed:
return jsonify({"success": True, "data": {"deleted": tool_id}})
return jsonify({"success": False, "error": "未找到该工具"}), 404
except ValueError as exc:
return jsonify({"success": False, "error": str(exc)}), 400
except Exception as exc:
logging.exception("custom-tools API error")
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/admin/custom-tools/file', methods=['GET', 'POST'])
@api_login_required
@admin_api_required
def admin_custom_tools_file_api():
tool_id = request.args.get("id") or (request.get_json() or {}).get("id")
name = request.args.get("name") or (request.get_json() or {}).get("name")
if not tool_id or not name:
return jsonify({"success": False, "error": "缺少 id 或 name"}), 400
tool_dir = Path(custom_tool_registry.root) / tool_id
if not tool_dir.exists():
return jsonify({"success": False, "error": "工具不存在"}), 404
target = tool_dir / name
if request.method == 'GET':
if not target.exists():
return jsonify({"success": False, "error": "文件不存在"}), 404
try:
return target.read_text(encoding="utf-8")
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
# POST 保存文件
payload = request.get_json() or {}
content = payload.get("content")
try:
target.write_text(content or "", encoding="utf-8")
return jsonify({"success": True})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/admin/custom-tools/reload', methods=['POST'])
@api_login_required
@admin_api_required
def admin_custom_tools_reload_api():
try:
custom_tool_registry.reload()
return jsonify({"success": True})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/effective-policy', methods=['GET'])
@api_login_required
def effective_policy_api():
record = get_current_user_record()
policy = resolve_admin_policy(record)
return jsonify({"success": True, "data": policy})

View File

@ -1,548 +0,0 @@
@app.route('/api/thinking-mode', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("thinking_mode_toggle", 15, 60, scope="user")
def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""切换思考模式"""
try:
data = request.get_json() or {}
requested_mode = data.get('mode')
if requested_mode in {"fast", "thinking", "deep"}:
target_mode = requested_mode
elif 'thinking_mode' in data:
target_mode = "thinking" if bool(data.get('thinking_mode')) else "fast"
else:
target_mode = terminal.run_mode
terminal.set_run_mode(target_mode)
if terminal.thinking_mode:
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
else:
terminal.api_client.start_new_task()
session['thinking_mode'] = terminal.thinking_mode
session['run_mode'] = terminal.run_mode
# 更新当前对话的元数据
ctx = terminal.context_manager
if ctx.current_conversation_id:
try:
ctx.conversation_manager.save_conversation(
conversation_id=ctx.current_conversation_id,
messages=ctx.conversation_history,
project_path=str(ctx.project_path),
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode,
model_key=getattr(terminal, "model_key", None)
)
except Exception as exc:
print(f"[API] 保存思考模式到对话失败: {exc}")
status = terminal.get_status()
socketio.emit('status_update', status, room=f"user_{username}")
return jsonify({
"success": True,
"data": {
"thinking_mode": terminal.thinking_mode,
"mode": terminal.run_mode
}
})
except Exception as exc:
print(f"[API] 切换思考模式失败: {exc}")
code = 400 if isinstance(exc, ValueError) else 500
return jsonify({
"success": False,
"error": str(exc),
"message": "切换思考模式时发生异常"
}), code
@app.route('/api/model', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("model_switch", 10, 60, scope="user")
def update_model(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""切换基础模型(快速/思考模型组合)。"""
try:
data = request.get_json() or {}
model_key = data.get("model_key")
if not model_key:
return jsonify({"success": False, "error": "缺少 model_key"}), 400
# 管理员禁用模型校验
policy = resolve_admin_policy(get_current_user_record())
disabled_models = set(policy.get("disabled_models") or [])
if model_key in disabled_models:
return jsonify({
"success": False,
"error": "该模型已被管理员禁用",
"message": "被管理员强制禁用"
}), 403
terminal.set_model(model_key)
# fast-only 时 run_mode 可能被强制为 fast
session["model_key"] = terminal.model_key
session["run_mode"] = terminal.run_mode
session["thinking_mode"] = terminal.thinking_mode
# 更新当前对话元数据
ctx = terminal.context_manager
if ctx.current_conversation_id:
try:
ctx.conversation_manager.save_conversation(
conversation_id=ctx.current_conversation_id,
messages=ctx.conversation_history,
project_path=str(ctx.project_path),
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode,
model_key=terminal.model_key,
has_images=getattr(ctx, "has_images", False)
)
except Exception as exc:
print(f"[API] 保存模型到对话失败: {exc}")
status = terminal.get_status()
socketio.emit('status_update', status, room=f"user_{username}")
return jsonify({
"success": True,
"data": {
"model_key": terminal.model_key,
"run_mode": terminal.run_mode,
"thinking_mode": terminal.thinking_mode
}
})
except Exception as exc:
print(f"[API] 切换模型失败: {exc}")
code = 400 if isinstance(exc, ValueError) else 500
return jsonify({"success": False, "error": str(exc), "message": str(exc)}), code
@app.route('/api/personalization', methods=['GET'])
@api_login_required
@with_terminal
def get_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取个性化配置"""
try:
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_personal_space"):
return jsonify({"success": False, "error": "个人空间已被管理员禁用"}), 403
data = load_personalization_config(workspace.data_dir)
return jsonify({
"success": True,
"data": data,
"tool_categories": terminal.get_tool_settings_snapshot(),
"thinking_interval_default": THINKING_FAST_INTERVAL,
"thinking_interval_range": {
"min": THINKING_INTERVAL_MIN,
"max": THINKING_INTERVAL_MAX
}
})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/personalization', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("personalization_update", 20, 300, scope="user")
def update_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""更新个性化配置"""
payload = request.get_json() or {}
try:
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_personal_space"):
return jsonify({"success": False, "error": "个人空间已被管理员禁用"}), 403
config = save_personalization_config(workspace.data_dir, payload)
try:
terminal.apply_personalization_preferences(config)
session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode
ctx = getattr(terminal, 'context_manager', None)
if ctx and getattr(ctx, 'current_conversation_id', None):
try:
ctx.conversation_manager.save_conversation(
conversation_id=ctx.current_conversation_id,
messages=ctx.conversation_history,
project_path=str(ctx.project_path),
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode
)
except Exception as meta_exc:
debug_log(f"应用个性化偏好失败: 同步对话元数据异常 {meta_exc}")
try:
status = terminal.get_status()
socketio.emit('status_update', status, room=f"user_{username}")
except Exception as status_exc:
debug_log(f"广播个性化状态失败: {status_exc}")
except Exception as exc:
debug_log(f"应用个性化偏好失败: {exc}")
return jsonify({
"success": True,
"data": config,
"tool_categories": terminal.get_tool_settings_snapshot(),
"thinking_interval_default": THINKING_FAST_INTERVAL,
"thinking_interval_range": {
"min": THINKING_INTERVAL_MIN,
"max": THINKING_INTERVAL_MAX
}
})
except ValueError as exc:
return jsonify({"success": False, "error": str(exc)}), 400
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/memory', methods=['GET'])
@api_login_required
@with_terminal
def api_memory_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""返回主/任务记忆条目列表,供虚拟显示器加载"""
memory_type = request.args.get('type', 'main')
if memory_type not in ('main', 'task'):
return jsonify({"success": False, "error": "type 必须是 main 或 task"}), 400
try:
entries = terminal.memory_manager._read_entries(memory_type) # type: ignore
return jsonify({"success": True, "type": memory_type, "entries": entries})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/gui/monitor_snapshot', methods=['GET'])
@api_login_required
def get_monitor_snapshot_api():
execution_id = request.args.get('executionId') or request.args.get('execution_id') or request.args.get('id')
if not execution_id:
return jsonify({
'success': False,
'error': '缺少 executionId 参数'
}), 400
stage = (request.args.get('stage') or 'before').lower()
if stage not in {'before', 'after'}:
stage = 'before'
snapshot = get_cached_monitor_snapshot(execution_id, stage)
if not snapshot:
return jsonify({
'success': False,
'error': '未找到对应快照'
}), 404
return jsonify({
'success': True,
'snapshot': snapshot,
'stage': stage
})
@app.route('/api/focused')
@api_login_required
@with_terminal
def get_focused_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""聚焦功能已废弃,返回空列表保持接口兼容。"""
return jsonify({})
@app.route('/api/todo-list')
@api_login_required
@with_terminal
def get_todo_list(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取当前待办列表"""
todo_snapshot = terminal.context_manager.get_todo_snapshot()
return jsonify({
"success": True,
"data": todo_snapshot
})
@app.route('/api/upload', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("legacy_upload", 20, 300, scope="user")
def upload_file(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""处理前端文件上传请求"""
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_upload"):
return jsonify({
"success": False,
"error": "文件上传已被管理员禁用",
"message": "被管理员禁用上传"
}), 403
if 'file' not in request.files:
return jsonify({
"success": False,
"error": "未找到文件",
"message": "请求中缺少文件字段"
}), 400
uploaded_file = request.files['file']
original_name = (request.form.get('filename') or '').strip()
if not uploaded_file or not uploaded_file.filename or uploaded_file.filename.strip() == '':
return jsonify({
"success": False,
"error": "文件名为空",
"message": "请选择要上传的文件"
}), 400
raw_name = original_name or uploaded_file.filename
filename = sanitize_filename_preserve_unicode(raw_name)
if not filename:
filename = secure_filename(raw_name)
if not filename:
return jsonify({
"success": False,
"error": "非法文件名",
"message": "文件名包含不支持的字符"
}), 400
file_manager = getattr(terminal, 'file_manager', None)
if file_manager is None:
return jsonify({
"success": False,
"error": "文件管理器未初始化"
}), 500
target_folder_relative = UPLOAD_FOLDER_NAME
valid_folder, folder_error, folder_path = file_manager._validate_path(target_folder_relative)
if not valid_folder:
return jsonify({
"success": False,
"error": folder_error
}), 400
try:
folder_path.mkdir(parents=True, exist_ok=True)
except Exception as exc:
return jsonify({
"success": False,
"error": f"创建上传目录失败: {exc}"
}), 500
target_relative = str(Path(target_folder_relative) / filename)
valid_file, file_error, target_full_path = file_manager._validate_path(target_relative)
if not valid_file:
return jsonify({
"success": False,
"error": file_error
}), 400
final_path = target_full_path
if final_path.exists():
stem = final_path.stem
suffix = final_path.suffix
counter = 1
while final_path.exists():
candidate_name = f"{stem}_{counter}{suffix}"
target_relative = str(Path(target_folder_relative) / candidate_name)
valid_file, file_error, candidate_path = file_manager._validate_path(target_relative)
if not valid_file:
return jsonify({
"success": False,
"error": file_error
}), 400
final_path = candidate_path
counter += 1
try:
relative_path = str(final_path.relative_to(workspace.project_path))
except Exception as exc:
return jsonify({
"success": False,
"error": f"路径解析失败: {exc}"
}), 400
guard = get_upload_guard(workspace)
try:
result = guard.process_upload(
uploaded_file,
final_path,
username=username,
source="legacy_upload",
original_name=raw_name,
relative_path=relative_path,
)
except UploadSecurityError as exc:
return build_upload_error_response(exc)
except Exception as exc:
return jsonify({
"success": False,
"error": f"保存文件失败: {exc}"
}), 500
metadata = result.get("metadata", {})
print(f"{OUTPUT_FORMATS['file']} 上传文件: {relative_path}")
return jsonify({
"success": True,
"path": relative_path,
"filename": final_path.name,
"folder": target_folder_relative,
"scan": metadata.get("scan"),
"sha256": metadata.get("sha256"),
"size": metadata.get("size"),
})
@app.errorhandler(RequestEntityTooLarge)
def handle_file_too_large(error):
"""全局捕获上传超大小"""
size_mb = MAX_UPLOAD_SIZE / (1024 * 1024)
return jsonify({
"success": False,
"error": "文件过大",
"message": f"单个文件大小不可超过 {size_mb:.1f} MB"
}), 413
@app.route('/api/download/file')
@api_login_required
@with_terminal
def download_file_api(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""下载单个文件"""
path = (request.args.get('path') or '').strip()
if not path:
return jsonify({"success": False, "error": "缺少路径参数"}), 400
valid, error, full_path = terminal.file_manager._validate_path(path)
if not valid or full_path is None:
return jsonify({"success": False, "error": error or "路径校验失败"}), 400
if not full_path.exists() or not full_path.is_file():
return jsonify({"success": False, "error": "文件不存在"}), 404
return send_file(
full_path,
as_attachment=True,
download_name=full_path.name
)
@app.route('/api/download/folder')
@api_login_required
@with_terminal
def download_folder_api(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""打包并下载文件夹"""
path = (request.args.get('path') or '').strip()
if not path:
return jsonify({"success": False, "error": "缺少路径参数"}), 400
valid, error, full_path = terminal.file_manager._validate_path(path)
if not valid or full_path is None:
return jsonify({"success": False, "error": error or "路径校验失败"}), 400
if not full_path.exists() or not full_path.is_dir():
return jsonify({"success": False, "error": "文件夹不存在"}), 404
buffer = BytesIO()
folder_name = Path(path).name or full_path.name or "archive"
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_buffer:
# 确保目录本身被包含
zip_buffer.write(full_path, arcname=folder_name + '/')
for item in full_path.rglob('*'):
relative_name = Path(folder_name) / item.relative_to(full_path)
if item.is_dir():
zip_buffer.write(item, arcname=str(relative_name) + '/')
else:
zip_buffer.write(item, arcname=str(relative_name))
buffer.seek(0)
return send_file(
buffer,
mimetype='application/zip',
as_attachment=True,
download_name=f"{folder_name}.zip"
)
@app.route('/api/tool-settings', methods=['GET', 'POST'])
@api_login_required
@with_terminal
def tool_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取或更新工具启用状态"""
if request.method == 'GET':
snapshot = terminal.get_tool_settings_snapshot()
return jsonify({
"success": True,
"categories": snapshot
})
data = request.get_json() or {}
category = data.get('category')
if category is None:
return jsonify({
"success": False,
"error": "缺少类别参数",
"message": "请求体需要提供 category 字段"
}), 400
if 'enabled' not in data:
return jsonify({
"success": False,
"error": "缺少启用状态",
"message": "请求体需要提供 enabled 字段"
}), 400
try:
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_tool_toggle"):
return jsonify({
"success": False,
"error": "工具开关已被管理员禁用",
"message": "被管理员强制禁用"
}), 403
enabled = bool(data['enabled'])
forced = getattr(terminal, "admin_forced_category_states", {}) or {}
if isinstance(forced.get(category), bool) and forced[category] != enabled:
return jsonify({
"success": False,
"error": "该工具类别已被管理员强制为启用/禁用,无法修改",
"message": "被管理员强制启用/禁用"
}), 403
terminal.set_tool_category_enabled(category, enabled)
snapshot = terminal.get_tool_settings_snapshot()
socketio.emit('tool_settings_updated', {
'categories': snapshot
}, room=f"user_{username}")
return jsonify({
"success": True,
"categories": snapshot
})
except ValueError as exc:
return jsonify({
"success": False,
"error": str(exc)
}), 400
@app.route('/api/terminals')
@api_login_required
@with_terminal
def get_terminals(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取终端会话列表"""
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
return jsonify({"success": False, "error": "实时终端已被管理员禁用"}), 403
if terminal.terminal_manager:
result = terminal.terminal_manager.list_terminals()
return jsonify(result)
else:
return jsonify({"sessions": [], "active": None, "total": 0})
@app.route('/api/socket-token', methods=['GET'])
@api_login_required
def issue_socket_token():
"""生成一次性 WebSocket token供握手阶段使用。"""
username = get_current_username()
prune_socket_tokens()
now = time.time()
for token_value, meta in list(pending_socket_tokens.items()):
if meta.get("username") == username:
pending_socket_tokens.pop(token_value, None)
token_value = secrets.token_urlsafe(32)
pending_socket_tokens[token_value] = {
"username": username,
"expires_at": now + SOCKET_TOKEN_TTL_SECONDS,
"fingerprint": (request.headers.get('User-Agent') or '')[:128],
}
return jsonify({
"success": True,
"token": token_value,
"expires_in": SOCKET_TOKEN_TTL_SECONDS
})

View File

@ -1,561 +0,0 @@
@app.route('/api/usage', methods=['GET'])
@api_login_required
def get_usage_stats():
"""返回当前用户的模型/搜索调用统计。"""
username = get_current_username()
tracker = get_or_create_usage_tracker(username)
if not tracker:
return jsonify({"success": False, "error": "未找到用户"}), 404
return jsonify({
"success": True,
"data": tracker.get_stats()
})
@app.route('/api/thinking-mode', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("thinking_mode_toggle", 15, 60, scope="user")
def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""切换思考模式"""
try:
data = request.get_json() or {}
requested_mode = data.get('mode')
if requested_mode in {"fast", "thinking", "deep"}:
target_mode = requested_mode
elif 'thinking_mode' in data:
target_mode = "thinking" if bool(data.get('thinking_mode')) else "fast"
else:
target_mode = terminal.run_mode
terminal.set_run_mode(target_mode)
if terminal.thinking_mode:
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
else:
terminal.api_client.start_new_task()
session['thinking_mode'] = terminal.thinking_mode
session['run_mode'] = terminal.run_mode
# 更新当前对话的元数据
ctx = terminal.context_manager
if ctx.current_conversation_id:
try:
ctx.conversation_manager.save_conversation(
conversation_id=ctx.current_conversation_id,
messages=ctx.conversation_history,
project_path=str(ctx.project_path),
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode,
model_key=getattr(terminal, "model_key", None)
)
except Exception as exc:
print(f"[API] 保存思考模式到对话失败: {exc}")
status = terminal.get_status()
socketio.emit('status_update', status, room=f"user_{username}")
return jsonify({
"success": True,
"data": {
"thinking_mode": terminal.thinking_mode,
"mode": terminal.run_mode
}
})
except Exception as exc:
print(f"[API] 切换思考模式失败: {exc}")
code = 400 if isinstance(exc, ValueError) else 500
return jsonify({
"success": False,
"error": str(exc),
"message": "切换思考模式时发生异常"
}), code
@app.route('/api/model', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("model_switch", 10, 60, scope="user")
def update_model(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""切换基础模型(快速/思考模型组合)。"""
try:
data = request.get_json() or {}
model_key = data.get("model_key")
if not model_key:
return jsonify({"success": False, "error": "缺少 model_key"}), 400
# 管理员禁用模型校验
policy = resolve_admin_policy(get_current_user_record())
disabled_models = set(policy.get("disabled_models") or [])
if model_key in disabled_models:
return jsonify({
"success": False,
"error": "该模型已被管理员禁用",
"message": "被管理员强制禁用"
}), 403
terminal.set_model(model_key)
# fast-only 时 run_mode 可能被强制为 fast
session["model_key"] = terminal.model_key
session["run_mode"] = terminal.run_mode
session["thinking_mode"] = terminal.thinking_mode
# 更新当前对话元数据
ctx = terminal.context_manager
if ctx.current_conversation_id:
try:
ctx.conversation_manager.save_conversation(
conversation_id=ctx.current_conversation_id,
messages=ctx.conversation_history,
project_path=str(ctx.project_path),
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode,
model_key=terminal.model_key,
has_images=getattr(ctx, "has_images", False)
)
except Exception as exc:
print(f"[API] 保存模型到对话失败: {exc}")
status = terminal.get_status()
socketio.emit('status_update', status, room=f"user_{username}")
return jsonify({
"success": True,
"data": {
"model_key": terminal.model_key,
"run_mode": terminal.run_mode,
"thinking_mode": terminal.thinking_mode
}
})
except Exception as exc:
print(f"[API] 切换模型失败: {exc}")
code = 400 if isinstance(exc, ValueError) else 500
return jsonify({"success": False, "error": str(exc), "message": str(exc)}), code
@app.route('/api/personalization', methods=['GET'])
@api_login_required
@with_terminal
def get_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取个性化配置"""
try:
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_personal_space"):
return jsonify({"success": False, "error": "个人空间已被管理员禁用"}), 403
data = load_personalization_config(workspace.data_dir)
return jsonify({
"success": True,
"data": data,
"tool_categories": terminal.get_tool_settings_snapshot(),
"thinking_interval_default": THINKING_FAST_INTERVAL,
"thinking_interval_range": {
"min": THINKING_INTERVAL_MIN,
"max": THINKING_INTERVAL_MAX
}
})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/personalization', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("personalization_update", 20, 300, scope="user")
def update_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""更新个性化配置"""
payload = request.get_json() or {}
try:
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_personal_space"):
return jsonify({"success": False, "error": "个人空间已被管理员禁用"}), 403
config = save_personalization_config(workspace.data_dir, payload)
try:
terminal.apply_personalization_preferences(config)
session['run_mode'] = terminal.run_mode
session['thinking_mode'] = terminal.thinking_mode
ctx = getattr(terminal, 'context_manager', None)
if ctx and getattr(ctx, 'current_conversation_id', None):
try:
ctx.conversation_manager.save_conversation(
conversation_id=ctx.current_conversation_id,
messages=ctx.conversation_history,
project_path=str(ctx.project_path),
todo_list=ctx.todo_list,
thinking_mode=terminal.thinking_mode,
run_mode=terminal.run_mode
)
except Exception as meta_exc:
debug_log(f"应用个性化偏好失败: 同步对话元数据异常 {meta_exc}")
try:
status = terminal.get_status()
socketio.emit('status_update', status, room=f"user_{username}")
except Exception as status_exc:
debug_log(f"广播个性化状态失败: {status_exc}")
except Exception as exc:
debug_log(f"应用个性化偏好失败: {exc}")
return jsonify({
"success": True,
"data": config,
"tool_categories": terminal.get_tool_settings_snapshot(),
"thinking_interval_default": THINKING_FAST_INTERVAL,
"thinking_interval_range": {
"min": THINKING_INTERVAL_MIN,
"max": THINKING_INTERVAL_MAX
}
})
except ValueError as exc:
return jsonify({"success": False, "error": str(exc)}), 400
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/memory', methods=['GET'])
@api_login_required
@with_terminal
def api_memory_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""返回主/任务记忆条目列表,供虚拟显示器加载"""
memory_type = request.args.get('type', 'main')
if memory_type not in ('main', 'task'):
return jsonify({"success": False, "error": "type 必须是 main 或 task"}), 400
try:
entries = terminal.memory_manager._read_entries(memory_type) # type: ignore
return jsonify({"success": True, "type": memory_type, "entries": entries})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/gui/monitor_snapshot', methods=['GET'])
@api_login_required
def get_monitor_snapshot_api():
execution_id = request.args.get('executionId') or request.args.get('execution_id') or request.args.get('id')
if not execution_id:
return jsonify({
'success': False,
'error': '缺少 executionId 参数'
}), 400
stage = (request.args.get('stage') or 'before').lower()
if stage not in {'before', 'after'}:
stage = 'before'
snapshot = get_cached_monitor_snapshot(execution_id, stage)
if not snapshot:
return jsonify({
'success': False,
'error': '未找到对应快照'
}), 404
return jsonify({
'success': True,
'snapshot': snapshot,
'stage': stage
})
@app.route('/api/focused')
@api_login_required
@with_terminal
def get_focused_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""聚焦功能已废弃,返回空列表保持接口兼容。"""
return jsonify({})
@app.route('/api/todo-list')
@api_login_required
@with_terminal
def get_todo_list(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取当前待办列表"""
todo_snapshot = terminal.context_manager.get_todo_snapshot()
return jsonify({
"success": True,
"data": todo_snapshot
})
@app.route('/api/upload', methods=['POST'])
@api_login_required
@with_terminal
@rate_limited("legacy_upload", 20, 300, scope="user")
def upload_file(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""处理前端文件上传请求"""
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_upload"):
return jsonify({
"success": False,
"error": "文件上传已被管理员禁用",
"message": "被管理员禁用上传"
}), 403
if 'file' not in request.files:
return jsonify({
"success": False,
"error": "未找到文件",
"message": "请求中缺少文件字段"
}), 400
uploaded_file = request.files['file']
original_name = (request.form.get('filename') or '').strip()
if not uploaded_file or not uploaded_file.filename or uploaded_file.filename.strip() == '':
return jsonify({
"success": False,
"error": "文件名为空",
"message": "请选择要上传的文件"
}), 400
raw_name = original_name or uploaded_file.filename
filename = sanitize_filename_preserve_unicode(raw_name)
if not filename:
filename = secure_filename(raw_name)
if not filename:
return jsonify({
"success": False,
"error": "非法文件名",
"message": "文件名包含不支持的字符"
}), 400
file_manager = getattr(terminal, 'file_manager', None)
if file_manager is None:
return jsonify({
"success": False,
"error": "文件管理器未初始化"
}), 500
target_folder_relative = UPLOAD_FOLDER_NAME
valid_folder, folder_error, folder_path = file_manager._validate_path(target_folder_relative)
if not valid_folder:
return jsonify({
"success": False,
"error": folder_error
}), 400
try:
folder_path.mkdir(parents=True, exist_ok=True)
except Exception as exc:
return jsonify({
"success": False,
"error": f"创建上传目录失败: {exc}"
}), 500
target_relative = str(Path(target_folder_relative) / filename)
valid_file, file_error, target_full_path = file_manager._validate_path(target_relative)
if not valid_file:
return jsonify({
"success": False,
"error": file_error
}), 400
final_path = target_full_path
if final_path.exists():
stem = final_path.stem
suffix = final_path.suffix
counter = 1
while final_path.exists():
candidate_name = f"{stem}_{counter}{suffix}"
target_relative = str(Path(target_folder_relative) / candidate_name)
valid_file, file_error, candidate_path = file_manager._validate_path(target_relative)
if not valid_file:
return jsonify({
"success": False,
"error": file_error
}), 400
final_path = candidate_path
counter += 1
try:
relative_path = str(final_path.relative_to(workspace.project_path))
except Exception as exc:
return jsonify({
"success": False,
"error": f"路径解析失败: {exc}"
}), 400
guard = get_upload_guard(workspace)
try:
result = guard.process_upload(
uploaded_file,
final_path,
username=username,
source="legacy_upload",
original_name=raw_name,
relative_path=relative_path,
)
except UploadSecurityError as exc:
return build_upload_error_response(exc)
except Exception as exc:
return jsonify({
"success": False,
"error": f"保存文件失败: {exc}"
}), 500
metadata = result.get("metadata", {})
print(f"{OUTPUT_FORMATS['file']} 上传文件: {relative_path}")
return jsonify({
"success": True,
"path": relative_path,
"filename": final_path.name,
"folder": target_folder_relative,
"scan": metadata.get("scan"),
"sha256": metadata.get("sha256"),
"size": metadata.get("size"),
})
@app.errorhandler(RequestEntityTooLarge)
def handle_file_too_large(error):
"""全局捕获上传超大小"""
size_mb = MAX_UPLOAD_SIZE / (1024 * 1024)
return jsonify({
"success": False,
"error": "文件过大",
"message": f"单个文件大小不可超过 {size_mb:.1f} MB"
}), 413
@app.route('/api/download/file')
@api_login_required
@with_terminal
def download_file_api(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""下载单个文件"""
path = (request.args.get('path') or '').strip()
if not path:
return jsonify({"success": False, "error": "缺少路径参数"}), 400
valid, error, full_path = terminal.file_manager._validate_path(path)
if not valid or full_path is None:
return jsonify({"success": False, "error": error or "路径校验失败"}), 400
if not full_path.exists() or not full_path.is_file():
return jsonify({"success": False, "error": "文件不存在"}), 404
return send_file(
full_path,
as_attachment=True,
download_name=full_path.name
)
@app.route('/api/download/folder')
@api_login_required
@with_terminal
def download_folder_api(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""打包并下载文件夹"""
path = (request.args.get('path') or '').strip()
if not path:
return jsonify({"success": False, "error": "缺少路径参数"}), 400
valid, error, full_path = terminal.file_manager._validate_path(path)
if not valid or full_path is None:
return jsonify({"success": False, "error": error or "路径校验失败"}), 400
if not full_path.exists() or not full_path.is_dir():
return jsonify({"success": False, "error": "文件夹不存在"}), 404
buffer = BytesIO()
folder_name = Path(path).name or full_path.name or "archive"
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_buffer:
# 确保目录本身被包含
zip_buffer.write(full_path, arcname=folder_name + '/')
for item in full_path.rglob('*'):
relative_name = Path(folder_name) / item.relative_to(full_path)
if item.is_dir():
zip_buffer.write(item, arcname=str(relative_name) + '/')
else:
zip_buffer.write(item, arcname=str(relative_name))
buffer.seek(0)
return send_file(
buffer,
mimetype='application/zip',
as_attachment=True,
download_name=f"{folder_name}.zip"
)
@app.route('/api/tool-settings', methods=['GET', 'POST'])
@api_login_required
@with_terminal
def tool_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取或更新工具启用状态"""
if request.method == 'GET':
snapshot = terminal.get_tool_settings_snapshot()
return jsonify({
"success": True,
"categories": snapshot
})
data = request.get_json() or {}
category = data.get('category')
if category is None:
return jsonify({
"success": False,
"error": "缺少类别参数",
"message": "请求体需要提供 category 字段"
}), 400
if 'enabled' not in data:
return jsonify({
"success": False,
"error": "缺少启用状态",
"message": "请求体需要提供 enabled 字段"
}), 400
try:
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_tool_toggle"):
return jsonify({
"success": False,
"error": "工具开关已被管理员禁用",
"message": "被管理员强制禁用"
}), 403
enabled = bool(data['enabled'])
forced = getattr(terminal, "admin_forced_category_states", {}) or {}
if isinstance(forced.get(category), bool) and forced[category] != enabled:
return jsonify({
"success": False,
"error": "该工具类别已被管理员强制为启用/禁用,无法修改",
"message": "被管理员强制启用/禁用"
}), 403
terminal.set_tool_category_enabled(category, enabled)
snapshot = terminal.get_tool_settings_snapshot()
socketio.emit('tool_settings_updated', {
'categories': snapshot
}, room=f"user_{username}")
return jsonify({
"success": True,
"categories": snapshot
})
except ValueError as exc:
return jsonify({
"success": False,
"error": str(exc)
}), 400
@app.route('/api/terminals')
@api_login_required
@with_terminal
def get_terminals(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取终端会话列表"""
policy = resolve_admin_policy(get_current_user_record())
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
return jsonify({"success": False, "error": "实时终端已被管理员禁用"}), 403
if terminal.terminal_manager:
result = terminal.terminal_manager.list_terminals()
return jsonify(result)
else:
return jsonify({"sessions": [], "active": None, "total": 0})
@app.route('/api/socket-token', methods=['GET'])
@api_login_required
def issue_socket_token():
"""生成一次性 WebSocket token供握手阶段使用。"""
username = get_current_username()
prune_socket_tokens()
now = time.time()
for token_value, meta in list(pending_socket_tokens.items()):
if meta.get("username") == username:
pending_socket_tokens.pop(token_value, None)
token_value = secrets.token_urlsafe(32)
pending_socket_tokens[token_value] = {
"username": username,
"expires_at": now + SOCKET_TOKEN_TTL_SECONDS,
"fingerprint": (request.headers.get('User-Agent') or '')[:128],
}
return jsonify({
"success": True,
"token": token_value,
"expires_in": SOCKET_TOKEN_TTL_SECONDS
})

View File

@ -1,20 +0,0 @@
DEFAULT_PORT = 8091
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
CSRF_HEADER_NAME = "X-CSRF-Token"
CSRF_SESSION_KEY = "_csrf_token"
CSRF_SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"}
CSRF_PROTECTED_PATHS = {"/login", "/register", "/logout"}
CSRF_PROTECTED_PREFIXES = ("/api/",)
CSRF_EXEMPT_PATHS = {"/api/csrf-token"}
FAILED_LOGIN_LIMIT = 5
FAILED_LOGIN_LOCK_SECONDS = 300
SOCKET_TOKEN_TTL_SECONDS = 45
PROJECT_STORAGE_CACHE: Dict[str, Dict[str, Any]] = {}
PROJECT_STORAGE_CACHE_TTL_SECONDS = float(os.environ.get("PROJECT_STORAGE_CACHE_TTL", "30"))
USER_IDLE_TIMEOUT_SECONDS = int(os.environ.get("USER_IDLE_TIMEOUT_SECONDS", "900"))
LAST_ACTIVE_FILE = Path(LOGS_DIR).expanduser().resolve() / "last_active.json"
_last_active_lock = threading.Lock()
_last_active_cache: Dict[str, float] = {}
_idle_reaper_started = False
TITLE_PROMPT_PATH = PROJECT_ROOT / "prompts" / "title_generation_prompt.txt"

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
user_manager = UserManager()
custom_tool_registry = CustomToolRegistry()
container_manager = UserContainerManager()
user_terminals: Dict[str, WebTerminal] = {}
terminal_rooms: Dict[str, set] = {}
connection_users: Dict[str, str] = {}
stop_flags: Dict[str, Dict[str, Any]] = {}
MONITOR_FILE_TOOLS = {'append_to_file', 'modify_file', 'write_file', 'edit_file'}
MONITOR_MEMORY_TOOLS = {'update_memory'}
MONITOR_SNAPSHOT_CHAR_LIMIT = 60000
MONITOR_MEMORY_ENTRY_LIMIT = 256
RATE_LIMIT_BUCKETS: Dict[str, deque] = defaultdict(deque)
FAILURE_TRACKERS: Dict[str, Dict[str, float]] = {}
pending_socket_tokens: Dict[str, Dict[str, Any]] = {}
usage_trackers: Dict[str, UsageTracker] = {}
MONITOR_SNAPSHOT_CACHE: Dict[str, Dict[str, Any]] = {}
MONITOR_SNAPSHOT_CACHE_LIMIT = 120
ADMIN_ASSET_DIR = (Path(app.static_folder) / 'admin_dashboard').resolve()
ADMIN_CUSTOM_TOOLS_DIR = (Path(app.static_folder) / 'custom_tools').resolve()
ADMIN_CUSTOM_TOOLS_DIR = (Path(app.static_folder) / 'custom_tools').resolve()
RECENT_UPLOAD_EVENT_LIMIT = 150
RECENT_UPLOAD_FEED_LIMIT = 60

View File

@ -1,297 +0,0 @@
@socketio.on('connect')
def handle_connect(auth):
"""客户端连接"""
print(f"[WebSocket] 客户端连接: {request.sid}")
username = get_current_username()
token_value = (auth or {}).get('socket_token') if isinstance(auth, dict) else None
if not username or not consume_socket_token(token_value, username):
emit('error', {'message': '未登录或连接凭证无效'})
disconnect()
return
emit('connected', {'status': 'Connected to server'})
connection_users[request.sid] = username
# 清理可能存在的停止标志和状态
stop_flags.pop(request.sid, None)
join_room(f"user_{username}")
join_room(f"user_{username}_terminal")
if request.sid not in terminal_rooms:
terminal_rooms[request.sid] = set()
terminal_rooms[request.sid].update({f"user_{username}", f"user_{username}_terminal"})
terminal, workspace = get_user_resources(username)
if terminal:
reset_system_state(terminal)
emit('system_ready', {
'project_path': str(workspace.project_path),
'thinking_mode': bool(getattr(terminal, "thinking_mode", False)),
'version': AGENT_VERSION
}, room=request.sid)
if terminal.terminal_manager:
terminals = terminal.terminal_manager.get_terminal_list()
emit('terminal_list_update', {
'terminals': terminals,
'active': terminal.terminal_manager.active_terminal
}, room=request.sid)
if terminal.terminal_manager.active_terminal:
for name, term in terminal.terminal_manager.terminals.items():
emit('terminal_started', {
'session': name,
'working_dir': str(term.working_dir),
'shell': term.shell_command,
'time': term.start_time.isoformat() if term.start_time else None
}, room=request.sid)
@socketio.on('disconnect')
def handle_disconnect():
"""客户端断开"""
print(f"[WebSocket] 客户端断开: {request.sid}")
username = connection_users.pop(request.sid, None)
task_info = stop_flags.get(request.sid)
if isinstance(task_info, dict):
task_info['stop'] = True
pending_task = task_info.get('task')
if pending_task and not pending_task.done():
debug_log(f"disconnect: cancel task for {request.sid}")
pending_task.cancel()
terminal = task_info.get('terminal')
if terminal:
reset_system_state(terminal)
# 清理停止标志
stop_flags.pop(request.sid, None)
# 从所有房间移除
for room in list(terminal_rooms.get(request.sid, [])):
leave_room(room)
if request.sid in terminal_rooms:
del terminal_rooms[request.sid]
if username:
leave_room(f"user_{username}")
leave_room(f"user_{username}_terminal")
@socketio.on('stop_task')
def handle_stop_task():
"""处理停止任务请求"""
print(f"[停止] 收到停止请求: {request.sid}")
task_info = stop_flags.get(request.sid)
if not isinstance(task_info, dict):
task_info = {'stop': False, 'task': None, 'terminal': None}
stop_flags[request.sid] = task_info
if task_info.get('task') and not task_info['task'].done():
debug_log(f"正在取消任务: {request.sid}")
task_info['task'].cancel()
task_info['stop'] = True
if task_info.get('terminal'):
reset_system_state(task_info['terminal'])
emit('stop_requested', {
'message': '停止请求已接收,正在取消任务...'
})
@socketio.on('terminal_subscribe')
def handle_terminal_subscribe(data):
"""订阅终端事件"""
session_name = data.get('session')
subscribe_all = data.get('all', False)
username, terminal, _ = get_terminal_for_sid(request.sid)
if not username or not terminal or not terminal.terminal_manager:
emit('error', {'message': 'Terminal system not initialized'})
return
policy = resolve_admin_policy(user_manager.get_user(username))
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
emit('error', {'message': '实时终端已被管理员禁用'})
return
if request.sid not in terminal_rooms:
terminal_rooms[request.sid] = set()
if subscribe_all:
# 订阅所有终端事件
room_name = f"user_{username}_terminal"
join_room(room_name)
terminal_rooms[request.sid].add(room_name)
print(f"[Terminal] {request.sid} 订阅所有终端事件")
# 发送当前终端状态
emit('terminal_subscribed', {
'type': 'all',
'terminals': terminal.terminal_manager.get_terminal_list()
})
elif session_name:
# 订阅特定终端会话
room_name = f'user_{username}_terminal_{session_name}'
join_room(room_name)
terminal_rooms[request.sid].add(room_name)
print(f"[Terminal] {request.sid} 订阅终端: {session_name}")
# 发送该终端的当前输出
output_result = terminal.terminal_manager.get_terminal_output(session_name, 100)
if output_result['success']:
emit('terminal_history', {
'session': session_name,
'output': output_result['output']
})
@socketio.on('terminal_unsubscribe')
def handle_terminal_unsubscribe(data):
"""取消订阅终端事件"""
session_name = data.get('session')
username = connection_users.get(request.sid)
if session_name:
room_name = f'user_{username}_terminal_{session_name}' if username else f'terminal_{session_name}'
leave_room(room_name)
if request.sid in terminal_rooms:
terminal_rooms[request.sid].discard(room_name)
print(f"[Terminal] {request.sid} 取消订阅终端: {session_name}")
@socketio.on('get_terminal_output')
def handle_get_terminal_output(data):
"""获取终端输出历史"""
session_name = data.get('session')
lines = data.get('lines', 50)
username, terminal, _ = get_terminal_for_sid(request.sid)
if not terminal or not terminal.terminal_manager:
emit('error', {'message': 'Terminal system not initialized'})
return
policy = resolve_admin_policy(user_manager.get_user(username))
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
emit('error', {'message': '实时终端已被管理员禁用'})
return
result = terminal.terminal_manager.get_terminal_output(session_name, lines)
if result['success']:
emit('terminal_output_history', {
'session': session_name,
'output': result['output'],
'is_interactive': result.get('is_interactive', False),
'last_command': result.get('last_command', '')
})
else:
emit('error', {'message': result['error']})
@socketio.on('send_message')
def handle_message(data):
"""处理用户消息"""
username, terminal, workspace = get_terminal_for_sid(request.sid)
if not terminal:
emit('error', {'message': 'System not initialized'})
return
message = (data.get('message') or '').strip()
images = data.get('images') or []
if not message and not images:
emit('error', {'message': '消息不能为空'})
return
if images and getattr(terminal, "model_key", None) not in {"qwen3-vl-plus", "kimi-k2.5"}:
emit('error', {'message': '当前模型不支持图片,请切换到 Qwen3.5 或 Kimi-k2.5'})
return
print(f"[WebSocket] 收到消息: {message}")
debug_log(f"\n{'='*80}\n新任务开始: {message}\n{'='*80}")
record_user_activity(username)
requested_conversation_id = data.get('conversation_id')
try:
conversation_id, created_new = ensure_conversation_loaded(terminal, requested_conversation_id)
except RuntimeError as exc:
emit('error', {'message': str(exc)})
return
try:
conv_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) or {}
except Exception:
conv_data = {}
title = conv_data.get('title', '新对话')
socketio.emit('conversation_resolved', {
'conversation_id': conversation_id,
'title': title,
'created': created_new
}, room=request.sid)
if created_new:
socketio.emit('conversation_list_update', {
'action': 'created',
'conversation_id': conversation_id
}, room=f"user_{username}")
socketio.emit('conversation_changed', {
'conversation_id': conversation_id,
'title': title
}, room=request.sid)
client_sid = request.sid
def send_to_client(event_type, data):
"""发送消息到客户端"""
socketio.emit(event_type, data, room=client_sid)
# 模型活动事件:用于刷新“在线”心跳(回复/工具调用都算活动)
activity_events = {
'ai_message_start', 'thinking_start', 'thinking_chunk', 'thinking_end',
'text_start', 'text_chunk', 'text_end',
'tool_preparing', 'tool_start', 'update_action',
'append_payload', 'modify_payload', 'system_message',
'task_complete'
}
last_model_activity = 0.0
def send_with_activity(event_type, data):
"""模型产生输出或调用工具时刷新活跃时间,防止长回复被误判下线。"""
nonlocal last_model_activity
if event_type in activity_events:
now = time.time()
# 轻量节流1 秒内多次事件只记一次
if now - last_model_activity >= 1.0:
record_user_activity(username)
last_model_activity = now
send_to_client(event_type, data)
# 传递客户端ID
images = data.get('images') or []
socketio.start_background_task(process_message_task, terminal, message, images, send_with_activity, client_sid, workspace, username)
@socketio.on('client_chunk_log')
def handle_client_chunk_log(data):
"""前端chunk日志上报"""
conversation_id = data.get('conversation_id')
chunk_index = int(data.get('index') or data.get('chunk_index') or 0)
elapsed = float(data.get('elapsed') or 0.0)
length = int(data.get('length') or len(data.get('content') or ""))
client_ts = float(data.get('ts') or 0.0)
log_frontend_chunk(conversation_id, chunk_index, elapsed, length, client_ts)
@socketio.on('client_stream_debug_log')
def handle_client_stream_debug_log(data):
"""前端流式调试日志"""
if not isinstance(data, dict):
return
entry = dict(data)
entry.setdefault('server_ts', time.time())
log_streaming_debug_entry(entry)
# 在 web_server.py 中添加以下对话管理API接口
# 添加在现有路由之后,@socketio 事件处理之前
# ==========================================
# 对话管理API接口
# ==========================================
# conversation routes moved to server/conversation.py
@app.route('/resource_busy')
def resource_busy_page():
return app.send_static_file('resource_busy.html'), 503

View File

@ -1,13 +0,0 @@
@app.route('/api/usage', methods=['GET'])
@api_login_required
def get_usage_stats():
"""返回当前用户的模型/搜索调用统计。"""
username = get_current_username()
tracker = get_or_create_usage_tracker(username)
if not tracker:
return jsonify({"success": False, "error": "未找到用户"}), 404
return jsonify({
"success": True,
"data": tracker.get_stats()
})