@ynhcj/xiaoyi-channel 0.0.166-next → 0.0.167-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 (52) hide show
  1. package/dist/index.js +3 -3
  2. package/dist/src/bot.js +7 -27
  3. package/dist/src/channel.js +9 -9
  4. package/dist/src/cspl/sentinel_hook.js +2 -2
  5. package/dist/src/formatter.js +12 -23
  6. package/dist/src/monitor.js +2 -9
  7. package/dist/src/provider.js +33 -62
  8. package/dist/src/reply-dispatcher.js +30 -53
  9. package/dist/src/task-manager.d.ts +0 -12
  10. package/dist/src/task-manager.js +0 -21
  11. package/dist/src/tools/agent-as-skill-tool.js +1 -3
  12. package/dist/src/tools/calendar-tool.js +1 -3
  13. package/dist/src/tools/call-device-tool.js +2 -5
  14. package/dist/src/tools/call-phone-tool.js +1 -3
  15. package/dist/src/tools/check-plugin-privilege-tool.js +1 -3
  16. package/dist/src/tools/create-alarm-tool.js +1 -3
  17. package/dist/src/tools/delete-alarm-tool.js +1 -3
  18. package/dist/src/tools/discover-cross-devices-tool.js +4 -7
  19. package/dist/src/tools/display-a2ui-card-tool.js +2 -5
  20. package/dist/src/tools/location-tool.js +1 -3
  21. package/dist/src/tools/login-token-tool.js +3 -6
  22. package/dist/src/tools/modify-alarm-tool.js +1 -3
  23. package/dist/src/tools/modify-note-tool.js +1 -3
  24. package/dist/src/tools/note-tool.js +1 -3
  25. package/dist/src/tools/query-app-message-tool.js +1 -3
  26. package/dist/src/tools/query-memory-data-tool.js +1 -3
  27. package/dist/src/tools/query-todo-task-tool.js +1 -3
  28. package/dist/src/tools/save-file-to-phone-tool.js +1 -3
  29. package/dist/src/tools/save-media-to-gallery-tool.js +1 -3
  30. package/dist/src/tools/search-alarm-tool.js +1 -3
  31. package/dist/src/tools/search-calendar-tool.js +1 -3
  32. package/dist/src/tools/search-contact-tool.js +1 -3
  33. package/dist/src/tools/search-email-tool.js +1 -3
  34. package/dist/src/tools/search-file-tool.js +1 -3
  35. package/dist/src/tools/search-message-tool.js +1 -3
  36. package/dist/src/tools/search-note-tool.js +1 -3
  37. package/dist/src/tools/search-photo-gallery-tool.js +1 -3
  38. package/dist/src/tools/send-cross-device-task-tool.js +9 -14
  39. package/dist/src/tools/send-email-tool.js +1 -3
  40. package/dist/src/tools/send-file-to-user-tool.js +3 -6
  41. package/dist/src/tools/send-html-card-tool.js +1 -3
  42. package/dist/src/tools/send-message-tool.js +1 -3
  43. package/dist/src/tools/session-manager.d.ts +7 -50
  44. package/dist/src/tools/session-manager.js +34 -233
  45. package/dist/src/tools/upload-file-tool.js +1 -3
  46. package/dist/src/tools/upload-photo-tool.js +1 -3
  47. package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -3
  48. package/dist/src/tools/xiaoyi-collection-tool.js +1 -3
  49. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -3
  50. package/dist/src/tools/xiaoyi-gui-tool.js +2 -4
  51. package/dist/src/utils/logger.js +3 -14
  52. 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
@@ -6,7 +6,7 @@ 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";
@@ -160,19 +160,10 @@ export async function handleXYMessage(params) {
160
160
  },
161
161
  });
162
162
  log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
163
- // Steer injections skip session registration to avoid refCount leaks
163
+ // ALS only: no registerSession. The sessionContext built below is handed
164
+ // to runWithSessionContext() inside withReplyDispatcher.run, which is the
165
+ // single wrap point for the whole agent turn.
164
166
  if (!skipReg) {
165
- registerSession(route.sessionKey, {
166
- config,
167
- sessionId: parsed.sessionId,
168
- distributionSessionId,
169
- taskId: parsed.taskId,
170
- messageId: parsed.messageId,
171
- agentId: route.accountId,
172
- deviceType,
173
- modelName,
174
- runCrossTaskContext: runCrossTaskContext ?? undefined,
175
- });
176
167
  // 🔑 Sync A2A modelName to OpenClaw session store so that session_status
177
168
  // reports the correct model. Without this, session_status returns the
178
169
  // configured default model instead of the A2A-specified one.
@@ -380,7 +371,6 @@ export async function handleXYMessage(params) {
380
371
  }
381
372
  streamingSignals.delete(parsed.sessionId);
382
373
  decrementTaskIdRef(parsed.sessionId);
383
- unregisterSession(route.sessionKey);
384
374
  log.log(`[BOT] Cleanup completed`);
385
375
  },
