fix(terminal): remove command completion protocol (#12055)

This commit is contained in:
ssongliu
2026-03-04 14:25:05 +08:00
committed by GitHub
parent c21910c203
commit 65dbb53a07
7 changed files with 0 additions and 557 deletions

View File

@@ -1,155 +0,0 @@
package terminal
import (
"bufio"
"fmt"
"os"
"os/exec"
"sort"
"strings"
)
type TabCompleter struct {
shell string
}
func NewTabCompleter() *TabCompleter {
return &TabCompleter{
shell: os.Getenv("SHELL"),
}
}
func (tc *TabCompleter) GetCompletions(input string) []string {
input = strings.TrimLeft(input, " \t")
trailingSpace := strings.HasSuffix(input, " ") || strings.HasSuffix(input, "\t")
if tc.shell == "/bin/bash" || tc.shell == "/usr/bin/bash" {
return tc.bashComplete(input, trailingSpace)
}
if tc.shell == "/bin/zsh" || tc.shell == "/usr/bin/zsh" {
return tc.zshComplete(input)
}
return tc.bashComplete(input, trailingSpace)
}
func (tc *TabCompleter) bashComplete(input string, trailingSpace bool) []string {
var completions []string
parts := strings.Fields(input)
if trailingSpace {
parts = append(parts, "")
}
if len(parts) <= 1 {
cmdStr := fmt.Sprintf("compgen -c -- '%s'", input)
cmd := exec.Command("bash", "-c", cmdStr)
out, err := cmd.Output()
if err == nil {
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" {
completions = append(completions, line)
}
}
}
} else {
mainCmd := parts[0]
curWord := parts[len(parts)-1]
if trailingSpace {
curWord = ""
}
cmdStr := fmt.Sprintf(`
[ -f /etc/bash_completion ] && source /etc/bash_completion
[ -f /usr/share/bash-completion/bash_completion ] &&
source /usr/share/bash-completion/bash_completion
_completion_loader %s 2>/dev/null || true
[ -f /usr/share/bash-completion/completions/%s ] &&
source /usr/share/bash-completion/completions/%s
COMP_WORDS=(%s)
COMP_CWORD=%d
COMP_LINE='%s'
COMP_POINT=%d
_%s 2>/dev/null || complete -p %s &>/dev/null || compgen -f -- '%s'
printf '%%s\n' "${COMPREPLY[@]}"
`, mainCmd, mainCmd, mainCmd,
strings.Join(parts, " "), len(parts)-1,
input, len(input),
mainCmd, mainCmd, curWord)
cmd := exec.Command("bash", "-c", cmdStr)
out, err := cmd.Output()
if err == nil {
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" {
completions = append(completions, line)
}
}
}
}
return tc.filterCompletions(input, completions)
}
func (tc *TabCompleter) filterCompletions(input string, completions []string) []string {
var filtered []string
seen := make(map[string]bool)
inputLower := strings.ToLower(input)
lastToken := inputLower
if idx := strings.LastIndex(inputLower, " "); idx != -1 {
lastToken = strings.TrimSpace(inputLower[idx+1:])
}
if lastToken == "" {
lastToken = inputLower
}
for _, comp := range completions {
if seen[comp] {
continue
}
seen[comp] = true
if strings.HasPrefix(strings.ToLower(comp), lastToken) {
filtered = append(filtered, comp)
}
}
sort.SliceStable(filtered, func(i, j int) bool {
if len(filtered[i]) != len(filtered[j]) {
return len(filtered[i]) < len(filtered[j])
}
return filtered[i] < filtered[j]
})
if len(filtered) > 20 {
filtered = filtered[:20]
}
return filtered
}
func (tc *TabCompleter) zshComplete(input string) []string {
var completions []string
cmdStr := fmt.Sprintf(`
autoload -Uz compinit
compinit
compset -P '%s'
compadd -x '%s' 2>/dev/null
`, input, input)
cmd := exec.Command("zsh", "-c", cmdStr)
out, _ := cmd.Output()
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" {
completions = append(completions, line)
}
}
return completions
}

View File

