add: tgbot

This commit is contained in:
ctwj
2025-09-16 00:07:02 +08:00
parent bada678490
commit 8ced3d0327
13 changed files with 1667 additions and 33 deletions

View File

@@ -0,0 +1,181 @@
package converter
import (
"fmt"
"time"
"github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity"
)
// TelegramChannelToResponse 将TelegramChannel实体转换为响应DTO
func TelegramChannelToResponse(channel entity.TelegramChannel) dto.TelegramChannelResponse {
return dto.TelegramChannelResponse{
ID: channel.ID,
ChatID: channel.ChatID,
ChatName: channel.ChatName,
ChatType: channel.ChatType,
PushEnabled: channel.PushEnabled,
PushFrequency: channel.PushFrequency,
ContentCategories: channel.ContentCategories,
ContentTags: channel.ContentTags,
IsActive: channel.IsActive,
LastPushAt: channel.LastPushAt,
RegisteredBy: channel.RegisteredBy,
RegisteredAt: channel.RegisteredAt,
}
}
// TelegramChannelsToResponse 将TelegramChannel实体列表转换为响应DTO列表
func TelegramChannelsToResponse(channels []entity.TelegramChannel) []dto.TelegramChannelResponse {
var responses []dto.TelegramChannelResponse
for _, channel := range channels {
responses = append(responses, TelegramChannelToResponse(channel))
}
return responses
}
// RequestToTelegramChannel 将请求DTO转换为TelegramChannel实体
func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy string) entity.TelegramChannel {
return entity.TelegramChannel{
ChatID: req.ChatID,
ChatName: req.ChatName,
ChatType: req.ChatType,
PushEnabled: req.PushEnabled,
PushFrequency: req.PushFrequency,
ContentCategories: req.ContentCategories,
ContentTags: req.ContentTags,
IsActive: req.IsActive,
RegisteredBy: registeredBy,
RegisteredAt: time.Now(),
}
}
// TelegramBotConfigToResponse 将Telegram bot配置转换为响应DTO
func TelegramBotConfigToResponse(
botEnabled bool,
botApiKey string,
autoReplyEnabled bool,
autoReplyTemplate string,
autoDeleteEnabled bool,
autoDeleteInterval int,
) dto.TelegramBotConfigResponse {
return dto.TelegramBotConfigResponse{
BotEnabled: botEnabled,
BotApiKey: botApiKey,
AutoReplyEnabled: autoReplyEnabled,
AutoReplyTemplate: autoReplyTemplate,
AutoDeleteEnabled: autoDeleteEnabled,
AutoDeleteInterval: autoDeleteInterval,
}
}
// SystemConfigToTelegramBotConfig 将系统配置转换为Telegram bot配置响应
func SystemConfigToTelegramBotConfig(configs []entity.SystemConfig) dto.TelegramBotConfigResponse {
botEnabled := false
botApiKey := ""
autoReplyEnabled := true
autoReplyTemplate := "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
autoDeleteEnabled := false
autoDeleteInterval := 60
for _, config := range configs {
switch config.Key {
case entity.ConfigKeyTelegramBotEnabled:
botEnabled = config.Value == "true"
case entity.ConfigKeyTelegramBotApiKey:
botApiKey = config.Value
case entity.ConfigKeyTelegramAutoReplyEnabled:
autoReplyEnabled = config.Value == "true"
case entity.ConfigKeyTelegramAutoReplyTemplate:
autoReplyTemplate = config.Value
case entity.ConfigKeyTelegramAutoDeleteEnabled:
autoDeleteEnabled = config.Value == "true"
case entity.ConfigKeyTelegramAutoDeleteInterval:
if config.Value != "" {
// 简单解析整数,这里可以改进错误处理
var val int
if _, err := fmt.Sscanf(config.Value, "%d", &val); err == nil {
autoDeleteInterval = val
}
}
}
}
return TelegramBotConfigToResponse(
botEnabled,
botApiKey,
autoReplyEnabled,
autoReplyTemplate,
autoDeleteEnabled,
autoDeleteInterval,
)
}
// TelegramBotConfigRequestToSystemConfigs 将Telegram bot配置请求转换为系统配置实体列表
func TelegramBotConfigRequestToSystemConfigs(req dto.TelegramBotConfigRequest) []entity.SystemConfig {
configs := []entity.SystemConfig{}
if req.BotEnabled != nil {
configs = append(configs, entity.SystemConfig{
Key: entity.ConfigKeyTelegramBotEnabled,
Value: boolToString(*req.BotEnabled),
Type: entity.ConfigTypeBool,
})
}
if req.BotApiKey != nil {
configs = append(configs, entity.SystemConfig{
Key: entity.ConfigKeyTelegramBotApiKey,
Value: *req.BotApiKey,
Type: entity.ConfigTypeString,
})
}
if req.AutoReplyEnabled != nil {
configs = append(configs, entity.SystemConfig{
Key: entity.ConfigKeyTelegramAutoReplyEnabled,
Value: boolToString(*req.AutoReplyEnabled),
Type: entity.ConfigTypeBool,
})
}
if req.AutoReplyTemplate != nil {
configs = append(configs, entity.SystemConfig{
Key: entity.ConfigKeyTelegramAutoReplyTemplate,
Value: *req.AutoReplyTemplate,
Type: entity.ConfigTypeString,
})
}
if req.AutoDeleteEnabled != nil {
configs = append(configs, entity.SystemConfig{
Key: entity.ConfigKeyTelegramAutoDeleteEnabled,
Value: boolToString(*req.AutoDeleteEnabled),
Type: entity.ConfigTypeBool,
})
}
if req.AutoDeleteInterval != nil {
configs = append(configs, entity.SystemConfig{
Key: entity.ConfigKeyTelegramAutoDeleteInterval,
Value: intToString(*req.AutoDeleteInterval),
Type: entity.ConfigTypeInt,
})
}
return configs
}
// 辅助函数:布尔转换为字符串
func boolToString(b bool) string {
if b {
return "true"
}
return "false"
}
// 辅助函数:整数转换为字符串
func intToString(i int) string {
return fmt.Sprintf("%d", i)
}

View File

