const path = require('path') const fs = require('fs') const crypto = require('crypto') const Database = require('better-sqlite3') const { initCrypto, dec, ensureDecrypted } = require('./weekly_crypto') const cfgPath = path.join(process.cwd(), 'config', 'weekly_embed.json') if (!fs.existsSync(path.dirname(cfgPath))) fs.mkdirSync(path.dirname(cfgPath), { recursive: true }) if (!fs.existsSync(cfgPath)) { fs.writeFileSync(cfgPath, JSON.stringify({ machine_salt: crypto.randomBytes(16).toString('hex'), access_key: 'K049876543' }, null, 2)) } const getConfig = () => { try { return JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) } catch { return {} } } const cfg = getConfig() const passphrase = process.env.SYSTEM_AUTH_PASS_93220 || process.env.WEEKLY_PASSWORD || '' const salt = cfg.machine_salt || 'salt' initCrypto(passphrase, salt) const dataDir = path.join(process.cwd(), 'data') const dbPath = path.join(dataDir, 'weekly.db') const sealPath = path.join(dataDir, 'weekly.enc') let db = null const getDB = () => { if (db) return db if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }) ensureDecrypted(dbPath, sealPath) db = new Database(dbPath, { readonly: true }) try { db.pragma('journal_mode = WAL') } catch {} try { db.pragma('synchronous = NORMAL') } catch {} try { db.pragma('busy_timeout = 4000') } catch {} return db } const weekIdForDate = (dateStr) => { try { const d = new Date(dateStr) const day = d.getUTCDay() const diffToMon = (day + 6) % 7 const mon = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - diffToMon)) const firstDay = new Date(Date.UTC(mon.getUTCFullYear(), 0, 1)) const firstDayWeekday = (firstDay.getUTCDay() + 6) % 7 const days = Math.floor((mon - firstDay) / 86400000) + firstDayWeekday + 1 const week = Math.ceil(days / 7) return `${mon.getUTCFullYear()}-W${String(week).padStart(2, '0')}` } catch { return `${new Date().getUTCFullYear()}-W01` } } const weeksForYear = (year) => { const y = parseInt(year, 10) const tz = 'Asia/Shanghai' const dateInTZ = (d) => new Date(d) const firstThursday = new Date(Date.UTC(y, 0, 4)) const day = firstThursday.getUTCDay() const diffToMon = (day + 6) % 7 let mon = new Date(Date.UTC(y, 0, 4 - diffToMon)) const out = [] let weekNum = 1 while (weekNum <= 53) { const thu = new Date(Date.UTC(mon.getUTCFullYear(), mon.getUTCMonth(), mon.getUTCDate() + 3)) const isoYear = thu.getUTCFullYear() if (isoYear !== y) break const sun = new Date(Date.UTC(mon.getUTCFullYear(), mon.getUTCMonth(), mon.getUTCDate() + 6)) const weekLabel = `${y}-W${String(weekNum).padStart(2, '0')}` const startStr = new Intl.DateTimeFormat('zh-CN', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(dateInTZ(mon)).replace(/\//g, '-') const endStr = new Intl.DateTimeFormat('zh-CN', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(dateInTZ(sun)).replace(/\//g, '-') out.push({ week_id: weekLabel, start: startStr, end: endStr }) mon = new Date(Date.UTC(mon.getUTCFullYear(), mon.getUTCMonth(), mon.getUTCDate() + 7)) weekNum++ } try { const db = getDB() const reports = db.prepare(`SELECT id, week_id FROM reports WHERE week_id LIKE ?`).all(`${y}-%`) const reportMap = new Map() reports.forEach(r => reportMap.set(r.week_id, r.id)) if (reports.length > 0) { const placeholders = reports.map(() => '?').join(',') const reportIds = reports.map(r => r.id) const eventRows = db.prepare(`SELECT DISTINCT report_id FROM events WHERE report_id IN (${placeholders})`).all(...reportIds) const eventSet = new Set(eventRows.map(r => r.report_id)) out.forEach(w => { const rid = reportMap.get(w.week_id) w.has_report = !!rid w.has_events = rid ? eventSet.has(rid) : false }) } else { out.forEach(w => { w.has_report = false; w.has_events = false }) } } catch {} return out } const getReportByWeek = (week_id) => { const row = getDB().prepare(`SELECT * FROM reports WHERE week_id=? ORDER BY id DESC LIMIT 1`).get(week_id) if (!row) return null return { id: row.id, week_id: row.week_id, title: dec(row.title_enc, row.title_iv, row.title_tag), summary: dec(row.summary_enc, row.summary_iv, row.summary_tag), content: dec(row.content_enc, row.content_iv, row.content_tag), created_at: row.created_at, updated_at: row.updated_at } } const listEvents = (report_id) => { const rows = getDB().prepare(`SELECT * FROM events WHERE report_id=? ORDER BY id ASC`).all(parseInt(report_id, 10)) return rows.map(r => ({ id: r.id, report_id: r.report_id, title: dec(r.title_enc, r.title_iv, r.title_tag), content: dec(r.content_enc, r.content_iv, r.content_tag), status_code: r.status_code, status: dec(r.status_enc, r.status_iv, r.status_tag), deadline: r.deadline, parent_event_id: r.parent_event_id, completed_at: r.completed_at, push_required: !!r.push_required, created_at: r.created_at, updated_at: r.updated_at })) } const bindRoutes = (app) => { const accessKey = String(cfg.access_key || 'K049876543') const mustKey = (req, res) => { const key = String(req.get('X-Weekly-Embed-Key') || req.query.key || '') if (!key || key !== accessKey) { res.status(403).json({ ok: false, error: 'forbidden' }) return false } return true } app.use('/api/weekly_embed', (req, res, next) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate') res.set('Pragma', 'no-cache') res.set('Expires', '0') res.set('Surrogate-Control', 'no-store') if (!mustKey(req, res)) return next() }) app.get('/api/weekly_embed/years', (req, res) => { try { const trace = [] trace.push('打开只读周报数据库') const db = getDB() trace.push('查询可用年份') const rows = db.prepare(`SELECT DISTINCT SUBSTR(week_id,1,4) AS y FROM reports WHERE week_id IS NOT NULL AND LENGTH(week_id)>=4 ORDER BY y DESC`).all() trace.push(`返回年份 ${rows.length} 条`) const years = rows.map(r => parseInt(String(r.y), 10)).filter(y => y > 0) res.json({ ok: true, years, trace }) } catch (e) { res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) }) } }) app.get('/api/weekly_embed/weeks', (req, res) => { const trace = [] const year = parseInt(String(req.query.year || new Date().getFullYear()), 10) trace.push(`计算 ${year} 年 ISO 周列表`) const weeks = weeksForYear(year) trace.push(`合并周报/事件存在标记 ${weeks.length} 条`) const currentWeekId = weekIdForDate(new Date().toISOString()) trace.push(`定位当前周 ${currentWeekId}`) res.json({ ok: true, weeks, currentWeekId, trace }) }) app.get('/api/weekly_embed/report', (req, res) => { const trace = [] const weekId = String(req.query.week_id || '') trace.push(`按 week_id 读取周报 ${weekId}`) const report = weekId ? getReportByWeek(weekId) : null trace.push(report ? `读取到周报 id=${report.id}` : '未找到周报') res.json({ ok: true, report, trace }) }) app.get('/api/weekly_embed/events', (req, res) => { const trace = [] const reportId = String(req.query.report_id || '') if (!reportId) return res.json({ ok: true, events: [], rows: [], trace: ['缺少 report_id,返回空列表'] }) trace.push(`按 report_id=${reportId} 读取事件列表`) const events = listEvents(reportId) trace.push(`事件数量 ${events.length}`) res.json({ ok: true, events, rows: events, trace }) }) } module.exports = { bindRoutes }