tt-help-cli-ycl 1.4.30 → 1.4.32
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/package.json +1 -1
- package/src/cli/attach.js +16 -2
- package/src/cli/comments.js +6 -8
- package/src/cli/explore.js +8 -0
- package/src/cli/recheck.js +16 -2
- package/src/cli/refresh.js +8 -0
- package/src/cli/search.js +57 -24
- package/src/cli/tag.js +66 -43
- package/src/lib/api-interceptor.js +61 -18
- package/src/lib/args.js +4 -1
- package/src/lib/constants.js +4 -2
- package/src/lib/search-collector.js +30 -14
- package/src/lib/shop-scraper.mjs +280 -0
- package/src/scraper/explore-core.js +29 -0
- package/src/watch/data-store.js +40 -0
- package/src/watch/db-columns.js +7 -0
- package/src/watch/db-schema.js +14 -0
- package/src/watch/public/app.js +28 -3
- package/src/watch/public/index.html +4 -0
- package/src/watch/public/style.css +21 -0
- package/src/watch/server.js +7 -0
package/package.json
CHANGED
package/src/cli/attach.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
|
|
2
2
|
import { CDNBlockedError } from "../lib/parse-ssr.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
proxy as configuredProxy,
|
|
5
|
+
userId as configuredUserId,
|
|
6
|
+
saveUserId,
|
|
7
|
+
} from "../lib/constants.js";
|
|
8
|
+
import { getMacOrUuid } from "../lib/mac-or-uuid.js";
|
|
4
9
|
import { createApiClient } from "../lib/api-client.js";
|
|
5
10
|
import v8 from "node:v8";
|
|
6
11
|
|
|
@@ -110,6 +115,15 @@ export async function handleAttach(options) {
|
|
|
110
115
|
`[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}${countryStr}`,
|
|
111
116
|
);
|
|
112
117
|
|
|
118
|
+
// ── 设备标识 ──
|
|
119
|
+
let userId = configuredUserId;
|
|
120
|
+
if (!userId) {
|
|
121
|
+
userId = await getMacOrUuid();
|
|
122
|
+
saveUserId(userId);
|
|
123
|
+
attachLog(`[Attach] 未检测到本地用户编号,已生成并使用: ${userId}`);
|
|
124
|
+
}
|
|
125
|
+
attachLog(`[Attach] 用户编号: ${userId}`);
|
|
126
|
+
|
|
113
127
|
const { apiGet, apiPost } = createApiClient({ log: false });
|
|
114
128
|
|
|
115
129
|
const scraper = new TikTokScraper({
|
|
@@ -174,7 +188,7 @@ export async function handleAttach(options) {
|
|
|
174
188
|
? `&countries=${encodeURIComponent(attachCountries.join(","))}`
|
|
175
189
|
: "";
|
|
176
190
|
const { total, tasks } = await apiGet(
|
|
177
|
-
`${serverUrl}/api/user-update-tasks?limit=${attachParallel}${countryParam}`,
|
|
191
|
+
`${serverUrl}/api/user-update-tasks?limit=${attachParallel}&userId=${encodeURIComponent(userId)}${countryParam}`,
|
|
178
192
|
);
|
|
179
193
|
|
|
180
194
|
if (!tasks || tasks.length === 0) {
|
package/src/cli/comments.js
CHANGED
|
@@ -683,8 +683,8 @@ async function runAutoMode(options) {
|
|
|
683
683
|
let tasks;
|
|
684
684
|
try {
|
|
685
685
|
const apiUrl = jobLocations
|
|
686
|
-
? `${serverUrl}/api/comment-tasks?limit=${actualParallel}&locations=${encodeURIComponent(jobLocations)}`
|
|
687
|
-
: `${serverUrl}/api/comment-tasks?limit=${actualParallel}`;
|
|
686
|
+
? `${serverUrl}/api/comment-tasks?limit=${actualParallel}&locations=${encodeURIComponent(jobLocations)}&userId=${encodeURIComponent(userId)}`
|
|
687
|
+
: `${serverUrl}/api/comment-tasks?limit=${actualParallel}&userId=${encodeURIComponent(userId)}`;
|
|
688
688
|
const resp = await apiGet(apiUrl);
|
|
689
689
|
tasks = resp.tasks || [];
|
|
690
690
|
} catch (err) {
|
|
@@ -736,15 +736,13 @@ async function runAutoMode(options) {
|
|
|
736
736
|
try {
|
|
737
737
|
const modals = await detectModals(session.page);
|
|
738
738
|
const loginModal = modals.find(
|
|
739
|
-
(m) =>
|
|
740
|
-
m.type === "login-modal" || m.type === "modal-container",
|
|
739
|
+
(m) => m.type === "login-modal" || m.type === "modal-container",
|
|
741
740
|
);
|
|
742
741
|
if (loginModal) {
|
|
743
742
|
console.error(` 🔔 检测到登录弹窗,正在关闭...`);
|
|
744
|
-
const { closed, failed } = await closeAllModals(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
);
|
|
743
|
+
const { closed, failed } = await closeAllModals(session.page, {
|
|
744
|
+
waitMs: 300,
|
|
745
|
+
});
|
|
748
746
|
console.error(
|
|
749
747
|
` 🔔 弹窗关闭: closed=${JSON.stringify(closed)} failed=${JSON.stringify(failed)}`,
|
|
750
748
|
);
|
package/src/cli/explore.js
CHANGED
|
@@ -168,6 +168,9 @@ export async function handleExplore(options) {
|
|
|
168
168
|
|
|
169
169
|
const page = await getOrCreatePage(browser);
|
|
170
170
|
|
|
171
|
+
// 创建专用商品页(后台复用,避免重复打开/关闭闪屏)
|
|
172
|
+
const shopPage = await browser.contexts()[0].newPage();
|
|
173
|
+
|
|
171
174
|
// 先导航到 TikTok 页面,再检测登录状态
|
|
172
175
|
await withRetry("启动页面导航", async () => {
|
|
173
176
|
await page.goto(STARTUP_TIKTOK_URL, {
|
|
@@ -397,6 +400,7 @@ export async function handleExplore(options) {
|
|
|
397
400
|
location: exploreLocation,
|
|
398
401
|
proxyServer: cdpOptions.proxyServer || null,
|
|
399
402
|
browser,
|
|
403
|
+
shopPage,
|
|
400
404
|
},
|
|
401
405
|
console.error,
|
|
402
406
|
);
|
|
@@ -435,6 +439,7 @@ export async function handleExplore(options) {
|
|
|
435
439
|
location: exploreLocation,
|
|
436
440
|
proxyServer: cdpOptions.proxyServer || null,
|
|
437
441
|
browser,
|
|
442
|
+
shopPage,
|
|
438
443
|
},
|
|
439
444
|
console.error,
|
|
440
445
|
);
|
|
@@ -521,6 +526,9 @@ export async function handleExplore(options) {
|
|
|
521
526
|
|
|
522
527
|
const payload = {
|
|
523
528
|
userInfo: result.userInfo || {},
|
|
529
|
+
recentEcVideos: result.recentEcVideos || null,
|
|
530
|
+
topRecentVideo: result.topRecentVideo || null,
|
|
531
|
+
productDetail: result.productDetail || null,
|
|
524
532
|
discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
|
|
525
533
|
handle: Array.isArray(f) ? f[0] : f,
|
|
526
534
|
displayName: Array.isArray(f) ? f[1] : null,
|
package/src/cli/recheck.js
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
|
|
10
10
|
import { CDNBlockedError } from "../lib/parse-ssr.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
proxy as configuredProxy,
|
|
13
|
+
userId as configuredUserId,
|
|
14
|
+
saveUserId,
|
|
15
|
+
} from "../lib/constants.js";
|
|
16
|
+
import { getMacOrUuid } from "../lib/mac-or-uuid.js";
|
|
12
17
|
import { createApiClient } from "../lib/api-client.js";
|
|
13
18
|
import v8 from "node:v8";
|
|
14
19
|
|
|
@@ -120,6 +125,15 @@ export async function handleRecheck(options) {
|
|
|
120
125
|
`[Recheck] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}${countryStr}`,
|
|
121
126
|
);
|
|
122
127
|
|
|
128
|
+
// ── 设备标识 ──
|
|
129
|
+
let userId = configuredUserId;
|
|
130
|
+
if (!userId) {
|
|
131
|
+
userId = await getMacOrUuid();
|
|
132
|
+
saveUserId(userId);
|
|
133
|
+
recheckLog(`[Recheck] 未检测到本地用户编号,已生成并使用: ${userId}`);
|
|
134
|
+
}
|
|
135
|
+
recheckLog(`[Recheck] 用户编号: ${userId}`);
|
|
136
|
+
|
|
123
137
|
const { apiGet, apiPost } = createApiClient({ log: false });
|
|
124
138
|
|
|
125
139
|
const scraper = new TikTokScraper({
|
|
@@ -189,7 +203,7 @@ export async function handleRecheck(options) {
|
|
|
189
203
|
? `&countries=${encodeURIComponent(attachCountries.join(","))}`
|
|
190
204
|
: "";
|
|
191
205
|
const { total, tasks } = await apiGet(
|
|
192
|
-
`${serverUrl}/api/recheck-jobs?limit=${attachParallel}${countryParam}`,
|
|
206
|
+
`${serverUrl}/api/recheck-jobs?limit=${attachParallel}&userId=${encodeURIComponent(userId)}${countryParam}`,
|
|
193
207
|
);
|
|
194
208
|
|
|
195
209
|
if (!tasks || tasks.length === 0) {
|
package/src/cli/refresh.js
CHANGED
|
@@ -170,6 +170,9 @@ export async function handleRefresh(options) {
|
|
|
170
170
|
|
|
171
171
|
const page = await getOrCreatePage(browser);
|
|
172
172
|
|
|
173
|
+
// 创建专用商品页(后台复用,避免重复打开/关闭闪屏)
|
|
174
|
+
const shopPage = await browser.contexts()[0].newPage();
|
|
175
|
+
|
|
173
176
|
// 导航到 TikTok 页面
|
|
174
177
|
await withRetry("启动页面导航", async () => {
|
|
175
178
|
await page.goto(STARTUP_TIKTOK_URL, {
|
|
@@ -381,6 +384,7 @@ export async function handleRefresh(options) {
|
|
|
381
384
|
location: exploreLocation,
|
|
382
385
|
locationMode: "refresh",
|
|
383
386
|
browser,
|
|
387
|
+
shopPage,
|
|
384
388
|
proxyServer: cdpOptions.proxyServer || null,
|
|
385
389
|
},
|
|
386
390
|
console.error,
|
|
@@ -407,6 +411,7 @@ export async function handleRefresh(options) {
|
|
|
407
411
|
location: exploreLocation,
|
|
408
412
|
locationMode: "refresh",
|
|
409
413
|
browser,
|
|
414
|
+
shopPage,
|
|
410
415
|
proxyServer: cdpOptions.proxyServer || null,
|
|
411
416
|
},
|
|
412
417
|
console.error,
|
|
@@ -446,6 +451,7 @@ export async function handleRefresh(options) {
|
|
|
446
451
|
location: exploreLocation,
|
|
447
452
|
locationMode: "refresh",
|
|
448
453
|
browser,
|
|
454
|
+
shopPage,
|
|
449
455
|
proxyServer: cdpOptions.proxyServer || null,
|
|
450
456
|
},
|
|
451
457
|
console.error,
|
|
@@ -542,6 +548,8 @@ export async function handleRefresh(options) {
|
|
|
542
548
|
const payload = {
|
|
543
549
|
userInfo: refreshUserInfo,
|
|
544
550
|
recentEcVideos: result.recentEcVideos || null,
|
|
551
|
+
topRecentVideo: result.topRecentVideo || null,
|
|
552
|
+
productDetail: result.productDetail || null,
|
|
545
553
|
discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
|
|
546
554
|
handle: Array.isArray(f) ? f[0] : f,
|
|
547
555
|
displayName: Array.isArray(f) ? f[1] : null,
|
package/src/cli/search.js
CHANGED
|
@@ -43,7 +43,10 @@ import { HealthChecker } from "../lib/browser/health-checker.js";
|
|
|
43
43
|
import {
|
|
44
44
|
server as cfgServer,
|
|
45
45
|
proxy as configuredProxy,
|
|
46
|
+
userId as configuredUserId,
|
|
47
|
+
saveUserId,
|
|
46
48
|
} from "../lib/constants.js";
|
|
49
|
+
import { getMacOrUuid } from "../lib/mac-or-uuid.js";
|
|
47
50
|
|
|
48
51
|
const DEFAULT_SERVER = cfgServer || "http://127.0.0.1:3000";
|
|
49
52
|
const STARTUP_TIKTOK_URL = "https://www.tiktok.com/@ycl5007";
|
|
@@ -71,6 +74,7 @@ export async function handleSearch(parsed) {
|
|
|
71
74
|
serverUrl,
|
|
72
75
|
basePort = 3333,
|
|
73
76
|
portCount = 10,
|
|
77
|
+
parallel = 1,
|
|
74
78
|
proxy: cliProxy,
|
|
75
79
|
} = searchOpts || {};
|
|
76
80
|
|
|
@@ -128,6 +132,15 @@ export async function handleSearch(parsed) {
|
|
|
128
132
|
meta: { port: cdpOptions.port, type: "tag-search" },
|
|
129
133
|
});
|
|
130
134
|
|
|
135
|
+
// ── 设备标识 ──
|
|
136
|
+
let userId = configuredUserId;
|
|
137
|
+
if (!userId) {
|
|
138
|
+
userId = await getMacOrUuid();
|
|
139
|
+
saveUserId(userId);
|
|
140
|
+
log(`[Search] 未检测到本地用户编号,已生成并使用: ${userId}`);
|
|
141
|
+
}
|
|
142
|
+
log(`[Search] 用户编号: ${userId}`);
|
|
143
|
+
|
|
131
144
|
setDelayConfig("normal");
|
|
132
145
|
|
|
133
146
|
try {
|
|
@@ -196,7 +209,9 @@ export async function handleSearch(parsed) {
|
|
|
196
209
|
try {
|
|
197
210
|
return await p.evaluate(async () => {
|
|
198
211
|
try {
|
|
199
|
-
const res = await fetch(
|
|
212
|
+
const res = await fetch(
|
|
213
|
+
"https://www.tiktok.com/passport/web/account/info/",
|
|
214
|
+
);
|
|
200
215
|
const data = await res.json();
|
|
201
216
|
return data?.data?.unique_id || null;
|
|
202
217
|
} catch {}
|
|
@@ -366,9 +381,12 @@ export async function handleSearch(parsed) {
|
|
|
366
381
|
await delay(switchMax, switchMax * 3);
|
|
367
382
|
|
|
368
383
|
// ── 1. 获取搜索任务 ──
|
|
369
|
-
const taskRes = await fetch(
|
|
370
|
-
|
|
371
|
-
|
|
384
|
+
const taskRes = await fetch(
|
|
385
|
+
`${baseUrl}/api/tags/search-task?userId=${encodeURIComponent(userId)}`,
|
|
386
|
+
{
|
|
387
|
+
headers: headers(),
|
|
388
|
+
},
|
|
389
|
+
);
|
|
372
390
|
const task = await taskRes.json();
|
|
373
391
|
|
|
374
392
|
if (!task.tag) {
|
|
@@ -393,11 +411,14 @@ export async function handleSearch(parsed) {
|
|
|
393
411
|
);
|
|
394
412
|
|
|
395
413
|
// ── 2. 锁定标签 ──
|
|
396
|
-
const claimRes = await fetch(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
414
|
+
const claimRes = await fetch(
|
|
415
|
+
`${baseUrl}/api/tags/search-claim?userId=${encodeURIComponent(userId)}`,
|
|
416
|
+
{
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: headers({ "Content-Type": "application/json" }),
|
|
419
|
+
body: JSON.stringify({ tag }),
|
|
420
|
+
},
|
|
421
|
+
);
|
|
401
422
|
const claimData = await claimRes.json();
|
|
402
423
|
if (!claimData.ok) {
|
|
403
424
|
log(` ⏭️ ${claimData.error},跳过`);
|
|
@@ -410,6 +431,7 @@ export async function handleSearch(parsed) {
|
|
|
410
431
|
let searchError = null;
|
|
411
432
|
try {
|
|
412
433
|
ids = await collectSearchAuthors(browser, tag, {
|
|
434
|
+
parallel,
|
|
413
435
|
log,
|
|
414
436
|
exclude: myUsername ? [myUsername] : [],
|
|
415
437
|
});
|
|
@@ -432,6 +454,7 @@ export async function handleSearch(parsed) {
|
|
|
432
454
|
// 重试搜索
|
|
433
455
|
try {
|
|
434
456
|
ids = await collectSearchAuthors(browser, tag, {
|
|
457
|
+
parallel,
|
|
435
458
|
log,
|
|
436
459
|
exclude: myUsername ? [myUsername] : [],
|
|
437
460
|
});
|
|
@@ -457,6 +480,7 @@ export async function handleSearch(parsed) {
|
|
|
457
480
|
await new Promise((r) => setTimeout(r, 5000));
|
|
458
481
|
try {
|
|
459
482
|
ids = await collectSearchAuthors(browser, tag, {
|
|
483
|
+
parallel,
|
|
460
484
|
log,
|
|
461
485
|
exclude: myUsername ? [myUsername] : [],
|
|
462
486
|
});
|
|
@@ -485,11 +509,14 @@ export async function handleSearch(parsed) {
|
|
|
485
509
|
).catch(() => {});
|
|
486
510
|
|
|
487
511
|
// 仍提交空结果,释放标签锁定
|
|
488
|
-
await fetch(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
512
|
+
await fetch(
|
|
513
|
+
`${baseUrl}/api/tags/search-result?userId=${encodeURIComponent(userId)}`,
|
|
514
|
+
{
|
|
515
|
+
method: "POST",
|
|
516
|
+
headers: headers({ "Content-Type": "application/json" }),
|
|
517
|
+
body: JSON.stringify({ tag, authors: [], country }),
|
|
518
|
+
},
|
|
519
|
+
).catch(() => {});
|
|
493
520
|
log(` 提交: 0 个作者(搜索异常)`);
|
|
494
521
|
totalSearched++;
|
|
495
522
|
continue;
|
|
@@ -525,20 +552,26 @@ export async function handleSearch(parsed) {
|
|
|
525
552
|
// ── 4. 提交结果 ──
|
|
526
553
|
let pushed = 0;
|
|
527
554
|
if (ids.length > 0) {
|
|
528
|
-
const resultRes = await fetch(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
555
|
+
const resultRes = await fetch(
|
|
556
|
+
`${baseUrl}/api/tags/search-result?userId=${encodeURIComponent(userId)}`,
|
|
557
|
+
{
|
|
558
|
+
method: "POST",
|
|
559
|
+
headers: headers({ "Content-Type": "application/json" }),
|
|
560
|
+
body: JSON.stringify({ tag, authors: ids, country }),
|
|
561
|
+
},
|
|
562
|
+
);
|
|
533
563
|
const result = await resultRes.json();
|
|
534
564
|
pushed = result.pushed || 0;
|
|
535
565
|
log(` 提交: 新增 ${pushed} 个, 跳过 ${result.skipped} 个`);
|
|
536
566
|
} else {
|
|
537
|
-
await fetch(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
567
|
+
await fetch(
|
|
568
|
+
`${baseUrl}/api/tags/search-result?userId=${encodeURIComponent(userId)}`,
|
|
569
|
+
{
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers: headers({ "Content-Type": "application/json" }),
|
|
572
|
+
body: JSON.stringify({ tag, authors: [], country }),
|
|
573
|
+
},
|
|
574
|
+
);
|
|
542
575
|
log(` 提交: 0 个作者`);
|
|
543
576
|
}
|
|
544
577
|
|
package/src/cli/tag.js
CHANGED
|
@@ -724,9 +724,10 @@ export async function handleScore(parsed) {
|
|
|
724
724
|
log("");
|
|
725
725
|
}
|
|
726
726
|
|
|
727
|
-
async function reportToServer(baseUrl, result, clientId, meta) {
|
|
727
|
+
async function reportToServer(baseUrl, result, clientId, meta, userId = "") {
|
|
728
728
|
try {
|
|
729
|
-
const
|
|
729
|
+
const userIdParam = userId ? `?userId=${encodeURIComponent(userId)}` : "";
|
|
730
|
+
const res = await fetch(`${baseUrl}/api/tags/score-result${userIdParam}`, {
|
|
730
731
|
method: "POST",
|
|
731
732
|
headers: buildClientHeaders(clientId, meta, {
|
|
732
733
|
"Content-Type": "application/json",
|
|
@@ -972,9 +973,12 @@ export async function handleScoreAll(parsed) {
|
|
|
972
973
|
while (true) {
|
|
973
974
|
try {
|
|
974
975
|
// 从服务端取下一个 new 标签(同时查剩余数量)
|
|
975
|
-
const tagsRes = await fetch(
|
|
976
|
-
|
|
977
|
-
|
|
976
|
+
const tagsRes = await fetch(
|
|
977
|
+
`${baseUrl}/api/tags?status=new&limit=1&userId=${encodeURIComponent(userId)}`,
|
|
978
|
+
{
|
|
979
|
+
headers: buildClientHeaders(clientId, clientMeta),
|
|
980
|
+
},
|
|
981
|
+
);
|
|
978
982
|
const tagsData = await tagsRes.json();
|
|
979
983
|
|
|
980
984
|
if (!tagsData.tags || tagsData.tags.length === 0) {
|
|
@@ -1018,13 +1022,16 @@ export async function handleScoreAll(parsed) {
|
|
|
1018
1022
|
};
|
|
1019
1023
|
|
|
1020
1024
|
// 锁定 tag(meta 中不放入 tag,避免非 ASCII 字符导致 header ByteString 报错)
|
|
1021
|
-
const claimRes = await fetch(
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1025
|
+
const claimRes = await fetch(
|
|
1026
|
+
`${baseUrl}/api/tags/claim?userId=${encodeURIComponent(userId)}`,
|
|
1027
|
+
{
|
|
1028
|
+
method: "POST",
|
|
1029
|
+
headers: buildClientHeaders(clientId, clientMeta, {
|
|
1030
|
+
"Content-Type": "application/json",
|
|
1031
|
+
}),
|
|
1032
|
+
body: JSON.stringify({ tag }),
|
|
1033
|
+
},
|
|
1034
|
+
);
|
|
1028
1035
|
const claimData = await claimRes.json();
|
|
1029
1036
|
if (!claimData.ok) {
|
|
1030
1037
|
// already claimed: 其他机器抢先了,跳过不标 dead
|
|
@@ -1036,7 +1043,7 @@ export async function handleScoreAll(parsed) {
|
|
|
1036
1043
|
log(` ⚠️ 无法锁定 (${claimData.error}),标记为 dead 并跳过`);
|
|
1037
1044
|
result.error = claimData.error;
|
|
1038
1045
|
result.status = "dead";
|
|
1039
|
-
await reportToServer(baseUrl, result, clientId, clientMeta);
|
|
1046
|
+
await reportToServer(baseUrl, result, clientId, clientMeta, userId);
|
|
1040
1047
|
totalScored++;
|
|
1041
1048
|
await recyclePage();
|
|
1042
1049
|
continue;
|
|
@@ -1068,7 +1075,7 @@ export async function handleScoreAll(parsed) {
|
|
|
1068
1075
|
log(` ⚠️ 标签不存在 (${deadSec}s),标记 dead`);
|
|
1069
1076
|
result.status = "dead";
|
|
1070
1077
|
result.error = "tag_not_found";
|
|
1071
|
-
await reportToServer(baseUrl, result, clientId, clientMeta);
|
|
1078
|
+
await reportToServer(baseUrl, result, clientId, clientMeta, userId);
|
|
1072
1079
|
totalScored++;
|
|
1073
1080
|
await randomDelay(0, 5000);
|
|
1074
1081
|
await recyclePage();
|
|
@@ -1128,7 +1135,7 @@ export async function handleScoreAll(parsed) {
|
|
|
1128
1135
|
}
|
|
1129
1136
|
|
|
1130
1137
|
// 上报结果
|
|
1131
|
-
await reportToServer(baseUrl, result, clientId, clientMeta);
|
|
1138
|
+
await reportToServer(baseUrl, result, clientId, clientMeta, userId);
|
|
1132
1139
|
|
|
1133
1140
|
totalScored++;
|
|
1134
1141
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -1622,9 +1629,12 @@ export async function handleTagRefresh(parsed) {
|
|
|
1622
1629
|
while (true) {
|
|
1623
1630
|
try {
|
|
1624
1631
|
// 从服务端获取下一个 productive 标签
|
|
1625
|
-
const taskRes = await fetch(
|
|
1626
|
-
|
|
1627
|
-
|
|
1632
|
+
const taskRes = await fetch(
|
|
1633
|
+
`${baseUrl}/api/tags/refresh-task?userId=${encodeURIComponent(userId)}`,
|
|
1634
|
+
{
|
|
1635
|
+
headers: buildClientHeaders(clientId, clientMeta),
|
|
1636
|
+
},
|
|
1637
|
+
);
|
|
1628
1638
|
const taskData = await taskRes.json();
|
|
1629
1639
|
|
|
1630
1640
|
if (!taskData.tag) {
|
|
@@ -1661,13 +1671,16 @@ export async function handleTagRefresh(parsed) {
|
|
|
1661
1671
|
};
|
|
1662
1672
|
|
|
1663
1673
|
// 锁定标签
|
|
1664
|
-
const claimRes = await fetch(
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1674
|
+
const claimRes = await fetch(
|
|
1675
|
+
`${baseUrl}/api/tags/refresh-claim?userId=${encodeURIComponent(userId)}`,
|
|
1676
|
+
{
|
|
1677
|
+
method: "POST",
|
|
1678
|
+
headers: buildClientHeaders(clientId, clientMeta, {
|
|
1679
|
+
"Content-Type": "application/json",
|
|
1680
|
+
}),
|
|
1681
|
+
body: JSON.stringify({ tag }),
|
|
1682
|
+
},
|
|
1683
|
+
);
|
|
1671
1684
|
const claimData = await claimRes.json();
|
|
1672
1685
|
if (!claimData.ok) {
|
|
1673
1686
|
if (
|
|
@@ -1705,13 +1718,20 @@ export async function handleTagRefresh(parsed) {
|
|
|
1705
1718
|
const deadSec = ((Date.now() - fetchStart) / 1000).toFixed(1);
|
|
1706
1719
|
log(` ⚠️ 无视频 (${deadSec}s),保留 productive 状态,等待下次刷新`);
|
|
1707
1720
|
result.error = "no videos found";
|
|
1708
|
-
await fetch(
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1721
|
+
await fetch(
|
|
1722
|
+
`${baseUrl}/api/tags/refresh-result?userId=${encodeURIComponent(userId)}`,
|
|
1723
|
+
{
|
|
1724
|
+
method: "POST",
|
|
1725
|
+
headers: buildClientHeaders(clientId, clientMeta, {
|
|
1726
|
+
"Content-Type": "application/json",
|
|
1727
|
+
}),
|
|
1728
|
+
body: JSON.stringify({
|
|
1729
|
+
tag,
|
|
1730
|
+
pushedUsers: 0,
|
|
1731
|
+
error: result.error,
|
|
1732
|
+
}),
|
|
1733
|
+
},
|
|
1734
|
+
).catch(() => {});
|
|
1715
1735
|
totalRefreshed++;
|
|
1716
1736
|
await randomDelay(0, 5000);
|
|
1717
1737
|
await recyclePage();
|
|
@@ -1775,17 +1795,20 @@ export async function handleTagRefresh(parsed) {
|
|
|
1775
1795
|
}
|
|
1776
1796
|
|
|
1777
1797
|
// 上报刷新结果(只更新 pushed_users,不改变 score/status)
|
|
1778
|
-
await fetch(
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1798
|
+
await fetch(
|
|
1799
|
+
`${baseUrl}/api/tags/refresh-result?userId=${encodeURIComponent(userId)}`,
|
|
1800
|
+
{
|
|
1801
|
+
method: "POST",
|
|
1802
|
+
headers: buildClientHeaders(clientId, clientMeta, {
|
|
1803
|
+
"Content-Type": "application/json",
|
|
1804
|
+
}),
|
|
1805
|
+
body: JSON.stringify({
|
|
1806
|
+
tag,
|
|
1807
|
+
pushedUsers: result.pushedUsers,
|
|
1808
|
+
error: result.error,
|
|
1809
|
+
}),
|
|
1810
|
+
},
|
|
1811
|
+
).catch(() => {});
|
|
1789
1812
|
|
|
1790
1813
|
totalRefreshed++;
|
|
1791
1814
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -5,6 +5,65 @@ import {
|
|
|
5
5
|
detectPageErrorWithWait,
|
|
6
6
|
} from "./page-error-detector.js";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 从 item.anchors 中提取 TikTok Shop 商品信息
|
|
10
|
+
* anchors[].extra 是两层 JSON 字符串:
|
|
11
|
+
* 第1层 parse → [{id, keyword, component_key, extra: "...", ...}]
|
|
12
|
+
* 第2层 parse → {product_id, title, seo_url, skus, seller_id, ...}
|
|
13
|
+
*
|
|
14
|
+
* 注意:product_id / seller_id 为 19 位数字,超出 JS Number.MAX_SAFE_INTEGER,
|
|
15
|
+
* 必须用正则从原始字符串中提取,避免 JSON.parse 精度丢失。
|
|
16
|
+
*/
|
|
17
|
+
function extractProductInfo(item) {
|
|
18
|
+
if (!item.anchors || !Array.isArray(item.anchors)) return null;
|
|
19
|
+
for (const anchor of item.anchors) {
|
|
20
|
+
try {
|
|
21
|
+
const extraArr = JSON.parse(anchor.extra || "[]");
|
|
22
|
+
if (!Array.isArray(extraArr)) continue;
|
|
23
|
+
for (const e of extraArr) {
|
|
24
|
+
if (!e.extra || !e.extra.includes("product_id")) continue;
|
|
25
|
+
|
|
26
|
+
// 从原始 JSON 字符串中用正则提取,绕过 JS 大整数精度问题
|
|
27
|
+
const raw = e.extra;
|
|
28
|
+
const pidMatch = raw.match(/"product_id":(\d+)/);
|
|
29
|
+
const sidMatch = raw.match(/"seller_id":"?(\d+)"?/);
|
|
30
|
+
const seoMatch = raw.match(/"seo_url":"((?:[^"\\]|\\.)*)"/);
|
|
31
|
+
const titleMatch = raw.match(/"title":"((?:[^"\\]|\\.)*)"/);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
productId: pidMatch ? pidMatch[1] : "",
|
|
35
|
+
productTitle: titleMatch ? titleMatch[1] : e.keyword || "",
|
|
36
|
+
seoUrl: seoMatch ? seoMatch[1] : "",
|
|
37
|
+
sellerId: sidMatch ? sidMatch[1] : "",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// 解析失败则跳过该 anchor
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 构建统一的视频对象
|
|
49
|
+
*/
|
|
50
|
+
function buildVideoObject(item, username) {
|
|
51
|
+
const href = `https://www.tiktok.com/@${username}/video/${item.id}`;
|
|
52
|
+
const productInfo = extractProductInfo(item);
|
|
53
|
+
return {
|
|
54
|
+
id: item.id,
|
|
55
|
+
href,
|
|
56
|
+
createTime: item.createTime || null,
|
|
57
|
+
playCount: item.stats?.playCount || 0,
|
|
58
|
+
isECVideo: item.isECVideo ? 1 : 0,
|
|
59
|
+
isAd: item.isAd ? 1 : 0,
|
|
60
|
+
productId: productInfo?.productId || null,
|
|
61
|
+
productTitle: productInfo?.productTitle || null,
|
|
62
|
+
seoUrl: productInfo?.seoUrl || null,
|
|
63
|
+
sellerId: productInfo?.sellerId || null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
8
67
|
async function processAPIResponse(
|
|
9
68
|
data,
|
|
10
69
|
username,
|
|
@@ -17,15 +76,7 @@ async function processAPIResponse(
|
|
|
17
76
|
const firstPageItems = data.itemList || [];
|
|
18
77
|
for (const item of firstPageItems) {
|
|
19
78
|
if (items.length >= maxVideos) break;
|
|
20
|
-
|
|
21
|
-
items.push({
|
|
22
|
-
id: item.id,
|
|
23
|
-
href,
|
|
24
|
-
createTime: item.createTime || null,
|
|
25
|
-
playCount: item.stats?.playCount || 0,
|
|
26
|
-
isECVideo: item.isECVideo ? 1 : 0,
|
|
27
|
-
isAd: item.isAd ? 1 : 0,
|
|
28
|
-
});
|
|
79
|
+
items.push(buildVideoObject(item, username));
|
|
29
80
|
}
|
|
30
81
|
|
|
31
82
|
let cursor = data.cursor;
|
|
@@ -68,15 +119,7 @@ async function processAPIResponse(
|
|
|
68
119
|
if (pageData && pageData.itemList) {
|
|
69
120
|
for (const item of pageData.itemList) {
|
|
70
121
|
if (items.length >= maxVideos) break;
|
|
71
|
-
|
|
72
|
-
items.push({
|
|
73
|
-
id: item.id,
|
|
74
|
-
href,
|
|
75
|
-
createTime: item.createTime || null,
|
|
76
|
-
playCount: item.stats?.playCount || 0,
|
|
77
|
-
isECVideo: item.isECVideo ? 1 : 0,
|
|
78
|
-
isAd: item.isAd ? 1 : 0,
|
|
79
|
-
});
|
|
122
|
+
items.push(buildVideoObject(item, username));
|
|
80
123
|
}
|
|
81
124
|
}
|
|
82
125
|
|
package/src/lib/args.js
CHANGED
|
@@ -560,6 +560,7 @@ function parseSearchArgs(args) {
|
|
|
560
560
|
let serverUrl = null;
|
|
561
561
|
let basePort = 3333;
|
|
562
562
|
let portCount = 10;
|
|
563
|
+
let parallel = 1;
|
|
563
564
|
let proxy = null;
|
|
564
565
|
|
|
565
566
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -570,6 +571,8 @@ function parseSearchArgs(args) {
|
|
|
570
571
|
basePort = parseInt(args[++i]) || 3333;
|
|
571
572
|
} else if (arg === "--port-count") {
|
|
572
573
|
portCount = parseInt(args[++i]) || 10;
|
|
574
|
+
} else if (arg === "-p" || arg === "--parallel") {
|
|
575
|
+
parallel = parseInt(args[++i]) || 1;
|
|
573
576
|
} else if (arg === "--proxy") {
|
|
574
577
|
proxy = args[++i];
|
|
575
578
|
}
|
|
@@ -577,7 +580,7 @@ function parseSearchArgs(args) {
|
|
|
577
580
|
|
|
578
581
|
return {
|
|
579
582
|
subcommand: "search",
|
|
580
|
-
search: { serverUrl, basePort, portCount, proxy },
|
|
583
|
+
search: { serverUrl, basePort, portCount, parallel, proxy },
|
|
581
584
|
urls: [],
|
|
582
585
|
outputFormat: "json",
|
|
583
586
|
exploreCount: 0,
|