refactor: 重构禅音工具路径与配置,新增私人剪贴板技能
这是一次大型重构和功能新增: 1. 将原 zen_box 工具重命名为 web_zen_box,调整所有相关路径与配置 2. 新增 web_order_box 原生Web应用的完整资源与部署脚本 3. 新增私人剪贴板后端技能与配套测试脚本 4. 修复订单盒子APP的API地址配置,避免路径重复拼接 5. 新增本地开发跨域白名单支持localhost 6. 清理旧版zen_box的冗余文件
This commit is contained in:
@@ -10,6 +10,7 @@ const { insert, upsertLatest, queryLatest, queryList, queryByTimeRange, count }
|
||||
const { loadSkillConfig } = require('./auth')
|
||||
const { bindRoutes: bindExternalStorageRoutes } = require('./skills/external_storage')
|
||||
const { bindRoutes: bindEmailSenderRoutes } = require('./skills/email_sender')
|
||||
const { bindRoutes: bindPrivateClipboardRoutes } = require('./skills/private_clipboard')
|
||||
|
||||
const setNoCache = (res) => {
|
||||
try {
|
||||
@@ -94,6 +95,16 @@ const bindRoutes = (app) => {
|
||||
console.error(`[data_gateway] Failed to bind email_sender routes: ${e.message}`)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 私人剪贴板 skill(对外写入剪贴板)
|
||||
// 路由挂载在 /api/v1/ingest/private_clipboard/*
|
||||
// ============================================================
|
||||
try {
|
||||
bindPrivateClipboardRoutes(app)
|
||||
} catch (e) {
|
||||
console.error(`[data_gateway] Failed to bind private_clipboard routes: ${e.message}`)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 外部写入接口(公网暴露)
|
||||
// POST /api/v1/ingest/:skillId
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,214 @@
|
||||
// ============================================================
|
||||
// clipboard_bridge.js - 桥接模块,复用私人剪贴板的数据库和文件存储逻辑
|
||||
// 供 data_gateway 的 private_clipboard skill 调用
|
||||
// ============================================================
|
||||
const Database = require('better-sqlite3')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data')
|
||||
const UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'private_clipboard')
|
||||
const DB_PATH = process.env.PRIVATE_CLIPBOARD_DB_PATH || path.join(DATA_DIR, 'private_clipboard.db')
|
||||
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })
|
||||
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
||||
|
||||
let db = null
|
||||
|
||||
const ensureSchema = () => {
|
||||
if (!db) return
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS clipboard_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
text_content TEXT,
|
||||
file_name TEXT,
|
||||
file_mime TEXT,
|
||||
file_size INTEGER,
|
||||
file_path TEXT,
|
||||
group_id INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_clipboard_items_created_at ON clipboard_items(created_at);
|
||||
`)
|
||||
const cols = db.prepare('PRAGMA table_info(clipboard_items)').all()
|
||||
if (!cols.some(c => c.name === 'group_id')) db.exec('ALTER TABLE clipboard_items ADD COLUMN group_id INTEGER')
|
||||
}
|
||||
|
||||
const getDb = () => {
|
||||
if (db) {
|
||||
ensureSchema()
|
||||
return db
|
||||
}
|
||||
db = new Database(DB_PATH)
|
||||
db.pragma('journal_mode = WAL')
|
||||
db.pragma('synchronous = NORMAL')
|
||||
db.pragma('busy_timeout = 4000')
|
||||
ensureSchema()
|
||||
return db
|
||||
}
|
||||
|
||||
const normalizeName = (name) => {
|
||||
let s = String(name || '').replace(/\0/g, '')
|
||||
// 移除路径穿越模式
|
||||
s = s.replace(/\.\./g, '')
|
||||
const base = path.basename(s)
|
||||
return base || 'file'
|
||||
}
|
||||
|
||||
const extFromMime = (mime) => {
|
||||
const m = String(mime || '').toLowerCase()
|
||||
if (m === 'image/png') return '.png'
|
||||
if (m === 'image/jpeg' || m === 'image/jpg') return '.jpg'
|
||||
if (m === 'image/webp') return '.webp'
|
||||
if (m === 'image/gif') return '.gif'
|
||||
if (m === 'image/bmp') return '.bmp'
|
||||
if (m === 'image/svg+xml') return '.svg'
|
||||
return ''
|
||||
}
|
||||
|
||||
const saveFile = (rawBuffer, name, mime) => {
|
||||
const safe = normalizeName(name)
|
||||
let ext = path.extname(safe)
|
||||
if (!ext) ext = extFromMime(mime)
|
||||
const stamp = Date.now()
|
||||
const rand = crypto.randomBytes(6).toString('hex')
|
||||
const fileName = `${stamp}-${rand}${ext}`
|
||||
const rel = path.join('uploads', 'private_clipboard', fileName)
|
||||
const abs = path.join(process.cwd(), rel)
|
||||
fs.writeFileSync(abs, rawBuffer)
|
||||
const size = rawBuffer.length
|
||||
return { rel, fileName: safe, size }
|
||||
}
|
||||
|
||||
const formatItem = (row) => {
|
||||
const filePath = row.file_path ? String(row.file_path) : ''
|
||||
const url = filePath ? '/' + filePath.replace(/^\/+/, '') : ''
|
||||
return {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
text_content: row.text_content || '',
|
||||
file_name: row.file_name || '',
|
||||
file_mime: row.file_mime || '',
|
||||
file_size: row.file_size || 0,
|
||||
file_url: url,
|
||||
created_at: row.created_at
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 公开 API
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 插入一条文本记录
|
||||
*/
|
||||
const insertText = (text, groupId) => {
|
||||
const database = getDb()
|
||||
const now = Date.now()
|
||||
const info = database.prepare(
|
||||
'INSERT INTO clipboard_items (type, text_content, group_id, created_at) VALUES (?, ?, ?, ?)'
|
||||
).run('text', String(text || ''), groupId !== null && groupId !== undefined ? groupId : null, now)
|
||||
return { id: info.lastInsertRowid, type: 'text', created_at: now }
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入多个文件(支持一次上传多个)
|
||||
* 返回 { group_id, items: [...] }
|
||||
*/
|
||||
const insertFiles = (files, groupId) => {
|
||||
const database = getDb()
|
||||
const now = Date.now()
|
||||
// 如果没有提供 groupId,自动生成一个
|
||||
const actualGroupId = (groupId !== null && groupId !== undefined) ? groupId : -now
|
||||
|
||||
const tx = database.transaction(() => {
|
||||
const items = []
|
||||
for (const f of files) {
|
||||
const { rel, fileName, size } = saveFile(f.buffer, f.name, f.mime)
|
||||
const type = String(f.mime || '').toLowerCase().startsWith('image/') ? 'image' : 'file'
|
||||
const info = database.prepare(
|
||||
'INSERT INTO clipboard_items (type, file_name, file_mime, file_size, file_path, group_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(
|
||||
type, fileName, f.mime || '', size, rel, actualGroupId, now
|
||||
)
|
||||
items.push({
|
||||
id: info.lastInsertRowid,
|
||||
type,
|
||||
file_name: fileName,
|
||||
file_mime: f.mime || '',
|
||||
file_size: size,
|
||||
file_url: '/' + rel.replace(/^\/+/, ''),
|
||||
created_at: now
|
||||
})
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const items = tx()
|
||||
return { group_id: actualGroupId, items }
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最近 N 条记录(按 group 分组)
|
||||
*/
|
||||
const queryLatest = (limit = 50) => {
|
||||
const database = getDb()
|
||||
const safeLimit = Math.min(Math.max(1, Number(limit) || 50), 500)
|
||||
|
||||
const rows = database.prepare(`
|
||||
SELECT * FROM clipboard_items
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`).all(safeLimit)
|
||||
|
||||
// 按 group_id 分组
|
||||
const groups = new Map()
|
||||
for (const row of rows) {
|
||||
const gid = row.group_id
|
||||
const key = gid !== null && gid !== undefined ? `g:${gid}` : `r:${row.id}`
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
id: gid || row.id,
|
||||
type: row.type,
|
||||
text_content: row.text_content || '',
|
||||
created_at: row.created_at,
|
||||
items: [],
|
||||
group_types: []
|
||||
})
|
||||
}
|
||||
const group = groups.get(key)
|
||||
group.group_types.push(row.type)
|
||||
if (row.type === 'text') {
|
||||
if (!group.text_content) group.text_content = row.text_content || ''
|
||||
} else {
|
||||
group.items.push(formatItem(row))
|
||||
}
|
||||
}
|
||||
|
||||
const result = Array.from(groups.values()).map(group => {
|
||||
const unique = Array.from(new Set(group.group_types.filter(Boolean)))
|
||||
let type = group.type
|
||||
if (group.items.length) {
|
||||
type = unique.length === 1 ? unique[0] : 'mixed'
|
||||
} else {
|
||||
type = 'text'
|
||||
}
|
||||
return {
|
||||
id: group.id,
|
||||
type,
|
||||
text_content: group.text_content,
|
||||
created_at: group.created_at,
|
||||
items: group.items,
|
||||
group_types: unique
|
||||
}
|
||||
})
|
||||
|
||||
// 按 created_at 降序
|
||||
result.sort((a, b) => b.created_at - a.created_at)
|
||||
|
||||
return { items: result, total: result.length }
|
||||
}
|
||||
|
||||
module.exports = { getDb, insertText, insertFiles, queryLatest }
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"_comment": "私人剪贴板 skill - 供第三方程序向剪贴板写入数据",
|
||||
"id": "private_clipboard",
|
||||
"name": "私人剪贴板",
|
||||
"enabled": true,
|
||||
"api_key_comment": "API Key(明文),用于写入剪贴板数据的鉴权",
|
||||
"api_key": "pcb_nuS0qf-W63qcvAPsrG5ePA",
|
||||
"api_key_hash_comment": "API Key 的 SHA256 哈希,用于服务端校验",
|
||||
"api_key_hash": "sha256:d28127ad7b0b84563891fbc2621f0623915055e12c7097cd31191014ef2b1121",
|
||||
"rate_limit_comment": "每分钟最多 60 次请求",
|
||||
"rate_limit_per_minute": 60,
|
||||
"max_files_per_request_comment": "单次上传最多文件数",
|
||||
"max_files_per_request": 10,
|
||||
"max_file_size_comment": "单文件最大 200MB(与现有剪贴板逻辑一致)",
|
||||
"max_file_size": 209715200,
|
||||
"latest_limit_comment": "latest 接口默认返回最近 50 条",
|
||||
"latest_limit": 50,
|
||||
"db_path_comment": "剪贴板数据库路径(读取环境变量 PRIVATE_CLIPBOARD_DB_PATH,默认 data/private_clipboard.db)",
|
||||
"upload_dir_comment": "剪贴板上传目录(复用现有 uploads/private_clipboard/)"
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// ============================================================
|
||||
// private_clipboard/index.js - 私人剪贴板对外写入 API
|
||||
// 路由挂载在 /api/v1/ingest/private_clipboard/*
|
||||
// ============================================================
|
||||
const express = require('express')
|
||||
const multer = require('multer')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const { verifyApiKey } = require('../../auth')
|
||||
const { logJSON } = require('../../../logger')
|
||||
const { insertText, insertFiles, queryLatest } = require('./clipboard_bridge')
|
||||
|
||||
const SKILL_ID = 'private_clipboard'
|
||||
const RATE_LIMIT_MAX = 60
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000
|
||||
const MAX_FILES = 10
|
||||
const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
|
||||
const MAX_TEXT_LENGTH = 50000
|
||||
|
||||
// 临时目录
|
||||
const TEMP_DIR = path.join(os.tmpdir(), 'private_clipboard_ingest')
|
||||
if (!fs.existsSync(TEMP_DIR)) {
|
||||
try { fs.mkdirSync(TEMP_DIR, { recursive: true }) } catch {}
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
dest: TEMP_DIR,
|
||||
limits: { fileSize: MAX_FILE_SIZE, files: MAX_FILES }
|
||||
})
|
||||
|
||||
// 速率限制
|
||||
const rateLimitMap = new Map()
|
||||
const checkRate = (ip) => {
|
||||
const rec = rateLimitMap.get(ip)
|
||||
if (!rec || Date.now() - rec.start > RATE_LIMIT_WINDOW_MS) {
|
||||
rateLimitMap.set(ip, { start: Date.now(), count: 1 })
|
||||
return { ok: true }
|
||||
}
|
||||
rec.count++
|
||||
if (rec.count > RATE_LIMIT_MAX) {
|
||||
const waitSec = Math.ceil((rec.start + RATE_LIMIT_WINDOW_MS - Date.now()) / 1000)
|
||||
return { ok: false, retryAfter: Math.max(1, waitSec) }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
for (const [ip, rec] of rateLimitMap.entries()) {
|
||||
if (now - rec.start > RATE_LIMIT_WINDOW_MS + 10000) rateLimitMap.delete(ip)
|
||||
}
|
||||
}, 60000)
|
||||
|
||||
// 安全文件名清理
|
||||
const safeName = (name) => {
|
||||
let s = String(name || 'file')
|
||||
try {
|
||||
const recovered = Buffer.from(s, 'latin1').toString('utf8')
|
||||
if (recovered !== s && !/\ufffd/.test(recovered)) s = recovered
|
||||
} catch {}
|
||||
// 移除路径穿越模式
|
||||
s = s.replace(/\.\./g, '')
|
||||
return s.replace(/[<>\":/\\|?*\x00-\x1F]/g, '_').trim() || 'file'
|
||||
}
|
||||
|
||||
// HTML 转义(防御存储型 XSS)
|
||||
const escapeHtml = (str) => {
|
||||
return String(str || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const bindRoutes = (app) => {
|
||||
const router = express.Router()
|
||||
|
||||
// API Key 鉴权中间件
|
||||
router.use((req, res, next) => {
|
||||
const auth = verifyApiKey(req)
|
||||
if (!auth.ok) {
|
||||
try { logJSON('private_clipboard.auth.fail', { error: auth.error, ip: req.ip }, 'private_clipboard') } catch {}
|
||||
return res.status(401).json({ ok: false, error: auth.error })
|
||||
}
|
||||
const apiId = String(req.headers['x-api-id'] || '').trim()
|
||||
if (apiId !== SKILL_ID) {
|
||||
return res.status(403).json({ ok: false, error: 'skill id mismatch' })
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// 无缓存
|
||||
router.use((req, res, next) => {
|
||||
setNoCache(res)
|
||||
next()
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 写入文本
|
||||
// POST /api/v1/ingest/private_clipboard/text
|
||||
// Body: { "text": "...", "group_id": -123 }(group_id 可选)
|
||||
// ============================================================
|
||||
router.post('/text', express.json({
|
||||
type: 'application/json',
|
||||
limit: '1mb',
|
||||
verify: (req, res, buf) => {
|
||||
// 强制按 UTF-8 解码原始字节,绕过全局 express.json() 的可能编码错误
|
||||
req._rawBody = buf.toString('utf8')
|
||||
}
|
||||
}), (req, res) => {
|
||||
try {
|
||||
// 速率限制
|
||||
const clientIp = req.ip || req.connection.remoteAddress || 'unknown'
|
||||
const rateCheck = checkRate(clientIp)
|
||||
if (!rateCheck.ok) {
|
||||
return res.status(429).json({ ok: false, error: '请求过于频繁,请在 ' + rateCheck.retryAfter + ' 秒后重试' })
|
||||
}
|
||||
|
||||
// 优先使用 UTF-8 原始 buffer 解析,绕过全局 express.json() 的可能编码问题
|
||||
let body = {}
|
||||
let text = ''
|
||||
if (req._rawBody) {
|
||||
try {
|
||||
body = JSON.parse(req._rawBody)
|
||||
text = String(body.text || '').trim()
|
||||
} catch { body = req.body || {} }
|
||||
}
|
||||
// 如果没有 rawBody 或解析失败,回退到 req.body
|
||||
if (!text) {
|
||||
body = req.body || {}
|
||||
text = String(body.text || '').trim()
|
||||
}
|
||||
|
||||
if (!text) return res.status(400).json({ ok: false, error: '文本内容不能为空' })
|
||||
if (text.length > MAX_TEXT_LENGTH) {
|
||||
return res.status(400).json({ ok: false, error: `文本超过 ${MAX_TEXT_LENGTH} 字符限制` })
|
||||
}
|
||||
|
||||
// HTML 转义后存库,防御存储型 XSS
|
||||
const safeText = escapeHtml(text)
|
||||
const groupId = body.group_id !== undefined ? Number(body.group_id) : null
|
||||
const result = insertText(safeText, groupId)
|
||||
|
||||
try { logJSON('private_clipboard.text.ok', { id: result.id, groupId }, 'private_clipboard') } catch {}
|
||||
res.json({ ok: true, id: result.id, type: result.type, created_at: result.created_at })
|
||||
} catch (e) {
|
||||
try { logJSON('private_clipboard.text.error', { error: String(e.message || e) }, 'private_clipboard') } catch {}
|
||||
res.status(500).json({ ok: false, error: '操作失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 上传文件/图片(支持多文件)
|
||||
// POST /api/v1/ingest/private_clipboard/upload
|
||||
// multipart/form-data, field: "files", 可选 field: "group_id"
|
||||
// ============================================================
|
||||
router.post('/upload', upload.array('files', MAX_FILES), async (req, res) => {
|
||||
try {
|
||||
// 速率限制
|
||||
const clientIp = req.ip || req.connection.remoteAddress || 'unknown'
|
||||
const rateCheck = checkRate(clientIp)
|
||||
if (!rateCheck.ok) {
|
||||
return res.status(429).json({ ok: false, error: '请求过于频繁,请在 ' + rateCheck.retryAfter + ' 秒后重试' })
|
||||
}
|
||||
|
||||
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
|
||||
return res.status(400).json({ ok: false, error: '未收到文件' })
|
||||
}
|
||||
if (req.files.length > MAX_FILES) {
|
||||
return res.status(400).json({ ok: false, error: `文件数量不能超过 ${MAX_FILES} 个` })
|
||||
}
|
||||
|
||||
const groupId = req.body.group_id !== undefined ? Number(req.body.group_id) : null
|
||||
|
||||
const files = req.files.map(f => ({
|
||||
buffer: fs.readFileSync(f.path),
|
||||
name: safeName(f.originalname || 'file'),
|
||||
mime: f.mimetype || 'application/octet-stream'
|
||||
}))
|
||||
|
||||
const result = insertFiles(files, groupId)
|
||||
|
||||
// 清理临时文件
|
||||
for (const f of req.files) {
|
||||
if (f.path) try { fs.unlinkSync(f.path) } catch {}
|
||||
}
|
||||
|
||||
try { logJSON('private_clipboard.upload.ok', { count: result.items.length, groupId: result.group_id }, 'private_clipboard') } catch {}
|
||||
res.json({ ok: true, group_id: result.group_id, items: result.items })
|
||||
} catch (e) {
|
||||
// 清理临时文件
|
||||
if (req.files && Array.isArray(req.files)) {
|
||||
for (const f of req.files) {
|
||||
if (f.path) try { fs.unlinkSync(f.path) } catch {}
|
||||
}
|
||||
}
|
||||
try { logJSON('private_clipboard.upload.error', { error: String(e.message || e) }, 'private_clipboard') } catch {}
|
||||
res.status(500).json({ ok: false, error: '上传失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 查询最近 N 条
|
||||
// GET /api/v1/ingest/private_clipboard/latest?limit=50
|
||||
// ============================================================
|
||||
router.get('/latest', (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit, 10) || 50
|
||||
const result = queryLatest(limit)
|
||||
res.json({ ok: true, ...result })
|
||||
} catch (e) {
|
||||
try { logJSON('private_clipboard.latest.error', { error: String(e.message || e) }, 'private_clipboard') } catch {}
|
||||
res.status(500).json({ ok: false, error: '查询失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// 挂载路由
|
||||
app.use('/api/v1/ingest/private_clipboard', router)
|
||||
console.log('[private_clipboard] Routes mounted at /api/v1/ingest/private_clipboard')
|
||||
}
|
||||
|
||||
module.exports = { bindRoutes }
|
||||
+25
-2
@@ -100,7 +100,9 @@ const allowedOrigins = [
|
||||
'https://traesite.umersoft.com',
|
||||
'http://traesite.umersoft.com',
|
||||
'https://traesite.umersoft.com:8975',
|
||||
'http://traesite.umersoft.com:8975'
|
||||
'http://traesite.umersoft.com:8975',
|
||||
'http://localhost:8081',
|
||||
'http://127.0.0.1:8081'
|
||||
]
|
||||
app.use(cors({
|
||||
origin: function (origin, callback) {
|
||||
@@ -5906,9 +5908,30 @@ app.get('/api/box_backup/download/lowfreq_decrypted', async (req, res) => {
|
||||
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
|
||||
})
|
||||
|
||||
// web_order_box routes - 原生 HTML/CSS/JS Web 应用
|
||||
app.get('/tools/web_order_box', (req, res) => {
|
||||
res.sendFile(path.join(process.cwd(), 'public', 'tools', 'web_order_box', 'index.html'))
|
||||
})
|
||||
app.use('/tools/web_order_box', (req, res, next) => {
|
||||
try {
|
||||
const authConfigPath = path.join(process.cwd(), 'public', 'tools', 'web_order_box', 'auth_config.json')
|
||||
if (fs.existsSync(authConfigPath)) {
|
||||
const authConfig = JSON.parse(fs.readFileSync(authConfigPath, 'utf-8'))
|
||||
if (authConfig.enable_auth === false) return next()
|
||||
}
|
||||
if (hasNavAuthRoot(req)) return next()
|
||||
return res.status(401).send('未授权')
|
||||
} catch { return res.status(401).send('未授权') }
|
||||
})
|
||||
app.use('/tools/web_order_box', (req, res, next) => {
|
||||
res.set('X-Frame-Options', 'SAMEORIGIN')
|
||||
next()
|
||||
})
|
||||
app.use('/tools/web_order_box', express.static(path.join(process.cwd(), 'public', 'tools', 'web_order_box')))
|
||||
|
||||
app.get('/api/zen_box/playlists', (req, res) => {
|
||||
try {
|
||||
const zenDir = path.join(process.cwd(), 'public', 'tools', 'zen_box')
|
||||
const zenDir = path.join(process.cwd(), 'public', 'tools', 'web_zen_box')
|
||||
const files = fs.existsSync(zenDir) ? fs.readdirSync(zenDir, { withFileTypes: true }) : []
|
||||
const playlists = files
|
||||
.filter(entry => entry && entry.isFile && entry.isFile())
|
||||
|
||||
Reference in New Issue
Block a user