- 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 身份路由矩阵 Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com> Co-authored-by: Codex powered by ChatGPT-6-Astra <codex@example.com>
162 lines
7.0 KiB
Python
162 lines
7.0 KiB
Python
"""系统状态重置与对话级 terminal 24h TTL 回收器。"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import time
|
||
from typing import Optional
|
||
|
||
from core.web_terminal import WebTerminal
|
||
from server import state
|
||
from server.utils_common import debug_log
|
||
|
||
|
||
def reset_system_state(terminal: Optional[WebTerminal]):
|
||
"""完整重置系统状态"""
|
||
if not terminal:
|
||
return
|
||
try:
|
||
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()}")
|
||
|
||
|
||
# ====== 对话级 terminal 24h TTL 回收器 ======
|
||
# 对话级 terminal(key 为 username::workspace_id::conversation_id 三段)常驻内存,
|
||
# 仅当「超过 TTL 无活动 且 该对话无运行中工作」时回收;工作区级服务 terminal 不回收。
|
||
CONVERSATION_TERMINAL_TTL_SECONDS = float(os.environ.get("CONVERSATION_TERMINAL_TTL_SECONDS", str(24 * 3600)))
|
||
CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS = float(os.environ.get("CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS", "600"))
|
||
_conversation_terminal_reaper_started = False
|
||
|
||
|
||
def _conversation_terminal_has_running_work(
|
||
username: str,
|
||
workspace_id: str,
|
||
conversation_id: str,
|
||
terminal: WebTerminal,
|
||
) -> bool:
|
||
"""判定对话是否仍有运行中的工作(主任务/子智能体/后台命令/多智能体)。
|
||
|
||
判定失败时保守返回 True(不回收)。
|
||
"""
|
||
try:
|
||
from server.tasks import task_manager
|
||
active_statuses = {"pending", "running", "cancel_requested"}
|
||
for rec in task_manager.list_tasks(username, workspace_id):
|
||
if rec.conversation_id == conversation_id and rec.status in active_statuses:
|
||
return True
|
||
status = task_manager.get_conversation_running_status(terminal, conversation_id)
|
||
if any(bool(v) for v in (status or {}).values()):
|
||
return True
|
||
except Exception as exc:
|
||
debug_log(f"[ConvTerminalReaper] 运行状态判定失败 {conversation_id}: {exc}")
|
||
return True
|
||
return False
|
||
|
||
|
||
def reap_idle_conversation_terminals(now: Optional[float] = None) -> int:
|
||
"""回收超过 TTL 且无运行任务的对话级 terminal,返回回收数量(可测试)。"""
|
||
now = now or time.time()
|
||
reaped = 0
|
||
for term_key, terminal in list(state.user_terminals.items()):
|
||
parts = term_key.split("::")
|
||
if len(parts) < 3:
|
||
continue # 工作区级服务 terminal 不回收
|
||
username, workspace_id = parts[0], parts[1]
|
||
conversation_id = "::".join(parts[2:])
|
||
try:
|
||
last_active = float(getattr(terminal, "last_activity_at", None))
|
||
except (TypeError, ValueError):
|
||
last_active = None
|
||
if last_active is None:
|
||
# 无时间戳实例(旧版本创建):补上当前时间,下轮再判定
|
||
try:
|
||
terminal.last_activity_at = now
|
||
except Exception:
|
||
pass
|
||
continue
|
||
if now - last_active < CONVERSATION_TERMINAL_TTL_SECONDS:
|
||
continue
|
||
if _conversation_terminal_has_running_work(username, workspace_id, conversation_id, terminal):
|
||
continue
|
||
# 竞态防护:判定到关闭之间存在窗口,期间新请求可能拿到该实例并建任务。
|
||
# 先打关闭标记(get_user_resources 见到标记会原地重建新实例),
|
||
# 并二次确认活动时间/运行工作未变化,最后 pop 时校验实例身份。
|
||
try:
|
||
terminal._reaper_closing = True
|
||
except Exception:
|
||
pass
|
||
aborted = False
|
||
latest_active = float(getattr(terminal, "last_activity_at", 0) or 0)
|
||
if latest_active > last_active:
|
||
debug_log(f"[ConvTerminalReaper] 关闭前检测到新活动,取消回收: {term_key}")
|
||
aborted = True
|
||
elif _conversation_terminal_has_running_work(username, workspace_id, conversation_id, terminal):
|
||
debug_log(f"[ConvTerminalReaper] 关闭前检测到运行任务,取消回收: {term_key}")
|
||
aborted = True
|
||
if aborted:
|
||
try:
|
||
terminal._reaper_closing = False
|
||
except Exception:
|
||
pass
|
||
continue
|
||
try:
|
||
cm = getattr(terminal, "context_manager", None)
|
||
# 与 __del__ 同理:空 history 保存会把磁盘上非空对话覆盖为空
|
||
if cm and getattr(cm, "current_conversation_id", None) and getattr(cm, "conversation_history", None):
|
||
cm.save_current_conversation()
|
||
except Exception as exc:
|
||
debug_log(f"[ConvTerminalReaper] 保存对话失败 {conversation_id}: {exc}")
|
||
try:
|
||
tm = getattr(terminal, "terminal_manager", None)
|
||
if tm:
|
||
tm.close_all()
|
||
except Exception as exc:
|
||
debug_log(f"[ConvTerminalReaper] 关闭 shell 失败 {conversation_id}: {exc}")
|
||
try:
|
||
mcp = getattr(terminal, "mcp_client_manager", None)
|
||
if mcp:
|
||
mcp.close_all_clients()
|
||
except Exception as exc:
|
||
debug_log(f"[ConvTerminalReaper] 关闭 MCP 失败 {conversation_id}: {exc}")
|
||
# 仅当缓存里仍是本实例时才移除(可能已被请求侧原地重建)
|
||
if state.user_terminals.get(term_key) is terminal:
|
||
state.user_terminals.pop(term_key, None)
|
||
reaped += 1
|
||
debug_log(f"[ConvTerminalReaper] 已回收对话级 terminal: {term_key} (idle {int(now - last_active)}s)")
|
||
return reaped
|
||
|
||
|
||
def _conversation_terminal_reaper_loop():
|
||
"""后台循环:定期扫描回收空闲对话级 terminal。"""
|
||
while True:
|
||
try:
|
||
reap_idle_conversation_terminals()
|
||
time.sleep(CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS)
|
||
except Exception as exc:
|
||
debug_log(f"[ConvTerminalReaper] 后台循环异常: {exc}")
|
||
time.sleep(CONVERSATION_TERMINAL_REAP_INTERVAL_SECONDS)
|
||
|
||
|
||
def start_conversation_terminal_reaper():
|
||
"""幂等启动对话级 terminal TTL 回收后台线程。"""
|
||
global _conversation_terminal_reaper_started
|
||
if _conversation_terminal_reaper_started:
|
||
return
|
||
_conversation_terminal_reaper_started = True
|
||
from server.extensions import socketio
|
||
socketio.start_background_task(_conversation_terminal_reaper_loop)
|