fix: data-action buttons, sources enabled, email notify, ticket detail single-page

- app.js: mount page objects via inline script (CSP blocks eval → data-action buttons dead)
- app.js: add tickets list entry to sidebar nav (admin could not find claimed tickets)
- sources: GET /sources?manage=1 returns enabled field for admin view
- mailer: sendNotifyEmail consumes email-type notification_configs (webhook_url = recipients)
- tickets: trigger notify on created/claimed/transferred/updated/status-change
- notifications: email type shows recipient field, validates email list
- ticket-detail: single-page chat flow (player/staff bubbles) + processing log timeline
- css: chat bubble styles
This commit is contained in:
2026-08-22 20:00:12 +08:00
parent e5a1d693f1
commit c5ceba7cf0
9 changed files with 120 additions and 37 deletions

View File

@@ -33,11 +33,19 @@ async function validateNotify(body) {
if (name !== undefined && (!name || String(name).length > 100)) return '名称不能为空且不超过100字符';
if (type !== undefined && !NOTIFY_TYPES.includes(type)) return '无效的通知类型';
if (webhook_url !== undefined && webhook_url !== '') {
let u;
try { u = new URL(webhook_url); } catch { return 'Webhook地址格式无效'; }
if (u.protocol !== 'https:' && u.protocol !== 'http:') return 'Webhook地址协议不支持';
if (await isPrivateUrl(webhook_url)) return 'Webhook地址不允许指向内网';
if (type === 'email') {
// email 类型: webhook_url 复用为收件人邮箱(逗号分隔)
const emails = String(webhook_url).split(',').map(s => s.trim()).filter(Boolean);
if (!emails.length) return '收件人邮箱不能为空';
if (!emails.every(e => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))) return '收件人邮箱格式无效';
} else {
let u;
try { u = new URL(webhook_url); } catch { return 'Webhook地址格式无效'; }
if (u.protocol !== 'https:' && u.protocol !== 'http:') return 'Webhook地址协议不支持';
if (await isPrivateUrl(webhook_url)) return 'Webhook地址不允许指向内网';
}
}
if (type === 'email' && !webhook_url) return '邮件通知需填写收件人邮箱';
if (events !== undefined) {
const list = String(events).split(',').map(s => s.trim());
if (!list.every(e => NOTIFY_EVENTS.includes(e))) return '无效的触发事件';