Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab52ca2e69 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudsearch-backend",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/main.ts",
|
||||
|
||||
@@ -274,6 +274,8 @@ export async function checkAndRunScheduledCleanup(): Promise<void> {
|
||||
if (stats.trashEmptied) lines.push('已清空回收站');
|
||||
if (stats.errors.length > 0) lines.push(`⚠️ ${stats.errors.length} 个错误(${stats.errors.slice(0, 3).join('; ')}${stats.errors.length > 3 ? `...` : ''})`);
|
||||
if (lines.length > 0) {
|
||||
notifyEvent('cleanup', `🧹 清理完成`, lines.join('\n'), stats.errors.length > 0 ? 'warn' : 'info');
|
||||
notifyEvent('cleanup', `🧹 清理完成`, lines.join('\n'), stats.errors.length > 0 ? 'warn' : 'info', {
|
||||
stats_lines: lines.join('\n'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
|
||||
if (existing?.share_url) {
|
||||
const { LinkValidator } = await import('../validation/link-validator.service');
|
||||
const validator = new LinkValidator();
|
||||
const validation = await validator.validateWithLocalFallback(existing.share_url, 'quark');
|
||||
const validation = await validator.validate(existing.share_url, 'quark');
|
||||
if (validation.status === 'valid') {
|
||||
const isFirstReuse = dedupCutoff ? !db.prepare(
|
||||
`SELECT 1 FROM save_records WHERE source_url = ? AND created_at >= ? AND status = 'reused' LIMIT 1`
|
||||
|
||||
@@ -17,7 +17,6 @@ export function getAdKeywords(): string[] {
|
||||
const raw = getSystemConfig("quark_ad_keywords") || "";
|
||||
return raw
|
||||
.split("\n")
|
||||
.flatMap((line) => line.split(","))
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -27,7 +26,6 @@ export function getWarningFolderNames(): string[] {
|
||||
const raw = getSystemConfig("quark_warning_folder_names") || "";
|
||||
return raw
|
||||
.split("\n")
|
||||
.flatMap((line) => line.split(","))
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -68,8 +66,7 @@ export async function deleteAdFiles(
|
||||
dirFid: string,
|
||||
keywords: string[],
|
||||
): Promise<number> {
|
||||
const extensions = getSusExtensions();
|
||||
if (!keywords.length && !extensions.length) return 0;
|
||||
if (!keywords.length) return 0;
|
||||
|
||||
let deletedCount = 0;
|
||||
const stack: string[] = [dirFid];
|
||||
@@ -128,7 +125,6 @@ async function batchDeleteFiles(
|
||||
cookie: string,
|
||||
fids: string[],
|
||||
): Promise<boolean> {
|
||||
if (!fids.length) return true;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`https://drive-pc.quark.cn/1/clouddrive/file/trash?${makeQuery()}`,
|
||||
@@ -139,17 +135,13 @@ async function batchDeleteFiles(
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action_type: 1,
|
||||
filelist: fids,
|
||||
exclude_filelist: [],
|
||||
action_type: 2, // 2 = 移入回收站
|
||||
file_list: fids.map((fid) => ({ fid })),
|
||||
exclude_fids: [],
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
},
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.log(`[Quark-AdCleanup] batchDelete HTTP ${resp.status}`);
|
||||
return false;
|
||||
}
|
||||
const data = (await resp.json()) as any;
|
||||
if (data.status === 200) {
|
||||
return true;
|
||||
@@ -164,7 +156,6 @@ async function batchDeleteFiles(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==================== 警示文件夹创建 ====================
|
||||
|
||||
/**
|
||||
@@ -264,23 +255,22 @@ export async function runAdCleanup(
|
||||
savedDirFid: string,
|
||||
): Promise<{ adDeleted: number; warningDirs: number }> {
|
||||
const keywords = getAdKeywords();
|
||||
const susExtensions = getSusExtensions();
|
||||
const warningNames = getWarningFolderNames();
|
||||
|
||||
let adDeleted = 0;
|
||||
let warningDirs = 0;
|
||||
|
||||
// 1. 广告关键词 + 可疑后缀清理
|
||||
if (keywords.length > 0 || susExtensions.length > 0) {
|
||||
// 1. 广告关键词清理
|
||||
if (keywords.length > 0) {
|
||||
console.log(
|
||||
`[Quark-AdCleanup] 开始文件清理: ${keywords.length} 个关键词, ${susExtensions.length} 个可疑后缀`,
|
||||
`[Quark-AdCleanup] 开始广告关键词清理: ${keywords.length} 个关键词`,
|
||||
);
|
||||
adDeleted = await deleteAdFiles(cookie, savedDirFid, keywords);
|
||||
console.log(
|
||||
`[Quark-AdCleanup] 清理完成,共删除 ${adDeleted} 个文件/文件夹`,
|
||||
`[Quark-AdCleanup] 广告清理完成,共删除 ${adDeleted} 个文件/文件夹`,
|
||||
);
|
||||
} else {
|
||||
console.log("[Quark-AdCleanup] 无关键词/可疑后缀配置,跳过清理");
|
||||
console.log("[Quark-AdCleanup] 无广告关键词配置,跳过清理");
|
||||
}
|
||||
|
||||
// 2. 创建警示文件夹
|
||||
|
||||
@@ -198,13 +198,13 @@ export async function trashFiles(cookie: string, fids: string[]): Promise<boolea
|
||||
if (!fids.length) return true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${BASE_URL}/1/clouddrive/file/delete?${makeQuery()}`,
|
||||
`${BASE_URL}/1/clouddrive/file/trash?${makeQuery()}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { ...getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action_type: 1, // 1 = move to trash
|
||||
filelist: fids.map(fid => ({ fid })),
|
||||
filelist: fids,
|
||||
exclude_filelist: [],
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
|
||||
@@ -111,9 +111,21 @@ export function notifyEvent(
|
||||
eventName: string,
|
||||
title: string,
|
||||
content: string,
|
||||
level: 'info' | 'warn' | 'error' = 'info'
|
||||
level: 'info' | 'warn' | 'error' = 'info',
|
||||
templateVars?: Record<string, string>
|
||||
): void {
|
||||
if (!checkEventEnabled(eventName)) return;
|
||||
// Apply global template if available
|
||||
const eventKey = 'on_' + eventName;
|
||||
const templates = getEventTemplates();
|
||||
const tmpl = templates[eventKey];
|
||||
if (tmpl && tmpl.content) {
|
||||
const vars: Record<string, string> = { ...(templateVars || {}) };
|
||||
if (!vars.content && content) vars.content = content;
|
||||
if (!vars.title && title) vars.title = title;
|
||||
title = applyTemplate(tmpl.title || title, vars);
|
||||
content = applyTemplate(tmpl.content, vars);
|
||||
}
|
||||
notify(title, content, level);
|
||||
}
|
||||
|
||||
@@ -132,13 +144,40 @@ function getConfigNotifySettings(configId: number): PerConfigNotify {
|
||||
return {};
|
||||
}
|
||||
|
||||
function applyTemplate(template: string, vars: Record<string, string>): string {
|
||||
return template.replace(/\{([^}]+)\}/g, (_, key) => vars[key] || '{' + key + '}');
|
||||
}
|
||||
|
||||
function getEventTemplates(): Record<string, { title: string; content: string }> {
|
||||
try {
|
||||
const raw = getSystemConfig('global_notify_config') || '{}';
|
||||
const cfg = JSON.parse(raw);
|
||||
return cfg.eventTemplates || {};
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
export function notifyConfigEvent(
|
||||
configId: number,
|
||||
eventName: string,
|
||||
title: string,
|
||||
content: string,
|
||||
level: 'info' | 'warn' | 'error' = 'info'
|
||||
level: 'info' | 'warn' | 'error' = 'info',
|
||||
templateVars?: Record<string, string>
|
||||
): void {
|
||||
// Apply global template if available
|
||||
const eventKey = 'on_' + eventName;
|
||||
const templates = getEventTemplates();
|
||||
const tmpl = templates[eventKey];
|
||||
if (tmpl && tmpl.content) {
|
||||
const vars: Record<string, string> = { ...(templateVars || {}) };
|
||||
if (!vars.content && content) vars.content = content;
|
||||
if (!vars.title && title) vars.title = title;
|
||||
const appliedTitle = applyTemplate(tmpl.title || title, vars);
|
||||
const appliedContent = applyTemplate(tmpl.content, vars);
|
||||
title = appliedTitle;
|
||||
content = appliedContent;
|
||||
}
|
||||
|
||||
// Find matching push user by cloud_configs.promotion_account
|
||||
const pushUser = findPushUserForConfig(configId);
|
||||
if (!pushUser) {
|
||||
@@ -151,7 +190,6 @@ export function notifyConfigEvent(
|
||||
try { notifyConfig = JSON.parse(pushUser.notify_config); } catch {}
|
||||
|
||||
// Check event switch
|
||||
const eventKey = 'on_' + eventName;
|
||||
if (notifyConfig.events && notifyConfig.events[eventKey] === false) return;
|
||||
|
||||
// Build channels from push user config
|
||||
@@ -173,11 +211,15 @@ export function notifyConfigEvent(
|
||||
/** 测试某个通道 */
|
||||
export async function testChannel(
|
||||
channelName: string,
|
||||
account?: string
|
||||
account?: string,
|
||||
directParams?: Record<string, string>
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
let params: Record<string, string> = {};
|
||||
|
||||
if (account) {
|
||||
// If direct params provided, use them directly (for global panel test without saving first)
|
||||
if (directParams) {
|
||||
params = directParams;
|
||||
} else if (account) {
|
||||
const pushUser = findPushUserForConfig(undefined);
|
||||
// Use pushUser lookup by account instead
|
||||
const { getPushUserByAccount } = require('./push-user.service');
|
||||
|
||||
@@ -660,9 +660,9 @@ router.put('/admin/cloud-configs/:id/notify', (req: Request, res: Response) => {
|
||||
/** POST /api/admin/notify/test — test a notification channel */
|
||||
router.post('/admin/notify/test', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { channelType, account, configId } = req.body;
|
||||
const { channelType, account, configId, params } = req.body;
|
||||
const ctx = account || (configId ? String(configId) : undefined);
|
||||
const result = await testChannel(channelType as string, ctx);
|
||||
const result = await testChannel(channelType as string, ctx, params);
|
||||
res.json(result);
|
||||
} catch (err: any) {
|
||||
res.json({ success: false, message: err.message || '测试发送失败' });
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "0.2.6";
|
||||
export const VERSION = "0.2.2";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudsearch-frontend",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -346,9 +346,10 @@ export async function getAllNotifierProviders(): Promise<Record<string, { name:
|
||||
|
||||
export async function testNotifyChannel(
|
||||
channelType: string,
|
||||
configId?: number
|
||||
configId?: number,
|
||||
params?: Record<string, string>
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const { data } = await api.post('/admin/notify/test', { channelType, configId })
|
||||
const { data } = await api.post('/admin/notify/test', { channelType, configId, params })
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -524,9 +524,15 @@
|
||||
<el-divider content-position="left">全局事件开关</el-divider>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:16px;">
|
||||
<el-switch v-model="globalNotifyForm.events.on_save_success" active-text="转存成功" />
|
||||
<el-button v-if="globalNotifyForm.events.on_save_success" text type="primary" size="small" @click="openEventTemplate('on_save_success')">模板</el-button>
|
||||
<el-switch v-model="globalNotifyForm.events.on_save_fail" active-text="转存失败" />
|
||||
<el-button v-if="globalNotifyForm.events.on_save_fail" text type="primary" size="small" @click="openEventTemplate('on_save_fail')">模板</el-button>
|
||||
<el-switch v-model="globalNotifyForm.events.on_cookie_expire" active-text="Cookie过期" />
|
||||
<el-button v-if="globalNotifyForm.events.on_cookie_expire" text type="primary" size="small" @click="openEventTemplate('on_cookie_expire')">模板</el-button>
|
||||
<el-switch v-model="globalNotifyForm.events.on_cleanup" active-text="清理完成" />
|
||||
<el-button v-if="globalNotifyForm.events.on_cleanup" text type="primary" size="small" @click="openEventTemplate('on_cleanup')">模板</el-button>
|
||||
<el-switch v-model="globalNotifyForm.events.on_daily_report" active-text="每日报告" />
|
||||
<el-button v-if="globalNotifyForm.events.on_daily_report" text type="primary" size="small" @click="openEventTemplate('on_daily_report')">模板</el-button>
|
||||
</div>
|
||||
<div class="form-tip" style="margin-top:8px;">全局推送作为兜底通道。设置了推送用户的网盘配置走用户推送,未设置的走全局推送。</div>
|
||||
</el-form>
|
||||
@@ -546,7 +552,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="pushUserForm.channels" multiple collapse-tags collapse-tags-tooltip placeholder="选择您所需的消息频道" style="width:260px;">
|
||||
<el-select v-model="pushUserForm.channel" placeholder="选择您所需的消息频道" style="width:260px;" @change="onPushUserChannelChange">
|
||||
<el-option
|
||||
v-for="(np, nkey) in enabledNotifyProviders"
|
||||
:key="nkey"
|
||||
@@ -555,15 +561,35 @@
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<!-- Channel params (shown when a channel is selected) -->
|
||||
<div v-if="pushUserForm.channel && pushUserChannelParams.length > 0" style="width:100%; margin-top:4px;">
|
||||
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<template v-for="param in pushUserChannelParams" :key="param.key">
|
||||
<el-input
|
||||
v-model="pushUserForm.paramValues[param.key]"
|
||||
:type="param.type === 'password' ? 'password' : 'text'"
|
||||
:placeholder="param.placeholder || param.label"
|
||||
style="width:200px;"
|
||||
size="small"
|
||||
>
|
||||
<template #prefix>{{ param.label }}</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap; width:100%; margin-top:8px;">
|
||||
<el-switch v-model="pushUserForm.events.on_save_success" active-text="转存成功" />
|
||||
<el-switch v-model="pushUserForm.events.on_save_fail" active-text="转存失败" />
|
||||
<el-switch v-model="pushUserForm.events.on_cookie_expire" active-text="Cookie过期" />
|
||||
<el-switch v-model="pushUserForm.events.on_cleanup" active-text="清理完成" />
|
||||
<el-switch v-model="pushUserForm.events.on_daily_report" active-text="每日报告" />
|
||||
|
||||
<el-button type="primary" size="small" :loading="pushUserSaving" @click="savePushUser">{{ pushUserForm.id ? '更新' : '确认添加' }}</el-button>
|
||||
<el-button v-if="pushUserForm.id" size="small" @click="cancelEditPushUser">取消编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">推送用户列表</el-divider>
|
||||
|
||||
@@ -614,6 +640,30 @@
|
||||
|
||||
<!-- 🔄 系统维护 --> <el-card id="section-sys-maintenance" v-show="!activeSection || activeSection === 'sys-maintenance'"> <template #header> <span>🔄 系统维护</span> </template> <el-form label-width="180px" label-position="left"> <el-form-item label="自动更新镜像"> <el-switch v-model="autoUpdateEnabled" active-text="启用" inactive-text="禁用" /> <div class="form-tip">启用后 CloudSearch 将自动检测并更新到最新镜像版本</div> <div class="form-tip" style="color: var(--(--el-color-warning,#e6a23c));"> 当前需手动在服务器执行:docker-compose -f /opt/CloudSearch/docker-compose.yml pull && docker-compose -f /opt/CloudSearch/docker-compose.yml up -d </div> </el-form-item> </el-form> </el-card>
|
||||
|
||||
<!-- 模板编辑弹窗 -->
|
||||
<el-dialog v-model="eventTemplateDialog.visible" title="编辑推送模板" width="520px">
|
||||
<el-form label-width="80px" label-position="top">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="eventTemplateDialog.title" placeholder="推送标题" :rows="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容模板">
|
||||
<el-input v-model="eventTemplateDialog.content" type="textarea" :rows="6" placeholder="推送内容,支持变量替换" />
|
||||
</el-form-item>
|
||||
<el-form-item label="可用变量">
|
||||
<div style="color:#909399;font-size:12px;line-height:1.8;">
|
||||
<div v-for="(desc, vname) in EVENT_TEMPLATE_VARS[eventTemplateDialog.eventKey] || {}" :key="vname" style="margin:2px 0;">
|
||||
<code style="background:#f4f4f5;padding:1px 5px;border-radius:3px;font-size:11px;">{{ '{' + vname + '}' }}</code>
|
||||
<span style="margin-left:4px;color:#666;">= {{ desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="eventTemplateDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveEventTemplate">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="save-bar">
|
||||
<el-button type="primary" size="large" :loading="saving" @click="handleSave">
|
||||
@@ -717,12 +767,14 @@ async function loadPushUserAccountOptions() {
|
||||
const pushUserForm = reactive<any>({
|
||||
id: null,
|
||||
account: '',
|
||||
channels: [],
|
||||
channel: '',
|
||||
paramValues: {},
|
||||
events: {
|
||||
on_save_success: true,
|
||||
on_save_fail: true,
|
||||
on_cookie_expire: true,
|
||||
on_cleanup: false,
|
||||
on_daily_report: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -737,9 +789,31 @@ const enabledNotifyProviders = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
const globalNotifyForm = reactive<{ channels: Record<string, any>; events: Record<string, boolean> }>({
|
||||
// Params for the currently selected channel (exclude title/content/level — those are admin-set)
|
||||
const pushUserChannelParams = computed(() => {
|
||||
const key = pushUserForm.channel
|
||||
if (!key || !notifyProviders.value[key]) return []
|
||||
return (notifyProviders.value[key].params || []).filter(
|
||||
(p: any) => !['title', 'content', 'level'].includes(p.key)
|
||||
)
|
||||
})
|
||||
|
||||
function onPushUserChannelChange(val: string) {
|
||||
pushUserForm.paramValues = {}
|
||||
// Pre-fill defaults
|
||||
const np = notifyProviders.value[val]
|
||||
if (np && np.params) {
|
||||
for (const p of np.params) {
|
||||
if (p.default !== undefined && !['title', 'content', 'level'].includes(p.key)) {
|
||||
pushUserForm.paramValues[p.key] = p.default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const globalNotifyForm = reactive<{ channels: Record<string, any>; events: Record<string, boolean>; eventTemplates: Record<string, { title: string; content: string }> }>({
|
||||
channels: {},
|
||||
events: { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false },
|
||||
events: { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: false },
|
||||
})
|
||||
|
||||
function initPushUserChannelForm() {
|
||||
@@ -758,26 +832,31 @@ function editPushUser(row?: any) {
|
||||
pushUserForm.id = row.id
|
||||
pushUserForm.account = row.account
|
||||
const nc = row.notify_config || {}
|
||||
pushUserForm.channels = Object.keys(nc.channels || {})
|
||||
const chKeys = Object.keys(nc.channels || {})
|
||||
pushUserForm.channel = chKeys.length > 0 ? chKeys[0] : ''
|
||||
pushUserForm.paramValues = chKeys.length > 0 ? { ...(nc.channels[chKeys[0]] || {}) } : {}
|
||||
pushUserForm.events = {
|
||||
on_save_success: nc.events?.on_save_success !== false,
|
||||
on_save_fail: nc.events?.on_save_fail !== false,
|
||||
on_cookie_expire: nc.events?.on_cookie_expire !== false,
|
||||
on_cleanup: nc.events?.on_cleanup === true,
|
||||
on_daily_report: nc.events?.on_daily_report === true,
|
||||
}
|
||||
} else {
|
||||
pushUserForm.id = null
|
||||
pushUserForm.account = ''
|
||||
pushUserForm.channels = []
|
||||
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
|
||||
pushUserForm.channel = ''
|
||||
pushUserForm.paramValues = {}
|
||||
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: true }
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditPushUser() {
|
||||
pushUserForm.id = null
|
||||
pushUserForm.account = ''
|
||||
pushUserForm.channels = []
|
||||
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
|
||||
pushUserForm.channel = ''
|
||||
pushUserForm.paramValues = {}
|
||||
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: true }
|
||||
}
|
||||
|
||||
function getEventEnabled(row: any, eventKey: string): boolean {
|
||||
@@ -797,10 +876,10 @@ async function savePushUser() {
|
||||
account: pushUserForm.account,
|
||||
notify_config: { channels: {}, events: pushUserForm.events },
|
||||
}
|
||||
// Build channels from selected keys (no params — use global config at push time)
|
||||
// Build channels from selected channel + param values
|
||||
const ch: Record<string, any> = {}
|
||||
for (const key of pushUserForm.channels) {
|
||||
ch[key] = {}
|
||||
if (pushUserForm.channel) {
|
||||
ch[pushUserForm.channel] = { ...pushUserForm.paramValues }
|
||||
}
|
||||
payload.notify_config.channels = ch
|
||||
if (pushUserForm.id) {
|
||||
@@ -819,8 +898,9 @@ async function savePushUser() {
|
||||
const isUpdate = !!pushUserForm.id
|
||||
pushUserForm.id = null
|
||||
pushUserForm.account = ''
|
||||
pushUserForm.channels = []
|
||||
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
|
||||
pushUserForm.channel = ''
|
||||
pushUserForm.paramValues = {}
|
||||
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: true }
|
||||
ElMessage.success(isUpdate ? '推送用户已更新' : '推送用户已添加')
|
||||
await loadPushUsers()
|
||||
} catch (e: any) {
|
||||
@@ -890,7 +970,7 @@ function initGlobalNotifyForm() {
|
||||
}
|
||||
}
|
||||
globalNotifyForm.channels = channels
|
||||
globalNotifyForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
|
||||
globalNotifyForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: false }
|
||||
}
|
||||
|
||||
async function loadGlobalNotifyConfig() {
|
||||
@@ -917,6 +997,7 @@ async function loadGlobalNotifyConfig() {
|
||||
globalNotifyForm.events.on_save_fail = parsed.events.on_save_fail !== false
|
||||
globalNotifyForm.events.on_cookie_expire = parsed.events.on_cookie_expire !== false
|
||||
globalNotifyForm.events.on_cleanup = parsed.events.on_cleanup === true
|
||||
globalNotifyForm.events.on_daily_report = parsed.events.on_daily_report === true
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -928,7 +1009,12 @@ async function testGlobalChannel(channelName: string) {
|
||||
if (!ch || !ch._enabled) return
|
||||
ch._testing = true
|
||||
try {
|
||||
const result = await testNotifyChannel(channelName)
|
||||
// Pass current form params directly (no need to save to DB first)
|
||||
const params: Record<string, string> = {}
|
||||
for (const [pk, pv] of Object.entries(ch)) {
|
||||
if (!pk.startsWith('_') && pv !== '') params[pk] = String(pv)
|
||||
}
|
||||
const result = await testNotifyChannel(channelName, undefined, params)
|
||||
if (result.success) {
|
||||
ElMessage.success(result.message)
|
||||
} else {
|
||||
@@ -940,6 +1026,56 @@ async function testGlobalChannel(channelName: string) {
|
||||
ch._testing = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Event Template Editor ====================
|
||||
const eventTemplateDialog = reactive({
|
||||
visible: false,
|
||||
eventKey: '',
|
||||
title: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
const EVENT_TEMPLATE_DEFAULTS: Record<string, { title: string; content: string }> = {
|
||||
on_save_success: { title: '✅ 转存成功', content: '{cloud_type} · {nickname}\n文件: {file_name}\n耗时: {duration}s' },
|
||||
on_save_fail: { title: '❌ 转存连续失败 {fail_count} 次', content: '{cloud_type} · {nickname}\n链接: {share_url}\n错误: {error}' },
|
||||
on_cookie_expire: { title: '⚠️ Cookie过期', content: '{cloud_type} · {nickname}\n链接: {share_url}\n请重新登录' },
|
||||
on_cleanup: { title: '🧹 清理完成', content: '{stats_lines}' },
|
||||
on_daily_report: { title: '📊 每日报告 - {date}', content: '昨日({date})网盘推送报告\n\n总计转存: {total_saves} 次\n成功: {success_count} 次 | 失败: {fail_count} 次\n\n各网盘详情:\n{details}' },
|
||||
}
|
||||
|
||||
const EVENT_TEMPLATE_VARS: Record<string, Record<string, string>> = {
|
||||
on_save_success: { file_name: '文件名', file_size: '文件大小', cloud_type: '网盘类型', nickname: '来源账号昵称', duration: '耗时(秒)', share_url: '分享链接' },
|
||||
on_save_fail: { file_name: '文件名', fail_count: '连续失败次数', cloud_type: '网盘类型', nickname: '来源账号昵称', error: '错误信息', share_url: '分享链接' },
|
||||
on_cookie_expire: { cloud_type: '网盘类型', nickname: '来源账号昵称', share_url: '分享链接' },
|
||||
on_cleanup: { stats_lines: '清理统计(多行文本)' },
|
||||
on_daily_report: { date: '报告日期', total_saves: '总转存次数', success_count: '成功次数', fail_count: '失败次数', details: '详细记录(多行文本)' },
|
||||
}
|
||||
|
||||
function openEventTemplate(eventKey: string) {
|
||||
const saved = globalNotifyForm.eventTemplates?.[eventKey]
|
||||
const def = EVENT_TEMPLATE_DEFAULTS[eventKey] || { title: '', content: '' }
|
||||
eventTemplateDialog.eventKey = eventKey
|
||||
eventTemplateDialog.title = saved?.title || def.title
|
||||
eventTemplateDialog.content = saved?.content || def.content
|
||||
eventTemplateDialog.visible = true
|
||||
}
|
||||
|
||||
function saveEventTemplate() {
|
||||
if (!globalNotifyForm.eventTemplates) {
|
||||
globalNotifyForm.eventTemplates = {}
|
||||
}
|
||||
globalNotifyForm.eventTemplates[eventTemplateDialog.eventKey] = {
|
||||
title: eventTemplateDialog.title,
|
||||
content: eventTemplateDialog.content,
|
||||
}
|
||||
eventTemplateDialog.visible = false
|
||||
ElMessage.success('模板已保存(点击「保存配置」后生效)')
|
||||
}
|
||||
|
||||
function getEventTemplate(eventKey: string): { title: string; content: string } | null {
|
||||
return globalNotifyForm.eventTemplates?.[eventKey] || null
|
||||
}
|
||||
|
||||
const passwordForm = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
|
||||
Reference in New Issue
Block a user