Compare commits

..

10 Commits

Author SHA1 Message Date
公明 b8ebf023a0 Update config.yaml 2026-06-02 12:19:14 +08:00
公明 604ce34d5e Merge pull request #136 from Opr4Mp3r/fix/sse-mcp-session-context
fix(mcp): keep SSE client session alive after connect
2026-06-02 11:37:23 +08:00
opr4 b29b36bfd5 fix(mcp): keep SSE client session alive after connect 2026-06-01 21:36:42 +08:00
公明 11bab83fc5 Update config.yaml 2026-06-01 19:07:09 +08:00
公明 dc750e3680 Add files via upload 2026-06-01 19:06:25 +08:00
公明 0236d1c155 Add files via upload 2026-06-01 19:04:14 +08:00
公明 be59ddcab6 Add files via upload 2026-06-01 17:35:41 +08:00
公明 25464a68e6 Add files via upload 2026-05-31 19:07:26 +08:00
公明 eabfed09c9 Add files via upload 2026-05-31 13:33:32 +08:00
公明 cbcbd414cd Add files via upload 2026-05-29 17:59:19 +08:00
9 changed files with 690 additions and 73 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
# ============================================ # ============================================
# 前端显示的版本号(可选,不填则显示默认版本) # 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.6.27" version: "v1.6.29"
# 服务器配置 # 服务器配置
server: server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口 host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 178 KiB

