@ynhcj/xiaoyi-channel 0.0.154-beta → 0.0.154-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 (76) hide show
  1. package/dist/src/bot.js +13 -5
  2. package/dist/src/cron-command.d.ts +4 -5
  3. package/dist/src/cron-command.js +19 -34
  4. package/dist/src/cron-query-handler.d.ts +7 -0
  5. package/dist/src/cron-query-handler.js +188 -0
  6. package/dist/src/cspl/call_api.d.ts +1 -1
  7. package/dist/src/cspl/call_api.js +2 -2
  8. package/dist/src/cspl/config.js +30 -10
  9. package/dist/src/cspl/constants.d.ts +3 -0
  10. package/dist/src/cspl/constants.js +5 -0
  11. package/dist/src/cspl/sentinel_hook.js +26 -7
  12. package/dist/src/cspl/utils.d.ts +9 -3
  13. package/dist/src/cspl/utils.js +17 -11
  14. package/dist/src/file-upload.d.ts +5 -0
  15. package/dist/src/file-upload.js +101 -0
  16. package/dist/src/formatter.d.ts +32 -0
  17. package/dist/src/formatter.js +62 -6
  18. package/dist/src/monitor.js +44 -23
  19. package/dist/src/parser.d.ts +6 -0
  20. package/dist/src/parser.js +47 -1
  21. package/dist/src/provider.js +25 -17
  22. package/dist/src/push.d.ts +11 -1
  23. package/dist/src/push.js +93 -2
  24. package/dist/src/reply-dispatcher.js +51 -19
  25. package/dist/src/self-evolution-handler.d.ts +1 -1
  26. package/dist/src/self-evolution-handler.js +13 -2
  27. package/dist/src/tools/agent-as-skill-tool.js +55 -4
  28. package/dist/src/tools/calendar-tool.js +1 -1
  29. package/dist/src/tools/call-device-tool.js +0 -3
  30. package/dist/src/tools/call-phone-tool.js +1 -1
  31. package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
  32. package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
  33. package/dist/src/tools/create-alarm-tool.js +1 -1
  34. package/dist/src/tools/create-all-tools.js +8 -4
  35. package/dist/src/tools/delete-alarm-tool.js +1 -1
  36. package/dist/src/tools/device-tool-map.js +1 -0
  37. package/dist/src/tools/discover-cross-devices-tool.js +1 -1
  38. package/dist/src/tools/display-a2ui-card-tool.d.ts +2 -0
  39. package/dist/src/tools/display-a2ui-card-tool.js +85 -0
  40. package/dist/src/tools/get-collection-tool-schema.js +1 -1
  41. package/dist/src/tools/get-device-file-tool-schema.js +2 -3
  42. package/dist/src/tools/location-tool.js +1 -1
  43. package/dist/src/tools/modify-alarm-tool.js +1 -1
  44. package/dist/src/tools/modify-note-tool.js +1 -1
  45. package/dist/src/tools/note-tool.js +1 -1
  46. package/dist/src/tools/query-app-message-tool.js +1 -1
  47. package/dist/src/tools/query-memory-data-tool.js +1 -1
  48. package/dist/src/tools/query-todo-task-tool.js +1 -1
  49. package/dist/src/tools/save-file-to-phone-tool.js +1 -1
  50. package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
  51. package/dist/src/tools/schema-tool-factory.js +1 -1
  52. package/dist/src/tools/search-alarm-tool.js +1 -1
  53. package/dist/src/tools/search-calendar-tool.js +1 -1
  54. package/dist/src/tools/search-contact-tool.js +1 -1
  55. package/dist/src/tools/search-email-tool.js +1 -1
  56. package/dist/src/tools/search-file-tool.js +5 -10
  57. package/dist/src/tools/search-note-tool.js +1 -1
  58. package/dist/src/tools/search-photo-gallery-tool.js +1 -1
  59. package/dist/src/tools/send-cross-device-task-tool.js +18 -22
  60. package/dist/src/tools/send-email-tool.js +1 -1
  61. package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
  62. package/dist/src/tools/send-file-to-user-tool.js +35 -6
  63. package/dist/src/tools/send-html-card-tool.d.ts +7 -0
  64. package/dist/src/tools/send-html-card-tool.js +113 -0
  65. package/dist/src/tools/session-manager.d.ts +6 -2
  66. package/dist/src/tools/session-manager.js +42 -7
  67. package/dist/src/tools/upload-file-tool.d.ts +1 -1
  68. package/dist/src/tools/upload-file-tool.js +3 -17
  69. package/dist/src/tools/upload-photo-tool.js +1 -1
  70. package/dist/src/tools/xiaoyi-add-collection-tool.js +3 -2
  71. package/dist/src/tools/xiaoyi-collection-tool.js +2 -2
  72. package/dist/src/tools/xiaoyi-delete-collection-tool.js +2 -2
  73. package/dist/src/tools/xiaoyi-gui-tool.js +5 -0
  74. package/dist/src/types.d.ts +10 -3
  75. package/dist/src/websocket.js +44 -5
  76. package/package.json +1 -2
