@next-open-ai/openclawx 0.8.16 → 0.8.22

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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -126
  3. package/apps/desktop/renderer/dist/assets/index-B0_RWD2F.css +10 -0
  4. package/apps/desktop/renderer/dist/assets/index-vZN87oBP.js +89 -0
  5. package/apps/desktop/renderer/dist/index.html +2 -2
  6. package/dist/core/agent/agent-manager.d.ts +10 -4
  7. package/dist/core/agent/agent-manager.js +49 -22
  8. package/dist/core/agent/proxy/adapters/coze-adapter.js +7 -0
  9. package/dist/core/agent/proxy/adapters/local-adapter.js +4 -11
  10. package/dist/core/agent/proxy/adapters/openclawx-adapter.js +7 -0
  11. package/dist/core/agent/proxy/adapters/opencode-adapter.d.ts +11 -0
  12. package/dist/core/agent/proxy/adapters/opencode-adapter.js +716 -0
  13. package/dist/core/agent/proxy/adapters/opencode-free-models.d.ts +20 -0
  14. package/dist/core/agent/proxy/adapters/opencode-free-models.js +14 -0
  15. package/dist/core/agent/proxy/adapters/opencode-local-runner.d.ts +5 -0
  16. package/dist/core/agent/proxy/adapters/opencode-local-runner.js +86 -0
  17. package/dist/core/agent/proxy/index.js +3 -1
  18. package/dist/core/agent/proxy/run-for-channel.js +1 -1
  19. package/dist/core/agent/proxy/types.d.ts +2 -0
  20. package/dist/core/agent/run.js +1 -1
  21. package/dist/core/config/desktop-config.d.ts +71 -3
  22. package/dist/core/config/desktop-config.js +222 -24
  23. package/dist/core/memory/compaction-extension.d.ts +4 -3
  24. package/dist/core/memory/compaction-extension.js +6 -14
  25. package/dist/core/memory/embedding-types.d.ts +10 -0
  26. package/dist/core/memory/embedding-types.js +5 -0
  27. package/dist/core/memory/embedding.d.ts +2 -1
  28. package/dist/core/memory/embedding.js +38 -6
  29. package/dist/core/memory/index.js +3 -0
  30. package/dist/core/memory/local-embedding-llama.d.ts +13 -0
  31. package/dist/core/memory/local-embedding-llama.js +76 -0
  32. package/dist/core/memory/local-embedding.d.ts +10 -0
  33. package/dist/core/memory/local-embedding.js +29 -0
  34. package/dist/core/memory/persist-compaction-on-close.d.ts +14 -0
  35. package/dist/core/memory/persist-compaction-on-close.js +32 -0
  36. package/dist/core/tools/bookmark-tool.d.ts +4 -0
  37. package/dist/core/tools/bookmark-tool.js +59 -3
  38. package/dist/core/tools/index.d.ts +2 -1
  39. package/dist/core/tools/index.js +2 -1
  40. package/dist/core/tools/memory-recall-tool.d.ts +6 -0
  41. package/dist/core/tools/memory-recall-tool.js +77 -0
  42. package/dist/gateway/channel/adapters/wechat.d.ts +24 -0
  43. package/dist/gateway/channel/adapters/wechat.js +205 -0
  44. package/dist/gateway/methods/agent-cancel.d.ts +3 -1
  45. package/dist/gateway/methods/agent-cancel.js +13 -2
  46. package/dist/gateway/methods/agent-chat.js +109 -23
  47. package/dist/gateway/methods/run-scheduled-task.js +3 -7
  48. package/dist/gateway/proxy-run-abort.d.ts +6 -0
  49. package/dist/gateway/proxy-run-abort.js +39 -0
  50. package/dist/gateway/server.js +62 -7
  51. package/dist/server/agent-config/agent-config.controller.d.ts +2 -2
  52. package/dist/server/agent-config/agent-config.controller.js +8 -4
  53. package/dist/server/agent-config/agent-config.module.js +3 -1
  54. package/dist/server/agent-config/agent-config.service.d.ts +41 -6
  55. package/dist/server/agent-config/agent-config.service.js +30 -3
  56. package/dist/server/agents/agents.service.js +1 -1
  57. package/dist/server/bootstrap.js +9 -2
  58. package/dist/server/config/config.controller.d.ts +31 -2
  59. package/dist/server/config/config.controller.js +14 -0
  60. package/dist/server/config/config.module.js +2 -2
  61. package/dist/server/config/config.service.d.ts +14 -1
  62. package/dist/server/config/config.service.js +1 -0
  63. package/dist/server/workspace/workspace.service.d.ts +7 -0
  64. package/dist/server/workspace/workspace.service.js +16 -0
  65. package/package.json +6 -1
  66. package/skills/url-bookmark/SKILL.md +12 -12
  67. package/apps/desktop/renderer/dist/assets/index-DmIfN-Vc.js +0 -89
  68. package/apps/desktop/renderer/dist/assets/index-DvB8yW8I.css +0 -10
