shennian 0.2.88 → 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 (63) 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.d.ts +8 -0
  7. package/dist/src/agents/codex.js +53 -0
  8. package/dist/src/channels/base.d.ts +4 -1
  9. package/dist/src/channels/runtime.d.ts +1 -0
  10. package/dist/src/channels/runtime.js +32 -1
  11. package/dist/src/channels/secret-registry.d.ts +1 -0
  12. package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
  13. package/dist/src/channels/wechat-channel/anchor.js +65 -0
  14. package/dist/src/channels/wechat-channel/client.d.ts +74 -0
  15. package/dist/src/channels/wechat-channel/client.js +96 -0
  16. package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
  17. package/dist/src/channels/wechat-channel/cooldown.js +38 -0
  18. package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
  19. package/dist/src/channels/wechat-channel/fingerprint.js +71 -0
  20. package/dist/src/channels/wechat-channel/helper-assets.d.ts +28 -0
  21. package/dist/src/channels/wechat-channel/helper-assets.js +68 -0
  22. package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
  23. package/dist/src/channels/wechat-channel/helper-client.js +149 -0
  24. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
  25. package/dist/src/channels/wechat-channel/helper-protocol.js +115 -0
  26. package/dist/src/channels/wechat-channel/index.d.ts +16 -0
  27. package/dist/src/channels/wechat-channel/index.js +19 -0
  28. package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
  29. package/dist/src/channels/wechat-channel/ledger.js +54 -0
  30. package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
  31. package/dist/src/channels/wechat-channel/media-resolver.js +181 -0
  32. package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
  33. package/dist/src/channels/wechat-channel/message-key.js +105 -0
  34. package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
  35. package/dist/src/channels/wechat-channel/observer.js +118 -0
  36. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +66 -0
  37. package/dist/src/channels/wechat-channel/outbound-ledger.js +112 -0
  38. package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
  39. package/dist/src/channels/wechat-channel/preflight.js +48 -0
  40. package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
  41. package/dist/src/channels/wechat-channel/runner.js +84 -0
  42. package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
  43. package/dist/src/channels/wechat-channel/runtime.js +66 -0
  44. package/dist/src/channels/wechat-channel/scheduler.d.ts +30 -0
  45. package/dist/src/channels/wechat-channel/scheduler.js +152 -0
  46. package/dist/src/channels/wechat-rpa.d.ts +21 -0
  47. package/dist/src/channels/wechat-rpa.js +12 -6
  48. package/dist/src/commands/manager.js +2 -0
  49. package/dist/src/fs/text-decoder.d.ts +10 -0
  50. package/dist/src/fs/text-decoder.js +110 -0
  51. package/dist/src/manager/runtime.js +4 -0
  52. package/dist/src/native-fusion/service.d.ts +10 -0
  53. package/dist/src/native-fusion/service.js +27 -0
  54. package/dist/src/session/handlers/chat.js +18 -0
  55. package/dist/src/session/handlers/fs.js +39 -3
  56. package/dist/src/session/handlers/session-refresh.js +12 -0
  57. package/dist/src/session/handlers/tool-detail.d.ts +3 -0
  58. package/dist/src/session/handlers/tool-detail.js +218 -0
  59. package/dist/src/session/manager.d.ts +3 -0
  60. package/dist/src/session/manager.js +58 -0
  61. package/dist/src/session/types.d.ts +4 -0
  62. package/package.json +2 -2
  63. package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
@@ -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,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;
@@ -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) {
@@ -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))
@@ -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'>;
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;
@@ -51,6 +51,9 @@ export type ExternalChannelView = {
51
51
  wechatRpaLastTracePath?: string | null;
52
52
  wechatRpaLastTraceSummary?: string | null;
53
53
  wechatRpaRecentTaskSummaries?: ExternalChannelSessionStatus['wechatRpaRecentTaskSummaries'];
54
+ wechatRpaPreflightChecks?: ExternalChannelSessionStatus['wechatRpaPreflightChecks'];
55
+ wechatRpaServerDecisionAvailable?: boolean | null;
56
+ wechatRpaPrivacyConsentAccepted?: boolean | null;
54
57
  };
55
58
  export type ExternalMessageEvent = {
56
59
  type: 'external.message';
@@ -100,6 +100,7 @@ export declare class ChannelRuntime {
100
100
  downloadAttachments?: boolean;
101
101
  downloadAttachmentsDir?: string;
102
102
  selfNickname?: string;
103
+ privacyConsentAccepted?: boolean;
103
104
  flowScriptPath?: string;
104
105
  }): Promise<ExternalChannelView>;
105
106
  }
