@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
@@ -1,6 +1,9 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
+ import { XYFileUploadService } from "./file-upload.js";
2
3
  import { XYPushService } from "./push.js";
3
4
  import { getCurrentSessionContext } from "./tools/session-manager.js";
5
+ import { savePushData } from "./utils/pushdata-manager.js";
6
+ import { getAllPushIds } from "./utils/pushid-manager.js";
4
7
  // Special marker for default push delivery when no target is specified
5
8
  const DEFAULT_PUSH_MARKER = "default";
6
9
  // File extension to MIME type mapping
@@ -87,13 +90,6 @@ export const xyOutbound = {
87
90
  };
88
91
  },
89
92
  sendText: async ({ cfg, to, text, accountId }) => {
90
- // Log parameters
91
- console.log(`[xyOutbound.sendText] Called with:`, {
92
- to,
93
- accountId,
94
- textLength: text?.length || 0,
95
- textPreview: text?.slice(0, 100),
96
- });
97
93
  // Resolve configuration
98
94
  const config = resolveXYConfig(cfg);
99
95
  // Handle default push marker (for cron jobs without explicit target)
@@ -104,119 +100,139 @@ export const xyOutbound = {
104
100
  // The push service will handle it based on config
105
101
  actualTo = config.defaultSessionId || "";
106
102
  }
103
+ // 1. 持久化推送消息内容,获取 pushDataId
104
+ console.log(`[xyOutbound.sendText] Saving push data to local storage...`);
105
+ let pushDataId;
106
+ try {
107
+ pushDataId = await savePushData(text);
108
+ console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
109
+ }
110
+ catch (error) {
111
+ console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
112
+ // 如果持久化失败,仍然继续发送(不阻塞主流程)
113
+ pushDataId = "";
114
+ }
115
+ // 2. 读取所有 pushId
116
+ console.log(`[xyOutbound.sendText] Loading all pushIds...`);
117
+ let pushIdList = [];
118
+ try {
119
+ pushIdList = await getAllPushIds();
120
+ console.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
121
+ }
122
+ catch (error) {
123
+ console.error(`[xyOutbound.sendText] ❌ Failed to load pushIds:`, error);
124
+ }
125
+ // 3. 如果 pushIdList 为空,回退到原有逻辑(使用 config pushId)
126
+ if (pushIdList.length === 0) {
127
+ console.log(`[xyOutbound.sendText] ⚠️ No pushIds found, falling back to config pushId`);
128
+ pushIdList = [config.pushId];
129
+ }
107
130
  // Create push service
108
131
  const pushService = new XYPushService(config);
109
132
  // Extract title (first 57 chars or first line)
110
133
  const title = text.split("\n")[0].slice(0, 57);
111
134
  // Truncate push content to max length 1000
112
135
  const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
113
- // Send push message (content, title, data, sessionId)
114
- await pushService.sendPush(pushText, title, undefined, actualTo);
115
- console.log(`[xyOutbound.sendText] Completed successfully`);
136
+ // 4. 遍历所有 pushId,依次发送推送通知
137
+ console.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
138
+ let successCount = 0;
139
+ let failureCount = 0;
140
+ for (const pushId of pushIdList) {
141
+ try {
142
+ // 传入 pushId 和 pushDataId,使用 kind="data" 格式
143
+ await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
144
+ successCount++;
145
+ console.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
146
+ }
147
+ catch (error) {
148
+ failureCount++;
149
+ console.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
150
+ // 单个 pushId 发送失败不影响其他,继续处理下一个
151
+ }
152
+ }
116
153
  // Return message info
117
154
  return {
118
155
  channel: "xiaoyi-channel",
119
- messageId: Date.now().toString(),
156
+ messageId: pushDataId || Date.now().toString(),
120
157
  chatId: actualTo,
121
158
  };
122
159
  },
123
160
  sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
