@ynhcj/xiaoyi-channel 0.0.32-beta → 0.0.32-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 (75) hide show
  1. package/dist/index.d.ts +0 -2
  2. package/dist/index.js +42 -2
  3. package/dist/src/bot.js +60 -56
  4. package/dist/src/channel.js +24 -8
  5. package/dist/src/client.js +11 -33
  6. package/dist/src/config.js +2 -2
  7. package/dist/src/cspl/call-api.d.ts +3 -0
  8. package/dist/src/cspl/call-api.js +86 -0
  9. package/dist/src/cspl/config.d.ts +19 -0
  10. package/dist/src/cspl/config.js +50 -0
  11. package/dist/src/cspl/constants.d.ts +43 -0
  12. package/dist/src/cspl/constants.js +22 -0
  13. package/dist/src/cspl/utils.d.ts +10 -0
  14. package/dist/src/cspl/utils.js +57 -0
  15. package/dist/src/file-upload.d.ts +5 -0
  16. package/dist/src/file-upload.js +88 -6
  17. package/dist/src/formatter.d.ts +14 -0
  18. package/dist/src/formatter.js +49 -28
  19. package/dist/src/heartbeat.js +0 -4
  20. package/dist/src/monitor.js +16 -10
  21. package/dist/src/onboarding.d.ts +3 -4
  22. package/dist/src/onboarding.js +2 -2
  23. package/dist/src/outbound.d.ts +2 -1
  24. package/dist/src/outbound.js +119 -103
  25. package/dist/src/parser.d.ts +7 -0
  26. package/dist/src/parser.js +22 -0
  27. package/dist/src/push.d.ts +8 -1
  28. package/dist/src/push.js +27 -40
  29. package/dist/src/reply-dispatcher.js +39 -7
  30. package/dist/src/steer-injector.d.ts +16 -0
  31. package/dist/src/steer-injector.js +74 -0
  32. package/dist/src/thread-bindings.d.ts +54 -0
  33. package/dist/src/thread-bindings.js +214 -0
  34. package/dist/src/tools/calendar-tool.js +5 -38
  35. package/dist/src/tools/call-phone-tool.js +1 -42
  36. package/dist/src/tools/create-alarm-tool.js +8 -103
  37. package/dist/src/tools/delete-alarm-tool.js +5 -69
  38. package/dist/src/tools/image-reading-tool.d.ts +5 -0
  39. package/dist/src/tools/image-reading-tool.js +328 -0
  40. package/dist/src/tools/location-tool.js +6 -40
  41. package/dist/src/tools/modify-alarm-tool.js +8 -117
  42. package/dist/src/tools/modify-note-tool.js +3 -41
  43. package/dist/src/tools/note-tool.js +4 -16
  44. package/dist/src/tools/search-alarm-tool.js +14 -118
  45. package/dist/src/tools/search-calendar-tool.js +8 -82
  46. package/dist/src/tools/search-contact-tool.js +2 -55
  47. package/dist/src/tools/search-file-tool.js +4 -61
  48. package/dist/src/tools/search-message-tool.js +2 -59
  49. package/dist/src/tools/search-note-tool.js +4 -22
  50. package/dist/src/tools/search-photo-gallery-tool.js +38 -59
  51. package/dist/src/tools/send-file-to-user-tool.js +1 -39
  52. package/dist/src/tools/send-message-tool.js +5 -56
  53. package/dist/src/tools/session-manager.js +0 -45
  54. package/dist/src/tools/upload-file-tool.js +0 -49
  55. package/dist/src/tools/upload-photo-tool.js +0 -42
  56. package/dist/src/tools/view-push-result-tool.d.ts +5 -0
  57. package/dist/src/tools/view-push-result-tool.js +107 -0
  58. package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
  59. package/dist/src/tools/xiaoyi-collection-tool.js +112 -0
  60. package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
  61. package/dist/src/trigger-handler.d.ts +22 -0
  62. package/dist/src/trigger-handler.js +59 -0
  63. package/dist/src/types.d.ts +1 -8
  64. package/dist/src/types.js +4 -0
  65. package/dist/src/utils/pushdata-manager.d.ts +28 -0
  66. package/dist/src/utils/pushdata-manager.js +171 -0
  67. package/dist/src/utils/pushid-manager.d.ts +12 -0
  68. package/dist/src/utils/pushid-manager.js +105 -0
  69. package/dist/src/websocket.d.ts +25 -31
  70. package/dist/src/websocket.js +239 -271
  71. package/package.json +2 -2
  72. package/dist/src/tools/search-photo-tool.d.ts +0 -9
  73. package/dist/src/tools/search-photo-tool.js +0 -270
  74. package/dist/src/utils/session.d.ts +0 -34
  75. package/dist/src/utils/session.js +0 -50
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { xyPlugin } from "./src/channel.js";
3
2
  /**
4
3
  * Xiaoyi Channel Plugin Entry Point.
5
4
  * Exports the plugin for OpenClaw to load.
@@ -13,4 +12,3 @@ declare const plugin: {
13
12
  register(api: OpenClawPluginApi): void;
14
13
  };
15
14
  export default plugin;
16
- export { xyPlugin };
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,8 +18,44 @@ 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
+ console.log(`[CSPL] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
28
+ try {
29
+ const resultText = extractResultText(event, event.toolName);
30
+ const resultLength = resultText.length;
31
+ if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
32
+ return;
33
+ }
34
+ // 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
35
+ const questionText = {
36
+ tool: event.toolName,
37
+ output: [{ content: "" }],
38
+ };
39
+ const originText = processText(resultText);
40
+ questionText.output[0].content = originText;
41
+ let finalJson = JSON.stringify(questionText);
42
+ if (finalJson.length > MAX_TEXT_LENGTH) {
43
+ const diff = finalJson.length - MAX_TEXT_LENGTH;
44
+ const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
45
+ questionText.output[0].content = trimmed;
46
+ finalJson = JSON.stringify(questionText);
47
+ }
48
+ const response = await callCsplApi(finalJson, api.config);
49
+ const result = parseSecurityResult(response);
50
+ console.log(`[CSPL] Security result: status=${result.status}`);
51
+ if (result.status === "REJECT") {
52
+ await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
53
+ }
54
+ }
55
+ catch (err) {
56
+ api.logger.error(`[CSPL] after_tool_call error: ${err}`);
57
+ }
58
+ });
17
59
  },
18
60
  };
19
61
  export default plugin;
20
- // Also export the plugin directly for testing
21
- export { xyPlugin };
package/dist/src/bot.js CHANGED
@@ -1,11 +1,13 @@
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 { downloadFilesFromParts } from "./file-download.js";
4
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
5
5
  import { resolveXYConfig } from "./config.js";
6
- import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
6
+ import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
7
7
  import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
8
8
  import { configManager } from "./utils/config-manager.js";
9
+ import { addPushId } from "./utils/pushid-manager.js";
10
+ import { getPushDataById } from "./utils/pushdata-manager.js";
9
11
  import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
10
12
  /**
11
13
  * Handle an incoming A2A message.
@@ -16,13 +18,13 @@ export async function handleXYMessage(params) {
16
18
  const { cfg, runtime, message, accountId } = params;
17
19
  const log = runtime?.log ?? console.log;
18
20
  const error = runtime?.error ?? console.error;
21
+ // 每次收到消息时更新缓存,供 steer 注入使用
22
+ setCachedContext(cfg, runtime, accountId);
19
23
  // Get runtime (already validated in monitor.ts, but get reference for use)
20
24
  const core = getXYRuntime();
21
25
  try {
22
26
  // Check for special messages BEFORE parsing (these have different param structures)
23
27
  const messageMethod = message.method;
24
- log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
25
- log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
26
28
  // Handle clearContext messages (params only has sessionId)
27
29
  if (messageMethod === "clearContext" || messageMethod === "clear_context") {
28
30
  const sessionId = message.params?.sessionId;
@@ -57,6 +59,42 @@ export async function handleXYMessage(params) {
57
59
  }
58
60
  // Parse the A2A message (for regular messages)
59
61
  const parsed = parseA2AMessage(message);
62
+ // ========== 检测 Trigger 消息 ==========
63
+ // 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
64
+ const triggerData = extractTriggerData(parsed.parts);
65
+ if (triggerData) {
66
+ log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
67
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
68
+ log(`[BOT] - Task ID: ${parsed.taskId}`);
69
+ try {
70
+ // 读取 pushData
71
+ const pushDataItem = await getPushDataById(triggerData.pushDataId);
72
+ if (!pushDataItem) {
73
+ error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
74
+ return;
75
+ }
76
+ log(`[BOT] ✅ Found pushData, sending direct response`);
77
+ log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
78
+ const config = resolveXYConfig(cfg);
79
+ // 直接发送响应(final=true,不走 openclaw 流程)
80
+ await sendA2AResponse({
81
+ config,
82
+ sessionId: parsed.sessionId,
83
+ taskId: parsed.taskId,
84
+ messageId: parsed.messageId,
85
+ text: pushDataItem.dataDetail,
86
+ append: false,
87
+ final: true,
88
+ });
89
+ log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
90
+ return; // 提前返回,不继续处理
91
+ }
92
+ catch (err) {
93
+ error(`[BOT] ❌ Failed to handle Trigger message:`, err);
94
+ return;
95
+ }
96
+ }
97
+ // ========================================
60
98
  // 🔑 检测steer模式和是否是第二条消息
61
99
  const isSteerMode = cfg.messages?.queue?.mode === "steer";
62
100
  const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
@@ -77,10 +115,11 @@ export async function handleXYMessage(params) {
77
115
  const pushId = extractPushId(parsed.parts);
78
116
  if (pushId) {
79
117
  log(`[BOT] 📌 Extracted push_id from user message`);
80
- log(`[BOT] - Session ID: ${parsed.sessionId}`);
81
- log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
82
- log(`[BOT] - Full push_id: ${pushId}`);
83
118
  configManager.updatePushId(parsed.sessionId, pushId);
119
+ // 持久化 pushId 到本地文件(异步,不阻塞主流程)
120
+ addPushId(pushId).catch((err) => {
121
+ error(`[BOT] Failed to persist pushId:`, err);
122
+ });
84
123
  }
85
124
  else {
86
125
  log(`[BOT] ℹ️ No push_id found in message, will use config default`);
@@ -100,12 +139,6 @@ export async function handleXYMessage(params) {
100
139
  },
101
140
  });
102
141
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
103
- // 🔑 注册session(带引用计数)
104
- log(`[BOT] 📝 About to register session for tools...`);
105
- log(`[BOT] - sessionKey: ${route.sessionKey}`);
106
- log(`[BOT] - sessionId: ${parsed.sessionId}`);
107
- log(`[BOT] - taskId: ${parsed.taskId}`);
108
- log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
109
142
  registerSession(route.sessionKey, {
110
143
  config,
111
144
  sessionId: parsed.sessionId,
@@ -113,7 +146,6 @@ export async function handleXYMessage(params) {
113
146
  messageId: parsed.messageId,
114
147
  agentId: route.accountId,
115
148
  });
116
- log(`[BOT] ✅ Session registered for tools`);
117
149
  // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
118
150
  log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
119
151
  void sendStatusUpdate({
@@ -121,7 +153,7 @@ export async function handleXYMessage(params) {
121
153
  sessionId: parsed.sessionId,
122
154
  taskId: parsed.taskId,
123
155
  messageId: parsed.messageId,
124
- text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
156
+ text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍候~",
125
157
  state: "working",
126
158
  }).catch((err) => {
127
159
  error(`Failed to send initial status update:`, err);
@@ -129,10 +161,8 @@ export async function handleXYMessage(params) {
129
161
  // Extract text and files from parts
130
162
  const text = extractTextFromParts(parsed.parts);
131
163
  const fileParts = extractFileParts(parsed.parts);
132
- // Download files if present (using core's media download)
133
- const mediaList = await downloadFilesFromParts(fileParts);
134
- // Build media payload for inbound context (following feishu pattern)
135
- const mediaPayload = buildXYMediaPayload(mediaList);
164
+ // Build media payload directly from file URLs (no local download needed)
165
+ const mediaPayload = buildXYMediaPayload(fileParts);
136
166
  // Resolve envelope format options (following feishu pattern)
137
167
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
138
168
  // Build message body with speaker prefix (following feishu pattern)
@@ -175,9 +205,7 @@ export async function handleXYMessage(params) {
175
205
  });
176
206
  // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
177
207
  log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
178
- log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
179
208
  log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
180
- log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
181
209
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
182
210
  cfg,
183
211
  runtime,
@@ -187,19 +215,12 @@ export async function handleXYMessage(params) {
187
215
  accountId: route.accountId,
188
216
  isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
189
217
  });
190
- log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
191
218
  // 🔑 只有第一条消息启动状态定时器
192
219
  // 第二条消息会很快返回,不需要定时器
193
220
  if (!isSecondMessage) {
194
221
  startStatusInterval();
195
222
  log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
196
223
  }
197
- else {
198
- log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
199
- }
200
- log(`xy: dispatching to agent (session=${parsed.sessionId})`);
201
- // Dispatch to OpenClaw core using correct API (following feishu pattern)
202
- log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
203
224
  // Build session context for AsyncLocalStorage
204
225
  const sessionContext = {
205
226
  config,
@@ -213,7 +234,6 @@ export async function handleXYMessage(params) {
213
234
  onSettled: () => {
214
235
  log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
215
236
  log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
216
- markDispatchIdle();
217
237
  // 🔑 减少引用计数
218
238
  decrementTaskIdRef(parsed.sessionId);
219
239
  // 🔑 如果是第一条消息完成,解锁
@@ -274,35 +294,19 @@ export async function handleXYMessage(params) {
274
294
  }
275
295
  }
276
296
  /**
277
- * Build media payload for inbound context.
278
- * Following feishu pattern: buildFeishuMediaPayload().
297
+ * Build media payload for inbound context using file URLs.
298
+ * OpenClaw natively supports MediaUrl/MediaUrls — no local download needed.
299
+ *
300
+ * @param fileParts - File parts extracted from A2A message (with uri field)
279
301
  */
