@ynhcj/xiaoyi-channel 0.0.202-beta → 0.0.204-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/index.js +3 -3
  2. package/dist/src/bot.js +52 -34
  3. package/dist/src/channel.js +68 -64
  4. package/dist/src/client.d.ts +5 -0
  5. package/dist/src/client.js +10 -0
  6. package/dist/src/cspl/call_api.js +2 -0
  7. package/dist/src/cspl/sentinel_hook.js +9 -8
  8. package/dist/src/formatter.js +21 -31
  9. package/dist/src/monitor.js +20 -9
  10. package/dist/src/outbound.js +5 -0
  11. package/dist/src/provider.js +42 -13
  12. package/dist/src/reply-dispatcher.d.ts +3 -0
  13. package/dist/src/reply-dispatcher.js +146 -159
  14. package/dist/src/self-evolution-handler.js +2 -1
  15. package/dist/src/task-manager.d.ts +5 -6
  16. package/dist/src/task-manager.js +5 -9
  17. package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
  18. package/dist/src/tools/agent-as-skill-tool.js +144 -146
  19. package/dist/src/tools/calendar-tool.d.ts +24 -2
  20. package/dist/src/tools/calendar-tool.js +115 -117
  21. package/dist/src/tools/call-device-tool.d.ts +20 -6
  22. package/dist/src/tools/call-device-tool.js +116 -138
  23. package/dist/src/tools/call-phone-tool.d.ts +21 -2
  24. package/dist/src/tools/call-phone-tool.js +112 -114
  25. package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
  26. package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
  27. package/dist/src/tools/create-alarm-tool.d.ts +39 -2
  28. package/dist/src/tools/create-alarm-tool.js +229 -231
  29. package/dist/src/tools/create-all-tools.js +0 -2
  30. package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
  31. package/dist/src/tools/delete-alarm-tool.js +134 -136
  32. package/dist/src/tools/device-tool-map.js +3 -2
  33. package/dist/src/tools/discover-cross-devices-tool.d.ts +16 -2
  34. package/dist/src/tools/discover-cross-devices-tool.js +121 -124
  35. package/dist/src/tools/display-a2ui-card-bypath-tool.d.ts +21 -2
  36. package/dist/src/tools/display-a2ui-card-bypath-tool.js +53 -56
  37. package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
  38. package/dist/src/tools/get-alarm-tool-schema.js +10 -16
  39. package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
  40. package/dist/src/tools/get-calendar-tool-schema.js +8 -12
  41. package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
  42. package/dist/src/tools/get-collection-tool-schema.js +9 -11
  43. package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
  44. package/dist/src/tools/get-contact-tool-schema.js +10 -16
  45. package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
  46. package/dist/src/tools/get-device-file-tool-schema.js +9 -13
  47. package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
  48. package/dist/src/tools/get-email-tool-schema.js +8 -11
  49. package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
  50. package/dist/src/tools/get-note-tool-schema.js +9 -14
  51. package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
  52. package/dist/src/tools/get-photo-tool-schema.js +9 -12
  53. package/dist/src/tools/image-reading-tool.d.ts +28 -2
  54. package/dist/src/tools/image-reading-tool.js +76 -76
  55. package/dist/src/tools/location-tool.d.ts +11 -2
  56. package/dist/src/tools/location-tool.js +93 -92
  57. package/dist/src/tools/login-token-tool.d.ts +20 -2
  58. package/dist/src/tools/login-token-tool.js +123 -125
  59. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  60. package/dist/src/tools/modify-alarm-tool.js +250 -252
  61. package/dist/src/tools/modify-note-tool.d.ts +20 -2
  62. package/dist/src/tools/modify-note-tool.js +107 -109
  63. package/dist/src/tools/note-tool.d.ts +20 -2
  64. package/dist/src/tools/note-tool.js +106 -108
  65. package/dist/src/tools/query-app-message-tool.d.ts +28 -2
  66. package/dist/src/tools/query-app-message-tool.js +111 -113
  67. package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
  68. package/dist/src/tools/query-memory-data-tool.js +112 -114
  69. package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
  70. package/dist/src/tools/query-todo-task-tool.js +106 -108
  71. package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
  72. package/dist/src/tools/save-file-to-phone-tool.js +130 -132
  73. package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
  74. package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
  75. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
  76. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
  77. package/dist/src/tools/search-alarm-tool.d.ts +34 -2
  78. package/dist/src/tools/search-alarm-tool.js +174 -176
  79. package/dist/src/tools/search-calendar-tool.d.ts +24 -2
  80. package/dist/src/tools/search-calendar-tool.js +148 -150
  81. package/dist/src/tools/search-contact-tool.d.ts +16 -2
  82. package/dist/src/tools/search-contact-tool.js +101 -103
  83. package/dist/src/tools/search-email-tool.d.ts +21 -2
  84. package/dist/src/tools/search-email-tool.js +110 -112
  85. package/dist/src/tools/search-file-tool.d.ts +16 -2
  86. package/dist/src/tools/search-file-tool.js +104 -106
  87. package/dist/src/tools/search-message-tool.d.ts +16 -2
  88. package/dist/src/tools/search-message-tool.js +103 -105
  89. package/dist/src/tools/search-note-tool.d.ts +16 -2
  90. package/dist/src/tools/search-note-tool.js +98 -100
  91. package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
  92. package/dist/src/tools/search-photo-gallery-tool.js +35 -37
  93. package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
  94. package/dist/src/tools/send-cross-device-task-tool.js +140 -143
  95. package/dist/src/tools/send-email-tool.d.ts +24 -2
  96. package/dist/src/tools/send-email-tool.js +108 -110
  97. package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
  98. package/dist/src/tools/send-file-to-user-tool.js +174 -176
  99. package/dist/src/tools/send-html-card-tool.d.ts +20 -2
  100. package/dist/src/tools/send-html-card-tool.js +85 -87
  101. package/dist/src/tools/send-message-tool.d.ts +20 -2
  102. package/dist/src/tools/send-message-tool.js +122 -124
  103. package/dist/src/tools/session-manager.d.ts +7 -50
  104. package/dist/src/tools/session-manager.js +40 -231
  105. package/dist/src/tools/upload-file-tool.d.ts +20 -2
  106. package/dist/src/tools/upload-file-tool.js +80 -82
  107. package/dist/src/tools/upload-photo-tool.d.ts +20 -2
  108. package/dist/src/tools/upload-photo-tool.js +68 -70
  109. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
  110. package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
  111. package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
  112. package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
  113. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
  114. package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
  115. package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
  116. package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
  117. package/dist/src/utils/logger.js +3 -14
  118. package/dist/src/websocket.d.ts +1 -1
  119. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { xiaoyiProvider } from "./src/provider.js";
