@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.
Files changed (50) hide show
  1. package/dist/index.js +7 -4
  2. package/dist/src/bot.d.ts +1 -0
  3. package/dist/src/bot.js +45 -32
  4. package/dist/src/channel.js +16 -2
  5. package/dist/src/client.js +0 -6
  6. package/dist/src/cspl/call-api.js +14 -32
  7. package/dist/src/cspl/config.js +3 -3
  8. package/dist/src/cspl/constants.d.ts +1 -0
  9. package/dist/src/file-download.js +5 -6
  10. package/dist/src/file-upload.js +1 -11
  11. package/dist/src/formatter.d.ts +2 -0
  12. package/dist/src/formatter.js +11 -6
  13. package/dist/src/monitor.js +8 -10
  14. package/dist/src/outbound.js +1 -19
  15. package/dist/src/parser.d.ts +6 -0
  16. package/dist/src/parser.js +16 -0
  17. package/dist/src/provider.d.ts +2 -0
  18. package/dist/src/provider.js +124 -0
  19. package/dist/src/push.js +0 -21
  20. package/dist/src/reply-dispatcher.d.ts +4 -0
  21. package/dist/src/reply-dispatcher.js +15 -11
  22. package/dist/src/tools/call-phone-tool.js +2 -18
  23. package/dist/src/tools/create-alarm-tool.js +3 -7
  24. package/dist/src/tools/device-tool-map.d.ts +4 -0
  25. package/dist/src/tools/device-tool-map.js +35 -0
  26. package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
  27. package/dist/src/tools/find-pc-devices-tool.js +98 -0
  28. package/dist/src/tools/image-reading-tool.js +3 -3
  29. package/dist/src/tools/modify-alarm-tool.js +3 -7
  30. package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
  31. package/dist/src/tools/save-file-to-phone-tool.js +170 -0
  32. package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
  33. package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
  34. package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
  35. package/dist/src/tools/send-command-to-car-tool.js +85 -0
  36. package/dist/src/tools/send-file-to-user-tool.js +1 -0
  37. package/dist/src/tools/session-manager.d.ts +1 -0
  38. package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
  39. package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
  40. package/dist/src/tools/upload-file-tool.js +2 -2
  41. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
  42. package/dist/src/tools/xiaoyi-add-collection-tool.js +188 -0
  43. package/dist/src/tools/xiaoyi-collection-tool.js +42 -7
  44. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
  45. package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
  46. package/dist/src/utils/runtime-manager.d.ts +7 -0
  47. package/dist/src/utils/runtime-manager.js +42 -0
  48. package/dist/src/websocket.js +15 -9
  49. package/openclaw.plugin.json +1 -0
  50. 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
- // CSPL after_tool_call hook: 监听工具结果,发送至 CSPL API 进行安全检测
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(`[CSPL] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
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(`[CSPL] Security result: status=${result.status}`);
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(`[CSPL] after_tool_call error: ${err}`);
59
+ api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
57
60
  }
58
61
  });
59
62
  },
package/dist/src/bot.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface HandleXYMessageParams {
8
8
  runtime: RuntimeEnv;
9
9
  message: A2AJsonRpcRequest;
10
10
  accountId: string;
11
+ webSocketSessionId?: string;
11
12
  }
12
13
  /**
13
14
  * Handle an incoming A2A message.
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, () => core.channel.reply.dispatchReplyFromConfig({
277
- ctx: ctxPayload,
278
- cfg,
279
- dispatcher,
280
- replyOptions,
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})`);
@@ -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: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, xiaoyiCollectionTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool],
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();
@@ -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
- // CSPL API 请求模块
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": generateTraceId(),
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("[CSPL] API response is empty");
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(`[CSPL] API error: ${json.retMsg || "unknown"}`);
38
+ throw new Error(`[SENTINEL HOOK] API error: ${json.retMsg || "unknown"}`);
37
39
  }
38
40
  if (!json.retCode && json.code) {
39
- throw new Error(`[CSPL] Backend error: ${json.desc || "unknown"}`);
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(`[CSPL] HTTP error: ${res.statusCode}`));
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(`[CSPL API] ✅ 请求成功`);
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(`[CSPL API] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
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(`[CSPL API] ❌ 请求错误: ${error instanceof Error ? error.message : String(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(`[CSPL API] ⏰ 请求超时 (${config.api.timeout}ms)`);
82
+ console.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
101
83
  req.destroy();
102
- reject(new Error("[CSPL] Request timeout"));
84
+ reject(new Error("[SENTINEL HOOK] Request timeout"));
103
85
  });
104
86
  req.write(JSON.stringify(payload));
105
87
  req.end();
@@ -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(`[CSPL] Environment file not found: ${ENV_FILE_PATH}`);
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("[CSPL] Missing SERVICE_URL in env file");
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("[CSPL] Config loaded (uid/apiKey from XYChannelConfig)");
48
+ logger.log("[SENTINEL HOOK] Config loaded (uid/apiKey from XYChannelConfig)");
49
49
  return cachedConfig;
50
50
  }
@@ -10,6 +10,7 @@ export interface ApiPayload {
10
10
  questionText: string;
11
11
  textSource: string;
12
12
  action: string;
13
+ extra: string;
13
14
  }
14
15
  export interface ApiResponse {
15
16
  data?: {
@@ -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
- logger.debug(`Downloading file from ${url} to ${destPath}`);
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
- logger.debug(`File downloaded successfully: ${destPath}`);
20
+ console.log(`File downloaded successfully: ${destPath}`);
22
21
  }
23
22
  catch (error) {
24
23
  if (error.name === 'AbortError') {
25
- logger.error(`Download timeout (30s) for ${url}`);
24
+ console.log(`Download timeout (30s) for ${url}`);
26
25
  throw new Error(`Download timeout after 30 seconds`);
27
26
  }
28
- logger.error(`Failed to download file from ${url}:`, error);
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
- logger.error(`Failed to download file ${name}:`, error);
56
+ console.log(`Failed to download file ${name}:`, error);
58
57
  // Continue with other files
59
58
  }
60
59
  }
@@ -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: ${fileName} → URL=${fileUrl}`);
182
+ console.log(`[XY File Upload] File upload successful`);
193
183
  return fileUrl;
194
184
  }
195
185
  catch (error) {
@@ -15,6 +15,8 @@ export interface SendA2AResponseParams {
15
15
  fileType: string;
16
16
  fileId: string;
17
17
  }>;
18
+ errorCode?: number | string;
19
+ errorMessage?: string;
18
20
  }
19
21
  /**
20
22
  * Send an A2A artifact update response.
@@ -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 error = runtime?.error ?? console.error;
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).