tt-help-cli-ycl 1.3.33 → 1.3.35

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.3.33",
3
+ "version": "1.3.35",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -107,6 +107,10 @@ IF "%BASE_PORT%"=="" SET "BASE_PORT=9222"
107
107
  SET /P "PORT_COUNT=Port count (--port-count) [default: 10]: "
108
108
  IF "%PORT_COUNT%"=="" SET "PORT_COUNT=10"
109
109
 
110
+ SET /P "JOB_LOCATIONS=Job locations (--job-locations) [default: none]: "
111
+ SET "JOB_LOC_ARGS="
112
+ IF NOT "%JOB_LOCATIONS%"=="" SET "JOB_LOC_ARGS=--job-locations %JOB_LOCATIONS%"
113
+
110
114
  REM ---------- 4. Get user ID from config and compose ----------
111
115
  SET "CONFIG_USER_ID=user"
112
116
  IF EXIST "%CONFIG_PATH%" (
@@ -122,6 +126,7 @@ ECHO ========================================
122
126
  CALL ECHO User ID: %%USER_ID%%
123
127
  CALL ECHO Base port: %%BASE_PORT%%
124
128
  CALL ECHO Port count: %%PORT_COUNT%%
129
+ IF NOT "%JOB_LOCATIONS%"=="" ECHO Job locations: %JOB_LOCATIONS%
125
130
  ECHO Speed: stealth (slowest)
126
131
  ECHO ========================================
127
- CALL tt-help explore stealth --user-id %%USER_ID%% --base-port %%BASE_PORT%% --port-count %%PORT_COUNT%%
132
+ CALL tt-help explore stealth --user-id %%USER_ID%% --base-port %%BASE_PORT%% --port-count %%PORT_COUNT%% %%JOB_LOC_ARGS%%
@@ -127,6 +127,9 @@ $basePort = if ($inputBasePort) { $inputBasePort } else { "9222" }
127
127
  $inputPortCount = Read-Host "端口数量 (--port-count) [默认: 10]"
128
128
  $portCount = if ($inputPortCount) { $inputPortCount } else { "10" }
129
129
 
130
+ $inputJobLocations = Read-Host "任务国家 (--job-locations) [默认: 无,使用全部]"
131
+ $jobLocationsArg = if ($inputJobLocations) { "--job-locations $inputJobLocations" } else { "" }
132
+
130
133
  # ---------- 4. 获取配置中的用户编号并拼接 ----------
131
134
  $configUserId = ""
132
135
  if (Test-Path $configPath) {
@@ -148,6 +151,7 @@ Write-Host "========================================"
148
151
  Write-Host " 用户编号: $userId"
149
152
  Write-Host " 起始端口: $basePort"
150
153
  Write-Host " 端口数量: $portCount"
154
+ if ($inputJobLocations) { Write-Host " 任务国家: $inputJobLocations" }
151
155
  Write-Host " 速度: stealth (最慢)"
152
156
  Write-Host "========================================"
153
- tt-help explore stealth --user-id $userId --base-port $basePort --port-count $portCount
157
+ tt-help explore stealth --user-id $userId --base-port $basePort --port-count $portCount $jobLocationsArg
@@ -91,6 +91,9 @@ printf "端口数量 (--port-count) [默认: 10]: "
91
91
  read INPUT_PORT_COUNT
92
92
  PORT_COUNT="${INPUT_PORT_COUNT:-10}"
93
93
 
94
+ printf "任务国家 (--job-locations) [默认: 无,使用全部]: "
95
+ read INPUT_JOB_LOCATIONS
96
+
94
97
  # ---------- 4. 获取配置中的用户编号并拼接 ----------
95
98
  CONFIG_USER_ID=""
96
99
  if [ -f "$CONFIG_PATH" ]; then
@@ -108,6 +111,9 @@ echo "========================================"
108
111
  echo " 用户编号: $USER_ID"
109
112
  echo " 起始端口: $BASE_PORT"
110
113
  echo " 端口数量: $PORT_COUNT"
114
+ if [ -n "$INPUT_JOB_LOCATIONS" ]; then echo " 任务国家: $INPUT_JOB_LOCATIONS"; fi
111
115
  echo " 速度: stealth (最慢)"
112
116
  echo "========================================"
113
- tt-help explore stealth --user-id "$USER_ID" --base-port "$BASE_PORT" --port-count "$PORT_COUNT"
117
+ JOB_LOC_ARGS=""
118
+ if [ -n "$INPUT_JOB_LOCATIONS" ]; then JOB_LOC_ARGS="--job-locations $INPUT_JOB_LOCATIONS"; fi
119
+ tt-help explore stealth --user-id "$USER_ID" --base-port "$BASE_PORT" --port-count "$PORT_COUNT" $JOB_LOC_ARGS
@@ -197,6 +197,7 @@ export async function handleExplore(options) {
197
197
  );
198
198
  lastFollowSuccessTime = Date.now(); // 切换账户后重置
199
199
  captchaCount = 0; // 重置验证码计数
200
+ blockedCount = 0; // 重置被封计数
200
201
  consecutiveNetworkErrors = 0; // 重置网络错误计数
201
202
  }
202
203
 
@@ -204,13 +205,14 @@ export async function handleExplore(options) {
204
205
  let errorCount = 0;
205
206
  let consecutiveNetworkErrors = 0;
206
207
  let captchaCount = 0; // 验证码累计计数
208
+ let blockedCount = 0; // 被封累计计数
207
209
  let lastFollowSuccessTime = Date.now(); // 最近一次成功获取关注/粉丝的时间
208
210
  const FOLLOW_BLOCK_THRESHOLD = 10 * 60 * 1000; // 10分钟
209
211
 
210
212
  while (true) {
211
213
  const jobQuery = exploreJobLocations
212
- ? `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&locations=${encodeURIComponent(exploreJobLocations)}`
213
- : `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}`;
214
+ ? `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&locations=${encodeURIComponent(exploreJobLocations)}&loggedIn=${loggedIn}`
215
+ : `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&loggedIn=${loggedIn}`;
214
216
  const job = await apiGet(jobQuery);
215
217
  if (!job.hasJob) break;
216
218
 
@@ -385,7 +387,12 @@ export async function handleExplore(options) {
385
387
  }),
386
388
  ).catch(() => {});
387
389
  if (errorType === "被封") {
388
- await handleAccountSwitch("账号被封");
390
+ blockedCount++;
391
+ console.error(` [被封] 累计 ${blockedCount} 次`);
392
+ if (blockedCount >= 3) {
393
+ await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
394
+ blockedCount = 0;
395
+ }
389
396
  continue;
390
397
  }
391
398
  if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
@@ -1,14 +1,10 @@
1
1
  import { delay } from "./delay.js";
2
- import { retryWithBackoff } from "./retry.js";
3
2
  import { assertPageUrl } from "./browser/page.js";
4
3
  import {
5
4
  detectPageError,
6
5
  detectPageErrorWithWait,
7
6
  } from "./page-error-detector.js";
8
7
 
9
- /**
10
- * 处理 API 响应数据:提取首页视频 + 翻页
11
- */
12
8
  async function processAPIResponse(
13
9
  data,
14
10
  username,
@@ -18,7 +14,6 @@ async function processAPIResponse(
18
14
  apiRequestUrl,
19
15
  log,
20
16
  ) {
21
- // 提取首页视频
22
17
  const firstPageItems = data.itemList || [];
23
18
  for (const item of firstPageItems) {
24
19
  if (items.length >= maxVideos) break;
@@ -26,7 +21,6 @@ async function processAPIResponse(
26
21
  items.push({ id: item.id, href });
27
22
  }
28
23
 
29
- // 翻页获取后续视频
30
24
  let cursor = data.cursor;
31
25
  let hasMore = data.hasMore;
32
26
 
@@ -61,7 +55,6 @@ async function processAPIResponse(
61
55
 
62
56
  log(` [API拦截] 共 ${items.length} 条视频`);
63
57
 
64
- // 转成 Map 返回,与 collectVideos 一致
65
58
  const videoMap = new Map();
66
59
  for (const v of items) {
67
60
  if (!videoMap.has(v.id)) videoMap.set(v.id, v);
@@ -70,232 +63,80 @@ async function processAPIResponse(
70
63
  return videoMap;
71
64
  }
72
65
 
73
- /**
74
- * 单次 API 拦截尝试:注册拦截器 导航 → 用 waitForResponse 精确等待 API
75
- * @param {number} apiTimeout - API 响应超时时间(ms),默认 15000
76
- * @returns {{data: object|null, apiRequestUrl: string|null, elapsed: number}}
77
- */
78
- async function tryInterceptAPI(page, username, log, apiTimeout = 15000) {
79
- const url = `https://www.tiktok.com/@${username}`;
66
+ async function fetchUserVideosAPI(page, username, maxVideos, log) {
67
+ log(` [API拦截] 获取 @${username} 视频 ...`);
68
+
80
69
  let apiRequestUrl = null;
81
70
  let sawApiRequest = false;
82
- let sawApiResponse = false;
83
-
84
- const responseHandler = async (response) => {
85
- if (response.url().includes("/api/post/item_list/")) {
86
- sawApiResponse = true;
87
- }
88
- };
89
71
 
90
72
  const requestHandler = (request) => {
91
- if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
92
- sawApiRequest = true;
93
- apiRequestUrl = request.url();
94
- } else if (request.url().includes("/api/post/item_list/")) {
73
+ if (request.url().includes("/api/post/item_list/")) {
95
74
  sawApiRequest = true;
75
+ if (!apiRequestUrl) apiRequestUrl = request.url();
96
76
  }
97
77
  };
98
78
 
99
- page.on("response", responseHandler);
100
79
  page.on("request", requestHandler);
101
80
 
102
81
  const t0 = Date.now();
82
+ let data = null;
83
+ let interceptionError = null;
103
84
 
104
85
  try {
105
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
106
- await assertPageUrl(page, `@${username}`);
107
-
108
- // 用 waitForResponse 精确等待 API 响应(超时时间可配置)
109
- const response = await page.waitForResponse(
110
- (res) => res.url().includes("/api/post/item_list/"),
111
- { timeout: apiTimeout },
86
+ await page.goto(
87
+ `https://www.tiktok.com/@${username}`,
88
+ { waitUntil: "domcontentloaded", timeout: 30000 },
112
89
  );
90
+ await assertPageUrl(page, `@${username}`);
113
91
 
114
- sawApiResponse = true;
115
- const data = await response.json();
116
- const elapsed = Date.now() - t0;
117
- return { data, apiRequestUrl, elapsed, sawApiRequest, sawApiResponse };
118
- } catch (e) {
119
- const elapsed = Date.now() - t0;
120
- return {
121
- data: null,
122
- apiRequestUrl,
123
- elapsed,
124
- error: e.message,
125
- sawApiRequest,
126
- sawApiResponse,
127
- };
128
- } finally {
129
- page.off("response", responseHandler);
130
- page.off("request", requestHandler);
131
- }
132
- }
133
-
134
- /**
135
- * 软重试:不重新 goto,直接在当前页继续等待 item_list API。
136
- */
137
- async function tryWaitCurrentPageAPI(page, apiTimeout = 12000) {
138
- let apiRequestUrl = null;
139
- let sawApiRequest = false;
140
- let sawApiResponse = false;
141
-
142
- const requestHandler = (request) => {
143
- if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
144
- sawApiRequest = true;
145
- apiRequestUrl = request.url();
146
- } else if (request.url().includes("/api/post/item_list/")) {
147
- sawApiRequest = true;
148
- }
149
- };
150
-
151
- const responseHandler = (response) => {
152
- if (response.url().includes("/api/post/item_list/")) {
153
- sawApiResponse = true;
154
- }
155
- };
156
-
157
- const t0 = Date.now();
158
- page.on("request", requestHandler);
159
- page.on("response", responseHandler);
160
-
161
- try {
162
92
  const response = await page.waitForResponse(
163
93
  (res) => res.url().includes("/api/post/item_list/"),
164
- { timeout: apiTimeout },
94
+ { timeout: 30000 },
165
95
  );
166
96
 
167
- sawApiResponse = true;
168
- const data = await response.json();
169
- const elapsed = Date.now() - t0;
170
- return { data, apiRequestUrl, elapsed, sawApiRequest, sawApiResponse };
97
+ data = await response.json();
171
98
  } catch (e) {
172
- const elapsed = Date.now() - t0;
173
- return {
174
- data: null,
175
- apiRequestUrl,
176
- elapsed,
177
- error: e.message,
178
- sawApiRequest,
179
- sawApiResponse,
180
- };
99
+ interceptionError = e.message;
181
100
  } finally {
182
101
  page.off("request", requestHandler);
183
- page.off("response", responseHandler);
184
102
  }
185
- }
186
-
187
- /**
188
- * 通过拦截 TikTok 内部 API 获取用户视频列表
189
- * 比 DOM 滚动解析快 5-10 倍
190
- *
191
- * 弱网优化:
192
- * - 第 1 轮:正常 goto,等待 15s
193
- * - 第 1.5 轮:不重新 goto,留在当前页继续等待 12s
194
- * - 第 2 轮:重新 goto,等待 30s
195
- * - 使用 waitForResponse 精确等待 API 响应(而非固定超时)
196
- *
197
- * @param {import('playwright').Page} page - Playwright page (CDP 连接)
198
- * @param {string} username - TikTok 用户名
199
- * @param {number} maxVideos - 最大视频数
200
- * @param {Function} log - 日志函数
201
- * @returns {Map<string, {id: string, href: string}>} 与 collectVideos 返回格式一致
202
- * @throws {Error} API 超时或网络异常时抛出
203
- */
204
- async function fetchUserVideosAPI(page, username, maxVideos, log) {
205
- const items = [];
206
- const noRetryErrors = new Set([
207
- "service_error",
208
- "not_found",
209
- "region_blocked",
210
- "login_required",
211
- ]);
212
103
 
213
- const attempts = [
214
- {
215
- label: ` [API拦截] 获取 @${username} 视频 ...`,
216
- mode: "goto",
217
- apiTimeout: 15000,
218
- },
219
- {
220
- label: " [API拦截] 首轮未命中,当前页继续补等 12s ...",
221
- mode: "wait",
222
- apiTimeout: 12000,
223
- },
224
- {
225
- label: " [API拦截] 重新加载主页后重试 (超时 30s)",
226
- mode: "goto",
227
- apiTimeout: 30000,
228
- },
229
- ];
104
+ const elapsed = Date.now() - t0;
230
105
 
231
- for (let attempt = 0; attempt < attempts.length; attempt++) {
232
- const currentAttempt = attempts[attempt];
233
- log(currentAttempt.label);
234
-
235
- const {
106
+ if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
107
+ log(` [API拦截] 成功,耗时 ${elapsed}ms`);
108
+ return await processAPIResponse(
236
109
  data,
110
+ username,
111
+ maxVideos,
112
+ [],
113
+ page,
237
114
  apiRequestUrl,
238
- elapsed,
239
- error,
240
- sawApiRequest,
241
- sawApiResponse,
242
- } =
243
- currentAttempt.mode === "goto"
244
- ? await tryInterceptAPI(page, username, log, currentAttempt.apiTimeout)
245
- : await tryWaitCurrentPageAPI(page, currentAttempt.apiTimeout);
246
-
247
- if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
248
- // 成功拦截且拿到了视频,处理数据
249
- return await processAPIResponse(
250
- data,
251
- username,
252
- maxVideos,
253
- items,
254
- page,
255
- apiRequestUrl,
256
- log,
257
- );
258
- }
259
-
260
- // API 已返回但没有视频时,优先判定是否是确定性的页面错误。
261
- let pageError = await detectPageError(page);
262
-
263
- // service_error 经常比 API 判定晚一点渲染,补一个短等待避免漏判“被封”。
264
- if (!pageError) {
265
- const delayedPageError = await detectPageErrorWithWait(page, 3000);
266
- if (delayedPageError) pageError = delayedPageError;
267
- }
268
-
269
- if (pageError === "service_error") {
270
- throw new Error(`被封: ${username}`);
271
- }
272
-
273
- if (data && Array.isArray(data.itemList) && data.itemList.length === 0) {
274
- if (noRetryErrors.has(pageError)) {
275
- return new Map();
276
- }
115
+ log,
116
+ );
117
+ }
277
118
 
278
- return new Map();
279
- }
119
+ let pageError = await detectPageError(page);
120
+ if (!pageError) {
121
+ pageError = await detectPageErrorWithWait(page, 5000);
122
+ }
280
123
 
281
- if (noRetryErrors.has(pageError)) {
282
- return new Map();
283
- }
124
+ if (pageError === "service_error") {
125
+ throw new Error(`被封: ${username}`);
126
+ }
284
127
 
285
- log(
286
- ` [API拦截] 未命中: request=${sawApiRequest ? "Y" : "N"}, response=${sawApiResponse ? "Y" : "N"}, elapsed=${elapsed}ms${error ? `, error=${error}` : ""}${apiRequestUrl ? ", apiUrl=Y" : ", apiUrl=N"}`,
287
- );
128
+ if (pageError) {
129
+ log(` [API拦截] 数据为空,识别到页面错误: ${pageError}`);
130
+ return new Map();
131
+ }
288
132
 
289
- // 最后一次重试也失败了
290
- if (attempt === attempts.length - 1) {
291
- if (!pageError) {
292
- throw new Error(
293
- `@${username} 页面异常(首轮 + 补等 + 重载后仍未拦截到 API)`,
294
- );
295
- }
296
- return new Map();
297
- }
133
+ if (interceptionError) {
134
+ log(` [API拦截] 超时/拦截失败: ${interceptionError},耗时 ${elapsed}ms`);
135
+ throw new Error(`@${username} 页面异常(API拦截超时,未识别到错误类型)`);
298
136
  }
137
+
138
+ log(` [API拦截] 数据为空,未识别到错误类型,当作无视频处理`);
139
+ return new Map();
299
140
  }
300
141
 
301
- export { fetchUserVideosAPI };
142
+ export { fetchUserVideosAPI };
package/src/lib/args.js CHANGED
@@ -170,7 +170,7 @@ function parseExploreArgs(args) {
170
170
  let exploreEnableFollow = true;
171
171
  let exploreMaxFollowing = 50;
172
172
  let exploreMaxFollowers = 50;
173
- let exploreLocation = "PL,NL,BE,DE,FR,IT,ES,IE";
173
+ let exploreLocation = "PL,NL,BE,DE,FR,IT,ES,IE,AT";
174
174
  let exploreJobLocations = null;
175
175
  let exploreMaxUsers = 0;
176
176
  let explorePort = null;
@@ -109,16 +109,19 @@ const HELP_TEXT = [
109
109
  ' 预设: fast, normal(默认), slow, stealth',
110
110
  ' 选项:',
111
111
  ' --server <URL> 服务端地址,默认 http://127.0.0.1:3001',
112
- ' --location <国家代码> 国家筛选,逗号分隔,默认 PL,NL,BE,DE,FR,IT,ES,IE',
112
+ ' --location <国家代码> 国家筛选,逗号分隔,默认 PL,NL,BE,DE,FR,IT,ES,IE,AT',
113
+ ' --job-locations <国家> 任务国家筛选,逗号分隔(仅筛选服务端任务)',
113
114
  ' --max-comments <数量> 每视频最大评论数,默认 10',
114
115
  ' --max-guess <数量> 每视频最大猜你喜欢数,默认 0',
116
+ ' --max-videos <数量> 每用户最大视频数,默认 16',
115
117
  ' --enable-follow 启用关注/粉丝提取(默认启用)',
116
118
  ' --disable-follow 禁用关注/粉丝提取',
117
- ' --max-following <数量> 最大获取关注数,默认 5',
118
- ' --max-followers <数量> 最大获取粉丝数,默认 5',
119
+ ' --max-following <数量> 最大获取关注数,默认 50(同时设置粉丝数)',
120
+ ' --max-followers <数量> 最大获取粉丝数,默认 50',
119
121
  ' --max-users <数量> 最大处理用户数,默认无限制',
120
- ' --port <端口号> CDP 端口,默认 9222',
121
- ' --profile <名称> 浏览器 Profile 名称',
122
+ ' --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)',
123
+ ' --base-port <端口号> 起始端口,默认 9222',
124
+ ' --port-count <数量> 端口数量(账户数),默认 10',
122
125
  ' --user-id <编号> 客户端编号(设备ID),默认自动生成',
123
126
  '',
124
127
  ' tt-help info <URL> [URL2 ...] [--onlyvideo]',
@@ -66,19 +66,29 @@ async function processExplore(page, username, options, log) {
66
66
  return result;
67
67
  }
68
68
 
69
- // 从第一个视频获取 locationCreated
69
+ // 从最多 5 个视频并发获取 locationCreated,取众数
70
+ const SAMPLE_SIZE = 5;
70
71
  let locationCreated = null;
71
- if (videoArray.length > 0) {
72
- const firstVideo = videoArray[0];
73
- const firstVideoUrl = firstVideo.href.startsWith("http")
74
- ? firstVideo.href
75
- : `https://www.tiktok.com${firstVideo.href}`;
76
-
77
- locationCreated = await extractVideoLocation(firstVideoUrl);
72
+ const sampleVideos = videoArray.slice(0, SAMPLE_SIZE);
73
+ if (sampleVideos.length > 0) {
74
+ const sampleUrls = sampleVideos.map(v =>
75
+ v.href.startsWith("http") ? v.href : `https://www.tiktok.com${v.href}`
76
+ );
77
+ const locations = await Promise.all(sampleUrls.map(url => extractVideoLocation(url)));
78
+ log(` 国家采样(${locations.length}个): [${locations.filter(Boolean).join(", ") || "无数据"}]`);
79
+ const freq = {};
80
+ for (const loc of locations) {
81
+ if (loc) {
82
+ const key = loc.toUpperCase();
83
+ freq[key] = (freq[key] || 0) + 1;
84
+ }
85
+ }
86
+ const entries = Object.entries(freq).sort((a, b) => b[1] - a[1]);
87
+ locationCreated = entries.length > 0 ? entries[0][0] : null;
78
88
  }
79
89
 
80
90
  result.locationCreated = locationCreated || null;
81
- log(` 国家: ${result.locationCreated || "未知"}`);
91
+ log(` 国家: ${result.locationCreated || "未知"} (众数)`);
82
92
 
83
93
  // 国家筛选
84
94
  const locationList = (location || "ES")
@@ -414,7 +414,12 @@ export function createStore(filePath) {
414
414
  return data;
415
415
  }
416
416
 
417
- function claimNextJob(userId, expireMs = 5 * 60 * 1000, locations = null) {
417
+ function claimNextJob(
418
+ userId,
419
+ expireMs = 5 * 60 * 1000,
420
+ locations = null,
421
+ loggedIn = true,
422
+ ) {
418
423
  const now = Date.now();
419
424
 
420
425
  // 0. 该客户端有未过期的任务,续期返回
@@ -452,7 +457,7 @@ export function createStore(filePath) {
452
457
  return locations.includes(loc);
453
458
  }
454
459
 
455
- // 从候选列表中按优先级取第一个:pinned > 超时回收 > seed > ttSeller > follow > other
460
+ // 从候选列表中按优先级取第一个:pinned > 超时回收 > seed > ttSeller(仅登录) > follow > other
456
461
  function pickCandidate(candidates) {
457
462
  let next = candidates.find((u) => u.pinned);
458
463
 
@@ -479,7 +484,8 @@ export function createStore(filePath) {
479
484
  next = seed[0] || null;
480
485
  }
481
486
 
482
- if (!next) {
487
+ // 未登录时跳过 ttSeller 优先级
488
+ if (!next && loggedIn) {
483
489
  const ttSeller = candidates.filter(
484
490
  (u) => u.ttSeller === true && u.verified === false,
485
491
  );
@@ -511,6 +517,10 @@ export function createStore(filePath) {
511
517
  if (locations && locations.length > 0) {
512
518
  pending = pending.filter(locationFilter);
513
519
  }
520
+ // 未登录客户端不能领取 ttSeller 用户
521
+ if (!loggedIn) {
522
+ pending = pending.filter((u) => u.ttSeller !== true);
523
+ }
514
524
  let hasVideo = pending.filter((u) => u.videoCount > 0);
515
525
  const next = pickCandidate(hasVideo) || pickCandidate(pending);
516
526