@ynhcj/xiaoyi-channel 0.0.188-beta → 0.0.190-beta

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.
@@ -217,11 +217,12 @@ export async function sendCommand(params) {
217
217
  if (commands.length === 0) {
218
218
  throw new Error("sendCommand requires command or commands.");
219
219
  }
220
- // ── Cron mode: disabled ────────────────────────────────────────
221
- // sendCommandViaPush is disabled in this version. Cron-triggered
222
- // tool calls that try to send commands will be rejected.
220
+ // ── Cron mode: route through push channel ──────────────────────
221
+ // Detected via: (a) sessionId "cron-" prefix from synthetic session, OR
222
+ // (b) toolCallId marked by before_tool_call hook from openclaw's sessionKey.
223
223
  if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
224
- throw new Error("sendCommandViaPush is disabled in this version");
224
+ const { sendCommandViaPush } = await import("./cron-command.js");
225
+ return sendCommandViaPush({ config, command: commands[0] });
225
226
  }
226
227
  // ── Normal mode: WebSocket ─────────────────────────────────────
227
228
  // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
@@ -56,22 +56,16 @@ export function extractRunCrossTaskContext(parts) {
56
56
  return null;
57
57
  }
58
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) {
59
+ const fileName = typeof candidate.fileName === "string" ? candidate.fileName.trim() : "";
60
+ const fileId = typeof candidate.fileId === "string" ? candidate.fileId.trim() : "";
61
+ const mimeType = typeof candidate.mimeType === "string" ? candidate.mimeType.trim() : "";
62
+ if (!fileName || !fileId) {
69
63
  return null;
70
64
  }
71
65
  return {
72
- ...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
73
- ...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
74
- ...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
66
+ fileName,
67
+ fileId,
68
+ ...(mimeType ? { mimeType } : {}),
75
69
  };
76
70
  })
77
71
  .filter((item) => item !== null);
@@ -39,17 +39,23 @@ function buildCrossTaskExecuteResultCommand(code, message, sentFiles = []) {
39
39
  async function sendRunCrossTaskResult(params) {
40
40
  const { config, sessionId, taskId, messageId, context, resultCode, resultMessage } = params;
41
41
  const sentFiles = Array.isArray(context.sentFiles) ? context.sentFiles : [];
42
+ const fileCardCount = sentFiles.length;
42
43
  const statusCommand = buildDistributionStatusCommand(context);
43
44
  const resultCommand = buildCrossTaskExecuteResultCommand(resultCode, resultMessage, sentFiles);
44
- await sendCommand({
45
- config,
46
- sessionId,
47
- taskId,
48
- messageId,
49
- commands: [statusCommand, resultCommand],
50
- });
51
- clearRunCrossTaskSentFiles(context);
52
- logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, sentFileCount=${sentFiles.length}, clearedSentFileCount=${sentFiles.length}, messageLength=${resultMessage.length}`);
45
+ try {
46
+ await sendCommand({
47
+ config,
48
+ sessionId,
49
+ taskId,
50
+ messageId,
51
+ commands: [statusCommand, resultCommand],
52
+ });
53
+ logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, fileCardCount=${fileCardCount}, messageLength=${resultMessage.length}`);
54
+ }
55
+ finally {
56
+ clearRunCrossTaskSentFiles(context);
57
+ logger.log(`${RUN_CROSS_TASK_LOG_TAG} cleared cross-task sentFiles, sessionId=${sessionId}, taskId=${taskId}, clearedFileCardCount=${fileCardCount}`);
58
+ }
53
59
  }
