@ynhcj/xiaoyi-channel 0.0.227-next → 0.0.228-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 +28 -162
- package/dist/src/bot.js +28 -36
- package/dist/src/channel.js +2 -8
- package/dist/src/config-schema.d.ts +5 -0
- package/dist/src/config-schema.js +5 -0
- package/dist/src/config.js +1 -0
- package/dist/src/cron-command.js +24 -2
- package/dist/src/cspl/sentinel_hook.js +11 -8
- package/dist/src/cspl/utils.js +8 -8
- package/dist/src/formatter.d.ts +0 -7
- package/dist/src/formatter.js +94 -71
- package/dist/src/monitor.js +0 -17
- package/dist/src/outbound.js +5 -7
- package/dist/src/provider.js +214 -52
- package/dist/src/reply-dispatcher.js +163 -63
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/task-manager.d.ts +9 -3
- package/dist/src/task-manager.js +48 -16
- package/dist/src/tools/agent-as-skill-tool.js +1 -1
- package/dist/src/tools/calendar-tool.js +1 -1
- package/dist/src/tools/call-device-tool.js +13 -24
- package/dist/src/tools/call-phone-tool.js +1 -1
- package/dist/src/tools/check-plugin-privilege-tool.js +1 -1
- package/dist/src/tools/create-alarm-tool.js +1 -1
- package/dist/src/tools/delete-alarm-tool.js +1 -1
- package/dist/src/tools/discover-cross-devices-tool.js +1 -1
- package/dist/src/tools/hmos-cli.js +2 -2
- package/dist/src/tools/invoke.js +7 -3
- package/dist/src/tools/location-tool.js +1 -1
- package/dist/src/tools/login-token-tool.js +1 -1
- package/dist/src/tools/modify-alarm-tool.js +1 -1
- package/dist/src/tools/modify-note-tool.js +1 -1
- package/dist/src/tools/note-tool.js +1 -1
- package/dist/src/tools/query-app-message-tool.js +1 -1
- package/dist/src/tools/query-memory-data-tool.js +1 -1
- package/dist/src/tools/query-todo-task-tool.js +1 -1
- package/dist/src/tools/save-file-to-phone-tool.js +1 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +1 -1
- package/dist/src/tools/search-calendar-tool.js +1 -1
- package/dist/src/tools/search-contact-tool.js +1 -1
- package/dist/src/tools/search-email-tool.js +1 -1
- package/dist/src/tools/search-file-tool.js +1 -1
- package/dist/src/tools/search-message-tool.js +1 -1
- package/dist/src/tools/search-note-tool.js +1 -1
- package/dist/src/tools/search-photo-gallery-tool.js +1 -1
- package/dist/src/tools/send-cross-device-task-tool.js +1 -1
- package/dist/src/tools/send-email-tool.js +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +1 -1
- package/dist/src/tools/send-message-tool.js +1 -1
- package/dist/src/tools/session-manager.js +1 -1
- package/dist/src/tools/upload-file-tool.js +1 -1
- package/dist/src/tools/upload-photo-tool.js +1 -1
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
- package/dist/src/tools/xiaoyi-gui-tool.js +3 -3
- package/dist/src/trigger-handler.js +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/src/websocket.js +10 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,8 +15,6 @@ import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
|
15
15
|
import { registerCLIHook } from "./src/tools/hmos-cli.js";
|
|
16
16
|
import { recoverCronState } from "./src/cron-recovery.js";
|
|
17
17
|
import { writeSkillUsage } from "./src/utils/skills-logger.js";
|
|
18
|
-
import { markSubagentSpawned, markSubagentEnded, deliverSubagentFinalResult, } from "./src/conversation/conversation-manager.js";
|
|
19
|
-
import { notifyCronAgentEnd } from "./src/conversation/cron-buffer.js";
|
|
20
18
|
import { logger } from "./src/utils/logger.js";
|
|
21
19
|
/**
|
|
22
20
|
* Parse a file path string to detect if it refers to a SKILL.md file within
|
|
@@ -45,23 +43,37 @@ function extractSkillNameFromPath(filePath) {
|
|
|
45
43
|
* the `after_tool_call` hook and write the skill name to the skills log.
|
|
46
44
|
*/
|
|
47
45
|
function registerSkillsDiagnosticHook(api) {
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
message: "
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
// Skill name → tool names mapping for direct tool-based skill usage logging
|
|
47
|
+
const SKILL_TOOLS = {
|
|
48
|
+
GetCurrentLocation: ["get_user_location"],
|
|
49
|
+
Schedule: ["create_calendar_event", "search_calendar_event"],
|
|
50
|
+
memorandum: ["create_note", "search_notes", "modify_note"],
|
|
51
|
+
gallery: ["search_photo_gallery", "save_media_to_gallery"],
|
|
52
|
+
contact: ["search_contact"],
|
|
53
|
+
file: ["search_file", "upload_file", "save_file_to_file_manager"],
|
|
54
|
+
clock: ["create_alarm", "delete_alarm", "search_alarm", "modify_alarm"],
|
|
55
|
+
message: ["search_message", "send_message"],
|
|
56
|
+
phone: ["call_phone"],
|
|
57
|
+
"xiaoyi-collection": ["add_collection", "query_collection", "delete_collection"],
|
|
58
|
+
"xiaoyi-image-understanding": ["image_reading"],
|
|
61
59
|
};
|
|
60
|
+
// Inverted to tool name → skill name for lookup
|
|
61
|
+
const TOOL_SKILL_MAP = Object.fromEntries(Object.entries(SKILL_TOOLS).flatMap(([skill, tools]) => tools.map((t) => [t, skill])));
|
|
62
|
+
// Resolve the actual tool name from call_device_tool wrapper.
|
|
63
|
+
// The model calls call_device_tool({ toolName: "...", arguments: {...} })
|
|
64
|
+
// — the real tool name is inside params, not event.toolName.
|
|
65
|
+
function resolveActualToolName(event) {
|
|
66
|
+
if (event.toolName === "call_device_tool") {
|
|
67
|
+
const inner = event.params?.toolName;
|
|
68
|
+
if (typeof inner === "string" && inner.length > 0)
|
|
69
|
+
return inner;
|
|
70
|
+
}
|
|
71
|
+
return event.toolName;
|
|
72
|
+
}
|
|
62
73
|
// Log skill usage for known device tools on before_tool_call
|
|
63
74
|
api.on("before_tool_call", async (event, _ctx) => {
|
|
64
|
-
const
|
|
75
|
+
const actualToolName = resolveActualToolName(event);
|
|
76
|
+
const skillName = TOOL_SKILL_MAP[actualToolName];
|
|
65
77
|
if (skillName) {
|
|
66
78
|
writeSkillUsage(skillName);
|
|
67
79
|
}
|
|
@@ -306,153 +318,7 @@ function registerCronRecoveryHook(api) {
|
|
|
306
318
|
logger.log(`${logTag} ═══════════════════════════════════════════`);
|
|
307
319
|
});
|
|
308
320
|
}
|
|
309
|
-
/**
|
|
310
|
-
* 从 sessions_spawn 工具结果中提取 ACP child sessionKey。
|
|
311
|
-
* 兼容三种 result 形态:{status, childSessionKey} 直返、JSON 字符串、
|
|
312
|
-
* {content:[{text: "<json>"}], details:{...}} 工具信封。
|
|
313
|
-
* 仅接受 status="accepted" 且 key 形如 agent:<id>:acp:<uuid> 的结果。
|
|
314
|
-
*/
|
|
315
|
-
function extractAcceptedAcpChildSessionKey(result) {
|
|
316
|
-
const pick = (obj) => {
|
|
317
|
-
if (!obj || typeof obj !== "object")
|
|
318
|
-
return null;
|
|
319
|
-
const record = obj;
|
|
320
|
-
if (record.status !== "accepted")
|
|
321
|
-
return null;
|
|
322
|
-
const key = record.childSessionKey;
|
|
323
|
-
return typeof key === "string" && key.includes(":acp:") ? key : null;
|
|
324
|
-
};
|
|
325
|
-
const direct = pick(result);
|
|
326
|
-
if (direct)
|
|
327
|
-
return direct;
|
|
328
|
-
const envelope = result;
|
|
329
|
-
const fromDetails = pick(envelope?.details);
|
|
330
|
-
if (fromDetails)
|
|
331
|
-
return fromDetails;
|
|
332
|
-
if (Array.isArray(envelope?.content)) {
|
|
333
|
-
for (const item of envelope.content) {
|
|
334
|
-
if (typeof item?.text !== "string")
|
|
335
|
-
continue;
|
|
336
|
-
try {
|
|
337
|
-
const fromText = pick(JSON.parse(item.text));
|
|
338
|
-
if (fromText)
|
|
339
|
-
return fromText;
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
// 非 JSON 文本,忽略
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
if (typeof result === "string") {
|
|
347
|
-
try {
|
|
348
|
-
return pick(JSON.parse(result));
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
function registerSubagentHooks(api) {
|
|
357
|
-
// subagent_spawned: fires after a subagent run is successfully registered.
|
|
358
|
-
// We increment the expected completion count so that onIdle knows to wait.
|
|
359
|
-
api.on("subagent_spawned", async (_event, ctx) => {
|
|
360
|
-
const requesterSessionKey = ctx?.requesterSessionKey;
|
|
361
|
-
if (!requesterSessionKey)
|
|
362
|
-
return;
|
|
363
|
-
const count = markSubagentSpawned(requesterSessionKey);
|
|
364
|
-
if (count > 0) {
|
|
365
|
-
logger.log(`[XY-SUBAGENT] spawned, requesterSessionKey=${requesterSessionKey.slice(0, 30)}, expected=${count}`);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
// ACP SPAWN TRACKING: openclaw 的 subagent_spawned hook 仅由内嵌 subagent
|
|
369
|
-
// 路径(subagent-spawn.ts)发射;acp-spawn.ts(runtime="acp",如 Claude Code)
|
|
370
|
-
// 不发射 —— 实测 ACP run 仅在结束时经 run registry 触发 subagent_ended,
|
|
371
|
-
// 导致等待态从未建立、父 turn onIdle 直接发 final 结束对话。
|
|
372
|
-
// 因此在 after_tool_call 补记:sessions_spawn 成功且 childSessionKey 为 ACP
|
|
373
|
-
// 形态时递增等待计数。内嵌 subagent 的 childKey 形如 agent:<id>:subagent:<id>,
|
|
374
|
-
// 不命中此分支,仍由 subagent_spawned hook 负责,不会重复计数。
|
|
375
|
-
api.on("after_tool_call", async (event, ctx) => {
|
|
376
|
-
try {
|
|
377
|
-
if (event?.toolName !== "sessions_spawn" || event?.error)
|
|
378
|
-
return;
|
|
379
|
-
const childKey = extractAcceptedAcpChildSessionKey(event?.result);
|
|
380
|
-
if (!childKey)
|
|
381
|
-
return;
|
|
382
|
-
const sessionKey = ctx?.sessionKey;
|
|
383
|
-
if (!sessionKey)
|
|
384
|
-
return;
|
|
385
|
-
const count = markSubagentSpawned(sessionKey);
|
|
386
|
-
if (count > 0) {
|
|
387
|
-
logger.log(`[XY-SUBAGENT] ACP spawned, childSessionKey=${childKey.slice(0, 40)}, expected=${count}`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
catch (err) {
|
|
391
|
-
logger.error(`[XY-SUBAGENT] Error tracking ACP spawn:`, err);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
// subagent_ended: fires when a subagent run terminates (complete/error/killed).
|
|
395
|
-
// This is the PRIMARY delivery tracking mechanism. When all expected
|
|
396
|
-
// subagents have ended and parent has settled, we finalize the A2A session.
|
|
397
|
-
api.on("subagent_ended", async (event, ctx) => {
|
|
398
|
-
try {
|
|
399
|
-
const requesterSessionKey = ctx?.requesterSessionKey;
|
|
400
|
-
if (!requesterSessionKey) {
|
|
401
|
-
logger.log(`[XY-SUBAGENT-END] no requesterSessionKey in ctx`);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const transition = markSubagentEnded(requesterSessionKey);
|
|
405
|
-
logger.log(`[XY-SUBAGENT-END] ended, targetSessionKey=${event?.targetSessionKey?.slice(0, 30)}, outcome=${event?.outcome}, complete=${transition?.isComplete ?? false}, shouldFinalize=${transition?.shouldFinalize ?? false}, transition=${!!transition}`);
|
|
406
|
-
if (transition?.shouldFinalize) {
|
|
407
|
-
logger.log(`[XY-SUBAGENT-END] Starting finalization...`);
|
|
408
|
-
// Grace: announce 投递的 sendText 与 subagent_ended hook 并发到达
|
|
409
|
-
// (实测 sendText 可能先于 hook 约 1s,也可能晚于 hook;长文本还会
|
|
410
|
-
// 被 textChunkLimit 分片成多次 sendText)。捕获窗口直到
|
|
411
|
-
// finalDeliveryStarted 才关闭,此处的等待直接扩大迟到来片的入窗机会。
|
|
412
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
413
|
-
// 最终交付由对话管理层统一负责(含 config 解析、final 帧、清理)
|
|
414
|
-
await deliverSubagentFinalResult({
|
|
415
|
-
state: transition.state,
|
|
416
|
-
reason: "all-subagents-ended-after-parent-settled",
|
|
417
|
-
});
|
|
418
|
-
logger.log(`[XY-SUBAGENT-END] Finalized A2A session after all subagents ended`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
catch (err) {
|
|
422
|
-
logger.error(`[XY-SUBAGENT-END] Error in subagent_ended hook:`, err);
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
321
|
function registerFullHooks(api) {
|
|
427
|
-
// SUBAGENT HOOKS: track subagent spawn/end lifecycle for session keep-alive
|
|
428
|
-
registerSubagentHooks(api);
|
|
429
|
-
// CRON AGGREGATION: 通过 agent 事件总线感知 cron isolated run 结束。
|
|
430
|
-
// 注意:agent_end 插件 hook 只在 dispatch-from-config 路径触发,cron
|
|
431
|
-
// isolated run 不会触发它;embedded run 的 lifecycle 事件才是可靠信号。
|
|
432
|
-
// phase=end/error 后到达的 sendText 即为 announce 最终结果(放行)。
|
|
433
|
-
api.agent.events.registerAgentEventSubscription({
|
|
434
|
-
id: "xy-cron-turn-tracker",
|
|
435
|
-
description: "Track cron isolated run completion for sendText aggregation",
|
|
436
|
-
streams: ["lifecycle"],
|
|
437
|
-
handle: (event) => {
|
|
438
|
-
try {
|
|
439
|
-
const phase = event?.data?.phase;
|
|
440
|
-
// 仅 phase=end 是 run 级终态信号;phase=error 可能是单次模型调用
|
|
441
|
-
// 失败(后续还有重试/start),不能据此放行 announce。
|
|
442
|
-
// 真正失败的 run(无 end)由 120s 安全超时兜底 flush 缓冲文本。
|
|
443
|
-
if (phase !== "end")
|
|
444
|
-
return;
|
|
445
|
-
const sessionKey = event?.sessionKey ?? "";
|
|
446
|
-
if (sessionKey.startsWith("cron:") || sessionKey.includes(":cron:") || isCronActive()) {
|
|
447
|
-
logger.log(`[XY-CRON] Cron run lifecycle end, sessionKey=${sessionKey || "unknown"}`);
|
|
448
|
-
notifyCronAgentEnd();
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
catch (err) {
|
|
452
|
-
logger.error(`[XY-CRON] Error in cron lifecycle subscription:`, err);
|
|
453
|
-
}
|
|
454
|
-
},
|
|
455
|
-
});
|
|
456
322
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
457
323
|
const pluginConfig = api.pluginConfig || {};
|
|
458
324
|
const skillRetrieverConfig = normalizeToolRetrieverConfig({
|
package/dist/src/bot.js
CHANGED
|
@@ -242,20 +242,18 @@ export async function handleXYMessage(params) {
|
|
|
242
242
|
log.error(`[BOT] Failed to patch session model override:`, patchErr);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
-
// 🔑
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
258
|
-
}
|
|
245
|
+
// 🔑 发送初始状态更新
|
|
246
|
+
log.log(`[BOT] Sending initial status update`);
|
|
247
|
+
void sendStatusUpdate({
|
|
248
|
+
config,
|
|
249
|
+
sessionId: parsed.sessionId,
|
|
250
|
+
taskId: parsed.taskId,
|
|
251
|
+
messageId: parsed.messageId,
|
|
252
|
+
text: "任务正在处理中,请稍候~",
|
|
253
|
+
state: "working",
|
|
254
|
+
}).catch((err) => {
|
|
255
|
+
log.error(`Failed to send initial status update:`, err);
|
|
256
|
+
});
|
|
259
257
|
}
|
|
260
258
|
// Extract text and files from parts
|
|
261
259
|
const text = extractTextFromParts(parsed.parts);
|
|
@@ -281,10 +279,8 @@ export async function handleXYMessage(params) {
|
|
|
281
279
|
}
|
|
282
280
|
}
|
|
283
281
|
// ── Build message and dispatch via auto-reply pipeline ──────────
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// does not need its own steer detection or polling logic.
|
|
287
|
-
// File download
|
|
282
|
+
// 首条消息和 steer 回退都走此统一 dispatch 路径。
|
|
283
|
+
// File download — for both first message and steer fallthrough
|
|
288
284
|
let mediaPayload = {};
|
|
289
285
|
if (!skipReg || isUpdate) {
|
|
290
286
|
const fileParts = extractFileParts(parsed.parts);
|
|
@@ -305,7 +301,7 @@ export async function handleXYMessage(params) {
|
|
|
305
301
|
// per-session ReplyOperation lock in admitReplyTurn which would otherwise
|
|
306
302
|
// block the steer message until the first message completes — making steer
|
|
307
303
|
// indistinguishable from a followup.
|
|
308
|
-
if (isUpdate &&
|
|
304
|
+
if (isUpdate && route.sessionKey) {
|
|
309
305
|
const activeSessionId = resolveActiveEmbeddedRunSessionId(route.sessionKey);
|
|
310
306
|
if (activeSessionId) {
|
|
311
307
|
log.log(`[BOT-STEER] Direct steer attempt: activeSessionId=${activeSessionId}, textLen=${textForAgent.length}`);
|
|
@@ -314,8 +310,10 @@ export async function handleXYMessage(params) {
|
|
|
314
310
|
});
|
|
315
311
|
if (queued) {
|
|
316
312
|
log.log(`[BOT-STEER] Direct steer succeeded — message injected into active run`);
|
|
317
|
-
// Steer message taskId
|
|
318
|
-
|
|
313
|
+
// Steer message taskId stays in the binding so getActiveTaskId()
|
|
314
|
+
// resolves to the latest taskId for onPartialReply replies.
|
|
315
|
+
// Cleanup happens via the original dispatcher's onIdleComplete →
|
|
316
|
+
// decrementTaskIdRef(sessionId) which removes the entire binding.
|
|
319
317
|
return;
|
|
320
318
|
}
|
|
321
319
|
log.log(`[BOT-STEER] Direct steer failed (queued=false), falling through to dispatchReplyFromConfig`);
|
|
@@ -327,10 +325,8 @@ export async function handleXYMessage(params) {
|
|
|
327
325
|
// Resolve envelope format options (following feishu pattern)
|
|
328
326
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
329
327
|
// Build message body with speaker prefix (following feishu pattern)
|
|
330
|
-
let messageBody = textForAgent;
|
|
331
|
-
// Add speaker prefix for clarity
|
|
332
328
|
const speaker = parsed.sessionId;
|
|
333
|
-
messageBody = `${speaker}: ${
|
|
329
|
+
const messageBody = `${speaker}: ${textForAgent}`;
|
|
334
330
|
// Format agent envelope (following feishu pattern)
|
|
335
331
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
336
332
|
channel: "xiaoyi-channel",
|
|
@@ -430,7 +426,7 @@ export async function handleXYMessage(params) {
|
|
|
430
426
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
431
427
|
await core.channel.reply.withReplyDispatcher({
|
|
432
428
|
dispatcher,
|
|
433
|
-
onSettled: () => {
|
|
429
|
+
onSettled: async () => {
|
|
434
430
|
log.log(`[BOT] onSettled, steered=${steerState.steered}`);
|
|
435
431
|
// 🔑 When steered, skip cleanup — the first message's dispatcher is still running
|
|
436
432
|
if (steerState.steered) {
|
|
@@ -448,9 +444,9 @@ export async function handleXYMessage(params) {
|
|
|
448
444
|
// signal init complete to release the global dispatch gate
|
|
449
445
|
// for the next session.
|
|
450
446
|
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
451
|
-
const isSteerDispatch = isUpdate
|
|
447
|
+
const isSteerDispatch = isUpdate;
|
|
452
448
|
log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=${isSteerDispatch}`);
|
|
453
|
-
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}
|
|
449
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
|
|
454
450
|
try {
|
|
455
451
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
456
452
|
ctx: ctxPayload,
|
|
@@ -458,13 +454,7 @@ export async function handleXYMessage(params) {
|
|
|
458
454
|
dispatcher,
|
|
459
455
|
replyOptions,
|
|
460
456
|
});
|
|
461
|
-
|
|
462
|
-
if (result === undefined) {
|
|
463
|
-
log.log(`[BOT-STEER] dispatchReplyFromConfig returned undefined — steer injected via fast path OK, sessionKey=${route.sessionKey}`);
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, resultType=${typeof result}, isSteer=${isSteerDispatch}, steered=${steerState.steered}`);
|
|
467
|
-
}
|
|
457
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, result=${JSON.stringify(result)}`);
|
|
468
458
|
return result;
|
|
469
459
|
}
|
|
470
460
|
catch (dispatchErr) {
|
|
@@ -515,19 +505,21 @@ export async function handleXYMessage(params) {
|
|
|
515
505
|
function buildXYMediaPayload(mediaList) {
|
|
516
506
|
const first = mediaList[0];
|
|
517
507
|
const mediaPaths = mediaList.map((media) => media.path);
|
|
508
|
+
const mediaUrls = mediaList.map((media) => media.uri);
|
|
518
509
|
const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
|
|
519
510
|
return {
|
|
520
511
|
MediaPath: first?.path,
|
|
521
512
|
MediaType: first?.mimeType,
|
|
513
|
+
MediaUrl: first?.uri,
|
|
522
514
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
515
|
+
MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
|
|
523
516
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
524
517
|
};
|
|
525
518
|
}
|
|
526
519
|
// ─────────────────────────────────────────────────────────────
|
|
527
520
|
// Dispatcher updaters (cross-chain taskId bridging)
|
|
528
521
|
// ─────────────────────────────────────────────────────────────
|
|
529
|
-
// Use globalThis to survive module deduplication
|
|
530
|
-
// different copy of bot.ts, so a plain module-level Map would be two objects.
|
|
522
|
+
// Use globalThis to survive module deduplication
|
|
531
523
|
const _g = globalThis;
|
|
532
524
|
if (!_g.__xyDispatcherUpdaters)
|
|
533
525
|
_g.__xyDispatcherUpdaters = new Map();
|
package/dist/src/channel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./config.js";
|
|
2
2
|
import { xyConfigSchema } from "./config-schema.js";
|
|
3
|
-
import { xyOutbound } from "./
|
|
3
|
+
import { xyOutbound } from "./outbound.js";
|
|
4
4
|
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
5
5
|
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
6
6
|
import { logger } from "./utils/logger.js";
|
|
@@ -23,16 +23,11 @@ import { getAlarmToolSchemaTool } from "./tools/get-alarm-tool-schema.js";
|
|
|
23
23
|
import { getCollectionToolSchemaTool } from "./tools/get-collection-tool-schema.js";
|
|
24
24
|
import { loginTokenTool } from "./tools/login-token-tool.js";
|
|
25
25
|
import { agentAsSkillTool } from "./tools/agent-as-skill-tool.js";
|
|
26
|
-
import { discoverCrossDevicesTool } from "./tools/discover-cross-devices-tool.js";
|
|
27
|
-
import { sendCrossDeviceTaskTool } from "./tools/send-cross-device-task-tool.js";
|
|
28
26
|
import { displayA2UICardByPathTool } from "./tools/display-a2ui-card-bypath-tool.js";
|
|
29
|
-
import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
|
|
30
27
|
import { invokeTool } from "./tools/invoke.js";
|
|
31
28
|
import { xiaoyiAppendReferenceTool } from "./tools/xiaoyi-append-reference.js";
|
|
32
29
|
const ALL_TOOLS = [
|
|
33
30
|
locationTool,
|
|
34
|
-
discoverCrossDevicesTool,
|
|
35
|
-
sendCrossDeviceTaskTool,
|
|
36
31
|
displayA2UICardByPathTool,
|
|
37
32
|
callDeviceTool,
|
|
38
33
|
getNoteToolSchemaTool,
|
|
@@ -51,7 +46,6 @@ const ALL_TOOLS = [
|
|
|
51
46
|
saveSelfEvolutionSkillTool,
|
|
52
47
|
loginTokenTool,
|
|
53
48
|
agentAsSkillTool,
|
|
54
|
-
checkPluginPrivilegeTool,
|
|
55
49
|
invokeTool,
|
|
56
50
|
xiaoyiAppendReferenceTool,
|
|
57
51
|
];
|
|
@@ -158,7 +152,7 @@ export const xyPlugin = {
|
|
|
158
152
|
},
|
|
159
153
|
gateway: {
|
|
160
154
|
async startAccount(context) {
|
|
161
|
-
const { monitorXYProvider } = await import("./
|
|
155
|
+
const { monitorXYProvider } = await import("./monitor.js");
|
|
162
156
|
const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
|
|
163
157
|
const account = resolveXYConfig(context.cfg);
|
|
164
158
|
context.setStatus?.({
|
|
@@ -49,6 +49,11 @@ export declare const xyConfigSchema: {
|
|
|
49
49
|
readonly type: "string";
|
|
50
50
|
readonly description: "Default session ID for push notifications (used when no target is specified, e.g., in cron jobs)";
|
|
51
51
|
};
|
|
52
|
+
readonly terminalFrameDelayMs: {
|
|
53
|
+
readonly type: "number";
|
|
54
|
+
readonly description: "Delay in ms before sending terminal frames (completed status + final) so the content artifact clears downstream pipelines first; 0 disables";
|
|
55
|
+
readonly default: 300;
|
|
56
|
+
};
|
|
52
57
|
};
|
|
53
58
|
readonly required: readonly ["apiKey", "agentId", "uid", "apiId", "pushId"];
|
|
54
59
|
};
|
|
@@ -50,6 +50,11 @@ export const xyConfigSchema = {
|
|
|
50
50
|
type: "string",
|
|
51
51
|
description: "Default session ID for push notifications (used when no target is specified, e.g., in cron jobs)",
|
|
52
52
|
},
|
|
53
|
+
terminalFrameDelayMs: {
|
|
54
|
+
type: "number",
|
|
55
|
+
description: "Delay in ms before sending terminal frames (completed status + final) so the content artifact clears downstream pipelines first; 0 disables",
|
|
56
|
+
default: 300,
|
|
57
|
+
},
|
|
53
58
|
},
|
|
54
59
|
required: ["apiKey", "agentId", "uid", "apiId", "pushId"],
|
|
55
60
|
};
|
package/dist/src/config.js
CHANGED
|
@@ -27,6 +27,7 @@ export function resolveXYConfig(cfg) {
|
|
|
27
27
|
fileUploadUrl: xyConfig.fileUploadUrl ?? "http://localhost:8767",
|
|
28
28
|
pushUrl: xyConfig.pushUrl,
|
|
29
29
|
defaultSessionId: xyConfig.defaultSessionId,
|
|
30
|
+
terminalFrameDelayMs: xyConfig.terminalFrameDelayMs ?? 300,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
/**
|
package/dist/src/cron-command.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// independently of any session. The device processes the command and returns
|
|
6
6
|
// results through the normal WebSocket connection, so response listening
|
|
7
7
|
// works the same as for regular tool calls.
|
|
8
|
-
import {
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { XYPushService } from "./push.js";
|
|
10
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
9
11
|
import { logger } from "./utils/logger.js";
|
|
10
12
|
/**
|
|
11
13
|
* Send a tool command through the push channel (for cron-triggered tool calls).
|
|
@@ -22,8 +24,28 @@ export async function sendCommandViaPush(params) {
|
|
|
22
24
|
command.header?.name ??
|
|
23
25
|
"Command";
|
|
24
26
|
logger.log(`[CRON-CMD] Sending command via push, intent=${intentName}`);
|
|
27
|
+
// 1. 选 pushId:优先用调用方解析出的设备 pushId(多设备路由正确);
|
|
28
|
+
// 未提供时回退到 getAllPushIds()[0](单设备兼容旧行为)。
|
|
29
|
+
let pushId = config.pushId;
|
|
30
|
+
if (params.pushId) {
|
|
31
|
+
pushId = params.pushId;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
try {
|
|
35
|
+
const pushIdList = await getAllPushIds();
|
|
36
|
+
if (pushIdList.length > 0) {
|
|
37
|
+
pushId = pushIdList[0];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.error("[CRON-CMD] Failed to load pushIds:", error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 2. Build and send push notification with command in directives
|
|
45
|
+
const pushService = new XYPushService(config);
|
|
46
|
+
const sessionId = randomUUID();
|
|
25
47
|
try {
|
|
26
|
-
await
|
|
48
|
+
await pushService.sendPushWithDirectives(pushId, sessionId, [command]);
|
|
27
49
|
logger.log(`[CRON-CMD] Push sent successfully, intent=${intentName}`);
|
|
28
50
|
}
|
|
29
51
|
catch (error) {
|
|
@@ -7,11 +7,11 @@ import { callApi } from './call_api.js';
|
|
|
7
7
|
import { processText, extractResultText, validateAndTruncateText, parseSecurityResult, handleExecToolInput, handleMessageToolInput, handleOtherToolInput, buildToolOutputPayload, extractInterActionId, extractSessionId } from './utils.js';
|
|
8
8
|
import { ALLOWED_TOOLS, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, MAX_TEXT_LENGTH, STEER_ABORT_MESSAGE } from './constants.js';
|
|
9
9
|
import { getCurrentSessionContext } from '../tools/session-manager.js';
|
|
10
|
-
import {
|
|
10
|
+
import { tryInjectSteer } from './steer-context.js';
|
|
11
11
|
// 主入口模块
|
|
12
12
|
export default function register(api) {
|
|
13
13
|
api.on("before_tool_call", async (event, _ctx) => {
|
|
14
|
-
logger.log(`[SENTINEL HOOK] before_tool_call_event toolName: ${event.toolName}`);
|
|
14
|
+
logger.log(`[SENTINEL HOOK] toolCallId=${event.toolCallId}, before_tool_call_event toolName: ${event.toolName}`);
|
|
15
15
|
// 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
|
|
16
16
|
const sessionCtx = getCurrentSessionContext();
|
|
17
17
|
const sessionId = sessionCtx?.sessionId || event.runId || crypto.randomBytes(16).toString('hex');
|
|
@@ -46,7 +46,7 @@ export default function register(api) {
|
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
try {
|
|
49
|
-
logger.log(`[SENTINEL HOOK] after_tool_call_event toolName: ${event.toolName}`);
|
|
49
|
+
logger.log(`[SENTINEL HOOK] toolCallId=${event.toolCallId}, after_tool_call_event toolName: ${event.toolName}`);
|
|
50
50
|
// 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
|
|
51
51
|
const sessionCtx = getCurrentSessionContext();
|
|
52
52
|
const sessionId = sessionCtx?.sessionId || event.runId || crypto.randomBytes(16).toString('hex');
|
|
@@ -81,13 +81,16 @@ export default function register(api) {
|
|
|
81
81
|
try {
|
|
82
82
|
const response = await callApi(outputPayload, api, sessionId);
|
|
83
83
|
const result = parseSecurityResult(response);
|
|
84
|
-
logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
|
|
84
|
+
logger.log(`[SENTINEL HOOK] toolCallId=${event.toolCallId}, TOOL_OUTPUT response: status=${result.status}.`);
|
|
85
85
|
if (result.status === 'REJECT') {
|
|
86
86
|
logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
|
|
87
|
-
if (sessionCtx?.sessionId) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
if (sessionCtx?.sessionId && sessionCtx?.taskId) {
|
|
88
|
+
await tryInjectSteer({
|
|
89
|
+
sessionId: sessionCtx.sessionId,
|
|
90
|
+
taskId: sessionCtx.taskId,
|
|
91
|
+
message: STEER_ABORT_MESSAGE,
|
|
92
|
+
source: 'cspl',
|
|
93
|
+
});
|
|
91
94
|
}
|
|
92
95
|
else {
|
|
93
96
|
logger.warn(`[SENTINEL HOOK] Cannot inject steer: sessionKey=${ctx.sessionKey}, sessionCtx found=${!!sessionCtx}`);
|
package/dist/src/cspl/utils.js
CHANGED
|
@@ -270,10 +270,10 @@ export function buildToolOutputPayload(taskID, sessionID, funcName, content, too
|
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
272
|
// 发送新接口请求并处理响应,返回扫描结果(保留block/steer能力)
|
|
273
|
-
async function sendToolInputRequest(payload, api, sessionId) {
|
|
273
|
+
async function sendToolInputRequest(payload, api, sessionId, toolCallId) {
|
|
274
274
|
const response = await callApi(payload, api, sessionId);
|
|
275
275
|
const result = parseSecurityResult(response);
|
|
276
|
-
logger.log(`[SENTINEL HOOK] TOOL_INPUT response: status=${result.status}`);
|
|
276
|
+
logger.log(`[SENTINEL HOOK] toolCallId=${toolCallId}, TOOL_INPUT response: status=${result.status}`);
|
|
277
277
|
return result;
|
|
278
278
|
}
|
|
279
279
|
// 处理exec工具的TOOL_INPUT数据采集,返回扫描结果(保留block/steer能力)
|
|
@@ -309,7 +309,7 @@ export async function handleExecToolInput(event, api, sessionId, taskId) {
|
|
|
309
309
|
const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'exec', command, event.toolCallId, interActionID, { file: { type: 'doc', url: obsUrl, hash: fileHash, size: fileSize, body: bodyContent } });
|
|
310
310
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for file: ${path.basename(filePath)}, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
311
311
|
try {
|
|
312
|
-
lastResult = await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
312
|
+
lastResult = await sendToolInputRequest(toolInputPayload, api, sessionId, event.toolCallId);
|
|
313
313
|
if (lastResult.status === 'REJECT') {
|
|
314
314
|
return lastResult;
|
|
315
315
|
}
|
|
@@ -330,7 +330,7 @@ export async function handleExecToolInput(event, api, sessionId, taskId) {
|
|
|
330
330
|
logger.log('[SENTINEL HOOK] No code files found in command, treating as direct code execution');
|
|
331
331
|
const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'exec', command, event.toolCallId, interActionID);
|
|
332
332
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for direct code execution, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
333
|
-
return await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
333
|
+
return await sendToolInputRequest(toolInputPayload, api, sessionId, event.toolCallId);
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
// 处理message工具的TOOL_INPUT数据采集,返回扫描结果(保留block/steer能力)
|
|
@@ -344,7 +344,7 @@ export async function handleMessageToolInput(event, api, sessionId, taskId) {
|
|
|
344
344
|
const interActionID = extractInterActionId(taskId);
|
|
345
345
|
const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'message', message, event.toolCallId, interActionID);
|
|
346
346
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for message, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
347
|
-
return await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
347
|
+
return await sendToolInputRequest(toolInputPayload, api, sessionId, event.toolCallId);
|
|
348
348
|
}
|
|
349
349
|
// 计算项目目录的哈希值(遍历所有文件)
|
|
350
350
|
export function calculateProjectHash(sourcePath) {
|
|
@@ -456,10 +456,10 @@ export async function handleOtherToolInput(event, api, sessionId, taskId) {
|
|
|
456
456
|
return null;
|
|
457
457
|
}
|
|
458
458
|
logger.log(`[SENTINEL HOOK] Processing other tool input, toolName: ${event.toolName}`);
|
|
459
|
-
// 将 params 序列化为 JSON
|
|
460
|
-
const paramsJson = JSON.stringify(params);
|
|
459
|
+
// 将 params 序列化为 JSON 字符串,并限制长度
|
|
460
|
+
const paramsJson = JSON.stringify(params).substring(0, MAX_TEXT_LENGTH);
|
|
461
461
|
const interActionID = extractInterActionId(taskId);
|
|
462
462
|
const toolInputPayload = buildToolInputPayload(taskId, sessionId, event.toolName, paramsJson, event.toolCallId, interActionID);
|
|
463
463
|
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for ${event.toolName}, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
464
|
-
return await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
464
|
+
return await sendToolInputRequest(toolInputPayload, api, sessionId, event.toolCallId);
|
|
465
465
|
}
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -73,13 +73,6 @@ export interface SendCommandParams {
|
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* Send a command as an artifact update (final=false).
|
|
76
|
-
*
|
|
77
|
-
* Cron-aware: if the sessionId starts with the cron prefix ("cron-"),
|
|
78
|
-
* the command is delivered through the push channel instead of the
|
|
79
|
-
* WebSocket session, because cron-triggered tool calls have no active
|
|
80
|
-
* WebSocket session. The device receives the push, executes the command,
|
|
81
|
-
* and returns results through the normal WebSocket path — so response
|
|
82
|
-
* listening in the calling tool works unchanged.
|
|
83
76
|
*/
|
|
84
77
|
export declare function sendCommand(params: SendCommandParams): Promise<void>;
|
|
85
78
|
/**
|