Files
Toolbox/src/server/thought_lab/index.js
T
yangxiangyuan 876a3b8768 feat(thought-lab): 新增万花筒与闵可夫斯基画板两个思想实验室
新增两个独立思想实验室:
1. 万花筒模拟器:支持可调参数物理万花筒效果,配置方案SQLite持久化
2. 闵可夫斯基画板:支持世界线绘制、洛伦兹变换、光锥因果判定
同时添加自动绑定实验室后端的逻辑,以及配套的单元测试与前端页面
2026-08-06 17:41:43 +08:00

172 lines
6.2 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.
// ============================================================
// thought_lab/index.js - 思想实验室路由模块
// 职责:
// - /tools/thought_lab/labs/<lab_id>/ 实验页面与静态资源(仅导航鉴权 nav_gate)
// - GET /api/thought_lab/labs 返回已启用的 lab 列表(首页页签渲染用)
//
// 目录约定(参照 data_gateway 的 skills 分类思想):
// - 每个 lab 是 labs/ 下一个完全独立的子文件夹,互不干扰、互不继承
// - 后端侧 labs/<lab_id>/ 存放该 lab 的 config.json;
// 未来若该 lab 需要数据库、OSS、定时任务等后台逻辑,也落在该文件夹内
// - 前端页面对应 public/tools/thought_lab/labs/<lab_id>/index.html
// ============================================================
const fs = require('fs')
const path = require('path')
const express = require('express')
const LABS_DIR = path.join(__dirname, 'labs')
const PUBLIC_DIR = path.join(process.cwd(), 'public', 'tools', 'thought_lab')
const FLAGS_PATH = path.join(process.cwd(), 'config', 'flags.json')
const NAV_AUTH_COOKIE = 'nav_gate'
// 全局导航鉴权开关(与首页 / 其他工具同一来源 config/flags.json -> navAuth.enable_auth)
// 开关关闭时全站放开,本模块同步放开,保持与容器行为一致
const isNavAuthEnabled = () => {
try {
const f = JSON.parse(fs.readFileSync(FLAGS_PATH, 'utf-8'))
const cfg = (f && f.navAuth) || {}
return cfg.enable_auth !== false
} catch { return true }
}
// ------------------------------------------------------------
// 鉴权:仅导航鉴权(nav_gate cookie),无自定义 gate / GUID / token
// ------------------------------------------------------------
const parseCookie = (raw) => {
const out = {}
try {
String(raw || '').split(';').forEach(item => {
const idx = item.indexOf('=')
if (idx < 1) return
const key = item.slice(0, idx).trim()
const val = item.slice(idx + 1).trim()
if (key) out[key] = val
})
} catch {}
return out
}
const hasNavAuth = (req) => {
try {
const cookies = parseCookie(req.headers.cookie || '')
return cookies[NAV_AUTH_COOKIE] === '1'
} catch { return false }
}
// 同源 Referer 判定:从首页导航点击/模态框进入 lab 页面时携带同源 Referer
const hasSameOriginReferer = (req) => {
try {
const referer = String(req.headers.referer || '')
const host = String(req.headers.host || '')
if (!referer || !host) return false
const u = new URL(referer)
return u.host === host
} catch { return false }
}
const navAuthPage = (req, res, next) => {
if (!isNavAuthEnabled()) return next()
if (hasNavAuth(req)) return next()
// 导航鉴权语义:允许从首页导航进入(同源 Referer),直链/外站访问仍拒绝
if (hasSameOriginReferer(req)) return next()
return res.status(401).send('未授权')
}
const navAuthApi = (req, res, next) => {
if (!isNavAuthEnabled()) return next()
if (hasNavAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
}
// ------------------------------------------------------------
// lab 配置读取
// ------------------------------------------------------------
const loadLabConfig = (labId) => {
try {
const cfgPath = path.join(LABS_DIR, labId, 'config.json')
if (!fs.existsSync(cfgPath)) return null
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
if (!cfg || typeof cfg !== 'object') return null
return cfg
} catch {
return null
}
}
// 枚举所有已启用的 lab,按 sort_order 升序(同序按 id 字典序)
const listLabs = () => {
const result = []
try {
if (!fs.existsSync(LABS_DIR)) return result
const dirs = fs.readdirSync(LABS_DIR, { withFileTypes: true })
for (const d of dirs) {
if (!d.isDirectory()) continue
const cfg = loadLabConfig(d.name)
if (!cfg || cfg.enabled === false) continue
const id = String(cfg.id || d.name)
result.push({
id,
name: String(cfg.name || id),
icon: String(cfg.icon || '🧪'),
desc: String(cfg.desc || ''),
sort_order: Number(cfg.sort_order || 0),
href: `/tools/thought_lab/labs/${d.name}/index.html`
})
}
} catch {}
result.sort((a, b) => {
if (a.sort_order !== b.sort_order) return a.sort_order - b.sort_order
return String(a.id).localeCompare(String(b.id))
})
return result
}
// ------------------------------------------------------------
// 路由注册
// 顺序:静态鉴权中间件必须注册在 express.static 之前(Rule 12.4)
// ------------------------------------------------------------
const bindRoutes = (app) => {
// 1) /tools/thought_lab 全部子路径:仅导航鉴权
app.use('/tools/thought_lab', navAuthPage)
// 2) 静态资源:public/tools/thought_lab/labs/<lab_id>/...
app.use('/tools/thought_lab', express.static(PUBLIC_DIR))
// 3) 白名单公开接口:lab 列表(仅名称/图标/简介等元数据,供首页页签渲染,
// 与 /api/tools 同级;按 Rule 12.8 在 API 守卫之前显式注册)
app.get('/api/thought_lab/labs', (req, res) => {
try {
return res.json({ ok: true, labs: listLabs() })
} catch (e) {
return res.status(500).json({ ok: false, error: 'internal_error' })
}
})
// 4) 其余 /api/thought_lab 接口:默认导航鉴权
app.use('/api/thought_lab', navAuthApi)
// 5) lab 后端自动绑定:labs/<lab_id>/index.js 若导出 bindRoutes 则独立注册
// (注册在 API 守卫之后,自动继承导航鉴权;各 lab 互不干扰)
bindLabBackends(app)
}
const bindLabBackends = (app) => {
try {
if (!fs.existsSync(LABS_DIR)) return
const dirs = fs.readdirSync(LABS_DIR, { withFileTypes: true })
for (const d of dirs) {
if (!d.isDirectory()) continue
const entry = path.join(LABS_DIR, d.name, 'index.js')
if (!fs.existsSync(entry)) continue
try {
const mod = require(entry)
if (mod && typeof mod.bindRoutes === 'function') mod.bindRoutes(app)
} catch (e) {
try { console.error(`[thought_lab] 绑定 lab 后端失败: ${d.name} ${String(e.message || e)}`) } catch {}
}
}
} catch {}
}
module.exports = { bindRoutes, listLabs, loadLabConfig }