tt-help-cli-ycl 1.3.52 → 1.3.57
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 +25 -13
- package/src/cli/info.js +50 -28
- package/src/cli/open.js +3 -2
- package/src/lib/parse-ssr.mjs +70 -15
- package/src/lib/scrape.js +2 -3
- package/src/lib/tiktok-scraper.mjs +118 -217
- package/src/scraper/explore-core.js +2 -0
- package/src/watch/data-store.js +488 -112
- package/src/watch/public/index.html +115 -16
- package/src/watch/server.js +45 -3
- package/scripts/run-explore copy.bat +0 -101
- package/scripts/test-captcha-lib.mjs +0 -68
- package/scripts/test-captcha.mjs +0 -81
- package/scripts/test-incognito-lib.mjs +0 -36
- package/scripts/test-login-state.mjs +0 -128
- package/scripts/test-safe-click.mjs +0 -45
- package/scripts/test-watch-db-smoke.mjs +0 -246
package/src/watch/data-store.js
CHANGED
|
@@ -114,9 +114,21 @@ function initUserDb(filePath) {
|
|
|
114
114
|
updated_at INTEGER,
|
|
115
115
|
region TEXT,
|
|
116
116
|
signature TEXT,
|
|
117
|
-
sec_uid TEXT
|
|
117
|
+
sec_uid TEXT,
|
|
118
|
+
status_code INTEGER
|
|
118
119
|
)
|
|
119
120
|
`);
|
|
121
|
+
|
|
122
|
+
// 迁移:为已存在的 jobs 表添加 status_code 列
|
|
123
|
+
const existingJobColumns = new Set(
|
|
124
|
+
db
|
|
125
|
+
.prepare("PRAGMA table_info(jobs)")
|
|
126
|
+
.all()
|
|
127
|
+
.map((c) => c.name),
|
|
128
|
+
);
|
|
129
|
+
if (!existingJobColumns.has("status_code")) {
|
|
130
|
+
db.exec(`ALTER TABLE jobs ADD COLUMN status_code INTEGER`);
|
|
131
|
+
}
|
|
120
132
|
db.exec(`
|
|
121
133
|
CREATE TABLE IF NOT EXISTS raw_jobs (
|
|
122
134
|
unique_id TEXT PRIMARY KEY,
|
|
@@ -146,9 +158,21 @@ function initUserDb(filePath) {
|
|
|
146
158
|
updated_at INTEGER,
|
|
147
159
|
region TEXT,
|
|
148
160
|
signature TEXT,
|
|
149
|
-
sec_uid TEXT
|
|
161
|
+
sec_uid TEXT,
|
|
162
|
+
status_code INTEGER
|
|
150
163
|
)
|
|
151
164
|
`);
|
|
165
|
+
|
|
166
|
+
// 迁移:为已存在的 raw_jobs 表添加 status_code 列
|
|
167
|
+
const existingRawJobColumns = new Set(
|
|
168
|
+
db
|
|
169
|
+
.prepare("PRAGMA table_info(raw_jobs)")
|
|
170
|
+
.all()
|
|
171
|
+
.map((c) => c.name),
|
|
172
|
+
);
|
|
173
|
+
if (!existingRawJobColumns.has("status_code")) {
|
|
174
|
+
db.exec(`ALTER TABLE raw_jobs ADD COLUMN status_code INTEGER`);
|
|
175
|
+
}
|
|
152
176
|
db.exec(`
|
|
153
177
|
CREATE TABLE IF NOT EXISTS videos (
|
|
154
178
|
id TEXT PRIMARY KEY,
|
|
@@ -1065,9 +1089,7 @@ function restoreRawJobsByFilter({ search, location }) {
|
|
|
1065
1089
|
|
|
1066
1090
|
const count =
|
|
1067
1091
|
db
|
|
1068
|
-
.prepare(
|
|
1069
|
-
`SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql}`,
|
|
1070
|
-
)
|
|
1092
|
+
.prepare(`SELECT COUNT(*) as c FROM raw_jobs WHERE ${whereSql}`)
|
|
1071
1093
|
.get(...args)?.c || 0;
|
|
1072
1094
|
|
|
1073
1095
|
if (!count) {
|
|
@@ -1146,6 +1168,17 @@ function getRawJobsPageFromDb({ search, location, limit, offset }) {
|
|
|
1146
1168
|
};
|
|
1147
1169
|
}
|
|
1148
1170
|
|
|
1171
|
+
// 调试接口:直接执行 SQL 查询,返回原始数据
|
|
1172
|
+
function rawQuery(sql, params = []) {
|
|
1173
|
+
if (!db) return { error: "db not ready" };
|
|
1174
|
+
try {
|
|
1175
|
+
const rows = db.prepare(sql).all(...params);
|
|
1176
|
+
return { rows };
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
return { error: e.message };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1149
1182
|
function getUsersPageFromDb({
|
|
1150
1183
|
status,
|
|
1151
1184
|
search,
|
|
@@ -1293,6 +1326,7 @@ const jobBooleanColumns = new Set([
|
|
|
1293
1326
|
"processed",
|
|
1294
1327
|
"tt_seller",
|
|
1295
1328
|
"verified",
|
|
1329
|
+
"error",
|
|
1296
1330
|
]);
|
|
1297
1331
|
|
|
1298
1332
|
const videoBooleanColumns = new Set(["tt_seller"]);
|
|
@@ -1324,17 +1358,20 @@ const writableJobColumns = new Set([
|
|
|
1324
1358
|
"region",
|
|
1325
1359
|
"signature",
|
|
1326
1360
|
"sec_uid",
|
|
1361
|
+
"status_code",
|
|
1327
1362
|
]);
|
|
1328
1363
|
|
|
1329
1364
|
function normalizeJobValue(column, value) {
|
|
1365
|
+
if (value === undefined || value === null) return null;
|
|
1330
1366
|
if (column === "sources") {
|
|
1331
1367
|
if (!Array.isArray(value)) return JSON.stringify([]);
|
|
1332
1368
|
return JSON.stringify([...new Set(value)]);
|
|
1333
1369
|
}
|
|
1334
1370
|
if (jobBooleanColumns.has(column)) {
|
|
1335
|
-
if (value === undefined || value === null || value === "") return null;
|
|
1336
1371
|
return value ? 1 : 0;
|
|
1337
1372
|
}
|
|
1373
|
+
// 防御:如果值是对象或数组,转为 JSON 字符串
|
|
1374
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1338
1375
|
return value;
|
|
1339
1376
|
}
|
|
1340
1377
|
|
|
@@ -1812,7 +1849,15 @@ export function createStore(filePath) {
|
|
|
1812
1849
|
const args = [];
|
|
1813
1850
|
if (!loggedIn) {
|
|
1814
1851
|
where.push("COALESCE(tt_seller, 0) != 1");
|
|
1852
|
+
// 未登录:只能领取 status_code 为空或 0 的任务
|
|
1853
|
+
where.push("(status_code IS NULL OR status_code = 0)");
|
|
1854
|
+
} else {
|
|
1855
|
+
// 登录:可以领取 status_code 为空、0、或 209002 的任务
|
|
1856
|
+
where.push(
|
|
1857
|
+
"(status_code IS NULL OR status_code = 0 OR status_code = 209002)",
|
|
1858
|
+
);
|
|
1815
1859
|
}
|
|
1860
|
+
// 其他 status_code 值的任务不被领取
|
|
1816
1861
|
if (requireVideo) {
|
|
1817
1862
|
where.push("COALESCE(video_count, 0) > 0");
|
|
1818
1863
|
}
|
|
@@ -1989,124 +2034,433 @@ export function createStore(filePath) {
|
|
|
1989
2034
|
return null;
|
|
1990
2035
|
}
|
|
1991
2036
|
|
|
1992
|
-
|
|
2037
|
+
if (!db) {
|
|
2038
|
+
const now = Date.now();
|
|
1993
2039
|
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2040
|
+
// 0. 该客户端有未过期的任务,续期返回
|
|
2041
|
+
const ongoing = data.find(
|
|
2042
|
+
(u) =>
|
|
2043
|
+
u.status === "processing" &&
|
|
2044
|
+
u.claimedBy === userId &&
|
|
2045
|
+
u.claimedAt &&
|
|
2046
|
+
now - u.claimedAt < expireMs,
|
|
2047
|
+
);
|
|
2048
|
+
if (ongoing) {
|
|
2049
|
+
ongoing.claimedAt = now;
|
|
2050
|
+
save();
|
|
2051
|
+
return {
|
|
2052
|
+
uniqueId: ongoing.uniqueId,
|
|
2053
|
+
nickname: ongoing.nickname,
|
|
2054
|
+
claimedAt: ongoing.claimedAt,
|
|
2055
|
+
claimedBy: userId,
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2012
2058
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2059
|
+
// 按猜测国家梯队排序
|
|
2060
|
+
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
2061
|
+
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
2062
|
+
function locationTier(u) {
|
|
2063
|
+
const loc = (u.guessedLocation || "").toUpperCase();
|
|
2064
|
+
if (tier1.has(loc)) return 0;
|
|
2065
|
+
if (tier2.has(loc)) return 1;
|
|
2066
|
+
return 2;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// 国家过滤:如果指定了 locations,只保留 guessedLocation 在列表中的用户
|
|
2070
|
+
function locationFilter(u) {
|
|
2071
|
+
if (!locations || locations.length === 0) return true;
|
|
2072
|
+
return isLocationInList(u.guessedLocation, locations);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// 从候选列表中按优先级取第一个:pinned > 超时回收 > seed > ttSeller(仅登录) > follow > other
|
|
2076
|
+
function pickCandidate(candidates) {
|
|
2077
|
+
let next = candidates.find((u) => u.pinned);
|
|
2078
|
+
|
|
2079
|
+
if (!next) {
|
|
2080
|
+
const expired = data.find(
|
|
2081
|
+
(u) =>
|
|
2082
|
+
u.status === "processing" &&
|
|
2083
|
+
u.claimedAt &&
|
|
2084
|
+
now - u.claimedAt > expireMs,
|
|
2085
|
+
);
|
|
2086
|
+
if (expired) {
|
|
2087
|
+
expired.status = "pending";
|
|
2088
|
+
markStatsDirty();
|
|
2089
|
+
delete expired.claimedAt;
|
|
2090
|
+
next = expired;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if (!next) {
|
|
2095
|
+
const seed = candidates.filter(
|
|
2096
|
+
(u) => u.sources && u.sources.includes("seed"),
|
|
2097
|
+
);
|
|
2098
|
+
seed.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2099
|
+
next = seed[0] || null;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// 未登录时跳过 ttSeller 优先级
|
|
2103
|
+
if (!next && loggedIn) {
|
|
2104
|
+
const ttSeller = candidates.filter(
|
|
2105
|
+
(u) => u.ttSeller === true && u.verified === false,
|
|
2106
|
+
);
|
|
2107
|
+
ttSeller.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2108
|
+
next = ttSeller[0] || null;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
if (!next) {
|
|
2112
|
+
const follow = candidates.filter(
|
|
2113
|
+
(u) =>
|
|
2114
|
+
u.sources &&
|
|
2115
|
+
(u.sources.includes("following") ||
|
|
2116
|
+
u.sources.includes("follower")),
|
|
2117
|
+
);
|
|
2118
|
+
follow.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2119
|
+
next = follow[0] || null;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (!next) {
|
|
2123
|
+
candidates.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2124
|
+
next = candidates[0] || null;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
return next;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// 先在有视频的 pending 用户中找;找不到再用全部 pending 用户兜底
|
|
2131
|
+
let pending = data.filter((u) => u.status === "pending");
|
|
2132
|
+
// 应用国家过滤
|
|
2133
|
+
if (locations && locations.length > 0) {
|
|
2134
|
+
pending = pending.filter(locationFilter);
|
|
2135
|
+
}
|
|
2136
|
+
// 未登录客户端不能领取 ttSeller 用户
|
|
2137
|
+
if (!loggedIn) {
|
|
2138
|
+
pending = pending.filter((u) => u.ttSeller !== true);
|
|
2139
|
+
}
|
|
2140
|
+
// status_code 过滤:只领取空值、0 或 209002 的任务
|
|
2141
|
+
pending = pending.filter(
|
|
2142
|
+
(u) =>
|
|
2143
|
+
u.statusCode == null ||
|
|
2144
|
+
u.statusCode === 0 ||
|
|
2145
|
+
(loggedIn && u.statusCode === 209002),
|
|
2146
|
+
);
|
|
2147
|
+
let hasVideo = pending.filter((u) => u.videoCount > 0);
|
|
2148
|
+
const next = pickCandidate(hasVideo) || pickCandidate(pending);
|
|
2022
2149
|
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2150
|
+
if (next) {
|
|
2151
|
+
next.status = "processing";
|
|
2152
|
+
markStatsDirty();
|
|
2153
|
+
next.claimedAt = now;
|
|
2154
|
+
next.claimedBy = userId;
|
|
2155
|
+
save();
|
|
2156
|
+
return {
|
|
2157
|
+
uniqueId: next.uniqueId,
|
|
2158
|
+
nickname: next.nickname,
|
|
2159
|
+
claimedAt: next.claimedAt,
|
|
2160
|
+
claimedBy: userId,
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
return null;
|
|
2027
2164
|
}
|
|
2028
2165
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
let next = candidates.find((u) => u.pinned);
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2032
2168
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2169
|
+
function debugClaimNextJob(
|
|
2170
|
+
userId,
|
|
2171
|
+
expireMs = 5 * 60 * 1000,
|
|
2172
|
+
locations = null,
|
|
2173
|
+
loggedIn = true,
|
|
2174
|
+
) {
|
|
2175
|
+
if (db) {
|
|
2176
|
+
const now = Date.now();
|
|
2177
|
+
const info = {
|
|
2178
|
+
path: "db",
|
|
2179
|
+
userId,
|
|
2180
|
+
expireMs,
|
|
2181
|
+
loggedIn,
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
const ongoingRow = db
|
|
2185
|
+
.prepare(
|
|
2186
|
+
`
|
|
2187
|
+
SELECT *
|
|
2188
|
+
FROM jobs
|
|
2189
|
+
WHERE status = 'processing'
|
|
2190
|
+
AND claimed_by = ?
|
|
2191
|
+
AND claimed_at IS NOT NULL
|
|
2192
|
+
AND ? - claimed_at < ?
|
|
2193
|
+
ORDER BY claimed_at DESC
|
|
2194
|
+
LIMIT 1
|
|
2195
|
+
`,
|
|
2196
|
+
)
|
|
2197
|
+
.get(userId, now, expireMs);
|
|
2198
|
+
info.ongoing = ongoingRow
|
|
2199
|
+
? {
|
|
2200
|
+
uniqueId: ongoingRow.unique_id,
|
|
2201
|
+
claimedBy: ongoingRow.claimed_by,
|
|
2202
|
+
claimedAt: ongoingRow.claimed_at,
|
|
2203
|
+
}
|
|
2204
|
+
: null;
|
|
2205
|
+
|
|
2206
|
+
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
2207
|
+
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
2208
|
+
const normalizedLocations = Array.isArray(locations)
|
|
2209
|
+
? locations
|
|
2210
|
+
.map((loc) => String(loc).trim().toUpperCase())
|
|
2211
|
+
.filter(Boolean)
|
|
2212
|
+
: [];
|
|
2213
|
+
|
|
2214
|
+
function getLocationGroups() {
|
|
2215
|
+
const selected = normalizedLocations.length
|
|
2216
|
+
? normalizedLocations
|
|
2217
|
+
: null;
|
|
2218
|
+
const tier1List = selected
|
|
2219
|
+
? selected.filter((loc) => tier1.has(loc))
|
|
2220
|
+
: [...tier1];
|
|
2221
|
+
const tier2List = selected
|
|
2222
|
+
? selected.filter((loc) => tier2.has(loc))
|
|
2223
|
+
: [...tier2];
|
|
2224
|
+
const otherList = selected
|
|
2225
|
+
? selected.filter((loc) => !tier1.has(loc) && !tier2.has(loc))
|
|
2226
|
+
: null;
|
|
2227
|
+
const groups = [];
|
|
2228
|
+
if (tier1List.length > 0)
|
|
2229
|
+
groups.push({ type: "include", values: tier1List });
|
|
2230
|
+
if (tier2List.length > 0)
|
|
2231
|
+
groups.push({ type: "include", values: tier2List });
|
|
2232
|
+
if (selected) {
|
|
2233
|
+
if (otherList.length > 0)
|
|
2234
|
+
groups.push({ type: "include", values: otherList });
|
|
2235
|
+
} else {
|
|
2236
|
+
groups.push({ type: "exclude", values: [...tier1, ...tier2] });
|
|
2045
2237
|
}
|
|
2238
|
+
return groups;
|
|
2046
2239
|
}
|
|
2047
2240
|
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2241
|
+
const locationGroups = getLocationGroups();
|
|
2242
|
+
info.locationGroups = locationGroups;
|
|
2243
|
+
|
|
2244
|
+
function applyLocationGroup(where, args, group) {
|
|
2245
|
+
if (!group) return;
|
|
2246
|
+
if (group.type === "include") {
|
|
2247
|
+
where.push(
|
|
2248
|
+
`UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")})`,
|
|
2249
|
+
);
|
|
2250
|
+
args.push(...group.values);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
where.push(
|
|
2254
|
+
`UPPER(COALESCE(guessed_location, '')) NOT IN (${group.values.map(() => "?").join(", ")})`,
|
|
2051
2255
|
);
|
|
2052
|
-
|
|
2053
|
-
next = seed[0] || null;
|
|
2256
|
+
args.push(...group.values);
|
|
2054
2257
|
}
|
|
2055
2258
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2259
|
+
function queryPendingOne({ requireVideo, group, filters = [] }) {
|
|
2260
|
+
const where = ["status = 'pending'"];
|
|
2261
|
+
const args = [];
|
|
2262
|
+
if (!loggedIn) {
|
|
2263
|
+
where.push("COALESCE(tt_seller, 0) != 1");
|
|
2264
|
+
where.push("(status_code IS NULL OR status_code = 0)");
|
|
2265
|
+
} else {
|
|
2266
|
+
where.push(
|
|
2267
|
+
"(status_code IS NULL OR status_code = 0 OR status_code = 209002)",
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
if (requireVideo) {
|
|
2271
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
2272
|
+
}
|
|
2273
|
+
applyLocationGroup(where, args, group);
|
|
2274
|
+
for (const filter of filters) {
|
|
2275
|
+
where.push(filter);
|
|
2276
|
+
}
|
|
2277
|
+
const sql = `
|
|
2278
|
+
SELECT *
|
|
2279
|
+
FROM jobs
|
|
2280
|
+
WHERE ${where.join(" AND ")}
|
|
2281
|
+
ORDER BY follower_count DESC, created_at ASC, unique_id ASC
|
|
2282
|
+
LIMIT 1
|
|
2283
|
+
`;
|
|
2284
|
+
const row = db.prepare(sql).get(...args);
|
|
2285
|
+
return { row, sql, args };
|
|
2063
2286
|
}
|
|
2064
2287
|
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
(
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2288
|
+
function queryPendingByGroup({ requireVideo, group, filters = [] }) {
|
|
2289
|
+
if (group?.type === "include" && group.values.length > 1) {
|
|
2290
|
+
for (const location of group.values) {
|
|
2291
|
+
const ret = queryPendingOne({
|
|
2292
|
+
requireVideo,
|
|
2293
|
+
group: { type: "include", values: [location] },
|
|
2294
|
+
filters,
|
|
2295
|
+
});
|
|
2296
|
+
if (ret.row) return ret;
|
|
2297
|
+
}
|
|
2298
|
+
return { row: null, sql: null, args: [] };
|
|
2299
|
+
}
|
|
2300
|
+
return queryPendingOne({ requireVideo, group, filters });
|
|
2073
2301
|
}
|
|
2074
2302
|
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2303
|
+
function findPinnedPending(requireVideo) {
|
|
2304
|
+
const where = ["status = 'pending'", "COALESCE(pinned, 0) = 1"];
|
|
2305
|
+
const args = [];
|
|
2306
|
+
if (!loggedIn) {
|
|
2307
|
+
where.push("COALESCE(tt_seller, 0) != 1");
|
|
2308
|
+
}
|
|
2309
|
+
if (requireVideo) {
|
|
2310
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
2311
|
+
}
|
|
2312
|
+
if (normalizedLocations.length > 0) {
|
|
2313
|
+
where.push(
|
|
2314
|
+
`UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`,
|
|
2315
|
+
);
|
|
2316
|
+
args.push(...normalizedLocations);
|
|
2317
|
+
}
|
|
2318
|
+
const sql = `
|
|
2319
|
+
SELECT *
|
|
2320
|
+
FROM jobs
|
|
2321
|
+
WHERE ${where.join(" AND ")}
|
|
2322
|
+
ORDER BY created_at ASC, unique_id ASC
|
|
2323
|
+
LIMIT 1
|
|
2324
|
+
`;
|
|
2325
|
+
const row = db.prepare(sql).get(...args);
|
|
2326
|
+
return { row, sql, args };
|
|
2078
2327
|
}
|
|
2079
2328
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2329
|
+
const expiredSql = `
|
|
2330
|
+
SELECT *
|
|
2331
|
+
FROM jobs
|
|
2332
|
+
WHERE status = 'processing'
|
|
2333
|
+
AND claimed_at IS NOT NULL
|
|
2334
|
+
AND ? - claimed_at > ?
|
|
2335
|
+
ORDER BY claimed_at ASC
|
|
2336
|
+
LIMIT 1
|
|
2337
|
+
`;
|
|
2338
|
+
const expiredRow = db.prepare(expiredSql).get(now, expireMs);
|
|
2339
|
+
info.expired = expiredRow
|
|
2340
|
+
? {
|
|
2341
|
+
uniqueId: expiredRow.unique_id,
|
|
2342
|
+
claimedBy: expiredRow.claimed_by,
|
|
2343
|
+
claimedAt: expiredRow.claimed_at,
|
|
2344
|
+
diffMs: now - expiredRow.claimed_at,
|
|
2345
|
+
}
|
|
2346
|
+
: null;
|
|
2082
2347
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
const next = pickCandidate(hasVideo) || pickCandidate(pending);
|
|
2348
|
+
info.requireVideoPasses = [];
|
|
2349
|
+
for (const requireVideo of [true, false]) {
|
|
2350
|
+
const pass = { requireVideo };
|
|
2351
|
+
const pinned = findPinnedPending(requireVideo);
|
|
2352
|
+
pass.pinned = pinned.row
|
|
2353
|
+
? {
|
|
2354
|
+
uniqueId: pinned.row.unique_id,
|
|
2355
|
+
sql: pinned.sql,
|
|
2356
|
+
args: pinned.args,
|
|
2357
|
+
}
|
|
2358
|
+
: null;
|
|
2095
2359
|
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2360
|
+
if (!pass.pinned) {
|
|
2361
|
+
for (const group of locationGroups) {
|
|
2362
|
+
const seed = queryPendingByGroup({
|
|
2363
|
+
requireVideo,
|
|
2364
|
+
group,
|
|
2365
|
+
filters: [
|
|
2366
|
+
"COALESCE(pinned, 0) = 0",
|
|
2367
|
+
`instr(COALESCE(sources, ''), '"seed"') > 0`,
|
|
2368
|
+
],
|
|
2369
|
+
});
|
|
2370
|
+
if (seed.row) {
|
|
2371
|
+
pass.seed = {
|
|
2372
|
+
uniqueId: seed.row.unique_id,
|
|
2373
|
+
group,
|
|
2374
|
+
sql: seed.sql,
|
|
2375
|
+
args: seed.args,
|
|
2376
|
+
};
|
|
2377
|
+
break;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
if (!pass.pinned && !pass.seed && loggedIn) {
|
|
2383
|
+
for (const group of locationGroups) {
|
|
2384
|
+
const seller = queryPendingByGroup({
|
|
2385
|
+
requireVideo,
|
|
2386
|
+
group,
|
|
2387
|
+
filters: [
|
|
2388
|
+
"COALESCE(pinned, 0) = 0",
|
|
2389
|
+
"tt_seller = 1",
|
|
2390
|
+
"verified = 0",
|
|
2391
|
+
],
|
|
2392
|
+
});
|
|
2393
|
+
if (seller.row) {
|
|
2394
|
+
pass.seller = {
|
|
2395
|
+
uniqueId: seller.row.unique_id,
|
|
2396
|
+
group,
|
|
2397
|
+
sql: seller.sql,
|
|
2398
|
+
args: seller.args,
|
|
2399
|
+
};
|
|
2400
|
+
break;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
if (!pass.pinned && !pass.seed && !pass.seller) {
|
|
2406
|
+
for (const group of locationGroups) {
|
|
2407
|
+
const follow = queryPendingByGroup({
|
|
2408
|
+
requireVideo,
|
|
2409
|
+
group,
|
|
2410
|
+
filters: [
|
|
2411
|
+
"COALESCE(pinned, 0) = 0",
|
|
2412
|
+
`(
|
|
2413
|
+
instr(COALESCE(sources, ''), '"following"') > 0
|
|
2414
|
+
OR instr(COALESCE(sources, ''), '"follower"') > 0
|
|
2415
|
+
)`,
|
|
2416
|
+
],
|
|
2417
|
+
});
|
|
2418
|
+
if (follow.row) {
|
|
2419
|
+
pass.follow = {
|
|
2420
|
+
uniqueId: follow.row.unique_id,
|
|
2421
|
+
group,
|
|
2422
|
+
sql: follow.sql,
|
|
2423
|
+
args: follow.args,
|
|
2424
|
+
};
|
|
2425
|
+
break;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
if (!pass.pinned && !pass.seed && !pass.seller && !pass.follow) {
|
|
2431
|
+
for (const group of locationGroups) {
|
|
2432
|
+
const other = queryPendingByGroup({
|
|
2433
|
+
requireVideo,
|
|
2434
|
+
group,
|
|
2435
|
+
filters: ["COALESCE(pinned, 0) = 0"],
|
|
2436
|
+
});
|
|
2437
|
+
if (other.row) {
|
|
2438
|
+
pass.other = {
|
|
2439
|
+
uniqueId: other.row.unique_id,
|
|
2440
|
+
group,
|
|
2441
|
+
sql: other.sql,
|
|
2442
|
+
args: other.args,
|
|
2443
|
+
};
|
|
2444
|
+
break;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
info.requireVideoPasses.push(pass);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
return info;
|
|
2108
2453
|
}
|
|
2109
|
-
|
|
2454
|
+
|
|
2455
|
+
return {
|
|
2456
|
+
path: "memory",
|
|
2457
|
+
userId,
|
|
2458
|
+
expireMs,
|
|
2459
|
+
loggedIn,
|
|
2460
|
+
totalUsers: data.length,
|
|
2461
|
+
processingUsers: data.filter((u) => u.status === "processing").length,
|
|
2462
|
+
pendingUsers: data.filter((u) => u.status === "pending").length,
|
|
2463
|
+
};
|
|
2110
2464
|
}
|
|
2111
2465
|
|
|
2112
2466
|
function processDiscoveredUsers(result) {
|
|
@@ -2215,6 +2569,7 @@ export function createStore(filePath) {
|
|
|
2215
2569
|
}
|
|
2216
2570
|
}
|
|
2217
2571
|
}
|
|
2572
|
+
user.restricted = true;
|
|
2218
2573
|
user.processed = true;
|
|
2219
2574
|
user.processedAt = Date.now();
|
|
2220
2575
|
user.sources = [...new Set([...(user.sources || []), "restricted"])];
|
|
@@ -2644,7 +2999,9 @@ export function createStore(filePath) {
|
|
|
2644
2999
|
u.ttSeller === null || u.ttSeller === undefined || u.ttSeller === "";
|
|
2645
3000
|
if (!ttSellerEmpty) return false;
|
|
2646
3001
|
if (
|
|
2647
|
-
updateCount === null ||
|
|
3002
|
+
updateCount === null ||
|
|
3003
|
+
updateCount === undefined ||
|
|
3004
|
+
updateCount <= 0
|
|
2648
3005
|
) {
|
|
2649
3006
|
if (hasCountryFilter) {
|
|
2650
3007
|
const loc = (u.guessedLocation || "").toUpperCase();
|
|
@@ -2686,7 +3043,19 @@ export function createStore(filePath) {
|
|
|
2686
3043
|
function batchUpdateUserInfo(updates) {
|
|
2687
3044
|
if (db) {
|
|
2688
3045
|
const txn = db.transaction((items) =>
|
|
2689
|
-
items.map((item) =>
|
|
3046
|
+
items.map((item) => {
|
|
3047
|
+
// 处理 { error: true, statusCode: xxx } 的情况
|
|
3048
|
+
const info = item.info;
|
|
3049
|
+
if (info && info.error && info.statusCode !== undefined) {
|
|
3050
|
+
// 只更新 status_code,不更新其他字段
|
|
3051
|
+
return updateJobInfo(
|
|
3052
|
+
item.uniqueId,
|
|
3053
|
+
{ statusCode: info.statusCode },
|
|
3054
|
+
true,
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
return updateJobInfo(item.uniqueId, info, true);
|
|
3058
|
+
}),
|
|
2690
3059
|
);
|
|
2691
3060
|
return txn(updates).map((result, index) =>
|
|
2692
3061
|
result.error
|
|
@@ -2706,15 +3075,20 @@ export function createStore(filePath) {
|
|
|
2706
3075
|
results.push({ uniqueId: item.uniqueId, error: "user not found" });
|
|
2707
3076
|
continue;
|
|
2708
3077
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
3078
|
+
const info = item.info;
|
|
3079
|
+
if (info && info.error && info.statusCode !== undefined) {
|
|
3080
|
+
// 只更新 status_code
|
|
3081
|
+
user.statusCode = info.statusCode;
|
|
3082
|
+
} else {
|
|
3083
|
+
for (const key of Object.keys(info)) {
|
|
3084
|
+
if (key === "uniqueId" || key === "sources" || key === "error")
|
|
3085
|
+
continue;
|
|
2712
3086
|
if (
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
3087
|
+
info[key] !== undefined &&
|
|
3088
|
+
info[key] !== null &&
|
|
3089
|
+
info[key] !== ""
|
|
2716
3090
|
) {
|
|
2717
|
-
user[key] =
|
|
3091
|
+
user[key] = info[key];
|
|
2718
3092
|
}
|
|
2719
3093
|
}
|
|
2720
3094
|
}
|
|
@@ -2974,7 +3348,9 @@ export function createStore(filePath) {
|
|
|
2974
3348
|
getVideoCount,
|
|
2975
3349
|
getPendingCommentTasks,
|
|
2976
3350
|
commitCommentTask,
|
|
3351
|
+
debugClaimNextJob,
|
|
2977
3352
|
stopBackup,
|
|
3353
|
+
rawQuery,
|
|
2978
3354
|
data,
|
|
2979
3355
|
};
|
|
2980
3356
|
}
|