c26b06f1cc
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。 含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI 与开源文档; CI 使用 ubuntu-latest runner,Go/Docker 步骤走国内镜像与 shell,避免 GitHub 下载超时。 Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
4.7 KiB
Go
184 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/robfig/cron/v3"
|
|
|
|
"github.com/prism/proxy/internal/api"
|
|
"github.com/prism/proxy/internal/cache"
|
|
"github.com/prism/proxy/internal/configbuilder"
|
|
"github.com/prism/proxy/internal/core"
|
|
"github.com/prism/proxy/internal/geodata"
|
|
"github.com/prism/proxy/internal/health"
|
|
applog "github.com/prism/proxy/internal/log"
|
|
"github.com/prism/proxy/internal/metrics"
|
|
"github.com/prism/proxy/internal/mihomoapi"
|
|
"github.com/prism/proxy/internal/service"
|
|
"github.com/prism/proxy/internal/store"
|
|
"github.com/prism/proxy/internal/subscription"
|
|
"github.com/prism/proxy/internal/trafficlog"
|
|
)
|
|
|
|
func main() {
|
|
dataDir := flag.String("data", "data", "data directory")
|
|
flag.Parse()
|
|
|
|
log.Printf("Prism starting, data dir: %s", *dataDir)
|
|
|
|
if err := os.MkdirAll(*dataDir, 0o755); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
st, err := store.Open(*dataDir)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
log.Printf("database ready")
|
|
|
|
if n, err := st.BackfillNodeCountries(context.Background()); err != nil {
|
|
log.Printf("country backfill: %v", err)
|
|
} else if n > 0 {
|
|
log.Printf("country tags backfilled for %d nodes", n)
|
|
}
|
|
|
|
core.InitDataDir(*dataDir)
|
|
|
|
c := cache.New()
|
|
engine := core.NewEngine()
|
|
builder := configbuilder.New(st, *dataDir)
|
|
logCollector := applog.NewCollector(st, 5000, true)
|
|
subSvc := subscription.NewService(st)
|
|
|
|
app := &service.App{
|
|
Store: st,
|
|
Cache: c,
|
|
Engine: engine,
|
|
Builder: builder,
|
|
Subscription: subSvc,
|
|
Logs: logCollector,
|
|
DataDir: *dataDir,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
apiBase, secret, _ := app.MihomoAPIBase(ctx)
|
|
hc := health.NewChecker(st, c, apiBase, secret)
|
|
mc := metrics.NewCollector(st, c, apiBase, secret)
|
|
trafficCollector := trafficlog.NewCollector(st, 5000)
|
|
trafficCollector.Start(ctx, mihomoapi.New(apiBase, secret))
|
|
|
|
settings, _ := st.GetSettings(ctx)
|
|
applyGeoSettingsOnStart(*dataDir, settings)
|
|
if settings["geo_auto_download"] != "false" {
|
|
go func() {
|
|
log.Printf("checking geo data in background...")
|
|
cfg := geodata.ConfigFromSettings(settings)
|
|
if updated, err := geodata.Ensure(*dataDir, cfg, false); err != nil {
|
|
log.Printf("geo data: %v", err)
|
|
logCollector.Add("warn", "prism", "geo data: "+err.Error())
|
|
} else if len(updated) > 0 {
|
|
log.Printf("geo data downloaded: %v", updated)
|
|
}
|
|
}()
|
|
}
|
|
|
|
logCollector.Add("info", "prism", "Prism started")
|
|
log.Printf("loading proxy engine...")
|
|
|
|
if err := app.ReloadEngine(ctx); err != nil {
|
|
log.Printf("initial engine reload: %v (will retry after config)", err)
|
|
} else {
|
|
log.Printf("proxy engine ready")
|
|
}
|
|
go func() {
|
|
warmCtx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
_, _ = mc.Refresh(warmCtx)
|
|
app.RefreshEngineStatusAsync()
|
|
}()
|
|
|
|
srv := api.NewServer(app, hc, mc, subSvc, logCollector, trafficCollector)
|
|
router := srv.Router()
|
|
|
|
apiPort := settings["api_port"]
|
|
if apiPort == "" {
|
|
apiPort = "8080"
|
|
}
|
|
|
|
httpServer := &http.Server{
|
|
Addr: ":" + apiPort,
|
|
Handler: router,
|
|
}
|
|
|
|
cr := cron.New()
|
|
_, _ = cr.AddFunc("@every 1m", func() {
|
|
_, _ = mc.Refresh(context.Background())
|
|
})
|
|
_, _ = cr.AddFunc("@every 5m", func() {
|
|
settings, _ := st.GetSettings(context.Background())
|
|
sec, _ := strconv.Atoi(settings["health_interval_sec"])
|
|
if sec <= 0 {
|
|
sec = 300
|
|
}
|
|
// health checker runs on its own schedule approximation
|
|
_ = hc.TestAll(context.Background(), nil)
|
|
})
|
|
_, _ = cr.AddFunc("@every 10m", func() {
|
|
syncSubscriptions(app, subSvc)
|
|
})
|
|
cr.Start()
|
|
|
|
go func() {
|
|
log.Printf("Prism API listening on :%s", apiPort)
|
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
<-sig
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = httpServer.Shutdown(shutdownCtx)
|
|
cr.Stop()
|
|
engine.Shutdown()
|
|
}
|
|
|
|
func syncSubscriptions(app *service.App, subSvc *subscription.Service) {
|
|
ctx := context.Background()
|
|
settings, err := app.Store.GetSettings(ctx)
|
|
if err != nil || settings["sub_sync_enabled"] != "true" {
|
|
return
|
|
}
|
|
subs, err := app.Store.ListSubscriptions(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
for _, sub := range subs {
|
|
if !sub.Enabled {
|
|
continue
|
|
}
|
|
if sub.LastFetchAt != nil && now.Sub(*sub.LastFetchAt) < time.Duration(sub.IntervalSec)*time.Second {
|
|
continue
|
|
}
|
|
if err := subSvc.FetchAndStore(ctx, sub.ID); err != nil {
|
|
app.Logs.Add("error", "subscription", fmt.Sprintf("sync %s failed: %v", sub.Name, err))
|
|
continue
|
|
}
|
|
}
|
|
_ = app.ReloadEngine(ctx)
|
|
}
|