@@ -17,6 +17,11 @@ export declare class XYFileUploadService {
17
17
  * Uses completeAndQuery endpoint to get the file URL directly.
18
18
  */
19
19
  uploadFileAndGetUrl(filePath: string, objectType?: string): Promise<string>;
20
+ /**
21
+ * Upload a file and return a preview-able URL (needPreview=true).
22
+ * Same as uploadFileAndGetUrl but adds needPreview flag to get a directly viewable URL.
23
+ */
24
+ uploadFileAndGetPreviewUrl(filePath: string, objectType?: string): Promise<string>;
20
25
  /**
21
26
  * Upload multiple files and return their file IDs.
22
27
  */
@@ -235,6 +235,107 @@ export class XYFileUploadService {
235
235
  }
236
236
  }
237
237
  }
238
+ /**
239
+ * Upload a file and return a preview-able URL (needPreview=true).
240
+ * Same as uploadFileAndGetUrl but adds needPreview flag to get a directly viewable URL.
241
+ */
242
+ async uploadFileAndGetPreviewUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
243
+ let localFilePath = filePath;
244
+ let isTempFile = false;
245
+ try {
246
+ // Handle remote URLs by downloading first
247
+ if (isRemoteUrl(filePath)) {
248
+ localFilePath = await downloadToTempFile(filePath);
249
+ isTempFile = true;
250
+ }
251
+ // Read file
252
+ const fileBuffer = await fs.readFile(localFilePath);
253
+ const fileName = path.basename(localFilePath);
254
+ const fileSha256 = calculateSHA256(fileBuffer);
255
+ const fileSize = fileBuffer.length;
256
+ // Phase 1: Prepare
257
+ logger.log(`[XY File Upload] Phase 1 (preview): Prepare upload for ${fileName}`);
258
+ const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/json",
262
+ "x-uid": this.uid,
263
+ "x-api-key": this.apiKey,
264
+ "x-request-from": "openclaw",
265
+ },
266
+ body: JSON.stringify({
267
+ objectType,
268
+ fileName,
269
+ fileSha256,
270
+ fileSize,
271
+ fileOwnerInfo: {
272
+ uid: this.uid,
273
+ teamId: this.uid,
274
+ },
275
+ useEdge: false,
276
+ }),
277
+ });
278
+ if (!prepareResp.ok) {
279
+ throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
280
+ }
281
+ const prepareData = await prepareResp.json();
282
+ if (prepareData.code !== "0") {
283
+ throw new Error(`Prepare failed: ${prepareData.desc}`);
284
+ }
285
+ const { objectId, draftId, uploadInfos } = prepareData;
286
+ logger.log(`[XY File Upload] Prepare (preview) complete: objectId=${objectId}, draftId=${draftId}`);
287
+ // Phase 2: Upload
288
+ logger.log(`[XY File Upload] Phase 2 (preview): Upload file data`);
289
+ const uploadInfo = uploadInfos[0];
290
+ const uploadResp = await fetch(uploadInfo.url, {
291
+ method: uploadInfo.method,
292
+ headers: uploadInfo.headers,
293
+ body: fileBuffer,
294
+ });
295
+ if (!uploadResp.ok) {
296
+ throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
297
+ }
298
+ logger.log(`[XY File Upload] Upload (preview) complete`);
299
+ // Phase 3: CompleteAndQuery with needPreview=true
300
+ logger.log(`[XY File Upload] Phase 3 (preview): CompleteAndQuery with needPreview=true`);
301
+ const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
302
+ method: "POST",
303
+ headers: {
304
+ "Content-Type": "application/json",
305
+ "x-uid": this.uid,
306
+ "x-api-key": this.apiKey,
307
+ "x-request-from": "openclaw",
308
+ },
309
+ body: JSON.stringify({
310
+ objectId,
311
+ draftId,
312
+ needPreview: true,
313
+ }),
314
+ });
315
+ if (!completeResp.ok) {
316
+ throw new Error(`CompleteAndQuery (preview) failed: HTTP ${completeResp.status}`);
317
+ }
318
+ const completeData = await completeResp.json();
319
+ const fileUrl = completeData?.fileDetailInfo?.url || "";
320
+ if (!fileUrl) {
321
+ throw new Error("No file URL returned from completeAndQuery (preview)");
322
+ }
323
+ logger.log(`[XY File Upload] File upload with preview URL successful`);
324
+ return fileUrl;
325
+ }
326
+ catch (error) {
327
+ logger.error(`[XY File Upload] File upload with preview URL failed for ${filePath}:`, error);
328
+ throw error;
329
+ }
330
+ finally {
331
+ if (isTempFile) {
332
+ try {
333
+ await fs.unlink(localFilePath);
334
+ }
335
+ catch { }
336
+ }
337
+ }
338
+ }
238
339
  /**
239
340
  * Upload multiple files and return their file IDs.
240
341
  */
