tt-help-cli-ycl 1.3.80 → 1.3.81
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/lib/api-interceptor.js +14 -3
- package/src/lib/page-error-detector.js +31 -14
- package/src/lib/scroll-collector.js +1 -1
- package/src/videos/core.js +6 -2
- package/src/watch/data-store.js +118 -31
package/package.json
CHANGED
|
@@ -32,7 +32,7 @@ async function processAPIResponse(
|
|
|
32
32
|
|
|
33
33
|
try {
|
|
34
34
|
const pageData = await (() => {
|
|
35
|
-
//
|
|
35
|
+
// 重试包装:处理页面导航导致的执行上下文销毁和 CDP 断连
|
|
36
36
|
const tryEval = async (retries = 3) => {
|
|
37
37
|
for (let i = 0; i < retries; i++) {
|
|
38
38
|
try {
|
|
@@ -42,8 +42,11 @@ async function processAPIResponse(
|
|
|
42
42
|
}, newUrl);
|
|
43
43
|
} catch (e) {
|
|
44
44
|
if (
|
|
45
|
-
e.message
|
|
46
|
-
|
|
45
|
+
e.message &&
|
|
46
|
+
(e.message.includes("Execution context was destroyed") ||
|
|
47
|
+
e.message.includes("Target closed") ||
|
|
48
|
+
e.message.includes("Connection closed") ||
|
|
49
|
+
e.message.includes("Protocol error"))
|
|
47
50
|
) {
|
|
48
51
|
await delay(500 * (i + 1), 500 * (i + 1));
|
|
49
52
|
} else {
|
|
@@ -89,6 +92,14 @@ async function processAPIResponse(
|
|
|
89
92
|
async function fetchUserVideosAPI(page, username, maxVideos, log) {
|
|
90
93
|
log(` [API拦截] 获取 @${username} 视频 ...`);
|
|
91
94
|
|
|
95
|
+
// CDP 健康检查:确保 page 可用
|
|
96
|
+
try {
|
|
97
|
+
await page.evaluate(() => 1);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
log(` [API拦截] CDP 连接异常: ${e.message}`);
|
|
100
|
+
throw new Error(`CDP 连接异常: ${e.message}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
let apiRequestUrl = null;
|
|
93
104
|
let sawApiRequest = false;
|
|
94
105
|
|
|
@@ -54,23 +54,40 @@ const PATTERNS = {
|
|
|
54
54
|
service_error: ["出错了", "很抱歉"],
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
export async function detectPageError(page) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
export async function detectPageError(page, timeout = 10000) {
|
|
58
|
+
try {
|
|
59
|
+
return await page.evaluate(
|
|
60
|
+
(patterns) => {
|
|
61
|
+
const body = document.body;
|
|
62
|
+
if (!body) return null;
|
|
63
|
+
const bodyText = body.innerText;
|
|
64
|
+
const lower = bodyText.toLowerCase();
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
for (const [type, phrases] of Object.entries(patterns)) {
|
|
67
|
+
for (const phrase of phrases) {
|
|
68
|
+
if (lower.includes(phrase.toLowerCase())) {
|
|
69
|
+
return type;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
68
72
|
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
PATTERNS,
|
|
77
|
+
);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// CDP 断连或超时:返回 null 而非永久挂起
|
|
80
|
+
if (
|
|
81
|
+
e.message &&
|
|
82
|
+
(e.message.includes("Timeout") ||
|
|
83
|
+
e.message.includes("Target closed") ||
|
|
84
|
+
e.message.includes("Connection closed") ||
|
|
85
|
+
e.message.includes("Protocol error"))
|
|
86
|
+
) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
/**
|
|
@@ -30,7 +30,7 @@ async function doCollect(
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const fn = eval("(" + fnStr + ")");
|
|
33
|
-
return fn(el, args);
|
|
33
|
+
return args !== undefined ? fn(el, args) : fn(el);
|
|
34
34
|
},
|
|
35
35
|
{ fn: fnStr, containerSelector: container, findScrollableFlag: findScrollable, args: extraArgs },
|
|
36
36
|
);
|
package/src/videos/core.js
CHANGED
|
@@ -7,14 +7,18 @@ import {
|
|
|
7
7
|
import { fetchUserVideosAPI } from "../lib/api-interceptor.js";
|
|
8
8
|
|
|
9
9
|
async function getUserInfo(page) {
|
|
10
|
-
//
|
|
10
|
+
// 重试包装:处理页面导航导致的执行上下文销毁和 CDP 断连
|
|
11
11
|
const evaluateWithRetry = async (fn, retries = 3) => {
|
|
12
12
|
for (let i = 0; i < retries; i++) {
|
|
13
13
|
try {
|
|
14
14
|
return await page.evaluate(fn);
|
|
15
15
|
} catch (e) {
|
|
16
16
|
if (
|
|
17
|
-
e.message
|
|
17
|
+
e.message &&
|
|
18
|
+
(e.message.includes("Execution context was destroyed") ||
|
|
19
|
+
e.message.includes("Target closed") ||
|
|
20
|
+
e.message.includes("Connection closed") ||
|
|
21
|
+
e.message.includes("Protocol error")) &&
|
|
18
22
|
i < retries - 1
|
|
19
23
|
) {
|
|
20
24
|
await new Promise((r) => setTimeout(r, 500 * (i + 1)));
|
package/src/watch/data-store.js
CHANGED
|
@@ -650,13 +650,24 @@ function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
650
650
|
AND instr(COALESCE(sources, ''), '"guess"') = 0
|
|
651
651
|
AND instr(COALESCE(sources, ''), '"following"') = 0
|
|
652
652
|
AND instr(COALESCE(sources, ''), '"follower"') = 0
|
|
653
|
-
THEN 1 ELSE 0 END) as seed
|
|
654
|
-
SUM(CASE WHEN COALESCE(tt_seller, '') = '' AND COALESCE(user_update_count, 0) <= 0 THEN 1 ELSE 0 END) as userUpdateTasks
|
|
653
|
+
THEN 1 ELSE 0 END) as seed
|
|
655
654
|
FROM jobs
|
|
656
655
|
`,
|
|
657
656
|
)
|
|
658
657
|
.get(...targetParams);
|
|
659
658
|
|
|
659
|
+
// userUpdateTasks 单独从 jobs_base 统计
|
|
660
|
+
const userUpdateTasksRow = db
|
|
661
|
+
.prepare(
|
|
662
|
+
`
|
|
663
|
+
SELECT COUNT(*) as userUpdateTasks
|
|
664
|
+
FROM jobs_base
|
|
665
|
+
WHERE COALESCE(tt_seller, '') = ''
|
|
666
|
+
AND COALESCE(user_update_count, 0) <= 0
|
|
667
|
+
`,
|
|
668
|
+
)
|
|
669
|
+
.get();
|
|
670
|
+
|
|
660
671
|
// countryStats 和 targetCountryStats 需要 GROUP BY,保留为独立查询
|
|
661
672
|
const countryStats = db
|
|
662
673
|
.prepare(
|
|
@@ -712,7 +723,7 @@ function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
712
723
|
restrictedUsers: aggregateRow.restricted,
|
|
713
724
|
errorUsers: aggregateRow.error,
|
|
714
725
|
targetUsers: aggregateRow.targetUsers,
|
|
715
|
-
userUpdateTasks:
|
|
726
|
+
userUpdateTasks: userUpdateTasksRow.userUpdateTasks,
|
|
716
727
|
targetCountryStats,
|
|
717
728
|
countryStats,
|
|
718
729
|
sourceStats: {
|
|
@@ -761,7 +772,7 @@ function getUserUpdateByCountryFromDb() {
|
|
|
761
772
|
SELECT
|
|
762
773
|
COALESCE(guessed_location, '未知') as country,
|
|
763
774
|
COUNT(*) as count
|
|
764
|
-
FROM
|
|
775
|
+
FROM jobs_base
|
|
765
776
|
WHERE COALESCE(tt_seller, '') = ''
|
|
766
777
|
AND COALESCE(user_update_count, 0) <= 0
|
|
767
778
|
GROUP BY COALESCE(guessed_location, '未知')
|
|
@@ -782,7 +793,7 @@ function getAttachStuckByCountryFromDb() {
|
|
|
782
793
|
SELECT
|
|
783
794
|
COALESCE(guessed_location, '未知') as country,
|
|
784
795
|
COUNT(*) as count
|
|
785
|
-
FROM
|
|
796
|
+
FROM jobs_base
|
|
786
797
|
WHERE COALESCE(tt_seller, '') = ''
|
|
787
798
|
AND COALESCE(user_update_count, 0) = 1
|
|
788
799
|
GROUP BY COALESCE(guessed_location, '未知')
|
|
@@ -816,7 +827,7 @@ function restoreAttachStuckByCountry(country) {
|
|
|
816
827
|
.prepare(
|
|
817
828
|
`
|
|
818
829
|
SELECT COUNT(*) as c
|
|
819
|
-
FROM
|
|
830
|
+
FROM jobs_base
|
|
820
831
|
WHERE ${whereSql}
|
|
821
832
|
`,
|
|
822
833
|
)
|
|
@@ -828,7 +839,7 @@ function restoreAttachStuckByCountry(country) {
|
|
|
828
839
|
|
|
829
840
|
db.prepare(
|
|
830
841
|
`
|
|
831
|
-
UPDATE
|
|
842
|
+
UPDATE jobs_base
|
|
832
843
|
SET user_update_count = 0,
|
|
833
844
|
updated_at = ?,
|
|
834
845
|
claimed_by = NULL,
|
|
@@ -943,7 +954,7 @@ function moveJobsToRawByCountry(scope, country) {
|
|
|
943
954
|
.prepare(
|
|
944
955
|
`
|
|
945
956
|
SELECT COUNT(*) as c
|
|
946
|
-
FROM
|
|
957
|
+
FROM jobs_base
|
|
947
958
|
WHERE ${whereSql}
|
|
948
959
|
`,
|
|
949
960
|
)
|
|
@@ -1017,14 +1028,14 @@ function moveJobsToRawByCountry(scope, country) {
|
|
|
1017
1028
|
signature,
|
|
1018
1029
|
sec_uid,
|
|
1019
1030
|
latest_video_time
|
|
1020
|
-
FROM
|
|
1031
|
+
FROM jobs_base
|
|
1021
1032
|
WHERE ${whereSql}
|
|
1022
1033
|
`,
|
|
1023
1034
|
).run(targetCountry);
|
|
1024
1035
|
|
|
1025
1036
|
db.prepare(
|
|
1026
1037
|
`
|
|
1027
|
-
DELETE FROM
|
|
1038
|
+
DELETE FROM jobs_base
|
|
1028
1039
|
WHERE ${whereSql}
|
|
1029
1040
|
`,
|
|
1030
1041
|
).run(targetCountry);
|
|
@@ -1718,6 +1729,13 @@ function getJobRow(uniqueId) {
|
|
|
1718
1729
|
return db.prepare("SELECT * FROM jobs WHERE unique_id = ?").get(uniqueId);
|
|
1719
1730
|
}
|
|
1720
1731
|
|
|
1732
|
+
function getJobBaseRow(uniqueId) {
|
|
1733
|
+
if (!db) return null;
|
|
1734
|
+
return db
|
|
1735
|
+
.prepare("SELECT * FROM jobs_base WHERE unique_id = ?")
|
|
1736
|
+
.get(uniqueId);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1721
1739
|
function getJob(uniqueId) {
|
|
1722
1740
|
return mapJobRow(getJobRow(uniqueId));
|
|
1723
1741
|
}
|
|
@@ -1795,6 +1813,43 @@ function inferStatus(u) {
|
|
|
1795
1813
|
return "pending";
|
|
1796
1814
|
}
|
|
1797
1815
|
|
|
1816
|
+
function updateJobBaseInfo(uniqueId, info, incrementCount = true) {
|
|
1817
|
+
if (!db) return { error: "db not initialized" };
|
|
1818
|
+
const existing = getJobBaseRow(uniqueId);
|
|
1819
|
+
if (!existing) return { error: "user not found" };
|
|
1820
|
+
|
|
1821
|
+
const nextValues = {};
|
|
1822
|
+
for (const [key, value] of Object.entries(info || {})) {
|
|
1823
|
+
if (key === "uniqueId" || key === "unique_id") continue;
|
|
1824
|
+
if (value === undefined || value === "") continue;
|
|
1825
|
+
let column = camelToSnake(key);
|
|
1826
|
+
// 字段别名:bio → signature
|
|
1827
|
+
if (column === "bio") column = "signature";
|
|
1828
|
+
if (!writableJobColumns.has(column)) continue;
|
|
1829
|
+
nextValues[column] = normalizeJobValue(column, value);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
nextValues.updated_at = Date.now();
|
|
1833
|
+
if (incrementCount) {
|
|
1834
|
+
nextValues.user_update_count = (existing.user_update_count || 0) + 1;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const columns = Object.keys(nextValues);
|
|
1838
|
+
if (columns.length > 0) {
|
|
1839
|
+
const sql = `UPDATE jobs_base SET ${columns.map((column) => `${column} = ?`).join(", ")} WHERE unique_id = ?`;
|
|
1840
|
+
db.prepare(sql).run(
|
|
1841
|
+
...columns.map((column) => nextValues[column]),
|
|
1842
|
+
uniqueId,
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
return {
|
|
1847
|
+
ok: true,
|
|
1848
|
+
userUpdateCount:
|
|
1849
|
+
nextValues.user_update_count ?? existing.user_update_count ?? 0,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1798
1853
|
function addJobBaseToDb(user) {
|
|
1799
1854
|
if (!db) return;
|
|
1800
1855
|
const now = Date.now();
|
|
@@ -3375,7 +3430,7 @@ export function createStore(filePath) {
|
|
|
3375
3430
|
|
|
3376
3431
|
let sql = `
|
|
3377
3432
|
SELECT *
|
|
3378
|
-
FROM
|
|
3433
|
+
FROM jobs_base
|
|
3379
3434
|
WHERE COALESCE(tt_seller, '') = ''
|
|
3380
3435
|
AND COALESCE(user_update_count, 0) <= 0
|
|
3381
3436
|
`;
|
|
@@ -3395,7 +3450,7 @@ export function createStore(filePath) {
|
|
|
3395
3450
|
const now = Date.now();
|
|
3396
3451
|
const bumpStmt = db.prepare(
|
|
3397
3452
|
`
|
|
3398
|
-
UPDATE
|
|
3453
|
+
UPDATE jobs_base
|
|
3399
3454
|
SET user_update_count = COALESCE(user_update_count, 0) + 1,
|
|
3400
3455
|
updated_at = ?
|
|
3401
3456
|
WHERE unique_id = ?
|
|
@@ -3526,7 +3581,8 @@ export function createStore(filePath) {
|
|
|
3526
3581
|
function batchUpdateUserInfo(updates) {
|
|
3527
3582
|
if (db) {
|
|
3528
3583
|
const results = [];
|
|
3529
|
-
const
|
|
3584
|
+
const rawMoveList = [];
|
|
3585
|
+
const sellerMoveList = [];
|
|
3530
3586
|
|
|
3531
3587
|
const txn = db.transaction((items) => {
|
|
3532
3588
|
items.forEach((item) => {
|
|
@@ -3536,13 +3592,13 @@ export function createStore(filePath) {
|
|
|
3536
3592
|
let updateResult;
|
|
3537
3593
|
if (info && info.error && info.statusCode !== undefined) {
|
|
3538
3594
|
// 只更新 status_code,不更新其他字段
|
|
3539
|
-
updateResult =
|
|
3595
|
+
updateResult = updateJobBaseInfo(
|
|
3540
3596
|
uniqueId,
|
|
3541
3597
|
{ statusCode: info.statusCode },
|
|
3542
3598
|
true,
|
|
3543
3599
|
);
|
|
3544
3600
|
} else {
|
|
3545
|
-
updateResult =
|
|
3601
|
+
updateResult = updateJobBaseInfo(uniqueId, info, true);
|
|
3546
3602
|
}
|
|
3547
3603
|
|
|
3548
3604
|
if (updateResult.error) {
|
|
@@ -3550,34 +3606,66 @@ export function createStore(filePath) {
|
|
|
3550
3606
|
return;
|
|
3551
3607
|
}
|
|
3552
3608
|
|
|
3553
|
-
// 检查 tt_seller
|
|
3554
|
-
const row =
|
|
3609
|
+
// 检查 tt_seller:商家移到 jobs,非商家移到 raw_jobs
|
|
3610
|
+
const row = getJobBaseRow(uniqueId);
|
|
3555
3611
|
const ttSeller = row ? row.tt_seller : null;
|
|
3556
3612
|
if (ttSeller) {
|
|
3557
|
-
//
|
|
3613
|
+
// 商家:标记移动到 jobs
|
|
3558
3614
|
results.push({
|
|
3559
3615
|
uniqueId,
|
|
3560
3616
|
ok: true,
|
|
3561
3617
|
userUpdateCount: updateResult.userUpdateCount,
|
|
3618
|
+
_movedToJobs: true,
|
|
3562
3619
|
});
|
|
3620
|
+
sellerMoveList.push(uniqueId);
|
|
3563
3621
|
} else {
|
|
3564
|
-
//
|
|
3622
|
+
// 非商家:标记移动到 raw_jobs
|
|
3565
3623
|
results.push({
|
|
3566
3624
|
uniqueId,
|
|
3567
3625
|
ok: true,
|
|
3568
3626
|
userUpdateCount: updateResult.userUpdateCount,
|
|
3569
3627
|
_movedToRaw: true,
|
|
3570
3628
|
});
|
|
3571
|
-
|
|
3629
|
+
rawMoveList.push(uniqueId);
|
|
3572
3630
|
}
|
|
3573
3631
|
});
|
|
3574
3632
|
});
|
|
3575
3633
|
txn(updates);
|
|
3576
3634
|
|
|
3577
|
-
//
|
|
3578
|
-
if (
|
|
3579
|
-
const placeholders =
|
|
3580
|
-
|
|
3635
|
+
// 批量移动商家用户到 jobs
|
|
3636
|
+
if (sellerMoveList.length > 0) {
|
|
3637
|
+
const placeholders = sellerMoveList.map(() => "?").join(",");
|
|
3638
|
+
db.prepare(
|
|
3639
|
+
`
|
|
3640
|
+
INSERT OR REPLACE INTO jobs (
|
|
3641
|
+
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3642
|
+
error, pinned, no_video, restricted, user_update_count,
|
|
3643
|
+
tt_seller, verified, video_count, comment_count,
|
|
3644
|
+
guessed_location, location_created, confirmed_location, modified_at,
|
|
3645
|
+
follower_count, following_count, heart_count, refresh_time,
|
|
3646
|
+
processed, processed_at, created_at, updated_at,
|
|
3647
|
+
region, signature, bio_link, sec_uid, status_code, latest_video_time
|
|
3648
|
+
)
|
|
3649
|
+
SELECT
|
|
3650
|
+
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3651
|
+
error, pinned, no_video, restricted, user_update_count,
|
|
3652
|
+
tt_seller, verified, video_count, comment_count,
|
|
3653
|
+
guessed_location, location_created, confirmed_location, modified_at,
|
|
3654
|
+
follower_count, following_count, heart_count, refresh_time,
|
|
3655
|
+
processed, processed_at, created_at, updated_at,
|
|
3656
|
+
region, signature, bio_link, sec_uid, status_code, latest_video_time
|
|
3657
|
+
FROM jobs_base WHERE unique_id IN (${placeholders})
|
|
3658
|
+
`,
|
|
3659
|
+
).run(...sellerMoveList);
|
|
3660
|
+
|
|
3661
|
+
db.prepare(
|
|
3662
|
+
`DELETE FROM jobs_base WHERE unique_id IN (${placeholders})`,
|
|
3663
|
+
).run(...sellerMoveList);
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
// 批量移动非商家用户到 raw_jobs
|
|
3667
|
+
if (rawMoveList.length > 0) {
|
|
3668
|
+
const placeholders = rawMoveList.map(() => "?").join(",");
|
|
3581
3669
|
db.prepare(
|
|
3582
3670
|
`
|
|
3583
3671
|
INSERT OR REPLACE INTO raw_jobs (
|
|
@@ -3597,19 +3685,18 @@ export function createStore(filePath) {
|
|
|
3597
3685
|
follower_count, following_count, heart_count, refresh_time,
|
|
3598
3686
|
processed, processed_at, created_at, updated_at,
|
|
3599
3687
|
region, signature, bio_link, sec_uid, status_code, latest_video_time
|
|
3600
|
-
FROM
|
|
3688
|
+
FROM jobs_base WHERE unique_id IN (${placeholders})
|
|
3601
3689
|
`,
|
|
3602
|
-
).run(...
|
|
3690
|
+
).run(...rawMoveList);
|
|
3603
3691
|
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
);
|
|
3692
|
+
db.prepare(
|
|
3693
|
+
`DELETE FROM jobs_base WHERE unique_id IN (${placeholders})`,
|
|
3694
|
+
).run(...rawMoveList);
|
|
3608
3695
|
}
|
|
3609
3696
|
|
|
3610
3697
|
// 清理内部标记
|
|
3611
3698
|
return results.map((r) => {
|
|
3612
|
-
const { _movedToRaw, ...rest } = r;
|
|
3699
|
+
const { _movedToRaw, _movedToJobs, ...rest } = r;
|
|
3613
3700
|
return rest;
|
|
3614
3701
|
});
|
|
3615
3702
|
}
|