feat: 新增思想实验室模块与配套功能

- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点
- 新增思想实验室后端路由与鉴权逻辑
- 首页新增思想实验室页签与前端渲染逻辑
- 修复中韩半导体监控标的配置错误
- 新增市场技能推送白名单过滤逻辑与单元测试
This commit is contained in:
yangxiangyuan
2026-08-06 13:39:48 +08:00
parent 6a7a2b089a
commit ea379116cc
19 changed files with 2831 additions and 17 deletions
+149
View File
@@ -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 }