| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- // 共享库:env 回退链 / token 读写 / login / httpJson / fetchAllReports / htmlToText(零依赖, Node >=18)
- import fs from 'fs';
- import path from 'path';
- import { fileURLToPath } from 'url';
- export const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
- export const REPO_ROOT = path.dirname(SCRIPTS_DIR);
- export const TOKEN_FILE = path.join(SCRIPTS_DIR, '.token');
- export function fail(msg, code = 1) {
- console.error(msg);
- process.exit(code);
- }
- // ---------- env 回退链 ----------
- // --env > SMARTBADGE_ENV_FILE > 自目录向上扫 server/.env > 兄弟目录自动发现(名称含 device|smartbadge 的仓库)
- export function findEnvFile({ envPath, envFileEnv = process.env.SMARTBADGE_ENV_FILE } = {}) {
- const candidates = [];
- const push = (p) => candidates.push(p);
- if (envPath) push(path.resolve(envPath));
- if (envFileEnv) push(path.resolve(envFileEnv));
- for (let dir = REPO_ROOT; ; dir = path.dirname(dir)) {
- push(path.join(dir, 'server', '.env'));
- if (path.dirname(dir) === dir) break;
- }
- const siblings = path.dirname(REPO_ROOT);
- let dirNames = [];
- try { dirNames = fs.readdirSync(siblings); } catch { /* 忽略 */ }
- for (const name of dirNames.sort()) {
- if (/device|smartbadge/i.test(name)) {
- push(path.join(siblings, name, 'server', '.env'));
- }
- }
- const hit = candidates.find((p) => {
- try { return fs.statSync(p).isFile(); } catch { return false; }
- });
- return { hit, candidates };
- }
- export function loadEnv(file) {
- const parsed = {};
- try {
- const text = fs.readFileSync(file, 'utf8');
- for (const rawLine of text.split(/\r?\n/)) {
- const line = rawLine.trim();
- if (!line || line.startsWith('#') || !line.includes('=')) continue;
- const [key, ...rest] = line.split('=');
- const value = rest.join('=').trim().replace(/^['"]|['"]$/g, '');
- if (!process.env[key]) process.env[key] = value;
- parsed[key] = process.env[key];
- }
- } catch (err) {
- if (err.code !== 'ENOENT') throw err;
- }
- return parsed;
- }
- export function baseUrl(flagBase) {
- return flagBase || process.env.SMARTBADGE_API_BASE || 'https://recording.sh-lami.com';
- }
- // ---------- token ----------
- export function loadToken() {
- try { return fs.readFileSync(TOKEN_FILE, 'utf8').trim(); } catch { return ''; }
- }
- export function saveToken(token) {
- fs.writeFileSync(TOKEN_FILE, token);
- }
- // ---------- 登录 ----------
- export async function login({ base, username, password }) {
- if (!password) fail('用法: node api.mjs login <密码>(或环境变量 SMARTBADGE_PASSWORD)', 2);
- const res = await fetch(`${base}/api/auth/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
- body: JSON.stringify({ username: username || process.env.SMARTBADGE_USERNAME || 'lami123456', password }),
- });
- const json = await res.json().catch(() => ({ raw: '非 JSON 响应' }));
- if (!json.success || !json.data?.sessionToken) {
- fail(`登录失败(${res.status}): ${JSON.stringify(json)}`, 2);
- }
- saveToken(json.data.sessionToken);
- return json.data.sessionToken;
- }
- // ---------- httpJson ----------
- // relogin=true 且 401 且环境有 SMARTBADGE_PASSWORD 时自动重新登录重试一次
- export async function httpJson(method, apiPath, body, { base, token, raw = false, json = true, relogin = false } = {}) {
- const doFetch = async () => {
- const tokenValue = token || loadToken();
- const headers = { 'X-Requested-With': 'XMLHttpRequest' };
- if (body !== undefined) headers['Content-Type'] = 'application/json';
- if (tokenValue) headers['X-Parse-Session-Token'] = tokenValue;
- let res;
- try {
- res = await fetch(baseUrl(base) + apiPath, {
- method: method.toUpperCase(),
- headers,
- body: body !== undefined ? JSON.stringify(body) : undefined,
- });
- } catch (err) {
- fail(`无法连接服务器(${err.message})\n - 检查 SMARTBADGE_API_BASE / --base(默认线上 https://recording.sh-lami.com;本地 http://localhost:3002)\n - 以 / 开头的参数在 Git Bash 需先 export MSYS_NO_PATHCONV=1\n - 服务器是否已启动`, 3);
- }
- return res;
- };
- let res = await doFetch();
- if (res.status === 401 && relogin && process.env.SMARTBADGE_PASSWORD) {
- try { await login({ base: baseUrl(base), password: process.env.SMARTBADGE_PASSWORD }); } catch { /* 保留原 401 响应 */ }
- res = await doFetch();
- }
- const text = await res.text();
- if (json) {
- try { return { status: res.status, ok: res.ok, json: JSON.parse(text) }; } catch { return { status: res.status, ok: res.ok, json: { raw: text } }; }
- }
- return { status: res.status, ok: res.ok, json: null, text };
- }
- // ---------- 报告分页拉全 ----------
- export async function fetchAllReports({ base, token, type, start, end, deviceNo, pageSize = 100, relogin = false } = {}) {
- const all = [];
- let page = 1;
- for (;;) {
- const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
- if (type) qs.set('type', type);
- if (start) qs.set('start', start);
- if (end) qs.set('end', end);
- if (deviceNo) qs.set('deviceNo', deviceNo);
- const r = await httpJson('GET', `/api/reports?${qs}`, undefined, { base, token, raw: true, relogin });
- const data = r.json?.data;
- const items = data?.items ?? [];
- all.push(...items);
- const total = data?.total ?? 0;
- if (items.length === 0 || all.length >= total || items.length < 100) break;
- page++;
- }
- return all;
- }
- // ---------- htmlToText(正则剥标签,非完整 DOM;表格按行序文本) ----------
- const ENTITY = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
- export function htmlToText(html) {
- return String(html)
- .replace(/<script[\s\S]*?<\/script>/gi, ' ')
- .replace(/<style[\s\S]*?<\/style>/gi, ' ')
- .replace(/<(head|nav|footer)[\s\S]*?<\/\1>/gi, ' ')
- .replace(/<br\s*\/?>/gi, '\n')
- .replace(/<\/(p|li|tr|td|th|div|h[1-6])>/gi, '\n')
- .replace(/<li[^>]*>/gi, '- ')
- .replace(/<[^>]+>/g, ' ')
- .replace(/&([a-z]+);/gi, (m, name) => ENTITY[name.toLowerCase()] ?? m)
- .replace(/&#(\d+);/g, (m, n) => { try { return String.fromCharCode(n); } catch { return m; } })
- .replace(/[ \t]+\n/g, '\n')
- .replace(/^[ \t]+/gm, '')
- .replace(/\n{3,}/g, '\n\n')
- .replace(/^\s+|\s+$/g, '')
- .trim();
- }
|