Files
Toolbox/src/server/index.js
T
yangxiangyuan 4492b4106a feat: 新增3个思想实验室项目,完善私有剪贴板授权与上传功能,添加配套测试脚本
1. 新增怀旧弹珠台、人性与投资、穿越三体、四维空间四个思想实验室项目
2. 为私有剪贴板添加业务层授权校验,优化上传进度显示与鉴权失败处理
3. 新增多个调试与单元测试脚本,覆盖三体积分、弹珠台物理、集成测试等场景
4. 补全穿越三体项目的完整HTML页面资源
2026-08-07 17:49:24 +08:00

7127 lines
298 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express')
const cors = require('cors')
const path = require('path')
const fs = require('fs')
const axios = require('axios')
const Database = require('better-sqlite3')
const crypto = require('crypto')
// ===== 从 ~/Toolbox_local_creds.env.local 加载变量到 process.env =====
// ★ 规则:所有密码、密钥、Token、用户名等高危内容,禁止硬编码在源码里。
// 一律放在 ~/Toolbox_local_creds.env.local 中,在此处自动加载到 process.env
// 各模块通过 process.env.XXX 读取,不要直接 import 或 require 凭证。
// 优先级:已存在的 process.env > env 文件 > 模块内 '' fallback
try {
const os = require('os')
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
try {
if (fs.existsSync(usersRoot)) {
fs.readdirSync(usersRoot).forEach(name => {
const dir = path.join(usersRoot, name)
if (dir !== userProfile && fs.existsSync(dir)) candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
})
}
} catch {}
for (const p of candidates) {
if (!fs.existsSync(p)) continue
const content = fs.readFileSync(p, 'utf-8')
content.split(/\r?\n/).forEach(line => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
const idx = trimmed.indexOf('=')
if (idx <= 0) return
const key = trimmed.slice(0, idx).trim()
const val = trimmed.slice(idx + 1).trim()
if (key && !Object.prototype.hasOwnProperty.call(process.env, key)) {
process.env[key] = val
}
})
break
}
} catch {}
// ===== end env loader =====
const { fetchPosts, bootstrapCookies } = require('./weiboClient')
const { getCookiesFromDevTools, screenshotUrl } = require('./devtools')
const { insertPost, updateRetweeted, queryPostsAny, queryPostsAll, insertKeyword, insertTrackedUser, getSettings, upsertArchive, getArchive, insertSettingHistory } = require('./db')
const { fetchAll, queryByDate, getConfig: getMarketsCfg, setConfig: setMarketsCfg, startScheduler: startMarketsScheduler, sendTestEmail, maybeNotify } = require('./markets')
const marketSkills = require('./market_skills/manager')
const securityCalendar = require('./security_calendar')
const securityCalendarBridge = require('./security_calendar_calendar_bridge')
const marketsWeeklyReport = require('./markets_weekly_report')
const marketsDipEval = require('./markets_dip_eval')
const { upsertMarketArchive, getMarketArchive } = require('./db')
const { getConfig: getWallCfg, setConfig: setWallCfg, listCards: listWallCards, createCard: createWallCard, updateCardById: updateWallCardById, deleteCardById: deleteWallCardById } = require('./wall')
const fundsGuoxin = require('./funds_guoxin')
const fundsHuatai = require('./funds_huatai')
const freeShow = require('./free_show')
const ccbPrivateFunds = require('./ccb_private_funds')
const cloud = require('./cloud')
const cloudBalanceWatch = require('./cloud_balance_watch')
const expense = require('./expense')
const investmentLedger = require('./investment_ledger')
const styleCheck = require('./style_check')
let weekly = null
try { weekly = require('./weekly') } catch (e) { try { require('./logger').logJSON('weekly.require.error', { error: String(e.message || e) }, 'weekly') } catch {} }
let weeklyBound = false
let weeklyEmbed = null
try { weeklyEmbed = require('./weekly_embed') } catch (e) { try { require('./logger').logJSON('weekly_embed.require.error', { error: String(e.message || e) }, 'weekly_embed') } catch {} }
let weeklyEmbedBound = false
const aiLib = require('./ai-lib')
const { log, logJSON, getLogs, clearLogs } = require('./logger')
const cloudNotes = require('./cloud_notes')
const cloudSheets = require('./cloud_sheets')
const docCloudKeeper = require('./doc_cloud_keeper')
const docCalendarBridge = require('./doc_calendar_bridge')
const rationalLock = require('./rational_lock')
const languageBehaviorLock = require('./language_behavior_lock')
const plantHome = require('./plant_home')
const globalNews = require('./global_news')
const boxBackup = require('./box_backup')
const yuanzhupai = require('./yuanzhupai')
const shortLink = require('./short_link')
const dataGateway = require('./data_gateway')
const thoughtLab = require('./thought_lab')
const ossFileCabinet = require('./oss_file_cabinet')
yuanzhupai.initDb()
const app = express()
// 安全响应头
app.use((req, res, next) => {
res.set('X-Frame-Options', 'DENY')
res.set('X-Content-Type-Options', 'nosniff')
res.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()')
next()
})
// 移除 X-Powered-By 响应头(避免泄露 Express 版本信息)
app.disable('x-powered-by')
app.set('etag', false)
// CORS 白名单限制 — 仅允许指定域名跨域访问 API
const allowedOrigins = [
'https://traesite.umersoft.com',
'http://traesite.umersoft.com',
'https://traesite.umersoft.com:8975',
'http://traesite.umersoft.com:8975',
'http://localhost:8081',
'http://127.0.0.1:8081'
]
const isAllowedCorsOrigin = (origin) => !origin || allowedOrigins.includes(origin)
app.use((req, res, next) => {
const origin = String(req.headers.origin || '').trim()
const isAllowed = isAllowedCorsOrigin(origin)
if (origin && isAllowed) {
res.set('Access-Control-Allow-Origin', origin)
res.vary('Origin')
res.set('Access-Control-Allow-Credentials', 'true')
}
if (req.method === 'OPTIONS') {
if (!origin || isAllowed) {
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-Local-Creds-Token')
res.set('Access-Control-Allow-Credentials', 'true')
return res.status(204).end()
}
return res.status(403).json({ ok: false, error: 'cors_origin_denied' })
}
if (origin && !isAllowed) {
return res.status(403).json({ ok: false, error: 'cors_origin_denied' })
}
return next()
})
// 强制 HTTPS:HTTP 请求返回 301 跳转到 HTTPS
app.use((req, res, next) => {
const proto = req.headers['x-forwarded-proto'] || ''
// 非本地环境且非 HTTPS 时,强制跳转
if (proto && proto !== 'https') {
const httpsUrl = `https://${req.headers.host}${req.url}`
return res.redirect(301, httpsUrl)
}
next()
})
app.use(express.json({ limit: '50mb' }))
const readLocalCreds = () => {
try {
const os = require('os')
const path = require('path')
const fs = require('fs')
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
try {
if (fs.existsSync(usersRoot)) {
fs.readdirSync(usersRoot).forEach(name => {
const dir = path.join(usersRoot, name)
if (dir !== userProfile && fs.existsSync(dir)) {
candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
}
})
}
} catch {}
let content = null
for (const p of candidates) {
if (fs.existsSync(p)) { content = fs.readFileSync(p, 'utf-8'); break }
}
if (!content) return {}
const creds = {}
content.split(/\r?\n/).forEach(line => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
const idx = trimmed.indexOf('=')
if (idx > 0) creds[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim()
})
return creds
} catch { return {} }
}
const getLocalCredsClientToken = (req) => {
try {
const headerToken = String(req.headers['x-local-creds-token'] || req.headers['x-toolbox-creds-token'] || '').trim()
if (headerToken) return headerToken
const authHeader = String(req.headers.authorization || '').trim()
if (authHeader.startsWith('Bearer ')) return authHeader.slice(7).trim()
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
return String(req.body.token || '').trim()
}
} catch {}
return ''
}
const checkLocalCredsToken = (req, localCreds) => {
const serverToken = String(localCreds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (!serverToken) return true
const clientToken = getLocalCredsClientToken(req)
return clientToken === serverToken
}
const sendNoCreds = (res) => res.json({ ok: true, creds: null })
// Weekly: disable all HTTP caching for APIs
const setNoCache = (res) => {
try {
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')
res.set('Vary', 'Cookie')
} catch {}
}
app.use((req, res, next) => {
try {
if (
req.path.startsWith('/api/weekly') ||
req.path.startsWith('/tools/weekly/api/weekly') ||
req.path.startsWith('/api/weekly_embed') ||
req.path.startsWith('/api/doc_cloud_keeper') ||
req.path.startsWith('/api/doc_calendar_bridge') ||
req.path.startsWith('/api/markets/security_calendar') ||
req.path.startsWith('/api/markets/weekly_report') ||
req.path.startsWith('/api/markets/dip_eval') ||
req.path.startsWith('/api/global_news') ||
req.path.startsWith('/api/box_backup')
) setNoCache(res)
} catch {}
next()
})
const ensureRebalanceLoaded = () => {
let mod = null
try { mod = require('./rebalance') } catch (e) { try { logJSON('rebalance.require.error', { error: String(e.message || e) }, 'rebalance') } catch {}; setTimeout(ensureRebalanceLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('rebalance.bind_routes.done', { ok: true }, 'rebalance') } catch {}
}
} catch (e) {
try { logJSON('rebalance.bind_routes.error', { error: String(e.message || e) }, 'rebalance') } catch {}
}
}
ensureRebalanceLoaded()
const ensureDltDrawsLoaded = () => {
let mod = null
try { mod = require('./dlt_draws') } catch (e) { try { logJSON('dlt_draws.require.error', { error: String(e.message || e) }, 'dlt_draws') } catch {}; setTimeout(ensureDltDrawsLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('dlt_draws.bind_routes.done', { ok: true }, 'dlt_draws') } catch {}
}
} catch (e) {
try { logJSON('dlt_draws.bind_routes.error', { error: String(e.message || e) }, 'dlt_draws') } catch {}
}
}
ensureDltDrawsLoaded()
const ensureCalendarReminderLoaded = () => {
let mod = null
try { mod = require('./calendar_reminder') } catch (e) { try { logJSON('calendar_reminder.require.error', { error: String(e.message || e) }, 'calendar_reminder') } catch {}; setTimeout(ensureCalendarReminderLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('calendar_reminder.bind_routes.done', { ok: true }, 'calendar_reminder') } catch {}
}
} catch (e) {
try { logJSON('calendar_reminder.bind_routes.error', { error: String(e.message || e) }, 'calendar_reminder') } catch {}
}
}
ensureCalendarReminderLoaded()
const ensureAppCalendarReminderLoaded = () => {
let mod = null
try { mod = require('./app_calendar_reminder') } catch (e) { try { logJSON('app_calendar_reminder.require.error', { error: String(e.message || e) }, 'app_calendar_reminder') } catch {}; setTimeout(ensureAppCalendarReminderLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('app_calendar_reminder.bind_routes.done', { ok: true }, 'app_calendar_reminder') } catch {}
}
} catch (e) {
try { logJSON('app_calendar_reminder.bind_routes.error', { error: String(e.message || e) }, 'app_calendar_reminder') } catch {}
}
}
ensureAppCalendarReminderLoaded()
const ensureAppOrderBoxLoaded = () => {
let mod = null
try { mod = require('./app_order_box') } catch (e) { try { logJSON('app_order_box.require.error', { error: String(e.message || e) }, 'app_order_box') } catch {}; setTimeout(ensureAppOrderBoxLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('app_order_box.bind_routes.done', { ok: true }, 'app_order_box') } catch {}
}
} catch (e) {
try { logJSON('app_order_box.bind_routes.error', { error: String(e.message || e) }, 'app_order_box') } catch {}
}
}
ensureAppOrderBoxLoaded()
const CALENDAR_VOICE_AUTH_COOKIE = 'calendar_voice_gate'
const readCalendarVoiceAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_voice', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readCalendarVoiceJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'calendar_voice.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getCalendarVoiceCookieName = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return CALENDAR_VOICE_AUTH_COOKIE
}
const getCalendarVoiceMaxAge = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 120 * 24 * 3600 * 1000
}
const getCalendarVoiceGuidConfig = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'GUID').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'GUID', value: valueRaw }
} catch {
return { key: 'GUID', value: '' }
}
}
const hasValidCalendarVoiceGuid = (req) => {
try {
const cfg = readCalendarVoiceAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getCalendarVoiceGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const hasCalendarVoiceAuth = (req) => {
try {
const cfg = readCalendarVoiceAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getCalendarVoiceCookieName()
return cookies[name] === '1'
} catch { return false }
}
const isCalendarVoiceForceGuid = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return true
} catch { return true }
}
const hasValidCalendarVoiceToken = (req) => {
try {
const token = String(req.query.token || '')
if (!token) return false
const payload = verifyJwtWithKeys(token, jwkKeys(readCalendarVoiceJwk()))
if (!payload) return false
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-calendar_voice')) return false
return true
} catch {
return false
}
}
const grantCalendarVoiceAuth = (res) => {
const maxAgeCalendarVoice = getCalendarVoiceMaxAge()
const cookieName = getCalendarVoiceCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeCalendarVoice, path: '/' })
}
app.get('/tools/calendar_voice', (req, res, next) => {
try {
const forceGuid = isCalendarVoiceForceGuid()
if (hasCalendarVoiceAuth(req)) return next()
const guidKey = getCalendarVoiceGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidCalendarVoiceGuid(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
if (hasValidCalendarVoiceToken(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (String(req.query.token || '')) return res.status(401).send('未授权')
if (forceGuid) return res.status(401).send('未授权')
grantCalendarVoiceAuth(res)
return next()
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/calendar_voice', (req, res, next) => {
const forceGuid = isCalendarVoiceForceGuid()
if (hasCalendarVoiceAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
if (isHandshake && hasValidCalendarVoiceGuid(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (isHandshake && hasValidCalendarVoiceToken(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (isHandshake && String(req.query.token || '')) return res.status(401).send('未授权')
if (isHandshake && !forceGuid) {
grantCalendarVoiceAuth(res)
return next()
}
return res.status(401).send('未授权')
})
app.use('/api/calendar_voice', (req, res, next) => {
if (hasCalendarVoiceAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const ensureCalendarVoiceLoaded = () => {
let mod = null
try { mod = require('./calendar_voice') } catch (e) { try { logJSON('calendar_voice.require.error', { error: String(e.message || e) }, 'calendar_voice') } catch {}; return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('calendar_voice.bind_routes.done', { ok: true }, 'calendar_voice') } catch {}
}
} catch (e) {
try { logJSON('calendar_voice.bind_routes.error', { error: String(e.message || e) }, 'calendar_voice') } catch {}
}
}
ensureCalendarVoiceLoaded()
const ensurePscLoaded = () => {
let mod = null
try { mod = require('./psc') } catch (e) { try { logJSON('psc.require.error', { error: String(e.message || e) }, 'psc') } catch {}; setTimeout(ensurePscLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('psc.bind_routes.done', { ok: true }, 'psc') } catch {}
}
} catch (e) {
try { logJSON('psc.bind_routes.error', { error: String(e.message || e) }, 'psc') } catch {}
}
}
ensurePscLoaded()
let privateClipboardRouter = null
const ensurePrivateClipboardLoaded = () => {
let mod = null
try { mod = require('./private_clipboard') } catch (e) { try { logJSON('private_clipboard.require.error', { error: String(e.message || e) }, 'private_clipboard') } catch {}; setTimeout(ensurePrivateClipboardLoaded, 2000); return }
try {
if (mod && mod.createRouter) {
privateClipboardRouter = mod.createRouter()
try { logJSON('private_clipboard.create_router.done', { ok: true }, 'private_clipboard') } catch {}
} else if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('private_clipboard.bind_routes.done', { ok: true }, 'private_clipboard') } catch {}
}
} catch (e) {
try { logJSON('private_clipboard.create_router.error', { error: String(e.message || e) }, 'private_clipboard') } catch {}
}
}
ensurePrivateClipboardLoaded()
const ensureFitnessDiaryLoaded = () => {
let mod = null
try { mod = require('./fitness_diary') } catch (e) { try { logJSON('fitness_diary.require.error', { error: String(e.message || e) }, 'fitness_diary') } catch {}; setTimeout(ensureFitnessDiaryLoaded, 2000); return }
try {
if (mod && mod.initDb) {
// Use __dirname to ensure correct path regardless of CWD
mod.initDb(path.join(__dirname, '..', '..', 'data', 'fitness_diary.db'))
}
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('fitness_diary.bind_routes.done', { ok: true }, 'fitness_diary') } catch {}
}
} catch (e) {
try { logJSON('fitness_diary.bind_routes.error', { error: String(e.message || e) }, 'fitness_diary') } catch {}
}
}
const ensurePlantHomeLoaded = () => {
let mod = null
try { mod = require('./plant_home') } catch (e) { try { logJSON('plant_home.require.error', { error: String(e.message || e) }, 'plant_home') } catch {}; setTimeout(ensurePlantHomeLoaded, 2000); return }
try {
if (mod && mod.initDb) {
mod.initDb(path.join(__dirname, '..', '..', 'data', 'plant_home.db'))
}
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('plant_home.bind_routes.done', { ok: true }, 'plant_home') } catch {}
}
} catch (e) {
try { logJSON('plant_home.bind_routes.error', { error: String(e.message || e) }, 'plant_home') } catch {}
}
}
const ensureAppleWatchLoaded = () => {
let mod = null
try { mod = require('./apple_watch') } catch (e) { try { logJSON('apple_watch.require.error', { error: String(e.message || e) }, 'apple_watch') } catch {}; setTimeout(ensureAppleWatchLoaded, 2000); return }
try {
if (mod && mod.initDb) {
mod.initDb(path.join(__dirname, '..', '..', 'data', 'apple_watch.db'))
}
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('apple_watch.bind_routes.done', { ok: true }, 'apple_watch') } catch {}
}
} catch (e) {
try { logJSON('apple_watch.bind_routes.error', { error: String(e.message || e) }, 'apple_watch') } catch {}
}
}
const FITNESS_DIARY_AUTH_COOKIE = 'fitness_diary_gate'
const readFitnessDiaryAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'fitness_diary', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readFitnessDiaryJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'fitness_diary.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getFitnessDiaryCookieName = () => {
try {
const cfg = readFitnessDiaryAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return FITNESS_DIARY_AUTH_COOKIE
}
const getFitnessDiaryMaxAge = () => {
try {
const cfg = readFitnessDiaryAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 90 * 24 * 3600 * 1000
}
const getFitnessDiaryGuidConfig = () => {
try {
const cfg = readFitnessDiaryAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch {
return { key: 'guid', value: '' }
}
}
const hasValidFitnessDiaryGuid = (req) => {
try {
const cfg = readFitnessDiaryAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getFitnessDiaryGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const hasFitnessDiaryAuth = (req) => {
try {
const config = readFitnessDiaryAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getFitnessDiaryCookieName()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/fitness_diary', (req, res, next) => {
try {
if (hasFitnessDiaryAuth(req)) return next()
const guidKey = getFitnessDiaryGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidFitnessDiaryGuid(req)) {
const maxAgeFD = getFitnessDiaryMaxAge()
const cookieName = getFitnessDiaryCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeFD, path: '/' })
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readFitnessDiaryJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'FitnessDiary')) return res.status(401).send('未授权')
}
const maxAgeFD = getFitnessDiaryMaxAge()
const cookieName = getFitnessDiaryCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeFD })
return res.redirect('/tools/fitness_diary/index.html')
} catch (e) {
return res.status(401).send('未授权')
}
})
app.use('/tools/fitness_diary', (req, res, next) => {
if (hasFitnessDiaryAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
if (isHandshake && (String(req.query.token || '') || hasValidFitnessDiaryGuid(req))) return next()
return res.status(401).send('未授权')
})
app.use('/api/fitness_diary', (req, res, next) => {
if (hasFitnessDiaryAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
ensureFitnessDiaryLoaded()
// ── 知资札记 鉴权 ──
const ZHIZI_NOTES_AUTH_COOKIE = 'zhizi_notes_gate'
const readZhiziNotesAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'zhizi_notes', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readZhiziNotesJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'zhizi_notes.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getZhiziNotesCookieName = () => {
try {
const cfg = readZhiziNotesAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return ZHIZI_NOTES_AUTH_COOKIE
}
const getZhiziNotesMaxAge = () => {
try {
const cfg = readZhiziNotesAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 30 * 24 * 3600 * 1000
}
const getZhiziNotesGuidConfig = () => {
try {
const cfg = readZhiziNotesAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch {
return { key: 'guid', value: '' }
}
}
const hasValidZhiziNotesGuid = (req) => {
try {
const cfg = readZhiziNotesAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getZhiziNotesGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const isZhiziNotesForceGuid = () => {
try {
const cfg = readZhiziNotesAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return false
} catch { return false }
}
const hasZhiziNotesAuth = (req) => {
try {
const config = readZhiziNotesAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
if (cookies[getZhiziNotesCookieName()] === '1') return true
return false
} catch { return false }
}
// 知资札记 入口守卫
app.get('/tools/zhizi_notes', (req, res, next) => {
setNoCache(res)
try {
const forceGuid = isZhiziNotesForceGuid()
if (hasZhiziNotesAuth(req)) return next()
const guidKey = getZhiziNotesGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidZhiziNotesGuid(req)) {
const maxAge = getZhiziNotesMaxAge()
const cookieName = getZhiziNotesCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/zhizi_notes/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readZhiziNotesJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-zhizi_notes')) return res.status(401).send('未授权')
}
const maxAge = getZhiziNotesMaxAge()
const cookieName = getZhiziNotesCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/zhizi_notes/index.html')
} catch (e) { return res.status(401).send('未授权') }
})
// 知资札记 子路径守卫
app.use('/tools/zhizi_notes', (req, res, next) => {
const forceGuid = isZhiziNotesForceGuid()
if (hasZhiziNotesAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
const guidKey = getZhiziNotesGuidConfig().key
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (isHandshake && hasValidZhiziNotesGuid(req)) {
const maxAge = getZhiziNotesMaxAge()
const cookieName = getZhiziNotesCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/zhizi_notes/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
if (isHandshake && String(req.query.token || '')) {
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readZhiziNotesJwk()))
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-zhizi_notes')) return res.status(401).send('未授权')
const maxAge = getZhiziNotesMaxAge()
const cookieName = getZhiziNotesCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/zhizi_notes/index.html')
}
}
if (isHandshake && forceGuid) return res.status(401).send('未授权')
return res.status(401).send('未授权')
})
// 知资札记 API 鉴权
app.use('/api/zhizi_notes', (req, res, next) => {
if (hasZhiziNotesAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const ensureZhiziNotesLoaded = () => {
let mod = null
try { mod = require('./zhizi_notes') } catch (e) { try { logJSON('zhizi_notes.require.error', { error: String(e.message || e) }, 'zhizi_notes') } catch {}; setTimeout(ensureZhiziNotesLoaded, 2000); return }
try {
if (mod && mod.initDb) {
mod.initDb(path.join(__dirname, '..', '..', 'data', 'zhizi_notes.db'))
}
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('zhizi_notes.bind_routes.done', { ok: true }, 'zhizi_notes') } catch {}
}
} catch (e) {
try { logJSON('zhizi_notes.bind_routes.error', { error: String(e.message || e) }, 'zhizi_notes') } catch {}
}
}
ensureZhiziNotesLoaded()
const PLANT_HOME_AUTH_COOKIE = 'plant_home_gate'
const readPlantHomeAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'plant_home', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readPlantHomeJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'plant_home.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getPlantHomeCookieName = () => {
try {
const cfg = readPlantHomeAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return PLANT_HOME_AUTH_COOKIE
}
const getPlantHomeMaxAge = () => {
try {
const cfg = readPlantHomeAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 90 * 24 * 3600 * 1000
}
const getPlantHomeGuidConfig = () => {
try {
const cfg = readPlantHomeAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch {
return { key: 'guid', value: '' }
}
}
const hasValidPlantHomeGuid = (req) => {
try {
const cfg = readPlantHomeAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getPlantHomeGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const isPlantHomeForceGuid = () => {
try {
const cfg = readPlantHomeAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return false
} catch { return false }
}
const hasPlantHomeAuth = (req) => {
try {
const config = readPlantHomeAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getPlantHomeCookieName()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/plant_home', (req, res, next) => {
try {
const forceGuid = isPlantHomeForceGuid()
if (hasPlantHomeAuth(req)) return next()
const guidKey = getPlantHomeGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidPlantHomeGuid(req)) {
const maxAge = getPlantHomeMaxAge()
const cookieName = getPlantHomeCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readPlantHomeJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-plant_home')) return res.status(401).send('未授权')
}
const maxAge = getPlantHomeMaxAge()
const cookieName = getPlantHomeCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge })
return res.redirect('/tools/plant_home/index.html')
} catch (e) {
return res.status(401).send('未授权')
}
})
app.use('/tools/plant_home', (req, res, next) => {
const forceGuid = isPlantHomeForceGuid()
if (hasPlantHomeAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
if (isHandshake && hasValidPlantHomeGuid(req)) {
const maxAge = getPlantHomeMaxAge()
const cookieName = getPlantHomeCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return next()
}
if (isHandshake && String(req.query.token || '')) return next()
if (isHandshake && forceGuid) return res.status(401).send('未授权')
if (!forceGuid) {
const maxAge = getPlantHomeMaxAge()
const cookieName = getPlantHomeCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return next()
}
return res.status(401).send('未授权')
})
app.use('/uploads/plant_home', (req, res, next) => {
if (hasPlantHomeAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/plant_home', (req, res, next) => {
if (hasPlantHomeAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
ensurePlantHomeLoaded()
const APPLE_WATCH_AUTH_COOKIE = 'apple_watch_gate'
const readAppleWatchJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'apple_watch.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasAppleWatchAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'apple_watch', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'apple_watch', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return APPLE_WATCH_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/apple_watch', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readAppleWatchJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-apple_watch')) return res.status(401).send('未授权')
}
const maxAge = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'apple_watch', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'apple_watch', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return APPLE_WATCH_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge })
return res.redirect('/tools/apple_watch/index.html')
} catch (e) {
return res.status(401).send('未授权')
}
})
app.use('/tools/apple_watch', (req, res, next) => {
if (hasAppleWatchAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/apple_watch', (req, res, next) => {
if (hasAppleWatchAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
ensureAppleWatchLoaded()
const RATIONAL_LOCK_AUTH_COOKIE = 'rational_lock_gate'
const readRationalLockAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'rational_lock', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readRationalLockJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'rational_lock.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getRationalLockCookieName = () => {
try {
const cfg = readRationalLockAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return RATIONAL_LOCK_AUTH_COOKIE
}
const getRationalLockMaxAge = () => {
try {
const cfg = readRationalLockAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 90 * 24 * 3600 * 1000
}
const hasRationalLockAuth = (req) => {
try {
const cfg = readRationalLockAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getRationalLockCookieName()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/rational_lock', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readRationalLockJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-rational_lock')) return res.status(401).send('未授权')
}
const maxAge = getRationalLockMaxAge()
const cookieName = getRationalLockCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/rational_lock/index.html')
} catch {
return res.status(401).send('未授权')
}
})
app.use('/tools/rational_lock', (req, res, next) => {
if (hasRationalLockAuth(req)) return next()
if (req.method === 'GET') return next()
return res.status(401).send('未授权')
})
app.use('/api/rational_lock', (req, res, next) => {
return next()
})
const LANGUAGE_BEHAVIOR_LOCK_AUTH_COOKIE = 'language_behavior_lock_gate'
const readLanguageBehaviorLockAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'language_behavior_lock', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readLanguageBehaviorLockJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'language_behavior_lock.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getLanguageBehaviorLockCookieName = () => {
try {
const cfg = readLanguageBehaviorLockAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return LANGUAGE_BEHAVIOR_LOCK_AUTH_COOKIE
}
const getLanguageBehaviorLockMaxAge = () => {
try {
const cfg = readLanguageBehaviorLockAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 90 * 24 * 3600 * 1000
}
const hasLanguageBehaviorLockAuth = (req) => {
try {
const cfg = readLanguageBehaviorLockAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getLanguageBehaviorLockCookieName()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/language_behavior_lock', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readLanguageBehaviorLockJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-language_behavior_lock')) return res.status(401).send('未授权')
}
const maxAge = getLanguageBehaviorLockMaxAge()
const cookieName = getLanguageBehaviorLockCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/language_behavior_lock/index.html')
} catch {
return res.status(401).send('未授权')
}
})
app.use('/tools/language_behavior_lock', (req, res, next) => {
if (hasLanguageBehaviorLockAuth(req)) return next()
if (req.method === 'GET') return next()
return res.status(401).send('未授权')
})
app.use('/api/language_behavior_lock', (req, res, next) => {
return next()
})
const ensureRationalLockLoaded = () => {
try {
if (rationalLock && rationalLock.bindRoutes) {
rationalLock.bindRoutes(app)
try { logJSON('rational_lock.bind_routes.done', { ok: true }, 'rational_lock') } catch {}
}
} catch (e) {
try { logJSON('rational_lock.bind_routes.error', { error: String(e.message || e) }, 'rational_lock') } catch {}
}
}
ensureRationalLockLoaded()
const ensureLanguageBehaviorLockLoaded = () => {
try {
if (languageBehaviorLock && languageBehaviorLock.bindRoutes) {
languageBehaviorLock.bindRoutes(app)
try { logJSON('language_behavior_lock.bind_routes.done', { ok: true }, 'language_behavior_lock') } catch {}
}
} catch (e) {
try { logJSON('language_behavior_lock.bind_routes.error', { error: String(e.message || e) }, 'language_behavior_lock') } catch {}
}
}
ensureLanguageBehaviorLockLoaded()
const invoicesDir = path.join(process.cwd(), 'uploads', 'cloud', 'invoices')
if (!fs.existsSync(invoicesDir)) fs.mkdirSync(invoicesDir, { recursive: true })
const settingsPath = path.join(process.cwd(), 'config', 'settings.json')
if (!fs.existsSync(path.dirname(settingsPath))) fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
if (!fs.existsSync(settingsPath)) fs.writeFileSync(settingsPath, JSON.stringify({ users: [{ uid: '7716940453', screen_name: '' }], keywords: ['上海', '上海 无料'], headers: {}, count: 50 }, null, 2))
const flagsPath = path.join(process.cwd(), 'config', 'flags.json')
if (!fs.existsSync(flagsPath)) fs.writeFileSync(flagsPath, JSON.stringify({
debugTool: { enabled: true, launchDate: null, autoHideAfterDays: 30 },
debug: { weibo: true, markets: true, wall: true, funds_guoxin: true, funds_huatai: true, free_show: true },
navAuth: { enable_auth: true, iss: 'TRAE-NAV', iss_strict: false }
}, null, 2))
const computeDebugEnabled = () => {
let f = { debugTool: { enabled: true, launchDate: null, autoHideAfterDays: 30 } }
try { f = JSON.parse(fs.readFileSync(flagsPath, 'utf-8')) } catch {}
const cfg = f.debugTool || {}
if (!cfg.enabled) return false
if (cfg.launchDate) {
const launch = new Date(cfg.launchDate)
if (!isNaN(launch.getTime())) {
const hideAfterDays = Number(cfg.autoHideAfterDays || 30)
const hideTime = new Date(launch.getTime() + hideAfterDays * 24 * 3600 * 1000)
if (Date.now() > hideTime.getTime()) return false
}
}
return true
}
const computeToolDebug = tool => {
let f = { debug: { weibo: true, markets: true, wall: true } }
try { f = JSON.parse(fs.readFileSync(flagsPath, 'utf-8')) } catch {}
const m = f.debug || {}
const envKey = `DEBUG_${String(tool || '').toUpperCase()}`
if (process.env[envKey] === '0') return false
if (process.env[envKey] === '1') return true
return !!m[tool]
}
const computeNavAuthEnabled = () => {
let f = { navAuth: { enable_auth: true } }
try { f = JSON.parse(fs.readFileSync(flagsPath, 'utf-8')) } catch {}
const cfg = f.navAuth || {}
return cfg.enable_auth !== false
}
const computeNavIssStrict = () => {
let f = { navAuth: { iss_strict: false } }
try { f = JSON.parse(fs.readFileSync(flagsPath, 'utf-8')) } catch {}
const cfg = f.navAuth || {}
return cfg.iss_strict === true
}
const getNavIssFromFlags = () => {
try {
let f = { navAuth: { iss: 'TRAE-NAV' } }
try { f = JSON.parse(fs.readFileSync(flagsPath, 'utf-8')) } catch {}
const cfg = f.navAuth || {}
const v = String(cfg.iss || '').trim()
return v || 'TRAE-NAV'
} catch {
return 'TRAE-NAV'
}
}
const isHttps = (req) => {
const xfwd = String(((req.get && req.get('X-Forwarded-Proto')) || (req.headers && req.headers['x-forwarded-proto']) || '')).toLowerCase()
if (xfwd.split(',')[0].trim() === 'https') return true
if (req.secure) return true
const arr = String((req.headers && req.headers['x-arr-ssl']) || '')
if (arr) return true
const xp = String((req.headers && req.headers['x-forwarded-protocol']) || '').toLowerCase()
if (xp === 'https') return true
return false
}
app.use((req, res, next) => {
if (isHttps(req)) {
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
res.set('Referrer-Policy', 'strict-origin-when-cross-origin')
}
next()
})
app.use((req, res, next) => {
const orig = res.cookie.bind(res)
res.cookie = (name, val, options) => {
const o = Object.assign({ httpOnly: true, sameSite: 'lax' }, options || {})
if (isHttps(req)) o.secure = true
return orig(name, val, o)
}
next()
})
app.use((req, res, next) => {
if (req.path.startsWith('/tools/debug') && !computeDebugEnabled()) return res.status(404).end()
next()
})
app.use((req, res, next) => {
if (req.path.startsWith('/tools/weekly/api/weekly')) req.url = req.url.replace('/tools/weekly/api/weekly','/api/weekly')
if (req.path.startsWith('/tools/fitness_diary/api/fitness_diary')) req.url = req.url.replace('/tools/fitness_diary/api/fitness_diary','/api/fitness_diary')
if (req.path.startsWith('/tools/apple_watch/api/apple_watch')) req.url = req.url.replace('/tools/apple_watch/api/apple_watch','/api/apple_watch')
if (req.path.startsWith('/tools/ai-lib/api/ai-lib')) req.url = req.url.replace('/tools/ai-lib/api/ai-lib','/api/ai-lib')
next()
})
app.use((req, res, next) => {
if (req.path.startsWith('/api/ai-lib')) {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate')
res.set('Pragma', 'no-cache')
res.set('Expires', '0')
}
next()
})
const EXPENSE_JWK = (() => {
try {
const p = path.join(process.cwd(), 'config', 'expense.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
})()
const parseCookie = (cookieStr) => {
const out = {}
String(cookieStr || '').split(/;\s*/).forEach(p => { const idx = p.indexOf('='); if (idx > 0) out[p.slice(0, idx)] = decodeURIComponent(p.slice(idx + 1)) })
return out
}
const NAV_BOOKMARKS_DB = (() => {
const dbPath = path.join(process.cwd(), 'data', 'nav_bookmarks.db')
try { fs.mkdirSync(path.dirname(dbPath), { recursive: true }) } catch {}
const db = new Database(dbPath)
db.pragma('journal_mode = WAL')
db.pragma('synchronous = NORMAL')
db.pragma('busy_timeout = 4000')
db.exec(`CREATE TABLE IF NOT EXISTS nav_categories (
code TEXT PRIMARY KEY,
name TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT,
updated_at TEXT
)`)
db.exec(`CREATE TABLE IF NOT EXISTS nav_bookmarks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL,
icon TEXT,
category_code TEXT,
remark TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT,
updated_at TEXT
)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_nav_bookmarks_category ON nav_bookmarks(category_code)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_nav_bookmarks_sort ON nav_bookmarks(sort_order)`)
return db
})()
const navNow = () => new Date().toISOString()
const navSeedCategories = () => {
try {
const count = NAV_BOOKMARKS_DB.prepare('SELECT COUNT(1) AS c FROM nav_categories').get()
if (count && count.c > 0) return
const seed = [
{ code: '1', name: '工作', sort: 1 },
{ code: '2', name: '投资', sort: 2 },
{ code: '3', name: '娱乐旅游出行', sort: 3 },
{ code: '4', name: '健康强体', sort: 4 },
{ code: '5', name: '花草鱼鸟', sort: 5 },
{ code: '6', name: '学习与AI开发', sort: 6 },
{ code: '7', name: '生活和家居', sort: 7 },
{ code: '8', name: '杨邮车(老家)', sort: 8 },
{ code: '9', name: '备用1', sort: 9 },
{ code: '10', name: '备用2', sort: 10 },
{ code: '11', name: '备用3', sort: 11 },
{ code: '99', name: '其他', sort: 99 }
]
const stmt = NAV_BOOKMARKS_DB.prepare('INSERT INTO nav_categories (code, name, sort_order, created_at, updated_at) VALUES (@code, @name, @sort, @createdAt, @updatedAt)')
const now = navNow()
const tx = NAV_BOOKMARKS_DB.transaction(() => {
seed.forEach(row => stmt.run({ code: row.code, name: row.name, sort: row.sort, createdAt: now, updatedAt: now }))
})
tx()
} catch {}
}
const navSeedBookmarksFromJson = () => {
try {
const count = NAV_BOOKMARKS_DB.prepare('SELECT COUNT(1) AS c FROM nav_bookmarks').get()
if (count && count.c > 0) return
const p = path.join(process.cwd(), 'public', 'common', 'nav.json')
if (!fs.existsSync(p)) return
const items = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (!Array.isArray(items) || !items.length) return
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare(`INSERT INTO nav_bookmarks (id, name, url, icon, category_code, remark, sort_order, created_at, updated_at)
VALUES (@id, @name, @url, @icon, @categoryCode, @remark, @sortOrder, @createdAt, @updatedAt)`)
const tx = NAV_BOOKMARKS_DB.transaction(() => {
items.forEach((item, idx) => {
const id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${idx}-${Math.random().toString(16).slice(2)}`
stmt.run({
id,
name: String(item.name || '未命名'),
url: String(item.url || '#'),
icon: item.icon ? String(item.icon) : '',
categoryCode: '99',
remark: '',
sortOrder: idx + 1,
createdAt: now,
updatedAt: now
})
})
})
tx()
} catch {}
}
navSeedCategories()
navSeedBookmarksFromJson()
const b64urlToBuf = (s) => {
s = String(s || '').replace(/-/g, '+').replace(/_/g, '/')
const pad = s.length % 4
if (pad) s += '='.repeat(4 - pad)
return Buffer.from(s, 'base64')
}
const jwkKeys = (obj) => {
if (!obj) return []
if (Array.isArray(obj.keys)) return obj.keys.filter(k => k && k.n && k.e)
if (obj.kty && obj.n && obj.e) return [obj]
return []
}
const decodePart = s => {
s = String(s || '').replace(/-/g, '+').replace(/_/g, '/')
const pad = s.length % 4
if (pad) s += '='.repeat(4 - pad)
return Buffer.from(s, 'base64').toString()
}
const readNavPubJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'nav.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return null
}
const audMatch = (payload, expected) => {
try {
const a = String((payload && payload.aud) || '')
if (!expected) return true
if (expected.endsWith('-')) return a.startsWith(expected)
return a === expected
} catch { return false }
}
const verifyJwtWithKeys = (token, keys) => {
try {
const parts = String(token || '').split('.')
if (parts.length !== 3) return null
const data = parts[0] + '.' + parts[1]
const sig = b64urlToBuf(parts[2])
let ks = Array.isArray(keys) ? keys.slice() : []
try {
const ext = String(process.env.NAV_JWK_JSON || '')
if (ext) {
const obj = JSON.parse(ext)
const extra = jwkKeys(obj)
ks = ks.concat(extra)
} else {
try {
const navPub = readNavPubJwk()
const extra = jwkKeys(navPub)
ks = ks.concat(extra)
} catch {}
try {
const priv = readNavPrivateJwk()
const pub = (priv && priv.kty && priv.n && priv.e) ? { kty: priv.kty, n: priv.n, e: priv.e } : null
if (pub) ks = ks.concat([pub])
} catch {}
}
} catch {}
for (const jwk of ks) {
try {
const pub = crypto.createPublicKey({ key: jwk, format: 'jwk' })
const ok = crypto.verify('RSA-SHA256', Buffer.from(data), pub, sig)
if (!ok) continue
const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString())
const now = Math.floor(Date.now() / 1000)
if (payload.exp && now > payload.exp) continue
return payload
} catch {}
}
return null
} catch { return null }
}
const debugVerifyJwt = (token, keys, opts) => {
const expectedIss = (opts && opts.expectedIss) || getNavIssFromFlags()
const out = { ok: false, sig_ok: false, matched_source: '', checked_keys: 0, keys_sources: [], payload: null, header: null, now: Math.floor(Date.now()/1000), exp: null, exp_ok: null, iss: '', expected_iss: String(expectedIss), iss_ok: null, iss_strict: !!(opts && opts.issStrict), reason: '' }
try {
const parts = String(token || '').split('.')
if (parts.length !== 3) { out.reason = 'bad_format'; return out }
out.header = JSON.parse(decodePart(parts[0]))
const data = parts[0] + '.' + parts[1]
const sig = b64urlToBuf(parts[2])
let pool = []
try {
const ext = String(process.env.NAV_JWK_JSON || '')
if (ext) { const obj = JSON.parse(ext); const extra = jwkKeys(obj).map(k => ({ src: 'env', key: k })); pool = pool.concat(extra) }
} catch {}
try {
const navPub = readNavPubJwk()
const extra = jwkKeys(navPub).map(k => ({ src: 'file_nav_pub', key: k }))
pool = pool.concat(extra)
} catch {}
try {
const priv = readNavPrivateJwk()
const pub = (priv && priv.kty && priv.n && priv.e) ? { kty: priv.kty, n: priv.n, e: priv.e } : null
const extra = jwkKeys(pub).map(k => ({ src: 'file_nav_private_pub', key: k }))
pool = pool.concat(extra)
} catch {}
if (Array.isArray(keys)) pool = pool.concat(keys.map(k => ({ src: 'passed', key: k })))
out.keys_sources = pool.map(p => p.src)
for (const item of pool) {
try {
out.checked_keys++
const pub = crypto.createPublicKey({ key: item.key, format: 'jwk' })
const ok = crypto.verify('RSA-SHA256', Buffer.from(data), pub, sig)
if (!ok) continue
out.sig_ok = true
out.matched_source = item.src
out.payload = JSON.parse(decodePart(parts[1]))
out.exp = out.payload.exp || null
out.exp_ok = out.exp ? (out.now <= out.exp) : true
out.iss = String(out.payload.iss || '')
out.iss_ok = out.iss_strict ? (out.iss === out.expected_iss) : true
out.ok = !!(out.sig_ok && out.exp_ok && out.iss_ok)
return out
} catch {}
}
out.reason = 'no_key_matched'
return out
} catch (e) { out.reason = String(e && e.message || 'error'); return out }
}
const readNavPrivateJwk = () => {
try {
const s = String(process.env.NAV_PRIVATE_JWK_JSON || process.env.NAV_PRIVATE_JWK || '')
if (s) {
const obj = JSON.parse(s)
return obj
}
} catch {}
try {
const f = String(process.env.NAV_PRIVATE_JWK_FILE || '')
if (f && fs.existsSync(f)) {
const obj = JSON.parse(fs.readFileSync(f, 'utf-8'))
return obj
}
} catch {}
const candidates = [
path.join(process.cwd(), 'config', 'nav-private.jwk.json'),
path.resolve(__dirname, '../../config/nav-private.jwk.json'),
path.resolve(__dirname, '../config/nav-private.jwk.json')
]
for (const pth of candidates) {
try {
if (fs.existsSync(pth)) {
const obj = JSON.parse(fs.readFileSync(pth, 'utf-8'))
return obj
}
} catch {}
}
return null
}
const signJwtRS256 = (payload, jwk) => {
const header = { alg: 'RS256', typ: 'JWT' }
const enc = v => Buffer.from(JSON.stringify(v)).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'')
const data = enc(header) + '.' + enc(payload)
const priv = crypto.createPrivateKey({ key: jwk, format: 'jwk' })
const sig = crypto.sign('RSA-SHA256', Buffer.from(data), priv).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'')
return data + '.' + sig
}
const extractJwtFromInput = (raw) => {
const text = String(raw || '').trim()
if (!text) return ''
if (/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/.test(text)) return text
try {
const u = new URL(text)
const fromQuery = String(u.searchParams.get('token') || '').trim()
if (fromQuery) return fromQuery
} catch {}
const match = text.match(/(?:^|[?&])token=([^&#\s]+)/)
if (match && match[1]) return decodeURIComponent(match[1])
return ''
}
const appendTokenToUrl = (baseUrl, token) => {
const base = String(baseUrl || '').trim()
if (!base) return `/?token=${token}`
return base.includes('?') ? `${base}&token=${token}` : `${base}?token=${token}`
}
const readNavTargets = () => {
try {
const s = String(process.env.NAV_TARGETS_JSON || '')
if (!s) return {}
const obj = JSON.parse(s)
return obj && typeof obj === 'object' ? obj : {}
} catch { return {} }
}
const mapSystemIdToUrl = (systemId) => {
const ext = readNavTargets()
if (ext && typeof ext[systemId] === 'string') {
const v = String(ext[systemId] || '').trim()
if (v) return v
}
const m = {
'Tools-weibo': '/tools/weibo',
'Tools-markets': '/tools/markets',
'Tools-wall-': '/tools/wall',
'Tools-funds_guoxin': '/tools/funds_guoxin',
'Tools-funds_huatai': '/tools/funds_huatai',
'Tools-free_show': '/tools/free_show',
'Tools-ccb_private_funds': '/tools/ccb_private_funds',
'Tools-cloud': '/tools/cloud',
'Tools-weekly': '/tools/weekly',
'Tools-ai-lib': '/tools/ai-lib',
'Tools-expense': '/tools/expense',
'Tools-rebalance': '/tools/rebalance',
'Tools-dlt_draws': '/tools/dlt_draws',
'Tools-calendar_reminder': '/tools/calendar_reminder',
'Tools-calendar_voice': '/tools/calendar_voice',
'TRAE-PSC': '/tools/psc',
'FitnessDiary': '/tools/fitness_diary',
'Tools-zhizi_notes': '/tools/zhizi_notes',
'Tools-doc_cloud_keeper': '/tools/doc_cloud_keeper',
'Tools-yuanzhupai': '/tools/yuanzhupai',
'Tools-oss_file_cabinet': '/tools/oss_file_cabinet',
'Tools-plant_home': '/tools/plant_home',
'Tools-apple_watch': '/tools/apple_watch',
'Tools-private_clipboard': '/tools/private_clipboard',
'Tools-style_check': '/tools/style_check/index.html',
'Tools-short_link': '/tools/short_link',
'Tools-web_mobile_calendar_reminder': '/tools/web_mobile_calendar_reminder'
}
if (m[systemId]) return m[systemId]
if (systemId.startsWith('Tools-')) {
const name = systemId.slice('Tools-'.length).trim()
if (name) return `/tools/${name}`
}
if (systemId.startsWith('Tools-wall-')) return '/tools/wall'
return ''
}
const expenseVerifyJwt = (token) => verifyJwtWithKeys(token, jwkKeys(EXPENSE_JWK))
const EXPENSE_AUTH_COOKIE = 'expense_gate'
const hasExpenseAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'expense', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'expense', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return EXPENSE_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/expense', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = expenseVerifyJwt(token)
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-expense')) return res.status(401).send('未授权')
}
const maxAgeExpense = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'expense', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'expense', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return EXPENSE_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeExpense })
return res.redirect('/tools/expense')
} catch (e) {
return res.status(401).send('未授权')
}
})
app.use('/tools/expense', (req, res, next) => {
if (hasExpenseAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/expense', (req, res, next) => {
if (hasExpenseAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/expense', (req, res, next) => {
if (hasExpenseAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const AI_LIB_AUTH_COOKIE = 'ai_lib_gate'
const readAiLibJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'ai-lib.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const aiLibVerifyJwt = (token) => verifyJwtWithKeys(token, jwkKeys(readAiLibJwk()))
const hasAiLibAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'ai-lib', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'ai-lib', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return AI_LIB_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/ai-lib', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = aiLibVerifyJwt(token)
if (!payload) {
logJSON('ai-lib.handshake.fail', { token_len: token.length, keys: jwkKeys(readAiLibJwk()).length }, 'ai-lib')
return res.status(401).send('未授权')
}
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-ai-lib')) return res.status(401).send('未授权')
}
const maxAgeAiLib = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'ai-lib', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'ai-lib', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return AI_LIB_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeAiLib })
return res.redirect('/tools/ai-lib')
} catch (e) {
logJSON('ai-lib.handshake.error', { message: String(e.message || e) }, 'ai-lib')
return res.status(401).send('未授权')
}
})
app.use('/tools/ai-lib', (req, res, next) => {
if (hasAiLibAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/ai-lib', (req, res, next) => {
if (hasAiLibAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/ai-lib', (req, res, next) => {
if (req.path === '/auth/local_creds') return next()
if (hasAiLibAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const CLOUD_AUTH_COOKIE = 'cloud_gate'
const readCloudJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'cloud.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasCloudAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'cloud', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CLOUD_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/cloud', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readCloudJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-cloud')) return res.status(401).send('未授权')
}
const maxAgeCloud = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CLOUD_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeCloud, path: '/' })
return res.redirect('/tools/cloud')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/cloud', (req, res, next) => {
if (hasCloudAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/cloud', (req, res, next) => {
if (hasCloudAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/cloud', (req, res, next) => {
if (hasCloudAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const FUNDS_GUOXIN_AUTH_COOKIE = 'funds_guoxin_gate'
const readFundsGuoxinJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'funds_guoxin.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasFundsGuoxinAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'funds_guoxin', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'funds_guoxin', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return FUNDS_GUOXIN_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/funds_guoxin', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readFundsGuoxinJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-funds_guoxin')) return res.status(401).send('未授权')
}
const maxAgeFundsGuoxin = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'funds_guoxin', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'funds_guoxin', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return FUNDS_GUOXIN_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeFundsGuoxin, path: '/' })
return res.redirect('/tools/funds_guoxin')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/funds_guoxin', (req, res, next) => {
if (hasFundsGuoxinAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/funds_guoxin', (req, res, next) => {
if (hasFundsGuoxinAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const FUNDS_HUATAI_AUTH_COOKIE = 'funds_huatai_gate'
const readFundsHuataiJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'funds_huatai.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasFundsHuataiAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'funds_huatai', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'funds_huatai', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return FUNDS_HUATAI_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/funds_huatai', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readFundsHuataiJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-funds_huatai')) return res.status(401).send('未授权')
}
const maxAgeFundsHuatai = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'funds_huatai', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'funds_huatai', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return FUNDS_HUATAI_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeFundsHuatai, path: '/' })
return res.redirect('/tools/funds_huatai')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/funds_huatai', (req, res, next) => {
if (hasFundsHuataiAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/funds_huatai', (req, res, next) => {
if (hasFundsHuataiAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
// ============ free_show(免费演出雷达)鉴权 ============
const FREE_SHOW_AUTH_COOKIE = 'free_show_gate'
const readFreeShowAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'free_show', 'auth_config.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return {}
}
const readFreeShowJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'free_show.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getFreeShowCookieName = () => {
try { const n = String(readFreeShowAuthConfig().cookieName || ''); if (n) return n } catch {}
return FREE_SHOW_AUTH_COOKIE
}
const getFreeShowMaxAge = () => {
try {
const days = Number(readFreeShowAuthConfig().max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 30 * 24 * 3600 * 1000
}
const getFreeShowGuidConfig = () => {
try {
const cfg = readFreeShowAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch { return { key: 'guid', value: '' } }
}
const hasValidFreeShowGuid = req => {
try {
const cfg = readFreeShowAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getFreeShowGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const isFreeShowForceGuid = () => {
try {
const cfg = readFreeShowAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return false
} catch { return false }
}
const hasFreeShowAuth = req => {
try {
const config = readFreeShowAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
return cookies[getFreeShowCookieName()] === '1'
} catch { return false }
}
app.get('/tools/free_show', (req, res, next) => {
setNoCache(res)
try {
const forceGuid = isFreeShowForceGuid()
if (hasFreeShowAuth(req)) return next()
const guidKey = getFreeShowGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidFreeShowGuid(req)) {
const maxAge = getFreeShowMaxAge()
const cookieName = getFreeShowCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/free_show/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readFreeShowJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-free_show')) return res.status(401).send('未授权')
}
const maxAge = getFreeShowMaxAge()
const cookieName = getFreeShowCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/free_show/index.html')
} catch (e) { return res.status(401).send('未授权') }
})
app.use('/tools/free_show', (req, res, next) => {
const forceGuid = isFreeShowForceGuid()
if (hasFreeShowAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
const guidKey = getFreeShowGuidConfig().key
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (isHandshake && hasValidFreeShowGuid(req)) {
const maxAge = getFreeShowMaxAge()
const cookieName = getFreeShowCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/free_show/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
if (isHandshake && String(req.query.token || '')) {
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readFreeShowJwk()))
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-free_show')) return res.status(401).send('未授权')
const maxAge = getFreeShowMaxAge()
const cookieName = getFreeShowCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/free_show/index.html')
}
}
if (isHandshake && forceGuid) return res.status(401).send('未授权')
return res.status(401).send('未授权')
})
app.use('/api/free_show', (req, res, next) => {
if (hasFreeShowAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const CCB_PRIVATE_FUNDS_AUTH_COOKIE = 'ccb_private_funds_gate'
const readCcbPrivateFundsJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'ccb_private_funds.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasCcbPrivateFundsAuth = req => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'ccb_private_funds', 'auth_config.json')
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
if (config.enable_auth === false) return true
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'ccb_private_funds', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CCB_PRIVATE_FUNDS_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/ccb_private_funds', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readCcbPrivateFundsJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-ccb_private_funds')) return res.status(401).send('未授权')
}
const maxAge = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'ccb_private_funds', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'ccb_private_funds', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CCB_PRIVATE_FUNDS_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/ccb_private_funds')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/ccb_private_funds', (req, res, next) => {
if (hasCcbPrivateFundsAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/ccb_private_funds', (req, res, next) => {
if (hasCcbPrivateFundsAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const MARKETS_AUTH_COOKIE = 'markets_gate'
const readMarketsAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'markets', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readMarketsJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'markets.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getMarketsCookieName = () => {
try {
const cfg = readMarketsAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return MARKETS_AUTH_COOKIE
}
const getMarketsMaxAge = () => {
try {
const cfg = readMarketsAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 120 * 24 * 3600 * 1000
}
const getMarketsGuidConfig = () => {
try {
const cfg = readMarketsAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch {
return { key: 'guid', value: '' }
}
}
const hasValidMarketsGuid = (req) => {
try {
const cfg = readMarketsAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getMarketsGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const hasMarketsAuth = (req) => {
try {
const config = readMarketsAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getMarketsCookieName()
return cookies[name] === '1'
} catch { return false }
}
const isMarketsForceGuid = () => {
try {
const cfg = readMarketsAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return false
} catch { return false }
}
app.get('/tools/markets', (req, res, next) => {
try {
const forceGuid = isMarketsForceGuid()
if (hasMarketsAuth(req)) return next()
const guidKey = getMarketsGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidMarketsGuid(req)) {
const maxAgeMarkets = getMarketsMaxAge()
const cookieName = getMarketsCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeMarkets, path: '/' })
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readMarketsJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-markets')) return res.status(401).send('未授权')
}
const maxAgeMarkets = getMarketsMaxAge()
const cookieName = getMarketsCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeMarkets, path: '/' })
return res.redirect('/tools/markets')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/markets', (req, res, next) => {
if (hasMarketsAuth(req)) return next()
const guidKey = getMarketsGuidConfig().key
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && hasValidMarketsGuid(req)) return next()
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/markets', (req, res, next) => {
if (hasMarketsAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const WALL_AUTH_COOKIE = 'wall_gate'
const readWallJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'wall.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasWallAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'wall', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'wall', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WALL_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/wall', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readWallJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-wall-')) return res.status(401).send('未授权')
}
const maxAgeWall = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'wall', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'wall', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WALL_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeWall, path: '/' })
return res.redirect('/tools/wall')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/wall', (req, res, next) => {
if (hasWallAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/wall', (req, res, next) => {
if (hasWallAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const PRIVATE_CLIPBOARD_AUTH_COOKIE = 'private_clipboard_gate'
const readPrivateClipboardAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'private_clipboard', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readPrivateClipboardJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'private_clipboard.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getPrivateClipboardCookieName = () => {
try {
const cfg = readPrivateClipboardAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return PRIVATE_CLIPBOARD_AUTH_COOKIE
}
const getPrivateClipboardMaxAge = () => {
try {
const cfg = readPrivateClipboardAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 120 * 24 * 3600 * 1000
}
const getPrivateClipboardGuidConfig = () => {
try {
const cfg = readPrivateClipboardAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'GUID').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'GUID', value: valueRaw }
} catch {
return { key: 'GUID', value: '' }
}
}
const hasValidPrivateClipboardGuid = (req) => {
try {
const cfg = readPrivateClipboardAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getPrivateClipboardGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const hasPrivateClipboardBusinessAuth = (req) => {
try {
const mod = require('./private_clipboard')
if (mod && typeof mod.hasBusinessAuth === 'function') return !!mod.hasBusinessAuth(req)
} catch {}
return false
}
const hasPrivateClipboardAuth = (req) => {
try {
const cfg = readPrivateClipboardAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getPrivateClipboardCookieName()
if (cookies[name] === '1') return true
// Rule 12.10:第4层业务登录态(HMAC 签名校验通过)同样视为外层有效授权,
// 避免 gate cookie 过期后已登录用户“贴和传没有响应”
return hasPrivateClipboardBusinessAuth(req)
} catch { return false }
}
const isPrivateClipboardForceGuid = () => {
try {
const cfg = readPrivateClipboardAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return true
} catch { return true }
}
app.get('/tools/private_clipboard', (req, res, next) => {
try {
const forceGuid = isPrivateClipboardForceGuid()
if (hasPrivateClipboardAuth(req)) return next()
const guidKey = getPrivateClipboardGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidPrivateClipboardGuid(req)) {
const maxAgeClipboard = getPrivateClipboardMaxAge()
const cookieName = getPrivateClipboardCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeClipboard, path: '/' })
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readPrivateClipboardJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-private_clipboard')) return res.status(401).send('未授权')
}
const maxAgeClipboard = getPrivateClipboardMaxAge()
const cookieName = getPrivateClipboardCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeClipboard, path: '/' })
return next()
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/private_clipboard', (req, res, next) => {
const forceGuid = isPrivateClipboardForceGuid()
if (hasPrivateClipboardAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && hasValidPrivateClipboardGuid(req)) return next()
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/private_clipboard', (req, res, next) => {
if (hasPrivateClipboardAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/private_clipboard', (req, res, next) => {
// 白名单:登录和登出接口不需要鉴权
if (req.path === '/auth/login' || req.path === '/auth/logout') return next()
if (hasPrivateClipboardAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.use('/api/private_clipboard', (req, res, next) => {
if (privateClipboardRouter) return privateClipboardRouter(req, res, next)
return res.status(503).json({ ok: false, error: 'service_not_ready' })
})
const OSS_FILE_CABINET_AUTH_COOKIE = 'oss_file_cabinet_gate'
const readOssFileCabinetAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'oss_file_cabinet', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readOssFileCabinetJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'oss_file_cabinet.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getOssFileCabinetCookieName = () => {
try {
const cfg = readOssFileCabinetAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return OSS_FILE_CABINET_AUTH_COOKIE
}
const getOssFileCabinetMaxAge = () => {
try {
const cfg = readOssFileCabinetAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 30 * 24 * 3600 * 1000
}
const getOssFileCabinetGuidConfig = () => {
try {
const cfg = readOssFileCabinetAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch { return { key: 'guid', value: '' } }
}
const hasValidOssFileCabinetGuid = (req) => {
try {
const cfg = readOssFileCabinetAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getOssFileCabinetGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const hasOssFileCabinetAuth = (req) => {
try {
const cfg = readOssFileCabinetAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getOssFileCabinetCookieName()
return cookies[name] === '1'
} catch { return false }
}
const isOssFileCabinetForceGuid = () => {
try {
const cfg = readOssFileCabinetAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return true
} catch { return true }
}
app.get('/tools/oss_file_cabinet', (req, res, next) => {
setNoCache(res)
try {
const forceGuid = isOssFileCabinetForceGuid()
if (hasOssFileCabinetAuth(req)) return next()
const guidKey = getOssFileCabinetGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidOssFileCabinetGuid(req)) {
const maxAgeOfc = getOssFileCabinetMaxAge()
const cookieName = getOssFileCabinetCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
return res.redirect('/tools/oss_file_cabinet/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readOssFileCabinetJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-oss_file_cabinet')) return res.status(401).send('未授权')
}
const maxAgeOfc = getOssFileCabinetMaxAge()
const cookieName = getOssFileCabinetCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
return res.redirect('/tools/oss_file_cabinet/index.html')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/oss_file_cabinet', (req, res, next) => {
const forceGuid = isOssFileCabinetForceGuid()
if (hasOssFileCabinetAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
const guidKey = getOssFileCabinetGuidConfig().key
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (isHandshake && hasValidOssFileCabinetGuid(req)) {
const maxAgeOfc = getOssFileCabinetMaxAge()
const cookieName = getOssFileCabinetCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
return res.redirect('/tools/oss_file_cabinet/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
if (isHandshake && String(req.query.token || '')) {
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readOssFileCabinetJwk()))
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-oss_file_cabinet')) return res.status(401).send('未授权')
const maxAgeOfc = getOssFileCabinetMaxAge()
const cookieName = getOssFileCabinetCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeOfc, path: '/' })
return res.redirect('/tools/oss_file_cabinet/index.html')
}
}
if (isHandshake && forceGuid) return res.status(401).send('未授权')
return res.status(401).send('未授权')
})
app.use('/api/oss_file_cabinet', (req, res, next) => {
if (req.path.startsWith('/share/access/')) return next()
if (hasOssFileCabinetAuth(req)) return next()
return res.status(401).json({ ok: false, error: '未授权' })
})
const WEEKLY_AUTH_COOKIE = 'weekly_gate'
const readWeeklyJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'weekly.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasWeeklyAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'weekly', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weekly', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WEEKLY_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/weekly', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readWeeklyJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-weekly')) return res.status(401).send('未授权')
}
const maxAgeWeekly = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weekly', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weekly', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WEEKLY_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeWeekly })
return res.redirect('/tools/weekly')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/weekly', (req, res, next) => {
if (hasWeeklyAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/weekly', (req, res, next) => {
if (hasWeeklyAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/weekly', (req, res, next) => {
try {
const p = String(req.path || '')
if (p.startsWith('/ext/')) return next()
if (p === '/auth/local_creds') return next()
} catch {}
if (hasWeeklyAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const CLOUD_NOTES_AUTH_COOKIE = 'cloud_notes_gate'
const hasCloudNotesAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'cloud_notes', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud_notes', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CLOUD_NOTES_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
const getCloudNotesCookieName = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud_notes', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CLOUD_NOTES_AUTH_COOKIE
}
// Cloud Notes 登录速率限制:同一 IP 5 次失败后锁定 15 分钟
const cnLoginRateLimit = new Map()
const CN_LOGIN_MAX_FAIL = 5
const CN_LOGIN_LOCK_MS = 15 * 60 * 1000
const checkCnLoginRate = (ip) => {
const rec = cnLoginRateLimit.get(ip)
if (!rec) return { ok: true }
if (rec.lockedUntil && Date.now() < rec.lockedUntil) return { ok: false, retryAfter: Math.ceil((rec.lockedUntil - Date.now()) / 1000) }
if (rec.failCount >= CN_LOGIN_MAX_FAIL) {
rec.lockedUntil = Date.now() + CN_LOGIN_LOCK_MS
rec.failCount = 0
return { ok: false, retryAfter: Math.ceil(CN_LOGIN_LOCK_MS / 1000) }
}
return { ok: true }
}
const recordCnLoginFail = (ip) => {
const rec = cnLoginRateLimit.get(ip) || { failCount: 0 }
rec.failCount++
cnLoginRateLimit.set(ip, rec)
}
const recordCnLoginSuccess = (ip) => {
cnLoginRateLimit.delete(ip)
}
// 每分钟清理一次过期记录
setInterval(() => {
const now = Date.now()
for (const [ip, rec] of cnLoginRateLimit.entries()) {
if (rec.lockedUntil && now > rec.lockedUntil + 60000) cnLoginRateLimit.delete(ip)
}
}, 60000)
app.post('/api/cloud_notes/login', (req, res) => {
try {
const clientIp = req.ip || req.connection.remoteAddress || 'unknown'
// 检查速率限制
const rateCheck = checkCnLoginRate(clientIp)
if (!rateCheck.ok) {
return res.status(429).json({ ok: false, error: '登录尝试过于频繁,请在 ' + rateCheck.retryAfter + ' 秒后重试' })
}
const body = req.body || {}
const u = cloudNotes.login(String(body.username || ''), String(body.password || ''))
if (!u) {
recordCnLoginFail(clientIp)
return res.status(401).json({ ok: false, error: 'unauthorized' })
}
recordCnLoginSuccess(clientIp)
const maxAgeCN = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud_notes', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 1 * 24 * 3600 * 1000
})()
const cookieName = getCloudNotesCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeCN })
res.json({ ok: true, user: { id: u.id, username: u.username } })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/logout', (req, res) => {
try {
const cookieName = getCloudNotesCookieName()
res.clearCookie(cookieName, { httpOnly: true, sameSite: 'lax' })
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/auth/local_creds', (req, res) => {
try {
const creds = readLocalCreds()
if (!checkLocalCredsToken(req, creds)) return sendNoCreds(res)
const u = creds.CLOUD_NOTES_USERNAME || ''
const p = creds.CLOUD_NOTES_PASSWORD || ''
if (!u && !p) return sendNoCreds(res)
res.json({ ok: true, creds: { username: u, password: p } })
} catch { sendNoCreds(res) }
})
app.get('/tools/cloud_notes', (req, res) => {
try {
return res.sendFile(path.join(process.cwd(), 'public', 'tools', 'cloud_notes', 'index.html'))
} catch { return res.status(404).send('Not Found') }
})
app.use('/uploads/cloud_notes', (req, res, next) => {
if (hasCloudNotesAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/cloud_notes', (req, res, next) => {
if (req.path === '/login' || req.path === '/logout' || req.path === '/auth/local_creds') return next()
if (hasCloudNotesAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/cloud_notes/me', (req, res) => {
try {
if (!hasCloudNotesAuth(req)) return res.status(401).json({ ok: false })
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1, username: '用户' } })()
res.json({ ok: true, user: { id: u.id, username: String(u.username || '用户') } })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const cnImagesDir = path.join(process.cwd(), 'uploads', 'cloud_notes', 'images')
if (!fs.existsSync(cnImagesDir)) fs.mkdirSync(cnImagesDir, { recursive: true })
const cnFilesDir = path.join(process.cwd(), 'uploads', 'cloud_notes', 'files')
if (!fs.existsSync(cnFilesDir)) fs.mkdirSync(cnFilesDir, { recursive: true })
app.post('/api/cloud_notes/upload/image', express.raw({ type: 'application/octet-stream', limit: '50mb' }), (req, res) => {
try {
const raw = req.body || Buffer.alloc(0)
const name = String(req.query.filename || 'image').replace(/[<>:"/\\|?*]/g, '_')
const out = `${Date.now()}-${name}`
fs.writeFileSync(path.join(cnImagesDir, out), raw)
res.json({ ok: true, path: `/uploads/cloud_notes/images/${out}` })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/attachment', express.raw({ type: 'application/octet-stream', limit: '200mb' }), (req, res) => {
try {
const raw = req.body || Buffer.alloc(0)
const name = String(req.query.filename || 'file.bin').replace(/[<>:"/\\|?*]/g, '_')
const note_id = parseInt(String(req.query.note_id || '0'), 10) || 0
const out = `${Date.now()}-${name}`
const full = path.join(cnFilesDir, out)
fs.writeFileSync(full, raw)
const stats = fs.statSync(full)
const id = cloudNotes.createAttachment({ note_id, filename: name, file_path: `/uploads/cloud_notes/files/${out}`, file_size: stats.size })
res.json({ ok: true, id, path: `/uploads/cloud_notes/files/${out}` })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/attachments', (req, res) => {
try {
const noteId = parseInt(String(req.query.note_id || '0'), 10) || 0
if (!noteId) return res.json({ ok: true, rows: [] })
const rows = cloudNotes.listAttachments(noteId)
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const summarizeNoteHtml = html => {
const plain = String(html || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
if (!plain) return ''
if (plain.length <= 120) return plain
return plain.slice(0, 120) + '...'
}
const WEEKLY_PASSPHRASE = process.env.SYSTEM_AUTH_PASS_93220 || process.env.WEEKLY_PASSWORD || ''
const weeklyCfgPath = path.join(process.cwd(), 'config', 'weekly.json')
const getWeeklyConfig = () => {
try {
if (fs.existsSync(weeklyCfgPath)) return JSON.parse(fs.readFileSync(weeklyCfgPath, 'utf-8'))
} catch {}
return {}
}
const getWeeklyBaseUrl = () => {
const cfg = getWeeklyConfig()
const base = cfg && cfg.internal_base_url ? String(cfg.internal_base_url).trim() : ''
if (base) return base
const p = process.env.PORT || 5050
return `http://127.0.0.1:${p}`
}
const callWeeklyCloudNote = async payload => {
try {
const url = `${getWeeklyBaseUrl()}/api/weekly/ext/cloud_note`
const resp = await axios.post(url, Object.assign({ key: WEEKLY_PASSPHRASE }, payload || {}), { timeout: 5000 })
const data = resp && resp.data ? resp.data : {}
return {
ok: !!data.ok,
id: data.id || 0,
week_id: data.week_id || '',
deleted: !!data.deleted,
skipped: !!data.skipped,
reason: data.reason || '',
error: data.ok ? '' : String(data.error || '')
}
} catch (e) {
return { ok: false, id: 0, week_id: '', deleted: false, skipped: false, reason: '', error: String(e && e.message || e || 'error') }
}
}
const sortNotesForList = rows => {
const list = Array.isArray(rows) ? rows.slice() : []
list.sort((a, b) => {
const ap = a && a.pinned ? 1 : 0
const bp = b && b.pinned ? 1 : 0
if (ap !== bp) return bp - ap
const as = a && a.starred ? 1 : 0
const bs = b && b.starred ? 1 : 0
if (as !== bs) return bs - as
const at = String((a && a.title) || '')
const bt = String((b && b.title) || '')
if (at === bt) return (a && a.id ? a.id : 0) - (b && b.id ? b.id : 0)
try { return at.localeCompare(bt) } catch { return at > bt ? 1 : -1 }
})
return list
}
app.get('/api/cloud_notes/dashboard', (req, res) => {
try {
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const data = cloudNotes.dashboard(parseInt(String(u.id||'1'),10))
res.json({ ok: true, data })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/cloud_notes/attachments/:id', (req, res) => {
try {
const id = parseInt(String(req.params.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
cloudNotes.deleteAttachmentWithFile(id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/note/save', async (req, res) => {
try {
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const body = Object.assign({}, req.body || {}, { user_id: parseInt(String(u.id||'1'),10) })
const rawId = parseInt(String(body.id || '0'), 10) || 0
const isCreate = !rawId
const result = cloudNotes.saveNote(body)
if (result && result.conflict) {
return res.status(409).json({ ok: false, conflict: true, latest: result.latest || null })
}
const id = result && result.note_id ? result.note_id : 0
const version_id = result && result.version_id ? result.version_id : 0
let weekly_sync = null
try {
const row = cloudNotes.getNote(id)
const summary = summarizeNoteHtml(row && row.html)
weekly_sync = await callWeeklyCloudNote({
action: isCreate ? 'create' : 'update',
note_id: id,
title: row && row.title,
summary,
pinned: row && row.pinned,
starred: row && row.starred
})
} catch (e) {
weekly_sync = { ok: false, id: 0, week_id: '', deleted: false, skipped: false, reason: '', error: String(e && e.message || e || 'error') }
}
res.json({ ok: true, id, version_id, weekly_sync })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/note/clone_attachments', (req, res) => {
try {
const body = req.body || {}
const srcId = parseInt(String(body.src_id || '0'), 10) || 0
const destId = parseInt(String(body.dest_id || '0'), 10) || 0
if (!srcId || !destId || srcId === destId) {
return res.status(400).json({ ok: false, error: 'bad_id' })
}
cloudNotes.cloneAttachments(srcId, destId)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/note/versions', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const versions = cloudNotes.listVersions(id)
res.json({ ok: true, versions })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/note/list', (req, res) => {
try {
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const rows = cloudNotes.listNotes(parseInt(String(u.id||'1'),10))
res.json({ ok: true, rows: sortNotesForList(rows) })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/note/list_by_tag', (req, res) => {
try {
const tagId = parseInt(String(req.query.tag_id || '0'), 10) || 0
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const rows = cloudNotes.listNotesByTag(parseInt(String(u.id||'1'),10), tagId)
res.json({ ok: true, rows: sortNotesForList(rows) })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/note/trash_list', (req, res) => {
try {
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const rows = cloudNotes.listTrashNotes(parseInt(String(u.id||'1'),10))
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/note/search', (req, res) => {
try {
const q = String(req.query.q || '').trim()
if (!q) return res.json({ ok: true, rows: [], keyword: '' })
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const rows = cloudNotes.searchNotes(parseInt(String(u.id||'1'),10), q)
res.json({ ok: true, rows, keyword: q })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/note/get', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const row = cloudNotes.getNote(id)
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/note/star', (req, res) => {
try {
const body = req.body || {}
cloudNotes.setStar({ id: parseInt(String(body.id||'0'),10), starred: !!body.starred })
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/note/pin', (req, res) => {
try {
const body = req.body || {}
cloudNotes.setPin({ id: parseInt(String(body.id||'0'),10), pinned: !!body.pinned })
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/note/trash', async (req, res) => {
try {
const body = req.body || {}
const id = parseInt(String(body.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
let weekly_sync = null
try {
const row = cloudNotes.getNote(id)
const summary = summarizeNoteHtml(row && row.html)
weekly_sync = await callWeeklyCloudNote({
action: 'soft_delete',
note_id: id,
title: row && row.title,
summary,
pinned: row && row.pinned,
starred: row && row.starred
})
} catch (e) {
weekly_sync = { ok: false, id: 0, week_id: '', deleted: false, skipped: false, reason: '', error: String(e && e.message || e || 'error') }
}
cloudNotes.softDeleteNote(id)
res.json({ ok: true, weekly_sync })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/note/restore', (req, res) => {
try {
const body = req.body || {}
const id = parseInt(String(body.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
cloudNotes.restoreNote(id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/note/trash_delete', async (req, res) => {
try {
const body = req.body || {}
const id = parseInt(String(body.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
let weekly_sync = null
try {
weekly_sync = await callWeeklyCloudNote({ action: 'hard_delete', note_id: id })
} catch (e) {
weekly_sync = { ok: false, id: 0, week_id: '', deleted: false, skipped: false, reason: '', error: String(e && e.message || e || 'error') }
}
cloudNotes.hardDeleteNote(id)
res.json({ ok: true, weekly_sync })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/tags/list', (req, res) => {
try {
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const rows = cloudNotes.listTags(parseInt(String(u.id||'1'),10))
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/tags/save', (req, res) => {
try {
const u = (() => { const c = cloudNotes.login('probe', ''); return c || { id: 1 } })()
const body = req.body || {}
const tags = Array.isArray(body.tags) ? body.tags : []
cloudNotes.saveTags(parseInt(String(u.id||'1'),10), tags)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_notes/settings/tiny_toolbar', (req, res) => {
try {
const settings = cloudNotes.getTinyToolbarSettings()
res.json({ ok: true, settings })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_notes/settings/tiny_toolbar', (req, res) => {
try {
const body = req.body || {}
const saved = cloudNotes.saveTinyToolbarSettings(body.settings || {})
res.json({ ok: true, settings: saved })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const CLOUD_SHEETS_AUTH_COOKIE = 'cloud_sheets_gate'
const getCloudSheetsCookieName = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud_sheets', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CLOUD_SHEETS_AUTH_COOKIE
}
const hasCloudSheetsAuth = req => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'cloud_sheets', 'auth_config.json')
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
if (config.enable_auth === false) return true
}
const cookies = parseCookie(req.headers.cookie || '')
return cookies[getCloudSheetsCookieName()] === '1'
} catch {
return false
}
}
app.post('/api/cloud_sheets/login', (req, res) => {
try {
const body = req.body || {}
const u = cloudSheets.login(String(body.username || ''), String(body.password || ''))
if (!u) return res.status(401).json({ ok: false, error: 'unauthorized' })
const maxAge = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'cloud_sheets', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 1 * 24 * 3600 * 1000
})()
res.cookie(getCloudSheetsCookieName(), '1', { httpOnly: true, sameSite: 'lax', maxAge })
res.json({ ok: true, user: { id: u.id, username: u.username } })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/logout', (req, res) => {
try {
res.clearCookie(getCloudSheetsCookieName(), { httpOnly: true, sameSite: 'lax' })
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_sheets/auth/local_creds', (req, res) => {
try {
const creds = readLocalCreds()
if (!checkLocalCredsToken(req, creds)) return sendNoCreds(res)
const u = creds.CLOUD_SHEETS_USERNAME || ''
const p = creds.CLOUD_SHEETS_PASSWORD || ''
if (!u && !p) return sendNoCreds(res)
res.json({ ok: true, creds: { username: u, password: p } })
} catch { sendNoCreds(res) }
})
app.get('/tools/cloud_sheets', (req, res) => {
try {
return res.sendFile(path.join(process.cwd(), 'public', 'tools', 'cloud_sheets', 'index.html'))
} catch { return res.status(404).send('Not Found') }
})
app.use('/uploads/cloud_sheets', (req, res, next) => {
if (hasCloudSheetsAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/cloud_sheets', (req, res, next) => {
if (req.path === '/login' || req.path === '/logout' || req.path === '/auth/local_creds') return next()
if (hasCloudSheetsAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/cloud_sheets/me', (req, res) => {
try {
if (!hasCloudSheetsAuth(req)) return res.status(401).json({ ok: false })
const u = (() => { const c = cloudSheets.login('probe', ''); return c || { id: 1, username: '用户' } })()
res.json({ ok: true, user: { id: u.id, username: String(u.username || '用户') } })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const csFilesDir = path.join(process.cwd(), 'uploads', 'cloud_sheets', 'files')
if (!fs.existsSync(csFilesDir)) fs.mkdirSync(csFilesDir, { recursive: true })
app.post('/api/cloud_sheets/attachment', express.raw({ type: 'application/octet-stream', limit: '200mb' }), (req, res) => {
try {
const raw = req.body || Buffer.alloc(0)
const name = String(req.query.filename || 'file.bin').replace(/[<>:"/\\|?*]/g, '_')
const sheet_id = parseInt(String(req.query.sheet_id || '0'), 10) || 0
const out = `${Date.now()}-${name}`
const full = path.join(csFilesDir, out)
fs.writeFileSync(full, raw)
const stats = fs.statSync(full)
const id = cloudSheets.createAttachment({ sheet_id, filename: name, file_path: `/uploads/cloud_sheets/files/${out}`, file_size: stats.size })
res.json({ ok: true, id, path: `/uploads/cloud_sheets/files/${out}` })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_sheets/attachments', (req, res) => {
try {
const sheetId = parseInt(String(req.query.sheet_id || '0'), 10) || 0
if (!sheetId) return res.json({ ok: true, rows: [] })
const rows = cloudSheets.listAttachments(sheetId)
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/cloud_sheets/attachments/:id', (req, res) => {
try {
const id = parseInt(String(req.params.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
cloudSheets.deleteAttachmentWithFile(id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/sheet/save', (req, res) => {
try {
const u = (() => { const c = cloudSheets.login('probe', ''); return c || { id: 1 } })()
const body = Object.assign({}, req.body || {}, { user_id: parseInt(String(u.id || '1'), 10) })
const result = cloudSheets.saveSheet(body)
if (result && result.conflict) {
return res.status(409).json({ ok: false, conflict: true, latest: result.latest || null })
}
const id = result && result.sheet_id ? result.sheet_id : 0
const version_id = result && result.version_id ? result.version_id : 0
res.json({ ok: true, id, version_id })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/sheet/clone_attachments', (req, res) => {
try {
const body = req.body || {}
const srcId = parseInt(String(body.src_id || '0'), 10) || 0
const destId = parseInt(String(body.dest_id || '0'), 10) || 0
if (!srcId || !destId || srcId === destId) return res.status(400).json({ ok: false, error: 'bad_id' })
cloudSheets.cloneAttachments(srcId, destId)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_sheets/sheet/list', (req, res) => {
try {
const u = (() => { const c = cloudSheets.login('probe', ''); return c || { id: 1 } })()
const rows = cloudSheets.listSheets(parseInt(String(u.id || '1'), 10))
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_sheets/sheet/trash_list', (req, res) => {
try {
const u = (() => { const c = cloudSheets.login('probe', ''); return c || { id: 1 } })()
const rows = cloudSheets.listTrashSheets(parseInt(String(u.id || '1'), 10))
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_sheets/sheet/search', (req, res) => {
try {
const q = String(req.query.q || '').trim()
if (!q) return res.json({ ok: true, rows: [], keyword: '' })
const u = (() => { const c = cloudSheets.login('probe', ''); return c || { id: 1 } })()
const rows = cloudSheets.searchSheets(parseInt(String(u.id || '1'), 10), q)
res.json({ ok: true, rows, keyword: q })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_sheets/sheet/get', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const row = cloudSheets.getSheet(id)
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud_sheets/sheet/versions', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const versions = cloudSheets.listVersions(id)
res.json({ ok: true, versions })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/sheet/star', (req, res) => {
try {
const body = req.body || {}
cloudSheets.setStar({ id: parseInt(String(body.id || '0'), 10), starred: !!body.starred })
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/sheet/pin', (req, res) => {
try {
const body = req.body || {}
cloudSheets.setPin({ id: parseInt(String(body.id || '0'), 10), pinned: !!body.pinned })
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/sheet/trash', (req, res) => {
try {
const body = req.body || {}
const id = parseInt(String(body.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
cloudSheets.softDeleteSheet(id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/sheet/restore', (req, res) => {
try {
const body = req.body || {}
const id = parseInt(String(body.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
cloudSheets.restoreSheet(id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud_sheets/sheet/trash_delete', (req, res) => {
try {
const body = req.body || {}
const id = parseInt(String(body.id || '0'), 10) || 0
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
cloudSheets.hardDeleteSheet(id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const DOC_CLOUD_KEEPER_AUTH_COOKIE = 'doc_cloud_keeper_gate'
const getDocCloudKeeperCookieName = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return DOC_CLOUD_KEEPER_AUTH_COOKIE
}
const hasDocCloudKeeperAuth = req => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper', 'auth_config.json')
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
if (config.enable_auth === false) return true
}
const cookies = parseCookie(req.headers.cookie || '')
return cookies[getDocCloudKeeperCookieName()] === '1'
} catch {
return false
}
}
const readDocCloudKeeperJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'doc_cloud_keeper.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const syncDocToCalendarUpsert = async docId => {
try {
const id = parseInt(String(docId || '0'), 10) || 0
if (!id) return { ok: false, error: 'bad_doc_id' }
const doc = docCloudKeeper.getDocument(id)
if (!doc || doc.deleted_at) return await docCalendarBridge.remove(id)
const folder = docCloudKeeper.getFolderById ? docCloudKeeper.getFolderById(doc.folder_id) : null
const folderName = folder ? String(folder.name || '') : '根目录'
return await docCalendarBridge.upsert(doc, folderName)
} catch (e) {
return { ok: false, error: String(e && e.message ? e.message : e) }
}
}
const syncDocToCalendarRemove = async docId => {
try { return await docCalendarBridge.remove(docId) } catch (e) { return { ok: false, error: String(e && e.message ? e.message : e) } }
}
app.post('/api/doc_cloud_keeper/login', (req, res) => {
try {
const body = req.body || {}
const u = docCloudKeeper.login(String(body.username || ''), String(body.password || ''))
if (!u) return res.status(401).json({ ok: false, error: 'unauthorized' })
const maxAge = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 1 * 24 * 3600 * 1000
})()
res.cookie(getDocCloudKeeperCookieName(), '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
res.json({ ok: true, user: { id: u.id, username: u.username } })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/logout', (req, res) => {
try {
res.clearCookie(getDocCloudKeeperCookieName(), { httpOnly: true, sameSite: 'lax', path: '/' })
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/auth/local_creds', (req, res) => {
try {
const creds = readLocalCreds()
if (!checkLocalCredsToken(req, creds)) return sendNoCreds(res)
const u = creds.DOCK_USERNAME || ''
const p = creds.DOCK_PASSWORD || ''
if (!u && !p) return sendNoCreds(res)
res.json({ ok: true, creds: { username: u, password: p } })
} catch { sendNoCreds(res) }
})
app.use('/tools/doc_cloud_keeper/DocHelper', (req, res, next) => {
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(404).send('Not Found')
})
app.get('/tools/doc_cloud_keeper', (req, res, next) => {
try {
const token = String(req.query.token || '').trim()
if (token) {
let payload = verifyJwtWithKeys(token, jwkKeys(readDocCloudKeeperJwk()))
if (!payload) {
try {
const dbg = debugVerifyJwt(token, [], { issStrict: false })
const now = Math.floor(Date.now() / 1000)
const exp = Number((dbg && dbg.payload && dbg.payload.exp) || 0)
const inGrace = Number.isFinite(exp) && exp > 0 && now > exp && (now - exp) <= (24 * 3600)
if (dbg && dbg.sig_ok && dbg.payload && inGrace) payload = dbg.payload
} catch {}
}
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-doc_cloud_keeper')) {
// invalid aud, just fall through to index.html
} else {
const maxAge = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 180 * 24 * 3600 * 1000
})()
res.cookie(getDocCloudKeeperCookieName(), '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/doc_cloud_keeper/index.html')
}
}
}
return res.sendFile(path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper', 'index.html'))
} catch { return res.status(401).send('未授权') }
})
app.use('/uploads/doc_cloud_keeper', (req, res, next) => {
setNoCache(res)
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(401).send('未授权')
})
const docCloudKeeperAgentTokens = new Map()
app.use('/api/doc_cloud_keeper', (req, res, next) => {
if (req.path === '/login' || req.path === '/logout' || req.path === '/auth/local_creds') return next()
if (req.path === '/docs/agent_upload' || req.path === '/docs/download') {
// 允许助手凭借有效 token 直接上传或下载,跳过 cookie 鉴权
const token = String(req.headers['authorization'] || '').replace('Bearer ', '').trim()
if (token) {
const docId = parseInt(String(req.query.id || req.query.doc_id || '0'), 10) || 0
const valid = docCloudKeeperAgentTokens.get(token)
if (valid && valid.doc_id === docId && Date.now() < valid.exp) {
return next()
}
}
}
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.use('/api/doc_calendar_bridge', (req, res, next) => {
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/doc_cloud_keeper/me', (req, res) => {
try {
if (!hasDocCloudKeeperAuth(req)) return res.status(401).json({ ok: false })
const u = docCloudKeeper.getCurrentUser()
res.json({ ok: true, user: { id: u.id, username: String(u.username || 'admin') } })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const dckTempDir = path.join(process.cwd(), 'uploads', 'doc_cloud_keeper', 'temp')
if (!fs.existsSync(dckTempDir)) fs.mkdirSync(dckTempDir, { recursive: true })
app.post('/api/doc_cloud_keeper/upload/temp', express.raw({ type: 'application/octet-stream', limit: '300mb' }), (req, res) => {
try {
const raw = req.body || Buffer.alloc(0)
const filename = String(req.query.filename || 'document.txt')
const sha256 = String(req.query.sha256 || '')
const out = docCloudKeeper.createTempUpload(filename, raw, sha256)
res.json({ ok: true, temp_name: out.temp_name, size_bytes: out.size_bytes, ext: out.ext, sha256: out.sha256 })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/oss_probe/upload', express.raw({ type: '*/*', limit: '300mb' }), async (req, res) => {
try {
const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || '')
const filename = String(req.query.filename || 'oss_probe.txt')
const mime = String(req.query.mime || 'application/octet-stream')
const out = await docCloudKeeper.probeUploadToOss({ filename, mime, buffer: raw })
res.json(out)
} catch (e) {
res.json({
ok: false,
logs: [
'开始 OSS 检测上传',
'上传过程中出现异常,流程已中断',
String(e && e.message ? e.message : e)
],
error: String(e && e.message ? e.message : e)
})
}
})
app.post('/api/doc_cloud_keeper/docs/create', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const id = docCloudKeeper.createDocumentByTemp(Object.assign({}, req.body || {}, { user_id: u.id }))
syncDocToCalendarUpsert(id).catch(() => {})
res.json({ ok: true, id })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/overwrite', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.overwriteDocumentByTemp(body)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/update', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.updateDocumentMeta(body)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/rename', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.renameDocument(body.id, body.filename)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/move', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.moveDocument(body.id, body.folder_id)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/trash', (req, res) => {
try {
const id = (req.body || {}).id
docCloudKeeper.softDeleteDocument(id)
syncDocToCalendarRemove(id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/restore', (req, res) => {
try {
const id = (req.body || {}).id
docCloudKeeper.restoreDocument(id)
syncDocToCalendarUpsert(id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/trash_delete', (req, res) => {
try {
const id = (req.body || {}).id
docCloudKeeper.hardDeleteDocument(id)
syncDocToCalendarRemove(id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/bulk_trash', (req, res) => {
try {
const body = req.body || {}
const out = docCloudKeeper.bulkSoftDelete(body.ids || [])
Promise.allSettled((body.ids || []).map(id => syncDocToCalendarRemove(id)))
res.json({ ok: true, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/bulk_move', (req, res) => {
try {
const body = req.body || {}
const out = docCloudKeeper.bulkMove(body.ids || [], body.folder_id)
Promise.allSettled((body.ids || []).map(id => syncDocToCalendarUpsert(id)))
res.json({ ok: true, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/bulk_trash_delete', (req, res) => {
try {
const body = req.body || {}
const out = docCloudKeeper.bulkHardDelete(body.ids || [])
Promise.allSettled((body.ids || []).map(id => syncDocToCalendarRemove(id)))
res.json({ ok: true, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/upsert', async (req, res) => {
try {
const body = req.body || {}
const out = await syncDocToCalendarUpsert(body.doc_id)
res.json({ ok: !!out.ok, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/remove', async (req, res) => {
try {
const body = req.body || {}
const out = await syncDocToCalendarRemove(body.doc_id)
res.json({ ok: !!out.ok, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/restore', async (req, res) => {
try {
const body = req.body || {}
const out = await syncDocToCalendarUpsert(body.doc_id)
res.json({ ok: !!out.ok, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_calendar_bridge/by_doc_id', (req, res) => {
try {
const docId = parseInt(String(req.query.doc_id || '0'), 10) || 0
const row = docCalendarBridge.byDocId(docId)
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/sync_full', async (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const active = docCloudKeeper.listDocuments({ user_id: u.id, include_deleted: false, sort_by: 'updated_at', sort_dir: 'desc' }) || []
const trashed = docCloudKeeper.listDocuments({ user_id: u.id, include_deleted: true, sort_by: 'updated_at', sort_dir: 'desc' }) || []
const upsertResults = await Promise.all(active.map(r => syncDocToCalendarUpsert(r.id)))
const removeResults = await Promise.all(trashed.map(r => syncDocToCalendarRemove(r.id)))
res.json({
ok: true,
stats: {
active: active.length,
trashed: trashed.length,
upsert_ok: upsertResults.filter(x => x && x.ok).length,
remove_ok: removeResults.filter(x => x && x.ok).length
}
})
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/list', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const rows = docCloudKeeper.listDocuments({
user_id: u.id,
include_deleted: String(req.query.trash || '') === '1',
q: req.query.q,
folder_id: req.query.folder_id,
priority: req.query.priority,
status: req.query.status,
sort_by: req.query.sort_by,
sort_dir: req.query.sort_dir
})
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/get', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const row = docCloudKeeper.getDocument(id)
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/get_edit_token', (req, res) => {
try {
const id = parseInt(String(req.body.id || '0'), 10) || 0
const row = docCloudKeeper.getDocument(id)
if (!row) throw new Error('Document not found')
// Generate a short-lived token (1 hour) specifically for the local agent
const crypto = require('crypto')
const token = crypto.randomBytes(32).toString('hex')
docCloudKeeperAgentTokens.set(token, {
doc_id: id,
exp: Date.now() + 3600 * 1000 // 1 hour
})
// Cleanup old tokens occasionally
if (docCloudKeeperAgentTokens.size > 100) {
const now = Date.now()
for (const [k, v] of docCloudKeeperAgentTokens.entries()) {
if (now > v.exp) docCloudKeeperAgentTokens.delete(k)
}
}
res.json({ ok: true, token, doc_id: id, filename: row.filename })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/agent_upload', express.raw({ type: 'application/octet-stream', limit: '300mb' }), (req, res) => {
try {
// 助手直接上传修改后的文件内容,需要提取 doc_id 和文件流
const raw = req.body || Buffer.alloc(0)
const docId = parseInt(String(req.query.doc_id || '0'), 10) || 0
const explicitSize = parseInt(String(req.query.size_bytes || '0'), 10) || raw.length
const row = docCloudKeeper.getDocument(docId)
if (!row) throw new Error('Document not found')
// 复用 overwrite 的逻辑:先存到 temp,再覆盖
const crypto = require('crypto')
const sha256 = crypto.createHash('sha256').update(raw).digest('hex')
const out = docCloudKeeper.createTempUpload(row.filename, raw, sha256)
docCloudKeeper.overwriteDocumentByTemp({
id: docId,
temp_name: out.temp_name,
size_bytes: explicitSize
})
syncDocToCalendarUpsert(docId).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const dckAttachmentName = rawName => {
const safe = String(rawName || 'document').replace(/[\r\n"]/g, '_')
const fallback = safe.replace(/[^\x20-\x7E]/g, '_')
const utf8 = encodeURIComponent(safe)
return `attachment; filename="${fallback}"; filename*=UTF-8''${utf8}`
}
const dckFormatSize = bytes => {
const n = Number(bytes || 0)
if (!Number.isFinite(n) || n <= 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let v = n
let i = 0
while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1 }
return `${v.toFixed(i === 0 ? 0 : 2)} ${units[i]}`
}
const dckSafeFileLabel = text => String(text || '').replace(/[:]/g, ':').replace(/[|]/g, '|').replace(/[<>"/\\?*]/g, '_').trim()
const dckHistoryDownloadName = payload => {
const data = payload || {}
const filename = dckSafeFileLabel(data.filename || 'document')
const sizeStr = dckSafeFileLabel(dckFormatSize(data.size_bytes || 0))
const recordTime = dckSafeFileLabel(String(data.record_time || '').replace(/:/g, ':'))
const uploadTime = dckSafeFileLabel(String(data.upload_time || '').replace(/:/g, ':'))
const sourceName = dckSafeFileLabel(data.source_name || '下载文件')
const deletedPrefix = data.is_deleted ? '【物理删除】' : ''
const parts = [
`文件:${filename}`,
`大小:${sizeStr}`
]
if (recordTime) parts.push(`记录时间:${recordTime}`)
if (uploadTime) parts.push(`上传时间:${uploadTime}`)
parts.push(`来源:${sourceName}`)
return `${deletedPrefix}${parts.join(' | ')}`
}
app.get('/api/doc_cloud_keeper/docs/download', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = docCloudKeeper.getDocumentForDownload(id)
if (!hit) return res.status(404).send('Not Found')
res.setHeader('Content-Disposition', dckAttachmentName(hit.row.filename))
res.setHeader('Content-Type', 'application/octet-stream')
res.sendFile(hit.full)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.get('/api/doc_cloud_keeper/docs/versions', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const versions = docCloudKeeper.listVersions(id)
res.json({ ok: true, versions })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/compare_versions', async (req, res) => {
try {
const comparison = await docCloudKeeper.compareDocumentVersions(req.body || {})
res.json({ ok: true, comparison })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/oss_versions', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const versions = docCloudKeeper.listOssBackups(id)
res.json({ ok: true, versions })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/version_download', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = docCloudKeeper.getVersionForDownload(id)
if (!hit) return res.status(404).send('Not Found')
const v = hit.row
const ext = v.ext ? `.${v.ext}` : ''
const friendlyName = dckHistoryDownloadName({
filename: v.filename || `version-${v.version_no || id}${ext}`,
size_bytes: v.size_bytes || 0,
record_time: v.created_at || '',
upload_time: '',
source_name: `云服务器版本库${ext}`
})
res.setHeader('Content-Disposition', dckAttachmentName(friendlyName))
res.setHeader('Content-Type', 'application/octet-stream')
res.sendFile(hit.full)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.get('/api/doc_cloud_keeper/docs/oss_version_download', async (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = await docCloudKeeper.createOssDownloadStream(id)
if (!hit || !hit.stream || !hit.row) return res.status(404).send('Not Found')
const row = hit.row
const ext = row.ext ? `.${row.ext}` : ''
const friendlyName = dckHistoryDownloadName({
filename: row.filename || `oss-backup-${id}${ext}`,
size_bytes: row.size_bytes || 0,
record_time: row.created_at || '',
upload_time: row.last_uploaded_at || '',
source_name: `OSS容灾库${ext}`,
is_deleted: !!row.is_physical_deleted
})
res.setHeader('Content-Disposition', dckAttachmentName(friendlyName))
res.setHeader('Content-Type', row.mime || 'application/octet-stream')
hit.stream.on('error', err => {
if (!res.headersSent) res.status(500).send(String(err && err.message ? err.message : err))
else res.destroy(err)
})
hit.stream.pipe(res)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.post('/api/doc_cloud_keeper/docs/restore_version', (req, res) => {
try {
docCloudKeeper.restoreVersion(req.body || {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/preview', async (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const out = await docCloudKeeper.previewDocument(id)
res.json(out)
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/preview_pdf', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = docCloudKeeper.getDocumentForPreview(id)
if (!hit) return res.status(404).send('Not Found')
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'inline; filename="preview.pdf"')
res.setHeader('Cache-Control', 'no-cache')
res.sendFile(hit.full)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.get('/api/doc_cloud_keeper/folders/list', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const rows = docCloudKeeper.listFolders(u.id)
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/create', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const id = docCloudKeeper.createFolder(Object.assign({}, req.body || {}, { user_id: u.id }))
res.json({ ok: true, id })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/update', (req, res) => {
try {
docCloudKeeper.updateFolder(req.body || {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/trash', (req, res) => {
try {
docCloudKeeper.softDeleteFolder((req.body || {}).id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/restore', (req, res) => {
try {
docCloudKeeper.restoreFolder((req.body || {}).id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/trash_delete', (req, res) => {
try {
docCloudKeeper.hardDeleteFolder((req.body || {}).id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/stats', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const s = docCloudKeeper.stats(u.id)
res.json({ ok: true, stats: s })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/change_password', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const body = req.body || {}
docCloudKeeper.changePassword(u.id, body.old_password, body.new_password)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const WEIBO_AUTH_COOKIE = 'weibo_gate'
const readWeiboJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'weibo.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasWeiboAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WEIBO_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/weibo', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readWeiboJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-weibo')) return res.status(401).send('未授权')
}
const maxAgeWeibo = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WEIBO_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeWeibo })
return res.redirect('/tools/weibo')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/weibo', (req, res, next) => {
if (hasWeiboAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/weibo', (req, res, next) => {
if (hasWeiboAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/ping', (req, res) => {
res.json({ ok: true })
})
app.get('/api/debug/https', (req, res) => {
res.json({ https: isHttps(req), xfwd: String((req.headers && req.headers['x-forwarded-proto']) || '') })
})
app.get('/api/debug/test-cookie', (req, res) => {
res.cookie('test_cookie', 'ok', { path: '/', maxAge: 60 * 1000 })
res.json({ ok: true })
})
const CALENDAR_AUTH_COOKIE = 'calendar_reminder_gate'
const readCalendarJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'calendar_reminder.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasCalendarAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CALENDAR_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/calendar_reminder', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readCalendarJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-calendar_reminder')) return res.status(401).send('未授权')
}
const maxAgeCal = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CALENDAR_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeCal })
return res.redirect('/tools/calendar_reminder')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/calendar_reminder', (req, res, next) => {
if (hasCalendarAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.get('/go', (req, res) => {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
res.set('Pragma', 'no-cache')
res.set('Expires', '0')
try {
const priv = readNavPrivateJwk()
if (!priv) return res.status(500).send('导航私钥未配置')
const systemId = String(req.query.systemId || '')
if (!systemId) return res.status(400).send('参数错误')
const now = Math.floor(Date.now()/1000)
const exp = now + Math.max(30, Math.min(300, Number(req.query.ttl||120)))
const jti = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2,10)
const iss = getNavIssFromFlags()
const token = signJwtRS256({ iss, aud: systemId, iat: now, exp, jti }, priv)
let dest = String(req.query.url || '')
if (systemId === 'TRAE-PSC') dest = '/tools/psc'
if (!dest) dest = mapSystemIdToUrl(systemId)
if (!dest) return res.status(400).send('目标地址缺失')
const hasQuery = dest.includes('?')
const sep = hasQuery ? '&' : '?'
const finalUrl = dest + sep + 'token=' + encodeURIComponent(token)
if (/^https?:\/\//i.test(finalUrl)) {
try {
const u = new URL(finalUrl)
const host = String(req.headers.host || '')
if (u.host !== host) {
res.set('Content-Type', 'text/html; charset=utf-8')
return res.status(200).send(`<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><script>location.href=${JSON.stringify(finalUrl)}</script></body></html>`)
}
} catch {}
}
res.redirect(finalUrl)
} catch (e) {
res.status(500).send('服务器错误')
}
})
app.get('/api/debug/jwt/verify', (req, res) => {
try {
const token = String(req.query.token || '')
const tool = String(req.query.tool || '')
const readers = {
'expense': () => EXPENSE_JWK,
'ai-lib': () => readAiLibJwk(),
'cloud': () => readCloudJwk(),
'funds_guoxin': () => readFundsGuoxinJwk(),
'funds_huatai': () => readFundsHuataiJwk(),
'free_show': () => readFreeShowJwk(),
'markets': () => readMarketsJwk(),
'wall': () => readWallJwk(),
'weekly': () => readWeeklyJwk(),
'weibo': () => readWeiboJwk()
}
const keys = jwkKeys((readers[tool] || (() => readAiLibJwk()))())
const payload = verifyJwtWithKeys(token, keys)
res.json({ ok: !!payload, keys: keys.length, payload })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/debug/nav/verify', (req, res) => {
try {
const token = String(req.query.token || '')
const issStrict = String(req.query.iss_strict || '') === '1' ? true : computeNavIssStrict()
const dbg = debugVerifyJwt(token, [], { issStrict })
const hasCookie = !!(req.headers && req.headers.cookie && parseCookie(req.headers.cookie || '')['nav_gate'] === '1')
res.json({ ok: dbg.ok, has_cookie: hasCookie, dbg })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/debug/nav/private', (req, res) => {
try {
const sources = []
let loaded = null
try {
const s = String(process.env.NAV_PRIVATE_JWK_JSON || process.env.NAV_PRIVATE_JWK || '')
if (s) { sources.push({ src: 'env_json', len: s.length }); loaded = JSON.parse(s) }
} catch {}
if (!loaded) {
const f = String(process.env.NAV_PRIVATE_JWK_FILE || '')
if (f) {
if (fs.existsSync(f)) { sources.push({ src: 'env_file', path: f, exists: true }); try { loaded = JSON.parse(fs.readFileSync(f, 'utf-8')) } catch {} }
else sources.push({ src: 'env_file', path: f, exists: false })
}
}
const candidates = [
path.join(process.cwd(), 'config', 'nav-private.jwk.json'),
path.resolve(__dirname, '../../config/nav-private.jwk.json'),
path.resolve(__dirname, '../config/nav-private.jwk.json')
]
for (const p of candidates) {
try {
const ex = fs.existsSync(p)
sources.push({ src: 'candidate', path: p, exists: ex })
if (!loaded && ex) { loaded = JSON.parse(fs.readFileSync(p, 'utf-8')) }
} catch {}
}
const ok = !!(loaded && loaded.kty && loaded.n && loaded.e && loaded.d)
res.json({ ok, sources, loaded_keys: loaded ? Object.keys(loaded) : [] })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/token_lab/auth/status', (req, res) => {
try {
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
return res.json({
ok: true,
authenticated: hasTokenLabAuth(req)
})
} catch (e) {
return res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/token_lab/auth/login', (req, res) => {
try {
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
const username = String(req.body?.username || '').trim()
const password = String(req.body?.password || '')
const expectedUser = String(process.env.TOKEN_LAB_USERNAME || '').trim()
const expectedPass = String(process.env.TOKEN_LAB_PASSWORD || '')
if (!expectedUser || !expectedPass) return res.status(500).json({ ok: false, error: 'token_lab_creds_not_configured' })
if (username !== expectedUser || password !== expectedPass) {
return res.status(401).json({ ok: false, error: '账号或密码错误' })
}
res.cookie(TOKEN_LAB_AUTH_COOKIE, '1', { httpOnly: true, sameSite: 'lax', maxAge: getTokenLabAuthMaxAge(), path: '/' })
return res.json({ ok: true })
} catch (e) {
return res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/token_lab/auth/logout', (req, res) => {
try {
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
res.clearCookie(TOKEN_LAB_AUTH_COOKIE, { httpOnly: true, sameSite: 'lax', path: '/' })
return res.json({ ok: true })
} catch (e) {
return res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.use('/api/token_lab', (req, res, next) => {
try {
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
if (req.path === '/auth/status' || req.path === '/auth/login' || req.path === '/auth/logout') return next()
if (hasTokenLabAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'token_lab_login_required' })
} catch {
return res.status(401).json({ ok: false, error: 'unauthorized' })
}
})
app.post('/api/token_lab/nav/generate', (req, res) => {
try {
const baseUrl = String(req.body?.baseUrl || '').trim()
const exp = parseInt(String(req.body?.exp || ''), 10)
const now = Math.floor(Date.now() / 1000)
if (!Number.isFinite(exp) || exp <= now) return res.status(400).json({ ok: false, error: 'bad_exp' })
if (exp > now + 3600 * 24 * 365 * 2) return res.status(400).json({ ok: false, error: 'exp_too_far' })
const priv = readNavPrivateJwk()
if (!priv) return res.status(500).json({ ok: false, error: 'nav_private_key_missing' })
const payload = {
iss: getNavIssFromFlags(),
iat: now,
exp,
jti: crypto.randomUUID()
}
const token = signJwtRS256(payload, priv)
const link = appendTokenToUrl(baseUrl || '/', token)
return res.json({ ok: true, token, link, payload })
} catch (e) {
return res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/token_lab/nav/verify', (req, res) => {
try {
const raw = String(req.body?.value || '').trim()
const token = extractJwtFromInput(raw)
if (!token) return res.status(400).json({ ok: false, error: 'missing_token' })
const result = debugVerifyJwt(token, [], { issStrict: computeNavIssStrict() })
return res.json({
ok: true,
token,
result,
payload: result.payload || null,
header: result.header || null
})
} catch (e) {
return res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/flags', (req, res) => {
const enabled = computeDebugEnabled()
res.json({ ok: true, debugToolEnabled: enabled, debug: { weibo: computeToolDebug('weibo'), markets: computeToolDebug('markets'), wall: computeToolDebug('wall'), funds_guoxin: computeToolDebug('funds_guoxin'), funds_huatai: computeToolDebug('funds_huatai'), free_show: computeToolDebug('free_show'), weekly: computeToolDebug('weekly') } })
})
app.get('/api/weibo/devtools/status', async (req, res) => {
try {
const cookie = await getCookiesFromDevTools(['https://m.weibo.cn','https://weibo.com'])
const s = String(cookie || '')
const pairs = s.split(';').map(v => v.trim()).filter(Boolean)
logJSON('weibo.devtools.status', { connected: !!cookie, cookie_len: s.length, pairs: pairs.length }, 'weibo')
res.json({ ok: true, connected: !!cookie, cookie_len: s.length, pairs: pairs.length })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/settings', (req, res) => {
const fileSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
const dbSettings = getSettings()
res.json({ file: fileSettings, db: dbSettings })
})
app.post('/api/settings', (req, res) => {
const body = req.body || {}
const users = Array.isArray(body.users) ? body.users : []
const keywords = Array.isArray(body.keywords) ? body.keywords : []
let headers = body.headers
const normalizeCookie = c => String(c || '').replace(/^Cookie\s*:\s*/i, '').replace(/\r?\n/g, '').trim()
if (typeof headers === 'string') {
headers = { Cookie: normalizeCookie(headers) }
} else if (headers && typeof headers === 'object') {
if (!headers.Cookie && headers.cookie) headers.Cookie = headers.cookie
if (headers.Cookie) headers.Cookie = normalizeCookie(headers.Cookie)
} else {
headers = {}
}
let count = parseInt(String(body.count || '50'), 10)
if (!Number.isFinite(count)) count = 50
count = Math.max(1, Math.min(200, count))
fs.writeFileSync(settingsPath, JSON.stringify({ users, keywords, headers, count }, null, 2))
for (const k of keywords) insertKeyword.run(String(k).trim())
for (const u of users) insertTrackedUser.run(String(u.uid || '').trim(), String(u.screen_name || '').trim())
try { insertSettingHistory.run({ tool: 'weibo', payload: JSON.stringify({ users, keywords, headers, count }) }) } catch {}
logJSON('settings.update', { users, keywords, headers, count })
res.json({ ok: true })
})
app.get('/api/weibo/:uid/fetch', async (req, res) => {
try {
const uid = req.params.uid
const fileSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
const count = Math.max(1, Math.min(200, parseInt(req.query.count || String(fileSettings.count || '50'), 10)))
logJSON('weibo.fetch.start', { uid, count }, 'weibo')
const pad = (n, w = 2) => String(n).padStart(w, '0')
const fmtBeijing = (dateVal) => {
try {
const tz = 'Asia/Shanghai'
const d = (dateVal instanceof Date) ? dateVal : new Date(dateVal)
if (isNaN(d.getTime())) return ''
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: tz,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).formatToParts(d)
const get = (type) => { const p = parts.find(x => x.type === type); return p ? p.value : '' }
const y = get('year'), m = get('month'), day = get('day'), h = get('hour'), mi = get('minute'), s = get('second')
const ms = pad(d.getMilliseconds(), 3)
return `${y}-${m}-${day} ${h}:${mi}:${s} ${ms}`
} catch { return '' }
}
const tryFetch = async (headersObj) => {
const posts = await fetchPosts(uid, count, headersObj || {})
for (const p of posts) {
insertPost.run(p)
if (p.retweeted_mid) updateRetweeted.run({
mid: p.mid,
retweeted_mid: p.retweeted_mid,
retweeted_screen_name: p.retweeted_screen_name,
retweeted_text: p.retweeted_text,
retweeted_text_plain: p.retweeted_text_plain,
retweeted_url: p.retweeted_url,
retweeted_pics_json: p.retweeted_pics_json,
text_plain: p.text_plain
})
}
const latest = posts.reduce((acc, p) => {
const t = String(p.created_at || '')
if (!t) return acc
if (!acc) return t
return (t > acc) ? t : acc
}, '')
return { fetched: posts.length, latest_time: latest }
}
const summarize = c => {
const s = String(c || '')
const pairs = s.split(';').map(v => v.trim()).filter(Boolean)
const names = pairs.map(p => p.split('=')[0]).filter(Boolean)
return { len: s.length, pairs: pairs.length, names: names.slice(0, 12) }
}
let fetched = 0
let latestTime = ''
let usedSource = ''
const devCookie = await getCookiesFromDevTools(['https://m.weibo.cn', 'https://weibo.com'])
if (devCookie) {
logJSON('weibo.fetch.try', { uid, source: 'devtools', cookie: summarize(devCookie) }, 'weibo')
try {
const r = await tryFetch({ Cookie: devCookie })
fetched = r.fetched; latestTime = r.latest_time
usedSource = 'devtools'
} catch (e) {
logJSON('weibo.fetch.try.error', { uid, source: 'devtools', error: String(e.message || e) }, 'weibo')
}
} else {
logJSON('weibo.fetch.try.skip', { uid, source: 'devtools' }, 'weibo')
}
if (!usedSource) {
const sHdr = fileSettings.headers || {}
const sCookie = sHdr.Cookie || sHdr.cookie || ''
if (sCookie) {
logJSON('weibo.fetch.try', { uid, source: 'settings', cookie: summarize(sCookie) }, 'weibo')
try {
const r = await tryFetch(sHdr)
fetched = r.fetched; latestTime = r.latest_time
usedSource = 'settings'
} catch (e) {
logJSON('weibo.fetch.try.error', { uid, source: 'settings', error: String(e.message || e) }, 'weibo')
}
} else {
logJSON('weibo.fetch.try.skip', { uid, source: 'settings' }, 'weibo')
}
}
if (!usedSource) {
const anonCookie = await bootstrapCookies(uid)
const headersPreferred = Object.assign({}, fileSettings.headers || {}, anonCookie ? { Cookie: anonCookie } : {})
logJSON('weibo.fetch.try', { uid, source: 'anonymous', cookie: summarize(anonCookie) }, 'weibo')
try {
const r = await tryFetch(headersPreferred)
fetched = r.fetched; latestTime = r.latest_time
usedSource = 'anonymous'
} catch (e) {
const msg = String(e.message || e)
logJSON('weibo.fetch.error', { uid, error: msg }, 'weibo')
const hint = '建议在服务器Chrome登录微博并开启9222端口,本工具将自动读取登录Cookie。'
return res.status(500).json({ ok: false, error: msg, hint })
}
}
logJSON('weibo.fetch.done', { uid, fetched, source: usedSource }, 'weibo')
res.json({ ok: true, fetched, latest_time: latestTime, source: usedSource })
} catch (e) {
const msg = String(e.message || e)
logJSON('weibo.fetch.error', { error: msg }, 'weibo')
const hint = 'Weibo API可能需要浏览器Cookie, 请在设置中填写Cookie后重试'
res.status(500).json({ ok: false, error: msg, hint })
}
})
app.get('/api/weibo/preview', async (req, res) => {
try {
const uid = String(req.query.uid || '7716940453')
const url = String(req.query.url || `https://m.weibo.cn/u/${uid}`)
const cookieDev = await getCookiesFromDevTools(['https://m.weibo.cn','https://weibo.com'])
const cookieSettings = (() => { try { const s = JSON.parse(fs.readFileSync(settingsPath,'utf-8')); const h=s.headers||{}; return h.Cookie||h.cookie||'' } catch { return '' } })()
const cookieAnon = await bootstrapCookies(uid)
const source = cookieDev ? 'devtools' : (cookieSettings ? 'settings' : (cookieAnon ? 'anonymous' : 'empty'))
const summarize = c => { const s=String(c||''); const pairs=s.split(';').map(v=>v.trim()).filter(Boolean); const names=pairs.map(p=>p.split('=')[0]).filter(Boolean); return { len:s.length, pairs:pairs.length, names:names.slice(0,12) } }
logJSON('weibo.preview.start', { uid, url, source, cookie: summarize(cookieDev||cookieSettings||cookieAnon) }, 'weibo')
const data = await screenshotUrl(url)
if (!data) {
logJSON('weibo.preview.error', { uid, url, source, error: 'screenshot_empty' }, 'weibo')
return res.json({ ok: false, url, source, screenshot: '', hint: '服务器Chrome未开启9222或未登录微博' })
}
res.json({ ok: true, url, source, screenshot: `data:image/png;base64,${data}` })
} catch (e) {
logJSON('weibo.preview.error', { error: String(e.message||e) }, 'weibo')
res.status(500).json({ ok: false, error: String(e.message||e) })
}
})
app.get('/api/weibo/search', (req, res) => {
const uid = String(req.query.uid || '7716940453')
const q = String(req.query.q || '').trim()
const mode = String(req.query.mode || 'any')
const limit = Math.max(1, Math.min(200, parseInt(req.query.limit || '50', 10)))
const terms = q.length > 0 ? q.split(/\s+/).filter(Boolean) : []
let rows = []
if (terms.length === 0) {
rows = queryPostsAny(uid, [''], limit)
} else if (mode === 'all') {
rows = queryPostsAll(uid, terms, limit)
} else {
rows = queryPostsAny(uid, terms, limit)
}
logJSON('weibo.search', { uid, q, mode, limit, rows: rows.length }, 'weibo')
res.json({ ok: true, rows })
})
app.get('/api/debug/logs', (req, res) => {
const limit = Math.max(1, Math.min(10000, parseInt(String(req.query.limit || '2000'), 10)))
const channel = String(req.query.channel || '').trim() || undefined
const lines = getLogs(limit, channel)
res.json({ ok: true, lines })
})
app.post('/api/debug/clear', (req, res) => {
const channel = String((req.query && req.query.channel) || '').trim() || undefined
clearLogs(channel)
res.json({ ok: true })
})
app.post('/api/debug/event', (req, res) => {
const body = req.body || {}
const channel = body && body.source ? String(body.source).trim() : undefined
logJSON('frontend.event', body, channel)
res.json({ ok: true })
})
app.post('/api/weibo/bootstrap', async (req, res) => {
try {
const uid = String(req.query.uid || '7716940453')
let cookie = ''
try { cookie = await getCookiesFromDevTools(['https://m.weibo.cn','https://weibo.com']) } catch {}
if (!cookie) cookie = await bootstrapCookies(uid)
const cfg = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
cfg.headers = cfg.headers || {}
cfg.headers.Cookie = cookie
fs.writeFileSync(settingsPath, JSON.stringify(cfg, null, 2))
logJSON('weibo.cookie.bootstrap', { uid, source: cookie ? 'devtools_or_anon' : 'empty', length: (cookie || '').length }, 'weibo')
res.json({ ok: true, cookie })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
const proxyMarketsAutomationFetch = async req => {
const cfg = getMarketsCfg()
const currentPort = Number(process.env.PORT || '8976') || 8976
const automationPort = Number(cfg.automation_port || 8977) || 8977
if (currentPort === automationPort) return null
const url = `http://127.0.0.1:${automationPort}/api/markets/fetch`
const proxied = await axios.get(url, {
params: req.query || {},
headers: {
Cookie: req.headers.cookie || ''
},
timeout: 45000,
validateStatus: () => true
})
return {
status: proxied.status || 200,
body: Object.assign({}, proxied.data || {}, {
proxied_to_automation: true,
current_port: currentPort,
automation_port: automationPort
})
}
}
app.get('/api/markets/fetch', async (req, res) => {
try {
const forwarded = await proxyMarketsAutomationFetch(req)
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
const payload = await fetchAll()
const quotes = Array.isArray(payload && payload.quotes) ? payload.quotes : []
// #region debug-point B:manual-fetch-route
;(() => { try { const p = path.join(process.cwd(), '.dbg', 'markets-yahoo-429.env'); let u = 'http://127.0.0.1:7777/event', s = 'markets-yahoo-429'; try { const e = fs.readFileSync(p, 'utf8'); u = (e.match(/DEBUG_SERVER_URL=(.+)/) || [])[1] || u; s = (e.match(/DEBUG_SESSION_ID=(.+)/) || [])[1] || s } catch {} const overseas = quotes.filter(item => String(item && item.card_type || '').toLowerCase() === 'overseas'); fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: s, runId: 'post-fix', hypothesisId: 'B', location: 'index.js:/api/markets/fetch', msg: '[DEBUG] markets manual fetch route result', data: { notify: String(req.query.notify || '') === '1', quotes_count: quotes.length, overseas_count: overseas.length, overseas_symbols: overseas.map(item => item.symbol) }, ts: Date.now() }) }).catch(() => {}) } catch {} })()
// #endregion
logJSON('markets.fetch', { count: quotes.length }, 'markets')
const notify = String(req.query.notify || '') === '1'
if (notify) { try { await maybeNotify(quotes) } catch (e) { logJSON('markets.fetch.notify.error', { error: String(e.message || e) }, 'markets') } }
const cfg = getMarketsCfg()
res.json({ ok: true, quotes, threshold_percent: cfg.threshold_percent, trace: payload && payload.trace ? payload.trace : null })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/markets/day', (req, res) => {
const date = String(req.query.date || '') || new Date().toISOString().slice(0, 10)
const rows = queryByDate.all(date)
res.json({ ok: true, date, rows })
})
app.get('/api/markets/config', (req, res) => {
res.json({ ok: true, config: getMarketsCfg() })
})
app.get('/api/markets/security_calendar', async (req, res) => {
try {
const month = String(req.query.month || '')
const forceRefresh = String(req.query.refresh || '') === '1'
const payload = await securityCalendar.getMonthCalendar(month, { force_refresh: forceRefresh })
res.json(payload)
} catch (e) {
res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) })
}
})
app.post('/api/markets/security_calendar/ai_verify', async (req, res) => {
try {
const body = req.body || {}
const forceRefresh = String(body.force_refresh || '') === '1' || body.force_refresh === true
const result = await securityCalendar.runAiVerification({ trigger: 'manual', force_refresh: forceRefresh })
res.json({ ok: !!result.ok, result })
} catch (e) {
res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) })
}
})
app.post('/api/markets/security_calendar/ipo_calendar_sync', async (req, res) => {
try {
const body = req.body || {}
let startDate = String(body.start_date || '').trim()
let endDate = String(body.end_date || '').trim()
if ((!startDate || !endDate) && body.month) {
const range = securityCalendar.getMonthRange(String(body.month || '').trim())
startDate = range.start_date
endDate = range.end_date
}
if (!startDate || !endDate) {
return res.status(400).json({ ok: false, error: 'missing month or start_date/end_date' })
}
await securityCalendar.ensureDateRangeData(startDate, endDate, { force_refresh: body.force_refresh === true || String(body.force_refresh || '') === '1' })
const result = await securityCalendarBridge.syncRange(startDate, endDate)
res.json({ ok: !!result.ok, result })
} catch (e) {
res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) })
}
})
app.get('/api/markets/weekly_report', async (req, res) => {
try {
const week = String(req.query.week || '')
const forceRefresh = String(req.query.refresh || '') === '1'
const currentPort = Number(process.env.PORT || '8976')
const currentWeek = marketsWeeklyReport.getWeekRange('').week_start
const targetWeek = marketsWeeklyReport.getWeekRange(week).week_start
const shouldPreferAutomation = currentPort !== 8977 && (forceRefresh || targetWeek === currentWeek)
if (shouldPreferAutomation) {
try {
const automationTarget = (process.env.AUTOMATION_TARGET || 'http://127.0.0.1:8977').replace(/\/$/, '')
const r = await axios.get(`${automationTarget}/api/markets/weekly_report`, {
params: req.query,
timeout: 180000,
validateStatus: () => true
})
if (r.status >= 200 && r.status < 300 && r.data && r.data.ok) {
return res.status(r.status).json(r.data)
}
} catch (proxyError) {
try {
logJSON('markets.weekly_report.proxy_8977.error', {
error: String(proxyError && proxyError.message ? proxyError.message : proxyError),
target_week: targetWeek
}, 'markets')
} catch {}
}
}
const payload = await marketsWeeklyReport.getWeeklyReport(week, { force_refresh: forceRefresh })
res.json(payload)
} catch (e) {
res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) })
}
})
// 建行定投评估:按需抓取四大指数 PE-TTM 与近10年分位(点击页面按钮触发,无定时任务)
app.get('/api/markets/dip_eval', async (req, res) => {
try {
const payload = await marketsDipEval.getDipEvalReport()
// 统一 200 + ok 标志:整体失败时 ok=false,前端交互弹窗读取 error 文案
res.json(payload || { ok: false, error: '评估失败' })
} catch (e) {
res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) })
}
})
app.post('/api/markets/config', (req, res) => {
const body = req.body || {}
const cfg = getMarketsCfg()
if (Array.isArray(body.watchlist)) cfg.watchlist = body.watchlist
if (typeof body.threshold_percent === 'number') cfg.threshold_percent = body.threshold_percent
if (body.email && typeof body.email === 'object') cfg.email = body.email
if (typeof body.ui_mode === 'string' && body.ui_mode.trim()) cfg.ui_mode = String(body.ui_mode).trim()
if (body.yahoo_probe && typeof body.yahoo_probe === 'object') cfg.yahoo_probe = body.yahoo_probe
setMarketsCfg(cfg)
try { insertSettingHistory.run({ tool: 'markets', payload: JSON.stringify(cfg) }) } catch {}
res.json({ ok: true })
})
app.post('/api/markets/test-email', async (req, res) => {
try {
await sendTestEmail()
logJSON('markets.email.test', { ok: true }, 'markets')
res.json({ ok: true })
} catch (e) {
logJSON('markets.email.test.error', { error: String(e.message || e) }, 'markets')
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/markets/test-email', async (req, res) => {
try {
await sendTestEmail()
logJSON('markets.email.test', { ok: true, method: 'GET' }, 'markets')
res.json({ ok: true })
} catch (e) {
logJSON('markets.email.test.error', { error: String(e.message || e), method: 'GET' }, 'markets')
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/markets/skills-meta', async (req, res) => {
try {
const skillsDir = path.join(process.cwd(), 'src', 'server', 'market_skills')
const logFile = path.join(process.cwd(), 'logs', 'market_skills.log')
const readDirConfigs = () => {
const list = []
if (!fs.existsSync(skillsDir)) return list
const dirs = fs.readdirSync(skillsDir, { withFileTypes: true })
for (const dirent of dirs) {
if (!dirent.isDirectory()) continue
const id = dirent.name
const configPath = path.join(skillsDir, id, 'config.json')
const indexPath = path.join(skillsDir, id, 'index.js')
if (!fs.existsSync(configPath) || !fs.existsSync(indexPath)) continue
try {
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
if (cfg.enabled === false) continue
list.push({ id, config: cfg, indexPath })
} catch {}
}
list.sort((a, b) => ((a.config.card_order ?? 9999) - (b.config.card_order ?? 9999)))
return list
}
const analyzeSkillCode = (id, indexPath) => {
const code = fs.readFileSync(indexPath, 'utf-8')
const isShared = /^module\.exports\s*=\s*require\(/.test(code)
const sharedMatch = code.match(/require\(['"]\.\.\/?([\w_-]+)\/index\.js['"]\)/)
const sharedFrom = sharedMatch ? sharedMatch[1] : null
let backupSource = ''
if (/fetchEastmoneyQuote|fetchPizzint|fetchFredVix|fetchSina|fetchYahoo/.test(code)) {
const candidates = []
if (/fetchTencentQuote|fetchTencent/.test(code)) candidates.push('腾讯')
if (/fetchEastmoneyQuote|push2his\.eastmoney/.test(code)) candidates.push('东方财富K线')
if (/fetchSina/.test(code)) candidates.push('新浪')
if (/fetchYahoo/.test(code)) candidates.push('雅虎')
if (/fetchPizzint/.test(code)) candidates.push('PizzINT')
if (/fetchFredVix/.test(code)) candidates.push('FRED VIX')
if (/fetchFredSeries/.test(code) && /SP500/.test(code)) candidates.push('FRED SP500')
if (/fetchFredSeries/.test(code) && /GDP/.test(code)) candidates.push('FRED GDP')
backupSource = candidates.join(', ')
}
return { isShared, sharedFrom, backupSource, hasBackup: backupSource.length > 0 }
}
const parseLogStatus = () => {
const state = new Map()
if (!fs.existsSync(logFile)) return state
try {
const raw = fs.readFileSync(logFile, 'utf-8')
const lines = raw.split(/\r?\n/).filter(Boolean)
const recent = lines.slice(Math.max(0, lines.length - 800))
for (const line of recent) {
try {
const m = line.match(/^([^Z]+Z)\s+(\S+)\s+(.+)$/)
if (!m) continue
const evt = m[2]
const payload = JSON.parse(m[3])
const skillId = payload.skill
if (!skillId) continue
if (!state.has(skillId)) {
state.set(skillId, { lastSavedAt: null, lastErrorAt: null, lastErrorMsg: '', errors: 0 })
}
const s = state.get(skillId)
if (evt === 'skills.data.saved') {
s.lastSavedAt = m[1]
}
if (evt === 'skill.fetch.error') {
s.lastErrorAt = m[1]
s.lastErrorMsg = payload.message || ''
s.errors++
}
if (evt === 'skills.run.error') {
s.lastErrorAt = m[1]
s.lastErrorMsg = payload.error || ''
s.errors++
}
} catch {}
}
} catch {}
return state
}
const doHeartbeat = async (label, url, opts = {}) => {
try {
const res = await axios.get(url, { timeout: 8000, validateStatus: () => true, ...opts })
if (res.status >= 500) return { label, ok: false, status: res.status, latency: '—' }
return { label, ok: true, status: res.status, latency: 'OK' }
} catch (e) {
return { label, ok: false, status: e.code || 'ERR', latency: '—' }
}
}
const items = readDirConfigs()
const logState = parseLogStatus()
const skills = items.map(({ id, config, indexPath }) => {
const codeInfo = analyzeSkillCode(id, indexPath)
const log = logState.get(id) || { lastSavedAt: null, lastErrorAt: null, lastErrorMsg: '', errors: 0 }
const sourceLabels = { tencent: '腾讯财经', eastmoney: '东方财富', sina: '新浪', yahoo: '雅虎', fred: 'FRED', fred_vix: 'FRED VIX' }
const mainSource = sourceLabels[config.source] || config.source || '—'
let health = '✅'
let healthNote = ''
if (log.errors > 0) {
health = log.errors >= 5 ? '❌' : '⚠️'
healthNote = `${log.errors} 次错误:${log.lastErrorMsg}`
}
if (!log.lastSavedAt) {
health = '❌'
healthNote = '从未收到数据'
}
return {
id,
name: config.name,
symbol: config.symbol,
card_order: config.card_order ?? 9999,
card_type: config.card_type || '',
schedule: config.schedule || '',
main_source: mainSource,
source_raw: config.source || '',
backup_source: codeInfo.backupSource || '—',
is_shared: codeInfo.isShared,
shared_from: codeInfo.sharedFrom || null,
has_backup: codeInfo.hasBackup,
health,
health_note: healthNote,
last_data_at: log.lastSavedAt || '—',
last_error_at: log.lastErrorAt || '—',
errors: log.errors
}
})
const heartbeats = await Promise.all([
doHeartbeat('腾讯 qt.gtimg.cn', 'http://qt.gtimg.cn/q=sh000001', { responseType: 'arraybuffer' }),
doHeartbeat('东方财富 push2his', 'https://push2his.eastmoney.com/api/qt/stock/kline/get?secid=1.000001&lmt=1', { headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://quote.eastmoney.com/' } }),
doHeartbeat('FRED', 'https://fred.stlouisfed.org/graph/fredgraph.csv?id=SP500', { responseType: 'text', transformResponse: [d => d] }),
doHeartbeat('新浪 hq.sinajs.cn', 'http://hq.sinajs.cn/list=sh000001', { headers: { 'Referer': 'https://finance.sina.com.cn/', 'User-Agent': 'Mozilla/5.0' }, responseType: 'arraybuffer' })
])
const anomalies = []
for (const hb of heartbeats) {
if (!hb.ok) anomalies.push({ source: hb.label, detail: `状态码 ${hb.status},不可达`, level: 'critical' })
}
const eastmoneySkills = skills.filter(s => s.source_raw === 'eastmoney')
if (eastmoneySkills.length > 0) {
anomalies.push({
source: '东方财富残留',
detail: `仍有 ${eastmoneySkills.length} 个卡使用 eastmoney 源(${eastmoneySkills.map(s => s.name).join(', ')}),可能存在拉取失败`,
level: 'warning'
})
}
const errorSkills = skills.filter(s => s.errors > 0)
for (const s of errorSkills) {
anomalies.push({
source: s.name,
detail: s.health_note,
level: s.errors >= 5 ? 'critical' : 'warning'
})
}
const neverDataSkills = skills.filter(s => s.last_data_at === '—')
for (const s of neverDataSkills) {
if (!anomalies.some(a => a.source === s.name)) {
anomalies.push({
source: s.name,
detail: `从未收到数据,数据源 ${s.main_source} 可能异常`,
level: 'critical'
})
}
}
res.json({
ok: true,
skills,
heartbeats,
anomalies,
total_skills: skills.length,
healthy_count: skills.filter(s => s.health === '✅').length,
generated_at: new Date().toISOString()
})
} catch (e) {
logJSON('markets.skills-meta.error', { error: String(e.message || e) }, 'markets')
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/markets/email-log', async (req, res) => {
try {
const limit = Math.max(1, Math.min(500, parseInt(String(req.query.limit || '100'), 10) || 100))
const { db } = require('./db')
const rows = db.prepare(`SELECT * FROM alert_email_log ORDER BY id DESC LIMIT ?`).all(limit)
res.json({ ok: true, rows, total: rows.length })
} catch (e) {
logJSON('markets.email-log.error', { error: String(e.message || e) }, 'markets')
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// 市场工具 TTS 语音合成(阿里云)
const marketTts = require('./market_tts')
app.get('/api/markets/tts', async (req, res) => {
try {
const text = String(req.query.text || '').trim()
if (!text) return res.status(400).json({ ok: false, error: 'Missing text' })
if (text.length > 500) return res.status(400).json({ ok: false, error: 'Text too long (max 500)' })
const voice = String(req.query.voice || 'Zhixuan').trim()
const audioBuf = await marketTts.synthesize(text, { voice })
const ct = audioBuf[0] === 0x52 ? 'audio/wav' : 'audio/mpeg'
res.set({ 'Content-Type': ct, 'Content-Length': audioBuf.length })
res.send(audioBuf)
} catch (e) {
logJSON('markets.tts.error', { error: String(e.message || e) }, 'markets')
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// 整章 TTS 生成(POST JSON,返回所有日志)
app.post('/api/markets/tts-generate', express.json(), async (req, res) => {
const { text, voice, ch, meta } = req.body || {}
if (!text || !text.trim()) return res.status(400).json({ ok: false, error: 'Missing text' })
const logs = []
const log = (msg, type = 'info') => {
logs.push({ msg, type })
console.log('[MarketTTS]', msg)
}
try {
log(`收到文本:${text.length} 字${meta ? '(' + meta + ')' : ''}`)
const result = await marketTts.generateChapter(text.trim(), voice || 'Zhixuan', (msg, type) => {
log(msg, type)
})
const chIdx = parseInt(String(ch || ''), 10)
const fileName = !isNaN(chIdx) && chIdx >= 0
? `ch_${String(chIdx).padStart(2, '0')}.wav`
: `ch_${Date.now()}.wav`
const urlPath = marketTts.saveAudioFile(result.buffer, fileName)
log(`语音文件已保存:${fileName}`, 'success')
res.json({ ok: true, logs, result: { url: urlPath, duration: result.duration, fileName } })
} catch (e) {
log(`生成失败:${e.message}`, 'error')
res.json({ ok: false, logs, error: e.message })
}
})
// 检查某章节是否已生成语音(支持 .mp3 和 .wav)
app.get('/api/markets/tts-check', async (req, res) => {
try {
const chIdx = parseInt(String(req.query.ch || ''), 10)
if (isNaN(chIdx) || chIdx < 0) return res.status(400).json({ ok: false, error: 'Invalid ch' })
const base = `ch_${String(chIdx).padStart(2, '0')}`
const basePath = path.join(process.cwd(), 'public', 'tools', 'markets', 'audio')
// 优先 .mp3,其次 .wav
let ext = 'mp3'
let filePath = path.join(basePath, base + '.mp3')
if (!fs.existsSync(filePath)) {
ext = 'wav'
filePath = path.join(basePath, base + '.wav')
}
const exists = fs.existsSync(filePath)
let duration = 0
if (exists) {
const stat = fs.statSync(filePath)
// .wav 可以算时长(44字节头,16-bit mono 16kHz)
if (ext === 'wav') {
const dataSize = stat.size - 44
duration = dataSize > 0 ? parseFloat((dataSize / 16000 / 2).toFixed(1)) : 0
}
// .mp3 让浏览器自己报时长,这里填 0
}
res.json({ ok: true, exists, url: exists ? '/tools/markets/audio/' + base + '.' + ext : null, duration })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/markets/weekly_report_health', async (req, res) => {
try {
const cfgPath = path.join(process.cwd(), 'config', 'markets_weekly_report.json')
if (!fs.existsSync(cfgPath)) return res.json({ ok: true, items: [], heartbeats: [], diagnosis: [] })
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
const items = Array.isArray(cfg.items) ? cfg.items : []
const sourceLabels = {
tencent: '腾讯K线',
eastmoney: '东财K线',
eastmoney_browser_realtime: '东财实时',
nasdaq_index: 'Nasdaq API',
sina_global_futures: '新浪期货',
globalstockdata: 'GlobalStockData',
yahoo_probe: 'Yahoo 探针'
}
const reliabilityLabel = (item) => {
const sources = Array.isArray(item.history_sources) ? item.history_sources : []
const hasEastmoney = sources.includes('eastmoney')
const hasBrowser = sources.includes('eastmoney_browser_realtime')
const hasTencent = sources.includes('tencent')
const hasNasdaq = sources.includes('nasdaq_index')
const hasSina = sources.includes('sina_global_futures')
const hasGsd = sources.includes('globalstockdata')
const hasYahoo = sources.includes('yahoo_probe')
if ((hasTencent && !hasEastmoney) || hasNasdaq || hasSina || hasYahoo) {
if (sources.every(s => s !== 'globalstockdata' && s !== 'eastmoney')) return { level: '✅ 可靠', color: '#16a34a' }
}
if (hasEastmoney && sources.length <= 2) return { level: '❌ 脆弱', color: '#dc2626' }
if (hasGsd || (hasEastmoney && sources.length > 2)) return { level: '⚠️ 有风险', color: '#ea580c' }
return { level: '⚠️ 未知', color: '#8c8c8c' }
}
const doHeartbeat = async (label, url, opts = {}) => {
try {
const res = await axios.get(url, { timeout: 8000, validateStatus: () => true, ...opts })
if (res.status >= 500) return { label, ok: false, status: res.status }
return { label, ok: true, status: res.status }
} catch (e) {
return { label, ok: false, status: e.code || 'ERR' }
}
}
const heartbeats = await Promise.all([
doHeartbeat('腾讯K线 web.ifzq', 'http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=sh000001,day,,,5,qfq', { timeout: 8000 }),
doHeartbeat('东财K线 push2his', 'https://push2his.eastmoney.com/api/qt/stock/kline/get?secid=1.000001&lmt=1', { headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://quote.eastmoney.com/' } }),
doHeartbeat('东财实时 push2', 'https://push2.eastmoney.com/api/qt/ulist/get?fltt=1&invt=2&cb=x&fields=f2&secids=1.000001&ut=fa5fd1943c7b386f172d6893dbfba10b&pn=1&np=1&pz=1&dect=1&_=1', { timeout: 8000 }),
doHeartbeat('Nasdaq API', 'https://api.nasdaq.com/api/quote/COMP/historical?assetclass=index&limit=1', { headers: { 'User-Agent': 'Mozilla/5.0', 'Origin': 'https://www.nasdaq.com', 'Referer': 'https://www.nasdaq.com/' } }),
doHeartbeat('新浪期货', 'https://stock2.finance.sina.com.cn/futures/api/jsonp.php/var%20x=/GlobalFuturesService.getGlobalFuturesDailyKLine?symbol=GC', { headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://finance.sina.com.cn/' } })
])
const itemsOut = items.map(item => {
const rel = reliabilityLabel(item)
const sources = Array.isArray(item.history_sources) ? item.history_sources : []
return {
symbol: item.symbol,
label: item.label,
group: item.market_group || '',
main_source: sourceLabels[sources[0]] || sources[0] || '—',
backup_source: sources.slice(1).map(s => sourceLabels[s] || s).join(' + ') || '—',
reliability: rel.level,
reliability_color: rel.color,
source_count: sources.length,
sources_raw: sources
}
})
const eastmoneyCards = itemsOut.filter(it => it.sources_raw.includes('eastmoney'))
const diagnosis = []
if (eastmoneyCards.length > 0) {
diagnosis.push({
level: 'critical',
source: '东财K线 push2his 不可用',
detail: `影响 ${eastmoneyCards.length} 张卡:${eastmoneyCards.map(it => it.label).join('、')}。历史周数据无法通过东财K线获取,已自动降级为东财实时或腾讯源。`
})
}
const tencentOnlyCards = itemsOut.filter(it =>
it.sources_raw.includes('tencent') && !it.sources_raw.includes('eastmoney') &&
!it.sources_raw.includes('nasdaq_index') && !it.sources_raw.includes('sina_global_futures') &&
!it.sources_raw.includes('globalstockdata')
)
if (tencentOnlyCards.some(it => ['usINX', 'usDJI', 'usIXIC', 'usFLSA', 'usNFTY'].includes(it.symbol))) {
diagnosis.push({
level: 'warning',
source: '腾讯外盘K线仅1天',
detail: `标普500、道琼斯、沙特、印度等美股权重卡,腾讯K线仅返回最近1个交易日,历史周靠备源globalstockdata补全。`
})
}
const gsdCards = itemsOut.filter(it => it.sources_raw.includes('globalstockdata'))
if (gsdCards.length > 0) {
diagnosis.push({
level: 'warning',
source: 'GlobalStockData HTML抓取',
detail: `日经225、德国DAX30 等 ${gsdCards.length} 张卡依赖 GlobalStockData HTML 页面抓取,该源不稳定,随时可能因页面结构变更失效。`
})
}
for (const hb of heartbeats) {
if (!hb.ok) {
diagnosis.push({
level: 'critical',
source: hb.label,
detail: `状态码 ${hb.status},不可达`
})
}
}
res.json({
ok: true,
items: itemsOut,
heartbeats,
diagnosis,
total: itemsOut.length,
eastmoney_count: eastmoneyCards.length
})
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/wall/backgrounds', (req, res) => {
try {
const dir = path.join(process.cwd(), 'public', 'tools', 'wall')
const files = fs.readdirSync(dir)
const images = files.filter(f => /^bjt_.*\.(?:jpg|jpeg|png|webp)$/i.test(f)).map(f => `/tools/wall/${f}`)
res.json({ ok: true, images })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/wall/config', (req, res) => {
try {
const cfg = getWallCfg()
res.json({ ok: true, config: cfg })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/wall/cards', (req, res) => {
try {
res.set('Cache-Control', 'no-store')
const rows = listWallCards()
res.json({ ok: true, rows })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Funds Guoxin tool
app.get('/api/funds_guoxin/config', (req, res) => {
try { res.json({ ok: true, config: fundsGuoxin.getConfig() }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message||e) }) }
})
app.post('/api/funds_guoxin/config', (req, res) => {
try { const cfg = fundsGuoxin.getConfig(); const body = req.body || {}; const next = Object.assign({}, cfg, body); fundsGuoxin.setConfig(next); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/funds_guoxin/flows', (req, res) => {
try { res.set('Cache-Control', 'no-store'); const start = String(req.query.start||''); const end = String(req.query.end||''); const type = String(req.query.type||'all'); const kw = String(req.query.kw||''); const rows = fundsGuoxin.listFlows(start, end, type, kw); res.json({ ok: true, rows }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.post('/api/funds_guoxin/flows', (req, res) => {
try { fundsGuoxin.createFlow(req.body||{}); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.put('/api/funds_guoxin/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id,10); fundsGuoxin.updateFlowById(id, req.body||{}); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.delete('/api/funds_guoxin/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id,10); fundsGuoxin.deleteFlow(id); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/funds_guoxin/export.csv', (req, res) => {
try { const start = String(req.query.start||''); const end = String(req.query.end||''); const rows = fundsGuoxin.listFlows(start, end, 'all', ''); const header = '日期,类型,金额(人民币),银行方/状态,期末金额,备注\n'; const content = rows.map(r => `${r.date},${r.type==='in'?'汇入':'汇出'},${(r.amount_cents/100).toFixed(2)},${(r.bank_status||'').replace(/,/g,' ')},${(r.ending_cents/100).toFixed(2)},${(r.remark||'').replace(/,/g,' ')}`).join('\n'); res.setHeader('Content-Type','text/csv; charset=utf-8'); res.setHeader('Content-Disposition','attachment; filename="funds_guoxin.csv"'); res.send(header+content) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
// Funds Huatai tool
app.get('/api/funds_huatai/config', (req, res) => {
try { res.json({ ok: true, config: fundsHuatai.getConfig() }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message||e) }) }
})
app.post('/api/funds_huatai/config', (req, res) => {
try { const cfg = fundsHuatai.getConfig(); const body = req.body || {}; const next = Object.assign({}, cfg, body); fundsHuatai.setConfig(next); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/funds_huatai/flows', (req, res) => {
try { res.set('Cache-Control', 'no-store'); const start = String(req.query.start||''); const end = String(req.query.end||''); const type = String(req.query.type||'all'); const kw = String(req.query.kw||''); const rows = fundsHuatai.listFlows(start, end, type, kw); res.json({ ok: true, rows }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.post('/api/funds_huatai/flows', (req, res) => {
try { fundsHuatai.createFlow(req.body||{}); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.put('/api/funds_huatai/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id,10); fundsHuatai.updateFlowById(id, req.body||{}); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.delete('/api/funds_huatai/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id,10); fundsHuatai.deleteFlow(id); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/funds_huatai/export.csv', (req, res) => {
try { const start = String(req.query.start||''); const end = String(req.query.end||''); const rows = fundsHuatai.listFlows(start, end, 'all', ''); const header = '日期,类型,金额(人民币),银行方/状态,期末金额,备注\n'; const content = rows.map(r => `${r.date},${r.type==='in'?'汇入':'汇出'},${(r.amount_cents/100).toFixed(2)},${(r.bank_status||'').replace(/,/g,' ')},${(r.ending_cents/100).toFixed(2)},${(r.remark||'').replace(/,/g,' ')}`).join('\n'); res.setHeader('Content-Type','text/csv; charset=utf-8'); res.setHeader('Content-Disposition','attachment; filename="funds_huatai.csv"'); res.send(header+content) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/ccb_private_funds/config', (req, res) => {
try { res.json({ ok: true, config: ccbPrivateFunds.getConfig() }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/ccb_private_funds/config', (req, res) => {
try { const cfg = ccbPrivateFunds.getConfig(); const body = req.body || {}; const next = Object.assign({}, cfg, body); ccbPrivateFunds.setConfig(next); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/ccb_private_funds/flows', (req, res) => {
try { res.set('Cache-Control', 'no-store'); const start = String(req.query.start || ''); const end = String(req.query.end || ''); const type = String(req.query.type || 'all'); const kw = String(req.query.kw || ''); const rows = ccbPrivateFunds.listFlows(start, end, type, kw); res.json({ ok: true, rows }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/ccb_private_funds/flows', (req, res) => {
try { const id = ccbPrivateFunds.createFlow(req.body || {}); res.json({ ok: true, id }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.put('/api/ccb_private_funds/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id, 10); ccbPrivateFunds.updateFlowById(id, req.body || {}); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/ccb_private_funds/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id, 10); ccbPrivateFunds.deleteFlow(id); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/ccb_private_funds/export.csv', (req, res) => {
try { const start = String(req.query.start || ''); const end = String(req.query.end || ''); const rows = ccbPrivateFunds.listFlows(start, end, 'all', ''); const header = '日期,类型,金额(人民币),银行方/状态,期末金额,备注\n'; const content = rows.map(r => `${r.date},${r.type === 'in' ? '汇入' : '汇出'},${(r.amount_cents / 100).toFixed(2)},${(r.bank_status || '').replace(/,/g, ' ')},${(r.ending_cents / 100).toFixed(2)},${(r.remark || '').replace(/,/g, ' ')}`).join('\n'); res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', 'attachment; filename="ccb_private_funds.csv"'); res.send(header + content) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/ccb_private_funds/attachments/:flow_id', (req, res) => {
try {
const flowId = parseInt(req.params.flow_id, 10)
if (!flowId) return res.status(400).json({ ok: false, error: 'bad_flow_id' })
const rows = ccbPrivateFunds.listAttachmentsByFlowId(flowId)
res.json({ ok: true, rows })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/ccb_private_funds/attachments/upload', express.raw({ type: 'application/octet-stream', limit: '200mb' }), (req, res) => {
try {
const flow_id = parseInt(req.query.flow_id || '0', 10)
if (!flow_id) return res.status(400).json({ ok: false, error: 'bad_flow_id' })
const raw = req.body || Buffer.alloc(0)
if (!raw.length) return res.status(400).json({ ok: false, error: 'empty_file' })
const filename = String(req.query.filename || 'file.bin').replace(/[<>:"/\\|?*]/g, '_')
const mime = String(req.query.mime || 'application/octet-stream')
const ext = path.extname(filename || '').slice(0, 12)
const outName = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext || '.bin'}`
const rel = path.join('ccb_private_funds', 'attachments', outName).replace(/\\/g, '/')
const abs = path.join(process.cwd(), 'uploads', rel)
if (!fs.existsSync(path.dirname(abs))) fs.mkdirSync(path.dirname(abs), { recursive: true })
fs.writeFileSync(abs, raw)
const id = ccbPrivateFunds.addAttachment({ flow_id, filename, path: rel, size_bytes: raw.length, mime })
res.json({ ok: true, id, url: `/uploads/${rel}` })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/ccb_private_funds/attachments/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10)
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
ccbPrivateFunds.deleteAttachmentById(id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Nav Bookmarks API
app.get('/api/nav_bookmarks/categories', (req, res) => {
try {
const rows = NAV_BOOKMARKS_DB.prepare('SELECT code, name, sort_order, created_at, updated_at FROM nav_categories ORDER BY sort_order ASC, code ASC').all()
res.json({ ok: true, rows })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/nav_bookmarks/categories', (req, res) => {
try {
const body = req.body || {}
const code = String(body.code || '').trim()
const name = String(body.name || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
if (!code || !name) return res.status(400).json({ ok: false, error: 'missing_code_or_name' })
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare('INSERT INTO nav_categories (code, name, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?)')
stmt.run(code, name, sortOrder, now, now)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.put('/api/nav_bookmarks/categories/:code', (req, res) => {
try {
const code = String(req.params.code || '').trim()
if (!code) return res.status(400).json({ ok: false, error: 'missing_code' })
const body = req.body || {}
const name = String(body.name || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare('UPDATE nav_categories SET name = ?, sort_order = ?, updated_at = ? WHERE code = ?')
stmt.run(name, sortOrder, now, code)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/nav_bookmarks/categories/:code', (req, res) => {
try {
const code = String(req.params.code || '').trim()
if (!code) return res.status(400).json({ ok: false, error: 'missing_code' })
const inUse = NAV_BOOKMARKS_DB.prepare('SELECT COUNT(1) AS c FROM nav_bookmarks WHERE category_code = ?').get(code)
if (inUse && inUse.c > 0) return res.status(400).json({ ok: false, error: 'category_in_use' })
NAV_BOOKMARKS_DB.prepare('DELETE FROM nav_categories WHERE code = ?').run(code)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/nav_bookmarks/bookmarks', (req, res) => {
try {
const category = String(req.query.category_code || '').trim()
const q = String(req.query.q || '').trim().toLowerCase()
let rows = NAV_BOOKMARKS_DB.prepare('SELECT id, name, url, icon, category_code, remark, sort_order, created_at, updated_at FROM nav_bookmarks ORDER BY sort_order ASC, created_at ASC').all()
if (category) rows = rows.filter(r => String(r.category_code || '') === category)
if (q) {
rows = rows.filter(r => {
const name = String(r.name || '').toLowerCase()
const url = String(r.url || '').toLowerCase()
const remark = String(r.remark || '').toLowerCase()
return name.includes(q) || url.includes(q) || remark.includes(q)
})
}
res.json({ ok: true, rows })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/nav_bookmarks/bookmarks', (req, res) => {
try {
const body = req.body || {}
const name = String(body.name || '').trim()
const url = String(body.url || '').trim()
if (!name || !url) return res.status(400).json({ ok: false, error: 'missing_name_or_url' })
const icon = String(body.icon || '').trim()
const categoryCode = String(body.category_code || '').trim()
const remark = String(body.remark || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
const id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare(`INSERT INTO nav_bookmarks (id, name, url, icon, category_code, remark, sort_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
stmt.run(id, name, url, icon, categoryCode, remark, sortOrder, now, now)
res.json({ ok: true, id })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.put('/api/nav_bookmarks/bookmarks/:id', (req, res) => {
try {
const id = String(req.params.id || '').trim()
if (!id) return res.status(400).json({ ok: false, error: 'missing_id' })
const body = req.body || {}
const name = String(body.name || '').trim()
const url = String(body.url || '').trim()
const icon = String(body.icon || '').trim()
const categoryCode = String(body.category_code || '').trim()
const remark = String(body.remark || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare(`UPDATE nav_bookmarks SET name = ?, url = ?, icon = ?, category_code = ?, remark = ?, sort_order = ?, updated_at = ? WHERE id = ?`)
stmt.run(name, url, icon, categoryCode, remark, sortOrder, now, id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/nav_bookmarks/bookmarks/:id', (req, res) => {
try {
const id = String(req.params.id || '').trim()
if (!id) return res.status(400).json({ ok: false, error: 'missing_id' })
NAV_BOOKMARKS_DB.prepare('DELETE FROM nav_bookmarks WHERE id = ?').run(id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/nav_bookmarks/state', (req, res) => {
try {
const categories = NAV_BOOKMARKS_DB.prepare('SELECT code, name, sort_order, created_at, updated_at FROM nav_categories ORDER BY sort_order ASC, code ASC').all()
const bookmarks = NAV_BOOKMARKS_DB.prepare('SELECT id, name, url, icon, category_code, remark, sort_order, created_at, updated_at FROM nav_bookmarks ORDER BY sort_order ASC, created_at ASC').all()
res.json({ ok: true, categories, bookmarks })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Cloud tool
app.get('/api/cloud/operators', (req, res) => { try { res.json({ ok: true, rows: cloud.listOperatorsFn() }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/operators', (req, res) => { try { cloud.createOperator(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/operators/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateOperator(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/operators/:id', (req, res) => { try { cloud.deleteOperator(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.get('/api/cloud/ecs', (req, res) => { try { const op = req.query.operator_id ? parseInt(req.query.operator_id,10) : null; res.json({ ok:true, rows: cloud.listEcsFn(op) }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/ecs', (req, res) => { try { cloud.createEcs(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/ecs/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateEcs(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/ecs/:id', (req, res) => { try { cloud.deleteEcs(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.get('/api/cloud/vas', (req, res) => { try { const op = req.query.operator_id ? parseInt(req.query.operator_id,10) : null; res.json({ ok:true, rows: cloud.listVasFn(op) }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/vas', (req, res) => { try { cloud.createVas(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/vas/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateVas(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/vas/:id', (req, res) => { try { cloud.deleteVas(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.get('/api/cloud/renew', (req, res) => { try { const st = String(req.query.subject_type||''); const sid = req.query.subject_id ? parseInt(req.query.subject_id,10) : null; res.json({ ok:true, rows: cloud.listRenewFn(st || null, sid) }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/renew', (req, res) => { try { cloud.createRenew(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/renew/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateRenew(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/renew/:id', (req, res) => { try { cloud.deleteRenew(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/renew/invoice', express.raw({ type: 'application/octet-stream', limit: '50mb' }), (req, res) => {
try {
const raw = req.body || Buffer.alloc(0)
const name = String(req.query.filename || 'invoice.bin').replace(/[^a-zA-Z0-9._-]/g, '_')
const outName = `${Date.now()}-${name}`
const full = path.join(invoicesDir, outName)
fs.writeFileSync(full, raw)
res.json({ ok: true, path: `/uploads/cloud/invoices/${outName}` })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Learning Notes API
const notesDir = path.join(process.cwd(), 'uploads', 'cloud', 'notes')
if (!fs.existsSync(notesDir)) fs.mkdirSync(notesDir, { recursive: true })
app.get('/api/cloud/notes', (req, res) => {
const st = String(req.query.subject_type || '')
const sid = parseInt(req.query.subject_id || '0', 10)
const rows = cloud.listNotesFn(st, sid)
res.json({ ok: true, rows })
})
app.post('/api/cloud/notes', (req, res) => {
try {
const body = req.body || {}
const id = cloud.createNote(body)
res.json({ ok: true, id })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.put('/api/cloud/notes/:id', (req, res) => {
try {
const body = Object.assign({}, req.body || {}, { id: parseInt(req.params.id, 10) })
cloud.updateNote(body)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/cloud/notes/:id', (req, res) => {
try {
cloud.deleteNote(parseInt(req.params.id, 10))
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud/notes/attachment', express.raw({ type: 'application/octet-stream', limit: '200mb' }), (req, res) => {
try {
const note_id = parseInt(req.query.note_id || '0', 10)
if (!note_id) return res.status(400).json({ ok: false, error: 'missing_note_id' })
const raw = req.body || Buffer.alloc(0)
const name = String(req.query.filename || 'file.bin').replace(/[<>:"/\\|?*]/g, '_')
// Chunked upload support
const chunkIdx = parseInt(req.query.chunk_idx || '-1', 10)
const totalChunks = parseInt(req.query.total_chunks || '1', 10)
const uploadId = String(req.query.upload_id || Date.now())
const outName = `${uploadId}-${name}`
const full = path.join(notesDir, outName)
if (chunkIdx >= 0) {
if (chunkIdx === 0) {
fs.writeFileSync(full, raw)
} else {
fs.appendFileSync(full, raw)
}
if (chunkIdx === totalChunks - 1) {
const stats = fs.statSync(full)
const attId = cloud.createAttachment({ note_id, filename: name, file_path: `/uploads/cloud/notes/${outName}`, file_size: stats.size })
return res.json({ ok: true, id: attId, path: `/uploads/cloud/notes/${outName}` })
} else {
return res.json({ ok: true, chunk: chunkIdx })
}
} else {
fs.writeFileSync(full, raw)
const attId = cloud.createAttachment({ note_id, filename: name, file_path: `/uploads/cloud/notes/${outName}`, file_size: raw.length })
res.json({ ok: true, id: attId, path: `/uploads/cloud/notes/${outName}` })
}
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/cloud/notes/attachment/:id', (req, res) => {
try {
cloud.deleteAttachment(parseInt(req.params.id, 10))
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud/balance_alerts', (req, res) => {
try {
const page = parseInt(String(req.query.page || '1'), 10) || 1
const pageSize = parseInt(String(req.query.page_size || '20'), 10) || 20
const keyword = String(req.query.keyword || '').trim()
const result = cloudBalanceWatch.listAlerts({ page, pageSize, keyword })
res.json(Object.assign({ ok: true }, result))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud/balance_alerts/status', (req, res) => {
;(async () => {
try {
const forwarded = await proxyCloudBalanceAutomationStatus('/api/cloud/balance_alerts/status')
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
res.json({ ok: true, data: cloudBalanceWatch.getStatus() })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})()
})
app.get('/api/cloud/balance_alerts/:id', (req, res) => {
try {
const row = cloudBalanceWatch.getAlert(parseInt(String(req.params.id || '0'), 10) || 0)
if (!row) return res.status(404).json({ ok: false, error: 'not_found' })
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const proxyCloudBalanceAutomationRequest = async (req, routePath) => {
const cfg = cloudBalanceWatch.readConfig()
const currentPort = Number(process.env.PORT || '8976') || 8976
const automationPort = Number(cfg.automation_port || 8977) || 8977
if (currentPort === automationPort) return null
const url = `http://127.0.0.1:${automationPort}${routePath}`
const proxied = await axios({
url,
method: req.method || 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: req.headers.cookie || ''
},
data: req.body || {},
timeout: 60000,
validateStatus: () => true
})
return {
status: proxied.status || 200,
body: Object.assign({}, proxied.data || {}, {
proxied_to_automation: true,
current_port: currentPort,
automation_port: automationPort
})
}
}
const proxyCloudBalanceAutomationStatus = async routePath => {
const cfg = cloudBalanceWatch.readConfig()
const currentPort = Number(process.env.PORT || '8976') || 8976
const automationPort = Number(cfg.automation_port || 8977) || 8977
if (currentPort === automationPort) return null
const url = `http://127.0.0.1:${automationPort}${routePath}`
const proxied = await axios.get(url, {
timeout: 15000,
validateStatus: () => true
})
return {
status: proxied.status || 200,
body: Object.assign({}, proxied.data || {}, {
proxied_to_automation: true,
current_port: currentPort,
automation_port: automationPort
})
}
}
app.post('/api/cloud/balance_alerts/scan', async (req, res) => {
try {
const forwarded = await proxyCloudBalanceAutomationRequest(req, '/api/cloud/balance_alerts/scan')
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
const result = cloudBalanceWatch.requestScan('manual')
res.json(Object.assign({ ok: true }, result || {}))
} catch (e) {
res.status(500).json({
ok: false,
error: String(e && e.message ? e.message : e),
detail: String(e && e.stack ? e.stack : (e && e.message ? e.message : e)),
route: '/api/cloud/balance_alerts/scan'
})
}
})
app.post('/api/cloud/balance_alerts/:id/retry_calendar', async (req, res) => {
try {
const forwarded = await proxyCloudBalanceAutomationRequest(req, `/api/cloud/balance_alerts/${encodeURIComponent(String(req.params.id || '0'))}/retry_calendar`)
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
const result = await cloudBalanceWatch.retryCalendarById(parseInt(String(req.params.id || '0'), 10) || 0)
res.json(Object.assign({ ok: true }, result || {}))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud/balance_alerts/:id/retry_weekly', async (req, res) => {
try {
const forwarded = await proxyCloudBalanceAutomationRequest(req, `/api/cloud/balance_alerts/${encodeURIComponent(String(req.params.id || '0'))}/retry_weekly`)
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
const result = await cloudBalanceWatch.retryWeeklyById(parseInt(String(req.params.id || '0'), 10) || 0)
res.json(Object.assign({ ok: true }, result || {}))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/wall/config', (req, res) => {
try {
const body = req.body || {}
const cfg = getWallCfg()
if (Array.isArray(body.cards)) cfg.cards = body.cards
if (typeof body.autoplay_ms === 'number') cfg.autoplay_ms = body.autoplay_ms
if (typeof body.swipe_pause_ms === 'number') cfg.swipe_pause_ms = body.swipe_pause_ms
if (typeof body.max_cards_on_screen === 'number') cfg.max_cards_on_screen = body.max_cards_on_screen
if (body.background && typeof body.background === 'object') cfg.background = body.background
setWallCfg(cfg)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/wall/cards', (req, res) => {
try {
const body = req.body || {}
createWallCard(body)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.put('/api/wall/cards/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10)
const body = req.body || {}
updateWallCardById(id, body)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/wall/cards/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10)
deleteWallCardById(id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/markets/archive/get', (req, res) => {
const day = String(req.query.day || '') || new Date().toISOString().slice(0,10)
const row = getMarketArchive.get(day)
res.json({ ok: true, day, html: row ? row.html : '' })
})
app.post('/api/markets/archive/save', (req, res) => {
const body = req.body || {}
const day = String(body.day || '') || new Date().toISOString().slice(0,10)
const html = String(body.html || '')
upsertMarketArchive.run({ day, html })
logJSON('markets.archive.save', { day, html_len: html.length }, 'markets')
res.json({ ok: true })
})
const normalizeDate = s => {
const d = new Date(s || Date.now())
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
const getWeekRangeFromDate = s => {
const d = new Date(s || Date.now())
const dow = d.getDay()
const shiftToMonday = dow === 0 ? -6 : 1 - dow
const start = new Date(d.getFullYear(), d.getMonth(), d.getDate() + shiftToMonday)
const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 6)
return { start: normalizeDate(start), end: normalizeDate(end) }
}
app.post('/api/archives/save', (req, res) => {
try {
const body = req.body || {}
const uid = String(body.uid || '7716940453')
const start = normalizeDate(body.start || Date.now())
const end = normalizeDate(body.end || Date.now())
const html = String(body.html || '')
upsertArchive.run({ uid, week_start: start, week_end: end, html })
logJSON('archive.save', { uid, start, end, html_len: html.length }, 'weibo')
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/archives/get', (req, res) => {
try {
const uid = String(req.query.uid || '7716940453')
const date = String(req.query.date || '')
const startParam = String(req.query.start || '')
const range = startParam ? { start: startParam, end: String(req.query.end || '') } : getWeekRangeFromDate(date || Date.now())
const row = getArchive.get(uid, range.start)
logJSON('archive.get', { uid, start: range.start, found: !!row }, 'weibo')
res.json({ ok: true, start: range.start, end: range.end, html: row ? row.html : '' })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
const NAV_AUTH_COOKIE = 'nav_gate'
const TOKEN_LAB_AUTH_COOKIE = 'token_lab_gate'
const hasNavAuthRoot = (req) => {
try {
const cookies = parseCookie(req.headers.cookie || '')
return cookies[NAV_AUTH_COOKIE] === '1'
} catch { return false }
}
const hasTokenLabAuth = (req) => {
try {
const cookies = parseCookie(req.headers.cookie || '')
return cookies[TOKEN_LAB_AUTH_COOKIE] === '1'
} catch { return false }
}
const getTokenLabAuthMaxAge = () => 1000 * 60 * 60 * 24
app.get('/index.html', (req, res) => {
try {
const token = String(req.query.token || '')
const hasCookie = hasNavAuthRoot(req)
if (token) {
const dbg = debugVerifyJwt(token, [], { issStrict: computeNavIssStrict() })
if (!dbg.ok) {
try { logJSON('nav.handshake.fail', { has_cookie: hasCookie, dbg }, 'nav') } catch {}
if (hasCookie) return res.sendFile(path.join(process.cwd(), 'public', 'index.html'))
return res.status(401).send('未授权')
}
res.cookie(NAV_AUTH_COOKIE, '1', { httpOnly: true, sameSite: 'lax' })
return res.redirect('/index.html')
}
if (!computeNavAuthEnabled()) return res.sendFile(path.join(process.cwd(), 'public', 'index.html'))
if (!hasCookie) return res.status(401).send('未授权')
res.sendFile(path.join(process.cwd(), 'public', 'index.html'))
} catch { return res.status(401).send('未授权') }
})
const getQrLib = (() => {
let mod = null
return () => {
if (mod) return mod
try { mod = require('qrcode') } catch {}
return mod
}
})()
app.get('/share/qr', async (req, res) => {
try {
const url = String(req.query.url || '').trim()
const sizeRaw = parseInt(String(req.query.size || ''), 10)
const width = Number.isFinite(sizeRaw) ? Math.min(Math.max(sizeRaw, 80), 1024) : 220
if (!url) return res.status(400).send('missing_url')
const QRCode = getQrLib()
if (!QRCode || typeof QRCode.toFileStream !== 'function') return res.status(500).send('qrcode_unavailable')
res.set('Cache-Control', 'no-store')
res.set('Content-Type', 'image/png')
await QRCode.toFileStream(res, url, { width, margin: 0 })
} catch (e) {
res.status(500).send('qrcode_error')
}
})
app.get('/s/:id', (req, res) => {
res.set('Cache-Control', 'no-store')
res.sendFile(path.join(process.cwd(), 'public', 'tools', 'psc', 'share.html'))
})
const readPscJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'psc.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
app.get('/tools/psc', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readPscJwk()))
if (!payload) return res.status(401).send('未授权')
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'TRAE-PSC')) return res.status(401).send('未授权')
const maxAgePsc = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'psc', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
res.cookie(NAV_AUTH_COOKIE, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgePsc })
return res.sendFile(path.join(process.cwd(), 'public', 'tools', 'psc', 'index.html'))
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/psc', (req, res, next) => {
const hasPscCookie = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'psc', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const name = String(cfg.cookieName || '')
if (name) {
const cookies = parseCookie(req.headers.cookie || '')
return cookies[name] && cookies[name].length > 0
}
}
} catch {}
return false
})()
if (hasNavAuthRoot(req) || hasPscCookie) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
const readRebalanceJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'rebalance.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
app.get('/tools/rebalance', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readRebalanceJwk()))
if (!payload) return res.status(401).send('未授权')
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-rebalance')) return res.status(401).send('未授权')
const cfgPath = path.join(process.cwd(), 'public', 'tools', 'rebalance', 'assets', 'auth_config.json')
const { maxAgeRb, cookieNameRb } = (() => {
let cookieName = 'rebalance_gate'
let maxAge = 90 * 24 * 3600 * 1000
try {
if (fs.existsSync(cfgPath)) {
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) maxAge = Math.min(days, 365) * 24 * 3600 * 1000
cookieName = String(cfg.cookieName || cookieName)
}
} catch {}
return { maxAgeRb: maxAge, cookieNameRb: cookieName }
})()
res.cookie(cookieNameRb, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeRb })
return res.redirect('/tools/rebalance')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/rebalance', (req, res, next) => {
if (hasNavAuthRoot(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
const readDltJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'dlt_draws.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
app.get('/tools/dlt_draws', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readDltJwk()))
if (!payload) return res.status(401).send('未授权')
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-dlt_draws')) return res.status(401).send('未授权')
const cfgPath = path.join(process.cwd(), 'public', 'tools', 'dlt_draws', 'assets', 'auth_config.json')
const { maxAgeDlt, cookieNameDlt } = (() => {
let cookieName = 'dlt_draws_gate'
let maxAge = 90 * 24 * 3600 * 1000
try {
if (fs.existsSync(cfgPath)) {
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) maxAge = Math.min(days, 365) * 24 * 3600 * 1000
cookieName = String(cfg.cookieName || cookieName)
}
} catch {}
return { maxAgeDlt: maxAge, cookieNameDlt: cookieName }
})()
res.cookie(cookieNameDlt, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeDlt })
return res.redirect('/tools/dlt_draws')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/dlt_draws', (req, res, next) => {
if (hasNavAuthRoot(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
// Yuanzhupai: JWT handshake MUST be before express.static (otherwise directory index intercepts it)
const YUANZHUPAI_AUTH_COOKIE = 'yuanzhupai_gate'
const readYuanzhupaiAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'yuanzhupai', 'auth_config.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return {}
}
const readYuanzhupaiJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'yuanzhupai.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getYuanzhupaiCookieName = () => {
try { const n = String(readYuanzhupaiAuthConfig().cookieName || ''); if (n) return n } catch {}
return YUANZHUPAI_AUTH_COOKIE
}
const getYuanzhupaiMaxAge = () => {
try { const days = Number(readYuanzhupaiAuthConfig().max_age_days || 0); if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000 } catch {}
return 30 * 24 * 3600 * 1000
}
const getYuanzhupaiGuidConfig = () => {
try {
const cfg = readYuanzhupaiAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch { return { key: 'guid', value: '' } }
}
const hasValidYuanzhupaiGuid = req => {
try {
const cfg = readYuanzhupaiAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getYuanzhupaiGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const isYuanzhupaiForceGuid = () => {
try {
const cfg = readYuanzhupaiAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return false
} catch { return false }
}
const hasYuanzhupaiAuth = req => {
try {
const config = readYuanzhupaiAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
return cookies[getYuanzhupaiCookieName()] === '1'
} catch { return false }
}
app.get('/tools/yuanzhupai', (req, res, next) => {
setNoCache(res)
try {
const forceGuid = isYuanzhupaiForceGuid()
if (hasYuanzhupaiAuth(req)) return next()
const guidKey = getYuanzhupaiGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidYuanzhupaiGuid(req)) {
const maxAge = getYuanzhupaiMaxAge()
const cookieName = getYuanzhupaiCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/yuanzhupai/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readYuanzhupaiJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-yuanzhupai')) return res.status(401).send('未授权')
}
const maxAge = getYuanzhupaiMaxAge()
const cookieName = getYuanzhupaiCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/yuanzhupai/index.html')
} catch (e) { return res.status(401).send('未授权') }
})
app.use('/tools/yuanzhupai', (req, res, next) => {
const forceGuid = isYuanzhupaiForceGuid()
if (hasYuanzhupaiAuth(req)) return next()
const guidKey = getYuanzhupaiGuidConfig().key
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
if (isHandshake && hasValidYuanzhupaiGuid(req)) {
const maxAge = getYuanzhupaiMaxAge()
const cookieName = getYuanzhupaiCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/yuanzhupai/index.html')
}
if (isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)) return res.status(401).send('未授权')
if (isHandshake && String(req.query.token || '')) {
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readYuanzhupaiJwk()))
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-yuanzhupai')) return res.status(401).send('未授权')
const maxAge = getYuanzhupaiMaxAge()
const cookieName = getYuanzhupaiCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/yuanzhupai/index.html')
}
}
if (isHandshake && forceGuid) return res.status(401).send('未授权')
return res.status(401).send('未授权')
})
// Short Link: 鉴权基础函数 + 路由守卫(必须在 express.static 之前)
const SHORT_LINK_AUTH_COOKIE = 'short_link_gate'
const readShortLinkAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'short_link', 'auth_config.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return {}
}
const readShortLinkJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'short_link.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getShortLinkCookieName = () => {
try { const n = String(readShortLinkAuthConfig().cookieName || ''); if (n) return n } catch {}
return SHORT_LINK_AUTH_COOKIE
}
const getShortLinkMaxAge = () => {
try { const days = Number(readShortLinkAuthConfig().max_age_days || 0); if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000 } catch {}
return 30 * 24 * 3600 * 1000
}
const getShortLinkGuidConfig = () => {
try {
const cfg = readShortLinkAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch { return { key: 'guid', value: '' } }
}
const hasValidShortLinkGuid = req => {
try {
const cfg = readShortLinkAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getShortLinkGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const isShortLinkForceGuid = () => {
try {
const cfg = readShortLinkAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return false
} catch { return false }
}
const hasShortLinkAuth = req => {
try {
const config = readShortLinkAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
return cookies[getShortLinkCookieName()] === '1'
} catch { return false }
}
// 第1层:精确入口守卫
app.get('/tools/short_link', (req, res, next) => {
setNoCache(res)
try {
const forceGuid = isShortLinkForceGuid()
if (hasShortLinkAuth(req)) return next()
const guidKey = getShortLinkGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidShortLinkGuid(req)) {
const maxAge = getShortLinkMaxAge()
const cookieName = getShortLinkCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/short_link/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readShortLinkJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-short_link')) return res.status(401).send('未授权')
}
const maxAge = getShortLinkMaxAge()
const cookieName = getShortLinkCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/short_link/index.html')
} catch (e) { return res.status(401).send('未授权') }
})
// 第2层:子路径守卫(拦截静态资源绕过)
app.use('/tools/short_link', (req, res, next) => {
const forceGuid = isShortLinkForceGuid()
if (hasShortLinkAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
const guidKey = getShortLinkGuidConfig().key
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (isHandshake && hasValidShortLinkGuid(req)) {
const maxAge = getShortLinkMaxAge()
const cookieName = getShortLinkCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/short_link/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
if (isHandshake && String(req.query.token || '')) {
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readShortLinkJwk()))
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-short_link')) return res.status(401).send('未授权')
const maxAge = getShortLinkMaxAge()
const cookieName = getShortLinkCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/short_link/index.html')
}
}
if (isHandshake && forceGuid) return res.status(401).send('未授权')
return res.status(401).send('未授权')
})
const BOX_BACKUP_AUTH_COOKIE = 'box_disaster_sentinel_gate'
const readBoxBackupAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'box_disaster_sentinel', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readBoxBackupJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'box_disaster_sentinel.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getBoxBackupCookieName = () => {
try {
const cfg = readBoxBackupAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return BOX_BACKUP_AUTH_COOKIE
}
const getBoxBackupMaxAge = () => {
try {
const cfg = readBoxBackupAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 30 * 24 * 3600 * 1000
}
const hasBoxBackupAuth = req => {
try {
const cfg = readBoxBackupAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
return cookies[getBoxBackupCookieName()] === '1'
} catch {
return false
}
}
app.get('/tools/box_disaster_sentinel', (req, res) => {
try {
if (hasBoxBackupAuth(req)) {
return res.sendFile(path.join(process.cwd(), 'public', 'tools', 'box_disaster_sentinel', 'index.html'))
}
const token = String(req.query.token || '').trim()
if (!token) return res.status(401).send('未授权')
const payload = verifyJwtWithKeys(token, jwkKeys(readBoxBackupJwk()))
if (!payload) return res.status(401).send('未授权')
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-box_disaster_sentinel')) return res.status(401).send('未授权')
res.cookie(getBoxBackupCookieName(), '1', { httpOnly: true, sameSite: 'lax', maxAge: getBoxBackupMaxAge(), path: '/' })
return res.redirect('/tools/box_disaster_sentinel/index.html')
} catch {
return res.status(401).send('未授权')
}
})
app.use('/tools/box_disaster_sentinel', (req, res, next) => {
if (hasBoxBackupAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/box_backup', (req, res, next) => {
if (hasBoxBackupAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/box_backup/overview', (req, res) => {
try {
res.json(boxBackup.getOverview())
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/current_run', (req, res) => {
try {
res.json(boxBackup.getCurrentRun())
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/runs', (req, res) => {
try {
const limit = parseInt(String(req.query.limit || '50'), 10) || 50
res.json({ ok: true, rows: boxBackup.listRuns(limit) })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/events', (req, res) => {
try {
const limit = parseInt(String(req.query.limit || '200'), 10) || 200
const failuresOnly = String(req.query.failures_only || '') === '1'
res.json({ ok: true, rows: boxBackup.listEvents({ limit, failuresOnly }) })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/alerts', (req, res) => {
try {
const limit = parseInt(String(req.query.limit || '100'), 10) || 100
res.json({ ok: true, rows: boxBackup.listAlerts(limit) })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/catalog', (req, res) => {
try {
const runLimit = parseInt(String(req.query.run_limit || '30'), 10) || 30
const objectLimit = parseInt(String(req.query.object_limit || '200'), 10) || 200
const status = String(req.query.status || '').trim()
res.json(boxBackup.getBackupCatalog({ runLimit, objectLimit, status }))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/config', (req, res) => {
try {
res.json(boxBackup.getConfig())
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/box_backup/config/save', (req, res) => {
try {
res.json(boxBackup.saveConfig((req.body || {}).config || {}))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/box_backup/manual_baidu_backup', (req, res) => {
try {
const row = boxBackup.recordManualBaiduBackup((req.body || {}).note || '')
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/box_backup/run/high', async (req, res) => {
try {
res.json(await boxBackup.triggerRun('high', 'manual'))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/box_backup/run/low', async (req, res) => {
try {
res.json(await boxBackup.triggerRun('low', 'manual'))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/box_backup/run/all', async (req, res) => {
try {
res.json(await boxBackup.triggerRun('all', 'manual'))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/box_backup/run/verify', async (req, res) => {
try {
res.json(await boxBackup.triggerRun('verify', 'manual'))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/box_backup/run/restore', async (req, res) => {
try {
res.json(await boxBackup.triggerRun('restore', 'manual'))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/download/highfreq', async (req, res) => {
try {
const row = await boxBackup.createHighfreqDownload(req.query.id || '')
const safe = String(row.file_name || 'box_backup_highfreq.zip').replace(/[\r\n"]/g, '_')
const fallback = safe.replace(/[^\x20-\x7E]/g, '_')
const utf8 = encodeURIComponent(safe)
res.setHeader('Content-Type', row.content_type || 'application/octet-stream')
if (row.content_length) res.setHeader('Content-Length', String(row.content_length))
res.setHeader('Content-Disposition', `attachment; filename="${fallback}"; filename*=UTF-8''${utf8}`)
row.stream.on('error', () => { try { res.destroy() } catch {} })
row.stream.pipe(res)
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/download/lowfreq', async (req, res) => {
try {
const row = await boxBackup.createLowfreqDownload(req.query.id || '')
const safe = String(row.file_name || 'box_backup_lowfreq.bin').replace(/[\r\n"]/g, '_')
const fallback = safe.replace(/[^\x20-\x7E]/g, '_')
const utf8 = encodeURIComponent(safe)
res.setHeader('Content-Type', row.content_type || 'application/octet-stream')
if (row.content_length) res.setHeader('Content-Length', String(row.content_length))
res.setHeader('Content-Disposition', `attachment; filename="${fallback}"; filename*=UTF-8''${utf8}`)
row.stream.on('error', () => { try { res.destroy() } catch {} })
row.stream.pipe(res)
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/box_backup/download/lowfreq_decrypted', async (req, res) => {
try {
const row = await boxBackup.createLowfreqDecryptedDownload(req.query.id || '')
const safe = String(row.file_name || 'box_backup_lowfreq.bin').replace(/[\r\n"]/g, '_')
const fallback = safe.replace(/[^\x20-\x7E]/g, '_')
const utf8 = encodeURIComponent(safe)
res.setHeader('Content-Type', row.content_type || 'application/octet-stream')
if (row.content_length) res.setHeader('Content-Length', String(row.content_length))
res.setHeader('Content-Disposition', `attachment; filename="${fallback}"; filename*=UTF-8''${utf8}`)
res.send(row.data)
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
// web_order_box routes - 原生 HTML/CSS/JS Web 应用
app.get('/tools/web_order_box', (req, res) => {
res.sendFile(path.join(process.cwd(), 'public', 'tools', 'web_order_box', 'index.html'))
})
app.use('/tools/web_order_box', (req, res, next) => {
try {
const authConfigPath = path.join(process.cwd(), 'public', 'tools', 'web_order_box', 'auth_config.json')
if (fs.existsSync(authConfigPath)) {
const authConfig = JSON.parse(fs.readFileSync(authConfigPath, 'utf-8'))
if (authConfig.enable_auth === false) return next()
}
if (hasNavAuthRoot(req)) return next()
return res.status(401).send('未授权')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/web_order_box', (req, res, next) => {
res.set('X-Frame-Options', 'SAMEORIGIN')
next()
})
app.use('/tools/web_order_box', express.static(path.join(process.cwd(), 'public', 'tools', 'web_order_box')))
app.get('/tools/token_lab', (req, res) => {
try {
if (hasNavAuthRoot(req)) return res.redirect('/tools/token_lab/index.html')
const token = String(req.query.token || '').trim()
if (!token) return res.status(401).send('未授权')
const payload = verifyJwtWithKeys(token, [])
if (!payload) return res.status(401).send('未授权')
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-token_lab')) return res.status(401).send('未授权')
res.cookie(NAV_AUTH_COOKIE, '1', { httpOnly: true, sameSite: 'lax' })
return res.redirect('/tools/token_lab/index.html')
} catch {
return res.status(401).send('未授权')
}
})
app.use('/tools/token_lab', (req, res, next) => {
try {
if (hasNavAuthRoot(req)) return next()
return res.status(401).send('未授权')
} catch {
return res.status(401).send('未授权')
}
})
app.use('/tools/token_lab', (req, res, next) => {
res.set('X-Frame-Options', 'SAMEORIGIN')
next()
})
app.use('/tools/token_lab', express.static(path.join(process.cwd(), 'public', 'tools', 'token_lab')))
app.use('/tools/investment_ledger', (req, res, next) => {
try {
if (hasNavAuthRoot(req)) return next()
return res.status(401).send('未授权')
} catch { return res.status(401).send('未授权') }
})
// ============================================================
// 思想实验室:纯导航鉴权(nav_gate)+ lab 静态资源 + lab 列表 API
// 必须注册在下方全局 express.static 之前,否则静态资源会绕过鉴权中间件
// ============================================================
try {
thoughtLab.bindRoutes(app)
} catch (e) {
try { logJSON('thought_lab.bind_routes.error', { error: String(e.message || e) }, 'thought_lab') } catch {}
}
app.get(['/package.json', '/package-lock.json', '/pnpm-lock.yaml', '/yarn.lock', '/robots.txt'], (_req, res) => {
res.status(404).send('Not Found')
})
app.get('/api/zen_box/playlists', (req, res) => {
try {
const zenDir = path.join(process.cwd(), 'public', 'tools', 'web_zen_box')
const files = fs.existsSync(zenDir) ? fs.readdirSync(zenDir, { withFileTypes: true }) : []
const playlists = files
.filter(entry => entry && entry.isFile && entry.isFile())
.map(entry => String(entry.name || ''))
.filter(name => /playlist\.json$/i.test(name))
.sort((a, b) => {
if (a === 'playlist.json') return -1
if (b === 'playlist.json') return 1
return a.localeCompare(b, 'zh-CN')
})
res.json({ ok: true, playlists })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// 观空禅音:登录验证(读环境变量,不走用户表)
app.post('/api/zen_box/auth/login', (req, res) => {
try {
const { username, password } = req.body || {}
const expectedUser = process.env.ZEN_BOX_USERNAME || ''
const expectedPass = process.env.ZEN_BOX_PASSWORD || ''
if (expectedUser && expectedPass && username === expectedUser && password === expectedPass) {
return res.json({ ok: true, username })
}
return res.status(401).json({ ok: false, error: '账号或密码错误' })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// 观空禅音:云端配置存储(简易文件存储,按 username 隔离)
const ZEN_BOX_CONFIG_DIR = path.join(process.cwd(), 'data', 'zen_box_configs')
app.get('/api/zen_box/config', (req, res) => {
try {
const username = String(req.query.username || '').trim()
if (!username) return res.status(400).json({ ok: false, error: '缺少 username 参数' })
const configFile = path.join(ZEN_BOX_CONFIG_DIR, `${username}.json`)
if (!fs.existsSync(configFile)) return res.json({ ok: true, config: null })
const raw = fs.readFileSync(configFile, 'utf-8')
const config = JSON.parse(raw)
return res.json({ ok: true, config })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/zen_box/config', (req, res) => {
try {
const { username, config } = req.body || {}
if (!username || !config) return res.status(400).json({ ok: false, error: '缺少参数' })
if (!fs.existsSync(ZEN_BOX_CONFIG_DIR)) {
fs.mkdirSync(ZEN_BOX_CONFIG_DIR, { recursive: true })
}
const configFile = path.join(ZEN_BOX_CONFIG_DIR, `${username}.json`)
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf-8')
return res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Static: keep weekly tool assets always fresh
app.use(express.static(path.join(process.cwd(), 'public'), {
setHeaders: (res, filePath) => {
try {
const weeklyRoot = path.join(process.cwd(), 'public', 'tools', 'weekly')
const docCloudKeeperRoot = path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper')
const boxBackupRoot = path.join(process.cwd(), 'public', 'tools', 'box_disaster_sentinel')
if (String(filePath || '').startsWith(weeklyRoot) || String(filePath || '').startsWith(docCloudKeeperRoot) || String(filePath || '').startsWith(boxBackupRoot)) {
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')
try { res.removeHeader('ETag') } catch {}
try { res.removeHeader('Last-Modified') } catch {}
}
} catch {}
}
}))
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')))
app.get(['/', '/tools', '/tools/'], (req, res) => {
try {
const token = String(req.query.token || '')
const hasCookie = hasNavAuthRoot(req)
if (token) {
const dbg = debugVerifyJwt(token, [], { issStrict: computeNavIssStrict() })
if (!dbg.ok) {
try { logJSON('nav.handshake.fail', { has_cookie: hasCookie, dbg }, 'nav') } catch {}
if (hasCookie) return res.sendFile(path.join(process.cwd(), 'public', 'index.html'))
return res.status(401).send('未授权')
}
res.cookie(NAV_AUTH_COOKIE, '1', { httpOnly: true, sameSite: 'lax' })
return res.redirect(req.path)
}
if (!computeNavAuthEnabled()) return res.sendFile(path.join(process.cwd(), 'public', 'index.html'))
if (!hasCookie) return res.status(401).send('未授权')
res.sendFile(path.join(process.cwd(), 'public', 'index.html'))
} catch { return res.status(401).send('未授权') }
})
app.get('/api/tools', (req, res) => {
const toolsPath = path.join(process.cwd(), 'config', 'tools.json')
res.sendFile(toolsPath)
})
app.get('/api/home/bg', async (req, res) => {
try {
const theme = String(req.query.theme || 'default')
const url = String(req.query.url || '').trim()
const preferLocal = String(req.query.prefer_local || '') === '1'
const allPools = String(req.query.all || '') === '1'
if (allPools) {
const baseDir = path.join(process.cwd(), 'public', 'home-bg')
let entries = []
try {
const dirs = fs.readdirSync(baseDir)
for (const name of dirs) {
try {
const full = path.join(baseDir, name)
const st = fs.statSync(full)
if (!st.isDirectory()) continue
const files = fs.readdirSync(full).filter(n => /^pool-\d+(?:-\d+)?\.(?:jpg|jpeg|png|webp)$/i.test(n))
for (const f of files) entries.push({ dir: name, file: f })
} catch {}
}
} catch {}
if (preferLocal) {
if (entries.length) {
const pick = entries[Math.floor(Math.random() * entries.length)]
const p = `/home-bg/${pick.dir}/${pick.file}?v=${Date.now()}`
return res.json({ ok: true, url: p })
}
return res.json({ ok: true, url: url || '' })
}
if (entries.length) {
const pick = entries[Math.floor(Math.random() * entries.length)]
const p = `/home-bg/${pick.dir}/${pick.file}?v=${Date.now()}`
return res.json({ ok: true, url: p })
}
return res.json({ ok: true, url: url || '' })
}
const themeSlug = (name) => {
const map = {
'夜空星轨': 'star-trails',
'蓝天白云': 'blue-sky',
'治愈森林': 'healing-forest',
'运动风': 'sport-style',
'科技霓虹': 'neon-tech',
'商务简约': 'business-minimal',
'暖色日落': 'warm-sunset',
'冷色极夜': 'polar-night'
}
const base = String(map[name] || name).toLowerCase()
const sanitized = base.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g,'') || 'default'
const suffix = crypto.createHash('md5').update(name).digest('hex').slice(0,8)
return `${sanitized}-${suffix}`
}
const safe = themeSlug(theme)
const legacySafe = `${(theme.replace(/[^a-zA-Z0-9._-]/g, '_') || 'default')}-${crypto.createHash('md5').update(theme).digest('hex').slice(0,8)}`
const dir = path.join(process.cwd(), 'public', 'home-bg', safe)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
const legacyDir = path.join(process.cwd(), 'public', 'home-bg', legacySafe)
try {
const poolNew = fs.readdirSync(dir).filter(n => /^pool-\d+(?:-\d+)?\.(?:jpg|jpeg|png|webp)$/i.test(n))
const poolLegacy = fs.existsSync(legacyDir) ? fs.readdirSync(legacyDir).filter(n => /^pool-\d+(?:-\d+)?\.(?:jpg|jpeg|png|webp)$/i.test(n)) : []
if (poolNew.length === 0 && poolLegacy.length > 0) {
for (const f of poolLegacy) {
try { fs.copyFileSync(path.join(legacyDir, f), path.join(dir, f)) } catch {}
}
}
} catch {}
const latestFile = (() => { try { const files = fs.readdirSync(dir); const f = files.find(n => /^latest\.(?:jpg|jpeg|png|webp)$/i.test(n)); return f || '' } catch { return '' } })()
const poolFiles = (() => { try { const files = fs.readdirSync(dir); return files.filter(n => /^pool-\d+(?:-\d+)?\.(?:jpg|jpeg|png|webp)$/i.test(n)) } catch { return [] } })()
const seedDir = path.join(process.cwd(), 'public', 'tools', 'wall')
const seedFiles = (() => { try { return fs.readdirSync(seedDir).filter(n => /^bjt.*\.(?:jpg|jpeg|png|webp)$/i.test(n)) } catch { return [] } })()
const keywords = (() => {
const k = {
'夜空星轨': 'night,sky,stars,astronomy',
'蓝天白云': 'sky,clouds',
'治愈森林': 'forest,trees,nature',
'运动风': 'sport,fitness,run',
'科技霓虹': 'neon,technology,city,night',
'商务简约': 'minimal,business,abstract',
'暖色日落': 'sunset,orange,evening',
'冷色极夜': 'arctic,blue,night,ice'
}
return k[theme] || 'nature,landscape'
})()
const tryFetch = async () => {
if (!url) throw new Error('empty_url')
// SSRF 防护:禁止访问内网地址
try {
const parsed = new URL(url)
const hostname = parsed.hostname.toLowerCase()
const blocked = ['localhost', '127.', '0.', '10.', '172.16.', '172.17.', '172.18.', '172.19.',
'172.20.', '172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.',
'172.28.', '172.29.', '172.30.', '172.31.', '192.168.', '169.254.', '::1', '[::1]',
'fe80:', 'fc00:', 'fd00:']
if (blocked.some(b => hostname === b || hostname.startsWith(b))) {
throw new Error('ssrf_blocked')
}
} catch (e) {
if (e.message === 'ssrf_blocked') throw e
// URL 解析失败也放行(相对路径等)
}
const resp = await axios.get(url, { responseType: 'arraybuffer', timeout: 2000, maxContentLength: 15728640 })
const ct = String((resp.headers && resp.headers['content-type']) || '')
if (!/^image\//i.test(ct)) throw new Error('not_image')
const ext = ct.includes('webp') ? 'webp' : (ct.includes('png') ? 'png' : 'jpg')
const out = path.join(dir, `latest.${ext}`)
fs.writeFileSync(out, Buffer.from(resp.data))
return `/home-bg/${safe}/latest.${ext}?v=${Date.now()}`
}
const tryFetchRandomUnsplash = async () => {
const sig = Math.floor(Math.random()*1000000)
const u = `https://image.pollinations.ai/prompt/${encodeURIComponent(keywords)}?width=1920&height=1080&nologo=true&seed=${sig}`
const resp = await axios.get(u, { responseType: 'arraybuffer', timeout: 2000, maxContentLength: 15728640 })
const ct = String((resp.headers && resp.headers['content-type']) || '')
if (!/^image\//i.test(ct)) throw new Error('not_image')
const ext = ct.includes('webp') ? 'webp' : (ct.includes('png') ? 'png' : 'jpg')
const out = path.join(dir, `latest.${ext}`)
fs.writeFileSync(out, Buffer.from(resp.data))
return `/home-bg/${safe}/latest.${ext}?v=${Date.now()}`
}
const pickLocal = () => {
if (poolFiles.length > 0) {
const f = poolFiles[Math.floor(Math.random()*poolFiles.length)]
return `/home-bg/${safe}/${f}?v=${Date.now()}`
}
if (latestFile) {
return `/home-bg/${safe}/${latestFile}?v=${Date.now()}`
}
if (seedFiles.length > 0) {
try {
const s = seedFiles[Math.floor(Math.random()*seedFiles.length)]
const ext = (/\.(webp|png|jpg|jpeg)$/i.test(s) ? RegExp.$1.toLowerCase() : 'jpg')
const out = path.join(dir, `pool-${Date.now()}-seed.${ext === 'jpeg' ? 'jpg' : ext}`)
fs.copyFileSync(s, out)
const name = path.basename(out)
return `/home-bg/${safe}/${name}?v=${Date.now()}`
} catch {}
}
return ''
}
const ensurePool = () => {
const need = Math.max(0, 10 - poolFiles.length)
if (need <= 0) return
const tasks = Array.from({ length: need }).map((_, i) => (async () => {
try {
const sig = Math.floor(Math.random()*1000000)
const u = `https://image.pollinations.ai/prompt/${encodeURIComponent(keywords)}?width=1920&height=1080&nologo=true&seed=${sig}`
const resp = await axios.get(u, { responseType: 'arraybuffer', timeout: 4000, maxContentLength: 15728640 })
const ct = String((resp.headers && resp.headers['content-type']) || '')
if (!/^image\//i.test(ct)) return
const ext = ct.includes('webp') ? 'webp' : (ct.includes('png') ? 'png' : 'jpg')
const out = path.join(dir, `pool-${Date.now()}-${i}.${ext}`)
fs.writeFileSync(out, Buffer.from(resp.data))
} catch {}
}))
tasks.forEach(t => { try { t.then(()=>{}).catch(()=>{}) } catch {} })
}
ensurePool()
let finalUrl = ''
if (preferLocal) {
const p = pickLocal()
if (p) return res.json({ ok: true, url: p })
return res.json({ ok: true, url: url || '' })
}
try {
finalUrl = await tryFetch()
} catch {
try {
finalUrl = await tryFetchRandomUnsplash()
} catch {
const files = poolFiles.length ? poolFiles : (latestFile ? [latestFile] : [])
if (files.length) {
const f = files[Math.floor(Math.random()*files.length)]
finalUrl = `/home-bg/${safe}/${f}?v=${Date.now()}`
} else {
finalUrl = url
}
}
}
res.json({ ok: true, url: finalUrl })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/home/bg/default-list', (req, res) => {
try {
const baseDir = path.join(process.cwd(), 'public', 'home-bg', 'default-c21f969b')
if (!fs.existsSync(baseDir)) return res.json({ ok: true, files: [] })
const files = fs.readdirSync(baseDir).filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f))
res.json({ ok: true, files })
} catch (e) {
res.json({ ok: true, files: [] })
}
})
app.get('/api/home/bg/default-random', (req, res) => {
try {
const baseDir = path.join(process.cwd(), 'public', 'home-bg', 'default-c21f969b')
if (!fs.existsSync(baseDir)) return res.json({ ok: false, error: 'dir not found' })
const files = fs.readdirSync(baseDir).filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f))
if (!files.length) return res.json({ ok: false, error: 'no files' })
const pick = files[Math.floor(Math.random() * files.length)]
res.json({ ok: true, url: `/home-bg/default-c21f969b/${pick}?v=${Date.now()}` })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/home/bg/populate', async (req, res) => {
try {
const themes = [
'夜空星轨','蓝天白云','治愈森林','运动风','科技霓虹','商务简约','暖色日落','冷色极夜'
]
const keywordsMap = {
'夜空星轨': 'night,sky,stars,astronomy',
'蓝天白云': 'sky,clouds',
'治愈森林': 'forest,trees,nature',
'运动风': 'sport,fitness,run',
'科技霓虹': 'neon,technology,city,night',
'商务简约': 'minimal,business,abstract',
'暖色日落': 'sunset,orange,evening',
'冷色极夜': 'arctic,blue,night,ice'
}
const count = Math.max(1, Math.min(20, parseInt(String((req.body||{}).count||'10'),10) || 10))
const seedDir = path.join(process.cwd(), 'public', 'tools', 'wall')
const seedFiles = (() => { try { return fs.readdirSync(seedDir).filter(n => /^bjt.*\.(?:jpg|jpeg|png|webp)$/i.test(n)).map(n => path.join(seedDir, n)) } catch { return [] } })()
const results = []
for (const theme of themes) {
const themeSlug = (name) => {
const map = {
'夜空星轨': 'star-trails',
'蓝天白云': 'blue-sky',
'治愈森林': 'healing-forest',
'运动风': 'sport-style',
'科技霓虹': 'neon-tech',
'商务简约': 'business-minimal',
'暖色日落': 'warm-sunset',
'冷色极夜': 'polar-night'
}
const base = String(map[name] || name).toLowerCase()
const sanitized = base.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g,'') || 'default'
const suffix = crypto.createHash('md5').update(name).digest('hex').slice(0,8)
return `${sanitized}-${suffix}`
}
const safe = themeSlug(theme)
const legacySafe = `${(theme.replace(/[^a-zA-Z0-9._-]/g, '_') || 'default')}-${crypto.createHash('md5').update(theme).digest('hex').slice(0,8)}`
const dir = path.join(process.cwd(), 'public', 'home-bg', safe)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
const legacyDir = path.join(process.cwd(), 'public', 'home-bg', legacySafe)
try {
const poolNew = fs.readdirSync(dir).filter(n => /^pool-\d+(?:-\d+)?\.(?:jpg|jpeg|png|webp)$/i.test(n))
const poolLegacy = fs.existsSync(legacyDir) ? fs.readdirSync(legacyDir).filter(n => /^pool-\d+(?:-\d+)?\.(?:jpg|jpeg|png|webp)$/i.test(n)) : []
if (poolNew.length === 0 && poolLegacy.length > 0) {
for (const f of poolLegacy) {
try { fs.copyFileSync(path.join(legacyDir, f), path.join(dir, f)) } catch {}
}
}
} catch {}
const existing = (() => { try { return fs.readdirSync(dir).filter(n => /^pool-\d+(?:-\d+)?\.(?:jpg|jpeg|png|webp)$/i.test(n)) } catch { return [] } })()
const need = Math.max(0, count - existing.length)
const kw = keywordsMap[theme] || 'nature,landscape'
let ok = 0, fail = 0
for (let i = 0; i < need; i++) {
const sig = Math.floor(Math.random()*1000000)
const u = `https://image.pollinations.ai/prompt/${encodeURIComponent(kw)}?width=1920&height=1080&nologo=true&seed=${sig}`
try {
const resp = await axios.get(u, { responseType: 'arraybuffer', timeout: 8000, maxContentLength: 15728640 })
const ct = String((resp.headers && resp.headers['content-type']) || '')
if (!/^image\//i.test(ct)) throw new Error('not_image')
const ext = ct.includes('webp') ? 'webp' : (ct.includes('png') ? 'png' : 'jpg')
const out = path.join(dir, `pool-${Date.now()}-${i}.${ext}`)
fs.writeFileSync(out, Buffer.from(resp.data))
ok++
} catch {
try {
const picsumUrl = `https://picsum.photos/1920/1080?random=${sig}`
const resp2 = await axios.get(picsumUrl, { responseType: 'arraybuffer', timeout: 8000, maxContentLength: 15728640 })
const ct2 = String((resp2.headers && resp2.headers['content-type']) || '')
if (!/^image\//i.test(ct2)) throw new Error('not_image')
const ext2 = ct2.includes('png') ? 'png' : 'jpg'
const out2 = path.join(dir, `pool-${Date.now()}-${i}.${ext2}`)
fs.writeFileSync(out2, Buffer.from(resp2.data))
ok++
} catch {
try {
if (seedFiles.length) {
const pick = seedFiles[i % seedFiles.length]
const ext3 = (/\.(webp|png)$/i.test(pick)) ? (RegExp.$1.toLowerCase()) : 'jpg'
const out3 = path.join(dir, `pool-${Date.now()}-${i}.${ext3}`)
fs.copyFileSync(pick, out3)
ok++
} else {
fail++
}
} catch { fail++ }
}
}
}
results.push({ theme, dir: `/home-bg/${safe}/`, downloaded: ok, failed: fail, existing: existing.length, seedCount: seedFiles.length })
}
res.json({ ok: true, results })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
const ensureWeeklyLoaded = () => {
let mod = null
try { mod = require('./weekly') } catch (e) { try { logJSON('weekly.require.error', { error: String(e.message || e) }, 'weekly') } catch {}; setTimeout(ensureWeeklyLoaded, 2000); return }
weekly = mod
try {
if (!weeklyBound && weekly && weekly.bindRoutes) {
weekly.bindRoutes(app)
weeklyBound = true
try { logJSON('weekly.bind_routes.done', { ok: true }, 'weekly') } catch {}
} else {
try { logJSON('weekly.bind_routes.missing', { ok: false }, 'weekly') } catch {}
}
} catch (e) {
try { logJSON('weekly.bind_routes.error', { error: String(e.message || e) }, 'weekly') } catch {}
}
try {
if (weekly && weekly.startScheduler) weekly.startScheduler()
} catch (e) {
try { logJSON('weekly.scheduler.start.error', { error: String(e.message || e) }, 'weekly') } catch {}
}
}
const ensureWeeklyEmbedLoaded = () => {
let mod = null
try { mod = require('./weekly_embed') } catch (e) { try { logJSON('weekly_embed.require.error', { error: String(e.message || e) }, 'weekly_embed') } catch {}; setTimeout(ensureWeeklyEmbedLoaded, 2000); return }
weeklyEmbed = mod
try {
if (!weeklyEmbedBound && weeklyEmbed && weeklyEmbed.bindRoutes) {
weeklyEmbed.bindRoutes(app)
weeklyEmbedBound = true
try { logJSON('weekly_embed.bind_routes.done', { ok: true }, 'weekly_embed') } catch {}
} else {
try { logJSON('weekly_embed.bind_routes.missing', { ok: false }, 'weekly_embed') } catch {}
}
} catch (e) {
try { logJSON('weekly_embed.bind_routes.error', { error: String(e.message || e) }, 'weekly_embed') } catch {}
}
}
ensureWeeklyLoaded()
ensureWeeklyEmbedLoaded()
ensurePscLoaded()
aiLib.bindRoutes(app)
expense.bindRoutes(app)
investmentLedger.bindRoutes(app, {
requireAuth: (req, res, next) => {
if (hasNavAuthRoot(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
}
})
styleCheck.bindRoutes(app)
globalNews.bindRoutes(app)
dataGateway.bindRoutes(app)
ossFileCabinet.bindRoutes(app)
// 第3层:Short Link API 鉴权(必须在 bindRoutes 之前注册)
app.use('/api/short_link', (req, res, next) => {
if (req.path === '/auth/hint') return next()
if (hasShortLinkAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/short_link/auth/hint', (req, res) => {
setNoCache(res)
res.json({ ok: true, message: '请通过首页九宫格点击「短链接」进入,或使用正确的访问链接' })
})
// Short Link 业务路由(/s/:code 公开跳转 + /api/short_link/* 管理接口)
shortLink.bindRoutes(app)
freeShow.bindRoutes(app)
const marketsCfgAtBoot = getMarketsCfg()
const currentPort = Number(process.env.PORT || '8976') || 8976
const marketsAutomationPort = Number(marketsCfgAtBoot.automation_port || 8977) || 8977
if (currentPort === marketsAutomationPort) {
startMarketsScheduler()
marketSkills.start()
freeShow.startScheduler()
}
securityCalendar.startScheduler()
securityCalendarBridge.startScheduler(securityCalendar)
marketsWeeklyReport.startScheduler()
globalNews.startScheduler()
app.use('/api/yuanzhupai', (req, res, next) => {
if (req.path === '/auth/hint') return next()
if (hasYuanzhupaiAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/yuanzhupai/auth/hint', (req, res) => {
setNoCache(res)
res.json({ ok: true, message: '请通过首页九宫格点击「圆桌派」进入,或使用正确的访问链接' })
})
app.get('/api/yuanzhupai/scenarios', (req, res) => {
setNoCache(res)
try {
const scenarios = yuanzhupai.listScenarios()
res.json({ ok: true, scenarios })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/yuanzhupai/analysts', (req, res) => {
setNoCache(res)
try {
const cfg = yuanzhupai.readConfig()
const version = String(req.query.version || 'x').trim()
const scenarioId = String(req.query.scenario || 'stock_invest').trim()
const { panelists, chairman, scenario } = yuanzhupai.getScenarioAnalysts(scenarioId, version)
const analysts = chairman ? [...panelists, chairman] : panelists
res.json({ ok: true, analysts, ui_mode: (cfg && cfg.ui_mode) || 'debug', scenario })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/yuanzhupai/analyze', async (req, res) => {
setNoCache(res)
try {
const body = req.body || {}
const code = String(body.code || '').trim()
if (!code) return res.status(400).json({ ok: false, error: '请输入会议议题' })
const version = String(body.version || 'x').trim()
const scenarioId = String(body.scenario || 'stock_invest').trim()
const imageBase64 = String(body.imageBase64 || '')
const userProfile = body.userProfile || null
const logs = []
const result = await yuanzhupai.runMeeting(scenarioId, code, version, line => logs.push(line), { imageBase64, userProfile })
res.json({ ok: true, result, logs })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/yuanzhupai/sessions', (req, res) => {
setNoCache(res)
try {
const limit = Math.min(Number(req.query.limit) || 20, 100)
const sessions = yuanzhupai.listSessions(limit)
res.json({ ok: true, sessions })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/yuanzhupai/sessions/:id', (req, res) => {
setNoCache(res)
try {
const session = yuanzhupai.getSession(req.params.id)
if (!session) return res.status(404).json({ ok: false, error: 'session_not_found' })
res.json({ ok: true, session })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.use((err, req, res, next) => {
try {
logJSON('http.unhandled.error', {
path: req && req.path,
method: req && req.method,
error: String(err && err.message ? err.message : err)
}, 'server')
} catch {}
if (res.headersSent) return next(err)
if (req && req.path && req.path.startsWith('/api/')) {
return res.status(500).json({ ok: false, error: 'internal_error' })
}
return res.status(500).send('服务器开小差了')
})
// 404 处理:覆盖 Express 默认 "Cannot GET" 错误页,避免泄露技术栈特征
app.use((req, res) => {
res.status(404).json({ ok: false, error: 'not_found' })
})
const port = process.env.PORT || 8976
app.listen(port, () => {
console.log(`Yang's Toolbox server listening on http://localhost:${port}`)
cloudBalanceWatch.start().catch(err => {
try { logJSON('cloud.balance.listen.start.error', { error: String(err && err.message ? err.message : err) }, 'cloud') } catch {}
})
})