feat: 新增思想实验室模块与配套功能
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点 - 新增思想实验室后端路由与鉴权逻辑 - 首页新增思想实验室页签与前端渲染逻辑 - 修复中韩半导体监控标的配置错误 - 新增市场技能推送白名单过滤逻辑与单元测试
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// ============================================================
|
||||
// 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)
|
||||
}
|
||||
|
||||
module.exports = { bindRoutes, listLabs, loadLabConfig }
|
||||
Reference in New Issue
Block a user