Files
sproutclaw-web/backend/internal/services/web_terminal.go
2026-06-14 20:31:10 +08:00

141 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"}
}