@@ -0,0 +1,205 @@
1
+ import { dispatchMessage } from "../registry.js";
2
+ let currentQrCodeBase64 = null;
3
+ let currentLoginStatus = "logged_out";
4
+ let currentUserName = null;
5
+ /** 供 Gateway API 查询当前二维码(base64 Data URL 或 null) */
6
+ export function getWechatQrCode() {
7
+ return currentQrCodeBase64;
8
+ }
9
+ /** 供 Gateway API 查询登录状态 */
10
+ export function getWechatStatus() {
11
+ return { status: currentLoginStatus, userName: currentUserName };
12
+ }
13
+ /** 重启 Wechaty 以刷新二维码(二维码过期后调用) */
14
+ export async function refreshWechatQrCode() {
15
+ if (currentLoginStatus === "logged_in")
16
+ return; // 已登录无需刷新
17
+ if (!wechatyBot)
18
+ return; // bot 不存在
19
+ console.log("[WeChat] Refreshing QR code by restarting bot...");
20
+ currentQrCodeBase64 = null;
21
+ currentLoginStatus = "logged_out";
22
+ currentUserName = null;
23
+ try {
24
+ await wechatyBot.stop();
25
+ await wechatyBot.start();
26
+ console.log("[WeChat] Bot restarted for QR refresh");
27
+ }
28
+ catch (e) {
29
+ console.error("[WeChat] Refresh failed:", e);
30
+ }
31
+ }
32
+ /* ---------- Wechaty 实例引用 ---------- */
33
+ let wechatyBot = null;
34
+ /* ---------- Inbound ---------- */
35
+ class WechatInbound {
36
+ config;
37
+ messageHandler = null;
38
+ stopped = false;
39
+ constructor(config) {
40
+ this.config = config;
41
+ }
42
+ setMessageHandler(handler) {
43
+ this.messageHandler = handler;
44
+ }
45
+ async start() {
46
+ this.stopped = false;
47
+ currentLoginStatus = "logged_out";
48
+ currentQrCodeBase64 = null;
49
+ currentUserName = null;
50
+ try {
51
+ // 动态导入 wechaty 和 qrcode(ESM)
52
+ const { WechatyBuilder } = await import("wechaty");
53
+ const QRCode = await import("qrcode");
54
+ const opts = { name: "openclawx-wechat" };
55
+ if (this.config.puppet) {
56
+ opts.puppet = this.config.puppet;
57
+ }
58
+ const builder = WechatyBuilder.build(opts);
59
+ wechatyBot = builder;
60
+ builder.on("scan", async (qrcode, status) => {
61
+ if (this.stopped)
62
+ return;
63
+ console.log(`[WeChat] Scan event, status=${status}, qrcode=${qrcode ? 'present(' + qrcode.length + ' chars)' : 'empty'}`);
64
+ // Status: 0=Unknown, 2=WaitingForScan, 3=WaitingForConfirm, 4=Cancelled, 5=Expired
65
+ if (status === 4 || status === 5) {
66
+ // QR code expired or cancelled
67
+ currentQrCodeBase64 = null;
68
+ currentLoginStatus = "logged_out";
69
+ console.log("[WeChat] QR code expired or cancelled");
70
+ return;
71
+ }
72
+ if (qrcode) {
73
+ try {
74
+ currentQrCodeBase64 = await QRCode.toDataURL(qrcode, { width: 256 });
75
+ currentLoginStatus = "scanning";
76
+ console.log("[WeChat] QR code generated successfully, waiting for scan...");
77
+ }
78
+ catch (e) {
79
+ console.error("[WeChat] QR code generation failed:", e);
80
+ }
81
+ }
82
+ else {
83
+ console.log("[WeChat] Scan event without qrcode data, status:", status);
84
+ }
85
+ });
86
+ builder.on("login", (user) => {
87
+ currentLoginStatus = "logged_in";
88
+ currentQrCodeBase64 = null;
89
+ currentUserName = user?.name?.() || user?.payload?.name || String(user);
90
+ console.log(`[WeChat] Logged in as: ${currentUserName}`);
91
+ });
92
+ builder.on("logout", (user) => {
93
+ currentLoginStatus = "logged_out";
94
+ currentQrCodeBase64 = null;
95
+ currentUserName = null;
96
+ console.log("[WeChat] Logged out:", user?.name?.() || user);
97
+ });
98
+ builder.on("message", async (message) => {
99
+ if (this.stopped)
100
+ return;
101
+ try {
102
+ const isSelf = await message.self?.();
103
+ if (isSelf)
104
+ return;
105
+ const text = (await message.text?.())?.trim?.() || message.text?.trim?.() || '';
106
+ if (!text)
107
+ return;
108
+ console.log(`[WeChat] Received: "${text.substring(0, 80)}${text.length > 80 ? '...' : ''}"`);
109
+ const talker = message.talker?.() ?? message.from?.();
110
+ const room = await message.room?.();
111
+ const talkerId = talker?.id || "unknown";
112
+ const talkerName = (await talker?.name?.()) || talker?.name || undefined;
113
+ const roomId = room?.id;
114
+ const threadId = room ? (roomId || String(await room.topic?.())) : talkerId;
115
+ const unified = {
116
+ channelId: "wechat",
117
+ threadId: String(threadId),
118
+ userId: String(talkerId),
119
+ userName: typeof talkerName === 'string' ? talkerName : undefined,
120
+ messageText: text,
121
+ replyTarget: String(threadId),
122
+ raw: { roomId, talkerId },
123
+ };
124
+ if (this.messageHandler) {
125
+ await this.messageHandler(unified);
126
+ }
127
+ else {
128
+ await dispatchMessage(unified);
129
+ }
130
+ console.log("[WeChat] message dispatched");
131
+ }
132
+ catch (e) {
133
+ console.error("[WeChat] message handler error:", e);
134
+ }
135
+ });
136
+ builder.on("error", (e) => {
137
+ console.error("[WeChat] bot error:", e?.message || e);
138
+ });
139
+ await builder.start();
140
+ console.log("[WeChat] Wechaty bot started, waiting for scan...");
141
+ }
142
+ catch (e) {
143
+ console.error("[WeChat] Failed to start Wechaty bot:", e);
144
+ currentLoginStatus = "logged_out";
145
+ }
146
+ }
147
+ async stop() {
148
+ this.stopped = true;
149
+ currentLoginStatus = "logged_out";
150
+ currentQrCodeBase64 = null;
151
+ currentUserName = null;
152
+ if (wechatyBot) {
153
+ try {
154
+ await wechatyBot.stop();
155
+ }
156
+ catch (e) {
157
+ console.warn("[WeChat] stop error:", e);
158
+ }
159
+ wechatyBot = null;
160
+ }
161
+ }
162
+ }
163
+ /* ---------- Outbound ---------- */
164
+ class WechatOutbound {
165
+ async send(targetId, reply) {
166
+ if (!wechatyBot) {
167
+ console.warn("[WeChat] bot not started, cannot send");
168
+ return;
169
+ }
170
+ const text = reply.text?.trim() || "(无内容)";
171
+ try {
172
+ // 先尝试作为 Room(群聊)发送
173
+ const room = await wechatyBot.Room?.find?.({ id: targetId });
174
+ if (room) {
175
+ await room.say(text);
176
+ return { sent: true, type: "room", id: targetId };
177
+ }
178
+ // 否则作为 Contact(私聊)发送
179
+ const contact = await wechatyBot.Contact?.find?.({ id: targetId });
180
+ if (contact) {
181
+ await contact.say(text);
182
+ return { sent: true, type: "contact", id: targetId };
183
+ }
184
+ console.warn("[WeChat] cannot find room or contact for targetId:", targetId);
185
+ return { sent: false, reason: "target not found" };
186
+ }
187
+ catch (e) {
188
+ console.error("[WeChat] send failed:", e);
189
+ throw e;
190
+ }
191
+ }
192
+ }
193
+ /* ---------- Channel 工厂 ---------- */
194
+ export function createWechatChannel(config) {
195
+ const inbound = new WechatInbound(config);
196
+ const outbound = new WechatOutbound();
197
+ inbound.setMessageHandler((msg) => dispatchMessage(msg));
198
+ return {
199
+ id: "wechat",
200
+ name: "WeChat",
201
+ defaultAgentId: config.defaultAgentId ?? "default",
202
+ getInbounds: () => [inbound],
203
+ getOutbounds: () => [outbound],
204
+ };
205
+ }
@@ -1,10 +1,12 @@
1
1
  import type { GatewayClient } from "../types.js";