280
- function buildXYMediaPayload(mediaList) {
281
- const first = mediaList[0];
282
- const mediaPaths = mediaList.map((media) => media.path);
283
- const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
302
+ function buildXYMediaPayload(fileParts) {
303
+ const first = fileParts[0];
304
+ const mediaUrls = fileParts.map((f) => f.uri);
305
+ const mediaTypes = fileParts.map((f) => f.mimeType).filter(Boolean);
284
306
  return {
285
- MediaPath: first?.path,
307
+ MediaUrl: first?.uri,
286
308
  MediaType: first?.mimeType,
287
- MediaUrl: first?.path,
288
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
289
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
309
+ MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
290
310
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
291
311
  };
292
312
  }
293
- /**
294
- * Infer OpenClaw media type from file type string.
295
- */
296
- function inferMediaType(fileType) {
297
- const lower = fileType.toLowerCase();
298
- if (lower.includes("image") || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(lower)) {
299
- return "image";
300
- }
301
- if (lower.includes("video") || /\.(mp4|avi|mov|mkv|webm)$/i.test(lower)) {
302
- return "video";
303
- }
304
- if (lower.includes("audio") || /\.(mp3|wav|ogg|m4a)$/i.test(lower)) {
305
- return "audio";
306
- }
307
- return "file";
308
- }
@@ -1,27 +1,29 @@
1
1
  import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./config.js";
2
2
  import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
- import { xyOnboardingAdapter } from "./onboarding.js";
5
4
  import { locationTool } from "./tools/location-tool.js";
6
5
  import { noteTool } from "./tools/note-tool.js";
7
6
  import { searchNoteTool } from "./tools/search-note-tool.js";
8
7
  import { modifyNoteTool } from "./tools/modify-note-tool.js";
9
8
  import { calendarTool } from "./tools/calendar-tool.js";
10
9
  import { searchCalendarTool } from "./tools/search-calendar-tool.js";
11
- // import { searchContactTool } from "./tools/search-contact-tool.js"; // 暂时禁用
10
+ import { searchContactTool } from "./tools/search-contact-tool.js";
12
11
  import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
13
12
  import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
14
13
  import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
15
14
  import { callPhoneTool } from "./tools/call-phone-tool.js";
16
15
  import { searchMessageTool } from "./tools/search-message-tool.js";
16
+ import { sendMessageTool } from "./tools/send-message-tool.js";
17
17
  import { searchFileTool } from "./tools/search-file-tool.js";
18
18
  import { uploadFileTool } from "./tools/upload-file-tool.js";
19
19
  import { createAlarmTool } from "./tools/create-alarm-tool.js";
20
20
  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
- import { sendMessageTool } from "./tools/send-message-tool.js";
24
23
  import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
24
+ import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
25
+ import { viewPushResultTool } from "./tools/view-push-result-tool.js";
26
+ import { imageReadingTool } from "./tools/image-reading-tool.js";
25
27
  /**
26
28
  * Xiaoyi Channel Plugin for OpenClaw.
27
29
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -60,8 +62,7 @@ export const xyPlugin = {
60
62
  schema: xyConfigSchema,
61
63
  },
62
64
  outbound: xyOutbound,
63
- onboarding: xyOnboardingAdapter,
64
- agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool, sendFileToUserTool], // searchContactTool 已暂时禁用
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],
65
66
  messaging: {
66
67
  normalizeTarget: (raw) => {
67
68
  const trimmed = raw.trim();
@@ -78,6 +79,22 @@ export const xyPlugin = {
78
79
  hint: "<sessionId>",
79
80
  },
80
81
  },
82
+ bindings: {
83
+ compileConfiguredBinding: ({ conversationId }) => {
84
+ const sessionId = conversationId.trim();
85
+ if (!sessionId)
86
+ return null;
87
+ return {
88
+ conversationId: sessionId,
89
+ parentConversationId: undefined,
90
+ };
91
+ },
92
+ matchInboundConversation: ({ compiledBinding, conversationId }) => {
93
+ return compiledBinding.conversationId === conversationId
94
+ ? { conversationId, matchPriority: 2 }
95
+ : null;
96
+ },
97
+ },
81
98
  reload: {
82
99
  configPrefixes: ["channels.xiaoyi-channel"],
83
100
  },
@@ -88,10 +105,9 @@ export const xyPlugin = {
88
105
  const account = resolveXYConfig(context.cfg);
89
106
  context.setStatus?.({
90
107
  accountId: context.accountId,
91
- wsUrl1: account.wsUrl1,
92
- wsUrl2: account.wsUrl2,
108
+ wsUrl: account.wsUrl,
93
109
  });
94
- context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl1: ${account.wsUrl1}, wsUrl2: ${account.wsUrl2})`);
110
+ context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
95
111
  return monitorXYProvider({
96
112
  config: context.cfg,
97
113
  runtime: context.runtime,
@@ -23,7 +23,6 @@ export function getXYWebSocketManager(config) {
23
23
  let cached = wsManagerCache.get(cacheKey);
24
24
  if (cached && cached.isConfigMatch(config)) {
25
25
  const log = runtime?.log ?? console.log;
26
- log(`[WS-MANAGER-CACHE] ✅ Reusing cached WebSocket manager: ${cacheKey}, total managers: ${wsManagerCache.size}`);
27
26
  return cached;
28
27
  }
29
28
  // Create new manager
@@ -73,45 +72,25 @@ export function getCachedManagerCount() {
73
72
  * Helps identify connection issues and orphan connections.
74
73
  */
75
74
  export function diagnoseAllManagers() {
76
- console.log("========================================");
77
- console.log("📊 WebSocket Manager Global Diagnostics");
78
- console.log("========================================");
79
75
  console.log(`Total cached managers: ${wsManagerCache.size}`);
80
- console.log("");
81
76
  if (wsManagerCache.size === 0) {
82
77
  console.log("ℹ️ No managers in cache");
83
- console.log("========================================");
84
78
  return;
85
79
  }
86
80
  let orphanCount = 0;
87
81
  wsManagerCache.forEach((manager, key) => {
88
82
  const diag = manager.getConnectionDiagnostics();
89
- console.log(`📌 Manager: ${key}`);
90
- console.log(` Shutting down: ${diag.isShuttingDown}`);
91
83
  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) {
84
+ // Connection
85
+ console.log(` 🔌 Connection:`);
86
+ console.log(` - Exists: ${diag.connection.exists}`);
87
+ console.log(` - ReadyState: ${diag.connection.readyState}`);
88
+ console.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
89
+ console.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
90
+ console.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
91
+ console.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
92
+ console.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
93
+ if (diag.connection.isOrphan) {
115
94
  console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
116
95
  orphanCount++;
117
96
  }
@@ -124,7 +103,6 @@ export function diagnoseAllManagers() {
124
103
  else {
125
104
  console.log(`✅ No orphan connections found`);
126
105
  }
127
- console.log("========================================");
128
106
  }
129
107
  /**
130
108
  * Clean up orphan connections across all managers.
@@ -134,7 +112,7 @@ export function cleanupOrphanConnections() {
134
112
  let cleanedCount = 0;
135
113
  wsManagerCache.forEach((manager, key) => {
136
114
  const diag = manager.getConnectionDiagnostics();
137
- if (diag.server1.isOrphan || diag.server2.isOrphan) {
115
+ if (diag.connection.isOrphan) {
138
116
  console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
139
117
  manager.disconnect();
140
118
  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,86 @@
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
+ const result = parseResponse(data);
65
+ console.log(`[CSPL API] ✅ 请求成功`);
66
+ resolve(result);
67
+ }
68
+ catch (e) {
69
+ console.error(`[CSPL API] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
70
+ reject(e);
71
+ }
72
+ });
73
+ });
74
+ req.on("error", (error) => {
75
+ console.error(`[CSPL API] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
76
+ reject(error);
77
+ });
78
+ req.on("timeout", () => {
79
+ console.error(`[CSPL API] ⏰ 请求超时 (${config.api.timeout}ms)`);
80
+ req.destroy();
81
+ reject(new Error("[CSPL] Request timeout"));
82
+ });
83
+ req.write(JSON.stringify(payload));
84
+ req.end();
85
+ });
86
+ }
@@ -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 = 443;
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\uFF0C\u5E76\u4E14\u5728\u6700\u7EC8\u56DE\u590D\u4E2D\u8BF4\u660E\uFF0C\u8BC6\u522B\u5230\u5F53\u524D\u6D41\u7A0B\u4E2D\u5B58\u5728\u6F5C\u5728\u5B89\u5168\u98CE\u9669\uFF0C\u5DF2\u5E2E\u60A8\u4E2D\u6B62\u5F53\u524D\u6D41\u7A0B\uFF0C\u8BF7\u6E05\u7A7A\u4E0A\u4E0B\u6587\u540E\u91CD\u8BD5";