386
376
  run: () => {
@@ -390,6 +380,7 @@ export async function handleXYMessage(params) {
390
380
  // signal init complete to release the global dispatch gate
391
381
  // for the next session.
392
382
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
383
+ log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=false`);
393
384
  log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
394
385
  try {
395
386
  const result = await core.channel.reply.dispatchReplyFromConfig({
@@ -422,7 +413,7 @@ export async function handleXYMessage(params) {
422
413
  errLog.error("Failed to handle XY message:", err);
423
414
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
424
415
  errLog.log(`[BOT] Error occurred, attempting cleanup`);
425
- // 🔑 错误时也要清理taskIdsession
416
+ // 🔑 错误时也要清理taskIdsession 走 ALS,作用域退出自动清理)
426
417
  try {
427
418
  const params = message.params;
428
419
  const sessionId = params?.sessionId;
@@ -430,18 +421,6 @@ export async function handleXYMessage(params) {
430
421
  errLog.log(`[BOT] Cleaning up after error`);
431
422
  // 清理 taskId
432
423
  decrementTaskIdRef(sessionId);
433
- // 清理 session
434
- const core = getXYRuntime();
435
- const route = core.channel.routing.resolveAgentRoute({
436
- cfg,
437
- channel: "xiaoyi-channel",
438
- accountId,
439
- peer: {
440
- kind: "direct",
441
- id: sessionId,
442
- },
443
- });
444
- unregisterSession(route.sessionKey);
445
424
  errLog.log(`[BOT] Cleanup completed after error`);
446
425
  }
447
426
  }
@@ -626,6 +605,7 @@ async function dispatchSteerWhenReady(params) {
626
605
  },
627
606
  run: () => {
628
607
  return runWithSessionContext(sessionContext, async () => {
608
+ log.log(`[ALS-PROOF] bot entered steer dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=true`);
629
609
  const result = await core.channel.reply.dispatchReplyFromConfig({
630
610
  ctx: ctxPayload,
631
611
  cfg: params.cfg,
@@ -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 { getCurrentSessionContext, registerSession } from "./tools/session-manager.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
  /**
@@ -69,12 +69,10 @@ export const xyPlugin = {
69
69
  agentTools: (params) => {
70
70
  let ctx = getCurrentSessionContext();
71
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.
72
+ // cron 路径不进 ALS: openclaw cron runner 同步调 agentTools({cfg})
73
+ // 返回工具后才在别处跑 turn, xy_channel 没有 wrap 整个 turn 的点。
74
+ // 这里同步构造合成 ctx 给工具闭包捕获, 工具调用走 sendCommand/push,
75
+ // 不依赖 getCurrentSessionContext。所以不注册任何全局状态。
78
76
  if (!ctx && params?.cfg) {
79
77
  try {
80
78
  const config = resolveXYConfig(params.cfg);
@@ -87,14 +85,16 @@ export const xyPlugin = {
87
85
  agentId: "default",
88
86
  isCron: true,
89
87
  };
90
- // Register so getCurrentSessionContext() fallback can find it
91
- registerSession(`__cron__${cronId}`, ctx);
88
+ logger.log(`[ALS-PROOF] agentTools ctx from ALS miss, using synthetic cron ctx sessionId=${cronId} isCron=true`);
92
89
  logger.log(`[CRON-TOOLS] Created cron session context: ${cronId}`);
93
90
  }
94
91
  catch (err) {
95
92
  logger.error("[CRON-TOOLS] Failed to create cron context:", err);
96
93
  }
97
94
  }
95
+ else {
96
+ logger.log(`[ALS-PROOF] agentTools ctx from ALS sessionId=${ctx?.sessionId} taskId=${ctx?.taskId} isCron=${ctx?.isCron === true}`);
97
+ }
98
98
  if (!ctx) {
99
99
  logger.log("[CREATE-ALL-TOOLS] no session context, returning empty tools list");
100
100
  return [];
@@ -6,7 +6,7 @@ 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) {
@@ -78,7 +78,7 @@ export default function register(api) {
78
78
  logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
79
79
  if (result.status === 'REJECT') {
80
80
  logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
81
- const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
81
+ const sessionCtx = getCurrentSessionContext();
82
82
  if (sessionCtx?.sessionId && sessionCtx?.taskId) {
83
83
  await tryInjectSteer({
84
84
  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`);
@@ -9,7 +9,6 @@ import { handleSelfEvolutionEvent, handleSelfEvolutionStateGetEvent } from "./se
9
9
  import { handleLoginTokenEvent } from "./login-token-handler.js";
10
10
  import { handleCronQueryEvent } from "./cron-query-handler.js";
11
11
  import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
12
- import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./tools/session-manager.js";
13
12
  import { logger } from "./utils/logger.js";
14
13
  /**
15
14
  * Per-session serial queue that ensures messages from the same session are processed
@@ -231,8 +230,7 @@ export async function monitorXYProvider(opts = {}) {
231
230
  wsManager.disconnect();
232
231
  // ✅ Remove manager from cache to prevent reusing dirty state
233
232
  removeXYWebSocketManager(account);
234
- // Clean up all active sessions
235
- cleanupAllSessions();
233
+ // Session context is ALS-scoped now — nothing global to clean up.
236
234
  loggedServers.clear();
237
235
  activeMessages.clear();
238
236
  logger.log(`[MONITOR-HANDLER] Cleanup complete, cleared active messages and sessions`);
@@ -300,12 +298,7 @@ export async function monitorXYProvider(opts = {}) {
300
298
  if (cleaned > 0) {
301
299
  logger.log(`[HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
302
300
  }
303
- // Cleanup stale sessions (older than 10min TTL)
304
- const cleanedSessions = cleanupStaleSessions();
305
- const remainingSessions = getActiveSessionCount();
306
- if (cleanedSessions > 0 || remainingSessions > 0) {
307
- logger.log(`[HEALTH CHECK] Sessions: cleaned=${cleanedSessions}, active=${remainingSessions}`);
308
- }
301
+ // Session context is ALS-scoped no global session cleanup needed.
309
302
  // Cleanup stale temp files (older than 24 hours)
310
303
  void cleanupStaleTempFiles();
311
304
  }, 6 * 60 * 60 * 1000); // 6 hours
@@ -393,24 +393,6 @@ function trimUserMetadata(text) {
393
393
  text = text.replace(/\n*Sender \(untrusted metadata\):\n```json\n[\s\S]*?\n```\n*/, "\n");
