mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-18 14:04:52 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2545774187 | |||
| 4bc62773a9 | |||
| 38285ba888 | |||
| 251b5fd440 | |||
| 922136f545 | |||
| 735cd5edc4 | |||
| 6a32dcc08e | |||
| b8b7aa0ffe | |||
| 5224c68bc7 | |||
| b504f405a8 | |||
| 3dc6dbcfe0 |
@@ -174,10 +174,20 @@ go build -o cyberstrike-ai cmd/server/main.go
|
|||||||
|
|
||||||
### Version Update (No Breaking Changes)
|
### Version Update (No Breaking Changes)
|
||||||
|
|
||||||
**CyberStrikeAI version update (when there are no compatibility changes):**
|
**CyberStrikeAI one-click upgrade (recommended):**
|
||||||
1. Download the latest source code.
|
1. (First time) enable the script: `chmod +x upgrade.sh`
|
||||||
2. Copy the old project's `/data` folder and `config.yaml` file into the new source directory.
|
2. Upgrade with: `./upgrade.sh` (optional flags: `--tag vX.Y.Z`, `--no-venv`, `--preserve-custom`, `--yes`)
|
||||||
3. Restart with: `chmod +x run.sh && ./run.sh`
|
3. The script will back up your `config.yaml` and `data/`, upgrade the code from GitHub Release, update `config.yaml`'s `version`, then restart the server.
|
||||||
|
|
||||||
|
Recommended one-liner:
|
||||||
|
`chmod +x upgrade.sh && ./upgrade.sh --yes`
|
||||||
|
|
||||||
|
If something goes wrong, you can restore from `.upgrade-backup/` (or manually copy `/data` and `config.yaml` back) and run `./run.sh` again.
|
||||||
|
|
||||||
|
Requirements / tips:
|
||||||
|
* You need `curl` or `wget` for downloading Release packages.
|
||||||
|
* `rsync` is recommended/required for the safe code sync.
|
||||||
|
* If GitHub API rate-limits you, set `export GITHUB_TOKEN="..."` before running `./upgrade.sh`.
|
||||||
|
|
||||||
⚠️ **Note:** This procedure only applies to version updates without compatibility or breaking changes. If a release includes compatibility changes, this method may not apply.
|
⚠️ **Note:** This procedure only applies to version updates without compatibility or breaking changes. If a release includes compatibility changes, this method may not apply.
|
||||||
|
|
||||||
|
|||||||
+13
-3
@@ -173,9 +173,19 @@ go build -o cyberstrike-ai cmd/server/main.go
|
|||||||
|
|
||||||
### CyberStrikeAI 版本更新(无兼容性问题)
|
### CyberStrikeAI 版本更新(无兼容性问题)
|
||||||
|
|
||||||
1. 下载最新源代码;
|
1. (首次使用)启用脚本:`chmod +x upgrade.sh`
|
||||||
2. 将旧项目的 `/data` 文件夹、`config.yaml` 文件复制至新版源代码目录;
|
2. 一键升级:`./upgrade.sh`(可选参数:`--tag vX.Y.Z`、`--no-venv`、`--preserve-custom`、`--yes`)
|
||||||
3. 执行命令重启:`chmod +x run.sh && ./run.sh`
|
3. 脚本会备份你的 `config.yaml` 和 `data/`,从 GitHub Release 升级代码,更新 `config.yaml` 的 `version` 字段后重启服务。
|
||||||
|
|
||||||
|
推荐的一键指令:
|
||||||
|
`chmod +x upgrade.sh && ./upgrade.sh --yes`
|
||||||
|
|
||||||
|
如果升级失败,可以从 `.upgrade-backup/` 恢复,或按旧方式手动拷贝 `/data` 和 `config.yaml` 后再运行 `./run.sh`。
|
||||||
|
|
||||||
|
依赖/提示:
|
||||||
|
* 需要 `curl` 或 `wget` 用于下载 GitHub Release 包。
|
||||||
|
* 建议/需要 `rsync` 用于安全同步代码。
|
||||||
|
* 如果遇到 GitHub API 限流,运行前设置 `export GITHUB_TOKEN="..."` 再执行 `./upgrade.sh`。
|
||||||
|
|
||||||
⚠️ **注意:** 仅适用于无兼容性变更的版本更新。若版本存在兼容性调整,此方法不适用。
|
⚠️ **注意:** 仅适用于无兼容性变更的版本更新。若版本存在兼容性调整,此方法不适用。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||||
version: "v1.3.28"
|
version: "v1.3.29"
|
||||||
|
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
|||||||
attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger)
|
attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger)
|
||||||
vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger)
|
vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger)
|
||||||
webshellHandler := handler.NewWebShellHandler(log.Logger, db)
|
webshellHandler := handler.NewWebShellHandler(log.Logger, db)
|
||||||
|
chatUploadsHandler := handler.NewChatUploadsHandler(log.Logger)
|
||||||
registerWebshellTools(mcpServer, db, webshellHandler, log.Logger)
|
registerWebshellTools(mcpServer, db, webshellHandler, log.Logger)
|
||||||
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
|
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
|
||||||
externalMCPHandler := handler.NewExternalMCPHandler(externalMCPMgr, cfg, configPath, log.Logger)
|
externalMCPHandler := handler.NewExternalMCPHandler(externalMCPMgr, cfg, configPath, log.Logger)
|
||||||
@@ -439,6 +440,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
|||||||
app, // 传递 App 实例以便动态获取 knowledgeHandler
|
app, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||||
vulnerabilityHandler,
|
vulnerabilityHandler,
|
||||||
webshellHandler,
|
webshellHandler,
|
||||||
|
chatUploadsHandler,
|
||||||
roleHandler,
|
roleHandler,
|
||||||
skillsHandler,
|
skillsHandler,
|
||||||
fofaHandler,
|
fofaHandler,
|
||||||
@@ -567,6 +569,7 @@ func setupRoutes(
|
|||||||
app *App, // 传递 App 实例以便动态获取 knowledgeHandler
|
app *App, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||||
vulnerabilityHandler *handler.VulnerabilityHandler,
|
vulnerabilityHandler *handler.VulnerabilityHandler,
|
||||||
webshellHandler *handler.WebShellHandler,
|
webshellHandler *handler.WebShellHandler,
|
||||||
|
chatUploadsHandler *handler.ChatUploadsHandler,
|
||||||
roleHandler *handler.RoleHandler,
|
roleHandler *handler.RoleHandler,
|
||||||
skillsHandler *handler.SkillsHandler,
|
skillsHandler *handler.SkillsHandler,
|
||||||
fofaHandler *handler.FofaHandler,
|
fofaHandler *handler.FofaHandler,
|
||||||
@@ -838,6 +841,15 @@ func setupRoutes(
|
|||||||
protected.POST("/webshell/exec", webshellHandler.Exec)
|
protected.POST("/webshell/exec", webshellHandler.Exec)
|
||||||
protected.POST("/webshell/file", webshellHandler.FileOp)
|
protected.POST("/webshell/file", webshellHandler.FileOp)
|
||||||
|
|
||||||
|
// 对话附件(chat_uploads)管理
|
||||||
|
protected.GET("/chat-uploads", chatUploadsHandler.List)
|
||||||
|
protected.GET("/chat-uploads/download", chatUploadsHandler.Download)
|
||||||
|
protected.GET("/chat-uploads/content", chatUploadsHandler.GetContent)
|
||||||
|
protected.POST("/chat-uploads", chatUploadsHandler.Upload)
|
||||||
|
protected.DELETE("/chat-uploads", chatUploadsHandler.Delete)
|
||||||
|
protected.PUT("/chat-uploads/rename", chatUploadsHandler.Rename)
|
||||||
|
protected.PUT("/chat-uploads/content", chatUploadsHandler.PutContent)
|
||||||
|
|
||||||
// 角色管理
|
// 角色管理
|
||||||
protected.GET("/roles", roleHandler.GetRoles)
|
protected.GET("/roles", roleHandler.GetRoles)
|
||||||
protected.GET("/roles/:name", roleHandler.GetRole)
|
protected.GET("/roles/:name", roleHandler.GetRole)
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
chatUploadsRootDirName = "chat_uploads"
|
||||||
|
maxChatUploadEditBytes = 2 * 1024 * 1024 // 文本编辑上限
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChatUploadsHandler 对话中上传附件(chat_uploads 目录)的管理 API
|
||||||
|
type ChatUploadsHandler struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChatUploadsHandler 创建处理器
|
||||||
|
func NewChatUploadsHandler(logger *zap.Logger) *ChatUploadsHandler {
|
||||||
|
return &ChatUploadsHandler{logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatUploadsHandler) absRoot() (string, error) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Abs(filepath.Join(cwd, chatUploadsRootDirName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveUnderChatUploads 校验 relativePath(使用 / 分隔)对应文件必须在 chat_uploads 根下
|
||||||
|
func (h *ChatUploadsHandler) resolveUnderChatUploads(relativePath string) (abs string, err error) {
|
||||||
|
root, err := h.absRoot()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
rel := strings.TrimSpace(relativePath)
|
||||||
|
if rel == "" {
|
||||||
|
return "", fmt.Errorf("empty path")
|
||||||
|
}
|
||||||
|
rel = filepath.Clean(filepath.FromSlash(rel))
|
||||||
|
if rel == "." || strings.HasPrefix(rel, "..") {
|
||||||
|
return "", fmt.Errorf("invalid path")
|
||||||
|
}
|
||||||
|
full := filepath.Join(root, rel)
|
||||||
|
full, err = filepath.Abs(full)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
rootAbs, _ := filepath.Abs(root)
|
||||||
|
if full != rootAbs && !strings.HasPrefix(full, rootAbs+string(filepath.Separator)) {
|
||||||
|
return "", fmt.Errorf("path escapes chat_uploads root")
|
||||||
|
}
|
||||||
|
return full, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatUploadFileItem 列表项
|
||||||
|
type ChatUploadFileItem struct {
|
||||||
|
RelativePath string `json:"relativePath"`
|
||||||
|
AbsolutePath string `json:"absolutePath"` // 服务器上的绝对路径,便于在对话中引用(与附件落盘路径一致)
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
ModifiedUnix int64 `json:"modifiedUnix"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ConversationID string `json:"conversationId"`
|
||||||
|
// SubPath 为日期、会话目录之下的子路径(不含文件名),如 date/conv/a/b/file 则为 "a/b";无嵌套则为 ""。
|
||||||
|
SubPath string `json:"subPath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// List GET /api/chat-uploads
|
||||||
|
func (h *ChatUploadsHandler) List(c *gin.Context) {
|
||||||
|
conversationFilter := strings.TrimSpace(c.Query("conversation"))
|
||||||
|
root, err := h.absRoot()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(root); os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"files": []ChatUploadFileItem{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var files []ChatUploadFileItem
|
||||||
|
err = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
info, err := d.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
relSlash := filepath.ToSlash(rel)
|
||||||
|
parts := strings.Split(relSlash, "/")
|
||||||
|
var dateStr, convID string
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
dateStr = parts[0]
|
||||||
|
}
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
convID = parts[1]
|
||||||
|
}
|
||||||
|
var subPath string
|
||||||
|
if len(parts) >= 4 {
|
||||||
|
subPath = strings.Join(parts[2:len(parts)-1], "/")
|
||||||
|
}
|
||||||
|
if conversationFilter != "" && convID != conversationFilter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
absPath, _ := filepath.Abs(path)
|
||||||
|
files = append(files, ChatUploadFileItem{
|
||||||
|
RelativePath: relSlash,
|
||||||
|
AbsolutePath: absPath,
|
||||||
|
Name: d.Name(),
|
||||||
|
Size: info.Size(),
|
||||||
|
ModifiedUnix: info.ModTime().Unix(),
|
||||||
|
Date: dateStr,
|
||||||
|
ConversationID: convID,
|
||||||
|
SubPath: subPath,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("列举对话附件失败", zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
return files[i].ModifiedUnix > files[j].ModifiedUnix
|
||||||
|
})
|
||||||
|
c.JSON(http.StatusOK, gin.H{"files": files})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download GET /api/chat-uploads/download?path=...
|
||||||
|
func (h *ChatUploadsHandler) Download(c *gin.Context) {
|
||||||
|
p := c.Query("path")
|
||||||
|
abs, err := h.resolveUnderChatUploads(p)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st, err := os.Stat(abs)
|
||||||
|
if err != nil || st.IsDir() {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.FileAttachment(abs, filepath.Base(abs))
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatUploadPathBody struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete DELETE /api/chat-uploads
|
||||||
|
func (h *ChatUploadsHandler) Delete(c *gin.Context) {
|
||||||
|
var body chatUploadPathBody
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.Path) == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
abs, err := h.resolveUnderChatUploads(body.Path)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st, err := os.Stat(abs)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if st.IsDir() {
|
||||||
|
if err := os.RemoveAll(abs); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := os.Remove(abs); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatUploadRenameBody struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
NewName string `json:"newName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename PUT /api/chat-uploads/rename
|
||||||
|
func (h *ChatUploadsHandler) Rename(c *gin.Context) {
|
||||||
|
var body chatUploadRenameBody
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newName := strings.TrimSpace(body.NewName)
|
||||||
|
if newName == "" || strings.ContainsAny(newName, `/\`) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid newName"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
abs, err := h.resolveUnderChatUploads(body.Path)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(abs)
|
||||||
|
newAbs := filepath.Join(dir, filepath.Base(newName))
|
||||||
|
root, _ := h.absRoot()
|
||||||
|
newAbs, _ = filepath.Abs(newAbs)
|
||||||
|
if newAbs != root && !strings.HasPrefix(newAbs, root+string(filepath.Separator)) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target path"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.Rename(abs, newAbs); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newRel, _ := filepath.Rel(root, newAbs)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"ok": true, "relativePath": filepath.ToSlash(newRel)})
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatUploadContentBody struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContent GET /api/chat-uploads/content?path=...
|
||||||
|
func (h *ChatUploadsHandler) GetContent(c *gin.Context) {
|
||||||
|
p := c.Query("path")
|
||||||
|
abs, err := h.resolveUnderChatUploads(p)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st, err := os.Stat(abs)
|
||||||
|
if err != nil || st.IsDir() {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if st.Size() > maxChatUploadEditBytes {
|
||||||
|
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "file too large for editor"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(abs)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !utf8.Valid(b) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "binary file not editable in UI"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"content": string(b)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutContent PUT /api/chat-uploads/content
|
||||||
|
func (h *ChatUploadsHandler) PutContent(c *gin.Context) {
|
||||||
|
var body chatUploadContentBody
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !utf8.ValidString(body.Content) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "content must be valid UTF-8"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(body.Content) > maxChatUploadEditBytes {
|
||||||
|
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "content too large"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
abs, err := h.resolveUnderChatUploads(body.Path)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(abs, []byte(body.Content), 0644); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatUploadShortRand(n int) string {
|
||||||
|
const letters = "0123456789abcdef"
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[int(b[i])%len(letters)]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload POST /api/chat-uploads multipart: file;conversationId 可选;relativeDir 可选(chat_uploads 下目录的相对路径,将文件直接上传至该目录)
|
||||||
|
func (h *ChatUploadsHandler) Upload(c *gin.Context) {
|
||||||
|
fh, err := c.FormFile("file")
|
||||||
|
if err != nil || fh == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root, err := h.absRoot()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetDir string
|
||||||
|
targetRel := strings.TrimSpace(c.PostForm("relativeDir"))
|
||||||
|
if targetRel != "" {
|
||||||
|
absDir, err := h.resolveUnderChatUploads(targetRel)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st, err := os.Stat(absDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(absDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if !st.IsDir() {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "relativeDir is not a directory"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetDir = absDir
|
||||||
|
} else {
|
||||||
|
convID := strings.TrimSpace(c.PostForm("conversationId"))
|
||||||
|
convDir := convID
|
||||||
|
if convDir == "" {
|
||||||
|
convDir = "_manual"
|
||||||
|
} else {
|
||||||
|
convDir = strings.ReplaceAll(convDir, string(filepath.Separator), "_")
|
||||||
|
}
|
||||||
|
dateStr := time.Now().Format("2006-01-02")
|
||||||
|
targetDir = filepath.Join(root, dateStr, convDir)
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseName := filepath.Base(fh.Filename)
|
||||||
|
if baseName == "" || baseName == "." {
|
||||||
|
baseName = "file"
|
||||||
|
}
|
||||||
|
baseName = strings.ReplaceAll(baseName, string(filepath.Separator), "_")
|
||||||
|
ext := filepath.Ext(baseName)
|
||||||
|
nameNoExt := strings.TrimSuffix(baseName, ext)
|
||||||
|
suffix := fmt.Sprintf("_%s_%s", time.Now().Format("150405"), chatUploadShortRand(6))
|
||||||
|
var unique string
|
||||||
|
if ext != "" {
|
||||||
|
unique = nameNoExt + suffix + ext
|
||||||
|
} else {
|
||||||
|
unique = baseName + suffix
|
||||||
|
}
|
||||||
|
fullPath := filepath.Join(targetDir, unique)
|
||||||
|
src, err := fh.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
dst, err := os.Create(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
if _, err := io.Copy(dst, src); err != nil {
|
||||||
|
_ = os.Remove(fullPath)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rel, _ := filepath.Rel(root, fullPath)
|
||||||
|
absSaved, _ := filepath.Abs(fullPath)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"relativePath": filepath.ToSlash(rel),
|
||||||
|
"absolutePath": absSaved,
|
||||||
|
"name": unique,
|
||||||
|
})
|
||||||
|
}
|
||||||
+421
@@ -0,0 +1,421 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# CyberStrikeAI GitHub one-click upgrade script (Release/Tag)
|
||||||
|
#
|
||||||
|
# Default preserves:
|
||||||
|
# - config.yaml
|
||||||
|
# - data/
|
||||||
|
# - venv/ (disabled with --no-venv)
|
||||||
|
#
|
||||||
|
# Optional preserves (may overwrite upstream updates):
|
||||||
|
# - roles/
|
||||||
|
# - skills/
|
||||||
|
# - tools/
|
||||||
|
# Enable with --preserve-custom
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
BINARY_NAME="cyberstrike-ai"
|
||||||
|
CONFIG_FILE="$ROOT_DIR/config.yaml"
|
||||||
|
DATA_DIR="$ROOT_DIR/data"
|
||||||
|
VENV_DIR="$ROOT_DIR/venv"
|
||||||
|
KNOWLEDGE_BASE_DIR="$ROOT_DIR/knowledge_base"
|
||||||
|
|
||||||
|
BACKUP_BASE_DIR="$ROOT_DIR/.upgrade-backup"
|
||||||
|
|
||||||
|
GITHUB_REPO="Ed1s0nZ/CyberStrikeAI"
|
||||||
|
|
||||||
|
TAG=""
|
||||||
|
PRESERVE_CUSTOM=0
|
||||||
|
PRESERVE_VENV=1
|
||||||
|
STOP_SERVICE=1
|
||||||
|
FORCE_STOP=0
|
||||||
|
YES=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage:
|
||||||
|
./upgrade.sh [--tag vX.Y.Z] [--preserve-custom] [--no-venv] [--no-stop]
|
||||||
|
[--force-stop] [--yes]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--tag <tag> Specify GitHub Release tag (e.g. v1.3.28).
|
||||||
|
If omitted, the script uses the latest release.
|
||||||
|
--preserve-custom Preserve roles/skills/tools (may overwrite upstream files).
|
||||||
|
Use with caution.
|
||||||
|
--no-venv Do not preserve venv/ (Python deps will be re-installed).
|
||||||
|
--no-stop Do not try to stop the running service.
|
||||||
|
--force-stop If no process matching current directory is found, also stop
|
||||||
|
any cyberstrike-ai processes (use with caution).
|
||||||
|
--yes Do not ask for confirmation.
|
||||||
|
|
||||||
|
Description:
|
||||||
|
The script backs up config.yaml/data/ (and optionally venv/roles/skills/tools) to
|
||||||
|
.upgrade-backup/
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() { printf "%s\n" "$*"; }
|
||||||
|
info() { log "[INFO] $*"; }
|
||||||
|
warn() { log "[WARN] $*"; }
|
||||||
|
err() { log "[ERROR] $*"; }
|
||||||
|
|
||||||
|
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
http_get() {
|
||||||
|
# $1: url
|
||||||
|
if have_cmd curl; then
|
||||||
|
# If GITHUB_TOKEN is provided, use it for api.github.com to avoid low rate limits.
|
||||||
|
if [[ -n "${GITHUB_TOKEN:-}" && "$1" == https://api.github.com/* ]]; then
|
||||||
|
# Do not use `-f` so we can parse GitHub error JSON bodies and show `message`.
|
||||||
|
curl -sSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$1"
|
||||||
|
else
|
||||||
|
# Do not use `-f` so we can parse GitHub error JSON bodies and show `message`.
|
||||||
|
curl -sSL "$1"
|
||||||
|
fi
|
||||||
|
elif have_cmd wget; then
|
||||||
|
wget -qO- "$1"
|
||||||
|
else
|
||||||
|
err "curl or wget is required to download GitHub releases. Please install one of them."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
# Try to stop the service that is running from the current project directory.
|
||||||
|
# If nothing is found and --force-stop is enabled, stop all cyberstrike-ai processes.
|
||||||
|
if [[ "$STOP_SERVICE" -ne 1 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pids=""
|
||||||
|
if have_cmd pgrep; then
|
||||||
|
# Prefer matches where the command line contains the current project path.
|
||||||
|
pids="$(pgrep -f "${ROOT_DIR}.*${BINARY_NAME}" || true)"
|
||||||
|
if [[ -z "$pids" && "$FORCE_STOP" -eq 1 ]]; then
|
||||||
|
warn "No ${BINARY_NAME} process found under the current directory. Will try to force-stop all matching ${BINARY_NAME} processes."
|
||||||
|
pids="$(pgrep -f "${BINARY_NAME}" || true)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$pids" ]]; then
|
||||||
|
info "No ${BINARY_NAME} process detected (or no matching process). Skipping stop step."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "Detected running PID(s): ${pids}"
|
||||||
|
for pid in $pids; do
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
info "Sending SIGTERM to PID=${pid}..."
|
||||||
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for exit
|
||||||
|
local deadline=$((SECONDS + 20))
|
||||||
|
while [[ $SECONDS -lt $deadline ]]; do
|
||||||
|
local alive=0
|
||||||
|
for pid in $pids; do
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
alive=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$alive" -eq 0 ]]; then
|
||||||
|
info "Service stopped."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
warn "Timed out waiting for processes to exit. Still running PID(s): ${pids} (may still hold file handles)."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_dir_tgz() {
|
||||||
|
# $1: label, $2: path
|
||||||
|
local label="$1"
|
||||||
|
local path="$2"
|
||||||
|
if [[ -e "$path" ]]; then
|
||||||
|
info "Backing up ${label} -> ${BACKUP_BASE_DIR}/$(basename "$path").tgz"
|
||||||
|
tar -czf "${BACKUP_BASE_DIR}/$(basename "$path").tgz" -C "$ROOT_DIR" "$(basename "$path")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_config() {
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
cp -a "$CONFIG_FILE" "${BACKUP_BASE_DIR}/config.yaml"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_git_style_env() {
|
||||||
|
# No hard requirement; just a sanity check.
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
err "Could not find ${CONFIG_FILE}. Please verify you are in the correct project directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_or_exit() {
|
||||||
|
if [[ "$YES" -eq 1 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -t 0 ]]; then
|
||||||
|
err "Non-interactive terminal detected. Please add --yes to continue."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "About to perform upgrade:"
|
||||||
|
info " - Preserve config.yaml: yes"
|
||||||
|
info " - Preserve data/: yes"
|
||||||
|
if [[ "$PRESERVE_VENV" -eq 1 ]]; then
|
||||||
|
info " - Preserve venv/: yes"
|
||||||
|
else
|
||||||
|
info " - Preserve venv/: no (will remove old venv and re-install deps)"
|
||||||
|
fi
|
||||||
|
if [[ "$PRESERVE_CUSTOM" -eq 1 ]]; then
|
||||||
|
info " - Preserve roles/skills/tools: yes (may overwrite upstream updates)"
|
||||||
|
else
|
||||||
|
info " - Preserve roles/skills/tools: no (will use upstream versions)"
|
||||||
|
fi
|
||||||
|
info " - Stop service: ${STOP_SERVICE}"
|
||||||
|
echo ""
|
||||||
|
read -r -p "Continue? (y/N) " ans
|
||||||
|
if [[ "${ans:-N}" != "y" && "${ans:-N}" != "Y" ]]; then
|
||||||
|
err "Cancelled."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_tag() {
|
||||||
|
if [[ -n "$TAG" ]]; then
|
||||||
|
info "Using specified tag: $TAG"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
|
||||||
|
info "Fetching latest Release..."
|
||||||
|
local json
|
||||||
|
json="$(http_get "$api_url")"
|
||||||
|
TAG="$(printf '%s' "$json" | python3 - <<'PY'
|
||||||
|
import json, sys
|
||||||
|
data=json.loads(sys.stdin.read() or "{}")
|
||||||
|
print(data.get("tag_name",""))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -z "$TAG" ]]; then
|
||||||
|
local msg
|
||||||
|
msg="$(printf '%s' "$json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read() or '{}'); print(d.get('message',''))" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
# Fallback: try query releases list (sometimes latest endpoint returns error JSON without tag_name).
|
||||||
|
local fallback_url="https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=1"
|
||||||
|
info "Fallback to: ${fallback_url}"
|
||||||
|
local fallback_json
|
||||||
|
fallback_json="$(http_get "$fallback_url" 2>/dev/null || true)"
|
||||||
|
local fallback_tag
|
||||||
|
fallback_tag="$(printf '%s' "$fallback_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read() or '[]'); print(d[0].get('tag_name','') if isinstance(d,list) and d else '')" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -n "$fallback_tag" ]]; then
|
||||||
|
TAG="$fallback_tag"
|
||||||
|
info "Latest Release tag (fallback): $TAG"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local snippet
|
||||||
|
snippet="$(printf '%s' "$json" | python3 -c "import sys; s=sys.stdin.read(); print(s[:300].replace('\\n',' '))" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -n "$msg" ]]; then
|
||||||
|
err "Failed to fetch latest tag: ${msg}"
|
||||||
|
else
|
||||||
|
err "Failed to fetch latest tag."
|
||||||
|
fi
|
||||||
|
if [[ -n "$snippet" ]]; then
|
||||||
|
err "API response snippet: ${snippet}"
|
||||||
|
fi
|
||||||
|
err "Please try using --tag to specify the version, or set export GITHUB_TOKEN=\"...\"."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
info "Latest Release tag: $TAG"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_config_version() {
|
||||||
|
# Replace config.yaml's version: ... with the specified tag.
|
||||||
|
local new_tag="$1"
|
||||||
|
python3 - "$CONFIG_FILE" "$new_tag" <<PY
|
||||||
|
import re, sys
|
||||||
|
path=sys.argv[1]
|
||||||
|
tag=sys.argv[2]
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
lines=f.readlines()
|
||||||
|
|
||||||
|
out=[]
|
||||||
|
replaced=False
|
||||||
|
for line in lines:
|
||||||
|
if re.match(r'^\s*version\s*:', line):
|
||||||
|
out.append(f'version: "{tag}"\\n')
|
||||||
|
replaced=True
|
||||||
|
else:
|
||||||
|
out.append(line)
|
||||||
|
|
||||||
|
if not replaced:
|
||||||
|
# If no version field is found, insert at the beginning (near the top).
|
||||||
|
out.insert(0, f'version: "{tag}"\\n')
|
||||||
|
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.writelines(out)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_code() {
|
||||||
|
local tmp_dir="$1"
|
||||||
|
local new_src_dir="$2"
|
||||||
|
|
||||||
|
# rsync sync: overwrite files from the new version and delete removed files.
|
||||||
|
# Preserve user data/config (and optional directories).
|
||||||
|
|
||||||
|
if ! have_cmd rsync; then
|
||||||
|
err "rsync not found. This script depends on rsync for safe synchronization. Please install it and retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a rsync_excludes
|
||||||
|
rsync_excludes+=( "--exclude=.upgrade-backup/" )
|
||||||
|
rsync_excludes+=( "--exclude=config.yaml" )
|
||||||
|
rsync_excludes+=( "--exclude=data/" )
|
||||||
|
|
||||||
|
if [[ "$PRESERVE_VENV" -eq 1 ]]; then
|
||||||
|
rsync_excludes+=( "--exclude=venv/" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# knowledge_base may not be referenced in config, but many users treat it as the knowledge files directory.
|
||||||
|
if [[ -d "$KNOWLEDGE_BASE_DIR" ]]; then
|
||||||
|
rsync_excludes+=( "--exclude=knowledge_base/" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PRESERVE_CUSTOM" -eq 1 ]]; then
|
||||||
|
rsync_excludes+=( "--exclude=roles/" )
|
||||||
|
rsync_excludes+=( "--exclude=skills/" )
|
||||||
|
rsync_excludes+=( "--exclude=tools/" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure this upgrade script itself is not deleted.
|
||||||
|
rsync_excludes+=( "--exclude=upgrade.sh" )
|
||||||
|
|
||||||
|
# shellcheck disable=SC2068
|
||||||
|
info "Syncing code into current directory (preserving data/config; using rsync --delete)..."
|
||||||
|
rsync -a --delete \
|
||||||
|
${rsync_excludes[@]} \
|
||||||
|
"${new_src_dir}/" "${ROOT_DIR}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
ensure_git_style_env
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--tag)
|
||||||
|
TAG="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--preserve-custom)
|
||||||
|
PRESERVE_CUSTOM=1
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
--no-venv)
|
||||||
|
PRESERVE_VENV=0
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
--no-stop)
|
||||||
|
STOP_SERVICE=0
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
--force-stop)
|
||||||
|
FORCE_STOP=1
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
--yes)
|
||||||
|
YES=1
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
err "Unknown parameter: $1"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
confirm_or_exit
|
||||||
|
|
||||||
|
stop_service
|
||||||
|
|
||||||
|
resolve_tag
|
||||||
|
|
||||||
|
local ts
|
||||||
|
ts="$(date +"%Y%m%d_%H%M%S")"
|
||||||
|
BACKUP_BASE_DIR="${BACKUP_BASE_DIR}/${ts}"
|
||||||
|
mkdir -p "$BACKUP_BASE_DIR"
|
||||||
|
|
||||||
|
info "Starting backup into: $BACKUP_BASE_DIR"
|
||||||
|
backup_config
|
||||||
|
backup_dir_tgz "data" "$DATA_DIR"
|
||||||
|
if [[ "$PRESERVE_VENV" -eq 1 ]]; then
|
||||||
|
backup_dir_tgz "venv" "$VENV_DIR"
|
||||||
|
else
|
||||||
|
if [[ -d "$VENV_DIR" ]]; then
|
||||||
|
warn "With --no-venv: removing old venv/ (run.sh will re-install Python deps after upgrade)."
|
||||||
|
rm -rf "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -d "$KNOWLEDGE_BASE_DIR" ]]; then
|
||||||
|
backup_dir_tgz "knowledge_base" "$KNOWLEDGE_BASE_DIR"
|
||||||
|
fi
|
||||||
|
if [[ "$PRESERVE_CUSTOM" -eq 1 ]]; then
|
||||||
|
backup_dir_tgz "roles" "$ROOT_DIR/roles"
|
||||||
|
backup_dir_tgz "skills" "$ROOT_DIR/skills"
|
||||||
|
backup_dir_tgz "tools" "$ROOT_DIR/tools"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_dir
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp_dir" >/dev/null 2>&1 || true' EXIT
|
||||||
|
|
||||||
|
local tarball="${tmp_dir}/source.tar.gz"
|
||||||
|
local url="https://github.com/${GITHUB_REPO}/archive/refs/tags/${TAG}.tar.gz"
|
||||||
|
info "Downloading source package: ${url}"
|
||||||
|
http_get "$url" >"$tarball"
|
||||||
|
|
||||||
|
info "Extracting source package..."
|
||||||
|
tar -xzf "$tarball" -C "$tmp_dir"
|
||||||
|
|
||||||
|
# GitHub tarball usually creates a top-level directory.
|
||||||
|
local extracted_dir
|
||||||
|
extracted_dir="$(ls -d "${tmp_dir}"/*/ 2>/dev/null | head -n 1 || true)"
|
||||||
|
if [[ -z "$extracted_dir" || ! -f "${extracted_dir}/run.sh" ]]; then
|
||||||
|
err "run.sh not found in the extracted directory. Please check network/download contents."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sync_code "$tmp_dir" "$extracted_dir"
|
||||||
|
|
||||||
|
# Update config.yaml version display
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
info "Updating config.yaml version field to: $TAG"
|
||||||
|
update_config_version "$TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Upgrade complete. Starting service..."
|
||||||
|
chmod +x ./run.sh
|
||||||
|
./run.sh
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
||||||
@@ -3447,6 +3447,27 @@ header {
|
|||||||
|
|
||||||
.terminal-container .xterm-viewport {
|
.terminal-container .xterm-viewport {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
/* 与 WebShell 终端一致:细窄、深色,避免系统默认浅色粗滚动条 */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(110, 118, 129, 0.5) transparent;
|
||||||
|
}
|
||||||
|
.terminal-container .xterm-viewport::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.terminal-container .xterm-viewport::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(110, 118, 129, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(110, 118, 129, 0.65);
|
||||||
|
}
|
||||||
|
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb:active {
|
||||||
|
background: rgba(139, 148, 158, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-error {
|
.terminal-error {
|
||||||
@@ -12860,3 +12881,477 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 对话附件文件管理 */
|
||||||
|
.chat-files-intro {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-filters {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分组视图:外层不再套一层大边框,由各分组卡片承担 */
|
||||||
|
.chat-files-table-wrap.chat-files-table-wrap--grouped {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GitHub 式:单表 + 首列缩进,无嵌套子表、无重复表头 */
|
||||||
|
.chat-files-table-wrap.chat-files-table-wrap--tree {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-browse-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-browse-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary, #f8f9fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px 2px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-breadcrumb-link {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--accent-color);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-breadcrumb-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-breadcrumb-sep {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-breadcrumb-current {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-browse-up {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-browse-up:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tr-folder--nav {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tr-folder--nav:hover {
|
||||||
|
background: rgba(0, 102, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-folder-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 24px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table--tree-flat {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table--tree-flat thead th {
|
||||||
|
background: var(--bg-secondary, #f8f9fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table--tree-flat .chat-files-tr-folder {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table--tree-flat .chat-files-tr-folder .chat-files-tree-name-cell--folder {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table--tree-flat .chat-files-tr-file:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tree-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--accent-color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tree-icon path {
|
||||||
|
fill: var(--bg-primary);
|
||||||
|
stroke: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tree-file-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tree-name-cell {
|
||||||
|
max-width: min(100%, 560px);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tree-name-inner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tree-name-text {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-tree-muted {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-path-breadcrumb {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px 2px;
|
||||||
|
line-height: 1.45;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-path-sep {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-path-crumb {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-path-root {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-grouped {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group > summary.chat-files-group-summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--bg-secondary, #f8f9fa);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group > summary.chat-files-group-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group > summary.chat-files-group-summary::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 6px solid var(--text-secondary);
|
||||||
|
margin-right: 2px;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group[open] > summary.chat-files-group-summary::before {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group-count {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group-body {
|
||||||
|
overflow-x: auto;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group-body .chat-files-table {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group-body .chat-files-table th {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table th,
|
||||||
|
.chat-files-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-secondary, #f8f9fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-cell-name {
|
||||||
|
max-width: 280px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-cell-conv code {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
max-width: 160px;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-cell-subpath {
|
||||||
|
max-width: 280px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-group-title--folder {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-action-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-action-bar .btn-icon {
|
||||||
|
min-width: 34px;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-dropdown-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
min-width: 220px;
|
||||||
|
padding: 8px 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JS 使用 fixed 定位时覆盖 absolute,避免被表格区域 overflow 裁切 */
|
||||||
|
.chat-files-dropdown.chat-files-dropdown-fixed {
|
||||||
|
position: fixed;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-dropdown-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-dropdown-item.is-danger {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-dropdown-item.is-danger:hover:not(:disabled) {
|
||||||
|
background: rgba(220, 53, 69, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-dropdown-item.is-disabled {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: not-allowed;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-no-edit {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
cursor: help;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-modal-path {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-edit-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 240px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-rename-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-toast {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1100;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(12px);
|
||||||
|
max-width: min(520px, calc(100vw - 32px));
|
||||||
|
padding: 12px 18px;
|
||||||
|
background: var(--text-primary, #1a1a1a);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-files-toast.chat-files-toast-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+68
-10
@@ -46,17 +46,18 @@
|
|||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"vulnerabilities": "Vulnerabilities",
|
"vulnerabilities": "Vulnerabilities",
|
||||||
"webshell": "WebShell Management",
|
"webshell": "WebShell Management",
|
||||||
|
"chatFiles": "File Management",
|
||||||
"mcp": "MCP",
|
"mcp": "MCP",
|
||||||
"mcpMonitor": "MCP Monitor",
|
"mcpMonitor": "MCP Monitor",
|
||||||
"mcpManagement": "MCP Management",
|
"mcpManagement": "MCP Management",
|
||||||
"knowledge": "Knowledge",
|
"knowledge": "Knowledge",
|
||||||
"knowledgeRetrievalLogs": "Retrieval history",
|
"knowledgeRetrievalLogs": "Retrieval history",
|
||||||
"knowledgeManagement": "Knowledge management",
|
"knowledgeManagement": "Knowledge Management",
|
||||||
"skills": "Skills",
|
"skills": "Skills",
|
||||||
"skillsMonitor": "Skills monitor",
|
"skillsMonitor": "Skills monitor",
|
||||||
"skillsManagement": "Skills management",
|
"skillsManagement": "Skills Management",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
"rolesManagement": "Roles management",
|
"rolesManagement": "Roles Management",
|
||||||
"settings": "System settings"
|
"settings": "System settings"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
@@ -186,7 +187,7 @@
|
|||||||
"execFailed": "Execution failed"
|
"execFailed": "Execution failed"
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"title": "Task management",
|
"title": "Task Management",
|
||||||
"stopTask": "Stop task",
|
"stopTask": "Stop task",
|
||||||
"collapseDetail": "Collapse details",
|
"collapseDetail": "Collapse details",
|
||||||
"newTask": "New task",
|
"newTask": "New task",
|
||||||
@@ -324,7 +325,7 @@
|
|||||||
"parseModalApplyRun": "Fill and query"
|
"parseModalApplyRun": "Fill and query"
|
||||||
},
|
},
|
||||||
"vulnerability": {
|
"vulnerability": {
|
||||||
"title": "Vulnerability management",
|
"title": "Vulnerability Management",
|
||||||
"addVuln": "Add vulnerability",
|
"addVuln": "Add vulnerability",
|
||||||
"editVuln": "Edit vulnerability",
|
"editVuln": "Edit vulnerability",
|
||||||
"loadFailed": "Failed to load vulnerabilities",
|
"loadFailed": "Failed to load vulnerabilities",
|
||||||
@@ -488,10 +489,14 @@
|
|||||||
"title": "System settings",
|
"title": "System settings",
|
||||||
"nav": {
|
"nav": {
|
||||||
"basic": "Basic",
|
"basic": "Basic",
|
||||||
|
"knowledge": "Knowledge base",
|
||||||
"robots": "Bots",
|
"robots": "Bots",
|
||||||
"terminal": "Terminal",
|
"terminal": "Terminal",
|
||||||
"security": "Security"
|
"security": "Security"
|
||||||
},
|
},
|
||||||
|
"knowledge": {
|
||||||
|
"title": "Knowledge base"
|
||||||
|
},
|
||||||
"robots": {
|
"robots": {
|
||||||
"title": "Bot settings",
|
"title": "Bot settings",
|
||||||
"description": "Configure WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.",
|
"description": "Configure WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.",
|
||||||
@@ -552,7 +557,7 @@
|
|||||||
"loggedOut": "Signed out"
|
"loggedOut": "Signed out"
|
||||||
},
|
},
|
||||||
"knowledge": {
|
"knowledge": {
|
||||||
"title": "Knowledge management",
|
"title": "Knowledge Management",
|
||||||
"retrievalLogs": "Retrieval history",
|
"retrievalLogs": "Retrieval history",
|
||||||
"totalItems": "Total items",
|
"totalItems": "Total items",
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
@@ -565,7 +570,7 @@
|
|||||||
"goToSettings": "Go to settings"
|
"goToSettings": "Go to settings"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"title": "Role management",
|
"title": "Role Management",
|
||||||
"createRole": "Create role",
|
"createRole": "Create role",
|
||||||
"searchPlaceholder": "Search roles...",
|
"searchPlaceholder": "Search roles...",
|
||||||
"deleteConfirm": "Delete this role?",
|
"deleteConfirm": "Delete this role?",
|
||||||
@@ -579,7 +584,7 @@
|
|||||||
"noDescriptionShort": "No description"
|
"noDescriptionShort": "No description"
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
"title": "Skills management",
|
"title": "Skills Management",
|
||||||
"monitorTitle": "Skills monitor",
|
"monitorTitle": "Skills monitor",
|
||||||
"createSkill": "Create Skill",
|
"createSkill": "Create Skill",
|
||||||
"callStats": "Call stats",
|
"callStats": "Call stats",
|
||||||
@@ -994,6 +999,59 @@
|
|||||||
"exportXlsxTitle": "Export results as Excel",
|
"exportXlsxTitle": "Export results as Excel",
|
||||||
"batchScanTitle": "Create batch task queue from selected rows"
|
"batchScanTitle": "Create batch task queue from selected rows"
|
||||||
},
|
},
|
||||||
|
"chatFilesPage": {
|
||||||
|
"title": "File Management",
|
||||||
|
"intro": "Files uploaded in chat appear here. Click “Copy path” to copy the server absolute path and paste it into a conversation so the model can reference the file.",
|
||||||
|
"upload": "Upload",
|
||||||
|
"conversationFilter": "Conversation ID",
|
||||||
|
"conversationPlaceholder": "Leave empty for all",
|
||||||
|
"searchName": "File name",
|
||||||
|
"searchNamePlaceholder": "Filter by file name",
|
||||||
|
"groupBy": "Group by",
|
||||||
|
"groupNone": "None (flat list)",
|
||||||
|
"groupByDate": "By date",
|
||||||
|
"groupByConversation": "By conversation",
|
||||||
|
"groupByFolder": "By folder (path navigation)",
|
||||||
|
"browseRoot": "chat_uploads",
|
||||||
|
"browseUp": "Up",
|
||||||
|
"enterFolderTitle": "Open folder",
|
||||||
|
"copyFolderPathTitle": "Copy relative path under chat_uploads/…",
|
||||||
|
"folderPathCopied": "Folder path copied — paste into chat if needed",
|
||||||
|
"folderEmpty": "This folder is empty",
|
||||||
|
"confirmDeleteFolder": "Delete this folder and everything inside it? This cannot be undone.",
|
||||||
|
"deleteFolderTitle": "Delete folder",
|
||||||
|
"uploadToFolderTitle": "Upload file into this folder",
|
||||||
|
"colSubPath": "Subfolder",
|
||||||
|
"folderRoot": "(root)",
|
||||||
|
"groupCount": "{{count}} files",
|
||||||
|
"convManual": "Manual upload",
|
||||||
|
"convNew": "New chat",
|
||||||
|
"colDate": "Date",
|
||||||
|
"colConversation": "Conversation",
|
||||||
|
"colName": "Name",
|
||||||
|
"colSize": "Size",
|
||||||
|
"colModified": "Modified",
|
||||||
|
"colActions": "Actions",
|
||||||
|
"copyPath": "Copy path",
|
||||||
|
"copyPathTitle": "Copy the absolute path on the server; paste into chat to reference this file",
|
||||||
|
"pathCopied": "Path copied — paste it into chat",
|
||||||
|
"uploadOkHint": "Uploaded. Use “Copy path” to copy the absolute path.",
|
||||||
|
"moreActions": "More: open chat, edit, rename, delete",
|
||||||
|
"download": "Download",
|
||||||
|
"edit": "Edit",
|
||||||
|
"rename": "Rename",
|
||||||
|
"openChat": "Open chat",
|
||||||
|
"confirmDelete": "Delete this file?",
|
||||||
|
"editTitle": "Edit file",
|
||||||
|
"renameTitle": "Rename",
|
||||||
|
"newFileName": "New file name",
|
||||||
|
"empty": "No chat uploads yet",
|
||||||
|
"errorLoad": "Failed to load",
|
||||||
|
"editBinaryHint": "Binary files (images, archives, etc.) cannot be edited as text here. Use Download and open locally.",
|
||||||
|
"editUnavailable": "N/A",
|
||||||
|
"editTooLarge": "File exceeds 2MB and cannot be edited here. Download and edit locally.",
|
||||||
|
"errorGeneric": "Something went wrong. Please try again."
|
||||||
|
},
|
||||||
"vulnerabilityPage": {
|
"vulnerabilityPage": {
|
||||||
"statTotal": "Total",
|
"statTotal": "Total",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
@@ -1361,10 +1419,10 @@
|
|||||||
"userPromptHint": "This prompt is appended before user message to guide AI. It does not change system prompt.",
|
"userPromptHint": "This prompt is appended before user message to guide AI. It does not change system prompt.",
|
||||||
"relatedTools": "Related tools (optional)",
|
"relatedTools": "Related tools (optional)",
|
||||||
"defaultRoleToolsTitle": "Default role uses all tools",
|
"defaultRoleToolsTitle": "Default role uses all tools",
|
||||||
"defaultRoleToolsDesc": "Default role uses all tools enabled in MCP management.",
|
"defaultRoleToolsDesc": "Default role uses all tools enabled in MCP Management.",
|
||||||
"searchToolsPlaceholder": "Search tools...",
|
"searchToolsPlaceholder": "Search tools...",
|
||||||
"loadingTools": "Loading tools...",
|
"loadingTools": "Loading tools...",
|
||||||
"relatedToolsHint": "Select tools to link; empty = use all from MCP management.",
|
"relatedToolsHint": "Select tools to link; empty = use all from MCP Management.",
|
||||||
"relatedSkills": "Related Skills (optional)",
|
"relatedSkills": "Related Skills (optional)",
|
||||||
"searchSkillsPlaceholder": "Search skill...",
|
"searchSkillsPlaceholder": "Search skill...",
|
||||||
"loadingSkills": "Loading skills...",
|
"loadingSkills": "Loading skills...",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"tasks": "任务管理",
|
"tasks": "任务管理",
|
||||||
"vulnerabilities": "漏洞管理",
|
"vulnerabilities": "漏洞管理",
|
||||||
"webshell": "WebShell管理",
|
"webshell": "WebShell管理",
|
||||||
|
"chatFiles": "文件管理",
|
||||||
"mcp": "MCP",
|
"mcp": "MCP",
|
||||||
"mcpMonitor": "MCP状态监控",
|
"mcpMonitor": "MCP状态监控",
|
||||||
"mcpManagement": "MCP管理",
|
"mcpManagement": "MCP管理",
|
||||||
@@ -488,10 +489,14 @@
|
|||||||
"title": "系统设置",
|
"title": "系统设置",
|
||||||
"nav": {
|
"nav": {
|
||||||
"basic": "基本设置",
|
"basic": "基本设置",
|
||||||
|
"knowledge": "知识库",
|
||||||
"robots": "机器人设置",
|
"robots": "机器人设置",
|
||||||
"terminal": "终端",
|
"terminal": "终端",
|
||||||
"security": "安全设置"
|
"security": "安全设置"
|
||||||
},
|
},
|
||||||
|
"knowledge": {
|
||||||
|
"title": "知识库设置"
|
||||||
|
},
|
||||||
"robots": {
|
"robots": {
|
||||||
"title": "机器人设置",
|
"title": "机器人设置",
|
||||||
"description": "配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。",
|
"description": "配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。",
|
||||||
@@ -994,6 +999,59 @@
|
|||||||
"exportXlsxTitle": "导出当前结果为 Excel",
|
"exportXlsxTitle": "导出当前结果为 Excel",
|
||||||
"batchScanTitle": "将所选行创建为批量任务队列"
|
"batchScanTitle": "将所选行创建为批量任务队列"
|
||||||
},
|
},
|
||||||
|
"chatFilesPage": {
|
||||||
|
"title": "文件管理",
|
||||||
|
"intro": "管理在对话中上传的文件。需要让 AI 引用某文件时,在列表中点击「复制路径」,到对话里粘贴即可(路径为服务器上的绝对路径,与对话附件保存位置一致)。",
|
||||||
|
"upload": "上传文件",
|
||||||
|
"conversationFilter": "会话 ID",
|
||||||
|
"conversationPlaceholder": "留空表示全部",
|
||||||
|
"searchName": "文件名",
|
||||||
|
"searchNamePlaceholder": "筛选文件名",
|
||||||
|
"groupBy": "分组方式",
|
||||||
|
"groupNone": "不分组(平铺)",
|
||||||
|
"groupByDate": "按日期",
|
||||||
|
"groupByConversation": "按会话",
|
||||||
|
"groupByFolder": "按文件夹(路径浏览)",
|
||||||
|
"browseRoot": "chat_uploads",
|
||||||
|
"browseUp": "上级",
|
||||||
|
"enterFolderTitle": "进入此文件夹",
|
||||||
|
"copyFolderPathTitle": "复制该目录的相对路径(chat_uploads/…)",
|
||||||
|
"folderPathCopied": "目录路径已复制,可粘贴到对话中",
|
||||||
|
"folderEmpty": "此文件夹为空",
|
||||||
|
"confirmDeleteFolder": "确定删除该文件夹及其中的全部文件?此操作不可恢复。",
|
||||||
|
"deleteFolderTitle": "删除文件夹",
|
||||||
|
"uploadToFolderTitle": "上传文件到此文件夹",
|
||||||
|
"colSubPath": "子路径",
|
||||||
|
"folderRoot": "(根目录)",
|
||||||
|
"groupCount": "{{count}} 个文件",
|
||||||
|
"convManual": "手动上传",
|
||||||
|
"convNew": "新对话",
|
||||||
|
"colDate": "日期",
|
||||||
|
"colConversation": "会话",
|
||||||
|
"colName": "文件名",
|
||||||
|
"colSize": "大小",
|
||||||
|
"colModified": "修改时间",
|
||||||
|
"colActions": "操作",
|
||||||
|
"copyPath": "复制路径",
|
||||||
|
"copyPathTitle": "复制服务器上的绝对路径,可粘贴到对话中让模型引用该文件",
|
||||||
|
"pathCopied": "路径已复制,可到对话中粘贴使用",
|
||||||
|
"uploadOkHint": "上传成功。点击「复制路径」可复制绝对路径到剪贴板。",
|
||||||
|
"moreActions": "更多:打开对话、编辑、重命名、删除",
|
||||||
|
"download": "下载",
|
||||||
|
"edit": "编辑",
|
||||||
|
"rename": "重命名",
|
||||||
|
"openChat": "打开对话",
|
||||||
|
"confirmDelete": "确定删除该文件?",
|
||||||
|
"editTitle": "编辑文件",
|
||||||
|
"renameTitle": "重命名",
|
||||||
|
"newFileName": "新文件名",
|
||||||
|
"empty": "暂无对话附件",
|
||||||
|
"errorLoad": "加载失败",
|
||||||
|
"editBinaryHint": "图片、压缩包等二进制文件无法在此以文本方式编辑,请使用「下载」后在本地查看或处理。",
|
||||||
|
"editUnavailable": "不可编辑",
|
||||||
|
"editTooLarge": "文件超过 2MB,无法在此编辑,请下载后本地处理。",
|
||||||
|
"errorGeneric": "操作失败,请稍后重试。"
|
||||||
|
},
|
||||||
"vulnerabilityPage": {
|
"vulnerabilityPage": {
|
||||||
"statTotal": "总漏洞数",
|
"statTotal": "总漏洞数",
|
||||||
"filter": "筛选",
|
"filter": "筛选",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ function initRouter() {
|
|||||||
if (hash) {
|
if (hash) {
|
||||||
const hashParts = hash.split('?');
|
const hashParts = hash.split('?');
|
||||||
const pageId = hashParts[0];
|
const pageId = hashParts[0];
|
||||||
if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'webshell', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
|
if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
|
||||||
switchPage(pageId);
|
switchPage(pageId);
|
||||||
|
|
||||||
// 如果是chat页面且带有conversation参数,加载对应对话
|
// 如果是chat页面且带有conversation参数,加载对应对话
|
||||||
@@ -299,6 +299,11 @@ function initPage(pageId) {
|
|||||||
initWebshellPage();
|
initWebshellPage();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'chat-files':
|
||||||
|
if (typeof initChatFilesPage === 'function') {
|
||||||
|
initChatFilesPage();
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
// 初始化设置页面(不需要加载工具列表)
|
// 初始化设置页面(不需要加载工具列表)
|
||||||
if (typeof loadConfig === 'function') {
|
if (typeof loadConfig === 'function') {
|
||||||
@@ -368,7 +373,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const hashParts = hash.split('?');
|
const hashParts = hash.split('?');
|
||||||
const pageId = hashParts[0];
|
const pageId = hashParts[0];
|
||||||
|
|
||||||
if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'webshell', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
|
if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
|
||||||
switchPage(pageId);
|
switchPage(pageId);
|
||||||
|
|
||||||
// 如果是chat页面且带有conversation参数,加载对应对话
|
// 如果是chat页面且带有conversation参数,加载对应对话
|
||||||
|
|||||||
+103
-5
@@ -48,8 +48,8 @@
|
|||||||
<span data-i18n="header.apiDocs">API 文档</span>
|
<span data-i18n="header.apiDocs">API 文档</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="openapi-doc-btn" onclick="window.open('https://github.com/Ed1s0nZ/CyberStrikeAI', '_blank')" data-i18n="header.github" data-i18n-attr="title" data-i18n-skip-text="true" title="GitHub">
|
<button class="openapi-doc-btn" onclick="window.open('https://github.com/Ed1s0nZ/CyberStrikeAI', '_blank')" data-i18n="header.github" data-i18n-attr="title" data-i18n-skip-text="true" title="GitHub">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 98 96" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill="currentColor" d="M12 2C6.48 2 2 6.58 2 12.26c0 4.55 2.87 8.4 6.84 9.77.5.1.68-.22.68-.48 0-.24-.01-.88-.01-1.73-2.78.61-3.37-1.35-3.37-1.35-.45-1.17-1.11-1.48-1.11-1.48-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.9 1.55 2.36 1.1 2.94.84.09-.65.35-1.1.63-1.35-2.22-.26-4.56-1.13-4.56-5.04 0-1.11.39-2.01 1.03-2.72-.1-.26-.45-1.3.1-2.7 0 0 .84-.27 2.75 1.04.8-.23 1.65-.35 2.5-.35.85 0 1.7.12 2.5.35 1.9-1.31 2.74-1.04 2.74-1.04.56 1.4.2 2.44.1 2.7.64.71 1.03 1.61 1.03 2.72 0 3.92-2.34 4.78-4.57 5.03.36.32.68.94.68 1.9 0 1.38-.01 2.5-.01 2.84 0 .26.18.58.69.48 3.96-1.37 6.83-5.21 6.83-9.77C22 6.58 17.52 2 12 2z"/>
|
<path d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="header.github">GitHub</span>
|
<span data-i18n="header.github">GitHub</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -150,6 +150,14 @@
|
|||||||
<span data-i18n="nav.webshell">WebShell管理</span>
|
<span data-i18n="nav.webshell">WebShell管理</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item" data-page="chat-files">
|
||||||
|
<div class="nav-item-content" data-title="文件管理" onclick="switchPage('chat-files')" data-i18n="nav.chatFiles" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<span data-i18n="nav.chatFiles">文件管理</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="nav-item nav-item-has-submenu" data-page="mcp">
|
<div class="nav-item nav-item-has-submenu" data-page="mcp">
|
||||||
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')" data-i18n="nav.mcp" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')" data-i18n="nav.mcp" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -1000,6 +1008,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 对话附件 / 文件管理 -->
|
||||||
|
<div id="page-chat-files" class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 data-i18n="chatFilesPage.title">文件管理</h2>
|
||||||
|
<div class="page-header-actions">
|
||||||
|
<button type="button" class="btn-primary" onclick="chatFilesOpenUploadPicker()" data-i18n="chatFilesPage.upload">上传文件</button>
|
||||||
|
<input type="file" id="chat-files-upload-input" style="display:none" onchange="onChatFilesUploadPick(event)" />
|
||||||
|
<button class="btn-secondary" onclick="loadChatFilesPage()" data-i18n="common.refresh">刷新</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-content">
|
||||||
|
<p class="chat-files-intro" data-i18n="chatFilesPage.intro">管理在对话中上传的文件。需要让 AI 引用某文件时,在列表中点击「复制路径」,到对话里粘贴即可。</p>
|
||||||
|
<div class="tasks-filters chat-files-filters">
|
||||||
|
<label>
|
||||||
|
<span data-i18n="chatFilesPage.conversationFilter">会话 ID</span>
|
||||||
|
<input type="text" id="chat-files-filter-conv" class="form-control" data-i18n="chatFilesPage.conversationPlaceholder" data-i18n-attr="placeholder" placeholder="留空表示全部" onkeydown="if(event.key==='Enter') loadChatFilesPage()" />
|
||||||
|
</label>
|
||||||
|
<label style="flex:1;min-width:180px;max-width:360px;">
|
||||||
|
<span data-i18n="chatFilesPage.searchName">文件名</span>
|
||||||
|
<input type="text" id="chat-files-filter-name" class="form-control" data-i18n="chatFilesPage.searchNamePlaceholder" data-i18n-attr="placeholder" placeholder="筛选文件名" oninput="chatFilesFilterNameOnInput()" onkeydown="if(event.key==='Enter') loadChatFilesPage()" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="chatFilesPage.groupBy">分组</span>
|
||||||
|
<select id="chat-files-group-by" class="form-control" onchange="chatFilesGroupByChange()">
|
||||||
|
<option value="none" data-i18n="chatFilesPage.groupNone">不分组</option>
|
||||||
|
<option value="date" data-i18n="chatFilesPage.groupByDate">按日期</option>
|
||||||
|
<option value="conversation" data-i18n="chatFilesPage.groupByConversation">按会话</option>
|
||||||
|
<option value="folder" data-i18n="chatFilesPage.groupByFolder">按文件夹</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button class="btn-secondary" type="button" onclick="loadChatFilesPage()" data-i18n="common.search">搜索</button>
|
||||||
|
</div>
|
||||||
|
<div id="chat-files-list-wrap" class="chat-files-table-wrap">
|
||||||
|
<div class="loading-spinner" data-i18n="common.loading">加载中…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 任务管理页面 -->
|
<!-- 任务管理页面 -->
|
||||||
<div id="page-tasks" class="page">
|
<div id="page-tasks" class="page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -1142,6 +1188,9 @@
|
|||||||
<div class="settings-nav-item active" data-section="basic" onclick="switchSettingsSection('basic')">
|
<div class="settings-nav-item active" data-section="basic" onclick="switchSettingsSection('basic')">
|
||||||
<span data-i18n="settings.nav.basic">基本设置</span>
|
<span data-i18n="settings.nav.basic">基本设置</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-nav-item" data-section="knowledge" onclick="switchSettingsSection('knowledge')">
|
||||||
|
<span data-i18n="settings.nav.knowledge">知识库</span>
|
||||||
|
</div>
|
||||||
<div class="settings-nav-item" data-section="robots" onclick="switchSettingsSection('robots')">
|
<div class="settings-nav-item" data-section="robots" onclick="switchSettingsSection('robots')">
|
||||||
<span data-i18n="settings.nav.robots">机器人设置</span>
|
<span data-i18n="settings.nav.robots">机器人设置</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1213,7 +1262,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 知识库配置 -->
|
<div class="settings-actions">
|
||||||
|
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 知识库设置 -->
|
||||||
|
<div id="settings-section-knowledge" class="settings-section-content">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<h3 data-i18n="settings.knowledge.title">知识库设置</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-subsection">
|
<div class="settings-subsection">
|
||||||
<h4 data-i18n="settingsBasic.knowledgeConfig">知识库配置</h4>
|
<h4 data-i18n="settingsBasic.knowledgeConfig">知识库配置</h4>
|
||||||
<div class="settings-form">
|
<div class="settings-form">
|
||||||
@@ -1272,7 +1331,7 @@
|
|||||||
<input type="number" id="knowledge-retrieval-hybrid-weight" min="0" max="1" step="0.1" data-i18n="settingsBasic.hybridPlaceholder" data-i18n-attr="placeholder" placeholder="0.7" />
|
<input type="number" id="knowledge-retrieval-hybrid-weight" min="0" max="1" step="0.1" data-i18n="settingsBasic.hybridPlaceholder" data-i18n-attr="placeholder" placeholder="0.7" />
|
||||||
<small class="form-hint" data-i18n="settingsBasic.hybridHint">向量检索的权重(0-1),1.0表示纯向量检索,0.0表示纯关键词检索</small>
|
<small class="form-hint" data-i18n="settingsBasic.hybridHint">向量检索的权重(0-1),1.0表示纯向量检索,0.0表示纯关键词检索</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="settings-subsection-header">
|
<div class="settings-subsection-header">
|
||||||
<h5 data-i18n="settingsBasic.indexConfig">索引配置</h5>
|
<h5 data-i18n="settingsBasic.indexConfig">索引配置</h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -1310,7 +1369,9 @@
|
|||||||
<label for="knowledge-indexing-retry-delay-ms" data-i18n="settingsBasic.retryDelay">重试间隔(毫秒)</label>
|
<label for="knowledge-indexing-retry-delay-ms" data-i18n="settingsBasic.retryDelay">重试间隔(毫秒)</label>
|
||||||
<input type="number" id="knowledge-indexing-retry-delay-ms" min="0" max="10000" data-i18n="settingsBasic.retryDelayPlaceholder" data-i18n-attr="placeholder" placeholder="1000" />
|
<input type="number" id="knowledge-indexing-retry-delay-ms" min="0" max="10000" data-i18n="settingsBasic.retryDelayPlaceholder" data-i18n-attr="placeholder" placeholder="1000" />
|
||||||
<small class="form-hint" data-i18n="settingsBasic.retryDelayHint">重试间隔毫秒数(默认 1000),每次重试会递增延迟</small>
|
<small class="form-hint" data-i18n="settingsBasic.retryDelayHint">重试间隔毫秒数(默认 1000),每次重试会递增延迟</small>
|
||||||
</div> </div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||||
@@ -1721,6 +1782,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="chat-files-edit-modal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 720px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="chatFilesPage.editTitle">编辑文件</h2>
|
||||||
|
<span class="modal-close" onclick="closeChatFilesEditModal()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="chat-files-modal-path"><code id="chat-files-edit-path"></code></p>
|
||||||
|
<textarea id="chat-files-edit-textarea" class="form-control chat-files-edit-textarea" rows="18"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn-secondary" onclick="closeChatFilesEditModal()" data-i18n="common.cancel">取消</button>
|
||||||
|
<button type="button" class="btn-primary" onclick="saveChatFilesEdit()" data-i18n="common.save">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chat-files-rename-modal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 480px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="chatFilesPage.renameTitle">重命名</h2>
|
||||||
|
<span class="modal-close" onclick="closeChatFilesRenameModal()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<label class="chat-files-rename-label">
|
||||||
|
<span data-i18n="chatFilesPage.newFileName">新文件名</span>
|
||||||
|
<input type="text" id="chat-files-rename-input" class="form-control" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn-secondary" onclick="closeChatFilesRenameModal()" data-i18n="common.cancel">取消</button>
|
||||||
|
<button type="button" class="btn-primary" onclick="submitChatFilesRename()" data-i18n="common.ok">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Marked.js for Markdown parsing -->
|
<!-- Marked.js for Markdown parsing -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||||
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
||||||
@@ -2328,6 +2425,7 @@ version: 1.0.0<br>
|
|||||||
<script src="/static/js/skills.js"></script>
|
<script src="/static/js/skills.js"></script>
|
||||||
<script src="/static/js/vulnerability.js?v=4"></script>
|
<script src="/static/js/vulnerability.js?v=4"></script>
|
||||||
<script src="/static/js/webshell.js"></script>
|
<script src="/static/js/webshell.js"></script>
|
||||||
|
<script src="/static/js/chat-files.js"></script>
|
||||||
<script src="/static/js/tasks.js"></script>
|
<script src="/static/js/tasks.js"></script>
|
||||||
<script src="/static/js/roles.js"></script>
|
<script src="/static/js/roles.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user