@ynhcj/xiaoyi-channel 0.0.107-next → 0.0.108-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 (95) hide show
  1. package/dist/src/bot.js +64 -31
  2. package/dist/src/channel.js +5 -15
  3. package/dist/src/formatter.js +5 -5
  4. package/dist/src/monitor.js +12 -12
  5. package/dist/src/outbound.js +5 -5
  6. package/dist/src/provider.js +39 -75
  7. package/dist/src/reply-dispatcher.js +3 -5
  8. package/dist/src/steer-injector.js +7 -5
  9. package/dist/src/tools/calendar-tool.d.ts +2 -1
  10. package/dist/src/tools/calendar-tool.js +7 -8
  11. package/dist/src/tools/call-device-tool.d.ts +2 -1
  12. package/dist/src/tools/call-device-tool.js +31 -31
  13. package/dist/src/tools/call-phone-tool.d.ts +2 -1
  14. package/dist/src/tools/call-phone-tool.js +7 -8
  15. package/dist/src/tools/create-alarm-tool.d.ts +2 -1
  16. package/dist/src/tools/create-alarm-tool.js +7 -8
  17. package/dist/src/tools/create-all-tools.d.ts +8 -7
  18. package/dist/src/tools/create-all-tools.js +24 -20
  19. package/dist/src/tools/delete-alarm-tool.d.ts +2 -1
  20. package/dist/src/tools/delete-alarm-tool.js +7 -8
  21. package/dist/src/tools/get-alarm-tool-schema.d.ts +2 -1
  22. package/dist/src/tools/get-alarm-tool-schema.js +5 -5
  23. package/dist/src/tools/get-calendar-tool-schema.d.ts +2 -1
  24. package/dist/src/tools/get-calendar-tool-schema.js +3 -3
  25. package/dist/src/tools/get-collection-tool-schema.d.ts +2 -1
  26. package/dist/src/tools/get-collection-tool-schema.js +2 -2
  27. package/dist/src/tools/get-contact-tool-schema.d.ts +2 -1
  28. package/dist/src/tools/get-contact-tool-schema.js +5 -5
  29. package/dist/src/tools/get-device-file-tool-schema.d.ts +2 -1
  30. package/dist/src/tools/get-device-file-tool-schema.js +4 -4
  31. package/dist/src/tools/get-email-tool-schema.d.ts +2 -1
  32. package/dist/src/tools/get-email-tool-schema.js +3 -3
  33. package/dist/src/tools/get-note-tool-schema.d.ts +2 -1
  34. package/dist/src/tools/get-note-tool-schema.js +4 -4
  35. package/dist/src/tools/get-photo-tool-schema.d.ts +2 -1
  36. package/dist/src/tools/get-photo-tool-schema.js +3 -3
  37. package/dist/src/tools/image-reading-tool.d.ts +2 -1
  38. package/dist/src/tools/image-reading-tool.js +4 -5
  39. package/dist/src/tools/location-tool.d.ts +2 -1
  40. package/dist/src/tools/location-tool.js +7 -8
  41. package/dist/src/tools/login-token-tool.d.ts +2 -1
  42. package/dist/src/tools/login-token-tool.js +9 -9
  43. package/dist/src/tools/modify-alarm-tool.d.ts +2 -1
  44. package/dist/src/tools/modify-alarm-tool.js +7 -8
  45. package/dist/src/tools/modify-note-tool.d.ts +2 -1
  46. package/dist/src/tools/modify-note-tool.js +7 -8
  47. package/dist/src/tools/note-tool.d.ts +2 -1
  48. package/dist/src/tools/note-tool.js +7 -8
  49. package/dist/src/tools/query-app-message-tool.d.ts +2 -1
  50. package/dist/src/tools/query-app-message-tool.js +7 -8
  51. package/dist/src/tools/query-memory-data-tool.d.ts +2 -1
  52. package/dist/src/tools/query-memory-data-tool.js +7 -8
  53. package/dist/src/tools/query-todo-task-tool.d.ts +2 -1
  54. package/dist/src/tools/query-todo-task-tool.js +7 -8
  55. package/dist/src/tools/save-file-to-phone-tool.d.ts +2 -1
  56. package/dist/src/tools/save-file-to-phone-tool.js +8 -9
  57. package/dist/src/tools/save-media-to-gallery-tool.d.ts +2 -1
  58. package/dist/src/tools/save-media-to-gallery-tool.js +8 -9
  59. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +2 -1
  60. package/dist/src/tools/save-self-evolution-skill-tool.js +3 -4
  61. package/dist/src/tools/search-alarm-tool.d.ts +2 -1
  62. package/dist/src/tools/search-alarm-tool.js +7 -8
  63. package/dist/src/tools/search-calendar-tool.d.ts +2 -1
  64. package/dist/src/tools/search-calendar-tool.js +7 -8
  65. package/dist/src/tools/search-contact-tool.d.ts +2 -1
  66. package/dist/src/tools/search-contact-tool.js +7 -8
  67. package/dist/src/tools/search-email-tool.d.ts +2 -1
  68. package/dist/src/tools/search-email-tool.js +7 -8
  69. package/dist/src/tools/search-file-tool.d.ts +2 -1
  70. package/dist/src/tools/search-file-tool.js +7 -8
  71. package/dist/src/tools/search-message-tool.d.ts +2 -1
  72. package/dist/src/tools/search-message-tool.js +7 -8
  73. package/dist/src/tools/search-note-tool.d.ts +2 -1
  74. package/dist/src/tools/search-note-tool.js +7 -8
  75. package/dist/src/tools/search-photo-gallery-tool.d.ts +2 -1
  76. package/dist/src/tools/search-photo-gallery-tool.js +4 -5
  77. package/dist/src/tools/send-email-tool.d.ts +2 -1
  78. package/dist/src/tools/send-email-tool.js +7 -8
  79. package/dist/src/tools/send-file-to-user-tool.d.ts +2 -1
  80. package/dist/src/tools/send-file-to-user-tool.js +12 -11
  81. package/dist/src/tools/send-message-tool.d.ts +2 -1
  82. package/dist/src/tools/send-message-tool.js +7 -8
  83. package/dist/src/tools/upload-file-tool.d.ts +2 -1
  84. package/dist/src/tools/upload-file-tool.js +4 -5
  85. package/dist/src/tools/upload-photo-tool.d.ts +2 -1
  86. package/dist/src/tools/upload-photo-tool.js +4 -5
  87. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +2 -1
  88. package/dist/src/tools/xiaoyi-add-collection-tool.js +8 -9
  89. package/dist/src/tools/xiaoyi-collection-tool.d.ts +2 -1
  90. package/dist/src/tools/xiaoyi-collection-tool.js +7 -8
  91. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +2 -1
  92. package/dist/src/tools/xiaoyi-delete-collection-tool.js +7 -8
  93. package/dist/src/tools/xiaoyi-gui-tool.d.ts +2 -1
  94. package/dist/src/tools/xiaoyi-gui-tool.js +10 -10
  95. package/package.json +1 -1
