shennian 0.2.89 → 0.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,181 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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};
|