feat: 支持配置通知器,在视频源处理或整个下载任务出现错误时会触发消息通知 (#526)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-11-07 20:37:09 +08:00
committed by GitHub
parent a871db655f
commit 8ac6829e61
9 changed files with 350 additions and 5 deletions

View File

@@ -272,6 +272,20 @@ export interface ConcurrentLimit {
download: ConcurrentDownloadLimit;
}
// Notifier 相关类型
export interface TelegramNotifier {
type: 'telegram';
bot_token: string;
chat_id: string;
}
export interface WebhookNotifier {
type: 'webhook';
url: string;
}
export type Notifier = TelegramNotifier | WebhookNotifier;
export interface Config {
auth_token: string;
bind_address: string;
@@ -281,6 +295,7 @@ export interface Config {
skip_option: SkipOption;
video_name: string;
page_name: string;
notifiers: Notifier[] | null;
favorite_default_path: string;
collection_default_path: string;
submission_default_path: string;

View File

@@ -7,11 +7,13 @@
import * as Tabs from '$lib/components/ui/tabs/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import PasswordInput from '$lib/components/custom/password-input.svelte';
import NotifierDialog from './NotifierDialog.svelte';
import api from '$lib/api';
import { toast } from 'svelte-sonner';
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import type { Config, ApiError } from '$lib/types';
import type { Config, ApiError, Notifier } from '$lib/types';
let frontendToken = ''; // 前端认证token
let config: Config | null = null;
@@ -19,6 +21,55 @@
let saving = false;
let loading = false;
// Notifier 管理相关
let showNotifierDialog = false;
let editingNotifier: Notifier | null = null;
let editingNotifierIndex: number | null = null;
let isEditing = false;
function openAddNotifierDialog() {
editingNotifier = null;
editingNotifierIndex = null;
isEditing = false;
showNotifierDialog = true;
}
function openEditNotifierDialog(notifier: Notifier, index: number) {
editingNotifier = { ...notifier };
editingNotifierIndex = index;
isEditing = true;
showNotifierDialog = true;
}
function closeNotifierDialog() {
showNotifierDialog = false;
editingNotifier = null;
editingNotifierIndex = null;
isEditing = false;
}
function addNotifier(notifier: Notifier) {
if (!formData) return;
if (!formData.notifiers) {
formData.notifiers = [];
}
formData.notifiers = [...formData.notifiers, notifier];
closeNotifierDialog();
}
function updateNotifier(index: number, notifier: Notifier) {
if (!formData?.notifiers) return;
const newNotifiers = [...formData.notifiers];
newNotifiers[index] = notifier;
formData.notifiers = newNotifiers;
closeNotifierDialog();
}
function deleteNotifier(index: number) {
if (!formData?.notifiers) return;
formData.notifiers = formData.notifiers.filter((_, i) => i !== index);
}
async function loadConfig() {
loading = true;
try {
@@ -142,11 +193,12 @@
{:else if formData}
<div class="space-y-6">
<Tabs.Root value="basic" class="w-full">
<Tabs.List class="grid w-full grid-cols-5">
<Tabs.List class="grid w-full grid-cols-6">
<Tabs.Trigger value="basic">基本设置</Tabs.Trigger>
<Tabs.Trigger value="auth">B站认证</Tabs.Trigger>
<Tabs.Trigger value="filter">视频处理</Tabs.Trigger>
<Tabs.Trigger value="danmaku">弹幕渲染</Tabs.Trigger>
<Tabs.Trigger value="notifiers">通知设置</Tabs.Trigger>
<Tabs.Trigger value="advanced">高级设置</Tabs.Trigger>
</Tabs.List>
@@ -620,6 +672,66 @@
</div>
</Tabs.Content>
<!-- 通知设置 -->
<Tabs.Content value="notifiers" class="mt-6 space-y-6">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold">通知器管理</h3>
<p class="text-muted-foreground text-sm">
配置通知器,在下载任务出现错误时发送通知
</p>
</div>
<Button onclick={openAddNotifierDialog}>+ 添加通知器</Button>
</div>
{#if !formData.notifiers || formData.notifiers.length === 0}
<div class="rounded-lg border-2 border-dashed py-12 text-center">
<p class="text-muted-foreground">暂无通知器配置</p>
<Button class="mt-4" variant="outline" onclick={openAddNotifierDialog}>
添加第一个通知器
</Button>
</div>
{:else}
<div class="space-y-3">
{#each formData.notifiers as notifier, index (index)}
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="flex-1">
{#if notifier.type === 'telegram'}
<div class="flex items-center gap-2">
<Badge variant="secondary">Telegram</Badge>
<span class="text-muted-foreground text-sm">
Chat ID: {notifier.chat_id}
</span>
</div>
{:else if notifier.type === 'webhook'}
<div class="flex items-center gap-2">
<Badge variant="secondary">Webhook</Badge>
<span class="text-muted-foreground text-sm">
{notifier.url}
</span>
</div>
{/if}
</div>
<div class="flex gap-2">
<Button
size="sm"
variant="outline"
onclick={() => openEditNotifierDialog(notifier, index)}
>
编辑
</Button>
<Button size="sm" variant="destructive" onclick={() => deleteNotifier(index)}>
删除
</Button>
</div>
</div>
{/each}
</div>
{/if}
</div>
</Tabs.Content>
<!-- 高级设置 -->
<Tabs.Content value="advanced" class="mt-6 space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
@@ -756,3 +868,33 @@
</div>
{/if}
</div>
<Dialog.Root bind:open={showNotifierDialog}>
<Dialog.Portal>
<Dialog.Overlay class="bg-background/80 fixed inset-0 z-50 backdrop-blur-sm" />
<Dialog.Content
class="bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg"
>
<Dialog.Header>
<Dialog.Title>
{isEditing ? '编辑通知器' : '添加通知器'}
</Dialog.Title>
<Dialog.Description>配置通知器类型和参数</Dialog.Description>
</Dialog.Header>
{#if showNotifierDialog}
<NotifierDialog
notifier={editingNotifier}
onSave={(notifier) => {
if (isEditing && editingNotifierIndex !== null) {
updateNotifier(editingNotifierIndex, notifier);
} else {
addNotifier(notifier);
}
}}
onCancel={closeNotifierDialog}
/>
{/if}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View File

@@ -0,0 +1,122 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { toast } from 'svelte-sonner';
import type { Notifier } from '$lib/types';
const jsonExample = '{"text": "您的消息内容"}';
export let notifier: Notifier | null = null;
export let onSave: (notifier: Notifier) => void;
export let onCancel: () => void;
let type: 'telegram' | 'webhook' = 'telegram';
let botToken = '';
let chatId = '';
let webhookUrl = '';
// 初始化表单
$: {
if (notifier) {
if (notifier.type === 'telegram') {
type = 'telegram';
botToken = notifier.bot_token;
chatId = notifier.chat_id;
} else {
type = 'webhook';
webhookUrl = notifier.url;
}
} else {
type = 'telegram';
botToken = '';
chatId = '';
webhookUrl = '';
}
}
function handleSave() {
// 验证表单
if (type === 'telegram') {
if (!botToken.trim()) {
toast.error('请输入 Bot Token');
return;
}
if (!chatId.trim()) {
toast.error('请输入 Chat ID');
return;
}
const newNotifier: Notifier = {
type: 'telegram',
bot_token: botToken.trim(),
chat_id: chatId.trim()
};
onSave(newNotifier);
} else {
if (!webhookUrl.trim()) {
toast.error('请输入 Webhook URL');
return;
}
// 简单的 URL 验证
try {
new URL(webhookUrl.trim());
} catch {
toast.error('请输入有效的 Webhook URL');
return;
}
const newNotifier: Notifier = {
type: 'webhook',
url: webhookUrl.trim()
};
onSave(newNotifier);
}
}
</script>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="notifier-type">通知器类型</Label>
<select
id="notifier-type"
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
bind:value={type}
>
<option value="telegram">Telegram Bot</option>
<option value="webhook">Webhook</option>
</select>
</div>
{#if type === 'telegram'}
<div class="space-y-2">
<Label for="bot-token">Bot Token</Label>
<Input
id="bot-token"
placeholder="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
bind:value={botToken}
/>
<p class="text-muted-foreground text-xs">从 @BotFather 获取的 Bot Token</p>
</div>
<div class="space-y-2">
<Label for="chat-id">Chat ID</Label>
<Input id="chat-id" placeholder="-1001234567890" bind:value={chatId} />
<p class="text-muted-foreground text-xs">目标聊天室的 ID个人用户、群组或频道</p>
</div>
{:else if type === 'webhook'}
<div class="space-y-2">
<Label for="webhook-url">Webhook URL</Label>
<Input id="webhook-url" placeholder="https://example.com/webhook" bind:value={webhookUrl} />
<p class="text-muted-foreground text-xs">
接收通知的 Webhook 地址<br />
格式示例:{jsonExample}
</p>
</div>
{/if}
</div>
<div class="flex justify-end gap-3">
<Button variant="outline" onclick={onCancel}>取消</Button>
<Button onclick={handleSave}>保存</Button>
</div>