tt-help-cli-ycl 1.3.0 → 1.3.2

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.
Files changed (58) hide show
  1. package/README.md +17 -17
  2. package/cli.js +9 -9
  3. package/package.json +44 -44
  4. package/src/cli/auto.js +94 -0
  5. package/src/cli/explore.js +117 -0
  6. package/src/cli/progress.js +111 -0
  7. package/src/cli/scrape.js +47 -0
  8. package/src/cli/utils.js +18 -0
  9. package/src/cli/videos.js +41 -0
  10. package/src/cli/watch.js +28 -0
  11. package/src/lib/args.js +386 -397
  12. package/src/lib/browser/anti-detect.js +23 -0
  13. package/src/lib/browser/cdp.js +142 -0
  14. package/src/lib/browser/launch.js +43 -0
  15. package/src/lib/browser/page.js +80 -0
  16. package/src/lib/constants.js +85 -168
  17. package/src/lib/delay.js +54 -0
  18. package/src/lib/explore-fetch.js +118 -0
  19. package/src/lib/fetcher.js +45 -60
  20. package/src/lib/filter.js +66 -66
  21. package/src/lib/io.js +54 -76
  22. package/src/lib/output.js +80 -80
  23. package/src/lib/parser.js +47 -47
  24. package/src/lib/retry.js +44 -0
  25. package/src/lib/scrape.js +40 -39
  26. package/src/lib/url.js +52 -0
  27. package/src/main.mjs +199 -962
  28. package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
  29. package/src/scraper/auto-core.mjs +183 -0
  30. package/src/scraper/{core.cjs → core.mjs} +188 -214
  31. package/src/{explore-core.cjs → scraper/explore-core.mjs} +44 -42
  32. package/src/scraper/modules/captcha-handler.mjs +114 -0
  33. package/src/scraper/modules/comment-extractor.mjs +69 -0
  34. package/src/scraper/modules/follow-extractor.mjs +121 -0
  35. package/src/scraper/modules/{guess-extractor.cjs → guess-extractor.mjs} +51 -53
  36. package/src/scraper/modules/page-error-detector.mjs +70 -0
  37. package/src/scraper/modules/page-helpers.mjs +46 -0
  38. package/src/scraper/modules/scroll-collector.mjs +189 -0
  39. package/src/{get-user-videos-core.cjs → videos/core.mjs} +126 -143
  40. package/src/watch/data-store.mjs +239 -0
  41. package/src/watch/public/index.html +446 -271
  42. package/src/watch/server.mjs +257 -153
  43. package/src/auto-core.cjs +0 -367
  44. package/src/data-store.cjs +0 -69
  45. package/src/get-user-videos.cjs +0 -59
  46. package/src/lib/auto-browser.mjs +0 -13
  47. package/src/lib/explore.js +0 -225
  48. package/src/lib/get-user-videos-browser.mjs +0 -6
  49. package/src/lib/scrape-browser.mjs +0 -6
  50. package/src/scraper/index.cjs +0 -97
  51. package/src/scraper/modules/comment-extractor.cjs +0 -49
  52. package/src/scraper/modules/follow-extractor.cjs +0 -112
  53. package/src/scraper/modules/page-helpers.cjs +0 -422
  54. package/src/scraper/modules/scroll-collector.cjs +0 -173
  55. package/src/scraper/modules/video-scanner.cjs +0 -43
  56. package/src/test-auto-follow.cjs +0 -109
  57. package/src/test-extractors.cjs +0 -75
  58. package/src/test-follow.cjs +0 -41
