@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.
- package/LICENSE +21 -0
- package/README.md +89 -126
- package/apps/desktop/renderer/dist/assets/index-B0_RWD2F.css +10 -0
- package/apps/desktop/renderer/dist/assets/index-vZN87oBP.js +89 -0
- package/apps/desktop/renderer/dist/index.html +2 -2
- package/dist/core/agent/agent-manager.d.ts +10 -4
- package/dist/core/agent/agent-manager.js +49 -22
- package/dist/core/agent/proxy/adapters/coze-adapter.js +7 -0
- package/dist/core/agent/proxy/adapters/local-adapter.js +4 -11
- package/dist/core/agent/proxy/adapters/openclawx-adapter.js +7 -0
- package/dist/core/agent/proxy/adapters/opencode-adapter.d.ts +11 -0
- package/dist/core/agent/proxy/adapters/opencode-adapter.js +716 -0
- package/dist/core/agent/proxy/adapters/opencode-free-models.d.ts +20 -0
- package/dist/core/agent/proxy/adapters/opencode-free-models.js +14 -0
- package/dist/core/agent/proxy/adapters/opencode-local-runner.d.ts +5 -0
- package/dist/core/agent/proxy/adapters/opencode-local-runner.js +86 -0
- package/dist/core/agent/proxy/index.js +3 -1
- package/dist/core/agent/proxy/run-for-channel.js +1 -1
- package/dist/core/agent/proxy/types.d.ts +2 -0
- package/dist/core/agent/run.js +1 -1
- package/dist/core/config/desktop-config.d.ts +71 -3
- package/dist/core/config/desktop-config.js +222 -24
- package/dist/core/memory/compaction-extension.d.ts +4 -3
- package/dist/core/memory/compaction-extension.js +6 -14
- package/dist/core/memory/embedding-types.d.ts +10 -0
- package/dist/core/memory/embedding-types.js +5 -0
- package/dist/core/memory/embedding.d.ts +2 -1
- package/dist/core/memory/embedding.js +38 -6
- package/dist/core/memory/index.js +3 -0
- package/dist/core/memory/local-embedding-llama.d.ts +13 -0
- package/dist/core/memory/local-embedding-llama.js +76 -0
- package/dist/core/memory/local-embedding.d.ts +10 -0
- package/dist/core/memory/local-embedding.js +29 -0
- package/dist/core/memory/persist-compaction-on-close.d.ts +14 -0
- package/dist/core/memory/persist-compaction-on-close.js +32 -0
- package/dist/core/tools/bookmark-tool.d.ts +4 -0
- package/dist/core/tools/bookmark-tool.js +59 -3
- package/dist/core/tools/index.d.ts +2 -1
- package/dist/core/tools/index.js +2 -1
- package/dist/core/tools/memory-recall-tool.d.ts +6 -0
- package/dist/core/tools/memory-recall-tool.js +77 -0
- package/dist/gateway/channel/adapters/wechat.d.ts +24 -0
- package/dist/gateway/channel/adapters/wechat.js +205 -0
- package/dist/gateway/methods/agent-cancel.d.ts +3 -1
- package/dist/gateway/methods/agent-cancel.js +13 -2
- package/dist/gateway/methods/agent-chat.js +109 -23
- package/dist/gateway/methods/run-scheduled-task.js +3 -7
- package/dist/gateway/proxy-run-abort.d.ts +6 -0
- package/dist/gateway/proxy-run-abort.js +39 -0
- package/dist/gateway/server.js +62 -7
- package/dist/server/agent-config/agent-config.controller.d.ts +2 -2
- package/dist/server/agent-config/agent-config.controller.js +8 -4
- package/dist/server/agent-config/agent-config.module.js +3 -1
- package/dist/server/agent-config/agent-config.service.d.ts +41 -6
- package/dist/server/agent-config/agent-config.service.js +30 -3
- package/dist/server/agents/agents.service.js +1 -1
- package/dist/server/bootstrap.js +9 -2
- package/dist/server/config/config.controller.d.ts +31 -2
- package/dist/server/config/config.controller.js +14 -0
- package/dist/server/config/config.module.js +2 -2
- package/dist/server/config/config.service.d.ts +14 -1
- package/dist/server/config/config.service.js +1 -0
- package/dist/server/workspace/workspace.service.d.ts +7 -0
- package/dist/server/workspace/workspace.service.js +16 -0
- package/package.json +6 -1
- package/skills/url-bookmark/SKILL.md +12 -12
- package/apps/desktop/renderer/dist/assets/index-DmIfN-Vc.js +0 -89
- 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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
+
}
|