feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能

- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API
- 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据
- 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件
- 补充项目配置、站点地图、调试诊断脚本等配套文件
- 完善各模块的页面配置与样式文件
This commit is contained in:
yangxiangyuan
2026-09-02 22:20:37 +08:00
parent 132d132473
commit db7648eb4d
75 changed files with 8618 additions and 70 deletions
+160 -1
View File
@@ -304,6 +304,20 @@ const ensureAppOrderBoxLoaded = () => {
}
ensureAppOrderBoxLoaded()
const ensureMpTimeAssistantLoaded = () => {
let mod = null
try { mod = require('./mp_time_assistant') } catch (e) { try { logJSON('mp_time_assistant.require.error', { error: String(e.message || e) }, 'mp_time_assistant') } catch {}; setTimeout(ensureMpTimeAssistantLoaded, 2000); return }
try {
if (mod && mod.bindRoutes) {
mod.bindRoutes(app)
try { logJSON('mp_time_assistant.bind_routes.done', { ok: true }, 'mp_time_assistant') } catch {}
}
} catch (e) {
try { logJSON('mp_time_assistant.bind_routes.error', { error: String(e.message || e) }, 'mp_time_assistant') } catch {}
}
}
ensureMpTimeAssistantLoaded()
const CALENDAR_VOICE_AUTH_COOKIE = 'calendar_voice_gate'
const readCalendarVoiceAuthConfig = () => {
try {
@@ -819,6 +833,150 @@ const ensureZhiziNotesLoaded = () => {
}
ensureZhiziNotesLoaded()
const WEB_MP_TIME_ASSISTANT_AUTH_COOKIE = 'web_mp_time_assistant_gate'
const readWebMpTimeAssistantAuthConfig = () => {
try {
const p = path.join(process.cwd(), 'public', 'tools', 'web_mp_time_assistant', 'auth_config.json')
if (fs.existsSync(p)) {
const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (cfg && typeof cfg === 'object') return cfg
}
} catch {}
return {}
}
const readWebMpTimeAssistantJwk = () => {
try {
const p = path.join(process.cwd(), 'config', 'web_mp_time_assistant.jwk.json')
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
} catch {}
return { kty: 'RSA', n: '', e: '' }
}
const getWebMpTimeAssistantCookieName = () => {
try {
const cfg = readWebMpTimeAssistantAuthConfig()
const n = String(cfg.cookieName || '')
if (n) return n
} catch {}
return WEB_MP_TIME_ASSISTANT_AUTH_COOKIE
}
const getWebMpTimeAssistantMaxAge = () => {
try {
const cfg = readWebMpTimeAssistantAuthConfig()
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
} catch {}
return 30 * 24 * 3600 * 1000
}
const getWebMpTimeAssistantGuidConfig = () => {
try {
const cfg = readWebMpTimeAssistantAuthConfig()
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
return { key: keyRaw || 'guid', value: valueRaw }
} catch {
return { key: 'guid', value: '' }
}
}
const hasValidWebMpTimeAssistantGuid = (req) => {
try {
const cfg = readWebMpTimeAssistantAuthConfig()
if (cfg.enable_auth === false) return false
const { key, value } = getWebMpTimeAssistantGuidConfig()
if (!value) return false
const q = req && req.query ? req.query : {}
const raw = q[key]
if (raw === undefined || raw === null) return false
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
return incoming === value
} catch { return false }
}
const isWebMpTimeAssistantForceGuid = () => {
try {
const cfg = readWebMpTimeAssistantAuthConfig()
if (cfg.enable_auth === false) return false
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
return false
} catch { return false }
}
const hasWebMpTimeAssistantAuth = (req) => {
try {
const config = readWebMpTimeAssistantAuthConfig()
if (config.enable_auth === false) return true
const cookies = parseCookie(req.headers.cookie || '')
if (cookies[getWebMpTimeAssistantCookieName()] === '1') return true
return false
} catch { return false }
}
app.get('/tools/web_mp_time_assistant', (req, res, next) => {
setNoCache(res)
try {
const forceGuid = isWebMpTimeAssistantForceGuid()
if (hasWebMpTimeAssistantAuth(req)) return next()
const guidKey = getWebMpTimeAssistantGuidConfig().key
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (hasValidWebMpTimeAssistantGuid(req)) {
const maxAge = getWebMpTimeAssistantMaxAge()
const cookieName = getWebMpTimeAssistantCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/web_mp_time_assistant/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
const token = String(req.query.token || '')
if (!token) {
if (forceGuid) return res.status(401).send('未授权')
return next()
}
const payload = verifyJwtWithKeys(token, jwkKeys(readWebMpTimeAssistantJwk()))
if (!payload) return res.status(401).send('未授权')
{
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-web_mp_time_assistant')) return res.status(401).send('未授权')
}
const maxAge = getWebMpTimeAssistantMaxAge()
const cookieName = getWebMpTimeAssistantCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/web_mp_time_assistant/index.html')
} catch (e) { return res.status(401).send('未授权') }
})
app.use('/tools/web_mp_time_assistant', (req, res, next) => {
const forceGuid = isWebMpTimeAssistantForceGuid()
if (hasWebMpTimeAssistantAuth(req)) return next()
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html' || req.path === '/hub.html')
const guidKey = getWebMpTimeAssistantGuidConfig().key
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
if (isHandshake && hasValidWebMpTimeAssistantGuid(req)) {
const maxAge = getWebMpTimeAssistantMaxAge()
const cookieName = getWebMpTimeAssistantCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/web_mp_time_assistant/index.html')
}
if (hasGuidInput) return res.status(401).send('未授权')
if (isHandshake && String(req.query.token || '')) {
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readWebMpTimeAssistantJwk()))
if (payload) {
const iss = String(payload.iss || '')
const navIss = getNavIssFromFlags()
if (iss === navIss && !audMatch(payload, 'Tools-web_mp_time_assistant')) return res.status(401).send('未授权')
const maxAge = getWebMpTimeAssistantMaxAge()
const cookieName = getWebMpTimeAssistantCookieName()
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
return res.redirect('/tools/web_mp_time_assistant/index.html')
}
}
if (isHandshake && forceGuid) return res.status(401).send('未授权')
return res.status(401).send('未授权')
})
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('/uploads/mp_time_assistant', (req, res, next) => {
if (hasWebMpTimeAssistantAuth(req)) return next()
return res.status(401).send('未授权')
})
const PLANT_HOME_AUTH_COOKIE = 'plant_home_gate'
const readPlantHomeAuthConfig = () => {
try {
@@ -1643,7 +1801,8 @@ const mapSystemIdToUrl = (systemId) => {
'Tools-short_link': '/tools/short_link',
'Tools-web_mobile_calendar_reminder': '/tools/web_mobile_calendar_reminder',
'Tools-greedy_snake': '/tools/greedy_snake',
'Tools-oss_browser_link': '/tools/oss_browser_link'
'Tools-oss_browser_link': '/tools/oss_browser_link',
'Tools-web_mp_time_assistant': '/tools/web_mp_time_assistant'
}
if (m[systemId]) return m[systemId]
if (systemId.startsWith('Tools-')) {