shennian 0.2.89 → 0.2.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +13 -4
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/bin/shennian.js +1 -1
  4. package/dist/publish-build-manifest.json +548 -0
  5. package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
  6. package/dist/src/agent-env.js +4 -105
  7. package/dist/src/agents/adapter.js +1 -19
  8. package/dist/src/agents/claude.js +8 -305
  9. package/dist/src/agents/codex-control.js +2 -188
  10. package/dist/src/agents/codex-utils.js +7 -200
  11. package/dist/src/agents/codex.js +15 -916
  12. package/dist/src/agents/command-spec.js +2 -413
  13. package/dist/src/agents/config-status.js +1 -226
  14. package/dist/src/agents/cursor.js +1 -249
  15. package/dist/src/agents/custom.js +4 -271
  16. package/dist/src/agents/detect.js +1 -56
  17. package/dist/src/agents/external-channel-instructions.js +10 -94
  18. package/dist/src/agents/gemini.js +1 -173
  19. package/dist/src/agents/manager.js +13 -157
  20. package/dist/src/agents/model-registry/cache.js +1 -37
  21. package/dist/src/agents/model-registry/discovery.js +2 -187
  22. package/dist/src/agents/model-registry/parsers.js +4 -447
  23. package/dist/src/agents/model-registry/runner.js +1 -30
  24. package/dist/src/agents/model-registry/service.js +1 -78
  25. package/dist/src/agents/model-registry/types.js +1 -8
  26. package/dist/src/agents/model-registry.js +1 -18
  27. package/dist/src/agents/openclaw.js +2 -275
  28. package/dist/src/agents/opencode.js +1 -231
  29. package/dist/src/agents/pi-context.js +12 -217
  30. package/dist/src/agents/pi.js +14 -723
  31. package/dist/src/agents/platform-instructions.js +9 -54
  32. package/dist/src/channels/base.js +1 -3
  33. package/dist/src/channels/registry.js +1 -30
  34. package/dist/src/channels/reply-split.js +10 -89
  35. package/dist/src/channels/runtime.js +5 -564
  36. package/dist/src/channels/secret-registry.js +1 -46
  37. package/dist/src/channels/websocket.js +8 -378
  38. package/dist/src/channels/wechat-channel/anchor.js +1 -65
  39. package/dist/src/channels/wechat-channel/client.js +1 -96
  40. package/dist/src/channels/wechat-channel/cooldown.js +1 -38
  41. package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
  42. package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
  43. package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
  44. package/dist/src/channels/wechat-channel/helper-client.js +3 -149
  45. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
  46. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
  47. package/dist/src/channels/wechat-channel/index.d.ts +1 -0
  48. package/dist/src/channels/wechat-channel/index.js +1 -19
  49. package/dist/src/channels/wechat-channel/ledger.js +1 -54
  50. package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
  51. package/dist/src/channels/wechat-channel/message-key.js +1 -105
  52. package/dist/src/channels/wechat-channel/observer.js +1 -118
  53. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
  54. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
  55. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  56. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  57. package/dist/src/channels/wechat-channel/preflight.js +1 -48
  58. package/dist/src/channels/wechat-channel/runner.js +1 -84
  59. package/dist/src/channels/wechat-channel/runtime.js +1 -66
  60. package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
  61. package/dist/src/channels/wechat-channel/scheduler.js +1 -152
  62. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  63. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  64. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  65. package/dist/src/channels/wechat-rpa.js +6 -1028
  66. package/dist/src/channels/wecom.js +4 -357
  67. package/dist/src/commands/agent.js +6 -131
  68. package/dist/src/commands/daemon-windows.js +8 -48
  69. package/dist/src/commands/daemon.js +19 -1013
  70. package/dist/src/commands/external-attachments.js +1 -51
  71. package/dist/src/commands/external.js +1 -137
  72. package/dist/src/commands/manager.js +2 -391
  73. package/dist/src/commands/pair-qr.js +1 -6
  74. package/dist/src/commands/pair.js +9 -287
  75. package/dist/src/commands/tools.js +1 -34
  76. package/dist/src/commands/upgrade.js +1 -198
  77. package/dist/src/config/index.js +1 -35
  78. package/dist/src/daemon-log.js +6 -58
  79. package/dist/src/env-path.js +1 -64
  80. package/dist/src/fs/boundary.js +1 -126
  81. package/dist/src/fs/handler.js +1 -130
  82. package/dist/src/fs/security.js +1 -32
  83. package/dist/src/fs/text-decoder.js +1 -110
  84. package/dist/src/index.js +2 -404
  85. package/dist/src/log-reporter.js +1 -16
  86. package/dist/src/manager/prompt.js +29 -34
  87. package/dist/src/manager/registry.js +2 -269
  88. package/dist/src/manager/runtime.js +19 -1007
  89. package/dist/src/native-fusion/config.js +1 -5
  90. package/dist/src/native-fusion/opencode-parser.js +3 -123
  91. package/dist/src/native-fusion/parser-common.js +8 -264
  92. package/dist/src/native-fusion/parsers.js +8 -729
  93. package/dist/src/native-fusion/service.js +2 -225
  94. package/dist/src/native-fusion/state.js +1 -22
  95. package/dist/src/native-fusion/types.js +1 -1
  96. package/dist/src/region.js +1 -88
  97. package/dist/src/relay/client.js +1 -343
  98. package/dist/src/session/archive-zip.js +1 -220
  99. package/dist/src/session/handlers/agent-config.js +1 -150
  100. package/dist/src/session/handlers/agents.js +1 -55
  101. package/dist/src/session/handlers/chat.js +2 -751
  102. package/dist/src/session/handlers/control.js +1 -55
  103. package/dist/src/session/handlers/fs.js +1 -783
  104. package/dist/src/session/handlers/session-refresh.js +1 -47
  105. package/dist/src/session/handlers/skills.js +1 -121
  106. package/dist/src/session/handlers/title.js +1 -60
  107. package/dist/src/session/handlers/tool-detail.js +1 -218
  108. package/dist/src/session/manager.js +1 -319
  109. package/dist/src/session/projection.js +1 -54
  110. package/dist/src/session/queue.js +4 -317
  111. package/dist/src/session/remote-attachments.js +1 -72
  112. package/dist/src/session/store.js +3 -109
  113. package/dist/src/session/types.js +1 -4
  114. package/dist/src/skills/registry.js +15 -148
  115. package/dist/src/skills/setup.js +1 -101
  116. package/dist/src/tools/markdown-to-pdf.js +10 -346
  117. package/dist/src/upgrade/engine.js +3 -347
  118. package/package.json +3 -2