@@ -3,7 +3,6 @@ package terminal
import (
"encoding/base64"
"encoding/json"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -17,7 +16,6 @@ type LocalWsSession struct {
allowCtrlC bool
writeMutex sync.Mutex
completer *TabCompleter
}
func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand, allowCtrlC bool) (*LocalWsSession, error) {
@@ -30,7 +28,6 @@ func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalComma
wsConn: wsConn,
allowCtrlC: allowCtrlC,
completer: NewTabCompleter(),
}, nil
}
@@ -117,14 +114,6 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err)
}
case WsMsgComplete:
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data)
if err != nil {
global.LOG.Errorf("websock complete string base64 decoding failed, err: %v", err)
break
}
suggestion := sws.completer.GetCompletions(string(decodeBytes))
sws.sendComplete(wsConn, strings.Join(suggestion, "\n"))
}
}
}
@@ -135,17 +124,3 @@ func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdByt
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
}
}
func (sws *LocalWsSession) sendComplete(wsConn *websocket.Conn, suggestion string) {
wsData, err := json.Marshal(WsMsg{
Type: WsMsgComplete,
Data: base64.StdEncoding.EncodeToString([]byte(suggestion)),
})
if err != nil {
global.LOG.Errorf("encoding complete output to json failed, err: %v", err)
return
}
if err := wsConn.WriteMessage(websocket.TextMessage, wsData); err != nil {
global.LOG.Errorf("sending complete output to webSocket failed, err: %v", err)
}
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/json"
"io"
"strings"
"sync"
"time"
@@ -39,7 +38,6 @@ const (
WsMsgCmd = "cmd"
WsMsgResize = "resize"
WsMsgHeartbeat = "heartbeat"
WsMsgComplete = "complete"
)
type WsMsg struct {
@@ -57,7 +55,6 @@ type LogicSshWsSession struct {
inputFilterBuff *safeBuffer
session *ssh.Session
wsConn *websocket.Conn
completer *TabCompleter
isAdmin bool
IsFlagged bool
}
@@ -90,7 +87,6 @@ func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websock
if err := sshSession.Shell(); err != nil {
return nil, err
}
completer := NewTabCompleter()
if len(initCmd) != 0 {
time.Sleep(100 * time.Millisecond)
_, _ = stdinP.Write([]byte(initCmd + "\n"))
@@ -102,7 +98,6 @@ func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websock
inputFilterBuff: inputBuf,
session: sshSession,
wsConn: wsConn,
completer: completer,
isAdmin: true,
IsFlagged: false,
}, nil
@@ -162,14 +157,6 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err)
}
case WsMsgComplete:
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data)
if err != nil {
global.LOG.Errorf("websock complete string base64 decoding failed, err: %v", err)
break
}
suggestion := sws.completer.GetCompletions(string(decodeBytes))
sws.sendComplete(wsConn, strings.Join(suggestion, "\n"))
}
}
}
@@ -224,20 +211,6 @@ func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) {
}
}
func (sws *LogicSshWsSession) sendComplete(wsConn *websocket.Conn, suggestion string) {
wsData, err := json.Marshal(WsMsg{
Type: WsMsgComplete,
Data: base64.StdEncoding.EncodeToString([]byte(suggestion)),
})
if err != nil {
global.LOG.Errorf("encoding complete output to json failed, err: %v", err)
return
}
if err := wsConn.WriteMessage(websocket.TextMessage, wsData); err != nil {
global.LOG.Errorf("sending complete output to webSocket failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) Wait(quitChan chan bool) {
if err := sws.session.Wait(); err != nil {
setQuit(quitChan)

View File

@@ -1,155 +0,0 @@
package terminal
import (
"bufio"
"fmt"
"os"
"os/exec"
"sort"
"strings"
)
type TabCompleter struct {
shell string
}
func NewTabCompleter() *TabCompleter {
return &TabCompleter{
shell: os.Getenv("SHELL"),
}
}
func (tc *TabCompleter) GetCompletions(input string) []string {
input = strings.TrimLeft(input, " \t")
trailingSpace := strings.HasSuffix(input, " ") || strings.HasSuffix(input, "\t")
if tc.shell == "/bin/bash" || tc.shell == "/usr/bin/bash" {
return tc.bashComplete(input, trailingSpace)
}
if tc.shell == "/bin/zsh" || tc.shell == "/usr/bin/zsh" {
return tc.zshComplete(input)
}
return tc.bashComplete(input, trailingSpace)
}
func (tc *TabCompleter) bashComplete(input string, trailingSpace bool) []string {
var completions []string
parts := strings.Fields(input)
if trailingSpace {
parts = append(parts, "")
}
if len(parts) <= 1 {
cmdStr := fmt.Sprintf("compgen -c -- '%s'", input)
cmd := exec.Command("bash", "-c", cmdStr)
out, err := cmd.Output()
if err == nil {
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" {
completions = append(completions, line)
}
}
}
} else {
mainCmd := parts[0]
curWord := parts[len(parts)-1]
if trailingSpace {
curWord = ""
}
cmdStr := fmt.Sprintf(`
[ -f /etc/bash_completion ] && source /etc/bash_completion
[ -f /usr/share/bash-completion/bash_completion ] &&
source /usr/share/bash-completion/bash_completion
_completion_loader %s 2>/dev/null || true
[ -f /usr/share/bash-completion/completions/%s ] &&
source /usr/share/bash-completion/completions/%s
COMP_WORDS=(%s)
COMP_CWORD=%d
COMP_LINE='%s'
COMP_POINT=%d
_%s 2>/dev/null || complete -p %s &>/dev/null || compgen -f -- '%s'
printf '%%s\n' "${COMPREPLY[@]}"
`, mainCmd, mainCmd, mainCmd,
strings.Join(parts, " "), len(parts)-1,
input, len(input),
mainCmd, mainCmd, curWord)
cmd := exec.Command("bash", "-c", cmdStr)
out, err := cmd.Output()
if err == nil {
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" {
completions = append(completions, line)
}
}
}
}
return tc.filterCompletions(input, completions)
}
func (tc *TabCompleter) filterCompletions(input string, completions []string) []string {
var filtered []string
seen := make(map[string]bool)
inputLower := strings.ToLower(input)
lastToken := inputLower
if idx := strings.LastIndex(inputLower, " "); idx != -1 {
lastToken = strings.TrimSpace(inputLower[idx+1:])
}
if lastToken == "" {
lastToken = inputLower
}
for _, comp := range completions {
if seen[comp] {
continue
}
seen[comp] = true
if strings.HasPrefix(strings.ToLower(comp), lastToken) {
filtered = append(filtered, comp)
}
}
sort.SliceStable(filtered, func(i, j int) bool {
if len(filtered[i]) != len(filtered[j]) {
return len(filtered[i]) < len(filtered[j])
}
return filtered[i] < filtered[j]
})
if len(filtered) > 20 {
filtered = filtered[:20]
}
return filtered
}
func (tc *TabCompleter) zshComplete(input string) []string {
var completions []string
cmdStr := fmt.Sprintf(`
autoload -Uz compinit
compinit
compset -P '%s'
compadd -x '%s' 2>/dev/null
`, input, input)
cmd := exec.Command("zsh", "-c", cmdStr)
out, _ := cmd.Output()
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" {
completions = append(completions, line)
}
}
return completions
}