3
3
  import { xyPlugin } from "./src/channel.js";
4
4
  import registerSentinelHook from "./src/cspl/sentinel_hook.js";
5
5
  import { setXYRuntime } from "./src/runtime.js";
6
- import { markCronToolCall, clearCronToolCall, getSessionContext } from "./src/tools/session-manager.js";
6
+ import { markCronToolCall, clearCronToolCall, getCurrentSessionContext } from "./src/tools/session-manager.js";
7
7
  import { configManager } from "./src/utils/config-manager.js";
8
8
  import { setJobPushId } from "./src/utils/cron-push-map.js";
9
9
  import { getAllPushIds } from "./src/utils/pushid-manager.js";
@@ -54,10 +54,10 @@ async function captureCronAddMapping(event, ctx) {
54
54
  return;
55
55
  }
56
56
  console.log(`[CRONMAP] extracted jobId=${jobId}`);
57
- const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
57
+ const sessionCtx = getCurrentSessionContext();
58
58
  const sessionId = sessionCtx?.sessionId;
59
59
  if (!sessionId) {
60
- console.log(`[CRONMAP] skip: no sessionId (sessionKey=${ctx.sessionKey}, ctxFound=${!!sessionCtx})`);
60
+ console.log(`[CRONMAP] skip: no sessionId in ALS scope (ctxFound=${!!sessionCtx})`);
61
61
  return;
62
62
  }
63
63
  const pushId = await resolvePushId(sessionId);
