@ynhcj/xiaoyi-channel 0.0.18-beta → 0.0.18-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 (79) hide show
  1. package/dist/index.js +40 -0
  2. package/dist/src/bot.js +123 -41
  3. package/dist/src/channel.js +17 -4
  4. package/dist/src/client.js +11 -24
  5. package/dist/src/config.js +2 -2
  6. package/dist/src/cspl/call-api.d.ts +3 -0
  7. package/dist/src/cspl/call-api.js +79 -0
  8. package/dist/src/cspl/config.d.ts +19 -0
  9. package/dist/src/cspl/config.js +50 -0
  10. package/dist/src/cspl/constants.d.ts +43 -0
  11. package/dist/src/cspl/constants.js +22 -0
  12. package/dist/src/cspl/utils.d.ts +10 -0
  13. package/dist/src/cspl/utils.js +57 -0
  14. package/dist/src/file-upload.d.ts +5 -0
  15. package/dist/src/file-upload.js +92 -0
  16. package/dist/src/formatter.d.ts +14 -0
  17. package/dist/src/formatter.js +46 -2
  18. package/dist/src/monitor.js +47 -5
  19. package/dist/src/outbound.js +52 -6
  20. package/dist/src/parser.d.ts +7 -0
  21. package/dist/src/parser.js +22 -0
  22. package/dist/src/push.d.ts +8 -1
  23. package/dist/src/push.js +30 -22
  24. package/dist/src/reply-dispatcher.d.ts +1 -0
  25. package/dist/src/reply-dispatcher.js +116 -101
  26. package/dist/src/steer-injector.d.ts +16 -0
  27. package/dist/src/steer-injector.js +82 -0
  28. package/dist/src/task-manager.d.ts +55 -0
  29. package/dist/src/task-manager.js +136 -0
  30. package/dist/src/tools/calendar-tool.js +5 -3
  31. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  32. package/dist/src/tools/call-phone-tool.js +183 -0
  33. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  34. package/dist/src/tools/create-alarm-tool.js +420 -0
  35. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  36. package/dist/src/tools/delete-alarm-tool.js +216 -0
  37. package/dist/src/tools/image-reading-tool.d.ts +5 -0
  38. package/dist/src/tools/image-reading-tool.js +375 -0
  39. package/dist/src/tools/location-tool.js +8 -11
  40. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  41. package/dist/src/tools/modify-alarm-tool.js +439 -0
  42. package/dist/src/tools/modify-note-tool.js +4 -9
  43. package/dist/src/tools/note-tool.js +32 -21
  44. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  45. package/dist/src/tools/search-alarm-tool.js +343 -0
  46. package/dist/src/tools/search-calendar-tool.js +9 -46
  47. package/dist/src/tools/search-contact-tool.js +4 -27
  48. package/dist/src/tools/search-file-tool.d.ts +5 -0
  49. package/dist/src/tools/search-file-tool.js +161 -0
  50. package/dist/src/tools/search-message-tool.d.ts +5 -0
  51. package/dist/src/tools/search-message-tool.js +149 -0
  52. package/dist/src/tools/search-note-tool.js +29 -22
  53. package/dist/src/tools/search-photo-gallery-tool.js +51 -34
  54. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  55. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  56. package/dist/src/tools/send-message-tool.d.ts +5 -0
  57. package/dist/src/tools/send-message-tool.js +176 -0
  58. package/dist/src/tools/session-manager.d.ts +15 -0
  59. package/dist/src/tools/session-manager.js +99 -18
  60. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  61. package/dist/src/tools/upload-file-tool.js +265 -0
  62. package/dist/src/tools/upload-photo-tool.js +14 -4
  63. package/dist/src/tools/view-push-result-tool.d.ts +5 -0
  64. package/dist/src/tools/view-push-result-tool.js +118 -0
  65. package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
  66. package/dist/src/tools/xiaoyi-collection-tool.js +190 -0
  67. package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
  68. package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
  69. package/dist/src/trigger-handler.d.ts +22 -0
  70. package/dist/src/trigger-handler.js +59 -0
  71. package/dist/src/types.d.ts +1 -8
  72. package/dist/src/types.js +4 -0
  73. package/dist/src/utils/pushdata-manager.d.ts +28 -0
  74. package/dist/src/utils/pushdata-manager.js +171 -0
  75. package/dist/src/utils/pushid-manager.d.ts +12 -0
  76. package/dist/src/utils/pushid-manager.js +105 -0
  77. package/dist/src/websocket.d.ts +26 -31
  78. package/dist/src/websocket.js +227 -267
  79. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
