shennian 0.2.87 → 0.2.89

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 (73) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +13 -0
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/src/agents/adapter.d.ts +6 -0
  4. package/dist/src/agents/codex-control.d.ts +35 -0
  5. package/dist/src/agents/codex-control.js +188 -0
  6. package/dist/src/agents/codex-utils.d.ts +5 -0
  7. package/dist/src/agents/codex-utils.js +5 -0
  8. package/dist/src/agents/codex.d.ts +8 -0
  9. package/dist/src/agents/codex.js +55 -2
  10. package/dist/src/agents/model-registry/discovery.js +2 -1
  11. package/dist/src/channels/base.d.ts +4 -13
  12. package/dist/src/channels/runtime.d.ts +1 -3
  13. package/dist/src/channels/runtime.js +32 -5
  14. package/dist/src/channels/secret-registry.d.ts +1 -4
  15. package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
  16. package/dist/src/channels/wechat-channel/anchor.js +65 -0
  17. package/dist/src/channels/wechat-channel/client.d.ts +74 -0
  18. package/dist/src/channels/wechat-channel/client.js +96 -0
  19. package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
  20. package/dist/src/channels/wechat-channel/cooldown.js +38 -0
  21. package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
  22. package/dist/src/channels/wechat-channel/fingerprint.js +71 -0
  23. package/dist/src/channels/wechat-channel/helper-assets.d.ts +28 -0
  24. package/dist/src/channels/wechat-channel/helper-assets.js +68 -0
  25. package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
  26. package/dist/src/channels/wechat-channel/helper-client.js +149 -0
  27. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
  28. package/dist/src/channels/wechat-channel/helper-protocol.js +115 -0
  29. package/dist/src/channels/wechat-channel/index.d.ts +16 -0
  30. package/dist/src/channels/wechat-channel/index.js +19 -0
  31. package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
  32. package/dist/src/channels/wechat-channel/ledger.js +54 -0
  33. package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
  34. package/dist/src/channels/wechat-channel/media-resolver.js +181 -0
  35. package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
  36. package/dist/src/channels/wechat-channel/message-key.js +105 -0
  37. package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
  38. package/dist/src/channels/wechat-channel/observer.js +118 -0
  39. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +66 -0
  40. package/dist/src/channels/wechat-channel/outbound-ledger.js +112 -0
  41. package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
  42. package/dist/src/channels/wechat-channel/preflight.js +48 -0
  43. package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
  44. package/dist/src/channels/wechat-channel/runner.js +84 -0
  45. package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
  46. package/dist/src/channels/wechat-channel/runtime.js +66 -0
  47. package/dist/src/channels/wechat-channel/scheduler.d.ts +30 -0
  48. package/dist/src/channels/wechat-channel/scheduler.js +152 -0
  49. package/dist/src/channels/wechat-rpa/macos-flow.d.ts +0 -28
  50. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -134
  51. package/dist/src/channels/wechat-rpa.d.ts +21 -0
  52. package/dist/src/channels/wechat-rpa.js +39 -61
  53. package/dist/src/commands/manager.d.ts +1 -1
  54. package/dist/src/commands/manager.js +5 -10
  55. package/dist/src/fs/text-decoder.d.ts +10 -0
  56. package/dist/src/fs/text-decoder.js +110 -0
  57. package/dist/src/manager/runtime.js +4 -6
  58. package/dist/src/native-fusion/service.d.ts +10 -0
  59. package/dist/src/native-fusion/service.js +27 -0
  60. package/dist/src/session/handlers/chat.js +18 -2
  61. package/dist/src/session/handlers/fs.js +39 -3
  62. package/dist/src/session/handlers/session-refresh.js +12 -0
  63. package/dist/src/session/handlers/tool-detail.d.ts +3 -0
  64. package/dist/src/session/handlers/tool-detail.js +218 -0
  65. package/dist/src/session/manager.d.ts +3 -0
  66. package/dist/src/session/manager.js +58 -0
  67. package/dist/src/session/types.d.ts +4 -0
  68. package/package.json +2 -2
  69. package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
  70. package/dist/scripts/wechat-rpa-win-visual.mjs +0 -1735
  71. package/dist/scripts/wechat-rpa-win.mjs +0 -352
  72. package/dist/src/channels/wechat-rpa/windows-visual-flow.d.ts +0 -40
  73. package/dist/src/channels/wechat-rpa/windows-visual-flow.js +0 -189
@@ -6,9 +6,9 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { ChannelSecretRegistry } from './secret-registry.js';
9
+ import { resolveShennianPath } from '../config/index.js';
9
10
  import { probeMacWeChat, observedMessageFromProbe } from './wechat-rpa/macos.js';
10
11
  import { runMacWeChatRpaFlow } from './wechat-rpa/macos-flow.js';
11
- import { runWindowsWeChatRpaVisualFlow } from './wechat-rpa/windows-visual-flow.js';
12
12
  import { normalizeWeChatRpaMessage, WeChatRpaDeduper, weChatRpaConversationId, } from './wechat-rpa/normalizer.js';
13
13
  const DEFAULT_POLL_INTERVAL_MS = 5_000;
