mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 05:33:32 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90508c9084 | |||
| 361480f2d1 | |||
| 538565117b | |||
| 1c8742b7b6 | |||
| 2fb6a1d1ef | |||
| 6e390acb3d |
@@ -501,20 +501,6 @@ Compress the 5 MB nuclei report, summarize critical CVEs, and attach the artifac
|
||||
Build an attack chain for the latest engagement and export the node list with severity >= high.
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### Recent Highlights
|
||||
|
||||
- **2026-01-27** – OpenAPI documentation with interactive testing interface, supporting conversation management, message interaction, and result querying
|
||||
- **2026-01-15** – Skills system with 20+ predefined security testing skills
|
||||
- **2026-01-11** – Role-based testing with predefined security testing roles
|
||||
- **2026-01-08** – SSE transport mode support for external MCP servers
|
||||
- **2026-01-01** – Batch task management with queue-based execution
|
||||
- **2025-12-25** – Vulnerability management and conversation grouping features
|
||||
- **2025-12-20** – Knowledge base with vector search and hybrid retrieval
|
||||
|
||||
|
||||
|
||||
## 404Starlink
|
||||
|
||||
<img src="./images/404StarLinkLogo.png" width="30%">
|
||||
@@ -532,6 +518,22 @@ CyberStrikeAI has joined [404Starlink](https://github.com/knownsec/404StarLink)
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
**This tool is for educational and authorized testing purposes only!**
|
||||
|
||||
CyberStrikeAI is a professional security testing platform designed to assist security researchers, penetration testers, and IT professionals in conducting security assessments and vulnerability research **with explicit authorization**.
|
||||
|
||||
**By using this tool, you agree to:**
|
||||
- Use this tool only on systems where you have clear written authorization
|
||||
- Comply with all applicable laws, regulations, and ethical standards
|
||||
- Take full responsibility for any unauthorized use or misuse
|
||||
- Not use this tool for any illegal or malicious purposes
|
||||
|
||||
**The developers are not responsible for any misuse!** Please ensure your usage complies with local laws and regulations, and that you have obtained explicit authorization from the target system owner.
|
||||
|
||||
---
|
||||
|
||||
Need help or want to contribute? Open an issue or PR—community tooling additions are welcome!
|
||||
|
||||
+16
-13
@@ -500,19 +500,6 @@ CyberStrikeAI/
|
||||
构建最新一次测试的攻击链,只导出风险 >= 高的节点列表。
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 近期亮点
|
||||
|
||||
- **2026-01-27** – 新增 OpenAPI 文档,提供交互式测试界面,支持对话管理、消息交互和结果查询
|
||||
- **2026-01-15** – 新增 Skills 技能系统,内置 20+ 预设安全测试技能
|
||||
- **2026-01-11** – 新增角色化测试功能,支持预设安全测试角色
|
||||
- **2026-01-08** – 新增 SSE 传输模式支持,外部 MCP 联邦支持三种模式
|
||||
- **2026-01-01** – 新增批量任务管理功能,支持队列式任务执行
|
||||
- **2025-12-25** – 新增漏洞管理和对话分组功能
|
||||
- **2025-12-20** – 新增知识库功能,支持向量检索和混合搜索
|
||||
|
||||
|
||||
## 404星链计划
|
||||
<img src="./images/404StarLinkLogo.png" width="30%">
|
||||
|
||||
@@ -530,4 +517,20 @@ CyberStrikeAI 现已加入 [404星链计划](https://github.com/knownsec/404Star
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 免责声明
|
||||
|
||||
**本工具仅供教育和授权测试使用!**
|
||||
|
||||
CyberStrikeAI 是一个专业的安全测试平台,旨在帮助安全研究人员、渗透测试人员和IT专业人员在**获得明确授权**的情况下进行安全评估和漏洞研究。
|
||||
|
||||
**使用本工具即表示您同意:**
|
||||
- 仅在您拥有明确书面授权的系统上使用此工具
|
||||
- 遵守所有适用的法律法规和道德准则
|
||||
- 对任何未经授权的使用或滥用行为承担全部责任
|
||||
- 不会将本工具用于任何非法或恶意目的
|
||||
|
||||
**开发者不对任何滥用行为负责!** 请确保您的使用符合当地法律法规,并获得目标系统所有者的明确授权。
|
||||
|
||||
---
|
||||
|
||||
欢迎提交 Issue/PR 贡献新的工具模版或优化建议!
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.3.15"
|
||||
version: "v1.3.16"
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.4.22
|
||||
github.com/mattn/go-sqlite3 v1.14.18
|
||||
github.com/modelcontextprotocol/go-sdk v1.2.0
|
||||
@@ -29,7 +30,6 @@ require (
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
|
||||
@@ -634,6 +634,7 @@ func setupRoutes(
|
||||
// 系统设置 - 终端(执行命令,提高运维效率)
|
||||
protected.POST("/terminal/run", terminalHandler.RunCommand)
|
||||
protected.POST("/terminal/run/stream", terminalHandler.RunCommandStream)
|
||||
protected.GET("/terminal/ws", terminalHandler.RunCommandWS)
|
||||
|
||||
// 外部MCP管理
|
||||
protected.GET("/external-mcp", externalMCPHandler.GetExternalMCPs)
|
||||
|
||||
+11
-38
@@ -128,9 +128,8 @@ type ChatRequest struct {
|
||||
}
|
||||
|
||||
const (
|
||||
maxAttachments = 10
|
||||
maxAttachmentBytes = 2 * 1024 * 1024 // 单文件约 2MB(仅用于是否内联展示内容,不限制上传)
|
||||
chatUploadsDirName = "chat_uploads" // 对话附件保存的根目录(相对当前工作目录)
|
||||
maxAttachments = 10
|
||||
chatUploadsDirName = "chat_uploads" // 对话附件保存的根目录(相对当前工作目录)
|
||||
)
|
||||
|
||||
// saveAttachmentsToDateAndConversationDir 将附件保存到 chat_uploads/YYYY-MM-DD/{conversationID}/,返回每个文件的保存路径(与 attachments 顺序一致)
|
||||
@@ -223,45 +222,19 @@ func userMessageContentForStorage(message string, attachments []ChatAttachment,
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// appendAttachmentsToMessage 将附件内容拼接到用户消息末尾;若 savedPaths 与 attachments 一一对应,会先写入“已保存到”路径供大模型按路径读取
|
||||
func appendAttachmentsToMessage(msg string, attachments []ChatAttachment, savedPaths []string, logger *zap.Logger) string {
|
||||
// appendAttachmentsToMessage 仅将附件的保存路径追加到用户消息末尾,不再内联附件内容,避免上下文过长
|
||||
func appendAttachmentsToMessage(msg string, attachments []ChatAttachment, savedPaths []string) string {
|
||||
if len(attachments) == 0 {
|
||||
return msg
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(msg)
|
||||
if len(savedPaths) == len(attachments) {
|
||||
b.WriteString("\n\n[用户上传的文件已保存到以下路径(可使用 cat/exec 等工具按路径读取)]\n")
|
||||
for i, a := range attachments {
|
||||
b.WriteString(fmt.Sprintf("- %s: %s\n", a.FileName, savedPaths[i]))
|
||||
}
|
||||
b.WriteString("\n[以下为附件内容(便于直接参考)]\n")
|
||||
}
|
||||
b.WriteString("\n\n[用户上传的文件已保存到以下路径(请按需读取文件内容,而不是依赖内联内容)]\n")
|
||||
for i, a := range attachments {
|
||||
b.WriteString(fmt.Sprintf("\n--- 附件 %d: %s ---\n", i+1, a.FileName))
|
||||
content := a.Content
|
||||
mime := strings.ToLower(strings.TrimSpace(a.MimeType))
|
||||
isText := strings.HasPrefix(mime, "text/") || mime == "" ||
|
||||
strings.Contains(mime, "json") || strings.Contains(mime, "xml") ||
|
||||
strings.Contains(mime, "javascript") || strings.Contains(mime, "shell")
|
||||
if isText && len(content) > 0 {
|
||||
if decoded, err := base64.StdEncoding.DecodeString(content); err == nil && len(decoded) > 0 {
|
||||
content = string(decoded)
|
||||
}
|
||||
b.WriteString("```\n")
|
||||
b.WriteString(content)
|
||||
b.WriteString("\n```\n")
|
||||
if i < len(savedPaths) && savedPaths[i] != "" {
|
||||
b.WriteString(fmt.Sprintf("- %s: %s\n", a.FileName, savedPaths[i]))
|
||||
} else {
|
||||
if decoded, err := base64.StdEncoding.DecodeString(content); err == nil {
|
||||
content = string(decoded)
|
||||
}
|
||||
if utf8.ValidString(content) && len(content) < maxAttachmentBytes {
|
||||
b.WriteString("```\n")
|
||||
b.WriteString(content)
|
||||
b.WriteString("\n```\n")
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("(二进制文件,约 %d 字节,已保存到上述路径,可按路径读取)\n", len(content)))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("- %s: (路径未知,可能保存失败)\n", a.FileName))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
@@ -373,7 +346,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
finalMessage = appendAttachmentsToMessage(finalMessage, req.Attachments, savedPaths, h.logger)
|
||||
finalMessage = appendAttachmentsToMessage(finalMessage, req.Attachments, savedPaths)
|
||||
|
||||
// 保存用户消息:有附件时一并保存附件名与路径,刷新后显示、继续对话时大模型也能从历史中拿到路径
|
||||
userContent := userMessageContentForStorage(req.Message, req.Attachments, savedPaths)
|
||||
@@ -829,8 +802,8 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 将附件内容拼接到 finalMessage,便于大模型识别上传了哪些文件及内容
|
||||
finalMessage = appendAttachmentsToMessage(finalMessage, req.Attachments, savedPaths, h.logger)
|
||||
// 仅将附件保存路径追加到 finalMessage,避免将文件内容内联到大模型上下文中
|
||||
finalMessage = appendAttachmentsToMessage(finalMessage, req.Attachments, savedPaths)
|
||||
// 如果roleTools为空,表示使用所有工具(默认角色或未配置工具的角色)
|
||||
|
||||
// 保存用户消息:有附件时一并保存附件名与路径,刷新后显示、继续对话时大模型也能从历史中拿到路径
|
||||
|
||||
@@ -27,6 +27,19 @@ type TerminalHandler struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// maskTerminalCommand 对可能包含敏感信息的终端命令做脱敏,避免在日志中直接记录密码等内容
|
||||
func maskTerminalCommand(cmd string) string {
|
||||
trimmed := strings.TrimSpace(cmd)
|
||||
lower := strings.ToLower(trimmed)
|
||||
if strings.Contains(lower, "sudo") || strings.Contains(lower, "password") {
|
||||
return "[masked sensitive terminal command]"
|
||||
}
|
||||
if len(trimmed) > 256 {
|
||||
return trimmed[:256] + "..."
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// NewTerminalHandler 创建终端处理器
|
||||
func NewTerminalHandler(logger *zap.Logger) *TerminalHandler {
|
||||
return &TerminalHandler{logger: logger}
|
||||
@@ -146,7 +159,7 @@ func (h *TerminalHandler) RunCommand(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
h.logger.Debug("终端命令执行异常", zap.String("command", cmdStr), zap.Error(err))
|
||||
h.logger.Debug("终端命令执行异常", zap.String("command", maskTerminalCommand(cmdStr)), zap.Error(err))
|
||||
}
|
||||
|
||||
// 统一为 \n,避免前端因 \r 出现错位/对角线排版
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
//go:build !windows
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// wsUpgrader 仅用于系统设置中的终端 WebSocket,会复用已有的登录保护(JWT 中间件在上层路由组)
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// 由于已在 Gin 路由层做了认证,这里放宽 Origin,方便在同一域名下通过 HTTPS/WSS 访问
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// RunCommandWS 提供真正交互式 Shell:基于 WebSocket + PTY 的长会话
|
||||
// 前端建立 WebSocket 连接后,所有键盘输入都会透传到 Shell,Shell 的输出也会实时写回前端。
|
||||
func (h *TerminalHandler) RunCommandWS(c *gin.Context) {
|
||||
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 启动交互式 Shell,这里优先使用 bash,找不到则退回 sh
|
||||
shell := "bash"
|
||||
if _, err := exec.LookPath(shell); err != nil {
|
||||
shell = "sh"
|
||||
}
|
||||
cmd := exec.Command(shell)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"COLUMNS=120",
|
||||
"LINES=40",
|
||||
"TERM=xterm-256color",
|
||||
)
|
||||
|
||||
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: ptyCols, Rows: ptyRows})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer ptmx.Close()
|
||||
|
||||
// Shell -> WebSocket:将 PTY 输出实时发给前端
|
||||
doneChan := make(chan struct{})
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := ptmx.Read(buf)
|
||||
if n > 0 {
|
||||
_ = conn.WriteMessage(websocket.TextMessage, buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
close(doneChan)
|
||||
}()
|
||||
|
||||
// WebSocket -> Shell:将前端输入写入 PTY(包括 sudo 密码、Ctrl+C 等)
|
||||
conn.SetReadLimit(64 * 1024)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(terminalTimeout))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(terminalTimeout))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
msgType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
if msgType != websocket.TextMessage && msgType != websocket.BinaryMessage {
|
||||
continue
|
||||
}
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := ptmx.Write(data); err != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
<-doneChan
|
||||
}
|
||||
|
||||
+63
-184
@@ -15,7 +15,7 @@
|
||||
var currentTabId = 1;
|
||||
var inited = false;
|
||||
var tabIdCounter = 1;
|
||||
var PROMPT = '\x1b[32m$\x1b[0m ';
|
||||
var PROMPT = ''; // 真实 Shell 自己输出提示符,这里不再自定义
|
||||
var HISTORY_MAX = 100;
|
||||
var CANCEL_AFTER_MS = 125000;
|
||||
|
||||
@@ -26,20 +26,16 @@
|
||||
return terminals[0] || null;
|
||||
}
|
||||
|
||||
var WELCOME_LINE = 'CyberStrikeAI 终端 - 直接输入命令,Enter 执行;↑↓ 历史;Ctrl+L 清屏\r\n';
|
||||
var WELCOME_LINE = 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏\r\n';
|
||||
|
||||
function writePrompt(tab) {
|
||||
var t = tab || getCurrent();
|
||||
if (t && t.term) t.term.write(PROMPT);
|
||||
// 提示符交由后端 Shell 自行输出,这里仅保留占位函数,避免旧代码报错
|
||||
}
|
||||
|
||||
function redrawTabDisplay(t) {
|
||||
if (!t || !t.term) return;
|
||||
t.term.clear();
|
||||
t.lineBuffer = '';
|
||||
if (t.cursorIndex !== undefined) t.cursorIndex = 0;
|
||||
t.term.write(WELCOME_LINE);
|
||||
t.term.write(PROMPT);
|
||||
}
|
||||
|
||||
function writeln(tabOrS, s) {
|
||||
@@ -65,100 +61,66 @@
|
||||
t.term.write(suffix);
|
||||
}
|
||||
|
||||
function getAuthHeaders() {
|
||||
var h = new Headers();
|
||||
h.set('Content-Type', 'application/json');
|
||||
// 从本地存储中获取当前登录 token(与 auth.js 使用的结构保持一致)
|
||||
function getStoredAuthToken() {
|
||||
try {
|
||||
var auth = localStorage.getItem('cyberstrike-auth');
|
||||
if (auth) {
|
||||
var o = JSON.parse(auth);
|
||||
if (o && o.token) h.set('Authorization', 'Bearer ' + o.token);
|
||||
}
|
||||
var raw = localStorage.getItem('cyberstrike-auth');
|
||||
if (!raw) return null;
|
||||
var o = JSON.parse(raw);
|
||||
if (o && o.token) return o.token;
|
||||
} catch (e) {}
|
||||
return h;
|
||||
return null;
|
||||
}
|
||||
|
||||
function runCommand(cmd, tab) {
|
||||
var t = tab || getCurrent();
|
||||
if (!t) return;
|
||||
if (t.running) return;
|
||||
runCommandImpl(cmd, t);
|
||||
// WebSocket 地址构造(兼容 http/https,并通过 query 传递 token 以通过后端鉴权)
|
||||
function buildTerminalWSURL() {
|
||||
var proto = (window.location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
var url = proto + window.location.host + '/api/terminal/ws';
|
||||
var token = getStoredAuthToken();
|
||||
if (token) {
|
||||
url += '?token=' + encodeURIComponent(token);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function runCommandImpl(cmd, t) {
|
||||
t.running = true;
|
||||
t.abortController = new AbortController();
|
||||
var cancelTimer = setTimeout(function () {
|
||||
if (!t.running) return;
|
||||
t.running = false;
|
||||
writeln(t, '\x1b[2m(已取消 可继续输入)\x1b[0m');
|
||||
writePrompt(t);
|
||||
}, CANCEL_AFTER_MS);
|
||||
function ensureTerminalWS(tab) {
|
||||
if (tab.ws && (tab.ws.readyState === WebSocket.OPEN || tab.ws.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var ws = new WebSocket(buildTerminalWSURL());
|
||||
tab.ws = ws;
|
||||
tab.running = true;
|
||||
|
||||
var done = function () {
|
||||
clearTimeout(cancelTimer);
|
||||
t.running = false;
|
||||
t.abortController = null;
|
||||
writePrompt(t);
|
||||
};
|
||||
ws.onopen = function () {
|
||||
if (tab.term) {
|
||||
tab.term.focus();
|
||||
}
|
||||
};
|
||||
|
||||
fetch('/api/terminal/run/stream', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ command: cmd }),
|
||||
signal: t.abortController.signal
|
||||
}).then(function (res) {
|
||||
if (!res.ok) return res.json().then(function (d) { throw new Error(d.error || 'HTTP ' + res.status); });
|
||||
var ct = res.headers.get('Content-Type') || '';
|
||||
if (ct.indexOf('text/event-stream') !== -1 && res.body) {
|
||||
return readSSEStream(res.body, t).then(done).catch(function () { done(); });
|
||||
}
|
||||
return res.json().then(function (data) {
|
||||
if (data.stdout) writeOutput(t, data.stdout, false);
|
||||
if (data.stderr) writeOutput(t, data.stderr, true);
|
||||
done();
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
writeln(t, '\x1b[2m(已取消)\x1b[0m');
|
||||
} else {
|
||||
writeln(t, '\x1b[31m错误: ' + (err.message || String(err)) + '\x1b[0m');
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
ws.onmessage = function (ev) {
|
||||
if (!tab.term) return;
|
||||
tab.term.write(ev.data);
|
||||
};
|
||||
|
||||
function readSSEStream(body, t) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var reader = body.getReader();
|
||||
var decoder = new TextDecoder();
|
||||
var buf = '';
|
||||
function read() {
|
||||
reader.read().then(function (result) {
|
||||
if (result.done) { resolve(); return; }
|
||||
buf += decoder.decode(result.value, { stream: true });
|
||||
var i;
|
||||
while ((i = buf.indexOf('\n\n')) !== -1) {
|
||||
var block = buf.slice(0, i);
|
||||
buf = buf.slice(i + 2);
|
||||
var dataLine = block.match(/data:\s*(.+)/);
|
||||
if (dataLine) {
|
||||
try {
|
||||
var ev = JSON.parse(dataLine[1]);
|
||||
if (ev.t === 'out' && ev.d !== undefined) t.term.writeln(ev.d);
|
||||
else if (ev.t === 'err' && ev.d !== undefined) t.term.write('\x1b[31m' + ev.d + '\x1b[0m\n');
|
||||
else if (ev.t === 'exit') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
read();
|
||||
}).catch(reject);
|
||||
ws.onclose = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[2m[会话已关闭]\x1b[0m');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[终端连接出错]\x1b[0m');
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[无法连接终端服务: ' + String(e) + ']\x1b[0m');
|
||||
}
|
||||
read();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createTerminalInContainer(container, tab) {
|
||||
@@ -206,7 +168,6 @@
|
||||
}
|
||||
term.open(container);
|
||||
term.write(WELCOME_LINE);
|
||||
term.write(PROMPT);
|
||||
container.addEventListener('click', function () {
|
||||
switchTerminalTab(tab.id);
|
||||
if (term) term.focus();
|
||||
@@ -214,105 +175,23 @@
|
||||
container.setAttribute('tabindex', '0');
|
||||
container.title = '点击此处后输入命令';
|
||||
|
||||
function redrawLine(t) {
|
||||
if (!t || !t.term) return;
|
||||
var n = t.lineBuffer.length - t.cursorIndex;
|
||||
t.term.write('\r\x1b[K' + PROMPT + t.lineBuffer);
|
||||
if (n > 0) t.term.write('\x1b[' + n + 'D');
|
||||
function sendToWS(data) {
|
||||
ensureTerminalWS(tab);
|
||||
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
tab.ws.send(data);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
term.onData(function (data) {
|
||||
// Ctrl+L:本地清屏,同时把 ^L 也发给后端
|
||||
if (data === '\x0c') {
|
||||
term.clear();
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
writePrompt(tab);
|
||||
sendToWS(data);
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[A') {
|
||||
if (tab.history.length === 0) return;
|
||||
if (tab.historyIndex < 0) tab.historyIndex = tab.history.length;
|
||||
tab.historyIndex--;
|
||||
if (tab.historyIndex < 0) tab.historyIndex = 0;
|
||||
tab.lineBuffer = tab.history[tab.historyIndex];
|
||||
tab.cursorIndex = tab.lineBuffer.length;
|
||||
term.write('\r\x1b[K' + PROMPT + tab.lineBuffer);
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[B') {
|
||||
if (tab.history.length === 0) return;
|
||||
tab.historyIndex++;
|
||||
if (tab.historyIndex >= tab.history.length) {
|
||||
tab.historyIndex = -1;
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
term.write('\r\x1b[K' + PROMPT);
|
||||
} else {
|
||||
tab.lineBuffer = tab.history[tab.historyIndex];
|
||||
tab.cursorIndex = tab.lineBuffer.length;
|
||||
term.write('\r\x1b[K' + PROMPT + tab.lineBuffer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[D') {
|
||||
if (tab.cursorIndex > 0) {
|
||||
tab.cursorIndex--;
|
||||
term.write('\x1b[D');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[C') {
|
||||
if (tab.cursorIndex < tab.lineBuffer.length) {
|
||||
tab.cursorIndex++;
|
||||
term.write('\x1b[C');
|
||||
}
|
||||
return;
|
||||
}
|
||||
var code = data.charCodeAt(0);
|
||||
if (code === 13 || code === 10) {
|
||||
var cmd = tab.lineBuffer.trim();
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
tab.historyIndex = -1;
|
||||
term.writeln('');
|
||||
if (cmd) {
|
||||
if (tab.history.indexOf(cmd) === -1) {
|
||||
tab.history.push(cmd);
|
||||
if (tab.history.length > HISTORY_MAX) tab.history.shift();
|
||||
}
|
||||
runCommand(cmd, tab);
|
||||
} else {
|
||||
writePrompt(tab);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (code === 127) {
|
||||
if (tab.cursorIndex > 0) {
|
||||
tab.lineBuffer = tab.lineBuffer.slice(0, tab.cursorIndex - 1) + tab.lineBuffer.slice(tab.cursorIndex);
|
||||
tab.cursorIndex--;
|
||||
redrawLine(tab);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (code === 3) {
|
||||
if (tab.running && tab.abortController) {
|
||||
tab.abortController.abort();
|
||||
}
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
term.writeln('^C');
|
||||
writePrompt(tab);
|
||||
return;
|
||||
}
|
||||
if (data.length === 1 && code >= 32) {
|
||||
tab.lineBuffer = tab.lineBuffer.slice(0, tab.cursorIndex) + data + tab.lineBuffer.slice(tab.cursorIndex);
|
||||
tab.cursorIndex++;
|
||||
redrawLine(tab);
|
||||
return;
|
||||
}
|
||||
tab.lineBuffer += data;
|
||||
tab.cursorIndex = tab.lineBuffer.length;
|
||||
term.write(data);
|
||||
sendToWS(data);
|
||||
});
|
||||
|
||||
tab.term = term;
|
||||
|
||||
Reference in New Issue
Block a user