| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 |
- #!/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;接口相对路径 /api/... 亦支持)
- // 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, 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) && !url.startsWith('/')) fail(`--url 仅接受 http(s) 绝对地址、或以 / 开头的接口相对路径(拒绝 file:// 等本地协议),收到: ${url}`, 4);
- async function getRedirectTarget() {
- const r = await httpJson('GET', `/api/reports/${id}`, undefined, { base, token: loadToken(), raw: true, relogin: true });
- if (!r.ok || !r.json?.data) {
- const recent = await fetchAllReports({ base, token: loadToken(), pageSize: 3, relogin: true }).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 {
- if (target.startsWith('/')) {
- // 列表返回的接口相对 downloadUrl(302 至 OSS/本地回源): 走鉴权接口(带 token + 401 自动重登)
- const r = await httpJson('GET', target, undefined, { base, token: loadToken(), json: false, relogin: true });
- if (!r.ok) fail(`下载失败 HTTP ${r.status}: ${target}`, 6);
- return r.text;
- }
- 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(url.split('?')[0]) || '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}]`);
- }
|