shennian 0.2.78 → 0.2.84
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-win-visual.mjs +1155 -127
- package/dist/scripts/wechat-rpa-win.mjs +227 -1
- package/dist/src/agents/external-channel-instructions.js +1 -3
- package/dist/src/channels/base.d.ts +9 -2
- package/dist/src/channels/runtime.d.ts +2 -1
- package/dist/src/channels/runtime.js +16 -32
- package/dist/src/channels/secret-registry.d.ts +3 -1
- package/dist/src/channels/wechat-rpa/macos-flow.d.ts +6 -5
- package/dist/src/channels/wechat-rpa/macos-flow.js +7 -80
- package/dist/src/channels/wechat-rpa/normalizer.d.ts +5 -1
- package/dist/src/channels/wechat-rpa/normalizer.js +14 -1
- package/dist/src/channels/wechat-rpa/windows-visual-flow.d.ts +4 -0
- package/dist/src/channels/wechat-rpa/windows-visual-flow.js +13 -6
- package/dist/src/channels/wechat-rpa.d.ts +12 -5
- package/dist/src/channels/wechat-rpa.js +362 -71
- package/dist/src/commands/daemon.d.ts +13 -2
- package/dist/src/commands/daemon.js +175 -14
- package/dist/src/commands/manager.d.ts +1 -1
- package/dist/src/commands/manager.js +13 -10
- package/dist/src/index.js +64 -39
- package/dist/src/manager/runtime.js +35 -3
- package/dist/src/native-fusion/opencode-parser.js +2 -0
- package/dist/src/native-fusion/parsers.js +15 -0
- package/dist/src/native-fusion/service.js +3 -23
- package/package.json +3 -3
|
@@ -32,10 +32,13 @@ function printHelp() {
|
|
|
32
32
|
node scripts/wechat-rpa-win.mjs send-files --group <name> --file <path> [--file <path> ...]
|
|
33
33
|
node scripts/wechat-rpa-win.mjs listen --group <name> --seconds 60 --poll-ms 1000
|
|
34
34
|
node scripts/wechat-rpa-win.mjs capture --region window --output C:\\tmp\\wechat.png
|
|
35
|
-
node scripts/wechat-rpa-win.mjs
|
|
35
|
+
node scripts/wechat-rpa-win.mjs resize-window --width 1200 --height 820 [--x 120 --y 80] [--include-offscreen]
|
|
36
|
+
node scripts/wechat-rpa-win.mjs click --x 480 --y 470 --no-raise [--right]
|
|
36
37
|
node scripts/wechat-rpa-win.mjs paste-text --text "hello"
|
|
37
38
|
node scripts/wechat-rpa-win.mjs paste-files --file C:\\tmp\\demo.png
|
|
39
|
+
node scripts/wechat-rpa-win.mjs read-clipboard
|
|
38
40
|
node scripts/wechat-rpa-win.mjs press --keys "%s"
|
|
41
|
+
node scripts/wechat-rpa-win.mjs dismiss-menus --count 2
|
|
39
42
|
|
|
40
43
|
Options:
|
|
41
44
|
--helper <path> Override helper exe path.
|
|
@@ -70,6 +73,33 @@ async function main() {
|
|
|
70
73
|
'Set SHENNIAN_WECHAT_RPA_ALLOW_UIA_DIAGNOSTICS=1 in a local diagnostic shell to use it.',
|
|
71
74
|
)
|
|
72
75
|
}
|
|
76
|
+
if (command === 'restart-wechat-for-uia') {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'restart-wechat-for-uia is disabled from this lab CLI because it closes the user\'s WeChat session. ' +
|
|
79
|
+
'Do not restart or kill WeChat during Windows RPA validation.',
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
if (command === 'resize-window') {
|
|
83
|
+
const width = Number(takeOption(argv, '--width'))
|
|
84
|
+
const height = Number(takeOption(argv, '--height'))
|
|
85
|
+
const x = takeOption(argv, '--x')
|
|
86
|
+
const y = takeOption(argv, '--y')
|
|
87
|
+
const includeOffscreen = argv.includes('--include-offscreen')
|
|
88
|
+
if (includeOffscreen) argv.splice(argv.indexOf('--include-offscreen'), 1)
|
|
89
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) throw new Error('resize-window requires --width and --height')
|
|
90
|
+
if (width < 760 || height < 600) throw new Error('WeChat RPA window must be at least 760x600')
|
|
91
|
+
if (x != null && !Number.isFinite(Number(x))) throw new Error('resize-window --x must be a number')
|
|
92
|
+
if (y != null && !Number.isFinite(Number(y))) throw new Error('resize-window --y must be a number')
|
|
93
|
+
const result = await resizeWindowWithPowerShell({
|
|
94
|
+
width: Math.round(width),
|
|
95
|
+
height: Math.round(height),
|
|
96
|
+
x: x == null ? null : Math.round(Number(x)),
|
|
97
|
+
y: y == null ? null : Math.round(Number(y)),
|
|
98
|
+
includeOffscreen,
|
|
99
|
+
})
|
|
100
|
+
process.stdout.write(`${JSON.stringify({ ok: true, command, payload: result }, null, 2)}\n`)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
73
103
|
|
|
74
104
|
const helper = takeOption(argv, '--helper') ?? process.env.WECHAT_RPA_WIN_HELPER ?? defaultHelper
|
|
75
105
|
if (!fs.existsSync(helper)) {
|
|
@@ -120,6 +150,202 @@ async function main() {
|
|
|
120
150
|
}
|
|
121
151
|
}
|
|
122
152
|
|
|
153
|
+
async function resizeWindowWithPowerShell({ width, height, x, y, includeOffscreen = false }) {
|
|
154
|
+
const script = `
|
|
155
|
+
$ErrorActionPreference = "Stop"
|
|
156
|
+
$typeDefinition = '
|
|
157
|
+
using System;
|
|
158
|
+
using System.Collections.Generic;
|
|
159
|
+
using System.Runtime.InteropServices;
|
|
160
|
+
using System.Text;
|
|
161
|
+
|
|
162
|
+
public static class Win32WechatResize {
|
|
163
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
164
|
+
|
|
165
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
166
|
+
public struct RECT {
|
|
167
|
+
public int Left;
|
|
168
|
+
public int Top;
|
|
169
|
+
public int Right;
|
|
170
|
+
public int Bottom;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
174
|
+
public struct MONITORINFO {
|
|
175
|
+
public int cbSize;
|
|
176
|
+
public RECT rcMonitor;
|
|
177
|
+
public RECT rcWork;
|
|
178
|
+
public uint dwFlags;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
[DllImport("user32.dll")]
|
|
182
|
+
public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
|
|
183
|
+
|
|
184
|
+
[DllImport("user32.dll")]
|
|
185
|
+
public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
186
|
+
|
|
187
|
+
[DllImport("user32.dll")]
|
|
188
|
+
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
|
189
|
+
|
|
190
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
191
|
+
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
192
|
+
|
|
193
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
194
|
+
public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int maxCount);
|
|
195
|
+
|
|
196
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
197
|
+
public static extern int GetClassName(IntPtr hWnd, StringBuilder text, int maxCount);
|
|
198
|
+
|
|
199
|
+
[DllImport("user32.dll")]
|
|
200
|
+
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
201
|
+
|
|
202
|
+
[DllImport("user32.dll")]
|
|
203
|
+
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint flags);
|
|
204
|
+
|
|
205
|
+
[DllImport("user32.dll")]
|
|
206
|
+
public static extern IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
|
|
207
|
+
|
|
208
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
209
|
+
public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
|
|
210
|
+
}
|
|
211
|
+
'
|
|
212
|
+
Add-Type -TypeDefinition $typeDefinition
|
|
213
|
+
|
|
214
|
+
function Read-WindowText([IntPtr]$Handle) {
|
|
215
|
+
$builder = New-Object System.Text.StringBuilder 512
|
|
216
|
+
[void][Win32WechatResize]::GetWindowText($Handle, $builder, $builder.Capacity)
|
|
217
|
+
$builder.ToString()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function Read-ClassName([IntPtr]$Handle) {
|
|
221
|
+
$builder = New-Object System.Text.StringBuilder 256
|
|
222
|
+
[void][Win32WechatResize]::GetClassName($Handle, $builder, $builder.Capacity)
|
|
223
|
+
$builder.ToString()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
$processIds = @(Get-Process Weixin,WeChat,WeChatAppEx -ErrorAction SilentlyContinue | ForEach-Object { [uint32]$_.Id })
|
|
227
|
+
if ($processIds.Count -eq 0) { throw "Cannot find WeChat/Weixin process" }
|
|
228
|
+
|
|
229
|
+
$windows = New-Object System.Collections.Generic.List[object]
|
|
230
|
+
$callback = [Win32WechatResize+EnumWindowsProc]{
|
|
231
|
+
param([IntPtr]$Handle, [IntPtr]$Param)
|
|
232
|
+
$includeOffscreen = ${includeOffscreen ? '$true' : '$false'}
|
|
233
|
+
$visible = [Win32WechatResize]::IsWindowVisible($Handle)
|
|
234
|
+
if (-not $includeOffscreen -and -not $visible) { return $true }
|
|
235
|
+
[uint32]$windowPid = 0
|
|
236
|
+
[void][Win32WechatResize]::GetWindowThreadProcessId($Handle, [ref]$windowPid)
|
|
237
|
+
if ($processIds -notcontains $windowPid) { return $true }
|
|
238
|
+
$rect = New-Object Win32WechatResize+RECT
|
|
239
|
+
if (-not [Win32WechatResize]::GetWindowRect($Handle, [ref]$rect)) { return $true }
|
|
240
|
+
$w = $rect.Right - $rect.Left
|
|
241
|
+
$h = $rect.Bottom - $rect.Top
|
|
242
|
+
$title = Read-WindowText $Handle
|
|
243
|
+
$className = Read-ClassName $Handle
|
|
244
|
+
if ($title -ne "微信" -and $className -notmatch "Qt|WeChat") { return $true }
|
|
245
|
+
if (-not $includeOffscreen -and ($w -lt 760 -or $h -lt 600)) { return $true }
|
|
246
|
+
$windows.Add([pscustomobject]@{
|
|
247
|
+
handle = $Handle.ToInt64()
|
|
248
|
+
processId = $windowPid
|
|
249
|
+
title = $title
|
|
250
|
+
className = $className
|
|
251
|
+
visible = $visible
|
|
252
|
+
x = $rect.Left
|
|
253
|
+
y = $rect.Top
|
|
254
|
+
width = $w
|
|
255
|
+
height = $h
|
|
256
|
+
area = $w * $h
|
|
257
|
+
offscreen = (-not $visible -or $rect.Left -lt -1000 -or $rect.Top -lt -1000 -or $w -lt 760 -or $h -lt 600)
|
|
258
|
+
}) | Out-Null
|
|
259
|
+
return $true
|
|
260
|
+
}
|
|
261
|
+
[void][Win32WechatResize]::EnumWindows($callback, [IntPtr]::Zero)
|
|
262
|
+
$window = $windows | Sort-Object @{ Expression = { if ($_.offscreen) { 1 } else { 0 } } }, @{ Expression = 'area'; Descending = $true } | Select-Object -First 1
|
|
263
|
+
if (-not $window) { throw "Cannot locate WeChat main window via Win32" }
|
|
264
|
+
$requestedWidth = ${width}
|
|
265
|
+
$requestedHeight = ${height}
|
|
266
|
+
$left = [int](${x == null ? '$window.x' : Number(x)})
|
|
267
|
+
$top = [int](${y == null ? '$window.y' : Number(y)})
|
|
268
|
+
$monitor = [Win32WechatResize]::MonitorFromWindow([IntPtr]$window.handle, 2)
|
|
269
|
+
$info = New-Object Win32WechatResize+MONITORINFO
|
|
270
|
+
$info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf([type][Win32WechatResize+MONITORINFO])
|
|
271
|
+
if (-not [Win32WechatResize]::GetMonitorInfo($monitor, [ref]$info)) { throw "Cannot read monitor work area" }
|
|
272
|
+
$margin = 8
|
|
273
|
+
$workLeft = [int]$info.rcWork.Left
|
|
274
|
+
$workTop = [int]$info.rcWork.Top
|
|
275
|
+
$workRight = [int]$info.rcWork.Right
|
|
276
|
+
$workBottom = [int]$info.rcWork.Bottom
|
|
277
|
+
$minWidth = 760
|
|
278
|
+
$minHeight = 600
|
|
279
|
+
$maxWidth = [Math]::Max($minWidth, $workRight - $workLeft - ($margin * 2))
|
|
280
|
+
$maxHeight = [Math]::Max($minHeight, $workBottom - $workTop - ($margin * 2))
|
|
281
|
+
$targetWidth = [Math]::Min($requestedWidth, $maxWidth)
|
|
282
|
+
$targetHeight = [Math]::Min($requestedHeight, $maxHeight)
|
|
283
|
+
if ($left -lt ($workLeft + $margin)) { $left = $workLeft + $margin }
|
|
284
|
+
if ($top -lt ($workTop + $margin)) { $top = $workTop + $margin }
|
|
285
|
+
if (($left + $targetWidth) -gt ($workRight - $margin)) { $left = [Math]::Max($workLeft + $margin, $workRight - $targetWidth - $margin) }
|
|
286
|
+
if (($top + $targetHeight) -gt ($workBottom - $margin)) { $top = [Math]::Max($workTop + $margin, $workBottom - $targetHeight - $margin) }
|
|
287
|
+
[void][Win32WechatResize]::ShowWindow([IntPtr]$window.handle, 9)
|
|
288
|
+
[void][Win32WechatResize]::SetWindowPos([IntPtr]$window.handle, [intptr](-2), [int]$left, [int]$top, [int]$targetWidth, [int]$targetHeight, 0x0040)
|
|
289
|
+
Start-Sleep -Milliseconds 250
|
|
290
|
+
$afterRect = New-Object Win32WechatResize+RECT
|
|
291
|
+
[void][Win32WechatResize]::GetWindowRect([IntPtr]$window.handle, [ref]$afterRect)
|
|
292
|
+
$afterWidth = $afterRect.Right - $afterRect.Left
|
|
293
|
+
$afterHeight = $afterRect.Bottom - $afterRect.Top
|
|
294
|
+
if ($targetWidth -ge $minWidth -and $targetHeight -ge $minHeight -and ($afterWidth -lt ($minWidth - 20) -or $afterHeight -lt ($minHeight - 20))) {
|
|
295
|
+
throw ("resize-window did not produce a usable WeChat main window; after=" + $afterWidth + "x" + $afterHeight + " requested=" + $requestedWidth + "x" + $requestedHeight)
|
|
296
|
+
}
|
|
297
|
+
if ([Math]::Abs($afterWidth - $targetWidth) -gt 80 -or [Math]::Abs($afterHeight - $targetHeight) -gt 80) {
|
|
298
|
+
throw ("resize-window result diverged from applied target; after=" + $afterWidth + "x" + $afterHeight + " applied=" + $targetWidth + "x" + $targetHeight)
|
|
299
|
+
}
|
|
300
|
+
@{
|
|
301
|
+
action = "resize-window"
|
|
302
|
+
requested = @{
|
|
303
|
+
x = ${x == null ? '$null' : Number(x)}
|
|
304
|
+
y = ${y == null ? '$null' : Number(y)}
|
|
305
|
+
width = $requestedWidth
|
|
306
|
+
height = $requestedHeight
|
|
307
|
+
}
|
|
308
|
+
applied = @{
|
|
309
|
+
x = $left
|
|
310
|
+
y = $top
|
|
311
|
+
width = $targetWidth
|
|
312
|
+
height = $targetHeight
|
|
313
|
+
}
|
|
314
|
+
workArea = @{
|
|
315
|
+
x = $workLeft
|
|
316
|
+
y = $workTop
|
|
317
|
+
width = $workRight - $workLeft
|
|
318
|
+
height = $workBottom - $workTop
|
|
319
|
+
}
|
|
320
|
+
before = $window
|
|
321
|
+
after = @{
|
|
322
|
+
x = $afterRect.Left
|
|
323
|
+
y = $afterRect.Top
|
|
324
|
+
width = $afterWidth
|
|
325
|
+
height = $afterHeight
|
|
326
|
+
}
|
|
327
|
+
} | ConvertTo-Json -Depth 5
|
|
328
|
+
`
|
|
329
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64')
|
|
330
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], {
|
|
331
|
+
cwd: repoRoot,
|
|
332
|
+
windowsHide: true,
|
|
333
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
334
|
+
})
|
|
335
|
+
let stdout = ''
|
|
336
|
+
let stderr = ''
|
|
337
|
+
child.stdout.setEncoding('utf8')
|
|
338
|
+
child.stderr.setEncoding('utf8')
|
|
339
|
+
child.stdout.on('data', chunk => { stdout += chunk })
|
|
340
|
+
child.stderr.on('data', chunk => { stderr += chunk })
|
|
341
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
342
|
+
child.on('error', reject)
|
|
343
|
+
child.on('close', resolve)
|
|
344
|
+
})
|
|
345
|
+
if (exitCode !== 0) throw new Error(stderr.trim() || stdout.trim() || `resize-window PowerShell exited ${exitCode}`)
|
|
346
|
+
return JSON.parse(stdout.trim())
|
|
347
|
+
}
|
|
348
|
+
|
|
123
349
|
main().catch(error => {
|
|
124
350
|
console.error(error instanceof Error ? error.message : String(error))
|
|
125
351
|
process.exit(1)
|
|
@@ -62,9 +62,7 @@ function weChatRpaRuntimeInstructions(channel) {
|
|
|
62
62
|
'这是运行在绑定电脑上的个人微信 RPA 通道,不是云端托管企业微信账号。',
|
|
63
63
|
groups.length ? `当前监听群:${groups.join('、')}。只把这些群里的消息当成此通道上下文。` : '',
|
|
64
64
|
channel.forceForeground ? '本通道允许在用户空闲时短暂前台化微信。' : '本通道默认不强制抢前台;用户正在操作时可能延迟收发。',
|
|
65
|
-
|
|
66
|
-
? `截图识别不足时会通过神念服务端调用云端 OCR fallback(模式:${channel.cloudOcrMode}),边缘端不直接持有千问 API key。`
|
|
67
|
-
: '默认只用本机 OCR/视觉结果;不要假设云端多模态已经理解图片内容。',
|
|
65
|
+
'运行时只用绑定电脑上的本机 OCR/视觉结果;不要假设服务端或云端多模态已经理解图片内容。',
|
|
68
66
|
].filter(Boolean).join('\n');
|
|
69
67
|
}
|
|
70
68
|
export function buildExternalChannelInstructions(channel, workDir, sessionId, mode = 'agent') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExternalChannelSessionStatus } from '@shennian/wire';
|
|
2
|
-
export type ExternalChannelRuntimeStatus = Pick<ExternalChannelSessionStatus, 'wechatRpaRuntimeState' | 'wechatRpaLastRunAt' | 'wechatRpaLastMessageAt' | 'wechatRpaLastInterruptedAt' | 'wechatRpaPendingReplyCount' | 'wechatRpaLastError' | 'wechatRpaLastCloudOcrAt' | 'wechatRpaLastCloudOcrPurpose' | 'wechatRpaLastCloudOcrRequestId' | 'wechatRpaLastCloudOcrImageHash' | 'wechatRpaLastCloudOcrUsage'>;
|
|
2
|
+
export type ExternalChannelRuntimeStatus = Pick<ExternalChannelSessionStatus, 'wechatRpaRuntimeState' | 'wechatRpaLastRunAt' | 'wechatRpaLastMessageAt' | 'wechatRpaLastInterruptedAt' | 'wechatRpaPendingReplyCount' | 'wechatRpaLastError' | 'wechatRpaLastRunId' | 'wechatRpaLastTracePath' | 'wechatRpaLastTraceSummary' | 'wechatRpaRecentTaskSummaries' | 'wechatRpaLastCloudOcrAt' | 'wechatRpaLastCloudOcrPurpose' | 'wechatRpaLastCloudOcrRequestId' | 'wechatRpaLastCloudOcrImageHash' | 'wechatRpaLastCloudOcrUsage'>;
|
|
3
3
|
export type ExternalChannelType = 'wecom' | 'websocket' | 'wechat-rpa';
|
|
4
4
|
export type ExternalChannelConfig = {
|
|
5
5
|
id: string;
|
|
@@ -41,6 +41,7 @@ export type ExternalChannelView = {
|
|
|
41
41
|
noRestore?: boolean;
|
|
42
42
|
downloadAttachments?: boolean;
|
|
43
43
|
downloadAttachmentsDir?: string;
|
|
44
|
+
selfNickname?: string | null;
|
|
44
45
|
cloudOcrUrl?: string;
|
|
45
46
|
cloudOcrToken?: string;
|
|
46
47
|
cloudOcrMode?: string;
|
|
@@ -49,6 +50,10 @@ export type ExternalChannelView = {
|
|
|
49
50
|
wechatRpaLastMessageAt?: string | null;
|
|
50
51
|
wechatRpaLastInterruptedAt?: string | null;
|
|
51
52
|
wechatRpaLastError?: string | null;
|
|
53
|
+
wechatRpaLastRunId?: string | null;
|
|
54
|
+
wechatRpaLastTracePath?: string | null;
|
|
55
|
+
wechatRpaLastTraceSummary?: string | null;
|
|
56
|
+
wechatRpaRecentTaskSummaries?: ExternalChannelSessionStatus['wechatRpaRecentTaskSummaries'];
|
|
52
57
|
wechatRpaLastCloudOcrAt?: string | null;
|
|
53
58
|
wechatRpaLastCloudOcrPurpose?: string | null;
|
|
54
59
|
wechatRpaLastCloudOcrRequestId?: string | null;
|
|
@@ -64,6 +69,7 @@ export type ExternalMessageEvent = {
|
|
|
64
69
|
channelId: string;
|
|
65
70
|
channelType: ExternalChannelType;
|
|
66
71
|
conversationId: string;
|
|
72
|
+
conversationName?: string | null;
|
|
67
73
|
messageId: string;
|
|
68
74
|
sender: {
|
|
69
75
|
id: string;
|
|
@@ -72,6 +78,7 @@ export type ExternalMessageEvent = {
|
|
|
72
78
|
text: string;
|
|
73
79
|
attachments: ExternalMessageAttachment[];
|
|
74
80
|
receivedAt: string;
|
|
81
|
+
isMentioned?: boolean;
|
|
75
82
|
replyTarget: string;
|
|
76
83
|
rawRef?: string | null;
|
|
77
84
|
};
|
|
@@ -107,7 +114,7 @@ export type ExternalReply = {
|
|
|
107
114
|
idempotencyKey?: string;
|
|
108
115
|
};
|
|
109
116
|
export type ExternalChannelSendResult = void | {
|
|
110
|
-
status: 'sent' | 'queued';
|
|
117
|
+
status: 'sent' | 'queued' | 'manual-review';
|
|
111
118
|
reason?: string;
|
|
112
119
|
};
|
|
113
120
|
export interface ExternalChannelAdapter {
|
|
@@ -91,7 +91,7 @@ export declare class ChannelRuntime {
|
|
|
91
91
|
}>;
|
|
92
92
|
canReply?: boolean;
|
|
93
93
|
systemPrompt?: string;
|
|
94
|
-
source?: 'macos-flow' | 'macos-probe' | 'windows-visual-flow' | 'fixture-jsonl';
|
|
94
|
+
source?: 'macos-flow' | 'macos-probe' | 'windows-visual-flow' | 'wechat-rpa-lab' | 'fixture-jsonl';
|
|
95
95
|
pollIntervalMs?: number;
|
|
96
96
|
recentLimit?: number;
|
|
97
97
|
idleSeconds?: number;
|
|
@@ -99,6 +99,7 @@ export declare class ChannelRuntime {
|
|
|
99
99
|
noRestore?: boolean;
|
|
100
100
|
downloadAttachments?: boolean;
|
|
101
101
|
downloadAttachmentsDir?: string;
|
|
102
|
+
selfNickname?: string;
|
|
102
103
|
flowScriptPath?: string;
|
|
103
104
|
cloudOcrUrl?: string;
|
|
104
105
|
cloudOcrToken?: string;
|
|
@@ -9,8 +9,6 @@ import { WeComChannelAdapter } from './wecom.js';
|
|
|
9
9
|
import { WeChatRpaChannelAdapter } from './wechat-rpa.js';
|
|
10
10
|
import { ExternalWebSocketChannelAdapter } from './websocket.js';
|
|
11
11
|
import { splitExternalReplyText } from './reply-split.js';
|
|
12
|
-
import { loadConfig } from '../config/index.js';
|
|
13
|
-
import { SERVERS } from '../region.js';
|
|
14
12
|
export class ChannelRuntime {
|
|
15
13
|
onExternalMessage;
|
|
16
14
|
createReplyTarget;
|
|
@@ -82,6 +80,9 @@ export class ChannelRuntime {
|
|
|
82
80
|
pending = true;
|
|
83
81
|
continue;
|
|
84
82
|
}
|
|
83
|
+
if (result?.status === 'manual-review') {
|
|
84
|
+
return { ok: false, error: result.reason || 'Reply requires manual review before retry' };
|
|
85
|
+
}
|
|
85
86
|
if (idempotencyKey)
|
|
86
87
|
this.markReplyCompleted(config, input.conversationId, idempotencyKey);
|
|
87
88
|
}
|
|
@@ -152,7 +153,7 @@ export class ChannelRuntime {
|
|
|
152
153
|
tokenConfigured: Boolean(secret?.token),
|
|
153
154
|
canReply: Boolean(secret?.canReply),
|
|
154
155
|
systemPrompt: typeof secret?.systemPrompt === 'string' ? secret.systemPrompt : '',
|
|
155
|
-
...wechatRpaViewFields(secret
|
|
156
|
+
...wechatRpaViewFields(secret),
|
|
156
157
|
...adapterStatus,
|
|
157
158
|
};
|
|
158
159
|
}
|
|
@@ -178,7 +179,7 @@ export class ChannelRuntime {
|
|
|
178
179
|
tokenConfigured: Boolean(secret?.token),
|
|
179
180
|
canReply: Boolean(secret?.canReply),
|
|
180
181
|
systemPrompt: typeof secret?.systemPrompt === 'string' ? secret.systemPrompt : '',
|
|
181
|
-
...wechatRpaViewFields(secret
|
|
182
|
+
...wechatRpaViewFields(secret),
|
|
182
183
|
...adapterStatus,
|
|
183
184
|
};
|
|
184
185
|
}
|
|
@@ -332,6 +333,8 @@ export class ChannelRuntime {
|
|
|
332
333
|
const groups = normalizeWeChatRpaGroups(input.groups);
|
|
333
334
|
if (input.enabled && !groups.length)
|
|
334
335
|
throw new Error('WeChat RPA 至少需要配置一个群');
|
|
336
|
+
if (input.enabled && groups.length > 1)
|
|
337
|
+
throw new Error('WeChat RPA 每个对话只能绑定一个群');
|
|
335
338
|
const nextConfig = {
|
|
336
339
|
id: input.id,
|
|
337
340
|
type: 'wechat-rpa',
|
|
@@ -346,7 +349,7 @@ export class ChannelRuntime {
|
|
|
346
349
|
secretRef: previous?.secretRef || `channel:${input.id}`,
|
|
347
350
|
};
|
|
348
351
|
const priorSecret = this.secrets.get(nextConfig.secretRef);
|
|
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());
|
|
352
|
+
const source = input.source || (priorSecret?.source === 'macos-probe' || priorSecret?.source === 'fixture-jsonl' || priorSecret?.source === 'macos-flow' || priorSecret?.source === 'windows-visual-flow' || priorSecret?.source === 'wechat-rpa-lab' ? priorSecret.source : defaultWeChatRpaSource());
|
|
350
353
|
const configs = allConfigs
|
|
351
354
|
.filter((channel) => channel.id !== nextConfig.id)
|
|
352
355
|
.map((channel) => (channel.sessionId ?? channel.managerSessionId) === boundSessionId && channel.type === 'wechat-rpa'
|
|
@@ -354,8 +357,7 @@ export class ChannelRuntime {
|
|
|
354
357
|
: channel);
|
|
355
358
|
configs.push(nextConfig);
|
|
356
359
|
this.configs.replaceAll(configs);
|
|
357
|
-
const cloudOcrMode =
|
|
358
|
-
const cloudOcrDefaults = cloudOcrMode === 'off' ? {} : defaultWeChatRpaCloudOcrConfig();
|
|
360
|
+
const cloudOcrMode = 'off';
|
|
359
361
|
this.secrets.upsert(nextConfig.secretRef, {
|
|
360
362
|
type: 'wechat-rpa',
|
|
361
363
|
source,
|
|
@@ -367,9 +369,10 @@ export class ChannelRuntime {
|
|
|
367
369
|
noRestore: input.noRestore ?? (priorSecret?.noRestore === undefined ? true : Boolean(priorSecret.noRestore)),
|
|
368
370
|
downloadAttachments: input.downloadAttachments ?? (priorSecret?.downloadAttachments === undefined ? true : Boolean(priorSecret.downloadAttachments)),
|
|
369
371
|
downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
|
|
372
|
+
selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
|
|
370
373
|
flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
|
|
371
|
-
cloudOcrUrl:
|
|
372
|
-
cloudOcrToken:
|
|
374
|
+
cloudOcrUrl: '',
|
|
375
|
+
cloudOcrToken: '',
|
|
373
376
|
cloudOcrMode,
|
|
374
377
|
canReply: input.canReply ?? priorSecret?.canReply ?? false,
|
|
375
378
|
systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
|
|
@@ -397,7 +400,7 @@ function isChannelSecretConfigured(config, secret) {
|
|
|
397
400
|
return Boolean(secret.token || secret.botId || secret.secret);
|
|
398
401
|
return Boolean(secret.token);
|
|
399
402
|
}
|
|
400
|
-
function wechatRpaViewFields(secret
|
|
403
|
+
function wechatRpaViewFields(secret) {
|
|
401
404
|
if (!secret || secret.type !== 'wechat-rpa')
|
|
402
405
|
return {};
|
|
403
406
|
return {
|
|
@@ -410,9 +413,7 @@ function wechatRpaViewFields(secret, includeSecret) {
|
|
|
410
413
|
noRestore: secret.noRestore === undefined ? undefined : Boolean(secret.noRestore),
|
|
411
414
|
downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
|
|
412
415
|
downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : '',
|
|
413
|
-
|
|
414
|
-
cloudOcrToken: includeSecret && typeof secret.cloudOcrToken === 'string' ? secret.cloudOcrToken : '',
|
|
415
|
-
cloudOcrMode: typeof secret.cloudOcrMode === 'string' ? secret.cloudOcrMode : 'off',
|
|
416
|
+
selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : '',
|
|
416
417
|
};
|
|
417
418
|
}
|
|
418
419
|
function wechatRpaStatusFields(secret) {
|
|
@@ -428,8 +429,7 @@ function wechatRpaStatusFields(secret) {
|
|
|
428
429
|
noRestore: secret.noRestore === undefined ? null : Boolean(secret.noRestore),
|
|
429
430
|
downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
|
|
430
431
|
downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : null,
|
|
431
|
-
|
|
432
|
-
cloudOcrMode: typeof secret.cloudOcrMode === 'string' ? secret.cloudOcrMode : 'off',
|
|
432
|
+
selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : null,
|
|
433
433
|
};
|
|
434
434
|
}
|
|
435
435
|
function normalizeWeChatRpaGroups(groups) {
|
|
@@ -444,24 +444,8 @@ function normalizeWeChatRpaGroups(groups) {
|
|
|
444
444
|
}
|
|
445
445
|
return result;
|
|
446
446
|
}
|
|
447
|
-
function normalizeCloudOcrMode(value) {
|
|
448
|
-
return value === 'fallback' || value === 'always' ? value : 'off';
|
|
449
|
-
}
|
|
450
447
|
function defaultWeChatRpaSource() {
|
|
451
|
-
return process.platform === 'win32' ? 'windows-visual-flow' : '
|
|
452
|
-
}
|
|
453
|
-
function defaultWeChatRpaCloudOcrConfig() {
|
|
454
|
-
const config = loadConfig();
|
|
455
|
-
const serverUrl = (config.serverUrl || SERVERS.cn.url).replace(/\/+$/, '');
|
|
456
|
-
const token = config.machineToken?.trim() || config.accessToken?.trim();
|
|
457
|
-
return {
|
|
458
|
-
cloudOcrUrl: `${serverApiBaseUrl(serverUrl)}/integrations/wechat-rpa/ocr`,
|
|
459
|
-
...(token ? { cloudOcrToken: token } : {}),
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
function serverApiBaseUrl(serverUrl) {
|
|
463
|
-
const normalized = serverUrl.replace(/\/+$/, '');
|
|
464
|
-
return normalized.endsWith('/api') ? normalized : `${normalized}/api`;
|
|
448
|
+
return process.platform === 'win32' ? 'windows-visual-flow' : 'wechat-rpa-lab';
|
|
465
449
|
}
|
|
466
450
|
export function planExternalReplySends(channelType, input) {
|
|
467
451
|
const parts = splitExternalReplyText(input.text);
|
|
@@ -6,7 +6,7 @@ type ChannelSecretRecord = {
|
|
|
6
6
|
token?: string;
|
|
7
7
|
canReply?: boolean;
|
|
8
8
|
systemPrompt?: string;
|
|
9
|
-
source?: 'macos-probe' | 'macos-flow' | 'windows-visual-flow' | 'fixture-jsonl';
|
|
9
|
+
source?: 'macos-probe' | 'macos-flow' | 'windows-visual-flow' | 'wechat-rpa-lab' | 'fixture-jsonl';
|
|
10
10
|
fixturePath?: string;
|
|
11
11
|
pollIntervalMs?: number;
|
|
12
12
|
groups?: Array<{
|
|
@@ -16,8 +16,10 @@ type ChannelSecretRecord = {
|
|
|
16
16
|
noRestore?: boolean;
|
|
17
17
|
downloadAttachments?: boolean;
|
|
18
18
|
downloadAttachmentsDir?: string;
|
|
19
|
+
selfNickname?: string;
|
|
19
20
|
idleSeconds?: number;
|
|
20
21
|
recentLimit?: number;
|
|
22
|
+
readMode?: 'local-ocr' | 'hybrid-vlm';
|
|
21
23
|
flowScriptPath?: string;
|
|
22
24
|
cloudOcrUrl?: string;
|
|
23
25
|
cloudOcrToken?: string;
|
|
@@ -20,6 +20,8 @@ export type MacWeChatRpaFlowResult = {
|
|
|
20
20
|
groupName: string;
|
|
21
21
|
interrupted?: boolean;
|
|
22
22
|
reason?: string;
|
|
23
|
+
rpaRunId?: string;
|
|
24
|
+
rpaTraceSummary?: string;
|
|
23
25
|
screenshotPath?: string;
|
|
24
26
|
recentMessages?: MacWeChatRpaFlowMessage[];
|
|
25
27
|
newMessages?: MacWeChatRpaFlowMessage[];
|
|
@@ -40,6 +42,10 @@ export type MacWeChatRpaFlowMessage = {
|
|
|
40
42
|
id?: string;
|
|
41
43
|
text?: string;
|
|
42
44
|
confidence?: number;
|
|
45
|
+
senderName?: string | null;
|
|
46
|
+
observedAt?: string | null;
|
|
47
|
+
timestampIso?: string | null;
|
|
48
|
+
isMentioned?: boolean | null;
|
|
43
49
|
attachments?: MacWeChatRpaAttachment[];
|
|
44
50
|
};
|
|
45
51
|
export type MacWeChatRpaAttachment = {
|
|
@@ -68,11 +74,6 @@ export type MacWeChatRpaCloudOcrUsage = {
|
|
|
68
74
|
totalTokens?: number;
|
|
69
75
|
};
|
|
70
76
|
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;
|
|
76
77
|
export declare function selectCloudOcrRequest(result: Pick<MacWeChatRpaFlowResult, 'newMessages' | 'recentMessages' | 'screenshotPath' | 'postSendScreenshotPath' | 'sentReply' | 'sentAttachment'>, mode: 'off' | 'fallback' | 'always'): {
|
|
77
78
|
screenshotPath: string;
|
|
78
79
|
purpose: MacWeChatRpaCloudOcrPurpose;
|
|
@@ -57,6 +57,8 @@ export async function runMacWeChatRpaFlow(options) {
|
|
|
57
57
|
const parsed = JSON.parse(stdout);
|
|
58
58
|
if (parsed.interrupted)
|
|
59
59
|
return parsed;
|
|
60
|
+
if (!parsed.ok && hasSendAttempt(parsed))
|
|
61
|
+
return parsed;
|
|
60
62
|
if (!parsed.ok)
|
|
61
63
|
throw new Error(parsed.error || 'WeChat RPA flow failed');
|
|
62
64
|
return await maybeEnrichWithCloudOcr(parsed, options);
|
|
@@ -77,87 +79,12 @@ function parseFlowStdout(stdout) {
|
|
|
77
79
|
function hasPartialSendProgress(result) {
|
|
78
80
|
return Boolean(result.sentReplyObserved || result.sentAttachmentObserved);
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const request = selectCloudOcrRequest(result, mode);
|
|
83
|
-
if (!request || !options.cloudOcrUrl || !options.cloudOcrToken)
|
|
84
|
-
return result;
|
|
85
|
-
const imagePayload = await buildCloudOcrImagePayload(request.screenshotPath, options);
|
|
86
|
-
if (!imagePayload)
|
|
87
|
-
return result;
|
|
88
|
-
const response = await fetch(options.cloudOcrUrl, {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
headers: {
|
|
91
|
-
authorization: `Bearer ${options.cloudOcrToken}`,
|
|
92
|
-
'content-type': 'application/json',
|
|
93
|
-
},
|
|
94
|
-
body: JSON.stringify({
|
|
95
|
-
...imagePayload,
|
|
96
|
-
mimeType: 'image/png',
|
|
97
|
-
conversationName: options.groupName,
|
|
98
|
-
purpose: request.purpose,
|
|
99
|
-
channelId: options.cloudOcrChannelId,
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
102
|
-
if (!response.ok)
|
|
103
|
-
return result;
|
|
104
|
-
const payload = await response.json().catch(() => null);
|
|
105
|
-
const observations = Array.isArray(payload?.observations) ? payload.observations : [];
|
|
106
|
-
if (!observations.length)
|
|
107
|
-
return result;
|
|
108
|
-
const cloudMessages = messagesFromCloudOcrObservations(options.groupName, observations);
|
|
109
|
-
const recentLimit = Math.max(1, options.recentLimit || 5);
|
|
110
|
-
return {
|
|
111
|
-
...result,
|
|
112
|
-
cloudOcrPurpose: request.purpose,
|
|
113
|
-
cloudOcrObservations: observations,
|
|
114
|
-
...(payload?.requestId ? { cloudOcrRequestId: payload.requestId } : {}),
|
|
115
|
-
...(payload?.imageHash ? { cloudOcrImageHash: payload.imageHash } : {}),
|
|
116
|
-
...(payload?.usage ? { cloudOcrUsage: payload.usage } : {}),
|
|
117
|
-
newMessages: mergeCloudMessages(result.newMessages ?? [], cloudMessages),
|
|
118
|
-
recentMessages: mergeCloudMessages(result.recentMessages ?? [], cloudMessages).slice(-recentLimit),
|
|
119
|
-
};
|
|
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;
|
|
82
|
+
function hasSendAttempt(result) {
|
|
83
|
+
return Boolean(result.sentReply || result.sentAttachment || result.sentReplyObserved || result.sentAttachmentObserved);
|
|
149
84
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
}
|
|
85
|
+
async function maybeEnrichWithCloudOcr(result, options) {
|
|
86
|
+
void options;
|
|
87
|
+
return result;
|
|
161
88
|
}
|
|
162
89
|
export function selectCloudOcrRequest(result, mode) {
|
|
163
90
|
if (!shouldUseCloudOcr(result, mode))
|
|
@@ -4,6 +4,7 @@ export type WeChatRpaObservedMessage = {
|
|
|
4
4
|
text: string;
|
|
5
5
|
attachments?: WeChatRpaObservedAttachment[];
|
|
6
6
|
observedAt?: string | null;
|
|
7
|
+
isMentioned?: boolean | null;
|
|
7
8
|
rawId?: string | null;
|
|
8
9
|
};
|
|
9
10
|
export type WeChatRpaObservedAttachment = {
|
|
@@ -31,6 +32,7 @@ export type WeChatRpaNormalizedMessage = {
|
|
|
31
32
|
text: string;
|
|
32
33
|
attachments: WeChatRpaObservedAttachment[];
|
|
33
34
|
receivedAt: string;
|
|
35
|
+
isMentioned: boolean;
|
|
34
36
|
rawRef: string | null;
|
|
35
37
|
};
|
|
36
38
|
export declare class WeChatRpaDeduper {
|
|
@@ -41,5 +43,7 @@ export declare class WeChatRpaDeduper {
|
|
|
41
43
|
snapshot(): string[];
|
|
42
44
|
private trim;
|
|
43
45
|
}
|
|
44
|
-
export declare function normalizeWeChatRpaMessage(input: WeChatRpaObservedMessage
|
|
46
|
+
export declare function normalizeWeChatRpaMessage(input: WeChatRpaObservedMessage, options?: {
|
|
47
|
+
selfNicknames?: string[];
|
|
48
|
+
}): WeChatRpaNormalizedMessage | null;
|
|
45
49
|
export declare function weChatRpaConversationId(conversationName: string): string;
|