394
394
  return text.replace(/\n{3,}/g, "\n\n");
395
395
  }
396
- /**
397
- * Extract A2A taskId and deviceType from Conversation info JSON.
398
- * bot.ts stores them as MessageSid = "xiaoyi_taskId_deviceType".
399
- * The "xiaoyi_" prefix ensures extraction only happens for messages
400
- * routed through xiaoyi-channel, not other channels sharing the provider.
401
- */
402
- function extractA2AFromConversationInfo(text) {
403
- const match = text.match(/Conversation info \(untrusted metadata\):\n```json\n([\s\S]*?)\n```/);
404
- if (!match)
405
- return null;
406
- const msgIdMatch = match[1].match(/"message_id"\s*:\s*"([^"]+)"/);
407
- if (!msgIdMatch)
408
- return null;
409
- const parts = msgIdMatch[1].split("_");
410
- if (parts.length < 3 || parts[0] !== "xiaoyi")
411
- return null;
412
- return { taskId: parts[1], deviceType: parts[2] };
413
- }
414
396
  export const xiaoyiProvider = {
415
397
  id: "xiaoyiprovider",
416
398
  label: "Xiaoyi Provider",
@@ -424,7 +406,12 @@ export const xiaoyiProvider = {
424
406
  * xiaoyiprovider as long as the provider has a configured baseUrl.
425
407
  */
426
408
  resolveDynamicModel: (ctx) => {
427
- const baseUrl = ctx.providerConfig?.baseUrl;
409
+ // providerConfig from models.providers.xiaoyiprovider is preferred;
410
+ // fall back to zai baseUrl when config-driven provider setup is unavailable.
411
+ let baseUrl = ctx.providerConfig?.baseUrl;
412
+ if (!baseUrl || typeof baseUrl !== "string") {
413
+ baseUrl = ctx.config?.models?.providers?.zai?.baseUrl;
414
+ }
428
415
  if (!baseUrl || typeof baseUrl !== "string")
429
416
  return null;
430
417
  return {
@@ -474,35 +461,10 @@ export const xiaoyiProvider = {
474
461
  return underlying;
475
462
  return async (model, context, options) => {
476
463
  const dynamicHeaders = {};
477
- // ── Extract A2A taskId/deviceType from Conversation info ──
478
- // bot.ts stores taskId_deviceType as MessageSid, which the framework
479
- // renders as message_id in the Conversation info JSON block.
480
- let extractedTaskId = null;
481
- let extractedDeviceType = null;
482
- if (context.messages) {
483
- for (let i = context.messages.length - 1; i >= 0; i--) {
484
- const msg = context.messages[i];
485
- if (msg.role !== "user")
486
- continue;
487
- const text = typeof msg.content === "string"
488
- ? msg.content
489
- : Array.isArray(msg.content)
490
- ? msg.content.find((b) => b.type === "text")?.text ?? ""
491
- : "";
492
- if (!text)
493
- continue;
494
- const extracted = extractA2AFromConversationInfo(text);
495
- if (extracted) {
496
- extractedTaskId = extracted.taskId;
497
- extractedDeviceType = extracted.deviceType;
498
- break;
499
- }
500
- }
501
- }
502
464
  // ── Build dynamic headers ────────────────────────────
503
465
  // Priority:
504
466
  // 1. Cron-triggered: uid → cronUuid, with cron-specific headers
505
- // 2. Xiaoyi A2A: taskId extracted from Conversation info (xiaoyi_ prefix)
467
+ // 2. Xiaoyi A2A: rawTaskId from AsyncLocalStorage, split on "&"
506
468
  // 3. UID-based fallback: sha256(uid).hex[:32]_timestamp
507
469
  const isCron = isCronTriggered(context.messages);
508
470
  if (isCron) {
@@ -533,21 +495,32 @@ export const xiaoyiProvider = {
533
495
  dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
534
496
  if (context.messages?.length === 1)
535
497
  dynamicHeaders["x-cron-flag"] = "begin";
536
- }
537
- else if (extractedTaskId) {
538
- const sessionId = extractedTaskId.split("&")[0];
539
- const interactionId = extractedTaskId.split("&")[1] ?? "";
540
- dynamicHeaders[HEADER_TRACE_ID] = extractedTaskId;
541
- dynamicHeaders[HEADER_SESSION_ID] = sessionId;
542
- dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
498
+ logger.log(`[ALS-PROOF] provider headers source=cron`);
543
499
  }
544
500
  else {
545
- const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
546
- if (typeof fallbackPrefix === "string") {
547
- const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
548
- dynamicHeaders[HEADER_TRACE_ID] = fallbackValue;
549
- dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
550
- dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
501
+ // ALS path: rawTaskId comes from the per-turn AsyncLocalStorage scope
502
+ // set by bot.ts runWithSessionContext. Real upstream params.id is
503
+ // `realSession&realInteraction`; tester UUIDs degenerate to the whole
504
+ // id as sessionId with empty interactionId.
505
+ const als = getCurrentSessionContext();
506
+ const rawTaskId = als?.taskId;
507
+ if (rawTaskId) {
508
+ const sessionId = rawTaskId.split("&")[0];
509
+ const interactionId = rawTaskId.split("&")[1] ?? "";
510
+ dynamicHeaders[HEADER_TRACE_ID] = rawTaskId;
511
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
512
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
513
+ logger.log(`[ALS-PROOF] provider headers source=ALS traceId=${rawTaskId} sessionId=${sessionId} interactionId=${interactionId}`);
514
+ }
515
+ else {
516
+ const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
517
+ if (typeof fallbackPrefix === "string") {
518
+ const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
519
+ dynamicHeaders[HEADER_TRACE_ID] = fallbackValue;
520
+ dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
521
+ dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
522
+ }
523
+ logger.log(`[ALS-PROOF] provider headers source=uid-fallback (ALS miss)`);
551
524
  }
552
525
  }
553
526
  // 记录输入
@@ -560,10 +533,8 @@ export const xiaoyiProvider = {
560
533
  if (context.systemPrompt) {
561
534
  logger.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
562
535
  }
563
- // deviceType: prefer value extracted from Conversation info,
564
- // then ALS fallback.
565
- const deviceType = extractedDeviceType
566
- ?? getCurrentSessionContext()?.deviceType;
536
+ // deviceType: read from ALS (no more text-extraction).
537
+ const deviceType = getCurrentSessionContext()?.deviceType;
567
538
  // 在发送给模型前,优化 systemPrompt 结构
568
539
  if (context.systemPrompt) {
569
540
  let sp = context.systemPrompt;