fc3a66dc83
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。 含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI(runs-on: ubuntu-latest) 与开源文档;镜像发布至 git.rc707blog.top Container Registry。 Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
4.5 KiB
Go
167 lines
4.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/prism/proxy/internal/country"
|
|
"github.com/prism/proxy/internal/mihomoapi"
|
|
"github.com/prism/proxy/internal/store"
|
|
)
|
|
|
|
type OutboundMember struct {
|
|
RuntimeName string `json:"runtime_name"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type OutboundGroup struct {
|
|
RuntimeName string `json:"runtime_name"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Source string `json:"source"`
|
|
Now string `json:"now"`
|
|
NowName string `json:"now_name"`
|
|
Selectable bool `json:"selectable"`
|
|
IsCountry bool `json:"is_country"`
|
|
Members []OutboundMember `json:"members"`
|
|
}
|
|
|
|
type OutboundOverview struct {
|
|
DefaultPolicy string `json:"default_policy"`
|
|
Groups []OutboundGroup `json:"groups"`
|
|
}
|
|
|
|
func (a *App) OutboundOverview(ctx context.Context) (OutboundOverview, error) {
|
|
settings, err := a.Store.GetSettings(ctx)
|
|
if err != nil {
|
|
return OutboundOverview{}, err
|
|
}
|
|
defaultPolicy := settings["default_policy"]
|
|
if defaultPolicy == "" {
|
|
defaultPolicy = "DIRECT"
|
|
}
|
|
|
|
base, secret, err := a.MihomoAPIBase(ctx)
|
|
if err != nil {
|
|
return OutboundOverview{}, err
|
|
}
|
|
live, err := mihomoapi.New(base, secret).ListProxiesCached(8 * time.Second)
|
|
if err != nil {
|
|
return OutboundOverview{}, fmt.Errorf("mihomo proxies: %w", err)
|
|
}
|
|
|
|
nodes, err := a.Store.ListProxyNodes(ctx, "", nil)
|
|
if err != nil {
|
|
return OutboundOverview{}, err
|
|
}
|
|
groups, err := a.Store.ListProxyGroups(ctx)
|
|
if err != nil {
|
|
return OutboundOverview{}, err
|
|
}
|
|
display := buildDisplayMap(nodes, groups)
|
|
|
|
var outGroups []OutboundGroup
|
|
seen := map[string]bool{}
|
|
for _, g := range groups {
|
|
if !g.Enabled {
|
|
continue
|
|
}
|
|
state, ok := live[g.RuntimeName]
|
|
if !ok || !mihomoapi.IsPolicyGroupType(state.Type) {
|
|
continue
|
|
}
|
|
seen[g.RuntimeName] = true
|
|
outGroups = append(outGroups, buildOutboundGroup(g.RuntimeName, g.Name, g.Source, state, display))
|
|
}
|
|
for name, state := range live {
|
|
if seen[name] || !mihomoapi.IsPolicyGroupType(state.Type) {
|
|
continue
|
|
}
|
|
outGroups = append(outGroups, buildOutboundGroup(name, display[name], "runtime", state, display))
|
|
}
|
|
|
|
return OutboundOverview{
|
|
DefaultPolicy: defaultPolicy,
|
|
Groups: outGroups,
|
|
}, nil
|
|
}
|
|
|
|
func (a *App) SelectOutboundGroup(ctx context.Context, groupRuntime, proxyRuntime string) error {
|
|
base, secret, err := a.MihomoAPIBase(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := mihomoapi.New(base, secret).SelectProxy(groupRuntime, proxyRuntime); err != nil {
|
|
return err
|
|
}
|
|
_ = a.Store.AddAuditLog(ctx, "select", "outbound", groupRuntime+" -> "+proxyRuntime)
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SetDefaultPolicy(ctx context.Context, policy string) error {
|
|
if err := a.Store.SetSettings(ctx, map[string]string{"default_policy": policy}); err != nil {
|
|
return err
|
|
}
|
|
return a.ReloadEngine(ctx)
|
|
}
|
|
|
|
func buildOutboundGroup(runtimeName, name, source string, state mihomoapi.ProxyState, display map[string]string) OutboundGroup {
|
|
if name == "" {
|
|
name = runtimeName
|
|
}
|
|
autoOnly := state.Type == "URLTest" || state.Type == "Fallback"
|
|
members := make([]OutboundMember, 0, len(state.All))
|
|
for _, m := range state.All {
|
|
if autoOnly && (m == "DIRECT" || m == "REJECT" || m == "REJECT-DROP") {
|
|
continue
|
|
}
|
|
if m == "DIRECT" || m == "REJECT" || m == "REJECT-DROP" {
|
|
members = append(members, OutboundMember{RuntimeName: m, Name: m})
|
|
continue
|
|
}
|
|
members = append(members, OutboundMember{
|
|
RuntimeName: m,
|
|
Name: displayName(display, m),
|
|
})
|
|
}
|
|
return OutboundGroup{
|
|
RuntimeName: runtimeName,
|
|
Name: name,
|
|
Type: state.Type,
|
|
Source: source,
|
|
Now: state.Now,
|
|
NowName: displayName(display, state.Now),
|
|
Selectable: state.Type == "Selector",
|
|
IsCountry: strings.HasPrefix(runtimeName, "country_"),
|
|
Members: members,
|
|
}
|
|
}
|
|
|
|
func buildDisplayMap(nodes []store.ProxyNode, groups []store.ProxyGroup) map[string]string {
|
|
m := map[string]string{
|
|
"DIRECT": "DIRECT",
|
|
"REJECT": "REJECT",
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, n := range nodes {
|
|
m[n.RuntimeName] = n.Name
|
|
if n.Country != "" && !seen[n.Country] {
|
|
seen[n.Country] = true
|
|
m[country.GroupRuntime(n.Country)] = country.Name(n.Country) + "(国家池)"
|
|
}
|
|
}
|
|
for _, g := range groups {
|
|
m[g.RuntimeName] = g.Name
|
|
}
|
|
return m
|
|
}
|
|
|
|
func displayName(display map[string]string, runtime string) string {
|
|
if name, ok := display[runtime]; ok && name != "" {
|
|
return name
|
|
}
|
|
return runtime
|
|
}
|