2
2
  /**
3
3
  * Handle agent.cancel: abort the current turn for the given session.
4
- * Uses pi-coding-agent's session.abort() to stop the running agent and wait until idle.
4
+ * - Proxy agents: abort in-flight run via registered AbortController.
5
+ * - Local agent: uses pi-coding-agent's session.abort().
5
6
  */
6
7
  export declare function handleAgentCancel(client: GatewayClient, params: {
7
8
  sessionId?: string;
9
+ agentId?: string;
8
10
  }): Promise<{
9
11
  status: string;
10
12
  }>;
@@ -1,14 +1,25 @@
1
1
  import { agentManager } from "../../core/agent/agent-manager.js";
2
+ import { abortProxyRun } from "../proxy-run-abort.js";
3
+ const COMPOSITE_KEY_SEP = "::";
2
4
  /**
3
5
  * Handle agent.cancel: abort the current turn for the given session.
4
- * Uses pi-coding-agent's session.abort() to stop the running agent and wait until idle.
6
+ * - Proxy agents: abort in-flight run via registered AbortController.
7
+ * - Local agent: uses pi-coding-agent's session.abort().
5
8
  */
6
9
  export async function handleAgentCancel(client, params) {
7
10
  const sessionId = params?.sessionId ?? client.sessionId;
8
11
  if (!sessionId) {
9
12
  throw new Error("No session ID available");
10
13
  }
11
- const session = agentManager.getSession(sessionId);
14
+ const agentId = params?.agentId ?? client.agentId ?? "default";
15
+ if (abortProxyRun(sessionId, agentId)) {
16
+ return { status: "aborted" };
17
+ }
18
+ const compositeKey = sessionId + COMPOSITE_KEY_SEP + agentId;
19
+ let session = agentManager.getSession(compositeKey);
20
+ if (!session) {
21
+ session = agentManager.getSessionBySessionId(sessionId);
22
+ }
12
23
  if (!session) {
13
24
  return { status: "no_session" };
14
25
  }
@@ -1,10 +1,10 @@
1
1
  import { agentManager } from "../../core/agent/agent-manager.js";
2
2
  import { runForChannelStream } from "../../core/agent/proxy/index.js";
3
3
  import { getSessionCurrentAgentResolver, getSessionCurrentAgentUpdater } from "../../core/session-current-agent.js";
4
- import { getExperienceContextForUserMessage } from "../../core/memory/index.js";
5
4
  import { send, createEvent } from "../utils.js";
6
5
  import { connectedClients } from "../clients.js";
7
6
  import { getDesktopConfig, loadDesktopAgentConfig } from "../../core/config/desktop-config.js";
7
+ import { registerProxyRunAbort } from "../proxy-run-abort.js";
8
8
  const COMPOSITE_KEY_SEP = "::";
9
9
  /**
10
10
  * Broadcast message to all clients subscribed to a session
@@ -59,40 +59,55 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
59
59
  apiKey = agentConfig.apiKey;
60
60
  }
61
61
  const runnerType = agentConfig?.runnerType ?? "local";
62
- const isProxyAgent = runnerType === "coze" || runnerType === "openclawx";
62
+ const isProxyAgent = runnerType === "coze" || runnerType === "openclawx" || runnerType === "opencode";
63
63
  if (isProxyAgent) {
64
64
  console.log(`[agent.chat] Using proxy agent (${runnerType}) for session=${targetSessionId}, agentId=${currentAgentId}`);
65
65
  }
66
- // 代理智能体(Coze / OpenClawX):走 AgentProxy 统一入口,流式结果通过 WebSocket 推给客户端
66
+ // 代理智能体(Coze / OpenClawX / OpenCode):走 AgentProxy 统一入口,流式结果通过 WebSocket 推给客户端
67
67
  if (isProxyAgent) {
68
+ const { signal, unregister } = registerProxyRunAbort(targetSessionId, currentAgentId);
69
+ const finishAndUnregister = () => {
70
+ unregister();
71
+ broadcastToSession(targetSessionId, createEvent("turn_end", { sessionId: targetSessionId, content: "" }));
72
+ broadcastToSession(targetSessionId, createEvent("message_complete", { sessionId: targetSessionId, content: "" }));
73
+ broadcastToSession(targetSessionId, createEvent("agent_end", { sessionId: targetSessionId }));
74
+ broadcastToSession(targetSessionId, createEvent("conversation_end", { sessionId: targetSessionId }));
75
+ };
68
76
  try {
69
77
  await runForChannelStream({
70
78
  sessionId: targetSessionId,
71
79
  message,
72
80
  agentId: currentAgentId,
81
+ signal,
73
82
  }, {
74
83
  onChunk(delta) {
75
- broadcastToSession(targetSessionId, createEvent("agent.chunk", { text: delta }));
84
+ broadcastToSession(targetSessionId, createEvent("agent.chunk", { text: delta, sessionId: targetSessionId }));
76
85
  },
77
86
  onTurnEnd() {
78
87
  broadcastToSession(targetSessionId, createEvent("turn_end", { sessionId: targetSessionId, content: "" }));
79
88
  broadcastToSession(targetSessionId, createEvent("message_complete", { sessionId: targetSessionId, content: "" }));
80
89
  },
81
90
  onDone() {
82
- broadcastToSession(targetSessionId, createEvent("agent_end", { sessionId: targetSessionId }));
83
- broadcastToSession(targetSessionId, createEvent("conversation_end", { sessionId: targetSessionId }));
91
+ finishAndUnregister();
84
92
  },
85
93
  });
86
94
  return { status: "completed", sessionId: targetSessionId };
87
95
  }
88
96
  catch (error) {
89
- console.error(`Error in agent chat (proxy ${runnerType}):`, error);
90
- throw error;
97
+ const isAbort = error?.name === "AbortError" || (typeof error?.message === "string" && error.message.includes("abort"));
98
+ if (!isAbort)
99
+ console.error(`Error in agent chat (proxy ${runnerType}):`, error);
100
+ finishAndUnregister();
101
+ if (!isAbort) {
102
+ const errMsg = error?.message || String(error);
103
+ broadcastToSession(targetSessionId, createEvent("agent.chunk", { text: `请求失败:${errMsg}`, sessionId: targetSessionId }));
104
+ }
105
+ return { status: "completed", sessionId: targetSessionId };
91
106
  }
92
107
  }
93
108
  const isEphemeralSession = sessionType === "system" || sessionType === "scheduled";
94
109
  if (isEphemeralSession) {
95
- agentManager.deleteSession(targetSessionId + COMPOSITE_KEY_SEP + currentAgentId);
110
+ await agentManager.deleteSession(targetSessionId + COMPOSITE_KEY_SEP + currentAgentId);
96
111
  }
97
112
  const effectiveTargetAgentId = sessionType === "system" ? targetAgentId : currentAgentId;
98
113
  const { maxAgentSessions } = getDesktopConfig();
@@ -108,6 +123,7 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
108
123
  targetAgentId: effectiveTargetAgentId,
109
124
  mcpServers: agentConfig?.mcpServers,
110
125
  systemPrompt: agentConfig?.systemPrompt,
126
+ useLongMemory: agentConfig?.useLongMemory,
111
127
  });
112
128
  }
113
129
  catch (err) {
@@ -119,17 +135,37 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
119
135
  throw err;
120
136
  }
121
137
  // 向各通道广播:turn_end(本小轮结束)、agent_end(整轮对话结束),并保留 message_complete / conversation_end 兼容。各端按需处理。
122
- const unsubscribe = session.subscribe((event) => {
123
- // console.log(`Agent event received: ${event.type}`); // Reduce noise
138
+ // 必须等 agent_end 后再 resolve 并 unsubscribe,否则 sendUserMessage return 就 unsubscribe,流式 text_delta 会收不到,前端看不到回复。
139
+ let resolveAgentDone;
140
+ const agentDonePromise = new Promise((resolve) => {
141
+ resolveAgentDone = resolve;
142
+ });
143
+ let didUnsubscribe = false;
144
+ let unsubscribe;
145
+ const doUnsubscribe = () => {
146
+ if (didUnsubscribe)
147
+ return;
148
+ didUnsubscribe = true;
149
+ unsubscribe();
150
+ };
151
+ let hasReceivedAnyChunk = false;
152
+ unsubscribe = session.subscribe((event) => {
153
+ if (event.type !== "message_update") {
154
+ console.log(`[agent.chat] event: ${event.type}`);
155
+ }
124
156
  let wsMessage = null;
125
157
  if (event.type === "message_update") {
126
158
  const update = event;
127
159
  if (update.assistantMessageEvent && update.assistantMessageEvent.type === "text_delta") {
160
+ hasReceivedAnyChunk = true;
128
161
  wsMessage = createEvent("agent.chunk", { text: update.assistantMessageEvent.delta });
129
162
  }
130
163
  else if (update.assistantMessageEvent && update.assistantMessageEvent.type === "thinking_delta") {
131
164
  wsMessage = createEvent("agent.chunk", { text: update.assistantMessageEvent.delta, isThinking: true });
132
165
  }
166
+ else if (update.assistantMessageEvent?.type === "error" && update.assistantMessageEvent?.error?.errorMessage) {
167
+ console.warn("[agent.chat] model error:", update.assistantMessageEvent.error.errorMessage);
168
+ }
133
169
  }
134
170
  else if (event.type === "tool_execution_start") {
135
171
  wsMessage = createEvent("agent.tool", {
@@ -148,8 +184,48 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
148
184
  isError: event.isError
149
185
  });
150
186
  }
187
+ else if (event.type === "message_end") {
188
+ const msg = event.message;
189
+ if (msg?.role === "assistant" && msg?.content && Array.isArray(msg.content)) {
190
+ const text = msg.content
191
+ .filter((c) => c?.type === "text" && typeof c.text === "string")
192
+ .map((c) => c.text)
193
+ .join("");
194
+ if (text) {
195
+ hasReceivedAnyChunk = true;
196
+ broadcastToSession(targetSessionId, createEvent("agent.chunk", { text }));
197
+ }
198
+ }
199
+ if (msg?.errorMessage) {
200
+ console.warn("[agent.chat] message_end error:", msg.errorMessage);
201
+ const errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
202
+ ? "API 余额不足,请到「设置」检查并充值后重试。"
203
+ : `请求失败:${msg.errorMessage}`;
204
+ broadcastToSession(targetSessionId, createEvent("agent.chunk", { text: errText }));
205
+ }
206
+ wsMessage = null;
207
+ }
151
208
  else if (event.type === "turn_end") {
152
- const usage = event.message?.usage;
209
+ const msg = event.message;
210
+ if (!hasReceivedAnyChunk && msg?.content && Array.isArray(msg.content)) {
211
+ const fullText = msg.content
212
+ .filter((c) => c?.type === "text" && typeof c.text === "string")
213
+ .map((c) => c.text)
214
+ .join("");
215
+ if (fullText) {
216
+ broadcastToSession(targetSessionId, createEvent("agent.chunk", { text: fullText }));
217
+ hasReceivedAnyChunk = true;
218
+ }
219
+ }
220
+ if (msg?.errorMessage) {
221
+ console.warn("[agent.chat] turn message error:", msg.errorMessage);
222
+ const errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
223
+ ? "API 余额不足,请到「设置」检查并充值后重试。"
224
+ : `请求失败:${msg.errorMessage}`;
225
+ broadcastToSession(targetSessionId, createEvent("agent.chunk", { text: errText }));
226
+ hasReceivedAnyChunk = true;
227
+ }
228
+ const usage = msg?.usage;
153
229
  const promptTokens = Number(usage?.input ?? usage?.input_tokens ?? 0) || 0;
154
230
  const completionTokens = Number(usage?.output ?? usage?.output_tokens ?? 0) || 0;
155
231
  const usagePayload = promptTokens > 0 || completionTokens > 0
@@ -165,25 +241,36 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
165
241
  wsMessage = null;
166
242
  }
167
243
  else if (event.type === "agent_end") {
244
+ if (!hasReceivedAnyChunk && Array.isArray(event.messages)) {
245
+ const messages = event.messages;
246
+ const lastAssistant = [...messages].reverse().find((m) => m?.role === "assistant" && m?.content);
247
+ if (lastAssistant?.content) {
248
+ const text = lastAssistant.content
249
+ .filter((c) => c?.type === "text" && typeof c.text === "string")
250
+ .map((c) => c.text)
251
+ .join("");
252
+ if (text)
253
+ broadcastToSession(targetSessionId, createEvent("agent.chunk", { text }));
254
+ }
255
+ }
168
256
  const agentPayload = { sessionId: targetSessionId };
169
257
  broadcastToSession(targetSessionId, createEvent("agent_end", agentPayload));
170
258
  broadcastToSession(targetSessionId, createEvent("conversation_end", agentPayload));
171
259
  wsMessage = null;
260
+ resolveAgentDone();
261
+ doUnsubscribe();
262
+ if (isEphemeralSession) {
263
+ void agentManager.deleteSession(targetSessionId + COMPOSITE_KEY_SEP + currentAgentId).catch(() => { });
264
+ }
172
265
  }
173
266
  if (wsMessage) {
174
267
  broadcastToSession(targetSessionId, wsMessage);
175
268
  }
176
- if (event.type === "agent_end" && isEphemeralSession) {
177
- agentManager.deleteSession(targetSessionId + COMPOSITE_KEY_SEP + currentAgentId);
178
- }
179
269
  });
180
270
  try {
181
- const experienceBlock = await getExperienceContextForUserMessage();
182
- const userMessageToSend = experienceBlock.trim().length > 0
183
- ? `${experienceBlock}\n\n用户问题:\n${message}`
184
- : message;
185
271
  // 若 agent 正在流式输出,deliverAs: 'followUp' 将本条消息排队,避免抛出 "Agent is already processing"
186
- await session.sendUserMessage(userMessageToSend, { deliverAs: "followUp" });
272
+ await session.sendUserMessage(message, { deliverAs: "followUp" });
273
+ await agentDonePromise;
187
274
  console.log(`Agent chat completed for session ${targetSessionId}`);
188
275
  return {
189
276
  status: "completed",
@@ -192,9 +279,8 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
192
279
  }
193
280
  catch (error) {
194
281
  console.error(`Error in agent chat:`, error);
282
+ resolveAgentDone();
283
+ doUnsubscribe();
195
284
  throw error;
196
285
  }
197
- finally {
198
- unsubscribe();
199
- }
200
286
  }
@@ -1,5 +1,4 @@
1
1
  import { agentManager } from "../../core/agent/agent-manager.js";
2
- import { getExperienceContextForUserMessage } from "../../core/memory/index.js";
3
2
  import { loadDesktopAgentConfig } from "../../core/config/desktop-config.js";
4
3
  async function readBody(req) {
5
4
  return new Promise((resolve, reject) => {
@@ -59,6 +58,7 @@ export async function handleRunScheduledTask(req, res) {
59
58
  apiKey,
60
59
  mcpServers: agentConfig?.mcpServers,
61
60
  systemPrompt: agentConfig?.systemPrompt,
61
+ useLongMemory: agentConfig?.useLongMemory,
62
62
  });
63
63
  let assistantContent = "";
64
64
  let turnPromptTokens = 0;
@@ -78,10 +78,6 @@ export async function handleRunScheduledTask(req, res) {
78
78
  }
79
79
  }
80
80
  });
81
- const experienceBlock = await getExperienceContextForUserMessage();
82
- const userMessageToSend = experienceBlock.trim().length > 0
83
- ? `${experienceBlock}\n\n用户问题:\n${message}`
84
- : message;
85
81
  // 定时任务复用同一 session:若上次执行未结束会报 "Agent is already processing"。先等待空闲再发,避免并发。
86
82
  const idleTimeoutMs = 10 * 60 * 1000;
87
83
  const pollMs = 2000;
@@ -93,7 +89,7 @@ export async function handleRunScheduledTask(req, res) {
93
89
  if (session.isStreaming) {
94
90
  throw new Error("Session still busy after waiting; try again later.");
95
91
  }
96
- await session.sendUserMessage(userMessageToSend);
92
+ await session.sendUserMessage(message);
97
93
  unsubscribe();
98
94
  if (backendBaseUrl && assistantContent !== undefined) {
99
95
  const url = `${backendBaseUrl.replace(/\/$/, "")}/server-api/agents/sessions/${encodeURIComponent(sessionId)}/messages`;
@@ -124,6 +120,6 @@ export async function handleRunScheduledTask(req, res) {
124
120
  res.end(JSON.stringify({ success: false, error: friendlyError }));
125
121
  }
126
122
  finally {
127
- agentManager.deleteSession(sessionId + COMPOSITE_KEY_SEP + sessionAgentId);
123
+ await agentManager.deleteSession(sessionId + COMPOSITE_KEY_SEP + sessionAgentId);
128
124
  }
129
125
  }
@@ -0,0 +1,6 @@
1
+ export declare function registerProxyRunAbort(sessionId: string, agentId: string): {
2
+ signal: AbortSignal;
3
+ unregister: () => void;
4
+ };
5
+ /** Abort the proxy run for this session+agent if any; returns true if aborted. */
6
+ export declare function abortProxyRun(sessionId: string, agentId: string): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Registry of AbortControllers for in-flight proxy runs (Coze/OpenClawX/OpenCode).
3
+ * agent.cancel looks up by sessionId+agentId and aborts the controller so the run stops.
4
+ */
5
+ const SEP = "::";
6
+ function key(sessionId, agentId) {
7
+ return sessionId + SEP + agentId;
8
+ }
9
+ const controllers = new Map();
10
+ export function registerProxyRunAbort(sessionId, agentId) {
11
+ const k = key(sessionId, agentId);
12
+ const existing = controllers.get(k);
13
+ if (existing) {
14
+ existing.abort();
15
+ controllers.delete(k);
16
+ }
17
+ const controller = new AbortController();
18
+ controllers.set(k, controller);
19
+ return {
20
+ signal: controller.signal,
21
+ unregister: () => {
22
+ controllers.delete(k);
23
+ },
24
+ };
25
+ }
26
+ /** Abort the proxy run for this session+agent if any; returns true if aborted. */
27
+ export function abortProxyRun(sessionId, agentId) {
28
+ const k = key(sessionId, agentId);
29
+ const c = controllers.get(k);
30
+ if (!c)
31
+ return false;
32
+ try {
33
+ c.abort();
34
+ }
35
+ finally {
36
+ controllers.delete(k);
37
+ }
38
+ return true;
39
+ }