2
  import { xyPlugin } from "./src/channel.js";
3
3
  import { setXYRuntime } from "./src/runtime.js";
4
+ import { tryInjectSteer } from "./src/steer-injector.js";
5
+ import { callCsplApi } from "./src/cspl/call-api.js";
6
+ import { extractResultText, processText, parseSecurityResult, validateAndTruncateText } from "./src/cspl/utils.js";
7
+ import { ALLOWED_TOOLS, MIN_TEXT_LENGTH, MAX_TOTAL_LENGTH, MAX_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
4
8
  /**
5
9
  * Xiaoyi Channel Plugin Entry Point.
6
10
  * Exports the plugin for OpenClaw to load.
@@ -14,6 +18,42 @@ const plugin = {
14
18
  register(api) {
15
19
  setXYRuntime(api.runtime);
16
20
  api.registerChannel({ plugin: xyPlugin });
21
+ // CSPL after_tool_call hook: 监听工具结果,发送至 CSPL API 进行安全检测
22
+ // 如果响应为 REJECT,注入 steer 消息中止当前对话
23
+ api.on("after_tool_call", async (event, ctx) => {
24
+ if (!ALLOWED_TOOLS.includes(event.toolName)) {
25
+ return;
26
+ }
27
+ try {
28
+ const resultText = extractResultText(event, event.toolName);
29
+ const resultLength = resultText.length;
30
+ if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
31
+ return;
32
+ }
33
+ // 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
34
+ const questionText = {
35
+ tool: event.toolName,
36
+ output: [{ content: "" }],
37
+ };
38
+ const originText = processText(resultText);
39
+ questionText.output[0].content = originText;
40
+ let finalJson = JSON.stringify(questionText);
41
+ if (finalJson.length > MAX_TEXT_LENGTH) {
42
+ const diff = finalJson.length - MAX_TEXT_LENGTH;
43
+ const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
44
+ questionText.output[0].content = trimmed;
45
+ finalJson = JSON.stringify(questionText);
46
+ }
47
+ const response = await callCsplApi(finalJson, api.config);
48
+ const result = parseSecurityResult(response);
49
+ if (result.status === "REJECT") {
50
+ await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
51
+ }
52
+ }
53
+ catch (err) {
54
+ api.logger.error(`[CSPL] after_tool_call error: ${err}`);
55
+ }
56
+ });
17
57
  },
18
58
  };
19
59
  export default plugin;
package/dist/src/bot.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
+ import { setCachedContext } from "./steer-injector.js";
2
3
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
3
- import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
4
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
4
5
  import { downloadFilesFromParts } from "./file-download.js";
5
6
  import { resolveXYConfig } from "./config.js";
6
- import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
- import { registerSession, unregisterSession } from "./tools/session-manager.js";
7
+ import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
8
+ import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
8
9
  import { configManager } from "./utils/config-manager.js";
10
+ import { addPushId } from "./utils/pushid-manager.js";
11
+ import { getPushDataById } from "./utils/pushdata-manager.js";
12
+ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
9
13
  /**
10
14
  * Handle an incoming A2A message.
11
15
  * This is the main entry point for message processing.
@@ -15,6 +19,8 @@ export async function handleXYMessage(params) {
15
19
  const { cfg, runtime, message, accountId } = params;
16
20
  const log = runtime?.log ?? console.log;
17
21
  const error = runtime?.error ?? console.error;
22
+ // 每次收到消息时更新缓存,供 steer 注入使用
23
+ setCachedContext(cfg, runtime, accountId);
18
24
  // Get runtime (already validated in monitor.ts, but get reference for use)
19
25
  const core = getXYRuntime();
20
26
  try {
@@ -56,6 +62,60 @@ export async function handleXYMessage(params) {
56
62
  }
57
63
  // Parse the A2A message (for regular messages)
58
64
  const parsed = parseA2AMessage(message);
65
+ // ========== 检测 Trigger 消息 ==========
66
+ // 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
67
+ const triggerData = extractTriggerData(parsed.parts);
68
+ if (triggerData) {
69
+ log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
70
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
71
+ log(`[BOT] - Task ID: ${parsed.taskId}`);
72
+ try {
73
+ // 读取 pushData
74
+ const pushDataItem = await getPushDataById(triggerData.pushDataId);
75
+ if (!pushDataItem) {
76
+ error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
77
+ return;
78
+ }
79
+ log(`[BOT] ✅ Found pushData, sending direct response`);
80
+ log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
81
+ log(`[BOT] - time: ${pushDataItem.time}`);
82
+ log(`[BOT] - content length: ${pushDataItem.dataDetail.length} chars`);
83
+ const config = resolveXYConfig(cfg);
84
+ // 直接发送响应(final=true,不走 openclaw 流程)
85
+ await sendA2AResponse({
86
+ config,
87
+ sessionId: parsed.sessionId,
88
+ taskId: parsed.taskId,
89
+ messageId: parsed.messageId,
90
+ text: pushDataItem.dataDetail,
91
+ append: false,
92
+ final: true,
93
+ });
94
+ log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
95
+ return; // 提前返回,不继续处理
96
+ }
97
+ catch (err) {
98
+ error(`[BOT] ❌ Failed to handle Trigger message:`, err);
99
+ return;
100
+ }
101
+ }
102
+ // ========================================
103
+ // 🔑 检测steer模式和是否是第二条消息
104
+ const isSteerMode = cfg.messages?.queue?.mode === "steer";
105
+ const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
106
+ if (isSecondMessage) {
107
+ log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
108
+ log(`[BOT] - Session: ${parsed.sessionId}`);
109
+ log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
110
+ }
111
+ // 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
112
+ const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
113
+ );
114
+ // 🔑 如果是第一条消息,锁定taskId防止被过早清理
115
+ if (!isUpdate) {
116
+ lockTaskId(parsed.sessionId);
117
+ log(`[BOT] 🔒 Locked taskId for first message`);
118
+ }
59
119
  // Extract and update push_id if present
60
120
  const pushId = extractPushId(parsed.parts);
61
121
  if (pushId) {
@@ -64,6 +124,10 @@ export async function handleXYMessage(params) {
64
124
  log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
65
125
  log(`[BOT] - Full push_id: ${pushId}`);
66
126
  configManager.updatePushId(parsed.sessionId, pushId);
127
+ // 持久化 pushId 到本地文件(异步,不阻塞主流程)
128
+ addPushId(pushId).catch((err) => {
129
+ error(`[BOT] Failed to persist pushId:`, err);
130
+ });
67
131
  }
68
132
  else {
69
133
  log(`[BOT] ℹ️ No push_id found in message, will use config default`);
@@ -83,11 +147,12 @@ export async function handleXYMessage(params) {
83
147
  },
84
148
  });
85
149
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
86
- // Register session context for tools
150
+ // 🔑 注册session(带引用计数)
87
151
  log(`[BOT] 📝 About to register session for tools...`);
88
152
  log(`[BOT] - sessionKey: ${route.sessionKey}`);
89
153
  log(`[BOT] - sessionId: ${parsed.sessionId}`);
90
154
  log(`[BOT] - taskId: ${parsed.taskId}`);
155
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
91
156
  registerSession(route.sessionKey, {
92
157
  config,
93
158
  sessionId: parsed.sessionId,
@@ -96,14 +161,14 @@ export async function handleXYMessage(params) {
96
161
  agentId: route.accountId,
97
162
  });
98
163
  log(`[BOT] ✅ Session registered for tools`);
99
- // Send initial status update immediately after parsing message
164
+ // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
100
165
  log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
101
166
  void sendStatusUpdate({
102
167
  config,
103
168
  sessionId: parsed.sessionId,
104
169
  taskId: parsed.taskId,
105
170
  messageId: parsed.messageId,
106
- text: "任务正在处理中,请稍后~",
171
+ text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍候~",
107
172
  state: "working",
108
173
  }).catch((err) => {
109
174
  error(`Failed to send initial status update:`, err);
@@ -111,10 +176,9 @@ export async function handleXYMessage(params) {
111
176
  // Extract text and files from parts
112
177
  const text = extractTextFromParts(parsed.parts);
113
178
  const fileParts = extractFileParts(parsed.parts);
114
- // Download files if present (using core's media download)
115
- const mediaList = await downloadFilesFromParts(fileParts);
116
- // Build media payload for inbound context (following feishu pattern)
117
- const mediaPayload = buildXYMediaPayload(mediaList);
179
+ // Download files to local disk
180
+ const downloadedFiles = await downloadFilesFromParts(fileParts);
181
+ const mediaPayload = buildXYMediaPayload(downloadedFiles);
118
182
  // Resolve envelope format options (following feishu pattern)
119
183
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
120
184
  // Build message body with speaker prefix (following feishu pattern)
@@ -155,51 +219,66 @@ export async function handleXYMessage(params) {
155
219
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
156
220
  ...mediaPayload,
157
221
  });
158
- // Send initial status update immediately after parsing message
159
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
160
- void sendStatusUpdate({
161
- config,
162
- sessionId: parsed.sessionId,
163
- taskId: parsed.taskId,
164
- messageId: parsed.messageId,
165
- text: "任务正在处理中,请稍后~",
166
- state: "working",
167
- }).catch((err) => {
168
- error(`Failed to send initial status update:`, err);
169
- });
170
- // Create reply dispatcher (following feishu pattern)
171
- log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher for session=${parsed.sessionId}, taskId=${parsed.taskId}, messageId=${parsed.messageId}`);
222
+ // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
223
+ log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
224
+ log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
225
+ log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
226
+ log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
172
227
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
173
228
  cfg,
174
229
  runtime,
175
230
  sessionId: parsed.sessionId,
176
231
  taskId: parsed.taskId,
177
232
  messageId: parsed.messageId,
178
- accountId: route.accountId, // ✅ Use route.accountId
233
+ accountId: route.accountId,
234
+ isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
179
235
  });
180
236
  log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
181
- // Start status update interval (will send updates every 60 seconds)
182
- // Interval will be automatically stopped when onIdle/onCleanup is triggered
183
- startStatusInterval();
237
+ // 🔑 只有第一条消息启动状态定时器
238
+ // 第二条消息会很快返回,不需要定时器
239
+ if (!isSecondMessage) {
240
+ startStatusInterval();
241
+ log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
242
+ }
243
+ else {
244
+ log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
245
+ }
184
246
  log(`xy: dispatching to agent (session=${parsed.sessionId})`);
185
247
  // Dispatch to OpenClaw core using correct API (following feishu pattern)
186
248
  log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
249
+ // Build session context for AsyncLocalStorage
250
+ const sessionContext = {
251
+ config,
252
+ sessionId: parsed.sessionId,
253
+ taskId: parsed.taskId,
254
+ messageId: parsed.messageId,
255
+ agentId: route.accountId,
256
+ };
187
257
  await core.channel.reply.withReplyDispatcher({
188
258
  dispatcher,
189
259
  onSettled: () => {
190
260
  log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
191
- log(`[BOT] - About to unregister session...`);
261
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
192
262
  markDispatchIdle();
193
- // Unregister session context when done
263
+ // 🔑 减少引用计数
264
+ decrementTaskIdRef(parsed.sessionId);
265
+ // 🔑 如果是第一条消息完成,解锁
266
+ if (!isSecondMessage) {
267
+ unlockTaskId(parsed.sessionId);
268
+ log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
269
+ }
270
+ // 减少session引用计数
194
271
  unregisterSession(route.sessionKey);
195
- log(`[BOT] ✅ Session unregistered in onSettled`);
272
+ log(`[BOT] ✅ Cleanup completed`);
196
273
  },
197
- run: () => core.channel.reply.dispatchReplyFromConfig({
274
+ run: () =>
275
+ // 🔐 Use AsyncLocalStorage to provide session context to tools
276
+ runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
198
277
  ctx: ctxPayload,
199
278
  cfg,
200
279
  dispatcher,
201
280
  replyOptions,
202
- }),
281
+ })),
203
282
  });
204
283
  log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
205
284
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
@@ -209,25 +288,28 @@ export async function handleXYMessage(params) {
209
288
  error("Failed to handle XY message:", err);
210
289
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
211
290
  log(`[BOT] ❌ Error occurred, attempting cleanup...`);
212
- // Try to unregister session on error (if route was established)
291
+ // 🔑 错误时也要清理taskId和session
213
292
  try {
214
- const core = getXYRuntime();
215
293
  const params = message.params;
216
294
  const sessionId = params?.sessionId;
217
295
  if (sessionId) {
218
- log(`[BOT] 🧹 Cleaning up session after error: ${sessionId}`);
296
+ log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
297
+ // 清理 taskId
298
+ decrementTaskIdRef(sessionId);
299
+ unlockTaskId(sessionId);
300
+ // 清理 session
301
+ const core = getXYRuntime();
219
302
  const route = core.channel.routing.resolveAgentRoute({
220
303
  cfg,
221
304
  channel: "xiaoyi-channel",
222
305
  accountId,
223
306
  peer: {
224
307
  kind: "direct",
225
- id: sessionId, // ✅ Use sessionId for cleanup consistency
308
+ id: sessionId,
226
309
  },
227
310
  });
228
- log(`[BOT] - Unregistering session: ${route.sessionKey}`);
229
311
  unregisterSession(route.sessionKey);
230
- log(`[BOT] ✅ Session unregistered after error`);
312
+ log(`[BOT] ✅ Cleanup completed after error`);
231
313
  }
232
314
  }
233
315
  catch (cleanupErr) {
@@ -240,6 +322,8 @@ export async function handleXYMessage(params) {
240
322
  /**
241
323
  * Build media payload for inbound context.
242
324
  * Following feishu pattern: buildFeishuMediaPayload().
325
+ *
326
+ * @param mediaList - Downloaded files with local paths
243
327
  */
244
328
  function buildXYMediaPayload(mediaList) {
245
329
  const first = mediaList[0];
@@ -248,9 +332,7 @@ function buildXYMediaPayload(mediaList) {
248
332
  return {
249
333
  MediaPath: first?.path,
250
334
  MediaType: first?.mimeType,
251
- MediaUrl: first?.path,
252
335
  MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
253
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
254
336
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
255
337
  };
256
338
  }
@@ -11,6 +11,20 @@ import { searchCalendarTool } from "./tools/search-calendar-tool.js";
11
11
  import { searchContactTool } from "./tools/search-contact-tool.js";
12
12
  import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
13
13
  import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
14
+ import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
15
+ import { callPhoneTool } from "./tools/call-phone-tool.js";
16
+ import { searchMessageTool } from "./tools/search-message-tool.js";
17
+ import { sendMessageTool } from "./tools/send-message-tool.js";
18
+ import { searchFileTool } from "./tools/search-file-tool.js";
19
+ import { uploadFileTool } from "./tools/upload-file-tool.js";
20
+ import { createAlarmTool } from "./tools/create-alarm-tool.js";
21
+ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
22
+ import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
23
+ import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
24
+ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
25
+ import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
26
+ import { viewPushResultTool } from "./tools/view-push-result-tool.js";
27
+ import { imageReadingTool } from "./tools/image-reading-tool.js";
14
28
  /**
15
29
  * Xiaoyi Channel Plugin for OpenClaw.
16
30
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -50,7 +64,7 @@ export const xyPlugin = {
50
64
  },
51
65
  outbound: xyOutbound,
52
66
  onboarding: xyOnboardingAdapter,
53
- agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool],
67
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, xiaoyiCollectionTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool],
54
68
  messaging: {
55
69
  normalizeTarget: (raw) => {
56
70
  const trimmed = raw.trim();
@@ -77,10 +91,9 @@ export const xyPlugin = {
77
91
  const account = resolveXYConfig(context.cfg);
78
92
  context.setStatus?.({
79
93
  accountId: context.accountId,
80
- wsUrl1: account.wsUrl1,
81
- wsUrl2: account.wsUrl2,
94
+ wsUrl: account.wsUrl,
82
95
  });
83
- context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl1: ${account.wsUrl1}, wsUrl2: ${account.wsUrl2})`);
96
+ context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
84
97
  return monitorXYProvider({
85
98
  config: context.cfg,
86
99
  runtime: context.runtime,
@@ -89,29 +89,16 @@ export function diagnoseAllManagers() {
89
89
  console.log(`📌 Manager: ${key}`);
90
90
  console.log(` Shutting down: ${diag.isShuttingDown}`);
91
91
  console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
92
- // Server 1
93
- console.log(` 🔌 Server1:`);
94
- console.log(` - Exists: ${diag.server1.exists}`);
95
- console.log(` - ReadyState: ${diag.server1.readyState}`);
96
- console.log(` - State connected/ready: ${diag.server1.stateConnected}/${diag.server1.stateReady}`);
97
- console.log(` - Reconnect attempts: ${diag.server1.reconnectAttempts}`);
98
- console.log(` - Listeners on WebSocket: ${diag.server1.listenerCount}`);
99
- console.log(` - Heartbeat active: ${diag.server1.heartbeatActive}`);
100
- console.log(` - Has reconnect timer: ${diag.server1.hasReconnectTimer}`);
101
- if (diag.server1.isOrphan) {
102
- console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
103
- orphanCount++;
104
- }
105
- // Server 2
106
- console.log(` 🔌 Server2:`);
107
- console.log(` - Exists: ${diag.server2.exists}`);
108
- console.log(` - ReadyState: ${diag.server2.readyState}`);
109
- console.log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
110
- console.log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
111
- console.log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
112
- console.log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
113
- console.log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
114
- if (diag.server2.isOrphan) {
92
+ // Connection
93
+ console.log(` 🔌 Connection:`);
94
+ console.log(` - Exists: ${diag.connection.exists}`);
95
+ console.log(` - ReadyState: ${diag.connection.readyState}`);
96
+ console.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
97
+ console.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
98
+ console.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
99
+ console.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
100
+ console.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
101
+ if (diag.connection.isOrphan) {
115
102
  console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
116
103
  orphanCount++;
117
104
  }
@@ -134,7 +121,7 @@ export function cleanupOrphanConnections() {
134
121
  let cleanedCount = 0;
135
122
  wsManagerCache.forEach((manager, key) => {
136
123
  const diag = manager.getConnectionDiagnostics();
137
- if (diag.server1.isOrphan || diag.server2.isOrphan) {
124
+ if (diag.connection.isOrphan) {
138
125
  console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
139
126
  manager.disconnect();
140
127
  cleanedCount++;
@@ -17,8 +17,8 @@ export function resolveXYConfig(cfg) {
17
17
  // Return configuration with defaults
18
18
  return {
19
19
  enabled: xyConfig.enabled ?? false,
20
- wsUrl1: xyConfig.wsUrl1 ?? "ws://localhost:8765/ws/link",
21
- wsUrl2: xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
20
+ // ✅ 兼容旧配置:优先使用 wsUrl,然后 wsUrl2(wsUrl1 被忽略)
21
+ wsUrl: xyConfig.wsUrl ?? xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
22
22
  apiKey: xyConfig.apiKey,
23
23
  uid: xyConfig.uid,
24
24
  agentId: xyConfig.agentId,
@@ -0,0 +1,3 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import type { ApiResponse } from "./constants.js";
3
+ export declare function callCsplApi(questionText: string, cfg: ClawdbotConfig): Promise<ApiResponse>;
@@ -0,0 +1,79 @@
1
+ // CSPL API 请求模块
2
+ import https from "node:https";
3
+ import { URL } from "node:url";
4
+ import { randomBytes } from "node:crypto";
5
+ import { getCsplConfig } from "./config.js";
6
+ import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST } from "./constants.js";
7
+ function generateTraceId() {
8
+ return randomBytes(16).toString("hex");
9
+ }
10
+ function buildHeaders(config) {
11
+ return {
12
+ "x-hag-trace-id": generateTraceId(),
13
+ "x-uid": config.uid,
14
+ "x-api-key": config.apiKey,
15
+ "x-request-from": config.requestFrom,
16
+ "x-skill-id": config.skillId,
17
+ "content-type": "application/json",
18
+ };
19
+ }
20
+ function buildRequestOptions(url, headers, timeout) {
21
+ const urlObj = new URL(url);
22
+ return {
23
+ hostname: urlObj.hostname,
24
+ port: urlObj.port || DEFAULT_HTTP_PORT,
25
+ path: urlObj.pathname,
26
+ method: "POST",
27
+ headers: headers,
28
+ timeout,
29
+ };
30
+ }
31
+ function parseResponse(data) {
32
+ if (!data?.trim())
33
+ throw new Error("[CSPL] API response is empty");
34
+ const json = JSON.parse(data);
35
+ if (json.retCode && json.retCode !== "0") {
36
+ throw new Error(`[CSPL] API error: ${json.retMsg || "unknown"}`);
37
+ }
38
+ if (!json.retCode && json.code) {
39
+ throw new Error(`[CSPL] Backend error: ${json.desc || "unknown"}`);
40
+ }
41
+ return json;
42
+ }
43
+ export async function callCsplApi(questionText, cfg) {
44
+ const config = getCsplConfig(cfg);
45
+ const headers = buildHeaders(config);
46
+ const payload = {
47
+ questionText,
48
+ textSource: config.textSource,
49
+ action: config.action,
50
+ };
51
+ return new Promise((resolve, reject) => {
52
+ const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
53
+ const req = https.request(options, (res) => {
54
+ if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
55
+ reject(new Error(`[CSPL] HTTP error: ${res.statusCode}`));
56
+ return;
57
+ }
58
+ let data = "";
59
+ res.on("data", (chunk) => {
60
+ data += chunk;
61
+ });
62
+ res.on("end", () => {
63
+ try {
64
+ resolve(parseResponse(data));
65
+ }
66
+ catch (e) {
67
+ reject(e);
68
+ }
69
+ });
70
+ });
71
+ req.on("error", reject);
72
+ req.on("timeout", () => {
73
+ req.destroy();
74
+ reject(new Error("[CSPL] Request timeout"));
75
+ });
76
+ req.write(JSON.stringify(payload));
77
+ req.end();
78
+ });
79
+ }
@@ -0,0 +1,19 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ export interface ApiConfig {
3
+ url: string;
4
+ timeout: number;
5
+ }
6
+ export interface CsplConfig {
7
+ api: ApiConfig;
8
+ uid: string;
9
+ apiKey: string;
10
+ skillId: string;
11
+ requestFrom: string;
12
+ textSource: string;
13
+ action: string;
14
+ }
15
+ /**
16
+ * 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
17
+ * serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
18
+ */
19
+ export declare function getCsplConfig(cfg: ClawdbotConfig): CsplConfig;
@@ -0,0 +1,50 @@
1
+ // CSPL Hook 配置管理
2
+ // uid 和 apiKey 复用 XYChannelConfig,skillId 写死在常量中
3
+ import { resolveXYConfig } from "../config.js";
4
+ import { CSPL_STATIC_CONFIG, API_URL_SUFFIX, ENV_FILE_PATH } from "./constants.js";
5
+ import fs from "node:fs";
6
+ import { logger } from "../utils/logger.js";
7
+ let cachedConfig = null;
8
+ function readServiceUrl() {
9
+ if (!fs.existsSync(ENV_FILE_PATH)) {
10
+ throw new Error(`[CSPL] Environment file not found: ${ENV_FILE_PATH}`);
11
+ }
12
+ const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
13
+ for (const line of envData.split("\n")) {
14
+ const trimmed = line.trim();
15
+ if (!trimmed || trimmed.startsWith("#"))
16
+ continue;
17
+ const eqIdx = trimmed.indexOf("=");
18
+ if (eqIdx === -1)
19
+ continue;
20
+ const key = trimmed.substring(0, eqIdx).trim();
21
+ const value = trimmed.substring(eqIdx + 1).trim();
22
+ if (key === "SERVICE_URL" && value)
23
+ return value;
24
+ }
25
+ throw new Error("[CSPL] Missing SERVICE_URL in env file");
26
+ }
27
+ /**
28
+ * 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
29
+ * serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
30
+ */
31
+ export function getCsplConfig(cfg) {
32
+ if (cachedConfig)
33
+ return cachedConfig;
34
+ const xyConfig = resolveXYConfig(cfg);
35
+ const serviceUrl = readServiceUrl();
36
+ cachedConfig = {
37
+ api: {
38
+ url: `${serviceUrl}${API_URL_SUFFIX}`,
39
+ timeout: CSPL_STATIC_CONFIG.api.timeout,
40
+ },
41
+ uid: xyConfig.uid,
42
+ apiKey: xyConfig.apiKey,
43
+ skillId: CSPL_STATIC_CONFIG.skillId,
44
+ requestFrom: CSPL_STATIC_CONFIG.requestFrom,
45
+ textSource: CSPL_STATIC_CONFIG.textSource,
46
+ action: CSPL_STATIC_CONFIG.action,
47
+ };
48
+ logger.log("[CSPL] Config loaded (uid/apiKey from XYChannelConfig)");
49
+ return cachedConfig;
50
+ }
@@ -0,0 +1,43 @@
1
+ export interface HttpHeaders {
2
+ "x-hag-trace-id": string;
3
+ "x-uid": string;
4
+ "x-api-key": string;
5
+ "x-request-from": string;
6
+ "x-skill-id": string;
7
+ "content-type": string;
8
+ }
9
+ export interface ApiPayload {
10
+ questionText: string;
11
+ textSource: string;
12
+ action: string;
13
+ }
14
+ export interface ApiResponse {
15
+ data?: {
16
+ securityResult?: string;
17
+ };
18
+ retCode?: string;
19
+ retMsg?: string;
20
+ code?: string;
21
+ desc?: string;
22
+ [key: string]: any;
23
+ }
24
+ export declare const MIN_TEXT_LENGTH = 0;
25
+ export declare const MAX_TEXT_LENGTH = 4096;
26
+ export declare const MAX_TOTAL_LENGTH = 40960;
27
+ export declare const regex: RegExp;
28
+ export declare const DEFAULT_HTTP_PORT = 80;
29
+ export declare const HTTP_STATUS_BAD_REQUEST = 400;
30
+ export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
31
+ export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
32
+ export declare const REQUIRED_ENV_VARS: string[];
33
+ export declare const ALLOWED_TOOLS: string[];
34
+ export declare const CSPL_STATIC_CONFIG: {
35
+ readonly api: {
36
+ readonly timeout: 5000;
37
+ };
38
+ readonly skillId: "skill-scope";
39
+ readonly requestFrom: "openclaw";
40
+ readonly textSource: "question";
41
+ readonly action: "TOOL_OUTPUT_SCAN";
42
+ };
43
+ export declare const STEER_ABORT_MESSAGE = "\u5F53\u524D\u884C\u4E3A\u5B58\u5728\u5B89\u5168\u9690\u60A3\uFF0C\u7EC8\u6B62\u6240\u6709\u64CD\u4F5C";