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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.3.52",
3
+ "version": "1.3.57",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/attach.js CHANGED
@@ -244,10 +244,16 @@ export async function handleAttach(options) {
244
244
  );
245
245
  failCount++;
246
246
  } else if (info) {
247
- successTasks.push({ uniqueId, info });
247
+ // info 可能是 { error: true, statusCode: xxx } 表示 TikTok 给了明确响应
248
+ if (info.error) {
249
+ // 有 statusCode 说明 TikTok 已给出明确响应,提交到后端记录,不算错误
250
+ successTasks.push({ uniqueId, info });
251
+ } else {
252
+ successTasks.push({ uniqueId, info });
253
+ }
248
254
  } else {
249
- attachLog(` - @${uniqueId} 无用户信息`);
250
- successTasks.push({ uniqueId, info: {} });
255
+ attachLog(` @${uniqueId} 未获取到用户信息`);
256
+ failCount++;
251
257
  }
252
258
  } else {
253
259
  attachLog(
@@ -263,26 +269,32 @@ export async function handleAttach(options) {
263
269
  const batchRet = await apiPost(`${serverUrl}/api/user-info-batch`, {
264
270
  updates: successTasks,
265
271
  });
266
- if (batchRet && batchRet.results) {
267
- const nicknameMap = {};
268
- for (const { uniqueId, info } of successTasks) {
269
- if (info?.nickname) {
270
- nicknameMap[uniqueId] = info.nickname;
271
- }
272
- }
272
+ if (batchRet && batchRet.results && Array.isArray(batchRet.results)) {
273
273
  for (const r of batchRet.results) {
274
274
  if (r.ok) {
275
275
  successCount++;
276
- const nickname = nicknameMap[r.uniqueId] || r.uniqueId;
277
- attachLog(` ✓ @${r.uniqueId} (${nickname}) 已提交更新`);
276
+ // 查找对应的 info 判断是否有 statusCode
277
+ const task = successTasks.find(
278
+ (t) => t.uniqueId === r.uniqueId,
279
+ );
280
+ if (task && task.info && task.info.error) {
281
+ attachLog(
282
+ ` ⚠ @${r.uniqueId} 已记录 (statusCode=${task.info.statusCode})`,
283
+ );
284
+ } else {
285
+ attachLog(` ✓ @${r.uniqueId} 已提交更新`);
286
+ }
278
287
  } else {
279
288
  failCount++;
280
289
  attachLog(` ✗ @${r.uniqueId} 提交失败: ${r.error}`);
281
290
  }
282
291
  }
283
292
  } else {
293
+ // 后端返回格式异常,降级处理
294
+ attachLog(
295
+ ` ⚠ 后端响应格式异常 (batchRet=${JSON.stringify(batchRet).slice(0, 200)}), 降级为批量成功`,
296
+ );
284
297
  successCount = successTasks.length;
285
- attachLog(` ✓ 批量提交完成 (${successTasks.length} 条)`);
286
298
  }
287
299
  } catch (err) {
288
300
  failCount += successTasks.length;
package/src/cli/info.js CHANGED
@@ -1,25 +1,38 @@
1
- import { TikTokScraper } from '../lib/tiktok-scraper.mjs';
2
- import { isProfileUrl, isVideoUrl, extractUniqueId, normalizeUsername } from '../lib/url.js';
1
+ import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
2
+ import {
3
+ isProfileUrl,
4
+ isVideoUrl,
5
+ extractUniqueId,
6
+ normalizeUsername,
7
+ } from "../lib/url.js";
3
8
 
4
9
  async function handleInfo(options) {
5
10
  const { infoUrls, infoOnlyVideo } = options;
6
11
 
7
12
  if (!infoUrls || infoUrls.length === 0) {
8
- console.error('用法: tt-help info <URL> [URL2 URL3...] [--onlyvideo]');
9
- console.error('');
10
- console.error('参数:');
11
- console.error(' <URL> TikTok 主页或视频 URL,支持多个 URL 同时查询');
12
- console.error(' --onlyvideo 只返回视频信息(不返回用户信息)');
13
- console.error('');
14
- console.error('默认行为:');
15
- console.error(' 主页 URL → 返回用户信息(bio、region、粉丝数等)');
16
- console.error(' 视频 URL → 返回用户信息 + 视频信息');
17
- console.error(' 视频 URL + --onlyvideo → 只返回视频信息');
18
- console.error('');
19
- console.error('示例:');
20
- console.error(' tt-help info https://www.tiktok.com/@nike');
21
- console.error(' tt-help info https://www.tiktok.com/@nike/video/7234567890');
22
- console.error(' tt-help info https://www.tiktok.com/@nike https://www.tiktok.com/@apple');
13
+ console.error("用法: tt-help info <URL> [URL2 URL3...] [--onlyvideo]");
14
+ console.error("");
15
+ console.error("参数:");
16
+ console.error(
17
+ " <URL> TikTok 主页或视频 URL,支持多个 URL 同时查询",
18
+ );
19
+ console.error(" --onlyvideo 只返回视频信息(不返回用户信息)");
20
+ console.error("");
21
+ console.error("默认行为:");
22
+ console.error(
23
+ " 主页 URL → 返回用户信息(bio、region、粉丝数等)",
24
+ );
25
+ console.error(" 视频 URL → 返回用户信息 + 视频信息");
26
+ console.error(" 视频 URL + --onlyvideo → 只返回视频信息");
27
+ console.error("");
28
+ console.error("示例:");
29
+ console.error(" tt-help info https://www.tiktok.com/@nike");
30
+ console.error(
31
+ " tt-help info https://www.tiktok.com/@nike/video/7234567890",
32
+ );
33
+ console.error(
34
+ " tt-help info https://www.tiktok.com/@nike https://www.tiktok.com/@apple",
35
+ );
23
36
  process.exit(1);
24
37
  }
25
38
 
@@ -34,28 +47,35 @@ async function handleInfo(options) {
34
47
  const uniqueId = extractUniqueId(url);
35
48
  const normalized = normalizeUsername(uniqueId);
36
49
  const user = await scraper.getUserInfo(normalized);
37
- if (user) {
38
- result[normalized] = { user };
39
- console.error(`用户: @${user.uniqueId} (${user.nickname})`);
50
+ if (!user || user.error) {
51
+ const code = user?.statusCode;
52
+ console.error(
53
+ `无法获取用户 @${uniqueId} 的信息${code !== undefined ? ` (statusCode=${code})` : ""}`,
54
+ );
55
+ continue;
40
56
  }
57
+ result[normalized] = { user };
58
+ console.error(`用户: @${user.uniqueId} (${user.nickname})`);
41
59
  } else if (isVideoUrl(url)) {
42
60
  const uniqueId = extractUniqueId(url);
43
61
  const normalized = normalizeUsername(uniqueId);
44
62
 
45
63
  if (infoOnlyVideo) {
46
64
  const video = await scraper.getVideoInfo(url);
47
- if (video) {
48
- const key = normalized + '/video/' + video.id;
49
- result[key] = { video };
50
- console.error(`视频: ${video.id}`);
65
+ if (!video) {
66
+ console.error(`无法获取视频信息: ${url}`);
67
+ continue;
51
68
  }
69
+ const key = normalized + "/video/" + video.id;
70
+ result[key] = { video };
71
+ console.error(`视频: ${video.id}`);
52
72
  } else {
53
73
  const [user, video] = await Promise.all([
54
74
  scraper.getUserInfo(normalized),
55
75
  scraper.getVideoInfo(url),
56
76
  ]);
57
77
  const entry = {};
58
- if (user) {
78
+ if (user && !user.error) {
59
79
  entry.user = user;
60
80
  console.error(`用户: @${user.uniqueId} (${user.nickname})`);
61
81
  }
@@ -63,10 +83,12 @@ async function handleInfo(options) {
63
83
  entry.video = video;
64
84
  console.error(`视频: ${video.id}`);
65
85
  }
66
- if (user || video) {
67
- const key = normalized + '/video/' + (video ? video.id : 'unknown');
68
- result[key] = entry;
86
+ if ((!user || user.error) && !video) {
87
+ console.error(`无法获取信息: ${url}`);
88
+ continue;
69
89
  }
90
+ const key = normalized + "/video/" + (video ? video.id : "unknown");
91
+ result[key] = entry;
70
92
  }
71
93
  } else {
72
94
  console.error(`无法识别 URL: ${url}`);
package/src/cli/open.js CHANGED
@@ -33,13 +33,14 @@ export async function handleOpen(parsed) {
33
33
  if (!openPort) {
34
34
  console.error("用法: tt-help open <端口>");
35
35
  console.error("示例: tt-help open 9222");
36
- console.error('运行 "tt-help open --list" 查看所有内置配置');
36
+ console.error("");
37
+ console.error('运行 "tt-help open --list" 查看内置浏览器配置');
37
38
  process.exit(1);
38
39
  }
39
40
 
40
41
  const port = parseInt(openPort);
41
42
  if (isNaN(port) || port < 1 || port > 65535) {
42
- console.error(`端口 ${openPort} 无效,请输入 1-65535 之间的端口号`);
43
+ console.error(`端口 ${openPort} 无效 (请使用 1 - 65535)`);
43
44
  process.exit(1);
44
45
  }
45
46
 
@@ -1,20 +1,65 @@
1
+ /**
2
+ * 判断失败是否可重试
3
+ * - 有 statusCode(无论值是多少):TikTok 给了明确响应,不可重试
4
+ * - 没有 statusCode(空壳 HTML,无 SSR):并发限流,可重试
5
+ */
6
+ export function isRetryableFailure(rawHtml) {
7
+ if (!rawHtml || typeof rawHtml !== "string") return false;
8
+ // 没有 SSR 标记 = 空壳 HTML = 可重试
9
+ if (!rawHtml.includes("__UNIVERSAL_DATA_FOR_REHYDRATION__")) {
10
+ return true;
11
+ }
12
+ // 有 SSR 数据,检查是否有 statusCode
13
+ try {
14
+ const idx = rawHtml.indexOf("__UNIVERSAL_DATA_FOR_REHYDRATION__");
15
+ const sIdx = rawHtml.indexOf(">", idx) + 1;
16
+ const eIdx = rawHtml.indexOf("</script>", sIdx);
17
+ if (sIdx < 0 || eIdx < 0) return true;
18
+ const data = JSON.parse(rawHtml.substring(sIdx, eIdx));
19
+ const ud = data.__DEFAULT_SCOPE__?.["webapp.user-detail"];
20
+ // 有 statusCode 说明 TikTok 给了明确响应(0=成功,10202=被封,10221=不存在等),不可重试
21
+ if (ud && "statusCode" in ud) return false;
22
+ } catch {}
23
+ return true;
24
+ }
25
+
1
26
  function parseSSR(rawHtml) {
2
- if (!rawHtml.includes('__UNIVERSAL_DATA_FOR_REHYDRATION__')) return null;
3
- const dataStart = rawHtml.indexOf('__UNIVERSAL_DATA_FOR_REHYDRATION__');
4
- // 从该字符串后面找 <script 标签的 >,确保找到的是正确行的 >
5
- const scriptStart = rawHtml.lastIndexOf('<script', dataStart);
6
- const sIdx = (scriptStart >= 0 ? rawHtml.indexOf('>', scriptStart) : rawHtml.indexOf('>', dataStart)) + 1;
7
- const eIdx = rawHtml.indexOf('</script>', sIdx);
8
- if (sIdx < 0 || eIdx < 0) return null;
27
+ if (!rawHtml || typeof rawHtml !== "string") {
28
+ return null;
29
+ }
30
+ if (!rawHtml.includes("__UNIVERSAL_DATA_FOR_REHYDRATION__")) {
31
+ return null;
32
+ }
33
+ const dataStart = rawHtml.indexOf("__UNIVERSAL_DATA_FOR_REHYDRATION__");
34
+ const scriptStart = rawHtml.lastIndexOf("<script", dataStart);
35
+ const sIdx =
36
+ (scriptStart >= 0
37
+ ? rawHtml.indexOf(">", scriptStart)
38
+ : rawHtml.indexOf(">", dataStart)) + 1;
39
+ const eIdx = rawHtml.indexOf("</script>", sIdx);
40
+ if (sIdx < 0 || eIdx < 0) {
41
+ return null;
42
+ }
9
43
  const jsonStr = rawHtml.substring(sIdx, eIdx);
10
- return JSON.parse(jsonStr);
44
+ try {
45
+ return JSON.parse(jsonStr);
46
+ } catch (e) {
47
+ return null;
48
+ }
11
49
  }
12
50
 
13
51
  export function parseUserInfo(rawHtml) {
14
52
  const data = parseSSR(rawHtml);
15
53
  if (!data) return null;
16
- const ud = data.__DEFAULT_SCOPE__['webapp.user-detail'];
17
- if (!ud || !ud.userInfo) return null;
54
+ const scopeKeys = data.__DEFAULT_SCOPE__
55
+ ? Object.keys(data.__DEFAULT_SCOPE__)
56
+ : [];
57
+ const ud =
58
+ data.__DEFAULT_SCOPE__ && data.__DEFAULT_SCOPE__["webapp.user-detail"];
59
+ if (!ud || !ud.userInfo) {
60
+ const code = ud?.statusCode;
61
+ return { error: true, statusCode: code };
62
+ }
18
63
  const u = ud.userInfo.user;
19
64
  const s = ud.userInfo.stats;
20
65
  return {
@@ -24,8 +69,8 @@ export function parseUserInfo(rawHtml) {
24
69
  verified: u.verified,
25
70
  privateAccount: u.privateAccount,
26
71
  language: u.language,
27
- bio: u.signature || '',
28
- avatar: u.avatarLarger || u.avatarMedium || u.avatarThumb || '',
72
+ bio: u.signature || "",
73
+ avatar: u.avatarLarger || u.avatarMedium || u.avatarThumb || "",
29
74
  followerCount: s.followerCount,
30
75
  followingCount: s.followingCount,
31
76
  heartCount: s.heartCount,
@@ -35,20 +80,30 @@ export function parseUserInfo(rawHtml) {
35
80
  secUid: u.secUid,
36
81
  ttSeller: u.ttSeller || false,
37
82
  locationCreated: u.locationCreated || null,
83
+ statusCode: 0,
38
84
  };
39
85
  }
40
86
 
41
87
  export function parseVideoInfo(rawHtml) {
42
88
  const data = parseSSR(rawHtml);
43
89
  if (!data) return null;
44
- const vd = data.__DEFAULT_SCOPE__['webapp.video-detail'];
45
- if (!vd || !vd.itemInfo || !vd.itemInfo.itemStruct) return null;
90
+ const scopeKeys = data.__DEFAULT_SCOPE__
91
+ ? Object.keys(data.__DEFAULT_SCOPE__)
92
+ : [];
93
+ const vd =
94
+ data.__DEFAULT_SCOPE__ && data.__DEFAULT_SCOPE__["webapp.video-detail"];
95
+ if (!vd || !vd.itemInfo || !vd.itemInfo.itemStruct) {
96
+ console.error(
97
+ `[parseVideoInfo] webapp.video-detail 不存在, scope keys: ${JSON.stringify(scopeKeys)}`,
98
+ );
99
+ return null;
100
+ }
46
101
  const item = vd.itemInfo.itemStruct;
47
102
  const author = item.author || {};
48
103
  const stats = item.stats || {};
49
104
  return {
50
105
  id: item.id,
51
- desc: item.desc || '',
106
+ desc: item.desc || "",
52
107
  createTime: item.createTime || null,
53
108
  locationCreated: item.locationCreated || null,
54
109
  author: {
package/src/lib/scrape.js CHANGED
@@ -57,7 +57,7 @@ export async function extractUserData(url) {
57
57
  const uniqueId = extractUniqueId(url);
58
58
  if (!uniqueId) throw new Error(`无法从URL提取用户名: ${url}`);
59
59
  const user = await scraper.getUserInfo(normalizeUsername(uniqueId));
60
- if (!user) return null;
60
+ if (!user) throw new Error('无法解析用户信息');
61
61
  return mapUserInfo(user);
62
62
  }
63
63
 
@@ -70,7 +70,7 @@ export async function extractVideoLocation(videoUrl) {
70
70
  export async function processUrl(url) {
71
71
  if (isProfileUrl(url)) {
72
72
  const profileData = await extractUserData(url);
73
- return profileData ? [profileData] : [];
73
+ return [profileData];
74
74
  }
75
75
 
76
76
  if (isVideoUrl(url)) {
@@ -82,7 +82,6 @@ export async function processUrl(url) {
82
82
  extractVideoLocation(url),
83
83
  ]);
84
84
 
85
- if (!profileData) return [];
86
85
  return [{ ...profileData, locationCreated }];
87
86
  }
88
87