// ============================================================ // thought_lab/index.js - 思想实验室路由模块 // 职责: // - /tools/thought_lab/labs// 实验页面与静态资源(仅导航鉴权 nav_gate) // - GET /api/thought_lab/labs 返回已启用的 lab 列表(首页页签渲染用) // // 目录约定(参照 data_gateway 的 skills 分类思想): // - 每个 lab 是 labs/ 下一个完全独立的子文件夹,互不干扰、互不继承 // - 后端侧 labs// 存放该 lab 的 config.json; // 未来若该 lab 需要数据库、OSS、定时任务等后台逻辑,也落在该文件夹内 // - 前端页面对应 public/tools/thought_lab/labs//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//... 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//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 }