@@ -1,153 +1,257 @@
1
- import http from 'http';
2
- import { readFileSync, existsSync, writeFileSync } from 'fs';
3
- import { join, dirname, resolve } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { spawn } from 'child_process';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = dirname(__filename);
9
- const publicDir = join(__dirname, 'public');
10
-
11
- function analyzeData(users) {
12
- if (!Array.isArray(users)) users = [];
13
-
14
- const totalUsers = users.length;
15
- const processedUsers = users.filter(u => u.processed).length;
16
- const pendingUsers = users.filter(u => !u.processed && !u.error && !u.restricted).length;
17
- const restrictedUsers = users.filter(u => u.restricted).length;
18
- const errorUsers = users.filter(u => u.error && !u.processed).length;
19
- const noVideoUsers = users.filter(u => u.noVideo).length;
20
- const keepFollowUsers = users.filter(u => u.keepFollow).length;
21
-
22
- const countryMap = {};
23
- for (const u of users) {
24
- if (!u.processed) continue;
25
- const loc = u.locationCreated || '未知';
26
- countryMap[loc] = (countryMap[loc] || 0) + 1;
27
- }
28
- const countryStats = Object.entries(countryMap)
29
- .map(([country, count]) => ({ country, count }))
30
- .sort((a, b) => b.count - a.count);
31
-
32
- const sourceCounts = { seed: 0, video: 0, comment: 0, guess: 0, following: 0, follower: 0, processed: 0, restricted: 0, error: 0, noVideo: 0 };
33
- for (const u of users) {
34
- if (u.restricted) {
35
- sourceCounts.restricted++;
36
- continue;
37
- }
38
- if (u.error && !u.processed) {
39
- sourceCounts.error++;
40
- continue;
41
- }
42
- if (u.noVideo) sourceCounts.noVideo++;
43
- const sources = u.sources || [];
44
- if (u.processed) sourceCounts.processed++;
45
- if (sources.includes('video') && !u.processed) sourceCounts.video++;
46
- if (sources.includes('comment') && !u.processed) sourceCounts.comment++;
47
- if (sources.includes('guess') && !u.processed) sourceCounts.guess++;
48
- if (sources.includes('following') && !u.processed) sourceCounts.following++;
49
- if (sources.includes('follower') && !u.processed) sourceCounts.follower++;
50
- if (!sources.includes('video') && !sources.includes('comment') && !sources.includes('guess') && !sources.includes('following') && !sources.includes('follower') && !u.processed) sourceCounts.seed++;
51
- }
52
-
53
- return {
54
- totalUsers,
55
- processedUsers,
56
- pendingUsers,
57
- restrictedUsers,
58
- errorUsers,
59
- noVideoUsers,
60
- keepFollowUsers,
61
- countryStats,
62
- sourceStats: sourceCounts,
63
- users,
64
- };
65
- }
66
-
67
- function readDataFile(filePath) {
68
- const resolved = resolve(filePath);
69
- if (!existsSync(resolved)) {
70
- return [];
71
- }
72
- try {
73
- const raw = readFileSync(resolved, 'utf-8');
74
- const data = JSON.parse(raw);
75
- return Array.isArray(data) ? data : [];
76
- } catch (e) {
77
- return [];
78
- }
79
- }
80
-
81
- export function startWatchServer(outputFile, port = 3000) {
82
- return new Promise((resolve, reject) => {
83
- const server = http.createServer((req, res) => {
84
- if (req.url === '/' || req.url === '/index.html') {
85
- const html = readFileSync(join(publicDir, 'index.html'), 'utf-8');
86
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
87
- res.end(html);
88
- } else if (req.url === '/api/data') {
89
- const users = readDataFile(outputFile);
90
- const data = analyzeData(users);
91
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
92
- res.end(JSON.stringify(data));
93
- } else if (req.url === '/api/users' && req.method === 'POST') {
94
- let body = '';
95
- req.on('data', chunk => body += chunk);
96
- req.on('end', () => {
97
- try {
98
- const { usernames } = JSON.parse(body);
99
- if (!Array.isArray(usernames) || usernames.length === 0) {
100
- res.writeHead(400, { 'Content-Type': 'application/json' });
101
- res.end(JSON.stringify({ error: 'usernames 数组不能为空' }));
102
- return;
103
- }
104
- const resolved = resolve(outputFile);
105
- const existing = existsSync(resolved) ? readDataFile(outputFile) : [];
106
- const existingIds = new Set(existing.map(u => u.uniqueId));
107
- const newUsers = usernames
108
- .map(u => u.replace(/^@/, '').trim())
109
- .filter(u => u && !existingIds.has(u))
110
- .map(u => ({ uniqueId: u, sources: ['seed'] }));
111
- const updated = [...newUsers, ...existing];
112
- writeFileSync(resolved, JSON.stringify(updated, null, 2));
113
- res.writeHead(200, { 'Content-Type': 'application/json' });
114
- res.end(JSON.stringify({ added: newUsers.length, skipped: usernames.length - newUsers.length }));
115
- } catch (e) {
116
- res.writeHead(400, { 'Content-Type': 'application/json' });
117
- res.end(JSON.stringify({ error: e.message }));
118
- }
119
- });
120
- } else {
121
- res.writeHead(404);
122
- res.end('Not Found');
123
- }
124
- });
125
-
126
- server.on('error', (err) => {
127
- if (err.code === 'EADDRINUSE') {
128
- console.error(`端口 ${port} 已被占用,请更换端口后重试`);
129
- reject(err);
130
- } else {
131
- reject(err);
132
- }
133
- return;
134
- });
135
-
136
- server.listen(port, '127.0.0.1', () => {
137
- console.error(`Watch 监控服务已启动: http://127.0.0.1:${port}`);
138
- resolve({ server, port });
139
- });
140
- });
141
- }
142
-
143
- export function startWatchServerStandalone(outputFile, port = 3000) {
144
- return new Promise((resolve, reject) => {
145
- const openProcess = spawn('open', [`http://127.0.0.1:${port}`]);
146
- openProcess.on('error', () => {});
147
- startWatchServer(outputFile, port).then(resolve).catch(reject);
148
- });
149
- }
150
-
151
- export function openBrowser(port) {
152
- spawn('open', [`http://127.0.0.1:${port}`]).on('error', () => {});
153
- }
1
+ import http from 'http';
2
+ import { readFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { spawn } from 'child_process';
6
+ import { createStore } from './data-store.mjs';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const publicDir = join(__dirname, 'public');
11
+
12
+ function parseQuery(url) {
13
+ const idx = url.indexOf('?');
14
+ if (idx === -1) return { path: url, params: {} };
15
+ const params = {};
16
+ for (const kv of url.slice(idx + 1).split('&')) {
17
+ const [k, v] = kv.split('=');
18
+ params[decodeURIComponent(k)] = decodeURIComponent(v || '');
19
+ }
20
+ return { path: url.slice(0, idx), params };
21
+ }
22
+
23
+ function computeStats(users) {
24
+ const total = users.length;
25
+ const statusCounts = { pending: 0, processing: 0, done: 0, error: 0, restricted: 0 };
26
+ for (const u of users) {
27
+ statusCounts[u.status] = (statusCounts[u.status] || 0) + 1;
28
+ }
29
+
30
+ const targetUsers = users.filter(u =>
31
+ u.ttSeller && u.verified === false && u.locationCreated === 'ES'
32
+ ).length;
33
+
34
+ const countryMap = {};
35
+ for (const u of users) {
36
+ if (u.status !== 'done') continue;
37
+ const loc = u.locationCreated || '\u672a\u77e5';
38
+ countryMap[loc] = (countryMap[loc] || 0) + 1;
39
+ }
40
+ const countryStats = Object.entries(countryMap)
41
+ .map(([country, count]) => ({ country, count }))
42
+ .sort((a, b) => b.count - a.count);
43
+
44
+ const sourceCounts = { seed: 0, video: 0, comment: 0, guess: 0, following: 0, follower: 0, processed: 0, restricted: 0, error: 0, noVideo: 0 };
45
+ for (const u of users) {
46
+ if (u.status === 'restricted') { sourceCounts.restricted++; continue; }
47
+ if (u.status === 'error') { sourceCounts.error++; continue; }
48
+ if (u.noVideo) sourceCounts.noVideo++;
49
+ const sources = u.sources || [];
50
+ if (u.status === 'done') sourceCounts.processed++;
51
+ if (sources.includes('video') && u.status !== 'done') sourceCounts.video++;
52
+ if (sources.includes('comment') && u.status !== 'done') sourceCounts.comment++;
53
+ if (sources.includes('guess') && u.status !== 'done') sourceCounts.guess++;
54
+ if (sources.includes('following') && u.status !== 'done') sourceCounts.following++;
55
+ if (sources.includes('follower') && u.status !== 'done') sourceCounts.follower++;
56
+ if (!sources.includes('video') && !sources.includes('comment') && !sources.includes('guess') &&
57
+ !sources.includes('following') && !sources.includes('follower') && u.status !== 'done') sourceCounts.seed++;
58
+ }
59
+
60
+ return {
61
+ totalUsers: total,
62
+ processedUsers: statusCounts.done,
63
+ pendingUsers: statusCounts.pending,
64
+ processingUsers: statusCounts.processing,
65
+ restrictedUsers: statusCounts.restricted,
66
+ errorUsers: statusCounts.error,
67
+ targetUsers,
68
+ countryStats,
69
+ sourceStats: sourceCounts,
70
+ };
71
+ }
72
+
73
+ function readBody(req) {
74
+ return new Promise((resolve, reject) => {
75
+ let body = '';
76
+ req.on('data', chunk => body += chunk);
77
+ req.on('end', () => {
78
+ try {
79
+ resolve(body ? JSON.parse(body) : {});
80
+ } catch (e) {
81
+ reject(e);
82
+ }
83
+ });
84
+ req.on('error', reject);
85
+ });
86
+ }
87
+
88
+ function sendJSON(res, code, data) {
89
+ res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
90
+ res.end(JSON.stringify(data));
91
+ }
92
+
93
+ export function startWatchServer(outputFile, port = 3000) {
94
+ return new Promise((_resolve, reject) => {
95
+ const store = createStore(outputFile);
96
+
97
+ const server = http.createServer(async (req, res) => {
98
+ const { path: routePath, params } = parseQuery(req.url);
99
+
100
+ if (req.method === 'POST' && routePath === '/api/users') {
101
+ try {
102
+ const { usernames } = await readBody(req);
103
+ if (!Array.isArray(usernames) || usernames.length === 0) {
104
+ sendJSON(res, 400, { error: 'usernames \u6570\u7ec4\u4e0d\u80fd\u4e3a\u7a7a' });
105
+ return;
106
+ }
107
+ const existingIds = new Set(store.getAllUsers().map(u => u.uniqueId));
108
+ const newUsers = usernames
109
+ .map(u => u.replace(/^@/, '').trim())
110
+ .filter(u => u && !existingIds.has(u));
111
+ for (const nu of newUsers) {
112
+ store.addUser({ uniqueId: nu, sources: ['seed'], status: 'pending' });
113
+ }
114
+ store.save();
115
+ sendJSON(res, 200, {
116
+ added: newUsers.length,
117
+ skipped: usernames.length - newUsers.length,
118
+ message: `\u5df2\u63d2\u5165 ${newUsers.length} \u4e2a\u7528\u6237`
119
+ });
120
+ } catch (e) {
121
+ sendJSON(res, 400, { error: e.message });
122
+ }
123
+ return;
124
+ }
125
+
126
+ if (req.method === 'GET' && routePath === '/api/job') {
127
+ const job = store.claimNextJob();
128
+ if (job) {
129
+ store.save();
130
+ sendJSON(res, 200, { hasJob: true, user: job });
131
+ } else {
132
+ sendJSON(res, 200, { hasJob: false });
133
+ }
134
+ return;
135
+ }
136
+
137
+ const jobCommitMatch = routePath.match(/^\/api\/job\/([^/]+)$/);
138
+ if (req.method === 'POST' && jobCommitMatch) {
139
+ const uniqueId = jobCommitMatch[1];
140
+ try {
141
+ const result = await readBody(req);
142
+ const ret = store.commitJob(uniqueId, result);
143
+ if (ret.saved) {
144
+ sendJSON(res, 200, ret);
145
+ } else {
146
+ sendJSON(res, 404, ret);
147
+ }
148
+ } catch (e) {
149
+ sendJSON(res, 400, { error: e.message });
150
+ }
151
+ return;
152
+ }
153
+
154
+ const jobResetMatch = routePath.match(/^\/api\/job\/([^/]+)\/reset$/);
155
+ if (req.method === 'POST' && jobResetMatch) {
156
+ const uniqueId = jobResetMatch[1];
157
+ const ret = store.resetJob(uniqueId);
158
+ if (ret.saved) {
159
+ sendJSON(res, 200, ret);
160
+ } else {
161
+ sendJSON(res, 404, ret);
162
+ }
163
+ return;
164
+ }
165
+
166
+ const jobPinMatch = routePath.match(/^\/api\/job\/([^/]+)\/pin$/);
167
+ if (req.method === 'POST' && jobPinMatch) {
168
+ const uniqueId = jobPinMatch[1];
169
+ const ret = store.togglePin(uniqueId);
170
+ sendJSON(res, ret.saved ? 200 : 404, ret);
171
+ return;
172
+ }
173
+
174
+ if (req.method === 'GET' && routePath === '/api/stats') {
175
+ const stats = computeStats(store.getAllUsers());
176
+ sendJSON(res, 200, stats);
177
+ return;
178
+ }
179
+
180
+ if (req.method === 'GET' && routePath === '/api/target-users') {
181
+ const all = store.getAllUsers();
182
+ const targets = all.filter(u =>
183
+ u.ttSeller && u.verified === false && u.locationCreated === 'ES'
184
+ ).map(u => ({
185
+ uniqueId: u.uniqueId,
186
+ nickname: u.nickname || '',
187
+ followerCount: u.followerCount || 0,
188
+ }));
189
+ sendJSON(res, 200, { total: targets.length, users: targets });
190
+ return;
191
+ }
192
+
193
+ if (req.method === 'GET' && routePath === '/api/users') {
194
+ const all = store.getAllUsers();
195
+ let filtered = [...all];
196
+
197
+ if (params.status && params.status !== 'all') {
198
+ filtered = filtered.filter(u => u.status === params.status);
199
+ }
200
+ if (params.search) {
201
+ const s = params.search.toLowerCase();
202
+ filtered = filtered.filter(u =>
203
+ u.uniqueId.toLowerCase().includes(s) ||
204
+ (u.nickname || '').toLowerCase().includes(s)
205
+ );
206
+ }
207
+
208
+ const statusOrder = { processing: 0, pending: 1, done: 2, error: 3, restricted: 4 };
209
+ filtered.sort((a, b) => {
210
+ // 置顶优先
211
+ if (a.pinned && !b.pinned) return -1;
212
+ if (!a.pinned && b.pinned) return 1;
213
+ const sa = statusOrder[a.status] ?? 9;
214
+ const sb = statusOrder[b.status] ?? 9;
215
+ if (sa !== sb) return sa - sb;
216
+ if (a.status === 'done' && b.status === 'done') return (b.processedAt || 0) - (a.processedAt || 0);
217
+ return (b.followerCount || 0) - (a.followerCount || 0);
218
+ });
219
+
220
+ const limit = parseInt(params.limit) || 50;
221
+ const offset = parseInt(params.offset) || 0;
222
+ const paged = filtered.slice(offset, offset + limit);
223
+
224
+ sendJSON(res, 200, { total: filtered.length, users: paged });
225
+ return;
226
+ }
227
+
228
+ if ((req.method === 'GET' && (routePath === '/' || routePath === '/index.html'))) {
229
+ const html = readFileSync(join(publicDir, 'index.html'), 'utf-8');
230
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
231
+ res.end(html);
232
+ return;
233
+ }
234
+
235
+ res.writeHead(404);
236
+ res.end('Not Found');
237
+ });
238
+
239
+ server.on('error', (err) => {
240
+ if (err.code === 'EADDRINUSE') {
241
+ console.error(`\u7aef\u53e3 ${port} \u5df2\u88ab\u5360\u7528\uff0c\u8bf7\u66f4\u6362\u7aef\u53e3\u540e\u91cd\u8bd5`);
242
+ reject(err);
243
+ } else {
244
+ reject(err);
245
+ }
246
+ });
247
+
248
+ server.listen(port, '127.0.0.1', () => {
249
+ console.error(`Watch \u76d1\u63a7\u670d\u52a1\u5df2\u542f\u52a8: http://127.0.0.1:${port}`);
250
+ _resolve({ server, port });
251
+ });
252
+ });
253
+ }
254
+
255
+ export function openBrowser(port) {
256
+ spawn('open', [`http://127.0.0.1:${port}`]).on('error', () => {});
257
+ }