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,66 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-productization-plan.md
|
|
2
|
+
// @arch docs/features/wechat-rpa-helper-runtime.md
|
|
3
|
+
// @test src/__tests__/wechat-channel-runtime.test.ts
|
|
4
|
+
export const WECHAT_CHANNEL_RUNTIME_KIND = 'wechat_channel';
|
|
5
|
+
export const WECHAT_CHANNEL_RUNTIME_VERSION = 1;
|
|
6
|
+
export const WECHAT_CHANNEL_RECENT_MESSAGE_WINDOW = 20;
|
|
7
|
+
export const WECHAT_CHANNEL_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000;
|
|
8
|
+
export const WECHAT_CHANNEL_MIN_POLL_INTERVAL_MS = 60 * 1000;
|
|
9
|
+
export const WECHAT_CHANNEL_MAX_POLL_INTERVAL_MS = 10 * 60 * 1000;
|
|
10
|
+
export function defaultWeChatChannelRuntimePolicy(pollIntervalMs) {
|
|
11
|
+
return {
|
|
12
|
+
kind: WECHAT_CHANNEL_RUNTIME_KIND,
|
|
13
|
+
runtimeVersion: WECHAT_CHANNEL_RUNTIME_VERSION,
|
|
14
|
+
platform: 'darwin',
|
|
15
|
+
pollIntervalMs: normalizeWeChatChannelPollIntervalMs(pollIntervalMs),
|
|
16
|
+
minPollIntervalMs: WECHAT_CHANNEL_MIN_POLL_INTERVAL_MS,
|
|
17
|
+
maxPollIntervalMs: WECHAT_CHANNEL_MAX_POLL_INTERVAL_MS,
|
|
18
|
+
recentMessageWindow: WECHAT_CHANNEL_RECENT_MESSAGE_WINDOW,
|
|
19
|
+
requiresServerDecision: true,
|
|
20
|
+
requiresMacHelper: true,
|
|
21
|
+
productRuntimeUsesLabFacade: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function normalizeWeChatChannelPollIntervalMs(value) {
|
|
25
|
+
if (!Number.isFinite(value))
|
|
26
|
+
return WECHAT_CHANNEL_DEFAULT_POLL_INTERVAL_MS;
|
|
27
|
+
return Math.min(WECHAT_CHANNEL_MAX_POLL_INTERVAL_MS, Math.max(WECHAT_CHANNEL_MIN_POLL_INTERVAL_MS, Number(value)));
|
|
28
|
+
}
|
|
29
|
+
export function createWeChatChannelRuntime(config) {
|
|
30
|
+
const runtimeId = normalizeRequiredString(config.runtimeId, 'runtimeId');
|
|
31
|
+
const machineId = normalizeRequiredString(config.machineId, 'machineId');
|
|
32
|
+
const pollIntervalMs = normalizeWeChatChannelPollIntervalMs(config.pollIntervalMs);
|
|
33
|
+
return {
|
|
34
|
+
kind: WECHAT_CHANNEL_RUNTIME_KIND,
|
|
35
|
+
runtimeId,
|
|
36
|
+
machineId,
|
|
37
|
+
foregroundPolicy: config.foregroundPolicy === 'work' ? 'work' : 'polite',
|
|
38
|
+
policy: defaultWeChatChannelRuntimePolicy(pollIntervalMs),
|
|
39
|
+
bindings: normalizeBindings(config.bindings ?? []),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function normalizeBindings(bindings) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const result = [];
|
|
45
|
+
for (const binding of bindings) {
|
|
46
|
+
const bindingId = normalizeRequiredString(binding.bindingId, 'bindingId');
|
|
47
|
+
if (seen.has(bindingId))
|
|
48
|
+
continue;
|
|
49
|
+
seen.add(bindingId);
|
|
50
|
+
result.push({
|
|
51
|
+
bindingId,
|
|
52
|
+
sessionId: normalizeRequiredString(binding.sessionId, 'sessionId'),
|
|
53
|
+
conversationDisplayName: normalizeRequiredString(binding.conversationDisplayName, 'conversationDisplayName'),
|
|
54
|
+
enabled: binding.enabled !== false,
|
|
55
|
+
allowReply: binding.allowReply !== false,
|
|
56
|
+
downloadMedia: binding.downloadMedia !== false,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function normalizeRequiredString(value, field) {
|
|
62
|
+
const normalized = String(value || '').trim();
|
|
63
|
+
if (!normalized)
|
|
64
|
+
throw new Error(`WeChat channel ${field} is required`);
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WeChatChannelRuntime, WeChatChannelBindingConfig } from './runtime.js';
|
|
2
|
+
import type { WeChatChannelApiClient, WeChatChannelObservedMessage } from './client.js';
|
|
3
|
+
export type WeChatChannelSchedulerOptions = {
|
|
4
|
+
runtime: WeChatChannelRuntime;
|
|
5
|
+
workDir: string;
|
|
6
|
+
api: Pick<WeChatChannelApiClient, 'upsertRuntime' | 'ingest' | 'reportRunStatus'> & Partial<Pick<WeChatChannelApiClient, 'reportOutboundStatus'>>;
|
|
7
|
+
observeBinding: (binding: WeChatChannelBindingConfig) => Promise<WeChatChannelObservedMessage[]>;
|
|
8
|
+
onInboundMessages?: (binding: WeChatChannelBindingConfig, messages: WeChatChannelObservedMessage[]) => Promise<void> | void;
|
|
9
|
+
ledgerPath?: string;
|
|
10
|
+
outboundLedgerPath?: string;
|
|
11
|
+
};
|
|
12
|
+
export type WeChatChannelSchedulerTickResult = {
|
|
13
|
+
bindingId: string;
|
|
14
|
+
observedCount: number;
|
|
15
|
+
newInboundCount: number;
|
|
16
|
+
confirmedEchoCount: number;
|
|
17
|
+
manualReviewCount: number;
|
|
18
|
+
revision: number;
|
|
19
|
+
}[];
|
|
20
|
+
export declare class WeChatChannelScheduler {
|
|
21
|
+
private options;
|
|
22
|
+
private timer;
|
|
23
|
+
private running;
|
|
24
|
+
constructor(options: WeChatChannelSchedulerOptions);
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
stop(): void;
|
|
27
|
+
tick(): Promise<WeChatChannelSchedulerTickResult | void>;
|
|
28
|
+
noteInterruption(bindingId: string, reason?: string, now?: Date): void;
|
|
29
|
+
private reportOutboundProjections;
|
|
30
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-productization-plan.md
|
|
2
|
+
// @test src/__tests__/wechat-channel-scheduler.test.ts
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadWeChatChannelLedger, markWeChatChannelBindingDisabled, saveWeChatChannelLedger, updateWeChatChannelBindingLedger, } from './ledger.js';
|
|
5
|
+
import { classifyWeChatOutboundEchoes, loadWeChatChannelOutboundLedger, saveWeChatChannelOutboundLedger, } from './outbound-ledger.js';
|
|
6
|
+
import { clearExpiredWeChatChannelCooldown, isWeChatChannelCooldownActive, noteWeChatChannelInterruption, noteWeChatChannelStableRun, } from './cooldown.js';
|
|
7
|
+
export class WeChatChannelScheduler {
|
|
8
|
+
options;
|
|
9
|
+
timer = null;
|
|
10
|
+
running = false;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
async start() {
|
|
15
|
+
if (this.timer)
|
|
16
|
+
return;
|
|
17
|
+
await this.tick();
|
|
18
|
+
this.timer = setInterval(() => {
|
|
19
|
+
void this.tick().catch(() => { });
|
|
20
|
+
}, this.options.runtime.policy.pollIntervalMs);
|
|
21
|
+
this.timer.unref();
|
|
22
|
+
}
|
|
23
|
+
stop() {
|
|
24
|
+
if (!this.timer)
|
|
25
|
+
return;
|
|
26
|
+
clearInterval(this.timer);
|
|
27
|
+
this.timer = null;
|
|
28
|
+
}
|
|
29
|
+
async tick() {
|
|
30
|
+
if (this.running)
|
|
31
|
+
return;
|
|
32
|
+
this.running = true;
|
|
33
|
+
try {
|
|
34
|
+
const ledgerPath = this.options.ledgerPath || defaultLedgerPath(this.options.workDir, this.options.runtime.runtimeId);
|
|
35
|
+
const outboundLedgerPath = this.options.outboundLedgerPath || defaultOutboundLedgerPath(this.options.workDir, this.options.runtime.runtimeId);
|
|
36
|
+
const ledger = loadWeChatChannelLedger(ledgerPath, this.options.runtime.runtimeId);
|
|
37
|
+
const outboundLedger = loadWeChatChannelOutboundLedger(outboundLedgerPath, this.options.runtime.runtimeId);
|
|
38
|
+
const results = [];
|
|
39
|
+
for (const binding of this.options.runtime.bindings.filter((item) => !item.enabled)) {
|
|
40
|
+
if (!ledger.bindings[binding.bindingId]?.disabledSince) {
|
|
41
|
+
markWeChatChannelBindingDisabled({ ledger, bindingId: binding.bindingId });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const binding of this.options.runtime.bindings.filter((item) => item.enabled)) {
|
|
45
|
+
await this.options.api.upsertRuntime(this.options.runtime, binding);
|
|
46
|
+
const existingBinding = ledger.bindings[binding.bindingId];
|
|
47
|
+
if (existingBinding?.cooldown) {
|
|
48
|
+
existingBinding.cooldown = clearExpiredWeChatChannelCooldown(existingBinding.cooldown);
|
|
49
|
+
if (isWeChatChannelCooldownActive(existingBinding.cooldown)) {
|
|
50
|
+
await this.options.api.reportRunStatus(this.options.runtime, binding, {
|
|
51
|
+
status: 'cooldown',
|
|
52
|
+
reasonCode: existingBinding.cooldown.manualReviewReason || 'user_interruption_cooldown',
|
|
53
|
+
});
|
|
54
|
+
results.push({
|
|
55
|
+
bindingId: binding.bindingId,
|
|
56
|
+
observedCount: 0,
|
|
57
|
+
newInboundCount: 0,
|
|
58
|
+
confirmedEchoCount: 0,
|
|
59
|
+
manualReviewCount: 0,
|
|
60
|
+
revision: existingBinding.revision,
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const observedMessages = await this.options.observeBinding(binding);
|
|
66
|
+
const echoes = classifyWeChatOutboundEchoes({
|
|
67
|
+
ledger: outboundLedger,
|
|
68
|
+
bindingId: binding.bindingId,
|
|
69
|
+
messages: observedMessages,
|
|
70
|
+
});
|
|
71
|
+
const state = updateWeChatChannelBindingLedger({
|
|
72
|
+
ledger,
|
|
73
|
+
bindingId: binding.bindingId,
|
|
74
|
+
observedMessages,
|
|
75
|
+
});
|
|
76
|
+
state.binding.cooldown = noteWeChatChannelStableRun(state.binding.cooldown);
|
|
77
|
+
await this.options.api.ingest(this.options.runtime, binding, {
|
|
78
|
+
idempotencyKey: `${binding.bindingId}:${state.binding.revision}:${lastMessageKey(state.binding.recent)}`,
|
|
79
|
+
messages: state.binding.recent,
|
|
80
|
+
});
|
|
81
|
+
if (state.newMessages.length > 0)
|
|
82
|
+
await this.options.onInboundMessages?.(binding, state.newMessages);
|
|
83
|
+
await this.reportOutboundProjections(binding, [...echoes.confirmedRecords, ...echoes.manualReviewRecords]);
|
|
84
|
+
await this.options.api.reportRunStatus(this.options.runtime, binding, {
|
|
85
|
+
status: runStatusForTick(state.newMessages.length, echoes.confirmedRecords, echoes.manualReviewRecords),
|
|
86
|
+
});
|
|
87
|
+
results.push({
|
|
88
|
+
bindingId: binding.bindingId,
|
|
89
|
+
observedCount: observedMessages.length,
|
|
90
|
+
newInboundCount: state.newMessages.length,
|
|
91
|
+
confirmedEchoCount: echoes.confirmedRecords.length,
|
|
92
|
+
manualReviewCount: echoes.manualReviewRecords.length,
|
|
93
|
+
revision: state.binding.revision,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
saveWeChatChannelLedger(ledgerPath, ledger);
|
|
97
|
+
saveWeChatChannelOutboundLedger(outboundLedgerPath, outboundLedger);
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
this.running = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
noteInterruption(bindingId, reason, now = new Date()) {
|
|
105
|
+
const ledgerPath = this.options.ledgerPath || defaultLedgerPath(this.options.workDir, this.options.runtime.runtimeId);
|
|
106
|
+
const ledger = loadWeChatChannelLedger(ledgerPath, this.options.runtime.runtimeId);
|
|
107
|
+
const existing = ledger.bindings[bindingId] ?? {
|
|
108
|
+
bindingId,
|
|
109
|
+
baselineEstablished: false,
|
|
110
|
+
revision: 0,
|
|
111
|
+
recent: [],
|
|
112
|
+
pendingSendKeys: [],
|
|
113
|
+
};
|
|
114
|
+
existing.cooldown = noteWeChatChannelInterruption({ state: existing.cooldown, reason, now });
|
|
115
|
+
ledger.bindings[bindingId] = existing;
|
|
116
|
+
saveWeChatChannelLedger(ledgerPath, ledger);
|
|
117
|
+
}
|
|
118
|
+
async reportOutboundProjections(binding, records) {
|
|
119
|
+
if (!this.options.api.reportOutboundStatus)
|
|
120
|
+
return;
|
|
121
|
+
for (const record of records) {
|
|
122
|
+
await this.options.api.reportOutboundStatus(this.options.runtime, binding, {
|
|
123
|
+
replyId: record.replyId,
|
|
124
|
+
idempotencyKey: record.idempotencyKey,
|
|
125
|
+
status: record.sendStatus,
|
|
126
|
+
replyBaseRevision: record.replyBaseRevision,
|
|
127
|
+
sentAt: record.sentAt,
|
|
128
|
+
confirmedAt: record.confirmedAt,
|
|
129
|
+
failureCode: record.failureCode,
|
|
130
|
+
lastErrorSummary: record.lastErrorSummary,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function defaultLedgerPath(workDir, runtimeId) {
|
|
136
|
+
return path.join(workDir, 'wechat-channel', `${runtimeId}.ledger.json`);
|
|
137
|
+
}
|
|
138
|
+
function defaultOutboundLedgerPath(workDir, runtimeId) {
|
|
139
|
+
return path.join(workDir, 'wechat-channel', `${runtimeId}.outbound-ledger.json`);
|
|
140
|
+
}
|
|
141
|
+
function lastMessageKey(messages) {
|
|
142
|
+
return messages.at(-1)?.stableMessageKey || 'empty';
|
|
143
|
+
}
|
|
144
|
+
function runStatusForTick(newMessageCount, confirmedRecords, manualReviewRecords) {
|
|
145
|
+
if (manualReviewRecords.length > 0)
|
|
146
|
+
return 'manual_review';
|
|
147
|
+
if (newMessageCount > 0)
|
|
148
|
+
return 'new_messages_ingested';
|
|
149
|
+
if (confirmedRecords.length > 0)
|
|
150
|
+
return 'outbound_echo_confirmed';
|
|
151
|
+
return 'baseline_or_no_change';
|
|
152
|
+
}
|
|
@@ -10,10 +10,6 @@ export type MacWeChatRpaFlowOptions = {
|
|
|
10
10
|
recentLimit?: number;
|
|
11
11
|
downloadAttachmentsDir?: string;
|
|
12
12
|
timeoutMs?: number;
|
|
13
|
-
cloudOcrUrl?: string;
|
|
14
|
-
cloudOcrToken?: string;
|
|
15
|
-
cloudOcrMode?: 'off' | 'fallback' | 'always';
|
|
16
|
-
cloudOcrChannelId?: string;
|
|
17
13
|
};
|
|
18
14
|
export type MacWeChatRpaFlowResult = {
|
|
19
15
|
ok: boolean;
|
|
@@ -30,14 +26,8 @@ export type MacWeChatRpaFlowResult = {
|
|
|
30
26
|
sentAttachment?: boolean;
|
|
31
27
|
sentAttachmentObserved?: boolean;
|
|
32
28
|
postSendScreenshotPath?: string;
|
|
33
|
-
cloudOcrPurpose?: MacWeChatRpaCloudOcrPurpose;
|
|
34
|
-
cloudOcrObservations?: MacWeChatRpaCloudOcrObservation[];
|
|
35
|
-
cloudOcrRequestId?: string;
|
|
36
|
-
cloudOcrImageHash?: string;
|
|
37
|
-
cloudOcrUsage?: MacWeChatRpaCloudOcrUsage;
|
|
38
29
|
error?: string;
|
|
39
30
|
};
|
|
40
|
-
export type MacWeChatRpaCloudOcrPurpose = 'message-read' | 'attachment-localization' | 'send-confirmation';
|
|
41
31
|
export type MacWeChatRpaFlowMessage = {
|
|
42
32
|
id?: string;
|
|
43
33
|
text?: string;
|
|
@@ -62,22 +52,4 @@ export type MacWeChatRpaAttachment = {
|
|
|
62
52
|
expiresAt?: string;
|
|
63
53
|
providerError?: string;
|
|
64
54
|
};
|
|
65
|
-
export type MacWeChatRpaCloudOcrObservation = {
|
|
66
|
-
text: string;
|
|
67
|
-
confidence?: number;
|
|
68
|
-
role?: string;
|
|
69
|
-
attachment?: MacWeChatRpaAttachment;
|
|
70
|
-
};
|
|
71
|
-
export type MacWeChatRpaCloudOcrUsage = {
|
|
72
|
-
inputTokens?: number;
|
|
73
|
-
outputTokens?: number;
|
|
74
|
-
totalTokens?: number;
|
|
75
|
-
};
|
|
76
55
|
export declare function runMacWeChatRpaFlow(options: MacWeChatRpaFlowOptions): Promise<MacWeChatRpaFlowResult>;
|
|
77
|
-
export declare function selectCloudOcrRequest(result: Pick<MacWeChatRpaFlowResult, 'newMessages' | 'recentMessages' | 'screenshotPath' | 'postSendScreenshotPath' | 'sentReply' | 'sentAttachment'>, mode: 'off' | 'fallback' | 'always'): {
|
|
78
|
-
screenshotPath: string;
|
|
79
|
-
purpose: MacWeChatRpaCloudOcrPurpose;
|
|
80
|
-
} | null;
|
|
81
|
-
export declare function shouldUseCloudOcr(result: Pick<MacWeChatRpaFlowResult, 'newMessages' | 'recentMessages'>, mode: 'off' | 'fallback' | 'always'): boolean;
|
|
82
|
-
export declare function mergeCloudMessages(localMessages: MacWeChatRpaFlowMessage[], cloudMessages: MacWeChatRpaFlowMessage[]): MacWeChatRpaFlowMessage[];
|
|
83
|
-
export declare function messagesFromCloudOcrObservations(groupName: string, observations: MacWeChatRpaCloudOcrObservation[]): MacWeChatRpaFlowMessage[];
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// @test src/__tests__/wechat-rpa-normalizer.test.ts
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
5
|
import { execFile } from 'node:child_process';
|
|
7
6
|
import { promisify } from 'node:util';
|
|
8
7
|
const execFileAsync = promisify(execFile);
|
|
@@ -61,7 +60,7 @@ export async function runMacWeChatRpaFlow(options) {
|
|
|
61
60
|
return parsed;
|
|
62
61
|
if (!parsed.ok)
|
|
63
62
|
throw new Error(parsed.error || 'WeChat RPA flow failed');
|
|
64
|
-
return
|
|
63
|
+
return parsed;
|
|
65
64
|
}
|
|
66
65
|
catch (error) {
|
|
67
66
|
const detail = stderr.trim() || stdout.slice(0, 1_000);
|
|
@@ -82,138 +81,6 @@ function hasPartialSendProgress(result) {
|
|
|
82
81
|
function hasSendAttempt(result) {
|
|
83
82
|
return Boolean(result.sentReply || result.sentAttachment || result.sentReplyObserved || result.sentAttachmentObserved);
|
|
84
83
|
}
|
|
85
|
-
async function maybeEnrichWithCloudOcr(result, options) {
|
|
86
|
-
void options;
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
export function selectCloudOcrRequest(result, mode) {
|
|
90
|
-
if (!shouldUseCloudOcr(result, mode))
|
|
91
|
-
return null;
|
|
92
|
-
if ((result.sentReply || result.sentAttachment) && result.postSendScreenshotPath) {
|
|
93
|
-
return { screenshotPath: result.postSendScreenshotPath, purpose: 'send-confirmation' };
|
|
94
|
-
}
|
|
95
|
-
if (!result.screenshotPath)
|
|
96
|
-
return null;
|
|
97
|
-
const messages = [...(result.newMessages ?? []), ...(result.recentMessages ?? [])];
|
|
98
|
-
const hasUnresolvedAttachment = messages.some((message) => (message.attachments ?? []).some((attachment) => attachment.availability === 'metadata-only'
|
|
99
|
-
|| attachment.availability === 'pending-download'
|
|
100
|
-
|| Boolean(attachment.providerError)));
|
|
101
|
-
return {
|
|
102
|
-
screenshotPath: result.screenshotPath,
|
|
103
|
-
purpose: hasUnresolvedAttachment ? 'attachment-localization' : 'message-read',
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
export function shouldUseCloudOcr(result, mode) {
|
|
107
|
-
if (mode === 'off')
|
|
108
|
-
return false;
|
|
109
|
-
if (mode === 'always')
|
|
110
|
-
return true;
|
|
111
|
-
const messages = [...(result.newMessages ?? []), ...(result.recentMessages ?? [])];
|
|
112
|
-
if (!(result.newMessages?.length))
|
|
113
|
-
return true;
|
|
114
|
-
if (messages.some((message) => Number.isFinite(message.confidence) && Number(message.confidence) < 0.65))
|
|
115
|
-
return true;
|
|
116
|
-
return messages.some((message) => (message.attachments ?? []).some((attachment) => attachment.availability === 'metadata-only'
|
|
117
|
-
|| attachment.availability === 'pending-download'
|
|
118
|
-
|| Boolean(attachment.providerError)));
|
|
119
|
-
}
|
|
120
|
-
export function mergeCloudMessages(localMessages, cloudMessages) {
|
|
121
|
-
const merged = [...localMessages];
|
|
122
|
-
for (const cloud of cloudMessages) {
|
|
123
|
-
const signature = messageSignature(cloud);
|
|
124
|
-
const index = merged.findIndex((message) => messageSignature(message) === signature);
|
|
125
|
-
if (index < 0) {
|
|
126
|
-
merged.push(cloud);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const existingAttachments = merged[index]?.attachments ?? [];
|
|
130
|
-
const cloudAttachments = cloud.attachments ?? [];
|
|
131
|
-
if (!existingAttachments.length && cloudAttachments.length) {
|
|
132
|
-
const existing = merged[index] ?? {};
|
|
133
|
-
merged[index] = { ...existing, attachments: cloudAttachments };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return merged;
|
|
137
|
-
}
|
|
138
|
-
export function messagesFromCloudOcrObservations(groupName, observations) {
|
|
139
|
-
const messages = [];
|
|
140
|
-
for (const item of observations) {
|
|
141
|
-
const text = String(item.text || '').trim();
|
|
142
|
-
if (!text)
|
|
143
|
-
continue;
|
|
144
|
-
const role = String(item.role || 'unknown');
|
|
145
|
-
if (!['message', 'attachment', 'unknown'].includes(role))
|
|
146
|
-
continue;
|
|
147
|
-
const attachment = normalizeCloudAttachment(item.attachment) ?? (role === 'attachment' ? attachmentFromText(text) : null);
|
|
148
|
-
messages.push({
|
|
149
|
-
id: `cloud:${stableId(`${groupName}\n${role}\n${text}`)}`,
|
|
150
|
-
text,
|
|
151
|
-
confidence: item.confidence,
|
|
152
|
-
attachments: attachment ? [attachment] : [],
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
return messages;
|
|
156
|
-
}
|
|
157
|
-
function messageSignature(message) {
|
|
158
|
-
const text = String(message.text || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
159
|
-
const attachments = (message.attachments ?? [])
|
|
160
|
-
.map((attachment) => `${attachment.type}:${attachment.name || ''}:${attachment.mimeType || ''}`)
|
|
161
|
-
.sort()
|
|
162
|
-
.join('|');
|
|
163
|
-
return text || attachments || String(message.id || '');
|
|
164
|
-
}
|
|
165
|
-
function normalizeCloudAttachment(value) {
|
|
166
|
-
if (!value || typeof value !== 'object')
|
|
167
|
-
return null;
|
|
168
|
-
const record = value;
|
|
169
|
-
const type = String(record.type || 'file').trim() || 'file';
|
|
170
|
-
const name = String(record.name || '').replace(/\s+/g, ' ').trim();
|
|
171
|
-
const mimeType = String(record.mimeType || '').replace(/\s+/g, ' ').trim();
|
|
172
|
-
const size = Number(record.size);
|
|
173
|
-
return {
|
|
174
|
-
type: ['image', 'video', 'audio', 'file'].includes(type) ? type : 'file',
|
|
175
|
-
...(name ? { name } : {}),
|
|
176
|
-
...(mimeType ? { mimeType } : {}),
|
|
177
|
-
...(Number.isFinite(size) && size > 0 ? { size } : {}),
|
|
178
|
-
availability: 'metadata-only',
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
function attachmentFromText(text) {
|
|
182
|
-
const matches = Array.from(text.matchAll(/[\p{L}\p{N}_().[\]-]+\.(png|jpe?g|gif|webp|heic|mp4|mov|avi|mkv|pdf|docx?|xlsx?|pptx?|txt|md|csv|zip|rar)/giu));
|
|
183
|
-
const filename = matches.at(-1)?.[0]?.trim();
|
|
184
|
-
if (!filename)
|
|
185
|
-
return null;
|
|
186
|
-
const ext = path.extname(filename).toLowerCase();
|
|
187
|
-
return { type: attachmentTypeFromExt(ext), name: filename, mimeType: mimeTypeFromExt(ext), availability: 'metadata-only' };
|
|
188
|
-
}
|
|
189
|
-
function attachmentTypeFromExt(ext) {
|
|
190
|
-
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.heic'].includes(ext))
|
|
191
|
-
return 'image';
|
|
192
|
-
if (['.mp4', '.mov', '.avi', '.mkv'].includes(ext))
|
|
193
|
-
return 'video';
|
|
194
|
-
return 'file';
|
|
195
|
-
}
|
|
196
|
-
function mimeTypeFromExt(ext) {
|
|
197
|
-
const map = {
|
|
198
|
-
'.png': 'image/png',
|
|
199
|
-
'.jpg': 'image/jpeg',
|
|
200
|
-
'.jpeg': 'image/jpeg',
|
|
201
|
-
'.gif': 'image/gif',
|
|
202
|
-
'.webp': 'image/webp',
|
|
203
|
-
'.heic': 'image/heic',
|
|
204
|
-
'.mp4': 'video/mp4',
|
|
205
|
-
'.mov': 'video/quicktime',
|
|
206
|
-
'.pdf': 'application/pdf',
|
|
207
|
-
'.txt': 'text/plain',
|
|
208
|
-
'.md': 'text/markdown',
|
|
209
|
-
'.csv': 'text/csv',
|
|
210
|
-
'.zip': 'application/zip',
|
|
211
|
-
};
|
|
212
|
-
return map[ext] || 'application/octet-stream';
|
|
213
|
-
}
|
|
214
|
-
function stableId(value) {
|
|
215
|
-
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 24);
|
|
216
|
-
}
|
|
217
84
|
function resolveFlowScriptPath(scriptPath, workDir) {
|
|
218
85
|
const candidates = [
|
|
219
86
|
scriptPath,
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import type { ExternalChannelAdapter, ExternalChannelConfig, ExternalChannelRuntimeStatus, ExternalChannelSendResult, ExternalMessageEvent, ExternalReply } from './base.js';
|
|
2
2
|
import { type WeChatRpaObservedMessage } from './wechat-rpa/normalizer.js';
|
|
3
|
+
type WeChatRpaSecret = {
|
|
4
|
+
type: 'wechat-rpa';
|
|
5
|
+
source?: 'macos-probe' | 'macos-flow' | 'windows-visual-flow' | 'wechat-rpa-lab' | 'fixture-jsonl';
|
|
6
|
+
fixturePath?: string;
|
|
7
|
+
pollIntervalMs?: number;
|
|
8
|
+
groups?: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
}>;
|
|
11
|
+
forceForeground?: boolean;
|
|
12
|
+
noRestore?: boolean;
|
|
13
|
+
downloadAttachments?: boolean;
|
|
14
|
+
downloadAttachmentsDir?: string;
|
|
15
|
+
idleSeconds?: number;
|
|
16
|
+
recentLimit?: number;
|
|
17
|
+
selfNickname?: string;
|
|
18
|
+
flowScriptPath?: string;
|
|
19
|
+
canReply?: boolean;
|
|
20
|
+
systemPrompt?: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
};
|
|
3
23
|
export declare function selectWeChatRpaLabReadKind(recentLimit?: number | null): 'read-latest' | 'scroll-read';
|
|
4
24
|
type WeChatRpaEvent = ExternalMessageEvent & {
|
|
5
25
|
type: 'external.message';
|
|
@@ -51,5 +71,6 @@ export declare function planWeChatRpaLabSendTasks(input: {
|
|
|
51
71
|
}): Array<Record<string, unknown>>;
|
|
52
72
|
export declare function weChatRpaLabTaskKindForAttachment(filePath: string): 'send-image' | 'send-video' | 'send-file';
|
|
53
73
|
export declare function materializeWeChatRpaOutboundAttachment(workDir: string, attachment: NonNullable<ExternalReply['attachment']>): Promise<string>;
|
|
74
|
+
export declare function resolveWeChatRpaInboundAttachmentDir(config: Pick<ExternalChannelConfig, 'id'>, secret: Pick<WeChatRpaSecret, 'downloadAttachments' | 'downloadAttachmentsDir'>): string | undefined;
|
|
54
75
|
export declare function annotateWeChatRpaInboundAttachments(attachments: WeChatRpaObservedMessage['attachments']): WeChatRpaObservedMessage['attachments'];
|
|
55
76
|
export {};
|