54
60
  /**
55
61
  * 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
@@ -116,6 +122,7 @@ export function createXYReplyDispatcher(params) {
116
122
  let hasSentResponse = false;
117
123
  let finalSent = false;
118
124
  let accumulatedText = "";
125
+ let finalReplyText = "";
119
126
  const initialRunCrossTaskContext = getCurrentSessionContext()?.runCrossTaskContext;
120
127
  const getRunCrossTaskContext = () => {
121
128
  return getCurrentSessionContext()?.runCrossTaskContext ?? initialRunCrossTaskContext;
@@ -172,6 +179,10 @@ export function createXYReplyDispatcher(params) {
172
179
  scopedLog().log(`[DELIVER SKIP] Empty text, skipping`);
173
180
  return;
174
181
  }
182
+ if (info?.kind === "final") {
183
+ finalReplyText = text;
184
+ scopedLog().log(`[DELIVER] Captured final reply text, length=${finalReplyText.length}`);
185
+ }
175
186
  // 🔑 如果 onPartialReply 已经流式发送过文本,deliver 不再重复发送
176
187
  if (hasSentResponse) {
177
188
  scopedLog().log(`[DELIVER SKIP] Already sent via onPartialReply`);
@@ -232,7 +243,11 @@ export function createXYReplyDispatcher(params) {
232
243
  }
233
244
  // 正常模式(或未被steer的dispatch)
234
245
  if (hasSentResponse && !finalSent) {
235
- scopedLog().log(`[ON-IDLE] Sending accumulated text, length=${accumulatedText.length}`);
246
+ const trimmedFinalReplyText = finalReplyText.trim();
247
+ const trimmedAccumulatedText = accumulatedText.trim();
248
+ const crossTaskResultMessage = trimmedFinalReplyText || trimmedAccumulatedText;
249
+ const crossTaskResultSource = trimmedFinalReplyText ? "final" : "accumulated";
250
+ scopedLog().log(`[ON-IDLE] [SendCrossResult]Sending cross-task result, source=${crossTaskResultSource}, resultMessage.length=${crossTaskResultMessage.length}`);
236
251
  try {
237
252
  const runCrossTaskContext = getRunCrossTaskContext();
238
253
  if (runCrossTaskContext) {
@@ -243,7 +258,7 @@ export function createXYReplyDispatcher(params) {
243
258
  messageId: currentMessageId,
244
259
  context: runCrossTaskContext,
245
260
  resultCode: "0",
246
- resultMessage: accumulatedText,
261
+ resultMessage: crossTaskResultMessage,
247
262
  });
248
263
  }
249
264
  // 🔑 使用动态taskId发送完成状态
@@ -16,6 +16,11 @@ import { createGetAlarmToolSchemaTool } from "./get-alarm-tool-schema.js";
16
16
  import { createGetCollectionToolSchemaTool } from "./get-collection-tool-schema.js";
17
17
  // import { createGetEmailToolSchemaTool } from "./get-email-tool-schema.js";
18
18
  import { createLoginTokenTool } from "./login-token-tool.js";
19
+ import { createAgentAsSkillTool } from "./agent-as-skill-tool.js";
20
+ import { createDiscoverCrossDevicesTool } from "./discover-cross-devices-tool.js";
21
+ import { createSendCrossDeviceTaskTool } from "./send-cross-device-task-tool.js";
22
+ import { createDisplayA2UICardTool } from "./display-a2ui-card-tool.js";
23
+ import { createCheckPluginPrivilegeTool } from "./check-plugin-privilege-tool.js";
19
24
  import { logger } from "../utils/logger.js";
20
25
  /**
21
26
  * Create all XY channel tools for the given session context.
@@ -31,9 +36,9 @@ export function createAllTools(ctx) {
31
36
  logger.log(`[CREATE-ALL-TOOLS] creating tools`);
32
37
  return [
33
38
  createLocationTool(ctx),
34
- // createDiscoverCrossDevicesTool(ctx),
35
- // createSendCrossDeviceTaskTool(ctx),
36
- // createDisplayA2UICardTool(ctx),
39
+ createDiscoverCrossDevicesTool(ctx),
40
+ createSendCrossDeviceTaskTool(ctx),
41
+ createDisplayA2UICardTool(ctx),
37
42
  createCallDeviceTool(ctx),
38
43
  createGetNoteToolSchemaTool(ctx),
39
44
  createGetCalendarToolSchemaTool(ctx),
@@ -51,7 +56,7 @@ export function createAllTools(ctx) {
51
56
  timestampToUtc8Tool,
52
57
  createSaveSelfEvolutionSkillTool(ctx),
53
58
  createLoginTokenTool(ctx),
54
- // createAgentAsSkillTool(ctx),
55
- // createCheckPluginPrivilegeTool(ctx),
59
+ createAgentAsSkillTool(ctx),
60
+ createCheckPluginPrivilegeTool(ctx),
56
61
  ];
57
62
  }
@@ -1,7 +1,6 @@
1
1
  import { sendCommand, sendStatusUpdate } from "../formatter.js";
2
2
  import { getXYWebSocketManager } from "../client.js";
3
3
  import { getCurrentMessageId, getCurrentTaskId } from "../task-manager.js";
4
- import { createSendFileToUserTool } from "./send-file-to-user-tool.js";
5
4
  import { logger } from "../utils/logger.js";
6
5
  const LOG_TAG = "[SendPcDeviceTask]";
7
6
  const SEND_CROSS_RESULT_LOG_TAG = "[SendCrossResult]";
@@ -28,7 +27,7 @@ function buildModelToolResult(result) {
28
27
  let message = `跨端任务执行结果:${baseMessage}`;
29
28
  if (resultStatus === "对端设备执行任务成功且返回有文件") {
30
29
  if (result.autoSendFileToUser?.success) {
31
- message += "\n\n对端设备返回了文件,系统已自动通过 send_file_to_user 将文件卡片发送给用户。请你基于跨端任务结果生成最终回复,告知用户任务已完成且文件已发送。";
30
+ message += "\n\n对端设备返回了文件,系统已自动将文件卡片发送给用户。请你基于跨端任务结果生成最终回复,告知用户任务已完成且文件已发送。";
32
31
  }
33
32
  else {
34
33
  const errorMessage = result.autoSendFileToUser?.error || "未知错误";
@@ -56,22 +55,92 @@ function buildCrossDeviceResult(params) {
56
55
  };
57
56
  return result;
58
57
  }
58
+ function collectSentFileCards(sentFiles) {
59
+ const cardsByFileId = new Map();
60
+ for (const card of sentFiles) {
61
+ const fileId = typeof card.fileId === "string" ? card.fileId.trim() : "";
62
+ const fileName = typeof card.fileName === "string" ? card.fileName.trim() : "";
63
+ const mimeType = typeof card.mimeType === "string" ? card.mimeType.trim() : "";
64
+ if (!fileId || !fileName || cardsByFileId.has(fileId)) {
65
+ continue;
66
+ }
67
+ cardsByFileId.set(fileId, {
68
+ fileId,
69
+ fileName,
70
+ ...(mimeType ? { mimeType } : {}),
71
+ });
72
+ }
73
+ return Array.from(cardsByFileId.values());
74
+ }
75
+ function countSentFileCards(sentFiles) {
76
+ return collectSentFileCards(sentFiles).length;
77
+ }
78
+ async function sendFileCardsToUser(ctx, fileCards) {
79
+ const { config, sessionId, taskId, messageId } = ctx;
80
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
81
+ const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
82
+ const wsManager = getXYWebSocketManager(config);
83
+ const sentFileCards = [];
84
+ for (const card of fileCards) {
85
+ const agentResponse = {
86
+ msgType: "agent_response",
87
+ agentId: config.agentId,
88
+ sessionId,
89
+ taskId: currentTaskId,
90
+ msgDetail: JSON.stringify({
91
+ jsonrpc: "2.0",
92
+ id: currentMessageId,
93
+ result: {
94
+ kind: "artifact-update",
95
+ append: true,
96
+ lastChunk: false,
97
+ final: false,
98
+ artifact: {
99
+ artifactId: currentTaskId,
100
+ parts: [
101
+ {
102
+ kind: "file",
103
+ file: {
104
+ name: card.fileName,
105
+ mimeType: card.mimeType,
106
+ fileId: card.fileId,
107
+ },
108
+ },
109
+ ],
110
+ },
111
+ },
112
+ error: { code: 0 },
113
+ }),
114
+ };
115
+ logger.log(`${SEND_CROSS_RESULT_LOG_TAG} sending file card by fileId, fileName=${card.fileName}`);
116
+ await wsManager.sendMessage(sessionId, agentResponse);
117
+ sentFileCards.push({ fileName: card.fileName, fileId: card.fileId });
118
+ }
119
+ return sentFileCards;
120
+ }
59
121
  async function autoSendFileToUserIfNeeded(result, ctx) {
60
122
  const sentFiles = Array.isArray(result.sentFiles) ? result.sentFiles : [];
61
123
  if (sentFiles.length === 0) {
62
124
  return result;
63
125
  }
64
- logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending ${sentFiles.length} cross-device file(s) to user`);
126
+ const fileCards = collectSentFileCards(sentFiles);
127
+ if (fileCards.length === 0) {
128
+ const errorMessage = "Cross-device result contains no valid fileCards.";
129
+ logger.error(`${SEND_CROSS_RESULT_LOG_TAG} auto file card send skipped, error=${errorMessage}`);
130
+ return {
131
+ ...result,
132
+ autoSendFileToUser: {
133
+ success: false,
134
+ error: errorMessage,
135
+ },
136
+ };
137
+ }
138
+ logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending cross-device file cards, fileCardCount=${fileCards.length}`);
65
139
  try {
66
- const sendFileTool = createSendFileToUserTool(ctx);
67
- const sendFileResult = await (async () => {
68
- const results = [];
69
- for (const sentFileParams of sentFiles) {
70
- results.push(await sendFileTool.execute("auto_send_cross_device_file", sentFileParams));
71
- }
72
- return results;
73
- })();
74
- logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto send_file_to_user completed`);
140
+ const sendFileResult = {
141
+ fileCards: await sendFileCardsToUser(ctx, fileCards),
142
+ };
143
+ logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto file card send completed, fileCardCount=${sendFileResult.fileCards.length}`);
75
144
  return {
76
145
  ...result,
77
146
  autoSendFileToUser: {
@@ -82,7 +151,7 @@ async function autoSendFileToUserIfNeeded(result, ctx) {
82
151
  }
83
152
  catch (error) {
84
153
  const errorMessage = error instanceof Error ? error.message : String(error);
85
- logger.error(`${SEND_CROSS_RESULT_LOG_TAG} auto send_file_to_user failed, error=${errorMessage}`);
154
+ logger.error(`${SEND_CROSS_RESULT_LOG_TAG} auto file card send failed, error=${errorMessage}`);
86
155
  return {
87
156
  ...result,
88
157
  autoSendFileToUser: {
@@ -218,7 +287,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
218
287
  }
219
288
  settled = true;
220
289
  const modelResult = buildModelToolResult(result);
221
- logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code}, sentFileCount=${result.sentFiles.length}`);
290
+ logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code}, fileCardCount=${countSentFileCards(result.sentFiles)}`);
222
291
  cleanup();
223
292
  resolve(buildResultText(modelResult));
224
293
  };
@@ -226,7 +295,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
226
295
  if (event.sessionId && event.sessionId !== sessionId && event.sessionId !== distributionSessionId) {
227
296
  return;
228
297
  }
229
- logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code}, sentFileCount=${event.sentFiles.length}`);
298
+ logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code}, fileCardCount=${countSentFileCards(event.sentFiles)}`);
230
299
  void (async () => {
231
300
  if (resultHandlingStarted) {
232
301
  return;
@@ -174,16 +174,6 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
174
174
  throw new Error(`fileNames length (${fileNames.length}) must match fileRemoteUrls length (${fileRemoteUrls.length})`);
175
175
  }
176
176
  }
177
- if (ctx.runCrossTaskContext && (fileLocalUrls.length > 0 || fileRemoteUrls.length > 0)) {
178
- const cachedSentFiles = appendRunCrossTaskSentFiles([
179
- {
180
- ...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
181
- ...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
182
- ...(fileNames.length > 0 ? { fileNames } : {}),
183
- },
184
- ], ctx.runCrossTaskContext);
185
- logger.log(`[RunCrossTask] cached ${cachedSentFiles.length} send_file_to_user input(s) for cross-task result`);
186
- }
187
177
  // Get WebSocket manager
188
178
  const wsManager = getXYWebSocketManager(config);
189
179
  // Create upload service
@@ -237,6 +227,7 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
237
227
  }
238
228
  // Build and send agent_response messages for each file
239
229
  const sentFiles = [];
230
+ let cachedSentFilesForReturn = [];
240
231
  for (const uploadedFile of uploadedFiles) {
241
232
  const { fileName, fileId, mimeType } = uploadedFile;
242
233
  const agentResponse = {
@@ -273,6 +264,12 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
273
264
  // Send WebSocket message
274
265
  await wsManager.sendMessage(sessionId, agentResponse);
275
266
  logger.log(`[SEND-FILE-TO-USER] send ${fileName} file to user success`);
267
+ if (ctx.runCrossTaskContext) {
268
+ const sentFileCard = { fileName, fileId, mimeType };
269
+ const cachedSentFiles = appendRunCrossTaskSentFiles([sentFileCard], ctx.runCrossTaskContext);
270
+ cachedSentFilesForReturn = cachedSentFiles;
271
+ logger.log(`[RunCrossTask] cached file card for cross-task result, fileName=${fileName}, cachedFileCardCount=${cachedSentFiles.length}`);
272
+ }
276
273
  sentFiles.push({ fileName, fileId });
277
274
  }
278
275
  return {
@@ -282,7 +279,8 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
282
279
  text: JSON.stringify({
283
280
  sentFiles,
284
281
  count: sentFiles.length,
285
- message: `成功发送 ${sentFiles.length} 个文件到用户设备`
282
+ message: `成功发送 ${sentFiles.length} 个文件到用户设备`,
283
+ cachedSentFiles: cachedSentFilesForReturn
286
284
  }),
287
285
  },
288
286
  ],
@@ -1,5 +1,5 @@
1
1
  import { AsyncLocalStorage } from "async_hooks";
2
- import type { RunCrossTaskContext, SentFileParams, XYChannelConfig } from "../types.js";
2
+ import type { RunCrossTaskContext, SentFileCard, XYChannelConfig } from "../types.js";
3
3
  export interface SessionContext {
4
4
  config: XYChannelConfig;
5
5
  sessionId: string;
@@ -79,6 +79,6 @@ export declare function cleanupStaleSessions(): number;
79
79
  * Get the current number of active sessions (for diagnostics).
80
80
  */
