@ynhcj/xiaoyi-channel 0.0.214-next → 0.0.215-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.
@@ -18,6 +18,12 @@ interface CompactionSessionSnapshot {
18
18
  deviceType?: string;
19
19
  appVer?: string;
20
20
  sdkApiVersion?: string;
21
+ /** Auth headers from options.headers in the last normal request. */
22
+ requestHeaders?: Record<string, string>;
23
+ /** API key from the last normal request (for Bearer token auth). */
24
+ apiKey?: string;
25
+ /** Model-level headers from resolveDynamicModel (streamSimple adds these). */
26
+ modelHeaders?: Record<string, string>;
21
27
  }
22
28
  /** Store config captured from prepareExtraParams so summarize() can read it. */
23
29
  export declare function setCompactionConfig(config: CompactionConfig | null): void;
@@ -123,8 +123,20 @@ export const xiaoyiCompactionProvider = {
123
123
  }
124
124
  promptText += `\n\n${instructions}`;
125
125
  // ── Build request ─────────────────────────────────────────────
126
+ // Reconstruct the exact same headers that the normal request uses.
127
+ // streamSimple adds model.headers to the HTTP request directly, so
128
+ // we capture them separately from options.headers.
129
+ const reqHdrs = session?.requestHeaders;
130
+ const modelHdrs = session?.modelHeaders;
131
+ const reqKeys = reqHdrs ? Object.keys(reqHdrs) : [];
132
+ const modelKeys = modelHdrs ? Object.keys(modelHdrs) : [];
133
+ logger.log(`[compaction-provider] snapshot auth: hasApiKey=${!!session?.apiKey} ` +
134
+ `requestHeaders=[${reqKeys.join(",")}] modelHeaders=[${modelKeys.join(",")}]`);
126
135
  const headers = {
127
136
  "Content-Type": "application/json",
137
+ ...modelHdrs,
138
+ ...reqHdrs,
139
+ ...(session?.apiKey ? { Authorization: `Bearer ${session.apiKey}` } : {}),
128
140
  [HEADER_TRACE_ID]: traceId,
129
141
  [HEADER_SESSION_ID]: sessionId,
130
142
  [HEADER_INTERACTION_ID]: interactionId,
@@ -138,9 +150,6 @@ export const xiaoyiCompactionProvider = {
138
150
  if (session?.sdkApiVersion) {
139
151
  headers["x-sdk-api-version"] = session.sdkApiVersion;
140
152
  }
141
- if (cfg.apiKey) {
142
- headers["Authorization"] = `Bearer ${cfg.apiKey}`;
143
- }
144
153
  const body = {
145
154
  model: cfg.modelName,
146
155
  messages: [
@@ -151,7 +160,17 @@ export const xiaoyiCompactionProvider = {
151
160
  temperature: 0.3,
152
161
  };
153
162
  const url = `${cfg.baseUrl.replace(/\/+$/, "")}/chat/completions`;
154
- logger.log(`[compaction-provider] POST ${url}`);
163
+ // Log headers for diagnostics (redact sensitive values)
164
+ const safeHeaders = {};
165
+ for (const [key, value] of Object.entries(headers)) {
166
+ if (key.toLowerCase().includes("auth") || key.toLowerCase().includes("api-key")) {
167
+ safeHeaders[key] = value ? `${value.slice(0, 4)}***` : "(empty)";
168
+ }
169
+ else {
170
+ safeHeaders[key] = value;
171
+ }
172
+ }
173
+ logger.log(`[compaction-provider] POST ${url} headers=${JSON.stringify(safeHeaders)}`);
155
174
  // ── Call ──────────────────────────────────────────────────────
156
175
  let response;
157
176
  try {
@@ -110,7 +110,38 @@ export async function handleCronQueryEvent(context, cfg) {
110
110
  // ── list ──────────────────────────────────────────────────────
111
111
  case "list": {
112
112
  const gatewayParams = buildListParams(params);
113
- const gatewayResult = await callGatewayTool("cron.list", { timeoutMs: GATEWAY_TIMEOUT_MS }, gatewayParams);
113
+ let gatewayResult = await callGatewayTool("cron.list", { timeoutMs: GATEWAY_TIMEOUT_MS }, gatewayParams);
114
+ // Scan for systemEvent jobs and migrate them to agentTurn
115
+ // Skip main-session jobs — gateway requires payload.kind="systemEvent" for those
116
+ const jobs = gatewayResult?.jobs ?? [];
117
+ const systemEventJobs = jobs.filter((j) => j?.payload?.kind === "systemEvent");
118
+ if (systemEventJobs.length > 0) {
119
+ log.log(`[CRON-QUERY] list: found ${systemEventJobs.length} systemEvent jobs, migrating to agentTurn`);
120
+ for (const job of systemEventJobs) {
121
+ const updatePatch = {
122
+ sessionTarget: "isolated",
123
+ description: job.payload?.text ?? job.payload?.message ?? "",
124
+ payload: {
125
+ kind: "agentTurn",
126
+ message: job.payload?.text ?? job.payload?.message ?? "",
127
+ },
128
+ delivery: {
129
+ channel: "xiaoyi-channel",
130
+ mode: "announce",
131
+ to: "default"
132
+ }
133
+ };
134
+ try {
135
+ await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, { id: job.id, patch: updatePatch });
136
+ log.log(`[CRON-QUERY] list: migrated job ${job.id} (${job.name ?? ""}) from systemEvent → agentTurn`);
137
+ }
138
+ catch (err) {
139
+ log.warn(`[CRON-QUERY] list: failed to migrate job ${job.id}:`, err);
140
+ }
141
+ }
142
+ // Re-fetch list after migration
143
+ gatewayResult = await callGatewayTool("cron.list", { timeoutMs: GATEWAY_TIMEOUT_MS }, gatewayParams);
144
+ }
114
145
  result = transformListResponse(gatewayResult, params);
115
146
  break;
116
147
  }
@@ -255,8 +286,7 @@ function buildUpdateParams(jobId, params) {
255
286
  const patch = params?.patch ?? params ?? {};
256
287
  return {
257
288
  id: jobId,
258
- patch,
259
- description: patch?.payload?.message,
289
+ patch
260
290
  };
261
291
  }
262
292
  // ============================================================================
@@ -300,6 +330,7 @@ function transformAddResponse(gatewayResult) {
300
330
  id: gatewayResult?.id,
301
331
  name: gatewayResult?.name,
302
332
  enabled: gatewayResult?.enabled,
333
+ description: gatewayResult?.payload?.message,
303
334
  createdAtMs: gatewayResult?.createdAtMs,
304
335
  updatedAtMs: gatewayResult?.updatedAtMs,
305
336
  schedule: gatewayResult?.schedule,
@@ -376,32 +407,23 @@ async function persistCronPushMap(sessionId, result) {
376
407
  async function queryTimeListFromGateway(params) {
377
408
  // Fetch from local runs folder and gateway RPC in parallel
378
409
  const [localEntries, rpcResult] = await Promise.all([
379
- readAllLocalRunLogs().catch((err) => {
380
- logger.error(`[CRON-QUERY] readAllLocalRunLogs failed:`, err);
381
- return [];
382
- }),
410
+ readAllLocalRunLogs().catch(() => []),
383
411
  callGatewayTool("cron.runs", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
384
412
  scope: "all",
385
413
  limit: 200,
386
414
  sortDir: "desc",
387
415
  ...(params ?? {}),
388
- }).catch((err) => {
389
- logger.error(`[CRON-QUERY] gateway cron.runs RPC failed:`, err);
390
- return { entries: [] };
391
- }),
416
+ }).catch(() => ({ entries: [] })),
392
417
  ]);
393
418
  const gatewayEntries = rpcResult?.entries ?? [];
394
- logger.log(`[CRON-QUERY] queryTimeList: local=${localEntries.length}, gateway=${gatewayEntries.length}`);
395
419
  // Merge and deduplicate
396
420
  const merged = mergeAndDedupeRunEntries(localEntries, gatewayEntries);
397
- logger.log(`[CRON-QUERY] queryTimeList: merged=${merged.length} (after dedup)`);
398
421
  if (merged.length === 0) {
399
422
  return [];
400
423
  }
401
424
  // Filter to last 7 days
402
425
  const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
403
426
  const recent = merged.filter((e) => e.ts && e.ts >= sevenDaysAgo);
404
- logger.log(`[CRON-QUERY] queryTimeList: recent=${recent.length} (within 7 days, threshold=${new Date(sevenDaysAgo).toISOString()})`);
405
427
  // Ensure .name is populated (gateway returns jobName; normalize to .name)
406
428
  for (const run of recent) {
407
429
  if (!run.name && run.jobName) {
@@ -479,7 +501,6 @@ function extractJobIdFromRunLogName(name) {
479
501
  * may have renamed the file to .jsonl.migrated.
480
502
  */
481
503
  async function readLocalRunLogsForJob(jobId) {
482
- logger.log(`[CRON-QUERY] readLocalRunLogsForJob: jobId=${jobId}, dir=${LEGACY_CRON_RUNS_DIR}`);
483
504
  // Try active file first, then archived variants
484
505
  const candidates = [
485
506
  `${jobId}.jsonl`,
@@ -490,13 +511,11 @@ async function readLocalRunLogsForJob(jobId) {
490
511
  try {
491
512
  const raw = await fsp.readFile(filePath, "utf-8");
492
513
  if (raw.trim()) {
493
- const parsed = parseCronRunLogEntriesFromJsonl(raw, { jobId });
494
- logger.log(`[CRON-QUERY] readLocalRunLogsForJob: found ${candidate}, bytes=${raw.length}, entries=${parsed.length}`);
495
- return parsed;
514
+ return parseCronRunLogEntriesFromJsonl(raw, { jobId });
496
515
  }
497
516
  }
498
517
  catch {
499
- logger.log(`[CRON-QUERY] readLocalRunLogsForJob: ${candidate} not found`);
518
+ // Try next candidate
500
519
  }
501
520
  }
502
521
  // Also scan for .migrated.N variants
@@ -508,9 +527,7 @@ async function readLocalRunLogsForJob(jobId) {
508
527
  try {
509
528
  const raw = await fsp.readFile(path.join(LEGACY_CRON_RUNS_DIR, entry.name), "utf-8");
510
529
  if (raw.trim()) {
511
- const parsed = parseCronRunLogEntriesFromJsonl(raw, { jobId });
512
- logger.log(`[CRON-QUERY] readLocalRunLogsForJob: found ${entry.name}, entries=${parsed.length}`);
513
- return parsed;
530
+ return parseCronRunLogEntriesFromJsonl(raw, { jobId });
514
531
  }
515
532
  }
516
533
  catch {
@@ -522,7 +539,6 @@ async function readLocalRunLogsForJob(jobId) {
522
539
  catch {
523
540
  // Directory listing failed, give up
524
541
  }
525
- logger.log(`[CRON-QUERY] readLocalRunLogsForJob: no local run-log files found for jobId=${jobId}`);
526
542
  return [];
527
543
  }
528
544
  /**
@@ -533,29 +549,23 @@ async function readLocalRunLogsForJob(jobId) {
533
549
  */
534
550
  async function readAllLocalRunLogs() {
535
551
  try {
536
- logger.log(`[CRON-QUERY] readAllLocalRunLogs: scanning ${LEGACY_CRON_RUNS_DIR}`);
537
552
  const entries = await fsp.readdir(LEGACY_CRON_RUNS_DIR, { withFileTypes: true });
538
553
  const runLogFiles = entries.filter((e) => e.isFile() && isRunLogFile(e.name));
539
- logger.log(`[CRON-QUERY] readAllLocalRunLogs: total dir entries=${entries.length}, runLogFiles=${runLogFiles.length}` +
540
- (runLogFiles.length > 0 ? ` files=[${runLogFiles.map(f => f.name).join(", ")}]` : ""));
541
554
  const allEntries = [];
542
555
  for (const file of runLogFiles) {
543
556
  const jobId = extractJobIdFromRunLogName(file.name);
544
557
  try {
545
558
  const raw = await fsp.readFile(path.join(LEGACY_CRON_RUNS_DIR, file.name), "utf-8");
546
559
  const parsed = parseCronRunLogEntriesFromJsonl(raw, { jobId });
547
- logger.log(`[CRON-QUERY] readAllLocalRunLogs: file=${file.name} jobId=${jobId} bytes=${raw.length} entries=${parsed.length}`);
548
560
  allEntries.push(...parsed);
549
561
  }
550
- catch (err) {
551
- logger.warn(`[CRON-QUERY] readAllLocalRunLogs: failed to read ${file.name}:`, err);
562
+ catch {
563
+ // Skip unreadable files
552
564
  }
553
565
  }
554
- logger.log(`[CRON-QUERY] readAllLocalRunLogs: total local entries=${allEntries.length}`);
555
566
  return allEntries;
556
567
  }
557
- catch (err) {
558
- logger.warn(`[CRON-QUERY] readAllLocalRunLogs: failed to scan runs dir:`, err);
568
+ catch {
559
569
  return [];
560
570
  }
561
571
  }
@@ -606,6 +606,16 @@ export const xiaoyiProvider = {
606
606
  logger.log(`[ALS-PROOF] provider headers source=uid-fallback (ALS miss)`);
607
607
  }
608
608
  }
609
+ // Mutate model.headers so the LLM compaction path picks up trace context.
610
+ // The LLM path (summarizeViaLLM → summarizeInStages) bypasses wrapStreamFn
611
+ // but reuses the same model object, so mutating model.headers here ensures
612
+ // x-hag-trace-id et al are forwarded even when the custom compaction
613
+ // provider is not reached (e.g. queued compaction with different sessionManager).
614
+ if (model?.headers && dynamicHeaders[HEADER_TRACE_ID]) {
615
+ model.headers[HEADER_TRACE_ID] = dynamicHeaders[HEADER_TRACE_ID];
616
+ model.headers[HEADER_SESSION_ID] = dynamicHeaders[HEADER_SESSION_ID];
617
+ model.headers[HEADER_INTERACTION_ID] = dynamicHeaders[HEADER_INTERACTION_ID];
618
+ }
609
619
  // 记录输入
610
620
  logger.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
611
621
  if (context.systemPrompt) {
@@ -628,9 +638,32 @@ export const xiaoyiProvider = {
628
638
  // During compaction, getCurrentSessionContext() returns null because
629
639
  // compaction runs outside the original A2A async scope. By storing
630
640
  // the snapshot from the last normal request, the CompactionProvider
631
- // can reuse the same A2A traceId/sessionId/devicetype in its
632
- // summarization API call.
641
+ // can reuse the same A2A traceId/sessionId/devicetype and auth
642
+ // headers in its summarization API call.
633
643
  if (dynamicHeaders[HEADER_TRACE_ID]) {
644
+ const requestHeaders = {};
645
+ if (options?.headers) {
646
+ for (const [key, value] of Object.entries(options.headers)) {
647
+ if (typeof value === "string") {
648
+ requestHeaders[key] = value;
649
+ }
650
+ }
651
+ }
652
+ // model.headers comes from resolveDynamicModel — streamSimple
653
+ // reads these directly when building the HTTP request.
654
+ const modelHeaders = {};
655
+ if (model?.headers) {
656
+ for (const [key, value] of Object.entries(model.headers)) {
657
+ if (typeof value === "string") {
658
+ modelHeaders[key] = value;
659
+ }
660
+ }
661
+ }
662
+ const apiKey = typeof options?.apiKey === "string" ? options.apiKey : undefined;
663
+ logger.log(`[compaction-provider] capturing snapshot: hasApiKey=${!!apiKey} ` +
664
+ `requestHeaders=[${Object.keys(requestHeaders).join(",")}] ` +
665
+ `modelHeaders=[${Object.keys(modelHeaders).join(",")}] ` +
666
+ `traceId=${dynamicHeaders[HEADER_TRACE_ID]}`);
634
667
  setCompactionSessionSnapshot({
635
668
  traceId: dynamicHeaders[HEADER_TRACE_ID] ?? "",
636
669
  sessionId: dynamicHeaders[HEADER_SESSION_ID] ?? "",
@@ -638,6 +671,9 @@ export const xiaoyiProvider = {
638
671
  deviceType: deviceType ?? undefined,
639
672
  appVer: appVer ?? undefined,
640
673
  sdkApiVersion: sdkApiVersion ?? undefined,
674
+ requestHeaders: Object.keys(requestHeaders).length > 0 ? requestHeaders : undefined,
675
+ modelHeaders: Object.keys(modelHeaders).length > 0 ? modelHeaders : undefined,
676
+ apiKey: apiKey ?? undefined,
641
677
  });
642
678
  }
643
679
  // 在发送给模型前,优化 systemPrompt 结构
@@ -16,7 +16,6 @@ export declare const callPhoneTool: {
16
16
  slotId: {
17
17
  type: string;
18
18
  description: string;
19
- default: number;
20
19
  };
21
20
  };
22
21
  required: string[];
@@ -9,7 +9,7 @@ import { logger } from "../utils/logger.js";
9
9
  export const callPhoneTool = {
10
10
  name: "call_phone",
11
11
  label: "Call Phone",
12
- description: "拨打电话。需要提供要拨打的电话号码。slotId参数可选,默认为0(主卡),如果用户明确要求使用副卡则设置为1。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。",
12
+ description: "拨打电话。需要提供要拨打的电话号码。slotId参数可选,如果用户明确指定为主卡为0(主卡),如果用户明确要求使用副卡则设置为1,如果用户没有指定使用哪张卡,则此参数不填。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。",
13
13
  parameters: {
14
14
  type: "object",
15
15
  properties: {
@@ -19,8 +19,7 @@ export const callPhoneTool = {
19
19
  },
20
20
  slotId: {
21
21
  type: "number",
22
- description: "SIM卡槽ID,默认为0(主卡),设置为1表示副卡。仅当用户明确要求使用副卡时才设置为1",
23
- default: 0,
22
+ description: "SIM卡槽ID,仅当用户明确指定主卡/副卡时才设置,否则不传",
24
23
  },
25
24
  },
26
25
  required: ["phoneNumber"],
@@ -32,10 +31,8 @@ export const callPhoneTool = {
32
31
  if (!params.phoneNumber || typeof params.phoneNumber !== "string" || params.phoneNumber.trim() === "") {
33
32
  throw new Error("Missing required parameter: phoneNumber must be a non-empty string");
34
33
  }
35
- // Set default slotId if not provided
36
- const slotId = params.slotId !== undefined && params.slotId !== null ? params.slotId : 0;
37
- // Validate slotId (must be 0 or 1)
38
- if (slotId !== 0 && slotId !== 1) {
34
+ // Validate slotId if provided (must be 0 or 1)
35
+ if (params.slotId !== undefined && params.slotId !== null && params.slotId !== 0 && params.slotId !== 1) {
39
36
  throw new Error("Invalid slotId: must be 0 (primary SIM) or 1 (secondary SIM)");
40
37
  }
41
38
  // Get WebSocket manager
@@ -58,7 +55,7 @@ export const callPhoneTool = {
58
55
  timeOut: 5,
59
56
  intentParam: {
60
57
  phoneNumber: params.phoneNumber.trim(),
61
- slotId: slotId,
58
+ ...(params.slotId !== undefined && params.slotId !== null ? { slotId: params.slotId } : {}),
62
59
  },
63
60
  permissionId: [],
64
61
  achieveType: "INTENT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.214-next",
3
+ "version": "0.0.215-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",