124
- // Log parameters
125
- console.log(`[xyOutbound.sendMedia] Called with:`, {
126
- to,
127
- accountId,
128
- text,
129
- mediaUrl,
130
- mediaLocalRoots,
161
+ // Parse to: "sessionId::taskId"
162
+ const parts = to.split("::");
163
+ if (parts.length !== 2) {
164
+ throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
165
+ }
166
+ const [sessionId, taskId] = parts;
167
+ // Resolve configuration
168
+ const config = resolveXYConfig(cfg);
169
+ // Create upload service
170
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
171
+ // Validate mediaUrl
172
+ if (!mediaUrl) {
173
+ throw new Error("mediaUrl is required for sendMedia");
174
+ }
175
+ // Upload file
176
+ const fileId = await uploadService.uploadFile(mediaUrl);
177
+ // Check if fileId is empty
178
+ if (!fileId) {
179
+ console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
180
+ return {
181
+ channel: "xiaoyi-channel",
182
+ messageId: "",
183
+ chatId: to,
184
+ };
185
+ }
186
+ console.log(`[xyOutbound.sendMedia] File uploaded:`, {
187
+ fileId,
188
+ sessionId,
189
+ taskId,
131
190
  });
132
- // All sendMedia processing logic has been disabled
133
- // Use send_file_to_user tool instead for file transfers to user device
134
- console.log(`[xyOutbound.sendMedia] Processing disabled, use send_file_to_user tool`);
135
- // Return empty message info
191
+ // Get filename and mime type from mediaUrl
192
+ // mediaUrl may be a local file path or URL
193
+ const fileName = mediaUrl.split("/").pop() || "unknown";
194
+ const mimeType = getMimeTypeFromFilename(fileName);
195
+ // Build agent_response message
196
+ const agentResponse = {
197
+ msgType: "agent_response",
198
+ agentId: config.agentId,
199
+ sessionId: sessionId,
200
+ taskId: taskId,
201
+ msgDetail: JSON.stringify({
202
+ jsonrpc: "2.0",
203
+ id: taskId,
204
+ result: {
205
+ kind: "artifact-update",
206
+ append: true,
207
+ lastChunk: false,
208
+ final: false,
209
+ artifact: {
210
+ artifactId: taskId,
211
+ parts: [
212
+ {
213
+ kind: "file",
214
+ file: {
215
+ name: fileName,
216
+ mimeType: mimeType,
217
+ fileId: fileId,
218
+ },
219
+ },
220
+ ],
221
+ },
222
+ },
223
+ error: { code: 0 },
224
+ }),
225
+ };
226
+ // Get WebSocket manager and send message
227
+ const { getXYWebSocketManager } = await import("./client.js");
228
+ const wsManager = getXYWebSocketManager(config);
229
+ await wsManager.sendMessage(sessionId, agentResponse);
230
+ console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
231
+ // Return message info
136
232
  return {
137
233
  channel: "xiaoyi-channel",
138
- messageId: "",
234
+ messageId: fileId,
139
235
  chatId: to,
140
236
  };
141
- // // Parse to: "sessionId::taskId"
142
- // const parts = to.split("::");
143
- // if (parts.length !== 2) {
144
- // throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
145
- // }
146
- // const [sessionId, taskId] = parts;
147
- // // Resolve configuration
148
- // const config = resolveXYConfig(cfg);
149
- // // Create upload service
150
- // const uploadService = new XYFileUploadService(
151
- // config.fileUploadUrl,
152
- // config.apiKey,
153
- // config.uid
154
- // );
155
- // // Validate mediaUrl
156
- // if (!mediaUrl) {
157
- // throw new Error("mediaUrl is required for sendMedia");
158
- // }
159
- // // Upload file
160
- // const fileId = await uploadService.uploadFile(mediaUrl);
161
- // // Check if fileId is empty
162
- // if (!fileId) {
163
- // console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
164
- // return {
165
- // channel: "xiaoyi-channel",
166
- // messageId: "",
167
- // chatId: to,
168
- // };
169
- // }
170
- // console.log(`[xyOutbound.sendMedia] File uploaded:`, {
171
- // fileId,
172
- // sessionId,
173
- // taskId,
174
- // });
175
- // // Get filename and mime type from mediaUrl
176
- // // mediaUrl may be a local file path or URL
177
- // const fileName = mediaUrl.split("/").pop() || "unknown";
178
- // const mimeType = getMimeTypeFromFilename(fileName);
179
- // // Build agent_response message
180
- // const agentResponse: OutboundWebSocketMessage = {
181
- // msgType: "agent_response",
182
- // agentId: config.agentId,
183
- // sessionId: sessionId,
184
- // taskId: taskId,
185
- // msgDetail: JSON.stringify({
186
- // jsonrpc: "2.0",
187
- // id: taskId,
188
- // result: {
189
- // kind: "artifact-update",
190
- // append: true,
191
- // lastChunk: false,
192
- // final: false,
193
- // artifact: {
194
- // artifactId: taskId,
195
- // parts: [
196
- // {
197
- // kind: "file",
198
- // file: {
199
- // name: fileName,
200
- // mimeType: mimeType,
201
- // fileId: fileId,
202
- // },
203
- // },
204
- // ],
205
- // },
206
- // },
207
- // error: { code: 0 },
208
- // }),
209
- // };
210
- // // Get WebSocket manager and send message
211
- // const { getXYWebSocketManager } = await import("./client.js");
212
- // const wsManager = getXYWebSocketManager(config);
213
- // await wsManager.sendMessage(sessionId, agentResponse);
214
- // console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
215
- // // Return message info
216
- // return {
217
- // channel: "xiaoyi-channel",
218
- // messageId: fileId,
219
- // chatId: to,
220
- // };
221
237
  },
222
238
  };
