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,64 @@
1
+ import type { WeChatChannelHelperCommandName, WeChatChannelHelperResponse } from './helper-protocol.js';
2
+ import type { WeChatChannelRuntime, WeChatChannelBindingConfig } from './runtime.js';
3
+ import type { WeChatChannelApiClient, WeChatChannelObservedMessage } from './client.js';
4
+ import { type WeChatConversationListFingerprint } from './fingerprint.js';
5
+ export type WeChatChannelHelperTransport = {
6
+ request<T = unknown>(command: WeChatChannelHelperCommandName, params?: Record<string, unknown>, traceId?: string): Promise<WeChatChannelHelperResponse<T>>;
7
+ };
8
+ export type WeChatChannelWindowInfo = {
9
+ windowId: string;
10
+ appName?: string | null;
11
+ title?: string | null;
12
+ bounds?: {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ coordinateSpace?: string;
18
+ };
19
+ };
20
+ export type WeChatChannelScreenshot = {
21
+ mimeType: string;
22
+ dataBase64: string;
23
+ width: number;
24
+ height: number;
25
+ windowId?: string | null;
26
+ };
27
+ export type WeChatChannelOcrBlock = {
28
+ text?: string;
29
+ bbox?: {
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ coordinateSpace?: string;
35
+ };
36
+ confidence?: number;
37
+ };
38
+ export type WeChatChannelOcrResult = {
39
+ blocks?: WeChatChannelOcrBlock[];
40
+ visibleConversationFingerprints?: WeChatConversationListFingerprint[];
41
+ };
42
+ export type WeChatChannelObserveApi = Pick<WeChatChannelApiClient, 'observe'>;
43
+ export type HelperBackedObserveOptions = {
44
+ runtime: WeChatChannelRuntime;
45
+ binding: WeChatChannelBindingConfig;
46
+ helper: WeChatChannelHelperTransport;
47
+ api: WeChatChannelObserveApi;
48
+ traceId?: string;
49
+ localLedgerTailAnchors?: unknown[];
50
+ };
51
+ export declare function observeWeChatChannelBindingViaHelper(options: HelperBackedObserveOptions): Promise<WeChatChannelObservedMessage[]>;
52
+ export declare function ensureHelperPreflight(helper: WeChatChannelHelperTransport, traceId?: string): Promise<void>;
53
+ export declare function focusWeChatWindow(helper: WeChatChannelHelperTransport, traceId?: string): Promise<WeChatChannelWindowInfo>;
54
+ export declare function openConversationInVisibleList(input: {
55
+ helper: WeChatChannelHelperTransport;
56
+ window: WeChatChannelWindowInfo;
57
+ binding: WeChatChannelBindingConfig;
58
+ traceId?: string;
59
+ }): Promise<{
60
+ opened: boolean;
61
+ reason: string;
62
+ }>;
63
+ export declare function captureWeChatWindow(helper: WeChatChannelHelperTransport, windowId: string, traceId?: string): Promise<WeChatChannelScreenshot>;
64
+ export declare function recognizeWeChatScreenshot(helper: WeChatChannelHelperTransport, screenshot: WeChatChannelScreenshot, traceId?: string): Promise<WeChatChannelOcrResult>;
@@ -0,0 +1,118 @@
1
+ // @arch docs/features/wechat-rpa-productization-plan.md
2
+ // @arch docs/features/wechat-rpa-helper-runtime.md
3
+ // @test src/__tests__/wechat-channel-observer.test.ts
4
+ import { matchWeChatConversationFingerprints, } from './fingerprint.js';
5
+ export async function observeWeChatChannelBindingViaHelper(options) {
6
+ await ensureHelperPreflight(options.helper, options.traceId);
7
+ const window = await focusWeChatWindow(options.helper, options.traceId);
8
+ await openConversationInVisibleList({
9
+ helper: options.helper,
10
+ window,
11
+ binding: options.binding,
12
+ traceId: options.traceId,
13
+ });
14
+ const capture = await captureWeChatWindow(options.helper, window.windowId, options.traceId);
15
+ const ocr = await recognizeWeChatScreenshot(options.helper, capture, options.traceId);
16
+ const decision = await options.api.observe(options.runtime, options.binding, {
17
+ screenshots: [{
18
+ captureIndex: 0,
19
+ mimeType: capture.mimeType,
20
+ dataBase64: capture.dataBase64,
21
+ width: capture.width,
22
+ height: capture.height,
23
+ }],
24
+ edgeOcrBlocks: ocr.blocks ?? [],
25
+ visibleConversationFingerprints: ocr.visibleConversationFingerprints ?? [],
26
+ localLedgerTailAnchors: options.localLedgerTailAnchors ?? [],
27
+ });
28
+ return decision.observedMessages ?? [];
29
+ }
30
+ export async function ensureHelperPreflight(helper, traceId) {
31
+ const response = await helper.request('permissions.check', {}, traceId);
32
+ assertHelperOk(response, 'permissions.check');
33
+ const result = response.result ?? {};
34
+ if (result.screenRecording === false)
35
+ throw new Error('permission_screen_recording_missing');
36
+ if (result.accessibility === false)
37
+ throw new Error('permission_accessibility_missing');
38
+ if (result.automation === false)
39
+ throw new Error('permission_automation_missing');
40
+ if (result.wechatRunning === false)
41
+ throw new Error('wechat_not_running');
42
+ if (result.wechatWindowAvailable === false)
43
+ throw new Error('wechat_window_unavailable');
44
+ }
45
+ export async function focusWeChatWindow(helper, traceId) {
46
+ const response = await helper.request('windows.list', {}, traceId);
47
+ assertHelperOk(response, 'windows.list');
48
+ const window = (response.result?.windows ?? []).find(isWeChatWindow);
49
+ if (!window)
50
+ throw new Error('wechat_window_unavailable');
51
+ const focus = await helper.request('windows.focus', { windowId: window.windowId }, traceId);
52
+ assertHelperOk(focus, 'windows.focus');
53
+ return window;
54
+ }
55
+ export async function openConversationInVisibleList(input) {
56
+ const capture = await captureWeChatWindow(input.helper, input.window.windowId, input.traceId);
57
+ const ocr = await recognizeWeChatScreenshot(input.helper, capture, input.traceId);
58
+ const match = matchWeChatConversationFingerprints({
59
+ visibleItems: ocr.visibleConversationFingerprints ?? [],
60
+ targets: [{
61
+ bindingId: input.binding.bindingId,
62
+ conversationDisplayName: input.binding.conversationDisplayName,
63
+ }],
64
+ })[0];
65
+ if (!match)
66
+ return { opened: false, reason: 'conversation_not_visible' };
67
+ const center = bboxCenter(match.item.bbox);
68
+ if (!center)
69
+ return { opened: false, reason: 'conversation_bbox_missing' };
70
+ const click = await input.helper.request('mouse.click', {
71
+ x: center.x,
72
+ y: center.y,
73
+ coordinateSpace: center.coordinateSpace,
74
+ windowId: input.window.windowId,
75
+ }, input.traceId);
76
+ assertHelperOk(click, 'mouse.click');
77
+ return { opened: true, reason: match.reason };
78
+ }
79
+ export async function captureWeChatWindow(helper, windowId, traceId) {
80
+ const response = await helper.request('windows.capture', { windowId, scope: 'full-window' }, traceId);
81
+ assertHelperOk(response, 'windows.capture');
82
+ const result = response.result;
83
+ if (!result?.dataBase64 || !result.mimeType || !result.width || !result.height) {
84
+ throw new Error('helper_invalid_response: windows.capture missing screenshot data');
85
+ }
86
+ return result;
87
+ }
88
+ export async function recognizeWeChatScreenshot(helper, screenshot, traceId) {
89
+ const response = await helper.request('ocr.recognize', {
90
+ mimeType: screenshot.mimeType,
91
+ dataBase64: screenshot.dataBase64,
92
+ width: screenshot.width,
93
+ height: screenshot.height,
94
+ }, traceId);
95
+ assertHelperOk(response, 'ocr.recognize');
96
+ return response.result ?? {};
97
+ }
98
+ function assertHelperOk(response, command) {
99
+ if (!response.ok)
100
+ throw new Error(`${response.errorCode || 'helper_command_failed'}: ${response.errorSummary || command}`);
101
+ }
102
+ function isWeChatWindow(window) {
103
+ const haystack = `${window.appName || ''} ${window.title || ''}`.toLowerCase();
104
+ return haystack.includes('wechat') || haystack.includes('微信');
105
+ }
106
+ function bboxCenter(value) {
107
+ if (!value || typeof value !== 'object')
108
+ return null;
109
+ const bbox = value;
110
+ const x = Number(bbox.x);
111
+ const y = Number(bbox.y);
112
+ const width = Number(bbox.width);
113
+ const height = Number(bbox.height);
114
+ if (![x, y, width, height].every(Number.isFinite))
115
+ return null;
116
+ const coordinateSpace = typeof bbox.coordinateSpace === 'string' ? bbox.coordinateSpace : undefined;
117
+ return { x: x + width / 2, y: y + height / 2, coordinateSpace };
118
+ }
@@ -0,0 +1,66 @@
1
+ import type { WeChatChannelObservedMessage } from './client.js';
2
+ export type WeChatChannelOutboundStatus = 'queued' | 'sending' | 'sent_unconfirmed' | 'confirmed_echo' | 'failed' | 'stale' | 'manual_review';
3
+ export type WeChatChannelOutboundRecord = {
4
+ replyId: string;
5
+ idempotencyKey: string;
6
+ bindingId: string;
7
+ runtimeId: string;
8
+ sessionId: string;
9
+ conversationName: string;
10
+ replyBaseRevision: number;
11
+ textNormalized?: string;
12
+ attachmentLocalRefs?: string[];
13
+ createdAt: string;
14
+ queuedAt?: string;
15
+ sentAt?: string;
16
+ confirmedAt?: string;
17
+ sendStatus: WeChatChannelOutboundStatus;
18
+ expectedEchoAnchor?: string;
19
+ confirmedEchoAnchor?: string;
20
+ observeAttemptsAfterSent?: number;
21
+ failureCode?: string;
22
+ lastErrorSummary?: string;
23
+ };
24
+ export type WeChatChannelOutboundLedger = {
25
+ version: 1;
26
+ runtimeId: string;
27
+ records: WeChatChannelOutboundRecord[];
28
+ };
29
+ export type EchoClassificationResult = {
30
+ remainingMessages: WeChatChannelObservedMessage[];
31
+ confirmedRecords: WeChatChannelOutboundRecord[];
32
+ manualReviewRecords: WeChatChannelOutboundRecord[];
33
+ };
34
+ export declare function loadWeChatChannelOutboundLedger(filePath: string, runtimeId: string): WeChatChannelOutboundLedger;
35
+ export declare function saveWeChatChannelOutboundLedger(filePath: string, ledger: WeChatChannelOutboundLedger): void;
36
+ export declare function enqueueWeChatOutboundReply(input: {
37
+ ledger: WeChatChannelOutboundLedger;
38
+ replyId: string;
39
+ idempotencyKey: string;
40
+ bindingId: string;
41
+ runtimeId: string;
42
+ sessionId: string;
43
+ conversationName: string;
44
+ replyBaseRevision: number;
45
+ text?: string;
46
+ attachmentLocalRefs?: string[];
47
+ now?: Date;
48
+ }): WeChatChannelOutboundRecord;
49
+ export declare function guardWeChatOutboundRevision(input: {
50
+ record: WeChatChannelOutboundRecord;
51
+ currentLastInboundRevision: number;
52
+ now?: Date;
53
+ }): {
54
+ ok: true;
55
+ } | {
56
+ ok: false;
57
+ reason: 'stale';
58
+ };
59
+ export declare function markWeChatOutboundSentUnconfirmed(record: WeChatChannelOutboundRecord, now?: Date): void;
60
+ export declare function classifyWeChatOutboundEchoes(input: {
61
+ ledger: WeChatChannelOutboundLedger;
62
+ bindingId: string;
63
+ messages: WeChatChannelObservedMessage[];
64
+ now?: Date;
65
+ }): EchoClassificationResult;
66
+ export declare function suppressSelfOnlyWeChatMessages(messages: WeChatChannelObservedMessage[]): WeChatChannelObservedMessage[];
@@ -0,0 +1,112 @@
1
+ // @arch docs/features/wechat-rpa-outbound-ledger.md
2
+ // @test src/__tests__/wechat-channel-outbound-ledger.test.ts
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { normalizeWeChatAnchorText, weChatTextSimilarity } from './anchor.js';
6
+ const MAX_OUTBOUND_RECORDS = 500;
7
+ const MANUAL_REVIEW_AFTER_OBSERVE_ATTEMPTS = 2;
8
+ const MANUAL_REVIEW_AFTER_MS = 10 * 60 * 1000;
9
+ export function loadWeChatChannelOutboundLedger(filePath, runtimeId) {
10
+ try {
11
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
12
+ if (parsed?.version === 1 && parsed.runtimeId === runtimeId && Array.isArray(parsed.records))
13
+ return parsed;
14
+ }
15
+ catch { }
16
+ return { version: 1, runtimeId, records: [] };
17
+ }
18
+ export function saveWeChatChannelOutboundLedger(filePath, ledger) {
19
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
+ ledger.records = ledger.records.slice(-MAX_OUTBOUND_RECORDS);
21
+ fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2));
22
+ }
23
+ export function enqueueWeChatOutboundReply(input) {
24
+ const existing = input.ledger.records.find((record) => record.idempotencyKey === input.idempotencyKey);
25
+ if (existing)
26
+ return existing;
27
+ const now = (input.now ?? new Date()).toISOString();
28
+ const textNormalized = normalizeWeChatAnchorText(input.text);
29
+ const record = {
30
+ replyId: input.replyId,
31
+ idempotencyKey: input.idempotencyKey,
32
+ bindingId: input.bindingId,
33
+ runtimeId: input.runtimeId,
34
+ sessionId: input.sessionId,
35
+ conversationName: input.conversationName,
36
+ replyBaseRevision: input.replyBaseRevision,
37
+ textNormalized: textNormalized || undefined,
38
+ attachmentLocalRefs: input.attachmentLocalRefs ?? [],
39
+ createdAt: now,
40
+ queuedAt: now,
41
+ sendStatus: 'queued',
42
+ expectedEchoAnchor: textNormalized || undefined,
43
+ observeAttemptsAfterSent: 0,
44
+ };
45
+ input.ledger.records.push(record);
46
+ return record;
47
+ }
48
+ export function guardWeChatOutboundRevision(input) {
49
+ if (input.currentLastInboundRevision <= input.record.replyBaseRevision)
50
+ return { ok: true };
51
+ input.record.sendStatus = 'stale';
52
+ input.record.failureCode = 'reply_revision_stale';
53
+ input.record.lastErrorSummary = 'Inbound revision advanced before reply send';
54
+ input.record.confirmedAt = input.record.confirmedAt;
55
+ return { ok: false, reason: 'stale' };
56
+ }
57
+ export function markWeChatOutboundSentUnconfirmed(record, now = new Date()) {
58
+ record.sendStatus = 'sent_unconfirmed';
59
+ record.sentAt = now.toISOString();
60
+ record.observeAttemptsAfterSent = 0;
61
+ }
62
+ export function classifyWeChatOutboundEchoes(input) {
63
+ const now = input.now ?? new Date();
64
+ const confirmedRecords = [];
65
+ const manualReviewRecords = [];
66
+ const pendingRecords = input.ledger.records.filter((record) => record.bindingId === input.bindingId && record.sendStatus === 'sent_unconfirmed');
67
+ const consumedMessageIndexes = new Set();
68
+ for (const record of pendingRecords) {
69
+ const matchIndex = input.messages.findIndex((message, index) => !consumedMessageIndexes.has(index) && isOutboundEcho(record, message));
70
+ if (matchIndex >= 0) {
71
+ consumedMessageIndexes.add(matchIndex);
72
+ const message = input.messages[matchIndex];
73
+ record.sendStatus = 'confirmed_echo';
74
+ record.confirmedAt = now.toISOString();
75
+ record.confirmedEchoAnchor = normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || '') || undefined;
76
+ confirmedRecords.push(record);
77
+ continue;
78
+ }
79
+ record.observeAttemptsAfterSent = (record.observeAttemptsAfterSent ?? 0) + 1;
80
+ if (shouldMoveOutboundToManualReview(record, now)) {
81
+ record.sendStatus = 'manual_review';
82
+ record.failureCode = 'echo_confirmation_timeout';
83
+ record.lastErrorSummary = 'Sent reply echo was not confirmed after observe threshold';
84
+ manualReviewRecords.push(record);
85
+ }
86
+ }
87
+ return {
88
+ remainingMessages: input.messages.filter((_, index) => !consumedMessageIndexes.has(index)),
89
+ confirmedRecords,
90
+ manualReviewRecords,
91
+ };
92
+ }
93
+ export function suppressSelfOnlyWeChatMessages(messages) {
94
+ return messages.filter((message) => message.senderRole !== 'self');
95
+ }
96
+ function isOutboundEcho(record, message) {
97
+ if (message.senderRole !== 'self')
98
+ return false;
99
+ const expected = record.expectedEchoAnchor || record.textNormalized || '';
100
+ const actual = normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || '');
101
+ if (!expected || !actual)
102
+ return false;
103
+ return weChatTextSimilarity(expected, actual) >= 0.9;
104
+ }
105
+ function shouldMoveOutboundToManualReview(record, now) {
106
+ if ((record.observeAttemptsAfterSent ?? 0) >= MANUAL_REVIEW_AFTER_OBSERVE_ATTEMPTS)
107
+ return true;
108
+ if (!record.sentAt)
109
+ return false;
110
+ const sentAt = new Date(record.sentAt).getTime();
111
+ return Number.isFinite(sentAt) && now.getTime() - sentAt >= MANUAL_REVIEW_AFTER_MS;
112
+ }
@@ -0,0 +1,37 @@
1
+ import { type WeChatChannelHelperReady, type WeChatChannelHelperErrorCode } from './helper-protocol.js';
2
+ import type { WeChatChannelRuntime, WeChatChannelBindingConfig } from './runtime.js';
3
+ export type WeChatChannelPreflightSeverity = 'blocking' | 'warning';
4
+ export type WeChatChannelPreflightCode = 'ok' | 'mac_only' | 'enterprise_required' | 'privacy_consent_required' | 'server_decision_unavailable' | 'same_machine_required' | 'local_agent_unavailable' | 'helper_not_ready' | WeChatChannelHelperErrorCode;
5
+ export type WeChatChannelPreflightCheck = {
6
+ code: WeChatChannelPreflightCode;
7
+ ok: boolean;
8
+ severity: WeChatChannelPreflightSeverity;
9
+ message: string;
10
+ };
11
+ export type WeChatChannelPreflightInput = {
12
+ platform: string;
13
+ accountTier?: string | null;
14
+ privacyConsentAccepted?: boolean;
15
+ serverDecisionAvailable?: boolean;
16
+ currentMachineId?: string | null;
17
+ localAgentMachineId?: string | null;
18
+ localAgentAvailable?: boolean;
19
+ runtime: WeChatChannelRuntime;
20
+ binding?: WeChatChannelBindingConfig | null;
21
+ expectedHelperVersion: string;
22
+ helperReady?: WeChatChannelHelperReady | null;
23
+ permissions?: {
24
+ screenRecording?: boolean;
25
+ accessibility?: boolean;
26
+ automation?: boolean;
27
+ wechatRunning?: boolean;
28
+ wechatWindowAvailable?: boolean;
29
+ } | null;
30
+ };
31
+ export type WeChatChannelPreflightResult = {
32
+ ok: boolean;
33
+ canEnable: boolean;
34
+ status: 'ready' | 'blocked';
35
+ checks: WeChatChannelPreflightCheck[];
36
+ };
37
+ export declare function evaluateWeChatChannelPreflight(input: WeChatChannelPreflightInput): WeChatChannelPreflightResult;
@@ -0,0 +1,48 @@
1
+ // @arch docs/features/wechat-rpa-productization-plan.md
2
+ // @arch docs/features/wechat-rpa-helper-runtime.md
3
+ // @test src/__tests__/wechat-channel-preflight.test.ts
4
+ import { validateWeChatChannelHelperReady, } from './helper-protocol.js';
5
+ export function evaluateWeChatChannelPreflight(input) {
6
+ const checks = [];
7
+ checks.push(blockingCheck(input.platform === 'darwin', 'mac_only', '当前阶段微信 Channel 只支持 macOS。'));
8
+ checks.push(blockingCheck(input.accountTier === 'enterprise', 'enterprise_required', '当前阶段微信 Channel 仅企业版账号可用。'));
9
+ checks.push(blockingCheck(Boolean(input.privacyConsentAccepted), 'privacy_consent_required', '启用前需要确认微信 Channel 数据与隐私授权。'));
10
+ checks.push(blockingCheck(Boolean(input.serverDecisionAvailable), 'server_decision_unavailable', '服务端判断能力不可用,暂不能启用。'));
11
+ if (input.currentMachineId && input.runtime.machineId) {
12
+ checks.push(blockingCheck(input.currentMachineId === input.runtime.machineId, 'same_machine_required', '第一版只支持同机微信与同机神念运行时绑定。'));
13
+ }
14
+ if (input.localAgentMachineId && input.runtime.machineId) {
15
+ checks.push(blockingCheck(input.localAgentMachineId === input.runtime.machineId, 'same_machine_required', '绑定的 Agent 必须与微信运行时在同一台机器。'));
16
+ }
17
+ checks.push(blockingCheck(input.localAgentAvailable !== false, 'local_agent_unavailable', '本机 Agent 不可用,无法安全接管微信回复。'));
18
+ if (!input.helperReady) {
19
+ checks.push(blockingCheck(false, 'helper_not_ready', 'macOS helper 未就绪。'));
20
+ }
21
+ else {
22
+ const validation = validateWeChatChannelHelperReady(input.helperReady, input.expectedHelperVersion);
23
+ if (!validation.ok) {
24
+ checks.push(blockingCheck(false, validation.errorCode, validation.errorSummary));
25
+ }
26
+ else {
27
+ checks.push(blockingCheck(true, 'ok', 'macOS helper 版本与能力检查通过。'));
28
+ }
29
+ }
30
+ const permissions = input.permissions;
31
+ if (permissions) {
32
+ checks.push(blockingCheck(permissions.screenRecording !== false, 'permission_screen_recording_missing', '缺少屏幕录制权限。'));
33
+ checks.push(blockingCheck(permissions.accessibility !== false, 'permission_accessibility_missing', '缺少辅助功能权限。'));
34
+ checks.push(blockingCheck(permissions.automation !== false, 'permission_automation_missing', '缺少自动化控制权限。'));
35
+ checks.push(blockingCheck(permissions.wechatRunning !== false, 'wechat_not_running', '微信未运行。'));
36
+ checks.push(blockingCheck(permissions.wechatWindowAvailable !== false, 'wechat_window_unavailable', '微信窗口不可用。'));
37
+ }
38
+ const blockingFailures = checks.filter((check) => !check.ok && check.severity === 'blocking');
39
+ return {
40
+ ok: blockingFailures.length === 0,
41
+ canEnable: blockingFailures.length === 0,
42
+ status: blockingFailures.length === 0 ? 'ready' : 'blocked',
43
+ checks,
44
+ };
45
+ }
46
+ function blockingCheck(ok, code, message) {
47
+ return { ok, code: ok ? 'ok' : code, severity: 'blocking', message };
48
+ }
@@ -0,0 +1,34 @@
1
+ import type { WeChatChannelRuntime, WeChatChannelBindingConfig } from './runtime.js';
2
+ import { type WeChatChannelApiClient, type WeChatChannelObservedMessage } from './client.js';
3
+ import { type WeChatChannelHelperClientOptions } from './helper-client.js';
4
+ import type { WeChatChannelHelperTransport } from './observer.js';
5
+ import { WeChatChannelScheduler, type WeChatChannelSchedulerTickResult } from './scheduler.js';
6
+ export type WeChatChannelProductRunnerOptions = {
7
+ runtime: WeChatChannelRuntime;
8
+ workDir: string;
9
+ api?: WeChatChannelApiClient;
10
+ helper?: WeChatChannelHelperTransport & {
11
+ start?: () => Promise<unknown>;
12
+ stop?: () => Promise<void>;
13
+ };
14
+ helperClientOptions?: WeChatChannelHelperClientOptions;
15
+ ledgerPath?: string;
16
+ outboundLedgerPath?: string;
17
+ onInboundMessages?: (binding: WeChatChannelBindingConfig, messages: WeChatChannelObservedMessage[]) => Promise<void> | void;
18
+ };
19
+ export declare class WeChatChannelProductRunner {
20
+ private options;
21
+ readonly scheduler: WeChatChannelScheduler;
22
+ readonly helper: WeChatChannelHelperTransport & {
23
+ start?: () => Promise<unknown>;
24
+ stop?: () => Promise<void>;
25
+ };
26
+ private helperStarted;
27
+ constructor(options: WeChatChannelProductRunnerOptions);
28
+ start(): Promise<void>;
29
+ tick(): Promise<WeChatChannelSchedulerTickResult | void>;
30
+ stop(): Promise<void>;
31
+ private ensureHelperStarted;
32
+ }
33
+ export declare function createWeChatChannelProductRunner(options: WeChatChannelProductRunnerOptions): WeChatChannelProductRunner;
34
+ export declare function buildLocalLedgerTailAnchors(ledgerPath: string, runtimeId: string, bindingId: string, limit?: number): Array<Record<string, unknown>>;
@@ -0,0 +1,84 @@
1
+ // @arch docs/features/wechat-rpa-productization-plan.md
2
+ // @test src/__tests__/wechat-channel-runner.test.ts
3
+ import path from 'node:path';
4
+ import { createWeChatChannelApiClient, } from './client.js';
5
+ import { WeChatChannelHelperClient } from './helper-client.js';
6
+ import { resolveWeChatChannelHelperAsset, WECHAT_CHANNEL_HELPER_VERSION } from './helper-assets.js';
7
+ import { observeWeChatChannelBindingViaHelper } from './observer.js';
8
+ import { WeChatChannelScheduler } from './scheduler.js';
9
+ import { loadWeChatChannelLedger } from './ledger.js';
10
+ export class WeChatChannelProductRunner {
11
+ options;
12
+ scheduler;
13
+ helper;
14
+ helperStarted = false;
15
+ constructor(options) {
16
+ this.options = options;
17
+ const api = options.api ?? createWeChatChannelApiClient();
18
+ this.helper = options.helper ?? createHelperClient(options.helperClientOptions);
19
+ this.scheduler = new WeChatChannelScheduler({
20
+ runtime: options.runtime,
21
+ workDir: options.workDir,
22
+ api,
23
+ ledgerPath: options.ledgerPath,
24
+ outboundLedgerPath: options.outboundLedgerPath,
25
+ onInboundMessages: options.onInboundMessages,
26
+ observeBinding: (binding) => observeWeChatChannelBindingViaHelper({
27
+ runtime: options.runtime,
28
+ binding,
29
+ helper: this.helper,
30
+ api,
31
+ localLedgerTailAnchors: buildLocalLedgerTailAnchors(options.ledgerPath ?? defaultLedgerPath(options.workDir, options.runtime.runtimeId), options.runtime.runtimeId, binding.bindingId),
32
+ }),
33
+ });
34
+ }
35
+ async start() {
36
+ await this.ensureHelperStarted();
37
+ await this.scheduler.start();
38
+ }
39
+ async tick() {
40
+ await this.ensureHelperStarted();
41
+ return this.scheduler.tick();
42
+ }
43
+ async stop() {
44
+ this.scheduler.stop();
45
+ if (this.helperStarted && this.helper.stop)
46
+ await this.helper.stop();
47
+ this.helperStarted = false;
48
+ }
49
+ async ensureHelperStarted() {
50
+ if (this.helperStarted)
51
+ return;
52
+ if (this.helper.start)
53
+ await this.helper.start();
54
+ this.helperStarted = true;
55
+ }
56
+ }
57
+ export function createWeChatChannelProductRunner(options) {
58
+ return new WeChatChannelProductRunner(options);
59
+ }
60
+ export function buildLocalLedgerTailAnchors(ledgerPath, runtimeId, bindingId, limit = 3) {
61
+ const ledger = loadWeChatChannelLedger(ledgerPath, runtimeId);
62
+ const recent = ledger.bindings[bindingId]?.recent ?? [];
63
+ return recent.slice(-limit).map((message) => ({
64
+ stableMessageKey: message.stableMessageKey,
65
+ senderRole: message.senderRole,
66
+ kind: message.kind,
67
+ anchorText: message.anchorText ?? message.normalizedText ?? message.textExcerpt ?? null,
68
+ anchorMetadata: message.anchorMetadata ?? null,
69
+ }));
70
+ }
71
+ function createHelperClient(options) {
72
+ if (options)
73
+ return new WeChatChannelHelperClient(options);
74
+ const resolved = resolveWeChatChannelHelperAsset();
75
+ if (!resolved.ok)
76
+ throw new Error(`${resolved.reasonCode}: ${resolved.message}`);
77
+ return new WeChatChannelHelperClient({
78
+ helperPath: resolved.helperPath,
79
+ expectedHelperVersion: resolved.version || WECHAT_CHANNEL_HELPER_VERSION,
80
+ });
81
+ }
82
+ function defaultLedgerPath(workDir, runtimeId) {
83
+ return path.join(workDir, 'wechat-channel', `${runtimeId}.ledger.json`);
84
+ }
@@ -0,0 +1,45 @@
1
+ export declare const WECHAT_CHANNEL_RUNTIME_KIND: "wechat_channel";
2
+ export declare const WECHAT_CHANNEL_RUNTIME_VERSION = 1;
3
+ export declare const WECHAT_CHANNEL_RECENT_MESSAGE_WINDOW = 20;
4
+ export declare const WECHAT_CHANNEL_DEFAULT_POLL_INTERVAL_MS: number;
5
+ export declare const WECHAT_CHANNEL_MIN_POLL_INTERVAL_MS: number;
6
+ export declare const WECHAT_CHANNEL_MAX_POLL_INTERVAL_MS: number;
7
+ export type WeChatChannelForegroundPolicy = 'polite' | 'work';
8
+ export type WeChatChannelRuntimePolicy = {
9
+ kind: typeof WECHAT_CHANNEL_RUNTIME_KIND;
10
+ runtimeVersion: number;
11
+ platform: 'darwin';
12
+ pollIntervalMs: number;
13
+ minPollIntervalMs: number;
14
+ maxPollIntervalMs: number;
15
+ recentMessageWindow: number;
16
+ requiresServerDecision: true;
17
+ requiresMacHelper: true;
18
+ productRuntimeUsesLabFacade: false;
19
+ };
20
+ export type WeChatChannelBindingConfig = {
21
+ bindingId: string;
22
+ sessionId: string;
23
+ conversationDisplayName: string;
24
+ enabled: boolean;
25
+ allowReply: boolean;
26
+ downloadMedia: boolean;
27
+ };
28
+ export type WeChatChannelRuntimeConfig = {
29
+ runtimeId: string;
30
+ machineId: string;
31
+ pollIntervalMs?: number | null;
32
+ foregroundPolicy?: WeChatChannelForegroundPolicy | null;
33
+ bindings?: WeChatChannelBindingConfig[] | null;
34
+ };
35
+ export type WeChatChannelRuntime = {
36
+ kind: typeof WECHAT_CHANNEL_RUNTIME_KIND;
37
+ runtimeId: string;
38
+ machineId: string;
39
+ foregroundPolicy: WeChatChannelForegroundPolicy;
40
+ policy: WeChatChannelRuntimePolicy;
41
+ bindings: WeChatChannelBindingConfig[];
42
+ };
43
+ export declare function defaultWeChatChannelRuntimePolicy(pollIntervalMs?: number | null): WeChatChannelRuntimePolicy;
44
+ export declare function normalizeWeChatChannelPollIntervalMs(value?: number | null): number;
45
+ export declare function createWeChatChannelRuntime(config: WeChatChannelRuntimeConfig): WeChatChannelRuntime;