@ynhcj/xiaoyi-channel 0.0.166-beta → 0.0.166-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 +141 -1
- package/dist/src/bot.js +60 -5
- package/dist/src/cron-command.d.ts +2 -0
- package/dist/src/cron-command.js +14 -8
- package/dist/src/cron-query-handler.js +45 -8
- package/dist/src/cspl/call_api.js +2 -2
- package/dist/src/cspl/sentinel_hook.js +9 -4
- package/dist/src/cspl/upload_file.js +2 -2
- package/dist/src/cspl/utils.d.ts +9 -3
- package/dist/src/cspl/utils.js +15 -9
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +102 -0
- package/dist/src/formatter.d.ts +29 -0
- package/dist/src/formatter.js +100 -2
- package/dist/src/monitor.js +35 -23
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +23 -13
- package/dist/src/provider.js +41 -1
- package/dist/src/reply-dispatcher.js +34 -16
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +12 -1
- package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
- package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
- package/dist/src/tools/create-all-tools.js +4 -0
- 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/send-html-card-tool.d.ts +7 -0
- package/dist/src/tools/send-html-card-tool.js +113 -0
- package/dist/src/tools/session-manager.d.ts +11 -2
- package/dist/src/tools/session-manager.js +65 -18
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -1
- package/dist/src/types.d.ts +9 -7
- package/dist/src/utils/config-manager.d.ts +3 -2
- package/dist/src/utils/config-manager.js +22 -2
- 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/src/file-upload.js
CHANGED
|
@@ -235,6 +235,108 @@ export class XYFileUploadService {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Upload a file and return a preview-able URL (needPreview=true).
|
|
240
|
+
* Same as uploadFileAndGetUrl but adds needPreview flag to get a directly viewable URL.
|
|
241
|
+
*/
|
|
242
|
+
async uploadFileAndGetPreviewUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
243
|
+
let localFilePath = filePath;
|
|
244
|
+
let isTempFile = false;
|
|
245
|
+
try {
|
|
246
|
+
// Handle remote URLs by downloading first
|
|
247
|
+
if (isRemoteUrl(filePath)) {
|
|
248
|
+
localFilePath = await downloadToTempFile(filePath);
|
|
249
|
+
isTempFile = true;
|
|
250
|
+
}
|
|
251
|
+
// Read file
|
|
252
|
+
const fileBuffer = await fs.readFile(localFilePath);
|
|
253
|
+
const fileName = path.basename(localFilePath);
|
|
254
|
+
const fileSha256 = calculateSHA256(fileBuffer);
|
|
255
|
+
const fileSize = fileBuffer.length;
|
|
256
|
+
// Phase 1: Prepare
|
|
257
|
+
logger.log(`[XY File Upload] Phase 1 (preview): Prepare upload for ${fileName}`);
|
|
258
|
+
const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: {
|
|
261
|
+
"Content-Type": "application/json",
|
|
262
|
+
"x-uid": this.uid,
|
|
263
|
+
"x-api-key": this.apiKey,
|
|
264
|
+
"x-request-from": "openclaw",
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
objectType,
|
|
268
|
+
fileName,
|
|
269
|
+
fileSha256,
|
|
270
|
+
fileSize,
|
|
271
|
+
fileOwnerInfo: {
|
|
272
|
+
uid: this.uid,
|
|
273
|
+
teamId: this.uid,
|
|
274
|
+
},
|
|
275
|
+
useEdge: false,
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
if (!prepareResp.ok) {
|
|
279
|
+
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
280
|
+
}
|
|
281
|
+
const prepareData = await prepareResp.json();
|
|
282
|
+
if (prepareData.code !== "0") {
|
|
283
|
+
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
284
|
+
}
|
|
285
|
+
const { objectId, draftId, uploadInfos } = prepareData;
|
|
286
|
+
logger.log(`[XY File Upload] Prepare (preview) complete: objectId=${objectId}, draftId=${draftId}`);
|
|
287
|
+
// Phase 2: Upload
|
|
288
|
+
logger.log(`[XY File Upload] Phase 2 (preview): Upload file data`);
|
|
289
|
+
const uploadInfo = uploadInfos[0];
|
|
290
|
+
const uploadResp = await fetch(uploadInfo.url, {
|
|
291
|
+
method: uploadInfo.method,
|
|
292
|
+
headers: uploadInfo.headers,
|
|
293
|
+
body: fileBuffer,
|
|
294
|
+
});
|
|
295
|
+
if (!uploadResp.ok) {
|
|
296
|
+
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
297
|
+
}
|
|
298
|
+
logger.log(`[XY File Upload] Upload (preview) complete`);
|
|
299
|
+
// Phase 3: CompleteAndQuery with needPreview=true
|
|
300
|
+
logger.log(`[XY File Upload] Phase 3 (preview): CompleteAndQuery with needPreview=true`);
|
|
301
|
+
const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers: {
|
|
304
|
+
"Content-Type": "application/json",
|
|
305
|
+
"x-uid": this.uid,
|
|
306
|
+
"x-api-key": this.apiKey,
|
|
307
|
+
"x-request-from": "openclaw",
|
|
308
|
+
},
|
|
309
|
+
body: JSON.stringify({
|
|
310
|
+
objectId,
|
|
311
|
+
draftId,
|
|
312
|
+
needPreview: true,
|
|
313
|
+
expireTime: 259200,
|
|
314
|
+
}),
|
|
315
|
+
});
|
|
316
|
+
if (!completeResp.ok) {
|
|
317
|
+
throw new Error(`CompleteAndQuery (preview) failed: HTTP ${completeResp.status}`);
|
|
318
|
+
}
|
|
319
|
+
const completeData = await completeResp.json();
|
|
320
|
+
const fileUrl = completeData?.fileDetailInfo?.url || "";
|
|
321
|
+
if (!fileUrl) {
|
|
322
|
+
throw new Error("No file URL returned from completeAndQuery (preview)");
|
|
323
|
+
}
|
|
324
|
+
logger.log(`[XY File Upload] File upload with preview URL successful`);
|
|
325
|
+
return fileUrl;
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
logger.error(`[XY File Upload] File upload with preview URL failed for ${filePath}:`, error);
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
if (isTempFile) {
|
|
333
|
+
try {
|
|
334
|
+
await fs.unlink(localFilePath);
|
|
335
|
+
}
|
|
336
|
+
catch { }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
238
340
|
/**
|
|
239
341
|
* Upload multiple files and return their file IDs.
|
|
240
342
|
*/
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -82,6 +82,35 @@ export interface SendCommandParams {
|
|
|
82
82
|
* listening in the calling tool works unchanged.
|
|
83
83
|
*/
|
|
84
84
|
export declare function sendCommand(params: SendCommandParams): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Parameters for sending a card (e.g., HTML H5 card).
|
|
87
|
+
*/
|
|
88
|
+
export interface SendCardParams {
|
|
89
|
+
config: XYChannelConfig;
|
|
90
|
+
sessionId: string;
|
|
91
|
+
taskId: string;
|
|
92
|
+
messageId: string;
|
|
93
|
+
/** toolCallId from the tool's execute() — used for cron detection via hook-set Map. */
|
|
94
|
+
toolCallId?: string;
|
|
95
|
+
/** When true, the artifact-update is sent with final=true. Default: false. */
|
|
96
|
+
final?: boolean;
|
|
97
|
+
/** Array of card data objects to send. */
|
|
98
|
+
cardsInfo: CardDataObject[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Card data object for sending display cards.
|
|
102
|
+
*/
|
|
103
|
+
export interface CardDataObject {
|
|
104
|
+
cardName: string;
|
|
105
|
+
cardData: Record<string, any>;
|
|
106
|
+
displayType: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Send a card (e.g., HTML H5 card) as an artifact update (final=false).
|
|
110
|
+
*
|
|
111
|
+
* Cron-aware: same routing logic as sendCommand.
|
|
112
|
+
*/
|
|
113
|
+
export declare function sendCard(params: SendCardParams): Promise<void>;
|
|
85
114
|
/**
|
|
86
115
|
* Parameters for sending a clearContext response.
|
|
87
116
|
*/
|
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
|
*
|
|
@@ -222,7 +264,10 @@ export async function sendCommand(params) {
|
|
|
222
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");
|
|
225
|
-
|
|
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 });
|
|
226
271
|
}
|
|
227
272
|
// ── Normal mode: WebSocket ─────────────────────────────────────
|
|
228
273
|
// Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
|
|
@@ -272,6 +317,59 @@ export async function sendCommand(params) {
|
|
|
272
317
|
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
273
318
|
log.log(`[A2A_COMMAND] Command sent successfully`);
|
|
274
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Send a card (e.g., HTML H5 card) as an artifact update (final=false).
|
|
322
|
+
*
|
|
323
|
+
* Cron-aware: same routing logic as sendCommand.
|
|
324
|
+
*/
|
|
325
|
+
export async function sendCard(params) {
|
|
326
|
+
const { config, sessionId, taskId, messageId, toolCallId } = params;
|
|
327
|
+
// ── Cron mode: route through push channel ──────────────────────
|
|
328
|
+
if (sessionId.startsWith("cron-") || isCronToolCall(toolCallId)) {
|
|
329
|
+
throw new Error("sendCard does not support cron mode");
|
|
330
|
+
}
|
|
331
|
+
// ── Normal mode: WebSocket ─────────────────────────────────────
|
|
332
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
333
|
+
const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
|
|
334
|
+
const log = logger.withContext(sessionId, currentTaskId);
|
|
335
|
+
// Build artifact update with cardsInfo as data
|
|
336
|
+
const artifact = {
|
|
337
|
+
taskId: currentTaskId,
|
|
338
|
+
kind: "artifact-update",
|
|
339
|
+
append: false,
|
|
340
|
+
lastChunk: true,
|
|
341
|
+
final: params.final ?? false,
|
|
342
|
+
artifact: {
|
|
343
|
+
artifactId: uuidv4(),
|
|
344
|
+
parts: [
|
|
345
|
+
{
|
|
346
|
+
kind: "data",
|
|
347
|
+
data: {
|
|
348
|
+
cardsInfo: params.cardsInfo,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
// Build JSON-RPC response
|
|
355
|
+
const jsonRpcResponse = {
|
|
356
|
+
jsonrpc: "2.0",
|
|
357
|
+
id: currentMessageId,
|
|
358
|
+
result: artifact,
|
|
359
|
+
};
|
|
360
|
+
// Send via WebSocket
|
|
361
|
+
const wsManager = getXYWebSocketManager(config);
|
|
362
|
+
const outboundMessage = {
|
|
363
|
+
msgType: "agent_response",
|
|
364
|
+
agentId: config.agentId,
|
|
365
|
+
sessionId,
|
|
366
|
+
taskId: currentTaskId,
|
|
367
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
368
|
+
};
|
|
369
|
+
log.log(`[A2A_CARD] Sending card`);
|
|
370
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
371
|
+
log.log(`[A2A_CARD] Card sent successfully`);
|
|
372
|
+
}
|
|
275
373
|
/**
|
|
276
374
|
* Send a clearContext response.
|
|
277
375
|
*/
|
package/dist/src/monitor.js
CHANGED
|
@@ -116,35 +116,45 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
// 🔑 核心改造:检测steer模式
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
// clearContext / tasks/cancel 没有 params,跳过 parseA2AMessage 直接入队
|
|
120
|
+
const messageMethod = message.method;
|
|
121
|
+
if (messageMethod === "clearContext" || messageMethod === "clear_context"
|
|
122
|
+
|| messageMethod === "tasks/cancel" || messageMethod === "tasks_cancel") {
|
|
123
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
124
|
+
logger.error(`XY gateway: queue processing failed: ${String(err)}`);
|
|
125
|
+
activeMessages.delete(messageKey);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = parseA2AMessage(message);
|
|
131
|
+
const steerMode = cfg.messages?.queue?.mode === "steer";
|
|
132
|
+
const hasActiveRun = hasActiveTask(parsed.sessionId);
|
|
133
|
+
if (steerMode && hasActiveRun) {
|
|
134
|
+
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
135
|
+
logger.log(`[MONITOR-HANDLER] STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
136
|
+
void task().catch((err) => {
|
|
137
|
+
logger.error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
138
|
+
activeMessages.delete(messageKey);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// 正常模式:入队列串行执行
|
|
143
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
144
|
+
logger.error(`XY gateway: queue processing failed: ${String(err)}`);
|
|
145
|
+
activeMessages.delete(messageKey);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
131
148
|
}
|
|
132
|
-
|
|
133
|
-
//
|
|
149
|
+
catch (parseErr) {
|
|
150
|
+
// 解析失败,回退到正常队列模式
|
|
151
|
+
logger.error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
|
|
134
152
|
void enqueue(sessionId, task).catch((err) => {
|
|
135
153
|
logger.error(`XY gateway: queue processing failed: ${String(err)}`);
|
|
136
154
|
activeMessages.delete(messageKey);
|
|
137
155
|
});
|
|
138
156
|
}
|
|
139
157
|
}
|
|
140
|
-
catch (parseErr) {
|
|
141
|
-
// 解析失败,回退到正常队列模式
|
|
142
|
-
logger.error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
|
|
143
|
-
void enqueue(sessionId, task).catch((err) => {
|
|
144
|
-
logger.error(`XY gateway: queue processing failed: ${String(err)}`);
|
|
145
|
-
activeMessages.delete(messageKey);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
158
|
};
|
|
149
159
|
const connectedHandler = (serverId) => {
|
|
150
160
|
if (!loggedServers.has(serverId)) {
|
|
@@ -175,7 +185,9 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
175
185
|
};
|
|
176
186
|
const selfEvolutionHandler = (context) => {
|
|
177
187
|
logger.log(`[MONITOR] Received self-evolution-event, dispatching to handler...`);
|
|
178
|
-
handleSelfEvolutionEvent(context,
|
|
188
|
+
handleSelfEvolutionEvent(context, cfg).catch((err) => {
|
|
189
|
+
logger.error(`[MONITOR] Failed to handle self-evolution-event:`, err);
|
|
190
|
+
});
|
|
179
191
|
};
|
|
180
192
|
const selfEvolutionStateGetHandler = (context) => {
|
|
181
193
|
logger.log(`[MONITOR] Received self-evolution-state-get-event, dispatching to handler...`);
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
|
50
50
|
* (same level as push_id).
|
|
51
51
|
*/
|
|
52
52
|
export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Extract modelName from message parts.
|
|
55
|
+
* Looks for modelName in data parts under variables.clientVariables.modelName
|
|
56
|
+
* (same level as systemVariables).
|
|
57
|
+
*/
|
|
58
|
+
export declare function extractModelName(parts: A2AMessagePart[]): string | null;
|
|
53
59
|
/**
|
|
54
60
|
* Extract Trigger event data from message parts.
|
|
55
61
|
* Looks for Trigger events with pushDataId in data parts.
|
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);
|
|
@@ -143,6 +137,22 @@ export function extractDeviceType(parts) {
|
|
|
143
137
|
}
|
|
144
138
|
return null;
|
|
145
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract modelName from message parts.
|
|
142
|
+
* Looks for modelName in data parts under variables.clientVariables.modelName
|
|
143
|
+
* (same level as systemVariables).
|
|
144
|
+
*/
|
|
145
|
+
export function extractModelName(parts) {
|
|
146
|
+
for (const part of parts) {
|
|
147
|
+
if (part.kind === "data" && part.data) {
|
|
148
|
+
const modelName = part.data.variables?.clientVariables?.modelName;
|
|
149
|
+
if (modelName && typeof modelName === "string" && modelName.trim() !== "" && modelName.toLowerCase() !== "none") {
|
|
150
|
+
return modelName;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
146
156
|
/**
|
|
147
157
|
* Extract Trigger event data from message parts.
|
|
148
158
|
* Looks for Trigger events with pushDataId in data parts.
|
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 ──────────────────────────────────────────────
|
|
@@ -417,6 +417,32 @@ export const xiaoyiProvider = {
|
|
|
417
417
|
docsPath: "/providers/models",
|
|
418
418
|
auth: [],
|
|
419
419
|
isCacheTtlEligible: () => true,
|
|
420
|
+
/**
|
|
421
|
+
* Dynamic model resolution for A2A-specified model names.
|
|
422
|
+
* A2A messages carry a dynamic modelName that isn't in any static catalog.
|
|
423
|
+
* This hook lets OpenClaw's resolveModelAsync accept any model ID under
|
|
424
|
+
* xiaoyiprovider as long as the provider has a configured baseUrl.
|
|
425
|
+
*/
|
|
426
|
+
resolveDynamicModel: (ctx) => {
|
|
427
|
+
const baseUrl = ctx.providerConfig?.baseUrl;
|
|
428
|
+
if (!baseUrl || typeof baseUrl !== "string")
|
|
429
|
+
return null;
|
|
430
|
+
return {
|
|
431
|
+
id: ctx.modelId,
|
|
432
|
+
name: ctx.modelId,
|
|
433
|
+
api: ctx.providerConfig?.api ?? "openai-completions",
|
|
434
|
+
provider: "xiaoyiprovider",
|
|
435
|
+
baseUrl,
|
|
436
|
+
reasoning: false,
|
|
437
|
+
input: ["text"],
|
|
438
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
439
|
+
contextWindow: 256_000,
|
|
440
|
+
maxTokens: 8192,
|
|
441
|
+
...(ctx.providerConfig?.headers && typeof ctx.providerConfig.headers === "object"
|
|
442
|
+
? { headers: ctx.providerConfig.headers }
|
|
443
|
+
: {}),
|
|
444
|
+
};
|
|
445
|
+
},
|
|
420
446
|
/**
|
|
421
447
|
* Store uid-based fallback prefix for lazy timestamp generation in wrapStreamFn.
|
|
422
448
|
* Session-level headers (traceId / sessionId / interactionId) are resolved
|
|
@@ -480,6 +506,14 @@ export const xiaoyiProvider = {
|
|
|
480
506
|
// 3. UID-based fallback: sha256(uid).hex[:32]_timestamp
|
|
481
507
|
const isCron = isCronTriggered(context.messages);
|
|
482
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
|
+
}
|
|
483
517
|
const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
|
|
484
518
|
if (typeof fallbackPrefix === "string") {
|
|
485
519
|
const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
|
|
@@ -587,6 +621,12 @@ export const xiaoyiProvider = {
|
|
|
587
621
|
}
|
|
588
622
|
}
|
|
589
623
|
}
|
|
624
|
+
// ── Override model.id if A2A message specified modelName ──
|
|
625
|
+
const modelNameOverride = getCurrentSessionContext()?.modelName;
|
|
626
|
+
if (modelNameOverride && modelNameOverride.trim() !== "" && modelNameOverride.toLowerCase() !== "none") {
|
|
627
|
+
logger.log(`[xiaoyiprovider] overriding model.id: ${model.id} → ${modelNameOverride}`);
|
|
628
|
+
model = { ...model, id: modelNameOverride };
|
|
629
|
+
}
|
|
590
630
|
// ── Retry-capable streaming ──────────────────────────────
|
|
591
631
|
const cronJob = isCronTriggered(context.messages);
|
|
592
632
|
if (cronJob)
|
|
@@ -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发送完成状态
|
|
@@ -263,7 +278,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
263
278
|
taskId: currentTaskId,
|
|
264
279
|
messageId: currentMessageId,
|
|
265
280
|
text: "",
|
|
266
|
-
append:
|
|
281
|
+
append: true,
|
|
267
282
|
final: true,
|
|
268
283
|
});
|
|
269
284
|
finalSent = true;
|
|
@@ -304,7 +319,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
304
319
|
taskId: currentTaskId,
|
|
305
320
|
messageId: currentMessageId,
|
|
306
321
|
text: "任务执行异常,请重试~",
|
|
307
|
-
append:
|
|
322
|
+
append: true,
|
|
308
323
|
final: true,
|
|
309
324
|
errorCode: 99921111,
|
|
310
325
|
errorMessage: "任务执行异常,请重试",
|
|
@@ -396,10 +411,14 @@ export function createXYReplyDispatcher(params) {
|
|
|
396
411
|
}
|
|
397
412
|
const currentTaskId = getActiveTaskId();
|
|
398
413
|
const currentMessageId = getActiveMessageId();
|
|
399
|
-
|
|
414
|
+
let text = payload.text ?? "";
|
|
415
|
+
// Strip "Reasoning:" prefix that some reasoning models add to their thinking output
|
|
416
|
+
const lines = text.split(/\r?\n/);
|
|
417
|
+
if (lines[0]?.trim() === "Reasoning:") {
|
|
418
|
+
text = lines.slice(1).join("\n").trim();
|
|
419
|
+
}
|
|
400
420
|
try {
|
|
401
421
|
if (text.length > 0) {
|
|
402
|
-
// 🔑 将模型真实的thinking/reasoning内容通过reasoningText转发
|
|
403
422
|
await sendReasoningTextUpdate({
|
|
404
423
|
config,
|
|
405
424
|
sessionId,
|
|
@@ -426,7 +445,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
426
445
|
if (text.length > 0) {
|
|
427
446
|
accumulatedText += text;
|
|
428
447
|
hasSentResponse = true;
|
|
429
|
-
// 🔑 流式文本通过 A2A text 通道发送(而非 reasoningText)
|
|
430
448
|
await sendA2AResponse({
|
|
431
449
|
config,
|
|
432
450
|
sessionId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { XYWebSocketManager } from "./websocket.js";
|
|
2
|
-
export declare function handleSelfEvolutionEvent(context: any,
|
|
2
|
+
export declare function handleSelfEvolutionEvent(context: any, cfg: any): Promise<void>;
|
|
3
3
|
/**
|
|
4
4
|
* 读取 .xiaoyiruntime 中的 selfEvolutionState 并直接通过 wsManager 下发指令回复设备
|
|
5
5
|
* 参考trigger实现:直接使用当前已连接的 wsManager 发送消息,避免 getXYWebSocketManager 返回未连接实例
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { resolveXYConfig } from "./config.js";
|
|
4
|
+
import { sendA2AResponse } from "./formatter.js";
|
|
3
5
|
import { logger } from "./utils/logger.js";
|
|
4
6
|
const XIAOYIRUNTIME_PATH = "/home/sandbox/.openclaw/.xiaoyiruntime";
|
|
5
|
-
export function handleSelfEvolutionEvent(context,
|
|
7
|
+
export async function handleSelfEvolutionEvent(context, cfg) {
|
|
6
8
|
try {
|
|
7
9
|
const state = context.event?.payload?.selfEvolutionState;
|
|
8
10
|
if (typeof state !== "string") {
|
|
9
11
|
logger.error("[SELF_EVOLUTION] invalid payload: missing selfEvolutionState");
|
|
10
12
|
return;
|
|
11
13
|
}
|
|
14
|
+
const sessionId = context.sessionId ?? "";
|
|
15
|
+
const taskId = context.taskId ?? sessionId;
|
|
16
|
+
const messageId = context.messageId ?? uuidv4();
|
|
17
|
+
const config = resolveXYConfig(cfg);
|
|
12
18
|
logger.log(`[SELF_EVOLUTION] received state: ${state}`);
|
|
13
19
|
let content;
|
|
14
20
|
try {
|
|
@@ -19,6 +25,8 @@ export function handleSelfEvolutionEvent(context, runtime) {
|
|
|
19
25
|
logger.log(`[SELF_EVOLUTION] ${XIAOYIRUNTIME_PATH} not found, creating new file`);
|
|
20
26
|
writeFileSync(XIAOYIRUNTIME_PATH, `selfEvolutionState=${state}\n`, "utf-8");
|
|
21
27
|
logger.log(`[SELF_EVOLUTION] wrote selfEvolutionState=${state}`);
|
|
28
|
+
await sendA2AResponse({ config, sessionId, taskId, messageId, text: "", append: false, final: true });
|
|
29
|
+
logger.log(`[SELF_EVOLUTION] Sent final response (empty, stream end)`);
|
|
22
30
|
return;
|
|
23
31
|
}
|
|
24
32
|
const lines = content.split("\n");
|
|
@@ -40,6 +48,9 @@ export function handleSelfEvolutionEvent(context, runtime) {
|
|
|
40
48
|
writeFileSync(XIAOYIRUNTIME_PATH, updated.join("\n"), "utf-8");
|
|
41
49
|
}
|
|
42
50
|
logger.log(`[SELF_EVOLUTION] updated selfEvolutionState=${state} in ${XIAOYIRUNTIME_PATH}`);
|
|
51
|
+
// Reply with final empty response to acknowledge the state update
|
|
52
|
+
await sendA2AResponse({ config, sessionId, taskId, messageId, text: "", append: false, final: true });
|
|
53
|
+
logger.log(`[SELF_EVOLUTION] Sent final response (empty, stream end)`);
|
|
43
54
|
}
|
|
44
55
|
catch (err) {
|
|
45
56
|
logger.error("[SELF_EVOLUTION] failed to handle event:", err);
|