@ynhcj/xiaoyi-channel 0.0.52-beta → 0.0.52-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 (61) hide show
  1. package/dist/index.d.ts +0 -2
  2. package/dist/index.js +7 -6
  3. package/dist/src/bot.d.ts +1 -0
  4. package/dist/src/bot.js +44 -32
  5. package/dist/src/channel.js +32 -4
  6. package/dist/src/client.js +0 -6
  7. package/dist/src/cspl/call-api.js +19 -9
  8. package/dist/src/cspl/config.js +3 -3
  9. package/dist/src/cspl/constants.d.ts +2 -1
  10. package/dist/src/cspl/constants.js +1 -1
  11. package/dist/src/file-download.js +1 -1
  12. package/dist/src/file-upload.js +1 -11
  13. package/dist/src/formatter.d.ts +2 -0
  14. package/dist/src/formatter.js +11 -6
  15. package/dist/src/monitor.js +8 -10
  16. package/dist/src/onboarding.d.ts +3 -4
  17. package/dist/src/onboarding.js +2 -2
  18. package/dist/src/outbound.d.ts +2 -1
  19. package/dist/src/outbound.js +1 -19
  20. package/dist/src/parser.d.ts +6 -0
  21. package/dist/src/parser.js +16 -0
  22. package/dist/src/provider.d.ts +2 -0
  23. package/dist/src/provider.js +124 -0
  24. package/dist/src/push.js +0 -21
  25. package/dist/src/reply-dispatcher.d.ts +4 -0
  26. package/dist/src/reply-dispatcher.js +21 -13
  27. package/dist/src/thread-bindings.d.ts +54 -0
  28. package/dist/src/thread-bindings.js +214 -0
  29. package/dist/src/tools/call-phone-tool.js +2 -18
  30. package/dist/src/tools/create-alarm-tool.js +3 -7
  31. package/dist/src/tools/device-tool-map.d.ts +4 -0
  32. package/dist/src/tools/device-tool-map.js +35 -0
  33. package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
  34. package/dist/src/tools/find-pc-devices-tool.js +98 -0
  35. package/dist/src/tools/image-reading-tool.js +3 -3
  36. package/dist/src/tools/modify-alarm-tool.js +3 -7
  37. package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
  38. package/dist/src/tools/save-file-to-phone-tool.js +170 -0
  39. package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
  40. package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
  41. package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
  42. package/dist/src/tools/send-command-to-car-tool.js +85 -0
  43. package/dist/src/tools/send-file-to-user-tool.js +1 -0
  44. package/dist/src/tools/session-manager.d.ts +1 -0
  45. package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
  46. package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
  47. package/dist/src/tools/upload-file-tool.js +2 -2
  48. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
  49. package/dist/src/tools/xiaoyi-add-collection-tool.js +188 -0
  50. package/dist/src/tools/xiaoyi-collection-tool.js +42 -7
  51. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
  52. package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
  53. package/dist/src/utils/runtime-manager.d.ts +7 -0
  54. package/dist/src/utils/runtime-manager.js +42 -0
  55. package/dist/src/websocket.js +15 -9
  56. package/openclaw.plugin.json +1 -0
  57. package/package.json +3 -4
  58. package/dist/src/tools/search-photo-tool.d.ts +0 -9
  59. package/dist/src/tools/search-photo-tool.js +0 -270
  60. package/dist/src/utils/session.d.ts +0 -34
  61. package/dist/src/utils/session.js +0 -50
@@ -4,6 +4,7 @@ import { handleXYMessage } from "./bot.js";
4
4
  import { parseA2AMessage } from "./parser.js";
5
5
  import { hasActiveTask } from "./task-manager.js";
6
6
  import { handleTriggerEvent } from "./trigger-handler.js";
