@web-auto/camo 0.1.14 → 0.1.16

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.
@@ -1,181 +0,0 @@
1
- export function buildDetailHarvestScript() {
2
- return `(async () => {
3
- const state = window.__camoXhsState || (window.__camoXhsState = {});
4
- const scroller = document.querySelector('.note-scroller')
5
- || document.querySelector('.comments-el')
6
- || document.scrollingElement
7
- || document.documentElement;
8
- for (let i = 0; i < 3; i += 1) {
9
- scroller.scrollBy({ top: 360, behavior: 'auto' });
10
- await new Promise((resolve) => setTimeout(resolve, 120));
11
- }
12
- const title = (document.querySelector('.note-title') || {}).textContent || '';
13
- const content = (document.querySelector('.note-content') || {}).textContent || '';
14
- state.lastDetail = {
15
- title: String(title).trim().slice(0, 200),
16
- contentLength: String(content).trim().length,
17
- capturedAt: new Date().toISOString(),
18
- };
19
- return { harvested: true, detail: state.lastDetail };
20
- })()`;
21
- }
22
-
23
- export function buildExpandRepliesScript() {
24
- return `(async () => {
25
- const buttons = Array.from(document.querySelectorAll([
26
- '.note-detail-mask .show-more',
27
- '.note-detail-mask .reply-expand',
28
- '.note-detail-mask [class*="expand"]',
29
- '.note-detail-page .show-more',
30
- '.note-detail-page .reply-expand',
31
- '.note-detail-page [class*="expand"]',
32
- ].join(',')));
33
- let clicked = 0;
34
- for (const button of buttons.slice(0, 8)) {
35
- if (!(button instanceof HTMLElement)) continue;
36
- const text = (button.textContent || '').trim();
37
- if (!text) continue;
38
- button.scrollIntoView({ behavior: 'auto', block: 'center' });
39
- await new Promise((resolve) => setTimeout(resolve, 60));
40
- button.click();
41
- clicked += 1;
42
- await new Promise((resolve) => setTimeout(resolve, 120));
43
- }
44
- return { expanded: clicked, scanned: buttons.length };
45
- })()`;
46
- }
47
-
48
- export function buildCloseDetailScript() {
49
- return `(async () => {
50
- const state = window.__camoXhsState || (window.__camoXhsState = {});
51
- const metrics = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
52
- state.metrics = metrics;
53
- metrics.searchCount = Number(metrics.searchCount || 0);
54
- metrics.rollbackCount = Number(metrics.rollbackCount || 0);
55
- metrics.returnToSearchCount = Number(metrics.returnToSearchCount || 0);
56
- const harvest = state.lastCommentsHarvest && typeof state.lastCommentsHarvest === 'object'
57
- ? state.lastCommentsHarvest
58
- : null;
59
- const exitMeta = {
60
- pageExitReason: String(harvest?.exitReason || 'close_without_harvest').trim(),
61
- reachedBottom: typeof harvest?.reachedBottom === 'boolean' ? harvest.reachedBottom : null,
62
- commentsCollected: Number.isFinite(Number(harvest?.collected)) ? Number(harvest.collected) : null,
63
- expectedCommentsCount: Number.isFinite(Number(harvest?.expectedCommentsCount)) ? Number(harvest.expectedCommentsCount) : null,
64
- commentCoverageRate: Number.isFinite(Number(harvest?.commentCoverageRate)) ? Number(harvest.commentCoverageRate) : null,
65
- scrollRecoveries: Number.isFinite(Number(harvest?.recoveries)) ? Number(harvest.recoveries) : 0,
66
- harvestRounds: Number.isFinite(Number(harvest?.rounds)) ? Number(harvest.rounds) : null,
67
- };
68
- const detailSelectors = [
69
- '.note-detail-mask',
70
- '.note-detail-page',
71
- '.note-detail-dialog',
72
- '.note-detail-mask .detail-container',
73
- '.note-detail-mask .media-container',
74
- '.note-detail-mask .note-scroller',
75
- '.note-detail-mask .note-content',
76
- '.note-detail-mask .interaction-container',
77
- '.note-detail-mask .comments-container',
78
- ];
79
- const searchSelectors = ['.note-item', '.search-result-list', '#search-input', '.feeds-page'];
80
- const hasVisible = (selectors) => selectors.some((selector) => {
81
- const node = document.querySelector(selector);
82
- if (!node || !(node instanceof HTMLElement)) return false;
83
- const style = window.getComputedStyle(node);
84
- if (!style) return false;
85
- if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
86
- const rect = node.getBoundingClientRect();
87
- return rect.width > 1 && rect.height > 1;
88
- });
89
- const isDetailVisible = () => hasVisible(detailSelectors);
90
- const isSearchVisible = () => hasVisible(searchSelectors);
91
- const dispatchEscape = () => {
92
- const target = document.activeElement || document.body || document.documentElement;
93
- const opts = { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true };
94
- target.dispatchEvent(new KeyboardEvent('keydown', opts));
95
- target.dispatchEvent(new KeyboardEvent('keyup', opts));
96
- document.dispatchEvent(new KeyboardEvent('keydown', opts));
97
- document.dispatchEvent(new KeyboardEvent('keyup', opts));
98
- };
99
- const waitForCloseAnimation = async () => {
100
- for (let i = 0; i < 45; i += 1) {
101
- if (!isDetailVisible() && isSearchVisible()) return true;
102
- await new Promise((resolve) => setTimeout(resolve, 120));
103
- }
104
- return !isDetailVisible() && isSearchVisible();
105
- };
106
- const buildCounterMeta = (returnedToSearch) => ({
107
- searchCount: Number(metrics.searchCount || 0),
108
- rollbackCount: Number(metrics.rollbackCount || 0),
109
- returnToSearchCount: Number(metrics.returnToSearchCount || 0),
110
- returnedToSearch: Boolean(returnedToSearch),
111
- });
112
-
113
- if (!isDetailVisible()) {
114
- return {
115
- closed: true,
116
- via: 'already_closed',
117
- searchVisible: isSearchVisible(),
118
- ...buildCounterMeta(false),
119
- ...exitMeta,
120
- };
121
- }
122
-
123
- metrics.rollbackCount += 1;
124
- metrics.lastRollbackAt = new Date().toISOString();
125
-
126
- for (let attempt = 1; attempt <= 4; attempt += 1) {
127
- dispatchEscape();
128
- await new Promise((resolve) => setTimeout(resolve, 220));
129
- if (await waitForCloseAnimation()) {
130
- const searchVisible = isSearchVisible();
131
- if (searchVisible) {
132
- metrics.returnToSearchCount += 1;
133
- metrics.lastReturnToSearchAt = new Date().toISOString();
134
- }
135
- return {
136
- closed: true,
137
- via: 'escape',
138
- attempts: attempt,
139
- searchVisible,
140
- ...buildCounterMeta(searchVisible),
141
- ...exitMeta,
142
- };
143
- }
144
- }
145
-
146
- const selectors = ['.note-detail-mask .close-box', '.note-detail-mask .close-circle', '.close-box', '.close-circle'];
147
- for (const selector of selectors) {
148
- const target = document.querySelector(selector);
149
- if (!target || !(target instanceof HTMLElement)) continue;
150
- target.scrollIntoView({ behavior: 'auto', block: 'center' });
151
- await new Promise((resolve) => setTimeout(resolve, 100));
152
- target.click();
153
- await new Promise((resolve) => setTimeout(resolve, 220));
154
- if (await waitForCloseAnimation()) {
155
- const searchVisible = isSearchVisible();
156
- if (searchVisible) {
157
- metrics.returnToSearchCount += 1;
158
- metrics.lastReturnToSearchAt = new Date().toISOString();
159
- }
160
- return {
161
- closed: true,
162
- via: selector,
163
- attempts: 5,
164
- searchVisible,
165
- ...buildCounterMeta(searchVisible),
166
- ...exitMeta,
167
- };
168
- }
169
- }
170
-
171
- return {
172
- closed: false,
173
- via: 'escape_failed',
174
- detailVisible: isDetailVisible(),
175
- searchVisible: isSearchVisible(),
176
- ...buildCounterMeta(false),
177
- ...exitMeta,
178
- };
179
- })()`;
180
- }
181
-
@@ -1,466 +0,0 @@
1
- import path from 'node:path';
2
- import { normalizeArray } from '../../../container/runtime-core/utils.mjs';
3
- import { callAPI } from '../../../utils/browser-service.mjs';
4
- import {
5
- extractEvaluateResultData,
6
- extractScreenshotBase64,
7
- runEvaluateScript,
8
- } from './common.mjs';
9
- import {
10
- compileLikeRules,
11
- matchLikeText,
12
- normalizeText,
13
- } from './like-rules.mjs';
14
- import {
15
- appendLikedSignature,
16
- ensureDir,
17
- loadLikedSignatures,
18
- makeLikeSignature,
19
- mergeCommentsJsonl,
20
- resolveXhsOutputContext,
21
- savePngBase64,
22
- writeJsonFile,
23
- } from './persistence.mjs';
24
-
25
- function buildReadStateScript() {
26
- return `(() => {
27
- const state = window.__camoXhsState || {};
28
- return {
29
- keyword: state.keyword || null,
30
- currentNoteId: state.currentNoteId || null,
31
- lastCommentsHarvest: state.lastCommentsHarvest && typeof state.lastCommentsHarvest === 'object'
32
- ? state.lastCommentsHarvest
33
- : null,
34
- };
35
- })()`;
36
- }
37
-
38
- function buildCollectLikeTargetsScript() {
39
- return `(() => {
40
- const state = window.__camoXhsState || (window.__camoXhsState = {});
41
- const rows = [];
42
- const findLikeControl = (item) => {
43
- const selectors = [
44
- '.like-wrapper',
45
- '.comment-like',
46
- '.interactions .like-wrapper',
47
- '.interactions [class*="like"]',
48
- 'button[class*="like"]',
49
- '[aria-label*="赞"]',
50
- ];
51
- for (const selector of selectors) {
52
- const node = item.querySelector(selector);
53
- if (node instanceof HTMLElement) return node;
54
- }
55
- return null;
56
- };
57
- const isAlreadyLiked = (node) => {
58
- if (!node) return false;
59
- const className = String(node.className || '').toLowerCase();
60
- const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
61
- const text = String(node.textContent || '');
62
- const useNode = node.querySelector('use');
63
- const useHref = String(useNode?.getAttribute?.('xlink:href') || useNode?.getAttribute?.('href') || '').toLowerCase();
64
- return className.includes('like-active')
65
- || ariaPressed === 'true'
66
- || /已赞|取消赞/.test(text)
67
- || useHref.includes('liked');
68
- };
69
- const readText = (item, selectors) => {
70
- for (const selector of selectors) {
71
- const node = item.querySelector(selector);
72
- const text = String(node?.textContent || '').replace(/\\s+/g, ' ').trim();
73
- if (text) return text;
74
- }
75
- return '';
76
- };
77
- const readAttr = (item, attrNames) => {
78
- for (const attr of attrNames) {
79
- const value = String(item.getAttribute?.(attr) || '').trim();
80
- if (value) return value;
81
- }
82
- return '';
83
- };
84
-
85
- const matchedSet = new Set(
86
- Array.isArray(state.matchedComments)
87
- ? state.matchedComments.map((row) => Number(row?.index)).filter((index) => Number.isFinite(index))
88
- : [],
89
- );
90
- const items = Array.from(document.querySelectorAll('.comment-item'));
91
- for (let index = 0; index < items.length; index += 1) {
92
- const item = items[index];
93
- const text = readText(item, ['.content', '.comment-content', 'p']);
94
- if (!text) continue;
95
- const userName = readText(item, ['.name', '.author', '.user-name', '.username', '[class*="author"]', '[class*="name"]']);
96
- const userId = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
97
- const timestamp = readText(item, ['.date', '.time', '.timestamp', '[class*="time"]']);
98
- const likeControl = findLikeControl(item);
99
- rows.push({
100
- index,
101
- text,
102
- userName,
103
- userId,
104
- timestamp,
105
- hasLikeControl: Boolean(likeControl),
106
- alreadyLiked: isAlreadyLiked(likeControl),
107
- matchedByState: matchedSet.has(index),
108
- });
109
- }
110
- return {
111
- noteId: state.currentNoteId || null,
112
- matchedByStateCount: matchedSet.size,
113
- reachedBottom: typeof state.lastCommentsHarvest?.reachedBottom === 'boolean'
114
- ? state.lastCommentsHarvest.reachedBottom
115
- : false,
116
- stopReason: String(state.lastCommentsHarvest?.exitReason || '').trim() || null,
117
- rows,
118
- };
119
- })()`;
120
- }
121
-
122
- function buildClickLikeByIndexScript(index, highlight) {
123
- return `(async () => {
124
- const idx = Number(${JSON.stringify(index)});
125
- const items = Array.from(document.querySelectorAll('.comment-item'));
126
- const item = items[idx];
127
- if (!item) return { clicked: false, reason: 'comment_item_not_found', index: idx };
128
- const findLikeControl = (node) => {
129
- const selectors = [
130
- '.like-wrapper',
131
- '.comment-like',
132
- '.interactions .like-wrapper',
133
- '.interactions [class*="like"]',
134
- 'button[class*="like"]',
135
- '[aria-label*="赞"]',
136
- ];
137
- for (const selector of selectors) {
138
- const found = node.querySelector(selector);
139
- if (found instanceof HTMLElement) return found;
140
- }
141
- return null;
142
- };
143
- const isAlreadyLiked = (node) => {
144
- if (!node) return false;
145
- const className = String(node.className || '').toLowerCase();
146
- const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
147
- const text = String(node.textContent || '');
148
- const useNode = node.querySelector('use');
149
- const useHref = String(useNode?.getAttribute?.('xlink:href') || useNode?.getAttribute?.('href') || '').toLowerCase();
150
- return className.includes('like-active')
151
- || ariaPressed === 'true'
152
- || /已赞|取消赞/.test(text)
153
- || useHref.includes('liked');
154
- };
155
-
156
- const likeControl = findLikeControl(item);
157
- if (!likeControl) return { clicked: false, reason: 'like_control_not_found', index: idx };
158
- const beforeLiked = isAlreadyLiked(likeControl);
159
- if (beforeLiked) {
160
- return { clicked: false, alreadyLiked: true, reason: 'already_liked', index: idx };
161
- }
162
- item.scrollIntoView({ behavior: 'auto', block: 'center' });
163
- likeControl.scrollIntoView({ behavior: 'auto', block: 'center' });
164
- await new Promise((resolve) => setTimeout(resolve, 120));
165
- if (${highlight ? 'true' : 'false'}) {
166
- const prev = likeControl.style.outline;
167
- likeControl.style.outline = '2px solid #00d6ff';
168
- setTimeout(() => { likeControl.style.outline = prev; }, 450);
169
- }
170
- likeControl.click();
171
- await new Promise((resolve) => setTimeout(resolve, 220));
172
- return {
173
- clicked: true,
174
- alreadyLiked: false,
175
- likedAfter: isAlreadyLiked(likeControl),
176
- reason: 'clicked',
177
- index: idx,
178
- };
179
- })()`;
180
- }
181
-
182
- async function captureScreenshotToFile({ profileId, filePath }) {
183
- try {
184
- const payload = await callAPI('screenshot', { profileId, fullPage: false });
185
- const base64 = extractScreenshotBase64(payload);
186
- if (!base64) return null;
187
- return savePngBase64(filePath, base64);
188
- } catch {
189
- return null;
190
- }
191
- }
192
-
193
- export async function executeCommentLikeOperation({ profileId, params = {} }) {
194
- const maxLikes = Math.max(1, Number(params.maxLikes ?? params.maxLikesPerRound ?? 1) || 1);
195
- const rawKeywords = normalizeArray(params.keywords || params.likeKeywords);
196
- const rules = compileLikeRules(rawKeywords);
197
- const highlight = params.highlight !== false;
198
- const dryRun = params.dryRun === true;
199
- const saveEvidence = params.saveEvidence !== false;
200
- const persistLikeState = params.persistLikeState !== false;
201
- const persistComments = params.persistComments === true || params.persistCollectedComments === true;
202
-
203
- const stateRaw = await runEvaluateScript({
204
- profileId,
205
- script: buildReadStateScript(),
206
- highlight: false,
207
- });
208
- const state = extractEvaluateResultData(stateRaw) || {};
209
-
210
- const collectedRaw = await runEvaluateScript({
211
- profileId,
212
- script: buildCollectLikeTargetsScript(),
213
- highlight: false,
214
- });
215
- const collected = extractEvaluateResultData(collectedRaw) || {};
216
- const rows = Array.isArray(collected.rows) ? collected.rows : [];
217
-
218
- const output = resolveXhsOutputContext({
219
- params,
220
- state,
221
- noteId: collected.noteId || state.currentNoteId || params.noteId,
222
- });
223
- const evidenceDir = dryRun ? output.virtualLikeEvidenceDir : output.likeEvidenceDir;
224
- if (saveEvidence) {
225
- await ensureDir(evidenceDir);
226
- }
227
-
228
- const likedSignatures = persistLikeState ? await loadLikedSignatures(output.likeStatePath) : new Set();
229
- const likedComments = [];
230
- const matchedByStateCount = Number(collected.matchedByStateCount || 0);
231
- const useStateMatches = matchedByStateCount > 0;
232
-
233
- let hitCount = 0;
234
- let likedCount = 0;
235
- let dedupSkipped = 0;
236
- let alreadyLikedSkipped = 0;
237
- let missingLikeControl = 0;
238
- let clickFailed = 0;
239
- let verifyFailed = 0;
240
-
241
- if (persistComments && rows.length > 0) {
242
- await mergeCommentsJsonl({
243
- filePath: output.commentsPath,
244
- noteId: output.noteId,
245
- comments: rows,
246
- }).catch(() => null);
247
- }
248
-
249
- for (const row of rows) {
250
- if (likedCount >= maxLikes) break;
251
- if (!row || typeof row !== 'object') continue;
252
- const text = normalizeText(row.text);
253
- if (!text) continue;
254
-
255
- let match = null;
256
- if (useStateMatches) {
257
- if (!row.matchedByState) continue;
258
- match = { ok: true, reason: 'state_match', matchedRule: 'state_match' };
259
- } else {
260
- match = matchLikeText(text, rules);
261
- if (!match.ok) continue;
262
- }
263
- hitCount += 1;
264
-
265
- const signature = makeLikeSignature({
266
- noteId: output.noteId,
267
- userId: String(row.userId || ''),
268
- userName: String(row.userName || ''),
269
- text,
270
- });
271
-
272
- if (signature && likedSignatures.has(signature)) {
273
- dedupSkipped += 1;
274
- continue;
275
- }
276
-
277
- if (!row.hasLikeControl) {
278
- missingLikeControl += 1;
279
- continue;
280
- }
281
-
282
- if (row.alreadyLiked) {
283
- alreadyLikedSkipped += 1;
284
- if (persistLikeState && signature) {
285
- likedSignatures.add(signature);
286
- await appendLikedSignature(output.likeStatePath, signature, {
287
- noteId: output.noteId,
288
- userId: String(row.userId || ''),
289
- userName: String(row.userName || ''),
290
- reason: 'already_liked',
291
- }).catch(() => null);
292
- }
293
- continue;
294
- }
295
-
296
- if (dryRun) {
297
- continue;
298
- }
299
-
300
- const beforePath = saveEvidence
301
- ? await captureScreenshotToFile({
302
- profileId,
303
- filePath: path.join(evidenceDir, `like-before-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
304
- })
305
- : null;
306
-
307
- const clickRaw = await runEvaluateScript({
308
- profileId,
309
- script: buildClickLikeByIndexScript(row.index, highlight),
310
- highlight: false,
311
- });
312
- const clickResult = extractEvaluateResultData(clickRaw) || {};
313
-
314
- const afterPath = saveEvidence
315
- ? await captureScreenshotToFile({
316
- profileId,
317
- filePath: path.join(evidenceDir, `like-after-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
318
- })
319
- : null;
320
-
321
- if (clickResult.alreadyLiked) {
322
- alreadyLikedSkipped += 1;
323
- if (persistLikeState && signature) {
324
- likedSignatures.add(signature);
325
- await appendLikedSignature(output.likeStatePath, signature, {
326
- noteId: output.noteId,
327
- userId: String(row.userId || ''),
328
- userName: String(row.userName || ''),
329
- reason: 'already_liked_after_click',
330
- }).catch(() => null);
331
- }
332
- continue;
333
- }
334
-
335
- if (!clickResult.clicked) {
336
- clickFailed += 1;
337
- continue;
338
- }
339
-
340
- if (!clickResult.likedAfter) {
341
- verifyFailed += 1;
342
- continue;
343
- }
344
-
345
- likedCount += 1;
346
- if (persistLikeState && signature) {
347
- likedSignatures.add(signature);
348
- await appendLikedSignature(output.likeStatePath, signature, {
349
- noteId: output.noteId,
350
- userId: String(row.userId || ''),
351
- userName: String(row.userName || ''),
352
- reason: 'liked',
353
- }).catch(() => null);
354
- }
355
- likedComments.push({
356
- index: Number(row.index),
357
- userId: String(row.userId || ''),
358
- userName: String(row.userName || ''),
359
- content: text,
360
- timestamp: String(row.timestamp || ''),
361
- matchedRule: match.matchedRule || match.reason,
362
- screenshots: {
363
- before: beforePath,
364
- after: afterPath,
365
- },
366
- });
367
- }
368
-
369
- const skippedCount = missingLikeControl + clickFailed + verifyFailed;
370
- const likedTotal = likedCount + dedupSkipped + alreadyLikedSkipped;
371
- const hitCheckOk = likedTotal + skippedCount === hitCount;
372
- const summary = {
373
- noteId: output.noteId,
374
- keyword: output.keyword,
375
- env: output.env,
376
- likeKeywords: rawKeywords,
377
- maxLikes,
378
- scannedCount: rows.length,
379
- hitCount,
380
- likedCount,
381
- skippedCount,
382
- likedTotal,
383
- hitCheckOk,
384
- skippedBreakdown: {
385
- missingLikeControl,
386
- clickFailed,
387
- verifyFailed,
388
- },
389
- likedBreakdown: {
390
- newLikes: likedCount,
391
- alreadyLiked: alreadyLikedSkipped,
392
- dedup: dedupSkipped,
393
- },
394
- reachedBottom: collected.reachedBottom === true,
395
- stopReason: String(collected.stopReason || '').trim() || null,
396
- likedComments,
397
- ts: new Date().toISOString(),
398
- };
399
-
400
- let summaryPath = null;
401
- if (saveEvidence) {
402
- summaryPath = await writeJsonFile(path.join(evidenceDir, `summary-${Date.now()}.json`), summary).catch(() => null);
403
- }
404
-
405
- return {
406
- ok: true,
407
- code: 'OPERATION_DONE',
408
- message: 'xhs_comment_like done',
409
- data: {
410
- noteId: output.noteId,
411
- scannedCount: rows.length,
412
- hitCount,
413
- likedCount,
414
- skippedCount,
415
- likedTotal,
416
- hitCheckOk,
417
- dedupSkipped,
418
- alreadyLikedSkipped,
419
- missingLikeControl,
420
- clickFailed,
421
- verifyFailed,
422
- likedComments,
423
- commentsPath: persistComments ? output.commentsPath : null,
424
- likeStatePath: persistLikeState ? output.likeStatePath : null,
425
- evidenceDir: saveEvidence ? evidenceDir : null,
426
- summaryPath,
427
- reachedBottom: collected.reachedBottom === true,
428
- stopReason: String(collected.stopReason || '').trim() || null,
429
- },
430
- };
431
- }
432
-
433
- export function buildCommentReplyScript(params = {}) {
434
- const replyText = String(params.replyText || '').trim();
435
- return `(async () => {
436
- const state = window.__camoXhsState || (window.__camoXhsState = {});
437
- const replyText = ${JSON.stringify(replyText)};
438
- const matches = Array.isArray(state.matchedComments) ? state.matchedComments : [];
439
- if (matches.length === 0) return { typed: false, reason: 'no_match' };
440
- const index = Number(matches[0].index);
441
- const nodes = Array.from(document.querySelectorAll('.comment-item'));
442
- const target = nodes[index];
443
- if (!target) return { typed: false, reason: 'match_not_visible', index };
444
- target.scrollIntoView({ behavior: 'auto', block: 'center' });
445
- await new Promise((resolve) => setTimeout(resolve, 100));
446
- target.click();
447
- await new Promise((resolve) => setTimeout(resolve, 120));
448
- const input = document.querySelector('textarea, input[placeholder*="说点"], [contenteditable="true"]');
449
- if (!input) return { typed: false, reason: 'reply_input_not_found', index };
450
- if (input instanceof HTMLTextAreaElement || input instanceof HTMLInputElement) {
451
- input.focus();
452
- input.value = replyText;
453
- input.dispatchEvent(new Event('input', { bubbles: true }));
454
- } else {
455
- input.focus();
456
- input.textContent = replyText;
457
- input.dispatchEvent(new Event('input', { bubbles: true }));
458
- }
459
- await new Promise((resolve) => setTimeout(resolve, 120));
460
- const sendButton = Array.from(document.querySelectorAll('button'))
461
- .find((button) => /发送|回复/.test(String(button.textContent || '').trim()));
462
- if (sendButton) sendButton.click();
463
- state.lastReply = { typed: true, index, at: new Date().toISOString() };
464
- return state.lastReply;
465
- })()`;
466
- }
@@ -1,57 +0,0 @@
1
- import { normalizeArray } from '../../../container/runtime-core/utils.mjs';
2
-
3
- export function normalizeText(value) {
4
- return String(value || '').replace(/\s+/g, ' ').trim();
5
- }
6
-
7
- function parseLikeRuleToken(token) {
8
- const raw = normalizeText(token);
9
- if (!raw) return null;
10
- const matched = raw.match(/^\{\s*(.+?)\s*([+\-\uFF0B\uFF0D])\s*(.+?)\s*\}$/);
11
- if (!matched) {
12
- return { kind: 'contains', include: raw, raw };
13
- }
14
- const left = normalizeText(matched[1]);
15
- const right = normalizeText(matched[3]);
16
- if (!left || !right) return null;
17
- const op = matched[2] === '\uFF0B' ? '+' : matched[2] === '\uFF0D' ? '-' : matched[2];
18
- if (op === '+') {
19
- return { kind: 'and', includeA: left, includeB: right, raw: `{${left} + ${right}}` };
20
- }
21
- return { kind: 'include_without', include: left, exclude: right, raw: `{${left} - ${right}}` };
22
- }
23
-
24
- export function compileLikeRules(keywords) {
25
- const rules = [];
26
- for (const token of normalizeArray(keywords)) {
27
- const parsed = parseLikeRuleToken(token);
28
- if (parsed) rules.push(parsed);
29
- }
30
- return rules;
31
- }
32
-
33
- export function matchLikeText(textRaw, rules) {
34
- const text = normalizeText(textRaw);
35
- if (!text) return { ok: false, reason: 'empty_text' };
36
- if (!Array.isArray(rules) || rules.length === 0) return { ok: false, reason: 'no_rules' };
37
-
38
- for (const rule of rules) {
39
- if (rule.kind === 'contains') {
40
- if (text.includes(rule.include)) {
41
- return { ok: true, reason: 'contains_match', matchedRule: rule.raw };
42
- }
43
- continue;
44
- }
45
- if (rule.kind === 'and') {
46
- if (text.includes(rule.includeA) && text.includes(rule.includeB)) {
47
- return { ok: true, reason: 'and_match', matchedRule: rule.raw };
48
- }
49
- continue;
50
- }
51
- if (text.includes(rule.include) && !text.includes(rule.exclude)) {
52
- return { ok: true, reason: 'include_without_match', matchedRule: rule.raw };
53
- }
54
- }
55
-
56
- return { ok: false, reason: 'no_rule_match' };
57
- }