tt-help-cli-ycl 1.4.145 → 1.4.146
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/test-product-fill-cdp.mjs +168 -0
- package/src/cli/comments.js +265 -388
- package/src/cli/product-fill.js +255 -128
- package/src/cli/tag.js +43 -9
- package/src/lib/args.js +15 -5
- package/src/lib/constants.js +8 -5
- package/src/lib/shop-scraper.mjs +69 -6
- package/src/watch/data-store.js +41 -6
- package/src/watch/public/app.js +58 -0
- package/src/watch/public/index.html +46 -1
- package/src/watch/server.js +4 -1
- package/scripts/test-product-fill.mjs +0 -306
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* product-fill CDP 模式 + 空数据防御测试
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* 1. isEmptyProductDetail 全空判定
|
|
6
|
+
* 2. parseProductHtml 空壳页抛可重试错误(防全 null 当成功回写)
|
|
7
|
+
* 3. parseProductHtml 正常 PDP 页面解析
|
|
8
|
+
* 4. parseProductHtml 验证码页抛 captcha 错误
|
|
9
|
+
* 5. parseArgs --cdp 参数解析(HTTP 模式默认不启用)
|
|
10
|
+
*
|
|
11
|
+
* 运行: node scripts/test-product-fill-cdp.mjs
|
|
12
|
+
*/
|
|
13
|
+
import assert from "node:assert";
|
|
14
|
+
import {
|
|
15
|
+
isEmptyProductDetail,
|
|
16
|
+
parseProductHtml,
|
|
17
|
+
} from "../src/lib/shop-scraper.mjs";
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
function check(name, fn) {
|
|
21
|
+
try {
|
|
22
|
+
fn();
|
|
23
|
+
passed++;
|
|
24
|
+
console.log(` ✅ ${name}`);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(` ❌ ${name}\n ${e.message}`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 最小可解析的 PDP HTML:含 product_model / sold_count / review_ratings 标识
|
|
32
|
+
const VALID_HTML =
|
|
33
|
+
'<html><body><script>{"product_model":{"name":"Test Product","product_id":"123456","seller_id":"789"},"sold_count":"296","review_ratings":{"overall_score":4.7,"review_count":"3303","rating_result":[5,4,3,2,1]}}</script></body></html>';
|
|
34
|
+
|
|
35
|
+
console.log("\n[1] isEmptyProductDetail 全空判定");
|
|
36
|
+
check("全 null → true", () => {
|
|
37
|
+
assert.strictEqual(
|
|
38
|
+
isEmptyProductDetail({
|
|
39
|
+
rating: null,
|
|
40
|
+
reviewCount: null,
|
|
41
|
+
soldCount: null,
|
|
42
|
+
productName: null,
|
|
43
|
+
ratingBreakdown: null,
|
|
44
|
+
productId: null,
|
|
45
|
+
sellerId: null,
|
|
46
|
+
}),
|
|
47
|
+
true,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
check("缺失字段(undefined)→ true", () =>
|
|
51
|
+
assert.strictEqual(isEmptyProductDetail({}), true),
|
|
52
|
+
);
|
|
53
|
+
check("有 rating → false", () =>
|
|
54
|
+
assert.strictEqual(isEmptyProductDetail({ rating: 4.7 }), false),
|
|
55
|
+
);
|
|
56
|
+
check("有 soldCount → false", () =>
|
|
57
|
+
assert.strictEqual(isEmptyProductDetail({ soldCount: 296 }), false),
|
|
58
|
+
);
|
|
59
|
+
check("null/undefined 混入 → true", () =>
|
|
60
|
+
assert.strictEqual(
|
|
61
|
+
isEmptyProductDetail({ rating: null, soldCount: undefined }),
|
|
62
|
+
true,
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
console.log("\n[2] parseProductHtml 空壳页抛可重试错误");
|
|
67
|
+
check("纯空 HTML → 抛 Empty product page", () => {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => parseProductHtml("<html><body></body></html>"),
|
|
70
|
+
/Empty product page/,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
check("风控降级页(无商品标识)→ 抛 Empty product page", () => {
|
|
74
|
+
assert.throws(
|
|
75
|
+
() =>
|
|
76
|
+
parseProductHtml(
|
|
77
|
+
"<html><head><title>TikTok Shop</title></head><body>Please verify you are a human</body></html>",
|
|
78
|
+
),
|
|
79
|
+
/Empty product page/,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log("\n[3] parseProductHtml 正常 PDP 页面解析");
|
|
84
|
+
const detail = parseProductHtml(VALID_HTML);
|
|
85
|
+
check("rating 解析正确", () => assert.strictEqual(detail.rating, 4.7));
|
|
86
|
+
check("reviewCount 解析正确", () =>
|
|
87
|
+
assert.strictEqual(detail.reviewCount, 3303),
|
|
88
|
+
);
|
|
89
|
+
check("soldCount 解析正确", () => assert.strictEqual(detail.soldCount, 296));
|
|
90
|
+
check("productName 解析正确", () =>
|
|
91
|
+
assert.strictEqual(detail.productName, "Test Product"),
|
|
92
|
+
);
|
|
93
|
+
check("productId/sellerId 解析正确", () => {
|
|
94
|
+
assert.strictEqual(detail.productId, "123456");
|
|
95
|
+
assert.strictEqual(detail.sellerId, "789");
|
|
96
|
+
});
|
|
97
|
+
check("解析结果非全空", () =>
|
|
98
|
+
assert.strictEqual(isEmptyProductDetail(detail), false),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
console.log("\n[4] parseProductHtml 验证码页抛 captcha 错误");
|
|
102
|
+
check("captcha-verify-container → 抛 captcha", () => {
|
|
103
|
+
assert.throws(
|
|
104
|
+
() =>
|
|
105
|
+
parseProductHtml(
|
|
106
|
+
'<html><body><div class="captcha-verify-container"></div></body></html>',
|
|
107
|
+
),
|
|
108
|
+
/Blocked by TikTok/,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
check("Security Check 标题 → 抛 captcha", () => {
|
|
112
|
+
assert.throws(
|
|
113
|
+
() => parseProductHtml("<html><title>Security Check</title></html>"),
|
|
114
|
+
/Blocked by TikTok/,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log("\n[5] parseArgs --profiles 参数解析");
|
|
119
|
+
const { parseArgs } = await import("../src/lib/args.js");
|
|
120
|
+
const origArgv = process.argv;
|
|
121
|
+
try {
|
|
122
|
+
process.argv = [
|
|
123
|
+
"node",
|
|
124
|
+
"tt-help",
|
|
125
|
+
"product-fill",
|
|
126
|
+
"--profiles",
|
|
127
|
+
"us/9223",
|
|
128
|
+
"-l",
|
|
129
|
+
"3",
|
|
130
|
+
];
|
|
131
|
+
const parsed = parseArgs();
|
|
132
|
+
check("单对国家小写转大写", () =>
|
|
133
|
+
assert.deepStrictEqual(parsed.productFillProfiles, [
|
|
134
|
+
{ country: "US", port: 9223 },
|
|
135
|
+
]),
|
|
136
|
+
);
|
|
137
|
+
check("subcommand = product-fill", () =>
|
|
138
|
+
assert.strictEqual(parsed.subcommand, "product-fill"),
|
|
139
|
+
);
|
|
140
|
+
check("-l 3 生效", () => assert.strictEqual(parsed.productFillLimit, 3));
|
|
141
|
+
|
|
142
|
+
process.argv = [
|
|
143
|
+
"node",
|
|
144
|
+
"tt-help",
|
|
145
|
+
"product-fill",
|
|
146
|
+
"--profiles",
|
|
147
|
+
"US/9223,JP/1222",
|
|
148
|
+
];
|
|
149
|
+
const parsedMulti = parseArgs();
|
|
150
|
+
check("多对解析", () =>
|
|
151
|
+
assert.deepStrictEqual(parsedMulti.productFillProfiles, [
|
|
152
|
+
{ country: "US", port: 9223 },
|
|
153
|
+
{ country: "JP", port: 1222 },
|
|
154
|
+
]),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
process.argv = ["node", "tt-help", "product-fill", "--profiles", "us/abc,xx"];
|
|
158
|
+
const parsedBad = parseArgs();
|
|
159
|
+
check("非法段被丢弃", () =>
|
|
160
|
+
assert.deepStrictEqual(parsedBad.productFillProfiles, []),
|
|
161
|
+
);
|
|
162
|
+
} finally {
|
|
163
|
+
process.argv = origArgv;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(
|
|
167
|
+
`\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
|
|
168
|
+
);
|