- 新增个人化设置 group_sidebar_by_workspace / sidebar_pinned_workspaces / sidebar_workspace_order,支持分组视图、置顶、排序 - 后端 conversation API 支持 workspace_id 参数,用于获取/创建/加载/删除 指定工作区的对话,避免切换视图时 session 漂移 - server/context.py 新增 update_session 参数,临时解析目标 terminal 时不污染 当前会话 - 前端 ConversationSidebar 实现分组折叠、运行中指示器、新建/更多操作菜单、 动画同步 - config/terminal.py 默认 TERMINAL_SANDBOX_MODE 由 docker 改为 host
731 lines
33 KiB
Python
731 lines
33 KiB
Python
"""用户终端与工作区相关的共享辅助函数。"""
|
||
from __future__ import annotations
|
||
from functools import wraps
|
||
from typing import Optional, Tuple, Dict, Any
|
||
from flask import session, jsonify, has_request_context
|
||
|
||
from core.web_terminal import WebTerminal
|
||
from modules.gui_file_manager import GuiFileManager
|
||
from modules.upload_security import UploadQuarantineManager, UploadSecurityError
|
||
from modules.personalization_manager import load_personalization_config
|
||
from modules.skills_manager import infer_private_skills_dir, sync_workspace_skills
|
||
from modules.host_workspace_manager import resolve_host_workspace
|
||
from utils.host_workspace_debug import write_host_workspace_debug
|
||
import json
|
||
from pathlib import Path
|
||
from modules.usage_tracker import UsageTracker
|
||
from config import (
|
||
DATA_DIR,
|
||
LOGS_DIR,
|
||
TERMINAL_SANDBOX_MODE,
|
||
UPLOAD_QUARANTINE_SUBDIR,
|
||
)
|
||
from config.model_profiles import get_registered_model_keys
|
||
|
||
from . import state
|
||
from .utils_common import debug_log
|
||
from .auth_helpers import get_current_username, get_current_user_record, get_current_user_role # will create helper module
|
||
|
||
|
||
def make_terminal_callback(username: str):
|
||
"""生成面向指定用户的广播函数"""
|
||
from .extensions import socketio
|
||
def _callback(event_type, data):
|
||
try:
|
||
socketio.emit(event_type, data, room=f"user_{username}")
|
||
except Exception as exc:
|
||
debug_log(f"广播事件失败 ({username}): {event_type} - {exc}")
|
||
return _callback
|
||
|
||
|
||
def attach_user_broadcast(terminal: WebTerminal, username: str):
|
||
"""确保终端的广播函数指向当前用户的房间"""
|
||
callback = make_terminal_callback(username)
|
||
terminal.message_callback = callback
|
||
if terminal.terminal_manager:
|
||
terminal.terminal_manager.broadcast = callback
|
||
|
||
|
||
def _make_terminal_key(username: str, workspace_id: Optional[str] = None) -> str:
|
||
return f"{username}::{workspace_id}" if workspace_id else username
|
||
|
||
|
||
def _set_terminal_workspace_label(terminal: WebTerminal, label: Optional[str]) -> None:
|
||
label_text = str(label or "").strip()
|
||
try:
|
||
terminal.workspace_label = label_text
|
||
except Exception:
|
||
pass
|
||
try:
|
||
if getattr(terminal, "context_manager", None):
|
||
terminal.context_manager.workspace_label = label_text
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _apply_workspace_personalization_preferences(terminal: WebTerminal, workspace, update_session: bool = True) -> None:
|
||
"""Apply persisted workspace personalization after policy/workspace resolution."""
|
||
try:
|
||
config = load_personalization_config(workspace.data_dir)
|
||
session_model = None
|
||
if has_request_context():
|
||
raw_session_model = session.get("model_key")
|
||
if isinstance(raw_session_model, str) and raw_session_model.strip():
|
||
session_model = raw_session_model.strip()
|
||
|
||
# default_model 是“新会话初始偏好”,不能在每次 /api/status、任务创建、
|
||
# 加载资源时覆盖用户已经在当前会话里手动切换的模型。
|
||
if session_model and session_model != getattr(terminal, "model_key", None):
|
||
try:
|
||
terminal.set_model(session_model)
|
||
except Exception as exc:
|
||
debug_log(f"[Personalization] 恢复会话模型失败: {session_model} ({exc})")
|
||
|
||
apply_default_model = not bool(session_model) and not bool(
|
||
getattr(terminal, "_workspace_default_model_applied", False)
|
||
)
|
||
terminal.apply_personalization_preferences(config, apply_default_model=apply_default_model)
|
||
if apply_default_model:
|
||
try:
|
||
terminal._workspace_default_model_applied = True
|
||
except Exception:
|
||
pass
|
||
if has_request_context() and update_session:
|
||
session["run_mode"] = getattr(terminal, "run_mode", session.get("run_mode"))
|
||
session["thinking_mode"] = getattr(terminal, "thinking_mode", session.get("thinking_mode"))
|
||
session["model_key"] = getattr(terminal, "model_key", session.get("model_key"))
|
||
except Exception as exc:
|
||
debug_log(f"[Personalization] 应用工作区偏好失败: {exc}")
|
||
|
||
|
||
def _ensure_workspace_skills_synced(terminal: WebTerminal, workspace) -> None:
|
||
"""
|
||
确保工作区 skills 已按当前个性化配置完成同步。
|
||
使用终端实例上的路径标记避免每次请求都重复全量拷贝。
|
||
"""
|
||
try:
|
||
project_path = str(Path(workspace.project_path).resolve())
|
||
except Exception:
|
||
project_path = str(workspace.project_path)
|
||
|
||
if getattr(terminal, "_skills_synced_project_path", None) == project_path:
|
||
return
|
||
|
||
try:
|
||
config = load_personalization_config(workspace.data_dir)
|
||
enabled_skills = config.get("enabled_skills") if isinstance(config, dict) else None
|
||
result = sync_workspace_skills(
|
||
workspace.project_path,
|
||
enabled_skills,
|
||
private_dir=infer_private_skills_dir(workspace.data_dir),
|
||
)
|
||
if not result.get("success"):
|
||
debug_log(f"[Skills] 工作区同步失败: {result.get('error')}")
|
||
return
|
||
terminal._skills_synced_project_path = project_path
|
||
debug_log(f"[Skills] 工作区技能已同步: {project_path} ({result.get('copied', 0)} 项)")
|
||
except Exception as exc:
|
||
debug_log(f"[Skills] 工作区同步异常: {exc}")
|
||
|
||
|
||
def get_user_resources(
|
||
username: Optional[str] = None,
|
||
workspace_id: Optional[str] = None,
|
||
update_session: bool = True,
|
||
) -> Tuple[Optional[WebTerminal], Optional['modules.user_manager.UserWorkspace']]:
|
||
from modules.user_manager import UserWorkspace
|
||
username = (username or get_current_username())
|
||
if not username:
|
||
return None, None
|
||
|
||
# 宿主机免登录模式:根据 host_workspaces.json 选择路径,不创建 /users/<user>/project
|
||
host_mode_session = bool(session.get("host_mode")) if has_request_context() else False
|
||
sandbox_is_host = (TERMINAL_SANDBOX_MODE or "host").lower() == "host"
|
||
if host_mode_session and sandbox_is_host:
|
||
# 宿主机多工作区并行:资源选择必须优先由显式 workspace_id / 当前请求 session 决定,
|
||
# 不能被进程级 HOST_ACTIVE_WORKSPACE_ID 覆盖,否则后台任务会在用户切换视图后串到新工作区。
|
||
selected_workspace_id = (
|
||
workspace_id
|
||
or (session.get("host_workspace_id") if has_request_context() else None)
|
||
or (session.get("workspace_id") if has_request_context() else None)
|
||
)
|
||
with state.HOST_ACTIVE_WORKSPACE_LOCK:
|
||
active_workspace_id = state.HOST_ACTIVE_WORKSPACE_ID
|
||
active_workspace_path = state.HOST_ACTIVE_WORKSPACE_PATH
|
||
active_workspace_version = state.HOST_ACTIVE_WORKSPACE_VERSION
|
||
if not selected_workspace_id and active_workspace_id:
|
||
selected_workspace_id = active_workspace_id
|
||
_, host_workspace = resolve_host_workspace(selected_workspace_id)
|
||
if (
|
||
active_workspace_id
|
||
and active_workspace_path
|
||
and selected_workspace_id == active_workspace_id
|
||
):
|
||
host_workspace = dict(host_workspace)
|
||
host_workspace["workspace_id"] = active_workspace_id
|
||
host_workspace["path"] = active_workspace_path
|
||
project_path = Path(host_workspace.get("path") or "").expanduser().resolve()
|
||
write_host_workspace_debug(
|
||
"context.get_user_resources.host.selected_workspace",
|
||
selected_workspace_id=selected_workspace_id,
|
||
active_workspace_id=active_workspace_id,
|
||
active_workspace_path=active_workspace_path,
|
||
active_workspace_version=active_workspace_version,
|
||
resolved_workspace_id=host_workspace.get("workspace_id"),
|
||
project_path=str(project_path),
|
||
username=username,
|
||
)
|
||
project_path.mkdir(parents=True, exist_ok=True)
|
||
data_dir = Path(DATA_DIR).expanduser().resolve()
|
||
data_dir.mkdir(parents=True, exist_ok=True)
|
||
logs_dir = Path(LOGS_DIR).expanduser().resolve()
|
||
logs_dir.mkdir(parents=True, exist_ok=True)
|
||
uploads_dir = project_path / ".agents" / "user_upload"
|
||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||
skills_dir = project_path / ".agents" / "skills"
|
||
skills_dir.mkdir(parents=True, exist_ok=True)
|
||
quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
|
||
if not quarantine_root.is_absolute():
|
||
quarantine_root = (project_path.parent / UPLOAD_QUARANTINE_SUBDIR).resolve()
|
||
quarantine_root.mkdir(parents=True, exist_ok=True)
|
||
|
||
workspace = UserWorkspace(
|
||
username="host",
|
||
root=project_path.parent,
|
||
project_path=project_path,
|
||
data_dir=data_dir,
|
||
logs_dir=logs_dir,
|
||
uploads_dir=uploads_dir,
|
||
quarantine_dir=quarantine_root,
|
||
)
|
||
if not hasattr(workspace, "workspace_id"):
|
||
workspace.workspace_id = host_workspace.get("workspace_id") or "default"
|
||
|
||
workspace_id_value = getattr(workspace, "workspace_id", None) or host_workspace.get("workspace_id") or "default"
|
||
term_key = _make_terminal_key("host", workspace_id_value)
|
||
container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key, preferred_mode="host")
|
||
usage_tracker = None # 宿主机模式不计配额
|
||
terminal = state.user_terminals.get(term_key)
|
||
target_project_path = str(project_path)
|
||
if terminal:
|
||
should_recreate_terminal = False
|
||
try:
|
||
current_project_path = str(Path(getattr(terminal, "project_path", "")).expanduser().resolve())
|
||
except Exception:
|
||
current_project_path = str(getattr(terminal, "project_path", ""))
|
||
try:
|
||
current_context_project_path = str(
|
||
Path(getattr(getattr(terminal, "context_manager", None), "project_path", "")).expanduser().resolve()
|
||
)
|
||
except Exception:
|
||
current_context_project_path = str(
|
||
getattr(getattr(terminal, "context_manager", None), "project_path", "")
|
||
)
|
||
if current_project_path != target_project_path or current_context_project_path != target_project_path:
|
||
write_host_workspace_debug(
|
||
"context.get_user_resources.host.path_mismatch",
|
||
terminal_id=id(terminal),
|
||
current_project_path=current_project_path,
|
||
current_context_project_path=current_context_project_path,
|
||
target_project_path=target_project_path,
|
||
)
|
||
try:
|
||
if hasattr(terminal, "update_project_path"):
|
||
terminal.update_project_path(target_project_path)
|
||
else:
|
||
should_recreate_terminal = True
|
||
except Exception as exc:
|
||
debug_log(f"[HostWorkspace] update_project_path 失败,回退重建终端: {exc}")
|
||
should_recreate_terminal = True
|
||
|
||
if not should_recreate_terminal:
|
||
try:
|
||
updated_project_path = str(Path(getattr(terminal, "project_path", "")).expanduser().resolve())
|
||
except Exception:
|
||
updated_project_path = str(getattr(terminal, "project_path", ""))
|
||
if updated_project_path != target_project_path:
|
||
should_recreate_terminal = True
|
||
|
||
if should_recreate_terminal:
|
||
write_host_workspace_debug(
|
||
"context.get_user_resources.host.recreate_terminal",
|
||
terminal_id=id(terminal),
|
||
target_project_path=target_project_path,
|
||
)
|
||
try:
|
||
if getattr(terminal, "terminal_manager", None):
|
||
terminal.terminal_manager.close_all()
|
||
except Exception:
|
||
pass
|
||
state.user_terminals.pop(term_key, None)
|
||
terminal = None
|
||
if not terminal:
|
||
run_mode = session.get('run_mode') if has_request_context() else None
|
||
thinking_mode_flag = session.get('thinking_mode') if has_request_context() else None
|
||
if run_mode not in {"fast", "thinking", "deep"}:
|
||
run_mode = "fast"
|
||
thinking_mode_flag = False
|
||
thinking_mode = bool(thinking_mode_flag) if thinking_mode_flag is not None else (run_mode != "fast")
|
||
terminal = WebTerminal(
|
||
project_path=str(project_path),
|
||
thinking_mode=thinking_mode,
|
||
run_mode=run_mode,
|
||
message_callback=make_terminal_callback("host"),
|
||
data_dir=str(data_dir),
|
||
container_session=container_handle,
|
||
usage_tracker=usage_tracker
|
||
)
|
||
if terminal.terminal_manager:
|
||
terminal.terminal_manager.broadcast = terminal.message_callback
|
||
state.user_terminals[term_key] = terminal
|
||
terminal.username = "host"
|
||
terminal.user_role = "admin"
|
||
terminal.quota_update_callback = None
|
||
if has_request_context() and update_session:
|
||
session['run_mode'] = terminal.run_mode
|
||
session['thinking_mode'] = terminal.thinking_mode
|
||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||
session['host_workspace_id'] = getattr(workspace, "workspace_id", None)
|
||
else:
|
||
terminal.update_container_session(container_handle)
|
||
attach_user_broadcast(terminal, "host")
|
||
terminal.username = "host"
|
||
terminal.user_role = "admin"
|
||
if has_request_context() and update_session:
|
||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||
session['host_workspace_id'] = getattr(workspace, "workspace_id", None)
|
||
_set_terminal_workspace_label(
|
||
terminal,
|
||
host_workspace.get("label") or host_workspace.get("workspace_id") or getattr(workspace, "workspace_id", None),
|
||
)
|
||
|
||
# 宿主机模式同样需要应用管理员策略(否则前端工具菜单会退化成静态基础分类)
|
||
try:
|
||
from core.tool_config import ToolCategory
|
||
from modules import admin_policy_manager
|
||
|
||
record = get_current_user_record()
|
||
role = get_current_user_role(record) if record else "admin"
|
||
invite_code = getattr(record, "invite_code", None) if record else None
|
||
policy = admin_policy_manager.get_effective_policy(
|
||
record.username if record else username,
|
||
role,
|
||
invite_code,
|
||
)
|
||
categories_map = {
|
||
cid: ToolCategory(
|
||
label=cat.get("label") or cid,
|
||
tools=list(cat.get("tools") or []),
|
||
default_enabled=bool(cat.get("default_enabled", True)),
|
||
silent_when_disabled=bool(cat.get("silent_when_disabled", False)),
|
||
)
|
||
for cid, cat in policy.get("categories", {}).items()
|
||
}
|
||
forced_states = policy.get("forced_category_states") or {}
|
||
disabled_models = policy.get("disabled_models") or []
|
||
terminal.set_admin_policy(categories_map, forced_states, disabled_models)
|
||
terminal.admin_policy_ui_blocks = policy.get("ui_blocks") or {}
|
||
terminal.admin_policy_version = policy.get("updated_at")
|
||
if terminal.model_key in disabled_models:
|
||
for candidate in get_registered_model_keys(visible_only=True):
|
||
if candidate not in disabled_models:
|
||
try:
|
||
terminal.set_model(candidate)
|
||
if has_request_context() and update_session:
|
||
session["model_key"] = terminal.model_key
|
||
break
|
||
except Exception:
|
||
continue
|
||
except Exception as exc:
|
||
debug_log(f"[admin_policy][host_mode] 应用失败: {exc}")
|
||
|
||
_apply_workspace_personalization_preferences(terminal, workspace)
|
||
_ensure_workspace_skills_synced(terminal, workspace)
|
||
write_host_workspace_debug(
|
||
"context.get_user_resources.host.return",
|
||
terminal_id=id(terminal),
|
||
terminal_project_path=str(getattr(terminal, "project_path", "")),
|
||
context_project_path=str(getattr(getattr(terminal, "context_manager", None), "project_path", "")),
|
||
workspace_project_path=str(getattr(workspace, "project_path", "")),
|
||
workspace_id=getattr(workspace, "workspace_id", None),
|
||
current_conversation_id=getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None),
|
||
)
|
||
return terminal, workspace
|
||
|
||
is_api_user = bool(session.get("is_api_user")) if has_request_context() else False
|
||
# API 用户与网页用户使用不同的 manager
|
||
if is_api_user:
|
||
record = None
|
||
if workspace_id is None:
|
||
raise RuntimeError("API 调用缺少 workspace_id")
|
||
workspace = state.api_user_manager.ensure_workspace(username, workspace_id)
|
||
else:
|
||
record = get_current_user_record()
|
||
selected_workspace_id = (
|
||
workspace_id
|
||
or (session.get("workspace_id") if has_request_context() else None)
|
||
or "default"
|
||
)
|
||
workspace = state.user_manager.ensure_user_workspace(username, selected_workspace_id)
|
||
# 为兼容后续逻辑,补充 workspace_id 属性
|
||
if not hasattr(workspace, "workspace_id"):
|
||
try:
|
||
workspace.workspace_id = selected_workspace_id or "default"
|
||
except Exception:
|
||
pass
|
||
workspace_id_value = getattr(workspace, "workspace_id", None) or "default"
|
||
term_key = _make_terminal_key(username, workspace_id_value)
|
||
container_handle = state.container_manager.ensure_container(username, str(workspace.project_path), container_key=term_key, preferred_mode="docker")
|
||
usage_tracker = None if is_api_user else get_or_create_usage_tracker(username, workspace)
|
||
terminal = state.user_terminals.get(term_key)
|
||
if not terminal:
|
||
run_mode = session.get('run_mode') if has_request_context() else None
|
||
thinking_mode_flag = session.get('thinking_mode') if has_request_context() else None
|
||
if run_mode not in {"fast", "thinking", "deep"}:
|
||
preferred_run_mode = None
|
||
try:
|
||
personal_config = load_personalization_config(workspace.data_dir)
|
||
candidate_mode = (personal_config or {}).get('default_run_mode')
|
||
if isinstance(candidate_mode, str) and candidate_mode.lower() in {"fast", "thinking", "deep"}:
|
||
preferred_run_mode = candidate_mode.lower()
|
||
except Exception as exc:
|
||
debug_log(f"[UserInit] 加载个性化偏好失败: {exc}")
|
||
|
||
if preferred_run_mode:
|
||
run_mode = preferred_run_mode
|
||
thinking_mode_flag = preferred_run_mode != "fast"
|
||
elif thinking_mode_flag:
|
||
run_mode = "deep"
|
||
else:
|
||
run_mode = "fast"
|
||
thinking_mode = run_mode != "fast"
|
||
terminal = WebTerminal(
|
||
project_path=str(workspace.project_path),
|
||
thinking_mode=thinking_mode,
|
||
run_mode=run_mode,
|
||
message_callback=make_terminal_callback(username),
|
||
data_dir=str(workspace.data_dir),
|
||
container_session=container_handle,
|
||
usage_tracker=usage_tracker
|
||
)
|
||
if terminal.terminal_manager:
|
||
terminal.terminal_manager.broadcast = terminal.message_callback
|
||
state.user_terminals[term_key] = terminal
|
||
terminal.username = username
|
||
terminal.user_role = "api" if is_api_user else get_current_user_role(record)
|
||
terminal.quota_update_callback = (lambda metric=None: emit_user_quota_update(username)) if not is_api_user else None
|
||
if has_request_context() and update_session:
|
||
session['run_mode'] = terminal.run_mode
|
||
session['thinking_mode'] = terminal.thinking_mode
|
||
session['model_key'] = getattr(terminal, "model_key", None)
|
||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||
else:
|
||
terminal.update_container_session(container_handle)
|
||
attach_user_broadcast(terminal, username)
|
||
terminal.username = username
|
||
terminal.user_role = "api" if is_api_user else get_current_user_role(record)
|
||
terminal.quota_update_callback = (lambda metric=None: emit_user_quota_update(username)) if not is_api_user else None
|
||
if has_request_context() and update_session:
|
||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||
|
||
if is_api_user:
|
||
workspace_label = workspace_id_value
|
||
else:
|
||
try:
|
||
workspace_label = (
|
||
state.user_manager.list_user_workspaces(username)
|
||
.get(workspace_id_value, {})
|
||
.get("label")
|
||
) or workspace_id_value
|
||
except Exception:
|
||
workspace_label = workspace_id_value
|
||
_set_terminal_workspace_label(terminal, workspace_label)
|
||
|
||
# 应用管理员策略
|
||
if not is_api_user:
|
||
try:
|
||
from core.tool_config import ToolCategory
|
||
from modules import admin_policy_manager
|
||
policy = admin_policy_manager.get_effective_policy(
|
||
record.username if record else None,
|
||
get_current_user_role(record),
|
||
getattr(record, "invite_code", None),
|
||
)
|
||
categories_map = {
|
||
cid: ToolCategory(
|
||
label=cat.get("label") or cid,
|
||
tools=list(cat.get("tools") or []),
|
||
default_enabled=bool(cat.get("default_enabled", True)),
|
||
silent_when_disabled=bool(cat.get("silent_when_disabled", False)),
|
||
)
|
||
for cid, cat in policy.get("categories", {}).items()
|
||
}
|
||
forced_states = policy.get("forced_category_states") or {}
|
||
disabled_models = policy.get("disabled_models") or []
|
||
terminal.set_admin_policy(categories_map, forced_states, disabled_models)
|
||
terminal.admin_policy_ui_blocks = policy.get("ui_blocks") or {}
|
||
terminal.admin_policy_version = policy.get("updated_at")
|
||
if terminal.model_key in disabled_models:
|
||
for candidate in get_registered_model_keys(visible_only=True):
|
||
if candidate not in disabled_models:
|
||
try:
|
||
terminal.set_model(candidate)
|
||
if update_session:
|
||
session["model_key"] = terminal.model_key
|
||
break
|
||
except Exception:
|
||
continue
|
||
except Exception as exc:
|
||
debug_log(f"[admin_policy] 应用失败: {exc}")
|
||
|
||
_apply_workspace_personalization_preferences(terminal, workspace, update_session=update_session)
|
||
_ensure_workspace_skills_synced(terminal, workspace)
|
||
return terminal, workspace
|
||
|
||
|
||
def get_or_create_usage_tracker(username: Optional[str], workspace: Optional['modules.user_manager.UserWorkspace'] = None) -> Optional[UsageTracker]:
|
||
if not username:
|
||
return None
|
||
tracker = state.usage_trackers.get(username)
|
||
if tracker:
|
||
return tracker
|
||
from modules.user_manager import UserWorkspace
|
||
if workspace is None:
|
||
workspace = state.user_manager.ensure_user_workspace(username)
|
||
record = state.user_manager.get_user(username)
|
||
role = getattr(record, "role", "user") if record else "user"
|
||
tracker = UsageTracker(str(workspace.data_dir), role=role or "user")
|
||
state.usage_trackers[username] = tracker
|
||
return tracker
|
||
|
||
|
||
def emit_user_quota_update(username: Optional[str]):
|
||
from .extensions import socketio
|
||
if not username:
|
||
return
|
||
tracker = get_or_create_usage_tracker(username)
|
||
if not tracker:
|
||
return
|
||
try:
|
||
snapshot = tracker.get_quota_snapshot()
|
||
socketio.emit('quota_update', {'quotas': snapshot}, room=f"user_{username}")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def apply_conversation_overrides(terminal: WebTerminal, workspace, conversation_id: Optional[str]):
|
||
"""根据对话元数据应用自定义 prompt / personalization(仅 API 用途)。"""
|
||
if not conversation_id:
|
||
return
|
||
conv_path = Path(workspace.data_dir) / "conversations" / f"{conversation_id}.json"
|
||
if not conv_path.exists():
|
||
return
|
||
try:
|
||
data = json.loads(conv_path.read_text(encoding="utf-8"))
|
||
meta = data.get("metadata") or {}
|
||
prompt_name = meta.get("custom_prompt_name")
|
||
personalization_name = meta.get("personalization_name")
|
||
# prompt override
|
||
if prompt_name:
|
||
prompt_path = Path(workspace.data_dir) / "prompts" / f"{prompt_name}.txt"
|
||
if prompt_path.exists():
|
||
terminal.context_manager.custom_system_prompt = prompt_path.read_text(encoding="utf-8")
|
||
else:
|
||
terminal.context_manager.custom_system_prompt = None
|
||
else:
|
||
terminal.context_manager.custom_system_prompt = None
|
||
# personalization override
|
||
if personalization_name:
|
||
pers_path = Path(workspace.data_dir) / "personalization" / f"{personalization_name}.json"
|
||
if pers_path.exists():
|
||
try:
|
||
terminal.context_manager.custom_personalization_config = json.loads(pers_path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
terminal.context_manager.custom_personalization_config = None
|
||
else:
|
||
terminal.context_manager.custom_personalization_config = None
|
||
else:
|
||
terminal.context_manager.custom_personalization_config = None
|
||
|
||
# 应用个性化偏好(含禁用工具分类)到当前终端
|
||
try:
|
||
terminal.apply_personalization_preferences(terminal.context_manager.custom_personalization_config)
|
||
except Exception as exc:
|
||
debug_log(f"[apply_overrides] 应用个性化失败: {exc}")
|
||
except Exception as exc:
|
||
debug_log(f"[apply_overrides] 读取对话元数据失败: {exc}")
|
||
|
||
|
||
def with_terminal(func):
|
||
"""注入用户专属终端和工作区"""
|
||
@wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
username = get_current_username()
|
||
try:
|
||
terminal, workspace = get_user_resources(username)
|
||
except RuntimeError as exc:
|
||
return jsonify({"error": str(exc), "code": "resource_busy"}), 503
|
||
if not terminal or not workspace:
|
||
return jsonify({"error": "System not initialized"}), 503
|
||
kwargs.update({
|
||
'terminal': terminal,
|
||
'workspace': workspace,
|
||
'username': username
|
||
})
|
||
return func(*args, **kwargs)
|
||
return wrapper
|
||
|
||
|
||
def get_terminal_for_sid(sid: str):
|
||
username = state.connection_users.get(sid)
|
||
if not username:
|
||
return None, None, None
|
||
try:
|
||
terminal, workspace = get_user_resources(username)
|
||
except RuntimeError:
|
||
return username, None, None
|
||
return username, terminal, workspace
|
||
|
||
|
||
def get_gui_manager(workspace):
|
||
return GuiFileManager(str(workspace.project_path))
|
||
|
||
|
||
def get_upload_guard(workspace):
|
||
return UploadQuarantineManager(workspace)
|
||
|
||
|
||
def build_upload_error_response(exc: UploadSecurityError):
|
||
status = 400
|
||
if exc.code in {"scanner_missing", "scanner_unavailable"}:
|
||
status = 500
|
||
return jsonify({
|
||
"success": False,
|
||
"error": str(exc),
|
||
"code": exc.code,
|
||
}), status
|
||
|
||
|
||
def ensure_conversation_loaded(
|
||
terminal: WebTerminal,
|
||
conversation_id: Optional[str],
|
||
workspace=None,
|
||
):
|
||
created_new = False
|
||
if not conversation_id:
|
||
result = terminal.create_new_conversation()
|
||
if not result.get("success"):
|
||
raise RuntimeError(result.get("message", "创建对话失败"))
|
||
conversation_id = result["conversation_id"]
|
||
if has_request_context():
|
||
session['run_mode'] = terminal.run_mode
|
||
session['thinking_mode'] = terminal.thinking_mode
|
||
created_new = True
|
||
else:
|
||
conversation_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}"
|
||
current_id = terminal.context_manager.current_conversation_id
|
||
if current_id != conversation_id:
|
||
load_result = terminal.load_conversation(conversation_id)
|
||
if not load_result.get("success"):
|
||
raise RuntimeError(load_result.get("message", "对话加载失败"))
|
||
write_host_workspace_debug(
|
||
"context.ensure_conversation_loaded.after_load",
|
||
terminal_id=id(terminal),
|
||
conversation_id=conversation_id,
|
||
terminal_project_path=str(getattr(terminal, "project_path", "")),
|
||
context_project_path=str(getattr(getattr(terminal, "context_manager", None), "project_path", "")),
|
||
metadata_project_path=(
|
||
getattr(getattr(terminal, "context_manager", None), "conversation_metadata", {}) or {}
|
||
).get("project_path"),
|
||
)
|
||
try:
|
||
conv_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) or {}
|
||
meta = conv_data.get("metadata", {}) or {}
|
||
run_mode_meta = meta.get("run_mode")
|
||
if run_mode_meta:
|
||
terminal.set_run_mode(run_mode_meta)
|
||
elif meta.get("thinking_mode"):
|
||
terminal.set_run_mode("thinking")
|
||
else:
|
||
terminal.set_run_mode("fast")
|
||
if terminal.thinking_mode:
|
||
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
|
||
else:
|
||
terminal.api_client.start_new_task()
|
||
if has_request_context():
|
||
session['run_mode'] = terminal.run_mode
|
||
session['thinking_mode'] = terminal.thinking_mode
|
||
session['model_key'] = getattr(terminal, "model_key", None)
|
||
except Exception:
|
||
pass
|
||
if workspace is not None:
|
||
try:
|
||
workspace_project_path = str(Path(workspace.project_path).expanduser().resolve())
|
||
terminal.update_project_path(workspace_project_path)
|
||
write_host_workspace_debug(
|
||
"context.ensure_conversation_loaded.reapply_workspace_path",
|
||
terminal_id=id(terminal),
|
||
conversation_id=conversation_id,
|
||
workspace_project_path=workspace_project_path,
|
||
terminal_project_path=str(getattr(terminal, "project_path", "")),
|
||
context_project_path=str(getattr(getattr(terminal, "context_manager", None), "project_path", "")),
|
||
)
|
||
except Exception as exc:
|
||
write_host_workspace_debug(
|
||
"context.ensure_conversation_loaded.reapply_workspace_path_failed",
|
||
terminal_id=id(terminal),
|
||
conversation_id=conversation_id,
|
||
error=str(exc),
|
||
)
|
||
# 应用对话级自定义 prompt / personalization(仅 API)。
|
||
# 注意:ensure_conversation_loaded 在 WebSocket/后台任务等多处复用,有些调用点拿不到 workspace;
|
||
# 因此这里允许 workspace 为空(仅跳过 override,不影响正常对话加载)。
|
||
if workspace is not None:
|
||
try:
|
||
apply_conversation_overrides(terminal, workspace, conversation_id)
|
||
except Exception as exc:
|
||
debug_log(f"[apply_overrides] 失败: {exc}")
|
||
return conversation_id, created_new
|
||
|
||
|
||
def reset_system_state(terminal: Optional[WebTerminal]):
|
||
"""完整重置系统状态"""
|
||
if not terminal:
|
||
return
|
||
try:
|
||
if hasattr(terminal, 'api_client') and terminal.api_client:
|
||
debug_log("重置API客户端状态")
|
||
terminal.api_client.start_new_task(force_deep=getattr(terminal, "deep_thinking_mode", False))
|
||
if hasattr(terminal, 'current_session_id'):
|
||
terminal.current_session_id += 1
|
||
debug_log(f"重置会话ID为: {terminal.current_session_id}")
|
||
web_attrs = ['streamingMessage', 'currentMessageIndex', 'preparingTools', 'activeTools']
|
||
for attr in web_attrs:
|
||
if hasattr(terminal, attr):
|
||
if attr in ['streamingMessage']:
|
||
setattr(terminal, attr, False)
|
||
elif attr in ['currentMessageIndex']:
|
||
setattr(terminal, attr, -1)
|
||
elif attr in ['preparingTools', 'activeTools'] and hasattr(getattr(terminal, attr), 'clear'):
|
||
getattr(terminal, attr).clear()
|
||
debug_log("系统状态重置完成")
|
||
except Exception as e:
|
||
debug_log(f"状态重置过程中出现错误: {e}")
|
||
import traceback
|
||
debug_log(f"错误详情: {traceback.format_exc()}")
|
||
|
||
|
||
__all__ = [
|
||
"get_user_resources",
|
||
"with_terminal",
|
||
"get_terminal_for_sid",
|
||
"get_gui_manager",
|
||
"get_upload_guard",
|
||
"build_upload_error_response",
|
||
"ensure_conversation_loaded",
|
||
"reset_system_state",
|
||
"get_or_create_usage_tracker",
|
||
"emit_user_quota_update",
|
||
"attach_user_broadcast",
|
||
]
|