tt-help-cli-ycl 1.3.62 → 1.3.64
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/scripts/run-explore.bat +1 -1
- package/src/cli/explore.js +8 -4
- package/src/cli/refresh.js +437 -163
- package/src/lib/args.js +54 -8
- package/src/lib/browser/cdp.js +11 -5
- package/src/lib/browser/page.js +41 -0
- package/src/lib/constants.js +31 -9
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/data-store.js +145 -9
- package/src/watch/public/app.js +296 -42
- package/src/watch/public/index.html +63 -0
- package/src/watch/public/style.css +80 -3
- package/src/watch/server.js +91 -2
- package/src/scraper/refresh-core.js +0 -213
package/src/lib/args.js
CHANGED
|
@@ -180,6 +180,7 @@ function parseExploreArgs(args) {
|
|
|
180
180
|
let explorePortCount = null;
|
|
181
181
|
let exploreUserId = null;
|
|
182
182
|
let exploreMaxVideos = 16;
|
|
183
|
+
let exploreProxy = null;
|
|
183
184
|
|
|
184
185
|
const positional = [];
|
|
185
186
|
const PRESETS = ["fast", "normal", "slow", "stealth"];
|
|
@@ -218,6 +219,8 @@ function parseExploreArgs(args) {
|
|
|
218
219
|
exploreUserId = args[++i];
|
|
219
220
|
} else if (arg === "--max-videos") {
|
|
220
221
|
exploreMaxVideos = parseInt(args[++i]) || 16;
|
|
222
|
+
} else if (arg === "--proxy") {
|
|
223
|
+
exploreProxy = args[++i];
|
|
221
224
|
} else {
|
|
222
225
|
positional.push(arg);
|
|
223
226
|
}
|
|
@@ -258,6 +261,7 @@ function parseExploreArgs(args) {
|
|
|
258
261
|
explorePortCount,
|
|
259
262
|
exploreUserId,
|
|
260
263
|
exploreMaxVideos,
|
|
264
|
+
exploreProxy,
|
|
261
265
|
urls: [],
|
|
262
266
|
outputFormat: "json",
|
|
263
267
|
exploreCount: 0,
|
|
@@ -439,39 +443,77 @@ function parseDbImportArgs(args) {
|
|
|
439
443
|
function parseRefreshArgs(args) {
|
|
440
444
|
let serverUrl = defaultServer;
|
|
441
445
|
let explorePreset = "normal";
|
|
442
|
-
let
|
|
443
|
-
let
|
|
446
|
+
let exploreInterval = 30;
|
|
447
|
+
let exploreEnableFollow = true;
|
|
448
|
+
let exploreMaxFollowing = 100;
|
|
449
|
+
let exploreMaxFollowers = 100;
|
|
450
|
+
let exploreLocation = DEFAULT_TARGET_LOCATIONS_CSV;
|
|
451
|
+
let exploreMaxUsers = 0;
|
|
444
452
|
let explorePort = null;
|
|
453
|
+
let exploreBasePort = null;
|
|
454
|
+
let explorePortCount = null;
|
|
445
455
|
let exploreProfile = null;
|
|
446
456
|
let exploreUserId = null;
|
|
457
|
+
let exploreMaxVideos = 16;
|
|
458
|
+
let exploreRedoMaxAge = 0;
|
|
459
|
+
let exploreProxy = null;
|
|
447
460
|
|
|
448
461
|
for (let i = 0; i < args.length; i++) {
|
|
449
462
|
const arg = args[i];
|
|
450
463
|
if (arg === "--server") {
|
|
451
464
|
serverUrl = args[++i];
|
|
452
|
-
} else if (arg === "--comments") {
|
|
453
|
-
exploreMaxComments = parseInt(args[++i]) || 10;
|
|
454
|
-
} else if (arg === "--guess") {
|
|
455
|
-
exploreMaxGuess = parseInt(args[++i]) || 0;
|
|
456
465
|
} else if (arg === "--preset") {
|
|
457
466
|
explorePreset = args[++i];
|
|
467
|
+
} else if (arg === "-i" || arg === "--interval") {
|
|
468
|
+
exploreInterval = parseInt(args[++i], 10) || 30;
|
|
458
469
|
} else if (arg === "--port") {
|
|
459
470
|
explorePort = parseInt(args[++i]) || 9222;
|
|
471
|
+
} else if (arg === "--base-port") {
|
|
472
|
+
exploreBasePort = parseInt(args[++i]) || 9222;
|
|
473
|
+
} else if (arg === "--port-count") {
|
|
474
|
+
explorePortCount = parseInt(args[++i]) || 10;
|
|
460
475
|
} else if (arg === "--profile") {
|
|
461
476
|
exploreProfile = args[++i];
|
|
462
477
|
} else if (arg === "--user-id") {
|
|
463
478
|
exploreUserId = args[++i];
|
|
479
|
+
} else if (arg === "--max-videos") {
|
|
480
|
+
exploreMaxVideos = parseInt(args[++i]) || 16;
|
|
481
|
+
} else if (arg === "--max-following") {
|
|
482
|
+
exploreMaxFollowing = parseInt(args[++i]) || 100;
|
|
483
|
+
} else if (arg === "--max-followers") {
|
|
484
|
+
exploreMaxFollowers = parseInt(args[++i]) || 100;
|
|
485
|
+
} else if (arg === "--max-age") {
|
|
486
|
+
exploreRedoMaxAge = parseInt(args[++i]) || 43200;
|
|
487
|
+
} else if (arg === "--proxy") {
|
|
488
|
+
exploreProxy = args[++i];
|
|
489
|
+
} else if (arg === "--location") {
|
|
490
|
+
exploreLocation = args[++i];
|
|
491
|
+
} else if (arg === "--enable-follow") {
|
|
492
|
+
exploreEnableFollow = true;
|
|
493
|
+
} else if (arg === "--disable-follow") {
|
|
494
|
+
exploreEnableFollow = false;
|
|
495
|
+
} else if (arg === "--max-users") {
|
|
496
|
+
exploreMaxUsers = parseInt(args[++i]) || 0;
|
|
464
497
|
}
|
|
465
498
|
}
|
|
466
499
|
|
|
467
500
|
return {
|
|
468
501
|
subcommand: "refresh",
|
|
469
502
|
explorePreset,
|
|
470
|
-
|
|
471
|
-
|
|
503
|
+
exploreInterval,
|
|
504
|
+
exploreEnableFollow,
|
|
505
|
+
exploreMaxFollowing,
|
|
506
|
+
exploreMaxFollowers,
|
|
507
|
+
exploreLocation,
|
|
508
|
+
exploreMaxUsers,
|
|
472
509
|
explorePort,
|
|
510
|
+
exploreBasePort,
|
|
511
|
+
explorePortCount,
|
|
473
512
|
exploreProfile,
|
|
474
513
|
exploreUserId,
|
|
514
|
+
exploreMaxVideos,
|
|
515
|
+
exploreRedoMaxAge,
|
|
516
|
+
exploreProxy,
|
|
475
517
|
serverUrl,
|
|
476
518
|
urls: [],
|
|
477
519
|
outputFormat: "json",
|
|
@@ -721,6 +763,10 @@ export function parseArgs() {
|
|
|
721
763
|
return parseDbImportArgs(args.slice(1));
|
|
722
764
|
}
|
|
723
765
|
|
|
766
|
+
if (args.length > 0 && args[0] === "refresh") {
|
|
767
|
+
return parseRefreshArgs(args.slice(1));
|
|
768
|
+
}
|
|
769
|
+
|
|
724
770
|
const urls = [];
|
|
725
771
|
let inputFile = null;
|
|
726
772
|
let outputFile = null;
|
package/src/lib/browser/cdp.js
CHANGED
|
@@ -143,7 +143,7 @@ function killEdgeProcesses(targetDir) {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function launchEdgeWithCDP(port, userDataDir) {
|
|
146
|
+
function launchEdgeWithCDP(port, userDataDir, proxyServer) {
|
|
147
147
|
return new Promise((resolve, reject) => {
|
|
148
148
|
const platform = os.platform();
|
|
149
149
|
const edgePath = getEdgePath();
|
|
@@ -162,14 +162,19 @@ function launchEdgeWithCDP(port, userDataDir) {
|
|
|
162
162
|
"--disable-breakpad",
|
|
163
163
|
"--disable-background-networking",
|
|
164
164
|
"--disable-sync",
|
|
165
|
-
]
|
|
165
|
+
];
|
|
166
|
+
if (proxyServer) {
|
|
167
|
+
extraArgs.push(`--proxy-server="${proxyServer}"`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const argsStr = extraArgs.join(" ");
|
|
166
171
|
|
|
167
172
|
if (platform === "darwin") {
|
|
168
|
-
command = `open -a ${edgePath} --new --args ${
|
|
173
|
+
command = `open -a ${edgePath} --new --args ${argsStr}`;
|
|
169
174
|
} else if (platform === "win32") {
|
|
170
|
-
command = `start msedge ${
|
|
175
|
+
command = `start msedge ${argsStr}`;
|
|
171
176
|
} else {
|
|
172
|
-
command = `msedge ${
|
|
177
|
+
command = `msedge ${argsStr} &`;
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
exec(command, (err) => {
|
|
@@ -194,6 +199,7 @@ export { killEdgeProcesses };
|
|
|
194
199
|
export async function ensureBrowserReady(options = {}) {
|
|
195
200
|
const port = options.port || DEFAULT_CDP_PORT;
|
|
196
201
|
const userDataDir = options.userDataDir || DEFAULT_USER_DATA_DIR;
|
|
202
|
+
const proxyServer = options.proxyServer || null;
|
|
197
203
|
const isCustom = port !== DEFAULT_CDP_PORT || !!options.userDataDir;
|
|
198
204
|
|
|
199
205
|
const isReady = await checkCDPPort(port);
|
package/src/lib/browser/page.js
CHANGED
|
@@ -68,6 +68,8 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
|
68
68
|
|
|
69
69
|
const DOM_CHECK_TIMEOUT = 20000; // 单次 DOM 检测超时 20 秒
|
|
70
70
|
const DOM_CHECK_RETRIES = 3; // DOM 检测最大重试次数
|
|
71
|
+
const SAFE_CHECK_ROUNDS = 2; // 安全登录检测轮数(首次检测 + 额外1轮确认)
|
|
72
|
+
const SAFE_CHECK_INTERVAL = 5000; // 安全登录检测每轮间隔 5 秒
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
75
|
* 判断登录状态:Cookie 为主,DOM 验真为辅。
|
|
@@ -102,6 +104,45 @@ export async function isLoggedIn(page) {
|
|
|
102
104
|
return true;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 安全登录检测:发现未登录时多检测几轮,避免因 TikTok 页面渲染延迟导致误判。
|
|
109
|
+
* - 首次检测为已登录 → 直接返回 true
|
|
110
|
+
* - 首次检测为未登录 → 等待后重新导航并检测,连续 SAFE_CHECK_ROUNDS 轮都为未登录才确认
|
|
111
|
+
* - 任何一轮检测为已登录 → 立即返回 true
|
|
112
|
+
*/
|
|
113
|
+
export async function safeCheckLogin(page) {
|
|
114
|
+
// 第一轮检测
|
|
115
|
+
let loggedIn = await isLoggedIn(page);
|
|
116
|
+
if (loggedIn) {
|
|
117
|
+
console.error(`[安全登录检测] 第 1 轮: 已登录 ✓`);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
console.error(
|
|
121
|
+
`[安全登录检测] 第 1 轮: 未登录 ✗,等待 ${SAFE_CHECK_INTERVAL / 1000}s 后重检...`,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 后续轮次:等待后重新导航到 TikTok 页面再检测
|
|
125
|
+
for (let round = 2; round <= SAFE_CHECK_ROUNDS; round++) {
|
|
126
|
+
await new Promise((r) => setTimeout(r, SAFE_CHECK_INTERVAL));
|
|
127
|
+
// 重新导航到 TikTok 页面,确保页面状态刷新
|
|
128
|
+
await page.goto("https://www.tiktok.com", {
|
|
129
|
+
waitUntil: "domcontentloaded",
|
|
130
|
+
});
|
|
131
|
+
loggedIn = await isLoggedIn(page);
|
|
132
|
+
if (loggedIn) {
|
|
133
|
+
console.error(`[安全登录检测] 第 ${round} 轮: 已登录 ✓`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
console.error(`[安全登录检测] 第 ${round} 轮: 未登录 ✗`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 连续 SAFE_CHECK_ROUNDS 轮都为未登录
|
|
140
|
+
console.error(
|
|
141
|
+
`[安全登录检测] 连续 ${SAFE_CHECK_ROUNDS} 轮均为未登录,确认为未登录`,
|
|
142
|
+
);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
105
146
|
/**
|
|
106
147
|
* 通过 DOM 元素判断登录状态(验真方案)
|
|
107
148
|
* 使用 locator API + state: 'attached' 来避免 CDP 连接下 waitForSelector 的可见性问题
|
package/src/lib/constants.js
CHANGED
|
@@ -131,12 +131,33 @@ const HELP_TEXT = [
|
|
|
131
131
|
" --disable-follow 禁用关注/粉丝提取",
|
|
132
132
|
" --max-following <数量> 最大获取关注数,默认 50",
|
|
133
133
|
" --max-followers <数量> 最大获取粉丝数,默认 50",
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
" --max-users <数量> 最大处理用户数,默认无限制",
|
|
135
|
+
" -i, --interval <秒数> 无任务时轮询间隔,默认 10 秒",
|
|
136
|
+
" --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
|
|
137
|
+
" --base-port <端口号> 起始端口,默认 9222",
|
|
138
|
+
" --port-count <数量> 端口数量(账户数),默认 10",
|
|
139
|
+
" --user-id <编号> 客户端编号(设备ID),默认自动生成",
|
|
140
|
+
" --proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080)",
|
|
141
|
+
"",
|
|
142
|
+
" tt-help refresh [选项]",
|
|
143
|
+
" 对目标商家用户进行轮回刷新,重新采集视频 + 关注 + 粉丝",
|
|
144
|
+
" 筛选条件: tt_seller=1, verified=0, 目标国家",
|
|
145
|
+
" 选项:",
|
|
146
|
+
" --server <URL> 服务端地址,默认 http://127.0.0.1:3001",
|
|
147
|
+
` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
|
|
148
|
+
" --max-videos <数量> 每用户最大视频数,默认 16",
|
|
149
|
+
" --enable-follow 启用关注/粉丝提取(默认启用)",
|
|
150
|
+
" --disable-follow 禁用关注/粉丝提取",
|
|
151
|
+
" --max-following <数量> 最大获取关注数,默认 100",
|
|
152
|
+
" --max-followers <数量> 最大获取粉丝数,默认 100",
|
|
153
|
+
" --max-users <数量> 最大处理用户数,默认无限制",
|
|
154
|
+
" -i, --interval <秒数> 无任务时轮询间隔,默认 30 秒",
|
|
155
|
+
" --max-age <秒数> 最小刷新间隔,默认 43200(12小时)",
|
|
156
|
+
" --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
|
|
157
|
+
" --base-port <端口号> 起始端口,默认 9222",
|
|
158
|
+
" --port-count <数量> 端口数量(账户数),默认 10",
|
|
159
|
+
" --user-id <编号> 客户端编号(设备ID),默认自动生成",
|
|
160
|
+
" --proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080)",
|
|
140
161
|
"",
|
|
141
162
|
" tt-help info <URL> [URL2 ...] [--onlyvideo]",
|
|
142
163
|
" 获取用户/视频信息,支持多个 URL",
|
|
@@ -196,9 +217,10 @@ const HELP_TEXT = [
|
|
|
196
217
|
" -h, --help 显示帮助",
|
|
197
218
|
" --version 显示版本号",
|
|
198
219
|
"",
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
" 示例: tt-help info https://www.tiktok.com/@nike https://www.tiktok.com/@adidas",
|
|
221
|
+
" tt-help explore qiqi23280 fast --location ES --max-comments 50",
|
|
222
|
+
" tt-help refresh --server http://127.0.0.1:3001 --port 9222",
|
|
223
|
+
" tt-help config set server http://127.0.0.1:3001",
|
|
202
224
|
" tt-help attach -p 5 -i 10",
|
|
203
225
|
" tt-help watch -o data/result.db",
|
|
204
226
|
" tt-help videostats data/result.db -p 3",
|
package/src/main.js
CHANGED
|
@@ -10,6 +10,7 @@ import { handleComments } from "./cli/comments.js";
|
|
|
10
10
|
import { handleVideoStats } from "./cli/videostats.js";
|
|
11
11
|
import { handleDbImport } from "./cli/db-import.js";
|
|
12
12
|
import { handleWebserver } from "./cli/webserver.js";
|
|
13
|
+
import { handleRefresh } from "./cli/refresh.js";
|
|
13
14
|
|
|
14
15
|
async function main() {
|
|
15
16
|
const parsed = parseArgs();
|
|
@@ -33,6 +34,8 @@ async function main() {
|
|
|
33
34
|
return handleVideoStats(parsed);
|
|
34
35
|
case "db-import":
|
|
35
36
|
return handleDbImport(parsed);
|
|
37
|
+
case "refresh":
|
|
38
|
+
return handleRefresh(parsed);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const {
|
package/src/npm-main.js
CHANGED
|
@@ -6,6 +6,7 @@ import { handleAttach } from "./cli/attach.js";
|
|
|
6
6
|
import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
|
|
7
7
|
import { handleOpen } from "./cli/open.js";
|
|
8
8
|
import { handleComments } from "./cli/comments.js";
|
|
9
|
+
import { handleRefresh } from "./cli/refresh.js";
|
|
9
10
|
|
|
10
11
|
function exitUnsupportedCommand(command) {
|
|
11
12
|
console.error(
|
|
@@ -33,6 +34,8 @@ async function main() {
|
|
|
33
34
|
return handleOpen(parsed);
|
|
34
35
|
case "comments":
|
|
35
36
|
return handleComments(parsed);
|
|
37
|
+
case "refresh":
|
|
38
|
+
return handleRefresh(parsed);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const {
|
package/src/watch/data-store.js
CHANGED
|
@@ -775,6 +775,53 @@ function restoreAttachStuckByCountry(country) {
|
|
|
775
775
|
return { restored: count, country: normalizedCountry };
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
function resetPendingByCountry(country) {
|
|
779
|
+
if (!db) {
|
|
780
|
+
return { reset: 0, country, error: "db not ready" };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const normalizedCountry = String(country == null ? "未知" : country).trim();
|
|
784
|
+
if (!normalizedCountry) {
|
|
785
|
+
return {
|
|
786
|
+
reset: 0,
|
|
787
|
+
country: normalizedCountry,
|
|
788
|
+
error: "country is required",
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const whereSql = `
|
|
793
|
+
status = 'pending'
|
|
794
|
+
AND COALESCE(guessed_location, '未知') = ?
|
|
795
|
+
`;
|
|
796
|
+
const count =
|
|
797
|
+
db
|
|
798
|
+
.prepare(
|
|
799
|
+
`
|
|
800
|
+
SELECT COUNT(*) as c
|
|
801
|
+
FROM jobs
|
|
802
|
+
WHERE ${whereSql}
|
|
803
|
+
`,
|
|
804
|
+
)
|
|
805
|
+
.get(normalizedCountry)?.c || 0;
|
|
806
|
+
|
|
807
|
+
if (!count) {
|
|
808
|
+
return { reset: 0, country: normalizedCountry };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
db.prepare(
|
|
812
|
+
`
|
|
813
|
+
UPDATE jobs
|
|
814
|
+
SET user_update_count = 0,
|
|
815
|
+
updated_at = ?,
|
|
816
|
+
claimed_by = NULL,
|
|
817
|
+
claimed_at = NULL
|
|
818
|
+
WHERE ${whereSql}
|
|
819
|
+
`,
|
|
820
|
+
).run(Date.now(), normalizedCountry);
|
|
821
|
+
|
|
822
|
+
return { reset: count, country: normalizedCountry };
|
|
823
|
+
}
|
|
824
|
+
|
|
778
825
|
function getRawByCountryFromDb() {
|
|
779
826
|
if (!db) return [];
|
|
780
827
|
|
|
@@ -1077,7 +1124,7 @@ function restoreRawJobById(uniqueId) {
|
|
|
1077
1124
|
return { restored: 1, uniqueId: safeId };
|
|
1078
1125
|
}
|
|
1079
1126
|
|
|
1080
|
-
function restoreRawJobsByFilter({ search, location }) {
|
|
1127
|
+
function restoreRawJobsByFilter({ search, location, hasVideo, hasFollower }) {
|
|
1081
1128
|
if (!db) {
|
|
1082
1129
|
return { restored: 0, error: "db not ready" };
|
|
1083
1130
|
}
|
|
@@ -1098,6 +1145,14 @@ function restoreRawJobsByFilter({ search, location }) {
|
|
|
1098
1145
|
args.push(location);
|
|
1099
1146
|
}
|
|
1100
1147
|
|
|
1148
|
+
if (hasVideo) {
|
|
1149
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (hasFollower) {
|
|
1153
|
+
where.push("COALESCE(follower_count, 0) > 0");
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1101
1156
|
if (where.length === 0) {
|
|
1102
1157
|
return { restored: 0, error: "at least one filter is required" };
|
|
1103
1158
|
}
|
|
@@ -1140,7 +1195,14 @@ function restoreRawJobsByFilter({ search, location }) {
|
|
|
1140
1195
|
return { restored: count };
|
|
1141
1196
|
}
|
|
1142
1197
|
|
|
1143
|
-
function getRawJobsPageFromDb({
|
|
1198
|
+
function getRawJobsPageFromDb({
|
|
1199
|
+
search,
|
|
1200
|
+
location,
|
|
1201
|
+
limit,
|
|
1202
|
+
offset,
|
|
1203
|
+
hasVideo,
|
|
1204
|
+
hasFollower,
|
|
1205
|
+
}) {
|
|
1144
1206
|
if (!db) return null;
|
|
1145
1207
|
|
|
1146
1208
|
const safeLimit = Math.max(1, Math.min(200, parseInt(limit) || 50));
|
|
@@ -1159,6 +1221,12 @@ function getRawJobsPageFromDb({ search, location, limit, offset }) {
|
|
|
1159
1221
|
where.push("COALESCE(guessed_location, '未知') = ?");
|
|
1160
1222
|
args.push(location);
|
|
1161
1223
|
}
|
|
1224
|
+
if (hasVideo) {
|
|
1225
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
1226
|
+
}
|
|
1227
|
+
if (hasFollower) {
|
|
1228
|
+
where.push("COALESCE(follower_count, 0) > 0");
|
|
1229
|
+
}
|
|
1162
1230
|
|
|
1163
1231
|
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
1164
1232
|
const total = db
|
|
@@ -1329,6 +1397,62 @@ function getTargetUsersFromDb(targetLocations = []) {
|
|
|
1329
1397
|
};
|
|
1330
1398
|
}
|
|
1331
1399
|
|
|
1400
|
+
function getTargetUsersByCountryFromDb(targetLocations = []) {
|
|
1401
|
+
if (!db) return null;
|
|
1402
|
+
if (!targetLocations.length) {
|
|
1403
|
+
return { countries: [] };
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const placeholders = targetLocations.map(() => "?").join(", ");
|
|
1407
|
+
const rows = db
|
|
1408
|
+
.prepare(
|
|
1409
|
+
`
|
|
1410
|
+
SELECT
|
|
1411
|
+
unique_id,
|
|
1412
|
+
nickname,
|
|
1413
|
+
follower_count,
|
|
1414
|
+
video_count,
|
|
1415
|
+
tt_seller,
|
|
1416
|
+
verified,
|
|
1417
|
+
location_created,
|
|
1418
|
+
latest_video_time,
|
|
1419
|
+
refresh_time,
|
|
1420
|
+
status,
|
|
1421
|
+
sources
|
|
1422
|
+
FROM jobs
|
|
1423
|
+
WHERE tt_seller = 1
|
|
1424
|
+
AND verified = 0
|
|
1425
|
+
AND location_created IN (${placeholders})
|
|
1426
|
+
ORDER BY location_created ASC, COALESCE(latest_video_time, 0) DESC
|
|
1427
|
+
`,
|
|
1428
|
+
)
|
|
1429
|
+
.all(...targetLocations)
|
|
1430
|
+
.map(mapJobRow);
|
|
1431
|
+
|
|
1432
|
+
const countryMap = new Map();
|
|
1433
|
+
for (const row of rows) {
|
|
1434
|
+
const country = row.locationCreated || "未知";
|
|
1435
|
+
if (!countryMap.has(country)) {
|
|
1436
|
+
countryMap.set(country, []);
|
|
1437
|
+
}
|
|
1438
|
+
countryMap.get(country).push(row);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const countries = [];
|
|
1442
|
+
for (const [country, users] of countryMap) {
|
|
1443
|
+
countries.push({
|
|
1444
|
+
country,
|
|
1445
|
+
count: users.length,
|
|
1446
|
+
users,
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
return {
|
|
1451
|
+
total: rows.length,
|
|
1452
|
+
countries,
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1332
1456
|
function snakeToCamel(key) {
|
|
1333
1457
|
return key.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
1334
1458
|
}
|
|
@@ -2786,9 +2910,10 @@ export function createStore(filePath) {
|
|
|
2786
2910
|
return { saved: true, pinned: user.pinned };
|
|
2787
2911
|
}
|
|
2788
2912
|
|
|
2789
|
-
function getNextRedoJob(userId) {
|
|
2913
|
+
function getNextRedoJob(userId, maxAgeSeconds = 43200) {
|
|
2790
2914
|
if (db) {
|
|
2791
2915
|
const now = Date.now();
|
|
2916
|
+
const threshold = now - maxAgeSeconds * 1000;
|
|
2792
2917
|
const defaultTime = new Date("2016-01-01T00:00:00Z").getTime();
|
|
2793
2918
|
const targetLocations = [
|
|
2794
2919
|
"CZ",
|
|
@@ -2813,11 +2938,12 @@ export function createStore(filePath) {
|
|
|
2813
2938
|
WHERE tt_seller = 1
|
|
2814
2939
|
AND verified = 0
|
|
2815
2940
|
AND location_created IN (${placeholders})
|
|
2941
|
+
AND COALESCE(refresh_time, ?) < ?
|
|
2816
2942
|
ORDER BY COALESCE(refresh_time, ?) ASC
|
|
2817
2943
|
LIMIT 1
|
|
2818
2944
|
`,
|
|
2819
2945
|
)
|
|
2820
|
-
.get(...targetLocations, defaultTime);
|
|
2946
|
+
.get(...targetLocations, defaultTime, threshold, defaultTime);
|
|
2821
2947
|
if (!row) return null;
|
|
2822
2948
|
db.prepare(
|
|
2823
2949
|
"UPDATE jobs SET refresh_time = ?, updated_at = ? WHERE unique_id = ?",
|
|
@@ -2830,6 +2956,7 @@ export function createStore(filePath) {
|
|
|
2830
2956
|
}
|
|
2831
2957
|
|
|
2832
2958
|
const now = Date.now();
|
|
2959
|
+
const threshold = now - maxAgeSeconds * 1000;
|
|
2833
2960
|
const defaultTime = new Date("2016-01-01T00:00:00Z").getTime();
|
|
2834
2961
|
|
|
2835
2962
|
// 筛选目标国家用户,按 refreshTime 升序取最远的(没有则默认 2016-01-01)
|
|
@@ -2855,13 +2982,19 @@ export function createStore(filePath) {
|
|
|
2855
2982
|
);
|
|
2856
2983
|
if (targetUsers.length === 0) return null;
|
|
2857
2984
|
|
|
2858
|
-
targetUsers.
|
|
2985
|
+
const recentEnough = targetUsers.filter((u) => {
|
|
2986
|
+
const rt = u.refreshTime || defaultTime;
|
|
2987
|
+
return rt < threshold;
|
|
2988
|
+
});
|
|
2989
|
+
if (recentEnough.length === 0) return null;
|
|
2990
|
+
|
|
2991
|
+
recentEnough.sort((a, b) => {
|
|
2859
2992
|
const ta = a.refreshTime || defaultTime;
|
|
2860
2993
|
const tb = b.refreshTime || defaultTime;
|
|
2861
2994
|
return ta - tb;
|
|
2862
2995
|
});
|
|
2863
2996
|
|
|
2864
|
-
const next =
|
|
2997
|
+
const next = recentEnough[0];
|
|
2865
2998
|
next.refreshTime = now;
|
|
2866
2999
|
save();
|
|
2867
3000
|
return {
|
|
@@ -2889,9 +3022,10 @@ export function createStore(filePath) {
|
|
|
2889
3022
|
}
|
|
2890
3023
|
}
|
|
2891
3024
|
}
|
|
3025
|
+
const newUsers = processDiscoveredUsers(result);
|
|
2892
3026
|
const ret = updateJobInfo(uniqueId, user, false);
|
|
2893
3027
|
if (ret.error) return { saved: false, error: ret.error };
|
|
2894
|
-
return { saved: true };
|
|
3028
|
+
return { saved: true, newUsers };
|
|
2895
3029
|
}
|
|
2896
3030
|
|
|
2897
3031
|
const user = getUser(uniqueId);
|
|
@@ -2908,8 +3042,8 @@ export function createStore(filePath) {
|
|
|
2908
3042
|
}
|
|
2909
3043
|
}
|
|
2910
3044
|
}
|
|
2911
|
-
|
|
2912
|
-
return { saved: true };
|
|
3045
|
+
const newUsers = processDiscoveredUsers(result);
|
|
3046
|
+
return { saved: true, newUsers };
|
|
2913
3047
|
}
|
|
2914
3048
|
|
|
2915
3049
|
function reportClientError(
|
|
@@ -3349,12 +3483,14 @@ export function createStore(filePath) {
|
|
|
3349
3483
|
getRawByCountry: getRawByCountryFromDb,
|
|
3350
3484
|
moveJobsToRawByCountry,
|
|
3351
3485
|
restoreAttachStuckByCountry,
|
|
3486
|
+
resetPendingByCountry,
|
|
3352
3487
|
restoreRawJobsByCountry,
|
|
3353
3488
|
restoreRawJobById,
|
|
3354
3489
|
restoreRawJobsByFilter,
|
|
3355
3490
|
getUsersPage: getUsersPageFromDb,
|
|
3356
3491
|
getRawJobsPage: getRawJobsPageFromDb,
|
|
3357
3492
|
getTargetUsers: getTargetUsersFromDb,
|
|
3493
|
+
getTargetUsersByCountry: getTargetUsersByCountryFromDb,
|
|
3358
3494
|
getStats,
|
|
3359
3495
|
getStatusGroups,
|
|
3360
3496
|
markGroupsDirty,
|