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:
238
pkg/git/git.go
Normal file
238
pkg/git/git.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"cs-bridge/pkg/logger"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProgressCallback 进度回调函数类型
|
||||
// 用于向外部报告git操作的进度
|
||||
type ProgressCallback func(message string, percent int)
|
||||
|
||||
// SSHConfig SSH连接配置
|
||||
type SSHConfig struct {
|
||||
Host string // SSH服务器地址
|
||||
Port int // SSH端口
|
||||
User string // SSH用户名
|
||||
KeyPath string // SSH私钥路径
|
||||
}
|
||||
|
||||
// CheckRepoExists 检查指定路径是否存在git仓库
|
||||
// path: 要检查的路径
|
||||
// 返回true表示存在.git目录,false表示不存在
|
||||
func CheckRepoExists(path string) bool {
|
||||
log := logger.GetLogger()
|
||||
gitDir := filepath.Join(path, ".git")
|
||||
info, err := os.Stat(gitDir)
|
||||
if err != nil {
|
||||
log.Debug(fmt.Sprintf("[Git] 本地仓库不存在: %s", path))
|
||||
return false
|
||||
}
|
||||
exists := info.IsDir()
|
||||
log.Info(fmt.Sprintf("[Git] 本地仓库检查 - Path: %s, Exists: %v", path, exists))
|
||||
return exists
|
||||
}
|
||||
|
||||
// CheckRepoExistsRemote 通过SSH检查远程服务器上是否存在git仓库
|
||||
func CheckRepoExistsRemote(sshCfg SSHConfig, path string) bool {
|
||||
log := logger.GetLogger()
|
||||
log.Info(fmt.Sprintf("[Git] SSH检查远程仓库 - Host: %s, Path: %s", sshCfg.Host, path))
|
||||
|
||||
cmd := buildSSHCommand(sshCfg, fmt.Sprintf("test -d '%s/.git' && echo 'exists' || echo 'not_exists'", path))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] SSH检查失败: %v, Output: %s", err, string(output)))
|
||||
return false
|
||||
}
|
||||
exists := strings.TrimSpace(string(output)) == "exists"
|
||||
log.Info(fmt.Sprintf("[Git] SSH远程仓库检查结果 - Exists: %v", exists))
|
||||
return exists
|
||||
}
|
||||
|
||||
// CloneRepo 克隆git仓库(本地执行)
|
||||
func CloneRepo(repoURL, destPath string, callback ProgressCallback) error {
|
||||
log := logger.GetLogger()
|
||||
log.Info(fmt.Sprintf("[Git] 开始本地克隆 - URL: %s, Dest: %s", repoURL, destPath))
|
||||
|
||||
// 确保父目录存在
|
||||
parentDir := filepath.Dir(destPath)
|
||||
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] 创建父目录失败: %v", err))
|
||||
return fmt.Errorf("failed to create parent directory: %w", err)
|
||||
}
|
||||
|
||||
// 如果目标目录存在但不是git仓库,删除它
|
||||
if _, err := os.Stat(destPath); err == nil {
|
||||
if !CheckRepoExists(destPath) {
|
||||
log.Info(fmt.Sprintf("[Git] 清理非git目录: %s", destPath))
|
||||
if err := os.RemoveAll(destPath); err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] 清理目录失败: %v", err))
|
||||
return fmt.Errorf("failed to clean destination directory: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if callback != nil {
|
||||
callback("正在克隆仓库...", 10)
|
||||
}
|
||||
|
||||
// 执行git clone命令
|
||||
cmd := exec.Command("git", "clone", repoURL, destPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] 克隆失败: %v, Output: %s", err, string(output)))
|
||||
return fmt.Errorf("git clone failed: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("[Git] 克隆成功: %s", destPath))
|
||||
if callback != nil {
|
||||
callback("仓库克隆完成", 100)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneRepoRemote 通过SSH在远程服务器上克隆git仓库
|
||||
func CloneRepoRemote(sshCfg SSHConfig, repoURL, destPath string, callback ProgressCallback) error {
|
||||
log := logger.GetLogger()
|
||||
log.Info(fmt.Sprintf("[Git] 开始SSH远程克隆 - Host: %s, URL: %s, Dest: %s", sshCfg.Host, repoURL, destPath))
|
||||
|
||||
if callback != nil {
|
||||
callback("正在通过SSH连接服务器...", 5)
|
||||
}
|
||||
|
||||
// 确保父目录存在
|
||||
parentDir := path.Dir(destPath)
|
||||
log.Debug(fmt.Sprintf("[Git] 创建远程父目录: %s", parentDir))
|
||||
mkdirCmd := buildSSHCommand(sshCfg, fmt.Sprintf("mkdir -p '%s'", parentDir))
|
||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] SSH创建目录失败: %v, Output: %s", err, string(output)))
|
||||
return fmt.Errorf("failed to create parent directory: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
// 如果目标目录存在但不是git仓库,删除它
|
||||
log.Debug(fmt.Sprintf("[Git] 检查并清理远程目录: %s", destPath))
|
||||
cleanCmd := buildSSHCommand(sshCfg, fmt.Sprintf(
|
||||
"if [ -d '%s' ] && [ ! -d '%s/.git' ]; then rm -rf '%s'; fi",
|
||||
destPath, destPath, destPath,
|
||||
))
|
||||
if output, err := cleanCmd.CombinedOutput(); err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] SSH清理目录失败: %v, Output: %s", err, string(output)))
|
||||
return fmt.Errorf("failed to clean destination: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
if callback != nil {
|
||||
callback("正在克隆仓库...", 20)
|
||||
}
|
||||
|
||||
// 执行git clone
|
||||
log.Info(fmt.Sprintf("[Git] 执行SSH git clone: %s -> %s", repoURL, destPath))
|
||||
cloneCmd := buildSSHCommand(sshCfg, fmt.Sprintf("git clone '%s' '%s'", repoURL, destPath))
|
||||
output, err := cloneCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] SSH克隆失败: %v, Output: %s", err, string(output)))
|
||||
return fmt.Errorf("git clone failed: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("[Git] SSH克隆成功: %s", destPath))
|
||||
if callback != nil {
|
||||
callback("仓库克隆完成", 100)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PullRepo 更新git仓库(本地执行)
|
||||
func PullRepo(repoPath string, callback ProgressCallback) error {
|
||||
log := logger.GetLogger()
|
||||
log.Info(fmt.Sprintf("[Git] 开始本地更新仓库: %s", repoPath))
|
||||
|
||||
if !CheckRepoExists(repoPath) {
|
||||
return fmt.Errorf("not a git repository: %s", repoPath)
|
||||
}
|
||||
|
||||
if callback != nil {
|
||||
callback("正在更新仓库...", 10)
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", "pull")
|
||||
cmd.Dir = repoPath
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] 更新失败: %v, Output: %s", err, string(output)))
|
||||
return fmt.Errorf("git pull failed: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("[Git] 更新成功: %s", strings.TrimSpace(string(output))))
|
||||
if callback != nil {
|
||||
callback(fmt.Sprintf("仓库更新完成: %s", strings.TrimSpace(string(output))), 100)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PullRepoRemote 通过SSH在远程服务器上更新git仓库
|
||||
func PullRepoRemote(sshCfg SSHConfig, repoPath string, callback ProgressCallback) error {
|
||||
log := logger.GetLogger()
|
||||
log.Info(fmt.Sprintf("[Git] 开始SSH远程更新仓库 - Host: %s, Path: %s", sshCfg.Host, repoPath))
|
||||
|
||||
if callback != nil {
|
||||
callback("正在通过SSH连接服务器...", 5)
|
||||
}
|
||||
|
||||
if callback != nil {
|
||||
callback("正在更新仓库...", 20)
|
||||
}
|
||||
|
||||
pullCmd := buildSSHCommand(sshCfg, fmt.Sprintf("cd '%s' && git pull", repoPath))
|
||||
output, err := pullCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("[Git] SSH更新失败: %v, Output: %s", err, string(output)))
|
||||
return fmt.Errorf("git pull failed: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("[Git] SSH更新成功: %s", strings.TrimSpace(string(output))))
|
||||
if callback != nil {
|
||||
callback(fmt.Sprintf("仓库更新完成: %s", strings.TrimSpace(string(output))), 100)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepoName 从仓库URL中提取仓库名称
|
||||
func GetRepoName(repoURL string) string {
|
||||
base := filepath.Base(repoURL)
|
||||
if len(base) > 4 && base[len(base)-4:] == ".git" {
|
||||
return base[:len(base)-4]
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// buildSSHCommand 构建SSH命令
|
||||
func buildSSHCommand(sshCfg SSHConfig, remoteCmd string) *exec.Cmd {
|
||||
log := logger.GetLogger()
|
||||
|
||||
// SSH密钥路径需要转换为Unix风格(SSH命令总是在Linux上执行)
|
||||
// 即使在Windows上编译,SSH密钥路径传给ssh命令时也必须用正斜杠
|
||||
keyPath := filepath.ToSlash(sshCfg.KeyPath)
|
||||
|
||||
args := []string{
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "BatchMode=yes",
|
||||
}
|
||||
|
||||
if sshCfg.Port != 0 && sshCfg.Port != 22 {
|
||||
args = append(args, "-p", fmt.Sprintf("%d", sshCfg.Port))
|
||||
}
|
||||
|
||||
if keyPath != "" { // Use the converted keyPath
|
||||
args = append(args, "-i", keyPath)
|
||||
}
|
||||
|
||||
args = append(args, fmt.Sprintf("%s@%s", sshCfg.User, sshCfg.Host), remoteCmd)
|
||||
|
||||
log.Debug(fmt.Sprintf("[Git] SSH命令: ssh %s", strings.Join(args, " ")))
|
||||
|
||||
return exec.Command("ssh", args...)
|
||||
}
|
||||
17
pkg/httpclient/resty.go
Normal file
17
pkg/httpclient/resty.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var Default *resty.Client
|
||||
|
||||
func Init() {
|
||||
c := resty.New()
|
||||
// TODO: 将代理配置移到config中
|
||||
// c.SetProxy("http://127.0.0.1:9000")
|
||||
c.SetTimeout(10 * time.Second)
|
||||
Default = c
|
||||
}
|
||||
27
pkg/logger/func.go
Normal file
27
pkg/logger/func.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package logger
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
func GetLogger() *zap.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
func Debug(msg string, fields ...zap.Field) {
|
||||
logger.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
func Info(msg string, fields ...zap.Field) {
|
||||
logger.Info(msg, fields...)
|
||||
}
|
||||
|
||||
func Warn(msg string, fields ...zap.Field) {
|
||||
logger.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
func Error(msg string, fields ...zap.Field) {
|
||||
logger.Error(msg, fields...)
|
||||
}
|
||||
|
||||
func Sync() {
|
||||
_ = logger.Sync()
|
||||
}
|
||||
87
pkg/logger/logger.go
Normal file
87
pkg/logger/logger.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"cs-bridge/internal/config"
|
||||
"cs-bridge/internal/consts"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
|
||||
func Init(log config.Log) {
|
||||
var zapLevel zapcore.Level
|
||||
|
||||
// 日志等级解析
|
||||
switch log.Level {
|
||||
case "debug":
|
||||
zapLevel = zap.DebugLevel
|
||||
case "info":
|
||||
zapLevel = zap.InfoLevel
|
||||
case "warning":
|
||||
zapLevel = zap.WarnLevel
|
||||
case "error":
|
||||
zapLevel = zap.ErrorLevel
|
||||
default:
|
||||
zapLevel = zap.InfoLevel
|
||||
}
|
||||
|
||||
// lumberjack 日志切割配置
|
||||
writeSyncer := zapcore.AddSync(&lumberjack.Logger{
|
||||
Filename: log.Filepath,
|
||||
MaxSize: log.MaxSizeMB,
|
||||
MaxBackups: log.Backups,
|
||||
MaxAge: log.MaxAgeDay,
|
||||
Compress: log.Compress,
|
||||
})
|
||||
|
||||
// 日志编码格式
|
||||
encoderConfigColor := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "Stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.CapitalColorLevelEncoder, // 彩色等级输出(终端)
|
||||
// EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||
EncodeTime: timeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "Stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
// EncodeLevel: zapcore.CapitalColorLevelEncoder, // 彩色等级输出(终端)
|
||||
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||
EncodeTime: timeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
encoderConsole := zapcore.NewConsoleEncoder(encoderConfigColor)
|
||||
encoderJson := zapcore.NewJSONEncoder(encoderConfig)
|
||||
|
||||
core := zapcore.NewTee(
|
||||
zapcore.NewCore(encoderJson, writeSyncer, zapLevel),
|
||||
zapcore.NewCore(encoderConsole, zapcore.AddSync(os.Stdout), zapLevel))
|
||||
|
||||
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||||
zap.ReplaceGlobals(logger)
|
||||
}
|
||||
|
||||
// 时间格式
|
||||
func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format(consts.TimeFormatDateTime))
|
||||
}
|
||||
393
pkg/lua/api.go
Normal file
393
pkg/lua/api.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"cs-bridge/pkg/httpclient"
|
||||
"cs-bridge/pkg/logger"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// RegisterHTTPModule registers the http module in Lua
|
||||
func RegisterHTTPModule(L *lua.LState) {
|
||||
httpMod := L.NewTable()
|
||||
|
||||
// http.get(url, headers)
|
||||
httpMod.RawSetString("get", L.NewFunction(luaHTTPGet))
|
||||
|
||||
// http.post(url, data, headers)
|
||||
httpMod.RawSetString("post", L.NewFunction(luaHTTPPost))
|
||||
|
||||
// http.put(url, data, headers)
|
||||
httpMod.RawSetString("put", L.NewFunction(luaHTTPPut))
|
||||
|
||||
// http.delete(url, headers)
|
||||
httpMod.RawSetString("delete", L.NewFunction(luaHTTPDelete))
|
||||
|
||||
L.SetGlobal("http", httpMod)
|
||||
}
|
||||
|
||||
// luaHTTPGet implements http.get(url, headers)
|
||||
func luaHTTPGet(L *lua.LState) int {
|
||||
url := L.CheckString(1)
|
||||
headers := L.OptTable(2, nil)
|
||||
|
||||
req := httpclient.Default.R()
|
||||
|
||||
// Set headers if provided
|
||||
if headers != nil {
|
||||
headers.ForEach(func(key, value lua.LValue) {
|
||||
req.SetHeader(key.String(), value.String())
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := req.Get(url)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
result := parseHTTPResponse(L, resp)
|
||||
L.Push(result)
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaHTTPPost implements http.post(url, data, headers)
|
||||
func luaHTTPPost(L *lua.LState) int {
|
||||
url := L.CheckString(1)
|
||||
data := L.CheckTable(2)
|
||||
headers := L.OptTable(3, nil)
|
||||
|
||||
req := httpclient.Default.R()
|
||||
|
||||
// Convert Lua table to map for request body
|
||||
bodyMap := luaTableToMap(data)
|
||||
req.SetBody(bodyMap)
|
||||
req.SetHeader("Content-Type", "application/json")
|
||||
|
||||
// Set additional headers if provided
|
||||
if headers != nil {
|
||||
headers.ForEach(func(key, value lua.LValue) {
|
||||
req.SetHeader(key.String(), value.String())
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := req.Post(url)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
result := parseHTTPResponse(L, resp)
|
||||
L.Push(result)
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaHTTPPut implements http.put(url, data, headers)
|
||||
func luaHTTPPut(L *lua.LState) int {
|
||||
url := L.CheckString(1)
|
||||
data := L.CheckTable(2)
|
||||
headers := L.OptTable(3, nil)
|
||||
|
||||
req := httpclient.Default.R()
|
||||
|
||||
bodyMap := luaTableToMap(data)
|
||||
req.SetBody(bodyMap)
|
||||
req.SetHeader("Content-Type", "application/json")
|
||||
|
||||
if headers != nil {
|
||||
headers.ForEach(func(key, value lua.LValue) {
|
||||
req.SetHeader(key.String(), value.String())
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := req.Put(url)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
result := parseHTTPResponse(L, resp)
|
||||
L.Push(result)
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaHTTPDelete implements http.delete(url, headers)
|
||||
func luaHTTPDelete(L *lua.LState) int {
|
||||
url := L.CheckString(1)
|
||||
headers := L.OptTable(2, nil)
|
||||
|
||||
req := httpclient.Default.R()
|
||||
|
||||
if headers != nil {
|
||||
headers.ForEach(func(key, value lua.LValue) {
|
||||
req.SetHeader(key.String(), value.String())
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := req.Delete(url)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
result := parseHTTPResponse(L, resp)
|
||||
L.Push(result)
|
||||
return 1
|
||||
}
|
||||
|
||||
// parseHTTPResponse parses HTTP response and returns Lua table
|
||||
func parseHTTPResponse(L *lua.LState, resp *resty.Response) lua.LValue {
|
||||
// Read response body
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.GetLogger().Error(fmt.Sprintf("读取响应体失败: %v", err))
|
||||
// Return error table
|
||||
result := L.NewTable()
|
||||
result.RawSetString("error", lua.LString(err.Error()))
|
||||
result.RawSetString("status", lua.LNumber(resp.StatusCode()))
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Log response for debugging
|
||||
logger.GetLogger().Debug(fmt.Sprintf("HTTP响应 [%d]: %s", resp.StatusCode(), string(body)))
|
||||
|
||||
// Try to parse as JSON first
|
||||
var jsonData any
|
||||
if err := json.Unmarshal(body, &jsonData); err == nil {
|
||||
logger.GetLogger().Debug(fmt.Sprintf("JSON解析成功,类型: %T, 值: %+v", jsonData, jsonData))
|
||||
result := GoToLua(L, jsonData)
|
||||
logger.GetLogger().Debug(fmt.Sprintf("转换为Lua后类型: %s", result.Type().String()))
|
||||
|
||||
// Try to access a test field if it's a table
|
||||
if tbl, ok := result.(*lua.LTable); ok {
|
||||
testVal := tbl.RawGetString("access_token")
|
||||
logger.GetLogger().Debug(fmt.Sprintf("测试访问access_token: %s (type: %s)", testVal.String(), testVal.Type().String()))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// If not JSON, return as string in a table
|
||||
logger.GetLogger().Debug(fmt.Sprintf("响应不是有效JSON: %v", err))
|
||||
result := L.NewTable()
|
||||
result.RawSetString("body", lua.LString(string(body)))
|
||||
result.RawSetString("status", lua.LNumber(resp.StatusCode()))
|
||||
return result
|
||||
}
|
||||
|
||||
// luaTableToMap converts a Lua table to a Go map
|
||||
func luaTableToMap(t *lua.LTable) map[string]any {
|
||||
result := make(map[string]any)
|
||||
t.ForEach(func(key, value lua.LValue) {
|
||||
result[key.String()] = luaValueToGo(value)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// luaValueToGo converts a Lua value to a Go value
|
||||
func luaValueToGo(lv lua.LValue) any {
|
||||
switch v := lv.(type) {
|
||||
case *lua.LNilType:
|
||||
return nil
|
||||
case lua.LBool:
|
||||
return bool(v)
|
||||
case lua.LNumber:
|
||||
return float64(v)
|
||||
case lua.LString:
|
||||
return string(v)
|
||||
case *lua.LTable:
|
||||
// Check if it's an array or map
|
||||
if v.Len() > 0 {
|
||||
// Array
|
||||
arr := make([]any, 0, v.Len())
|
||||
for i := 1; i <= v.Len(); i++ {
|
||||
arr = append(arr, luaValueToGo(v.RawGetInt(i)))
|
||||
}
|
||||
return arr
|
||||
}
|
||||
// Map
|
||||
m := make(map[string]any)
|
||||
v.ForEach(func(key, value lua.LValue) {
|
||||
m[key.String()] = luaValueToGo(value)
|
||||
})
|
||||
return m
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterJSONModule registers the json module in Lua
|
||||
func RegisterJSONModule(L *lua.LState) {
|
||||
jsonMod := L.NewTable()
|
||||
|
||||
// json.encode(table)
|
||||
jsonMod.RawSetString("encode", L.NewFunction(luaJSONEncode))
|
||||
|
||||
// json.decode(string)
|
||||
jsonMod.RawSetString("decode", L.NewFunction(luaJSONDecode))
|
||||
|
||||
L.SetGlobal("json", jsonMod)
|
||||
}
|
||||
|
||||
// luaJSONEncode implements json.encode(value)
|
||||
func luaJSONEncode(L *lua.LState) int {
|
||||
value := L.CheckAny(1)
|
||||
|
||||
goValue := luaValueToGo(value)
|
||||
jsonBytes, err := json.Marshal(goValue)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LString(string(jsonBytes)))
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaJSONDecode implements json.decode(jsonString)
|
||||
func luaJSONDecode(L *lua.LState) int {
|
||||
jsonStr := L.CheckString(1)
|
||||
|
||||
var data any
|
||||
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(GoToLua(L, data))
|
||||
return 1
|
||||
}
|
||||
|
||||
// RegisterLogModule registers the log module in Lua
|
||||
func RegisterLogModule(L *lua.LState) {
|
||||
logMod := L.NewTable()
|
||||
|
||||
logMod.RawSetString("debug", L.NewFunction(luaLogDebug))
|
||||
logMod.RawSetString("info", L.NewFunction(luaLogInfo))
|
||||
logMod.RawSetString("warn", L.NewFunction(luaLogWarn))
|
||||
logMod.RawSetString("error", L.NewFunction(luaLogError))
|
||||
|
||||
L.SetGlobal("log", logMod)
|
||||
}
|
||||
|
||||
// luaLogDebug implements log.debug(message, ...)
|
||||
func luaLogDebug(L *lua.LState) int {
|
||||
msg := formatLogMessage(L)
|
||||
logger.GetLogger().Debug(msg)
|
||||
return 0
|
||||
}
|
||||
|
||||
// luaLogInfo implements log.info(message, ...)
|
||||
func luaLogInfo(L *lua.LState) int {
|
||||
msg := formatLogMessage(L)
|
||||
logger.GetLogger().Info(msg)
|
||||
return 0
|
||||
}
|
||||
|
||||
// luaLogWarn implements log.warn(message, ...)
|
||||
func luaLogWarn(L *lua.LState) int {
|
||||
msg := formatLogMessage(L)
|
||||
logger.GetLogger().Warn(msg)
|
||||
return 0
|
||||
}
|
||||
|
||||
// luaLogError implements log.error(message, ...)
|
||||
func luaLogError(L *lua.LState) int {
|
||||
msg := formatLogMessage(L)
|
||||
logger.GetLogger().Error(msg)
|
||||
return 0
|
||||
}
|
||||
|
||||
// formatLogMessage formats log message from Lua arguments
|
||||
func formatLogMessage(L *lua.LState) string {
|
||||
n := L.GetTop()
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return L.CheckString(1)
|
||||
}
|
||||
|
||||
// Format string with arguments
|
||||
format := L.CheckString(1)
|
||||
args := make([]any, n-1)
|
||||
for i := 2; i <= n; i++ {
|
||||
args[i-2] = luaValueToGo(L.Get(i))
|
||||
}
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
// RegisterUtilModule registers utility functions in Lua
|
||||
func RegisterUtilModule(L *lua.LState) {
|
||||
utilMod := L.NewTable()
|
||||
|
||||
// base64.encode(string)
|
||||
utilMod.RawSetString("base64_encode", L.NewFunction(luaBase64Encode))
|
||||
|
||||
// base64.decode(string)
|
||||
utilMod.RawSetString("base64_decode", L.NewFunction(luaBase64Decode))
|
||||
|
||||
// url.encode(string)
|
||||
utilMod.RawSetString("url_encode", L.NewFunction(luaURLEncode))
|
||||
|
||||
// url.decode(string)
|
||||
utilMod.RawSetString("url_decode", L.NewFunction(luaURLDecode))
|
||||
|
||||
L.SetGlobal("util", utilMod)
|
||||
}
|
||||
|
||||
// luaBase64Encode implements base64_encode(str)
|
||||
func luaBase64Encode(L *lua.LState) int {
|
||||
str := L.CheckString(1)
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(str))
|
||||
L.Push(lua.LString(encoded))
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaBase64Decode implements base64_decode(str)
|
||||
func luaBase64Decode(L *lua.LState) int {
|
||||
str := L.CheckString(1)
|
||||
decoded, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LString(string(decoded)))
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaURLEncode implements url_encode(str)
|
||||
func luaURLEncode(L *lua.LState) int {
|
||||
str := L.CheckString(1)
|
||||
encoded := url.QueryEscape(str)
|
||||
L.Push(lua.LString(encoded))
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaURLDecode implements url_decode(str)
|
||||
func luaURLDecode(L *lua.LState) int {
|
||||
str := L.CheckString(1)
|
||||
decoded, err := url.QueryUnescape(str)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LString(decoded))
|
||||
return 1
|
||||
}
|
||||
307
pkg/lua/engine.go
Normal file
307
pkg/lua/engine.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
L *lua.LState
|
||||
}
|
||||
|
||||
func New() *Engine {
|
||||
L := lua.NewState()
|
||||
return &Engine{L: L}
|
||||
}
|
||||
|
||||
func (e *Engine) Close() {
|
||||
e.L.Close()
|
||||
}
|
||||
|
||||
// RegisterAPI registers all Lua API modules (http, json, log, etc.)
|
||||
func (e *Engine) RegisterAPI() {
|
||||
RegisterHTTPModule(e.L)
|
||||
RegisterJSONModule(e.L)
|
||||
RegisterLogModule(e.L)
|
||||
RegisterUtilModule(e.L)
|
||||
}
|
||||
|
||||
func (e *Engine) LoadFile(path string) error {
|
||||
return e.L.DoFile(path)
|
||||
}
|
||||
|
||||
func (e *Engine) CallString(fn string, args ...lua.LValue) (string, error) {
|
||||
L := e.L
|
||||
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: L.GetGlobal(fn),
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, args...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ret := L.Get(-1)
|
||||
L.Pop(1)
|
||||
return ret.String(), nil
|
||||
}
|
||||
|
||||
func (e *Engine) CallStruct(fn string, out any, args ...lua.LValue) error {
|
||||
L := e.L
|
||||
|
||||
f := L.GetGlobal(fn)
|
||||
if f.Type() != lua.LTFunction {
|
||||
return fmt.Errorf("lua function %s not found", fn)
|
||||
}
|
||||
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: f,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret := L.Get(-1)
|
||||
L.Pop(1)
|
||||
|
||||
table, ok := ret.(*lua.LTable)
|
||||
if !ok {
|
||||
return fmt.Errorf("lua function %s must return table", fn)
|
||||
}
|
||||
|
||||
return luaTableToStruct(table, out)
|
||||
}
|
||||
|
||||
func luaTableToStruct(t *lua.LTable, out any) error {
|
||||
v := reflect.ValueOf(out)
|
||||
if v.Kind() != reflect.Ptr {
|
||||
return errors.New("out must be pointer")
|
||||
}
|
||||
|
||||
v = v.Elem()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Type().Field(i)
|
||||
key := field.Tag.Get("lua")
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lv := t.RawGetString(key)
|
||||
if lv == lua.LNil {
|
||||
continue
|
||||
}
|
||||
|
||||
fv := v.Field(i)
|
||||
if err := setFieldFromLua(fv, lv); err != nil {
|
||||
return fmt.Errorf("field %s: %w", field.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setFieldFromLua sets a reflect.Value field from a lua.LValue
|
||||
func setFieldFromLua(fv reflect.Value, lv lua.LValue) error {
|
||||
if !fv.CanSet() {
|
||||
return errors.New("field cannot be set")
|
||||
}
|
||||
|
||||
switch fv.Kind() {
|
||||
case reflect.String:
|
||||
fv.SetString(lv.String())
|
||||
|
||||
case reflect.Bool:
|
||||
fv.SetBool(lua.LVAsBool(lv))
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if n, ok := lv.(lua.LNumber); ok {
|
||||
fv.SetInt(int64(n))
|
||||
}
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if n, ok := lv.(lua.LNumber); ok {
|
||||
fv.SetUint(uint64(n))
|
||||
}
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if n, ok := lv.(lua.LNumber); ok {
|
||||
fv.SetFloat(float64(n))
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
if ltable, ok := lv.(*lua.LTable); ok {
|
||||
return setSliceFromLuaTable(fv, ltable)
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
if ltable, ok := lv.(*lua.LTable); ok {
|
||||
return setMapFromLuaTable(fv, ltable)
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
if ltable, ok := lv.(*lua.LTable); ok {
|
||||
return luaTableToStruct(ltable, fv.Addr().Interface())
|
||||
}
|
||||
|
||||
case reflect.Ptr:
|
||||
if fv.IsNil() {
|
||||
fv.Set(reflect.New(fv.Type().Elem()))
|
||||
}
|
||||
return setFieldFromLua(fv.Elem(), lv)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSliceFromLuaTable converts a Lua table to a Go slice
|
||||
func setSliceFromLuaTable(fv reflect.Value, t *lua.LTable) error {
|
||||
length := t.Len()
|
||||
slice := reflect.MakeSlice(fv.Type(), length, length)
|
||||
|
||||
for i := 1; i <= length; i++ {
|
||||
lv := t.RawGetInt(i)
|
||||
if lv == lua.LNil {
|
||||
continue
|
||||
}
|
||||
|
||||
elem := slice.Index(i - 1)
|
||||
if err := setFieldFromLua(elem, lv); err != nil {
|
||||
return fmt.Errorf("index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
fv.Set(slice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setMapFromLuaTable converts a Lua table to a Go map
|
||||
func setMapFromLuaTable(fv reflect.Value, t *lua.LTable) error {
|
||||
mapType := fv.Type()
|
||||
newMap := reflect.MakeMap(mapType)
|
||||
|
||||
var convErr error
|
||||
t.ForEach(func(key, value lua.LValue) {
|
||||
if convErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert key
|
||||
k := reflect.New(mapType.Key()).Elem()
|
||||
if err := setFieldFromLua(k, key); err != nil {
|
||||
convErr = fmt.Errorf("key conversion: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert value
|
||||
v := reflect.New(mapType.Elem()).Elem()
|
||||
if err := setFieldFromLua(v, value); err != nil {
|
||||
convErr = fmt.Errorf("value conversion: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
newMap.SetMapIndex(k, v)
|
||||
})
|
||||
|
||||
if convErr != nil {
|
||||
return convErr
|
||||
}
|
||||
|
||||
fv.Set(newMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GoToLua converts a Go value to a Lua value
|
||||
func GoToLua(L *lua.LState, v any) lua.LValue {
|
||||
if v == nil {
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(v)
|
||||
return goValueToLua(L, val)
|
||||
}
|
||||
|
||||
// goValueToLua converts a reflect.Value to lua.LValue
|
||||
func goValueToLua(L *lua.LState, v reflect.Value) lua.LValue {
|
||||
if !v.IsValid() {
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
// Dereference pointers and interfaces
|
||||
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
|
||||
if v.IsNil() {
|
||||
return lua.LNil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return lua.LBool(v.Bool())
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return lua.LNumber(v.Int())
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return lua.LNumber(v.Uint())
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return lua.LNumber(v.Float())
|
||||
|
||||
case reflect.String:
|
||||
return lua.LString(v.String())
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
table := L.NewTable()
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
table.Append(goValueToLua(L, v.Index(i)))
|
||||
}
|
||||
return table
|
||||
|
||||
case reflect.Map:
|
||||
table := L.NewTable()
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
val := goValueToLua(L, iter.Value())
|
||||
|
||||
// For string keys, use RawSetString for better Lua compatibility
|
||||
if key.Kind() == reflect.String {
|
||||
table.RawSetString(key.String(), val)
|
||||
} else {
|
||||
// For non-string keys, convert and use RawSet
|
||||
luaKey := goValueToLua(L, key)
|
||||
table.RawSet(luaKey, val)
|
||||
}
|
||||
}
|
||||
return table
|
||||
|
||||
case reflect.Struct:
|
||||
table := L.NewTable()
|
||||
t := v.Type()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
|
||||
// Skip unexported fields
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get lua tag or use field name
|
||||
luaKey := field.Tag.Get("lua")
|
||||
if luaKey == "" {
|
||||
luaKey = field.Name
|
||||
}
|
||||
|
||||
fieldValue := goValueToLua(L, v.Field(i))
|
||||
table.RawSetString(luaKey, fieldValue)
|
||||
}
|
||||
return table
|
||||
|
||||
default:
|
||||
return lua.LNil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user