@@ -0,0 +1,63 @@
package dto
import "time"
// TelegramChannelRequest 创建/更新 Telegram 频道/群组请求
type TelegramChannelRequest struct {
ChatID int64 `json:"chat_id" binding:"required"`
ChatName string `json:"chat_name" binding:"required"`
ChatType string `json:"chat_type" binding:"required"` // channel 或 group
PushEnabled bool `json:"push_enabled"`
PushFrequency int `json:"push_frequency"`
ContentCategories string `json:"content_categories"`
ContentTags string `json:"content_tags"`
IsActive bool `json:"is_active"`
}
// TelegramChannelResponse Telegram 频道/群组响应
type TelegramChannelResponse struct {
ID uint `json:"id"`
ChatID int64 `json:"chat_id"`
ChatName string `json:"chat_name"`
ChatType string `json:"chat_type"`
PushEnabled bool `json:"push_enabled"`
PushFrequency int `json:"push_frequency"`
ContentCategories string `json:"content_categories"`
ContentTags string `json:"content_tags"`
IsActive bool `json:"is_active"`
LastPushAt *time.Time `json:"last_push_at"`
RegisteredBy string `json:"registered_by"`
RegisteredAt time.Time `json:"registered_at"`
}
// TelegramBotConfigRequest Telegram 机器人配置请求
type TelegramBotConfigRequest struct {
BotEnabled *bool `json:"bot_enabled"`
BotApiKey *string `json:"bot_api_key"`
AutoReplyEnabled *bool `json:"auto_reply_enabled"`
AutoReplyTemplate *string `json:"auto_reply_template"`
AutoDeleteEnabled *bool `json:"auto_delete_enabled"`
AutoDeleteInterval *int `json:"auto_delete_interval"`
}
// TelegramBotConfigResponse Telegram 机器人配置响应
type TelegramBotConfigResponse struct {
BotEnabled bool `json:"bot_enabled"`
BotApiKey string `json:"bot_api_key"`
AutoReplyEnabled bool `json:"auto_reply_enabled"`
AutoReplyTemplate string `json:"auto_reply_template"`
AutoDeleteEnabled bool `json:"auto_delete_enabled"`
AutoDeleteInterval int `json:"auto_delete_interval"`
}
// ValidateTelegramApiKeyRequest 验证 Telegram API Key 请求
type ValidateTelegramApiKeyRequest struct {
ApiKey string `json:"api_key" binding:"required"`
}
// ValidateTelegramApiKeyResponse 验证 Telegram API Key 响应
type ValidateTelegramApiKeyResponse struct {
Valid bool `json:"valid"`
Error string `json:"error,omitempty"`
BotInfo map[string]interface{} `json:"bot_info,omitempty"`
}

View File

@@ -42,6 +42,14 @@ const (
ConfigKeyMeilisearchPort = "meilisearch_port"
ConfigKeyMeilisearchMasterKey = "meilisearch_master_key"
ConfigKeyMeilisearchIndexName = "meilisearch_index_name"
// Telegram配置
ConfigKeyTelegramBotEnabled = "telegram_bot_enabled"
ConfigKeyTelegramBotApiKey = "telegram_bot_api_key"
ConfigKeyTelegramAutoReplyEnabled = "telegram_auto_reply_enabled"
ConfigKeyTelegramAutoReplyTemplate = "telegram_auto_reply_template"
ConfigKeyTelegramAutoDeleteEnabled = "telegram_auto_delete_enabled"
ConfigKeyTelegramAutoDeleteInterval = "telegram_auto_delete_interval"
)
// ConfigType 配置类型常量
@@ -98,6 +106,14 @@ const (
ConfigResponseFieldMeilisearchPort = "meilisearch_port"
ConfigResponseFieldMeilisearchMasterKey = "meilisearch_master_key"
ConfigResponseFieldMeilisearchIndexName = "meilisearch_index_name"
// Telegram配置字段
ConfigResponseFieldTelegramBotEnabled = "telegram_bot_enabled"
ConfigResponseFieldTelegramBotApiKey = "telegram_bot_api_key"
ConfigResponseFieldTelegramAutoReplyEnabled = "telegram_auto_reply_enabled"
ConfigResponseFieldTelegramAutoReplyTemplate = "telegram_auto_reply_template"
ConfigResponseFieldTelegramAutoDeleteEnabled = "telegram_auto_delete_enabled"
ConfigResponseFieldTelegramAutoDeleteInterval = "telegram_auto_delete_interval"
)
// ConfigDefaultValue 配置默认值常量
@@ -141,4 +157,12 @@ const (
ConfigDefaultMeilisearchPort = "7700"
ConfigDefaultMeilisearchMasterKey = ""
ConfigDefaultMeilisearchIndexName = "resources"
// Telegram配置默认值
ConfigDefaultTelegramBotEnabled = "false"
ConfigDefaultTelegramBotApiKey = ""
ConfigDefaultTelegramAutoReplyEnabled = "true"
ConfigDefaultTelegramAutoReplyTemplate = "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
ConfigDefaultTelegramAutoDeleteEnabled = "false"
ConfigDefaultTelegramAutoDeleteInterval = "60"
)

View File

@@ -0,0 +1,36 @@
package entity
import (
"time"
)
// TelegramChannel Telegram 频道/群组实体
type TelegramChannel struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Telegram 频道/群组信息
ChatID int64 `json:"chat_id" gorm:"not null;comment:Telegram 聊天ID"`
ChatName string `json:"chat_name" gorm:"size:255;not null;comment:聊天名称"`
ChatType string `json:"chat_type" gorm:"size:50;not null;comment:类型channel/group"`
// 推送配置
PushEnabled bool `json:"push_enabled" gorm:"default:true;comment:是否启用推送"`
PushFrequency int `json:"push_frequency" gorm:"default:24;comment:推送频率(小时)"`
ContentCategories string `json:"content_categories" gorm:"type:text;comment:推送的内容分类,用逗号分隔"`
ContentTags string `json:"content_tags" gorm:"type:text;comment:推送的标签,用逗号分隔"`
// 频道状态
IsActive bool `json:"is_active" gorm:"default:true;comment:是否活跃"`
LastPushAt *time.Time `json:"last_push_at" gorm:"comment:最后推送时间"`
// 注册信息
RegisteredBy string `json:"registered_by" gorm:"size:100;comment:注册者用户名"`
RegisteredAt time.Time `json:"registered_at"`
}
// TableName 指定表名
func (TelegramChannel) TableName() string {
return "telegram_channels"
}

View File

