3 Commits

11 changed files with 58 additions and 229 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "cloudsearch-backend", "name": "cloudsearch-backend",
"version": "0.2.2", "version": "0.2.6",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "tsx watch src/main.ts", "dev": "tsx watch src/main.ts",

View File

@@ -274,8 +274,6 @@ export async function checkAndRunScheduledCleanup(): Promise<void> {
if (stats.trashEmptied) lines.push('已清空回收站'); 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 (stats.errors.length > 0) lines.push(`⚠️ ${stats.errors.length} 个错误(${stats.errors.slice(0, 3).join('; ')}${stats.errors.length > 3 ? `...` : ''}`);
if (lines.length > 0) { 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'),
});
} }
} }

View File

@@ -107,7 +107,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
if (existing?.share_url) { if (existing?.share_url) {
const { LinkValidator } = await import('../validation/link-validator.service'); const { LinkValidator } = await import('../validation/link-validator.service');
const validator = new LinkValidator(); const validator = new LinkValidator();
const validation = await validator.validate(existing.share_url, 'quark'); const validation = await validator.validateWithLocalFallback(existing.share_url, 'quark');
if (validation.status === 'valid') { if (validation.status === 'valid') {
const isFirstReuse = dedupCutoff ? !db.prepare( const isFirstReuse = dedupCutoff ? !db.prepare(
`SELECT 1 FROM save_records WHERE source_url = ? AND created_at >= ? AND status = 'reused' LIMIT 1` `SELECT 1 FROM save_records WHERE source_url = ? AND created_at >= ? AND status = 'reused' LIMIT 1`

View File

@@ -17,6 +17,7 @@ export function getAdKeywords(): string[] {
const raw = getSystemConfig("quark_ad_keywords") || ""; const raw = getSystemConfig("quark_ad_keywords") || "";
return raw return raw
.split("\n") .split("\n")
.flatMap((line) => line.split(","))
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean); .filter(Boolean);
} }
@@ -26,6 +27,7 @@ export function getWarningFolderNames(): string[] {
const raw = getSystemConfig("quark_warning_folder_names") || ""; const raw = getSystemConfig("quark_warning_folder_names") || "";
return raw return raw
.split("\n") .split("\n")
.flatMap((line) => line.split(","))
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean); .filter(Boolean);
} }
@@ -66,7 +68,8 @@ export async function deleteAdFiles(
dirFid: string, dirFid: string,
keywords: string[], keywords: string[],
): Promise<number> { ): Promise<number> {
if (!keywords.length) return 0; const extensions = getSusExtensions();
if (!keywords.length && !extensions.length) return 0;
let deletedCount = 0; let deletedCount = 0;
const stack: string[] = [dirFid]; const stack: string[] = [dirFid];
@@ -125,6 +128,7 @@ async function batchDeleteFiles(
cookie: string, cookie: string,
fids: string[], fids: string[],
): Promise<boolean> { ): Promise<boolean> {
if (!fids.length) return true;
try { try {
const resp = await fetch( const resp = await fetch(
`https://drive-pc.quark.cn/1/clouddrive/file/trash?${makeQuery()}`, `https://drive-pc.quark.cn/1/clouddrive/file/trash?${makeQuery()}`,
@@ -135,13 +139,17 @@ async function batchDeleteFiles(
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
action_type: 2, // 2 = 移入回收站 action_type: 1,
file_list: fids.map((fid) => ({ fid })), filelist: fids,
exclude_fids: [], exclude_filelist: [],
}), }),
signal: AbortSignal.timeout(15000), signal: AbortSignal.timeout(30000),
}, },
); );
if (!resp.ok) {
console.log(`[Quark-AdCleanup] batchDelete HTTP ${resp.status}`);
return false;
}
const data = (await resp.json()) as any; const data = (await resp.json()) as any;
if (data.status === 200) { if (data.status === 200) {
return true; return true;
@@ -156,6 +164,7 @@ async function batchDeleteFiles(
} }
} }
// ==================== 警示文件夹创建 ==================== // ==================== 警示文件夹创建 ====================
/** /**
@@ -255,22 +264,23 @@ export async function runAdCleanup(
savedDirFid: string, savedDirFid: string,
): Promise<{ adDeleted: number; warningDirs: number }> { ): Promise<{ adDeleted: number; warningDirs: number }> {
const keywords = getAdKeywords(); const keywords = getAdKeywords();
const susExtensions = getSusExtensions();
const warningNames = getWarningFolderNames(); const warningNames = getWarningFolderNames();
let adDeleted = 0; let adDeleted = 0;
let warningDirs = 0; let warningDirs = 0;
// 1. 广告关键词清理 // 1. 广告关键词 + 可疑后缀清理
if (keywords.length > 0) { if (keywords.length > 0 || susExtensions.length > 0) {
console.log( console.log(
`[Quark-AdCleanup] 开始广告关键词清理: ${keywords.length} 个关键词`, `[Quark-AdCleanup] 开始文件清理: ${keywords.length} 个关键词, ${susExtensions.length} 个可疑后缀`,
); );
adDeleted = await deleteAdFiles(cookie, savedDirFid, keywords); adDeleted = await deleteAdFiles(cookie, savedDirFid, keywords);
console.log( console.log(
`[Quark-AdCleanup] 广告清理完成,共删除 ${adDeleted} 个文件/文件夹`, `[Quark-AdCleanup] 清理完成,共删除 ${adDeleted} 个文件/文件夹`,
); );
} else { } else {
console.log("[Quark-AdCleanup] 无广告关键词配置,跳过清理"); console.log("[Quark-AdCleanup] 无关键词/可疑后缀配置,跳过清理");
} }
// 2. 创建警示文件夹 // 2. 创建警示文件夹

View File

@@ -198,13 +198,13 @@ export async function trashFiles(cookie: string, fids: string[]): Promise<boolea
if (!fids.length) return true; if (!fids.length) return true;
try { try {
const response = await fetch( const response = await fetch(
`${BASE_URL}/1/clouddrive/file/trash?${makeQuery()}`, `${BASE_URL}/1/clouddrive/file/delete?${makeQuery()}`,
{ {
method: 'POST', method: 'POST',
headers: { ...getHeaders(cookie), 'Content-Type': 'application/json' }, headers: { ...getHeaders(cookie), 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
action_type: 1, // 1 = move to trash action_type: 1, // 1 = move to trash
filelist: fids, filelist: fids.map(fid => ({ fid })),
exclude_filelist: [], exclude_filelist: [],
}), }),
signal: AbortSignal.timeout(30000), signal: AbortSignal.timeout(30000),

View File

@@ -111,21 +111,9 @@ export function notifyEvent(
eventName: string, eventName: string,
title: string, title: string,
content: string, content: string,
level: 'info' | 'warn' | 'error' = 'info', level: 'info' | 'warn' | 'error' = 'info'
templateVars?: Record<string, string>
): void { ): void {
if (!checkEventEnabled(eventName)) return; 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); notify(title, content, level);
} }
@@ -144,40 +132,13 @@ function getConfigNotifySettings(configId: number): PerConfigNotify {
return {}; 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( export function notifyConfigEvent(
configId: number, configId: number,
eventName: string, eventName: string,
title: string, title: string,
content: string, content: string,
level: 'info' | 'warn' | 'error' = 'info', level: 'info' | 'warn' | 'error' = 'info'
templateVars?: Record<string, string>
): void { ): 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 // Find matching push user by cloud_configs.promotion_account
const pushUser = findPushUserForConfig(configId); const pushUser = findPushUserForConfig(configId);
if (!pushUser) { if (!pushUser) {
@@ -190,6 +151,7 @@ export function notifyConfigEvent(
try { notifyConfig = JSON.parse(pushUser.notify_config); } catch {} try { notifyConfig = JSON.parse(pushUser.notify_config); } catch {}
// Check event switch // Check event switch
const eventKey = 'on_' + eventName;
if (notifyConfig.events && notifyConfig.events[eventKey] === false) return; if (notifyConfig.events && notifyConfig.events[eventKey] === false) return;
// Build channels from push user config // Build channels from push user config
@@ -211,15 +173,11 @@ export function notifyConfigEvent(
/** 测试某个通道 */ /** 测试某个通道 */
export async function testChannel( export async function testChannel(
channelName: string, channelName: string,
account?: string, account?: string
directParams?: Record<string, string>
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
let params: Record<string, string> = {}; let params: Record<string, string> = {};
// If direct params provided, use them directly (for global panel test without saving first) if (account) {
if (directParams) {
params = directParams;
} else if (account) {
const pushUser = findPushUserForConfig(undefined); const pushUser = findPushUserForConfig(undefined);
// Use pushUser lookup by account instead // Use pushUser lookup by account instead
const { getPushUserByAccount } = require('./push-user.service'); const { getPushUserByAccount } = require('./push-user.service');

View File

@@ -660,9 +660,9 @@ router.put('/admin/cloud-configs/:id/notify', (req: Request, res: Response) => {
/** POST /api/admin/notify/test — test a notification channel */ /** POST /api/admin/notify/test — test a notification channel */
router.post('/admin/notify/test', async (req: Request, res: Response) => { router.post('/admin/notify/test', async (req: Request, res: Response) => {
try { try {
const { channelType, account, configId, params } = req.body; const { channelType, account, configId } = req.body;
const ctx = account || (configId ? String(configId) : undefined); const ctx = account || (configId ? String(configId) : undefined);
const result = await testChannel(channelType as string, ctx, params); const result = await testChannel(channelType as string, ctx);
res.json(result); res.json(result);
} catch (err: any) { } catch (err: any) {
res.json({ success: false, message: err.message || '测试发送失败' }); res.json({ success: false, message: err.message || '测试发送失败' });

View File

@@ -1 +1 @@
export const VERSION = "0.2.2"; export const VERSION = "0.2.6";

View File

@@ -1,6 +1,6 @@
{ {
"name": "cloudsearch-frontend", "name": "cloudsearch-frontend",
"version": "0.2.2", "version": "0.2.6",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -346,10 +346,9 @@ export async function getAllNotifierProviders(): Promise<Record<string, { name:
export async function testNotifyChannel( export async function testNotifyChannel(
channelType: string, channelType: string,
configId?: number, configId?: number
params?: Record<string, string>
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
const { data } = await api.post('/admin/notify/test', { channelType, configId, params }) const { data } = await api.post('/admin/notify/test', { channelType, configId })
return data return data
} }

View File

@@ -524,15 +524,9 @@
<el-divider content-position="left">全局事件开关</el-divider> <el-divider content-position="left">全局事件开关</el-divider>
<div style="display:flex; flex-wrap:wrap; gap:16px;"> <div style="display:flex; flex-wrap:wrap; gap:16px;">
<el-switch v-model="globalNotifyForm.events.on_save_success" active-text="转存成功" /> <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-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-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-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>
<div class="form-tip" style="margin-top:8px;">全局推送作为兜底通道设置了推送用户的网盘配置走用户推送未设置的走全局推送</div> <div class="form-tip" style="margin-top:8px;">全局推送作为兜底通道设置了推送用户的网盘配置走用户推送未设置的走全局推送</div>
</el-form> </el-form>
@@ -552,7 +546,7 @@
/> />
</el-select> </el-select>
<el-select v-model="pushUserForm.channel" placeholder="选择您所需的消息频道" style="width:260px;" @change="onPushUserChannelChange"> <el-select v-model="pushUserForm.channels" multiple collapse-tags collapse-tags-tooltip placeholder="选择您所需的消息频道" style="width:260px;">
<el-option <el-option
v-for="(np, nkey) in enabledNotifyProviders" v-for="(np, nkey) in enabledNotifyProviders"
:key="nkey" :key="nkey"
@@ -561,33 +555,13 @@
/> />
</el-select> </el-select>
<!-- Channel params (shown when a channel is selected) --> <el-switch v-model="pushUserForm.events.on_save_success" active-text="转存成功" />
<div v-if="pushUserForm.channel && pushUserChannelParams.length > 0" style="width:100%; margin-top:4px;"> <el-switch v-model="pushUserForm.events.on_save_fail" active-text="转存失败" />
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;"> <el-switch v-model="pushUserForm.events.on_cookie_expire" active-text="Cookie过期" />
<template v-for="param in pushUserChannelParams" :key="param.key"> <el-switch v-model="pushUserForm.events.on_cleanup" active-text="清理完成" />
<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-button type="primary" size="small" :loading="pushUserSaving" @click="savePushUser">{{ pushUserForm.id ? '更新' : '确认添加' }}</el-button>
<el-switch v-model="pushUserForm.events.on_save_success" active-text="转存成功" /> <el-button v-if="pushUserForm.id" size="small" @click="cancelEditPushUser">取消编辑</el-button>
<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>
</div> </div>
@@ -640,30 +614,6 @@
<!-- 🔄 系统维护 --> <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-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"> <div class="save-bar">
<el-button type="primary" size="large" :loading="saving" @click="handleSave"> <el-button type="primary" size="large" :loading="saving" @click="handleSave">
@@ -767,14 +717,12 @@ async function loadPushUserAccountOptions() {
const pushUserForm = reactive<any>({ const pushUserForm = reactive<any>({
id: null, id: null,
account: '', account: '',
channel: '', channels: [],
paramValues: {},
events: { events: {
on_save_success: true, on_save_success: true,
on_save_fail: true, on_save_fail: true,
on_cookie_expire: true, on_cookie_expire: true,
on_cleanup: false, on_cleanup: false,
on_daily_report: true,
}, },
}) })
@@ -789,31 +737,9 @@ const enabledNotifyProviders = computed(() => {
return result return result
}) })
// Params for the currently selected channel (exclude title/content/level — those are admin-set) const globalNotifyForm = reactive<{ channels: Record<string, any>; events: Record<string, boolean> }>({
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: {}, channels: {},
events: { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: false }, events: { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false },
}) })
function initPushUserChannelForm() { function initPushUserChannelForm() {
@@ -832,31 +758,26 @@ function editPushUser(row?: any) {
pushUserForm.id = row.id pushUserForm.id = row.id
pushUserForm.account = row.account pushUserForm.account = row.account
const nc = row.notify_config || {} const nc = row.notify_config || {}
const chKeys = Object.keys(nc.channels || {}) pushUserForm.channels = Object.keys(nc.channels || {})
pushUserForm.channel = chKeys.length > 0 ? chKeys[0] : ''
pushUserForm.paramValues = chKeys.length > 0 ? { ...(nc.channels[chKeys[0]] || {}) } : {}
pushUserForm.events = { pushUserForm.events = {
on_save_success: nc.events?.on_save_success !== false, on_save_success: nc.events?.on_save_success !== false,
on_save_fail: nc.events?.on_save_fail !== false, on_save_fail: nc.events?.on_save_fail !== false,
on_cookie_expire: nc.events?.on_cookie_expire !== false, on_cookie_expire: nc.events?.on_cookie_expire !== false,
on_cleanup: nc.events?.on_cleanup === true, on_cleanup: nc.events?.on_cleanup === true,
on_daily_report: nc.events?.on_daily_report === true,
} }
} else { } else {
pushUserForm.id = null pushUserForm.id = null
pushUserForm.account = '' pushUserForm.account = ''
pushUserForm.channel = '' pushUserForm.channels = []
pushUserForm.paramValues = {} pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: true }
} }
} }
function cancelEditPushUser() { function cancelEditPushUser() {
pushUserForm.id = null pushUserForm.id = null
pushUserForm.account = '' pushUserForm.account = ''
pushUserForm.channel = '' pushUserForm.channels = []
pushUserForm.paramValues = {} pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
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 { function getEventEnabled(row: any, eventKey: string): boolean {
@@ -876,10 +797,10 @@ async function savePushUser() {
account: pushUserForm.account, account: pushUserForm.account,
notify_config: { channels: {}, events: pushUserForm.events }, notify_config: { channels: {}, events: pushUserForm.events },
} }
// Build channels from selected channel + param values // Build channels from selected keys (no params — use global config at push time)
const ch: Record<string, any> = {} const ch: Record<string, any> = {}
if (pushUserForm.channel) { for (const key of pushUserForm.channels) {
ch[pushUserForm.channel] = { ...pushUserForm.paramValues } ch[key] = {}
} }
payload.notify_config.channels = ch payload.notify_config.channels = ch
if (pushUserForm.id) { if (pushUserForm.id) {
@@ -898,9 +819,8 @@ async function savePushUser() {
const isUpdate = !!pushUserForm.id const isUpdate = !!pushUserForm.id
pushUserForm.id = null pushUserForm.id = null
pushUserForm.account = '' pushUserForm.account = ''
pushUserForm.channel = '' pushUserForm.channels = []
pushUserForm.paramValues = {} pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
pushUserForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: true }
ElMessage.success(isUpdate ? '推送用户已更新' : '推送用户已添加') ElMessage.success(isUpdate ? '推送用户已更新' : '推送用户已添加')
await loadPushUsers() await loadPushUsers()
} catch (e: any) { } catch (e: any) {
@@ -970,7 +890,7 @@ function initGlobalNotifyForm() {
} }
} }
globalNotifyForm.channels = channels globalNotifyForm.channels = channels
globalNotifyForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false, on_daily_report: false } globalNotifyForm.events = { on_save_success: true, on_save_fail: true, on_cookie_expire: true, on_cleanup: false }
} }
async function loadGlobalNotifyConfig() { async function loadGlobalNotifyConfig() {
@@ -997,7 +917,6 @@ async function loadGlobalNotifyConfig() {
globalNotifyForm.events.on_save_fail = parsed.events.on_save_fail !== false 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_cookie_expire = parsed.events.on_cookie_expire !== false
globalNotifyForm.events.on_cleanup = parsed.events.on_cleanup === true globalNotifyForm.events.on_cleanup = parsed.events.on_cleanup === true
globalNotifyForm.events.on_daily_report = parsed.events.on_daily_report === true
} }
} catch {} } catch {}
} }
@@ -1009,12 +928,7 @@ async function testGlobalChannel(channelName: string) {
if (!ch || !ch._enabled) return if (!ch || !ch._enabled) return
ch._testing = true ch._testing = true
try { try {
// Pass current form params directly (no need to save to DB first) const result = await testNotifyChannel(channelName)
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) { if (result.success) {
ElMessage.success(result.message) ElMessage.success(result.message)
} else { } else {
@@ -1026,56 +940,6 @@ async function testGlobalChannel(channelName: string) {
ch._testing = false 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({ const passwordForm = reactive({
oldPassword: '', oldPassword: '',
newPassword: '', newPassword: '',