@ynhcj/xiaoyi-channel 0.0.161-next → 0.0.162-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.
- package/dist/index.js +53 -1
- package/dist/src/bot.js +2 -1
- package/dist/src/cron-command.d.ts +2 -0
- package/dist/src/cron-command.js +14 -8
- package/dist/src/cron-query-handler.js +35 -8
- package/dist/src/cspl/call_api.js +2 -2
- package/dist/src/cspl/upload_file.js +2 -2
- package/dist/src/formatter.js +51 -5
- package/dist/src/parser.js +7 -13
- package/dist/src/provider.js +10 -2
- package/dist/src/reply-dispatcher.js +26 -12
- package/dist/src/tools/create-all-tools.js +8 -4
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +8 -1
- package/dist/src/tools/modify-alarm-tool.js +17 -0
- package/dist/src/tools/send-cross-device-task-tool.js +84 -15
- package/dist/src/tools/send-file-to-user-tool.js +9 -11
- package/dist/src/tools/session-manager.d.ts +8 -2
- package/dist/src/tools/session-manager.js +65 -18
- package/dist/src/types.d.ts +6 -6
- package/dist/src/utils/cron-push-map.d.ts +26 -0
- package/dist/src/utils/cron-push-map.js +131 -0
- package/dist/src/websocket.js +11 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,9 @@ import { xiaoyiProvider } from "./src/provider.js";
|
|
|
3
3
|
import { xyPlugin } from "./src/channel.js";
|
|
4
4
|
import registerSentinelHook from "./src/cspl/sentinel_hook.js";
|
|
5
5
|
import { setXYRuntime } from "./src/runtime.js";
|
|
6
|
-
import { markCronToolCall, clearCronToolCall } from "./src/tools/session-manager.js";
|
|
6
|
+
import { markCronToolCall, clearCronToolCall, getSessionContext } from "./src/tools/session-manager.js";
|
|
7
|
+
import { configManager } from "./src/utils/config-manager.js";
|
|
8
|
+
import { setJobPushId } from "./src/utils/cron-push-map.js";
|
|
7
9
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
8
10
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
9
11
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
@@ -25,8 +27,58 @@ function registerCronDetectionHook(api) {
|
|
|
25
27
|
if (event.toolCallId) {
|
|
26
28
|
clearCronToolCall(event.toolCallId);
|
|
27
29
|
}
|
|
30
|
+
// 捕获对话创建的 cron job:agent 调 cron(add) 后,从 result 拿 jobId,
|
|
31
|
+
// 配合当前会话的 pushId,写入 jobId↔pushId 映射,供 fire 时反查设备。
|
|
32
|
+
await captureCronAddMapping(event, ctx).catch((err) => {
|
|
33
|
+
// 捕获失败不影响工具结果
|
|
34
|
+
console.error("[xy] captureCronAddMapping failed:", err);
|
|
35
|
+
});
|
|
28
36
|
});
|
|
29
37
|
}
|
|
38
|
+
/** 从 cron add 工具结果中提取 jobId 并写入 pushId 映射。 */
|
|
39
|
+
async function captureCronAddMapping(event, ctx) {
|
|
40
|
+
if (event.toolName !== "cron")
|
|
41
|
+
return;
|
|
42
|
+
const action = typeof event.params?.action === "string" ? event.params.action : "";
|
|
43
|
+
if (action !== "add")
|
|
44
|
+
return;
|
|
45
|
+
const jobId = readJobIdFromResult(event.result);
|
|
46
|
+
if (!jobId)
|
|
47
|
+
return;
|
|
48
|
+
const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
|
|
49
|
+
const sessionId = sessionCtx?.sessionId;
|
|
50
|
+
if (!sessionId)
|
|
51
|
+
return;
|
|
52
|
+
const pushId = configManager.getPushId(sessionId);
|
|
53
|
+
if (!pushId)
|
|
54
|
+
return;
|
|
55
|
+
await setJobPushId(jobId, {
|
|
56
|
+
pushId,
|
|
57
|
+
sessionId,
|
|
58
|
+
deviceType: sessionCtx?.deviceType,
|
|
59
|
+
source: "conversation",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** 防御性地从 cron add 结果中取 job id(可能是对象或 JSON 字符串)。 */
|
|
63
|
+
function readJobIdFromResult(result) {
|
|
64
|
+
if (!result)
|
|
65
|
+
return undefined;
|
|
66
|
+
let obj = result;
|
|
67
|
+
if (typeof result === "string") {
|
|
68
|
+
try {
|
|
69
|
+
obj = JSON.parse(result);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (obj && typeof obj === "object") {
|
|
76
|
+
const id = obj.id;
|
|
77
|
+
if (typeof id === "string" && id.trim())
|
|
78
|
+
return id.trim();
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
30
82
|
function registerFullHooks(api) {
|
|
31
83
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
32
84
|
const pluginConfig = api.pluginConfig || {};
|
package/dist/src/bot.js
CHANGED
|
@@ -33,7 +33,6 @@ export async function handleXYMessage(params) {
|
|
|
33
33
|
try {
|
|
34
34
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
35
35
|
const messageMethod = message.method;
|
|
36
|
-
logger.log(`[BOT] Received A2A message: ${JSON.stringify(message)}`);
|
|
37
36
|
// Handle clearContext messages (sessionId at top level, no params)
|
|
38
37
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
39
38
|
const sessionId = message.sessionId ?? message.params?.sessionId;
|
|
@@ -189,6 +188,7 @@ export async function handleXYMessage(params) {
|
|
|
189
188
|
modelOverrideSource: "user",
|
|
190
189
|
model: "",
|
|
191
190
|
modelProvider: "",
|
|
191
|
+
contextTokens: 256_000,
|
|
192
192
|
}),
|
|
193
193
|
});
|
|
194
194
|
if (!result) {
|
|
@@ -206,6 +206,7 @@ export async function handleXYMessage(params) {
|
|
|
206
206
|
providerOverride: "xiaoyiprovider",
|
|
207
207
|
modelOverride: modelName,
|
|
208
208
|
modelOverrideSource: "user",
|
|
209
|
+
contextTokens: 256_000,
|
|
209
210
|
};
|
|
210
211
|
}
|
|
211
212
|
});
|
|
@@ -2,6 +2,8 @@ import type { XYChannelConfig, A2ACommand } from "./types.js";
|
|
|
2
2
|
export interface SendCommandViaPushParams {
|
|
3
3
|
config: XYChannelConfig;
|
|
4
4
|
command: A2ACommand;
|
|
5
|
+
/** 指定设备的 pushId(多设备路由)。未传时回退到 getAllPushIds()[0]。 */
|
|
6
|
+
pushId?: string;
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Send a tool command through the push channel (for cron-triggered tool calls).
|
package/dist/src/cron-command.js
CHANGED
|
@@ -24,16 +24,22 @@ export async function sendCommandViaPush(params) {
|
|
|
24
24
|
command.header?.name ??
|
|
25
25
|
"Command";
|
|
26
26
|
logger.log(`[CRON-CMD] Sending command via push, intent=${intentName}`);
|
|
27
|
-
// 1.
|
|
27
|
+
// 1. 选 pushId:优先用调用方解析出的设备 pushId(多设备路由正确);
|
|
28
|
+
// 未提供时回退到 getAllPushIds()[0](单设备兼容旧行为)。
|
|
28
29
|
let pushId = config.pushId;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (pushIdList.length > 0) {
|
|
32
|
-
pushId = pushIdList[0];
|
|
33
|
-
}
|
|
30
|
+
if (params.pushId) {
|
|
31
|
+
pushId = params.pushId;
|
|
34
32
|
}
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
else {
|
|
34
|
+
try {
|
|
35
|
+
const pushIdList = await getAllPushIds();
|
|
36
|
+
if (pushIdList.length > 0) {
|
|
37
|
+
pushId = pushIdList[0];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.error("[CRON-CMD] Failed to load pushIds:", error);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
// 2. Build and send push notification with command in directives
|
|
39
45
|
const pushService = new XYPushService(config);
|
|
@@ -7,6 +7,8 @@ import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
|
7
7
|
import * as os from "os";
|
|
8
8
|
import { sendCommand } from "./formatter.js";
|
|
9
9
|
import { resolveXYConfig } from "./config.js";
|
|
10
|
+
import { configManager } from "./utils/config-manager.js";
|
|
11
|
+
import { setJobPushId } from "./utils/cron-push-map.js";
|
|
10
12
|
import { logger } from "./utils/logger.js";
|
|
11
13
|
import { readFileSync, readdirSync } from "fs";
|
|
12
14
|
import { join } from "path";
|
|
@@ -19,7 +21,8 @@ const GATEWAY_TIMEOUT_MS = 60_000;
|
|
|
19
21
|
*/
|
|
20
22
|
export async function handleCronQueryEvent(context, cfg) {
|
|
21
23
|
const { action, jobId, params, sessionId, taskId, messageId } = context;
|
|
22
|
-
logger.
|
|
24
|
+
const log = logger.withContext(sessionId ?? "", taskId ?? "");
|
|
25
|
+
log.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
|
|
23
26
|
let result;
|
|
24
27
|
let error;
|
|
25
28
|
try {
|
|
@@ -38,6 +41,11 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
38
41
|
break;
|
|
39
42
|
case "add":
|
|
40
43
|
result = await callGatewayTool("cron.add", { timeoutMs: GATEWAY_TIMEOUT_MS }, params ?? {});
|
|
44
|
+
// 捕获 jobId↔pushId:cron-query 路径由 channel 自己建 job,
|
|
45
|
+
// 此处 context 握着 sessionId,configManager 有对应设备 pushId。
|
|
46
|
+
await persistCronPushMap(context.sessionId, result).catch((err) => {
|
|
47
|
+
logger.error(`[CRON-QUERY] Failed to persist cron-push-map:`, err);
|
|
48
|
+
});
|
|
41
49
|
break;
|
|
42
50
|
case "update":
|
|
43
51
|
result = await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
@@ -62,17 +70,17 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
62
70
|
break;
|
|
63
71
|
default:
|
|
64
72
|
error = `Unknown action: ${context.action}`;
|
|
65
|
-
|
|
73
|
+
log.error(`[CRON-QUERY] ${error}`);
|
|
66
74
|
result = { error };
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
catch (err) {
|
|
70
78
|
error = err instanceof Error ? err.message : String(err);
|
|
71
|
-
|
|
79
|
+
log.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
|
|
72
80
|
result = { error };
|
|
73
81
|
}
|
|
74
82
|
// Log the result
|
|
75
|
-
|
|
83
|
+
log.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
|
|
76
84
|
// Send result back via sendCommand as System.CronQuery with payload.ans
|
|
77
85
|
if (cfg && sessionId && taskId && messageId) {
|
|
78
86
|
try {
|
|
@@ -93,17 +101,36 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
93
101
|
taskId,
|
|
94
102
|
messageId,
|
|
95
103
|
command,
|
|
96
|
-
final:
|
|
104
|
+
final: sessionId.toLowerCase().endsWith("cronquery"),
|
|
97
105
|
});
|
|
98
|
-
|
|
106
|
+
log.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
|
|
99
107
|
}
|
|
100
108
|
catch (sendErr) {
|
|
101
|
-
|
|
109
|
+
log.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
else {
|
|
105
|
-
|
|
113
|
+
log.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 从 cron.add 结果中提取 jobId,配合 sessionId 对应的 pushId 写入映射。
|
|
118
|
+
*/
|
|
119
|
+
async function persistCronPushMap(sessionId, result) {
|
|
120
|
+
if (!sessionId)
|
|
121
|
+
return;
|
|
122
|
+
let jobId;
|
|
123
|
+
if (result && typeof result === "object") {
|
|
124
|
+
const id = result.id;
|
|
125
|
+
if (typeof id === "string" && id.trim())
|
|
126
|
+
jobId = id.trim();
|
|
106
127
|
}
|
|
128
|
+
if (!jobId)
|
|
129
|
+
return;
|
|
130
|
+
const pushId = configManager.getPushId(sessionId);
|
|
131
|
+
if (!pushId)
|
|
132
|
+
return;
|
|
133
|
+
await setJobPushId(jobId, { pushId, sessionId, source: "cron-query" });
|
|
107
134
|
}
|
|
108
135
|
/**
|
|
109
136
|
* Read local cron folder directly (bypassing openclaw RPC) and return
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import https from 'https';
|
|
5
5
|
import { URL } from 'url';
|
|
6
6
|
import { getConfig } from './config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { DEFAULT_HTTPS_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
|
|
8
8
|
function buildHeadersForCelia(config, sessionId) {
|
|
9
9
|
if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
|
|
10
10
|
throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
|
|
@@ -22,7 +22,7 @@ function buildRequestOptions(url, headers, timeout) {
|
|
|
22
22
|
const urlObj = new URL(url);
|
|
23
23
|
return {
|
|
24
24
|
hostname: urlObj.hostname,
|
|
25
|
-
port: urlObj.port ||
|
|
25
|
+
port: urlObj.port || DEFAULT_HTTPS_PORT,
|
|
26
26
|
path: urlObj.pathname,
|
|
27
27
|
method: "POST",
|
|
28
28
|
headers: headers,
|
|
@@ -6,7 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import https from 'https';
|
|
7
7
|
import { URL } from 'url';
|
|
8
8
|
import { getConfig } from './config.js';
|
|
9
|
-
import {
|
|
9
|
+
import { DEFAULT_HTTPS_PORT, MAX_TIMES, CONNECT_TIMEOUT, READ_TIMEOUT, EXPIRE_TIME, OSMS_PREPARE_URL, OSMS_COMPLETE_URL, TEMPORARY_MATERIAL_PACKAGE, FILE_OWNER_UID, FILE_OWNER_TEAM_ID } from './constants.js';
|
|
10
10
|
function buildOsmsHeaders(config, traceId) {
|
|
11
11
|
return {
|
|
12
12
|
'content-type': 'application/json',
|
|
@@ -22,7 +22,7 @@ function httpRequest(url, method, headers, body, timeout) {
|
|
|
22
22
|
const urlObj = new URL(url);
|
|
23
23
|
const options = {
|
|
24
24
|
hostname: urlObj.hostname,
|
|
25
|
-
port: urlObj.port ||
|
|
25
|
+
port: urlObj.port || DEFAULT_HTTPS_PORT,
|
|
26
26
|
path: urlObj.pathname + urlObj.search,
|
|
27
27
|
method: method,
|
|
28
28
|
headers: headers,
|
package/dist/src/formatter.js
CHANGED
|
@@ -5,7 +5,10 @@ import { logger } from "./utils/logger.js";
|
|
|
5
5
|
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
6
6
|
import { redactSensitiveText, containsSensitiveInfo } from "./sensitive-redactor.js";
|
|
7
7
|
import { rewriteOutboundApprovalText } from "./approval-bridge.js";
|
|
8
|
-
import { isCronToolCall } from "./tools/session-manager.js";
|
|
8
|
+
import { isCronToolCall, getCurrentCronJobId } from "./tools/session-manager.js";
|
|
9
|
+
import { configManager } from "./utils/config-manager.js";
|
|
10
|
+
import { getPushIdByJobId } from "./utils/cron-push-map.js";
|
|
11
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
9
12
|
// ─────────────────────────────────────────────────────────────
|
|
10
13
|
// 敏感信息脱敏辅助函数
|
|
11
14
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -201,6 +204,45 @@ export async function sendStatusUpdate(params) {
|
|
|
201
204
|
log.log(`[A2A_STATUS] Sending status-update, text="${redactedText}"`);
|
|
202
205
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
203
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* 解析 cron fire 时应使用的 pushId(多设备路由)。
|
|
209
|
+
*
|
|
210
|
+
* 查询链(逐级回退):
|
|
211
|
+
* 1. 合成 sessionId → jobId → cron-push-map.json → 创建时记录的设备 pushId
|
|
212
|
+
* 2. configManager 同进程的 sessionId→pushId(进程未重启时兜底)
|
|
213
|
+
* 3. getAllPushIds()[0](单设备兼容旧行为)
|
|
214
|
+
* 返回 undefined 表示走兜底(由 sendCommandViaPush 内部处理)。
|
|
215
|
+
*/
|
|
216
|
+
async function resolveCronPushId(sessionId, config) {
|
|
217
|
+
// 1. jobId → 持久化映射
|
|
218
|
+
const jobId = getCurrentCronJobId(sessionId);
|
|
219
|
+
if (jobId) {
|
|
220
|
+
const hit = await getPushIdByJobId(jobId);
|
|
221
|
+
if (hit?.pushId) {
|
|
222
|
+
logger.log(`[CRON-PUSH] Resolved pushId via map, jobId=${jobId}`);
|
|
223
|
+
return hit.pushId;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 2. 同进程 configManager 兜底
|
|
227
|
+
const sessionPushId = configManager.getPushId(sessionId);
|
|
228
|
+
if (sessionPushId) {
|
|
229
|
+
logger.log(`[CRON-PUSH] Resolved pushId via configManager (fallback)`);
|
|
230
|
+
return sessionPushId;
|
|
231
|
+
}
|
|
232
|
+
// 3. config.pushId / getAllPushIds()[0] 交给 sendCommandViaPush 内部处理
|
|
233
|
+
void config;
|
|
234
|
+
try {
|
|
235
|
+
const all = await getAllPushIds();
|
|
236
|
+
if (all.length > 0) {
|
|
237
|
+
logger.log(`[CRON-PUSH] Resolved pushId via getAllPushIds[0] (legacy fallback)`);
|
|
238
|
+
return all[0];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
logger.error(`[CRON-PUSH] getAllPushIds failed:`, error);
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
204
246
|
/**
|
|
205
247
|
* Send a command as an artifact update (final=false).
|
|
206
248
|
*
|
|
@@ -217,11 +259,15 @@ export async function sendCommand(params) {
|
|
|
217
259
|
if (commands.length === 0) {
|
|
218
260
|
throw new Error("sendCommand requires command or commands.");
|
|
219
261
|
}
|
|
220
|
-
// ── Cron mode:
|
|
221
|
-
//
|
|
222
|
-
//
|
|
262
|
+
// ── Cron mode: route through push channel ──────────────────────
|
|
263
|
+
// Detected via: (a) sessionId "cron-" prefix from synthetic session, OR
|
|
264
|
+
// (b) toolCallId marked by before_tool_call hook from openclaw's sessionKey.
|
|
223
265
|
if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
|
|
224
|
-
|
|
266
|
+
const { sendCommandViaPush } = await import("./cron-command.js");
|
|
267
|
+
// 解析正确设备的 pushId:合成 sessionId → jobId → cron-push-map。
|
|
268
|
+
// provider.ts 在 isCron 分支已把 jobId 绑定到该 sessionId。
|
|
269
|
+
const pushId = await resolveCronPushId(sessionId, config);
|
|
270
|
+
return sendCommandViaPush({ config, command: commands[0], pushId });
|
|
225
271
|
}
|
|
226
272
|
// ── Normal mode: WebSocket ─────────────────────────────────────
|
|
227
273
|
// Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
|
package/dist/src/parser.js
CHANGED
|
@@ -56,22 +56,16 @@ export function extractRunCrossTaskContext(parts) {
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
const candidate = item;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
...(
|
|
66
|
+
fileName,
|
|
67
|
+
fileId,
|
|
68
|
+
...(mimeType ? { mimeType } : {}),
|
|
75
69
|
};
|
|
76
70
|
})
|
|
77
71
|
.filter((item) => item !== null);
|
package/dist/src/provider.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// models.providers.xiaoyiprovider.models = [...]
|
|
10
10
|
import { createHash } from "crypto";
|
|
11
11
|
import { logger } from "./utils/logger.js";
|
|
12
|
-
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
12
|
+
import { getCurrentSessionContext, setCurrentCronJobId } from "./tools/session-manager.js";
|
|
13
13
|
import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
14
14
|
import { notifyModelStreaming } from "./bot.js";
|
|
15
15
|
// ── Retry config ──────────────────────────────────────────────
|
|
@@ -436,7 +436,7 @@ export const xiaoyiProvider = {
|
|
|
436
436
|
reasoning: false,
|
|
437
437
|
input: ["text"],
|
|
438
438
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
439
|
-
contextWindow:
|
|
439
|
+
contextWindow: 256_000,
|
|
440
440
|
maxTokens: 8192,
|
|
441
441
|
...(ctx.providerConfig?.headers && typeof ctx.providerConfig.headers === "object"
|
|
442
442
|
? { headers: ctx.providerConfig.headers }
|
|
@@ -506,6 +506,14 @@ export const xiaoyiProvider = {
|
|
|
506
506
|
// 3. UID-based fallback: sha256(uid).hex[:32]_timestamp
|
|
507
507
|
const isCron = isCronTriggered(context.messages);
|
|
508
508
|
if (isCron) {
|
|
509
|
+
// fire 期 jobId 桥:把首条消息 `[cron:<jobId> ...]` 解析出的真实 jobId
|
|
510
|
+
// 绑定到本次 cron run 的合成 sessionId。sendCommand 凭同一 sessionId
|
|
511
|
+
// 反查 jobId → cron-push-map → 正确设备的 pushId(多设备路由)。
|
|
512
|
+
const cronJobId = extractCronUuid(context.messages);
|
|
513
|
+
const cronCtx = getCurrentSessionContext();
|
|
514
|
+
if (cronJobId && cronCtx?.sessionId) {
|
|
515
|
+
setCurrentCronJobId(cronCtx.sessionId, cronJobId);
|
|
516
|
+
}
|
|
509
517
|
const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
|
|
510
518
|
if (typeof fallbackPrefix === "string") {
|
|
511
519
|
const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
|
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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:
|
|
261
|
+
resultMessage: crossTaskResultMessage,
|
|
247
262
|
});
|
|
248
263
|
}
|
|
249
264
|
// 🔑 使用动态taskId发送完成状态
|
|
@@ -402,7 +417,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
402
417
|
if (lines[0]?.trim() === "Reasoning:") {
|
|
403
418
|
text = lines.slice(1).join("\n").trim();
|
|
404
419
|
}
|
|
405
|
-
scopedLog().log(`[REASONING-STREAM] ${text.substring(0, 100)}`);
|
|
406
420
|
try {
|
|
407
421
|
if (text.length > 0) {
|
|
408
422
|
await sendReasoningTextUpdate({
|
|
@@ -17,6 +17,10 @@ import { createGetCollectionToolSchemaTool } from "./get-collection-tool-schema.
|
|
|
17
17
|
// import { createGetEmailToolSchemaTool } from "./get-email-tool-schema.js";
|
|
18
18
|
import { createLoginTokenTool } from "./login-token-tool.js";
|
|
19
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";
|
|
20
24
|
import { logger } from "../utils/logger.js";
|
|
21
25
|
/**
|
|
22
26
|
* Create all XY channel tools for the given session context.
|
|
@@ -32,9 +36,9 @@ export function createAllTools(ctx) {
|
|
|
32
36
|
logger.log(`[CREATE-ALL-TOOLS] creating tools`);
|
|
33
37
|
return [
|
|
34
38
|
createLocationTool(ctx),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
createDiscoverCrossDevicesTool(ctx),
|
|
40
|
+
createSendCrossDeviceTaskTool(ctx),
|
|
41
|
+
createDisplayA2UICardTool(ctx),
|
|
38
42
|
createCallDeviceTool(ctx),
|
|
39
43
|
createGetNoteToolSchemaTool(ctx),
|
|
40
44
|
createGetCalendarToolSchemaTool(ctx),
|
|
@@ -53,6 +57,6 @@ export function createAllTools(ctx) {
|
|
|
53
57
|
createSaveSelfEvolutionSkillTool(ctx),
|
|
54
58
|
createLoginTokenTool(ctx),
|
|
55
59
|
createAgentAsSkillTool(ctx),
|
|
56
|
-
|
|
60
|
+
createCheckPluginPrivilegeTool(ctx),
|
|
57
61
|
];
|
|
58
62
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Known device type enum. */
|
|
2
|
-
export declare const DEVICE_TYPES: readonly ["car", "2in1", "phone", "web"];
|
|
2
|
+
export declare const DEVICE_TYPES: readonly ["car", "2in1", "phone", "web", "pad"];
|
|
3
3
|
export type DeviceType = (typeof DEVICE_TYPES)[number];
|
|
4
4
|
export declare function filterToolsByDevice(tools: any[], deviceType?: string): any[];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// - denylist: listed tools are blocked, everything else is available (used for permissive devices like pc)
|
|
5
5
|
// Tools NOT listed in any device entry → available to all devices (no restriction).
|
|
6
6
|
/** Known device type enum. */
|
|
7
|
-
export const DEVICE_TYPES = ["car", "2in1", "phone", "web"];
|
|
7
|
+
export const DEVICE_TYPES = ["car", "2in1", "phone", "web", "pad"];
|
|
8
8
|
const DEVICE_TOOL_POLICY = {
|
|
9
9
|
"2in1": {
|
|
10
10
|
allowlist: false,
|
|
@@ -15,6 +15,13 @@ const DEVICE_TOOL_POLICY = {
|
|
|
15
15
|
"search_message",
|
|
16
16
|
"search_contact",
|
|
17
17
|
"get_contact_tool_schema",
|
|
18
|
+
"send_html_card"
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
"pad": {
|
|
22
|
+
allowlist: false,
|
|
23
|
+
tools: [
|
|
24
|
+
"xiaoyi_gui_agent"
|
|
18
25
|
],
|
|
19
26
|
},
|
|
20
27
|
"web": {
|
|
@@ -75,6 +75,23 @@ export function createModifyAlarmTool(ctx) {
|
|
|
75
75
|
required: ["entityId"],
|
|
76
76
|
},
|
|
77
77
|
async execute(toolCallId, params) {
|
|
78
|
+
// Coerce numeric string params to actual numbers
|
|
79
|
+
// The model may produce "1" instead of 1, which would fail typeof checks
|
|
80
|
+
const numericParams = [
|
|
81
|
+
"alarmState",
|
|
82
|
+
"alarmSnoozeDuration",
|
|
83
|
+
"alarmSnoozeTotal",
|
|
84
|
+
"alarmRingDuration",
|
|
85
|
+
"daysOfWakeType",
|
|
86
|
+
];
|
|
87
|
+
for (const key of numericParams) {
|
|
88
|
+
if (typeof params[key] === "string") {
|
|
89
|
+
const num = Number(params[key]);
|
|
90
|
+
if (!isNaN(num)) {
|
|
91
|
+
params[key] = num;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
78
95
|
// ===== Validate required parameter: entityId =====
|
|
79
96
|
if (!params.entityId || typeof params.entityId !== "string") {
|
|
80
97
|
throw new Error("Missing required parameter: entityId must be a string obtained from search_alarm or create_alarm");
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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},
|
|
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},
|
|
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,
|
|
2
|
+
import type { RunCrossTaskContext, SentFileCard, XYChannelConfig } from "../types.js";
|
|
3
3
|
export interface SessionContext {
|
|
4
4
|
config: XYChannelConfig;
|
|
5
5
|
sessionId: string;
|
|
@@ -27,6 +27,12 @@ export declare function markCronToolCall(toolCallId: string): void;
|
|
|
27
27
|
export declare function isCronToolCall(toolCallId?: string): boolean;
|
|
28
28
|
/** Clean up a cron tool call marker after use. */
|
|
29
29
|
export declare function clearCronToolCall(toolCallId: string): void;
|
|
30
|
+
/** 把 fire 期解析出的 jobId 绑定到当前 cron run 的合成 sessionId。 */
|
|
31
|
+
export declare function setCurrentCronJobId(cronSessionId: string, jobId: string): void;
|
|
32
|
+
/** 凭合成 cron sessionId 取本次 run 的 jobId(供 sendCommand 反查 pushId)。 */
|
|
33
|
+
export declare function getCurrentCronJobId(cronSessionId?: string): string | undefined;
|
|
34
|
+
/** cron run 结束后清理。 */
|
|
35
|
+
export declare function clearCronJobId(cronSessionId: string): void;
|
|
30
36
|
export declare const asyncLocalStorage: AsyncLocalStorage<SessionContext>;
|
|
31
37
|
/**
|
|
32
38
|
* Register a session context for tool access.
|
|
@@ -79,6 +85,6 @@ export declare function cleanupStaleSessions(): number;
|
|
|
79
85
|
* Get the current number of active sessions (for diagnostics).
|
|
80
86
|
*/
|
|
81
87
|
export declare function getActiveSessionCount(): number;
|
|
82
|
-
export declare function appendRunCrossTaskSentFiles(sentFiles:
|
|
88
|
+
export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileCard[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileCard[];
|
|
83
89
|
export declare function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext?: RunCrossTaskContext): void;
|
|
84
90
|
export {};
|
|
@@ -48,6 +48,32 @@ export function isCronToolCall(toolCallId) {
|
|
|
48
48
|
export function clearCronToolCall(toolCallId) {
|
|
49
49
|
cronToolCallMap.delete(toolCallId);
|
|
50
50
|
}
|
|
51
|
+
// ── Cron session ↔ jobId bridge ────────────────────────────────────
|
|
52
|
+
// fire 期 jobId 传递桥。provider.ts 在 isCron 分支从首条消息
|
|
53
|
+
// `[cron:<jobId> ...]` 解析出真实 jobId,写入合成 cron sessionId;
|
|
54
|
+
// sendCommand/formatter 凭同一合成 sessionId 反查 jobId,再去
|
|
55
|
+
// cron-push-map 取对应设备的 pushId。同一 cron run 内 ALS 上下文共享,
|
|
56
|
+
// 合成 sessionId 在 provider 与 sendCommand 之间一致。
|
|
57
|
+
if (!_g.__xyCronSessionJobId) {
|
|
58
|
+
_g.__xyCronSessionJobId = new Map();
|
|
59
|
+
}
|
|
60
|
+
const cronSessionJobIdMap = _g.__xyCronSessionJobId;
|
|
61
|
+
/** 把 fire 期解析出的 jobId 绑定到当前 cron run 的合成 sessionId。 */
|
|
62
|
+
export function setCurrentCronJobId(cronSessionId, jobId) {
|
|
63
|
+
if (cronSessionId && jobId) {
|
|
64
|
+
cronSessionJobIdMap.set(cronSessionId, jobId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** 凭合成 cron sessionId 取本次 run 的 jobId(供 sendCommand 反查 pushId)。 */
|
|
68
|
+
export function getCurrentCronJobId(cronSessionId) {
|
|
69
|
+
if (!cronSessionId)
|
|
70
|
+
return undefined;
|
|
71
|
+
return cronSessionJobIdMap.get(cronSessionId);
|
|
72
|
+
}
|
|
73
|
+
/** cron run 结束后清理。 */
|
|
74
|
+
export function clearCronJobId(cronSessionId) {
|
|
75
|
+
cronSessionJobIdMap.delete(cronSessionId);
|
|
76
|
+
}
|
|
51
77
|
// AsyncLocalStorage for thread-safe session context isolation
|
|
52
78
|
export const asyncLocalStorage = new AsyncLocalStorage();
|
|
53
79
|
// Export AsyncLocalStorage to globalThis so logger.ts can access it
|
|
@@ -64,9 +90,17 @@ export function registerSession(sessionKey, context) {
|
|
|
64
90
|
setLastRegisteredKey(sessionKey);
|
|
65
91
|
const existing = activeSessions.get(sessionKey);
|
|
66
92
|
if (existing) {
|
|
67
|
-
//
|
|
93
|
+
// 更新上下文(全量同步所有字段,避免 global Map 残留旧值,
|
|
94
|
+
// 当 ALS 丢失回退到 Map 时拿到过期的 modelName / deviceType)
|
|
95
|
+
existing.config = context.config;
|
|
96
|
+
existing.sessionId = context.sessionId;
|
|
97
|
+
existing.distributionSessionId = context.distributionSessionId;
|
|
68
98
|
existing.taskId = context.taskId;
|
|
69
99
|
existing.messageId = context.messageId;
|
|
100
|
+
existing.agentId = context.agentId;
|
|
101
|
+
existing.deviceType = context.deviceType;
|
|
102
|
+
existing.modelName = context.modelName;
|
|
103
|
+
existing.runCrossTaskContext = context.runCrossTaskContext;
|
|
70
104
|
existing.refCount++;
|
|
71
105
|
existing.createdAt = Date.now(); // 刷新存活时间,长对话不受 TTL 影响
|
|
72
106
|
}
|
|
@@ -237,36 +271,49 @@ export function cleanupStaleSessions() {
|
|
|
237
271
|
export function getActiveSessionCount() {
|
|
238
272
|
return activeSessions.size;
|
|
239
273
|
}
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
? params.fileNames.filter((name) => typeof name === "string" && name.length > 0)
|
|
249
|
-
: [];
|
|
250
|
-
if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
|
|
274
|
+
function normalizeSentFileCard(card) {
|
|
275
|
+
if (!card || typeof card !== "object") {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const fileName = typeof card.fileName === "string" ? card.fileName.trim() : "";
|
|
279
|
+
const fileId = typeof card.fileId === "string" ? card.fileId.trim() : "";
|
|
280
|
+
const mimeType = typeof card.mimeType === "string" ? card.mimeType.trim() : "";
|
|
281
|
+
if (!fileName || !fileId) {
|
|
251
282
|
return null;
|
|
252
283
|
}
|
|
253
284
|
return {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
...(
|
|
285
|
+
fileName,
|
|
286
|
+
fileId,
|
|
287
|
+
...(mimeType ? { mimeType } : {}),
|
|
257
288
|
};
|
|
258
289
|
}
|
|
290
|
+
function dedupeSentFilesByFileId(existing, incoming) {
|
|
291
|
+
const knownFileIds = new Set();
|
|
292
|
+
for (const card of existing) {
|
|
293
|
+
if (card.fileId) {
|
|
294
|
+
knownFileIds.add(card.fileId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return incoming.filter((card) => {
|
|
298
|
+
if (knownFileIds.has(card.fileId)) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
knownFileIds.add(card.fileId);
|
|
302
|
+
return true;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
259
305
|
export function appendRunCrossTaskSentFiles(sentFiles, explicitRunCrossTaskContext) {
|
|
260
306
|
const context = asyncLocalStorage.getStore() ?? null;
|
|
261
307
|
const runCrossTaskContext = explicitRunCrossTaskContext ?? context?.runCrossTaskContext;
|
|
262
308
|
const normalizedSentFiles = sentFiles
|
|
263
|
-
.map((
|
|
264
|
-
.filter((
|
|
309
|
+
.map((card) => normalizeSentFileCard(card))
|
|
310
|
+
.filter((card) => card !== null);
|
|
265
311
|
if (!runCrossTaskContext || normalizedSentFiles.length === 0) {
|
|
266
312
|
return runCrossTaskContext?.sentFiles ?? [];
|
|
267
313
|
}
|
|
268
314
|
const existing = Array.isArray(runCrossTaskContext.sentFiles) ? runCrossTaskContext.sentFiles : [];
|
|
269
|
-
const
|
|
315
|
+
const dedupedSentFiles = dedupeSentFilesByFileId(existing, normalizedSentFiles);
|
|
316
|
+
const merged = [...existing, ...dedupedSentFiles];
|
|
270
317
|
runCrossTaskContext.sentFiles = merged;
|
|
271
318
|
const sessionWithRef = Array.from(activeSessions.values()).find((session) => session.runCrossTaskContext === runCrossTaskContext);
|
|
272
319
|
if (sessionWithRef?.runCrossTaskContext) {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export interface CrossDeviceTaskResultEvent {
|
|
|
68
68
|
sessionId: string;
|
|
69
69
|
code: string;
|
|
70
70
|
message: string;
|
|
71
|
-
sentFiles:
|
|
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:
|
|
81
|
+
sentFiles: SentFileCard[];
|
|
82
82
|
rawContext: any;
|
|
83
83
|
}
|
|
84
|
-
export interface
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
export interface SentFileCard {
|
|
85
|
+
fileName: string;
|
|
86
|
+
fileId: string;
|
|
87
|
+
mimeType?: string;
|
|
88
88
|
}
|
|
89
89
|
export interface A2ATaskArtifactUpdateEvent {
|
|
90
90
|
taskId: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** 映射来源,区分两种创建路径。 */
|
|
2
|
+
export type CronPushMapSource = "conversation" | "cron-query";
|
|
3
|
+
export interface CronPushMapEntry {
|
|
4
|
+
pushId: string;
|
|
5
|
+
/** 创建该 cron 时的 xy sessionId,便于同进程兜底。 */
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
/** 冗余记录设备类型,fire 时可按设备类型做差异化处理。 */
|
|
8
|
+
deviceType?: string;
|
|
9
|
+
source?: CronPushMapSource;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface CronPushMapFile {
|
|
13
|
+
version: 1;
|
|
14
|
+
entries: Record<string, CronPushMapEntry>;
|
|
15
|
+
}
|
|
16
|
+
/** 写入/更新一条 jobId → pushId 映射。 */
|
|
17
|
+
export declare function setJobPushId(jobId: string, entry: Omit<CronPushMapEntry, "createdAt">): Promise<void>;
|
|
18
|
+
/** fire 时主查询:按 jobId 取 pushId。 */
|
|
19
|
+
export declare function getPushIdByJobId(jobId: string): Promise<{
|
|
20
|
+
pushId: string;
|
|
21
|
+
entry: CronPushMapEntry;
|
|
22
|
+
} | null>;
|
|
23
|
+
/** 删除一条映射(cron job 被移除时清理)。 */
|
|
24
|
+
export declare function removeJob(jobId: string): Promise<void>;
|
|
25
|
+
/** 对账:删除 openclaw 里已不存在的 job,避免映射无限增长。 */
|
|
26
|
+
export declare function pruneStale(existingJobIds: Set<string>): Promise<void>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Cron job ↔ pushId 持久化映射
|
|
2
|
+
//
|
|
3
|
+
// 背景:cron 定时任务触发时,工具调用走 push 通道(sendCommandViaPush),
|
|
4
|
+
// 需要正确的 pushId 才能推到"创建该任务的那台设备"。pushIdList.json 是
|
|
5
|
+
// 扁平去重数组,无法区分设备;本文件按真实 jobId 保存创建时的 pushId,
|
|
6
|
+
// fire 时通过 jobId 反查即可定位到正确设备。
|
|
7
|
+
//
|
|
8
|
+
// jobId 的两端来源(均为真实 jobId,无需规范化反解):
|
|
9
|
+
// - 写入:after_tool_call 拦 cron/add → event.result.id;
|
|
10
|
+
// cron-query-handler inline → result.id
|
|
11
|
+
// - 读取:provider.ts extractCronUuid(context.messages) → "[cron:<jobId> ...]"
|
|
12
|
+
import { promises as fs } from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { logger } from "./logger.js";
|
|
15
|
+
const CRON_PUSH_MAP_FILE = "/home/sandbox/.openclaw/cron-push-map.json";
|
|
16
|
+
async function ensureDirectoryExists(filePath) {
|
|
17
|
+
const dir = path.dirname(filePath);
|
|
18
|
+
try {
|
|
19
|
+
await fs.mkdir(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
logger.error(`[CronPushMap] Failed to create directory ${dir}:`, error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readMap() {
|
|
26
|
+
try {
|
|
27
|
+
await ensureDirectoryExists(CRON_PUSH_MAP_FILE);
|
|
28
|
+
const content = await fs.readFile(CRON_PUSH_MAP_FILE, "utf-8");
|
|
29
|
+
const parsed = JSON.parse(content);
|
|
30
|
+
if (parsed &&
|
|
31
|
+
typeof parsed === "object" &&
|
|
32
|
+
parsed.version === 1 &&
|
|
33
|
+
parsed.entries &&
|
|
34
|
+
typeof parsed.entries === "object") {
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
logger.warn(`[CronPushMap] Unexpected file shape, returning empty map`);
|
|
38
|
+
return { version: 1, entries: {} };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code === "ENOENT") {
|
|
42
|
+
return { version: 1, entries: {} };
|
|
43
|
+
}
|
|
44
|
+
logger.error(`[CronPushMap] Failed to read map:`, error);
|
|
45
|
+
return { version: 1, entries: {} };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function writeMap(map) {
|
|
49
|
+
try {
|
|
50
|
+
await ensureDirectoryExists(CRON_PUSH_MAP_FILE);
|
|
51
|
+
await fs.writeFile(CRON_PUSH_MAP_FILE, JSON.stringify(map, null, 2), "utf-8");
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
logger.error(`[CronPushMap] Failed to write map:`, error);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 写入/更新一条 jobId → pushId 映射。 */
|
|
59
|
+
export async function setJobPushId(jobId, entry) {
|
|
60
|
+
if (!jobId || typeof jobId !== "string") {
|
|
61
|
+
logger.warn(`[CronPushMap] Invalid jobId: ${jobId}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!entry.pushId || typeof entry.pushId !== "string") {
|
|
65
|
+
logger.warn(`[CronPushMap] Skipping setJobPushId: missing pushId for jobId=${jobId}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const map = await readMap();
|
|
70
|
+
map.entries[jobId] = { ...entry, createdAt: Date.now() };
|
|
71
|
+
await writeMap(map);
|
|
72
|
+
logger.log(`[CronPushMap] Saved jobId=${jobId}, source=${entry.source ?? "?"}, pushId=${entry.pushId.substring(0, 20)}...`);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
logger.error(`[CronPushMap] Failed to setJobPushId:`, error);
|
|
76
|
+
// 不抛出,避免影响主流程
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** fire 时主查询:按 jobId 取 pushId。 */
|
|
80
|
+
export async function getPushIdByJobId(jobId) {
|
|
81
|
+
if (!jobId)
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
const map = await readMap();
|
|
85
|
+
const entry = map.entries[jobId];
|
|
86
|
+
if (entry && entry.pushId) {
|
|
87
|
+
return { pushId: entry.pushId, entry };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
logger.error(`[CronPushMap] Failed to getPushIdByJobId:`, error);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** 删除一条映射(cron job 被移除时清理)。 */
|
|
97
|
+
export async function removeJob(jobId) {
|
|
98
|
+
if (!jobId)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
const map = await readMap();
|
|
102
|
+
if (map.entries[jobId]) {
|
|
103
|
+
delete map.entries[jobId];
|
|
104
|
+
await writeMap(map);
|
|
105
|
+
logger.log(`[CronPushMap] Removed jobId=${jobId}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
logger.error(`[CronPushMap] Failed to removeJob:`, error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** 对账:删除 openclaw 里已不存在的 job,避免映射无限增长。 */
|
|
113
|
+
export async function pruneStale(existingJobIds) {
|
|
114
|
+
try {
|
|
115
|
+
const map = await readMap();
|
|
116
|
+
let removed = 0;
|
|
117
|
+
for (const key of Object.keys(map.entries)) {
|
|
118
|
+
if (!existingJobIds.has(key)) {
|
|
119
|
+
delete map.entries[key];
|
|
120
|
+
removed++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (removed > 0) {
|
|
124
|
+
await writeMap(map);
|
|
125
|
+
logger.log(`[CronPushMap] Pruned ${removed} stale entries`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
logger.error(`[CronPushMap] Failed to pruneStale:`, error);
|
|
130
|
+
}
|
|
131
|
+
}
|
package/dist/src/websocket.js
CHANGED
|
@@ -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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
...(
|
|
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,
|