@@ -6,38 +6,40 @@ import (
// RepositoryManager Repository管理器
type RepositoryManager struct {
PanRepository PanRepository
CksRepository CksRepository
ResourceRepository ResourceRepository
CategoryRepository CategoryRepository
TagRepository TagRepository
ReadyResourceRepository ReadyResourceRepository
UserRepository UserRepository
SearchStatRepository SearchStatRepository
SystemConfigRepository SystemConfigRepository
HotDramaRepository HotDramaRepository
ResourceViewRepository ResourceViewRepository
TaskRepository TaskRepository
TaskItemRepository TaskItemRepository
FileRepository FileRepository
PanRepository PanRepository
CksRepository CksRepository
ResourceRepository ResourceRepository
CategoryRepository CategoryRepository
TagRepository TagRepository
ReadyResourceRepository ReadyResourceRepository
UserRepository UserRepository
SearchStatRepository SearchStatRepository
SystemConfigRepository SystemConfigRepository
HotDramaRepository HotDramaRepository
ResourceViewRepository ResourceViewRepository
TaskRepository TaskRepository
TaskItemRepository TaskItemRepository
FileRepository FileRepository
TelegramChannelRepository TelegramChannelRepository
}
// NewRepositoryManager 创建Repository管理器
func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
return &RepositoryManager{
PanRepository: NewPanRepository(db),
CksRepository: NewCksRepository(db),
ResourceRepository: NewResourceRepository(db),
CategoryRepository: NewCategoryRepository(db),
TagRepository: NewTagRepository(db),
ReadyResourceRepository: NewReadyResourceRepository(db),
UserRepository: NewUserRepository(db),
SearchStatRepository: NewSearchStatRepository(db),
SystemConfigRepository: NewSystemConfigRepository(db),
HotDramaRepository: NewHotDramaRepository(db),
ResourceViewRepository: NewResourceViewRepository(db),
TaskRepository: NewTaskRepository(db),
TaskItemRepository: NewTaskItemRepository(db),
FileRepository: NewFileRepository(db),
PanRepository: NewPanRepository(db),
CksRepository: NewCksRepository(db),
ResourceRepository: NewResourceRepository(db),
CategoryRepository: NewCategoryRepository(db),
TagRepository: NewTagRepository(db),
ReadyResourceRepository: NewReadyResourceRepository(db),
UserRepository: NewUserRepository(db),
SearchStatRepository: NewSearchStatRepository(db),
SystemConfigRepository: NewSystemConfigRepository(db),
HotDramaRepository: NewHotDramaRepository(db),
ResourceViewRepository: NewResourceViewRepository(db),
TaskRepository: NewTaskRepository(db),
TaskItemRepository: NewTaskItemRepository(db),
FileRepository: NewFileRepository(db),
TelegramChannelRepository: NewTelegramChannelRepository(db),
}
}

View File

@@ -0,0 +1,94 @@
package repo
import (
"time"
"github.com/ctwj/urldb/db/entity"
"gorm.io/gorm"
)
type TelegramChannelRepository interface {
BaseRepository[entity.TelegramChannel]
FindActiveChannels() ([]entity.TelegramChannel, error)
FindByChatID(chatID int64) (*entity.TelegramChannel, error)
FindByChatType(chatType string) ([]entity.TelegramChannel, error)
UpdateLastPushAt(id uint, lastPushAt time.Time) error
FindDueForPush() ([]entity.TelegramChannel, error)
}
type TelegramChannelRepositoryImpl struct {
BaseRepositoryImpl[entity.TelegramChannel]
}
func NewTelegramChannelRepository(db *gorm.DB) TelegramChannelRepository {
return &TelegramChannelRepositoryImpl{
BaseRepositoryImpl: BaseRepositoryImpl[entity.TelegramChannel]{db: db},
}
}
// 实现基类方法
func (r *TelegramChannelRepositoryImpl) Create(entity *entity.TelegramChannel) error {
return r.db.Create(entity).Error
}
func (r *TelegramChannelRepositoryImpl) Update(entity *entity.TelegramChannel) error {
return r.db.Save(entity).Error
}
func (r *TelegramChannelRepositoryImpl) Delete(id uint) error {
return r.db.Delete(&entity.TelegramChannel{}, id).Error
}
func (r *TelegramChannelRepositoryImpl) FindByID(id uint) (*entity.TelegramChannel, error) {
var channel entity.TelegramChannel
err := r.db.First(&channel, id).Error
if err != nil {
return nil, err
}
return &channel, nil
}
func (r *TelegramChannelRepositoryImpl) FindAll() ([]entity.TelegramChannel, error) {
var channels []entity.TelegramChannel
err := r.db.Order("created_at desc").Find(&channels).Error
return channels, err
}
// FindActiveChannels 查找活跃的频道/群组
func (r *TelegramChannelRepositoryImpl) FindActiveChannels() ([]entity.TelegramChannel, error) {
var channels []entity.TelegramChannel
err := r.db.Where("is_active = ? AND push_enabled = ?", true, true).Order("created_at desc").Find(&channels).Error
return channels, err
}
// FindByChatID 根据 ChatID 查找频道/群组
func (r *TelegramChannelRepositoryImpl) FindByChatID(chatID int64) (*entity.TelegramChannel, error) {
var channel entity.TelegramChannel
err := r.db.Where("chat_id = ?", chatID).First(&channel).Error
if err != nil {
return nil, err
}
return &channel, nil
}
// FindByChatType 根据类型查找频道/群组
func (r *TelegramChannelRepositoryImpl) FindByChatType(chatType string) ([]entity.TelegramChannel, error) {
var channels []entity.TelegramChannel
err := r.db.Where("chat_type = ?", chatType).Order("created_at desc").Find(&channels).Error
return channels, err
}
// UpdateLastPushAt 更新最后推送时间
func (r *TelegramChannelRepositoryImpl) UpdateLastPushAt(id uint, lastPushAt time.Time) error {
return r.db.Model(&entity.TelegramChannel{}).Where("id = ?", id).Update("last_push_at", lastPushAt).Error
}
// FindDueForPush 查找需要推送的频道/群组
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
}

2
go.mod
View File

