@ynhcj/xiaoyi-channel 0.0.172-next → 0.0.173-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.
Files changed (121) hide show
  1. package/dist/index.js +3 -3
  2. package/dist/src/bot.js +14 -34
  3. package/dist/src/channel.js +61 -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 +12 -23
  9. package/dist/src/memory-query-handler.js +2 -33
  10. package/dist/src/monitor.js +20 -9
  11. package/dist/src/parser.d.ts +3 -3
  12. package/dist/src/parser.js +6 -6
  13. package/dist/src/provider.js +40 -69
  14. package/dist/src/reply-dispatcher.js +29 -45
  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 +2 -0
  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 +1 -0
  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 -0
  36. package/dist/src/tools/display-a2ui-card-bypath-tool.js +65 -0
  37. package/dist/src/tools/display-a2ui-card-tool.d.ts +27 -2
  38. package/dist/src/tools/display-a2ui-card-tool.js +65 -68
  39. package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
  40. package/dist/src/tools/get-alarm-tool-schema.js +10 -16
  41. package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
  42. package/dist/src/tools/get-calendar-tool-schema.js +8 -12
  43. package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
  44. package/dist/src/tools/get-collection-tool-schema.js +9 -11
  45. package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
  46. package/dist/src/tools/get-contact-tool-schema.js +10 -16
  47. package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
  48. package/dist/src/tools/get-device-file-tool-schema.js +9 -13
  49. package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
  50. package/dist/src/tools/get-email-tool-schema.js +8 -11
  51. package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
  52. package/dist/src/tools/get-note-tool-schema.js +9 -14
  53. package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
  54. package/dist/src/tools/get-photo-tool-schema.js +9 -12
  55. package/dist/src/tools/image-reading-tool.d.ts +28 -2
  56. package/dist/src/tools/image-reading-tool.js +76 -76
  57. package/dist/src/tools/location-tool.d.ts +11 -2
  58. package/dist/src/tools/location-tool.js +93 -92
  59. package/dist/src/tools/login-token-tool.d.ts +20 -2
  60. package/dist/src/tools/login-token-tool.js +122 -125
  61. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  62. package/dist/src/tools/modify-alarm-tool.js +250 -252
  63. package/dist/src/tools/modify-note-tool.d.ts +20 -2
  64. package/dist/src/tools/modify-note-tool.js +107 -109
  65. package/dist/src/tools/note-tool.d.ts +20 -2
  66. package/dist/src/tools/note-tool.js +106 -108
  67. package/dist/src/tools/query-app-message-tool.d.ts +28 -2
  68. package/dist/src/tools/query-app-message-tool.js +111 -113
  69. package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
  70. package/dist/src/tools/query-memory-data-tool.js +112 -114
  71. package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
  72. package/dist/src/tools/query-todo-task-tool.js +106 -108
  73. package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
  74. package/dist/src/tools/save-file-to-phone-tool.js +130 -132
  75. package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
  76. package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
  77. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
  78. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
  79. package/dist/src/tools/search-alarm-tool.d.ts +34 -2
  80. package/dist/src/tools/search-alarm-tool.js +174 -176
  81. package/dist/src/tools/search-calendar-tool.d.ts +24 -2
  82. package/dist/src/tools/search-calendar-tool.js +148 -150
  83. package/dist/src/tools/search-contact-tool.d.ts +16 -2
  84. package/dist/src/tools/search-contact-tool.js +101 -103
  85. package/dist/src/tools/search-email-tool.d.ts +21 -2
  86. package/dist/src/tools/search-email-tool.js +110 -112
  87. package/dist/src/tools/search-file-tool.d.ts +16 -2
  88. package/dist/src/tools/search-file-tool.js +104 -106
  89. package/dist/src/tools/search-message-tool.d.ts +16 -2
  90. package/dist/src/tools/search-message-tool.js +103 -105
  91. package/dist/src/tools/search-note-tool.d.ts +16 -2
  92. package/dist/src/tools/search-note-tool.js +98 -100
  93. package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
  94. package/dist/src/tools/search-photo-gallery-tool.js +35 -37
  95. package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
  96. package/dist/src/tools/send-cross-device-task-tool.js +138 -143
  97. package/dist/src/tools/send-email-tool.d.ts +24 -2
  98. package/dist/src/tools/send-email-tool.js +108 -110
  99. package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
  100. package/dist/src/tools/send-file-to-user-tool.js +172 -176
  101. package/dist/src/tools/send-html-card-tool.d.ts +20 -2
  102. package/dist/src/tools/send-html-card-tool.js +85 -87
  103. package/dist/src/tools/send-message-tool.d.ts +20 -2
  104. package/dist/src/tools/send-message-tool.js +122 -124
  105. package/dist/src/tools/session-manager.d.ts +9 -52
  106. package/dist/src/tools/session-manager.js +40 -231
  107. package/dist/src/tools/upload-file-tool.d.ts +20 -2
  108. package/dist/src/tools/upload-file-tool.js +80 -82
  109. package/dist/src/tools/upload-photo-tool.d.ts +20 -2
  110. package/dist/src/tools/upload-photo-tool.js +68 -70
  111. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
  112. package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
  113. package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
  114. package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
  115. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
  116. package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
  117. package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
  118. package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
  119. package/dist/src/utils/logger.js +3 -14
  120. package/dist/src/websocket.d.ts +1 -1
  121. 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,12 +1,12 @@
