@ynhcj/xiaoyi-channel 0.0.151-beta → 0.0.151-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 +23 -0
- package/dist/src/bot.js +9 -1
- package/dist/src/channel.js +59 -5
- package/dist/src/cron-command.d.ts +15 -0
- package/dist/src/cron-command.js +49 -0
- package/dist/src/cron-query-handler.d.ts +7 -0
- package/dist/src/cron-query-handler.js +188 -0
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +2 -2
- package/dist/src/cspl/config.js +30 -10
- package/dist/src/cspl/constants.d.ts +3 -0
- package/dist/src/cspl/constants.js +5 -0
- package/dist/src/cspl/sentinel_hook.js +17 -3
- package/dist/src/cspl/utils.js +2 -2
- package/dist/src/formatter.d.ts +14 -1
- package/dist/src/formatter.js +31 -8
- package/dist/src/monitor.js +13 -1
- package/dist/src/parser.d.ts +2 -1
- package/dist/src/parser.js +55 -0
- package/dist/src/provider.js +19 -17
- package/dist/src/push.d.ts +11 -1
- package/dist/src/push.js +93 -2
- package/dist/src/reply-dispatcher.js +113 -14
- package/dist/src/self-evolution-handler.js +1 -1
- package/dist/src/tools/agent-as-skill-tool.js +56 -4
- package/dist/src/tools/calendar-tool.js +2 -1
- package/dist/src/tools/call-device-tool.js +0 -3
- package/dist/src/tools/call-phone-tool.js +2 -1
- package/dist/src/tools/create-alarm-tool.js +2 -1
- package/dist/src/tools/create-all-tools.js +8 -4
- package/dist/src/tools/delete-alarm-tool.js +2 -1
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +12 -5
- 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/display-a2ui-card-tool.d.ts +2 -0
- package/dist/src/tools/display-a2ui-card-tool.js +85 -0
- package/dist/src/tools/find-pc-devices-tool.js +1 -0
- package/dist/src/tools/get-collection-tool-schema.js +1 -1
- package/dist/src/tools/get-device-file-tool-schema.js +2 -3
- package/dist/src/tools/location-tool.js +2 -1
- package/dist/src/tools/modify-alarm-tool.js +2 -1
- package/dist/src/tools/modify-note-tool.js +2 -1
- package/dist/src/tools/note-tool.js +2 -1
- package/dist/src/tools/query-app-message-tool.js +4 -3
- package/dist/src/tools/query-memory-data-tool.js +4 -3
- package/dist/src/tools/query-todo-task-tool.js +4 -3
- package/dist/src/tools/save-file-to-phone-tool.js +2 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +2 -1
- package/dist/src/tools/schema-tool-factory.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +2 -1
- package/dist/src/tools/search-calendar-tool.js +2 -1
- package/dist/src/tools/search-contact-tool.js +2 -1
- package/dist/src/tools/search-email-tool.js +4 -3
- package/dist/src/tools/search-file-tool.js +6 -10
- package/dist/src/tools/search-message-tool.js +1 -0
- package/dist/src/tools/search-note-tool.js +2 -1
- package/dist/src/tools/search-photo-gallery-tool.js +4 -3
- package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
- package/dist/src/tools/send-cross-device-task-tool.js +299 -0
- package/dist/src/tools/send-email-tool.js +4 -3
- package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +35 -6
- package/dist/src/tools/send-message-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +14 -1
- package/dist/src/tools/session-manager.js +73 -0
- package/dist/src/tools/upload-file-tool.js +6 -14
- package/dist/src/tools/upload-photo-tool.js +4 -3
- package/dist/src/tools/xiaoyi-add-collection-tool.js +4 -2
- package/dist/src/tools/xiaoyi-collection-tool.js +3 -2
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +3 -2
- package/dist/src/tools/xiaoyi-gui-tool.js +6 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +207 -15
- package/package.json +1 -1
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
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -41,7 +42,7 @@ function buildTextPreview(text) {
|
|
|
41
42
|
* Send an A2A artifact update response.
|
|
42
43
|
*/
|
|
43
44
|
export async function sendA2AResponse(params) {
|
|
44
|
-
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage } = params;
|
|
45
|
+
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage, log: shouldLog = true } = params;
|
|
45
46
|
const log = logger.withContext(sessionId, taskId);
|
|
46
47
|
// 审批桥接:将 OpenClaw 的审批提示翻译成用户友好的确认文案
|
|
47
48
|
const bridgedText = text === undefined ? text : rewriteOutboundApprovalText(sessionId, text);
|
|
@@ -96,11 +97,14 @@ export async function sendA2AResponse(params) {
|
|
|
96
97
|
taskId,
|
|
97
98
|
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
98
99
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
if (shouldLog) {
|
|
101
|
+
const redactedText = redactSensitiveText(bridgedText ?? "");
|
|
102
|
+
log.log(`[A2A_RESPONSE] Sending artifact-update, append=${append}, final=${final}, text=${buildTextPreview(redactedText)}, files=${files?.length ?? 0}, sensitive=${containsSensitiveInfo(bridgedText ?? "")}`);
|
|
103
|
+
}
|
|
102
104
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
103
|
-
|
|
105
|
+
if (shouldLog) {
|
|
106
|
+
log.log(`[A2A_RESPONSE] Message sent successfully`);
|
|
107
|
+
}
|
|
104
108
|
}
|
|
105
109
|
/**
|
|
106
110
|
* Send an A2A artifact-update with reasoningText part.
|
|
@@ -199,9 +203,28 @@ export async function sendStatusUpdate(params) {
|
|
|
199
203
|
}
|
|
200
204
|
/**
|
|
201
205
|
* Send a command as an artifact update (final=false).
|
|
206
|
+
*
|
|
207
|
+
* Cron-aware: if the sessionId starts with the cron prefix ("cron-"),
|
|
208
|
+
* the command is delivered through the push channel instead of the
|
|
209
|
+
* WebSocket session, because cron-triggered tool calls have no active
|
|
210
|
+
* WebSocket session. The device receives the push, executes the command,
|
|
211
|
+
* and returns results through the normal WebSocket path — so response
|
|
212
|
+
* listening in the calling tool works unchanged.
|
|
202
213
|
*/
|
|
203
214
|
export async function sendCommand(params) {
|
|
204
|
-
const { config, sessionId, taskId, messageId,
|
|
215
|
+
const { config, sessionId, taskId, messageId, toolCallId } = params;
|
|
216
|
+
const commands = params.commands ?? (params.command ? [params.command] : []);
|
|
217
|
+
if (commands.length === 0) {
|
|
218
|
+
throw new Error("sendCommand requires command or commands.");
|
|
219
|
+
}
|
|
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
|
+
if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
|
|
224
|
+
const { sendCommandViaPush } = await import("./cron-command.js");
|
|
225
|
+
return sendCommandViaPush({ config, command: commands[0] });
|
|
226
|
+
}
|
|
227
|
+
// ── Normal mode: WebSocket ─────────────────────────────────────
|
|
205
228
|
// Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
|
|
206
229
|
// fall back to closure-captured values
|
|
207
230
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
@@ -214,14 +237,14 @@ export async function sendCommand(params) {
|
|
|
214
237
|
kind: "artifact-update",
|
|
215
238
|
append: false,
|
|
216
239
|
lastChunk: true,
|
|
217
|
-
final: false,
|
|
240
|
+
final: params.final ?? false,
|
|
218
241
|
artifact: {
|
|
219
242
|
artifactId: uuidv4(),
|
|
220
243
|
parts: [
|
|
221
244
|
{
|
|
222
245
|
kind: "data",
|
|
223
246
|
data: {
|
|
224
|
-
commands
|
|
247
|
+
commands,
|
|
225
248
|
},
|
|
226
249
|
},
|
|
227
250
|
],
|
package/dist/src/monitor.js
CHANGED
|
@@ -7,6 +7,7 @@ import { sendA2AResponse } from "./formatter.js";
|
|
|
7
7
|
import { handleTriggerEvent } from "./trigger-handler.js";
|
|
8
8
|
import { handleSelfEvolutionEvent, handleSelfEvolutionStateGetEvent } from "./self-evolution-handler.js";
|
|
9
9
|
import { handleLoginTokenEvent } from "./login-token-handler.js";
|
|
10
|
+
import { handleCronQueryEvent } from "./cron-query-handler.js";
|
|
10
11
|
import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
|
|
11
12
|
import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./tools/session-manager.js";
|
|
12
13
|
import { logger } from "./utils/logger.js";
|
|
@@ -103,10 +104,13 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
103
104
|
}
|
|
104
105
|
catch (err) {
|
|
105
106
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
106
|
-
releaseGate();
|
|
107
107
|
logger.error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
|
|
108
108
|
}
|
|
109
109
|
finally {
|
|
110
|
+
// 🔑 确保门控始终被释放。handleXYMessage 内部会 catch 所有异常
|
|
111
|
+
// 且某些提前返回路径(clearContext、tasks/cancel 等)不会调用
|
|
112
|
+
// onInitComplete,因此必须在 finally 中兜底释放。
|
|
113
|
+
releaseGate();
|
|
110
114
|
// Remove from active messages when done
|
|
111
115
|
activeMessages.delete(messageKey);
|
|
112
116
|
}
|
|
@@ -183,6 +187,12 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
183
187
|
logger.log(`[MONITOR] Received login-token-event, dispatching to handler...`);
|
|
184
188
|
handleLoginTokenEvent(context, runtime);
|
|
185
189
|
};
|
|
190
|
+
const cronQueryEventHandler = (context) => {
|
|
191
|
+
logger.log(`[MONITOR] Received cron-query-event, dispatching to handler...`);
|
|
192
|
+
handleCronQueryEvent(context, cfg).catch((err) => {
|
|
193
|
+
logger.error(`[MONITOR] Failed to handle cron-query-event:`, err);
|
|
194
|
+
});
|
|
195
|
+
};
|
|
186
196
|
const cleanup = () => {
|
|
187
197
|
logger.log("XY gateway: cleaning up...");
|
|
188
198
|
// 🔍 Diagnose before cleanup
|
|
@@ -203,6 +213,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
203
213
|
wsManager.off("self-evolution-event", selfEvolutionHandler);
|
|
204
214
|
wsManager.off("self-evolution-state-get-event", selfEvolutionStateGetHandler);
|
|
205
215
|
wsManager.off("login-token-event", loginTokenEventHandler);
|
|
216
|
+
wsManager.off("cron-query-event", cronQueryEventHandler);
|
|
206
217
|
// ✅ Disconnect the wsManager to prevent connection leaks
|
|
207
218
|
// This is safe because each gateway lifecycle should have clean connections
|
|
208
219
|
wsManager.disconnect();
|
|
@@ -266,6 +277,7 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
266
277
|
wsManager.on("self-evolution-event", selfEvolutionHandler);
|
|
267
278
|
wsManager.on("self-evolution-state-get-event", selfEvolutionStateGetHandler);
|
|
268
279
|
wsManager.on("login-token-event", loginTokenEventHandler);
|
|
280
|
+
wsManager.on("cron-query-event", cronQueryEventHandler);
|
|
269
281
|
// Start periodic health check (every 6 hours)
|
|
270
282
|
logger.log("Starting periodic health check (every 6 hours)...");
|
|
271
283
|
healthCheckInterval = setInterval(() => {
|
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,61 @@ 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
|
+
const normalizeSentFiles = (value) => {
|
|
50
|
+
if (!Array.isArray(value)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return value
|
|
54
|
+
.map((item) => {
|
|
55
|
+
if (!item || typeof item !== "object") {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const candidate = item;
|
|
59
|
+
const fileLocalUrls = Array.isArray(candidate.fileLocalUrls)
|
|
60
|
+
? candidate.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
61
|
+
: [];
|
|
62
|
+
const fileRemoteUrls = Array.isArray(candidate.fileRemoteUrls)
|
|
63
|
+
? candidate.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
64
|
+
: [];
|
|
65
|
+
const fileNames = Array.isArray(candidate.fileNames)
|
|
66
|
+
? candidate.fileNames.filter((name) => typeof name === "string" && name.length > 0)
|
|
67
|
+
: [];
|
|
68
|
+
if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
|
|
73
|
+
...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
|
|
74
|
+
...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
.filter((item) => item !== null);
|
|
78
|
+
};
|
|
79
|
+
for (const part of parts) {
|
|
80
|
+
if (part.kind !== "data" || !part.data) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const context = part.data.runCrossTaskContext;
|
|
84
|
+
if (!context || typeof context !== "object") {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const networkId = typeof context.networkId === "string" ? context.networkId : "";
|
|
88
|
+
if (!networkId) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
agentId: typeof context.agentId === "string" ? context.agentId : "",
|
|
93
|
+
sessionId: typeof context.sessionId === "string" ? context.sessionId : "",
|
|
94
|
+
isDistributed: context.isDistributed === true,
|
|
95
|
+
networkId,
|
|
96
|
+
isSupportAgent: context.isSupportAgent !== false,
|
|
97
|
+
sentFiles: normalizeSentFiles(context.sentFiles),
|
|
98
|
+
rawContext: context,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
48
103
|
/**
|
|
49
104
|
* Check if message is a clearContext request.
|
|
50
105
|
*/
|
package/dist/src/provider.js
CHANGED
|
@@ -536,26 +536,28 @@ export const xiaoyiProvider = {
|
|
|
536
536
|
const beforeLen = sp.length;
|
|
537
537
|
// 删除 ## Tooling 与 TOOLS.md 声明之间的内容
|
|
538
538
|
sp = sp.replace(/(## Tooling)[\s\S]*?(TOOLS\.md does not control tool availability; it is user guidance for how to use external tools\.)/, "$1\n\n$2");
|
|
539
|
-
// (1)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
539
|
+
// (1) Skills 部分:移动到 ## Runtime 之前
|
|
540
|
+
if (sp.includes('## Runtime')) {
|
|
541
|
+
// 提取 ## Skills (mandatory) 到 </available_skills> 作为第一部分
|
|
542
|
+
const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
|
|
543
|
+
if (skillsMatch) {
|
|
544
|
+
const part1 = skillsMatch[0];
|
|
545
|
+
sp = sp.replace(part1, '');
|
|
546
|
+
sp = sp.replace('## Runtime', part1 + '\n\n## Runtime');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// (2) SOUL.md 部分:移动到 ## Silent Replies 之前
|
|
550
|
+
if (sp.includes('## Silent Replies')) {
|
|
551
|
+
// 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 其特定脚注结束标志 的内容作为第二部分
|
|
552
|
+
const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?_This file is yours to evolve\. As you learn who you are, update it\._)/);
|
|
553
|
+
if (soulMatch) {
|
|
554
|
+
const part2 = soulMatch[1].trim();
|
|
550
555
|
sp = sp.replace(soulMatch[1], '');
|
|
551
|
-
|
|
552
|
-
sp = sp.replace(/\n{3,}/g, '\n\n');
|
|
553
|
-
// (3) 将 第二部分 + 第一部分 插入到 ## Runtime 上面
|
|
554
|
-
const combined = (part2 + '\n\n' + part1).trim();
|
|
555
|
-
if (combined && sp.includes('## Runtime')) {
|
|
556
|
-
sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
|
|
556
|
+
sp = sp.replace('## Silent Replies', part2 + '\n\n## Silent Replies');
|
|
557
557
|
}
|
|
558
558
|
}
|
|
559
|
+
// 清理多余空行
|
|
560
|
+
sp = sp.replace(/\n{3,}/g, '\n\n');
|
|
559
561
|
logger.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
|
|
560
562
|
context.systemPrompt = sp;
|
|
561
563
|
}
|
package/dist/src/push.d.ts
CHANGED
|
@@ -5,9 +5,14 @@ import type { XYChannelConfig } from "./types.js";
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class XYPushService {
|
|
7
7
|
private config;
|
|
8
|
-
private readonly
|
|
8
|
+
private readonly PROD_PUSH_URL;
|
|
9
|
+
private readonly TEST_PUSH_URL;
|
|
9
10
|
private readonly REQUEST_FROM;
|
|
10
11
|
constructor(config: XYChannelConfig);
|
|
12
|
+
/**
|
|
13
|
+
* Resolve push URL: config.pushUrl > inferred from fileUploadUrl > production default.
|
|
14
|
+
*/
|
|
15
|
+
private resolvePushUrl;
|
|
11
16
|
/**
|
|
12
17
|
* Generate a random trace ID for request tracking.
|
|
13
18
|
*/
|
|
@@ -23,6 +28,11 @@ export declare class XYPushService {
|
|
|
23
28
|
* @param pushId - Push ID to use (required)
|
|
24
29
|
*/
|
|
25
30
|
sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string, pushDataId?: string, pushId?: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Send a push message with command directives embedded directly.
|
|
33
|
+
* Used for cron-triggered commands where pushText is empty and pushType=101.
|
|
34
|
+
*/
|
|
35
|
+
sendPushWithDirectives(pushId: string, sessionId: string, directives: any[]): Promise<void>;
|
|
26
36
|
/**
|
|
27
37
|
* Send a push message with file attachments.
|
|
28
38
|
*/
|
package/dist/src/push.js
CHANGED
|
@@ -8,11 +8,24 @@ import { logger } from "./utils/logger.js";
|
|
|
8
8
|
*/
|
|
9
9
|
export class XYPushService {
|
|
10
10
|
config;
|
|
11
|
-
|
|
11
|
+
PROD_PUSH_URL = "https://hag.cloud.huawei.com/open-ability-agent/v1/agent-webhook";
|
|
12
|
+
TEST_PUSH_URL = "https://lfhagcp.hwcloudtest.cn:58447/open-ability-agent/v1/agent-webhook";
|
|
12
13
|
REQUEST_FROM = "openclaw";
|
|
13
14
|
constructor(config) {
|
|
14
15
|
this.config = config;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve push URL: config.pushUrl > inferred from fileUploadUrl > production default.
|
|
19
|
+
*/
|
|
20
|
+
resolvePushUrl() {
|
|
21
|
+
if (this.config.pushUrl) {
|
|
22
|
+
return this.config.pushUrl;
|
|
23
|
+
}
|
|
24
|
+
if (this.config.fileUploadUrl?.includes("lfhagmirror")) {
|
|
25
|
+
return this.TEST_PUSH_URL;
|
|
26
|
+
}
|
|
27
|
+
return this.PROD_PUSH_URL;
|
|
28
|
+
}
|
|
16
29
|
/**
|
|
17
30
|
* Generate a random trace ID for request tracking.
|
|
18
31
|
*/
|
|
@@ -30,7 +43,7 @@ export class XYPushService {
|
|
|
30
43
|
* @param pushId - Push ID to use (required)
|
|
31
44
|
*/
|
|
32
45
|
async sendPush(content, title, data, sessionId, pushDataId, pushId) {
|
|
33
|
-
const pushUrl = this.
|
|
46
|
+
const pushUrl = this.resolvePushUrl();
|
|
34
47
|
const traceId = this.generateTraceId();
|
|
35
48
|
// Use provided pushId or fall back to config pushId
|
|
36
49
|
const actualPushId = pushId || this.config.pushId;
|
|
@@ -114,6 +127,84 @@ export class XYPushService {
|
|
|
114
127
|
throw error;
|
|
115
128
|
}
|
|
116
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Send a push message with command directives embedded directly.
|
|
132
|
+
* Used for cron-triggered commands where pushText is empty and pushType=101.
|
|
133
|
+
*/
|
|
134
|
+
async sendPushWithDirectives(pushId, sessionId, directives) {
|
|
135
|
+
const pushUrl = this.resolvePushUrl();
|
|
136
|
+
const traceId = this.generateTraceId();
|
|
137
|
+
logger.log(`[PUSH] Preparing to send push with directives, pushId: ${pushId.substring(0, 20)}...`);
|
|
138
|
+
const requestBody = {
|
|
139
|
+
jsonrpc: "2.0",
|
|
140
|
+
id: randomUUID(),
|
|
141
|
+
result: {
|
|
142
|
+
id: randomUUID(),
|
|
143
|
+
apiId: this.config.apiId,
|
|
144
|
+
pushId,
|
|
145
|
+
pushText: "",
|
|
146
|
+
pushType: 101,
|
|
147
|
+
kind: "task",
|
|
148
|
+
sessionId,
|
|
149
|
+
artifacts: [
|
|
150
|
+
{
|
|
151
|
+
artifactId: randomUUID(),
|
|
152
|
+
parts: [
|
|
153
|
+
{
|
|
154
|
+
kind: "data",
|
|
155
|
+
data: { directives },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(pushUrl, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: {
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
"Accept": "application/json",
|
|
168
|
+
"x-hag-trace-id": traceId,
|
|
169
|
+
"x-uid": this.config.uid,
|
|
170
|
+
"x-api-key": this.config.apiKey,
|
|
171
|
+
"x-request-from": this.REQUEST_FROM,
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify(requestBody),
|
|
174
|
+
});
|
|
175
|
+
logger.log(`[PUSH] Response received, HTTP Status: ${response.status} ${response.statusText}`);
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const errorText = await response.text();
|
|
178
|
+
logger.error(`[PUSH] Push request failed, HTTP Status: ${response.status}`);
|
|
179
|
+
throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
|
|
180
|
+
}
|
|
181
|
+
let result;
|
|
182
|
+
try {
|
|
183
|
+
const responseText = await response.text();
|
|
184
|
+
if (!responseText || responseText.trim() === '') {
|
|
185
|
+
logger.error(`[PUSH] Received empty response body`);
|
|
186
|
+
result = {};
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
result = JSON.parse(responseText);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (parseError) {
|
|
193
|
+
logger.error(`[PUSH] Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
194
|
+
throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
195
|
+
}
|
|
196
|
+
logger.log(`[PUSH] Push message sent successfully, Trace ID: ${traceId}`);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error instanceof Error) {
|
|
200
|
+
logger.error(`[PUSH] Failed to send push message: ${error.name} - ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
logger.error(`[PUSH] Failed to send push message:`, error);
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
117
208
|
/**
|
|
118
209
|
* Send a push message with file attachments.
|
|
119
210
|
*/
|
|
@@ -1,11 +1,56 @@
|
|
|
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 { clearRunCrossTaskSentFiles, 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, sentFiles = []) {
|
|
27
|
+
return {
|
|
28
|
+
header: {
|
|
29
|
+
namespace: "DistributionInteraction",
|
|
30
|
+
name: "CrossTaskExecuteResult",
|
|
31
|
+
},
|
|
32
|
+
payload: {
|
|
33
|
+
code,
|
|
34
|
+
message,
|
|
35
|
+
sentFiles,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function sendRunCrossTaskResult(params) {
|
|
40
|
+
const { config, sessionId, taskId, messageId, context, resultCode, resultMessage } = params;
|
|
41
|
+
const sentFiles = Array.isArray(context.sentFiles) ? context.sentFiles : [];
|
|
42
|
+
const statusCommand = buildDistributionStatusCommand(context);
|
|
43
|
+
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}`);
|
|
53
|
+
}
|
|
9
54
|
/**
|
|
10
55
|
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
11
56
|
*/
|
|
@@ -71,6 +116,10 @@ export function createXYReplyDispatcher(params) {
|
|
|
71
116
|
let hasSentResponse = false;
|
|
72
117
|
let finalSent = false;
|
|
73
118
|
let accumulatedText = "";
|
|
119
|
+
const initialRunCrossTaskContext = getCurrentSessionContext()?.runCrossTaskContext;
|
|
120
|
+
const getRunCrossTaskContext = () => {
|
|
121
|
+
return getCurrentSessionContext()?.runCrossTaskContext ?? initialRunCrossTaskContext;
|
|
122
|
+
};
|
|
74
123
|
/**
|
|
75
124
|
* Start the status update interval
|
|
76
125
|
*/
|
|
@@ -123,18 +172,23 @@ export function createXYReplyDispatcher(params) {
|
|
|
123
172
|
scopedLog().log(`[DELIVER SKIP] Empty text, skipping`);
|
|
124
173
|
return;
|
|
125
174
|
}
|
|
175
|
+
// 🔑 如果 onPartialReply 已经流式发送过文本,deliver 不再重复发送
|
|
176
|
+
if (hasSentResponse) {
|
|
177
|
+
scopedLog().log(`[DELIVER SKIP] Already sent via onPartialReply`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
126
180
|
accumulatedText += text;
|
|
127
181
|
hasSentResponse = true;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
await sendReasoningTextUpdate({
|
|
182
|
+
// 🔑 使用动态taskId发送A2A响应(流式append)
|
|
183
|
+
await sendA2AResponse({
|
|
131
184
|
config,
|
|
132
185
|
sessionId,
|
|
133
186
|
taskId: currentTaskId,
|
|
134
187
|
messageId: currentMessageId,
|
|
135
188
|
text,
|
|
189
|
+
append: true,
|
|
190
|
+
final: false,
|
|
136
191
|
});
|
|
137
|
-
scopedLog().log(`[DELIVER] Sent deliver text as reasoningText update`);
|
|
138
192
|
}
|
|
139
193
|
catch (deliverError) {
|
|
140
194
|
scopedLog().error(`Failed to deliver message:`, deliverError);
|
|
@@ -180,6 +234,18 @@ export function createXYReplyDispatcher(params) {
|
|
|
180
234
|
if (hasSentResponse && !finalSent) {
|
|
181
235
|
scopedLog().log(`[ON-IDLE] Sending accumulated text, length=${accumulatedText.length}`);
|
|
182
236
|
try {
|
|
237
|
+
const runCrossTaskContext = getRunCrossTaskContext();
|
|
238
|
+
if (runCrossTaskContext) {
|
|
239
|
+
await sendRunCrossTaskResult({
|
|
240
|
+
config,
|
|
241
|
+
sessionId,
|
|
242
|
+
taskId: currentTaskId,
|
|
243
|
+
messageId: currentMessageId,
|
|
244
|
+
context: runCrossTaskContext,
|
|
245
|
+
resultCode: "0",
|
|
246
|
+
resultMessage: accumulatedText,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
183
249
|
// 🔑 使用动态taskId发送完成状态
|
|
184
250
|
await sendStatusUpdate({
|
|
185
251
|
config,
|
|
@@ -190,18 +256,18 @@ export function createXYReplyDispatcher(params) {
|
|
|
190
256
|
state: "completed",
|
|
191
257
|
});
|
|
192
258
|
scopedLog().log(`[ON-IDLE] Sent completion status update`);
|
|
193
|
-
// 🔑 使用动态taskId
|
|
259
|
+
// 🔑 使用动态taskId发送最终响应(空字符串表示流结束)
|
|
194
260
|
await sendA2AResponse({
|
|
195
261
|
config,
|
|
196
262
|
sessionId,
|
|
197
263
|
taskId: currentTaskId,
|
|
198
264
|
messageId: currentMessageId,
|
|
199
|
-
text:
|
|
265
|
+
text: "",
|
|
200
266
|
append: false,
|
|
201
267
|
final: true,
|
|
202
268
|
});
|
|
203
269
|
finalSent = true;
|
|
204
|
-
scopedLog().log(`[ON-IDLE] Sent final response`);
|
|
270
|
+
scopedLog().log(`[ON-IDLE] Sent final response (empty, stream end)`);
|
|
205
271
|
}
|
|
206
272
|
catch (err) {
|
|
207
273
|
scopedLog().error(`[ON-IDLE] Failed to send final response:`, err);
|
|
@@ -211,6 +277,18 @@ export function createXYReplyDispatcher(params) {
|
|
|
211
277
|
// 正常失败场景(非steered)
|
|
212
278
|
scopedLog().log(`[ON-IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
|
|
213
279
|
try {
|
|
280
|
+
const runCrossTaskContext = getRunCrossTaskContext();
|
|
281
|
+
if (runCrossTaskContext) {
|
|
282
|
+
await sendRunCrossTaskResult({
|
|
283
|
+
config,
|
|
284
|
+
sessionId,
|
|
285
|
+
taskId: currentTaskId,
|
|
286
|
+
messageId: currentMessageId,
|
|
287
|
+
context: runCrossTaskContext,
|
|
288
|
+
resultCode: "1",
|
|
289
|
+
resultMessage: "任务执行异常,请重试",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
214
292
|
await sendStatusUpdate({
|
|
215
293
|
config,
|
|
216
294
|
sessionId,
|
|
@@ -249,6 +327,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
249
327
|
dispatcher,
|
|
250
328
|
replyOptions: {
|
|
251
329
|
...replyOptions,
|
|
330
|
+
suppressToolErrorWarnings: true,
|
|
252
331
|
onModelSelected: prefixContext.onModelSelected,
|
|
253
332
|
onToolStart: async ({ name, phase }) => {
|
|
254
333
|
// 🔑 steered dispatch不发送tool状态(让主dispatcher处理)
|
|
@@ -315,10 +394,25 @@ export function createXYReplyDispatcher(params) {
|
|
|
315
394
|
if (steerState.steered) {
|
|
316
395
|
return;
|
|
317
396
|
}
|
|
397
|
+
const currentTaskId = getActiveTaskId();
|
|
398
|
+
const currentMessageId = getActiveMessageId();
|
|
318
399
|
const text = payload.text ?? "";
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
400
|
+
try {
|
|
401
|
+
if (text.length > 0) {
|
|
402
|
+
// 🔑 将模型真实的thinking/reasoning内容通过reasoningText转发
|
|
403
|
+
await sendReasoningTextUpdate({
|
|
404
|
+
config,
|
|
405
|
+
sessionId,
|
|
406
|
+
taskId: currentTaskId,
|
|
407
|
+
messageId: currentMessageId,
|
|
408
|
+
text,
|
|
409
|
+
append: false,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
scopedLog().error(`[REASONING-STREAM] Failed to send reasoning text:`, err);
|
|
415
|
+
}
|
|
322
416
|
},
|
|
323
417
|
onPartialReply: async (payload) => {
|
|
324
418
|
// 🔑 steered dispatch不发送partial reply(让主dispatcher处理)
|
|
@@ -330,18 +424,23 @@ export function createXYReplyDispatcher(params) {
|
|
|
330
424
|
const text = payload.text ?? "";
|
|
331
425
|
try {
|
|
332
426
|
if (text.length > 0) {
|
|
333
|
-
|
|
427
|
+
accumulatedText += text;
|
|
428
|
+
hasSentResponse = true;
|
|
429
|
+
// 🔑 流式文本通过 A2A text 通道发送(而非 reasoningText)
|
|
430
|
+
await sendA2AResponse({
|
|
334
431
|
config,
|
|
335
432
|
sessionId,
|
|
336
433
|
taskId: currentTaskId,
|
|
337
434
|
messageId: currentMessageId,
|
|
338
|
-
text,
|
|
435
|
+
text: accumulatedText,
|
|
339
436
|
append: false,
|
|
437
|
+
final: false,
|
|
438
|
+
log: false,
|
|
340
439
|
});
|
|
341
440
|
}
|
|
342
441
|
}
|
|
343
442
|
catch (err) {
|
|
344
|
-
scopedLog().error(`[PARTIAL
|
|
443
|
+
scopedLog().error(`[PARTIAL-REPLY] Failed to send partial reply:`, err);
|
|
345
444
|
}
|
|
346
445
|
},
|
|
347
446
|
},
|