@@ -8,9 +8,11 @@ require (
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.10.1
github.com/go-resty/resty/v2 v2.16.5
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/joho/godotenv v1.5.1
github.com/meilisearch/meilisearch-go v0.33.1
github.com/robfig/cron/v3 v3.0.1
golang.org/x/crypto v0.40.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0

4
go.sum
View File

@@ -36,6 +36,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
@@ -98,6 +100,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=

View File

@@ -0,0 +1,258 @@
package handlers
import (
"net/http"
"strconv"
"github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/services"
"github.com/gin-gonic/gin"
)
// TelegramHandler Telegram 处理器
type TelegramHandler struct {
telegramChannelRepo repo.TelegramChannelRepository
systemConfigRepo repo.SystemConfigRepository
telegramBotService services.TelegramBotService
}
// NewTelegramHandler 创建 Telegram 处理器
func NewTelegramHandler(
telegramChannelRepo repo.TelegramChannelRepository,
systemConfigRepo repo.SystemConfigRepository,
telegramBotService services.TelegramBotService,
) *TelegramHandler {
return &TelegramHandler{
telegramChannelRepo: telegramChannelRepo,
systemConfigRepo: systemConfigRepo,
telegramBotService: telegramBotService,
}
}
// GetBotConfig 获取机器人配置
func (h *TelegramHandler) GetBotConfig(c *gin.Context) {
configs, err := h.systemConfigRepo.GetOrCreateDefault()
if err != nil {
ErrorResponse(c, "获取配置失败", http.StatusInternalServerError)
return
}
botConfig := converter.SystemConfigToTelegramBotConfig(configs)
SuccessResponse(c, botConfig)
}
// UpdateBotConfig 更新机器人配置
func (h *TelegramHandler) UpdateBotConfig(c *gin.Context) {
var req dto.TelegramBotConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
// 转换为系统配置实体
configs := converter.TelegramBotConfigRequestToSystemConfigs(req)
// 保存配置
if len(configs) > 0 {
err := h.systemConfigRepo.UpsertConfigs(configs)
if err != nil {
ErrorResponse(c, "保存配置失败", http.StatusInternalServerError)
return
}
}
// 重新加载配置缓存
if err := h.systemConfigRepo.SafeRefreshConfigCache(); err != nil {
ErrorResponse(c, "刷新配置缓存失败", http.StatusInternalServerError)
return
}
// TODO: 这里应该启动或停止 Telegram bot 服务
// if req.BotEnabled != nil && *req.BotEnabled {
// go h.telegramBotService.Start()
// } else {
// go h.telegramBotService.Stop()
// }
// 返回成功
SuccessResponse(c, map[string]interface{}{
"success": true,
"message": "配置更新成功",
})
}
// ValidateApiKey 校验 API Key
func (h *TelegramHandler) ValidateApiKey(c *gin.Context) {
var req dto.ValidateTelegramApiKeyRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
valid, botInfo, err := h.telegramBotService.ValidateApiKey(req.ApiKey)
if err != nil {
ErrorResponse(c, "校验失败: "+err.Error(), http.StatusInternalServerError)
return
}
response := dto.ValidateTelegramApiKeyResponse{
Valid: valid,
BotInfo: botInfo,
}
if !valid {
response.Error = "无效的 API Key"
}
SuccessResponse(c, response)
}
// GetChannels 获取频道列表
func (h *TelegramHandler) GetChannels(c *gin.Context) {
channels, err := h.telegramChannelRepo.FindAll()
if err != nil {
ErrorResponse(c, "获取频道列表失败", http.StatusInternalServerError)
return
}
channelResponses := converter.TelegramChannelsToResponse(channels)
SuccessResponse(c, channelResponses)
}
// CreateChannel 创建频道
func (h *TelegramHandler) CreateChannel(c *gin.Context) {
var req dto.TelegramChannelRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
// 检查频道是否已存在
existing, err := h.telegramChannelRepo.FindByChatID(req.ChatID)
if err == nil && existing != nil {
ErrorResponse(c, "该频道/群组已注册", http.StatusBadRequest)
return
}
// 获取当前用户信息作为注册者
username := getCurrentUsername(c) // 需要实现获取用户信息的方法
channel := converter.RequestToTelegramChannel(req, username)
if err := h.telegramChannelRepo.Create(&channel); err != nil {
ErrorResponse(c, "创建频道失败", http.StatusInternalServerError)
return
}
response := converter.TelegramChannelToResponse(channel)
SuccessResponse(c, response)
}
// UpdateChannel 更新频道
func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
var req dto.TelegramChannelRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
// 查找现有频道
channel, err := h.telegramChannelRepo.FindByID(uint(id))
if err != nil {
ErrorResponse(c, "频道不存在", http.StatusNotFound)
return
}
// 更新频道信息
channel.ChatName = req.ChatName
channel.ChatType = req.ChatType
channel.PushEnabled = req.PushEnabled
channel.PushFrequency = req.PushFrequency
channel.ContentCategories = req.ContentCategories
channel.ContentTags = req.ContentTags
channel.IsActive = req.IsActive
if err := h.telegramChannelRepo.Update(channel); err != nil {
ErrorResponse(c, "更新频道失败", http.StatusInternalServerError)
return
}
response := converter.TelegramChannelToResponse(*channel)
SuccessResponse(c, response)
}
// DeleteChannel 删除频道
func (h *TelegramHandler) DeleteChannel(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
// 检查频道是否存在
channel, err := h.telegramChannelRepo.FindByID(uint(id))
if err != nil {
ErrorResponse(c, "频道不存在", http.StatusNotFound)
return
}
// 删除频道
if err := h.telegramChannelRepo.Delete(uint(id)); err != nil {
ErrorResponse(c, "删除频道失败", http.StatusInternalServerError)
return
}
SuccessResponse(c, map[string]interface{}{
"success": true,
"message": "频道 " + channel.ChatName + " 已成功移除",
})
}
// RegisterChannelByCommand 通过命令注册频道(供内部调用)
func (h *TelegramHandler) RegisterChannelByCommand(chatID int64, chatName, chatType string) error {
// 检查是否已注册
existing, err := h.telegramChannelRepo.FindByChatID(chatID)
if err == nil && existing != nil {
// 已存在,返回成功
return nil
}
// 创建新的频道记录
channel := entity.TelegramChannel{
ChatID: chatID,
ChatName: chatName,
ChatType: chatType,
PushEnabled: true,
PushFrequency: 24, // 默认24小时
IsActive: true,
RegisteredBy: "bot_command",
}
return h.telegramChannelRepo.Create(&channel)
}
// HandleWebhook 处理 Telegram Webhook
func (h *TelegramHandler) HandleWebhook(c *gin.Context) {
// 将消息交给 bot 服务处理
// 这里可以根据需要添加身份验证
h.telegramBotService.HandleWebhookUpdate(c)
}
// getCurrentUsername 获取当前用户名(临时实现)
func getCurrentUsername(c *gin.Context) string {
// 这里应该从中间件中获取用户信息
// 暂时返回默认值
return "admin"
}

27
main.go
View File