81
81
  export declare function getActiveSessionCount(): number;
82
- export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileParams[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileParams[];
82
+ export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileCard[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileCard[];
83
83
  export declare function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext?: RunCrossTaskContext): void;
84
84
  export {};
@@ -245,36 +245,49 @@ export function cleanupStaleSessions() {
245
245
  export function getActiveSessionCount() {
246
246
  return activeSessions.size;
247
247
  }
248
- function normalizeSentFileParams(params) {
249
- const fileLocalUrls = Array.isArray(params.fileLocalUrls)
250
- ? params.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
251
- : [];
252
- const fileRemoteUrls = Array.isArray(params.fileRemoteUrls)
253
- ? params.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
254
- : [];
255
- const fileNames = Array.isArray(params.fileNames)
256
- ? params.fileNames.filter((name) => typeof name === "string" && name.length > 0)
257
- : [];
258
- if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
248
+ function normalizeSentFileCard(card) {
249
+ if (!card || typeof card !== "object") {
250
+ return null;
251
+ }
252
+ const fileName = typeof card.fileName === "string" ? card.fileName.trim() : "";
253
+ const fileId = typeof card.fileId === "string" ? card.fileId.trim() : "";
254
+ const mimeType = typeof card.mimeType === "string" ? card.mimeType.trim() : "";
255
+ if (!fileName || !fileId) {
259
256
  return null;
260
257
  }
261
258
  return {
262
- ...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
263
- ...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
264
- ...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
259
+ fileName,
260
+ fileId,
261
+ ...(mimeType ? { mimeType } : {}),
265
262
  };
266
263
  }
264
+ function dedupeSentFilesByFileId(existing, incoming) {
265
+ const knownFileIds = new Set();
266
+ for (const card of existing) {
267
+ if (card.fileId) {
268
+ knownFileIds.add(card.fileId);
269
+ }
270
+ }
271
+ return incoming.filter((card) => {
272
+ if (knownFileIds.has(card.fileId)) {
273
+ return false;
274
+ }
275
+ knownFileIds.add(card.fileId);
276
+ return true;
277
+ });
278
+ }
267
279
  export function appendRunCrossTaskSentFiles(sentFiles, explicitRunCrossTaskContext) {
268
280
  const context = asyncLocalStorage.getStore() ?? null;
269
281
  const runCrossTaskContext = explicitRunCrossTaskContext ?? context?.runCrossTaskContext;
270
282
  const normalizedSentFiles = sentFiles
271
- .map((params) => normalizeSentFileParams(params))
272
- .filter((params) => params !== null);
283
+ .map((card) => normalizeSentFileCard(card))
284
+ .filter((card) => card !== null);
273
285
  if (!runCrossTaskContext || normalizedSentFiles.length === 0) {
274
286
  return runCrossTaskContext?.sentFiles ?? [];
275
287
  }
276
288
  const existing = Array.isArray(runCrossTaskContext.sentFiles) ? runCrossTaskContext.sentFiles : [];
277
- const merged = [...existing, ...normalizedSentFiles];
289
+ const dedupedSentFiles = dedupeSentFilesByFileId(existing, normalizedSentFiles);
290
+ const merged = [...existing, ...dedupedSentFiles];
278
291
  runCrossTaskContext.sentFiles = merged;
279
292
  const sessionWithRef = Array.from(activeSessions.values()).find((session) => session.runCrossTaskContext === runCrossTaskContext);
280
293
  if (sessionWithRef?.runCrossTaskContext) {
@@ -68,7 +68,7 @@ export interface CrossDeviceTaskResultEvent {
68
68
  sessionId: string;
69
69
  code: string;
70
70
  message: string;
71
- sentFiles: SentFileParams[];
71
+ sentFiles: SentFileCard[];
72
72
  status: "success" | "failed";
73
73
  rawEvent: any;
74
74
  }
@@ -78,13 +78,13 @@ export interface RunCrossTaskContext {
78
78
  isDistributed: boolean;
79
79
  networkId: string;
80
80
  isSupportAgent: boolean;
81
- sentFiles: SentFileParams[];
81
+ sentFiles: SentFileCard[];
82
82
  rawContext: any;
83
83
  }
84
- export interface SentFileParams {
85
- fileLocalUrls?: string[];
86
- fileRemoteUrls?: string[];
87
- fileNames?: string[];
84
+ export interface SentFileCard {
85
+ fileName: string;
86
+ fileId: string;
87
+ mimeType?: string;
88
88
  }
89
89
  export interface A2ATaskArtifactUpdateEvent {
90
90
  taskId: string;
@@ -7,6 +7,7 @@ import { HeartbeatManager } from "./heartbeat.js";
7
7
  import { MessageQueue } from "./message-queue.js";
8
8
  import { v4 as uuidv4 } from "uuid";
9
9
  const RUN_CROSS_TASK_LOG_TAG = "[RunCrossTask]";
10
+ const SEND_CROSS_RESULT_LOG_TAG = "[SendCrossResult]";
10
11
  const RUN_CROSS_TASK_QUERY_PREFIX = `# 跨设备协作接收模式<br/><br/>你当前正在接收来自其他设备的协作请求。请注意以下角色转换规则:<br/><br/>## 角色转换规则<br/><br/>- 指令中的"我" = 发送请求的远程用户<br/>- 你是执行协作任务的本地智能体<br/>- 任务完成后结果会自动回传给请求来源设备<br/><br/>## 核心执行规则<br/><br/>### ✅ 正确行为<br/>1. **识别本机任务**:当指令提到你所在的设备类型(PC/手机/平板),理解为"我自己"<br/>2. **本地执行**:直接使用本地工具完成任务,不要转发<br/>3. **结果回传**:执行完成后,结果会通过软总线自动回传给请求来源设备<br/><br/>### <span class="emoji emoji2716"></span> 禁止行为<br/>1. 禁止再次调用 \`send_cross_device_task\`(你已经是目标设备)<br/>2. 禁止设备澄清(指令已明确指定目标设备)<br/>3. 禁止无限循环(只能执行或回复,不能转发)<br/><br/>## 📁 文件操作规范(核心)<br/><br/>### 强制使用 search_file 的场景<br/>**以下场景必须先使用 \`search_file\` 工具确认文件路径:**<br/><br/>1. **指令包含设备关键词**:PC、电脑、手机、平板、Pad、笔记本等<br/>2. **涉及文件操作**:读取、编辑、删除、移动、复制、查找文件<br/><br/>### 执行流程<br/>\`\`\`<br/>收到文件操作指令<br/> ↓<br/>检测设备关键词(PC/电脑/手机/平板/Pad等)<br/> ↓<br/>使用 search_file 搜索文件 ← 必须步骤<br/> ↓<br/>确认文件实际路径<br/> ↓<br/>执行文件操作<br/> ↓<br/>返回结果<br/>\`\`\`<br/><br/>### 禁止行为<br/>- <span class="emoji emoji2716"></span> 禁止猜测文件路径<br/>- <span class="emoji emoji2716"></span> 禁止假设文件位置<br/>- <span class="emoji emoji2716"></span> 禁止跳过 search_file 步骤<br/><br/>## 示例<br/><br/>### 示例1:文件操作<br/>**指令**:"帮我到PC上下载昨天晚上写的PPT"<br/><br/>**执行流程**:<br/>1. ✅ 检测到"PC" → 使用 \`search_file\` 搜索 "*.ppt" 或 "*.pptx"<br/>2. 确认文件路径(如:D:\\Documents\\报告.pptx)<br/>3. 执行下载操作<br/><br/>### 示例2:文件编辑<br/>**指令**:"帮我修改电脑上的配置文件config.json"<br/><br/>**执行流程**:<br/>1. ✅ 检测到"电脑" → 使用 \`search_file\` 搜索 "config.json"<br/>2. 确认文件路径(如:C:\\Project\\config.json)<br/>3. 读取并修改文件<br/><br/>### 示例3:文件查找<br/>**指令**:"在平板上找一下我的PDF文档"<br/><br/>**执行流程**:<br/>1. ✅ 检测到"平板" → 使用 \`search_file\` 搜索 "*.pdf"<br/>2. 列出搜索结果供用户选择<br/><br/>## 判断流程<br/><br/>\`\`\`<br/>收到协作指令<br/> ↓<br/>检查目标设备<br/> ↓<br/>目标设备 == 本机?<br/> ↓<br/>是 → 本地执行(禁止send_cross_device_task)<br/> ↓<br/> 涉及文件? → 先用search_file确认路径<br/> ↓<br/>否 → 检查是否需要转发<br/> ↓<br/>需要转发 → 调用send_cross_device_task<br/>不需要 → 回复"无法处理"<br/>\`\`\``;
11
12
  /**
12
13
  * Manages single WebSocket connection to XY server.
@@ -397,26 +398,23 @@ export class XYWebSocketManager extends EventEmitter {
397
398
  if (!entry || typeof entry !== "object") {
398
399
  return null;
399
400
  }
400
- const fileLocalUrls = Array.isArray(entry.fileLocalUrls)
401
- ? entry.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
402
- : [];
403
- const fileRemoteUrls = Array.isArray(entry.fileRemoteUrls)
404
- ? entry.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
405
- : [];
406
- const fileNames = Array.isArray(entry.fileNames)
407
- ? entry.fileNames.filter((name) => typeof name === "string" && name.length > 0)
408
- : [];
409
- if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
401
+ const candidate = entry;
402
+ const fileName = typeof candidate.fileName === "string" ? candidate.fileName.trim() : "";
403
+ const fileId = typeof candidate.fileId === "string" ? candidate.fileId.trim() : "";
404
+ const mimeType = typeof candidate.mimeType === "string" ? candidate.mimeType.trim() : "";
405
+ if (!fileName || !fileId) {
410
406
  return null;
411
407
  }
412
408
  return {
413
- ...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
414
- ...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
415
- ...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
409
+ fileName,
410
+ fileId,
411
+ ...(mimeType ? { mimeType } : {}),
416
412
  };
417
413
  }).filter((entry) => entry !== null)
418
414
  : [];
419
415
  const status = code === "0" ? "success" : "failed";
416
+ const fileCardCount = sentFiles.length;
417
+ this.log(`${SEND_CROSS_RESULT_LOG_TAG} normalized CrossTaskExecuteResult, sessionId=${sessionId}, status=${status}, code=${code}, fileCardCount=${fileCardCount}, messageLength=${message.length}`);
420
418
  const event = {
421
419
  sessionId,
422
420
  code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.188-beta",
3
+ "version": "0.0.190-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",