tt-help-cli-ycl 1.2.0 → 1.3.1

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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/auto-core.mjs +174 -0
  3. package/src/cli/auto.js +94 -0
  4. package/src/cli/explore.js +117 -0
  5. package/src/cli/progress.js +111 -0
  6. package/src/cli/scrape.js +47 -0
  7. package/src/cli/utils.js +18 -0
  8. package/src/cli/videos.js +41 -0
  9. package/src/cli/watch.js +28 -0
  10. package/src/data-store.mjs +213 -0
  11. package/src/{explore-core.cjs → explore-core.mjs} +148 -157
  12. package/src/{get-user-videos-core.cjs → get-user-videos-core.mjs} +6 -23
  13. package/src/lib/args.js +19 -38
  14. package/src/lib/auto-browser.mjs +5 -12
  15. package/src/lib/browser/anti-detect.js +23 -0
  16. package/src/lib/browser/cdp.js +142 -0
  17. package/src/lib/browser/launch.js +43 -0
  18. package/src/lib/browser/page.js +62 -0
  19. package/src/lib/constants.js +13 -95
  20. package/src/lib/delay.js +54 -0
  21. package/src/lib/explore.js +16 -123
  22. package/src/lib/fetcher.js +3 -18
  23. package/src/lib/get-user-videos-browser.mjs +1 -6
  24. package/src/lib/io.js +8 -30
  25. package/src/lib/parser.js +1 -1
  26. package/src/lib/retry.js +44 -0
  27. package/src/lib/scrape-browser.mjs +1 -6
  28. package/src/lib/scrape.js +5 -4
  29. package/src/lib/url.js +52 -0
  30. package/src/main.mjs +59 -822
  31. package/src/scraper/{core.cjs → core.mjs} +25 -57
  32. package/src/scraper/modules/{comment-extractor.cjs → comment-extractor.mjs} +23 -15
  33. package/src/scraper/modules/follow-extractor.mjs +121 -0
  34. package/src/scraper/modules/{guess-extractor.cjs → guess-extractor.mjs} +3 -5
  35. package/src/scraper/modules/page-error-detector.mjs +68 -0
  36. package/src/scraper/modules/page-helpers.mjs +44 -0
  37. package/src/scraper/modules/scroll-collector.mjs +189 -0
  38. package/src/watch/public/index.html +139 -64
  39. package/src/watch/server.mjs +234 -153
  40. package/src/auto-core.cjs +0 -367
  41. package/src/data-store.cjs +0 -69
  42. package/src/get-user-videos.cjs +0 -59
  43. package/src/scraper/index.cjs +0 -97
  44. package/src/scraper/modules/follow-extractor.cjs +0 -112
  45. package/src/scraper/modules/page-helpers.cjs +0 -422
  46. package/src/scraper/modules/scroll-collector.cjs +0 -173
  47. package/src/scraper/modules/video-scanner.cjs +0 -43
@@ -1,153 +1,234 @@
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
+ if (req.method === 'GET' && routePath === '/api/stats') {
155
+ const stats = computeStats(store.getAllUsers());
156
+ sendJSON(res, 200, stats);
157
+ return;
158
+ }
159
+
160
+ if (req.method === 'GET' && routePath === '/api/target-users') {
161
+ const all = store.getAllUsers();
162
+ const targets = all.filter(u =>
163
+ u.ttSeller && u.verified === false && u.locationCreated === 'ES'
164
+ ).map(u => ({
165
+ uniqueId: u.uniqueId,
166
+ nickname: u.nickname || '',
167
+ followerCount: u.followerCount || 0,
168
+ }));
169
+ sendJSON(res, 200, { total: targets.length, users: targets });
170
+ return;
171
+ }
172
+
173
+ if (req.method === 'GET' && routePath === '/api/users') {
174
+ const all = store.getAllUsers();
175
+ let filtered = [...all];
176
+
177
+ if (params.status && params.status !== 'all') {
178
+ filtered = filtered.filter(u => u.status === params.status);
179
+ }
180
+ if (params.search) {
181
+ const s = params.search.toLowerCase();
182
+ filtered = filtered.filter(u =>
183
+ u.uniqueId.toLowerCase().includes(s) ||
184
+ (u.nickname || '').toLowerCase().includes(s)
185
+ );
186
+ }
187
+
188
+ const statusOrder = { processing: 0, pending: 1, done: 2, error: 3, restricted: 4 };
189
+ filtered.sort((a, b) => {
190
+ const sa = statusOrder[a.status] ?? 9;
191
+ const sb = statusOrder[b.status] ?? 9;
192
+ if (sa !== sb) return sa - sb;
193
+ if (a.status === 'done' && b.status === 'done') return (b.processedAt || 0) - (a.processedAt || 0);
194
+ return (b.followerCount || 0) - (a.followerCount || 0);
195
+ });
196
+
197
+ const limit = parseInt(params.limit) || 50;
198
+ const offset = parseInt(params.offset) || 0;
199
+ const paged = filtered.slice(offset, offset + limit);
200
+
201
+ sendJSON(res, 200, { total: filtered.length, users: paged });
202
+ return;
203
+ }
204
+
205
+ if ((req.method === 'GET' && (routePath === '/' || routePath === '/index.html'))) {
206
+ const html = readFileSync(join(publicDir, 'index.html'), 'utf-8');
207
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
208
+ res.end(html);
209
+ return;
210
+ }
211
+
212
+ res.writeHead(404);
213
+ res.end('Not Found');
214
+ });
215
+
216
+ server.on('error', (err) => {
217
+ if (err.code === 'EADDRINUSE') {
218
+ console.error(`\u7aef\u53e3 ${port} \u5df2\u88ab\u5360\u7528\uff0c\u8bf7\u66f4\u6362\u7aef\u53e3\u540e\u91cd\u8bd5`);
219
+ reject(err);
220
+ } else {
221
+ reject(err);
222
+ }
223
+ });
224
+
225
+ server.listen(port, '127.0.0.1', () => {
226
+ console.error(`Watch \u76d1\u63a7\u670d\u52a1\u5df2\u542f\u52a8: http://127.0.0.1:${port}`);
227
+ _resolve({ server, port });
228
+ });
229
+ });
230
+ }
231
+
232
+ export function openBrowser(port) {
233
+ spawn('open', [`http://127.0.0.1:${port}`]).on('error', () => {});
234
+ }