@@ -17,6 +17,7 @@ export interface SendA2AResponseParams {
17
17
  }>;
18
18
  errorCode?: number | string;
19
19
  errorMessage?: string;
20
+ log?: boolean;
20
21
  }
21
22
  /**
22
23
  * Send an A2A artifact update response.
@@ -67,6 +68,8 @@ export interface SendCommandParams {
67
68
  commands?: A2ACommand[];
68
69
  /** toolCallId from the tool's execute() — used for cron detection via hook-set Map. */
69
70
  toolCallId?: string;
71
+ /** When true, the artifact-update is sent with final=true. Default: false. */
72
+ final?: boolean;
70
73
  }
71
74
  /**
72
75
  * Send a command as an artifact update (final=false).
@@ -79,6 +82,35 @@ export interface SendCommandParams {
79
82
  * listening in the calling tool works unchanged.
80
83
  */
81
84
  export declare function sendCommand(params: SendCommandParams): Promise<void>;
85
+ /**
86
+ * Parameters for sending a card (e.g., HTML H5 card).
87
+ */
88
+ export interface SendCardParams {
89
+ config: XYChannelConfig;
90
+ sessionId: string;
91
+ taskId: string;
92
+ messageId: string;
93
+ /** toolCallId from the tool's execute() — used for cron detection via hook-set Map. */
94
+ toolCallId?: string;
95
+ /** When true, the artifact-update is sent with final=true. Default: false. */
96
+ final?: boolean;
97
+ /** Array of card data objects to send. */
98
+ cardsInfo: CardDataObject[];
99
+ }
100
+ /**
101
+ * Card data object for sending display cards.
102
+ */
103
+ export interface CardDataObject {
104
+ cardName: string;
105
+ cardData: Record<string, any>;
106
+ displayType: string;
107
+ }
108
+ /**
109
+ * Send a card (e.g., HTML H5 card) as an artifact update (final=false).
110
+ *
111
+ * Cron-aware: same routing logic as sendCommand.
112
+ */
113
+ export declare function sendCard(params: SendCardParams): Promise<void>;
82
114
  /**
83
115
  * Parameters for sending a clearContext response.
84
116
  */
