Files
opsx-tools 91974fc30f feat: 后端限速实现 + 前端i18n全面国际化 + 安全与代码质量修复
后端:
- trafficmgr: 实现 tc htb 限速(ApplyRateLimit/RemoveRateLimit),自动推导接口名
- 各协议服务: 代码质量改进与错误处理增强

前端:
- protocols.js: 170个硬编码中文字符串全部转为 i18n key,中英文双语翻译
- 提取 fmtBytes() 到 utils/format.js 公共模块(消除 4 处重复)
- router.js: 添加路由级权限守卫,非管理员无法直接 URL 访问管理页
- Tunnels.vue: SSE 日志流改用 fetch+ReadableStream,token 放 Authorization header
- VPNUsers.vue: 修复 catch 块中文硬编码 fallback
- 新增 TaskProgress 组件、taskmgr 后端任务管理
2026-07-23 12:57:46 +08:00

233 lines
5.0 KiB
Go

// Package taskmgr 提供后台并发任务管理器
package taskmgr
import (
"fmt"
"sync"
"time"
)
// TaskStatus 任务状态
type TaskStatus string
const (
StatusPending TaskStatus = "pending"
StatusRunning TaskStatus = "running"
StatusDone TaskStatus = "done"
StatusFailed TaskStatus = "failed"
StatusCanceled TaskStatus = "canceled"
)
// TaskAction 任务动作
type TaskAction string
const (
ActionStart TaskAction = "start"
ActionStop TaskAction = "stop"
ActionDelete TaskAction = "delete"
)
// SubTask 单个子任务(对应一个隧道)
type SubTask struct {
TunnelID uint `json:"tunnel_id"`
TunnelName string `json:"tunnel_name"`
Status TaskStatus `json:"status"`
Error string `json:"error,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
}
// Task 一个批量任务
type Task struct {
ID string `json:"id"`
Action TaskAction `json:"action"`
Status TaskStatus `json:"status"`
Total int `json:"total"`
Success int `json:"success"`
Failed int `json:"failed"`
SubTasks []SubTask `json:"sub_tasks"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Error string `json:"error,omitempty"`
}
// Progress 返回进度百分比 0-100
func (t *Task) Progress() int {
if t.Total == 0 {
return 0
}
done := t.Success + t.Failed
return done * 100 / t.Total
}
// Manager 内存任务管理器
type Manager struct {
mu sync.RWMutex
tasks map[string]*Task
seq int
}
func NewManager() *Manager {
return &Manager{tasks: make(map[string]*Task)}
}
// Create 创建任务并返回 ID
func (m *Manager) Create(action TaskAction, tunnelIDs []uint, names map[uint]string) string {
m.mu.Lock()
m.seq++
id := fmt.Sprintf("task_%d_%d", time.Now().UnixMilli(), m.seq)
subTasks := make([]SubTask, len(tunnelIDs))
for i, tid := range tunnelIDs {
subTasks[i] = SubTask{
TunnelID: tid,
TunnelName: names[tid],
Status: StatusPending,
}
}
t := &Task{
ID: id,
Action: action,
Status: StatusPending,
Total: len(tunnelIDs),
SubTasks: subTasks,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
m.tasks[id] = t
m.mu.Unlock()
// 自动清理 30 分钟前的任务
go m.cleanup()
return id
}
// Get 获取任务快照
func (m *Manager) Get(id string) (*Task, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
t, ok := m.tasks[id]
if !ok {
return nil, false
}
// 返回副本
cp := *t
cp.SubTasks = make([]SubTask, len(t.SubTasks))
copy(cp.SubTasks, t.SubTasks)
return &cp, true
}
// List 列出最近的任务(最多 50 个)
func (m *Manager) List() []*Task {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]*Task, 0, len(m.tasks))
for _, t := range m.tasks {
cp := *t
cp.SubTasks = make([]SubTask, len(t.SubTasks))
copy(cp.SubTasks, t.SubTasks)
out = append(out, &cp)
}
// 按创建时间倒序
for i := 0; i < len(out)-1; i++ {
for j := i + 1; j < len(out); j++ {
if out[j].CreatedAt.After(out[i].CreatedAt) {
out[i], out[j] = out[j], out[i]
}
}
}
if len(out) > 50 {
out = out[:50]
}
return out
}
// MarkRunning 标记子任务运行中
func (m *Manager) MarkRunning(taskID string, tunnelID uint) {
m.mu.Lock()
defer m.mu.Unlock()
t, ok := m.tasks[taskID]
if !ok {
return
}
t.Status = StatusRunning
now := time.Now()
t.UpdatedAt = now
for i := range t.SubTasks {
if t.SubTasks[i].TunnelID == tunnelID {
t.SubTasks[i].Status = StatusRunning
t.SubTasks[i].StartedAt = &now
break
}
}
}
// MarkDone 标记子任务成功
func (m *Manager) MarkDone(taskID string, tunnelID uint) {
m.mu.Lock()
defer m.mu.Unlock()
t, ok := m.tasks[taskID]
if !ok {
return
}
now := time.Now()
t.UpdatedAt = now
for i := range t.SubTasks {
if t.SubTasks[i].TunnelID == tunnelID {
t.SubTasks[i].Status = StatusDone
t.SubTasks[i].FinishedAt = &now
t.Success++
break
}
}
t.maybeDone()
}
// MarkFailed 标记子任务失败
func (m *Manager) MarkFailed(taskID string, tunnelID uint, err error) {
m.mu.Lock()
defer m.mu.Unlock()
t, ok := m.tasks[taskID]
if !ok {
return
}
now := time.Now()
t.UpdatedAt = now
for i := range t.SubTasks {
if t.SubTasks[i].TunnelID == tunnelID {
t.SubTasks[i].Status = StatusFailed
t.SubTasks[i].Error = err.Error()
t.SubTasks[i].FinishedAt = &now
t.Failed++
break
}
}
t.maybeDone()
}
// maybeDone 检查任务是否全部完成(调用者需持锁)
func (t *Task) maybeDone() {
if t.Success+t.Failed >= t.Total {
if t.Failed > 0 {
t.Status = StatusDone // 部分失败也算完成
} else {
t.Status = StatusDone
}
now := time.Now()
t.UpdatedAt = now
}
}
func (m *Manager) cleanup() {
m.mu.Lock()
defer m.mu.Unlock()
cutoff := time.Now().Add(-30 * time.Minute)
for id, t := range m.tasks {
if t.Status == StatusDone || t.Status == StatusFailed || t.Status == StatusCanceled {
if t.UpdatedAt.Before(cutoff) {
delete(m.tasks, id)
}
}
}
}