mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-06 06:13:58 +02:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9f1a2001e | |||
| 905dd519ed | |||
| 60ea106301 | |||
| 92c0ae19bb | |||
| 43c6a0648d | |||
| 6b96e77120 | |||
| a397922361 | |||
| 1e6e92b4af | |||
| 444f85b9c4 | |||
| 679a8192ae | |||
| 9a3f5e54b0 | |||
| ce2eb56253 | |||
| da6cb347df | |||
| fb2658b2eb | |||
| e791782c46 | |||
| 9b0efbb90f | |||
| 0d9eebffe6 | |||
| 403d4421d2 | |||
| e606369e31 | |||
| da8fdafe59 | |||
| 0492365430 | |||
| 3a6bc60276 | |||
| 3a401ade68 | |||
| 71aade5bd9 | |||
| a5f11cc003 | |||
| dcea95968b | |||
| 7db0294d5c | |||
| b4d85c5a77 | |||
| fcbc7b9226 | |||
| b8b1e8431b | |||
| 203a99bed4 | |||
| 449781c029 | |||
| 924f59015d | |||
| f0fb634a6b | |||
| b8dfb9556a | |||
| 9c1d3ae85e |
@@ -117,7 +117,8 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
|
|||||||
- 🛡️ Vulnerability management with CRUD operations, severity tracking, status workflow, and statistics
|
- 🛡️ Vulnerability management with CRUD operations, severity tracking, status workflow, and statistics
|
||||||
- 📋 Batch task management: create task queues, add multiple tasks, and execute them sequentially
|
- 📋 Batch task management: create task queues, add multiple tasks, and execute them sequentially
|
||||||
- 🎭 Role-based testing: predefined security testing roles (Penetration Testing, CTF, Web App Scanning, etc.) with custom prompts and tool restrictions
|
- 🎭 Role-based testing: predefined security testing roles (Penetration Testing, CTF, Web App Scanning, etc.) with custom prompts and tool restrictions
|
||||||
- 🧩 **Multi-agent (CloudWeGo Eino)**: alongside **single-agent ReAct** (`/api/agent-loop`), **multi mode** (`/api/multi-agent/stream`) offers **`deep`** (coordinator + `task` sub-agents), **`plan_execute`** (planner / executor / replanner), and **`supervisor`** (orchestrator + `transfer` / `exit`); chosen per request via **`orchestration`**. Markdown under `agents/`: `orchestrator.md` (Deep), `orchestrator-plan-execute.md`, `orchestrator-supervisor.md`, plus sub-agent `*.md` where applicable (see [Multi-agent doc](docs/MULTI_AGENT_EINO.md))
|
- 🧩 **Agent orchestration (CloudWeGo Eino)**: **single-agent** via **`/api/eino-agent/stream`** (Eino ADK `ChatModelAgent`); **multi-agent** via **`/api/multi-agent/stream`** with **`deep`** (coordinator + `task` sub-agents), **`plan_execute`**, or **`supervisor`** (`orchestration` in the request body). Markdown under `agents/`: `orchestrator.md`, `orchestrator-plan-execute.md`, `orchestrator-supervisor.md`, plus sub-agent `*.md` (see [Multi-agent doc](docs/MULTI_AGENT_EINO.md))
|
||||||
|
- 🖼️ **Vision analysis (`analyze_image`)**: separate VL model (e.g. `qwen-vl-max`) via MCP for local screenshots, captchas, and UI; image bytes stay out of agent history (text summaries only). Configure `vision` in `config.yaml`; see [docs/VISION.md](docs/VISION.md)
|
||||||
- 🎯 **Skills (refactored for Eino)**: packs under `skills_dir` follow **Agent Skills** layout (`SKILL.md` + optional files); **multi-agent** sessions use the official Eino ADK **`skill`** tool for **progressive disclosure** (load by name), with optional **host filesystem / shell** via `multi_agent.eino_skills`; optional **`eino_middleware`** adds patchtoolcalls, tool_search, plantask, reduction, checkpoints, and Deep tuning—20+ sample domains (SQLi, XSS, API security, …) ship under `skills/`
|
- 🎯 **Skills (refactored for Eino)**: packs under `skills_dir` follow **Agent Skills** layout (`SKILL.md` + optional files); **multi-agent** sessions use the official Eino ADK **`skill`** tool for **progressive disclosure** (load by name), with optional **host filesystem / shell** via `multi_agent.eino_skills`; optional **`eino_middleware`** adds patchtoolcalls, tool_search, plantask, reduction, checkpoints, and Deep tuning—20+ sample domains (SQLi, XSS, API security, …) ship under `skills/`
|
||||||
- 📱 **Chatbot**: DingTalk and Lark (Feishu) long-lived connections so you can talk to CyberStrikeAI from mobile (see [Robot / Chatbot guide](docs/robot_en.md) for setup and commands)
|
- 📱 **Chatbot**: DingTalk and Lark (Feishu) long-lived connections so you can talk to CyberStrikeAI from mobile (see [Robot / Chatbot guide](docs/robot_en.md) for setup and commands)
|
||||||
- 🧑⚖️ **Human-in-the-loop (HITL)**: Chat sidebar to set approval mode and tool allowlists (listed tools skip approval); global list in `config.yaml` under `hitl.tool_whitelist`; **Apply** can merge new tools into the file and update the running server without restart; dedicated **HITL** page for pending approvals
|
- 🧑⚖️ **Human-in-the-loop (HITL)**: Chat sidebar to set approval mode and tool allowlists (listed tools skip approval); global list in `config.yaml` under `hitl.tool_whitelist`; **Apply** can merge new tools into the file and update the running server without restart; dedicated **HITL** page for pending approvals
|
||||||
@@ -235,7 +236,7 @@ Requirements / tips:
|
|||||||
|
|
||||||
### Core Workflows
|
### Core Workflows
|
||||||
- **Conversation testing** – Natural-language prompts trigger toolchains with streaming SSE output.
|
- **Conversation testing** – Natural-language prompts trigger toolchains with streaming SSE output.
|
||||||
- **Single vs multi-agent** – With `multi_agent.enabled: true`, the chat UI can switch between **single** (classic **ReAct** loop, `/api/agent-loop/stream`) and **multi** (`/api/multi-agent/stream`). Multi mode keeps **`deep`** as the baseline coordinator + **`task`** sub-agents, and adds **`plan_execute`** and **`supervisor`** orchestrations via the request body **`orchestration`** field. MCP tools are bridged the same way as single-agent.
|
- **Single vs multi-agent** – Chat UI switches between **Eino single-agent** (`/api/eino-agent/stream`) and **multi-agent** (`/api/multi-agent/stream` with `orchestration`: `deep` | `plan_execute` | `supervisor`). Multi mode requires `multi_agent.enabled: true`. MCP tools are bridged the same way for both paths.
|
||||||
- **Role-based testing** – Select from predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, etc.) to customize AI behavior and tool availability. Each role applies custom system prompts and can restrict available tools for focused testing scenarios.
|
- **Role-based testing** – Select from predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, etc.) to customize AI behavior and tool availability. Each role applies custom system prompts and can restrict available tools for focused testing scenarios.
|
||||||
- **Tool monitor** – Inspect running jobs, execution logs, and large-result attachments.
|
- **Tool monitor** – Inspect running jobs, execution logs, and large-result attachments.
|
||||||
- **History & audit** – Every conversation and tool invocation is stored in SQLite with replay.
|
- **History & audit** – Every conversation and tool invocation is stored in SQLite with replay.
|
||||||
@@ -259,7 +260,7 @@ Requirements / tips:
|
|||||||
- **Predefined roles** – System includes 12+ predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, Binary Analysis, Cloud Security Audit, etc.) in the `roles/` directory.
|
- **Predefined roles** – System includes 12+ predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, Binary Analysis, Cloud Security Audit, etc.) in the `roles/` directory.
|
||||||
- **Custom prompts** – Each role can define a `user_prompt` that prepends to user messages, guiding the AI to adopt specialized testing methodologies and focus areas.
|
- **Custom prompts** – Each role can define a `user_prompt` that prepends to user messages, guiding the AI to adopt specialized testing methodologies and focus areas.
|
||||||
- **Tool restrictions** – Roles can specify a `tools` list to limit available tools, ensuring focused testing workflows (e.g., CTF role restricts to CTF-specific utilities).
|
- **Tool restrictions** – Roles can specify a `tools` list to limit available tools, ensuring focused testing workflows (e.g., CTF role restricts to CTF-specific utilities).
|
||||||
- **Skills** – Skill packs live under `skills_dir` and are loaded in **multi-agent / Eino** sessions via the ADK **`skill`** tool (**progressive disclosure**). Configure **`multi_agent.eino_skills`** for middleware, tool name override, and optional host **read_file / glob / grep / write / edit / execute** (**Deep / Supervisor** when enabled; **plan_execute** differs—see docs). Single-agent ReAct does not mount this Eino skill stack today.
|
- **Skills** – Skill packs live under `skills_dir` and load via the Eino ADK **`skill`** tool (**progressive disclosure**) in both **single- and multi-agent** sessions when **`multi_agent.eino_skills`** is enabled. Optional host **read_file / glob / grep / write / edit / execute** and **`eino_middleware`** (tool_search, reduction, checkpoints, etc.) apply per mode—see docs.
|
||||||
- **Easy role creation** – Create custom roles by adding YAML files to the `roles/` directory. Each role defines `name`, `description`, `user_prompt`, `icon`, `tools`, and `enabled` fields.
|
- **Easy role creation** – Create custom roles by adding YAML files to the `roles/` directory. Each role defines `name`, `description`, `user_prompt`, `icon`, `tools`, and `enabled` fields.
|
||||||
- **Web UI integration** – Select roles from a dropdown in the chat interface. Role selection affects both AI behavior and available tool suggestions.
|
- **Web UI integration** – Select roles from a dropdown in the chat interface. Role selection affects both AI behavior and available tool suggestions.
|
||||||
|
|
||||||
@@ -279,7 +280,7 @@ Requirements / tips:
|
|||||||
2. Restart the server or reload configuration; the role appears in the role selector dropdown.
|
2. Restart the server or reload configuration; the role appears in the role selector dropdown.
|
||||||
|
|
||||||
### Multi-Agent Mode (Eino: Deep, Plan-Execute, Supervisor)
|
### Multi-Agent Mode (Eino: Deep, Plan-Execute, Supervisor)
|
||||||
- **What it is** – An optional execution path beside **single-agent ReAct**, built on CloudWeGo **Eino** `adk/prebuilt`: **`deep`** — coordinator + **`task`** sub-agents; **`plan_execute`** — planner / executor / replanner loop (no YAML/Markdown sub-agent list); **`supervisor`** — orchestrator with **`transfer`** and **`exit`** over Markdown-defined specialists. The client sends **`orchestration`**: `deep` | `plan_execute` | `supervisor` (default `deep`).
|
- **What it is** – Multi-agent orchestration on CloudWeGo **Eino** `adk/prebuilt` (alongside **Eino single-agent** on `/api/eino-agent*`): **`deep`** — coordinator + **`task`** sub-agents; **`plan_execute`** — planner / executor / replanner; **`supervisor`** — orchestrator with **`transfer`** / **`exit`**. Client sends **`orchestration`**: `deep` | `plan_execute` | `supervisor` (default `deep`).
|
||||||
- **Markdown agents** – Under `agents_dir` (default `agents/`):
|
- **Markdown agents** – Under `agents_dir` (default `agents/`):
|
||||||
- **Deep orchestrator**: `orchestrator.md` *or* one `.md` with `kind: orchestrator`. Body or `multi_agent.orchestrator_instruction`, then Eino defaults.
|
- **Deep orchestrator**: `orchestrator.md` *or* one `.md` with `kind: orchestrator`. Body or `multi_agent.orchestrator_instruction`, then Eino defaults.
|
||||||
- **Plan-Execute orchestrator**: fixed name **`orchestrator-plan-execute.md`** (plus optional `orchestrator_instruction_plan_execute` in YAML).
|
- **Plan-Execute orchestrator**: fixed name **`orchestrator-plan-execute.md`** (plus optional `orchestrator_instruction_plan_execute` in YAML).
|
||||||
@@ -536,8 +537,8 @@ skills_dir: "skills" # Skills directory (relative to config file)
|
|||||||
agents_dir: "agents" # Multi-agent Markdown definitions (orchestrator + sub-agents)
|
agents_dir: "agents" # Multi-agent Markdown definitions (orchestrator + sub-agents)
|
||||||
multi_agent:
|
multi_agent:
|
||||||
enabled: false
|
enabled: false
|
||||||
default_mode: "single" # single | multi (UI default when multi-agent is enabled)
|
default_mode: "eino_single" # eino_single | multi (UI default when multi-agent is enabled)
|
||||||
robot_default_agent_mode: react
|
robot_default_agent_mode: eino_single
|
||||||
batch_use_multi_agent: false
|
batch_use_multi_agent: false
|
||||||
orchestrator_instruction: "" # Deep; used when orchestrator.md body is empty
|
orchestrator_instruction: "" # Deep; used when orchestrator.md body is empty
|
||||||
# orchestrator_instruction_plan_execute / orchestrator_instruction_supervisor optional
|
# orchestrator_instruction_plan_execute / orchestrator_instruction_supervisor optional
|
||||||
|
|||||||
+7
-6
@@ -116,7 +116,8 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
|||||||
- 🛡️ 漏洞管理功能:完整的漏洞 CRUD 操作,支持严重程度分级、状态流转、按对话/严重程度/状态过滤,以及统计看板
|
- 🛡️ 漏洞管理功能:完整的漏洞 CRUD 操作,支持严重程度分级、状态流转、按对话/严重程度/状态过滤,以及统计看板
|
||||||
- 📋 批量任务管理:创建任务队列,批量添加任务,依次顺序执行,支持任务编辑与状态跟踪
|
- 📋 批量任务管理:创建任务队列,批量添加任务,依次顺序执行,支持任务编辑与状态跟踪
|
||||||
- 🎭 角色化测试:预设安全测试角色(渗透测试、CTF、Web 应用扫描等),支持自定义提示词和工具限制
|
- 🎭 角色化测试:预设安全测试角色(渗透测试、CTF、Web 应用扫描等),支持自定义提示词和工具限制
|
||||||
- 🧩 **多代理(CloudWeGo Eino)**:在 **单代理 ReAct**(`/api/agent-loop`)之外,**多代理**(`/api/multi-agent/stream`)提供 **`deep`**(协调主代理 + `task` 子代理)、**`plan_execute`**(规划 / 执行 / 重规划)、**`supervisor`**(主代理 `transfer` / `exit` 监督子代理);由请求体 **`orchestration`** 选择。`agents/` 下分模式主代理:`orchestrator.md`(Deep)、`orchestrator-plan-execute.md`、`orchestrator-supervisor.md`,及适用的子代理 `*.md`(详见 [多代理说明](docs/MULTI_AGENT_EINO.md))
|
- 🧩 **Agent 编排(CloudWeGo Eino)**:**单代理** `POST /api/eino-agent/stream`(Eino ADK);**多代理** `POST /api/multi-agent/stream`,`orchestration` 选 **`deep`** / **`plan_execute`** / **`supervisor`**。`agents/` 下主代理与子代理 Markdown 见 [多代理说明](docs/MULTI_AGENT_EINO.md)
|
||||||
|
- 🖼️ **视觉分析(`analyze_image`)**:独立 Vision 模型(如 `qwen-vl-max`),MCP 工具分析本地截图/验证码/UI;图片仅在单次 VL 调用中出现,对话上下文只保留文字摘要。配置见 `config.yaml` → `vision` 与 [视觉分析说明](docs/VISION.md)
|
||||||
- 🎯 **Skills(面向 Eino 重构)**:技能包放在 **`skills_dir`**,遵循 **Agent Skills** 目录规范(`SKILL.md` + 可选文件);**多代理** 下通过 Eino 官方 **`skill`** 工具 **渐进式披露**(按 name 加载)。**`multi_agent.eino_skills`** 控制是否启用、本机文件/Shell 工具、工具名覆盖;**`eino_middleware`** 可选 patch、tool_search、plantask、reduction、断点目录及 Deep 调参。20+ 领域示例仍可绑定角色
|
- 🎯 **Skills(面向 Eino 重构)**:技能包放在 **`skills_dir`**,遵循 **Agent Skills** 目录规范(`SKILL.md` + 可选文件);**多代理** 下通过 Eino 官方 **`skill`** 工具 **渐进式披露**(按 name 加载)。**`multi_agent.eino_skills`** 控制是否启用、本机文件/Shell 工具、工具名覆盖;**`eino_middleware`** 可选 patch、tool_search、plantask、reduction、断点目录及 Deep 调参。20+ 领域示例仍可绑定角色
|
||||||
- 📱 **机器人**:支持钉钉、飞书长连接,在手机端与 CyberStrikeAI 对话(配置与命令详见 [机器人使用说明](docs/robot.md))
|
- 📱 **机器人**:支持钉钉、飞书长连接,在手机端与 CyberStrikeAI 对话(配置与命令详见 [机器人使用说明](docs/robot.md))
|
||||||
- 🧑⚖️ **人机协同(HITL)**:对话页侧栏配置协同模式与免审批工具白名单;全局列表在 `config.yaml` 的 `hitl.tool_whitelist`;点「应用」可将新增工具合并写入配置文件且**无需重启**即可生效;导航 **人机协同** 页处理待审批工具调用
|
- 🧑⚖️ **人机协同(HITL)**:对话页侧栏配置协同模式与免审批工具白名单;全局列表在 `config.yaml` 的 `hitl.tool_whitelist`;点「应用」可将新增工具合并写入配置文件且**无需重启**即可生效;导航 **人机协同** 页处理待审批工具调用
|
||||||
@@ -233,7 +234,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
|||||||
|
|
||||||
### 常用流程
|
### 常用流程
|
||||||
- **对话测试**:自然语言触发多步工具编排,SSE 实时输出。
|
- **对话测试**:自然语言触发多步工具编排,SSE 实时输出。
|
||||||
- **单代理 / 多代理**:`multi_agent.enabled: true` 后可在聊天中切换 **单代理**(原有 **ReAct**,`/api/agent-loop/stream`)与 **多代理**(`/api/multi-agent/stream`)。多代理在既有 **`deep`**(`task` 子代理)基础上,新增 **`plan_execute`**、**`supervisor`**,由 **`orchestration`** 指定。MCP 工具与单代理同源桥接。
|
- **单代理 / 多代理**:聊天可选 **Eino 单代理**(`/api/eino-agent/stream`)与 **多代理**(`/api/multi-agent/stream` + `orchestration`)。多代理需 `multi_agent.enabled: true`。MCP 工具桥接一致。
|
||||||
- **角色化测试**:从预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试等)中选择,自定义 AI 行为和可用工具。每个角色可应用自定义系统提示词,并可限制可用工具列表,实现聚焦的测试场景。
|
- **角色化测试**:从预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试等)中选择,自定义 AI 行为和可用工具。每个角色可应用自定义系统提示词,并可限制可用工具列表,实现聚焦的测试场景。
|
||||||
- **工具监控**:查看任务队列、执行日志、大文件附件。
|
- **工具监控**:查看任务队列、执行日志、大文件附件。
|
||||||
- **会话历史**:所有对话与工具调用保存在 SQLite,可随时重放。
|
- **会话历史**:所有对话与工具调用保存在 SQLite,可随时重放。
|
||||||
@@ -257,7 +258,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
|||||||
- **预设角色**:系统内置 12+ 个预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试、二进制分析、云安全审计等),位于 `roles/` 目录。
|
- **预设角色**:系统内置 12+ 个预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试、二进制分析、云安全审计等),位于 `roles/` 目录。
|
||||||
- **自定义提示词**:每个角色可定义 `user_prompt`,会在用户消息前自动添加,引导 AI 采用特定的测试方法和关注重点。
|
- **自定义提示词**:每个角色可定义 `user_prompt`,会在用户消息前自动添加,引导 AI 采用特定的测试方法和关注重点。
|
||||||
- **工具限制**:角色可指定 `tools` 列表,限制可用工具,实现聚焦的测试流程(如 CTF 角色限制为 CTF 专用工具)。
|
- **工具限制**:角色可指定 `tools` 列表,限制可用工具,实现聚焦的测试流程(如 CTF 角色限制为 CTF 专用工具)。
|
||||||
- **Skills**:技能包位于 `skills_dir`;**多代理 / Eino** 下由 **`skill`** 工具 **按需加载**(渐进式披露)。**`multi_agent.eino_skills`** 控制中间件与本机 read_file/glob/grep/write/edit/execute(**Deep / Supervisor** 主/子代理;**plan_execute** 执行器无独立 skill 中间件,见文档)。**单代理 ReAct** 当前不挂载该 Eino skill 链。
|
- **Skills**:技能包位于 `skills_dir`;启用 **`multi_agent.eino_skills`** 后,**单代理与多代理**均可通过 Eino **`skill`** 工具按需加载。中间件与本机 read_file/glob/grep 等见文档。
|
||||||
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`enabled` 字段。
|
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`enabled` 字段。
|
||||||
- **Web 界面集成**:在聊天界面通过下拉菜单选择角色。角色选择会影响 AI 行为和可用工具建议。
|
- **Web 界面集成**:在聊天界面通过下拉菜单选择角色。角色选择会影响 AI 行为和可用工具建议。
|
||||||
|
|
||||||
@@ -277,7 +278,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
|||||||
2. 重启服务或重新加载配置,角色会出现在角色选择下拉菜单中。
|
2. 重启服务或重新加载配置,角色会出现在角色选择下拉菜单中。
|
||||||
|
|
||||||
### 多代理模式(Eino:Deep / Plan-Execute / Supervisor)
|
### 多代理模式(Eino:Deep / Plan-Execute / Supervisor)
|
||||||
- **能力说明**:与 **单代理 ReAct** 并存的可选路径,基于 CloudWeGo **Eino** `adk/prebuilt`:**`deep`** — 协调主代理 + **`task`** 子代理;**`plan_execute`** — 规划 / 执行 / 重规划闭环(不使用 YAML/Markdown 子代理列表);**`supervisor`** — 主代理 **`transfer`** / **`exit`** 调度 Markdown 专家。客户端通过 **`orchestration`** 选 `deep` | `plan_execute` | `supervisor`(缺省 `deep`)。
|
- **能力说明**:在 **Eino 单代理**(`/api/eino-agent*`)之外,多代理基于 CloudWeGo **Eino** `adk/prebuilt`:**`deep`**、**`plan_execute`**、**`supervisor`**;客户端 **`orchestration`** 选择(缺省 `deep`)。
|
||||||
- **Markdown 定义**(`agents_dir`,默认 `agents/`):
|
- **Markdown 定义**(`agents_dir`,默认 `agents/`):
|
||||||
- **Deep 主代理**:`orchestrator.md` 或唯一 `kind: orchestrator` 的 `.md`;正文或 `multi_agent.orchestrator_instruction`,再回退 Eino 默认。
|
- **Deep 主代理**:`orchestrator.md` 或唯一 `kind: orchestrator` 的 `.md`;正文或 `multi_agent.orchestrator_instruction`,再回退 Eino 默认。
|
||||||
- **Plan-Execute 主代理**:固定 **`orchestrator-plan-execute.md`**(另可配 `orchestrator_instruction_plan_execute`)。
|
- **Plan-Execute 主代理**:固定 **`orchestrator-plan-execute.md`**(另可配 `orchestrator_instruction_plan_execute`)。
|
||||||
@@ -534,8 +535,8 @@ skills_dir: "skills" # Skills 目录(相对于配置文件所在目录)
|
|||||||
agents_dir: "agents" # 多代理 Markdown(主代理 orchestrator.md + 子代理 *.md)
|
agents_dir: "agents" # 多代理 Markdown(主代理 orchestrator.md + 子代理 *.md)
|
||||||
multi_agent:
|
multi_agent:
|
||||||
enabled: false
|
enabled: false
|
||||||
default_mode: "single" # single | multi(开启多代理时的界面默认模式)
|
default_mode: "eino_single" # eino_single | multi(开启多代理时的界面默认模式)
|
||||||
robot_default_agent_mode: react
|
robot_default_agent_mode: eino_single
|
||||||
batch_use_multi_agent: false
|
batch_use_multi_agent: false
|
||||||
orchestrator_instruction: "" # Deep;orchestrator.md 正文为空时使用
|
orchestrator_instruction: "" # Deep;orchestrator.md 正文为空时使用
|
||||||
# orchestrator_instruction_plan_execute / orchestrator_instruction_supervisor 可选
|
# orchestrator_instruction_plan_execute / orchestrator_instruction_supervisor 可选
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ description: supervisor 模式下的协调者:通过 transfer 委派专家子
|
|||||||
- **`transfer` 交接包(强制,避免专家重复侦察)**:**把专家当作刚走进房间的同事——它没看过你的对话,不知道你做了什么,也不了解这个任务为什么重要。** 在触发 `transfer` 的**同一条助手正文**中写清(勿仅依赖历史里的长工具输出;摘要后专家可能看不到细节):
|
- **`transfer` 交接包(强制,避免专家重复侦察)**:**把专家当作刚走进房间的同事——它没看过你的对话,不知道你做了什么,也不了解这个任务为什么重要。** 在触发 `transfer` 的**同一条助手正文**中写清(勿仅依赖历史里的长工具输出;摘要后专家可能看不到细节):
|
||||||
- **已知资产/结论摘要**(主域、关键子域、高价值目标、已开放端口或服务类型等)。
|
- **已知资产/结论摘要**(主域、关键子域、高价值目标、已开放端口或服务类型等)。
|
||||||
- **本轮唯一任务**与 **禁止项**(例如:「不得再做全量子域枚举;仅对下列主机做 MQTT 验证」)。
|
- **本轮唯一任务**与 **禁止项**(例如:「不得再做全量子域枚举;仅对下列主机做 MQTT 验证」)。
|
||||||
|
- **图片/验证码(若有)**:本地绝对路径 + 期望输出格式(如验证码「只输出字符」);专家默认看不到父对话识图结果,须在交接正文中写明。
|
||||||
- **专家类型**:验证/利用/协议分析派对应专家,**避免**把「仅差验证」的工作交给 `recon` 导致其按习惯从侦察阶段重来。
|
- **专家类型**:验证/利用/协议分析派对应专家,**避免**把「仅差验证」的工作交给 `recon` 导致其按习惯从侦察阶段重来。
|
||||||
- **transfer 前目标完整性校验(强制)**:在 `transfer` 前必须具备并显式写入:
|
- **transfer 前目标完整性校验(强制)**:在 `transfer` 前必须具备并显式写入:
|
||||||
- 目标标识:`URL` 或 `IP:Port` 或 `域名 + 具体路径/API 基址`
|
- 目标标识:`URL` 或 `IP:Port` 或 `域名 + 具体路径/API 基址`
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ description: 多代理模式下的 Deep 编排者:在已授权安全场景中
|
|||||||
- **`task` 上下文交接(强制,避免重复劳动)**:**把子代理当作刚走进房间的同事——它没看过你的对话,不知道你做了什么,也不了解这个任务为什么重要。** 框架下子代理默认**只看到**你传入的 `description` 文本,**看不到**你在父对话里已跑过的工具输出全文。因此每次 `task` 的 `description` 必须自带**交接包**(可精简,但不可省略关键事实):
|
- **`task` 上下文交接(强制,避免重复劳动)**:**把子代理当作刚走进房间的同事——它没看过你的对话,不知道你做了什么,也不了解这个任务为什么重要。** 框架下子代理默认**只看到**你传入的 `description` 文本,**看不到**你在父对话里已跑过的工具输出全文。因此每次 `task` 的 `description` 必须自带**交接包**(可精简,但不可省略关键事实):
|
||||||
- **已完成**:已枚举的主域/子域要点、已扫端口或服务结论、已确认 IP/URL、协调者已知的漏洞假设等(用列表或短段落即可)。
|
- **已完成**:已枚举的主域/子域要点、已扫端口或服务结论、已确认 IP/URL、协调者已知的漏洞假设等(用列表或短段落即可)。
|
||||||
- **本轮只做**:明确写「本轮禁止重复全量子域爆破 / 禁止重复相同 subfinder 参数集」等(若确实需要增量,写清增量范围)。
|
- **本轮只做**:明确写「本轮禁止重复全量子域爆破 / 禁止重复相同 subfinder 参数集」等(若确实需要增量,写清增量范围)。
|
||||||
|
- **图片/验证码(若有)**:本地绝对路径 + 期望输出格式(如验证码「只输出字符」、登录页 UI 要素列表);子代理默认看不到父对话里的识图结果,须在 description 中写明路径与格式。
|
||||||
- **专家匹配**:验证、利用、协议深挖(如 MQTT)等应委派给**对应专项子代理**;不要把此类子目标交给纯侦察(`recon`)角色除非任务仅为补充攻击面。
|
- **专家匹配**:验证、利用、协议深挖(如 MQTT)等应委派给**对应专项子代理**;不要把此类子目标交给纯侦察(`recon`)角色除非任务仅为补充攻击面。
|
||||||
- **派单前目标完整性校验(强制)**:在调用 `task` 前,你必须检查并写入最小必需字段;任一缺失时**禁止委派**,先向用户澄清或先自行补充证据:
|
- **派单前目标完整性校验(强制)**:在调用 `task` 前,你必须检查并写入最小必需字段;任一缺失时**禁止委派**,先向用户澄清或先自行补充证据:
|
||||||
- **目标标识**:`URL` 或 `IP:Port` 或 `域名 + 具体路径/API 基址`
|
- **目标标识**:`URL` 或 `IP:Port` 或 `域名 + 具体路径/API 基址`
|
||||||
|
|||||||
+23
-12
@@ -10,7 +10,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||||
version: "v1.6.29"
|
version: "v1.6.31"
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||||
@@ -65,6 +65,20 @@ openai:
|
|||||||
allow_client_reasoning: true # false 时忽略对话请求体 reasoning,仅以下方为准
|
allow_client_reasoning: true # false 时忽略对话请求体 reasoning,仅以下方为准
|
||||||
profile: openai_compat # auto | deepseek_compat | openai_compat | output_config_effort
|
profile: openai_compat # auto | deepseek_compat | openai_compat | output_config_effort
|
||||||
# extra_request_fields: {} # 可选:管理员自定义根级 JSON 片段(高级)
|
# extra_request_fields: {} # 可选:管理员自定义根级 JSON 片段(高级)
|
||||||
|
# 视觉分析(analyze_image MCP 工具;图片仅在单次 VL 调用中出现,Agent 上下文只保留文字摘要)
|
||||||
|
vision:
|
||||||
|
enabled: false # true 且 model 非空时注册 analyze_image
|
||||||
|
model: qwen-vl # VL 模型名(enabled 时必填)
|
||||||
|
api_key: "" # 留空则复用 openai.api_key
|
||||||
|
base_url: "" # 留空则复用 openai.base_url
|
||||||
|
provider: # 留空则复用 openai.provider(openai | claude)
|
||||||
|
max_image_bytes: 5242880 # 原始文件上限(字节),默认 5MB
|
||||||
|
max_dimension: 2048 # 长边缩放像素
|
||||||
|
jpeg_quality: 82
|
||||||
|
max_payload_bytes: 524288 # 编码后送 VL API 上限,默认 512KB
|
||||||
|
skip_preprocess_below_bytes: 2097152 # 低于 2MB 且长边<=max_dimension 且<=max_payload 时原图直传;0=始终压缩
|
||||||
|
detail: auto # low | high | auto(Eino ImageURLDetail)
|
||||||
|
timeout_seconds: 60
|
||||||
# ============================================
|
# ============================================
|
||||||
# 信息收集(FOFA)配置(可选)
|
# 信息收集(FOFA)配置(可选)
|
||||||
# ============================================
|
# ============================================
|
||||||
@@ -77,28 +91,26 @@ fofa:
|
|||||||
# Agent 配置
|
# Agent 配置
|
||||||
# 达到最大迭代次数时,AI 会自动总结测试结果
|
# 达到最大迭代次数时,AI 会自动总结测试结果
|
||||||
agent:
|
agent:
|
||||||
max_iterations: 12000 # 最大迭代次数,AI 代理最多执行多少轮工具调用
|
max_iterations: 12000 # 全局最大迭代次数(单代理 / Deep / Supervisor / Plan-Execute 主执行器 / 子代理均沿用;agents/*.md 中 max_iterations>0 可单独覆盖)
|
||||||
large_result_threshold: 102400 # 大结果阈值(字节),默认50KB,超过此大小会自动保存到存储
|
large_result_threshold: 102400 # 大结果阈值(字节),默认50KB,超过此大小会自动保存到存储
|
||||||
result_storage_dir: tmp # 结果存储目录,大结果会保存在此目录下
|
result_storage_dir: tmp # 结果存储目录,大结果会保存在此目录下
|
||||||
tool_timeout_minutes: 60 # 单次工具执行最大时长(分钟),超时自动终止;0 表示不限制(不推荐,易出现长时间挂起)
|
tool_timeout_minutes: 60 # 单次工具执行最大时长(分钟),超时自动终止;0 表示不限制(不推荐,易出现长时间挂起)
|
||||||
# system_prompt_path: prompts/single-react.md # 可选:单代理系统提示文件(相对本配置文件所在目录);非空且可读时替换内置提示
|
# system_prompt_path: prompts/single-agent.md # 可选:单代理系统提示文件(相对本配置文件所在目录);非空且可读时替换内置提示
|
||||||
|
|
||||||
system_prompt_path: ""
|
system_prompt_path: ""
|
||||||
# 人机协同(HITL)全局白名单:此处列出的工具始终免审批,与对话页「白名单工具(免审批,逗号分隔)」合并为并集;侧栏「应用」可合并写入本列表并立即生效。
|
# 人机协同(HITL)全局白名单:此处列出的工具始终免审批,与对话页「白名单工具(免审批,逗号分隔)」合并为并集;侧栏「应用」可合并写入本列表并立即生效。
|
||||||
hitl:
|
hitl:
|
||||||
# 按你环境里的真实工具名增删(与侧栏一致、小写不敏感);不需要全局免审批可改为 []
|
# 按你环境里的真实工具名增删(与侧栏一致、小写不敏感);不需要全局免审批可改为 []
|
||||||
tool_whitelist: [read_file, list_dir, glob, grep]
|
tool_whitelist: [read_file, list_dir, glob, grep]
|
||||||
# 多代理(CloudWeGo Eino DeepAgent,与上方单 Agent /api/agent-loop 并存)
|
# 多代理与 Eino 单代理(CloudWeGo Eino ADK;单代理入口 /api/eino-agent*,多代理 /api/multi-agent*)
|
||||||
# 依赖在 go.mod 中拉取;若下载失败可设置: go env -w GOPROXY=https://goproxy.cn,direct
|
# 依赖在 go.mod 中拉取;若下载失败可设置: go env -w GOPROXY=https://goproxy.cn,direct
|
||||||
# 启用后需重启服务才会注册 /api/multi-agent 与 /api/multi-agent/stream;Deep / Plan-Execute / Supervisor 由对话页与 WebShell 所选模式在请求体中传入;机器人按 robot_default_agent_mode
|
# Deep / Plan-Execute / Supervisor 由对话页与 WebShell 所选模式在请求体 orchestration 中指定;机器人按 robot_default_agent_mode
|
||||||
multi_agent:
|
multi_agent:
|
||||||
enabled: true
|
enabled: true
|
||||||
robot_default_agent_mode: eino_single # 企微/钉钉/飞书机器人默认对话模式:react | eino_single | deep | plan_execute | supervisor
|
robot_default_agent_mode: eino_single # 企微/钉钉/飞书机器人默认:eino_single | deep | plan_execute | supervisor
|
||||||
batch_use_multi_agent: false # true 时「批量任务」队列中每个子任务也走 Eino 多代理(成本更高)
|
batch_use_multi_agent: false # true 时「批量任务」队列中每个子任务也走 Eino 多代理(成本更高)
|
||||||
max_iteration: 0 # 主代理 / plan_execute 执行器最大轮次,0 表示沿用 agent.max_iterations
|
# plan_execute 专用:execute↔replan 外层循环上限,0 表示 Eino 默认 10。主/子代理 ReAct 轮次见 agent.max_iterations。
|
||||||
# plan_execute 专用:execute↔replan 外层循环上限,0 表示 Eino 默认 10。当前实现下 Executor 会挂载 patch/reduction/tool_search 等前置中间件。
|
|
||||||
plan_execute_loop_max_iterations: 0
|
plan_execute_loop_max_iterations: 0
|
||||||
sub_agent_max_iterations: 120
|
|
||||||
sub_agent_user_context_max_runes: 0 # 子代理 task 描述中自动注入用户原始请求的字符上限;0=默认2000,负数=禁用
|
sub_agent_user_context_max_runes: 0 # 子代理 task 描述中自动注入用户原始请求的字符上限;0=默认2000,负数=禁用
|
||||||
without_general_sub_agent: false # false 时保留 Deep 内置 general-purpose 子代理
|
without_general_sub_agent: false # false 时保留 Deep 内置 general-purpose 子代理
|
||||||
without_write_todos: false
|
without_write_todos: false
|
||||||
@@ -116,7 +128,7 @@ multi_agent:
|
|||||||
tool_search_enable: true # true:工具数 ≥ min 时启用 tool_search,仅前 N 个工具常驻,其余按正则按需解锁,省 token、减误选;false:全量工具进上下文
|
tool_search_enable: true # true:工具数 ≥ min 时启用 tool_search,仅前 N 个工具常驻,其余按正则按需解锁,省 token、减误选;false:全量工具进上下文
|
||||||
tool_search_min_tools: 20 # 达到该数量才启用 tool_search(避免工具很少时多此一举);与 always_visible 配合使用
|
tool_search_min_tools: 20 # 达到该数量才启用 tool_search(避免工具很少时多此一举);与 always_visible 配合使用
|
||||||
tool_search_always_visible: 12 # 始终直接暴露给模型的工具个数(顺序与角色工具列表一致);其余工具进入动态池,需 tool_search 解锁
|
tool_search_always_visible: 12 # 始终直接暴露给模型的工具个数(顺序与角色工具列表一致);其余工具进入动态池,需 tool_search 解锁
|
||||||
tool_search_always_visible_tools: [read_file, glob, grep, write_file, edit_file, execute, task, transfer_to_agent, exit, write_todos, skill, tool_search, TaskCreate, TaskGet, TaskUpdate, TaskList, record_vulnerability, list_vulnerabilities, get_vulnerability, list_knowledge_risk_types, search_knowledge_base, webshell_exec, webshell_file_list, webshell_file_read, webshell_file_write, manage_webshell_list, manage_webshell_add, manage_webshell_update, manage_webshell_delete, manage_webshell_test, batch_task_list, batch_task_get, batch_task_start, batch_task_rerun, batch_task_pause, batch_task_update_metadata, batch_task_update_schedule, batch_task_schedule_enabled, batch_task_update_task, batch_task_remove_task, batch_task_delete, batch_task_create, batch_task_add_task, http-framework-test] # 后端内置常驻工具白名单(优先于 always_visible 数量策略)
|
tool_search_always_visible_tools: [read_file, glob, grep, analyze_image, write_file, edit_file, execute, task, transfer_to_agent, exit, write_todos, skill, tool_search, TaskCreate, TaskGet, TaskUpdate, TaskList, record_vulnerability, list_vulnerabilities, get_vulnerability, list_knowledge_risk_types, search_knowledge_base, webshell_exec, webshell_file_list, webshell_file_read, webshell_file_write, manage_webshell_list, manage_webshell_add, manage_webshell_update, manage_webshell_delete, manage_webshell_test, batch_task_list, batch_task_get, batch_task_start, batch_task_rerun, batch_task_pause, batch_task_update_metadata, batch_task_update_schedule, batch_task_schedule_enabled, batch_task_update_task, batch_task_remove_task, batch_task_delete, batch_task_create, batch_task_add_task, http-framework-test] # 后端内置常驻工具白名单(优先于 always_visible 数量策略)
|
||||||
plantask_enable: false # true:主代理(Deep / Supervisor 主)挂载 TaskCreate/Get/Update/List;需 eino_skills 可用且 skills_dir 存在,否则仅打日志并跳过
|
plantask_enable: false # true:主代理(Deep / Supervisor 主)挂载 TaskCreate/Get/Update/List;需 eino_skills 可用且 skills_dir 存在,否则仅打日志并跳过
|
||||||
plantask_rel_dir: .eino/plantask # 结构化任务文件相对 skills_dir 的子目录,其下再按会话 ID 分子目录存放
|
plantask_rel_dir: .eino/plantask # 结构化任务文件相对 skills_dir 的子目录,其下再按会话 ID 分子目录存放
|
||||||
reduction_enable: true # true:大工具输出截断/落盘以控上下文;依赖与 plantask 相同的 eino local 写盘后端,无后端时不挂载
|
reduction_enable: true # true:大工具输出截断/落盘以控上下文;依赖与 plantask 相同的 eino local 写盘后端,无后端时不挂载
|
||||||
@@ -127,7 +139,6 @@ multi_agent:
|
|||||||
reduction_sub_agents: true # true:子代理也挂 reduction;false:仅编排主代理使用 reduction
|
reduction_sub_agents: true # true:子代理也挂 reduction;false:仅编排主代理使用 reduction
|
||||||
summarization_trigger_ratio: 0.8 # summarization 触发比例(max_total_tokens * ratio),建议 0.75~0.85
|
summarization_trigger_ratio: 0.8 # summarization 触发比例(max_total_tokens * ratio),建议 0.75~0.85
|
||||||
summarization_emit_internal_events: true # true:发出 summarization 内部事件(便于诊断)
|
summarization_emit_internal_events: true # true:发出 summarization 内部事件(便于诊断)
|
||||||
history_input_budget_ratio: 0.35 # 历史入队预算比例(max_total_tokens * ratio)
|
|
||||||
plan_execute_user_input_budget_ratio: 0.35 # plan_execute 中 userInput 预算比例(planner/replanner/executor 共用)
|
plan_execute_user_input_budget_ratio: 0.35 # plan_execute 中 userInput 预算比例(planner/replanner/executor 共用)
|
||||||
plan_execute_executed_steps_budget_ratio: 0.2 # plan_execute 中 executed_steps 预算比例
|
plan_execute_executed_steps_budget_ratio: 0.2 # plan_execute 中 executed_steps 预算比例
|
||||||
plan_execute_max_step_result_runes: 4000 # plan_execute 每步结果最大字符数(超出截断)
|
plan_execute_max_step_result_runes: 4000 # plan_execute 每步结果最大字符数(超出截断)
|
||||||
@@ -281,7 +292,7 @@ skills_dir: skills # Skills配置文件目录(相对于配置文件所在目
|
|||||||
# ============================================
|
# ============================================
|
||||||
# 多代理子 Agent(Markdown,唯一维护处)
|
# 多代理子 Agent(Markdown,唯一维护处)
|
||||||
# ============================================
|
# ============================================
|
||||||
# 每个 .md:YAML front matter(name / id / description / tools / bind_role / max_iterations / 可选 kind: orchestrator)+ 正文为系统提示词
|
# 每个 .md:YAML front matter(name / id / description / tools / bind_role / 可选 max_iterations>0 覆盖全局 / 可选 kind: orchestrator)+ 正文为系统提示词
|
||||||
# 主代理:固定文件名 orchestrator.md,或任意文件名 + front matter kind: orchestrator(全目录仅允许一个);主代理不参与 task 子代理列表
|
# 主代理:固定文件名 orchestrator.md,或任意文件名 + front matter kind: orchestrator(全目录仅允许一个);主代理不参与 task 子代理列表
|
||||||
# 高级用法:仍可在 multi_agent 块内写 sub_agents,会与本文目录合并且同 id 时 YAML 可被 .md 覆盖
|
# 高级用法:仍可在 multi_agent 块内写 sub_agents,会与本文目录合并且同 id 时 YAML 可被 .md 覆盖
|
||||||
agents_dir: agents
|
agents_dir: agents
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
# Eino 多代理改造说明(DeepAgent)
|
# Eino 多代理改造说明(DeepAgent)
|
||||||
|
|
||||||
本文档记录 **单 Agent(原有 ReAct)** 与 **多 Agent(CloudWeGo Eino `adk/prebuilt/deep`)** 并存的改造范围、进度与后续事项。
|
本文档记录 **Eino 单代理(ADK)** 与 **多 Agent(CloudWeGo Eino `adk/prebuilt`)** 的改造范围、进度与后续事项。原生 ReAct 执行路径已移除。
|
||||||
|
|
||||||
## 总体结论
|
## 总体结论
|
||||||
|
|
||||||
- **改造已可用于生产试验**:流式对话、MCP 工具桥接、配置开关、前端模式切换均已落地。
|
- **改造已可用于生产试验**:流式对话、MCP 工具桥接、配置开关、前端模式切换均已落地。
|
||||||
- **入口策略**:主聊天与 WebShell 在开启多代理且用户选择 **Deep / Plan-Execute / Supervisor** 时走 `/api/multi-agent/stream`,请求体字段 **`orchestration`** 指定当次编排(与界面一致);**原生 ReAct** 走 `/api/agent-loop/stream`。机器人、批量任务无该请求体时服务端按 **`deep`** 执行。均需 `multi_agent.enabled`。
|
- **入口策略**:**单代理** 走 `/api/eino-agent/stream`;多代理(**Deep / Plan-Execute / Supervisor**)走 `/api/multi-agent/stream`,请求体 **`orchestration`** 指定编排。机器人默认 `robot_default_agent_mode: eino_single`;批量队列默认 `eino_single`,多代理模式需 `multi_agent.enabled`。
|
||||||
|
|
||||||
## 已完成项
|
## 已完成项
|
||||||
|
|
||||||
| 项 | 说明 |
|
| 项 | 说明 |
|
||||||
|----|------|
|
|----|------|
|
||||||
| 依赖与代理 | `go.mod` 直接依赖 `github.com/cloudwego/eino`、`eino-ext/.../openai`;`go.mod` 注释与 `scripts/bootstrap-go.sh` 指导 **GOPROXY**(如 `https://goproxy.cn,direct`)。 |
|
| 依赖与代理 | `go.mod` 直接依赖 `github.com/cloudwego/eino`、`eino-ext/.../openai`;`go.mod` 注释与 `scripts/bootstrap-go.sh` 指导 **GOPROXY**(如 `https://goproxy.cn,direct`)。 |
|
||||||
| 配置 | `config.yaml` → `multi_agent`:`enabled`、`robot_use_multi_agent`、`max_iteration`、`sub_agents`(含可选 `bind_role`)、`eino_skills`、`eino_middleware` 等;结构体见 `internal/config/config.go`。 |
|
| 配置 | `config.yaml` → `agent.max_iterations` 为全局 ReAct 上限(主/子代理统一);`multi_agent`:`enabled`、`robot_use_multi_agent`、`sub_agents`(含可选 `bind_role`)、`eino_skills`、`eino_middleware` 等;结构体见 `internal/config/config.go`。 |
|
||||||
| Markdown 子代理 / 主代理 | 在 `agents_dir` 下放 `*.md`。**子代理**:供 Deep `task` 与 `supervisor` `transfer`。**主代理(按模式分离)**:`orchestrator.md`(或 `kind: orchestrator` 的**单个**其他 .md)→ **Deep**;固定名 `orchestrator-plan-execute.md` → **plan_execute**;固定名 `orchestrator-supervisor.md` → **supervisor**。正文优先于 YAML:`multi_agent.orchestrator_instruction`、`orchestrator_instruction_plan_execute`、`orchestrator_instruction_supervisor`;plan_execute / supervisor **不会**回退到 Deep 的 `orchestrator_instruction`。皆空时 plan_execute / supervisor 使用代码内置默认提示。管理:**Agents → Agent管理**;API:`/api/multi-agent/markdown-agents*`。 |
|
| Markdown 子代理 / 主代理 | 在 `agents_dir` 下放 `*.md`。**子代理**:供 Deep `task` 与 `supervisor` `transfer`。**主代理(按模式分离)**:`orchestrator.md`(或 `kind: orchestrator` 的**单个**其他 .md)→ **Deep**;固定名 `orchestrator-plan-execute.md` → **plan_execute**;固定名 `orchestrator-supervisor.md` → **supervisor**。正文优先于 YAML:`multi_agent.orchestrator_instruction`、`orchestrator_instruction_plan_execute`、`orchestrator_instruction_supervisor`;plan_execute / supervisor **不会**回退到 Deep 的 `orchestrator_instruction`。皆空时 plan_execute / supervisor 使用代码内置默认提示。管理:**Agents → Agent管理**;API:`/api/multi-agent/markdown-agents*`。 |
|
||||||
| MCP 桥 | `internal/einomcp`:`ToolsFromDefinitions` + 会话 ID 持有者,执行走 `Agent.ExecuteMCPToolForConversation`。 |
|
| MCP 桥 | `internal/einomcp`:`ToolsFromDefinitions` + 会话 ID 持有者,执行走 `Agent.ExecuteMCPToolForConversation`。 |
|
||||||
| 编排 | `internal/multiagent/runner.go`:`deep.New` + 子 `ChatModelAgent` + `adk.NewRunner`(`EnableStreaming: true`,可选 `CheckPointStore`),事件映射为现有 SSE `tool_call` / `response_delta` 等。 |
|
| 编排 | `internal/multiagent/runner.go`:`deep.New` + 子 `ChatModelAgent` + `adk.NewRunner`(`EnableStreaming: true`,可选 `CheckPointStore`),事件映射为现有 SSE `tool_call` / `response_delta` 等。 |
|
||||||
| HTTP | `POST /api/multi-agent`(非流式)、`POST /api/multi-agent/stream`(SSE);路由**常注册**,是否可用由运行时 `multi_agent.enabled` 决定(流式未启用时 SSE 内 `error` + `done`)。 |
|
| HTTP | `POST /api/multi-agent`(非流式)、`POST /api/multi-agent/stream`(SSE);路由**常注册**,是否可用由运行时 `multi_agent.enabled` 决定(流式未启用时 SSE 内 `error` + `done`)。 |
|
||||||
| 会话准备 | `internal/handler/multi_agent_prepare.go`:`prepareMultiAgentSession`(含 **WebShell** `CreateConversationWithWebshell`、工具白名单与单代理一致)。 |
|
| 会话准备 | `internal/handler/multi_agent_prepare.go`:`prepareMultiAgentSession`(含 **WebShell** `CreateConversationWithWebshell`、工具白名单与单代理一致)。 |
|
||||||
| 单 Agent | `internal/agent` 增加 `ToolsForRole`、`ExecuteMCPToolForConversation`;原 `/api/agent-loop` 未删改语义。 |
|
| 单 Agent | `internal/agent` 为 MCP/工具层(`ToolsForRole`、`ExecuteMCPToolForConversation`);单代理编排走 `RunEinoSingleChatModelAgent`(`/api/eino-agent*`)。 |
|
||||||
| 前端 | 主聊天 / WebShell:`multi_agent.enabled` 时可选 **原生 ReAct** 与三种 Eino 命名,多代理路径在 JSON 中带 `orchestration`。设置页不再配置预置编排项;`plan_execute` 外层循环上限等仍可在设置中保存。 |
|
| 前端 | 主聊天 / WebShell:**Eino 单代理**(`/api/eino-agent/stream`)与 **Deep / Plan-Execute / Supervisor**(`/api/multi-agent/stream` + `orchestration`);`multi_agent.enabled` 控制多代理选项是否展示。 |
|
||||||
| 流式兼容 | 与 `/api/agent-loop/stream` 共用 `handleStreamEvent`:`conversation`、`progress`、`response_start` / `response_delta`、`thinking` / `thinking_stream_*`(模型 `ReasoningContent`)、`tool_*`、`response`、`done` 等;`tool_result` 带 `toolCallId` 与 `tool_call` 联动;`data.mcpExecutionIds` 与进度 i18n 已对齐。 |
|
| 流式兼容 | Eino 单/多代理与 Web UI 共用 `handleStreamEvent`:`conversation`、`progress`、`response_start` / `response_delta`、`thinking` / `thinking_stream_*`、`tool_*`、`response`、`done` 等。 |
|
||||||
| 批量任务 | 队列 `agentMode` 为 `deep` / `plan_execute` / `supervisor` 时子任务带对应 `orchestration` 调用 `RunDeepAgent`;旧值 `multi` 与「`agentMode` 为空且 `batch_use_multi_agent: true`」均按 `deep`。 |
|
| 批量任务 | 队列 `agentMode` 为 `deep` / `plan_execute` / `supervisor` 时子任务带对应 `orchestration` 调用 `RunDeepAgent`;旧值 `multi` 与「`agentMode` 为空且 `batch_use_multi_agent: true`」均按 `deep`。 |
|
||||||
| 配置 API | `GET /api/config` 返回 `multi_agent: { enabled, robot_use_multi_agent, sub_agent_count }`;`PUT /api/config` 可更新 `enabled`、`robot_use_multi_agent`(不覆盖 `sub_agents`)。 |
|
| 配置 API | `GET /api/config` 返回 `multi_agent: { enabled, robot_use_multi_agent, sub_agent_count }`;`PUT /api/config` 可更新 `enabled`、`robot_use_multi_agent`(不覆盖 `sub_agents`)。 |
|
||||||
| OpenAPI | 多代理路径说明已更新(流式未启用为 SSE 错误事件)。 |
|
| OpenAPI | 多代理路径说明已更新(流式未启用为 SSE 错误事件)。 |
|
||||||
| 机器人 | `ProcessMessageForRobot` 在 `enabled && robot_use_multi_agent` 时调用 `multiagent.RunDeepAgent`。 |
|
| 机器人 | `ProcessMessageForRobot` 按 `robot_default_agent_mode`(默认 `eino_single`)调用 `RunEinoSingleChatModelAgent` 或 `RunDeepAgent`。 |
|
||||||
| 预置编排 | 聊天 / WebShell:`POST /api/multi-agent*` 请求体 `orchestration`:`deep` \| `plan_execute` \| `supervisor`(缺省 `deep`)。`plan_execute` 不构建 YAML/Markdown 子代理;`plan_execute_loop_max_iterations` 仍来自配置。`supervisor` 至少需一个子代理。 |
|
| 预置编排 | 聊天 / WebShell:`POST /api/multi-agent*` 请求体 `orchestration`:`deep` \| `plan_execute` \| `supervisor`(缺省 `deep`)。`plan_execute` 不构建 YAML/Markdown 子代理;`plan_execute_loop_max_iterations` 仍来自配置。`supervisor` 至少需一个子代理。 |
|
||||||
| Eino 中间件 | `multi_agent.eino_middleware`(可选):`patchtoolcalls`(默认开)、`toolsearch`(按阈值拆分 MCP 工具列表)、`plantask`(需 `eino_skills`)、`reduction`(大工具输出截断/落盘)、`checkpoint_dir`(Runner 断点)、`deep_output_key` / `deep_model_retry_max_retries` / `task_tool_description_prefix`(Deep 与 supervisor 主代理共享其中模型重试与 OutputKey)。`plan_execute` 的 Executor 无 Handlers:仅继承 **ToolsConfig** 侧效果(如 `tool_search` 列表拆分),不挂载 patch/plantask/reduction 中间件。 |
|
| Eino 中间件 | `multi_agent.eino_middleware`(可选):`patchtoolcalls`(默认开)、`toolsearch`(按阈值拆分 MCP 工具列表)、`plantask`(需 `eino_skills`)、`reduction`(大工具输出截断/落盘)、`checkpoint_dir`(Runner 断点)、`deep_output_key` / `deep_model_retry_max_retries` / `task_tool_description_prefix`(Deep 与 supervisor 主代理共享其中模型重试与 OutputKey)。`plan_execute` 的 Executor 无 Handlers:仅继承 **ToolsConfig** 侧效果(如 `tool_search` 列表拆分),不挂载 patch/plantask/reduction 中间件。 |
|
||||||
|
|
||||||
@@ -59,3 +59,4 @@
|
|||||||
| 2026-03-22 | `orchestrator.md` / `kind: orchestrator` 主代理、列表主/子标记、与 `orchestrator_instruction` 优先级。 |
|
| 2026-03-22 | `orchestrator.md` / `kind: orchestrator` 主代理、列表主/子标记、与 `orchestrator_instruction` 优先级。 |
|
||||||
| 2026-04-19 | 主聊天「对话模式」:原生 ReAct 与 Deep / Plan-Execute / Supervisor;`POST /api/multi-agent*` 请求体 `orchestration` 与界面一致;`config.yaml` / 设置页不再维护预置编排字段(机器人/批量默认 `deep`)。 |
|
| 2026-04-19 | 主聊天「对话模式」:原生 ReAct 与 Deep / Plan-Execute / Supervisor;`POST /api/multi-agent*` 请求体 `orchestration` 与界面一致;`config.yaml` / 设置页不再维护预置编排字段(机器人/批量默认 `deep`)。 |
|
||||||
| 2026-04-21 | 移除角色 `skills` 与 `/api/roles/skills/list`;`bind_role` 仅继承 tools;Skills 仅通过 Eino `skill` 工具按需加载。 |
|
| 2026-04-21 | 移除角色 `skills` 与 `/api/roles/skills/list`;`bind_role` 仅继承 tools;Skills 仅通过 Eino `skill` 工具按需加载。 |
|
||||||
|
| 2026-06-02 | **移除原生 ReAct**:删除 `/api/agent-loop*` 执行入口与 `AgentLoopWithProgress`;统一 Eino ADK(单代理 `/api/eino-agent*`,多代理 `/api/multi-agent*`);任务 cancel/tasks API 保留。 |
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 视觉分析(analyze_image)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
- **工具名**:`analyze_image`(MCP 内置)
|
||||||
|
- **行为**:读取本地图片 → `imaging` 缩放/JPEG 压缩 → 调用独立 **Vision** 模型 → 返回**纯文本**给 Agent
|
||||||
|
- **上下文**:图片字节**不会**写入对话历史;仅路径与文字摘要进入 Agent 上下文
|
||||||
|
|
||||||
|
## 配置(`config.yaml` → `vision`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vision:
|
||||||
|
enabled: true
|
||||||
|
model: qwen-vl-max # 必填
|
||||||
|
api_key: # 留空 → openai.api_key
|
||||||
|
base_url: # 留空 → openai.base_url
|
||||||
|
provider: # 留空 → openai.provider
|
||||||
|
max_image_bytes: 5242880
|
||||||
|
max_dimension: 2048
|
||||||
|
jpeg_quality: 82
|
||||||
|
max_payload_bytes: 524288
|
||||||
|
skip_preprocess_below_bytes: 2097152 # 低于 2MB 且长边<=max_dimension 时原图直传;0=始终 JPEG 压缩
|
||||||
|
detail: low # low | high | auto
|
||||||
|
timeout_seconds: 60
|
||||||
|
```
|
||||||
|
|
||||||
|
`enabled: false` 时不注册工具。
|
||||||
|
|
||||||
|
## Web 设置
|
||||||
|
|
||||||
|
**系统设置 → 基本设置 → 视觉分析(analyze_image)** 可配置启用开关、视觉模型、API Key/Base URL(留空复用 OpenAI)、预处理参数;**保存并应用** 后写入 `config.yaml` 并重新注册 MCP 工具。
|
||||||
|
|
||||||
|
## 路径
|
||||||
|
|
||||||
|
`analyze_image` 可读取服务器上任意可读的图片文件路径(绝对路径或相对于进程工作目录的相对路径)。仍校验图片扩展名与常规文件类型。
|
||||||
|
|
||||||
|
## Agent 使用
|
||||||
|
|
||||||
|
系统提示已说明:遇图片调用 `analyze_image`,勿用 `read_file` 读二进制图。
|
||||||
|
|
||||||
|
`multi_agent.eino_middleware.tool_search_always_visible_tools` 建议包含 `analyze_image`。
|
||||||
|
|
||||||
|
## 合规
|
||||||
|
|
||||||
|
启用后图片会发往 Vision API 配置的上游;敏感环境请使用可信网关或保持 `enabled: false`。
|
||||||
+1
-1
@@ -272,4 +272,4 @@ curl -X POST "http://localhost:8080/api/robot/test" \
|
|||||||
|
|
||||||
- 钉钉、飞书均**仅处理文本消息**;其他类型(如图片、语音)会提示暂不支持或忽略。
|
- 钉钉、飞书均**仅处理文本消息**;其他类型(如图片、语音)会提示暂不支持或忽略。
|
||||||
- 会话与 Web 端共用同一套对话数据:在机器人里创建的对话会在 Web 端「对话」列表中看到,反之亦然。
|
- 会话与 Web 端共用同一套对话数据:在机器人里创建的对话会在 Web 端「对话」列表中看到,反之亦然。
|
||||||
- 机器人执行逻辑与 **`/api/agent-loop/stream`** 一致(含进度回调、过程详情写入数据库),仅不向客户端推送 SSE,最后将完整回复一次性发回钉钉/飞书/企业微信。
|
- 机器人执行与 **Eino 单/多代理** 相同逻辑(`ProcessMessageForRobot`,含进度回调与过程详情入库),仅不向客户端推送 SSE,最后一次性回复钉钉/飞书/企业微信。默认 `robot_default_agent_mode: eino_single`。
|
||||||
|
|||||||
+1
-1
@@ -269,4 +269,4 @@ Check in this order:
|
|||||||
|
|
||||||
- DingTalk and Lark: **text messages only**; other types (e.g. image, voice) are not supported and may be ignored.
|
- DingTalk and Lark: **text messages only**; other types (e.g. image, voice) are not supported and may be ignored.
|
||||||
- Conversations are shared with the web UI: conversations created from the bot appear in the web “Conversations” list and vice versa.
|
- Conversations are shared with the web UI: conversations created from the bot appear in the web “Conversations” list and vice versa.
|
||||||
- Bot execution uses the same logic as **`/api/agent-loop/stream`** (progress callbacks, process details stored in the DB); only the final reply is sent back to DingTalk/Lark in one message (no SSE to the client).
|
- Bot execution uses the same **Eino single/multi-agent** path as the web UI (`ProcessMessageForRobot`, with progress callbacks and process details stored in the DB); only the final reply is sent back to DingTalk/Lark in one message (no SSE). Default: `robot_default_agent_mode: eino_single`.
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ require (
|
|||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 // indirect
|
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 // indirect
|
||||||
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/evanphx/json-patch v0.5.2 // indirect
|
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||||
@@ -90,6 +91,7 @@ require (
|
|||||||
golang.org/x/arch v0.15.0 // indirect
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.39.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -240,6 +242,8 @@ golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
|||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
|||||||
+3
-1035
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import (
|
|||||||
"cyberstrike-ai/internal/project"
|
"cyberstrike-ai/internal/project"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultSingleAgentSystemPrompt 单代理(ReAct / MCP)内置系统提示;可通过 agent.system_prompt_path 覆盖为文件。
|
// DefaultSingleAgentSystemPrompt 单代理(Eino ADK / MCP)内置系统提示;可通过 agent.system_prompt_path 覆盖为文件。
|
||||||
func DefaultSingleAgentSystemPrompt() string {
|
func DefaultSingleAgentSystemPrompt() string {
|
||||||
return `你是CyberStrikeAI,是一个专业的网络安全渗透测试专家。你可以使用各种安全工具进行自主渗透测试。分析目标并选择最佳测试策略。
|
return `你是CyberStrikeAI,是一个专业的网络安全渗透测试专家。你可以使用各种安全工具进行自主渗透测试。分析目标并选择最佳测试策略。
|
||||||
|
|
||||||
@@ -112,6 +112,6 @@ func DefaultSingleAgentSystemPrompt() string {
|
|||||||
## 技能库(Skills)与知识库
|
## 技能库(Skills)与知识库
|
||||||
|
|
||||||
- 技能包位于服务器 skills/ 目录(各子目录 SKILL.md,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。
|
- 技能包位于服务器 skills/ 目录(各子目录 SKILL.md,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。
|
||||||
- 单代理本会话通过 MCP 使用知识库与漏洞记录等;Skills 的渐进式加载在「多代理 / Eino DeepAgent」中由内置 skill 工具完成(需在配置中启用 multi_agent.eino_skills)。
|
- 本会话通过 MCP 使用知识库与漏洞记录等。Skills 由 Eino ADK skill 工具按需加载(配置 multi_agent.eino_skills;单代理与多代理均可,未启用时无 skill 工具)。
|
||||||
- 若当前无 skill 工具,需要完整 Skill 工作流时请使用多代理模式或切换为 Eino 编排会话(亦可选 Eino ADK 单代理路径 /api/eino-agent)。`
|
- 需要完整 Skill 工作流但当前无 skill 工具时,请确认已启用 multi_agent.eino_skills,或改用 Deep / Supervisor 等多代理编排(/api/multi-agent/stream)。`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,491 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"cyberstrike-ai/internal/config"
|
|
||||||
"cyberstrike-ai/internal/openai"
|
|
||||||
|
|
||||||
"github.com/pkoukk/tiktoken-go"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultMinRecentMessage 压缩历史消息时保留的最近消息数量,确保最近的对话上下文不被压缩
|
|
||||||
DefaultMinRecentMessage = 5
|
|
||||||
// defaultChunkSize 压缩历史消息时每次处理的消息块大小,将旧消息分成多个块进行摘要
|
|
||||||
defaultChunkSize = 10
|
|
||||||
// defaultMaxImages 压缩时最多保留的图片数量,超过此数量的图片会被移除以节省上下文空间
|
|
||||||
defaultMaxImages = 3
|
|
||||||
// defaultSummaryTimeout 生成消息摘要时的超时时间
|
|
||||||
defaultSummaryTimeout = 10 * time.Minute
|
|
||||||
|
|
||||||
summaryPromptTemplate = `你是一名负责为安全代理执行上下文压缩的助手,任务是在保持所有关键渗透信息完整的前提下压缩扫描数据。
|
|
||||||
|
|
||||||
必须保留的关键信息:
|
|
||||||
- 已发现的漏洞与潜在攻击路径
|
|
||||||
- 扫描结果与工具输出(可压缩,但需保留核心发现)
|
|
||||||
- 获取到的访问凭证、令牌或认证细节
|
|
||||||
- 系统架构洞察与潜在薄弱点
|
|
||||||
- 当前评估进展
|
|
||||||
- 失败尝试与死路(避免重复劳动)
|
|
||||||
- 关于测试策略的所有决策记录
|
|
||||||
|
|
||||||
压缩指南:
|
|
||||||
- 保留精确技术细节(URL、路径、参数、Payload 等)
|
|
||||||
- 将冗长的工具输出压缩成概述,但保留关键发现
|
|
||||||
- 记录版本号与识别出的技术/组件信息
|
|
||||||
- 保留可能暗示漏洞的原始报错
|
|
||||||
- 将重复或相似发现整合成一条带有共性说明的结论
|
|
||||||
|
|
||||||
请牢记:另一位安全代理会依赖这份摘要继续测试,他必须在不损失任何作战上下文的情况下无缝接手。
|
|
||||||
|
|
||||||
需要压缩的对话片段:
|
|
||||||
%s
|
|
||||||
|
|
||||||
请给出技术精准且简明扼要的摘要,覆盖全部与安全评估相关的上下文。`
|
|
||||||
)
|
|
||||||
|
|
||||||
// MemoryCompressor 负责在调用LLM前压缩历史上下文,以避免Token爆炸。
|
|
||||||
type MemoryCompressor struct {
|
|
||||||
maxTotalTokens int
|
|
||||||
minRecentMessage int
|
|
||||||
maxImages int
|
|
||||||
chunkSize int
|
|
||||||
summaryModel string
|
|
||||||
timeout time.Duration
|
|
||||||
|
|
||||||
tokenCounter TokenCounter
|
|
||||||
completionClient CompletionClient
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryCompressorConfig 用于初始化 MemoryCompressor。
|
|
||||||
type MemoryCompressorConfig struct {
|
|
||||||
MaxTotalTokens int
|
|
||||||
MinRecentMessage int
|
|
||||||
MaxImages int
|
|
||||||
ChunkSize int
|
|
||||||
SummaryModel string
|
|
||||||
Timeout time.Duration
|
|
||||||
TokenCounter TokenCounter
|
|
||||||
CompletionClient CompletionClient
|
|
||||||
Logger *zap.Logger
|
|
||||||
|
|
||||||
// 当 CompletionClient 为空时,可以通过 OpenAIConfig + HTTPClient 构造默认的客户端。
|
|
||||||
OpenAIConfig *config.OpenAIConfig
|
|
||||||
HTTPClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMemoryCompressor 创建新的 MemoryCompressor。
|
|
||||||
func NewMemoryCompressor(cfg MemoryCompressorConfig) (*MemoryCompressor, error) {
|
|
||||||
if cfg.Logger == nil {
|
|
||||||
cfg.Logger = zap.NewNop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有显式配置 MaxTotalTokens,则后续逻辑会根据模型的最大上下文长度进行控制;
|
|
||||||
// 优先推荐在 config.yaml 的 openai.max_total_tokens 中统一配置。
|
|
||||||
if cfg.MinRecentMessage <= 0 {
|
|
||||||
cfg.MinRecentMessage = DefaultMinRecentMessage
|
|
||||||
}
|
|
||||||
if cfg.MaxImages <= 0 {
|
|
||||||
cfg.MaxImages = defaultMaxImages
|
|
||||||
}
|
|
||||||
if cfg.ChunkSize <= 0 {
|
|
||||||
cfg.ChunkSize = defaultChunkSize
|
|
||||||
}
|
|
||||||
if cfg.Timeout <= 0 {
|
|
||||||
cfg.Timeout = defaultSummaryTimeout
|
|
||||||
}
|
|
||||||
if cfg.SummaryModel == "" && cfg.OpenAIConfig != nil && cfg.OpenAIConfig.Model != "" {
|
|
||||||
cfg.SummaryModel = cfg.OpenAIConfig.Model
|
|
||||||
}
|
|
||||||
if cfg.SummaryModel == "" {
|
|
||||||
return nil, errors.New("summary model is required (either SummaryModel or OpenAIConfig.Model must be set)")
|
|
||||||
}
|
|
||||||
if cfg.TokenCounter == nil {
|
|
||||||
cfg.TokenCounter = NewTikTokenCounter()
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.CompletionClient == nil {
|
|
||||||
if cfg.OpenAIConfig == nil {
|
|
||||||
return nil, errors.New("memory compressor requires either CompletionClient or OpenAIConfig")
|
|
||||||
}
|
|
||||||
if cfg.HTTPClient == nil {
|
|
||||||
cfg.HTTPClient = &http.Client{
|
|
||||||
Timeout: 5 * time.Minute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg.CompletionClient = NewOpenAICompletionClient(cfg.OpenAIConfig, cfg.HTTPClient, cfg.Logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MemoryCompressor{
|
|
||||||
maxTotalTokens: cfg.MaxTotalTokens,
|
|
||||||
minRecentMessage: cfg.MinRecentMessage,
|
|
||||||
maxImages: cfg.MaxImages,
|
|
||||||
chunkSize: cfg.ChunkSize,
|
|
||||||
summaryModel: cfg.SummaryModel,
|
|
||||||
timeout: cfg.Timeout,
|
|
||||||
tokenCounter: cfg.TokenCounter,
|
|
||||||
completionClient: cfg.CompletionClient,
|
|
||||||
logger: cfg.Logger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateConfig 更新OpenAI配置(用于动态更新模型配置)
|
|
||||||
func (mc *MemoryCompressor) UpdateConfig(cfg *config.OpenAIConfig) {
|
|
||||||
if cfg == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新summaryModel字段
|
|
||||||
if cfg.Model != "" {
|
|
||||||
mc.summaryModel = cfg.Model
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新completionClient中的配置(如果是OpenAICompletionClient)
|
|
||||||
if openAIClient, ok := mc.completionClient.(*OpenAICompletionClient); ok {
|
|
||||||
openAIClient.UpdateConfig(cfg)
|
|
||||||
mc.logger.Info("MemoryCompressor配置已更新",
|
|
||||||
zap.String("model", cfg.Model),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompressHistory 根据 Token 限制压缩历史消息。reservedTokens 为预留给 tools 等非消息内容的 token 数,压缩时使用 (maxTotalTokens - reservedTokens) 作为消息上限。
|
|
||||||
func (mc *MemoryCompressor) CompressHistory(ctx context.Context, messages []ChatMessage, reservedTokens int) ([]ChatMessage, bool, error) {
|
|
||||||
if len(messages) == 0 {
|
|
||||||
return messages, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mc.handleImages(messages)
|
|
||||||
|
|
||||||
systemMsgs, regularMsgs := mc.splitMessages(messages)
|
|
||||||
if len(regularMsgs) <= mc.minRecentMessage {
|
|
||||||
return messages, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveMax := mc.maxTotalTokens
|
|
||||||
if reservedTokens > 0 && reservedTokens < mc.maxTotalTokens {
|
|
||||||
effectiveMax = mc.maxTotalTokens - reservedTokens
|
|
||||||
}
|
|
||||||
|
|
||||||
totalTokens := mc.countTotalTokens(systemMsgs, regularMsgs)
|
|
||||||
if totalTokens <= int(float64(effectiveMax)*0.9) {
|
|
||||||
return messages, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
recentStart := len(regularMsgs) - mc.minRecentMessage
|
|
||||||
recentStart = mc.adjustRecentStartForToolCalls(regularMsgs, recentStart)
|
|
||||||
oldMsgs := regularMsgs[:recentStart]
|
|
||||||
recentMsgs := regularMsgs[recentStart:]
|
|
||||||
|
|
||||||
mc.logger.Info("memory compression triggered",
|
|
||||||
zap.Int("total_tokens", totalTokens),
|
|
||||||
zap.Int("max_total_tokens", mc.maxTotalTokens),
|
|
||||||
zap.Int("reserved_tokens", reservedTokens),
|
|
||||||
zap.Int("effective_max", effectiveMax),
|
|
||||||
zap.Int("system_messages", len(systemMsgs)),
|
|
||||||
zap.Int("regular_messages", len(regularMsgs)),
|
|
||||||
zap.Int("old_messages", len(oldMsgs)),
|
|
||||||
zap.Int("recent_messages", len(recentMsgs)))
|
|
||||||
|
|
||||||
var compressed []ChatMessage
|
|
||||||
for i := 0; i < len(oldMsgs); i += mc.chunkSize {
|
|
||||||
end := i + mc.chunkSize
|
|
||||||
if end > len(oldMsgs) {
|
|
||||||
end = len(oldMsgs)
|
|
||||||
}
|
|
||||||
chunk := oldMsgs[i:end]
|
|
||||||
if len(chunk) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
summary, err := mc.summarizeChunk(ctx, chunk)
|
|
||||||
if err != nil {
|
|
||||||
mc.logger.Warn("chunk summary failed, fallback to raw chunk",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.Int("start", i),
|
|
||||||
zap.Int("end", end))
|
|
||||||
compressed = append(compressed, chunk...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
compressed = append(compressed, summary)
|
|
||||||
}
|
|
||||||
|
|
||||||
finalMessages := make([]ChatMessage, 0, len(systemMsgs)+len(compressed)+len(recentMsgs))
|
|
||||||
finalMessages = append(finalMessages, systemMsgs...)
|
|
||||||
finalMessages = append(finalMessages, compressed...)
|
|
||||||
finalMessages = append(finalMessages, recentMsgs...)
|
|
||||||
|
|
||||||
return finalMessages, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MemoryCompressor) handleImages(messages []ChatMessage) {
|
|
||||||
if mc.maxImages <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
count := 0
|
|
||||||
for i := len(messages) - 1; i >= 0; i-- {
|
|
||||||
content := messages[i].Content
|
|
||||||
if !strings.Contains(content, "[IMAGE]") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
if count > mc.maxImages {
|
|
||||||
messages[i].Content = "[Previously attached image removed to preserve context]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MemoryCompressor) splitMessages(messages []ChatMessage) (systemMsgs, regularMsgs []ChatMessage) {
|
|
||||||
for _, msg := range messages {
|
|
||||||
if strings.EqualFold(msg.Role, "system") {
|
|
||||||
systemMsgs = append(systemMsgs, msg)
|
|
||||||
} else {
|
|
||||||
regularMsgs = append(regularMsgs, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MemoryCompressor) countTotalTokens(systemMsgs, regularMsgs []ChatMessage) int {
|
|
||||||
total := 0
|
|
||||||
for _, msg := range systemMsgs {
|
|
||||||
total += mc.countTokens(msg.Content)
|
|
||||||
}
|
|
||||||
for _, msg := range regularMsgs {
|
|
||||||
total += mc.countTokens(msg.Content)
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
// getModelName 获取当前使用的模型名称(优先从completionClient获取最新配置)
|
|
||||||
func (mc *MemoryCompressor) getModelName() string {
|
|
||||||
// 如果completionClient是OpenAICompletionClient,从它获取最新的模型名称
|
|
||||||
if openAIClient, ok := mc.completionClient.(*OpenAICompletionClient); ok {
|
|
||||||
if openAIClient.config != nil && openAIClient.config.Model != "" {
|
|
||||||
return openAIClient.config.Model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 否则使用保存的summaryModel
|
|
||||||
return mc.summaryModel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MemoryCompressor) countTokens(text string) int {
|
|
||||||
if mc.tokenCounter == nil {
|
|
||||||
return len(text) / 4
|
|
||||||
}
|
|
||||||
modelName := mc.getModelName()
|
|
||||||
count, err := mc.tokenCounter.Count(modelName, text)
|
|
||||||
if err != nil {
|
|
||||||
return len(text) / 4
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountTextTokens 对外暴露的文本 Token 计数,用于统计 tools 等非消息内容的 token(如 agent 侧序列化 tools 后计数)。
|
|
||||||
func (mc *MemoryCompressor) CountTextTokens(text string) int {
|
|
||||||
return mc.countTokens(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// totalTokensFor provides token statistics without mutating the message list.
|
|
||||||
func (mc *MemoryCompressor) totalTokensFor(messages []ChatMessage) (totalTokens int, systemCount int, regularCount int) {
|
|
||||||
if len(messages) == 0 {
|
|
||||||
return 0, 0, 0
|
|
||||||
}
|
|
||||||
systemMsgs, regularMsgs := mc.splitMessages(messages)
|
|
||||||
return mc.countTotalTokens(systemMsgs, regularMsgs), len(systemMsgs), len(regularMsgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MemoryCompressor) summarizeChunk(ctx context.Context, chunk []ChatMessage) (ChatMessage, error) {
|
|
||||||
if len(chunk) == 0 {
|
|
||||||
return ChatMessage{}, errors.New("chunk is empty")
|
|
||||||
}
|
|
||||||
formatted := make([]string, 0, len(chunk))
|
|
||||||
for _, msg := range chunk {
|
|
||||||
formatted = append(formatted, fmt.Sprintf("%s: %s", msg.Role, mc.extractMessageText(msg)))
|
|
||||||
}
|
|
||||||
conversation := strings.Join(formatted, "\n")
|
|
||||||
prompt := fmt.Sprintf(summaryPromptTemplate, conversation)
|
|
||||||
|
|
||||||
// 使用动态获取的模型名称,而不是保存的summaryModel
|
|
||||||
modelName := mc.getModelName()
|
|
||||||
summary, err := mc.completionClient.Complete(ctx, modelName, prompt, mc.timeout)
|
|
||||||
if err != nil {
|
|
||||||
return ChatMessage{}, err
|
|
||||||
}
|
|
||||||
summary = strings.TrimSpace(summary)
|
|
||||||
if summary == "" {
|
|
||||||
return chunk[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChatMessage{
|
|
||||||
Role: "assistant",
|
|
||||||
Content: fmt.Sprintf("<context_summary message_count='%d'>%s</context_summary>", len(chunk), summary),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MemoryCompressor) extractMessageText(msg ChatMessage) string {
|
|
||||||
return msg.Content
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MemoryCompressor) adjustRecentStartForToolCalls(msgs []ChatMessage, recentStart int) int {
|
|
||||||
if recentStart <= 0 || recentStart >= len(msgs) {
|
|
||||||
return recentStart
|
|
||||||
}
|
|
||||||
|
|
||||||
adjusted := recentStart
|
|
||||||
for adjusted > 0 && strings.EqualFold(msgs[adjusted].Role, "tool") {
|
|
||||||
adjusted--
|
|
||||||
}
|
|
||||||
|
|
||||||
if adjusted != recentStart {
|
|
||||||
mc.logger.Debug("adjusted recent window to keep tool call context",
|
|
||||||
zap.Int("original_recent_start", recentStart),
|
|
||||||
zap.Int("adjusted_recent_start", adjusted),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adjusted
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenCounter 用于计算文本Token数量。
|
|
||||||
type TokenCounter interface {
|
|
||||||
Count(model, text string) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TikTokenCounter 基于 tiktoken 的 Token 统计器。
|
|
||||||
type TikTokenCounter struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
cache map[string]*tiktoken.Tiktoken
|
|
||||||
fallbackEncoding *tiktoken.Tiktoken
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTikTokenCounter 创建新的 TikTokenCounter。
|
|
||||||
func NewTikTokenCounter() *TikTokenCounter {
|
|
||||||
return &TikTokenCounter{
|
|
||||||
cache: make(map[string]*tiktoken.Tiktoken),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count 实现 TokenCounter 接口。
|
|
||||||
func (tc *TikTokenCounter) Count(model, text string) (int, error) {
|
|
||||||
enc, err := tc.encodingForModel(model)
|
|
||||||
if err != nil {
|
|
||||||
return len(text) / 4, err
|
|
||||||
}
|
|
||||||
tokens := enc.Encode(text, nil, nil)
|
|
||||||
return len(tokens), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *TikTokenCounter) encodingForModel(model string) (*tiktoken.Tiktoken, error) {
|
|
||||||
tc.mu.RLock()
|
|
||||||
if enc, ok := tc.cache[model]; ok {
|
|
||||||
tc.mu.RUnlock()
|
|
||||||
return enc, nil
|
|
||||||
}
|
|
||||||
tc.mu.RUnlock()
|
|
||||||
|
|
||||||
tc.mu.Lock()
|
|
||||||
defer tc.mu.Unlock()
|
|
||||||
|
|
||||||
if enc, ok := tc.cache[model]; ok {
|
|
||||||
return enc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
enc, err := tiktoken.EncodingForModel(model)
|
|
||||||
if err != nil {
|
|
||||||
if tc.fallbackEncoding == nil {
|
|
||||||
tc.fallbackEncoding, err = tiktoken.GetEncoding("cl100k_base")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tc.cache[model] = tc.fallbackEncoding
|
|
||||||
return tc.fallbackEncoding, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.cache[model] = enc
|
|
||||||
return enc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompletionClient 对话压缩时使用的补全接口。
|
|
||||||
type CompletionClient interface {
|
|
||||||
Complete(ctx context.Context, model string, prompt string, timeout time.Duration) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAICompletionClient 基于 OpenAI Chat Completion。
|
|
||||||
type OpenAICompletionClient struct {
|
|
||||||
config *config.OpenAIConfig
|
|
||||||
client *openai.Client
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOpenAICompletionClient 创建 OpenAICompletionClient。
|
|
||||||
func NewOpenAICompletionClient(cfg *config.OpenAIConfig, client *http.Client, logger *zap.Logger) *OpenAICompletionClient {
|
|
||||||
if logger == nil {
|
|
||||||
logger = zap.NewNop()
|
|
||||||
}
|
|
||||||
return &OpenAICompletionClient{
|
|
||||||
config: cfg,
|
|
||||||
client: openai.NewClient(cfg, client, logger),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateConfig 更新底层配置。
|
|
||||||
func (c *OpenAICompletionClient) UpdateConfig(cfg *config.OpenAIConfig) {
|
|
||||||
c.config = cfg
|
|
||||||
if c.client != nil {
|
|
||||||
c.client.UpdateConfig(cfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete 调用OpenAI获取摘要。
|
|
||||||
func (c *OpenAICompletionClient) Complete(ctx context.Context, model string, prompt string, timeout time.Duration) (string, error) {
|
|
||||||
if c.config == nil {
|
|
||||||
return "", errors.New("openai config is required")
|
|
||||||
}
|
|
||||||
if model == "" {
|
|
||||||
return "", errors.New("model name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
reqBody := OpenAIRequest{
|
|
||||||
Model: model,
|
|
||||||
Messages: []ChatMessage{
|
|
||||||
{Role: "user", Content: prompt},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
requestCtx := ctx
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
if timeout > 0 {
|
|
||||||
requestCtx, cancel = context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
var completion OpenAIResponse
|
|
||||||
if c.client == nil {
|
|
||||||
return "", errors.New("openai completion client not initialized")
|
|
||||||
}
|
|
||||||
if err := c.client.ChatCompletion(requestCtx, reqBody, &completion); err != nil {
|
|
||||||
if apiErr, ok := err.(*openai.APIError); ok {
|
|
||||||
return "", fmt.Errorf("openai completion failed, status: %d, body: %s", apiErr.StatusCode, apiErr.Body)
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if completion.Error != nil {
|
|
||||||
return "", errors.New(completion.Error.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(completion.Choices) == 0 || completion.Choices[0].Message.Content == "" {
|
|
||||||
return "", errors.New("empty completion response")
|
|
||||||
}
|
|
||||||
return completion.Choices[0].Message.Content, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/pkoukk/tiktoken-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenCounter 估算文本 token 数(tiktoken;模型未知时回退 cl100k_base)。
|
||||||
|
type TokenCounter interface {
|
||||||
|
Count(model, text string) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tikTokenCounter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
cache map[string]*tiktoken.Tiktoken
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTikTokenCounter 创建基于 tiktoken 的 TokenCounter。
|
||||||
|
func NewTikTokenCounter() TokenCounter {
|
||||||
|
return &tikTokenCounter{cache: make(map[string]*tiktoken.Tiktoken)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tikTokenCounter) encoding(model string) (*tiktoken.Tiktoken, error) {
|
||||||
|
key := model
|
||||||
|
if key == "" {
|
||||||
|
key = "cl100k_base"
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if enc, ok := c.cache[key]; ok {
|
||||||
|
return enc, nil
|
||||||
|
}
|
||||||
|
enc, err := tiktoken.EncodingForModel(key)
|
||||||
|
if err != nil {
|
||||||
|
enc, err = tiktoken.GetEncoding("cl100k_base")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.cache[key] = enc
|
||||||
|
return enc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tikTokenCounter) Count(model, text string) (int, error) {
|
||||||
|
if text == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
enc, err := c.encoding(model)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(enc.Encode(text, nil, nil)), nil
|
||||||
|
}
|
||||||
+5
-4
@@ -113,6 +113,7 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
|
|||||||
// 注册漏洞记录工具
|
// 注册漏洞记录工具
|
||||||
registerVulnerabilityTools(mcpServer, db, log.Logger)
|
registerVulnerabilityTools(mcpServer, db, log.Logger)
|
||||||
registerProjectFactTools(mcpServer, db, cfg, log.Logger)
|
registerProjectFactTools(mcpServer, db, cfg, log.Logger)
|
||||||
|
registerVisionTools(mcpServer, cfg, log.Logger)
|
||||||
|
|
||||||
if cfg.Auth.GeneratedPassword != "" {
|
if cfg.Auth.GeneratedPassword != "" {
|
||||||
config.PrintGeneratedPasswordWarning(cfg.Auth.GeneratedPassword, cfg.Auth.GeneratedPasswordPersisted, cfg.Auth.GeneratedPasswordPersistErr)
|
config.PrintGeneratedPasswordWarning(cfg.Auth.GeneratedPassword, cfg.Auth.GeneratedPasswordPersisted, cfg.Auth.GeneratedPasswordPersistErr)
|
||||||
@@ -418,6 +419,7 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
|
|||||||
vulnerabilityRegistrar := func() error {
|
vulnerabilityRegistrar := func() error {
|
||||||
registerVulnerabilityTools(mcpServer, db, log.Logger)
|
registerVulnerabilityTools(mcpServer, db, log.Logger)
|
||||||
registerProjectFactTools(mcpServer, db, cfg, log.Logger)
|
registerProjectFactTools(mcpServer, db, cfg, log.Logger)
|
||||||
|
registerVisionTools(mcpServer, cfg, log.Logger)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
configHandler.SetVulnerabilityToolRegistrar(vulnerabilityRegistrar)
|
configHandler.SetVulnerabilityToolRegistrar(vulnerabilityRegistrar)
|
||||||
@@ -801,10 +803,6 @@ func setupRoutes(
|
|||||||
protected.POST("/robot/wechat/qrcode/verify", wechatRobotHandler.HandleWechatVerifyCode)
|
protected.POST("/robot/wechat/qrcode/verify", wechatRobotHandler.HandleWechatVerifyCode)
|
||||||
protected.GET("/robot/wechat/status", wechatRobotHandler.HandleWechatStatus)
|
protected.GET("/robot/wechat/status", wechatRobotHandler.HandleWechatStatus)
|
||||||
|
|
||||||
// Agent Loop
|
|
||||||
protected.POST("/agent-loop", agentHandler.AgentLoop)
|
|
||||||
// Agent Loop 流式输出
|
|
||||||
protected.POST("/agent-loop/stream", agentHandler.AgentLoopStream)
|
|
||||||
// Eino ADK 单代理(ChatModelAgent + Runner;不依赖 multi_agent.enabled)
|
// Eino ADK 单代理(ChatModelAgent + Runner;不依赖 multi_agent.enabled)
|
||||||
protected.POST("/eino-agent", agentHandler.EinoSingleAgentLoop)
|
protected.POST("/eino-agent", agentHandler.EinoSingleAgentLoop)
|
||||||
protected.POST("/eino-agent/stream", agentHandler.EinoSingleAgentLoopStream)
|
protected.POST("/eino-agent/stream", agentHandler.EinoSingleAgentLoopStream)
|
||||||
@@ -882,6 +880,7 @@ func setupRoutes(
|
|||||||
protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution)
|
protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution)
|
||||||
protected.DELETE("/monitor/executions", monitorHandler.DeleteExecutions)
|
protected.DELETE("/monitor/executions", monitorHandler.DeleteExecutions)
|
||||||
protected.GET("/monitor/stats", monitorHandler.GetStats)
|
protected.GET("/monitor/stats", monitorHandler.GetStats)
|
||||||
|
protected.GET("/monitor/calls-timeline", monitorHandler.GetCallsTimeline)
|
||||||
protected.GET("/notifications/summary", notificationHandler.GetSummary)
|
protected.GET("/notifications/summary", notificationHandler.GetSummary)
|
||||||
protected.POST("/notifications/read", notificationHandler.MarkRead)
|
protected.POST("/notifications/read", notificationHandler.MarkRead)
|
||||||
|
|
||||||
@@ -892,6 +891,7 @@ func setupRoutes(
|
|||||||
protected.PUT("/config", configHandler.UpdateConfig)
|
protected.PUT("/config", configHandler.UpdateConfig)
|
||||||
protected.POST("/config/apply", configHandler.ApplyConfig)
|
protected.POST("/config/apply", configHandler.ApplyConfig)
|
||||||
protected.POST("/config/test-openai", configHandler.TestOpenAI)
|
protected.POST("/config/test-openai", configHandler.TestOpenAI)
|
||||||
|
protected.POST("/config/test-vision", configHandler.TestVision)
|
||||||
|
|
||||||
// 系统设置 - 终端(执行命令,提高运维效率)
|
// 系统设置 - 终端(执行命令,提高运维效率)
|
||||||
protected.POST("/terminal/run", terminalHandler.RunCommand)
|
protected.POST("/terminal/run", terminalHandler.RunCommand)
|
||||||
@@ -1066,6 +1066,7 @@ func setupRoutes(
|
|||||||
// 漏洞管理
|
// 漏洞管理
|
||||||
protected.GET("/vulnerabilities", vulnerabilityHandler.ListVulnerabilities)
|
protected.GET("/vulnerabilities", vulnerabilityHandler.ListVulnerabilities)
|
||||||
protected.GET("/vulnerabilities/export", vulnerabilityHandler.ExportVulnerabilities)
|
protected.GET("/vulnerabilities/export", vulnerabilityHandler.ExportVulnerabilities)
|
||||||
|
protected.DELETE("/vulnerabilities/batch", vulnerabilityHandler.BatchDeleteVulnerabilities)
|
||||||
protected.GET("/vulnerabilities/filter-options", vulnerabilityHandler.GetVulnerabilityFilterOptions)
|
protected.GET("/vulnerabilities/filter-options", vulnerabilityHandler.GetVulnerabilityFilterOptions)
|
||||||
protected.GET("/vulnerabilities/stats", vulnerabilityHandler.GetVulnerabilityStats)
|
protected.GET("/vulnerabilities/stats", vulnerabilityHandler.GetVulnerabilityStats)
|
||||||
protected.GET("/vulnerabilities/:id", vulnerabilityHandler.GetVulnerability)
|
protected.GET("/vulnerabilities/:id", vulnerabilityHandler.GetVulnerability)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
"cyberstrike-ai/internal/mcp"
|
||||||
|
"cyberstrike-ai/internal/vision"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerVisionTools(mcpServer *mcp.Server, cfg *config.Config, logger *zap.Logger) {
|
||||||
|
vision.RegisterAnalyzeImageTool(mcpServer, cfg, logger)
|
||||||
|
}
|
||||||
+27
-31
@@ -37,6 +37,7 @@ type Config struct {
|
|||||||
AgentsDir string `yaml:"agents_dir,omitempty" json:"agents_dir,omitempty"` // 多代理子 Agent Markdown 定义目录(*.md,YAML front matter)
|
AgentsDir string `yaml:"agents_dir,omitempty" json:"agents_dir,omitempty"` // 多代理子 Agent Markdown 定义目录(*.md,YAML front matter)
|
||||||
MultiAgent MultiAgentConfig `yaml:"multi_agent,omitempty" json:"multi_agent,omitempty"`
|
MultiAgent MultiAgentConfig `yaml:"multi_agent,omitempty" json:"multi_agent,omitempty"`
|
||||||
Project ProjectConfig `yaml:"project,omitempty" json:"project,omitempty"`
|
Project ProjectConfig `yaml:"project,omitempty" json:"project,omitempty"`
|
||||||
|
Vision VisionConfig `yaml:"vision,omitempty" json:"vision,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectConfig 项目黑板(跨对话共享事实)配置。
|
// ProjectConfig 项目黑板(跨对话共享事实)配置。
|
||||||
@@ -64,17 +65,19 @@ func (c ProjectConfig) FactSummaryMaxRunesEffective() int {
|
|||||||
return c.FactSummaryMaxRunes
|
return c.FactSummaryMaxRunes
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiAgentConfig 基于 CloudWeGo Eino adk/prebuilt 的多代理编排(deep | plan_execute | supervisor,与单 Agent /agent-loop 并存)。
|
// MultiAgentConfig 基于 CloudWeGo Eino adk/prebuilt 的多代理编排(deep | plan_execute | supervisor)。
|
||||||
type MultiAgentConfig struct {
|
type MultiAgentConfig struct {
|
||||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||||
RobotDefaultAgentMode string `yaml:"robot_default_agent_mode,omitempty" json:"robot_default_agent_mode,omitempty"` // react | eino_single | deep | plan_execute | supervisor
|
RobotDefaultAgentMode string `yaml:"robot_default_agent_mode,omitempty" json:"robot_default_agent_mode,omitempty"` // eino_single | deep | plan_execute | supervisor
|
||||||
BatchUseMultiAgent bool `yaml:"batch_use_multi_agent" json:"batch_use_multi_agent"` // 为 true 时批量任务队列中每子任务走 Eino 多代理
|
BatchUseMultiAgent bool `yaml:"batch_use_multi_agent" json:"batch_use_multi_agent"` // 为 true 时批量任务队列中每子任务走 Eino 多代理
|
||||||
// Orchestration 已弃用:保留仅兼容旧版 config.yaml;编排由聊天/WebShell 请求体 orchestration 决定,未传时按 deep。
|
// Orchestration 已弃用:保留仅兼容旧版 config.yaml;编排由聊天/WebShell 请求体 orchestration 决定,未传时按 deep。
|
||||||
Orchestration string `yaml:"orchestration,omitempty" json:"orchestration,omitempty"`
|
Orchestration string `yaml:"orchestration,omitempty" json:"orchestration,omitempty"`
|
||||||
MaxIteration int `yaml:"max_iteration" json:"max_iteration"` // 主代理 / 执行器最大推理轮次(Deep、Supervisor、plan_execute 的 Executor)
|
// MaxIteration 已废弃:统一使用 agent.max_iterations(YAML 中保留字段仅为兼容旧配置,运行时不读取)。
|
||||||
|
MaxIteration int `yaml:"max_iteration,omitempty" json:"max_iteration,omitempty"`
|
||||||
// PlanExecuteLoopMaxIterations plan_execute 模式下 execute↔replan 外层循环上限;0 表示用 Eino 默认 10。
|
// PlanExecuteLoopMaxIterations plan_execute 模式下 execute↔replan 外层循环上限;0 表示用 Eino 默认 10。
|
||||||
PlanExecuteLoopMaxIterations int `yaml:"plan_execute_loop_max_iterations,omitempty" json:"plan_execute_loop_max_iterations,omitempty"`
|
PlanExecuteLoopMaxIterations int `yaml:"plan_execute_loop_max_iterations,omitempty" json:"plan_execute_loop_max_iterations,omitempty"`
|
||||||
SubAgentMaxIterations int `yaml:"sub_agent_max_iterations" json:"sub_agent_max_iterations"`
|
// SubAgentMaxIterations 已废弃:子代理与主代理均使用 agent.max_iterations(Markdown max_iterations>0 可覆盖)。
|
||||||
|
SubAgentMaxIterations int `yaml:"sub_agent_max_iterations,omitempty" json:"sub_agent_max_iterations,omitempty"`
|
||||||
WithoutGeneralSubAgent bool `yaml:"without_general_sub_agent" json:"without_general_sub_agent"`
|
WithoutGeneralSubAgent bool `yaml:"without_general_sub_agent" json:"without_general_sub_agent"`
|
||||||
WithoutWriteTodos bool `yaml:"without_write_todos" json:"without_write_todos"`
|
WithoutWriteTodos bool `yaml:"without_write_todos" json:"without_write_todos"`
|
||||||
OrchestratorInstruction string `yaml:"orchestrator_instruction" json:"orchestrator_instruction"`
|
OrchestratorInstruction string `yaml:"orchestrator_instruction" json:"orchestrator_instruction"`
|
||||||
@@ -237,9 +240,6 @@ type MultiAgentEinoMiddlewareConfig struct {
|
|||||||
SummarizationTriggerRatio float64 `yaml:"summarization_trigger_ratio,omitempty" json:"summarization_trigger_ratio,omitempty"`
|
SummarizationTriggerRatio float64 `yaml:"summarization_trigger_ratio,omitempty" json:"summarization_trigger_ratio,omitempty"`
|
||||||
// SummarizationEmitInternalEvents controls middleware internal event emission (default true).
|
// SummarizationEmitInternalEvents controls middleware internal event emission (default true).
|
||||||
SummarizationEmitInternalEvents *bool `yaml:"summarization_emit_internal_events,omitempty" json:"summarization_emit_internal_events,omitempty"`
|
SummarizationEmitInternalEvents *bool `yaml:"summarization_emit_internal_events,omitempty" json:"summarization_emit_internal_events,omitempty"`
|
||||||
// HistoryInputBudgetRatio 已不影响 Eino:从 last_react 轨迹转 ADK 消息时**不再**按 token 比例裁剪(完整注入)。
|
|
||||||
// 字段仍保留,便于旧版 config 不报错;新部署可省略。
|
|
||||||
HistoryInputBudgetRatio float64 `yaml:"history_input_budget_ratio,omitempty" json:"history_input_budget_ratio,omitempty"`
|
|
||||||
// PlanExecuteUserInputBudgetRatio caps planner/replanner/executor userInput prompt budget ratio (default 0.35).
|
// PlanExecuteUserInputBudgetRatio caps planner/replanner/executor userInput prompt budget ratio (default 0.35).
|
||||||
PlanExecuteUserInputBudgetRatio float64 `yaml:"plan_execute_user_input_budget_ratio,omitempty" json:"plan_execute_user_input_budget_ratio,omitempty"`
|
PlanExecuteUserInputBudgetRatio float64 `yaml:"plan_execute_user_input_budget_ratio,omitempty" json:"plan_execute_user_input_budget_ratio,omitempty"`
|
||||||
// PlanExecuteExecutedStepsBudgetRatio caps executed_steps prompt budget ratio (default 0.2).
|
// PlanExecuteExecutedStepsBudgetRatio caps executed_steps prompt budget ratio (default 0.2).
|
||||||
@@ -283,20 +283,6 @@ func (c MultiAgentEinoMiddlewareConfig) SummarizationEmitInternalEventsEffective
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c MultiAgentEinoMiddlewareConfig) HistoryInputBudgetRatioEffective() float64 {
|
|
||||||
v := c.HistoryInputBudgetRatio
|
|
||||||
if v <= 0 {
|
|
||||||
return 0.35
|
|
||||||
}
|
|
||||||
if v < 0.15 {
|
|
||||||
return 0.15
|
|
||||||
}
|
|
||||||
if v > 0.6 {
|
|
||||||
return 0.6
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c MultiAgentEinoMiddlewareConfig) PlanExecuteUserInputBudgetRatioEffective() float64 {
|
func (c MultiAgentEinoMiddlewareConfig) PlanExecuteUserInputBudgetRatioEffective() float64 {
|
||||||
v := c.PlanExecuteUserInputBudgetRatio
|
v := c.PlanExecuteUserInputBudgetRatio
|
||||||
if v <= 0 {
|
if v <= 0 {
|
||||||
@@ -403,16 +389,26 @@ type MultiAgentPublic struct {
|
|||||||
ToolSearchAlwaysVisibleEffectiveTools []string `json:"tool_search_always_visible_effective_tools,omitempty"`
|
ToolSearchAlwaysVisibleEffectiveTools []string `json:"tool_search_always_visible_effective_tools,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeRobotAgentMode 解析机器人默认对话模式(react | eino_single | deep | plan_execute | supervisor);空值视为 react。
|
// NormalizeAgentMode 解析代理模式(eino_single | deep | plan_execute | supervisor);空值默认 eino_single。
|
||||||
func NormalizeRobotAgentMode(ma MultiAgentConfig) string {
|
func NormalizeAgentMode(mode string) string {
|
||||||
s := strings.TrimSpace(strings.ToLower(ma.RobotDefaultAgentMode))
|
s := strings.TrimSpace(strings.ToLower(mode))
|
||||||
if s == "" || s == "single" || s == "react" {
|
switch s {
|
||||||
return "react"
|
case "", "eino_single":
|
||||||
}
|
return "eino_single"
|
||||||
if s == "eino_single" {
|
case "deep":
|
||||||
|
return "deep"
|
||||||
|
case "plan_execute", "plan-execute", "planexecute", "pe":
|
||||||
|
return "plan_execute"
|
||||||
|
case "supervisor", "super", "sv":
|
||||||
|
return "supervisor"
|
||||||
|
default:
|
||||||
return "eino_single"
|
return "eino_single"
|
||||||
}
|
}
|
||||||
return NormalizeMultiAgentOrchestration(s)
|
}
|
||||||
|
|
||||||
|
// NormalizeRobotAgentMode 解析机器人默认对话模式。
|
||||||
|
func NormalizeRobotAgentMode(ma MultiAgentConfig) string {
|
||||||
|
return NormalizeAgentMode(ma.RobotDefaultAgentMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeMultiAgentOrchestration 返回 deep、plan_execute 或 supervisor。
|
// NormalizeMultiAgentOrchestration 返回 deep、plan_execute 或 supervisor。
|
||||||
@@ -532,7 +528,7 @@ type OpenAIConfig struct {
|
|||||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
BaseURL string `yaml:"base_url" json:"base_url"`
|
||||||
Model string `yaml:"model" json:"model"`
|
Model string `yaml:"model" json:"model"`
|
||||||
MaxTotalTokens int `yaml:"max_total_tokens,omitempty" json:"max_total_tokens,omitempty"`
|
MaxTotalTokens int `yaml:"max_total_tokens,omitempty" json:"max_total_tokens,omitempty"`
|
||||||
// Reasoning 控制 Eino ChatModel 的 thinking / reasoning_effort / output_config 等(仅 Eino 路径生效;原生 ReAct 忽略)。
|
// Reasoning 控制 Eino ChatModel 的 thinking / reasoning_effort / output_config 等(Eino 单/多代理路径生效)。
|
||||||
Reasoning OpenAIReasoningConfig `yaml:"reasoning,omitempty" json:"reasoning,omitempty"`
|
Reasoning OpenAIReasoningConfig `yaml:"reasoning,omitempty" json:"reasoning,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// VisionConfig 独立视觉模型与 analyze_image 工具参数;enabled 时注册 MCP 工具 analyze_image。
|
||||||
|
type VisionConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||||
|
APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"`
|
||||||
|
BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"`
|
||||||
|
Model string `yaml:"model,omitempty" json:"model,omitempty"`
|
||||||
|
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
|
||||||
|
TimeoutSeconds int `yaml:"timeout_seconds,omitempty" json:"timeout_seconds,omitempty"`
|
||||||
|
MaxImageBytes int64 `yaml:"max_image_bytes,omitempty" json:"max_image_bytes,omitempty"`
|
||||||
|
MaxDimension int `yaml:"max_dimension,omitempty" json:"max_dimension,omitempty"`
|
||||||
|
JPEGQuality int `yaml:"jpeg_quality,omitempty" json:"jpeg_quality,omitempty"`
|
||||||
|
MaxPayloadBytes int64 `yaml:"max_payload_bytes,omitempty" json:"max_payload_bytes,omitempty"`
|
||||||
|
SkipPreprocessBelowBytes int64 `yaml:"skip_preprocess_below_bytes,omitempty" json:"skip_preprocess_below_bytes,omitempty"` // 0=始终压缩;默认 2MB 且长边已<=max_dimension 时原图直传
|
||||||
|
Detail string `yaml:"detail,omitempty" json:"detail,omitempty"` // low | high | auto
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VisionConfig) TimeoutSecondsEffective() int {
|
||||||
|
if v.TimeoutSeconds <= 0 {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
return v.TimeoutSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VisionConfig) MaxImageBytesEffective() int64 {
|
||||||
|
if v.MaxImageBytes <= 0 {
|
||||||
|
return 5 * 1024 * 1024
|
||||||
|
}
|
||||||
|
return v.MaxImageBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VisionConfig) MaxDimensionEffective() int {
|
||||||
|
if v.MaxDimension <= 0 {
|
||||||
|
return 2048
|
||||||
|
}
|
||||||
|
return v.MaxDimension
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VisionConfig) JPEGQualityEffective() int {
|
||||||
|
if v.JPEGQuality <= 0 || v.JPEGQuality > 100 {
|
||||||
|
return 82
|
||||||
|
}
|
||||||
|
return v.JPEGQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VisionConfig) MaxPayloadBytesEffective() int64 {
|
||||||
|
if v.MaxPayloadBytes <= 0 {
|
||||||
|
return 512 * 1024
|
||||||
|
}
|
||||||
|
return v.MaxPayloadBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipPreprocessBelowBytesEffective 低于该字节数且长边<=max_dimension、且<=max_payload 时可原图直传;0 表示始终压缩。
|
||||||
|
func (v VisionConfig) SkipPreprocessBelowBytesEffective() int64 {
|
||||||
|
if v.SkipPreprocessBelowBytes < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return v.SkipPreprocessBelowBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VisionConfig) DetailEffective() string {
|
||||||
|
d := strings.ToLower(strings.TrimSpace(v.Detail))
|
||||||
|
switch d {
|
||||||
|
case "high", "low", "auto":
|
||||||
|
return d
|
||||||
|
default:
|
||||||
|
return "low"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAICfgEffective 合并主 openai 配置与 vision 覆盖项,供 VL ChatModel 使用。
|
||||||
|
// vision.api_key / base_url / provider 留空或省略时,沿用 main(openai)对应字段;vision.model 必填(由 Ready 校验)。
|
||||||
|
func (v VisionConfig) OpenAICfgEffective(main OpenAIConfig) OpenAIConfig {
|
||||||
|
out := main
|
||||||
|
if k := strings.TrimSpace(v.APIKey); k != "" {
|
||||||
|
out.APIKey = k
|
||||||
|
}
|
||||||
|
if u := strings.TrimSpace(v.BaseURL); u != "" {
|
||||||
|
out.BaseURL = u
|
||||||
|
}
|
||||||
|
if m := strings.TrimSpace(v.Model); m != "" {
|
||||||
|
out.Model = m
|
||||||
|
}
|
||||||
|
if p := strings.TrimSpace(v.Provider); p != "" {
|
||||||
|
out.Provider = p
|
||||||
|
}
|
||||||
|
out.Reasoning.Mode = "off"
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ready 表示已启用且模型名非空。
|
||||||
|
func (v VisionConfig) Ready() bool {
|
||||||
|
return v.Enabled && strings.TrimSpace(v.Model) != ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestVisionConfig_OpenAICfgEffective_fallbackToMain(t *testing.T) {
|
||||||
|
main := OpenAIConfig{
|
||||||
|
APIKey: "main-key",
|
||||||
|
BaseURL: "https://main.example/v1",
|
||||||
|
Model: "main-model",
|
||||||
|
Provider: "openai",
|
||||||
|
}
|
||||||
|
v := VisionConfig{Model: "qwen-vl-max"}
|
||||||
|
out := v.OpenAICfgEffective(main)
|
||||||
|
if out.APIKey != main.APIKey || out.BaseURL != main.BaseURL || out.Provider != main.Provider {
|
||||||
|
t.Fatalf("expected openai fallback, got key=%q url=%q provider=%q", out.APIKey, out.BaseURL, out.Provider)
|
||||||
|
}
|
||||||
|
if out.Model != "qwen-vl-max" {
|
||||||
|
t.Fatalf("model: %s", out.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisionConfig_OpenAICfgEffective(t *testing.T) {
|
||||||
|
main := OpenAIConfig{
|
||||||
|
APIKey: "main-key",
|
||||||
|
BaseURL: "https://main.example/v1",
|
||||||
|
Model: "main-model",
|
||||||
|
Provider: "openai",
|
||||||
|
Reasoning: OpenAIReasoningConfig{Mode: "on"},
|
||||||
|
}
|
||||||
|
v := VisionConfig{
|
||||||
|
Model: "vl-model",
|
||||||
|
APIKey: "vl-key",
|
||||||
|
BaseURL: "https://vl.example/v1",
|
||||||
|
Provider: "claude",
|
||||||
|
}
|
||||||
|
out := v.OpenAICfgEffective(main)
|
||||||
|
if out.APIKey != "vl-key" || out.BaseURL != "https://vl.example/v1" || out.Model != "vl-model" {
|
||||||
|
t.Fatalf("unexpected merge: %+v", out)
|
||||||
|
}
|
||||||
|
if out.Provider != "claude" {
|
||||||
|
t.Fatalf("provider: %s", out.Provider)
|
||||||
|
}
|
||||||
|
if out.Reasoning.Mode != "off" {
|
||||||
|
t.Fatalf("reasoning should be off for vision, got %s", out.Reasoning.Mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisionConfig_Ready(t *testing.T) {
|
||||||
|
if (VisionConfig{Enabled: true, Model: "x"}).Ready() != true {
|
||||||
|
t.Fatal("expected ready")
|
||||||
|
}
|
||||||
|
if (VisionConfig{Enabled: true}).Ready() != false {
|
||||||
|
t.Fatal("expected not ready without model")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -388,7 +388,7 @@ func (db *DB) initTables() error {
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
role TEXT,
|
role TEXT,
|
||||||
agent_mode TEXT NOT NULL DEFAULT 'single',
|
agent_mode TEXT NOT NULL DEFAULT 'eino_single',
|
||||||
schedule_mode TEXT NOT NULL DEFAULT 'manual',
|
schedule_mode TEXT NOT NULL DEFAULT 'manual',
|
||||||
cron_expr TEXT,
|
cron_expr TEXT,
|
||||||
next_run_at DATETIME,
|
next_run_at DATETIME,
|
||||||
@@ -984,14 +984,14 @@ func (db *DB) migrateBatchTaskQueuesTable() error {
|
|||||||
var agentModeCount int
|
var agentModeCount int
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('batch_task_queues') WHERE name='agent_mode'").Scan(&agentModeCount)
|
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('batch_task_queues') WHERE name='agent_mode'").Scan(&agentModeCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, addErr := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN agent_mode TEXT NOT NULL DEFAULT 'single'"); addErr != nil {
|
if _, addErr := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN agent_mode TEXT NOT NULL DEFAULT 'eino_single'"); addErr != nil {
|
||||||
errMsg := strings.ToLower(addErr.Error())
|
errMsg := strings.ToLower(addErr.Error())
|
||||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||||
db.logger.Warn("添加agent_mode字段失败", zap.Error(addErr))
|
db.logger.Warn("添加agent_mode字段失败", zap.Error(addErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if agentModeCount == 0 {
|
} else if agentModeCount == 0 {
|
||||||
if _, err := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN agent_mode TEXT NOT NULL DEFAULT 'single'"); err != nil {
|
if _, err := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN agent_mode TEXT NOT NULL DEFAULT 'eino_single'"); err != nil {
|
||||||
db.logger.Warn("添加agent_mode字段失败", zap.Error(err))
|
db.logger.Warn("添加agent_mode字段失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,6 +493,63 @@ func (db *DB) UpdateToolStats(toolName string, totalCalls, successCalls, failedC
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallsTimelineBucket 调用趋势时间桶
|
||||||
|
type CallsTimelineBucket struct {
|
||||||
|
BucketTime time.Time
|
||||||
|
Total int
|
||||||
|
Failed int
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCallsTimeline 按时间范围加载调用趋势(since 起至今,含边界)
|
||||||
|
func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTimelineBucket, error) {
|
||||||
|
var bucketExpr string
|
||||||
|
if dailyBuckets {
|
||||||
|
bucketExpr = `strftime('%Y-%m-%d 00:00:00', start_time)`
|
||||||
|
} else {
|
||||||
|
bucketExpr = `strftime('%Y-%m-%d %H:00:00', start_time)`
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT ` + bucketExpr + ` AS bucket,
|
||||||
|
COUNT(*) AS total,
|
||||||
|
SUM(CASE WHEN status IN ('failed', 'cancelled') THEN 1 ELSE 0 END) AS failed
|
||||||
|
FROM tool_executions
|
||||||
|
WHERE start_time >= ?
|
||||||
|
GROUP BY bucket
|
||||||
|
ORDER BY bucket ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query, since)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var buckets []CallsTimelineBucket
|
||||||
|
for rows.Next() {
|
||||||
|
var bucketStr string
|
||||||
|
var total, failed int
|
||||||
|
if err := rows.Scan(&bucketStr, &total, &failed); err != nil {
|
||||||
|
db.logger.Warn("加载调用趋势失败", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, parseErr := time.ParseInLocation("2006-01-02 15:04:05", bucketStr, time.Local)
|
||||||
|
if parseErr != nil {
|
||||||
|
t, parseErr = time.Parse("2006-01-02 15:04:05", bucketStr)
|
||||||
|
if parseErr != nil {
|
||||||
|
db.logger.Warn("解析趋势时间桶失败", zap.String("bucket", bucketStr), zap.Error(parseErr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buckets = append(buckets, CallsTimelineBucket{
|
||||||
|
BucketTime: t,
|
||||||
|
Total: total,
|
||||||
|
Failed: failed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return buckets, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DecreaseToolStats 减少工具统计信息(用于删除执行记录时)
|
// DecreaseToolStats 减少工具统计信息(用于删除执行记录时)
|
||||||
// 如果统计信息变为0,则删除该统计记录
|
// 如果统计信息变为0,则删除该统计记录
|
||||||
func (db *DB) DecreaseToolStats(toolName string, totalCalls, successCalls, failedCalls int) error {
|
func (db *DB) DecreaseToolStats(toolName string, totalCalls, successCalls, failedCalls int) error {
|
||||||
|
|||||||
@@ -263,6 +263,39 @@ func (db *DB) UpdateVulnerability(id string, vuln *Vulnerability) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteVulnerabilitiesByFilter 按筛选条件批量删除漏洞,返回实际删除条数
|
||||||
|
func (db *DB) DeleteVulnerabilitiesByFilter(filter VulnerabilityListFilter) (int64, error) {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("开启事务失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
where := "WHERE 1=1"
|
||||||
|
args := []interface{}{}
|
||||||
|
where, args = filter.appendWhere(where, args)
|
||||||
|
|
||||||
|
clearQuery := `UPDATE project_facts SET related_vulnerability_id = NULL
|
||||||
|
WHERE related_vulnerability_id IN (SELECT id FROM vulnerabilities ` + where + `)`
|
||||||
|
if _, err := tx.Exec(clearQuery, args...); err != nil {
|
||||||
|
return 0, fmt.Errorf("清理事实漏洞关联失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteQuery := `DELETE FROM vulnerabilities ` + where
|
||||||
|
result, err := tx.Exec(deleteQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("批量删除漏洞失败: %w", err)
|
||||||
|
}
|
||||||
|
deleted, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("获取删除条数失败: %w", err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return 0, fmt.Errorf("提交事务失败: %w", err)
|
||||||
|
}
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteVulnerability 删除漏洞
|
// DeleteVulnerability 删除漏洞
|
||||||
func (db *DB) DeleteVulnerability(id string) error {
|
func (db *DB) DeleteVulnerability(id string) error {
|
||||||
tx, err := db.Begin()
|
tx, err := db.Begin()
|
||||||
|
|||||||
+108
-834
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCreateProgressCallback_ConcurrentToolEvents 回归 issue #142:并行 tool 回调不得 concurrent map panic。
|
||||||
|
func TestCreateProgressCallback_ConcurrentToolEvents(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
h := &AgentHandler{
|
||||||
|
logger: logger,
|
||||||
|
config: &config.Config{},
|
||||||
|
}
|
||||||
|
cb := h.createProgressCallback(context.Background(), nil, "conv-race-test", "", nil)
|
||||||
|
|
||||||
|
const workers = 64
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(workers * 2)
|
||||||
|
for i := 0; i < workers; i++ {
|
||||||
|
i := i
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
toolCallID := fmt.Sprintf("tc-%d", i)
|
||||||
|
cb("tool_call", "calling skill", map[string]interface{}{
|
||||||
|
"toolCallId": toolCallID,
|
||||||
|
"toolName": "skill",
|
||||||
|
"argumentsObj": map[string]interface{}{"skill_name": "demo-skill"},
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
toolCallID := fmt.Sprintf("tc-%d", i)
|
||||||
|
cb("tool_result", "skill done", map[string]interface{}{
|
||||||
|
"toolCallId": toolCallID,
|
||||||
|
"toolName": "skill",
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
"cyberstrike-ai/internal/database"
|
"cyberstrike-ai/internal/database"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -128,7 +129,7 @@ func (m *BatchTaskManager) CreateBatchQueue(
|
|||||||
Title: title,
|
Title: title,
|
||||||
Role: role,
|
Role: role,
|
||||||
ProjectID: strings.TrimSpace(projectID),
|
ProjectID: strings.TrimSpace(projectID),
|
||||||
AgentMode: normalizeBatchQueueAgentMode(agentMode),
|
AgentMode: config.NormalizeAgentMode(agentMode),
|
||||||
ScheduleMode: normalizeBatchQueueScheduleMode(scheduleMode),
|
ScheduleMode: normalizeBatchQueueScheduleMode(scheduleMode),
|
||||||
CronExpr: strings.TrimSpace(cronExpr),
|
CronExpr: strings.TrimSpace(cronExpr),
|
||||||
NextRunAt: nextRunAt,
|
NextRunAt: nextRunAt,
|
||||||
@@ -225,7 +226,7 @@ func (m *BatchTaskManager) loadQueueFromDB(queueID string) *BatchTaskQueue {
|
|||||||
|
|
||||||
queue := &BatchTaskQueue{
|
queue := &BatchTaskQueue{
|
||||||
ID: queueRow.ID,
|
ID: queueRow.ID,
|
||||||
AgentMode: "single",
|
AgentMode: "eino_single",
|
||||||
ScheduleMode: "manual",
|
ScheduleMode: "manual",
|
||||||
Status: queueRow.Status,
|
Status: queueRow.Status,
|
||||||
CreatedAt: queueRow.CreatedAt,
|
CreatedAt: queueRow.CreatedAt,
|
||||||
@@ -240,7 +241,7 @@ func (m *BatchTaskManager) loadQueueFromDB(queueID string) *BatchTaskQueue {
|
|||||||
queue.Role = queueRow.Role.String
|
queue.Role = queueRow.Role.String
|
||||||
}
|
}
|
||||||
if queueRow.AgentMode.Valid {
|
if queueRow.AgentMode.Valid {
|
||||||
queue.AgentMode = normalizeBatchQueueAgentMode(queueRow.AgentMode.String)
|
queue.AgentMode = config.NormalizeAgentMode(queueRow.AgentMode.String)
|
||||||
}
|
}
|
||||||
if queueRow.ScheduleMode.Valid {
|
if queueRow.ScheduleMode.Valid {
|
||||||
queue.ScheduleMode = normalizeBatchQueueScheduleMode(queueRow.ScheduleMode.String)
|
queue.ScheduleMode = normalizeBatchQueueScheduleMode(queueRow.ScheduleMode.String)
|
||||||
@@ -464,7 +465,7 @@ func (m *BatchTaskManager) LoadFromDB() error {
|
|||||||
|
|
||||||
queue := &BatchTaskQueue{
|
queue := &BatchTaskQueue{
|
||||||
ID: queueRow.ID,
|
ID: queueRow.ID,
|
||||||
AgentMode: "single",
|
AgentMode: "eino_single",
|
||||||
ScheduleMode: "manual",
|
ScheduleMode: "manual",
|
||||||
Status: queueRow.Status,
|
Status: queueRow.Status,
|
||||||
CreatedAt: queueRow.CreatedAt,
|
CreatedAt: queueRow.CreatedAt,
|
||||||
@@ -479,7 +480,7 @@ func (m *BatchTaskManager) LoadFromDB() error {
|
|||||||
queue.Role = queueRow.Role.String
|
queue.Role = queueRow.Role.String
|
||||||
}
|
}
|
||||||
if queueRow.AgentMode.Valid {
|
if queueRow.AgentMode.Valid {
|
||||||
queue.AgentMode = normalizeBatchQueueAgentMode(queueRow.AgentMode.String)
|
queue.AgentMode = config.NormalizeAgentMode(queueRow.AgentMode.String)
|
||||||
}
|
}
|
||||||
if queueRow.ScheduleMode.Valid {
|
if queueRow.ScheduleMode.Valid {
|
||||||
queue.ScheduleMode = normalizeBatchQueueScheduleMode(queueRow.ScheduleMode.String)
|
queue.ScheduleMode = normalizeBatchQueueScheduleMode(queueRow.ScheduleMode.String)
|
||||||
@@ -669,7 +670,7 @@ func (m *BatchTaskManager) UpdateQueueMetadata(queueID, title, role, agentMode s
|
|||||||
|
|
||||||
// 如果未传 agentMode,保留原值
|
// 如果未传 agentMode,保留原值
|
||||||
if strings.TrimSpace(agentMode) != "" {
|
if strings.TrimSpace(agentMode) != "" {
|
||||||
agentMode = normalizeBatchQueueAgentMode(agentMode)
|
agentMode = config.NormalizeAgentMode(agentMode)
|
||||||
} else {
|
} else {
|
||||||
agentMode = queue.AgentMode
|
agentMode = queue.AgentMode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
"cyberstrike-ai/internal/mcp"
|
"cyberstrike-ai/internal/mcp"
|
||||||
"cyberstrike-ai/internal/mcp/builtin"
|
"cyberstrike-ai/internal/mcp/builtin"
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
|
|||||||
|
|
||||||
【何时用】用户明确要批量排队执行、Cron 周期跑同一批指令、或需要与任务管理页面对齐时调用。需要即时追问、强依赖当前对话上下文的分析/编码,应在本对话内直接完成,不要为了”委派”而创建队列。
|
【何时用】用户明确要批量排队执行、Cron 周期跑同一批指令、或需要与任务管理页面对齐时调用。需要即时追问、强依赖当前对话上下文的分析/编码,应在本对话内直接完成,不要为了”委派”而创建队列。
|
||||||
|
|
||||||
【参数】tasks(字符串数组)或 tasks_text(多行,每行一条)二选一;每项是一条将来由系统按队列顺序执行的指令文案。agent_mode:single(原生 ReAct,默认)、eino_single(Eino ADK 单代理)、deep / plan_execute / supervisor(需系统启用多代理);兼容旧值 multi(视为 deep)。非”把主对话拆给子代理”。schedule_mode:manual(默认)或 cron;cron 须填 cron_expr(5 段,如 “0 */6 * * *”)。
|
【参数】tasks(字符串数组)或 tasks_text(多行,每行一条)二选一;每项是一条将来由系统按队列顺序执行的指令文案。agent_mode:eino_single(Eino ADK 单代理,默认)、deep / plan_execute / supervisor(需系统启用多代理)。非”把主对话拆给子代理”。schedule_mode:manual(默认)或 cron;cron 须填 cron_expr(5 段,如 “0 */6 * * *”)。
|
||||||
|
|
||||||
【执行】默认创建后为 pending,不自动跑。execute_now=true 可创建后立即跑;否则之后调用 batch_task_start。Cron 自动下一轮需 schedule_enabled 为 true(可用 batch_task_schedule_enabled)。`,
|
【执行】默认创建后为 pending,不自动跑。execute_now=true 可创建后立即跑;否则之后调用 batch_task_start。Cron 自动下一轮需 schedule_enabled 为 true(可用 batch_task_schedule_enabled)。`,
|
||||||
ShortDescription: "任务管理:创建批量任务队列(登记多条指令,可选立即或 Cron)",
|
ShortDescription: "任务管理:创建批量任务队列(登记多条指令,可选立即或 Cron)",
|
||||||
@@ -160,8 +161,8 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
|
|||||||
},
|
},
|
||||||
"agent_mode": map[string]interface{}{
|
"agent_mode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "执行模式:single(原生 ReAct)、eino_single(Eino ADK)、deep/plan_execute/supervisor(Eino 编排,需启用多代理);multi 兼容为 deep",
|
"description": "执行模式:eino_single(Eino ADK,默认)、deep/plan_execute/supervisor(Eino 编排,需启用多代理)",
|
||||||
"enum": []string{"single", "eino_single", "deep", "plan_execute", "supervisor", "multi"},
|
"enum": []string{"eino_single", "deep", "plan_execute", "supervisor"},
|
||||||
},
|
},
|
||||||
"schedule_mode": map[string]interface{}{
|
"schedule_mode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -189,7 +190,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
|
|||||||
}
|
}
|
||||||
title := mcpArgString(args, "title")
|
title := mcpArgString(args, "title")
|
||||||
role := mcpArgString(args, "role")
|
role := mcpArgString(args, "role")
|
||||||
agentMode := normalizeBatchQueueAgentMode(mcpArgString(args, "agent_mode"))
|
agentMode := config.NormalizeAgentMode(mcpArgString(args, "agent_mode"))
|
||||||
scheduleMode := normalizeBatchQueueScheduleMode(mcpArgString(args, "schedule_mode"))
|
scheduleMode := normalizeBatchQueueScheduleMode(mcpArgString(args, "schedule_mode"))
|
||||||
cronExpr := strings.TrimSpace(mcpArgString(args, "cron_expr"))
|
cronExpr := strings.TrimSpace(mcpArgString(args, "cron_expr"))
|
||||||
var nextRunAt *time.Time
|
var nextRunAt *time.Time
|
||||||
@@ -393,8 +394,8 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
|
|||||||
},
|
},
|
||||||
"agent_mode": map[string]interface{}{
|
"agent_mode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "代理模式:single、eino_single、deep、plan_execute、supervisor;multi 视为 deep",
|
"description": "代理模式:eino_single、deep、plan_execute、supervisor",
|
||||||
"enum": []string{"single", "eino_single", "deep", "plan_execute", "supervisor", "multi"},
|
"enum": []string{"eino_single", "deep", "plan_execute", "supervisor"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"queue_id"},
|
"required": []string{"queue_id"},
|
||||||
|
|||||||
+145
-1
@@ -237,6 +237,7 @@ func (h *ConfigHandler) ApplyWechatRobotBinding(wc config.RobotWechatConfig) err
|
|||||||
// GetConfigResponse 获取配置响应
|
// GetConfigResponse 获取配置响应
|
||||||
type GetConfigResponse struct {
|
type GetConfigResponse struct {
|
||||||
OpenAI config.OpenAIConfig `json:"openai"`
|
OpenAI config.OpenAIConfig `json:"openai"`
|
||||||
|
Vision config.VisionConfig `json:"vision"`
|
||||||
FOFA config.FofaConfig `json:"fofa"`
|
FOFA config.FofaConfig `json:"fofa"`
|
||||||
MCP config.MCPConfig `json:"mcp"`
|
MCP config.MCPConfig `json:"mcp"`
|
||||||
Tools []ToolConfigInfo `json:"tools"`
|
Tools []ToolConfigInfo `json:"tools"`
|
||||||
@@ -333,6 +334,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, GetConfigResponse{
|
c.JSON(http.StatusOK, GetConfigResponse{
|
||||||
OpenAI: h.config.OpenAI,
|
OpenAI: h.config.OpenAI,
|
||||||
|
Vision: h.config.Vision,
|
||||||
FOFA: h.config.FOFA,
|
FOFA: h.config.FOFA,
|
||||||
MCP: h.config.MCP,
|
MCP: h.config.MCP,
|
||||||
Tools: tools,
|
Tools: tools,
|
||||||
@@ -638,6 +640,7 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
|||||||
// UpdateConfigRequest 更新配置请求
|
// UpdateConfigRequest 更新配置请求
|
||||||
type UpdateConfigRequest struct {
|
type UpdateConfigRequest struct {
|
||||||
OpenAI *config.OpenAIConfig `json:"openai,omitempty"`
|
OpenAI *config.OpenAIConfig `json:"openai,omitempty"`
|
||||||
|
Vision *config.VisionConfig `json:"vision,omitempty"`
|
||||||
FOFA *config.FofaConfig `json:"fofa,omitempty"`
|
FOFA *config.FofaConfig `json:"fofa,omitempty"`
|
||||||
MCP *config.MCPConfig `json:"mcp,omitempty"`
|
MCP *config.MCPConfig `json:"mcp,omitempty"`
|
||||||
Tools []ToolEnableStatus `json:"tools,omitempty"`
|
Tools []ToolEnableStatus `json:"tools,omitempty"`
|
||||||
@@ -707,6 +710,14 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Vision != nil {
|
||||||
|
h.config.Vision = *req.Vision
|
||||||
|
h.logger.Info("更新 Vision 配置",
|
||||||
|
zap.Bool("enabled", h.config.Vision.Enabled),
|
||||||
|
zap.String("model", h.config.Vision.Model),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 更新FOFA配置
|
// 更新FOFA配置
|
||||||
if req.FOFA != nil {
|
if req.FOFA != nil {
|
||||||
h.config.FOFA = *req.FOFA
|
h.config.FOFA = *req.FOFA
|
||||||
@@ -783,7 +794,7 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
|
|||||||
if mode := strings.TrimSpace(req.MultiAgent.RobotDefaultAgentMode); mode != "" {
|
if mode := strings.TrimSpace(req.MultiAgent.RobotDefaultAgentMode); mode != "" {
|
||||||
h.config.MultiAgent.RobotDefaultAgentMode = mode
|
h.config.MultiAgent.RobotDefaultAgentMode = mode
|
||||||
} else {
|
} else {
|
||||||
h.config.MultiAgent.RobotDefaultAgentMode = "react"
|
h.config.MultiAgent.RobotDefaultAgentMode = "eino_single"
|
||||||
}
|
}
|
||||||
if req.MultiAgent.PlanExecuteLoopMaxIterations != nil {
|
if req.MultiAgent.PlanExecuteLoopMaxIterations != nil {
|
||||||
h.config.MultiAgent.PlanExecuteLoopMaxIterations = *req.MultiAgent.PlanExecuteLoopMaxIterations
|
h.config.MultiAgent.PlanExecuteLoopMaxIterations = *req.MultiAgent.PlanExecuteLoopMaxIterations
|
||||||
@@ -1031,6 +1042,99 @@ func (h *ConfigHandler) TestOpenAI(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVisionRequest 测试 Vision 模型连接;vision.api_key/base_url 留空时可传 openai 段作回退。
|
||||||
|
type TestVisionRequest struct {
|
||||||
|
Vision config.VisionConfig `json:"vision"`
|
||||||
|
OpenAI config.OpenAIConfig `json:"openai,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVision 测试视觉模型 API 连接(最小 chat completion)。
|
||||||
|
func (h *ConfigHandler) TestVision(c *gin.Context) {
|
||||||
|
var req TestVisionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oa := req.Vision.OpenAICfgEffective(req.OpenAI)
|
||||||
|
if strings.TrimSpace(oa.APIKey) == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "API Key 不能为空(可填写 vision.api_key 或 openai.api_key)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(oa.Model) == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "视觉模型不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := strings.TrimSuffix(strings.TrimSpace(oa.BaseURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") {
|
||||||
|
baseURL = "https://api.anthropic.com"
|
||||||
|
} else {
|
||||||
|
baseURL = "https://api.openai.com/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"model": oa.Model,
|
||||||
|
"messages": []map[string]string{
|
||||||
|
{"role": "user", "content": "Hi"},
|
||||||
|
},
|
||||||
|
"max_completion_tokens": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpCfg := &config.OpenAIConfig{
|
||||||
|
Provider: oa.Provider,
|
||||||
|
BaseURL: baseURL,
|
||||||
|
APIKey: strings.TrimSpace(oa.APIKey),
|
||||||
|
Model: oa.Model,
|
||||||
|
}
|
||||||
|
client := openai.NewClient(tmpCfg, nil, h.logger)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
var chatResp struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Choices []struct {
|
||||||
|
Message struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"message"`
|
||||||
|
} `json:"choices"`
|
||||||
|
}
|
||||||
|
err := client.ChatCompletion(ctx, payload, &chatResp)
|
||||||
|
latency := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if apiErr, ok := err.(*openai.APIError); ok {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"error": fmt.Sprintf("API 返回错误 (HTTP %d): %s", apiErr.StatusCode, apiErr.Body),
|
||||||
|
"status_code": apiErr.StatusCode,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"error": "连接失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(chatResp.Choices) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"error": "API 响应缺少 choices 字段,请检查 Base URL 与视觉模型名称",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"model": chatResp.Model,
|
||||||
|
"latency_ms": latency.Milliseconds(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ApplyConfig 应用配置(重新加载并重启相关服务)
|
// ApplyConfig 应用配置(重新加载并重启相关服务)
|
||||||
func (h *ConfigHandler) ApplyConfig(c *gin.Context) {
|
func (h *ConfigHandler) ApplyConfig(c *gin.Context) {
|
||||||
// 先检查是否需要动态初始化知识库(在锁外执行,避免阻塞其他请求)
|
// 先检查是否需要动态初始化知识库(在锁外执行,避免阻塞其他请求)
|
||||||
@@ -1286,6 +1390,7 @@ func (h *ConfigHandler) saveConfig() error {
|
|||||||
updateAgentConfig(root, h.config.Agent)
|
updateAgentConfig(root, h.config.Agent)
|
||||||
updateMCPConfig(root, h.config.MCP)
|
updateMCPConfig(root, h.config.MCP)
|
||||||
updateOpenAIConfig(root, h.config.OpenAI)
|
updateOpenAIConfig(root, h.config.OpenAI)
|
||||||
|
updateVisionConfig(root, h.config.Vision)
|
||||||
updateFOFAConfig(root, h.config.FOFA)
|
updateFOFAConfig(root, h.config.FOFA)
|
||||||
updateKnowledgeConfig(root, h.config.Knowledge)
|
updateKnowledgeConfig(root, h.config.Knowledge)
|
||||||
updateC2Config(root, h.config.C2)
|
updateC2Config(root, h.config.C2)
|
||||||
@@ -1406,6 +1511,45 @@ func updateMCPConfig(doc *yaml.Node, cfg config.MCPConfig) {
|
|||||||
setIntInMap(mcpNode, "port", cfg.Port)
|
setIntInMap(mcpNode, "port", cfg.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateVisionConfig(doc *yaml.Node, cfg config.VisionConfig) {
|
||||||
|
root := doc.Content[0]
|
||||||
|
visionNode := ensureMap(root, "vision")
|
||||||
|
setBoolInMap(visionNode, "enabled", cfg.Enabled)
|
||||||
|
if strings.TrimSpace(cfg.APIKey) != "" {
|
||||||
|
setStringInMap(visionNode, "api_key", cfg.APIKey)
|
||||||
|
} else {
|
||||||
|
setStringInMap(visionNode, "api_key", "")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.BaseURL) != "" {
|
||||||
|
setStringInMap(visionNode, "base_url", cfg.BaseURL)
|
||||||
|
} else {
|
||||||
|
setStringInMap(visionNode, "base_url", "")
|
||||||
|
}
|
||||||
|
setStringInMap(visionNode, "model", cfg.Model)
|
||||||
|
if strings.TrimSpace(cfg.Provider) != "" {
|
||||||
|
setStringInMap(visionNode, "provider", cfg.Provider)
|
||||||
|
}
|
||||||
|
if cfg.TimeoutSeconds > 0 {
|
||||||
|
setIntInMap(visionNode, "timeout_seconds", cfg.TimeoutSeconds)
|
||||||
|
}
|
||||||
|
if cfg.MaxImageBytes > 0 {
|
||||||
|
setIntInMap(visionNode, "max_image_bytes", int(cfg.MaxImageBytes))
|
||||||
|
}
|
||||||
|
if cfg.MaxDimension > 0 {
|
||||||
|
setIntInMap(visionNode, "max_dimension", cfg.MaxDimension)
|
||||||
|
}
|
||||||
|
if cfg.JPEGQuality > 0 {
|
||||||
|
setIntInMap(visionNode, "jpeg_quality", cfg.JPEGQuality)
|
||||||
|
}
|
||||||
|
if cfg.MaxPayloadBytes > 0 {
|
||||||
|
setIntInMap(visionNode, "max_payload_bytes", int(cfg.MaxPayloadBytes))
|
||||||
|
}
|
||||||
|
setIntInMap(visionNode, "skip_preprocess_below_bytes", int(cfg.SkipPreprocessBelowBytes))
|
||||||
|
if strings.TrimSpace(cfg.Detail) != "" {
|
||||||
|
setStringInMap(visionNode, "detail", cfg.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) {
|
func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) {
|
||||||
root := doc.Content[0]
|
root := doc.Content[0]
|
||||||
openaiNode := ensureMap(root, "openai")
|
openaiNode := ensureMap(root, "openai")
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cyberstrike-ai/internal/agent"
|
|
||||||
"cyberstrike-ai/internal/database"
|
"cyberstrike-ai/internal/database"
|
||||||
"cyberstrike-ai/internal/multiagent"
|
"cyberstrike-ai/internal/multiagent"
|
||||||
|
|
||||||
@@ -691,35 +690,6 @@ func (h *AgentHandler) interceptHITLForEinoTool(runCtx context.Context, cancelRu
|
|||||||
return arguments, nil
|
return arguments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AgentHandler) interceptHITLForReactTool(runCtx context.Context, cancelRun context.CancelCauseFunc, conversationID, assistantMessageID string, sendEventFunc func(eventType, message string, data interface{}), toolName string, arguments map[string]interface{}, toolCallID string) (map[string]interface{}, error) {
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"toolName": toolName,
|
|
||||||
"argumentsObj": arguments,
|
|
||||||
"toolCallId": toolCallID,
|
|
||||||
"source": "react_pre_exec",
|
|
||||||
}
|
|
||||||
d, err := h.waitHITLApproval(runCtx, cancelRun, conversationID, assistantMessageID, toolName, toolCallID, payload, sendEventFunc)
|
|
||||||
if err != nil || d == nil {
|
|
||||||
return arguments, err
|
|
||||||
}
|
|
||||||
if d.Decision == "reject" {
|
|
||||||
comment := strings.TrimSpace(d.Comment)
|
|
||||||
if comment == "" {
|
|
||||||
comment = "no extra feedback"
|
|
||||||
}
|
|
||||||
return arguments, errors.New("human rejected this tool call; feedback: " + comment)
|
|
||||||
}
|
|
||||||
if len(d.EditedArguments) > 0 {
|
|
||||||
return d.EditedArguments, nil
|
|
||||||
}
|
|
||||||
return arguments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AgentHandler) injectReactHITLInterceptor(ctx context.Context, cancelRun context.CancelCauseFunc, conversationID, assistantMessageID string, sendEventFunc func(eventType, message string, data interface{})) context.Context {
|
|
||||||
return agent.WithToolCallInterceptor(ctx, func(c context.Context, toolName string, args map[string]interface{}, toolCallID string) (map[string]interface{}, error) {
|
|
||||||
return h.interceptHITLForReactTool(c, cancelRun, conversationID, assistantMessageID, sendEventFunc, toolName, args, toolCallID)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type hitlConfigReq struct {
|
type hitlConfigReq struct {
|
||||||
ConversationID string `json:"conversationId" binding:"required"`
|
ConversationID string `json:"conversationId" binding:"required"`
|
||||||
|
|||||||
@@ -327,6 +327,124 @@ func (h *MonitorHandler) GetStats(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, stats)
|
c.JSON(http.StatusOK, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallsTimelinePoint 调用趋势数据点
|
||||||
|
type CallsTimelinePoint struct {
|
||||||
|
T time.Time `json:"t"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallsTimelineSummary 调用趋势汇总
|
||||||
|
type CallsTimelineSummary struct {
|
||||||
|
TotalCalls int `json:"totalCalls"`
|
||||||
|
Peak int `json:"peak"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallsTimelineResponse 调用趋势响应
|
||||||
|
type CallsTimelineResponse struct {
|
||||||
|
Range string `json:"range"`
|
||||||
|
Points []CallsTimelinePoint `json:"points"`
|
||||||
|
Summary CallsTimelineSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type callsTimelineConfig struct {
|
||||||
|
rangeKey string
|
||||||
|
duration time.Duration
|
||||||
|
bucketSize time.Duration
|
||||||
|
dailyBuckets bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCallsTimelineRange(raw string) (callsTimelineConfig, bool) {
|
||||||
|
switch strings.TrimSpace(raw) {
|
||||||
|
case "24h":
|
||||||
|
return callsTimelineConfig{rangeKey: "24h", duration: 24 * time.Hour, bucketSize: time.Hour, dailyBuckets: false}, true
|
||||||
|
case "30d":
|
||||||
|
return callsTimelineConfig{rangeKey: "30d", duration: 30 * 24 * time.Hour, bucketSize: 24 * time.Hour, dailyBuckets: true}, true
|
||||||
|
default:
|
||||||
|
return callsTimelineConfig{rangeKey: "7d", duration: 7 * 24 * time.Hour, bucketSize: time.Hour, dailyBuckets: false}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateToBucket(t time.Time, bucketSize time.Duration, dailyBuckets bool) time.Time {
|
||||||
|
if dailyBuckets {
|
||||||
|
y, m, d := t.Date()
|
||||||
|
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
|
||||||
|
}
|
||||||
|
return t.Truncate(bucketSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCallsTimelinePoints(cfg callsTimelineConfig, buckets map[time.Time]struct{ total, failed int }) []CallsTimelinePoint {
|
||||||
|
now := time.Now()
|
||||||
|
start := truncateToBucket(now.Add(-cfg.duration), cfg.bucketSize, cfg.dailyBuckets)
|
||||||
|
end := truncateToBucket(now, cfg.bucketSize, cfg.dailyBuckets)
|
||||||
|
|
||||||
|
points := make([]CallsTimelinePoint, 0)
|
||||||
|
for current := start; !current.After(end); current = current.Add(cfg.bucketSize) {
|
||||||
|
val := buckets[current]
|
||||||
|
points = append(points, CallsTimelinePoint{
|
||||||
|
T: current,
|
||||||
|
Total: val.total,
|
||||||
|
Failed: val.failed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MonitorHandler) loadCallsTimeline(cfg callsTimelineConfig) []CallsTimelinePoint {
|
||||||
|
since := time.Now().Add(-cfg.duration)
|
||||||
|
bucketMap := make(map[time.Time]struct{ total, failed int })
|
||||||
|
|
||||||
|
if h.db != nil {
|
||||||
|
dbBuckets, err := h.db.LoadCallsTimeline(since, cfg.dailyBuckets)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("从数据库加载调用趋势失败,回退到内存数据", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
for _, b := range dbBuckets {
|
||||||
|
key := truncateToBucket(b.BucketTime, cfg.bucketSize, cfg.dailyBuckets)
|
||||||
|
entry := bucketMap[key]
|
||||||
|
entry.total += b.Total
|
||||||
|
entry.failed += b.Failed
|
||||||
|
bucketMap[key] = entry
|
||||||
|
}
|
||||||
|
return buildCallsTimelinePoints(cfg, bucketMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exec := range h.mcpServer.GetAllExecutions() {
|
||||||
|
if exec == nil || exec.StartTime.Before(since) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := truncateToBucket(exec.StartTime, cfg.bucketSize, cfg.dailyBuckets)
|
||||||
|
entry := bucketMap[key]
|
||||||
|
entry.total++
|
||||||
|
if exec.Status == "failed" || exec.Status == "cancelled" {
|
||||||
|
entry.failed++
|
||||||
|
}
|
||||||
|
bucketMap[key] = entry
|
||||||
|
}
|
||||||
|
return buildCallsTimelinePoints(cfg, bucketMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCallsTimeline 获取 MCP 工具调用趋势
|
||||||
|
func (h *MonitorHandler) GetCallsTimeline(c *gin.Context) {
|
||||||
|
cfg, _ := parseCallsTimelineRange(c.Query("range"))
|
||||||
|
points := h.loadCallsTimeline(cfg)
|
||||||
|
|
||||||
|
summary := CallsTimelineSummary{}
|
||||||
|
for _, p := range points {
|
||||||
|
summary.TotalCalls += p.Total
|
||||||
|
if p.Total > summary.Peak {
|
||||||
|
summary.Peak = p.Total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, CallsTimelineResponse{
|
||||||
|
Range: cfg.rangeKey,
|
||||||
|
Points: points,
|
||||||
|
Summary: summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteExecution 删除执行记录
|
// DeleteExecution 删除执行记录
|
||||||
func (h *MonitorHandler) DeleteExecution(c *gin.Context) {
|
func (h *MonitorHandler) DeleteExecution(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
|||||||
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiAgentLoop Eino DeepAgent 非流式对话(与 POST /api/agent-loop 对齐,需 multi_agent.enabled)。
|
// MultiAgentLoop Eino DeepAgent 非流式对话(需 multi_agent.enabled)。
|
||||||
func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
||||||
if h.config == nil || !h.config.MultiAgent.Enabled {
|
if h.config == nil || !h.config.MultiAgent.Enabled {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "多代理未启用,请在 config.yaml 中设置 multi_agent.enabled: true"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "多代理未启用,请在 config.yaml 中设置 multi_agent.enabled: true"})
|
||||||
|
|||||||
+101
-149
@@ -423,8 +423,8 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
"agentMode": map[string]interface{}{
|
"agentMode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "代理模式:single(原生 ReAct)| eino_single(Eino ADK 单代理)| deep | plan_execute | supervisor;react 同 single;旧值 multi 按 deep",
|
"description": "代理模式:eino_single(Eino ADK 单代理,默认)| deep | plan_execute | supervisor",
|
||||||
"enum": []string{"single", "eino_single", "deep", "plan_execute", "supervisor", "multi", "react"},
|
"enum": []string{"eino_single", "deep", "plan_execute", "supervisor"},
|
||||||
},
|
},
|
||||||
"scheduleMode": map[string]interface{}{
|
"scheduleMode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -778,11 +778,54 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
"ConfigResponse": map[string]interface{}{
|
"ConfigResponse": map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "配置信息",
|
"description": "配置信息(含 openai、vision、multi_agent 等)",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"vision": map[string]interface{}{
|
||||||
|
"$ref": "#/components/schemas/VisionConfig",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"UpdateConfigRequest": map[string]interface{}{
|
"UpdateConfigRequest": map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "更新配置请求",
|
"description": "更新配置请求",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"vision": map[string]interface{}{
|
||||||
|
"$ref": "#/components/schemas/VisionConfig",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"VisionConfig": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"description": "视觉分析(analyze_image MCP 工具);enabled 且 model 非空时注册工具",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"enabled": map[string]interface{}{"type": "boolean", "description": "是否启用 analyze_image"},
|
||||||
|
"model": map[string]interface{}{"type": "string", "description": "视觉模型名(必填)", "example": "qwen-vl-max"},
|
||||||
|
"api_key": map[string]interface{}{"type": "string", "description": "API Key;留空复用 openai.api_key"},
|
||||||
|
"base_url": map[string]interface{}{"type": "string", "description": "Base URL;留空复用 openai.base_url"},
|
||||||
|
"provider": map[string]interface{}{"type": "string", "description": "提供商;留空复用 openai.provider"},
|
||||||
|
"timeout_seconds": map[string]interface{}{"type": "integer", "description": "VL 调用超时(秒)"},
|
||||||
|
"max_image_bytes": map[string]interface{}{"type": "integer", "description": "原始文件大小上限(字节)"},
|
||||||
|
"max_dimension": map[string]interface{}{"type": "integer", "description": "长边缩放像素"},
|
||||||
|
"jpeg_quality": map[string]interface{}{"type": "integer", "description": "JPEG 质量 60-100"},
|
||||||
|
"max_payload_bytes": map[string]interface{}{"type": "integer", "description": "送 API 体积上限(字节)"},
|
||||||
|
"skip_preprocess_below_bytes": map[string]interface{}{"type": "integer", "description": "低于该字节且尺寸合规时可原图直传;0=始终压缩"},
|
||||||
|
"detail": map[string]interface{}{"type": "string", "enum": []string{"low", "high", "auto"}, "description": "OpenAI 兼容 image detail"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"AnalyzeImageToolCall": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"description": "内置 MCP 工具 analyze_image:分析服务器本地图片,返回纯文本(验证码/UI/报错等)",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"path": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "图片绝对路径或相对于进程工作目录的路径",
|
||||||
|
},
|
||||||
|
"question": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "可选:重点问题;验证码建议「只输出验证码字符」",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"path"},
|
||||||
},
|
},
|
||||||
"ExternalMCPConfig": map[string]interface{}{
|
"ExternalMCPConfig": map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1121,7 +1164,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"post": map[string]interface{}{
|
"post": map[string]interface{}{
|
||||||
"tags": []string{"对话管理"},
|
"tags": []string{"对话管理"},
|
||||||
"summary": "创建对话",
|
"summary": "创建对话",
|
||||||
"description": "创建一个新的安全测试对话。\n**重要说明**:\n- ✅ 创建的对话会**立即保存到数据库**\n- ✅ 前端页面会**自动刷新**显示新对话\n- ✅ 与前端创建的对话**完全一致**\n**创建对话的两种方式**:\n**方式1(推荐):** 直接使用 `/api/agent-loop` 发送消息,**不提供** `conversationId` 参数,系统会自动创建新对话并发送消息。这是最简单的方式,一步完成创建和发送。\n**方式2:** 先调用此端点创建空对话,然后使用返回的 `conversationId` 调用 `/api/agent-loop` 发送消息。适用于需要先创建对话,稍后再发送消息的场景。\n**示例**:\n```json\n{\n \"title\": \"Web应用安全测试\"\n}\n```",
|
"description": "创建一个新的安全测试对话。\n**重要说明**:\n- ✅ 创建的对话会**立即保存到数据库**\n- ✅ 前端页面会**自动刷新**显示新对话\n- ✅ 与前端创建的对话**完全一致**\n**创建对话的两种方式**:\n**方式1(推荐):** 直接使用 `/api/eino-agent` 发送消息,**不提供** `conversationId` 参数,系统会自动创建新对话并发送消息。这是最简单的方式,一步完成创建和发送。\n**方式2:** 先调用此端点创建空对话,然后使用返回的 `conversationId` 调用 `/api/eino-agent` 发送消息。适用于需要先创建对话,稍后再发送消息的场景。\n**示例**:\n```json\n{\n \"title\": \"Web应用安全测试\"\n}\n```",
|
||||||
"operationId": "createConversation",
|
"operationId": "createConversation",
|
||||||
"requestBody": map[string]interface{}{
|
"requestBody": map[string]interface{}{
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1412,148 +1455,11 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/api/agent-loop": map[string]interface{}{
|
|
||||||
"post": map[string]interface{}{
|
|
||||||
"tags": []string{"对话交互"},
|
|
||||||
"summary": "发送消息并获取AI回复(非流式)",
|
|
||||||
"description": "向AI发送消息并获取回复(非流式响应)。**这是与AI交互的核心端点**,与前端聊天功能完全一致。\n**重要说明**:\n- ✅ 通过此API创建/发送的消息会**立即保存到数据库**\n- ✅ 前端页面会**自动刷新**显示新创建的对话和消息\n- ✅ 所有操作都有**完整的交互痕迹**,就像在前端操作一样\n- ✅ 支持角色配置,可以指定使用哪个测试角色\n**推荐使用流程**:\n1. **先创建对话**:调用 `POST /api/conversations` 创建新对话,获取 `conversationId`\n2. **再发送消息**:使用返回的 `conversationId` 调用此端点发送消息\n**使用示例**:\n**步骤1 - 创建对话:**\n```json\nPOST /api/conversations\n{\n \"title\": \"Web应用安全测试\"\n}\n```\n**步骤2 - 发送消息:**\n```json\nPOST /api/agent-loop\n{\n \"conversationId\": \"返回的对话ID\",\n \"message\": \"扫描 http://example.com 的SQL注入漏洞\",\n \"role\": \"渗透测试\"\n}\n```\n**其他方式**:\n如果不提供 `conversationId`,系统会自动创建新对话并发送消息。但**推荐先创建对话**,这样可以更好地管理对话列表。\n**响应**:返回AI的回复、对话ID和MCP执行ID列表。前端会自动刷新显示新消息。",
|
|
||||||
"operationId": "sendMessage",
|
|
||||||
"requestBody": map[string]interface{}{
|
|
||||||
"required": true,
|
|
||||||
"content": map[string]interface{}{
|
|
||||||
"application/json": map[string]interface{}{
|
|
||||||
"schema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"message": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "要发送的消息(必需)",
|
|
||||||
"example": "扫描 http://example.com 的SQL注入漏洞",
|
|
||||||
},
|
|
||||||
"conversationId": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "对话ID(可选)。\n- **不提供**:自动创建新对话并发送消息(推荐)\n- **提供**:消息会添加到指定对话中(对话必须存在)",
|
|
||||||
"example": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
},
|
|
||||||
"role": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "角色名称(可选),如:默认、渗透测试、Web应用扫描等",
|
|
||||||
"example": "默认",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"message"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"responses": map[string]interface{}{
|
|
||||||
"200": map[string]interface{}{
|
|
||||||
"description": "消息发送成功,返回AI回复",
|
|
||||||
"content": map[string]interface{}{
|
|
||||||
"application/json": map[string]interface{}{
|
|
||||||
"schema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"response": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "AI的回复内容",
|
|
||||||
},
|
|
||||||
"conversationId": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "对话ID",
|
|
||||||
},
|
|
||||||
"mcpExecutionIds": map[string]interface{}{
|
|
||||||
"type": "array",
|
|
||||||
"description": "MCP执行ID列表",
|
|
||||||
"items": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"time": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "响应时间",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": map[string]interface{}{
|
|
||||||
"description": "请求参数错误",
|
|
||||||
},
|
|
||||||
"401": map[string]interface{}{
|
|
||||||
"description": "未授权,需要有效的Token",
|
|
||||||
},
|
|
||||||
"500": map[string]interface{}{
|
|
||||||
"description": "服务器内部错误",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"/api/agent-loop/stream": map[string]interface{}{
|
|
||||||
"post": map[string]interface{}{
|
|
||||||
"tags": []string{"对话交互"},
|
|
||||||
"summary": "发送消息并获取AI回复(流式)",
|
|
||||||
"description": "向AI发送消息并获取流式回复(Server-Sent Events)。**这是与AI交互的核心端点**,与前端聊天功能完全一致。\n**重要说明**:\n- ✅ 通过此API创建/发送的消息会**立即保存到数据库**\n- ✅ 前端页面会**自动刷新**显示新创建的对话和消息\n- ✅ 所有操作都有**完整的交互痕迹**,就像在前端操作一样\n- ✅ 支持角色配置,可以指定使用哪个测试角色\n- ✅ 返回流式响应,适合实时显示AI回复\n**推荐使用流程**:\n1. **先创建对话**:调用 `POST /api/conversations` 创建新对话,获取 `conversationId`\n2. **再发送消息**:使用返回的 `conversationId` 调用此端点发送消息\n**使用示例**:\n**步骤1 - 创建对话:**\n```json\nPOST /api/conversations\n{\n \"title\": \"Web应用安全测试\"\n}\n```\n**步骤2 - 发送消息(流式):**\n```json\nPOST /api/agent-loop/stream\n{\n \"conversationId\": \"返回的对话ID\",\n \"message\": \"扫描 http://example.com 的SQL注入漏洞\",\n \"role\": \"渗透测试\"\n}\n```\n**响应格式**:Server-Sent Events (SSE),事件类型包括:\n- `message`: 用户消息确认\n- `response`: AI回复片段\n- `progress`: 进度更新\n- `done`: 完成\n- `error`: 错误\n- `cancelled`: 已取消",
|
|
||||||
"operationId": "sendMessageStream",
|
|
||||||
"requestBody": map[string]interface{}{
|
|
||||||
"required": true,
|
|
||||||
"content": map[string]interface{}{
|
|
||||||
"application/json": map[string]interface{}{
|
|
||||||
"schema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"message": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "要发送的消息(必需)",
|
|
||||||
"example": "扫描 http://example.com 的SQL注入漏洞",
|
|
||||||
},
|
|
||||||
"conversationId": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "对话ID(可选)。\n- **不提供**:自动创建新对话并发送消息(推荐)\n- **提供**:消息会添加到指定对话中(对话必须存在)",
|
|
||||||
"example": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
},
|
|
||||||
"role": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "角色名称(可选),如:默认、渗透测试、Web应用扫描等",
|
|
||||||
"example": "默认",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"message"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"responses": map[string]interface{}{
|
|
||||||
"200": map[string]interface{}{
|
|
||||||
"description": "流式响应(Server-Sent Events)",
|
|
||||||
"content": map[string]interface{}{
|
|
||||||
"text/event-stream": map[string]interface{}{
|
|
||||||
"schema": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "SSE流式数据",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": map[string]interface{}{
|
|
||||||
"description": "请求参数错误",
|
|
||||||
},
|
|
||||||
"401": map[string]interface{}{
|
|
||||||
"description": "未授权,需要有效的Token",
|
|
||||||
},
|
|
||||||
"500": map[string]interface{}{
|
|
||||||
"description": "服务器内部错误",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"/api/eino-agent": map[string]interface{}{
|
"/api/eino-agent": map[string]interface{}{
|
||||||
"post": map[string]interface{}{
|
"post": map[string]interface{}{
|
||||||
"tags": []string{"对话交互"},
|
"tags": []string{"对话交互"},
|
||||||
"summary": "发送消息并获取 AI 回复(Eino ADK 单代理,非流式)",
|
"summary": "发送消息并获取 AI 回复(Eino ADK 单代理,非流式)",
|
||||||
"description": "与 `POST /api/agent-loop` 请求体相同,由 **CloudWeGo Eino** `adk.NewChatModelAgent` + `adk.NewRunner.Run` 执行(单代理 MCP 工具链)。**不依赖** `multi_agent.enabled`;`multi_agent.eino_skills` / `eino_middleware` 等与多代理主代理一致时可生效。支持 `webshellConnectionId`。",
|
"description": "向 AI 发送消息并获取回复(非流式)。由 **CloudWeGo Eino** `adk.NewChatModelAgent` + `adk.NewRunner.Run` 执行单代理 MCP 工具链。**不依赖** `multi_agent.enabled`;`multi_agent.eino_skills` / `eino_middleware` 等与多代理主代理一致时可生效。支持 `webshellConnectionId`、角色与附件。",
|
||||||
"operationId": "sendMessageEinoSingleAgent",
|
"operationId": "sendMessageEinoSingleAgent",
|
||||||
"requestBody": map[string]interface{}{
|
"requestBody": map[string]interface{}{
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1573,7 +1479,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"responses": map[string]interface{}{
|
"responses": map[string]interface{}{
|
||||||
"200": map[string]interface{}{"description": "成功,响应格式同 /api/agent-loop"},
|
"200": map[string]interface{}{"description": "成功,响应格式同 /api/eino-agent"},
|
||||||
"400": map[string]interface{}{"description": "参数错误"},
|
"400": map[string]interface{}{"description": "参数错误"},
|
||||||
"401": map[string]interface{}{"description": "未授权"},
|
"401": map[string]interface{}{"description": "未授权"},
|
||||||
"500": map[string]interface{}{"description": "执行失败"},
|
"500": map[string]interface{}{"description": "执行失败"},
|
||||||
@@ -1584,7 +1490,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"post": map[string]interface{}{
|
"post": map[string]interface{}{
|
||||||
"tags": []string{"对话交互"},
|
"tags": []string{"对话交互"},
|
||||||
"summary": "发送消息并获取 AI 回复(Eino ADK 单代理,SSE)",
|
"summary": "发送消息并获取 AI 回复(Eino ADK 单代理,SSE)",
|
||||||
"description": "与 `POST /api/agent-loop/stream` 类似;由 Eino **单代理** ADK 执行。事件类型与多代理流式一致(含 `tool_call` / `response_delta` 等)。**不依赖** `multi_agent.enabled`。",
|
"description": "向 AI 发送消息并获取流式回复(SSE)。由 Eino **单代理** ADK 执行;事件类型与多代理流式一致(含 `tool_call` / `response_delta` / `thinking` 等)。**不依赖** `multi_agent.enabled`。",
|
||||||
"operationId": "sendMessageEinoSingleAgentStream",
|
"operationId": "sendMessageEinoSingleAgentStream",
|
||||||
"requestBody": map[string]interface{}{
|
"requestBody": map[string]interface{}{
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1623,7 +1529,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"post": map[string]interface{}{
|
"post": map[string]interface{}{
|
||||||
"tags": []string{"对话交互"},
|
"tags": []string{"对话交互"},
|
||||||
"summary": "发送消息并获取 AI 回复(Eino 多代理,非流式)",
|
"summary": "发送消息并获取 AI 回复(Eino 多代理,非流式)",
|
||||||
"description": "与 `POST /api/agent-loop` 请求体相同,但由 **CloudWeGo Eino** 多代理执行。编排由请求体 `orchestration`(`deep` | `plan_execute` | `supervisor`)指定,缺省为 `deep`。**前提**:`multi_agent.enabled: true`;未启用时返回 404 JSON。支持 `webshellConnectionId`。",
|
"description": "与 `POST /api/eino-agent` 请求体相同,但由 **CloudWeGo Eino** 多代理执行。编排由请求体 `orchestration`(`deep` | `plan_execute` | `supervisor`)指定,缺省为 `deep`。**前提**:`multi_agent.enabled: true`;未启用时返回 404 JSON。支持 `webshellConnectionId`。",
|
||||||
"operationId": "sendMessageMultiAgent",
|
"operationId": "sendMessageMultiAgent",
|
||||||
"requestBody": map[string]interface{}{
|
"requestBody": map[string]interface{}{
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1646,7 +1552,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
"webshellConnectionId": map[string]interface{}{
|
"webshellConnectionId": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "WebShell 连接 ID(可选,与 agent-loop 行为一致)",
|
"description": "WebShell 连接 ID(可选,与 Eino 单/多代理流式行为一致)",
|
||||||
},
|
},
|
||||||
"orchestration": map[string]interface{}{
|
"orchestration": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1661,7 +1567,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
"responses": map[string]interface{}{
|
"responses": map[string]interface{}{
|
||||||
"200": map[string]interface{}{
|
"200": map[string]interface{}{
|
||||||
"description": "成功,响应格式同 /api/agent-loop",
|
"description": "成功,响应格式同 /api/eino-agent",
|
||||||
},
|
},
|
||||||
"400": map[string]interface{}{"description": "参数错误"},
|
"400": map[string]interface{}{"description": "参数错误"},
|
||||||
"401": map[string]interface{}{"description": "未授权"},
|
"401": map[string]interface{}{"description": "未授权"},
|
||||||
@@ -1674,7 +1580,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"post": map[string]interface{}{
|
"post": map[string]interface{}{
|
||||||
"tags": []string{"对话交互"},
|
"tags": []string{"对话交互"},
|
||||||
"summary": "发送消息并获取 AI 回复(Eino 多代理,SSE)",
|
"summary": "发送消息并获取 AI 回复(Eino 多代理,SSE)",
|
||||||
"description": "与 `POST /api/agent-loop/stream` 类似;由 Eino 多代理执行。`orchestration` 指定 deep / plan_execute / supervisor,缺省 deep。**前提**:`multi_agent.enabled: true`;未启用时 SSE 内首条为 `type: error` 后接 `done`。支持 `webshellConnectionId`。",
|
"description": "与 `POST /api/eino-agent/stream` 类似;由 Eino 多代理执行。`orchestration` 指定 deep / plan_execute / supervisor,缺省 deep。**前提**:`multi_agent.enabled: true`;未启用时 SSE 内首条为 `type: error` 后接 `done`。支持 `webshellConnectionId`。",
|
||||||
"operationId": "sendMessageMultiAgentStream",
|
"operationId": "sendMessageMultiAgentStream",
|
||||||
"requestBody": map[string]interface{}{
|
"requestBody": map[string]interface{}{
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -4790,7 +4696,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"title": map[string]interface{}{"type": "string", "description": "队列标题"},
|
"title": map[string]interface{}{"type": "string", "description": "队列标题"},
|
||||||
"role": map[string]interface{}{"type": "string", "description": "使用的角色名称"},
|
"role": map[string]interface{}{"type": "string", "description": "使用的角色名称"},
|
||||||
"agentMode": map[string]interface{}{"type": "string", "description": "代理模式", "enum": []string{"single", "eino_single", "deep", "plan_execute", "supervisor"}},
|
"agentMode": map[string]interface{}{"type": "string", "description": "代理模式", "enum": []string{"eino_single", "deep", "plan_execute", "supervisor"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -5037,6 +4943,52 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ==================== 配置管理 - 缺失端点 ====================
|
// ==================== 配置管理 - 缺失端点 ====================
|
||||||
|
"/api/config/test-vision": map[string]interface{}{
|
||||||
|
"post": map[string]interface{}{
|
||||||
|
"tags": []string{"配置管理"},
|
||||||
|
"summary": "测试视觉模型连接",
|
||||||
|
"description": "测试 Vision 模型 API 是否可用。vision.api_key/base_url 留空时可传 openai 段作回退。",
|
||||||
|
"operationId": "testVision",
|
||||||
|
"requestBody": map[string]interface{}{
|
||||||
|
"required": true,
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"application/json": map[string]interface{}{
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"required": []string{"vision"},
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"vision": map[string]interface{}{"$ref": "#/components/schemas/VisionConfig"},
|
||||||
|
"openai": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"description": "主 LLM 配置(vision 字段留空时用于 API Key/Base URL 回退)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"responses": map[string]interface{}{
|
||||||
|
"200": map[string]interface{}{
|
||||||
|
"description": "测试结果",
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"application/json": map[string]interface{}{
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"success": map[string]interface{}{"type": "boolean"},
|
||||||
|
"error": map[string]interface{}{"type": "string"},
|
||||||
|
"model": map[string]interface{}{"type": "string"},
|
||||||
|
"latency_ms": map[string]interface{}{"type": "number"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"400": map[string]interface{}{"description": "参数错误"},
|
||||||
|
"401": map[string]interface{}{"description": "未授权"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/config/test-openai": map[string]interface{}{
|
"/api/config/test-openai": map[string]interface{}{
|
||||||
"post": map[string]interface{}{
|
"post": map[string]interface{}{
|
||||||
"tags": []string{"配置管理"},
|
"tags": []string{"配置管理"},
|
||||||
|
|||||||
@@ -311,6 +311,38 @@ func (h *VulnerabilityHandler) DeleteVulnerability(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchDeleteVulnerabilities 按当前筛选条件批量删除漏洞
|
||||||
|
func (h *VulnerabilityHandler) BatchDeleteVulnerabilities(c *gin.Context) {
|
||||||
|
filter := parseVulnerabilityListFilter(c)
|
||||||
|
|
||||||
|
total, err := h.db.CountVulnerabilities(filter)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("统计待删除漏洞失败", zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "当前筛选条件下没有可删除的漏洞", "deleted": 0})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted, err := h.db.DeleteVulnerabilitiesByFilter(filter)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("批量删除漏洞失败", zap.Error(err), zap.Int("count", total))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.audit != nil {
|
||||||
|
h.audit.RecordOK(c, "vulnerability", "delete_batch", "批量删除漏洞记录", "vulnerability", "", map[string]interface{}{
|
||||||
|
"deleted": deleted,
|
||||||
|
"filter": filter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "批量删除成功", "deleted": deleted})
|
||||||
|
}
|
||||||
|
|
||||||
// GetVulnerabilityStats 获取漏洞统计
|
// GetVulnerabilityStats 获取漏洞统计
|
||||||
func (h *VulnerabilityHandler) GetVulnerabilityStats(c *gin.Context) {
|
func (h *VulnerabilityHandler) GetVulnerabilityStats(c *gin.Context) {
|
||||||
filter := parseVulnerabilityListFilter(c)
|
filter := parseVulnerabilityListFilter(c)
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const (
|
|||||||
ToolListKnowledgeRiskTypes = "list_knowledge_risk_types"
|
ToolListKnowledgeRiskTypes = "list_knowledge_risk_types"
|
||||||
ToolSearchKnowledgeBase = "search_knowledge_base"
|
ToolSearchKnowledgeBase = "search_knowledge_base"
|
||||||
|
|
||||||
|
// 视觉分析(本地图片 → VL 模型 → 文本摘要)
|
||||||
|
ToolAnalyzeImage = "analyze_image"
|
||||||
|
|
||||||
// WebShell 助手工具(AI 在 WebShell 管理 - AI 助手 中使用)
|
// WebShell 助手工具(AI 在 WebShell 管理 - AI 助手 中使用)
|
||||||
ToolWebshellExec = "webshell_exec"
|
ToolWebshellExec = "webshell_exec"
|
||||||
ToolWebshellFileList = "webshell_file_list"
|
ToolWebshellFileList = "webshell_file_list"
|
||||||
@@ -73,6 +76,7 @@ func IsBuiltinTool(toolName string) bool {
|
|||||||
ToolRestoreProjectFact,
|
ToolRestoreProjectFact,
|
||||||
ToolListKnowledgeRiskTypes,
|
ToolListKnowledgeRiskTypes,
|
||||||
ToolSearchKnowledgeBase,
|
ToolSearchKnowledgeBase,
|
||||||
|
ToolAnalyzeImage,
|
||||||
ToolWebshellExec,
|
ToolWebshellExec,
|
||||||
ToolWebshellFileList,
|
ToolWebshellFileList,
|
||||||
ToolWebshellFileRead,
|
ToolWebshellFileRead,
|
||||||
@@ -124,6 +128,7 @@ func GetAllBuiltinTools() []string {
|
|||||||
ToolRestoreProjectFact,
|
ToolRestoreProjectFact,
|
||||||
ToolListKnowledgeRiskTypes,
|
ToolListKnowledgeRiskTypes,
|
||||||
ToolSearchKnowledgeBase,
|
ToolSearchKnowledgeBase,
|
||||||
|
ToolAnalyzeImage,
|
||||||
ToolWebshellExec,
|
ToolWebshellExec,
|
||||||
ToolWebshellFileList,
|
ToolWebshellFileList,
|
||||||
ToolWebshellFileRead,
|
ToolWebshellFileRead,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
const einoSingleAgentName = "cyberstrike-eino-single"
|
const einoSingleAgentName = "cyberstrike-eino-single"
|
||||||
|
|
||||||
// RunEinoSingleChatModelAgent 使用 Eino adk.NewChatModelAgent + adk.NewRunner.Run(官方 Quick Start 的 Query 同属 Runner API;此处用历史 + 用户消息切片等价于多轮 Query)。
|
// RunEinoSingleChatModelAgent 使用 Eino adk.NewChatModelAgent + adk.NewRunner.Run(官方 Quick Start 的 Query 同属 Runner API;此处用历史 + 用户消息切片等价于多轮 Query)。
|
||||||
// 不替代既有原生 ReAct;与 RunDeepAgent 共享 runEinoADKAgentLoop 的 SSE 映射与 MCP 桥。
|
// 与 RunDeepAgent 共享 runEinoADKAgentLoop 的 SSE 映射与 MCP 桥。
|
||||||
func RunEinoSingleChatModelAgent(
|
func RunEinoSingleChatModelAgent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
appCfg *config.Config,
|
appCfg *config.Config,
|
||||||
@@ -160,13 +160,7 @@ func RunEinoSingleChatModelAgent(
|
|||||||
handlers = append(handlers, capMw)
|
handlers = append(handlers, capMw)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxIter := ma.MaxIteration
|
maxIter := agentMaxIterations(appCfg)
|
||||||
if maxIter <= 0 {
|
|
||||||
maxIter = appCfg.Agent.MaxIterations
|
|
||||||
}
|
|
||||||
if maxIter <= 0 {
|
|
||||||
maxIter = 40
|
|
||||||
}
|
|
||||||
|
|
||||||
mainToolsCfg := adk.ToolsConfig{
|
mainToolsCfg := adk.ToolsConfig{
|
||||||
ToolsNodeConfig: compose.ToolsNodeConfig{
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
||||||
@@ -180,6 +174,7 @@ func RunEinoSingleChatModelAgent(
|
|||||||
EmitInternalEvents: true,
|
EmitInternalEvents: true,
|
||||||
}
|
}
|
||||||
ins := project.AppendSystemPromptBlock(ag.EinoSingleAgentSystemInstruction(), systemPromptExtra)
|
ins := project.AppendSystemPromptBlock(ag.EinoSingleAgentSystemInstruction(), systemPromptExtra)
|
||||||
|
ins = project.AppendVisionImageAnalysisIfReady(ins, appCfg.Vision.Ready())
|
||||||
ins = injectToolNamesOnlyInstruction(ctx, ins, mainTools, singleToolSearchActive)
|
ins = injectToolNamesOnlyInstruction(ctx, ins, mainTools, singleToolSearchActive)
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
names := collectToolNames(ctx, mainTools)
|
names := collectToolNames(ctx, mainTools)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// einoSummarizeUserInstruction 与单 Agent MemoryCompressor 目标一致:压缩时保留渗透关键信息。
|
// einoSummarizeUserInstruction:压缩历史时保留渗透测试关键信息。
|
||||||
const einoSummarizeUserInstruction = `在保持所有关键安全测试信息完整的前提下压缩对话历史。
|
const einoSummarizeUserInstruction = `在保持所有关键安全测试信息完整的前提下压缩对话历史。
|
||||||
|
|
||||||
必须保留:已确认漏洞与攻击路径、工具输出中的核心发现、凭证与认证细节、架构与薄弱点、当前进度、失败尝试与死路、策略决策。
|
必须保留:已确认漏洞与攻击路径、工具输出中的核心发现、凭证与认证细节、架构与薄弱点、当前进度、失败尝试与死路、策略决策。
|
||||||
@@ -29,7 +29,7 @@ const einoSummarizeUserInstruction = `在保持所有关键安全测试信息完
|
|||||||
输出须使后续代理能无缝继续同一授权测试任务。`
|
输出须使后续代理能无缝继续同一授权测试任务。`
|
||||||
|
|
||||||
// newEinoSummarizationMiddleware 使用 Eino ADK Summarization 中间件(见 https://www.cloudwego.io/zh/docs/eino/core_modules/eino_adk/eino_adk_chatmodelagentmiddleware/middleware_summarization/)。
|
// newEinoSummarizationMiddleware 使用 Eino ADK Summarization 中间件(见 https://www.cloudwego.io/zh/docs/eino/core_modules/eino_adk/eino_adk_chatmodelagentmiddleware/middleware_summarization/)。
|
||||||
// 触发阈值与单 Agent MemoryCompressor 一致:当估算 token 超过 openai.max_total_tokens 的 90% 时摘要。
|
// 触发阈值:估算 token 超过 openai.max_total_tokens * summarization_trigger_ratio(默认 0.8)时摘要。
|
||||||
func newEinoSummarizationMiddleware(
|
func newEinoSummarizationMiddleware(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
summaryModel model.BaseChatModel,
|
summaryModel model.BaseChatModel,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package multiagent
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,10 +23,6 @@ func isEinoTransientRunError(err error) bool {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// io.EOF 常见于流式正常收尾,不应触发分段重试。
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -66,6 +61,7 @@ func isEinoTransientRunError(err error) bool {
|
|||||||
"tls handshake timeout",
|
"tls handshake timeout",
|
||||||
"stream error",
|
"stream error",
|
||||||
"unexpected eof",
|
"unexpected eof",
|
||||||
|
`": eof`, // net/http: Post "url": EOF (often wraps io.EOF)
|
||||||
"unexpected end of json",
|
"unexpected end of json",
|
||||||
"status code: 406",
|
"status code: 406",
|
||||||
"status code: 502",
|
"status code: 502",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package multiagent
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -21,6 +22,8 @@ func TestIsEinoTransientRunError(t *testing.T) {
|
|||||||
{"nil", nil, false},
|
{"nil", nil, false},
|
||||||
{"io eof", io.EOF, false},
|
{"io eof", io.EOF, false},
|
||||||
{"plain eof text", errors.New("EOF"), false},
|
{"plain eof text", errors.New("EOF"), false},
|
||||||
|
{"post chat completions eof", errors.New(`Post "https://token-plan-cn.xiaomimimo.com/v1/chat/completions": EOF`), true},
|
||||||
|
{"post eof wraps io.EOF", fmt.Errorf(`Post %q: %w`, "https://token-plan-cn.xiaomimimo.com/v1/chat/completions", io.EOF), true},
|
||||||
{"429", errors.New("HTTP 429 Too Many Requests"), true},
|
{"429", errors.New("HTTP 429 Too Many Requests"), true},
|
||||||
{"rate limit", errors.New(`{"error":"rate limit exceeded"}`), true},
|
{"rate limit", errors.New(`{"error":"rate limit exceeded"}`), true},
|
||||||
{"connection reset", errors.New("read tcp: connection reset by peer"), true},
|
{"connection reset", errors.New("read tcp: connection reset by peer"), true},
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import "cyberstrike-ai/internal/config"
|
||||||
|
|
||||||
|
const defaultAgentMaxIterations = 3000
|
||||||
|
|
||||||
|
// agentMaxIterations 全局上限:仅使用 config.agent.max_iterations;≤0 时与 config 默认一致为 3000。
|
||||||
|
func agentMaxIterations(appCfg *config.Config) int {
|
||||||
|
if appCfg != nil && appCfg.Agent.MaxIterations > 0 {
|
||||||
|
return appCfg.Agent.MaxIterations
|
||||||
|
}
|
||||||
|
return defaultAgentMaxIterations
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveMaxIterations 统一迭代上限:Markdown/子代理 front matter 中 max_iterations>0 可单独覆盖,否则使用 agent.max_iterations。
|
||||||
|
// multi_agent.max_iteration 与 sub_agent_max_iterations 已废弃,不再参与计算。
|
||||||
|
func resolveMaxIterations(appCfg *config.Config, markdownOverride int) int {
|
||||||
|
if markdownOverride > 0 {
|
||||||
|
return markdownOverride
|
||||||
|
}
|
||||||
|
return agentMaxIterations(appCfg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAgentMaxIterations(t *testing.T) {
|
||||||
|
if got := agentMaxIterations(nil); got != defaultAgentMaxIterations {
|
||||||
|
t.Fatalf("nil cfg: got %d want %d", got, defaultAgentMaxIterations)
|
||||||
|
}
|
||||||
|
cfg := &config.Config{Agent: config.AgentConfig{MaxIterations: 12000}}
|
||||||
|
if got := agentMaxIterations(cfg); got != 12000 {
|
||||||
|
t.Fatalf("got %d want 12000", got)
|
||||||
|
}
|
||||||
|
cfg.Agent.MaxIterations = 0
|
||||||
|
if got := agentMaxIterations(cfg); got != defaultAgentMaxIterations {
|
||||||
|
t.Fatalf("zero: got %d want %d", got, defaultAgentMaxIterations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveMaxIterations(t *testing.T) {
|
||||||
|
cfg := &config.Config{Agent: config.AgentConfig{MaxIterations: 12000}}
|
||||||
|
if got := resolveMaxIterations(cfg, 0); got != 12000 {
|
||||||
|
t.Fatalf("global: got %d want 12000", got)
|
||||||
|
}
|
||||||
|
if got := resolveMaxIterations(cfg, 50); got != 50 {
|
||||||
|
t.Fatalf("override: got %d want 50", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,18 +170,7 @@ func RunDeepAgent(
|
|||||||
}
|
}
|
||||||
reasoning.ApplyToEinoChatModelConfig(baseModelCfg, &appCfg.OpenAI, reasoningClient)
|
reasoning.ApplyToEinoChatModelConfig(baseModelCfg, &appCfg.OpenAI, reasoningClient)
|
||||||
|
|
||||||
deepMaxIter := ma.MaxIteration
|
deepMaxIter := agentMaxIterations(appCfg)
|
||||||
if deepMaxIter <= 0 {
|
|
||||||
deepMaxIter = appCfg.Agent.MaxIterations
|
|
||||||
}
|
|
||||||
if deepMaxIter <= 0 {
|
|
||||||
deepMaxIter = 40
|
|
||||||
}
|
|
||||||
|
|
||||||
subDefaultIter := ma.SubAgentMaxIterations
|
|
||||||
if subDefaultIter <= 0 {
|
|
||||||
subDefaultIter = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
var subAgents []adk.Agent
|
var subAgents []adk.Agent
|
||||||
if orchMode != "plan_execute" {
|
if orchMode != "plan_execute" {
|
||||||
@@ -230,10 +219,7 @@ func RunDeepAgent(
|
|||||||
return nil, fmt.Errorf("子代理 %q eino 中间件: %w", id, err)
|
return nil, fmt.Errorf("子代理 %q eino 中间件: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
subMax := sub.MaxIterations
|
subMax := resolveMaxIterations(appCfg, sub.MaxIterations)
|
||||||
if subMax <= 0 {
|
|
||||||
subMax = subDefaultIter
|
|
||||||
}
|
|
||||||
|
|
||||||
subSumMw, err := newEinoSummarizationMiddleware(ctx, subModel, appCfg, &ma.EinoMiddleware, conversationID, logger)
|
subSumMw, err := newEinoSummarizationMiddleware(ctx, subModel, appCfg, &ma.EinoMiddleware, conversationID, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -262,7 +248,8 @@ func RunDeepAgent(
|
|||||||
subHandlers = append(subHandlers, teleMw)
|
subHandlers = append(subHandlers, teleMw)
|
||||||
}
|
}
|
||||||
|
|
||||||
subInstrFinal := injectToolNamesOnlyInstruction(ctx, instr, subTools, subToolSearchActive)
|
subInstrFinal := project.AppendVisionImageAnalysisIfReady(instr, appCfg.Vision.Ready())
|
||||||
|
subInstrFinal = injectToolNamesOnlyInstruction(ctx, subInstrFinal, subTools, subToolSearchActive)
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
subNames := collectToolNames(ctx, subTools)
|
subNames := collectToolNames(ctx, subTools)
|
||||||
mountedNames := collectToolNames(ctx, subToolsForCfg)
|
mountedNames := collectToolNames(ctx, subToolsForCfg)
|
||||||
@@ -342,6 +329,7 @@ func RunDeepAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
orchInstruction = project.AppendSystemPromptBlock(orchInstruction, systemPromptExtra)
|
orchInstruction = project.AppendSystemPromptBlock(orchInstruction, systemPromptExtra)
|
||||||
|
orchInstruction = project.AppendVisionImageAnalysisIfReady(orchInstruction, appCfg.Vision.Ready())
|
||||||
orchInstruction = injectToolNamesOnlyInstruction(ctx, orchInstruction, mainTools, mainToolSearchActive)
|
orchInstruction = injectToolNamesOnlyInstruction(ctx, orchInstruction, mainTools, mainToolSearchActive)
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
mainNames := collectToolNames(ctx, mainTools)
|
mainNames := collectToolNames(ctx, mainTools)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// VisionImageAnalysisSection 单/多代理共用的图片分析提示(analyze_image;上下文仅保留文字摘要)。
|
||||||
|
func VisionImageAnalysisSection() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("## 图片分析\n\n")
|
||||||
|
b.WriteString("- 遇到图片文件(截图、验证码、登录页、报告配图)时,若存在工具 analyze_image,请传入服务器上的文件路径进行分析。\n")
|
||||||
|
b.WriteString("- 不要对二进制图片使用 read_file 指望理解内容;用户消息中「📎 xxx.png: /path」即为可传给 analyze_image 的路径。\n")
|
||||||
|
b.WriteString("- 验证码类:若已从页面或接口保存为本地图片(如 captcha.png),用 analyze_image,question 写明「只输出验证码字符」;识别失败则刷新验证码后重新保存再识;复杂滑块/行为验证码勿指望单次识图成功。\n")
|
||||||
|
b.WriteString("- 委派子代理时,若子任务含验证码/截图识读,在 task description 中写明图片路径与期望输出格式。\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendVisionImageAnalysisIfReady 仅在 vision.enabled 且 model 已配置时追加图片分析提示。
|
||||||
|
func AppendVisionImageAnalysisIfReady(base string, visionReady bool) string {
|
||||||
|
if !visionReady {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return AppendSystemPromptBlock(base, VisionImageAnalysisSection())
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
"cyberstrike-ai/internal/openai"
|
||||||
|
|
||||||
|
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 调用独立 Vision ChatModel(单次 Generate)。
|
||||||
|
type Client struct {
|
||||||
|
cfg config.VisionConfig
|
||||||
|
mainOA config.OpenAIConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 构造视觉客户端。
|
||||||
|
func NewClient(visionCfg config.VisionConfig, mainOpenAI config.OpenAIConfig) *Client {
|
||||||
|
return &Client{cfg: visionCfg, mainOA: mainOpenAI}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze 将图片字节送入 VL 模型并返回文本描述。
|
||||||
|
func (c *Client) Analyze(ctx context.Context, img ImagePayload, question string) (string, error) {
|
||||||
|
if len(img.Bytes) == 0 {
|
||||||
|
return "", fmt.Errorf("empty image payload")
|
||||||
|
}
|
||||||
|
mime := strings.TrimSpace(img.MIMEType)
|
||||||
|
if mime == "" {
|
||||||
|
mime = "image/jpeg"
|
||||||
|
}
|
||||||
|
oa := c.cfg.OpenAICfgEffective(c.mainOA)
|
||||||
|
if strings.TrimSpace(oa.APIKey) == "" {
|
||||||
|
return "", fmt.Errorf("vision API key is empty (set vision.api_key or openai.api_key)")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(oa.Model) == "" {
|
||||||
|
return "", fmt.Errorf("vision model is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.Duration(c.cfg.TimeoutSecondsEffective()) * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: timeout + 15*time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
KeepAlive: 60 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
ResponseHeaderTimeout: timeout + 10*time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
httpClient = openai.NewEinoHTTPClient(&oa, httpClient)
|
||||||
|
|
||||||
|
modelCfg := &einoopenai.ChatModelConfig{
|
||||||
|
APIKey: oa.APIKey,
|
||||||
|
BaseURL: strings.TrimSuffix(oa.BaseURL, "/"),
|
||||||
|
Model: oa.Model,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
}
|
||||||
|
chatModel, err := einoopenai.NewChatModel(ctx, modelCfg)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("vision chat model: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b64 := base64.StdEncoding.EncodeToString(img.Bytes)
|
||||||
|
detail := schema.ImageURLDetailLow
|
||||||
|
switch c.cfg.DetailEffective() {
|
||||||
|
case "high":
|
||||||
|
detail = schema.ImageURLDetailHigh
|
||||||
|
case "auto":
|
||||||
|
detail = schema.ImageURLDetailAuto
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := buildVisionPrompt(question)
|
||||||
|
userMsg := &schema.Message{
|
||||||
|
Role: schema.User,
|
||||||
|
UserInputMultiContent: []schema.MessageInputPart{
|
||||||
|
{Type: schema.ChatMessagePartTypeText, Text: prompt},
|
||||||
|
{
|
||||||
|
Type: schema.ChatMessagePartTypeImageURL,
|
||||||
|
Image: &schema.MessageInputImage{
|
||||||
|
MessagePartCommon: schema.MessagePartCommon{
|
||||||
|
Base64Data: &b64,
|
||||||
|
MIMEType: mime,
|
||||||
|
},
|
||||||
|
Detail: detail,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := chatModel.Generate(ctx, []*schema.Message{userMsg})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("vision generate: %w", err)
|
||||||
|
}
|
||||||
|
if resp == nil || strings.TrimSpace(resp.Content) == "" {
|
||||||
|
return "", fmt.Errorf("vision model returned empty content")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(resp.Content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVisionPrompt(question string) string {
|
||||||
|
q := strings.TrimSpace(question)
|
||||||
|
if q == "" {
|
||||||
|
q = "请对图片做通用描述,侧重授权安全测试场景(可见文本、表单、按钮、验证码、错误信息、技术栈线索)。"
|
||||||
|
}
|
||||||
|
extra := ""
|
||||||
|
if looksLikeCaptchaQuestion(q) {
|
||||||
|
extra = "\n若为验证码:仅输出你辨认出的字符序列,不要空格、标点、解释;看不清则明确说无法识别。"
|
||||||
|
}
|
||||||
|
return `你是授权安全测试助手。请根据图片回答用户问题,只描述你能从图中确认的内容,不要编造。
|
||||||
|
用户问题:` + q + extra
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeCaptchaQuestion(q string) bool {
|
||||||
|
s := strings.ToLower(q)
|
||||||
|
for _, kw := range []string{"验证码", "captcha", "verification code", "verify code", "vcode", "图形码"} {
|
||||||
|
if strings.Contains(s, kw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Contains(s, "只输出") && (strings.Contains(s, "字符") || strings.Contains(s, "character"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLooksLikeCaptchaQuestion(t *testing.T) {
|
||||||
|
if !looksLikeCaptchaQuestion("识别验证码,只输出字符") {
|
||||||
|
t.Fatal("expected captcha hint")
|
||||||
|
}
|
||||||
|
if looksLikeCaptchaQuestion("描述登录页布局") {
|
||||||
|
t.Fatal("expected non-captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var allowedImageExt = map[string]struct{}{
|
||||||
|
".png": {}, ".jpg": {}, ".jpeg": {}, ".webp": {}, ".gif": {},
|
||||||
|
".bmp": {}, ".tif": {}, ".tiff": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveImagePath 解析并校验可读图片路径(支持任意目录;仍校验扩展名与常规文件)。
|
||||||
|
func ResolveImagePath(path string, cwd string) (string, error) {
|
||||||
|
p := strings.TrimSpace(path)
|
||||||
|
if p == "" {
|
||||||
|
return "", fmt.Errorf("path is empty")
|
||||||
|
}
|
||||||
|
cwdTrim := strings.TrimSpace(cwd)
|
||||||
|
if cwdTrim == "" {
|
||||||
|
var err error
|
||||||
|
cwdTrim, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getwd: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cwdAbs, err := filepath.Abs(filepath.Clean(cwdTrim))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate string
|
||||||
|
if filepath.IsAbs(p) {
|
||||||
|
candidate = filepath.Clean(p)
|
||||||
|
} else {
|
||||||
|
candidate = filepath.Clean(filepath.Join(cwdAbs, p))
|
||||||
|
}
|
||||||
|
resolved := normalizeAbsPath(candidate)
|
||||||
|
if resolved == "" {
|
||||||
|
return "", fmt.Errorf("invalid path")
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(resolved))
|
||||||
|
if _, ok := allowedImageExt[ext]; !ok {
|
||||||
|
return "", fmt.Errorf("unsupported image extension %q", ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
st, err := os.Stat(resolved)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("stat: %w", err)
|
||||||
|
}
|
||||||
|
if st.IsDir() {
|
||||||
|
return "", fmt.Errorf("not a regular file")
|
||||||
|
}
|
||||||
|
if st.Size() > 0 && st.Size() > 1<<30 {
|
||||||
|
return "", fmt.Errorf("file too large on disk")
|
||||||
|
}
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAbsPath(p string) string {
|
||||||
|
abs, err := filepath.Abs(filepath.Clean(p))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if link, err := filepath.EvalSymlinks(abs); err == nil {
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
return abs
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveImagePath_underCWD(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
img := filepath.Join(dir, "shot.png")
|
||||||
|
if err := os.WriteFile(img, []byte{0x89, 0x50, 0x4e, 0x47}, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := ResolveImagePath(img, dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := normalizeAbsPath(img)
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveImagePath_absoluteOutsideCWD(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cwd := t.TempDir()
|
||||||
|
img := filepath.Join(dir, "remote.png")
|
||||||
|
if err := os.WriteFile(img, []byte{0x89, 0x50, 0x4e, 0x47}, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := ResolveImagePath(img, cwd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected absolute path outside cwd to be allowed: %v", err)
|
||||||
|
}
|
||||||
|
want := normalizeAbsPath(img)
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveImagePath_rejectsNonImageExt(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
f := filepath.Join(dir, "notes.txt")
|
||||||
|
if err := os.WriteFile(f, []byte("x"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err := ResolveImagePath(f, dir)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-image extension")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImagePayload 送入 VL API 的图片字节与 MIME。
|
||||||
|
type ImagePayload struct {
|
||||||
|
Bytes []byte
|
||||||
|
MIMEType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreprocessMeta 记录缩放与编码结果,供工具输出与排障。
|
||||||
|
type PreprocessMeta struct {
|
||||||
|
OriginalPath string
|
||||||
|
OriginalBytes int64
|
||||||
|
OriginalWidth int
|
||||||
|
OriginalHeight int
|
||||||
|
OutputWidth int
|
||||||
|
OutputHeight int
|
||||||
|
OutputBytes int
|
||||||
|
OutputMIMEType string
|
||||||
|
JPEGQuality int // 0 表示未 JPEG 重编码(原图直传)
|
||||||
|
PreprocessMode string // passthrough | jpeg
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreprocessOptions 图片预处理参数。
|
||||||
|
type PreprocessOptions struct {
|
||||||
|
MaxImageBytes int64
|
||||||
|
MaxDimension int
|
||||||
|
JPEGQuality int
|
||||||
|
MaxPayloadBytes int64
|
||||||
|
SkipPreprocessBelowBytes int64 // 0 = 始终压缩;>0 时小图+尺寸合规可直传
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreprocessImageFile 读取图片;大图或超尺寸走 imaging 缩放+JPEG,否则可原图直传。
|
||||||
|
func PreprocessImageFile(path string, opt PreprocessOptions) (ImagePayload, PreprocessMeta, error) {
|
||||||
|
var meta PreprocessMeta
|
||||||
|
meta.OriginalPath = path
|
||||||
|
|
||||||
|
st, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return ImagePayload{}, meta, err
|
||||||
|
}
|
||||||
|
meta.OriginalBytes = st.Size()
|
||||||
|
if opt.MaxImageBytes > 0 && st.Size() > opt.MaxImageBytes {
|
||||||
|
return ImagePayload{}, meta, fmt.Errorf("file size %d exceeds max_image_bytes %d", st.Size(), opt.MaxImageBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgW, cfgH, format, err := imageDimensions(path)
|
||||||
|
if err != nil {
|
||||||
|
return ImagePayload{}, meta, err
|
||||||
|
}
|
||||||
|
meta.OriginalWidth = cfgW
|
||||||
|
meta.OriginalHeight = cfgH
|
||||||
|
|
||||||
|
maxDim := opt.MaxDimension
|
||||||
|
if maxDim <= 0 {
|
||||||
|
maxDim = 2048
|
||||||
|
}
|
||||||
|
maxPayload := opt.MaxPayloadBytes
|
||||||
|
if maxPayload <= 0 {
|
||||||
|
maxPayload = 512 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload, meta, ok, err := tryPassthrough(path, st.Size(), cfgW, cfgH, format, opt, maxDim, maxPayload); ok {
|
||||||
|
return payload, meta, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return compressWithImaging(path, opt, maxDim, maxPayload, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryPassthrough(path string, size int64, w, h int, format string, opt PreprocessOptions, maxDim int, maxPayload int64) (ImagePayload, PreprocessMeta, bool, error) {
|
||||||
|
var meta PreprocessMeta
|
||||||
|
meta.OriginalPath = path
|
||||||
|
meta.OriginalBytes = size
|
||||||
|
meta.OriginalWidth = w
|
||||||
|
meta.OriginalHeight = h
|
||||||
|
|
||||||
|
threshold := opt.SkipPreprocessBelowBytes
|
||||||
|
if threshold <= 0 {
|
||||||
|
return ImagePayload{}, meta, false, nil
|
||||||
|
}
|
||||||
|
if size > threshold {
|
||||||
|
return ImagePayload{}, meta, false, nil
|
||||||
|
}
|
||||||
|
longEdge := w
|
||||||
|
if h > longEdge {
|
||||||
|
longEdge = h
|
||||||
|
}
|
||||||
|
if longEdge > maxDim {
|
||||||
|
return ImagePayload{}, meta, false, nil
|
||||||
|
}
|
||||||
|
if size > maxPayload {
|
||||||
|
return ImagePayload{}, meta, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return ImagePayload{}, meta, false, err
|
||||||
|
}
|
||||||
|
mime := mimeFromImageFormat(format)
|
||||||
|
if mime == "" {
|
||||||
|
return ImagePayload{}, meta, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.OutputWidth = w
|
||||||
|
meta.OutputHeight = h
|
||||||
|
meta.OutputBytes = len(raw)
|
||||||
|
meta.OutputMIMEType = mime
|
||||||
|
meta.PreprocessMode = "passthrough"
|
||||||
|
return ImagePayload{Bytes: raw, MIMEType: mime}, meta, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressWithImaging(path string, opt PreprocessOptions, maxDim int, maxPayload int64, meta PreprocessMeta) (ImagePayload, PreprocessMeta, error) {
|
||||||
|
src, err := imaging.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return ImagePayload{}, meta, fmt.Errorf("open image: %w", err)
|
||||||
|
}
|
||||||
|
bounds := src.Bounds()
|
||||||
|
meta.OriginalWidth = bounds.Dx()
|
||||||
|
meta.OriginalHeight = bounds.Dy()
|
||||||
|
|
||||||
|
dst := imaging.Fit(src, maxDim, maxDim, imaging.Lanczos)
|
||||||
|
outBounds := dst.Bounds()
|
||||||
|
meta.OutputWidth = outBounds.Dx()
|
||||||
|
meta.OutputHeight = outBounds.Dy()
|
||||||
|
|
||||||
|
quality := opt.JPEGQuality
|
||||||
|
if quality <= 0 || quality > 100 {
|
||||||
|
quality = 82
|
||||||
|
}
|
||||||
|
|
||||||
|
dim := maxDim
|
||||||
|
for attempt := 0; attempt < 6; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
dim = int(float64(dim) * 0.85)
|
||||||
|
if dim < 256 {
|
||||||
|
dim = 256
|
||||||
|
}
|
||||||
|
dst = imaging.Fit(src, dim, dim, imaging.Lanczos)
|
||||||
|
outBounds = dst.Bounds()
|
||||||
|
meta.OutputWidth = outBounds.Dx()
|
||||||
|
meta.OutputHeight = outBounds.Dy()
|
||||||
|
}
|
||||||
|
q := quality
|
||||||
|
for q >= 60 {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := imaging.Encode(&buf, dst, imaging.JPEG, imaging.JPEGQuality(q)); err != nil {
|
||||||
|
return ImagePayload{}, meta, fmt.Errorf("encode jpeg: %w", err)
|
||||||
|
}
|
||||||
|
if int64(buf.Len()) <= maxPayload {
|
||||||
|
meta.JPEGQuality = q
|
||||||
|
meta.OutputBytes = buf.Len()
|
||||||
|
meta.OutputMIMEType = "image/jpeg"
|
||||||
|
meta.PreprocessMode = "jpeg"
|
||||||
|
return ImagePayload{Bytes: buf.Bytes(), MIMEType: "image/jpeg"}, meta, nil
|
||||||
|
}
|
||||||
|
q -= 5
|
||||||
|
}
|
||||||
|
quality = 75
|
||||||
|
}
|
||||||
|
return ImagePayload{}, meta, fmt.Errorf("could not compress image under max_payload_bytes %d", maxPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageDimensions(path string) (w, h int, format string, err error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
cfg, format, err := image.DecodeConfig(f)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, "", fmt.Errorf("decode image config: %w", err)
|
||||||
|
}
|
||||||
|
return cfg.Width, cfg.Height, format, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mimeFromImageFormat(format string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(format)) {
|
||||||
|
case "jpeg", "jpg":
|
||||||
|
return "image/jpeg"
|
||||||
|
case "png":
|
||||||
|
return "image/png"
|
||||||
|
case "gif":
|
||||||
|
return "image/gif"
|
||||||
|
case "webp":
|
||||||
|
return "image/webp"
|
||||||
|
case "bmp":
|
||||||
|
return "image/bmp"
|
||||||
|
case "tiff":
|
||||||
|
return "image/tiff"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeImageConfig 用于测试:确认文件可被解码。
|
||||||
|
func DecodeImageConfig(path string) (image.Config, string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return image.Config{}, "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return image.DecodeConfig(f)
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPreprocessImageFile_scalesAndLimitsPayload(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "big.png")
|
||||||
|
img := imaging.New(3000, 2000, color.White)
|
||||||
|
if err := imaging.Save(img, path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, meta, err := PreprocessImageFile(path, PreprocessOptions{
|
||||||
|
MaxImageBytes: 10 * 1024 * 1024,
|
||||||
|
MaxDimension: 1024,
|
||||||
|
JPEGQuality: 85,
|
||||||
|
MaxPayloadBytes: 600 * 1024,
|
||||||
|
SkipPreprocessBelowBytes: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(out.Bytes) == 0 {
|
||||||
|
t.Fatal("empty output")
|
||||||
|
}
|
||||||
|
if meta.PreprocessMode != "jpeg" {
|
||||||
|
t.Fatalf("mode: %s", meta.PreprocessMode)
|
||||||
|
}
|
||||||
|
if meta.OutputWidth > 1024 || meta.OutputHeight > 1024 {
|
||||||
|
t.Fatalf("expected fit within 1024, got %dx%d", meta.OutputWidth, meta.OutputHeight)
|
||||||
|
}
|
||||||
|
if int64(len(out.Bytes)) > 600*1024 {
|
||||||
|
t.Fatalf("payload %d exceeds max", len(out.Bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreprocessImageFile_passthroughSmallPNG(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "small.png")
|
||||||
|
if err := imaging.Save(imaging.New(400, 300, color.White), path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, meta, err := PreprocessImageFile(path, PreprocessOptions{
|
||||||
|
MaxImageBytes: 5 * 1024 * 1024,
|
||||||
|
MaxDimension: 2048,
|
||||||
|
MaxPayloadBytes: 512 * 1024,
|
||||||
|
SkipPreprocessBelowBytes: 2 * 1024 * 1024,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if meta.PreprocessMode != "passthrough" {
|
||||||
|
t.Fatalf("expected passthrough, got %s", meta.PreprocessMode)
|
||||||
|
}
|
||||||
|
if out.MIMEType != "image/png" {
|
||||||
|
t.Fatalf("mime: %s", out.MIMEType)
|
||||||
|
}
|
||||||
|
if meta.OutputWidth != 400 || meta.OutputHeight != 300 {
|
||||||
|
t.Fatalf("dims: %dx%d", meta.OutputWidth, meta.OutputHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreprocessImageFile_passthroughDisabled(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "small.png")
|
||||||
|
if err := imaging.Save(imaging.New(100, 100, color.White), path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, meta, err := PreprocessImageFile(path, PreprocessOptions{
|
||||||
|
MaxDimension: 2048,
|
||||||
|
MaxPayloadBytes: 512 * 1024,
|
||||||
|
SkipPreprocessBelowBytes: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if meta.PreprocessMode != "jpeg" {
|
||||||
|
t.Fatalf("expected jpeg compress, got %s", meta.PreprocessMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreprocessImageFile_rejectsOversizeFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "tiny.png")
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := png.Encode(f, image.NewRGBA(image.Rect(0, 0, 2, 2))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
_, _, err = PreprocessImageFile(path, PreprocessOptions{MaxImageBytes: 1})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when file exceeds max_image_bytes")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
"cyberstrike-ai/internal/mcp"
|
||||||
|
"cyberstrike-ai/internal/mcp/builtin"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterAnalyzeImageTool 在 vision.enabled 且 model 已配置时注册 MCP 工具 analyze_image。
|
||||||
|
func RegisterAnalyzeImageTool(mcpServer *mcp.Server, cfg *config.Config, logger *zap.Logger) {
|
||||||
|
if mcpServer == nil || cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cfg.Vision.Ready() {
|
||||||
|
if cfg.Vision.Enabled && logger != nil {
|
||||||
|
logger.Warn("vision.enabled 但 vision.model 为空,跳过注册 analyze_image")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Warn("vision: getwd failed, skip analyze_image", zap.Error(err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preOpt := PreprocessOptions{
|
||||||
|
MaxImageBytes: cfg.Vision.MaxImageBytesEffective(),
|
||||||
|
MaxDimension: cfg.Vision.MaxDimensionEffective(),
|
||||||
|
JPEGQuality: cfg.Vision.JPEGQualityEffective(),
|
||||||
|
MaxPayloadBytes: cfg.Vision.MaxPayloadBytesEffective(),
|
||||||
|
SkipPreprocessBelowBytes: cfg.Vision.SkipPreprocessBelowBytesEffective(),
|
||||||
|
}
|
||||||
|
client := NewClient(cfg.Vision, cfg.OpenAI)
|
||||||
|
|
||||||
|
tool := mcp.Tool{
|
||||||
|
Name: builtin.ToolAnalyzeImage,
|
||||||
|
Description: "分析服务器上的本地图片并返回文字描述(验证码、UI 元素、报错、架构图要点等)。" +
|
||||||
|
"输入为文件路径(如用户上传的 chat_uploads 路径或工具截图路径)。" +
|
||||||
|
"输出仅为文本,不含图片数据。不要对二进制图片使用 read_file 指望理解内容。",
|
||||||
|
ShortDescription: "分析本地图片并返回文字描述(验证码/UI/报错等)",
|
||||||
|
InputSchema: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"path": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "图片绝对路径或相对于进程工作目录的路径",
|
||||||
|
},
|
||||||
|
"question": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "可选:希望模型重点回答的问题。验证码图建议:只输出验证码字符,不要空格和解释",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"path"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
|
||||||
|
path, _ := args["path"].(string)
|
||||||
|
question, _ := args["question"].(string)
|
||||||
|
|
||||||
|
abs, err := ResolveImagePath(path, cwd)
|
||||||
|
if err != nil {
|
||||||
|
return textResult(fmt.Sprintf("路径校验失败: %v", err), true), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
img, meta, err := PreprocessImageFile(abs, preOpt)
|
||||||
|
if err != nil {
|
||||||
|
return textResult(fmt.Sprintf("图片预处理失败: %v", err), true), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := client.Analyze(ctx, img, question)
|
||||||
|
if err != nil {
|
||||||
|
return textResult(fmt.Sprintf("视觉模型调用失败: %v", err), true), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body := formatAnalysisResult(abs, meta, summary)
|
||||||
|
return textResult(body, false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpServer.RegisterTool(tool, handler)
|
||||||
|
if logger != nil {
|
||||||
|
logger.Info("vision: analyze_image 工具已注册", zap.String("model", cfg.Vision.Model))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textResult(text string, isError bool) *mcp.ToolResult {
|
||||||
|
return &mcp.ToolResult{
|
||||||
|
Content: []mcp.Content{{Type: "text", Text: text}},
|
||||||
|
IsError: isError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAnalysisResult(path string, meta PreprocessMeta, summary string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("## Image analysis\n")
|
||||||
|
b.WriteString("- **path**: ")
|
||||||
|
b.WriteString(path)
|
||||||
|
b.WriteString("\n")
|
||||||
|
switch meta.PreprocessMode {
|
||||||
|
case "passthrough":
|
||||||
|
b.WriteString(fmt.Sprintf("- **preprocess**: passthrough %dx%d, %s, %dKB (original %dKB)\n\n",
|
||||||
|
meta.OutputWidth, meta.OutputHeight, meta.OutputMIMEType,
|
||||||
|
(meta.OutputBytes+1023)/1024, (meta.OriginalBytes+1023)/1024))
|
||||||
|
default:
|
||||||
|
b.WriteString(fmt.Sprintf("- **preprocess**: %dx%d → %dx%d, jpeg q=%d, %dKB (original %dKB)\n\n",
|
||||||
|
meta.OriginalWidth, meta.OriginalHeight,
|
||||||
|
meta.OutputWidth, meta.OutputHeight,
|
||||||
|
meta.JPEGQuality, (meta.OutputBytes+1023)/1024,
|
||||||
|
(meta.OriginalBytes+1023)/1024))
|
||||||
|
}
|
||||||
|
b.WriteString("### Summary\n")
|
||||||
|
b.WriteString(strings.TrimSpace(summary))
|
||||||
|
b.WriteString("\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
- Configure **Host / Port / HTTPS / Password** and choose an agent mode
|
- Configure **Host / Port / HTTPS / Password** and choose an agent mode
|
||||||
- Click **Validate** to login (`POST /api/auth/login`) and verify token (`GET /api/auth/validate`)
|
- Click **Validate** to login (`POST /api/auth/login`) and verify token (`GET /api/auth/validate`)
|
||||||
- Right-click any HTTP message in Burp and send it to CyberStrikeAI for **streaming web pentest**
|
- Right-click any HTTP message in Burp and send it to CyberStrikeAI for **streaming web pentest** (agent modes: **Eino Single**, Deep, Plan-Execute, Supervisor — maps to `/api/eino-agent/stream` or `/api/multi-agent/stream`)
|
||||||
- Keep a **test history sidebar** (searchable) so you can revisit previous runs
|
- Keep a **test history sidebar** (searchable) so you can revisit previous runs
|
||||||
- Output is split into **collapsible Progress** + **Final Response** (Markdown rendering supported)
|
- Output is split into **collapsible Progress** + **Final Response** (Markdown rendering supported)
|
||||||
- View captured **Request / Response** for each run
|
- View captured **Request / Response** for each run
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
- 右键任意 HTTP 请求包 → **Send to CyberStrikeAI (stream test)**:
|
- 右键任意 HTTP 请求包 → **Send to CyberStrikeAI (stream test)**:
|
||||||
- 将该 HTTP 请求(含 headers/body;若存在响应则附带截断片段)发送到 CyberStrikeAI
|
- 将该 HTTP 请求(含 headers/body;若存在响应则附带截断片段)发送到 CyberStrikeAI
|
||||||
- 以 **SSE 流式**接收返回内容,并在标签页中实时展示
|
- 以 **SSE 流式**接收返回内容,并在标签页中实时展示
|
||||||
- 单 Agent:`POST /api/agent-loop/stream`
|
- 单 Agent:`POST /api/eino-agent/stream`
|
||||||
- 多 Agent:`POST /api/multi-agent/stream`(需要服务端启用 `multi_agent.enabled: true`)
|
- 多 Agent:`POST /api/multi-agent/stream`(需 `multi_agent.enabled: true`,请求体 `orchestration`)
|
||||||
- **测试历史侧边栏(可搜索)**:每次发送都会新增一条记录,方便回看与对比
|
- **测试历史侧边栏(可搜索)**:每次发送都会新增一条记录,方便回看与对比
|
||||||
- **Output 分区**:`Progress`(可折叠)+ `Final Response`(主区域)
|
- **Output 分区**:`Progress`(可折叠)+ `Final Response`(主区域)
|
||||||
- **Markdown 渲染**:最终输出可在 Output 主区域渲染为富文本(可开关)
|
- **Markdown 渲染**:最终输出可在 Output 主区域渲染为富文本(可开关)
|
||||||
|
|||||||
-1
@@ -38,7 +38,6 @@ final class CyberStrikeAIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum AgentMode {
|
enum AgentMode {
|
||||||
NATIVE_REACT("Native ReAct", "/api/agent-loop/stream", null),
|
|
||||||
EINO_SINGLE("Eino Single (ADK)", "/api/eino-agent/stream", null),
|
EINO_SINGLE("Eino Single (ADK)", "/api/eino-agent/stream", null),
|
||||||
DEEP("Deep (DeepAgent)", "/api/multi-agent/stream", "deep"),
|
DEEP("Deep (DeepAgent)", "/api/multi-agent/stream", "deep"),
|
||||||
PLAN_EXECUTE("Plan-Execute", "/api/multi-agent/stream", "plan_execute"),
|
PLAN_EXECUTE("Plan-Execute", "/api/multi-agent/stream", "plan_execute"),
|
||||||
|
|||||||
+11
-4
@@ -16,9 +16,16 @@ final class CyberStrikeAITab implements ITab {
|
|||||||
private final JTextField portField = new JTextField("8080");
|
private final JTextField portField = new JTextField("8080");
|
||||||
private final JCheckBox useHttpsBox = new JCheckBox("HTTPS", true);
|
private final JCheckBox useHttpsBox = new JCheckBox("HTTPS", true);
|
||||||
private final JPasswordField passwordField = new JPasswordField();
|
private final JPasswordField passwordField = new JPasswordField();
|
||||||
private final JComboBox<String> agentModeBox = new JComboBox<>(new String[]{
|
private final JComboBox<String> agentModeBox = new JComboBox<>(agentModeLabels());
|
||||||
"Native ReAct", "Eino Single (ADK)", "Deep (DeepAgent)", "Plan-Execute", "Supervisor"
|
private static String[] agentModeLabels() {
|
||||||
});
|
CyberStrikeAIClient.AgentMode[] modes = CyberStrikeAIClient.AgentMode.values();
|
||||||
|
String[] labels = new String[modes.length];
|
||||||
|
for (int i = 0; i < modes.length; i++) {
|
||||||
|
labels[i] = modes[i].displayName;
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
private final JButton validateButton = new JButton("Validate");
|
private final JButton validateButton = new JButton("Validate");
|
||||||
private final JButton clearButton = new JButton("Clear Output");
|
private final JButton clearButton = new JButton("Clear Output");
|
||||||
private final JButton stopButton = new JButton("Stop");
|
private final JButton stopButton = new JButton("Stop");
|
||||||
@@ -554,7 +561,7 @@ final class CyberStrikeAITab implements ITab {
|
|||||||
int idx = agentModeBox.getSelectedIndex();
|
int idx = agentModeBox.getSelectedIndex();
|
||||||
CyberStrikeAIClient.AgentMode mode = (idx >= 0 && idx < AGENT_MODES.length)
|
CyberStrikeAIClient.AgentMode mode = (idx >= 0 && idx < AGENT_MODES.length)
|
||||||
? AGENT_MODES[idx]
|
? AGENT_MODES[idx]
|
||||||
: CyberStrikeAIClient.AgentMode.NATIVE_REACT;
|
: CyberStrikeAIClient.AgentMode.EINO_SINGLE;
|
||||||
return new CyberStrikeAIClient.Config(baseUrl, password, mode);
|
return new CyberStrikeAIClient.Config(baseUrl, password, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1136
-714
File diff suppressed because it is too large
Load Diff
@@ -501,8 +501,6 @@
|
|||||||
"historyGroupEarlier": "Older",
|
"historyGroupEarlier": "Older",
|
||||||
"agentModeSelectAria": "Choose conversation execution mode",
|
"agentModeSelectAria": "Choose conversation execution mode",
|
||||||
"agentModePanelTitle": "Conversation mode",
|
"agentModePanelTitle": "Conversation mode",
|
||||||
"agentModeReactNative": "Native ReAct",
|
|
||||||
"agentModeReactNativeHint": "Classic single-agent ReAct with MCP tools",
|
|
||||||
"agentModeEinoSingle": "Eino single (ADK)",
|
"agentModeEinoSingle": "Eino single (ADK)",
|
||||||
"agentModeEinoSingleHint": "Eino ChatModelAgent + Runner with MCP tools (/api/eino-agent)",
|
"agentModeEinoSingleHint": "Eino ChatModelAgent + Runner with MCP tools (/api/eino-agent)",
|
||||||
"agentModeDeep": "Deep (DeepAgent)",
|
"agentModeDeep": "Deep (DeepAgent)",
|
||||||
@@ -513,7 +511,7 @@
|
|||||||
"agentModeSupervisorHint": "Supervisor coordinates via transfer to sub-agents",
|
"agentModeSupervisorHint": "Supervisor coordinates via transfer to sub-agents",
|
||||||
"agentModeSingle": "Single-agent",
|
"agentModeSingle": "Single-agent",
|
||||||
"agentModeMulti": "Multi-agent",
|
"agentModeMulti": "Multi-agent",
|
||||||
"agentModeSingleHint": "Single-model ReAct loop for chat and tool use",
|
"agentModeSingleHint": "Eino ADK single-agent for chat and tool use",
|
||||||
"agentModeMultiHint": "Eino prebuilt orchestration (deep / plan_execute / supervisor) for complex tasks",
|
"agentModeMultiHint": "Eino prebuilt orchestration (deep / plan_execute / supervisor) for complex tasks",
|
||||||
"reasoningModeLabel": "Model reasoning",
|
"reasoningModeLabel": "Model reasoning",
|
||||||
"reasoningEffortLabel": "Reasoning effort",
|
"reasoningEffortLabel": "Reasoning effort",
|
||||||
@@ -1501,9 +1499,15 @@
|
|||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"noStatsData": "No statistical data",
|
"noStatsData": "No statistical data",
|
||||||
"noExecutions": "No execution records",
|
"noExecutions": "No execution records",
|
||||||
|
"emptyHint": "Execution records will appear here after you invoke MCP tools in chat or tasks",
|
||||||
"noRecordsWithFilter": "No records with current filter",
|
"noRecordsWithFilter": "No records with current filter",
|
||||||
"paginationInfo": "Show {{start}}-{{end}} of {{total}} records",
|
"paginationInfo": "Show {{start}}-{{end}} of {{total}} records",
|
||||||
"perPageLabel": "Per page",
|
"perPageLabel": "Per page",
|
||||||
|
"firstPage": "First",
|
||||||
|
"prevPage": "Previous",
|
||||||
|
"nextPage": "Next",
|
||||||
|
"lastPage": "Last",
|
||||||
|
"pageInfo": "Page {{page}} of {{total}}",
|
||||||
"loadStatsError": "Failed to load statistics",
|
"loadStatsError": "Failed to load statistics",
|
||||||
"loadExecutionsError": "Failed to load execution records",
|
"loadExecutionsError": "Failed to load execution records",
|
||||||
"totalCalls": "Total calls",
|
"totalCalls": "Total calls",
|
||||||
@@ -1516,6 +1520,17 @@
|
|||||||
"unknownTool": "Unknown tool",
|
"unknownTool": "Unknown tool",
|
||||||
"successFailedRate": "Success {{success}} / Failed {{failed}} · {{rate}}% success rate",
|
"successFailedRate": "Success {{success}} / Failed {{failed}} · {{rate}}% success rate",
|
||||||
"topToolsTitle": "Top {{n}} tools by calls",
|
"topToolsTitle": "Top {{n}} tools by calls",
|
||||||
|
"toolRankingTitle": "Tool call ranking",
|
||||||
|
"toolStatsTitle": "Tool statistics",
|
||||||
|
"toolStatsHint": "Click a bar segment or row to filter records below; hover to highlight",
|
||||||
|
"scopeCumulative": "All time",
|
||||||
|
"scopeTimeline": "Trend period",
|
||||||
|
"filterActive": "Filtered: {{tool}}",
|
||||||
|
"kpiScopeNote": "Lifetime totals",
|
||||||
|
"columnCalls": "Calls",
|
||||||
|
"columnShare": "Share",
|
||||||
|
"columnSuccessRate": "Success rate",
|
||||||
|
"rankingSummary": "Top {{n}} {{pct}}% · {{total}} total calls",
|
||||||
"barVolumeLegend": "Bar length: relative call volume; green/red: success vs failure share",
|
"barVolumeLegend": "Bar length: relative call volume; green/red: success vs failure share",
|
||||||
"clickToFilterTool": "Click a row to filter records below",
|
"clickToFilterTool": "Click a row to filter records below",
|
||||||
"toolRowAriaLabel": "{{name}}, {{total}} calls, {{rate}}% success rate. Click to filter records.",
|
"toolRowAriaLabel": "{{name}}, {{total}} calls, {{rate}}% success rate. Click to filter records.",
|
||||||
@@ -1528,9 +1543,21 @@
|
|||||||
"rateWarning": "Some failures detected",
|
"rateWarning": "Some failures detected",
|
||||||
"rateCritical": "High failure rate",
|
"rateCritical": "High failure rate",
|
||||||
"statsSubtitle": "Refreshed {{time}} · {{count}} tools",
|
"statsSubtitle": "Refreshed {{time}} · {{count}} tools",
|
||||||
|
"timelineTitle": "Call trend",
|
||||||
|
"timelineHint": "All tools combined (not split by tool)",
|
||||||
|
"timelineRange24h": "24h",
|
||||||
|
"timelineRange7d": "7d",
|
||||||
|
"timelineRange30d": "30d",
|
||||||
|
"timelineSummary": "{{total}} calls in range · peak {{peak}}",
|
||||||
|
"timelineSparseHint": "Most buckets are empty; peak {{peak}} calls at {{peakTime}}",
|
||||||
|
"timelineNoData": "No calls in this period",
|
||||||
|
"timelineLoadError": "Failed to load call trend",
|
||||||
|
"timelineTotalLegend": "Total calls",
|
||||||
|
"timelineFailedLegend": "Failed",
|
||||||
|
"timelineTooltip": "{{time}}: {{total}} calls ({{failed}} failed)",
|
||||||
"distTitle": "Call distribution",
|
"distTitle": "Call distribution",
|
||||||
"distLegend": "Slice area shows share of all calls",
|
"distLegend": "Slice area shows share of all calls",
|
||||||
"distClickHint": "Click legend or slice to filter records",
|
"distClickHint": "Click a bar segment to filter records",
|
||||||
"distHeaderHint": "{{n}} total calls",
|
"distHeaderHint": "{{n}} total calls",
|
||||||
"distSegmentAria": "{{name}}, {{pct}}% of calls, {{calls}} times",
|
"distSegmentAria": "{{name}}, {{pct}}% of calls, {{calls}} times",
|
||||||
"distOthersNoFilter": "Other tools cannot be filtered individually",
|
"distOthersNoFilter": "Other tools cannot be filtered individually",
|
||||||
@@ -1760,6 +1787,12 @@
|
|||||||
"loadListFailed": "Failed to load",
|
"loadListFailed": "Failed to load",
|
||||||
"noRecords": "No vulnerability records",
|
"noRecords": "No vulnerability records",
|
||||||
"batchExport": "Batch export",
|
"batchExport": "Batch export",
|
||||||
|
"batchDelete": "Batch delete",
|
||||||
|
"batchDeleteNoResults": "No vulnerabilities match the current filters to delete",
|
||||||
|
"batchDeleteConfirm": "Delete {{count}} vulnerability record(s) matching the current filters? This cannot be undone.",
|
||||||
|
"batchDeleteConfirmAll": "No filters are set. This will delete all {{count}} vulnerability record(s). This cannot be undone. Continue?",
|
||||||
|
"batchDeleteSuccess": "Successfully deleted {{count}} vulnerability record(s)",
|
||||||
|
"batchDeleteFailed": "Batch delete failed",
|
||||||
"downloadMarkdownTitle": "Download Markdown",
|
"downloadMarkdownTitle": "Download Markdown",
|
||||||
"exportNoResults": "No vulnerabilities match the current filters",
|
"exportNoResults": "No vulnerabilities match the current filters",
|
||||||
"exportStarted": "Started downloading {{count}} file(s)",
|
"exportStarted": "Started downloading {{count}} file(s)",
|
||||||
@@ -1838,7 +1871,7 @@
|
|||||||
"descPlaceholder": "When the orchestrator should delegate to this agent",
|
"descPlaceholder": "When the orchestrator should delegate to this agent",
|
||||||
"fieldTools": "Tools (comma-separated; same keys as role tools)",
|
"fieldTools": "Tools (comma-separated; same keys as role tools)",
|
||||||
"fieldBindRole": "Bind role (optional)",
|
"fieldBindRole": "Bind role (optional)",
|
||||||
"fieldMaxIter": "Max sub-agent iterations (0 = use global default)",
|
"fieldMaxIter": "Max iterations (0 = use Settings → agent.max_iterations)",
|
||||||
"fieldInstruction": "System prompt (Markdown body)",
|
"fieldInstruction": "System prompt (Markdown body)",
|
||||||
"instructionPlaceholder": "You are a specialist agent...",
|
"instructionPlaceholder": "You are a specialist agent...",
|
||||||
"nameRequired": "Display name is required",
|
"nameRequired": "Display name is required",
|
||||||
@@ -1959,6 +1992,24 @@
|
|||||||
"retryDelay": "Retry delay (ms)",
|
"retryDelay": "Retry delay (ms)",
|
||||||
"retryDelayPlaceholder": "1000",
|
"retryDelayPlaceholder": "1000",
|
||||||
"retryDelayHint": "Delay between retries (ms)",
|
"retryDelayHint": "Delay between retries (ms)",
|
||||||
|
"visionConfig": "Vision analysis (analyze_image)",
|
||||||
|
"visionEnabled": "Enable analyze_image vision tool",
|
||||||
|
"visionEnabledHint": "Registers the MCP tool when enabled; images are sent only for one VL call; agent context keeps text summaries only. Save & apply to take effect.",
|
||||||
|
"visionBaseUrlPlaceholder": "Leave empty to reuse OpenAI Base URL",
|
||||||
|
"visionApiKeyPlaceholder": "Leave empty to reuse OpenAI API Key",
|
||||||
|
"visionModel": "Vision model",
|
||||||
|
"visionModelPlaceholder": "qwen-vl-max",
|
||||||
|
"visionModelRequired": "Vision model name is required when vision is enabled",
|
||||||
|
"visionAdvanced": "Advanced: preprocessing & limits",
|
||||||
|
"visionMaxImageBytes": "Max original file size (bytes)",
|
||||||
|
"visionMaxDimension": "Max long-edge pixels",
|
||||||
|
"visionJpegQuality": "JPEG quality",
|
||||||
|
"visionMaxPayloadBytes": "Max API payload (bytes)",
|
||||||
|
"visionSkipPreprocessBytes": "Passthrough below (bytes)",
|
||||||
|
"visionSkipPreprocessHint": "0 = always JPEG compress; must also fit long-edge and payload limits.",
|
||||||
|
"visionDetail": "Image detail",
|
||||||
|
"visionTimeout": "Timeout (seconds)",
|
||||||
|
"visionTestFillRequired": "Enter vision model and ensure API Key is available (or reuse OpenAI)",
|
||||||
"testConnection": "Test Connection",
|
"testConnection": "Test Connection",
|
||||||
"testFillRequired": "Please fill in API Key and Model first",
|
"testFillRequired": "Please fill in API Key and Model first",
|
||||||
"testing": "Testing connection...",
|
"testing": "Testing connection...",
|
||||||
@@ -2289,9 +2340,9 @@
|
|||||||
"projectNone": "(Unbound)",
|
"projectNone": "(Unbound)",
|
||||||
"projectHint": "Optionally bind this queue to a project; leave empty to keep it unbound.",
|
"projectHint": "Optionally bind this queue to a project; leave empty to keep it unbound.",
|
||||||
"agentMode": "Agent mode",
|
"agentMode": "Agent mode",
|
||||||
"agentModeSingle": "Single-agent (ReAct)",
|
"agentModeSingle": "Single-agent (Eino ADK)",
|
||||||
"agentModeMulti": "Multi-agent (Eino)",
|
"agentModeMulti": "Multi-agent (Eino)",
|
||||||
"agentModeHint": "Same as chat: native ReAct, Eino single-agent (ADK), or Deep / Plan-Execute / Supervisor (the last three require multi-agent enabled).",
|
"agentModeHint": "Same as chat: Eino single-agent (ADK), or Deep / Plan-Execute / Supervisor (last three require multi_agent.enabled).",
|
||||||
"scheduleMode": "Schedule mode",
|
"scheduleMode": "Schedule mode",
|
||||||
"scheduleModeManual": "Manual",
|
"scheduleModeManual": "Manual",
|
||||||
"scheduleModeCron": "Cron expression",
|
"scheduleModeCron": "Cron expression",
|
||||||
|
|||||||
@@ -490,8 +490,6 @@
|
|||||||
"historyGroupEarlier": "更早",
|
"historyGroupEarlier": "更早",
|
||||||
"agentModeSelectAria": "选择对话执行模式",
|
"agentModeSelectAria": "选择对话执行模式",
|
||||||
"agentModePanelTitle": "对话模式",
|
"agentModePanelTitle": "对话模式",
|
||||||
"agentModeReactNative": "原生 ReAct 模式",
|
|
||||||
"agentModeReactNativeHint": "经典单代理 ReAct 与 MCP 工具",
|
|
||||||
"agentModeEinoSingle": "Eino 单代理(ADK)",
|
"agentModeEinoSingle": "Eino 单代理(ADK)",
|
||||||
"agentModeEinoSingleHint": "Eino ChatModelAgent + Runner,MCP 工具(/api/eino-agent)",
|
"agentModeEinoSingleHint": "Eino ChatModelAgent + Runner,MCP 工具(/api/eino-agent)",
|
||||||
"agentModeDeep": "Deep(DeepAgent)",
|
"agentModeDeep": "Deep(DeepAgent)",
|
||||||
@@ -502,7 +500,7 @@
|
|||||||
"agentModeSupervisorHint": "监督者协调,transfer 委派子代理",
|
"agentModeSupervisorHint": "监督者协调,transfer 委派子代理",
|
||||||
"agentModeSingle": "单代理",
|
"agentModeSingle": "单代理",
|
||||||
"agentModeMulti": "多代理",
|
"agentModeMulti": "多代理",
|
||||||
"agentModeSingleHint": "单模型 ReAct 循环,适合常规对话与工具调用",
|
"agentModeSingleHint": "Eino ADK 单代理,适合常规对话与工具调用",
|
||||||
"agentModeMultiHint": "Eino 预置编排(deep / plan_execute / supervisor),适合复杂任务",
|
"agentModeMultiHint": "Eino 预置编排(deep / plan_execute / supervisor),适合复杂任务",
|
||||||
"reasoningModeLabel": "模型推理",
|
"reasoningModeLabel": "模型推理",
|
||||||
"reasoningEffortLabel": "推理强度",
|
"reasoningEffortLabel": "推理强度",
|
||||||
@@ -1490,9 +1488,15 @@
|
|||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"noStatsData": "暂无统计数据",
|
"noStatsData": "暂无统计数据",
|
||||||
"noExecutions": "暂无执行记录",
|
"noExecutions": "暂无执行记录",
|
||||||
|
"emptyHint": "在对话或任务中调用 MCP 工具后,执行记录将显示在此处",
|
||||||
"noRecordsWithFilter": "当前筛选条件下暂无记录",
|
"noRecordsWithFilter": "当前筛选条件下暂无记录",
|
||||||
"paginationInfo": "显示 {{start}}-{{end}} / 共 {{total}} 条记录",
|
"paginationInfo": "显示 {{start}}-{{end}} / 共 {{total}} 条记录",
|
||||||
"perPageLabel": "每页显示",
|
"perPageLabel": "每页显示",
|
||||||
|
"firstPage": "首页",
|
||||||
|
"prevPage": "上一页",
|
||||||
|
"nextPage": "下一页",
|
||||||
|
"lastPage": "末页",
|
||||||
|
"pageInfo": "第 {{page}} / {{total}} 页",
|
||||||
"loadStatsError": "无法加载统计信息",
|
"loadStatsError": "无法加载统计信息",
|
||||||
"loadExecutionsError": "无法加载执行记录",
|
"loadExecutionsError": "无法加载执行记录",
|
||||||
"totalCalls": "总调用次数",
|
"totalCalls": "总调用次数",
|
||||||
@@ -1505,6 +1509,17 @@
|
|||||||
"unknownTool": "未知工具",
|
"unknownTool": "未知工具",
|
||||||
"successFailedRate": "成功 {{success}} / 失败 {{failed}} · 成功率 {{rate}}%",
|
"successFailedRate": "成功 {{success}} / 失败 {{failed}} · 成功率 {{rate}}%",
|
||||||
"topToolsTitle": "工具调用 Top {{n}}",
|
"topToolsTitle": "工具调用 Top {{n}}",
|
||||||
|
"toolRankingTitle": "工具调用排行",
|
||||||
|
"toolStatsTitle": "工具统计",
|
||||||
|
"toolStatsHint": "点击色条或列表行筛选下方执行记录;悬停联动高亮",
|
||||||
|
"scopeCumulative": "累计",
|
||||||
|
"scopeTimeline": "趋势时段",
|
||||||
|
"filterActive": "已筛选:{{tool}}",
|
||||||
|
"kpiScopeNote": "累计统计(全时段)",
|
||||||
|
"columnCalls": "调用",
|
||||||
|
"columnShare": "占比",
|
||||||
|
"columnSuccessRate": "成功率",
|
||||||
|
"rankingSummary": "Top {{n}} 占 {{pct}}% · 共 {{total}} 次调用",
|
||||||
"barVolumeLegend": "条长表示相对调用量,条内绿/红为成功/失败占比",
|
"barVolumeLegend": "条长表示相对调用量,条内绿/红为成功/失败占比",
|
||||||
"clickToFilterTool": "点击行筛选下方执行记录",
|
"clickToFilterTool": "点击行筛选下方执行记录",
|
||||||
"toolRowAriaLabel": "{{name}},{{total}} 次调用,成功率 {{rate}}%,点击查看执行记录",
|
"toolRowAriaLabel": "{{name}},{{total}} 次调用,成功率 {{rate}}%,点击查看执行记录",
|
||||||
@@ -1517,9 +1532,21 @@
|
|||||||
"rateWarning": "存在失败调用",
|
"rateWarning": "存在失败调用",
|
||||||
"rateCritical": "失败率偏高",
|
"rateCritical": "失败率偏高",
|
||||||
"statsSubtitle": "最后刷新 {{time}} · 共 {{count}} 个工具",
|
"statsSubtitle": "最后刷新 {{time}} · 共 {{count}} 个工具",
|
||||||
|
"timelineTitle": "调用趋势",
|
||||||
|
"timelineHint": "全部工具合计,不按工具拆分",
|
||||||
|
"timelineRange24h": "24 小时",
|
||||||
|
"timelineRange7d": "7 天",
|
||||||
|
"timelineRange30d": "30 天",
|
||||||
|
"timelineSummary": "区间内 {{total}} 次 · 峰值 {{peak}}",
|
||||||
|
"timelineSparseHint": "该时段多数时间为 0,峰值 {{peak}} 次出现在 {{peakTime}}",
|
||||||
|
"timelineNoData": "该时段暂无调用",
|
||||||
|
"timelineLoadError": "无法加载调用趋势",
|
||||||
|
"timelineTotalLegend": "总调用",
|
||||||
|
"timelineFailedLegend": "失败",
|
||||||
|
"timelineTooltip": "{{time}}:{{total}} 次(失败 {{failed}})",
|
||||||
"distTitle": "调用分布",
|
"distTitle": "调用分布",
|
||||||
"distLegend": "扇区面积为占全部调用比例",
|
"distLegend": "扇区面积为占全部调用比例",
|
||||||
"distClickHint": "点击图例或扇区筛选执行记录",
|
"distClickHint": "点击色条筛选执行记录",
|
||||||
"distHeaderHint": "共 {{n}} 次调用",
|
"distHeaderHint": "共 {{n}} 次调用",
|
||||||
"distSegmentAria": "{{name}},占 {{pct}}%,{{calls}} 次",
|
"distSegmentAria": "{{name}},占 {{pct}}%,{{calls}} 次",
|
||||||
"distOthersNoFilter": "其他工具无法单独筛选",
|
"distOthersNoFilter": "其他工具无法单独筛选",
|
||||||
@@ -1749,6 +1776,12 @@
|
|||||||
"loadListFailed": "加载失败",
|
"loadListFailed": "加载失败",
|
||||||
"noRecords": "暂无漏洞记录",
|
"noRecords": "暂无漏洞记录",
|
||||||
"batchExport": "批量导出",
|
"batchExport": "批量导出",
|
||||||
|
"batchDelete": "批量删除",
|
||||||
|
"batchDeleteNoResults": "当前筛选条件下没有可删除的漏洞",
|
||||||
|
"batchDeleteConfirm": "确定要删除当前筛选条件下的 {{count}} 条漏洞吗?此操作不可恢复。",
|
||||||
|
"batchDeleteConfirmAll": "未设置筛选条件,将删除全部 {{count}} 条漏洞。此操作不可恢复,确定继续?",
|
||||||
|
"batchDeleteSuccess": "成功删除 {{count}} 条漏洞",
|
||||||
|
"batchDeleteFailed": "批量删除失败",
|
||||||
"downloadMarkdownTitle": "下载 Markdown",
|
"downloadMarkdownTitle": "下载 Markdown",
|
||||||
"exportNoResults": "当前筛选条件下无可导出漏洞",
|
"exportNoResults": "当前筛选条件下无可导出漏洞",
|
||||||
"exportStarted": "已开始下载 {{count}} 份报告",
|
"exportStarted": "已开始下载 {{count}} 份报告",
|
||||||
@@ -1827,7 +1860,7 @@
|
|||||||
"descPlaceholder": "何时由协调者调度该子代理",
|
"descPlaceholder": "何时由协调者调度该子代理",
|
||||||
"fieldTools": "可用工具(逗号分隔,与角色工具 key 一致)",
|
"fieldTools": "可用工具(逗号分隔,与角色工具 key 一致)",
|
||||||
"fieldBindRole": "绑定角色(可选)",
|
"fieldBindRole": "绑定角色(可选)",
|
||||||
"fieldMaxIter": "子代理最大迭代(0=使用全局默认)",
|
"fieldMaxIter": "最大迭代(0=沿用设置页 agent.max_iterations)",
|
||||||
"fieldInstruction": "系统提示词(Markdown 正文)",
|
"fieldInstruction": "系统提示词(Markdown 正文)",
|
||||||
"instructionPlaceholder": "You are a specialist agent...",
|
"instructionPlaceholder": "You are a specialist agent...",
|
||||||
"nameRequired": "请填写显示名称",
|
"nameRequired": "请填写显示名称",
|
||||||
@@ -1948,6 +1981,24 @@
|
|||||||
"retryDelay": "重试间隔(毫秒)",
|
"retryDelay": "重试间隔(毫秒)",
|
||||||
"retryDelayPlaceholder": "1000",
|
"retryDelayPlaceholder": "1000",
|
||||||
"retryDelayHint": "重试间隔毫秒数(默认 1000),每次重试会递增延迟",
|
"retryDelayHint": "重试间隔毫秒数(默认 1000),每次重试会递增延迟",
|
||||||
|
"visionConfig": "视觉分析(analyze_image)",
|
||||||
|
"visionEnabled": "启用视觉分析工具 analyze_image",
|
||||||
|
"visionEnabledHint": "启用后注册 MCP 工具;图片仅在单次 VL 调用中出现,Agent 上下文只保留文字摘要。保存并应用后生效。",
|
||||||
|
"visionBaseUrlPlaceholder": "留空则复用 OpenAI Base URL",
|
||||||
|
"visionApiKeyPlaceholder": "留空则复用 OpenAI API Key",
|
||||||
|
"visionModel": "视觉模型",
|
||||||
|
"visionModelPlaceholder": "qwen-vl-max",
|
||||||
|
"visionModelRequired": "启用视觉分析时请填写视觉模型名称",
|
||||||
|
"visionAdvanced": "高级:预处理与限制",
|
||||||
|
"visionMaxImageBytes": "原始文件上限(字节)",
|
||||||
|
"visionMaxDimension": "长边缩放像素",
|
||||||
|
"visionJpegQuality": "JPEG 质量",
|
||||||
|
"visionMaxPayloadBytes": "送 API 体积上限(字节)",
|
||||||
|
"visionSkipPreprocessBytes": "低于该字节可原图直传",
|
||||||
|
"visionSkipPreprocessHint": "0 表示始终 JPEG 压缩;需同时满足长边与 payload 限制。",
|
||||||
|
"visionDetail": "Image detail",
|
||||||
|
"visionTimeout": "超时(秒)",
|
||||||
|
"visionTestFillRequired": "请填写视觉模型,并确保 API Key 可用(可复用 OpenAI)",
|
||||||
"testConnection": "测试连接",
|
"testConnection": "测试连接",
|
||||||
"testFillRequired": "请先填写 API Key 和模型",
|
"testFillRequired": "请先填写 API Key 和模型",
|
||||||
"testing": "测试中...",
|
"testing": "测试中...",
|
||||||
@@ -2278,9 +2329,9 @@
|
|||||||
"projectNone": "(未绑定)",
|
"projectNone": "(未绑定)",
|
||||||
"projectHint": "可为队列绑定项目;留空则不绑定项目上下文。",
|
"projectHint": "可为队列绑定项目;留空则不绑定项目上下文。",
|
||||||
"agentMode": "代理模式",
|
"agentMode": "代理模式",
|
||||||
"agentModeSingle": "单代理(ReAct)",
|
"agentModeSingle": "单代理(Eino ADK)",
|
||||||
"agentModeMulti": "多代理(Eino)",
|
"agentModeMulti": "多代理(Eino)",
|
||||||
"agentModeHint": "与对话页一致:原生 ReAct、Eino 单代理(ADK),或 Deep / Plan-Execute / Supervisor(后三种需已启用多代理)。",
|
"agentModeHint": "与对话页一致:Eino 单代理(ADK),或 Deep / Plan-Execute / Supervisor(后三种需已启用多代理)。",
|
||||||
"scheduleMode": "调度方式",
|
"scheduleMode": "调度方式",
|
||||||
"scheduleModeManual": "手工执行",
|
"scheduleModeManual": "手工执行",
|
||||||
"scheduleModeCron": "调度表达式(Cron)",
|
"scheduleModeCron": "调度表达式(Cron)",
|
||||||
|
|||||||
+6
-41
@@ -343,48 +343,13 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMarkdown(text) {
|
/** @param {string} text @param {{ profile?: 'chat'|'timeline' }} [options] */
|
||||||
const sanitizeConfig = {
|
function formatMarkdown(text, options) {
|
||||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
return window.csMarkdownSanitize.formatMarkdownToHtml(text, options);
|
||||||
ALLOW_DATA_ATTR: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const raw = text == null ? '' : String(text);
|
|
||||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
|
||||||
? window.normalizeAssistantMarkdownSource(raw)
|
|
||||||
: raw;
|
|
||||||
|
|
||||||
if (typeof DOMPurify !== 'undefined') {
|
|
||||||
if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(src)) {
|
|
||||||
try {
|
|
||||||
marked.setOptions({
|
|
||||||
breaks: true,
|
|
||||||
gfm: true,
|
|
||||||
});
|
|
||||||
const parsedContent = marked.parse(src, { async: false });
|
|
||||||
return DOMPurify.sanitize(parsedContent, sanitizeConfig);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Markdown 解析失败:', e);
|
|
||||||
return DOMPurify.sanitize(src, sanitizeConfig);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return DOMPurify.sanitize(src, sanitizeConfig);
|
|
||||||
}
|
|
||||||
} else if (typeof marked !== 'undefined') {
|
|
||||||
try {
|
|
||||||
marked.setOptions({
|
|
||||||
breaks: true,
|
|
||||||
gfm: true,
|
|
||||||
});
|
|
||||||
return marked.parse(src, { async: false });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Markdown 解析失败:', e);
|
|
||||||
return escapeHtml(src).replace(/\n/g, '<br>');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return escapeHtml(src).replace(/\n/g, '<br>');
|
|
||||||
}
|
}
|
||||||
|
const raw = text == null ? '' : String(text);
|
||||||
|
return escapeHtml(raw).replace(/\n/g, '<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLoginUI() {
|
function setupLoginUI() {
|
||||||
|
|||||||
+22
-138
@@ -38,11 +38,10 @@ function isInterruptContinueInjectChatMessage(content) {
|
|||||||
let chatAttachments = [];
|
let chatAttachments = [];
|
||||||
let chatAttachmentSeq = 0;
|
let chatAttachmentSeq = 0;
|
||||||
|
|
||||||
// 对话模式:react = 原生 ReAct(/agent-loop);eino_single = Eino ADK 单代理(/api/eino-agent/stream);deep / plan_execute / supervisor = Eino 多代理(/api/multi-agent/stream,请求体 orchestration)
|
// 对话模式:eino_single = Eino ADK 单代理(/api/eino-agent/stream);deep / plan_execute / supervisor = Eino 多代理(/api/multi-agent/stream,请求体 orchestration)
|
||||||
const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode';
|
const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode';
|
||||||
const REASONING_MODE_LS = 'cyberstrike-chat-reasoning-mode';
|
const REASONING_MODE_LS = 'cyberstrike-chat-reasoning-mode';
|
||||||
const REASONING_EFFORT_LS = 'cyberstrike-chat-reasoning-effort';
|
const REASONING_EFFORT_LS = 'cyberstrike-chat-reasoning-effort';
|
||||||
const CHAT_AGENT_MODE_REACT = 'react';
|
|
||||||
const CHAT_AGENT_MODE_EINO_SINGLE = 'eino_single';
|
const CHAT_AGENT_MODE_EINO_SINGLE = 'eino_single';
|
||||||
const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor'];
|
const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor'];
|
||||||
let multiAgentAPIEnabled = false;
|
let multiAgentAPIEnabled = false;
|
||||||
@@ -391,19 +390,16 @@ async function applyHitlSidebarConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将 localStorage / 历史值规范为 react | eino_single | deep | plan_execute | supervisor */
|
/** 将 localStorage 规范为 eino_single | deep | plan_execute | supervisor */
|
||||||
function chatAgentModeNormalizeStored(stored, cfg) {
|
function chatAgentModeNormalizeStored(stored, cfg) {
|
||||||
const pub = cfg && cfg.multi_agent ? cfg.multi_agent : null;
|
const pub = cfg && cfg.multi_agent ? cfg.multi_agent : null;
|
||||||
const multiOn = !!(pub && pub.enabled);
|
const multiOn = !!(pub && pub.enabled);
|
||||||
const defOrch = 'deep';
|
const s = stored;
|
||||||
let s = stored;
|
if (chatAgentModeIsEinoSingle(s)) return s;
|
||||||
if (s === 'single') s = CHAT_AGENT_MODE_REACT;
|
|
||||||
if (s === 'multi') s = defOrch;
|
|
||||||
if (s === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(s)) return s;
|
|
||||||
if (chatAgentModeIsEino(s)) {
|
if (chatAgentModeIsEino(s)) {
|
||||||
return multiOn ? s : CHAT_AGENT_MODE_REACT;
|
return multiOn ? s : CHAT_AGENT_MODE_EINO_SINGLE;
|
||||||
}
|
}
|
||||||
return CHAT_AGENT_MODE_REACT;
|
return CHAT_AGENT_MODE_EINO_SINGLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -411,7 +407,6 @@ if (typeof window !== 'undefined') {
|
|||||||
window.csaiChatAgentMode = {
|
window.csaiChatAgentMode = {
|
||||||
EINO_MODES: CHAT_AGENT_EINO_MODES,
|
EINO_MODES: CHAT_AGENT_EINO_MODES,
|
||||||
EINO_SINGLE: CHAT_AGENT_MODE_EINO_SINGLE,
|
EINO_SINGLE: CHAT_AGENT_MODE_EINO_SINGLE,
|
||||||
REACT: CHAT_AGENT_MODE_REACT,
|
|
||||||
isEino: chatAgentModeIsEino,
|
isEino: chatAgentModeIsEino,
|
||||||
isEinoSingle: chatAgentModeIsEinoSingle,
|
isEinoSingle: chatAgentModeIsEinoSingle,
|
||||||
normalizeStored: chatAgentModeNormalizeStored,
|
normalizeStored: chatAgentModeNormalizeStored,
|
||||||
@@ -448,8 +443,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
function getAgentModeLabelForValue(mode) {
|
function getAgentModeLabelForValue(mode) {
|
||||||
if (typeof window.t === 'function') {
|
if (typeof window.t === 'function') {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case CHAT_AGENT_MODE_REACT:
|
|
||||||
return window.t('chat.agentModeReactNative');
|
|
||||||
case 'deep':
|
case 'deep':
|
||||||
return window.t('chat.agentModeDeep');
|
return window.t('chat.agentModeDeep');
|
||||||
case 'plan_execute':
|
case 'plan_execute':
|
||||||
@@ -463,7 +456,6 @@ function getAgentModeLabelForValue(mode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case CHAT_AGENT_MODE_REACT: return '原生 ReAct';
|
|
||||||
case CHAT_AGENT_MODE_EINO_SINGLE: return 'Eino 单代理';
|
case CHAT_AGENT_MODE_EINO_SINGLE: return 'Eino 单代理';
|
||||||
case 'deep': return 'Deep';
|
case 'deep': return 'Deep';
|
||||||
case 'plan_execute': return 'Plan-Execute';
|
case 'plan_execute': return 'Plan-Execute';
|
||||||
@@ -474,7 +466,6 @@ function getAgentModeLabelForValue(mode) {
|
|||||||
|
|
||||||
function getAgentModeIconForValue(mode) {
|
function getAgentModeIconForValue(mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case CHAT_AGENT_MODE_REACT: return '🤖';
|
|
||||||
case CHAT_AGENT_MODE_EINO_SINGLE: return '⚡';
|
case CHAT_AGENT_MODE_EINO_SINGLE: return '⚡';
|
||||||
case 'deep': return '🧩';
|
case 'deep': return '🧩';
|
||||||
case 'plan_execute': return '📋';
|
case 'plan_execute': return '📋';
|
||||||
@@ -655,7 +646,7 @@ function toggleAgentModePanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectAgentMode(mode) {
|
function selectAgentMode(mode) {
|
||||||
const ok = mode === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(mode) || chatAgentModeIsEino(mode);
|
const ok = chatAgentModeIsEinoSingle(mode) || chatAgentModeIsEino(mode);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(AGENT_MODE_STORAGE_KEY, mode);
|
localStorage.setItem(AGENT_MODE_STORAGE_KEY, mode);
|
||||||
@@ -672,8 +663,8 @@ async function initChatAgentModeFromConfig() {
|
|||||||
// 先展示基础模式,避免首次登录时配置接口短暂失败导致入口被隐藏。
|
// 先展示基础模式,避免首次登录时配置接口短暂失败导致入口被隐藏。
|
||||||
wrap.style.display = '';
|
wrap.style.display = '';
|
||||||
let stored = localStorage.getItem(AGENT_MODE_STORAGE_KEY);
|
let stored = localStorage.getItem(AGENT_MODE_STORAGE_KEY);
|
||||||
if (!(stored === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(stored) || chatAgentModeIsEino(stored))) {
|
if (!(chatAgentModeIsEinoSingle(stored) || chatAgentModeIsEino(stored))) {
|
||||||
stored = CHAT_AGENT_MODE_REACT;
|
stored = CHAT_AGENT_MODE_EINO_SINGLE;
|
||||||
}
|
}
|
||||||
sel.value = stored;
|
sel.value = stored;
|
||||||
syncAgentModeFromValue(stored);
|
syncAgentModeFromValue(stored);
|
||||||
@@ -725,7 +716,7 @@ document.addEventListener('languagechange', function () {
|
|||||||
const hid = document.getElementById('agent-mode-select');
|
const hid = document.getElementById('agent-mode-select');
|
||||||
if (!hid) return;
|
if (!hid) return;
|
||||||
const v = hid.value;
|
const v = hid.value;
|
||||||
if (v === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(v) || chatAgentModeIsEino(v)) {
|
if (chatAgentModeIsEinoSingle(v) || chatAgentModeIsEino(v)) {
|
||||||
syncAgentModeFromValue(v);
|
syncAgentModeFromValue(v);
|
||||||
}
|
}
|
||||||
if (typeof updateChatReasoningSummary === 'function') {
|
if (typeof updateChatReasoningSummary === 'function') {
|
||||||
@@ -945,10 +936,9 @@ async function sendMessage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const modeSel = document.getElementById('agent-mode-select');
|
const modeSel = document.getElementById('agent-mode-select');
|
||||||
const modeVal = modeSel ? modeSel.value : CHAT_AGENT_MODE_REACT;
|
let modeVal = modeSel ? modeSel.value : CHAT_AGENT_MODE_EINO_SINGLE;
|
||||||
const useEinoSingle = chatAgentModeIsEinoSingle(modeVal);
|
|
||||||
const useMulti = multiAgentAPIEnabled && chatAgentModeIsEino(modeVal);
|
const useMulti = multiAgentAPIEnabled && chatAgentModeIsEino(modeVal);
|
||||||
const streamPath = useEinoSingle ? '/api/eino-agent/stream' : useMulti ? '/api/multi-agent/stream' : '/api/agent-loop/stream';
|
const streamPath = useMulti ? '/api/multi-agent/stream' : '/api/eino-agent/stream';
|
||||||
if (useMulti && modeVal) {
|
if (useMulti && modeVal) {
|
||||||
body.orchestration = modeVal;
|
body.orchestration = modeVal;
|
||||||
}
|
}
|
||||||
@@ -1872,25 +1862,9 @@ function refreshSystemReadyMessageBubbles() {
|
|||||||
div.textContent = s;
|
div.textContent = s;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
};
|
};
|
||||||
const defaultSanitizeConfig = {
|
|
||||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
|
||||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
|
||||||
ALLOW_DATA_ATTR: false,
|
|
||||||
};
|
|
||||||
let formattedContent;
|
let formattedContent;
|
||||||
if (typeof marked !== 'undefined') {
|
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||||
try {
|
formattedContent = window.csMarkdownSanitize.formatMarkdownToHtml(text, { profile: 'chat' });
|
||||||
marked.setOptions({ breaks: true, gfm: true });
|
|
||||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
|
||||||
? window.normalizeAssistantMarkdownSource(text)
|
|
||||||
: text;
|
|
||||||
const parsed = marked.parse(src, { async: false });
|
|
||||||
formattedContent = typeof DOMPurify !== 'undefined'
|
|
||||||
? DOMPurify.sanitize(parsed, defaultSanitizeConfig)
|
|
||||||
: parsed;
|
|
||||||
} catch (e) {
|
|
||||||
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
|
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
|
||||||
}
|
}
|
||||||
@@ -1946,13 +1920,6 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
|||||||
|
|
||||||
// 解析 Markdown 或 HTML 格式
|
// 解析 Markdown 或 HTML 格式
|
||||||
let formattedContent;
|
let formattedContent;
|
||||||
const defaultSanitizeConfig = {
|
|
||||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
|
||||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
|
||||||
ALLOW_DATA_ATTR: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// HTML实体编码函数
|
|
||||||
const escapeHtml = (text) => {
|
const escapeHtml = (text) => {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@@ -1960,31 +1927,6 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 注意:代码块内容不需要转义,因为:
|
|
||||||
// 1. Markdown解析后,代码块会被包裹在<code>或<pre>标签中
|
|
||||||
// 2. 浏览器不会执行<code>和<pre>标签内的HTML(它们是文本节点)
|
|
||||||
// 3. DOMPurify会保留这些标签内的文本内容
|
|
||||||
// 这样既能防止XSS,又能正常显示代码
|
|
||||||
|
|
||||||
const parseMarkdown = (raw) => {
|
|
||||||
if (typeof marked === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
marked.setOptions({
|
|
||||||
breaks: true,
|
|
||||||
gfm: true,
|
|
||||||
});
|
|
||||||
const src = typeof window.normalizeAssistantMarkdownSource === 'function'
|
|
||||||
? window.normalizeAssistantMarkdownSource(raw)
|
|
||||||
: raw;
|
|
||||||
return marked.parse(src, { async: false });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Markdown 解析失败:', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 助手消息中的已知中文错误前缀做国际化替换(后端固定返回中文)
|
// 助手消息中的已知中文错误前缀做国际化替换(后端固定返回中文)
|
||||||
let displayContent = content;
|
let displayContent = content;
|
||||||
if (role === 'assistant' && typeof displayContent === 'string' && typeof window.t === 'function') {
|
if (role === 'assistant' && typeof displayContent === 'string' && typeof window.t === 'function') {
|
||||||
@@ -1999,57 +1941,11 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
|||||||
// 对于用户消息,直接转义HTML,不进行Markdown解析,以保留所有特殊字符
|
// 对于用户消息,直接转义HTML,不进行Markdown解析,以保留所有特殊字符
|
||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||||
} else if (typeof DOMPurify !== 'undefined') {
|
} else if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||||
// 直接解析Markdown(代码块会被包裹在<code>/<pre>中,DOMPurify会保留其文本内容)
|
formattedContent = window.csMarkdownSanitize.formatMarkdownToHtml(
|
||||||
let parsedContent = parseMarkdown(role === 'assistant' ? displayContent : content);
|
role === 'assistant' ? displayContent : content,
|
||||||
if (!parsedContent) {
|
{ profile: 'chat' }
|
||||||
parsedContent = content;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// 使用DOMPurify清理,只添加必要的URL验证钩子(DOMPurify默认会处理事件处理器等)
|
|
||||||
if (DOMPurify.addHook) {
|
|
||||||
// 移除之前可能存在的钩子
|
|
||||||
try {
|
|
||||||
DOMPurify.removeHook('uponSanitizeAttribute');
|
|
||||||
} catch (e) {
|
|
||||||
// 钩子不存在,忽略
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只验证URL属性,防止危险协议(DOMPurify默认会处理事件处理器、style等)
|
|
||||||
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
|
||||||
const attrName = data.attrName.toLowerCase();
|
|
||||||
|
|
||||||
// 只验证URL属性(src, href)
|
|
||||||
if ((attrName === 'src' || attrName === 'href') && data.attrValue) {
|
|
||||||
const value = data.attrValue.trim().toLowerCase();
|
|
||||||
// 禁止危险协议
|
|
||||||
if (value.startsWith('javascript:') ||
|
|
||||||
value.startsWith('vbscript:') ||
|
|
||||||
value.startsWith('data:text/html') ||
|
|
||||||
value.startsWith('data:text/javascript')) {
|
|
||||||
data.keepAttr = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 对于img的src,禁止可疑的短URL(防止404和XSS)
|
|
||||||
if (attrName === 'src' && node.tagName && node.tagName.toLowerCase() === 'img') {
|
|
||||||
if (value.length <= 2 || /^[a-z]$/i.test(value)) {
|
|
||||||
data.keepAttr = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedContent = DOMPurify.sanitize(parsedContent, defaultSanitizeConfig);
|
|
||||||
} else if (typeof marked !== 'undefined') {
|
|
||||||
const rawForParse = role === 'assistant' ? displayContent : content;
|
|
||||||
const parsedContent = parseMarkdown(rawForParse);
|
|
||||||
if (parsedContent) {
|
|
||||||
formattedContent = parsedContent;
|
|
||||||
} else {
|
|
||||||
formattedContent = escapeHtml(rawForParse).replace(/\n/g, '<br>');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const rawForEscape = role === 'assistant' ? displayContent : content;
|
const rawForEscape = role === 'assistant' ? displayContent : content;
|
||||||
formattedContent = escapeHtml(rawForEscape).replace(/\n/g, '<br>');
|
formattedContent = escapeHtml(rawForEscape).replace(/\n/g, '<br>');
|
||||||
@@ -2057,21 +1953,9 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
|||||||
|
|
||||||
bubble.innerHTML = formattedContent;
|
bubble.innerHTML = formattedContent;
|
||||||
|
|
||||||
// 最后的安全检查:只处理明显的可疑图片(防止404和XSS)
|
if (typeof window.csMarkdownSanitize !== 'undefined') {
|
||||||
// DOMPurify已经处理了大部分XSS向量,这里只做必要的补充
|
window.csMarkdownSanitize.stripSuspiciousImages(bubble);
|
||||||
const images = bubble.querySelectorAll('img');
|
}
|
||||||
images.forEach(img => {
|
|
||||||
const src = img.getAttribute('src');
|
|
||||||
if (src) {
|
|
||||||
const trimmedSrc = src.trim();
|
|
||||||
// 只检查明显的可疑URL(短字符串、单个字符)
|
|
||||||
if (trimmedSrc.length <= 2 || /^[a-z]$/i.test(trimmedSrc)) {
|
|
||||||
img.remove();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
img.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 为每个表格添加独立的滚动容器
|
// 为每个表格添加独立的滚动容器
|
||||||
wrapTablesInBubble(bubble);
|
wrapTablesInBubble(bubble);
|
||||||
|
|||||||
+876
-249
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* 统一的 Markdown → 安全 HTML 渲染(DOMPurify + marked)。
|
||||||
|
* 时间线/过程详情使用 stricter profile,整页 HTML 回退为转义 <pre>。
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const CHAT_SANITIZE_CONFIG = {
|
||||||
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote',
|
||||||
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img',
|
||||||
|
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||||
|
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 过程详情时间线:禁止 img,减少外连与恶意资源 */
|
||||||
|
const TIMELINE_SANITIZE_CONFIG = {
|
||||||
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote',
|
||||||
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a',
|
||||||
|
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||||
|
ALLOWED_ATTR: ['href', 'title', 'alt', 'class'],
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DANGEROUS_URL_PREFIXES = [
|
||||||
|
'javascript:',
|
||||||
|
'vbscript:',
|
||||||
|
'data:text/html',
|
||||||
|
'data:text/javascript',
|
||||||
|
'data:application/javascript',
|
||||||
|
];
|
||||||
|
|
||||||
|
let domPurifyHooksInstalled = false;
|
||||||
|
|
||||||
|
function escapeHtmlLocal(text) {
|
||||||
|
if (text == null || text === '') return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = String(text);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installDomPurifyHooks() {
|
||||||
|
if (domPurifyHooksInstalled || typeof DOMPurify === 'undefined' || !DOMPurify.addHook) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DOMPurify.addHook('uponSanitizeAttribute', function (node, data) {
|
||||||
|
const attrName = (data.attrName || '').toLowerCase();
|
||||||
|
if ((attrName !== 'src' && attrName !== 'href') || !data.attrValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = String(data.attrValue).trim().toLowerCase();
|
||||||
|
for (let i = 0; i < DANGEROUS_URL_PREFIXES.length; i++) {
|
||||||
|
if (value.indexOf(DANGEROUS_URL_PREFIXES[i]) === 0) {
|
||||||
|
data.keepAttr = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value.indexOf('blob:') === 0) {
|
||||||
|
data.keepAttr = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attrName === 'src' && node.tagName && node.tagName.toLowerCase() === 'img') {
|
||||||
|
if (value.length <= 2 || /^[a-z]$/i.test(value)) {
|
||||||
|
data.keepAttr = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
domPurifyHooksInstalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 探测工具返回的整页 HTML,不宜当作富文本渲染 */
|
||||||
|
function isHeavyRawHtml(src) {
|
||||||
|
const s = String(src);
|
||||||
|
if (/<!DOCTYPE\s+html/i.test(s) || /<\s*html\b/i.test(s)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/<\s*(head|body|iframe|object|embed|form|script|style|meta|link|base)\b/i.test(s)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const tags = s.match(/<[a-z][^>]*>/gi);
|
||||||
|
return tags != null && tags.length >= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHtmlAsEscapedPre(text) {
|
||||||
|
return '<pre class="tool-result sanitized-raw-html-fallback">' + escapeHtmlLocal(text) + '</pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSource(text) {
|
||||||
|
const raw = text == null ? '' : String(text);
|
||||||
|
if (typeof global.normalizeAssistantMarkdownSource === 'function') {
|
||||||
|
return global.normalizeAssistantMarkdownSource(raw);
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdownSrc(src) {
|
||||||
|
if (typeof marked === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
marked.setOptions({ breaks: true, gfm: true });
|
||||||
|
return marked.parse(src, { async: false });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Markdown 解析失败:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeConfigForProfile(profile) {
|
||||||
|
return profile === 'timeline' ? TIMELINE_SANITIZE_CONFIG : CHAT_SANITIZE_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|null|undefined} text
|
||||||
|
* @param {{ profile?: 'chat'|'timeline' }} [options]
|
||||||
|
* @returns {string} 安全 HTML
|
||||||
|
*/
|
||||||
|
function formatMarkdownToHtml(text, options) {
|
||||||
|
const profile = (options && options.profile === 'timeline') ? 'timeline' : 'chat';
|
||||||
|
const src = normalizeSource(text);
|
||||||
|
|
||||||
|
if (isHeavyRawHtml(src)) {
|
||||||
|
return formatHtmlAsEscapedPre(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof DOMPurify === 'undefined') {
|
||||||
|
return escapeHtmlLocal(src).replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
installDomPurifyHooks();
|
||||||
|
const config = sanitizeConfigForProfile(profile);
|
||||||
|
|
||||||
|
let html;
|
||||||
|
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(src);
|
||||||
|
if (typeof marked !== 'undefined' && !hasHtmlTags) {
|
||||||
|
const parsed = parseMarkdownSrc(src);
|
||||||
|
html = parsed != null ? parsed : escapeHtmlLocal(src).replace(/\n/g, '<br>');
|
||||||
|
} else if (hasHtmlTags) {
|
||||||
|
html = src;
|
||||||
|
} else {
|
||||||
|
html = escapeHtmlLocal(src).replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return DOMPurify.sanitize(html, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRichHtml(html, profile) {
|
||||||
|
if (typeof DOMPurify === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
installDomPurifyHooks();
|
||||||
|
return DOMPurify.sanitize(html, sanitizeConfigForProfile(profile || 'chat'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripSuspiciousImages(root) {
|
||||||
|
if (!root || !root.querySelectorAll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.querySelectorAll('img').forEach(function (img) {
|
||||||
|
const src = (img.getAttribute('src') || '').trim();
|
||||||
|
if (!src || src.length <= 2 || /^[a-z]$/i.test(src)) {
|
||||||
|
img.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
global.csMarkdownSanitize = {
|
||||||
|
CHAT_SANITIZE_CONFIG: CHAT_SANITIZE_CONFIG,
|
||||||
|
TIMELINE_SANITIZE_CONFIG: TIMELINE_SANITIZE_CONFIG,
|
||||||
|
installDomPurifyHooks: installDomPurifyHooks,
|
||||||
|
formatMarkdownToHtml: formatMarkdownToHtml,
|
||||||
|
sanitizeRichHtml: sanitizeRichHtml,
|
||||||
|
isHeavyRawHtml: isHeavyRawHtml,
|
||||||
|
escapeHtmlLocal: escapeHtmlLocal,
|
||||||
|
stripSuspiciousImages: stripSuspiciousImages,
|
||||||
|
};
|
||||||
|
|
||||||
|
global.formatMarkdown = function formatMarkdown(text, options) {
|
||||||
|
return formatMarkdownToHtml(text, options);
|
||||||
|
};
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis);
|
||||||
+119
-6
@@ -40,7 +40,7 @@ function syncRobotAgentModeSelectOptions(multiEnabled) {
|
|||||||
if (opt) opt.disabled = !multiEnabled;
|
if (opt) opt.disabled = !multiEnabled;
|
||||||
});
|
});
|
||||||
if (!multiEnabled && ['deep', 'plan_execute', 'supervisor'].indexOf(sel.value) >= 0) {
|
if (!multiEnabled && ['deep', 'plan_execute', 'supervisor'].indexOf(sel.value) >= 0) {
|
||||||
sel.value = 'react';
|
sel.value = 'eino_single';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +197,8 @@ async function loadConfig(loadTools = true) {
|
|||||||
orAllowEl.checked = orm.allow_client_reasoning !== false;
|
orAllowEl.checked = orm.allow_client_reasoning !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillVisionConfigFromCurrent(currentConfig.vision || {});
|
||||||
|
|
||||||
// 填充FOFA配置
|
// 填充FOFA配置
|
||||||
const fofa = currentConfig.fofa || {};
|
const fofa = currentConfig.fofa || {};
|
||||||
const fofaEmailEl = document.getElementById('fofa-email');
|
const fofaEmailEl = document.getElementById('fofa-email');
|
||||||
@@ -227,8 +229,7 @@ async function loadConfig(loadTools = true) {
|
|||||||
}
|
}
|
||||||
const maRobotMode = document.getElementById('multi-agent-robot-mode');
|
const maRobotMode = document.getElementById('multi-agent-robot-mode');
|
||||||
if (maRobotMode) {
|
if (maRobotMode) {
|
||||||
let mode = (ma.robot_default_agent_mode || 'react').trim().toLowerCase();
|
let mode = (ma.robot_default_agent_mode || 'eino_single').trim().toLowerCase();
|
||||||
if (mode === 'single') mode = 'react';
|
|
||||||
maRobotMode.value = mode;
|
maRobotMode.value = mode;
|
||||||
syncRobotAgentModeSelectOptions(ma.enabled === true);
|
syncRobotAgentModeSelectOptions(ma.enabled === true);
|
||||||
}
|
}
|
||||||
@@ -1075,6 +1076,14 @@ async function applySettings() {
|
|||||||
alert(msg);
|
alert(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visionPayload = collectVisionConfigFromForm();
|
||||||
|
if (visionPayload.enabled && !visionPayload.model) {
|
||||||
|
const vm = document.getElementById('vision-model');
|
||||||
|
if (vm) vm.classList.add('error');
|
||||||
|
alert((typeof window.t === 'function') ? window.t('settingsBasic.visionModelRequired') : '启用视觉分析时请填写视觉模型名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 收集配置
|
// 收集配置
|
||||||
const knowledgeEnabledCheckbox = document.getElementById('knowledge-enabled');
|
const knowledgeEnabledCheckbox = document.getElementById('knowledge-enabled');
|
||||||
@@ -1147,6 +1156,7 @@ async function applySettings() {
|
|||||||
allow_client_reasoning: document.getElementById('openai-reasoning-allow-client')?.checked !== false
|
allow_client_reasoning: document.getElementById('openai-reasoning-allow-client')?.checked !== false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
vision: visionPayload,
|
||||||
fofa: {
|
fofa: {
|
||||||
email: document.getElementById('fofa-email')?.value.trim() || '',
|
email: document.getElementById('fofa-email')?.value.trim() || '',
|
||||||
api_key: document.getElementById('fofa-api-key')?.value.trim() || '',
|
api_key: document.getElementById('fofa-api-key')?.value.trim() || '',
|
||||||
@@ -1160,9 +1170,9 @@ async function applySettings() {
|
|||||||
const peParsed = parseInt(peRaw, 10);
|
const peParsed = parseInt(peRaw, 10);
|
||||||
const peLoop = Number.isNaN(peParsed) ? 0 : Math.max(0, peParsed);
|
const peLoop = Number.isNaN(peParsed) ? 0 : Math.max(0, peParsed);
|
||||||
const maEnabled = document.getElementById('multi-agent-enabled')?.checked === true;
|
const maEnabled = document.getElementById('multi-agent-enabled')?.checked === true;
|
||||||
let robotMode = document.getElementById('multi-agent-robot-mode')?.value || 'react';
|
let robotMode = document.getElementById('multi-agent-robot-mode')?.value || 'eino_single';
|
||||||
if (!maEnabled && ['deep', 'plan_execute', 'supervisor'].indexOf(robotMode) >= 0) {
|
if (!maEnabled && ['deep', 'plan_execute', 'supervisor'].indexOf(robotMode) >= 0) {
|
||||||
robotMode = 'react';
|
robotMode = 'eino_single';
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
enabled: maEnabled,
|
enabled: maEnabled,
|
||||||
@@ -1342,6 +1352,109 @@ async function applySettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillVisionConfigFromCurrent(v) {
|
||||||
|
const en = document.getElementById('vision-enabled');
|
||||||
|
if (en) en.checked = v.enabled === true;
|
||||||
|
const prov = document.getElementById('vision-provider');
|
||||||
|
if (prov) prov.value = (v.provider || '').trim();
|
||||||
|
const setVal = (id, val) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = val != null && val !== '' ? String(val) : '';
|
||||||
|
};
|
||||||
|
setVal('vision-api-key', v.api_key || '');
|
||||||
|
setVal('vision-base-url', v.base_url || '');
|
||||||
|
setVal('vision-model', v.model || '');
|
||||||
|
setVal('vision-max-image-bytes', v.max_image_bytes || 5242880);
|
||||||
|
setVal('vision-max-dimension', v.max_dimension || 2048);
|
||||||
|
setVal('vision-jpeg-quality', v.jpeg_quality || 82);
|
||||||
|
setVal('vision-max-payload-bytes', v.max_payload_bytes || 524288);
|
||||||
|
setVal('vision-skip-preprocess-bytes', v.skip_preprocess_below_bytes != null ? v.skip_preprocess_below_bytes : 2097152);
|
||||||
|
setVal('vision-timeout-seconds', v.timeout_seconds || 60);
|
||||||
|
const det = document.getElementById('vision-detail');
|
||||||
|
if (det) {
|
||||||
|
const d = (v.detail || 'low').toString().toLowerCase();
|
||||||
|
det.value = ['low', 'auto', 'high'].includes(d) ? d : 'low';
|
||||||
|
}
|
||||||
|
syncVisionFormEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectVisionConfigFromForm() {
|
||||||
|
const parseIntOr = (id, fallback) => {
|
||||||
|
const n = parseInt(document.getElementById(id)?.value, 10);
|
||||||
|
return Number.isNaN(n) ? fallback : n;
|
||||||
|
};
|
||||||
|
const provider = document.getElementById('vision-provider')?.value.trim() || '';
|
||||||
|
return {
|
||||||
|
enabled: document.getElementById('vision-enabled')?.checked === true,
|
||||||
|
api_key: document.getElementById('vision-api-key')?.value.trim() || '',
|
||||||
|
base_url: document.getElementById('vision-base-url')?.value.trim() || '',
|
||||||
|
model: document.getElementById('vision-model')?.value.trim() || '',
|
||||||
|
provider: provider,
|
||||||
|
timeout_seconds: parseIntOr('vision-timeout-seconds', 60),
|
||||||
|
max_image_bytes: parseIntOr('vision-max-image-bytes', 5242880),
|
||||||
|
max_dimension: parseIntOr('vision-max-dimension', 2048),
|
||||||
|
jpeg_quality: parseIntOr('vision-jpeg-quality', 82),
|
||||||
|
max_payload_bytes: parseIntOr('vision-max-payload-bytes', 524288),
|
||||||
|
skip_preprocess_below_bytes: parseIntOr('vision-skip-preprocess-bytes', 2097152),
|
||||||
|
detail: document.getElementById('vision-detail')?.value || 'low'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncVisionFormEnabled() {
|
||||||
|
const enabled = document.getElementById('vision-enabled')?.checked === true;
|
||||||
|
const panel = document.getElementById('vision-fields-panel');
|
||||||
|
if (panel) {
|
||||||
|
panel.style.opacity = enabled ? '1' : '0.55';
|
||||||
|
panel.querySelectorAll('input, select, textarea, a').forEach(el => {
|
||||||
|
if (el.id === 'test-vision-btn') return;
|
||||||
|
el.disabled = !enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testVisionConnection() {
|
||||||
|
const resultEl = document.getElementById('test-vision-result');
|
||||||
|
const vision = collectVisionConfigFromForm();
|
||||||
|
const openai = {
|
||||||
|
provider: document.getElementById('openai-provider')?.value || 'openai',
|
||||||
|
api_key: document.getElementById('openai-api-key')?.value.trim() || '',
|
||||||
|
base_url: document.getElementById('openai-base-url')?.value.trim() || '',
|
||||||
|
model: document.getElementById('openai-model')?.value.trim() || ''
|
||||||
|
};
|
||||||
|
const apiKey = vision.api_key || openai.api_key;
|
||||||
|
const model = vision.model;
|
||||||
|
if (!apiKey || !model) {
|
||||||
|
if (resultEl) {
|
||||||
|
resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.visionTestFillRequired') : '请填写视觉模型,并确保 API Key 可用';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resultEl) {
|
||||||
|
resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testing') : '测试中...';
|
||||||
|
resultEl.style.color = '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await apiFetch('/api/config/test-vision', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ vision: vision, openai: openai })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
const latency = result.latency_ms != null ? ` (${result.latency_ms}ms)` : '';
|
||||||
|
const modelInfo = result.model ? ` [${result.model}]` : '';
|
||||||
|
resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testSuccess') : '连接成功') + modelInfo + latency;
|
||||||
|
resultEl.style.color = 'var(--success-color, #38a169)';
|
||||||
|
} else {
|
||||||
|
resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testFailed') : '连接失败') + ': ' + (result.error || '未知错误');
|
||||||
|
resultEl.style.color = 'var(--error-color, #e53e3e)';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testError') : '测试出错') + ': ' + error.message;
|
||||||
|
resultEl.style.color = 'var(--error-color, #e53e3e)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 测试OpenAI连接
|
// 测试OpenAI连接
|
||||||
async function testOpenAIConnection() {
|
async function testOpenAIConnection() {
|
||||||
const btn = document.getElementById('test-openai-btn');
|
const btn = document.getElementById('test-openai-btn');
|
||||||
@@ -1415,7 +1528,7 @@ async function saveToolsConfig() {
|
|||||||
agent: currentConfig.agent || {},
|
agent: currentConfig.agent || {},
|
||||||
multi_agent: {
|
multi_agent: {
|
||||||
enabled: currentConfig?.multi_agent?.enabled === true,
|
enabled: currentConfig?.multi_agent?.enabled === true,
|
||||||
robot_default_agent_mode: currentConfig?.multi_agent?.robot_default_agent_mode || 'react',
|
robot_default_agent_mode: currentConfig?.multi_agent?.robot_default_agent_mode || 'eino_single',
|
||||||
batch_use_multi_agent: currentConfig?.multi_agent?.batch_use_multi_agent === true,
|
batch_use_multi_agent: currentConfig?.multi_agent?.batch_use_multi_agent === true,
|
||||||
plan_execute_loop_max_iterations: Number(currentConfig?.multi_agent?.plan_execute_loop_max_iterations || 0),
|
plan_execute_loop_max_iterations: Number(currentConfig?.multi_agent?.plan_execute_loop_max_iterations || 0),
|
||||||
tool_search_always_visible_tools: Array.from(alwaysVisibleToolNames).filter(name => !alwaysVisibleBuiltinToolNames.has(name))
|
tool_search_always_visible_tools: Array.from(alwaysVisibleToolNames).filter(name => !alwaysVisibleBuiltinToolNames.has(name))
|
||||||
|
|||||||
+11
-14
@@ -15,7 +15,7 @@ function _tPlain(key, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 与创建队列 / API 一致的合法 agentMode */
|
/** 与创建队列 / API 一致的合法 agentMode */
|
||||||
const BATCH_QUEUE_AGENT_MODES = ['single', 'eino_single', 'deep', 'plan_execute', 'supervisor'];
|
const BATCH_QUEUE_AGENT_MODES = ['eino_single', 'deep', 'plan_execute', 'supervisor'];
|
||||||
|
|
||||||
function isBatchQueueAgentMode(mode) {
|
function isBatchQueueAgentMode(mode) {
|
||||||
return BATCH_QUEUE_AGENT_MODES.indexOf(String(mode || '').toLowerCase()) >= 0;
|
return BATCH_QUEUE_AGENT_MODES.indexOf(String(mode || '').toLowerCase()) >= 0;
|
||||||
@@ -23,13 +23,12 @@ function isBatchQueueAgentMode(mode) {
|
|||||||
|
|
||||||
/** 批量队列 agentMode 展示文案(与对话模式命名一致) */
|
/** 批量队列 agentMode 展示文案(与对话模式命名一致) */
|
||||||
function batchQueueAgentModeLabel(mode) {
|
function batchQueueAgentModeLabel(mode) {
|
||||||
const m = String(mode || 'single').toLowerCase();
|
const m = String(mode || 'eino_single').toLowerCase();
|
||||||
if (m === 'single') return _t('chat.agentModeReactNative');
|
|
||||||
if (m === 'eino_single') return _t('chat.agentModeEinoSingle');
|
if (m === 'eino_single') return _t('chat.agentModeEinoSingle');
|
||||||
if (m === 'multi' || m === 'deep') return _t('chat.agentModeDeep');
|
if (m === 'deep') return _t('chat.agentModeDeep');
|
||||||
if (m === 'plan_execute') return _t('chat.agentModePlanExecuteLabel');
|
if (m === 'plan_execute') return _t('chat.agentModePlanExecuteLabel');
|
||||||
if (m === 'supervisor') return _t('chat.agentModeSupervisorLabel');
|
if (m === 'supervisor') return _t('chat.agentModeSupervisorLabel');
|
||||||
return _t('chat.agentModeReactNative');
|
return _t('chat.agentModeEinoSingle');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cron 队列在「本轮 completed」等状态下的展示文案(底层 status 不变,仅 UI 强调循环调度) */
|
/** Cron 队列在「本轮 completed」等状态下的展示文案(底层 status 不变,仅 UI 强调循环调度) */
|
||||||
@@ -867,7 +866,7 @@ async function showBatchImportModal() {
|
|||||||
projectSelect.value = '';
|
projectSelect.value = '';
|
||||||
}
|
}
|
||||||
if (agentModeSelect) {
|
if (agentModeSelect) {
|
||||||
agentModeSelect.value = 'single';
|
agentModeSelect.value = 'eino_single';
|
||||||
}
|
}
|
||||||
if (scheduleModeSelect) {
|
if (scheduleModeSelect) {
|
||||||
scheduleModeSelect.value = 'manual';
|
scheduleModeSelect.value = 'manual';
|
||||||
@@ -997,8 +996,8 @@ async function createBatchQueue() {
|
|||||||
// 获取角色(可选,空字符串表示默认角色)
|
// 获取角色(可选,空字符串表示默认角色)
|
||||||
const role = roleSelect ? roleSelect.value || '' : '';
|
const role = roleSelect ? roleSelect.value || '' : '';
|
||||||
const projectId = projectSelect ? (projectSelect.value || '').trim() : '';
|
const projectId = projectSelect ? (projectSelect.value || '').trim() : '';
|
||||||
const rawMode = agentModeSelect ? agentModeSelect.value : 'single';
|
const rawMode = agentModeSelect ? agentModeSelect.value : 'eino_single';
|
||||||
const agentMode = isBatchQueueAgentMode(rawMode) ? rawMode : 'single';
|
const agentMode = isBatchQueueAgentMode(rawMode) ? rawMode : 'eino_single';
|
||||||
const scheduleMode = scheduleModeSelect ? (scheduleModeSelect.value === 'cron' ? 'cron' : 'manual') : 'manual';
|
const scheduleMode = scheduleModeSelect ? (scheduleModeSelect.value === 'cron' ? 'cron' : 'manual') : 'manual';
|
||||||
const cronExpr = cronExprInput ? cronExprInput.value.trim() : '';
|
const cronExpr = cronExprInput ? cronExprInput.value.trim() : '';
|
||||||
const executeNow = executeNowCheckbox ? !!executeNowCheckbox.checked : false;
|
const executeNow = executeNowCheckbox ? !!executeNowCheckbox.checked : false;
|
||||||
@@ -2217,12 +2216,10 @@ function startInlineEditAgentMode() {
|
|||||||
if (!queueId) return;
|
if (!queueId) return;
|
||||||
apiFetch(`/api/batch-tasks/${queueId}`).then(r => r.json()).then(detail => {
|
apiFetch(`/api/batch-tasks/${queueId}`).then(r => r.json()).then(detail => {
|
||||||
const queue = detail.queue;
|
const queue = detail.queue;
|
||||||
let currentMode = (queue.agentMode || 'single').toLowerCase();
|
let currentMode = (queue.agentMode || 'eino_single').toLowerCase();
|
||||||
if (currentMode === 'multi') currentMode = 'deep';
|
if (!isBatchQueueAgentMode(currentMode)) currentMode = 'eino_single';
|
||||||
if (!isBatchQueueAgentMode(currentMode)) currentMode = 'single';
|
|
||||||
container.innerHTML = `<span class="bq-inline-edit-controls">
|
container.innerHTML = `<span class="bq-inline-edit-controls">
|
||||||
<select id="bq-edit-agentmode">
|
<select id="bq-edit-agentmode">
|
||||||
<option value="single" ${currentMode === 'single' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeReactNative'))}</option>
|
|
||||||
<option value="eino_single" ${currentMode === 'eino_single' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeEinoSingle'))}</option>
|
<option value="eino_single" ${currentMode === 'eino_single' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeEinoSingle'))}</option>
|
||||||
<option value="deep" ${currentMode === 'deep' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeDeep'))}</option>
|
<option value="deep" ${currentMode === 'deep' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeDeep'))}</option>
|
||||||
<option value="plan_execute" ${currentMode === 'plan_execute' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModePlanExecuteLabel'))}</option>
|
<option value="plan_execute" ${currentMode === 'plan_execute' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModePlanExecuteLabel'))}</option>
|
||||||
@@ -2247,8 +2244,8 @@ async function saveInlineAgentMode() {
|
|||||||
const queueId = batchQueuesState.currentQueueId;
|
const queueId = batchQueuesState.currentQueueId;
|
||||||
if (!queueId) { _bqInlineSaving = false; return; }
|
if (!queueId) { _bqInlineSaving = false; return; }
|
||||||
const sel = document.getElementById('bq-edit-agentmode');
|
const sel = document.getElementById('bq-edit-agentmode');
|
||||||
const raw = sel ? sel.value : 'single';
|
const raw = sel ? sel.value : 'eino_single';
|
||||||
const agentMode = isBatchQueueAgentMode(raw) ? raw : 'single';
|
const agentMode = isBatchQueueAgentMode(raw) ? raw : 'eino_single';
|
||||||
try {
|
try {
|
||||||
const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`);
|
const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`);
|
||||||
const detail = await detailResp.json();
|
const detail = await detailResp.json();
|
||||||
|
|||||||
@@ -720,7 +720,7 @@ async function loadVulnerabilityStats() {
|
|||||||
throw new Error('apiFetch未定义');
|
throw new Error('apiFetch未定义');
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = buildVulnerabilityFilterParams();
|
const params = buildVulnerabilityDashboardStatsParams();
|
||||||
|
|
||||||
const response = await apiFetch(`/api/vulnerabilities/stats?${params.toString()}`);
|
const response = await apiFetch(`/api/vulnerabilities/stats?${params.toString()}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -1531,6 +1531,13 @@ function buildVulnerabilityFilterParams() {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 看板统计:保留项目/关键词等筛选,但不带严重度(卡片本身用于切换严重度筛选) */
|
||||||
|
function buildVulnerabilityDashboardStatsParams() {
|
||||||
|
const params = buildVulnerabilityFilterParams();
|
||||||
|
params.delete('severity');
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
function triggerTextDownload(fileName, content) {
|
function triggerTextDownload(fileName, content) {
|
||||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -1543,6 +1550,53 @@ function triggerTextDownload(fileName, content) {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasActiveVulnerabilityFilters() {
|
||||||
|
const keys = ['q', 'id', 'project_id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
|
||||||
|
return keys.some(function (k) {
|
||||||
|
return Boolean(vulnerabilityFilters[k]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchDeleteVulnerabilityReports() {
|
||||||
|
try {
|
||||||
|
const params = buildVulnerabilityFilterParams();
|
||||||
|
const statsResponse = await apiFetch(`/api/vulnerabilities/stats?${params.toString()}`);
|
||||||
|
if (!statsResponse.ok) {
|
||||||
|
throw new Error(vulnT('vulnerabilityPage.deleteFailed'));
|
||||||
|
}
|
||||||
|
const stats = await statsResponse.json();
|
||||||
|
const count = stats.total || 0;
|
||||||
|
if (count <= 0) {
|
||||||
|
alert(vulnT('vulnerabilityPage.batchDeleteNoResults'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmKey = hasActiveVulnerabilityFilters()
|
||||||
|
? 'vulnerabilityPage.batchDeleteConfirm'
|
||||||
|
: 'vulnerabilityPage.batchDeleteConfirmAll';
|
||||||
|
if (!confirm(vulnT(confirmKey, { count: count }))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiFetch(`/api/vulnerabilities/batch?${params.toString()}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: vulnT('vulnerabilityPage.deleteFailed') }));
|
||||||
|
throw new Error(error.error || vulnT('vulnerabilityPage.deleteFailed'));
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const deleted = data.deleted || 0;
|
||||||
|
alert(vulnT('vulnerabilityPage.batchDeleteSuccess', { count: deleted }));
|
||||||
|
vulnerabilityPagination.currentPage = 1;
|
||||||
|
loadVulnerabilityStats();
|
||||||
|
loadVulnerabilities();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除漏洞失败:', error);
|
||||||
|
alert(vulnT('vulnerabilityPage.batchDeleteFailed') + ': ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function exportVulnerabilityReports() {
|
async function exportVulnerabilityReports() {
|
||||||
try {
|
try {
|
||||||
const params = buildVulnerabilityFilterParams();
|
const params = buildVulnerabilityFilterParams();
|
||||||
|
|||||||
+18
-21
@@ -154,33 +154,26 @@ function applyWebshellDetectedOS(conn, data) {
|
|||||||
/** 与主对话页一致:Eino 模式走 /api/multi-agent/stream,body 带 orchestration */
|
/** 与主对话页一致:Eino 模式走 /api/multi-agent/stream,body 带 orchestration */
|
||||||
function resolveWebshellAiStreamRequest() {
|
function resolveWebshellAiStreamRequest() {
|
||||||
if (typeof apiFetch === 'undefined') {
|
if (typeof apiFetch === 'undefined') {
|
||||||
return Promise.resolve({ path: '/api/agent-loop/stream', orchestration: null });
|
return Promise.resolve({ path: '/api/eino-agent/stream', orchestration: null });
|
||||||
}
|
}
|
||||||
return apiFetch('/api/config').then(function (r) {
|
return apiFetch('/api/config').then(function (r) {
|
||||||
if (!r.ok) return null;
|
if (!r.ok) return null;
|
||||||
return r.json();
|
return r.json();
|
||||||
}).then(function (cfg) {
|
}).then(function (cfg) {
|
||||||
var norm = null;
|
var norm = 'eino_single';
|
||||||
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') {
|
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') {
|
||||||
norm = window.csaiChatAgentMode.normalizeStored(localStorage.getItem('cyberstrike-chat-agent-mode'), cfg);
|
norm = window.csaiChatAgentMode.normalizeStored(localStorage.getItem('cyberstrike-chat-agent-mode'), cfg);
|
||||||
} else {
|
} else {
|
||||||
var mode = localStorage.getItem('cyberstrike-chat-agent-mode');
|
var mode = localStorage.getItem('cyberstrike-chat-agent-mode');
|
||||||
if (mode === 'single') mode = 'react';
|
norm = (mode && (mode === 'eino_single' || mode === 'deep' || mode === 'plan_execute' || mode === 'supervisor')) ? mode : 'eino_single';
|
||||||
if (mode === 'multi') mode = 'deep';
|
|
||||||
norm = mode || 'react';
|
|
||||||
}
|
}
|
||||||
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.isEinoSingle === 'function' && window.csaiChatAgentMode.isEinoSingle(norm)) {
|
if (cfg && cfg.multi_agent && cfg.multi_agent.enabled &&
|
||||||
return { path: '/api/eino-agent/stream', orchestration: null };
|
typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.isEino === 'function' && window.csaiChatAgentMode.isEino(norm)) {
|
||||||
}
|
|
||||||
if (!cfg || !cfg.multi_agent || !cfg.multi_agent.enabled) {
|
|
||||||
return { path: '/api/agent-loop/stream', orchestration: null };
|
|
||||||
}
|
|
||||||
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.isEino === 'function' && window.csaiChatAgentMode.isEino(norm)) {
|
|
||||||
return { path: '/api/multi-agent/stream', orchestration: norm };
|
return { path: '/api/multi-agent/stream', orchestration: norm };
|
||||||
}
|
}
|
||||||
return { path: '/api/agent-loop/stream', orchestration: null };
|
return { path: '/api/eino-agent/stream', orchestration: null };
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
return { path: '/api/agent-loop/stream', orchestration: null };
|
return { path: '/api/eino-agent/stream', orchestration: null };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,15 +297,17 @@ function wsInitAgentMode() {
|
|||||||
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') {
|
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') {
|
||||||
norm = window.csaiChatAgentMode.normalizeStored(stored, cfg);
|
norm = window.csaiChatAgentMode.normalizeStored(stored, cfg);
|
||||||
} else {
|
} else {
|
||||||
norm = stored || 'react';
|
norm = stored || 'eino_single';
|
||||||
if (norm === 'single') norm = 'react';
|
if (norm !== 'eino_single' && norm !== 'deep' && norm !== 'plan_execute' && norm !== 'supervisor') {
|
||||||
|
norm = 'eino_single';
|
||||||
|
}
|
||||||
if (norm === 'multi') norm = 'deep';
|
if (norm === 'multi') norm = 'deep';
|
||||||
}
|
}
|
||||||
wsSyncAgentMode(norm);
|
wsSyncAgentMode(norm);
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
var wrapper = document.getElementById('ws-agent-mode-wrapper');
|
var wrapper = document.getElementById('ws-agent-mode-wrapper');
|
||||||
if (wrapper) wrapper.style.display = '';
|
if (wrapper) wrapper.style.display = '';
|
||||||
wsSyncAgentMode('react');
|
wsSyncAgentMode('eino_single');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +351,10 @@ function wsCloseAgentModePanel() {
|
|||||||
function wsRefreshSelectors() {
|
function wsRefreshSelectors() {
|
||||||
wsUpdateRoleSelectorDisplay();
|
wsUpdateRoleSelectorDisplay();
|
||||||
wsRenderRoleList();
|
wsRenderRoleList();
|
||||||
var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'react';
|
var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'eino_single';
|
||||||
|
if (stored !== 'eino_single' && stored !== 'deep' && stored !== 'plan_execute' && stored !== 'supervisor') {
|
||||||
|
stored = 'eino_single';
|
||||||
|
}
|
||||||
wsSyncAgentMode(stored);
|
wsSyncAgentMode(stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2020,7 +2018,7 @@ function selectWebshell(id, stateReady) {
|
|||||||
'<div class="agent-mode-inner">' +
|
'<div class="agent-mode-inner">' +
|
||||||
'<button type="button" class="role-selector-btn agent-mode-btn" id="ws-agent-mode-btn" onclick="wsToggleAgentModePanel()">' +
|
'<button type="button" class="role-selector-btn agent-mode-btn" id="ws-agent-mode-btn" onclick="wsToggleAgentModePanel()">' +
|
||||||
'<span id="ws-agent-mode-icon" class="role-selector-icon">\ud83e\udd16</span>' +
|
'<span id="ws-agent-mode-icon" class="role-selector-icon">\ud83e\udd16</span>' +
|
||||||
'<span id="ws-agent-mode-text" class="role-selector-text">' + (wsT('chat.agentModeReactNative') || '原生 ReAct') + '</span>' +
|
'<span id="ws-agent-mode-text" class="role-selector-text">' + (wsT('chat.agentModeEinoSingle') || 'Eino 单代理') + '</span>' +
|
||||||
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
||||||
'</button>' +
|
'</button>' +
|
||||||
'<div id="ws-agent-mode-panel" class="agent-mode-panel" style="display:none;" role="listbox">' +
|
'<div id="ws-agent-mode-panel" class="agent-mode-panel" style="display:none;" role="listbox">' +
|
||||||
@@ -2028,13 +2026,12 @@ function selectWebshell(id, stateReady) {
|
|||||||
'<button type="button" class="role-selection-panel-close" onclick="wsCloseAgentModePanel()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
|
'<button type="button" class="role-selection-panel-close" onclick="wsCloseAgentModePanel()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="agent-mode-options">' +
|
'<div class="agent-mode-options">' +
|
||||||
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="react" role="option" onclick="wsSelectAgentMode(\'react\')"><div class="role-selection-item-icon-main">\ud83e\udd16</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeReactNative') || '原生 ReAct 模式') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeReactNativeHint') || '经典单代理 ReAct 与 MCP 工具') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="react">\u2713</div></button>' +
|
|
||||||
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="eino_single" role="option" onclick="wsSelectAgentMode(\'eino_single\')"><div class="role-selection-item-icon-main">\u26a1</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeEinoSingle') || 'Eino 单代理(ADK)') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeEinoSingleHint') || 'Eino ChatModelAgent + Runner') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="eino_single">\u2713</div></button>' +
|
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="eino_single" role="option" onclick="wsSelectAgentMode(\'eino_single\')"><div class="role-selection-item-icon-main">\u26a1</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeEinoSingle') || 'Eino 单代理(ADK)') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeEinoSingleHint') || 'Eino ChatModelAgent + Runner') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="eino_single">\u2713</div></button>' +
|
||||||
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="deep" role="option" onclick="wsSelectAgentMode(\'deep\')"><div class="role-selection-item-icon-main">\ud83e\udde9</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeDeep') || 'Deep(DeepAgent)') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeDeepHint') || 'Eino DeepAgent,task 调度子代理') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="deep">\u2713</div></button>' +
|
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="deep" role="option" onclick="wsSelectAgentMode(\'deep\')"><div class="role-selection-item-icon-main">\ud83e\udde9</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeDeep') || 'Deep(DeepAgent)') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeDeepHint') || 'Eino DeepAgent,task 调度子代理') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="deep">\u2713</div></button>' +
|
||||||
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="plan_execute" role="option" onclick="wsSelectAgentMode(\'plan_execute\')"><div class="role-selection-item-icon-main">\ud83d\udccb</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModePlanExecuteLabel') || 'Plan-Execute') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModePlanExecuteHint') || '规划 → 执行 → 重规划') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="plan_execute">\u2713</div></button>' +
|
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="plan_execute" role="option" onclick="wsSelectAgentMode(\'plan_execute\')"><div class="role-selection-item-icon-main">\ud83d\udccb</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModePlanExecuteLabel') || 'Plan-Execute') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModePlanExecuteHint') || '规划 → 执行 → 重规划') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="plan_execute">\u2713</div></button>' +
|
||||||
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="supervisor" role="option" onclick="wsSelectAgentMode(\'supervisor\')"><div class="role-selection-item-icon-main">\ud83c\udfaf</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeSupervisorLabel') || 'Supervisor') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeSupervisorHint') || '监督者协调,transfer 委派子代理') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="supervisor">\u2713</div></button>' +
|
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="supervisor" role="option" onclick="wsSelectAgentMode(\'supervisor\')"><div class="role-selection-item-icon-main">\ud83c\udfaf</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeSupervisorLabel') || 'Supervisor') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeSupervisorHint') || '监督者协调,transfer 委派子代理') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="supervisor">\u2713</div></button>' +
|
||||||
'</div></div></div>' +
|
'</div></div></div>' +
|
||||||
'<input type="hidden" id="ws-agent-mode-select" value="react" autocomplete="off" />' +
|
'<input type="hidden" id="ws-agent-mode-select" value="eino_single" autocomplete="off" />' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="webshell-ai-input-row">' +
|
'<div class="webshell-ai-input-row">' +
|
||||||
|
|||||||
+96
-23
@@ -1031,14 +1031,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-mode-options">
|
<div class="agent-mode-options">
|
||||||
<button type="button" class="role-selection-item-main agent-mode-option" data-value="react" role="option" onclick="selectAgentMode('react')">
|
|
||||||
<div class="role-selection-item-icon-main" aria-hidden="true">🤖</div>
|
|
||||||
<div class="role-selection-item-content-main">
|
|
||||||
<div class="role-selection-item-name-main" data-i18n="chat.agentModeReactNative">原生 ReAct 模式</div>
|
|
||||||
<div class="role-selection-item-description-main" data-i18n="chat.agentModeReactNativeHint">经典单代理 ReAct 与 MCP 工具(/api/agent-loop)</div>
|
|
||||||
</div>
|
|
||||||
<div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="react">✓</div>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="role-selection-item-main agent-mode-option" data-value="eino_single" role="option" onclick="selectAgentMode('eino_single')">
|
<button type="button" class="role-selection-item-main agent-mode-option" data-value="eino_single" role="option" onclick="selectAgentMode('eino_single')">
|
||||||
<div class="role-selection-item-icon-main" aria-hidden="true">⚡</div>
|
<div class="role-selection-item-icon-main" aria-hidden="true">⚡</div>
|
||||||
<div class="role-selection-item-content-main">
|
<div class="role-selection-item-content-main">
|
||||||
@@ -1074,7 +1066,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="agent-mode-select" value="react" autocomplete="off">
|
<input type="hidden" id="agent-mode-select" value="eino_single" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input-with-files">
|
<div class="chat-input-with-files">
|
||||||
@@ -1122,20 +1114,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MCP状态监控页面 -->
|
<!-- MCP状态监控页面 -->
|
||||||
<div id="page-mcp-monitor" class="page">
|
<div id="page-mcp-monitor" class="page mcp-monitor-page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2 data-i18n="mcp.monitorTitle">MCP 状态监控</h2>
|
<div class="page-header-main">
|
||||||
<button class="btn-secondary" onclick="refreshMonitorPanel()"><span data-i18n="common.refresh">刷新</span></button>
|
<h2 data-i18n="mcp.monitorTitle">MCP 状态监控</h2>
|
||||||
|
<p id="monitor-stats-subtitle" class="monitor-page-subtitle" hidden></p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-actions">
|
||||||
|
<button type="button" class="btn-secondary btn-icon-text" onclick="refreshMonitorPanel()" aria-label="刷新">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||||
|
<span data-i18n="common.refresh">刷新</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="monitor-sections">
|
<div class="monitor-sections">
|
||||||
<section class="monitor-section monitor-overview">
|
<section class="monitor-section monitor-overview">
|
||||||
<div class="section-header monitor-stats-section-header">
|
|
||||||
<div class="monitor-stats-header-text">
|
|
||||||
<h3 data-i18n="mcp.execStats">执行统计</h3>
|
|
||||||
<p id="monitor-stats-subtitle" class="monitor-stats-subtitle" hidden></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="monitor-stats" class="mcp-exec-stats-root">
|
<div id="monitor-stats" class="mcp-exec-stats-root">
|
||||||
<div class="monitor-empty" data-i18n="mcpMonitor.loading">加载中...</div>
|
<div class="monitor-empty" data-i18n="mcpMonitor.loading">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1720,6 +1714,7 @@
|
|||||||
<h2 data-i18n="vulnerability.title">漏洞管理</h2>
|
<h2 data-i18n="vulnerability.title">漏洞管理</h2>
|
||||||
<div class="page-header-actions">
|
<div class="page-header-actions">
|
||||||
<button class="btn-secondary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
<button class="btn-secondary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
||||||
|
<button class="btn-secondary btn-delete" onclick="batchDeleteVulnerabilityReports()" data-i18n="vulnerabilityPage.batchDelete">批量删除</button>
|
||||||
<button class="btn-secondary" onclick="refreshVulnerabilities()" data-i18n="common.refresh">刷新</button>
|
<button class="btn-secondary" onclick="refreshVulnerabilities()" data-i18n="common.refresh">刷新</button>
|
||||||
<button class="btn-primary" onclick="showAddVulnerabilityModal()" data-i18n="vulnerability.addVuln">添加漏洞</button>
|
<button class="btn-primary" onclick="showAddVulnerabilityModal()" data-i18n="vulnerability.addVuln">添加漏洞</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2356,7 +2351,7 @@
|
|||||||
<input type="text" id="agent-md-bind-role" placeholder="" autocomplete="off" />
|
<input type="text" id="agent-md-bind-role" placeholder="" autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="agentsPage.fieldMaxIter">子代理最大迭代(0=使用全局默认)</label>
|
<label data-i18n="agentsPage.fieldMaxIter">最大迭代(0=沿用设置页 agent.max_iterations)</label>
|
||||||
<input type="number" id="agent-md-max-iter" min="0" value="0" />
|
<input type="number" id="agent-md-max-iter" min="0" value="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -2483,13 +2478,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Vision 视觉分析 -->
|
||||||
|
<div class="settings-subsection">
|
||||||
|
<h4 data-i18n="settingsBasic.visionConfig">视觉分析(analyze_image)</h4>
|
||||||
|
<div class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="vision-enabled" class="modern-checkbox" onchange="syncVisionFormEnabled()" />
|
||||||
|
<span class="checkbox-custom"></span>
|
||||||
|
<span class="checkbox-text" data-i18n="settingsBasic.visionEnabled">启用视觉分析工具 analyze_image</span>
|
||||||
|
</label>
|
||||||
|
<small class="form-hint" data-i18n="settingsBasic.visionEnabledHint">启用后注册 MCP 工具;图片仅在单次 VL 调用中出现,Agent 上下文只保留文字摘要。</small>
|
||||||
|
</div>
|
||||||
|
<div id="vision-fields-panel">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-provider" data-i18n="settingsBasic.provider">提供商</label>
|
||||||
|
<select id="vision-provider" style="width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 6px; background: var(--card-bg, #fff); color: var(--text-color, #2d3748); font-size: 0.875rem;">
|
||||||
|
<option value="">OpenAI 配置(留空复用)</option>
|
||||||
|
<option value="openai">OpenAI / 兼容 OpenAI 协议</option>
|
||||||
|
<option value="claude">Claude (Anthropic Messages API)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-base-url" data-i18n="settingsBasic.baseUrl">Base URL</label>
|
||||||
|
<input type="text" id="vision-base-url" data-i18n="settingsBasic.visionBaseUrlPlaceholder" data-i18n-attr="placeholder" placeholder="留空则复用 OpenAI Base URL" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-api-key" data-i18n="settingsBasic.apiKey">API Key</label>
|
||||||
|
<input type="password" id="vision-api-key" data-i18n="settingsBasic.visionApiKeyPlaceholder" data-i18n-attr="placeholder" placeholder="留空则复用 OpenAI API Key" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-model"><span data-i18n="settingsBasic.visionModel">视觉模型</span> <span style="color: red;">*</span></label>
|
||||||
|
<input type="text" id="vision-model" data-i18n="settingsBasic.visionModelPlaceholder" data-i18n-attr="placeholder" placeholder="qwen-vl-max" />
|
||||||
|
</div>
|
||||||
|
<details style="margin-top: 8px;">
|
||||||
|
<summary style="cursor: pointer; font-size: 0.875rem; color: var(--accent-color, #3182ce);" data-i18n="settingsBasic.visionAdvanced">高级:预处理与限制</summary>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-max-image-bytes" data-i18n="settingsBasic.visionMaxImageBytes">原始文件上限(字节)</label>
|
||||||
|
<input type="number" id="vision-max-image-bytes" min="0" step="1024" placeholder="5242880" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-max-dimension" data-i18n="settingsBasic.visionMaxDimension">长边缩放像素</label>
|
||||||
|
<input type="number" id="vision-max-dimension" min="256" step="1" placeholder="2048" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-jpeg-quality" data-i18n="settingsBasic.visionJpegQuality">JPEG 质量</label>
|
||||||
|
<input type="number" id="vision-jpeg-quality" min="60" max="100" step="1" placeholder="82" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-max-payload-bytes" data-i18n="settingsBasic.visionMaxPayloadBytes">送 API 体积上限(字节)</label>
|
||||||
|
<input type="number" id="vision-max-payload-bytes" min="0" step="1024" placeholder="524288" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-skip-preprocess-bytes" data-i18n="settingsBasic.visionSkipPreprocessBytes">低于该字节可原图直传</label>
|
||||||
|
<input type="number" id="vision-skip-preprocess-bytes" min="0" step="1024" placeholder="2097152" />
|
||||||
|
<small class="form-hint" data-i18n="settingsBasic.visionSkipPreprocessHint">0 表示始终 JPEG 压缩;需同时满足长边与 payload 限制。</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-detail" data-i18n="settingsBasic.visionDetail">Image detail</label>
|
||||||
|
<select id="vision-detail" style="width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 6px;">
|
||||||
|
<option value="low">low</option>
|
||||||
|
<option value="auto">auto</option>
|
||||||
|
<option value="high">high</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vision-timeout-seconds" data-i18n="settingsBasic.visionTimeout">超时(秒)</label>
|
||||||
|
<input type="number" id="vision-timeout-seconds" min="5" step="1" placeholder="60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||||
|
<a href="javascript:void(0)" id="test-vision-btn" onclick="testVisionConnection()" style="font-size: 0.8125rem; color: var(--accent-color, #3182ce); text-decoration: none; cursor: pointer;" data-i18n="settingsBasic.testConnection">测试连接</a>
|
||||||
|
<span id="test-vision-result" style="font-size: 0.8125rem;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Agent配置 -->
|
<!-- Agent配置 -->
|
||||||
<div class="settings-subsection">
|
<div class="settings-subsection">
|
||||||
<h4 data-i18n="settingsBasic.agentConfig">Agent 配置</h4>
|
<h4 data-i18n="settingsBasic.agentConfig">Agent 配置</h4>
|
||||||
<div class="settings-form">
|
<div class="settings-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="agent-max-iterations" data-i18n="settingsBasic.maxIterations">最大迭代次数</label>
|
<label for="agent-max-iterations" data-i18n="settingsBasic.maxIterations">最大迭代次数</label>
|
||||||
<input type="number" id="agent-max-iterations" min="1" max="100" data-i18n="settingsBasic.iterationsPlaceholder" data-i18n-attr="placeholder" placeholder="30" />
|
<input type="number" id="agent-max-iterations" min="1" data-i18n="settingsBasic.iterationsPlaceholder" data-i18n-attr="placeholder" placeholder="30" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
@@ -2507,7 +2581,6 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="multi-agent-robot-mode" data-i18n="settingsBasic.multiAgentRobotMode">机器人默认对话模式</label>
|
<label for="multi-agent-robot-mode" data-i18n="settingsBasic.multiAgentRobotMode">机器人默认对话模式</label>
|
||||||
<select id="multi-agent-robot-mode" class="form-select">
|
<select id="multi-agent-robot-mode" class="form-select">
|
||||||
<option value="react" data-i18n="chat.agentModeReactNative">原生 ReAct</option>
|
|
||||||
<option value="eino_single" data-i18n="chat.agentModeEinoSingle">Eino 单代理(ADK)</option>
|
<option value="eino_single" data-i18n="chat.agentModeEinoSingle">Eino 单代理(ADK)</option>
|
||||||
<option value="deep" data-i18n="chat.agentModeDeep">Deep(DeepAgent)</option>
|
<option value="deep" data-i18n="chat.agentModeDeep">Deep(DeepAgent)</option>
|
||||||
<option value="plan_execute" data-i18n="chat.agentModePlanExecuteLabel">Plan-Execute</option>
|
<option value="plan_execute" data-i18n="chat.agentModePlanExecuteLabel">Plan-Execute</option>
|
||||||
@@ -3438,6 +3511,7 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||||
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
|
||||||
|
<script src="/static/js/sanitize-markdown.js"></script>
|
||||||
<!-- Cytoscape.js for attack chain visualization -->
|
<!-- Cytoscape.js for attack chain visualization -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script>
|
||||||
<!-- ELK.js for high-quality DAG layout (reduces edge crossings) -->
|
<!-- ELK.js for high-quality DAG layout (reduces edge crossings) -->
|
||||||
@@ -3760,13 +3834,12 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="batch-queue-agent-mode" data-i18n="batchImportModal.agentMode">代理模式</label>
|
<label for="batch-queue-agent-mode" data-i18n="batchImportModal.agentMode">代理模式</label>
|
||||||
<select id="batch-queue-agent-mode" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
|
<select id="batch-queue-agent-mode" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
|
||||||
<option value="single" data-i18n="chat.agentModeReactNative">原生 ReAct 模式</option>
|
|
||||||
<option value="eino_single" data-i18n="chat.agentModeEinoSingle">Eino 单代理(ADK)</option>
|
<option value="eino_single" data-i18n="chat.agentModeEinoSingle">Eino 单代理(ADK)</option>
|
||||||
<option value="deep" data-i18n="chat.agentModeDeep">Deep(DeepAgent)</option>
|
<option value="deep" data-i18n="chat.agentModeDeep">Deep(DeepAgent)</option>
|
||||||
<option value="plan_execute" data-i18n="chat.agentModePlanExecuteLabel">Plan-Execute</option>
|
<option value="plan_execute" data-i18n="chat.agentModePlanExecuteLabel">Plan-Execute</option>
|
||||||
<option value="supervisor" data-i18n="chat.agentModeSupervisorLabel">Supervisor</option>
|
<option value="supervisor" data-i18n="chat.agentModeSupervisorLabel">Supervisor</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.agentModeHint">与对话页一致:原生 ReAct、Eino 单代理(ADK),或 Deep / Plan-Execute / Supervisor(后三种需已启用多代理)。</div>
|
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.agentModeHint">与对话页一致:Eino 单代理(ADK),或 Deep / Plan-Execute / Supervisor(后三种需已启用多代理)。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="batch-queue-schedule-mode" data-i18n="batchImportModal.scheduleMode">调度方式</label>
|
<label for="batch-queue-schedule-mode" data-i18n="batchImportModal.scheduleMode">调度方式</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user