New features: TG Telegram robot terminal [One-click configuration] and [Subscription conversion]

This commit is contained in:
心隨緣動 2025-10-08 00:03:34 +08:00 committed by GitHub
parent a9b814278c
commit e18ce0f23f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -17,6 +17,7 @@ import (
"strconv"
"strings"
"time"
"context"
"x-ui/config"
"x-ui/database"
@ -95,11 +96,17 @@ type Release struct {
type ServerService struct {
xrayService XrayService
inboundService InboundService
tgService TelegramService
cachedIPv4 string
cachedIPv6 string
noIPv6 bool
}
// 【新增方法】: 用于从外部注入 TelegramService 实例
func (s *ServerService) SetTelegramService(tgService TelegramService) {
s.tgService = tgService
}
func getPublicIP(url string) string {
client := &http.Client{
Timeout: 3 * time.Second,
@ -590,7 +597,8 @@ func (s *ServerService) GetConfigJson() (any, error) {
if err != nil {
return nil, err
}
contents, err := json.MarshalIndent(config, "", " ")
// 修复:将 U+00A0 替换为标准空格
contents, err := json.MarshalIndent(config, "", " ")
if err != nil {
return nil, err
}
@ -832,7 +840,7 @@ func (s *ServerService) GetNewX25519Cert() (any, error) {
keyPair := map[string]any{
"privateKey": privateKey,
"publicKey": publicKey,
"publicKey": publicKey, // 修复U+00A0 替换为标准空格
}
return keyPair, nil
@ -888,7 +896,6 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
}, nil
}
func (s *ServerService) GetNewVlessEnc() (any, error) {
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
var out bytes.Buffer
@ -941,7 +948,6 @@ func (s *ServerService) GetNewUUID() (map[string]string, error) {
}, nil
}
func (s *ServerService) GetNewmlkem768() (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "mlkem768")
@ -970,25 +976,235 @@ func (s *ServerService) GetNewmlkem768() (any, error) {
// SaveLinkHistory 保存一个新的链接记录,并确保其被永久写入数据库文件。
func (s *ServerService) SaveLinkHistory(historyType, link string) error {
record := &database.LinkHistory{
Type: historyType,
Link: link,
CreatedAt: time.Now(),
}
record := &database.LinkHistory{
Type: historyType,
Link: link,
CreatedAt: time.Now(),
}
// 【核心修正】: 第一步,调用重构后的 AddLinkHistory 函数。
// 这个函数现在是一个原子事务。如果它没有返回错误,就意味着数据已经成功提交到了 .wal 日志文件。
err := database.AddLinkHistory(record)
if err != nil {
return err // 如果事务失败,直接返回错误,不执行后续操作
}
// 【核心修正】: 第一步,调用重构后的 AddLinkHistory 函数。
// 这个函数现在是一个原子事务。如果它没有返回错误,就意味着数据已经成功提交到了 .wal 日志文件。
err := database.AddLinkHistory(record)
if err != nil {
return err // 如果事务失败,直接返回错误,不执行后续操作
}
// 【核心修正】: 第二步,在事务成功提交后,我们在这里调用 Checkpoint。
// 此时 .wal 文件中已经包含了我们的新数据,调用 Checkpoint 可以确保这些数据被立即写入主数据库文件。
return database.Checkpoint()
// 【核心修正】: 第二步,在事务成功提交后,我们在这里调用 Checkpoint。
// 此时 .wal 文件中已经包含了我们的新数据,调用 Checkpoint 可以确保这些数据被立即写入主数据库文件。
return database.Checkpoint()
}
// LoadLinkHistory loads the latest 10 links from the database
func (s *ServerService) LoadLinkHistory() ([]*database.LinkHistory, error) {
return database.GetLinkHistory()
}
// 〔新增方法〕: 安装 Subconverter (异步执行)
// 〔中文注释〕: 此方法用于接收前端或 TG 的请求,并执行 x-ui.sh 脚本中的 subconverter 函数
func (s *ServerService) InstallSubconverter() error {
// 〔中文注释〕: 使用一个新的 goroutine 来执行耗时的安装任务,这样 API 可以立即返回
go func() {
// 【新增功能】:执行端口放行操作
var ufwWarning string
if ufwErr := s.openSubconverterPorts(); ufwErr != nil {
// 不中断流程,只生成警告消息
logger.Warningf("自动放行 Subconverter 端口失败: %v", ufwErr)
ufwWarning = fmt.Sprintf("⚠️ **警告:订阅转换端口放行失败**\n\n自动执行 UFW 命令失败,请务必**手动**在您的 VPS 上放行端口 `8000` 和 `15268`,否则服务将无法访问。失败详情:%v\n\n", ufwErr)
}
// 〔中文注释〕: 检查全局的 TgBot 实例是否存在并且正在运行
if s.tgService == nil || !s.tgService.IsRunning() {
logger.Warning("TgBot 未运行,无法发送【订阅转换】状态通知。")
// 即使机器人未运行,安装流程也应继续,只是不发通知
ufwWarning = "" // 如果机器人不在线,不发送任何警告/消息
}
// 脚本路径为 /usr/bin/x-ui
// 〔中文注释〕: 通常,安装脚本会将主命令软链接或复制到 /usr/bin/ 目录下,使其成为一个系统命令。
// 直接调用这个命令比调用源文件路径更规范,也能确保执行的是用户在命令行中使用的同一个脚本。
scriptPath := "/usr/bin/x-ui"
// 〔中文注释〕: 检查脚本文件是否存在
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
errMsg := fmt.Sprintf("订阅转换安装失败:关键脚本文件 `%s` 未找到。", scriptPath)
logger.Error(errMsg)
if s.tgService != nil && s.tgService.IsRunning() {
// 〔中文注释〕: 使用 Markdown 格式发送错误消息
s.tgService.SendMessage("❌ " + errMsg)
}
return
}
// 〔中文注释〕: 正确的调用方式是:命令是 "x-ui",参数是 "subconverter"。
cmd := exec.Command(scriptPath, "subconverter")
// 〔中文注释〕: 执行命令并获取其合并的输出(标准输出 + 标准错误),方便排查问题。
// 〔重要〕: 这个命令可能需要几分钟才能执行完毕Go程序会在此等待直到脚本执行完成。
output, err := cmd.CombinedOutput()
if err != nil {
if s.tgService != nil && s.tgService.IsRunning() {
// 构造失败消息
message := fmt.Sprintf("❌ **订阅转换安装失败**\n\n**错误信息**: %v\n**输出**: %s", err, string(output))
s.tgService.SendMessage(message)
}
logger.Errorf("订阅转换安装失败: %v\n输出: %s", err, string(output))
return
} else {
// 【新增逻辑】:如果之前端口放行失败,先发送警告消息
if ufwWarning != "" {
s.tgService.SendMessage(ufwWarning)
}
// 安装成功后,发送通知到 TG 机器人
if s.tgService != nil && s.tgService.IsRunning() {
// 获取面板域名注意t.getDomain() 是 Tgbot 的方法
domain, getDomainErr := s.tgService.GetDomain()
if getDomainErr != nil {
logger.Errorf("TG Bot: 订阅转换安装成功,但获取域名失败: %v", getDomainErr)
} else {
// 构造消息,使用用户指定的格式
message := fmt.Sprintf(
"🎉 **恭喜!【订阅转换】模块已成功安装!**\n\n"+
"您现在可以使用以下地址访问 Web 界面:\n\n"+
"🔗 **登录地址**: `https://%s:15268`\n\n"+
"默认用户名: `admin`\n"+
"默认 密码: `123456`\n\n"+
"可登录订阅转换后台修改您的密码!", domain)
// 发送成功消息
if sendErr := s.tgService.SendMessage(message); sendErr != nil {
logger.Errorf("TG Bot: 订阅转换安装成功,但发送通知失败: %v", sendErr)
} else {
logger.Info("TG Bot: 订阅转换安装成功通知已发送。")
}
}
}
logger.Info("订阅转换安装成功。")
return
}
}()
return nil // 立即返回,表示指令已接收
}
// openSubconverterPorts 检查/安装 ufw 并放行 8000 和 15268 端口
func (s *ServerService) openSubconverterPorts() error {
// Shell 脚本:检查/安装 UFW然后循环放行 8000 和 15268 端口,最后尝试激活
shellCommand := `
PORTS_TO_OPEN="8000 15268"
echo "正在为订阅转换自动检查并放行端口 $PORTS_TO_OPEN"
# 1. 检查/安装 ufw
if ! command -v ufw &>/dev/null; then
echo "ufw 防火墙未安装,正在安装..."
# 静默更新和安装
DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update -qq >/dev/null
DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install -y ufw
if [ $? -ne 0 ]; then echo "❌ ufw 安装失败或权限不足。"; exit 1; fi
fi
# 2. 放行端口
for port in $PORTS_TO_OPEN; do
echo "正在执行 ufw allow $port..."
ufw allow $port
if [ $? -ne 0 ]; then echo "❌ ufw 端口 $port 放行失败。"; exit 1; fi
done
# 3. 检查/激活防火墙
if ! ufw status | grep -q "Status: active"; then
echo "ufw 状态:未激活。正在尝试激活..."
ufw --force enable
if [ $? -ne 0 ]; then echo "❌ ufw 激活失败。"; exit 1; fi
fi
echo "✅ 端口 $PORTS_TO_OPEN 已成功放行/检查。"
exit 0
`
// 使用 /bin/bash -c 执行命令,并捕获输出
cmd := exec.CommandContext(context.Background(), "/bin/bash", "-c", shellCommand)
output, err := cmd.CombinedOutput()
logOutput := string(output)
// 记录日志,无论成功与否
logger.Infof("执行 Subconverter 端口放行命令结果:\n%s", logOutput)
if err != nil {
// 如果 Shell 命令返回非零退出码,则返回错误
return fmt.Errorf("ufw 端口放行失败: %v. 脚本输出: %s", err, logOutput)
}
return nil
}
// 【新增方法实现】: 后台前端开放指定端口
// OpenPort 供前端调用,自动检查/安装 ufw 并放行指定的端口。
// 〔中文注释〕: 整个函数逻辑被放入一个 go func() 协程中,实现异步后台执行。
// 〔中文注释〕: 函数签名不再返回 error因为它会立即返回无法得知后台任务的最终结果。
func (s *ServerService) OpenPort(port string) {
// 〔中文注释〕: 启动一个新的协程来处理耗时任务,这样 HTTP 请求可以立刻返回。
go func() {
// 1. 将 port string 转换为 int
portInt, err := strconv.Atoi(port)
if err != nil {
// 〔中文注释〕: 在后台任务中,如果出错,我们只能记录日志,因为无法再返回给前端。
logger.Errorf("端口号格式错误,无法转换为数字: %s", port)
return
}
// 2. 将 Shell 逻辑整合为一个可执行的命令,并使用 /bin/bash -c 执行
shellCommand := fmt.Sprintf(`
PORT_TO_OPEN=%d
echo "正在为入站配置自动检查并放行端口 $PORT_TO_OPEN"
# 1. 检查/安装 ufw (仅限 Debian/Ubuntu 系统)
if ! command -v ufw &>/dev/null; then
echo "ufw 防火墙未安装,正在安装..."
# 使用绝对路径执行 apt-get避免 PATH 问题
DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update -qq >/dev/null
DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install -y ufw
if [ $? -ne 0 ]; then echo "❌ ufw 安装失败,可能不是 Debian/Ubuntu 系统,或者权限不足。"; exit 1; fi
fi
# 2. 放行端口 (TCP/UDP)
echo "正在执行 ufw allow $PORT_TO_OPEN..."
ufw allow $PORT_TO_OPEN
if [ $? -ne 0 ]; then echo "❌ ufw 端口 $PORT_TO_OPEN 放行失败。"; exit 1; fi
# 3. 检查/激活防火墙
if ! ufw status | grep -q "Status: active"; then
echo "ufw 状态:未激活。正在尝试激活..."
ufw --force enable
if [ $? -ne 0 ]; then echo "❌ ufw 激活失败。"; exit 1; fi
fi
echo "✅ 端口 $PORT_TO_OPEN 已成功放行/检查。"
`, portInt) // 使用转换后的 portInt
// 3. 使用 exec.CommandContext 运行命令
// 添加 70 秒超时,防止命令挂起导致 HTTP 连接断开
ctx, cancel := context.WithTimeout(context.Background(), 70*time.Second)
defer cancel() // 确保 context 在函数退出时被取消
cmd := exec.CommandContext(ctx, "/bin/bash", "-c", shellCommand)
// 4. 捕获命令的输出
output, err := cmd.CombinedOutput()
// 5. 记录日志,以便诊断
logOutput := strings.TrimSpace(string(output))
logger.Infof("执行 ufw 端口放行命令(端口 %s结果\n%s", port, logOutput)
// 〔中文注释〕: 这里的错误处理现在只用于在后台记录日志。
if err != nil {
errorMsg := fmt.Sprintf("后台执行端口 %s 自动放行失败。错误: %v", port, err)
logger.Error(errorMsg)
// 〔可选〕: 未来可以在这里加入 Telegram 机器人通知等功能,来通知管理员任务失败。
}
}()
}