@@ -42,7 +42,7 @@ function buildTextPreview(text) {
42
42
  * Send an A2A artifact update response.
43
43
  */
44
44
  export async function sendA2AResponse(params) {
45
- const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage } = params;
45
+ const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage, log: shouldLog = true } = params;
46
46
  const log = logger.withContext(sessionId, taskId);
47
47
  // 审批桥接:将 OpenClaw 的审批提示翻译成用户友好的确认文案
48
48
  const bridgedText = text === undefined ? text : rewriteOutboundApprovalText(sessionId, text);
@@ -97,11 +97,14 @@ export async function sendA2AResponse(params) {
97
97
  taskId,
98
98
  msgDetail: JSON.stringify(jsonRpcResponse),
99
99
  };
100
- // Log complete response body
101
- const redactedText = redactSensitiveText(bridgedText ?? "");
102
- log.log(`[A2A_RESPONSE] Sending artifact-update, append=${append}, final=${final}, text=${buildTextPreview(redactedText)}, files=${files?.length ?? 0}, sensitive=${containsSensitiveInfo(bridgedText ?? "")}`);
100
+ if (shouldLog) {
101
+ const redactedText = redactSensitiveText(bridgedText ?? "");
102
+ log.log(`[A2A_RESPONSE] Sending artifact-update, append=${append}, final=${final}, text=${buildTextPreview(redactedText)}, files=${files?.length ?? 0}, sensitive=${containsSensitiveInfo(bridgedText ?? "")}`);
103
+ }
103
104
  await wsManager.sendMessage(sessionId, outboundMessage);
104
- log.log(`[A2A_RESPONSE] Message sent successfully`);
105
+ if (shouldLog) {
106
+ log.log(`[A2A_RESPONSE] Message sent successfully`);
107
+ }
105
108
  }
106
109
  /**
107
110
  * Send an A2A artifact-update with reasoningText part.
@@ -234,7 +237,7 @@ export async function sendCommand(params) {
234
237
  kind: "artifact-update",
235
238
  append: false,
236
239
  lastChunk: true,
237
- final: false, // Commands are not final
240
+ final: params.final ?? false,
238
241
  artifact: {
239
242
  artifactId: uuidv4(),
240
243
  parts: [
@@ -269,6 +272,59 @@ export async function sendCommand(params) {
269
272
  await wsManager.sendMessage(sessionId, outboundMessage);
270
273
  log.log(`[A2A_COMMAND] Command sent successfully`);
271
274
  }
275
+ /**
276
+ * Send a card (e.g., HTML H5 card) as an artifact update (final=false).
277
+ *
278
+ * Cron-aware: same routing logic as sendCommand.
279
+ */
280
+ export async function sendCard(params) {
281
+ const { config, sessionId, taskId, messageId, toolCallId } = params;
282
+ // ── Cron mode: route through push channel ──────────────────────
283
+ if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
284
+ throw new Error("sendCard does not support cron mode");
285
+ }
286
+ // ── Normal mode: WebSocket ─────────────────────────────────────
287
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
288
+ const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
289
+ const log = logger.withContext(sessionId, currentTaskId);
290
+ // Build artifact update with cardsInfo as data
291
+ const artifact = {
292
+ taskId: currentTaskId,
293
+ kind: "artifact-update",
294
+ append: false,
295
+ lastChunk: true,
296
+ final: params.final ?? false,
297
+ artifact: {
298
+ artifactId: uuidv4(),
299
+ parts: [
300
+ {
301
+ kind: "data",
302
+ data: {
303
+ cardsInfo: params.cardsInfo,
304
+ },
305
+ },
306
+ ],
307
+ },
308
+ };
309
+ // Build JSON-RPC response
310
+ const jsonRpcResponse = {
311
+ jsonrpc: "2.0",
312
+ id: currentMessageId,
313
+ result: artifact,
314
+ };
315
+ // Send via WebSocket
316
+ const wsManager = getXYWebSocketManager(config);
317
+ const outboundMessage = {
318
+ msgType: "agent_response",
319
+ agentId: config.agentId,
320
+ sessionId,
321
+ taskId: currentTaskId,
322
+ msgDetail: JSON.stringify(jsonRpcResponse),
323
+ };
324
+ log.log(`[A2A_CARD] Sending card`);
325
+ await wsManager.sendMessage(sessionId, outboundMessage);
326
+ log.log(`[A2A_CARD] Card sent successfully`);
327
+ }
272
328
  /**
273
329
  * Send a clearContext response.
274
330
  */