@@ -1,181 +1 @@
1
- // @arch docs/features/wechat-rpa-productization-plan.md
2
- // @arch docs/features/wechat-rpa-helper-runtime.md
3
- // @test src/__tests__/wechat-channel-media-resolver.test.ts
4
- import crypto from 'node:crypto';
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- const MAX_INBOUND_ATTACHMENT_BYTES = 20 * 1024 * 1024;
8
- export function defaultWeChatChannelAttachmentDir(workDir, runtimeId, bindingId) {
9
- const key = crypto.createHash('sha256').update(`${runtimeId}:${bindingId}`).digest('hex').slice(0, 16);
10
- return path.join(workDir, '.uploads', 'wechat-channel', 'inbound', key);
11
- }
12
- export async function resolveVisibleWeChatChannelMedia(input) {
13
- const results = [];
14
- for (const candidate of input.candidates) {
15
- if (!isDownloadableCandidate(candidate)) {
16
- results.push(metadataOnlyResult(candidate, 'unsupported_media_kind'));
17
- continue;
18
- }
19
- if (!candidate.bbox) {
20
- results.push(metadataOnlyResult(candidate, 'media_bbox_missing'));
21
- continue;
22
- }
23
- const snapshot = await input.helper.request('clipboard.snapshot', {}, input.traceId);
24
- assertHelperOk(snapshot, 'clipboard.snapshot');
25
- try {
26
- const center = bboxCenter(candidate.bbox);
27
- const rightClick = await input.helper.request('mouse.rightClick', {
28
- x: center.x,
29
- y: center.y,
30
- coordinateSpace: center.coordinateSpace,
31
- windowId: input.windowId,
32
- }, input.traceId);
33
- assertHelperOk(rightClick, 'mouse.rightClick');
34
- const picked = await input.helper.request('menu.pickItem', {
35
- labels: menuLabelsForCandidate(candidate),
36
- disallowLabels: ['保存', '另存为', 'Save As', 'Save to Downloads'],
37
- }, input.traceId);
38
- assertHelperOk(picked, 'menu.pickItem');
39
- const fileUrls = await input.helper.request('clipboard.readFileUrls', {}, input.traceId);
40
- assertHelperOk(fileUrls, 'clipboard.readFileUrls');
41
- const localPath = firstClipboardPath(fileUrls.result);
42
- if (!localPath) {
43
- results.push(metadataOnlyResult(candidate, 'clipboard_file_url_unavailable'));
44
- continue;
45
- }
46
- results.push({
47
- messageKey: candidate.messageKey,
48
- attachment: materializeLocalAttachment(candidate, localPath, input.attachmentsDir),
49
- reasonCode: 'edge_local',
50
- });
51
- }
52
- finally {
53
- const restore = await input.helper.request('clipboard.restore', snapshot.result && typeof snapshot.result === 'object' ? snapshot.result : {}, input.traceId);
54
- if (!restore.ok) {
55
- results.push(metadataOnlyResult(candidate, restore.errorCode || 'clipboard_restore_failed'));
56
- }
57
- }
58
- }
59
- return results;
60
- }
61
- export function materializeLocalAttachment(candidate, sourcePath, attachmentsDir) {
62
- const stat = fs.statSync(sourcePath);
63
- if (!stat.isFile())
64
- throw new Error('wechat_channel_attachment_not_file');
65
- if (stat.size > MAX_INBOUND_ATTACHMENT_BYTES) {
66
- return {
67
- type: normalizeAttachmentType(candidate.kind),
68
- name: safeFileName(candidate.fileName || path.basename(sourcePath)),
69
- size: stat.size,
70
- mimeType: candidate.mimeType || mimeFromPath(sourcePath),
71
- availability: 'unavailable-large',
72
- providerError: 'attachment_too_large',
73
- };
74
- }
75
- fs.mkdirSync(attachmentsDir, { recursive: true });
76
- const buffer = fs.readFileSync(sourcePath);
77
- const hash = crypto.createHash('sha256').update(buffer).digest('hex');
78
- const name = safeFileName(candidate.fileName || path.basename(sourcePath));
79
- const ext = path.extname(name) || path.extname(sourcePath);
80
- const stem = ext ? name.slice(0, -ext.length) : name;
81
- const targetPath = path.join(attachmentsDir, `${stem}-${hash.slice(0, 12)}${ext}`);
82
- if (!fs.existsSync(targetPath))
83
- fs.writeFileSync(targetPath, buffer);
84
- return {
85
- type: normalizeAttachmentType(candidate.kind),
86
- name,
87
- mimeType: candidate.mimeType || mimeFromPath(sourcePath),
88
- size: buffer.byteLength,
89
- localPath: targetPath,
90
- hash,
91
- availability: 'edge-local',
92
- };
93
- }
94
- export function metadataOnlyResult(candidate, reasonCode) {
95
- return {
96
- messageKey: candidate.messageKey,
97
- reasonCode,
98
- attachment: {
99
- type: normalizeAttachmentType(candidate.kind),
100
- name: safeFileName(candidate.fileName || `${candidate.kind || 'attachment'}`),
101
- ...(candidate.mimeType ? { mimeType: candidate.mimeType } : {}),
102
- ...(Number.isFinite(candidate.size) ? { size: Number(candidate.size) } : {}),
103
- availability: candidate.mediaStatus === 'not_downloaded' ? 'pending-download' : 'metadata-only',
104
- providerError: reasonCode,
105
- },
106
- };
107
- }
108
- function isDownloadableCandidate(candidate) {
109
- return candidateMediaType(candidate.kind) !== null;
110
- }
111
- function candidateMediaType(kind) {
112
- const normalized = String(kind || '').toLowerCase();
113
- if (normalized.includes('video'))
114
- return 'video';
115
- if (normalized.includes('image') || normalized.includes('photo'))
116
- return 'image';
117
- if (normalized.includes('file') || normalized.includes('document'))
118
- return 'file';
119
- return null;
120
- }
121
- function normalizeAttachmentType(kind) {
122
- const mediaType = candidateMediaType(kind);
123
- if (mediaType)
124
- return mediaType;
125
- return 'file';
126
- }
127
- function menuLabelsForCandidate(candidate) {
128
- const type = normalizeAttachmentType(candidate.kind);
129
- if (type === 'image')
130
- return ['复制图片', '复制', 'Copy Image', 'Copy'];
131
- if (type === 'video')
132
- return ['复制', 'Copy'];
133
- return ['复制', 'Copy'];
134
- }
135
- function firstClipboardPath(result) {
136
- const raw = result?.filePaths?.[0] || result?.fileUrls?.[0];
137
- if (!raw)
138
- return null;
139
- if (raw.startsWith('file://'))
140
- return decodeURIComponent(new URL(raw).pathname);
141
- return raw;
142
- }
143
- function bboxCenter(bbox) {
144
- return {
145
- x: bbox.x + bbox.width / 2,
146
- y: bbox.y + bbox.height / 2,
147
- coordinateSpace: bbox.coordinateSpace,
148
- };
149
- }
150
- function safeFileName(name) {
151
- return path.basename(name || 'attachment')
152
- .normalize('NFKC')
153
- .replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
154
- .replace(/\s+/g, ' ')
155
- .replace(/^[ ._]+|[ ._]+$/g, '')
156
- || 'attachment';
157
- }
158
- function mimeFromPath(filePath) {
159
- const ext = path.extname(filePath).toLowerCase();
160
- if (ext === '.png')
161
- return 'image/png';
162
- if (ext === '.jpg' || ext === '.jpeg')
163
- return 'image/jpeg';
164
- if (ext === '.gif')
165
- return 'image/gif';
166
- if (ext === '.webp')
167
- return 'image/webp';
168
- if (ext === '.mp4')
169
- return 'video/mp4';
170
- if (ext === '.mov')
171
- return 'video/quicktime';
172
- if (ext === '.pdf')
173
- return 'application/pdf';
174
- if (ext === '.txt')
175
- return 'text/plain';
176
- return 'application/octet-stream';
177
- }
178
- function assertHelperOk(response, command) {
179
- if (!response.ok)
180
- throw new Error(`${response.errorCode || 'helper_command_failed'}: ${response.errorSummary || command}`);
181
- }
1
+ import y from"node:crypto";import s from"node:fs";import n from"node:path";const w=20*1024*1024;function z(e,t,r){const i=y.createHash("sha256").update(`${t}:${r}`).digest("hex").slice(0,16);return n.join(e,".uploads","wechat-channel","inbound",i)}async function F(e){const t=[];for(const r of e.candidates){if(!C(r)){t.push(p(r,"unsupported_media_kind"));continue}if(!r.bbox){t.push(p(r,"media_bbox_missing"));continue}const i=await e.helper.request("clipboard.snapshot",{},e.traceId);d(i,"clipboard.snapshot");try{const a=v(r.bbox),c=await e.helper.request("mouse.rightClick",{x:a.x,y:a.y,coordinateSpace:a.coordinateSpace,windowId:e.windowId},e.traceId);d(c,"mouse.rightClick");const o=await e.helper.request("menu.pickItem",{labels:x(r),disallowLabels:["\u4FDD\u5B58","\u53E6\u5B58\u4E3A","Save As","Save to Downloads"]},e.traceId);d(o,"menu.pickItem");const l=await e.helper.request("clipboard.readFileUrls",{},e.traceId);d(l,"clipboard.readFileUrls");const m=k(l.result);if(!m){t.push(p(r,"clipboard_file_url_unavailable"));continue}t.push({messageKey:r.messageKey,attachment:_(r,m,e.attachmentsDir),reasonCode:"edge_local"})}finally{const a=await e.helper.request("clipboard.restore",i.result&&typeof i.result=="object"?i.result:{},e.traceId);a.ok||t.push(p(r,a.errorCode||"clipboard_restore_failed"))}}return t}function _(e,t,r){const i=s.statSync(t);if(!i.isFile())throw new Error("wechat_channel_attachment_not_file");if(i.size>w)return{type:u(e.kind),name:h(e.fileName||n.basename(t)),size:i.size,mimeType:e.mimeType||b(t),availability:"unavailable-large",providerError:"attachment_too_large"};s.mkdirSync(r,{recursive:!0});const a=s.readFileSync(t),c=y.createHash("sha256").update(a).digest("hex"),o=h(e.fileName||n.basename(t)),l=n.extname(o)||n.extname(t),m=l?o.slice(0,-l.length):o,f=n.join(r,`${m}-${c.slice(0,12)}${l}`);return s.existsSync(f)||s.writeFileSync(f,a),{type:u(e.kind),name:o,mimeType:e.mimeType||b(t),size:a.byteLength,localPath:f,hash:c,availability:"edge-local"}}function p(e,t){return{messageKey:e.messageKey,reasonCode:t,attachment:{type:u(e.kind),name:h(e.fileName||`${e.kind||"attachment"}`),...e.mimeType?{mimeType:e.mimeType}:{},...Number.isFinite(e.size)?{size:Number(e.size)}:{},availability:e.mediaStatus==="not_downloaded"?"pending-download":"metadata-only",providerError:t}}}function C(e){return g(e.kind)!==null}function g(e){const t=String(e||"").toLowerCase();return t.includes("video")?"video":t.includes("image")||t.includes("photo")?"image":t.includes("file")||t.includes("document")?"file":null}function u(e){const t=g(e);return t||"file"}function x(e){const t=u(e.kind);return t==="image"?["\u590D\u5236\u56FE\u7247","\u590D\u5236","Copy Image","Copy"]:t==="video"?["\u590D\u5236","Copy"]:["\u590D\u5236","Copy"]}function k(e){const t=e?.filePaths?.[0]||e?.fileUrls?.[0];return t?t.startsWith("file://")?decodeURIComponent(new URL(t).pathname):t:null}function v(e){return{x:e.x+e.width/2,y:e.y+e.height/2,coordinateSpace:e.coordinateSpace}}function h(e){return n.basename(e||"attachment").normalize("NFKC").replace(/[<>:"/\\|?*\x00-\x1F]/g,"_").replace(/\s+/g," ").replace(/^[ ._]+|[ ._]+$/g,"")||"attachment"}function b(e){const t=n.extname(e).toLowerCase();return t===".png"?"image/png":t===".jpg"||t===".jpeg"?"image/jpeg":t===".gif"?"image/gif":t===".webp"?"image/webp":t===".mp4"?"video/mp4":t===".mov"?"video/quicktime":t===".pdf"?"application/pdf":t===".txt"?"text/plain":"application/octet-stream"}function d(e,t){if(!e.ok)throw new Error(`${e.errorCode||"helper_command_failed"}: ${e.errorSummary||t}`)}export{z as defaultWeChatChannelAttachmentDir,_ as materializeLocalAttachment,p as metadataOnlyResult,F as resolveVisibleWeChatChannelMedia};
@@ -1,105 +1 @@
1
- // @arch docs/features/wechat-rpa-productization-plan.md
2
- // @test src/__tests__/wechat-channel-message-key.test.ts
3
- import crypto from 'node:crypto';
4
- import { normalizeWeChatAnchorText } from './anchor.js';
5
- export function normalizeWeChatObservedWindowForLedger(messages) {
6
- const baseCounts = new Map();
7
- return messages.map((message, windowIndex) => {
8
- const base = stableMessageBase(message, windowIndex);
9
- const occurrence = baseCounts.get(base) ?? 0;
10
- baseCounts.set(base, occurrence + 1);
11
- const anchorMetadata = buildAnchorMetadata(message, windowIndex, occurrence);
12
- const stableMessageKey = normalizeExplicitKey(message.stableMessageKey) || hashStableMessageKey({ base, occurrence });
13
- const anchorText = message.anchorText || message.normalizedText || message.textExcerpt || '';
14
- return {
15
- ...message,
16
- stableMessageKey,
17
- stableKeyVersion: 1,
18
- anchorText,
19
- anchorMetadata: {
20
- ...(isRecord(message.anchorMetadata) ? message.anchorMetadata : {}),
21
- ...anchorMetadata,
22
- },
23
- };
24
- });
25
- }
26
- export function buildStableWeChatMessageKey(message, windowIndex = 0, occurrence = 0) {
27
- return normalizeExplicitKey(message.stableMessageKey) || hashStableMessageKey({
28
- base: stableMessageBase(message, windowIndex),
29
- occurrence,
30
- });
31
- }
32
- export function buildAnchorMetadata(message, windowIndex, occurrence) {
33
- return {
34
- stableKeyVersion: 1,
35
- windowIndex,
36
- occurrence,
37
- senderRole: message.senderRole,
38
- kind: message.kind,
39
- anchorText: normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || ''),
40
- bboxBand: bboxBand(message.bbox),
41
- mediaSignature: mediaSignature(message),
42
- };
43
- }
44
- function stableMessageBase(message, windowIndex) {
45
- return JSON.stringify({
46
- senderRole: message.senderRole || 'unknown',
47
- senderName: normalizeWeChatAnchorText(message.senderName || ''),
48
- kind: message.kind || 'text',
49
- anchorText: normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || ''),
50
- bboxBand: bboxBand(message.bbox),
51
- mediaSignature: mediaSignature(message),
52
- neighborSignature: neighborSignature(message.neighborContext),
53
- orderBand: Math.floor(windowIndex / 4),
54
- });
55
- }
56
- function hashStableMessageKey(input) {
57
- return `wk1_${crypto.createHash('sha256').update(`${input.base}|${input.occurrence}`).digest('hex').slice(0, 24)}`;
58
- }
59
- function normalizeExplicitKey(value) {
60
- if (typeof value !== 'string')
61
- return null;
62
- const trimmed = value.trim();
63
- return trimmed || null;
64
- }
65
- function bboxBand(value) {
66
- if (!isRecord(value))
67
- return null;
68
- const x = numberPart(value.x);
69
- const y = numberPart(value.y);
70
- const width = numberPart(value.width);
71
- const height = numberPart(value.height);
72
- if ([x, y, width, height].some((part) => part === null))
73
- return null;
74
- return [x, y, width, height].map((part) => Math.round((part ?? 0) / 20) * 20).join(',');
75
- }
76
- function mediaSignature(message) {
77
- const metadata = isRecord(message.mediaMetadata) ? message.mediaMetadata : {};
78
- const visualBlocks = Array.isArray(message.visualBlocks) ? message.visualBlocks : [];
79
- const parts = [
80
- stringPart(metadata.fileName),
81
- stringPart(metadata.mimeType),
82
- stringPart(metadata.size),
83
- ...visualBlocks.map((block) => `${block.blockKind}:${block.blockId}`),
84
- ].filter(Boolean);
85
- return parts.length ? parts.join('|') : null;
86
- }
87
- function neighborSignature(value) {
88
- if (!isRecord(value))
89
- return null;
90
- const before = normalizeWeChatAnchorText(value.beforeText || value.previousText || value.prev);
91
- const after = normalizeWeChatAnchorText(value.afterText || value.nextText || value.next);
92
- return before || after ? `${before.slice(0, 24)}|${after.slice(0, 24)}` : null;
93
- }
94
- function numberPart(value) {
95
- const number = Number(value);
96
- return Number.isFinite(number) ? number : null;
97
- }
98
- function stringPart(value) {
99
- if (value === undefined || value === null)
100
- return null;
101
- return String(value).trim() || null;
102
- }
103
- function isRecord(value) {
104
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
105
- }
1
+ import M from"node:crypto";import{normalizeWeChatAnchorText as a}from"./anchor.js";function K(t){const n=new Map;return t.map((r,o)=>{const e=l(r,o),i=n.get(e)??0;n.set(e,i+1);const s=T(r,o,i),p=x(r.stableMessageKey)||b({base:e,occurrence:i}),y=r.anchorText||r.normalizedText||r.textExcerpt||"";return{...r,stableMessageKey:p,stableKeyVersion:1,anchorText:y,anchorMetadata:{...u(r.anchorMetadata)?r.anchorMetadata:{},...s}}})}function S(t,n=0,r=0){return x(t.stableMessageKey)||b({base:l(t,n),occurrence:r})}function T(t,n,r){return{stableKeyVersion:1,windowIndex:n,occurrence:r,senderRole:t.senderRole,kind:t.kind,anchorText:a(t.anchorText||t.normalizedText||t.textExcerpt||""),bboxBand:h(t.bbox),mediaSignature:f(t)}}function l(t,n){return JSON.stringify({senderRole:t.senderRole||"unknown",senderName:a(t.senderName||""),kind:t.kind||"text",anchorText:a(t.anchorText||t.normalizedText||t.textExcerpt||""),bboxBand:h(t.bbox),mediaSignature:f(t),neighborSignature:k(t.neighborContext),orderBand:Math.floor(n/4)})}function b(t){return`wk1_${M.createHash("sha256").update(`${t.base}|${t.occurrence}`).digest("hex").slice(0,24)}`}function x(t){return typeof t!="string"?null:t.trim()||null}function h(t){if(!u(t))return null;const n=c(t.x),r=c(t.y),o=c(t.width),e=c(t.height);return[n,r,o,e].some(i=>i===null)?null:[n,r,o,e].map(i=>Math.round((i??0)/20)*20).join(",")}function f(t){const n=u(t.mediaMetadata)?t.mediaMetadata:{},r=Array.isArray(t.visualBlocks)?t.visualBlocks:[],o=[d(n.fileName),d(n.mimeType),d(n.size),...r.map(e=>`${e.blockKind}:${e.blockId}`)].filter(Boolean);return o.length?o.join("|"):null}function k(t){if(!u(t))return null;const n=a(t.beforeText||t.previousText||t.prev),r=a(t.afterText||t.nextText||t.next);return n||r?`${n.slice(0,24)}|${r.slice(0,24)}`:null}function c(t){const n=Number(t);return Number.isFinite(n)?n:null}function d(t){return t==null?null:String(t).trim()||null}function u(t){return!!t&&typeof t=="object"&&!Array.isArray(t)}export{T as buildAnchorMetadata,S as buildStableWeChatMessageKey,K as normalizeWeChatObservedWindowForLedger};
@@ -1,118 +1 @@
1
- // @arch docs/features/wechat-rpa-productization-plan.md
2
- // @arch docs/features/wechat-rpa-helper-runtime.md
3
- // @test src/__tests__/wechat-channel-observer.test.ts
4
- import { matchWeChatConversationFingerprints, } from './fingerprint.js';
5
- export async function observeWeChatChannelBindingViaHelper(options) {
6
- await ensureHelperPreflight(options.helper, options.traceId);
7
- const window = await focusWeChatWindow(options.helper, options.traceId);
8
- await openConversationInVisibleList({
9
- helper: options.helper,
10
- window,
11
- binding: options.binding,
12
- traceId: options.traceId,
13
- });
14
- const capture = await captureWeChatWindow(options.helper, window.windowId, options.traceId);
15
- const ocr = await recognizeWeChatScreenshot(options.helper, capture, options.traceId);
16
- const decision = await options.api.observe(options.runtime, options.binding, {
17
- screenshots: [{
18
- captureIndex: 0,
19
- mimeType: capture.mimeType,
20
- dataBase64: capture.dataBase64,
21
- width: capture.width,
22
- height: capture.height,
23
- }],
24
- edgeOcrBlocks: ocr.blocks ?? [],
25
- visibleConversationFingerprints: ocr.visibleConversationFingerprints ?? [],
26
- localLedgerTailAnchors: options.localLedgerTailAnchors ?? [],
27
- });
28
- return decision.observedMessages ?? [];
29
- }
30
- export async function ensureHelperPreflight(helper, traceId) {
31
- const response = await helper.request('permissions.check', {}, traceId);
32
- assertHelperOk(response, 'permissions.check');
33
- const result = response.result ?? {};
34
- if (result.screenRecording === false)
35
- throw new Error('permission_screen_recording_missing');
36
- if (result.accessibility === false)
37
- throw new Error('permission_accessibility_missing');
38
- if (result.automation === false)
39
- throw new Error('permission_automation_missing');
40
- if (result.wechatRunning === false)
41
- throw new Error('wechat_not_running');
42
- if (result.wechatWindowAvailable === false)
43
- throw new Error('wechat_window_unavailable');
44
- }
45
- export async function focusWeChatWindow(helper, traceId) {
46
- const response = await helper.request('windows.list', {}, traceId);
47
- assertHelperOk(response, 'windows.list');
48
- const window = (response.result?.windows ?? []).find(isWeChatWindow);
49
- if (!window)
50
- throw new Error('wechat_window_unavailable');
51
- const focus = await helper.request('windows.focus', { windowId: window.windowId }, traceId);
52
- assertHelperOk(focus, 'windows.focus');
53
- return window;
54
- }
55
- export async function openConversationInVisibleList(input) {
56
- const capture = await captureWeChatWindow(input.helper, input.window.windowId, input.traceId);
57
- const ocr = await recognizeWeChatScreenshot(input.helper, capture, input.traceId);
58
- const match = matchWeChatConversationFingerprints({
59
- visibleItems: ocr.visibleConversationFingerprints ?? [],
60
- targets: [{
61
- bindingId: input.binding.bindingId,
62
- conversationDisplayName: input.binding.conversationDisplayName,
63
- }],
64
- })[0];
65
- if (!match)
66
- return { opened: false, reason: 'conversation_not_visible' };
67
- const center = bboxCenter(match.item.bbox);
68
- if (!center)
69
- return { opened: false, reason: 'conversation_bbox_missing' };
70
- const click = await input.helper.request('mouse.click', {
71
- x: center.x,
72
- y: center.y,
73
- coordinateSpace: center.coordinateSpace,
74
- windowId: input.window.windowId,
75
- }, input.traceId);
76
- assertHelperOk(click, 'mouse.click');
77
- return { opened: true, reason: match.reason };
78
- }
79
- export async function captureWeChatWindow(helper, windowId, traceId) {
80
- const response = await helper.request('windows.capture', { windowId, scope: 'full-window' }, traceId);
81
- assertHelperOk(response, 'windows.capture');
82
- const result = response.result;
83
- if (!result?.dataBase64 || !result.mimeType || !result.width || !result.height) {
84
- throw new Error('helper_invalid_response: windows.capture missing screenshot data');
85
- }
86
- return result;
87
- }
88
- export async function recognizeWeChatScreenshot(helper, screenshot, traceId) {
89
- const response = await helper.request('ocr.recognize', {
90
- mimeType: screenshot.mimeType,
91
- dataBase64: screenshot.dataBase64,
92
- width: screenshot.width,
93
- height: screenshot.height,
94
- }, traceId);
95
- assertHelperOk(response, 'ocr.recognize');
96
- return response.result ?? {};
97
- }
98
- function assertHelperOk(response, command) {
99
- if (!response.ok)
100
- throw new Error(`${response.errorCode || 'helper_command_failed'}: ${response.errorSummary || command}`);
101
- }
102
- function isWeChatWindow(window) {
103
- const haystack = `${window.appName || ''} ${window.title || ''}`.toLowerCase();
104
- return haystack.includes('wechat') || haystack.includes('微信');
105
- }
106
- function bboxCenter(value) {
107
- if (!value || typeof value !== 'object')
108
- return null;
109
- const bbox = value;
110
- const x = Number(bbox.x);
111
- const y = Number(bbox.y);
112
- const width = Number(bbox.width);
113
- const height = Number(bbox.height);
114
- if (![x, y, width, height].every(Number.isFinite))
115
- return null;
116
- const coordinateSpace = typeof bbox.coordinateSpace === 'string' ? bbox.coordinateSpace : undefined;
117
- return { x: x + width / 2, y: y + height / 2, coordinateSpace };
118
- }
1
+ import{matchWeChatConversationFingerprints as d}from"./fingerprint.js";async function p(e){await l(e.helper,e.traceId);const r=await h(e.helper,e.traceId);await u({helper:e.helper,window:r,binding:e.binding,traceId:e.traceId});const i=await s(e.helper,r.windowId,e.traceId),n=await c(e.helper,i,e.traceId);return(await e.api.observe(e.runtime,e.binding,{screenshots:[{captureIndex:0,mimeType:i.mimeType,dataBase64:i.dataBase64,width:i.width,height:i.height}],edgeOcrBlocks:n.blocks??[],visibleConversationFingerprints:n.visibleConversationFingerprints??[],localLedgerTailAnchors:e.localLedgerTailAnchors??[]})).observedMessages??[]}async function l(e,r){const i=await e.request("permissions.check",{},r);a(i,"permissions.check");const n=i.result??{};if(n.screenRecording===!1)throw new Error("permission_screen_recording_missing");if(n.accessibility===!1)throw new Error("permission_accessibility_missing");if(n.automation===!1)throw new Error("permission_automation_missing");if(n.wechatRunning===!1)throw new Error("wechat_not_running");if(n.wechatWindowAvailable===!1)throw new Error("wechat_window_unavailable")}async function h(e,r){const i=await e.request("windows.list",{},r);a(i,"windows.list");const n=(i.result?.windows??[]).find(m);if(!n)throw new Error("wechat_window_unavailable");const t=await e.request("windows.focus",{windowId:n.windowId},r);return a(t,"windows.focus"),n}async function u(e){const r=await s(e.helper,e.window.windowId,e.traceId),i=await c(e.helper,r,e.traceId),n=d({visibleItems:i.visibleConversationFingerprints??[],targets:[{bindingId:e.binding.bindingId,conversationDisplayName:e.binding.conversationDisplayName}]})[0];if(!n)return{opened:!1,reason:"conversation_not_visible"};const t=f(n.item.bbox);if(!t)return{opened:!1,reason:"conversation_bbox_missing"};const o=await e.helper.request("mouse.click",{x:t.x,y:t.y,coordinateSpace:t.coordinateSpace,windowId:e.window.windowId},e.traceId);return a(o,"mouse.click"),{opened:!0,reason:n.reason}}async function s(e,r,i){const n=await e.request("windows.capture",{windowId:r,scope:"full-window"},i);a(n,"windows.capture");const t=n.result;if(!t?.dataBase64||!t.mimeType||!t.width||!t.height)throw new Error("helper_invalid_response: windows.capture missing screenshot data");return t}async function c(e,r,i){const n=await e.request("ocr.recognize",{mimeType:r.mimeType,dataBase64:r.dataBase64,width:r.width,height:r.height},i);return a(n,"ocr.recognize"),n.result??{}}function a(e,r){if(!e.ok)throw new Error(`${e.errorCode||"helper_command_failed"}: ${e.errorSummary||r}`)}function m(e){const r=`${e.appName||""} ${e.title||""}`.toLowerCase();return r.includes("wechat")||r.includes("\u5FAE\u4FE1")}function f(e){if(!e||typeof e!="object")return null;const r=e,i=Number(r.x),n=Number(r.y),t=Number(r.width),o=Number(r.height);if(![i,n,t,o].every(Number.isFinite))return null;const w=typeof r.coordinateSpace=="string"?r.coordinateSpace:void 0;return{x:i+t/2,y:n+o/2,coordinateSpace:w}}export{s as captureWeChatWindow,l as ensureHelperPreflight,h as focusWeChatWindow,p as observeWeChatChannelBindingViaHelper,u as openConversationInVisibleList,c as recognizeWeChatScreenshot};
@@ -8,6 +8,7 @@ export type WeChatChannelOutboundRecord = {
8
8
  sessionId: string;
9
9
  conversationName: string;
10
10
  replyBaseRevision: number;
11
+ text?: string;
11
12
  textNormalized?: string;
12
13
  attachmentLocalRefs?: string[];
13
14
  createdAt: string;
@@ -56,7 +57,9 @@ export declare function guardWeChatOutboundRevision(input: {
56
57
  ok: false;
57
58
  reason: 'stale';
58
59
  };
60
+ export declare function markWeChatOutboundSending(record: WeChatChannelOutboundRecord): void;
59
61
  export declare function markWeChatOutboundSentUnconfirmed(record: WeChatChannelOutboundRecord, now?: Date): void;
62
+ export declare function markWeChatOutboundFailed(record: WeChatChannelOutboundRecord, failureCode: string, errorSummary: string): void;
60
63
  export declare function classifyWeChatOutboundEchoes(input: {
61
64
  ledger: WeChatChannelOutboundLedger;
62
65
  bindingId: string;
@@ -1,112 +1,2 @@
1
- // @arch docs/features/wechat-rpa-outbound-ledger.md
2
- // @test src/__tests__/wechat-channel-outbound-ledger.test.ts
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { normalizeWeChatAnchorText, weChatTextSimilarity } from './anchor.js';
6
- const MAX_OUTBOUND_RECORDS = 500;
7
- const MANUAL_REVIEW_AFTER_OBSERVE_ATTEMPTS = 2;
8
- const MANUAL_REVIEW_AFTER_MS = 10 * 60 * 1000;
9
- export function loadWeChatChannelOutboundLedger(filePath, runtimeId) {
10
- try {
11
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
12
- if (parsed?.version === 1 && parsed.runtimeId === runtimeId && Array.isArray(parsed.records))
13
- return parsed;
14
- }
15
- catch { }
16
- return { version: 1, runtimeId, records: [] };
17
- }
18
- export function saveWeChatChannelOutboundLedger(filePath, ledger) {
19
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
- ledger.records = ledger.records.slice(-MAX_OUTBOUND_RECORDS);
21
- fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2));
22
- }
23
- export function enqueueWeChatOutboundReply(input) {
24
- const existing = input.ledger.records.find((record) => record.idempotencyKey === input.idempotencyKey);
25
- if (existing)
26
- return existing;
27
- const now = (input.now ?? new Date()).toISOString();
28
- const textNormalized = normalizeWeChatAnchorText(input.text);
29
- const record = {
30
- replyId: input.replyId,
31
- idempotencyKey: input.idempotencyKey,
32
- bindingId: input.bindingId,
33
- runtimeId: input.runtimeId,
34
- sessionId: input.sessionId,
35
- conversationName: input.conversationName,
36
- replyBaseRevision: input.replyBaseRevision,
37
- textNormalized: textNormalized || undefined,
38
- attachmentLocalRefs: input.attachmentLocalRefs ?? [],
39
- createdAt: now,
40
- queuedAt: now,
41
- sendStatus: 'queued',
42
- expectedEchoAnchor: textNormalized || undefined,
43
- observeAttemptsAfterSent: 0,
44
- };
45
- input.ledger.records.push(record);
46
- return record;
47
- }
48
- export function guardWeChatOutboundRevision(input) {
49
- if (input.currentLastInboundRevision <= input.record.replyBaseRevision)
50
- return { ok: true };
51
- input.record.sendStatus = 'stale';
52
- input.record.failureCode = 'reply_revision_stale';
53
- input.record.lastErrorSummary = 'Inbound revision advanced before reply send';
54
- input.record.confirmedAt = input.record.confirmedAt;
55
- return { ok: false, reason: 'stale' };
56
- }
57
- export function markWeChatOutboundSentUnconfirmed(record, now = new Date()) {
58
- record.sendStatus = 'sent_unconfirmed';
59
- record.sentAt = now.toISOString();
60
- record.observeAttemptsAfterSent = 0;
61
- }
62
- export function classifyWeChatOutboundEchoes(input) {
63
- const now = input.now ?? new Date();
64
- const confirmedRecords = [];
65
- const manualReviewRecords = [];
66
- const pendingRecords = input.ledger.records.filter((record) => record.bindingId === input.bindingId && record.sendStatus === 'sent_unconfirmed');
67
- const consumedMessageIndexes = new Set();
68
- for (const record of pendingRecords) {
69
- const matchIndex = input.messages.findIndex((message, index) => !consumedMessageIndexes.has(index) && isOutboundEcho(record, message));
70
- if (matchIndex >= 0) {
71
- consumedMessageIndexes.add(matchIndex);
72
- const message = input.messages[matchIndex];
73
- record.sendStatus = 'confirmed_echo';
74
- record.confirmedAt = now.toISOString();
75
- record.confirmedEchoAnchor = normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || '') || undefined;
76
- confirmedRecords.push(record);
77
- continue;
78
- }
79
- record.observeAttemptsAfterSent = (record.observeAttemptsAfterSent ?? 0) + 1;
80
- if (shouldMoveOutboundToManualReview(record, now)) {
81
- record.sendStatus = 'manual_review';
82
- record.failureCode = 'echo_confirmation_timeout';
83
- record.lastErrorSummary = 'Sent reply echo was not confirmed after observe threshold';
84
- manualReviewRecords.push(record);
85
- }
86
- }
87
- return {
88
- remainingMessages: input.messages.filter((_, index) => !consumedMessageIndexes.has(index)),
89
- confirmedRecords,
90
- manualReviewRecords,
91
- };
92
- }
93
- export function suppressSelfOnlyWeChatMessages(messages) {
94
- return messages.filter((message) => message.senderRole !== 'self');
95
- }
96
- function isOutboundEcho(record, message) {
97
- if (message.senderRole !== 'self')
98
- return false;
99
- const expected = record.expectedEchoAnchor || record.textNormalized || '';
100
- const actual = normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || '');
101
- if (!expected || !actual)
102
- return false;
103
- return weChatTextSimilarity(expected, actual) >= 0.9;
104
- }
105
- function shouldMoveOutboundToManualReview(record, now) {
106
- if ((record.observeAttemptsAfterSent ?? 0) >= MANUAL_REVIEW_AFTER_OBSERVE_ATTEMPTS)
107
- return true;
108
- if (!record.sentAt)
109
- return false;
110
- const sentAt = new Date(record.sentAt).getTime();
111
- return Number.isFinite(sentAt) && now.getTime() - sentAt >= MANUAL_REVIEW_AFTER_MS;
112
- }
1
+ import c from"node:fs";import l from"node:path";import{normalizeWeChatAnchorText as u,weChatTextSimilarity as m}from"./anchor.js";const h=500,S=2,y=600*1e3;function E(e,t){try{const n=JSON.parse(c.readFileSync(e,"utf8"));if(n?.version===1&&n.runtimeId===t&&Array.isArray(n.records))return n}catch{}return{version:1,runtimeId:t,records:[]}}function R(e,t){c.mkdirSync(l.dirname(e),{recursive:!0}),t.records=t.records.slice(-h),c.writeFileSync(e,JSON.stringify(t,null,2))}function O(e){const t=e.ledger.records.find(s=>s.idempotencyKey===e.idempotencyKey);if(t)return t;const n=(e.now??new Date).toISOString(),o=u(e.text),i={replyId:e.replyId,idempotencyKey:e.idempotencyKey,bindingId:e.bindingId,runtimeId:e.runtimeId,sessionId:e.sessionId,conversationName:e.conversationName,replyBaseRevision:e.replyBaseRevision,text:b(e.text)||void 0,textNormalized:o||void 0,attachmentLocalRefs:e.attachmentLocalRefs??[],createdAt:n,queuedAt:n,sendStatus:"queued",expectedEchoAnchor:o||void 0,observeAttemptsAfterSent:0};return e.ledger.records.push(i),i}function I(e){return e.currentLastInboundRevision<=e.record.replyBaseRevision?{ok:!0}:(e.record.sendStatus="stale",e.record.failureCode="reply_revision_stale",e.record.lastErrorSummary="Inbound revision advanced before reply send",{ok:!1,reason:"stale"})}function C(e){e.sendStatus="sending",e.failureCode=void 0,e.lastErrorSummary=void 0}function _(e,t=new Date){e.sendStatus="sent_unconfirmed",e.sentAt=t.toISOString(),e.observeAttemptsAfterSent=0,e.failureCode=void 0,e.lastErrorSummary=void 0}function T(e,t,n){e.sendStatus="failed",e.failureCode=t,e.lastErrorSummary=n.slice(0,500)}function w(e){const t=e.now??new Date,n=[],o=[],i=e.ledger.records.filter(r=>r.bindingId===e.bindingId&&r.sendStatus==="sent_unconfirmed"),s=new Set;for(const r of i){const a=e.messages.findIndex((d,f)=>!s.has(f)&&x(r,d));if(a>=0){s.add(a);const d=e.messages[a];r.sendStatus="confirmed_echo",r.confirmedAt=t.toISOString(),r.confirmedEchoAnchor=u(d.anchorText||d.normalizedText||d.textExcerpt||"")||void 0,n.push(r);continue}r.observeAttemptsAfterSent=(r.observeAttemptsAfterSent??0)+1,A(r,t)&&(r.sendStatus="manual_review",r.failureCode="echo_confirmation_timeout",r.lastErrorSummary="Sent reply echo was not confirmed after observe threshold",o.push(r))}return{remainingMessages:e.messages.filter((r,a)=>!s.has(a)),confirmedRecords:n,manualReviewRecords:o}}function W(e){return e.filter(t=>t.senderRole!=="self")}function x(e,t){if(t.senderRole!=="self")return!1;const n=e.expectedEchoAnchor||e.textNormalized||"",o=u(t.anchorText||t.normalizedText||t.textExcerpt||"");return!n||!o?!1:m(n,o)>=.9}function A(e,t){if((e.observeAttemptsAfterSent??0)>=S)return!0;if(!e.sentAt)return!1;const n=new Date(e.sentAt).getTime();return Number.isFinite(n)&&t.getTime()-n>=y}function b(e){return typeof e=="string"?e.replace(/\r\n/g,`
2
+ `).trim():""}export{w as classifyWeChatOutboundEchoes,O as enqueueWeChatOutboundReply,I as guardWeChatOutboundRevision,E as loadWeChatChannelOutboundLedger,T as markWeChatOutboundFailed,C as markWeChatOutboundSending,_ as markWeChatOutboundSentUnconfirmed,R as saveWeChatChannelOutboundLedger,W as suppressSelfOnlyWeChatMessages};
@@ -0,0 +1,26 @@
1
+ import type { WeChatChannelHelperTransport } from './observer.js';
2
+ import { type WeChatChannelOutboundLedger, type WeChatChannelOutboundRecord } from './outbound-ledger.js';
3
+ export type WeChatChannelOutboundSendResult = {
4
+ sentRecords: WeChatChannelOutboundRecord[];
5
+ staleRecords: WeChatChannelOutboundRecord[];
6
+ failedRecords: WeChatChannelOutboundRecord[];
7
+ };
8
+ export declare function sendQueuedWeChatOutboundRecords(input: {
9
+ ledger: WeChatChannelOutboundLedger;
10
+ bindingId: string;
11
+ currentLastInboundRevision: number;
12
+ sender: WeChatChannelOutboundSender;
13
+ now?: Date;
14
+ }): Promise<WeChatChannelOutboundSendResult>;
15
+ export declare class WeChatChannelOutboundSender {
16
+ private options;
17
+ constructor(options: {
18
+ helper: WeChatChannelHelperTransport;
19
+ openConversation: (conversationName: string) => Promise<{
20
+ opened: boolean;
21
+ reason: string;
22
+ }>;
23
+ traceId?: string;
24
+ });
25
+ send(record: WeChatChannelOutboundRecord): Promise<void>;
26
+ }
@@ -0,0 +1 @@
1
+ import c from"node:fs";import{guardWeChatOutboundRevision as d,markWeChatOutboundFailed as u,markWeChatOutboundSending as h,markWeChatOutboundSentUnconfirmed as l}from"./outbound-ledger.js";async function w(e){const t={sentRecords:[],staleRecords:[],failedRecords:[]},r=e.ledger.records.filter(o=>o.bindingId===e.bindingId&&o.sendStatus==="queued");for(const o of r){if(!d({record:o,currentLastInboundRevision:e.currentLastInboundRevision,now:e.now}).ok){t.staleRecords.push(o);continue}h(o);try{await e.sender.send(o),l(o,e.now??new Date),t.sentRecords.push(o)}catch(n){u(o,f(n),n instanceof Error?n.message:String(n)),t.failedRecords.push(o)}}return t}class _{options;constructor(t){this.options=t}async send(t){const r=await this.options.openConversation(t.conversationName);if(!r.opened)throw new Error(r.reason||"conversation_not_opened");const o=await this.options.helper.request("clipboard.snapshot",{},this.options.traceId);i(o,"clipboard.snapshot");try{if(t.text){const s=await this.options.helper.request("clipboard.setText",{text:t.text},this.options.traceId);i(s,"clipboard.setText"),await a(this.options.helper,"v",["command"],this.options.traceId),await a(this.options.helper,"return",[],this.options.traceId)}for(const s of t.attachmentLocalRefs??[]){p(s);const n=await this.options.helper.request("clipboard.setFiles",{filePaths:[s]},this.options.traceId);i(n,"clipboard.setFiles"),await a(this.options.helper,"v",["command"],this.options.traceId),await a(this.options.helper,"return",[],this.options.traceId)}}finally{const s=o.result&&typeof o.result=="object"?o.result:{},n=await this.options.helper.request("clipboard.restore",s,this.options.traceId);i(n,"clipboard.restore")}}}async function a(e,t,r,o){const s=await e.request("keyboard.shortcut",{key:t,modifiers:r},o);i(s,"keyboard.shortcut")}function p(e){if(!c.statSync(e).isFile())throw new Error(`wechat_channel_attachment_not_file:${e}`)}function f(e){const t=e instanceof Error?e.message:String(e);return/conversation_not|conversation.*visible|conversation.*open/i.test(t)?"conversation_not_opened":/permission|accessibility|screen|automation/i.test(t)?"permission_missing":/clipboard/i.test(t)?"clipboard_failed":/attachment|not_file|ENOENT|no such file/i.test(t)?"attachment_unavailable":"send_failed"}function i(e,t){if(!e.ok)throw new Error(`${e.errorCode||"helper_command_failed"}: ${e.errorSummary||t}`)}export{_ as WeChatChannelOutboundSender,w as sendQueuedWeChatOutboundRecords};
@@ -1,48 +1 @@
1
- // @arch docs/features/wechat-rpa-productization-plan.md
2
- // @arch docs/features/wechat-rpa-helper-runtime.md
3
- // @test src/__tests__/wechat-channel-preflight.test.ts
4
- import { validateWeChatChannelHelperReady, } from './helper-protocol.js';
5
- export function evaluateWeChatChannelPreflight(input) {
6
- const checks = [];
7
- checks.push(blockingCheck(input.platform === 'darwin', 'mac_only', '当前阶段微信 Channel 只支持 macOS。'));
8
- checks.push(blockingCheck(input.accountTier === 'enterprise', 'enterprise_required', '当前阶段微信 Channel 仅企业版账号可用。'));
9
- checks.push(blockingCheck(Boolean(input.privacyConsentAccepted), 'privacy_consent_required', '启用前需要确认微信 Channel 数据与隐私授权。'));
10
- checks.push(blockingCheck(Boolean(input.serverDecisionAvailable), 'server_decision_unavailable', '服务端判断能力不可用,暂不能启用。'));
11
- if (input.currentMachineId && input.runtime.machineId) {
12
- checks.push(blockingCheck(input.currentMachineId === input.runtime.machineId, 'same_machine_required', '第一版只支持同机微信与同机神念运行时绑定。'));
13
- }
14
- if (input.localAgentMachineId && input.runtime.machineId) {
15
- checks.push(blockingCheck(input.localAgentMachineId === input.runtime.machineId, 'same_machine_required', '绑定的 Agent 必须与微信运行时在同一台机器。'));
16
- }
17
- checks.push(blockingCheck(input.localAgentAvailable !== false, 'local_agent_unavailable', '本机 Agent 不可用,无法安全接管微信回复。'));
18
- if (!input.helperReady) {
19
- checks.push(blockingCheck(false, 'helper_not_ready', 'macOS helper 未就绪。'));
20
- }
21
- else {
22
- const validation = validateWeChatChannelHelperReady(input.helperReady, input.expectedHelperVersion);
23
- if (!validation.ok) {
24
- checks.push(blockingCheck(false, validation.errorCode, validation.errorSummary));
25
- }
26
- else {
27
- checks.push(blockingCheck(true, 'ok', 'macOS helper 版本与能力检查通过。'));
28
- }
29
- }
30
- const permissions = input.permissions;
31
- if (permissions) {
32
- checks.push(blockingCheck(permissions.screenRecording !== false, 'permission_screen_recording_missing', '缺少屏幕录制权限。'));
33
- checks.push(blockingCheck(permissions.accessibility !== false, 'permission_accessibility_missing', '缺少辅助功能权限。'));
34
- checks.push(blockingCheck(permissions.automation !== false, 'permission_automation_missing', '缺少自动化控制权限。'));
35
- checks.push(blockingCheck(permissions.wechatRunning !== false, 'wechat_not_running', '微信未运行。'));
36
- checks.push(blockingCheck(permissions.wechatWindowAvailable !== false, 'wechat_window_unavailable', '微信窗口不可用。'));
37
- }
38
- const blockingFailures = checks.filter((check) => !check.ok && check.severity === 'blocking');
39
- return {
40
- ok: blockingFailures.length === 0,
41
- canEnable: blockingFailures.length === 0,
42
- status: blockingFailures.length === 0 ? 'ready' : 'blocked',
43
- checks,
44
- };
45
- }
46
- function blockingCheck(ok, code, message) {
47
- return { ok, code: ok ? 'ok' : code, severity: 'blocking', message };
48
- }
1
+ import{validateWeChatChannelHelperReady as l}from"./helper-protocol.js";function o(e){const a=[];if(a.push(n(e.platform==="darwin","mac_only","\u5F53\u524D\u9636\u6BB5\u5FAE\u4FE1 Channel \u53EA\u652F\u6301 macOS\u3002")),a.push(n(e.accountTier==="enterprise","enterprise_required","\u5F53\u524D\u9636\u6BB5\u5FAE\u4FE1 Channel \u4EC5\u4F01\u4E1A\u7248\u8D26\u53F7\u53EF\u7528\u3002")),a.push(n(!!e.privacyConsentAccepted,"privacy_consent_required","\u542F\u7528\u524D\u9700\u8981\u786E\u8BA4\u5FAE\u4FE1 Channel \u6570\u636E\u4E0E\u9690\u79C1\u6388\u6743\u3002")),a.push(n(!!e.serverDecisionAvailable,"server_decision_unavailable","\u670D\u52A1\u7AEF\u5224\u65AD\u80FD\u529B\u4E0D\u53EF\u7528\uFF0C\u6682\u4E0D\u80FD\u542F\u7528\u3002")),e.currentMachineId&&e.runtime.machineId&&a.push(n(e.currentMachineId===e.runtime.machineId,"same_machine_required","\u7B2C\u4E00\u7248\u53EA\u652F\u6301\u540C\u673A\u5FAE\u4FE1\u4E0E\u540C\u673A\u795E\u5FF5\u8FD0\u884C\u65F6\u7ED1\u5B9A\u3002")),e.localAgentMachineId&&e.runtime.machineId&&a.push(n(e.localAgentMachineId===e.runtime.machineId,"same_machine_required","\u7ED1\u5B9A\u7684 Agent \u5FC5\u987B\u4E0E\u5FAE\u4FE1\u8FD0\u884C\u65F6\u5728\u540C\u4E00\u53F0\u673A\u5668\u3002")),a.push(n(e.localAgentAvailable!==!1,"local_agent_unavailable","\u672C\u673A Agent \u4E0D\u53EF\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u63A5\u7BA1\u5FAE\u4FE1\u56DE\u590D\u3002")),!e.helperReady)a.push(n(!1,"helper_not_ready","macOS helper \u672A\u5C31\u7EEA\u3002"));else{const s=l(e.helperReady,e.expectedHelperVersion);s.ok?a.push(n(!0,"ok","macOS helper \u7248\u672C\u4E0E\u80FD\u529B\u68C0\u67E5\u901A\u8FC7\u3002")):a.push(n(!1,s.errorCode,s.errorSummary))}const r=e.permissions;r&&(a.push(n(r.screenRecording!==!1,"permission_screen_recording_missing","\u7F3A\u5C11\u5C4F\u5E55\u5F55\u5236\u6743\u9650\u3002")),a.push(n(r.accessibility!==!1,"permission_accessibility_missing","\u7F3A\u5C11\u8F85\u52A9\u529F\u80FD\u6743\u9650\u3002")),a.push(n(r.automation!==!1,"permission_automation_missing","\u7F3A\u5C11\u81EA\u52A8\u5316\u63A7\u5236\u6743\u9650\u3002")),a.push(n(r.wechatRunning!==!1,"wechat_not_running","\u5FAE\u4FE1\u672A\u8FD0\u884C\u3002")),a.push(n(r.wechatWindowAvailable!==!1,"wechat_window_unavailable","\u5FAE\u4FE1\u7A97\u53E3\u4E0D\u53EF\u7528\u3002")));const i=a.filter(s=>!s.ok&&s.severity==="blocking");return{ok:i.length===0,canEnable:i.length===0,status:i.length===0?"ready":"blocked",checks:a}}function n(e,a,r){return{ok:e,code:e?"ok":a,severity:"blocking",message:r}}export{o as evaluateWeChatChannelPreflight};