diff --git a/server/app_legacy.py b/server/app_legacy.py index b020351..a971c9e 100644 --- a/server/app_legacy.py +++ b/server/app_legacy.py @@ -783,6 +783,7 @@ DEBUG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "debug_stream.log" CHUNK_BACKEND_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "chunk_backend.log" CHUNK_FRONTEND_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "chunk_frontend.log" STREAMING_DEBUG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "streaming_debug.log" +GOAL_MODE_DEBUG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "goal_mode_debug.log" UPLOAD_FOLDER_NAME = ".agents/user_upload" @@ -1171,6 +1172,14 @@ def log_streaming_debug_entry(data: Dict[str, Any]): _write_log(STREAMING_DEBUG_LOG_FILE, serialized) +def log_goal_mode_debug_entry(data: Dict[str, Any]): + try: + serialized = json.dumps(data, ensure_ascii=False) + except Exception: + serialized = str(data) + _write_log(GOAL_MODE_DEBUG_LOG_FILE, serialized) + + def get_thinking_state(terminal: WebTerminal) -> Dict[str, Any]: """获取(或初始化)思考调度状态。""" state = getattr(terminal, "_thinking_state", None) @@ -1399,6 +1408,19 @@ def voice_debug(): return jsonify({'ok': True, 'file': filename}) +@app.route('/api/client_debug_log', methods=['POST']) +def client_debug_log(): + """接收前端目标模式等调试日志""" + try: + data = request.get_json(silent=True) or {} + entry = dict(data) + entry.setdefault('server_ts', time.time()) + log_goal_mode_debug_entry(entry) + return jsonify({'ok': True}) + except Exception as e: + return jsonify({'ok': False, 'error': str(e)}), 500 + + if __name__ == "__main__": args = parse_arguments() run_server( diff --git a/server/tasks.py b/server/tasks.py index a0efede..216a877 100644 --- a/server/tasks.py +++ b/server/tasks.py @@ -20,6 +20,7 @@ from .state import stop_flags from .utils_common import debug_log, log_conn_diag from utils.host_workspace_debug import write_host_workspace_debug from config import DATA_DIR, WORKSPACE_SKILLS_DIRNAME +from modules.goal_state_manager import GoalStateManager, REASON_USER_CANCEL SKILL_FRONTMATTER_RE = re.compile(r"^---\s*\n(?P.*?)\n---\s*\n?", re.S) @@ -980,6 +981,18 @@ def create_task_api(): message_source = payload.get("message_source") max_iterations = payload.get("max_iterations") goal_mode = bool(payload.get("goal_mode")) + # 用户显式发送非目标模式消息时,若工作区仍有上一轮残留的活动目标,先清理, + # 避免新对话错误继承旧目标状态。 + if not goal_mode: + try: + _terminal, workspace = get_user_resources(username, workspace_id) + if workspace: + gsm = GoalStateManager(workspace.data_dir) + if gsm.is_active(): + gsm.mark_stopped("new_conversation") + debug_log(f"[Goal] 新任务未开启目标模式,清理工作区残留目标状态") + except Exception as exc: + debug_log(f"[Goal] 新任务清理残留目标状态失败: {exc}") skill_context_messages: List[Dict[str, str]] = [] raw_skill_refs = payload.get("skill_refs") if raw_skill_refs: @@ -1133,9 +1146,21 @@ def get_task_api(task_id: str): @api_login_required def cancel_task_api(task_id: str): username = get_current_username() - ok = task_manager.cancel_task(username, task_id) - if not ok: + rec = task_manager.get_task(username, task_id) + if not rec: return jsonify({"success": False, "error": "任务不存在"}), 404 + ok = task_manager.cancel_task(username, task_id) + # 用户取消任务时,一并停止该工作区的目标模式,避免后续新对话继承旧目标。 + try: + if ok and rec.workspace_id: + _, workspace = get_user_resources(username, rec.workspace_id) + if workspace: + gsm = GoalStateManager(workspace.data_dir) + if gsm.is_active(): + gsm.mark_stopped(REASON_USER_CANCEL) + debug_log(f"[Goal] 用户取消任务 {task_id},同步停止工作区目标模式") + except Exception as exc: + debug_log(f"[Goal] 取消任务时停止目标模式失败: {exc}") return jsonify({"success": True}) diff --git a/static/src/app/methods/common.ts b/static/src/app/methods/common.ts index e05770f..868ebb5 100644 --- a/static/src/app/methods/common.ts +++ b/static/src/app/methods/common.ts @@ -19,3 +19,45 @@ export const traceLog = (...args) => { // ignore } }; + +// 目标模式调试日志:上报到后端 goal_mode_debug.log,便于复现状态继承问题。 +// 默认关闭,避免日常刷屏;需要时可在浏览器控制台执行 localStorage.setItem('goalModeDebug','1') 后刷新。 +function isGoalModeDebugEnabled() { + if (typeof window === 'undefined') return false; + try { + const explicit = (window as any).__GOAL_MODE_DEBUG__; + if (explicit === true || explicit === '1') return true; + if (explicit === false || explicit === '0') return false; + const localFlag = window.localStorage?.getItem('goalModeDebug'); + if (localFlag === '1' || localFlag === 'true') return true; + return false; + } catch { + return false; + } +} + +let goalModeDebugSeq = 0; +export function goalModeDebugLog(event: string, payload: Record = {}) { + if (!isGoalModeDebugEnabled()) return; + try { + goalModeDebugSeq += 1; + const entry = { + event, + seq: goalModeDebugSeq, + client_ts: new Date().toISOString(), + ...payload + }; + // 同时输出到浏览器控制台,方便开发时查看 + console.log('[GOAL_MODE_DEBUG]', event, entry); + // 上报后端持久化 + if (typeof fetch !== 'undefined') { + void fetch('/api/client_debug_log', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(entry) + }).catch(() => {}); + } + } catch (e) { + // ignore logging errors + } +}