refactor(runtime-mode): 运行期模式切换通知改为基线漂移检测机制
This commit is contained in:
parent
2da6c87245
commit
dc61bd92dc
@ -85,8 +85,9 @@ MAX_ACTIVE_USER_CONTAINERS=8
|
|||||||
# === MCP 工具扩展(可选) =====================================================
|
# === MCP 工具扩展(可选) =====================================================
|
||||||
# 1=启用,0=禁用
|
# 1=启用,0=禁用
|
||||||
MCP_TOOLS_ENABLED=1
|
MCP_TOOLS_ENABLED=1
|
||||||
# MCP 服务配置文件路径(默认 data/mcp_servers.json)
|
# MCP 服务配置文件路径(默认 {DATA_DIR}/mcp_servers.json,即 ~/.astrion/astrion/<mode>/data/ 下,不在源码树)
|
||||||
MCP_SERVERS_FILE=./data/mcp_servers.json
|
# 下面仅为覆盖示例,按需启用:
|
||||||
|
# MCP_SERVERS_FILE=./data/mcp_servers.json
|
||||||
# MCP 协议版本(默认 2025-06-18)
|
# MCP 协议版本(默认 2025-06-18)
|
||||||
MCP_PROTOCOL_VERSION=2025-06-18
|
MCP_PROTOCOL_VERSION=2025-06-18
|
||||||
# MCP 默认超时(秒)
|
# MCP 默认超时(秒)
|
||||||
|
|||||||
@ -323,13 +323,16 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
current = self.get_permission_mode() if hasattr(self, "get_permission_mode") else "unrestricted"
|
current = self.get_permission_mode() if hasattr(self, "get_permission_mode") else "unrestricted"
|
||||||
if pending_permission != current:
|
if pending_permission != current:
|
||||||
self.set_permission_mode(pending_permission, persist=False)
|
self.set_permission_mode(pending_permission, persist=False)
|
||||||
label = {
|
if hasattr(self, "build_runtime_mode_switch_notice"):
|
||||||
"readonly": "只读",
|
notice_text = self.build_runtime_mode_switch_notice("permission_mode", pending_permission)
|
||||||
"approval": "审批",
|
else:
|
||||||
"auto_approval": "自动审核",
|
notice_text = f"权限模式被用户修改为 {pending_permission}"
|
||||||
"unrestricted": "无限制",
|
notices.append({
|
||||||
}.get(pending_permission, pending_permission)
|
"text": notice_text,
|
||||||
notices.append({"text": f"权限模式被用户修改为 {label}", "source": "权限变更"})
|
"source": "permission_change",
|
||||||
|
"kind": "permission_mode",
|
||||||
|
"mode": pending_permission,
|
||||||
|
})
|
||||||
updates["permission_mode"] = pending_permission
|
updates["permission_mode"] = pending_permission
|
||||||
updates["pending_permission_mode"] = None
|
updates["pending_permission_mode"] = None
|
||||||
self.pending_permission_mode = None
|
self.pending_permission_mode = None
|
||||||
@ -339,12 +342,18 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
current_exec = self.get_execution_mode()
|
current_exec = self.get_execution_mode()
|
||||||
if pending_execution != current_exec:
|
if pending_execution != current_exec:
|
||||||
self.set_execution_mode(pending_execution)
|
self.set_execution_mode(pending_execution)
|
||||||
label = {"sandbox": "沙箱", "direct": "完全访问权限"}.get(pending_execution, pending_execution)
|
if hasattr(self, "build_runtime_mode_switch_notice"):
|
||||||
if hasattr(self, "_build_execution_mode_switch_notice"):
|
notice_text = self.build_runtime_mode_switch_notice("execution_mode", pending_execution)
|
||||||
|
elif hasattr(self, "_build_execution_mode_switch_notice"):
|
||||||
notice_text = self._build_execution_mode_switch_notice(pending_execution)
|
notice_text = self._build_execution_mode_switch_notice(pending_execution)
|
||||||
else:
|
else:
|
||||||
notice_text = f"执行环境被用户修改为 {label}"
|
notice_text = f"执行环境被用户修改为 {pending_execution}"
|
||||||
notices.append({"text": notice_text, "source": "执行环境变更"})
|
notices.append({
|
||||||
|
"text": notice_text,
|
||||||
|
"source": "execution_change",
|
||||||
|
"kind": "execution_mode",
|
||||||
|
"mode": pending_execution,
|
||||||
|
})
|
||||||
updates["execution_mode"] = pending_execution
|
updates["execution_mode"] = pending_execution
|
||||||
updates["pending_execution_mode"] = None
|
updates["pending_execution_mode"] = None
|
||||||
self.pending_execution_mode = None
|
self.pending_execution_mode = None
|
||||||
@ -354,8 +363,16 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
current_network = self.get_network_permission()
|
current_network = self.get_network_permission()
|
||||||
if pending_network != current_network:
|
if pending_network != current_network:
|
||||||
self.set_network_permission(pending_network)
|
self.set_network_permission(pending_network)
|
||||||
label = {"restricted": "受限", "full": "完全开放", "none": "完全禁止"}.get(pending_network, pending_network)
|
if hasattr(self, "build_runtime_mode_switch_notice"):
|
||||||
notices.append({"text": f"网络权限被用户修改为 {label}", "source": "网络权限变更"})
|
notice_text = self.build_runtime_mode_switch_notice("network_permission", pending_network)
|
||||||
|
else:
|
||||||
|
notice_text = f"网络权限被用户修改为 {pending_network}"
|
||||||
|
notices.append({
|
||||||
|
"text": notice_text,
|
||||||
|
"source": "network_change",
|
||||||
|
"kind": "network_permission",
|
||||||
|
"mode": pending_network,
|
||||||
|
})
|
||||||
updates["network_permission"] = pending_network
|
updates["network_permission"] = pending_network
|
||||||
updates["pending_network_permission"] = None
|
updates["pending_network_permission"] = None
|
||||||
self.pending_network_permission = None
|
self.pending_network_permission = None
|
||||||
|
|||||||
@ -110,6 +110,25 @@ class ModeMixin:
|
|||||||
"direct": "完全访问权限",
|
"direct": "完全访问权限",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_NETWORK_PERMISSION_LABEL = {
|
||||||
|
"restricted": "受限",
|
||||||
|
"full": "完全开放",
|
||||||
|
"none": "完全禁止",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 运行期模式(权限/执行环境/网络权限)切换通知的 kind 与 source 映射。
|
||||||
|
# source 需与 server/chat_flow_task_support.py 的 _VALID_SOURCES 保持一致。
|
||||||
|
_RUNTIME_MODE_KINDS = ("permission_mode", "execution_mode", "network_permission")
|
||||||
|
_RUNTIME_MODE_SOURCE = {
|
||||||
|
"permission_mode": "permission_change",
|
||||||
|
"execution_mode": "execution_change",
|
||||||
|
"network_permission": "network_change",
|
||||||
|
}
|
||||||
|
# conversation metadata key:本对话「智能体已知晓」的运行期模式基线。
|
||||||
|
# 只在通知真正注入对话后更新;空闲期切换不改它,从而在下一条 user 消息时
|
||||||
|
# 通过 drift 检测一次性补发通知(天然去重:怎么切都只比较最终值)。
|
||||||
|
_RUNTIME_MODE_BASELINE_META_KEY = "runtime_mode_baseline"
|
||||||
|
|
||||||
def _build_permission_mode_message(self) -> Optional[str]:
|
def _build_permission_mode_message(self) -> Optional[str]:
|
||||||
"""根据当前权限模式构建权限说明消息(从模板读取并替换占位符)"""
|
"""根据当前权限模式构建权限说明消息(从模板读取并替换占位符)"""
|
||||||
template = self.load_prompt("permission_mode")
|
template = self.load_prompt("permission_mode")
|
||||||
@ -317,6 +336,87 @@ class ModeMixin:
|
|||||||
"此前的命令如果是按 Linux 沙箱环境编写的,请按上述规则改写后重新执行。"
|
"此前的命令如果是按 Linux 沙箱环境编写的,请按上述规则改写后重新执行。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---- 运行期模式基线(权限/执行环境/网络权限的「智能体已知晓」状态) ----
|
||||||
|
|
||||||
|
def _current_runtime_modes(self) -> Dict[str, str]:
|
||||||
|
"""当前实际生效的三种运行期模式。"""
|
||||||
|
modes: Dict[str, str] = {}
|
||||||
|
try:
|
||||||
|
if hasattr(self, "get_permission_mode"):
|
||||||
|
modes["permission_mode"] = str(self.get_permission_mode() or "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if hasattr(self, "get_execution_mode"):
|
||||||
|
modes["execution_mode"] = str(self.get_execution_mode() or "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if hasattr(self, "get_network_permission"):
|
||||||
|
modes["network_permission"] = str(self.get_network_permission() or "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return modes
|
||||||
|
|
||||||
|
def get_runtime_mode_baseline(self) -> Dict[str, str]:
|
||||||
|
"""读取本对话的运行期模式基线(conversation metadata)。"""
|
||||||
|
cm = getattr(self, "context_manager", None)
|
||||||
|
meta = getattr(cm, "conversation_metadata", None) if cm else None
|
||||||
|
raw = meta.get(self._RUNTIME_MODE_BASELINE_META_KEY) if isinstance(meta, dict) else None
|
||||||
|
return dict(raw) if isinstance(raw, dict) else {}
|
||||||
|
|
||||||
|
def update_runtime_mode_baseline(self, updates: Dict[str, str]) -> None:
|
||||||
|
"""通知真正注入对话后更新基线。只允许在通知注入点调用。"""
|
||||||
|
if not isinstance(updates, dict) or not updates:
|
||||||
|
return
|
||||||
|
baseline = self.get_runtime_mode_baseline()
|
||||||
|
baseline.update({k: str(v) for k, v in updates.items() if v})
|
||||||
|
if hasattr(self, "_persist_runtime_mode_metadata"):
|
||||||
|
try:
|
||||||
|
self._persist_runtime_mode_metadata({self._RUNTIME_MODE_BASELINE_META_KEY: baseline})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def collect_runtime_mode_drift(self) -> List[Dict[str, str]]:
|
||||||
|
"""比较当前实际模式与本对话基线,返回需要补发通知的差异列表。
|
||||||
|
|
||||||
|
- 基线缺失(新对话/历史对话):静默初始化为当前模式,返回空列表(不通知)。
|
||||||
|
新对话首轮消息的冻结系统提示词本身就是当前模式,无需额外通知。
|
||||||
|
- 返回项结构:{"kind": ..., "mode": ..., "source": ...}
|
||||||
|
"""
|
||||||
|
current = self._current_runtime_modes()
|
||||||
|
baseline = self.get_runtime_mode_baseline()
|
||||||
|
if not baseline:
|
||||||
|
self.update_runtime_mode_baseline(current)
|
||||||
|
return []
|
||||||
|
drift: List[Dict[str, str]] = []
|
||||||
|
for kind in self._RUNTIME_MODE_KINDS:
|
||||||
|
cur = current.get(kind)
|
||||||
|
if not cur:
|
||||||
|
continue
|
||||||
|
if baseline.get(kind) != cur:
|
||||||
|
drift.append({
|
||||||
|
"kind": kind,
|
||||||
|
"mode": cur,
|
||||||
|
"source": self._RUNTIME_MODE_SOURCE.get(kind, "notify"),
|
||||||
|
})
|
||||||
|
return drift
|
||||||
|
|
||||||
|
def build_runtime_mode_switch_notice(self, kind: str, mode: str) -> str:
|
||||||
|
"""构建运行期模式切换通知文本(运行期注入与空闲期补注共用,保证两处一致)。"""
|
||||||
|
if kind == "permission_mode":
|
||||||
|
label = self._PERMISSION_MODE_LABEL.get(mode, mode)
|
||||||
|
return f"权限模式被用户修改为 {label}"
|
||||||
|
if kind == "network_permission":
|
||||||
|
label = self._NETWORK_PERMISSION_LABEL.get(mode, mode)
|
||||||
|
return f"网络权限被用户修改为 {label}"
|
||||||
|
if kind == "execution_mode":
|
||||||
|
if hasattr(self, "_build_execution_mode_switch_notice"):
|
||||||
|
return self._build_execution_mode_switch_notice(mode)
|
||||||
|
label = self._EXECUTION_MODE_LABEL.get(mode, mode)
|
||||||
|
return f"执行环境被用户修改为 {label}"
|
||||||
|
return f"运行模式被用户修改为 {mode}"
|
||||||
|
|
||||||
def _get_or_init_frozen_mode_prompt(self, key: str, builder) -> Optional[str]:
|
def _get_or_init_frozen_mode_prompt(self, key: str, builder) -> Optional[str]:
|
||||||
return self._get_or_init_frozen_prompt(key, builder)
|
return self._get_or_init_frozen_prompt(key, builder)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from server.chat import chat_bp
|
from server.chat import chat_bp
|
||||||
from server.chat.settings import _dispatch_runtime_mode_notice
|
|
||||||
import json, time
|
import json, time
|
||||||
|
|
||||||
PERMISSION_MODE_OPTIONS = ["readonly", "approval", "auto_approval", "unrestricted"]
|
PERMISSION_MODE_OPTIONS = ["readonly", "approval", "auto_approval", "unrestricted"]
|
||||||
@ -194,8 +193,8 @@ def update_permission_mode(terminal: WebTerminal, workspace: UserWorkspace, user
|
|||||||
"message": "权限模式将在当前工具执行完成后生效",
|
"message": "权限模式将在当前工具执行完成后生效",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 空闲期间:直接生效
|
# 空闲期间:直接生效。切换通知由 baseline 机制在下一条真实 user 消息时补发
|
||||||
previous_mode = terminal.get_permission_mode() if hasattr(terminal, "get_permission_mode") else None
|
# (见 chat_flow_task_main 的 drift 注入点),此处不再 enqueue。
|
||||||
try:
|
try:
|
||||||
applied_mode = terminal.set_permission_mode(target_mode)
|
applied_mode = terminal.set_permission_mode(target_mode)
|
||||||
if hasattr(terminal, "pending_permission_mode"):
|
if hasattr(terminal, "pending_permission_mode"):
|
||||||
@ -214,14 +213,6 @@ def update_permission_mode(terminal: WebTerminal, workspace: UserWorkspace, user
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
session["permission_mode"] = applied_mode
|
session["permission_mode"] = applied_mode
|
||||||
if applied_mode != previous_mode:
|
|
||||||
permission_label = {
|
|
||||||
"readonly": "只读",
|
|
||||||
"approval": "批准",
|
|
||||||
"auto_approval": "自动审核",
|
|
||||||
"unrestricted": "无限制",
|
|
||||||
}.get(applied_mode, applied_mode)
|
|
||||||
_dispatch_runtime_mode_notice(terminal, username, f"权限模式被用户修改为 {permission_label}", source="权限变更")
|
|
||||||
status = terminal.get_status()
|
status = terminal.get_status()
|
||||||
socketio.emit('status_update', status, room=f"user_{username}")
|
socketio.emit('status_update', status, room=f"user_{username}")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -297,8 +288,7 @@ def update_execution_mode(terminal: WebTerminal, workspace: UserWorkspace, usern
|
|||||||
"message": "执行环境将在当前工具执行完成后生效",
|
"message": "执行环境将在当前工具执行完成后生效",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 空闲期间:直接生效
|
# 空闲期间:直接生效。切换通知由 baseline 机制在下一条真实 user 消息时补发。
|
||||||
previous_mode = terminal.get_execution_mode() if hasattr(terminal, "get_execution_mode") else None
|
|
||||||
try:
|
try:
|
||||||
state = terminal.set_execution_mode(target_mode)
|
state = terminal.set_execution_mode(target_mode)
|
||||||
if hasattr(terminal, "pending_execution_mode"):
|
if hasattr(terminal, "pending_execution_mode"):
|
||||||
@ -312,10 +302,6 @@ def update_execution_mode(terminal: WebTerminal, workspace: UserWorkspace, usern
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"success": False, "error": str(exc), "message": "更新执行环境失败"}), 500
|
return jsonify({"success": False, "error": str(exc), "message": "更新执行环境失败"}), 500
|
||||||
status = terminal.get_status()
|
status = terminal.get_status()
|
||||||
current_mode = state.get("mode", target_mode)
|
|
||||||
if current_mode != previous_mode:
|
|
||||||
execution_label = {"sandbox": "沙箱", "direct": "完全访问权限"}.get(current_mode, current_mode)
|
|
||||||
_dispatch_runtime_mode_notice(terminal, username, f"执行环境被用户修改为 {execution_label}", source="执行环境变更")
|
|
||||||
socketio.emit('status_update', status, room=f"user_{username}")
|
socketio.emit('status_update', status, room=f"user_{username}")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -383,7 +369,7 @@ def update_network_permission(terminal: WebTerminal, workspace: UserWorkspace, u
|
|||||||
"message": "网络权限将在当前工具执行完成后生效",
|
"message": "网络权限将在当前工具执行完成后生效",
|
||||||
})
|
})
|
||||||
|
|
||||||
previous_mode = terminal.get_network_permission() if hasattr(terminal, "get_network_permission") else None
|
# 空闲期间:直接生效。切换通知由 baseline 机制在下一条真实 user 消息时补发。
|
||||||
try:
|
try:
|
||||||
applied = terminal.set_network_permission(target_mode)
|
applied = terminal.set_network_permission(target_mode)
|
||||||
if hasattr(terminal, "pending_network_permission"):
|
if hasattr(terminal, "pending_network_permission"):
|
||||||
@ -397,9 +383,6 @@ def update_network_permission(terminal: WebTerminal, workspace: UserWorkspace, u
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"success": False, "error": str(exc), "message": "更新网络权限失败"}), 500
|
return jsonify({"success": False, "error": str(exc), "message": "更新网络权限失败"}), 500
|
||||||
status = terminal.get_status()
|
status = terminal.get_status()
|
||||||
if applied != previous_mode:
|
|
||||||
label = {"restricted": "受限", "full": "完全开放", "none": "完全禁止"}.get(applied, applied)
|
|
||||||
_dispatch_runtime_mode_notice(terminal, username, f"网络权限被用户修改为 {label}", source="网络权限变更")
|
|
||||||
socketio.emit('status_update', status, room=f"user_{username}")
|
socketio.emit('status_update', status, room=f"user_{username}")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|||||||
@ -46,36 +46,6 @@ from server.monitor import get_cached_monitor_snapshot
|
|||||||
from server.files import sanitize_filename_preserve_unicode
|
from server.files import sanitize_filename_preserve_unicode
|
||||||
|
|
||||||
UPLOAD_FOLDER_NAME = ".astrion/user_upload"
|
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'])
|
@chat_bp.route('/api/thinking-mode', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
|||||||
@ -122,6 +122,7 @@ from .chat_flow_task_support import (
|
|||||||
process_background_command_updates,
|
process_background_command_updates,
|
||||||
process_multi_agent_master_messages,
|
process_multi_agent_master_messages,
|
||||||
inject_multi_agent_master_message,
|
inject_multi_agent_master_message,
|
||||||
|
inject_runtime_user_message,
|
||||||
_auto_message_type_for_multi_agent_subtype,
|
_auto_message_type_for_multi_agent_subtype,
|
||||||
)
|
)
|
||||||
from .chat_flow_tool_loop import execute_tool_calls
|
from .chat_flow_tool_loop import execute_tool_calls
|
||||||
@ -1516,6 +1517,43 @@ async def handle_task_with_sender(
|
|||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
debug_log(f"[TaskFlow] 发送 user_message 回显失败: {exc}")
|
debug_log(f"[TaskFlow] 发送 user_message 回显失败: {exc}")
|
||||||
|
# 空闲期运行期模式(权限/执行环境/网络权限)切换的补发通知。
|
||||||
|
# 仅真实用户消息(直接发送 / 提前输入队列 / 引导消息转换)触发,
|
||||||
|
# 自动消息(子智能体/后台命令完成等触发的续答)不触发。
|
||||||
|
# drift 检测比较「当前实际模式 vs 本对话已通知基线」,天然去重:
|
||||||
|
# 空闲期反复切换只在最终值与基线不同时补发一遍;
|
||||||
|
# 基线缺失(新对话/历史对话)时静默初始化,不补发。
|
||||||
|
if user_message_source in {"user", "presend", "guidance"}:
|
||||||
|
try:
|
||||||
|
drift = (
|
||||||
|
web_terminal.collect_runtime_mode_drift()
|
||||||
|
if hasattr(web_terminal, "collect_runtime_mode_drift")
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
drift = []
|
||||||
|
debug_log(f"[RuntimeMode] 空闲期模式漂移检测失败: {exc}")
|
||||||
|
for drift_item in drift:
|
||||||
|
try:
|
||||||
|
notice_text = web_terminal.build_runtime_mode_switch_notice(
|
||||||
|
drift_item.get("kind"), drift_item.get("mode")
|
||||||
|
)
|
||||||
|
if not str(notice_text or "").strip():
|
||||||
|
continue
|
||||||
|
inject_runtime_user_message(
|
||||||
|
web_terminal=web_terminal,
|
||||||
|
messages=None,
|
||||||
|
text=notice_text,
|
||||||
|
source=str(drift_item.get("source") or "notify"),
|
||||||
|
sender=sender,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
web_terminal.update_runtime_mode_baseline(
|
||||||
|
{drift_item.get("kind"): drift_item.get("mode")}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[RuntimeMode] 空闲期切换通知注入失败: {exc}")
|
||||||
_prepare_hidden_versioning_baseline_for_first_input(
|
_prepare_hidden_versioning_baseline_for_first_input(
|
||||||
web_terminal=web_terminal,
|
web_terminal=web_terminal,
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
@ -2409,7 +2447,9 @@ async def handle_task_with_sender(
|
|||||||
# 结束,不会进入 execute_tool_calls → apply_pending_runtime_mode_changes。
|
# 结束,不会进入 execute_tool_calls → apply_pending_runtime_mode_changes。
|
||||||
# 若不在此清理,pending 会残留到下一次工作首次工具调用时才被应用,
|
# 若不在此清理,pending 会残留到下一次工作首次工具调用时才被应用,
|
||||||
# 并把"变更通知"作为 user 消息注入到新一轮对话中(bug)。
|
# 并把"变更通知"作为 user 消息注入到新一轮对话中(bug)。
|
||||||
# 正确行为:task 结束时立即应用 pending(等同空闲期切换),但不生成通知。
|
# 正确行为:task 结束时立即应用 pending(等同空闲期切换),此处不生成通知;
|
||||||
|
# 由于通知基线(runtime_mode_baseline)未更新,下一条真实 user 消息时会通过
|
||||||
|
# drift 检测一次性补发切换通知(见 user_message 回显后的注入点)。
|
||||||
try:
|
try:
|
||||||
if hasattr(web_terminal, "apply_pending_runtime_mode_changes"):
|
if hasattr(web_terminal, "apply_pending_runtime_mode_changes"):
|
||||||
residual_notices = web_terminal.apply_pending_runtime_mode_changes() or []
|
residual_notices = web_terminal.apply_pending_runtime_mode_changes() or []
|
||||||
@ -2431,7 +2471,7 @@ async def handle_task_with_sender(
|
|||||||
)
|
)
|
||||||
# 权限/执行环境/网络权限变更通知在任务完成时不应该触发新一轮工作,
|
# 权限/执行环境/网络权限变更通知在任务完成时不应该触发新一轮工作,
|
||||||
# 只保留真正的引导消息(压缩续接等),丢弃变更通知。
|
# 只保留真正的引导消息(压缩续接等),丢弃变更通知。
|
||||||
runtime_skip_sources = {"权限变更", "执行环境变更", "网络权限变更", "notify"}
|
runtime_skip_sources = {"权限变更", "执行环境变更", "网络权限变更", "notify", "permission_change", "execution_change", "network_change"}
|
||||||
for item in (raw_items or []):
|
for item in (raw_items or []):
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
src = str(item.get("source") or "").strip().lower()
|
src = str(item.get("source") or "").strip().lower()
|
||||||
|
|||||||
@ -24,6 +24,9 @@ _VALID_SOURCES = {
|
|||||||
"permission",
|
"permission",
|
||||||
"sandbox",
|
"sandbox",
|
||||||
"skill",
|
"skill",
|
||||||
|
"permission_change",
|
||||||
|
"execution_change",
|
||||||
|
"network_change",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -34,6 +37,9 @@ def _runtime_message_ui_defaults(src: str, *, inline: bool = False) -> Dict[str,
|
|||||||
return {"visibility": "hidden", "starts_work": False}
|
return {"visibility": "hidden", "starts_work": False}
|
||||||
if normalized in {"guidance", "notify"}:
|
if normalized in {"guidance", "notify"}:
|
||||||
return {"visibility": "compact", "starts_work": False}
|
return {"visibility": "compact", "starts_work": False}
|
||||||
|
# 运行期模式(权限/执行环境/网络权限)切换通知:仅通知,不开启新一轮工作。
|
||||||
|
if normalized in {"permission_change", "execution_change", "network_change"}:
|
||||||
|
return {"visibility": "compact", "starts_work": False}
|
||||||
# 后台完成通知 / 运行期压缩续接:属于当前这轮工作的延续,不开启新的工作头与计时器,
|
# 后台完成通知 / 运行期压缩续接:属于当前这轮工作的延续,不开启新的工作头与计时器,
|
||||||
# 也不暂停上一轮计时器。与 task_main 的 _user_message_ui_defaults 保持一致,
|
# 也不暂停上一轮计时器。与 task_main 的 _user_message_ui_defaults 保持一致,
|
||||||
# 确保运行期直接渲染与刷新后从历史加载行为相同。
|
# 确保运行期直接渲染与刷新后从历史加载行为相同。
|
||||||
|
|||||||
@ -1333,9 +1333,13 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
if isinstance(notice, dict):
|
if isinstance(notice, dict):
|
||||||
_text = str(notice.get("text") or "").strip()
|
_text = str(notice.get("text") or "").strip()
|
||||||
_source = str(notice.get("source") or "notify").strip() or "notify"
|
_source = str(notice.get("source") or "notify").strip() or "notify"
|
||||||
|
_kind = str(notice.get("kind") or "").strip()
|
||||||
|
_mode = str(notice.get("mode") or "").strip()
|
||||||
else:
|
else:
|
||||||
_text = str(notice or "").strip()
|
_text = str(notice or "").strip()
|
||||||
_source = "notify"
|
_source = "notify"
|
||||||
|
_kind = ""
|
||||||
|
_mode = ""
|
||||||
if not _text:
|
if not _text:
|
||||||
continue
|
continue
|
||||||
_inject_runtime_mode_notice(
|
_inject_runtime_mode_notice(
|
||||||
@ -1346,6 +1350,12 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
sender=sender,
|
sender=sender,
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
)
|
)
|
||||||
|
# 通知真正注入对话后同步更新基线,避免下一条 user 消息重复补发。
|
||||||
|
if _kind and _mode and hasattr(web_terminal, "update_runtime_mode_baseline"):
|
||||||
|
try:
|
||||||
|
web_terminal.update_runtime_mode_baseline({_kind: _mode})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 运行期“引导对话”:需要等待同一轮全部工具执行结束后再注入,
|
# 运行期“引导对话”:需要等待同一轮全部工具执行结束后再注入,
|
||||||
# 避免一轮内并行/多工具调用时过早插入。
|
# 避免一轮内并行/多工具调用时过早插入。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user