View File

@@ -3,7 +3,6 @@ package terminal
import (
"encoding/base64"
"encoding/json"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/core/global"
@@ -17,7 +16,6 @@ type LocalWsSession struct {
allowCtrlC bool
writeMutex sync.Mutex
completer *TabCompleter
}
func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand, allowCtrlC bool) (*LocalWsSession, error) {
@@ -30,7 +28,6 @@ func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalComma
wsConn: wsConn,
allowCtrlC: allowCtrlC,
completer: NewTabCompleter(),
}, nil
}
@@ -119,14 +116,6 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err)
}
case WsMsgComplete:
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data)
if err != nil {
global.LOG.Errorf("websock complete string base64 decoding failed, err: %v", err)
break
}
suggestion := sws.completer.GetCompletions(string(decodeBytes))
sws.sendComplete(wsConn, strings.Join(suggestion, "\n"))
}
}
}
@@ -137,17 +126,3 @@ func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdByt
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
}
}
func (sws *LocalWsSession) sendComplete(wsConn *websocket.Conn, suggestion string) {
wsData, err := json.Marshal(WsMsg{
Type: WsMsgComplete,
Data: base64.StdEncoding.EncodeToString([]byte(suggestion)),
})
if err != nil {
global.LOG.Errorf("encoding complete output to json failed, err: %v", err)
return
}
if err := wsConn.WriteMessage(websocket.TextMessage, wsData); err != nil {
global.LOG.Errorf("sending complete output to webSocket failed, err: %v", err)
}
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/json"
"io"
"strings"
"sync"
"time"
@@ -39,7 +38,6 @@ const (
WsMsgCmd = "cmd"
WsMsgResize = "resize"
WsMsgHeartbeat = "heartbeat"
WsMsgComplete = "complete"
)
type WsMsg struct {
@@ -57,7 +55,6 @@ type LogicSshWsSession struct {
inputFilterBuff *safeBuffer
session *ssh.Session
wsConn *websocket.Conn
completer *TabCompleter
isAdmin bool
IsFlagged bool
}
@@ -101,7 +98,6 @@ func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websock
inputFilterBuff: inputBuf,
session: sshSession,
wsConn: wsConn,
completer: NewTabCompleter(),
isAdmin: true,
IsFlagged: false,
}, nil
@@ -162,14 +158,6 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err)
}
case WsMsgComplete:
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data)
if err != nil {
global.LOG.Errorf("websock complete string base64 decoding failed, err: %v", err)
break
}
suggestion := sws.completer.GetCompletions(string(decodeBytes))
sws.sendComplete(wsConn, strings.Join(suggestion, "\n"))
}
}
}
@@ -224,20 +212,6 @@ func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) {
}
}
func (sws *LogicSshWsSession) sendComplete(wsConn *websocket.Conn, suggestion string) {
wsData, err := json.Marshal(WsMsg{
Type: WsMsgComplete,
Data: base64.StdEncoding.EncodeToString([]byte(suggestion)),
})
if err != nil {
global.LOG.Errorf("encoding complete output to json failed, err: %v", err)
return
}
if err := wsConn.WriteMessage(websocket.TextMessage, wsData); err != nil {
global.LOG.Errorf("sending complete output to webSocket failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) Wait(quitChan chan bool) {
if err := sws.session.Wait(); err != nil {
setQuit(quitChan)

View File

@@ -20,12 +20,6 @@ const terminalSocket = ref<WebSocket>();
const heartbeatTimer = ref<NodeJS.Timer>();
const latency = ref(0);
const initCmd = ref('');
const currentLine = ref('');
const suggestionText = ref('');
const ghostText = ref('');
let suggestTimer: ReturnType<typeof setTimeout> | null = null;
const COMPLETION_DEBOUNCE_MS = 500;
const COMPLETION_MIN_CHARS = 2;
const readyWatcher = watch(
() => webSocketReady.value && termReady.value,
@@ -245,7 +239,6 @@ const onWSReceive = (message: MessageEvent) => {
const wsMsg = JSON.parse(message.data);
switch (wsMsg.type) {
case 'cmd': {
clearGhost();
term.value.element && term.value.focus();
if (wsMsg.data) {
let receiveMsg = Base64.decode(wsMsg.data);
@@ -257,27 +250,6 @@ const onWSReceive = (message: MessageEvent) => {
}
break;
}
case 'complete': {
if (!currentLine.value || currentLine.value.trim().length === 0) {
clearGhost();
break;
}
if (wsMsg.data) {
const raw = Base64.decode(wsMsg.data);
const items = raw
.split('\n')
.map((item) => item.trim())
.filter((item) => item.length > 0);
if (items.length >= 1) {
applySuggestion(items[0]);
} else {
clearGhost();
}
} else {
clearGhost();
}
break;
}
case 'heartbeat': {
latency.value = new Date().getTime() - wsMsg.timestamp;
break;
@@ -315,125 +287,9 @@ function sendMsg(data: string) {
}
}
function sendSuggestRequest(line: string) {
if (!line || line.trim().length === 0) {
return;
}
if (isWsOpen()) {
terminalSocket.value!.send(
JSON.stringify({
type: 'complete',
data: Base64.encode(line),
}),
);
}
}
function scheduleSuggest() {
if (suggestTimer) {
clearTimeout(suggestTimer);
}
if (!currentLine.value || currentLine.value.trim().length === 0) {
clearGhost();
return;
}
const token = currentLine.value.trim().split(/\s+/).pop() || '';
if (token.length < COMPLETION_MIN_CHARS) {
clearGhost();
return;
}
suggestTimer = setTimeout(() => {
sendSuggestRequest(currentLine.value);
}, COMPLETION_DEBOUNCE_MS);
}
function applySuggestion(raw: string) {
if (!raw) {
clearGhost();
return;
}
const lastTokenMatch = currentLine.value.match(/(\S+)$/);
const lastToken = lastTokenMatch ? lastTokenMatch[1] : '';
let suffix = raw;
if (lastToken && raw.startsWith(lastToken)) {
suffix = raw.slice(lastToken.length);
}
if (!suffix) {
clearGhost();
return;
}
suggestionText.value = suffix;
renderGhost(suffix);
}
function renderGhost(suffix: string) {
if (!term.value) return;
term.value.write('\x1b7');
term.value.write('\x1b[0K');
term.value.write(`\x1b[90m${suffix}\x1b[0m`);
term.value.write('\x1b8');
ghostText.value = suffix;
}
function clearGhost() {
if (!ghostText.value || !term.value) return;
term.value.write('\x1b7');
term.value.write('\x1b[0K');
term.value.write('\x1b8');
ghostText.value = '';
suggestionText.value = '';
}
function onTermData(data: string) {
if (!data) return;
if (data === '\t') {
if (ghostText.value) {
sendMsg(ghostText.value);
currentLine.value += ghostText.value;
clearGhost();
scheduleSuggest();
return;
}
sendMsg(data);
return;
}
if (data === '\r' || data === '\n') {
currentLine.value = '';
clearGhost();
sendMsg(data);
return;
}
if (data === '\x7f') {
if (currentLine.value.length > 0) {
currentLine.value = currentLine.value.slice(0, -1);
}
clearGhost();
sendMsg(data);
scheduleSuggest();
return;
}
if (data === '\x15') {
currentLine.value = '';
clearGhost();
sendMsg(data);
return;
}
if (data === '\x17') {
currentLine.value = currentLine.value.replace(/\s+\S*$/, '');
clearGhost();
sendMsg(data);
scheduleSuggest();
return;
}
if (data.startsWith('\x1b')) {
clearGhost();
sendMsg(data);
return;
}
currentLine.value += data;
clearGhost();
sendMsg(data);
scheduleSuggest();
}
// websocket 相关代码 end