@ynhcj/xiaoyi-channel 0.0.53-beta → 0.0.53-next
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 +7 -4
- package/dist/src/bot.d.ts +1 -0
- package/dist/src/bot.js +45 -32
- package/dist/src/channel.js +16 -2
- package/dist/src/client.js +0 -6
- package/dist/src/cspl/call-api.js +14 -32
- package/dist/src/cspl/config.js +3 -3
- package/dist/src/cspl/constants.d.ts +1 -0
- package/dist/src/file-download.js +5 -6
- package/dist/src/file-upload.js +1 -11
- package/dist/src/formatter.d.ts +2 -0
- package/dist/src/formatter.js +11 -6
- package/dist/src/monitor.js +8 -10
- package/dist/src/outbound.js +1 -19
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +16 -0
- package/dist/src/provider.d.ts +2 -0
- package/dist/src/provider.js +124 -0
- package/dist/src/push.js +0 -21
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +15 -11
- package/dist/src/tools/call-phone-tool.js +2 -18
- package/dist/src/tools/create-alarm-tool.js +3 -7
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +35 -0
- package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
- package/dist/src/tools/find-pc-devices-tool.js +98 -0
- package/dist/src/tools/image-reading-tool.js +3 -3
- package/dist/src/tools/modify-alarm-tool.js +3 -7
- package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
- package/dist/src/tools/save-file-to-phone-tool.js +170 -0
- package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
- package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
- package/dist/src/tools/send-command-to-car-tool.js +85 -0
- package/dist/src/tools/send-file-to-user-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +1 -0
- package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
- package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
- package/dist/src/tools/upload-file-tool.js +2 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-add-collection-tool.js +188 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +42 -7
- package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
- package/dist/src/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/dist/src/websocket.js +15 -9
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { xyPlugin } from "./src/channel.js";
|
|
3
|
+
import { xiaoyiProvider } from "./src/provider.js";
|
|
3
4
|
import { setXYRuntime } from "./src/runtime.js";
|
|
4
5
|
import { tryInjectSteer } from "./src/steer-injector.js";
|
|
5
6
|
import { callCsplApi } from "./src/cspl/call-api.js";
|
|
@@ -18,13 +19,14 @@ const plugin = {
|
|
|
18
19
|
register(api) {
|
|
19
20
|
setXYRuntime(api.runtime);
|
|
20
21
|
api.registerChannel({ plugin: xyPlugin });
|
|
21
|
-
|
|
22
|
+
api.registerProvider(xiaoyiProvider);
|
|
23
|
+
// SENTINEL HOOK after_tool_call hook: 监听工具结果,发送至安全检测 API 进行安全检测
|
|
22
24
|
// 如果响应为 REJECT,注入 steer 消息中止当前对话
|
|
23
25
|
api.on("after_tool_call", async (event, ctx) => {
|
|
24
26
|
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
|
-
console.log(`[
|
|
29
|
+
console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
28
30
|
try {
|
|
29
31
|
const resultText = extractResultText(event, event.toolName);
|
|
30
32
|
const resultLength = resultText.length;
|
|
@@ -33,6 +35,7 @@ const plugin = {
|
|
|
33
35
|
}
|
|
34
36
|
// 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
|
|
35
37
|
const questionText = {
|
|
38
|
+
subSceneID: 'TOOL_OUTPUT',
|
|
36
39
|
tool: event.toolName,
|
|
37
40
|
output: [{ content: "" }],
|
|
38
41
|
};
|
|
@@ -47,13 +50,13 @@ const plugin = {
|
|
|
47
50
|
}
|
|
48
51
|
const response = await callCsplApi(finalJson, api.config);
|
|
49
52
|
const result = parseSecurityResult(response);
|
|
50
|
-
console.log(`[
|
|
53
|
+
console.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
|
|
51
54
|
if (result.status === "REJECT") {
|
|
52
55
|
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
catch (err) {
|
|
56
|
-
api.logger.error(`[
|
|
59
|
+
api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
|
|
57
60
|
}
|
|
58
61
|
});
|
|
59
62
|
},
|
package/dist/src/bot.d.ts
CHANGED
package/dist/src/bot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { setCachedContext } from "./steer-injector.js";
|
|
3
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
4
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
|
|
4
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
5
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
6
6
|
import { resolveXYConfig } from "./config.js";
|
|
7
7
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -9,6 +9,7 @@ import { registerSession, unregisterSession, runWithSessionContext } from "./too
|
|
|
9
9
|
import { configManager } from "./utils/config-manager.js";
|
|
10
10
|
import { addPushId } from "./utils/pushid-manager.js";
|
|
11
11
|
import { getPushDataById } from "./utils/pushdata-manager.js";
|
|
12
|
+
import { saveRuntimeInfo } from "./utils/runtime-manager.js";
|
|
12
13
|
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
13
14
|
/**
|
|
14
15
|
* Handle an incoming A2A message.
|
|
@@ -16,7 +17,7 @@ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActive
|
|
|
16
17
|
* Runtime is expected to be validated before calling this function.
|
|
17
18
|
*/
|
|
18
19
|
export async function handleXYMessage(params) {
|
|
19
|
-
const { cfg, runtime, message, accountId } = params;
|
|
20
|
+
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
20
21
|
const log = runtime?.log ?? console.log;
|
|
21
22
|
const error = runtime?.error ?? console.error;
|
|
22
23
|
// 每次收到消息时更新缓存,供 steer 注入使用
|
|
@@ -26,8 +27,6 @@ export async function handleXYMessage(params) {
|
|
|
26
27
|
try {
|
|
27
28
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
28
29
|
const messageMethod = message.method;
|
|
29
|
-
log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
|
|
30
|
-
log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
|
|
31
30
|
// Handle clearContext messages (params only has sessionId)
|
|
32
31
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
33
32
|
const sessionId = message.params?.sessionId;
|
|
@@ -62,6 +61,7 @@ export async function handleXYMessage(params) {
|
|
|
62
61
|
}
|
|
63
62
|
// Parse the A2A message (for regular messages)
|
|
64
63
|
const parsed = parseA2AMessage(message);
|
|
64
|
+
console.log("Parsed A2A message:", JSON.stringify(parsed, null, 2));
|
|
65
65
|
// ========== 检测 Trigger 消息 ==========
|
|
66
66
|
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
67
67
|
const triggerData = extractTriggerData(parsed.parts);
|
|
@@ -78,8 +78,6 @@ export async function handleXYMessage(params) {
|
|
|
78
78
|
}
|
|
79
79
|
log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
80
80
|
log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
81
|
-
log(`[BOT] - time: ${pushDataItem.time}`);
|
|
82
|
-
log(`[BOT] - content length: ${pushDataItem.dataDetail.length} chars`);
|
|
83
81
|
const config = resolveXYConfig(cfg);
|
|
84
82
|
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
85
83
|
await sendA2AResponse({
|
|
@@ -120,9 +118,6 @@ export async function handleXYMessage(params) {
|
|
|
120
118
|
const pushId = extractPushId(parsed.parts);
|
|
121
119
|
if (pushId) {
|
|
122
120
|
log(`[BOT] 📌 Extracted push_id from user message`);
|
|
123
|
-
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
124
|
-
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
125
|
-
log(`[BOT] - Full push_id: ${pushId}`);
|
|
126
121
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
127
122
|
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
128
123
|
addPushId(pushId).catch((err) => {
|
|
@@ -132,6 +127,18 @@ export async function handleXYMessage(params) {
|
|
|
132
127
|
else {
|
|
133
128
|
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
134
129
|
}
|
|
130
|
+
// Extract deviceType if present (same level as push_id in systemVariables)
|
|
131
|
+
const deviceType = extractDeviceType(parsed.parts);
|
|
132
|
+
if (deviceType) {
|
|
133
|
+
log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
|
|
134
|
+
}
|
|
135
|
+
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
136
|
+
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
137
|
+
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
138
|
+
parsed.taskId // TASK_ID (param.id)
|
|
139
|
+
).catch((err) => {
|
|
140
|
+
error(`[BOT] Failed to save runtime info:`, err);
|
|
141
|
+
});
|
|
135
142
|
// Resolve configuration (needed for status updates)
|
|
136
143
|
const config = resolveXYConfig(cfg);
|
|
137
144
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -147,12 +154,6 @@ export async function handleXYMessage(params) {
|
|
|
147
154
|
},
|
|
148
155
|
});
|
|
149
156
|
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
150
|
-
// 🔑 注册session(带引用计数)
|
|
151
|
-
log(`[BOT] 📝 About to register session for tools...`);
|
|
152
|
-
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
153
|
-
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
154
|
-
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
155
|
-
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
156
157
|
registerSession(route.sessionKey, {
|
|
157
158
|
config,
|
|
158
159
|
sessionId: parsed.sessionId,
|
|
@@ -160,7 +161,6 @@ export async function handleXYMessage(params) {
|
|
|
160
161
|
messageId: parsed.messageId,
|
|
161
162
|
agentId: route.accountId,
|
|
162
163
|
});
|
|
163
|
-
log(`[BOT] ✅ Session registered for tools`);
|
|
164
164
|
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
165
165
|
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
166
166
|
void sendStatusUpdate({
|
|
@@ -178,6 +178,7 @@ export async function handleXYMessage(params) {
|
|
|
178
178
|
const fileParts = extractFileParts(parsed.parts);
|
|
179
179
|
// Download files to local disk
|
|
180
180
|
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
181
|
+
console.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
|
|
181
182
|
const mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
182
183
|
// Resolve envelope format options (following feishu pattern)
|
|
183
184
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
@@ -221,9 +222,7 @@ export async function handleXYMessage(params) {
|
|
|
221
222
|
});
|
|
222
223
|
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
223
224
|
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
224
|
-
log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
|
|
225
225
|
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
226
|
-
log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
|
|
227
226
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
228
227
|
cfg,
|
|
229
228
|
runtime,
|
|
@@ -233,19 +232,12 @@ export async function handleXYMessage(params) {
|
|
|
233
232
|
accountId: route.accountId,
|
|
234
233
|
isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
|
|
235
234
|
});
|
|
236
|
-
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
237
235
|
// 🔑 只有第一条消息启动状态定时器
|
|
238
236
|
// 第二条消息会很快返回,不需要定时器
|
|
239
237
|
if (!isSecondMessage) {
|
|
240
238
|
startStatusInterval();
|
|
241
239
|
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
242
240
|
}
|
|
243
|
-
else {
|
|
244
|
-
log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
|
|
245
|
-
}
|
|
246
|
-
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
247
|
-
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
248
|
-
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
249
241
|
// Build session context for AsyncLocalStorage
|
|
250
242
|
const sessionContext = {
|
|
251
243
|
config,
|
|
@@ -253,13 +245,14 @@ export async function handleXYMessage(params) {
|
|
|
253
245
|
taskId: parsed.taskId,
|
|
254
246
|
messageId: parsed.messageId,
|
|
255
247
|
agentId: route.accountId,
|
|
248
|
+
deviceType,
|
|
256
249
|
};
|
|
250
|
+
log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
257
251
|
await core.channel.reply.withReplyDispatcher({
|
|
258
252
|
dispatcher,
|
|
259
253
|
onSettled: () => {
|
|
260
254
|
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
261
255
|
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
262
|
-
markDispatchIdle();
|
|
263
256
|
// 🔑 减少引用计数
|
|
264
257
|
decrementTaskIdRef(parsed.sessionId);
|
|
265
258
|
// 🔑 如果是第一条消息完成,解锁
|
|
@@ -273,12 +266,32 @@ export async function handleXYMessage(params) {
|
|
|
273
266
|
},
|
|
274
267
|
run: () =>
|
|
275
268
|
// 🔐 Use AsyncLocalStorage to provide session context to tools
|
|
276
|
-
runWithSessionContext(sessionContext, () =>
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
269
|
+
runWithSessionContext(sessionContext, async () => {
|
|
270
|
+
log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
|
|
271
|
+
log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
272
|
+
log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
273
|
+
log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
274
|
+
log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
275
|
+
log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
276
|
+
try {
|
|
277
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
278
|
+
ctx: ctxPayload,
|
|
279
|
+
cfg,
|
|
280
|
+
dispatcher,
|
|
281
|
+
replyOptions,
|
|
282
|
+
});
|
|
283
|
+
log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
|
|
284
|
+
log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
catch (dispatchErr) {
|
|
288
|
+
error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
|
|
289
|
+
error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
290
|
+
error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
291
|
+
error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
292
|
+
throw dispatchErr;
|
|
293
|
+
}
|
|
294
|
+
}),
|
|
282
295
|
});
|
|
283
296
|
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
284
297
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
package/dist/src/channel.js
CHANGED
|
@@ -21,9 +21,17 @@ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
|
|
|
21
21
|
import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
|
|
22
22
|
import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
|
|
23
23
|
import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
24
|
-
import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
|
|
25
24
|
import { viewPushResultTool } from "./tools/view-push-result-tool.js";
|
|
26
25
|
import { imageReadingTool } from "./tools/image-reading-tool.js";
|
|
26
|
+
import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
|
|
27
|
+
import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
|
|
28
|
+
import { xiaoyiAddCollectionTool } from "./tools/xiaoyi-add-collection-tool.js";
|
|
29
|
+
import { xiaoyiDeleteCollectionTool } from "./tools/xiaoyi-delete-collection-tool.js";
|
|
30
|
+
import { saveMediaToGalleryTool } from "./tools/save-media-to-gallery-tool.js";
|
|
31
|
+
import { saveFileToPhoneTool } from "./tools/save-file-to-phone-tool.js";
|
|
32
|
+
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
33
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
34
|
+
import { logger } from "./utils/logger.js";
|
|
27
35
|
/**
|
|
28
36
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
29
37
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -62,7 +70,13 @@ export const xyPlugin = {
|
|
|
62
70
|
schema: xyConfigSchema,
|
|
63
71
|
},
|
|
64
72
|
outbound: xyOutbound,
|
|
65
|
-
agentTools:
|
|
73
|
+
agentTools: () => {
|
|
74
|
+
const allTools = [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, xiaoyiCollectionTool, xiaoyiAddCollectionTool, xiaoyiDeleteCollectionTool, saveMediaToGalleryTool, saveFileToPhoneTool];
|
|
75
|
+
const ctx = getCurrentSessionContext();
|
|
76
|
+
const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
|
|
77
|
+
logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
78
|
+
return filtered;
|
|
79
|
+
},
|
|
66
80
|
messaging: {
|
|
67
81
|
normalizeTarget: (raw) => {
|
|
68
82
|
const trimmed = raw.trim();
|
package/dist/src/client.js
CHANGED
|
@@ -72,14 +72,9 @@ export function getCachedManagerCount() {
|
|
|
72
72
|
* Helps identify connection issues and orphan connections.
|
|
73
73
|
*/
|
|
74
74
|
export function diagnoseAllManagers() {
|
|
75
|
-
console.log("========================================");
|
|
76
|
-
console.log("📊 WebSocket Manager Global Diagnostics");
|
|
77
|
-
console.log("========================================");
|
|
78
75
|
console.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
79
|
-
console.log("");
|
|
80
76
|
if (wsManagerCache.size === 0) {
|
|
81
77
|
console.log("ℹ️ No managers in cache");
|
|
82
|
-
console.log("========================================");
|
|
83
78
|
return;
|
|
84
79
|
}
|
|
85
80
|
let orphanCount = 0;
|
|
@@ -108,7 +103,6 @@ export function diagnoseAllManagers() {
|
|
|
108
103
|
else {
|
|
109
104
|
console.log(`✅ No orphan connections found`);
|
|
110
105
|
}
|
|
111
|
-
console.log("========================================");
|
|
112
106
|
}
|
|
113
107
|
/**
|
|
114
108
|
* Clean up orphan connections across all managers.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// SENTINEL HOOK API 请求模块
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -8,8 +8,10 @@ function generateTraceId() {
|
|
|
8
8
|
return randomBytes(16).toString("hex");
|
|
9
9
|
}
|
|
10
10
|
function buildHeaders(config) {
|
|
11
|
+
const traceId = generateTraceId();
|
|
12
|
+
console.log(`[SENTINEL HOOK] trace-id: ${traceId}`);
|
|
11
13
|
return {
|
|
12
|
-
"x-hag-trace-id":
|
|
14
|
+
"x-hag-trace-id": traceId,
|
|
13
15
|
"x-uid": config.uid,
|
|
14
16
|
"x-api-key": config.apiKey,
|
|
15
17
|
"x-request-from": config.requestFrom,
|
|
@@ -30,13 +32,13 @@ function buildRequestOptions(url, headers, timeout) {
|
|
|
30
32
|
}
|
|
31
33
|
function parseResponse(data) {
|
|
32
34
|
if (!data?.trim())
|
|
33
|
-
throw new Error("[
|
|
35
|
+
throw new Error("[SENTINEL HOOK] API response is empty");
|
|
34
36
|
const json = JSON.parse(data);
|
|
35
37
|
if (json.retCode && json.retCode !== "0") {
|
|
36
|
-
throw new Error(`[
|
|
38
|
+
throw new Error(`[SENTINEL HOOK] API error: ${json.retMsg || "unknown"}`);
|
|
37
39
|
}
|
|
38
40
|
if (!json.retCode && json.code) {
|
|
39
|
-
throw new Error(`[
|
|
41
|
+
throw new Error(`[SENTINEL HOOK] Backend error: ${json.desc || "unknown"}`);
|
|
40
42
|
}
|
|
41
43
|
return json;
|
|
42
44
|
}
|
|
@@ -47,30 +49,13 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
47
49
|
questionText,
|
|
48
50
|
textSource: config.textSource,
|
|
49
51
|
action: config.action,
|
|
52
|
+
extra: JSON.stringify({ userId: config.uid }),
|
|
50
53
|
};
|
|
51
|
-
// 打印请求信息
|
|
52
|
-
console.log(`[CSPL API] ==================== 发起请求 ====================`);
|
|
53
|
-
console.log(`[CSPL API] URL: ${config.api.url}`);
|
|
54
|
-
console.log(`[CSPL API] Method: POST`);
|
|
55
|
-
console.log(`[CSPL API] Headers:`);
|
|
56
|
-
console.log(`[CSPL API] - x-hag-trace-id: ${headers["x-hag-trace-id"]}`);
|
|
57
|
-
console.log(`[CSPL API] - x-uid: ${headers["x-uid"]}`);
|
|
58
|
-
console.log(`[CSPL API] - x-api-key: ${headers["x-api-key"] ? "***" + headers["x-api-key"].slice(-8) : "undefined"}`);
|
|
59
|
-
console.log(`[CSPL API] - x-request-from: ${headers["x-request-from"]}`);
|
|
60
|
-
console.log(`[CSPL API] - x-skill-id: ${headers["x-skill-id"]}`);
|
|
61
|
-
console.log(`[CSPL API] - content-type: ${headers["content-type"]}`);
|
|
62
|
-
console.log(`[CSPL API] Body:`);
|
|
63
|
-
console.log(`[CSPL API] - questionText: ${questionText.substring(0, 100)}${questionText.length > 100 ? "..." : ""}`);
|
|
64
|
-
console.log(`[CSPL API] - textSource: ${payload.textSource}`);
|
|
65
|
-
console.log(`[CSPL API] - action: ${payload.action}`);
|
|
66
|
-
console.log(`[CSPL API] =================================================`);
|
|
67
54
|
return new Promise((resolve, reject) => {
|
|
68
55
|
const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
|
|
69
56
|
const req = https.request(options, (res) => {
|
|
70
|
-
console.log(`[CSPL API] Response Status: ${res.statusCode}`);
|
|
71
|
-
console.log(`[CSPL API] Response Headers: ${JSON.stringify(res.headers)}`);
|
|
72
57
|
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
73
|
-
reject(new Error(`[
|
|
58
|
+
reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
|
|
74
59
|
return;
|
|
75
60
|
}
|
|
76
61
|
let data = "";
|
|
@@ -80,26 +65,23 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
80
65
|
res.on("end", () => {
|
|
81
66
|
try {
|
|
82
67
|
const result = parseResponse(data);
|
|
83
|
-
console.log(`[
|
|
84
|
-
console.log(`[CSPL API] Response Body: ${data.substring(0, 200)}${data.length > 200 ? "..." : ""}`);
|
|
85
|
-
console.log(`[CSPL API] =================================================`);
|
|
68
|
+
console.log(`[SENTINEL HOOK] ✅ 请求成功`);
|
|
86
69
|
resolve(result);
|
|
87
70
|
}
|
|
88
71
|
catch (e) {
|
|
89
|
-
console.error(`[
|
|
90
|
-
console.error(`[CSPL API] Response Body: ${data}`);
|
|
72
|
+
console.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
91
73
|
reject(e);
|
|
92
74
|
}
|
|
93
75
|
});
|
|
94
76
|
});
|
|
95
77
|
req.on("error", (error) => {
|
|
96
|
-
console.error(`[
|
|
78
|
+
console.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
79
|
reject(error);
|
|
98
80
|
});
|
|
99
81
|
req.on("timeout", () => {
|
|
100
|
-
console.error(`[
|
|
82
|
+
console.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
|
|
101
83
|
req.destroy();
|
|
102
|
-
reject(new Error("[
|
|
84
|
+
reject(new Error("[SENTINEL HOOK] Request timeout"));
|
|
103
85
|
});
|
|
104
86
|
req.write(JSON.stringify(payload));
|
|
105
87
|
req.end();
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -7,7 +7,7 @@ import { logger } from "../utils/logger.js";
|
|
|
7
7
|
let cachedConfig = null;
|
|
8
8
|
function readServiceUrl() {
|
|
9
9
|
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
10
|
-
throw new Error(`[
|
|
10
|
+
throw new Error(`[SENTINEL HOOK] Environment file not found: ${ENV_FILE_PATH}`);
|
|
11
11
|
}
|
|
12
12
|
const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
|
|
13
13
|
for (const line of envData.split("\n")) {
|
|
@@ -22,7 +22,7 @@ function readServiceUrl() {
|
|
|
22
22
|
if (key === "SERVICE_URL" && value)
|
|
23
23
|
return value;
|
|
24
24
|
}
|
|
25
|
-
throw new Error("[
|
|
25
|
+
throw new Error("[SENTINEL HOOK] Missing SERVICE_URL in env file");
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
@@ -45,6 +45,6 @@ export function getCsplConfig(cfg) {
|
|
|
45
45
|
textSource: CSPL_STATIC_CONFIG.textSource,
|
|
46
46
|
action: CSPL_STATIC_CONFIG.action,
|
|
47
47
|
};
|
|
48
|
-
logger.log("[
|
|
48
|
+
logger.log("[SENTINEL HOOK] Config loaded (uid/apiKey from XYChannelConfig)");
|
|
49
49
|
return cachedConfig;
|
|
50
50
|
}
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { logger } from "./utils/logger.js";
|
|
6
5
|
/**
|
|
7
6
|
* Download a file from URL to local path.
|
|
8
7
|
*/
|
|
9
8
|
export async function downloadFile(url, destPath) {
|
|
10
|
-
|
|
9
|
+
console.log(`Downloading file from ${url} to ${destPath}`);
|
|
11
10
|
const controller = new AbortController();
|
|
12
11
|
const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds timeout
|
|
13
12
|
try {
|
|
@@ -18,14 +17,14 @@ export async function downloadFile(url, destPath) {
|
|
|
18
17
|
const arrayBuffer = await response.arrayBuffer();
|
|
19
18
|
const buffer = Buffer.from(arrayBuffer);
|
|
20
19
|
await fs.writeFile(destPath, buffer);
|
|
21
|
-
|
|
20
|
+
console.log(`File downloaded successfully: ${destPath}`);
|
|
22
21
|
}
|
|
23
22
|
catch (error) {
|
|
24
23
|
if (error.name === 'AbortError') {
|
|
25
|
-
|
|
24
|
+
console.log(`Download timeout (30s) for ${url}`);
|
|
26
25
|
throw new Error(`Download timeout after 30 seconds`);
|
|
27
26
|
}
|
|
28
|
-
|
|
27
|
+
console.log(`Failed to download file from ${url}:`, error);
|
|
29
28
|
throw error;
|
|
30
29
|
}
|
|
31
30
|
finally {
|
|
@@ -54,7 +53,7 @@ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_chann
|
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
55
|
catch (error) {
|
|
57
|
-
|
|
56
|
+
console.log(`Failed to download file ${name}:`, error);
|
|
58
57
|
// Continue with other files
|
|
59
58
|
}
|
|
60
59
|
}
|
package/dist/src/file-upload.js
CHANGED
|
@@ -55,12 +55,10 @@ export class XYFileUploadService {
|
|
|
55
55
|
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
56
56
|
}
|
|
57
57
|
const prepareData = await prepareResp.json();
|
|
58
|
-
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
59
58
|
if (prepareData.code !== "0") {
|
|
60
59
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
61
60
|
}
|
|
62
61
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
63
|
-
console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
64
62
|
// Phase 2: Upload
|
|
65
63
|
console.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
66
64
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
@@ -69,11 +67,8 @@ export class XYFileUploadService {
|
|
|
69
67
|
headers: uploadInfo.headers,
|
|
70
68
|
body: fileBuffer,
|
|
71
69
|
});
|
|
72
|
-
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}, url: ${uploadInfo.url}`);
|
|
73
|
-
console.log(`[XY File Upload] Upload response headers:`, JSON.stringify(Object.fromEntries(uploadResp.headers.entries()), null, 2));
|
|
74
70
|
if (!uploadResp.ok) {
|
|
75
71
|
const uploadErrorText = await uploadResp.text();
|
|
76
|
-
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
77
72
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
78
73
|
}
|
|
79
74
|
console.log(`[XY File Upload] Upload complete`);
|
|
@@ -96,7 +91,6 @@ export class XYFileUploadService {
|
|
|
96
91
|
throw new Error(`Complete failed: HTTP ${completeResp.status}`);
|
|
97
92
|
}
|
|
98
93
|
const completeData = await completeResp.json();
|
|
99
|
-
console.log(`[XY File Upload] Complete response:`, JSON.stringify(completeData, null, 2));
|
|
100
94
|
console.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
|
|
101
95
|
return objectId;
|
|
102
96
|
}
|
|
@@ -110,7 +104,6 @@ export class XYFileUploadService {
|
|
|
110
104
|
* Uses completeAndQuery endpoint to get the file URL directly.
|
|
111
105
|
*/
|
|
112
106
|
async uploadFileAndGetUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
113
|
-
console.log(`[XY File Upload] Starting file upload with URL retrieval: ${filePath}`);
|
|
114
107
|
try {
|
|
115
108
|
// Read file
|
|
116
109
|
const fileBuffer = await fs.readFile(filePath);
|
|
@@ -143,7 +136,6 @@ export class XYFileUploadService {
|
|
|
143
136
|
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
144
137
|
}
|
|
145
138
|
const prepareData = await prepareResp.json();
|
|
146
|
-
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
147
139
|
if (prepareData.code !== "0") {
|
|
148
140
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
149
141
|
}
|
|
@@ -160,7 +152,6 @@ export class XYFileUploadService {
|
|
|
160
152
|
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
|
|
161
153
|
if (!uploadResp.ok) {
|
|
162
154
|
const uploadErrorText = await uploadResp.text();
|
|
163
|
-
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
164
155
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
165
156
|
}
|
|
166
157
|
console.log(`[XY File Upload] Upload complete`);
|
|
@@ -183,13 +174,12 @@ export class XYFileUploadService {
|
|
|
183
174
|
throw new Error(`CompleteAndQuery failed: HTTP ${completeResp.status}`);
|
|
184
175
|
}
|
|
185
176
|
const completeData = await completeResp.json();
|
|
186
|
-
console.log(`[XY File Upload] CompleteAndQuery response:`, JSON.stringify(completeData, null, 2));
|
|
187
177
|
// Extract file URL from response
|
|
188
178
|
const fileUrl = completeData?.fileDetailInfo?.url || "";
|
|
189
179
|
if (!fileUrl) {
|
|
190
180
|
throw new Error("No file URL returned from completeAndQuery");
|
|
191
181
|
}
|
|
192
|
-
console.log(`[XY File Upload] File upload successful
|
|
182
|
+
console.log(`[XY File Upload] File upload successful`);
|
|
193
183
|
return fileUrl;
|
|
194
184
|
}
|
|
195
185
|
catch (error) {
|
package/dist/src/formatter.d.ts
CHANGED
package/dist/src/formatter.js
CHANGED
|
@@ -6,10 +6,10 @@ import { getXYRuntime } from "./runtime.js";
|
|
|
6
6
|
* Send an A2A artifact update response.
|
|
7
7
|
*/
|
|
8
8
|
export async function sendA2AResponse(params) {
|
|
9
|
-
const { config, sessionId, taskId, messageId, text, append, final, files } = params;
|
|
9
|
+
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage } = params;
|
|
10
10
|
const runtime = getXYRuntime();
|
|
11
11
|
const log = runtime?.log ?? console.log;
|
|
12
|
-
const
|
|
12
|
+
const errorFn = runtime?.error ?? console.error;
|
|
13
13
|
// Build artifact update event
|
|
14
14
|
const artifact = {
|
|
15
15
|
taskId,
|
|
@@ -42,6 +42,14 @@ export async function sendA2AResponse(params) {
|
|
|
42
42
|
id: messageId,
|
|
43
43
|
result: artifact,
|
|
44
44
|
};
|
|
45
|
+
// 🔑 添加 error 字段(仅当提供 errorCode 时)
|
|
46
|
+
if (errorCode !== undefined) {
|
|
47
|
+
jsonRpcResponse.error = {
|
|
48
|
+
code: errorCode,
|
|
49
|
+
message: errorMessage ?? "任务执行异常,请重试",
|
|
50
|
+
};
|
|
51
|
+
log(`[A2A_RESPONSE] ⚠️ Including error code: ${errorCode}`);
|
|
52
|
+
}
|
|
45
53
|
// Send via WebSocket
|
|
46
54
|
const wsManager = getXYWebSocketManager(config);
|
|
47
55
|
const outboundMessage = {
|
|
@@ -55,7 +63,7 @@ export async function sendA2AResponse(params) {
|
|
|
55
63
|
log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response: taskId: ${taskId}`);
|
|
56
64
|
log(`[A2A_RESPONSE] - append: ${append}`);
|
|
57
65
|
log(`[A2A_RESPONSE] - final: ${final}`);
|
|
58
|
-
log(`[A2A_RESPONSE] - text: ${text}`);
|
|
66
|
+
log(`[A2A_RESPONSE] - text: ${text.length <= 10 ? text : text.slice(0, 5) + '***' + text.slice(-5)}`);
|
|
59
67
|
log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
|
|
60
68
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
61
69
|
log(`[A2A_RESPONSE] ✅ Message sent successfully`);
|
|
@@ -146,11 +154,8 @@ export async function sendStatusUpdate(params) {
|
|
|
146
154
|
// 📋 Log complete response body
|
|
147
155
|
log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
|
|
148
156
|
log(`[A2A_STATUS] - taskId: ${taskId}`);
|
|
149
|
-
log(`[A2A_STATUS] - messageId: ${messageId}`);
|
|
150
|
-
log(`[A2A_STATUS] - state: ${state}`);
|
|
151
157
|
log(`[A2A_STATUS] - text: "${text}"`);
|
|
152
158
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
153
|
-
log(`[A2A_STATUS] ✅ Status update sent successfully`);
|
|
154
159
|
}
|
|
155
160
|
/**
|
|
156
161
|
* Send a command as an artifact update (final=false).
|