fetch-report-text.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. #!/usr/bin/env node
  2. // 报告正文拉取: --id 按 reportId 定位(经 /api/reports/:id)或 --url 直链,下载 HTML → 剥标签存 out/ 并打印
  3. // 用法:
  4. // node fetch-report-text.mjs --id <reportId> # 按报告 id(当前窗口内最佳)
  5. // node fetch-report-text.mjs --url <ossUrl|downloadUrl> # 直链(自动跟随 302;接口相对路径 /api/... 亦支持)
  6. // node fetch-report-text.mjs --id xxxx --preview # 只输出标题+前 28 行摘要
  7. // node fetch-report-text.mjs --id xxxx --json # 输出 {id,title,chars,file}
  8. // node fetch-report-text.mjs --id xxxx --out 报告.txt # 指定输出文件
  9. // 密码: SMARTBADGE_PASSWORD(无则复用 scripts/.token)
  10. import fs from 'fs';
  11. import path from 'path';
  12. import { fileURLToPath } from 'url';
  13. import { baseUrl, fail, loadToken, httpJson, htmlToText, fetchAllReports } from './lib.mjs';
  14. const args = process.argv.slice(2);
  15. const argVal = (name) => {
  16. const i = args.indexOf(name);
  17. return i >= 0 ? args[i + 1] : undefined;
  18. };
  19. const id = argVal('--id');
  20. const url = argVal('--url');
  21. const outPath = argVal('--out');
  22. const preview = args.includes('--preview');
  23. const jsonOut = args.includes('--json');
  24. const base = baseUrl();
  25. if (!id && !url) fail('用法: node fetch-report-text.mjs --id <reportId> | --url <http(s)> [--preview] [--json] [--out 路径]', 3);
  26. if (url && !/^https?:\/\//i.test(url) && !url.startsWith('/')) fail(`--url 仅接受 http(s) 绝对地址、或以 / 开头的接口相对路径(拒绝 file:// 等本地协议),收到: ${url}`, 4);
  27. async function getRedirectTarget() {
  28. const r = await httpJson('GET', `/api/reports/${id}`, undefined, { base, token: loadToken(), raw: true, relogin: true });
  29. if (!r.ok || !r.json?.data) {
  30. const recent = await fetchAllReports({ base, token: loadToken(), pageSize: 3, relogin: true }).then((rs) => rs.map((x) => `${x.id}(${x.type}, ${x.periodStart})`).join('、'));
  31. fail(`报告 id 未找到(${id}). 近期报告参考: ${recent || '无'}`, 5);
  32. }
  33. const d = r.json.data;
  34. const target = d.ossUrl || d.downloadUrl;
  35. if (!target) fail(`报告 ${id} 无 ossUrl/downloadUrl,请在服务器侧检查 OSS 配置`, 6);
  36. return { id: String(d.id), target, title: d.title, type: d.type, period: `${d.periodStart} ~ ${d.periodEnd}` };
  37. }
  38. async function fetchHtml(target) {
  39. const controller = new AbortController();
  40. const timer = setTimeout(() => controller.abort(), 30000);
  41. try {
  42. if (target.startsWith('/')) {
  43. // 列表返回的接口相对 downloadUrl(302 至 OSS/本地回源): 走鉴权接口(带 token + 401 自动重登)
  44. const r = await httpJson('GET', target, undefined, { base, token: loadToken(), json: false, relogin: true });
  45. if (!r.ok) fail(`下载失败 HTTP ${r.status}: ${target}`, 6);
  46. return r.text;
  47. }
  48. const res = await fetch(target, { signal: controller.signal });
  49. if (!res.ok) fail(`下载失败 HTTP ${res.status}: ${target}`, 6);
  50. return await res.text();
  51. } catch (err) {
  52. fail(`下载失败(超时 30s 或网络异常): ${err.message}\n 直链: ${target}`, 7);
  53. } finally {
  54. clearTimeout(timer);
  55. }
  56. }
  57. const meta = id
  58. ? await getRedirectTarget()
  59. : { id: `url-${Date.now()}`, target: url, title: path.basename(url.split('?')[0]) || 'report', type: '', period: url };
  60. const html = await fetchHtml(meta.target);
  61. const text = htmlToText(html);
  62. const safeName = String(meta.id).replace(/[^a-zA-Z0-9._-]/g, '_');
  63. const file = path.resolve(outPath || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'out', `report-${safeName}.txt`));
  64. fs.mkdirSync(path.dirname(file), { recursive: true });
  65. fs.writeFileSync(file, text);
  66. if (jsonOut) {
  67. console.log(JSON.stringify({ id: meta.id, title: meta.title, type: meta.type, period: meta.period, chars: text.length, file }));
  68. process.exit(0);
  69. }
  70. if (preview) {
  71. const lines = text.split('\n').filter(Boolean);
  72. const first = lines.slice(0, 28).join('\n');
  73. console.log(`标题: ${meta.title || '(无)'}\n${first}\n...[共 ${text.length} 字符, 全文见 ${file}]`);
  74. } else {
  75. console.log(text);
  76. console.error(`\n[全文已另存: ${file}]`);
  77. }