Compare commits

..

26 Commits

Author SHA1 Message Date
公明 8d999792b8 Update config.yaml 2026-06-16 16:22:14 +08:00
公明 afae8970d1 Add files via upload 2026-06-16 16:21:24 +08:00
公明 4d7330c5c3 Add files via upload 2026-06-16 15:48:11 +08:00
公明 8884bfb0b4 Add files via upload 2026-06-16 13:07:04 +08:00
公明 fb351c80b6 Add files via upload 2026-06-15 22:06:46 +08:00
公明 664834e338 Add files via upload 2026-06-15 22:03:29 +08:00
公明 95bf62db88 Add files via upload 2026-06-15 21:56:42 +08:00
公明 656242614d Add files via upload 2026-06-15 21:41:02 +08:00
公明 a9d6d8c00e Add files via upload 2026-06-15 21:30:39 +08:00
公明 0d6a43c0a8 Add files via upload 2026-06-15 20:43:51 +08:00
公明 702f286eb1 Add files via upload 2026-06-15 20:24:17 +08:00
公明 f4906543a8 Update config.yaml 2026-06-15 11:55:49 +08:00
公明 b073421637 Add files via upload 2026-06-15 11:55:04 +08:00
公明 08436c27aa Add files via upload 2026-06-15 11:49:53 +08:00
公明 25ce0b221f Add files via upload 2026-06-14 21:07:51 +08:00
公明 87e629f270 Add files via upload 2026-06-14 20:19:52 +08:00
公明 04f8d73b0e Add files via upload 2026-06-14 19:58:04 +08:00
公明 33e4f023b5 Add files via upload 2026-06-14 19:48:07 +08:00
公明 fc2e822448 Add files via upload 2026-06-14 19:46:13 +08:00
公明 7487c45799 Add files via upload 2026-06-14 19:43:59 +08:00
公明 6c4b3bf131 Add files via upload 2026-06-14 19:42:14 +08:00
公明 54cea1b172 Add files via upload 2026-06-13 19:56:09 +08:00
公明 b8775997e4 Add files via upload 2026-06-13 12:32:30 +08:00
公明 4223ec47f9 Add files via upload 2026-06-13 12:27:21 +08:00
公明 9887589d99 Add files via upload 2026-06-13 12:15:55 +08:00
公明 b7c01f41c7 Add files via upload 2026-06-13 12:08:04 +08:00
39 changed files with 8633 additions and 599 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
# ============================================
# 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.6.36"
version: "v1.6.38"
# 服务器配置
server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

After

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 179 KiB

