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' })) app.use((req, res, next) => { const oldJson = res.json.bind(res) res.json = (payload) => { if (!res.getHeader('Content-Type')) { res.setHeader('Content-Type', 'application/json; charset=utf-8') } else { const cur = String(res.getHeader('Content-Type') || '') if (cur.indexOf('charset') === -1) { res.setHeader('Content-Type', cur + '; charset=utf-8') } } return oldJson(payload) } next() }) 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 ensureMpTimeAssistantLoaded = () => { let mod = null try { mod = require('./mp_time_assistant') } catch (e) { try { logJSON('mp_time_assistant.require.error', { error: String(e.message || e) }, 'mp_time_assistant') } catch {}; setTimeout(ensureMpTimeAssistantLoaded, 2000); return } try { if (mod && mod.bindRoutes) { mod.bindRoutes(app) try { logJSON('mp_time_assistant.bind_routes.done', { ok: true }, 'mp_time_assistant') } catch {} } } catch (e) { try { logJSON('mp_time_assistant.bind_routes.error', { error: String(e.message || e) }, 'mp_time_assistant') } catch {} } } ensureMpTimeAssistantLoaded() const ensureMpPersonalCollectLoaded = () => { let mod = null try { mod = require('./mp_personal_collect') } catch (e) { try { logJSON('mp_personal_collect.require.error', { error: String(e.message || e) }, 'mp_personal_collect') } catch {}; setTimeout(ensureMpPersonalCollectLoaded, 2000); return } try { if (mod && mod.bindRoutes) { mod.bindRoutes(app) try { logJSON('mp_personal_collect.bind_routes.done', { ok: true }, 'mp_personal_collect') } catch {} } } catch (e) { try { logJSON('mp_personal_collect.bind_routes.error', { error: String(e.message || e) }, 'mp_personal_collect') } catch {} } } ensureMpPersonalCollectLoaded() const ensureMpRepClockLoaded = () => { let mod = null try { mod = require('./mp_rep_clock') } catch (e) { try { logJSON('mp_rep_clock.require.error', { error: String(e.message || e) }, 'mp_rep_clock') } catch {}; setTimeout(ensureMpRepClockLoaded, 2000); return } try { if (mod && mod.bindRoutes) { mod.bindRoutes(app) try { logJSON('mp_rep_clock.bind_routes.done', { ok: true }, 'mp_rep_clock') } catch {} } } catch (e) { try { logJSON('mp_rep_clock.bind_routes.error', { error: String(e.message || e) }, 'mp_rep_clock') } catch {} } } ensureMpRepClockLoaded() 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 let privateClipboardShareRouter = 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 {} } if (mod && mod.createPublicShareRouter) { privateClipboardShareRouter = mod.createPublicShareRouter() try { logJSON('private_clipboard.create_share_router.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 WEB_MP_TIME_ASSISTANT_AUTH_COOKIE = 'web_mp_time_assistant_gate' const readWebMpTimeAssistantAuthConfig = () => { try { const p = path.join(process.cwd(), 'public', 'tools', 'web_mp_time_assistant', '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 readWebMpTimeAssistantJwk = () => { try { const p = path.join(process.cwd(), 'config', 'web_mp_time_assistant.jwk.json') if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8')) } catch {} return { kty: 'RSA', n: '', e: '' } } const getWebMpTimeAssistantCookieName = () => { try { const cfg = readWebMpTimeAssistantAuthConfig() const n = String(cfg.cookieName || '') if (n) return n } catch {} return WEB_MP_TIME_ASSISTANT_AUTH_COOKIE } const getWebMpTimeAssistantMaxAge = () => { try { const cfg = readWebMpTimeAssistantAuthConfig() 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 getWebMpTimeAssistantGuidConfig = () => { try { const cfg = readWebMpTimeAssistantAuthConfig() 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 hasValidWebMpTimeAssistantGuid = (req) => { try { const cfg = readWebMpTimeAssistantAuthConfig() if (cfg.enable_auth === false) return false const { key, value } = getWebMpTimeAssistantGuidConfig() 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 isWebMpTimeAssistantForceGuid = () => { try { const cfg = readWebMpTimeAssistantAuthConfig() 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 hasWebMpTimeAssistantAuth = (req) => { try { const config = readWebMpTimeAssistantAuthConfig() if (config.enable_auth === false) return true const ua = String((req && req.headers && (req.headers['user-agent'] || req.headers['User-Agent'])) || '').toLowerCase() const mpHdr = String((req && req.headers && (req.headers['x-mp-timeassistant'] || req.headers['X-Mp-Timeassistant'])) || '').trim() if (ua && ua.indexOf('miniprogram') !== -1) return true if (ua && ua.indexOf('micromessenger') !== -1 && ua.indexOf('wxdebug') !== -1) return true if (mpHdr && mpHdr.length >= 6) return true const cookies = parseCookie(req.headers.cookie || '') if (cookies[getWebMpTimeAssistantCookieName()] === '1') return true return false } catch { return false } } app.get('/tools/web_mp_time_assistant', (req, res, next) => { setNoCache(res) try { const forceGuid = isWebMpTimeAssistantForceGuid() if (hasWebMpTimeAssistantAuth(req)) return next() const guidKey = getWebMpTimeAssistantGuidConfig().key const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey) if (hasValidWebMpTimeAssistantGuid(req)) { const maxAge = getWebMpTimeAssistantMaxAge() const cookieName = getWebMpTimeAssistantCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/web_mp_time_assistant/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(readWebMpTimeAssistantJwk())) if (!payload) return res.status(401).send('未授权') { const iss = String(payload.iss || '') const navIss = getNavIssFromFlags() if (iss === navIss && !audMatch(payload, 'Tools-web_mp_time_assistant')) return res.status(401).send('未授权') } const maxAge = getWebMpTimeAssistantMaxAge() const cookieName = getWebMpTimeAssistantCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/web_mp_time_assistant/index.html') } catch (e) { return res.status(401).send('未授权') } }) app.use('/tools/web_mp_time_assistant', (req, res, next) => { const forceGuid = isWebMpTimeAssistantForceGuid() if (hasWebMpTimeAssistantAuth(req)) return next() const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html' || req.path === '/hub.html') const guidKey = getWebMpTimeAssistantGuidConfig().key const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey) if (isHandshake && hasValidWebMpTimeAssistantGuid(req)) { const maxAge = getWebMpTimeAssistantMaxAge() const cookieName = getWebMpTimeAssistantCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/web_mp_time_assistant/index.html') } if (hasGuidInput) return res.status(401).send('未授权') if (isHandshake && String(req.query.token || '')) { const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readWebMpTimeAssistantJwk())) if (payload) { const iss = String(payload.iss || '') const navIss = getNavIssFromFlags() if (iss === navIss && !audMatch(payload, 'Tools-web_mp_time_assistant')) return res.status(401).send('未授权') const maxAge = getWebMpTimeAssistantMaxAge() const cookieName = getWebMpTimeAssistantCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/web_mp_time_assistant/index.html') } } if (isHandshake && forceGuid) return res.status(401).send('未授权') return res.status(401).send('未授权') }) app.use('/api/mp_time_assistant', (req, res, next) => { if (hasWebMpTimeAssistantAuth(req)) return next() return res.status(401).json({ ok: false, error: 'unauthorized' }) }) app.use('/api/mp_personal_collect', (req, res, next) => { if (hasWebMpTimeAssistantAuth(req)) return next() return res.status(401).json({ ok: false, error: 'unauthorized' }) }) app.use('/api/mp_rep_clock', (req, res, next) => { if (hasWebMpTimeAssistantAuth(req)) return next() return res.status(401).json({ ok: false, error: 'unauthorized' }) }) app.use('/uploads/mp_time_assistant', (req, res, next) => { if (hasWebMpTimeAssistantAuth(req)) return next() return res.status(401).send('未授权') }) app.use('/uploads/mp_personal_collect', (req, res, next) => { if (hasWebMpTimeAssistantAuth(req)) return next() return res.status(401).send('未授权') }) app.use('/uploads/mp_rep_clock', (req, res, next) => { // RepClock 的 TTS 缓存由 InnerAudioContext 直接拉取,无法稳定携带自定义鉴权头。 // 这里只对白名单目录放开,其他上传资源仍保持鉴权。 if (req && typeof req.path === 'string' && req.path.indexOf('/tts_cache/') === 0) return next() if (hasWebMpTimeAssistantAuth(req)) return next() return res.status(401).send('未授权') }) 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', 'Tools-greedy_snake': '/tools/greedy_snake', 'Tools-oss_browser_link': '/tools/oss_browser_link', 'Tools-web_mp_time_assistant': '/tools/web_mp_time_assistant' } 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('未授权') }) // 私人剪贴板「限时直通URL」公开访问入口:无任何鉴权,供第三方浏览器/程序直连; // 安全性由 token 随机性(32位hex)与 3 天有效期控制,过期返回失效提示页。 app.use('/share', (req, res, next) => { if (privateClipboardShareRouter) return privateClipboardShareRouter(req, res, next) return res.status(503).json({ ok: false, error: 'service_not_ready' }) }) 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(``) } } 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) }) } }) // 建行定投评估:A股按需评估;海外部分优先读取本地快照,必要时通过美西探针补抓并持久化 app.get('/api/markets/dip_eval', async (req, res) => { try { const payload = await marketsDipEval.getDipEvalReport({ force: String(req.query.force || '') === '1' }) // 统一 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', req) 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, req) => { 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}` // 必须转发 Cookie,否则 8977 的 /api/cloud 守卫会返回 401 const proxied = await axios.get(url, { headers: { Cookie: (req && req.headers && req.headers.cookie) || '' }, 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) }) } }) // ============================================================ // 贪蛇出动 greedy_snake:鉴权基础函数 + 路由守卫 + 轻量战绩存储 // 必须注册在下方全局 express.static 之前,否则静态资源会绕过鉴权中间件 // ============================================================ const GREEDY_SNAKE_AUTH_COOKIE = 'greedy_snake_gate' const readGreedySnakeAuthConfig = () => { try { const p = path.join(process.cwd(), 'public', 'tools', 'greedy_snake', 'auth_config.json') if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8')) } catch {} return {} } const readGreedySnakeJwk = () => { try { const p = path.join(process.cwd(), 'config', 'greedy_snake.jwk.json') if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8')) } catch {} return { kty: 'RSA', n: '', e: '' } } const getGreedySnakeCookieName = () => { try { const n = String(readGreedySnakeAuthConfig().cookieName || ''); if (n) return n } catch {} return GREEDY_SNAKE_AUTH_COOKIE } const getGreedySnakeMaxAge = () => { try { const days = Number(readGreedySnakeAuthConfig().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 getGreedySnakeGuidConfig = () => { try { const cfg = readGreedySnakeAuthConfig() 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 hasValidGreedySnakeGuid = req => { try { const cfg = readGreedySnakeAuthConfig() if (cfg.enable_auth === false) return false const { key, value } = getGreedySnakeGuidConfig() 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 isGreedySnakeForceGuid = () => { try { const cfg = readGreedySnakeAuthConfig() 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 hasGreedySnakeAuth = req => { try { const config = readGreedySnakeAuthConfig() if (config.enable_auth === false) return true const cookies = parseCookie(req.headers.cookie || '') return cookies[getGreedySnakeCookieName()] === '1' } catch { return false } } app.get('/tools/greedy_snake', (req, res, next) => { setNoCache(res) try { const forceGuid = isGreedySnakeForceGuid() if (hasGreedySnakeAuth(req)) return next() const guidKey = getGreedySnakeGuidConfig().key const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey) if (hasValidGreedySnakeGuid(req)) { const maxAge = getGreedySnakeMaxAge() const cookieName = getGreedySnakeCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/greedy_snake/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(readGreedySnakeJwk())) if (!payload) return res.status(401).send('未授权') { const iss = String(payload.iss || '') const navIss = getNavIssFromFlags() if (iss === navIss && !audMatch(payload, 'Tools-greedy_snake')) return res.status(401).send('未授权') } const maxAge = getGreedySnakeMaxAge() const cookieName = getGreedySnakeCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/greedy_snake/index.html') } catch (e) { return res.status(401).send('未授权') } }) app.use('/tools/greedy_snake', (req, res, next) => { const forceGuid = isGreedySnakeForceGuid() if (hasGreedySnakeAuth(req)) return next() const guidKey = getGreedySnakeGuidConfig().key const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html') if (isHandshake && hasValidGreedySnakeGuid(req)) { const maxAge = getGreedySnakeMaxAge() const cookieName = getGreedySnakeCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/greedy_snake/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(readGreedySnakeJwk())) if (payload) { const iss = String(payload.iss || '') const navIss = getNavIssFromFlags() if (iss === navIss && !audMatch(payload, 'Tools-greedy_snake')) return res.status(401).send('未授权') const maxAge = getGreedySnakeMaxAge() const cookieName = getGreedySnakeCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/greedy_snake/index.html') } } if (isHandshake && forceGuid) return res.status(401).send('未授权') return res.status(401).send('未授权') }) // 贪蛇出动:战绩/配置 API(默认受 gate 保护) const GREEDY_SNAKE_DATA_FILE = path.join(process.cwd(), 'data', 'greedy_snake.json') const readGreedySnakeData = () => { try { if (fs.existsSync(GREEDY_SNAKE_DATA_FILE)) return JSON.parse(fs.readFileSync(GREEDY_SNAKE_DATA_FILE, 'utf-8')) } catch {} return { gems: 0, totalCoins: 0, best: 0, todayBest: null, _init: false } } const writeGreedySnakeData = data => { try { const dir = path.dirname(GREEDY_SNAKE_DATA_FILE) if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync(GREEDY_SNAKE_DATA_FILE, JSON.stringify(data, null, 2), 'utf-8') return true } catch { return false } } const greedySnakeTodayBJ = () => { try { return new Date().toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' }) } catch { return new Date().toISOString().slice(0, 10) } } app.use('/api/greedy_snake', (req, res, next) => { if (hasGreedySnakeAuth(req)) return next() return res.status(401).json({ ok: false, error: 'unauthorized' }) }) app.get('/api/greedy_snake/config', (req, res) => { try { const p = path.join(process.cwd(), 'config', 'greedy_snake.json') const cfg = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf-8')) : {} return res.json({ ok: true, data: cfg }) } catch (e) { return res.status(500).json({ ok: false, error: String(e.message || e) }) } }) app.get('/api/greedy_snake/stats', (req, res) => { try { const d = readGreedySnakeData() if (!d._init) { // 首次访问:写入初始钻石 d.gems = d.gems || 100 d._init = true writeGreedySnakeData(d) } const today = greedySnakeTodayBJ() const todayBest = d.todayBest && d.todayBest.date === today ? d.todayBest : null return res.json({ ok: true, data: { gems: d.gems || 0, totalCoins: d.totalCoins || 0, best: d.best || 0, todayBest } }) } catch (e) { return res.status(500).json({ ok: false, error: String(e.message || e) }) } }) app.post('/api/greedy_snake/stats', (req, res) => { try { const b = req.body || {} const d = readGreedySnakeData() if (typeof b.gems === 'number' && Number.isFinite(b.gems)) d.gems = Math.max(0, Math.floor(b.gems)) if (typeof b.totalCoins === 'number' && Number.isFinite(b.totalCoins)) d.totalCoins = Math.max(d.totalCoins || 0, Math.floor(b.totalCoins)) const score = Number(b.score) if (Number.isFinite(score) && score > 0) { d.best = Math.max(d.best || 0, Math.floor(score)) const today = greedySnakeTodayBJ() if (!d.todayBest || d.todayBest.date !== today || (d.todayBest.score || 0) < score) { d.todayBest = { date: today, score: Math.floor(score), name: String(b.name || '').slice(0, 20) || '贪蛇小将' } } } writeGreedySnakeData(d) const today = greedySnakeTodayBJ() return res.json({ ok: true, data: { gems: d.gems || 0, totalCoins: d.totalCoins || 0, best: d.best || 0, todayBest: d.todayBest && d.todayBest.date === today ? d.todayBest : null } }) } catch (e) { return res.status(500).json({ ok: false, error: String(e.message || e) }) } }) // ============================================================ // OSS中转直链 oss_browser_link:鉴权基础函数 + 路由守卫 + 受保护下载 // 必须注册在下方全局 express.static 之前,否则静态资源会绕过鉴权中间件 // ============================================================ const OSS_BROWSER_LINK_AUTH_COOKIE = 'oss_browser_link_gate' const readOssBrowserLinkAuthConfig = () => { try { const p = path.join(process.cwd(), 'public', 'tools', 'oss_browser_link', 'auth_config.json') if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8')) } catch {} return {} } const readOssBrowserLinkJwk = () => { try { const p = path.join(process.cwd(), 'config', 'oss_browser_link.jwk.json') if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8')) } catch {} return { kty: 'RSA', n: '', e: '' } } const getOssBrowserLinkCookieName = () => { try { const n = String(readOssBrowserLinkAuthConfig().cookieName || ''); if (n) return n } catch {} return OSS_BROWSER_LINK_AUTH_COOKIE } const getOssBrowserLinkMaxAge = () => { try { const days = Number(readOssBrowserLinkAuthConfig().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 getOssBrowserLinkGuidConfig = () => { try { const cfg = readOssBrowserLinkAuthConfig() 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 hasValidOssBrowserLinkGuid = req => { try { const cfg = readOssBrowserLinkAuthConfig() if (cfg.enable_auth === false) return false const { key, value } = getOssBrowserLinkGuidConfig() 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 isOssBrowserLinkForceGuid = () => { try { const cfg = readOssBrowserLinkAuthConfig() 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 hasOssBrowserLinkAuth = req => { try { const config = readOssBrowserLinkAuthConfig() if (config.enable_auth === false) return true const cookies = parseCookie(req.headers.cookie || '') return cookies[getOssBrowserLinkCookieName()] === '1' } catch { return false } } app.get('/tools/oss_browser_link', (req, res, next) => { setNoCache(res) try { const forceGuid = isOssBrowserLinkForceGuid() if (hasOssBrowserLinkAuth(req)) return next() const guidKey = getOssBrowserLinkGuidConfig().key const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey) if (hasValidOssBrowserLinkGuid(req)) { const maxAge = getOssBrowserLinkMaxAge() const cookieName = getOssBrowserLinkCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/oss_browser_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(readOssBrowserLinkJwk())) if (!payload) return res.status(401).send('未授权') { const iss = String(payload.iss || '') const navIss = getNavIssFromFlags() if (iss === navIss && !audMatch(payload, 'Tools-oss_browser_link')) return res.status(401).send('未授权') } const maxAge = getOssBrowserLinkMaxAge() const cookieName = getOssBrowserLinkCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/oss_browser_link/index.html') } catch (e) { return res.status(401).send('未授权') } }) app.use('/tools/oss_browser_link', (req, res, next) => { const forceGuid = isOssBrowserLinkForceGuid() if (hasOssBrowserLinkAuth(req)) return next() const guidKey = getOssBrowserLinkGuidConfig().key const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html') if (isHandshake && hasValidOssBrowserLinkGuid(req)) { const maxAge = getOssBrowserLinkMaxAge() const cookieName = getOssBrowserLinkCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/oss_browser_link/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(readOssBrowserLinkJwk())) if (payload) { const iss = String(payload.iss || '') const navIss = getNavIssFromFlags() if (iss === navIss && !audMatch(payload, 'Tools-oss_browser_link')) return res.status(401).send('未授权') const maxAge = getOssBrowserLinkMaxAge() const cookieName = getOssBrowserLinkCookieName() res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' }) return res.redirect('/tools/oss_browser_link/index.html') } } if (isHandshake && forceGuid) return res.status(401).send('未授权') return res.status(401).send('未授权') }) // OSS中转直链:受保护的 zip 下载(必须在全局 express.static 之前注册) const OSS_BROWSER_LINK_ZIP_NAME = 'oss-browser-win32-x64.zip' app.get(`/download_files/${OSS_BROWSER_LINK_ZIP_NAME}`, (req, res) => { if (!hasOssBrowserLinkAuth(req)) return res.status(401).json({ ok: false, error: 'unauthorized' }) const p = path.join(process.cwd(), 'public', 'download_files', OSS_BROWSER_LINK_ZIP_NAME) if (!fs.existsSync(p)) return res.status(404).json({ ok: false, error: '文件尚未中转,暂不可下载' }) res.download(p, OSS_BROWSER_LINK_ZIP_NAME) }) // OSS中转直链:API(默认受 gate 保护,auth/hint 白名单放行) app.get('/api/oss_browser_link/auth/hint', (req, res) => { res.json({ ok: true, message: '请通过首页九宫格「OSS中转直链」进入,或使用正确的访问链接' }) }) app.use('/api/oss_browser_link', (req, res, next) => { if (hasOssBrowserLinkAuth(req)) return next() return res.status(401).json({ ok: false, error: 'unauthorized' }) }) app.get('/api/oss_browser_link/config', (req, res) => { try { const p = path.join(process.cwd(), 'config', 'oss_browser_link.json') const cfg = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf-8')) : {} return res.json({ ok: true, data: { ui_mode: cfg.ui_mode || 'debug', public_url: cfg.public_url || '', launch_protocol: cfg.launch_protocol || 'ossbrowser' } }) } catch (e) { return res.status(500).json({ ok: false, error: String(e.message || e) }) } }) app.get('/api/oss_browser_link/info', (req, res) => { try { const p = path.join(process.cwd(), 'public', 'download_files', OSS_BROWSER_LINK_ZIP_NAME) if (!fs.existsSync(p)) return res.json({ ok: true, data: { available: false, name: OSS_BROWSER_LINK_ZIP_NAME, size: 0 } }) const st = fs.statSync(p) const mtime = new Date(st.mtime).toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' }) return res.json({ ok: true, data: { available: true, name: OSS_BROWSER_LINK_ZIP_NAME, size: st.size, updated_at: mtime } }) } catch (e) { return 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() marketsDipEval.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 {} }) })