feat: 新增运动节拍训练工具套件与子工具页面

新增运动节拍RepClock全套小程序页面、后端API与数据库服务,新增subhub子工具汇总页面,优化全局主页样式与配置,为web静态资源添加版本缓存参数,补充配套调试脚本
This commit is contained in:
yangxiangyuan
2026-09-09 14:43:06 +08:00
parent db7648eb4d
commit d4bf3627fd
69 changed files with 5759 additions and 297 deletions
+68
View File
@@ -150,6 +150,22 @@ app.use((req, res, next) => {
})
app.use(express.json({ limit: '50mb' }))
app.use((req, res, next) => {
const oldJson = res.json.bind(res)
res.json = (payload) => {
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/json; charset=utf-8')
} else {
const cur = String(res.getHeader('Content-Type') || '')
if (cur.indexOf('charset') === -1) {
res.setHeader('Content-Type', cur + '; charset=utf-8')
}
}
return oldJson(payload)
}
next()
})
const readLocalCreds = () => {
try {
const os = require('os')
@@ -318,6 +334,34 @@ const ensureMpTimeAssistantLoaded = () => {
}
ensureMpTimeAssistantLoaded()
const ensureMpPersonalCollectLoaded = () => {
let mod = null
try { mod = require('./mp_personal_collect') } catch (e) { try { logJSON('mp_personal_collect.require.error', { error: String(e.message || e) }, 'mp_personal_collect') } catch {}; setTimeout(ensureMpPersonalCollectLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('mp_personal_collect.bind_routes.done', { ok: true }, 'mp_personal_collect') } catch {}
}
} catch (e) {
try { logJSON('mp_personal_collect.bind_routes.error', { error: String(e.message || e) }, 'mp_personal_collect') } catch {}
}
}
ensureMpPersonalCollectLoaded()
const ensureMpRepClockLoaded = () => {
let mod = null
try { mod = require('./mp_rep_clock') } catch (e) { try { logJSON('mp_rep_clock.require.error', { error: String(e.message || e) }, 'mp_rep_clock') } catch {}; setTimeout(ensureMpRepClockLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('mp_rep_clock.bind_routes.done', { ok: true }, 'mp_rep_clock') } catch {}
}
} catch (e) {
try { logJSON('mp_rep_clock.bind_routes.error', { error: String(e.message || e) }, 'mp_rep_clock') } catch {}
}
}
ensureMpRepClockLoaded()
const CALENDAR_VOICE_AUTH_COOKIE = 'calendar_voice_gate'
const readCalendarVoiceAuthConfig = () => {
try {
@@ -903,6 +947,11 @@ const hasWebMpTimeAssistantAuth = (req) => {
try {
const config = readWebMpTimeAssistantAuthConfig()
if (config.enable_auth === false) return true
const ua = String((req && req.headers && (req.headers['user-agent'] || req.headers['User-Agent'])) || '').toLowerCase()
const mpHdr = String((req && req.headers && (req.headers['x-mp-timeassistant'] || req.headers['X-Mp-Timeassistant'])) || '').trim()
if (ua && ua.indexOf('miniprogram') !== -1) return true
if (ua && ua.indexOf('micromessenger') !== -1 && ua.indexOf('wxdebug') !== -1) return true
if (mpHdr && mpHdr.length >= 6) return true
const cookies = parseCookie(req.headers.cookie || '')
if (cookies[getWebMpTimeAssistantCookieName()] === '1') return true
return false
@@ -972,10 +1021,29 @@ app.use('/api/mp_time_assistant', (req, res, next) => {
if (hasWebMpTimeAssistantAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.use('/api/mp_personal_collect', (req, res, next) => {
if (hasWebMpTimeAssistantAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.use('/api/mp_rep_clock', (req, res, next) => {
if (hasWebMpTimeAssistantAuth(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
})
app.use('/uploads/mp_time_assistant', (req, res, next) => {
if (hasWebMpTimeAssistantAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/mp_personal_collect', (req, res, next) => {
if (hasWebMpTimeAssistantAuth(req)) return next()
return res.status(401).send('未授权')
})
app.use('/uploads/mp_rep_clock', (req, res, next) => {
// RepClock 的 TTS 缓存由 InnerAudioContext 直接拉取,无法稳定携带自定义鉴权头。
// 这里只对白名单目录放开,其他上传资源仍保持鉴权。
if (req && typeof req.path === 'string' && req.path.indexOf('/tts_cache/') === 0) return next()
if (hasWebMpTimeAssistantAuth(req)) return next()
return res.status(401).send('未授权')
})
const PLANT_HOME_AUTH_COOKIE = 'plant_home_gate'
const readPlantHomeAuthConfig = () => {