Compare commits

...

15 Commits

Author SHA1 Message Date
公明 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
28 changed files with 1075 additions and 567 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
# ============================================ # ============================================
# 前端显示的版本号(可选,不填则显示默认版本) # 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.6.36" version: "v1.6.37"
# 服务器配置 # 服务器配置
server: server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口 host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

After

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 179 KiB

+12 -2
View File
@@ -12,6 +12,16 @@ import (
"go.uber.org/zap" "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 项目管理处理器。 // ProjectHandler 项目管理处理器。
type ProjectHandler struct { type ProjectHandler struct {
db *database.DB db *database.DB
@@ -48,7 +58,7 @@ func (h *ProjectHandler) CreateProject(c *gin.Context) {
} }
p := &database.Project{ p := &database.Project{
Name: strings.TrimSpace(req.Name), Name: strings.TrimSpace(req.Name),
Description: req.Description, Description: clampProjectDescription(req.Description),
ScopeJSON: req.ScopeJSON, ScopeJSON: req.ScopeJSON,
Status: strings.TrimSpace(req.Status), Status: strings.TrimSpace(req.Status),
} }
@@ -184,7 +194,7 @@ func (h *ProjectHandler) UpdateProject(c *gin.Context) {
} }
} }
if req.Description != nil { if req.Description != nil {
p.Description = *req.Description p.Description = clampProjectDescription(*req.Description)
} }
if req.ScopeJSON != nil { if req.ScopeJSON != nil {
p.ScopeJSON = *req.ScopeJSON p.ScopeJSON = *req.ScopeJSON
+107 -107
View File
@@ -2,11 +2,11 @@
set -euo pipefail set -euo pipefail
# CyberStrikeAI 一键部署启动脚本 # CyberStrikeAI one-click deploy and start script
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT_DIR" cd "$ROOT_DIR"
# 颜色定义 # Color definitions
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
@@ -14,31 +14,31 @@ BLUE='\033[0;34m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# 打印带颜色的消息 # Print colored messages
info() { echo -e "${BLUE}$1${NC}"; } info() { echo -e "${BLUE}$1${NC}"; }
success() { echo -e "${GREEN}$1${NC}"; } success() { echo -e "${GREEN}$1${NC}"; }
warning() { echo -e "${YELLOW}⚠️ $1${NC}"; } warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
error() { echo -e "${RED}$1${NC}"; } error() { echo -e "${RED}$1${NC}"; }
note() { echo -e "${CYAN}$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}" PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}"
GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
# 保存原始环境变量(用于恢复) # Save original env vars (for restoration)
ORIGINAL_PIP_INDEX_URL="${PIP_INDEX_URL:-}" ORIGINAL_PIP_INDEX_URL="${PIP_INDEX_URL:-}"
ORIGINAL_GOPROXY="${GOPROXY:-}" ORIGINAL_GOPROXY="${GOPROXY:-}"
# 进度显示函数 # Progress display helper
show_progress() { show_progress() {
local pid=$1 local pid=$1
local message=$2 local message=$2
local i=0 local i=0
local dots="" local dots=""
# 检查进程是否存在 # Check if the process exists
if ! kill -0 "$pid" 2>/dev/null; then if ! kill -0 "$pid" 2>/dev/null; then
# 进程已经结束,立即返回 # Process already finished; return immediately
return 0 return 0
fi fi
@@ -53,7 +53,7 @@ show_progress() {
printf "\r${BLUE}⏳ %s%s${NC}" "$message" "$dots" printf "\r${BLUE}⏳ %s%s${NC}" "$message" "$dots"
sleep 0.5 sleep 0.5
# 再次检查进程是否还存在 # Re-check whether the process is still running
if ! kill -0 "$pid" 2>/dev/null; then if ! kill -0 "$pid" 2>/dev/null; then
break break
fi fi
@@ -63,21 +63,21 @@ show_progress() {
echo "" echo ""
echo "==========================================" echo "=========================================="
echo " CyberStrikeAI 一键部署启动脚本" echo " CyberStrikeAI Deploy & Start Script"
echo " (默认 HTTPS 自签证书;纯 HTTP 请用: $0 --http" echo " (HTTPS with self-signed cert by default; plain HTTP: $0 --http)"
echo "==========================================" echo "=========================================="
echo "" echo ""
# 显示临时源配置信息 # Show temporary mirror/proxy info
echo "" echo ""
warning "⚠️ 注意:此脚本将使用临时镜像源加速下载" warning "Note: this script uses temporary mirrors to speed up downloads"
echo "" echo ""
info "Python pip 临时镜像源:" info "Python pip temporary mirror:"
echo " ${PIP_INDEX_URL}" echo " ${PIP_INDEX_URL}"
info "Go Proxy 临时镜像源:" info "Go temporary proxy:"
echo " ${GOPROXY}" echo " ${GOPROXY}"
echo "" echo ""
note "这些设置仅在脚本运行期间生效,不会修改系统配置" note "These settings apply only while this script runs and do not change system config"
echo "" echo ""
sleep 1 sleep 1
@@ -86,19 +86,19 @@ VENV_DIR="$ROOT_DIR/venv"
REQUIREMENTS_FILE="$ROOT_DIR/requirements.txt" REQUIREMENTS_FILE="$ROOT_DIR/requirements.txt"
BINARY_NAME="cyberstrike-ai" BINARY_NAME="cyberstrike-ai"
# 检查配置文件 # Check config file
if [ ! -f "$CONFIG_FILE" ]; then if [ ! -f "$CONFIG_FILE" ]; then
error "配置文件 config.yaml 不存在" error "Config file config.yaml not found"
info "请确保在项目根目录运行此脚本" info "Make sure you run this script from the project root"
exit 1 exit 1
fi fi
# 检查并安装 Python 环境 # Check Python environment
check_python() { check_python() {
if ! command -v python3 >/dev/null 2>&1; then if ! command -v python3 >/dev/null 2>&1; then
error "未找到 python3" error "python3 not found"
echo "" echo ""
info "请先安装 Python 3.10 或更高版本:" info "Install Python 3.10 or later first:"
echo " macOS: brew install python3" echo " macOS: brew install python3"
echo " Ubuntu: sudo apt-get install python3 python3-venv" echo " Ubuntu: sudo apt-get install python3 python3-venv"
echo " CentOS: sudo yum install python3 python3-pip" echo " CentOS: sudo yum install python3 python3-pip"
@@ -110,23 +110,23 @@ check_python() {
PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2) PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
if [ "$PYTHON_MAJOR" -lt 3 ] || ([ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 10 ]); then 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 exit 1
fi fi
success "Python 环境检查通过: $PYTHON_VERSION" success "Python check passed: $PYTHON_VERSION"
} }
# 检查并安装 Go 环境 # Check Go environment
check_go() { check_go() {
if ! command -v go >/dev/null 2>&1; then if ! command -v go >/dev/null 2>&1; then
error "未找到 Go" error "Go not found"
echo "" echo ""
info "请先安装 Go 1.21 或更高版本:" info "Install Go 1.21 or later first:"
echo " macOS: brew install go" echo " macOS: brew install go"
echo " Ubuntu: sudo apt-get install golang-go" echo " Ubuntu: sudo apt-get install golang-go"
echo " CentOS: sudo yum install golang" echo " CentOS: sudo yum install golang"
echo " 或访问: https://go.dev/dl/" echo " Or visit: https://go.dev/dl/"
exit 1 exit 1
fi fi
@@ -135,63 +135,63 @@ check_go() {
GO_MINOR=$(echo "$GO_VERSION" | cut -d. -f2) GO_MINOR=$(echo "$GO_VERSION" | cut -d. -f2)
if [ "$GO_MAJOR" -lt 1 ] || ([ "$GO_MAJOR" -eq 1 ] && [ "$GO_MINOR" -lt 21 ]); then 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 exit 1
fi fi
success "Go 环境检查通过: $(go version)" success "Go check passed: $(go version)"
} }
# 设置 Python 虚拟环境 # Set up Python virtual environment
setup_python_env() { setup_python_env() {
if [ ! -d "$VENV_DIR" ]; then if [ ! -d "$VENV_DIR" ]; then
info "创建 Python 虚拟环境..." info "Creating Python virtual environment..."
python3 -m venv "$VENV_DIR" python3 -m venv "$VENV_DIR"
success "虚拟环境创建完成" success "Virtual environment created"
else else
info "Python 虚拟环境已存在" info "Python virtual environment already exists"
fi fi
info "激活虚拟环境..." info "Activating virtual environment..."
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
if [ -f "$REQUIREMENTS_FILE" ]; then if [ -f "$REQUIREMENTS_FILE" ]; then
echo "" echo ""
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
note "⚠️ 使用临时 pip 镜像源(仅本次脚本运行有效)" note "Using temporary pip mirror (this script run only)"
note " 镜像地址: ${PIP_INDEX_URL}" note " Mirror URL: ${PIP_INDEX_URL}"
note " 如需永久配置,请设置环境变量 PIP_INDEX_URL" note " For a permanent setting, set the PIP_INDEX_URL env var"
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "" echo ""
info "升级 pip..." info "Upgrading pip..."
pip install --index-url "$PIP_INDEX_URL" --upgrade pip >/dev/null 2>&1 || true pip install --index-url "$PIP_INDEX_URL" --upgrade pip >/dev/null 2>&1 || true
info "安装 Python 依赖包..." info "Installing Python dependencies..."
echo "" echo ""
# 尝试安装依赖,捕获错误输出并显示进度 # Install deps in background; capture errors and show progress
PIP_LOG=$(mktemp) 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 pip install --index-url "$PIP_INDEX_URL" -r "$REQUIREMENTS_FILE" >"$PIP_LOG" 2>&1
echo $? > "${PIP_LOG}.exit" echo $? > "${PIP_LOG}.exit"
) & ) &
PIP_PID=$! PIP_PID=$!
# 等待一小段时间,确保进程启动 # Brief pause so the process can start
sleep 0.1 sleep 0.1
# 显示进度(如果进程还在运行) # Show progress while still running
if kill -0 "$PIP_PID" 2>/dev/null; then if kill -0 "$PIP_PID" 2>/dev/null; then
show_progress "$PIP_PID" "正在安装依赖包" show_progress "$PIP_PID" "Installing dependencies"
else else
# 进程已经结束,等待一下确保退出码文件已写入 # Process already finished; wait for exit code file
sleep 0.2 sleep 0.2
fi fi
# 等待进程完成,忽略 wait 的退出码 # Wait for completion; ignore wait exit code
wait "$PIP_PID" 2>/dev/null || true wait "$PIP_PID" 2>/dev/null || true
PIP_EXIT_CODE=0 PIP_EXIT_CODE=0
@@ -199,74 +199,74 @@ setup_python_env() {
PIP_EXIT_CODE=$(cat "${PIP_LOG}.exit" 2>/dev/null || echo "1") PIP_EXIT_CODE=$(cat "${PIP_LOG}.exit" 2>/dev/null || echo "1")
rm -f "${PIP_LOG}.exit" 2>/dev/null || true rm -f "${PIP_LOG}.exit" 2>/dev/null || true
else 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 if [ -f "$PIP_LOG" ] && grep -q -i "error\|failed\|exception" "$PIP_LOG" 2>/dev/null; then
PIP_EXIT_CODE=1 PIP_EXIT_CODE=1
fi fi
fi fi
if [ $PIP_EXIT_CODE -eq 0 ]; then if [ $PIP_EXIT_CODE -eq 0 ]; then
success "Python 依赖安装完成" success "Python dependencies installed"
else 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 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 "" echo ""
info "angr 是可选依赖,主要用于二进制分析工具" info "angr is optional and mainly used for binary analysis tools"
info "如果需要使用 angr,请先安装 Rust:" info "To use angr, install Rust first:"
echo " macOS: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" 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 " Ubuntu: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
echo " 或访问: https://rustup.rs/" echo " Or visit: https://rustup.rs/"
echo "" echo ""
info "其他依赖已安装,可以继续使用(部分工具可能不可用)" info "Other dependencies are installed; you can continue (some tools may be unavailable)"
else else
warning "部分 Python 依赖安装失败,但可以继续尝试运行" warning "Some Python dependencies failed to install, but continuing"
warning "如果遇到问题,请检查错误信息并手动安装缺失的依赖" warning "If you hit issues, check the errors and install missing packages manually"
# 显示最后几行错误信息 # Show last lines of error output
echo "" echo ""
info "错误详情(最后 10 行):" info "Error details (last 10 lines):"
tail -n 10 "$PIP_LOG" | sed 's/^/ /' tail -n 10 "$PIP_LOG" | sed 's/^/ /'
echo "" echo ""
fi fi
fi fi
rm -f "$PIP_LOG" rm -f "$PIP_LOG"
else else
warning "未找到 requirements.txt,跳过 Python 依赖安装" warning "requirements.txt not found; skipping Python dependency install"
fi fi
} }
# 构建 Go 项目 # Build Go project
build_go_project() { build_go_project() {
echo "" echo ""
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
note "⚠️ 使用临时 Go Proxy(仅本次脚本运行有效)" note "Using temporary Go proxy (this script run only)"
note " Proxy 地址: ${GOPROXY}" note " Proxy URL: ${GOPROXY}"
note " 如需永久配置,请设置环境变量 GOPROXY" note " For a permanent setting, set the GOPROXY env var"
note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" note "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "" echo ""
info "下载 Go 依赖..." info "Downloading Go dependencies..."
GO_DOWNLOAD_LOG=$(mktemp) GO_DOWNLOAD_LOG=$(mktemp)
( (
set +e # 在子shell中禁用错误退出 set +e # disable errexit in subshell
export GOPROXY="$GOPROXY" export GOPROXY="$GOPROXY"
go mod download >"$GO_DOWNLOAD_LOG" 2>&1 go mod download >"$GO_DOWNLOAD_LOG" 2>&1
echo $? > "${GO_DOWNLOAD_LOG}.exit" echo $? > "${GO_DOWNLOAD_LOG}.exit"
) & ) &
GO_DOWNLOAD_PID=$! GO_DOWNLOAD_PID=$!
# 等待一小段时间,确保进程启动 # Brief pause so the process can start
sleep 0.1 sleep 0.1
# 显示进度(如果进程还在运行) # Show progress while still running
if kill -0 "$GO_DOWNLOAD_PID" 2>/dev/null; then 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 else
# 进程已经结束,等待一下确保退出码文件已写入 # Process already finished; wait for exit code file
sleep 0.2 sleep 0.2
fi fi
# 等待进程完成,忽略 wait 的退出码 # Wait for completion; ignore wait exit code
wait "$GO_DOWNLOAD_PID" 2>/dev/null || true wait "$GO_DOWNLOAD_PID" 2>/dev/null || true
GO_DOWNLOAD_EXIT_CODE=0 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") GO_DOWNLOAD_EXIT_CODE=$(cat "${GO_DOWNLOAD_LOG}.exit" 2>/dev/null || echo "1")
rm -f "${GO_DOWNLOAD_LOG}.exit" 2>/dev/null || true rm -f "${GO_DOWNLOAD_LOG}.exit" 2>/dev/null || true
else 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 if [ -f "$GO_DOWNLOAD_LOG" ] && grep -q -i "error\|failed" "$GO_DOWNLOAD_LOG" 2>/dev/null; then
GO_DOWNLOAD_EXIT_CODE=1 GO_DOWNLOAD_EXIT_CODE=1
fi fi
@@ -282,33 +282,33 @@ build_go_project() {
rm -f "$GO_DOWNLOAD_LOG" 2>/dev/null || true rm -f "$GO_DOWNLOAD_LOG" 2>/dev/null || true
if [ $GO_DOWNLOAD_EXIT_CODE -ne 0 ]; then if [ $GO_DOWNLOAD_EXIT_CODE -ne 0 ]; then
error "Go 依赖下载失败" error "Go dependency download failed"
exit 1 exit 1
fi fi
success "Go 依赖下载完成" success "Go dependencies downloaded"
info "构建项目..." info "Building project..."
GO_BUILD_LOG=$(mktemp) GO_BUILD_LOG=$(mktemp)
( (
set +e # 在子shell中禁用错误退出 set +e # disable errexit in subshell
export GOPROXY="$GOPROXY" export GOPROXY="$GOPROXY"
go build -o "$BINARY_NAME" cmd/server/main.go >"$GO_BUILD_LOG" 2>&1 go build -o "$BINARY_NAME" cmd/server/main.go >"$GO_BUILD_LOG" 2>&1
echo $? > "${GO_BUILD_LOG}.exit" echo $? > "${GO_BUILD_LOG}.exit"
) & ) &
GO_BUILD_PID=$! GO_BUILD_PID=$!
# 等待一小段时间,确保进程启动 # Brief pause so the process can start
sleep 0.1 sleep 0.1
# 显示进度(如果进程还在运行) # Show progress while still running
if kill -0 "$GO_BUILD_PID" 2>/dev/null; then if kill -0 "$GO_BUILD_PID" 2>/dev/null; then
show_progress "$GO_BUILD_PID" "正在构建项目" show_progress "$GO_BUILD_PID" "Building project"
else else
# 进程已经结束,等待一下确保退出码文件已写入 # Process already finished; wait for exit code file
sleep 0.2 sleep 0.2
fi fi
# 等待进程完成,忽略 wait 的退出码 # Wait for completion; ignore wait exit code
wait "$GO_BUILD_PID" 2>/dev/null || true wait "$GO_BUILD_PID" 2>/dev/null || true
GO_BUILD_EXIT_CODE=0 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") GO_BUILD_EXIT_CODE=$(cat "${GO_BUILD_LOG}.exit" 2>/dev/null || echo "1")
rm -f "${GO_BUILD_LOG}.exit" 2>/dev/null || true rm -f "${GO_BUILD_LOG}.exit" 2>/dev/null || true
else 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 if [ -f "$GO_BUILD_LOG" ] && grep -q -i "error\|failed" "$GO_BUILD_LOG" 2>/dev/null; then
GO_BUILD_EXIT_CODE=1 GO_BUILD_EXIT_CODE=1
fi fi
fi fi
if [ $GO_BUILD_EXIT_CODE -eq 0 ]; then if [ $GO_BUILD_EXIT_CODE -eq 0 ]; then
success "项目构建完成: $BINARY_NAME" success "Build complete: $BINARY_NAME"
rm -f "$GO_BUILD_LOG" rm -f "$GO_BUILD_LOG"
else else
error "项目构建失败" error "Build failed"
# 显示构建错误 # Show build errors
echo "" echo ""
info "构建错误详情:" info "Build error details:"
cat "$GO_BUILD_LOG" | sed 's/^/ /' cat "$GO_BUILD_LOG" | sed 's/^/ /'
echo "" echo ""
rm -f "$GO_BUILD_LOG" rm -f "$GO_BUILD_LOG"
@@ -337,24 +337,24 @@ build_go_project() {
fi fi
} }
# 检查是否需要重新构建 # Check whether a rebuild is needed
need_rebuild() { need_rebuild() {
if [ ! -f "$BINARY_NAME" ]; then if [ ! -f "$BINARY_NAME" ]; then
return 0 # 需要构建 return 0 # needs build
fi fi
# 检查源代码是否有更新 # Check if source changed since last build
if [ "$BINARY_NAME" -ot cmd/server/main.go ] || \ if [ "$BINARY_NAME" -ot cmd/server/main.go ] || \
[ "$BINARY_NAME" -ot go.mod ] || \ [ "$BINARY_NAME" -ot go.mod ] || \
find internal cmd -name "*.go" -newer "$BINARY_NAME" 2>/dev/null | grep -q .; then find internal cmd -name "*.go" -newer "$BINARY_NAME" 2>/dev/null | grep -q .; then
return 0 # 需要重新构建 return 0 # needs rebuild
fi fi
return 1 # 不需要构建 return 1 # no rebuild needed
} }
# 主流程 # Main flow
# 默认启动主站 HTTPS--https 传给二进制);传 --http 则走明文 HTTP # Default: HTTPS (--https passed to binary); --http uses plain HTTP.
main() { main() {
USE_HTTPS=1 USE_HTTPS=1
FORWARD_ARGS=() FORWARD_ARGS=()
@@ -366,39 +366,39 @@ main() {
FORWARD_ARGS+=("$arg") FORWARD_ARGS+=("$arg")
done done
# 环境检查 # Environment checks
info "检查运行环境..." info "Checking runtime environment..."
check_python check_python
check_go check_go
echo "" echo ""
# 设置 Python 环境 # Python setup
info "设置 Python 环境..." info "Setting up Python environment..."
setup_python_env setup_python_env
echo "" echo ""
# 构建 Go 项目 # Go build
if need_rebuild; then if need_rebuild; then
info "准备构建项目..." info "Preparing to build project..."
build_go_project build_go_project
else else
success "可执行文件已是最新,跳过构建" success "Binary is up to date; skipping build"
fi fi
echo "" echo ""
# 启动服务器 # Start server
success "所有准备工作完成!" success "All setup complete!"
echo "" echo ""
if [ "$USE_HTTPS" -eq 1 ]; then if [ "$USE_HTTPS" -eq 1 ]; then
info "启动 CyberStrikeAI 服务器(HTTPS + HTTP/2,自签证书)..." info "Starting CyberStrikeAI server (HTTPS + HTTP/2, self-signed cert)..."
note "纯 HTTP 启动请使用: $0 --http" note "For plain HTTP, use: $0 --http"
else else
info "启动 CyberStrikeAI 服务器(HTTP..." info "Starting CyberStrikeAI server (HTTP)..."
fi fi
echo "==========================================" echo "=========================================="
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 [ "$USE_HTTPS" -eq 1 ]; then
if [ "${#FORWARD_ARGS[@]}" -gt 0 ]; then if [ "${#FORWARD_ARGS[@]}" -gt 0 ]; then
exec "./$BINARY_NAME" -config "$CONFIG_FILE" --https "${FORWARD_ARGS[@]}" exec "./$BINARY_NAME" -config "$CONFIG_FILE" --https "${FORWARD_ARGS[@]}"
@@ -414,5 +414,5 @@ main() {
fi fi
} }
# 执行主流程(支持参数,如: ./run.sh --http # Run main (supports args, e.g. ./run.sh --http)
main "$@" main "$@"
+3 -5
View File
@@ -1371,7 +1371,6 @@
Modal Modal
============================================================================ */ ============================================================================ */
/* Toast 须高于模态遮罩 (10050),避免被 backdrop-filter 模糊 */
#c2-toast-container { #c2-toast-container {
z-index: 10100 !important; z-index: 10100 !important;
} }
@@ -1379,9 +1378,7 @@
.c2-modal-overlay { .c2-modal-overlay {
position: fixed; position: fixed;
top: 0; left: 0; right: 0; bottom: 0; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(15, 23, 42, 0.5); background: rgba(15, 23, 42, 0.52);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1404,7 +1401,8 @@
overflow-y: auto; overflow-y: auto;
box-shadow: var(--c2-shadow-lg); box-shadow: var(--c2-shadow-lg);
border: 1px solid var(--c2-border); 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 { @keyframes c2-slide-up {
+202 -24
View File
@@ -3326,9 +3326,9 @@ header {
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(15, 23, 42, 0.52);
overflow: auto; overflow: auto;
animation: fadeIn 0.2s ease-in; animation: fadeIn 0.15s ease-out;
} }
.modal-content { .modal-content {
@@ -3343,8 +3343,9 @@ header {
flex-direction: column; flex-direction: column;
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
animation: slideDown 0.3s ease-out; animation: slideDown 0.18s ease-out;
overflow: hidden; overflow: hidden;
contain: layout style paint;
} }
@keyframes slideDown { @keyframes slideDown {
@@ -7196,17 +7197,68 @@ header {
stroke-width: 2; stroke-width: 2;
} }
.mcp-stats-timeline-empty,
.mcp-stats-timeline-error { .mcp-stats-timeline-error {
margin: 0; margin: 0;
padding: 20px 8px; padding: 16px 8px;
text-align: center; text-align: center;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-muted); color: #b91c1c;
} }
.mcp-stats-timeline-error { .mcp-stats-timeline-empty-state {
color: #b91c1c; 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 { .mcp-stats-timeline-tooltip {
@@ -19323,6 +19375,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
cursor: pointer; cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
position: relative; position: relative;
min-width: 0;
overflow: hidden;
} }
.role-selection-item-main:hover { .role-selection-item-main:hover {
@@ -19385,6 +19439,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
margin: 0; margin: 0;
transition: color 0.2s cubic-bezier(0.16, 1, 0.3, 1); transition: color 0.2s cubic-bezier(0.16, 1, 0.3, 1);
letter-spacing: -0.01em; 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 { .role-selection-item-main.selected .role-selection-item-name-main {
@@ -19392,6 +19450,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
font-weight: 600; font-weight: 600;
} }
.role-selection-item-main.selected .role-selection-item-content-main {
padding-right: 24px;
}
.role-selection-item-description-main { .role-selection-item-description-main {
font-size: 0.75rem; font-size: 0.75rem;
color: #666666; color: #666666;
@@ -22463,7 +22525,10 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
} }
.projects-list-item { .projects-list-item {
position: relative; position: relative;
padding: 10px 12px 10px 14px; display: flex;
align-items: center;
gap: 4px;
padding: 10px 8px 10px 14px;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-size: 0.875rem; font-size: 0.875rem;
@@ -22496,8 +22561,43 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
color: #94a3b8; color: #94a3b8;
} }
.projects-list-item-body { .projects-list-item-body {
flex: 1;
min-width: 0; 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 { .projects-list-item-name {
font-weight: 600; font-weight: 600;
color: var(--text-primary, #0f172a); color: var(--text-primary, #0f172a);
@@ -22592,21 +22692,38 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-detail-header-main { .projects-detail-header-main {
min-width: 0; min-width: 0;
flex: 1; flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
} }
.projects-detail-title-row { .projects-detail-headline {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px 12px;
min-width: 0;
}
.projects-detail-title-group {
display: flex;
align-items: center;
gap: 10px; gap: 10px;
min-width: 0;
max-width: min(560px, 100%);
} }
.projects-detail-title { .projects-detail-title {
margin: 0; margin: 0;
min-width: 0;
font-size: 1.375rem; font-size: 1.375rem;
font-weight: 600; font-weight: 600;
color: #0f172a; color: #0f172a;
letter-spacing: -0.02em; letter-spacing: -0.02em;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.projects-status-pill { .projects-status-pill {
flex-shrink: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
font-size: 0.6875rem; font-size: 0.6875rem;
@@ -22614,41 +22731,75 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding: 3px 10px; padding: 3px 10px;
border-radius: 999px; border-radius: 999px;
line-height: 1.2; line-height: 1.2;
border: 1px solid transparent;
} }
.projects-status-pill--active { .projects-status-pill--active {
background: #dcfce7; background: #dcfce7;
color: #166534; color: #166534;
border-color: #86efac;
} }
.projects-status-pill--archived { .projects-status-pill--archived {
background: #f1f5f9; background: #f1f5f9;
color: #64748b; color: #64748b;
border-color: #e2e8f0;
} }
.projects-detail-meta { .projects-detail-meta {
margin: 6px 0 0; margin: 0;
font-size: 0.8125rem; font-size: 0.8125rem;
color: #94a3b8; color: #94a3b8;
line-height: 1.4;
} }
.projects-detail-desc { .projects-detail-desc {
margin: 10px 0 0; margin: 0;
max-width: min(640px, 100%);
font-size: 0.875rem; font-size: 0.875rem;
color: #475569; color: #475569;
line-height: 1.55; 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;
overflow: hidden;
}
.projects-description-textarea {
max-height: 200px;
resize: vertical;
overflow-y: auto;
} }
.projects-detail-stats { .projects-detail-stats {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; align-items: center;
margin-top: 14px; gap: 6px;
flex-shrink: 0;
padding-left: 12px;
border-left: 1px solid #e2e8f0;
} }
.projects-stat-chip { .projects-stat-chip {
font-size: 0.75rem; font-size: 0.6875rem;
font-weight: 500; font-weight: 500;
color: #475569; color: #475569;
background: #f1f5f9; background: #f1f5f9;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
padding: 4px 10px; padding: 3px 8px;
border-radius: 999px; 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 { .projects-stat-chip--warn {
color: #92400e; color: #92400e;
@@ -22660,7 +22811,23 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; 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 { .projects-tabs {
display: flex; display: flex;
@@ -23621,10 +23788,8 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
justify-content: center; justify-content: center;
padding: 24px 16px; padding: 24px 16px;
box-sizing: border-box; box-sizing: border-box;
background: rgba(15, 23, 42, 0.45); background: rgba(15, 23, 42, 0.52);
backdrop-filter: blur(4px); animation: projectsOverlayIn 0.15s ease-out;
-webkit-backdrop-filter: blur(4px);
animation: projectsOverlayIn 0.2s ease-out;
} }
@keyframes projectsOverlayIn { @keyframes projectsOverlayIn {
from { opacity: 0; } from { opacity: 0; }
@@ -23642,7 +23807,8 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
0 24px 48px rgba(15, 23, 42, 0.18), 0 24px 48px rgba(15, 23, 42, 0.18),
0 0 0 1px rgba(15, 23, 42, 0.06); 0 0 0 1px rgba(15, 23, 42, 0.06);
overflow: hidden; 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 { .projects-modal-dialog--wide {
max-width: 640px; max-width: 640px;
@@ -23703,6 +23869,13 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding: 10px 12px; padding: 10px 12px;
font-size: 0.875rem; font-size: 0.875rem;
transition: border-color 0.15s, box-shadow 0.15s; 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 { .projects-modal-body .form-input:focus {
outline: none; outline: none;
@@ -23726,7 +23899,8 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-modal-footer .btn-primary { .projects-modal-footer .btn-primary {
min-width: 100px; min-width: 100px;
} }
body.projects-modal-open { body.projects-modal-open,
body.app-modal-open {
overflow: hidden; overflow: hidden;
} }
.fact-detail-prev-wrap { .fact-detail-prev-wrap {
@@ -23874,8 +24048,11 @@ body.projects-modal-open {
/* 对话区项目选择器(与角色/代理模式共用 role-selector-* */ /* 对话区项目选择器(与角色/代理模式共用 role-selector-* */
.project-selector-wrapper .role-selector-text { .project-selector-wrapper .role-selector-text {
max-width: 108px; max-width: 108px;
min-width: 0;
flex-shrink: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }
.chat-project-panel { .chat-project-panel {
width: 280px; width: 280px;
@@ -23895,6 +24072,7 @@ body.projects-modal-open {
padding-right: 0; padding-right: 0;
margin: 0; margin: 0;
width: 100%; width: 100%;
overflow-x: hidden;
} }
.chat-project-panel .role-selection-item-main { .chat-project-panel .role-selection-item-main {
width: 100%; width: 100%;
+10
View File
@@ -286,6 +286,8 @@
"status": "Status", "status": "Status",
"modalNewTitle": "New project", "modalNewTitle": "New project",
"modalNewSubtitle": "After creation, bind conversations to share fact board across chats", "modalNewSubtitle": "After creation, bind conversations to share fact board across chats",
"modalEditTitle": "Edit project",
"modalEditSubtitle": "Update project name and description",
"projectName": "Project name", "projectName": "Project name",
"projectNamePlaceholder": "e.g. Client A Web pentest", "projectNamePlaceholder": "e.g. Client A Web pentest",
"projectDescription": "Project description", "projectDescription": "Project description",
@@ -324,6 +326,9 @@
"statsSparse": "{{count}} incomplete", "statsSparse": "{{count}} incomplete",
"projectNotFound": "Project not found", "projectNotFound": "Project not found",
"updatedPrefix": "Updated {{time}}", "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", "noMatchingFacts": "No matching facts, try adjusting filters",
"noFacts": "No facts yet. Click Add fact or let Agent write facts automatically", "noFacts": "No facts yet. Click Add fact or let Agent write facts automatically",
"relatedVulnIdTitle": "Related vulnerability ID", "relatedVulnIdTitle": "Related vulnerability ID",
@@ -407,6 +412,10 @@
"dangerZoneTitle": "Danger zone", "dangerZoneTitle": "Danger zone",
"dangerZoneHint": "Archived projects are hidden unless 'Show archived' is enabled; deletion removes all facts permanently.", "dangerZoneHint": "Archived projects are hidden unless 'Show archived' is enabled; deletion removes all facts permanently.",
"archiveRestore": "Archive / Restore", "archiveRestore": "Archive / Restore",
"archiveProject": "Archive",
"editProject": "Edit",
"restoreProjectActive": "Restore to active",
"projectActions": "Project actions",
"deleteProject": "Delete project", "deleteProject": "Delete project",
"saveChangesHint": "Click save to sync changes to server", "saveChangesHint": "Click save to sync changes to server",
"saveSettings": "Save changes", "saveSettings": "Save changes",
@@ -1583,6 +1592,7 @@
"timelineSummary": "{{total}} calls in range · peak {{peak}}", "timelineSummary": "{{total}} calls in range · peak {{peak}}",
"timelineSparseHint": "Most buckets are empty; peak {{peak}} calls at {{peakTime}}", "timelineSparseHint": "Most buckets are empty; peak {{peak}} calls at {{peakTime}}",
"timelineNoData": "No calls in this period", "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", "timelineLoadError": "Failed to load call trend",
"timelineTotalLegend": "Total calls", "timelineTotalLegend": "Total calls",
"timelineFailedLegend": "Failed", "timelineFailedLegend": "Failed",
+10
View File
@@ -274,6 +274,8 @@
"status": "状态", "status": "状态",
"modalNewTitle": "新建项目", "modalNewTitle": "新建项目",
"modalNewSubtitle": "创建后可绑定对话,跨会话共享事实黑板", "modalNewSubtitle": "创建后可绑定对话,跨会话共享事实黑板",
"modalEditTitle": "编辑项目",
"modalEditSubtitle": "修改项目名称与描述",
"projectName": "项目名称", "projectName": "项目名称",
"projectNamePlaceholder": "例如:某客户 Web 渗透", "projectNamePlaceholder": "例如:某客户 Web 渗透",
"projectDescription": "项目描述", "projectDescription": "项目描述",
@@ -312,6 +314,9 @@
"statsSparse": "{{count}} 待补全", "statsSparse": "{{count}} 待补全",
"projectNotFound": "项目不存在", "projectNotFound": "项目不存在",
"updatedPrefix": "更新于 {{time}}", "updatedPrefix": "更新于 {{time}}",
"descExpand": "展开全部",
"descCollapse": "收起",
"descriptionLengthHint": "简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body",
"noMatchingFacts": "无匹配事实,请调整筛选条件", "noMatchingFacts": "无匹配事实,请调整筛选条件",
"noFacts": "暂无事实,点击「添加事实」或由 Agent 自动写入", "noFacts": "暂无事实,点击「添加事实」或由 Agent 自动写入",
"relatedVulnIdTitle": "关联漏洞 ID", "relatedVulnIdTitle": "关联漏洞 ID",
@@ -395,6 +400,10 @@
"dangerZoneTitle": "危险操作", "dangerZoneTitle": "危险操作",
"dangerZoneHint": "归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。", "dangerZoneHint": "归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。",
"archiveRestore": "归档 / 恢复", "archiveRestore": "归档 / 恢复",
"archiveProject": "归档",
"editProject": "编辑",
"restoreProjectActive": "恢复为进行中",
"projectActions": "项目操作",
"deleteProject": "删除项目", "deleteProject": "删除项目",
"saveChangesHint": "修改后请点击保存以同步到服务器", "saveChangesHint": "修改后请点击保存以同步到服务器",
"saveSettings": "保存更改", "saveSettings": "保存更改",
@@ -1571,6 +1580,7 @@
"timelineSummary": "区间内 {{total}} 次 · 峰值 {{peak}}", "timelineSummary": "区间内 {{total}} 次 · 峰值 {{peak}}",
"timelineSparseHint": "该时段多数时间为 0,峰值 {{peak}} 次出现在 {{peakTime}}", "timelineSparseHint": "该时段多数时间为 0,峰值 {{peak}} 次出现在 {{peakTime}}",
"timelineNoData": "该时段暂无调用", "timelineNoData": "该时段暂无调用",
"timelineEmptyHint": "切换时间范围查看其他时段,或在对话/任务中调用 MCP 工具",
"timelineLoadError": "无法加载调用趋势", "timelineLoadError": "无法加载调用趋势",
"timelineTotalLegend": "总调用", "timelineTotalLegend": "总调用",
"timelineFailedLegend": "失败", "timelineFailedLegend": "失败",
+20 -17
View File
@@ -105,45 +105,48 @@ function showAddMarkdownAgentModal() {
document.getElementById('agent-md-bind-role').value = ''; document.getElementById('agent-md-bind-role').value = '';
document.getElementById('agent-md-max-iter').value = '0'; document.getElementById('agent-md-max-iter').value = '0';
document.getElementById('agent-md-instruction').value = ''; document.getElementById('agent-md-instruction').value = '';
if (modal) modal.style.display = 'flex'; openAppModal('agent-md-modal');
} }
async function editMarkdownAgent(filename) { async function editMarkdownAgent(filename) {
if (!filename) return; if (!filename) return;
const modal = document.getElementById('agent-md-modal');
const title = document.getElementById('agent-md-modal-title'); const title = document.getElementById('agent-md-modal-title');
const row = document.getElementById('agent-md-filename-row'); const row = document.getElementById('agent-md-filename-row');
markdownAgentsEditingFilename = null; markdownAgentsEditingFilename = null;
markdownAgentsEditingIsOrchestrator = false; markdownAgentsEditingIsOrchestrator = false;
if (title) title.textContent = _agentsT('agentsPage.editTitle'); if (title) title.textContent = _agentsT('agentsPage.editTitle');
if (row) row.style.display = 'none'; if (row) row.style.display = 'none';
document.getElementById('agent-md-instruction').value = '';
openAppModal('agent-md-modal', { focus: false });
try { try {
const r = await apiFetch('/api/multi-agent/markdown-agents/' + encodeURIComponent(filename)); const r = await apiFetch('/api/multi-agent/markdown-agents/' + encodeURIComponent(filename));
const data = await r.json(); const data = await r.json();
if (!r.ok) throw new Error(data.error || r.statusText); if (!r.ok) throw new Error(data.error || r.statusText);
markdownAgentsEditingFilename = data.filename || filename; markdownAgentsEditingFilename = data.filename || filename;
markdownAgentsEditingIsOrchestrator = !!data.is_orchestrator; markdownAgentsEditingIsOrchestrator = !!data.is_orchestrator;
document.getElementById('agent-md-filename-current').value = data.filename || filename; deferModalContent(function () {
document.getElementById('agent-md-filename').value = data.filename || filename; document.getElementById('agent-md-filename-current').value = data.filename || filename;
document.getElementById('agent-md-filename').disabled = true; document.getElementById('agent-md-filename').value = data.filename || filename;
var roleEl2 = document.getElementById('agent-md-role'); document.getElementById('agent-md-filename').disabled = true;
if (roleEl2) roleEl2.value = data.is_orchestrator ? 'orchestrator' : 'sub'; var roleEl2 = document.getElementById('agent-md-role');
document.getElementById('agent-md-id').value = data.id || ''; if (roleEl2) roleEl2.value = data.is_orchestrator ? 'orchestrator' : 'sub';
document.getElementById('agent-md-name').value = data.name || ''; document.getElementById('agent-md-id').value = data.id || '';
document.getElementById('agent-md-description').value = data.description || ''; document.getElementById('agent-md-name').value = data.name || '';
document.getElementById('agent-md-tools').value = Array.isArray(data.tools) ? data.tools.join(', ') : ''; document.getElementById('agent-md-description').value = data.description || '';
document.getElementById('agent-md-bind-role').value = data.bind_role || ''; document.getElementById('agent-md-tools').value = Array.isArray(data.tools) ? data.tools.join(', ') : '';
document.getElementById('agent-md-max-iter').value = String(data.max_iterations != null ? data.max_iterations : 0); document.getElementById('agent-md-bind-role').value = data.bind_role || '';
document.getElementById('agent-md-instruction').value = data.instruction || ''; document.getElementById('agent-md-max-iter').value = String(data.max_iterations != null ? data.max_iterations : 0);
if (modal) modal.style.display = 'flex'; document.getElementById('agent-md-instruction').value = data.instruction || '';
document.getElementById('agent-md-name')?.focus();
});
} catch (e) { } catch (e) {
closeMarkdownAgentModal();
showNotification(_agentsT('agentsPage.loadOneFailed') + ': ' + e.message, 'error'); showNotification(_agentsT('agentsPage.loadOneFailed') + ': ' + e.message, 'error');
} }
} }
function closeMarkdownAgentModal() { function closeMarkdownAgentModal() {
const modal = document.getElementById('agent-md-modal'); closeAppModal('agent-md-modal');
if (modal) modal.style.display = 'none';
markdownAgentsEditingFilename = null; markdownAgentsEditingFilename = null;
markdownAgentsEditingIsOrchestrator = false; markdownAgentsEditingIsOrchestrator = false;
} }
+38 -33
View File
@@ -533,56 +533,61 @@ async function exportAuditLogsCsv() {
} }
function closeAuditDetailModal() { function closeAuditDetailModal() {
closeAppModal('audit-detail-modal');
const el = document.getElementById('audit-detail-modal'); const el = document.getElementById('audit-detail-modal');
if (el) el.remove(); if (el) el.remove();
syncAppModalBodyLock();
} }
async function showAuditLogDetail(id) { async function showAuditLogDetail(id) {
if (!id || typeof apiFetch !== 'function') return; if (!id || typeof apiFetch !== 'function') return;
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); }; const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
try { 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)); const r = await apiFetch('/api/audit/logs/' + encodeURIComponent(id));
if (!r.ok) throw new Error('not found'); if (!r.ok) throw new Error('not found');
const data = await r.json(); const data = await r.json();
const log = data.log || {}; const log = data.log || {};
const detail = log.detail ? JSON.stringify(log.detail, null, 2) : ''; 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 || '')); const catAction = esc(auditCategoryLabel(log.category || '')) + ' / ' + esc(auditActionLabel(log.action || ''));
overlay.innerHTML = deferModalContent(function () {
'<div class="modal-content" style="max-width: 720px;">' + overlay.innerHTML =
'<div class="modal-header">' + '<div class="modal-content" style="max-width: 720px;">' +
'<h2>' + esc(auditT('settingsAudit.detailTitle', null, '审计详情')) + '</h2>' + '<div class="modal-header">' +
'<span class="modal-close" onclick="closeAuditDetailModal()">&times;</span>' + '<h2>' + esc(auditT('settingsAudit.detailTitle', null, '审计详情')) + '</h2>' +
'</div>' + '<span class="modal-close" onclick="closeAuditDetailModal()">&times;</span>' +
'<div class="modal-body audit-detail-body">' + '</div>' +
'<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' + '<div class="modal-body audit-detail-body">' +
'<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' + '<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(log.result || '') + '</p>' + '<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(log.message || '') + '</p>' + '<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(log.result || '') + '</p>' +
(log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') + '<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(log.message || '') + '</p>' +
(log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') + (log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') +
(log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') + (log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') +
auditResourceMeta(log) + (log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') +
(detail ? '<pre class="audit-detail-pre">' + esc(detail) + '</pre>' : '') + auditResourceMeta(log) +
'</div>' + (detail ? '<pre class="audit-detail-pre">' + esc(detail) + '</pre>' : '') +
'<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAuditDetailModal()">' + '</div>' +
esc(auditT('common.close', null, '关闭')) + '</button></div>' + '<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAuditDetailModal()">' +
'</div>'; esc(auditT('common.close', null, '关闭')) + '</button></div>' +
document.body.appendChild(overlay); '</div>';
const chatBtn = overlay.querySelector('.audit-open-chat-btn'); const chatBtn = overlay.querySelector('.audit-open-chat-btn');
if (chatBtn) { if (chatBtn) {
chatBtn.addEventListener('click', function () { chatBtn.addEventListener('click', function () {
auditOpenConversationChat(chatBtn.getAttribute('data-conversation-id')); 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) { } catch (e) {
closeAuditDetailModal();
if (typeof showToast === 'function') { if (typeof showToast === 'function') {
showToast(e.message || String(e), 'error'); showToast(e.message || String(e), 'error');
} }
+3 -5
View File
@@ -72,7 +72,7 @@ function showLoginOverlay(message = '') {
if (!overlay) { if (!overlay) {
return; return;
} }
overlay.style.display = 'flex'; openAppModal('login-overlay', { focus: false });
if (errorBox) { if (errorBox) {
if (message) { if (message) {
errorBox.textContent = message; errorBox.textContent = message;
@@ -82,7 +82,7 @@ function showLoginOverlay(message = '') {
errorBox.style.display = 'none'; errorBox.style.display = 'none';
} }
} }
setTimeout(() => { setTimeout(function () {
if (passwordInput) { if (passwordInput) {
passwordInput.focus(); passwordInput.focus();
} }
@@ -93,9 +93,7 @@ function hideLoginOverlay() {
const overlay = document.getElementById('login-overlay'); const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error'); const errorBox = document.getElementById('login-error');
const passwordInput = document.getElementById('login-password'); const passwordInput = document.getElementById('login-password');
if (overlay) { closeAppModal('login-overlay');
overlay.style.display = 'none';
}
if (errorBox) { if (errorBox) {
errorBox.textContent = ''; errorBox.textContent = '';
errorBox.style.display = 'none'; errorBox.style.display = 'none';
+5 -5
View File
@@ -478,7 +478,7 @@
const content = document.getElementById('c2-modal-content'); const content = document.getElementById('c2-modal-content');
if (!content || !modal) return; if (!content || !modal) return;
modal.style.display = 'flex'; openAppModal(modal);
content.innerHTML = ` content.innerHTML = `
<div class="c2-modal-header"> <div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}</h3> <h3>${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}</h3>
@@ -635,7 +635,7 @@
const content = document.getElementById('c2-modal-content'); const content = document.getElementById('c2-modal-content');
if (!content || !modal) return; if (!content || !modal) return;
modal.style.display = 'flex'; openAppModal(modal);
content.innerHTML = ` content.innerHTML = `
<div class="c2-modal-header"> <div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.editTitle'))}</h3> <h3>${escapeHtml(c2t('c2.listeners.editTitle'))}</h3>
@@ -2376,7 +2376,7 @@
<button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.close'))}</button> <button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.close'))}</button>
</div> </div>
`; `;
modal.style.display = 'flex'; openAppModal(modal);
}; };
const local = C2.tasks.find(x => x.id === id); 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> <button class="btn-primary" onclick="C2.createProfile()">${escapeHtml(c2t('c2.profiles.submitCreate'))}</button>
</div> </div>
`; `;
modal.style.display = 'flex'; openAppModal(modal);
}; };
C2.createProfile = function() { C2.createProfile = function() {
@@ -2981,10 +2981,10 @@
C2.closeModal = function() { C2.closeModal = function() {
const modal = document.getElementById('c2-modal'); const modal = document.getElementById('c2-modal');
if (modal) { if (modal) {
modal.style.display = 'none';
const modalBox = modal.querySelector('.c2-modal'); const modalBox = modal.querySelector('.c2-modal');
if (modalBox) modalBox.classList.remove('c2-modal--wide'); 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'); const modal = document.getElementById('chat-files-edit-modal');
if (pathEl) pathEl.textContent = relativePath; if (pathEl) pathEl.textContent = relativePath;
if (ta) ta.value = ''; if (ta) ta.value = '';
if (modal) modal.style.display = 'block'; openAppModal('chat-files-edit-modal', { focus: false });
try { try {
const res = await apiFetch('/api/chat-uploads/content?path=' + encodeURIComponent(relativePath)); 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); throw new Error(errText || res.status);
} }
const data = await res.json(); 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) { } catch (e) {
if (modal) modal.style.display = 'none'; closeAppModal('chat-files-edit-modal');
alert(chatFilesAlertMessage(e && e.message)); alert(chatFilesAlertMessage(e && e.message));
} }
} }
function closeChatFilesEditModal() { function closeChatFilesEditModal() {
const modal = document.getElementById('chat-files-edit-modal'); closeAppModal('chat-files-edit-modal');
if (modal) modal.style.display = 'none';
chatFilesEditRelativePath = ''; chatFilesEditRelativePath = '';
} }
@@ -1060,7 +1063,7 @@ function openChatFilesRename(relativePath, currentName) {
input.value = currentName || ''; input.value = currentName || '';
input.select(); input.select();
} }
if (modal) modal.style.display = 'flex'; if (modal) openAppModal(modal);
if (modal && typeof window.applyTranslations === 'function') { if (modal && typeof window.applyTranslations === 'function') {
window.applyTranslations(modal); window.applyTranslations(modal);
} }
@@ -1068,8 +1071,7 @@ function openChatFilesRename(relativePath, currentName) {
} }
function closeChatFilesRenameModal() { function closeChatFilesRenameModal() {
const modal = document.getElementById('chat-files-rename-modal'); closeAppModal('chat-files-rename-modal');
if (modal) modal.style.display = 'none';
const hint = document.getElementById('chat-files-rename-path-hint'); const hint = document.getElementById('chat-files-rename-path-hint');
if (hint) hint.textContent = ''; if (hint) hint.textContent = '';
chatFilesRenameRelativePath = ''; chatFilesRenameRelativePath = '';
@@ -1106,7 +1108,7 @@ function openChatFilesMkdirModal() {
const p = chatFilesBrowsePath.join('/'); const p = chatFilesBrowsePath.join('/');
if (hint) hint.textContent = p ? ('chat_uploads/' + p) : 'chat_uploads'; if (hint) hint.textContent = p ? ('chat_uploads/' + p) : 'chat_uploads';
if (input) input.value = ''; if (input) input.value = '';
if (modal) modal.style.display = 'flex'; if (modal) openAppModal(modal);
if (modal && typeof window.applyTranslations === 'function') { if (modal && typeof window.applyTranslations === 'function') {
window.applyTranslations(modal); window.applyTranslations(modal);
} }
@@ -1116,8 +1118,7 @@ function openChatFilesMkdirModal() {
} }
function closeChatFilesMkdirModal() { function closeChatFilesMkdirModal() {
const modal = document.getElementById('chat-files-mkdir-modal'); closeAppModal('chat-files-mkdir-modal');
if (modal) modal.style.display = 'none';
const input = document.getElementById('chat-files-mkdir-input'); const input = document.getElementById('chat-files-mkdir-input');
if (input) input.value = ''; if (input) input.value = '';
} }
+31 -51
View File
@@ -2535,10 +2535,17 @@ async function batchUpdateButtonToolNames(buttonsContainer, executionIds) {
// 显示MCP调用详情 // 显示MCP调用详情
async function showMCPDetail(executionId) { async function showMCPDetail(executionId) {
try { try {
openAppModal('mcp-detail-modal', { focus: false });
const response = await apiFetch(`/api/monitor/execution/${executionId}`); const response = await apiFetch(`/api/monitor/execution/${executionId}`);
const exec = await response.json(); 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-tool-name').textContent = exec.toolName || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : 'Unknown');
document.getElementById('detail-execution-id').textContent = exec.id || 'N/A'; document.getElementById('detail-execution-id').textContent = exec.id || 'N/A';
@@ -2645,20 +2652,16 @@ async function showMCPDetail(executionId) {
delete abortBtn.dataset.execId; 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) { } catch (error) {
closeMCPDetail();
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + error.message); alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + error.message);
} }
} }
// 关闭MCP详情模态框 // 关闭MCP详情模态框
function closeMCPDetail() { function closeMCPDetail() {
document.getElementById('mcp-detail-modal').style.display = 'none'; closeAppModal('mcp-detail-modal');
} }
/** 从详情模态框触发:取消当前进行中的 MCP 工具调用 */ /** 从详情模态框触发:取消当前进行中的 MCP 工具调用 */
@@ -2682,18 +2685,12 @@ function openMcpToolAbortModal(executionId, options = {}) {
if (ta) { if (ta) {
ta.value = ''; ta.value = '';
} }
const m = document.getElementById('mcp-tool-abort-modal'); openAppModal('mcp-tool-abort-modal');
if (m) {
m.style.display = 'block';
}
} }
function closeMcpToolAbortModal() { function closeMcpToolAbortModal() {
window.__mcpToolAbortContext = null; window.__mcpToolAbortContext = null;
const m = document.getElementById('mcp-tool-abort-modal'); closeAppModal('mcp-tool-abort-modal');
if (m) {
m.style.display = 'none';
}
} }
async function submitMcpToolAbortModal() { async function submitMcpToolAbortModal() {
@@ -2846,10 +2843,12 @@ async function startNewConversation() {
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
currentConversationGroupId = null; // 新对话不属于任何分组 currentConversationGroupId = null; // 新对话不属于任何分组
if (typeof ensureDefaultActiveProjectForNewChat === 'function') { if (typeof ensureDefaultActiveProjectForNewChat === 'function') {
ensureDefaultActiveProjectForNewChat().catch(() => {}); try {
await ensureDefaultActiveProjectForNewChat();
} catch (e) { /* ignore */ }
} }
if (typeof refreshChatProjectSelector === 'function') { if (typeof refreshChatProjectSelector === 'function') {
refreshChatProjectSelector(); await refreshChatProjectSelector();
} }
document.getElementById('chat-messages').innerHTML = ''; document.getElementById('chat-messages').innerHTML = '';
const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'; const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
@@ -3125,7 +3124,7 @@ async function loadConversation(conversationId) {
// 如果攻击链模态框打开且显示的不是当前对话,关闭它 // 如果攻击链模态框打开且显示的不是当前对话,关闭它
const attackChainModal = document.getElementById('attack-chain-modal'); const attackChainModal = document.getElementById('attack-chain-modal');
if (attackChainModal && attackChainModal.style.display === 'block') { if (attackChainModal && isAppModalOpen('attack-chain-modal')) {
if (currentAttackChainConversationId !== conversationId) { if (currentAttackChainConversationId !== conversationId) {
closeAttackChainModal(); closeAttackChainModal();
} }
@@ -3415,7 +3414,7 @@ async function deleteConversation(conversationId, skipConfirm = false) {
// 批量管理弹窗打开时,同步刷新弹窗内列表 // 批量管理弹窗打开时,同步刷新弹窗内列表
const batchModal = document.getElementById('batch-manage-modal'); 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); allConversationsForBatch = allConversationsForBatch.filter(c => c.id !== conversationId);
updateBatchManageTitle(allConversationsForBatch.length); updateBatchManageTitle(allConversationsForBatch.length);
const searchInput = document.getElementById('batch-search-input'); const searchInput = document.getElementById('batch-search-input');
@@ -3522,7 +3521,7 @@ async function showAttackChain(conversationId) {
if (isAttackChainLoading(conversationId) && currentAttackChainConversationId === conversationId) { if (isAttackChainLoading(conversationId) && currentAttackChainConversationId === conversationId) {
// 如果模态框已经打开且显示的是同一个对话,不重复打开 // 如果模态框已经打开且显示的是同一个对话,不重复打开
const modal = document.getElementById('attack-chain-modal'); const modal = document.getElementById('attack-chain-modal');
if (modal && modal.style.display === 'block') { if (modal && isAppModalOpen('attack-chain-modal')) {
console.log('攻击链正在加载中,模态框已打开'); console.log('攻击链正在加载中,模态框已打开');
return; return;
} }
@@ -3535,8 +3534,7 @@ async function showAttackChain(conversationId) {
return; return;
} }
modal.style.display = 'block'; openAppModal('attack-chain-modal', { focus: false });
// 打开时立即按当前语言刷新统计(避免红框内仍显示硬编码中文)
updateAttackChainStats({ nodes: [], edges: [] }); updateAttackChainStats({ nodes: [], edges: [] });
// 清空容器 // 清空容器
@@ -4668,10 +4666,7 @@ function closeNodeDetails() {
// 关闭攻击链模态框 // 关闭攻击链模态框
function closeAttackChainModal() { function closeAttackChainModal() {
const modal = document.getElementById('attack-chain-modal'); closeAppModal('attack-chain-modal');
if (modal) {
modal.style.display = 'none';
}
// 关闭节点详情 // 关闭节点详情
closeNodeDetails(); closeNodeDetails();
@@ -7214,19 +7209,14 @@ async function showBatchManageModal() {
updateBatchManageTitle(allConversationsForBatch.length); updateBatchManageTitle(allConversationsForBatch.length);
renderBatchConversations(); renderBatchConversations();
if (modal) { openAppModal('batch-manage-modal');
modal.style.display = 'flex';
}
} catch (error) { } catch (error) {
console.error('加载对话列表失败:', error); console.error('加载对话列表失败:', error);
// 错误时使用空数组,不显示错误提示(更友好的用户体验) // 错误时使用空数组,不显示错误提示(更友好的用户体验)
allConversationsForBatch = []; allConversationsForBatch = [];
const modal = document.getElementById('batch-manage-modal');
updateBatchManageTitle(0); updateBatchManageTitle(0);
if (modal) { renderBatchConversations();
renderBatchConversations(); openAppModal('batch-manage-modal');
modal.style.display = 'flex';
}
} }
} }
@@ -7381,10 +7371,7 @@ async function deleteSelectedConversations() {
// 关闭批量管理模态框 // 关闭批量管理模态框
function closeBatchManageModal() { function closeBatchManageModal() {
const modal = document.getElementById('batch-manage-modal'); closeAppModal('batch-manage-modal');
if (modal) {
modal.style.display = 'none';
}
const selectAll = document.getElementById('batch-select-all'); const selectAll = document.getElementById('batch-select-all');
if (selectAll) { if (selectAll) {
selectAll.checked = false; selectAll.checked = false;
@@ -7424,8 +7411,7 @@ function refreshChatPanelI18n() {
}); });
} }
const mcpModal = document.getElementById('mcp-detail-modal'); if (isAppModalOpen('mcp-detail-modal')) {
if (mcpModal && mcpModal.style.display === 'block') {
const detailTimeEl = document.getElementById('detail-time'); const detailTimeEl = document.getElementById('detail-time');
if (detailTimeEl && detailTimeEl.dataset.detailTimeIso) { if (detailTimeEl && detailTimeEl.dataset.detailTimeIso) {
try { try {
@@ -7447,7 +7433,7 @@ document.addEventListener('languagechange', function () {
refreshSystemReadyMessageBubbles(); refreshSystemReadyMessageBubbles();
refreshChatPanelI18n(); refreshChatPanelI18n();
const modal = document.getElementById('batch-manage-modal'); const modal = document.getElementById('batch-manage-modal');
if (modal && modal.style.display === 'flex') { if (isAppModalOpen('batch-manage-modal')) {
updateBatchManageTitle(allConversationsForBatch.length); updateBatchManageTitle(allConversationsForBatch.length);
} }
// 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式 // 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式
@@ -7482,20 +7468,14 @@ function showCreateGroupModal(andMoveConversation = false) {
iconPicker.style.display = 'none'; iconPicker.style.display = 'none';
} }
if (modal) { if (modal) {
modal.style.display = 'flex'; openAppModal('create-group-modal', { focusEl: input });
modal.dataset.moveConversation = andMoveConversation ? 'true' : 'false'; modal.dataset.moveConversation = andMoveConversation ? 'true' : 'false';
if (input) {
setTimeout(() => input.focus(), 100);
}
} }
} }
// 关闭创建分组模态框 // 关闭创建分组模态框
function closeCreateGroupModal() { function closeCreateGroupModal() {
const modal = document.getElementById('create-group-modal'); closeAppModal('create-group-modal');
if (modal) {
modal.style.display = 'none';
}
const input = document.getElementById('create-group-name-input'); const input = document.getElementById('create-group-name-input');
if (input) { if (input) {
input.value = ''; input.value = '';
+18 -10
View File
@@ -344,7 +344,9 @@ function showFofaParseModal(nlText, parsed) {
const modal = document.createElement('div'); const modal = document.createElement('div');
modal.id = 'fofa-parse-modal'; modal.id = 'fofa-parse-modal';
modal.className = 'modal'; modal.className = 'modal';
modal.style.display = 'block'; document.body.appendChild(modal);
openAppModal(modal, { focus: false });
deferModalContent(function () {
modal.innerHTML = ` modal.innerHTML = `
<div class="modal-content" style="max-width: 900px;"> <div class="modal-content" style="max-width: 900px;">
<div class="modal-header"> <div class="modal-header">
@@ -384,24 +386,24 @@ function showFofaParseModal(nlText, parsed) {
</div> </div>
`; `;
document.body.appendChild(modal);
const queryTextarea = document.getElementById('fofa-parse-query'); const queryTextarea = document.getElementById('fofa-parse-query');
if (queryTextarea) { if (queryTextarea) {
queryTextarea.value = (parsed?.query || '').trim(); queryTextarea.value = (parsed?.query || '').trim();
setTimeout(() => { queryTextarea.focus();
try { queryTextarea.focus(); } catch (e) { /* ignore */ }
}, 0);
} }
const close = () => modal.remove(); const close = function () {
modal.addEventListener('click', (e) => { closeAppModal(modal);
modal.remove();
syncAppModalBodyLock();
};
modal.addEventListener('click', function (e) {
if (e.target === modal) close(); if (e.target === modal) close();
}); });
document.getElementById('fofa-parse-modal-close')?.addEventListener('click', close); document.getElementById('fofa-parse-modal-close')?.addEventListener('click', close);
document.getElementById('fofa-parse-cancel')?.addEventListener('click', close); document.getElementById('fofa-parse-cancel')?.addEventListener('click', close);
const applyToQuery = (run) => { const applyToQuery = function (run) {
const els = getFofaFormElements(); const els = getFofaFormElements();
const q = (queryTextarea?.value || '').trim(); const q = (queryTextarea?.value || '').trim();
if (!q) { if (!q) {
@@ -435,6 +437,7 @@ function showFofaParseModal(nlText, parsed) {
} }
}; };
document.addEventListener('keydown', onKey); document.addEventListener('keydown', onKey);
});
} }
function setFofaMeta(text) { function setFofaMeta(text) {
@@ -1091,8 +1094,13 @@ function showCellDetailModal(field, fullText) {
`; `;
document.body.appendChild(modal); document.body.appendChild(modal);
openAppModal(modal);
const close = () => modal.remove(); const close = function () {
closeAppModal(modal);
modal.remove();
syncAppModalBodyLock();
};
modal.addEventListener('click', (e) => { modal.addEventListener('click', (e) => {
if (e.target === modal) close(); 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-category').value = '';
document.getElementById('knowledge-item-title').value = ''; document.getElementById('knowledge-item-title').value = '';
document.getElementById('knowledge-item-content').value = ''; document.getElementById('knowledge-item-content').value = '';
document.getElementById('knowledge-item-modal').style.display = 'block'; openAppModal('knowledge-item-modal');
} }
// 编辑知识项 // 编辑知识项
async function editKnowledgeItem(id) { async function editKnowledgeItem(id) {
try { 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}`); const response = await apiFetch(`/api/knowledge/items/${id}`);
if (!response.ok) { if (!response.ok) {
throw new Error('获取知识项失败'); throw new Error('获取知识项失败');
} }
const item = await response.json(); const item = await response.json();
deferModalContent(() => {
currentEditingItemId = id; document.getElementById('knowledge-item-category').value = item.category;
document.getElementById('knowledge-item-modal-title').textContent = '编辑知识'; document.getElementById('knowledge-item-title').value = item.title;
document.getElementById('knowledge-item-category').value = item.category; document.getElementById('knowledge-item-content').value = item.content;
document.getElementById('knowledge-item-title').value = item.title; document.getElementById('knowledge-item-title')?.focus();
document.getElementById('knowledge-item-content').value = item.content; });
document.getElementById('knowledge-item-modal').style.display = 'block';
} catch (error) { } catch (error) {
closeAppModal('knowledge-item-modal');
currentEditingItemId = null;
console.error('编辑知识项失败:', error); console.error('编辑知识项失败:', error);
showNotification('编辑知识项失败: ' + error.message, 'error'); showNotification('编辑知识项失败: ' + error.message, 'error');
} }
@@ -1232,10 +1239,7 @@ function updateKnowledgeStatsAfterDelete() {
// 关闭知识项模态框 // 关闭知识项模态框
function closeKnowledgeItemModal() { function closeKnowledgeItemModal() {
const modal = document.getElementById('knowledge-item-modal'); closeAppModal('knowledge-item-modal');
if (modal) {
modal.style.display = 'none';
}
// 重置编辑状态 // 重置编辑状态
currentEditingItemId = null; currentEditingItemId = null;
@@ -1786,8 +1790,11 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
document.body.appendChild(modal); document.body.appendChild(modal);
} }
// 填充内容
const content = document.getElementById('retrieval-log-details-content'); 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 timeAgo = getTimeAgo(log.createdAt);
const fullTime = formatTime(log.createdAt); const fullTime = formatTime(log.createdAt);
@@ -1880,16 +1887,12 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
</div> </div>
</div> </div>
`; `;
});
modal.style.display = 'block';
} }
// 关闭检索日志详情模态框 // 关闭检索日志详情模态框
function closeRetrievalLogDetailsModal() { function closeRetrievalLogDetailsModal() {
const modal = document.getElementById('retrieval-log-details-modal'); closeAppModal('retrieval-log-details-modal');
if (modal) {
modal.style.display = 'none';
}
} }
// 点击模态框外部关闭 // 点击模态框外部关闭
@@ -2118,7 +2121,8 @@ function showToastNotification(message, type = 'info') {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.45; line-height: 1.45;
word-wrap: break-word; word-wrap: break-word;
backdrop-filter: blur(8px); backdrop-filter: none;
-webkit-backdrop-filter: none;
`; `;
toast.innerHTML = ` 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;
})();
+27 -15
View File
@@ -944,18 +944,12 @@ function openUserInterruptModal(progressId, conversationId) {
if (ta) { if (ta) {
ta.value = ''; ta.value = '';
} }
const m = document.getElementById('user-interrupt-modal'); openAppModal('user-interrupt-modal');
if (m) {
m.style.display = 'block';
}
} }
function closeUserInterruptModal() { function closeUserInterruptModal() {
userInterruptModalPending = null; userInterruptModalPending = null;
const m = document.getElementById('user-interrupt-modal'); closeAppModal('user-interrupt-modal');
if (m) {
m.style.display = 'none';
}
} }
async function submitUserInterruptContinue() { async function submitUserInterruptContinue() {
@@ -3986,7 +3980,9 @@ async function setMcpMonitorTimelineRange(range) {
monitorState.timeline = timelineJson; monitorState.timeline = timelineJson;
const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner');
if (timelineInner) { 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(); bindMcpStatsTimelineEvents();
syncMcpMonitorTimelineRangeUI(range); syncMcpMonitorTimelineRangeUI(range);
} else if (monitorState.stats && Object.keys(monitorState.stats).length > 0) { } else if (monitorState.stats && Object.keys(monitorState.stats).length > 0) {
@@ -3996,7 +3992,9 @@ async function setMcpMonitorTimelineRange(range) {
monitorState.timelineError = err.message || 'error'; monitorState.timelineError = err.message || 'error';
const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner');
if (timelineInner) { 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(); bindMcpStatsTimelineEvents();
syncMcpMonitorTimelineRangeUI(range); syncMcpMonitorTimelineRangeUI(range);
} }
@@ -4014,7 +4012,21 @@ function renderMcpStatsTimelineRangeButtons() {
}).join(''); }).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'); const hint = mcpMonitorT('timelineHint') || monitorFallback('全部工具合计', 'All tools combined');
if (timelineError) { if (timelineError) {
@@ -4029,8 +4041,7 @@ function renderMcpStatsTimelineBody(timeline, timelineError) {
|| `区间内 ${summaryTotal} 次 · 峰值 ${peak}`; || `区间内 ${summaryTotal} 次 · 峰值 ${peak}`;
if (points.length === 0 || summaryTotal === 0) { if (points.length === 0 || summaryTotal === 0) {
const noData = mcpMonitorT('timelineNoData') || monitorFallback('该时段暂无调用', 'No calls in this period'); return renderMcpStatsTimelineEmptyState(!!compactEmpty);
return `<p class="mcp-stats-timeline-empty">${escapeHtml(noData)}</p>`;
} }
const rangeKey = timeline.range || getMcpMonitorTimelineRange(); const rangeKey = timeline.range || getMcpMonitorTimelineRange();
@@ -4083,7 +4094,7 @@ function renderMcpStatsCombinedSection(topTools, totals, activeToolFilter, timel
const timelineCol = showTimeline const timelineCol = showTimeline
? `<div class="mcp-stats-combined__timeline"> ? `<div class="mcp-stats-combined__timeline">
<p class="mcp-stats-combined__col-label">${escapeHtml(timelineTitle)}</p> <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>` </div>`
: ''; : '';
@@ -4897,7 +4908,8 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0)) .sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
.slice(0, MCP_STATS_TOP_N); .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 = ` const html = `
<div class="mcp-exec-stats"> <div class="mcp-exec-stats">
${renderMcpStatsMetricsBar(totals, successRate, rateTone, rateSubText, lastCallText, hasCalls)} ${renderMcpStatsMetricsBar(totals, successRate, rateTone, rateSubText, lastCallText, hasCalls)}
+295 -109
View File
@@ -5,12 +5,15 @@ let projectsCache = [];
let projectsCacheAll = []; let projectsCacheAll = [];
const PROJECTS_LIST_PAGE_SIZE_KEY = 'cyberstrike.projects_list_page_size'; const PROJECTS_LIST_PAGE_SIZE_KEY = 'cyberstrike.projects_list_page_size';
let currentProjectId = null; let currentProjectId = null;
let currentProjectUpdatedAt = null;
let currentProjectTab = 'facts'; let currentProjectTab = 'facts';
const projectNameById = {}; const projectNameById = {};
let _projectsListReady = false; let _projectsListReady = false;
let _projectsFetchPromise = null; let _projectsFetchPromise = null;
const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId'; const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId';
const PROJECT_DESCRIPTION_MAX_LENGTH = 4000;
const PROJECT_NAME_MAX_LENGTH = 200;
function tp(key, opts) { function tp(key, opts) {
if (typeof window.t === 'function') return window.t(key, opts); if (typeof window.t === 'function') return window.t(key, opts);
@@ -304,23 +307,9 @@ function prefetchProjectsForChat() {
ensureProjectsLoaded().catch(() => {}); ensureProjectsLoaded().catch(() => {});
} }
/** 新对话时:保留有效 activeProjectId,否则默认选中第一个进行中的项目 */ /** 新对话时默认不绑定项目;用户需主动选择后才写入共享黑板 */
async function ensureDefaultActiveProjectForNewChat() { async function ensureDefaultActiveProjectForNewChat() {
try { setActiveProjectId('');
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);
}
return ''; return '';
} }
@@ -343,7 +332,9 @@ async function initProjectsPage() {
const page = document.getElementById('page-projects'); const page = document.getElementById('page-projects');
if (!page || page.style.display === 'none') return; if (!page || page.style.display === 'none') return;
initProjectsModalEscape(); initProjectsModalEscape();
syncProjectsModalBodyLock(); if (typeof syncAppModalBodyLock === 'function') {
syncAppModalBodyLock();
}
updateProjectsDetailVisibility(); updateProjectsDetailVisibility();
projectsListPagination.pageSize = getProjectsListPageSize(); projectsListPagination.pageSize = getProjectsListPageSize();
renderProjectsPagination(); renderProjectsPagination();
@@ -611,11 +602,41 @@ function renderProjectsSidebar() {
<div class="projects-list-item-name">${escapeHtml(p.name)}${badges}</div> <div class="projects-list-item-name">${escapeHtml(p.name)}${badges}</div>
<div class="projects-list-item-meta">${formatProjectTime(p.updated_at)}</div> <div class="projects-list-item-meta">${formatProjectTime(p.updated_at)}</div>
</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>`; </div>`;
}).join(''); }).join('');
updateProjectsDetailVisibility(); 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) { function updateProjectStatusPill(status) {
const el = document.getElementById('projects-detail-status'); const el = document.getElementById('projects-detail-status');
if (!el) return; if (!el) return;
@@ -624,6 +645,24 @@ function updateProjectStatusPill(status) {
el.className = 'projects-status-pill ' + (archived ? 'projects-status-pill--archived' : 'projects-status-pill--active'); 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) { function updateProjectStats(stats) {
const s = stats || {}; const s = stats || {};
const f = document.getElementById('project-stat-facts'); const f = document.getElementById('project-stat-facts');
@@ -669,8 +708,7 @@ async function selectProject(id) {
const res = await apiFetch(`/api/projects/${id}`); const res = await apiFetch(`/api/projects/${id}`);
if (!res.ok) throw new Error(tp('projects.projectNotFound')); if (!res.ok) throw new Error(tp('projects.projectNotFound'));
const p = await res.json(); const p = await res.json();
const titleEl = document.getElementById('projects-detail-title'); renderProjectDetailTitle(p.name);
if (titleEl) titleEl.textContent = p.name || tp('projects.defaultProjectName');
document.getElementById('project-edit-name').value = p.name || ''; document.getElementById('project-edit-name').value = p.name || '';
document.getElementById('project-edit-description').value = p.description || ''; document.getElementById('project-edit-description').value = p.description || '';
document.getElementById('project-edit-scope').value = p.scope_json || ''; document.getElementById('project-edit-scope').value = p.scope_json || '';
@@ -679,19 +717,9 @@ async function selectProject(id) {
const pinEl = document.getElementById('project-edit-pinned'); const pinEl = document.getElementById('project-edit-pinned');
if (pinEl) pinEl.checked = !!p.pinned; if (pinEl) pinEl.checked = !!p.pinned;
updateProjectStatusPill(p.status || 'active'); updateProjectStatusPill(p.status || 'active');
const metaEl = document.getElementById('projects-detail-meta'); currentProjectUpdatedAt = p.updated_at;
if (metaEl) metaEl.textContent = tpFmt('projects.updatedPrefix', `Updated ${formatProjectTime(p.updated_at)}`, { time: formatProjectTime(p.updated_at) }); renderProjectDetailMeta(currentProjectUpdatedAt);
const descEl = document.getElementById('projects-detail-desc'); renderProjectDetailDesc(p.description);
if (descEl) {
const desc = (p.description || '').trim();
if (desc) {
descEl.textContent = desc;
descEl.hidden = false;
} else {
descEl.textContent = '';
descEl.hidden = true;
}
}
projectNameById[p.id] = p.name || p.id; projectNameById[p.id] = p.name || p.id;
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
@@ -858,38 +886,52 @@ let _factDetailFact = null;
let _projectFactsFilterDebounce = null; let _projectFactsFilterDebounce = null;
async function viewProjectFactBody(factKey) { async function viewProjectFactBody(factKey) {
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`); document.getElementById('fact-detail-title').textContent = factKey;
if (!res.ok) return alert(tp('common.loadFailed')); document.getElementById('fact-detail-meta').textContent = '…';
const f = await res.json(); document.getElementById('fact-detail-body').textContent = '';
_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');
const warnEl = document.getElementById('fact-detail-sparse-warn'); const warnEl = document.getElementById('fact-detail-sparse-warn');
if (warnEl) { if (warnEl) {
if (isSparseFactBody(f.category, f.fact_key, f.body)) { warnEl.hidden = true;
warnEl.hidden = false; warnEl.textContent = '';
warnEl.textContent = tp('projects.factSparseWarn');
} else {
warnEl.hidden = true;
warnEl.textContent = '';
}
} }
const linkBtn = document.getElementById('fact-detail-link-vuln-btn'); const linkBtn = document.getElementById('fact-detail-link-vuln-btn');
const createBtn = document.getElementById('fact-detail-create-vuln-btn'); const createBtn = document.getElementById('fact-detail-create-vuln-btn');
if (linkBtn) linkBtn.hidden = false; if (linkBtn) linkBtn.hidden = true;
if (createBtn) createBtn.hidden = false; if (createBtn) createBtn.hidden = true;
openProjectsOverlay('fact-detail-modal'); 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() { function editFactFromDetail() {
@@ -1144,41 +1186,16 @@ async function viewFactsForVulnerability(vulnId) {
else loadProjectFacts(); else loadProjectFacts();
} }
function openProjectsOverlay(id) { function openProjectsOverlay(id, opts) {
const el = document.getElementById(id); openAppModal(id, opts);
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 isProjectsOverlayVisible(id) { function isProjectsOverlayVisible(id) {
const el = document.getElementById(id); return isAppModalOpen(id);
if (!el) return false;
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden';
}
function hasVisibleProjectsOverlay() {
const overlays = document.querySelectorAll('.projects-modal-overlay');
return Array.from(overlays).some((el) => {
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden';
});
}
function syncProjectsModalBodyLock() {
if (hasVisibleProjectsOverlay()) document.body.classList.add('projects-modal-open');
else document.body.classList.remove('projects-modal-open');
} }
function closeProjectsOverlay(id) { function closeProjectsOverlay(id) {
const el = document.getElementById(id); closeAppModal(id);
if (el) el.style.display = 'none';
syncProjectsModalBodyLock();
} }
function showNewProjectModal() { function showNewProjectModal() {
@@ -1193,6 +1210,42 @@ function showNewProjectModal() {
openProjectsOverlay('project-modal'); 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() { function showNewProjectModalFromChat() {
closeChatProjectPanel(); closeChatProjectPanel();
@@ -1201,11 +1254,11 @@ function showNewProjectModalFromChat() {
} }
async function saveProjectModal() { 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')); if (!name) return alert(tp('projects.enterProjectName'));
const body = { const body = {
name, name,
description: document.getElementById('project-modal-description').value.trim(), description: clampProjectDescription(document.getElementById('project-modal-description').value),
}; };
const editId = window._projectModalEditId; const editId = window._projectModalEditId;
const res = editId const res = editId
@@ -1232,6 +1285,7 @@ async function saveProjectModal() {
function closeProjectModal() { function closeProjectModal() {
window._projectModalFromChat = false; window._projectModalFromChat = false;
window._projectModalEditId = null;
closeProjectsOverlay('project-modal'); closeProjectsOverlay('project-modal');
} }
@@ -1272,7 +1326,7 @@ async function saveProjectSettings() {
} }
const body = { const body = {
name: document.getElementById('project-edit-name').value.trim(), 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, scope_json: scopeRaw,
status: document.getElementById('project-edit-status')?.value || 'active', status: document.getElementById('project-edit-status')?.value || 'active',
pinned: !!document.getElementById('project-edit-pinned')?.checked, pinned: !!document.getElementById('project-edit-pinned')?.checked,
@@ -1288,30 +1342,112 @@ async function saveProjectSettings() {
alert(tp('projects.saved')); alert(tp('projects.saved'));
} }
async function archiveCurrentProject() { function findProjectById(projectId) {
if (!currentProjectId) return; return projectsCache.find((p) => p.id === projectId) || projectsCacheAll.find((p) => p.id === projectId);
const statusEl = document.getElementById('project-edit-status'); }
const cur = statusEl?.value || 'active';
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'; const next = cur === 'archived' ? 'active' : 'archived';
if (!confirm(next === 'archived' ? tp('projects.confirmArchiveProject') : tp('projects.confirmRestoreProjectActive'))) return; 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', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }), body: JSON.stringify({ status: next }),
}); });
if (!res.ok) return alert(tp('projects.operationFailed')); if (!res.ok) return alert(tp('projects.operationFailed'));
await loadProjectsList(); 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() { async function deleteProjectById(projectId) {
if (!currentProjectId || !confirm(tp('projects.confirmDeleteProject'))) return; if (!projectId || !confirm(tp('projects.confirmDeleteProject'))) return;
const deletedId = currentProjectId; const deletedIndex = projectsCache.findIndex((p) => p.id === projectId);
const deletedIndex = projectsCache.findIndex((p) => p.id === deletedId); const res = await apiFetch(`/api/projects/${projectId}`, { method: 'DELETE' });
const res = await apiFetch(`/api/projects/${deletedId}`, { method: 'DELETE' });
if (!res.ok) return alert(tp('projects.deleteFailed')); if (!res.ok) return alert(tp('projects.deleteFailed'));
if (getActiveProjectId() === deletedId) setActiveProjectId(''); if (getActiveProjectId() === projectId) setActiveProjectId('');
currentProjectId = null; if (currentProjectId === projectId) currentProjectId = null;
await loadProjectsList(); await loadProjectsList();
if (projectsCache.length) { if (projectsCache.length) {
const nextIndex = Math.min(deletedIndex >= 0 ? deletedIndex : 0, projectsCache.length - 1); const nextIndex = Math.min(deletedIndex >= 0 ? deletedIndex : 0, projectsCache.length - 1);
@@ -1321,6 +1457,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() { function resetFactModalForm() {
window._factModalEditId = null; window._factModalEditId = null;
const keyEl = document.getElementById('fact-modal-key'); const keyEl = document.getElementById('fact-modal-key');
@@ -1380,14 +1547,20 @@ function showAddFactModal() {
async function showEditFactModal(factKey) { async function showEditFactModal(factKey) {
if (!currentProjectId) return alert(tp('projects.selectProjectFirst')); if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
resetFactModalForm();
openProjectsOverlay('fact-modal', { focus: false });
const res = await apiFetch( const res = await apiFetch(
`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`, `/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(); const f = await res.json();
resetFactModalForm(); deferModalContent(() => {
fillFactModalForm(f); fillFactModalForm(f);
openProjectsOverlay('fact-modal'); document.getElementById('fact-modal-key')?.focus();
});
} }
function closeFactModal() { function closeFactModal() {
@@ -1714,6 +1887,10 @@ function initChatProjectSelector() {
const panel = document.getElementById('chat-project-panel'); const panel = document.getElementById('chat-project-panel');
if (panel && panel.style.display === 'flex') renderChatProjectPanelList(); if (panel && panel.style.display === 'flex') renderChatProjectPanelList();
if (currentProjectId) { 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(() => {}); refreshProjectHeaderStats().catch(() => {});
switchProjectTab(currentProjectTab || 'facts'); switchProjectTab(currentProjectTab || 'facts');
} }
@@ -1731,13 +1908,18 @@ function initChatProjectSelector() {
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initChatProjectSelector); document.addEventListener('DOMContentLoaded', () => {
initChatProjectSelector();
initProjectListActionMenu();
});
} else { } else {
initChatProjectSelector(); initChatProjectSelector();
initProjectListActionMenu();
} }
window.initProjectsPage = initProjectsPage; window.initProjectsPage = initProjectsPage;
window.showNewProjectModal = showNewProjectModal; window.showNewProjectModal = showNewProjectModal;
window.showEditProjectModal = showEditProjectModal;
window.showNewProjectModalFromChat = showNewProjectModalFromChat; window.showNewProjectModalFromChat = showNewProjectModalFromChat;
window.saveProjectModal = saveProjectModal; window.saveProjectModal = saveProjectModal;
window.closeProjectModal = closeProjectModal; window.closeProjectModal = closeProjectModal;
@@ -1752,6 +1934,10 @@ window.closeFactDetailModal = closeFactDetailModal;
window.saveProjectSettings = saveProjectSettings; window.saveProjectSettings = saveProjectSettings;
window.archiveCurrentProject = archiveCurrentProject; window.archiveCurrentProject = archiveCurrentProject;
window.deleteCurrentProject = deleteCurrentProject; window.deleteCurrentProject = deleteCurrentProject;
window.showProjectListActionMenu = showProjectListActionMenu;
window.editProjectFromListMenu = editProjectFromListMenu;
window.toggleProjectArchiveFromListMenu = toggleProjectArchiveFromListMenu;
window.deleteProjectFromListMenu = deleteProjectFromListMenu;
window.refreshChatProjectSelector = refreshChatProjectSelector; window.refreshChatProjectSelector = refreshChatProjectSelector;
window.onChatProjectChange = onChatProjectChange; window.onChatProjectChange = onChatProjectChange;
window.toggleChatProjectPanel = toggleChatProjectPanel; window.toggleChatProjectPanel = toggleChatProjectPanel;
+8 -6
View File
@@ -1112,7 +1112,7 @@ async function showAddRoleModal() {
// 确保统计信息正确更新(显示0/108) // 确保统计信息正确更新(显示0/108)
updateRoleToolsStats(); 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() { function closeRoleModal() {
const modal = document.getElementById('role-modal'); closeAppModal('role-modal');
if (modal) { }
modal.style.display = 'none';
} function closeRoleSelectModal() {
closeAppModal('role-select-modal');
} }
// 获取所有选中的工具(包括未在MCP管理中启用的工具) // 获取所有选中的工具(包括未在MCP管理中启用的工具)
@@ -1634,6 +1635,7 @@ if (typeof window !== 'undefined') {
window.getCurrentRole = getCurrentRole; window.getCurrentRole = getCurrentRole;
window.toggleRoleSelectionPanel = toggleRoleSelectionPanel; window.toggleRoleSelectionPanel = toggleRoleSelectionPanel;
window.closeRoleSelectionPanel = closeRoleSelectionPanel; window.closeRoleSelectionPanel = closeRoleSelectionPanel;
window.closeRoleSelectModal = closeRoleSelectModal;
window.filterRoleToolsByStatus = filterRoleToolsByStatus; window.filterRoleToolsByStatus = filterRoleToolsByStatus;
window.currentSelectedRole = getCurrentRole(); window.currentSelectedRole = getCurrentRole();
+1 -1
View File
@@ -505,7 +505,7 @@ document.addEventListener('DOMContentLoaded', function() {
let pageId = hashParts[0]; let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners'; 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); switchPage(pageId);
if (pageId === 'chat') { if (pageId === 'chat') {
scheduleChatConversationFromHash(200); 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').style.display = 'none';
document.getElementById('external-mcp-json-error').textContent = ''; document.getElementById('external-mcp-json-error').textContent = '';
document.getElementById('external-mcp-json').classList.remove('error'); document.getElementById('external-mcp-json').classList.remove('error');
document.getElementById('external-mcp-modal').style.display = 'block'; openAppModal('external-mcp-modal');
} }
// 关闭外部MCP模态框 // 关闭外部MCP模态框
function closeExternalMCPModal() { function closeExternalMCPModal() {
document.getElementById('external-mcp-modal').style.display = 'none'; closeAppModal('external-mcp-modal');
currentEditingMCPName = null; currentEditingMCPName = null;
} }
// 编辑外部MCP // 编辑外部MCP
async function editExternalMCP(name) { async function editExternalMCP(name) {
try { 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)}`); const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`);
if (!response.ok) { if (!response.ok) {
throw new Error(typeof window.t === 'function' ? window.t('mcp.getConfigFailed') : '获取外部MCP配置失败'); throw new Error(typeof window.t === 'function' ? window.t('mcp.getConfigFailed') : '获取外部MCP配置失败');
} }
const server = await response.json(); 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 }; const config = { ...server.config };
// 移除tool_count、external_mcp_enable等前端字段,但保留enabled/disabled用于向后兼容
delete config.tool_count; delete config.tool_count;
delete config.external_mcp_enable; delete config.external_mcp_enable;
// 包装成对象格式:{ "name": { config } }
const configObj = {}; const configObj = {};
configObj[name] = config; configObj[name] = config;
// 格式化JSON
const jsonStr = JSON.stringify(configObj, null, 2); const jsonStr = JSON.stringify(configObj, null, 2);
document.getElementById('external-mcp-json').value = jsonStr; deferModalContent(() => {
document.getElementById('external-mcp-json-error').style.display = 'none'; document.getElementById('external-mcp-json').value = jsonStr;
document.getElementById('external-mcp-json-error').textContent = ''; document.getElementById('external-mcp-json')?.focus();
document.getElementById('external-mcp-json').classList.remove('error'); });
document.getElementById('external-mcp-modal').style.display = 'block';
} catch (error) { } catch (error) {
closeExternalMCPModal();
console.error('编辑外部MCP失败:', error); console.error('编辑外部MCP失败:', error);
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '编辑失败') + ': ' + error.message); 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'); const modal = document.getElementById('skill-modal');
if (modal && modal.style.display === 'flex') { if (modal && isAppModalOpen('skill-modal')) {
return true; return true;
} }
@@ -465,7 +465,7 @@ function showAddSkillModal() {
const addTa = document.getElementById('skill-content-add'); const addTa = document.getElementById('skill-content-add');
if (addTa) addTa.value = ''; if (addTa) addTa.value = '';
modal.style.display = 'flex'; openAppModal('skill-modal');
} }
function skillPackagePathDepth(path) { function skillPackagePathDepth(path) {
@@ -555,6 +555,22 @@ async function selectSkillPackageFile(skillId, path, opts) {
// 编辑skill // 编辑skill
async function editSkill(skillId) { async function editSkill(skillId) {
wireSkillModalOnce(); 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 { try {
const [detailRes, filesRes] = await Promise.all([ const [detailRes, filesRes] = await Promise.all([
apiFetch(`/api/skills/${encodeURIComponent(skillId)}?depth=full`), apiFetch(`/api/skills/${encodeURIComponent(skillId)}?depth=full`),
@@ -565,39 +581,24 @@ async function editSkill(skillId) {
} }
const data = await detailRes.json(); const data = await detailRes.json();
const skill = data.skill; const skill = data.skill;
let files = [];
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 || '';
if (filesRes.ok) { if (filesRes.ok) {
const fd = await filesRes.json(); const fd = await filesRes.json();
skillPackageFiles = fd.files || []; files = fd.files || [];
} else {
skillPackageFiles = [];
} }
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; 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) { } catch (error) {
closeSkillModal();
console.error('加载skill详情失败:', error); console.error('加载skill详情失败:', error);
showNotification(_t('skills.loadDetailFailed') + ': ' + error.message, 'error'); showNotification(_t('skills.loadDetailFailed') + ': ' + error.message, 'error');
} }
@@ -659,7 +660,7 @@ async function viewSkill(skillId) {
</div> </div>
`; `;
document.body.appendChild(modal); document.body.appendChild(modal);
modal.style.display = 'flex'; openAppModal(modal);
const close = () => closeSkillViewModal(); const close = () => closeSkillViewModal();
modal.querySelectorAll('[data-skill-view-close]').forEach(el => el.addEventListener('click', close)); modal.querySelectorAll('[data-skill-view-close]').forEach(el => el.addEventListener('click', close));
@@ -691,23 +692,22 @@ async function viewSkill(skillId) {
// 关闭查看模态框 // 关闭查看模态框
function closeSkillViewModal() { function closeSkillViewModal() {
closeAppModal('skill-view-modal');
const modal = document.getElementById('skill-view-modal'); const modal = document.getElementById('skill-view-modal');
if (modal) { if (modal) {
modal.remove(); modal.remove();
syncAppModalBodyLock();
} }
} }
// 关闭skill模态框 // 关闭skill模态框
function closeSkillModal() { function closeSkillModal() {
const modal = document.getElementById('skill-modal'); closeAppModal('skill-modal');
if (modal) { currentEditingSkillName = null;
modal.style.display = 'none'; skillModalAddMode = true;
currentEditingSkillName = null; skillFileDirty = false;
skillModalAddMode = true; skillPackageFiles = [];
skillFileDirty = false; skillActivePath = 'SKILL.md';
skillPackageFiles = [];
skillActivePath = 'SKILL.md';
}
} }
// 保存skill // 保存skill
+18 -25
View File
@@ -914,18 +914,14 @@ async function showBatchImportModal() {
} }
} }
await refreshBatchProjectSelectOptions(); await refreshBatchProjectSelectOptions();
modal.style.display = 'block'; openAppModal('batch-import-modal', { focusEl: input });
input.focus();
} }
} }
// 关闭新建任务模态框 // 关闭新建任务模态框
function closeBatchImportModal() { function closeBatchImportModal() {
const modal = document.getElementById('batch-import-modal'); closeAppModal('batch-import-modal');
if (modal) {
modal.style.display = 'none';
}
} }
function handleBatchScheduleModeChange() { function handleBatchScheduleModeChange() {
@@ -1350,7 +1346,13 @@ async function showBatchQueueDetail(queueId) {
const addTaskBtn = document.getElementById('batch-queue-add-task-btn'); const addTaskBtn = document.getElementById('batch-queue-add-task-btn');
if (!modal || !content) return; 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 { try {
// 加载角色列表(如果还未加载) // 加载角色列表(如果还未加载)
let loadedRoles = []; let loadedRoles = [];
@@ -1459,6 +1461,7 @@ async function showBatchQueueDetail(queueId) {
const sameQueueAsBefore = prevDetailFor === queue.id; const sameQueueAsBefore = prevDetailFor === queue.id;
const savedTechDetailsOpen = sameQueueAsBefore && !!(prevTechDetails && prevTechDetails.open); const savedTechDetailsOpen = sameQueueAsBefore && !!(prevTechDetails && prevTechDetails.open);
deferModalContent(function () {
content.innerHTML = ` content.innerHTML = `
<div class="batch-queue-detail-layout" data-bq-detail-for="${escapeHtml(queue.id)}"> <div class="batch-queue-detail-layout" data-bq-detail-for="${escapeHtml(queue.id)}">
<section class="batch-queue-detail-hero"> <section class="batch-queue-detail-hero">
@@ -1529,8 +1532,7 @@ async function showBatchQueueDetail(queueId) {
if (newTechDetails && savedTechDetailsOpen) { if (newTechDetails && savedTechDetailsOpen) {
newTechDetails.open = true; newTechDetails.open = true;
} }
});
modal.style.display = 'block';
// 仅运行中定时拉取详情;其它状态应停止,避免 innerHTML 重绘把 <details> 等 UI 打回默认态 // 仅运行中定时拉取详情;其它状态应停止,避免 innerHTML 重绘把 <details> 等 UI 打回默认态
if (queue.status === 'running') { if (queue.status === 'running') {
@@ -1540,6 +1542,7 @@ async function showBatchQueueDetail(queueId) {
} }
} catch (error) { } catch (error) {
console.error('获取队列详情失败:', error); console.error('获取队列详情失败:', error);
closeBatchQueueDetailModal();
alert(_t('tasks.getQueueDetailFailed') + ': ' + error.message); alert(_t('tasks.getQueueDetailFailed') + ': ' + error.message);
} }
} }
@@ -1708,10 +1711,7 @@ async function deleteBatchQueueFromList(queueId) {
// 关闭批量任务队列详情模态框 // 关闭批量任务队列详情模态框
function closeBatchQueueDetailModal() { function closeBatchQueueDetailModal() {
const modal = document.getElementById('batch-queue-detail-modal'); closeAppModal('batch-queue-detail-modal');
if (modal) {
modal.style.display = 'none';
}
batchQueuesState.currentQueueId = null; batchQueuesState.currentQueueId = null;
stopBatchQueueRefresh(); stopBatchQueueRefresh();
} }
@@ -1730,7 +1730,7 @@ function startBatchQueueRefresh(queueId) {
content.querySelector('.bq-inline-edit-controls') || content.querySelector('.bq-inline-edit-controls') ||
content.querySelector('.batch-task-inline-edit') content.querySelector('.batch-task-inline-edit')
); );
if ((addModal && addModal.style.display === 'block') || hasInlineEdit) { if ((addModal && isAppModalOpen('add-batch-task-modal')) || hasInlineEdit) {
return; return;
} }
if (batchQueuesState._bqDetailRefreshing) { if (batchQueuesState._bqDetailRefreshing) {
@@ -1891,12 +1891,7 @@ function showAddBatchTaskModal() {
} }
messageInput.value = ''; messageInput.value = '';
modal.style.display = 'block'; openAppModal('add-batch-task-modal', { focusEl: messageInput });
// 聚焦到输入框
setTimeout(() => {
messageInput.focus();
}, 100);
// 清理旧的事件监听器 // 清理旧的事件监听器
if (showAddBatchTaskModal._escHandler) { if (showAddBatchTaskModal._escHandler) {
@@ -1940,9 +1935,7 @@ function closeAddBatchTaskModal() {
} }
const modal = document.getElementById('add-batch-task-modal'); const modal = document.getElementById('add-batch-task-modal');
const messageInput = document.getElementById('add-task-message'); const messageInput = document.getElementById('add-task-message');
if (modal) { closeAppModal('add-batch-task-modal');
modal.style.display = 'none';
}
if (messageInput) { if (messageInput) {
messageInput.value = ''; messageInput.value = '';
} }
@@ -2462,7 +2455,7 @@ document.addEventListener('languagechange', function () {
const detailModal = document.getElementById('batch-queue-detail-modal'); const detailModal = document.getElementById('batch-queue-detail-modal');
if ( if (
detailModal && detailModal &&
detailModal.style.display === 'block' && isAppModalOpen('batch-queue-detail-modal') &&
batchQueuesState.currentQueueId batchQueuesState.currentQueueId
) { ) {
showBatchQueueDetail(batchQueuesState.currentQueueId); showBatchQueueDetail(batchQueuesState.currentQueueId);
+24 -25
View File
@@ -1090,37 +1090,36 @@ async function showAddVulnerabilityModal() {
document.getElementById('vulnerability-impact').value = ''; document.getElementById('vulnerability-impact').value = '';
document.getElementById('vulnerability-recommendation').value = ''; document.getElementById('vulnerability-recommendation').value = '';
document.getElementById('vulnerability-modal').style.display = 'block'; openAppModal('vulnerability-modal');
} }
// 编辑漏洞 // 编辑漏洞
async function editVulnerability(id) { async function editVulnerability(id) {
try { try {
const response = await apiFetch(`/api/vulnerabilities/${id}`);
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
const vuln = await response.json();
currentVulnerabilityId = id; currentVulnerabilityId = id;
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.editVuln'); document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.editVuln');
openAppModal('vulnerability-modal', { focus: false });
// 填充表单 const response = await apiFetch(`/api/vulnerabilities/${id}`);
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || ''; if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || ''; const vuln = await response.json();
document.getElementById('vulnerability-task-tag').value = vuln.task_tag || ''; deferModalContent(async () => {
document.getElementById('vulnerability-title').value = vuln.title || ''; document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
document.getElementById('vulnerability-description').value = vuln.description || ''; document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || '';
document.getElementById('vulnerability-severity').value = vuln.severity || ''; document.getElementById('vulnerability-task-tag').value = vuln.task_tag || '';
document.getElementById('vulnerability-status').value = vuln.status || 'open'; document.getElementById('vulnerability-title').value = vuln.title || '';
document.getElementById('vulnerability-type').value = vuln.type || ''; document.getElementById('vulnerability-description').value = vuln.description || '';
document.getElementById('vulnerability-target').value = vuln.target || ''; document.getElementById('vulnerability-severity').value = vuln.severity || '';
document.getElementById('vulnerability-proof').value = vuln.proof || ''; document.getElementById('vulnerability-status').value = vuln.status || 'open';
document.getElementById('vulnerability-impact').value = vuln.impact || ''; document.getElementById('vulnerability-type').value = vuln.type || '';
document.getElementById('vulnerability-recommendation').value = vuln.recommendation || ''; document.getElementById('vulnerability-target').value = vuln.target || '';
document.getElementById('vulnerability-proof').value = vuln.proof || '';
await populateVulnerabilityModalProjectSelect(vuln.project_id || ''); document.getElementById('vulnerability-impact').value = vuln.impact || '';
document.getElementById('vulnerability-recommendation').value = vuln.recommendation || '';
document.getElementById('vulnerability-modal').style.display = 'block'; await populateVulnerabilityModalProjectSelect(vuln.project_id || '');
document.getElementById('vulnerability-title')?.focus();
});
} catch (error) { } catch (error) {
closeVulnerabilityModal();
console.error('加载漏洞失败:', error); console.error('加载漏洞失败:', error);
alert(vulnT('vulnerability.loadFailed') + ': ' + error.message); alert(vulnT('vulnerability.loadFailed') + ': ' + error.message);
} }
@@ -1233,7 +1232,7 @@ async function deleteVulnerability(id) {
// 关闭漏洞模态框 // 关闭漏洞模态框
function closeVulnerabilityModal() { function closeVulnerabilityModal() {
document.getElementById('vulnerability-modal').style.display = 'none'; closeAppModal('vulnerability-modal');
currentVulnerabilityId = null; currentVulnerabilityId = null;
} }
@@ -1749,7 +1748,7 @@ async function refreshVulnerabilityProjectFilter() {
sel.innerHTML = html; sel.innerHTML = html;
if (cur) sel.value = cur; if (cur) sel.value = cur;
const modalSel = document.getElementById('vulnerability-project-id'); 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 || ''; const modalCur = modalSel.value || '';
modalSel.innerHTML = buildVulnerabilityProjectOptionsHtml(modalCur); modalSel.innerHTML = buildVulnerabilityProjectOptionsHtml(modalCur);
modalSel.value = modalCur; modalSel.value = modalCur;
+27 -22
View File
@@ -2301,10 +2301,14 @@ function selectWebshell(id, stateReady) {
function setDbProfileModalVisible(visible, mode) { function setDbProfileModalVisible(visible, mode) {
if (!dbProfileModalEl) return; if (!dbProfileModalEl) return;
dbProfileModalEl.style.display = visible ? 'block' : 'none'; if (visible) {
if (dbProfileModalTitleEl) { if (dbProfileModalTitleEl) {
if (mode === 'add') dbProfileModalTitleEl.textContent = wsT('webshell.dbAddProfile') || '新增连接'; if (mode === 'add') dbProfileModalTitleEl.textContent = wsT('webshell.dbAddProfile') || '新增连接';
else dbProfileModalTitleEl.textContent = wsT('webshell.editConnectionTitle') || '编辑连接'; else dbProfileModalTitleEl.textContent = wsT('webshell.editConnectionTitle') || '编辑连接';
}
openAppModal(dbProfileModalEl);
} else {
closeAppModal(dbProfileModalEl);
} }
} }
@@ -4369,37 +4373,38 @@ function showAddWebshellModal() {
var titleEl = document.getElementById('webshell-modal-title'); var titleEl = document.getElementById('webshell-modal-title');
if (titleEl) titleEl.textContent = wsT('webshell.addConnection'); if (titleEl) titleEl.textContent = wsT('webshell.addConnection');
var modal = document.getElementById('webshell-modal'); var modal = document.getElementById('webshell-modal');
if (modal) modal.style.display = 'block'; if (modal) openAppModal(modal);
} }
// 打开编辑连接弹窗(预填当前连接信息) // 打开编辑连接弹窗(预填当前连接信息)
function showEditWebshellModal(connId) { function showEditWebshellModal(connId) {
var conn = webshellConnections.find(function (c) { return c.id === connId; }); var conn = webshellConnections.find(function (c) { return c.id === connId; });
if (!conn) return; 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'); var titleEl = document.getElementById('webshell-modal-title');
if (titleEl) titleEl.textContent = wsT('webshell.editConnectionTitle'); if (titleEl) titleEl.textContent = wsT('webshell.editConnectionTitle');
var modal = document.getElementById('webshell-modal'); openAppModal('webshell-modal', { focus: false });
if (modal) modal.style.display = 'block'; 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() { function closeWebshellModal() {
var editIdEl = document.getElementById('webshell-edit-id'); var editIdEl = document.getElementById('webshell-edit-id');
if (editIdEl) editIdEl.value = ''; if (editIdEl) editIdEl.value = '';
var modal = document.getElementById('webshell-modal'); closeAppModal('webshell-modal');
if (modal) modal.style.display = 'none';
} }
// 语言切换时刷新 WebShell 页面内所有由 JS 生成的文案(不重建终端) // 语言切换时刷新 WebShell 页面内所有由 JS 生成的文案(不重建终端)
@@ -4571,7 +4576,7 @@ function refreshWebshellUIOnLanguageChange() {
} }
var modal = document.getElementById('webshell-modal'); 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 titleEl = document.getElementById('webshell-modal-title');
var editIdEl = document.getElementById('webshell-edit-id'); var editIdEl = document.getElementById('webshell-edit-id');
if (titleEl) { if (titleEl) {
+33 -14
View File
@@ -9,6 +9,7 @@
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/c2.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="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
<script src="/static/js/router.js"></script>
</head> </head>
<body> <body>
<div id="login-overlay" class="login-overlay" style="display: none;"> <div id="login-overlay" class="login-overlay" style="display: none;">
@@ -1471,18 +1472,20 @@
<div class="projects-detail-inner" id="projects-detail-inner" hidden> <div class="projects-detail-inner" id="projects-detail-inner" hidden>
<header class="projects-detail-header"> <header class="projects-detail-header">
<div class="projects-detail-header-main"> <div class="projects-detail-header-main">
<div class="projects-detail-title-row"> <div class="projects-detail-headline">
<h3 id="projects-detail-title" class="projects-detail-title" data-i18n="projects.defaultProjectName">项目</h3> <div class="projects-detail-title-group">
<span id="projects-detail-status" class="projects-status-pill projects-status-pill--active" data-i18n="projects.statusActive">进行中</span> <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> </div>
<p id="projects-detail-meta" class="projects-detail-meta"></p> <p id="projects-detail-meta" class="projects-detail-meta"></p>
<p id="projects-detail-desc" class="projects-detail-desc"></p> <p id="projects-detail-desc" class="projects-detail-desc" hidden></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>
</div> </div>
<div class="projects-detail-header-actions"> <div class="projects-detail-header-actions">
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.vulnerabilityManagement">漏洞管理</button> <button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.vulnerabilityManagement">漏洞管理</button>
@@ -1669,7 +1672,8 @@
</div> </div>
<div class="projects-form-field"> <div class="projects-form-field">
<label for="project-edit-description" data-i18n="projects.projectDescription">描述</label> <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>
</div> </div>
</section> </section>
@@ -3777,6 +3781,20 @@
</div> </div>
</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 id="batch-import-modal" class="modal">
<div class="modal-content" style="max-width: 800px;"> <div class="modal-content" style="max-width: 800px;">
@@ -4155,11 +4173,12 @@
<div class="projects-modal-body"> <div class="projects-modal-body">
<div class="projects-form-field"> <div class="projects-form-field">
<label for="project-modal-name" data-i18n="projects.projectName">项目名称 <span class="required">*</span></label> <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>
<div class="projects-form-field"> <div class="projects-form-field">
<label for="project-modal-description" data-i18n="projects.projectDescription">项目描述</label> <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> </div>
<div class="projects-modal-footer"> <div class="projects-modal-footer">
@@ -4272,9 +4291,9 @@
<script src="/static/js/i18n.js"></script> <script src="/static/js/i18n.js"></script>
<script src="/static/js/builtin-tools.js"></script> <script src="/static/js/builtin-tools.js"></script>
<script src="/static/js/auth.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/notifications.js"></script>
<script src="/static/js/info-collect.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/agents.js"></script>
<script src="/static/js/dashboard.js"></script> <script src="/static/js/dashboard.js"></script>
<script src="/static/js/chat-scroll.js"></script> <script src="/static/js/chat-scroll.js"></script>