feat: 初始提交 - Code Server Bridge完整实现

- OAuth认证系统(Gitea + Lua扩展)
- Git自动化操作(本地/SSH远程)
- 实时进度WebSocket推送
- 现代化Tab界面UI
- Cobra CLI命令行(init/version/serve)
- 完整构建系统(Makefile + Taskfile)
- UPX压缩支持(体积减少70%)
This commit is contained in:
2026-01-08 23:32:29 +08:00
commit 8265df0dcd
40 changed files with 3786 additions and 0 deletions

129
internal/config/config.go Normal file
View File

@@ -0,0 +1,129 @@
package config
import (
"cs-bridge/internal/consts"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sync"
"github.com/go-yaml/yaml"
"github.com/spf13/viper"
)
var (
globalViper *viper.Viper
globalViperOnce sync.Once
configMu sync.RWMutex
cachedConfig *Config
)
// NewConfig 初始化配置
func NewConfig() *Config {
globalViperOnce.Do(func() {
globalViper = viper.New()
})
configPath := loadConfigFile()
if configPath == "" {
log.Println("⚠️ 未找到配置文件,使用默认配置并导出为 config.yaml")
cachedConfig = DefaultConfig()
exportConfig(cachedConfig, "yaml")
return cachedConfig
}
conf, err := parseConfig()
if err != nil {
log.Printf("⚠️ 解析配置失败,使用默认配置并导出为 config.yaml: %v", err)
conf = DefaultConfig()
exportConfig(conf, "yaml")
}
cachedConfig = conf
return conf
}
// loadConfigFile 加载配置文件
func loadConfigFile() string {
files := []string{
fmt.Sprintf("%s.yaml", consts.ConfigName),
fmt.Sprintf("%s.yml", consts.ConfigName),
}
for _, file := range files {
if _, err := os.Stat(file); err == nil {
ext := filepath.Ext(file)[1:]
globalViper.SetConfigFile(file)
globalViper.SetConfigType(ext)
globalViper.AddConfigPath(".")
if err := globalViper.ReadInConfig(); err != nil {
log.Printf("⚠️ 读取配置文件 %s 失败: %v", file, err)
continue
}
log.Printf("✅ 成功加载配置文件: %s", file)
return file
}
}
return ""
}
// parseConfig 将配置解析为结构体
func parseConfig() (*Config, error) {
configMu.Lock()
defer configMu.Unlock()
var conf Config
if err := globalViper.Unmarshal(&conf); err != nil {
return nil, err
}
return &conf, nil
}
// exportConfig 导出配置文件(默认保存为 config.yaml
func exportConfig(conf *Config, format string) {
var (
data []byte
err error
)
switch format {
case "json":
data, err = json.MarshalIndent(conf, "", " ")
case "yaml", "yml", "":
format = "yaml"
data, err = yaml.Marshal(conf)
default:
log.Printf("⚠️ 不支持的导出格式: %s", format)
return
}
if err != nil {
log.Printf("⚠️ 导出 %s 格式配置失败: %v", format, err)
return
}
filename := fmt.Sprintf("%s.%s", consts.ConfigName, format)
if err := os.WriteFile(filename, data, 0644); err != nil {
log.Printf("⚠️ 写入配置文件 %s 失败: %v", filename, err)
} else {
log.Printf("✅ 已导出配置文件: %s", filename)
}
log.Printf("📋 当前 %s 配置:\n%s", format, data)
}
// GetConfig 获取当前配置(线程安全)
func GetConfig() *Config {
configMu.RLock()
defer configMu.RUnlock()
if cachedConfig == nil {
return DefaultConfig()
}
conf := *cachedConfig
return &conf
}

116
internal/config/type.go Normal file
View File