+15
View File
@@ -1185,6 +1185,8 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
}
}
flushResponsePlan()
// 助手正文开始前,推理流通常已结束;落库以便刷新后「渗透测试详情」可回放
flushThinkingStreams()
respPlan.meta = nil
if dataMap, ok := data.(map[string]interface{}); ok {
respPlan.meta = make(map[string]interface{}, len(dataMap))
@@ -1220,6 +1222,19 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
}
if eventType == "response" {
flushResponsePlan()
flushThinkingStreams()
return
}
if eventType == "done" {
flushResponsePlan()
flushThinkingStreams()
return
}
// 流式思考/推理结束:聚合落库(与 eino_agent_reply_stream_end 同理)
if eventType == "thinking_stream_end" || eventType == "reasoning_chain_stream_end" {
flushResponsePlan()
flushThinkingStreams()
return
}
@@ -3,10 +3,14 @@ package handler
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"testing"
"cyberstrike-ai/internal/config"
"cyberstrike-ai/internal/database"
"cyberstrike-ai/internal/openai"
"go.uber.org/zap"
)
@@ -46,3 +50,50 @@ func TestCreateProgressCallback_ConcurrentToolEvents(t *testing.T) {
}
wg.Wait()
}
// TestCreateProgressCallback_FlushesReasoningOnDone 流式推理聚合须在 done/response 时落库,刷新后可回放。
func TestCreateProgressCallback_FlushesReasoningOnDone(t *testing.T) {
tmp := t.TempDir()
db, err := database.NewDB(filepath.Join(tmp, "test.sqlite"), zap.NewNop())
if err != nil {
t.Fatalf("NewDB: %v", err)
}
defer os.RemoveAll(tmp)
conv, err := db.CreateConversation("test", database.ConversationCreateMeta{})
if err != nil {
t.Fatalf("CreateConversation: %v", err)
}
asst, err := db.AddMessage(conv.ID, "assistant", "处理中...", nil)
if err != nil {
t.Fatalf("AddMessage: %v", err)
}
h := &AgentHandler{logger: zap.NewNop(), db: db}
cb := h.createProgressCallback(context.Background(), nil, conv.ID, asst.ID, nil)
streamID := "eino-reasoning-test-1"
cb("reasoning_chain_stream_start", " ", map[string]interface{}{
"streamId": streamID,
"source": "eino",
})
cb("reasoning_chain_stream_delta", "step one", openai.WithSSEAccumulated(map[string]interface{}{
"streamId": streamID,
}, "step one"))
cb("done", "", map[string]interface{}{"conversationId": conv.ID})
details, err := db.GetProcessDetails(asst.ID)
if err != nil {
t.Fatalf("GetProcessDetails: %v", err)
}
found := false
for _, d := range details {
if d.EventType == "reasoning_chain" && d.Message == "step one" {
found = true
break
}
}
if !found {
t.Fatalf("expected reasoning_chain persisted on done, got %+v", details)
}
}
+12 -2
View File
@@ -12,6 +12,16 @@ import (
"go.uber.org/zap"
)
const maxProjectDescriptionRunes = 4000
func clampProjectDescription(s string) string {
r := []rune(s)
if len(r) <= maxProjectDescriptionRunes {
return s
}
return string(r[:maxProjectDescriptionRunes])
}
// ProjectHandler 项目管理处理器。
type ProjectHandler struct {
db *database.DB
@@ -48,7 +58,7 @@ func (h *ProjectHandler) CreateProject(c *gin.Context) {
}
p := &database.Project{
Name: strings.TrimSpace(req.Name),
Description: req.Description,
Description: clampProjectDescription(req.Description),
ScopeJSON: req.ScopeJSON,
Status: strings.TrimSpace(req.Status),
}
@@ -184,7 +194,7 @@ func (h *ProjectHandler) UpdateProject(c *gin.Context) {
}
}
if req.Description != nil {
p.Description = *req.Description
p.Description = clampProjectDescription(*req.Description)
}
if req.ScopeJSON != nil {
p.ScopeJSON = *req.ScopeJSON
+10
View File
@@ -785,6 +785,16 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
}
}
}
if progress != nil && reasoningStreamID != "" && strings.TrimSpace(reasoningBuf) != "" {
progress("reasoning_chain_stream_end", openai.DisplayReasoningContent(strings.TrimSpace(reasoningBuf)), map[string]interface{}{
"streamId": reasoningStreamID,
"conversationId": conversationID,
"source": "eino",
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
"orchestration": orchMode,
})
}
if streamsMainAssistant(ev.AgentName) {
s := strings.TrimSpace(mainAssistantBuf)
if mainAssistDupTarget != "" {
+107 -107
View File
@@ -2,11 +2,11 @@
set -euo pipefail
# CyberStrikeAI 一键部署启动脚本
# CyberStrikeAI one-click deploy and start script
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT_DIR"
# 颜色定义
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
@@ -14,31 +14,31 @@ BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# 打印带颜色的消息
# Print colored messages
info() { echo -e "${BLUE}$1${NC}"; }
success() { echo -e "${GREEN}$1${NC}"; }
warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
error() { echo -e "${RED}$1${NC}"; }
note() { echo -e "${CYAN}$1${NC}"; }
# 临时源配置(仅在此脚本中生效)
# Temporary mirror/proxy settings (only effective in this script)
PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}"
GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
# 保存原始环境变量(用于恢复)
# Save original env vars (for restoration)
ORIGINAL_PIP_INDEX_URL="${PIP_INDEX_URL:-}"
ORIGINAL_GOPROXY="${GOPROXY:-}"
# 进度显示函数
# Progress display helper
show_progress() {
local pid=$1
local message=$2
local i=0
local dots=""
# 检查进程是否存在
# Check if the process exists
if ! kill -0 "$pid" 2>/dev/null; then
# 进程已经结束,立即返回
# Process already finished; return immediately
return 0
fi
@@ -53,7 +53,7 @@ show_progress() {
printf "\r${BLUE}⏳ %s%s${NC}" "$message" "$dots"
sleep 0.5
# 再次检查进程是否还存在
# Re-check whether the process is still running
if ! kill -0 "$pid" 2>/dev/null; then
break
fi
@@ -63,21 +63,21 @@ show_progress() {
echo ""
echo "=========================================="
echo " CyberStrikeAI 一键部署启动脚本"
echo " (默认 HTTPS 自签证书;纯 HTTP 请用: $0 --http"
echo " CyberStrikeAI Deploy & Start Script"
echo " (HTTPS with self-signed cert by default; plain HTTP: $0 --http)"
echo "=========================================="
echo ""
# 显示临时源配置信息
# Show temporary mirror/proxy info
echo ""
warning "⚠️ 注意:此脚本将使用临时镜像源加速下载"
warning "Note: this script uses temporary mirrors to speed up downloads"
echo ""
info "Python pip 临时镜像源:"
info "Python pip temporary mirror:"
echo " ${PIP_INDEX_URL}"
info "Go Proxy 临时镜像源:"
info "Go temporary proxy:"
echo " ${GOPROXY}"
echo ""
note "这些设置仅在脚本运行期间生效,不会修改系统配置"
note "These settings apply only while this script runs and do not change system config"
echo ""
sleep 1
@@ -86,19 +86,19 @@ VENV_DIR="$ROOT_DIR/venv"
REQUIREMENTS_FILE="$ROOT_DIR/requirements.txt"
BINARY_NAME="cyberstrike-ai"
# 检查配置文件
# Check config file
if [ ! -f "$CONFIG_FILE" ]; then
error "配置文件 config.yaml 不存在"
info "请确保在项目根目录运行此脚本"
error "Config file config.yaml not found"
info "Make sure you run this script from the project root"
exit 1
fi
# 检查并安装 Python 环境
# Check Python environment
check_python() {
if ! command -v python3 >/dev/null 2>&1; then
error "未找到 python3"
error "python3 not found"
echo ""
info "请先安装 Python 3.10 或更高版本:"
info "Install Python 3.10 or later first:"
echo " macOS: brew install python3"
echo " Ubuntu: sudo apt-get install python3 python3-venv"
echo " CentOS: sudo yum install python3 python3-pip"
@@ -110,23 +110,23 @@ check_python() {
PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
if [ "$PYTHON_MAJOR" -lt 3 ] || ([ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 10 ]); then
error "Python 版本过低: $PYTHON_VERSION (需要 3.10+)"
error "Python version too old: $PYTHON_VERSION (requires 3.10+)"
exit 1
fi
success "Python 环境检查通过: $PYTHON_VERSION"
success "Python check passed: $PYTHON_VERSION"
}
# 检查并安装 Go 环境
# Check Go environment
check_go() {
if ! command -v go >/dev/null 2>&1; then
error "未找到 Go"
error "Go not found"
echo ""
info "请先安装 Go 1.21 或更高版本:"
info "Install Go 1.21 or later first:"
echo " macOS: brew install go"
echo " Ubuntu: sudo apt-get install golang-go"
echo " CentOS: sudo yum install golang"
echo " 或访问: https://go.dev/dl/"
echo " Or visit: https://go.dev/dl/"
exit 1
fi
@@ -135,63 +135,63 @@ check_go() {
GO_MINOR=$(echo "$GO_VERSION" | cut -d. -f2)
if [ "$GO_MAJOR" -lt 1 ] || ([ "$GO_MAJOR" -eq 1 ] && [ "$GO_MINOR" -lt 21 ]); then
error "Go 版本过低: $GO_VERSION (需要 1.21+)"
error "Go version too old: $GO_VERSION (requires 1.21+)"
exit 1
fi
success "Go 环境检查通过: $(go version)"
success "Go check passed: $(go version)"
}
# 设置 Python 虚拟环境
# Set up Python virtual environment
setup_python_env() {
if [ ! -d "$VENV_DIR" ]; then
info "创建 Python 虚拟环境..."
info "Creating Python virtual environment..."
python3 -m venv "$VENV_DIR"
success "虚拟环境创建完成"
success "Virtual environment created"
else
info "Python 虚拟环境已存在"
info "Python virtual environment already exists"
fi
info "激活虚拟环境..."
info "Activating virtual environment..."
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
if [ -f "$REQUIREMENTS_FILE" ]; then
echo ""
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
note "⚠️ 使用临时 pip 镜像源(仅本次脚本运行有效)"
note " 镜像地址: ${PIP_INDEX_URL}"
note " 如需永久配置,请设置环境变量 PIP_INDEX_URL"
note "Using temporary pip mirror (this script run only)"
note " Mirror URL: ${PIP_INDEX_URL}"
note " For a permanent setting, set the PIP_INDEX_URL env var"
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
info "升级 pip..."
info "Upgrading pip..."
pip install --index-url "$PIP_INDEX_URL" --upgrade pip >/dev/null 2>&1 || true
info "安装 Python 依赖包..."
info "Installing Python dependencies..."
echo ""
# 尝试安装依赖,捕获错误输出并显示进度
# Install deps in background; capture errors and show progress
PIP_LOG=$(mktemp)
(
set +e # 在子shell中禁用错误退出
set +e # disable errexit in subshell
pip install --index-url "$PIP_INDEX_URL" -r "$REQUIREMENTS_FILE" >"$PIP_LOG" 2>&1
echo $? > "${PIP_LOG}.exit"
) &
PIP_PID=$!
# 等待一小段时间,确保进程启动
# Brief pause so the process can start
sleep 0.1
# 显示进度(如果进程还在运行)
# Show progress while still running
if kill -0 "$PIP_PID" 2>/dev/null; then
show_progress "$PIP_PID" "正在安装依赖包"
show_progress "$PIP_PID" "Installing dependencies"
else
# 进程已经结束,等待一下确保退出码文件已写入
# Process already finished; wait for exit code file
sleep 0.2
fi
# 等待进程完成,忽略 wait 的退出码
# Wait for completion; ignore wait exit code
wait "$PIP_PID" 2>/dev/null || true
PIP_EXIT_CODE=0
@@ -199,74 +199,74 @@ setup_python_env() {
PIP_EXIT_CODE=$(cat "${PIP_LOG}.exit" 2>/dev/null || echo "1")
rm -f "${PIP_LOG}.exit" 2>/dev/null || true
else
# 如果没有退出码文件,检查日志中是否有错误
# No exit code file; check log for errors
if [ -f "$PIP_LOG" ] && grep -q -i "error\|failed\|exception" "$PIP_LOG" 2>/dev/null; then
PIP_EXIT_CODE=1
fi
fi
if [ $PIP_EXIT_CODE -eq 0 ]; then
success "Python 依赖安装完成"
success "Python dependencies installed"
else
# 检查是否是 angr 安装失败(需要 Rust
# Check for angr install failure (needs Rust)
if grep -q "angr" "$PIP_LOG" && grep -q "Rust compiler\|can't find Rust" "$PIP_LOG"; then
warning "angr 安装失败(需要 Rust 编译器)"
warning "angr install failed (Rust compiler required)"
echo ""
info "angr 是可选依赖,主要用于二进制分析工具"
info "如果需要使用 angr,请先安装 Rust:"
info "angr is optional and mainly used for binary analysis tools"
info "To use angr, install Rust first:"
echo " macOS: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
echo " Ubuntu: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
echo " 或访问: https://rustup.rs/"
echo " Or visit: https://rustup.rs/"
echo ""
info "其他依赖已安装,可以继续使用(部分工具可能不可用)"
info "Other dependencies are installed; you can continue (some tools may be unavailable)"
else
warning "部分 Python 依赖安装失败,但可以继续尝试运行"
warning "如果遇到问题,请检查错误信息并手动安装缺失的依赖"
# 显示最后几行错误信息
warning "Some Python dependencies failed to install, but continuing"
warning "If you hit issues, check the errors and install missing packages manually"
# Show last lines of error output
echo ""
info "错误详情(最后 10 行):"
info "Error details (last 10 lines):"
tail -n 10 "$PIP_LOG" | sed 's/^/ /'
echo ""
fi
fi
rm -f "$PIP_LOG"
else
warning "未找到 requirements.txt,跳过 Python 依赖安装"
warning "requirements.txt not found; skipping Python dependency install"
fi
}
# 构建 Go 项目
# Build Go project
build_go_project() {
echo ""
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
note "⚠️ 使用临时 Go Proxy(仅本次脚本运行有效)"
note " Proxy 地址: ${GOPROXY}"
note " 如需永久配置,请设置环境变量 GOPROXY"
note "Using temporary Go proxy (this script run only)"
note " Proxy URL: ${GOPROXY}"
note " For a permanent setting, set the GOPROXY env var"
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
info "下载 Go 依赖..."
info "Downloading Go dependencies..."
GO_DOWNLOAD_LOG=$(mktemp)
(
set +e # 在子shell中禁用错误退出
set +e # disable errexit in subshell
export GOPROXY="$GOPROXY"
go mod download >"$GO_DOWNLOAD_LOG" 2>&1
echo $? > "${GO_DOWNLOAD_LOG}.exit"
) &
GO_DOWNLOAD_PID=$!
# 等待一小段时间,确保进程启动
# Brief pause so the process can start
sleep 0.1
# 显示进度(如果进程还在运行)
# Show progress while still running
if kill -0 "$GO_DOWNLOAD_PID" 2>/dev/null; then
show_progress "$GO_DOWNLOAD_PID" "正在下载 Go 依赖"
show_progress "$GO_DOWNLOAD_PID" "Downloading Go dependencies"
else
# 进程已经结束,等待一下确保退出码文件已写入
# Process already finished; wait for exit code file
sleep 0.2
fi
# 等待进程完成,忽略 wait 的退出码
# Wait for completion; ignore wait exit code
wait "$GO_DOWNLOAD_PID" 2>/dev/null || true
GO_DOWNLOAD_EXIT_CODE=0
@@ -274,7 +274,7 @@ build_go_project() {
GO_DOWNLOAD_EXIT_CODE=$(cat "${GO_DOWNLOAD_LOG}.exit" 2>/dev/null || echo "1")
rm -f "${GO_DOWNLOAD_LOG}.exit" 2>/dev/null || true
else
# 如果没有退出码文件,检查日志中是否有错误
# No exit code file; check log for errors
if [ -f "$GO_DOWNLOAD_LOG" ] && grep -q -i "error\|failed" "$GO_DOWNLOAD_LOG" 2>/dev/null; then
GO_DOWNLOAD_EXIT_CODE=1
fi
@@ -282,33 +282,33 @@ build_go_project() {
rm -f "$GO_DOWNLOAD_LOG" 2>/dev/null || true
if [ $GO_DOWNLOAD_EXIT_CODE -ne 0 ]; then
error "Go 依赖下载失败"
error "Go dependency download failed"
exit 1
fi
success "Go 依赖下载完成"
success "Go dependencies downloaded"
info "构建项目..."
info "Building project..."
GO_BUILD_LOG=$(mktemp)
(
set +e # 在子shell中禁用错误退出
set +e # disable errexit in subshell
export GOPROXY="$GOPROXY"
go build -o "$BINARY_NAME" cmd/server/main.go >"$GO_BUILD_LOG" 2>&1
echo $? > "${GO_BUILD_LOG}.exit"
) &
GO_BUILD_PID=$!
# 等待一小段时间,确保进程启动
# Brief pause so the process can start
sleep 0.1
# 显示进度(如果进程还在运行)
# Show progress while still running
if kill -0 "$GO_BUILD_PID" 2>/dev/null; then
show_progress "$GO_BUILD_PID" "正在构建项目"
show_progress "$GO_BUILD_PID" "Building project"
else
# 进程已经结束,等待一下确保退出码文件已写入
# Process already finished; wait for exit code file
sleep 0.2
fi
# 等待进程完成,忽略 wait 的退出码
# Wait for completion; ignore wait exit code
wait "$GO_BUILD_PID" 2>/dev/null || true
GO_BUILD_EXIT_CODE=0
@@ -316,20 +316,20 @@ build_go_project() {
GO_BUILD_EXIT_CODE=$(cat "${GO_BUILD_LOG}.exit" 2>/dev/null || echo "1")
rm -f "${GO_BUILD_LOG}.exit" 2>/dev/null || true
else
# 如果没有退出码文件,检查日志中是否有错误
# No exit code file; check log for errors
if [ -f "$GO_BUILD_LOG" ] && grep -q -i "error\|failed" "$GO_BUILD_LOG" 2>/dev/null; then
GO_BUILD_EXIT_CODE=1
fi
fi
if [ $GO_BUILD_EXIT_CODE -eq 0 ]; then
success "项目构建完成: $BINARY_NAME"
success "Build complete: $BINARY_NAME"
rm -f "$GO_BUILD_LOG"
else
error "项目构建失败"
# 显示构建错误
error "Build failed"
# Show build errors
echo ""
info "构建错误详情:"
info "Build error details:"
cat "$GO_BUILD_LOG" | sed 's/^/ /'
echo ""
rm -f "$GO_BUILD_LOG"
@@ -337,24 +337,24 @@ build_go_project() {
fi
}
# 检查是否需要重新构建
# Check whether a rebuild is needed
need_rebuild() {
if [ ! -f "$BINARY_NAME" ]; then
return 0 # 需要构建
return 0 # needs build
fi
# 检查源代码是否有更新
# Check if source changed since last build
if [ "$BINARY_NAME" -ot cmd/server/main.go ] || \
[ "$BINARY_NAME" -ot go.mod ] || \
find internal cmd -name "*.go" -newer "$BINARY_NAME" 2>/dev/null | grep -q .; then
return 0 # 需要重新构建
return 0 # needs rebuild
fi
return 1 # 不需要构建
return 1 # no rebuild needed
}
# 主流程
# 默认启动主站 HTTPS--https 传给二进制);传 --http 则走明文 HTTP
# Main flow
# Default: HTTPS (--https passed to binary); --http uses plain HTTP.
main() {
USE_HTTPS=1
FORWARD_ARGS=()
@@ -366,39 +366,39 @@ main() {
FORWARD_ARGS+=("$arg")
done
# 环境检查
info "检查运行环境..."
# Environment checks
info "Checking runtime environment..."
check_python
check_go
echo ""
# 设置 Python 环境
info "设置 Python 环境..."
# Python setup
info "Setting up Python environment..."
setup_python_env
echo ""
# 构建 Go 项目
# Go build
if need_rebuild; then
info "准备构建项目..."
info "Preparing to build project..."
build_go_project
else
success "可执行文件已是最新,跳过构建"
success "Binary is up to date; skipping build"
fi
echo ""
# 启动服务器
success "所有准备工作完成!"
# Start server
success "All setup complete!"
echo ""
if [ "$USE_HTTPS" -eq 1 ]; then
info "启动 CyberStrikeAI 服务器(HTTPS + HTTP/2,自签证书)..."
note "纯 HTTP 启动请使用: $0 --http"
info "Starting CyberStrikeAI server (HTTPS + HTTP/2, self-signed cert)..."
note "For plain HTTP, use: $0 --http"
else
info "启动 CyberStrikeAI 服务器(HTTP..."
info "Starting CyberStrikeAI server (HTTP)..."
fi
echo "=========================================="
echo ""
# 始终传入项目根目录下的 config.yaml,避免 cwd 不在项目根时找不到配置;额外参数仍可追加(如再次 -config 覆盖,以 Go flag 后写为准)。
# Always pass config.yaml from project root so cwd does not matter; extra args still apply (e.g. -config override; last Go flag wins).
if [ "$USE_HTTPS" -eq 1 ]; then
if [ "${#FORWARD_ARGS[@]}" -gt 0 ]; then
exec "./$BINARY_NAME" -config "$CONFIG_FILE" --https "${FORWARD_ARGS[@]}"
@@ -414,5 +414,5 @@ main() {
fi
}
# 执行主流程(支持参数,如: ./run.sh --http
# Run main (supports args, e.g. ./run.sh --http)
main "$@"
+3 -5
View File
@@ -1371,7 +1371,6 @@
Modal
============================================================================ */
/* Toast 须高于模态遮罩 (10050),避免被 backdrop-filter 模糊 */
#c2-toast-container {
z-index: 10100 !important;
}
@@ -1379,9 +1378,7 @@
.c2-modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background: rgba(15, 23, 42, 0.52);
display: flex;
align-items: center;
justify-content: center;
@@ -1404,7 +1401,8 @@
overflow-y: auto;
box-shadow: var(--c2-shadow-lg);
border: 1px solid var(--c2-border);
animation: c2-slide-up 0.2s ease-out;
animation: c2-slide-up 0.18s ease-out;
contain: layout style paint;
}
@keyframes c2-slide-up {
+276 -26
View File
@@ -2419,6 +2419,9 @@ header {
padding-top: 12px;
border-top: 1px solid var(--border-color);
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.mcp-call-label {
@@ -2473,10 +2476,15 @@ header {
padding-top: 12px;
border-top: 1px solid var(--border-color);
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.process-details-content {
width: 100%;
min-width: 0;
max-width: 100%;
}
.process-details-content .progress-timeline {
@@ -2488,6 +2496,7 @@ header {
.process-details-content .progress-timeline.expanded {
max-height: 2000px;
overflow-x: hidden;
overflow-y: auto;
opacity: 1;
margin-top: 12px;
@@ -3326,9 +3335,9 @@ header {
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
background-color: rgba(15, 23, 42, 0.52);
overflow: auto;
animation: fadeIn 0.2s ease-in;
animation: fadeIn 0.15s ease-out;
}
.modal-content {
@@ -3343,8 +3352,9 @@ header {
flex-direction: column;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
animation: slideDown 0.3s ease-out;
animation: slideDown 0.18s ease-out;
overflow: hidden;
contain: layout style paint;
}
@keyframes slideDown {
@@ -3912,6 +3922,7 @@ header {
.progress-timeline.expanded {
max-height: 2000px;
overflow-x: hidden;
overflow-y: auto;
}
@@ -3919,7 +3930,8 @@ header {
.progress-container.is-streaming .progress-timeline.expanded,
.process-details-container.is-streaming .process-details-content .progress-timeline.expanded {
max-height: none;
overflow: visible;
overflow-x: hidden;
overflow-y: visible;
}
.timeline-item {
@@ -3930,6 +3942,10 @@ header {
background: var(--bg-secondary);
border-radius: 4px;
transition: all 0.2s;
min-width: 0;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
}
.timeline-item:hover {
@@ -4087,6 +4103,12 @@ header {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.6;
min-width: 0;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
overflow-x: auto;
box-sizing: border-box;
}
/* 流式增量阶段纯文本展示(避免半段 Markdown 反复解析) */
@@ -4095,6 +4117,37 @@ header {
word-break: break-word;
}
/* 过程详情 Markdown:避免长 URL/代码/表格撑破紫/蓝时间线条目 */
.timeline-item-content p,
.timeline-item-content li,
.timeline-item-content td,
.timeline-item-content th {
overflow-wrap: break-word;
word-break: break-word;
}
.timeline-item-content pre {
max-width: 100%;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
box-sizing: border-box;
}
.timeline-item-content code {
overflow-wrap: break-word;
word-break: break-word;
}
.timeline-item-content .table-wrapper {
max-width: 100%;
overflow-x: auto;
}
.timeline-item-content table {
max-width: 100%;
}
/* 长过程详情:跳过视口外时间线条目的布局/绘制,减轻大段工具输出时的主线程压力 */
.progress-timeline .timeline-item {
content-visibility: auto;
@@ -7196,17 +7249,68 @@ header {
stroke-width: 2;
}
.mcp-stats-timeline-empty,
.mcp-stats-timeline-error {
margin: 0;
padding: 20px 8px;
padding: 16px 8px;
text-align: center;
font-size: 0.75rem;
color: var(--text-muted);
color: #b91c1c;
}
.mcp-stats-timeline-error {
color: #b91c1c;
.mcp-stats-timeline-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
flex: 1;
min-height: 88px;
padding: 20px 16px;
text-align: center;
border-radius: 8px;
background: rgba(148, 163, 184, 0.06);
border: 1px dashed rgba(148, 163, 184, 0.28);
}
.mcp-stats-timeline-empty-state--compact {
min-height: 72px;
padding: 14px 10px;
gap: 4px;
}
.mcp-stats-timeline-empty-state__icon {
color: rgba(148, 163, 184, 0.75);
flex-shrink: 0;
}
.mcp-stats-timeline-empty-state--compact .mcp-stats-timeline-empty-state__icon {
width: 28px;
height: 28px;
}
.mcp-stats-timeline-empty-state__title {
margin: 0;
font-size: 0.8125rem;
font-weight: 500;
color: var(--text-secondary);
line-height: 1.4;
}
.mcp-stats-timeline-empty-state--compact .mcp-stats-timeline-empty-state__title {
font-size: 0.75rem;
}
.mcp-stats-timeline-empty-state__hint {
margin: 0;
max-width: 28em;
font-size: 0.6875rem;
color: var(--text-muted);
line-height: 1.45;
}
.mcp-stats-timeline-empty-state--compact .mcp-stats-timeline-empty-state__hint {
font-size: 0.625rem;
max-width: 100%;
}
.mcp-stats-timeline-tooltip {
@@ -14597,6 +14701,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
}
.webshell-ai-process-block .process-details-content .progress-timeline.expanded {
max-height: 2000px;
overflow-x: hidden;
overflow-y: auto;
}
@@ -14828,6 +14933,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
position: relative;
flex-shrink: 0;
}
.ws-project-selector-wrapper {
position: relative;
flex-shrink: 0;
}
.ws-agent-mode-wrapper {
flex-shrink: 0;
}
@@ -19203,6 +19312,12 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
display: flex;
flex-direction: column;
gap: 6px;
min-height: 0;
overflow-y: auto;
}
.agent-mode-options > .role-selection-item-main {
flex-shrink: 0;
}
/* 选项为 <button>,浏览器默认 text-align:center 会继承到文案,强制左对齐与角色列表一致 */
@@ -19234,7 +19349,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
left: 0;
width: 340px;
max-width: calc(100vw - 32px);
max-height: 60vh;
max-height: min(580px, 60vh);
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 16px;
@@ -19244,6 +19359,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
animation: slideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
@@ -19286,11 +19402,17 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
gap: 6px;
/* 限制显示8个角色:每个角色约70px高度 + gap8个角色约580px */
max-height: 580px;
min-height: 0;
overflow-y: auto;
padding-right: 6px;
flex: 1;
}
/* 防止 flex 列容器在高度受限时把列表项纵向压扁(应滚动而非压缩) */
.role-selection-list-main > .role-selection-item-main {
flex-shrink: 0;
}
.role-selection-list-main::-webkit-scrollbar {
width: 8px;
}
@@ -19323,6 +19445,9 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
min-width: 0;
flex-shrink: 0;
box-sizing: border-box;
}
.role-selection-item-main:hover {
@@ -19385,6 +19510,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
margin: 0;
transition: color 0.2s cubic-bezier(0.16, 1, 0.3, 1);
letter-spacing: -0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: anywhere;
}
.role-selection-item-main.selected .role-selection-item-name-main {
@@ -19392,6 +19521,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
font-weight: 600;
}
.role-selection-item-main.selected .role-selection-item-content-main {
padding-right: 24px;
}
.role-selection-item-description-main {
font-size: 0.75rem;
color: #666666;
@@ -22463,7 +22596,10 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
}
.projects-list-item {
position: relative;
padding: 10px 12px 10px 14px;
display: flex;
align-items: center;
gap: 4px;
padding: 10px 8px 10px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 0.875rem;
@@ -22496,8 +22632,43 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
color: #94a3b8;
}
.projects-list-item-body {
flex: 1;
min-width: 0;
}
.projects-list-item-menu {
width: 24px;
height: 24px;
padding: 0;
border: none;
background: transparent;
color: var(--text-muted, #94a3b8);
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 16px;
font-weight: 600;
line-height: 1;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.projects-list-item:hover .projects-list-item-menu,
.projects-list-item.is-active .projects-list-item-menu {
opacity: 0.75;
}
.projects-list-item-menu:hover,
.projects-list-item-menu:focus-visible {
opacity: 1;
background: #e2e8f0;
color: var(--text-primary, #0f172a);
outline: none;
}
.projects-list-item.is-active .projects-list-item-menu:hover,
.projects-list-item.is-active .projects-list-item-menu:focus-visible {
background: #dbeafe;
}
.projects-list-item-name {
font-weight: 600;
color: var(--text-primary, #0f172a);
@@ -22592,21 +22763,38 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-detail-header-main {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.projects-detail-title-row {
.projects-detail-headline {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 12px;
min-width: 0;
}
.projects-detail-title-group {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
max-width: min(560px, 100%);
}
.projects-detail-title {
margin: 0;
min-width: 0;
font-size: 1.375rem;
font-weight: 600;
color: #0f172a;
letter-spacing: -0.02em;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.projects-status-pill {
flex-shrink: 0;
display: inline-flex;
align-items: center;
font-size: 0.6875rem;
@@ -22614,41 +22802,76 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding: 3px 10px;
border-radius: 999px;
line-height: 1.2;
border: 1px solid transparent;
}
.projects-status-pill--active {
background: #dcfce7;
color: #166534;
border-color: #86efac;
}
.projects-status-pill--archived {
background: #f1f5f9;
color: #64748b;
border-color: #e2e8f0;
}
.projects-detail-meta {
margin: 6px 0 0;
margin: 0;
font-size: 0.8125rem;
color: #94a3b8;
line-height: 1.4;
}
.projects-detail-desc {
margin: 10px 0 0;
margin: 0;
max-width: min(640px, 100%);
font-size: 0.875rem;
color: #475569;
line-height: 1.55;
max-width: 640px;
word-break: break-word;
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
}
.projects-description-textarea {
max-height: 200px;
resize: vertical;
overflow-y: auto;
}
.projects-detail-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
align-items: center;
gap: 6px;
flex-shrink: 0;
padding-left: 12px;
border-left: 1px solid #e2e8f0;
}
.projects-stat-chip {
font-size: 0.75rem;
font-size: 0.6875rem;
font-weight: 500;
color: #475569;
background: #f1f5f9;
border: 1px solid #e2e8f0;
padding: 4px 10px;
padding: 3px 8px;
border-radius: 999px;
white-space: nowrap;
}
.projects-stat-chip--facts {
color: #1d4ed8;
background: #dbeafe;
border-color: #93c5fd;
}
.projects-stat-chip--vulns {
color: #c2410c;
background: #ffedd5;
border-color: #fdba74;
}
.projects-stat-chip--conversations {
color: #6d28d9;
background: #ede9fe;
border-color: #c4b5fd;
}
.projects-stat-chip--warn {
color: #92400e;
@@ -22660,7 +22883,23 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: flex-start;
align-items: center;
align-self: flex-start;
margin-top: 2px;
}
@media (max-width: 860px) {
.projects-detail-header {
flex-direction: column;
align-items: stretch;
}
.projects-detail-header-actions {
align-self: stretch;
margin-top: 0;
}
.projects-detail-stats {
padding-left: 0;
border-left: none;
}
}
.projects-tabs {
display: flex;
@@ -23621,10 +23860,8 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
justify-content: center;
padding: 24px 16px;
box-sizing: border-box;
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
animation: projectsOverlayIn 0.2s ease-out;
background: rgba(15, 23, 42, 0.52);
animation: projectsOverlayIn 0.15s ease-out;
}
@keyframes projectsOverlayIn {
from { opacity: 0; }
@@ -23642,7 +23879,8 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
0 24px 48px rgba(15, 23, 42, 0.18),
0 0 0 1px rgba(15, 23, 42, 0.06);
overflow: hidden;
animation: projectsDialogIn 0.25s cubic-bezier(0.22, 1, 0.36, 1);
animation: projectsDialogIn 0.18s cubic-bezier(0.22, 1, 0.36, 1);
contain: layout style paint;
}
.projects-modal-dialog--wide {
max-width: 640px;
@@ -23703,6 +23941,13 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding: 10px 12px;
font-size: 0.875rem;
transition: border-color 0.15s, box-shadow 0.15s;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
#project-modal-name {
overflow: hidden;
text-overflow: ellipsis;
}
.projects-modal-body .form-input:focus {
outline: none;
@@ -23726,7 +23971,8 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-modal-footer .btn-primary {
min-width: 100px;
}
body.projects-modal-open {
body.projects-modal-open,
body.app-modal-open {
overflow: hidden;
}
.fact-detail-prev-wrap {
@@ -23874,8 +24120,11 @@ body.projects-modal-open {
/* 对话区项目选择器(与角色/代理模式共用 role-selector-* */
.project-selector-wrapper .role-selector-text {
max-width: 108px;
min-width: 0;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-project-panel {
width: 280px;
@@ -23895,6 +24144,7 @@ body.projects-modal-open {
padding-right: 0;
margin: 0;
width: 100%;
overflow-x: hidden;
}
.chat-project-panel .role-selection-item-main {
width: 100%;
+10
View File
@@ -286,6 +286,8 @@
"status": "Status",
"modalNewTitle": "New project",
"modalNewSubtitle": "After creation, bind conversations to share fact board across chats",
"modalEditTitle": "Edit project",
"modalEditSubtitle": "Update project name and description",
"projectName": "Project name",
"projectNamePlaceholder": "e.g. Client A Web pentest",
"projectDescription": "Project description",
@@ -324,6 +326,9 @@
"statsSparse": "{{count}} incomplete",
"projectNotFound": "Project not found",
"updatedPrefix": "Updated {{time}}",
"descExpand": "Show all",
"descCollapse": "Show less",
"descriptionLengthHint": "Keep it brief (max 4000 chars). Put long logs/POCs in fact board body instead.",
"noMatchingFacts": "No matching facts, try adjusting filters",
"noFacts": "No facts yet. Click Add fact or let Agent write facts automatically",
"relatedVulnIdTitle": "Related vulnerability ID",
@@ -407,6 +412,10 @@
"dangerZoneTitle": "Danger zone",
"dangerZoneHint": "Archived projects are hidden unless 'Show archived' is enabled; deletion removes all facts permanently.",
"archiveRestore": "Archive / Restore",
"archiveProject": "Archive",
"editProject": "Edit",
"restoreProjectActive": "Restore to active",
"projectActions": "Project actions",
"deleteProject": "Delete project",
"saveChangesHint": "Click save to sync changes to server",
"saveSettings": "Save changes",
@@ -1583,6 +1592,7 @@
"timelineSummary": "{{total}} calls in range · peak {{peak}}",
"timelineSparseHint": "Most buckets are empty; peak {{peak}} calls at {{peakTime}}",
"timelineNoData": "No calls in this period",
"timelineEmptyHint": "Switch the time range or invoke MCP tools in chat or tasks",
"timelineLoadError": "Failed to load call trend",
"timelineTotalLegend": "Total calls",
"timelineFailedLegend": "Failed",
+10
View File
@@ -274,6 +274,8 @@
"status": "状态",
"modalNewTitle": "新建项目",
"modalNewSubtitle": "创建后可绑定对话,跨会话共享事实黑板",
"modalEditTitle": "编辑项目",
"modalEditSubtitle": "修改项目名称与描述",
"projectName": "项目名称",
"projectNamePlaceholder": "例如:某客户 Web 渗透",
"projectDescription": "项目描述",
@@ -312,6 +314,9 @@
"statsSparse": "{{count}} 待补全",
"projectNotFound": "项目不存在",
"updatedPrefix": "更新于 {{time}}",
"descExpand": "展开全部",
"descCollapse": "收起",
"descriptionLengthHint": "简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body",
"noMatchingFacts": "无匹配事实,请调整筛选条件",
"noFacts": "暂无事实,点击「添加事实」或由 Agent 自动写入",
"relatedVulnIdTitle": "关联漏洞 ID",
@@ -395,6 +400,10 @@
"dangerZoneTitle": "危险操作",
"dangerZoneHint": "归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。",
"archiveRestore": "归档 / 恢复",
"archiveProject": "归档",
"editProject": "编辑",
"restoreProjectActive": "恢复为进行中",
"projectActions": "项目操作",
"deleteProject": "删除项目",
"saveChangesHint": "修改后请点击保存以同步到服务器",
"saveSettings": "保存更改",
@@ -1571,6 +1580,7 @@
"timelineSummary": "区间内 {{total}} 次 · 峰值 {{peak}}",
"timelineSparseHint": "该时段多数时间为 0,峰值 {{peak}} 次出现在 {{peakTime}}",
"timelineNoData": "该时段暂无调用",
"timelineEmptyHint": "切换时间范围查看其他时段,或在对话/任务中调用 MCP 工具",
"timelineLoadError": "无法加载调用趋势",
"timelineTotalLegend": "总调用",
"timelineFailedLegend": "失败",
+20 -17
View File
@@ -105,45 +105,48 @@ function showAddMarkdownAgentModal() {
document.getElementById('agent-md-bind-role').value = '';
document.getElementById('agent-md-max-iter').value = '0';
document.getElementById('agent-md-instruction').value = '';
if (modal) modal.style.display = 'flex';
openAppModal('agent-md-modal');
}
async function editMarkdownAgent(filename) {
if (!filename) return;
const modal = document.getElementById('agent-md-modal');
const title = document.getElementById('agent-md-modal-title');
const row = document.getElementById('agent-md-filename-row');
markdownAgentsEditingFilename = null;
markdownAgentsEditingIsOrchestrator = false;
if (title) title.textContent = _agentsT('agentsPage.editTitle');
if (row) row.style.display = 'none';
document.getElementById('agent-md-instruction').value = '';
openAppModal('agent-md-modal', { focus: false });
try {
const r = await apiFetch('/api/multi-agent/markdown-agents/' + encodeURIComponent(filename));
const data = await r.json();
if (!r.ok) throw new Error(data.error || r.statusText);
markdownAgentsEditingFilename = data.filename || filename;
markdownAgentsEditingIsOrchestrator = !!data.is_orchestrator;
document.getElementById('agent-md-filename-current').value = data.filename || filename;
document.getElementById('agent-md-filename').value = data.filename || filename;
document.getElementById('agent-md-filename').disabled = true;
var roleEl2 = document.getElementById('agent-md-role');
if (roleEl2) roleEl2.value = data.is_orchestrator ? 'orchestrator' : 'sub';
document.getElementById('agent-md-id').value = data.id || '';
document.getElementById('agent-md-name').value = data.name || '';
document.getElementById('agent-md-description').value = data.description || '';
document.getElementById('agent-md-tools').value = Array.isArray(data.tools) ? data.tools.join(', ') : '';
document.getElementById('agent-md-bind-role').value = data.bind_role || '';
document.getElementById('agent-md-max-iter').value = String(data.max_iterations != null ? data.max_iterations : 0);
document.getElementById('agent-md-instruction').value = data.instruction || '';
if (modal) modal.style.display = 'flex';
deferModalContent(function () {
document.getElementById('agent-md-filename-current').value = data.filename || filename;
document.getElementById('agent-md-filename').value = data.filename || filename;
document.getElementById('agent-md-filename').disabled = true;
var roleEl2 = document.getElementById('agent-md-role');
if (roleEl2) roleEl2.value = data.is_orchestrator ? 'orchestrator' : 'sub';
document.getElementById('agent-md-id').value = data.id || '';
document.getElementById('agent-md-name').value = data.name || '';
document.getElementById('agent-md-description').value = data.description || '';
document.getElementById('agent-md-tools').value = Array.isArray(data.tools) ? data.tools.join(', ') : '';
document.getElementById('agent-md-bind-role').value = data.bind_role || '';
document.getElementById('agent-md-max-iter').value = String(data.max_iterations != null ? data.max_iterations : 0);
document.getElementById('agent-md-instruction').value = data.instruction || '';
document.getElementById('agent-md-name')?.focus();
});
} catch (e) {
closeMarkdownAgentModal();
showNotification(_agentsT('agentsPage.loadOneFailed') + ': ' + e.message, 'error');
}
}
function closeMarkdownAgentModal() {
const modal = document.getElementById('agent-md-modal');
if (modal) modal.style.display = 'none';
closeAppModal('agent-md-modal');
markdownAgentsEditingFilename = null;
markdownAgentsEditingIsOrchestrator = false;
}
+38 -33
View File
@@ -533,56 +533,61 @@ async function exportAuditLogsCsv() {
}
function closeAuditDetailModal() {
closeAppModal('audit-detail-modal');
const el = document.getElementById('audit-detail-modal');
if (el) el.remove();
syncAppModalBodyLock();
}
async function showAuditLogDetail(id) {
if (!id || typeof apiFetch !== 'function') return;
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
try {
closeAuditDetailModal();
const overlay = document.createElement('div');
overlay.id = 'audit-detail-modal';
overlay.className = 'modal';
document.body.appendChild(overlay);
openAppModal(overlay, { focus: false });
const r = await apiFetch('/api/audit/logs/' + encodeURIComponent(id));
if (!r.ok) throw new Error('not found');
const data = await r.json();
const log = data.log || {};
const detail = log.detail ? JSON.stringify(log.detail, null, 2) : '';
closeAuditDetailModal();
const overlay = document.createElement('div');
overlay.id = 'audit-detail-modal';
overlay.className = 'modal';
overlay.style.display = 'block';
const catAction = esc(auditCategoryLabel(log.category || '')) + ' / ' + esc(auditActionLabel(log.action || ''));
overlay.innerHTML =
'<div class="modal-content" style="max-width: 720px;">' +
'<div class="modal-header">' +
'<h2>' + esc(auditT('settingsAudit.detailTitle', null, '审计详情')) + '</h2>' +
'<span class="modal-close" onclick="closeAuditDetailModal()">&times;</span>' +
'</div>' +
'<div class="modal-body audit-detail-body">' +
'<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(log.result || '') + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(log.message || '') + '</p>' +
(log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') +
(log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') +
(log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') +
auditResourceMeta(log) +
(detail ? '<pre class="audit-detail-pre">' + esc(detail) + '</pre>' : '') +
'</div>' +
'<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAuditDetailModal()">' +
esc(auditT('common.close', null, '关闭')) + '</button></div>' +
'</div>';
document.body.appendChild(overlay);
const chatBtn = overlay.querySelector('.audit-open-chat-btn');
if (chatBtn) {
chatBtn.addEventListener('click', function () {
auditOpenConversationChat(chatBtn.getAttribute('data-conversation-id'));
deferModalContent(function () {
overlay.innerHTML =
'<div class="modal-content" style="max-width: 720px;">' +
'<div class="modal-header">' +
'<h2>' + esc(auditT('settingsAudit.detailTitle', null, '审计详情')) + '</h2>' +
'<span class="modal-close" onclick="closeAuditDetailModal()">&times;</span>' +
'</div>' +
'<div class="modal-body audit-detail-body">' +
'<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(log.result || '') + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(log.message || '') + '</p>' +
(log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') +
(log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') +
(log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') +
auditResourceMeta(log) +
(detail ? '<pre class="audit-detail-pre">' + esc(detail) + '</pre>' : '') +
'</div>' +
'<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAuditDetailModal()">' +
esc(auditT('common.close', null, '关闭')) + '</button></div>' +
'</div>';
const chatBtn = overlay.querySelector('.audit-open-chat-btn');
if (chatBtn) {
chatBtn.addEventListener('click', function () {
auditOpenConversationChat(chatBtn.getAttribute('data-conversation-id'));
});
}
overlay.addEventListener('click', function (ev) {
if (ev.target === overlay) closeAuditDetailModal();
});
}
overlay.addEventListener('click', function (ev) {
if (ev.target === overlay) closeAuditDetailModal();
});
} catch (e) {
closeAuditDetailModal();
if (typeof showToast === 'function') {
showToast(e.message || String(e), 'error');
}
+3 -5
View File
@@ -72,7 +72,7 @@ function showLoginOverlay(message = '') {
if (!overlay) {
return;
}
overlay.style.display = 'flex';
openAppModal('login-overlay', { focus: false });
if (errorBox) {
if (message) {
errorBox.textContent = message;
@@ -82,7 +82,7 @@ function showLoginOverlay(message = '') {
errorBox.style.display = 'none';
}
}
setTimeout(() => {
setTimeout(function () {
if (passwordInput) {
passwordInput.focus();
}
@@ -93,9 +93,7 @@ function hideLoginOverlay() {
const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error');
const passwordInput = document.getElementById('login-password');
if (overlay) {
overlay.style.display = 'none';
}
closeAppModal('login-overlay');
if (errorBox) {
errorBox.textContent = '';
errorBox.style.display = 'none';
+5 -5
View File
@@ -478,7 +478,7 @@
const content = document.getElementById('c2-modal-content');
if (!content || !modal) return;
modal.style.display = 'flex';
openAppModal(modal);
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}</h3>
@@ -635,7 +635,7 @@
const content = document.getElementById('c2-modal-content');
if (!content || !modal) return;
modal.style.display = 'flex';
openAppModal(modal);
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.editTitle'))}</h3>
@@ -2376,7 +2376,7 @@
<button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.close'))}</button>
</div>
`;
modal.style.display = 'flex';
openAppModal(modal);
};
const local = C2.tasks.find(x => x.id === id);
@@ -2920,7 +2920,7 @@
<button class="btn-primary" onclick="C2.createProfile()">${escapeHtml(c2t('c2.profiles.submitCreate'))}</button>
</div>
`;
modal.style.display = 'flex';
openAppModal(modal);
};
C2.createProfile = function() {
@@ -2981,10 +2981,10 @@
C2.closeModal = function() {
const modal = document.getElementById('c2-modal');
if (modal) {
modal.style.display = 'none';
const modalBox = modal.querySelector('.c2-modal');
if (modalBox) modalBox.classList.remove('c2-modal--wide');
}
closeAppModal('c2-modal');
};
// ============================================================================
+12 -11
View File
@@ -1002,7 +1002,7 @@ async function openChatFilesEdit(relativePath) {
const modal = document.getElementById('chat-files-edit-modal');
if (pathEl) pathEl.textContent = relativePath;
if (ta) ta.value = '';
if (modal) modal.style.display = 'block';
openAppModal('chat-files-edit-modal', { focus: false });
try {
const res = await apiFetch('/api/chat-uploads/content?path=' + encodeURIComponent(relativePath));
@@ -1017,16 +1017,19 @@ async function openChatFilesEdit(relativePath) {
throw new Error(errText || res.status);
}
const data = await res.json();
if (ta) ta.value = data.content != null ? String(data.content) : '';
const content = data.content != null ? String(data.content) : '';
deferModalContent(() => {
if (ta) ta.value = content;
ta?.focus();
});
} catch (e) {
if (modal) modal.style.display = 'none';
closeAppModal('chat-files-edit-modal');
alert(chatFilesAlertMessage(e && e.message));
}
}
function closeChatFilesEditModal() {
const modal = document.getElementById('chat-files-edit-modal');
if (modal) modal.style.display = 'none';
closeAppModal('chat-files-edit-modal');
chatFilesEditRelativePath = '';
}
@@ -1060,7 +1063,7 @@ function openChatFilesRename(relativePath, currentName) {
input.value = currentName || '';
input.select();
}
if (modal) modal.style.display = 'flex';
if (modal) openAppModal(modal);
if (modal && typeof window.applyTranslations === 'function') {
window.applyTranslations(modal);
}
@@ -1068,8 +1071,7 @@ function openChatFilesRename(relativePath, currentName) {
}
function closeChatFilesRenameModal() {
const modal = document.getElementById('chat-files-rename-modal');
if (modal) modal.style.display = 'none';
closeAppModal('chat-files-rename-modal');
const hint = document.getElementById('chat-files-rename-path-hint');
if (hint) hint.textContent = '';
chatFilesRenameRelativePath = '';
@@ -1106,7 +1108,7 @@ function openChatFilesMkdirModal() {
const p = chatFilesBrowsePath.join('/');
if (hint) hint.textContent = p ? ('chat_uploads/' + p) : 'chat_uploads';
if (input) input.value = '';
if (modal) modal.style.display = 'flex';
if (modal) openAppModal(modal);
if (modal && typeof window.applyTranslations === 'function') {
window.applyTranslations(modal);
}
@@ -1116,8 +1118,7 @@ function openChatFilesMkdirModal() {
}
function closeChatFilesMkdirModal() {
const modal = document.getElementById('chat-files-mkdir-modal');
if (modal) modal.style.display = 'none';
closeAppModal('chat-files-mkdir-modal');
const input = document.getElementById('chat-files-mkdir-input');
if (input) input.value = '';
}
+145 -56
View File
@@ -2164,6 +2164,97 @@ function showCopySuccess(button) {
}
}
/** Claude extended thinking 内部尾缀(与后端 DisplayReasoningContent 一致,UI 不展示) */
const CLAUDE_REASONING_UI_SUFFIX = '\n---CSAI_CLAUDE_THINKING_BLOCKS---\n';
function normalizeReasoningContentForDisplay(text) {
if (text == null) return '';
let s = String(text).trim();
if (!s) return '';
const idx = s.lastIndexOf(CLAUDE_REASONING_UI_SUFFIX);
if (idx >= 0) {
s = s.slice(0, idx).trim();
}
return s;
}
function setMessageReasoningContent(messageIdOrEl, reasoningContent) {
const el = typeof messageIdOrEl === 'string' ? document.getElementById(messageIdOrEl) : messageIdOrEl;
if (!el || !el.dataset) return;
const rc = normalizeReasoningContentForDisplay(reasoningContent);
if (rc) {
el.dataset.reasoningContent = rc;
} else {
delete el.dataset.reasoningContent;
}
}
function getMessageReasoningContent(messageIdOrEl) {
const el = typeof messageIdOrEl === 'string' ? document.getElementById(messageIdOrEl) : messageIdOrEl;
if (!el || !el.dataset) return '';
return normalizeReasoningContentForDisplay(el.dataset.reasoningContent || '');
}
function reasoningTextAlreadyInProcessDetails(processDetails, rc) {
if (!rc) return true;
const list = Array.isArray(processDetails) ? processDetails : [];
for (let i = 0; i < list.length; i++) {
const d = list[i];
if (!d) continue;
const et = d.eventType || '';
if (et !== 'reasoning_chain' && et !== 'thinking') continue;
const msg = normalizeReasoningContentForDisplay(d.message || '');
if (!msg) continue;
if (msg === rc || msg.includes(rc) || rc.includes(msg)) {
return true;
}
}
return false;
}
/** 合并 messages.reasoningContent 与 process_details 中的 reasoning_chain,两者都读、都展示(去重后) */
function mergeMessageReasoningContentIntoProcessDetails(processDetails, reasoningContent) {
const rc = normalizeReasoningContentForDisplay(reasoningContent);
const details = Array.isArray(processDetails) ? processDetails.slice() : [];
if (!rc || reasoningTextAlreadyInProcessDetails(details, rc)) {
return details;
}
details.push({
eventType: 'reasoning_chain',
message: rc,
data: { source: 'message.reasoningContent' }
});
return details;
}
async function syncAssistantReasoningContentFromServer(backendMessageId, domAssistantId) {
if (!backendMessageId || !domAssistantId || !currentConversationId || typeof apiFetch !== 'function') {
return;
}
try {
const convRes = await apiFetch(`/api/conversations/${encodeURIComponent(currentConversationId)}?include_process_details=0`);
const conv = await convRes.json().catch(() => ({}));
if (!convRes.ok || !Array.isArray(conv.messages)) return;
const msg = conv.messages.find((m) => m && String(m.id) === String(backendMessageId));
if (!msg || !msg.reasoningContent) return;
setMessageReasoningContent(domAssistantId, msg.reasoningContent);
const pdRes = await apiFetch(`/api/messages/${encodeURIComponent(String(backendMessageId))}/process-details`);
const pdJson = await pdRes.json().catch(() => ({}));
const details = pdRes.ok && Array.isArray(pdJson.processDetails) ? pdJson.processDetails : [];
if (typeof renderProcessDetails === 'function') {
renderProcessDetails(domAssistantId, details);
}
} catch (e) {
console.warn('syncAssistantReasoningContentFromServer failed', e);
}
}
window.normalizeReasoningContentForDisplay = normalizeReasoningContentForDisplay;
window.setMessageReasoningContent = setMessageReasoningContent;
window.getMessageReasoningContent = getMessageReasoningContent;
window.mergeMessageReasoningContentIntoProcessDetails = mergeMessageReasoningContentIntoProcessDetails;
window.syncAssistantReasoningContentFromServer = syncAssistantReasoningContentFromServer;
/** 相邻且类型/正文/data 完全一致的过程详情只保留一条(与后端去重一致,避免时间线叠多条相同块) */
function dedupeConsecutiveProcessDetailRows(details) {
if (!Array.isArray(details) || details.length < 2) {
@@ -2282,20 +2373,27 @@ function renderProcessDetails(messageId, processDetails) {
detailsContainer.appendChild(contentDiv);
}
// processDetails === null 表示“尚未加载(懒加载)”
// processDetails === null 表示“尚未加载(懒加载)”messages.reasoningContent 可先展示
const isLazyNotLoaded = (processDetails === null);
if (isLazyNotLoaded) {
const reasoningFromMessage = getMessageReasoningContent(messageElement);
if (isLazyNotLoaded && !reasoningFromMessage) {
detailsContainer.dataset.lazyNotLoaded = '1';
detailsContainer.dataset.loaded = '0';
timeline.innerHTML = '<div class="progress-timeline-empty">' +
(typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
'(点击后加载)</div>';
// 默认折叠
timeline.classList.remove('expanded');
return;
}
detailsContainer.dataset.lazyNotLoaded = '0';
detailsContainer.dataset.loaded = '1';
if (isLazyNotLoaded) {
detailsContainer.dataset.lazyNotLoaded = '1';
detailsContainer.dataset.loaded = '0';
processDetails = [];
} else {
detailsContainer.dataset.lazyNotLoaded = '0';
detailsContainer.dataset.loaded = '1';
}
processDetails = mergeMessageReasoningContentIntoProcessDetails(processDetails, reasoningFromMessage);
processDetails = dedupeConsecutiveProcessDetailRows(processDetails);
if (typeof window.coalesceProcessDetailsToolPairs === 'function') {
processDetails = window.coalesceProcessDetailsToolPairs(processDetails);
@@ -2426,6 +2524,14 @@ function renderProcessDetails(messageId, processDetails) {
}
addTimelineItem(timeline, eventType, timelineOpts);
});
if (isLazyNotLoaded && reasoningFromMessage) {
const lazyHint = document.createElement('div');
lazyHint.className = 'progress-timeline-empty progress-timeline-lazy-hint';
lazyHint.textContent = (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
'(点击后加载完整过程详情)';
timeline.appendChild(lazyHint);
}
// 检查是否有错误或取消事件,如果有,确保详情默认折叠(但仍有待审批 HITL 时保持展开,由 restoreHitlInlineForConversation 处理)
const hasPendingHitlInDetails = processDetails.some(d => d && d.eventType === 'hitl_interrupt');
@@ -2535,10 +2641,17 @@ async function batchUpdateButtonToolNames(buttonsContainer, executionIds) {
// 显示MCP调用详情
async function showMCPDetail(executionId) {
try {
openAppModal('mcp-detail-modal', { focus: false });
const response = await apiFetch(`/api/monitor/execution/${executionId}`);
const exec = await response.json();
if (response.ok) {
if (!response.ok) {
closeMCPDetail();
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + (exec.error || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : '未知错误')));
return;
}
deferModalContent(function () {
// 填充模态框内容
document.getElementById('detail-tool-name').textContent = exec.toolName || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : 'Unknown');
document.getElementById('detail-execution-id').textContent = exec.id || 'N/A';
@@ -2645,20 +2758,16 @@ async function showMCPDetail(executionId) {
delete abortBtn.dataset.execId;
}
}
// 显示模态框
document.getElementById('mcp-detail-modal').style.display = 'block';
} else {
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + (exec.error || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : '未知错误')));
}
});
} catch (error) {
closeMCPDetail();
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + error.message);
}
}
// 关闭MCP详情模态框
function closeMCPDetail() {
document.getElementById('mcp-detail-modal').style.display = 'none';
closeAppModal('mcp-detail-modal');
}
/** 从详情模态框触发:取消当前进行中的 MCP 工具调用 */
@@ -2682,18 +2791,12 @@ function openMcpToolAbortModal(executionId, options = {}) {
if (ta) {
ta.value = '';
}
const m = document.getElementById('mcp-tool-abort-modal');
if (m) {
m.style.display = 'block';
}
openAppModal('mcp-tool-abort-modal');
}
function closeMcpToolAbortModal() {
window.__mcpToolAbortContext = null;
const m = document.getElementById('mcp-tool-abort-modal');
if (m) {
m.style.display = 'none';
}
closeAppModal('mcp-tool-abort-modal');
}
async function submitMcpToolAbortModal() {
@@ -2846,10 +2949,12 @@ async function startNewConversation() {
} catch (e) { /* ignore */ }
currentConversationGroupId = null; // 新对话不属于任何分组
if (typeof ensureDefaultActiveProjectForNewChat === 'function') {
ensureDefaultActiveProjectForNewChat().catch(() => {});
try {
await ensureDefaultActiveProjectForNewChat();
} catch (e) { /* ignore */ }
}
if (typeof refreshChatProjectSelector === 'function') {
refreshChatProjectSelector();
await refreshChatProjectSelector();
}
document.getElementById('chat-messages').innerHTML = '';
const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
@@ -3125,7 +3230,7 @@ async function loadConversation(conversationId) {
// 如果攻击链模态框打开且显示的不是当前对话,关闭它
const attackChainModal = document.getElementById('attack-chain-modal');
if (attackChainModal && attackChainModal.style.display === 'block') {
if (attackChainModal && isAppModalOpen('attack-chain-modal')) {
if (currentAttackChainConversationId !== conversationId) {
closeAttackChainModal();
}
@@ -3194,6 +3299,9 @@ async function loadConversation(conversationId) {
attachDeleteTurnButton(messageEl);
}
if (msg.role === 'assistant') {
if (messageEl && msg.reasoningContent) {
setMessageReasoningContent(messageEl, msg.reasoningContent);
}
const hasField = msg && Object.prototype.hasOwnProperty.call(msg, 'processDetails');
renderProcessDetails(messageId, hasField ? (msg.processDetails || []) : null);
if (msg.processDetails && msg.processDetails.length > 0) {
@@ -3415,7 +3523,7 @@ async function deleteConversation(conversationId, skipConfirm = false) {
// 批量管理弹窗打开时,同步刷新弹窗内列表
const batchModal = document.getElementById('batch-manage-modal');
if (batchModal && batchModal.style.display === 'flex') {
if (batchModal && isAppModalOpen('batch-manage-modal')) {
allConversationsForBatch = allConversationsForBatch.filter(c => c.id !== conversationId);
updateBatchManageTitle(allConversationsForBatch.length);
const searchInput = document.getElementById('batch-search-input');
@@ -3522,7 +3630,7 @@ async function showAttackChain(conversationId) {
if (isAttackChainLoading(conversationId) && currentAttackChainConversationId === conversationId) {
// 如果模态框已经打开且显示的是同一个对话,不重复打开
const modal = document.getElementById('attack-chain-modal');
if (modal && modal.style.display === 'block') {
if (modal && isAppModalOpen('attack-chain-modal')) {
console.log('攻击链正在加载中,模态框已打开');
return;
}
@@ -3535,8 +3643,7 @@ async function showAttackChain(conversationId) {
return;
}
modal.style.display = 'block';
// 打开时立即按当前语言刷新统计(避免红框内仍显示硬编码中文)
openAppModal('attack-chain-modal', { focus: false });
updateAttackChainStats({ nodes: [], edges: [] });
// 清空容器
@@ -4668,10 +4775,7 @@ function closeNodeDetails() {
// 关闭攻击链模态框
function closeAttackChainModal() {
const modal = document.getElementById('attack-chain-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('attack-chain-modal');
// 关闭节点详情
closeNodeDetails();
@@ -7214,19 +7318,14 @@ async function showBatchManageModal() {
updateBatchManageTitle(allConversationsForBatch.length);
renderBatchConversations();
if (modal) {
modal.style.display = 'flex';
}
openAppModal('batch-manage-modal');
} catch (error) {
console.error('加载对话列表失败:', error);
// 错误时使用空数组,不显示错误提示(更友好的用户体验)
allConversationsForBatch = [];
const modal = document.getElementById('batch-manage-modal');
updateBatchManageTitle(0);
if (modal) {
renderBatchConversations();
modal.style.display = 'flex';
}
renderBatchConversations();
openAppModal('batch-manage-modal');
}
}
@@ -7381,10 +7480,7 @@ async function deleteSelectedConversations() {
// 关闭批量管理模态框
function closeBatchManageModal() {
const modal = document.getElementById('batch-manage-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('batch-manage-modal');
const selectAll = document.getElementById('batch-select-all');
if (selectAll) {
selectAll.checked = false;
@@ -7424,8 +7520,7 @@ function refreshChatPanelI18n() {
});
}
const mcpModal = document.getElementById('mcp-detail-modal');
if (mcpModal && mcpModal.style.display === 'block') {
if (isAppModalOpen('mcp-detail-modal')) {
const detailTimeEl = document.getElementById('detail-time');
if (detailTimeEl && detailTimeEl.dataset.detailTimeIso) {
try {
@@ -7447,7 +7542,7 @@ document.addEventListener('languagechange', function () {
refreshSystemReadyMessageBubbles();
refreshChatPanelI18n();
const modal = document.getElementById('batch-manage-modal');
if (modal && modal.style.display === 'flex') {
if (isAppModalOpen('batch-manage-modal')) {
updateBatchManageTitle(allConversationsForBatch.length);
}
// 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式
@@ -7482,20 +7577,14 @@ function showCreateGroupModal(andMoveConversation = false) {
iconPicker.style.display = 'none';
}
if (modal) {
modal.style.display = 'flex';
openAppModal('create-group-modal', { focusEl: input });
modal.dataset.moveConversation = andMoveConversation ? 'true' : 'false';
if (input) {
setTimeout(() => input.focus(), 100);
}
}
}
// 关闭创建分组模态框
function closeCreateGroupModal() {
const modal = document.getElementById('create-group-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('create-group-modal');
const input = document.getElementById('create-group-name-input');
if (input) {
input.value = '';
+18 -10
View File
@@ -344,7 +344,9 @@ function showFofaParseModal(nlText, parsed) {
const modal = document.createElement('div');
modal.id = 'fofa-parse-modal';
modal.className = 'modal';
modal.style.display = 'block';
document.body.appendChild(modal);
openAppModal(modal, { focus: false });
deferModalContent(function () {
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
@@ -384,24 +386,24 @@ function showFofaParseModal(nlText, parsed) {
</div>
`;
document.body.appendChild(modal);
const queryTextarea = document.getElementById('fofa-parse-query');
if (queryTextarea) {
queryTextarea.value = (parsed?.query || '').trim();
setTimeout(() => {
try { queryTextarea.focus(); } catch (e) { /* ignore */ }
}, 0);
queryTextarea.focus();
}
const close = () => modal.remove();
modal.addEventListener('click', (e) => {
const close = function () {
closeAppModal(modal);
modal.remove();
syncAppModalBodyLock();
};
modal.addEventListener('click', function (e) {
if (e.target === modal) close();
});
document.getElementById('fofa-parse-modal-close')?.addEventListener('click', close);
document.getElementById('fofa-parse-cancel')?.addEventListener('click', close);
const applyToQuery = (run) => {
const applyToQuery = function (run) {
const els = getFofaFormElements();
const q = (queryTextarea?.value || '').trim();
if (!q) {
@@ -435,6 +437,7 @@ function showFofaParseModal(nlText, parsed) {
}
};
document.addEventListener('keydown', onKey);
});
}
function setFofaMeta(text) {
@@ -1091,8 +1094,13 @@ function showCellDetailModal(field, fullText) {
`;
document.body.appendChild(modal);
openAppModal(modal);
const close = () => modal.remove();
const close = function () {
closeAppModal(modal);
modal.remove();
syncAppModalBodyLock();
};
modal.addEventListener('click', (e) => {
if (e.target === modal) close();
});
+24 -20
View File
@@ -905,25 +905,32 @@ function showAddKnowledgeItemModal() {
document.getElementById('knowledge-item-category').value = '';
document.getElementById('knowledge-item-title').value = '';
document.getElementById('knowledge-item-content').value = '';
document.getElementById('knowledge-item-modal').style.display = 'block';
openAppModal('knowledge-item-modal');
}
// 编辑知识项
async function editKnowledgeItem(id) {
try {
currentEditingItemId = id;
document.getElementById('knowledge-item-modal-title').textContent = '编辑知识';
document.getElementById('knowledge-item-category').value = '';
document.getElementById('knowledge-item-title').value = '';
document.getElementById('knowledge-item-content').value = '';
openAppModal('knowledge-item-modal', { focus: false });
const response = await apiFetch(`/api/knowledge/items/${id}`);
if (!response.ok) {
throw new Error('获取知识项失败');
}
const item = await response.json();
currentEditingItemId = id;
document.getElementById('knowledge-item-modal-title').textContent = '编辑知识';
document.getElementById('knowledge-item-category').value = item.category;
document.getElementById('knowledge-item-title').value = item.title;
document.getElementById('knowledge-item-content').value = item.content;
document.getElementById('knowledge-item-modal').style.display = 'block';
deferModalContent(() => {
document.getElementById('knowledge-item-category').value = item.category;
document.getElementById('knowledge-item-title').value = item.title;
document.getElementById('knowledge-item-content').value = item.content;
document.getElementById('knowledge-item-title')?.focus();
});
} catch (error) {
closeAppModal('knowledge-item-modal');
currentEditingItemId = null;
console.error('编辑知识项失败:', error);
showNotification('编辑知识项失败: ' + error.message, 'error');
}
@@ -1232,10 +1239,7 @@ function updateKnowledgeStatsAfterDelete() {
// 关闭知识项模态框
function closeKnowledgeItemModal() {
const modal = document.getElementById('knowledge-item-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('knowledge-item-modal');
// 重置编辑状态
currentEditingItemId = null;
@@ -1786,8 +1790,11 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
document.body.appendChild(modal);
}
// 填充内容
const content = document.getElementById('retrieval-log-details-content');
if (content) content.innerHTML = '<p style="color:#64748b;margin:0;">…</p>';
openAppModal(modal, { focus: false });
deferModalContent(() => {
const timeAgo = getTimeAgo(log.createdAt);
const fullTime = formatTime(log.createdAt);
@@ -1880,16 +1887,12 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
</div>
</div>
`;
modal.style.display = 'block';
});
}
// 关闭检索日志详情模态框
function closeRetrievalLogDetailsModal() {
const modal = document.getElementById('retrieval-log-details-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('retrieval-log-details-modal');
}
// 点击模态框外部关闭
@@ -2118,7 +2121,8 @@ function showToastNotification(message, type = 'info') {
font-size: 0.875rem;
line-height: 1.45;
word-wrap: break-word;
backdrop-filter: blur(8px);
backdrop-filter: none;
-webkit-backdrop-filter: none;
`;
toast.innerHTML = `
+92
View File
@@ -0,0 +1,92 @@
/**
* 统一弹窗先显示遮罩下一帧再填大段内容避免与 backdrop 绘制抢主线程
*/
(function () {
const BODY_LOCK = 'app-modal-open';
const LEGACY_BODY_LOCK = 'projects-modal-open';
const OVERLAY_SELECTOR =
'.projects-modal-overlay, .c2-modal-overlay, .modal, .info-collect-cell-modal, #login-overlay';
const FLEX_MODAL_IDS = new Set([
'role-modal',
'skill-modal',
'agent-md-modal',
'batch-manage-modal',
'create-group-modal',
'login-overlay',
]);
function resolveEl(idOrEl) {
if (!idOrEl) return null;
return typeof idOrEl === 'string' ? document.getElementById(idOrEl) : idOrEl;
}
function isElVisible(el) {
if (!el) return false;
const s = window.getComputedStyle(el);
return s.display !== 'none' && s.visibility !== 'hidden';
}
function defaultDisplay(el) {
if (el.classList.contains('projects-modal-overlay') || el.classList.contains('c2-modal-overlay')) {
return 'flex';
}
if (el.classList.contains('info-collect-cell-modal')) {
return 'flex';
}
if (FLEX_MODAL_IDS.has(el.id)) {
return 'flex';
}
return 'block';
}
function syncBodyLock() {
const anyOpen = Array.from(document.querySelectorAll(OVERLAY_SELECTOR)).some(isElVisible);
document.body.classList.toggle(BODY_LOCK, anyOpen);
const projectsOpen = Array.from(document.querySelectorAll('.projects-modal-overlay')).some(isElVisible);
document.body.classList.toggle(LEGACY_BODY_LOCK, projectsOpen);
}
function openAppModal(idOrEl, opts) {
opts = opts || {};
const el = resolveEl(idOrEl);
if (!el) return null;
el.style.display = opts.display || defaultDisplay(el);
syncBodyLock();
if (opts.focus === false) return el;
const sel =
opts.focusSelector ||
'input.form-input, textarea.form-input, select.form-input, input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])';
const focusTarget = opts.focusEl || el.querySelector(sel);
if (focusTarget) {
requestAnimationFrame(function () {
focusTarget.focus();
});
}
return el;
}
function closeAppModal(idOrEl) {
const el = resolveEl(idOrEl);
if (el) el.style.display = 'none';
syncBodyLock();
return el;
}
function isAppModalOpen(idOrEl) {
return isElVisible(resolveEl(idOrEl));
}
/** 双 rAF:等遮罩绘制完成后再写入大段 DOM / 表单 */
function deferModalContent(fn) {
requestAnimationFrame(function () {
requestAnimationFrame(fn);
});
}
window.openAppModal = openAppModal;
window.closeAppModal = closeAppModal;
window.isAppModalOpen = isAppModalOpen;
window.deferModalContent = deferModalContent;
window.syncAppModalBodyLock = syncBodyLock;
})();
+97 -22
View File
@@ -172,6 +172,59 @@ function einoMainStreamPlanningTitle(responseData) {
return prefix + '📝 ' + plan;
}
/**
* 主通道 response 结束时将流式占位条目固化为 planning与后端 flushResponsePlan 落库类型一致
* 避免 integrateProgressToMCPSection 快照前删除占位导致助手输出仅刷新后才出现
*/
function finalizeMainResponseStreamItem(streamState, finalMessage, responseData) {
if (!streamState || !streamState.itemId) return false;
const item = document.getElementById(streamState.itemId);
if (!item || !item.parentNode) return false;
const fullText = (finalMessage != null && String(finalMessage).trim() !== '')
? String(finalMessage)
: (streamState.buffer || '');
if (!String(fullText).trim()) {
item.parentNode.removeChild(item);
return false;
}
const meta = Object.assign({}, streamState.streamMeta || {}, responseData || {});
item.classList.remove('timeline-item-thinking');
item.classList.add('timeline-item-planning');
item.dataset.timelineType = 'planning';
delete item.dataset.responseStreamPlaceholder;
if (meta.orchestration != null && String(meta.orchestration).trim() !== '') {
item.dataset.orchestration = String(meta.orchestration).trim();
}
if (meta.einoAgent != null && String(meta.einoAgent).trim() !== '') {
item.dataset.einoAgent = String(meta.einoAgent).trim();
}
const titleEl = item.querySelector('.timeline-item-title');
if (titleEl && typeof einoMainStreamPlanningTitle === 'function') {
titleEl.textContent = einoMainStreamPlanningTitle(meta);
}
let contentEl = item.querySelector('.timeline-item-content');
if (!contentEl) {
contentEl = document.createElement('div');
contentEl.className = 'timeline-item-content';
item.appendChild(contentEl);
}
flushStreamPlainTextUpdate(contentEl);
const body = typeof formatTimelineStreamBody === 'function'
? formatTimelineStreamBody(fullText, meta)
: fullText;
if (typeof formatMarkdown === 'function') {
setTimelineItemContentStreamRich(contentEl, formatMarkdown(body, timelineMarkdownOpts));
} else {
setTimelineItemContentStreamPlain(contentEl, body);
}
return true;
}
function translateProgressMessage(message, data) {
if (!message || typeof message !== 'string') return message;
if (typeof window.t !== 'function') return message;
@@ -224,6 +277,7 @@ if (typeof window !== 'undefined') {
window.translateProgressMessage = translateProgressMessage;
window.translatePlanExecuteAgentName = translatePlanExecuteAgentName;
window.einoMainStreamPlanningTitle = einoMainStreamPlanningTitle;
window.finalizeMainResponseStreamItem = finalizeMainResponseStreamItem;
window.formatTimelineStreamBody = formatTimelineStreamBody;
}
@@ -944,18 +998,12 @@ function openUserInterruptModal(progressId, conversationId) {
if (ta) {
ta.value = '';
}
const m = document.getElementById('user-interrupt-modal');
if (m) {
m.style.display = 'block';
}
openAppModal('user-interrupt-modal');
}
function closeUserInterruptModal() {
userInterruptModalPending = null;
const m = document.getElementById('user-interrupt-modal');
if (m) {
m.style.display = 'none';
}
closeAppModal('user-interrupt-modal');
}
async function submitUserInterruptContinue() {
@@ -2407,14 +2455,18 @@ function handleStreamEvent(event, progressElement, progressId,
updateAssistantBubbleContent(assistantIdFinal, event.message, true);
}
// 移除 response_start/response_delta 阶段创建的「规划中」占位条目。
// 该条目属于 UI-only 的流式展示,不应被拷贝到最终的过程详情里;
// 否则会出现“不刷新页面仍显示规划中,刷新后消失”的不一致。
// response_start/response_delta 占位固化为 planning,与后端落库一致后再快照过程详情
if (streamState && streamState.itemId) {
const planningItem = document.getElementById(streamState.itemId);
if (planningItem && planningItem.parentNode) {
planningItem.parentNode.removeChild(planningItem);
}
finalizeMainResponseStreamItem(streamState, event.message, responseData);
} else if (event.message && String(event.message).trim()) {
addTimelineItem(timeline, 'planning', {
title: typeof einoMainStreamPlanningTitle === 'function'
? einoMainStreamPlanningTitle(responseData)
: ('📝 ' + (typeof window.t === 'function' ? window.t('chat.planning') : '规划中')),
message: event.message,
data: responseData,
expanded: false
});
}
// 最终回复时隐藏进度卡片(多代理模式下,迭代过程已完整展示)
@@ -2435,6 +2487,11 @@ function handleStreamEvent(event, progressElement, progressId,
const respMid = responseData.messageId;
if (respMid) {
applyBackendMessageIdToAssistantDom(assistantIdFinal, respMid);
if (typeof window.syncAssistantReasoningContentFromServer === 'function') {
setTimeout(function () {
window.syncAssistantReasoningContentFromServer(respMid, assistantIdFinal);
}, 400);
}
}
setTimeout(() => {
@@ -3986,7 +4043,9 @@ async function setMcpMonitorTimelineRange(range) {
monitorState.timeline = timelineJson;
const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner');
if (timelineInner) {
timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError);
const combined = timelineInner.closest('.mcp-stats-combined');
const compactEmpty = combined && !!combined.querySelector('.mcp-stats-combined__main');
timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError, compactEmpty);
bindMcpStatsTimelineEvents();
syncMcpMonitorTimelineRangeUI(range);
} else if (monitorState.stats && Object.keys(monitorState.stats).length > 0) {
@@ -3996,7 +4055,9 @@ async function setMcpMonitorTimelineRange(range) {
monitorState.timelineError = err.message || 'error';
const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner');
if (timelineInner) {
timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError);
const combined = timelineInner.closest('.mcp-stats-combined');
const compactEmpty = combined && !!combined.querySelector('.mcp-stats-combined__main');
timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError, compactEmpty);
bindMcpStatsTimelineEvents();
syncMcpMonitorTimelineRangeUI(range);
}
@@ -4014,7 +4075,21 @@ function renderMcpStatsTimelineRangeButtons() {
}).join('');
}
function renderMcpStatsTimelineBody(timeline, timelineError) {
const MCP_TIMELINE_EMPTY_ICON = '<svg class="mcp-stats-timeline-empty-state__icon" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
function renderMcpStatsTimelineEmptyState(compact) {
const noData = mcpMonitorT('timelineNoData') || monitorFallback('该时段暂无调用', 'No calls in this period');
const emptyHint = mcpMonitorT('timelineEmptyHint')
|| monitorFallback('切换时间范围查看其他时段,或在对话/任务中调用 MCP 工具', 'Switch the time range or invoke MCP tools in chat or tasks');
const compactClass = compact ? ' mcp-stats-timeline-empty-state--compact' : '';
return `<div class="mcp-stats-timeline-empty-state${compactClass}">
${MCP_TIMELINE_EMPTY_ICON}
<p class="mcp-stats-timeline-empty-state__title">${escapeHtml(noData)}</p>
<p class="mcp-stats-timeline-empty-state__hint">${escapeHtml(emptyHint)}</p>
</div>`;
}
function renderMcpStatsTimelineBody(timeline, timelineError, compactEmpty) {
const hint = mcpMonitorT('timelineHint') || monitorFallback('全部工具合计', 'All tools combined');
if (timelineError) {
@@ -4029,8 +4104,7 @@ function renderMcpStatsTimelineBody(timeline, timelineError) {
|| `区间内 ${summaryTotal} 次 · 峰值 ${peak}`;
if (points.length === 0 || summaryTotal === 0) {
const noData = mcpMonitorT('timelineNoData') || monitorFallback('该时段暂无调用', 'No calls in this period');
return `<p class="mcp-stats-timeline-empty">${escapeHtml(noData)}</p>`;
return renderMcpStatsTimelineEmptyState(!!compactEmpty);
}
const rangeKey = timeline.range || getMcpMonitorTimelineRange();
@@ -4083,7 +4157,7 @@ function renderMcpStatsCombinedSection(topTools, totals, activeToolFilter, timel
const timelineCol = showTimeline
? `<div class="mcp-stats-combined__timeline">
<p class="mcp-stats-combined__col-label">${escapeHtml(timelineTitle)}</p>
<div class="mcp-stats-combined__timeline-inner">${renderMcpStatsTimelineBody(timeline, timelineError)}</div>
<div class="mcp-stats-combined__timeline-inner">${renderMcpStatsTimelineBody(timeline, timelineError, hasTools)}</div>
</div>`
: '';
@@ -4897,7 +4971,8 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
.slice(0, MCP_STATS_TOP_N);
const showCombined = showTimeline || topTools.length > 0;
const hasAnyCalls = totals.total > 0;
const showCombined = hasAnyCalls && (topTools.length > 0 || showTimeline);
const html = `
<div class="mcp-exec-stats">
${renderMcpStatsMetricsBar(totals, successRate, rateTone, rateSubText, lastCallText, hasCalls)}
+302 -110
View File
@@ -5,12 +5,15 @@ let projectsCache = [];
let projectsCacheAll = [];
const PROJECTS_LIST_PAGE_SIZE_KEY = 'cyberstrike.projects_list_page_size';
let currentProjectId = null;
let currentProjectUpdatedAt = null;
let currentProjectTab = 'facts';
const projectNameById = {};
let _projectsListReady = false;
let _projectsFetchPromise = null;
const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId';
const PROJECT_DESCRIPTION_MAX_LENGTH = 4000;
const PROJECT_NAME_MAX_LENGTH = 200;
function tp(key, opts) {
if (typeof window.t === 'function') return window.t(key, opts);
@@ -304,23 +307,9 @@ function prefetchProjectsForChat() {
ensureProjectsLoaded().catch(() => {});
}
/** 新对话时:保留有效 activeProjectId,否则默认选中第一个进行中的项目 */
/** 新对话时默认不绑定项目;用户需主动选择后才写入共享黑板 */
async function ensureDefaultActiveProjectForNewChat() {
try {
await ensureProjectsLoaded();
const cur = getActiveProjectId();
if (cur && isActiveChatProjectId(cur)) return cur;
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
const first =
source.find((p) => p.pinned && p.status !== 'archived') ||
source.find((p) => p.status !== 'archived');
if (first) {
setActiveProjectId(first.id);
return first.id;
}
} catch (e) {
console.warn(e);
}
setActiveProjectId('');
return '';
}
@@ -343,7 +332,9 @@ async function initProjectsPage() {
const page = document.getElementById('page-projects');
if (!page || page.style.display === 'none') return;
initProjectsModalEscape();
syncProjectsModalBodyLock();
if (typeof syncAppModalBodyLock === 'function') {
syncAppModalBodyLock();
}
updateProjectsDetailVisibility();
projectsListPagination.pageSize = getProjectsListPageSize();
renderProjectsPagination();
@@ -611,11 +602,41 @@ function renderProjectsSidebar() {
<div class="projects-list-item-name">${escapeHtml(p.name)}${badges}</div>
<div class="projects-list-item-meta">${formatProjectTime(p.updated_at)}</div>
</div>
<button type="button" class="projects-list-item-menu" title="${escapeHtml(tp('projects.projectActions'))}" aria-label="${escapeHtml(tp('projects.projectActions'))}" onclick="showProjectListActionMenu(event, '${escapeHtml(p.id)}')"></button>
</div>`;
}).join('');
updateProjectsDetailVisibility();
}
function clampProjectDescription(text) {
const s = (text || '').trim();
if (s.length <= PROJECT_DESCRIPTION_MAX_LENGTH) return s;
return s.slice(0, PROJECT_DESCRIPTION_MAX_LENGTH);
}
function renderProjectDetailTitle(name) {
const titleEl = document.getElementById('projects-detail-title');
if (!titleEl) return;
const text = (name || '').trim() || tp('projects.defaultProjectName');
titleEl.textContent = text;
titleEl.title = text;
}
function renderProjectDetailDesc(desc) {
const descEl = document.getElementById('projects-detail-desc');
if (!descEl) return;
const text = (desc || '').trim();
if (!text) {
descEl.hidden = true;
descEl.textContent = '';
descEl.removeAttribute('title');
return;
}
descEl.textContent = text;
descEl.title = text;
descEl.hidden = false;
}
function updateProjectStatusPill(status) {
const el = document.getElementById('projects-detail-status');
if (!el) return;
@@ -624,6 +645,24 @@ function updateProjectStatusPill(status) {
el.className = 'projects-status-pill ' + (archived ? 'projects-status-pill--archived' : 'projects-status-pill--active');
}
function renderProjectDetailMeta(updatedAt) {
const metaEl = document.getElementById('projects-detail-meta');
if (!metaEl) return;
const time = formatProjectTime(updatedAt);
metaEl.textContent = tpFmt('projects.updatedPrefix', `Updated ${time}`, { time });
}
function refreshProjectDetailMetaI18n() {
if (!currentProjectId) return;
let updatedAt = currentProjectUpdatedAt;
if (updatedAt == null) {
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
const p = source.find((x) => x.id === currentProjectId);
updatedAt = p?.updated_at;
}
renderProjectDetailMeta(updatedAt);
}
function updateProjectStats(stats) {
const s = stats || {};
const f = document.getElementById('project-stat-facts');
@@ -669,8 +708,7 @@ async function selectProject(id) {
const res = await apiFetch(`/api/projects/${id}`);
if (!res.ok) throw new Error(tp('projects.projectNotFound'));
const p = await res.json();
const titleEl = document.getElementById('projects-detail-title');
if (titleEl) titleEl.textContent = p.name || tp('projects.defaultProjectName');
renderProjectDetailTitle(p.name);
document.getElementById('project-edit-name').value = p.name || '';
document.getElementById('project-edit-description').value = p.description || '';
document.getElementById('project-edit-scope').value = p.scope_json || '';
@@ -679,19 +717,9 @@ async function selectProject(id) {
const pinEl = document.getElementById('project-edit-pinned');
if (pinEl) pinEl.checked = !!p.pinned;
updateProjectStatusPill(p.status || 'active');
const metaEl = document.getElementById('projects-detail-meta');
if (metaEl) metaEl.textContent = tpFmt('projects.updatedPrefix', `Updated ${formatProjectTime(p.updated_at)}`, { time: formatProjectTime(p.updated_at) });
const descEl = document.getElementById('projects-detail-desc');
if (descEl) {
const desc = (p.description || '').trim();
if (desc) {
descEl.textContent = desc;
descEl.hidden = false;
} else {
descEl.textContent = '';
descEl.hidden = true;
}
}
currentProjectUpdatedAt = p.updated_at;
renderProjectDetailMeta(currentProjectUpdatedAt);
renderProjectDetailDesc(p.description);
projectNameById[p.id] = p.name || p.id;
} catch (e) {
console.warn(e);
@@ -858,38 +886,52 @@ let _factDetailFact = null;
let _projectFactsFilterDebounce = null;
async function viewProjectFactBody(factKey) {
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`);
if (!res.ok) return alert(tp('common.loadFailed'));
const f = await res.json();
_factDetailKey = f.fact_key;
_factDetailFact = f;
document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`;
const metaParts = [
tpFmt('projects.factMetaCategory', `Category: ${f.category}`, { value: f.category }),
tpFmt('projects.factMetaConfidence', `Confidence: ${f.confidence}`, { value: f.confidence }),
tpFmt('projects.factMetaUpdated', `Updated: ${formatProjectTime(f.updated_at, f.created_at)}`, {
time: formatProjectTime(f.updated_at, f.created_at),
}),
];
if (f.related_vulnerability_id) metaParts.push(tpFmt('projects.factMetaRelatedVuln', `Related vulnerability: ${f.related_vulnerability_id}`, { value: f.related_vulnerability_id }));
if (f.source_conversation_id) metaParts.push(tpFmt('projects.factMetaSourceConversation', `Source conversation: ${f.source_conversation_id}`, { value: f.source_conversation_id }));
document.getElementById('fact-detail-meta').textContent = metaParts.join(' · ');
document.getElementById('fact-detail-body').textContent = f.body || tp('projects.emptyBody');
document.getElementById('fact-detail-title').textContent = factKey;
document.getElementById('fact-detail-meta').textContent = '…';
document.getElementById('fact-detail-body').textContent = '';
const warnEl = document.getElementById('fact-detail-sparse-warn');
if (warnEl) {
if (isSparseFactBody(f.category, f.fact_key, f.body)) {
warnEl.hidden = false;
warnEl.textContent = tp('projects.factSparseWarn');
} else {
warnEl.hidden = true;
warnEl.textContent = '';
}
warnEl.hidden = true;
warnEl.textContent = '';
}
const linkBtn = document.getElementById('fact-detail-link-vuln-btn');
const createBtn = document.getElementById('fact-detail-create-vuln-btn');
if (linkBtn) linkBtn.hidden = false;
if (createBtn) createBtn.hidden = false;
openProjectsOverlay('fact-detail-modal');
if (linkBtn) linkBtn.hidden = true;
if (createBtn) createBtn.hidden = true;
openProjectsOverlay('fact-detail-modal', { focus: false });
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`);
if (!res.ok) {
closeFactDetailModal();
return alert(tp('common.loadFailed'));
}
const f = await res.json();
_factDetailKey = f.fact_key;
_factDetailFact = f;
deferModalContent(() => {
document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`;
const metaParts = [
tpFmt('projects.factMetaCategory', `Category: ${f.category}`, { value: f.category }),
tpFmt('projects.factMetaConfidence', `Confidence: ${f.confidence}`, { value: f.confidence }),
tpFmt('projects.factMetaUpdated', `Updated: ${formatProjectTime(f.updated_at, f.created_at)}`, {
time: formatProjectTime(f.updated_at, f.created_at),
}),
];
if (f.related_vulnerability_id) metaParts.push(tpFmt('projects.factMetaRelatedVuln', `Related vulnerability: ${f.related_vulnerability_id}`, { value: f.related_vulnerability_id }));
if (f.source_conversation_id) metaParts.push(tpFmt('projects.factMetaSourceConversation', `Source conversation: ${f.source_conversation_id}`, { value: f.source_conversation_id }));
document.getElementById('fact-detail-meta').textContent = metaParts.join(' · ');
document.getElementById('fact-detail-body').textContent = f.body || tp('projects.emptyBody');
if (warnEl) {
if (isSparseFactBody(f.category, f.fact_key, f.body)) {
warnEl.hidden = false;
warnEl.textContent = tp('projects.factSparseWarn');
} else {
warnEl.hidden = true;
warnEl.textContent = '';
}
}
if (linkBtn) linkBtn.hidden = false;
if (createBtn) createBtn.hidden = false;
});
}
function editFactFromDetail() {
@@ -1144,41 +1186,16 @@ async function viewFactsForVulnerability(vulnId) {
else loadProjectFacts();
}
function openProjectsOverlay(id) {
const el = document.getElementById(id);
if (!el) return;
el.style.display = 'flex';
syncProjectsModalBodyLock();
const focusTarget = el.querySelector('input.form-input, textarea.form-input, select.form-input');
if (focusTarget) {
setTimeout(() => focusTarget.focus(), 80);
}
function openProjectsOverlay(id, opts) {
openAppModal(id, opts);
}
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');
return isAppModalOpen(id);
}
function closeProjectsOverlay(id) {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
syncProjectsModalBodyLock();
closeAppModal(id);
}
function showNewProjectModal() {
@@ -1193,6 +1210,42 @@ function showNewProjectModal() {
openProjectsOverlay('project-modal');
}
async function showEditProjectModal(projectId) {
if (!projectId) return;
window._projectModalFromChat = false;
window._projectModalEditId = projectId;
document.getElementById('project-modal-title').textContent = tp('projects.modalEditTitle');
const sub = document.getElementById('project-modal-subtitle');
if (sub) sub.textContent = tp('projects.modalEditSubtitle');
const submitBtn = document.getElementById('project-modal-submit-btn');
if (submitBtn) submitBtn.textContent = tp('projects.saveChanges');
const nameEl = document.getElementById('project-modal-name');
const descEl = document.getElementById('project-modal-description');
if (nameEl) nameEl.value = '';
if (descEl) descEl.value = '';
openProjectsOverlay('project-modal', { focus: false });
let p = findProjectById(projectId);
if (!p) {
try {
const res = await apiFetch(`/api/projects/${encodeURIComponent(projectId)}`);
if (!res.ok) throw new Error(tp('projects.projectNotFound'));
p = await res.json();
} catch (e) {
closeProjectModal();
alert(e.message || tp('projects.projectNotFound'));
window._projectModalEditId = null;
return;
}
}
const name = (p.name || '').slice(0, PROJECT_NAME_MAX_LENGTH);
const description = clampProjectDescription(p.description || '');
deferModalContent(() => {
if (nameEl) nameEl.value = name;
if (descEl) descEl.value = description;
nameEl?.focus();
});
}
/** 从对话区「选择项目」面板打开新建项目,创建成功后自动绑定当前对话 */
function showNewProjectModalFromChat() {
closeChatProjectPanel();
@@ -1201,11 +1254,11 @@ function showNewProjectModalFromChat() {
}
async function saveProjectModal() {
const name = document.getElementById('project-modal-name').value.trim();
const name = document.getElementById('project-modal-name').value.trim().slice(0, PROJECT_NAME_MAX_LENGTH);
if (!name) return alert(tp('projects.enterProjectName'));
const body = {
name,
description: document.getElementById('project-modal-description').value.trim(),
description: clampProjectDescription(document.getElementById('project-modal-description').value),
};
const editId = window._projectModalEditId;
const res = editId
@@ -1217,12 +1270,18 @@ async function saveProjectModal() {
return;
}
const fromChat = !!window._projectModalFromChat;
const fromWebshellConnId = window._projectModalFromWebshellConnId || '';
window._projectModalFromChat = false;
window._projectModalFromWebshellConnId = '';
closeProjectModal();
const saved = await res.json();
await loadProjectsList();
if (saved.id) {
if (fromChat && !editId) {
if (fromWebshellConnId && !editId) {
if (typeof applyWebshellAiProjectSelection === 'function') {
await applyWebshellAiProjectSelection(saved.id);
}
} else if (fromChat && !editId) {
await applyChatProjectSelection(saved.id);
} else {
await selectProject(saved.id);
@@ -1232,6 +1291,7 @@ async function saveProjectModal() {
function closeProjectModal() {
window._projectModalFromChat = false;
window._projectModalEditId = null;
closeProjectsOverlay('project-modal');
}
@@ -1272,7 +1332,7 @@ async function saveProjectSettings() {
}
const body = {
name: document.getElementById('project-edit-name').value.trim(),
description: document.getElementById('project-edit-description').value.trim(),
description: clampProjectDescription(document.getElementById('project-edit-description').value),
scope_json: scopeRaw,
status: document.getElementById('project-edit-status')?.value || 'active',
pinned: !!document.getElementById('project-edit-pinned')?.checked,
@@ -1288,30 +1348,112 @@ async function saveProjectSettings() {
alert(tp('projects.saved'));
}
async function archiveCurrentProject() {
if (!currentProjectId) return;
const statusEl = document.getElementById('project-edit-status');
const cur = statusEl?.value || 'active';
function findProjectById(projectId) {
return projectsCache.find((p) => p.id === projectId) || projectsCacheAll.find((p) => p.id === projectId);
}
let _projectListMenuTargetId = null;
let _projectListMenuDocClickBound = false;
function closeProjectListActionMenu() {
const menu = document.getElementById('projects-list-action-menu');
if (!menu) return;
menu.style.display = 'none';
_projectListMenuTargetId = null;
}
function positionProjectListActionMenu(event) {
const menu = document.getElementById('projects-list-action-menu');
if (!menu) return;
menu.style.display = 'block';
menu.style.visibility = 'visible';
menu.style.opacity = '1';
void menu.offsetHeight;
const menuRect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = event.clientX;
let top = event.clientY;
if (left + menuRect.width > viewportWidth) {
left = Math.max(8, event.clientX - menuRect.width);
}
if (top + menuRect.height > viewportHeight) {
top = Math.max(8, event.clientY - menuRect.height);
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
}
function showProjectListActionMenu(event, projectId) {
event.stopPropagation();
event.preventDefault();
const menu = document.getElementById('projects-list-action-menu');
if (!menu) return;
if (_projectListMenuTargetId === projectId && menu.style.display === 'block') {
closeProjectListActionMenu();
return;
}
closeProjectListActionMenu();
const p = findProjectById(projectId);
if (!p) return;
_projectListMenuTargetId = projectId;
const editText = document.getElementById('projects-list-menu-edit-text');
const archiveText = document.getElementById('projects-list-menu-archive-text');
const deleteText = document.getElementById('projects-list-menu-delete-text');
if (editText) editText.textContent = tp('projects.editProject');
if (archiveText) {
archiveText.textContent = p.status === 'archived'
? tp('projects.restoreProjectActive')
: tp('projects.archiveProject');
}
if (deleteText) deleteText.textContent = tp('projects.deleteProject');
positionProjectListActionMenu(event);
}
function initProjectListActionMenu() {
if (_projectListMenuDocClickBound) return;
_projectListMenuDocClickBound = true;
document.addEventListener('click', (event) => {
const menu = document.getElementById('projects-list-action-menu');
if (!menu || menu.style.display === 'none') return;
if (menu.contains(event.target)) return;
if (event.target.closest('.projects-list-item-menu')) return;
closeProjectListActionMenu();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closeProjectListActionMenu();
});
}
async function toggleProjectArchiveById(projectId) {
const p = findProjectById(projectId);
if (!p) return;
const cur = p.status || 'active';
const next = cur === 'archived' ? 'active' : 'archived';
if (!confirm(next === 'archived' ? tp('projects.confirmArchiveProject') : tp('projects.confirmRestoreProjectActive'))) return;
const res = await apiFetch(`/api/projects/${currentProjectId}`, {
const res = await apiFetch(`/api/projects/${projectId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }),
});
if (!res.ok) return alert(tp('projects.operationFailed'));
await loadProjectsList();
await selectProject(currentProjectId);
if (currentProjectId === projectId && projectsCache.some((item) => item.id === projectId)) {
await selectProject(projectId);
} else if (currentProjectId === projectId) {
currentProjectId = null;
updateProjectsDetailVisibility();
if (projectsCache.length) await selectProject(projectsCache[0].id);
}
}
async function deleteCurrentProject() {
if (!currentProjectId || !confirm(tp('projects.confirmDeleteProject'))) return;
const deletedId = currentProjectId;
const deletedIndex = projectsCache.findIndex((p) => p.id === deletedId);
const res = await apiFetch(`/api/projects/${deletedId}`, { method: 'DELETE' });
async function deleteProjectById(projectId) {
if (!projectId || !confirm(tp('projects.confirmDeleteProject'))) return;
const deletedIndex = projectsCache.findIndex((p) => p.id === projectId);
const res = await apiFetch(`/api/projects/${projectId}`, { method: 'DELETE' });
if (!res.ok) return alert(tp('projects.deleteFailed'));
if (getActiveProjectId() === deletedId) setActiveProjectId('');
currentProjectId = null;
if (getActiveProjectId() === projectId) setActiveProjectId('');
if (currentProjectId === projectId) currentProjectId = null;
await loadProjectsList();
if (projectsCache.length) {
const nextIndex = Math.min(deletedIndex >= 0 ? deletedIndex : 0, projectsCache.length - 1);
@@ -1321,6 +1463,37 @@ async function deleteCurrentProject() {
}
}
async function toggleProjectArchiveFromListMenu() {
const projectId = _projectListMenuTargetId;
closeProjectListActionMenu();
if (!projectId) return;
await toggleProjectArchiveById(projectId);
}
function editProjectFromListMenu() {
const projectId = _projectListMenuTargetId;
closeProjectListActionMenu();
if (!projectId) return;
showEditProjectModal(projectId);
}
async function deleteProjectFromListMenu() {
const projectId = _projectListMenuTargetId;
closeProjectListActionMenu();
if (!projectId) return;
await deleteProjectById(projectId);
}
async function archiveCurrentProject() {
if (!currentProjectId) return;
await toggleProjectArchiveById(currentProjectId);
}
async function deleteCurrentProject() {
if (!currentProjectId) return;
await deleteProjectById(currentProjectId);
}
function resetFactModalForm() {
window._factModalEditId = null;
const keyEl = document.getElementById('fact-modal-key');
@@ -1380,14 +1553,20 @@ function showAddFactModal() {
async function showEditFactModal(factKey) {
if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
resetFactModalForm();
openProjectsOverlay('fact-modal', { focus: false });
const res = await apiFetch(
`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`,
);
if (!res.ok) return alert(tp('projects.loadFactFailed'));
if (!res.ok) {
closeFactModal();
return alert(tp('projects.loadFactFailed'));
}
const f = await res.json();
resetFactModalForm();
fillFactModalForm(f);
openProjectsOverlay('fact-modal');
deferModalContent(() => {
fillFactModalForm(f);
document.getElementById('fact-modal-key')?.focus();
});
}
function closeFactModal() {
@@ -1714,6 +1893,10 @@ function initChatProjectSelector() {
const panel = document.getElementById('chat-project-panel');
if (panel && panel.style.display === 'flex') renderChatProjectPanelList();
if (currentProjectId) {
refreshProjectDetailMetaI18n();
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
const p = source.find((x) => x.id === currentProjectId);
if (p) updateProjectStatusPill(p.status || 'active');
refreshProjectHeaderStats().catch(() => {});
switchProjectTab(currentProjectTab || 'facts');
}
@@ -1731,13 +1914,18 @@ function initChatProjectSelector() {
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initChatProjectSelector);
document.addEventListener('DOMContentLoaded', () => {
initChatProjectSelector();
initProjectListActionMenu();
});
} else {
initChatProjectSelector();
initProjectListActionMenu();
}
window.initProjectsPage = initProjectsPage;
window.showNewProjectModal = showNewProjectModal;
window.showEditProjectModal = showEditProjectModal;
window.showNewProjectModalFromChat = showNewProjectModalFromChat;
window.saveProjectModal = saveProjectModal;
window.closeProjectModal = closeProjectModal;
@@ -1752,6 +1940,10 @@ window.closeFactDetailModal = closeFactDetailModal;
window.saveProjectSettings = saveProjectSettings;
window.archiveCurrentProject = archiveCurrentProject;
window.deleteCurrentProject = deleteCurrentProject;
window.showProjectListActionMenu = showProjectListActionMenu;
window.editProjectFromListMenu = editProjectFromListMenu;
window.toggleProjectArchiveFromListMenu = toggleProjectArchiveFromListMenu;
window.deleteProjectFromListMenu = deleteProjectFromListMenu;
window.refreshChatProjectSelector = refreshChatProjectSelector;
window.onChatProjectChange = onChatProjectChange;
window.toggleChatProjectPanel = toggleChatProjectPanel;
+8 -6
View File
@@ -1112,7 +1112,7 @@ async function showAddRoleModal() {
// 确保统计信息正确更新(显示0/108)
updateRoleToolsStats();
modal.style.display = 'flex';
openAppModal('role-modal');
}
// 编辑角色
@@ -1274,15 +1274,16 @@ async function editRole(roleName) {
}
}
modal.style.display = 'flex';
openAppModal('role-modal');
}
// 关闭角色模态框
function closeRoleModal() {
const modal = document.getElementById('role-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('role-modal');
}
function closeRoleSelectModal() {
closeAppModal('role-select-modal');
}
// 获取所有选中的工具(包括未在MCP管理中启用的工具)
@@ -1634,6 +1635,7 @@ if (typeof window !== 'undefined') {
window.getCurrentRole = getCurrentRole;
window.toggleRoleSelectionPanel = toggleRoleSelectionPanel;
window.closeRoleSelectionPanel = closeRoleSelectionPanel;
window.closeRoleSelectModal = closeRoleSelectModal;
window.filterRoleToolsByStatus = filterRoleToolsByStatus;
window.currentSelectedRole = getCurrentRole();
+1 -1
View File
@@ -505,7 +505,7 @@ document.addEventListener('DOMContentLoaded', function() {
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'tasks', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
switchPage(pageId);
if (pageId === 'chat') {
scheduleChatConversationFromHash(200);
+14 -19
View File
@@ -2096,47 +2096,42 @@ function showAddExternalMCPModal() {
document.getElementById('external-mcp-json-error').style.display = 'none';
document.getElementById('external-mcp-json-error').textContent = '';
document.getElementById('external-mcp-json').classList.remove('error');
document.getElementById('external-mcp-modal').style.display = 'block';
openAppModal('external-mcp-modal');
}
// 关闭外部MCP模态框
function closeExternalMCPModal() {
document.getElementById('external-mcp-modal').style.display = 'none';
closeAppModal('external-mcp-modal');
currentEditingMCPName = null;
}
// 编辑外部MCP
async function editExternalMCP(name) {
try {
currentEditingMCPName = name;
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.editExternalMCP') : '编辑外部MCP');
document.getElementById('external-mcp-json').value = '';
document.getElementById('external-mcp-json-error').style.display = 'none';
document.getElementById('external-mcp-json-error').textContent = '';
document.getElementById('external-mcp-json').classList.remove('error');
openAppModal('external-mcp-modal', { focus: false });
const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`);
if (!response.ok) {
throw new Error(typeof window.t === 'function' ? window.t('mcp.getConfigFailed') : '获取外部MCP配置失败');
}
const server = await response.json();
currentEditingMCPName = name;
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.editExternalMCP') : '编辑外部MCP');
// 将配置转换为对象格式(key为名称)
const config = { ...server.config };
// 移除tool_count、external_mcp_enable等前端字段,但保留enabled/disabled用于向后兼容
delete config.tool_count;
delete config.external_mcp_enable;
// 包装成对象格式:{ "name": { config } }
const configObj = {};
configObj[name] = config;
// 格式化JSON
const jsonStr = JSON.stringify(configObj, null, 2);
document.getElementById('external-mcp-json').value = jsonStr;
document.getElementById('external-mcp-json-error').style.display = 'none';
document.getElementById('external-mcp-json-error').textContent = '';
document.getElementById('external-mcp-json').classList.remove('error');
document.getElementById('external-mcp-modal').style.display = 'block';
deferModalContent(() => {
document.getElementById('external-mcp-json').value = jsonStr;
document.getElementById('external-mcp-json')?.focus();
});
} catch (error) {
closeExternalMCPModal();
console.error('编辑外部MCP失败:', error);
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '编辑失败') + ': ' + error.message);
}
+40 -40
View File
@@ -40,7 +40,7 @@ function shouldSkipSkillsAutoRefresh() {
}
const modal = document.getElementById('skill-modal');
if (modal && modal.style.display === 'flex') {
if (modal && isAppModalOpen('skill-modal')) {
return true;
}
@@ -465,7 +465,7 @@ function showAddSkillModal() {
const addTa = document.getElementById('skill-content-add');
if (addTa) addTa.value = '';
modal.style.display = 'flex';
openAppModal('skill-modal');
}
function skillPackagePathDepth(path) {
@@ -555,6 +555,22 @@ async function selectSkillPackageFile(skillId, path, opts) {
// 编辑skill
async function editSkill(skillId) {
wireSkillModalOnce();
const modal = document.getElementById('skill-modal');
if (!modal) return;
skillModalAddMode = false;
skillFileDirty = false;
skillActivePath = 'SKILL.md';
const pkg = document.getElementById('skill-package-editor');
const addEd = document.getElementById('skill-add-editor');
if (pkg) pkg.style.display = 'block';
if (addEd) addEd.style.display = 'none';
document.getElementById('skill-modal-title').textContent = _t('skills.editSkill');
document.getElementById('skill-name').value = '';
document.getElementById('skill-name').disabled = true;
document.getElementById('skill-description').value = '';
const ta = document.getElementById('skill-content');
if (ta) ta.value = '';
openAppModal('skill-modal', { focus: false });
try {
const [detailRes, filesRes] = await Promise.all([
apiFetch(`/api/skills/${encodeURIComponent(skillId)}?depth=full`),
@@ -565,39 +581,24 @@ async function editSkill(skillId) {
}
const data = await detailRes.json();
const skill = data.skill;
const modal = document.getElementById('skill-modal');
if (!modal) return;
skillModalAddMode = false;
skillFileDirty = false;
skillActivePath = 'SKILL.md';
const pkg = document.getElementById('skill-package-editor');
const addEd = document.getElementById('skill-add-editor');
if (pkg) pkg.style.display = 'block';
if (addEd) addEd.style.display = 'none';
document.getElementById('skill-modal-title').textContent = _t('skills.editSkill');
document.getElementById('skill-name').value = skill.id || skillId;
document.getElementById('skill-name').disabled = true;
document.getElementById('skill-description').value = skill.description || '';
let files = [];
if (filesRes.ok) {
const fd = await filesRes.json();
skillPackageFiles = fd.files || [];
} else {
skillPackageFiles = [];
files = fd.files || [];
}
renderSkillPackageTree();
const ta = document.getElementById('skill-content');
if (ta) ta.value = skill.content || '';
const hint = document.getElementById('skill-body-hint-edit');
if (hint) hint.style.display = 'block';
currentEditingSkillName = skillId;
modal.style.display = 'flex';
deferModalContent(() => {
document.getElementById('skill-name').value = skill.id || skillId;
document.getElementById('skill-description').value = skill.description || '';
skillPackageFiles = files;
renderSkillPackageTree();
if (ta) ta.value = skill.content || '';
const hint = document.getElementById('skill-body-hint-edit');
if (hint) hint.style.display = 'block';
document.getElementById('skill-name')?.focus();
});
} catch (error) {
closeSkillModal();
console.error('加载skill详情失败:', error);
showNotification(_t('skills.loadDetailFailed') + ': ' + error.message, 'error');
}
@@ -659,7 +660,7 @@ async function viewSkill(skillId) {
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'flex';
openAppModal(modal);
const close = () => closeSkillViewModal();
modal.querySelectorAll('[data-skill-view-close]').forEach(el => el.addEventListener('click', close));
@@ -691,23 +692,22 @@ async function viewSkill(skillId) {
// 关闭查看模态框
function closeSkillViewModal() {
closeAppModal('skill-view-modal');
const modal = document.getElementById('skill-view-modal');
if (modal) {
modal.remove();
syncAppModalBodyLock();
}
}
// 关闭skill模态框
function closeSkillModal() {
const modal = document.getElementById('skill-modal');
if (modal) {
modal.style.display = 'none';
currentEditingSkillName = null;
skillModalAddMode = true;
skillFileDirty = false;
skillPackageFiles = [];
skillActivePath = 'SKILL.md';
}
closeAppModal('skill-modal');
currentEditingSkillName = null;
skillModalAddMode = true;
skillFileDirty = false;
skillPackageFiles = [];
skillActivePath = 'SKILL.md';
}
// 保存skill
+18 -25
View File
@@ -914,18 +914,14 @@ async function showBatchImportModal() {
}
}
await refreshBatchProjectSelectOptions();
modal.style.display = 'block';
input.focus();
openAppModal('batch-import-modal', { focusEl: input });
}
}
// 关闭新建任务模态框
function closeBatchImportModal() {
const modal = document.getElementById('batch-import-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('batch-import-modal');
}
function handleBatchScheduleModeChange() {
@@ -1350,7 +1346,13 @@ async function showBatchQueueDetail(queueId) {
const addTaskBtn = document.getElementById('batch-queue-add-task-btn');
if (!modal || !content) return;
const alreadyOpen = isAppModalOpen('batch-queue-detail-modal');
if (!alreadyOpen) {
if (content) content.innerHTML = '<p style="color:#64748b;margin:0;">…</p>';
openAppModal('batch-queue-detail-modal', { focus: false });
}
try {
// 加载角色列表(如果还未加载)
let loadedRoles = [];
@@ -1459,6 +1461,7 @@ async function showBatchQueueDetail(queueId) {
const sameQueueAsBefore = prevDetailFor === queue.id;
const savedTechDetailsOpen = sameQueueAsBefore && !!(prevTechDetails && prevTechDetails.open);
deferModalContent(function () {
content.innerHTML = `
<div class="batch-queue-detail-layout" data-bq-detail-for="${escapeHtml(queue.id)}">
<section class="batch-queue-detail-hero">
@@ -1529,8 +1532,7 @@ async function showBatchQueueDetail(queueId) {
if (newTechDetails && savedTechDetailsOpen) {
newTechDetails.open = true;
}
modal.style.display = 'block';
});
// 仅运行中定时拉取详情;其它状态应停止,避免 innerHTML 重绘把 <details> 等 UI 打回默认态
if (queue.status === 'running') {
@@ -1540,6 +1542,7 @@ async function showBatchQueueDetail(queueId) {
}
} catch (error) {
console.error('获取队列详情失败:', error);
closeBatchQueueDetailModal();
alert(_t('tasks.getQueueDetailFailed') + ': ' + error.message);
}
}
@@ -1708,10 +1711,7 @@ async function deleteBatchQueueFromList(queueId) {
// 关闭批量任务队列详情模态框
function closeBatchQueueDetailModal() {
const modal = document.getElementById('batch-queue-detail-modal');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('batch-queue-detail-modal');
batchQueuesState.currentQueueId = null;
stopBatchQueueRefresh();
}
@@ -1730,7 +1730,7 @@ function startBatchQueueRefresh(queueId) {
content.querySelector('.bq-inline-edit-controls') ||
content.querySelector('.batch-task-inline-edit')
);
if ((addModal && addModal.style.display === 'block') || hasInlineEdit) {
if ((addModal && isAppModalOpen('add-batch-task-modal')) || hasInlineEdit) {
return;
}
if (batchQueuesState._bqDetailRefreshing) {
@@ -1891,12 +1891,7 @@ function showAddBatchTaskModal() {
}
messageInput.value = '';
modal.style.display = 'block';
// 聚焦到输入框
setTimeout(() => {
messageInput.focus();
}, 100);
openAppModal('add-batch-task-modal', { focusEl: messageInput });
// 清理旧的事件监听器
if (showAddBatchTaskModal._escHandler) {
@@ -1940,9 +1935,7 @@ function closeAddBatchTaskModal() {
}
const modal = document.getElementById('add-batch-task-modal');
const messageInput = document.getElementById('add-task-message');
if (modal) {
modal.style.display = 'none';
}
closeAppModal('add-batch-task-modal');
if (messageInput) {
messageInput.value = '';
}
@@ -2462,7 +2455,7 @@ document.addEventListener('languagechange', function () {
const detailModal = document.getElementById('batch-queue-detail-modal');
if (
detailModal &&
detailModal.style.display === 'block' &&
isAppModalOpen('batch-queue-detail-modal') &&
batchQueuesState.currentQueueId
) {
showBatchQueueDetail(batchQueuesState.currentQueueId);
+24 -25
View File
@@ -1090,37 +1090,36 @@ async function showAddVulnerabilityModal() {
document.getElementById('vulnerability-impact').value = '';
document.getElementById('vulnerability-recommendation').value = '';
document.getElementById('vulnerability-modal').style.display = 'block';
openAppModal('vulnerability-modal');
}
// 编辑漏洞
async function editVulnerability(id) {
try {
const response = await apiFetch(`/api/vulnerabilities/${id}`);
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
const vuln = await response.json();
currentVulnerabilityId = id;
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.editVuln');
// 填充表单
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || '';
document.getElementById('vulnerability-task-tag').value = vuln.task_tag || '';
document.getElementById('vulnerability-title').value = vuln.title || '';
document.getElementById('vulnerability-description').value = vuln.description || '';
document.getElementById('vulnerability-severity').value = vuln.severity || '';
document.getElementById('vulnerability-status').value = vuln.status || 'open';
document.getElementById('vulnerability-type').value = vuln.type || '';
document.getElementById('vulnerability-target').value = vuln.target || '';
document.getElementById('vulnerability-proof').value = vuln.proof || '';
document.getElementById('vulnerability-impact').value = vuln.impact || '';
document.getElementById('vulnerability-recommendation').value = vuln.recommendation || '';
await populateVulnerabilityModalProjectSelect(vuln.project_id || '');
document.getElementById('vulnerability-modal').style.display = 'block';
openAppModal('vulnerability-modal', { focus: false });
const response = await apiFetch(`/api/vulnerabilities/${id}`);
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
const vuln = await response.json();
deferModalContent(async () => {
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || '';
document.getElementById('vulnerability-task-tag').value = vuln.task_tag || '';
document.getElementById('vulnerability-title').value = vuln.title || '';
document.getElementById('vulnerability-description').value = vuln.description || '';
document.getElementById('vulnerability-severity').value = vuln.severity || '';
document.getElementById('vulnerability-status').value = vuln.status || 'open';
document.getElementById('vulnerability-type').value = vuln.type || '';
document.getElementById('vulnerability-target').value = vuln.target || '';
document.getElementById('vulnerability-proof').value = vuln.proof || '';
document.getElementById('vulnerability-impact').value = vuln.impact || '';
document.getElementById('vulnerability-recommendation').value = vuln.recommendation || '';
await populateVulnerabilityModalProjectSelect(vuln.project_id || '');
document.getElementById('vulnerability-title')?.focus();
});
} catch (error) {
closeVulnerabilityModal();
console.error('加载漏洞失败:', error);
alert(vulnT('vulnerability.loadFailed') + ': ' + error.message);
}
@@ -1233,7 +1232,7 @@ async function deleteVulnerability(id) {
// 关闭漏洞模态框
function closeVulnerabilityModal() {
document.getElementById('vulnerability-modal').style.display = 'none';
closeAppModal('vulnerability-modal');
currentVulnerabilityId = null;
}
@@ -1749,7 +1748,7 @@ async function refreshVulnerabilityProjectFilter() {
sel.innerHTML = html;
if (cur) sel.value = cur;
const modalSel = document.getElementById('vulnerability-project-id');
if (modalSel && document.getElementById('vulnerability-modal')?.style.display === 'block') {
if (modalSel && isAppModalOpen('vulnerability-modal')) {
const modalCur = modalSel.value || '';
modalSel.innerHTML = buildVulnerabilityProjectOptionsHtml(modalCur);
modalSel.value = modalCur;
+290 -30
View File
@@ -27,6 +27,9 @@ const WEBSHELL_HISTORY_MAX = 100;
let webshellClearInProgress = false;
// AI 助手:按连接 ID 保存对话 ID,便于多轮对话
let webshellAiConvMap = {};
// AI 助手:项目绑定(已有对话按 convId,新对话按 connId 草稿)
let webshellAiProjectByConvId = {};
let webshellAiDraftProjectByConn = {};
let webshellAiSending = false;
let webshellAiAbortController = null; // AbortController for current AI stream
let webshellAiStreamReader = null; // Current ReadableStreamDefaultReader
@@ -266,6 +269,7 @@ function wsToggleRolePanel() {
var isOpen = panel.style.display === 'flex';
if (isOpen) { wsCloseRolePanel(); return; }
wsCloseAgentModePanel();
wsCloseProjectPanel();
panel.style.display = 'flex';
}
function wsCloseRolePanel() {
@@ -340,6 +344,7 @@ function wsToggleAgentModePanel() {
var isOpen = panel.style.display === 'flex';
if (isOpen) { wsCloseAgentModePanel(); return; }
wsCloseRolePanel();
wsCloseProjectPanel();
panel.style.display = 'flex';
}
function wsCloseAgentModePanel() {
@@ -347,10 +352,204 @@ function wsCloseAgentModePanel() {
if (panel) panel.style.display = 'none';
}
// ─── WebShell AI 项目选择器(与主「对话」页对齐) ───
function wsProjectT(key, fallback) {
if (typeof window.t === 'function') {
var v = window.t(key);
if (v && v !== key) return v;
}
return fallback;
}
function getWebshellAiConvId(conn) {
if (!conn || !conn.id) return '';
return webshellAiConvMap[conn.id] || '';
}
function getWebshellAiProjectSelection(conn) {
if (!conn || !conn.id) return '';
var convId = getWebshellAiConvId(conn);
if (convId) return webshellAiProjectByConvId[convId] || '';
return webshellAiDraftProjectByConn[conn.id] || '';
}
function wsSetWebshellAiProject(conn, projectId) {
if (!conn || !conn.id) return;
var pid = projectId || '';
var convId = getWebshellAiConvId(conn);
if (convId) {
if (pid) webshellAiProjectByConvId[convId] = pid;
else delete webshellAiProjectByConvId[convId];
} else if (pid) {
webshellAiDraftProjectByConn[conn.id] = pid;
} else {
delete webshellAiDraftProjectByConn[conn.id];
}
wsUpdateProjectButtonLabel();
}
function wsIsActiveProjectId(id) {
if (!id) return false;
var map = window.projectNameById || {};
return !!map[id];
}
function wsResolveWebshellAiProjectSelection(conn) {
var raw = getWebshellAiProjectSelection(conn);
if (!raw) return '';
return wsIsActiveProjectId(raw) ? raw : '';
}
function wsUpdateProjectButtonLabel() {
var textEl = document.getElementById('ws-project-text');
if (!textEl || !webshellCurrentConn) return;
var id = wsResolveWebshellAiProjectSelection(webshellCurrentConn);
var nameMap = window.projectNameById || {};
textEl.textContent = id && nameMap[id] ? nameMap[id] : wsProjectT('projects.noProject', '无项目');
}
async function wsRenderProjectPanelList() {
var list = document.getElementById('ws-project-list');
if (!list || !webshellCurrentConn) return;
var conn = webshellCurrentConn;
var selected = wsResolveWebshellAiProjectSelection(conn);
var projects = [];
try {
if (typeof window.fetchAllProjects === 'function') {
projects = await window.fetchAllProjects(false);
}
} catch (e) {
list.innerHTML = '<div class="chat-project-panel-empty">' + escapeHtml(wsProjectT('projects.loadFailedRetry', '加载失败,请重试')) + '</div>';
return;
}
if (typeof window.rebuildProjectNameMap === 'function') {
window.rebuildProjectNameMap(projects);
}
var activeProjects = projects.filter(function (p) { return p.status !== 'archived'; });
var items = [{ id: '', name: wsProjectT('projects.noProject', '无项目'), description: wsProjectT('projects.noProjectDescription', '不绑定项目') }].concat(activeProjects);
list.innerHTML = '';
items.forEach(function (p) {
var isNone = !p.id;
var isSelected = isNone ? !selected : selected === p.id;
var desc = isNone
? (p.description || '')
: ((p.description || '').trim().slice(0, 80) || wsProjectT('projects.sharedFactBoard', '共享事实黑板'));
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : '');
btn.setAttribute('role', 'option');
btn.onclick = function () { wsSelectProject(p.id || ''); };
btn.innerHTML = '<div class="role-selection-item-icon-main">' + (isNone ? '—' : '📁') + '</div>' +
'<div class="role-selection-item-content-main">' +
'<div class="role-selection-item-name-main">' + escapeHtml(p.name || '未命名') + '</div>' +
'<div class="role-selection-item-description-main">' + escapeHtml(desc) + '</div></div>' +
(isSelected ? '<div class="role-selection-checkmark-main">✓</div>' : '');
list.appendChild(btn);
});
}
async function wsRenderProjectPanel() {
var list = document.getElementById('ws-project-list');
if (!list) return;
list.innerHTML = '<div class="chat-project-panel-loading">' + escapeHtml(wsProjectT('common.loading', '加载中...')) + '</div>';
await wsRenderProjectPanelList();
}
function wsCloseProjectPanel() {
var panel = document.getElementById('ws-project-panel');
var btn = document.getElementById('ws-project-btn');
if (panel) panel.style.display = 'none';
if (btn) {
btn.classList.remove('active');
btn.setAttribute('aria-expanded', 'false');
}
}
async function wsToggleProjectPanel() {
var panel = document.getElementById('ws-project-panel');
var btn = document.getElementById('ws-project-btn');
if (!panel) return;
var isHidden = panel.style.display === 'none' || !panel.style.display;
if (!isHidden) {
wsCloseProjectPanel();
return;
}
wsCloseRolePanel();
wsCloseAgentModePanel();
panel.style.display = 'flex';
if (btn) {
btn.classList.add('active');
btn.setAttribute('aria-expanded', 'true');
}
await wsRenderProjectPanel();
}
async function wsSelectProject(projectId) {
wsCloseProjectPanel();
await applyWebshellAiProjectSelection(projectId || '');
}
async function applyWebshellAiProjectSelection(projectId) {
var conn = webshellCurrentConn;
if (!conn || !conn.id) return;
var prev = getWebshellAiProjectSelection(conn);
if (projectId === prev) {
wsUpdateProjectButtonLabel();
return;
}
var convId = getWebshellAiConvId(conn);
if (convId) {
try {
var res = await apiFetch('/api/conversations/' + encodeURIComponent(convId) + '/project', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: projectId }),
});
if (!res.ok) {
var err = await res.json().catch(function () { return {}; });
throw new Error(err.error || res.statusText);
}
wsSetWebshellAiProject(conn, projectId);
if (typeof showNotification === 'function') {
showNotification(
projectId ? wsProjectT('projects.projectBound', '已绑定项目') : wsProjectT('projects.projectUnbound', '已解除项目绑定'),
'success'
);
}
} catch (e) {
console.error(e);
alert(wsProjectT('projects.updateProjectBindingFailed', '更新项目绑定失败') + ': ' + (e.message || e));
wsUpdateProjectButtonLabel();
return;
}
} else {
wsSetWebshellAiProject(conn, projectId);
}
wsUpdateProjectButtonLabel();
}
function showNewProjectModalFromWebshellAi() {
wsCloseProjectPanel();
if (webshellCurrentConn && webshellCurrentConn.id) {
window._projectModalFromWebshellConnId = webshellCurrentConn.id;
}
window._projectModalFromChat = false;
if (typeof showNewProjectModal === 'function') showNewProjectModal();
}
window.applyWebshellAiProjectSelection = applyWebshellAiProjectSelection;
window.showNewProjectModalFromWebshellAi = showNewProjectModalFromWebshellAi;
window.wsToggleProjectPanel = wsToggleProjectPanel;
window.wsCloseProjectPanel = wsCloseProjectPanel;
// ─── end WebShell AI 项目选择器 ───
/** 当 WebShell AI Tab 可见时刷新选择器显示(同步主页可能的更改) */
function wsRefreshSelectors() {
wsUpdateRoleSelectorDisplay();
wsRenderRoleList();
wsUpdateProjectButtonLabel();
var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'eino_single';
if (stored !== 'eino_single' && stored !== 'deep' && stored !== 'plan_execute' && stored !== 'supervisor') {
stored = 'eino_single';
@@ -370,6 +569,11 @@ document.addEventListener('click', function (e) {
if (modePanel && modePanel.style.display !== 'none' && modeBtn && !modePanel.contains(e.target) && !modeBtn.contains(e.target)) {
wsCloseAgentModePanel();
}
var projectPanel = document.getElementById('ws-project-panel');
var projectBtn = document.getElementById('ws-project-btn');
if (projectPanel && projectPanel.style.display !== 'none' && projectBtn && !projectPanel.contains(e.target) && !projectBtn.contains(e.target)) {
wsCloseProjectPanel();
}
});
// ─── end WebShell AI 选择器 ───
@@ -1873,6 +2077,7 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
apiFetch('/api/conversations/' + encodeURIComponent(convId) + '?include_process_details=1', { method: 'GET' })
.then(function (r) { return r.json(); })
.then(function (data) {
wsSetWebshellAiProject(conn, data.projectId || data.project_id || '');
messagesContainer.innerHTML = '';
var list = data.messages || [];
list.forEach(function (msg) {
@@ -1893,9 +2098,14 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
}
}
messagesContainer.appendChild(div);
if (role === 'assistant' && msg.processDetails && msg.processDetails.length > 0) {
var block = renderWebshellProcessDetailsBlock(msg.processDetails, true);
if (block) messagesContainer.appendChild(block);
if (role === 'assistant') {
var wsMergedDetails = (typeof window.mergeMessageReasoningContentIntoProcessDetails === 'function')
? window.mergeMessageReasoningContentIntoProcessDetails(msg.processDetails || [], msg.reasoningContent)
: (msg.processDetails || []);
if (wsMergedDetails.length > 0) {
var block = renderWebshellProcessDetailsBlock(wsMergedDetails, true);
if (block) messagesContainer.appendChild(block);
}
}
});
if (list.length === 0) {
@@ -2003,6 +2213,25 @@ function selectWebshell(id, stateReady) {
'<div id="webshell-ai-messages" class="webshell-ai-messages"></div>' +
'<div class="webshell-ai-input-area">' +
'<div class="webshell-ai-selectors-row">' +
'<div class="ws-project-selector-wrapper project-selector-wrapper">' +
'<button type="button" id="ws-project-btn" class="role-selector-btn" onclick="wsToggleProjectPanel()" aria-label="' + escapeHtml(wsProjectT('projects.chatSelectorButton', '选择项目')) + '" aria-haspopup="listbox" aria-expanded="false" title="' + escapeHtml(wsProjectT('projects.chatSelectorButton', '绑定项目后共享事实黑板(跨对话)')) + '">' +
'<span class="role-selector-icon" aria-hidden="true">📁</span>' +
'<span id="ws-project-text" class="role-selector-text">' + escapeHtml(wsProjectT('projects.noProject', '无项目')) + '</span>' +
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
'</button>' +
'<div id="ws-project-panel" class="role-selection-panel chat-project-panel" style="display:none;" role="listbox">' +
'<div class="role-selection-panel-header">' +
'<h3 class="role-selection-panel-title">' + escapeHtml(wsProjectT('projects.selectProject', '选择项目')) + '</h3>' +
'<button type="button" class="role-selection-panel-close" onclick="wsCloseProjectPanel()" title="' + escapeHtml(wsProjectT('common.close', '关闭')) + '" aria-label="' + escapeHtml(wsProjectT('common.close', '关闭')) + '">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
'</div>' +
'<div class="chat-project-panel-body">' +
'<div id="ws-project-list" class="role-selection-list-main"></div>' +
'<div class="chat-project-panel-footer">' +
'<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromWebshellAi()">' +
'<span class="chat-project-panel-create-icon" aria-hidden="true">+</span>' +
'<span class="chat-project-panel-create-label">' + escapeHtml(wsProjectT('projects.newProject', '新建项目')) + '</span>' +
'</button></div></div></div></div>' +
'<div class="ws-role-selector-wrapper">' +
'<button type="button" class="role-selector-btn ws-role-selector-btn" id="ws-role-selector-btn" onclick="wsToggleRolePanel()">' +
'<span id="ws-role-selector-icon" class="role-selector-icon">\ud83d\udd35</span>' +
@@ -2174,9 +2403,11 @@ function selectWebshell(id, stateReady) {
var aiNewConvBtn = document.getElementById('webshell-ai-new-conv');
var aiConvListEl = document.getElementById('webshell-ai-conv-list');
// 初始化角色 + 模式选择器
// 初始化角色 + 模式 + 项目选择器
wsLoadRoles();
wsInitAgentMode();
if (typeof prefetchProjectsForChat === 'function') prefetchProjectsForChat();
wsUpdateProjectButtonLabel();
var aiMemoInput = document.getElementById('webshell-ai-memo-input');
var aiMemoStatus = document.getElementById('webshell-ai-memo-status');
var aiMemoClearBtn = document.getElementById('webshell-ai-memo-clear');
@@ -2225,6 +2456,8 @@ function selectWebshell(id, stateReady) {
if (aiNewConvBtn) {
aiNewConvBtn.addEventListener('click', function () {
delete webshellAiConvMap[conn.id];
delete webshellAiDraftProjectByConn[conn.id];
wsUpdateProjectButtonLabel();
if (aiMessages) {
aiMessages.innerHTML = '';
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
@@ -2301,10 +2534,14 @@ function selectWebshell(id, stateReady) {
function setDbProfileModalVisible(visible, mode) {
if (!dbProfileModalEl) return;
dbProfileModalEl.style.display = visible ? 'block' : 'none';
if (dbProfileModalTitleEl) {
if (mode === 'add') dbProfileModalTitleEl.textContent = wsT('webshell.dbAddProfile') || '新增连接';
else dbProfileModalTitleEl.textContent = wsT('webshell.editConnectionTitle') || '编辑连接';
if (visible) {
if (dbProfileModalTitleEl) {
if (mode === 'add') dbProfileModalTitleEl.textContent = wsT('webshell.dbAddProfile') || '新增连接';
else dbProfileModalTitleEl.textContent = wsT('webshell.editConnectionTitle') || '编辑连接';
}
openAppModal(dbProfileModalEl);
} else {
closeAppModal(dbProfileModalEl);
}
}
@@ -2763,7 +3000,15 @@ function loadWebshellAiHistory(conn, messagesContainer) {
return apiFetch('/api/webshell/connections/' + encodeURIComponent(conn.id) + '/ai-history', { method: 'GET' })
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.conversationId) webshellAiConvMap[conn.id] = data.conversationId;
if (data.conversationId) {
webshellAiConvMap[conn.id] = data.conversationId;
apiFetch('/api/conversations/' + encodeURIComponent(data.conversationId), { method: 'GET' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (conv) {
if (conv) wsSetWebshellAiProject(conn, conv.projectId || conv.project_id || '');
})
.catch(function () { /* ignore */ });
}
var list = Array.isArray(data.messages) ? data.messages : [];
list.forEach(function (msg) {
var role = (msg.role || '').toLowerCase();
@@ -2783,9 +3028,14 @@ function loadWebshellAiHistory(conn, messagesContainer) {
}
}
messagesContainer.appendChild(div);
if (role === 'assistant' && msg.processDetails && msg.processDetails.length > 0) {
var block = renderWebshellProcessDetailsBlock(msg.processDetails, true);
if (block) messagesContainer.appendChild(block);
if (role === 'assistant') {
var wsHistMerged = (typeof window.mergeMessageReasoningContentIntoProcessDetails === 'function')
? window.mergeMessageReasoningContentIntoProcessDetails(msg.processDetails || [], msg.reasoningContent)
: (msg.processDetails || []);
if (wsHistMerged.length > 0) {
var block = renderWebshellProcessDetailsBlock(wsHistMerged, true);
if (block) messagesContainer.appendChild(block);
}
}
});
if (list.length === 0) {
@@ -2918,6 +3168,10 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
conversationId: convId,
role: wsRole
};
if (!convId) {
var wsPid = getWebshellAiProjectSelection(conn);
if (wsPid) body.projectId = wsPid;
}
// 流式输出:支持 progress 实时更新、response 打字机效果;若后端发送多段 response 则追加
var streamingTarget = ''; // 当前要打字显示的目标全文(用于打字机效果)
@@ -2966,6 +3220,11 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
if (_et === 'conversation' && _ed.conversationId) {
var convId = _ed.conversationId;
var prevDraft = webshellAiDraftProjectByConn[conn.id];
if (prevDraft) {
webshellAiProjectByConvId[convId] = prevDraft;
delete webshellAiDraftProjectByConn[conn.id];
}
webshellAiConvMap[conn.id] = convId;
var listEl = document.getElementById('webshell-ai-conv-list');
if (listEl) fetchAndRenderWebshellAiConvList(conn, listEl).then(function () {
@@ -4369,37 +4628,38 @@ function showAddWebshellModal() {
var titleEl = document.getElementById('webshell-modal-title');
if (titleEl) titleEl.textContent = wsT('webshell.addConnection');
var modal = document.getElementById('webshell-modal');
if (modal) modal.style.display = 'block';
if (modal) openAppModal(modal);
}
// 打开编辑连接弹窗(预填当前连接信息)
function showEditWebshellModal(connId) {
var conn = webshellConnections.find(function (c) { return c.id === connId; });
if (!conn) return;
var editIdEl = document.getElementById('webshell-edit-id');
if (editIdEl) editIdEl.value = conn.id;
document.getElementById('webshell-url').value = conn.url || '';
document.getElementById('webshell-password').value = conn.password || '';
document.getElementById('webshell-type').value = conn.type || 'php';
document.getElementById('webshell-method').value = (conn.method || 'post').toLowerCase();
document.getElementById('webshell-cmd-param').value = conn.cmdParam || '';
var osEditEl = document.getElementById('webshell-os');
if (osEditEl) osEditEl.value = normalizeWebshellOS(conn.os);
var encEditEl = document.getElementById('webshell-encoding');
if (encEditEl) encEditEl.value = normalizeWebshellEncoding(conn.encoding);
document.getElementById('webshell-remark').value = conn.remark || '';
var titleEl = document.getElementById('webshell-modal-title');
if (titleEl) titleEl.textContent = wsT('webshell.editConnectionTitle');
var modal = document.getElementById('webshell-modal');
if (modal) modal.style.display = 'block';
openAppModal('webshell-modal', { focus: false });
deferModalContent(function () {
var editIdEl = document.getElementById('webshell-edit-id');
if (editIdEl) editIdEl.value = conn.id;
document.getElementById('webshell-url').value = conn.url || '';
document.getElementById('webshell-password').value = conn.password || '';
document.getElementById('webshell-type').value = conn.type || 'php';
document.getElementById('webshell-method').value = (conn.method || 'post').toLowerCase();
document.getElementById('webshell-cmd-param').value = conn.cmdParam || '';
var osEditEl = document.getElementById('webshell-os');
if (osEditEl) osEditEl.value = normalizeWebshellOS(conn.os);
var encEditEl = document.getElementById('webshell-encoding');
if (encEditEl) encEditEl.value = normalizeWebshellEncoding(conn.encoding);
document.getElementById('webshell-remark').value = conn.remark || '';
document.getElementById('webshell-url')?.focus();
});
}
// 关闭弹窗
function closeWebshellModal() {
var editIdEl = document.getElementById('webshell-edit-id');
if (editIdEl) editIdEl.value = '';
var modal = document.getElementById('webshell-modal');
if (modal) modal.style.display = 'none';
closeAppModal('webshell-modal');
}
// 语言切换时刷新 WebShell 页面内所有由 JS 生成的文案(不重建终端)
@@ -4571,7 +4831,7 @@ function refreshWebshellUIOnLanguageChange() {
}
var modal = document.getElementById('webshell-modal');
if (modal && modal.style.display === 'block') {
if (modal && isAppModalOpen('webshell-modal')) {
var titleEl = document.getElementById('webshell-modal-title');
var editIdEl = document.getElementById('webshell-edit-id');
if (titleEl) {
File diff suppressed because one or more lines are too long
+6696
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(function(){return(()=>{"use strict";var e={775:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0;var r=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core;if(0!==e._renderService.dimensions.actualCellWidth&&0!==e._renderService.dimensions.actualCellHeight){var t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),i=Math.max(0,parseInt(t.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),o=r-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=i-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(o/e._renderService.dimensions.actualCellHeight))}}}},e}();t.FitAddon=r}},t={};return function r(i){if(t[i])return t[i].exports;var n=t[i]={exports:{}};return e[i](n,n.exports,r),n.exports}(775)})()}));
//# sourceMappingURL=xterm-addon-fit.js.map
+190
View File
@@ -0,0 +1,190 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
opacity: 0.5;
}
.xterm-underline {
text-decoration: underline;
}
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
.xterm-decoration-overview-ruler {
z-index: 7;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}
.xterm-decoration-top {
z-index: 2;
position: relative;
}
+2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -934,7 +934,7 @@ Content-Type: application/json
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
<script src="/static/vendor/i18next.min.js"></script>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/api-docs.js"></script>
</body>
+41 -22
View File
@@ -8,7 +8,8 @@
<link rel="shortcut icon" type="image/png" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/c2.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
<link rel="stylesheet" href="/static/vendor/xterm.css">
<script src="/static/js/router.js"></script>
</head>
<body>
<div id="login-overlay" class="login-overlay" style="display: none;">
@@ -1471,18 +1472,20 @@
<div class="projects-detail-inner" id="projects-detail-inner" hidden>
<header class="projects-detail-header">
<div class="projects-detail-header-main">
<div class="projects-detail-title-row">
<h3 id="projects-detail-title" class="projects-detail-title" data-i18n="projects.defaultProjectName">项目</h3>
<span id="projects-detail-status" class="projects-status-pill projects-status-pill--active" data-i18n="projects.statusActive">进行中</span>
<div class="projects-detail-headline">
<div class="projects-detail-title-group">
<h3 id="projects-detail-title" class="projects-detail-title" data-i18n="projects.defaultProjectName">项目</h3>
<span id="projects-detail-status" class="projects-status-pill projects-status-pill--active" data-i18n="projects.statusActive">进行中</span>
</div>
<div class="projects-detail-stats" id="projects-detail-stats">
<span class="projects-stat-chip projects-stat-chip--facts" id="project-stat-facts">0 条事实</span>
<span class="projects-stat-chip projects-stat-chip--vulns" id="project-stat-vulns">0 个漏洞</span>
<span class="projects-stat-chip projects-stat-chip--conversations" id="project-stat-conversations">0 个对话</span>
<span class="projects-stat-chip projects-stat-chip--warn" id="project-stat-sparse" hidden>0 待补全</span>
</div>
</div>
<p id="projects-detail-meta" class="projects-detail-meta"></p>
<p id="projects-detail-desc" class="projects-detail-desc"></p>
<div class="projects-detail-stats" id="projects-detail-stats">
<span class="projects-stat-chip" id="project-stat-facts">0 条事实</span>
<span class="projects-stat-chip" id="project-stat-vulns">0 个漏洞</span>
<span class="projects-stat-chip" id="project-stat-conversations">0 个对话</span>
<span class="projects-stat-chip projects-stat-chip--warn" id="project-stat-sparse" hidden>0 待补全</span>
</div>
<p id="projects-detail-desc" class="projects-detail-desc" hidden></p>
</div>
<div class="projects-detail-header-actions">
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.vulnerabilityManagement">漏洞管理</button>
@@ -1669,7 +1672,8 @@
</div>
<div class="projects-form-field">
<label for="project-edit-description" data-i18n="projects.projectDescription">描述</label>
<textarea id="project-edit-description" class="form-input" rows="3" placeholder="测试目标、授权范围、联系人、注意事项…" data-i18n="projects.editDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<textarea id="project-edit-description" class="form-input projects-description-textarea" rows="3" maxlength="4000" placeholder="测试目标、授权范围、联系人、注意事项…" data-i18n="projects.editDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<small class="form-hint" data-i18n="projects.descriptionLengthHint">简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body</small>
</div>
</div>
</section>
@@ -3480,16 +3484,16 @@
</div>
</div>
<!-- Marked.js + DOMPurify:本地 vendor避免 CDN 不可用导致 Markdown 降级为纯文本 -->
<!-- Marked.js + DOMPurify + 其他前端依赖:本地 vendor内网/离线部署不依赖 CDN -->
<script src="/static/vendor/marked.min.js"></script>
<script src="/static/vendor/purify.min.js"></script>
<script src="/static/js/sanitize-markdown.js"></script>
<!-- Cytoscape.js for attack chain visualization -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script>
<script src="/static/vendor/cytoscape.min.js"></script>
<!-- ELK.js for high-quality DAG layout (reduces edge crossings) -->
<script src="https://cdn.jsdelivr.net/npm/elkjs@0.9.2/lib/elk.bundled.js"></script>
<script src="/static/vendor/elk.bundled.js"></script>
<!-- SheetJS for XLSX export (info-collect) -->
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script src="/static/vendor/xlsx.full.min.js"></script>
<script>
// 确保ELK对象全局可用
if (typeof ELK === 'undefined' && typeof elk !== 'undefined') {
@@ -3777,6 +3781,20 @@
</div>
</div>
<!-- 项目列表操作菜单 -->
<div id="projects-list-action-menu" class="context-menu" style="display: none;" role="menu">
<div id="projects-list-menu-edit" class="context-menu-item" onclick="editProjectFromListMenu()">
<span id="projects-list-menu-edit-text"></span>
</div>
<div id="projects-list-menu-archive" class="context-menu-item" onclick="toggleProjectArchiveFromListMenu()">
<span id="projects-list-menu-archive-text"></span>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item context-menu-item-danger" onclick="deleteProjectFromListMenu()">
<span id="projects-list-menu-delete-text"></span>
</div>
</div>
<!-- 新建任务模态框 -->
<div id="batch-import-modal" class="modal">
<div class="modal-content" style="max-width: 800px;">
@@ -4155,11 +4173,12 @@
<div class="projects-modal-body">
<div class="projects-form-field">
<label for="project-modal-name" data-i18n="projects.projectName">项目名称 <span class="required">*</span></label>
<input type="text" id="project-modal-name" class="form-input" placeholder="例如:某客户 Web 渗透" autocomplete="off" data-i18n="projects.projectNamePlaceholder" data-i18n-attr="placeholder">
<input type="text" id="project-modal-name" class="form-input" maxlength="200" placeholder="例如:某客户 Web 渗透" autocomplete="off" data-i18n="projects.projectNamePlaceholder" data-i18n-attr="placeholder">
</div>
<div class="projects-form-field">
<label for="project-modal-description" data-i18n="projects.projectDescription">项目描述</label>
<textarea id="project-modal-description" class="form-input" rows="4" placeholder="测试范围、授权边界、注意事项…" data-i18n="projects.projectDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<textarea id="project-modal-description" class="form-input projects-description-textarea" rows="4" maxlength="4000" placeholder="测试范围、授权边界、注意事项…" data-i18n="projects.projectDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<small class="form-hint" data-i18n="projects.descriptionLengthHint">简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body</small>
</div>
</div>
<div class="projects-modal-footer">
@@ -4268,13 +4287,13 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
<script src="/static/vendor/i18next.min.js"></script>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/builtin-tools.js"></script>
<script src="/static/js/auth.js"></script>
<script src="/static/js/modal.js"></script>
<script src="/static/js/notifications.js"></script>
<script src="/static/js/info-collect.js"></script>
<script src="/static/js/router.js"></script>
<script src="/static/js/agents.js"></script>
<script src="/static/js/dashboard.js"></script>
<script src="/static/js/chat-scroll.js"></script>
@@ -4284,8 +4303,8 @@
<script src="/static/js/settings.js"></script>
<script src="/static/js/audit.js"></script>
<script src="/static/js/wechat-robot.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
<script src="/static/vendor/xterm.js"></script>
<script src="/static/vendor/xterm-addon-fit.js"></script>
<script src="/static/js/terminal.js"></script>
<script src="/static/js/knowledge.js"></script>
<script src="/static/js/skills.js"></script>