- 工作区内部路径 .agents/ -> .astrion/ - 默认运行态数据根 ~/.agents/agents/ -> ~/.astrion/astrion/ - 环境变量 AGENTS_DATA_ROOT -> ASTRION_DATA_ROOT(无兼容) - 同步更新代码、测试、文档与脚本中的路径引用 - 新增 .gitignore 忽略 .astrion/ 与 .agents/ 运行态目录
337 lines
15 KiB
Python
337 lines
15 KiB
Python
from __future__ import annotations
|
|
from server.chat import chat_bp
|
|
import json, time
|
|
from datetime import datetime
|
|
from typing import Dict, Any, Optional
|
|
from pathlib import Path
|
|
from io import BytesIO
|
|
import zipfile
|
|
import os
|
|
|
|
from flask import Blueprint, jsonify, request, session, send_file
|
|
from werkzeug.utils import secure_filename
|
|
from werkzeug.exceptions import RequestEntityTooLarge
|
|
import secrets
|
|
|
|
from config import THINKING_FAST_INTERVAL, MAX_UPLOAD_SIZE, OUTPUT_FORMATS
|
|
from modules.personalization_manager import (
|
|
load_personalization_config,
|
|
resolve_context_compression_settings,
|
|
save_personalization_config,
|
|
THINKING_INTERVAL_MIN,
|
|
THINKING_INTERVAL_MAX,
|
|
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
|
|
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
|
|
)
|
|
from modules.skills_manager import (
|
|
get_skills_catalog,
|
|
infer_private_skills_dir,
|
|
merge_enabled_skills,
|
|
sync_workspace_skills,
|
|
)
|
|
from modules.upload_security import UploadSecurityError
|
|
from modules.host_sandbox_policy import load_policy, save_policy
|
|
from modules.user_manager import UserWorkspace
|
|
from core.web_terminal import WebTerminal
|
|
from config.model_profiles import get_model_context_window
|
|
|
|
from server.auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username
|
|
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker
|
|
from server.security import rate_limited, prune_socket_tokens
|
|
from server.utils_common import debug_log
|
|
from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
|
|
from server.state import tool_approval_manager, user_question_manager
|
|
from server.extensions import socketio
|
|
from server.monitor import get_cached_monitor_snapshot
|
|
from server.files import sanitize_filename_preserve_unicode
|
|
|
|
UPLOAD_FOLDER_NAME = ".astrion/user_upload"
|
|
def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: str, source: str = "notify") -> None:
|
|
notice = str(text or "").strip()
|
|
if not notice:
|
|
return
|
|
|
|
# 仅在“有运行中的任务”时注入 [系统通知|notify],空闲期切换不写入 user 消息。
|
|
try:
|
|
from server.tasks import task_manager
|
|
|
|
current_conv = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None)
|
|
running_tasks = [
|
|
r for r in task_manager.list_tasks(username) if r.status in {"pending", "running", "cancel_requested"}
|
|
]
|
|
if current_conv:
|
|
running_tasks.sort(key=lambda r: 0 if r.conversation_id == current_conv else 1)
|
|
if not running_tasks:
|
|
debug_log(f"[RuntimeMode] idle switch ignored notify injection: {notice}")
|
|
return
|
|
|
|
target = running_tasks[0]
|
|
result = task_manager.enqueue_runtime_guidance(
|
|
username=username,
|
|
task_id=target.task_id,
|
|
message=notice,
|
|
source=source,
|
|
)
|
|
if not result.get("success"):
|
|
debug_log(f"[RuntimeMode] enqueue runtime guidance failed: {result}")
|
|
except Exception as exc:
|
|
debug_log(f"[RuntimeMode] dispatch notice failed: {exc}")
|
|
|
|
@chat_bp.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),
|
|
has_images=getattr(ctx, "has_images", False),
|
|
has_videos=getattr(ctx, "has_videos", 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": {
|
|
"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
|
|
|
|
@chat_bp.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),
|
|
has_videos=getattr(ctx, "has_videos", 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
|
|
|
|
@chat_bp.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)
|
|
private_skills_dir = infer_private_skills_dir(workspace.data_dir)
|
|
skills_catalog = get_skills_catalog(private_dir=private_skills_dir)
|
|
enabled_skills = merge_enabled_skills(
|
|
data.get("enabled_skills"),
|
|
skills_catalog,
|
|
data.get("skills_catalog_snapshot"),
|
|
)
|
|
data_out = dict(data)
|
|
data_out["enabled_skills"] = enabled_skills
|
|
compression_settings = resolve_context_compression_settings(data_out)
|
|
try:
|
|
model_window = get_model_context_window(getattr(terminal, "model_key", None))
|
|
except Exception:
|
|
model_window = None
|
|
return jsonify({
|
|
"success": True,
|
|
"data": data_out,
|
|
"tool_categories": terminal.get_tool_settings_snapshot(),
|
|
"skills_catalog": skills_catalog,
|
|
"context_compression_settings": {
|
|
**compression_settings,
|
|
"context_window_tokens": min(
|
|
int(compression_settings.get("deep_trigger_tokens") or 0),
|
|
int(model_window or compression_settings.get("deep_trigger_tokens") or 0),
|
|
),
|
|
},
|
|
"thinking_interval_default": THINKING_FAST_INTERVAL,
|
|
"thinking_interval_range": {
|
|
"min": THINKING_INTERVAL_MIN,
|
|
"max": THINKING_INTERVAL_MAX
|
|
},
|
|
"recent_conversations_prompt_limit_range": {
|
|
"min": RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
|
|
"max": RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
|
|
}
|
|
})
|
|
except Exception as exc:
|
|
return jsonify({"success": False, "error": str(exc)}), 500
|
|
|
|
@chat_bp.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)
|
|
private_skills_dir = infer_private_skills_dir(workspace.data_dir)
|
|
skills_catalog = get_skills_catalog(private_dir=private_skills_dir)
|
|
enabled_skills = merge_enabled_skills(
|
|
config.get("enabled_skills"),
|
|
skills_catalog,
|
|
config.get("skills_catalog_snapshot"),
|
|
)
|
|
stored_skills = None if len(enabled_skills) == len(skills_catalog) else enabled_skills
|
|
catalog_snapshot = [item.get("id") for item in skills_catalog if item.get("id")]
|
|
if config.get("enabled_skills") != stored_skills or config.get("skills_catalog_snapshot") != catalog_snapshot:
|
|
config = dict(config)
|
|
config["enabled_skills"] = stored_skills
|
|
config["skills_catalog_snapshot"] = catalog_snapshot
|
|
config = save_personalization_config(workspace.data_dir, config)
|
|
try:
|
|
sync_workspace_skills(workspace.project_path, enabled_skills, private_dir=private_skills_dir)
|
|
except Exception as sync_exc:
|
|
debug_log(f"[Skills] 同步失败: {sync_exc}")
|
|
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}")
|
|
config_out = dict(config)
|
|
config_out["enabled_skills"] = enabled_skills
|
|
compression_settings = resolve_context_compression_settings(config_out)
|
|
try:
|
|
model_window = get_model_context_window(getattr(terminal, "model_key", None))
|
|
except Exception:
|
|
model_window = None
|
|
return jsonify({
|
|
"success": True,
|
|
"data": config_out,
|
|
"tool_categories": terminal.get_tool_settings_snapshot(),
|
|
"skills_catalog": skills_catalog,
|
|
"context_compression_settings": {
|
|
**compression_settings,
|
|
"context_window_tokens": min(
|
|
int(compression_settings.get("deep_trigger_tokens") or 0),
|
|
int(model_window or compression_settings.get("deep_trigger_tokens") or 0),
|
|
),
|
|
},
|
|
"thinking_interval_default": THINKING_FAST_INTERVAL,
|
|
"thinking_interval_range": {
|
|
"min": THINKING_INTERVAL_MIN,
|
|
"max": THINKING_INTERVAL_MAX
|
|
},
|
|
"recent_conversations_prompt_limit_range": {
|
|
"min": RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
|
|
"max": RECENT_CONVERSATIONS_PROMPT_LIMIT_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
|