@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.
- package/README.md +19 -37
- package/package.json +1 -1
- package/src/autoscript/action-providers/index.mjs +3 -6
- package/src/autoscript/runtime.mjs +14 -12
- package/src/autoscript/schema.mjs +6 -0
- package/src/cli.mjs +5 -1
- package/src/commands/autoscript.mjs +14 -103
- package/src/commands/browser.mjs +247 -19
- package/src/commands/mouse.mjs +9 -3
- package/src/container/runtime-core/checkpoint.mjs +21 -7
- package/src/container/runtime-core/operations/index.mjs +392 -38
- package/src/container/runtime-core/subscription.mjs +79 -7
- package/src/container/runtime-core/validation.mjs +2 -2
- package/src/utils/browser-service.mjs +41 -6
- package/src/utils/help.mjs +0 -1
- package/src/utils/js-policy.mjs +13 -0
- package/src/autoscript/action-providers/xhs/comments.mjs +0 -412
- package/src/autoscript/action-providers/xhs/common.mjs +0 -77
- package/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/src/autoscript/action-providers/xhs/interaction.mjs +0 -466
- package/src/autoscript/action-providers/xhs/like-rules.mjs +0 -57
- package/src/autoscript/action-providers/xhs/persistence.mjs +0 -167
- package/src/autoscript/action-providers/xhs/search.mjs +0 -174
- package/src/autoscript/action-providers/xhs.mjs +0 -133
- package/src/autoscript/xhs-unified-template.mjs +0 -934
|
@@ -9,6 +9,31 @@ import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
|
|
|
9
9
|
import { touchSessionActivity } from '../lifecycle/session-registry.mjs';
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
|
+
const DEFAULT_API_TIMEOUT_MS = 30000;
|
|
13
|
+
|
|
14
|
+
function resolveApiTimeoutMs(options = {}) {
|
|
15
|
+
const optionValue = Number(options?.timeoutMs);
|
|
16
|
+
if (Number.isFinite(optionValue) && optionValue > 0) {
|
|
17
|
+
return Math.max(1000, Math.floor(optionValue));
|
|
18
|
+
}
|
|
19
|
+
const envValue = Number(process.env.CAMO_API_TIMEOUT_MS);
|
|
20
|
+
if (Number.isFinite(envValue) && envValue > 0) {
|
|
21
|
+
return Math.max(1000, Math.floor(envValue));
|
|
22
|
+
}
|
|
23
|
+
return DEFAULT_API_TIMEOUT_MS;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTimeoutError(error) {
|
|
27
|
+
const name = String(error?.name || '').toLowerCase();
|
|
28
|
+
const message = String(error?.message || '').toLowerCase();
|
|
29
|
+
return (
|
|
30
|
+
name.includes('timeout')
|
|
31
|
+
|| name.includes('abort')
|
|
32
|
+
|| message.includes('timeout')
|
|
33
|
+
|| message.includes('timed out')
|
|
34
|
+
|| message.includes('aborted')
|
|
35
|
+
);
|
|
36
|
+
}
|
|
12
37
|
|
|
13
38
|
function shouldTrackSessionActivity(action, payload) {
|
|
14
39
|
const profileId = String(payload?.profileId || '').trim();
|
|
@@ -17,12 +42,22 @@ function shouldTrackSessionActivity(action, payload) {
|
|
|
17
42
|
return true;
|
|
18
43
|
}
|
|
19
44
|
|
|
20
|
-
export async function callAPI(action, payload = {}) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
export async function callAPI(action, payload = {}, options = {}) {
|
|
46
|
+
const timeoutMs = resolveApiTimeoutMs(options);
|
|
47
|
+
let r;
|
|
48
|
+
try {
|
|
49
|
+
r = await fetch(`${BROWSER_SERVICE_URL}/command`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ action, args: payload }),
|
|
53
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (isTimeoutError(error)) {
|
|
57
|
+
throw new Error(`browser-service timeout after ${timeoutMs}ms: ${action}`);
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
26
61
|
|
|
27
62
|
let body;
|
|
28
63
|
try {
|
package/src/utils/help.mjs
CHANGED
|
@@ -155,7 +155,6 @@ CONTAINER FILTER & SUBSCRIPTION:
|
|
|
155
155
|
container list [profileId] List visible elements in viewport
|
|
156
156
|
|
|
157
157
|
AUTOSCRIPT (STRATEGY LAYER):
|
|
158
|
-
autoscript scaffold xhs-unified [--output <file>] Generate xiaohongshu unified-harvest autoscript template
|
|
159
158
|
autoscript validate <file> Validate autoscript schema and references
|
|
160
159
|
autoscript explain <file> Print normalized graph and defaults
|
|
161
160
|
autoscript snapshot <jsonl-file> [--out <snapshot-file>] Build resumable snapshot from run JSONL
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function isTruthy(value) {
|
|
2
|
+
const text = String(value || '').trim().toLowerCase();
|
|
3
|
+
return ['1', 'true', 'yes', 'on'].includes(text);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isJsExecutionEnabled() {
|
|
7
|
+
return isTruthy(process.env.CAMO_ALLOW_JS);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ensureJsExecutionEnabled(scope = 'JavaScript execution') {
|
|
11
|
+
if (isJsExecutionEnabled()) return;
|
|
12
|
+
throw new Error(`${scope} is disabled by default. Re-run with --js to enable.`);
|
|
13
|
+
}
|
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
export function buildCommentsHarvestScript(params = {}) {
|
|
2
|
-
const maxRounds = Math.max(1, Number(params.maxRounds ?? params.maxScrollRounds ?? 14) || 14);
|
|
3
|
-
const scrollStep = Math.max(120, Number(params.scrollStep ?? 420) || 420);
|
|
4
|
-
const settleMs = Math.max(80, Number(params.settleMs ?? 180) || 180);
|
|
5
|
-
const stallRounds = Math.max(1, Number(params.stallRounds ?? 2) || 2);
|
|
6
|
-
const requireBottom = params.requireBottom !== false;
|
|
7
|
-
const includeComments = params.includeComments !== false;
|
|
8
|
-
const commentsLimit = Math.max(0, Number(params.commentsLimit ?? 0) || 0);
|
|
9
|
-
const recoveryStuckRounds = Math.max(1, Number(params.recoveryStuckRounds ?? 2) || 2);
|
|
10
|
-
const recoveryUpRounds = Math.max(1, Number(params.recoveryUpRounds ?? 2) || 2);
|
|
11
|
-
const recoveryDownRounds = Math.max(1, Number(params.recoveryDownRounds ?? 3) || 3);
|
|
12
|
-
const maxRecoveries = Math.max(0, Number(params.maxRecoveries ?? 3) || 3);
|
|
13
|
-
const recoveryUpStep = Math.max(80, Number(params.recoveryUpStep ?? Math.floor(scrollStep * 0.75)) || Math.floor(scrollStep * 0.75));
|
|
14
|
-
const recoveryDownStep = Math.max(120, Number(params.recoveryDownStep ?? Math.floor(scrollStep * 1.3)) || Math.floor(scrollStep * 1.3));
|
|
15
|
-
const recoveryNoProgressRounds = Math.max(1, Number(params.recoveryNoProgressRounds ?? 3) || 3);
|
|
16
|
-
const progressDiffThreshold = Math.max(2, Number(
|
|
17
|
-
params.progressDiffThreshold ?? Math.max(12, Math.floor(scrollStep * 0.08)),
|
|
18
|
-
) || Math.max(12, Math.floor(scrollStep * 0.08)));
|
|
19
|
-
const recoveryDownBoostPerAttempt = Math.max(0, Number(params.recoveryDownBoostPerAttempt ?? 1) || 1);
|
|
20
|
-
const maxRecoveryDownBoost = Math.max(0, Number(params.maxRecoveryDownBoost ?? 2) || 2);
|
|
21
|
-
const adaptiveMaxRounds = params.adaptiveMaxRounds !== false;
|
|
22
|
-
const adaptiveExpectedPerRound = Math.max(1, Number(params.adaptiveExpectedPerRound ?? 6) || 6);
|
|
23
|
-
const adaptiveBufferRounds = Math.max(0, Number(params.adaptiveBufferRounds ?? 22) || 22);
|
|
24
|
-
const adaptiveMinBoostRounds = Math.max(0, Number(params.adaptiveMinBoostRounds ?? 36) || 36);
|
|
25
|
-
const adaptiveMaxRoundsCap = Math.max(maxRounds, Number(params.adaptiveMaxRoundsCap ?? 320) || 320);
|
|
26
|
-
|
|
27
|
-
return `(async () => {
|
|
28
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
29
|
-
const metricsState = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
|
|
30
|
-
state.metrics = metricsState;
|
|
31
|
-
metricsState.searchCount = Number(metricsState.searchCount || 0);
|
|
32
|
-
const detailSelectors = [
|
|
33
|
-
'.note-detail-mask',
|
|
34
|
-
'.note-detail-page',
|
|
35
|
-
'.note-detail-dialog',
|
|
36
|
-
'.note-detail-mask .detail-container',
|
|
37
|
-
'.note-detail-mask .media-container',
|
|
38
|
-
'.note-detail-mask .note-scroller',
|
|
39
|
-
'.note-detail-mask .note-content',
|
|
40
|
-
'.note-detail-mask .interaction-container',
|
|
41
|
-
'.note-detail-mask .comments-container',
|
|
42
|
-
];
|
|
43
|
-
const isVisible = (node) => {
|
|
44
|
-
if (!node || !(node instanceof HTMLElement)) return false;
|
|
45
|
-
const style = window.getComputedStyle(node);
|
|
46
|
-
if (!style) return false;
|
|
47
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
48
|
-
const rect = node.getBoundingClientRect();
|
|
49
|
-
return rect.width > 1 && rect.height > 1;
|
|
50
|
-
};
|
|
51
|
-
const isDetailVisible = () => detailSelectors.some((selector) => isVisible(document.querySelector(selector)));
|
|
52
|
-
const parseCountToken = (raw) => {
|
|
53
|
-
const token = String(raw || '').trim();
|
|
54
|
-
const matched = token.match(/^([0-9]+(?:\\.[0-9]+)?)(万|w|W)?$/);
|
|
55
|
-
if (!matched) return null;
|
|
56
|
-
const base = Number(matched[1]);
|
|
57
|
-
if (!Number.isFinite(base)) return null;
|
|
58
|
-
if (!matched[2]) return Math.round(base);
|
|
59
|
-
return Math.round(base * 10000);
|
|
60
|
-
};
|
|
61
|
-
const readExpectedCommentsCount = () => {
|
|
62
|
-
const scopeSelectors = [
|
|
63
|
-
'.note-detail-mask .interaction-container',
|
|
64
|
-
'.note-detail-mask .comments-container',
|
|
65
|
-
'.note-detail-page .interaction-container',
|
|
66
|
-
'.note-detail-page .comments-container',
|
|
67
|
-
'.note-detail-mask',
|
|
68
|
-
'.note-detail-page',
|
|
69
|
-
];
|
|
70
|
-
const patterns = [
|
|
71
|
-
/([0-9]+(?:\\.[0-9]+)?(?:万|w|W)?)\\s*条?评论/,
|
|
72
|
-
/评论\\s*([0-9]+(?:\\.[0-9]+)?(?:万|w|W)?)/,
|
|
73
|
-
/共\\s*([0-9]+(?:\\.[0-9]+)?(?:万|w|W)?)\\s*条/,
|
|
74
|
-
];
|
|
75
|
-
for (const selector of scopeSelectors) {
|
|
76
|
-
const root = document.querySelector(selector);
|
|
77
|
-
if (!root) continue;
|
|
78
|
-
const text = String(root.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
79
|
-
if (!text) continue;
|
|
80
|
-
for (const re of patterns) {
|
|
81
|
-
const matched = text.match(re);
|
|
82
|
-
if (!matched || !matched[1]) continue;
|
|
83
|
-
const parsed = parseCountToken(matched[1]);
|
|
84
|
-
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const scroller = document.querySelector('.note-scroller')
|
|
91
|
-
|| document.querySelector('.comments-el')
|
|
92
|
-
|| document.querySelector('.comments-container')
|
|
93
|
-
|| document.scrollingElement
|
|
94
|
-
|| document.documentElement;
|
|
95
|
-
const readMetrics = () => {
|
|
96
|
-
const target = scroller || document.documentElement;
|
|
97
|
-
return {
|
|
98
|
-
scrollTop: Number(target?.scrollTop || 0),
|
|
99
|
-
scrollHeight: Number(target?.scrollHeight || 0),
|
|
100
|
-
clientHeight: Number(target?.clientHeight || window.innerHeight || 0),
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
const commentMap = new Map();
|
|
104
|
-
const collect = (round) => {
|
|
105
|
-
const nodes = Array.from(document.querySelectorAll('.comment-item, [class*="comment-item"]'));
|
|
106
|
-
for (const item of nodes) {
|
|
107
|
-
const textNode = item.querySelector('.content, .comment-content, p');
|
|
108
|
-
const authorNode = item.querySelector('.name, .author, .user-name, [class*="author"], [class*="name"]');
|
|
109
|
-
const text = String((textNode && textNode.textContent) || '').trim();
|
|
110
|
-
const author = String((authorNode && authorNode.textContent) || '').trim();
|
|
111
|
-
if (!text) continue;
|
|
112
|
-
const key = author + '::' + text;
|
|
113
|
-
if (commentMap.has(key)) continue;
|
|
114
|
-
const likeNode = item.querySelector('.like-wrapper, .comment-like, [class*="like"]');
|
|
115
|
-
commentMap.set(key, {
|
|
116
|
-
author,
|
|
117
|
-
text,
|
|
118
|
-
liked: Boolean(likeNode && /like-active/.test(String(likeNode.className || ''))),
|
|
119
|
-
firstSeenRound: round,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const configuredMaxRounds = Number(${maxRounds});
|
|
125
|
-
const scrollStep = Number(${scrollStep});
|
|
126
|
-
const settleMs = Number(${settleMs});
|
|
127
|
-
const stallRounds = Number(${stallRounds});
|
|
128
|
-
const requireBottom = ${requireBottom ? 'true' : 'false'};
|
|
129
|
-
const includeComments = ${includeComments ? 'true' : 'false'};
|
|
130
|
-
const commentsLimit = Number(${commentsLimit});
|
|
131
|
-
const recoveryStuckRounds = Number(${recoveryStuckRounds});
|
|
132
|
-
const recoveryUpRounds = Number(${recoveryUpRounds});
|
|
133
|
-
const recoveryDownRounds = Number(${recoveryDownRounds});
|
|
134
|
-
const maxRecoveries = Number(${maxRecoveries});
|
|
135
|
-
const recoveryUpStep = Number(${recoveryUpStep});
|
|
136
|
-
const recoveryDownStep = Number(${recoveryDownStep});
|
|
137
|
-
const recoveryNoProgressRounds = Number(${recoveryNoProgressRounds});
|
|
138
|
-
const progressDiffThreshold = Number(${progressDiffThreshold});
|
|
139
|
-
const recoveryDownBoostPerAttempt = Number(${recoveryDownBoostPerAttempt});
|
|
140
|
-
const maxRecoveryDownBoost = Number(${maxRecoveryDownBoost});
|
|
141
|
-
const adaptiveMaxRounds = ${adaptiveMaxRounds ? 'true' : 'false'};
|
|
142
|
-
const adaptiveExpectedPerRound = Number(${adaptiveExpectedPerRound});
|
|
143
|
-
const adaptiveBufferRounds = Number(${adaptiveBufferRounds});
|
|
144
|
-
const adaptiveMinBoostRounds = Number(${adaptiveMinBoostRounds});
|
|
145
|
-
const adaptiveMaxRoundsCap = Number(${adaptiveMaxRoundsCap});
|
|
146
|
-
let maxRounds = configuredMaxRounds;
|
|
147
|
-
let maxRoundsSource = 'configured';
|
|
148
|
-
let budgetExpectedCommentsCount = null;
|
|
149
|
-
const applyAdaptiveRounds = (expectedCommentsCount) => {
|
|
150
|
-
const expected = Number(expectedCommentsCount);
|
|
151
|
-
if (!adaptiveMaxRounds || !Number.isFinite(expected) || expected <= 0) return false;
|
|
152
|
-
const estimatedRounds = Math.ceil(expected / adaptiveExpectedPerRound) + adaptiveBufferRounds;
|
|
153
|
-
if (estimatedRounds <= configuredMaxRounds) return false;
|
|
154
|
-
const boostedRounds = Math.max(configuredMaxRounds + adaptiveMinBoostRounds, estimatedRounds);
|
|
155
|
-
const nextRounds = Math.max(configuredMaxRounds, Math.min(adaptiveMaxRoundsCap, boostedRounds));
|
|
156
|
-
maxRounds = nextRounds;
|
|
157
|
-
maxRoundsSource = 'adaptive_expected_comments';
|
|
158
|
-
budgetExpectedCommentsCount = Math.round(expected);
|
|
159
|
-
return true;
|
|
160
|
-
};
|
|
161
|
-
applyAdaptiveRounds(readExpectedCommentsCount());
|
|
162
|
-
let rounds = 0;
|
|
163
|
-
let reachedBottom = false;
|
|
164
|
-
let exitReason = 'max_rounds_reached';
|
|
165
|
-
let noProgressRounds = 0;
|
|
166
|
-
let noNewCommentsStreak = 0;
|
|
167
|
-
let stalledScrollRounds = 0;
|
|
168
|
-
let noEffectStreak = 0;
|
|
169
|
-
let recoveries = 0;
|
|
170
|
-
let bestRemainingDiff = Number.POSITIVE_INFINITY;
|
|
171
|
-
const recoveryReasonCounts = {
|
|
172
|
-
no_effect: 0,
|
|
173
|
-
no_new_comments: 0,
|
|
174
|
-
};
|
|
175
|
-
const performScroll = async (deltaY, waitMs = settleMs) => {
|
|
176
|
-
if (typeof scroller?.scrollBy === 'function') {
|
|
177
|
-
scroller.scrollBy({ top: deltaY, behavior: 'auto' });
|
|
178
|
-
} else {
|
|
179
|
-
window.scrollBy({ top: deltaY, behavior: 'auto' });
|
|
180
|
-
}
|
|
181
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
for (let round = 1; round <= maxRounds; round += 1) {
|
|
185
|
-
rounds = round;
|
|
186
|
-
if (!isDetailVisible()) {
|
|
187
|
-
exitReason = 'detail_hidden';
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
const beforeCount = commentMap.size;
|
|
191
|
-
collect(round);
|
|
192
|
-
if ((budgetExpectedCommentsCount === null || budgetExpectedCommentsCount === undefined) && round <= 6) {
|
|
193
|
-
applyAdaptiveRounds(readExpectedCommentsCount());
|
|
194
|
-
}
|
|
195
|
-
const beforeMetrics = readMetrics();
|
|
196
|
-
const beforeDiff = beforeMetrics.scrollHeight - (beforeMetrics.scrollTop + beforeMetrics.clientHeight);
|
|
197
|
-
if (Number.isFinite(beforeDiff) && beforeDiff >= 0) {
|
|
198
|
-
bestRemainingDiff = Math.min(bestRemainingDiff, beforeDiff);
|
|
199
|
-
}
|
|
200
|
-
if (beforeDiff <= 6) {
|
|
201
|
-
reachedBottom = true;
|
|
202
|
-
exitReason = 'bottom_reached';
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const prevTop = beforeMetrics.scrollTop;
|
|
207
|
-
if (typeof scroller?.scrollBy === 'function') {
|
|
208
|
-
scroller.scrollBy({ top: scrollStep, behavior: 'auto' });
|
|
209
|
-
} else {
|
|
210
|
-
window.scrollBy({ top: scrollStep, behavior: 'auto' });
|
|
211
|
-
}
|
|
212
|
-
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
213
|
-
collect(round);
|
|
214
|
-
let afterMetrics = readMetrics();
|
|
215
|
-
let moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
|
|
216
|
-
if (!moved && typeof window.scrollBy === 'function') {
|
|
217
|
-
window.scrollBy({ top: Math.max(120, Math.floor(scrollStep / 2)), behavior: 'auto' });
|
|
218
|
-
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
219
|
-
collect(round);
|
|
220
|
-
afterMetrics = readMetrics();
|
|
221
|
-
moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
|
|
222
|
-
}
|
|
223
|
-
const increased = commentMap.size > beforeCount;
|
|
224
|
-
const afterDiff = afterMetrics.scrollHeight - (afterMetrics.scrollTop + afterMetrics.clientHeight);
|
|
225
|
-
const diffImproved = Number.isFinite(afterDiff) && Number.isFinite(beforeDiff)
|
|
226
|
-
? afterDiff <= (beforeDiff - progressDiffThreshold)
|
|
227
|
-
: false;
|
|
228
|
-
const bestImproved = Number.isFinite(afterDiff) && Number.isFinite(bestRemainingDiff)
|
|
229
|
-
? afterDiff <= (bestRemainingDiff - progressDiffThreshold)
|
|
230
|
-
: false;
|
|
231
|
-
if (Number.isFinite(afterDiff) && afterDiff >= 0) {
|
|
232
|
-
bestRemainingDiff = Math.min(bestRemainingDiff, afterDiff);
|
|
233
|
-
}
|
|
234
|
-
const progressedByScroll = diffImproved || bestImproved;
|
|
235
|
-
const progressed = increased || progressedByScroll;
|
|
236
|
-
if (!progressed) {
|
|
237
|
-
noProgressRounds += 1;
|
|
238
|
-
noNewCommentsStreak += 1;
|
|
239
|
-
} else {
|
|
240
|
-
noProgressRounds = 0;
|
|
241
|
-
noNewCommentsStreak = 0;
|
|
242
|
-
}
|
|
243
|
-
if (!moved) stalledScrollRounds += 1;
|
|
244
|
-
else stalledScrollRounds = 0;
|
|
245
|
-
if (!moved) noEffectStreak += 1;
|
|
246
|
-
else noEffectStreak = 0;
|
|
247
|
-
if (afterDiff <= 6) {
|
|
248
|
-
reachedBottom = true;
|
|
249
|
-
exitReason = 'bottom_reached';
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
let recoveryTrigger = null;
|
|
253
|
-
if (noNewCommentsStreak >= recoveryNoProgressRounds) recoveryTrigger = 'no_new_comments';
|
|
254
|
-
if (!recoveryTrigger && noEffectStreak >= recoveryStuckRounds) recoveryTrigger = 'no_effect';
|
|
255
|
-
if (recoveryTrigger) {
|
|
256
|
-
if (recoveries >= maxRecoveries) {
|
|
257
|
-
exitReason = recoveryTrigger === 'no_new_comments'
|
|
258
|
-
? 'no_new_comments_after_recovery_budget'
|
|
259
|
-
: 'scroll_stalled_after_recovery';
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
recoveries += 1;
|
|
263
|
-
recoveryReasonCounts[recoveryTrigger] += 1;
|
|
264
|
-
for (let i = 0; i < recoveryUpRounds; i += 1) {
|
|
265
|
-
await performScroll(-recoveryUpStep, settleMs + 120);
|
|
266
|
-
collect(round);
|
|
267
|
-
}
|
|
268
|
-
const downBoost = Math.min(maxRecoveryDownBoost, Math.max(0, recoveries - 1) * recoveryDownBoostPerAttempt);
|
|
269
|
-
const downRounds = recoveryDownRounds + downBoost;
|
|
270
|
-
for (let i = 0; i < downRounds; i += 1) {
|
|
271
|
-
await performScroll(recoveryDownStep, settleMs + 180);
|
|
272
|
-
collect(round);
|
|
273
|
-
}
|
|
274
|
-
const recoveredMetrics = readMetrics();
|
|
275
|
-
const recoveredDiff = recoveredMetrics.scrollHeight - (recoveredMetrics.scrollTop + recoveredMetrics.clientHeight);
|
|
276
|
-
const recoveredDiffImproved = Number.isFinite(recoveredDiff) && Number.isFinite(afterDiff)
|
|
277
|
-
? recoveredDiff <= (afterDiff - progressDiffThreshold)
|
|
278
|
-
: false;
|
|
279
|
-
const recoveredBestImproved = Number.isFinite(recoveredDiff) && Number.isFinite(bestRemainingDiff)
|
|
280
|
-
? recoveredDiff <= (bestRemainingDiff - progressDiffThreshold)
|
|
281
|
-
: false;
|
|
282
|
-
if (Number.isFinite(recoveredDiff) && recoveredDiff >= 0) {
|
|
283
|
-
bestRemainingDiff = Math.min(bestRemainingDiff, recoveredDiff);
|
|
284
|
-
}
|
|
285
|
-
if (recoveredDiff <= 6) {
|
|
286
|
-
reachedBottom = true;
|
|
287
|
-
exitReason = 'bottom_reached';
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
if (commentMap.size > beforeCount || recoveredDiffImproved || recoveredBestImproved) {
|
|
291
|
-
noProgressRounds = 0;
|
|
292
|
-
noNewCommentsStreak = 0;
|
|
293
|
-
}
|
|
294
|
-
noEffectStreak = 0;
|
|
295
|
-
stalledScrollRounds = 0;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (stalledScrollRounds >= stallRounds) {
|
|
299
|
-
exitReason = 'scroll_stalled';
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
if (noProgressRounds >= stallRounds) {
|
|
303
|
-
if (!requireBottom) {
|
|
304
|
-
exitReason = 'no_new_comments';
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (round === maxRounds) {
|
|
309
|
-
exitReason = 'max_rounds_reached';
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const comments = Array.from(commentMap.values())
|
|
314
|
-
.sort((a, b) => Number(a.firstSeenRound || 0) - Number(b.firstSeenRound || 0))
|
|
315
|
-
.map((item, index) => ({
|
|
316
|
-
index,
|
|
317
|
-
author: item.author,
|
|
318
|
-
text: item.text,
|
|
319
|
-
liked: item.liked,
|
|
320
|
-
}));
|
|
321
|
-
const metrics = readMetrics();
|
|
322
|
-
const detectedExpectedCommentsCount = readExpectedCommentsCount();
|
|
323
|
-
const expectedCommentsCount = Number.isFinite(Number(detectedExpectedCommentsCount))
|
|
324
|
-
? Number(detectedExpectedCommentsCount)
|
|
325
|
-
: (Number.isFinite(Number(budgetExpectedCommentsCount)) ? Number(budgetExpectedCommentsCount) : null);
|
|
326
|
-
const commentCoverageRate = Number.isFinite(Number(expectedCommentsCount)) && Number(expectedCommentsCount) > 0
|
|
327
|
-
? Number(Math.min(1, comments.length / Number(expectedCommentsCount)).toFixed(4))
|
|
328
|
-
: null;
|
|
329
|
-
|
|
330
|
-
state.currentComments = comments;
|
|
331
|
-
state.commentsCollectedAt = new Date().toISOString();
|
|
332
|
-
state.lastCommentsHarvest = {
|
|
333
|
-
noteId: state.currentNoteId || null,
|
|
334
|
-
searchCount: Number(metricsState.searchCount || 0),
|
|
335
|
-
collected: comments.length,
|
|
336
|
-
expectedCommentsCount,
|
|
337
|
-
commentCoverageRate,
|
|
338
|
-
recoveries,
|
|
339
|
-
recoveryReasonCounts,
|
|
340
|
-
maxRecoveries,
|
|
341
|
-
recoveryNoProgressRounds,
|
|
342
|
-
reachedBottom,
|
|
343
|
-
exitReason,
|
|
344
|
-
rounds,
|
|
345
|
-
configuredMaxRounds,
|
|
346
|
-
maxRounds,
|
|
347
|
-
maxRoundsSource,
|
|
348
|
-
budgetExpectedCommentsCount,
|
|
349
|
-
scroll: metrics,
|
|
350
|
-
at: state.commentsCollectedAt,
|
|
351
|
-
};
|
|
352
|
-
const payload = {
|
|
353
|
-
noteId: state.currentNoteId || null,
|
|
354
|
-
searchCount: Number(metricsState.searchCount || 0),
|
|
355
|
-
collected: comments.length,
|
|
356
|
-
expectedCommentsCount,
|
|
357
|
-
commentCoverageRate,
|
|
358
|
-
recoveries,
|
|
359
|
-
recoveryReasonCounts,
|
|
360
|
-
maxRecoveries,
|
|
361
|
-
recoveryNoProgressRounds,
|
|
362
|
-
firstComment: comments[0] || null,
|
|
363
|
-
reachedBottom,
|
|
364
|
-
exitReason,
|
|
365
|
-
rounds,
|
|
366
|
-
configuredMaxRounds,
|
|
367
|
-
maxRounds,
|
|
368
|
-
maxRoundsSource,
|
|
369
|
-
budgetExpectedCommentsCount,
|
|
370
|
-
scroll: metrics,
|
|
371
|
-
};
|
|
372
|
-
if (includeComments) {
|
|
373
|
-
const bounded = commentsLimit > 0 ? comments.slice(0, commentsLimit) : comments;
|
|
374
|
-
payload.comments = bounded;
|
|
375
|
-
payload.commentsTruncated = commentsLimit > 0 && comments.length > commentsLimit;
|
|
376
|
-
}
|
|
377
|
-
return payload;
|
|
378
|
-
})()`;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export function buildCommentMatchScript(params = {}) {
|
|
382
|
-
const keywords = (Array.isArray(params.keywords || params.matchKeywords)
|
|
383
|
-
? (params.keywords || params.matchKeywords)
|
|
384
|
-
: String(params.keywords || params.matchKeywords || '').split(','))
|
|
385
|
-
.map((item) => String(item))
|
|
386
|
-
.filter(Boolean);
|
|
387
|
-
const mode = String(params.mode || params.matchMode || 'any');
|
|
388
|
-
const minHits = Math.max(1, Number(params.minHits ?? params.matchMinHits ?? 1) || 1);
|
|
389
|
-
|
|
390
|
-
return `(async () => {
|
|
391
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
392
|
-
const rows = Array.isArray(state.currentComments) ? state.currentComments : [];
|
|
393
|
-
const keywords = ${JSON.stringify(keywords)};
|
|
394
|
-
const mode = ${JSON.stringify(mode)};
|
|
395
|
-
const minHits = Number(${minHits});
|
|
396
|
-
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
397
|
-
const tokens = keywords.map((item) => normalize(item)).filter(Boolean);
|
|
398
|
-
const matches = [];
|
|
399
|
-
for (const row of rows) {
|
|
400
|
-
const text = normalize(row.text);
|
|
401
|
-
if (!text || tokens.length === 0) continue;
|
|
402
|
-
const hits = tokens.filter((token) => text.includes(token));
|
|
403
|
-
if (mode === 'all' && hits.length < tokens.length) continue;
|
|
404
|
-
if (mode === 'atLeast' && hits.length < Math.max(1, minHits)) continue;
|
|
405
|
-
if (mode !== 'all' && mode !== 'atLeast' && hits.length === 0) continue;
|
|
406
|
-
matches.push({ index: row.index, hits });
|
|
407
|
-
}
|
|
408
|
-
state.matchedComments = matches;
|
|
409
|
-
state.matchRule = { tokens, mode, minHits };
|
|
410
|
-
return { matchCount: matches.length, mode, minHits: Math.max(1, minHits) };
|
|
411
|
-
})()`;
|
|
412
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { callAPI } from '../../../utils/browser-service.mjs';
|
|
2
|
-
|
|
3
|
-
export function withOperationHighlight(script, color = '#ff7a00') {
|
|
4
|
-
return `(() => {
|
|
5
|
-
const flashNode = (node, duration = 420) => {
|
|
6
|
-
if (!(node instanceof HTMLElement)) return;
|
|
7
|
-
const prevOutline = node.style.outline;
|
|
8
|
-
const prevOffset = node.style.outlineOffset;
|
|
9
|
-
const prevTransition = node.style.transition;
|
|
10
|
-
node.style.transition = 'outline 80ms ease';
|
|
11
|
-
node.style.outline = '2px solid ${color}';
|
|
12
|
-
node.style.outlineOffset = '2px';
|
|
13
|
-
setTimeout(() => {
|
|
14
|
-
node.style.outline = prevOutline;
|
|
15
|
-
node.style.outlineOffset = prevOffset;
|
|
16
|
-
node.style.transition = prevTransition;
|
|
17
|
-
}, duration);
|
|
18
|
-
};
|
|
19
|
-
const flashViewport = (duration = 420) => {
|
|
20
|
-
const root = document.documentElement;
|
|
21
|
-
if (!(root instanceof HTMLElement)) return;
|
|
22
|
-
const prevShadow = root.style.boxShadow;
|
|
23
|
-
const prevTransition = root.style.transition;
|
|
24
|
-
root.style.transition = 'box-shadow 80ms ease';
|
|
25
|
-
root.style.boxShadow = 'inset 0 0 0 3px ${color}';
|
|
26
|
-
setTimeout(() => {
|
|
27
|
-
root.style.boxShadow = prevShadow;
|
|
28
|
-
root.style.transition = prevTransition;
|
|
29
|
-
}, duration);
|
|
30
|
-
};
|
|
31
|
-
flashViewport();
|
|
32
|
-
const target = document.activeElement instanceof HTMLElement
|
|
33
|
-
? document.activeElement
|
|
34
|
-
: (document.body || document.documentElement);
|
|
35
|
-
flashNode(target);
|
|
36
|
-
return (${script});
|
|
37
|
-
})()`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function runEvaluateScript({ profileId, script, highlight = true }) {
|
|
41
|
-
const wrappedScript = highlight ? withOperationHighlight(script) : script;
|
|
42
|
-
return callAPI('evaluate', { profileId, script: wrappedScript });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function extractEvaluateResultData(payload) {
|
|
46
|
-
if (!payload || typeof payload !== 'object') return null;
|
|
47
|
-
if ('result' in payload) return payload.result;
|
|
48
|
-
if (payload.data && typeof payload.data === 'object' && 'result' in payload.data) {
|
|
49
|
-
return payload.data.result;
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function extractScreenshotBase64(payload) {
|
|
55
|
-
if (!payload || typeof payload !== 'object') return '';
|
|
56
|
-
if (typeof payload.data === 'string' && payload.data) return payload.data;
|
|
57
|
-
if (payload.result && typeof payload.result === 'object' && typeof payload.result.data === 'string') {
|
|
58
|
-
return payload.result.data;
|
|
59
|
-
}
|
|
60
|
-
if (payload.data && typeof payload.data === 'object' && typeof payload.data.data === 'string') {
|
|
61
|
-
return payload.data.data;
|
|
62
|
-
}
|
|
63
|
-
return '';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function evaluateWithScript({ profileId, script, message, highlight }) {
|
|
67
|
-
const result = await runEvaluateScript({ profileId, script, highlight });
|
|
68
|
-
return { ok: true, code: 'OPERATION_DONE', message, data: result };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function createEvaluateHandler(message, buildScript) {
|
|
72
|
-
return async ({ profileId, params }) => {
|
|
73
|
-
const script = buildScript(params);
|
|
74
|
-
const highlight = params.highlight !== false;
|
|
75
|
-
return evaluateWithScript({ profileId, script, message, highlight });
|
|
76
|
-
};
|
|
77
|
-
}
|