chore: remove obsolete segmented server snapshot files
This commit is contained in:
parent
5c92f93e8c
commit
067500b163
@ -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})
|
|
||||||
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
})
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
})
|
|
||||||
|
|
||||||
@ -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
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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()
|
|
||||||
})
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user