first commit

This commit is contained in:
2026-06-14 20:31:10 +08:00
parent c33b143176
commit 1ed3f576fa
51 changed files with 3362 additions and 810 deletions

View File

@@ -0,0 +1,140 @@
package services
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sync"
"github.com/Kodecable/crosspty"
"github.com/gorilla/websocket"
)
type terminalWSMessage struct {
Type string `json:"type"`
Data string `json:"data,omitempty"`
Cols uint16 `json:"cols,omitempty"`
Rows uint16 `json:"rows,omitempty"`
}
// ServeWebTerminal attaches a PTY shell session to the websocket connection.
func ServeWebTerminal(conn *websocket.Conn, workDir string) error {
abs, err := filepath.Abs(workDir)
if err != nil {
return fmt.Errorf("解析目录失败: %w", err)
}
info, err := os.Stat(abs)
if err != nil {
return fmt.Errorf("目录不存在: %s", abs)
}
if !info.IsDir() {
return fmt.Errorf("不是有效目录: %s", abs)
}
ptmx, err := crosspty.Start(crosspty.CommandConfig{
Argv: defaultShellArgv(),
Dir: abs,
Env: os.Environ(),
EnvInject: map[string]string{
"TERM": "xterm-256color",
"COLORTERM": "truecolor",
},
Size: crosspty.TermSize{Rows: 24, Cols: 80},
})
if err != nil {
if err == crosspty.ErrConPTYNotSupported {
return fmt.Errorf("当前 Windows 版本不支持 ConPTY需 Windows 10 1809 或更高版本")
}
return fmt.Errorf("启动终端失败: %w", err)
}
defer ptmx.Close()
var writeMu sync.Mutex
writeJSON := func(v any) error {
writeMu.Lock()
defer writeMu.Unlock()
return conn.WriteJSON(v)
}
go func() {
buf := make([]byte, 4096)
for {
n, readErr := ptmx.Read(buf)
if n > 0 {
writeMu.Lock()
werr := conn.WriteMessage(websocket.BinaryMessage, buf[:n])
writeMu.Unlock()
if werr != nil {
return
}
}
if readErr != nil {
return
}
}
}()
for {
msgType, payload, readErr := conn.ReadMessage()
if readErr != nil {
if websocket.IsCloseError(readErr, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
return nil
}
if readErr == io.EOF {
return nil
}
return readErr
}
switch msgType {
case websocket.BinaryMessage:
if _, werr := ptmx.Write(payload); werr != nil {
return werr
}
case websocket.TextMessage:
var msg terminalWSMessage
if err := json.Unmarshal(payload, &msg); err != nil {
if _, werr := ptmx.Write(payload); werr != nil {
return werr
}
continue
}
switch msg.Type {
case "input":
if _, werr := ptmx.Write([]byte(msg.Data)); werr != nil {
return werr
}
case "resize":
if msg.Cols > 0 && msg.Rows > 0 {
_ = ptmx.Resize(crosspty.TermSize{Rows: msg.Rows, Cols: msg.Cols})
}
case "ping":
if err := writeJSON(map[string]string{"type": "pong"}); err != nil {
return err
}
default:
if msg.Data != "" {
if _, werr := ptmx.Write([]byte(msg.Data)); werr != nil {
return werr
}
}
}
}
}
}
func defaultShellArgv() []string {
if runtime.GOOS == "windows" {
if comspec := os.Getenv("COMSPEC"); comspec != "" {
return []string{comspec}
}
return []string{"cmd.exe"}
}
if shell := os.Getenv("SHELL"); shell != "" {
return []string{shell, "-l"}
}
return []string{"/bin/bash", "-l"}
}