fetch-report-text.mjs 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  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)
  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, login, 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)) fail(`--url 仅接受 http(s) 协议(拒绝 file:// 等本地协议),收到: ${url}`, 4);
  27. async function getRedirectTarget() {
  28. const token = loadToken();
  29. let r = await httpJson('GET', `/api/reports/${id}`, undefined, { base, token, raw: true });
  30. if (!r.ok || !r.json?.data) {
  31. if (r.status === 401 && process.env.SMARTBADGE_PASSWORD) {
  32. await login({ base, password: process.env.SMARTBADGE_PASSWORD });
  33. r = await httpJson('GET', `/api/reports/${id}`, undefined, { base, token: loadToken(), raw: true });
  34. }
  35. if (!r.ok || !r.json?.data) {
  36. const recent = await fetchAllReports({ base, token, pageSize: 3 }).then((rs) => rs.map((x) => `${x.id}(${x.type}, ${x.periodStart})`).join('、'));
  37. fail(`报告 id 未找到(${id}). 近期报告参考: ${recent || '无'}`, 5);
  38. }
  39. }
  40. const d = r.json.data;
  41. const target = d.ossUrl || d.downloadUrl;
  42. if (!target) fail(`报告 ${id} 无 ossUrl/downloadUrl,请在服务器侧检查 OSS 配置`, 6);
  43. return { id: String(d.id), target, title: d.title, type: d.type, period: `${d.periodStart} ~ ${d.periodEnd}` };
  44. }
  45. async function fetchHtml(target) {
  46. const controller = new AbortController();
  47. const timer = setTimeout(() => controller.abort(), 30000);
  48. try {
  49. const res = await fetch(target, { signal: controller.signal });
  50. if (!res.ok) fail(`下载失败 HTTP ${res.status}: ${target}`, 6);
  51. return await res.text();
  52. } catch (err) {
  53. fail(`下载失败(超时 30s 或网络异常): ${err.message}\n 直链: ${target}`, 7);
  54. } finally {
  55. clearTimeout(timer);
  56. }
  57. }
  58. const meta = id
  59. ? await getRedirectTarget()
  60. : { id: `url-${Date.now()}`, target: url, title: path.basename(new URL(url).pathname) || 'report', type: '', period: url };
  61. const html = await fetchHtml(meta.target);
  62. const text = htmlToText(html);
  63. const safeName = String(meta.id).replace(/[^a-zA-Z0-9._-]/g, '_');
  64. const file = path.resolve(outPath || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'out', `report-${safeName}.txt`));
  65. fs.mkdirSync(path.dirname(file), { recursive: true });
  66. fs.writeFileSync(file, text);
  67. if (jsonOut) {
  68. console.log(JSON.stringify({ id: meta.id, title: meta.title, type: meta.type, period: meta.period, chars: text.length, file }));
  69. process.exit(0);
  70. }
  71. if (preview) {
  72. const lines = text.split('\n').filter(Boolean);
  73. const first = lines.slice(0, 28).join('\n');
  74. console.log(`标题: ${meta.title || '(无)'}\n${first}\n...[共 ${text.length} 字符, 全文见 ${file}]`);
  75. } else {
  76. console.log(text);
  77. console.error(`\n[全文已另存: ${file}]`);
  78. }