Files
Toolbox/src/server/weekly_embed.js
T

198 lines
7.6 KiB
JavaScript

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 }