tt-help-cli-ycl 1.4.169 → 1.4.172
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-subscription-allocator.mjs +220 -0
- package/scripts/test-subscription-auto-alloc-ui.mjs +208 -0
- package/scripts/test-subscription-auto-alloc.mjs +259 -0
- package/scripts/test-subscription-center-e2e.mjs +134 -0
- package/scripts/test-subscription-center-smoke.mjs +154 -0
- package/scripts/test-subscription-center-ui.mjs +111 -0
- package/scripts/vnc-login.sh +68 -0
- package/src/cli/info.js +6 -1
- package/src/lib/browser/cdp.js +7 -1
- package/src/watch/public/app.js +713 -39
- package/src/watch/public/index.html +74 -3
- package/src/watch/subscription-allocator.js +255 -0
- package/src/watch/subscription-center.js +590 -0
- package/src/watch/watchdog-server.js +210 -0
- package/src/watchdog/agent.js +104 -10
- package/scripts/test-api-interceptor-429.mjs +0 -122
- package/scripts/test-api-interceptor-captcha.mjs +0 -176
- package/scripts/test-claim-fix.mjs +0 -216
- package/scripts/test-clash-discover.mjs +0 -126
- package/scripts/test-clash-hysteresis.mjs +0 -131
- package/scripts/test-clash-multichannel.mjs +0 -120
- package/scripts/test-client-error-monitor.mjs +0 -261
- package/scripts/test-country-fix.mjs +0 -341
- package/scripts/test-daemon-failover.mjs +0 -301
- package/scripts/test-daemon-switch-history.mjs +0 -141
- package/scripts/test-daemon-whitelist.mjs +0 -308
- package/scripts/test-explore-captcha-wait.mjs +0 -170
- package/scripts/test-photo-post.mjs +0 -156
- package/scripts/test-precheck-perf.mjs +0 -270
- package/scripts/test-precise-index.mjs +0 -114
- package/scripts/test-probe-live.mjs +0 -83
- package/scripts/test-probe.mjs +0 -315
- package/scripts/test-product-fill-cdp.mjs +0 -336
- package/scripts/test-product-fill-db.mjs +0 -216
- package/scripts/test-product-fill-http.mjs +0 -117
- package/scripts/test-proxy-switch-restart.mjs +0 -198
- package/scripts/test-refill-order.mjs +0 -218
- package/scripts/test-sample-all-fail.mjs +0 -142
- package/scripts/test-seller-any-country.mjs +0 -167
- package/scripts/test-stats-smoke.mjs +0 -43
- package/scripts/test-upgrade-spawn.mjs +0 -179
- package/scripts/test-webserver-error-classify.mjs +0 -409
- package/scripts/test-wmi-free-upgrade.mjs +0 -98
- package/src/cli/test-real-attach.js +0 -0
- package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* 冒烟测试:验证 refillJobsFromRaw 按 video_count ASC 排序
|
|
4
|
-
*
|
|
5
|
-
* 测试场景:
|
|
6
|
-
* 1. raw_jobs 中插入不同 video_count 的测试数据
|
|
7
|
-
* 2. 调用 refillJobsFromRaw 移动数据
|
|
8
|
-
* 3. 验证 jobs 中数据的顺序是 video_count 从小到大
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import Database from "better-sqlite3";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import fs from "fs";
|
|
14
|
-
import { fileURLToPath } from "url";
|
|
15
|
-
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const dbPath = path.join(__dirname, "../test-refill-order.db");
|
|
18
|
-
|
|
19
|
-
// 清理旧数据库
|
|
20
|
-
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
21
|
-
|
|
22
|
-
const db = new Database(dbPath);
|
|
23
|
-
|
|
24
|
-
// 建表
|
|
25
|
-
db.exec(`
|
|
26
|
-
CREATE TABLE raw_jobs (
|
|
27
|
-
unique_id TEXT PRIMARY KEY,
|
|
28
|
-
nickname TEXT,
|
|
29
|
-
video_count INTEGER,
|
|
30
|
-
follower_count INTEGER,
|
|
31
|
-
following_count INTEGER,
|
|
32
|
-
guessed_location TEXT,
|
|
33
|
-
sources TEXT,
|
|
34
|
-
pinned INTEGER DEFAULT 0,
|
|
35
|
-
tt_seller INTEGER DEFAULT 0,
|
|
36
|
-
verified INTEGER DEFAULT 0,
|
|
37
|
-
comment_count INTEGER DEFAULT 0,
|
|
38
|
-
location_created TEXT,
|
|
39
|
-
confirmed_location TEXT,
|
|
40
|
-
heart_count INTEGER DEFAULT 0,
|
|
41
|
-
created_at INTEGER,
|
|
42
|
-
updated_at INTEGER,
|
|
43
|
-
region TEXT,
|
|
44
|
-
signature TEXT,
|
|
45
|
-
bio_link TEXT,
|
|
46
|
-
sec_uid TEXT,
|
|
47
|
-
status_code INTEGER,
|
|
48
|
-
latest_video_time INTEGER
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
CREATE TABLE jobs (
|
|
52
|
-
unique_id TEXT PRIMARY KEY,
|
|
53
|
-
nickname TEXT,
|
|
54
|
-
status TEXT DEFAULT 'pending',
|
|
55
|
-
video_count INTEGER,
|
|
56
|
-
follower_count INTEGER,
|
|
57
|
-
following_count INTEGER,
|
|
58
|
-
guessed_location TEXT,
|
|
59
|
-
sources TEXT,
|
|
60
|
-
pinned INTEGER DEFAULT 0,
|
|
61
|
-
tt_seller INTEGER DEFAULT 0,
|
|
62
|
-
verified INTEGER DEFAULT 0,
|
|
63
|
-
comment_count INTEGER DEFAULT 0,
|
|
64
|
-
location_created TEXT,
|
|
65
|
-
confirmed_location TEXT,
|
|
66
|
-
heart_count INTEGER DEFAULT 0,
|
|
67
|
-
created_at INTEGER,
|
|
68
|
-
updated_at INTEGER,
|
|
69
|
-
region TEXT,
|
|
70
|
-
signature TEXT,
|
|
71
|
-
bio_link TEXT,
|
|
72
|
-
sec_uid TEXT,
|
|
73
|
-
status_code INTEGER,
|
|
74
|
-
latest_video_time INTEGER,
|
|
75
|
-
claimed_at INTEGER,
|
|
76
|
-
claimed_by TEXT
|
|
77
|
-
);
|
|
78
|
-
`);
|
|
79
|
-
|
|
80
|
-
// 插入测试数据:video_count 分别为 100, 5, 50, 1, 200
|
|
81
|
-
const testData = [
|
|
82
|
-
{
|
|
83
|
-
unique_id: "user_a",
|
|
84
|
-
video_count: 100,
|
|
85
|
-
follower_count: 500,
|
|
86
|
-
following_count: 100,
|
|
87
|
-
guessed_location: "DE",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
unique_id: "user_b",
|
|
91
|
-
video_count: 5,
|
|
92
|
-
follower_count: 200,
|
|
93
|
-
following_count: 50,
|
|
94
|
-
guessed_location: "DE",
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
unique_id: "user_c",
|
|
98
|
-
video_count: 50,
|
|
99
|
-
follower_count: 300,
|
|
100
|
-
following_count: 80,
|
|
101
|
-
guessed_location: "DE",
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
unique_id: "user_d",
|
|
105
|
-
video_count: 1,
|
|
106
|
-
follower_count: 100,
|
|
107
|
-
following_count: 30,
|
|
108
|
-
guessed_location: "DE",
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
unique_id: "user_e",
|
|
112
|
-
video_count: 200,
|
|
113
|
-
follower_count: 1000,
|
|
114
|
-
following_count: 200,
|
|
115
|
-
guessed_location: "DE",
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
unique_id: "user_f",
|
|
119
|
-
video_count: null,
|
|
120
|
-
follower_count: 150,
|
|
121
|
-
following_count: 40,
|
|
122
|
-
guessed_location: "DE",
|
|
123
|
-
},
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
const insertStmt = db.prepare(`
|
|
127
|
-
INSERT INTO raw_jobs (unique_id, nickname, video_count, follower_count, following_count, guessed_location, created_at, updated_at)
|
|
128
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
129
|
-
`);
|
|
130
|
-
|
|
131
|
-
const now = Date.now();
|
|
132
|
-
testData.forEach((row, i) => {
|
|
133
|
-
insertStmt.run(
|
|
134
|
-
row.unique_id,
|
|
135
|
-
`Nickname ${row.unique_id}`,
|
|
136
|
-
row.video_count,
|
|
137
|
-
row.follower_count,
|
|
138
|
-
row.following_count,
|
|
139
|
-
row.guessed_location,
|
|
140
|
-
now - i * 1000,
|
|
141
|
-
now,
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
console.error("=== 插入测试数据 ===");
|
|
146
|
-
testData.forEach((row) => {
|
|
147
|
-
console.error(` ${row.unique_id}: video_count=${row.video_count}`);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// 动态加载 createStore
|
|
151
|
-
const { createStore } = await import("../src/watch/data-store.js");
|
|
152
|
-
|
|
153
|
-
const store = createStore(dbPath);
|
|
154
|
-
|
|
155
|
-
// 调用 refillJobsFromRaw(不启用 LLM)
|
|
156
|
-
console.error("\n=== 调用 refillJobsFromRaw ===");
|
|
157
|
-
const result = store.refillJobsFromRaw(["DE"], 500, { llmScore: false });
|
|
158
|
-
console.error(` 移动了 ${result.moved} 条记录`);
|
|
159
|
-
|
|
160
|
-
// 验证 jobs 中的数据顺序
|
|
161
|
-
const jobs = db
|
|
162
|
-
.prepare(`SELECT unique_id, video_count FROM jobs ORDER BY rowid ASC`)
|
|
163
|
-
.all();
|
|
164
|
-
|
|
165
|
-
console.error("\n=== jobs 表中的数据顺序(按插入顺序 rowid)===");
|
|
166
|
-
jobs.forEach((row, i) => {
|
|
167
|
-
console.error(` ${i + 1}. ${row.unique_id}: video_count=${row.video_count}`);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// 验证排序:video_count 应该是从小到大
|
|
171
|
-
let orderCorrect = true;
|
|
172
|
-
for (let i = 1; i < jobs.length; i++) {
|
|
173
|
-
const prev = jobs[i - 1].video_count ?? 0;
|
|
174
|
-
const curr = jobs[i].video_count ?? 0;
|
|
175
|
-
if (prev > curr) {
|
|
176
|
-
orderCorrect = false;
|
|
177
|
-
console.error(
|
|
178
|
-
`\n ❌ 顺序错误: 第 ${i} 条 (${prev}) > 第 ${i + 1} 条 (${curr})`,
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 预期顺序:user_d(1), user_b(5), user_f(null→0), user_c(50), user_a(100), user_e(200)
|
|
184
|
-
// 注意:null 会被 COALESCE 为 0,所以 user_f 应该在 user_d 和 user_b 之间
|
|
185
|
-
const expectedOrder = [
|
|
186
|
-
"user_d",
|
|
187
|
-
"user_f",
|
|
188
|
-
"user_b",
|
|
189
|
-
"user_c",
|
|
190
|
-
"user_a",
|
|
191
|
-
"user_e",
|
|
192
|
-
];
|
|
193
|
-
const actualOrder = jobs.map((j) => j.unique_id);
|
|
194
|
-
|
|
195
|
-
console.error("\n=== 顺序验证 ===");
|
|
196
|
-
console.error(` 预期: ${expectedOrder.join(", ")}`);
|
|
197
|
-
console.error(` 实际: ${actualOrder.join(", ")}`);
|
|
198
|
-
|
|
199
|
-
if (JSON.stringify(expectedOrder) === JSON.stringify(actualOrder)) {
|
|
200
|
-
console.error(
|
|
201
|
-
"\n✅ 冒烟测试通过:refillJobsFromRaw 按 video_count ASC 排序正确",
|
|
202
|
-
);
|
|
203
|
-
} else {
|
|
204
|
-
console.error("\n⚠️ 顺序与预期不完全一致,但检查是否为升序...");
|
|
205
|
-
if (orderCorrect) {
|
|
206
|
-
console.error(
|
|
207
|
-
"✅ 数据是升序排列的(null 和 0 的排序可能因 SQLite 版本略有差异)",
|
|
208
|
-
);
|
|
209
|
-
} else {
|
|
210
|
-
console.error("❌ 冒烟测试失败:数据未按 video_count 升序排列");
|
|
211
|
-
process.exitCode = 1;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 清理
|
|
216
|
-
db.close();
|
|
217
|
-
fs.unlinkSync(dbPath);
|
|
218
|
-
console.error(`\n已清理测试数据库: ${dbPath}`);
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
// 验证:国家采样全失败(通道故障)→ 任务按 error 回流,不再静默 done
|
|
2
|
-
// 运行:node scripts/test-sample-all-fail.mjs
|
|
3
|
-
let pass = 0;
|
|
4
|
-
let fail = 0;
|
|
5
|
-
function check(name, ok, detail = "") {
|
|
6
|
-
if (ok) {
|
|
7
|
-
pass++;
|
|
8
|
-
console.log(`✓ ${name}${detail ? `: ${detail}` : ""}`);
|
|
9
|
-
} else {
|
|
10
|
-
fail++;
|
|
11
|
-
console.error(`✗ ${name}${detail ? `: ${detail}` : ""}`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// [1] 全失败判定(等价于实现中 Promise.all + sampleFailCount 计数)
|
|
16
|
-
{
|
|
17
|
-
const sampleUrls = ["u1", "u2", "u3", "u4", "u5"];
|
|
18
|
-
// 模拟通道故障:每个 fetch 都抛错 → catch 内计数 +1,返回 null
|
|
19
|
-
let sampleFailCount = 0;
|
|
20
|
-
const locations = await Promise.all(
|
|
21
|
-
sampleUrls.map(async () => {
|
|
22
|
-
try {
|
|
23
|
-
throw new Error("fetch failed");
|
|
24
|
-
} catch {
|
|
25
|
-
sampleFailCount++;
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}),
|
|
29
|
-
);
|
|
30
|
-
const allFailed =
|
|
31
|
-
sampleUrls.length > 0 && sampleFailCount === sampleUrls.length;
|
|
32
|
-
check(
|
|
33
|
-
"全失败判定:5/5 fetch 抛错 → allFailed=true",
|
|
34
|
-
allFailed === true && locations.every((l) => l === null),
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// [2] 部分失败判定:2/5 失败 → allFailed=false(保持众数逻辑)
|
|
39
|
-
{
|
|
40
|
-
const sampleUrls = ["u1", "u2", "u3", "u4", "u5"];
|
|
41
|
-
let sampleFailCount = 0;
|
|
42
|
-
const locations = await Promise.all(
|
|
43
|
-
sampleUrls.map(async (u) => {
|
|
44
|
-
try {
|
|
45
|
-
if (u === "u1" || u === "u2") throw new Error("timeout");
|
|
46
|
-
return "TH";
|
|
47
|
-
} catch {
|
|
48
|
-
sampleFailCount++;
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}),
|
|
52
|
-
);
|
|
53
|
-
const allFailed =
|
|
54
|
-
sampleUrls.length > 0 && sampleFailCount === sampleUrls.length;
|
|
55
|
-
check(
|
|
56
|
-
"部分失败判定:2/5 失败 → allFailed=false(走众数)",
|
|
57
|
-
allFailed === false && locations.filter(Boolean).length === 3,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// [3] 全成功但无数据判定:sampleFailCount=0 → 不转 error(保持 done 无国家回炉机制)
|
|
62
|
-
{
|
|
63
|
-
const sampleUrls = ["u1", "u2", "u3"];
|
|
64
|
-
let sampleFailCount = 0;
|
|
65
|
-
const locations = await Promise.all(
|
|
66
|
-
sampleUrls.map(async () => {
|
|
67
|
-
const data = { locationCreated: null }; // 成功响应但无字段
|
|
68
|
-
return data?.locationCreated || null;
|
|
69
|
-
}),
|
|
70
|
-
);
|
|
71
|
-
const allFailed =
|
|
72
|
-
sampleUrls.length > 0 && sampleFailCount === sampleUrls.length;
|
|
73
|
-
check(
|
|
74
|
-
"全成功无数据:failCount=0 → 不转 error(done 无国家,2h 回炉兜底)",
|
|
75
|
-
allFailed === false && locations.every((l) => l === null),
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// [4] 不可达 webserver → fetch 必抛错(通道故障可稳定复现)
|
|
80
|
-
{
|
|
81
|
-
let fetchThrows = false;
|
|
82
|
-
try {
|
|
83
|
-
await fetch("http://127.0.0.1:1/api/tiktok/video", {
|
|
84
|
-
method: "POST",
|
|
85
|
-
signal: AbortSignal.timeout(2000),
|
|
86
|
-
});
|
|
87
|
-
} catch {
|
|
88
|
-
fetchThrows = true;
|
|
89
|
-
}
|
|
90
|
-
check(
|
|
91
|
-
"不可达 webserver → fetch 抛错(通道故障可复现)",
|
|
92
|
-
fetchThrows === true,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// [5] 真实 processExplore:page mock 让 collectVideos 走不通(API 拦截失败抛错)
|
|
97
|
-
// 无论命中哪条错误路径,都不应出现"processed=true 且无 error 且无国家"的静默 done
|
|
98
|
-
{
|
|
99
|
-
const fakePage = {
|
|
100
|
-
async evaluate() {
|
|
101
|
-
return {};
|
|
102
|
-
},
|
|
103
|
-
async goto() {},
|
|
104
|
-
on() {
|
|
105
|
-
return this;
|
|
106
|
-
},
|
|
107
|
-
off() {
|
|
108
|
-
return this;
|
|
109
|
-
},
|
|
110
|
-
url: () => "https://www.tiktok.com/@test",
|
|
111
|
-
waitForResponse() {
|
|
112
|
-
return Promise.reject(new Error("timeout"));
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
const { processExplore } = await import("../src/scraper/explore-core.js");
|
|
116
|
-
const result = await processExplore(
|
|
117
|
-
fakePage,
|
|
118
|
-
"testuser",
|
|
119
|
-
{
|
|
120
|
-
maxVideos: 1,
|
|
121
|
-
enableFollow: false,
|
|
122
|
-
loggedIn: false,
|
|
123
|
-
mini: false,
|
|
124
|
-
webserverUrl: "http://127.0.0.1:1",
|
|
125
|
-
location: "TH",
|
|
126
|
-
},
|
|
127
|
-
() => {},
|
|
128
|
-
);
|
|
129
|
-
const silentDone =
|
|
130
|
-
result.processed === true &&
|
|
131
|
-
!result.error &&
|
|
132
|
-
!result.noVideo &&
|
|
133
|
-
!result.locationCreated;
|
|
134
|
-
check(
|
|
135
|
-
"通道故障下不出现静默 done",
|
|
136
|
-
silentDone === false,
|
|
137
|
-
`error=${result.error ? `"${result.error.slice(0, 40)}"` : "无"}`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
console.error(`\n结果: ${pass} 通过, ${fail} 失败`);
|
|
142
|
-
process.exit(fail > 0 ? 1 : 0);
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 商家任务国家放开领取测试(claimNextJob 分支顺序)
|
|
3
|
-
*
|
|
4
|
-
* 验证(临时 SQLite 库):
|
|
5
|
-
* 1. 传了国家子集:先领对应国家的 ttseller,领完再领任意国家 ttseller
|
|
6
|
-
* 2. 未传国家:领商家(任意国家均可)
|
|
7
|
-
* 3. 其他来源级顺序不变:seed 仍排商家前,非商家任务仍按国家分层
|
|
8
|
-
* 4. 未登录客户端不领商家任务(保持原逻辑)
|
|
9
|
-
*
|
|
10
|
-
* 运行: TMPDIR=$(mktemp -d) node scripts/test-seller-any-country.mjs
|
|
11
|
-
*/
|
|
12
|
-
import { createStore } from "../src/watch/data-store.js";
|
|
13
|
-
import { getDb } from "../src/watch/db-schema.js";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import { mkdtempSync } from "node:fs";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
|
-
|
|
18
|
-
const tmp = mkdtempSync(join(tmpdir(), "seller-any-"));
|
|
19
|
-
const store = createStore(join(tmp, "test.db"));
|
|
20
|
-
const db = getDb();
|
|
21
|
-
if (!db) {
|
|
22
|
-
console.error("❌ db 未初始化");
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let passed = 0;
|
|
27
|
-
function check(name, fn) {
|
|
28
|
-
try {
|
|
29
|
-
fn();
|
|
30
|
-
passed++;
|
|
31
|
-
console.log(` ✅ ${name}`);
|
|
32
|
-
} catch (e) {
|
|
33
|
-
console.error(` ❌ ${name}\n ${e.message}`);
|
|
34
|
-
process.exitCode = 1;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function assert(cond, msg = "断言失败") {
|
|
38
|
-
if (!cond) throw new Error(msg);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const now = Date.now();
|
|
42
|
-
// 直接插 task_queue(refill 后形态)
|
|
43
|
-
const insert = (id, loc, seller, sources, opts = {}) =>
|
|
44
|
-
db
|
|
45
|
-
.prepare(
|
|
46
|
-
`INSERT INTO task_queue (unique_id, status, pinned, guessed_location, tt_seller, verified, sources, video_count, follower_count, created_at, updated_at)
|
|
47
|
-
VALUES (?, 'pending', 0, ?, ?, 0, ?, ?, ?, ?, ?)`,
|
|
48
|
-
)
|
|
49
|
-
.run(
|
|
50
|
-
id,
|
|
51
|
-
loc,
|
|
52
|
-
seller,
|
|
53
|
-
JSON.stringify(sources),
|
|
54
|
-
opts.videoCount ?? 3,
|
|
55
|
-
opts.followers ?? 100,
|
|
56
|
-
now,
|
|
57
|
-
now,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
console.log("===== 商家任务国家放开领取测试 =====");
|
|
61
|
-
|
|
62
|
-
// ── 场景 1:传 US,队列里有 US 商家 + DE 商家 ──
|
|
63
|
-
console.log("\n[1] 传国家子集 US:US 商家优先于 DE 商家");
|
|
64
|
-
insert("seller_de", "DE", 1, ["following"]);
|
|
65
|
-
insert("seller_us", "US", 1, ["following"]);
|
|
66
|
-
{
|
|
67
|
-
const job = await store.claimNextJob("t1", 5 * 60 * 1000, ["US"], true);
|
|
68
|
-
check("先领到 US 商家", () =>
|
|
69
|
-
assert(job?.uniqueId === "seller_us", `实际: ${job?.uniqueId}`),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── 场景 2:US 商家已不在(被领走),传 US → 兜任意国家 DE 商家 ──
|
|
74
|
-
console.log("\n[2] 无 US 商家可领时:兜到任意国家商家(DE)");
|
|
75
|
-
{
|
|
76
|
-
// seller_us 已被 t1 领走(processing),队列只剩 DE 商家 pending
|
|
77
|
-
const job = await store.claimNextJob("t2", 5 * 60 * 1000, ["US"], true);
|
|
78
|
-
check("兜到 DE 商家", () =>
|
|
79
|
-
assert(job?.uniqueId === "seller_de", `实际: ${job?.uniqueId}`),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── 场景 3:传 US 但队列只有 FR 商家 → 领 FR ──
|
|
84
|
-
console.log("\n[3] 传 US 但无 US 商家 → 领 FR 商家(任意国家兜底)");
|
|
85
|
-
db.prepare(`DELETE FROM task_queue`).run();
|
|
86
|
-
insert("seller_fr", "FR", 1, ["following"]);
|
|
87
|
-
{
|
|
88
|
-
const job = await store.claimNextJob("t3", 5 * 60 * 1000, ["US"], true);
|
|
89
|
-
check("领到 FR 商家", () =>
|
|
90
|
-
assert(job?.uniqueId === "seller_fr", `实际: ${job?.uniqueId}`),
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ── 场景 4:空位置商家(guessed_location='')→ 任意国家分支领到 ──
|
|
95
|
-
console.log("\n[4] 空位置商家可被任意国家分支领到");
|
|
96
|
-
db.prepare(`DELETE FROM task_queue`).run();
|
|
97
|
-
insert("seller_noloc", "", 1, ["following"]);
|
|
98
|
-
{
|
|
99
|
-
const job = await store.claimNextJob("t4", 5 * 60 * 1000, ["US"], true);
|
|
100
|
-
check("领到空位置商家", () =>
|
|
101
|
-
assert(job?.uniqueId === "seller_noloc", `实际: ${job?.uniqueId}`),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ── 场景 5:未传国家,队列只有配置外国商家 → 直接领到 ──
|
|
106
|
-
console.log("\n[5] 未传国家:配置外国商家(FR)也能领到");
|
|
107
|
-
db.prepare(`DELETE FROM task_queue`).run();
|
|
108
|
-
insert("seller_fr2", "FR", 1, ["following"]);
|
|
109
|
-
{
|
|
110
|
-
const job = await store.claimNextJob("t5", 5 * 60 * 1000, null, true);
|
|
111
|
-
check("领到 FR 商家(未传国家不再被挡)", () =>
|
|
112
|
-
assert(job?.uniqueId === "seller_fr2", `实际: ${job?.uniqueId}`),
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── 场景 6:seed 仍优先于商家(顺序不变)──
|
|
117
|
-
console.log("\n[6] seed 优先级仍高于商家");
|
|
118
|
-
db.prepare(`DELETE FROM task_queue`).run();
|
|
119
|
-
insert("seed_pl", "PL", 0, ["seed"]);
|
|
120
|
-
insert("seller_xx", "XX", 1, ["following"]);
|
|
121
|
-
{
|
|
122
|
-
const job = await store.claimNextJob("t6", 5 * 60 * 1000, null, true);
|
|
123
|
-
check("先领 seed", () =>
|
|
124
|
-
assert(job?.uniqueId === "seed_pl", `实际: ${job?.uniqueId}`),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ── 场景 7:非商家任务仍按国家分层(PL 配置内优先于 ZZ 配置外)──
|
|
129
|
-
console.log("\n[7] 商家清空后:非商家按国家顺序领取");
|
|
130
|
-
db.prepare(`DELETE FROM task_queue`).run();
|
|
131
|
-
insert("other_pl", "PL", 0, ["search"]);
|
|
132
|
-
insert("other_zz", "ZZ", 0, ["search"]);
|
|
133
|
-
{
|
|
134
|
-
const job = await store.claimNextJob("t7", 5 * 60 * 1000, null, true);
|
|
135
|
-
check("PL 非商家先于 ZZ(配置内优先)", () =>
|
|
136
|
-
assert(job?.uniqueId === "other_pl", `实际: ${job?.uniqueId}`),
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ── 场景 8:未登录客户端不领商家(原逻辑不变)──
|
|
141
|
-
console.log("\n[8] 未登录不领商家任务");
|
|
142
|
-
db.prepare(`DELETE FROM task_queue`).run();
|
|
143
|
-
insert("seller_only", "US", 1, ["following"]);
|
|
144
|
-
insert("other_task", "PL", 0, ["search"]);
|
|
145
|
-
{
|
|
146
|
-
const job = await store.claimNextJob("t8", 5 * 60 * 1000, null, false);
|
|
147
|
-
check("未登录跳过商家领普通任务", () =>
|
|
148
|
-
assert(job?.uniqueId === "other_task", `实际: ${job?.uniqueId}`),
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── 场景 9:非商家任务不享受任意国家兜底(配置内先于配置外)──
|
|
153
|
-
console.log("\n[9] 非商家配置外国家不越级");
|
|
154
|
-
db.prepare(`DELETE FROM task_queue`).run();
|
|
155
|
-
insert("plain_de", "DE", 0, ["search"]); // DE 在默认配置内
|
|
156
|
-
insert("plain_zz", "ZZ", 0, ["search"]); // ZZ 不在配置
|
|
157
|
-
{
|
|
158
|
-
const job = await store.claimNextJob("t9", 5 * 60 * 1000, null, true);
|
|
159
|
-
check("DE(配置内)先于 ZZ(配置外)", () =>
|
|
160
|
-
assert(job?.uniqueId === "plain_de", `实际: ${job?.uniqueId}`),
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
store.stopBackup?.();
|
|
165
|
-
console.log(
|
|
166
|
-
`\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
|
|
167
|
-
);
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// /api/stats 冒烟:fast 组直接含 targetUsers/preciseMerchants(烟花 30s 级)
|
|
2
|
-
// 用完即删
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { createStore } from "../src/watch/data-store.js";
|
|
5
|
-
import { getDb } from "../src/watch/db-schema.js";
|
|
6
|
-
import { startWatchServer } from "../src/watch/server.js";
|
|
7
|
-
|
|
8
|
-
const TMP = "/tmp/tk-stats-smoke";
|
|
9
|
-
fs.rmSync(TMP, { recursive: true, force: true });
|
|
10
|
-
fs.mkdirSync(TMP, { recursive: true });
|
|
11
|
-
const DB = `${TMP}/t.db`;
|
|
12
|
-
const PORT = 3199;
|
|
13
|
-
|
|
14
|
-
const store = createStore(DB, {});
|
|
15
|
-
const db = getDb();
|
|
16
|
-
db.exec("ALTER TABLE jobs ADD COLUMN product_skus TEXT");
|
|
17
|
-
db.exec(`INSERT INTO jobs (unique_id, status, tt_seller, verified, location_created,
|
|
18
|
-
top_video_seo_url, product_review_count, product_sold_count, recent_ec_videos, follower_count)
|
|
19
|
-
VALUES ('m1','done',1,0,'DE','/x',0,20,'[{"playCount":3000}]',100)`);
|
|
20
|
-
db.exec(`INSERT INTO jobs (unique_id, status, tt_seller, verified, location_created, follower_count)
|
|
21
|
-
VALUES ('plain','done',1,0,'BR',50)`);
|
|
22
|
-
|
|
23
|
-
await startWatchServer(DB, PORT, store, { llmDiscover: false }, null);
|
|
24
|
-
|
|
25
|
-
let ok = false;
|
|
26
|
-
for (let i = 0; i < 20 && !ok; i++) {
|
|
27
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(`http://127.0.0.1:${PORT}/api/stats`);
|
|
30
|
-
const s = await res.json();
|
|
31
|
-
if (s && typeof s.targetUsers !== "undefined") {
|
|
32
|
-
ok = true;
|
|
33
|
-
console.log("targetUsers =", s.targetUsers, s.targetUsers === 1 ? "✅" : "❌ 应为 1");
|
|
34
|
-
console.log("preciseMerchants=", s.preciseMerchants, s.preciseMerchants === 1 ? "✅" : "❌ 应为 1");
|
|
35
|
-
console.log("processedUsers =", s.processedUsers, "(slow 组 jobsAgg 未就绪时可为 0,5 分钟内后台补全)");
|
|
36
|
-
const pass = s.targetUsers === 1 && s.preciseMerchants === 1;
|
|
37
|
-
console.log(pass ? "SMOKE PASS" : "SMOKE FAIL");
|
|
38
|
-
process.exit(pass ? 0 : 1);
|
|
39
|
-
}
|
|
40
|
-
} catch {}
|
|
41
|
-
}
|
|
42
|
-
console.log("SMOKE FAIL: /api/stats 未响应");
|
|
43
|
-
process.exit(1);
|