@@ -323,6 +323,33 @@ func main() {
api.GET("/files", middleware.AuthMiddleware(), fileHandler.GetFileList)
api.DELETE("/files", middleware.AuthMiddleware(), fileHandler.DeleteFiles)
api.PUT("/files", middleware.AuthMiddleware(), fileHandler.UpdateFile)
// 创建Telegram Bot服务
telegramBotService := services.NewTelegramBotService(
repoManager.SystemConfigRepository,
repoManager.TelegramChannelRepository,
repoManager.ResourceRepository,
)
// 启动Telegram Bot服务
if err := telegramBotService.Start(); err != nil {
utils.Error("启动Telegram Bot服务失败: %v", err)
}
// Telegram相关路由
telegramHandler := handlers.NewTelegramHandler(
repoManager.TelegramChannelRepository,
repoManager.SystemConfigRepository,
telegramBotService,
)
api.GET("/telegram/bot-config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetBotConfig)
api.PUT("/telegram/bot-config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.UpdateBotConfig)
api.POST("/telegram/validate-api-key", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.ValidateApiKey)
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)
}
// 静态文件服务

View File

@@ -0,0 +1,491 @@
package services
import (
"fmt"
"strings"
"time"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/utils"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/robfig/cron/v3"
)
type TelegramBotService interface {
Start() error
Stop() error
ValidateApiKey(apiKey string) (bool, map[string]interface{}, error)
GetBotUsername() string
SendMessage(chatID int64, text string) error
DeleteMessage(chatID int64, messageID int) error
RegisterChannel(chatID int64, chatName, chatType string) error
IsChannelRegistered(chatID int64) bool
HandleWebhookUpdate(c interface{})
}
type TelegramBotServiceImpl struct {
bot *tgbotapi.BotAPI
isRunning bool
systemConfigRepo repo.SystemConfigRepository
channelRepo repo.TelegramChannelRepository
resourceRepo repo.ResourceRepository // 添加资源仓库用于搜索
cronScheduler *cron.Cron
config *TelegramBotConfig
}
type TelegramBotConfig struct {
Enabled bool
ApiKey string
AutoReplyEnabled bool
AutoReplyTemplate string
AutoDeleteEnabled bool
AutoDeleteInterval int // 分钟
}
func NewTelegramBotService(
systemConfigRepo repo.SystemConfigRepository,
channelRepo repo.TelegramChannelRepository,
resourceRepo repo.ResourceRepository,
) TelegramBotService {
return &TelegramBotServiceImpl{
isRunning: false,
systemConfigRepo: systemConfigRepo,
channelRepo: channelRepo,
resourceRepo: resourceRepo,
cronScheduler: cron.New(),
config: &TelegramBotConfig{},
}
}
// loadConfig 加载配置
func (s *TelegramBotServiceImpl) loadConfig() error {
configs, err := s.systemConfigRepo.GetOrCreateDefault()
if err != nil {
return fmt.Errorf("加载配置失败: %v", err)
}
s.config.Enabled = false
s.config.ApiKey = ""
s.config.AutoReplyEnabled = true
s.config.AutoReplyTemplate = "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
s.config.AutoDeleteEnabled = false
s.config.AutoDeleteInterval = 60
for _, config := range configs {
switch config.Key {
case entity.ConfigKeyTelegramBotEnabled:
s.config.Enabled = config.Value == "true"
case entity.ConfigKeyTelegramBotApiKey:
s.config.ApiKey = config.Value
case entity.ConfigKeyTelegramAutoReplyEnabled:
s.config.AutoReplyEnabled = config.Value == "true"
case entity.ConfigKeyTelegramAutoReplyTemplate:
if config.Value != "" {
s.config.AutoReplyTemplate = config.Value
}
case entity.ConfigKeyTelegramAutoDeleteEnabled:
s.config.AutoDeleteEnabled = config.Value == "true"
case entity.ConfigKeyTelegramAutoDeleteInterval:
if config.Value != "" {
fmt.Sscanf(config.Value, "%d", &s.config.AutoDeleteInterval)
}
}
}
utils.Info("Telegram Bot 配置已加载: Enabled=%v", s.config.Enabled)
return nil
}
// Start 启动机器人服务
func (s *TelegramBotServiceImpl) Start() error {
if s.isRunning {
utils.Info("Telegram Bot 服务已经在运行中")
return nil
}
// 加载配置
if err := s.loadConfig(); err != nil {
return fmt.Errorf("加载配置失败: %v", err)
}
if !s.config.Enabled || s.config.ApiKey == "" {
utils.Info("Telegram Bot 未启用或 API Key 未配置")
return nil
}
// 创建 Bot 实例
bot, err := tgbotapi.NewBotAPI(s.config.ApiKey)
if err != nil {
return fmt.Errorf("创建 Telegram Bot 失败: %v", err)
}
s.bot = bot
s.isRunning = true
utils.Info("Telegram Bot (@%s) 已启动", s.GetBotUsername())
// 启动推送调度器
s.startContentPusher()
// 设置 webhook在实际部署时配置
if err := s.setupWebhook(); err != nil {
utils.Error("设置 Webhook 失败: %v", err)
}
// 启动消息处理循环(长轮询模式)
go s.messageLoop()
return nil
}
// Stop 停止机器人服务
func (s *TelegramBotServiceImpl) Stop() error {
if !s.isRunning {
return nil
}
s.isRunning = false
if s.cronScheduler != nil {
s.cronScheduler.Stop()
}
utils.Info("Telegram Bot 服务已停止")
return nil
}
// 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)
if err != nil {
return false, nil, fmt.Errorf("无效的 API Key: %v", err)
}
// 获取机器人信息
botInfo, err := bot.GetMe()
if err != nil {
return false, nil, fmt.Errorf("获取机器人信息失败: %v", err)
}
botData := map[string]interface{}{
"id": botInfo.ID,
"username": strings.TrimPrefix(botInfo.UserName, "@"),
"first_name": botInfo.FirstName,
"last_name": botInfo.LastName,
}
return true, botData, nil
}
// setupWebhook 设置 Webhook可选
func (s *TelegramBotServiceImpl) setupWebhook() error {
// 在生产环境中,这里会设置 webhook URL
// 暂时使用长轮询模式,不设置 webhook
utils.Info("使用长轮询模式处理消息")
return nil
}
// messageLoop 消息处理循环(长轮询模式)
func (s *TelegramBotServiceImpl) messageLoop() {
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := s.bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil {
s.handleMessage(update.Message)
}
}
}
// handleMessage 处理接收到的消息
func (s *TelegramBotServiceImpl) handleMessage(message *tgbotapi.Message) {
chatID := message.Chat.ID
text := strings.TrimSpace(message.Text)
utils.Info("收到消息: ChatID=%d, Text='%s', User=%s", chatID, text, message.From.UserName)
if text == "" {
return
}
// 处理 /register 命令
if strings.ToLower(text) == "/register" {
s.handleRegisterCommand(message)
return
}
// 处理 /start 命令
if strings.ToLower(text) == "/start" {
s.handleStartCommand(message)
return
}
// 处理普通文本消息(搜索请求)
if len(text) > 0 && !strings.HasPrefix(text, "/") {
s.handleSearchRequest(message)
return
}
// 默认自动回复
if s.config.AutoReplyEnabled {
s.sendReply(message, s.config.AutoReplyTemplate)
}
}
// handleRegisterCommand 处理注册命令
func (s *TelegramBotServiceImpl) handleRegisterCommand(message *tgbotapi.Message) {
chatID := message.Chat.ID
chatTitle := message.Chat.Title
if chatTitle == "" {
// 如果没有标题,使用用户名作为名称
if message.Chat.UserName != "" {
chatTitle = message.Chat.UserName
} else {
chatTitle = fmt.Sprintf("Chat_%d", chatID)
}
}
chatType := "private"
if message.Chat.IsChannel() {
chatType = "channel"
} else if message.Chat.IsGroup() || message.Chat.IsSuperGroup() {
chatType = "group"
}
err := s.RegisterChannel(chatID, chatTitle, chatType)
if err != nil {
errorMsg := fmt.Sprintf("注册失败: %v", err)
s.sendReply(message, errorMsg)
return
}
successMsg := fmt.Sprintf("✅ 注册成功!\n\n频道/群组: %s\n类型: %s\n\n现在可以向此频道推送资源内容了。", chatTitle, chatType)
s.sendReply(message, successMsg)
}
// handleStartCommand 处理开始命令
func (s *TelegramBotServiceImpl) handleStartCommand(message *tgbotapi.Message) {
welcomeMsg := `🤖 欢迎使用网盘资源机器人!
我会帮您搜索网盘资源。使用方法:
• 直接发送关键词搜索资源
• 发送 /register 注册当前频道用于推送
享受使用吧!`
if s.config.AutoReplyEnabled && s.config.AutoReplyTemplate != "" {
welcomeMsg += "\n\n" + s.config.AutoReplyTemplate
}
s.sendReply(message, welcomeMsg)
}
// handleSearchRequest 处理搜索请求
func (s *TelegramBotServiceImpl) handleSearchRequest(message *tgbotapi.Message) {
query := strings.TrimSpace(message.Text)
if query == "" {
s.sendReply(message, "请输入搜索关键词")
return
}
// 这里使用简单的资源搜索,实际项目中需要完善搜索逻辑
// resources, err := s.resourceRepo.Search(query, nil, 0, 10)
// 暂时模拟一个搜索结果
results := []string{
fmt.Sprintf("🔍 搜索关键词: %s", query),
"暂无相关资源,请尝试其他关键词。",
"",
fmt.Sprintf("💡 提示:如需精确搜索,请使用更具体的关键词。"),
}
resultText := strings.Join(results, "\n")
s.sendReply(message, resultText)
}
// sendReply 发送回复消息
func (s *TelegramBotServiceImpl) sendReply(message *tgbotapi.Message, text string) {
msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ParseMode = "Markdown"
msg.ReplyToMessageID = message.MessageID
sentMsg, err := s.bot.Send(msg)
if err != nil {
utils.Error("发送消息失败: %v", err)
return
}
// 如果启用了自动删除,启动删除定时器
if s.config.AutoDeleteEnabled && s.config.AutoDeleteInterval > 0 {
time.AfterFunc(time.Duration(s.config.AutoDeleteInterval)*time.Minute, func() {
deleteConfig := tgbotapi.DeleteMessageConfig{
ChatID: sentMsg.Chat.ID,
MessageID: sentMsg.MessageID,
}
_, err := s.bot.Request(deleteConfig)
if err != nil {
utils.Error("删除消息失败: %v", err)
}
})
}
}
// startContentPusher 启动内容推送器
func (s *TelegramBotServiceImpl) startContentPusher() {
// 每小时检查一次需要推送的频道
s.cronScheduler.AddFunc("@every 1h", func() {
s.pushContentToChannels()
})
s.cronScheduler.Start()
utils.Info("内容推送调度器已启动")
}
// pushContentToChannels 推送内容到频道
func (s *TelegramBotServiceImpl) pushContentToChannels() {
// 获取需要推送的频道
channels, err := s.channelRepo.FindDueForPush()
if err != nil {
utils.Error("获取推送频道失败: %v", err)
return
}
if len(channels) == 0 {
utils.Debug("没有需要推送的频道")
return
}
utils.Info("开始推送内容到 %d 个频道", len(channels))
for _, channel := range channels {
go s.pushToChannel(channel)
}
}
// pushToChannel 推送内容到一个频道
func (s *TelegramBotServiceImpl) pushToChannel(channel entity.TelegramChannel) {
// 这里实现推送逻辑
// 1. 根据频道设置过滤资源
resources := s.findResourcesForChannel(channel)
if len(resources) == 0 {
utils.Debug("频道 %s 没有可推送的内容", channel.ChatName)
return
}
// 2. 构建推送消息
message := s.buildPushMessage(channel, resources)
// 3. 发送消息
err := s.SendMessage(channel.ChatID, message)
if err != nil {
utils.Error("推送失败到频道 %s (%d): %v", channel.ChatName, channel.ChatID, err)
// 可以考虑将频道标记为非活跃或记录错误
return
}
// 4. 更新最后推送时间
err = s.channelRepo.UpdateLastPushAt(channel.ID, time.Now())
if err != nil {
utils.Error("更新推送时间失败: %v", err)
}
utils.Info("成功推送内容到频道: %s", channel.ChatName)
}
// findResourcesForChannel 查找适合频道的资源
func (s *TelegramBotServiceImpl) findResourcesForChannel(channel entity.TelegramChannel) []interface{} {
// 这里需要实现根据频道配置过滤资源
// 暂时返回空数组,实际实现中需要查询资源数据库
return []interface{}{}
}
// buildPushMessage 构建推送消息
func (s *TelegramBotServiceImpl) buildPushMessage(channel entity.TelegramChannel, resources []interface{}) string {
message := fmt.Sprintf("📢 **%s**\n\n", channel.ChatName)
if len(resources) == 0 {
message += "暂无新内容推送"
} else {
message += fmt.Sprintf("🆕 发现 %d 个新资源:\n\n", len(resources))
// 这里需要格式化资源列表
message += "*详细资源列表请查看网站*"
}
message += fmt.Sprintf("\n\n⏰ 下次推送: %d 小时后", channel.PushFrequency)
return message
}
// GetBotUsername 获取机器人用户名
func (s *TelegramBotServiceImpl) GetBotUsername() string {
if s.bot != nil {
return s.bot.Self.UserName
}
return ""
}
// SendMessage 发送消息
func (s *TelegramBotServiceImpl) SendMessage(chatID int64, text string) error {
if s.bot == nil {
return fmt.Errorf("Bot 未初始化")
}
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "Markdown"
_, err := s.bot.Send(msg)
return err
}
// DeleteMessage 删除消息
func (s *TelegramBotServiceImpl) DeleteMessage(chatID int64, messageID int) error {
if s.bot == nil {
return fmt.Errorf("Bot 未初始化")
}
deleteConfig := tgbotapi.NewDeleteMessage(chatID, messageID)
_, err := s.bot.Request(deleteConfig)
return err
}
// RegisterChannel 注册频道
func (s *TelegramBotServiceImpl) RegisterChannel(chatID int64, chatName, chatType string) error {
// 检查是否已注册
if s.IsChannelRegistered(chatID) {
return fmt.Errorf("该频道/群组已注册")
}
channel := entity.TelegramChannel{
ChatID: chatID,
ChatName: chatName,
ChatType: chatType,
PushEnabled: true,
PushFrequency: 24, // 默认24小时
IsActive: true,
RegisteredBy: "bot_command",
RegisteredAt: time.Now(),
ContentCategories: "",
ContentTags: "",
}
return s.channelRepo.Create(&channel)
}
// IsChannelRegistered 检查频道是否已注册
func (s *TelegramBotServiceImpl) IsChannelRegistered(chatID int64) bool {
channel, err := s.channelRepo.FindByChatID(chatID)
return err == nil && channel != nil
}
// HandleWebhookUpdate 处理 Webhook 更新(预留接口,目前使用长轮询)
func (s *TelegramBotServiceImpl) HandleWebhookUpdate(c interface{}) {
// 目前使用长轮询模式webhook 接口预留
// 将来可以实现从 webhook 接收消息的处理逻辑
// 如果需要实现 webhook 模式,可以在这里添加处理逻辑
}