14
14
  const DEFAULT_RECENT_LIMIT = 5;
@@ -159,7 +159,6 @@ export class WeChatRpaChannelAdapter {
159
159
  noteManualReview(conn, pendingKey, reason);
160
160
  return { status: 'manual-review', reason };
161
161
  }
162
- recordCloudOcrRuntime(conn, flow);
163
162
  addTaskSummary(conn, {
164
163
  status: 'sent',
165
164
  runId: conn.lastRunId ?? null,
@@ -225,11 +224,6 @@ export class WeChatRpaChannelAdapter {
225
224
  wechatRpaLastTracePath: conn.lastTracePath ?? null,
226
225
  wechatRpaLastTraceSummary: conn.lastTraceSummary ?? null,
227
226
  wechatRpaRecentTaskSummaries: conn.recentTaskSummaries,
228
- wechatRpaLastCloudOcrAt: conn.lastCloudOcrAt ?? null,
229
- wechatRpaLastCloudOcrPurpose: conn.lastCloudOcrPurpose ?? null,
230
- wechatRpaLastCloudOcrRequestId: conn.lastCloudOcrRequestId ?? null,
231
- wechatRpaLastCloudOcrImageHash: conn.lastCloudOcrImageHash ?? null,
232
- wechatRpaLastCloudOcrUsage: conn.lastCloudOcrUsage ?? null,
233
227
  };
234
228
  }
235
229
  ensureConnection(config) {
@@ -284,7 +278,6 @@ export class WeChatRpaChannelAdapter {
284
278
  conn.runtimeState = 'syncing';
285
279
  const observed = await this.readObservedMessages(conn.config, secret, (flow) => {
286
280
  recordTraceRuntime(conn, flow);
287
- recordCloudOcrRuntime(conn, flow);
288
281
  if (!flow.interrupted)
289
282
  return;
290
283
  interrupted = true;
@@ -345,7 +338,6 @@ export class WeChatRpaChannelAdapter {
345
338
  persistPendingReplyState(conn);
346
339
  const flow = await runSendFlow(conn.config, secret, pending.conversationName, pending.skipText ? '' : pending.text, pending.attachmentPath, pending.key);
347
340
  recordTraceRuntime(conn, flow);
348
- recordCloudOcrRuntime(conn, flow);
349
341
  const partial = applyPartialSendProgress(flow, {
350
342
  text: pending.skipText ? '' : pending.text,
351
343
  attachmentPath: pending.attachmentPath,
@@ -446,16 +438,14 @@ function isWeChatRpaTextMentioned(text, aliases) {
446
438
  export function windowsVisualFlowHealth(secret, platform = process.platform) {
447
439
  if (platform !== 'win32')
448
440
  return { ok: false, message: 'WeChat RPA Windows visual flow requires Windows' };
449
- if (!configuredGroups({ type: 'wechat-rpa', groups: secret.groups, updatedAt: '' }).length) {
450
- return { ok: false, message: 'WeChat RPA Windows visual flow requires at least one group' };
451
- }
452
- return { ok: true, message: 'WeChat RPA Windows visual flow configured' };
441
+ void secret;
442
+ return { ok: false, message: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it' };
453
443
  }
454
444
  async function readLabMessages(config, secret, onFlow) {
455
445
  const api = await importWeChatRpaLabApi(config.workDir);
456
446
  const result = [];
457
447
  for (const name of configuredGroups(secret)) {
458
- const flow = await runLabReadFlow(api, secret, name);
448
+ const flow = await runLabReadFlow(api, config, secret, name);
459
449
  onFlow?.(flow);
460
450
  if (!flow.ok || flow.interrupted)
461
451
  continue;
@@ -467,7 +457,7 @@ async function readLabMessages(config, secret, onFlow) {
467
457
  }
468
458
  return result;
469
459
  }
470
- async function runLabReadFlow(api, secret, groupName) {
460
+ async function runLabReadFlow(api, config, secret, groupName) {
471
461
  const kind = selectWeChatRpaLabReadKind(secret.recentLimit);
472
462
  const task = {
473
463
  kind,
@@ -475,7 +465,7 @@ async function runLabReadFlow(api, secret, groupName) {
475
465
  targetGroup: groupName,
476
466
  policy: secret.forceForeground ? 'work' : 'polite',
477
467
  limit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 1, 50),
478
- readMode: secret.readMode === 'hybrid-vlm' ? 'hybrid-vlm' : 'local-ocr',
468
+ attachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
479
469
  };
480
470
  const result = await api.runWechatRpaReadLatest(task);
481
471
  const structuredMessages = Array.isArray(result.data?.structuredMessages) ? result.data.structuredMessages : [];
@@ -497,7 +487,6 @@ async function runLabReadFlow(api, secret, groupName) {
497
487
  recentMessages: flowMessages,
498
488
  newMessages: flowMessages,
499
489
  screenshotPath: result.tracePath,
500
- cloudOcrUsage: result.data?.hybrid?.usage,
501
490
  error: labResultError(result),
502
491
  };
503
492
  }
@@ -629,8 +618,7 @@ async function readMacFlowMessages(config, secret, onFlow) {
629
618
  noRestore: secret.noRestore !== false,
630
619
  idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
631
620
  recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
632
- downloadAttachmentsDir: resolveInboundAttachmentDir(config.workDir, secret),
633
- cloudOcrMode: 'off',
621
+ downloadAttachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
634
622
  });
635
623
  onFlow?.(flow);
636
624
  if (flow.interrupted)
@@ -644,26 +632,18 @@ async function readMacFlowMessages(config, secret, onFlow) {
644
632
  return result;
645
633
  }
646
634
  async function readWindowsVisualFlowMessages(config, secret, onFlow) {
647
- const result = [];
648
- for (const name of configuredGroups(secret)) {
649
- const flow = await runWindowsWeChatRpaVisualFlow({
650
- groupName: name,
651
- scriptPath: secret.flowScriptPath,
652
- workDir: config.workDir,
653
- recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
654
- downloadAttachmentsDir: resolveInboundAttachmentDir(config.workDir, secret),
655
- cloudOcrMode: 'off',
656
- });
657
- onFlow?.(flow);
658
- if (flow.interrupted)
659
- continue;
660
- for (const message of flow.newMessages ?? []) {
661
- const observed = flowMessageToObservedMessage(name, message, secret);
662
- if (observed)
663
- result.push(observed);
664
- }
665
- }
666
- return result;
635
+ void config;
636
+ const groupName = configuredGroups(secret)[0] || '<unbound>';
637
+ onFlow?.({
638
+ ok: false,
639
+ groupName,
640
+ interrupted: true,
641
+ reason: 'windows-visual-flow-archived',
642
+ newMessages: [],
643
+ recentMessages: [],
644
+ error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
645
+ });
646
+ return [];
667
647
  }
668
648
  function flowMessageToObservedMessage(conversationName, message, secret) {
669
649
  const text = String(message.text || '').trim();
@@ -684,16 +664,19 @@ function flowMessageToObservedMessage(conversationName, message, secret) {
684
664
  }
685
665
  async function runSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey) {
686
666
  if (secret.source === 'windows-visual-flow') {
687
- return runWindowsWeChatRpaVisualFlow({
667
+ void config;
668
+ void text;
669
+ void attachmentPath;
670
+ void dedupeKey;
671
+ return {
672
+ ok: false,
688
673
  groupName: conversationName,
689
- replyText: text || undefined,
690
- attachmentPath,
691
- scriptPath: secret.flowScriptPath,
692
- workDir: config.workDir,
693
- recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
694
- downloadAttachmentsDir: resolveInboundAttachmentDir(config.workDir, secret),
695
- cloudOcrMode: 'off',
696
- });
674
+ interrupted: true,
675
+ reason: 'windows-visual-flow-archived',
676
+ newMessages: [],
677
+ recentMessages: [],
678
+ error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
679
+ };
697
680
  }
698
681
  if (secret.source === 'wechat-rpa-lab') {
699
682
  return runLabSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey);
@@ -708,12 +691,11 @@ async function runSendFlow(config, secret, conversationName, text, attachmentPat
708
691
  noRestore: secret.noRestore !== false,
709
692
  idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
710
693
  recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
711
- downloadAttachmentsDir: resolveInboundAttachmentDir(config.workDir, secret),
712
- cloudOcrMode: 'off',
694
+ downloadAttachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
713
695
  });
714
696
  }
715
697
  function isFlowSource(source) {
716
- return source === 'macos-flow' || source === 'windows-visual-flow' || source === 'wechat-rpa-lab';
698
+ return source === 'macos-flow' || source === 'wechat-rpa-lab';
717
699
  }
718
700
  function applyPartialSendProgress(flow, input) {
719
701
  if (input.text && input.attachmentPath && flow.sentReplyObserved && !flow.sentAttachmentObserved) {
@@ -883,7 +865,6 @@ function noteInterruption(conn, flow, reason) {
883
865
  conn.interruptionCooldownUntil = Date.now() + INTERRUPTION_COOLDOWN_MS;
884
866
  conn.runtimeState = 'cooldown';
885
867
  }
886
- recordCloudOcrRuntime(conn, flow);
887
868
  addTaskSummary(conn, {
888
869
  status: 'interrupted',
889
870
  runId: flow.rpaRunId || conn.lastRunId || null,
@@ -1016,10 +997,14 @@ function safeFileName(name) {
1016
997
  .replace(/^[ ._]+|[ ._]+$/g, '')
1017
998
  || 'attachment';
1018
999
  }
1019
- function resolveInboundAttachmentDir(workDir, secret) {
1000
+ export function resolveWeChatRpaInboundAttachmentDir(config, secret) {
1020
1001
  if (secret.downloadAttachments === false)
1021
1002
  return undefined;
1022
- return secret.downloadAttachmentsDir?.trim() || path.join(workDir, '.uploads', 'wechat-rpa', 'inbound');
1003
+ const explicitDir = secret.downloadAttachmentsDir?.trim();
1004
+ if (explicitDir)
1005
+ return path.resolve(explicitDir);
1006
+ const channelKey = crypto.createHash('sha256').update(config.id).digest('hex').slice(0, 16);
1007
+ return resolveShennianPath('wechat-rpa', 'attachments', 'inbound', channelKey);
1023
1008
  }
1024
1009
  export function annotateWeChatRpaInboundAttachments(attachments) {
1025
1010
  if (!Array.isArray(attachments))
@@ -1041,10 +1026,3 @@ function clampNumber(value, fallback, min, max) {
1041
1026
  return fallback;
1042
1027
  return Math.min(max, Math.max(min, number));
1043
1028
  }
1044
- function recordCloudOcrRuntime(conn, flow) {
1045
- void conn;
1046
- void flow;
1047
- // WeChat RPA production routing is local OCR only. Legacy cloud OCR payload
1048
- // fields may still appear in old fixtures, but the channel must not publish
1049
- // them as runtime status or invite a server OCR dependency back in.
1050
- }
@@ -1,2 +1,2 @@
1
- import { type Command } from 'commander';
1
+ import type { Command } from 'commander';
2
2
  export declare function registerManagerCommand(program: Command): void;
@@ -2,7 +2,6 @@
2
2
  // @test src/__tests__/manager-runtime.test.ts
3
3
  // @test src/__tests__/manager-command.test.ts
4
4
  import fs from 'node:fs';
5
- import { Option } from 'commander';
6
5
  import chalk from 'chalk';
7
6
  import { readExternalAttachment } from './external-attachments.js';
8
7
  function requireManagerContext() {
@@ -75,9 +74,6 @@ function printWeChatRpaStatus(channel) {
75
74
  channel.wechatRpaLastRunAt ? `lastRun=${String(channel.wechatRpaLastRunAt)}` : '',
76
75
  channel.wechatRpaLastMessageAt ? `lastMessage=${String(channel.wechatRpaLastMessageAt)}` : '',
77
76
  channel.wechatRpaLastInterruptedAt ? `lastInterrupted=${String(channel.wechatRpaLastInterruptedAt)}` : '',
78
- channel.wechatRpaLastCloudOcrAt ? `lastLocalOcr=${String(channel.wechatRpaLastCloudOcrAt)}` : '',
79
- channel.wechatRpaLastCloudOcrPurpose ? `lastLocalOcrPurpose=${String(channel.wechatRpaLastCloudOcrPurpose)}` : '',
80
- channel.wechatRpaLastCloudOcrRequestId ? `lastLocalOcrRequestId=${String(channel.wechatRpaLastCloudOcrRequestId)}` : '',
81
77
  channel.wechatRpaLastError ? `lastError=${String(channel.wechatRpaLastError)}` : 'lastError=none',
82
78
  ].filter(Boolean);
83
79
  console.log(fields.join('\n'));
@@ -353,20 +349,18 @@ export function registerManagerCommand(program) {
353
349
  .option('--id <id>', 'Channel id')
354
350
  .option('--name <name>', 'Channel display name')
355
351
  .requiredOption('--enabled <true|false>', 'Whether the channel should be enabled')
356
- .option('--group <name>', 'Bound WeChat group name; pass once per conversation', collect, [])
352
+ .option('--group <name>', 'Bound WeChat conversation name; pass once per Shennian conversation', collect, [])
357
353
  .option('--can-reply <true|false>', 'Whether reply should be allowed')
358
- .option('--source <macos-flow|windows-visual-flow|wechat-rpa-lab|macos-probe|fixture-jsonl>', 'WeChat RPA implementation source')
354
+ .option('--source <macos-flow|wechat-rpa-lab|macos-probe|fixture-jsonl>', 'WeChat RPA implementation source')
359
355
  .option('--poll-interval-ms <n>', 'Polling interval in milliseconds')
360
- .option('--recent-limit <n>', 'Recent message OCR debug limit')
356
+ .option('--recent-limit <n>', 'Recent message limit')
361
357
  .option('--idle-seconds <n>', 'Minimum user idle seconds before foreground automation')
362
358
  .option('--force-foreground <true|false>', 'Force WeChat foreground while syncing')
363
359
  .option('--restore-previous <true|false>', 'Restore previous foreground app after syncing')
364
360
  .option('--download-attachments <true|false>', 'Click and localize inbound attachment candidates')
365
361
  .option('--download-attachments-dir <path>', 'Directory for localized inbound WeChat attachments')
362
+ .option('--privacy-consent <true|false>', 'Confirm WeChat channel data and privacy consent')
366
363
  .option('--flow-script-path <path>', 'Override macOS flow script path')
367
- .addOption(new Option('--cloud-ocr-url <url>', 'Deprecated; ignored because WeChat RPA uses local-only OCR').hideHelp())
368
- .addOption(new Option('--cloud-ocr-token <token>', 'Deprecated; ignored because WeChat RPA uses local-only OCR').hideHelp())
369
- .addOption(new Option('--cloud-ocr-mode <off|fallback|always>', 'Deprecated; ignored because WeChat RPA uses local-only OCR').hideHelp())
370
364
  .option('--json', 'Print JSON')
371
365
  .action(async (opts) => {
372
366
  if (opts.enabled === 'true' && opts.group.length > 1) {
@@ -386,6 +380,7 @@ export function registerManagerCommand(program) {
386
380
  noRestore: opts.restorePrevious === undefined ? undefined : opts.restorePrevious !== 'true',
387
381
  downloadAttachments: parseBool(opts.downloadAttachments),
388
382
  downloadAttachmentsDir: opts.downloadAttachmentsDir,
383
+ privacyConsentAccepted: parseBool(opts.privacyConsent),
389
384
  flowScriptPath: opts.flowScriptPath,
390
385
  });
391
386
  if (opts.json)
@@ -0,0 +1,10 @@
1
+ export declare class BinaryTextPreviewError extends Error {
2
+ constructor();
3
+ }
4
+ export type AutoTextDecodeResult = {
5
+ content: string;
6
+ encoding: 'utf8' | 'utf16le' | 'utf16be' | 'gb18030' | 'windows-1252';
7
+ fallback: boolean;
8
+ };
9
+ export declare function isAutoTextEncoding(value: string): boolean;
10
+ export declare function decodeTextBufferAuto(buffer: Buffer): AutoTextDecodeResult;
@@ -0,0 +1,110 @@
1
+ // @arch docs/architecture/cli/daemon.md#文件系统权限边界
2
+ // @test src/__tests__/session-manager.test.ts
3
+ import { TextDecoder } from 'node:util';
4
+ const AUTO_TEXT_ENCODINGS = new Set(['auto', 'utf8-auto', 'utf-8-auto', 'text-auto']);
5
+ const TEXT_SAMPLE_BYTES = 8192;
6
+ const MAX_SUSPICIOUS_CONTROL_RATIO = 0.01;
7
+ export class BinaryTextPreviewError extends Error {
8
+ constructor() {
9
+ super('File appears to be binary; use encoding=base64');
10
+ this.name = 'BinaryTextPreviewError';
11
+ }
12
+ }
13
+ export function isAutoTextEncoding(value) {
14
+ return AUTO_TEXT_ENCODINGS.has(value.trim().toLowerCase());
15
+ }
16
+ function startsWithBytes(buffer, bytes) {
17
+ if (buffer.length < bytes.length)
18
+ return false;
19
+ return bytes.every((byte, index) => buffer[index] === byte);
20
+ }
21
+ function isAllowedTextControlByte(byte) {
22
+ return byte === 0x09 || byte === 0x0a || byte === 0x0c || byte === 0x0d || byte === 0x1b;
23
+ }
24
+ function hasBinaryMagic(buffer) {
25
+ return (startsWithBytes(buffer, [0x25, 0x50, 0x44, 0x46]) || // PDF
26
+ startsWithBytes(buffer, [0x50, 0x4b, 0x03, 0x04]) || // ZIP / Office Open XML
27
+ startsWithBytes(buffer, [0x50, 0x4b, 0x05, 0x06]) ||
28
+ startsWithBytes(buffer, [0x50, 0x4b, 0x07, 0x08]) ||
29
+ startsWithBytes(buffer, [0x89, 0x50, 0x4e, 0x47]) || // PNG
30
+ startsWithBytes(buffer, [0xff, 0xd8, 0xff]) || // JPEG
31
+ startsWithBytes(buffer, [0x47, 0x49, 0x46, 0x38]) || // GIF
32
+ startsWithBytes(buffer, [0x52, 0x49, 0x46, 0x46]) || // RIFF / WebP / WAV / AVI
33
+ startsWithBytes(buffer, [0x1f, 0x8b]) || // gzip
34
+ startsWithBytes(buffer, [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]) || // 7z
35
+ startsWithBytes(buffer, [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07]) || // RAR
36
+ startsWithBytes(buffer, [0x7f, 0x45, 0x4c, 0x46]) || // ELF
37
+ startsWithBytes(buffer, [0xcf, 0xfa, 0xed, 0xfe]) || // Mach-O
38
+ startsWithBytes(buffer, [0xca, 0xfe, 0xba, 0xbe]) ||
39
+ startsWithBytes(buffer, [0x4d, 0x5a]) || // Windows PE
40
+ startsWithBytes(buffer, [0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66]));
41
+ }
42
+ function looksLikeBinaryBuffer(buffer) {
43
+ if (hasBinaryMagic(buffer))
44
+ return true;
45
+ const sample = buffer.subarray(0, Math.min(buffer.length, TEXT_SAMPLE_BYTES));
46
+ if (sample.length === 0)
47
+ return false;
48
+ let suspiciousControls = 0;
49
+ for (const byte of sample) {
50
+ if (byte === 0x00)
51
+ return true;
52
+ if (byte < 0x20 && !isAllowedTextControlByte(byte))
53
+ suspiciousControls += 1;
54
+ }
55
+ return suspiciousControls / sample.length > MAX_SUSPICIOUS_CONTROL_RATIO;
56
+ }
57
+ function isAllowedTextControlCode(codePoint) {
58
+ return codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0c || codePoint === 0x0d || codePoint === 0x1b;
59
+ }
60
+ function hasDecodedControlNoise(content) {
61
+ if (!content)
62
+ return false;
63
+ let suspiciousControls = 0;
64
+ let total = 0;
65
+ for (const char of content.slice(0, TEXT_SAMPLE_BYTES)) {
66
+ const codePoint = char.codePointAt(0) ?? 0;
67
+ total += 1;
68
+ if (((codePoint >= 0x00 && codePoint < 0x20) || (codePoint >= 0x7f && codePoint <= 0x9f)) &&
69
+ !isAllowedTextControlCode(codePoint)) {
70
+ suspiciousControls += 1;
71
+ }
72
+ }
73
+ return total > 0 && suspiciousControls / total > MAX_SUSPICIOUS_CONTROL_RATIO;
74
+ }
75
+ function decodeStrict(buffer, decoderEncoding, encoding, fallback) {
76
+ const content = new TextDecoder(decoderEncoding, { fatal: true }).decode(buffer);
77
+ if (hasDecodedControlNoise(content))
78
+ throw new BinaryTextPreviewError();
79
+ return { content, encoding, fallback };
80
+ }
81
+ export function decodeTextBufferAuto(buffer) {
82
+ if (buffer.length === 0)
83
+ return { content: '', encoding: 'utf8', fallback: false };
84
+ if (startsWithBytes(buffer, [0xef, 0xbb, 0xbf])) {
85
+ return decodeStrict(buffer, 'utf-8', 'utf8', false);
86
+ }
87
+ if (startsWithBytes(buffer, [0xff, 0xfe])) {
88
+ return decodeStrict(buffer, 'utf-16le', 'utf16le', true);
89
+ }
90
+ if (startsWithBytes(buffer, [0xfe, 0xff])) {
91
+ return decodeStrict(buffer, 'utf-16be', 'utf16be', true);
92
+ }
93
+ if (looksLikeBinaryBuffer(buffer))
94
+ throw new BinaryTextPreviewError();
95
+ try {
96
+ return decodeStrict(buffer, 'utf-8', 'utf8', false);
97
+ }
98
+ catch (error) {
99
+ if (error instanceof BinaryTextPreviewError)
100
+ throw error;
101
+ }
102
+ try {
103
+ return decodeStrict(buffer, 'gb18030', 'gb18030', true);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof BinaryTextPreviewError)
107
+ throw error;
108
+ }
109
+ return decodeStrict(buffer, 'windows-1252', 'windows-1252', true);
110
+ }
@@ -664,10 +664,8 @@ export class ManagerRuntimeService {
664
664
  noRestore: body.noRestore === undefined ? undefined : Boolean(body.noRestore),
665
665
  downloadAttachments: body.downloadAttachments === undefined ? undefined : Boolean(body.downloadAttachments),
666
666
  downloadAttachmentsDir: typeof body.downloadAttachmentsDir === 'string' ? body.downloadAttachmentsDir : undefined,
667
+ privacyConsentAccepted: body.privacyConsentAccepted === undefined ? undefined : Boolean(body.privacyConsentAccepted),
667
668
  flowScriptPath: typeof body.flowScriptPath === 'string' ? body.flowScriptPath : undefined,
668
- cloudOcrUrl: typeof body.cloudOcrUrl === 'string' ? body.cloudOcrUrl : undefined,
669
- cloudOcrToken: typeof body.cloudOcrToken === 'string' ? body.cloudOcrToken : undefined,
670
- cloudOcrMode: parseCloudOcrMode(body.cloudOcrMode),
671
669
  });
672
670
  this.registry.upsertManager({
673
671
  ...manager,
@@ -973,6 +971,9 @@ function externalChannelForChatEnqueue(channel) {
973
971
  noRestore: channel.noRestore,
974
972
  downloadAttachments: channel.downloadAttachments,
975
973
  selfNickname: channel.selfNickname,
974
+ wechatRpaPrivacyConsentAccepted: channel.wechatRpaPrivacyConsentAccepted,
975
+ wechatRpaServerDecisionAvailable: channel.wechatRpaServerDecisionAvailable,
976
+ wechatRpaPreflightChecks: channel.wechatRpaPreflightChecks,
976
977
  wechatRpaRuntimeState: channel.wechatRpaRuntimeState,
977
978
  wechatRpaLastMessageAt: channel.wechatRpaLastMessageAt,
978
979
  wechatRpaPendingReplyCount: channel.wechatRpaPendingReplyCount,
@@ -1004,9 +1005,6 @@ function mimeTypeFromExternalAttachment(attachment) {
1004
1005
  return 'audio/*';
1005
1006
  return 'application/octet-stream';
1006
1007
  }
1007
- function parseCloudOcrMode(value) {
1008
- return value === 'off' || value === 'fallback' || value === 'always' ? value : undefined;
1009
- }
1010
1008
  function optionalNumber(value) {
1011
1009
  const number = Number(value);
1012
1010
  return Number.isFinite(number) ? number : undefined;
@@ -1,4 +1,5 @@
1
1
  import type { CliRelayClient } from '../relay/client.js';
2
+ import type { SessionActivitySnapshot } from '@shennian/wire';
2
3
  export declare class NativeSessionFusionService {
3
4
  private client;
4
5
  private timer;
@@ -20,6 +21,15 @@ export declare class NativeSessionFusionService {
20
21
  noteManagedSourceSession(sessionId: string, agentType: string, sourceSessionKey: string | null): void;
21
22
  private pruneState;
22
23
  scanNow(): Promise<void>;
24
+ getCodexThreadActivity(params: {
25
+ sessionId: string;
26
+ threadId: string;
27
+ workDir?: string;
28
+ }): Promise<SessionActivitySnapshot | null | undefined>;
29
+ interruptCodexThread(params: {
30
+ threadId: string;
31
+ workDir?: string;
32
+ }): Promise<boolean>;
23
33
  private runScan;
24
34
  private tryClaimManagedEcho;
25
35
  private isSuppressed;
@@ -3,6 +3,7 @@
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import fs from 'node:fs';
5
5
  import { listClaudeTranscriptFiles, listCodexRolloutFiles, listOpenCodeSessionFiles, parseClaudeTranscriptChunk, parseCodexRolloutChunk, parseOpenCodeSessionFile, } from './parsers.js';
6
+ import { probeCodexThreadActivity, interruptCodexThread } from '../agents/codex-control.js';
6
7
  import { loadNativeScannerState, saveNativeScannerState } from './state.js';
7
8
  const SCAN_INTERVAL_MS = 60_000;
8
9
  const CLAIM_TTL_MS = SCAN_INTERVAL_MS * 2;
@@ -89,6 +90,32 @@ export class NativeSessionFusionService {
89
90
  });
90
91
  return this.scanPromise;
91
92
  }
93
+ async getCodexThreadActivity(params) {
94
+ const activity = await probeCodexThreadActivity({
95
+ threadId: params.threadId,
96
+ workDir: params.workDir,
97
+ });
98
+ if (!activity)
99
+ return undefined;
100
+ if (!activity.active || !activity.runPhase)
101
+ return null;
102
+ const nowIso = new Date().toISOString();
103
+ return {
104
+ sessionId: params.sessionId,
105
+ runId: activity.turnId ?? `codex:${params.threadId}`,
106
+ runPhase: activity.runPhase,
107
+ startedAt: nowIso,
108
+ updatedAt: nowIso,
109
+ canStop: activity.canStop,
110
+ };
111
+ }
112
+ async interruptCodexThread(params) {
113
+ const activity = await interruptCodexThread({
114
+ threadId: params.threadId,
115
+ workDir: params.workDir,
116
+ });
117
+ return Boolean(activity);
118
+ }
92
119
  async runScan() {
93
120
  this.pruneState();
94
121
  const state = loadNativeScannerState();
@@ -36,11 +36,13 @@ function extractSummary(text) {
36
36
  }
37
37
  function buildRelayAgentPayload(event, sessionId, extra = {}) {
38
38
  if (event.state === 'tool-call' || event.state === 'tool-result') {
39
+ const detailRef = { runId: event.runId, sourceSeq: event.seq };
39
40
  return {
40
41
  state: event.state,
41
42
  runId: event.runId,
42
43
  seq: event.seq,
43
44
  sessionId,
45
+ detailRef,
44
46
  ...(event.name ? { name: event.name } : {}),
45
47
  ...(event.source ? { source: event.source } : {}),
46
48
  ...(event.agentSessionId ? { agentSessionId: event.agentSessionId } : {}),
@@ -55,6 +57,8 @@ function runPhaseFromAgentEvent(event) {
55
57
  return event.runPhase ?? null;
56
58
  if (event.state === 'tool-call' || event.state === 'tool-result')
57
59
  return 'tool_running';
60
+ if (event.state === 'approval-pending')
61
+ return 'waiting_approval';
58
62
  if (event.state === 'delta')
59
63
  return event.thinking ? 'thinking' : 'streaming_text';
60
64
  if (event.state === 'init' || event.state === 'start')
@@ -144,8 +148,6 @@ function normalizeExternalChannel(value) {
144
148
  noRestore: raw.noRestore === undefined || raw.noRestore === null ? null : Boolean(raw.noRestore),
145
149
  downloadAttachments: raw.downloadAttachments === undefined || raw.downloadAttachments === null ? null : Boolean(raw.downloadAttachments),
146
150
  downloadAttachmentsDir: typeof raw.downloadAttachmentsDir === 'string' ? raw.downloadAttachmentsDir : null,
147
- cloudOcrUrl: typeof raw.cloudOcrUrl === 'string' ? raw.cloudOcrUrl : null,
148
- cloudOcrMode: typeof raw.cloudOcrMode === 'string' ? raw.cloudOcrMode : null,
149
151
  };
150
152
  }
151
153
  function externalChannelEnabled(channel) {
@@ -295,6 +297,8 @@ function bindAdapterEvents(runtime, sessionId, agentType, adapter) {
295
297
  v: 1,
296
298
  type: event.state === 'tool-call' ? 'tool_use' : 'tool_result',
297
299
  name: event.name,
300
+ status: event.state === 'tool-call' ? 'running' : 'completed',
301
+ detailRef: { runId: event.runId, sourceSeq: event.seq },
298
302
  args: event.args,
299
303
  result: event.result,
300
304
  }),
@@ -730,6 +734,18 @@ export async function handleChatAbort(runtime, req) {
730
734
  }
731
735
  catch { /* best-effort: still emit synthetic abort below */ }
732
736
  emitSyntheticAbort(runtime, sessionId);
737
+ runtime.activityPublisher?.publish(sessionId, null);
738
+ }
739
+ else {
740
+ const params = req.params;
741
+ if (params.agentType === 'codex' && params.agentSessionId && runtime.nativeFusion) {
742
+ const ok = await runtime.nativeFusion.interruptCodexThread({
743
+ threadId: params.agentSessionId,
744
+ workDir: params.workDir,
745
+ });
746
+ if (ok)
747
+ runtime.activityPublisher?.publish(sessionId, null);
748
+ }
733
749
  }
734
750
  runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
735
751
  }
@@ -5,6 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { convertMarkdownToPdf, defaultPdfOutputPath, MarkdownPdfBrowserMissingError, } from '../../tools/markdown-to-pdf.js';
7
7
  import { createZipArchive } from '../archive-zip.js';
8
+ import { BinaryTextPreviewError, decodeTextBufferAuto, isAutoTextEncoding, } from '../../fs/text-decoder.js';
8
9
  const FILE_SYSTEM_ROOTS_PATH = '__roots__';
9
10
  const MAX_FOLDER_UPLOAD_FILES = 2000;
10
11
  const MAX_FOLDER_UPLOAD_TOTAL_SIZE = 1024 * 1024 * 1024;
@@ -151,7 +152,8 @@ export async function handleFsRead(runtime, req) {
151
152
  return;
152
153
  }
153
154
  const filePath = resolved.path;
154
- const encoding = req.params.encoding || 'utf8';
155
+ const requestedEncoding = req.params.encoding;
156
+ const encoding = typeof requestedEncoding === 'string' ? requestedEncoding : 'utf8';
155
157
  const offset = req.params.offset;
156
158
  const length = req.params.length;
157
159
  try {
@@ -214,6 +216,23 @@ export async function handleFsRead(runtime, req) {
214
216
  });
215
217
  return;
216
218
  }
219
+ if (isAutoTextEncoding(encoding)) {
220
+ const buffer = fs.readFileSync(filePath);
221
+ const decoded = decodeTextBufferAuto(buffer);
222
+ runtime.client.sendRes({
223
+ type: 'res',
224
+ id: req.id,
225
+ ok: true,
226
+ payload: {
227
+ content: decoded.content,
228
+ path: filePath,
229
+ size: stat.size,
230
+ encoding: decoded.encoding,
231
+ encodingFallback: decoded.fallback,
232
+ },
233
+ });
234
+ return;
235
+ }
217
236
  const content = fs.readFileSync(filePath, 'utf-8');
218
237
  runtime.client.sendRes({
219
238
  type: 'res',
@@ -223,7 +242,12 @@ export async function handleFsRead(runtime, req) {
223
242
  });
224
243
  }
225
244
  catch (err) {
226
- runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
245
+ runtime.client.sendRes({
246
+ type: 'res',
247
+ id: req.id,
248
+ ok: false,
249
+ error: err instanceof BinaryTextPreviewError ? err.message : String(err),
250
+ });
227
251
  }
228
252
  }
229
253
  export async function handleFsWrite(runtime, req) {
@@ -378,7 +402,19 @@ export async function handleFsExportMarkdownPdf(runtime, req) {
378
402
  }
379
403
  }
380
404
  function safeArchiveBaseName(name) {
381
- const trimmed = name.trim().replace(/[<>:"/\\|?*\u0000-\u001f]+/g, '-').replace(/[. ]+$/g, '');
405
+ let safeName = '';
406
+ for (const char of name.trim()) {
407
+ const code = char.charCodeAt(0);
408
+ const unsafe = code <= 0x1f || '<>:"/\\|?*'.includes(char);
409
+ if (unsafe) {
410
+ if (!safeName.endsWith('-'))
411
+ safeName += '-';
412
+ }
413
+ else {
414
+ safeName += char;
415
+ }
416
+ }
417
+ const trimmed = safeName.replace(/[. ]+$/g, '');
382
418
  return trimmed || 'folder';
383
419
  }
384
420
  export async function handleFsArchiveZip(runtime, req) {
@@ -21,6 +21,18 @@ export async function handleSessionRefresh(runtime, req) {
21
21
  return;
22
22
  }
23
23
  await runtime.nativeFusion.scanNow();
24
+ if (params.agentType === 'codex' &&
25
+ params.agentSessionId &&
26
+ typeof runtime.nativeFusion.getCodexThreadActivity === 'function') {
27
+ const activity = await runtime.nativeFusion.getCodexThreadActivity({
28
+ sessionId: params.sessionId,
29
+ threadId: params.agentSessionId,
30
+ workDir: params.workDir,
31
+ });
32
+ if (activity !== undefined) {
33
+ runtime.activityPublisher?.publish(params.sessionId, activity);
34
+ }
35
+ }
24
36
  runtime.client.sendRes({
25
37
  type: 'res',
26
38
  id: req.id,
@@ -0,0 +1,3 @@
1
+ import type { ReqFrame } from '@shennian/wire';
2
+ import type { SessionManagerRuntime } from '../types.js';
3
+ export declare function handleSessionToolDetail(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;