mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-16 01:24:24 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91d79b6b3a | |||
| fc7df6bd64 | |||
| 25dfb84324 | |||
| 4ebd6703fe | |||
| 1f53a39784 |
@@ -1,6 +1,6 @@
|
|||||||
# FLVX
|
# FLVX
|
||||||
|
|
||||||
> **联系我们**: [Telegram群组](https://t.me/flvxpanel)
|
> **联系我们**: [Telegram群组](https://t.me/flvxchannel)
|
||||||
|
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|||||||
@@ -180,13 +180,41 @@ func (e *systemUpgradeExecutor) selectComposeAsset(current []byte) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *systemUpgradeExecutor) helperScript() string {
|
func (e *systemUpgradeExecutor) helperScript() string {
|
||||||
return strings.Join([]string{
|
return `set -eu
|
||||||
"set -eu",
|
LOGFILE="$PANEL_DEPLOY_DIR/upgrade.log"
|
||||||
`cd "$PANEL_DEPLOY_DIR"`,
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE"; }
|
||||||
"docker compose pull backend frontend",
|
|
||||||
"sleep 5",
|
cd "$PANEL_DEPLOY_DIR"
|
||||||
"docker compose up -d backend frontend",
|
echo "" > "$LOGFILE"
|
||||||
}, "\n")
|
log "开始面板升级"
|
||||||
|
log "工作目录: $(pwd)"
|
||||||
|
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
log "错误: docker-compose.yml 不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
log "错误: .env 不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "拉取新镜像..."
|
||||||
|
if ! docker compose pull backend frontend 2>&1 | tee -a "$LOGFILE"; then
|
||||||
|
log "错误: 拉取镜像失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "等待旧容器释放资源..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
log "重启服务(force-recreate)..."
|
||||||
|
if ! docker compose up -d --force-recreate --remove-orphans backend frontend 2>&1 | tee -a "$LOGFILE"; then
|
||||||
|
log "错误: 重启服务失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "升级完成"
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *systemUpgradeExecutor) buildHelperRunArgs(imageID, helperName string) ([]string, error) {
|
func (e *systemUpgradeExecutor) buildHelperRunArgs(imageID, helperName string) ([]string, error) {
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ func autoMigrateAll(db *gorm.DB) error {
|
|||||||
m := db.Migrator()
|
m := db.Migrator()
|
||||||
hasNode := m.HasTable(&model.Node{})
|
hasNode := m.HasTable(&model.Node{})
|
||||||
hasTunnel := m.HasTable(&model.Tunnel{})
|
hasTunnel := m.HasTable(&model.Tunnel{})
|
||||||
|
hasForward := m.HasTable(&model.Forward{})
|
||||||
|
|
||||||
for _, item := range models {
|
for _, item := range models {
|
||||||
if hasNode {
|
if hasNode {
|
||||||
@@ -316,6 +317,11 @@ func autoMigrateAll(db *gorm.DB) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if hasForward {
|
||||||
|
if _, ok := item.(*model.Forward); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := db.AutoMigrate(item); err != nil {
|
if err := db.AutoMigrate(item); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -396,7 +402,7 @@ func prepareSQLiteLegacyColumns(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.HasTable(&model.Forward{}) {
|
if m.HasTable(&model.Forward{}) {
|
||||||
for _, field := range []string{"ProxyProtocol"} {
|
for _, field := range []string{"MaxConn", "IPMaxConn", "IPSpeedID", "ProxyProtocol"} {
|
||||||
if m.HasColumn(&model.Forward{}, field) {
|
if m.HasColumn(&model.Forward{}, field) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,68 @@ func TestOpenBackfillsSQLiteLegacyTunnelProbeTargetColumns(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenBackfillsSQLiteLegacyForwardColumns(t *testing.T) {
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "legacy-forward.db")
|
||||||
|
db, err := gorm.Open(gsqlite.Open(dbPath), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open legacy sqlite: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Exec(`
|
||||||
|
CREATE TABLE forward (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
user_name VARCHAR(100) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
tunnel_id INTEGER NOT NULL,
|
||||||
|
remote_addr TEXT NOT NULL,
|
||||||
|
strategy VARCHAR(100) NOT NULL DEFAULT 'fifo',
|
||||||
|
in_flow INTEGER NOT NULL DEFAULT 0,
|
||||||
|
out_flow INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_time INTEGER NOT NULL,
|
||||||
|
updated_time INTEGER NOT NULL,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
inx INTEGER NOT NULL DEFAULT 0,
|
||||||
|
speed_id INTEGER
|
||||||
|
)
|
||||||
|
`).Error; err != nil {
|
||||||
|
t.Fatalf("create legacy forward table: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
INSERT INTO forward(id, user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx, speed_id)
|
||||||
|
VALUES(1, 2, 'legacy-user', 'legacy-forward', 3, '127.0.0.1:9000', 'fifo', 0, 0, 1, 1, 1, 0, NULL)
|
||||||
|
`).Error; err != nil {
|
||||||
|
t.Fatalf("insert legacy forward: %v", err)
|
||||||
|
}
|
||||||
|
if sqlDB, _ := db.DB(); sqlDB != nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open migrated sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = r.Close() })
|
||||||
|
|
||||||
|
m := r.DB().Migrator()
|
||||||
|
for _, field := range []string{"MaxConn", "IPMaxConn", "IPSpeedID", "ProxyProtocol"} {
|
||||||
|
if !m.HasColumn(&model.Forward{}, field) {
|
||||||
|
t.Fatalf("expected forward.%s column to exist", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxConn, ipMaxConn, proxyProtocol int
|
||||||
|
var ipSpeedID sql.NullInt64
|
||||||
|
if err := r.DB().Raw(`SELECT max_conn, ip_max_conn, ip_speed_id, proxy_protocol FROM forward WHERE id = 1`).Row().Scan(&maxConn, &ipMaxConn, &ipSpeedID, &proxyProtocol); err != nil {
|
||||||
|
t.Fatalf("query forward defaults: %v", err)
|
||||||
|
}
|
||||||
|
if maxConn != 0 || ipMaxConn != 0 || ipSpeedID.Valid || proxyProtocol != 0 {
|
||||||
|
t.Fatalf("expected default forward columns 0/0/NULL/0, got max_conn=%d ip_max_conn=%d ip_speed_id=%+v proxy_protocol=%d", maxConn, ipMaxConn, ipSpeedID, proxyProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMigrateSchemaRunsPostgresIDRepairEvenAtCurrentVersion(t *testing.T) {
|
func TestMigrateSchemaRunsPostgresIDRepairEvenAtCurrentVersion(t *testing.T) {
|
||||||
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
|
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
|||||||
@@ -158,6 +158,9 @@ type WebSocketReporter struct {
|
|||||||
addr string // 保存服务器地址
|
addr string // 保存服务器地址
|
||||||
secret string // 保存密钥
|
secret string // 保存密钥
|
||||||
version string // 保存版本号
|
version string // 保存版本号
|
||||||
|
http int
|
||||||
|
tls int
|
||||||
|
socks int
|
||||||
preferredWSScheme string
|
preferredWSScheme string
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
curBackoff time.Duration // 当前重连退避间隔
|
curBackoff time.Duration // 当前重连退避间隔
|
||||||
@@ -296,9 +299,9 @@ func (w *WebSocketReporter) connect() error {
|
|||||||
Socks int `json:"socks"`
|
Socks int `json:"socks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg LocalConfig
|
cfg := LocalConfig{Http: w.http, Tls: w.tls, Socks: w.socks}
|
||||||
if b, err := os.ReadFile("config.json"); err == nil {
|
if b, err := os.ReadFile("config.json"); err == nil {
|
||||||
json.Unmarshal(b, &cfg)
|
_ = json.Unmarshal(b, &cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := buildWebSocketCandidates(w.addr, w.secret, w.version, cfg.Http, cfg.Tls, cfg.Socks, w.preferredWSScheme)
|
candidates := buildWebSocketCandidates(w.addr, w.secret, w.version, cfg.Http, cfg.Tls, cfg.Socks, w.preferredWSScheme)
|
||||||
@@ -1369,7 +1372,7 @@ func (w *WebSocketReporter) handleUpgradeAgent(data interface{}) error {
|
|||||||
// 执行重启脚本
|
// 执行重启脚本
|
||||||
// 使用 systemd-run 在独立的 transient unit 中运行重启脚本,
|
// 使用 systemd-run 在独立的 transient unit 中运行重启脚本,
|
||||||
// 避免 systemctl stop 杀死 flux_agent cgroup 内所有进程(包括此脚本自身)导致 mv 未执行。
|
// 避免 systemctl stop 杀死 flux_agent cgroup 内所有进程(包括此脚本自身)导致 mv 未执行。
|
||||||
script := fmt.Sprintf("sleep 1 && systemctl stop flux_agent && mv %s %s && systemctl start flux_agent", tmpPath, binaryPath)
|
script := buildAgentRestartScript(tmpPath, binaryPath)
|
||||||
cmd := exec.Command("systemd-run", "--quiet", "/bin/sh", "-c", script)
|
cmd := exec.Command("systemd-run", "--quiet", "/bin/sh", "-c", script)
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
os.Remove(tmpPath)
|
os.Remove(tmpPath)
|
||||||
@@ -1403,6 +1406,14 @@ func (w *WebSocketReporter) handleRollbackAgent(data interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildAgentRestartScript(tmpPath, binaryPath string) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"sleep 1 && systemctl stop flux_agent && legacy_service='' && for service_file in /etc/systemd/system/gost.service /lib/systemd/system/gost.service /usr/lib/systemd/system/gost.service; do if [ -f \"$service_file\" ] && grep -Fq \"WorkingDirectory=/etc/gost\" \"$service_file\" && (grep -Fq \"ExecStart=/etc/gost/gost\" \"$service_file\" || (grep -Fq \"ExecStart=/usr/local/bin/gost\" \"$service_file\" && [ -f /etc/gost/config.json ] && [ -f /etc/gost/gost.json ])); then legacy_service=\"$service_file\"; break; fi; done && if [ -n \"$legacy_service\" ]; then (systemctl stop gost 2>/dev/null || true) && (systemctl disable gost 2>/dev/null || true) && rm -f /usr/local/bin/gost /etc/gost/gost \"$legacy_service\" && (systemctl daemon-reload 2>/dev/null || true); fi && mv %s %s && systemctl start flux_agent",
|
||||||
|
tmpPath,
|
||||||
|
binaryPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
|
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
|
||||||
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
|
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
|
||||||
path := "config.json"
|
path := "config.json"
|
||||||
@@ -1650,13 +1661,16 @@ func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls
|
|||||||
candidates := buildWebSocketCandidates(addr, secret, version, http, tls, socks, "")
|
candidates := buildWebSocketCandidates(addr, secret, version, http, tls, socks, "")
|
||||||
fullURL := candidates[0]
|
fullURL := candidates[0]
|
||||||
|
|
||||||
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
fmt.Printf("🔗 WebSocket连接URL: %s\n", sanitizeWebSocketURL(fullURL))
|
||||||
|
|
||||||
reporter := NewWebSocketReporter(fullURL, secret)
|
reporter := NewWebSocketReporter(fullURL, secret)
|
||||||
// 保存 addr, secret, version 供重连时使用
|
// 保存 addr, secret, version 和协议能力供重连时使用
|
||||||
reporter.addr = addr
|
reporter.addr = addr
|
||||||
reporter.secret = secret
|
reporter.secret = secret
|
||||||
reporter.version = version
|
reporter.version = version
|
||||||
|
reporter.http = http
|
||||||
|
reporter.tls = tls
|
||||||
|
reporter.socks = socks
|
||||||
reporter.Start()
|
reporter.Start()
|
||||||
return reporter
|
return reporter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,45 @@
|
|||||||
package socket
|
package socket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func captureStdout(t *testing.T, fn func()) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
orig := os.Stdout
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create stdout pipe: %v", err)
|
||||||
|
}
|
||||||
|
os.Stdout = w
|
||||||
|
defer func() {
|
||||||
|
os.Stdout = orig
|
||||||
|
_ = w.Close()
|
||||||
|
_ = r.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
fn()
|
||||||
|
|
||||||
|
_ = w.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&buf, r); err != nil {
|
||||||
|
t.Fatalf("read stdout: %v", err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildWebSocketCandidatesSecureFirst(t *testing.T) {
|
func TestBuildWebSocketCandidatesSecureFirst(t *testing.T) {
|
||||||
candidates := buildWebSocketCandidates("panel.example.com:443", "abc", "2.0.2", 1, 0, 1, "")
|
candidates := buildWebSocketCandidates("panel.example.com:443", "abc", "2.0.2", 1, 0, 1, "")
|
||||||
|
|
||||||
@@ -133,3 +163,103 @@ func TestFormatWebSocketDialErrorIncludesHTTPStatus(t *testing.T) {
|
|||||||
t.Fatalf("expected response body in message, got %s", msg)
|
t.Fatalf("expected response body in message, got %s", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentUpgradeRestartScriptStopsLegacyGostService(t *testing.T) {
|
||||||
|
script := buildAgentRestartScript("/tmp/flux_agent.new", "/etc/flux_agent/flux_agent")
|
||||||
|
|
||||||
|
if !strings.Contains(script, "systemctl stop flux_agent") {
|
||||||
|
t.Fatalf("expected script to stop flux_agent, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "mv /tmp/flux_agent.new /etc/flux_agent/flux_agent") {
|
||||||
|
t.Fatalf("expected script to replace the flux_agent binary, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "systemctl stop gost") {
|
||||||
|
t.Fatalf("expected script to stop the legacy gost service, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "systemctl disable gost") {
|
||||||
|
t.Fatalf("expected script to disable the legacy gost service, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "rm -f /usr/local/bin/gost") {
|
||||||
|
t.Fatalf("expected script to remove the legacy gost binary, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "WorkingDirectory=/etc/gost") {
|
||||||
|
t.Fatalf("expected script to scope cleanup to the legacy FLVX gost service definition, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "systemctl start flux_agent") {
|
||||||
|
t.Fatalf("expected script to restart flux_agent, got %s", script)
|
||||||
|
}
|
||||||
|
if strings.Contains(script, "systemctl stop flux_agent && systemctl stop gost 2>/dev/null || true") {
|
||||||
|
t.Fatalf("expected legacy gost cleanup fallback to be scoped, got %s", script)
|
||||||
|
}
|
||||||
|
if runtime.GOARCH == "" {
|
||||||
|
t.Fatalf("unexpected empty runtime arch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartWebSocketReporterWithConfigPreservesProtocolDefaultsWithoutConfigFile(t *testing.T) {
|
||||||
|
origDial := wsDial
|
||||||
|
defer func() { wsDial = origDial }()
|
||||||
|
|
||||||
|
origWD, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get working directory: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Chdir(origWD)
|
||||||
|
})
|
||||||
|
if err := os.Chdir(t.TempDir()); err != nil {
|
||||||
|
t.Fatalf("change working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
urls := make(chan string, 1)
|
||||||
|
wsDial = func(_ *websocket.Dialer, rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
select {
|
||||||
|
case urls <- rawURL:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil, nil, errors.New("dial failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
reporter := StartWebSocketReporterWithConfig("panel.example.com:443", "abc", 1, 0, 1, "2.0.2")
|
||||||
|
defer reporter.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case rawURL := <-urls:
|
||||||
|
if !strings.Contains(rawURL, "http=1&tls=0&socks=1") {
|
||||||
|
t.Fatalf("expected reconnect URL to preserve startup protocol values, got %s", rawURL)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for websocket dial")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartWebSocketReporterWithConfigLogsSanitizedURL(t *testing.T) {
|
||||||
|
origDial := wsDial
|
||||||
|
defer func() { wsDial = origDial }()
|
||||||
|
|
||||||
|
ready := make(chan struct{}, 1)
|
||||||
|
wsDial = func(_ *websocket.Dialer, rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
select {
|
||||||
|
case ready <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil, nil, errors.New("dial failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := captureStdout(t, func() {
|
||||||
|
reporter := StartWebSocketReporterWithConfig("panel.example.com:443", "abc123", 1, 0, 1, "2.0.2")
|
||||||
|
select {
|
||||||
|
case <-ready:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for websocket dial")
|
||||||
|
}
|
||||||
|
reporter.Stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
if strings.Contains(output, "secret=abc123") {
|
||||||
|
t.Fatalf("expected logged websocket URL to mask the node secret, got %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "secret=%2A%2A%2A") {
|
||||||
|
t.Fatalf("expected logged websocket URL to include masked secret, got %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+79
-11
@@ -24,6 +24,11 @@ get_architecture() {
|
|||||||
|
|
||||||
# 安装目录
|
# 安装目录
|
||||||
INSTALL_DIR="/etc/flux_agent"
|
INSTALL_DIR="/etc/flux_agent"
|
||||||
|
LEGACY_GOST_BINARY="/usr/local/bin/gost"
|
||||||
|
LEGACY_GOST_CONFIG_DIR="/etc/gost"
|
||||||
|
LEGACY_GOST_SERVICE_FILE_ETC="/etc/systemd/system/gost.service"
|
||||||
|
LEGACY_GOST_SERVICE_FILE_LIB="/lib/systemd/system/gost.service"
|
||||||
|
LEGACY_GOST_SERVICE_FILE_USR_LIB="/usr/lib/systemd/system/gost.service"
|
||||||
|
|
||||||
# 镜像加速配置(可由面板传入或交互式询问)
|
# 镜像加速配置(可由面板传入或交互式询问)
|
||||||
PROXY_ENABLED="${PROXY_ENABLED:-}"
|
PROXY_ENABLED="${PROXY_ENABLED:-}"
|
||||||
@@ -234,6 +239,69 @@ check_and_install_tcpkill() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
local value="$1"
|
||||||
|
value=${value//\\/\\\\}
|
||||||
|
value=${value//\"/\\\"}
|
||||||
|
value=${value//$'\n'/\\n}
|
||||||
|
value=${value//$'\r'/\\r}
|
||||||
|
value=${value//$'\t'/\\t}
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_flux_agent_config() {
|
||||||
|
local path="$1"
|
||||||
|
printf '{\n "addr": "%s",\n "secret": "%s"\n}\n' \
|
||||||
|
"$(json_escape "$SERVER_ADDR")" \
|
||||||
|
"$(json_escape "$SECRET")" > "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
local matched_service_files=()
|
||||||
|
local service_file=""
|
||||||
|
local removed_service_file="0"
|
||||||
|
|
||||||
|
for service_file in "$LEGACY_GOST_SERVICE_FILE_ETC" "$LEGACY_GOST_SERVICE_FILE_LIB" "$LEGACY_GOST_SERVICE_FILE_USR_LIB"; do
|
||||||
|
if [[ ! -f "$service_file" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! grep -Fq "WorkingDirectory=$LEGACY_GOST_CONFIG_DIR" "$service_file"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if grep -Fq "ExecStart=$LEGACY_GOST_CONFIG_DIR/gost" "$service_file" || \
|
||||||
|
(grep -Fq "ExecStart=$LEGACY_GOST_BINARY" "$service_file" && [[ -f "$LEGACY_GOST_CONFIG_DIR/config.json" && -f "$LEGACY_GOST_CONFIG_DIR/gost.json" ]]); then
|
||||||
|
matched_service_files+=("$service_file")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#matched_service_files[@]} -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl list-units --full -all 2>/dev/null | grep -Fq "gost.service"; then
|
||||||
|
systemctl stop gost 2>/dev/null || true
|
||||||
|
systemctl disable gost 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
for service_file in "${matched_service_files[@]}"; do
|
||||||
|
if [[ -f "$service_file" ]]; then
|
||||||
|
rm -f "$service_file"
|
||||||
|
removed_service_file="1"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -f "$LEGACY_GOST_BINARY" ]]; then
|
||||||
|
rm -f "$LEGACY_GOST_BINARY"
|
||||||
|
fi
|
||||||
|
if [[ -f "$LEGACY_GOST_CONFIG_DIR/gost" ]]; then
|
||||||
|
rm -f "$LEGACY_GOST_CONFIG_DIR/gost"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$removed_service_file" == "1" ]]; then
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 获取用户输入的配置参数
|
# 获取用户输入的配置参数
|
||||||
get_config_params() {
|
get_config_params() {
|
||||||
@@ -279,6 +347,8 @@ install_flux_agent() {
|
|||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
|
local tmp_binary="$INSTALL_DIR/flux_agent.new"
|
||||||
|
|
||||||
# 停止并禁用已有服务
|
# 停止并禁用已有服务
|
||||||
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
||||||
echo "🔍 检测到已存在的flux_agent服务"
|
echo "🔍 检测到已存在的flux_agent服务"
|
||||||
@@ -286,16 +356,17 @@ install_flux_agent() {
|
|||||||
systemctl disable flux_agent 2>/dev/null && echo "🚫 禁用自启"
|
systemctl disable flux_agent 2>/dev/null && echo "🚫 禁用自启"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 删除旧文件
|
|
||||||
[[ -f "$INSTALL_DIR/flux_agent" ]] && echo "🧹 删除旧文件 flux_agent" && rm -f "$INSTALL_DIR/flux_agent"
|
|
||||||
|
|
||||||
# 下载 flux_agent
|
# 下载 flux_agent
|
||||||
echo "⬇️ 下载 flux_agent 中..."
|
echo "⬇️ 下载 flux_agent 中..."
|
||||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent"
|
rm -f "$tmp_binary"
|
||||||
if [[ ! -f "$INSTALL_DIR/flux_agent" || ! -s "$INSTALL_DIR/flux_agent" ]]; then
|
curl -L "$DOWNLOAD_URL" -o "$tmp_binary"
|
||||||
|
if [[ ! -f "$tmp_binary" || ! -s "$tmp_binary" ]]; then
|
||||||
|
rm -f "$tmp_binary"
|
||||||
echo "❌ 下载失败,请检查网络或下载链接。"
|
echo "❌ 下载失败,请检查网络或下载链接。"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
cleanup_legacy_gost_installation
|
||||||
|
mv "$tmp_binary" "$INSTALL_DIR/flux_agent"
|
||||||
chmod +x "$INSTALL_DIR/flux_agent"
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
echo "✅ 下载完成"
|
echo "✅ 下载完成"
|
||||||
|
|
||||||
@@ -305,12 +376,7 @@ install_flux_agent() {
|
|||||||
# 写入 config.json (安装时总是创建新的)
|
# 写入 config.json (安装时总是创建新的)
|
||||||
CONFIG_FILE="$INSTALL_DIR/config.json"
|
CONFIG_FILE="$INSTALL_DIR/config.json"
|
||||||
echo "📄 创建新配置: config.json"
|
echo "📄 创建新配置: config.json"
|
||||||
cat > "$CONFIG_FILE" <<EOF
|
write_flux_agent_config "$CONFIG_FILE"
|
||||||
{
|
|
||||||
"addr": "$SERVER_ADDR",
|
|
||||||
"secret": "$SECRET"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 写入 gost.json
|
# 写入 gost.json
|
||||||
GOST_CONFIG="$INSTALL_DIR/gost.json"
|
GOST_CONFIG="$INSTALL_DIR/gost.json"
|
||||||
@@ -380,11 +446,13 @@ update_flux_agent() {
|
|||||||
|
|
||||||
# 先下载新版本
|
# 先下载新版本
|
||||||
echo "⬇️ 下载最新版本..."
|
echo "⬇️ 下载最新版本..."
|
||||||
|
rm -f "$INSTALL_DIR/flux_agent.new"
|
||||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent.new"
|
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent.new"
|
||||||
if [[ ! -f "$INSTALL_DIR/flux_agent.new" || ! -s "$INSTALL_DIR/flux_agent.new" ]]; then
|
if [[ ! -f "$INSTALL_DIR/flux_agent.new" || ! -s "$INSTALL_DIR/flux_agent.new" ]]; then
|
||||||
echo "❌ 下载失败。"
|
echo "❌ 下载失败。"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
cleanup_legacy_gost_installation
|
||||||
|
|
||||||
# 停止服务
|
# 停止服务
|
||||||
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ EOF
|
|||||||
chmod +x "$INSTALL_DIR/flux_agent"
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
|
|
||||||
local ask_called="0"
|
local ask_called="0"
|
||||||
|
local cleanup_called="0"
|
||||||
|
|
||||||
ask_proxy_config() {
|
ask_proxy_config() {
|
||||||
ask_called="1"
|
ask_called="1"
|
||||||
@@ -105,6 +106,10 @@ EOF
|
|||||||
DOWNLOAD_URL=""
|
DOWNLOAD_URL=""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
cleanup_called="1"
|
||||||
|
}
|
||||||
|
|
||||||
check_and_install_tcpkill() { :; }
|
check_and_install_tcpkill() { :; }
|
||||||
|
|
||||||
systemctl() {
|
systemctl() {
|
||||||
@@ -133,6 +138,7 @@ EOF
|
|||||||
update_flux_agent >/dev/null
|
update_flux_agent >/dev/null
|
||||||
|
|
||||||
assert_equals "1" "$ask_called" "update_flux_agent should ask for proxy config before downloading"
|
assert_equals "1" "$ask_called" "update_flux_agent should ask for proxy config before downloading"
|
||||||
|
assert_equals "1" "$cleanup_called" "update_flux_agent should clean up legacy gost before restarting the agent"
|
||||||
assert_equals "$(build_download_url)" "$DOWNLOAD_URL" "update_flux_agent should honor the prompted proxy choice"
|
assert_equals "$(build_download_url)" "$DOWNLOAD_URL" "update_flux_agent should honor the prompted proxy choice"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,6 +161,198 @@ test_update_flux_agent_skips_proxy_prompt_when_not_installed() (
|
|||||||
assert_equals "0" "$ask_called" "update_flux_agent should not prompt for proxy config when the agent is missing"
|
assert_equals "0" "$ask_called" "update_flux_agent should not prompt for proxy config when the agent is missing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_install_flux_agent_preserves_legacy_gost_when_download_fails() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
INSTALL_DIR=$(mktemp -d)
|
||||||
|
cat > "$INSTALL_DIR/flux_agent" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "old version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
|
SERVER_ADDR="panel.example.com:443"
|
||||||
|
SECRET="secret"
|
||||||
|
DOWNLOAD_URL="https://example.com/gost"
|
||||||
|
|
||||||
|
local cleanup_called="0"
|
||||||
|
local rc="0"
|
||||||
|
|
||||||
|
ask_proxy_config() { :; }
|
||||||
|
ensure_download_url_initialized() { :; }
|
||||||
|
get_config_params() { :; }
|
||||||
|
check_and_install_tcpkill() { :; }
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
cleanup_called="1"
|
||||||
|
}
|
||||||
|
systemctl() { return 0; }
|
||||||
|
curl() { return 0; }
|
||||||
|
|
||||||
|
( install_flux_agent >/dev/null ) || rc="$?"
|
||||||
|
|
||||||
|
assert_equals "1" "$rc" "install_flux_agent should fail when the download artifact is missing"
|
||||||
|
assert_equals "0" "$cleanup_called" "install_flux_agent should preserve legacy gost when download fails"
|
||||||
|
[[ -f "$INSTALL_DIR/flux_agent" ]] || fail "install_flux_agent should keep the existing flux_agent binary when download fails"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_update_flux_agent_preserves_legacy_gost_when_download_fails() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
INSTALL_DIR=$(mktemp -d)
|
||||||
|
cat > "$INSTALL_DIR/flux_agent" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "old version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
|
cat > "$INSTALL_DIR/flux_agent.new" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "stale version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$INSTALL_DIR/flux_agent.new"
|
||||||
|
|
||||||
|
local cleanup_called="0"
|
||||||
|
local rc="0"
|
||||||
|
|
||||||
|
ask_proxy_config() {
|
||||||
|
PROXY_ENABLED="false"
|
||||||
|
DOWNLOAD_URL="https://example.com/gost"
|
||||||
|
}
|
||||||
|
check_and_install_tcpkill() { :; }
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
cleanup_called="1"
|
||||||
|
}
|
||||||
|
systemctl() { return 0; }
|
||||||
|
curl() { return 0; }
|
||||||
|
|
||||||
|
update_flux_agent >/dev/null || rc="$?"
|
||||||
|
|
||||||
|
assert_equals "1" "$rc" "update_flux_agent should fail when the download artifact is missing"
|
||||||
|
assert_equals "0" "$cleanup_called" "update_flux_agent should preserve legacy gost when download fails"
|
||||||
|
[[ ! -f "$INSTALL_DIR/flux_agent.new" ]] || fail "update_flux_agent should remove stale download artifacts before retrying"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_install_flux_agent_writes_json_safe_config() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
INSTALL_DIR=$(mktemp -d)
|
||||||
|
SERVER_ADDR='panel"addr'
|
||||||
|
SECRET='sec\ret"1'
|
||||||
|
DOWNLOAD_URL="https://example.com/gost"
|
||||||
|
|
||||||
|
ask_proxy_config() { :; }
|
||||||
|
ensure_download_url_initialized() { :; }
|
||||||
|
get_config_params() { :; }
|
||||||
|
check_and_install_tcpkill() { :; }
|
||||||
|
cleanup_legacy_gost_installation() { :; }
|
||||||
|
systemctl() { return 0; }
|
||||||
|
curl() {
|
||||||
|
local output=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
if [[ "$1" == "-o" ]]; then
|
||||||
|
output="$2"
|
||||||
|
shift 2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
cat > "$output" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "new version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
( install_flux_agent >/dev/null 2>/dev/null ) || true
|
||||||
|
|
||||||
|
local actual
|
||||||
|
actual=$(<"$INSTALL_DIR/config.json")
|
||||||
|
local expected=$'{\n "addr": "panel\\"addr",\n "secret": "sec\\\\ret\\"1"\n}'
|
||||||
|
|
||||||
|
assert_equals "$expected" "$actual" "install_flux_agent should JSON-escape config values"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_cleanup_legacy_gost_installation_removes_service_and_binary() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
LEGACY_GOST_BINARY=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_ETC=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_USR_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_CONFIG_DIR=$(mktemp -d)
|
||||||
|
cat > "$LEGACY_GOST_SERVICE_FILE_ETC" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Gost Proxy Service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=$LEGACY_GOST_CONFIG_DIR
|
||||||
|
ExecStart=$LEGACY_GOST_CONFIG_DIR/gost
|
||||||
|
EOF
|
||||||
|
: > "$LEGACY_GOST_CONFIG_DIR/config.json"
|
||||||
|
: > "$LEGACY_GOST_CONFIG_DIR/gost.json"
|
||||||
|
|
||||||
|
local systemctl_calls=""
|
||||||
|
|
||||||
|
systemctl() {
|
||||||
|
systemctl_calls+=$'\n'"$*"
|
||||||
|
if [[ "$1" == "list-units" ]]; then
|
||||||
|
printf 'gost.service loaded active running\n'
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation >/dev/null
|
||||||
|
|
||||||
|
if [[ -e "$LEGACY_GOST_BINARY" ]]; then
|
||||||
|
fail "cleanup_legacy_gost_installation should remove the legacy gost binary"
|
||||||
|
fi
|
||||||
|
if [[ -e "$LEGACY_GOST_SERVICE_FILE_ETC" ]]; then
|
||||||
|
fail "cleanup_legacy_gost_installation should remove the legacy gost service file"
|
||||||
|
fi
|
||||||
|
[[ "$systemctl_calls" == *"stop gost"* ]] || fail "cleanup_legacy_gost_installation should stop the legacy gost service"
|
||||||
|
[[ "$systemctl_calls" == *"disable gost"* ]] || fail "cleanup_legacy_gost_installation should disable the legacy gost service"
|
||||||
|
[[ "$systemctl_calls" == *"daemon-reload"* ]] || fail "cleanup_legacy_gost_installation should reload systemd after removing the legacy service"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_cleanup_legacy_gost_installation_preserves_unrelated_gost() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
LEGACY_GOST_BINARY=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_ETC=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_USR_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_CONFIG_DIR=$(mktemp -d)
|
||||||
|
cat > "$LEGACY_GOST_SERVICE_FILE_ETC" <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Unrelated Gost Service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/srv/custom-gost
|
||||||
|
ExecStart=/usr/local/bin/gost -C /srv/custom-gost/gost.yaml
|
||||||
|
EOF
|
||||||
|
|
||||||
|
local systemctl_calls=""
|
||||||
|
|
||||||
|
systemctl() {
|
||||||
|
systemctl_calls+=$'\n'"$*"
|
||||||
|
if [[ "$1" == "list-units" ]]; then
|
||||||
|
printf 'gost.service loaded active running\n'
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation >/dev/null
|
||||||
|
|
||||||
|
[[ -e "$LEGACY_GOST_BINARY" ]] || fail "cleanup_legacy_gost_installation should preserve unrelated gost binaries"
|
||||||
|
[[ -e "$LEGACY_GOST_SERVICE_FILE_ETC" ]] || fail "cleanup_legacy_gost_installation should preserve unrelated gost service files"
|
||||||
|
[[ "$systemctl_calls" != *"stop gost"* ]] || fail "cleanup_legacy_gost_installation should not stop unrelated gost services"
|
||||||
|
[[ "$systemctl_calls" != *"disable gost"* ]] || fail "cleanup_legacy_gost_installation should not disable unrelated gost services"
|
||||||
|
)
|
||||||
|
|
||||||
test_install_script_accepts_proxy_url_env_without_prompt() (
|
test_install_script_accepts_proxy_url_env_without_prompt() (
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
load_script_without_main "$ROOT_DIR/install.sh"
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
@@ -303,6 +501,11 @@ test_install_script_asks_for_proxy_config
|
|||||||
test_install_script_recomputes_download_url_after_prompt
|
test_install_script_recomputes_download_url_after_prompt
|
||||||
test_update_flux_agent_asks_for_proxy_config
|
test_update_flux_agent_asks_for_proxy_config
|
||||||
test_update_flux_agent_skips_proxy_prompt_when_not_installed
|
test_update_flux_agent_skips_proxy_prompt_when_not_installed
|
||||||
|
test_install_flux_agent_preserves_legacy_gost_when_download_fails
|
||||||
|
test_update_flux_agent_preserves_legacy_gost_when_download_fails
|
||||||
|
test_install_flux_agent_writes_json_safe_config
|
||||||
|
test_cleanup_legacy_gost_installation_removes_service_and_binary
|
||||||
|
test_cleanup_legacy_gost_installation_preserves_unrelated_gost
|
||||||
test_install_script_accepts_proxy_url_env_without_prompt
|
test_install_script_accepts_proxy_url_env_without_prompt
|
||||||
test_panel_install_script_can_disable_proxy
|
test_panel_install_script_can_disable_proxy
|
||||||
test_panel_install_script_recomputes_compose_urls_after_prompt
|
test_panel_install_script_recomputes_compose_urls_after_prompt
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 多阶段构建 - 构建阶段
|
# 多阶段构建 - 构建阶段
|
||||||
FROM node:20.19.0 AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -508,8 +508,7 @@ export interface SystemUpgradeVersionApiData {
|
|||||||
capability: SystemUpgradeCapabilityApiData;
|
capability: SystemUpgradeCapabilityApiData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemUpgradeCheckApiData
|
export interface SystemUpgradeCheckApiData extends SystemUpgradeVersionApiData {
|
||||||
extends SystemUpgradeVersionApiData {
|
|
||||||
releases: SystemUpgradeReleaseApiItem[];
|
releases: SystemUpgradeReleaseApiItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import type {
|
||||||
|
SystemUpgradeCheckApiData,
|
||||||
|
SystemUpgradeRunApiData,
|
||||||
|
SystemUpgradeReleaseApiItem,
|
||||||
|
SystemUpgradeVersionApiData,
|
||||||
|
} from "@/api/types";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
@@ -32,12 +39,6 @@ import {
|
|||||||
runSystemUpgrade,
|
runSystemUpgrade,
|
||||||
type AnnouncementData,
|
type AnnouncementData,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import type {
|
|
||||||
SystemUpgradeCheckApiData,
|
|
||||||
SystemUpgradeRunApiData,
|
|
||||||
SystemUpgradeReleaseApiItem,
|
|
||||||
SystemUpgradeVersionApiData,
|
|
||||||
} from "@/api/types";
|
|
||||||
import { BackIcon, SettingsIcon } from "@/components/icons";
|
import { BackIcon, SettingsIcon } from "@/components/icons";
|
||||||
import { ThemeSettings } from "@/components/theme-settings";
|
import { ThemeSettings } from "@/components/theme-settings";
|
||||||
import { isAdmin } from "@/utils/auth";
|
import { isAdmin } from "@/utils/auth";
|
||||||
@@ -328,6 +329,12 @@ export default function ConfigPage() {
|
|||||||
systemUpgradeReleasesMatchChannel &&
|
systemUpgradeReleasesMatchChannel &&
|
||||||
systemUpgradeReleases.length > 0,
|
systemUpgradeReleases.length > 0,
|
||||||
);
|
);
|
||||||
|
const canTriggerSystemUpgrade = Boolean(
|
||||||
|
!systemUpgradeLoading &&
|
||||||
|
!systemUpgradeChecking &&
|
||||||
|
!systemUpgradeExecuting &&
|
||||||
|
systemUpgradeInfo?.capability.capable !== false,
|
||||||
|
);
|
||||||
const canOpenSystemUpgradeModal = Boolean(
|
const canOpenSystemUpgradeModal = Boolean(
|
||||||
systemUpgradeInfo?.capability.capable &&
|
systemUpgradeInfo?.capability.capable &&
|
||||||
systemUpgradeHasConfirmedUpdate &&
|
systemUpgradeHasConfirmedUpdate &&
|
||||||
@@ -421,7 +428,9 @@ export default function ConfigPage() {
|
|||||||
hasUpdate:
|
hasUpdate:
|
||||||
response.data.channel === channel ? response.data.hasUpdate : false,
|
response.data.channel === channel ? response.data.hasUpdate : false,
|
||||||
latestVersion:
|
latestVersion:
|
||||||
response.data.channel === channel ? response.data.latestVersion : "",
|
response.data.channel === channel
|
||||||
|
? response.data.latestVersion
|
||||||
|
: "",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setSystemUpgradeInfo(null);
|
setSystemUpgradeInfo(null);
|
||||||
@@ -512,7 +521,9 @@ export default function ConfigPage() {
|
|||||||
: "未获取到可用版本",
|
: "未获取到可用版本",
|
||||||
);
|
);
|
||||||
|
|
||||||
return Boolean(data.capability.capable && data.hasUpdate && data.releases?.length);
|
return Boolean(
|
||||||
|
data.capability.capable && data.hasUpdate && data.releases?.length,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setSystemUpgradeReleases([]);
|
setSystemUpgradeReleases([]);
|
||||||
setSystemUpgradeCheckedChannel(null);
|
setSystemUpgradeCheckedChannel(null);
|
||||||
@@ -1568,7 +1579,8 @@ export default function ConfigPage() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-default-500">部署目录</p>
|
<p className="text-xs text-default-500">部署目录</p>
|
||||||
<p className="mt-1 break-all font-medium">
|
<p className="mt-1 break-all font-medium">
|
||||||
{systemUpgradeInfo?.capability.deployDir || "未获取到部署目录"}
|
{systemUpgradeInfo?.capability.deployDir ||
|
||||||
|
"未获取到部署目录"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -1625,7 +1637,9 @@ export default function ConfigPage() {
|
|||||||
size="md"
|
size="md"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onSelectionChange={(keys) => {
|
onSelectionChange={(keys) => {
|
||||||
const selected = Array.from(keys)[0] as string | undefined;
|
const selected = Array.from(keys)[0] as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
setSystemUpgradeSelectedVersion(selected || "");
|
setSystemUpgradeSelectedVersion(selected || "");
|
||||||
}}
|
}}
|
||||||
@@ -1648,17 +1662,15 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-3 pt-1 sm:flex-row sm:justify-end">
|
<div className="flex flex-col gap-3 pt-1 sm:flex-row sm:justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="flat"
|
|
||||||
isLoading={systemUpgradeChecking}
|
isLoading={systemUpgradeChecking}
|
||||||
|
variant="flat"
|
||||||
onPress={handleCheckSystemUpgrade}
|
onPress={handleCheckSystemUpgrade}
|
||||||
>
|
>
|
||||||
检查更新
|
检查更新
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
isDisabled={
|
isDisabled={!canTriggerSystemUpgrade}
|
||||||
!canOpenSystemUpgradeModal
|
|
||||||
}
|
|
||||||
isLoading={systemUpgradeExecuting}
|
isLoading={systemUpgradeExecuting}
|
||||||
onPress={handleOpenSystemUpgradeModal}
|
onPress={handleOpenSystemUpgradeModal}
|
||||||
>
|
>
|
||||||
@@ -1925,10 +1937,12 @@ export default function ConfigPage() {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="space-y-3 text-sm text-default-700 dark:text-default-300">
|
<div className="space-y-3 text-sm text-default-700 dark:text-default-300">
|
||||||
<p>
|
<p>
|
||||||
升级过程需要访问 Docker Socket,并会在短时间内中断当前面板服务。
|
升级过程需要访问 Docker
|
||||||
|
Socket,并会在短时间内中断当前面板服务。
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
请确认已经允许面板管理容器与宿主机 Docker 交互,并且可以接受升级期间的临时不可用。
|
请确认已经允许面板管理容器与宿主机 Docker
|
||||||
|
交互,并且可以接受升级期间的临时不可用。
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2 rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-warning-800 dark:border-warning-900/40 dark:bg-warning-950/30 dark:text-warning-200">
|
<div className="space-y-2 rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-warning-800 dark:border-warning-900/40 dark:bg-warning-950/30 dark:text-warning-200">
|
||||||
<p className="text-xs font-medium">升级前请确认</p>
|
<p className="text-xs font-medium">升级前请确认</p>
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|||||||
const toSafeString = (value: unknown): string => {
|
const toSafeString = (value: unknown): string => {
|
||||||
if (typeof value === "string") return value;
|
if (typeof value === "string") return value;
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,11 +266,13 @@ const normalizeBestExitState = (value: unknown): BestExitState | undefined => {
|
|||||||
|
|
||||||
const bestExitOwnerRoleText = (role?: string) => {
|
const bestExitOwnerRoleText = (role?: string) => {
|
||||||
if (role === "chain") return "中转";
|
if (role === "chain") return "中转";
|
||||||
|
|
||||||
return "入口";
|
return "入口";
|
||||||
};
|
};
|
||||||
|
|
||||||
const bestExitDetailTitle = (state?: BestExitState) => {
|
const bestExitDetailTitle = (state?: BestExitState) => {
|
||||||
if (!state?.items?.length) return undefined;
|
if (!state?.items?.length) return undefined;
|
||||||
|
|
||||||
return state.items
|
return state.items
|
||||||
.map(
|
.map(
|
||||||
(item) =>
|
(item) =>
|
||||||
@@ -3108,7 +3111,9 @@ export default function TunnelPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectItem key="1">单向计算(仅上传)</SelectItem>
|
<SelectItem key="1">单向计算(仅上传)</SelectItem>
|
||||||
<SelectItem key="2">双向计算(上传+下载)</SelectItem>
|
<SelectItem key="2">
|
||||||
|
双向计算(上传+下载)
|
||||||
|
</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -3171,7 +3176,9 @@ export default function TunnelPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">质量检测目标</div>
|
<div className="text-sm font-medium">
|
||||||
|
质量检测目标
|
||||||
|
</div>
|
||||||
<p className="text-xs text-default-500 mt-0.5">
|
<p className="text-xs text-default-500 mt-0.5">
|
||||||
用于实时隧道质量检测、诊断目标和 best
|
用于实时隧道质量检测、诊断目标和 best
|
||||||
最优出口评分,留空使用 www.bing.com:443
|
最优出口评分,留空使用 www.bing.com:443
|
||||||
|
|||||||
@@ -401,6 +401,7 @@ export function Select<T>({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
|
aria-label={label ? undefined : ariaLabel}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-md border border-input bg-background px-3 py-2 text-foreground shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:[color-scheme:dark]",
|
"w-full rounded-md border border-input bg-background px-3 py-2 text-foreground shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:[color-scheme:dark]",
|
||||||
sizeClass(size),
|
sizeClass(size),
|
||||||
@@ -409,7 +410,6 @@ export function Select<T>({
|
|||||||
)}
|
)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
id={generatedId}
|
id={generatedId}
|
||||||
aria-label={label ? undefined : ariaLabel}
|
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
value={singleValue}
|
value={singleValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|||||||
Reference in New Issue
Block a user