7
+ import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
7
8
  /**
8
9
  * Per-session serial queue that ensures messages from the same session are processed
9
10
  * in arrival order while allowing different sessions to run concurrently.
@@ -67,7 +68,7 @@ export async function monitorXYProvider(opts = {}) {
67
68
  // Event handlers (defined early so they can be referenced in cleanup)
68
69
  const messageHandler = (message, sessionId, serverId) => {
69
70
  const messageKey = `${sessionId}::${message.id}`;
70
- log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
71
+ log(`[MONITOR-HANDLER] ####### messageHandler triggered: sessionId=${sessionId}, messageId=${message.id} #######`);
71
72
  // ✅ Report health: received a message
72
73
  trackEvent?.();
73
74
  // Check for duplicate message handling
@@ -75,17 +76,15 @@ export async function monitorXYProvider(opts = {}) {
75
76
  error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
76
77
  }
77
78
  activeMessages.add(messageKey);
78
- log(`[MONITOR-HANDLER] 📝 Active messages count: ${activeMessages.size}, messageKey: ${messageKey}`);
79
79
  const task = async () => {
80
80
  try {
81
- log(`[MONITOR-HANDLER] 🚀 Starting handleXYMessage for messageKey=${messageKey}`);
82
81
  await handleXYMessage({
83
82
  cfg,
84
83
  runtime,
85
84
  message,
86
85
  accountId, // ✅ Pass accountId ("default")
86
+ webSocketSessionId: sessionId, // ✅ 传递 WebSocket 层级的 sessionId
87
87
  });
88
- log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
89
88
  }
90
89
  catch (err) {
91
90
  // ✅ Only log error, don't re-throw to prevent gateway restart
@@ -94,7 +93,6 @@ export async function monitorXYProvider(opts = {}) {
94
93
  finally {
95
94
  // Remove from active messages when done
96
95
  activeMessages.delete(messageKey);
97
- log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
98
96
  }
99
97
  };
100
98
  // 🔑 核心改造:检测steer模式
@@ -107,7 +105,6 @@ export async function monitorXYProvider(opts = {}) {
107
105
  // Steer模式且有活跃任务:不入队列,直接并发执行
108
106
  log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
109
107
  log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
110
- log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
111
108
  void task().catch((err) => {
112
109
  error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
113
110
  activeMessages.delete(messageKey);
@@ -115,7 +112,6 @@ export async function monitorXYProvider(opts = {}) {
115
112
  }
116
113
  else {
117
114
  // 正常模式:入队列串行执行
118
- log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
119
115
  void enqueue(sessionId, task).catch((err) => {
120
116
  error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
121
117
  activeMessages.delete(messageKey);
@@ -207,8 +203,8 @@ export async function monitorXYProvider(opts = {}) {
207
203
  wsManager.on("disconnected", disconnectedHandler);
208
204
  wsManager.on("error", errorHandler);
209
205
  wsManager.on("trigger-event", triggerEventHandler);
210
- // Start periodic health check (every 5 minutes)
211
- console.log("🏥 Starting periodic health check (every 5 minutes)...");
206
+ // Start periodic health check (every 6 hours)
207
+ console.log("🏥 Starting periodic health check (every 6 hours)...");
212
208
  healthCheckInterval = setInterval(() => {
213
209
  console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
214
210
  diagnoseAllManagers();
@@ -217,7 +213,9 @@ export async function monitorXYProvider(opts = {}) {
217
213
  if (cleaned > 0) {
218
214
  console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
219
215
  }
220
- }, 5 * 60 * 1000); // 5 minutes
216
+ // Cleanup stale temp files (older than 24 hours)
217
+ void cleanupStaleTempFiles();
218
+ }, 6 * 60 * 60 * 1000); // 6 hours
221
219
  // Connect to WebSocket servers
222
220
  wsManager.connect()
223
221
  .then(() => {
@@ -1,6 +1,5 @@
1
- import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
2
1
  /**
3
- * XY Channel Onboarding Adapter
4
- * Implements the ChannelOnboardingAdapter interface for OpenClaw's onboarding system
2
+ * XY Channel Onboarding Adapter (DISABLED - not currently used)
3
+ * NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
5
4
  */