@@ -7,6 +7,7 @@ import { sendA2AResponse } from "./formatter.js";
7
7
  import { handleTriggerEvent } from "./trigger-handler.js";
8
8
  import { handleSelfEvolutionEvent, handleSelfEvolutionStateGetEvent } from "./self-evolution-handler.js";
9
9
  import { handleLoginTokenEvent } from "./login-token-handler.js";
10
+ import { handleCronQueryEvent } from "./cron-query-handler.js";
10
11
  import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
11
12
  import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./tools/session-manager.js";
12
13
  import { logger } from "./utils/logger.js";
@@ -115,35 +116,45 @@ export async function monitorXYProvider(opts = {}) {
115
116
  }
116
117
  };
117
118
  // 🔑 核心改造:检测steer模式
118
- // 需要提前解析消息以获取sessionId
119
- try {
120
- const parsed = parseA2AMessage(message);
121
- const steerMode = cfg.messages?.queue?.mode === "steer";
122
- const hasActiveRun = hasActiveTask(parsed.sessionId);
123
- if (steerMode && hasActiveRun) {
124
- // Steer模式且有活跃任务:不入队列,直接并发执行
125
- logger.log(`[MONITOR-HANDLER] STEER MODE: Executing concurrently for messageKey=${messageKey}`);
126
- void task().catch((err) => {
127
- logger.error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
128
- activeMessages.delete(messageKey);
129
- });
119
+ // clearContext / tasks/cancel 没有 params,跳过 parseA2AMessage 直接入队
120
+ const messageMethod = message.method;
121
+ if (messageMethod === "clearContext" || messageMethod === "clear_context"
122
+ || messageMethod === "tasks/cancel" || messageMethod === "tasks_cancel") {
123
+ void enqueue(sessionId, task).catch((err) => {
124
+ logger.error(`XY gateway: queue processing failed: ${String(err)}`);
125
+ activeMessages.delete(messageKey);
126
+ });
127
+ }
128
+ else {
129
+ try {
130
+ const parsed = parseA2AMessage(message);
131
+ const steerMode = cfg.messages?.queue?.mode === "steer";
132
+ const hasActiveRun = hasActiveTask(parsed.sessionId);
133
+ if (steerMode && hasActiveRun) {
134
+ // Steer模式且有活跃任务:不入队列,直接并发执行
135
+ logger.log(`[MONITOR-HANDLER] STEER MODE: Executing concurrently for messageKey=${messageKey}`);
136
+ void task().catch((err) => {
137
+ logger.error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
138
+ activeMessages.delete(messageKey);
139
+ });
140
+ }
141
+ else {
142
+ // 正常模式:入队列串行执行
143
+ void enqueue(sessionId, task).catch((err) => {
144
+ logger.error(`XY gateway: queue processing failed: ${String(err)}`);
145
+ activeMessages.delete(messageKey);
146
+ });
147
+ }
130
148
  }
131
- else {
132
- // 正常模式:入队列串行执行
149
+ catch (parseErr) {
150
+ // 解析失败,回退到正常队列模式
151
+ logger.error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
133
152
  void enqueue(sessionId, task).catch((err) => {
134
153
  logger.error(`XY gateway: queue processing failed: ${String(err)}`);
135
154
  activeMessages.delete(messageKey);
136
155
  });
137
156
  }
138
157
  }
139
- catch (parseErr) {
140
- // 解析失败,回退到正常队列模式
141
- logger.error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
142
- void enqueue(sessionId, task).catch((err) => {
143
- logger.error(`XY gateway: queue processing failed: ${String(err)}`);
144
- activeMessages.delete(messageKey);
145
- });
146
- }
147
158
  };
148
159
  const connectedHandler = (serverId) => {
149
160
  if (!loggedServers.has(serverId)) {
@@ -174,7 +185,9 @@ export async function monitorXYProvider(opts = {}) {
174
185
  };
175
186
  const selfEvolutionHandler = (context) => {
176
187
  logger.log(`[MONITOR] Received self-evolution-event, dispatching to handler...`);
177
- handleSelfEvolutionEvent(context, runtime);
188
+ handleSelfEvolutionEvent(context, cfg).catch((err) => {
189
+ logger.error(`[MONITOR] Failed to handle self-evolution-event:`, err);
190
+ });
178
191
  };
179
192
  const selfEvolutionStateGetHandler = (context) => {
180
193
  logger.log(`[MONITOR] Received self-evolution-state-get-event, dispatching to handler...`);
@@ -186,6 +199,12 @@ export async function monitorXYProvider(opts = {}) {
186
199
  logger.log(`[MONITOR] Received login-token-event, dispatching to handler...`);
187
200
  handleLoginTokenEvent(context, runtime);
188
201
  };
202
+ const cronQueryEventHandler = (context) => {
203
+ logger.log(`[MONITOR] Received cron-query-event, dispatching to handler...`);
204
+ handleCronQueryEvent(context, cfg).catch((err) => {
205
+ logger.error(`[MONITOR] Failed to handle cron-query-event:`, err);
206
+ });
207
+ };
189
208
  const cleanup = () => {
190
209
  logger.log("XY gateway: cleaning up...");
191
210
  // 🔍 Diagnose before cleanup
@@ -206,6 +225,7 @@ export async function monitorXYProvider(opts = {}) {
206
225
  wsManager.off("self-evolution-event", selfEvolutionHandler);
207
226
  wsManager.off("self-evolution-state-get-event", selfEvolutionStateGetHandler);
208
227
  wsManager.off("login-token-event", loginTokenEventHandler);
228
+ wsManager.off("cron-query-event", cronQueryEventHandler);
209
229
  // ✅ Disconnect the wsManager to prevent connection leaks
210
230
  // This is safe because each gateway lifecycle should have clean connections
211
231
  wsManager.disconnect();
@@ -269,6 +289,7 @@ export async function monitorXYProvider(opts = {}) {
269
289
  wsManager.on("self-evolution-event", selfEvolutionHandler);
270
290
  wsManager.on("self-evolution-state-get-event", selfEvolutionStateGetHandler);
271
291
  wsManager.on("login-token-event", loginTokenEventHandler);
292
+ wsManager.on("cron-query-event", cronQueryEventHandler);
272
293
  // Start periodic health check (every 6 hours)
273
294
  logger.log("Starting periodic health check (every 6 hours)...");
274
295
  healthCheckInterval = setInterval(() => {
@@ -50,6 +50,12 @@ export declare function extractPushId(parts: A2AMessagePart[]): string | null;
50
50
  * (same level as push_id).
51
51
  */
52
52
  export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
53
+ /**
54
+ * Extract modelName from message parts.
55
+ * Looks for modelName in data parts under variables.memoryVariables.modelName
56
+ * (same level as systemVariables).
57
+ */
58
+ export declare function extractModelName(parts: A2AMessagePart[]): string | null;
53
59
  /**
54
60
  * Extract Trigger event data from message parts.
55
61
  * Looks for Trigger events with pushDataId in data parts.
@@ -46,6 +46,36 @@ export function extractDataEvents(parts) {
46
46
  .filter((event) => event !== undefined);
47
47
  }
48
48
  export function extractRunCrossTaskContext(parts) {
49
+ const normalizeSentFiles = (value) => {
50
+ if (!Array.isArray(value)) {
51
+ return [];
52
+ }
53
+ return value
54
+ .map((item) => {
55
+ if (!item || typeof item !== "object") {
56
+ return null;
57
+ }
58
+ const candidate = item;
59
+ const fileLocalUrls = Array.isArray(candidate.fileLocalUrls)
60
+ ? candidate.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
61
+ : [];
62
+ const fileRemoteUrls = Array.isArray(candidate.fileRemoteUrls)
63
+ ? candidate.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
64
+ : [];
65
+ const fileNames = Array.isArray(candidate.fileNames)
66
+ ? candidate.fileNames.filter((name) => typeof name === "string" && name.length > 0)
67
+ : [];
68
+ if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
69
+ return null;
70
+ }
71
+ return {
72
+ ...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
73
+ ...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
74
+ ...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
75
+ };
76
+ })
77
+ .filter((item) => item !== null);
78
+ };
49
79
  for (const part of parts) {
50
80
  if (part.kind !== "data" || !part.data) {
51
81
  continue;
@@ -64,7 +94,7 @@ export function extractRunCrossTaskContext(parts) {
64
94
  isDistributed: context.isDistributed === true,
65
95
  networkId,
66
96
  isSupportAgent: context.isSupportAgent !== false,
67
- fileUrls: Array.isArray(context.fileUrls) ? context.fileUrls.filter((url) => typeof url === "string") : [],
97
+ sentFiles: normalizeSentFiles(context.sentFiles),
68
98
  rawContext: context,
69
99
  };
70
100
  }
@@ -113,6 +143,22 @@ export function extractDeviceType(parts) {
113
143
  }
114
144
  return null;
115
145
  }
146
+ /**
147
+ * Extract modelName from message parts.
148
+ * Looks for modelName in data parts under variables.memoryVariables.modelName
149
+ * (same level as systemVariables).
150
+ */
151
+ export function extractModelName(parts) {
152
+ for (const part of parts) {
153
+ if (part.kind === "data" && part.data) {
154
+ const modelName = part.data.variables?.memoryVariables?.modelName;
155
+ if (modelName && typeof modelName === "string" && modelName.trim() !== "" && modelName.toLowerCase() !== "none") {
156
+ return modelName;
157
+ }
158
+ }
159
+ }
160
+ return null;
161
+ }
116
162
  /**
117
163
  * Extract Trigger event data from message parts.
118
164
  * Looks for Trigger events with pushDataId in data parts.
@@ -536,26 +536,28 @@ export const xiaoyiProvider = {
536
536
  const beforeLen = sp.length;
537
537
  // 删除 ## Tooling 与 TOOLS.md 声明之间的内容
538
538
  sp = sp.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");
539
- // (1) 提取 ## Skills (mandatory) </available_skills> 作为第一部分
540
- const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
541
- const part1 = skillsMatch ? skillsMatch[0] : '';
542
- // (2) 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 ## /home/sandbox/.openclaw/workspace/TOOLS.md 之前的内容作为第二部分
543
- const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?)(?=## \/home\/sandbox\/\.openclaw\/workspace\/TOOLS\.md)/);
544
- const part2 = soulMatch ? soulMatch[1].trim() : '';
545
- if (part1 || part2) {
546
- // 从原始位置删除已提取的部分
547
- if (skillsMatch)
548
- sp = sp.replace(skillsMatch[0], '');
549
- if (soulMatch)
539
+ // (1) Skills 部分:移动到 ## Runtime 之前
540
+ if (sp.includes('## Runtime')) {
541
+ // 提取 ## Skills (mandatory) </available_skills> 作为第一部分
542
+ const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
543
+ if (skillsMatch) {
544
+ const part1 = skillsMatch[0];
545
+ sp = sp.replace(part1, '');
546
+ sp = sp.replace('## Runtime', part1 + '\n\n## Runtime');
547
+ }
548
+ }
549
+ // (2) SOUL.md 部分:移动到 ## Silent Replies 之前
550
+ if (sp.includes('## Silent Replies')) {
551
+ // 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 其特定脚注结束标志 的内容作为第二部分
552
+ const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?_This file is yours to evolve\. As you learn who you are, update it\._)/);
553
+ if (soulMatch) {
554
+ const part2 = soulMatch[1].trim();
550
555
  sp = sp.replace(soulMatch[1], '');
551
- // 清理多余空行
552
- sp = sp.replace(/\n{3,}/g, '\n\n');
553
- // (3) 将 第二部分 + 第一部分 插入到 ## Runtime 上面
554
- const combined = (part2 + '\n\n' + part1).trim();
555
- if (combined && sp.includes('## Runtime')) {
556
- sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
556
+ sp = sp.replace('## Silent Replies', part2 + '\n\n## Silent Replies');
557
557
  }
558
558
  }
559
+ // 清理多余空行
560
+ sp = sp.replace(/\n{3,}/g, '\n\n');
559
561
  logger.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
560
562
  context.systemPrompt = sp;
561
563
  }
@@ -585,6 +587,12 @@ export const xiaoyiProvider = {
585
587
  }
586
588
  }
587
589
  }
590
+ // ── Override model.id if A2A message specified modelName ──
591
+ const modelNameOverride = getCurrentSessionContext()?.modelName;
592
+ if (modelNameOverride && modelNameOverride.trim() !== "" && modelNameOverride.toLowerCase() !== "none") {
593
+ logger.log(`[xiaoyiprovider] overriding model.id: ${model.id} → ${modelNameOverride}`);
594
+ model = { ...model, id: modelNameOverride };
595
+ }
588
596
  // ── Retry-capable streaming ──────────────────────────────
589
597
  const cronJob = isCronTriggered(context.messages);
590
598
  if (cronJob)
@@ -5,9 +5,14 @@ import type { XYChannelConfig } from "./types.js";
5
5
  */
6
6
  export declare class XYPushService {
7
7
  private config;
8
- private readonly DEFAULT_PUSH_URL;
8
+ private readonly PROD_PUSH_URL;
9
+ private readonly TEST_PUSH_URL;
9
10
  private readonly REQUEST_FROM;
10
11
  constructor(config: XYChannelConfig);
12
+ /**
13
+ * Resolve push URL: config.pushUrl > inferred from fileUploadUrl > production default.
14
+ */
15
+ private resolvePushUrl;
11
16
  /**
12
17
  * Generate a random trace ID for request tracking.
13
18
  */
@@ -23,6 +28,11 @@ export declare class XYPushService {
23
28
  * @param pushId - Push ID to use (required)
24
29
  */
25
30
  sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string, pushDataId?: string, pushId?: string): Promise<void>;
31
+ /**
32
+ * Send a push message with command directives embedded directly.
33
+ * Used for cron-triggered commands where pushText is empty and pushType=101.
34
+ */
35
+ sendPushWithDirectives(pushId: string, sessionId: string, directives: any[]): Promise<void>;
26
36
  /**
27
37
  * Send a push message with file attachments.
28
38
  */