@@ -0,0 +1,116 @@
package config
import (
"cs-bridge/internal/consts"
"path/filepath"
"time"
)
type Server struct {
Debug bool `mapstructure:"debug"`
Port int `mapstructure:"port"`
}
type Log struct {
Level string `mapstructure:"level" yaml:"level"`
Filepath string `mapstructure:"filepath" yaml:"filepath"`
MaxSizeMB int `mapstructure:"max_size_mb" yaml:"max_size_mb"` // 单个日志文件最大(MB)
MaxAgeDay int `mapstructure:"max_age_day" yaml:"max_age_day"` // 日志文件最大保存天数
Backups int `mapstructure:"backups" yaml:"backups"` // 保留的旧文件个数
Compress bool `mapstructure:"compress" yaml:"compress"` // 是否压缩
}
type Database struct {
SqliteDbPath string `mapstructure:"sqlite_db_path" yaml:"sqlite_db_path"`
}
type Secret struct {
TokenTTL time.Duration `mapstructure:"token_ttl" yaml:"token_ttl"`
WSTTL time.Duration `mapstructure:"ws_ttl" yaml:"ws_ttl"`
}
type Provider struct {
Name string `mapstructure:"name" yaml:"name"`
Type string `mapstructure:"type" yaml:"type"`
ClientID string `mapstructure:"client_id" yaml:"client_id"`
ClientSecret string `mapstructure:"client_secret" yaml:"client_secret"`
BaseURL string `mapstructure:"base_url" yaml:"base_url"`
AuthorizeURL string `mapstructure:"authorize_url" yaml:"authorize_url"`
TokenURL string `mapstructure:"token_url" yaml:"token_url"`
UserURL string `mapstructure:"user_url" yaml:"user_url"`
}
type OAuth struct {
BaseURL string `mapstructure:"base_url" yaml:"base_url"`
Providers []Provider `mapstructure:"providers" yaml:"providers"`
}
type CodeServer struct {
BaseURL string `mapstructure:"base_url" yaml:"base_url"`
WorkspaceRoot string `mapstructure:"workspace_root" yaml:"workspace_root"` // 容器内路径,传递给code-server
// SSH配置 - 用于远程执行git操作
SSHHost string `mapstructure:"ssh_host" yaml:"ssh_host"` // SSH服务器地址
SSHPort int `mapstructure:"ssh_port" yaml:"ssh_port"` // SSH端口,默认22
SSHUser string `mapstructure:"ssh_user" yaml:"ssh_user"` // SSH用户名(专用账号)
SSHKeyPath string `mapstructure:"ssh_key_path" yaml:"ssh_key_path"` // SSH私钥路径
SSHWorkspaceRoot string `mapstructure:"ssh_workspace_root" yaml:"ssh_workspace_root"` // SSH服务器上的workspace路径(容器映射到宿主机的路径)
}
type Config struct {
Server Server `mapstructure:"server" yaml:"server"`
Log Log `mapstructure:"log" yaml:"log"`
Database Database `mapstructure:"database" yaml:"database"`
Secret Secret `mapstructure:"secret" yaml:"secret"`
OAuth OAuth `mapstructure:"oauth" yaml:"oauth"`
CodeServer CodeServer `mapstructure:"code_server" yaml:"code_server"`
}
func DefaultConfig() *Config {
logPath := filepath.Join(consts.DataDir, "log.log")
dbPath := filepath.Join(consts.DataDir, "sqlite.db")
return &Config{
Server: Server{
Debug: true,
Port: 8080,
},
Log: Log{
Level: "debug",
Filepath: logPath,
MaxSizeMB: 10,
MaxAgeDay: 7,
Backups: 3,
Compress: true,
},
Database: Database{
SqliteDbPath: dbPath,
},
Secret: Secret{
TokenTTL: time.Millisecond * 600,
WSTTL: time.Millisecond * 3600,
},
OAuth: OAuth{
BaseURL: "http://localhost:8080",
Providers: []Provider{
{
Name: "gitea",
Type: "gitea",
ClientID: "xxx",
ClientSecret: "xxx",
BaseURL: "https://xxx",
AuthorizeURL: "/login/oauth/authorize",
TokenURL: "/login/oauth/access_token",
UserURL: "/api/v1/user",
},
},
},
CodeServer: CodeServer{
BaseURL: "xxx",
WorkspaceRoot: "/config/workspace", // 容器内路径
SSHHost: "",
SSHPort: 22,
SSHUser: "git",
SSHKeyPath: "",
SSHWorkspaceRoot: "/root/code-server/data/workspace", // SSH服务器实际路径
},
}
}