1
web/components.d.ts vendored
View File

@@ -30,6 +30,7 @@ declare module 'vue' {
NImage: typeof import('naive-ui')['NImage']
NImageGroup: typeof import('naive-ui')['NImageGroup']
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NList: typeof import('naive-ui')['NList']
NListItem: typeof import('naive-ui')['NListItem']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']

View File

@@ -173,10 +173,234 @@
<n-tab-pane name="telegram" tab="Telegram机器人">
<div class="tab-content-container">
<div class="text-center py-12">
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
<p class="text-gray-500 dark:text-gray-400">Telegram机器人功能正在开发中敬请期待</p>
<div class="space-y-8">
<!-- 机器人基本配置 -->
<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-blue-600 text-white rounded-full flex items-center justify-center mr-3">
<span class="text-sm font-bold">1</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">启用 Telegram 机器人</label>
<p class="text-xs text-gray-500 dark:text-gray-400">开启后机器人将开始工作</p>
</div>
<n-switch
v-model:value="telegramBotConfig.bot_enabled"
@update:value="handleBotConfigChange"
/>
</div>
<!-- API Key 配置 -->
<div v-if="telegramBotConfig.bot_enabled" class="space-y-3">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">Bot API Key</label>
<div class="flex space-x-3">
<n-input
v-model:value="telegramBotConfig.bot_api_key"
placeholder="请输入 Telegram Bot API Key"
type="password"
show-password-on="click"
class="flex-1"
@input="handleBotConfigChange"
/>
<n-button
type="primary"
:loading="validatingApiKey"
@click="validateApiKey"
>
校验
</n-button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
@BotFather 获取 API Key
</p>
</div>
<!-- 校验结果 -->
<div v-if="apiKeyValidationResult" class="p-3 rounded-md"
:class="apiKeyValidationResult.valid ? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300' : 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'">
<div class="flex items-center">
<i :class="apiKeyValidationResult.valid ? 'fas fa-check-circle' : 'fas fa-times-circle'"
class="mr-2"></i>
<span>{{ apiKeyValidationResult.message }}</span>
</div>
<div v-if="apiKeyValidationResult.valid && apiKeyValidationResult.botInfo" class="mt-2 text-xs">
机器人@{{ apiKeyValidationResult.botInfo.username }} ({{ apiKeyValidationResult.botInfo.first_name }})
</div>
</div>
<div class="flex justify-end">
<n-button
type="primary"
:loading="savingBotConfig"
:disabled="!hasBotConfigChanges"
@click="saveBotConfig"
>
保存配置
</n-button>
</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 mb-6">
<div class="w-8 h-8 bg-green-600 text-white rounded-full flex items-center justify-center mr-3">
<span class="text-sm font-bold">2</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">收到消息时自动回复帮助信息</p>
</div>
<n-switch
v-model:value="telegramBotConfig.auto_reply_enabled"
@update:value="handleBotConfigChange"
/>
</div>
<!-- 回复模板 -->
<div v-if="telegramBotConfig.auto_reply_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"
type="textarea"
placeholder="请输入自动回复内容"
:rows="3"
@input="handleBotConfigChange"
/>
</div>
<!-- 自动删除开关 -->
<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="telegramBotConfig.auto_delete_enabled"
@update:value="handleBotConfigChange"
/>
</div>
<!-- 删除间隔 -->
<div v-if="telegramBotConfig.auto_delete_enabled">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">删除间隔分钟</label>
<n-input-number
v-model:value="telegramBotConfig.auto_delete_interval"
:min="1"
:max="1440"
@update:value="handleBotConfigChange"
/>
</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">
<div class="flex items-center">
<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>
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">频道和群组管理</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">管理推送对象的频道和群组</p>
</div>
</div>
<n-button
type="primary"
@click="showRegisterChannelDialog = true"
>
<template #icon>
<i class="fas fa-plus"></i>
</template>
注册频道/群组
</n-button>
</div>
<!-- 频道列表 -->
<div v-if="telegramChannels.length > 0" class="space-y-4">
<div v-for="channel in telegramChannels" :key="channel.id"
class="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<i :class="channel.chat_type === 'channel' ? 'fab fa-telegram-plane' : 'fas fa-users'"
class="text-lg text-blue-600 dark:text-blue-400"></i>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">{{ channel.chat_name }}</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ channel.chat_type === 'channel' ? '频道' : '群组' }} ID: {{ channel.chat_id }}
</p>
</div>
</div>
<div class="flex items-center space-x-2">
<n-tag :type="channel.is_active ? 'success' : 'warning'" size="small">
{{ channel.is_active ? '活跃' : '非活跃' }}
</n-tag>
<n-button size="small" @click="editChannel(channel)">
<template #icon>
<i class="fas fa-edit"></i>
</template>
</n-button>
<n-button size="small" type="error" @click="unregisterChannel(channel)">
<template #icon>
<i class="fas fa-trash"></i>
</template>
</n-button>
</div>
</div>
<!-- 推送配置 -->
<div v-if="channel.push_enabled" class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">推送频率</label>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ channel.push_frequency }} 小时</p>
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">内容分类</label>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ channel.content_categories || '全部' }}
</p>
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">标签</label>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ channel.content_tags || '全部' }}
</p>
</div>
</div>
<div v-else class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
<p class="text-sm text-gray-500 dark:text-gray-400">推送已禁用</p>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="text-center py-8">
<i class="fab fa-telegram-plane text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">暂无频道或群组</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">点击上方按钮注册推送对象</p>
<n-button type="primary" @click="showRegisterChannelDialog = true">
立即注册
</n-button>
</div>
</div>
</div>
</div>
</n-tab-pane>
@@ -191,6 +415,55 @@
</div>
</n-tab-pane>
</n-tabs>
<!-- 注册频道对话框 -->
<n-modal
v-model:show="showRegisterChannelDialog"
preset="card"
title="注册频道/群组"
size="huge"
:bordered="false"
:segmented="false"
>
<div class="space-y-6">
<div class="text-sm text-gray-600 dark:text-gray-400">
将机器人添加到频道或群组然后发送命令获取频道信息并注册为推送对象
</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>
<ol class="text-sm text-blue-700 dark:text-blue-300 space-y-1 list-decimal list-inside">
<li> @{{ telegramBotConfig.bot_enabled ? '机器人用户名' : '机器人' }} 添加为频道管理员或群组成员</li>
<li>在频道/群组中发送 <code class="bg-blue-200 dark:bg-blue-800 px-1 rounded">/register</code> 命令</li>
<li>机器人将自动识别并注册该频道/群组</li>
</ol>
</div>
</div>
</div>
<div v-if="!telegramBotConfig.bot_enabled || !telegramBotConfig.bot_api_key" class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
<div class="flex items-start space-x-3">
<i class="fas fa-exclamation-triangle text-yellow-600 dark:text-yellow-400 mt-1"></i>
<div>
<h4 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">配置未完成</h4>
<p class="text-sm text-yellow-700 dark:text-yellow-300">请先启用机器人并配置有效的 API Key</p>
</div>
</div>
</div>
<div class="text-center py-4">
<n-button
type="primary"
@click="showRegisterChannelDialog = false"
>
我知道了
</n-button>
</div>
</div>
</n-modal>
</div>
</template>
</AdminPageLayout>
@@ -226,6 +499,182 @@ const {
}
})
// Telegram 相关数据和状态
const telegramBotConfig = ref({
bot_enabled: false,
bot_api_key: '',
auto_reply_enabled: true,
auto_reply_template: '您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。',
auto_delete_enabled: false,
auto_delete_interval: 60,
})
const telegramChannels = ref([])
const validatingApiKey = ref(false)
const savingBotConfig = ref(false)
const apiKeyValidationResult = ref(null)
const hasBotConfigChanges = ref(false)
const showRegisterChannelDialog = ref(false)
// 获取 Telegram 配置
const fetchTelegramConfig = async () => {
try {
const response = await $fetch('/api/telegram/bot-config', {
method: 'GET'
})
if (response && response.data) {
telegramBotConfig.value = { ...response.data }
}
} catch (error) {
console.error('获取 Telegram 配置失败:', error)
}
}
// 获取频道列表
const fetchTelegramChannels = async () => {
try {
const response = await $fetch('/api/telegram/channels', {
method: 'GET'
})
if (response && response.data) {
telegramChannels.value = response.data
}
} catch (error) {
console.error('获取频道列表失败:', error)
}
}
// 处理机器人配置变更
const handleBotConfigChange = () => {
hasBotConfigChanges.value = true
}
// 校验 API Key
const validateApiKey = async () => {
if (!telegramBotConfig.value.bot_api_key) {
notification.error({
content: '请输入 API Key',
duration: 2000
})
return
}
validatingApiKey.value = true
try {
const response = await $fetch('/api/telegram/validate-api-key', {
method: 'POST',
body: {
api_key: telegramBotConfig.value.bot_api_key
}
})
if (response && response.data) {
apiKeyValidationResult.value = response.data
if (response.data.valid) {
notification.success({
content: 'API Key 校验成功',
duration: 2000
})
} else {
notification.error({
content: response.data.error,
duration: 3000
})
}
}
} catch (error) {
apiKeyValidationResult.value = {
valid: false,
error: error.data?.message || '校验失败'
}
notification.error({
content: 'API Key 校验失败',
duration: 2000
})
} finally {
validatingApiKey.value = false
}
}
// 保存机器人配置
const saveBotConfig = async () => {
savingBotConfig.value = true
try {
const configRequest = {}
if (hasBotConfigChanges.value) {
configRequest.bot_enabled = telegramBotConfig.value.bot_enabled
configRequest.bot_api_key = telegramBotConfig.value.bot_api_key
configRequest.auto_reply_enabled = telegramBotConfig.value.auto_reply_enabled
configRequest.auto_reply_template = telegramBotConfig.value.auto_reply_template
configRequest.auto_delete_enabled = telegramBotConfig.value.auto_delete_enabled
configRequest.auto_delete_interval = telegramBotConfig.value.auto_delete_interval
}
const response = await $fetch('/api/telegram/bot-config', {
method: 'PUT',
body: configRequest
})
if (response && response.success) {
notification.success({
content: '配置保存成功',
duration: 2000
})
hasBotConfigChanges.value = false
// 重新获取配置以确保同步
await fetchTelegramConfig()
}
} catch (error) {
notification.error({
content: error.data?.message || '配置保存失败',
duration: 3000
})
} finally {
savingBotConfig.value = false
}
}
// 编辑频道
const editChannel = (channel) => {
// TODO: 实现编辑频道功能
console.log('编辑频道:', channel)
}
// 注销频道
const unregisterChannel = async (channel) => {
try {
await notification.warning({
content: `确定取消注册 "${channel.chat_name}" 吗?`,
duration: 10000,
action: () => performUnregisterChannel(channel)
})
} catch (error) {
console.error('注销频道操作失败:', error)
}
}
const performUnregisterChannel = async (channel) => {
try {
const response = await $fetch(`/api/telegram/channels/${channel.id}`, {
method: 'DELETE'
})
if (response && response.success) {
notification.success({
content: '频道已取消注册',
duration: 2000
})
// 重新获取频道列表
await fetchTelegramChannels()
}
} catch (error) {
notification.error({
content: error.data?.message || '取消注册失败',
duration: 3000
})
}
}
const notification = useNotification()
const activeTab = ref('qq')
@@ -280,8 +729,10 @@ const copyToClipboard = async (text: string) => {
}
// 页面加载时获取配置
onMounted(() => {
onMounted(async () => {
fetchApiToken()
await fetchTelegramConfig()
await fetchTelegramChannels()
console.log('机器人管理页面已加载')
})
</script>