@@ -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'
@@ -369,6 +371,7 @@ export class ChannelRuntime {
369
371
  downloadAttachments: input.downloadAttachments ?? (priorSecret?.downloadAttachments === undefined ? true : Boolean(priorSecret.downloadAttachments)),
370
372
  downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
371
373
  selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
374
+ privacyConsentAccepted: input.privacyConsentAccepted ?? Boolean(priorSecret?.privacyConsentAccepted),
372
375
  flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
373
376
  canReply: input.canReply ?? priorSecret?.canReply ?? false,
374
377
  systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
@@ -410,6 +413,9 @@ function wechatRpaViewFields(secret) {
410
413
  downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
411
414
  downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : '',
412
415
  selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : '',
416
+ wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
417
+ wechatRpaServerDecisionAvailable: true,
418
+ wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
413
419
  };
414
420
  }
415
421
  function wechatRpaStatusFields(secret) {
@@ -426,6 +432,9 @@ function wechatRpaStatusFields(secret) {
426
432
  downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
427
433
  downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : null,
428
434
  selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : null,
435
+ wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
436
+ wechatRpaServerDecisionAvailable: true,
437
+ wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
429
438
  };
430
439
  }
431
440
  function normalizeWeChatRpaGroups(groups) {
@@ -441,7 +450,29 @@ function normalizeWeChatRpaGroups(groups) {
441
450
  return result;
442
451
  }
443
452
  function defaultWeChatRpaSource() {
444
- return '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;
445
476
  }
446
477
  export function planExternalReplySends(channelType, input) {
447
478
  const parts = splitExternalReplyText(input.text);
@@ -17,6 +17,7 @@ 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
23
  flowScriptPath?: string;
@@ -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
+ }
@@ -0,0 +1,74 @@
1
+ import type { WeChatChannelRuntime, WeChatChannelBindingConfig } from './runtime.js';
2
+ export type WeChatChannelApiClientOptions = {
3
+ serverUrl?: string;
4
+ machineToken?: string;
5
+ fetchImpl?: typeof fetch;
6
+ };
7
+ export type WeChatChannelObservedMessage = {
8
+ stableMessageKey: string;
9
+ senderRole: 'self' | 'contact' | 'system' | 'unknown';
10
+ senderName?: string | null;
11
+ kind: string;
12
+ normalizedText?: string | null;
13
+ anchorText?: string | null;
14
+ anchorMetadata?: unknown;
15
+ neighborContext?: unknown;
16
+ textExcerpt?: string | null;
17
+ bbox?: unknown;
18
+ mediaMetadata?: unknown;
19
+ isBaseline?: boolean;
20
+ deliveryStatus?: string;
21
+ observedAt?: string;
22
+ visualBlocks?: Array<{
23
+ blockId: string;
24
+ blockKind: string;
25
+ bbox?: unknown;
26
+ model: string;
27
+ dims: number;
28
+ vectorBase64: string;
29
+ }>;
30
+ };
31
+ export type WeChatChannelApiClient = ReturnType<typeof createWeChatChannelApiClient>;
32
+ export type WeChatChannelObserveInput = {
33
+ screenshots: Array<{
34
+ captureIndex?: number;
35
+ mimeType: string;
36
+ dataBase64: string;
37
+ width: number;
38
+ height: number;
39
+ }>;
40
+ edgeOcrBlocks?: unknown[];
41
+ visibleConversationFingerprints?: unknown[];
42
+ localLedgerTailAnchors?: unknown[];
43
+ };
44
+ export type WeChatChannelObserveResponse = {
45
+ ok: true;
46
+ observedMessages?: WeChatChannelObservedMessage[];
47
+ diff?: unknown;
48
+ reasonCode?: string;
49
+ };
50
+ export type WeChatChannelOutboundStatusInput = {
51
+ replyId: string;
52
+ idempotencyKey: string;
53
+ status: string;
54
+ replyBaseRevision?: number;
55
+ sentAt?: string;
56
+ confirmedAt?: string;
57
+ failureCode?: string;
58
+ lastErrorSummary?: string;
59
+ };
60
+ export declare function createWeChatChannelApiClient(options?: WeChatChannelApiClientOptions): {
61
+ getRuntimePolicy: () => Promise<unknown>;
62
+ upsertRuntime: (runtime: WeChatChannelRuntime, binding?: WeChatChannelBindingConfig) => Promise<unknown>;
63
+ observe: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: WeChatChannelObserveInput) => Promise<WeChatChannelObserveResponse>;
64
+ ingest: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: {
65
+ idempotencyKey: string;
66
+ messages: WeChatChannelObservedMessage[];
67
+ }) => Promise<unknown>;
68
+ reportOutboundStatus: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: WeChatChannelOutboundStatusInput) => Promise<unknown>;
69
+ reportRunStatus: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: {
70
+ status: string;
71
+ reasonCode?: string;
72
+ traceId?: string;
73
+ }) => Promise<unknown>;
74
+ };