1
1
  import { updateSessionStoreEntry, updateSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
2
2
  import { getXYRuntime } from "./runtime.js";
3
3
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
4
- import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractAppVer, extractDisplayVersion, extractModelName, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
4
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractAppVer, extractSdkApiVersion, extractModelName, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
5
5
  import { downloadFilesFromParts } from "./file-download.js";
6
6
  import { resolveXYConfig } from "./config.js";
7
7
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
8
8
  import { appendSelfEvolutionKeywordNudge, shouldNudgeForSelfEvolutionKeyword, } from "./self-evolution-keyword.js";
9
- import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
9
+ import { runWithSessionContext } from "./tools/session-manager.js";
10
10
  import { configManager } from "./utils/config-manager.js";
11
11
  import { addPushId } from "./utils/pushid-manager.js";
12
12
  import { getPushDataById } from "./utils/pushdata-manager.js";
@@ -139,14 +139,14 @@ export async function handleXYMessage(params) {
139
139
  if (deviceType) {
140
140
  log.log(`[BOT] Extracted deviceType: ${deviceType}`);
141
141
  }
142
- // Extract app_ver and display_version if present
142
+ // Extract app_ver and sdk_api_version if present
143
143
  const appVer = extractAppVer(parsed.parts);
144
144
  if (appVer) {
145
145
  log.log(`[BOT] Extracted app_ver: ${appVer}`);
146
146
  }
147
- const displayVersion = extractDisplayVersion(parsed.parts);
148
- if (displayVersion) {
149
- log.log(`[BOT] Extracted display_version: ${displayVersion}`);
147
+ const sdkApiVersion = extractSdkApiVersion(parsed.parts);
148
+ if (sdkApiVersion) {
149
+ log.log(`[BOT] Extracted sdk_api_version: ${sdkApiVersion}`);
150
150
  }
151
151
  // Extract modelName if present (used by provider.ts to override model.id)
152
152
  const modelName = extractModelName(parsed.parts);
@@ -169,21 +169,10 @@ export async function handleXYMessage(params) {
169
169
  },
170
170
  });
171
171
  log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
172
- // Steer injections skip session registration to avoid refCount leaks
172
+ // ALS only: no registerSession. The sessionContext built below is handed
173
+ // to runWithSessionContext() inside withReplyDispatcher.run, which is the
174
+ // single wrap point for the whole agent turn.
173
175
  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
- displayVersion: displayVersion ?? undefined,
184
- modelName,
185
- runCrossTaskContext: runCrossTaskContext ?? undefined,
186
- });
187
176
  // 🔑 Sync A2A modelName to OpenClaw session store so that session_status
188
177
  // reports the correct model. Without this, session_status returns the
189
178
  // configured default model instead of the A2A-specified one.
@@ -376,6 +365,8 @@ export async function handleXYMessage(params) {
376
365
  messageId: parsed.messageId,
377
366
  agentId: route.accountId,
378
367
  deviceType,
368
+ appVer: appVer ?? undefined,
369
+ sdkApiVersion: sdkApiVersion ?? undefined,
379
370
  modelName,
380
371
  runCrossTaskContext: runCrossTaskContext ?? undefined,
381
372
  };
