- 989 行单文件拆为 server/context/ 9 模块(identity/broadcast/personalization/usage/upload/conversation/resources/decorators/reaper)+ __init__.py 兼容 re-export,外部 import 路径不变 - 新增 RuntimeIdentity 显式身份快照(identity.py),get_user_resources 支持参数化身份驱动,为任务线程拆除 Flask 隐式上下文做准备 - 测试 patch 目标随迁至 server.context.personalization;新增 test_runtime_identity_resources 覆盖 web/host/api 身份路由矩阵
156 lines
7.4 KiB
Python
156 lines
7.4 KiB
Python
"""对话加载保障与对话级覆盖(custom prompt / personalization)应用。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
from flask import session, has_request_context
|
||
|
||
from core.web_terminal import WebTerminal
|
||
from modules.i18n import tr
|
||
from server.utils_common import debug_log
|
||
from utils.host_workspace_debug import write_host_workspace_debug
|
||
|
||
|
||
def ensure_conversation_loaded(
|
||
terminal: WebTerminal,
|
||
conversation_id: Optional[str],
|
||
workspace=None,
|
||
update_session: bool = True,
|
||
):
|
||
"""确保对话加载到 terminal。
|
||
|
||
update_session=False(任务线程等非请求上下文路径)时跳过 session 回写;
|
||
回写本是「刷新用户会话」的适配层语义,执行链路不应产生该副作用。
|
||
"""
|
||
created_new = False
|
||
if not conversation_id:
|
||
result = terminal.create_new_conversation()
|
||
if not result.get("success"):
|
||
raise RuntimeError(result.get("message", tr("context.create_conversation_failed")))
|
||
conversation_id = result["conversation_id"]
|
||
if update_session and 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", tr("context.load_conversation_failed")))
|
||
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._get_conversation_manager_for_id(conversation_id).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")
|
||
try:
|
||
terminal.set_reasoning_effort(meta.get("reasoning_effort"))
|
||
except (ValueError, AttributeError):
|
||
pass
|
||
if update_session and 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 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")
|
||
# 安全:元数据中的名称必须过资源名校验,防存储型路径穿越
|
||
import re as _re
|
||
def _safe_name(v):
|
||
v = (v or "").strip()
|
||
return v if _re.fullmatch(r"[A-Za-z0-9_-]{1,64}", v) else None
|
||
prompt_name = _safe_name(prompt_name)
|
||
personalization_name = _safe_name(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
|
||
|
||
# 应用个性化偏好(含禁用工具分类)到当前终端;
|
||
# 对话加载链路不应用默认 模型/模式/推理强度(以对话 meta 为权威)
|
||
try:
|
||
terminal.apply_personalization_preferences(
|
||
terminal.context_manager.custom_personalization_config,
|
||
apply_default_modes=False,
|
||
)
|
||
except Exception as exc:
|
||
debug_log(f"[apply_overrides] 应用个性化失败: {exc}")
|
||
except Exception as exc:
|
||
debug_log(f"[apply_overrides] 读取对话元数据失败: {exc}")
|