6
- export declare const xyOnboardingAdapter: ChannelOnboardingAdapter;
5
+ export declare const xyOnboardingAdapter: any;
@@ -152,8 +152,8 @@ async function configure({ cfg, prompter, }) {
152
152
  };
153
153
  }
154
154
  /**
155
- * XY Channel Onboarding Adapter
156
- * Implements the ChannelOnboardingAdapter interface for OpenClaw's onboarding system
155
+ * XY Channel Onboarding Adapter (DISABLED - not currently used)
156
+ * NOTE: This is kept for future reference. Xiaoyi uses simple single-account config.
157
157
  */
158
158
  export const xyOnboardingAdapter = {
159
159
  channel,
@@ -1,6 +1,7 @@
1
- import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
1
+ type ChannelOutboundAdapter = any;
2
2
  /**
3
3
  * Outbound adapter for sending messages from OpenClaw to XY.
4
4
  * Uses Push service for direct message delivery.
5
5
  */
6
6
  export declare const xyOutbound: ChannelOutboundAdapter;
7
+ export {};
@@ -90,13 +90,6 @@ export const xyOutbound = {
90
90
  };
91
91
  },
92
92
  sendText: async ({ cfg, to, text, accountId }) => {
93
- // Log parameters
94
- console.log(`[xyOutbound.sendText] Called with:`, {
95
- to,
96
- accountId,
97
- textLength: text?.length || 0,
98
- textPreview: text?.slice(0, 100),
99
- });
100
93
  // Resolve configuration
101
94
  const config = resolveXYConfig(cfg);
102
95
  // Handle default push marker (for cron jobs without explicit target)
@@ -112,7 +105,7 @@ export const xyOutbound = {
112
105
  let pushDataId;
113
106
  try {
114
107
  pushDataId = await savePushData(text);
115
- console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId}`);
108
+ console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId.substring(0, 20)}`);
116
109
  }
