shennian 0.2.86 → 0.2.88

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.
@@ -1,11 +1,12 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { AgentType, ChatAttachmentMeta, ExternalChannelSessionStatus } from '@shennian/wire';
2
+ import type { AgentType, ChatAttachmentMeta, ExternalChannelSessionStatus, SessionRunPhase } from '@shennian/wire';
3
3
  export type AgentEvent = {
4
4
  state: string;
5
5
  runId: string;
6
6
  seq: number;
7
7
  text?: string;
8
8
  thinking?: boolean;
9
+ runPhase?: SessionRunPhase;
9
10
  name?: string;
10
11
  args?: Record<string, unknown>;
11
12
  result?: string;
@@ -1,4 +1,9 @@
1
1
  export declare function makeThreadTitle(text: string): string;
2
+ export declare const CODEX_APP_SERVER_CLIENT_INFO: {
3
+ readonly name: "codex-tui";
4
+ readonly title: "Codex TUI";
5
+ readonly version: "0.0.0";
6
+ };
2
7
  export declare function isCodexCollabAgentToolName(name: string): boolean;
3
8
  export declare function isCodexCollabAgentItem(item: Record<string, unknown>): boolean;
4
9
  export declare function isCodexLegacyCollabAgentItem(item: {
@@ -5,6 +5,11 @@ export function makeThreadTitle(text) {
5
5
  const title = firstLine.trim().slice(0, 80);
6
6
  return title || 'Shennian';
7
7
  }
8
+ export const CODEX_APP_SERVER_CLIENT_INFO = {
9
+ name: 'codex-tui',
10
+ title: 'Codex TUI',
11
+ version: '0.0.0',
12
+ };
8
13
  const CODEX_COLLAB_AGENT_TOOL_NAMES = new Set([
9
14
  'spawnAgent',
10
15
  'sendInput',
@@ -6,7 +6,7 @@ import { AgentAdapter, registerAgent } from './adapter.js';
6
6
  import { resolveBuiltinCommand, spawnAgentCommand } from './command-spec.js';
7
7
  import { buildAgentProcessEnv } from '../agent-env.js';
8
8
  import { ensurePlatformInstructionsFile } from './platform-instructions.js';
9
- import { extractAppServerErrorMessage, formatCodexErrorMessage, formatTransientCodexStatus, isCodexCollabAgentItem, isCodexCollabAgentToolName, isCodexLegacyCollabAgentItem, isCodexUnsupportedEffortError, isMissingCodexRolloutError, isTransientCodexErrorMessage, looksLikeCodexInteractiveAuthPrompt, looksLikeFatalCodexStderr, makeThreadTitle, normalizeCodexModelId, normalizeCodexReasoningEffort, normalizeCodexStderr, normalizeTerminalText, safeStringify, stripGitDirectiveArtifacts, } from './codex-utils.js';
9
+ import { CODEX_APP_SERVER_CLIENT_INFO, extractAppServerErrorMessage, formatCodexErrorMessage, formatTransientCodexStatus, isCodexCollabAgentItem, isCodexCollabAgentToolName, isCodexLegacyCollabAgentItem, isCodexUnsupportedEffortError, isMissingCodexRolloutError, isTransientCodexErrorMessage, looksLikeCodexInteractiveAuthPrompt, looksLikeFatalCodexStderr, makeThreadTitle, normalizeCodexModelId, normalizeCodexReasoningEffort, normalizeCodexStderr, normalizeTerminalText, safeStringify, stripGitDirectiveArtifacts, } from './codex-utils.js';
10
10
  export { isCodexUnsupportedEffortError, isMissingCodexRolloutError, normalizeCodexModelId, normalizeCodexReasoningEffort, } from './codex-utils.js';
11
11
  function buildCodexTextInput(text, attachments) {
12
12
  const images = attachments
@@ -239,7 +239,7 @@ export class CodexAdapter extends AgentAdapter {
239
239
  async initializeAppServer(modelId) {
240
240
  const codexModelId = normalizeCodexModelId(modelId);
241
241
  await this.sendRpc('initialize', {
242
- clientInfo: { name: 'shennian', title: 'Shennian', version: '0.0.0' },
242
+ clientInfo: CODEX_APP_SERVER_CLIENT_INFO,
243
243
  capabilities: { experimentalApi: true },
244
244
  });
245
245
  if (this.agentSessionId) {
@@ -6,6 +6,7 @@ import { fallbackClaudeAliasModels, discoverClaudeAliasModelsFromEnv, fallbackGe
6
6
  import { runResolvedCommand } from './runner.js';
7
7
  import { DISCOVERY_WORKDIR } from './types.js';
8
8
  import { buildAgentProcessEnv, readLatestUserEnv } from '../../agent-env.js';
9
+ import { CODEX_APP_SERVER_CLIENT_INFO } from '../codex-utils.js';
9
10
  function sendAppServerRpc(proc, pending, id, method, params, timeoutMs) {
10
11
  if (!proc.stdin)
11
12
  return Promise.reject(new Error('codex app-server stdin unavailable'));
@@ -59,7 +60,7 @@ async function discoverCodexModelsViaAppServer(spec) {
59
60
  });
60
61
  try {
61
62
  await sendAppServerRpc(proc, pending, seq++, 'initialize', {
62
- clientInfo: { name: 'shennian', title: 'Shennian', version: '0.0.0' },
63
+ clientInfo: CODEX_APP_SERVER_CLIENT_INFO,
63
64
  capabilities: { experimentalApi: true },
64
65
  }, 10_000);
65
66
  const [modelList, configRead] = await Promise.all([
@@ -1,5 +1,5 @@
1
1
  import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
- export type ExternalChannelRuntimeStatus = Pick<ExternalChannelSessionStatus, 'wechatRpaRuntimeState' | 'wechatRpaLastRunAt' | 'wechatRpaLastMessageAt' | 'wechatRpaLastInterruptedAt' | 'wechatRpaPendingReplyCount' | 'wechatRpaLastError' | 'wechatRpaLastRunId' | 'wechatRpaLastTracePath' | 'wechatRpaLastTraceSummary' | 'wechatRpaRecentTaskSummaries' | 'wechatRpaLastCloudOcrAt' | 'wechatRpaLastCloudOcrPurpose' | 'wechatRpaLastCloudOcrRequestId' | 'wechatRpaLastCloudOcrImageHash' | 'wechatRpaLastCloudOcrUsage'>;
2
+ export type ExternalChannelRuntimeStatus = Pick<ExternalChannelSessionStatus, 'wechatRpaRuntimeState' | 'wechatRpaLastRunAt' | 'wechatRpaLastMessageAt' | 'wechatRpaLastInterruptedAt' | 'wechatRpaPendingReplyCount' | 'wechatRpaLastError' | 'wechatRpaLastRunId' | 'wechatRpaLastTracePath' | 'wechatRpaLastTraceSummary' | 'wechatRpaRecentTaskSummaries'>;
3
3
  export type ExternalChannelType = 'wecom' | 'websocket' | 'wechat-rpa';
4
4
  export type ExternalChannelConfig = {
5
5
  id: string;
@@ -42,9 +42,6 @@ export type ExternalChannelView = {
42
42
  downloadAttachments?: boolean;
43
43
  downloadAttachmentsDir?: string;
44
44
  selfNickname?: string | null;
45
- cloudOcrUrl?: string;
46
- cloudOcrToken?: string;
47
- cloudOcrMode?: string;
48
45
  wechatRpaRuntimeState?: ExternalChannelSessionStatus['wechatRpaRuntimeState'];
49
46
  wechatRpaLastRunAt?: string | null;
50
47
  wechatRpaLastMessageAt?: string | null;
@@ -54,15 +51,6 @@ export type ExternalChannelView = {
54
51
  wechatRpaLastTracePath?: string | null;
55
52
  wechatRpaLastTraceSummary?: string | null;
56
53
  wechatRpaRecentTaskSummaries?: ExternalChannelSessionStatus['wechatRpaRecentTaskSummaries'];
57
- wechatRpaLastCloudOcrAt?: string | null;
58
- wechatRpaLastCloudOcrPurpose?: string | null;
59
- wechatRpaLastCloudOcrRequestId?: string | null;
60
- wechatRpaLastCloudOcrImageHash?: string | null;
61
- wechatRpaLastCloudOcrUsage?: {
62
- inputTokens?: number;
63
- outputTokens?: number;
64
- totalTokens?: number;
65
- } | null;
66
54
  };
67
55
  export type ExternalMessageEvent = {
68
56
  type: 'external.message';
@@ -101,9 +101,6 @@ export declare class ChannelRuntime {
101
101
  downloadAttachmentsDir?: string;
102
102
  selfNickname?: string;
103
103
  flowScriptPath?: string;
104
- cloudOcrUrl?: string;
105
- cloudOcrToken?: string;
106
- cloudOcrMode?: 'off' | 'fallback' | 'always';
107
104
  }): Promise<ExternalChannelView>;
108
105
  }
109
106
  export type ExternalReplySendPlanItem = {
@@ -357,7 +357,6 @@ export class ChannelRuntime {
357
357
  : channel);
358
358
  configs.push(nextConfig);
359
359
  this.configs.replaceAll(configs);
360
- const cloudOcrMode = 'off';
361
360
  this.secrets.upsert(nextConfig.secretRef, {
362
361
  type: 'wechat-rpa',
363
362
  source,
@@ -371,9 +370,6 @@ export class ChannelRuntime {
371
370
  downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
372
371
  selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
373
372
  flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
374
- cloudOcrUrl: '',
375
- cloudOcrToken: '',
376
- cloudOcrMode,
377
373
  canReply: input.canReply ?? priorSecret?.canReply ?? false,
378
374
  systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
379
375
  });
@@ -445,7 +441,7 @@ function normalizeWeChatRpaGroups(groups) {
445
441
  return result;
446
442
  }
447
443
  function defaultWeChatRpaSource() {
448
- return process.platform === 'win32' ? 'windows-visual-flow' : 'wechat-rpa-lab';
444
+ return 'wechat-rpa-lab';
449
445
  }
450
446
  export function planExternalReplySends(channelType, input) {
451
447
  const parts = splitExternalReplyText(input.text);
@@ -19,11 +19,7 @@ type ChannelSecretRecord = {
19
19
  selfNickname?: string;
20
20
  idleSeconds?: number;
21
21
  recentLimit?: number;
22
- readMode?: 'local-ocr' | 'hybrid-vlm';
23
22
  flowScriptPath?: string;
24
- cloudOcrUrl?: string;
25
- cloudOcrToken?: string;
26
- cloudOcrMode?: 'off' | 'fallback' | 'always';
27
23
  updatedAt: string;
28
24
  };
29
25
  export declare class ChannelSecretRegistry {
@@ -10,10 +10,6 @@ export type MacWeChatRpaFlowOptions = {
10
10
  recentLimit?: number;
11
11
  downloadAttachmentsDir?: string;
12
12
  timeoutMs?: number;
13
- cloudOcrUrl?: string;
14
- cloudOcrToken?: string;
15
- cloudOcrMode?: 'off' | 'fallback' | 'always';
16
- cloudOcrChannelId?: string;
17
13
  };
18
14
  export type MacWeChatRpaFlowResult = {
19
15
  ok: boolean;
@@ -30,14 +26,8 @@ export type MacWeChatRpaFlowResult = {
30
26
  sentAttachment?: boolean;
31
27
  sentAttachmentObserved?: boolean;
32
28
  postSendScreenshotPath?: string;
33
- cloudOcrPurpose?: MacWeChatRpaCloudOcrPurpose;
34
- cloudOcrObservations?: MacWeChatRpaCloudOcrObservation[];
35
- cloudOcrRequestId?: string;
36
- cloudOcrImageHash?: string;
37
- cloudOcrUsage?: MacWeChatRpaCloudOcrUsage;
38
29
  error?: string;
39
30
  };
40
- export type MacWeChatRpaCloudOcrPurpose = 'message-read' | 'attachment-localization' | 'send-confirmation';
41
31
  export type MacWeChatRpaFlowMessage = {
42
32
  id?: string;
43
33
  text?: string;
@@ -62,22 +52,4 @@ export type MacWeChatRpaAttachment = {
62
52
  expiresAt?: string;
63
53
  providerError?: string;
64
54
  };
65
- export type MacWeChatRpaCloudOcrObservation = {
66
- text: string;
67
- confidence?: number;
68
- role?: string;
69
- attachment?: MacWeChatRpaAttachment;
70
- };
71
- export type MacWeChatRpaCloudOcrUsage = {
72
- inputTokens?: number;
73
- outputTokens?: number;
74
- totalTokens?: number;
75
- };
76
55
  export declare function runMacWeChatRpaFlow(options: MacWeChatRpaFlowOptions): Promise<MacWeChatRpaFlowResult>;
77
- export declare function selectCloudOcrRequest(result: Pick<MacWeChatRpaFlowResult, 'newMessages' | 'recentMessages' | 'screenshotPath' | 'postSendScreenshotPath' | 'sentReply' | 'sentAttachment'>, mode: 'off' | 'fallback' | 'always'): {
78
- screenshotPath: string;
79
- purpose: MacWeChatRpaCloudOcrPurpose;
80
- } | null;
81
- export declare function shouldUseCloudOcr(result: Pick<MacWeChatRpaFlowResult, 'newMessages' | 'recentMessages'>, mode: 'off' | 'fallback' | 'always'): boolean;
82
- export declare function mergeCloudMessages(localMessages: MacWeChatRpaFlowMessage[], cloudMessages: MacWeChatRpaFlowMessage[]): MacWeChatRpaFlowMessage[];
83
- export declare function messagesFromCloudOcrObservations(groupName: string, observations: MacWeChatRpaCloudOcrObservation[]): MacWeChatRpaFlowMessage[];
@@ -2,7 +2,6 @@
2
2
  // @test src/__tests__/wechat-rpa-normalizer.test.ts
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import crypto from 'node:crypto';
6
5
  import { execFile } from 'node:child_process';
7
6
  import { promisify } from 'node:util';
8
7
  const execFileAsync = promisify(execFile);
@@ -61,7 +60,7 @@ export async function runMacWeChatRpaFlow(options) {
61
60
  return parsed;
62
61
  if (!parsed.ok)
63
62
  throw new Error(parsed.error || 'WeChat RPA flow failed');
64
- return await maybeEnrichWithCloudOcr(parsed, options);
63
+ return parsed;
65
64
  }
66
65
  catch (error) {
67
66
  const detail = stderr.trim() || stdout.slice(0, 1_000);
@@ -82,138 +81,6 @@ function hasPartialSendProgress(result) {
82
81
  function hasSendAttempt(result) {
83
82
  return Boolean(result.sentReply || result.sentAttachment || result.sentReplyObserved || result.sentAttachmentObserved);
84
83
  }
85
- async function maybeEnrichWithCloudOcr(result, options) {
86
- void options;
87
- return result;
88
- }
89
- export function selectCloudOcrRequest(result, mode) {
90
- if (!shouldUseCloudOcr(result, mode))
91
- return null;
92
- if ((result.sentReply || result.sentAttachment) && result.postSendScreenshotPath) {
93
- return { screenshotPath: result.postSendScreenshotPath, purpose: 'send-confirmation' };
94
- }
95
- if (!result.screenshotPath)
96
- return null;
97
- const messages = [...(result.newMessages ?? []), ...(result.recentMessages ?? [])];
98
- const hasUnresolvedAttachment = messages.some((message) => (message.attachments ?? []).some((attachment) => attachment.availability === 'metadata-only'
99
- || attachment.availability === 'pending-download'
100
- || Boolean(attachment.providerError)));
101
- return {
102
- screenshotPath: result.screenshotPath,
103
- purpose: hasUnresolvedAttachment ? 'attachment-localization' : 'message-read',
104
- };
105
- }
106
- export function shouldUseCloudOcr(result, mode) {
107
- if (mode === 'off')
108
- return false;
109
- if (mode === 'always')
110
- return true;
111
- const messages = [...(result.newMessages ?? []), ...(result.recentMessages ?? [])];
112
- if (!(result.newMessages?.length))
113
- return true;
114
- if (messages.some((message) => Number.isFinite(message.confidence) && Number(message.confidence) < 0.65))
115
- return true;
116
- return messages.some((message) => (message.attachments ?? []).some((attachment) => attachment.availability === 'metadata-only'
117
- || attachment.availability === 'pending-download'
118
- || Boolean(attachment.providerError)));
119
- }
120
- export function mergeCloudMessages(localMessages, cloudMessages) {
121
- const merged = [...localMessages];
122
- for (const cloud of cloudMessages) {
123
- const signature = messageSignature(cloud);
124
- const index = merged.findIndex((message) => messageSignature(message) === signature);
125
- if (index < 0) {
126
- merged.push(cloud);
127
- continue;
128
- }
129
- const existingAttachments = merged[index]?.attachments ?? [];
130
- const cloudAttachments = cloud.attachments ?? [];
131
- if (!existingAttachments.length && cloudAttachments.length) {
132
- const existing = merged[index] ?? {};
133
- merged[index] = { ...existing, attachments: cloudAttachments };
134
- }
135
- }
136
- return merged;
137
- }
138
- export function messagesFromCloudOcrObservations(groupName, observations) {
139
- const messages = [];
140
- for (const item of observations) {
141
- const text = String(item.text || '').trim();
142
- if (!text)
143
- continue;
144
- const role = String(item.role || 'unknown');
145
- if (!['message', 'attachment', 'unknown'].includes(role))
146
- continue;
147
- const attachment = normalizeCloudAttachment(item.attachment) ?? (role === 'attachment' ? attachmentFromText(text) : null);
148
- messages.push({
149
- id: `cloud:${stableId(`${groupName}\n${role}\n${text}`)}`,
150
- text,
151
- confidence: item.confidence,
152
- attachments: attachment ? [attachment] : [],
153
- });
154
- }
155
- return messages;
156
- }
157
- function messageSignature(message) {
158
- const text = String(message.text || '').replace(/\s+/g, ' ').trim().toLowerCase();
159
- const attachments = (message.attachments ?? [])
160
- .map((attachment) => `${attachment.type}:${attachment.name || ''}:${attachment.mimeType || ''}`)
161
- .sort()
162
- .join('|');
163
- return text || attachments || String(message.id || '');
164
- }
165
- function normalizeCloudAttachment(value) {
166
- if (!value || typeof value !== 'object')
167
- return null;
168
- const record = value;
169
- const type = String(record.type || 'file').trim() || 'file';
170
- const name = String(record.name || '').replace(/\s+/g, ' ').trim();
171
- const mimeType = String(record.mimeType || '').replace(/\s+/g, ' ').trim();
172
- const size = Number(record.size);
173
- return {
174
- type: ['image', 'video', 'audio', 'file'].includes(type) ? type : 'file',
175
- ...(name ? { name } : {}),
176
- ...(mimeType ? { mimeType } : {}),
177
- ...(Number.isFinite(size) && size > 0 ? { size } : {}),
178
- availability: 'metadata-only',
179
- };
180
- }
181
- function attachmentFromText(text) {
182
- const matches = Array.from(text.matchAll(/[\p{L}\p{N}_().[\]-]+\.(png|jpe?g|gif|webp|heic|mp4|mov|avi|mkv|pdf|docx?|xlsx?|pptx?|txt|md|csv|zip|rar)/giu));
183
- const filename = matches.at(-1)?.[0]?.trim();
184
- if (!filename)
185
- return null;
186
- const ext = path.extname(filename).toLowerCase();
187
- return { type: attachmentTypeFromExt(ext), name: filename, mimeType: mimeTypeFromExt(ext), availability: 'metadata-only' };
188
- }
189
- function attachmentTypeFromExt(ext) {
190
- if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.heic'].includes(ext))
191
- return 'image';
192
- if (['.mp4', '.mov', '.avi', '.mkv'].includes(ext))
193
- return 'video';
194
- return 'file';
195
- }
196
- function mimeTypeFromExt(ext) {
197
- const map = {
198
- '.png': 'image/png',
199
- '.jpg': 'image/jpeg',
200
- '.jpeg': 'image/jpeg',
201
- '.gif': 'image/gif',
202
- '.webp': 'image/webp',
203
- '.heic': 'image/heic',
204
- '.mp4': 'video/mp4',
205
- '.mov': 'video/quicktime',
206
- '.pdf': 'application/pdf',
207
- '.txt': 'text/plain',
208
- '.md': 'text/markdown',
209
- '.csv': 'text/csv',
210
- '.zip': 'application/zip',
211
- };
212
- return map[ext] || 'application/octet-stream';
213
- }
214
- function stableId(value) {
215
- return crypto.createHash('sha256').update(value).digest('hex').slice(0, 24);
216
- }
217
84
  function resolveFlowScriptPath(scriptPath, workDir) {
218
85
  const candidates = [
219
86
  scriptPath,
@@ -8,7 +8,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { ChannelSecretRegistry } from './secret-registry.js';
9
9
  import { probeMacWeChat, observedMessageFromProbe } from './wechat-rpa/macos.js';
10
10
  import { runMacWeChatRpaFlow } from './wechat-rpa/macos-flow.js';
11
- import { runWindowsWeChatRpaVisualFlow } from './wechat-rpa/windows-visual-flow.js';
12
11
  import { normalizeWeChatRpaMessage, WeChatRpaDeduper, weChatRpaConversationId, } from './wechat-rpa/normalizer.js';
13
12
  const DEFAULT_POLL_INTERVAL_MS = 5_000;
14
13
  const DEFAULT_RECENT_LIMIT = 5;
@@ -159,7 +158,6 @@ export class WeChatRpaChannelAdapter {
159
158
  noteManualReview(conn, pendingKey, reason);
160
159
  return { status: 'manual-review', reason };
161
160
  }
162
- recordCloudOcrRuntime(conn, flow);
163
161
  addTaskSummary(conn, {
164
162
  status: 'sent',
165
163
  runId: conn.lastRunId ?? null,
@@ -225,11 +223,6 @@ export class WeChatRpaChannelAdapter {
225
223
  wechatRpaLastTracePath: conn.lastTracePath ?? null,
226
224
  wechatRpaLastTraceSummary: conn.lastTraceSummary ?? null,
227
225
  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
226
  };
234
227
  }
235
228
  ensureConnection(config) {
@@ -284,7 +277,6 @@ export class WeChatRpaChannelAdapter {
284
277
  conn.runtimeState = 'syncing';
285
278
  const observed = await this.readObservedMessages(conn.config, secret, (flow) => {
286
279
  recordTraceRuntime(conn, flow);
287
- recordCloudOcrRuntime(conn, flow);
288
280
  if (!flow.interrupted)
289
281
  return;
290
282
  interrupted = true;
@@ -345,7 +337,6 @@ export class WeChatRpaChannelAdapter {
345
337
  persistPendingReplyState(conn);
346
338
  const flow = await runSendFlow(conn.config, secret, pending.conversationName, pending.skipText ? '' : pending.text, pending.attachmentPath, pending.key);
347
339
  recordTraceRuntime(conn, flow);
348
- recordCloudOcrRuntime(conn, flow);
349
340
  const partial = applyPartialSendProgress(flow, {
350
341
  text: pending.skipText ? '' : pending.text,
351
342
  attachmentPath: pending.attachmentPath,
@@ -446,10 +437,8 @@ function isWeChatRpaTextMentioned(text, aliases) {
446
437
  export function windowsVisualFlowHealth(secret, platform = process.platform) {
447
438
  if (platform !== 'win32')
448
439
  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' };
440
+ void secret;
441
+ return { ok: false, message: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it' };
453
442
  }
454
443
  async function readLabMessages(config, secret, onFlow) {
455
444
  const api = await importWeChatRpaLabApi(config.workDir);
@@ -475,7 +464,6 @@ async function runLabReadFlow(api, secret, groupName) {
475
464
  targetGroup: groupName,
476
465
  policy: secret.forceForeground ? 'work' : 'polite',
477
466
  limit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 1, 50),
478
- readMode: secret.readMode === 'hybrid-vlm' ? 'hybrid-vlm' : 'local-ocr',
479
467
  };
480
468
  const result = await api.runWechatRpaReadLatest(task);
481
469
  const structuredMessages = Array.isArray(result.data?.structuredMessages) ? result.data.structuredMessages : [];
@@ -497,7 +485,6 @@ async function runLabReadFlow(api, secret, groupName) {
497
485
  recentMessages: flowMessages,
498
486
  newMessages: flowMessages,
499
487
  screenshotPath: result.tracePath,
500
- cloudOcrUsage: result.data?.hybrid?.usage,
501
488
  error: labResultError(result),
502
489
  };
503
490
  }
@@ -630,7 +617,6 @@ async function readMacFlowMessages(config, secret, onFlow) {
630
617
  idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
631
618
  recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
632
619
  downloadAttachmentsDir: resolveInboundAttachmentDir(config.workDir, secret),
633
- cloudOcrMode: 'off',
634
620
  });
635
621
  onFlow?.(flow);
636
622
  if (flow.interrupted)
@@ -644,26 +630,18 @@ async function readMacFlowMessages(config, secret, onFlow) {
644
630
  return result;
645
631
  }
646
632
  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;
633
+ void config;
634
+ const groupName = configuredGroups(secret)[0] || '<unbound>';
635
+ onFlow?.({
636
+ ok: false,
637
+ groupName,
638
+ interrupted: true,
639
+ reason: 'windows-visual-flow-archived',
640
+ newMessages: [],
641
+ recentMessages: [],
642
+ error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
643
+ });
644
+ return [];
667
645
  }
668
646
  function flowMessageToObservedMessage(conversationName, message, secret) {
669
647
  const text = String(message.text || '').trim();
@@ -684,16 +662,19 @@ function flowMessageToObservedMessage(conversationName, message, secret) {
684
662
  }
685
663
  async function runSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey) {
686
664
  if (secret.source === 'windows-visual-flow') {
687
- return runWindowsWeChatRpaVisualFlow({
665
+ void config;
666
+ void text;
667
+ void attachmentPath;
668
+ void dedupeKey;
669
+ return {
670
+ ok: false,
688
671
  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
- });
672
+ interrupted: true,
673
+ reason: 'windows-visual-flow-archived',
674
+ newMessages: [],
675
+ recentMessages: [],
676
+ error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
677
+ };
697
678
  }
698
679
  if (secret.source === 'wechat-rpa-lab') {
699
680
  return runLabSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey);
@@ -709,11 +690,10 @@ async function runSendFlow(config, secret, conversationName, text, attachmentPat
709
690
  idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
710
691
  recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
711
692
  downloadAttachmentsDir: resolveInboundAttachmentDir(config.workDir, secret),
712
- cloudOcrMode: 'off',
713
693
  });
714
694
  }
715
695
  function isFlowSource(source) {
716
- return source === 'macos-flow' || source === 'windows-visual-flow' || source === 'wechat-rpa-lab';
696
+ return source === 'macos-flow' || source === 'wechat-rpa-lab';
717
697
  }
718
698
  function applyPartialSendProgress(flow, input) {
719
699
  if (input.text && input.attachmentPath && flow.sentReplyObserved && !flow.sentAttachmentObserved) {
@@ -883,7 +863,6 @@ function noteInterruption(conn, flow, reason) {
883
863
  conn.interruptionCooldownUntil = Date.now() + INTERRUPTION_COOLDOWN_MS;
884
864
  conn.runtimeState = 'cooldown';
885
865
  }
886
- recordCloudOcrRuntime(conn, flow);
887
866
  addTaskSummary(conn, {
888
867
  status: 'interrupted',
889
868
  runId: flow.rpaRunId || conn.lastRunId || null,
@@ -1041,10 +1020,3 @@ function clampNumber(value, fallback, min, max) {
1041
1020
  return fallback;
1042
1021
  return Math.min(max, Math.max(min, number));
1043
1022
  }
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
- }
@@ -38,6 +38,7 @@ const SAFE_SNAPSHOT_ENV_KEYS = new Set([
38
38
  'LOCALAPPDATA',
39
39
  'SHENNIAN_DESKTOP_SERVER_URL',
40
40
  'SHENNIAN_HOME',
41
+ 'SHENNIAN_NATIVE_FUSION_DISABLED',
41
42
  ]);
42
43
  export function isSafeSnapshotEnvKey(key) {
43
44
  return SAFE_SNAPSHOT_ENV_KEYS.has(key);
@@ -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,17 @@ 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')
366
362
  .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
363
  .option('--json', 'Print JSON')
371
364
  .action(async (opts) => {
372
365
  if (opts.enabled === 'true' && opts.group.length > 1) {
package/dist/src/index.js CHANGED
@@ -25,6 +25,7 @@ const AUTO_UPGRADE_INITIAL_DELAY_MS = 30_000;
25
25
  const AUTO_UPGRADE_POLL_INTERVAL_MS = 5 * 60_000;
26
26
  import { getCachedAgentInfos, resolveAgentInfos } from './agents/model-registry.js';
27
27
  import { initCliLogReporter, reportLog } from './log-reporter.js';
28
+ import { isNativeFusionEnabled } from './native-fusion/config.js';
28
29
  import { NativeSessionFusionService } from './native-fusion/service.js';
29
30
  import { startDaemonLogRetention } from './daemon-log.js';
30
31
  const SHENNIAN_DIR = getShennianDir();
@@ -285,7 +286,7 @@ program
285
286
  },
286
287
  });
287
288
  nativeFusion =
288
- process.env.SHENNIAN_NATIVE_FUSION_ENABLED === '1'
289
+ isNativeFusionEnabled()
289
290
  ? new NativeSessionFusionService(client)
290
291
  : null;
291
292
  const sessionManager = new SessionManager(client, nativeFusion, currentCliVersion);
@@ -665,9 +665,6 @@ export class ManagerRuntimeService {
665
665
  downloadAttachments: body.downloadAttachments === undefined ? undefined : Boolean(body.downloadAttachments),
666
666
  downloadAttachmentsDir: typeof body.downloadAttachmentsDir === 'string' ? body.downloadAttachmentsDir : undefined,
667
667
  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
668
  });
672
669
  this.registry.upsertManager({
673
670
  ...manager,
@@ -1004,9 +1001,6 @@ function mimeTypeFromExternalAttachment(attachment) {
1004
1001
  return 'audio/*';
1005
1002
  return 'application/octet-stream';
1006
1003
  }
1007
- function parseCloudOcrMode(value) {
1008
- return value === 'off' || value === 'fallback' || value === 'always' ? value : undefined;
1009
- }
1010
1004
  function optionalNumber(value) {
1011
1005
  const number = Number(value);
1012
1006
  return Number.isFinite(number) ? number : undefined;
@@ -0,0 +1 @@
1
+ export declare function isNativeFusionEnabled(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,5 @@
1
+ // @arch docs/architecture/cli/native-session-fusion.md
2
+ // @test src/__tests__/native-fusion-config.test.ts
3
+ export function isNativeFusionEnabled(env = process.env) {
4
+ return env.SHENNIAN_NATIVE_FUSION_DISABLED !== '1';
5
+ }