package/dist/src/bot.js CHANGED
@@ -6,13 +6,14 @@ 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
10
  import { configManager } from "./utils/config-manager.js";
10
11
  import { addPushId } from "./utils/pushid-manager.js";
11
12
  import { getPushDataById } from "./utils/pushdata-manager.js";
12
13
  import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
13
14
  import { saveRuntimeInfo } from "./utils/runtime-manager.js";
14
15
  import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
15
- import { registerSession, unregisterSession, hasActiveSession, xyAsyncLocalStorage, } from "./xy-session-store.js";
16
+ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
16
17
  /**
17
18
  * Handle an incoming A2A message.
18
19
  * This is the main entry point for message processing.
@@ -100,26 +101,22 @@ export async function handleXYMessage(params) {
100
101
  }
101
102
  }
102
103
  // ========================================
103
- // 🔑 检测steer模式
104
- // Resolve route early to determine steer state
105
- const config = resolveXYConfig(cfg);
106
- let route = core.channel.routing.resolveAgentRoute({
107
- cfg,
108
- channel: "xiaoyi-channel",
109
- accountId,
110
- peer: {
111
- kind: "direct",
112
- id: parsed.sessionId,
113
- },
114
- });
115
- log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
104
+ // 🔑 检测steer模式和是否是第二条消息
116
105
  const isSteerMode = cfg.messages?.queue?.mode === "steer";
117
- const isSecondMessage = isSteerMode && hasActiveSession(route.sessionKey);
106
+ const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
118
107
  if (isSecondMessage) {
119
108
  log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
120
109
  log(`[BOT] - Session: ${parsed.sessionId}`);
121
110
  log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
122
111
  }
112
+ // 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
113
+ const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
114
+ );
115
+ // 🔑 如果是第一条消息,锁定taskId防止被过早清理
116
+ if (!isUpdate) {
117
+ lockTaskId(parsed.sessionId);
118
+ log(`[BOT] 🔒 Locked taskId for first message`);
119
+ }
123
120
  // Extract and update push_id if present
124
121
  const pushId = extractPushId(parsed.parts);
125
122
  if (pushId) {
@@ -145,13 +142,27 @@ export async function handleXYMessage(params) {
145
142
  ).catch((err) => {
146
143
  error(`[BOT] Failed to save runtime info:`, err);
147
144
  });
148
- // 🔑 Register / update session in unified store
145
+ // Resolve configuration (needed for status updates)
146
+ const config = resolveXYConfig(cfg);
147
+ // ✅ Resolve agent route (following feishu pattern)
148
+ // accountId is "default" for XY (single account mode)
149
+ // Use sessionId as peer.id to ensure all messages in the same session share context
150
+ let route = core.channel.routing.resolveAgentRoute({
151
+ cfg,
152
+ channel: "xiaoyi-channel",
153
+ accountId, // "default"
154
+ peer: {
155
+ kind: "direct",
156
+ id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
157
+ },
158
+ });
159
+ log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
149
160
  registerSession(route.sessionKey, {
150
161
  config,
151
- a2aSessionId: parsed.sessionId,
162
+ sessionId: parsed.sessionId,
152
163
  taskId: parsed.taskId,
153
164
  messageId: parsed.messageId,
154
- accountId: route.accountId,
165
+ agentId: route.accountId,
155
166
  deviceType,
156
167
  });
157
168
  // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
@@ -252,18 +263,35 @@ export async function handleXYMessage(params) {
252
263
  startStatusInterval();
253
264
  log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
254
265
  }
266
+ // Build session context for AsyncLocalStorage
267
+ const sessionContext = {
268
+ config,
269
+ sessionId: parsed.sessionId,
270
+ taskId: parsed.taskId,
271
+ messageId: parsed.messageId,
272
+ agentId: route.accountId,
273
+ deviceType,
274
+ };
255
275
  log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
256
- // 🔐 Set ALS BEFORE withReplyDispatcher so agentTools factory can read it
257
- // during dispatcher setup (which happens before run() is called).
258
- await xyAsyncLocalStorage.run({ openclawSessionKey: route.sessionKey }, () => core.channel.reply.withReplyDispatcher({
276
+ await core.channel.reply.withReplyDispatcher({
259
277
  dispatcher,
260
278
  onSettled: () => {
261
279
  log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
262
- // Cleanup session from store
280
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
281
+ // 🔑 减少引用计数
282
+ decrementTaskIdRef(parsed.sessionId);
283
+ // 🔑 如果是第一条消息完成,解锁
284
+ if (!isSecondMessage) {
285
+ unlockTaskId(parsed.sessionId);
286
+ log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
287
+ }
288
+ // 减少session引用计数
263
289
  unregisterSession(route.sessionKey);
264
290
  log(`[BOT] ✅ Cleanup completed`);
265
291
  },
266
- run: async () => {
292
+ run: () =>
293
+ // 🔐 Use AsyncLocalStorage to provide session context to tools
294
+ runWithSessionContext(sessionContext, async () => {
267
295
  log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
268
296
  log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
269
297
  log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
@@ -288,8 +316,8 @@ export async function handleXYMessage(params) {
288
316
  error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
289
317
  throw dispatchErr;
290
318
  }
291
- },
292
- }));
319
+ }),
320
+ });
293
321
  log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
294
322
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
295
323
  }
@@ -298,14 +326,18 @@ export async function handleXYMessage(params) {
298
326
  error("Failed to handle XY message:", err);
299
327
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
300
328
  log(`[BOT] ❌ Error occurred, attempting cleanup...`);
301
- // 🔑 错误时也要清理session
329
+ // 🔑 错误时也要清理taskId和session
302
330
  try {
303
- const msgParams = message.params;
304
- const sessionId = msgParams?.sessionId;
331
+ const params = message.params;
332
+ const sessionId = params?.sessionId;
305
333
  if (sessionId) {
306
334
  log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
307
- const core2 = getXYRuntime();
308
- const errorRoute = core2.channel.routing.resolveAgentRoute({
335
+ // 清理 taskId
336
+ decrementTaskIdRef(sessionId);
337
+ unlockTaskId(sessionId);
338
+ // 清理 session
339
+ const core = getXYRuntime();
340
+ const route = core.channel.routing.resolveAgentRoute({
309
341
  cfg,
310
342
  channel: "xiaoyi-channel",
311
343
  accountId,
@@ -314,12 +346,13 @@ export async function handleXYMessage(params) {
314
346
  id: sessionId,
315
347
  },
316
348
  });
317
- unregisterSession(errorRoute.sessionKey);
349
+ unregisterSession(route.sessionKey);
318
350
  log(`[BOT] ✅ Cleanup completed after error`);
319
351
  }
320
352
  }
321
353
  catch (cleanupErr) {
322
354
  log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
355
+ // Ignore cleanup errors
323
356
  }
324
357
  // ❌ Don't re-throw: message processing error should not affect gateway stability
325
358
  }
@@ -2,7 +2,7 @@ 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 { getSession, xyAsyncLocalStorage } from "./xy-session-store.js";
5
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
6
6
  import { createAllTools } from "./tools/create-all-tools.js";
7
7
  import { logger } from "./utils/logger.js";
8
8
  /**
@@ -44,20 +44,10 @@ export const xyPlugin = {
44
44
  },
45
45
  outbound: xyOutbound,
46
46
  agentTools: () => {
47
- // agentTools factory may be called before ALS is active (e.g. during
48
- // dispatcher setup). Don't bail out — tools will resolve their
49
- // session at execute time via requireSession() which falls back to ALS.
50
- const alsContext = xyAsyncLocalStorage.getStore();
51
- const sessionKey = alsContext?.openclawSessionKey;
52
- const allTools = createAllTools(sessionKey ?? undefined);
53
- const session = sessionKey ? getSession(sessionKey) : null;
54
- const filtered = filterToolsByDevice(allTools, session?.deviceType);
55
- if (sessionKey) {
56
- logger.log(`[CHANNEL-TOOLS] sessionKey=${sessionKey} deviceType=${session?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length}`);
57
- }
58
- else {
59
- logger.log(`[CHANNEL-TOOLS] no ALS at factory time, created ${filtered.length} tools (will resolve session at execute time)`);
60
- }
47
+ const ctx = getCurrentSessionContext();
48
+ const allTools = createAllTools(ctx);
49
+ const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
50
+ logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
61
51
  return filtered;
62
52
  },
63
53
  messaging: {
@@ -2,7 +2,7 @@
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 { getSessionByA2AId } from "./xy-session-store.js";
5
+ import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
6
6
  /**
7
7
  * Send an A2A artifact update response.
8
8
  */
