myagent-ai 1.23.37 → 1.23.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/base.py +3 -0
- package/agents/main_agent.py +23 -1
- package/core/output_parser.py +6 -5
- package/executor/engine.py +93 -23
- package/package.json +2 -2
- package/web/api_server.py +70 -0
- package/web/ui/chat/flow_engine.js +10 -0
package/agents/base.py
CHANGED
|
@@ -371,6 +371,9 @@ class BaseAgent(ABC):
|
|
|
371
371
|
model=request_kwargs.get("model", self.llm.model),
|
|
372
372
|
reasoning=full_reasoning,
|
|
373
373
|
)
|
|
374
|
+
except asyncio.CancelledError:
|
|
375
|
+
# [v1.23.38] 传播取消信号,不吞掉
|
|
376
|
+
raise
|
|
374
377
|
except Exception as e:
|
|
375
378
|
logger.error(f"LLM 流式调用失败: {e}")
|
|
376
379
|
return LLMResponse(success=False, error=str(e))
|
package/agents/main_agent.py
CHANGED
|
@@ -215,6 +215,8 @@ class MainAgent(BaseAgent):
|
|
|
215
215
|
self._exec_event_counter: int = 0
|
|
216
216
|
# 活跃会话上下文追踪(用于消息注入)
|
|
217
217
|
self.active_contexts: Dict[str, AgentContext] = {}
|
|
218
|
+
# [v1.23.37] 会话取消标志(由 api_server.handle_chat_stop 设置)
|
|
219
|
+
self._cancelled_sessions: set = set()
|
|
218
220
|
|
|
219
221
|
def init_context_builder(self, memory_manager=None, skill_registry=None, knowledge_base_dir=None, context_window=None):
|
|
220
222
|
"""初始化 Context Builder(在系统启动后调用,注入依赖)"""
|
|
@@ -737,6 +739,14 @@ class MainAgent(BaseAgent):
|
|
|
737
739
|
logger.info(f"[{task_id}] V2 迭代 {self._iteration_count}/{max_iter}")
|
|
738
740
|
_emitted_reasoning_this_iter = False # [v1.15.8] 本轮是否已发送过 v2_reasoning 事件(防 TTS 重复)
|
|
739
741
|
|
|
742
|
+
# [v1.23.37] 检查是否收到停止信号
|
|
743
|
+
if context.session_id in self._cancelled_sessions:
|
|
744
|
+
logger.info(f"[{task_id}] 收到停止信号,终止执行循环")
|
|
745
|
+
context.working_memory["final_response"] = "⏹️ 任务已被用户停止"
|
|
746
|
+
await self._emit_v2_event("v2_stopped", {"reason": "用户手动停止"}, stream_callback)
|
|
747
|
+
self._cancelled_sessions.discard(context.session_id)
|
|
748
|
+
break
|
|
749
|
+
|
|
740
750
|
# ── 检查配置热加载广播 ──
|
|
741
751
|
if self.config_broadcaster:
|
|
742
752
|
reloaded, reload_type = await self.config_broadcaster.check_and_wait(task_id)
|
|
@@ -1559,6 +1569,14 @@ class MainAgent(BaseAgent):
|
|
|
1559
1569
|
|
|
1560
1570
|
all_tool_outputs = "\n".join(tool_outputs_parts)
|
|
1561
1571
|
|
|
1572
|
+
# [v1.23.38] 工具执行完毕后检查取消信号
|
|
1573
|
+
if context.session_id in self._cancelled_sessions:
|
|
1574
|
+
logger.info(f"[{task_id}] 收到停止信号,跳过 LLM 回调")
|
|
1575
|
+
context.working_memory["final_response"] = "⏹️ 任务已被用户停止"
|
|
1576
|
+
await self._emit_v2_event("v2_stopped", {"reason": "用户手动停止"}, stream_callback)
|
|
1577
|
+
self._cancelled_sessions.discard(context.session_id)
|
|
1578
|
+
break
|
|
1579
|
+
|
|
1562
1580
|
# Step 12: 有工具执行完毕 → 统一回调 LLM(不再根据 finish 标签判断)
|
|
1563
1581
|
# 所有工具结果已收集到 all_tool_outputs(或任一超时提前收集),直接喂回 LLM 继续执行
|
|
1564
1582
|
_timeout_info = "(有工具超时,已停止剩余工具)" if _has_timeout else f"({len(tool_outputs_parts)} 个工具结果已收集)"
|
|
@@ -1652,8 +1670,12 @@ class MainAgent(BaseAgent):
|
|
|
1652
1670
|
"""[v1.22.0] V2 工具执行 — 统一分发到 ToolDispatcher"""
|
|
1653
1671
|
try:
|
|
1654
1672
|
import json as _json
|
|
1673
|
+
import html as _html
|
|
1655
1674
|
try:
|
|
1656
|
-
|
|
1675
|
+
# [v1.23.38] 防御性修复: 解码 HTML 实体 (& → & 等)
|
|
1676
|
+
# LLM 可能在 XML 输出中使用 & 代替 &,导致 bash 命令失败
|
|
1677
|
+
_clean_parms = _html.unescape(parms_str) if parms_str else ""
|
|
1678
|
+
params = _json.loads(_clean_parms) if _clean_parms else {}
|
|
1657
1679
|
except (_json.JSONDecodeError, TypeError):
|
|
1658
1680
|
params = {"raw_input": parms_str}
|
|
1659
1681
|
|
package/core/output_parser.py
CHANGED
|
@@ -48,6 +48,7 @@ Fault-tolerance features:
|
|
|
48
48
|
|
|
49
49
|
from __future__ import annotations
|
|
50
50
|
|
|
51
|
+
import html
|
|
51
52
|
import re
|
|
52
53
|
from dataclasses import dataclass, field
|
|
53
54
|
from typing import Any, Dict, List
|
|
@@ -227,7 +228,7 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
|
|
|
227
228
|
re.DOTALL | re.IGNORECASE,
|
|
228
229
|
)
|
|
229
230
|
if m:
|
|
230
|
-
return m.group(1)
|
|
231
|
+
return html.unescape(m.group(1))
|
|
231
232
|
|
|
232
233
|
# Conservative mode: only extract properly closed tags, skip all fallbacks
|
|
233
234
|
if conservative:
|
|
@@ -251,7 +252,7 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
|
|
|
251
252
|
re.DOTALL | re.IGNORECASE,
|
|
252
253
|
)
|
|
253
254
|
if m:
|
|
254
|
-
return m.group(1)
|
|
255
|
+
return html.unescape(m.group(1))
|
|
255
256
|
|
|
256
257
|
# Strategy 3: Self-closing <tag/> or <tag />
|
|
257
258
|
m = re.search(rf"<{tag_esc}[^>]*/\s*>", text, re.IGNORECASE)
|
|
@@ -268,7 +269,7 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
|
|
|
268
269
|
content = m.group(1).strip()
|
|
269
270
|
# Only return if there's actual content (not just whitespace)
|
|
270
271
|
if content:
|
|
271
|
-
return content
|
|
272
|
+
return html.unescape(content)
|
|
272
273
|
|
|
273
274
|
return ""
|
|
274
275
|
|
|
@@ -300,7 +301,7 @@ def _extract_all_tag_blocks(
|
|
|
300
301
|
re.DOTALL | re.IGNORECASE,
|
|
301
302
|
)
|
|
302
303
|
if properly_closed:
|
|
303
|
-
return properly_closed
|
|
304
|
+
return [html.unescape(b) for b in properly_closed]
|
|
304
305
|
|
|
305
306
|
# Conservative mode: only extract properly closed blocks
|
|
306
307
|
if conservative:
|
|
@@ -326,7 +327,7 @@ def _extract_all_tag_blocks(
|
|
|
326
327
|
else:
|
|
327
328
|
content_end = len(text)
|
|
328
329
|
|
|
329
|
-
blocks.append(text[content_start:content_end])
|
|
330
|
+
blocks.append(html.unescape(text[content_start:content_end]))
|
|
330
331
|
|
|
331
332
|
return blocks
|
|
332
333
|
|
package/executor/engine.py
CHANGED
|
@@ -221,6 +221,10 @@ class ExecutionEngine:
|
|
|
221
221
|
self.work_dir = work_dir or os.getcwd()
|
|
222
222
|
self._permission_checker = permission_checker # callable(perm_name) -> bool
|
|
223
223
|
self._agent_name = agent_name
|
|
224
|
+
# [v1.23.37] 当前正在运行的子进程引用,用于 cancel_current() 终止
|
|
225
|
+
self._current_process: Optional[asyncio.subprocess.Process] = None
|
|
226
|
+
# [v1.23.38] 取消标志,用于在长等待中响应停止请求
|
|
227
|
+
self._cancelled = False
|
|
224
228
|
|
|
225
229
|
# 安全: 合并正则模式 + 字符串黑名单
|
|
226
230
|
self._blocked = set(self.DANGEROUS_COMMANDS)
|
|
@@ -506,6 +510,9 @@ class ExecutionEngine:
|
|
|
506
510
|
work_dir = work_dir or self.work_dir
|
|
507
511
|
metadata = metadata or {}
|
|
508
512
|
|
|
513
|
+
# [v1.23.38] 每次执行前重置取消标志
|
|
514
|
+
self._cancelled = False
|
|
515
|
+
|
|
509
516
|
# 权限检查: execution 权限
|
|
510
517
|
if not self._check_permission("execution"):
|
|
511
518
|
return ExecResult(
|
|
@@ -606,6 +613,7 @@ class ExecutionEngine:
|
|
|
606
613
|
cwd=work_dir,
|
|
607
614
|
env=self._get_env(env),
|
|
608
615
|
)
|
|
616
|
+
self._current_process = process # [v1.23.37] 保存引用
|
|
609
617
|
|
|
610
618
|
try:
|
|
611
619
|
# 使用流式读取代替 communicate(),确保超时后能保留已产生的输出
|
|
@@ -620,7 +628,10 @@ class ExecutionEngine:
|
|
|
620
628
|
if not chunk:
|
|
621
629
|
break
|
|
622
630
|
chunks.append(chunk)
|
|
623
|
-
except
|
|
631
|
+
except asyncio.CancelledError:
|
|
632
|
+
# [v1.23.38] 传播取消信号,不再吞掉
|
|
633
|
+
raise
|
|
634
|
+
except Exception:
|
|
624
635
|
pass
|
|
625
636
|
|
|
626
637
|
# 同时启动 stdout 和 stderr 的读取任务
|
|
@@ -633,28 +644,63 @@ class ExecutionEngine:
|
|
|
633
644
|
|
|
634
645
|
timed_out = False
|
|
635
646
|
try:
|
|
636
|
-
#
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
647
|
+
# [v1.23.38] 使用短间隔轮询,确保取消信号能及时响应
|
|
648
|
+
wait_timeout = min(timeout, 3.0) # 每3秒检查一次取消标志
|
|
649
|
+
remaining = timeout
|
|
650
|
+
while True:
|
|
651
|
+
if self._cancelled:
|
|
652
|
+
# 收到停止信号,终止进程
|
|
653
|
+
try:
|
|
654
|
+
process.kill()
|
|
655
|
+
except ProcessLookupError:
|
|
656
|
+
pass
|
|
657
|
+
for t in [stdout_task, stderr_task]:
|
|
658
|
+
if not t.done():
|
|
659
|
+
t.cancel()
|
|
660
|
+
await asyncio.gather(
|
|
661
|
+
stdout_task, stderr_task,
|
|
662
|
+
return_exceptions=True,
|
|
663
|
+
)
|
|
664
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
665
|
+
stdout_str = b"".join(stdout_chunks).decode("utf-8", errors="replace")
|
|
666
|
+
stderr_str = b"".join(stderr_chunks).decode("utf-8", errors="replace")
|
|
667
|
+
logger.info(f"[executor] 执行被用户停止 (elapsed={elapsed:.1f}s)")
|
|
668
|
+
return ExecResult(
|
|
669
|
+
success=False,
|
|
670
|
+
output=stdout_str,
|
|
671
|
+
error="执行被用户停止",
|
|
672
|
+
execution_time=elapsed,
|
|
673
|
+
metadata={"cancelled": True},
|
|
674
|
+
)
|
|
675
|
+
done, pending = await asyncio.wait(
|
|
676
|
+
[
|
|
677
|
+
asyncio.ensure_future(process.wait()),
|
|
678
|
+
stdout_task,
|
|
679
|
+
stderr_task,
|
|
680
|
+
],
|
|
681
|
+
timeout=wait_timeout,
|
|
682
|
+
)
|
|
683
|
+
if process.returncode is not None:
|
|
684
|
+
# 进程已结束
|
|
685
|
+
if pending:
|
|
686
|
+
for t in pending:
|
|
687
|
+
t.cancel()
|
|
688
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
689
|
+
break
|
|
690
|
+
remaining -= wait_timeout
|
|
691
|
+
if remaining <= 0:
|
|
692
|
+
# 超时
|
|
693
|
+
timed_out = True
|
|
694
|
+
for t in pending:
|
|
695
|
+
t.cancel()
|
|
696
|
+
try:
|
|
697
|
+
process.kill()
|
|
698
|
+
except ProcessLookupError:
|
|
699
|
+
pass
|
|
700
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
701
|
+
break
|
|
702
|
+
except asyncio.CancelledError:
|
|
703
|
+
# [v1.23.38] asyncio.Task 被取消时传播
|
|
658
704
|
timed_out = True
|
|
659
705
|
try:
|
|
660
706
|
process.kill()
|
|
@@ -667,6 +713,7 @@ class ExecutionEngine:
|
|
|
667
713
|
stdout_task, stderr_task,
|
|
668
714
|
return_exceptions=True,
|
|
669
715
|
)
|
|
716
|
+
raise
|
|
670
717
|
|
|
671
718
|
elapsed = asyncio.get_event_loop().time() - start_time
|
|
672
719
|
stdout_str = b"".join(stdout_chunks).decode("utf-8", errors="replace")
|
|
@@ -747,6 +794,7 @@ class ExecutionEngine:
|
|
|
747
794
|
cwd=work_dir,
|
|
748
795
|
env=self._get_env(env),
|
|
749
796
|
)
|
|
797
|
+
self._current_process = process # [v1.23.37] 保存引用
|
|
750
798
|
|
|
751
799
|
# 使用流式读取代替 communicate(),确保超时后能保留已产生的输出
|
|
752
800
|
stdout_chunks = []
|
|
@@ -948,6 +996,7 @@ class ExecutionEngine:
|
|
|
948
996
|
cwd=work_dir,
|
|
949
997
|
env=self._get_env(env),
|
|
950
998
|
)
|
|
999
|
+
self._current_process = process # [v1.23.37] 保存引用
|
|
951
1000
|
try:
|
|
952
1001
|
stdout, stderr = await asyncio.wait_for(
|
|
953
1002
|
process.communicate(), timeout=timeout
|
|
@@ -989,6 +1038,27 @@ class ExecutionEngine:
|
|
|
989
1038
|
metadata={"mode": "sandbox"},
|
|
990
1039
|
)
|
|
991
1040
|
|
|
1041
|
+
# [v1.23.37] ── 取消当前执行 ──
|
|
1042
|
+
|
|
1043
|
+
def cancel_current(self):
|
|
1044
|
+
"""终止当前正在运行的子进程(由 /api/chat/stop 调用)"""
|
|
1045
|
+
self._cancelled = True # [v1.23.38] 设置取消标志
|
|
1046
|
+
proc = self._current_process
|
|
1047
|
+
if proc is None:
|
|
1048
|
+
return False
|
|
1049
|
+
try:
|
|
1050
|
+
if proc.returncode is None: # 进程仍在运行
|
|
1051
|
+
proc.kill()
|
|
1052
|
+
logger.info(f"[executor] 已终止子进程 PID={proc.pid}")
|
|
1053
|
+
self._current_process = None
|
|
1054
|
+
return True
|
|
1055
|
+
except ProcessLookupError:
|
|
1056
|
+
pass # 进程已结束
|
|
1057
|
+
except Exception as e:
|
|
1058
|
+
logger.debug(f"[executor] 终止子进程失败: {e}")
|
|
1059
|
+
self._current_process = None
|
|
1060
|
+
return False
|
|
1061
|
+
|
|
992
1062
|
def set_execution_mode(self, mode: str) -> bool:
|
|
993
1063
|
"""切换执行模式。返回是否切换成功。"""
|
|
994
1064
|
if mode not in ("local", "sandbox"):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagent-ai",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.39",
|
|
4
4
|
"description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
|
|
5
5
|
"main": "main.py",
|
|
6
6
|
"bin": {
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"python": ">=3.10",
|
|
44
44
|
"node": ">=18"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
package/web/api_server.py
CHANGED
|
@@ -406,6 +406,8 @@ class ApiServer:
|
|
|
406
406
|
r.add_post("/api/chat", self.handle_chat)
|
|
407
407
|
r.add_post("/api/chat/stream", self.handle_chat_stream)
|
|
408
408
|
r.add_post("/api/chat/inject", self.handle_chat_inject)
|
|
409
|
+
# [v1.23.37] 停止执行
|
|
410
|
+
r.add_post("/api/chat/stop", self.handle_chat_stop)
|
|
409
411
|
r.add_post("/api/voice-optimize", self.handle_voice_optimize)
|
|
410
412
|
r.add_post("/api/voice-stt", self.handle_voice_stt)
|
|
411
413
|
r.add_get("/chat", self.handle_chat_page)
|
|
@@ -1911,6 +1913,10 @@ window.addEventListener('beforeunload', function() {{
|
|
|
1911
1913
|
# {session_id: {running: bool, started_at: float, result: str, done: bool, error: str}}
|
|
1912
1914
|
_running_sessions: Dict[str, Dict] = {}
|
|
1913
1915
|
_running_sessions_ttl = 300 # 过期时间(秒),用于自动清理已完成会话
|
|
1916
|
+
# [v1.23.37] 存储后台 asyncio.Task 引用,用于取消执行
|
|
1917
|
+
_running_tasks: Dict[str, asyncio.Task] = {}
|
|
1918
|
+
# [v1.23.37] 会话取消标志,供 agent 执行循环检查
|
|
1919
|
+
_session_cancelled: Dict[str, bool] = {}
|
|
1914
1920
|
|
|
1915
1921
|
async def _get_session_lock(self, session_id: str) -> asyncio.Lock:
|
|
1916
1922
|
"""获取或创建会话级锁(每个 session_id 一个锁,防并发覆盖 MainAgent 共享状态)"""
|
|
@@ -2169,6 +2175,15 @@ window.addEventListener('beforeunload', function() {{
|
|
|
2169
2175
|
result_store["done"] = True
|
|
2170
2176
|
await safe_write({"type": "done", "exec_events": exec_events_q, "session_id": session_id})
|
|
2171
2177
|
|
|
2178
|
+
except asyncio.CancelledError:
|
|
2179
|
+
# [v1.23.38] 任务被取消(用户点击停止),清理资源
|
|
2180
|
+
logger.info(f"[{session_id}] 后台流式任务被取消")
|
|
2181
|
+
result_store["done"] = True
|
|
2182
|
+
result_store["cancelled"] = True
|
|
2183
|
+
try:
|
|
2184
|
+
await safe_write({"type": "stopped", "session_id": session_id})
|
|
2185
|
+
except Exception:
|
|
2186
|
+
pass
|
|
2172
2187
|
except Exception as e:
|
|
2173
2188
|
logger.error(f"[{session_id}] 后台流式任务异常: {e}", exc_info=True)
|
|
2174
2189
|
result_store["error"] = str(e)
|
|
@@ -2210,10 +2225,15 @@ window.addEventListener('beforeunload', function() {{
|
|
|
2210
2225
|
session_lock.release()
|
|
2211
2226
|
except RuntimeError:
|
|
2212
2227
|
pass
|
|
2228
|
+
# [v1.23.37] 清理任务引用和取消标志
|
|
2229
|
+
self._running_tasks.pop(session_id, None)
|
|
2230
|
+
self._session_cancelled.pop(session_id, None)
|
|
2213
2231
|
|
|
2214
2232
|
# ── 启动后台任务(不等它完成,前端断开也不影响) ──
|
|
2215
2233
|
logger.info(f"[{session_id}] 准备创建后台任务")
|
|
2216
2234
|
bg_task = asyncio.create_task(_run_stream_task())
|
|
2235
|
+
self._running_tasks[session_id] = bg_task # [v1.23.37] 保存引用用于取消
|
|
2236
|
+
self._session_cancelled[session_id] = False # [v1.23.37] 初始化取消标志
|
|
2217
2237
|
logger.info(f"[{session_id}] 后台任务已创建: {bg_task}")
|
|
2218
2238
|
|
|
2219
2239
|
# ── SSE 事件循环:实时转发后台任务的事件到客户端 ──
|
|
@@ -2240,6 +2260,56 @@ window.addEventListener('beforeunload', function() {{
|
|
|
2240
2260
|
|
|
2241
2261
|
return response
|
|
2242
2262
|
|
|
2263
|
+
# [v1.23.37] ── 停止执行 ──
|
|
2264
|
+
|
|
2265
|
+
async def handle_chat_stop(self, request):
|
|
2266
|
+
"""POST /api/chat/stop - 停止正在执行的任务
|
|
2267
|
+
|
|
2268
|
+
停止链:前端 abort → 调用此 API → 设置取消标志 + cancel task
|
|
2269
|
+
→ agent 循环检测到取消标志 → 退出循环 → 任务结束
|
|
2270
|
+
"""
|
|
2271
|
+
try:
|
|
2272
|
+
data = await request.json()
|
|
2273
|
+
except Exception:
|
|
2274
|
+
return web.json_response({"error": "invalid JSON"}, status=400)
|
|
2275
|
+
|
|
2276
|
+
session_id = data.get("session_id", "")
|
|
2277
|
+
if not session_id:
|
|
2278
|
+
return web.json_response({"ok": False, "error": "missing session_id"})
|
|
2279
|
+
|
|
2280
|
+
logger.info(f"[{session_id}] 收到停止执行请求")
|
|
2281
|
+
|
|
2282
|
+
# 1. 设置取消标志(agent 循环会检查)
|
|
2283
|
+
self._session_cancelled[session_id] = True
|
|
2284
|
+
|
|
2285
|
+
# 1.5 设置 MainAgent 上的取消标志(agent 循环会检查 _cancelled_sessions)
|
|
2286
|
+
if self.core.main_agent:
|
|
2287
|
+
self.core.main_agent._cancelled_sessions.add(session_id)
|
|
2288
|
+
|
|
2289
|
+
# 2. 尝试取消 asyncio.Task
|
|
2290
|
+
task = self._running_tasks.get(session_id)
|
|
2291
|
+
cancelled = False
|
|
2292
|
+
if task and not task.done():
|
|
2293
|
+
task.cancel()
|
|
2294
|
+
cancelled = True
|
|
2295
|
+
logger.info(f"[{session_id}] 已发送 task.cancel()")
|
|
2296
|
+
|
|
2297
|
+
# 3. 更新会话状态
|
|
2298
|
+
sess = self._running_sessions.get(session_id)
|
|
2299
|
+
if sess and sess.get("running"):
|
|
2300
|
+
sess["running"] = False
|
|
2301
|
+
sess["done"] = True
|
|
2302
|
+
sess["cancelled"] = True
|
|
2303
|
+
|
|
2304
|
+
# 4. 尝试终止正在运行的子进程(如果 executor 正在执行命令)
|
|
2305
|
+
try:
|
|
2306
|
+
if hasattr(self.core, 'executor') and self.core.executor:
|
|
2307
|
+
self.core.executor.cancel_current()
|
|
2308
|
+
except Exception as e:
|
|
2309
|
+
logger.debug(f"[{session_id}] executor 取消失败: {e}")
|
|
2310
|
+
|
|
2311
|
+
return web.json_response({"ok": True, "cancelled": cancelled})
|
|
2312
|
+
|
|
2243
2313
|
async def handle_chat_inject(self, request):
|
|
2244
2314
|
"""POST /api/chat/inject - 注入消息到正在执行的任务,或进入排队序列"""
|
|
2245
2315
|
try:
|
|
@@ -2392,6 +2392,16 @@ async function sendMessage(opts) {
|
|
|
2392
2392
|
|
|
2393
2393
|
// ── Stop Generation ──
|
|
2394
2394
|
function stopGenerating() {
|
|
2395
|
+
// [v1.23.37] 告知后端停止执行(kill subprocess + 设置取消标志)
|
|
2396
|
+
var sid = state.activeSessionId || '';
|
|
2397
|
+
if (sid) {
|
|
2398
|
+
fetch('/api/chat/stop', {
|
|
2399
|
+
method: 'POST',
|
|
2400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2401
|
+
body: JSON.stringify({ session_id: sid }),
|
|
2402
|
+
}).catch(function() {});
|
|
2403
|
+
}
|
|
2404
|
+
// 中断前端 SSE 连接
|
|
2395
2405
|
if (state.abortController) {
|
|
2396
2406
|
state.abortController.abort();
|
|
2397
2407
|
}
|