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.
- package/dist/assets/wechat-channel/macos/manifest.json +13 -0
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/src/agents/adapter.d.ts +6 -0
- package/dist/src/agents/codex-control.d.ts +35 -0
- package/dist/src/agents/codex-control.js +188 -0
- package/dist/src/agents/codex-utils.d.ts +5 -0
- package/dist/src/agents/codex-utils.js +5 -0
- package/dist/src/agents/codex.d.ts +8 -0
- package/dist/src/agents/codex.js +55 -2
- package/dist/src/agents/model-registry/discovery.js +2 -1
- package/dist/src/channels/base.d.ts +4 -13
- package/dist/src/channels/runtime.d.ts +1 -3
- package/dist/src/channels/runtime.js +32 -5
- package/dist/src/channels/secret-registry.d.ts +1 -4
- package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
- package/dist/src/channels/wechat-channel/anchor.js +65 -0
- package/dist/src/channels/wechat-channel/client.d.ts +74 -0
- package/dist/src/channels/wechat-channel/client.js +96 -0
- package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
- package/dist/src/channels/wechat-channel/cooldown.js +38 -0
- package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
- package/dist/src/channels/wechat-channel/fingerprint.js +71 -0
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +28 -0
- package/dist/src/channels/wechat-channel/helper-assets.js +68 -0
- package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
- package/dist/src/channels/wechat-channel/helper-client.js +149 -0
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
- package/dist/src/channels/wechat-channel/helper-protocol.js +115 -0
- package/dist/src/channels/wechat-channel/index.d.ts +16 -0
- package/dist/src/channels/wechat-channel/index.js +19 -0
- package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
- package/dist/src/channels/wechat-channel/ledger.js +54 -0
- package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
- package/dist/src/channels/wechat-channel/media-resolver.js +181 -0
- package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
- package/dist/src/channels/wechat-channel/message-key.js +105 -0
- package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
- package/dist/src/channels/wechat-channel/observer.js +118 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +66 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +112 -0
- package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
- package/dist/src/channels/wechat-channel/preflight.js +48 -0
- package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
- package/dist/src/channels/wechat-channel/runner.js +84 -0
- package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
- package/dist/src/channels/wechat-channel/runtime.js +66 -0
- package/dist/src/channels/wechat-channel/scheduler.d.ts +30 -0
- package/dist/src/channels/wechat-channel/scheduler.js +152 -0
- package/dist/src/channels/wechat-rpa/macos-flow.d.ts +0 -28
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -134
- package/dist/src/channels/wechat-rpa.d.ts +21 -0
- package/dist/src/channels/wechat-rpa.js +39 -61
- package/dist/src/commands/manager.d.ts +1 -1
- package/dist/src/commands/manager.js +5 -10
- package/dist/src/fs/text-decoder.d.ts +10 -0
- package/dist/src/fs/text-decoder.js +110 -0
- package/dist/src/manager/runtime.js +4 -6
- package/dist/src/native-fusion/service.d.ts +10 -0
- package/dist/src/native-fusion/service.js +27 -0
- package/dist/src/session/handlers/chat.js +18 -2
- package/dist/src/session/handlers/fs.js +39 -3
- package/dist/src/session/handlers/session-refresh.js +12 -0
- package/dist/src/session/handlers/tool-detail.d.ts +3 -0
- package/dist/src/session/handlers/tool-detail.js +218 -0
- package/dist/src/session/manager.d.ts +3 -0
- package/dist/src/session/manager.js +58 -0
- package/dist/src/session/types.d.ts +4 -0
- package/package.json +2 -2
- package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
- package/dist/scripts/wechat-rpa-win-visual.mjs +0 -1735
- package/dist/scripts/wechat-rpa-win.mjs +0 -352
- package/dist/src/channels/wechat-rpa/windows-visual-flow.d.ts +0 -40
- package/dist/src/channels/wechat-rpa/windows-visual-flow.js +0 -189
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-productization-plan.md
|
|
2
|
+
// @test src/__tests__/wechat-channel-client.test.ts
|
|
3
|
+
import { SERVERS } from '../../region.js';
|
|
4
|
+
import { loadConfig } from '../../config/index.js';
|
|
5
|
+
export function createWeChatChannelApiClient(options = {}) {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const serverUrl = normalizeServerUrl(options.serverUrl || config.serverUrl || SERVERS.cn.url);
|
|
8
|
+
const machineToken = options.machineToken || config.machineToken;
|
|
9
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
10
|
+
if (!machineToken)
|
|
11
|
+
throw new Error('WeChat channel requires a paired machine token');
|
|
12
|
+
async function request(path, body) {
|
|
13
|
+
const response = await fetchImpl(`${serverUrl}/api/channels/wechat${path}`, {
|
|
14
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
15
|
+
headers: {
|
|
16
|
+
authorization: `Bearer ${machineToken}`,
|
|
17
|
+
...(body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
18
|
+
},
|
|
19
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
20
|
+
});
|
|
21
|
+
const text = await response.text();
|
|
22
|
+
const payload = text ? JSON.parse(text) : null;
|
|
23
|
+
if (!response.ok || payload?.ok === false) {
|
|
24
|
+
const reason = payload?.reasonCode || response.statusText || 'request_failed';
|
|
25
|
+
throw new Error(`WeChat channel API ${path} failed: ${reason}`);
|
|
26
|
+
}
|
|
27
|
+
return payload;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
getRuntimePolicy: () => request('/runtime-policy'),
|
|
31
|
+
upsertRuntime: (runtime, binding) => request('/runtime', {
|
|
32
|
+
runtimeId: runtime.runtimeId,
|
|
33
|
+
machineId: runtime.machineId,
|
|
34
|
+
pollIntervalSeconds: Math.round(runtime.policy.pollIntervalMs / 1000),
|
|
35
|
+
foregroundPolicy: runtime.foregroundPolicy,
|
|
36
|
+
clientRuntimeVersion: String(runtime.policy.runtimeVersion),
|
|
37
|
+
...(binding ? bindingPayload(binding) : {}),
|
|
38
|
+
}),
|
|
39
|
+
observe: (runtime, binding, input) => request('/observe', {
|
|
40
|
+
runtimeId: runtime.runtimeId,
|
|
41
|
+
bindingId: binding.bindingId,
|
|
42
|
+
sessionId: binding.sessionId,
|
|
43
|
+
machineId: runtime.machineId,
|
|
44
|
+
conversationName: binding.conversationDisplayName,
|
|
45
|
+
schemaVersion: runtime.policy.runtimeVersion,
|
|
46
|
+
screenshots: input.screenshots,
|
|
47
|
+
edgeOcrBlocks: input.edgeOcrBlocks ?? [],
|
|
48
|
+
visibleConversationFingerprints: input.visibleConversationFingerprints ?? [],
|
|
49
|
+
localLedgerTailAnchors: input.localLedgerTailAnchors ?? [],
|
|
50
|
+
}),
|
|
51
|
+
ingest: (runtime, binding, input) => request('/ingest', {
|
|
52
|
+
runtimeId: runtime.runtimeId,
|
|
53
|
+
idempotencyKey: input.idempotencyKey,
|
|
54
|
+
bindingId: binding.bindingId,
|
|
55
|
+
sessionId: binding.sessionId,
|
|
56
|
+
machineId: runtime.machineId,
|
|
57
|
+
messages: input.messages,
|
|
58
|
+
}),
|
|
59
|
+
reportOutboundStatus: (runtime, binding, input) => request('/outbound-status', {
|
|
60
|
+
runtimeId: runtime.runtimeId,
|
|
61
|
+
replyId: input.replyId,
|
|
62
|
+
idempotencyKey: input.idempotencyKey,
|
|
63
|
+
bindingId: binding.bindingId,
|
|
64
|
+
sessionId: binding.sessionId,
|
|
65
|
+
machineId: runtime.machineId,
|
|
66
|
+
status: input.status,
|
|
67
|
+
replyBaseRevision: input.replyBaseRevision,
|
|
68
|
+
sentAt: input.sentAt,
|
|
69
|
+
confirmedAt: input.confirmedAt,
|
|
70
|
+
failureCode: input.failureCode,
|
|
71
|
+
lastErrorSummary: input.lastErrorSummary,
|
|
72
|
+
}),
|
|
73
|
+
reportRunStatus: (runtime, binding, input) => request('/run-status', {
|
|
74
|
+
runtimeId: runtime.runtimeId,
|
|
75
|
+
bindingId: binding.bindingId,
|
|
76
|
+
sessionId: binding.sessionId,
|
|
77
|
+
machineId: runtime.machineId,
|
|
78
|
+
status: input.status,
|
|
79
|
+
reasonCode: input.reasonCode,
|
|
80
|
+
traceId: input.traceId,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function bindingPayload(binding) {
|
|
85
|
+
return {
|
|
86
|
+
bindingId: binding.bindingId,
|
|
87
|
+
sessionId: binding.sessionId,
|
|
88
|
+
conversationName: binding.conversationDisplayName,
|
|
89
|
+
enabled: binding.enabled,
|
|
90
|
+
allowReply: binding.allowReply,
|
|
91
|
+
downloadMedia: binding.downloadMedia,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function normalizeServerUrl(url) {
|
|
95
|
+
return url.replace(/\/+$/, '');
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type WeChatChannelCooldownState = {
|
|
2
|
+
consecutiveInterruptions: number;
|
|
3
|
+
cooldownUntil?: string | null;
|
|
4
|
+
manualReviewReason?: string | null;
|
|
5
|
+
};
|
|
6
|
+
export declare const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_THRESHOLD = 3;
|
|
7
|
+
export declare const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_MS: number;
|
|
8
|
+
export declare function isWeChatChannelCooldownActive(state: WeChatChannelCooldownState | undefined, now?: Date): boolean;
|
|
9
|
+
export declare function noteWeChatChannelInterruption(input: {
|
|
10
|
+
state?: WeChatChannelCooldownState;
|
|
11
|
+
reason?: string;
|
|
12
|
+
now?: Date;
|
|
13
|
+
}): WeChatChannelCooldownState;
|
|
14
|
+
export declare function noteWeChatChannelStableRun(state?: WeChatChannelCooldownState): WeChatChannelCooldownState;
|
|
15
|
+
export declare function clearExpiredWeChatChannelCooldown(state: WeChatChannelCooldownState, now?: Date): WeChatChannelCooldownState;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-productization-plan.md
|
|
2
|
+
// @arch docs/features/wechat-rpa-outbound-ledger.md
|
|
3
|
+
// @test src/__tests__/wechat-channel-cooldown.test.ts
|
|
4
|
+
export const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_THRESHOLD = 3;
|
|
5
|
+
export const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_MS = 5 * 60 * 1000;
|
|
6
|
+
export function isWeChatChannelCooldownActive(state, now = new Date()) {
|
|
7
|
+
if (!state?.cooldownUntil)
|
|
8
|
+
return false;
|
|
9
|
+
const until = new Date(state.cooldownUntil).getTime();
|
|
10
|
+
return Number.isFinite(until) && until > now.getTime();
|
|
11
|
+
}
|
|
12
|
+
export function noteWeChatChannelInterruption(input) {
|
|
13
|
+
const now = input.now ?? new Date();
|
|
14
|
+
const previous = input.state ?? { consecutiveInterruptions: 0 };
|
|
15
|
+
const consecutiveInterruptions = previous.consecutiveInterruptions + 1;
|
|
16
|
+
const next = {
|
|
17
|
+
consecutiveInterruptions,
|
|
18
|
+
cooldownUntil: previous.cooldownUntil ?? null,
|
|
19
|
+
manualReviewReason: previous.manualReviewReason ?? null,
|
|
20
|
+
};
|
|
21
|
+
if (consecutiveInterruptions >= WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_THRESHOLD) {
|
|
22
|
+
next.cooldownUntil = new Date(now.getTime() + WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_MS).toISOString();
|
|
23
|
+
next.manualReviewReason = input.reason || 'user_interruption_cooldown';
|
|
24
|
+
}
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
export function noteWeChatChannelStableRun(state) {
|
|
28
|
+
return {
|
|
29
|
+
consecutiveInterruptions: 0,
|
|
30
|
+
cooldownUntil: state?.cooldownUntil ?? null,
|
|
31
|
+
manualReviewReason: state?.manualReviewReason ?? null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function clearExpiredWeChatChannelCooldown(state, now = new Date()) {
|
|
35
|
+
if (isWeChatChannelCooldownActive(state, now))
|
|
36
|
+
return state;
|
|
37
|
+
return { ...state, cooldownUntil: null };
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type WeChatConversationListFingerprint = {
|
|
2
|
+
conversationId?: string | null;
|
|
3
|
+
title?: string | null;
|
|
4
|
+
preview?: string | null;
|
|
5
|
+
timeText?: string | null;
|
|
6
|
+
unreadText?: string | null;
|
|
7
|
+
fingerprint?: string | null;
|
|
8
|
+
bbox?: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type WeChatConversationFingerprintTarget = {
|
|
11
|
+
bindingId: string;
|
|
12
|
+
conversationDisplayName: string;
|
|
13
|
+
lastConversationFingerprint?: string | null;
|
|
14
|
+
lastPreview?: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type WeChatConversationFingerprintMatch = {
|
|
17
|
+
bindingId: string;
|
|
18
|
+
item: WeChatConversationListFingerprint;
|
|
19
|
+
score: number;
|
|
20
|
+
reason: 'fingerprint' | 'title-preview' | 'title' | 'none';
|
|
21
|
+
};
|
|
22
|
+
export declare function buildWeChatConversationFingerprint(input: WeChatConversationListFingerprint): string;
|
|
23
|
+
export declare function matchWeChatConversationFingerprints(input: {
|
|
24
|
+
visibleItems: WeChatConversationListFingerprint[];
|
|
25
|
+
targets: WeChatConversationFingerprintTarget[];
|
|
26
|
+
minScore?: number;
|
|
27
|
+
}): WeChatConversationFingerprintMatch[];
|
|
28
|
+
export declare function scoreConversationFingerprint(target: WeChatConversationFingerprintTarget, item: WeChatConversationListFingerprint): WeChatConversationFingerprintMatch;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-productization-plan.md
|
|
2
|
+
// @test src/__tests__/wechat-channel-fingerprint.test.ts
|
|
3
|
+
export function buildWeChatConversationFingerprint(input) {
|
|
4
|
+
return [input.title, input.preview, input.timeText, input.unreadText]
|
|
5
|
+
.map((part) => normalizeFingerprintPart(part))
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.join('|');
|
|
8
|
+
}
|
|
9
|
+
export function matchWeChatConversationFingerprints(input) {
|
|
10
|
+
const minScore = input.minScore ?? 0.72;
|
|
11
|
+
const unusedItems = input.visibleItems.map((item) => ({
|
|
12
|
+
...item,
|
|
13
|
+
fingerprint: item.fingerprint || buildWeChatConversationFingerprint(item),
|
|
14
|
+
}));
|
|
15
|
+
const matches = [];
|
|
16
|
+
for (const target of input.targets) {
|
|
17
|
+
let best = null;
|
|
18
|
+
for (const item of unusedItems) {
|
|
19
|
+
const candidate = scoreConversationFingerprint(target, item);
|
|
20
|
+
if (!best || candidate.score > best.score)
|
|
21
|
+
best = candidate;
|
|
22
|
+
}
|
|
23
|
+
if (best && best.score >= minScore && best.reason !== 'none') {
|
|
24
|
+
matches.push(best);
|
|
25
|
+
const index = unusedItems.findIndex((item) => item === best.item);
|
|
26
|
+
if (index >= 0)
|
|
27
|
+
unusedItems.splice(index, 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return matches;
|
|
31
|
+
}
|
|
32
|
+
export function scoreConversationFingerprint(target, item) {
|
|
33
|
+
const itemFingerprint = item.fingerprint || buildWeChatConversationFingerprint(item);
|
|
34
|
+
if (target.lastConversationFingerprint && itemFingerprint && target.lastConversationFingerprint === itemFingerprint) {
|
|
35
|
+
return { bindingId: target.bindingId, item, score: 1, reason: 'fingerprint' };
|
|
36
|
+
}
|
|
37
|
+
const titleScore = similarity(target.conversationDisplayName, item.title);
|
|
38
|
+
const previewScore = target.lastPreview ? similarity(target.lastPreview, item.preview) : 0;
|
|
39
|
+
if (titleScore >= 0.8 && previewScore >= 0.55) {
|
|
40
|
+
return { bindingId: target.bindingId, item, score: titleScore * 0.7 + previewScore * 0.3, reason: 'title-preview' };
|
|
41
|
+
}
|
|
42
|
+
if (titleScore >= 0.88)
|
|
43
|
+
return { bindingId: target.bindingId, item, score: titleScore * 0.86, reason: 'title' };
|
|
44
|
+
return { bindingId: target.bindingId, item, score: titleScore * 0.5 + previewScore * 0.2, reason: 'none' };
|
|
45
|
+
}
|
|
46
|
+
function normalizeFingerprintPart(value) {
|
|
47
|
+
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ').toLowerCase() : '';
|
|
48
|
+
}
|
|
49
|
+
function similarity(left, right) {
|
|
50
|
+
const a = normalizeFingerprintPart(left);
|
|
51
|
+
const b = normalizeFingerprintPart(right);
|
|
52
|
+
if (!a || !b)
|
|
53
|
+
return 0;
|
|
54
|
+
if (a === b)
|
|
55
|
+
return 1;
|
|
56
|
+
const distance = levenshtein(a, b);
|
|
57
|
+
return 1 - distance / Math.max(a.length, b.length);
|
|
58
|
+
}
|
|
59
|
+
function levenshtein(a, b) {
|
|
60
|
+
const prev = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
61
|
+
const curr = Array.from({ length: b.length + 1 }, () => 0);
|
|
62
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
63
|
+
curr[0] = i;
|
|
64
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
65
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
66
|
+
}
|
|
67
|
+
for (let j = 0; j <= b.length; j += 1)
|
|
68
|
+
prev[j] = curr[j];
|
|
69
|
+
}
|
|
70
|
+
return prev[b.length];
|
|
71
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const WECHAT_CHANNEL_HELPER_VERSION = "0.1.0";
|
|
2
|
+
export type WeChatChannelHelperAssetManifest = {
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
helperVersion: string;
|
|
5
|
+
protocolVersion: number;
|
|
6
|
+
platforms: Record<string, {
|
|
7
|
+
executable: string;
|
|
8
|
+
sha256: string | null;
|
|
9
|
+
signed: boolean;
|
|
10
|
+
notarized: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
export type WeChatChannelHelperAssetResolution = {
|
|
14
|
+
ok: true;
|
|
15
|
+
helperPath: string;
|
|
16
|
+
version: string;
|
|
17
|
+
manifest: WeChatChannelHelperAssetManifest;
|
|
18
|
+
warning?: string;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reasonCode: 'unsupported_platform' | 'manifest_missing' | 'helper_missing' | 'integrity_mismatch';
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function resolveWeChatChannelHelperAsset(input?: {
|
|
25
|
+
platform?: NodeJS.Platform | string;
|
|
26
|
+
baseDir?: string;
|
|
27
|
+
verifyIntegrity?: boolean;
|
|
28
|
+
}): WeChatChannelHelperAssetResolution;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-helper-runtime.md
|
|
2
|
+
// @test src/__tests__/wechat-channel-helper-assets.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
export const WECHAT_CHANNEL_HELPER_VERSION = '0.1.0';
|
|
8
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export function resolveWeChatChannelHelperAsset(input = {}) {
|
|
10
|
+
const platform = input.platform ?? process.platform;
|
|
11
|
+
if (platform !== 'darwin') {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
reasonCode: 'unsupported_platform',
|
|
15
|
+
message: 'WeChat channel helper is only available on macOS in the first version',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const baseDir = input.baseDir ?? defaultHelperAssetBaseDir();
|
|
19
|
+
const manifestPath = path.join(baseDir, 'manifest.json');
|
|
20
|
+
const manifest = readManifest(manifestPath);
|
|
21
|
+
if (!manifest) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
reasonCode: 'manifest_missing',
|
|
25
|
+
message: `WeChat channel helper manifest is missing: ${manifestPath}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const asset = manifest.platforms.darwin;
|
|
29
|
+
const helperPath = asset ? path.join(baseDir, asset.executable) : '';
|
|
30
|
+
if (!asset || !helperPath || !fs.existsSync(helperPath)) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
reasonCode: 'helper_missing',
|
|
34
|
+
message: `WeChat channel helper executable is missing: ${helperPath || baseDir}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (input.verifyIntegrity !== false && asset.sha256) {
|
|
38
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(helperPath)).digest('hex');
|
|
39
|
+
if (actual !== asset.sha256) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
reasonCode: 'integrity_mismatch',
|
|
43
|
+
message: `WeChat channel helper integrity mismatch: ${helperPath}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
helperPath,
|
|
50
|
+
version: manifest.helperVersion,
|
|
51
|
+
manifest,
|
|
52
|
+
warning: asset.signed && asset.notarized ? undefined : 'helper_not_signed_or_notarized',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function defaultHelperAssetBaseDir() {
|
|
56
|
+
return path.resolve(moduleDir, '../../../assets/wechat-channel/macos');
|
|
57
|
+
}
|
|
58
|
+
function readManifest(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
61
|
+
if (parsed.schemaVersion !== 1 || typeof parsed.helperVersion !== 'string' || !parsed.platforms?.darwin)
|
|
62
|
+
return null;
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type WeChatChannelHelperCommandName, type WeChatChannelHelperHealthResult, type WeChatChannelHelperReady, type WeChatChannelHelperResponse, type WeChatChannelHelperWarmupSnapshot } from './helper-protocol.js';
|
|
2
|
+
export type WeChatChannelHelperClientOptions = {
|
|
3
|
+
helperPath: string;
|
|
4
|
+
expectedHelperVersion: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
cwd?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class WeChatChannelHelperClient {
|
|
9
|
+
private options;
|
|
10
|
+
private child;
|
|
11
|
+
private lines;
|
|
12
|
+
private readyState;
|
|
13
|
+
private warmupState;
|
|
14
|
+
private pending;
|
|
15
|
+
constructor(options: WeChatChannelHelperClientOptions);
|
|
16
|
+
start(): Promise<WeChatChannelHelperReady>;
|
|
17
|
+
request<T = unknown>(command: WeChatChannelHelperCommandName, params?: Record<string, unknown>, traceId?: string): Promise<WeChatChannelHelperResponse<T>>;
|
|
18
|
+
healthCheck(traceId?: string): Promise<WeChatChannelHelperResponse<WeChatChannelHelperHealthResult>>;
|
|
19
|
+
getReadyState(): WeChatChannelHelperReady | null;
|
|
20
|
+
getWarmupState(): WeChatChannelHelperWarmupSnapshot | null;
|
|
21
|
+
stop(): Promise<void>;
|
|
22
|
+
private handleLine;
|
|
23
|
+
private captureWarmupSnapshot;
|
|
24
|
+
private rejectAllPending;
|
|
25
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-helper-runtime.md
|
|
2
|
+
// @test src/__tests__/wechat-channel-helper-client.test.ts
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { createWeChatChannelHelperHello, extractWeChatChannelHelperWarmupSnapshot, timeoutForWeChatChannelHelperCommand, validateWeChatChannelHelperReady, } from './helper-protocol.js';
|
|
7
|
+
export class WeChatChannelHelperClient {
|
|
8
|
+
options;
|
|
9
|
+
child = null;
|
|
10
|
+
lines = null;
|
|
11
|
+
readyState = null;
|
|
12
|
+
warmupState = null;
|
|
13
|
+
pending = new Map();
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
async start() {
|
|
18
|
+
if (this.child)
|
|
19
|
+
throw new Error('WeChat channel helper is already started');
|
|
20
|
+
const child = spawn(this.options.helperPath, this.options.args ?? [], {
|
|
21
|
+
cwd: this.options.cwd,
|
|
22
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
23
|
+
});
|
|
24
|
+
this.child = child;
|
|
25
|
+
this.lines = createInterface({ input: child.stdout });
|
|
26
|
+
this.lines.on('line', (line) => this.handleLine(line));
|
|
27
|
+
child.once('exit', () => this.rejectAllPending(new Error('WeChat channel helper process exited')));
|
|
28
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
29
|
+
const timer = setTimeout(() => reject(new Error('WeChat channel helper handshake timed out')), 2_000);
|
|
30
|
+
const onLine = (line) => {
|
|
31
|
+
let frame;
|
|
32
|
+
try {
|
|
33
|
+
frame = JSON.parse(line);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
this.lines?.off('line', onLine);
|
|
38
|
+
reject(new Error('WeChat channel helper sent invalid handshake JSON'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!isReadyFrame(frame))
|
|
42
|
+
return;
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
this.lines?.off('line', onLine);
|
|
45
|
+
const validation = validateWeChatChannelHelperReady(frame, this.options.expectedHelperVersion);
|
|
46
|
+
if (!validation.ok)
|
|
47
|
+
reject(new Error(`${validation.errorCode}: ${validation.errorSummary}`));
|
|
48
|
+
else {
|
|
49
|
+
this.readyState = frame;
|
|
50
|
+
this.captureWarmupSnapshot(frame);
|
|
51
|
+
resolve(frame);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
this.lines?.on('line', onLine);
|
|
55
|
+
});
|
|
56
|
+
child.stdin.write(`${JSON.stringify(createWeChatChannelHelperHello(this.options.expectedHelperVersion))}\n`);
|
|
57
|
+
return readyPromise;
|
|
58
|
+
}
|
|
59
|
+
async request(command, params, traceId) {
|
|
60
|
+
if (!this.child)
|
|
61
|
+
throw new Error('WeChat channel helper is not started');
|
|
62
|
+
const id = randomUUID();
|
|
63
|
+
const timeoutMs = timeoutForWeChatChannelHelperCommand(command);
|
|
64
|
+
const promise = new Promise((resolve, reject) => {
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
this.pending.delete(id);
|
|
67
|
+
reject(new Error(`helper_command_timeout: ${command}`));
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
70
|
+
});
|
|
71
|
+
this.child.stdin.write(`${JSON.stringify({ id, command, params, traceId })}\n`);
|
|
72
|
+
const response = await promise;
|
|
73
|
+
this.captureWarmupSnapshot(response);
|
|
74
|
+
this.captureWarmupSnapshot(response.result);
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
async healthCheck(traceId) {
|
|
78
|
+
return this.request('health.check', {}, traceId);
|
|
79
|
+
}
|
|
80
|
+
getReadyState() {
|
|
81
|
+
return this.readyState ? { ...this.readyState, capabilities: [...this.readyState.capabilities] } : null;
|
|
82
|
+
}
|
|
83
|
+
getWarmupState() {
|
|
84
|
+
if (!this.warmupState)
|
|
85
|
+
return null;
|
|
86
|
+
return {
|
|
87
|
+
warmState: this.warmupState.warmState,
|
|
88
|
+
metrics: this.warmupState.metrics ? { ...this.warmupState.metrics } : undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async stop() {
|
|
92
|
+
const child = this.child;
|
|
93
|
+
this.child = null;
|
|
94
|
+
this.readyState = null;
|
|
95
|
+
this.lines?.close();
|
|
96
|
+
this.lines = null;
|
|
97
|
+
this.rejectAllPending(new Error('WeChat channel helper stopped'));
|
|
98
|
+
if (!child || child.killed)
|
|
99
|
+
return;
|
|
100
|
+
child.kill('SIGTERM');
|
|
101
|
+
}
|
|
102
|
+
handleLine(line) {
|
|
103
|
+
let frame;
|
|
104
|
+
try {
|
|
105
|
+
frame = JSON.parse(line);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!isResponseFrame(frame))
|
|
111
|
+
return;
|
|
112
|
+
const pending = this.pending.get(frame.id);
|
|
113
|
+
if (!pending)
|
|
114
|
+
return;
|
|
115
|
+
clearTimeout(pending.timer);
|
|
116
|
+
this.pending.delete(frame.id);
|
|
117
|
+
pending.resolve(frame);
|
|
118
|
+
}
|
|
119
|
+
captureWarmupSnapshot(value) {
|
|
120
|
+
const snapshot = extractWeChatChannelHelperWarmupSnapshot(value);
|
|
121
|
+
if (snapshot)
|
|
122
|
+
this.warmupState = snapshot;
|
|
123
|
+
}
|
|
124
|
+
rejectAllPending(error) {
|
|
125
|
+
for (const [id, pending] of this.pending) {
|
|
126
|
+
clearTimeout(pending.timer);
|
|
127
|
+
pending.reject(error);
|
|
128
|
+
this.pending.delete(id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function isReadyFrame(value) {
|
|
133
|
+
if (!value || typeof value !== 'object')
|
|
134
|
+
return false;
|
|
135
|
+
const record = value;
|
|
136
|
+
return record.type === 'ready'
|
|
137
|
+
&& typeof record.helperVersion === 'string'
|
|
138
|
+
&& typeof record.protocolVersion === 'number'
|
|
139
|
+
&& Array.isArray(record.capabilities)
|
|
140
|
+
&& typeof record.pid === 'number';
|
|
141
|
+
}
|
|
142
|
+
function isResponseFrame(value) {
|
|
143
|
+
if (!value || typeof value !== 'object')
|
|
144
|
+
return false;
|
|
145
|
+
const record = value;
|
|
146
|
+
return typeof record.id === 'string'
|
|
147
|
+
&& typeof record.ok === 'boolean'
|
|
148
|
+
&& typeof record.latencyMs === 'number';
|
|
149
|
+
}
|