shennian 0.2.74 → 0.2.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/wechat-rpa-confirmation.mjs +97 -0
- package/dist/scripts/wechat-rpa-download-candidates.mjs +84 -0
- package/dist/scripts/wechat-rpa-win-visual.mjs +668 -0
- package/dist/scripts/wechat-rpa-win.mjs +119 -0
- package/dist/src/agents/command-spec.js +24 -2
- package/dist/src/agents/external-channel-instructions.js +2 -0
- package/dist/src/channels/base.d.ts +1 -0
- package/dist/src/channels/runtime.d.ts +9 -2
- package/dist/src/channels/runtime.js +56 -6
- package/dist/src/channels/secret-registry.d.ts +1 -1
- package/dist/src/channels/wechat-rpa/macos-flow.d.ts +5 -0
- package/dist/src/channels/wechat-rpa/macos-flow.js +50 -2
- package/dist/src/channels/wechat-rpa/windows-visual-flow.d.ts +36 -0
- package/dist/src/channels/wechat-rpa/windows-visual-flow.js +182 -0
- package/dist/src/channels/wechat-rpa.d.ts +13 -16
- package/dist/src/channels/wechat-rpa.js +149 -14
- package/dist/src/commands/external-attachments.js +1 -1
- package/dist/src/commands/external.js +10 -1
- package/dist/src/commands/manager.js +18 -0
- package/dist/src/manager/runtime.js +23 -6
- package/dist/src/session/handlers/chat.js +12 -7
- package/dist/src/session/queue.js +2 -0
- package/dist/src/session/types.d.ts +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function containsAttachmentName(messages, filePath) {
|
|
5
|
+
const base = normalizeReplyText(path.basename(filePath))
|
|
6
|
+
const stem = normalizeReplyText(path.basename(filePath, path.extname(filePath)))
|
|
7
|
+
const haystack = normalizeReplyText(messages.map((message) => message.text).join(''))
|
|
8
|
+
return Boolean(base && haystack.includes(base)) || Boolean(stem.length >= 4 && haystack.includes(stem))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function containsSentAttachment(postSendMessages, observations, filePath, beforeMessages, beforeObservations) {
|
|
12
|
+
if (containsAttachmentName(postSendMessages, filePath)) return true
|
|
13
|
+
if (containsAttachmentNameInObservations(observations, filePath)) return true
|
|
14
|
+
const beforeIds = new Set(beforeMessages.map((message) => message.id))
|
|
15
|
+
const fresh = postSendMessages.filter((message) => !beforeIds.has(message.id))
|
|
16
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
17
|
+
const expectedType = attachmentTypeFromExt(ext)
|
|
18
|
+
if (expectedType !== 'file' && fresh.some((message) => message.attachments?.some((attachment) => attachment.type === expectedType))) return true
|
|
19
|
+
if (containsNewAttachmentPreviewLabel(observations, beforeObservations, expectedType)) return true
|
|
20
|
+
if (expectedType === 'image' && fresh.some((message) => /图片|照片|image/i.test(message.text))) return true
|
|
21
|
+
if (expectedType === 'video' && fresh.some((message) => /视频|video/i.test(message.text) || isVideoDurationLabel(message.text))) return true
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function containsReplyText(messages, text) {
|
|
26
|
+
return containsNormalizedReplyText(messages.map((message) => message.text).join(''), text)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function containsReplyTextInObservations(observations, text) {
|
|
30
|
+
return containsNormalizedReplyText(observations.map((item) => item.text).join(''), text)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function containsAttachmentNameInObservations(observations, filePath) {
|
|
34
|
+
const base = normalizeReplyText(path.basename(filePath))
|
|
35
|
+
const stem = normalizeReplyText(path.basename(filePath, path.extname(filePath)))
|
|
36
|
+
const haystack = normalizeReplyText(observations.map((item) => item.text).join(''))
|
|
37
|
+
return Boolean(base && haystack.includes(base)) || Boolean(stem.length >= 4 && haystack.includes(stem))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeReplyText(text) {
|
|
41
|
+
return String(text)
|
|
42
|
+
.replace(/[^\p{L}\p{N}]/gu, '')
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function postSendInitialDelayMs(attachmentPath) {
|
|
47
|
+
if (!attachmentPath) return 2_000
|
|
48
|
+
const type = attachmentTypeFromExt(path.extname(attachmentPath).toLowerCase())
|
|
49
|
+
if (type === 'image') return 8_000
|
|
50
|
+
if (type === 'video') return 15_000
|
|
51
|
+
return 2_000
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isVideoDurationLabel(text) {
|
|
55
|
+
return /^(?:\d{1,2}:)?\d{1,2}:\d{2}$/.test(String(text || '').trim())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function containsNewAttachmentPreviewLabel(observations, beforeObservations, expectedType) {
|
|
59
|
+
const beforeIds = new Set(beforeObservations.filter((item) => isAttachmentPreviewLabel(item, expectedType)).map(observationId))
|
|
60
|
+
return observations
|
|
61
|
+
.filter((item) => isAttachmentPreviewLabel(item, expectedType))
|
|
62
|
+
.some((item) => !beforeIds.has(observationId(item)))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function containsNormalizedReplyText(haystackText, text) {
|
|
66
|
+
const haystack = normalizeReplyText(haystackText)
|
|
67
|
+
const needle = normalizeReplyText(text)
|
|
68
|
+
if (!needle) return false
|
|
69
|
+
if (haystack.includes(needle)) return true
|
|
70
|
+
const head = needle.slice(0, Math.min(16, needle.length))
|
|
71
|
+
const tail = needle.slice(Math.max(0, needle.length - 12))
|
|
72
|
+
return head.length >= 8 && tail.length >= 8 && haystack.includes(head) && haystack.includes(tail)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isAttachmentPreviewLabel(item, expectedType) {
|
|
76
|
+
if (expectedType === 'image') {
|
|
77
|
+
return /\[?图片\]?|照片|image/i.test(item.text)
|
|
78
|
+
}
|
|
79
|
+
if (expectedType === 'video') {
|
|
80
|
+
return /\[?视频\]?|video/i.test(item.text) || isVideoDurationLabel(item.text)
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function observationId(item) {
|
|
86
|
+
return stableId(`${item.text}\n${item.x}\n${item.y}\n${item.width}\n${item.height}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function attachmentTypeFromExt(ext) {
|
|
90
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.heic'].includes(ext)) return 'image'
|
|
91
|
+
if (['.mp4', '.mov', '.avi', '.mkv'].includes(ext)) return 'video'
|
|
92
|
+
return 'file'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stableId(value) {
|
|
96
|
+
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 24)
|
|
97
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-channel.md
|
|
2
|
+
// @test packages/cli/src/__tests__/wechat-rpa-download-candidates.test.ts
|
|
3
|
+
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { normalizeReplyText } from './wechat-rpa-confirmation.mjs'
|
|
6
|
+
|
|
7
|
+
export function selectDownloadedAttachment(before, after, startedAt, attachment) {
|
|
8
|
+
const changed = Array.from(after.values())
|
|
9
|
+
.filter((file) => file.mtimeMs >= startedAt - 1_000)
|
|
10
|
+
.filter((file) => isPlausibleDownloadedAttachment(file, attachment))
|
|
11
|
+
.filter((file) => {
|
|
12
|
+
const prev = before.get(file.path)
|
|
13
|
+
return !prev || prev.size !== file.size || prev.mtimeMs !== file.mtimeMs
|
|
14
|
+
})
|
|
15
|
+
if (!changed.length) return null
|
|
16
|
+
const expectedName = normalizeReplyText(attachment?.name || '')
|
|
17
|
+
return changed
|
|
18
|
+
.map((file) => {
|
|
19
|
+
const base = normalizeReplyText(path.basename(file.path))
|
|
20
|
+
const expectedHead = expectedName.slice(0, Math.min(expectedName.length, 16))
|
|
21
|
+
const nameScore = expectedHead && base.includes(expectedHead) ? 10 : 0
|
|
22
|
+
return { ...file, score: nameScore + file.mtimeMs / 1_000_000_000_000 }
|
|
23
|
+
})
|
|
24
|
+
.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs)[0] || null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const INTERNAL_EXTENSIONS = new Set([
|
|
28
|
+
'.db',
|
|
29
|
+
'.ini',
|
|
30
|
+
'.log',
|
|
31
|
+
'.plist',
|
|
32
|
+
'.shm',
|
|
33
|
+
'.sqlite',
|
|
34
|
+
'.statistic',
|
|
35
|
+
'.tmp',
|
|
36
|
+
'.wal',
|
|
37
|
+
'.xlog',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const ATTACHMENT_EXTENSIONS = new Set([
|
|
41
|
+
'.7z',
|
|
42
|
+
'.aac',
|
|
43
|
+
'.avi',
|
|
44
|
+
'.csv',
|
|
45
|
+
'.doc',
|
|
46
|
+
'.docx',
|
|
47
|
+
'.gif',
|
|
48
|
+
'.heic',
|
|
49
|
+
'.jpeg',
|
|
50
|
+
'.jpg',
|
|
51
|
+
'.key',
|
|
52
|
+
'.m4a',
|
|
53
|
+
'.m4v',
|
|
54
|
+
'.md',
|
|
55
|
+
'.mov',
|
|
56
|
+
'.mp3',
|
|
57
|
+
'.mp4',
|
|
58
|
+
'.numbers',
|
|
59
|
+
'.pages',
|
|
60
|
+
'.pdf',
|
|
61
|
+
'.png',
|
|
62
|
+
'.ppt',
|
|
63
|
+
'.pptx',
|
|
64
|
+
'.rar',
|
|
65
|
+
'.rtf',
|
|
66
|
+
'.txt',
|
|
67
|
+
'.wav',
|
|
68
|
+
'.webp',
|
|
69
|
+
'.xls',
|
|
70
|
+
'.xlsx',
|
|
71
|
+
'.zip',
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
export function isPlausibleDownloadedAttachment(file, attachment) {
|
|
75
|
+
const base = path.basename(file?.path || '')
|
|
76
|
+
if (!base || base.startsWith('.')) return false
|
|
77
|
+
if (!Number.isFinite(file?.size) || file.size <= 0) return false
|
|
78
|
+
const ext = path.extname(base).toLowerCase()
|
|
79
|
+
if (!ext || INTERNAL_EXTENSIONS.has(ext)) return false
|
|
80
|
+
const expectedExt = path.extname(String(attachment?.name || '')).toLowerCase()
|
|
81
|
+
if (expectedExt && ext !== expectedExt) return false
|
|
82
|
+
if (ATTACHMENT_EXTENSIONS.has(ext)) return true
|
|
83
|
+
return Boolean(expectedExt)
|
|
84
|
+
}
|