@@ -114,8 +114,8 @@ export async function sendStatusUpdate(params) {
114
114
  const log = runtime?.log ?? console.log;
115
115
  // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
116
116
  // fall back to closure-captured values
117
- const currentTaskId = getSessionByA2AId(sessionId)?.taskId ?? taskId;
118
- const currentMessageId = getSessionByA2AId(sessionId)?.messageId ?? messageId;
117
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
118
+ const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
119
119
  // Build status update event following A2A protocol standard
120
120
  const statusUpdate = {
121
121
  taskId: currentTaskId,
@@ -162,8 +162,8 @@ export async function sendCommand(params) {
162
162
  const { config, sessionId, taskId, messageId, command } = params;
163
163
  // Dynamic lookup: use latest taskId/messageId from task-manager (handles steer/interrupt),
164
164
  // fall back to closure-captured values
165
- const currentTaskId = getSessionByA2AId(sessionId)?.taskId ?? taskId;
166
- const currentMessageId = getSessionByA2AId(sessionId)?.messageId ?? messageId;
165
+ const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
166
+ const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
167
167
  // Build artifact update with command as data
168
168
  // Wrap command in commands array as per protocol requirement
169
169
  const artifact = {
@@ -2,13 +2,13 @@ import { resolveXYConfig } from "./config.js";
2
2
  import { getXYWebSocketManager, setClientRuntime, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
3
3
  import { handleXYMessage } from "./bot.js";
4
4
  import { parseA2AMessage } from "./parser.js";
5
- import { getAllActiveSessions, getSessionByA2AId } from "./xy-session-store.js";
5
+ import { hasActiveTask, getAllActiveTaskBindings } from "./task-manager.js";
6
6
  import { sendA2AResponse } from "./formatter.js";
7
7
  import { handleTriggerEvent } from "./trigger-handler.js";
8
8
  import { handleSelfEvolutionEvent, handleSelfEvolutionStateGetEvent } from "./self-evolution-handler.js";
9
9
  import { handleLoginTokenEvent } from "./login-token-handler.js";
10
10
  import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
11
- import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./xy-session-store.js";
11
+ import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./tools/session-manager.js";
12
12
  /**
13
13
  * Per-session serial queue that ensures messages from the same session are processed
14
14
  * in arrival order while allowing different sessions to run concurrently.
@@ -106,7 +106,7 @@ export async function monitorXYProvider(opts = {}) {
106
106
  try {
107
107
  const parsed = parseA2AMessage(message);
108
108
  const steerMode = cfg.messages?.queue?.mode === "steer";
109
- const hasActiveRun = Boolean(getSessionByA2AId(parsed.sessionId));
109
+ const hasActiveRun = hasActiveTask(parsed.sessionId);
110
110
  if (steerMode && hasActiveRun) {
111
111
  // Steer模式且有活跃任务:不入队列,直接并发执行
112
112
  log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
@@ -214,25 +214,25 @@ export async function monitorXYProvider(opts = {}) {
214
214
  log("XY gateway: abort signal received, sending notifications before stopping");
215
215
  // 📤 Send restart notification to all active sessions before disconnecting
216
216
  try {
217
- const activeSessions = getAllActiveSessions();
218
- if (activeSessions.length > 0) {
217
+ const activeBindings = getAllActiveTaskBindings();
218
+ if (activeBindings.length > 0) {
219
219
  const config = resolveXYConfig(cfg);
220
220
  const notificationText = "Gateway即将重启,重启期间可能短暂出现\u201c环境异常\u201d提示,请稍候并耐心重试~";
221
- log(`[MONITOR] 📤 Sending restart notifications to ${activeSessions.length} active session(s)`);
222
- const sendPromises = activeSessions.map(({ session }) => sendA2AResponse({
221
+ log(`[MONITOR] 📤 Sending restart notifications to ${activeBindings.length} active session(s)`);
222
+ const sendPromises = activeBindings.map(binding => sendA2AResponse({
223
223
  config,
224
- sessionId: session.a2aSessionId,
225
- taskId: session.taskId,
226
- messageId: session.messageId,
224
+ sessionId: binding.sessionId,
225
+ taskId: binding.currentTaskId,
226
+ messageId: binding.currentMessageId,
227
227
  text: notificationText,
228
228
  append: false,
229
229
  final: true,
230
230
  runtime,
231
231
  }).catch(err => {
232
- error(`[MONITOR] Failed to send restart notification to session ${session.a2aSessionId}: ${String(err)}`);
232
+ error(`[MONITOR] Failed to send restart notification to session ${binding.sessionId}: ${String(err)}`);
233
233
  }));
234
234
  await Promise.all(sendPromises);
235
- log(`[MONITOR] ✅ Restart notifications sent to ${activeSessions.length} session(s)`);
235
+ log(`[MONITOR] ✅ Restart notifications sent to ${activeBindings.length} session(s)`);
236
236
  }
237
237
  else {
238
238
  log(`[MONITOR] No active sessions, skipping restart notifications`);
@@ -1,7 +1,7 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
2
  import { XYFileUploadService } from "./file-upload.js";
3
3
  import { XYPushService } from "./push.js";
4
- import { getSessionByA2AId } from "./xy-session-store.js";
4
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
5
5
  import { savePushData } from "./utils/pushdata-manager.js";
6
6
  import { getAllPushIds } from "./utils/pushid-manager.js";
7
7
  import { logger } from "./utils/logger.js";
@@ -68,10 +68,10 @@ export const xyOutbound = {
68
68
  // If the target doesn't contain "::", try to enhance it with taskId from session context
69
69
  if (!trimmedTo.includes("::")) {
70
70
  logger.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
71
- // Try to get the current session context via A2A sessionId
72
- const session = getSessionByA2AId(trimmedTo);
73
- if (session) {
74
- const enhancedTarget = `${trimmedTo}::${session.taskId}`;
71
+ // Try to get the current session context
72
+ const sessionContext = getCurrentSessionContext();
73
+ if (sessionContext && sessionContext.sessionId === trimmedTo) {
74
+ const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
75
75
  logger.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
76
76
  return {
77
77
  ok: true,
@@ -8,7 +8,8 @@
8
8
  // models.providers.xiaoyiprovider.api = "openai-completions"
9
9
  // models.providers.xiaoyiprovider.models = [...]
10
10
  import { createHash } from "crypto";
11
- import { getSession, getSessionByA2AId, getAllActiveSessions, xyAsyncLocalStorage } from "./xy-session-store.js";
11
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
12
+ import { getCurrentTaskId } from "./task-manager.js";
12
13
  import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
13
14
  // ── Retry config ──────────────────────────────────────────────
14
15
  const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000, 60_000];
@@ -404,12 +405,9 @@ export const xiaoyiProvider = {
404
405
  * 3. No uid available → return undefined (no headers injected)
405
406
  */
406
407
  prepareExtraParams: (ctx) => {
407
- // Try to get session from ALS (valid during agent run setup)
408
- const alsContext = xyAsyncLocalStorage.getStore();
409
- const sessionKey = alsContext?.openclawSessionKey;
410
- const session = sessionKey ? getSession(sessionKey) : null;
411
- if (session) {
412
- const taskId = session.taskId;
408
+ const sessionCtx = getCurrentSessionContext();
409
+ if (sessionCtx) {
410
+ const taskId = sessionCtx.taskId;
413
411
  const sessionId = taskId.split("&")[0];
414
412
  const interactionId = taskId.split("&")[1] || "";
415
413
  return {
@@ -417,7 +415,7 @@ export const xiaoyiProvider = {
417
415
  [HEADER_TRACE_ID]: taskId,
418
416
  [HEADER_SESSION_ID]: sessionId,
419
417
  [HEADER_INTERACTION_ID]: interactionId,
420
- [DEVICE_TYPE_KEY]: session.deviceType ?? "",
418
+ [DEVICE_TYPE_KEY]: sessionCtx.deviceType ?? "",
421
419
  };
422
420
  }
423
421
  // Fallback: store uid prefix for lazy timestamp generation in wrapStreamFn.
@@ -446,54 +444,17 @@ export const xiaoyiProvider = {
446
444
  if (!underlying)
447
445
  return underlying;
448
446
  // Capture A2A sessionId at agent setup time for multi-session isolation.
449
- // wrapStreamFn is called per-agent (per session), ALS is still valid here.
450
- // We capture the a2aSessionId and look up the store at each request time
451
- // to always get the latest taskId (supports steer).
452
- const alsContext = xyAsyncLocalStorage.getStore();
453
- const capturedA2ASessionId = alsContext?.openclawSessionKey
454
- ? getSession(alsContext.openclawSessionKey)?.a2aSessionId ?? null
455
- : null;
447
+ // openclaw calls wrapStreamFn per-agent (per session), so this runs inside
448
+ // the correct runWithSessionContext() ALS scope. When multiple sessions are
449
+ // active concurrently, getCurrentSessionContext() may later return the WRONG
450
+ // session (lastRegisteredKey fallback). The captured sessionId lets us
451
+ // bypass that fallback and look up the correct taskId directly from
452
+ // task-manager.
453
+ const capturedA2ASessionId = getCurrentSessionContext()?.sessionId ?? null;
456
454
  return async (model, context, options) => {
455
+ // 每次请求时从 ctx.extraParams 动态读取 header
457
456
  const dynamicHeaders = {};
458
- // Resolve taskId via three-layer fallback (same pattern as requireSession):
459
- // 1. capturedA2ASessionId → store lookup (set at wrapStreamFn setup if ALS was active)
460
- // 2. ALS → store lookup (works at request time if async chain preserved context)
461
- // 3. Store enumeration (safe when only one session is active)
462
- let resolvedTaskId = null;
463
- if (capturedA2ASessionId) {
464
- resolvedTaskId = getSessionByA2AId(capturedA2ASessionId)?.taskId ?? null;
465
- }
466
- if (!resolvedTaskId) {
467
- const alsContext = xyAsyncLocalStorage.getStore();
468
- const alsKey = alsContext?.openclawSessionKey;
469
- if (alsKey) {
470
- resolvedTaskId = getSession(alsKey)?.taskId ?? null;
471
- }
472
- }
473
- if (!resolvedTaskId) {
474
- const allSessions = getAllActiveSessions();
475
- if (allSessions.length === 1) {
476
- resolvedTaskId = allSessions[0].session.taskId;
477
- }
478
- }
479
- if (resolvedTaskId) {
480
- // Session mode: use taskId from store (supports steer).
481
- const sessionId = resolvedTaskId.split("&")[0];
482
- const interactionId = resolvedTaskId.split("&")[1] || "";
483
- const isCron = isCronTriggered(context.messages);
484
- dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${resolvedTaskId}_${Date.now()}` : resolvedTaskId;
485
- dynamicHeaders[HEADER_SESSION_ID] = sessionId;
486
- dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
487
- if (isCron) {
488
- const cronTitle = extractCronTitle(context.messages);
489
- if (cronTitle)
490
- dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
491
- if (context.messages?.length === 1)
492
- dynamicHeaders["x-cron-flag"] = "begin";
493
- }
494
- }
495
- else if (ctx.extraParams) {
496
- // No active session — fall back to prepareExtraParams cache.
457
+ if (ctx.extraParams) {
497
458
  const fallbackPrefix = ctx.extraParams[FALLBACK_PREFIX_KEY];
498
459
  if (typeof fallbackPrefix === "string") {
499
460
  // Fallback mode: generate fresh timestamp per request
@@ -511,10 +472,28 @@ export const xiaoyiProvider = {
511
472
  }
512
473
  }
513
474
  else {
514
- // No captured session, no fallback use whatever is in prepareExtraParams cache.
515
- const traceId = ctx.extraParams[HEADER_TRACE_ID];
516
- const sessionId = ctx.extraParams[HEADER_SESSION_ID];
517
- const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
475
+ // Session mode: resolve taskId for the correct session.
476
+ //
477
+ // Priority:
478
+ // 1. capturedA2ASessionId getCurrentTaskId() (most reliable,
479
+ // bypasses lastRegisteredKey fallback)
480
+ // 2. getCurrentSessionContext()?.taskId (works when ALS
481
+ // is intact)
482
+ // 3. ctx.extraParams cached values (last resort,
483
+ // may be stale / from wrong session)
484
+ let resolvedTaskId = null;
485
+ if (capturedA2ASessionId) {
486
+ resolvedTaskId = getCurrentTaskId(capturedA2ASessionId);
487
+ }
488
+ if (!resolvedTaskId) {
489
+ resolvedTaskId = getCurrentSessionContext()?.taskId ?? null;
490
+ }
491
+ const traceId = resolvedTaskId ?? ctx.extraParams[HEADER_TRACE_ID];
492
+ const sessionId = resolvedTaskId?.split("&")[0]
493
+ ?? ctx.extraParams[HEADER_SESSION_ID];
494
+ const interactionId = resolvedTaskId?.split("&")[1]
495
+ ?? ctx.extraParams[HEADER_INTERACTION_ID]
496
+ ?? "";
518
497
  if (typeof traceId === "string") {
519
498
  const isCron = isCronTriggered(context.messages);
520
499
  dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}_${Date.now()}` : traceId;
@@ -538,27 +517,12 @@ export const xiaoyiProvider = {
538
517
  console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
539
518
  }
540
519
  // Prefer deviceType from extraParams (set by prepareExtraParams).
541
- // Fall back to store lookup because OpenClaw caches
520
+ // Fall back to getCurrentSessionContext() because OpenClaw caches
542
521
  // resolvePreparedExtraParams by provider/modelId – the cache key does
543
522
  // not include session-specific data, so deviceType may be missing
544
523
  // from the cached extraParams even when a session is active.
545
524
  const extraParamsDeviceType = ctx.extraParams?.[DEVICE_TYPE_KEY] || undefined;
546
- let storeDeviceType;
547
- if (capturedA2ASessionId) {
548
- storeDeviceType = getSessionByA2AId(capturedA2ASessionId)?.deviceType;
549
- }
550
- if (!storeDeviceType) {
551
- const alsCtx = xyAsyncLocalStorage.getStore();
552
- const key = alsCtx?.openclawSessionKey;
553
- if (key)
554
- storeDeviceType = getSession(key)?.deviceType;
555
- }
556
- if (!storeDeviceType) {
557
- const all = getAllActiveSessions();
558
- if (all.length === 1)
559
- storeDeviceType = all[0].session.deviceType;
560
- }
561
- const deviceType = extraParamsDeviceType ?? storeDeviceType;
525
+ const deviceType = extraParamsDeviceType ?? getCurrentSessionContext()?.deviceType;
562
526
  // 在发送给模型前,优化 systemPrompt 结构
563
527
  if (context.systemPrompt) {
564
528
  let sp = context.systemPrompt;
@@ -1,7 +1,7 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
3
3
  import { resolveXYConfig } from "./config.js";
4
- import { getSessionByA2AId } from "./xy-session-store.js";
4
+ import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
5
5
  import fs from "fs/promises";
6
6
  import path from "path";
7
7
  import { logger } from "./utils/logger.js";
@@ -59,12 +59,10 @@ export function createXYReplyDispatcher(params) {
59
59
  * 每次需要taskId时,都从TaskManager获取最新值
60
60
  */
61
61
  const getActiveTaskId = () => {
62
- const session = getSessionByA2AId(sessionId);
63
- return session?.taskId ?? initialTaskId;
62
+ return getCurrentTaskId(sessionId) ?? initialTaskId;
64
63
  };
65
64
  const getActiveMessageId = () => {
66
- const session = getSessionByA2AId(sessionId);
67
- return session?.messageId ?? initialMessageId;
65
+ return getCurrentMessageId(sessionId) ?? initialMessageId;
68
66
  };
69
67
  const core = getXYRuntime();
70
68
  const config = resolveXYConfig(cfg);
@@ -1,5 +1,6 @@
1
1
  // Steer message injector for CSPL hook integration
2
- import { getSession, hasActiveSession } from "./xy-session-store.js";
2
+ import { getSessionContext } from "./tools/session-manager.js";
3
+ import { hasActiveTask, getCurrentTaskId } from "./task-manager.js";
3
4
  import { handleXYMessage } from "./bot.js";
4
5
  import { logger } from "./utils/logger.js";
5
6
  import { randomUUID } from "node:crypto";
@@ -28,12 +29,13 @@ export async function tryInjectSteer(sessionKey, message) {
28
29
  if (!sessionKey) {
29
30
  return false;
30
31
  }
31
- const session = getSession(sessionKey);
32
- if (!session) {
32
+ const sessionCtx = getSessionContext(sessionKey);
33
+ if (!sessionCtx) {
33
34
  return false;
34
35
  }
35
- const { a2aSessionId: sessionId, taskId: activeTaskId } = session;
36
- if (!hasActiveSession(sessionKey)) {
36
+ const { sessionId } = sessionCtx;
37
+ const activeTaskId = getCurrentTaskId(sessionId);
38
+ if (!hasActiveTask(sessionId)) {
37
39
  return false;
38
40
  }
39
41
  if (!cachedCfg || !cachedRuntime) {
@@ -1,6 +1,7 @@
1
+ import type { SessionContext } from "./session-manager.js";
1
2
  /**
2
3
  * XY calendar event tool - creates a calendar event on user's device.
3
4
  * Requires title, dtStart (start time), and dtEnd (end time) parameters.
4
5
  * Time format must be: yyyy-mm-dd hh:mm:ss
5
6
  */
6
- export declare function createCalendarTool(sessionKey: string | undefined): any;
7
+ export declare function createCalendarTool(ctx: SessionContext): any;
@@ -1,12 +1,12 @@
1
1
  import { getXYWebSocketManager } from "../client.js";
2
2
  import { sendCommand } from "../formatter.js";
3
- import { requireSession } from "./session-helper.js";
4
3
  /**
5
4
  * XY calendar event tool - creates a calendar event on user's device.
6
5
  * Requires title, dtStart (start time), and dtEnd (end time) parameters.
7
6
  * Time format must be: yyyy-mm-dd hh:mm:ss
8
7
  */
9
- export function createCalendarTool(sessionKey) {
8
+ export function createCalendarTool(ctx) {
9
+ const { config, sessionId, taskId, messageId } = ctx;
10
10
  return {
11
11
  name: "create_calendar_event",
12
12
  label: "Create Calendar Event",
@@ -34,7 +34,6 @@ export function createCalendarTool(sessionKey) {
34
34
  required: ["title", "dtStart", "dtEnd"],
35
35
  },
36
36
  async execute(toolCallId, params) {
37
- const session = requireSession(sessionKey);
38
37
  // Validate parameters
39
38
  if (!params.title || !params.dtStart || !params.dtEnd) {
40
39
  throw new Error("Missing required parameters: title, dtStart, and dtEnd are required");
@@ -46,7 +45,7 @@ export function createCalendarTool(sessionKey) {
46
45
  throw new Error("Invalid time format. Required format: yyyy-mm-dd hh:mm:ss (e.g., 2024-01-15 14:30:00)");
47
46
  }
48
47
  // Get WebSocket manager
49
- const wsManager = getXYWebSocketManager(session.config);
48
+ const wsManager = getXYWebSocketManager(config);
50
49
  // Build CreateCalendarEvent command
51
50
  const command = {
52
51
  header: {
@@ -112,10 +111,10 @@ export function createCalendarTool(sessionKey) {
112
111
  wsManager.on("data-event", handler);
113
112
  // Send the command
114
113
  sendCommand({
115
- config: session.config,
116
- sessionId: session.a2aSessionId,
117
- taskId: session.taskId,
118
- messageId: session.messageId,
114
+ config,
115
+ sessionId,
116
+ taskId,
117
+ messageId,
119
118
  command,
120
119
  })
121
120
  .then(() => {
@@ -1,5 +1,6 @@
1
+ import type { SessionContext } from "./session-manager.js";
1
2
  /**
2
3
  * call_device_tool - 通用端工具调度器。
3
4
  * LLM 必须先通过 get_xxx_tool_schema 获取具体工具 schema,再用本工具执行。
4
5
  */
5
- export declare function createCallDeviceTool(sessionKey: string | undefined): any;
6
+ export declare function createCallDeviceTool(ctx: SessionContext): any;