@ynhcj/xiaoyi-channel 0.0.132-beta → 0.0.133-beta
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/dist/index.js +4 -3
- package/dist/src/bot.js +54 -57
- package/dist/src/client.d.ts +1 -5
- package/dist/src/client.js +25 -39
- package/dist/src/file-download.js +3 -3
- package/dist/src/formatter.d.ts +0 -2
- package/dist/src/formatter.js +12 -14
- package/dist/src/heartbeat.js +3 -2
- package/dist/src/login-token-handler.js +7 -8
- package/dist/src/message-queue.js +2 -1
- package/dist/src/monitor.js +42 -46
- package/dist/src/outbound.js +3 -0
- package/dist/src/provider.js +15 -14
- package/dist/src/push.js +9 -9
- package/dist/src/reply-dispatcher.js +43 -53
- package/dist/src/self-evolution-handler.js +11 -14
- package/dist/src/tools/send-file-to-user-tool.js +0 -1
- package/dist/src/tools/session-manager.js +0 -12
- package/dist/src/trigger-handler.js +8 -9
- package/dist/src/utils/logger.js +106 -22
- package/dist/src/utils/throw.d.ts +5 -0
- package/dist/src/utils/throw.js +10 -0
- package/dist/src/websocket.js +4 -2
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { callCsplApi } from "./src/cspl/call-api.js";
|
|
|
5
5
|
import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
|
|
6
6
|
import { extractResultText, parseSecurityResult, processText, validateAndTruncateText, } from "./src/cspl/utils.js";
|
|
7
7
|
import { setXYRuntime } from "./src/runtime.js";
|
|
8
|
+
import { logger } from "./src/utils/logger.js";
|
|
8
9
|
import { tryInjectSteer } from "./src/steer-injector.js";
|
|
9
10
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
10
11
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
@@ -26,7 +27,7 @@ function registerFullHooks(api) {
|
|
|
26
27
|
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
logger.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
30
31
|
try {
|
|
31
32
|
const resultText = extractResultText(event, event.toolName);
|
|
32
33
|
const resultLength = resultText.length;
|
|
@@ -49,13 +50,13 @@ function registerFullHooks(api) {
|
|
|
49
50
|
}
|
|
50
51
|
const response = await callCsplApi(finalJson, api.config);
|
|
51
52
|
const result = parseSecurityResult(response);
|
|
52
|
-
|
|
53
|
+
logger.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
|
|
53
54
|
if (result.status === "REJECT") {
|
|
54
55
|
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
|
-
|
|
59
|
+
logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
|
|
59
60
|
}
|
|
60
61
|
});
|
|
61
62
|
}
|
package/dist/src/bot.js
CHANGED
|
@@ -14,6 +14,7 @@ import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
|
14
14
|
import { saveRuntimeInfo } from "./utils/runtime-manager.js";
|
|
15
15
|
import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
|
|
16
16
|
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
17
|
+
import { logger } from "./utils/logger.js";
|
|
17
18
|
/**
|
|
18
19
|
* Handle an incoming A2A message.
|
|
19
20
|
* This is the main entry point for message processing.
|
|
@@ -21,8 +22,6 @@ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActive
|
|
|
21
22
|
*/
|
|
22
23
|
export async function handleXYMessage(params) {
|
|
23
24
|
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
24
|
-
const log = runtime?.log ?? console.log;
|
|
25
|
-
const error = runtime?.error ?? console.error;
|
|
26
25
|
// 每次收到消息时更新缓存,供 steer 注入使用
|
|
27
26
|
setCachedContext(cfg, runtime, accountId);
|
|
28
27
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
@@ -36,7 +35,7 @@ export async function handleXYMessage(params) {
|
|
|
36
35
|
if (!sessionId) {
|
|
37
36
|
throw new Error("clearContext request missing sessionId in params");
|
|
38
37
|
}
|
|
39
|
-
log(`Clear context request for session ${sessionId}`);
|
|
38
|
+
logger.log(`Clear context request for session ${sessionId}`);
|
|
40
39
|
const config = resolveXYConfig(cfg);
|
|
41
40
|
await sendClearContextResponse({
|
|
42
41
|
config,
|
|
@@ -52,7 +51,7 @@ export async function handleXYMessage(params) {
|
|
|
52
51
|
if (!sessionId) {
|
|
53
52
|
throw new Error("tasks/cancel request missing sessionId in params");
|
|
54
53
|
}
|
|
55
|
-
log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
|
|
54
|
+
logger.log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
|
|
56
55
|
const config = resolveXYConfig(cfg);
|
|
57
56
|
await sendTasksCancelResponse({
|
|
58
57
|
config,
|
|
@@ -68,18 +67,18 @@ export async function handleXYMessage(params) {
|
|
|
68
67
|
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
69
68
|
const triggerData = extractTriggerData(parsed.parts);
|
|
70
69
|
if (triggerData) {
|
|
71
|
-
log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
|
|
72
|
-
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
73
|
-
log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
70
|
+
logger.log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
|
|
71
|
+
logger.log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
72
|
+
logger.log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
74
73
|
try {
|
|
75
74
|
// 读取 pushData
|
|
76
75
|
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
77
76
|
if (!pushDataItem) {
|
|
78
|
-
error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
77
|
+
logger.error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
79
78
|
return;
|
|
80
79
|
}
|
|
81
|
-
log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
82
|
-
log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
80
|
+
logger.log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
81
|
+
logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
83
82
|
const config = resolveXYConfig(cfg);
|
|
84
83
|
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
85
84
|
await sendA2AResponse({
|
|
@@ -90,13 +89,12 @@ export async function handleXYMessage(params) {
|
|
|
90
89
|
text: pushDataItem.dataDetail,
|
|
91
90
|
append: false,
|
|
92
91
|
final: true,
|
|
93
|
-
runtime,
|
|
94
92
|
});
|
|
95
|
-
log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
|
|
93
|
+
logger.log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
|
|
96
94
|
return; // 提前返回,不继续处理
|
|
97
95
|
}
|
|
98
96
|
catch (err) {
|
|
99
|
-
error(`[BOT] ❌ Failed to handle Trigger message:`, err);
|
|
97
|
+
logger.error(`[BOT] ❌ Failed to handle Trigger message:`, err);
|
|
100
98
|
return;
|
|
101
99
|
}
|
|
102
100
|
}
|
|
@@ -105,9 +103,9 @@ export async function handleXYMessage(params) {
|
|
|
105
103
|
const isSteerMode = cfg.messages?.queue?.mode === "steer";
|
|
106
104
|
const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
|
|
107
105
|
if (isSecondMessage) {
|
|
108
|
-
log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
|
|
109
|
-
log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
110
|
-
log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
|
|
106
|
+
logger.log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
|
|
107
|
+
logger.log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
108
|
+
logger.log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
|
|
111
109
|
}
|
|
112
110
|
// 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
|
|
113
111
|
const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
|
|
@@ -115,32 +113,32 @@ export async function handleXYMessage(params) {
|
|
|
115
113
|
// 🔑 如果是第一条消息,锁定taskId防止被过早清理
|
|
116
114
|
if (!isUpdate) {
|
|
117
115
|
lockTaskId(parsed.sessionId);
|
|
118
|
-
log(`[BOT] 🔒 Locked taskId for first message`);
|
|
116
|
+
logger.log(`[BOT] 🔒 Locked taskId for first message`);
|
|
119
117
|
}
|
|
120
118
|
// Extract and update push_id if present
|
|
121
119
|
const pushId = extractPushId(parsed.parts);
|
|
122
120
|
if (pushId) {
|
|
123
|
-
log(`[BOT] 📌 Extracted push_id from user message`);
|
|
121
|
+
logger.log(`[BOT] 📌 Extracted push_id from user message`);
|
|
124
122
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
125
123
|
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
126
124
|
addPushId(pushId).catch((err) => {
|
|
127
|
-
error(`[BOT] Failed to persist pushId:`, err);
|
|
125
|
+
logger.error(`[BOT] Failed to persist pushId:`, err);
|
|
128
126
|
});
|
|
129
127
|
}
|
|
130
128
|
else {
|
|
131
|
-
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
129
|
+
logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
132
130
|
}
|
|
133
131
|
// Extract deviceType if present (same level as push_id in systemVariables)
|
|
134
132
|
const deviceType = extractDeviceType(parsed.parts);
|
|
135
133
|
if (deviceType) {
|
|
136
|
-
log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
|
|
134
|
+
logger.log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
|
|
137
135
|
}
|
|
138
136
|
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
139
137
|
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
140
138
|
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
141
139
|
parsed.taskId // TASK_ID (param.id)
|
|
142
140
|
).catch((err) => {
|
|
143
|
-
error(`[BOT] Failed to save runtime info:`, err);
|
|
141
|
+
logger.error(`[BOT] Failed to save runtime info:`, err);
|
|
144
142
|
});
|
|
145
143
|
// Resolve configuration (needed for status updates)
|
|
146
144
|
const config = resolveXYConfig(cfg);
|
|
@@ -156,7 +154,7 @@ export async function handleXYMessage(params) {
|
|
|
156
154
|
id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
|
|
157
155
|
},
|
|
158
156
|
});
|
|
159
|
-
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
157
|
+
logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
160
158
|
registerSession(route.sessionKey, {
|
|
161
159
|
config,
|
|
162
160
|
sessionId: parsed.sessionId,
|
|
@@ -166,7 +164,7 @@ export async function handleXYMessage(params) {
|
|
|
166
164
|
deviceType,
|
|
167
165
|
});
|
|
168
166
|
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
169
|
-
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
167
|
+
logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
170
168
|
void sendStatusUpdate({
|
|
171
169
|
config,
|
|
172
170
|
sessionId: parsed.sessionId,
|
|
@@ -174,9 +172,8 @@ export async function handleXYMessage(params) {
|
|
|
174
172
|
messageId: parsed.messageId,
|
|
175
173
|
text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍候~",
|
|
176
174
|
state: "working",
|
|
177
|
-
runtime,
|
|
178
175
|
}).catch((err) => {
|
|
179
|
-
error(`Failed to send initial status update:`, err);
|
|
176
|
+
logger.error(`Failed to send initial status update:`, err);
|
|
180
177
|
});
|
|
181
178
|
// Extract text and files from parts
|
|
182
179
|
const text = extractTextFromParts(parsed.parts);
|
|
@@ -186,24 +183,24 @@ export async function handleXYMessage(params) {
|
|
|
186
183
|
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
187
184
|
if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
|
|
188
185
|
const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
|
|
189
|
-
log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
|
|
186
|
+
logger.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
|
|
190
187
|
if (shouldNudge) {
|
|
191
188
|
const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
|
|
192
189
|
textForAgent = augmented.text;
|
|
193
190
|
if (augmented.appended) {
|
|
194
|
-
log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
|
|
191
|
+
logger.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
|
|
195
192
|
}
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
}
|
|
199
196
|
catch (selfEvolutionError) {
|
|
200
|
-
error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
197
|
+
logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
201
198
|
}
|
|
202
199
|
}
|
|
203
200
|
const fileParts = extractFileParts(parsed.parts);
|
|
204
201
|
// Download files to local disk
|
|
205
202
|
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
206
|
-
log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
|
|
203
|
+
logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
|
|
207
204
|
const mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
208
205
|
// Resolve envelope format options (following feishu pattern)
|
|
209
206
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
@@ -246,8 +243,8 @@ export async function handleXYMessage(params) {
|
|
|
246
243
|
...mediaPayload,
|
|
247
244
|
});
|
|
248
245
|
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
249
|
-
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
250
|
-
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
246
|
+
logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
247
|
+
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
251
248
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
252
249
|
cfg,
|
|
253
250
|
runtime,
|
|
@@ -261,7 +258,7 @@ export async function handleXYMessage(params) {
|
|
|
261
258
|
// 第二条消息会很快返回,不需要定时器
|
|
262
259
|
if (!isSecondMessage) {
|
|
263
260
|
startStatusInterval();
|
|
264
|
-
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
261
|
+
logger.log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
265
262
|
}
|
|
266
263
|
// Build session context for AsyncLocalStorage
|
|
267
264
|
const sessionContext = {
|
|
@@ -272,22 +269,22 @@ export async function handleXYMessage(params) {
|
|
|
272
269
|
agentId: route.accountId,
|
|
273
270
|
deviceType,
|
|
274
271
|
};
|
|
275
|
-
log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
272
|
+
logger.log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
276
273
|
await core.channel.reply.withReplyDispatcher({
|
|
277
274
|
dispatcher,
|
|
278
275
|
onSettled: () => {
|
|
279
|
-
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
280
|
-
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
276
|
+
logger.log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
277
|
+
logger.log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
281
278
|
// 🔑 减少引用计数
|
|
282
279
|
decrementTaskIdRef(parsed.sessionId);
|
|
283
280
|
// 🔑 如果是第一条消息完成,解锁
|
|
284
281
|
if (!isSecondMessage) {
|
|
285
282
|
unlockTaskId(parsed.sessionId);
|
|
286
|
-
log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
|
|
283
|
+
logger.log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
|
|
287
284
|
}
|
|
288
285
|
// 减少session引用计数
|
|
289
286
|
unregisterSession(route.sessionKey);
|
|
290
|
-
log(`[BOT] ✅ Cleanup completed`);
|
|
287
|
+
logger.log(`[BOT] ✅ Cleanup completed`);
|
|
291
288
|
},
|
|
292
289
|
run: () => {
|
|
293
290
|
// 🔐 Use AsyncLocalStorage to provide session context to tools.
|
|
@@ -296,12 +293,12 @@ export async function handleXYMessage(params) {
|
|
|
296
293
|
// signal init complete to release the global dispatch gate
|
|
297
294
|
// for the next session.
|
|
298
295
|
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
299
|
-
log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
|
|
300
|
-
log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
301
|
-
log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
302
|
-
log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
303
|
-
log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
304
|
-
log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
296
|
+
logger.log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
|
|
297
|
+
logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
298
|
+
logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
299
|
+
logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
300
|
+
logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
301
|
+
logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
305
302
|
try {
|
|
306
303
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
307
304
|
ctx: ctxPayload,
|
|
@@ -309,15 +306,15 @@ export async function handleXYMessage(params) {
|
|
|
309
306
|
dispatcher,
|
|
310
307
|
replyOptions,
|
|
311
308
|
});
|
|
312
|
-
log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
|
|
313
|
-
log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
309
|
+
logger.log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
|
|
310
|
+
logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
314
311
|
return result;
|
|
315
312
|
}
|
|
316
313
|
catch (dispatchErr) {
|
|
317
|
-
error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
|
|
318
|
-
error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
319
|
-
error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
320
|
-
error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
314
|
+
logger.error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
|
|
315
|
+
logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
316
|
+
logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
317
|
+
logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
321
318
|
throw dispatchErr;
|
|
322
319
|
}
|
|
323
320
|
});
|
|
@@ -326,20 +323,20 @@ export async function handleXYMessage(params) {
|
|
|
326
323
|
return dispatchPromise;
|
|
327
324
|
},
|
|
328
325
|
});
|
|
329
|
-
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
330
|
-
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
326
|
+
logger.log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
327
|
+
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
331
328
|
}
|
|
332
329
|
catch (err) {
|
|
333
330
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
334
|
-
error("Failed to handle XY message:", err);
|
|
331
|
+
logger.error("Failed to handle XY message:", err);
|
|
335
332
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
336
|
-
log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
333
|
+
logger.log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
337
334
|
// 🔑 错误时也要清理taskId和session
|
|
338
335
|
try {
|
|
339
336
|
const params = message.params;
|
|
340
337
|
const sessionId = params?.sessionId;
|
|
341
338
|
if (sessionId) {
|
|
342
|
-
log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
|
|
339
|
+
logger.log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
|
|
343
340
|
// 清理 taskId
|
|
344
341
|
decrementTaskIdRef(sessionId);
|
|
345
342
|
unlockTaskId(sessionId);
|
|
@@ -355,11 +352,11 @@ export async function handleXYMessage(params) {
|
|
|
355
352
|
},
|
|
356
353
|
});
|
|
357
354
|
unregisterSession(route.sessionKey);
|
|
358
|
-
log(`[BOT] ✅ Cleanup completed after error`);
|
|
355
|
+
logger.log(`[BOT] ✅ Cleanup completed after error`);
|
|
359
356
|
}
|
|
360
357
|
}
|
|
361
358
|
catch (cleanupErr) {
|
|
362
|
-
log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
|
|
359
|
+
logger.log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
|
|
363
360
|
// Ignore cleanup errors
|
|
364
361
|
}
|
|
365
362
|
// ❌ Don't re-throw: message processing error should not affect gateway stability
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { XYWebSocketManager } from "./websocket.js";
|
|
2
2
|
import type { XYChannelConfig } from "./types.js";
|
|
3
3
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
4
|
-
/**
|
|
5
|
-
* Set the runtime for logging in client module.
|
|
6
|
-
*/
|
|
7
|
-
export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
|
|
8
4
|
/**
|
|
9
5
|
* Get or create a WebSocket manager for the given configuration.
|
|
10
6
|
* Reuses existing managers if config matches.
|
|
11
7
|
*/
|
|
12
|
-
export declare function getXYWebSocketManager(config: XYChannelConfig): XYWebSocketManager;
|
|
8
|
+
export declare function getXYWebSocketManager(config: XYChannelConfig, runtime?: RuntimeEnv): XYWebSocketManager;
|
|
13
9
|
/**
|
|
14
10
|
* Remove a specific WebSocket manager from cache.
|
|
15
11
|
* Disconnects the manager and removes it from the cache.
|
package/dist/src/client.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
// WebSocket client cache management
|
|
2
2
|
// Follows feishu/client.ts pattern for caching client instances
|
|
3
3
|
import { XYWebSocketManager } from "./websocket.js";
|
|
4
|
-
|
|
5
|
-
let runtime;
|
|
6
|
-
/**
|
|
7
|
-
* Set the runtime for logging in client module.
|
|
8
|
-
*/
|
|
9
|
-
export function setClientRuntime(rt) {
|
|
10
|
-
runtime = rt;
|
|
11
|
-
}
|
|
4
|
+
import { logger } from "./utils/logger.js";
|
|
12
5
|
/**
|
|
13
6
|
* Global cache for WebSocket managers.
|
|
14
7
|
* Key format: `${apiKey}-${agentId}`
|
|
@@ -24,19 +17,17 @@ const wsManagerCache = _g.__xyWsManagerCache;
|
|
|
24
17
|
* Get or create a WebSocket manager for the given configuration.
|
|
25
18
|
* Reuses existing managers if config matches.
|
|
26
19
|
*/
|
|
27
|
-
export function getXYWebSocketManager(config) {
|
|
20
|
+
export function getXYWebSocketManager(config, runtime) {
|
|
28
21
|
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
29
22
|
let cached = wsManagerCache.get(cacheKey);
|
|
30
23
|
if (cached && cached.isConfigMatch(config)) {
|
|
31
|
-
const log = runtime?.log ?? console.log;
|
|
32
24
|
return cached;
|
|
33
25
|
}
|
|
34
26
|
// Create new manager
|
|
35
|
-
|
|
36
|
-
log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
|
|
27
|
+
logger.log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
|
|
37
28
|
cached = new XYWebSocketManager(config, runtime);
|
|
38
29
|
wsManagerCache.set(cacheKey, cached);
|
|
39
|
-
log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
|
|
30
|
+
logger.log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
|
|
40
31
|
return cached;
|
|
41
32
|
}
|
|
42
33
|
/**
|
|
@@ -44,25 +35,23 @@ export function getXYWebSocketManager(config) {
|
|
|
44
35
|
* Disconnects the manager and removes it from the cache.
|
|
45
36
|
*/
|
|
46
37
|
export function removeXYWebSocketManager(config) {
|
|
47
|
-
const log = runtime?.log ?? console.log;
|
|
48
38
|
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
49
39
|
const manager = wsManagerCache.get(cacheKey);
|
|
50
40
|
if (manager) {
|
|
51
|
-
log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
41
|
+
logger.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
52
42
|
manager.disconnect();
|
|
53
43
|
wsManagerCache.delete(cacheKey);
|
|
54
|
-
log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
44
|
+
logger.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
55
45
|
}
|
|
56
46
|
else {
|
|
57
|
-
log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
47
|
+
logger.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
58
48
|
}
|
|
59
49
|
}
|
|
60
50
|
/**
|
|
61
51
|
* Clear all cached WebSocket managers.
|
|
62
52
|
*/
|
|
63
53
|
export function clearXYWebSocketManagers() {
|
|
64
|
-
|
|
65
|
-
log("Clearing all WebSocket manager caches");
|
|
54
|
+
logger.log("Clearing all WebSocket manager caches");
|
|
66
55
|
for (const manager of wsManagerCache.values()) {
|
|
67
56
|
manager.disconnect();
|
|
68
57
|
}
|
|
@@ -79,37 +68,35 @@ export function getCachedManagerCount() {
|
|
|
79
68
|
* Helps identify connection issues and orphan connections.
|
|
80
69
|
*/
|
|
81
70
|
export function diagnoseAllManagers() {
|
|
82
|
-
|
|
83
|
-
log(`Total cached managers: ${wsManagerCache.size}`);
|
|
71
|
+
logger.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
84
72
|
if (wsManagerCache.size === 0) {
|
|
85
|
-
log("ℹ️ No managers in cache");
|
|
73
|
+
logger.log("ℹ️ No managers in cache");
|
|
86
74
|
return;
|
|
87
75
|
}
|
|
88
76
|
let orphanCount = 0;
|
|
89
77
|
wsManagerCache.forEach((manager, key) => {
|
|
90
78
|
const diag = manager.getConnectionDiagnostics();
|
|
91
|
-
log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
79
|
+
logger.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
92
80
|
// Connection
|
|
93
|
-
log(` 🔌 Connection:`);
|
|
94
|
-
log(` - Exists: ${diag.connection.exists}`);
|
|
95
|
-
log(` - ReadyState: ${diag.connection.readyState}`);
|
|
96
|
-
log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
|
|
97
|
-
log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
|
|
98
|
-
log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
|
|
99
|
-
log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
|
|
100
|
-
log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
|
|
81
|
+
logger.log(` 🔌 Connection:`);
|
|
82
|
+
logger.log(` - Exists: ${diag.connection.exists}`);
|
|
83
|
+
logger.log(` - ReadyState: ${diag.connection.readyState}`);
|
|
84
|
+
logger.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
|
|
85
|
+
logger.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
|
|
86
|
+
logger.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
|
|
87
|
+
logger.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
|
|
88
|
+
logger.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
|
|
101
89
|
if (diag.connection.isOrphan) {
|
|
102
|
-
log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
90
|
+
logger.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
103
91
|
orphanCount++;
|
|
104
92
|
}
|
|
105
|
-
log("");
|
|
106
93
|
});
|
|
107
94
|
if (orphanCount > 0) {
|
|
108
|
-
log(`⚠️ Total orphan connections found: ${orphanCount}`);
|
|
109
|
-
log(`💡 Suggestion: These connections should be cleaned up`);
|
|
95
|
+
logger.log(`⚠️ Total orphan connections found: ${orphanCount}`);
|
|
96
|
+
logger.log(`💡 Suggestion: These connections should be cleaned up`);
|
|
110
97
|
}
|
|
111
98
|
else {
|
|
112
|
-
log(`✅ No orphan connections found`);
|
|
99
|
+
logger.log(`✅ No orphan connections found`);
|
|
113
100
|
}
|
|
114
101
|
}
|
|
115
102
|
/**
|
|
@@ -117,18 +104,17 @@ export function diagnoseAllManagers() {
|
|
|
117
104
|
* Returns the number of managers that had orphan connections.
|
|
118
105
|
*/
|
|
119
106
|
export function cleanupOrphanConnections() {
|
|
120
|
-
const log = runtime?.log ?? console.log;
|
|
121
107
|
let cleanedCount = 0;
|
|
122
108
|
wsManagerCache.forEach((manager, key) => {
|
|
123
109
|
const diag = manager.getConnectionDiagnostics();
|
|
124
110
|
if (diag.connection.isOrphan) {
|
|
125
|
-
log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
111
|
+
logger.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
126
112
|
manager.disconnect();
|
|
127
113
|
cleanedCount++;
|
|
128
114
|
}
|
|
129
115
|
});
|
|
130
116
|
if (cleanedCount > 0) {
|
|
131
|
-
log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
117
|
+
logger.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
132
118
|
}
|
|
133
119
|
return cleanedCount;
|
|
134
120
|
}
|
|
@@ -20,10 +20,10 @@ export async function downloadFile(url, destPath) {
|
|
|
20
20
|
}
|
|
21
21
|
catch (error) {
|
|
22
22
|
if (error.name === 'AbortError') {
|
|
23
|
-
logger.
|
|
23
|
+
logger.error(`Download timeout (30s) for ${url}`);
|
|
24
24
|
throw new Error(`Download timeout after 30 seconds`);
|
|
25
25
|
}
|
|
26
|
-
logger.
|
|
26
|
+
logger.error(`Failed to download file from ${url}:`);
|
|
27
27
|
throw error;
|
|
28
28
|
}
|
|
29
29
|
finally {
|
|
@@ -52,7 +52,7 @@ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_chann
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
|
-
logger.
|
|
55
|
+
logger.error(`Failed to download file ${name}:`);
|
|
56
56
|
// Continue with other files
|
|
57
57
|
}
|
|
58
58
|
}
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ export interface SendA2AResponseParams {
|
|
|
17
17
|
}>;
|
|
18
18
|
errorCode?: number | string;
|
|
19
19
|
errorMessage?: string;
|
|
20
|
-
runtime?: any;
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
22
|
* Send an A2A artifact update response.
|
|
@@ -50,7 +49,6 @@ export interface SendStatusUpdateParams {
|
|
|
50
49
|
messageId: string;
|
|
51
50
|
text: string;
|
|
52
51
|
state: "submitted" | "working" | "input-required" | "completed" | "canceled" | "failed" | "unknown";
|
|
53
|
-
runtime?: any;
|
|
54
52
|
}
|
|
55
53
|
/**
|
|
56
54
|
* Send an A2A task status update.
|
package/dist/src/formatter.js
CHANGED
|
@@ -7,8 +7,7 @@ import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
|
7
7
|
* Send an A2A artifact update response.
|
|
8
8
|
*/
|
|
9
9
|
export async function sendA2AResponse(params) {
|
|
10
|
-
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage
|
|
11
|
-
const log = runtime?.log ?? console.log;
|
|
10
|
+
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage } = params;
|
|
12
11
|
// Build artifact update event
|
|
13
12
|
const artifact = {
|
|
14
13
|
taskId,
|
|
@@ -47,7 +46,7 @@ export async function sendA2AResponse(params) {
|
|
|
47
46
|
code: errorCode,
|
|
48
47
|
message: errorMessage ?? "任务执行异常,请重试",
|
|
49
48
|
};
|
|
50
|
-
log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
|
|
49
|
+
logger.log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
|
|
51
50
|
}
|
|
52
51
|
// Send via WebSocket
|
|
53
52
|
const wsManager = getXYWebSocketManager(config);
|
|
@@ -59,13 +58,13 @@ export async function sendA2AResponse(params) {
|
|
|
59
58
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
60
59
|
};
|
|
61
60
|
// 📋 Log complete response body
|
|
62
|
-
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
63
|
-
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
64
|
-
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
65
|
-
log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
|
|
66
|
-
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
61
|
+
logger.log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
62
|
+
logger.log(`[A2A_RESPONSE] - append: ${append}`);
|
|
63
|
+
logger.log(`[A2A_RESPONSE] - final: ${final}`);
|
|
64
|
+
logger.log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
|
|
65
|
+
logger.log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
67
66
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
68
|
-
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
67
|
+
logger.log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
69
68
|
}
|
|
70
69
|
/**
|
|
71
70
|
* Send an A2A artifact-update with reasoningText part.
|
|
@@ -110,8 +109,7 @@ export async function sendReasoningTextUpdate(params) {
|
|
|
110
109
|
* Follows A2A protocol standard format with nested status object.
|
|
111
110
|
*/
|
|
112
111
|
export async function sendStatusUpdate(params) {
|
|
113
|
-
const { config, sessionId, taskId, messageId, text, state
|
|
114
|
-
const log = runtime?.log ?? console.log;
|
|
112
|
+
const { config, sessionId, taskId, messageId, text, state } = params;
|
|
115
113
|
// Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
|
|
116
114
|
// fall back to closure-captured values
|
|
117
115
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
@@ -150,9 +148,9 @@ export async function sendStatusUpdate(params) {
|
|
|
150
148
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
151
149
|
};
|
|
152
150
|
// 📋 Log complete response body
|
|
153
|
-
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
154
|
-
log(`[A2A_STATUS] - taskId: ${currentTaskId}`);
|
|
155
|
-
log(`[A2A_STATUS] - text: "${text}"`);
|
|
151
|
+
logger.log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
152
|
+
logger.log(`[A2A_STATUS] - taskId: ${currentTaskId}`);
|
|
153
|
+
logger.log(`[A2A_STATUS] - text: "${text}"`);
|
|
156
154
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
157
155
|
}
|
|
158
156
|
/**
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Heartbeat management for WebSocket connections
|
|
2
2
|
import WebSocket from "ws";
|
|
3
|
+
import { logger } from "./utils/logger.js";
|
|
3
4
|
/**
|
|
4
5
|
* Manages heartbeat for a WebSocket connection.
|
|
5
6
|
* Supports both application-level (30s) and protocol-level (20s) heartbeats.
|
|
@@ -23,8 +24,8 @@ export class HeartbeatManager {
|
|
|
23
24
|
this.onTimeout = onTimeout;
|
|
24
25
|
this.serverName = serverName;
|
|
25
26
|
this.onHeartbeatSuccess = onHeartbeatSuccess;
|
|
26
|
-
this.log = logFn ??
|
|
27
|
-
this.error = errorFn ??
|
|
27
|
+
this.log = logFn ?? ((msg, ...args) => logger.log(msg, ...args));
|
|
28
|
+
this.error = errorFn ?? ((msg, ...args) => logger.error(msg, ...args));
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
31
|
* Start heartbeat monitoring.
|