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.
@@ -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 click --x 480 --y 470 --no-raise
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
- channel.cloudOcrMode && channel.cloudOcrMode !== 'off'
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, opts.includeSecret),
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, opts.includeSecret),
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 = normalizeCloudOcrMode(input.cloudOcrMode ?? priorSecret?.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: input.cloudOcrUrl?.trim() || stringOrUndefined(priorSecret?.cloudOcrUrl) || cloudOcrDefaults.cloudOcrUrl,
372
- cloudOcrToken: input.cloudOcrToken?.trim() || stringOrUndefined(priorSecret?.cloudOcrToken) || cloudOcrDefaults.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, includeSecret) {
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
- cloudOcrUrl: typeof secret.cloudOcrUrl === 'string' ? secret.cloudOcrUrl : '',
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
- cloudOcrUrl: typeof secret.cloudOcrUrl === 'string' ? secret.cloudOcrUrl : null,
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' : 'macos-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
- async function maybeEnrichWithCloudOcr(result, options) {
81
- const mode = options.cloudOcrMode ?? 'off';
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
- 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
- }
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): WeChatRpaNormalizedMessage | null;
46
+ export declare function normalizeWeChatRpaMessage(input: WeChatRpaObservedMessage, options?: {
47
+ selfNicknames?: string[];
48
+ }): WeChatRpaNormalizedMessage | null;
45
49
  export declare function weChatRpaConversationId(conversationName: string): string;