@ynhcj/xiaoyi-channel 0.0.35-beta → 0.0.35-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 (78) hide show
  1. package/dist/index.d.ts +0 -2
  2. package/dist/index.js +42 -2
  3. package/dist/src/bot.js +53 -47
  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 +19 -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.d.ts +4 -0
  30. package/dist/src/reply-dispatcher.js +43 -7
  31. package/dist/src/steer-injector.d.ts +16 -0
  32. package/dist/src/steer-injector.js +74 -0
  33. package/dist/src/thread-bindings.d.ts +54 -0
  34. package/dist/src/thread-bindings.js +214 -0
  35. package/dist/src/tools/calendar-tool.js +5 -38
  36. package/dist/src/tools/call-phone-tool.js +1 -42
  37. package/dist/src/tools/create-alarm-tool.js +9 -108
  38. package/dist/src/tools/delete-alarm-tool.js +5 -69
  39. package/dist/src/tools/image-reading-tool.d.ts +5 -0
  40. package/dist/src/tools/image-reading-tool.js +328 -0
  41. package/dist/src/tools/location-tool.js +6 -40
  42. package/dist/src/tools/modify-alarm-tool.js +7 -113
  43. package/dist/src/tools/modify-note-tool.js +3 -41
  44. package/dist/src/tools/note-tool.js +4 -16
  45. package/dist/src/tools/search-alarm-tool.js +14 -118
  46. package/dist/src/tools/search-calendar-tool.js +8 -82
  47. package/dist/src/tools/search-contact-tool.js +2 -55
  48. package/dist/src/tools/search-file-tool.js +4 -61
  49. package/dist/src/tools/search-message-tool.js +2 -59
  50. package/dist/src/tools/search-note-tool.js +4 -22
  51. package/dist/src/tools/search-photo-gallery-tool.js +38 -59
  52. package/dist/src/tools/send-file-to-user-tool.js +1 -39
  53. package/dist/src/tools/send-message-tool.js +5 -56
  54. package/dist/src/tools/session-manager.js +0 -45
  55. package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
  56. package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
  57. package/dist/src/tools/upload-file-tool.js +0 -49
  58. package/dist/src/tools/upload-photo-tool.js +0 -42
  59. package/dist/src/tools/view-push-result-tool.d.ts +5 -0
  60. package/dist/src/tools/view-push-result-tool.js +107 -0
  61. package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
  62. package/dist/src/tools/xiaoyi-collection-tool.js +112 -0
  63. package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
  64. package/dist/src/trigger-handler.d.ts +22 -0
  65. package/dist/src/trigger-handler.js +59 -0
  66. package/dist/src/types.d.ts +1 -8
  67. package/dist/src/types.js +4 -0
  68. package/dist/src/utils/pushdata-manager.d.ts +28 -0
  69. package/dist/src/utils/pushdata-manager.js +171 -0
  70. package/dist/src/utils/pushid-manager.d.ts +12 -0
  71. package/dist/src/utils/pushid-manager.js +105 -0
  72. package/dist/src/websocket.d.ts +25 -31
  73. package/dist/src/websocket.js +239 -271
  74. package/package.json +2 -2
  75. package/dist/src/tools/search-photo-tool.d.ts +0 -9
  76. package/dist/src/tools/search-photo-tool.js +0 -270
  77. package/dist/src/utils/session.d.ts +0 -34
  78. 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);
@@ -8,6 +8,10 @@ export interface CreateXYReplyDispatcherParams {
8
8
  accountId: string;
9
9
  isSteerFollower?: boolean;
10
10
  }
11
+ /**
12
+ * 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
13
+ */
14
+ export declare function cleanupStaleTempFiles(tempDir?: string): Promise<void>;
11
15
  /**
12
16
  * Create a reply dispatcher for XY channel messages.
13
17
  * Follows feishu pattern with status updates and streaming support.
@@ -1,8 +1,43 @@
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
+ const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
8
+ /**
9
+ * 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
10
+ */
11
+ export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
12
+ try {
13
+ const stats = await fs.stat(tempDir).catch(() => null);
14
+ if (!stats?.isDirectory()) {
15
+ return;
16
+ }
17
+ const files = await fs.readdir(tempDir);
18
+ const now = Date.now();
19
+ let cleanedCount = 0;
20
+ for (const file of files) {
21
+ const filePath = path.join(tempDir, file);
22
+ try {
23
+ const fileStat = await fs.stat(filePath);
24
+ if (now - fileStat.mtimeMs > TEMP_FILE_TTL_MS) {
25
+ await fs.unlink(filePath);
26
+ cleanedCount++;
27
+ }
28
+ }
29
+ catch (err) {
30
+ // 忽略单个文件处理失败
31
+ }
32
+ }
33
+ if (cleanedCount > 0) {
34
+ console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} stale files (>${TEMP_FILE_TTL_MS / 1000 / 3600}h) from ${tempDir}`);
35
+ }
36
+ }
37
+ catch (err) {
38
+ console.error(`[CLEANUP] ❌ Failed to cleanup temp dir:`, err);
39
+ }
40
+ }
6
41
  /**
7
42
  * Create a reply dispatcher for XY channel messages.
8
43
  * Follows feishu pattern with status updates and streaming support.
@@ -13,9 +48,7 @@ export function createXYReplyDispatcher(params) {
13
48
  const log = runtime?.log ?? console.log;
14
49
  const error = runtime?.error ?? console.error;
15
50
  log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
16
- log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
17
51
  log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
18
- log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
19
52
  log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
20
53
  // 初始taskId和messageId(作为fallback)
21
54
  const initialTaskId = taskId;
@@ -32,7 +65,12 @@ export function createXYReplyDispatcher(params) {
32
65
  };
33
66
  const core = getXYRuntime();
34
67
  const config = resolveXYConfig(cfg);
35
- const prefixContext = createReplyPrefixContext({ cfg, agentId: accountId });
68
+ // Simplified prefix context for single-account Xiaoyi channel
69
+ const prefixContext = {
70
+ responsePrefix: undefined,
71
+ responsePrefixContextProvider: undefined,
72
+ onModelSelected: undefined,
73
+ };
36
74
  let statusUpdateInterval = null;
37
75
  let hasSentResponse = false;
38
76
  let finalSent = false;
@@ -54,7 +92,7 @@ export function createXYReplyDispatcher(params) {
54
92
  sessionId,
55
93
  taskId: currentTaskId, // 🔑 动态taskId
56
94
  messageId: currentMessageId, // 🔑 动态messageId
57
- text: "任务正在处理中,请稍后~",
95
+ text: "任务正在处理中,请稍候~",
58
96
  state: "working",
59
97
  }).catch((err) => {
60
98
  error(`Failed to send status update:`, err);
@@ -289,7 +327,6 @@ export function createXYReplyDispatcher(params) {
289
327
  const currentTaskId = getActiveTaskId();
290
328
  const currentMessageId = getActiveMessageId();
291
329
  const text = payload.text ?? "";
292
- log(`[PARTIAL REPLY] Partial reply chunk received, taskId: ${currentTaskId}`);
293
330
  try {
294
331
  if (text.length > 0) {
295
332
  await sendReasoningTextUpdate({
@@ -300,7 +337,6 @@ export function createXYReplyDispatcher(params) {
300
337
  text,
301
338
  append: false,
302
339
  });
303
- log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update`);
304
340
  }
305
341
  }
306
342
  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
+ }