package/dist/src/bot.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
1
2
  import { updateSessionStoreEntry, updateSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
2
3
  import { getXYRuntime } from "./runtime.js";
3
4
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
@@ -6,7 +7,7 @@ import { downloadFilesFromParts } from "./file-download.js";
6
7
  import { resolveXYConfig } from "./config.js";
7
8
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
8
9
  import { appendSelfEvolutionKeywordNudge, shouldNudgeForSelfEvolutionKeyword, } from "./self-evolution-keyword.js";
9
- import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
10
+ import { runWithSessionContext } from "./tools/session-manager.js";
10
11
  import { configManager } from "./utils/config-manager.js";
11
12
  import { addPushId } from "./utils/pushid-manager.js";
12
13
  import { getPushDataById } from "./utils/pushdata-manager.js";
@@ -113,6 +114,13 @@ export async function handleXYMessage(params) {
113
114
  // Steer injections skip taskId registration to avoid overwriting the active taskId
114
115
  if (!skipReg) {
115
116
  registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
117
+ // 🔑 steer 场景:同步更新活跃 dispatcher 的 fallback taskId/messageId
118
+ if (isUpdate) {
119
+ const updater = dispatcherUpdaters.get(parsed.sessionId);
120
+ if (updater) {
121
+ updater(parsed.taskId, parsed.messageId);
122
+ }
123
+ }
116
124
  // Extract and update push_id if present
117
125
  const pushId = extractPushId(parsed.parts);
118
126
  if (pushId) {
@@ -169,21 +177,23 @@ export async function handleXYMessage(params) {
169
177
  },
170
178
  });
171
179
  log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
172
- // Steer injections skip session registration to avoid refCount leaks
180
+ // Check for ACP runtime binding on this A2A conversation
181
+ const runtimeRoute = resolveRuntimeConversationBindingRoute({
182
+ route,
183
+ channel: "xiaoyi-channel",
184
+ accountId,
185
+ conversationId: parsed.sessionId,
186
+ });
187
+ route = runtimeRoute.route;
188
+ if (runtimeRoute.bindingRecord) {
189
+ log.log(runtimeRoute.boundSessionKey
190
+ ? `[BOT] routed via bound conversation ${parsed.sessionId} -> ${runtimeRoute.boundSessionKey}`
191
+ : `[BOT] plugin-bound conversation ${parsed.sessionId}`);
192
+ }
193
+ // ALS only: no registerSession. The sessionContext built below is handed
194
+ // to runWithSessionContext() inside withReplyDispatcher.run, which is the
195
+ // single wrap point for the whole agent turn.
173
196
  if (!skipReg) {
174
- registerSession(route.sessionKey, {
175
- config,
176
- sessionId: parsed.sessionId,
177
- distributionSessionId,
178
- taskId: parsed.taskId,
179
- messageId: parsed.messageId,
180
- agentId: route.accountId,
181
- deviceType,
182
- appVer: appVer ?? undefined,
183
- sdkApiVersion: sdkApiVersion ?? undefined,
184
- modelName,
185
- runCrossTaskContext: runCrossTaskContext ?? undefined,
186
- });
187
197
  // 🔑 Sync A2A modelName to OpenClaw session store so that session_status
188
198
  // reports the correct model. Without this, session_status returns the
189
199
  // configured default model instead of the A2A-specified one.
@@ -354,7 +364,18 @@ export async function handleXYMessage(params) {
354
364
  const steerState = { steered: false };
355
365
  // 🔑 创建dispatcher
356
366
  log.log(`[BOT-DISPATCHER] Creating reply dispatcher`);
357
- const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
367
+ // Cleanup: 必须在 onIdle 内部执行(参见 reply-dispatcher.ts onIdleComplete 的注释)
368
+ let cleaned = false;
369
+ const cleanup = () => {
370
+ if (cleaned)
371
+ return;
372
+ cleaned = true;
373
+ log.log(`[BOT] Cleanup started`);
374
+ streamingSignals.delete(parsed.sessionId);
375
+ decrementTaskIdRef(parsed.sessionId);
376
+ log.log(`[BOT] Cleanup completed`);
377
+ };
378
+ const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval, updateFallbackTaskId } = createXYReplyDispatcher({
358
379
  cfg,
359
380
  runtime,
360
381
  sessionId: parsed.sessionId,
@@ -362,7 +383,10 @@ export async function handleXYMessage(params) {
362
383
  messageId: parsed.messageId,
363
384
  accountId: route.accountId,
364
385
  steerState,
386
+ onIdleComplete: cleanup,
365
387
  });
388
+ // 🔑 注册 dispatcher 的 fallback taskId 更新函数,供 steer 路径调用
389
+ dispatcherUpdaters.set(parsed.sessionId, updateFallbackTaskId);
366
390
  // Steer injections don't need status intervals
367
391
  if (!skipReg) {
368
392
  startStatusInterval();
@@ -376,6 +400,8 @@ export async function handleXYMessage(params) {
376
400
  messageId: parsed.messageId,
377
401
  agentId: route.accountId,
378
402
  deviceType,
403
+ appVer: appVer ?? undefined,
404
+ sdkApiVersion: sdkApiVersion ?? undefined,
379
405
  modelName,
380
406
  runCrossTaskContext: runCrossTaskContext ?? undefined,
381
407
  };
@@ -384,15 +410,14 @@ export async function handleXYMessage(params) {
384
410
  dispatcher,
385
411
  onSettled: () => {
386
412
  log.log(`[BOT] onSettled, steered=${steerState.steered}`);
387
- // 🔑 When steered, skip heavy cleanup — the first message's dispatcher is still running
413
+ // 🔑 When steered, skip cleanup — the first message's dispatcher is still running
388
414
  if (steerState.steered) {
389
415
  log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
390
416
  return;
391
417
  }
392
- streamingSignals.delete(parsed.sessionId);
393
- decrementTaskIdRef(parsed.sessionId);
394
- unregisterSession(route.sessionKey);
395
- log.log(`[BOT] Cleanup completed`);
418
+ // cleanup 已由 onIdleComplete 在 onIdle 的 finally 中执行。
419
+ // onSettled 不做任何清理(直接在这里清理会发生 race condition)。
420
+ dispatcherUpdaters.delete(parsed.sessionId);
396
421
  },
397
422
  run: () => {
398
423
  // 🔐 Use AsyncLocalStorage to provide session context to tools.
@@ -401,6 +426,7 @@ export async function handleXYMessage(params) {
401
426
  // signal init complete to release the global dispatch gate
402
427
  // for the next session.
403
428
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
429
+ log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=false`);
404
430
  log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
405
431
  try {
406
432
  const result = await core.channel.reply.dispatchReplyFromConfig({
@@ -433,7 +459,7 @@ export async function handleXYMessage(params) {
433
459
  errLog.error("Failed to handle XY message:", err);
434
460
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
435
461
  errLog.log(`[BOT] Error occurred, attempting cleanup`);
436
- // 🔑 错误时也要清理taskIdsession
462
+ // 🔑 错误时也要清理taskIdsession 走 ALS,作用域退出自动清理)
437
463
  try {
438
464
  const params = message.params;
439
465
  const sessionId = params?.sessionId;
@@ -441,18 +467,6 @@ export async function handleXYMessage(params) {
441
467
  errLog.log(`[BOT] Cleaning up after error`);
442
468
  // 清理 taskId
443
469
  decrementTaskIdRef(sessionId);
444
- // 清理 session
445
- const core = getXYRuntime();
446
- const route = core.channel.routing.resolveAgentRoute({
447
- cfg,
448
- channel: "xiaoyi-channel",
449
- accountId,
450
- peer: {
451
- kind: "direct",
452
- id: sessionId,
453
- },
454
- });
455
- unregisterSession(route.sessionKey);
456
470
  errLog.log(`[BOT] Cleanup completed after error`);
457
471
  }
458
472
  }
@@ -487,8 +501,11 @@ if (!_g.__xyStreamingSignals)
487
501
  _g.__xyStreamingSignals = new Map();
488
502
  if (!_g.__xySteerQueues)
489
503
  _g.__xySteerQueues = new Map();
504
+ if (!_g.__xyDispatcherUpdaters)
505
+ _g.__xyDispatcherUpdaters = new Map();
490
506
  const streamingSignals = _g.__xyStreamingSignals;
491
507
  const steerQueues = _g.__xySteerQueues;
508
+ const dispatcherUpdaters = _g.__xyDispatcherUpdaters;
492
509
  /**
493
510
  * 由 provider.ts 在 wrapStreamFn 调用时触发。
494
511
  * 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
@@ -637,6 +654,7 @@ async function dispatchSteerWhenReady(params) {
637
654
  },
638
655
  run: () => {
639
656
  return runWithSessionContext(sessionContext, async () => {
657
+ log.log(`[ALS-PROOF] bot entered steer dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=true`);
640
658
  const result = await core.channel.reply.dispatchReplyFromConfig({
641
659
  ctx: ctxPayload,
642
660
  cfg: params.cfg,
@@ -2,15 +2,55 @@ import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./conf
2
2
  import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
4
  import { filterToolsByDevice } from "./tools/device-tool-map.js";
5
- import { getCurrentSessionContext, registerSession } from "./tools/session-manager.js";
6
- import { createAllTools } from "./tools/create-all-tools.js";
5
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
7
6
  import { logger } from "./utils/logger.js";
8
- /**
9
- * Prefix used for synthetic sessionIds created during cron-triggered tool
10
- * execution. `sendCommand()` checks this prefix to route commands through
11
- * the push channel instead of the (non-existent) WebSocket session.
12
- */
13
- const CRON_SESSION_PREFIX = "cron-";
7
+ // Static tool imports (3.24 pattern)
8
+ import { locationTool } from "./tools/location-tool.js";
9
+ import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
10
+ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
11
+ import { sendHtmlCardTool } from "./tools/send-html-card-tool.js";
12
+ import { viewPushResultTool } from "./tools/view-push-result-tool.js";
13
+ import { imageReadingTool } from "./tools/image-reading-tool.js";
14
+ import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
15
+ import { saveSelfEvolutionSkillTool } from "./tools/save-self-evolution-skill-tool.js";
16
+ import { callDeviceTool } from "./tools/call-device-tool.js";
17
+ import { getNoteToolSchemaTool } from "./tools/get-note-tool-schema.js";
18
+ import { getCalendarToolSchemaTool } from "./tools/get-calendar-tool-schema.js";
19
+ import { getContactToolSchemaTool } from "./tools/get-contact-tool-schema.js";
20
+ import { getPhotoToolSchemaTool } from "./tools/get-photo-tool-schema.js";
21
+ import { getDeviceFileToolSchemaTool } from "./tools/get-device-file-tool-schema.js";
22
+ import { getAlarmToolSchemaTool } from "./tools/get-alarm-tool-schema.js";
23
+ import { getCollectionToolSchemaTool } from "./tools/get-collection-tool-schema.js";
24
+ import { loginTokenTool } from "./tools/login-token-tool.js";
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
+ import { displayA2UICardByPathTool } from "./tools/display-a2ui-card-bypath-tool.js";
29
+ import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
30
+ const ALL_TOOLS = [
31
+ locationTool,
32
+ discoverCrossDevicesTool,
33
+ sendCrossDeviceTaskTool,
34
+ displayA2UICardByPathTool,
35
+ callDeviceTool,
36
+ getNoteToolSchemaTool,
37
+ getCalendarToolSchemaTool,
38
+ getContactToolSchemaTool,
39
+ getPhotoToolSchemaTool,
40
+ xiaoyiGuiTool,
41
+ getDeviceFileToolSchemaTool,
42
+ getAlarmToolSchemaTool,
43
+ getCollectionToolSchemaTool,
44
+ sendFileToUserTool,
45
+ sendHtmlCardTool,
46
+ viewPushResultTool,
47
+ imageReadingTool,
48
+ timestampToUtc8Tool,
49
+ saveSelfEvolutionSkillTool,
50
+ loginTokenTool,
51
+ agentAsSkillTool,
52
+ checkPluginPrivilegeTool,
53
+ ];
14
54
  /**
15
55
  * Xiaoyi Channel Plugin for OpenClaw.
16
56
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -32,7 +72,7 @@ export const xyPlugin = {
32
72
  ],
33
73
  },
34
74
  capabilities: {
35
- chatTypes: ["direct"], // Only private chat (no group support)
75
+ chatTypes: ["direct"],
36
76
  polls: false,
37
77
  threads: false,
38
78
  media: true,
@@ -49,59 +89,11 @@ export const xyPlugin = {
49
89
  schema: xyConfigSchema,
50
90
  },
51
91
  outbound: xyOutbound,
52
- /**
53
- * Provide channel-specific agent tools.
54
- *
55
- * Two execution contexts are supported:
56
- *
57
- * 1. **Normal (WebSocket) session** – `getCurrentSessionContext()` returns
58
- * a context that was registered by bot.ts during message processing.
59
- * Tools send commands through the WebSocket and listen for responses.
60
- *
61
- * 2. **Cron / scheduled-task session** – openclaw's cron runner calls
62
- * `agentTools({ cfg })` without an active WebSocket session. When no
63
- * session context exists but `cfg` is provided, we create a synthetic
64
- * "cron session" with `isCron: true` and a `cron-`-prefixed sessionId.
65
- * `sendCommand()` detects this prefix and routes commands through the
66
- * push channel. Response listening (WebSocket events) works unchanged
67
- * because the gateway WebSocket connection is always active.
68
- */
69
- agentTools: (params) => {
70
- let ctx = getCurrentSessionContext();
71
- // ── Cron / non-session fallback ──────────────────────────────
72
- // When no active xy WebSocket session exists but the openclaw cfg
73
- // is provided (framework calls agentTools({ cfg })), create a
74
- // synthetic "cron session". This enables cron-triggered agent
75
- // turns and cross-channel tool calls to use xiaoyi tools via the
76
- // push channel. sendCommand() detects the "cron-" sessionId
77
- // prefix and routes commands through push instead of WebSocket.
78
- if (!ctx && params?.cfg) {
79
- try {
80
- const config = resolveXYConfig(params.cfg);
81
- const cronId = `${CRON_SESSION_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
82
- ctx = {
83
- config,
84
- sessionId: cronId,
85
- taskId: cronId,
86
- messageId: cronId,
87
- agentId: "default",
88
- isCron: true,
89
- };
90
- // Register so getCurrentSessionContext() fallback can find it
91
- registerSession(`__cron__${cronId}`, ctx);
92
- logger.log(`[CRON-TOOLS] Created cron session context: ${cronId}`);
93
- }
94
- catch (err) {
95
- logger.error("[CRON-TOOLS] Failed to create cron context:", err);
96
- }
97
- }
98
- if (!ctx) {
99
- logger.log("[CREATE-ALL-TOOLS] no session context, returning empty tools list");
100
- return [];
101
- }
102
- const allTools = createAllTools(ctx);
103
- const filtered = filterToolsByDevice(allTools, ctx.deviceType);
104
- logger.log(`[DEVICE-FILTER] deviceType=${ctx.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
92
+ /** Static tool list (3.24 pattern). Tools read SessionContext at execute time via ALS. */
93
+ agentTools: () => {
94
+ const ctx = getCurrentSessionContext();
95
+ const filtered = filterToolsByDevice(ALL_TOOLS, ctx?.deviceType);
96
+ logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${ALL_TOOLS.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
105
97
  return filtered;
106
98
  },
107
99
  messaging: {
@@ -113,7 +105,6 @@ export const xyPlugin = {
113
105
  },
114
106
  targetResolver: {
115
107
  looksLikeId: (raw) => {
116
- // 信任所有非空字符串作为有效的 sessionId
117
108
  const trimmed = raw.trim();
118
109
  return trimmed.length > 0;
119
110
  },
@@ -135,20 +126,33 @@ export const xyPlugin = {
135
126
  ? { conversationId, matchPriority: 2 }
136
127
  : null;
137
128
  },
129
+ resolveCommandConversation: ({ accountId }) => {
130
+ // xiaoyi-channel: the A2A sessionId IS the conversationId.
131
+ // Read from the current ALS session context.
132
+ const ctx = getCurrentSessionContext();
133
+ const sessionId = ctx?.sessionId?.trim();
134
+ if (!sessionId)
135
+ return null;
136
+ return { conversationId: sessionId };
137
+ },
138
138
  },
139
139
  reload: {
140
140
  configPrefixes: ["channels.xiaoyi-channel"],
141
141
  },
142
- // Gateway adapter for receiving messages
143
142
  gateway: {
144
143
  async startAccount(context) {
145
144
  const { monitorXYProvider } = await import("./monitor.js");
145
+ const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
146
146
  const account = resolveXYConfig(context.cfg);
147
147
  context.setStatus?.({
148
148
  accountId: context.accountId,
149
149
  wsUrl: account.wsUrl,
150
150
  });
151
151
  context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
152
+ // Register ACP session binding adapter for this account.
153
+ // Enables sessions_spawn(runtime="acp") to bind subagent sessions
154
+ // to the current A2A conversation.
155
+ createXyAcpBindingManager({ accountId: context.accountId, cfg: context.cfg });
152
156
  return monitorXYProvider({
153
157
  config: context.cfg,
154
158
  runtime: context.runtime,
@@ -1,6 +1,11 @@
1
1
  import { XYWebSocketManager } from "./websocket.js";
2
2
  import type { XYChannelConfig } from "./types.js";
3
3
  import type { RuntimeEnv } from "openclaw/plugin-sdk";
4
+ /**
5
+ * Get a cached WebSocket manager without requiring config.
6
+ * Returns the first available manager. Use when ALS has no SessionContext.
7
+ */
8
+ export declare function getCachedXYWebSocketManager(): XYWebSocketManager;
4
9
  /**
5
10
  * Get or create a WebSocket manager for the given configuration.
6
11
  * Reuses existing managers if config matches.
@@ -13,6 +13,16 @@ if (!_g.__xyWsManagerCache) {
13
13
  _g.__xyWsManagerCache = new Map();
14
14
  }
15
15
  const wsManagerCache = _g.__xyWsManagerCache;
16
+ /**
17
+ * Get a cached WebSocket manager without requiring config.
18
+ * Returns the first available manager. Use when ALS has no SessionContext.
19
+ */
20
+ export function getCachedXYWebSocketManager() {
21
+ if (wsManagerCache.size === 0) {
22
+ throw new Error("No WebSocket manager available in cache");
23
+ }
24
+ return wsManagerCache.values().next().value;
25
+ }
16
26
  /**
17
27
  * Get or create a WebSocket manager for the given configuration.
18
28
  * Reuses existing managers if config matches.
@@ -5,6 +5,7 @@ import https from 'https';
5
5
  import { URL } from 'url';
6
6
  import { getConfig } from './config.js';
7
7
  import { DEFAULT_HTTPS_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
8
+ import { logger } from '../utils/logger.js';
8
9
  function buildHeadersForCelia(config, sessionId) {
9
10
  if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
10
11
  throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
@@ -89,6 +90,7 @@ export async function callApi(questionText, api, sessionId, action) {
89
90
  };
90
91
  const httpBody = JSON.stringify(payload);
91
92
  const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
93
+ logger.log(`[SENTINEL HOOK] callApi: action=${action}, x-hag-trace-id=${sessionId}, url=${apiUrl}`);
92
94
  return new Promise((resolve, reject) => {
93
95
  const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
94
96
  const req = https.request(options, (res) => {
@@ -6,15 +6,16 @@ import { callApi } from './call_api.js';
6
6
  import { processText, extractResultText, validateAndTruncateText, parseSecurityResult, handleExecToolInput, handleMessageToolInput, handleOtherToolInput } from './utils.js';
7
7
  import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, TOOL_OUTPUT_ACTION } from './constants.js';
8
8
  import { logger } from '../utils/logger.js';
9
- import { getSessionContext } from '../tools/session-manager.js';
9
+ import { getCurrentSessionContext } from '../tools/session-manager.js';
10
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
14
  logger.log(`[SENTINEL HOOK] before_tool_call_event toolName: ${event.toolName}`);
15
- // 生成sessionID
16
- const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
17
- logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
15
+ // 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
16
+ const sessionCtx = getCurrentSessionContext();
17
+ const sessionId = sessionCtx?.sessionId || (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
18
+ logger.log(`[SENTINEL HOOK] Session ID: ${sessionId} (fromALS: ${!!sessionCtx?.sessionId})`);
18
19
  // 处理 TOOL_INPUT 数据采集、发送数据,根据扫描结果决定是否阻塞
19
20
  try {
20
21
  let scanResult = null;
@@ -43,9 +44,10 @@ export default function register(api) {
43
44
  }
44
45
  try {
45
46
  logger.log(`[SENTINEL HOOK] after_tool_call_event toolName: ${event.toolName}`);
46
- // 生成sessionID
47
- const sessionId = (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
48
- logger.log(`[SENTINEL HOOK] Generated Session ID: ${sessionId}`);
47
+ // 获取真实sessionID:优先使用ALS中的A2A sessionId,降级到OpenClaw runId或随机值
48
+ const sessionCtx = getCurrentSessionContext();
49
+ const sessionId = sessionCtx?.sessionId || (event.runId?.replace(/-/g, '') || crypto.randomBytes(16).toString('hex'));
50
+ logger.log(`[SENTINEL HOOK] Session ID: ${sessionId} (fromALS: ${!!sessionCtx?.sessionId})`);
49
51
  // 处理TOOL_OUTPUT数据采集(保持现有逻辑)
50
52
  const resultText = extractResultText(event, event.toolName);
51
53
  const resultTextLength = resultText.length;
@@ -78,7 +80,6 @@ export default function register(api) {
78
80
  logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
79
81
  if (result.status === 'REJECT') {
80
82
  logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
81
- const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
82
83
  if (sessionCtx?.sessionId && sessionCtx?.taskId) {
83
84
  await tryInjectSteer({
84
85
  sessionId: sessionCtx.sessionId,
@@ -1,8 +1,8 @@
1
1
  // OpenClaw → A2A format conversion
2
+ import os from "os";
2
3
  import { v4 as uuidv4 } from "uuid";
3
4
  import { getXYWebSocketManager } from "./client.js";
4
5
  import { logger } from "./utils/logger.js";
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
8
  import { isCronToolCall, getCurrentCronJobId } from "./tools/session-manager.js";
@@ -98,7 +98,7 @@ export async function sendA2AResponse(params) {
98
98
  agentId: config.agentId,
99
99
  sessionId,
100
100
  taskId,
101
- msgDetail: JSON.stringify(jsonRpcResponse),
101
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
102
102
  };
103
103
  if (shouldLog) {
104
104
  const redactedText = redactSensitiveText(bridgedText ?? "");
@@ -148,7 +148,7 @@ export async function sendReasoningTextUpdate(params) {
148
148
  agentId: config.agentId,
149
149
  sessionId,
150
150
  taskId,
151
- msgDetail: JSON.stringify(jsonRpcResponse),
151
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
152
152
  };
153
153
  await wsManager.sendMessage(sessionId, outboundMessage);
154
154
  }
@@ -158,11 +158,7 @@ export async function sendReasoningTextUpdate(params) {
158
158
  */
159
159
  export async function sendStatusUpdate(params) {
160
160
  const { config, sessionId, taskId, messageId, text, state } = params;
161
- // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
162
- // fall back to closure-captured values
163
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
164
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
165
- const log = logger.withContext(sessionId, currentTaskId);
161
+ const log = logger.withContext(sessionId, taskId);
166
162
  // 审批桥接和脱敏
167
163
  const bridgedText = rewriteOutboundApprovalText(sessionId, text);
168
164
  const redactedText = redactSensitiveText(bridgedText);
@@ -177,7 +173,7 @@ export async function sendStatusUpdate(params) {
177
173
  ],
178
174
  });
179
175
  const statusUpdate = {
180
- taskId: currentTaskId,
176
+ taskId,
181
177
  kind: "status-update",
182
178
  final: false, // Status updates should not end the stream
183
179
  status: {
@@ -188,7 +184,7 @@ export async function sendStatusUpdate(params) {
188
184
  // Build JSON-RPC response
189
185
  const jsonRpcResponse = {
190
186
  jsonrpc: "2.0",
191
- id: currentMessageId,
187
+ id: messageId,
192
188
  result: statusUpdate,
193
189
  };
194
190
  // Send via WebSocket
@@ -197,8 +193,8 @@ export async function sendStatusUpdate(params) {
197
193
  msgType: "agent_response",
198
194
  agentId: config.agentId,
199
195
  sessionId,
200
- taskId: currentTaskId,
201
- msgDetail: JSON.stringify(jsonRpcResponse),
196
+ taskId,
197
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
202
198
  };
203
199
  // Log complete response body
204
200
  log.log(`[A2A_STATUS] Sending status-update, text="${redactedText}"`);
@@ -270,15 +266,11 @@ export async function sendCommand(params) {
270
266
  return sendCommandViaPush({ config, command: commands[0], pushId });
271
267
  }
272
268
  // ── Normal mode: WebSocket ─────────────────────────────────────
273
- // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
274
- // fall back to closure-captured values
275
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
276
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
277
- const log = logger.withContext(sessionId, currentTaskId);
269
+ const log = logger.withContext(sessionId, taskId);
278
270
  // Build artifact update with command as data
279
271
  // Wrap command in commands array as per protocol requirement
280
272
  const artifact = {
281
- taskId: currentTaskId,
273
+ taskId,
282
274
  kind: "artifact-update",
283
275
  append: false,
284
276
  lastChunk: true,
@@ -300,7 +292,7 @@ export async function sendCommand(params) {
300
292
  // Build JSON-RPC response
301
293
  const jsonRpcResponse = {
302
294
  jsonrpc: "2.0",
303
- id: currentMessageId,
295
+ id: messageId,
304
296
  result: artifact,
305
297
  };
306
298
  // Send via WebSocket
@@ -309,8 +301,8 @@ export async function sendCommand(params) {
309
301
  msgType: "agent_response",
310
302
  agentId: config.agentId,
311
303
  sessionId,
312
- taskId: currentTaskId,
313
- msgDetail: JSON.stringify(jsonRpcResponse),
304
+ taskId,
305
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
314
306
  };
315
307
  // Log complete response body
316
308
  log.log(`[A2A_COMMAND] Sending command`);
@@ -329,12 +321,10 @@ export async function sendCard(params) {
329
321
  throw new Error("sendCard does not support cron mode");
330
322
  }
331
323
  // ── Normal mode: WebSocket ─────────────────────────────────────
332
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
333
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
334
- const log = logger.withContext(sessionId, currentTaskId);
324
+ const log = logger.withContext(sessionId, taskId);
335
325
  // Build artifact update with cardsInfo as data
336
326
  const artifact = {
337
- taskId: currentTaskId,
327
+ taskId,
338
328
  kind: "artifact-update",
339
329
  append: false,
340
330
  lastChunk: true,
@@ -354,7 +344,7 @@ export async function sendCard(params) {
354
344
  // Build JSON-RPC response
355
345
  const jsonRpcResponse = {
356
346
  jsonrpc: "2.0",
357
- id: currentMessageId,
347
+ id: messageId,
358
348
  result: artifact,
359
349
  };
360
350
  // Send via WebSocket
@@ -363,8 +353,8 @@ export async function sendCard(params) {
363
353
  msgType: "agent_response",
364
354
  agentId: config.agentId,
365
355
  sessionId,
366
- taskId: currentTaskId,
367
- msgDetail: JSON.stringify(jsonRpcResponse),
356
+ taskId,
357
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
368
358
  };
369
359
  log.log(`[A2A_CARD] Sending card`);
370
360
  await wsManager.sendMessage(sessionId, outboundMessage);
@@ -398,7 +388,7 @@ export async function sendClearContextResponse(params) {
398
388
  agentId: config.agentId,
399
389
  sessionId,
400
390
  taskId: sessionId, // Use sessionId as taskId for clearContext
401
- msgDetail: JSON.stringify(jsonRpcResponse),
391
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
402
392
  };
403
393
  await wsManager.sendMessage(sessionId, outboundMessage);
404
394
  log.log(`[CLEAR_CONTEXT] Sent clearContext response`);
@@ -432,7 +422,7 @@ export async function sendTasksCancelResponse(params) {
432
422
  agentId: config.agentId,
433
423
  sessionId,
434
424
  taskId,
435
- msgDetail: JSON.stringify(jsonRpcResponse),
425
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
436
426
  };
437
427
  await wsManager.sendMessage(sessionId, outboundMessage);
438
428
  log.log(`[TASKS_CANCEL] Sent tasks/cancel response`);
@@ -480,7 +470,7 @@ export async function sendTriggerResponse(params) {
480
470
  agentId: config.agentId,
481
471
  sessionId,
482
472
  taskId,
483
- msgDetail: JSON.stringify(jsonRpcResponse),
473
+ msgDetail: JSON.stringify({ ...jsonRpcResponse, hostname: os.hostname() }),
484
474
  };
485
475
  log.log(`[TRIGGER_RESPONSE] Sending Trigger response, text=${buildTextPreview(redactedContent)}`);
486
476
  await wsManager.sendMessage(sessionId, outboundMessage);