@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
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
function sanitizeForPath(name, fallback = 'unknown') {
|
|
6
|
-
const text = String(name || '').trim();
|
|
7
|
-
if (!text) return fallback;
|
|
8
|
-
const cleaned = text.replace(/[\\/:"*?<>|]+/g, '_').trim();
|
|
9
|
-
return cleaned || fallback;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function resolveDownloadRoot(customRoot) {
|
|
13
|
-
const fromParams = String(customRoot || '').trim();
|
|
14
|
-
if (fromParams) return path.resolve(fromParams);
|
|
15
|
-
const fromEnv = String(process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR || '').trim();
|
|
16
|
-
if (fromEnv) return path.resolve(fromEnv);
|
|
17
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
18
|
-
return path.join(home, '.webauto', 'download');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function resolveXhsOutputContext({
|
|
22
|
-
params = {},
|
|
23
|
-
state = {},
|
|
24
|
-
noteId = null,
|
|
25
|
-
} = {}) {
|
|
26
|
-
const keywordRaw = String(params.keyword || state.keyword || 'unknown').trim();
|
|
27
|
-
const envRaw = String(params.env || state.env || 'debug').trim();
|
|
28
|
-
const resolvedNoteId = String(noteId || params.noteId || state.currentNoteId || 'unknown').trim();
|
|
29
|
-
const root = resolveDownloadRoot(params.outputRoot || params.downloadRoot || params.rootDir);
|
|
30
|
-
const keyword = sanitizeForPath(keywordRaw, 'unknown');
|
|
31
|
-
const env = sanitizeForPath(envRaw, 'debug');
|
|
32
|
-
const note = sanitizeForPath(resolvedNoteId, 'unknown');
|
|
33
|
-
const keywordDir = path.join(root, 'xiaohongshu', env, keyword);
|
|
34
|
-
const noteDir = path.join(keywordDir, note);
|
|
35
|
-
return {
|
|
36
|
-
root,
|
|
37
|
-
env,
|
|
38
|
-
keyword,
|
|
39
|
-
noteId: note,
|
|
40
|
-
keywordDir,
|
|
41
|
-
noteDir,
|
|
42
|
-
commentsPath: path.join(noteDir, 'comments.jsonl'),
|
|
43
|
-
likeStatePath: path.join(keywordDir, '.like-state.jsonl'),
|
|
44
|
-
likeEvidenceDir: path.join(keywordDir, 'like-evidence', note),
|
|
45
|
-
virtualLikeEvidenceDir: path.join(keywordDir, 'virtual-like', note),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function ensureDir(dirPath) {
|
|
50
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function readJsonlRows(filePath) {
|
|
54
|
-
try {
|
|
55
|
-
const text = await fs.readFile(filePath, 'utf8');
|
|
56
|
-
return text
|
|
57
|
-
.split('\n')
|
|
58
|
-
.map((line) => line.trim())
|
|
59
|
-
.filter(Boolean)
|
|
60
|
-
.map((line) => {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(line);
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
.filter(Boolean);
|
|
68
|
-
} catch {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function appendJsonlRows(filePath, rows) {
|
|
74
|
-
if (!Array.isArray(rows) || rows.length === 0) return;
|
|
75
|
-
await ensureDir(path.dirname(filePath));
|
|
76
|
-
const payload = rows.map((row) => JSON.stringify(row)).join('\n');
|
|
77
|
-
await fs.appendFile(filePath, `${payload}\n`, 'utf8');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function normalizeCommentRow(noteId, row) {
|
|
81
|
-
return {
|
|
82
|
-
noteId: String(noteId || ''),
|
|
83
|
-
userName: String(row?.userName || row?.author || row?.user_name || '').trim(),
|
|
84
|
-
userId: String(row?.userId || row?.user_id || '').trim(),
|
|
85
|
-
content: String(row?.content || row?.text || '').replace(/\s+/g, ' ').trim(),
|
|
86
|
-
time: String(row?.time || row?.timestamp || '').trim(),
|
|
87
|
-
likeCount: Number(row?.likeCount || row?.like_count || 0),
|
|
88
|
-
ts: new Date().toISOString(),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function commentDedupKey(row) {
|
|
93
|
-
return `${String(row?.userId || '')}:${String(row?.content || '')}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function mergeCommentsJsonl({ filePath, noteId, comments = [] }) {
|
|
97
|
-
const existing = await readJsonlRows(filePath);
|
|
98
|
-
const seen = new Set(
|
|
99
|
-
existing
|
|
100
|
-
.map((row) => commentDedupKey(row))
|
|
101
|
-
.filter((key) => key && !key.endsWith(':')),
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const added = [];
|
|
105
|
-
for (const row of comments) {
|
|
106
|
-
const normalized = normalizeCommentRow(noteId, row);
|
|
107
|
-
if (!normalized.content) continue;
|
|
108
|
-
const key = commentDedupKey(normalized);
|
|
109
|
-
if (!key || key.endsWith(':')) continue;
|
|
110
|
-
if (seen.has(key)) continue;
|
|
111
|
-
seen.add(key);
|
|
112
|
-
added.push(normalized);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
await appendJsonlRows(filePath, added);
|
|
116
|
-
return {
|
|
117
|
-
filePath,
|
|
118
|
-
added: added.length,
|
|
119
|
-
existing: existing.length,
|
|
120
|
-
total: existing.length + added.length,
|
|
121
|
-
rowsAdded: added,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function makeLikeSignature({ noteId, userId = '', userName = '', text = '' }) {
|
|
126
|
-
const normalizedText = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
127
|
-
return [
|
|
128
|
-
String(noteId || '').trim(),
|
|
129
|
-
String(userId || '').trim(),
|
|
130
|
-
String(userName || '').trim(),
|
|
131
|
-
normalizedText,
|
|
132
|
-
].join('|');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export async function loadLikedSignatures(filePath) {
|
|
136
|
-
const rows = await readJsonlRows(filePath);
|
|
137
|
-
const out = new Set();
|
|
138
|
-
for (const row of rows) {
|
|
139
|
-
const signature = String(row?.signature || '').trim();
|
|
140
|
-
if (signature) out.add(signature);
|
|
141
|
-
}
|
|
142
|
-
return out;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function appendLikedSignature(filePath, signature, extra = {}) {
|
|
146
|
-
const value = String(signature || '').trim();
|
|
147
|
-
if (!value) return;
|
|
148
|
-
await appendJsonlRows(filePath, [{
|
|
149
|
-
ts: new Date().toISOString(),
|
|
150
|
-
signature: value,
|
|
151
|
-
...extra,
|
|
152
|
-
}]);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function savePngBase64(filePath, base64Data) {
|
|
156
|
-
const payload = String(base64Data || '').trim();
|
|
157
|
-
if (!payload) return null;
|
|
158
|
-
await ensureDir(path.dirname(filePath));
|
|
159
|
-
await fs.writeFile(filePath, Buffer.from(payload, 'base64'));
|
|
160
|
-
return filePath;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function writeJsonFile(filePath, payload) {
|
|
164
|
-
await ensureDir(path.dirname(filePath));
|
|
165
|
-
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
166
|
-
return filePath;
|
|
167
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
export function buildSubmitSearchScript(params = {}) {
|
|
2
|
-
const keyword = String(params.keyword || '').trim();
|
|
3
|
-
return `(async () => {
|
|
4
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
5
|
-
const metrics = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
|
|
6
|
-
state.metrics = metrics;
|
|
7
|
-
metrics.searchCount = Number(metrics.searchCount || 0) + 1;
|
|
8
|
-
metrics.lastSearchAt = new Date().toISOString();
|
|
9
|
-
const input = document.querySelector('#search-input, input.search-input');
|
|
10
|
-
if (!(input instanceof HTMLInputElement)) {
|
|
11
|
-
throw new Error('SEARCH_INPUT_NOT_FOUND');
|
|
12
|
-
}
|
|
13
|
-
const targetKeyword = ${JSON.stringify(keyword)};
|
|
14
|
-
if (targetKeyword && input.value !== targetKeyword) {
|
|
15
|
-
input.focus();
|
|
16
|
-
input.value = targetKeyword;
|
|
17
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
18
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
19
|
-
}
|
|
20
|
-
const enterEvent = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
|
|
21
|
-
const beforeUrl = window.location.href;
|
|
22
|
-
input.focus();
|
|
23
|
-
input.dispatchEvent(new KeyboardEvent('keydown', enterEvent));
|
|
24
|
-
input.dispatchEvent(new KeyboardEvent('keypress', enterEvent));
|
|
25
|
-
input.dispatchEvent(new KeyboardEvent('keyup', enterEvent));
|
|
26
|
-
const candidates = ['.input-button .search-icon', '.input-button', 'button.min-width-search-icon'];
|
|
27
|
-
let clickedSelector = null;
|
|
28
|
-
for (const selector of candidates) {
|
|
29
|
-
const button = document.querySelector(selector);
|
|
30
|
-
if (!button) continue;
|
|
31
|
-
if (button instanceof HTMLElement) button.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
32
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
33
|
-
button.click();
|
|
34
|
-
clickedSelector = selector;
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
const form = input.closest('form');
|
|
38
|
-
if (form) {
|
|
39
|
-
if (typeof form.requestSubmit === 'function') form.requestSubmit();
|
|
40
|
-
else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
41
|
-
}
|
|
42
|
-
await new Promise((resolve) => setTimeout(resolve, 320));
|
|
43
|
-
return {
|
|
44
|
-
submitted: true,
|
|
45
|
-
via: clickedSelector || 'enter_or_form_submit',
|
|
46
|
-
beforeUrl,
|
|
47
|
-
afterUrl: window.location.href,
|
|
48
|
-
searchCount: metrics.searchCount,
|
|
49
|
-
};
|
|
50
|
-
})()`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function buildOpenDetailScript(params = {}) {
|
|
54
|
-
const mode = String(params.mode || 'first').trim().toLowerCase();
|
|
55
|
-
const maxNotes = Math.max(1, Number(params.maxNotes ?? params.limit ?? 20) || 20);
|
|
56
|
-
const keyword = String(params.keyword || '').trim();
|
|
57
|
-
|
|
58
|
-
return `(async () => {
|
|
59
|
-
const STATE_KEY = '__camoXhsState';
|
|
60
|
-
const normalizeVisited = (value) => {
|
|
61
|
-
if (!Array.isArray(value)) return [];
|
|
62
|
-
return value
|
|
63
|
-
.map((item) => String(item || '').trim())
|
|
64
|
-
.filter(Boolean);
|
|
65
|
-
};
|
|
66
|
-
const mergeVisited = (a, b) => Array.from(new Set([
|
|
67
|
-
...normalizeVisited(a),
|
|
68
|
-
...normalizeVisited(b),
|
|
69
|
-
]));
|
|
70
|
-
const loadState = () => {
|
|
71
|
-
const inMemory = window.__camoXhsState && typeof window.__camoXhsState === 'object' ? window.__camoXhsState : {};
|
|
72
|
-
try {
|
|
73
|
-
const stored = localStorage.getItem(STATE_KEY);
|
|
74
|
-
if (!stored) return { ...inMemory };
|
|
75
|
-
const parsed = JSON.parse(stored);
|
|
76
|
-
if (!parsed || typeof parsed !== 'object') return { ...inMemory };
|
|
77
|
-
const merged = { ...inMemory, ...parsed };
|
|
78
|
-
merged.visitedNoteIds = mergeVisited(parsed.visitedNoteIds, inMemory.visitedNoteIds);
|
|
79
|
-
return merged;
|
|
80
|
-
} catch {
|
|
81
|
-
return { ...inMemory };
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
const saveState = (nextState) => {
|
|
85
|
-
window.__camoXhsState = nextState;
|
|
86
|
-
try { localStorage.setItem(STATE_KEY, JSON.stringify(nextState)); } catch {}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const state = loadState();
|
|
90
|
-
if (!Array.isArray(state.visitedNoteIds)) state.visitedNoteIds = [];
|
|
91
|
-
state.maxNotes = Number(${maxNotes});
|
|
92
|
-
if (${JSON.stringify(keyword)}) state.keyword = ${JSON.stringify(keyword)};
|
|
93
|
-
|
|
94
|
-
if (${JSON.stringify(mode)} === 'next' && state.visitedNoteIds.length >= state.maxNotes) {
|
|
95
|
-
throw new Error('AUTOSCRIPT_DONE_MAX_NOTES');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const nodes = Array.from(document.querySelectorAll('.note-item'))
|
|
99
|
-
.map((item, index) => {
|
|
100
|
-
const cover = item.querySelector('a.cover');
|
|
101
|
-
if (!cover) return null;
|
|
102
|
-
const href = String(cover.getAttribute('href') || '').trim();
|
|
103
|
-
const noteId = href.split('/').filter(Boolean).pop() || ('idx_' + index);
|
|
104
|
-
return { cover, href, noteId };
|
|
105
|
-
})
|
|
106
|
-
.filter(Boolean);
|
|
107
|
-
if (nodes.length === 0) throw new Error('NO_SEARCH_RESULT_ITEM');
|
|
108
|
-
|
|
109
|
-
let next = null;
|
|
110
|
-
if (${JSON.stringify(mode)} === 'next') {
|
|
111
|
-
next = nodes.find((row) => !state.visitedNoteIds.includes(row.noteId));
|
|
112
|
-
if (!next) throw new Error('AUTOSCRIPT_DONE_NO_MORE_NOTES');
|
|
113
|
-
} else {
|
|
114
|
-
next = nodes.find((row) => !state.visitedNoteIds.includes(row.noteId)) || nodes[0];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const detailSelectors = [
|
|
118
|
-
'.note-detail-mask',
|
|
119
|
-
'.note-detail-page',
|
|
120
|
-
'.note-detail-dialog',
|
|
121
|
-
'.note-detail-mask .detail-container',
|
|
122
|
-
'.note-detail-mask .media-container',
|
|
123
|
-
'.note-detail-mask .note-scroller',
|
|
124
|
-
'.note-detail-mask .note-content',
|
|
125
|
-
'.note-detail-mask .interaction-container',
|
|
126
|
-
'.note-detail-mask .comments-container',
|
|
127
|
-
];
|
|
128
|
-
const isVisible = (node) => {
|
|
129
|
-
if (!node || !(node instanceof HTMLElement)) return false;
|
|
130
|
-
const style = window.getComputedStyle(node);
|
|
131
|
-
if (!style) return false;
|
|
132
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
133
|
-
const rect = node.getBoundingClientRect();
|
|
134
|
-
return rect.width > 1 && rect.height > 1;
|
|
135
|
-
};
|
|
136
|
-
const isDetailReady = () => detailSelectors.some((selector) => isVisible(document.querySelector(selector)));
|
|
137
|
-
|
|
138
|
-
next.cover.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
139
|
-
await new Promise((resolve) => setTimeout(resolve, 140));
|
|
140
|
-
const beforeUrl = window.location.href;
|
|
141
|
-
next.cover.click();
|
|
142
|
-
|
|
143
|
-
let detailReady = false;
|
|
144
|
-
for (let i = 0; i < 60; i += 1) {
|
|
145
|
-
if (isDetailReady()) {
|
|
146
|
-
detailReady = true;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
150
|
-
}
|
|
151
|
-
if (!detailReady) {
|
|
152
|
-
throw new Error('DETAIL_OPEN_TIMEOUT');
|
|
153
|
-
}
|
|
154
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
155
|
-
const afterUrl = window.location.href;
|
|
156
|
-
|
|
157
|
-
if (!state.visitedNoteIds.includes(next.noteId)) state.visitedNoteIds.push(next.noteId);
|
|
158
|
-
state.currentNoteId = next.noteId;
|
|
159
|
-
state.currentHref = next.href;
|
|
160
|
-
state.lastListUrl = beforeUrl;
|
|
161
|
-
saveState(state);
|
|
162
|
-
return {
|
|
163
|
-
opened: true,
|
|
164
|
-
source: ${JSON.stringify(mode)} === 'next' ? 'open_next_detail' : 'open_first_detail',
|
|
165
|
-
noteId: next.noteId,
|
|
166
|
-
visited: state.visitedNoteIds.length,
|
|
167
|
-
maxNotes: state.maxNotes,
|
|
168
|
-
openByClick: true,
|
|
169
|
-
beforeUrl,
|
|
170
|
-
afterUrl,
|
|
171
|
-
};
|
|
172
|
-
})()`;
|
|
173
|
-
}
|
|
174
|
-
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { asErrorPayload } from '../../container/runtime-core/utils.mjs';
|
|
2
|
-
import {
|
|
3
|
-
createEvaluateHandler,
|
|
4
|
-
extractEvaluateResultData,
|
|
5
|
-
evaluateWithScript,
|
|
6
|
-
runEvaluateScript,
|
|
7
|
-
} from './xhs/common.mjs';
|
|
8
|
-
import { buildCommentsHarvestScript, buildCommentMatchScript } from './xhs/comments.mjs';
|
|
9
|
-
import {
|
|
10
|
-
buildCloseDetailScript,
|
|
11
|
-
buildDetailHarvestScript,
|
|
12
|
-
buildExpandRepliesScript,
|
|
13
|
-
} from './xhs/detail.mjs';
|
|
14
|
-
import {
|
|
15
|
-
buildCommentReplyScript,
|
|
16
|
-
executeCommentLikeOperation,
|
|
17
|
-
} from './xhs/interaction.mjs';
|
|
18
|
-
import {
|
|
19
|
-
mergeCommentsJsonl,
|
|
20
|
-
resolveXhsOutputContext,
|
|
21
|
-
} from './xhs/persistence.mjs';
|
|
22
|
-
import { buildOpenDetailScript, buildSubmitSearchScript } from './xhs/search.mjs';
|
|
23
|
-
|
|
24
|
-
function buildReadStateScript() {
|
|
25
|
-
return `(() => {
|
|
26
|
-
const state = window.__camoXhsState || {};
|
|
27
|
-
return {
|
|
28
|
-
keyword: state.keyword || null,
|
|
29
|
-
currentNoteId: state.currentNoteId || null,
|
|
30
|
-
lastCommentsHarvest: state.lastCommentsHarvest && typeof state.lastCommentsHarvest === 'object'
|
|
31
|
-
? state.lastCommentsHarvest
|
|
32
|
-
: null,
|
|
33
|
-
};
|
|
34
|
-
})()`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function readXhsRuntimeState(profileId) {
|
|
38
|
-
try {
|
|
39
|
-
const payload = await runEvaluateScript({
|
|
40
|
-
profileId,
|
|
41
|
-
script: buildReadStateScript(),
|
|
42
|
-
highlight: false,
|
|
43
|
-
});
|
|
44
|
-
return extractEvaluateResultData(payload) || {};
|
|
45
|
-
} catch {
|
|
46
|
-
return {};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function handleRaiseError({ params }) {
|
|
51
|
-
const code = String(params.code || params.message || 'AUTOSCRIPT_ABORT').trim();
|
|
52
|
-
return asErrorPayload('OPERATION_FAILED', code || 'AUTOSCRIPT_ABORT');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function executeCommentsHarvestOperation({ profileId, params = {} }) {
|
|
56
|
-
const script = buildCommentsHarvestScript(params);
|
|
57
|
-
const highlight = params.highlight !== false;
|
|
58
|
-
const operationResult = await evaluateWithScript({
|
|
59
|
-
profileId,
|
|
60
|
-
script,
|
|
61
|
-
message: 'xhs_comments_harvest done',
|
|
62
|
-
highlight,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const payload = extractEvaluateResultData(operationResult.data) || {};
|
|
66
|
-
const shouldPersistComments = params.persistComments === true || params.persistCollectedComments === true;
|
|
67
|
-
const includeComments = params.includeComments !== false;
|
|
68
|
-
const comments = Array.isArray(payload.comments) ? payload.comments : [];
|
|
69
|
-
|
|
70
|
-
if (!shouldPersistComments || !includeComments || comments.length === 0) {
|
|
71
|
-
return {
|
|
72
|
-
...operationResult,
|
|
73
|
-
data: {
|
|
74
|
-
...payload,
|
|
75
|
-
commentsPath: null,
|
|
76
|
-
commentsAdded: 0,
|
|
77
|
-
commentsTotal: Number(payload.collected || comments.length || 0),
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const state = await readXhsRuntimeState(profileId);
|
|
83
|
-
const output = resolveXhsOutputContext({
|
|
84
|
-
params,
|
|
85
|
-
state,
|
|
86
|
-
noteId: payload.noteId || state.currentNoteId || params.noteId,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const merged = await mergeCommentsJsonl({
|
|
90
|
-
filePath: output.commentsPath,
|
|
91
|
-
noteId: output.noteId,
|
|
92
|
-
comments,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
ok: true,
|
|
97
|
-
code: 'OPERATION_DONE',
|
|
98
|
-
message: 'xhs_comments_harvest done',
|
|
99
|
-
data: {
|
|
100
|
-
...payload,
|
|
101
|
-
commentsPath: merged.filePath,
|
|
102
|
-
commentsAdded: merged.added,
|
|
103
|
-
commentsTotal: merged.total,
|
|
104
|
-
outputNoteDir: output.noteDir,
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const XHS_ACTION_HANDLERS = {
|
|
110
|
-
raise_error: handleRaiseError,
|
|
111
|
-
xhs_submit_search: createEvaluateHandler('xhs_submit_search done', buildSubmitSearchScript),
|
|
112
|
-
xhs_open_detail: createEvaluateHandler('xhs_open_detail done', buildOpenDetailScript),
|
|
113
|
-
xhs_detail_harvest: createEvaluateHandler('xhs_detail_harvest done', buildDetailHarvestScript),
|
|
114
|
-
xhs_expand_replies: createEvaluateHandler('xhs_expand_replies done', buildExpandRepliesScript),
|
|
115
|
-
xhs_comments_harvest: executeCommentsHarvestOperation,
|
|
116
|
-
xhs_comment_match: createEvaluateHandler('xhs_comment_match done', buildCommentMatchScript),
|
|
117
|
-
xhs_comment_like: executeCommentLikeOperation,
|
|
118
|
-
xhs_comment_reply: createEvaluateHandler('xhs_comment_reply done', buildCommentReplyScript),
|
|
119
|
-
xhs_close_detail: createEvaluateHandler('xhs_close_detail done', buildCloseDetailScript),
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export function isXhsAutoscriptAction(action) {
|
|
123
|
-
const normalized = String(action || '').trim();
|
|
124
|
-
return normalized === 'raise_error' || normalized.startsWith('xhs_');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function executeXhsAutoscriptOperation({ profileId, action, params = {} }) {
|
|
128
|
-
const handler = XHS_ACTION_HANDLERS[action];
|
|
129
|
-
if (!handler) {
|
|
130
|
-
return asErrorPayload('UNSUPPORTED_OPERATION', `Unsupported xhs operation: ${action}`);
|
|
131
|
-
}
|
|
132
|
-
return handler({ profileId, params });
|
|
133
|
-
}
|