Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80e5d24143 | |||
|
|
3179150596 | ||
|
|
b5d3620273 |
@@ -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",
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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. 创建警示文件夹
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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 || '测试发送失败' });
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const VERSION = "0.2.2";
|
export const VERSION = "0.2.6";
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user