117
110
  catch (error) {
118
111
  console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
@@ -146,7 +139,6 @@ export const xyOutbound = {
146
139
  let failureCount = 0;
147
140
  for (const pushId of pushIdList) {
148
141
  try {
149
- console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
150
142
  // 传入 pushId 和 pushDataId,使用 kind="data" 格式
151
143
  await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId, pushId);
152
144
  successCount++;
@@ -158,8 +150,6 @@ export const xyOutbound = {
158
150
  // 单个 pushId 发送失败不影响其他,继续处理下一个
159
151
  }
160
152
  }
161
- console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
162
- console.log(`[xyOutbound.sendText] Completed successfully`);
163
153
  // Return message info
164
154
  return {
165
155
  channel: "xiaoyi-channel",
@@ -168,14 +158,6 @@ export const xyOutbound = {
168
158
  };
169
159
  },
170
160
  sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
171
- // Log parameters
172
- console.log(`[xyOutbound.sendMedia] Called with:`, {
173
- to,
174
- accountId,
175
- text,
176
- mediaUrl,
177
- mediaLocalRoots,
178
- });
179
161
  // Parse to: "sessionId::taskId"
180
162
  const parts = to.split("::");
181
163
  if (parts.length !== 2) {
@@ -43,6 +43,12 @@ 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 deviceType from message parts.
48
+ * Looks for deviceType in data parts under variables.systemVariables.deviceType
49
+ * (same level as push_id).
50
+ */
51
+ export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
46
52
  /**
47
53
  * Extract Trigger event data from message parts.
48
54
  * Looks for Trigger events with pushDataId in data parts.
@@ -72,6 +72,22 @@ export function extractPushId(parts) {
72
72
  }
73
73
  return null;
74
74
  }
75
+ /**
76
+ * Extract deviceType from message parts.
77
+ * Looks for deviceType in data parts under variables.systemVariables.deviceType
78
+ * (same level as push_id).
79
+ */
80
+ export function extractDeviceType(parts) {
81
+ for (const part of parts) {
82
+ if (part.kind === "data" && part.data) {
83
+ const deviceType = part.data.variables?.systemVariables?.device_type;
84
+ if (deviceType && typeof deviceType === "string") {
85
+ return deviceType;
86
+ }
87
+ }
88
+ }
89
+ return null;
90
+ }
75
91
  /**
76
92
  * Extract Trigger event data from message parts.
77
93
  * Looks for Trigger events with pushDataId in data parts.
@@ -0,0 +1,2 @@
1
+ import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-models";
2
+ export declare const xiaoyiProvider: ProviderPlugin;
@@ -0,0 +1,124 @@
1
+ // Xiaoyi Provider
2
+ // Wraps any OpenAI-compatible endpoint and injects dynamic headers
3
+ // (taskId, sessionId, conversationId) from the current XY channel session.
4
+ // Falls back to uid-based values when no session context is available.
5
+ //
6
+ // Users configure the underlying model in config:
7
+ // models.providers.xiaoyiprovider.baseUrl = "https://..."
8
+ // models.providers.xiaoyiprovider.api = "openai-completions"
9
+ // models.providers.xiaoyiprovider.models = [...]
10
+ import { createHash } from "crypto";
11
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
12
+ /**
13
+ * Dynamic header keys injected via extraParams and forwarded to the HTTP request.
14
+ * Correspond to the three fields written to .xiaoyiruntime:
15
+ * TASK_ID, SESSION_ID, CONVERSATION_ID
16
+ */
17
+ const HEADER_TRACE_ID = "x-hag-trace-id";
18
+ const HEADER_SESSION_ID = "x-session-id";
19
+ const HEADER_INTERACTION_ID = "x-interaction-id";
20
+ /**
21
+ * Encode uid via SHA-256 and take first 32 hex chars.
22
+ */
23
+ function encodeUid(uid) {
24
+ return createHash("sha256").update(uid).digest("hex").slice(0, 32);
25
+ }
26
+ /**
27
+ * Get uid from plugin config (OpenClawConfig -> plugins -> xiaoyi-channel -> config).
28
+ */
29
+ function getUidFromConfig(config) {
30
+ return config?.plugins?.entries?.["xiaoyi-channel"]?.config?.uid;
31
+ }
32
+ export const xiaoyiProvider = {
33
+ id: "xiaoyiprovider",
34
+ label: "Xiaoyi Provider",
35
+ docsPath: "/providers/models",
36
+ auth: [],
37
+ isCacheTtlEligible: () => true,
38
+ /**
39
+ * Inject dynamic session params into extraParams so they flow
40
+ * through to wrapStreamFn's ctx.extraParams.
41
+ *
42
+ * Priority:
43
+ * 1. Session context (from AsyncLocalStorage, set by bot.ts)
44
+ * 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
45
+ * 3. No uid available → return undefined (no headers injected)
46
+ */
47
+ prepareExtraParams: (ctx) => {
48
+ const sessionCtx = getCurrentSessionContext();
49
+ if (sessionCtx) {
50
+ const taskId = sessionCtx.taskId;
51
+ const sessionId = taskId.split("&")[0];
52
+ const interactionId = taskId.split("&")[1] || "";
53
+ return {
54
+ ...ctx.extraParams,
55
+ [HEADER_TRACE_ID]: taskId,
56
+ [HEADER_SESSION_ID]: sessionId,
57
+ [HEADER_INTERACTION_ID]: interactionId,
58
+ };
59
+ }
60
+ // Fallback: uid-based values
61
+ const uid = getUidFromConfig(ctx.config);
62
+ if (!uid)
63
+ return undefined;
64
+ const prefix = encodeUid(uid);
65
+ const ts = Date.now();
66
+ const fallbackValue = `${prefix}_${ts}`;
67
+ return {
68
+ ...ctx.extraParams,
69
+ [HEADER_TRACE_ID]: fallbackValue,
70
+ [HEADER_SESSION_ID]: fallbackValue,
71
+ [HEADER_INTERACTION_ID]: fallbackValue,
72
+ };
73
+ },
74
+ /**
75
+ * Wrap the stream function to inject dynamic headers into every
76
+ * HTTP request to the model provider.
77
+ *
78
+ * Reads the values injected by prepareExtraParams and adds them
79
+ * as HTTP headers on the outgoing request.
80
+ */
81
+ wrapStreamFn: (ctx) => {
82
+ const underlying = ctx.streamFn;
83
+ if (!underlying)
84
+ return underlying;
85
+ return async (model, context, options) => {
86
+ // 每次请求时从 ctx.extraParams 动态读取 header
87
+ const dynamicHeaders = {};
88
+ if (ctx.extraParams) {
89
+ const traceId = ctx.extraParams[HEADER_TRACE_ID];
90
+ const sessionId = ctx.extraParams[HEADER_SESSION_ID];
91
+ const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
92
+ if (typeof traceId === "string")
93
+ dynamicHeaders[HEADER_TRACE_ID] = traceId;
94
+ if (typeof sessionId === "string")
95
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
96
+ if (typeof interactionId === "string")
97
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
98
+ }
99
+ // 记录输入
100
+ console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
101
+ if (context.systemPrompt) {
102
+ console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
103
+ }
104
+ // 在发送给模型前,删除 systemPrompt 中 ## Tooling 与 TOOLS.md 声明之间的内容
105
+ if (context.systemPrompt) {
106
+ const before = context.systemPrompt.length;
107
+ context.systemPrompt = context.systemPrompt.replace(/(## Tooling)[\s\S]*?(TOOLS\.md does not control tool availability; it is user guidance for how to use external tools\.)/, "$1\n\n$2");
108
+ console.log(`[xiaoyiprovider] system prompt trimmed: ${before} -> ${context.systemPrompt.length}`);
109
+ }
110
+ const stream = await underlying(model, context, {
111
+ ...options,
112
+ headers: {
113
+ ...options?.headers,
114
+ ...dynamicHeaders,
115
+ },
116
+ });
117
+ // 异步监听输出(不阻塞 stream 返回)
118
+ stream.result().then((result) => {
119
+ console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
120
+ }, (err) => console.log(`[xiaoyiprovider] stream error: ${JSON.stringify(err)}`));
121
+ return stream;
122
+ };
123
+ },
124
+ };
package/dist/src/push.js CHANGED
@@ -34,15 +34,7 @@ export class XYPushService {
34
34
  // Use provided pushId or fall back to config pushId
35
35
  const actualPushId = pushId || this.config.pushId;
36
36
  console.log(`[PUSH] 📤 Preparing to send push message`);
37
- console.log(`[PUSH] - Title: "${title}"`);
38
- console.log(`[PUSH] - Content length: ${content.length} chars`);
39
- console.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
40
- console.log(`[PUSH] - Trace ID: ${traceId}`);
41
- console.log(`[PUSH] - Push URL: ${pushUrl}`);
42
37
  console.log(`[PUSH] - Using pushId: ${actualPushId.substring(0, 20)}...`);
43
- console.log(`[PUSH] - Full pushId: ${actualPushId}`);
44
- console.log(`[PUSH] - API ID: ${this.config.apiId}`);
45
- console.log(`[PUSH] - UID: ${this.config.uid}`);
46
38
  try {
47
39
  const requestBody = {
48
40
  jsonrpc: "2.0",
@@ -75,7 +67,6 @@ export class XYPushService {
75
67
  ],
76
68
  },
77
69
  };
78
- console.log(`[PUSH] Full request body:`, JSON.stringify(requestBody, null, 2));
79
70
  const response = await fetch(pushUrl, {
80
71
  method: "POST",
81
72
  headers: {
@@ -91,21 +82,16 @@ export class XYPushService {
91
82
  // Log response status and headers
92
83
  console.log(`[PUSH] 📥 Response received`);
93
84
  console.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
94
- console.log(`[PUSH] - Content-Type: ${response.headers.get('content-type')}`);
95
- console.log(`[PUSH] - Content-Length: ${response.headers.get('content-length')}`);
96
85
  if (!response.ok) {
97
86
  const errorText = await response.text();
98
87
  console.log(`[PUSH] ❌ Push request failed`);
99
88
  console.log(`[PUSH] - HTTP Status: ${response.status}`);
100
- console.log(`[PUSH] - Response body: ${errorText}`);
101
89
  throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
102
90
  }
103
91
  // Try to parse JSON response with detailed error handling
104
92
  let result;
105
93
  try {
106
94
  const responseText = await response.text();
107
- console.log(`[PUSH] 📄 Response body length: ${responseText.length} chars`);
108
- console.log(`[PUSH] 📄 Response body preview: ${responseText.substring(0, 200)}`);
109
95
  if (!responseText || responseText.trim() === '') {
110
96
  console.log(`[PUSH] ⚠️ Received empty response body`);
111
97
  result = {};
@@ -120,20 +106,13 @@ export class XYPushService {
120
106
  throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
121
107
  }
122
108
  console.log(`[PUSH] ✅ Push message sent successfully`);
123
- console.log(`[PUSH] - Title: "${title}"`);
124
109
  console.log(`[PUSH] - Trace ID: ${traceId}`);
125
- console.log(`[PUSH] - Used pushId: ${actualPushId.substring(0, 20)}...`);
126
- console.log(`[PUSH] - Response:`, result);
127
110
  }
128
111
  catch (error) {
129
112
  console.log(`[PUSH] ❌ Failed to send push message`);
130
- console.log(`[PUSH] - Trace ID: ${traceId}`);
131
- console.log(`[PUSH] - Target URL: ${pushUrl}`);
132
- console.log(`[PUSH] - Push ID: ${actualPushId.substring(0, 20)}...`);
133
113
  if (error instanceof Error) {
134
114
  console.log(`[PUSH] - Error name: ${error.name}`);
135
115
  console.log(`[PUSH] - Error message: ${error.message}`);
136
- console.log(`[PUSH] - Error stack:`, error.stack);
137
116
  }
138
117
  else {
139
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,33 +1,37 @@
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";
6
5
  import fs from "fs/promises";
7
6
  import path from "path";
7
+ const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
8
8
  /**
9
- * 清理 /tmp/xy_channel 目录中的所有文件
9
+ * 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
10
10
  */
11
- async function cleanupTempDir(tempDir = "/tmp/xy_channel") {
11
+ export async function cleanupStaleTempFiles(tempDir = "/tmp/xy_channel") {
12
12
  try {
13
13
  const stats = await fs.stat(tempDir).catch(() => null);
14
14
  if (!stats?.isDirectory()) {
15
- return; // 目录不存在,直接返回
15
+ return;
16
16
  }
17
17
  const files = await fs.readdir(tempDir);
18
+ const now = Date.now();
18
19
  let cleanedCount = 0;
19
20
  for (const file of files) {
20
21
  const filePath = path.join(tempDir, file);
21
22
  try {
22
- await fs.unlink(filePath);
23
- cleanedCount++;
23
+ const fileStat = await fs.stat(filePath);
24
+ if (now - fileStat.mtimeMs > TEMP_FILE_TTL_MS) {
25
+ await fs.unlink(filePath);
26
+ cleanedCount++;
27
+ }
24
28
  }
25
29
  catch (err) {
26
- // 忽略单个文件删除失败,继续处理其他文件
30
+ // 忽略单个文件处理失败
27
31
  }
28
32
  }
29
33
  if (cleanedCount > 0) {
30
- console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} files from ${tempDir}`);
34
+ console.log(`[CLEANUP] 🧹 Cleaned ${cleanedCount} stale files (>${TEMP_FILE_TTL_MS / 1000 / 3600}h) from ${tempDir}`);
31
35
  }
32
36
  }
33
37
  catch (err) {
@@ -44,9 +48,7 @@ export function createXYReplyDispatcher(params) {
44
48
  const log = runtime?.log ?? console.log;
45
49
  const error = runtime?.error ?? console.error;
46
50
  log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
47
- log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
48
51
  log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
49
- log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
50
52
  log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
51
53
  // 初始taskId和messageId(作为fallback)
52
54
  const initialTaskId = taskId;
@@ -63,7 +65,12 @@ export function createXYReplyDispatcher(params) {
63
65
  };
64
66
  const core = getXYRuntime();
65
67
  const config = resolveXYConfig(cfg);
66
- 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
+ };
67
74
  let statusUpdateInterval = null;
68
75
  let hasSentResponse = false;
69
76
  let finalSent = false;
@@ -228,16 +235,17 @@ export function createXYReplyDispatcher(params) {
228
235
  text: "任务执行异常,请重试~",
229
236
  append: false,
230
237
  final: true,
238
+ errorCode: 99921111,
239
+ errorMessage: "任务执行异常,请重试",
231
240
  });
232
241
  finalSent = true;
233
- log(`[ON_IDLE] ✅ Sent error response`);
242
+ log(`[ON_IDLE] ✅ Sent error response with code: 99921111`);
234
243
  }
235
244
  catch (err) {
236
245
  error(`[ON_IDLE] Failed to send error response:`, err);
237
246
  }
238
247
  }
239
248
  stopStatusInterval();
240
- void cleanupTempDir();
241
249
  },
242
250
  onCleanup: () => {
243
251
  const currentTaskId = getActiveTaskId();
@@ -0,0 +1,54 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
2
+ import { type BindingTargetKind } from "openclaw/plugin-sdk/conversation-runtime";
3
+ type XYBindingTargetKind = "subagent" | "session";
4
+ /**
5
+ * Xiaoyi thread binding record.
6
+ * Simplified from feishu - uses sessionId as conversationId, no parentConversationId.
7
+ */
8
+ type XYThreadBindingRecord = {
9
+ accountId: string;
10
+ sessionId: string;
11
+ targetKind: XYBindingTargetKind;
12
+ targetSessionKey: string;
13
+ agentId?: string;
14
+ boundAt: number;
15
+ lastActivityAt: number;
16
+ };
17
+ /**
18
+ * Thread binding manager for Xiaoyi channel.
19
+ * Manages session bindings for single-account mode.
20
+ */
21
+ type XYThreadBindingManager = {
22
+ accountId: string;
23
+ getBySessionId: (sessionId: string) => XYThreadBindingRecord | undefined;
24
+ listBySessionKey: (targetSessionKey: string) => XYThreadBindingRecord[];
25
+ bindSession: (params: {
26
+ sessionId: string;
27
+ targetKind: BindingTargetKind;
28
+ targetSessionKey: string;
29
+ metadata?: Record<string, unknown>;
30
+ }) => XYThreadBindingRecord | null;
31
+ touchSession: (sessionId: string, at?: number) => XYThreadBindingRecord | null;
32
+ unbindSession: (sessionId: string) => XYThreadBindingRecord | null;
33
+ unbindBySessionKey: (targetSessionKey: string) => XYThreadBindingRecord[];
34
+ stop: () => void;
35
+ };
36
+ /**
37
+ * Creates a thread binding manager for Xiaoyi channel.
38
+ * Based on feishu implementation but simplified for single-account mode.
39
+ */
40
+ export declare function createXYThreadBindingManager(params: {
41
+ accountId?: string;
42
+ cfg: OpenClawConfig;
43
+ }): XYThreadBindingManager;
44
+ /**
45
+ * Gets the thread binding manager for a given account ID.
46
+ */
47
+ export declare function getXYThreadBindingManager(accountId?: string): XYThreadBindingManager | null;
48
+ /**
49
+ * Testing utilities for thread bindings.
50
+ */
51
+ export declare const __testing: {
52
+ resetXYThreadBindingsForTests(): void;
53
+ };
54
+ export {};