agent-Specialization/server/context/conversation.py
JOJO b472f9fe5e refactor(context): server/context.py 拆分为子包,get_user_resources 参数化
- 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 身份路由矩阵
2026-09-07 16:14:53 +08:00

156 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""对话加载保障与对话级覆盖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}")