@@ -391,7 +382,6 @@ export async function handleXYMessage(params) {
391
382
  }
392
383
  streamingSignals.delete(parsed.sessionId);
393
384
  decrementTaskIdRef(parsed.sessionId);
394
- unregisterSession(route.sessionKey);
395
385
  log.log(`[BOT] Cleanup completed`);
396
386
  },
397
387
  run: () => {
@@ -401,6 +391,7 @@ export async function handleXYMessage(params) {
401
391
  // signal init complete to release the global dispatch gate
402
392
  // for the next session.
403
393
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
394
+ log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=false`);
404
395
  log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
405
396
  try {
406
397
  const result = await core.channel.reply.dispatchReplyFromConfig({
@@ -433,7 +424,7 @@ export async function handleXYMessage(params) {
433
424
  errLog.error("Failed to handle XY message:", err);
434
425
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
435
426
  errLog.log(`[BOT] Error occurred, attempting cleanup`);
436
- // 🔑 错误时也要清理taskIdsession
427
+ // 🔑 错误时也要清理taskIdsession 走 ALS,作用域退出自动清理)
437
428
  try {
438
429
  const params = message.params;
439
430
  const sessionId = params?.sessionId;
@@ -441,18 +432,6 @@ export async function handleXYMessage(params) {
441
432
  errLog.log(`[BOT] Cleaning up after error`);
442
433
  // 清理 taskId
443
434
  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
435
  errLog.log(`[BOT] Cleanup completed after error`);
457
436
  }
458
437
  }
@@ -637,6 +616,7 @@ async function dispatchSteerWhenReady(params) {
637
616
  },
638
617
  run: () => {
639
618
  return runWithSessionContext(sessionContext, async () => {
619
+ log.log(`[ALS-PROOF] bot entered steer dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=true`);
640
620
  const result = await core.channel.reply.dispatchReplyFromConfig({
641
621
  ctx: ctxPayload,
642
622
  cfg: params.cfg,
@@ -2,15 +2,57 @@ 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 { displayA2UICardTool } from "./tools/display-a2ui-card-tool.js";
29
+ import { displayA2UICardByPathTool } from "./tools/display-a2ui-card-bypath-tool.js";
30
+ import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
31
+ const ALL_TOOLS = [
32
+ locationTool,
33
+ discoverCrossDevicesTool,
34
+ sendCrossDeviceTaskTool,
35
+ displayA2UICardTool,
36
+ displayA2UICardByPathTool,
37
+ callDeviceTool,
38
+ getNoteToolSchemaTool,
39
+ getCalendarToolSchemaTool,
40
+ getContactToolSchemaTool,
41
+ getPhotoToolSchemaTool,
42
+ xiaoyiGuiTool,
43
+ getDeviceFileToolSchemaTool,
44
+ getAlarmToolSchemaTool,
45
+ getCollectionToolSchemaTool,
46
+ sendFileToUserTool,
47
+ sendHtmlCardTool,
48
+ viewPushResultTool,
49
+ imageReadingTool,
50
+ timestampToUtc8Tool,
51
+ saveSelfEvolutionSkillTool,
52
+ loginTokenTool,
53
+ agentAsSkillTool,
54
+ checkPluginPrivilegeTool,
55
+ ];
14
56
  /**
15
57
  * Xiaoyi Channel Plugin for OpenClaw.
16
58
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -32,7 +74,7 @@ export const xyPlugin = {
32
74
  ],
33
75
  },
34
76
  capabilities: {
35
- chatTypes: ["direct"], // Only private chat (no group support)
77
+ chatTypes: ["direct"],
36
78
  polls: false,
37
79
  threads: false,
38
80
  media: true,
@@ -49,59 +91,11 @@ export const xyPlugin = {
49
91
  schema: xyConfigSchema,
50
92
  },
51
93
  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(", ")})`);
94
+ /** Static tool list (3.24 pattern). Tools read SessionContext at execute time via ALS. */
95
+ agentTools: () => {
96
+ const ctx = getCurrentSessionContext();
97
+ const filtered = filterToolsByDevice(ALL_TOOLS, ctx?.deviceType);
98
+ logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${ALL_TOOLS.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
105
99
  return filtered;
106
100
  },
107
101
  messaging: {
@@ -113,7 +107,6 @@ export const xyPlugin = {
113
107
  },
114
108
  targetResolver: {
115
109
  looksLikeId: (raw) => {
116
- // 信任所有非空字符串作为有效的 sessionId
117
110
  const trimmed = raw.trim();
118
111
  return trimmed.length > 0;
119
112
  },
@@ -139,16 +132,20 @@ export const xyPlugin = {
139
132
  reload: {
140
133
  configPrefixes: ["channels.xiaoyi-channel"],
141
134
  },
142
- // Gateway adapter for receiving messages
143
135
  gateway: {
144
136
  async startAccount(context) {
145
137
  const { monitorXYProvider } = await import("./monitor.js");
138
+ const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
146
139
  const account = resolveXYConfig(context.cfg);
147
140
  context.setStatus?.({
148
141
  accountId: context.accountId,
149
142
  wsUrl: account.wsUrl,
150
143
  });
151
144
  context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
145
+ // Register ACP session binding adapter for this account.
146
+ // Enables sessions_spawn(runtime="acp") to bind subagent sessions
147
+ // to the current A2A conversation.
148
+ createXyAcpBindingManager({ accountId: context.accountId, cfg: context.cfg });
152
149
  return monitorXYProvider({
153
150
  config: context.cfg,
154
151
  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,
@@ -2,7 +2,6 @@
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  import { getXYWebSocketManager } from "./client.js";
4
4
  import { logger } from "./utils/logger.js";
5
- import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
6
5
  import { redactSensitiveText, containsSensitiveInfo } from "./sensitive-redactor.js";
7
6
  import { rewriteOutboundApprovalText } from "./approval-bridge.js";
8
7
  import { isCronToolCall, getCurrentCronJobId } from "./tools/session-manager.js";
@@ -158,11 +157,7 @@ export async function sendReasoningTextUpdate(params) {
158
157
  */
159
158
  export async function sendStatusUpdate(params) {
160
159
  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);
160
+ const log = logger.withContext(sessionId, taskId);
166
161
  // 审批桥接和脱敏
167
162
  const bridgedText = rewriteOutboundApprovalText(sessionId, text);
168
163
  const redactedText = redactSensitiveText(bridgedText);
@@ -177,7 +172,7 @@ export async function sendStatusUpdate(params) {
177
172
  ],
178
173
  });
179
174
  const statusUpdate = {
180
- taskId: currentTaskId,
175
+ taskId,
181
176
  kind: "status-update",
182
177
  final: false, // Status updates should not end the stream
183
178
  status: {
@@ -188,7 +183,7 @@ export async function sendStatusUpdate(params) {
188
183
  // Build JSON-RPC response
189
184
  const jsonRpcResponse = {
190
185
  jsonrpc: "2.0",
191
- id: currentMessageId,
186
+ id: messageId,
192
187
  result: statusUpdate,
193
188
  };
194
189
  // Send via WebSocket
@@ -197,7 +192,7 @@ export async function sendStatusUpdate(params) {
197
192
  msgType: "agent_response",
198
193
  agentId: config.agentId,
199
194
  sessionId,
200
- taskId: currentTaskId,
195
+ taskId,
201
196
  msgDetail: JSON.stringify(jsonRpcResponse),
202
197
  };
203
198
  // Log complete response body
@@ -270,15 +265,11 @@ export async function sendCommand(params) {
270
265
  return sendCommandViaPush({ config, command: commands[0], pushId });
271
266
  }
272
267
  // ── 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);
268
+ const log = logger.withContext(sessionId, taskId);
278
269
  // Build artifact update with command as data
279
270
  // Wrap command in commands array as per protocol requirement
280
271
  const artifact = {
281
- taskId: currentTaskId,
272
+ taskId,
282
273
  kind: "artifact-update",
283
274
  append: false,
284
275
  lastChunk: true,
@@ -300,7 +291,7 @@ export async function sendCommand(params) {
300
291
  // Build JSON-RPC response
301
292
  const jsonRpcResponse = {
302
293
  jsonrpc: "2.0",
303
- id: currentMessageId,
294
+ id: messageId,
304
295
  result: artifact,
305
296
  };
306
297
  // Send via WebSocket
@@ -309,7 +300,7 @@ export async function sendCommand(params) {
309
300
  msgType: "agent_response",
310
301
  agentId: config.agentId,
311
302
  sessionId,
312
- taskId: currentTaskId,
303
+ taskId,
313
304
  msgDetail: JSON.stringify(jsonRpcResponse),
314
305
  };
315
306
  // Log complete response body
@@ -329,12 +320,10 @@ export async function sendCard(params) {
329
320
  throw new Error("sendCard does not support cron mode");
330
321
  }
331
322
  // ── Normal mode: WebSocket ─────────────────────────────────────
332
- const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
333
- const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
334
- const log = logger.withContext(sessionId, currentTaskId);
323
+ const log = logger.withContext(sessionId, taskId);
335
324
  // Build artifact update with cardsInfo as data
336
325
  const artifact = {
337
- taskId: currentTaskId,
326
+ taskId,
338
327
  kind: "artifact-update",
339
328
  append: false,
340
329
  lastChunk: true,
@@ -354,7 +343,7 @@ export async function sendCard(params) {
354
343
  // Build JSON-RPC response
355
344
  const jsonRpcResponse = {
356
345
  jsonrpc: "2.0",
357
- id: currentMessageId,
346
+ id: messageId,
358
347
  result: artifact,
359
348
  };
360
349
  // Send via WebSocket
@@ -363,7 +352,7 @@ export async function sendCard(params) {
363
352
  msgType: "agent_response",
364
353
  agentId: config.agentId,
365
354
  sessionId,
366
- taskId: currentTaskId,
355
+ taskId,
367
356
  msgDetail: JSON.stringify(jsonRpcResponse),
368
357
  };
369
358
  log.log(`[A2A_CARD] Sending card`);
@@ -42,9 +42,6 @@ export async function handleMemoryQueryEvent(context, cfg) {
42
42
  case "MemoryStateSet":
43
43
  result = handleMemoryStateSet(params);
44
44
  break;
45
- case "MemoryStateGet":
46
- result = handleMemoryStateGet();
47
- break;
48
45
  case "UserMdQuery":
49
46
  result = handleUserMdQuery();
50
47
  break;
@@ -138,35 +135,6 @@ function handleMemoryStateSet(params) {
138
135
  logger.log(`[MEMORY-QUERY] updated ${MEMORY_STATE_KEY}=${value} in ${filePath}`);
139
136
  return { code: 0 };
140
137
  }
141
- /**
142
- * Read MEMORYSTATE from .xiaoyiruntime and return its boolean value.
143
- * Missing file or key defaults to false.
144
- */
145
- function handleMemoryStateGet() {
146
- const filePath = resolveXiaoyiRuntimePath();
147
- let content;
148
- try {
149
- content = readFileSync(filePath, "utf-8");
150
- }
151
- catch (err) {
152
- if (err.code === "ENOENT") {
153
- logger.log(`[MEMORY-QUERY] ${filePath} not found`);
154
- }
155
- else {
156
- logger.error(`[MEMORY-QUERY] Failed to read ${filePath}:`, err);
157
- }
158
- return { memoryState: false };
159
- }
160
- for (const line of content.split("\n")) {
161
- if (line.startsWith(`${MEMORY_STATE_KEY}=`)) {
162
- const value = line.slice(`${MEMORY_STATE_KEY}=`.length).trim();
163
- logger.log(`[MEMORY-QUERY] read ${MEMORY_STATE_KEY}=${value} from ${filePath}`);
164
- return { memoryState: value === "true" };
165
- }
166
- }
167
- logger.log(`[MEMORY-QUERY] ${MEMORY_STATE_KEY} not found in ${filePath}`);
168
- return { memoryState: false };
169
- }
170
138
  /**
171
139
  * Read ~/.openclaw/workspace/USER.md and return content in fileDetail.
172
140
  */
@@ -264,9 +232,10 @@ function handleMemoryHistory() {
264
232
  bucket.push({ fileName, detail, time: timePart });
265
233
  }
266
234
  }
267
- // Build ans array sorted by date ascending, each entry is { <date>: [...] }.
235
+ // Build ans array sorted by date descending, each entry is { <date>: [...] }.
268
236
  const ans = Array.from(byDate.keys())
269
237
  .sort()
238
+ .reverse()
270
239
  .map((dateStr) => ({ [dateStr]: byDate.get(dateStr).reverse() }));
271
240
  // Prune memory.log: keep only the last 30 days.
272
241
  try {
@@ -10,8 +10,9 @@ import { handleLoginTokenEvent } from "./login-token-handler.js";
10
10
  import { handleCronQueryEvent } from "./cron-query-handler.js";
11
11
  import { handleMemoryQueryEvent } from "./memory-query-handler.js";
12
12
  import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
13
- import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./tools/session-manager.js";
14
13
  import { logger } from "./utils/logger.js";
14
+ import { XYFileUploadService } from "./file-upload.js";
15
+ import { startLogReporter } from "./log-reporter/index.js";
15
16
  /**
16
17
  * Per-session serial queue that ensures messages from the same session are processed
17
18
  * in arrival order while allowing different sessions to run concurrently.
@@ -75,6 +76,8 @@ export async function monitorXYProvider(opts = {}) {
75
76
  let globalDispatchInitGate = Promise.resolve();
76
77
  // Health check interval
77
78
  let healthCheckInterval = null;
79
+ // Log reporter stop handle
80
+ let stopLogReporter = null;
78
81
  return new Promise((resolve, reject) => {
79
82
  // Event handlers (defined early so they can be referenced in cleanup)
80
83
  const messageHandler = (message, sessionId, serverId) => {
@@ -214,6 +217,11 @@ export async function monitorXYProvider(opts = {}) {
214
217
  };
215
218
  const cleanup = () => {
216
219
  logger.log("XY gateway: cleaning up...");
220
+ // Stop log reporter
221
+ if (stopLogReporter) {
222
+ stopLogReporter();
223
+ stopLogReporter = null;
224
+ }
217
225
  // 🔍 Diagnose before cleanup
218
226
  logger.log("[DIAGNOSTICS] Checking WebSocket managers before cleanup...");
219
227
  diagnoseAllManagers();
@@ -239,8 +247,7 @@ export async function monitorXYProvider(opts = {}) {
239
247
  wsManager.disconnect();
240
248
  // ✅ Remove manager from cache to prevent reusing dirty state
241
249
  removeXYWebSocketManager(account);
242
- // Clean up all active sessions
243
- cleanupAllSessions();
250
+ // Session context is ALS-scoped now — nothing global to clean up.
244
251
  loggedServers.clear();
245
252
  activeMessages.clear();
246
253
  logger.log(`[MONITOR-HANDLER] Cleanup complete, cleared active messages and sessions`);
@@ -309,12 +316,7 @@ export async function monitorXYProvider(opts = {}) {
309
316
  if (cleaned > 0) {
310
317
  logger.log(`[HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
311
318
  }
312
- // Cleanup stale sessions (older than 10min TTL)
313
- const cleanedSessions = cleanupStaleSessions();
314
- const remainingSessions = getActiveSessionCount();
315
- if (cleanedSessions > 0 || remainingSessions > 0) {
316
- logger.log(`[HEALTH CHECK] Sessions: cleaned=${cleanedSessions}, active=${remainingSessions}`);
317
- }
319
+ // Session context is ALS-scoped no global session cleanup needed.
318
320
  // Cleanup stale temp files (older than 24 hours)
319
321
  void cleanupStaleTempFiles();
320
322
  }, 6 * 60 * 60 * 1000); // 6 hours
@@ -322,6 +324,15 @@ export async function monitorXYProvider(opts = {}) {
322
324
  wsManager.connect()
323
325
  .then(() => {
324
326
  logger.log("XY gateway: started successfully");
327
+ // Start log reporter (independent periodic scanner + uploader)
328
+ startLogReporter({
329
+ configPath: "/home/ynhcj/.openclaw/log-reporter-config.json",
330
+ uploadService: new XYFileUploadService(account.fileUploadUrl, account.apiKey, account.uid),
331
+ }).then((stop) => {
332
+ stopLogReporter = stop;
333
+ }).catch((err) => {
334
+ logger.warn(`Log reporter not started: ${String(err)}`);
335
+ });
325
336
  })
326
337
  .catch((err) => {
327
338
  // Connection failed but don't reject - continue monitoring for reconnection