@ynhcj/xiaoyi-channel 0.0.153-beta → 0.0.154-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.
- package/dist/index.js +23 -0
- package/dist/src/bot.js +9 -1
- package/dist/src/channel.js +59 -5
- package/dist/src/cron-command.d.ts +16 -0
- package/dist/src/cron-command.js +64 -0
- package/dist/src/formatter.d.ts +11 -1
- package/dist/src/formatter.js +22 -2
- package/dist/src/parser.d.ts +2 -1
- package/dist/src/parser.js +25 -0
- package/dist/src/reply-dispatcher.js +73 -1
- package/dist/src/tools/agent-as-skill-tool.js +1 -0
- package/dist/src/tools/calendar-tool.js +1 -0
- package/dist/src/tools/call-phone-tool.js +1 -0
- package/dist/src/tools/create-alarm-tool.js +1 -0
- package/dist/src/tools/create-all-tools.js +4 -0
- package/dist/src/tools/delete-alarm-tool.js +1 -0
- package/dist/src/tools/discover-cross-devices-tool.d.ts +2 -0
- package/dist/src/tools/discover-cross-devices-tool.js +235 -0
- package/dist/src/tools/find-pc-devices-tool.js +1 -0
- package/dist/src/tools/location-tool.js +1 -0
- package/dist/src/tools/modify-alarm-tool.js +1 -0
- package/dist/src/tools/modify-note-tool.js +1 -0
- package/dist/src/tools/note-tool.js +1 -0
- package/dist/src/tools/query-app-message-tool.js +3 -2
- package/dist/src/tools/query-memory-data-tool.js +3 -2
- package/dist/src/tools/query-todo-task-tool.js +3 -2
- package/dist/src/tools/save-file-to-phone-tool.js +1 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +1 -0
- package/dist/src/tools/search-alarm-tool.js +1 -0
- package/dist/src/tools/search-calendar-tool.js +1 -0
- package/dist/src/tools/search-contact-tool.js +1 -0
- package/dist/src/tools/search-email-tool.js +3 -2
- package/dist/src/tools/search-file-tool.js +1 -0
- package/dist/src/tools/search-message-tool.js +1 -0
- package/dist/src/tools/search-note-tool.js +1 -0
- package/dist/src/tools/search-photo-gallery-tool.js +3 -2
- package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
- package/dist/src/tools/send-cross-device-task-tool.js +303 -0
- package/dist/src/tools/send-email-tool.js +3 -2
- package/dist/src/tools/send-message-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +13 -1
- package/dist/src/tools/session-manager.js +38 -0
- package/dist/src/tools/upload-file-tool.d.ts +1 -1
- package/dist/src/tools/upload-file-tool.js +8 -2
- package/dist/src/tools/upload-photo-tool.js +3 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +168 -15
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -3,9 +3,30 @@ 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
7
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
7
8
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
8
9
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
10
|
+
/**
|
|
11
|
+
* Register the cron detection hook.
|
|
12
|
+
*
|
|
13
|
+
* When openclaw's cron runner triggers a tool call, the sessionKey has the
|
|
14
|
+
* format "cron:<jobId>". We use this to mark the toolCallId in a global Map
|
|
15
|
+
* so that sendCommand() can route the command through the push channel
|
|
16
|
+
* instead of the (non-existent) WebSocket session.
|
|
17
|
+
*/
|
|
18
|
+
function registerCronDetectionHook(api) {
|
|
19
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
20
|
+
if (ctx.sessionKey?.startsWith("cron:") && event.toolCallId) {
|
|
21
|
+
markCronToolCall(event.toolCallId);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
api.on("after_tool_call", async (event, ctx) => {
|
|
25
|
+
if (event.toolCallId) {
|
|
26
|
+
clearCronToolCall(event.toolCallId);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
9
30
|
function registerFullHooks(api) {
|
|
10
31
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
11
32
|
const pluginConfig = api.pluginConfig || {};
|
|
@@ -45,6 +66,8 @@ export default definePluginEntry({
|
|
|
45
66
|
registerFullHooks(api);
|
|
46
67
|
// CSPL sentinel hook: before_tool_call + after_tool_call security scanning
|
|
47
68
|
registerSentinelHook(api);
|
|
69
|
+
// Cron detection hook: marks toolCallIds from cron sessions
|
|
70
|
+
registerCronDetectionHook(api);
|
|
48
71
|
}
|
|
49
72
|
},
|
|
50
73
|
});
|
package/dist/src/bot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
3
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
|
|
4
4
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
5
5
|
import { resolveXYConfig } from "./config.js";
|
|
6
6
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -22,6 +22,9 @@ import { logger } from "./utils/logger.js";
|
|
|
22
22
|
*/
|
|
23
23
|
export async function handleXYMessage(params) {
|
|
24
24
|
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
25
|
+
const distributionSessionId = typeof message?.sessionId === "string" && message.sessionId.length > 0
|
|
26
|
+
? message.sessionId
|
|
27
|
+
: undefined;
|
|
25
28
|
// Cache context for CSPL steer injection (after_tool_call hook)
|
|
26
29
|
setCsplSteerContext(cfg, runtime);
|
|
27
30
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
@@ -135,6 +138,7 @@ export async function handleXYMessage(params) {
|
|
|
135
138
|
if (deviceType) {
|
|
136
139
|
log.log(`[BOT] Extracted deviceType: ${deviceType}`);
|
|
137
140
|
}
|
|
141
|
+
const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
|
|
138
142
|
// Resolve configuration (needed for status updates)
|
|
139
143
|
const config = resolveXYConfig(cfg);
|
|
140
144
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -155,10 +159,12 @@ export async function handleXYMessage(params) {
|
|
|
155
159
|
registerSession(route.sessionKey, {
|
|
156
160
|
config,
|
|
157
161
|
sessionId: parsed.sessionId,
|
|
162
|
+
distributionSessionId,
|
|
158
163
|
taskId: parsed.taskId,
|
|
159
164
|
messageId: parsed.messageId,
|
|
160
165
|
agentId: route.accountId,
|
|
161
166
|
deviceType,
|
|
167
|
+
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
162
168
|
});
|
|
163
169
|
// 🔑 发送初始状态更新
|
|
164
170
|
log.log(`[BOT] Sending initial status update`);
|
|
@@ -300,10 +306,12 @@ export async function handleXYMessage(params) {
|
|
|
300
306
|
const sessionContext = {
|
|
301
307
|
config,
|
|
302
308
|
sessionId: parsed.sessionId,
|
|
309
|
+
distributionSessionId,
|
|
303
310
|
taskId: parsed.taskId,
|
|
304
311
|
messageId: parsed.messageId,
|
|
305
312
|
agentId: route.accountId,
|
|
306
313
|
deviceType,
|
|
314
|
+
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
307
315
|
};
|
|
308
316
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
309
317
|
await core.channel.reply.withReplyDispatcher({
|
package/dist/src/channel.js
CHANGED
|
@@ -2,9 +2,15 @@ import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./conf
|
|
|
2
2
|
import { xyConfigSchema } from "./config-schema.js";
|
|
3
3
|
import { xyOutbound } from "./outbound.js";
|
|
4
4
|
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
5
|
-
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
|
+
import { getCurrentSessionContext, registerSession } from "./tools/session-manager.js";
|
|
6
6
|
import { createAllTools } from "./tools/create-all-tools.js";
|
|
7
7
|
import { logger } from "./utils/logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* Prefix used for synthetic sessionIds created during cron-triggered tool
|
|
10
|
+
* execution. `sendCommand()` checks this prefix to route commands through
|
|
11
|
+
* the push channel instead of the (non-existent) WebSocket session.
|
|
12
|
+
*/
|
|
13
|
+
const CRON_SESSION_PREFIX = "cron-";
|
|
8
14
|
/**
|
|
9
15
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
10
16
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -43,11 +49,59 @@ export const xyPlugin = {
|
|
|
43
49
|
schema: xyConfigSchema,
|
|
44
50
|
},
|
|
45
51
|
outbound: xyOutbound,
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Provide channel-specific agent tools.
|
|
54
|
+
*
|
|
55
|
+
* Two execution contexts are supported:
|
|
56
|
+
*
|
|
57
|
+
* 1. **Normal (WebSocket) session** – `getCurrentSessionContext()` returns
|
|
58
|
+
* a context that was registered by bot.ts during message processing.
|
|
59
|
+
* Tools send commands through the WebSocket and listen for responses.
|
|
60
|
+
*
|
|
61
|
+
* 2. **Cron / scheduled-task session** – openclaw's cron runner calls
|
|
62
|
+
* `agentTools({ cfg })` without an active WebSocket session. When no
|
|
63
|
+
* session context exists but `cfg` is provided, we create a synthetic
|
|
64
|
+
* "cron session" with `isCron: true` and a `cron-`-prefixed sessionId.
|
|
65
|
+
* `sendCommand()` detects this prefix and routes commands through the
|
|
66
|
+
* push channel. Response listening (WebSocket events) works unchanged
|
|
67
|
+
* because the gateway WebSocket connection is always active.
|
|
68
|
+
*/
|
|
69
|
+
agentTools: (params) => {
|
|
70
|
+
let ctx = getCurrentSessionContext();
|
|
71
|
+
// ── Cron / non-session fallback ──────────────────────────────
|
|
72
|
+
// When no active xy WebSocket session exists but the openclaw cfg
|
|
73
|
+
// is provided (framework calls agentTools({ cfg })), create a
|
|
74
|
+
// synthetic "cron session". This enables cron-triggered agent
|
|
75
|
+
// turns and cross-channel tool calls to use xiaoyi tools via the
|
|
76
|
+
// push channel. sendCommand() detects the "cron-" sessionId
|
|
77
|
+
// prefix and routes commands through push instead of WebSocket.
|
|
78
|
+
if (!ctx && params?.cfg) {
|
|
79
|
+
try {
|
|
80
|
+
const config = resolveXYConfig(params.cfg);
|
|
81
|
+
const cronId = `${CRON_SESSION_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
82
|
+
ctx = {
|
|
83
|
+
config,
|
|
84
|
+
sessionId: cronId,
|
|
85
|
+
taskId: cronId,
|
|
86
|
+
messageId: cronId,
|
|
87
|
+
agentId: "default",
|
|
88
|
+
isCron: true,
|
|
89
|
+
};
|
|
90
|
+
// Register so getCurrentSessionContext() fallback can find it
|
|
91
|
+
registerSession(`__cron__${cronId}`, ctx);
|
|
92
|
+
logger.log(`[CRON-TOOLS] Created cron session context: ${cronId}`);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
logger.error("[CRON-TOOLS] Failed to create cron context:", err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!ctx) {
|
|
99
|
+
logger.log("[CREATE-ALL-TOOLS] no session context, returning empty tools list");
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
48
102
|
const allTools = createAllTools(ctx);
|
|
49
|
-
const filtered = filterToolsByDevice(allTools, ctx
|
|
50
|
-
logger.log(`[DEVICE-FILTER] deviceType=${ctx
|
|
103
|
+
const filtered = filterToolsByDevice(allTools, ctx.deviceType);
|
|
104
|
+
logger.log(`[DEVICE-FILTER] deviceType=${ctx.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
51
105
|
return filtered;
|
|
52
106
|
},
|
|
53
107
|
messaging: {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { XYChannelConfig, A2ACommand } from "./types.js";
|
|
2
|
+
export interface SendCommandViaPushParams {
|
|
3
|
+
config: XYChannelConfig;
|
|
4
|
+
command: A2ACommand;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Send a tool command through the push channel (for cron-triggered tool calls).
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Command JSON is persisted via savePushData → pushDataId
|
|
11
|
+
* 2. Push notification is sent to all registered pushIds, referencing pushDataId
|
|
12
|
+
* 3. Device receives push → retrieves command → executes it
|
|
13
|
+
* 4. Device returns result via WebSocket (data-event / gui-agent-response / …)
|
|
14
|
+
* 5. The calling tool listens on the WebSocket manager as usual
|
|
15
|
+
*/
|
|
16
|
+
export declare function sendCommandViaPush(params: SendCommandViaPushParams): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Cron-triggered tool command delivery via push channel.
|
|
2
|
+
// When a cron/scheduled task executes a tool, there is no active WebSocket
|
|
3
|
+
// session to carry the command. Instead, the command is delivered through
|
|
4
|
+
// the push notification channel (agent-webhook), which reaches the device
|
|
5
|
+
// independently of any session. The device processes the command and returns
|
|
6
|
+
// results through the normal WebSocket connection, so response listening
|
|
7
|
+
// works the same as for regular tool calls.
|
|
8
|
+
import { XYPushService } from "./push.js";
|
|
9
|
+
import { savePushData } from "./utils/pushdata-manager.js";
|
|
10
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
11
|
+
import { logger } from "./utils/logger.js";
|
|
12
|
+
/**
|
|
13
|
+
* Send a tool command through the push channel (for cron-triggered tool calls).
|
|
14
|
+
*
|
|
15
|
+
* Flow:
|
|
16
|
+
* 1. Command JSON is persisted via savePushData → pushDataId
|
|
17
|
+
* 2. Push notification is sent to all registered pushIds, referencing pushDataId
|
|
18
|
+
* 3. Device receives push → retrieves command → executes it
|
|
19
|
+
* 4. Device returns result via WebSocket (data-event / gui-agent-response / …)
|
|
20
|
+
* 5. The calling tool listens on the WebSocket manager as usual
|
|
21
|
+
*/
|
|
22
|
+
export async function sendCommandViaPush(params) {
|
|
23
|
+
const { config, command } = params;
|
|
24
|
+
const commandJson = JSON.stringify(command);
|
|
25
|
+
const intentName = command.payload?.executeParam?.intentName ??
|
|
26
|
+
command.header?.name ??
|
|
27
|
+
"Command";
|
|
28
|
+
logger.log(`[CRON-CMD] Sending command via push, intent=${intentName}`);
|
|
29
|
+
// 1. Persist command data
|
|
30
|
+
let pushDataId = "";
|
|
31
|
+
try {
|
|
32
|
+
pushDataId = await savePushData(commandJson);
|
|
33
|
+
logger.log(`[CRON-CMD] Command data saved, pushDataId=${pushDataId.substring(0, 20)}`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.error("[CRON-CMD] Failed to save command data:", error);
|
|
37
|
+
}
|
|
38
|
+
// 2. Load push IDs
|
|
39
|
+
let pushIdList = [];
|
|
40
|
+
try {
|
|
41
|
+
pushIdList = await getAllPushIds();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.error("[CRON-CMD] Failed to load pushIds:", error);
|
|
45
|
+
}
|
|
46
|
+
if (pushIdList.length === 0) {
|
|
47
|
+
pushIdList = [config.pushId];
|
|
48
|
+
}
|
|
49
|
+
// 3. Broadcast push notification
|
|
50
|
+
const pushService = new XYPushService(config);
|
|
51
|
+
const title = `定时任务: ${intentName}`;
|
|
52
|
+
const pushText = commandJson.length > 1000 ? commandJson.slice(0, 1000) : commandJson;
|
|
53
|
+
let successCount = 0;
|
|
54
|
+
for (const pushId of pushIdList) {
|
|
55
|
+
try {
|
|
56
|
+
await pushService.sendPush(pushText, title, undefined, config.defaultSessionId || "", pushDataId, pushId);
|
|
57
|
+
successCount++;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger.error(`[CRON-CMD] Failed to send push to pushId=${pushId.substring(0, 20)}`, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
logger.log(`[CRON-CMD] Push sent to ${successCount}/${pushIdList.length} pushId(s), intent=${intentName}`);
|
|
64
|
+
}
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -63,10 +63,20 @@ export interface SendCommandParams {
|
|
|
63
63
|
sessionId: string;
|
|
64
64
|
taskId: string;
|
|
65
65
|
messageId: string;
|
|
66
|
-
command
|
|
66
|
+
command?: A2ACommand;
|
|
67
|
+
commands?: A2ACommand[];
|
|
68
|
+
/** toolCallId from the tool's execute() — used for cron detection via hook-set Map. */
|
|
69
|
+
toolCallId?: string;
|
|
67
70
|
}
|
|
68
71
|
/**
|
|
69
72
|
* Send a command as an artifact update (final=false).
|
|
73
|
+
*
|
|
74
|
+
* Cron-aware: if the sessionId starts with the cron prefix ("cron-"),
|
|
75
|
+
* the command is delivered through the push channel instead of the
|
|
76
|
+
* WebSocket session, because cron-triggered tool calls have no active
|
|
77
|
+
* WebSocket session. The device receives the push, executes the command,
|
|
78
|
+
* and returns results through the normal WebSocket path — so response
|
|
79
|
+
* listening in the calling tool works unchanged.
|
|
70
80
|
*/
|
|
71
81
|
export declare function sendCommand(params: SendCommandParams): Promise<void>;
|
|
72
82
|
/**
|
package/dist/src/formatter.js
CHANGED
|
@@ -5,6 +5,7 @@ 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
9
|
// ─────────────────────────────────────────────────────────────
|
|
9
10
|
// 敏感信息脱敏辅助函数
|
|
10
11
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -199,9 +200,28 @@ export async function sendStatusUpdate(params) {
|
|
|
199
200
|
}
|
|
200
201
|
/**
|
|
201
202
|
* Send a command as an artifact update (final=false).
|
|
203
|
+
*
|
|
204
|
+
* Cron-aware: if the sessionId starts with the cron prefix ("cron-"),
|
|
205
|
+
* the command is delivered through the push channel instead of the
|
|
206
|
+
* WebSocket session, because cron-triggered tool calls have no active
|
|
207
|
+
* WebSocket session. The device receives the push, executes the command,
|
|
208
|
+
* and returns results through the normal WebSocket path — so response
|
|
209
|
+
* listening in the calling tool works unchanged.
|
|
202
210
|
*/
|
|
203
211
|
export async function sendCommand(params) {
|
|
204
|
-
const { config, sessionId, taskId, messageId,
|
|
212
|
+
const { config, sessionId, taskId, messageId, toolCallId } = params;
|
|
213
|
+
const commands = params.commands ?? (params.command ? [params.command] : []);
|
|
214
|
+
if (commands.length === 0) {
|
|
215
|
+
throw new Error("sendCommand requires command or commands.");
|
|
216
|
+
}
|
|
217
|
+
// ── Cron mode: route through push channel ──────────────────────
|
|
218
|
+
// Detected via: (a) sessionId "cron-" prefix from synthetic session, OR
|
|
219
|
+
// (b) toolCallId marked by before_tool_call hook from openclaw's sessionKey.
|
|
220
|
+
if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
|
|
221
|
+
const { sendCommandViaPush } = await import("./cron-command.js");
|
|
222
|
+
return sendCommandViaPush({ config, command: commands[0] });
|
|
223
|
+
}
|
|
224
|
+
// ── Normal mode: WebSocket ─────────────────────────────────────
|
|
205
225
|
// Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
|
|
206
226
|
// fall back to closure-captured values
|
|
207
227
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
@@ -221,7 +241,7 @@ export async function sendCommand(params) {
|
|
|
221
241
|
{
|
|
222
242
|
kind: "data",
|
|
223
243
|
data: {
|
|
224
|
-
commands
|
|
244
|
+
commands,
|
|
225
245
|
},
|
|
226
246
|
},
|
|
227
247
|
],
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { A2AJsonRpcRequest, A2AMessagePart, A2ADataEvent } from "./types.js";
|
|
1
|
+
import type { A2AJsonRpcRequest, A2AMessagePart, A2ADataEvent, RunCrossTaskContext } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Parsed message information extracted from A2A request.
|
|
4
4
|
* Note: agentId is not extracted from message - it should come from config.
|
|
@@ -30,6 +30,7 @@ export declare function extractFileParts(parts: A2AMessagePart[]): Array<{
|
|
|
30
30
|
* Extract data events from message parts (for tool responses).
|
|
31
31
|
*/
|
|
32
32
|
export declare function extractDataEvents(parts: A2AMessagePart[]): A2ADataEvent[];
|
|
33
|
+
export declare function extractRunCrossTaskContext(parts: A2AMessagePart[]): RunCrossTaskContext | null;
|
|
33
34
|
/**
|
|
34
35
|
* Check if message is a clearContext request.
|
|
35
36
|
*/
|
package/dist/src/parser.js
CHANGED
|
@@ -45,6 +45,31 @@ export function extractDataEvents(parts) {
|
|
|
45
45
|
.map((part) => part.data.event)
|
|
46
46
|
.filter((event) => event !== undefined);
|
|
47
47
|
}
|
|
48
|
+
export function extractRunCrossTaskContext(parts) {
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
if (part.kind !== "data" || !part.data) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const context = part.data.runCrossTaskContext;
|
|
54
|
+
if (!context || typeof context !== "object") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const networkId = typeof context.networkId === "string" ? context.networkId : "";
|
|
58
|
+
if (!networkId) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
agentId: typeof context.agentId === "string" ? context.agentId : "",
|
|
63
|
+
sessionId: typeof context.sessionId === "string" ? context.sessionId : "",
|
|
64
|
+
isDistributed: context.isDistributed === true,
|
|
65
|
+
networkId,
|
|
66
|
+
isSupportAgent: context.isSupportAgent !== false,
|
|
67
|
+
fileUrls: Array.isArray(context.fileUrls) ? context.fileUrls.filter((url) => typeof url === "string") : [],
|
|
68
|
+
rawContext: context,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
48
73
|
/**
|
|
49
74
|
* Check if message is a clearContext request.
|
|
50
75
|
*/
|
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
|
-
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
|
|
2
|
+
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate, sendCommand } from "./formatter.js";
|
|
3
3
|
import { resolveXYConfig } from "./config.js";
|
|
4
4
|
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
5
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
6
|
import fs from "fs/promises";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import { logger } from "./utils/logger.js";
|
|
8
9
|
const TEMP_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
10
|
+
const RUN_CROSS_TASK_LOG_TAG = "[RunCrossTask]";
|
|
11
|
+
function buildDistributionStatusCommand(context) {
|
|
12
|
+
return {
|
|
13
|
+
header: {
|
|
14
|
+
namespace: "DistributionInteraction",
|
|
15
|
+
name: "DistributionStatus",
|
|
16
|
+
},
|
|
17
|
+
payload: {
|
|
18
|
+
agentId: context.agentId,
|
|
19
|
+
isDistributed: true,
|
|
20
|
+
networkId: context.networkId,
|
|
21
|
+
distributionType: "softbus",
|
|
22
|
+
distributionExecutePolicy: "backgroundExecution",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function buildCrossTaskExecuteResultCommand(code, message, fileUrls = []) {
|
|
27
|
+
return {
|
|
28
|
+
header: {
|
|
29
|
+
namespace: "DistributionInteraction",
|
|
30
|
+
name: "CrossTaskExecuteResult",
|
|
31
|
+
},
|
|
32
|
+
payload: {
|
|
33
|
+
code,
|
|
34
|
+
message,
|
|
35
|
+
fileUrls,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function sendRunCrossTaskResult(params) {
|
|
40
|
+
const { config, sessionId, taskId, messageId, context, resultCode, resultMessage } = params;
|
|
41
|
+
const fileUrls = Array.isArray(context.fileUrls) ? context.fileUrls : [];
|
|
42
|
+
const statusCommand = buildDistributionStatusCommand(context);
|
|
43
|
+
const resultCommand = buildCrossTaskExecuteResultCommand(resultCode, resultMessage, fileUrls);
|
|
44
|
+
await sendCommand({
|
|
45
|
+
config,
|
|
46
|
+
sessionId,
|
|
47
|
+
taskId,
|
|
48
|
+
messageId,
|
|
49
|
+
commands: [statusCommand, resultCommand],
|
|
50
|
+
});
|
|
51
|
+
logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, fileUrlCount=${fileUrls.length}, messageLength=${resultMessage.length}`);
|
|
52
|
+
}
|
|
9
53
|
/**
|
|
10
54
|
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
11
55
|
*/
|
|
@@ -71,6 +115,10 @@ export function createXYReplyDispatcher(params) {
|
|
|
71
115
|
let hasSentResponse = false;
|
|
72
116
|
let finalSent = false;
|
|
73
117
|
let accumulatedText = "";
|
|
118
|
+
const initialRunCrossTaskContext = getCurrentSessionContext()?.runCrossTaskContext;
|
|
119
|
+
const getRunCrossTaskContext = () => {
|
|
120
|
+
return getCurrentSessionContext()?.runCrossTaskContext ?? initialRunCrossTaskContext;
|
|
121
|
+
};
|
|
74
122
|
/**
|
|
75
123
|
* Start the status update interval
|
|
76
124
|
*/
|
|
@@ -180,6 +228,18 @@ export function createXYReplyDispatcher(params) {
|
|
|
180
228
|
if (hasSentResponse && !finalSent) {
|
|
181
229
|
scopedLog().log(`[ON-IDLE] Sending accumulated text, length=${accumulatedText.length}`);
|
|
182
230
|
try {
|
|
231
|
+
const runCrossTaskContext = getRunCrossTaskContext();
|
|
232
|
+
if (runCrossTaskContext) {
|
|
233
|
+
await sendRunCrossTaskResult({
|
|
234
|
+
config,
|
|
235
|
+
sessionId,
|
|
236
|
+
taskId: currentTaskId,
|
|
237
|
+
messageId: currentMessageId,
|
|
238
|
+
context: runCrossTaskContext,
|
|
239
|
+
resultCode: "0",
|
|
240
|
+
resultMessage: accumulatedText,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
183
243
|
// 🔑 使用动态taskId发送完成状态
|
|
184
244
|
await sendStatusUpdate({
|
|
185
245
|
config,
|
|
@@ -211,6 +271,18 @@ export function createXYReplyDispatcher(params) {
|
|
|
211
271
|
// 正常失败场景(非steered)
|
|
212
272
|
scopedLog().log(`[ON-IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
|
|
213
273
|
try {
|
|
274
|
+
const runCrossTaskContext = getRunCrossTaskContext();
|
|
275
|
+
if (runCrossTaskContext) {
|
|
276
|
+
await sendRunCrossTaskResult({
|
|
277
|
+
config,
|
|
278
|
+
sessionId,
|
|
279
|
+
taskId: currentTaskId,
|
|
280
|
+
messageId: currentMessageId,
|
|
281
|
+
context: runCrossTaskContext,
|
|
282
|
+
resultCode: "1",
|
|
283
|
+
resultMessage: "任务执行异常,请重试",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
214
286
|
await sendStatusUpdate({
|
|
215
287
|
config,
|
|
216
288
|
sessionId,
|
|
@@ -17,6 +17,8 @@ import { createGetEmailToolSchemaTool } from "./get-email-tool-schema.js";
|
|
|
17
17
|
import { createLoginTokenTool } from "./login-token-tool.js";
|
|
18
18
|
import { createAgentAsSkillTool } from "./agent-as-skill-tool.js";
|
|
19
19
|
import { createFindPcDevicesTool } from "./find-pc-devices-tool.js";
|
|
20
|
+
import { createDiscoverCrossDevicesTool } from "./discover-cross-devices-tool.js";
|
|
21
|
+
import { createSendCrossDeviceTaskTool } from "./send-cross-device-task-tool.js";
|
|
20
22
|
import { logger } from "../utils/logger.js";
|
|
21
23
|
/**
|
|
22
24
|
* Create all XY channel tools for the given session context.
|
|
@@ -32,6 +34,8 @@ export function createAllTools(ctx) {
|
|
|
32
34
|
logger.log(`[CREATE-ALL-TOOLS] creating tools`);
|
|
33
35
|
return [
|
|
34
36
|
createLocationTool(ctx),
|
|
37
|
+
createDiscoverCrossDevicesTool(ctx),
|
|
38
|
+
createSendCrossDeviceTaskTool(ctx),
|
|
35
39
|
createCallDeviceTool(ctx),
|
|
36
40
|
createGetNoteToolSchemaTool(ctx),
|
|
37
41
|
createGetCalendarToolSchemaTool(ctx),
|