update: tg

This commit is contained in:
Kerwin
2025-09-17 14:31:12 +08:00
parent 1eb37baa87
commit cd8c519b3a
11 changed files with 804 additions and 62 deletions

View File

@@ -17,6 +17,8 @@ func TelegramChannelToResponse(channel entity.TelegramChannel) dto.TelegramChann
ChatType: channel.ChatType,
PushEnabled: channel.PushEnabled,
PushFrequency: channel.PushFrequency,
PushStartTime: channel.PushStartTime,
PushEndTime: channel.PushEndTime,
ContentCategories: channel.ContentCategories,
ContentTags: channel.ContentTags,
IsActive: channel.IsActive,
@@ -43,6 +45,8 @@ func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy strin
ChatType: req.ChatType,
PushEnabled: req.PushEnabled,
PushFrequency: req.PushFrequency,
PushStartTime: req.PushStartTime,
PushEndTime: req.PushEndTime,
ContentCategories: req.ContentCategories,
ContentTags: req.ContentTags,
IsActive: req.IsActive,

View File

@@ -9,6 +9,8 @@ type TelegramChannelRequest struct {
ChatType string `json:"chat_type" binding:"required"` // channel 或 group
PushEnabled bool `json:"push_enabled"`
PushFrequency int `json:"push_frequency"`
PushStartTime string `json:"push_start_time"`
PushEndTime string `json:"push_end_time"`
ContentCategories string `json:"content_categories"`
ContentTags string `json:"content_tags"`
IsActive bool `json:"is_active"`
@@ -22,6 +24,8 @@ type TelegramChannelResponse struct {
ChatType string `json:"chat_type"`
PushEnabled bool `json:"push_enabled"`
PushFrequency int `json:"push_frequency"`
PushStartTime string `json:"push_start_time"`
PushEndTime string `json:"push_end_time"`
ContentCategories string `json:"content_categories"`
ContentTags string `json:"content_tags"`
IsActive bool `json:"is_active"`

View File

@@ -18,6 +18,8 @@ type TelegramChannel struct {
// 推送配置
PushEnabled bool `json:"push_enabled" gorm:"default:true;comment:是否启用推送"`
PushFrequency int `json:"push_frequency" gorm:"default:24;comment:推送频率(小时)"`
PushStartTime string `json:"push_start_time" gorm:"size:10;comment:推送开始时间格式HH:mm"`
PushEndTime string `json:"push_end_time" gorm:"size:10;comment:推送结束时间格式HH:mm"`
ContentCategories string `json:"content_categories" gorm:"type:text;comment:推送的内容分类,用逗号分隔"`
ContentTags string `json:"content_tags" gorm:"type:text;comment:推送的标签,用逗号分隔"`

View File

@@ -88,7 +88,29 @@ func (r *TelegramChannelRepositoryImpl) UpdateLastPushAt(id uint, lastPushAt tim
func (r *TelegramChannelRepositoryImpl) FindDueForPush() ([]entity.TelegramChannel, error) {
var channels []entity.TelegramChannel
// 查找活跃、启用推送的频道,且距离上次推送已超过推送频率小时的记录
err := r.db.Where("is_active = ? AND push_enabled = ? AND (last_push_at IS NULL OR last_push_at < DATE_SUB(NOW(), INTERVAL push_frequency HOUR))",
true, true).Find(&channels).Error
return channels, err
// 先获取所有活跃且启用推送的频道
err := r.db.Where("is_active = ? AND push_enabled = ?", true, true).Find(&channels).Error
if err != nil {
return nil, err
}
// 在内存中过滤出需要推送的频道(更可靠的跨数据库方案)
var dueChannels []entity.TelegramChannel
now := time.Now()
for _, channel := range channels {
// 如果从未推送过,或者距离上次推送已超过推送频率小时
if channel.LastPushAt == nil {
dueChannels = append(dueChannels, channel)
} else {
// 计算下次推送时间:上次推送时间 + 推送频率小时
nextPushTime := channel.LastPushAt.Add(time.Duration(channel.PushFrequency) * time.Hour)
if now.After(nextPushTime) {
dueChannels = append(dueChannels, channel)
}
}
}
return dueChannels, nil
}

View File

@@ -74,12 +74,11 @@ func (h *TelegramHandler) UpdateBotConfig(c *gin.Context) {
return
}
// TODO: 这里应该启动或停止 Telegram bot 服务
// if req.BotEnabled != nil && *req.BotEnabled {
// go h.telegramBotService.Start()
// } else {
// go h.telegramBotService.Stop()
// }
// 重新加载机器人服务配置
if err := h.telegramBotService.ReloadConfig(); err != nil {
ErrorResponse(c, "重新加载机器人配置失败", http.StatusInternalServerError)
return
}
// 返回成功
SuccessResponse(c, map[string]interface{}{
@@ -182,6 +181,8 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
channel.ChatType = req.ChatType
channel.PushEnabled = req.PushEnabled
channel.PushFrequency = req.PushFrequency
channel.PushStartTime = req.PushStartTime
channel.PushEndTime = req.PushEndTime
channel.ContentCategories = req.ContentCategories
channel.ContentTags = req.ContentTags
channel.IsActive = req.IsActive
@@ -255,16 +256,48 @@ func (h *TelegramHandler) HandleWebhook(c *gin.Context) {
// GetBotStatus 获取机器人状态
func (h *TelegramHandler) GetBotStatus(c *gin.Context) {
// 这里可以返回机器人运行状态、最后活动时间等信息
// 暂时返回基本状态信息
// 获取机器人运行状态
runtimeStatus := h.telegramBotService.GetRuntimeStatus()
botUsername := h.telegramBotService.GetBotUsername()
// 获取配置状态
configs, err := h.systemConfigRepo.GetOrCreateDefault()
if err != nil {
ErrorResponse(c, "获取配置失败", http.StatusInternalServerError)
return
}
// 解析配置状态
configStatus := map[string]interface{}{
"enabled": false,
"auto_reply_enabled": false,
"api_key_configured": false,
}
for _, config := range configs {
switch config.Key {
case "telegram_bot_enabled":
configStatus["enabled"] = config.Value == "true"
case "telegram_auto_reply_enabled":
configStatus["auto_reply_enabled"] = config.Value == "true"
case "telegram_bot_api_key":
configStatus["api_key_configured"] = config.Value != ""
}
}
// 合并状态信息
status := map[string]interface{}{
"bot_username": botUsername,
"service_running": botUsername != "",
"webhook_mode": false, // 当前使用长轮询模式
"polling_mode": true,
"config": configStatus,
"runtime": runtimeStatus,
"overall_status": runtimeStatus["is_running"].(bool),
"status_text": func() string {
if runtimeStatus["is_running"].(bool) {
return "运行中"
} else if configStatus["enabled"].(bool) {
return "已启用但未运行"
} else {
return "已停止"
}
}(),
}
SuccessResponse(c, status)
@@ -306,6 +339,35 @@ func (h *TelegramHandler) ReloadBotConfig(c *gin.Context) {
})
}
// DebugBotConnection 调试机器人连接
func (h *TelegramHandler) DebugBotConnection(c *gin.Context) {
// 获取机器人状态信息用于调试
botUsername := h.telegramBotService.GetBotUsername()
debugInfo := map[string]interface{}{
"bot_username": botUsername,
"is_running": botUsername != "",
"timestamp": "2024-01-01T12:00:00Z", // 当前时间
"debugging_enabled": true,
"expected_logs": []string{
"[TELEGRAM:SERVICE] Telegram Bot (@username) 已启动",
"[TELEGRAM:MESSAGE] 开始监听 Telegram 消息更新...",
"[TELEGRAM:MESSAGE] 消息监听循环已启动,等待消息...",
"[TELEGRAM:MESSAGE] 收到消息: ChatID=xxx, Text='/register'",
"[TELEGRAM:MESSAGE] 处理 /register 命令 from ChatID=xxx",
},
"troubleshooting_steps": []string{
"1. 检查服务器日志中是否有 TELEGRAM 相关日志",
"2. 确认机器人已添加到群组并设为管理员",
"3. 验证 API Key 是否正确",
"4. 检查自动回复是否已启用",
"5. 重启服务器重新加载配置",
},
}
SuccessResponse(c, debugInfo)
}
// GetTelegramLogs 获取Telegram相关的日志
func (h *TelegramHandler) GetTelegramLogs(c *gin.Context) {
// 解析查询参数

View File

@@ -348,14 +348,15 @@ func main() {
api.GET("/telegram/bot-status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetBotStatus)
api.POST("/telegram/reload-config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.ReloadBotConfig)
api.POST("/telegram/test-message", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.TestBotMessage)
api.GET("/telegram/debug-connection", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.DebugBotConnection)
api.GET("/telegram/channels", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetChannels)
api.POST("/telegram/channels", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.CreateChannel)
api.PUT("/telegram/channels/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.UpdateChannel)
api.DELETE("/telegram/channels/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.DeleteChannel)
api.POST("/telegram/webhook", telegramHandler.HandleWebhook)
api.GET("/telegram/logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetTelegramLogs)
api.GET("/telegram/logs/stats", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetTelegramLogStats)
api.POST("/telegram/logs/clear", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.ClearTelegramLogs)
api.POST("/telegram/webhook", telegramHandler.HandleWebhook)
}
// 静态文件服务

View File

@@ -89,7 +89,7 @@ func (m *MeilisearchService) HealthCheck() error {
// 使用官方SDK的健康检查
_, err := m.client.Health()
if err != nil {
utils.Error("Meilisearch健康检查失败: %v", err)
// utils.Error("Meilisearch健康检查失败: %v", err)
return fmt.Errorf("Meilisearch健康检查失败: %v", err)
}

View File

@@ -2,12 +2,15 @@ package services
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/utils"
"golang.org/x/net/proxy"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/robfig/cron/v3"
@@ -16,6 +19,9 @@ import (
type TelegramBotService interface {
Start() error
Stop() error
IsRunning() bool
ReloadConfig() error
GetRuntimeStatus() map[string]interface{}
ValidateApiKey(apiKey string) (bool, map[string]interface{}, error)
GetBotUsername() string
SendMessage(chatID int64, text string) error
@@ -42,6 +48,12 @@ type TelegramBotConfig struct {
AutoReplyTemplate string
AutoDeleteEnabled bool
AutoDeleteInterval int // 分钟
ProxyEnabled bool
ProxyType string // http, https, socks5
ProxyHost string
ProxyPort int
ProxyUsername string
ProxyPassword string
}
func NewTelegramBotService(
@@ -75,6 +87,13 @@ func (s *TelegramBotServiceImpl) loadConfig() error {
s.config.AutoReplyTemplate = "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
s.config.AutoDeleteEnabled = false
s.config.AutoDeleteInterval = 60
// 初始化代理默认值
s.config.ProxyEnabled = false
s.config.ProxyType = "http"
s.config.ProxyHost = ""
s.config.ProxyPort = 8080
s.config.ProxyUsername = ""
s.config.ProxyPassword = ""
for _, config := range configs {
switch config.Key {
@@ -100,6 +119,26 @@ func (s *TelegramBotServiceImpl) loadConfig() error {
fmt.Sscanf(config.Value, "%d", &s.config.AutoDeleteInterval)
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (AutoDeleteInterval: %d)", config.Key, config.Value, s.config.AutoDeleteInterval)
case "telegram_proxy_enabled":
s.config.ProxyEnabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyEnabled: %v)", config.Key, config.Value, s.config.ProxyEnabled)
case "telegram_proxy_type":
s.config.ProxyType = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyType: %s)", config.Key, config.Value, s.config.ProxyType)
case "telegram_proxy_host":
s.config.ProxyHost = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
case "telegram_proxy_port":
if config.Value != "" {
fmt.Sscanf(config.Value, "%d", &s.config.ProxyPort)
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyPort: %d)", config.Key, config.Value, s.config.ProxyPort)
case "telegram_proxy_username":
s.config.ProxyUsername = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
case "telegram_proxy_password":
s.config.ProxyPassword = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
default:
utils.Debug("未知配置: %s = %s", config.Key, config.Value)
}
@@ -128,11 +167,73 @@ func (s *TelegramBotServiceImpl) Start() error {
}
// 创建 Bot 实例
bot, err := tgbotapi.NewBotAPI(s.config.ApiKey)
var bot *tgbotapi.BotAPI
if s.config.ProxyEnabled && s.config.ProxyHost != "" {
// 配置代理
utils.Info("[TELEGRAM:PROXY] 配置代理: %s://%s:%d", s.config.ProxyType, s.config.ProxyHost, s.config.ProxyPort)
var httpClient *http.Client
if s.config.ProxyType == "socks5" {
// SOCKS5 代理配置
var auth *proxy.Auth
if s.config.ProxyUsername != "" {
auth = &proxy.Auth{
User: s.config.ProxyUsername,
Password: s.config.ProxyPassword,
}
}
dialer, proxyErr := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort), auth, proxy.Direct)
if proxyErr != nil {
return fmt.Errorf("创建 SOCKS5 代理失败: %v", proxyErr)
}
httpClient = &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
},
Timeout: 30 * time.Second,
}
} else {
// HTTP/HTTPS 代理配置
proxyURL := &url.URL{
Scheme: s.config.ProxyType,
Host: fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort),
User: nil,
}
if s.config.ProxyUsername != "" {
proxyURL.User = url.UserPassword(s.config.ProxyUsername, s.config.ProxyPassword)
}
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 30 * time.Second,
}
}
botInstance, botErr := tgbotapi.NewBotAPIWithClient(s.config.ApiKey, tgbotapi.APIEndpoint, httpClient)
if botErr != nil {
return fmt.Errorf("创建 Telegram Bot (代理模式) 失败: %v", botErr)
}
bot = botInstance
utils.Info("[TELEGRAM:PROXY] Telegram Bot 已配置代理连接")
} else {
// 直接连接(无代理)
var err error
bot, err = tgbotapi.NewBotAPI(s.config.ApiKey)
if err != nil {
return fmt.Errorf("创建 Telegram Bot 失败: %v", err)
}
utils.Info("[TELEGRAM:PROXY] Telegram Bot 使用直连模式")
}
s.bot = bot
s.isRunning = true
@@ -168,13 +269,104 @@ func (s *TelegramBotServiceImpl) Stop() error {
return nil
}
// IsRunning 检查机器人服务是否正在运行
func (s *TelegramBotServiceImpl) IsRunning() bool {
return s.isRunning && s.bot != nil
}
// ReloadConfig 重新加载机器人配置
func (s *TelegramBotServiceImpl) ReloadConfig() error {
utils.Info("[TELEGRAM:SERVICE] 开始重新加载配置...")
// 重新加载配置
if err := s.loadConfig(); err != nil {
utils.Error("[TELEGRAM:SERVICE] 重新加载配置失败: %v", err)
return fmt.Errorf("重新加载配置失败: %v", err)
}
utils.Info("[TELEGRAM:SERVICE] 配置重新加载完成: Enabled=%v, AutoReplyEnabled=%v",
s.config.Enabled, s.config.AutoReplyEnabled)
return nil
}
// GetRuntimeStatus 获取机器人运行时状态
func (s *TelegramBotServiceImpl) GetRuntimeStatus() map[string]interface{} {
status := map[string]interface{}{
"is_running": s.IsRunning(),
"bot_initialized": s.bot != nil,
"config_loaded": s.config != nil,
"cron_running": s.cronScheduler != nil,
"username": "",
"uptime": 0,
}
if s.bot != nil {
status["username"] = s.GetBotUsername()
}
return status
}
// ValidateApiKey 验证 API Key
func (s *TelegramBotServiceImpl) ValidateApiKey(apiKey string) (bool, map[string]interface{}, error) {
if apiKey == "" {
return false, nil, fmt.Errorf("API Key 不能为空")
}
bot, err := tgbotapi.NewBotAPI(apiKey)
var bot *tgbotapi.BotAPI
var err error
// 如果启用了代理,使用代理验证
if s.config.ProxyEnabled && s.config.ProxyHost != "" {
var httpClient *http.Client
if s.config.ProxyType == "socks5" {
var auth *proxy.Auth
if s.config.ProxyUsername != "" {
auth = &proxy.Auth{
User: s.config.ProxyUsername,
Password: s.config.ProxyPassword,
}
}
dialer, proxyErr := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort), auth, proxy.Direct)
if proxyErr != nil {
// 如果代理失败,回退到直连
utils.Warn("[TELEGRAM:PROXY] SOCKS5 代理验证失败,回退到直连: %v", proxyErr)
bot, err = tgbotapi.NewBotAPI(apiKey)
} else {
httpClient = &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
},
Timeout: 10 * time.Second,
}
bot, err = tgbotapi.NewBotAPIWithClient(apiKey, tgbotapi.APIEndpoint, httpClient)
}
} else {
proxyURL := &url.URL{
Scheme: s.config.ProxyType,
Host: fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort),
User: nil,
}
if s.config.ProxyUsername != "" {
proxyURL.User = url.UserPassword(s.config.ProxyUsername, s.config.ProxyPassword)
}
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 10 * time.Second,
}
bot, err = tgbotapi.NewBotAPIWithClient(apiKey, tgbotapi.APIEndpoint, httpClient)
}
} else {
// 直连验证
bot, err = tgbotapi.NewBotAPI(apiKey)
}
if err != nil {
return false, nil, fmt.Errorf("无效的 API Key: %v", err)
}
@@ -323,17 +515,50 @@ func (s *TelegramBotServiceImpl) handleSearchRequest(message *tgbotapi.Message)
return
}
// 这里使用简单的资源搜索,实际项目中需要完善搜索逻辑
// resources, err := s.resourceRepo.Search(query, nil, 0, 10)
// 暂时模拟一个搜索结果
results := []string{
fmt.Sprintf("🔍 搜索关键词: %s", query),
"暂无相关资源,请尝试其他关键词。",
"",
fmt.Sprintf("💡 提示:如需精确搜索,请使用更具体的关键词。"),
utils.Info("[TELEGRAM:SEARCH] 处理搜索请求: %s", query)
// 使用资源仓库进行搜索
resources, total, err := s.resourceRepo.Search(query, nil, 1, 5) // 限制为5个结果
if err != nil {
utils.Error("[TELEGRAM:SEARCH] 搜索失败: %v", err)
s.sendReply(message, "搜索服务暂时不可用,请稍后重试")
return
}
if total == 0 {
response := fmt.Sprintf("🔍 *搜索结果*\n\n关键词: `%s`\n\n❌ 未找到相关资源\n\n💡 建议:\n• 尝试使用更通用的关键词\n• 检查拼写是否正确\n• 减少关键词数量", query)
s.sendReply(message, response)
return
}
// 构建搜索结果消息
resultText := fmt.Sprintf("🔍 *搜索结果*\n\n关键词: `%s`\n总共找到: %d 个资源\n\n", query, total)
// 显示前5个结果
for i, resource := range resources {
if i >= 5 {
break
}
title := resource.Title
if len(title) > 50 {
title = title[:47] + "..."
}
description := resource.Description
if len(description) > 100 {
description = description[:97] + "..."
}
resultText += fmt.Sprintf("%d. *%s*\n%s\n\n", i+1, title, description)
}
// 如果有更多结果,添加提示
if total > 5 {
resultText += fmt.Sprintf("... 还有 %d 个结果\n\n", total-5)
resultText += "💡 如需查看更多结果,请访问网站搜索"
}
resultText := strings.Join(results, "\n")
s.sendReply(message, resultText)
}

1
web/components.d.ts vendored
View File

@@ -51,6 +51,7 @@ declare module 'vue' {
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NThing: typeof import('naive-ui')['NThing']
NTimePicker: typeof import('naive-ui')['NTimePicker']
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
NVirtualList: typeof import('naive-ui')['NVirtualList']

View File

@@ -13,14 +13,36 @@
<div class="space-y-4">
<!-- 机器人启用开关 -->
<div class="flex items-center justify-between">
<div>
<div class="flex-1">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">启用 Telegram 机器人</label>
<p class="text-xs text-gray-500 dark:text-gray-400">开启后机器人将开始工作</p>
</div>
<div class="flex items-center space-x-3">
<n-switch
v-model:value="telegramBotConfig.bot_enabled"
@update:value="handleBotConfigChange"
/>
<!-- 运行状态指示器 -->
<div v-if="botStatus" class="flex items-center space-x-2">
<n-tag
:type="botStatus.overall_status ? 'success' : (telegramBotConfig.bot_enabled ? 'warning' : 'default')"
size="small"
class="min-w-16 text-center"
>
{{ botStatus.status_text }}
</n-tag>
<n-button
size="small"
@click="refreshBotStatus"
:loading="statusRefreshing"
circle
>
<template #icon>
<i class="fas fa-sync-alt"></i>
</template>
</n-button>
</div>
</div>
</div>
<!-- API Key 配置 -->
@@ -83,12 +105,13 @@
</div>
<n-switch
v-model:value="telegramBotConfig.auto_reply_enabled"
:disabled="telegramBotConfig.bot_enabled"
@update:value="handleBotConfigChange"
/>
</div>
<!-- 回复模板 -->
<div v-if="telegramBotConfig.auto_reply_enabled">
<div v-if="telegramBotConfig.auto_reply_enabled || telegramBotConfig.bot_enabled">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">回复模板</label>
<n-input
v-model:value="telegramBotConfig.auto_reply_template"
@@ -124,6 +147,107 @@
</div>
</div>
<!-- 代理配置 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center mb-6">
<div class="w-8 h-8 bg-purple-600 text-white rounded-full flex items-center justify-center mr-3">
<span class="text-sm font-bold">3</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">代理配置</h3>
</div>
<div class="space-y-4">
<!-- 启用代理 -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">启用代理</label>
<p class="text-xs text-gray-500 dark:text-gray-400">通过代理服务器连接 Telegram API</p>
</div>
<n-switch
v-model:value="telegramBotConfig.proxy_enabled"
@update:value="handleBotConfigChange"
/>
</div>
<!-- 代理设置 -->
<div v-if="telegramBotConfig.proxy_enabled" class="space-y-4">
<!-- 代理类型 -->
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">代理类型</label>
<n-select
v-model:value="telegramBotConfig.proxy_type"
:options="[
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' },
{ label: 'SOCKS5', value: 'socks5' }
]"
placeholder="选择代理类型"
@update:value="handleBotConfigChange"
/>
</div>
<!-- 代理主机和端口 -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">代理主机</label>
<n-input
v-model:value="telegramBotConfig.proxy_host"
placeholder="例如: 127.0.0.1 或 proxy.example.com"
@input="handleBotConfigChange"
/>
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">代理端口</label>
<n-input-number
v-model:value="telegramBotConfig.proxy_port"
:min="1"
:max="65535"
placeholder="例如: 8080"
@update:value="handleBotConfigChange"
/>
</div>
</div>
<!-- 代理认证 -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">用户名 (可选)</label>
<n-input
v-model:value="telegramBotConfig.proxy_username"
placeholder="代理用户名"
@input="handleBotConfigChange"
/>
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">密码 (可选)</label>
<n-input
v-model:value="telegramBotConfig.proxy_password"
type="password"
placeholder="代理密码"
@input="handleBotConfigChange"
/>
</div>
</div>
<!-- 代理状态提示 -->
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<div class="flex items-start space-x-3">
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mt-1"></i>
<div>
<h4 class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">代理配置说明</h4>
<ul class="text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li> HTTP/HTTPS 代理支持基本的认证</li>
<li> SOCKS5 代理支持用户名/密码认证</li>
<li> 配置完成后需要重启机器人服务</li>
<li> 确保代理服务器稳定可靠</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 频道和群组管理 -->
<div v-if="telegramBotConfig.bot_enabled" class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center justify-between mb-6">
@@ -147,15 +271,6 @@
</template>
刷新
</n-button>
<n-button
@click="testBotConnection"
:loading="testingConnection"
>
<template #icon>
<i class="fas fa-robot"></i>
</template>
测试连接
</n-button>
<n-button
type="primary"
@click="showRegisterChannelDialog = true"
@@ -238,7 +353,24 @@
</div>
</div>
</div>
<div class="flex justify-end p-2">
<div class="flex justify-end p-2 gap-2">
<n-button
@click="testBotConnection"
:loading="testingConnection"
>
<template #icon>
<i class="fas fa-robot"></i>
</template>
测试连接
</n-button>
<n-button
@click="debugBotConnection"
>
<template #icon>
<i class="fas fa-bug"></i>
</template>
调试
</n-button>
<n-button @click="showLogDrawer = true">
<template #icon>
<i class="fas fa-list-alt"></i>
@@ -261,9 +393,9 @@
v-model:show="showRegisterChannelDialog"
preset="card"
title="注册频道/群组"
size="huge"
:bordered="false"
:segmented="false"
:style="{ width: '800px' }"
>
<div class="space-y-6">
<div class="text-sm text-gray-600 dark:text-gray-400">
@@ -305,6 +437,148 @@
</div>
</n-modal>
<!-- 编辑频道对话框 -->
<n-modal
v-model:show="showEditChannelDialog"
preset="card"
:title="`编辑频道 - ${editingChannel?.chat_name || ''}`"
size="large"
:bordered="false"
:segmented="false"
>
<div v-if="editingChannel" class="space-y-6">
<!-- 频道基本信息 -->
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-2">频道信息</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-600 dark:text-gray-400">频道名称:</span>
<span class="ml-2 text-gray-900 dark:text-white">{{ editingChannel.chat_name }}</span>
</div>
<div>
<span class="text-gray-600 dark:text-gray-400">频道ID:</span>
<span class="ml-2 text-gray-900 dark:text-white">{{ editingChannel.chat_id }}</span>
</div>
<div>
<span class="text-gray-600 dark:text-gray-400">类型:</span>
<span class="ml-2 text-gray-900 dark:text-white">{{ editingChannel.chat_type === 'channel' ? '频道' : '群组' }}</span>
</div>
<div>
<span class="text-gray-600 dark:text-gray-400">状态:</span>
<n-tag :type="editingChannel.is_active ? 'success' : 'warning'" size="small" class="ml-2">
{{ editingChannel.is_active ? '活跃' : '非活跃' }}
</n-tag>
</div>
</div>
</div>
<!-- 推送设置 -->
<div class="space-y-4">
<h4 class="text-base font-medium text-gray-900 dark:text-white">推送设置</h4>
<!-- 启用推送 -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">启用推送</label>
<p class="text-xs text-gray-500 dark:text-gray-400">是否向此频道推送内容</p>
</div>
<n-switch
v-model:value="editingChannel.push_enabled"
/>
</div>
<!-- 推送频率 -->
<div v-if="editingChannel.push_enabled">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">推送频率</label>
<n-select
v-model:value="editingChannel.push_frequency"
:options="[
{ label: '每5分钟', value: 0.0833 },
{ label: '每10分钟', value: 0.1667 },
{ label: '每15分钟', value: 0.25 },
{ label: '每30分钟', value: 0.5 },
{ label: '每小时', value: 1 },
{ label: '每2小时', value: 2 },
{ label: '每3小时', value: 3 },
{ label: '每6小时', value: 6 },
{ label: '每12小时', value: 12 },
{ label: '每天', value: 24 },
{ label: '每2天', value: 48 },
{ label: '每周', value: 168 }
]"
placeholder="选择推送频率"
/>
</div>
<!-- 推送时间段 -->
<div v-if="editingChannel.push_enabled">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">推送时间段</label>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-xs text-gray-600 dark:text-gray-400">开始时间</label>
<n-time-picker
v-model:value="editingChannel.push_start_time"
format="HH:mm"
placeholder="选择开始时间"
clearable
/>
</div>
<div>
<label class="text-xs text-gray-600 dark:text-gray-400">结束时间</label>
<n-time-picker
v-model:value="editingChannel.push_end_time"
format="HH:mm"
placeholder="选择结束时间"
clearable
/>
</div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
留空表示全天推送不设置时间限制
</p>
</div>
<!-- 内容分类 -->
<div v-if="editingChannel.push_enabled">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">内容分类</label>
<n-input
v-model:value="editingChannel.content_categories"
placeholder="输入内容分类,多个用逗号分隔 (如: 电影,电视剧,动漫)"
type="textarea"
:rows="2"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">留空表示推送所有分类的内容</p>
</div>
<!-- 标签过滤 -->
<div v-if="editingChannel.push_enabled">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">标签过滤</label>
<n-input
v-model:value="editingChannel.content_tags"
placeholder="输入标签关键词,多个用逗号分隔 (如: 高清,1080p,蓝光)"
type="textarea"
:rows="2"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">留空表示推送所有标签的内容</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-600">
<n-button @click="showEditChannelDialog = false">
取消
</n-button>
<n-button
type="primary"
:loading="savingChannel"
@click="saveChannelSettings"
>
保存设置
</n-button>
</div>
</div>
</n-modal>
<!-- Telegram 日志抽屉 -->
<n-drawer
v-model:show="showLogDrawer"
@@ -313,9 +587,9 @@
placement="right"
>
<n-drawer-content>
<div class="space-y-4">
<div class="space-y-4 h-full overflow-y-auto flex flex-col">
<!-- 日志控制栏 -->
<div class="flex items-center justify-between">
<div class="flex-0 flex items-center justify-between">
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-600 dark:text-gray-400">时间范围:</span>
<n-select
@@ -342,7 +616,7 @@
</div>
<!-- 日志列表 -->
<div class="space-y-2 max-h-96 overflow-y-auto">
<div class="h-1 flex-1 space-y-2 overflow-y-auto">
<div v-if="telegramLogs.length === 0 && !loadingLogs" class="text-center py-8">
<i class="fas fa-list-alt text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">暂无日志</h3>
@@ -376,7 +650,7 @@
</div>
<!-- 日志统计 -->
<div class="flex justify-between items-center text-sm text-gray-600 dark:text-gray-400">
<div class="flex-0 flex justify-between items-center text-sm text-gray-600 dark:text-gray-400">
<span>显示 {{ telegramLogs.length }} 条日志</span>
<span v-if="telegramLogs.length > 0">
加载于 {{ formatTimestamp(new Date().toISOString()) }}
@@ -400,6 +674,12 @@ const telegramBotConfig = ref<any>({
auto_reply_template: '您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。',
auto_delete_enabled: false,
auto_delete_interval: 60,
proxy_enabled: false,
proxy_type: 'http',
proxy_host: '',
proxy_port: 8080,
proxy_username: '',
proxy_password: '',
})
const telegramChannels = ref<any[]>([])
@@ -408,12 +688,19 @@ const savingBotConfig = ref(false)
const apiKeyValidationResult = ref<any>(null)
const hasBotConfigChanges = ref(false)
const showRegisterChannelDialog = ref(false)
const showEditChannelDialog = ref(false)
const showLogDrawer = ref(false)
const refreshingChannels = ref(false)
const testingConnection = ref(false)
const telegramLogs = ref<any[]>([])
const loadingLogs = ref(false)
const logHours = ref(24)
const editingChannel = ref<any>(null)
const savingChannel = ref(false)
// 机器人状态相关变量
const botStatus = ref<any>(null)
const statusRefreshing = ref(false)
// 使用统一的Telegram API
const telegramApi = useTelegramApi()
@@ -423,6 +710,10 @@ const fetchTelegramConfig = async () => {
try {
const data = await telegramApi.getBotConfig() as any
if (data) {
// 确保当机器人启用时自动回复始终为true
if (data.bot_enabled) {
data.auto_reply_enabled = true
}
telegramBotConfig.value = { ...data }
}
} catch (error) {
@@ -434,9 +725,12 @@ const fetchTelegramConfig = async () => {
const fetchTelegramChannels = async () => {
try {
const data = await telegramApi.getChannels() as any[]
if (data) {
telegramChannels.value = data
if (data !== undefined && data !== null) {
telegramChannels.value = Array.isArray(data) ? data : []
} else {
telegramChannels.value = []
}
console.log('频道列表已更新:', telegramChannels.value.length, '个频道')
} catch (error: any) {
console.error('获取频道列表失败:', error)
// 如果是表不存在的错误,给出更友好的提示
@@ -460,6 +754,10 @@ const fetchTelegramChannels = async () => {
// 处理机器人配置变更
const handleBotConfigChange = () => {
// 当机器人启用时自动回复必须为true
if (telegramBotConfig.value.bot_enabled) {
telegramBotConfig.value.auto_reply_enabled = true
}
hasBotConfigChanges.value = true
}
@@ -550,17 +848,24 @@ const saveBotConfig = async () => {
const config = telegramBotConfig.value as any
configRequest.bot_enabled = config.bot_enabled
configRequest.bot_api_key = config.bot_api_key
configRequest.auto_reply_enabled = config.auto_reply_enabled
// 当机器人启用时自动回复必须为true
configRequest.auto_reply_enabled = config.bot_enabled ? true : config.auto_reply_enabled
configRequest.auto_reply_template = config.auto_reply_template
configRequest.auto_delete_enabled = config.auto_delete_enabled
configRequest.auto_delete_interval = config.auto_delete_interval
configRequest.proxy_enabled = config.proxy_enabled
configRequest.proxy_type = config.proxy_type
configRequest.proxy_host = config.proxy_host
configRequest.proxy_port = config.proxy_port
configRequest.proxy_username = config.proxy_username
configRequest.proxy_password = config.proxy_password
}
await telegramApi.updateBotConfig(configRequest)
notification.success({
content: '配置保存成功',
duration: 2000
content: '配置保存成功,机器人服务已重新加载配置',
duration: 3000
})
hasBotConfigChanges.value = false
// 重新获取配置以确保同步
@@ -577,8 +882,8 @@ const saveBotConfig = async () => {
// 编辑频道
const editChannel = (channel: any) => {
// TODO: 实现编辑频道功能
console.log('编辑频道:', channel)
editingChannel.value = { ...channel }
showEditChannelDialog.value = true
}
// 注销频道(带确认)
@@ -612,6 +917,9 @@ const performUnregisterChannel = async (channel: any) => {
duration: 3000
})
// 添加短暂延迟确保数据库事务完成
await new Promise(resolve => setTimeout(resolve, 500))
// 重新获取频道列表更新UI
await fetchTelegramChannels()
@@ -694,14 +1002,20 @@ const testBotConnection = async () => {
testingConnection.value = true
try {
const data = await telegramApi.getBotStatus() as any
if (data && data.service_running) {
if (data && data.overall_status) {
notification.success({
content: `机器人连接正常!用户名:@${data.bot_username}`,
content: `机器人连接正常!用户名:@${data.runtime?.username || '未知'}`,
duration: 3000
})
} else {
let warningMessage = '机器人服务未运行或未配置'
if (data?.config?.enabled) {
warningMessage = '机器人已启用但未运行,请检查 API Key 配置'
} else if (!data?.config?.api_key_configured) {
warningMessage = 'API Key 未配置,请先配置有效的 API Key'
}
notification.warning({
content: '机器人服务未运行或未配置',
content: warningMessage,
duration: 3000
})
}
@@ -814,10 +1128,115 @@ const getCategoryLabel = (category: string): string => {
const notification = useNotification()
const dialog = useDialog()
// 保存频道设置
const saveChannelSettings = async () => {
if (!editingChannel.value) return
savingChannel.value = true
try {
const updateData = {
chat_name: editingChannel.value.chat_name,
chat_type: editingChannel.value.chat_type,
push_enabled: editingChannel.value.push_enabled,
push_frequency: editingChannel.value.push_frequency,
content_categories: editingChannel.value.content_categories,
content_tags: editingChannel.value.content_tags,
is_active: editingChannel.value.is_active,
push_start_time: editingChannel.value.push_start_time,
push_end_time: editingChannel.value.push_end_time
}
await telegramApi.updateChannel(editingChannel.value.id, updateData)
notification.success({
content: `频道 "${editingChannel.value.chat_name}" 设置已更新`,
duration: 3000
})
// 关闭对话框
showEditChannelDialog.value = false
// 刷新频道列表
await fetchTelegramChannels()
} catch (error: any) {
notification.error({
content: `保存频道设置失败: ${error?.message || '请稍后重试'}`,
duration: 3000
})
} finally {
savingChannel.value = false
}
}
// 刷新机器人状态
const refreshBotStatus = async () => {
statusRefreshing.value = true
try {
const data = await telegramApi.getBotStatus() as any
botStatus.value = data
notification.success({
content: '机器人状态已刷新',
duration: 2000
})
} catch (error: any) {
notification.error({
content: '刷新状态失败:' + (error?.message || '请稍后重试'),
duration: 3000
})
} finally {
statusRefreshing.value = false
}
}
// 调试机器人连接
const debugBotConnection = async () => {
try {
const data = await telegramApi.getBotStatus() as any
let message = `🔍 **Telegram 机器人调试信息**\n\n`
message += `🤖 机器人状态: ${data.runtime?.is_running ? '✅ 运行中' : '❌ 未运行'}\n`
message += `👤 用户名: @${data.runtime?.username || '未知'}\n`
message += `⚡ 工作模式: 长轮询\n\n`
message += `📋 **故障排查步骤:**\n`
message += `1. 检查服务器控制台是否有 [TELEGRAM] 日志\n`
message += `2. 确认机器人已添加到群组并设为管理员\n`
message += `3. 验证 API Key 配置是否正确\n`
message += `4. 确认自动回复功能已启用\n`
message += `5. 重启服务器重新加载配置\n\n`
message += `🔧 **预期日志输出:**\n`
message += `• [TELEGRAM:SERVICE] Telegram Bot (@用户名) 已启动\n`
message += `• [TELEGRAM:MESSAGE] 收到消息: ChatID=xxx, Text='/register'\n`
message += `• [TELEGRAM:MESSAGE] 处理 /register 命令 from ChatID=xxx\n`
message += `• [TELEGRAM:MESSAGE:SUCCESS] 消息发送成功\n\n`
message += `💡 **如果没有日志输出:**\n`
message += `• 服务器可能未正确启动机器人服务\n`
message += `• API Key 可能有误\n`
message += `• 数据库配置可能有问题`
notification.info({
title: '🤖 机器人连接调试',
content: message,
duration: 15000,
keepAliveOnHover: true
})
} catch (error: any) {
notification.error({
title: '🔧 调试失败',
content: `无法获取机器人状态: ${error?.message || '网络错误或服务未运行'}`,
duration: 5000
})
}
}
// 页面加载时获取配置
onMounted(async () => {
await fetchTelegramConfig()
await fetchTelegramChannels()
await refreshBotStatus() // 初始化机器人状态
console.log('Telegram 机器人标签已加载')
})

View File

@@ -269,6 +269,7 @@ export const useTelegramApi = () => {
const updateBotConfig = (data: any) => useApiFetch('/telegram/bot-config', { method: 'PUT', body: data }).then(parseApiResponse)
const validateApiKey = (data: any) => useApiFetch('/telegram/validate-api-key', { method: 'POST', body: data }).then(parseApiResponse)
const getBotStatus = () => useApiFetch('/telegram/bot-status').then(parseApiResponse)
const debugBotConnection = () => useApiFetch('/telegram/debug-connection').then(parseApiResponse)
const reloadBotConfig = () => useApiFetch('/telegram/reload-config', { method: 'POST' }).then(parseApiResponse)
const testBotMessage = (data: any) => useApiFetch('/telegram/test-message', { method: 'POST', body: data }).then(parseApiResponse)
const getChannels = () => useApiFetch('/telegram/channels').then(parseApiResponse)
@@ -283,6 +284,7 @@ export const useTelegramApi = () => {
updateBotConfig,
validateApiKey,
getBotStatus,
debugBotConnection,
reloadBotConfig,
testBotMessage,
getChannels,