@web-auto/webauto 0.1.18 → 0.1.19
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 +122 -53
- package/apps/desktop-console/dist/main/index.mjs +227 -12
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +282 -16
- package/apps/desktop-console/entry/ui-console.mjs +46 -15
- package/apps/webauto/entry/account.mjs +126 -27
- package/apps/webauto/entry/lib/account-detect.mjs +399 -9
- package/apps/webauto/entry/lib/account-store.mjs +201 -109
- package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
- package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
- package/apps/webauto/entry/lib/profilepool.mjs +12 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
- package/apps/webauto/entry/lib/session-init.mjs +227 -0
- package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
- package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
- package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
- package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
- package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
- package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
- package/apps/webauto/entry/profilepool.mjs +56 -9
- package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
- package/apps/webauto/entry/weibo-unified.mjs +84 -11
- package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
- package/apps/webauto/entry/xhs-unified.mjs +92 -997
- package/bin/webauto.mjs +22 -4
- package/dist/modules/camo-backend/src/index.js +33 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
- package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
- package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
- package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
- package/dist/modules/workflow/src/runner.js +2 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
- package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
- package/modules/camo-backend/src/index.ts +31 -0
- package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
- package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
- package/modules/camo-backend/src/internal/ws-server.ts +17 -17
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
- package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
- package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
- package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
- package/modules/workflow/blocks/EnsureSession.ts +0 -4
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
- package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
- package/modules/workflow/src/runner.ts +2 -0
- package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
- package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
- package/package.json +2 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* smart-reply-cli.mjs
|
|
4
|
+
*
|
|
5
|
+
* 智能回复 CLI 工具
|
|
6
|
+
*
|
|
7
|
+
* 流程:
|
|
8
|
+
* 1. 从已爬取的评论数据中分析高频关键词
|
|
9
|
+
* 2. 展示给用户选择
|
|
10
|
+
* 3. 用户指定回复中心意思
|
|
11
|
+
* 4. dryrun 模式验证
|
|
12
|
+
* 5. 真实回复(可选)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import { createInterface } from 'readline';
|
|
19
|
+
import { spawn } from 'child_process';
|
|
20
|
+
|
|
21
|
+
// 统一路径解析(与项目一致)
|
|
22
|
+
function resolveWebautoRoot() {
|
|
23
|
+
const explicit = String(process.env.WEBAUTO_HOME || '').trim();
|
|
24
|
+
if (explicit) return explicit;
|
|
25
|
+
|
|
26
|
+
const legacy = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || '').trim();
|
|
27
|
+
if (legacy) {
|
|
28
|
+
const base = path.basename(legacy).toLowerCase();
|
|
29
|
+
return (base === '.webauto' || base === 'webauto') ? legacy : path.join(legacy, '.webauto');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
const dExists = fs.existsSync('D:\\');
|
|
34
|
+
return dExists ? 'D:\\webauto' : path.join(os.homedir(), '.webauto');
|
|
35
|
+
}
|
|
36
|
+
return path.join(os.homedir(), '.webauto');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const WEBAUTO_ROOT = resolveWebautoRoot();
|
|
40
|
+
const DOWNLOAD_ROOT = path.join(WEBAUTO_ROOT, 'download');
|
|
41
|
+
|
|
42
|
+
// 简单的中文分词
|
|
43
|
+
function tokenize(text) {
|
|
44
|
+
return text
|
|
45
|
+
.replace(/[,。!?、;:""''()【】《》\s]+/g, ' ')
|
|
46
|
+
.split(/\s+/)
|
|
47
|
+
.filter(t => t.length >= 2 && t.length <= 10);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 统计词频
|
|
51
|
+
function countKeywords(comments) {
|
|
52
|
+
const wordFreq = new Map();
|
|
53
|
+
|
|
54
|
+
for (const comment of comments) {
|
|
55
|
+
const tokens = tokenize(comment.content || comment.text || '');
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
wordFreq.set(token, (wordFreq.get(token) || 0) + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Array.from(wordFreq.entries())
|
|
62
|
+
.sort((a, b) => b[1] - a[1])
|
|
63
|
+
.map(([word, count]) => ({ word, count }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 加载评论数据
|
|
67
|
+
function loadComments(keyword) {
|
|
68
|
+
const comments = [];
|
|
69
|
+
|
|
70
|
+
const searchDirs = [DOWNLOAD_ROOT];
|
|
71
|
+
|
|
72
|
+
for (const searchDir of searchDirs) {
|
|
73
|
+
if (!fs.existsSync(searchDir)) continue;
|
|
74
|
+
|
|
75
|
+
const findJsonlFiles = (dir) => {
|
|
76
|
+
const files = [];
|
|
77
|
+
try {
|
|
78
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (entry.name === 'node_modules') continue;
|
|
81
|
+
const full = path.join(dir, entry.name);
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
files.push(...findJsonlFiles(full));
|
|
84
|
+
} else if (entry.name === 'comments.jsonl') {
|
|
85
|
+
files.push(full);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
return files;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const files = findJsonlFiles(searchDir);
|
|
93
|
+
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
if (keyword && !file.includes(keyword)) continue;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const lines = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean);
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
try {
|
|
101
|
+
const comment = JSON.parse(line);
|
|
102
|
+
comments.push({ ...comment, file });
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return comments;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// CLI 交互
|
|
113
|
+
async function main() {
|
|
114
|
+
const rl = createInterface({
|
|
115
|
+
input: process.stdin,
|
|
116
|
+
output: process.stdout,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const question = (prompt) => new Promise((resolve) => {
|
|
120
|
+
rl.question(prompt, resolve);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log('========================================');
|
|
124
|
+
console.log(' 小红书智能回复工具');
|
|
125
|
+
console.log('========================================\n');
|
|
126
|
+
console.log(`数据目录: ${WEBAUTO_ROOT}\n`);
|
|
127
|
+
|
|
128
|
+
// 1. 输入关键词
|
|
129
|
+
const keyword = await question('请输入已爬取的关键词目录(如 deepseek): ');
|
|
130
|
+
|
|
131
|
+
console.log('\n正在加载评论数据...');
|
|
132
|
+
const comments = loadComments(keyword.trim());
|
|
133
|
+
|
|
134
|
+
if (comments.length === 0) {
|
|
135
|
+
console.log('未找到评论数据,请先运行爬取任务');
|
|
136
|
+
rl.close();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`找到 ${comments.length} 条评论\n`);
|
|
141
|
+
|
|
142
|
+
// 2. 分析高频词
|
|
143
|
+
console.log('正在分析评论关键词...\n');
|
|
144
|
+
const keywords = countKeywords(comments).slice(0, 20);
|
|
145
|
+
|
|
146
|
+
console.log('高频关键词 TOP 20:');
|
|
147
|
+
console.log('-'.repeat(40));
|
|
148
|
+
keywords.forEach((item, i) => {
|
|
149
|
+
console.log(`${(i + 1).toString().padStart(2)}. ${item.word.padEnd(15)} (${item.count}次)`);
|
|
150
|
+
});
|
|
151
|
+
console.log('-'.repeat(40));
|
|
152
|
+
|
|
153
|
+
// 3. 选择关键词
|
|
154
|
+
const selectedKeywords = await question('\n请选择要匹配的关键词(多个用逗号分隔,如: 牛逼,厉害): ');
|
|
155
|
+
const matchKeywords = selectedKeywords.split(/[,,]/).map(k => k.trim()).filter(Boolean);
|
|
156
|
+
|
|
157
|
+
if (matchKeywords.length === 0) {
|
|
158
|
+
console.log('未选择关键词');
|
|
159
|
+
rl.close();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 4. 筛选命中评论
|
|
164
|
+
const matchedComments = comments.filter(c => {
|
|
165
|
+
const text = (c.content || c.text || '').toLowerCase();
|
|
166
|
+
return matchKeywords.some(k => text.includes(k.toLowerCase()));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
console.log(`\n命中评论数: ${matchedComments.length}`);
|
|
170
|
+
|
|
171
|
+
if (matchedComments.length === 0) {
|
|
172
|
+
console.log('没有命中任何评论');
|
|
173
|
+
rl.close();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 5. 展示部分命中评论
|
|
178
|
+
console.log('\n命中评论预览 (前5条):');
|
|
179
|
+
console.log('-'.repeat(60));
|
|
180
|
+
matchedComments.slice(0, 5).forEach((c, i) => {
|
|
181
|
+
const text = (c.content || c.text || '').slice(0, 50);
|
|
182
|
+
console.log(`${i + 1}. [${c.userName || '匿名'}] ${text}...`);
|
|
183
|
+
});
|
|
184
|
+
console.log('-'.repeat(60));
|
|
185
|
+
|
|
186
|
+
// 6. 指定回复中心意思
|
|
187
|
+
const replyIntent = await question('\n请输入回复的中心意思(如: 感谢认可,告诉对方具体信息): ');
|
|
188
|
+
|
|
189
|
+
if (!replyIntent.trim()) {
|
|
190
|
+
console.log('未输入回复意图');
|
|
191
|
+
rl.close();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 7. 选择模式
|
|
196
|
+
const mode = await question('\n选择模式: (1) dryrun预览 (2) 真实回复: ');
|
|
197
|
+
const dryRun = mode.trim() === '1' || mode.trim().toLowerCase() === 'dryrun';
|
|
198
|
+
|
|
199
|
+
// 8. 确认
|
|
200
|
+
console.log('\n' + '='.repeat(50));
|
|
201
|
+
console.log('配置确认:');
|
|
202
|
+
console.log(` 关键词目录: ${keyword}`);
|
|
203
|
+
console.log(` 匹配关键词: ${matchKeywords.join(', ')}`);
|
|
204
|
+
console.log(` 命中评论数: ${matchedComments.length}`);
|
|
205
|
+
console.log(` 回复意图: ${replyIntent}`);
|
|
206
|
+
console.log(` 模式: ${dryRun ? 'dryrun(预览)' : '真实回复'}`);
|
|
207
|
+
console.log('='.repeat(50));
|
|
208
|
+
|
|
209
|
+
const confirm = await question('\n确认执行?(y/n): ');
|
|
210
|
+
if (confirm.toLowerCase() !== 'y') {
|
|
211
|
+
console.log('已取消');
|
|
212
|
+
rl.close();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 9. 输出配置文件
|
|
217
|
+
const config = {
|
|
218
|
+
keyword: keyword.trim(),
|
|
219
|
+
matchKeywords,
|
|
220
|
+
replyIntent: replyIntent.trim(),
|
|
221
|
+
dryRun,
|
|
222
|
+
comments: matchedComments.slice(0, dryRun ? 3 : undefined),
|
|
223
|
+
timestamp: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const configPath = path.join(WEBAUTO_ROOT, 'smart-reply-config.json');
|
|
227
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
228
|
+
|
|
229
|
+
console.log(`\n配置已保存到: ${configPath}`);
|
|
230
|
+
console.log('\n后续步骤:');
|
|
231
|
+
console.log('1. 确保 Unified API 运行中 (http://127.0.0.1:7701)');
|
|
232
|
+
console.log('2. 确保浏览器 session 已打开小红书页面');
|
|
233
|
+
console.log('3. 运行 SmartReplyBlock 进行实际回复');
|
|
234
|
+
|
|
235
|
+
// 10. 测试 AI 生成(在关闭 rl 之前)
|
|
236
|
+
const testGenerate = await question('\n是否立即测试 AI 生成回复?(y/n): ');
|
|
237
|
+
if (testGenerate.toLowerCase() === 'y') {
|
|
238
|
+
console.log('\n正在生成测试回复...\n');
|
|
239
|
+
|
|
240
|
+
const testComment = matchedComments[0];
|
|
241
|
+
const prompt = `你是一个小红书评论回复助手。请根据以下信息生成一条回复。
|
|
242
|
+
|
|
243
|
+
## 帖子评论
|
|
244
|
+
${testComment.content || testComment.text || ''}
|
|
245
|
+
|
|
246
|
+
## 回复要求
|
|
247
|
+
- 回复的中心意思:${replyIntent}
|
|
248
|
+
- 回复风格:友好、自然、口语化
|
|
249
|
+
- 字数限制:100字以内
|
|
250
|
+
- 不要使用表情符号开头
|
|
251
|
+
- 可以适当使用 1-2 个表情符号
|
|
252
|
+
|
|
253
|
+
请直接输出回复内容,不要有任何解释或说明。`;
|
|
254
|
+
|
|
255
|
+
// 在关闭 rl 之前启动子进程
|
|
256
|
+
const child = spawn('iflow', ['-p', prompt], { stdio: 'inherit' });
|
|
257
|
+
|
|
258
|
+
// 关闭 readline
|
|
259
|
+
rl.close();
|
|
260
|
+
|
|
261
|
+
await new Promise(resolve => child.on('close', resolve));
|
|
262
|
+
} else {
|
|
263
|
+
rl.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
main().catch(console.error);
|
|
@@ -2,20 +2,71 @@
|
|
|
2
2
|
import minimist from 'minimist';
|
|
3
3
|
import { runWorkflowById } from '../../../dist/modules/workflow/src/runner.js';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { assertProfileUsable } from './lib/profile-policy.mjs';
|
|
6
|
+
import { syncWeiboAccountByProfile } from './lib/account-detect.mjs';
|
|
7
|
+
import { cleanupIncompleteProfiles } from './lib/account-store.mjs';
|
|
8
|
+
import { ensureSessionInitialized } from './lib/session-init.mjs';
|
|
5
9
|
|
|
6
10
|
const WEIBO_HOME_URL = 'https://www.weibo.com';
|
|
7
|
-
const DEFAULT_PROFILE = '
|
|
11
|
+
const DEFAULT_PROFILE = '';
|
|
12
|
+
|
|
13
|
+
function parseBoolean(value, fallback = false) {
|
|
14
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
15
|
+
if (typeof value === 'boolean') return value;
|
|
16
|
+
const text = String(value).trim().toLowerCase();
|
|
17
|
+
if (['1', 'true', 'yes', 'on'].includes(text)) return true;
|
|
18
|
+
if (['0', 'false', 'no', 'off'].includes(text)) return false;
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseIntFlag(value, fallback, min = 1) {
|
|
23
|
+
const num = Number(value);
|
|
24
|
+
if (!Number.isFinite(num)) return fallback;
|
|
25
|
+
return Math.max(min, Math.floor(num));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ensureWeiboLoginValid(profileId) {
|
|
29
|
+
const state = await syncWeiboAccountByProfile(profileId);
|
|
30
|
+
const valid = state?.valid === true && Boolean(String(state?.accountId || '').trim());
|
|
31
|
+
if (!valid) {
|
|
32
|
+
const reason = String(state?.reason || state?.status || 'invalid').trim() || 'invalid';
|
|
33
|
+
throw new Error(`weibo account invalid, login gate blocked: ${profileId} (${reason})`);
|
|
34
|
+
}
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveWeiboWorkflow(argv = {}) {
|
|
39
|
+
const explicitWorkflow = String(argv.workflow || '').trim();
|
|
40
|
+
if (explicitWorkflow) return explicitWorkflow;
|
|
41
|
+
const taskType = String(argv['task-type'] || argv.taskType || 'search').trim().toLowerCase();
|
|
42
|
+
if (!taskType || taskType === 'search') return 'weibo-search-v1';
|
|
43
|
+
throw new Error(`unsupported weibo task-type: ${taskType} (currently only search)`);
|
|
44
|
+
}
|
|
8
45
|
|
|
9
46
|
async function runCommand(argv) {
|
|
10
|
-
|
|
47
|
+
cleanupIncompleteProfiles();
|
|
48
|
+
const profile = assertProfileUsable(String(argv.profile || DEFAULT_PROFILE).trim());
|
|
11
49
|
if (!profile) {
|
|
12
50
|
throw new Error('Profile ID is required. Use --profile <id>');
|
|
13
51
|
}
|
|
52
|
+
const initResult = await ensureSessionInitialized(profile, {
|
|
53
|
+
url: WEIBO_HOME_URL,
|
|
54
|
+
rootDir: process.cwd(),
|
|
55
|
+
timeoutMs: 60000,
|
|
56
|
+
});
|
|
57
|
+
if (!initResult?.ok) {
|
|
58
|
+
throw new Error(`weibo session init failed: ${initResult?.error || 'unknown_error'}`);
|
|
59
|
+
}
|
|
60
|
+
await ensureWeiboLoginValid(profile);
|
|
14
61
|
|
|
15
|
-
const workflowId =
|
|
62
|
+
const workflowId = resolveWeiboWorkflow(argv);
|
|
16
63
|
const keyword = String(argv.keyword || '').trim();
|
|
17
64
|
const targetCount = Number(argv['max-notes'] || argv.target || 50);
|
|
18
65
|
const maxComments = Number(argv['max-comments'] || 0); // 0 means no limit
|
|
66
|
+
const maxPages = Number(argv['max-pages'] || 10);
|
|
67
|
+
const collectComments = parseBoolean(argv['collect-comments'], maxComments > 0);
|
|
68
|
+
const tabCount = parseIntFlag(argv['tab-count'], 2, 1);
|
|
69
|
+
const tabOpenDelayMs = parseIntFlag(argv['tab-open-delay'], 800, 0);
|
|
19
70
|
|
|
20
71
|
if (!keyword && workflowId === 'weibo-search-v1') {
|
|
21
72
|
throw new Error('Keyword is required for search tasks. Use --keyword <text>');
|
|
@@ -29,13 +80,13 @@ async function runCommand(argv) {
|
|
|
29
80
|
env,
|
|
30
81
|
targetCount,
|
|
31
82
|
maxComments,
|
|
83
|
+
maxPages,
|
|
84
|
+
collectComments,
|
|
85
|
+
tabCount,
|
|
86
|
+
tabOpenDelayMs,
|
|
32
87
|
// Add other common parameters as needed
|
|
33
88
|
};
|
|
34
89
|
|
|
35
|
-
if (!workflowId) {
|
|
36
|
-
throw new Error('Workflow ID is required. e.g., --workflow weibo-search-v1');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
90
|
console.log(`[Weibo Unified] Running workflow: ${workflowId} with profile: ${profile}`);
|
|
40
91
|
const result = await runWorkflowById(workflowId, initialContext);
|
|
41
92
|
|
|
@@ -53,7 +104,6 @@ async function main() {
|
|
|
53
104
|
boolean: ['help'],
|
|
54
105
|
alias: { k: 'keyword', t: 'target', h: 'help' },
|
|
55
106
|
default: {
|
|
56
|
-
profile: DEFAULT_PROFILE,
|
|
57
107
|
env: 'debug',
|
|
58
108
|
target: 50,
|
|
59
109
|
},
|
|
@@ -67,10 +117,15 @@ Commands:
|
|
|
67
117
|
search Perform a Weibo search task.
|
|
68
118
|
|
|
69
119
|
Options:
|
|
70
|
-
--profile <id> Camo profile ID to use (
|
|
120
|
+
--profile <id> Camo profile ID to use (required; must be pre-created)
|
|
121
|
+
--task-type <type> 当前仅支持 search
|
|
71
122
|
--keyword <text> Keyword to search for (required for search command)
|
|
72
123
|
--target <n> Target number of posts to collect (default: 50)
|
|
124
|
+
--max-pages <n> Maximum search result pages to scan for links (default: 10)
|
|
73
125
|
--max-comments <n> Maximum comments to collect per post (default: 0, no limit)
|
|
126
|
+
--collect-comments <bool> Whether to collect comments in content phase (default: auto by --max-comments)
|
|
127
|
+
--tab-count <n> Tab count for round-robin detail collection (default: 2)
|
|
128
|
+
--tab-open-delay <ms> Delay after opening each extra tab (default: 800)
|
|
74
129
|
--env <debug|prod> Environment for data storage (default: debug)
|
|
75
130
|
--help Show this help message
|
|
76
131
|
|
|
@@ -104,11 +159,25 @@ if (isDirectExec) {
|
|
|
104
159
|
}
|
|
105
160
|
|
|
106
161
|
export async function runWeiboUnified(argv) {
|
|
107
|
-
|
|
162
|
+
cleanupIncompleteProfiles();
|
|
163
|
+
const workflowId = resolveWeiboWorkflow(argv);
|
|
108
164
|
const keyword = String(argv.keyword || argv.k || '').trim();
|
|
109
|
-
const profile = String(argv.profile || DEFAULT_PROFILE).trim();
|
|
165
|
+
const profile = assertProfileUsable(String(argv.profile || DEFAULT_PROFILE).trim());
|
|
166
|
+
const initResult = await ensureSessionInitialized(profile, {
|
|
167
|
+
url: WEIBO_HOME_URL,
|
|
168
|
+
rootDir: process.cwd(),
|
|
169
|
+
timeoutMs: 60000,
|
|
170
|
+
});
|
|
171
|
+
if (!initResult?.ok) {
|
|
172
|
+
throw new Error(`weibo session init failed: ${initResult?.error || 'unknown_error'}`);
|
|
173
|
+
}
|
|
174
|
+
await ensureWeiboLoginValid(profile);
|
|
110
175
|
const targetCount = Number(argv['max-notes'] || argv.target || argv['max-notes'] || 50);
|
|
111
176
|
const maxComments = Number(argv['max-comments'] || 0);
|
|
177
|
+
const maxPages = Number(argv['max-pages'] || 10);
|
|
178
|
+
const collectComments = parseBoolean(argv['collect-comments'], maxComments > 0);
|
|
179
|
+
const tabCount = parseIntFlag(argv['tab-count'], 2, 1);
|
|
180
|
+
const tabOpenDelayMs = parseIntFlag(argv['tab-open-delay'], 800, 0);
|
|
112
181
|
const env = String(argv.env || 'debug').trim();
|
|
113
182
|
|
|
114
183
|
if (!keyword) {
|
|
@@ -121,6 +190,10 @@ export async function runWeiboUnified(argv) {
|
|
|
121
190
|
env,
|
|
122
191
|
targetCount,
|
|
123
192
|
maxComments,
|
|
193
|
+
maxPages,
|
|
194
|
+
collectComments,
|
|
195
|
+
tabCount,
|
|
196
|
+
tabOpenDelayMs,
|
|
124
197
|
};
|
|
125
198
|
|
|
126
199
|
const result = await runWorkflowById(workflowId, initialContext);
|
|
@@ -4,7 +4,7 @@ import { runUnified } from './xhs-unified.mjs';
|
|
|
4
4
|
|
|
5
5
|
async function main() {
|
|
6
6
|
const argv = minimist(process.argv.slice(2));
|
|
7
|
-
const mode = String(argv.mode || 'phase1-phase2-unified').trim();
|
|
7
|
+
const mode = String(argv.mode || 'phase1-phase2-unified').trim().toLowerCase();
|
|
8
8
|
|
|
9
9
|
if (mode === 'phase1-only') {
|
|
10
10
|
console.log(JSON.stringify({ event: 'xhs.orchestrate.skip', mode, reason: 'phase1 is merged into runtime bootstrap' }));
|
|
@@ -13,6 +13,7 @@ async function main() {
|
|
|
13
13
|
|
|
14
14
|
if (mode === 'phase1-phase2') {
|
|
15
15
|
await runUnified(argv, {
|
|
16
|
+
stage: 'content',
|
|
16
17
|
doComments: false,
|
|
17
18
|
doLikes: false,
|
|
18
19
|
doReply: false,
|
|
@@ -22,6 +23,47 @@ async function main() {
|
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
if (mode === 'links-only') {
|
|
27
|
+
await runUnified(argv, {
|
|
28
|
+
stage: 'links',
|
|
29
|
+
doHomepage: false,
|
|
30
|
+
doImages: false,
|
|
31
|
+
doComments: false,
|
|
32
|
+
doLikes: false,
|
|
33
|
+
doReply: false,
|
|
34
|
+
doOcr: false,
|
|
35
|
+
persistComments: false,
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (mode === 'content-only') {
|
|
41
|
+
await runUnified(argv, {
|
|
42
|
+
stage: 'content',
|
|
43
|
+
doLikes: false,
|
|
44
|
+
doReply: false,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (mode === 'like-only') {
|
|
50
|
+
await runUnified(argv, {
|
|
51
|
+
stage: 'like',
|
|
52
|
+
doLikes: true,
|
|
53
|
+
doReply: false,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (mode === 'reply-only') {
|
|
59
|
+
await runUnified(argv, {
|
|
60
|
+
stage: 'reply',
|
|
61
|
+
doLikes: false,
|
|
62
|
+
doReply: true,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
25
67
|
if (mode === 'phase1-phase2-unified' || mode === 'unified-only') {
|
|
26
68
|
await runUnified(argv);
|
|
27
69
|
return;
|