commit e40ef6b559410e319817788a22f208077efb9137 Author: Hua Date: Fri Aug 9 21:46:59 2024 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ccc9e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Reference https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# OS General +Thumbs.db +.DS_Store + +# project +*.cert +*.key +*.log +bin/ +config.json + +# Develop tools +.vscode/ +.idea/ +*.swp \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bac9c4b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# 构建阶段 +FROM golang:alpine AS builder + +WORKDIR /build + +# 复制整个项目 +COPY . . + +# 设置 Go 环境 +ENV GO111MODULE=on GOPROXY=https://goproxy.cn,direct + +# 构建配置管理服务器 +WORKDIR /build/override-web/config +RUN go mod download +RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o config-server . + +# 构建原始 override 应用 +WORKDIR /build/override-web/override +RUN go mod download +RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o override . + +# 最终镜像 +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +# 从构建阶段复制编译好的二进制文件 +COPY --from=builder /build/override-web/config/config-server /usr/local/bin/ +COPY --from=builder /build/override-web/override/override /usr/local/bin/ + +# 复制配置文件和静态文件 +COPY override-web/config/web /app/web +COPY config.json.example /app/config.json + +WORKDIR /app +VOLUME /app + +# 暴露原始应用和配置管理服务器的端口 +EXPOSE 8181 9090 + +# 使用 shell 形式的 CMD,这样可以使用环境变量并同时运行两个应用 +CMD ["/bin/sh", "-c", "/usr/local/bin/config-server -app /usr/local/bin/override -config /app/config.json & /usr/local/bin/override"] \ No newline at end of file diff --git a/config/config-manager b/config/config-manager new file mode 100755 index 0000000..92da6a6 Binary files /dev/null and b/config/config-manager differ diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..0089516 --- /dev/null +++ b/config/config.go @@ -0,0 +1,18 @@ +package main + +import ( + "encoding/json" + "io/ioutil" +) + +func LoadConfig(path string) ([]byte, error) { + return ioutil.ReadFile(path) +} + +func SaveConfig(path string, config map[string]interface{}) error { + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(path, data, 0644) +} diff --git a/config/go.mod b/config/go.mod new file mode 100644 index 0000000..a3b1c99 --- /dev/null +++ b/config/go.mod @@ -0,0 +1,3 @@ +module config + +go 1.22 diff --git a/config/main.go b/config/main.go new file mode 100644 index 0000000..24860ea --- /dev/null +++ b/config/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "os/exec" + "time" +) + +var ( + configPath string + serverAddr string + originalApp string +) + +func main() { + flag.StringVar(&configPath, "config", "config.json", "path to config file") + flag.StringVar(&serverAddr, "addr", ":9090", "address for config server") + flag.StringVar(&originalApp, "app", "./override", "path to the original app") + flag.Parse() + + http.HandleFunc("/api/config", handleConfig) + http.HandleFunc("/api/restart", handleRestart) + http.HandleFunc("/api/start", start) + http.HandleFunc("/api/stop", stop) + http.HandleFunc("/_ping", handlePing) + http.Handle("/", http.FileServer(http.Dir("web"))) + + log.Printf("Config server starting on %s", serverAddr) + log.Fatal(http.ListenAndServe(serverAddr, nil)) +} + +func handleConfig(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + config, err := LoadConfig(configPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(config) + if err != nil { + return + } + } else if r.Method == http.MethodPost { + var newConfig map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&newConfig) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + err = SaveConfig(configPath, newConfig) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + } else { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func handleRestart(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + cmd := exec.Command(originalApp) + err := cmd.Start() + if err != nil { + http.Error(w, "Failed to restart the application", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte("Application restarted")) + if err != nil { + return + } +} + +func start(w http.ResponseWriter, r *http.Request) { + cmd := exec.Command("override") + err := cmd.Start() + if err != nil { + http.Error(w, "Failed to start override: "+err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +func stop(w http.ResponseWriter, r *http.Request) { + cmd := exec.Command("pkill", "override") + err := cmd.Run() + if err != nil { + http.Error(w, "Failed to stop override: "+err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +func handlePing(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + bindAddress := r.URL.Query().Get("bind") + protocol := r.URL.Query().Get("protocol") + + if bindAddress == "" { + http.Error(w, "bind address is required", http.StatusBadRequest) + return + } + + if protocol == "" { + protocol = "http" // 默认使用http + } + + url := fmt.Sprintf("%s://%s/_ping", protocol, bindAddress) + + client := &http.Client{ + Timeout: 5 * time.Second, + } + + resp, err := client.Get(url) + if err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + err := json.NewEncoder(w).Encode(map[string]string{ + "msg": fmt.Sprintf("无法连接到服务器: %v", err), + }) + if err != nil { + return + } + return + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + + } + }(resp.Body) + + if resp.StatusCode == http.StatusOK { + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(map[string]interface{}{ + "msg": "服务器已启动并正常运行", + "data": map[string]interface{}{ + "now": time.Now().Unix(), + "ns1": "200 OK", + }, + }) + if err != nil { + return + } + } else { + w.WriteHeader(http.StatusBadGateway) + err := json.NewEncoder(w).Encode(map[string]string{ + "msg": fmt.Sprintf("服务器响应异常: %s", resp.Status), + }) + if err != nil { + return + } + } +} diff --git a/config/web/app.js b/config/web/app.js new file mode 100644 index 0000000..ce86227 --- /dev/null +++ b/config/web/app.js @@ -0,0 +1,175 @@ +const MessageType = Object.freeze({ + SUCCESS: 'success', + ERROR: 'error', + WARNING: 'warning', + INFO: 'info' +}); +new Vue({ + el: '#app', + data: { + config: {}, + message: '', + textContent: '启动', + bgColor: '#3B82F6', + serverRunning: false, + showMessage: false, + messageType: MessageType.SUCCESS, + messageTimer: null, + selectedProtocol: 'http' + }, + mounted() { + this.fetchConfig(); + }, + methods: { + msg(message, type = MessageType.SUCCESS) { + this.message = message; + this.messageType = type; + + // 如果已经有一个定时器在运行,先清除它 + if (this.messageTimer) { + clearTimeout(this.messageTimer); + } + + // 显示消息 + this.showMessage = true; + + // 设置淡出定时器 + this.messageTimer = setTimeout(() => { + this.showMessage = false; + }, 2500); // 2.5秒后开始淡出 + }, + fetchConfig() { + axios.get('/api/config') + .then(response => { + this.config = this.processConfig(response.data); + }) + .catch(error => { + this.msg('获取配置失败: ' + error, MessageType.ERROR); + }); + }, + processConfig(config) { + const processed = {}; + for (const [key, value] of Object.entries(config)) { + if (typeof value === 'object' && value !== null) { + processed[key] = JSON.stringify(value, null, 2); + } else { + processed[key] = value; + } + } + return processed; + }, + updateConfig() { + const updatedConfig = this.prepareConfigForUpdate(this.config); + axios.post('/api/config', updatedConfig) + .then(() => { + this.msg('配置更新成功,服务器正在重启 '); + this.restartApp(); + }) + .catch(error => { + this.msg('获取配置失败: ' + error, MessageType.ERROR); + }); + }, + prepareConfigForUpdate(config) { + const prepared = {}; + for (const [key, value] of Object.entries(config)) { + if (typeof value === 'string' && value.trim().startsWith('{')) { + try { + prepared[key] = JSON.parse(value); + } catch (e) { + console.error(`解析 JSON 失败,键: ${key}:`, e); + prepared[key] = value; // 保持原始字符串 + } + } else { + prepared[key] = value; + } + } + return prepared; + }, + restartApp() { + axios.post('/api/restart') + .then(() => { + this.msg('应用正在重启'); + }) + .catch(error => { + this.msg('获取配置失败: ' + error, MessageType.ERROR); + }); + }, + + toggleServer() { + this.serverRunning = !this.serverRunning; + let message = ''; + let textContent = ''; + let bgColor = ''; + let url = ''; + let errorMsg = ''; + if (this.serverRunning) { + message = '应用正在停止'; + textContent = '停止'; + bgColor = '#EF4444'; + url = '/api/stop'; + errorMsg = '停止应用失败: '; + } else { + message = '应用正在启动'; + textContent = '启动'; + bgColor = '#3B82F6'; + url = '/api/start'; + errorMsg = '启动应用失败: '; + } + axios.post(url) + .then(() => { + this.textContent = textContent; + this.bgColor = bgColor; + this.msg(message); + }) + .catch(error => { + this.msg(errorMsg + error, MessageType.ERROR); + }); + }, + + toggleServerStyle() { + return { + backgroundColor: this.bgColor + } + }, + + copyVscodeConfig() { + let config = { + "github.copilot.advanced": { + "debug.overrideCAPIUrl": `http://${this.config.bind}/v1`, + "debug.overrideProxyUrl": `http://${this.config.bind}`, + "debug.chatOverrideProxyUrl": `http://${this.config.bind}/v1/chat/completions`, + "authProvider": "github-enterprise" + }, + "github-enterprise.uri": "https://cocopilot.org", + }; + + let configStr = JSON.stringify(config, null, 2); + configStr = configStr.slice(1, -1); + + navigator.clipboard.writeText(`${configStr.trimEnd()},\n`) + .then(r => { + this.msg('vscode 配置已复制到剪贴板'); + }); + }, + + testConnection() { + const params = new URLSearchParams({ + bind: this.config.bind, + protocol: this.selectedProtocol + }); + + axios.get(`/_ping?${params.toString()}`, { timeout: 5000 }) + .then(response => { + this.textContent = '停止'; + this.bgColor = '#EF4444'; + this.msg(response.data.msg); + }) + .catch(error => { + this.textContent = '启动'; + this.bgColor = '#3B82F6'; + const errorMsg = error.response ? error.response.data.msg : error.message; + this.msg('连接测试失败: ' + errorMsg, MessageType.ERROR); + }); + } + } +}); \ No newline at end of file diff --git a/config/web/index.html b/config/web/index.html new file mode 100644 index 0000000..fdea7c2 --- /dev/null +++ b/config/web/index.html @@ -0,0 +1,85 @@ + + + + + + Override-Config-Gui + + + + + +
+

Override-Config-Gui

+
+

服务管理

+ + + +
+ +
+
+
+

配置管理

+ +
+ +
+
+
+ {{ message }} +
+
+ + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..336c1c0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + override-app: + image: huagcs/override-config-ui:latest + container_name: override-config-ui + restart: always + build: + context: . + dockerfile: Dockerfile + volumes: + - ./config.json:/app/config.json + ports: + - "8181:8181" + - "9090:9090"