@ynhcj/xiaoyi-channel 0.0.149-beta → 0.0.151-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 +3 -69
- package/dist/src/approval-bridge.d.ts +48 -0
- package/dist/src/approval-bridge.js +382 -0
- package/dist/src/bot.js +64 -68
- package/dist/src/client.js +13 -23
- package/dist/src/cspl/call_api.d.ts +2 -0
- package/dist/src/cspl/call_api.js +107 -0
- package/dist/src/cspl/config.d.ts +4 -17
- package/dist/src/cspl/config.js +80 -70
- package/dist/src/cspl/configs.json +10 -0
- package/dist/src/cspl/constants.d.ts +46 -24
- package/dist/src/cspl/constants.js +41 -16
- package/dist/src/cspl/sentinel_hook.d.ts +2 -0
- package/dist/src/cspl/sentinel_hook.js +84 -0
- package/dist/src/cspl/steer-context.js +1 -1
- package/dist/src/cspl/upload_file.d.ts +1 -0
- package/dist/src/cspl/upload_file.js +211 -0
- package/dist/src/cspl/utils.d.ts +11 -2
- package/dist/src/cspl/utils.js +265 -15
- package/dist/src/formatter.js +92 -37
- package/dist/src/monitor.js +18 -21
- package/dist/src/outbound.js +8 -9
- package/dist/src/push.js +8 -15
- package/dist/src/reply-dispatcher.js +39 -48
- package/dist/src/self-evolution-handler.js +1 -1
- package/dist/src/sensitive-redactor.d.ts +4 -0
- package/dist/src/sensitive-redactor.js +364 -0
- package/dist/src/task-manager.js +6 -10
- package/dist/src/tools/agent-as-skill-tool.d.ts +7 -0
- package/dist/src/tools/agent-as-skill-tool.js +138 -0
- package/dist/src/tools/calendar-tool.js +1 -1
- package/dist/src/tools/call-device-tool.js +3 -0
- package/dist/src/tools/call-phone-tool.js +1 -1
- package/dist/src/tools/create-alarm-tool.js +1 -1
- package/dist/src/tools/create-all-tools.js +5 -1
- package/dist/src/tools/delete-alarm-tool.js +1 -1
- package/dist/src/tools/find-pc-devices-tool.d.ts +2 -1
- package/dist/src/tools/find-pc-devices-tool.js +84 -88
- package/dist/src/tools/get-device-file-tool-schema.js +3 -2
- package/dist/src/tools/image-reading-tool.js +4 -4
- package/dist/src/tools/location-tool.js +1 -1
- package/dist/src/tools/modify-alarm-tool.js +1 -1
- package/dist/src/tools/modify-note-tool.js +1 -1
- package/dist/src/tools/note-tool.js +1 -1
- package/dist/src/tools/query-app-message-tool.js +1 -1
- package/dist/src/tools/query-memory-data-tool.js +1 -1
- package/dist/src/tools/query-todo-task-tool.js +1 -1
- package/dist/src/tools/save-file-to-phone-tool.js +1 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +1 -1
- package/dist/src/tools/search-calendar-tool.js +1 -1
- package/dist/src/tools/search-contact-tool.js +1 -1
- package/dist/src/tools/search-email-tool.js +1 -1
- package/dist/src/tools/search-file-tool.js +11 -8
- package/dist/src/tools/search-message-tool.js +1 -1
- package/dist/src/tools/search-note-tool.js +1 -1
- package/dist/src/tools/search-photo-gallery-tool.js +1 -1
- package/dist/src/tools/send-email-tool.js +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +2 -2
- package/dist/src/tools/send-message-tool.js +1 -1
- package/dist/src/tools/session-manager.js +5 -0
- package/dist/src/tools/upload-file-tool.js +15 -5
- package/dist/src/tools/upload-photo-tool.js +1 -1
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -1
- package/dist/src/trigger-handler.js +4 -7
- package/dist/src/utils/config-manager.js +3 -6
- package/dist/src/utils/logger.d.ts +8 -0
- package/dist/src/utils/logger.js +69 -34
- package/dist/src/utils/pushdata-manager.js +1 -5
- package/dist/src/utils/pushid-manager.js +1 -2
- package/dist/src/utils/runtime-manager.js +1 -4
- package/dist/src/websocket.js +37 -25
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -35,7 +35,8 @@ export async function handleXYMessage(params) {
|
|
|
35
35
|
if (!sessionId) {
|
|
36
36
|
throw new Error("clearContext request missing sessionId in params");
|
|
37
37
|
}
|
|
38
|
-
logger.
|
|
38
|
+
const log = logger.withContext(sessionId, "");
|
|
39
|
+
log.log(`[BOT] Clear context request`);
|
|
39
40
|
const config = resolveXYConfig(cfg);
|
|
40
41
|
await sendClearContextResponse({
|
|
41
42
|
config,
|
|
@@ -51,7 +52,8 @@ export async function handleXYMessage(params) {
|
|
|
51
52
|
if (!sessionId) {
|
|
52
53
|
throw new Error("tasks/cancel request missing sessionId in params");
|
|
53
54
|
}
|
|
54
|
-
logger.
|
|
55
|
+
const log = logger.withContext(sessionId, taskId);
|
|
56
|
+
log.log(`[BOT] Tasks cancel request`);
|
|
55
57
|
const config = resolveXYConfig(cfg);
|
|
56
58
|
await sendTasksCancelResponse({
|
|
57
59
|
config,
|
|
@@ -63,22 +65,21 @@ export async function handleXYMessage(params) {
|
|
|
63
65
|
}
|
|
64
66
|
// Parse the A2A message (for regular messages)
|
|
65
67
|
const parsed = parseA2AMessage(message);
|
|
68
|
+
// Scoped logger for this session — avoids concurrent session log mixing
|
|
69
|
+
const log = logger.withContext(parsed.sessionId, parsed.taskId);
|
|
66
70
|
// ========== 检测 Trigger 消息 ==========
|
|
67
71
|
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
68
72
|
const triggerData = extractTriggerData(parsed.parts);
|
|
69
73
|
if (triggerData) {
|
|
70
|
-
|
|
71
|
-
logger.log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
72
|
-
logger.log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
74
|
+
log.log(`[BOT] Detected Trigger message, pushDataId=${triggerData.pushDataId}`);
|
|
73
75
|
try {
|
|
74
76
|
// 读取 pushData
|
|
75
77
|
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
76
78
|
if (!pushDataItem) {
|
|
77
|
-
|
|
79
|
+
log.error(`[BOT] pushData not found for ID: ${triggerData.pushDataId}`);
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
82
|
+
log.log(`[BOT] Found pushData, sending direct response, pushDataId=${pushDataItem.pushDataId}`);
|
|
82
83
|
const config = resolveXYConfig(cfg);
|
|
83
84
|
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
84
85
|
await sendA2AResponse({
|
|
@@ -90,11 +91,11 @@ export async function handleXYMessage(params) {
|
|
|
90
91
|
append: false,
|
|
91
92
|
final: true,
|
|
92
93
|
});
|
|
93
|
-
|
|
94
|
+
log.log(`[BOT] Trigger response sent successfully`);
|
|
94
95
|
return; // 提前返回,不继续处理
|
|
95
96
|
}
|
|
96
97
|
catch (err) {
|
|
97
|
-
|
|
98
|
+
log.error(`[BOT] Failed to handle Trigger message:`, err);
|
|
98
99
|
return;
|
|
99
100
|
}
|
|
100
101
|
}
|
|
@@ -103,9 +104,7 @@ export async function handleXYMessage(params) {
|
|
|
103
104
|
const isUpdate = hasActiveTask(parsed.sessionId);
|
|
104
105
|
const skipReg = params.skipRegistration === true;
|
|
105
106
|
if (isUpdate) {
|
|
106
|
-
|
|
107
|
-
logger.log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
108
|
-
logger.log(`[BOT] - New taskId: ${parsed.taskId}`);
|
|
107
|
+
log.log(`[BOT] STEER MODE - Second message detected, new taskId=${parsed.taskId}`);
|
|
109
108
|
}
|
|
110
109
|
// Steer injections skip taskId registration to avoid overwriting the active taskId
|
|
111
110
|
if (!skipReg) {
|
|
@@ -113,28 +112,28 @@ export async function handleXYMessage(params) {
|
|
|
113
112
|
// Extract and update push_id if present
|
|
114
113
|
const pushId = extractPushId(parsed.parts);
|
|
115
114
|
if (pushId) {
|
|
116
|
-
|
|
115
|
+
log.log(`[BOT] Extracted push_id from user message`);
|
|
117
116
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
118
117
|
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
119
118
|
addPushId(pushId).catch((err) => {
|
|
120
|
-
|
|
119
|
+
log.error(`[BOT] Failed to persist pushId:`, err);
|
|
121
120
|
});
|
|
122
121
|
}
|
|
123
122
|
else {
|
|
124
|
-
|
|
123
|
+
log.log(`[BOT] No push_id found in message, using config default`);
|
|
125
124
|
}
|
|
126
125
|
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
127
126
|
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
128
127
|
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
129
128
|
parsed.taskId // TASK_ID (param.id)
|
|
130
129
|
).catch((err) => {
|
|
131
|
-
|
|
130
|
+
log.error(`[BOT] Failed to save runtime info:`, err);
|
|
132
131
|
});
|
|
133
132
|
}
|
|
134
133
|
// Extract deviceType if present (always parse — used in ctxPayload.MessageSid)
|
|
135
134
|
const deviceType = extractDeviceType(parsed.parts);
|
|
136
135
|
if (deviceType) {
|
|
137
|
-
|
|
136
|
+
log.log(`[BOT] Extracted deviceType: ${deviceType}`);
|
|
138
137
|
}
|
|
139
138
|
// Resolve configuration (needed for status updates)
|
|
140
139
|
const config = resolveXYConfig(cfg);
|
|
@@ -150,7 +149,7 @@ export async function handleXYMessage(params) {
|
|
|
150
149
|
id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
|
|
151
150
|
},
|
|
152
151
|
});
|
|
153
|
-
|
|
152
|
+
log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
|
|
154
153
|
// Steer injections skip session registration to avoid refCount leaks
|
|
155
154
|
if (!skipReg) {
|
|
156
155
|
registerSession(route.sessionKey, {
|
|
@@ -162,7 +161,7 @@ export async function handleXYMessage(params) {
|
|
|
162
161
|
deviceType,
|
|
163
162
|
});
|
|
164
163
|
// 🔑 发送初始状态更新
|
|
165
|
-
|
|
164
|
+
log.log(`[BOT] Sending initial status update`);
|
|
166
165
|
void sendStatusUpdate({
|
|
167
166
|
config,
|
|
168
167
|
sessionId: parsed.sessionId,
|
|
@@ -171,7 +170,7 @@ export async function handleXYMessage(params) {
|
|
|
171
170
|
text: "任务正在处理中,请稍候~",
|
|
172
171
|
state: "working",
|
|
173
172
|
}).catch((err) => {
|
|
174
|
-
|
|
173
|
+
log.error(`Failed to send initial status update:`, err);
|
|
175
174
|
});
|
|
176
175
|
}
|
|
177
176
|
// Extract text and files from parts
|
|
@@ -183,18 +182,18 @@ export async function handleXYMessage(params) {
|
|
|
183
182
|
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
184
183
|
if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
|
|
185
184
|
const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
|
|
186
|
-
|
|
185
|
+
log.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
|
|
187
186
|
if (shouldNudge) {
|
|
188
187
|
const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
|
|
189
188
|
textForAgent = augmented.text;
|
|
190
189
|
if (augmented.appended) {
|
|
191
|
-
|
|
190
|
+
log.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
|
|
192
191
|
}
|
|
193
192
|
}
|
|
194
193
|
}
|
|
195
194
|
}
|
|
196
195
|
catch (selfEvolutionError) {
|
|
197
|
-
|
|
196
|
+
log.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
198
197
|
}
|
|
199
198
|
}
|
|
200
199
|
// 🔑 Steer消息: 跳过旧路径直接进入 streaming-signal 队列
|
|
@@ -209,9 +208,11 @@ export async function handleXYMessage(params) {
|
|
|
209
208
|
const steerDownloadedFiles = await downloadFilesFromParts(steerFileParts);
|
|
210
209
|
const steerMediaPayload = buildXYMediaPayload(steerDownloadedFiles);
|
|
211
210
|
if (steerFileParts.length > 0) {
|
|
212
|
-
|
|
211
|
+
log.log(`[BOT] Steer message with ${steerFileParts.length} file(s), enqueuing to streaming-signal queue`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
log.log(`[BOT] Steer message — enqueuing to streaming-signal queue`);
|
|
213
215
|
}
|
|
214
|
-
logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
|
|
215
216
|
await enqueueSteer({
|
|
216
217
|
sessionId: parsed.sessionId,
|
|
217
218
|
sessionKey: route.sessionKey,
|
|
@@ -223,8 +224,7 @@ export async function handleXYMessage(params) {
|
|
|
223
224
|
route,
|
|
224
225
|
deviceType,
|
|
225
226
|
});
|
|
226
|
-
|
|
227
|
-
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
227
|
+
log.log(`[BOT] Steer queue completed`);
|
|
228
228
|
return;
|
|
229
229
|
}
|
|
230
230
|
// ── First message (non-steer) path below ──────────────────────
|
|
@@ -236,7 +236,7 @@ export async function handleXYMessage(params) {
|
|
|
236
236
|
if (!skipReg) {
|
|
237
237
|
const fileParts = extractFileParts(parsed.parts);
|
|
238
238
|
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
239
|
-
|
|
239
|
+
log.log(`[BOT] Downloaded ${downloadedFiles.length} file(s)`);
|
|
240
240
|
mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
241
241
|
}
|
|
242
242
|
// Resolve envelope format options (following feishu pattern)
|
|
@@ -282,8 +282,7 @@ export async function handleXYMessage(params) {
|
|
|
282
282
|
// 🔑 Streaming 信号已在上方创建(在文件下载之前)
|
|
283
283
|
const steerState = { steered: false };
|
|
284
284
|
// 🔑 创建dispatcher
|
|
285
|
-
|
|
286
|
-
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
285
|
+
log.log(`[BOT-DISPATCHER] Creating reply dispatcher`);
|
|
287
286
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
288
287
|
cfg,
|
|
289
288
|
runtime,
|
|
@@ -306,21 +305,20 @@ export async function handleXYMessage(params) {
|
|
|
306
305
|
agentId: route.accountId,
|
|
307
306
|
deviceType,
|
|
308
307
|
};
|
|
309
|
-
|
|
308
|
+
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
310
309
|
await core.channel.reply.withReplyDispatcher({
|
|
311
310
|
dispatcher,
|
|
312
311
|
onSettled: () => {
|
|
313
|
-
|
|
314
|
-
logger.log(`[BOT] - steered: ${steerState.steered}`);
|
|
312
|
+
log.log(`[BOT] onSettled, steered=${steerState.steered}`);
|
|
315
313
|
// 🔑 When steered, skip heavy cleanup — the first message's dispatcher is still running
|
|
316
314
|
if (steerState.steered) {
|
|
317
|
-
|
|
315
|
+
log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
|
|
318
316
|
return;
|
|
319
317
|
}
|
|
320
318
|
streamingSignals.delete(parsed.sessionId);
|
|
321
319
|
decrementTaskIdRef(parsed.sessionId);
|
|
322
320
|
unregisterSession(route.sessionKey);
|
|
323
|
-
|
|
321
|
+
log.log(`[BOT] Cleanup completed`);
|
|
324
322
|
},
|
|
325
323
|
run: () => {
|
|
326
324
|
// 🔐 Use AsyncLocalStorage to provide session context to tools.
|
|
@@ -329,12 +327,7 @@ export async function handleXYMessage(params) {
|
|
|
329
327
|
// signal init complete to release the global dispatch gate
|
|
330
328
|
// for the next session.
|
|
331
329
|
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
332
|
-
|
|
333
|
-
logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
334
|
-
logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
335
|
-
logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
336
|
-
logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
337
|
-
logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
330
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
|
|
338
331
|
try {
|
|
339
332
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
340
333
|
ctx: ctxPayload,
|
|
@@ -342,15 +335,11 @@ export async function handleXYMessage(params) {
|
|
|
342
335
|
dispatcher,
|
|
343
336
|
replyOptions,
|
|
344
337
|
});
|
|
345
|
-
|
|
346
|
-
logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
338
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, result=${JSON.stringify(result)}`);
|
|
347
339
|
return result;
|
|
348
340
|
}
|
|
349
341
|
catch (dispatchErr) {
|
|
350
|
-
|
|
351
|
-
logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
352
|
-
logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
353
|
-
logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
342
|
+
log.error(`[BOT-DISPATCH] dispatchReplyFromConfig threw: ${dispatchErr instanceof Error ? `${dispatchErr.name}: ${dispatchErr.message}` : String(dispatchErr)}`, dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : undefined);
|
|
354
343
|
throw dispatchErr;
|
|
355
344
|
}
|
|
356
345
|
});
|
|
@@ -359,20 +348,23 @@ export async function handleXYMessage(params) {
|
|
|
359
348
|
return dispatchPromise;
|
|
360
349
|
},
|
|
361
350
|
});
|
|
362
|
-
|
|
363
|
-
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
351
|
+
log.log(`[BOT] Dispatcher completed`);
|
|
364
352
|
}
|
|
365
353
|
catch (err) {
|
|
366
354
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
367
|
-
|
|
355
|
+
// Note: if error occurs before parseA2AMessage, `log` may not be defined yet
|
|
356
|
+
const errSessionId = message.params?.sessionId || "";
|
|
357
|
+
const errTaskId = message.params?.id || message.id || "";
|
|
358
|
+
const errLog = logger.withContext(errSessionId, errTaskId);
|
|
359
|
+
errLog.error("Failed to handle XY message:", err);
|
|
368
360
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
369
|
-
|
|
361
|
+
errLog.log(`[BOT] Error occurred, attempting cleanup`);
|
|
370
362
|
// 🔑 错误时也要清理taskId和session
|
|
371
363
|
try {
|
|
372
364
|
const params = message.params;
|
|
373
365
|
const sessionId = params?.sessionId;
|
|
374
366
|
if (sessionId) {
|
|
375
|
-
|
|
367
|
+
errLog.log(`[BOT] Cleaning up after error`);
|
|
376
368
|
// 清理 taskId
|
|
377
369
|
decrementTaskIdRef(sessionId);
|
|
378
370
|
// 清理 session
|
|
@@ -387,11 +379,11 @@ export async function handleXYMessage(params) {
|
|
|
387
379
|
},
|
|
388
380
|
});
|
|
389
381
|
unregisterSession(route.sessionKey);
|
|
390
|
-
|
|
382
|
+
errLog.log(`[BOT] Cleanup completed after error`);
|
|
391
383
|
}
|
|
392
384
|
}
|
|
393
385
|
catch (cleanupErr) {
|
|
394
|
-
|
|
386
|
+
errLog.log(`[BOT] Cleanup failed:`, cleanupErr);
|
|
395
387
|
// Ignore cleanup errors
|
|
396
388
|
}
|
|
397
389
|
// ❌ Don't re-throw: message processing error should not affect gateway stability
|
|
@@ -428,20 +420,22 @@ const steerQueues = _g.__xySteerQueues;
|
|
|
428
420
|
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
429
421
|
*/
|
|
430
422
|
export function notifyModelStreaming(sessionId) {
|
|
423
|
+
const log = logger.withContext(sessionId, "");
|
|
431
424
|
const signal = streamingSignals.get(sessionId);
|
|
432
425
|
if (signal) {
|
|
433
426
|
// 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
|
|
434
427
|
// 清理由第一条消息的 onSettled 兜底。
|
|
435
428
|
signal.notify();
|
|
436
|
-
|
|
429
|
+
log.log(`[STEER-QUEUE] Model streaming signal fired`);
|
|
437
430
|
}
|
|
438
431
|
}
|
|
439
432
|
function createStreamingSignal(sessionId) {
|
|
433
|
+
const log = logger.withContext(sessionId, "");
|
|
440
434
|
let resolve;
|
|
441
435
|
const promise = new Promise(r => { resolve = r; });
|
|
442
436
|
const signal = { promise, notify: resolve };
|
|
443
437
|
streamingSignals.set(sessionId, signal);
|
|
444
|
-
|
|
438
|
+
log.log(`[STEER-QUEUE] Streaming signal created`);
|
|
445
439
|
return signal;
|
|
446
440
|
}
|
|
447
441
|
/**
|
|
@@ -451,13 +445,14 @@ function createStreamingSignal(sessionId) {
|
|
|
451
445
|
*/
|
|
452
446
|
function enqueueSteer(params) {
|
|
453
447
|
const { sessionId } = params;
|
|
448
|
+
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
454
449
|
// 取出当前队列尾部(或 undefined),然后链上新的 Promise
|
|
455
450
|
const prev = steerQueues.get(sessionId);
|
|
456
451
|
const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
|
|
457
452
|
steerQueues.set(sessionId, next);
|
|
458
453
|
// 链条结束后清理
|
|
459
454
|
next.catch((err) => {
|
|
460
|
-
|
|
455
|
+
log.error(`[STEER-QUEUE] Steer chain failed: ${String(err)}`);
|
|
461
456
|
}).finally(() => {
|
|
462
457
|
if (steerQueues.get(sessionId) === next) {
|
|
463
458
|
steerQueues.delete(sessionId);
|
|
@@ -467,37 +462,38 @@ function enqueueSteer(params) {
|
|
|
467
462
|
}
|
|
468
463
|
async function dispatchSteerWhenReady(params) {
|
|
469
464
|
const { sessionId, sessionKey, steerText } = params;
|
|
465
|
+
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
470
466
|
// 1. 等待第一条消息开始 streaming
|
|
471
467
|
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
472
468
|
// 轮询等待直到 signal 出现,最長等待 ~5 秒。
|
|
473
469
|
let signal = streamingSignals.get(sessionId);
|
|
474
470
|
if (!signal) {
|
|
475
|
-
|
|
471
|
+
log.log(`[STEER-QUEUE] Signal not yet created, polling`);
|
|
476
472
|
for (let i = 0; i < 50; i++) {
|
|
477
473
|
await new Promise(r => setTimeout(r, 100));
|
|
478
474
|
signal = streamingSignals.get(sessionId);
|
|
479
475
|
if (signal)
|
|
480
476
|
break;
|
|
481
477
|
if (!hasActiveTask(sessionId)) {
|
|
482
|
-
|
|
478
|
+
log.log(`[STEER-QUEUE] First message completed while waiting, skip steer`);
|
|
483
479
|
return;
|
|
484
480
|
}
|
|
485
481
|
}
|
|
486
482
|
}
|
|
487
483
|
if (signal) {
|
|
488
|
-
|
|
484
|
+
log.log(`[STEER-QUEUE] Waiting for streaming signal`);
|
|
489
485
|
await signal.promise;
|
|
490
|
-
|
|
486
|
+
log.log(`[STEER-QUEUE] Streaming signal received`);
|
|
491
487
|
}
|
|
492
488
|
else {
|
|
493
489
|
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
494
490
|
// 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
|
|
495
|
-
|
|
491
|
+
log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
|
|
496
492
|
return;
|
|
497
493
|
}
|
|
498
494
|
// 2. 第一条消息已结束 → 放弃
|
|
499
495
|
if (!hasActiveTask(sessionId)) {
|
|
500
|
-
|
|
496
|
+
log.log(`[STEER-QUEUE] First message completed, skip steer`);
|
|
501
497
|
return;
|
|
502
498
|
}
|
|
503
499
|
// 3. 构建 dispatch 上下文并 dispatch /steer
|
|
@@ -559,11 +555,11 @@ async function dispatchSteerWhenReady(params) {
|
|
|
559
555
|
agentId: params.route.accountId,
|
|
560
556
|
deviceType: params.deviceType,
|
|
561
557
|
};
|
|
562
|
-
|
|
558
|
+
log.log(`[STEER-QUEUE] Dispatching steer`);
|
|
563
559
|
await core.channel.reply.withReplyDispatcher({
|
|
564
560
|
dispatcher,
|
|
565
561
|
onSettled: () => {
|
|
566
|
-
|
|
562
|
+
log.log(`[STEER-QUEUE] Steer dispatch settled`);
|
|
567
563
|
},
|
|
568
564
|
run: () => {
|
|
569
565
|
return runWithSessionContext(sessionContext, async () => {
|
|
@@ -573,10 +569,10 @@ async function dispatchSteerWhenReady(params) {
|
|
|
573
569
|
dispatcher,
|
|
574
570
|
replyOptions,
|
|
575
571
|
});
|
|
576
|
-
|
|
572
|
+
log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
577
573
|
return result;
|
|
578
574
|
});
|
|
579
575
|
},
|
|
580
576
|
});
|
|
581
|
-
|
|
577
|
+
log.log(`[STEER-QUEUE] Steer dispatch completed`);
|
|
582
578
|
}
|
package/dist/src/client.js
CHANGED
|
@@ -24,10 +24,10 @@ export function getXYWebSocketManager(config, runtime) {
|
|
|
24
24
|
return cached;
|
|
25
25
|
}
|
|
26
26
|
// Create new manager
|
|
27
|
-
logger.log(`[WS-MANAGER-CACHE]
|
|
27
|
+
logger.log(`[WS-MANAGER-CACHE] Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
|
|
28
28
|
cached = new XYWebSocketManager(config, runtime);
|
|
29
29
|
wsManagerCache.set(cacheKey, cached);
|
|
30
|
-
logger.log(`[WS-MANAGER-CACHE]
|
|
30
|
+
logger.log(`[WS-MANAGER-CACHE] Total managers after creation: ${wsManagerCache.size}`);
|
|
31
31
|
return cached;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
@@ -38,13 +38,13 @@ export function removeXYWebSocketManager(config) {
|
|
|
38
38
|
const cacheKey = `${config.apiKey}-${config.agentId}`;
|
|
39
39
|
const manager = wsManagerCache.get(cacheKey);
|
|
40
40
|
if (manager) {
|
|
41
|
-
logger.log(
|
|
41
|
+
logger.log(`[WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
|
|
42
42
|
manager.disconnect();
|
|
43
43
|
wsManagerCache.delete(cacheKey);
|
|
44
|
-
logger.log(
|
|
44
|
+
logger.log(`[WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
|
-
logger.log(
|
|
47
|
+
logger.log(`[WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
@@ -68,35 +68,25 @@ export function getCachedManagerCount() {
|
|
|
68
68
|
* Helps identify connection issues and orphan connections.
|
|
69
69
|
*/
|
|
70
70
|
export function diagnoseAllManagers() {
|
|
71
|
-
logger.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
71
|
+
logger.log(`[DIAG] Total cached managers: ${wsManagerCache.size}`);
|
|
72
72
|
if (wsManagerCache.size === 0) {
|
|
73
|
-
logger.log("
|
|
73
|
+
logger.log("[DIAG] No managers in cache");
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
let orphanCount = 0;
|
|
77
77
|
wsManagerCache.forEach((manager, key) => {
|
|
78
78
|
const diag = manager.getConnectionDiagnostics();
|
|
79
|
-
logger.log(`
|
|
80
|
-
// Connection
|
|
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}`);
|
|
79
|
+
logger.log(`[DIAG] Manager ${key} — event listeners: ${diag.totalEventListeners} | Connection: exists=${diag.connection.exists}, readyState=${diag.connection.readyState}, stateConnected=${diag.connection.stateConnected}/${diag.connection.stateReady}, reconnectAttempts=${diag.connection.reconnectAttempts}, wsListeners=${diag.connection.listenerCount}, heartbeatActive=${diag.connection.heartbeatActive}, hasReconnectTimer=${diag.connection.hasReconnectTimer}`);
|
|
89
80
|
if (diag.connection.isOrphan) {
|
|
90
|
-
logger.log(`
|
|
81
|
+
logger.log(`[DIAG] ORPHAN CONNECTION DETECTED on manager: ${key}`);
|
|
91
82
|
orphanCount++;
|
|
92
83
|
}
|
|
93
84
|
});
|
|
94
85
|
if (orphanCount > 0) {
|
|
95
|
-
logger.log(
|
|
96
|
-
logger.log(`💡 Suggestion: These connections should be cleaned up`);
|
|
86
|
+
logger.log(`[DIAG] Total orphan connections found: ${orphanCount} — these connections should be cleaned up`);
|
|
97
87
|
}
|
|
98
88
|
else {
|
|
99
|
-
logger.log(
|
|
89
|
+
logger.log("[DIAG] No orphan connections found");
|
|
100
90
|
}
|
|
101
91
|
}
|
|
102
92
|
/**
|
|
@@ -108,13 +98,13 @@ export function cleanupOrphanConnections() {
|
|
|
108
98
|
wsManagerCache.forEach((manager, key) => {
|
|
109
99
|
const diag = manager.getConnectionDiagnostics();
|
|
110
100
|
if (diag.connection.isOrphan) {
|
|
111
|
-
logger.log(
|
|
101
|
+
logger.log(`[CLEANUP] Cleaning up orphan connections in manager: ${key}`);
|
|
112
102
|
manager.disconnect();
|
|
113
103
|
cleanedCount++;
|
|
114
104
|
}
|
|
115
105
|
});
|
|
116
106
|
if (cleanedCount > 0) {
|
|
117
|
-
logger.log(
|
|
107
|
+
logger.log(`[CLEANUP] Cleaned up ${cleanedCount} manager(s) with orphan connections`);
|
|
118
108
|
}
|
|
119
109
|
return cleanedCount;
|
|
120
110
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 版权所有 (c) 华为技术有限公司 2026-2026
|
|
3
|
+
*/
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
import { getConfig } from './config.js';
|
|
7
|
+
import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
|
|
8
|
+
function buildHeadersForCelia(config, sessionId) {
|
|
9
|
+
if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
|
|
10
|
+
throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
'x-hag-trace-id': sessionId,
|
|
14
|
+
'x-uid': config.uid,
|
|
15
|
+
'x-api-key': config.apiKey,
|
|
16
|
+
'x-request-from': config.requestFrom,
|
|
17
|
+
'x-skill-id': config.skillId,
|
|
18
|
+
'content-type': 'application/json'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildRequestOptions(url, headers, timeout) {
|
|
22
|
+
const urlObj = new URL(url);
|
|
23
|
+
return {
|
|
24
|
+
hostname: urlObj.hostname,
|
|
25
|
+
port: urlObj.port || DEFAULT_HTTP_PORT,
|
|
26
|
+
path: urlObj.pathname,
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: headers,
|
|
29
|
+
timeout: timeout
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function checkHttpStatus(res) {
|
|
33
|
+
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
34
|
+
throw new Error(`HTTP error! status: ${res.statusCode}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parseResponseData(data) {
|
|
38
|
+
try {
|
|
39
|
+
if (data === undefined || data === null || data.trim() === '') {
|
|
40
|
+
throw new Error('API response data is empty or invalid');
|
|
41
|
+
}
|
|
42
|
+
const jsonData = JSON.parse(data);
|
|
43
|
+
if (jsonData.retCode && jsonData.retCode !== "0") {
|
|
44
|
+
const errorMsg = jsonData.retMsg || 'Unknown API error';
|
|
45
|
+
throw new Error(`API error: ${errorMsg}`);
|
|
46
|
+
}
|
|
47
|
+
if (!jsonData.retCode && jsonData.code) {
|
|
48
|
+
const errorMsg = jsonData.desc || 'Unknown backend error';
|
|
49
|
+
throw new Error(`Backend error: ${errorMsg}`);
|
|
50
|
+
}
|
|
51
|
+
return jsonData;
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (e instanceof Error) {
|
|
55
|
+
throw new Error(`[SENTINEL HOOK] Failed to parse response:${e.message}`);
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function handleResponse(res, resolve, reject) {
|
|
61
|
+
let data = '';
|
|
62
|
+
try {
|
|
63
|
+
checkHttpStatus(res);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
reject(e);
|
|
67
|
+
}
|
|
68
|
+
res.on('data', (chunk) => {
|
|
69
|
+
data += chunk;
|
|
70
|
+
});
|
|
71
|
+
res.on('end', () => {
|
|
72
|
+
try {
|
|
73
|
+
const result = parseResponseData(data);
|
|
74
|
+
resolve(result);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
reject(e);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export async function callApi(questionText, api, sessionId) {
|
|
82
|
+
const config = getConfig(api);
|
|
83
|
+
const headersForCelia = buildHeadersForCelia(config, sessionId);
|
|
84
|
+
const payload = {
|
|
85
|
+
questionText: questionText,
|
|
86
|
+
textSource: config.textSource,
|
|
87
|
+
action: config.action,
|
|
88
|
+
extra: `${JSON.stringify({ userId: config.uid })}`
|
|
89
|
+
};
|
|
90
|
+
const httpBody = JSON.stringify(payload);
|
|
91
|
+
const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
|
|
94
|
+
const req = https.request(options, (res) => {
|
|
95
|
+
handleResponse(res, resolve, reject);
|
|
96
|
+
});
|
|
97
|
+
req.on('error', (error) => {
|
|
98
|
+
reject(error);
|
|
99
|
+
});
|
|
100
|
+
req.on('timeout', () => {
|
|
101
|
+
req.destroy();
|
|
102
|
+
reject(new Error('[SENTINEL HOOK] Request timeout'));
|
|
103
|
+
});
|
|
104
|
+
req.write(httpBody);
|
|
105
|
+
req.end();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { XYChannelConfig } from "../types.js";
|
|
1
|
+
import { HttpHeaders } from './constants.js';
|
|
3
2
|
export interface ApiConfig {
|
|
4
3
|
url: string;
|
|
5
4
|
timeout: number;
|
|
6
5
|
}
|
|
7
|
-
export interface
|
|
6
|
+
export interface Config {
|
|
8
7
|
api: ApiConfig;
|
|
8
|
+
headers?: HttpHeaders;
|
|
9
9
|
uid: string;
|
|
10
10
|
apiKey: string;
|
|
11
11
|
skillId: string;
|
|
@@ -13,17 +13,4 @@ export interface CsplConfig {
|
|
|
13
13
|
textSource: string;
|
|
14
14
|
action: string;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
18
|
-
* serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
|
|
19
|
-
*
|
|
20
|
-
* Accepts either ClawdbotConfig (legacy after_tool_call path) or
|
|
21
|
-
* XYChannelConfig (AgentToolResultMiddleware path). Config is cached
|
|
22
|
-
* after the first successful call so subsequent calls can omit the arg.
|
|
23
|
-
*/
|
|
24
|
-
export declare function getCsplConfig(cfg?: ClawdbotConfig): CsplConfig;
|
|
25
|
-
/**
|
|
26
|
-
* Initialize CSPL config from an already-resolved XYChannelConfig.
|
|
27
|
-
* Used by AgentToolResultMiddleware which has session context but not ClawdbotConfig.
|
|
28
|
-
*/
|
|
29
|
-
export declare function initCsplConfigFromXYConfig(xyConfig: XYChannelConfig): CsplConfig;
|
|
16
|
+
export declare function getConfig(api: any): Config;
|