+203 -18
View File
@@ -40,8 +40,13 @@ const (
robotCmdRoles = "角色" robotCmdRoles = "角色"
robotCmdRolesList = "角色列表" robotCmdRolesList = "角色列表"
robotCmdSwitchRole = "切换角色" robotCmdSwitchRole = "切换角色"
robotCmdDelete = "删除" robotCmdDelete = "删除"
robotCmdVersion = "版本" robotCmdVersion = "版本"
robotCmdProjects = "项目"
robotCmdProjectsList = "项目列表"
robotCmdBindProject = "绑定项目"
robotCmdNewProject = "新建项目"
robotCmdUnbindProject = "解除项目"
) )
// RobotHandler 企业微信/钉钉/飞书等机器人回调处理 // RobotHandler 企业微信/钉钉/飞书等机器人回调处理
@@ -269,21 +274,176 @@ func (h *RobotHandler) robotMessageTimeout() time.Duration {
} }
func (h *RobotHandler) cmdHelp() string { func (h *RobotHandler) cmdHelp() string {
return "**【CyberStrikeAI 机器人命令】**\n\n" + var b strings.Builder
"- `帮助` `help` — 显示本帮助 | Show this help\n" + b.WriteString("【CyberStrikeAI 机器人命令】\n\n")
"- `列表` `list` — 列出所有对话标题与 ID | List conversations\n" + b.WriteString("【通用 General】\n")
"- `切换 <ID>` `switch <ID>` — 指定对话继续 | Switch to conversation\n" + b.WriteString("· 帮助 / help — 显示本帮助\n")
"- `新对话` `new` — 开启新对话 | Start new conversation\n" + b.WriteString("· 版本 / version — 显示当前版本号\n")
"- `清空` `clear` — 清空当前上下文 | Clear context\n" + b.WriteString("\n【对话 Conversation】\n")
"- `当前` `current` — 显示当前对话 ID 与标题 | Show current conversation\n" + b.WriteString("· 列表 / list — 列出所有对话标题与 ID\n")
"- `停止` `stop` — 中断当前任务 | Stop running task\n" + b.WriteString("· 切换 <ID> / switch <ID> — 指定对话继续\n")
"- `角色` `roles` — 列出所有可用角色 | List roles\n" + b.WriteString("· 新对话 / new — 开启新对话\n")
"- `角色 <名>` `role <name>` — 切换当前角色 | Switch role\n" + b.WriteString("· 清空 / clear — 清空当前上下文\n")
"- `删除 <ID>` `delete <ID>` — 删除指定对话 | Delete conversation\n" + b.WriteString("· 当前 / current — 显示当前对话、角色与项目\n")
"- `版本` `version` — 显示当前版本号 | Show version\n\n" + b.WriteString("· 停止 / stop — 中断当前任务\n")
"---\n" + b.WriteString("· 删除 <ID> / delete <ID> — 删除指定对话\n")
"除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。\n" + b.WriteString("\n【角色 Role】\n")
"Otherwise, send any text for AI penetration testing / security analysis." b.WriteString("· 角色 / roles — 列出所有可用角色\n")
b.WriteString("· 角色 <名> / role <name> — 切换当前角色\n")
if h.projectsEnabled() {
b.WriteString("\n【项目 Project】\n")
b.WriteString("· 项目 / projects — 列出所有项目\n")
b.WriteString("· 新建项目 <名称> / new project <name> — 创建并绑定当前对话\n")
b.WriteString("· 绑定项目 <ID或名称> / bind project <ID|name> — 绑定到已有项目\n")
b.WriteString("· 解除项目 / unbind project — 解除项目绑定\n")
}
b.WriteString("\n──────────────\n")
b.WriteString("除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。")
return b.String()
}
func (h *RobotHandler) projectsEnabled() bool {
return h.config != nil && h.config.Project.Enabled
}
func (h *RobotHandler) resolveProjectByIDOrName(idOrName string) (*database.Project, string) {
idOrName = strings.TrimSpace(idOrName)
if idOrName == "" {
return nil, "请指定项目 ID 或名称,例如:绑定项目 xxx-xxx"
}
if p, err := h.db.GetProject(idOrName); err == nil {
return p, ""
}
list, err := h.db.ListProjects("", 200, 0)
if err != nil {
return nil, "查询项目失败: " + err.Error()
}
var matches []*database.Project
for _, p := range list {
if p.Name == idOrName {
matches = append(matches, p)
}
}
switch len(matches) {
case 0:
return nil, fmt.Sprintf("项目「%s」不存在。发送「项目」查看列表。", idOrName)
case 1:
return matches[0], ""
default:
var b strings.Builder
b.WriteString(fmt.Sprintf("名称「%s」匹配到多个项目,请使用 ID 绑定:\n", idOrName))
for _, p := range matches {
b.WriteString(fmt.Sprintf("· %s\n ID: %s\n", p.Name, p.ID))
}
return nil, strings.TrimSuffix(b.String(), "\n")
}
}
func (h *RobotHandler) formatProjectLabel(projectID string) string {
if strings.TrimSpace(projectID) == "" {
return "未绑定"
}
if p, err := h.db.GetProject(projectID); err == nil {
return fmt.Sprintf("「%s」 (%s)", p.Name, p.ID)
}
return projectID
}
func (h *RobotHandler) cmdProjects() string {
if !h.projectsEnabled() {
return "项目功能未启用(config.project.enabled)。"
}
list, err := h.db.ListProjects("", 50, 0)
if err != nil {
return "获取项目列表失败: " + err.Error()
}
if len(list) == 0 {
return "暂无项目。发送「新建项目 <名称>」创建并绑定到当前对话。"
}
var b strings.Builder
b.WriteString("【项目列表】\n")
for i, p := range list {
if i >= 20 {
b.WriteString("… 仅显示前 20 条\n")
break
}
status := p.Status
if status == "" {
status = "active"
}
b.WriteString(fmt.Sprintf("· %s [%s]\n ID: %s\n", p.Name, status, p.ID))
}
return strings.TrimSuffix(b.String(), "\n")
}
func (h *RobotHandler) cmdBindProject(platform, userID, idOrName string) string {
if !h.projectsEnabled() {
return "项目功能未启用(config.project.enabled)。"
}
p, errMsg := h.resolveProjectByIDOrName(idOrName)
if p == nil {
return errMsg
}
convID, _ := h.getOrCreateConversation(platform, userID, "")
if convID == "" {
return "无法获取当前对话,请稍后再试。"
}
if err := h.db.SetConversationProjectID(convID, p.ID); err != nil {
return "绑定失败: " + err.Error()
}
return fmt.Sprintf("已将当前对话绑定到项目:「%s」\nID: %s", p.Name, p.ID)
}
func (h *RobotHandler) cmdNewProject(platform, userID, name string) string {
if !h.projectsEnabled() {
return "项目功能未启用(config.project.enabled)。"
}
name = strings.TrimSpace(name)
if name == "" {
return "请指定项目名称,例如:新建项目 某目标渗透"
}
p := &database.Project{Name: name, Status: "active"}
created, err := h.db.CreateProject(p)
if err != nil {
return "创建项目失败: " + err.Error()
}
convID, _ := h.getOrCreateConversation(platform, userID, name)
if convID == "" {
return fmt.Sprintf("项目已创建:「%s」\nID: %s\n(绑定当前对话失败,请手动发送「绑定项目 %s」)", created.Name, created.ID, created.ID)
}
if err := h.db.SetConversationProjectID(convID, created.ID); err != nil {
return fmt.Sprintf("项目已创建:「%s」\nID: %s\n绑定失败: %s", created.Name, created.ID, err.Error())
}
return fmt.Sprintf("已创建项目并绑定当前对话:「%s」\nID: %s", created.Name, created.ID)
}
func (h *RobotHandler) cmdUnbindProject(platform, userID string) string {
if !h.projectsEnabled() {
return "项目功能未启用(config.project.enabled)。"
}
sk := h.sessionKey(platform, userID)
h.mu.RLock()
convID := h.sessions[sk]
h.mu.RUnlock()
if convID == "" {
if persistedConvID, _ := h.loadSessionBinding(sk); persistedConvID != "" {
convID = persistedConvID
}
}
if convID == "" {
return "当前没有进行中的对话,无需解除绑定。"
}
projectID, err := h.db.GetConversationProjectID(convID)
if err != nil {
return "获取对话项目失败: " + err.Error()
}
if strings.TrimSpace(projectID) == "" {
return "当前对话未绑定项目。"
}
if err := h.db.SetConversationProjectID(convID, ""); err != nil {
return "解除绑定失败: " + err.Error()
}
return "已解除当前对话的项目绑定。"
} }
func (h *RobotHandler) cmdList() string { func (h *RobotHandler) cmdList() string {
@@ -357,7 +517,12 @@ func (h *RobotHandler) cmdCurrent(platform, userID string) string {
return "当前对话 ID: " + convID + "(获取标题失败)" return "当前对话 ID: " + convID + "(获取标题失败)"
} }
role := h.getRole(platform, userID) role := h.getRole(platform, userID)
return fmt.Sprintf("当前对话:「%s」\nID: %s\n当前角色: %s", conv.Title, conv.ID, role) reply := fmt.Sprintf("当前对话:「%s」\nID: %s\n当前角色: %s", conv.Title, conv.ID, role)
if h.projectsEnabled() {
projectID, _ := h.db.GetConversationProjectID(conv.ID)
reply += "\n当前项目: " + h.formatProjectLabel(projectID)
}
return reply
} }
func (h *RobotHandler) cmdRoles() string { func (h *RobotHandler) cmdRoles() string {
@@ -494,6 +659,26 @@ func (h *RobotHandler) handleRobotCommand(platform, userID, text string) (string
return h.cmdDelete(platform, userID, convID), true return h.cmdDelete(platform, userID, convID), true
case text == robotCmdVersion || text == "version": case text == robotCmdVersion || text == "version":
return h.cmdVersion(), true return h.cmdVersion(), true
case text == robotCmdProjects || text == robotCmdProjectsList || text == "projects":
return h.cmdProjects(), true
case text == robotCmdUnbindProject || text == "unbind project":
return h.cmdUnbindProject(platform, userID), true
case strings.HasPrefix(text, robotCmdNewProject+" ") || strings.HasPrefix(text, "new project "):
var name string
if strings.HasPrefix(text, robotCmdNewProject+" ") {
name = strings.TrimSpace(text[len(robotCmdNewProject)+1:])
} else {
name = strings.TrimSpace(text[len("new project "):])
}
return h.cmdNewProject(platform, userID, name), true
case strings.HasPrefix(text, robotCmdBindProject+" ") || strings.HasPrefix(text, "bind project "):
var idOrName string
if strings.HasPrefix(text, robotCmdBindProject+" ") {
idOrName = strings.TrimSpace(text[len(robotCmdBindProject)+1:])
} else {
idOrName = strings.TrimSpace(text[len("bind project "):])
}
return h.cmdBindProject(platform, userID, idOrName), true
default: default:
return "", false return "", false
} }
+61 -8
View File
@@ -44,11 +44,12 @@ func newSDKClientFromSession(session *mcp.ClientSession, client *mcp.Client, log
// lazySDKClient 延迟连接:Initialize() 时才调用官方 SDK 建立连接,对外实现 ExternalMCPClient // lazySDKClient 延迟连接:Initialize() 时才调用官方 SDK 建立连接,对外实现 ExternalMCPClient
type lazySDKClient struct { type lazySDKClient struct {
serverCfg config.ExternalMCPServerConfig serverCfg config.ExternalMCPServerConfig
logger *zap.Logger logger *zap.Logger
inner ExternalMCPClient // 连接成功后为 *sdkClient sessionCancel context.CancelFunc
mu sync.RWMutex inner ExternalMCPClient // connected SDK client
status string mu sync.RWMutex
status string
} }
func newLazySDKClient(serverCfg config.ExternalMCPServerConfig, logger *zap.Logger) *lazySDKClient { func newLazySDKClient(serverCfg config.ExternalMCPServerConfig, logger *zap.Logger) *lazySDKClient {
@@ -92,14 +93,61 @@ func (c *lazySDKClient) Initialize(ctx context.Context) error {
} }
c.mu.Unlock() c.mu.Unlock()
inner, err := createSDKClient(ctx, c.serverCfg, c.logger) sessionCtx, sessionCancel := context.WithCancel(context.Background())
if err != nil { type connectResult struct {
inner ExternalMCPClient
err error
}
resultCh := make(chan connectResult)
abandoned := make(chan struct{})
go func() {
inner, err := createSDKClient(sessionCtx, c.serverCfg, c.logger)
select {
case resultCh <- connectResult{inner: inner, err: err}:
case <-abandoned:
if inner != nil {
_ = inner.Close()
}
sessionCancel()
}
}()
var result connectResult
select {
case result = <-resultCh:
case <-ctx.Done():
close(abandoned)
sessionCancel()
c.setStatus("error")
return ctx.Err()
}
if err := ctx.Err(); err != nil {
sessionCancel()
if result.inner != nil {
_ = result.inner.Close()
}
c.setStatus("error") c.setStatus("error")
return err return err
} }
if result.err != nil {
sessionCancel()
c.setStatus("error")
return result.err
}
c.mu.Lock() c.mu.Lock()
c.inner = inner if c.inner != nil {
c.mu.Unlock()
sessionCancel()
if result.inner != nil {
_ = result.inner.Close()
}
return nil
}
c.inner = result.inner
c.sessionCancel = sessionCancel
c.mu.Unlock() c.mu.Unlock()
c.setStatus("connected") c.setStatus("connected")
return nil return nil
@@ -128,9 +176,14 @@ func (c *lazySDKClient) CallTool(ctx context.Context, name string, args map[stri
func (c *lazySDKClient) Close() error { func (c *lazySDKClient) Close() error {
c.mu.Lock() c.mu.Lock()
inner := c.inner inner := c.inner
sessionCancel := c.sessionCancel
c.inner = nil c.inner = nil
c.sessionCancel = nil
c.mu.Unlock() c.mu.Unlock()
c.setStatus("disconnected") c.setStatus("disconnected")
if sessionCancel != nil {
sessionCancel()
}
if inner != nil { if inner != nil {
return inner.Close() return inner.Close()
} }
+229 -21
View File
@@ -9,6 +9,10 @@
--secondary-color: #2d2d2d; --secondary-color: #2d2d2d;
--accent-color: #0066ff; --accent-color: #0066ff;
--accent-hover: #0052cc; --accent-hover: #0052cc;
--brand-core: #141824;
--brand-ai-start: #0066ff;
--brand-ai-end: #7c3aed;
--brand-gradient: linear-gradient(135deg, var(--brand-ai-start) 0%, var(--brand-ai-end) 100%);
--bg-primary: #ffffff; --bg-primary: #ffffff;
--bg-secondary: #f8f9fa; --bg-secondary: #f8f9fa;
--bg-tertiary: #f1f3f5; --bg-tertiary: #f1f3f5;
@@ -125,11 +129,19 @@ body {
color: var(--text-primary); color: var(--text-primary);
} }
.main-sidebar-header .logo span { .main-sidebar-header .logo span,
.main-sidebar-header .brand-wordmark {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 700;
letter-spacing: -0.3px; letter-spacing: -0.04em;
color: var(--text-primary); color: var(--brand-core);
}
.main-sidebar-header .brand-wordmark__ai {
background: var(--brand-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
.main-sidebar-nav { .main-sidebar-nav {
@@ -592,37 +604,89 @@ header {
.logo { .logo {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
} }
.logo svg { .logo svg {
color: var(--accent-color); color: var(--accent-color);
} }
.logo h1 { /* 品牌字标:CyberStrike + AI 渐变 */
.brand-wordmark {
display: inline-flex;
align-items: baseline;
margin: 0;
font-size: 1.375rem;
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1;
white-space: nowrap;
}
.brand-wordmark--lg {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; }
letter-spacing: -0.5px;
.brand-wordmark--sm {
font-size: 1.25rem;
}
.brand-wordmark__core {
color: var(--brand-core);
}
.brand-wordmark__ai {
background: var(--brand-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 800;
}
.brand-logo {
width: 36px;
height: 36px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 102, 255, 0.18);
flex-shrink: 0;
object-fit: cover;
}
.logo h1,
.logo .brand-wordmark {
font-size: 1.375rem;
} }
.header-logo-link { .header-logo-link {
cursor: pointer; cursor: pointer;
transition: opacity 0.2s ease; transition: transform 0.2s ease, opacity 0.2s ease;
} }
.header-logo-link:hover { .header-logo-link:hover {
opacity: 0.85; opacity: 1;
transform: translateY(-1px);
}
.header-logo-link:hover .brand-logo {
box-shadow: 0 4px 14px rgba(0, 102, 255, 0.28);
} }
.version-badge { .version-badge {
display: inline-block; display: inline-flex;
margin-left: 6px; align-items: center;
font-size: 0.6875rem; margin-left: 4px;
font-weight: 400; padding: 3px 9px;
color: var(--text-muted); font-size: 0.625rem;
letter-spacing: 0.02em; font-weight: 600;
vertical-align: 0.35em; font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
color: var(--brand-ai-start);
background: linear-gradient(135deg, rgba(0, 102, 255, 0.08) 0%, rgba(124, 58, 237, 0.08) 100%);
border: 1px solid rgba(0, 102, 255, 0.22);
border-radius: 999px;
letter-spacing: 0.04em;
vertical-align: middle;
user-select: none; user-select: none;
line-height: 1.4;
} }
.header-right { .header-right {
@@ -3091,10 +3155,36 @@ header {
.login-brand { .login-brand {
padding: 32px 28px 24px; padding: 32px 28px 24px;
text-align: center; text-align: center;
background: linear-gradient(180deg, rgba(0, 102, 255, 0.06) 0%, transparent 100%); background: linear-gradient(180deg, rgba(0, 102, 255, 0.07) 0%, rgba(124, 58, 237, 0.04) 50%, transparent 100%);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.login-brand-logo {
width: 56px;
height: 56px;
margin: 0 auto 16px;
border-radius: 14px;
box-shadow: 0 6px 20px rgba(0, 102, 255, 0.2);
object-fit: cover;
}
.login-title {
margin: 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
gap: 0.35em;
font-size: 1.25rem;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: -0.01em;
}
.login-title .brand-wordmark {
font-size: 1.375rem;
}
.login-brand h2 { .login-brand h2 {
margin: 0; margin: 0;
font-size: 1.375rem; font-size: 1.375rem;
@@ -3530,8 +3620,19 @@ header {
flex-shrink: 0; flex-shrink: 0;
} }
.logo h1 { .logo h1,
font-size: 1.25rem; .logo .brand-wordmark {
font-size: 1.2rem;
}
.brand-logo {
width: 32px;
height: 32px;
}
.version-badge {
padding: 2px 7px;
font-size: 0.5625rem;
} }
.header-subtitle { .header-subtitle {
@@ -4298,6 +4399,31 @@ header {
margin-bottom: 12px; margin-bottom: 12px;
} }
.robot-cmd-category {
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-primary);
margin: 16px 0 6px;
letter-spacing: 0.02em;
}
.robot-cmd-category:first-of-type {
margin-top: 8px;
}
.robot-cmd-list {
color: var(--text-muted);
font-size: 13px;
line-height: 1.8;
margin: 0 0 4px 16px;
padding: 0;
}
.robot-cmd-footer {
margin-top: 12px !important;
margin-bottom: 0 !important;
}
.form-hint { .form-hint {
display: block; display: block;
font-size: 0.8125rem; font-size: 0.8125rem;
@@ -21258,10 +21384,11 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
border-radius: 14px; border-radius: 14px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
overflow: hidden; overflow: hidden;
min-height: 420px; min-height: 0;
align-self: stretch; align-self: stretch;
} }
.projects-detail-header { .projects-detail-header {
flex-shrink: 0;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
@@ -21349,6 +21476,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding: 12px 24px; padding: 12px 24px;
background: #f8fafc; background: #f8fafc;
border-bottom: 1px solid #eef2f7; border-bottom: 1px solid #eef2f7;
flex-shrink: 0;
} }
.projects-tab { .projects-tab {
padding: 8px 16px; padding: 8px 16px;
@@ -21594,6 +21722,86 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
} }
#project-panel-facts,
#project-panel-conversations,
#project-panel-vulns {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
#project-panel-facts .projects-fact-toolbar,
#project-panel-vulns .projects-fact-toolbar,
#project-panel-conversations .projects-panel-toolbar {
flex: 0 0 auto;
}
#project-panel-facts .projects-table-wrap,
#project-panel-conversations .projects-table-wrap,
#project-panel-vulns .projects-table-wrap {
flex: 1 1 auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
#project-panel-facts .projects-table-wrap .data-table--projects thead th,
#project-panel-conversations .projects-table-wrap .data-table--projects thead th,
#project-panel-vulns .projects-table-wrap .data-table--projects thead th {
position: sticky;
top: 0;
z-index: 2;
box-shadow: 0 1px 0 var(--border-color, #e2e8f0);
}
.projects-panel-toolbar--hint {
margin-bottom: 14px;
padding: 0;
background: transparent;
border: none;
border-radius: 0;
}
.projects-panel-toolbar--hint .projects-fact-toolbar-hint {
margin: 0;
}
#project-panel-conversations .data-table--projects th:nth-child(1),
#project-panel-conversations .data-table--projects td:nth-child(1) {
width: 48%;
}
#project-panel-conversations .data-table--projects th:nth-child(2),
#project-panel-conversations .data-table--projects td:nth-child(2) {
width: 22%;
}
#project-panel-conversations .data-table--projects th:nth-child(3),
#project-panel-conversations .data-table--projects td:nth-child(3) {
width: 30%;
}
.projects-vuln-toolbar-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.projects-vuln-toolbar-top .projects-fact-toolbar-hint {
flex: 1 1 240px;
margin: 0;
}
#project-panel-vulns .data-table--projects th:nth-child(1),
#project-panel-vulns .data-table--projects td:nth-child(1) {
width: 46%;
}
#project-panel-vulns .data-table--projects th:nth-child(2),
#project-panel-vulns .data-table--projects td:nth-child(2) {
width: 14%;
}
#project-panel-vulns .data-table--projects th:nth-child(3),
#project-panel-vulns .data-table--projects td:nth-child(3) {
width: 14%;
}
#project-panel-vulns .data-table--projects th:nth-child(4),
#project-panel-vulns .data-table--projects td:nth-child(4) {
width: 26%;
}
.projects-table-wrap .data-table--projects { .projects-table-wrap .data-table--projects {
min-width: 0; min-width: 0;
table-layout: fixed; table-layout: fixed;
+13 -1
View File
@@ -48,6 +48,7 @@
}, },
"login": { "login": {
"title": "Sign in to CyberStrikeAI", "title": "Sign in to CyberStrikeAI",
"titlePrefix": "Sign in to",
"subtitle": "Enter the access password from config", "subtitle": "Enter the access password from config",
"passwordLabel": "Password", "passwordLabel": "Password",
"passwordPlaceholder": "Enter password", "passwordPlaceholder": "Enter password",
@@ -258,6 +259,9 @@
"boundConversationsHint": "Conversations bound to this project; click to open", "boundConversationsHint": "Conversations bound to this project; click to open",
"titleLabel": "Title", "titleLabel": "Title",
"projectVulnSummaryHint": "Vulnerability summary under this project", "projectVulnSummaryHint": "Vulnerability summary under this project",
"searchVulnsSr": "Search vulnerabilities",
"searchVulnsPlaceholder": "Search title, description, type, target…",
"noMatchingVulns": "No matching vulnerabilities, try adjusting filters",
"viewInVulnerabilityManagement": "View in vulnerability management", "viewInVulnerabilityManagement": "View in vulnerability management",
"severity": "Severity", "severity": "Severity",
"status": "Status", "status": "Status",
@@ -2091,17 +2095,25 @@
"settingsRobotsExtra": { "settingsRobotsExtra": {
"botCommandsTitle": "Bot command instructions", "botCommandsTitle": "Bot command instructions",
"botCommandsDesc": "You can send the following commands in chat (Chinese and English supported):", "botCommandsDesc": "You can send the following commands in chat (Chinese and English supported):",
"botCmdCategoryGeneral": "General",
"botCmdCategoryConversation": "Conversation",
"botCmdCategoryRole": "Role",
"botCmdCategoryProject": "Project",
"botCmdHelp": "Show this help", "botCmdHelp": "Show this help",
"botCmdList": "List conversations", "botCmdList": "List conversations",
"botCmdSwitch": "Switch to conversation", "botCmdSwitch": "Switch to conversation",
"botCmdNew": "Start new conversation", "botCmdNew": "Start new conversation",
"botCmdClear": "Clear context", "botCmdClear": "Clear context",
"botCmdCurrent": "Show current conversation", "botCmdCurrent": "Show current conversation, role and project",
"botCmdStop": "Stop running task", "botCmdStop": "Stop running task",
"botCmdRoles": "List roles", "botCmdRoles": "List roles",
"botCmdRole": "Switch role", "botCmdRole": "Switch role",
"botCmdDelete": "Delete conversation", "botCmdDelete": "Delete conversation",
"botCmdVersion": "Show version", "botCmdVersion": "Show version",
"botCmdProjects": "List projects",
"botCmdNewProject": "Create project and bind current conversation",
"botCmdBindProject": "Bind current conversation to a project",
"botCmdUnbindProject": "Unbind project from current conversation",
"botCommandsFooter": "Otherwise, send any text for AI penetration testing / security analysis." "botCommandsFooter": "Otherwise, send any text for AI penetration testing / security analysis."
}, },
"mcpDetailModal": { "mcpDetailModal": {
+13 -1
View File
@@ -48,6 +48,7 @@
}, },
"login": { "login": {
"title": "登录 CyberStrikeAI", "title": "登录 CyberStrikeAI",
"titlePrefix": "登录",
"subtitle": "请输入配置中的访问密码", "subtitle": "请输入配置中的访问密码",
"passwordLabel": "密码", "passwordLabel": "密码",
"passwordPlaceholder": "输入登录密码", "passwordPlaceholder": "输入登录密码",
@@ -247,6 +248,9 @@
"boundConversationsHint": "绑定到本项目的对话;点击可打开会话", "boundConversationsHint": "绑定到本项目的对话;点击可打开会话",
"titleLabel": "标题", "titleLabel": "标题",
"projectVulnSummaryHint": "本项目下记录的漏洞汇总", "projectVulnSummaryHint": "本项目下记录的漏洞汇总",
"searchVulnsSr": "搜索漏洞",
"searchVulnsPlaceholder": "搜索标题、描述、类型、目标…",
"noMatchingVulns": "无匹配漏洞,请调整筛选条件",
"viewInVulnerabilityManagement": "在漏洞管理中查看", "viewInVulnerabilityManagement": "在漏洞管理中查看",
"severity": "严重度", "severity": "严重度",
"status": "状态", "status": "状态",
@@ -2080,17 +2084,25 @@
"settingsRobotsExtra": { "settingsRobotsExtra": {
"botCommandsTitle": "机器人命令说明", "botCommandsTitle": "机器人命令说明",
"botCommandsDesc": "在对话中可发送以下命令(支持中英文):", "botCommandsDesc": "在对话中可发送以下命令(支持中英文):",
"botCmdCategoryGeneral": "通用",
"botCmdCategoryConversation": "对话",
"botCmdCategoryRole": "角色",
"botCmdCategoryProject": "项目",
"botCmdHelp": "显示本帮助 | Show this help", "botCmdHelp": "显示本帮助 | Show this help",
"botCmdList": "列出所有对话标题与 ID | List conversations", "botCmdList": "列出所有对话标题与 ID | List conversations",
"botCmdSwitch": "指定对话继续 | Switch to conversation", "botCmdSwitch": "指定对话继续 | Switch to conversation",
"botCmdNew": "开启新对话 | Start new conversation", "botCmdNew": "开启新对话 | Start new conversation",
"botCmdClear": "清空当前上下文 | Clear context", "botCmdClear": "清空当前上下文 | Clear context",
"botCmdCurrent": "显示当前对话 ID 与标题 | Show current conversation", "botCmdCurrent": "显示当前对话、角色与项目 | Show current conversation",
"botCmdStop": "中断当前任务 | Stop running task", "botCmdStop": "中断当前任务 | Stop running task",
"botCmdRoles": "列出所有可用角色 | List roles", "botCmdRoles": "列出所有可用角色 | List roles",
"botCmdRole": "切换当前角色 | Switch role", "botCmdRole": "切换当前角色 | Switch role",
"botCmdDelete": "删除指定对话 | Delete conversation", "botCmdDelete": "删除指定对话 | Delete conversation",
"botCmdVersion": "显示当前版本号 | Show version", "botCmdVersion": "显示当前版本号 | Show version",
"botCmdProjects": "列出所有项目 | List projects",
"botCmdNewProject": "创建项目并绑定当前对话 | Create & bind project",
"botCmdBindProject": "将当前对话绑定到项目 | Bind conversation",
"botCmdUnbindProject": "解除当前对话的项目绑定 | Unbind project",
"botCommandsFooter": "除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis." "botCommandsFooter": "除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis."
}, },
"mcpDetailModal": { "mcpDetailModal": {
+85 -10
View File
@@ -226,9 +226,9 @@ function initProjectsModalEscape() {
window._projectsModalEscapeBound = true; window._projectsModalEscapeBound = true;
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return; if (e.key !== 'Escape') return;
if (document.getElementById('project-modal')?.style.display === 'flex') closeProjectModal(); if (isProjectsOverlayVisible('project-modal')) closeProjectModal();
else if (document.getElementById('fact-modal')?.style.display === 'flex') closeFactModal(); else if (isProjectsOverlayVisible('fact-modal')) closeFactModal();
else if (document.getElementById('fact-detail-modal')?.style.display === 'flex') closeFactDetailModal(); else if (isProjectsOverlayVisible('fact-detail-modal')) closeFactDetailModal();
}); });
} }
@@ -236,6 +236,7 @@ async function initProjectsPage() {
const page = document.getElementById('page-projects'); const page = document.getElementById('page-projects');
if (!page || page.style.display === 'none') return; if (!page || page.style.display === 'none') return;
initProjectsModalEscape(); initProjectsModalEscape();
syncProjectsModalBodyLock();
updateProjectsDetailVisibility(); updateProjectsDetailVisibility();
await loadProjectsList(); await loadProjectsList();
if (!currentProjectId && projectsCache.length) { if (!currentProjectId && projectsCache.length) {
@@ -335,6 +336,50 @@ function formatSeverityBadge(severity) {
return `<span class="projects-severity ${cls}">${escapeHtml(severity || '—')}</span>`; return `<span class="projects-severity ${cls}">${escapeHtml(severity || '—')}</span>`;
} }
function formatVulnStatusBadge(status) {
const s = (status || 'open').toLowerCase();
const labelMap = {
open: 'vulnerabilityPage.statusOpen',
confirmed: 'vulnerabilityPage.statusConfirmed',
fixed: 'vulnerabilityPage.statusFixed',
false_positive: 'vulnerabilityPage.statusFalsePositive',
};
const label = labelMap[s] ? tp(labelMap[s]) : status || '—';
const cls = ['open', 'confirmed', 'fixed', 'false_positive'].includes(s) ? s : 'open';
return `<span class="status-badge status-${escapeHtml(cls)}">${escapeHtml(label)}</span>`;
}
let _projectVulnsFilterDebounce = null;
function buildProjectVulnsQueryParams() {
const params = new URLSearchParams();
params.set('project_id', currentProjectId);
params.set('limit', '200');
const search = document.getElementById('project-vulns-search')?.value?.trim();
const severity = document.getElementById('project-vulns-filter-severity')?.value?.trim();
const status = document.getElementById('project-vulns-filter-status')?.value?.trim();
if (search) params.set('q', search);
if (severity) params.set('severity', severity);
if (status) params.set('status', status);
return params;
}
function projectVulnsHasActiveFilter() {
return !!(
document.getElementById('project-vulns-search')?.value?.trim() ||
document.getElementById('project-vulns-filter-severity')?.value ||
document.getElementById('project-vulns-filter-status')?.value
);
}
function debouncedLoadProjectVulnerabilities() {
if (_projectVulnsFilterDebounce) clearTimeout(_projectVulnsFilterDebounce);
_projectVulnsFilterDebounce = setTimeout(() => {
_projectVulnsFilterDebounce = null;
loadProjectVulnerabilities();
}, 280);
}
function getProjectsListFilter() { function getProjectsListFilter() {
return (document.getElementById('projects-list-search')?.value || '').trim().toLowerCase(); return (document.getElementById('projects-list-search')?.value || '').trim().toLowerCase();
} }
@@ -416,10 +461,16 @@ async function selectProject(id) {
const catEl = document.getElementById('project-facts-filter-category'); const catEl = document.getElementById('project-facts-filter-category');
const confEl = document.getElementById('project-facts-filter-confidence'); const confEl = document.getElementById('project-facts-filter-confidence');
const sparseEl = document.getElementById('project-facts-filter-sparse'); const sparseEl = document.getElementById('project-facts-filter-sparse');
const vulnSearchEl = document.getElementById('project-vulns-search');
const vulnSevEl = document.getElementById('project-vulns-filter-severity');
const vulnStatusEl = document.getElementById('project-vulns-filter-status');
if (searchEl) searchEl.value = ''; if (searchEl) searchEl.value = '';
if (catEl) catEl.value = ''; if (catEl) catEl.value = '';
if (confEl) confEl.value = ''; if (confEl) confEl.value = '';
if (sparseEl) sparseEl.checked = false; if (sparseEl) sparseEl.checked = false;
if (vulnSearchEl) vulnSearchEl.value = '';
if (vulnSevEl) vulnSevEl.value = '';
if (vulnStatusEl) vulnStatusEl.value = '';
renderProjectsSidebar(); renderProjectsSidebar();
updateProjectsDetailVisibility(); updateProjectsDetailVisibility();
try { try {
@@ -845,15 +896,18 @@ async function loadProjectVulnerabilities() {
const tbody = document.getElementById('project-vulns-tbody'); const tbody = document.getElementById('project-vulns-tbody');
if (!tbody || !currentProjectId) return; if (!tbody || !currentProjectId) return;
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('common.loading'))}</td></tr>`; tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('common.loading'))}</td></tr>`;
const res = await apiFetch(`/api/vulnerabilities?project_id=${encodeURIComponent(currentProjectId)}&limit=100`); const qs = buildProjectVulnsQueryParams().toString();
const res = await apiFetch(`/api/vulnerabilities?${qs}`);
if (!res.ok) { if (!res.ok) {
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('common.loadFailed'))}</td></tr>`; tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('common.loadFailed'))}</td></tr>`;
return; return;
} }
const data = await res.json(); const data = await res.json();
const items = data.Vulnerabilities || data.vulnerabilities || data.items || []; const items = data.Vulnerabilities || data.vulnerabilities || data.items || (Array.isArray(data) ? data : []);
if (!items.length) { if (!items.length) {
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('projects.noVulnerabilityRecords'))}</td></tr>`; tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${
projectVulnsHasActiveFilter() ? tp('projects.noMatchingVulns') : tp('projects.noVulnerabilityRecords')
}</td></tr>`;
refreshProjectHeaderStats(); refreshProjectHeaderStats();
return; return;
} }
@@ -862,7 +916,7 @@ async function loadProjectVulnerabilities() {
return `<tr> return `<tr>
<td class="cell-summary" title="${escapeHtml(v.title)}">${escapeHtml(v.title)}</td> <td class="cell-summary" title="${escapeHtml(v.title)}">${escapeHtml(v.title)}</td>
<td>${formatSeverityBadge(v.severity)}</td> <td>${formatSeverityBadge(v.severity)}</td>
<td>${escapeHtml(v.status)}</td> <td>${formatVulnStatusBadge(v.status)}</td>
<td class="col-actions"> <td class="col-actions">
<div class="projects-table-actions"> <div class="projects-table-actions">
<button type="button" class="projects-action-btn projects-action-btn--view" data-vuln-id="${idEsc}" onclick="openVulnerabilityDetail(this.dataset.vulnId)">${escapeHtml(tp('common.view'))}</button> <button type="button" class="projects-action-btn projects-action-btn--view" data-vuln-id="${idEsc}" onclick="openVulnerabilityDetail(this.dataset.vulnId)">${escapeHtml(tp('common.view'))}</button>
@@ -927,18 +981,37 @@ function openProjectsOverlay(id) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (!el) return; if (!el) return;
el.style.display = 'flex'; el.style.display = 'flex';
document.body.classList.add('projects-modal-open'); syncProjectsModalBodyLock();
const focusTarget = el.querySelector('input.form-input, textarea.form-input, select.form-input'); const focusTarget = el.querySelector('input.form-input, textarea.form-input, select.form-input');
if (focusTarget) { if (focusTarget) {
setTimeout(() => focusTarget.focus(), 80); setTimeout(() => focusTarget.focus(), 80);
} }
} }
function isProjectsOverlayVisible(id) {
const el = document.getElementById(id);
if (!el) return false;
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden';
}
function hasVisibleProjectsOverlay() {
const overlays = document.querySelectorAll('.projects-modal-overlay');
return Array.from(overlays).some((el) => {
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden';
});
}
function syncProjectsModalBodyLock() {
if (hasVisibleProjectsOverlay()) document.body.classList.add('projects-modal-open');
else document.body.classList.remove('projects-modal-open');
}
function closeProjectsOverlay(id) { function closeProjectsOverlay(id) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) el.style.display = 'none'; if (el) el.style.display = 'none';
const anyOpen = document.querySelector('.projects-modal-overlay[style*="flex"]'); syncProjectsModalBodyLock();
if (!anyOpen) document.body.classList.remove('projects-modal-open');
} }
function showNewProjectModal() { function showNewProjectModal() {
@@ -1522,6 +1595,8 @@ window.openVulnerabilitiesForProject = openVulnerabilitiesForProject;
window.openVulnerabilityDetail = openVulnerabilityDetail; window.openVulnerabilityDetail = openVulnerabilityDetail;
window.filterProjectsList = filterProjectsList; window.filterProjectsList = filterProjectsList;
window.debouncedLoadProjectFacts = debouncedLoadProjectFacts; window.debouncedLoadProjectFacts = debouncedLoadProjectFacts;
window.debouncedLoadProjectVulnerabilities = debouncedLoadProjectVulnerabilities;
window.loadProjectVulnerabilities = loadProjectVulnerabilities;
window.linkFactToExistingVulnerability = linkFactToExistingVulnerability; window.linkFactToExistingVulnerability = linkFactToExistingVulnerability;
window.createVulnerabilityFromCurrentFact = createVulnerabilityFromCurrentFact; window.createVulnerabilityFromCurrentFact = createVulnerabilityFromCurrentFact;
window.viewFactsForVulnerability = viewFactsForVulnerability; window.viewFactsForVulnerability = viewFactsForVulnerability;
+85 -13
View File
@@ -14,7 +14,13 @@
<div id="login-overlay" class="login-overlay" style="display: none;"> <div id="login-overlay" class="login-overlay" style="display: none;">
<div class="login-card"> <div class="login-card">
<div class="login-brand"> <div class="login-brand">
<h2 data-i18n="login.title">登录 CyberStrikeAI</h2> <img src="/static/logo.png" alt="" class="login-brand-logo" width="56" height="56">
<h2 class="login-title">
<span data-i18n="login.titlePrefix">登录</span>
<span class="brand-wordmark brand-wordmark--sm" aria-label="CyberStrikeAI">
<span class="brand-wordmark__core">CyberStrike</span><span class="brand-wordmark__ai">AI</span>
</span>
</h2>
<p class="login-subtitle" data-i18n="login.subtitle">请输入配置中的访问密码</p> <p class="login-subtitle" data-i18n="login.subtitle">请输入配置中的访问密码</p>
</div> </div>
<form id="login-form" class="login-form"> <form id="login-form" class="login-form">
@@ -34,8 +40,10 @@
<header> <header>
<div class="header-content"> <div class="header-content">
<div class="logo header-logo-link" onclick="switchPage('dashboard')" role="button" data-i18n="header.backToDashboard" data-i18n-attr="title" data-i18n-skip-text="true" title="返回仪表盘"> <div class="logo header-logo-link" onclick="switchPage('dashboard')" role="button" data-i18n="header.backToDashboard" data-i18n-attr="title" data-i18n-skip-text="true" title="返回仪表盘">
<img src="/static/logo.png" alt="CyberStrikeAI Logo" style="width: 32px; height: 32px; margin-right: 8px;"> <img src="/static/logo.png" alt="CyberStrikeAI Logo" class="brand-logo" width="36" height="36">
<h1>CyberStrikeAI</h1> <h1 class="brand-wordmark brand-wordmark--lg">
<span class="brand-wordmark__core">CyberStrike</span><span class="brand-wordmark__ai">AI</span>
</h1>
<span class="version-badge" data-i18n="header.version" data-i18n-attr="title" data-i18n-skip-text="true" title="当前版本">{{.Version}}</span> <span class="version-badge" data-i18n="header.version" data-i18n-attr="title" data-i18n-skip-text="true" title="当前版本">{{.Version}}</span>
</div> </div>
<div class="header-right"> <div class="header-right">
@@ -1539,8 +1547,14 @@
</div> </div>
</div> </div>
<div id="project-panel-conversations" class="projects-panel" role="tabpanel" hidden> <div id="project-panel-conversations" class="projects-panel" role="tabpanel" hidden>
<div class="projects-panel-toolbar"> <div class="projects-panel-toolbar projects-panel-toolbar--hint">
<span class="projects-panel-hint" data-i18n="projects.boundConversationsHint">绑定到本项目的对话;点击可打开会话</span> <p class="projects-fact-toolbar-hint" role="note">
<svg class="projects-fact-toolbar-hint-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
<path d="M12 10v6M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span data-i18n="projects.boundConversationsHint">绑定到本项目的对话;点击可打开会话</span>
</p>
</div> </div>
<div class="projects-table-wrap"> <div class="projects-table-wrap">
<table class="data-table data-table--projects"> <table class="data-table data-table--projects">
@@ -1550,9 +1564,48 @@
</div> </div>
</div> </div>
<div id="project-panel-vulns" class="projects-panel" role="tabpanel" hidden> <div id="project-panel-vulns" class="projects-panel" role="tabpanel" hidden>
<div class="projects-panel-toolbar"> <div class="projects-fact-toolbar">
<span class="projects-panel-hint" data-i18n="projects.projectVulnSummaryHint">本项目下记录的漏洞汇总</span> <div class="projects-vuln-toolbar-top">
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.viewInVulnerabilityManagement">在漏洞管理中查看</button> <p class="projects-fact-toolbar-hint" role="note">
<svg class="projects-fact-toolbar-hint-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
<path d="M12 10v6M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span data-i18n="projects.projectVulnSummaryHint">本项目下记录的漏洞汇总</span>
</p>
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.viewInVulnerabilityManagement">在漏洞管理中查看</button>
</div>
<div class="projects-fact-toolbar-filters" role="search">
<label class="projects-fact-filter-field projects-fact-filter-field--search">
<span class="sr-only" data-i18n="projects.searchVulnsSr">搜索漏洞</span>
<svg class="projects-fact-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2"/>
<path d="M20 20L16 16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<input type="search" id="project-vulns-search" placeholder="搜索标题、描述、类型、目标…" oninput="debouncedLoadProjectVulnerabilities()" autocomplete="off" data-i18n="projects.searchVulnsPlaceholder" data-i18n-attr="placeholder">
</label>
<label class="projects-fact-filter-field">
<span class="projects-fact-filter-label" data-i18n="projects.severity">严重度</span>
<select id="project-vulns-filter-severity" onchange="loadProjectVulnerabilities()">
<option value="" data-i18n="projects.all">全部</option>
<option value="critical">critical</option>
<option value="high">high</option>
<option value="medium">medium</option>
<option value="low">low</option>
<option value="info">info</option>
</select>
</label>
<label class="projects-fact-filter-field">
<span class="projects-fact-filter-label" data-i18n="projects.status">状态</span>
<select id="project-vulns-filter-status" onchange="loadProjectVulnerabilities()">
<option value="" data-i18n="projects.all">全部</option>
<option value="open" data-i18n="vulnerabilityPage.statusOpen">待处理</option>
<option value="confirmed" data-i18n="vulnerabilityPage.statusConfirmed">已确认</option>
<option value="fixed" data-i18n="vulnerabilityPage.statusFixed">已修复</option>
<option value="false_positive" data-i18n="vulnerabilityPage.statusFalsePositive">误报</option>
</select>
</label>
</div>
</div> </div>
<div class="projects-table-wrap"> <div class="projects-table-wrap">
<table class="data-table data-table--projects"> <table class="data-table data-table--projects">
@@ -2846,20 +2899,39 @@
<div class="settings-subsection"> <div class="settings-subsection">
<h4 data-i18n="settingsRobotsExtra.botCommandsTitle">机器人命令说明</h4> <h4 data-i18n="settingsRobotsExtra.botCommandsTitle">机器人命令说明</h4>
<p class="settings-description" data-i18n="settingsRobotsExtra.botCommandsDesc">在对话中可发送以下命令(支持中英文):</p> <p class="settings-description" data-i18n="settingsRobotsExtra.botCommandsDesc">在对话中可发送以下命令(支持中英文):</p>
<ul style="color: var(--text-muted); font-size: 13px; line-height: 1.8; margin: 8px 0 0 16px;">
<p class="robot-cmd-category" data-i18n="settingsRobotsExtra.botCmdCategoryGeneral">通用</p>
<ul class="robot-cmd-list">
<li><code>帮助</code> <code>help</code><span data-i18n="settingsRobotsExtra.botCmdHelp">显示本帮助 | Show this help</span></li> <li><code>帮助</code> <code>help</code><span data-i18n="settingsRobotsExtra.botCmdHelp">显示本帮助 | Show this help</span></li>
<li><code>版本</code> <code>version</code><span data-i18n="settingsRobotsExtra.botCmdVersion">显示当前版本号 | Show version</span></li>
</ul>
<p class="robot-cmd-category" data-i18n="settingsRobotsExtra.botCmdCategoryConversation">对话</p>
<ul class="robot-cmd-list">
<li><code>列表</code> <code>list</code><span data-i18n="settingsRobotsExtra.botCmdList">列出所有对话标题与 ID | List conversations</span></li> <li><code>列表</code> <code>list</code><span data-i18n="settingsRobotsExtra.botCmdList">列出所有对话标题与 ID | List conversations</span></li>
<li><code>切换 &lt;ID&gt;</code> <code>switch &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdSwitch">指定对话继续 | Switch to conversation</span></li> <li><code>切换 &lt;ID&gt;</code> <code>switch &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdSwitch">指定对话继续 | Switch to conversation</span></li>
<li><code>新对话</code> <code>new</code><span data-i18n="settingsRobotsExtra.botCmdNew">开启新对话 | Start new conversation</span></li> <li><code>新对话</code> <code>new</code><span data-i18n="settingsRobotsExtra.botCmdNew">开启新对话 | Start new conversation</span></li>
<li><code>清空</code> <code>clear</code><span data-i18n="settingsRobotsExtra.botCmdClear">清空当前上下文 | Clear context</span></li> <li><code>清空</code> <code>clear</code><span data-i18n="settingsRobotsExtra.botCmdClear">清空当前上下文 | Clear context</span></li>
<li><code>当前</code> <code>current</code><span data-i18n="settingsRobotsExtra.botCmdCurrent">显示当前对话 ID 与标题 | Show current conversation</span></li> <li><code>当前</code> <code>current</code><span data-i18n="settingsRobotsExtra.botCmdCurrent">显示当前对话、角色与项目 | Show current conversation</span></li>
<li><code>停止</code> <code>stop</code><span data-i18n="settingsRobotsExtra.botCmdStop">中断当前任务 | Stop running task</span></li> <li><code>停止</code> <code>stop</code><span data-i18n="settingsRobotsExtra.botCmdStop">中断当前任务 | Stop running task</span></li>
<li><code>删除 &lt;ID&gt;</code> <code>delete &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdDelete">删除指定对话 | Delete conversation</span></li>
</ul>
<p class="robot-cmd-category" data-i18n="settingsRobotsExtra.botCmdCategoryRole">角色</p>
<ul class="robot-cmd-list">
<li><code>角色</code> <code>roles</code><span data-i18n="settingsRobotsExtra.botCmdRoles">列出所有可用角色 | List roles</span></li> <li><code>角色</code> <code>roles</code><span data-i18n="settingsRobotsExtra.botCmdRoles">列出所有可用角色 | List roles</span></li>
<li><code>角色 &lt;&gt;</code> <code>role &lt;name&gt;</code><span data-i18n="settingsRobotsExtra.botCmdRole">切换当前角色 | Switch role</span></li> <li><code>角色 &lt;&gt;</code> <code>role &lt;name&gt;</code><span data-i18n="settingsRobotsExtra.botCmdRole">切换当前角色 | Switch role</span></li>
<li><code>删除 &lt;ID&gt;</code> <code>delete &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdDelete">删除指定对话 | Delete conversation</span></li>
<li><code>版本</code> <code>version</code><span data-i18n="settingsRobotsExtra.botCmdVersion">显示当前版本号 | Show version</span></li>
</ul> </ul>
<p class="settings-description" style="margin-top: 8px;" data-i18n="settingsRobotsExtra.botCommandsFooter">除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis.</p>
<p class="robot-cmd-category" data-i18n="settingsRobotsExtra.botCmdCategoryProject">项目</p>
<ul class="robot-cmd-list">
<li><code>项目</code> <code>projects</code><span data-i18n="settingsRobotsExtra.botCmdProjects">列出所有项目 | List projects</span></li>
<li><code>新建项目 &lt;名称&gt;</code> <code>new project &lt;name&gt;</code><span data-i18n="settingsRobotsExtra.botCmdNewProject">创建项目并绑定当前对话 | Create &amp; bind project</span></li>
<li><code>绑定项目 &lt;ID或名称&gt;</code> <code>bind project &lt;ID|name&gt;</code><span data-i18n="settingsRobotsExtra.botCmdBindProject">将当前对话绑定到项目 | Bind conversation</span></li>
<li><code>解除项目</code> <code>unbind project</code><span data-i18n="settingsRobotsExtra.botCmdUnbindProject">解除当前对话的项目绑定 | Unbind project</span></li>
</ul>
<p class="settings-description robot-cmd-footer" data-i18n="settingsRobotsExtra.botCommandsFooter">除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis.</p>
</div> </div>
<div class="settings-actions"> <div class="settings-actions">