| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485 |
- #!/usr/bin/env node
- // 报告正文拉取: --id 按 reportId 定位(经 /api/reports/:id)或 --url 直链,下载 HTML → 剥标签存 out/ 并打印
- // 用法:
- // node fetch-report-text.mjs --id <reportId> # 按报告 id(当前窗口内最佳)
- // node fetch-report-text.mjs --url <ossUrl|downloadUrl> # 直链(自动跟随 302)
- // node fetch-report-text.mjs --id xxxx --preview # 只输出标题+前 28 行摘要
- // node fetch-report-text.mjs --id xxxx --json # 输出 {id,title,chars,file}
- // node fetch-report-text.mjs --id xxxx --out 报告.txt # 指定输出文件
- // 密码: SMARTBADGE_PASSWORD(无则复用 scripts/.token)
- import fs from 'fs';
- import path from 'path';
- import { fileURLToPath } from 'url';
- import { baseUrl, fail, loadToken, login, httpJson, htmlToText, fetchAllReports } from './lib.mjs';
- const args = process.argv.slice(2);
- const argVal = (name) => {
- const i = args.indexOf(name);
- return i >= 0 ? args[i + 1] : undefined;
- };
- const id = argVal('--id');
- const url = argVal('--url');
- const outPath = argVal('--out');
- const preview = args.includes('--preview');
- const jsonOut = args.includes('--json');
- const base = baseUrl();
- if (!id && !url) fail('用法: node fetch-report-text.mjs --id <reportId> | --url <http(s)> [--preview] [--json] [--out 路径]', 3);
- if (url && !/^https?:\/\//i.test(url)) fail(`--url 仅接受 http(s) 协议(拒绝 file:// 等本地协议),收到: ${url}`, 4);
- async function getRedirectTarget() {
- const token = loadToken();
- let r = await httpJson('GET', `/api/reports/${id}`, undefined, { base, token, raw: true });
- if (!r.ok || !r.json?.data) {
- if (r.status === 401 && process.env.SMARTBADGE_PASSWORD) {
- await login({ base, password: process.env.SMARTBADGE_PASSWORD });
- r = await httpJson('GET', `/api/reports/${id}`, undefined, { base, token: loadToken(), raw: true });
- }
- if (!r.ok || !r.json?.data) {
- const recent = await fetchAllReports({ base, token, pageSize: 3 }).then((rs) => rs.map((x) => `${x.id}(${x.type}, ${x.periodStart})`).join('、'));
- fail(`报告 id 未找到(${id}). 近期报告参考: ${recent || '无'}`, 5);
- }
- }
- const d = r.json.data;
- const target = d.ossUrl || d.downloadUrl;
- if (!target) fail(`报告 ${id} 无 ossUrl/downloadUrl,请在服务器侧检查 OSS 配置`, 6);
- return { id: String(d.id), target, title: d.title, type: d.type, period: `${d.periodStart} ~ ${d.periodEnd}` };
- }
- async function fetchHtml(target) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 30000);
- try {
- const res = await fetch(target, { signal: controller.signal });
- if (!res.ok) fail(`下载失败 HTTP ${res.status}: ${target}`, 6);
- return await res.text();
- } catch (err) {
- fail(`下载失败(超时 30s 或网络异常): ${err.message}\n 直链: ${target}`, 7);
- } finally {
- clearTimeout(timer);
- }
- }
- const meta = id
- ? await getRedirectTarget()
- : { id: `url-${Date.now()}`, target: url, title: path.basename(new URL(url).pathname) || 'report', type: '', period: url };
- const html = await fetchHtml(meta.target);
- const text = htmlToText(html);
- const safeName = String(meta.id).replace(/[^a-zA-Z0-9._-]/g, '_');
- const file = path.resolve(outPath || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'out', `report-${safeName}.txt`));
- fs.mkdirSync(path.dirname(file), { recursive: true });
- fs.writeFileSync(file, text);
- if (jsonOut) {
- console.log(JSON.stringify({ id: meta.id, title: meta.title, type: meta.type, period: meta.period, chars: text.length, file }));
- process.exit(0);
- }
- if (preview) {
- const lines = text.split('\n').filter(Boolean);
- const first = lines.slice(0, 28).join('\n');
- console.log(`标题: ${meta.title || '(无)'}\n${first}\n...[共 ${text.length} 字符, 全文见 ${file}]`);
- } else {
- console.log(text);
- console.error(`\n[全文已另存: ${file}]`);
- }
|