shennian 0.2.74 → 0.2.76
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/scripts/wechat-rpa-confirmation.mjs +97 -0
- package/dist/scripts/wechat-rpa-download-candidates.mjs +84 -0
- package/dist/scripts/wechat-rpa-win-visual.mjs +668 -0
- package/dist/scripts/wechat-rpa-win.mjs +119 -0
- package/dist/src/agents/command-spec.js +24 -2
- package/dist/src/agents/external-channel-instructions.js +2 -0
- package/dist/src/channels/base.d.ts +1 -0
- package/dist/src/channels/runtime.d.ts +9 -2
- package/dist/src/channels/runtime.js +56 -6
- package/dist/src/channels/secret-registry.d.ts +1 -1
- package/dist/src/channels/wechat-rpa/macos-flow.d.ts +5 -0
- package/dist/src/channels/wechat-rpa/macos-flow.js +50 -2
- package/dist/src/channels/wechat-rpa/windows-visual-flow.d.ts +36 -0
- package/dist/src/channels/wechat-rpa/windows-visual-flow.js +182 -0
- package/dist/src/channels/wechat-rpa.d.ts +13 -16
- package/dist/src/channels/wechat-rpa.js +149 -14
- package/dist/src/commands/external-attachments.js +1 -1
- package/dist/src/commands/external.js +10 -1
- package/dist/src/commands/manager.js +18 -0
- package/dist/src/manager/runtime.js +23 -6
- package/dist/src/session/handlers/chat.js +12 -7
- package/dist/src/session/queue.js +2 -0
- package/dist/src/session/types.d.ts +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const defaultHelper = path.join(
|
|
10
|
+
repoRoot,
|
|
11
|
+
'packages/desktop/assets/native/wechat-rpa-win/win-x64/shennian-wechat-rpa-win.exe',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
function takeOption(argv, name) {
|
|
15
|
+
const index = argv.indexOf(name)
|
|
16
|
+
if (index < 0) return null
|
|
17
|
+
const value = argv[index + 1]
|
|
18
|
+
if (!value || value.startsWith('--')) {
|
|
19
|
+
throw new Error(`Missing value for ${name}`)
|
|
20
|
+
}
|
|
21
|
+
argv.splice(index, 2)
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printHelp() {
|
|
26
|
+
console.log(`Usage:
|
|
27
|
+
node scripts/wechat-rpa-win.mjs probe
|
|
28
|
+
node scripts/wechat-rpa-win.mjs probe --raise
|
|
29
|
+
node scripts/wechat-rpa-win.mjs open --group <name>
|
|
30
|
+
node scripts/wechat-rpa-win.mjs read-recent --group <name> --limit 5
|
|
31
|
+
node scripts/wechat-rpa-win.mjs send-text --group <name> --text <message>
|
|
32
|
+
node scripts/wechat-rpa-win.mjs send-files --group <name> --file <path> [--file <path> ...]
|
|
33
|
+
node scripts/wechat-rpa-win.mjs listen --group <name> --seconds 60 --poll-ms 1000
|
|
34
|
+
node scripts/wechat-rpa-win.mjs prepare-uia
|
|
35
|
+
node scripts/wechat-rpa-win.mjs capture --region window --output C:\\tmp\\wechat.png
|
|
36
|
+
node scripts/wechat-rpa-win.mjs click --x 480 --y 470 --no-raise
|
|
37
|
+
node scripts/wechat-rpa-win.mjs paste-text --text "hello"
|
|
38
|
+
node scripts/wechat-rpa-win.mjs paste-files --file C:\\tmp\\demo.png
|
|
39
|
+
node scripts/wechat-rpa-win.mjs press --keys "%s"
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--helper <path> Override helper exe path.
|
|
43
|
+
|
|
44
|
+
Environment:
|
|
45
|
+
WECHAT_RPA_WIN_HELPER Override helper exe path.
|
|
46
|
+
|
|
47
|
+
Diagnostic note:
|
|
48
|
+
probe does not raise WeChat by default. Use probe --raise only for diagnostics.
|
|
49
|
+
restart-wechat-for-uia is intentionally hidden from normal help because it closes
|
|
50
|
+
WeChat. It is guarded in the native helper and must never be used by product flows.`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const argv = process.argv.slice(2)
|
|
55
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
56
|
+
printHelp()
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (process.platform !== 'win32' && !argv.includes('--allow-non-windows')) {
|
|
61
|
+
throw new Error(`wechat-rpa-win only runs on Windows. Current platform: ${os.platform()}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const allowIndex = argv.indexOf('--allow-non-windows')
|
|
65
|
+
if (allowIndex >= 0) argv.splice(allowIndex, 1)
|
|
66
|
+
|
|
67
|
+
const helper = takeOption(argv, '--helper') ?? process.env.WECHAT_RPA_WIN_HELPER ?? defaultHelper
|
|
68
|
+
if (!fs.existsSync(helper)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Windows helper not found: ${helper}\n` +
|
|
71
|
+
'Build it on Windows with: pnpm --dir packages/desktop build:wechat-rpa-win',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const child = spawn(helper, argv, {
|
|
76
|
+
cwd: repoRoot,
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
let stdout = ''
|
|
82
|
+
let stderr = ''
|
|
83
|
+
child.stdout.setEncoding('utf8')
|
|
84
|
+
child.stderr.setEncoding('utf8')
|
|
85
|
+
child.stdout.on('data', chunk => {
|
|
86
|
+
stdout += chunk
|
|
87
|
+
})
|
|
88
|
+
child.stderr.on('data', chunk => {
|
|
89
|
+
stderr += chunk
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
93
|
+
child.on('error', reject)
|
|
94
|
+
child.on('close', resolve)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if (stderr.trim()) {
|
|
98
|
+
process.stderr.write(stderr)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const trimmed = stdout.trim()
|
|
102
|
+
if (trimmed) {
|
|
103
|
+
try {
|
|
104
|
+
const json = JSON.parse(trimmed)
|
|
105
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}\n`)
|
|
106
|
+
} catch {
|
|
107
|
+
process.stdout.write(stdout)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (exitCode !== 0) {
|
|
112
|
+
process.exit(exitCode ?? 1)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main().catch(error => {
|
|
117
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
118
|
+
process.exit(1)
|
|
119
|
+
})
|
|
@@ -63,6 +63,7 @@ function getFallbackCommandCandidates(command) {
|
|
|
63
63
|
const dirs = [
|
|
64
64
|
winPath.join(home, '.shennian', 'node'),
|
|
65
65
|
winPath.join('C:\\', 'nvm4w', 'nodejs'),
|
|
66
|
+
winPath.join(localAppData, 'npm-global'),
|
|
66
67
|
winPath.join(appData, 'npm'),
|
|
67
68
|
winPath.join(localAppData, 'pnpm'),
|
|
68
69
|
winPath.join(home, 'scoop', 'shims'),
|
|
@@ -85,12 +86,12 @@ function buildFallbackPathEnv(currentPath, command) {
|
|
|
85
86
|
const parts = currentPath?.split(path.delimiter).filter(Boolean) ?? [];
|
|
86
87
|
if (getProcessPlatform() === 'win32') {
|
|
87
88
|
if (command && path.basename(command) !== command) {
|
|
88
|
-
const commandDir = path.dirname(command);
|
|
89
|
+
const commandDir = path.win32.dirname(command);
|
|
89
90
|
if (!parts.includes(commandDir))
|
|
90
91
|
parts.unshift(commandDir);
|
|
91
92
|
}
|
|
92
93
|
for (const candidate of getFallbackCommandCandidates('shennian')) {
|
|
93
|
-
const dir = path.dirname(candidate);
|
|
94
|
+
const dir = path.win32.dirname(candidate);
|
|
94
95
|
if (!parts.includes(dir))
|
|
95
96
|
parts.push(dir);
|
|
96
97
|
}
|
|
@@ -144,6 +145,13 @@ function isWindowsCmdShim(filePath) {
|
|
|
144
145
|
const ext = path.extname(filePath).toLowerCase();
|
|
145
146
|
return ext === '.cmd' || ext === '.bat';
|
|
146
147
|
}
|
|
148
|
+
function isWindowsNodeScript(filePath) {
|
|
149
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
150
|
+
return ext === '.js' || ext === '.mjs' || ext === '.cjs';
|
|
151
|
+
}
|
|
152
|
+
function resolveWindowsNodeCommand() {
|
|
153
|
+
return lookupCommandPaths('node.exe')[0] ?? lookupCommandPaths('node')[0] ?? 'node';
|
|
154
|
+
}
|
|
147
155
|
function quoteCmdArg(text) {
|
|
148
156
|
return `"${text.replace(/"/g, '""')}"`;
|
|
149
157
|
}
|
|
@@ -250,6 +258,13 @@ export function resolveBuiltinCommand(type) {
|
|
|
250
258
|
}
|
|
251
259
|
export function buildLaunchSpec(spec, runtimeArgs, cwd) {
|
|
252
260
|
if (spec.kind === 'native') {
|
|
261
|
+
if (getProcessPlatform() === 'win32' && isWindowsNodeScript(spec.path)) {
|
|
262
|
+
return {
|
|
263
|
+
command: resolveWindowsNodeCommand(),
|
|
264
|
+
args: [spec.path, ...spec.args, ...runtimeArgs],
|
|
265
|
+
cwd,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
253
268
|
return {
|
|
254
269
|
command: spec.path,
|
|
255
270
|
args: [...spec.args, ...runtimeArgs],
|
|
@@ -352,6 +367,13 @@ export function buildCommandStringLaunchSpec(commandString, runtimeArgs, cwd) {
|
|
|
352
367
|
cwd,
|
|
353
368
|
};
|
|
354
369
|
}
|
|
370
|
+
if (isWindowsNodeScript(invocation.path)) {
|
|
371
|
+
return {
|
|
372
|
+
command: resolveWindowsNodeCommand(),
|
|
373
|
+
args: [invocation.path, ...baseArgs, ...runtimeArgs],
|
|
374
|
+
cwd,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
355
377
|
return {
|
|
356
378
|
command: invocation.path,
|
|
357
379
|
args: [...baseArgs, ...runtimeArgs],
|
|
@@ -15,6 +15,7 @@ function buildReplyCommandInstructions(mode, sessionHint, workdirHint) {
|
|
|
15
15
|
'发送图片:shennian manager external send-image --path "<图片绝对路径>" --caption "<可选说明>"',
|
|
16
16
|
'发送视频:shennian manager external send-video --path "<视频绝对路径>" --caption "<可选说明>"',
|
|
17
17
|
'发送文件:shennian manager external send-file --path "<文件绝对路径>" --caption "<可选说明>"',
|
|
18
|
+
'如果外部消息里显示“回复目标:rt_...”,多群监听或需要精确回复时在发送命令后追加 --reply-target "rt_..."。',
|
|
18
19
|
'如果需要转发外部消息里的图片/视频/文件链接,先把链接下载到本地临时文件,再用对应的 --path 命令发送;不要直接发送短时效链接,也不要只口头说明已发送。',
|
|
19
20
|
'只发送用户可见的最终内容,不要发送内部推理、工具日志或实现细节。',
|
|
20
21
|
].join('\n');
|
|
@@ -25,6 +26,7 @@ function buildReplyCommandInstructions(mode, sessionHint, workdirHint) {
|
|
|
25
26
|
'发送图片:shennian external send-image --path "<图片绝对路径>" --caption "<可选说明>"',
|
|
26
27
|
'发送视频:shennian external send-video --path "<视频绝对路径>" --caption "<可选说明>"',
|
|
27
28
|
'发送文件:shennian external send-file --path "<文件绝对路径>" --caption "<可选说明>"',
|
|
29
|
+
'如果外部消息里显示“回复目标:rt_...”,多群监听或需要精确回复时在发送命令后追加 --reply-target "rt_..."。',
|
|
28
30
|
'如果需要转发外部消息里的图片/视频/文件链接,先把链接下载到本地临时文件,再用对应的 --path 命令发送;不要直接发送短时效链接,也不要只口头说明已发送。',
|
|
29
31
|
sessionHint,
|
|
30
32
|
workdirHint,
|
|
@@ -7,6 +7,7 @@ export declare class ChannelRuntime {
|
|
|
7
7
|
private secrets;
|
|
8
8
|
private adapters;
|
|
9
9
|
private completedReplyKeys;
|
|
10
|
+
private recentMessages;
|
|
10
11
|
constructor(onExternalMessage: (sessionId: string, event: ExternalMessageEvent) => void, createReplyTarget: (input: {
|
|
11
12
|
managerSessionId: string;
|
|
12
13
|
channelId: string;
|
|
@@ -17,7 +18,7 @@ export declare class ChannelRuntime {
|
|
|
17
18
|
stop(): Promise<void>;
|
|
18
19
|
ingest(event: ExternalMessageEvent & {
|
|
19
20
|
managerSessionId?: string;
|
|
20
|
-
}):
|
|
21
|
+
}): ExternalMessageEvent;
|
|
21
22
|
reply(input: ExternalReply & {
|
|
22
23
|
managerSessionId: string;
|
|
23
24
|
}): Promise<{
|
|
@@ -53,6 +54,12 @@ export declare class ChannelRuntime {
|
|
|
53
54
|
};
|
|
54
55
|
}>;
|
|
55
56
|
listManagerExternalChannels(managerSessionId: string): ExternalChannelSessionStatus[];
|
|
57
|
+
syncManagerWeChatRpaChannel(managerSessionId: string): Promise<{
|
|
58
|
+
channel: ExternalChannelView;
|
|
59
|
+
messages: ExternalMessageEvent[];
|
|
60
|
+
}>;
|
|
61
|
+
private recordRecentMessage;
|
|
62
|
+
private getRecentMessages;
|
|
56
63
|
upsertManagerChannel(input: {
|
|
57
64
|
id: string;
|
|
58
65
|
managerSessionId: string;
|
|
@@ -84,7 +91,7 @@ export declare class ChannelRuntime {
|
|
|
84
91
|
}>;
|
|
85
92
|
canReply?: boolean;
|
|
86
93
|
systemPrompt?: string;
|
|
87
|
-
source?: 'macos-flow' | 'macos-probe' | 'fixture-jsonl';
|
|
94
|
+
source?: 'macos-flow' | 'macos-probe' | 'windows-visual-flow' | 'fixture-jsonl';
|
|
88
95
|
pollIntervalMs?: number;
|
|
89
96
|
recentLimit?: number;
|
|
90
97
|
idleSeconds?: number;
|
|
@@ -18,14 +18,15 @@ export class ChannelRuntime {
|
|
|
18
18
|
secrets = new ChannelSecretRegistry();
|
|
19
19
|
adapters = new Map();
|
|
20
20
|
completedReplyKeys = new Map();
|
|
21
|
+
recentMessages = new Map();
|
|
21
22
|
constructor(onExternalMessage, createReplyTarget) {
|
|
22
23
|
this.onExternalMessage = onExternalMessage;
|
|
23
24
|
this.createReplyTarget = createReplyTarget;
|
|
24
|
-
const wecom = new WeComChannelAdapter((event) => this.ingest({ type: 'external.message'
|
|
25
|
+
const wecom = new WeComChannelAdapter((event) => this.ingest({ ...event, type: 'external.message' }));
|
|
25
26
|
this.adapters.set(wecom.type, wecom);
|
|
26
|
-
const websocket = new ExternalWebSocketChannelAdapter((event) => this.ingest({ type: 'external.message'
|
|
27
|
+
const websocket = new ExternalWebSocketChannelAdapter((event) => this.ingest({ ...event, type: 'external.message' }));
|
|
27
28
|
this.adapters.set(websocket.type, websocket);
|
|
28
|
-
const wechatRpa = new WeChatRpaChannelAdapter((event) => this.ingest(
|
|
29
|
+
const wechatRpa = new WeChatRpaChannelAdapter((event) => this.ingest(event));
|
|
29
30
|
this.adapters.set(wechatRpa.type, wechatRpa);
|
|
30
31
|
}
|
|
31
32
|
async start() {
|
|
@@ -49,7 +50,10 @@ export class ChannelRuntime {
|
|
|
49
50
|
conversationId: event.conversationId,
|
|
50
51
|
messageId: event.messageId,
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
+
const normalized = { ...event, replyTarget };
|
|
54
|
+
this.onExternalMessage(sessionId, normalized);
|
|
55
|
+
this.recordRecentMessage(normalized);
|
|
56
|
+
return normalized;
|
|
53
57
|
}
|
|
54
58
|
async reply(input) {
|
|
55
59
|
const config = this.configs.get(input.channelId);
|
|
@@ -242,6 +246,33 @@ export class ChannelRuntime {
|
|
|
242
246
|
};
|
|
243
247
|
});
|
|
244
248
|
}
|
|
249
|
+
async syncManagerWeChatRpaChannel(managerSessionId) {
|
|
250
|
+
const config = this.configs.list()
|
|
251
|
+
.find((channel) => channel.enabled && channel.type === 'wechat-rpa' && (channel.sessionId ?? channel.managerSessionId) === managerSessionId);
|
|
252
|
+
if (!config)
|
|
253
|
+
throw new Error('No enabled WeChat RPA channel is bound to this session');
|
|
254
|
+
const adapter = this.adapters.get(config.type);
|
|
255
|
+
if (!adapter?.syncNow)
|
|
256
|
+
throw new Error('WeChat RPA channel does not support manual sync');
|
|
257
|
+
const recentSince = Date.now() - 2 * 60 * 1000;
|
|
258
|
+
const messages = mergeExternalMessages(await adapter.syncNow(config) ?? [], this.getRecentMessages(config.id, recentSince));
|
|
259
|
+
return {
|
|
260
|
+
channel: this.getManagerChannel(managerSessionId, 'wechat-rpa', { includeSecret: true }),
|
|
261
|
+
messages,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
recordRecentMessage(event) {
|
|
265
|
+
const current = this.recentMessages.get(event.channelId) ?? [];
|
|
266
|
+
current.push(event);
|
|
267
|
+
this.recentMessages.set(event.channelId, current.slice(-50));
|
|
268
|
+
}
|
|
269
|
+
getRecentMessages(channelId, sinceMs) {
|
|
270
|
+
const messages = this.recentMessages.get(channelId) ?? [];
|
|
271
|
+
return messages.filter((event) => {
|
|
272
|
+
const receivedAt = Date.parse(event.receivedAt);
|
|
273
|
+
return !Number.isFinite(receivedAt) || receivedAt >= sinceMs;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
245
276
|
async upsertManagerChannel(input) {
|
|
246
277
|
const previous = this.configs.get(input.id);
|
|
247
278
|
const allConfigs = this.configs.list();
|
|
@@ -315,7 +346,7 @@ export class ChannelRuntime {
|
|
|
315
346
|
secretRef: previous?.secretRef || `channel:${input.id}`,
|
|
316
347
|
};
|
|
317
348
|
const priorSecret = this.secrets.get(nextConfig.secretRef);
|
|
318
|
-
const source = input.source || (priorSecret?.source === 'macos-probe' || priorSecret?.source === 'fixture-jsonl' || priorSecret?.source === 'macos-flow'
|
|
349
|
+
const source = input.source || (priorSecret?.source === 'macos-probe' || priorSecret?.source === 'fixture-jsonl' || priorSecret?.source === 'macos-flow' || priorSecret?.source === 'windows-visual-flow' ? priorSecret.source : defaultWeChatRpaSource());
|
|
319
350
|
const configs = allConfigs
|
|
320
351
|
.filter((channel) => channel.id !== nextConfig.id)
|
|
321
352
|
.map((channel) => (channel.sessionId ?? channel.managerSessionId) === boundSessionId && channel.type === 'wechat-rpa'
|
|
@@ -416,15 +447,22 @@ function normalizeWeChatRpaGroups(groups) {
|
|
|
416
447
|
function normalizeCloudOcrMode(value) {
|
|
417
448
|
return value === 'fallback' || value === 'always' ? value : 'off';
|
|
418
449
|
}
|
|
450
|
+
function defaultWeChatRpaSource() {
|
|
451
|
+
return process.platform === 'win32' ? 'windows-visual-flow' : 'macos-flow';
|
|
452
|
+
}
|
|
419
453
|
function defaultWeChatRpaCloudOcrConfig() {
|
|
420
454
|
const config = loadConfig();
|
|
421
455
|
const serverUrl = (config.serverUrl || SERVERS.cn.url).replace(/\/+$/, '');
|
|
422
456
|
const token = config.machineToken?.trim() || config.accessToken?.trim();
|
|
423
457
|
return {
|
|
424
|
-
cloudOcrUrl: `${serverUrl}/integrations/wechat-rpa/ocr`,
|
|
458
|
+
cloudOcrUrl: `${serverApiBaseUrl(serverUrl)}/integrations/wechat-rpa/ocr`,
|
|
425
459
|
...(token ? { cloudOcrToken: token } : {}),
|
|
426
460
|
};
|
|
427
461
|
}
|
|
462
|
+
function serverApiBaseUrl(serverUrl) {
|
|
463
|
+
const normalized = serverUrl.replace(/\/+$/, '');
|
|
464
|
+
return normalized.endsWith('/api') ? normalized : `${normalized}/api`;
|
|
465
|
+
}
|
|
428
466
|
export function planExternalReplySends(channelType, input) {
|
|
429
467
|
const parts = splitExternalReplyText(input.text);
|
|
430
468
|
if (!parts.length && !input.attachment)
|
|
@@ -454,6 +492,18 @@ export function planExternalReplySends(channelType, input) {
|
|
|
454
492
|
}
|
|
455
493
|
return sends;
|
|
456
494
|
}
|
|
495
|
+
function mergeExternalMessages(...groups) {
|
|
496
|
+
const seen = new Set();
|
|
497
|
+
const out = [];
|
|
498
|
+
for (const event of groups.flat()) {
|
|
499
|
+
const key = `${event.channelId}\n${event.conversationId}\n${event.messageId}`;
|
|
500
|
+
if (seen.has(key))
|
|
501
|
+
continue;
|
|
502
|
+
seen.add(key);
|
|
503
|
+
out.push(event);
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
457
507
|
function replyCompletionKey(channelId, conversationId, idempotencyKey) {
|
|
458
508
|
return crypto.createHash('sha256')
|
|
459
509
|
.update(`${channelId}\n${conversationId}\n${idempotencyKey}`)
|
|
@@ -6,7 +6,7 @@ type ChannelSecretRecord = {
|
|
|
6
6
|
token?: string;
|
|
7
7
|
canReply?: boolean;
|
|
8
8
|
systemPrompt?: string;
|
|
9
|
-
source?: 'macos-probe' | 'macos-flow' | 'fixture-jsonl';
|
|
9
|
+
source?: 'macos-probe' | 'macos-flow' | 'windows-visual-flow' | 'fixture-jsonl';
|
|
10
10
|
fixturePath?: string;
|
|
11
11
|
pollIntervalMs?: number;
|
|
12
12
|
groups?: Array<{
|
|
@@ -68,6 +68,11 @@ export type MacWeChatRpaCloudOcrUsage = {
|
|
|
68
68
|
totalTokens?: number;
|
|
69
69
|
};
|
|
70
70
|
export declare function runMacWeChatRpaFlow(options: MacWeChatRpaFlowOptions): Promise<MacWeChatRpaFlowResult>;
|
|
71
|
+
export declare function buildCloudOcrImagePayload(screenshotPath: string, options: Pick<MacWeChatRpaFlowOptions, 'cloudOcrUrl' | 'cloudOcrToken'>): Promise<{
|
|
72
|
+
imageUrl: string;
|
|
73
|
+
imageObjectKey?: string;
|
|
74
|
+
} | null>;
|
|
75
|
+
export declare function cloudOcrImageUploadUrl(cloudOcrUrl?: string): string | null;
|
|
71
76
|
export declare function selectCloudOcrRequest(result: Pick<MacWeChatRpaFlowResult, 'newMessages' | 'recentMessages' | 'screenshotPath' | 'postSendScreenshotPath' | 'sentReply' | 'sentAttachment'>, mode: 'off' | 'fallback' | 'always'): {
|
|
72
77
|
screenshotPath: string;
|
|
73
78
|
purpose: MacWeChatRpaCloudOcrPurpose;
|
|
@@ -49,6 +49,8 @@ export async function runMacWeChatRpaFlow(options) {
|
|
|
49
49
|
const parsed = parseFlowStdout(stdout);
|
|
50
50
|
if (parsed?.interrupted)
|
|
51
51
|
return parsed;
|
|
52
|
+
if (parsed && hasPartialSendProgress(parsed))
|
|
53
|
+
return parsed;
|
|
52
54
|
throw new Error(stderr.trim() || stdout.slice(0, 1_000) || execError.message || 'WeChat RPA flow failed');
|
|
53
55
|
}
|
|
54
56
|
try {
|
|
@@ -72,12 +74,17 @@ function parseFlowStdout(stdout) {
|
|
|
72
74
|
return null;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
77
|
+
function hasPartialSendProgress(result) {
|
|
78
|
+
return Boolean(result.sentReplyObserved || result.sentAttachmentObserved);
|
|
79
|
+
}
|
|
75
80
|
async function maybeEnrichWithCloudOcr(result, options) {
|
|
76
81
|
const mode = options.cloudOcrMode ?? 'off';
|
|
77
82
|
const request = selectCloudOcrRequest(result, mode);
|
|
78
83
|
if (!request || !options.cloudOcrUrl || !options.cloudOcrToken)
|
|
79
84
|
return result;
|
|
80
|
-
const
|
|
85
|
+
const imagePayload = await buildCloudOcrImagePayload(request.screenshotPath, options);
|
|
86
|
+
if (!imagePayload)
|
|
87
|
+
return result;
|
|
81
88
|
const response = await fetch(options.cloudOcrUrl, {
|
|
82
89
|
method: 'POST',
|
|
83
90
|
headers: {
|
|
@@ -85,7 +92,7 @@ async function maybeEnrichWithCloudOcr(result, options) {
|
|
|
85
92
|
'content-type': 'application/json',
|
|
86
93
|
},
|
|
87
94
|
body: JSON.stringify({
|
|
88
|
-
|
|
95
|
+
...imagePayload,
|
|
89
96
|
mimeType: 'image/png',
|
|
90
97
|
conversationName: options.groupName,
|
|
91
98
|
purpose: request.purpose,
|
|
@@ -111,6 +118,47 @@ async function maybeEnrichWithCloudOcr(result, options) {
|
|
|
111
118
|
recentMessages: mergeCloudMessages(result.recentMessages ?? [], cloudMessages).slice(-recentLimit),
|
|
112
119
|
};
|
|
113
120
|
}
|
|
121
|
+
export async function buildCloudOcrImagePayload(screenshotPath, options) {
|
|
122
|
+
const buffer = fs.readFileSync(screenshotPath);
|
|
123
|
+
const uploadUrl = cloudOcrImageUploadUrl(options.cloudOcrUrl);
|
|
124
|
+
if (uploadUrl && options.cloudOcrToken) {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(uploadUrl, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
authorization: `Bearer ${options.cloudOcrToken}`,
|
|
130
|
+
'content-type': 'image/png',
|
|
131
|
+
},
|
|
132
|
+
body: buffer,
|
|
133
|
+
});
|
|
134
|
+
if (response.ok) {
|
|
135
|
+
const payload = await response.json().catch(() => null);
|
|
136
|
+
if (payload?.imageUrl) {
|
|
137
|
+
return {
|
|
138
|
+
imageUrl: payload.imageUrl,
|
|
139
|
+
...(payload.imageObjectKey ? { imageObjectKey: payload.imageObjectKey } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Cloud OCR is an optional fallback; if image upload is unavailable, keep the local result.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
export function cloudOcrImageUploadUrl(cloudOcrUrl) {
|
|
151
|
+
if (!cloudOcrUrl)
|
|
152
|
+
return null;
|
|
153
|
+
try {
|
|
154
|
+
const url = new URL(cloudOcrUrl);
|
|
155
|
+
url.pathname = url.pathname.replace(/\/ocr\/?$/, '/ocr/image');
|
|
156
|
+
return url.toString();
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
114
162
|
export function selectCloudOcrRequest(result, mode) {
|
|
115
163
|
if (!shouldUseCloudOcr(result, mode))
|
|
116
164
|
return null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExecFileOptions } from 'node:child_process';
|
|
2
|
+
import type { MacWeChatRpaAttachment, MacWeChatRpaFlowOptions, MacWeChatRpaFlowResult } from './macos-flow.js';
|
|
3
|
+
type WindowsVisualObservation = {
|
|
4
|
+
text?: string;
|
|
5
|
+
confidence?: number;
|
|
6
|
+
role?: string;
|
|
7
|
+
attachment?: MacWeChatRpaAttachment;
|
|
8
|
+
};
|
|
9
|
+
type WindowsVisualSent = {
|
|
10
|
+
type?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
files?: string[];
|
|
13
|
+
observations?: WindowsVisualObservation[];
|
|
14
|
+
};
|
|
15
|
+
type WindowsVisualSummary = {
|
|
16
|
+
ok?: boolean;
|
|
17
|
+
group?: string;
|
|
18
|
+
captureDir?: string;
|
|
19
|
+
recentMessages?: WindowsVisualObservation[];
|
|
20
|
+
sent?: WindowsVisualSent[];
|
|
21
|
+
artifacts?: string[];
|
|
22
|
+
error?: string;
|
|
23
|
+
};
|
|
24
|
+
export type WindowsWeChatRpaVisualFlowOptions = MacWeChatRpaFlowOptions & {
|
|
25
|
+
helperPath?: string;
|
|
26
|
+
ocrFixturePath?: string;
|
|
27
|
+
attachmentPaths?: string[];
|
|
28
|
+
};
|
|
29
|
+
export declare function buildWindowsVisualFlowExec(options: WindowsWeChatRpaVisualFlowOptions): {
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
execOptions: ExecFileOptions;
|
|
33
|
+
};
|
|
34
|
+
export declare function runWindowsWeChatRpaVisualFlow(options: WindowsWeChatRpaVisualFlowOptions): Promise<MacWeChatRpaFlowResult>;
|
|
35
|
+
export declare function windowsSummaryToFlowResult(options: Pick<WindowsWeChatRpaVisualFlowOptions, 'groupName' | 'replyText' | 'attachmentPath' | 'attachmentPaths'>, summary: WindowsVisualSummary): MacWeChatRpaFlowResult;
|
|
36
|
+
export {};
|