@@ -43,6 +43,13 @@ export declare function isTasksCancelMessage(method: string): boolean;
43
43
  * Looks for push_id in data parts under variables.systemVariables.push_id
44
44
  */
45
45
  export declare function extractPushId(parts: A2AMessagePart[]): string | null;
46
+ /**
47
+ * Extract Trigger event data from message parts.
48
+ * Looks for Trigger events with pushDataId in data parts.
49
+ */
50
+ export declare function extractTriggerData(parts: A2AMessagePart[]): {
51
+ pushDataId: string;
52
+ } | null;
46
53
  /**
47
54
  * Validate A2A request structure.
48
55
  */
@@ -72,6 +72,28 @@ export function extractPushId(parts) {
72
72
  }
73
73
  return null;
74
74
  }
75
+ /**
76
+ * Extract Trigger event data from message parts.
77
+ * Looks for Trigger events with pushDataId in data parts.
78
+ */
79
+ export function extractTriggerData(parts) {
80
+ for (const part of parts) {
81
+ if (part.kind === "data" && part.data) {
82
+ const events = part.data.events;
83
+ if (Array.isArray(events)) {
84
+ for (const event of events) {
85
+ if (event.header?.namespace === "Common" && event.header?.name === "Trigger") {
86
+ const pushDataId = event.payload?.dataMap?.pushDataId;
87
+ if (pushDataId && typeof pushDataId === "string") {
88
+ return { pushDataId };
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return null;
96
+ }
75
97
  /**
76
98
  * Validate A2A request structure.
77
99
  */
@@ -14,8 +14,15 @@ export declare class XYPushService {
14
14
  private generateTraceId;
15
15
  /**
16
16
  * Send a push message to a user session.
17
+ *
18
+ * @param content - Push message content
19
+ * @param title - Push message title
20
+ * @param data - Optional additional data
21
+ * @param sessionId - Optional session ID
22
+ * @param pushDataId - Optional pushDataId for kind="data" format
23
+ * @param pushId - Push ID to use (required)
17
24
  */
18
- sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string): Promise<void>;
25
+ sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string, pushDataId?: string, pushId?: string): Promise<void>;
19
26
  /**
20
27
  * Send a push message with file attachments.
21
28
  */
package/dist/src/push.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // Push message service for scheduled tasks
2
2
  import fetch from "node-fetch";
3
3
  import { randomUUID } from "crypto";
4
- import { configManager } from "./utils/config-manager.js";
5
4
  /**
6
5
  * Service for sending push messages to users.
7
6
  * Used for outbound messages and scheduled tasks.
@@ -21,29 +20,21 @@ export class XYPushService {
21
20
  }
22
21
  /**
23
22
  * Send a push message to a user session.
23
+ *
24
+ * @param content - Push message content
25
+ * @param title - Push message title
26
+ * @param data - Optional additional data
27
+ * @param sessionId - Optional session ID
28
+ * @param pushDataId - Optional pushDataId for kind="data" format
29
+ * @param pushId - Push ID to use (required)
24
30
  */
25
- async sendPush(content, title, data, sessionId) {
31
+ async sendPush(content, title, data, sessionId, pushDataId, pushId) {
26
32
  const pushUrl = this.config.pushUrl || this.DEFAULT_PUSH_URL;
27
33
  const traceId = this.generateTraceId();
28
- // Get dynamic pushId for the session (falls back to config pushId)
29
- const dynamicPushId = configManager.getPushId(sessionId);
30
- const pushId = dynamicPushId || this.config.pushId;
34
+ // Use provided pushId or fall back to config pushId
35
+ const actualPushId = pushId || this.config.pushId;
31
36
  console.log(`[PUSH] 📤 Preparing to send push message`);
32
- console.log(`[PUSH] - Title: "${title}"`);
33
- console.log(`[PUSH] - Content length: ${content.length} chars`);
34
- console.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
35
- console.log(`[PUSH] - Trace ID: ${traceId}`);
36
- console.log(`[PUSH] - Push URL: ${pushUrl}`);
37
- if (dynamicPushId) {
38
- console.log(`[PUSH] - Using dynamic pushId (from session): ${pushId.substring(0, 20)}...`);
39
- console.log(`[PUSH] - Full dynamic pushId: ${pushId}`);
40
- }
41
- else {
42
- console.log(`[PUSH] - Using config pushId (fallback): ${pushId.substring(0, 20)}...`);
43
- console.log(`[PUSH] - Full config pushId: ${pushId}`);
44
- }
45
- console.log(`[PUSH] - API ID: ${this.config.apiId}`);
46
- console.log(`[PUSH] - UID: ${this.config.uid}`);
37
+ console.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
47
38
  try {
48
39
  const requestBody = {
49
40
  jsonrpc: "2.0",
@@ -51,23 +42,31 @@ export class XYPushService {
51
42
  result: {
52
43
  id: randomUUID(),
53
44
  apiId: this.config.apiId,
54
- pushId: pushId, // Use dynamic pushId
45
+ pushId: actualPushId,
55
46
  pushText: title,
56
47
  kind: "task",
57
48
  artifacts: [
58
49
  {
59
50
  artifactId: randomUUID(),
60
- parts: [
61
- {
62
- kind: "text",
63
- text: content,
64
- },
65
- ],
51
+ parts: pushDataId
52
+ ? [
53
+ {
54
+ kind: "data",
55
+ data: {
56
+ pushDataId: pushDataId,
57
+ },
58
+ },
59
+ ]
60
+ : [
61
+ {
62
+ kind: "text",
63
+ text: content,
64
+ },
65
+ ],
66
66
  },
67
67
  ],
68
68
  },
69
69
  };
70
- console.log(`[PUSH] Full request body:`, JSON.stringify(requestBody, null, 2));
71
70
  const response = await fetch(pushUrl, {
72
71
  method: "POST",
73
72
  headers: {
@@ -83,21 +82,16 @@ export class XYPushService {
83
82
  // Log response status and headers
84
83
  console.log(`[PUSH] 📥 Response received`);
85
84
  console.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
86
- console.log(`[PUSH] - Content-Type: ${response.headers.get('content-type')}`);
87
- console.log(`[PUSH] - Content-Length: ${response.headers.get('content-length')}`);
88
85
  if (!response.ok) {
89
86
  const errorText = await response.text();
90
87
  console.log(`[PUSH] ❌ Push request failed`);
91
88
  console.log(`[PUSH] - HTTP Status: ${response.status}`);
92
- console.log(`[PUSH] - Response body: ${errorText}`);
93
89
  throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
94
90
  }
95
91
  // Try to parse JSON response with detailed error handling
96
92
  let result;
97
93
  try {
98
94
  const responseText = await response.text();
99
- console.log(`[PUSH] 📄 Response body length: ${responseText.length} chars`);
100
- console.log(`[PUSH] 📄 Response body preview: ${responseText.substring(0, 200)}`);
101
95
  if (!responseText || responseText.trim() === '') {
102
96
  console.log(`[PUSH] ⚠️ Received empty response body`);
103
97
  result = {};
@@ -112,20 +106,13 @@ export class XYPushService {
112
106
  throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
113
107
  }
114
108
  console.log(`[PUSH] ✅ Push message sent successfully`);
115
- console.log(`[PUSH] - Title: "${title}"`);
116
109
  console.log(`[PUSH] - Trace ID: ${traceId}`);
117
- console.log(`[PUSH] - Used pushId: ${pushId.substring(0, 20)}...`);
118
- console.log(`[PUSH] - Response:`, result);
119
110
  }
120
111
  catch (error) {
121
112
  console.log(`[PUSH] ❌ Failed to send push message`);
122
- console.log(`[PUSH] - Trace ID: ${traceId}`);
123
- console.log(`[PUSH] - Target URL: ${pushUrl}`);
124
- console.log(`[PUSH] - Push ID: ${pushId.substring(0, 20)}...`);
125
113
  if (error instanceof Error) {
126
114
  console.log(`[PUSH] - Error name: ${error.name}`);
127
115
  console.log(`[PUSH] - Error message: ${error.message}`);
128
- console.log(`[PUSH] - Error stack:`, error.stack);
129
116
  }
130
117
  else {
131
118
  console.log(`[PUSH] - Error:`, error);
@@ -1,8 +1,38 @@
1
- import { createReplyPrefixContext } from "openclaw/plugin-sdk";
2
1
  import { getXYRuntime } from "./runtime.js";
3
2
  import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
4
3
  import { resolveXYConfig } from "./config.js";
5
4
  import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+ /**
8
+ * 清理 /tmp/xy_channel 目录中的所有文件
9
+ */
10
+ async function cleanupTempDir(tempDir = "/tmp/xy_channel") {
11
+ try {
12
+ const stats = await fs.stat(tempDir).catch(() => null);
13
+ if (!stats?.isDirectory()) {
14
+ return; // 目录不存在,直接返回
15
+ }
16
+ const files = await fs.readdir(tempDir);
17
+ let cleanedCount = 0;
18
+ for (const file of files) {
19
+ const filePath = path.join(tempDir, file);
20
+ try {
21
+ await fs.unlink(filePath);
22
+ cleanedCount++;
23
+ }
24
+ catch (err) {
25
+ // 忽略单个文件删除失败,继续处理其他文件
26
+ }
27
+ }
28
+ if (cleanedCount > 0) {
29
+ console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} files from ${tempDir}`);
30
+ }
31
+ }
32
+ catch (err) {
33
+ console.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
34
+ }
35
+ }
6
36
  /**
7
37
  * Create a reply dispatcher for XY channel messages.
8
38
  * Follows feishu pattern with status updates and streaming support.
@@ -13,9 +43,7 @@ export function createXYReplyDispatcher(params) {
13
43
  const log = runtime?.log ?? console.log;
14
44
  const error = runtime?.error ?? console.error;
15
45
  log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
16
- log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
17
46
  log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
18
- log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
19
47
  log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
20
48
  // 初始taskId和messageId(作为fallback)
21
49
  const initialTaskId = taskId;
@@ -32,7 +60,12 @@ export function createXYReplyDispatcher(params) {
32
60
  };
33
61
  const core = getXYRuntime();
34
62
  const config = resolveXYConfig(cfg);
35
- const prefixContext = createReplyPrefixContext({ cfg, agentId: accountId });
63
+ // Simplified prefix context for single-account Xiaoyi channel
64
+ const prefixContext = {
65
+ responsePrefix: undefined,
66
+ responsePrefixContextProvider: undefined,
67
+ onModelSelected: undefined,
68
+ };
36
69
  let statusUpdateInterval = null;
37
70
  let hasSentResponse = false;
38
71
  let finalSent = false;
@@ -54,7 +87,7 @@ export function createXYReplyDispatcher(params) {
54
87
  sessionId,
55
88
  taskId: currentTaskId, // 🔑 动态taskId
56
89
  messageId: currentMessageId, // 🔑 动态messageId
57
- text: "任务正在处理中,请稍后~",
90
+ text: "任务正在处理中,请稍候~",
58
91
  state: "working",
59
92
  }).catch((err) => {
60
93
  error(`Failed to send status update:`, err);
@@ -206,6 +239,7 @@ export function createXYReplyDispatcher(params) {
206
239
  }
207
240
  }
208
241
  stopStatusInterval();
242
+ void cleanupTempDir();
209
243
  },
210
244
  onCleanup: () => {
211
245
  const currentTaskId = getActiveTaskId();
@@ -289,7 +323,6 @@ export function createXYReplyDispatcher(params) {
289
323
  const currentTaskId = getActiveTaskId();
290
324
  const currentMessageId = getActiveMessageId();
291
325
  const text = payload.text ?? "";
292
- log(`[PARTIAL REPLY] Partial reply chunk received, taskId: ${currentTaskId}`);
293
326
  try {
294
327
  if (text.length > 0) {
295
328
  await sendReasoningTextUpdate({
@@ -300,7 +333,6 @@ export function createXYReplyDispatcher(params) {
300
333
  text,
301
334
  append: false,
302
335
  });
303
- log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update`);
304
336
  }
305
337
  }
306
338
  catch (err) {
@@ -0,0 +1,16 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ /**
3
+ * 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
4
+ */
5
+ export declare function setCachedContext(cfg: ClawdbotConfig, runtime: RuntimeEnv, accountId: string): void;
6
+ /**
7
+ * 尝试向当前活跃会话注入 steer 消息。
8
+ * 两层保险:
9
+ * 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
10
+ * 2. hasActiveTask(sessionId) 确认任务仍在运行
11
+ *
12
+ * @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
13
+ * @param message 要注入的用户消息文本
14
+ * @returns true=已注入,false=跳过
15
+ */
16
+ export declare function tryInjectSteer(sessionKey: string | undefined, message: string): Promise<boolean>;
@@ -0,0 +1,74 @@
1
+ // Steer message injector for CSPL hook integration
2
+ import { getSessionContext } from "./tools/session-manager.js";
3
+ import { hasActiveTask, getCurrentTaskId } from "./task-manager.js";
4
+ import { handleXYMessage } from "./bot.js";
5
+ import { logger } from "./utils/logger.js";
6
+ import { randomUUID } from "node:crypto";
7
+ let cachedCfg = null;
8
+ let cachedRuntime = null;
9
+ let cachedAccountId = "default";
10
+ /**
11
+ * 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
12
+ */
13
+ export function setCachedContext(cfg, runtime, accountId) {
14
+ cachedCfg = cfg;
15
+ cachedRuntime = runtime;
16
+ cachedAccountId = accountId;
17
+ }
18
+ /**
19
+ * 尝试向当前活跃会话注入 steer 消息。
20
+ * 两层保险:
21
+ * 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
22
+ * 2. hasActiveTask(sessionId) 确认任务仍在运行
23
+ *
24
+ * @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
25
+ * @param message 要注入的用户消息文本
26
+ * @returns true=已注入,false=跳过
27
+ */
28
+ export async function tryInjectSteer(sessionKey, message) {
29
+ if (!sessionKey) {
30
+ return false;
31
+ }
32
+ const sessionCtx = getSessionContext(sessionKey);
33
+ if (!sessionCtx) {
34
+ return false;
35
+ }
36
+ const { sessionId } = sessionCtx;
37
+ const activeTaskId = getCurrentTaskId(sessionId);
38
+ if (!hasActiveTask(sessionId)) {
39
+ return false;
40
+ }
41
+ if (!cachedCfg || !cachedRuntime) {
42
+ logger.error("[STEER] No cached cfg/runtime available, cannot inject");
43
+ return false;
44
+ }
45
+ // 3. 构造合成 A2A 消息(伪装成用户在当前会话中发送的新消息)
46
+ const syntheticMessage = {
47
+ jsonrpc: "2.0",
48
+ method: "tasks/send",
49
+ id: `steer-msg-${randomUUID()}`,
50
+ params: {
51
+ sessionId,
52
+ id: activeTaskId ?? `steer-task-${randomUUID()}`,
53
+ agentLoginSessionId: "",
54
+ message: {
55
+ role: "user",
56
+ parts: [{ kind: "text", text: message }],
57
+ },
58
+ },
59
+ };
60
+ console.log(`[STEER] Injecting steer for sessionId=${sessionId}, taskId=${syntheticMessage.params.id}`);
61
+ try {
62
+ await handleXYMessage({
63
+ cfg: cachedCfg,
64
+ runtime: cachedRuntime,
65
+ message: syntheticMessage,
66
+ accountId: cachedAccountId,
67
+ });
68
+ return true;
69
+ }
70
+ catch (err) {
71
+ logger.error(`[STEER] ❌ Failed to inject steer: ${err}`);
72
+ return false;
73
+ }
74
+ }