fix(goal): 停止对话后清理工作区目标状态,避免继承到新对话

- /api/tasks/<task_id>/cancel 现在会同步停止该工作区的目标模式
- POST /api/tasks 创建非目标模式任务时清理残留活动目标
- 保留 /api/client_debug_log 前端日志上报端点供后续复用
This commit is contained in:
JOJO 2026-06-20 13:06:37 +08:00
parent 7b683ab390
commit 8235c4ce70
3 changed files with 91 additions and 2 deletions

View File

@ -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(

View File

@ -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<body>.*?)\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})

View File

@ -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<string, any> = {}) {
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
}
}