Files
Toolbox/src/server/index.js
T
yangxiangyuan d3856cb52a chore: add and update gitignore rules for project files
add a comprehensive gitignore file to exclude unnecessary files like dependencies, runtime data, uploads, local configs and debug temporary files
2026-06-22 21:24:46 +08:00

6088 lines
255 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express')
const cors = require('cors')
const path = require('path')
const fs = require('fs')
const axios = require('axios')
const Database = require('better-sqlite3')
const crypto = require('crypto')
// ===== 从 ~/Toolbox_local_creds.env.local 加载变量到 process.env =====
// ★ 规则:所有密码、密钥、Token、用户名等高危内容,禁止硬编码在源码里。
// 一律放在 ~/Toolbox_local_creds.env.local 中,在此处自动加载到 process.env
// 各模块通过 process.env.XXX 读取,不要直接 import 或 require 凭证。
// 优先级:已存在的 process.env > env 文件 > 模块内 '' fallback
try {
const os = require('os')
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
try {
if (fs.existsSync(usersRoot)) {
fs.readdirSync(usersRoot).forEach(name => {
const dir = path.join(usersRoot, name)
if (dir !== userProfile && fs.existsSync(dir)) candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
})
}
} catch {}
for (const p of candidates) {
if (!fs.existsSync(p)) continue
const content = fs.readFileSync(p, 'utf-8')
content.split(/\r?\n/).forEach(line => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
const idx = trimmed.indexOf('=')
if (idx <= 0) return
const key = trimmed.slice(0, idx).trim()
const val = trimmed.slice(idx + 1).trim()
if (key && !Object.prototype.hasOwnProperty.call(process.env, key)) {
process.env[key] = val
}
})
break
}
} catch {}
// ===== end env loader =====
const { fetchPosts, bootstrapCookies } = require('./weiboClient')
const { getCookiesFromDevTools, screenshotUrl } = require('./devtools')
const { insertPost, updateRetweeted, queryPostsAny, queryPostsAll, insertKeyword, insertTrackedUser, getSettings, upsertArchive, getArchive, insertSettingHistory } = require('./db')
const { fetchAll, queryByDate, getConfig: getMarketsCfg, setConfig: setMarketsCfg, startScheduler: startMarketsScheduler, sendTestEmail, maybeNotify } = require('./markets')
const marketSkills = require('./market_skills/manager')
const securityCalendar = require('./security_calendar')
const securityCalendarBridge = require('./security_calendar_calendar_bridge')
const marketsWeeklyReport = require('./markets_weekly_report')
const { upsertMarketArchive, getMarketArchive } = require('./db')
const { getConfig: getWallCfg, setConfig: setWallCfg, listCards: listWallCards, createCard: createWallCard, updateCardById: updateWallCardById, deleteCardById: deleteWallCardById } = require('./wall')
const funds = require('./funds')
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 dataGateway = require('./data_gateway')
const ossFileCabinet = require('./oss_file_cabinet')
yuanzhupai.initDb()
const app = express()
app.use(cors())
app.use(express.json({ limit: '50mb' }))
const readLocalCreds = () => {
try {
const os = require('os')
const path = require('path')
const fs = require('fs')
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
try {
if (fs.existsSync(usersRoot)) {
fs.readdirSync(usersRoot).forEach(name => {
const dir = path.join(usersRoot, name)
if (dir !== userProfile && fs.existsSync(dir)) {
candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
}
})
}
} catch {}
let content = null
for (const p of candidates) {
if (fs.existsSync(p)) { content = fs.readFileSync(p, 'utf-8'); break }
}
if (!content) return {}
const creds = {}
content.split(/\r?\n/).forEach(line => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
const idx = trimmed.indexOf('=')
if (idx > 0) creds[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim()
})
return creds
} catch { return {} }
}
const checkLocalCredsToken = (req, localCreds) => {
const serverToken = String(localCreds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (!serverToken) return true
const clientToken = String(req.query.token || '')
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/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 CALENDAR_VOICE_AUTH_COOKIE = 'calendar_voice_gate'
const readCalendarVoiceAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_voice', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readCalendarVoiceJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'calendar_voice.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getCalendarVoiceCookieName = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return CALENDAR_VOICE_AUTH_COOKIE
}
const getCalendarVoiceMaxAge = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 120 * 24 * 3600 * 1000
}
const getCalendarVoiceGuidConfig = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'GUID').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'GUID', value: valueRaw }
} catch {
return { key: 'GUID', value: '' }
}
}
const hasValidCalendarVoiceGuid = (req) => {
try {
const cfg = readCalendarVoiceAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getCalendarVoiceGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const hasCalendarVoiceAuth = (req) => {
try {
const cfg = readCalendarVoiceAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getCalendarVoiceCookieName()
return cookies[name] === '1'
} catch { return false }
}
const isCalendarVoiceForceGuid = () => {
try {
const cfg = readCalendarVoiceAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return true
} catch { return true }
}
const hasValidCalendarVoiceToken = (req) => {
try {
const token = String(req.query.token || '')
if (!token) return false
const payload = verifyJwtWithKeys(token, jwkKeys(readCalendarVoiceJwk()))
if (!payload) return false
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-calendar_voice')) return false
return true
} catch {
return false
}
}
const grantCalendarVoiceAuth = (res) => {
const maxAgeCalendarVoice = getCalendarVoiceMaxAge()
const cookieName = getCalendarVoiceCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeCalendarVoice, path: '/' })
}
app.get('/tools/calendar_voice', (req, res, next) => {
try {
const forceGuid = isCalendarVoiceForceGuid()
if (hasCalendarVoiceAuth(req)) return next()
const guidKey = getCalendarVoiceGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidCalendarVoiceGuid(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
if (hasValidCalendarVoiceToken(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (String(req.query.token || '')) return res.status(401).send('未授权')
if (forceGuid) return res.status(401).send('未授权')
grantCalendarVoiceAuth(res)
return next()
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/calendar_voice', (req, res, next) => {
const forceGuid = isCalendarVoiceForceGuid()
if (hasCalendarVoiceAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
if (isHandshake && hasValidCalendarVoiceGuid(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (isHandshake && hasValidCalendarVoiceToken(req)) {
grantCalendarVoiceAuth(res)
return next()
}
if (isHandshake && String(req.query.token || '')) return res.status(401).send('未授权')
if (isHandshake && !forceGuid) {
grantCalendarVoiceAuth(res)
return next()
}
return res.status(401).send('未授权')
})
app.use('/api/calendar_voice', (req, res, next) => {
if (hasCalendarVoiceAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
const ensureCalendarVoiceLoaded = () => {
let mod = null
try { mod = require('./calendar_voice') } catch (e) { try { logJSON('calendar_voice.require.error', { error: String(e.message || e) }, 'calendar_voice') } catch {}; return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('calendar_voice.bind_routes.done', { ok: true }, 'calendar_voice') } catch {}
}
} catch (e) {
try { logJSON('calendar_voice.bind_routes.error', { error: String(e.message || e) }, 'calendar_voice') } catch {}
}
}
ensureCalendarVoiceLoaded()
const ensurePscLoaded = () => {
let mod = null
try { mod = require('./psc') } catch (e) { try { logJSON('psc.require.error', { error: String(e.message || e) }, 'psc') } catch {}; setTimeout(ensurePscLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('psc.bind_routes.done', { ok: true }, 'psc') } catch {}
}
} catch (e) {
try { logJSON('psc.bind_routes.error', { error: String(e.message || e) }, 'psc') } catch {}
}
}
ensurePscLoaded()
let privateClipboardRouter = null
const ensurePrivateClipboardLoaded = () => {
let mod = null
try { mod = require('./private_clipboard') } catch (e) { try { logJSON('private_clipboard.require.error', { error: String(e.message || e) }, 'private_clipboard') } catch {}; setTimeout(ensurePrivateClipboardLoaded, 2000); return }
try {
if (mod && mod.createRouter) {
privateClipboardRouter = mod.createRouter()
try { logJSON('private_clipboard.create_router.done', { ok: true }, 'private_clipboard') } catch {}
} else if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('private_clipboard.bind_routes.done', { ok: true }, 'private_clipboard') } catch {}
}
} catch (e) {
try { logJSON('private_clipboard.create_router.error', { error: String(e.message || e) }, 'private_clipboard') } catch {}
}
}
ensurePrivateClipboardLoaded()
const ensureFitnessDiaryLoaded = () => {
let mod = null
try { mod = require('./fitness_diary') } catch (e) { try { logJSON('fitness_diary.require.error', { error: String(e.message || e) }, 'fitness_diary') } catch {}; setTimeout(ensureFitnessDiaryLoaded, 2000); return }
try {
if (mod && mod.initDb) {
// Use __dirname to ensure correct path regardless of CWD
mod.initDb(path.join(__dirname, '..', '..', 'data', 'fitness_diary.db'))
}
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('fitness_diary.bind_routes.done', { ok: true }, 'fitness_diary') } catch {}
}
} catch (e) {
try { logJSON('fitness_diary.bind_routes.error', { error: String(e.message || e) }, 'fitness_diary') } catch {}
}
}
const ensurePlantHomeLoaded = () => {
let mod = null
try { mod = require('./plant_home') } catch (e) { try { logJSON('plant_home.require.error', { error: String(e.message || e) }, 'plant_home') } catch {}; setTimeout(ensurePlantHomeLoaded, 2000); return }
try {
if (mod && mod.initDb) {
mod.initDb(path.join(__dirname, '..', '..', 'data', 'plant_home.db'))
}
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('plant_home.bind_routes.done', { ok: true }, 'plant_home') } catch {}
}
} catch (e) {
try { logJSON('plant_home.bind_routes.error', { error: String(e.message || e) }, 'plant_home') } catch {}
}
}
const ensureAppleWatchLoaded = () => {
let mod = null
try { mod = require('./apple_watch') } catch (e) { try { logJSON('apple_watch.require.error', { error: String(e.message || e) }, 'apple_watch') } catch {}; setTimeout(ensureAppleWatchLoaded, 2000); return }
try {
if (mod && mod.initDb) {
mod.initDb(path.join(__dirname, '..', '..', 'data', 'apple_watch.db'))
}
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('apple_watch.bind_routes.done', { ok: true }, 'apple_watch') } catch {}
}
} catch (e) {
try { logJSON('apple_watch.bind_routes.error', { error: String(e.message || e) }, 'apple_watch') } catch {}
}
}
const FITNESS_DIARY_AUTH_COOKIE = 'fitness_diary_gate'
const readFitnessDiaryAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'fitness_diary', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readFitnessDiaryJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'fitness_diary.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getFitnessDiaryCookieName = () => {
try {
const cfg = readFitnessDiaryAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return FITNESS_DIARY_AUTH_COOKIE
}
const getFitnessDiaryMaxAge = () => {
try {
const cfg = readFitnessDiaryAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 90 * 24 * 3600 * 1000
}
const getFitnessDiaryGuidConfig = () => {
try {
const cfg = readFitnessDiaryAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch {
return { key: 'guid', value: '' }
}
}
const hasValidFitnessDiaryGuid = (req) => {
try {
const cfg = readFitnessDiaryAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getFitnessDiaryGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const hasFitnessDiaryAuth = (req) => {
try {
const config = readFitnessDiaryAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getFitnessDiaryCookieName()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/fitness_diary', (req, res, next) => {
try {
if (hasFitnessDiaryAuth(req)) return next()
const guidKey = getFitnessDiaryGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidFitnessDiaryGuid(req)) {
const maxAgeFD = getFitnessDiaryMaxAge()
const cookieName = getFitnessDiaryCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeFD, path: '/' })
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readFitnessDiaryJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'FitnessDiary')) return res.status(401).send('未授权')
}
const maxAgeFD = getFitnessDiaryMaxAge()
const cookieName = getFitnessDiaryCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeFD })
return res.redirect('/tools/fitness_diary/index.html')
} catch (e) {
return res.status(401).send('未授权')
}
})
app.use('/tools/fitness_diary', (req, res, next) => {
if (hasFitnessDiaryAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
if (isHandshake && (String(req.query.token || '') || hasValidFitnessDiaryGuid(req))) return next()
return res.status(401).send('未授权')
})
app.use('/api/fitness_diary', (req, res, next) => {
if (hasFitnessDiaryAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
ensureFitnessDiaryLoaded()
const 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: 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 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': '/tools/funds',
'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-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'
}
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_AUTH_COOKIE = 'funds_gate'
const readFundsJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'funds.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasFundsAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', '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', '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 FUNDS_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/funds', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readFundsJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-funds')) return res.status(401).send('未授权')
}
const maxAgeFunds = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', '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', '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 FUNDS_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeFunds, path: '/' })
return res.redirect('/tools/funds')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/funds', (req, res, next) => {
if (hasFundsAuth(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', (req, res, next) => {
if (hasFundsAuth(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 hasPrivateClipboardAuth = (req) => {
try {
const cfg = readPrivateClipboardAuthConfig()
if (cfg.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
const name = getPrivateClipboardCookieName()
return cookies[name] === '1'
} catch { return false }
}
const isPrivateClipboardForceGuid = () => {
try {
const cfg = readPrivateClipboardAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return true
} catch { return true }
}
app.get('/tools/private_clipboard', (req, res, next) => {
try {
const forceGuid = isPrivateClipboardForceGuid()
if (hasPrivateClipboardAuth(req)) return next()
const guidKey = getPrivateClipboardGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidPrivateClipboardGuid(req)) {
const maxAgeClipboard = getPrivateClipboardMaxAge()
const cookieName = getPrivateClipboardCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeClipboard, path: '/' })
return next()
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readPrivateClipboardJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-private_clipboard')) return res.status(401).send('未授权')
}
const maxAgeClipboard = getPrivateClipboardMaxAge()
const cookieName = getPrivateClipboardCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeClipboard, path: '/' })
return next()
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/private_clipboard', (req, res, next) => {
const forceGuid = isPrivateClipboardForceGuid()
if (hasPrivateClipboardAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && hasValidPrivateClipboardGuid(req)) return next()
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/private_clipboard', (req, res, next) => {
if (hasPrivateClipboardAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/api/private_clipboard', (req, res, next) => {
if (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
}
app.post('/api/cloud_notes/login', (req, res) => {
try {
const body = req.body || {}
const u = cloudNotes.login(String(body.username || ''), String(body.password || ''))
if (!u) return res.status(401).json({ ok: false, error: 'unauthorized' })
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.get('/tools/doc_cloud_keeper', (req, res, next) => {
try {
const token = String(req.query.token || '').trim()
if (token) {
let payload = verifyJwtWithKeys(token, jwkKeys(readDocCloudKeeperJwk()))
if (!payload) {
try {
const dbg = debugVerifyJwt(token, [], { issStrict: false })
const now = Math.floor(Date.now() / 1000)
const exp = Number((dbg && dbg.payload && dbg.payload.exp) || 0)
const inGrace = Number.isFinite(exp) && exp > 0 && now > exp && (now - exp) <= (24 * 3600)
if (dbg && dbg.sig_ok && dbg.payload && inGrace) payload = dbg.payload
} catch {}
}
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-doc_cloud_keeper')) {
// invalid aud, just fall through to index.html
} else {
const maxAge = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 180 * 24 * 3600 * 1000
})()
res.cookie(getDocCloudKeeperCookieName(), '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/doc_cloud_keeper/index.html')
}
}
}
return res.sendFile(path.join(process.cwd(), 'public', 'tools', 'doc_cloud_keeper', 'index.html'))
} catch { return res.status(401).send('未授权') }
})
app.use('/uploads/doc_cloud_keeper', (req, res, next) => {
setNoCache(res)
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(401).send('未授权')
})
const docCloudKeeperAgentTokens = new Map()
app.use('/api/doc_cloud_keeper', (req, res, next) => {
if (req.path === '/login' || req.path === '/logout' || req.path === '/auth/local_creds') return next()
if (req.path === '/docs/agent_upload' || req.path === '/docs/download') {
// 允许助手凭借有效 token 直接上传或下载,跳过 cookie 鉴权
const token = String(req.headers['authorization'] || '').replace('Bearer ', '').trim()
if (token) {
const docId = parseInt(String(req.query.id || req.query.doc_id || '0'), 10) || 0
const valid = docCloudKeeperAgentTokens.get(token)
if (valid && valid.doc_id === docId && Date.now() < valid.exp) {
return next()
}
}
}
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.use('/api/doc_calendar_bridge', (req, res, next) => {
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/doc_cloud_keeper/me', (req, res) => {
try {
if (!hasDocCloudKeeperAuth(req)) return res.status(401).json({ ok: false })
const u = docCloudKeeper.getCurrentUser()
res.json({ ok: true, user: { id: u.id, username: String(u.username || 'admin') } })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const dckTempDir = path.join(process.cwd(), 'uploads', 'doc_cloud_keeper', 'temp')
if (!fs.existsSync(dckTempDir)) fs.mkdirSync(dckTempDir, { recursive: true })
app.post('/api/doc_cloud_keeper/upload/temp', express.raw({ type: 'application/octet-stream', limit: '300mb' }), (req, res) => {
try {
const raw = req.body || Buffer.alloc(0)
const filename = String(req.query.filename || 'document.txt')
const sha256 = String(req.query.sha256 || '')
const out = docCloudKeeper.createTempUpload(filename, raw, sha256)
res.json({ ok: true, temp_name: out.temp_name, size_bytes: out.size_bytes, ext: out.ext, sha256: out.sha256 })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/oss_probe/upload', express.raw({ type: '*/*', limit: '300mb' }), async (req, res) => {
try {
const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || '')
const filename = String(req.query.filename || 'oss_probe.txt')
const mime = String(req.query.mime || 'application/octet-stream')
const out = await docCloudKeeper.probeUploadToOss({ filename, mime, buffer: raw })
res.json(out)
} catch (e) {
res.json({
ok: false,
logs: [
'开始 OSS 检测上传',
'上传过程中出现异常,流程已中断',
String(e && e.message ? e.message : e)
],
error: String(e && e.message ? e.message : e)
})
}
})
app.post('/api/doc_cloud_keeper/docs/create', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const id = docCloudKeeper.createDocumentByTemp(Object.assign({}, req.body || {}, { user_id: u.id }))
syncDocToCalendarUpsert(id).catch(() => {})
res.json({ ok: true, id })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/overwrite', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.overwriteDocumentByTemp(body)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/update', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.updateDocumentMeta(body)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/rename', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.renameDocument(body.id, body.filename)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/move', (req, res) => {
try {
const body = req.body || {}
docCloudKeeper.moveDocument(body.id, body.folder_id)
syncDocToCalendarUpsert(body.id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/trash', (req, res) => {
try {
const id = (req.body || {}).id
docCloudKeeper.softDeleteDocument(id)
syncDocToCalendarRemove(id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/restore', (req, res) => {
try {
const id = (req.body || {}).id
docCloudKeeper.restoreDocument(id)
syncDocToCalendarUpsert(id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/trash_delete', (req, res) => {
try {
const id = (req.body || {}).id
docCloudKeeper.hardDeleteDocument(id)
syncDocToCalendarRemove(id).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/bulk_trash', (req, res) => {
try {
const body = req.body || {}
const out = docCloudKeeper.bulkSoftDelete(body.ids || [])
Promise.allSettled((body.ids || []).map(id => syncDocToCalendarRemove(id)))
res.json({ ok: true, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/bulk_move', (req, res) => {
try {
const body = req.body || {}
const out = docCloudKeeper.bulkMove(body.ids || [], body.folder_id)
Promise.allSettled((body.ids || []).map(id => syncDocToCalendarUpsert(id)))
res.json({ ok: true, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/bulk_trash_delete', (req, res) => {
try {
const body = req.body || {}
const out = docCloudKeeper.bulkHardDelete(body.ids || [])
Promise.allSettled((body.ids || []).map(id => syncDocToCalendarRemove(id)))
res.json({ ok: true, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/upsert', async (req, res) => {
try {
const body = req.body || {}
const out = await syncDocToCalendarUpsert(body.doc_id)
res.json({ ok: !!out.ok, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/remove', async (req, res) => {
try {
const body = req.body || {}
const out = await syncDocToCalendarRemove(body.doc_id)
res.json({ ok: !!out.ok, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/restore', async (req, res) => {
try {
const body = req.body || {}
const out = await syncDocToCalendarUpsert(body.doc_id)
res.json({ ok: !!out.ok, result: out })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_calendar_bridge/by_doc_id', (req, res) => {
try {
const docId = parseInt(String(req.query.doc_id || '0'), 10) || 0
const row = docCalendarBridge.byDocId(docId)
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_calendar_bridge/sync_full', async (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const active = docCloudKeeper.listDocuments({ user_id: u.id, include_deleted: false, sort_by: 'updated_at', sort_dir: 'desc' }) || []
const trashed = docCloudKeeper.listDocuments({ user_id: u.id, include_deleted: true, sort_by: 'updated_at', sort_dir: 'desc' }) || []
const upsertResults = await Promise.all(active.map(r => syncDocToCalendarUpsert(r.id)))
const removeResults = await Promise.all(trashed.map(r => syncDocToCalendarRemove(r.id)))
res.json({
ok: true,
stats: {
active: active.length,
trashed: trashed.length,
upsert_ok: upsertResults.filter(x => x && x.ok).length,
remove_ok: removeResults.filter(x => x && x.ok).length
}
})
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/list', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const rows = docCloudKeeper.listDocuments({
user_id: u.id,
include_deleted: String(req.query.trash || '') === '1',
q: req.query.q,
folder_id: req.query.folder_id,
priority: req.query.priority,
status: req.query.status,
sort_by: req.query.sort_by,
sort_dir: req.query.sort_dir
})
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/get', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const row = docCloudKeeper.getDocument(id)
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/get_edit_token', (req, res) => {
try {
const id = parseInt(String(req.body.id || '0'), 10) || 0
const row = docCloudKeeper.getDocument(id)
if (!row) throw new Error('Document not found')
// Generate a short-lived token (1 hour) specifically for the local agent
const crypto = require('crypto')
const token = crypto.randomBytes(32).toString('hex')
docCloudKeeperAgentTokens.set(token, {
doc_id: id,
exp: Date.now() + 3600 * 1000 // 1 hour
})
// Cleanup old tokens occasionally
if (docCloudKeeperAgentTokens.size > 100) {
const now = Date.now()
for (const [k, v] of docCloudKeeperAgentTokens.entries()) {
if (now > v.exp) docCloudKeeperAgentTokens.delete(k)
}
}
res.json({ ok: true, token, doc_id: id, filename: row.filename })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/agent_upload', express.raw({ type: 'application/octet-stream', limit: '300mb' }), (req, res) => {
try {
// 助手直接上传修改后的文件内容,需要提取 doc_id 和文件流
const raw = req.body || Buffer.alloc(0)
const docId = parseInt(String(req.query.doc_id || '0'), 10) || 0
const explicitSize = parseInt(String(req.query.size_bytes || '0'), 10) || raw.length
const row = docCloudKeeper.getDocument(docId)
if (!row) throw new Error('Document not found')
// 复用 overwrite 的逻辑:先存到 temp,再覆盖
const crypto = require('crypto')
const sha256 = crypto.createHash('sha256').update(raw).digest('hex')
const out = docCloudKeeper.createTempUpload(row.filename, raw, sha256)
docCloudKeeper.overwriteDocumentByTemp({
id: docId,
temp_name: out.temp_name,
size_bytes: explicitSize
})
syncDocToCalendarUpsert(docId).catch(() => {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const dckAttachmentName = rawName => {
const safe = String(rawName || 'document').replace(/[\r\n"]/g, '_')
const fallback = safe.replace(/[^\x20-\x7E]/g, '_')
const utf8 = encodeURIComponent(safe)
return `attachment; filename="${fallback}"; filename*=UTF-8''${utf8}`
}
const dckFormatSize = bytes => {
const n = Number(bytes || 0)
if (!Number.isFinite(n) || n <= 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let v = n
let i = 0
while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1 }
return `${v.toFixed(i === 0 ? 0 : 2)} ${units[i]}`
}
const dckSafeFileLabel = text => String(text || '').replace(/[:]/g, ':').replace(/[|]/g, '|').replace(/[<>"/\\?*]/g, '_').trim()
const dckHistoryDownloadName = payload => {
const data = payload || {}
const filename = dckSafeFileLabel(data.filename || 'document')
const sizeStr = dckSafeFileLabel(dckFormatSize(data.size_bytes || 0))
const recordTime = dckSafeFileLabel(String(data.record_time || '').replace(/:/g, ':'))
const uploadTime = dckSafeFileLabel(String(data.upload_time || '').replace(/:/g, ':'))
const sourceName = dckSafeFileLabel(data.source_name || '下载文件')
const deletedPrefix = data.is_deleted ? '【物理删除】' : ''
const parts = [
`文件:${filename}`,
`大小:${sizeStr}`
]
if (recordTime) parts.push(`记录时间:${recordTime}`)
if (uploadTime) parts.push(`上传时间:${uploadTime}`)
parts.push(`来源:${sourceName}`)
return `${deletedPrefix}${parts.join(' | ')}`
}
app.get('/api/doc_cloud_keeper/docs/download', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = docCloudKeeper.getDocumentForDownload(id)
if (!hit) return res.status(404).send('Not Found')
res.setHeader('Content-Disposition', dckAttachmentName(hit.row.filename))
res.setHeader('Content-Type', 'application/octet-stream')
res.sendFile(hit.full)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.get('/api/doc_cloud_keeper/docs/versions', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const versions = docCloudKeeper.listVersions(id)
res.json({ ok: true, versions })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/docs/compare_versions', async (req, res) => {
try {
const comparison = await docCloudKeeper.compareDocumentVersions(req.body || {})
res.json({ ok: true, comparison })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/oss_versions', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const versions = docCloudKeeper.listOssBackups(id)
res.json({ ok: true, versions })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/version_download', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = docCloudKeeper.getVersionForDownload(id)
if (!hit) return res.status(404).send('Not Found')
const v = hit.row
const ext = v.ext ? `.${v.ext}` : ''
const friendlyName = dckHistoryDownloadName({
filename: v.filename || `version-${v.version_no || id}${ext}`,
size_bytes: v.size_bytes || 0,
record_time: v.created_at || '',
upload_time: '',
source_name: `云服务器版本库${ext}`
})
res.setHeader('Content-Disposition', dckAttachmentName(friendlyName))
res.setHeader('Content-Type', 'application/octet-stream')
res.sendFile(hit.full)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.get('/api/doc_cloud_keeper/docs/oss_version_download', async (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = await docCloudKeeper.createOssDownloadStream(id)
if (!hit || !hit.stream || !hit.row) return res.status(404).send('Not Found')
const row = hit.row
const ext = row.ext ? `.${row.ext}` : ''
const friendlyName = dckHistoryDownloadName({
filename: row.filename || `oss-backup-${id}${ext}`,
size_bytes: row.size_bytes || 0,
record_time: row.created_at || '',
upload_time: row.last_uploaded_at || '',
source_name: `OSS容灾库${ext}`,
is_deleted: !!row.is_physical_deleted
})
res.setHeader('Content-Disposition', dckAttachmentName(friendlyName))
res.setHeader('Content-Type', row.mime || 'application/octet-stream')
hit.stream.on('error', err => {
if (!res.headersSent) res.status(500).send(String(err && err.message ? err.message : err))
else res.destroy(err)
})
hit.stream.pipe(res)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.post('/api/doc_cloud_keeper/docs/restore_version', (req, res) => {
try {
docCloudKeeper.restoreVersion(req.body || {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/preview', async (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const out = await docCloudKeeper.previewDocument(id)
res.json(out)
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/docs/preview_pdf', (req, res) => {
try {
const id = parseInt(String(req.query.id || '0'), 10) || 0
const hit = docCloudKeeper.getDocumentForPreview(id)
if (!hit) return res.status(404).send('Not Found')
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'inline; filename="preview.pdf"')
res.setHeader('Cache-Control', 'no-cache')
res.sendFile(hit.full)
} catch (e) { res.status(500).send(String(e.message || e)) }
})
app.get('/api/doc_cloud_keeper/folders/list', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const rows = docCloudKeeper.listFolders(u.id)
res.json({ ok: true, rows })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/create', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const id = docCloudKeeper.createFolder(Object.assign({}, req.body || {}, { user_id: u.id }))
res.json({ ok: true, id })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/update', (req, res) => {
try {
docCloudKeeper.updateFolder(req.body || {})
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/trash', (req, res) => {
try {
docCloudKeeper.softDeleteFolder((req.body || {}).id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/restore', (req, res) => {
try {
docCloudKeeper.restoreFolder((req.body || {}).id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/folders/trash_delete', (req, res) => {
try {
docCloudKeeper.hardDeleteFolder((req.body || {}).id)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/doc_cloud_keeper/stats', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const s = docCloudKeeper.stats(u.id)
res.json({ ok: true, stats: s })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/doc_cloud_keeper/change_password', (req, res) => {
try {
const u = docCloudKeeper.getCurrentUser()
const body = req.body || {}
docCloudKeeper.changePassword(u.id, body.old_password, body.new_password)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const WEIBO_AUTH_COOKIE = 'weibo_gate'
const readWeiboJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'weibo.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasWeiboAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WEIBO_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/weibo', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readWeiboJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-weibo')) return res.status(401).send('未授权')
}
const maxAgeWeibo = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'weibo', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return WEIBO_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeWeibo })
return res.redirect('/tools/weibo')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/weibo', (req, res, next) => {
if (hasWeiboAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.use('/api/weibo', (req, res, next) => {
if (hasWeiboAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/ping', (req, res) => {
res.json({ ok: true })
})
app.get('/api/debug/https', (req, res) => {
res.json({ https: isHttps(req), xfwd: String((req.headers && req.headers['x-forwarded-proto']) || '') })
})
app.get('/api/debug/test-cookie', (req, res) => {
res.cookie('test_cookie', 'ok', { path: '/', maxAge: 60 * 1000 })
res.json({ ok: true })
})
const CALENDAR_AUTH_COOKIE = 'calendar_reminder_gate'
const readCalendarJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'calendar_reminder.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const hasCalendarAuth = (req) => {
try {
const configPath = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (config.enable_auth === false) return true;
}
const cookies = parseCookie(req.headers.cookie || '')
const name = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CALENDAR_AUTH_COOKIE
})()
return cookies[name] === '1'
} catch { return false }
}
app.get('/tools/calendar_reminder', (req, res, next) => {
try {
const token = String(req.query.token || '')
if (!token) return next()
const payload = verifyJwtWithKeys(token, jwkKeys(readCalendarJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-calendar_reminder')) return res.status(401).send('未授权')
}
const maxAgeCal = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return 90 * 24 * 3600 * 1000
})()
const cookieName = (() => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'calendar_reminder', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
const n = String(cfg.cookieName || '')
if (n) return n
}
} catch {}
return CALENDAR_AUTH_COOKIE
})()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge: maxAgeCal })
return res.redirect('/tools/calendar_reminder')
} catch { return res.status(401).send('未授权') }
})
app.use('/tools/calendar_reminder', (req, res, next) => {
if (hasCalendarAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '')
if (isHandshake && String(req.query.token || '')) return next()
return res.status(401).send('未授权')
})
app.get('/go', (req, res) => {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
res.set('Pragma', 'no-cache')
res.set('Expires', '0')
try {
const priv = readNavPrivateJwk()
if (!priv) return res.status(500).send('导航私钥未配置')
const systemId = String(req.query.systemId || '')
if (!systemId) return res.status(400).send('参数错误')
const now = Math.floor(Date.now()/1000)
const exp = now + Math.max(30, Math.min(300, Number(req.query.ttl||120)))
const jti = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2,10)
const iss = getNavIssFromFlags()
const token = signJwtRS256({ iss, aud: systemId, iat: now, exp, jti }, priv)
let dest = String(req.query.url || '')
if (systemId === 'TRAE-PSC') dest = '/tools/psc'
if (!dest) dest = mapSystemIdToUrl(systemId)
if (!dest) return res.status(400).send('目标地址缺失')
const hasQuery = dest.includes('?')
const sep = hasQuery ? '&' : '?'
const finalUrl = dest + sep + 'token=' + encodeURIComponent(token)
if (/^https?:\/\//i.test(finalUrl)) {
try {
const u = new URL(finalUrl)
const host = String(req.headers.host || '')
if (u.host !== host) {
res.set('Content-Type', 'text/html; charset=utf-8')
return res.status(200).send(`<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><script>location.href=${JSON.stringify(finalUrl)}</script></body></html>`)
}
} catch {}
}
res.redirect(finalUrl)
} catch (e) {
res.status(500).send('服务器错误')
}
})
app.get('/api/debug/jwt/verify', (req, res) => {
try {
const token = String(req.query.token || '')
const tool = String(req.query.tool || '')
const readers = {
'expense': () => EXPENSE_JWK,
'ai-lib': () => readAiLibJwk(),
'cloud': () => readCloudJwk(),
'funds': () => readFundsJwk(),
'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/flags', (req, res) => {
const enabled = computeDebugEnabled()
res.json({ ok: true, debugToolEnabled: enabled, debug: { weibo: computeToolDebug('weibo'), markets: computeToolDebug('markets'), wall: computeToolDebug('wall'), funds: computeToolDebug('funds'), 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) })
}
})
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 tool
app.get('/api/funds/config', (req, res) => {
try { res.json({ ok: true, config: funds.getConfig() }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message||e) }) }
})
app.post('/api/funds/config', (req, res) => {
try { const cfg = funds.getConfig(); const body = req.body || {}; const next = Object.assign({}, cfg, body); funds.setConfig(next); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/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 = funds.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/flows', (req, res) => {
try { funds.createFlow(req.body||{}); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.put('/api/funds/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id,10); funds.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/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id,10); funds.deleteFlow(id); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/funds/export.csv', (req, res) => {
try { const start = String(req.query.start||''); const end = String(req.query.end||''); const rows = funds.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.csv"'); res.send(header+content) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) }
})
app.get('/api/ccb_private_funds/config', (req, res) => {
try { res.json({ ok: true, config: ccbPrivateFunds.getConfig() }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/ccb_private_funds/config', (req, res) => {
try { const cfg = ccbPrivateFunds.getConfig(); const body = req.body || {}; const next = Object.assign({}, cfg, body); ccbPrivateFunds.setConfig(next); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/ccb_private_funds/flows', (req, res) => {
try { res.set('Cache-Control', 'no-store'); const start = String(req.query.start || ''); const end = String(req.query.end || ''); const type = String(req.query.type || 'all'); const kw = String(req.query.kw || ''); const rows = ccbPrivateFunds.listFlows(start, end, type, kw); res.json({ ok: true, rows }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/ccb_private_funds/flows', (req, res) => {
try { const id = ccbPrivateFunds.createFlow(req.body || {}); res.json({ ok: true, id }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.put('/api/ccb_private_funds/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id, 10); ccbPrivateFunds.updateFlowById(id, req.body || {}); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/ccb_private_funds/flows/:id', (req, res) => {
try { const id = parseInt(req.params.id, 10); ccbPrivateFunds.deleteFlow(id); res.json({ ok: true }) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/ccb_private_funds/export.csv', (req, res) => {
try { const start = String(req.query.start || ''); const end = String(req.query.end || ''); const rows = ccbPrivateFunds.listFlows(start, end, 'all', ''); const header = '日期,类型,金额(人民币),银行方/状态,期末金额,备注\n'; const content = rows.map(r => `${r.date},${r.type === 'in' ? '汇入' : '汇出'},${(r.amount_cents / 100).toFixed(2)},${(r.bank_status || '').replace(/,/g, ' ')},${(r.ending_cents / 100).toFixed(2)},${(r.remark || '').replace(/,/g, ' ')}`).join('\n'); res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', 'attachment; filename="ccb_private_funds.csv"'); res.send(header + content) } catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/ccb_private_funds/attachments/:flow_id', (req, res) => {
try {
const flowId = parseInt(req.params.flow_id, 10)
if (!flowId) return res.status(400).json({ ok: false, error: 'bad_flow_id' })
const rows = ccbPrivateFunds.listAttachmentsByFlowId(flowId)
res.json({ ok: true, rows })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/ccb_private_funds/attachments/upload', express.raw({ type: 'application/octet-stream', limit: '200mb' }), (req, res) => {
try {
const flow_id = parseInt(req.query.flow_id || '0', 10)
if (!flow_id) return res.status(400).json({ ok: false, error: 'bad_flow_id' })
const raw = req.body || Buffer.alloc(0)
if (!raw.length) return res.status(400).json({ ok: false, error: 'empty_file' })
const filename = String(req.query.filename || 'file.bin').replace(/[<>:"/\\|?*]/g, '_')
const mime = String(req.query.mime || 'application/octet-stream')
const ext = path.extname(filename || '').slice(0, 12)
const outName = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext || '.bin'}`
const rel = path.join('ccb_private_funds', 'attachments', outName).replace(/\\/g, '/')
const abs = path.join(process.cwd(), 'uploads', rel)
if (!fs.existsSync(path.dirname(abs))) fs.mkdirSync(path.dirname(abs), { recursive: true })
fs.writeFileSync(abs, raw)
const id = ccbPrivateFunds.addAttachment({ flow_id, filename, path: rel, size_bytes: raw.length, mime })
res.json({ ok: true, id, url: `/uploads/${rel}` })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/ccb_private_funds/attachments/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10)
if (!id) return res.status(400).json({ ok: false, error: 'bad_id' })
ccbPrivateFunds.deleteAttachmentById(id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Nav Bookmarks API
app.get('/api/nav_bookmarks/categories', (req, res) => {
try {
const rows = NAV_BOOKMARKS_DB.prepare('SELECT code, name, sort_order, created_at, updated_at FROM nav_categories ORDER BY sort_order ASC, code ASC').all()
res.json({ ok: true, rows })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/nav_bookmarks/categories', (req, res) => {
try {
const body = req.body || {}
const code = String(body.code || '').trim()
const name = String(body.name || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
if (!code || !name) return res.status(400).json({ ok: false, error: 'missing_code_or_name' })
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare('INSERT INTO nav_categories (code, name, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?)')
stmt.run(code, name, sortOrder, now, now)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.put('/api/nav_bookmarks/categories/:code', (req, res) => {
try {
const code = String(req.params.code || '').trim()
if (!code) return res.status(400).json({ ok: false, error: 'missing_code' })
const body = req.body || {}
const name = String(body.name || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare('UPDATE nav_categories SET name = ?, sort_order = ?, updated_at = ? WHERE code = ?')
stmt.run(name, sortOrder, now, code)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/nav_bookmarks/categories/:code', (req, res) => {
try {
const code = String(req.params.code || '').trim()
if (!code) return res.status(400).json({ ok: false, error: 'missing_code' })
const inUse = NAV_BOOKMARKS_DB.prepare('SELECT COUNT(1) AS c FROM nav_bookmarks WHERE category_code = ?').get(code)
if (inUse && inUse.c > 0) return res.status(400).json({ ok: false, error: 'category_in_use' })
NAV_BOOKMARKS_DB.prepare('DELETE FROM nav_categories WHERE code = ?').run(code)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/nav_bookmarks/bookmarks', (req, res) => {
try {
const category = String(req.query.category_code || '').trim()
const q = String(req.query.q || '').trim().toLowerCase()
let rows = NAV_BOOKMARKS_DB.prepare('SELECT id, name, url, icon, category_code, remark, sort_order, created_at, updated_at FROM nav_bookmarks ORDER BY sort_order ASC, created_at ASC').all()
if (category) rows = rows.filter(r => String(r.category_code || '') === category)
if (q) {
rows = rows.filter(r => {
const name = String(r.name || '').toLowerCase()
const url = String(r.url || '').toLowerCase()
const remark = String(r.remark || '').toLowerCase()
return name.includes(q) || url.includes(q) || remark.includes(q)
})
}
res.json({ ok: true, rows })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/nav_bookmarks/bookmarks', (req, res) => {
try {
const body = req.body || {}
const name = String(body.name || '').trim()
const url = String(body.url || '').trim()
if (!name || !url) return res.status(400).json({ ok: false, error: 'missing_name_or_url' })
const icon = String(body.icon || '').trim()
const categoryCode = String(body.category_code || '').trim()
const remark = String(body.remark || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
const id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare(`INSERT INTO nav_bookmarks (id, name, url, icon, category_code, remark, sort_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
stmt.run(id, name, url, icon, categoryCode, remark, sortOrder, now, now)
res.json({ ok: true, id })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.put('/api/nav_bookmarks/bookmarks/:id', (req, res) => {
try {
const id = String(req.params.id || '').trim()
if (!id) return res.status(400).json({ ok: false, error: 'missing_id' })
const body = req.body || {}
const name = String(body.name || '').trim()
const url = String(body.url || '').trim()
const icon = String(body.icon || '').trim()
const categoryCode = String(body.category_code || '').trim()
const remark = String(body.remark || '').trim()
const sortOrder = Number.isFinite(body.sort_order) ? Number(body.sort_order) : 0
const now = navNow()
const stmt = NAV_BOOKMARKS_DB.prepare(`UPDATE nav_bookmarks SET name = ?, url = ?, icon = ?, category_code = ?, remark = ?, sort_order = ?, updated_at = ? WHERE id = ?`)
stmt.run(name, url, icon, categoryCode, remark, sortOrder, now, id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/nav_bookmarks/bookmarks/:id', (req, res) => {
try {
const id = String(req.params.id || '').trim()
if (!id) return res.status(400).json({ ok: false, error: 'missing_id' })
NAV_BOOKMARKS_DB.prepare('DELETE FROM nav_bookmarks WHERE id = ?').run(id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/nav_bookmarks/state', (req, res) => {
try {
const categories = NAV_BOOKMARKS_DB.prepare('SELECT code, name, sort_order, created_at, updated_at FROM nav_categories ORDER BY sort_order ASC, code ASC').all()
const bookmarks = NAV_BOOKMARKS_DB.prepare('SELECT id, name, url, icon, category_code, remark, sort_order, created_at, updated_at FROM nav_bookmarks ORDER BY sort_order ASC, created_at ASC').all()
res.json({ ok: true, categories, bookmarks })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Cloud tool
app.get('/api/cloud/operators', (req, res) => { try { res.json({ ok: true, rows: cloud.listOperatorsFn() }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/operators', (req, res) => { try { cloud.createOperator(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/operators/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateOperator(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/operators/:id', (req, res) => { try { cloud.deleteOperator(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.get('/api/cloud/ecs', (req, res) => { try { const op = req.query.operator_id ? parseInt(req.query.operator_id,10) : null; res.json({ ok:true, rows: cloud.listEcsFn(op) }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/ecs', (req, res) => { try { cloud.createEcs(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/ecs/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateEcs(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/ecs/:id', (req, res) => { try { cloud.deleteEcs(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.get('/api/cloud/vas', (req, res) => { try { const op = req.query.operator_id ? parseInt(req.query.operator_id,10) : null; res.json({ ok:true, rows: cloud.listVasFn(op) }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/vas', (req, res) => { try { cloud.createVas(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/vas/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateVas(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/vas/:id', (req, res) => { try { cloud.deleteVas(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.get('/api/cloud/renew', (req, res) => { try { const st = String(req.query.subject_type||''); const sid = req.query.subject_id ? parseInt(req.query.subject_id,10) : null; res.json({ ok:true, rows: cloud.listRenewFn(st || null, sid) }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/renew', (req, res) => { try { cloud.createRenew(req.body||{}); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.put('/api/cloud/renew/:id', (req, res) => { try { const body = Object.assign({}, req.body||{}, { id: parseInt(req.params.id,10) }); cloud.updateRenew(body); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.delete('/api/cloud/renew/:id', (req, res) => { try { cloud.deleteRenew(parseInt(req.params.id,10)); res.json({ ok:true }) } catch (e) { res.status(500).json({ ok:false, error:String(e.message||e) }) } })
app.post('/api/cloud/renew/invoice', express.raw({ type: 'application/octet-stream', limit: '50mb' }), (req, res) => {
try {
const raw = req.body || Buffer.alloc(0)
const name = String(req.query.filename || 'invoice.bin').replace(/[^a-zA-Z0-9._-]/g, '_')
const outName = `${Date.now()}-${name}`
const full = path.join(invoicesDir, outName)
fs.writeFileSync(full, raw)
res.json({ ok: true, path: `/uploads/cloud/invoices/${outName}` })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// Learning Notes API
const notesDir = path.join(process.cwd(), 'uploads', 'cloud', 'notes')
if (!fs.existsSync(notesDir)) fs.mkdirSync(notesDir, { recursive: true })
app.get('/api/cloud/notes', (req, res) => {
const st = String(req.query.subject_type || '')
const sid = parseInt(req.query.subject_id || '0', 10)
const rows = cloud.listNotesFn(st, sid)
res.json({ ok: true, rows })
})
app.post('/api/cloud/notes', (req, res) => {
try {
const body = req.body || {}
const id = cloud.createNote(body)
res.json({ ok: true, id })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.put('/api/cloud/notes/:id', (req, res) => {
try {
const body = Object.assign({}, req.body || {}, { id: parseInt(req.params.id, 10) })
cloud.updateNote(body)
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/cloud/notes/:id', (req, res) => {
try {
cloud.deleteNote(parseInt(req.params.id, 10))
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud/notes/attachment', express.raw({ type: 'application/octet-stream', limit: '200mb' }), (req, res) => {
try {
const note_id = parseInt(req.query.note_id || '0', 10)
if (!note_id) return res.status(400).json({ ok: false, error: 'missing_note_id' })
const raw = req.body || Buffer.alloc(0)
const name = String(req.query.filename || 'file.bin').replace(/[<>:"/\\|?*]/g, '_')
// Chunked upload support
const chunkIdx = parseInt(req.query.chunk_idx || '-1', 10)
const totalChunks = parseInt(req.query.total_chunks || '1', 10)
const uploadId = String(req.query.upload_id || Date.now())
const outName = `${uploadId}-${name}`
const full = path.join(notesDir, outName)
if (chunkIdx >= 0) {
if (chunkIdx === 0) {
fs.writeFileSync(full, raw)
} else {
fs.appendFileSync(full, raw)
}
if (chunkIdx === totalChunks - 1) {
const stats = fs.statSync(full)
const attId = cloud.createAttachment({ note_id, filename: name, file_path: `/uploads/cloud/notes/${outName}`, file_size: stats.size })
return res.json({ ok: true, id: attId, path: `/uploads/cloud/notes/${outName}` })
} else {
return res.json({ ok: true, chunk: chunkIdx })
}
} else {
fs.writeFileSync(full, raw)
const attId = cloud.createAttachment({ note_id, filename: name, file_path: `/uploads/cloud/notes/${outName}`, file_size: raw.length })
res.json({ ok: true, id: attId, path: `/uploads/cloud/notes/${outName}` })
}
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.delete('/api/cloud/notes/attachment/:id', (req, res) => {
try {
cloud.deleteAttachment(parseInt(req.params.id, 10))
res.json({ ok: true })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud/balance_alerts', (req, res) => {
try {
const page = parseInt(String(req.query.page || '1'), 10) || 1
const pageSize = parseInt(String(req.query.page_size || '20'), 10) || 20
const keyword = String(req.query.keyword || '').trim()
const result = cloudBalanceWatch.listAlerts({ page, pageSize, keyword })
res.json(Object.assign({ ok: true }, result))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.get('/api/cloud/balance_alerts/status', (req, res) => {
;(async () => {
try {
const forwarded = await proxyCloudBalanceAutomationStatus('/api/cloud/balance_alerts/status')
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
res.json({ ok: true, data: cloudBalanceWatch.getStatus() })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})()
})
app.get('/api/cloud/balance_alerts/:id', (req, res) => {
try {
const row = cloudBalanceWatch.getAlert(parseInt(String(req.params.id || '0'), 10) || 0)
if (!row) return res.status(404).json({ ok: false, error: 'not_found' })
res.json({ ok: true, row })
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
const proxyCloudBalanceAutomationRequest = async (req, routePath) => {
const cfg = cloudBalanceWatch.readConfig()
const currentPort = Number(process.env.PORT || '8976') || 8976
const automationPort = Number(cfg.automation_port || 8977) || 8977
if (currentPort === automationPort) return null
const url = `http://127.0.0.1:${automationPort}${routePath}`
const proxied = await axios({
url,
method: req.method || 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: req.headers.cookie || ''
},
data: req.body || {},
timeout: 60000,
validateStatus: () => true
})
return {
status: proxied.status || 200,
body: Object.assign({}, proxied.data || {}, {
proxied_to_automation: true,
current_port: currentPort,
automation_port: automationPort
})
}
}
const proxyCloudBalanceAutomationStatus = async routePath => {
const cfg = cloudBalanceWatch.readConfig()
const currentPort = Number(process.env.PORT || '8976') || 8976
const automationPort = Number(cfg.automation_port || 8977) || 8977
if (currentPort === automationPort) return null
const url = `http://127.0.0.1:${automationPort}${routePath}`
const proxied = await axios.get(url, {
timeout: 15000,
validateStatus: () => true
})
return {
status: proxied.status || 200,
body: Object.assign({}, proxied.data || {}, {
proxied_to_automation: true,
current_port: currentPort,
automation_port: automationPort
})
}
}
app.post('/api/cloud/balance_alerts/scan', async (req, res) => {
try {
const forwarded = await proxyCloudBalanceAutomationRequest(req, '/api/cloud/balance_alerts/scan')
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
const result = cloudBalanceWatch.requestScan('manual')
res.json(Object.assign({ ok: true }, result || {}))
} catch (e) {
res.status(500).json({
ok: false,
error: String(e && e.message ? e.message : e),
detail: String(e && e.stack ? e.stack : (e && e.message ? e.message : e)),
route: '/api/cloud/balance_alerts/scan'
})
}
})
app.post('/api/cloud/balance_alerts/:id/retry_calendar', async (req, res) => {
try {
const forwarded = await proxyCloudBalanceAutomationRequest(req, `/api/cloud/balance_alerts/${encodeURIComponent(String(req.params.id || '0'))}/retry_calendar`)
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
const result = await cloudBalanceWatch.retryCalendarById(parseInt(String(req.params.id || '0'), 10) || 0)
res.json(Object.assign({ ok: true }, result || {}))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/cloud/balance_alerts/:id/retry_weekly', async (req, res) => {
try {
const forwarded = await proxyCloudBalanceAutomationRequest(req, `/api/cloud/balance_alerts/${encodeURIComponent(String(req.params.id || '0'))}/retry_weekly`)
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
const result = await cloudBalanceWatch.retryWeeklyById(parseInt(String(req.params.id || '0'), 10) || 0)
res.json(Object.assign({ ok: true }, result || {}))
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
})
app.post('/api/wall/config', (req, res) => {
try {
const body = req.body || {}
const cfg = getWallCfg()
if (Array.isArray(body.cards)) cfg.cards = body.cards
if (typeof body.autoplay_ms === 'number') cfg.autoplay_ms = body.autoplay_ms
if (typeof body.swipe_pause_ms === 'number') cfg.swipe_pause_ms = body.swipe_pause_ms
if (typeof body.max_cards_on_screen === 'number') cfg.max_cards_on_screen = body.max_cards_on_screen
if (body.background && typeof body.background === 'object') cfg.background = body.background
setWallCfg(cfg)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/wall/cards', (req, res) => {
try {
const body = req.body || {}
createWallCard(body)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.put('/api/wall/cards/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10)
const body = req.body || {}
updateWallCardById(id, body)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.delete('/api/wall/cards/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10)
deleteWallCardById(id)
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/markets/archive/get', (req, res) => {
const day = String(req.query.day || '') || new Date().toISOString().slice(0,10)
const row = getMarketArchive.get(day)
res.json({ ok: true, day, html: row ? row.html : '' })
})
app.post('/api/markets/archive/save', (req, res) => {
const body = req.body || {}
const day = String(body.day || '') || new Date().toISOString().slice(0,10)
const html = String(body.html || '')
upsertMarketArchive.run({ day, html })
logJSON('markets.archive.save', { day, html_len: html.length }, 'markets')
res.json({ ok: true })
})
const normalizeDate = s => {
const d = new Date(s || Date.now())
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
const getWeekRangeFromDate = s => {
const d = new Date(s || Date.now())
const dow = d.getDay()
const shiftToMonday = dow === 0 ? -6 : 1 - dow
const start = new Date(d.getFullYear(), d.getMonth(), d.getDate() + shiftToMonday)
const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 6)
return { start: normalizeDate(start), end: normalizeDate(end) }
}
app.post('/api/archives/save', (req, res) => {
try {
const body = req.body || {}
const uid = String(body.uid || '7716940453')
const start = normalizeDate(body.start || Date.now())
const end = normalizeDate(body.end || Date.now())
const html = String(body.html || '')
upsertArchive.run({ uid, week_start: start, week_end: end, html })
logJSON('archive.save', { uid, start, end, html_len: html.length }, 'weibo')
res.json({ ok: true })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/archives/get', (req, res) => {
try {
const uid = String(req.query.uid || '7716940453')
const date = String(req.query.date || '')
const startParam = String(req.query.start || '')
const range = startParam ? { start: startParam, end: String(req.query.end || '') } : getWeekRangeFromDate(date || Date.now())
const row = getArchive.get(uid, range.start)
logJSON('archive.get', { uid, start: range.start, found: !!row }, 'weibo')
res.json({ ok: true, start: range.start, end: range.end, html: row ? row.html : '' })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
const NAV_AUTH_COOKIE = 'nav_gate'
const hasNavAuthRoot = (req) => {
try {
const cookies = parseCookie(req.headers.cookie || '')
return cookies[NAV_AUTH_COOKIE] === '1'
} catch { return false }
}
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('未授权')
})
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) }) }
})
// 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')
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.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)
styleCheck.bindRoutes(app)
globalNews.bindRoutes(app)
dataGateway.bindRoutes(app)
ossFileCabinet.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()
}
securityCalendar.startScheduler()
securityCalendarBridge.startScheduler(securityCalendar)
marketsWeeklyReport.startScheduler()
globalNews.startScheduler()
app.use('/api/yuanzhupai', (req, res, next) => {
if (req.path === '/auth/hint') return next()
if (hasYuanzhupaiAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.get('/api/yuanzhupai/auth/hint', (req, res) => {
setNoCache(res)
res.json({ ok: true, message: '请通过首页九宫格点击「圆桌派」进入,或使用正确的访问链接' })
})
app.get('/api/yuanzhupai/scenarios', (req, res) => {
setNoCache(res)
try {
const scenarios = yuanzhupai.listScenarios()
res.json({ ok: true, scenarios })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/yuanzhupai/analysts', (req, res) => {
setNoCache(res)
try {
const cfg = yuanzhupai.readConfig()
const version = String(req.query.version || 'x').trim()
const scenarioId = String(req.query.scenario || 'stock_invest').trim()
const { panelists, chairman, scenario } = yuanzhupai.getScenarioAnalysts(scenarioId, version)
const analysts = chairman ? [...panelists, chairman] : panelists
res.json({ ok: true, analysts, ui_mode: (cfg && cfg.ui_mode) || 'debug', scenario })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.post('/api/yuanzhupai/analyze', async (req, res) => {
setNoCache(res)
try {
const body = req.body || {}
const code = String(body.code || '').trim()
if (!code) return res.status(400).json({ ok: false, error: '请输入会议议题' })
const version = String(body.version || 'x').trim()
const scenarioId = String(body.scenario || 'stock_invest').trim()
const imageBase64 = String(body.imageBase64 || '')
const userProfile = body.userProfile || null
const logs = []
const result = await yuanzhupai.runMeeting(scenarioId, code, version, line => logs.push(line), { imageBase64, userProfile })
res.json({ ok: true, result, logs })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/yuanzhupai/sessions', (req, res) => {
setNoCache(res)
try {
const limit = Math.min(Number(req.query.limit) || 20, 100)
const sessions = yuanzhupai.listSessions(limit)
res.json({ ok: true, sessions })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
app.get('/api/yuanzhupai/sessions/:id', (req, res) => {
setNoCache(res)
try {
const session = yuanzhupai.getSession(req.params.id)
if (!session) return res.status(404).json({ ok: false, error: 'session_not_found' })
res.json({ ok: true, session })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
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 {}
})
})