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
@@ -0,0 +1,13 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "helperVersion": "0.1.0",
4
+ "protocolVersion": 1,
5
+ "platforms": {
6
+ "darwin": {
7
+ "executable": "shennian-wechat-channel-helper",
8
+ "sha256": "02b8569879cf116d9838474a7f2300f12e3bd252d41369647df2d450936e851d",
9
+ "signed": false,
10
+ "notarized": false
11
+ }
12
+ }
13
+ }
@@ -36,6 +36,12 @@ export declare abstract class AgentAdapter extends EventEmitter<AgentAdapterEven
36
36
  externalChannel?: ExternalChannelSessionStatus | null;
37
37
  env?: NodeJS.ProcessEnv;
38
38
  }): void;
39
+ getStatus?(): Promise<{
40
+ active: boolean;
41
+ runId?: string | null;
42
+ runPhase?: SessionRunPhase | null;
43
+ canStop?: boolean;
44
+ }>;
39
45
  setTitle?(agentSessionId: string, title: string, workDir?: string): Promise<void>;
40
46
  abstract start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
41
47
  abstract send(text: string, modelId?: string, reasoningEffort?: string, attachments?: ChatAttachmentMeta[]): Promise<void>;
@@ -0,0 +1,35 @@
1
+ import type { SessionRunPhase } from '@shennian/wire';
2
+ export type CodexThreadActivity = {
3
+ active: boolean;
4
+ turnId: string | null;
5
+ runPhase: SessionRunPhase | null;
6
+ canStop: boolean;
7
+ };
8
+ export declare class CodexAppServerProxyClient {
9
+ private readonly opts;
10
+ private process;
11
+ private rpcSeq;
12
+ private pendingRequests;
13
+ private stderr;
14
+ private initialized;
15
+ constructor(opts?: {
16
+ workDir?: string;
17
+ timeoutMs?: number;
18
+ });
19
+ start(): Promise<void>;
20
+ initialize(): Promise<void>;
21
+ readThreadActivity(threadId: string): Promise<CodexThreadActivity>;
22
+ interruptThread(threadId: string): Promise<CodexThreadActivity>;
23
+ close(): Promise<void>;
24
+ private sendRpc;
25
+ private handleMessage;
26
+ private rejectAllPending;
27
+ }
28
+ export declare function probeCodexThreadActivity(params: {
29
+ threadId: string;
30
+ workDir?: string;
31
+ }): Promise<CodexThreadActivity | null>;
32
+ export declare function interruptCodexThread(params: {
33
+ threadId: string;
34
+ workDir?: string;
35
+ }): Promise<CodexThreadActivity | null>;
@@ -0,0 +1,188 @@
1
+ // @arch docs/architecture/cli/agent-adapters.md
2
+ // @test src/__tests__/codex-control.test.ts
3
+ import { createInterface } from 'node:readline';
4
+ import { resolveBuiltinCommand, spawnAgentCommand } from './command-spec.js';
5
+ import { buildAgentProcessEnv } from '../agent-env.js';
6
+ import { CODEX_APP_SERVER_CLIENT_INFO } from './codex-utils.js';
7
+ function mapThreadStatusToRunPhase(status) {
8
+ const flags = status?.type === 'active' && Array.isArray(status.activeFlags) ? status.activeFlags : [];
9
+ if (flags.includes('waitingOnApproval'))
10
+ return 'waiting_approval';
11
+ if (flags.includes('waitingOnUserInput'))
12
+ return 'waiting_user_input';
13
+ return 'thinking';
14
+ }
15
+ function latestActiveTurnId(turns) {
16
+ if (!Array.isArray(turns))
17
+ return null;
18
+ for (let index = turns.length - 1; index >= 0; index--) {
19
+ const turn = turns[index];
20
+ if (!turn || typeof turn !== 'object')
21
+ continue;
22
+ const record = turn;
23
+ if (record.status !== 'inProgress')
24
+ continue;
25
+ return typeof record.id === 'string' ? record.id : null;
26
+ }
27
+ return null;
28
+ }
29
+ export class CodexAppServerProxyClient {
30
+ opts;
31
+ process = null;
32
+ rpcSeq = 1;
33
+ pendingRequests = new Map();
34
+ stderr = '';
35
+ initialized = false;
36
+ constructor(opts = {}) {
37
+ this.opts = opts;
38
+ }
39
+ async start() {
40
+ if (this.process)
41
+ return;
42
+ const spec = resolveBuiltinCommand('codex');
43
+ if (!spec)
44
+ throw new Error('Command "codex" not found. Is OpenAI Codex CLI installed?');
45
+ const proc = spawnAgentCommand(spec, ['app-server', 'proxy'], {
46
+ cwd: this.opts.workDir || undefined,
47
+ stdio: ['pipe', 'pipe', 'pipe'],
48
+ env: buildAgentProcessEnv({ NO_COLOR: '1' }),
49
+ });
50
+ this.process = proc;
51
+ this.stderr = '';
52
+ const rl = createInterface({ input: proc.stdout });
53
+ rl.on('line', (line) => {
54
+ if (!line.trim())
55
+ return;
56
+ let msg;
57
+ try {
58
+ msg = JSON.parse(line);
59
+ }
60
+ catch {
61
+ return;
62
+ }
63
+ this.handleMessage(msg);
64
+ });
65
+ proc.stderr?.on('data', (chunk) => {
66
+ this.stderr += chunk.toString();
67
+ });
68
+ proc.on('close', (code) => {
69
+ if (this.process !== proc)
70
+ return;
71
+ this.process = null;
72
+ this.rejectAllPending(new Error(this.stderr.trim() || `codex app-server proxy exited with code ${code}`));
73
+ });
74
+ proc.on('error', (error) => {
75
+ if (this.process !== proc)
76
+ return;
77
+ this.process = null;
78
+ this.rejectAllPending(error);
79
+ });
80
+ }
81
+ async initialize() {
82
+ if (this.initialized)
83
+ return;
84
+ await this.start();
85
+ await this.sendRpc('initialize', {
86
+ clientInfo: CODEX_APP_SERVER_CLIENT_INFO,
87
+ capabilities: { experimentalApi: true },
88
+ });
89
+ this.initialized = true;
90
+ }
91
+ async readThreadActivity(threadId) {
92
+ await this.initialize();
93
+ const response = await this.sendRpc('thread/read', { threadId, includeTurns: true });
94
+ const status = response.thread?.status ?? null;
95
+ const active = status?.type === 'active';
96
+ const turnId = latestActiveTurnId(response.thread?.turns);
97
+ return {
98
+ active,
99
+ turnId,
100
+ runPhase: active ? mapThreadStatusToRunPhase(status) : null,
101
+ canStop: Boolean(active && turnId),
102
+ };
103
+ }
104
+ async interruptThread(threadId) {
105
+ const activity = await this.readThreadActivity(threadId);
106
+ if (!activity.turnId)
107
+ return activity;
108
+ await this.sendRpc('turn/interrupt', { threadId, turnId: activity.turnId }, 5_000);
109
+ return { active: false, turnId: activity.turnId, runPhase: null, canStop: false };
110
+ }
111
+ async close() {
112
+ const proc = this.process;
113
+ this.process = null;
114
+ if (!proc)
115
+ return;
116
+ proc.kill('SIGTERM');
117
+ await new Promise((resolve) => {
118
+ proc.on('close', resolve);
119
+ setTimeout(() => {
120
+ proc.kill('SIGKILL');
121
+ resolve();
122
+ }, 1000).unref?.();
123
+ });
124
+ this.rejectAllPending(new Error('codex app-server proxy stopped'));
125
+ }
126
+ sendRpc(method, params, timeoutMs = this.opts.timeoutMs ?? 8_000) {
127
+ const proc = this.process;
128
+ if (!proc?.stdin)
129
+ return Promise.reject(new Error('codex app-server proxy is not running'));
130
+ const id = this.rpcSeq++;
131
+ const payload = JSON.stringify({ id, method, params });
132
+ return new Promise((resolve, reject) => {
133
+ const timer = setTimeout(() => {
134
+ this.pendingRequests.delete(id);
135
+ reject(new Error(`codex app-server proxy request timed out: ${method}`));
136
+ }, timeoutMs);
137
+ this.pendingRequests.set(id, { resolve, reject, timer });
138
+ proc.stdin.write(`${payload}\n`, (error) => {
139
+ if (!error)
140
+ return;
141
+ clearTimeout(timer);
142
+ this.pendingRequests.delete(id);
143
+ reject(error);
144
+ });
145
+ });
146
+ }
147
+ handleMessage(msg) {
148
+ if (msg.id == null)
149
+ return;
150
+ const pending = this.pendingRequests.get(msg.id);
151
+ if (!pending)
152
+ return;
153
+ clearTimeout(pending.timer);
154
+ this.pendingRequests.delete(msg.id);
155
+ if (msg.error)
156
+ pending.reject(new Error(msg.error.message || `codex app-server proxy error ${msg.error.code ?? ''}`.trim()));
157
+ else
158
+ pending.resolve(msg.result);
159
+ }
160
+ rejectAllPending(error) {
161
+ for (const [id, pending] of this.pendingRequests.entries()) {
162
+ clearTimeout(pending.timer);
163
+ pending.reject(error);
164
+ this.pendingRequests.delete(id);
165
+ }
166
+ }
167
+ }
168
+ export async function probeCodexThreadActivity(params) {
169
+ const client = new CodexAppServerProxyClient({ workDir: params.workDir, timeoutMs: 5_000 });
170
+ try {
171
+ return await client.readThreadActivity(params.threadId);
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ finally {
177
+ await client.close().catch(() => { });
178
+ }
179
+ }
180
+ export async function interruptCodexThread(params) {
181
+ const client = new CodexAppServerProxyClient({ workDir: params.workDir, timeoutMs: 8_000 });
182
+ try {
183
+ return await client.interruptThread(params.threadId);
184
+ }
185
+ finally {
186
+ await client.close().catch(() => { });
187
+ }
188
+ }
@@ -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',
@@ -1,5 +1,6 @@
1
1
  import { AgentAdapter } from './adapter.js';
2
2
  import type { ChatAttachmentMeta, ExternalChannelSessionStatus } from '@shennian/wire';
3
+ import type { SessionRunPhase } from '@shennian/wire';
3
4
  export { isCodexUnsupportedEffortError, isMissingCodexRolloutError, normalizeCodexModelId, normalizeCodexReasoningEffort, } from './codex-utils.js';
4
5
  export declare class CodexAdapter extends AgentAdapter {
5
6
  private readonly options;
@@ -40,6 +41,12 @@ export declare class CodexAdapter extends AgentAdapter {
40
41
  resume(agentSessionId: string): Promise<void>;
41
42
  setTitle(agentSessionId: string, title: string, workDir?: string): Promise<void>;
42
43
  stop(): Promise<void>;
44
+ getStatus(): Promise<{
45
+ active: boolean;
46
+ runId?: string | null;
47
+ runPhase?: SessionRunPhase | null;
48
+ canStop?: boolean;
49
+ }>;
43
50
  private spawnCodex;
44
51
  private spawnAppServer;
45
52
  private ensureAppServer;
@@ -49,6 +56,7 @@ export declare class CodexAdapter extends AgentAdapter {
49
56
  private interruptActiveTurn;
50
57
  private sendRpc;
51
58
  private handleAppServerMessage;
59
+ private mapThreadStatusToRunPhase;
52
60
  private handleAppServerNonJsonStdout;
53
61
  private failAppServerStartup;
54
62
  private handleAppServerCompletedItem;
@@ -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
@@ -117,6 +117,30 @@ export class CodexAdapter extends AgentAdapter {
117
117
  await this.interruptActiveTurn().catch(() => { });
118
118
  await this.killProcess();
119
119
  }
120
+ async getStatus() {
121
+ const threadId = this.agentSessionId;
122
+ const activeTurnId = this.activeTurnId;
123
+ if (!threadId || !this.process) {
124
+ return { active: false, runId: activeTurnId, runPhase: null, canStop: false };
125
+ }
126
+ let status = null;
127
+ try {
128
+ const response = await this.sendRpc('thread/read', { threadId, includeTurns: false }, 5_000);
129
+ status = response.thread?.status ?? null;
130
+ }
131
+ catch {
132
+ // If the local control channel is still alive but the status read fails,
133
+ // fall back to activeTurnId. This keeps the stop button available for
134
+ // the running turn while avoiding false positives for idle sessions.
135
+ }
136
+ const active = status?.type === 'active' || Boolean(activeTurnId);
137
+ return {
138
+ active,
139
+ runId: activeTurnId,
140
+ runPhase: active ? this.mapThreadStatusToRunPhase(status) : null,
141
+ canStop: Boolean(active && activeTurnId && this.process),
142
+ };
143
+ }
120
144
  spawnCodex(args) {
121
145
  const spec = resolveBuiltinCommand('codex');
122
146
  if (!spec) {
@@ -239,7 +263,7 @@ export class CodexAdapter extends AgentAdapter {
239
263
  async initializeAppServer(modelId) {
240
264
  const codexModelId = normalizeCodexModelId(modelId);
241
265
  await this.sendRpc('initialize', {
242
- clientInfo: { name: 'shennian', title: 'Shennian', version: '0.0.0' },
266
+ clientInfo: CODEX_APP_SERVER_CLIENT_INFO,
243
267
  capabilities: { experimentalApi: true },
244
268
  });
245
269
  if (this.agentSessionId) {
@@ -351,6 +375,27 @@ export class CodexAdapter extends AgentAdapter {
351
375
  return;
352
376
  const params = msg.params ?? {};
353
377
  switch (msg.method) {
378
+ case 'turn/started': {
379
+ const turn = typeof params.turn === 'object' && params.turn !== null
380
+ ? params.turn
381
+ : null;
382
+ const turnId = typeof turn?.id === 'string' ? turn.id : null;
383
+ if (turnId)
384
+ this.activeTurnId = turnId;
385
+ break;
386
+ }
387
+ case 'thread/status/changed': {
388
+ const threadId = typeof params.threadId === 'string' ? params.threadId : '';
389
+ if (threadId && this.agentSessionId && threadId !== this.agentSessionId)
390
+ break;
391
+ const status = typeof params.status === 'object' && params.status !== null
392
+ ? params.status
393
+ : null;
394
+ if (status?.type === 'active') {
395
+ this.emitEvent({ state: 'heartbeat', runPhase: this.mapThreadStatusToRunPhase(status) });
396
+ }
397
+ break;
398
+ }
354
399
  case 'item/agentMessage/delta': {
355
400
  const itemId = typeof params.itemId === 'string' ? params.itemId : '';
356
401
  if (itemId)
@@ -387,6 +432,14 @@ export class CodexAdapter extends AgentAdapter {
387
432
  }
388
433
  }
389
434
  }
435
+ mapThreadStatusToRunPhase(status) {
436
+ const flags = status?.type === 'active' && Array.isArray(status.activeFlags) ? status.activeFlags : [];
437
+ if (flags.includes('waitingOnApproval'))
438
+ return 'waiting_approval';
439
+ if (flags.includes('waitingOnUserInput'))
440
+ return 'waiting_user_input';
441
+ return 'thinking';
442
+ }
390
443
  handleAppServerNonJsonStdout(line) {
391
444
  const normalized = normalizeTerminalText(line);
392
445
  if (!looksLikeCodexInteractiveAuthPrompt(normalized))
@@ -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' | 'wechatRpaPreflightChecks' | 'wechatRpaServerDecisionAvailable' | 'wechatRpaPrivacyConsentAccepted'>;
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,9 @@ 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;
54
+ wechatRpaPreflightChecks?: ExternalChannelSessionStatus['wechatRpaPreflightChecks'];
55
+ wechatRpaServerDecisionAvailable?: boolean | null;
56
+ wechatRpaPrivacyConsentAccepted?: boolean | null;
66
57
  };
67
58
  export type ExternalMessageEvent = {
68
59
  type: 'external.message';
@@ -100,10 +100,8 @@ export declare class ChannelRuntime {
100
100
  downloadAttachments?: boolean;
101
101
  downloadAttachmentsDir?: string;
102
102
  selfNickname?: string;
103
+ privacyConsentAccepted?: boolean;
103
104
  flowScriptPath?: string;
104
- cloudOcrUrl?: string;
105
- cloudOcrToken?: string;
106
- cloudOcrMode?: 'off' | 'fallback' | 'always';
107
105
  }): Promise<ExternalChannelView>;
108
106
  }
109
107
  export type ExternalReplySendPlanItem = {
@@ -350,6 +350,8 @@ export class ChannelRuntime {
350
350
  };
351
351
  const priorSecret = this.secrets.get(nextConfig.secretRef);
352
352
  const source = input.source || (priorSecret?.source === 'macos-probe' || priorSecret?.source === 'fixture-jsonl' || priorSecret?.source === 'macos-flow' || priorSecret?.source === 'windows-visual-flow' || priorSecret?.source === 'wechat-rpa-lab' ? priorSecret.source : defaultWeChatRpaSource());
353
+ if (input.enabled && source === 'windows-visual-flow')
354
+ throw new Error('个人微信通道当前仅支持 macOS');
353
355
  const configs = allConfigs
354
356
  .filter((channel) => channel.id !== nextConfig.id)
355
357
  .map((channel) => (channel.sessionId ?? channel.managerSessionId) === boundSessionId && channel.type === 'wechat-rpa'
@@ -357,7 +359,6 @@ export class ChannelRuntime {
357
359
  : channel);
358
360
  configs.push(nextConfig);
359
361
  this.configs.replaceAll(configs);
360
- const cloudOcrMode = 'off';
361
362
  this.secrets.upsert(nextConfig.secretRef, {
362
363
  type: 'wechat-rpa',
363
364
  source,
@@ -370,10 +371,8 @@ export class ChannelRuntime {
370
371
  downloadAttachments: input.downloadAttachments ?? (priorSecret?.downloadAttachments === undefined ? true : Boolean(priorSecret.downloadAttachments)),
371
372
  downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
372
373
  selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
374
+ privacyConsentAccepted: input.privacyConsentAccepted ?? Boolean(priorSecret?.privacyConsentAccepted),
373
375
  flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
374
- cloudOcrUrl: '',
375
- cloudOcrToken: '',
376
- cloudOcrMode,
377
376
  canReply: input.canReply ?? priorSecret?.canReply ?? false,
378
377
  systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
379
378
  });
@@ -414,6 +413,9 @@ function wechatRpaViewFields(secret) {
414
413
  downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
415
414
  downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : '',
416
415
  selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : '',
416
+ wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
417
+ wechatRpaServerDecisionAvailable: true,
418
+ wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
417
419
  };
418
420
  }
419
421
  function wechatRpaStatusFields(secret) {
@@ -430,6 +432,9 @@ function wechatRpaStatusFields(secret) {
430
432
  downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
431
433
  downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : null,
432
434
  selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : null,
435
+ wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
436
+ wechatRpaServerDecisionAvailable: true,
437
+ wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
433
438
  };
434
439
  }
435
440
  function normalizeWeChatRpaGroups(groups) {
@@ -445,7 +450,29 @@ function normalizeWeChatRpaGroups(groups) {
445
450
  return result;
446
451
  }
447
452
  function defaultWeChatRpaSource() {
448
- return process.platform === 'win32' ? 'windows-visual-flow' : 'wechat-rpa-lab';
453
+ return 'macos-flow';
454
+ }
455
+ function buildWeChatRpaPreflightChecks(secret) {
456
+ const checks = [];
457
+ checks.push({
458
+ code: 'mac_only',
459
+ ok: secret.source !== 'windows-visual-flow',
460
+ severity: 'blocking',
461
+ message: secret.source === 'windows-visual-flow' ? '个人微信通道当前仅支持 macOS。' : '当前配置使用 macOS 微信通道。',
462
+ });
463
+ checks.push({
464
+ code: 'privacy_consent_required',
465
+ ok: Boolean(secret.privacyConsentAccepted),
466
+ severity: 'blocking',
467
+ message: Boolean(secret.privacyConsentAccepted) ? '已确认微信通道数据与隐私授权。' : '启用前需要确认微信通道数据与隐私授权。',
468
+ });
469
+ checks.push({
470
+ code: 'server_decision_unavailable',
471
+ ok: true,
472
+ severity: 'blocking',
473
+ message: '服务端判断能力可用。',
474
+ });
475
+ return checks;
449
476
  }
450
477
  export function planExternalReplySends(channelType, input) {
451
478
  const parts = splitExternalReplyText(input.text);
@@ -17,13 +17,10 @@ type ChannelSecretRecord = {
17
17
  downloadAttachments?: boolean;
18
18
  downloadAttachmentsDir?: string;
19
19
  selfNickname?: string;
20
+ privacyConsentAccepted?: boolean;
20
21
  idleSeconds?: number;
21
22
  recentLimit?: number;
22
- readMode?: 'local-ocr' | 'hybrid-vlm';
23
23
  flowScriptPath?: string;
24
- cloudOcrUrl?: string;
25
- cloudOcrToken?: string;
26
- cloudOcrMode?: 'off' | 'fallback' | 'always';
27
24
  updatedAt: string;
28
25
  };
29
26
  export declare class ChannelSecretRegistry {
@@ -0,0 +1,10 @@
1
+ import type { WeChatChannelObservedMessage } from './client.js';
2
+ export declare function normalizeWeChatAnchorText(value: unknown): string;
3
+ export declare function weChatAnchorText(message: WeChatChannelObservedMessage): string;
4
+ export declare function weChatTextSimilarity(left: unknown, right: unknown): number;
5
+ export declare function isLikelySameWeChatMessage(previous: WeChatChannelObservedMessage, current: WeChatChannelObservedMessage, threshold?: number): boolean;
6
+ export declare function filterNewWeChatMessagesByAnchor(input: {
7
+ previous: WeChatChannelObservedMessage[];
8
+ current: WeChatChannelObservedMessage[];
9
+ threshold?: number;
10
+ }): WeChatChannelObservedMessage[];
@@ -0,0 +1,65 @@
1
+ // @arch docs/features/wechat-rpa-productization-plan.md
2
+ // @test src/__tests__/wechat-channel-anchor.test.ts
3
+ export function normalizeWeChatAnchorText(value) {
4
+ if (typeof value !== 'string')
5
+ return '';
6
+ return value
7
+ .normalize('NFKC')
8
+ .replace(/\s+/g, ' ')
9
+ .trim()
10
+ .toLowerCase();
11
+ }
12
+ export function weChatAnchorText(message) {
13
+ return normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || '');
14
+ }
15
+ export function weChatTextSimilarity(left, right) {
16
+ const a = normalizeWeChatAnchorText(left);
17
+ const b = normalizeWeChatAnchorText(right);
18
+ if (!a || !b)
19
+ return 0;
20
+ if (a === b)
21
+ return 1;
22
+ const distance = levenshtein(a, b);
23
+ return 1 - distance / Math.max(a.length, b.length);
24
+ }
25
+ export function isLikelySameWeChatMessage(previous, current, threshold = 0.86) {
26
+ if (previous.stableMessageKey && previous.stableMessageKey === current.stableMessageKey)
27
+ return true;
28
+ if (previous.senderRole !== current.senderRole)
29
+ return false;
30
+ if (previous.kind !== current.kind)
31
+ return false;
32
+ const previousAnchor = weChatAnchorText(previous);
33
+ const currentAnchor = weChatAnchorText(current);
34
+ if (!previousAnchor || !currentAnchor)
35
+ return false;
36
+ return weChatTextSimilarity(previousAnchor, currentAnchor) >= threshold;
37
+ }
38
+ export function filterNewWeChatMessagesByAnchor(input) {
39
+ const consumed = new Set();
40
+ const result = [];
41
+ for (const current of input.current) {
42
+ const index = input.previous.findIndex((previous, previousIndex) => {
43
+ return !consumed.has(previousIndex) && isLikelySameWeChatMessage(previous, current, input.threshold);
44
+ });
45
+ if (index >= 0) {
46
+ consumed.add(index);
47
+ continue;
48
+ }
49
+ result.push(current);
50
+ }
51
+ return result;
52
+ }
53
+ function levenshtein(a, b) {
54
+ const prev = Array.from({ length: b.length + 1 }, (_, index) => index);
55
+ const curr = Array.from({ length: b.length + 1 }, () => 0);
56
+ for (let i = 1; i <= a.length; i += 1) {
57
+ curr[0] = i;
58
+ for (let j = 1; j <= b.length; j += 1) {
59
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
60
+ }
61
+ for (let j = 0; j <= b.length; j += 1)
62
+ prev[j] = curr[j];
63
+ }
64
+ return prev[b.length];
65
+ }