tt-help-cli-ycl 1.3.48 → 1.3.50
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/README.md +33 -33
- package/cli.js +9 -9
- package/package.json +52 -52
- package/scripts/run-explore copy.bat +101 -101
- package/scripts/run-explore.bat +134 -134
- package/scripts/run-explore.ps1 +159 -159
- package/scripts/run-explore.sh +121 -121
- package/scripts/test-captcha-lib.mjs +68 -0
- package/scripts/test-captcha.mjs +81 -0
- package/scripts/test-incognito-lib.mjs +36 -0
- package/scripts/test-login-state.mjs +128 -0
- package/scripts/test-safe-click.mjs +45 -0
- package/scripts/test-watch-db-smoke.mjs +246 -0
- package/src/cli/attach.js +331 -331
- package/src/cli/auto.js +265 -265
- package/src/cli/comments.js +620 -620
- package/src/cli/config.js +170 -170
- package/src/cli/db-import.js +51 -51
- package/src/cli/explore.js +555 -555
- package/src/cli/open.js +109 -111
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +288 -288
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/videostats.js +196 -196
- package/src/cli/watch.js +30 -30
- package/src/lib/api-interceptor.js +161 -161
- package/src/lib/args.js +809 -809
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +261 -261
- package/src/lib/browser/health-checker.js +114 -114
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +184 -184
- package/src/lib/constants.js +297 -297
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/output.js +80 -80
- package/src/lib/page-error-detector.js +109 -109
- package/src/lib/parse-ssr.mjs +69 -69
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +90 -90
- package/src/lib/target-locations.js +61 -61
- package/src/lib/tiktok-scraper.mjs +98 -61
- package/src/lib/url.js +52 -52
- package/src/main.js +73 -73
- package/src/npm-main.js +70 -70
- package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
- package/src/scraper/auto-core.js +203 -203
- package/src/scraper/core.js +255 -255
- package/src/scraper/explore-core.js +208 -208
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/follow-extractor.js +250 -250
- package/src/scraper/modules/guess-extractor.js +51 -51
- package/src/scraper/modules/page-helpers.js +48 -48
- package/src/scraper/refresh-core.js +213 -213
- package/src/videos/core.js +143 -143
- package/src/watch/data-store.js +2980 -2980
- package/src/watch/public/index.html +2355 -2355
- package/src/watch/server.js +727 -727
package/src/cli/open.js
CHANGED
|
@@ -1,111 +1,109 @@
|
|
|
1
|
-
import { ensureBrowserReady, killEdgeProcesses } from "../lib/browser/cdp.js";
|
|
2
|
-
import { getOrCreatePage } from "../lib/browser/page.js";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
|
|
6
|
-
const BASE_PORT = 9222;
|
|
7
|
-
const TOTAL_ACCOUNTS = 10;
|
|
8
|
-
|
|
9
|
-
export async function handleOpen(parsed) {
|
|
10
|
-
const { openPort, openList } = parsed;
|
|
11
|
-
|
|
12
|
-
if (openList) {
|
|
13
|
-
console.error("内置浏览器配置:");
|
|
14
|
-
for (let i = 0; i < TOTAL_ACCOUNTS; i++) {
|
|
15
|
-
const port = BASE_PORT + i;
|
|
16
|
-
const profile = `p${port}`;
|
|
17
|
-
const userDataDir = path.join(
|
|
18
|
-
os.homedir(),
|
|
19
|
-
"Library",
|
|
20
|
-
"Application Support",
|
|
21
|
-
`Microsoft Edge For Testing_${profile}`,
|
|
22
|
-
);
|
|
23
|
-
console.error(
|
|
24
|
-
` open ${port} → profile: ${profile}, userDataDir: ${userDataDir}`,
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
console.error("");
|
|
28
|
-
console.error("用法: tt-help open <端口>");
|
|
29
|
-
console.error("示例: tt-help open 9222");
|
|
30
|
-
process.exit(0);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!openPort) {
|
|
34
|
-
console.error("用法: tt-help open <端口>");
|
|
35
|
-
console.error("示例: tt-help open 9222");
|
|
36
|
-
console.error("");
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
console.error(
|
|
57
|
-
console.error(
|
|
58
|
-
console.error("");
|
|
59
|
-
console.error("
|
|
60
|
-
console.error("
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.error("");
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
}
|
|
1
|
+
import { ensureBrowserReady, killEdgeProcesses } from "../lib/browser/cdp.js";
|
|
2
|
+
import { getOrCreatePage } from "../lib/browser/page.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
const BASE_PORT = 9222;
|
|
7
|
+
const TOTAL_ACCOUNTS = 10;
|
|
8
|
+
|
|
9
|
+
export async function handleOpen(parsed) {
|
|
10
|
+
const { openPort, openList } = parsed;
|
|
11
|
+
|
|
12
|
+
if (openList) {
|
|
13
|
+
console.error("内置浏览器配置:");
|
|
14
|
+
for (let i = 0; i < TOTAL_ACCOUNTS; i++) {
|
|
15
|
+
const port = BASE_PORT + i;
|
|
16
|
+
const profile = `p${port}`;
|
|
17
|
+
const userDataDir = path.join(
|
|
18
|
+
os.homedir(),
|
|
19
|
+
"Library",
|
|
20
|
+
"Application Support",
|
|
21
|
+
`Microsoft Edge For Testing_${profile}`,
|
|
22
|
+
);
|
|
23
|
+
console.error(
|
|
24
|
+
` open ${port} → profile: ${profile}, userDataDir: ${userDataDir}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
console.error("");
|
|
28
|
+
console.error("用法: tt-help open <端口>");
|
|
29
|
+
console.error("示例: tt-help open 9222");
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!openPort) {
|
|
34
|
+
console.error("用法: tt-help open <端口>");
|
|
35
|
+
console.error("示例: tt-help open 9222");
|
|
36
|
+
console.error('运行 "tt-help open --list" 查看所有内置配置');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const port = parseInt(openPort);
|
|
41
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
42
|
+
console.error(`端口 ${openPort} 无效,请输入 1-65535 之间的端口号`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const profile = `p${port}`;
|
|
47
|
+
const userDataDir = path.join(
|
|
48
|
+
os.homedir(),
|
|
49
|
+
"Library",
|
|
50
|
+
"Application Support",
|
|
51
|
+
`Microsoft Edge For Testing_${profile}`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
console.error(`正在启动浏览器... 端口: ${port}, profile: ${profile}`);
|
|
55
|
+
console.error(`userDataDir: ${userDataDir}`);
|
|
56
|
+
console.error("");
|
|
57
|
+
console.error("启动后请在浏览器中登录 TikTok 账户");
|
|
58
|
+
console.error("登录完成后关闭浏览器即可,下次 open 会保留登录状态");
|
|
59
|
+
console.error("");
|
|
60
|
+
console.error("按 Ctrl+C 退出并关闭浏览器");
|
|
61
|
+
|
|
62
|
+
let browser = null;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const cdpOptions = {};
|
|
66
|
+
cdpOptions.port = port;
|
|
67
|
+
cdpOptions.userDataDir = userDataDir;
|
|
68
|
+
|
|
69
|
+
browser = await ensureBrowserReady(cdpOptions);
|
|
70
|
+
|
|
71
|
+
// Ctrl+C 关闭浏览器
|
|
72
|
+
process.on("SIGINT", async () => {
|
|
73
|
+
console.error("\n正在关闭浏览器...");
|
|
74
|
+
try {
|
|
75
|
+
await killEdgeProcesses(userDataDir);
|
|
76
|
+
await browser.close();
|
|
77
|
+
} catch {
|
|
78
|
+
/* ignore */
|
|
79
|
+
}
|
|
80
|
+
console.error("浏览器已关闭");
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
console.error("");
|
|
85
|
+
console.error("浏览器已启动并连接成功");
|
|
86
|
+
console.error("TikTok 页面将在浏览器中自动打开...");
|
|
87
|
+
|
|
88
|
+
// 打开 TikTok 首页
|
|
89
|
+
try {
|
|
90
|
+
const page = await getOrCreatePage(browser);
|
|
91
|
+
if (page) {
|
|
92
|
+
await page.goto("https://www.tiktok.com", {
|
|
93
|
+
waitUntil: "domcontentloaded",
|
|
94
|
+
timeout: 30000,
|
|
95
|
+
});
|
|
96
|
+
console.error("已打开 https://www.tiktok.com");
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error(`打开页面时出错: ${e.message}`);
|
|
100
|
+
console.error("浏览器已启动,可以手动在浏览器中打开 TikTok");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 保持进程运行
|
|
104
|
+
await new Promise(() => {});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(`启动失败: ${err.message}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/cli/progress.js
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
import { writeFileSync } from 'fs';
|
|
2
|
-
import { formatOutput } from '../lib/output.js';
|
|
3
|
-
import { deduplicate } from '../lib/output.js';
|
|
4
|
-
import { applyFilter, formatFilterDescription } from '../lib/filter.js';
|
|
5
|
-
import { calculateConcurrency, createMultiProgressBars, renderMultiProgressBars, clearProgressBars } from '../lib/io.js';
|
|
6
|
-
import { randomDelay } from '../lib/delay.js';
|
|
7
|
-
|
|
8
|
-
export async function processUrlsWithProgress({
|
|
9
|
-
urls,
|
|
10
|
-
proxyUrl,
|
|
11
|
-
outputFile,
|
|
12
|
-
outputFormat,
|
|
13
|
-
filter,
|
|
14
|
-
processFn,
|
|
15
|
-
label = '数据',
|
|
16
|
-
}) {
|
|
17
|
-
const allResults = [];
|
|
18
|
-
const errors = [];
|
|
19
|
-
|
|
20
|
-
if (urls.length === 0) {
|
|
21
|
-
console.error('\n未获取到数据');
|
|
22
|
-
if (outputFile) writeFileSync(outputFile, '[]', 'utf-8');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const concurrency = calculateConcurrency(urls.length);
|
|
27
|
-
const bars = createMultiProgressBars(concurrency);
|
|
28
|
-
|
|
29
|
-
const slots = Array.from({ length: concurrency }, () => []);
|
|
30
|
-
urls.forEach((url, i) => slots[i % concurrency].push(url));
|
|
31
|
-
|
|
32
|
-
bars.forEach((bar, i) => {
|
|
33
|
-
bar.total = slots[i].length;
|
|
34
|
-
bar.status = slots[i].length > 0 ? 'running' : 'done';
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
renderMultiProgressBars(bars);
|
|
38
|
-
|
|
39
|
-
const workers = slots.map(async (slotUrls, slotIndex) => {
|
|
40
|
-
for (const url of slotUrls) {
|
|
41
|
-
bars[slotIndex].url = url;
|
|
42
|
-
renderMultiProgressBars(bars);
|
|
43
|
-
|
|
44
|
-
await randomDelay();
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const results = await processFn(url, proxyUrl);
|
|
48
|
-
allResults.push(...results);
|
|
49
|
-
bars[slotIndex].current++;
|
|
50
|
-
bars[slotIndex].status = 'running';
|
|
51
|
-
} catch (err) {
|
|
52
|
-
errors.push({ url, message: err.message });
|
|
53
|
-
bars[slotIndex].current++;
|
|
54
|
-
bars[slotIndex].status = 'error';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
renderMultiProgressBars(bars);
|
|
58
|
-
}
|
|
59
|
-
bars[slotIndex].status = bars[slotIndex].current === bars[slotIndex].total ? 'done' : 'error';
|
|
60
|
-
renderMultiProgressBars(bars);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
await Promise.all(workers);
|
|
64
|
-
clearProgressBars();
|
|
65
|
-
|
|
66
|
-
const uniqueResults = deduplicate(allResults);
|
|
67
|
-
const filteredResults = applyFilter(uniqueResults, filter);
|
|
68
|
-
|
|
69
|
-
if (errors.length > 0) {
|
|
70
|
-
const firstMsg = errors[0].message;
|
|
71
|
-
const isProxyError = ['不可用', '连接被拒绝', '连接中断', '超时', '无法解析']
|
|
72
|
-
.some(kw => firstMsg.includes(kw));
|
|
73
|
-
|
|
74
|
-
if (filteredResults.length === 0) {
|
|
75
|
-
if (isProxyError) {
|
|
76
|
-
console.error(` 所有请求失败,请检查代理: ${proxyUrl}\n`);
|
|
77
|
-
} else {
|
|
78
|
-
const show = errors.slice(0, 5);
|
|
79
|
-
for (const e of show) console.error(` ✗ ${e.url}: ${e.message}\n`);
|
|
80
|
-
if (errors.length > 5) console.error(` ... 还有 ${errors.length - 5} 个失败\n`);
|
|
81
|
-
}
|
|
82
|
-
console.error('未获取到数据');
|
|
83
|
-
if (outputFile) writeFileSync(outputFile, '[]', 'utf-8');
|
|
84
|
-
return;
|
|
85
|
-
} else {
|
|
86
|
-
if (isProxyError) {
|
|
87
|
-
console.error(` ${errors.length} 个请求失败,请检查代理: ${proxyUrl}\n`);
|
|
88
|
-
} else {
|
|
89
|
-
console.error(` ${errors.length} 个失败:`);
|
|
90
|
-
const show = errors.slice(0, 5);
|
|
91
|
-
for (const e of show) console.error(` ✗ ${e.url}: ${e.message}`);
|
|
92
|
-
if (errors.length > 5) console.error(` ... 还有 ${errors.length - 5} 个`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const output = formatOutput(filteredResults, outputFormat);
|
|
98
|
-
|
|
99
|
-
if (outputFile) {
|
|
100
|
-
writeFileSync(outputFile, output, 'utf-8');
|
|
101
|
-
console.log(`\n结果已写入: ${outputFile}`);
|
|
102
|
-
} else {
|
|
103
|
-
process.stdout.write(output + '\n');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (filter) {
|
|
107
|
-
console.log(`\n共 ${uniqueResults.length} 个${label},过滤后 ${filteredResults.length} 个(过滤条件: ${formatFilterDescription(filter)})`);
|
|
108
|
-
} else {
|
|
109
|
-
console.log(`\n共 ${filteredResults.length} 个${label}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { formatOutput } from '../lib/output.js';
|
|
3
|
+
import { deduplicate } from '../lib/output.js';
|
|
4
|
+
import { applyFilter, formatFilterDescription } from '../lib/filter.js';
|
|
5
|
+
import { calculateConcurrency, createMultiProgressBars, renderMultiProgressBars, clearProgressBars } from '../lib/io.js';
|
|
6
|
+
import { randomDelay } from '../lib/delay.js';
|
|
7
|
+
|
|
8
|
+
export async function processUrlsWithProgress({
|
|
9
|
+
urls,
|
|
10
|
+
proxyUrl,
|
|
11
|
+
outputFile,
|
|
12
|
+
outputFormat,
|
|
13
|
+
filter,
|
|
14
|
+
processFn,
|
|
15
|
+
label = '数据',
|
|
16
|
+
}) {
|
|
17
|
+
const allResults = [];
|
|
18
|
+
const errors = [];
|
|
19
|
+
|
|
20
|
+
if (urls.length === 0) {
|
|
21
|
+
console.error('\n未获取到数据');
|
|
22
|
+
if (outputFile) writeFileSync(outputFile, '[]', 'utf-8');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const concurrency = calculateConcurrency(urls.length);
|
|
27
|
+
const bars = createMultiProgressBars(concurrency);
|
|
28
|
+
|
|
29
|
+
const slots = Array.from({ length: concurrency }, () => []);
|
|
30
|
+
urls.forEach((url, i) => slots[i % concurrency].push(url));
|
|
31
|
+
|
|
32
|
+
bars.forEach((bar, i) => {
|
|
33
|
+
bar.total = slots[i].length;
|
|
34
|
+
bar.status = slots[i].length > 0 ? 'running' : 'done';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
renderMultiProgressBars(bars);
|
|
38
|
+
|
|
39
|
+
const workers = slots.map(async (slotUrls, slotIndex) => {
|
|
40
|
+
for (const url of slotUrls) {
|
|
41
|
+
bars[slotIndex].url = url;
|
|
42
|
+
renderMultiProgressBars(bars);
|
|
43
|
+
|
|
44
|
+
await randomDelay();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const results = await processFn(url, proxyUrl);
|
|
48
|
+
allResults.push(...results);
|
|
49
|
+
bars[slotIndex].current++;
|
|
50
|
+
bars[slotIndex].status = 'running';
|
|
51
|
+
} catch (err) {
|
|
52
|
+
errors.push({ url, message: err.message });
|
|
53
|
+
bars[slotIndex].current++;
|
|
54
|
+
bars[slotIndex].status = 'error';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
renderMultiProgressBars(bars);
|
|
58
|
+
}
|
|
59
|
+
bars[slotIndex].status = bars[slotIndex].current === bars[slotIndex].total ? 'done' : 'error';
|
|
60
|
+
renderMultiProgressBars(bars);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await Promise.all(workers);
|
|
64
|
+
clearProgressBars();
|
|
65
|
+
|
|
66
|
+
const uniqueResults = deduplicate(allResults);
|
|
67
|
+
const filteredResults = applyFilter(uniqueResults, filter);
|
|
68
|
+
|
|
69
|
+
if (errors.length > 0) {
|
|
70
|
+
const firstMsg = errors[0].message;
|
|
71
|
+
const isProxyError = ['不可用', '连接被拒绝', '连接中断', '超时', '无法解析']
|
|
72
|
+
.some(kw => firstMsg.includes(kw));
|
|
73
|
+
|
|
74
|
+
if (filteredResults.length === 0) {
|
|
75
|
+
if (isProxyError) {
|
|
76
|
+
console.error(` 所有请求失败,请检查代理: ${proxyUrl}\n`);
|
|
77
|
+
} else {
|
|
78
|
+
const show = errors.slice(0, 5);
|
|
79
|
+
for (const e of show) console.error(` ✗ ${e.url}: ${e.message}\n`);
|
|
80
|
+
if (errors.length > 5) console.error(` ... 还有 ${errors.length - 5} 个失败\n`);
|
|
81
|
+
}
|
|
82
|
+
console.error('未获取到数据');
|
|
83
|
+
if (outputFile) writeFileSync(outputFile, '[]', 'utf-8');
|
|
84
|
+
return;
|
|
85
|
+
} else {
|
|
86
|
+
if (isProxyError) {
|
|
87
|
+
console.error(` ${errors.length} 个请求失败,请检查代理: ${proxyUrl}\n`);
|
|
88
|
+
} else {
|
|
89
|
+
console.error(` ${errors.length} 个失败:`);
|
|
90
|
+
const show = errors.slice(0, 5);
|
|
91
|
+
for (const e of show) console.error(` ✗ ${e.url}: ${e.message}`);
|
|
92
|
+
if (errors.length > 5) console.error(` ... 还有 ${errors.length - 5} 个`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const output = formatOutput(filteredResults, outputFormat);
|
|
98
|
+
|
|
99
|
+
if (outputFile) {
|
|
100
|
+
writeFileSync(outputFile, output, 'utf-8');
|
|
101
|
+
console.log(`\n结果已写入: ${outputFile}`);
|
|
102
|
+
} else {
|
|
103
|
+
process.stdout.write(output + '\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (filter) {
|
|
107
|
+
console.log(`\n共 ${uniqueResults.length} 个${label},过滤后 ${filteredResults.length} 个(过滤条件: ${formatFilterDescription(filter)})`);
|
|
108
|
+
} else {
|
|
109
|
+
console.log(`\n共 ${filteredResults.length} 个${label}`);
|
|
110
|
+
}
|
|
111
|
+
}
|