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.
@@ -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
+ }