模型准备结束工作时,队列中残留的权限/审核方式变更通知不应再 插入对话。改用 consume_runtime_guidance_for_injection 保留 source 信息,过滤掉 source 为'权限变更/执行环境变更/notify'的消息。
1740 lines
74 KiB
Python
1740 lines
74 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import time
|
||
import re
|
||
import zipfile
|
||
from collections import defaultdict, Counter, deque
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
from werkzeug.utils import secure_filename
|
||
|
||
from config import (
|
||
OUTPUT_FORMATS,
|
||
AUTO_FIX_TOOL_CALL,
|
||
AUTO_FIX_MAX_ATTEMPTS,
|
||
MAX_ITERATIONS_PER_TASK,
|
||
MAX_CONSECUTIVE_SAME_TOOL,
|
||
MAX_TOTAL_TOOL_CALLS,
|
||
TOOL_CALL_COOLDOWN,
|
||
MAX_UPLOAD_SIZE,
|
||
DEFAULT_CONVERSATIONS_LIMIT,
|
||
MAX_CONVERSATIONS_LIMIT,
|
||
CONVERSATIONS_DIR,
|
||
DEFAULT_RESPONSE_MAX_TOKENS,
|
||
DEFAULT_PROJECT_PATH,
|
||
LOGS_DIR,
|
||
AGENT_VERSION,
|
||
THINKING_FAST_INTERVAL,
|
||
PROJECT_MAX_STORAGE_MB,
|
||
PROJECT_MAX_STORAGE_BYTES,
|
||
UPLOAD_SCAN_LOG_SUBDIR,
|
||
)
|
||
from modules.personalization_manager import (
|
||
load_personalization_config,
|
||
save_personalization_config,
|
||
THINKING_INTERVAL_MIN,
|
||
THINKING_INTERVAL_MAX,
|
||
resolve_context_compression_settings,
|
||
)
|
||
from modules.skill_hint_manager import SkillHintManager
|
||
from modules.upload_security import UploadSecurityError
|
||
from modules.user_manager import UserWorkspace
|
||
from modules.usage_tracker import QUOTA_DEFAULTS
|
||
from modules.sub_agent_manager import TERMINAL_STATUSES
|
||
from modules.versioning_manager import ConversationVersioningManager, VersioningError
|
||
from core.web_terminal import WebTerminal
|
||
from utils.tool_result_formatter import format_tool_result_for_context
|
||
from utils.conversation_manager import ConversationManager
|
||
from config.model_profiles import get_model_context_window, get_model_profile
|
||
|
||
from .auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username
|
||
from .context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, reset_system_state, get_user_resources, get_or_create_usage_tracker
|
||
from .utils_common import (
|
||
build_review_lines,
|
||
debug_log,
|
||
log_backend_chunk,
|
||
log_frontend_chunk,
|
||
log_streaming_debug_entry,
|
||
brief_log,
|
||
DEBUG_LOG_FILE,
|
||
CHUNK_BACKEND_LOG_FILE,
|
||
CHUNK_FRONTEND_LOG_FILE,
|
||
STREAMING_DEBUG_LOG_FILE,
|
||
)
|
||
from .security import rate_limited, compact_web_search_result, consume_socket_token, prune_socket_tokens, validate_csrf_request, requires_csrf_protection, get_csrf_token
|
||
from .monitor import cache_monitor_snapshot, get_cached_monitor_snapshot
|
||
from .extensions import socketio
|
||
from .state import (
|
||
MONITOR_FILE_TOOLS,
|
||
MONITOR_MEMORY_TOOLS,
|
||
MONITOR_SNAPSHOT_CHAR_LIMIT,
|
||
MONITOR_MEMORY_ENTRY_LIMIT,
|
||
RATE_LIMIT_BUCKETS,
|
||
FAILURE_TRACKERS,
|
||
pending_socket_tokens,
|
||
usage_trackers,
|
||
MONITOR_SNAPSHOT_CACHE,
|
||
MONITOR_SNAPSHOT_CACHE_LIMIT,
|
||
PROJECT_STORAGE_CACHE,
|
||
PROJECT_STORAGE_CACHE_TTL_SECONDS,
|
||
RECENT_UPLOAD_EVENT_LIMIT,
|
||
RECENT_UPLOAD_FEED_LIMIT,
|
||
THINKING_FAILURE_KEYWORDS,
|
||
TITLE_PROMPT_PATH,
|
||
get_last_active_ts,
|
||
user_manager,
|
||
container_manager,
|
||
custom_tool_registry,
|
||
user_terminals,
|
||
terminal_rooms,
|
||
connection_users,
|
||
stop_flags,
|
||
active_polling_tasks,
|
||
get_stop_flag,
|
||
set_stop_flag,
|
||
clear_stop_flag,
|
||
)
|
||
from .chat_flow_helpers import (
|
||
detect_malformed_tool_call as _detect_malformed_tool_call,
|
||
detect_tool_failure,
|
||
get_thinking_state,
|
||
mark_force_thinking as _mark_force_thinking,
|
||
mark_suppress_thinking,
|
||
apply_thinking_schedule as _apply_thinking_schedule,
|
||
update_thinking_after_call as _update_thinking_after_call,
|
||
maybe_mark_failure_from_message as _maybe_mark_failure_from_message,
|
||
generate_conversation_title_background as _generate_conversation_title_background,
|
||
)
|
||
|
||
|
||
from .chat_flow_runner_helpers import (
|
||
extract_intent_from_partial,
|
||
resolve_monitor_path,
|
||
resolve_monitor_memory,
|
||
capture_monitor_snapshot,
|
||
)
|
||
|
||
|
||
from .chat_flow_runtime import (
|
||
generate_conversation_title_background,
|
||
mark_force_thinking,
|
||
apply_thinking_schedule,
|
||
update_thinking_after_call,
|
||
maybe_mark_failure_from_message,
|
||
detect_malformed_tool_call,
|
||
)
|
||
|
||
from .chat_flow_task_support import process_sub_agent_updates, process_background_command_updates
|
||
from .chat_flow_tool_loop import execute_tool_calls
|
||
from .chat_flow_stream_loop import run_streaming_attempts
|
||
from .deep_compression import run_deep_compression
|
||
from .goal_flow import (
|
||
maybe_start_goal,
|
||
inject_goal_prompt,
|
||
handle_goal_after_turn,
|
||
stop_goal_user_cancel,
|
||
goal_is_active,
|
||
emit_goal_progress,
|
||
)
|
||
|
||
_VALID_USER_MESSAGE_SOURCES = {
|
||
"user",
|
||
"guidance",
|
||
"notify",
|
||
"presend",
|
||
"sub_agent",
|
||
"background_command",
|
||
"goal",
|
||
"goal_prompt",
|
||
"goal_review",
|
||
"compression",
|
||
"compression_handoff",
|
||
"permission",
|
||
"sandbox",
|
||
"skill",
|
||
}
|
||
|
||
|
||
def _user_message_ui_defaults(source: str, *, auto_user_message_event: bool = False) -> Dict[str, Any]:
|
||
normalized = str(source or "").strip().lower()
|
||
if normalized in {"skill", "goal_prompt", "permission", "sandbox"}:
|
||
return {"visibility": "hidden", "starts_work": False}
|
||
if normalized in {"guidance", "notify"}:
|
||
return {"visibility": "compact", "starts_work": False}
|
||
if normalized in {"goal_review", "compression", "compression_handoff", "sub_agent", "background_command"}:
|
||
return {"visibility": "compact", "starts_work": True}
|
||
if normalized == "presend":
|
||
return {"visibility": "chat", "starts_work": True}
|
||
if normalized == "goal":
|
||
return {"visibility": "compact", "starts_work": True}
|
||
return {"visibility": "chat", "starts_work": normalized == "user" and not auto_user_message_event}
|
||
|
||
|
||
def _should_skip_versioning_for_message(
|
||
*,
|
||
message: str,
|
||
auto_user_message_event: bool,
|
||
) -> bool:
|
||
if auto_user_message_event:
|
||
return True
|
||
text = (message or "").strip()
|
||
if not text:
|
||
return True
|
||
if text.startswith("这是一句系统自动发送的user消息"):
|
||
return True
|
||
return False
|
||
|
||
|
||
def _prepare_hidden_versioning_baseline_for_first_input(
|
||
*,
|
||
web_terminal,
|
||
workspace,
|
||
conversation_id: str,
|
||
message: str,
|
||
auto_user_message_event: bool,
|
||
) -> None:
|
||
"""Ensure first-run baseline is committed (hidden, no checkpoint row)."""
|
||
try:
|
||
if _should_skip_versioning_for_message(
|
||
message=message,
|
||
auto_user_message_event=auto_user_message_event,
|
||
):
|
||
return
|
||
cm = getattr(getattr(web_terminal, "context_manager", None), "conversation_manager", None)
|
||
if not cm:
|
||
return
|
||
conv_data = cm.load_conversation(conversation_id) or {}
|
||
versioning_meta = ((conv_data.get("metadata") or {}).get("versioning") or {})
|
||
if not bool(versioning_meta.get("enabled", False)):
|
||
return
|
||
tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
|
||
versioning_meta.get("tracking_mode")
|
||
)
|
||
if (
|
||
tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||
and not bool(getattr(web_terminal, "username", None) == "host")
|
||
):
|
||
return
|
||
manager = ConversationVersioningManager(
|
||
project_path=workspace.project_path,
|
||
data_dir=workspace.data_dir,
|
||
conversation_id=conversation_id,
|
||
)
|
||
baseline = manager.ensure_baseline_for_first_input(tracking_mode=tracking_mode)
|
||
debug_log(
|
||
f"[Versioning][Baseline] conv={conversation_id} "
|
||
f"tracking_mode={tracking_mode} "
|
||
f"created={baseline.get('created')} skipped={baseline.get('skipped')} "
|
||
f"reason={baseline.get('reason')} head={baseline.get('head')}"
|
||
)
|
||
except VersioningError as exc:
|
||
debug_log(f"[Versioning] 创建首轮基线失败: {exc}")
|
||
except Exception as exc:
|
||
debug_log(f"[Versioning] 创建首轮基线异常: {exc}")
|
||
|
||
|
||
def _record_hidden_versioning_checkpoint_after_run(
|
||
*,
|
||
web_terminal,
|
||
workspace,
|
||
conversation_id: str,
|
||
message: str,
|
||
message_index: int,
|
||
auto_user_message_event: bool,
|
||
run_status: str = "completed",
|
||
) -> None:
|
||
"""Hidden snapshot after current manual user input run finished."""
|
||
try:
|
||
if _should_skip_versioning_for_message(
|
||
message=message,
|
||
auto_user_message_event=auto_user_message_event,
|
||
):
|
||
return
|
||
cm = getattr(getattr(web_terminal, "context_manager", None), "conversation_manager", None)
|
||
if not cm:
|
||
return
|
||
conv_data = cm.load_conversation(conversation_id) or {}
|
||
versioning_meta = ((conv_data.get("metadata") or {}).get("versioning") or {})
|
||
if not bool(versioning_meta.get("enabled", False)):
|
||
return
|
||
tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
|
||
versioning_meta.get("tracking_mode")
|
||
)
|
||
if (
|
||
tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||
and not bool(getattr(web_terminal, "username", None) == "host")
|
||
):
|
||
return
|
||
snapshot_messages = conv_data.get("messages") or []
|
||
snapshot_payload = {
|
||
"conversation_id": conversation_id,
|
||
"title": conv_data.get("title"),
|
||
"metadata": conv_data.get("metadata") or {},
|
||
"messages": snapshot_messages,
|
||
"message_index": int(message_index),
|
||
"run_status": str(run_status or "completed"),
|
||
}
|
||
|
||
manager = ConversationVersioningManager(
|
||
project_path=workspace.project_path,
|
||
data_dir=workspace.data_dir,
|
||
conversation_id=conversation_id,
|
||
)
|
||
row = manager.create_checkpoint(
|
||
message=message,
|
||
message_index=message_index,
|
||
workspace_path=str(workspace.project_path),
|
||
conversation_snapshot=snapshot_payload,
|
||
run_status=str(run_status or "completed"),
|
||
tracking_mode=tracking_mode,
|
||
)
|
||
debug_log(
|
||
f"[Versioning][Checkpoint] conv={conversation_id} seq={row.get('seq')} "
|
||
f"tracking_mode={tracking_mode} "
|
||
f"msg_index={message_index} snapshot_messages={len(snapshot_messages)} "
|
||
f"commit={row.get('commit')} changed={row.get('changed')} status={row.get('run_status')}"
|
||
)
|
||
cm.update_conversation_metadata(
|
||
conversation_id,
|
||
{
|
||
"versioning": {
|
||
"enabled": True,
|
||
"mode": "overwrite",
|
||
"tracking_mode": tracking_mode,
|
||
"last_commit": row.get("commit"),
|
||
"last_input_seq": int(row.get("seq") or 0),
|
||
"updated_at": datetime.now().isoformat(),
|
||
}
|
||
},
|
||
)
|
||
except VersioningError as exc:
|
||
debug_log(f"[Versioning] 记录快照失败: {exc}")
|
||
except Exception as exc:
|
||
debug_log(f"[Versioning] 记录快照异常: {exc}")
|
||
|
||
|
||
async def _dispatch_completion_user_notice(
|
||
*,
|
||
web_terminal,
|
||
workspace,
|
||
sender,
|
||
client_sid: str,
|
||
username: str,
|
||
conversation_id: str,
|
||
user_message: str,
|
||
extra_payload: Optional[Dict[str, Any]] = None,
|
||
):
|
||
"""复用子智能体完成后的 user 代发机制。"""
|
||
extra_payload = extra_payload or {}
|
||
message_source = str(extra_payload.get("message_source") or "").strip().lower()
|
||
if not message_source:
|
||
if extra_payload.get("runtime_mode_notice"):
|
||
message_source = "notify"
|
||
elif extra_payload.get("runtime_guidance"):
|
||
message_source = "guidance"
|
||
elif extra_payload.get("background_command_notice"):
|
||
message_source = "background_command"
|
||
elif extra_payload.get("sub_agent_notice"):
|
||
message_source = "sub_agent"
|
||
if message_source not in _VALID_USER_MESSAGE_SOURCES:
|
||
message_source = "user"
|
||
try:
|
||
from .tasks import task_manager
|
||
workspace_id = getattr(workspace, "workspace_id", None) or "default"
|
||
host_mode = bool(getattr(workspace, "username", None) == "host")
|
||
session_data = {
|
||
"username": username,
|
||
"role": getattr(web_terminal, "user_role", "user"),
|
||
"is_api_user": getattr(web_terminal, "user_role", "") == "api",
|
||
"host_mode": host_mode,
|
||
"host_workspace_id": workspace_id if host_mode else None,
|
||
"workspace_id": workspace_id,
|
||
"run_mode": getattr(web_terminal, "run_mode", None),
|
||
"thinking_mode": getattr(web_terminal, "thinking_mode", None),
|
||
"model_key": getattr(web_terminal, "model_key", None),
|
||
"message_source": message_source,
|
||
}
|
||
ui_defaults = _user_message_ui_defaults(
|
||
message_source,
|
||
auto_user_message_event=True,
|
||
)
|
||
# 关键:通知类后台任务需要把 user_message 写入任务事件流,
|
||
# 否则前端轮询只会看到 AI/tool 事件,看不到 user_message。
|
||
session_data["auto_user_message_event"] = True
|
||
session_data["auto_user_message_payload"] = {
|
||
**dict(extra_payload or {}),
|
||
**ui_defaults,
|
||
}
|
||
rec = task_manager.create_chat_task(
|
||
username,
|
||
workspace_id,
|
||
user_message,
|
||
[],
|
||
conversation_id,
|
||
model_key=session_data.get("model_key"),
|
||
thinking_mode=session_data.get("thinking_mode"),
|
||
run_mode=session_data.get("run_mode"),
|
||
session_data=session_data,
|
||
)
|
||
payload = {
|
||
'message': user_message,
|
||
'conversation_id': conversation_id,
|
||
'task_id': rec.task_id,
|
||
'message_source': message_source,
|
||
**ui_defaults,
|
||
}
|
||
payload.update(extra_payload)
|
||
payload["metadata"] = {
|
||
"message_source": message_source,
|
||
**ui_defaults,
|
||
**(payload.get("metadata") or {}),
|
||
}
|
||
sender('user_message', payload)
|
||
return
|
||
except Exception as e:
|
||
debug_log(f"[CompletionNotice] 创建后台消息任务失败,回退直接执行: {e}")
|
||
|
||
payload = {
|
||
'message': user_message,
|
||
'conversation_id': conversation_id,
|
||
'message_source': message_source,
|
||
}
|
||
payload.update(_user_message_ui_defaults(message_source, auto_user_message_event=True))
|
||
payload.update(extra_payload)
|
||
payload["metadata"] = {
|
||
"message_source": message_source,
|
||
"visibility": payload.get("visibility"),
|
||
"starts_work": payload.get("starts_work"),
|
||
**(payload.get("metadata") or {}),
|
||
}
|
||
sender('user_message', payload)
|
||
try:
|
||
task_handle = asyncio.create_task(handle_task_with_sender(
|
||
terminal=web_terminal,
|
||
workspace=workspace,
|
||
message=user_message,
|
||
images=[],
|
||
sender=sender,
|
||
client_sid=client_sid,
|
||
username=username,
|
||
videos=[],
|
||
auto_user_message_event=True,
|
||
))
|
||
await task_handle
|
||
except Exception as inner_exc:
|
||
debug_log(f"[CompletionNotice] 回退处理 user_message 失败: {inner_exc}")
|
||
|
||
|
||
def _build_shared_waiting_payload(items: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||
"""构建与子智能体一致的 waiting 事件载荷结构。"""
|
||
normalized = []
|
||
for item in items or []:
|
||
summary = item.get('summary') or item.get('command') or '后台任务'
|
||
normalized.append({
|
||
'agent_id': item.get('agent_id') or item.get('command_id') or item.get('task_id'),
|
||
'summary': summary,
|
||
})
|
||
return {
|
||
'count': len(normalized),
|
||
'tasks': normalized,
|
||
}
|
||
|
||
async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, client_sid, username):
|
||
"""后台轮询子智能体完成状态,完成后触发新一轮对话"""
|
||
from .extensions import socketio
|
||
|
||
manager = getattr(web_terminal, "sub_agent_manager", None)
|
||
if not manager:
|
||
debug_log("[SubAgent] poll_sub_agent_completion: manager 不存在")
|
||
return
|
||
if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
|
||
web_terminal._announced_sub_agent_tasks = set()
|
||
|
||
max_wait_time = 3600 # 最多等待1小时
|
||
start_wait = time.time()
|
||
|
||
debug_log(f"[SubAgent] 开始后台轮询,conversation_id={conversation_id}, username={username}")
|
||
|
||
# 创建 sender 函数,用于发送 socket 事件
|
||
def sender(event_type, data):
|
||
try:
|
||
socketio.emit(event_type, data, room=f"user_{username}")
|
||
debug_log(f"[SubAgent] 发送事件: {event_type}")
|
||
except Exception as e:
|
||
debug_log(f"[SubAgent] 发送事件失败: {event_type}, 错误: {e}")
|
||
|
||
while (time.time() - start_wait) < max_wait_time:
|
||
debug_log(f"[SubAgent] 轮询检查...")
|
||
|
||
# 检查停止标志
|
||
client_stop_info = get_stop_flag(client_sid, username)
|
||
if client_stop_info:
|
||
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
|
||
if stop_requested:
|
||
debug_log("[SubAgent] 用户请求停止,终止轮询")
|
||
break
|
||
|
||
# 若主对话仍在工具循环中,暂不消费完成事件,避免抢占 system 消息插入
|
||
if getattr(web_terminal, "_tool_loop_active", False):
|
||
debug_log("[SubAgent] 主对话工具循环中,延迟后台轮询发送 user 消息")
|
||
await asyncio.sleep(1)
|
||
continue
|
||
|
||
updates = manager.poll_updates()
|
||
debug_log(f"[SubAgent] poll_updates 返回 {len(updates)} 个更新")
|
||
|
||
for update in updates:
|
||
agent_id = update.get("agent_id")
|
||
summary = update.get("summary")
|
||
result_summary = update.get("result_summary") or update.get("message", "")
|
||
deliverables_dir = update.get("deliverables_dir", "")
|
||
status = update.get("status")
|
||
task_id = update.get("task_id")
|
||
task_info = manager.tasks.get(task_id) if task_id else None
|
||
task_conv_id = task_info.get("conversation_id") if isinstance(task_info, dict) else None
|
||
if task_conv_id and task_conv_id != conversation_id:
|
||
debug_log(f"[SubAgent] 跳过非当前对话任务: task={task_id} conv={task_conv_id} current={conversation_id}")
|
||
continue
|
||
if task_id and task_info is None:
|
||
debug_log(f"[SubAgent] 找不到任务详情,跳过: task={task_id}")
|
||
continue
|
||
if status == "terminated" or (isinstance(task_info, dict) and task_info.get("notified")):
|
||
debug_log(f"[SubAgent] 跳过已终止/已通知任务: task={task_id} status={status}")
|
||
continue
|
||
|
||
debug_log(f"[SubAgent] 子智能体{agent_id}完成,状态: {status}")
|
||
|
||
# 构建 user 消息(后台完成时才发送)
|
||
prefix = "这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成"
|
||
runtime_line = ""
|
||
elapsed_seconds = update.get("runtime_seconds")
|
||
if elapsed_seconds is None:
|
||
elapsed_seconds = update.get("elapsed_seconds")
|
||
if status == "completed" and isinstance(elapsed_seconds, (int, float)):
|
||
runtime_line = f"\n\n运行了{int(round(elapsed_seconds))}秒"
|
||
user_message = f"""{prefix}
|
||
|
||
子智能体{agent_id} ({summary}) 已完成任务。
|
||
|
||
{result_summary}
|
||
{runtime_line}
|
||
|
||
交付目录:{deliverables_dir}"""
|
||
|
||
debug_log(f"[SubAgent] 准备发送 user_message: {user_message[:100]}...")
|
||
|
||
has_remaining = False
|
||
remaining_count = 0
|
||
try:
|
||
if task_id:
|
||
web_terminal._announced_sub_agent_tasks.add(task_id)
|
||
if isinstance(task_info, dict):
|
||
task_info["notified"] = True
|
||
task_info["updated_at"] = time.time()
|
||
try:
|
||
manager._save_state()
|
||
except Exception as exc:
|
||
debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
|
||
|
||
# 计算剩余子智能体状态(用于前端清理等待标记)
|
||
if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
|
||
web_terminal._announced_sub_agent_tasks = set()
|
||
announced = web_terminal._announced_sub_agent_tasks
|
||
running_tasks = [
|
||
task for task in manager.tasks.values()
|
||
if isinstance(task, dict)
|
||
and task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
|
||
and task.get("run_in_background")
|
||
and task.get("conversation_id") == conversation_id
|
||
]
|
||
pending_notice_tasks = [
|
||
task for task in manager.tasks.values()
|
||
if isinstance(task, dict)
|
||
and task.get("status") in TERMINAL_STATUSES.union({"terminated"})
|
||
and task.get("run_in_background")
|
||
and task.get("conversation_id") == conversation_id
|
||
and task.get("task_id") not in announced
|
||
and not task.get("notified")
|
||
]
|
||
remaining_count = len(running_tasks) + len(pending_notice_tasks)
|
||
has_remaining = remaining_count > 0
|
||
await _dispatch_completion_user_notice(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
sender=sender,
|
||
client_sid=client_sid,
|
||
username=username,
|
||
conversation_id=conversation_id,
|
||
user_message=user_message,
|
||
extra_payload={
|
||
'sub_agent_notice': True,
|
||
'message_source': 'sub_agent',
|
||
'has_running_sub_agents': has_remaining,
|
||
'remaining_count': remaining_count,
|
||
},
|
||
)
|
||
except Exception as e:
|
||
debug_log(f"[SubAgent] 创建后台任务失败,回退直接执行: {e}")
|
||
await _dispatch_completion_user_notice(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
sender=sender,
|
||
client_sid=client_sid,
|
||
username=username,
|
||
conversation_id=conversation_id,
|
||
user_message=user_message,
|
||
extra_payload={
|
||
'sub_agent_notice': True,
|
||
'message_source': 'sub_agent',
|
||
'has_running_sub_agents': has_remaining,
|
||
'remaining_count': remaining_count,
|
||
},
|
||
)
|
||
|
||
return # 只处理第一个完成的子智能体
|
||
|
||
# 检查是否还有运行中的任务
|
||
running_tasks = [
|
||
task for task in manager.tasks.values()
|
||
if task.get("status") not in {"completed", "failed", "timeout", "terminated"}
|
||
and task.get("run_in_background")
|
||
and task.get("conversation_id") == conversation_id
|
||
]
|
||
|
||
debug_log(f"[SubAgent] 当前还有 {len(running_tasks)} 个运行中的任务")
|
||
|
||
if not running_tasks:
|
||
debug_log("[SubAgent] 所有子智能体已完成")
|
||
# 若状态已提前被更新为终态(poll_updates 返回空),补发完成提示
|
||
completed_tasks = [
|
||
task for task in manager.tasks.values()
|
||
if task.get("status") in {"completed", "failed", "timeout"}
|
||
and task.get("run_in_background")
|
||
and task.get("conversation_id") == conversation_id
|
||
and not task.get("notified")
|
||
]
|
||
if completed_tasks:
|
||
completed_tasks.sort(
|
||
key=lambda item: item.get("updated_at") or item.get("created_at") or 0,
|
||
reverse=True
|
||
)
|
||
task = completed_tasks[0]
|
||
agent_id = task.get("agent_id")
|
||
summary = task.get("summary") or ""
|
||
final_result = task.get("final_result") or {}
|
||
result_summary = (
|
||
final_result.get("message")
|
||
or final_result.get("result_summary")
|
||
or final_result.get("system_message")
|
||
or ""
|
||
)
|
||
deliverables_dir = final_result.get("deliverables_dir") or task.get("deliverables_dir") or ""
|
||
status = final_result.get("status") or task.get("status")
|
||
debug_log(f"[SubAgent] 补发完成提示: task={task.get('task_id')} status={status}")
|
||
|
||
user_message = f"""子智能体{agent_id} ({summary}) 已完成任务。
|
||
|
||
{result_summary}
|
||
|
||
交付目录:{deliverables_dir}"""
|
||
|
||
try:
|
||
task_id = task.get("task_id")
|
||
if task_id:
|
||
web_terminal._announced_sub_agent_tasks.add(task_id)
|
||
if isinstance(task, dict):
|
||
task["notified"] = True
|
||
task["updated_at"] = time.time()
|
||
try:
|
||
manager._save_state()
|
||
except Exception as exc:
|
||
debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
|
||
sender('user_message', {
|
||
'message': user_message,
|
||
'conversation_id': conversation_id
|
||
})
|
||
from .tasks import task_manager
|
||
workspace_id = getattr(workspace, "workspace_id", None) or "default"
|
||
host_mode = bool(getattr(workspace, "username", None) == "host")
|
||
session_data = {
|
||
"username": username,
|
||
"role": getattr(web_terminal, "user_role", "user"),
|
||
"is_api_user": getattr(web_terminal, "user_role", "") == "api",
|
||
"host_mode": host_mode,
|
||
"host_workspace_id": workspace_id if host_mode else None,
|
||
"workspace_id": workspace_id,
|
||
"run_mode": getattr(web_terminal, "run_mode", None),
|
||
"thinking_mode": getattr(web_terminal, "thinking_mode", None),
|
||
"model_key": getattr(web_terminal, "model_key", None),
|
||
}
|
||
# 标记为自动发送的user消息(子智能体完成通知)
|
||
session_data["auto_user_message_event"] = True
|
||
session_data["auto_user_message_payload"] = {
|
||
"sub_agent_notice": True,
|
||
"message_source": "sub_agent",
|
||
}
|
||
rec = task_manager.create_chat_task(
|
||
username,
|
||
workspace_id,
|
||
user_message,
|
||
[],
|
||
conversation_id,
|
||
model_key=session_data.get("model_key"),
|
||
thinking_mode=session_data.get("thinking_mode"),
|
||
run_mode=session_data.get("run_mode"),
|
||
session_data=session_data,
|
||
)
|
||
debug_log(f"[SubAgent] 补发通知创建后台任务: task_id={rec.task_id}")
|
||
except Exception as e:
|
||
debug_log(f"[SubAgent] 补发通知创建后台任务失败,回退直接执行: {e}")
|
||
try:
|
||
task_handle = asyncio.create_task(handle_task_with_sender(
|
||
terminal=web_terminal,
|
||
workspace=workspace,
|
||
message=user_message,
|
||
images=[],
|
||
sender=sender,
|
||
client_sid=client_sid,
|
||
username=username,
|
||
videos=[]
|
||
))
|
||
await task_handle
|
||
except Exception as inner_exc:
|
||
debug_log(f"[SubAgent] 补发完成提示失败: {inner_exc}")
|
||
import traceback
|
||
debug_log(f"[SubAgent] 错误堆栈: {traceback.format_exc()}")
|
||
break
|
||
|
||
await asyncio.sleep(5)
|
||
|
||
debug_log("[SubAgent] 后台轮询结束")
|
||
|
||
|
||
async def poll_background_command_completion(*, web_terminal, workspace, conversation_id: str, client_sid: str, username: str):
|
||
"""后台轮询 run_command 后台任务并在主流程结束后代发 user 通知。"""
|
||
from .extensions import socketio
|
||
|
||
manager = getattr(web_terminal, "background_command_manager", None)
|
||
if not manager:
|
||
debug_log("[BgCommand] poll_background_command_completion: manager 不存在")
|
||
return
|
||
|
||
max_wait_time = 3600
|
||
start_wait = time.time()
|
||
debug_log(f"[BgCommand] 开始后台轮询,conversation_id={conversation_id}, username={username}")
|
||
|
||
def sender(event_type, data):
|
||
try:
|
||
socketio.emit(event_type, data, room=f"user_{username}")
|
||
except Exception as e:
|
||
debug_log(f"[BgCommand] 发送事件失败: {event_type}, 错误: {e}")
|
||
|
||
while (time.time() - start_wait) < max_wait_time:
|
||
client_stop_info = get_stop_flag(client_sid, username)
|
||
if client_stop_info:
|
||
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
|
||
if stop_requested:
|
||
debug_log("[BgCommand] 用户请求停止,终止轮询")
|
||
break
|
||
|
||
if getattr(web_terminal, "_tool_loop_active", False):
|
||
debug_log("[BgCmdDebug] tool_loop_active=True, 延迟 user 代发轮询")
|
||
await asyncio.sleep(1)
|
||
continue
|
||
|
||
updates = manager.poll_updates(conversation_id=conversation_id)
|
||
debug_log(f"[BgCmdDebug] background polling updates={len(updates)} conv={conversation_id}")
|
||
for update in updates:
|
||
command_id = update.get("command_id")
|
||
output = update.get("output") or ""
|
||
content = "[后台 run_command 完成]\n" + (output if output else "[no_output]")
|
||
prefix = "这是一句系统自动发送的user消息,用于通知你后台run_command已经运行完成"
|
||
user_message = f"{prefix}\n\n{content}"
|
||
debug_log(f"[BgCmdDebug] preparing user notice command_id={command_id} output_len={len(output)}")
|
||
manager.mark_notified(str(command_id))
|
||
has_remaining = manager.has_pending_for_conversation(conversation_id)
|
||
await _dispatch_completion_user_notice(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
sender=sender,
|
||
client_sid=client_sid,
|
||
username=username,
|
||
conversation_id=conversation_id,
|
||
user_message=user_message,
|
||
extra_payload={
|
||
# 与子智能体完成通知完全复用同一前端通道/处理逻辑
|
||
'sub_agent_notice': True,
|
||
'message_source': 'background_command',
|
||
'remaining_count': 1 if has_remaining else 0,
|
||
'has_running_sub_agents': has_remaining,
|
||
'background_command_notice': True,
|
||
'has_running_background_commands': has_remaining,
|
||
},
|
||
)
|
||
debug_log(f"[BgCmdDebug] user notice dispatched command_id={command_id} has_remaining={has_remaining}")
|
||
return
|
||
|
||
if not manager.has_pending_for_conversation(conversation_id):
|
||
debug_log("[BgCmdDebug] no pending background commands, stop polling")
|
||
break
|
||
await asyncio.sleep(5)
|
||
|
||
debug_log("[BgCommand] 后台轮询结束")
|
||
|
||
async def handle_task_with_sender(
|
||
terminal: WebTerminal,
|
||
workspace: UserWorkspace,
|
||
message,
|
||
images,
|
||
sender,
|
||
client_sid,
|
||
username: str,
|
||
videos=None,
|
||
auto_user_message_event: bool = False,
|
||
):
|
||
"""处理任务并发送消息 - 集成token统计版本"""
|
||
from .extensions import socketio
|
||
|
||
web_terminal = terminal
|
||
conversation_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
|
||
videos = videos or []
|
||
raw_sender = sender
|
||
|
||
def sender(event_type, data):
|
||
"""为关键事件补充会话标识,便于前端定位报错归属。"""
|
||
if not isinstance(data, dict):
|
||
raw_sender(event_type, data)
|
||
return
|
||
payload = dict(data)
|
||
current_conv = conversation_id or getattr(web_terminal.context_manager, "current_conversation_id", None)
|
||
|
||
# 为所有事件添加 conversation_id,确保前端能正确匹配
|
||
if current_conv and event_type not in {"connect", "disconnect", "system_ready"}:
|
||
payload.setdefault("conversation_id", current_conv)
|
||
|
||
# 调试信息:记录关键事件
|
||
if event_type in {"user_message", "ai_message_start", "text_start", "text_chunk", "tool_preparing"}:
|
||
debug_log(f"[SENDER] 发送事件: {event_type}, conversation_id={current_conv}, data_keys={list(payload.keys())}")
|
||
|
||
# 为关键事件添加额外的标识信息
|
||
if event_type in {"error", "quota_exceeded", "task_stopped", "task_complete"}:
|
||
task_id = getattr(web_terminal, "task_id", None) or client_sid
|
||
if task_id:
|
||
payload.setdefault("task_id", task_id)
|
||
if client_sid:
|
||
payload.setdefault("client_sid", client_sid)
|
||
|
||
raw_sender(event_type, payload)
|
||
|
||
# 如果是思考模式,重置状态
|
||
if web_terminal.thinking_mode:
|
||
web_terminal.api_client.start_new_task(force_deep=web_terminal.deep_thinking_mode)
|
||
state = get_thinking_state(web_terminal)
|
||
state["fast_streak"] = 0
|
||
state["force_next"] = False
|
||
state["suppress_next"] = False
|
||
|
||
# 添加到对话历史
|
||
user_work_started_at = datetime.now().isoformat()
|
||
user_message_index = -1
|
||
current_work_user_message_index = -1
|
||
finalized_work_message_indices: set[int] = set()
|
||
history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or [])
|
||
is_first_user_message = history_len_before == 0
|
||
# 构建 user 消息来源与 metadata
|
||
source_from_terminal = str(getattr(web_terminal, "_current_user_message_source", "user") or "user").strip().lower()
|
||
user_message_source = source_from_terminal or "user"
|
||
if auto_user_message_event:
|
||
auto_payload = getattr(web_terminal, "_auto_user_message_payload", None)
|
||
if isinstance(auto_payload, dict):
|
||
src = str(auto_payload.get("message_source") or "").strip().lower()
|
||
if src:
|
||
user_message_source = src
|
||
elif auto_payload.get("runtime_mode_notice"):
|
||
user_message_source = "notify"
|
||
elif auto_payload.get("runtime_guidance"):
|
||
user_message_source = "guidance"
|
||
elif auto_payload.get("background_command_notice"):
|
||
user_message_source = "background_command"
|
||
elif auto_payload.get("sub_agent_notice"):
|
||
user_message_source = "sub_agent"
|
||
if user_message_source not in _VALID_USER_MESSAGE_SOURCES:
|
||
user_message_source = "user"
|
||
|
||
# 构建user消息metadata
|
||
user_message_metadata = {
|
||
"message_source": user_message_source,
|
||
}
|
||
user_message_metadata.update(
|
||
_user_message_ui_defaults(
|
||
user_message_source,
|
||
auto_user_message_event=bool(auto_user_message_event),
|
||
)
|
||
)
|
||
if user_message_metadata.get("starts_work") is True:
|
||
user_message_metadata["work_timer"] = {
|
||
"status": "working",
|
||
"started_at": user_work_started_at
|
||
}
|
||
# 如果是自动发送的user消息(子智能体/后台命令完成通知),添加标记
|
||
if auto_user_message_event:
|
||
user_message_metadata["is_auto_generated"] = True
|
||
user_message_metadata["auto_message_type"] = "completion_notice"
|
||
saved_user_message = web_terminal.context_manager.add_conversation(
|
||
"user",
|
||
message,
|
||
images=images,
|
||
videos=videos,
|
||
metadata=user_message_metadata
|
||
)
|
||
try:
|
||
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
|
||
except Exception:
|
||
user_message_index = -1
|
||
current_work_user_message_index = user_message_index
|
||
skill_context_messages = getattr(web_terminal, "_skill_context_messages", None)
|
||
if not auto_user_message_event and isinstance(skill_context_messages, list):
|
||
for skill_item in skill_context_messages:
|
||
if not isinstance(skill_item, dict):
|
||
continue
|
||
skill_content = str(skill_item.get("content") or "")
|
||
if not skill_content:
|
||
continue
|
||
skill_name = str(skill_item.get("name") or "").strip()
|
||
skill_path = str(skill_item.get("path") or "").strip()
|
||
web_terminal.context_manager.add_conversation(
|
||
"user",
|
||
f"[系统通知|skill]\n{skill_content}",
|
||
metadata={
|
||
"source": "skill",
|
||
"message_source": "skill",
|
||
"visibility": "hidden",
|
||
"starts_work": False,
|
||
"hidden": True,
|
||
"skill_name": skill_name,
|
||
"skill_path": skill_path,
|
||
}
|
||
)
|
||
if not auto_user_message_event:
|
||
try:
|
||
sender(
|
||
'user_message',
|
||
{
|
||
"message": message,
|
||
"images": (saved_user_message or {}).get("images") or images or [],
|
||
"videos": (saved_user_message or {}).get("videos") or videos or [],
|
||
"media_refs": (saved_user_message or {}).get("media_refs") or [],
|
||
"message_source": user_message_source,
|
||
"visibility": user_message_metadata.get("visibility"),
|
||
"starts_work": user_message_metadata.get("starts_work"),
|
||
"metadata": user_message_metadata,
|
||
"conversation_id": conversation_id,
|
||
},
|
||
)
|
||
except Exception as exc:
|
||
debug_log(f"[TaskFlow] 发送 user_message 回显失败: {exc}")
|
||
_prepare_hidden_versioning_baseline_for_first_input(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
conversation_id=conversation_id,
|
||
message=message,
|
||
auto_user_message_event=bool(auto_user_message_event),
|
||
)
|
||
|
||
def finalize_user_work_timer(index: Optional[int] = None):
|
||
target_index = current_work_user_message_index if index is None else index
|
||
if target_index in finalized_work_message_indices:
|
||
return
|
||
history = getattr(web_terminal.context_manager, "conversation_history", None) or []
|
||
if target_index < 0 or target_index >= len(history):
|
||
return
|
||
target_msg = history[target_index] or {}
|
||
if target_msg.get("role") != "user":
|
||
return
|
||
metadata = target_msg.get("metadata") or {}
|
||
timer = metadata.get("work_timer")
|
||
if not isinstance(timer, dict):
|
||
return
|
||
started_at = timer.get("started_at") or target_msg.get("timestamp") or user_work_started_at
|
||
start_ts = None
|
||
try:
|
||
start_ts = datetime.fromisoformat(str(started_at).replace("Z", "+00:00")).timestamp()
|
||
except Exception:
|
||
start_ts = None
|
||
now_ts = time.time()
|
||
duration_ms = int(max(0.0, (now_ts - start_ts) * 1000.0)) if start_ts is not None else 0
|
||
timer.update({
|
||
"status": "completed",
|
||
"started_at": started_at,
|
||
"finished_at": datetime.now().isoformat(),
|
||
"duration_ms": duration_ms
|
||
})
|
||
metadata["work_timer"] = timer
|
||
target_msg["metadata"] = metadata
|
||
history[target_index] = target_msg
|
||
web_terminal.context_manager.auto_save_conversation(force=True)
|
||
finalized_work_message_indices.add(target_index)
|
||
|
||
def switch_current_work_timer_to_latest_user():
|
||
nonlocal current_work_user_message_index
|
||
history = getattr(web_terminal.context_manager, "conversation_history", None) or []
|
||
for idx in range(len(history) - 1, -1, -1):
|
||
msg = history[idx] or {}
|
||
if msg.get("role") == "user":
|
||
metadata = msg.get("metadata") or {}
|
||
if isinstance(metadata.get("work_timer"), dict):
|
||
current_work_user_message_index = idx
|
||
return
|
||
|
||
versioning_checkpoint_recorded = False
|
||
|
||
def finalize_run_versioning_checkpoint(run_status: str = "completed"):
|
||
nonlocal versioning_checkpoint_recorded
|
||
if versioning_checkpoint_recorded:
|
||
return
|
||
_record_hidden_versioning_checkpoint_after_run(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
conversation_id=conversation_id,
|
||
message=message,
|
||
message_index=user_message_index,
|
||
auto_user_message_event=bool(auto_user_message_event),
|
||
run_status=run_status,
|
||
)
|
||
versioning_checkpoint_recorded = True
|
||
|
||
# Skill 提示系统:检测关键词并在用户消息之后插入 system 消息
|
||
try:
|
||
personal_config = load_personalization_config(workspace.data_dir)
|
||
skill_hints_enabled = personal_config.get("skill_hints_enabled", False)
|
||
|
||
if skill_hints_enabled and message:
|
||
hint_manager = SkillHintManager()
|
||
hint_manager.set_enabled(True)
|
||
hint_messages = hint_manager.build_hint_messages(message)
|
||
|
||
# 将提示消息插入到对话历史中(在用户消息之后)
|
||
for hint_msg in hint_messages:
|
||
debug_log(f"[Skill Hints] 插入提示消息: {hint_msg['content'][:100]}")
|
||
web_terminal.context_manager.add_conversation(
|
||
"system",
|
||
hint_msg["content"]
|
||
)
|
||
# 验证插入后的消息
|
||
last_msg = web_terminal.context_manager.conversation_history[-1]
|
||
debug_log(f"[Skill Hints] 插入后验证 - role: {last_msg.get('role')}, content: {last_msg.get('content')[:100]}")
|
||
except Exception as exc:
|
||
debug_log(f"Skill hints 处理失败: {exc}")
|
||
|
||
if is_first_user_message and getattr(web_terminal, "context_manager", None):
|
||
try:
|
||
personal_config = load_personalization_config(workspace.data_dir)
|
||
except Exception:
|
||
personal_config = {}
|
||
auto_title_enabled = personal_config.get("auto_generate_title", True)
|
||
skip_auto_title_generation = bool(
|
||
(web_terminal.context_manager.conversation_metadata or {}).get("skip_auto_title_generation", False)
|
||
)
|
||
if auto_title_enabled and not skip_auto_title_generation:
|
||
conv_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
|
||
socketio.start_background_task(
|
||
generate_conversation_title_background,
|
||
web_terminal,
|
||
conv_id,
|
||
message,
|
||
username
|
||
)
|
||
|
||
# 自动深层压缩(用户输入后触发)
|
||
try:
|
||
personal_config = load_personalization_config(workspace.data_dir)
|
||
except Exception:
|
||
personal_config = {}
|
||
compression_settings = resolve_context_compression_settings(personal_config)
|
||
auto_deep_enabled = bool(personal_config.get("auto_deep_compress_enabled", False))
|
||
current_tokens_for_deep = web_terminal.context_manager.get_current_context_tokens(conversation_id)
|
||
if (
|
||
auto_deep_enabled
|
||
and current_tokens_for_deep > compression_settings["deep_trigger_tokens"]
|
||
and not web_terminal.context_manager.is_compression_in_progress()
|
||
):
|
||
web_terminal.context_manager._set_meta_flag("is_ultra_long_conversation", True)
|
||
sender('compression_state', {
|
||
"conversation_id": conversation_id,
|
||
"in_progress": True,
|
||
"mode": "auto",
|
||
"stage": "queued"
|
||
})
|
||
deep_result = await run_deep_compression(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
conversation_id=conversation_id,
|
||
mode="auto",
|
||
sender=sender,
|
||
)
|
||
if not deep_result.get("success"):
|
||
sender('error', {
|
||
"message": deep_result.get("error") or "自动深层压缩失败",
|
||
"conversation_id": conversation_id,
|
||
})
|
||
else:
|
||
guide_message = (deep_result.get("guide_message") or "").strip()
|
||
if guide_message:
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("auto_deep_compress_handoff")
|
||
# 压缩后的续接引导语单独标记来源,使其以 compact 形式展示,
|
||
# 而不是混入普通用户消息流。
|
||
setattr(web_terminal, "_current_user_message_source", "compression_handoff")
|
||
try:
|
||
await handle_task_with_sender(
|
||
web_terminal,
|
||
workspace,
|
||
guide_message,
|
||
[],
|
||
sender,
|
||
client_sid,
|
||
username,
|
||
[]
|
||
)
|
||
finally:
|
||
setattr(web_terminal, "_current_user_message_source", "user")
|
||
return
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("auto_deep_compress_end")
|
||
return
|
||
|
||
# === 移除:不在这里计算输入token,改为在每次API调用前计算 ===
|
||
|
||
# 构建上下文和消息(用于API调用)
|
||
context = web_terminal.build_context()
|
||
messages = web_terminal.build_messages(context, message)
|
||
tools = web_terminal.define_tools()
|
||
|
||
# === 目标模式(Goal Mode)启动 / 续注入 ===
|
||
# 1) 用户请求开启目标模式 → 无条件覆盖工作区旧目标状态并启动新目标。
|
||
# 2) 工作区已存在活动目标(含压缩后重入、子智能体通知重入)→ 重新注入提示词,
|
||
# 确保主模型在长运行/压缩后仍“知道自己处于目标模式”。
|
||
try:
|
||
if bool(getattr(web_terminal, "_goal_mode_requested", False)):
|
||
maybe_start_goal(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
conversation_id=conversation_id,
|
||
goal_text=message,
|
||
current_tool_calls=0,
|
||
)
|
||
# 本标记只表示“本次用户显式开启目标模式”。目标状态已写入
|
||
# goal_state.json 后必须立即消费掉,否则自动深层压缩后的递归
|
||
# handoff 会再次进入 handle_task_with_sender,把压缩引导语误当作
|
||
# 新目标并重置目标文本、轮数、token、工具次数和开始时间。
|
||
setattr(web_terminal, "_goal_mode_requested", False)
|
||
if goal_is_active(workspace):
|
||
inject_goal_prompt(
|
||
web_terminal=web_terminal,
|
||
messages=messages,
|
||
sender=sender,
|
||
conversation_id=conversation_id,
|
||
)
|
||
emit_goal_progress(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
sender=sender,
|
||
total_tool_calls=0,
|
||
)
|
||
except Exception as exc:
|
||
debug_log(f"[Goal] 启动/注入失败: {exc}")
|
||
|
||
try:
|
||
profile = get_model_profile(getattr(web_terminal, "model_key", None))
|
||
web_terminal.apply_model_profile(profile)
|
||
except Exception as exc:
|
||
debug_log(f"更新模型配置失败: {exc}")
|
||
|
||
# === 上下文预算与安全校验(避免超出模型上下文) ===
|
||
max_context_tokens = get_model_context_window(getattr(web_terminal, "model_key", None))
|
||
current_tokens = web_terminal.context_manager.get_current_context_tokens(conversation_id)
|
||
# 提前同步给底层客户端,动态收缩 max_tokens
|
||
web_terminal.api_client.update_context_budget(current_tokens, max_context_tokens)
|
||
if max_context_tokens:
|
||
if current_tokens >= max_context_tokens:
|
||
err_msg = (
|
||
f"当前对话上下文已达 {current_tokens} tokens,超过模型上限 "
|
||
f"{max_context_tokens},请先使用压缩功能或清理对话后再试。"
|
||
)
|
||
debug_log(err_msg)
|
||
web_terminal.context_manager.add_conversation("system", err_msg)
|
||
sender('error', {
|
||
'message': err_msg,
|
||
'status_code': 400,
|
||
'error_type': 'context_overflow'
|
||
})
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("context_overflow")
|
||
return
|
||
usage_percent = (current_tokens / max_context_tokens) * 100
|
||
warned = web_terminal.context_manager.conversation_metadata.get("context_warning_sent", False)
|
||
if usage_percent >= 70 and not warned:
|
||
warn_msg = (
|
||
f"当前对话上下文约占 {usage_percent:.1f}%({current_tokens}/{max_context_tokens}),"
|
||
"建议使用压缩功能。"
|
||
)
|
||
web_terminal.context_manager.conversation_metadata["context_warning_sent"] = True
|
||
web_terminal.context_manager.auto_save_conversation(force=True)
|
||
sender('context_warning', {
|
||
'title': '上下文过长',
|
||
'message': warn_msg,
|
||
'type': 'warning',
|
||
'conversation_id': conversation_id
|
||
})
|
||
|
||
# 开始新的AI消息
|
||
sender('ai_message_start', {})
|
||
|
||
# 增量保存相关变量
|
||
accumulated_response = "" # 累积的响应内容
|
||
is_first_iteration = True # 是否是第一次迭代
|
||
|
||
# 统计和限制变量
|
||
total_iterations = 0
|
||
total_tool_calls = 0
|
||
consecutive_same_tool = defaultdict(int)
|
||
last_tool_name = ""
|
||
auto_fix_attempts = 0
|
||
last_tool_call_time = 0
|
||
detected_tool_intent: Dict[str, str] = {}
|
||
# 目标模式:记录本“目标轮段”内主模型是否调用过工具(空转保护用)。
|
||
# 每次续命注入后重置为 False,直到下一次 no-tool-call break。
|
||
goal_segment_made_tool_call = False
|
||
|
||
# 设置最大迭代次数(API 可覆盖);None 表示不限制
|
||
max_iterations_override = getattr(web_terminal, "max_iterations_override", None)
|
||
max_iterations = max_iterations_override if max_iterations_override is not None else MAX_ITERATIONS_PER_TASK
|
||
max_api_retries = 4
|
||
retry_delay_seconds = 10
|
||
|
||
|
||
iteration = 0
|
||
while max_iterations is None or iteration < max_iterations:
|
||
# 检查停止标志
|
||
stop_entry = get_stop_flag(client_sid, username)
|
||
if stop_entry and stop_entry.get('stop'):
|
||
debug_log(f"[Task] 检测到停止标志,退出循环")
|
||
try:
|
||
stop_goal_user_cancel(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
sender=sender,
|
||
total_tool_calls=total_tool_calls,
|
||
)
|
||
except Exception as exc:
|
||
debug_log(f"[Goal] 用户停止时停止目标失败: {exc}")
|
||
sender('task_stopped', {
|
||
'message': '任务已停止',
|
||
'reason': 'user_requested'
|
||
})
|
||
break
|
||
|
||
current_iteration = iteration + 1
|
||
iteration += 1
|
||
total_iterations += 1
|
||
iteration_limit_label = max_iterations if max_iterations is not None else "∞"
|
||
debug_log(f"\n--- 迭代 {current_iteration}/{iteration_limit_label} 开始 ---")
|
||
|
||
# 检查是否超过总工具调用限制
|
||
if MAX_TOTAL_TOOL_CALLS is not None and total_tool_calls >= MAX_TOTAL_TOOL_CALLS:
|
||
debug_log(f"已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS})")
|
||
sender('system_message', {
|
||
'content': f'⚠️ 已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS}),任务结束。'
|
||
})
|
||
mark_force_thinking(web_terminal, reason="tool_limit")
|
||
break
|
||
|
||
apply_thinking_schedule(web_terminal)
|
||
|
||
full_response = ""
|
||
tool_calls = []
|
||
current_thinking = ""
|
||
detected_tools = {}
|
||
last_usage_payload = None
|
||
|
||
# 状态标志
|
||
in_thinking = False
|
||
thinking_started = False
|
||
thinking_ended = False
|
||
text_started = False
|
||
text_has_content = False
|
||
text_streaming = False
|
||
text_chunk_index = 0
|
||
last_text_chunk_time: Optional[float] = None
|
||
|
||
# 计数器
|
||
chunk_count = 0
|
||
reasoning_chunks = 0
|
||
content_chunks = 0
|
||
tool_chunks = 0
|
||
last_finish_reason = None
|
||
|
||
thinking_expected = web_terminal.api_client.get_current_thinking_mode()
|
||
debug_log(f"思考模式: {thinking_expected}")
|
||
quota_allowed = True
|
||
quota_info = {}
|
||
if hasattr(web_terminal, "record_model_call"):
|
||
quota_allowed, quota_info = web_terminal.record_model_call(bool(thinking_expected))
|
||
if not quota_allowed:
|
||
quota_type = 'thinking' if thinking_expected else 'fast'
|
||
socketio.emit('quota_notice', {
|
||
'type': quota_type,
|
||
'reset_at': quota_info.get('reset_at'),
|
||
'limit': quota_info.get('limit'),
|
||
'count': quota_info.get('count')
|
||
}, room=f"user_{getattr(web_terminal, 'username', '')}")
|
||
sender('quota_exceeded', {
|
||
'type': quota_type,
|
||
'reset_at': quota_info.get('reset_at')
|
||
})
|
||
sender('error', {
|
||
'message': "配额已达到上限,暂时无法继续调用模型。",
|
||
'quota': quota_info
|
||
})
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("quota_exceeded")
|
||
return
|
||
|
||
tool_call_limit_label = MAX_TOTAL_TOOL_CALLS if MAX_TOTAL_TOOL_CALLS is not None else "∞"
|
||
print(f"[API] 第{current_iteration}次调用 (总工具调用: {total_tool_calls}/{tool_call_limit_label})")
|
||
|
||
stream_result = await run_streaming_attempts(
|
||
web_terminal=web_terminal,
|
||
messages=messages,
|
||
tools=tools,
|
||
sender=sender,
|
||
client_sid=client_sid,
|
||
username=username,
|
||
conversation_id=conversation_id,
|
||
current_iteration=current_iteration,
|
||
max_api_retries=max_api_retries,
|
||
retry_delay_seconds=retry_delay_seconds,
|
||
detected_tool_intent=detected_tool_intent,
|
||
full_response=full_response,
|
||
tool_calls=tool_calls,
|
||
current_thinking=current_thinking,
|
||
detected_tools=detected_tools,
|
||
last_usage_payload=last_usage_payload,
|
||
in_thinking=in_thinking,
|
||
thinking_started=thinking_started,
|
||
thinking_ended=thinking_ended,
|
||
text_started=text_started,
|
||
text_has_content=text_has_content,
|
||
text_streaming=text_streaming,
|
||
text_chunk_index=text_chunk_index,
|
||
last_text_chunk_time=last_text_chunk_time,
|
||
chunk_count=chunk_count,
|
||
reasoning_chunks=reasoning_chunks,
|
||
content_chunks=content_chunks,
|
||
tool_chunks=tool_chunks,
|
||
last_finish_reason=last_finish_reason,
|
||
accumulated_response=accumulated_response,
|
||
)
|
||
if stream_result.get("stopped"):
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("stopped")
|
||
return
|
||
|
||
full_response = stream_result["full_response"]
|
||
tool_calls = stream_result["tool_calls"]
|
||
current_thinking = stream_result["current_thinking"]
|
||
detected_tools = stream_result["detected_tools"]
|
||
last_usage_payload = stream_result["last_usage_payload"]
|
||
in_thinking = stream_result["in_thinking"]
|
||
thinking_started = stream_result["thinking_started"]
|
||
thinking_ended = stream_result["thinking_ended"]
|
||
text_started = stream_result["text_started"]
|
||
text_has_content = stream_result["text_has_content"]
|
||
text_streaming = stream_result["text_streaming"]
|
||
text_chunk_index = stream_result["text_chunk_index"]
|
||
last_text_chunk_time = stream_result["last_text_chunk_time"]
|
||
chunk_count = stream_result["chunk_count"]
|
||
reasoning_chunks = stream_result["reasoning_chunks"]
|
||
content_chunks = stream_result["content_chunks"]
|
||
tool_chunks = stream_result["tool_chunks"]
|
||
last_finish_reason = stream_result["last_finish_reason"]
|
||
accumulated_response = stream_result["accumulated_response"]
|
||
|
||
# 流结束后的处理
|
||
debug_log(f"\n流结束统计:")
|
||
debug_log(f" 总chunks: {chunk_count}")
|
||
debug_log(f" 思考chunks: {reasoning_chunks}")
|
||
debug_log(f" 内容chunks: {content_chunks}")
|
||
debug_log(f" 工具chunks: {tool_chunks}")
|
||
debug_log(f" 收集到的思考: {len(current_thinking)} 字符")
|
||
debug_log(f" 收集到的正文: {len(full_response)} 字符")
|
||
debug_log(f" 收集到的工具: {len(tool_calls)} 个")
|
||
|
||
# 结束未完成的流
|
||
if in_thinking and not thinking_ended:
|
||
sender('thinking_end', {'full_content': current_thinking})
|
||
await asyncio.sleep(0.1)
|
||
|
||
|
||
# 确保text_end事件被发送
|
||
if text_started and text_has_content:
|
||
debug_log(f"发送text_end事件,完整内容长度: {len(full_response)}")
|
||
sender('text_end', {'full_content': full_response})
|
||
await asyncio.sleep(0.1)
|
||
text_streaming = False
|
||
|
||
if full_response.strip():
|
||
debug_log(f"流式文本内容长度: {len(full_response)} 字符")
|
||
|
||
if web_terminal.api_client.last_call_used_thinking and current_thinking:
|
||
web_terminal.api_client.current_task_thinking = current_thinking or ""
|
||
if web_terminal.api_client.current_task_first_call:
|
||
web_terminal.api_client.current_task_first_call = False
|
||
update_thinking_after_call(web_terminal)
|
||
|
||
# 检测是否有格式错误的工具调用
|
||
if not tool_calls and full_response and AUTO_FIX_TOOL_CALL:
|
||
if detect_malformed_tool_call(full_response):
|
||
auto_fix_attempts += 1
|
||
|
||
if auto_fix_attempts <= AUTO_FIX_MAX_ATTEMPTS:
|
||
debug_log(f"检测到格式错误的工具调用,尝试自动修复 (尝试 {auto_fix_attempts}/{AUTO_FIX_MAX_ATTEMPTS})")
|
||
|
||
fix_message = "你使用了错误的格式输出工具调用。请使用正确的工具调用格式而不是直接输出JSON。根据当前进度继续执行任务。"
|
||
|
||
sender('system_message', {
|
||
'content': f'⚠️ 自动修复: {fix_message}'
|
||
})
|
||
maybe_mark_failure_from_message(web_terminal, f'⚠️ 自动修复: {fix_message}')
|
||
|
||
messages.append({
|
||
"role": "user",
|
||
"content": fix_message
|
||
})
|
||
|
||
await asyncio.sleep(1)
|
||
continue
|
||
else:
|
||
debug_log(f"自动修复尝试已达上限 ({AUTO_FIX_MAX_ATTEMPTS})")
|
||
sender('system_message', {
|
||
'content': f'⌘ 工具调用格式错误,自动修复失败。请手动检查并重试。'
|
||
})
|
||
maybe_mark_failure_from_message(web_terminal, '⌘ 工具调用格式错误,自动修复失败。请手动检查并重试。')
|
||
break
|
||
|
||
# 构建助手消息(用于API继续对话)
|
||
assistant_content_parts = []
|
||
|
||
if full_response:
|
||
assistant_content_parts.append(full_response)
|
||
|
||
assistant_content = "\n".join(assistant_content_parts) if assistant_content_parts else ""
|
||
|
||
# 添加到消息历史(用于API继续对话,不保存到文件)
|
||
assistant_message = {
|
||
"role": "assistant",
|
||
"content": assistant_content,
|
||
# thinking 模式下,reasoning_content 需要原样回传;即使该轮为空字符串也保留字段
|
||
"reasoning_content": current_thinking or "",
|
||
}
|
||
if tool_calls:
|
||
assistant_message["tool_calls"] = tool_calls
|
||
|
||
messages.append(assistant_message)
|
||
if assistant_content or current_thinking or tool_calls:
|
||
web_terminal.context_manager.add_conversation(
|
||
"assistant",
|
||
assistant_content,
|
||
tool_calls=tool_calls if tool_calls else None,
|
||
reasoning_content=current_thinking or ""
|
||
)
|
||
|
||
# 为下一轮迭代重置流状态标志,但保留 full_response 供上面保存使用
|
||
text_streaming = False
|
||
text_started = False
|
||
text_has_content = False
|
||
full_response = ""
|
||
|
||
if not tool_calls:
|
||
debug_log("没有工具调用,结束迭代")
|
||
# === 目标模式:turn 结束拦截 ===
|
||
try:
|
||
if goal_is_active(workspace):
|
||
goal_result = await handle_goal_after_turn(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
messages=messages,
|
||
sender=sender,
|
||
conversation_id=conversation_id,
|
||
assistant_content=assistant_content,
|
||
made_tool_call=goal_segment_made_tool_call,
|
||
total_tool_calls=total_tool_calls,
|
||
)
|
||
action = goal_result.get("action")
|
||
if action == "continue":
|
||
# 已注入续命 user 消息:上一段 assistant work 到此结束,
|
||
# 后续计时切换到刚注入的 goal_review 消息。
|
||
finalize_user_work_timer()
|
||
switch_current_work_timer_to_latest_user()
|
||
goal_segment_made_tool_call = False
|
||
is_first_iteration = False
|
||
debug_log(f"[Goal] 续命:{goal_result.get('message', '')[:80]}")
|
||
continue
|
||
if action == "done":
|
||
debug_log("[Goal] 目标已达成,结束任务")
|
||
elif action == "stop":
|
||
debug_log(f"[Goal] 目标停止:{goal_result.get('reason')}")
|
||
except Exception as exc:
|
||
debug_log(f"[Goal] turn 结束处理失败: {exc}")
|
||
break
|
||
|
||
# 目标模式:本轮段确实产生了工具调用
|
||
goal_segment_made_tool_call = True
|
||
|
||
# 检查连续相同工具调用
|
||
for tc in tool_calls:
|
||
tool_name = tc["function"]["name"]
|
||
|
||
if tool_name == last_tool_name:
|
||
consecutive_same_tool[tool_name] += 1
|
||
|
||
if (
|
||
MAX_CONSECUTIVE_SAME_TOOL is not None
|
||
and consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL
|
||
):
|
||
debug_log(f"警告: 连续调用相同工具 {tool_name} 已达 {MAX_CONSECUTIVE_SAME_TOOL} 次")
|
||
sender('system_message', {
|
||
'content': f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。'
|
||
})
|
||
maybe_mark_failure_from_message(web_terminal, f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。')
|
||
|
||
if consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL + 2:
|
||
debug_log(f"终止: 工具 {tool_name} 调用次数过多")
|
||
sender('system_message', {
|
||
'content': f'⌘ 工具 {tool_name} 重复调用过多,任务终止。'
|
||
})
|
||
maybe_mark_failure_from_message(web_terminal, f'⌘ 工具 {tool_name} 重复调用过多,任务终止。')
|
||
break
|
||
else:
|
||
consecutive_same_tool.clear()
|
||
consecutive_same_tool[tool_name] = 1
|
||
|
||
last_tool_name = tool_name
|
||
|
||
# 更新统计
|
||
total_tool_calls += len(tool_calls)
|
||
try:
|
||
if goal_is_active(workspace):
|
||
emit_goal_progress(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
sender=sender,
|
||
total_tool_calls=total_tool_calls,
|
||
)
|
||
except Exception as exc:
|
||
debug_log(f"[Goal] 进度广播失败: {exc}")
|
||
|
||
# 执行每个工具
|
||
tool_loop_result = await execute_tool_calls(
|
||
web_terminal=web_terminal,
|
||
tool_calls=tool_calls,
|
||
sender=sender,
|
||
messages=messages,
|
||
client_sid=client_sid,
|
||
username=username,
|
||
iteration=iteration,
|
||
conversation_id=conversation_id,
|
||
last_tool_call_time=last_tool_call_time,
|
||
process_sub_agent_updates=process_sub_agent_updates,
|
||
process_background_command_updates=process_background_command_updates,
|
||
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
|
||
mark_force_thinking=mark_force_thinking,
|
||
get_stop_flag=get_stop_flag,
|
||
clear_stop_flag=clear_stop_flag,
|
||
workspace=workspace,
|
||
)
|
||
last_tool_call_time = tool_loop_result.get("last_tool_call_time", last_tool_call_time)
|
||
if tool_loop_result.get("stopped"):
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("stopped")
|
||
return
|
||
if tool_loop_result.get("approval_rejected"):
|
||
sender('task_stopped', {
|
||
'message': tool_loop_result.get("approval_message") or '操作被用户拒绝',
|
||
'reason': 'approval_rejected',
|
||
'conversation_id': conversation_id
|
||
})
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("approval_rejected")
|
||
return
|
||
if tool_loop_result.get("deep_compressed"):
|
||
deep_result = tool_loop_result.get("deep_result") or {}
|
||
guide_message = (deep_result.get("guide_message") or "").strip()
|
||
if deep_result.get("success") and guide_message:
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("deep_compress_handoff")
|
||
# 压缩后的续接引导语单独标记来源,使其以 compact 形式展示,
|
||
# 而不是混入普通用户消息流。
|
||
setattr(web_terminal, "_current_user_message_source", "compression_handoff")
|
||
try:
|
||
await handle_task_with_sender(
|
||
web_terminal,
|
||
workspace,
|
||
guide_message,
|
||
[],
|
||
sender,
|
||
client_sid,
|
||
username,
|
||
[]
|
||
)
|
||
finally:
|
||
setattr(web_terminal, "_current_user_message_source", "user")
|
||
return
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("deep_compress_end")
|
||
return
|
||
|
||
# 标记不再是第一次迭代
|
||
is_first_iteration = False
|
||
|
||
|
||
# 最终统计
|
||
debug_log(f"\n{'='*40}")
|
||
debug_log(f"任务完成统计:")
|
||
debug_log(f" 总迭代次数: {total_iterations}")
|
||
debug_log(f" 总工具调用: {total_tool_calls}")
|
||
debug_log(f" 自动修复尝试: {auto_fix_attempts}")
|
||
debug_log(f" 累积响应: {len(accumulated_response)} 字符")
|
||
debug_log(f"{'='*40}\n")
|
||
|
||
# 检查是否有后台运行的子智能体或待通知的完成任务
|
||
manager = getattr(web_terminal, "sub_agent_manager", None)
|
||
has_running_sub_agents = False
|
||
bg_manager = getattr(web_terminal, "background_command_manager", None)
|
||
has_running_background_commands = False
|
||
if manager:
|
||
try:
|
||
manager.reconcile_task_states(conversation_id=conversation_id)
|
||
except Exception as exc:
|
||
debug_log(f"[SubAgent] reconcile_task_states failed: {exc}")
|
||
if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
|
||
web_terminal._announced_sub_agent_tasks = set()
|
||
running_tasks = [
|
||
task for task in manager.tasks.values()
|
||
if task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
|
||
and task.get("run_in_background")
|
||
and task.get("conversation_id") == conversation_id
|
||
]
|
||
pending_notice_tasks = [
|
||
task for task in manager.tasks.values()
|
||
if task.get("status") in TERMINAL_STATUSES.union({"terminated"})
|
||
and task.get("run_in_background")
|
||
and task.get("conversation_id") == conversation_id
|
||
and task.get("task_id") not in web_terminal._announced_sub_agent_tasks
|
||
]
|
||
|
||
if running_tasks or pending_notice_tasks:
|
||
has_running_sub_agents = True
|
||
notify_tasks = running_tasks + pending_notice_tasks
|
||
debug_log(f"[SubAgent] 后台子智能体等待: running={len(running_tasks)} pending_notice={len(pending_notice_tasks)}")
|
||
# 先通知前端:有子智能体在运行/待通知,保持等待状态
|
||
sender('sub_agent_waiting', {
|
||
'count': len(notify_tasks),
|
||
'tasks': [{'agent_id': t.get('agent_id'), 'summary': t.get('summary')} for t in notify_tasks]
|
||
})
|
||
|
||
# 启动后台任务来轮询/补发子智能体完成
|
||
def run_poll():
|
||
import asyncio
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
try:
|
||
loop.run_until_complete(poll_sub_agent_completion(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
conversation_id=conversation_id,
|
||
client_sid=client_sid,
|
||
username=username
|
||
))
|
||
finally:
|
||
loop.close()
|
||
|
||
socketio.start_background_task(run_poll)
|
||
|
||
# 检查是否有后台 run_command 或待通知任务
|
||
if bg_manager and conversation_id:
|
||
try:
|
||
bg_manager.reconcile_stale_records(conversation_id=conversation_id)
|
||
except Exception as exc:
|
||
debug_log(f"[BgCmdDebug] reconcile_stale_records failed: {exc}")
|
||
waiting_items = bg_manager.list_waiting_items(conversation_id)
|
||
if waiting_items:
|
||
has_running_background_commands = True
|
||
# 与子智能体完全复用同一 waiting 事件(前端已有稳定处理链路)
|
||
sender('sub_agent_waiting', _build_shared_waiting_payload(waiting_items))
|
||
|
||
def run_bg_poll():
|
||
import asyncio
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
try:
|
||
loop.run_until_complete(poll_background_command_completion(
|
||
web_terminal=web_terminal,
|
||
workspace=workspace,
|
||
conversation_id=conversation_id,
|
||
client_sid=client_sid,
|
||
username=username
|
||
))
|
||
finally:
|
||
loop.close()
|
||
|
||
socketio.start_background_task(run_bg_poll)
|
||
|
||
has_running_completion_jobs = has_running_sub_agents or has_running_background_commands
|
||
pending_runtime_guidance_messages: List[str] = []
|
||
try:
|
||
from .tasks import task_manager
|
||
|
||
raw_items = task_manager.consume_runtime_guidance_for_injection(
|
||
username=username,
|
||
task_id=client_sid,
|
||
)
|
||
# 权限/审核方式变更通知在任务完成时不应该触发新一轮工作,
|
||
# 只保留真正的引导消息(压缩续接等),丢弃权限/执行环境变更通知。
|
||
runtime_skip_sources = {"权限变更", "执行环境变更", "notify"}
|
||
for item in (raw_items or []):
|
||
if isinstance(item, dict):
|
||
src = str(item.get("source") or "").strip().lower()
|
||
text = str(item.get("text") or "").strip()
|
||
if src in runtime_skip_sources:
|
||
continue
|
||
if text:
|
||
pending_runtime_guidance_messages.append(text)
|
||
else:
|
||
text = str(item or "").strip()
|
||
if text:
|
||
pending_runtime_guidance_messages.append(text)
|
||
except Exception as exc:
|
||
debug_log(f"[RuntimeGuidance] 读取剩余引导消息失败: {exc}")
|
||
pending_runtime_guidance_messages = []
|
||
|
||
# 发送完成事件(如果有后台完成任务在运行,前端会保持等待状态)
|
||
if not has_running_completion_jobs:
|
||
finalize_user_work_timer()
|
||
finalize_run_versioning_checkpoint("completed")
|
||
else:
|
||
finalize_run_versioning_checkpoint("waiting_background")
|
||
sender('task_complete', {
|
||
'total_iterations': total_iterations,
|
||
'total_tool_calls': total_tool_calls,
|
||
'auto_fix_attempts': auto_fix_attempts,
|
||
# 沿用子智能体字段,确保前端直接走已验证通路
|
||
'has_running_sub_agents': has_running_completion_jobs,
|
||
'has_running_background_commands': has_running_background_commands,
|
||
'pending_runtime_guidance_messages': pending_runtime_guidance_messages,
|
||
})
|