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

393
pkg/lua/api.go Normal file
View 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
View 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
}
}