shennian 0.2.89 → 0.2.90
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 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,1028 +1,6 @@
|
|
|
1
|
-
// @arch docs/features/wechat-rpa-channel.md
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
-
import { ChannelSecretRegistry } from './secret-registry.js';
|
|
9
|
-
import { resolveShennianPath } from '../config/index.js';
|
|
10
|
-
import { probeMacWeChat, observedMessageFromProbe } from './wechat-rpa/macos.js';
|
|
11
|
-
import { runMacWeChatRpaFlow } from './wechat-rpa/macos-flow.js';
|
|
12
|
-
import { normalizeWeChatRpaMessage, WeChatRpaDeduper, weChatRpaConversationId, } from './wechat-rpa/normalizer.js';
|
|
13
|
-
const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
14
|
-
const DEFAULT_RECENT_LIMIT = 5;
|
|
15
|
-
const LAB_SCROLL_READ_RECENT_LIMIT = 8;
|
|
16
|
-
const MAX_OUTBOUND_ATTACHMENT_BYTES = Number(process.env.SHENNIAN_WECHAT_RPA_OUTBOUND_ATTACHMENT_MAX_BYTES || 20 * 1024 * 1024);
|
|
17
|
-
const INTERRUPTION_COOLDOWN_THRESHOLD = Number(process.env.SHENNIAN_WECHAT_RPA_INTERRUPTION_COOLDOWN_THRESHOLD || 3);
|
|
18
|
-
const INTERRUPTION_COOLDOWN_MS = Number(process.env.SHENNIAN_WECHAT_RPA_INTERRUPTION_COOLDOWN_MS || 5 * 60 * 1000);
|
|
19
|
-
const MAX_RECENT_TASK_SUMMARIES = 10;
|
|
20
|
-
export function selectWeChatRpaLabReadKind(recentLimit) {
|
|
21
|
-
return clampNumber(recentLimit, DEFAULT_RECENT_LIMIT, 1, 50) >= LAB_SCROLL_READ_RECENT_LIMIT ? 'scroll-read' : 'read-latest';
|
|
22
|
-
}
|
|
23
|
-
export class WeChatRpaChannelAdapter {
|
|
24
|
-
onMessage;
|
|
25
|
-
type = 'wechat-rpa';
|
|
26
|
-
secrets = new ChannelSecretRegistry();
|
|
27
|
-
connections = new Map();
|
|
28
|
-
constructor(onMessage) {
|
|
29
|
-
this.onMessage = onMessage;
|
|
30
|
-
}
|
|
31
|
-
async connect(config) {
|
|
32
|
-
if (!config.enabled)
|
|
33
|
-
return;
|
|
34
|
-
const secret = this.readSecret(config);
|
|
35
|
-
const conn = this.ensureConnection(config);
|
|
36
|
-
conn.stopped = false;
|
|
37
|
-
conn.config = config;
|
|
38
|
-
hydratePendingReplyState(conn, config);
|
|
39
|
-
hydrateMessageState(conn, config);
|
|
40
|
-
this.seedConfiguredConversations(conn, secret);
|
|
41
|
-
if (conn.timer)
|
|
42
|
-
return;
|
|
43
|
-
await this.enqueueOperation(conn, () => this.pollOnce(conn, this.readSecret(conn.config)));
|
|
44
|
-
conn.timer = setInterval(() => {
|
|
45
|
-
void this.enqueueOperation(conn, () => this.pollOnce(conn, this.readSecret(conn.config))).catch(() => { });
|
|
46
|
-
}, clampPollInterval(secret.pollIntervalMs));
|
|
47
|
-
conn.timer.unref();
|
|
48
|
-
}
|
|
49
|
-
async disconnect(config) {
|
|
50
|
-
const conn = this.connections.get(config.id);
|
|
51
|
-
if (!conn)
|
|
52
|
-
return;
|
|
53
|
-
conn.stopped = true;
|
|
54
|
-
if (conn.timer)
|
|
55
|
-
clearInterval(conn.timer);
|
|
56
|
-
this.connections.delete(config.id);
|
|
57
|
-
}
|
|
58
|
-
async syncNow(config) {
|
|
59
|
-
if (!config.enabled)
|
|
60
|
-
throw new Error('WeChat RPA channel is disabled');
|
|
61
|
-
const secret = this.readSecret(config);
|
|
62
|
-
const conn = this.ensureConnection(config);
|
|
63
|
-
conn.stopped = false;
|
|
64
|
-
conn.config = config;
|
|
65
|
-
hydratePendingReplyState(conn, config);
|
|
66
|
-
hydrateMessageState(conn, config);
|
|
67
|
-
this.seedConfiguredConversations(conn, secret);
|
|
68
|
-
return this.enqueueOperation(conn, () => this.pollOnce(conn, this.readSecret(conn.config)));
|
|
69
|
-
}
|
|
70
|
-
async send(config, reply) {
|
|
71
|
-
const secret = this.readSecret(config);
|
|
72
|
-
if (secret.canReply === false)
|
|
73
|
-
throw new Error('WeChat RPA channel does not allow replies');
|
|
74
|
-
if (!isFlowSource(secret.source)) {
|
|
75
|
-
throw new Error('WeChat RPA reply requires source=macos-flow, windows-visual-flow, or wechat-rpa-lab');
|
|
76
|
-
}
|
|
77
|
-
const conn = this.ensureConnection(config);
|
|
78
|
-
conn.config = config;
|
|
79
|
-
hydratePendingReplyState(conn, config);
|
|
80
|
-
hydrateMessageState(conn, config);
|
|
81
|
-
this.seedConfiguredConversations(conn, secret);
|
|
82
|
-
const conversationName = this.resolveConversationName(config, secret, reply.conversationId);
|
|
83
|
-
if (!conversationName)
|
|
84
|
-
throw new Error(`Unknown WeChat RPA conversation: ${reply.conversationId}`);
|
|
85
|
-
const pendingKey = pendingReplyKey(config, reply);
|
|
86
|
-
if (conn.completedPendingReplyKeys.has(pendingKey))
|
|
87
|
-
return { status: 'sent' };
|
|
88
|
-
const manualReview = conn.manualReviewReplies.get(pendingKey);
|
|
89
|
-
if (manualReview)
|
|
90
|
-
return { status: 'manual-review', reason: manualReview.reason };
|
|
91
|
-
const attachmentPath = reply.attachment
|
|
92
|
-
? await materializeWeChatRpaOutboundAttachment(config.workDir, reply.attachment)
|
|
93
|
-
: undefined;
|
|
94
|
-
const text = reply.text.trim();
|
|
95
|
-
if (!text && !attachmentPath)
|
|
96
|
-
throw new Error('Reply text or attachment is required');
|
|
97
|
-
if (isInterruptionCooldownActive(conn, secret)) {
|
|
98
|
-
const reason = 'WeChat RPA is cooling down after repeated user activity';
|
|
99
|
-
conn.runtimeState = 'cooldown';
|
|
100
|
-
enqueuePendingReply(conn, {
|
|
101
|
-
key: pendingKey,
|
|
102
|
-
conversationId: reply.conversationId,
|
|
103
|
-
conversationName,
|
|
104
|
-
text,
|
|
105
|
-
attachmentPath,
|
|
106
|
-
reason,
|
|
107
|
-
});
|
|
108
|
-
return { status: 'queued', reason };
|
|
109
|
-
}
|
|
110
|
-
let flow;
|
|
111
|
-
try {
|
|
112
|
-
conn.runtimeState = 'syncing';
|
|
113
|
-
flow = await this.enqueueOperation(conn, () => {
|
|
114
|
-
conn.lastRunAt = new Date().toISOString();
|
|
115
|
-
return runSendFlow(config, secret, conversationName, text, attachmentPath, pendingKey);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
conn.lastError = error instanceof Error ? error.message : String(error);
|
|
120
|
-
addTaskSummary(conn, {
|
|
121
|
-
status: classifyWeChatRpaFailure(conn.lastError),
|
|
122
|
-
runId: conn.lastRunId ?? null,
|
|
123
|
-
summary: conn.lastError,
|
|
124
|
-
});
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
recordTraceRuntime(conn, flow);
|
|
128
|
-
const partial = applyPartialSendProgress(flow, { text, attachmentPath });
|
|
129
|
-
if (partial.status === 'queued') {
|
|
130
|
-
const reason = partial.reason;
|
|
131
|
-
enqueuePendingReply(conn, {
|
|
132
|
-
key: pendingKey,
|
|
133
|
-
conversationId: reply.conversationId,
|
|
134
|
-
conversationName,
|
|
135
|
-
text: partial.text,
|
|
136
|
-
attachmentPath: partial.attachmentPath,
|
|
137
|
-
reason,
|
|
138
|
-
skipText: partial.skipText,
|
|
139
|
-
});
|
|
140
|
-
conn.lastError = reason;
|
|
141
|
-
return { status: 'queued', reason };
|
|
142
|
-
}
|
|
143
|
-
if (flow.interrupted) {
|
|
144
|
-
const reason = flow.error || 'WeChat RPA send was interrupted by user activity';
|
|
145
|
-
noteInterruption(conn, flow, reason);
|
|
146
|
-
enqueuePendingReply(conn, {
|
|
147
|
-
key: pendingKey,
|
|
148
|
-
conversationId: reply.conversationId,
|
|
149
|
-
conversationName,
|
|
150
|
-
text: partial.text,
|
|
151
|
-
attachmentPath,
|
|
152
|
-
reason,
|
|
153
|
-
skipText: partial.skipText,
|
|
154
|
-
});
|
|
155
|
-
return { status: 'queued', reason };
|
|
156
|
-
}
|
|
157
|
-
if (!flow.ok) {
|
|
158
|
-
const reason = flow.error || 'WeChat RPA send validator failed; manual review required before retry';
|
|
159
|
-
noteManualReview(conn, pendingKey, reason);
|
|
160
|
-
return { status: 'manual-review', reason };
|
|
161
|
-
}
|
|
162
|
-
addTaskSummary(conn, {
|
|
163
|
-
status: 'sent',
|
|
164
|
-
runId: conn.lastRunId ?? null,
|
|
165
|
-
summary: flow.rpaTraceSummary || `sent ${conversationName}`,
|
|
166
|
-
});
|
|
167
|
-
noteSuccessfulRun(conn);
|
|
168
|
-
conn.lastError = null;
|
|
169
|
-
return { status: 'sent' };
|
|
170
|
-
}
|
|
171
|
-
async health(config) {
|
|
172
|
-
const secret = this.readSecret(config);
|
|
173
|
-
if ((secret.source ?? 'macos-probe') === 'fixture-jsonl') {
|
|
174
|
-
return secret.fixturePath && fs.existsSync(secret.fixturePath)
|
|
175
|
-
? { ok: true }
|
|
176
|
-
: { ok: false, message: 'WeChat RPA fixture file is missing' };
|
|
177
|
-
}
|
|
178
|
-
if (secret.source === 'macos-flow') {
|
|
179
|
-
if (process.platform !== 'darwin')
|
|
180
|
-
return { ok: false, message: 'WeChat RPA macOS flow requires macOS' };
|
|
181
|
-
if (!configuredGroups(secret).length)
|
|
182
|
-
return { ok: false, message: 'WeChat RPA macOS flow requires at least one group' };
|
|
183
|
-
}
|
|
184
|
-
if (secret.source === 'wechat-rpa-lab') {
|
|
185
|
-
if (process.platform !== 'darwin')
|
|
186
|
-
return { ok: false, message: 'WeChat RPA Lab source requires macOS for live runs' };
|
|
187
|
-
if (!configuredGroups(secret).length)
|
|
188
|
-
return { ok: false, message: 'WeChat RPA Lab source requires at least one group' };
|
|
189
|
-
try {
|
|
190
|
-
resolveWeChatRpaLabIndex(config.workDir);
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
194
|
-
}
|
|
195
|
-
return { ok: true, message: 'WeChat RPA Lab source configured' };
|
|
196
|
-
}
|
|
197
|
-
if (secret.source === 'windows-visual-flow') {
|
|
198
|
-
return windowsVisualFlowHealth(secret, process.platform);
|
|
199
|
-
}
|
|
200
|
-
const probe = await probeMacWeChat();
|
|
201
|
-
return probe.ok
|
|
202
|
-
? { ok: true, message: probe.wechatRunning ? 'WeChat detected' : 'WeChat is not running' }
|
|
203
|
-
: { ok: false, message: probe.message };
|
|
204
|
-
}
|
|
205
|
-
async defaultConversation(config) {
|
|
206
|
-
const secret = this.readSecret(config);
|
|
207
|
-
const first = configuredGroups(secret)[0];
|
|
208
|
-
if (!first)
|
|
209
|
-
throw new Error('WeChat RPA channel has no configured groups');
|
|
210
|
-
return { conversationId: weChatRpaConversationId(first), conversationName: first };
|
|
211
|
-
}
|
|
212
|
-
runtimeStatus(config) {
|
|
213
|
-
const conn = this.connections.get(config.id);
|
|
214
|
-
if (!conn)
|
|
215
|
-
return {};
|
|
216
|
-
return {
|
|
217
|
-
wechatRpaRuntimeState: conn.runtimeState,
|
|
218
|
-
wechatRpaLastRunAt: conn.lastRunAt ?? null,
|
|
219
|
-
wechatRpaLastMessageAt: conn.lastMessageAt ?? null,
|
|
220
|
-
wechatRpaLastInterruptedAt: conn.lastInterruptedAt ?? null,
|
|
221
|
-
wechatRpaPendingReplyCount: conn.pendingReplies.size,
|
|
222
|
-
wechatRpaLastError: conn.lastError ?? null,
|
|
223
|
-
wechatRpaLastRunId: conn.lastRunId ?? null,
|
|
224
|
-
wechatRpaLastTracePath: conn.lastTracePath ?? null,
|
|
225
|
-
wechatRpaLastTraceSummary: conn.lastTraceSummary ?? null,
|
|
226
|
-
wechatRpaRecentTaskSummaries: conn.recentTaskSummaries,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
ensureConnection(config) {
|
|
230
|
-
let conn = this.connections.get(config.id);
|
|
231
|
-
if (!conn) {
|
|
232
|
-
conn = {
|
|
233
|
-
config,
|
|
234
|
-
timer: null,
|
|
235
|
-
deduper: new WeChatRpaDeduper(),
|
|
236
|
-
stopped: false,
|
|
237
|
-
conversations: new Map(),
|
|
238
|
-
pendingReplies: new Map(),
|
|
239
|
-
manualReviewReplies: new Map(),
|
|
240
|
-
completedPendingReplyKeys: new Set(),
|
|
241
|
-
pendingStatePath: undefined,
|
|
242
|
-
messageStatePath: undefined,
|
|
243
|
-
operation: Promise.resolve(),
|
|
244
|
-
recentTaskSummaries: [],
|
|
245
|
-
runtimeState: 'idle_waiting',
|
|
246
|
-
consecutiveInterruptions: 0,
|
|
247
|
-
};
|
|
248
|
-
this.connections.set(config.id, conn);
|
|
249
|
-
}
|
|
250
|
-
return conn;
|
|
251
|
-
}
|
|
252
|
-
enqueueOperation(conn, operation) {
|
|
253
|
-
const run = conn.operation.then(operation, operation);
|
|
254
|
-
conn.operation = run.then(() => undefined, () => undefined);
|
|
255
|
-
return run;
|
|
256
|
-
}
|
|
257
|
-
readSecret(config) {
|
|
258
|
-
const secret = this.secrets.get(config.secretRef);
|
|
259
|
-
if (!secret || secret.type !== 'wechat-rpa') {
|
|
260
|
-
throw new Error('WeChat RPA channel is not configured on this daemon');
|
|
261
|
-
}
|
|
262
|
-
return secret;
|
|
263
|
-
}
|
|
264
|
-
async pollOnce(conn, secret) {
|
|
265
|
-
if (conn.stopped)
|
|
266
|
-
return [];
|
|
267
|
-
const emitted = [];
|
|
268
|
-
conn.lastRunAt = new Date().toISOString();
|
|
269
|
-
try {
|
|
270
|
-
if (isInterruptionCooldownActive(conn, secret)) {
|
|
271
|
-
conn.runtimeState = 'cooldown';
|
|
272
|
-
return [];
|
|
273
|
-
}
|
|
274
|
-
const pendingInterrupted = await this.drainPendingReplies(conn, secret);
|
|
275
|
-
if (pendingInterrupted)
|
|
276
|
-
return [];
|
|
277
|
-
let interrupted = false;
|
|
278
|
-
conn.runtimeState = 'syncing';
|
|
279
|
-
const observed = await this.readObservedMessages(conn.config, secret, (flow) => {
|
|
280
|
-
recordTraceRuntime(conn, flow);
|
|
281
|
-
if (!flow.interrupted)
|
|
282
|
-
return;
|
|
283
|
-
interrupted = true;
|
|
284
|
-
noteInterruption(conn, flow, flow.error || 'WeChat RPA poll was interrupted by user activity');
|
|
285
|
-
});
|
|
286
|
-
conn.lastError = null;
|
|
287
|
-
for (const item of observed) {
|
|
288
|
-
const message = normalizeWeChatRpaMessage(item, { selfNicknames: wechatRpaSelfNicknames(secret) });
|
|
289
|
-
if (!message || !conn.deduper.accept(message.messageId))
|
|
290
|
-
continue;
|
|
291
|
-
persistMessageState(conn);
|
|
292
|
-
conn.conversations.set(message.conversationId, message.conversationName);
|
|
293
|
-
conn.lastMessageAt = message.receivedAt;
|
|
294
|
-
const event = {
|
|
295
|
-
type: 'external.message',
|
|
296
|
-
managerSessionId: conn.config.managerSessionId,
|
|
297
|
-
channelId: conn.config.id,
|
|
298
|
-
channelType: 'wechat-rpa',
|
|
299
|
-
conversationId: message.conversationId,
|
|
300
|
-
conversationName: message.conversationName,
|
|
301
|
-
messageId: message.messageId,
|
|
302
|
-
sender: message.sender,
|
|
303
|
-
text: message.text,
|
|
304
|
-
attachments: message.attachments,
|
|
305
|
-
receivedAt: message.receivedAt,
|
|
306
|
-
isMentioned: message.isMentioned,
|
|
307
|
-
replyTarget: '',
|
|
308
|
-
rawRef: message.rawRef,
|
|
309
|
-
};
|
|
310
|
-
emitted.push(this.onMessage?.(event) ?? event);
|
|
311
|
-
addTaskSummary(conn, {
|
|
312
|
-
status: 'received',
|
|
313
|
-
runId: conn.lastRunId ?? null,
|
|
314
|
-
summary: `received ${message.conversationName}${message.sender.name ? ` from ${message.sender.name}` : ''}: ${clipSummary(message.text || message.attachments[0]?.name || 'attachment')}`,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
if (!interrupted)
|
|
318
|
-
noteSuccessfulRun(conn);
|
|
319
|
-
return emitted;
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
conn.lastError = error instanceof Error ? error.message : String(error);
|
|
323
|
-
addTaskSummary(conn, {
|
|
324
|
-
status: classifyWeChatRpaFailure(conn.lastError),
|
|
325
|
-
runId: conn.lastRunId ?? null,
|
|
326
|
-
summary: conn.lastError,
|
|
327
|
-
});
|
|
328
|
-
throw error;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
async drainPendingReplies(conn, secret) {
|
|
332
|
-
if (!conn.pendingReplies.size)
|
|
333
|
-
return false;
|
|
334
|
-
for (const pending of Array.from(conn.pendingReplies.values())) {
|
|
335
|
-
conn.runtimeState = 'retrying';
|
|
336
|
-
pending.attempts += 1;
|
|
337
|
-
pending.lastAttemptAt = new Date().toISOString();
|
|
338
|
-
persistPendingReplyState(conn);
|
|
339
|
-
const flow = await runSendFlow(conn.config, secret, pending.conversationName, pending.skipText ? '' : pending.text, pending.attachmentPath, pending.key);
|
|
340
|
-
recordTraceRuntime(conn, flow);
|
|
341
|
-
const partial = applyPartialSendProgress(flow, {
|
|
342
|
-
text: pending.skipText ? '' : pending.text,
|
|
343
|
-
attachmentPath: pending.attachmentPath,
|
|
344
|
-
});
|
|
345
|
-
if (partial.status === 'queued') {
|
|
346
|
-
pending.text = partial.text;
|
|
347
|
-
pending.attachmentPath = partial.attachmentPath;
|
|
348
|
-
pending.skipText = partial.skipText;
|
|
349
|
-
pending.lastInterruptedReason = partial.reason;
|
|
350
|
-
conn.lastError = partial.reason;
|
|
351
|
-
persistPendingReplyState(conn);
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
354
|
-
if (flow.interrupted) {
|
|
355
|
-
const reason = flow.error || 'WeChat RPA pending send was interrupted by user activity';
|
|
356
|
-
pending.text = partial.text;
|
|
357
|
-
pending.attachmentPath = partial.attachmentPath;
|
|
358
|
-
pending.skipText = partial.skipText;
|
|
359
|
-
pending.lastInterruptedReason = reason;
|
|
360
|
-
noteInterruption(conn, flow, reason);
|
|
361
|
-
persistPendingReplyState(conn);
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
if (!flow.ok) {
|
|
365
|
-
const reason = flow.error || 'WeChat RPA pending send validator failed; manual review required before retry';
|
|
366
|
-
conn.pendingReplies.delete(pending.key);
|
|
367
|
-
noteManualReview(conn, pending.key, reason);
|
|
368
|
-
persistPendingReplyState(conn);
|
|
369
|
-
return true;
|
|
370
|
-
}
|
|
371
|
-
conn.pendingReplies.delete(pending.key);
|
|
372
|
-
conn.completedPendingReplyKeys.add(pending.key);
|
|
373
|
-
persistPendingReplyState(conn);
|
|
374
|
-
conn.lastError = null;
|
|
375
|
-
addTaskSummary(conn, {
|
|
376
|
-
status: 'sent',
|
|
377
|
-
runId: conn.lastRunId ?? null,
|
|
378
|
-
summary: flow.rpaTraceSummary || `sent ${pending.conversationName}`,
|
|
379
|
-
});
|
|
380
|
-
noteSuccessfulRun(conn);
|
|
381
|
-
}
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
async readObservedMessages(config, secret, onFlow) {
|
|
385
|
-
if (secret.source === 'fixture-jsonl') {
|
|
386
|
-
return readFixtureMessages(secret.fixturePath);
|
|
387
|
-
}
|
|
388
|
-
if (secret.source === 'macos-flow') {
|
|
389
|
-
return readMacFlowMessages(config, secret, onFlow);
|
|
390
|
-
}
|
|
391
|
-
if (secret.source === 'wechat-rpa-lab') {
|
|
392
|
-
return readLabMessages(config, secret, onFlow);
|
|
393
|
-
}
|
|
394
|
-
if (secret.source === 'windows-visual-flow') {
|
|
395
|
-
return readWindowsVisualFlowMessages(config, secret, onFlow);
|
|
396
|
-
}
|
|
397
|
-
const probe = await probeMacWeChat();
|
|
398
|
-
const message = observedMessageFromProbe(probe);
|
|
399
|
-
return message ? [message] : [];
|
|
400
|
-
}
|
|
401
|
-
seedConfiguredConversations(conn, secret) {
|
|
402
|
-
for (const name of configuredGroups(secret)) {
|
|
403
|
-
conn.conversations.set(weChatRpaConversationId(name), name);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
resolveConversationName(config, secret, conversationId) {
|
|
407
|
-
const conn = this.connections.get(config.id);
|
|
408
|
-
const cached = conn?.conversations.get(conversationId);
|
|
409
|
-
if (cached)
|
|
410
|
-
return cached;
|
|
411
|
-
return configuredGroups(secret).find((name) => weChatRpaConversationId(name) === conversationId) ?? null;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
function clampPollInterval(value) {
|
|
415
|
-
if (!Number.isFinite(value))
|
|
416
|
-
return DEFAULT_POLL_INTERVAL_MS;
|
|
417
|
-
return Math.min(60_000, Math.max(1_000, Number(value)));
|
|
418
|
-
}
|
|
419
|
-
function configuredGroups(secret) {
|
|
420
|
-
return Array.isArray(secret.groups)
|
|
421
|
-
? secret.groups.map((group) => String(group?.name || '').trim()).filter(Boolean)
|
|
422
|
-
: [];
|
|
423
|
-
}
|
|
424
|
-
function wechatRpaSelfNicknames(secret) {
|
|
425
|
-
return String(secret.selfNickname || '')
|
|
426
|
-
.split(/[,\n,、]/)
|
|
427
|
-
.map((item) => item.trim())
|
|
428
|
-
.filter(Boolean);
|
|
429
|
-
}
|
|
430
|
-
function isWeChatRpaTextMentioned(text, aliases) {
|
|
431
|
-
if (!aliases.length)
|
|
432
|
-
return false;
|
|
433
|
-
return aliases.some((alias) => {
|
|
434
|
-
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
435
|
-
return new RegExp(`@\\s*${escaped}(?=$|\\s|[,。!?,.!?::;;、)])`).test(text);
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
export function windowsVisualFlowHealth(secret, platform = process.platform) {
|
|
439
|
-
if (platform !== 'win32')
|
|
440
|
-
return { ok: false, message: 'WeChat RPA Windows visual flow requires Windows' };
|
|
441
|
-
void secret;
|
|
442
|
-
return { ok: false, message: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it' };
|
|
443
|
-
}
|
|
444
|
-
async function readLabMessages(config, secret, onFlow) {
|
|
445
|
-
const api = await importWeChatRpaLabApi(config.workDir);
|
|
446
|
-
const result = [];
|
|
447
|
-
for (const name of configuredGroups(secret)) {
|
|
448
|
-
const flow = await runLabReadFlow(api, config, secret, name);
|
|
449
|
-
onFlow?.(flow);
|
|
450
|
-
if (!flow.ok || flow.interrupted)
|
|
451
|
-
continue;
|
|
452
|
-
for (const message of flow.newMessages ?? []) {
|
|
453
|
-
const observed = flowMessageToObservedMessage(name, message, secret);
|
|
454
|
-
if (observed)
|
|
455
|
-
result.push(observed);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return result;
|
|
459
|
-
}
|
|
460
|
-
async function runLabReadFlow(api, config, secret, groupName) {
|
|
461
|
-
const kind = selectWeChatRpaLabReadKind(secret.recentLimit);
|
|
462
|
-
const task = {
|
|
463
|
-
kind,
|
|
464
|
-
requestId: `wechat-rpa:${groupName}:read:${Date.now()}`,
|
|
465
|
-
targetGroup: groupName,
|
|
466
|
-
policy: secret.forceForeground ? 'work' : 'polite',
|
|
467
|
-
limit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 1, 50),
|
|
468
|
-
attachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
|
|
469
|
-
};
|
|
470
|
-
const result = await api.runWechatRpaReadLatest(task);
|
|
471
|
-
const structuredMessages = Array.isArray(result.data?.structuredMessages) ? result.data.structuredMessages : [];
|
|
472
|
-
const latestMessages = result.validation?.deterministic?.latestMessages ?? [];
|
|
473
|
-
const flowMessages = structuredMessages.length
|
|
474
|
-
? structuredMessages.map((message, index) => structuredMessageToFlowMessage(message, result.runId || 'lab-read', index))
|
|
475
|
-
: latestMessages.map((text, index) => ({
|
|
476
|
-
id: `${result.runId || 'lab-read'}:${index}`,
|
|
477
|
-
text,
|
|
478
|
-
confidence: 0.8,
|
|
479
|
-
}));
|
|
480
|
-
return {
|
|
481
|
-
ok: Boolean(result.ok),
|
|
482
|
-
groupName,
|
|
483
|
-
interrupted: result.status === 'interrupted',
|
|
484
|
-
reason: result.status,
|
|
485
|
-
rpaRunId: result.runId,
|
|
486
|
-
rpaTraceSummary: `${kind} ${result.status || (result.ok ? 'success' : 'failed')} ${groupName}`,
|
|
487
|
-
recentMessages: flowMessages,
|
|
488
|
-
newMessages: flowMessages,
|
|
489
|
-
screenshotPath: result.tracePath,
|
|
490
|
-
error: labResultError(result),
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
function structuredMessageToFlowMessage(message, runId, index) {
|
|
494
|
-
return {
|
|
495
|
-
id: `${runId}:${message.index ?? index}`,
|
|
496
|
-
text: String(message.text || message.card?.title || '').trim(),
|
|
497
|
-
confidence: typeof message.confidence === 'number' ? message.confidence : 0.75,
|
|
498
|
-
senderName: typeof message.senderName === 'string' ? message.senderName : null,
|
|
499
|
-
attachments: structuredAttachments(message),
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
function structuredAttachments(message) {
|
|
503
|
-
const kind = String(message.kind || '');
|
|
504
|
-
if (!['image', 'file', 'video-file', 'video-card', 'link-card', 'official-account-card', 'mini-program-card'].includes(kind))
|
|
505
|
-
return [];
|
|
506
|
-
const localPath = typeof message.localPath === 'string' ? message.localPath : undefined;
|
|
507
|
-
const attachmentType = kind === 'video-file' || kind === 'video-card' ? 'video' : kind === 'link-card' || kind === 'official-account-card' || kind === 'mini-program-card' ? 'file' : kind;
|
|
508
|
-
return [{
|
|
509
|
-
type: attachmentType,
|
|
510
|
-
localPath,
|
|
511
|
-
availability: localPath ? 'edge-local' : 'metadata-only',
|
|
512
|
-
}];
|
|
513
|
-
}
|
|
514
|
-
async function runLabSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey) {
|
|
515
|
-
const api = await importWeChatRpaLabApi(config.workDir);
|
|
516
|
-
const tasks = planWeChatRpaLabSendTasks({
|
|
517
|
-
groupName: conversationName,
|
|
518
|
-
text,
|
|
519
|
-
attachmentPath,
|
|
520
|
-
dedupeKey,
|
|
521
|
-
requestId: dedupeKey,
|
|
522
|
-
policy: 'work',
|
|
523
|
-
});
|
|
524
|
-
const results = [];
|
|
525
|
-
for (const task of tasks) {
|
|
526
|
-
const result = await api.runWechatRpaTask(task);
|
|
527
|
-
results.push(result);
|
|
528
|
-
if (!result.ok)
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
const textResult = results.find((result) => result.kind === 'send-text');
|
|
532
|
-
const attachmentResult = results.find((result) => result.kind === 'send-image' || result.kind === 'send-video' || result.kind === 'send-file');
|
|
533
|
-
const ok = tasks.length > 0 && results.length === tasks.length && results.every((result) => result.ok);
|
|
534
|
-
const interrupted = results.some((result) => result.status === 'interrupted');
|
|
535
|
-
return {
|
|
536
|
-
ok,
|
|
537
|
-
groupName: conversationName,
|
|
538
|
-
interrupted,
|
|
539
|
-
reason: results.at(-1)?.status,
|
|
540
|
-
rpaRunId: results.at(-1)?.runId,
|
|
541
|
-
rpaTraceSummary: `send ${ok ? 'success' : results.at(-1)?.status || 'failed'} ${conversationName} (${results.length}/${tasks.length})`,
|
|
542
|
-
sentReply: Boolean(text),
|
|
543
|
-
sentReplyObserved: text ? Boolean(textResult?.ok) : false,
|
|
544
|
-
sentAttachment: Boolean(attachmentPath),
|
|
545
|
-
sentAttachmentObserved: attachmentPath ? Boolean(attachmentResult?.ok) : false,
|
|
546
|
-
postSendScreenshotPath: results.at(-1)?.tracePath,
|
|
547
|
-
error: ok ? undefined : labResultError(results.at(-1)),
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
export function planWeChatRpaLabSendTasks(input) {
|
|
551
|
-
const tasks = [];
|
|
552
|
-
const text = String(input.text || '').trim();
|
|
553
|
-
const policy = input.policy || 'work';
|
|
554
|
-
if (text) {
|
|
555
|
-
tasks.push({
|
|
556
|
-
kind: 'send-text',
|
|
557
|
-
requestId: input.requestId ? `${input.requestId}:text` : undefined,
|
|
558
|
-
targetGroup: input.groupName,
|
|
559
|
-
policy,
|
|
560
|
-
text,
|
|
561
|
-
...(input.dedupeKey ? { dedupeKey: `${input.dedupeKey}:text` } : {}),
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
if (input.attachmentPath) {
|
|
565
|
-
const kind = weChatRpaLabTaskKindForAttachment(input.attachmentPath);
|
|
566
|
-
tasks.push({
|
|
567
|
-
kind,
|
|
568
|
-
requestId: input.requestId ? `${input.requestId}:attachment` : undefined,
|
|
569
|
-
targetGroup: input.groupName,
|
|
570
|
-
policy,
|
|
571
|
-
filePath: input.attachmentPath,
|
|
572
|
-
...(input.dedupeKey ? { dedupeKey: `${input.dedupeKey}:attachment` } : {}),
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
return tasks;
|
|
576
|
-
}
|
|
577
|
-
export function weChatRpaLabTaskKindForAttachment(filePath) {
|
|
578
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
579
|
-
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.heic'].includes(ext))
|
|
580
|
-
return 'send-image';
|
|
581
|
-
if (['.mp4', '.mov', '.avi', '.mkv'].includes(ext))
|
|
582
|
-
return 'send-video';
|
|
583
|
-
return 'send-file';
|
|
584
|
-
}
|
|
585
|
-
async function importWeChatRpaLabApi(workDir) {
|
|
586
|
-
const modulePath = resolveWeChatRpaLabIndex(workDir);
|
|
587
|
-
return await import(pathToFileURL(modulePath).href);
|
|
588
|
-
}
|
|
589
|
-
function resolveWeChatRpaLabIndex(workDir) {
|
|
590
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
591
|
-
const candidates = [
|
|
592
|
-
process.env.SHENNIAN_WECHAT_RPA_LAB_INDEX,
|
|
593
|
-
path.resolve(moduleDir, '../../../../scripts/wechat-rpa-lab/index.mjs'),
|
|
594
|
-
workDir ? path.join(workDir, 'scripts/wechat-rpa-lab/index.mjs') : '',
|
|
595
|
-
path.resolve(process.cwd(), 'scripts/wechat-rpa-lab/index.mjs'),
|
|
596
|
-
].filter(Boolean);
|
|
597
|
-
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
|
598
|
-
if (!found) {
|
|
599
|
-
throw new Error('WeChat RPA Lab API is missing; set SHENNIAN_WECHAT_RPA_LAB_INDEX or run from the repository root');
|
|
600
|
-
}
|
|
601
|
-
return path.resolve(found);
|
|
602
|
-
}
|
|
603
|
-
function labResultError(result) {
|
|
604
|
-
if (!result)
|
|
605
|
-
return 'WeChat RPA Lab did not return a result';
|
|
606
|
-
if (typeof result.error === 'string')
|
|
607
|
-
return result.error;
|
|
608
|
-
return result.error?.message || (result.ok ? undefined : `WeChat RPA Lab task ${result.kind || '<unknown>'} ${result.status || 'failed'}`);
|
|
609
|
-
}
|
|
610
|
-
async function readMacFlowMessages(config, secret, onFlow) {
|
|
611
|
-
const result = [];
|
|
612
|
-
for (const name of configuredGroups(secret)) {
|
|
613
|
-
const flow = await runMacWeChatRpaFlow({
|
|
614
|
-
groupName: name,
|
|
615
|
-
scriptPath: secret.flowScriptPath,
|
|
616
|
-
workDir: config.workDir,
|
|
617
|
-
forceForeground: Boolean(secret.forceForeground),
|
|
618
|
-
noRestore: secret.noRestore !== false,
|
|
619
|
-
idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
|
|
620
|
-
recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
|
|
621
|
-
downloadAttachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
|
|
622
|
-
});
|
|
623
|
-
onFlow?.(flow);
|
|
624
|
-
if (flow.interrupted)
|
|
625
|
-
continue;
|
|
626
|
-
for (const message of flow.newMessages ?? []) {
|
|
627
|
-
const observed = flowMessageToObservedMessage(name, message, secret);
|
|
628
|
-
if (observed)
|
|
629
|
-
result.push(observed);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
return result;
|
|
633
|
-
}
|
|
634
|
-
async function readWindowsVisualFlowMessages(config, secret, onFlow) {
|
|
635
|
-
void config;
|
|
636
|
-
const groupName = configuredGroups(secret)[0] || '<unbound>';
|
|
637
|
-
onFlow?.({
|
|
638
|
-
ok: false,
|
|
639
|
-
groupName,
|
|
640
|
-
interrupted: true,
|
|
641
|
-
reason: 'windows-visual-flow-archived',
|
|
642
|
-
newMessages: [],
|
|
643
|
-
recentMessages: [],
|
|
644
|
-
error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
|
|
645
|
-
});
|
|
646
|
-
return [];
|
|
647
|
-
}
|
|
648
|
-
function flowMessageToObservedMessage(conversationName, message, secret) {
|
|
649
|
-
const text = String(message.text || '').trim();
|
|
650
|
-
const attachments = Array.isArray(message.attachments) ? message.attachments : [];
|
|
651
|
-
if (!text && attachments.length === 0)
|
|
652
|
-
return null;
|
|
653
|
-
const senderName = typeof message.senderName === 'string' ? message.senderName.trim() : '';
|
|
654
|
-
const observedAt = String(message.observedAt || message.timestampIso || '').trim() || new Date().toISOString();
|
|
655
|
-
return {
|
|
656
|
-
conversationName,
|
|
657
|
-
senderName: senderName || null,
|
|
658
|
-
text,
|
|
659
|
-
attachments: annotateWeChatRpaInboundAttachments(attachments),
|
|
660
|
-
observedAt,
|
|
661
|
-
isMentioned: message.isMentioned === true || isWeChatRpaTextMentioned(text, wechatRpaSelfNicknames(secret)),
|
|
662
|
-
rawId: String(message.id || `${conversationName}:${senderName}:${text}:${observedAt}`),
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
async function runSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey) {
|
|
666
|
-
if (secret.source === 'windows-visual-flow') {
|
|
667
|
-
void config;
|
|
668
|
-
void text;
|
|
669
|
-
void attachmentPath;
|
|
670
|
-
void dedupeKey;
|
|
671
|
-
return {
|
|
672
|
-
ok: false,
|
|
673
|
-
groupName: conversationName,
|
|
674
|
-
interrupted: true,
|
|
675
|
-
reason: 'windows-visual-flow-archived',
|
|
676
|
-
newMessages: [],
|
|
677
|
-
recentMessages: [],
|
|
678
|
-
error: 'WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it',
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
if (secret.source === 'wechat-rpa-lab') {
|
|
682
|
-
return runLabSendFlow(config, secret, conversationName, text, attachmentPath, dedupeKey);
|
|
683
|
-
}
|
|
684
|
-
return runMacWeChatRpaFlow({
|
|
685
|
-
groupName: conversationName,
|
|
686
|
-
replyText: text || undefined,
|
|
687
|
-
attachmentPath,
|
|
688
|
-
scriptPath: secret.flowScriptPath,
|
|
689
|
-
workDir: config.workDir,
|
|
690
|
-
forceForeground: Boolean(secret.forceForeground),
|
|
691
|
-
noRestore: secret.noRestore !== false,
|
|
692
|
-
idleSeconds: clampNumber(secret.idleSeconds, 15, 0, 3600),
|
|
693
|
-
recentLimit: clampNumber(secret.recentLimit, DEFAULT_RECENT_LIMIT, 0, 50),
|
|
694
|
-
downloadAttachmentsDir: resolveWeChatRpaInboundAttachmentDir(config, secret),
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
function isFlowSource(source) {
|
|
698
|
-
return source === 'macos-flow' || source === 'wechat-rpa-lab';
|
|
699
|
-
}
|
|
700
|
-
function applyPartialSendProgress(flow, input) {
|
|
701
|
-
if (input.text && input.attachmentPath && flow.sentReplyObserved && !flow.sentAttachmentObserved) {
|
|
702
|
-
return {
|
|
703
|
-
status: flow.interrupted ? 'continue' : 'queued',
|
|
704
|
-
text: '',
|
|
705
|
-
attachmentPath: input.attachmentPath,
|
|
706
|
-
skipText: true,
|
|
707
|
-
reason: flow.error || 'WeChat RPA sent the text but did not confirm the attachment; queued attachment-only retry',
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
return {
|
|
711
|
-
status: 'continue',
|
|
712
|
-
text: input.text,
|
|
713
|
-
attachmentPath: input.attachmentPath,
|
|
714
|
-
reason: flow.error || '',
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
function pendingReplyKey(config, reply) {
|
|
718
|
-
if (reply.idempotencyKey)
|
|
719
|
-
return reply.idempotencyKey;
|
|
720
|
-
return crypto.createHash('sha256')
|
|
721
|
-
.update(`${config.id}\n${reply.conversationId}\n${reply.text}\n${reply.attachment?.name || ''}\n${reply.attachment?.size || 0}`)
|
|
722
|
-
.digest('hex')
|
|
723
|
-
.slice(0, 24);
|
|
724
|
-
}
|
|
725
|
-
function enqueuePendingReply(conn, input) {
|
|
726
|
-
const existing = conn.pendingReplies.get(input.key);
|
|
727
|
-
conn.pendingReplies.set(input.key, {
|
|
728
|
-
key: input.key,
|
|
729
|
-
conversationId: input.conversationId,
|
|
730
|
-
conversationName: input.conversationName,
|
|
731
|
-
text: input.text,
|
|
732
|
-
attachmentPath: input.attachmentPath,
|
|
733
|
-
skipText: input.skipText,
|
|
734
|
-
queuedAt: existing?.queuedAt ?? new Date().toISOString(),
|
|
735
|
-
attempts: existing?.attempts ?? 0,
|
|
736
|
-
lastAttemptAt: existing?.lastAttemptAt,
|
|
737
|
-
lastInterruptedReason: input.reason,
|
|
738
|
-
});
|
|
739
|
-
persistPendingReplyState(conn);
|
|
740
|
-
}
|
|
741
|
-
function noteManualReview(conn, key, reason) {
|
|
742
|
-
conn.manualReviewReplies.set(key, {
|
|
743
|
-
key,
|
|
744
|
-
reason,
|
|
745
|
-
createdAt: conn.manualReviewReplies.get(key)?.createdAt ?? new Date().toISOString(),
|
|
746
|
-
});
|
|
747
|
-
conn.lastError = reason;
|
|
748
|
-
addTaskSummary(conn, {
|
|
749
|
-
status: 'failed',
|
|
750
|
-
runId: conn.lastRunId ?? null,
|
|
751
|
-
summary: `manual review required: ${reason}`,
|
|
752
|
-
});
|
|
753
|
-
persistPendingReplyState(conn);
|
|
754
|
-
}
|
|
755
|
-
function hydratePendingReplyState(conn, config) {
|
|
756
|
-
const filePath = pendingReplyStatePath(config);
|
|
757
|
-
if (conn.pendingStatePath === filePath)
|
|
758
|
-
return;
|
|
759
|
-
conn.pendingStatePath = filePath;
|
|
760
|
-
const store = readPendingReplyStore(filePath);
|
|
761
|
-
for (const pending of store.pending ?? []) {
|
|
762
|
-
if (!isPendingReplyRecord(pending))
|
|
763
|
-
continue;
|
|
764
|
-
if (!conn.pendingReplies.has(pending.key))
|
|
765
|
-
conn.pendingReplies.set(pending.key, pending);
|
|
766
|
-
}
|
|
767
|
-
for (const item of store.manualReview ?? []) {
|
|
768
|
-
if (!isManualReviewRecord(item))
|
|
769
|
-
continue;
|
|
770
|
-
if (!conn.manualReviewReplies.has(item.key))
|
|
771
|
-
conn.manualReviewReplies.set(item.key, item);
|
|
772
|
-
}
|
|
773
|
-
for (const key of store.completedKeys ?? []) {
|
|
774
|
-
if (typeof key === 'string' && key)
|
|
775
|
-
conn.completedPendingReplyKeys.add(key);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
function persistPendingReplyState(conn) {
|
|
779
|
-
if (!conn.pendingStatePath)
|
|
780
|
-
return;
|
|
781
|
-
const completedKeys = Array.from(conn.completedPendingReplyKeys).slice(-500);
|
|
782
|
-
const pending = Array.from(conn.pendingReplies.values()).slice(0, 500);
|
|
783
|
-
const manualReview = Array.from(conn.manualReviewReplies.values()).slice(-500);
|
|
784
|
-
try {
|
|
785
|
-
fs.mkdirSync(path.dirname(conn.pendingStatePath), { recursive: true });
|
|
786
|
-
fs.writeFileSync(conn.pendingStatePath, JSON.stringify({ version: 1, pending, manualReview, completedKeys }, null, 2));
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
// Runtime state is best-effort; the in-memory queue still protects the current daemon.
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
function readPendingReplyStore(filePath) {
|
|
793
|
-
try {
|
|
794
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
795
|
-
return parsed && parsed.version === 1 ? parsed : { version: 1, pending: [], completedKeys: [] };
|
|
796
|
-
}
|
|
797
|
-
catch {
|
|
798
|
-
return { version: 1, pending: [], completedKeys: [] };
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
function isPendingReplyRecord(value) {
|
|
802
|
-
if (!value || typeof value !== 'object')
|
|
803
|
-
return false;
|
|
804
|
-
const record = value;
|
|
805
|
-
return typeof record.key === 'string'
|
|
806
|
-
&& typeof record.conversationId === 'string'
|
|
807
|
-
&& typeof record.conversationName === 'string'
|
|
808
|
-
&& typeof record.text === 'string'
|
|
809
|
-
&& typeof record.queuedAt === 'string'
|
|
810
|
-
&& typeof record.attempts === 'number'
|
|
811
|
-
&& Number.isFinite(record.attempts)
|
|
812
|
-
&& (record.attachmentPath === undefined || typeof record.attachmentPath === 'string')
|
|
813
|
-
&& (record.skipText === undefined || typeof record.skipText === 'boolean');
|
|
814
|
-
}
|
|
815
|
-
function isManualReviewRecord(value) {
|
|
816
|
-
if (!value || typeof value !== 'object')
|
|
817
|
-
return false;
|
|
818
|
-
const record = value;
|
|
819
|
-
return typeof record.key === 'string'
|
|
820
|
-
&& typeof record.reason === 'string'
|
|
821
|
-
&& typeof record.createdAt === 'string';
|
|
822
|
-
}
|
|
823
|
-
function pendingReplyStatePath(config) {
|
|
824
|
-
const id = crypto.createHash('sha256').update(config.id).digest('hex').slice(0, 16);
|
|
825
|
-
return path.join(config.workDir, '.shennian', 'wechat-rpa-pending-replies', `${id}.json`);
|
|
826
|
-
}
|
|
827
|
-
function hydrateMessageState(conn, config) {
|
|
828
|
-
const filePath = messageStatePath(config);
|
|
829
|
-
if (conn.messageStatePath === filePath)
|
|
830
|
-
return;
|
|
831
|
-
conn.messageStatePath = filePath;
|
|
832
|
-
const store = readMessageSeenStore(filePath);
|
|
833
|
-
conn.deduper = new WeChatRpaDeduper((store.messageIds ?? []).filter((id) => typeof id === 'string' && id.length > 0));
|
|
834
|
-
}
|
|
835
|
-
function persistMessageState(conn) {
|
|
836
|
-
if (!conn.messageStatePath)
|
|
837
|
-
return;
|
|
838
|
-
try {
|
|
839
|
-
fs.mkdirSync(path.dirname(conn.messageStatePath), { recursive: true });
|
|
840
|
-
fs.writeFileSync(conn.messageStatePath, JSON.stringify({ version: 1, messageIds: conn.deduper.snapshot() }, null, 2));
|
|
841
|
-
}
|
|
842
|
-
catch {
|
|
843
|
-
// Best-effort only; in-memory dedupe still protects the current daemon.
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
function readMessageSeenStore(filePath) {
|
|
847
|
-
try {
|
|
848
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
849
|
-
return parsed && parsed.version === 1 ? parsed : { version: 1, messageIds: [] };
|
|
850
|
-
}
|
|
851
|
-
catch {
|
|
852
|
-
return { version: 1, messageIds: [] };
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
function messageStatePath(config) {
|
|
856
|
-
const id = crypto.createHash('sha256').update(config.id).digest('hex').slice(0, 16);
|
|
857
|
-
return path.join(config.workDir, '.shennian', 'wechat-rpa-seen-messages', `${id}.json`);
|
|
858
|
-
}
|
|
859
|
-
function noteInterruption(conn, flow, reason) {
|
|
860
|
-
conn.lastInterruptedAt = new Date().toISOString();
|
|
861
|
-
conn.lastError = null;
|
|
862
|
-
conn.consecutiveInterruptions += 1;
|
|
863
|
-
conn.runtimeState = 'interrupted';
|
|
864
|
-
if (shouldEnterInterruptionCooldown(conn)) {
|
|
865
|
-
conn.interruptionCooldownUntil = Date.now() + INTERRUPTION_COOLDOWN_MS;
|
|
866
|
-
conn.runtimeState = 'cooldown';
|
|
867
|
-
}
|
|
868
|
-
addTaskSummary(conn, {
|
|
869
|
-
status: 'interrupted',
|
|
870
|
-
runId: flow.rpaRunId || conn.lastRunId || null,
|
|
871
|
-
summary: reason,
|
|
872
|
-
});
|
|
873
|
-
void reason;
|
|
874
|
-
}
|
|
875
|
-
function noteSuccessfulRun(conn) {
|
|
876
|
-
conn.consecutiveInterruptions = 0;
|
|
877
|
-
conn.interruptionCooldownUntil = undefined;
|
|
878
|
-
conn.runtimeState = 'idle_waiting';
|
|
879
|
-
}
|
|
880
|
-
function recordTraceRuntime(conn, flow) {
|
|
881
|
-
const tracePath = flow.postSendScreenshotPath || flow.screenshotPath;
|
|
882
|
-
if (flow.rpaRunId)
|
|
883
|
-
conn.lastRunId = flow.rpaRunId;
|
|
884
|
-
if (tracePath)
|
|
885
|
-
conn.lastTracePath = tracePath;
|
|
886
|
-
conn.lastTraceSummary = flow.rpaTraceSummary || [
|
|
887
|
-
flow.groupName ? `group=${flow.groupName}` : '',
|
|
888
|
-
flow.ok === false ? 'failed' : flow.interrupted ? 'interrupted' : 'ok',
|
|
889
|
-
flow.reason ? `reason=${flow.reason}` : '',
|
|
890
|
-
].filter(Boolean).join(' ');
|
|
891
|
-
}
|
|
892
|
-
function addTaskSummary(conn, summary) {
|
|
893
|
-
const runId = summary.runId || conn.lastRunId || `local:${Date.now().toString(36)}`;
|
|
894
|
-
conn.lastRunId = runId;
|
|
895
|
-
conn.recentTaskSummaries.unshift({
|
|
896
|
-
at: new Date().toISOString(),
|
|
897
|
-
status: summary.status,
|
|
898
|
-
runId,
|
|
899
|
-
summary: clipSummary(summary.summary),
|
|
900
|
-
});
|
|
901
|
-
conn.recentTaskSummaries = conn.recentTaskSummaries.slice(0, MAX_RECENT_TASK_SUMMARIES);
|
|
902
|
-
}
|
|
903
|
-
function classifyWeChatRpaFailure(message) {
|
|
904
|
-
const value = String(message || '').toLowerCase();
|
|
905
|
-
return /permission|accessibility|screen recording|automation|window|foreground|target group|refusing|requires|安全|权限|窗口|目标群/.test(value)
|
|
906
|
-
? 'blocked'
|
|
907
|
-
: 'failed';
|
|
908
|
-
}
|
|
909
|
-
function clipSummary(value) {
|
|
910
|
-
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 180);
|
|
911
|
-
}
|
|
912
|
-
function isInterruptionCooldownActive(conn, secret) {
|
|
913
|
-
if (secret.forceForeground)
|
|
914
|
-
return false;
|
|
915
|
-
const until = conn.interruptionCooldownUntil;
|
|
916
|
-
if (!until)
|
|
917
|
-
return false;
|
|
918
|
-
if (Date.now() < until)
|
|
919
|
-
return true;
|
|
920
|
-
conn.interruptionCooldownUntil = undefined;
|
|
921
|
-
conn.consecutiveInterruptions = 0;
|
|
922
|
-
return false;
|
|
923
|
-
}
|
|
924
|
-
function shouldEnterInterruptionCooldown(conn) {
|
|
925
|
-
if (!Number.isFinite(INTERRUPTION_COOLDOWN_THRESHOLD) || INTERRUPTION_COOLDOWN_THRESHOLD <= 0)
|
|
926
|
-
return false;
|
|
927
|
-
if (!Number.isFinite(INTERRUPTION_COOLDOWN_MS) || INTERRUPTION_COOLDOWN_MS <= 0)
|
|
928
|
-
return false;
|
|
929
|
-
return conn.consecutiveInterruptions >= INTERRUPTION_COOLDOWN_THRESHOLD;
|
|
930
|
-
}
|
|
931
|
-
function readFixtureMessages(filePath) {
|
|
932
|
-
if (!filePath || !fs.existsSync(filePath))
|
|
933
|
-
return [];
|
|
934
|
-
return fs.readFileSync(filePath, 'utf-8')
|
|
935
|
-
.split('\n')
|
|
936
|
-
.map((line) => line.trim())
|
|
937
|
-
.filter(Boolean)
|
|
938
|
-
.map((line) => {
|
|
939
|
-
try {
|
|
940
|
-
return JSON.parse(line);
|
|
941
|
-
}
|
|
942
|
-
catch {
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
})
|
|
946
|
-
.filter((item) => Boolean(item));
|
|
947
|
-
}
|
|
948
|
-
export async function materializeWeChatRpaOutboundAttachment(workDir, attachment) {
|
|
949
|
-
if (attachment.localPath) {
|
|
950
|
-
if (!fs.existsSync(attachment.localPath))
|
|
951
|
-
throw new Error(`WeChat RPA local attachment does not exist: ${attachment.localPath}`);
|
|
952
|
-
return attachment.localPath;
|
|
953
|
-
}
|
|
954
|
-
if (attachment.url) {
|
|
955
|
-
return downloadOutboundAttachment(workDir, attachment);
|
|
956
|
-
}
|
|
957
|
-
throw new Error('WeChat RPA attachment requires localPath or url; dataBase64 is not accepted over Manager IPC');
|
|
958
|
-
}
|
|
959
|
-
async function downloadOutboundAttachment(workDir, attachment) {
|
|
960
|
-
if (!attachment.url || !/^https?:\/\//i.test(attachment.url))
|
|
961
|
-
throw new Error('WeChat RPA attachment url must be http(s)');
|
|
962
|
-
if (Number.isFinite(attachment.size) && attachment.size > MAX_OUTBOUND_ATTACHMENT_BYTES) {
|
|
963
|
-
throw new Error(`WeChat RPA cross-machine attachment is too large: ${attachment.size} bytes. Max: ${MAX_OUTBOUND_ATTACHMENT_BYTES} bytes.`);
|
|
964
|
-
}
|
|
965
|
-
const response = await fetch(attachment.url);
|
|
966
|
-
if (!response.ok)
|
|
967
|
-
throw new Error(`WeChat RPA attachment url download failed: ${response.status}`);
|
|
968
|
-
const contentLength = Number(response.headers.get('content-length') || attachment.size || 0);
|
|
969
|
-
if (contentLength > MAX_OUTBOUND_ATTACHMENT_BYTES) {
|
|
970
|
-
throw new Error(`WeChat RPA cross-machine attachment is too large: ${contentLength} bytes. Max: ${MAX_OUTBOUND_ATTACHMENT_BYTES} bytes.`);
|
|
971
|
-
}
|
|
972
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
973
|
-
return writeOutboundAttachmentBuffer(workDir, attachment, buffer);
|
|
974
|
-
}
|
|
975
|
-
function writeOutboundAttachmentBuffer(workDir, attachment, buffer) {
|
|
976
|
-
if (!buffer.byteLength)
|
|
977
|
-
throw new Error('WeChat RPA attachment is empty');
|
|
978
|
-
if (buffer.byteLength > MAX_OUTBOUND_ATTACHMENT_BYTES) {
|
|
979
|
-
throw new Error(`WeChat RPA cross-machine attachment is too large: ${buffer.byteLength} bytes. Max: ${MAX_OUTBOUND_ATTACHMENT_BYTES} bytes.`);
|
|
980
|
-
}
|
|
981
|
-
const dir = path.join(workDir, '.uploads', 'wechat-rpa', 'outbound');
|
|
982
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
983
|
-
const safe = safeFileName(attachment.name || 'attachment');
|
|
984
|
-
const hash = crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 12);
|
|
985
|
-
const ext = path.extname(safe);
|
|
986
|
-
const stem = ext ? safe.slice(0, -ext.length) : safe;
|
|
987
|
-
const filePath = path.join(dir, `${stem}-${hash}${ext}`);
|
|
988
|
-
if (!fs.existsSync(filePath))
|
|
989
|
-
fs.writeFileSync(filePath, buffer);
|
|
990
|
-
return filePath;
|
|
991
|
-
}
|
|
992
|
-
function safeFileName(name) {
|
|
993
|
-
return path.basename(name || 'attachment')
|
|
994
|
-
.normalize('NFKC')
|
|
995
|
-
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
|
|
996
|
-
.replace(/\s+/g, ' ')
|
|
997
|
-
.replace(/^[ ._]+|[ ._]+$/g, '')
|
|
998
|
-
|| 'attachment';
|
|
999
|
-
}
|
|
1000
|
-
export function resolveWeChatRpaInboundAttachmentDir(config, secret) {
|
|
1001
|
-
if (secret.downloadAttachments === false)
|
|
1002
|
-
return undefined;
|
|
1003
|
-
const explicitDir = secret.downloadAttachmentsDir?.trim();
|
|
1004
|
-
if (explicitDir)
|
|
1005
|
-
return path.resolve(explicitDir);
|
|
1006
|
-
const channelKey = crypto.createHash('sha256').update(config.id).digest('hex').slice(0, 16);
|
|
1007
|
-
return resolveShennianPath('wechat-rpa', 'attachments', 'inbound', channelKey);
|
|
1008
|
-
}
|
|
1009
|
-
export function annotateWeChatRpaInboundAttachments(attachments) {
|
|
1010
|
-
if (!Array.isArray(attachments))
|
|
1011
|
-
return [];
|
|
1012
|
-
const machineId = process.env.SHENNIAN_MACHINE_ID?.trim();
|
|
1013
|
-
return attachments.map((attachment) => {
|
|
1014
|
-
if (!attachment?.localPath)
|
|
1015
|
-
return attachment;
|
|
1016
|
-
return {
|
|
1017
|
-
...attachment,
|
|
1018
|
-
availability: attachment.availability || 'edge-local',
|
|
1019
|
-
...(attachment.machineId || !machineId ? {} : { machineId }),
|
|
1020
|
-
};
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
function clampNumber(value, fallback, min, max) {
|
|
1024
|
-
const number = Number(value);
|
|
1025
|
-
if (!Number.isFinite(number))
|
|
1026
|
-
return fallback;
|
|
1027
|
-
return Math.min(max, Math.max(min, number));
|
|
1028
|
-
}
|
|
1
|
+
import S from"node:crypto";import h from"node:fs";import p from"node:path";import{fileURLToPath as X,pathToFileURL as G}from"node:url";import{ChannelSecretRegistry as V}from"./secret-registry.js";import{resolveShennianPath as Y}from"../config/index.js";import{probeMacWeChat as O,observedMessageFromProbe as Q}from"./wechat-rpa/macos.js";import{runMacWeChatRpaFlow as $}from"./wechat-rpa/macos-flow.js";import{normalizeWeChatRpaMessage as Z,WeChatRpaDeduper as L,weChatRpaConversationId as I}from"./wechat-rpa/normalizer.js";const ee=5e3,P=5,te=8,R=Number(process.env.SHENNIAN_WECHAT_RPA_OUTBOUND_ATTACHMENT_MAX_BYTES||20*1024*1024),A=Number(process.env.SHENNIAN_WECHAT_RPA_INTERRUPTION_COOLDOWN_THRESHOLD||3),k=Number(process.env.SHENNIAN_WECHAT_RPA_INTERRUPTION_COOLDOWN_MS||300*1e3),re=10;function ne(t){return v(t,P,1,50)>=te?"scroll-read":"read-latest"}class Be{onMessage;type="wechat-rpa";secrets=new V;connections=new Map;constructor(e){this.onMessage=e}async connect(e){if(!e.enabled)return;const r=this.readSecret(e),n=this.ensureConnection(e);n.stopped=!1,n.config=e,C(n,e),x(n,e),this.seedConfiguredConversations(n,r),!n.timer&&(await this.enqueueOperation(n,()=>this.pollOnce(n,this.readSecret(n.config))),n.timer=setInterval(()=>{this.enqueueOperation(n,()=>this.pollOnce(n,this.readSecret(n.config))).catch(()=>{})},ae(r.pollIntervalMs)),n.timer.unref())}async disconnect(e){const r=this.connections.get(e.id);r&&(r.stopped=!0,r.timer&&clearInterval(r.timer),this.connections.delete(e.id))}async syncNow(e){if(!e.enabled)throw new Error("WeChat RPA channel is disabled");const r=this.readSecret(e),n=this.ensureConnection(e);return n.stopped=!1,n.config=e,C(n,e),x(n,e),this.seedConfiguredConversations(n,r),this.enqueueOperation(n,()=>this.pollOnce(n,this.readSecret(n.config)))}async send(e,r){const n=this.readSecret(e);if(n.canReply===!1)throw new Error("WeChat RPA channel does not allow replies");if(!ge(n.source))throw new Error("WeChat RPA reply requires source=macos-flow, windows-visual-flow, or wechat-rpa-lab");const a=this.ensureConnection(e);a.config=e,C(a,e),x(a,e),this.seedConfiguredConversations(a,n);const o=this.resolveConversationName(e,n,r.conversationId);if(!o)throw new Error(`Unknown WeChat RPA conversation: ${r.conversationId}`);const i=we(e,r);if(a.completedPendingReplyKeys.has(i))return{status:"sent"};const u=a.manualReviewReplies.get(i);if(u)return{status:"manual-review",reason:u.reason};const s=r.attachment?await Ce(e.workDir,r.attachment):void 0,m=r.text.trim();if(!m&&!s)throw new Error("Reply text or attachment is required");if(J(a,n)){const d="WeChat RPA is cooling down after repeated user activity";return a.runtimeState="cooldown",b(a,{key:i,conversationId:r.conversationId,conversationName:o,text:m,attachmentPath:s,reason:d}),{status:"queued",reason:d}}let c;try{a.runtimeState="syncing",c=await this.enqueueOperation(a,()=>(a.lastRunAt=new Date().toISOString(),H(e,n,o,m,s,i)))}catch(d){throw a.lastError=d instanceof Error?d.message:String(d),y(a,{status:j(a.lastError),runId:a.lastRunId??null,summary:a.lastError}),d}E(a,c);const l=K(c,{text:m,attachmentPath:s});if(l.status==="queued"){const d=l.reason;return b(a,{key:i,conversationId:r.conversationId,conversationName:o,text:l.text,attachmentPath:l.attachmentPath,reason:d,skipText:l.skipText}),a.lastError=d,{status:"queued",reason:d}}if(c.interrupted){const d=c.error||"WeChat RPA send was interrupted by user activity";return N(a,c,d),b(a,{key:i,conversationId:r.conversationId,conversationName:o,text:l.text,attachmentPath:s,reason:d,skipText:l.skipText}),{status:"queued",reason:d}}if(!c.ok){const d=c.error||"WeChat RPA send validator failed; manual review required before retry";return U(a,i,d),{status:"manual-review",reason:d}}return y(a,{status:"sent",runId:a.lastRunId??null,summary:c.rpaTraceSummary||`sent ${o}`}),T(a),a.lastError=null,{status:"sent"}}async health(e){const r=this.readSecret(e);if((r.source??"macos-probe")==="fixture-jsonl")return r.fixturePath&&h.existsSync(r.fixturePath)?{ok:!0}:{ok:!1,message:"WeChat RPA fixture file is missing"};if(r.source==="macos-flow"){if(process.platform!=="darwin")return{ok:!1,message:"WeChat RPA macOS flow requires macOS"};if(!g(r).length)return{ok:!1,message:"WeChat RPA macOS flow requires at least one group"}}if(r.source==="wechat-rpa-lab"){if(process.platform!=="darwin")return{ok:!1,message:"WeChat RPA Lab source requires macOS for live runs"};if(!g(r).length)return{ok:!1,message:"WeChat RPA Lab source requires at least one group"};try{q(e.workDir)}catch(a){return{ok:!1,message:a instanceof Error?a.message:String(a)}}return{ok:!0,message:"WeChat RPA Lab source configured"}}if(r.source==="windows-visual-flow")return ie(r,process.platform);const n=await O();return n.ok?{ok:!0,message:n.wechatRunning?"WeChat detected":"WeChat is not running"}:{ok:!1,message:n.message}}async defaultConversation(e){const r=this.readSecret(e),n=g(r)[0];if(!n)throw new Error("WeChat RPA channel has no configured groups");return{conversationId:I(n),conversationName:n}}runtimeStatus(e){const r=this.connections.get(e.id);return r?{wechatRpaRuntimeState:r.runtimeState,wechatRpaLastRunAt:r.lastRunAt??null,wechatRpaLastMessageAt:r.lastMessageAt??null,wechatRpaLastInterruptedAt:r.lastInterruptedAt??null,wechatRpaPendingReplyCount:r.pendingReplies.size,wechatRpaLastError:r.lastError??null,wechatRpaLastRunId:r.lastRunId??null,wechatRpaLastTracePath:r.lastTracePath??null,wechatRpaLastTraceSummary:r.lastTraceSummary??null,wechatRpaRecentTaskSummaries:r.recentTaskSummaries}:{}}ensureConnection(e){let r=this.connections.get(e.id);return r||(r={config:e,timer:null,deduper:new L,stopped:!1,conversations:new Map,pendingReplies:new Map,manualReviewReplies:new Map,completedPendingReplyKeys:new Set,pendingStatePath:void 0,messageStatePath:void 0,operation:Promise.resolve(),recentTaskSummaries:[],runtimeState:"idle_waiting",consecutiveInterruptions:0},this.connections.set(e.id,r)),r}enqueueOperation(e,r){const n=e.operation.then(r,r);return e.operation=n.then(()=>{},()=>{}),n}readSecret(e){const r=this.secrets.get(e.secretRef);if(!r||r.type!=="wechat-rpa")throw new Error("WeChat RPA channel is not configured on this daemon");return r}async pollOnce(e,r){if(e.stopped)return[];const n=[];e.lastRunAt=new Date().toISOString();try{if(J(e,r))return e.runtimeState="cooldown",[];if(await this.drainPendingReplies(e,r))return[];let o=!1;e.runtimeState="syncing";const i=await this.readObservedMessages(e.config,r,u=>{E(e,u),u.interrupted&&(o=!0,N(e,u,u.error||"WeChat RPA poll was interrupted by user activity"))});e.lastError=null;for(const u of i){const s=Z(u,{selfNicknames:_(r)});if(!s||!e.deduper.accept(s.messageId))continue;Pe(e),e.conversations.set(s.conversationId,s.conversationName),e.lastMessageAt=s.receivedAt;const m={type:"external.message",managerSessionId:e.config.managerSessionId,channelId:e.config.id,channelType:"wechat-rpa",conversationId:s.conversationId,conversationName:s.conversationName,messageId:s.messageId,sender:s.sender,text:s.text,attachments:s.attachments,receivedAt:s.receivedAt,isMentioned:s.isMentioned,replyTarget:"",rawRef:s.rawRef};n.push(this.onMessage?.(m)??m),y(e,{status:"received",runId:e.lastRunId??null,summary:`received ${s.conversationName}${s.sender.name?` from ${s.sender.name}`:""}: ${z(s.text||s.attachments[0]?.name||"attachment")}`})}return o||T(e),n}catch(a){throw e.lastError=a instanceof Error?a.message:String(a),y(e,{status:j(e.lastError),runId:e.lastRunId??null,summary:e.lastError}),a}}async drainPendingReplies(e,r){if(!e.pendingReplies.size)return!1;for(const n of Array.from(e.pendingReplies.values())){e.runtimeState="retrying",n.attempts+=1,n.lastAttemptAt=new Date().toISOString(),w(e);const a=await H(e.config,r,n.conversationName,n.skipText?"":n.text,n.attachmentPath,n.key);E(e,a);const o=K(a,{text:n.skipText?"":n.text,attachmentPath:n.attachmentPath});if(o.status==="queued")return n.text=o.text,n.attachmentPath=o.attachmentPath,n.skipText=o.skipText,n.lastInterruptedReason=o.reason,e.lastError=o.reason,w(e),!0;if(a.interrupted){const i=a.error||"WeChat RPA pending send was interrupted by user activity";return n.text=o.text,n.attachmentPath=o.attachmentPath,n.skipText=o.skipText,n.lastInterruptedReason=i,N(e,a,i),w(e),!0}if(!a.ok){const i=a.error||"WeChat RPA pending send validator failed; manual review required before retry";return e.pendingReplies.delete(n.key),U(e,n.key,i),w(e),!0}e.pendingReplies.delete(n.key),e.completedPendingReplyKeys.add(n.key),w(e),e.lastError=null,y(e,{status:"sent",runId:e.lastRunId??null,summary:a.rpaTraceSummary||`sent ${n.conversationName}`}),T(e)}return!1}async readObservedMessages(e,r,n){if(r.source==="fixture-jsonl")return be(r.fixturePath);if(r.source==="macos-flow")return me(e,r,n);if(r.source==="wechat-rpa-lab")return oe(e,r,n);if(r.source==="windows-visual-flow")return fe(e,r,n);const a=await O(),o=Q(a);return o?[o]:[]}seedConfiguredConversations(e,r){for(const n of g(r))e.conversations.set(I(n),n)}resolveConversationName(e,r,n){const o=this.connections.get(e.id)?.conversations.get(n);return o||(g(r).find(i=>I(i)===n)??null)}}function ae(t){return Number.isFinite(t)?Math.min(6e4,Math.max(1e3,Number(t))):ee}function g(t){return Array.isArray(t.groups)?t.groups.map(e=>String(e?.name||"").trim()).filter(Boolean):[]}function _(t){return String(t.selfNickname||"").split(/[,\n,、]/).map(e=>e.trim()).filter(Boolean)}function se(t,e){return e.length?e.some(r=>{const n=r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`@\\s*${n}(?=$|\\s|[\uFF0C\u3002\uFF01\uFF1F,.!?:\uFF1A\uFF1B;\u3001)])`).test(t)}):!1}function ie(t,e=process.platform){return e!=="win32"?{ok:!1,message:"WeChat RPA Windows visual flow requires Windows"}:{ok:!1,message:"WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it"}}async function oe(t,e,r){const n=await D(t.workDir),a=[];for(const o of g(e)){const i=await ue(n,t,e,o);if(r?.(i),!(!i.ok||i.interrupted))for(const u of i.newMessages??[]){const s=B(o,u,e);s&&a.push(s)}}return a}async function ue(t,e,r,n){const a=ne(r.recentLimit),o={kind:a,requestId:`wechat-rpa:${n}:read:${Date.now()}`,targetGroup:n,policy:r.forceForeground?"work":"polite",limit:v(r.recentLimit,P,1,50),attachmentsDir:M(e,r)},i=await t.runWechatRpaReadLatest(o),u=Array.isArray(i.data?.structuredMessages)?i.data.structuredMessages:[],s=i.validation?.deterministic?.latestMessages??[],m=u.length?u.map((c,l)=>de(c,i.runId||"lab-read",l)):s.map((c,l)=>({id:`${i.runId||"lab-read"}:${l}`,text:c,confidence:.8}));return{ok:!!i.ok,groupName:n,interrupted:i.status==="interrupted",reason:i.status,rpaRunId:i.runId,rpaTraceSummary:`${a} ${i.status||(i.ok?"success":"failed")} ${n}`,recentMessages:m,newMessages:m,screenshotPath:i.tracePath,error:F(i)}}function de(t,e,r){return{id:`${e}:${t.index??r}`,text:String(t.text||t.card?.title||"").trim(),confidence:typeof t.confidence=="number"?t.confidence:.75,senderName:typeof t.senderName=="string"?t.senderName:null,attachments:ce(t)}}function ce(t){const e=String(t.kind||"");if(!["image","file","video-file","video-card","link-card","official-account-card","mini-program-card"].includes(e))return[];const r=typeof t.localPath=="string"?t.localPath:void 0;return[{type:e==="video-file"||e==="video-card"?"video":e==="link-card"||e==="official-account-card"||e==="mini-program-card"?"file":e,localPath:r,availability:r?"edge-local":"metadata-only"}]}async function le(t,e,r,n,a,o){const i=await D(t.workDir),u=pe({groupName:r,text:n,attachmentPath:a,dedupeKey:o,requestId:o,policy:"work"}),s=[];for(const f of u){const W=await i.runWechatRpaTask(f);if(s.push(W),!W.ok)break}const m=s.find(f=>f.kind==="send-text"),c=s.find(f=>f.kind==="send-image"||f.kind==="send-video"||f.kind==="send-file"),l=u.length>0&&s.length===u.length&&s.every(f=>f.ok),d=s.some(f=>f.status==="interrupted");return{ok:l,groupName:r,interrupted:d,reason:s.at(-1)?.status,rpaRunId:s.at(-1)?.runId,rpaTraceSummary:`send ${l?"success":s.at(-1)?.status||"failed"} ${r} (${s.length}/${u.length})`,sentReply:!!n,sentReplyObserved:n?!!m?.ok:!1,sentAttachment:!!a,sentAttachmentObserved:a?!!c?.ok:!1,postSendScreenshotPath:s.at(-1)?.tracePath,error:l?void 0:F(s.at(-1))}}function pe(t){const e=[],r=String(t.text||"").trim(),n=t.policy||"work";if(r&&e.push({kind:"send-text",requestId:t.requestId?`${t.requestId}:text`:void 0,targetGroup:t.groupName,policy:n,text:r,...t.dedupeKey?{dedupeKey:`${t.dedupeKey}:text`}:{}}),t.attachmentPath){const a=he(t.attachmentPath);e.push({kind:a,requestId:t.requestId?`${t.requestId}:attachment`:void 0,targetGroup:t.groupName,policy:n,filePath:t.attachmentPath,...t.dedupeKey?{dedupeKey:`${t.dedupeKey}:attachment`}:{}})}return e}function he(t){const e=p.extname(t).toLowerCase();return[".png",".jpg",".jpeg",".gif",".webp",".heic"].includes(e)?"send-image":[".mp4",".mov",".avi",".mkv"].includes(e)?"send-video":"send-file"}async function D(t){const e=q(t);return await import(G(e).href)}function q(t){const e=p.dirname(X(import.meta.url)),n=[process.env.SHENNIAN_WECHAT_RPA_LAB_INDEX,p.resolve(e,"../../../../scripts/wechat-rpa-lab/index.mjs"),t?p.join(t,"scripts/wechat-rpa-lab/index.mjs"):"",p.resolve(process.cwd(),"scripts/wechat-rpa-lab/index.mjs")].filter(Boolean).find(a=>h.existsSync(a));if(!n)throw new Error("WeChat RPA Lab API is missing; set SHENNIAN_WECHAT_RPA_LAB_INDEX or run from the repository root");return p.resolve(n)}function F(t){return t?typeof t.error=="string"?t.error:t.error?.message||(t.ok?void 0:`WeChat RPA Lab task ${t.kind||"<unknown>"} ${t.status||"failed"}`):"WeChat RPA Lab did not return a result"}async function me(t,e,r){const n=[];for(const a of g(e)){const o=await $({groupName:a,scriptPath:e.flowScriptPath,workDir:t.workDir,forceForeground:!!e.forceForeground,noRestore:e.noRestore!==!1,idleSeconds:v(e.idleSeconds,15,0,3600),recentLimit:v(e.recentLimit,P,0,50),downloadAttachmentsDir:M(t,e)});if(r?.(o),!o.interrupted)for(const i of o.newMessages??[]){const u=B(a,i,e);u&&n.push(u)}}return n}async function fe(t,e,r){const n=g(e)[0]||"<unbound>";return r?.({ok:!1,groupName:n,interrupted:!0,reason:"windows-visual-flow-archived",newMessages:[],recentMessages:[],error:"WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it"}),[]}function B(t,e,r){const n=String(e.text||"").trim(),a=Array.isArray(e.attachments)?e.attachments:[];if(!n&&a.length===0)return null;const o=typeof e.senderName=="string"?e.senderName.trim():"",i=String(e.observedAt||e.timestampIso||"").trim()||new Date().toISOString();return{conversationName:t,senderName:o||null,text:n,attachments:Ee(a),observedAt:i,isMentioned:e.isMentioned===!0||se(n,_(r)),rawId:String(e.id||`${t}:${o}:${n}:${i}`)}}async function H(t,e,r,n,a,o){return e.source==="windows-visual-flow"?{ok:!1,groupName:r,interrupted:!0,reason:"windows-visual-flow-archived",newMessages:[],recentMessages:[],error:"WeChat RPA Windows visual flow is archived; redesign Windows support before enabling it"}:e.source==="wechat-rpa-lab"?le(t,e,r,n,a,o):$({groupName:r,replyText:n||void 0,attachmentPath:a,scriptPath:e.flowScriptPath,workDir:t.workDir,forceForeground:!!e.forceForeground,noRestore:e.noRestore!==!1,idleSeconds:v(e.idleSeconds,15,0,3600),recentLimit:v(e.recentLimit,P,0,50),downloadAttachmentsDir:M(t,e)})}function ge(t){return t==="macos-flow"||t==="wechat-rpa-lab"}function K(t,e){return e.text&&e.attachmentPath&&t.sentReplyObserved&&!t.sentAttachmentObserved?{status:t.interrupted?"continue":"queued",text:"",attachmentPath:e.attachmentPath,skipText:!0,reason:t.error||"WeChat RPA sent the text but did not confirm the attachment; queued attachment-only retry"}:{status:"continue",text:e.text,attachmentPath:e.attachmentPath,reason:t.error||""}}function we(t,e){return e.idempotencyKey?e.idempotencyKey:S.createHash("sha256").update(`${t.id}
|
|
2
|
+
${e.conversationId}
|
|
3
|
+
${e.text}
|
|
4
|
+
${e.attachment?.name||""}
|
|
5
|
+
${e.attachment?.size||0}`).digest("hex").slice(0,24)}function b(t,e){const r=t.pendingReplies.get(e.key);t.pendingReplies.set(e.key,{key:e.key,conversationId:e.conversationId,conversationName:e.conversationName,text:e.text,attachmentPath:e.attachmentPath,skipText:e.skipText,queuedAt:r?.queuedAt??new Date().toISOString(),attempts:r?.attempts??0,lastAttemptAt:r?.lastAttemptAt,lastInterruptedReason:e.reason}),w(t)}function U(t,e,r){t.manualReviewReplies.set(e,{key:e,reason:r,createdAt:t.manualReviewReplies.get(e)?.createdAt??new Date().toISOString()}),t.lastError=r,y(t,{status:"failed",runId:t.lastRunId??null,summary:`manual review required: ${r}`}),w(t)}function C(t,e){const r=Se(e);if(t.pendingStatePath===r)return;t.pendingStatePath=r;const n=ye(r);for(const a of n.pending??[])Re(a)&&(t.pendingReplies.has(a.key)||t.pendingReplies.set(a.key,a));for(const a of n.manualReview??[])ve(a)&&(t.manualReviewReplies.has(a.key)||t.manualReviewReplies.set(a.key,a));for(const a of n.completedKeys??[])typeof a=="string"&&a&&t.completedPendingReplyKeys.add(a)}function w(t){if(!t.pendingStatePath)return;const e=Array.from(t.completedPendingReplyKeys).slice(-500),r=Array.from(t.pendingReplies.values()).slice(0,500),n=Array.from(t.manualReviewReplies.values()).slice(-500);try{h.mkdirSync(p.dirname(t.pendingStatePath),{recursive:!0}),h.writeFileSync(t.pendingStatePath,JSON.stringify({version:1,pending:r,manualReview:n,completedKeys:e},null,2))}catch{}}function ye(t){try{const e=JSON.parse(h.readFileSync(t,"utf8"));return e&&e.version===1?e:{version:1,pending:[],completedKeys:[]}}catch{return{version:1,pending:[],completedKeys:[]}}}function Re(t){if(!t||typeof t!="object")return!1;const e=t;return typeof e.key=="string"&&typeof e.conversationId=="string"&&typeof e.conversationName=="string"&&typeof e.text=="string"&&typeof e.queuedAt=="string"&&typeof e.attempts=="number"&&Number.isFinite(e.attempts)&&(e.attachmentPath===void 0||typeof e.attachmentPath=="string")&&(e.skipText===void 0||typeof e.skipText=="boolean")}function ve(t){if(!t||typeof t!="object")return!1;const e=t;return typeof e.key=="string"&&typeof e.reason=="string"&&typeof e.createdAt=="string"}function Se(t){const e=S.createHash("sha256").update(t.id).digest("hex").slice(0,16);return p.join(t.workDir,".shennian","wechat-rpa-pending-replies",`${e}.json`)}function x(t,e){const r=Ae(e);if(t.messageStatePath===r)return;t.messageStatePath=r;const n=Ie(r);t.deduper=new L((n.messageIds??[]).filter(a=>typeof a=="string"&&a.length>0))}function Pe(t){if(t.messageStatePath)try{h.mkdirSync(p.dirname(t.messageStatePath),{recursive:!0}),h.writeFileSync(t.messageStatePath,JSON.stringify({version:1,messageIds:t.deduper.snapshot()},null,2))}catch{}}function Ie(t){try{const e=JSON.parse(h.readFileSync(t,"utf8"));return e&&e.version===1?e:{version:1,messageIds:[]}}catch{return{version:1,messageIds:[]}}}function Ae(t){const e=S.createHash("sha256").update(t.id).digest("hex").slice(0,16);return p.join(t.workDir,".shennian","wechat-rpa-seen-messages",`${e}.json`)}function N(t,e,r){t.lastInterruptedAt=new Date().toISOString(),t.lastError=null,t.consecutiveInterruptions+=1,t.runtimeState="interrupted",ke(t)&&(t.interruptionCooldownUntil=Date.now()+k,t.runtimeState="cooldown"),y(t,{status:"interrupted",runId:e.rpaRunId||t.lastRunId||null,summary:r})}function T(t){t.consecutiveInterruptions=0,t.interruptionCooldownUntil=void 0,t.runtimeState="idle_waiting"}function E(t,e){const r=e.postSendScreenshotPath||e.screenshotPath;e.rpaRunId&&(t.lastRunId=e.rpaRunId),r&&(t.lastTracePath=r),t.lastTraceSummary=e.rpaTraceSummary||[e.groupName?`group=${e.groupName}`:"",e.ok===!1?"failed":e.interrupted?"interrupted":"ok",e.reason?`reason=${e.reason}`:""].filter(Boolean).join(" ")}function y(t,e){const r=e.runId||t.lastRunId||`local:${Date.now().toString(36)}`;t.lastRunId=r,t.recentTaskSummaries.unshift({at:new Date().toISOString(),status:e.status,runId:r,summary:z(e.summary)}),t.recentTaskSummaries=t.recentTaskSummaries.slice(0,re)}function j(t){const e=String(t||"").toLowerCase();return/permission|accessibility|screen recording|automation|window|foreground|target group|refusing|requires|安全|权限|窗口|目标群/.test(e)?"blocked":"failed"}function z(t){return String(t||"").replace(/\s+/g," ").trim().slice(0,180)}function J(t,e){if(e.forceForeground)return!1;const r=t.interruptionCooldownUntil;return r?Date.now()<r?!0:(t.interruptionCooldownUntil=void 0,t.consecutiveInterruptions=0,!1):!1}function ke(t){return!Number.isFinite(A)||A<=0||!Number.isFinite(k)||k<=0?!1:t.consecutiveInterruptions>=A}function be(t){return!t||!h.existsSync(t)?[]:h.readFileSync(t,"utf-8").split(`
|
|
6
|
+
`).map(e=>e.trim()).filter(Boolean).map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(e=>!!e)}async function Ce(t,e){if(e.localPath){if(!h.existsSync(e.localPath))throw new Error(`WeChat RPA local attachment does not exist: ${e.localPath}`);return e.localPath}if(e.url)return xe(t,e);throw new Error("WeChat RPA attachment requires localPath or url; dataBase64 is not accepted over Manager IPC")}async function xe(t,e){if(!e.url||!/^https?:\/\//i.test(e.url))throw new Error("WeChat RPA attachment url must be http(s)");if(Number.isFinite(e.size)&&e.size>R)throw new Error(`WeChat RPA cross-machine attachment is too large: ${e.size} bytes. Max: ${R} bytes.`);const r=await fetch(e.url);if(!r.ok)throw new Error(`WeChat RPA attachment url download failed: ${r.status}`);const n=Number(r.headers.get("content-length")||e.size||0);if(n>R)throw new Error(`WeChat RPA cross-machine attachment is too large: ${n} bytes. Max: ${R} bytes.`);const a=Buffer.from(await r.arrayBuffer());return Ne(t,e,a)}function Ne(t,e,r){if(!r.byteLength)throw new Error("WeChat RPA attachment is empty");if(r.byteLength>R)throw new Error(`WeChat RPA cross-machine attachment is too large: ${r.byteLength} bytes. Max: ${R} bytes.`);const n=p.join(t,".uploads","wechat-rpa","outbound");h.mkdirSync(n,{recursive:!0});const a=Te(e.name||"attachment"),o=S.createHash("sha256").update(r).digest("hex").slice(0,12),i=p.extname(a),u=i?a.slice(0,-i.length):a,s=p.join(n,`${u}-${o}${i}`);return h.existsSync(s)||h.writeFileSync(s,r),s}function Te(t){return p.basename(t||"attachment").normalize("NFKC").replace(/[<>:"/\\|?*\x00-\x1F]/g,"_").replace(/\s+/g," ").replace(/^[ ._]+|[ ._]+$/g,"")||"attachment"}function M(t,e){if(e.downloadAttachments===!1)return;const r=e.downloadAttachmentsDir?.trim();if(r)return p.resolve(r);const n=S.createHash("sha256").update(t.id).digest("hex").slice(0,16);return Y("wechat-rpa","attachments","inbound",n)}function Ee(t){if(!Array.isArray(t))return[];const e=process.env.SHENNIAN_MACHINE_ID?.trim();return t.map(r=>r?.localPath?{...r,availability:r.availability||"edge-local",...r.machineId||!e?{}:{machineId:e}}:r)}function v(t,e,r,n){const a=Number(t);return Number.isFinite(a)?Math.min(n,Math.max(r,a)):e}export{Be as WeChatRpaChannelAdapter,Ee as annotateWeChatRpaInboundAttachments,Ce as materializeWeChatRpaOutboundAttachment,pe as planWeChatRpaLabSendTasks,M as resolveWeChatRpaInboundAttachmentDir,ne as selectWeChatRpaLabReadKind,he as weChatRpaLabTaskKindForAttachment,ie as windowsVisualFlowHealth};
|