Compare commits

...

8 Commits

Author SHA1 Message Date
公明 f2a701a846 Update config.yaml 2026-04-21 01:27:46 +08:00
公明 0ce79c6ef4 Add files via upload 2026-04-21 01:26:49 +08:00
公明 0d4f608c14 Add files via upload 2026-04-21 01:25:40 +08:00
公明 c801a97add Add files via upload 2026-04-21 01:24:01 +08:00
公明 68978b82e9 Add files via upload 2026-04-20 20:01:02 +08:00
公明 c43fde2612 Add files via upload 2026-04-20 19:46:40 +08:00
公明 fbd1ede8cb Add files via upload 2026-04-20 19:45:04 +08:00
公明 2d8ef3a1b0 Add files via upload 2026-04-20 19:42:11 +08:00
31 changed files with 3051 additions and 913 deletions
+4 -7
View File
@@ -117,7 +117,7 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
- 📋 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
- 🧩 **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))
- 🎯 **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, …) can still be bound to roles
- 🎯 **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)
- 🐚 **WebShell management**: Add and manage WebShell connections (e.g. IceSword/AntSword compatible), use a virtual terminal for command execution, a built-in file manager for file operations, and an AI assistant tab that orchestrates tests and keeps per-connection conversation history; supports PHP, ASP, ASPX, JSP and custom shell types with configurable request method and command parameter.
@@ -250,8 +250,8 @@ 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.
- **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).
- **Skills integration** Roles can attach security testing skills. Skill ids are hinted in the system prompt; in **multi-agent** sessions the Eino ADK **`skill`** tool loads package content **on demand** (progressive disclosure). **`multi_agent.eino_skills`** toggles the middleware, tool name override, and optional **read_file / glob / grep / write / edit / execute** on the host (**Deep / Supervisor** main and sub-agents when enabled; **plan_execute** executor has no custom skill middleware—see docs). Single-agent ReAct does not mount this Eino skill stack today.
- **Easy role creation** Create custom roles by adding YAML files to the `roles/` directory. Each role defines `name`, `description`, `user_prompt`, `icon`, `tools`, `skills`, and `enabled` fields.
- **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.
- **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.
**Creating a custom role (example):**
@@ -265,8 +265,6 @@ Requirements / tips:
- api-fuzzer
- arjun
- graphql-scanner
skills:
- cyberstrike-eino-demo
enabled: true
```
2. Restart the server or reload configuration; the role appears in the role selector dropdown.
@@ -286,14 +284,13 @@ Requirements / tips:
- **Layout** Each skill is a directory with **required** `SKILL.md` only ([Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)): YAML front matter **only** `name` and `description`, plus Markdown body. Optional sibling files (`FORMS.md`, `REFERENCE.md`, `scripts/*`, …). **No** `SKILL.yaml` (not part of Claude or Eino specs); sections/scripts/progressive behavior are **derived at runtime** from Markdown and the filesystem.
- **Runtime refactor** **`skills_dir`** is the single root for packs. **Multi-agent** loads them through Einos official **`skill`** middleware (**progressive disclosure**: model calls `skill` with a pack **name** instead of receiving full SKILL text up front). Configure via **`multi_agent.eino_skills`**: `disable`, `filesystem_tools` (host read/glob/grep/write/edit/execute), `skill_tool_name`.
- **Eino / RAG** Packages are also split into `schema.Document` chunks for `FilesystemSkillsRetriever` (`skills.AsEinoRetriever()`) in **compose** graphs (e.g. knowledge/indexing pipelines).
- **Skill hints in prompts** Role-bound skill **ids** (directory names) are recommended in the system prompt; full text is not injected by default.
- **HTTP API** `/api/skills` listing and `depth` (`summary` | `full`), `section`, and `resource_path` remain for the web UI and ops; **model-side** skill loading in multi-agent uses the **`skill`** tool, not MCP.
- **Optional `eino_middleware`** e.g. `tool_search` (dynamic MCP tool list), `patch_tool_calls`, `plantask` (structured tasks; persistence defaults under a subdirectory of `skills_dir`), `reduction`, `checkpoint_dir`, Deep output key / model retries / task-tool description prefix—see `config.yaml` and `internal/config/config.go`.
- **Shipped demo** `skills/cyberstrike-eino-demo/`; see `skills/README.md`.
**Creating a skill:**
1. `mkdir skills/<skill-id>` and add standard `SKILL.md` (+ any optional files), or drop in an open-source skill folder as-is.
2. Reference `<skill-id>` in a roles `skills` list in `roles/*.yaml`.
2. Use **multi-agent** with **`multi_agent.eino_skills`** enabled so the model can call the **`skill`** tool with that pack **name**.
### Tool Orchestration & Extensions
- **YAML recipes** in `tools/*.yaml` describe commands, arguments, prompts, and metadata.
+3 -6
View File
@@ -248,8 +248,8 @@ go build -o cyberstrike-ai cmd/server/main.go
- **预设角色**:系统内置 12+ 个预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试、二进制分析、云安全审计等),位于 `roles/` 目录。
- **自定义提示词**:每个角色可定义 `user_prompt`,会在用户消息前自动添加,引导 AI 采用特定的测试方法和关注重点。
- **工具限制**:角色可指定 `tools` 列表,限制可用工具,实现聚焦的测试流程(如 CTF 角色限制为 CTF 专用工具)。
- **Skills 集成**:角色可附加安全测试技能,id 写入提示;**多代理** 下由 Eino **`skill`** 工具 **按需加载**(渐进式披露)。**`multi_agent.eino_skills`** 控制中间件与本机 read_file/glob/grep/write/edit/execute**Deep / Supervisor** 主/子代理;**plan_execute** 执行器无独立 skill 中间件,见文档)。**单代理 ReAct** 当前不挂载该 Eino skill 链。
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`skills`、`enabled` 字段。
- **Skills**:技能包位于 `skills_dir`**多代理 / Eino** 下由 **`skill`** 工具 **按需加载**(渐进式披露)。**`multi_agent.eino_skills`** 控制中间件与本机 read_file/glob/grep/write/edit/execute**Deep / Supervisor** 主/子代理;**plan_execute** 执行器无独立 skill 中间件,见文档)。**单代理 ReAct** 当前不挂载该 Eino skill 链。
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`enabled` 字段。
- **Web 界面集成**:在聊天界面通过下拉菜单选择角色。角色选择会影响 AI 行为和可用工具建议。
**创建自定义角色示例:**
@@ -263,8 +263,6 @@ go build -o cyberstrike-ai cmd/server/main.go
- api-fuzzer
- arjun
- graphql-scanner
skills:
- cyberstrike-eino-demo
enabled: true
```
2. 重启服务或重新加载配置,角色会出现在角色选择下拉菜单中。
@@ -284,14 +282,13 @@ go build -o cyberstrike-ai cmd/server/main.go
- **目录规范**:与 [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) 一致,**仅**需目录下的 **`SKILL.md`**YAML 头只用官方的 **`name` 与 `description`**,正文为 Markdown。可选同目录其他文件(`FORMS.md`、`REFERENCE.md`、`scripts/*` 等)。**不使用 `SKILL.yaml`**Claude / Eino 官方均无此文件);章节、`scripts/` 列表、渐进式行为由运行时从正文与磁盘 **自动推导**。
- **运行侧重构****`skills_dir`** 为技能包唯一根目录;**多代理** 通过 Eino 官方 **`skill`** 中间件做 **渐进式披露**(模型按 **name** 调用 `skill`,而非一次性注入全文)。由 **`multi_agent.eino_skills`** 控制:`disable`、`filesystem_tools`(本机读写与 Shell)、`skill_tool_name`。
- **Eino / 知识流水线**:技能包可切分为 `schema.Document`,供 `FilesystemSkillsRetriever``skills.AsEinoRetriever()`)在 **compose** 图(如索引/编排)中使用。
- **提示词**:角色绑定的技能 **id**(文件夹名)会作为推荐写入系统提示;正文默认不整包注入。
- **HTTP 管理**`/api/skills` 列表与 `depth=summary|full`、`section`、`resource_path` 等仍用于 Web 与运维;**模型侧** 多代理走 **`skill`** 工具,而非 MCP。
- **可选 `eino_middleware`**:如 `tool_search`(动态工具列表)、`patch_tool_calls`、`plantask`(结构化任务;默认落在 `skills_dir` 下子目录)、`reduction`、`checkpoint_dir`、Deep 输出键 / 模型重试 / task 描述前缀等,见 `config.yaml` 与 `internal/config/config.go`。
- **自带示例**`skills/cyberstrike-eino-demo/`;说明见 `skills/README.md`。
**新建技能:**
1. 在 `skills/` 下创建 `<skill-id>/`,放入标准 `SKILL.md`(及任意可选文件),或直接解压开源技能包到该目录。
2. 在 `roles/*.yaml` 的 `skills` 列表中引用该 `<skill-id>`。
2. 启用 **`multi_agent.eino_skills`** 并使用 **多代理** 会话,由模型通过 **`skill`** 工具按包 **name** 加载
### 工具编排与扩展
- `tools/*.yaml` 定义命令、参数、提示词与元数据,可热加载。
+1 -1
View File
@@ -10,7 +10,7 @@
# ============================================
# 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.5.2"
version: "v1.5.3"
# 服务器配置
server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
+1
View File
@@ -58,3 +58,4 @@
| 2026-03-22 | `agents/*.md` 子代理定义、`agents_dir`、合并进 `RunDeepAgent`、前端 Agents 菜单与 CRUD API。 |
| 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-21 | 移除角色 `skills``/api/roles/skills/list``bind_role` 仅继承 toolsSkills 仅通过 Eino `skill` 工具按需加载。 |
+5 -44
View File
@@ -316,16 +316,16 @@ type ProgressCallback func(eventType, message string, data interface{})
// AgentLoop 执行Agent循环
func (a *Agent) AgentLoop(ctx context.Context, userInput string, historyMessages []ChatMessage) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil, nil, nil)
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil, nil)
}
// AgentLoopWithConversationID 执行Agent循环(带对话ID
func (a *Agent) AgentLoopWithConversationID(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil, nil)
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil)
}
// EinoSingleAgentSystemInstruction 供 Eino adk.ChatModelAgent.Instruction 使用,与 AgentLoopWithProgress 首条 system 对齐(含 system_prompt_path 与 Skills 提示)。
func (a *Agent) EinoSingleAgentSystemInstruction(roleSkills []string) string {
// EinoSingleAgentSystemInstruction 供 Eino adk.ChatModelAgent.Instruction 使用,与 AgentLoopWithProgress 首条 system 对齐(含 system_prompt_path)。
func (a *Agent) EinoSingleAgentSystemInstruction() string {
systemPrompt := DefaultSingleAgentSystemPrompt()
if a.agentConfig != nil {
if p := strings.TrimSpace(a.agentConfig.SystemPromptPath); p != "" {
@@ -343,30 +343,11 @@ func (a *Agent) EinoSingleAgentSystemInstruction(roleSkills []string) string {
}
}
}
if len(roleSkills) > 0 {
var skillsHint strings.Builder
skillsHint.WriteString("\n\n本角色推荐使用的Skills\n")
for i, skillName := range roleSkills {
if i > 0 {
skillsHint.WriteString("、")
}
skillsHint.WriteString("`")
skillsHint.WriteString(skillName)
skillsHint.WriteString("`")
}
skillsHint.WriteString("\n- 这些名称与 skills/ 下 SKILL.md 的 `name` 一致。")
skillsHint.WriteString("\n- 若当前会话已启用 Eino 内置 `skill` 工具,请按需加载;否则以 MCP 与文本工作流完成。")
skillsHint.WriteString("\n- 例如传入 skill 参数为 `")
skillsHint.WriteString(roleSkills[0])
skillsHint.WriteString("`")
systemPrompt += skillsHint.String()
}
return systemPrompt
}
// AgentLoopWithProgress 执行Agent循环(带进度回调和对话ID)
// roleSkills: 角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string, roleSkills []string) (*AgentLoopResult, error) {
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string) (*AgentLoopResult, error) {
// 设置当前对话ID
a.mu.Lock()
a.currentConversationID = conversationID
@@ -396,26 +377,6 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
}
}
// 如果角色配置了skills,在系统提示词中提示AI(但不硬编码内容)
if len(roleSkills) > 0 {
var skillsHint strings.Builder
skillsHint.WriteString("\n\n本角色推荐使用的Skills\n")
for i, skillName := range roleSkills {
if i > 0 {
skillsHint.WriteString("、")
}
skillsHint.WriteString("`")
skillsHint.WriteString(skillName)
skillsHint.WriteString("`")
}
skillsHint.WriteString("\n- 这些名称与 skills/ 下 SKILL.md 的 `name` 一致;在 **Eino 多代理** 会话中请用内置 `skill` 工具按需加载全文")
skillsHint.WriteString("\n- 例如:在支持 Eino skill 工具时传入 skill 参数为 `")
skillsHint.WriteString(roleSkills[0])
skillsHint.WriteString("`")
skillsHint.WriteString("\n- 单代理 MCP 模式不会注入 skill 工具;需要时请使用多代理(DeepAgent")
systemPrompt += skillsHint.String()
}
messages := []ChatMessage{
{
Role: "system",
-2
View File
@@ -327,7 +327,6 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
externalMCPHandler := handler.NewExternalMCPHandler(externalMCPMgr, cfg, configPath, log.Logger)
roleHandler := handler.NewRoleHandler(cfg, configPath, log.Logger)
roleHandler.SetSkillsManager(skillpackage.DirLister{SkillsRoot: skillsDir})
skillsHandler := handler.NewSkillsHandler(cfg, configPath, log.Logger)
fofaHandler := handler.NewFofaHandler(cfg, log.Logger)
terminalHandler := handler.NewTerminalHandler(log.Logger)
@@ -881,7 +880,6 @@ func setupRoutes(
// 角色管理
protected.GET("/roles", roleHandler.GetRoles)
protected.GET("/roles/:name", roleHandler.GetRole)
protected.GET("/roles/skills/list", roleHandler.GetSkills)
protected.POST("/roles", roleHandler.CreateRole)
protected.PUT("/roles/:name", roleHandler.UpdateRole)
protected.DELETE("/roles/:name", roleHandler.DeleteRole)
+4 -5
View File
@@ -120,7 +120,7 @@ type MultiAgentSubConfig struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
Instruction string `yaml:"instruction" json:"instruction"`
BindRole string `yaml:"bind_role,omitempty" json:"bind_role,omitempty"` // 可选:关联主配置 roles 中的角色名;未配 role_tools 时沿用该角色的 tools,并把 skills 写入指令提示
BindRole string `yaml:"bind_role,omitempty" json:"bind_role,omitempty"` // 可选:关联主配置 roles 中的角色名;未配 role_tools 时沿用该角色的 tools
RoleTools []string `yaml:"role_tools" json:"role_tools"` // 与单 Agent 角色工具相同 key;空表示全部工具(bind_role 可补全 tools
MaxIterations int `yaml:"max_iterations" json:"max_iterations"`
Kind string `yaml:"kind,omitempty" json:"kind,omitempty"` // 仅 Markdownkind=orchestrator 表示 Deep 主代理(与 orchestrator.md 二选一约定)
@@ -933,8 +933,7 @@ type RoleConfig struct {
Description string `yaml:"description" json:"description"` // 角色描述
UserPrompt string `yaml:"user_prompt" json:"user_prompt"` // 用户提示词(追加到用户消息前)
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` // 角色图标(可选)
Tools []string `yaml:"tools,omitempty" json:"tools,omitempty"` // 关联的工具列表(toolKey格式,如 "toolName" 或 "mcpName::toolName"
MCPs []string `yaml:"mcps,omitempty" json:"mcps,omitempty"` // 向后兼容:关联的MCP服务器列表(已废弃,使用tools替代)
Skills []string `yaml:"skills,omitempty" json:"skills,omitempty"` // 关联的skills列表(skill名称列表,在执行任务前会读取这些skills的内容)
Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用
Tools []string `yaml:"tools,omitempty" json:"tools,omitempty"` // 关联的工具列表(toolKey格式,如 "toolName" 或 "mcpName::toolName"
MCPs []string `yaml:"mcps,omitempty" json:"mcps,omitempty"` // 向后兼容:关联的MCP服务器列表(已废弃,使用tools替代)
Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用
}
+32 -33
View File
@@ -494,8 +494,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
// 应用角色用户提示词和工具配置
finalMessage := req.Message
var roleTools []string // 角色配置的工具列表
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
var roleTools []string // 角色配置的工具列表
// WebShell AI 助手模式:绑定当前连接,仅开放 webshell_* 工具并注入 connection_id
if req.WebShellConnectionID != "" {
@@ -509,8 +508,19 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
if remark == "" {
remark = conn.URL
}
finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base。Skills 包请使用「多代理 / Eino DeepAgent」会话中的内置 `skill` 工具渐进加载。\n\n用户请求:%s",
webshellContext := fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base。Skills 包请使用「多代理 / Eino DeepAgent」会话中的内置 `skill` 工具渐进加载。\n\n用户请求:%s",
conn.ID, remark, conn.ID, req.Message)
// WebShell 模式下如果同时指定了角色,追加角色 user_prompt(工具集仍仅限 webshell 专用工具)
if req.Role != "" && req.Role != "默认" && h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled && role.UserPrompt != "" {
finalMessage = role.UserPrompt + "\n\n" + webshellContext
h.logger.Info("WebShell + 角色: 应用角色提示词", zap.String("role", req.Role))
} else {
finalMessage = webshellContext
}
} else {
finalMessage = webshellContext
}
roleTools = []string{
builtin.ToolWebshellExec,
builtin.ToolWebshellFileList,
@@ -520,7 +530,6 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
builtin.ToolListKnowledgeRiskTypes,
builtin.ToolSearchKnowledgeBase,
}
roleSkills = nil
} else if req.Role != "" && req.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
@@ -534,11 +543,6 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
roleTools = role.Tools
h.logger.Info("使用角色配置的工具列表", zap.String("role", req.Role), zap.Int("toolCount", len(roleTools)))
}
// 获取角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
if len(role.Skills) > 0 {
roleSkills = role.Skills
h.logger.Info("角色配置了skills,将在系统提示词中提示AI", zap.String("role", req.Role), zap.Int("skillCount", len(roleSkills)), zap.Strings("skills", roleSkills))
}
}
}
}
@@ -563,8 +567,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
}
// 执行Agent Loop,传入历史消息和对话ID(使用包含角色提示词的finalMessage和角色工具列表)
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
result, err := h.agent.AgentLoopWithProgress(c.Request.Context(), finalMessage, agentHistoryMessages, conversationID, nil, roleTools, roleSkills)
result, err := h.agent.AgentLoopWithProgress(c.Request.Context(), finalMessage, agentHistoryMessages, conversationID, nil, roleTools)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
@@ -635,14 +638,13 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
}
finalMessage := message
var roleTools, roleSkills []string
var roleTools []string
if role != "" && role != "默认" && h.config.Roles != nil {
if r, exists := h.config.Roles[role]; exists && r.Enabled {
if r.UserPrompt != "" {
finalMessage = r.UserPrompt + "\n\n" + message
}
roleTools = r.Tools
roleSkills = r.Skills
}
}
@@ -709,7 +711,7 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
return resultMA.Response, conversationID, nil
}
result, err := h.agent.AgentLoopWithProgress(ctx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools, roleSkills)
result, err := h.agent.AgentLoopWithProgress(ctx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
if err != nil {
errMsg := "执行失败: " + err.Error()
if assistantMessageID != "" {
@@ -1252,7 +1254,6 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 应用角色用户提示词和工具配置
finalMessage := req.Message
var roleTools []string // 角色配置的工具列表
var roleSkills []string
if req.WebShellConnectionID != "" {
conn, errConn := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
if errConn != nil || conn == nil {
@@ -1264,8 +1265,19 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
if remark == "" {
remark = conn.URL
}
finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base。Skills 包请使用「多代理 / Eino DeepAgent」会话中的内置 `skill` 工具渐进加载。\n\n用户请求:%s",
webshellContext := fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base。Skills 包请使用「多代理 / Eino DeepAgent」会话中的内置 `skill` 工具渐进加载。\n\n用户请求:%s",
conn.ID, remark, conn.ID, req.Message)
// WebShell 模式下如果同时指定了角色,追加角色 user_prompt(工具集仍仅限 webshell 专用工具)
if req.Role != "" && req.Role != "默认" && h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled && role.UserPrompt != "" {
finalMessage = role.UserPrompt + "\n\n" + webshellContext
h.logger.Info("WebShell + 角色: 应用角色提示词(流式)", zap.String("role", req.Role))
} else {
finalMessage = webshellContext
}
} else {
finalMessage = webshellContext
}
roleTools = []string{
builtin.ToolWebshellExec,
builtin.ToolWebshellFileList,
@@ -1292,11 +1304,6 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 因为mcps是MCP服务器名称,不是工具列表
h.logger.Info("角色配置使用旧的mcps字段,将使用所有工具", zap.String("role", req.Role))
}
// 注意:角色 skills 仅在系统提示词中提示;运行时加载请使用 Eino 多代理内置 `skill` 工具
if len(role.Skills) > 0 {
roleSkills = role.Skills
h.logger.Info("角色配置了skillsAI可通过工具按需调用", zap.String("role", req.Role), zap.Int("skillCount", len(role.Skills)), zap.Strings("skills", role.Skills))
}
}
}
}
@@ -1401,12 +1408,11 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 执行Agent Loop,传入独立的上下文,确保任务不会因客户端断开而中断(使用包含角色提示词的finalMessage和角色工具列表)
sendEvent("progress", "正在分析您的请求...", nil)
// 注意:roleSkills 已在上方根据 req.Role 或 WebShell 模式设置
stopKeepalive := make(chan struct{})
go sseKeepalive(c, stopKeepalive, &sseWriteMu)
defer close(stopKeepalive)
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools, roleSkills)
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
cause := context.Cause(baseCtx)
@@ -2220,8 +2226,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
// 应用角色用户提示词和工具配置
finalMessage := task.Message
var roleTools []string // 角色配置的工具列表
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
var roleTools []string // 角色配置的工具列表
if queue.Role != "" && queue.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[queue.Role]; exists && role.Enabled {
@@ -2235,11 +2240,6 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
roleTools = role.Tools
h.logger.Info("使用角色配置的工具列表", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role), zap.Int("toolCount", len(roleTools)))
}
// 获取角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
if len(role.Skills) > 0 {
roleSkills = role.Skills
h.logger.Info("角色配置了skills,将在系统提示词中提示AI", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role), zap.Int("skillCount", len(roleSkills)), zap.Strings("skills", roleSkills))
}
}
}
}
@@ -2273,7 +2273,6 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
// 存储取消函数,以便在取消队列时能够取消当前任务
h.batchTaskManager.SetTaskCancel(queueID, cancel)
// 使用队列配置的角色工具列表(如果为空,表示使用所有工具)
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
useBatchMulti := false
useEinoSingle := false
batchOrch := "deep"
@@ -2304,10 +2303,10 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
if h.config == nil {
runErr = fmt.Errorf("服务器配置未加载")
} else {
resultMA, runErr = multiagent.RunEinoSingleChatModelAgent(ctx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, roleSkills, progressCallback)
resultMA, runErr = multiagent.RunEinoSingleChatModelAgent(ctx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback)
}
default:
result, runErr = h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools, roleSkills)
result, runErr = h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
}
// 任务执行完成,清理取消函数
h.batchTaskManager.SetTaskCancel(queueID, nil)
+23 -15
View File
@@ -27,7 +27,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- list ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskList,
Description: "列出批量任务队列(精简摘要,省上下文)。含队列元数据、子任务 id/status/截断后的 message、各状态计数。完整子任务(含 result/error/conversationId/时间等)请用 batch_task_get(queue_id)。",
Description: "列出批量任务队列(精简摘要,省上下文)。含队列元数据、子任务 id/status/截断后的 message、各状态计数。完整子任务(含 result/error/conversationId/时间等)请用 batch_task_get(queue_id)。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确提及查看/管理批量任务、任务队列时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "列出批量任务队列",
InputSchema: map[string]interface{}{
"type": "object",
@@ -101,7 +101,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- get ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskGet,
Description: "根据 queue_id 获取单个批量任务队列详情(含子任务列表、Cron、调度开关与最近错误信息)。",
Description: "根据 queue_id 获取单个批量任务队列详情(含子任务列表、Cron、调度开关与最近错误信息)。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确提及查看/管理批量任务、任务队列时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "获取批量任务队列详情",
InputSchema: map[string]interface{}{
"type": "object",
@@ -128,11 +128,13 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- create ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskCreate,
Description: `用途应用内任务管理 / 批量任务队列把多条彼此独立的用户指令登记成一条队列便于在界面里查看进度暂停/继续定时重跑等这是队列数据与调度入口不是再开一个子代理会话替你探索当前问题
Description: ` 调用约束本工具属于任务管理模块仅当用户明确要求创建批量任务任务队列时才可调用禁止在用户未提及批量任务任务队列定时任务等关键词时自行调用如果用户只是让你做某件事请在当前对话中直接完成不要自作主张创建任务队列
何时用户明确要批量排队执行Cron 周期跑同一批指令或需要与任务管理页面对齐时调用需要即时追问强依赖当前对话上下文的分析/编码应在本对话内直接完成要为了委派而创建队列
应用内任务管理 / 批量任务队列把多条彼此独立的用户指令登记成一条队列便于在界面里查看进度暂停/继续定时重跑等这是队列数据与调度入口是再开一个子代理会话替你探索当前问题
参数tasks字符串数组 tasks_text多行每行一条二选一每项是一条将来由系统按队列顺序执行的指令文案agent_modesingle原生 ReAct默认eino_singleEino ADK 单代理deep / plan_execute / supervisor需系统启用多代理兼容旧值 multi视为 deep把主对话拆给子代理schedule_modemanual默认 croncron 须填 cron_expr5 "0 */6 * * *"
何时用用户明确要批量排队执行Cron 周期跑同一批指令或需要与任务管理页面对齐时调用需要即时追问强依赖当前对话上下文的分析/编码应在本对话内直接完成不要为了委派而创建队列
参数tasks字符串数组 tasks_text多行每行一条二选一每项是一条将来由系统按队列顺序执行的指令文案agent_modesingle原生 ReAct默认eino_singleEino ADK 单代理deep / plan_execute / supervisor需系统启用多代理兼容旧值 multi视为 deep把主对话拆给子代理schedule_modemanual默认 croncron 须填 cron_expr5 0 */6 * * *
执行默认创建后为 pending不自动跑execute_now=true 可创建后立即跑否则之后调用 batch_task_startCron 自动下一轮需 schedule_enabled true可用 batch_task_schedule_enabled`,
ShortDescription: "任务管理:创建批量任务队列(登记多条指令,可选立即或 Cron)",
@@ -239,7 +241,9 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
reg(mcp.Tool{
Name: builtin.ToolBatchTaskStart,
Description: `启动或继续执行批量任务队列pending / paused
batch_task_create 配合使用仅创建队列不会自动执行需调用本工具才会开始跑子任务`,
batch_task_create 配合使用仅创建队列不会自动执行需调用本工具才会开始跑子任务
调用约束本工具属于任务管理模块仅当用户明确要求启动/继续批量任务时才可调用不要在用户未要求时自行调用`,
ShortDescription: "启动/继续批量任务队列(创建后需调用才会执行)",
InputSchema: map[string]interface{}{
"type": "object",
@@ -270,7 +274,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- rerun (reset + start for completed/cancelled queues) ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskRerun,
Description: "重跑已完成或已取消的批量任务队列。会重置所有子任务状态后重新执行一轮。",
Description: "重跑已完成或已取消的批量任务队列。会重置所有子任务状态后重新执行一轮。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确要求重跑批量任务时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "重跑批量任务队列",
InputSchema: map[string]interface{}{
"type": "object",
@@ -311,7 +315,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- pause ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskPause,
Description: "暂停正在运行的批量任务队列(当前子任务会被取消)。",
Description: "暂停正在运行的批量任务队列(当前子任务会被取消)。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确要求暂停批量任务时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "暂停批量任务队列",
InputSchema: map[string]interface{}{
"type": "object",
@@ -338,7 +342,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- delete queue ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskDelete,
Description: "删除批量任务队列及其子任务记录。",
Description: "删除批量任务队列及其子任务记录。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确要求删除批量任务队列时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "删除批量任务队列",
InputSchema: map[string]interface{}{
"type": "object",
@@ -365,7 +369,7 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
// --- update metadata (title/role/agentMode) ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskUpdateMetadata,
Description: "修改批量任务队列的标题、角色和代理模式。仅在队列非 running 状态下可修改。",
Description: "修改批量任务队列的标题、角色和代理模式。仅在队列非 running 状态下可修改。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确要求修改批量任务队列属性时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "修改批量任务队列标题/角色/代理模式",
InputSchema: map[string]interface{}{
"type": "object",
@@ -410,7 +414,9 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
reg(mcp.Tool{
Name: builtin.ToolBatchTaskUpdateSchedule,
Description: `修改批量任务队列的调度方式和 Cron 表达式仅在队列非 running 状态下可修改
schedule_mode cron 时必须提供有效 cron_expr manual 时会清除 Cron 配置`,
schedule_mode cron 时必须提供有效 cron_expr manual 时会清除 Cron 配置
调用约束本工具属于任务管理模块仅当用户明确要求修改批量任务调度配置时才可调用不要在用户未要求时自行调用`,
ShortDescription: "修改批量任务调度配置(Cron 表达式)",
InputSchema: map[string]interface{}{
"type": "object",
@@ -467,7 +473,9 @@ schedule_mode 为 cron 时必须提供有效 cron_expr;为 manual 时会清除
reg(mcp.Tool{
Name: builtin.ToolBatchTaskScheduleEnabled,
Description: `设置是否允许 Cron 自动触发该队列关闭后仍保留 Cron 表达式仅停止定时自动跑可用手工启动执行
仅对 schedule_mode cron 的队列有意义`,
仅对 schedule_mode cron 的队列有意义
调用约束本工具属于任务管理模块仅当用户明确要求开关批量任务自动调度时才可调用不要在用户未要求时自行调用`,
ShortDescription: "开关批量任务 Cron 自动调度",
InputSchema: map[string]interface{}{
"type": "object",
@@ -506,7 +514,7 @@ schedule_mode 为 cron 时必须提供有效 cron_expr;为 manual 时会清除
// --- add task ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskAdd,
Description: "向处于 pending 状态的队列追加一条子任务。",
Description: "向处于 pending 状态的队列追加一条子任务。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确要求向批量任务队列添加子任务时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "批量队列添加子任务",
InputSchema: map[string]interface{}{
"type": "object",
@@ -540,7 +548,7 @@ schedule_mode 为 cron 时必须提供有效 cron_expr;为 manual 时会清除
// --- update task ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskUpdate,
Description: "修改 pending 队列中仍为 pending 的子任务文案。",
Description: "修改 pending 队列中仍为 pending 的子任务文案。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确要求修改批量子任务内容时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "更新批量子任务内容",
InputSchema: map[string]interface{}{
"type": "object",
@@ -578,7 +586,7 @@ schedule_mode 为 cron 时必须提供有效 cron_expr;为 manual 时会清除
// --- remove task ---
reg(mcp.Tool{
Name: builtin.ToolBatchTaskRemove,
Description: "从 pending 队列中删除仍为 pending 的子任务。",
Description: "从 pending 队列中删除仍为 pending 的子任务。\n\n⚠️ 调用约束:本工具属于「任务管理」模块,仅当用户明确要求删除批量子任务时才可调用。不要在用户未要求时自行调用。",
ShortDescription: "删除批量子任务",
InputSchema: map[string]interface{}{
"type": "object",
+29 -4
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -305,6 +306,8 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
h.mu.RLock()
defer h.mu.RUnlock()
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
// 解析分页参数
page := 1
pageSize := 20
@@ -326,15 +329,26 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
searchTermLower = strings.ToLower(searchTerm)
}
// 解析状态筛选参数: "true" = 仅已启用, "false" = 仅已停用, "" = 全部
enabledFilter := c.Query("enabled")
// 解析状态筛选: tool_filter=on|off(角色弹窗等优先,避免与网关/代理对 enabled 的特殊处理冲突)
// 兼容旧参数 enabled=true|false
var filterEnabled *bool
if enabledFilter == "true" {
toolFilter := strings.TrimSpace(strings.ToLower(c.Query("tool_filter")))
switch toolFilter {
case "on", "1", "true", "enabled":
v := true
filterEnabled = &v
} else if enabledFilter == "false" {
case "off", "0", "false", "disabled":
v := false
filterEnabled = &v
default:
enabledFilter := strings.TrimSpace(c.Query("enabled"))
if enabledFilter == "true" {
v := true
filterEnabled = &v
} else if enabledFilter == "false" {
v := false
filterEnabled = &v
}
}
// 解析角色参数,用于过滤工具并标注启用状态
@@ -521,6 +535,17 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
// 注意:这里我们不直接过滤掉工具,而是保留所有工具,但通过 role_enabled 字段标注状态
// 这样前端可以显示所有工具,并标注哪些工具在当前角色中可用
// 统一按名称排序后再分页,避免配置文件中顺序导致「全部」与「仅已启用」前几页看起来完全一致
sort.SliceStable(allTools, func(i, j int) bool {
key := func(t ToolConfigInfo) string {
if t.IsExternal && t.ExternalMCP != "" {
return strings.ToLower(t.ExternalMCP + "::" + t.Name)
}
return strings.ToLower(t.Name)
}
return key(allTools[i]) < key(allTools[j])
})
total := len(allTools)
// 统计已启用的工具数(在角色中的启用工具数)
totalEnabled := 0
-2
View File
@@ -151,7 +151,6 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
prep.FinalMessage,
prep.History,
prep.RoleTools,
prep.RoleSkills,
progressCallback,
)
@@ -255,7 +254,6 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
prep.FinalMessage,
prep.History,
prep.RoleTools,
prep.RoleSkills,
progressCallback,
)
if runErr != nil {
+12 -5
View File
@@ -18,7 +18,6 @@ type multiAgentPrepared struct {
History []agent.ChatMessage
FinalMessage string
RoleTools []string
RoleSkills []string
AssistantMessageID string
UserMessageID string
}
@@ -68,7 +67,6 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
finalMessage := req.Message
var roleTools []string
var roleSkills []string
if req.WebShellConnectionID != "" {
conn, errConn := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
if errConn != nil || conn == nil {
@@ -79,8 +77,19 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
if remark == "" {
remark = conn.URL
}
finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base。Skills 包请使用 Eino 多代理内置 `skill` 工具。\n\n用户请求:%s",
webshellContext := fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base。Skills 包请使用 Eino 多代理内置 `skill` 工具。\n\n用户请求:%s",
conn.ID, remark, conn.ID, req.Message)
// WebShell 模式下如果同时指定了角色,追加角色 user_prompt(工具集仍仅限 webshell 专用工具)
if req.Role != "" && req.Role != "默认" && h.config != nil && h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled && role.UserPrompt != "" {
finalMessage = role.UserPrompt + "\n\n" + webshellContext
h.logger.Info("WebShell + 角色: 应用角色提示词(多代理)", zap.String("role", req.Role))
} else {
finalMessage = webshellContext
}
} else {
finalMessage = webshellContext
}
roleTools = []string{
builtin.ToolWebshellExec,
builtin.ToolWebshellFileList,
@@ -96,7 +105,6 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
finalMessage = role.UserPrompt + "\n\n" + req.Message
}
roleTools = role.Tools
roleSkills = role.Skills
}
}
@@ -135,7 +143,6 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
History: agentHistoryMessages,
FinalMessage: finalMessage,
RoleTools: roleTools,
RoleSkills: roleSkills,
AssistantMessageID: assistantMessageID,
UserMessageID: userMessageID,
}, nil
File diff suppressed because it is too large Load Diff
+35
View File
@@ -9,6 +9,8 @@ var apiDocI18nTagToKey = map[string]string{
"角色管理": "roleManagement", "Skills管理": "skillsManagement", "监控": "monitoring",
"配置管理": "configManagement", "外部MCP管理": "externalMCPManagement", "攻击链": "attackChain",
"知识库": "knowledgeBase", "MCP": "mcp",
"FOFA信息收集": "fofaRecon", "终端": "terminal", "WebShell管理": "webshellManagement",
"对话附件": "chatUploads", "机器人集成": "robotIntegration", "多代理Markdown": "markdownAgents",
}
var apiDocI18nSummaryToKey = map[string]string{
@@ -45,6 +47,29 @@ var apiDocI18nSummaryToKey = map[string]string{
"获取检索日志": "getRetrievalLogs", "删除检索日志": "deleteRetrievalLog",
"MCP端点": "mcpEndpoint", "列出所有工具": "listAllTools", "调用工具": "invokeTool", "初始化连接": "initConnection",
"成功响应": "successResponse", "错误响应": "errorResponse",
// 新增缺失端点
"删除对话轮次": "deleteConversationTurn", "获取消息过程详情": "getMessageProcessDetails",
"重跑批量任务队列": "rerunBatchQueue", "修改队列元数据": "updateBatchQueueMetadata",
"修改队列调度配置": "updateBatchQueueSchedule", "开关Cron自动调度": "setBatchQueueScheduleEnabled",
"获取所有分组映射": "getAllGroupMappings",
"FOFA搜索": "fofaSearch", "自然语言解析为FOFA语法": "fofaParse",
"测试OpenAI API连接": "testOpenAI",
"执行终端命令": "terminalRun", "流式执行终端命令": "terminalRunStream", "WebSocket终端": "terminalWS",
"列出WebShell连接": "listWebshellConnections", "创建WebShell连接": "createWebshellConnection",
"更新WebShell连接": "updateWebshellConnection", "删除WebShell连接": "deleteWebshellConnection",
"获取连接状态": "getWebshellConnectionState", "保存连接状态": "saveWebshellConnectionState",
"获取AI对话历史": "getWebshellAIHistory", "列出AI对话": "listWebshellAIConversations",
"执行WebShell命令": "webshellExec", "WebShell文件操作": "webshellFileOp",
"列出附件": "listChatUploads", "上传附件": "uploadChatFile", "删除附件": "deleteChatUpload",
"下载附件": "downloadChatUpload", "获取附件文本内容": "getChatUploadContent",
"写入附件文本内容": "putChatUploadContent", "创建附件目录": "mkdirChatUpload", "重命名附件": "renameChatUpload",
"企业微信回调验证": "wecomCallbackVerify", "企业微信消息回调": "wecomCallbackMessage",
"钉钉消息回调": "dingtalkCallback", "飞书消息回调": "larkCallback", "测试机器人消息处理": "testRobot",
"列出Markdown代理": "listMarkdownAgents", "创建Markdown代理": "createMarkdownAgent",
"获取Markdown代理详情": "getMarkdownAgent", "更新Markdown代理": "updateMarkdownAgent", "删除Markdown代理": "deleteMarkdownAgent",
"列出技能包文件": "listSkillPackageFiles", "获取技能包文件内容": "getSkillPackageFile", "写入技能包文件": "putSkillPackageFile",
"批量获取工具名称": "batchGetToolNames",
"获取知识库统计": "getKnowledgeStats",
}
var apiDocI18nResponseDescToKey = map[string]string{
@@ -62,6 +87,16 @@ var apiDocI18nResponseDescToKey = map[string]string{
"任务不存在": "taskNotFound", "对话或分组不存在": "conversationOrGroupNotFound",
"取消请求已提交": "cancelSubmitted", "未找到正在执行的任务": "noRunningTask",
"消息发送成功,返回AI回复": "messageSent", "流式响应(Server-Sent Events": "streamResponse",
// 新增缺失端点响应
"参数错误或删除失败": "badRequestOrDeleteFailed",
"参数错误": "paramError", "仅已完成或已取消的队列可以重跑": "onlyCompletedOrCancelledCanRerun",
"参数错误或队列正在运行中": "badRequestOrQueueRunning", "设置成功": "setSuccess",
"搜索成功": "searchSuccess", "解析成功": "parseSuccess", "测试结果": "testResult",
"执行完成": "executionDone", "SSE事件流": "sseEventStream", "WebSocket连接已建立": "wsEstablished",
"文件下载": "fileDownload", "文件不存在": "fileNotFound", "写入成功": "writeSuccess",
"重命名成功": "renameSuccess", "验证成功,返回解密后的echostr": "wecomVerifySuccess",
"处理成功": "processSuccess", "代理不存在": "agentNotFound", "保存成功": "saveSuccess",
"操作结果": "operationResult", "执行结果": "executionResult", "连接不存在": "connectionNotFound",
}
// enrichSpecWithI18nKeys 在 spec 的每个 operation 上写入 x-i18n-tags、x-i18n-summary
+3 -37
View File
@@ -18,15 +18,9 @@ import (
// RoleHandler 角色处理器
type RoleHandler struct {
config *config.Config
configPath string
logger *zap.Logger
skillsManager SkillsManager // Skills管理器接口(可选)
}
// SkillsManager Skills管理器接口
type SkillsManager interface {
ListSkills() ([]string, error)
config *config.Config
configPath string
logger *zap.Logger
}
// NewRoleHandler 创建新的角色处理器
@@ -38,34 +32,6 @@ func NewRoleHandler(cfg *config.Config, configPath string, logger *zap.Logger) *
}
}
// SetSkillsManager 设置Skills管理器
func (h *RoleHandler) SetSkillsManager(manager SkillsManager) {
h.skillsManager = manager
}
// GetSkills 获取所有可用的skills列表
func (h *RoleHandler) GetSkills(c *gin.Context) {
if h.skillsManager == nil {
c.JSON(http.StatusOK, gin.H{
"skills": []string{},
})
return
}
skills, err := h.skillsManager.ListSkills()
if err != nil {
h.logger.Warn("获取skills列表失败", zap.Error(err))
c.JSON(http.StatusOK, gin.H{
"skills": []string{},
})
return
}
c.JSON(http.StatusOK, gin.H{
"skills": skills,
})
}
// GetRoles 获取所有角色
func (h *RoleHandler) GetRoles(c *gin.Context) {
if h.config.Roles == nil {
+6 -72
View File
@@ -308,31 +308,10 @@ func (h *SkillsHandler) GetSkillBoundRoles(c *gin.Context) {
})
}
// getRolesBoundToSkill 获取绑定指定skill的角色列表(不修改配置)
// getRolesBoundToSkill 预留:角色不再配置 skill 绑定,始终返回空列表。
func (h *SkillsHandler) getRolesBoundToSkill(skillName string) []string {
if h.config.Roles == nil {
return []string{}
}
boundRoles := make([]string, 0)
for roleName, role := range h.config.Roles {
// 确保角色名称正确设置
if role.Name == "" {
role.Name = roleName
}
// 检查角色的Skills列表中是否包含该skill
if len(role.Skills) > 0 {
for _, skill := range role.Skills {
if skill == skillName {
boundRoles = append(boundRoles, roleName)
break
}
}
}
}
return boundRoles
_ = skillName
return nil
}
// CreateSkill 创建新 skill(标准 Agent Skills:生成 SKILL.md + YAML front matter
@@ -600,55 +579,10 @@ func (h *SkillsHandler) ClearSkillStatsByName(c *gin.Context) {
})
}
// removeSkillFromRoles 从所有角色中移除指定的skill绑定
// 返回受影响角色名称列表
// removeSkillFromRoles 预留:角色不再存储 skill 绑定,无操作。
func (h *SkillsHandler) removeSkillFromRoles(skillName string) []string {
if h.config.Roles == nil {
return []string{}
}
affectedRoles := make([]string, 0)
rolesToUpdate := make(map[string]config.RoleConfig)
// 遍历所有角色,查找并移除skill绑定
for roleName, role := range h.config.Roles {
// 确保角色名称正确设置
if role.Name == "" {
role.Name = roleName
}
// 检查角色的Skills列表中是否包含要删除的skill
if len(role.Skills) > 0 {
updated := false
newSkills := make([]string, 0, len(role.Skills))
for _, skill := range role.Skills {
if skill != skillName {
newSkills = append(newSkills, skill)
} else {
updated = true
}
}
if updated {
role.Skills = newSkills
rolesToUpdate[roleName] = role
affectedRoles = append(affectedRoles, roleName)
}
}
}
// 如果有角色需要更新,保存到文件
if len(rolesToUpdate) > 0 {
// 更新内存中的配置
for roleName, role := range rolesToUpdate {
h.config.Roles[roleName] = role
}
// 保存更新后的角色配置到文件
if err := h.saveRolesConfig(); err != nil {
h.logger.Error("保存角色配置失败", zap.Error(err))
}
}
return affectedRoles
_ = skillName
return nil
}
// saveRolesConfig 保存角色配置到文件(从SkillsHandler调用)
+40 -2
View File
@@ -79,6 +79,21 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
mcpIDsMu := args.McpIDsMu
mcpIDs := args.McpIDs
// panic recovery:防止 Eino 框架内部 panic 导致整个 goroutine 崩溃、连接无法正常关闭。
defer func() {
if r := recover(); r != nil {
if logger != nil {
logger.Error("eino runner panic recovered", zap.Any("recover", r), zap.Stack("stack"))
}
if progress != nil {
progress("error", fmt.Sprintf("Internal error: %v / 内部错误: %v", r, r), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
}
}()
var lastRunMsgs []adk.Message
var lastAssistant string
var lastPlanExecuteExecutor string
@@ -86,7 +101,8 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
emptyHint := strings.TrimSpace(args.EmptyResponseMessage)
if emptyHint == "" {
emptyHint = "Eino 会话已完成,但未捕获到助手文本输出。请查看过程详情或日志。)"
emptyHint = "(Eino session completed but no assistant text was captured. Check process details or logs.) " +
"(Eino 会话已完成,但未捕获到助手文本输出。请查看过程详情或日志。)"
}
attemptLoop:
@@ -191,6 +207,20 @@ attemptLoop:
iter := runner.Run(ctx, msgs)
for {
// 检测 context 取消(用户关闭浏览器、请求超时等),flush pending 工具状态避免 UI 卡在 "执行中"。
select {
case <-ctx.Done():
flushAllPendingAsFailed(ctx.Err())
if progress != nil {
progress("error", "Request cancelled / 请求已取消", map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return nil, ctx.Err()
default:
}
ev, ok := iter.Next()
if !ok {
lastRunMsgs = msgs
@@ -308,7 +338,10 @@ attemptLoop:
break
}
if logger != nil {
logger.Warn("eino stream recv", zap.Error(rerr))
logger.Warn("eino stream recv error, flushing incomplete stream",
zap.Error(rerr),
zap.String("agent", ev.AgentName),
zap.Int("toolFragments", len(toolStreamFragments)))
}
break
}
@@ -531,6 +564,11 @@ attemptLoop:
}
cleaned = dedupeRepeatedParagraphs(cleaned, 80)
cleaned = dedupeParagraphsByLineFingerprint(cleaned, 100)
// 防止超长响应导致 JSON 序列化慢或 OOM(多代理拼接大量工具输出时可能触发)。
const maxResponseRunes = 100000
if rs := []rune(cleaned); len(rs) > maxResponseRunes {
cleaned = string(rs[:maxResponseRunes]) + "\n\n... (response truncated / 响应已截断)"
}
out := &RunResult{
Response: cleaned,
MCPExecutionIDs: ids,
+67 -15
View File
@@ -26,6 +26,13 @@ type PlanExecuteRootArgs struct {
// AppCfg / Logger 非空时为 Executor 挂载与 Deep/Supervisor 一致的 Eino summarization 中间件。
AppCfg *config.Config
Logger *zap.Logger
// ExecPreMiddlewares 是由 prependEinoMiddlewares 构建的前置中间件(patchtoolcalls, reduction, toolsearch, plantask),
// 与 Deep/Supervisor 主代理的 mainOrchestratorPre 一致。
ExecPreMiddlewares []adk.ChatModelAgentMiddleware
// SkillMiddleware 是 Eino 官方 skill 渐进式披露中间件(可选)。
SkillMiddleware adk.ChatModelAgentMiddleware
// FilesystemMiddleware 是 Eino filesystem 中间件,当 eino_skills.filesystem_tools 启用时提供本机文件读写与 Shell 能力(可选)。
FilesystemMiddleware adk.ChatModelAgentMiddleware
}
// NewPlanExecuteRoot 返回 plan → execute → replan 预置编排根节点(与 Deep / Supervisor 并列)。
@@ -40,20 +47,39 @@ func NewPlanExecuteRoot(ctx context.Context, a *PlanExecuteRootArgs) (adk.Resuma
if !ok {
return nil, fmt.Errorf("plan_execute: 主模型需实现 ToolCallingChatModel")
}
planner, err := planexecute.NewPlanner(ctx, &planexecute.PlannerConfig{
plannerCfg := &planexecute.PlannerConfig{
ToolCallingChatModel: tcm,
})
}
if fn := planExecutePlannerGenInput(a.OrchInstruction); fn != nil {
plannerCfg.GenInputFn = fn
}
planner, err := planexecute.NewPlanner(ctx, plannerCfg)
if err != nil {
return nil, fmt.Errorf("plan_execute planner: %w", err)
}
replanner, err := planexecute.NewReplanner(ctx, &planexecute.ReplannerConfig{
ChatModel: tcm,
GenInputFn: planExecuteReplannerGenInput,
GenInputFn: planExecuteReplannerGenInput(a.OrchInstruction),
})
if err != nil {
return nil, fmt.Errorf("plan_execute replanner: %w", err)
}
// 组装 executor handler 栈,顺序与 Deep/Supervisor 主代理一致(outermost first)。
var execHandlers []adk.ChatModelAgentMiddleware
// 1. patchtoolcalls, reduction, toolsearch, plantask(来自 prependEinoMiddlewares
if len(a.ExecPreMiddlewares) > 0 {
execHandlers = append(execHandlers, a.ExecPreMiddlewares...)
}
// 2. filesystem 中间件(可选)
if a.FilesystemMiddleware != nil {
execHandlers = append(execHandlers, a.FilesystemMiddleware)
}
// 3. skill 中间件(可选)
if a.SkillMiddleware != nil {
execHandlers = append(execHandlers, a.SkillMiddleware)
}
// 4. summarization(最后,与 Deep/Supervisor 一致)
if a.AppCfg != nil {
sumMw, sumErr := newEinoSummarizationMiddleware(ctx, a.ExecModel, a.AppCfg, a.Logger)
if sumErr != nil {
@@ -82,6 +108,21 @@ func NewPlanExecuteRoot(ctx context.Context, a *PlanExecuteRootArgs) (adk.Resuma
})
}
// planExecutePlannerGenInput 将 orchestrator instruction 作为 SystemMessage 注入 planner 输入。
// 返回 nil 时 Eino 使用内置默认 planner prompt。
func planExecutePlannerGenInput(orchInstruction string) planexecute.GenPlannerModelInputFn {
oi := strings.TrimSpace(orchInstruction)
if oi == "" {
return nil
}
return func(ctx context.Context, userInput []adk.Message) ([]adk.Message, error) {
msgs := make([]adk.Message, 0, 1+len(userInput))
msgs = append(msgs, schema.SystemMessage(oi))
msgs = append(msgs, userInput...)
return msgs, nil
}
}
func planExecuteExecutorGenInput(orchInstruction string) planexecute.GenModelInputFn {
oi := strings.TrimSpace(orchInstruction)
return func(ctx context.Context, in *planexecute.ExecutionContext) ([]adk.Message, error) {
@@ -123,19 +164,30 @@ func planExecuteFormatExecutedSteps(results []planexecute.ExecutedStep) string {
return sb.String()
}
// planExecuteReplannerGenInput 与 Eino 默认 Replanner 输入一致,但 executed_steps 经 cap 后再写入 prompt
func planExecuteReplannerGenInput(ctx context.Context, in *planexecute.ExecutionContext) ([]adk.Message, error) {
planContent, err := in.Plan.MarshalJSON()
if err != nil {
return nil, err
// planExecuteReplannerGenInput 与 Eino 默认 Replanner 输入一致,但 executed_steps 经 cap 后再写入 prompt
// 且在 orchInstruction 非空时 prepend SystemMessage 使 replanner 也能接收全局指令。
func planExecuteReplannerGenInput(orchInstruction string) planexecute.GenModelInputFn {
oi := strings.TrimSpace(orchInstruction)
return func(ctx context.Context, in *planexecute.ExecutionContext) ([]adk.Message, error) {
planContent, err := in.Plan.MarshalJSON()
if err != nil {
return nil, err
}
msgs, err := planexecute.ReplannerPrompt.Format(ctx, map[string]any{
"plan": string(planContent),
"input": planExecuteFormatInput(in.UserInput),
"executed_steps": planExecuteFormatExecutedSteps(in.ExecutedSteps),
"plan_tool": planexecute.PlanToolInfo.Name,
"respond_tool": planexecute.RespondToolInfo.Name,
})
if err != nil {
return nil, err
}
if oi != "" {
msgs = append([]adk.Message{schema.SystemMessage(oi)}, msgs...)
}
return msgs, nil
}
return planexecute.ReplannerPrompt.Format(ctx, map[string]any{
"plan": string(planContent),
"input": planExecuteFormatInput(in.UserInput),
"executed_steps": planExecuteFormatExecutedSteps(in.ExecutedSteps),
"plan_tool": planexecute.PlanToolInfo.Name,
"respond_tool": planexecute.RespondToolInfo.Name,
})
}
// planExecuteStreamsMainAssistant 将规划/执行/重规划各阶段助手流式输出映射到主对话区。
+3 -3
View File
@@ -36,7 +36,6 @@ func RunEinoSingleChatModelAgent(
userMessage string,
history []agent.ChatMessage,
roleTools []string,
roleSkills []string,
progress func(eventType, message string, data interface{}),
) (*RunResult, error) {
if appCfg == nil || ag == nil {
@@ -169,7 +168,7 @@ func RunEinoSingleChatModelAgent(
chatCfg := &adk.ChatModelAgentConfig{
Name: einoSingleAgentName,
Description: "Eino ADK ChatModelAgent with MCP tools for authorized security testing.",
Instruction: ag.EinoSingleAgentSystemInstruction(roleSkills),
Instruction: ag.EinoSingleAgentSystemInstruction(),
Model: mainModel,
ToolsConfig: mainToolsCfg,
MaxIterations: maxIter,
@@ -212,6 +211,7 @@ func RunEinoSingleChatModelAgent(
McpIDsMu: &mcpIDsMu,
McpIDs: &mcpIDs,
DA: chatAgent,
EmptyResponseMessage: "Eino ADK 单代理会话已完成,但未捕获到助手文本输出。请查看过程详情或日志。)",
EmptyResponseMessage: "(Eino ADK single-agent session completed but no assistant text was captured. Check process details or logs.) " +
"Eino ADK 单代理会话已完成,但未捕获到助手文本输出。请查看过程详情或日志。)",
}, baseMsgs)
}
+2 -2
View File
@@ -23,13 +23,13 @@ func newPlanExecuteExecutor(ctx context.Context, cfg *planexecute.ExecutorConfig
genInput := func(ctx context.Context, instruction string, _ *adk.AgentInput) ([]adk.Message, error) {
plan, ok := adk.GetSessionValue(ctx, planexecute.PlanSessionKey)
if !ok {
panic("impossible: plan not found")
return nil, fmt.Errorf("plan_execute executor: session value %q missing (possible session corruption)", planexecute.PlanSessionKey)
}
plan_ := plan.(planexecute.Plan)
userInput, ok := adk.GetSessionValue(ctx, planexecute.UserInputSessionKey)
if !ok {
panic("impossible: user input not found")
return nil, fmt.Errorf("plan_execute executor: session value %q missing (possible session corruption)", planexecute.UserInputSessionKey)
}
userInput_ := userInput.([]adk.Message)
+19 -16
View File
@@ -213,19 +213,6 @@ func RunDeepAgent(
if len(roleTools) == 0 && len(r.Tools) > 0 {
roleTools = r.Tools
}
if len(r.Skills) > 0 {
var b strings.Builder
b.WriteString(instr)
b.WriteString("\n\n本角色推荐优先通过 Eino `skill` 工具(渐进式披露)加载的技能包 name:")
for i, s := range r.Skills {
if i > 0 {
b.WriteString("、")
}
b.WriteString(s)
}
b.WriteString("。")
instr = b.String()
}
}
}
@@ -335,6 +322,9 @@ func RunDeepAgent(
}
sb.WriteString("你是监督协调者:可将任务通过 transfer 工具委派给下列专家子代理(使用其在系统中的 Agent 名称)。专家列表:")
for _, sa := range subAgents {
if sa == nil {
continue
}
sb.WriteString("\n- ")
sb.WriteString(sa.Name(ctx))
}
@@ -349,14 +339,15 @@ func RunDeepAgent(
deepShell = einoLoc
}
deepHandlers := []adk.ChatModelAgentMiddleware{}
// noNestedTaskMiddleware 必须在最外层(最先拦截),防止 skill 或其他中间件内部触发 task 调用绕过检测。
deepHandlers := []adk.ChatModelAgentMiddleware{newNoNestedTaskMiddleware()}
if len(mainOrchestratorPre) > 0 {
deepHandlers = append(deepHandlers, mainOrchestratorPre...)
}
if einoSkillMW != nil {
deepHandlers = append(deepHandlers, einoSkillMW)
}
deepHandlers = append(deepHandlers, newNoNestedTaskMiddleware(), mainSumMw)
deepHandlers = append(deepHandlers, mainSumMw)
supHandlers := []adk.ChatModelAgentMiddleware{}
if len(mainOrchestratorPre) > 0 {
@@ -387,6 +378,14 @@ func RunDeepAgent(
if perr != nil {
return nil, fmt.Errorf("plan_execute 执行器模型: %w", perr)
}
// 构建 filesystem 中间件(与 Deep sub-agent 一致)
var peFsMw adk.ChatModelAgentMiddleware
if einoSkillMW != nil && einoFSTools && einoLoc != nil {
peFsMw, err = subAgentFilesystemMiddleware(ctx, einoLoc)
if err != nil {
return nil, fmt.Errorf("plan_execute filesystem 中间件: %w", err)
}
}
peRoot, perr := NewPlanExecuteRoot(ctx, &PlanExecuteRootArgs{
MainToolCallingModel: mainModel,
ExecModel: execModel,
@@ -396,6 +395,9 @@ func RunDeepAgent(
LoopMaxIter: ma.PlanExecuteLoopMaxIterations,
AppCfg: appCfg,
Logger: logger,
ExecPreMiddlewares: mainOrchestratorPre,
SkillMiddleware: einoSkillMW,
FilesystemMiddleware: peFsMw,
})
if perr != nil {
return nil, perr
@@ -493,7 +495,8 @@ func RunDeepAgent(
McpIDsMu: &mcpIDsMu,
McpIDs: &mcpIDs,
DA: da,
EmptyResponseMessage: "Eino 多代理编排已完成,但未捕获到助手文本输出。请查看过程详情或日志。)",
EmptyResponseMessage: "(Eino multi-agent orchestration completed but no assistant text was captured. Check process details or logs.) " +
"(Eino 多代理编排已完成,但未捕获到助手文本输出。请查看过程详情或日志。)",
}, baseMsgs)
}
@@ -3,6 +3,7 @@ package multiagent
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
@@ -44,6 +45,17 @@ func isSoftRecoverableToolError(err error) bool {
if err == nil {
return false
}
// 用户取消 — 不应重试,让 hard error 传播以终止编排。
if errors.Is(err, context.Canceled) {
return false
}
// 工具执行超时 — 转为 soft error 让 LLM 知晓并选择替代方案,而非全局重试。
if errors.Is(err, context.DeadlineExceeded) {
return true
}
s := strings.ToLower(err.Error())
// JSON unmarshal/parse failures — the model generated truncated or malformed arguments.
+2 -2
View File
@@ -33,12 +33,12 @@ func SkillsRootFromConfig(skillsDir string, configPath string) string {
return skillsDir
}
// DirLister satisfies handler.SkillsManager for role UI (lists package directory names).
// DirLister lists skill package directory names under SkillsRoot.
type DirLister struct {
SkillsRoot string
}
// ListSkills implements the role handler dependency.
// ListSkills returns skill package directory names that contain SKILL.md.
func (d DirLister) ListSkills() ([]string, error) {
return ListSkillDirNames(d.SkillsRoot)
}
+2 -3
View File
@@ -1,6 +1,6 @@
# 角色配置文件说明
本目录包含所有角色配置文件,每个角色定义了AI的行为模式可用工具和技能
本目录包含所有角色配置文件,每个角色定义了AI的行为模式可用工具。
## 创建新角色
@@ -41,7 +41,7 @@ enabled: true
按需还可加入 WebShell、批量任务等其它内置或外部工具(以 MCP 管理中已启用的为准)。
**Skills(技能包)**不由 MCP 工具列表提供。角色 `skills` 字段绑定技能 id 后,**多代理Eino DeepAgent** 会话中由 ADK **`skill`** 工具渐进加载;单代理路径不含该能力
**Skills(技能包)**:在 **多代理 / Eino** 会话中由内置 **`skill`** 工具按需加载 `skills_dir` 下的包,与角色 YAML 无绑定关系
**注意**:如果不设置 `tools` 字段,系统会默认使用所有 MCP 管理中已开启的工具。为明确控制角色可用工具,建议显式设置 `tools` 字段。
@@ -54,7 +54,6 @@ enabled: true
- **tools**: 工具列表,指定该角色可用的工具(可选)
- **如果不设置 `tools` 字段**:默认会选中**全部MCP管理中已开启的工具**
- **如果设置了 `tools` 字段**:只使用列表中指定的工具(建议至少包含上述核心内置工具)
- **skills**: 技能列表,指定该角色关联的技能(可选)
- **enabled**: 是否启用该角色(必填,true/false)
## 示例
+115 -116
View File
@@ -11353,12 +11353,53 @@ header {
.webshell-ai-msg ol {
padding-left: 20px;
}
.webshell-ai-input-row {
/* WebShell AI 输入区域:选择器 + 输入框同行 */
.webshell-ai-input-area {
flex-shrink: 0;
display: flex;
gap: 10px;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 8px 14px;
border-top: 1px solid var(--border-color);
}
.webshell-ai-selectors-row {
display: flex;
gap: 6px;
align-items: center;
flex-shrink: 0;
}
.webshell-ai-selectors-row .role-selector-btn {
height: 36px;
padding: 4px 10px;
font-size: 0.8125rem;
border-radius: 8px;
}
.webshell-ai-selectors-row .role-selector-icon {
font-size: 0.85rem;
}
.webshell-ai-selectors-row .role-selector-text {
font-size: 0.8125rem;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ws-role-selector-wrapper {
position: relative;
flex-shrink: 0;
}
.ws-agent-mode-wrapper {
flex-shrink: 0;
}
.ws-agent-mode-wrapper .agent-mode-inner {
position: relative;
}
.webshell-ai-input-row {
flex: 1;
min-width: 0;
display: flex;
gap: 10px;
align-items: center;
}
.webshell-ai-input {
@@ -11391,7 +11432,8 @@ header {
.webshell-ai-input::-webkit-scrollbar-thumb:hover {
background: rgba(15, 23, 42, 0.4);
}
.webshell-ai-input-row .btn-primary {
.webshell-ai-input-row .btn-primary,
.webshell-ai-input-row .webshell-ai-stop-btn {
flex-shrink: 0;
height: 36px;
min-width: 72px;
@@ -11400,6 +11442,18 @@ header {
align-items: center;
justify-content: center;
}
.webshell-ai-stop-btn {
background: #ef4444;
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
}
.webshell-ai-stop-btn:hover {
background: #dc2626;
}
/* WebShell 数据库管理 Tab */
.webshell-pane-db {
@@ -13465,6 +13519,7 @@ header {
min-width: 0;
flex: 1;
padding-top: 2px;
text-align: left;
}
.role-selection-item-name-main {
@@ -14133,7 +14188,9 @@ header {
.role-tools-stats {
display: flex;
gap: 16px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 8px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
@@ -14142,6 +14199,60 @@ header {
color: var(--text-secondary);
}
.role-tools-stats-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
.role-tools-stats-hint {
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.45;
width: 100%;
}
.role-tool-mcp-disabled-badge {
padding: 2px 6px;
background: rgba(108, 117, 125, 0.15);
color: var(--text-muted);
border-radius: 8px;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
margin-left: 4px;
}
.role-tools-filter-banner {
padding: 10px 12px;
margin-bottom: 10px;
border-radius: 6px;
font-size: 0.8125rem;
line-height: 1.5;
border: 1px solid var(--border-color);
}
.role-tools-filter-banner-on {
background: rgba(0, 102, 255, 0.08);
color: var(--text-primary);
border-color: rgba(0, 102, 255, 0.25);
}
.role-tools-filter-banner-off {
background: rgba(108, 117, 125, 0.1);
color: var(--text-secondary);
}
.role-tool-mcp-on-badge {
padding: 2px 6px;
background: rgba(25, 135, 84, 0.12);
color: #198754;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
margin-left: 6px;
}
.role-tools-stats span {
white-space: nowrap;
}
@@ -14393,118 +14504,6 @@ header {
}
}
/* Skills选择相关样式 */
.role-skills-controls {
margin-bottom: 12px;
}
.role-skills-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.role-skills-search-box {
position: relative;
flex: 1;
min-width: 200px;
max-width: 400px;
}
.role-skills-search-box input {
width: 100%;
padding: 8px 32px 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
background: var(--bg-primary);
color: var(--text-primary);
transition: all 0.2s;
}
.role-skills-search-box input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.role-skills-search-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
}
.role-skills-search-clear:hover {
color: var(--text-primary);
}
.role-skills-stats {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-top: 8px;
}
.role-skills-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px;
background: var(--bg-primary);
}
.role-skill-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
transition: all 0.2s ease;
margin-bottom: 6px;
}
.role-skill-item:last-child {
margin-bottom: 0;
}
.role-skill-item:hover {
background: var(--bg-secondary);
border-color: var(--accent-color);
box-shadow: 0 2px 4px rgba(0, 102, 255, 0.1);
}
.role-skill-item .checkbox-text {
font-size: 0.9375rem;
color: var(--text-primary);
font-weight: 500;
}
.skills-loading,
.skills-empty,
.skills-error {
padding: 20px;
text-align: center;
color: var(--text-secondary);
font-size: 0.875rem;
}
.skills-error {
color: var(--error-color);
}
/* Skills管理页面样式 */
.skills-controls {
margin-bottom: 8px;
+98 -17
View File
@@ -845,7 +845,13 @@
"externalMCPManagement": "External MCP Management",
"attackChain": "Attack Chain",
"knowledgeBase": "Knowledge Base",
"mcp": "MCP"
"mcp": "MCP",
"fofaRecon": "FOFA Recon",
"terminal": "Terminal",
"webshellManagement": "WebShell Management",
"chatUploads": "Chat Uploads",
"robotIntegration": "Robot Integration",
"markdownAgents": "Markdown Agents"
},
"summary": {
"login": "User login",
@@ -945,7 +951,53 @@
"invokeTool": "Invoke tool",
"initConnection": "Initialize connection",
"successResponse": "Success response",
"errorResponse": "Error response"
"errorResponse": "Error response",
"deleteConversationTurn": "Delete conversation turn",
"getMessageProcessDetails": "Get message process details",
"rerunBatchQueue": "Rerun batch task queue",
"updateBatchQueueMetadata": "Update queue metadata",
"updateBatchQueueSchedule": "Update queue schedule",
"setBatchQueueScheduleEnabled": "Toggle cron auto-schedule",
"getAllGroupMappings": "Get all group mappings",
"fofaSearch": "FOFA search",
"fofaParse": "Parse natural language to FOFA syntax",
"testOpenAI": "Test OpenAI API connection",
"terminalRun": "Run terminal command",
"terminalRunStream": "Run terminal command (stream)",
"terminalWS": "WebSocket terminal",
"listWebshellConnections": "List WebShell connections",
"createWebshellConnection": "Create WebShell connection",
"updateWebshellConnection": "Update WebShell connection",
"deleteWebshellConnection": "Delete WebShell connection",
"getWebshellConnectionState": "Get connection state",
"saveWebshellConnectionState": "Save connection state",
"getWebshellAIHistory": "Get AI chat history",
"listWebshellAIConversations": "List AI conversations",
"webshellExec": "Execute WebShell command",
"webshellFileOp": "WebShell file operation",
"listChatUploads": "List uploads",
"uploadChatFile": "Upload file",
"deleteChatUpload": "Delete upload",
"downloadChatUpload": "Download upload",
"getChatUploadContent": "Get file text content",
"putChatUploadContent": "Write file text content",
"mkdirChatUpload": "Create upload directory",
"renameChatUpload": "Rename upload",
"wecomCallbackVerify": "WeCom callback verification",
"wecomCallbackMessage": "WeCom message callback",
"dingtalkCallback": "DingTalk message callback",
"larkCallback": "Lark message callback",
"testRobot": "Test robot message processing",
"listMarkdownAgents": "List Markdown agents",
"createMarkdownAgent": "Create Markdown agent",
"getMarkdownAgent": "Get Markdown agent detail",
"updateMarkdownAgent": "Update Markdown agent",
"deleteMarkdownAgent": "Delete Markdown agent",
"listSkillPackageFiles": "List skill package files",
"getSkillPackageFile": "Get skill package file content",
"putSkillPackageFile": "Write skill package file",
"batchGetToolNames": "Batch get tool names",
"getKnowledgeStats": "Get knowledge base stats"
},
"response": {
"getSuccess": "Success",
@@ -980,7 +1032,29 @@
"cancelSubmitted": "Cancel request submitted",
"noRunningTask": "No running task found",
"messageSent": "Message sent, AI reply returned",
"streamResponse": "Stream response (Server-Sent Events)"
"streamResponse": "Stream response (Server-Sent Events)",
"badRequestOrDeleteFailed": "Bad request or delete failed",
"paramError": "Invalid parameters",
"onlyCompletedOrCancelledCanRerun": "Only completed or cancelled queues can be rerun",
"badRequestOrQueueRunning": "Bad request or queue is running",
"setSuccess": "Set successfully",
"searchSuccess": "Search successful",
"parseSuccess": "Parse successful",
"testResult": "Test result",
"executionDone": "Execution completed",
"sseEventStream": "SSE event stream",
"wsEstablished": "WebSocket connection established",
"fileDownload": "File download",
"fileNotFound": "File not found",
"writeSuccess": "Written successfully",
"renameSuccess": "Renamed successfully",
"wecomVerifySuccess": "Verification successful, decrypted echostr returned",
"processSuccess": "Processed successfully",
"agentNotFound": "Agent not found",
"saveSuccess": "Saved successfully",
"operationResult": "Operation result",
"executionResult": "Execution result",
"connectionNotFound": "Connection not found"
}
},
"chatGroup": {
@@ -1710,28 +1784,30 @@
"defaultRoleToolsDesc": "Default role uses all tools enabled in MCP Management.",
"searchToolsPlaceholder": "Search tools...",
"loadingTools": "Loading tools...",
"relatedToolsHint": "Select tools to link; empty = use all from MCP Management.",
"relatedSkills": "Related Skills (optional)",
"searchSkillsPlaceholder": "Search skill...",
"loadingSkills": "Loading skills...",
"relatedSkillsHint": "Selected skills are injected into system prompt before task execution.",
"relatedToolsHint": "Use “Linked / Not linked” above to filter by this roles checkboxes. MCP-wide on/off is in MCP Management.",
"enableRole": "Enable this role",
"selectAll": "Select All",
"deselectAll": "Deselect All",
"roleNameRequired": "Role name is required",
"roleNotFound": "Role not found",
"firstRoleNoToolsHint": "First role with no tools selected will use all tools by default.",
"currentPageSelected": "Current page: {{current}} / {{total}}",
"totalSelected": "Total selected: {{current}} / {{total}}",
"filterRoleAll": "All",
"filterRoleOn": "Linked to role",
"filterRoleOff": "Not linked",
"statsPageLinked": "This page checked: {{current}} / {{total}}",
"statsPageLinkedTitle": "Checked = link tool to this role; unrelated to MCP on/off",
"statsRoleLinked": "Role checked: {{current}} / {{max}}",
"statsRoleLinkedTitle": "Numerator: checked tools (MCP on only). Denominator: MCP-on tool count (same as MCP Management filter)",
"statsRoleLinkedNoMax": "Role checked: {{current}} (switch filter to All, no search, load once to sync cap)",
"statsRoleLinkedNoMaxTitle": "MCP-on total not cached yet",
"statsRoleUsesAll": "Policy: all MCP-on tools ({{mcpOn}}) · {{all}} total in catalog (incl. MCP off)",
"statsRoleUsesAllTitle": "Matches MCP Management “enabled” count; no explicit tool list",
"statsListScopeAll": "List: all {{n}}",
"statsListScopeRoleOn": "List: linked to this role {{n}}",
"statsListScopeRoleOff": "List: not linked to this role {{n}}",
"usingAllEnabledTools": "(Using all enabled tools)",
"currentPageSelectedTitle": "Selected on current page (enabled tools only)",
"totalSelectedTitle": "Total tools linked to this role",
"skillsSelectedCount": "Selected {{count}} / {{total}}",
"loadToolsFailed": "Failed to load tools",
"loadSkillsFailed": "Failed to load skills",
"cannotDeleteDefaultRole": "Cannot delete default role",
"noMatchingSkills": "No matching skills",
"noSkillsAvailable": "No skills available",
"usingAllTools": "Use all tools",
"andNMore": " and {{count}} more",
"toolsLabel": "Tools:",
@@ -1742,6 +1818,11 @@
"prevPage": "Previous",
"pageOf": "Page {{page}} / {{total}}",
"nextPage": "Next",
"lastPage": "Last"
"lastPage": "Last",
"mcpDisabledBadge": "MCP off",
"mcpDisabledBadgeTitle": "Off in MCP Management; check only expresses role linkage—turn on in MCP to run",
"roleFilterOnBanner": "These tools are checked and linked to this role (independent of MCP-wide enable).",
"roleFilterOffBanner": "These tools are unchecked and not linked to this role.",
"checkboxLinkTitle": "Check to link this tool to this role"
}
}
+98 -17
View File
@@ -845,7 +845,13 @@
"externalMCPManagement": "外部MCP管理",
"attackChain": "攻击链",
"knowledgeBase": "知识库",
"mcp": "MCP"
"mcp": "MCP",
"fofaRecon": "FOFA信息收集",
"terminal": "终端",
"webshellManagement": "WebShell管理",
"chatUploads": "对话附件",
"robotIntegration": "机器人集成",
"markdownAgents": "多代理Markdown"
},
"summary": {
"login": "用户登录",
@@ -945,7 +951,53 @@
"invokeTool": "调用工具",
"initConnection": "初始化连接",
"successResponse": "成功响应",
"errorResponse": "错误响应"
"errorResponse": "错误响应",
"deleteConversationTurn": "删除对话轮次",
"getMessageProcessDetails": "获取消息过程详情",
"rerunBatchQueue": "重跑批量任务队列",
"updateBatchQueueMetadata": "修改队列元数据",
"updateBatchQueueSchedule": "修改队列调度配置",
"setBatchQueueScheduleEnabled": "开关Cron自动调度",
"getAllGroupMappings": "获取所有分组映射",
"fofaSearch": "FOFA搜索",
"fofaParse": "自然语言解析为FOFA语法",
"testOpenAI": "测试OpenAI API连接",
"terminalRun": "执行终端命令",
"terminalRunStream": "流式执行终端命令",
"terminalWS": "WebSocket终端",
"listWebshellConnections": "列出WebShell连接",
"createWebshellConnection": "创建WebShell连接",
"updateWebshellConnection": "更新WebShell连接",
"deleteWebshellConnection": "删除WebShell连接",
"getWebshellConnectionState": "获取连接状态",
"saveWebshellConnectionState": "保存连接状态",
"getWebshellAIHistory": "获取AI对话历史",
"listWebshellAIConversations": "列出AI对话",
"webshellExec": "执行WebShell命令",
"webshellFileOp": "WebShell文件操作",
"listChatUploads": "列出附件",
"uploadChatFile": "上传附件",
"deleteChatUpload": "删除附件",
"downloadChatUpload": "下载附件",
"getChatUploadContent": "获取附件文本内容",
"putChatUploadContent": "写入附件文本内容",
"mkdirChatUpload": "创建附件目录",
"renameChatUpload": "重命名附件",
"wecomCallbackVerify": "企业微信回调验证",
"wecomCallbackMessage": "企业微信消息回调",
"dingtalkCallback": "钉钉消息回调",
"larkCallback": "飞书消息回调",
"testRobot": "测试机器人消息处理",
"listMarkdownAgents": "列出Markdown代理",
"createMarkdownAgent": "创建Markdown代理",
"getMarkdownAgent": "获取Markdown代理详情",
"updateMarkdownAgent": "更新Markdown代理",
"deleteMarkdownAgent": "删除Markdown代理",
"listSkillPackageFiles": "列出技能包文件",
"getSkillPackageFile": "获取技能包文件内容",
"putSkillPackageFile": "写入技能包文件",
"batchGetToolNames": "批量获取工具名称",
"getKnowledgeStats": "获取知识库统计"
},
"response": {
"getSuccess": "获取成功",
@@ -980,7 +1032,29 @@
"cancelSubmitted": "取消请求已提交",
"noRunningTask": "未找到正在执行的任务",
"messageSent": "消息发送成功,返回AI回复",
"streamResponse": "流式响应(Server-Sent Events"
"streamResponse": "流式响应(Server-Sent Events",
"badRequestOrDeleteFailed": "参数错误或删除失败",
"paramError": "参数错误",
"onlyCompletedOrCancelledCanRerun": "仅已完成或已取消的队列可以重跑",
"badRequestOrQueueRunning": "参数错误或队列正在运行中",
"setSuccess": "设置成功",
"searchSuccess": "搜索成功",
"parseSuccess": "解析成功",
"testResult": "测试结果",
"executionDone": "执行完成",
"sseEventStream": "SSE事件流",
"wsEstablished": "WebSocket连接已建立",
"fileDownload": "文件下载",
"fileNotFound": "文件不存在",
"writeSuccess": "写入成功",
"renameSuccess": "重命名成功",
"wecomVerifySuccess": "验证成功,返回解密后的echostr",
"processSuccess": "处理成功",
"agentNotFound": "代理不存在",
"saveSuccess": "保存成功",
"operationResult": "操作结果",
"executionResult": "执行结果",
"connectionNotFound": "连接不存在"
}
},
"chatGroup": {
@@ -1710,28 +1784,30 @@
"defaultRoleToolsDesc": "默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。",
"searchToolsPlaceholder": "搜索工具...",
"loadingTools": "正在加载工具列表...",
"relatedToolsHint": "勾选要关联的工具,留空则使用MCP管理中的全部工具配置。",
"relatedSkills": "关联的Skills(可选)",
"searchSkillsPlaceholder": "搜索skill...",
"loadingSkills": "正在加载skills列表...",
"relatedSkillsHint": "勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。",
"relatedToolsHint": "上方「本角色已开/已关」按复选框筛选;留空工具清单表示不限制。MCP 全局开关请在 MCP 管理中操作。",
"enableRole": "启用此角色",
"selectAll": "全选",
"deselectAll": "全不选",
"roleNameRequired": "角色名称不能为空",
"roleNotFound": "角色不存在",
"firstRoleNoToolsHint": "检测到这是首次添加角色且未选择工具,将默认使用全部工具",
"currentPageSelected": "当前页已选中: {{current}} / {{total}}",
"totalSelected": "总计已选中: {{current}} / {{total}}",
"filterRoleAll": "全部",
"filterRoleOn": "本角色已开",
"filterRoleOff": "本角色已关",
"statsPageLinked": "本页已勾选: {{current}} / {{total}}",
"statsPageLinkedTitle": "勾选=关联到本角色;与 MCP 里是否开启无关",
"statsRoleLinked": "本角色已勾选: {{current}} / {{max}}",
"statsRoleLinkedTitle": "分子为全库勾选数(仅 MCP 为开的工具);分母为 MCP 已开工具总数,与「MCP管理」里筛选 MCP已开 的条数一致",
"statsRoleLinkedNoMax": "本角色已勾选: {{current}}(请先切到「全部」且无搜索,加载一页以同步上限)",
"statsRoleLinkedNoMaxTitle": "尚未缓存 MCP 已开总数",
"statsRoleUsesAll": "工具策略: 使用全部 MCP 已开工具({{mcpOn}} 个)· 全库共 {{all}} 个(含 MCP 已关)",
"statsRoleUsesAllTitle": "与 MCP 管理中「MCP已开」数量一致;未单独限定工具清单",
"statsListScopeAll": "当前列表: 全部 {{n}} 条",
"statsListScopeRoleOn": "当前列表: 本角色已关联 {{n}} 条",
"statsListScopeRoleOff": "当前列表: 本角色未关联 {{n}} 条",
"usingAllEnabledTools": "(使用所有已启用工具)",
"currentPageSelectedTitle": "当前页选中的工具数(只统计已启用的工具)",
"totalSelectedTitle": "角色已关联的工具总数(基于角色实际配置)",
"skillsSelectedCount": "已选择 {{count}} / {{total}}",
"loadToolsFailed": "加载工具列表失败",
"loadSkillsFailed": "加载skills列表失败",
"cannotDeleteDefaultRole": "不能删除默认角色",
"noMatchingSkills": "没有找到匹配的skills",
"noSkillsAvailable": "暂无可用skills",
"usingAllTools": "使用所有工具",
"andNMore": " 等 {{count}} 个",
"toolsLabel": "工具:",
@@ -1742,6 +1818,11 @@
"prevPage": "上一页",
"pageOf": "第 {{page}} / {{total}} 页",
"nextPage": "下一页",
"lastPage": "末页"
"lastPage": "末页",
"mcpDisabledBadge": "MCP已关",
"mcpDisabledBadgeTitle": "MCP 管理里该工具为关闭;勾选只表示想关联到本角色,实际调用需先在 MCP 中打开",
"roleFilterOnBanner": "以下为「已勾选、关联到本角色」的工具(与 MCP 管理里全局开/关无关)。",
"roleFilterOffBanner": "以下为「未勾选、未关联到本角色」的工具。",
"checkboxLinkTitle": "勾选表示本角色关联使用该工具"
}
}
+306 -328
View File
@@ -7,9 +7,22 @@ let roles = [];
let rolesSearchKeyword = ''; // 角色搜索关键词
let rolesSearchTimeout = null; // 搜索防抖定时器
let allRoleTools = []; // 存储所有工具列表(用于角色工具选择)
// 与 MCP 工具配置共用 localStorage,便于统一运维习惯
function getRoleToolsPageSize() {
const saved = localStorage.getItem('toolsPageSize');
const n = saved ? parseInt(saved, 10) : 20;
return isNaN(n) || n < 1 ? 20 : n;
}
// 本角色关联筛选: '' = 全部, 'role_on' = 本角色已勾选关联, 'role_off' = 本角色未关联
let roleToolsStatusFilter = '';
/** 按角色关联筛选时缓存全量列表(匹配当前搜索),避免翻页丢状态 */
let roleToolsListCacheFull = [];
let roleToolsListCacheSearch = '';
/** 是否使用客户端分页(角色关联筛选模式下为 true) */
let roleToolsClientMode = false;
let roleToolsPagination = {
page: 1,
pageSize: 20,
pageSize: getRoleToolsPageSize(),
total: 0,
totalPages: 1
};
@@ -17,13 +30,11 @@ let roleToolsSearchKeyword = ''; // 工具搜索关键词
let roleToolStateMap = new Map(); // 工具状态映射:toolKey -> { enabled: boolean, ... }
let roleUsesAllTools = false; // 标记角色是否使用所有工具(当没有配置tools时)
let totalEnabledToolsInMCP = 0; // 已启用的工具总数(从MCP管理中获取,从API响应中获取)
// 仅在「无状态筛选、无搜索」的请求结果上更新,供统计条分母使用(避免筛选后 total 变小导致 25/9 这类错误)
let roleToolsStatsGrandTotal = 0; // 工具总条数(与 MCP 列表「全部」一致)
let roleToolsStatsMcpEnabledTotal = 0; // MCP 全局已启用工具数
let roleConfiguredTools = new Set(); // 角色配置的工具列表(用于确定哪些工具应该被选中)
// Skills相关
let allRoleSkills = []; // 存储所有skills列表
let roleSkillsSearchKeyword = ''; // Skills搜索关键词
let roleSelectedSkills = new Set(); // 选中的skills集合
// 对角色列表进行排序:默认角色排在第一个,其他按名称排序
function sortRoles(rolesArray) {
const sortedRoles = [...rolesArray];
@@ -418,6 +429,91 @@ function getToolKey(tool) {
return tool.name;
}
// 将单个工具合并进 roleToolStateMap(与 loadRoleTools 中单条逻辑一致)
function mergeToolIntoRoleStateMap(tool) {
const toolKey = getToolKey(tool);
if (!roleToolStateMap.has(toolKey)) {
let enabled = false;
if (roleUsesAllTools) {
enabled = tool.enabled ? true : false;
} else {
enabled = roleConfiguredTools.has(toolKey);
}
roleToolStateMap.set(toolKey, {
enabled: enabled,
is_external: tool.is_external || false,
external_mcp: tool.external_mcp || '',
name: tool.name,
mcpEnabled: tool.enabled
});
} else {
const state = roleToolStateMap.get(toolKey);
if (roleUsesAllTools && tool.enabled) {
state.enabled = true;
}
state.is_external = tool.is_external || false;
state.external_mcp = tool.external_mcp || '';
state.mcpEnabled = tool.enabled;
if (!state.name || state.name === toolKey.split('::').pop()) {
state.name = tool.name;
}
}
}
function getRoleLinkedForTool(toolKey, tool) {
if (roleToolStateMap.has(toolKey)) {
return !!roleToolStateMap.get(toolKey).enabled;
}
if (roleUsesAllTools) {
return tool.enabled !== false;
}
return roleConfiguredTools.has(toolKey);
}
function computeRoleLinkFilteredTools() {
if (!roleToolsListCacheFull.length) {
return [];
}
return roleToolsListCacheFull.filter(tool => {
const key = getToolKey(tool);
const linked = getRoleLinkedForTool(key, tool);
if (roleToolsStatusFilter === 'role_on') {
return linked;
}
if (roleToolsStatusFilter === 'role_off') {
return !linked;
}
return true;
});
}
async function fetchAllRoleToolsIntoCache(searchKeyword) {
const pageSize = 100;
let page = 1;
const all = [];
let totalPages = 1;
do {
let url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
if (searchKeyword) {
url += `&search=${encodeURIComponent(searchKeyword)}`;
}
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取工具列表失败');
}
const result = await response.json();
const tools = result.tools || [];
tools.forEach(tool => mergeToolIntoRoleStateMap(tool));
all.push(...tools);
totalPages = Math.max(1, result.total_pages || 1);
page++;
} while (page <= totalPages);
roleToolsListCacheFull = all;
roleToolsStatsGrandTotal = all.length;
roleToolsStatsMcpEnabledTotal = all.filter(t => t.enabled !== false).length;
totalEnabledToolsInMCP = roleToolsStatsMcpEnabledTotal;
}
// 保存当前页的工具状态到全局映射
function saveCurrentRolePageToolStates() {
document.querySelectorAll('#role-tools-list .role-tool-item').forEach(item => {
@@ -444,72 +540,70 @@ async function loadRoleTools(page = 1, searchKeyword = '') {
try {
// 在加载新页面之前,先保存当前页的状态到全局映射
saveCurrentRolePageToolStates();
const pageSize = roleToolsPagination.pageSize;
let url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
if (searchKeyword) {
url += `&search=${encodeURIComponent(searchKeyword)}`;
}
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取工具列表失败');
}
const result = await response.json();
allRoleTools = result.tools || [];
roleToolsPagination = {
page: result.page || page,
pageSize: result.page_size || pageSize,
total: result.total || 0,
totalPages: result.total_pages || 1
};
// 更新已启用的工具总数(从API响应中获取)
if (result.total_enabled !== undefined) {
totalEnabledToolsInMCP = result.total_enabled;
}
// 初始化工具状态映射(如果工具不在映射中,使用服务器返回的状态)
// 但要注意:如果工具已经在映射中(比如编辑角色时预先设置的选中工具),则保留映射中的状态
allRoleTools.forEach(tool => {
const toolKey = getToolKey(tool);
if (!roleToolStateMap.has(toolKey)) {
// 工具不在映射中
let enabled = false;
if (roleUsesAllTools) {
// 如果使用所有工具,且工具在MCP管理中已启用,则标记为选中
enabled = tool.enabled ? true : false;
} else {
// 如果不使用所有工具,只有工具在角色配置的工具列表中才标记为选中
enabled = roleConfiguredTools.has(toolKey);
}
roleToolStateMap.set(toolKey, {
enabled: enabled,
is_external: tool.is_external || false,
external_mcp: tool.external_mcp || '',
name: tool.name,
mcpEnabled: tool.enabled // 保存MCP管理中的原始启用状态
});
} else {
// 工具已在映射中(可能是预先设置的选中工具或用户手动选择的),保留映射中的状态
// 注意:即使使用所有工具,也不要强制覆盖用户已取消的工具选择
const state = roleToolStateMap.get(toolKey);
// 如果使用所有工具,且工具在MCP管理中已启用,确保标记为选中
if (roleUsesAllTools && tool.enabled) {
// 使用所有工具时,确保所有已启用的工具都被选中
state.enabled = true;
}
// 如果不使用所有工具,保留映射中的状态(不要覆盖,因为状态已经在初始化时正确设置了)
state.is_external = tool.is_external || false;
state.external_mcp = tool.external_mcp || '';
state.mcpEnabled = tool.enabled; // 更新MCP管理中的原始启用状态
if (!state.name || state.name === toolKey.split('::').pop()) {
state.name = tool.name; // 更新工具名称
const needRoleLinkFilter =
roleToolsStatusFilter === 'role_on' || roleToolsStatusFilter === 'role_off';
if (needRoleLinkFilter) {
roleToolsClientMode = true;
const searchChanged = searchKeyword !== roleToolsListCacheSearch;
if (searchChanged || roleToolsListCacheFull.length === 0) {
await fetchAllRoleToolsIntoCache(searchKeyword);
roleToolsListCacheSearch = searchKeyword;
}
const filtered = computeRoleLinkFilteredTools();
const total = filtered.length;
let totalPages = Math.max(1, Math.ceil(total / pageSize) || 1);
let p = page;
if (p > totalPages) {
p = totalPages;
}
if (p < 1) {
p = 1;
}
roleToolsPagination = {
page: p,
pageSize,
total,
totalPages
};
allRoleTools = filtered.slice((p - 1) * pageSize, p * pageSize);
} else {
roleToolsClientMode = false;
roleToolsListCacheFull = [];
roleToolsListCacheSearch = '';
let url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
if (searchKeyword) {
url += `&search=${encodeURIComponent(searchKeyword)}`;
}
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取工具列表失败');
}
const result = await response.json();
allRoleTools = result.tools || [];
roleToolsPagination = {
page: result.page || page,
pageSize: result.page_size || pageSize,
total: result.total || 0,
totalPages: result.total_pages || 1
};
if (roleToolsStatusFilter === '' && !searchKeyword) {
roleToolsStatsGrandTotal = result.total || 0;
if (result.total_enabled !== undefined) {
roleToolsStatsMcpEnabledTotal = result.total_enabled;
totalEnabledToolsInMCP = result.total_enabled;
}
}
});
allRoleTools.forEach(tool => mergeToolIntoRoleStateMap(tool));
}
renderRoleToolsList();
renderRoleToolsPagination();
updateRoleToolsStats();
@@ -529,6 +623,20 @@ function renderRoleToolsList() {
// 清除加载提示和旧内容
toolsList.innerHTML = '';
if (roleToolsStatusFilter === 'role_on') {
const banner = document.createElement('div');
banner.className = 'role-tools-filter-banner role-tools-filter-banner-on';
banner.setAttribute('role', 'status');
banner.textContent = _t('roleModal.roleFilterOnBanner');
toolsList.appendChild(banner);
} else if (roleToolsStatusFilter === 'role_off') {
const banner = document.createElement('div');
banner.className = 'role-tools-filter-banner role-tools-filter-banner-off';
banner.setAttribute('role', 'status');
banner.textContent = _t('roleModal.roleFilterOffBanner');
toolsList.appendChild(banner);
}
const listContainer = document.createElement('div');
listContainer.className = 'role-tools-list-items';
@@ -539,6 +647,8 @@ function renderRoleToolsList() {
toolsList.appendChild(listContainer);
return;
}
const chkTitle = escapeHtml(_t('roleModal.checkboxLinkTitle'));
allRoleTools.forEach(tool => {
const toolKey = getToolKey(tool);
@@ -564,17 +674,22 @@ function renderRoleToolsList() {
const badgeTitle = externalMcpName ? `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}` : '外部MCP工具';
externalBadge = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
}
let mcpDisabledBadge = '';
if (tool.enabled === false) {
mcpDisabledBadge = `<span class="role-tool-mcp-disabled-badge" title="${escapeHtml(_t('roleModal.mcpDisabledBadgeTitle'))}">${escapeHtml(_t('roleModal.mcpDisabledBadge'))}</span>`;
}
// 生成唯一的checkbox id
const checkboxId = `role-tool-${escapeHtml(toolKey).replace(/::/g, '--')}`;
toolItem.innerHTML = `
<input type="checkbox" id="${checkboxId}" ${toolState.enabled ? 'checked' : ''}
title="${chkTitle}" aria-label="${chkTitle}"
onchange="handleRoleToolCheckboxChange('${escapeHtml(toolKey)}', this.checked)" />
<div class="role-tool-item-info">
<div class="role-tool-item-name">
${escapeHtml(tool.name)}
${externalBadge}
${mcpDisabledBadge}
</div>
<div class="role-tool-item-desc">${escapeHtml(tool.description || '无描述')}</div>
</div>
@@ -585,7 +700,7 @@ function renderRoleToolsList() {
toolsList.appendChild(listContainer);
}
// 渲染工具列表分页控件
// 渲染工具列表分页控件(始终展示范围与每页条数,便于在仅一页时仍可调整 page size)
function renderRoleToolsPagination() {
const toolsList = document.getElementById('role-tools-list');
if (!toolsList) return;
@@ -596,34 +711,78 @@ function renderRoleToolsPagination() {
oldPagination.remove();
}
// 如果只有一页或没有数据,不显示分页
if (roleToolsPagination.totalPages <= 1) {
return;
}
const pagination = document.createElement('div');
pagination.className = 'role-tools-pagination';
const { page, totalPages, total } = roleToolsPagination;
const startItem = (page - 1) * roleToolsPagination.pageSize + 1;
const endItem = Math.min(page * roleToolsPagination.pageSize, total);
const { page, totalPages, total, pageSize } = roleToolsPagination;
const startItem = total === 0 ? 0 : (page - 1) * pageSize + 1;
const endItem = total === 0 ? 0 : Math.min(page * pageSize, total);
const savedPageSize = getRoleToolsPageSize();
const perPageLabel = typeof window.t === 'function' ? window.t('mcp.perPage') : '每页';
const paginationShowText = _t('roleModal.paginationShow', { start: startItem, end: endItem, total: total }) +
(roleToolsSearchKeyword ? _t('roleModal.paginationSearch', { keyword: roleToolsSearchKeyword }) : '');
const navDisabled = total === 0 || totalPages <= 1;
pagination.innerHTML = `
<div class="pagination-info">${paginationShowText}</div>
<div class="pagination-page-size">
<label for="role-tools-page-size-pagination">${escapeHtml(perPageLabel)}</label>
<select id="role-tools-page-size-pagination" onchange="changeRoleToolsPageSize()">
<option value="10" ${savedPageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${savedPageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${savedPageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${savedPageSize === 100 ? 'selected' : ''}>100</option>
</select>
</div>
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadRoleTools(1, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${_t('roleModal.firstPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${page - 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${_t('roleModal.prevPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(1, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 || navDisabled ? 'disabled' : ''}>${_t('roleModal.firstPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${page - 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 || navDisabled ? 'disabled' : ''}>${_t('roleModal.prevPage')}</button>
<span class="pagination-page">${_t('roleModal.pageOf', { page: page, total: totalPages })}</span>
<button class="btn-secondary" onclick="loadRoleTools(${page + 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${_t('roleModal.nextPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${totalPages}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${_t('roleModal.lastPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${page + 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages || navDisabled ? 'disabled' : ''}>${_t('roleModal.nextPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${totalPages}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages || navDisabled ? 'disabled' : ''}>${_t('roleModal.lastPage')}</button>
</div>
`;
toolsList.appendChild(pagination);
}
function syncRoleToolsFilterButtons() {
const wrap = document.getElementById('role-tools-status-filter');
if (!wrap) return;
wrap.querySelectorAll('.btn-filter').forEach(btn => {
const v = btn.getAttribute('data-filter');
const filterVal = v === null || v === undefined ? '' : String(v);
btn.classList.toggle('active', filterVal === roleToolsStatusFilter);
});
}
function roleToolsListScopeLine() {
const n = roleToolsPagination.total || 0;
if (roleToolsStatusFilter === 'role_on') {
return _t('roleModal.statsListScopeRoleOn', { n: n });
}
if (roleToolsStatusFilter === 'role_off') {
return _t('roleModal.statsListScopeRoleOff', { n: n });
}
return _t('roleModal.statsListScopeAll', { n: n });
}
function filterRoleToolsByStatus(status) {
roleToolsStatusFilter = status;
syncRoleToolsFilterButtons();
loadRoleTools(1, roleToolsSearchKeyword);
}
async function changeRoleToolsPageSize() {
const sel = document.getElementById('role-tools-page-size-pagination');
if (!sel) return;
const newPageSize = parseInt(sel.value, 10);
if (isNaN(newPageSize) || newPageSize < 1) return;
localStorage.setItem('toolsPageSize', String(newPageSize));
roleToolsPagination.pageSize = newPageSize;
await loadRoleTools(1, roleToolsSearchKeyword);
}
// 处理工具checkbox状态变化
function handleRoleToolCheckboxChange(toolKey, enabled) {
const toolItem = document.querySelector(`.role-tool-item[data-tool-key="${toolKey}"]`);
@@ -640,7 +799,14 @@ function handleRoleToolCheckboxChange(toolKey, enabled) {
mcpEnabled: existingState ? existingState.mcpEnabled : true // 保留MCP启用状态
});
}
updateRoleToolsStats();
if (
roleToolsClientMode &&
(roleToolsStatusFilter === 'role_on' || roleToolsStatusFilter === 'role_off')
) {
loadRoleTools(roleToolsPagination.page, roleToolsSearchKeyword);
} else {
updateRoleToolsStats();
}
}
// 全选工具
@@ -667,7 +833,14 @@ function selectAllRoleTools() {
}
}
});
updateRoleToolsStats();
if (
roleToolsClientMode &&
(roleToolsStatusFilter === 'role_on' || roleToolsStatusFilter === 'role_off')
) {
loadRoleTools(roleToolsPagination.page, roleToolsSearchKeyword);
} else {
updateRoleToolsStats();
}
}
// 全不选工具
@@ -692,7 +865,14 @@ function deselectAllRoleTools() {
}
}
});
updateRoleToolsStats();
if (
roleToolsClientMode &&
(roleToolsStatusFilter === 'role_on' || roleToolsStatusFilter === 'role_off')
) {
loadRoleTools(roleToolsPagination.page, roleToolsSearchKeyword);
} else {
updateRoleToolsStats();
}
}
// 搜索工具
@@ -711,90 +891,64 @@ function clearRoleToolsSearch() {
searchRoleTools('');
}
// 更新工具统计信息
// 更新工具统计信息(口径:分母「可关联上限」= 全库 MCP 已开工具数,与 MCP 管理页筛选「MCP已开」条数一致;勾选=关联本角色)
function updateRoleToolsStats() {
const statsEl = document.getElementById('role-tools-stats');
if (!statsEl) return;
// 统计当前页已选中的工具数
const currentPageEnabled = Array.from(document.querySelectorAll('#role-tools-list input[type="checkbox"]:checked')).length;
// 统计当前页已启用的工具数(在MCP管理中已启用的工具)
// 优先从状态映射中获取,如果没有则从工具数据中获取
let currentPageEnabledInMCP = 0;
allRoleTools.forEach(tool => {
const toolKey = getToolKey(tool);
const state = roleToolStateMap.get(toolKey);
// 如果工具在MCP管理中已启用(从状态映射或工具数据中获取),计入当前页已启用工具数
const mcpEnabled = state ? (state.mcpEnabled !== false) : (tool.enabled !== false);
if (mcpEnabled) {
currentPageEnabledInMCP++;
}
});
// 如果使用所有工具,使用从API获取的已启用工具总数
const pageChecked = Array.from(document.querySelectorAll('#role-tools-list input[type="checkbox"]:checked')).length;
const pageTotal = document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
const mcpOnMax =
(roleToolsStatsMcpEnabledTotal > 0 ? roleToolsStatsMcpEnabledTotal : totalEnabledToolsInMCP) || 0;
const grandAll =
(roleToolsStatsGrandTotal > 0 ? roleToolsStatsGrandTotal : roleToolsPagination.total) || 0;
const scopeLine = roleToolsListScopeLine();
if (roleUsesAllTools) {
// 使用从API响应中获取的已启用工具总数
const totalEnabled = totalEnabledToolsInMCP || 0;
// 当前页分母应该是当前页的总工具数(每页20个),而不是当前页已启用的工具数
const currentPageTotal = document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
// 总工具数(所有工具,包括已启用和未启用的)
const totalTools = roleToolsPagination.total || 0;
statsEl.innerHTML = `
<span title="${_t('roleModal.currentPageSelectedTitle')}"> ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })}</span>
<span title="${_t('roleModal.totalSelectedTitle')}">📊 ${_t('roleModal.totalSelected', { current: totalEnabled, total: totalTools })} <em>${_t('roleModal.usingAllEnabledTools')}</em></span>
<div class="role-tools-stats-row">
<span title="${escapeHtml(_t('roleModal.statsPageLinkedTitle'))}"> ${_t('roleModal.statsPageLinked', { current: pageChecked, total: pageTotal })}</span>
</div>
<div class="role-tools-stats-row">
<span title="${escapeHtml(_t('roleModal.statsRoleUsesAllTitle'))}">📊 ${_t('roleModal.statsRoleUsesAll', { mcpOn: mcpOnMax, all: grandAll })}</span>
</div>
<div class="role-tools-stats-hint">📋 ${escapeHtml(scopeLine)}</div>
`;
return;
}
// 统计角色实际选中的工具数(只统计在MCP管理中已启用的工具)
let totalSelected = 0;
let roleLinked = 0;
roleToolStateMap.forEach(state => {
// 只统计在MCP管理中已启用且被角色选中的工具
if (state.enabled && state.mcpEnabled !== false) {
totalSelected++;
roleLinked++;
}
});
// 如果当前页有未保存的状态,需要合并计算
document.querySelectorAll('#role-tools-list input[type="checkbox"]').forEach(checkbox => {
const toolItem = checkbox.closest('.role-tool-item');
if (toolItem) {
const toolKey = toolItem.dataset.toolKey;
const savedState = roleToolStateMap.get(toolKey);
if (savedState && savedState.enabled !== checkbox.checked && savedState.mcpEnabled !== false) {
// 状态不一致,使用checkbox状态(但只统计MCP管理中已启用的工具)
if (checkbox.checked && !savedState.enabled) {
totalSelected++;
roleLinked++;
} else if (!checkbox.checked && savedState.enabled) {
totalSelected--;
roleLinked--;
}
}
}
});
// 角色可选择的所有已启用工具总数(应该基于MCP管理中的总数,而不是状态映射)
// 因为角色可以选择任意已启用的工具,所以总数应该是所有已启用工具的总数
let totalEnabledForRole = totalEnabledToolsInMCP || 0;
// 如果API返回的总数为0或未设置,尝试从状态映射中统计(作为备选方案)
if (totalEnabledForRole === 0) {
roleToolStateMap.forEach(state => {
// 只统计在MCP管理中已启用的工具
if (state.mcpEnabled !== false) { // mcpEnabled 为 true 或 undefined(未设置时默认为启用)
totalEnabledForRole++;
}
});
}
// 当前页分母应该是当前页的总工具数(每页20个),而不是当前页已启用的工具数
const currentPageTotal = document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
// 总工具数(所有工具,包括已启用和未启用的)
const totalTools = roleToolsPagination.total || 0;
const roleRow =
mcpOnMax > 0
? `<span title="${escapeHtml(_t('roleModal.statsRoleLinkedTitle'))}">📊 ${_t('roleModal.statsRoleLinked', { current: roleLinked, max: mcpOnMax })}</span>`
: `<span title="${escapeHtml(_t('roleModal.statsRoleLinkedNoMaxTitle'))}">📊 ${_t('roleModal.statsRoleLinkedNoMax', { current: roleLinked })}</span>`;
statsEl.innerHTML = `
<span title="${_t('roleModal.currentPageSelectedTitle')}"> ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })}</span>
<span title="${_t('roleModal.totalSelectedTitle')}">📊 ${_t('roleModal.totalSelected', { current: totalSelected, total: totalTools })}</span>
<div class="role-tools-stats-row">
<span title="${escapeHtml(_t('roleModal.statsPageLinkedTitle'))}"> ${_t('roleModal.statsPageLinked', { current: pageChecked, total: pageTotal })}</span>
</div>
<div class="role-tools-stats-row">${roleRow}</div>
<div class="role-tools-stats-hint">📋 ${escapeHtml(scopeLine)}</div>
`;
}
@@ -893,24 +1047,15 @@ async function showAddRoleModal() {
if (clearBtn) {
clearBtn.style.display = 'none';
}
roleToolsStatusFilter = '';
syncRoleToolsFilterButtons();
roleToolsPagination.pageSize = getRoleToolsPageSize();
// 清空工具列表 DOM,避免 loadRoleTools 中的 saveCurrentRolePageToolStates 读取旧状态
if (toolsList) {
toolsList.innerHTML = '';
}
// 重置skills状态
roleSelectedSkills.clear();
roleSkillsSearchKeyword = '';
const skillsSearchInput = document.getElementById('role-skills-search');
if (skillsSearchInput) {
skillsSearchInput.value = '';
}
const skillsClearBtn = document.getElementById('role-skills-search-clear');
if (skillsClearBtn) {
skillsClearBtn.style.display = 'none';
}
// 加载并渲染工具列表
await loadRoleTools(1, '');
@@ -922,9 +1067,6 @@ async function showAddRoleModal() {
// 确保统计信息正确更新(显示0/108)
updateRoleToolsStats();
// 加载并渲染skills列表
await loadRoleSkills();
modal.style.display = 'flex';
}
@@ -1007,6 +1149,9 @@ async function editRole(roleName) {
if (clearBtn) {
clearBtn.style.display = 'none';
}
roleToolsStatusFilter = '';
syncRoleToolsFilterButtons();
roleToolsPagination.pageSize = getRoleToolsPageSize();
// 优先使用tools字段,如果没有则使用mcps字段(向后兼容)
const selectedTools = role.tools || (role.mcps && role.mcps.length > 0 ? role.mcps : []);
@@ -1084,16 +1229,6 @@ async function editRole(roleName) {
}
}
// 加载并设置skills
await loadRoleSkills();
// 设置角色配置的skills
const selectedSkills = role.skills || [];
roleSelectedSkills.clear();
selectedSkills.forEach(skill => {
roleSelectedSkills.add(skill);
});
renderRoleSkills();
modal.style.display = 'flex';
}
@@ -1317,16 +1452,12 @@ async function saveRole() {
}
}
// 获取选中的skills
const skills = Array.from(roleSelectedSkills);
const roleData = {
name: name,
description: description,
icon: icon || undefined, // 如果为空字符串,则不发送该字段
user_prompt: userPrompt,
tools: tools, // 默认角色为空数组,表示使用所有工具
skills: skills, // Skills列表
enabled: enabled
};
const url = isEdit ? `/api/roles/${encodeURIComponent(name)}` : '/api/roles';
@@ -1459,6 +1590,7 @@ if (typeof window !== 'undefined') {
window.getCurrentRole = getCurrentRole;
window.toggleRoleSelectionPanel = toggleRoleSelectionPanel;
window.closeRoleSelectionPanel = closeRoleSelectionPanel;
window.filterRoleToolsByStatus = filterRoleToolsByStatus;
window.currentSelectedRole = getCurrentRole();
// 监听角色变化,更新全局变量
@@ -1470,157 +1602,3 @@ if (typeof window !== 'undefined') {
}
};
}
// ==================== Skills相关函数 ====================
// 加载skills列表
async function loadRoleSkills() {
try {
const response = await apiFetch('/api/roles/skills/list');
if (!response.ok) {
throw new Error('加载skills列表失败');
}
const data = await response.json();
allRoleSkills = data.skills || [];
renderRoleSkills();
} catch (error) {
console.error('加载skills列表失败:', error);
allRoleSkills = [];
const skillsList = document.getElementById('role-skills-list');
if (skillsList) {
skillsList.innerHTML = '<div class="skills-error">' + _t('roleModal.loadSkillsFailed') + ': ' + error.message + '</div>';
}
}
}
// 渲染skills列表
function renderRoleSkills() {
const skillsList = document.getElementById('role-skills-list');
if (!skillsList) return;
// 过滤skills
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
if (filteredSkills.length === 0) {
skillsList.innerHTML = '<div class="skills-empty">' +
(roleSkillsSearchKeyword ? _t('roleModal.noMatchingSkills') : _t('roleModal.noSkillsAvailable')) +
'</div>';
updateRoleSkillsStats();
return;
}
// 渲染skills列表
skillsList.innerHTML = filteredSkills.map(skill => {
const isSelected = roleSelectedSkills.has(skill);
return `
<div class="role-skill-item" data-skill="${skill}">
<label class="checkbox-label">
<input type="checkbox" class="modern-checkbox"
${isSelected ? 'checked' : ''}
onchange="toggleRoleSkill('${skill}', this.checked)" />
<span class="checkbox-custom"></span>
<span class="checkbox-text">${escapeHtml(skill)}</span>
</label>
</div>
`;
}).join('');
updateRoleSkillsStats();
}
// 切换skill选中状态
function toggleRoleSkill(skill, checked) {
if (checked) {
roleSelectedSkills.add(skill);
} else {
roleSelectedSkills.delete(skill);
}
updateRoleSkillsStats();
}
// 全选skills
function selectAllRoleSkills() {
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
filteredSkills.forEach(skill => {
roleSelectedSkills.add(skill);
});
renderRoleSkills();
}
// 全不选skills
function deselectAllRoleSkills() {
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
filteredSkills.forEach(skill => {
roleSelectedSkills.delete(skill);
});
renderRoleSkills();
}
// 搜索skills
function searchRoleSkills(keyword) {
roleSkillsSearchKeyword = keyword;
const clearBtn = document.getElementById('role-skills-search-clear');
if (clearBtn) {
clearBtn.style.display = keyword ? 'block' : 'none';
}
renderRoleSkills();
}
// 清除skills搜索
function clearRoleSkillsSearch() {
const searchInput = document.getElementById('role-skills-search');
if (searchInput) {
searchInput.value = '';
}
roleSkillsSearchKeyword = '';
const clearBtn = document.getElementById('role-skills-search-clear');
if (clearBtn) {
clearBtn.style.display = 'none';
}
renderRoleSkills();
}
// 更新skills统计信息
function updateRoleSkillsStats() {
const statsEl = document.getElementById('role-skills-stats');
if (!statsEl) return;
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
const selectedCount = Array.from(roleSelectedSkills).filter(skill =>
filteredSkills.includes(skill)
).length;
statsEl.textContent = _t('roleModal.skillsSelectedCount', { count: selectedCount, total: filteredSkills.length });
}
// HTML转义函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
+5 -5
View File
@@ -59,6 +59,7 @@ function switchPage(pageId) {
initPage(pageId);
}
}
window.switchPage = switchPage;
// 更新导航状态
function updateNavState(pageId) {
@@ -159,6 +160,7 @@ function toggleSubmenu(menuId) {
navItem.classList.toggle('expanded');
}
}
window.toggleSubmenu = toggleSubmenu;
// 显示子菜单弹出框
function showSubmenuPopup(navItem, menuId) {
@@ -427,6 +429,7 @@ function toggleSidebar() {
localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false');
}
}
window.toggleSidebar = toggleSidebar;
// 初始化侧边栏状态
function initSidebarState() {
@@ -449,6 +452,7 @@ function toggleConversationSidebar() {
localStorage.setItem('conversationSidebarCollapsed', isCollapsed ? 'true' : 'false');
}
}
window.toggleConversationSidebar = toggleConversationSidebar;
// 恢复对话列表折叠状态(进入对话页时生效)
function initConversationSidebarState() {
@@ -463,10 +467,6 @@ function initConversationSidebarState() {
}
}
// 导出函数供其他脚本使用
window.switchPage = switchPage;
window.toggleSubmenu = toggleSubmenu;
window.toggleSidebar = toggleSidebar;
window.toggleConversationSidebar = toggleConversationSidebar;
// 导出函数供其他脚本使用(与上方尽早绑定保持一致,便于外部脚本探测)
window.currentPage = function() { return currentPage; };
+503 -84
View File
@@ -28,6 +28,8 @@ let webshellClearInProgress = false;
// AI 助手:按连接 ID 保存对话 ID,便于多轮对话
let webshellAiConvMap = {};
let webshellAiSending = false;
let webshellAiAbortController = null; // AbortController for current AI stream
let webshellAiStreamReader = null; // Current ReadableStreamDefaultReader
let webshellDbConfigByConn = {};
let webshellDirTreeByConn = {};
let webshellDirExpandedByConn = {};
@@ -70,6 +72,237 @@ function resolveWebshellAiStreamRequest() {
});
}
// ─── WebShell AI 助手:角色 + 对话模式选择器(与主「对话」页对齐) ───
let wsRolesCache = null; // 缓存 /api/roles 结果
function wsLoadRoles() {
if (typeof apiFetch === 'undefined') return;
apiFetch('/api/roles').then(function (r) { return r.json(); }).then(function (data) {
wsRolesCache = (data && Array.isArray(data.roles)) ? data.roles : [];
wsRenderRoleList();
wsUpdateRoleSelectorDisplay();
}).catch(function () { /* ignore */ });
}
function wsUpdateRoleSelectorDisplay() {
var iconEl = document.getElementById('ws-role-selector-icon');
var textEl = document.getElementById('ws-role-selector-text');
if (!iconEl || !textEl) return;
var cur = (typeof getCurrentRole === 'function') ? getCurrentRole() : (localStorage.getItem('currentRole') || '');
if (!cur) {
iconEl.textContent = '\ud83d\udd35';
textEl.textContent = (typeof window.t === 'function' ? window.t('chat.defaultRole') : '') || '默认';
return;
}
if (wsRolesCache) {
for (var i = 0; i < wsRolesCache.length; i++) {
if (wsRolesCache[i].name === cur) {
iconEl.textContent = wsRolesCache[i].icon || '\ud83d\udd35';
textEl.textContent = cur;
return;
}
}
}
iconEl.textContent = '\ud83d\udd35';
textEl.textContent = cur;
}
function wsRenderRoleList() {
var listEl = document.getElementById('ws-role-selection-list');
if (!listEl) return;
var cur = (typeof getCurrentRole === 'function') ? getCurrentRole() : (localStorage.getItem('currentRole') || '');
var html = '';
// 默认角色
var defSelected = !cur ? ' selected' : '';
html += '<button type="button" class="role-selection-item-main' + defSelected + '" onclick="wsSelectRole(\'\')">' +
'<div class="role-selection-item-icon-main">\ud83d\udd35</div>' +
'<div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' +
(wsTOr('chat.defaultRole', '默认')) +
'</div><div class="role-selection-item-description-main">' +
(wsTOr('roles.defaultRoleDescription', '默认角色,不额外携带用户提示词,使用所有工具')) +
'</div></div>' +
(defSelected ? '<div class="role-selection-checkmark-main">\u2713</div>' : '') +
'</button>';
if (wsRolesCache) {
for (var i = 0; i < wsRolesCache.length; i++) {
var r = wsRolesCache[i];
if (!r.enabled) continue;
if (r.name === '默认') continue; // 已在上方硬编码默认角色,跳过 API 返回的默认项
var sel = (r.name === cur) ? ' selected' : '';
html += '<button type="button" class="role-selection-item-main' + sel + '" onclick="wsSelectRole(\'' + r.name.replace(/'/g, "\\'") + '\')">' +
'<div class="role-selection-item-icon-main">' + (r.icon || '\ud83d\udd35') + '</div>' +
'<div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + r.name + '</div>' +
'<div class="role-selection-item-description-main">' + (r.description || '').substring(0, 60) + '</div></div>' +
(sel ? '<div class="role-selection-checkmark-main">\u2713</div>' : '') +
'</button>';
}
}
listEl.innerHTML = html;
}
function wsSelectRole(name) {
var roleName = name || '';
// 使用主页的 handleRoleChange 来同步 roles.js 内部状态和 localStorage
if (typeof handleRoleChange === 'function') {
try { handleRoleChange(roleName); } catch (e) { /* */ }
} else {
try { localStorage.setItem('currentRole', roleName); } catch (e) { /* */ }
}
if (typeof window.currentSelectedRole !== 'undefined') window.currentSelectedRole = roleName;
wsUpdateRoleSelectorDisplay();
wsRenderRoleList();
wsCloseRolePanel();
}
function wsToggleRolePanel() {
var panel = document.getElementById('ws-role-selection-panel');
if (!panel) return;
var isOpen = panel.style.display === 'flex';
if (isOpen) { wsCloseRolePanel(); return; }
wsCloseAgentModePanel();
panel.style.display = 'flex';
}
function wsCloseRolePanel() {
var panel = document.getElementById('ws-role-selection-panel');
if (panel) panel.style.display = 'none';
}
// ─── 对话模式选择器 ───
function wsInitAgentMode() {
if (typeof apiFetch === 'undefined') return;
apiFetch('/api/config').then(function (r) { return r.ok ? r.json() : null; }).then(function (cfg) {
var wrapper = document.getElementById('ws-agent-mode-wrapper');
if (!wrapper) return;
wrapper.style.display = '';
// 是否启用多代理
var multiOn = cfg && cfg.multi_agent && cfg.multi_agent.enabled;
// 隐藏/显示多代理选项
var opts = wrapper.querySelectorAll('.ws-agent-mode-option');
opts.forEach(function (el) {
var v = el.getAttribute('data-value');
if (v === 'deep' || v === 'plan_execute' || v === 'supervisor') {
el.style.display = multiOn ? '' : 'none';
}
});
// 标准化当前值
var stored = localStorage.getItem('cyberstrike-chat-agent-mode');
var norm;
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') {
norm = window.csaiChatAgentMode.normalizeStored(stored, cfg);
} else {
norm = stored || 'react';
if (norm === 'single') norm = 'react';
if (norm === 'multi') norm = 'deep';
}
wsSyncAgentMode(norm);
}).catch(function () {
var wrapper = document.getElementById('ws-agent-mode-wrapper');
if (wrapper) wrapper.style.display = '';
wsSyncAgentMode('react');
});
}
function wsSyncAgentMode(value) {
var hid = document.getElementById('ws-agent-mode-select');
var label = document.getElementById('ws-agent-mode-text');
var icon = document.getElementById('ws-agent-mode-icon');
if (hid) hid.value = value;
if (label) label.textContent = (typeof getAgentModeLabelForValue === 'function') ? getAgentModeLabelForValue(value) : value;
if (icon) icon.textContent = (typeof getAgentModeIconForValue === 'function') ? getAgentModeIconForValue(value) : '\ud83e\udd16';
var wrapper = document.getElementById('ws-agent-mode-wrapper');
if (wrapper) {
wrapper.querySelectorAll('.ws-agent-mode-option').forEach(function (el) {
el.classList.toggle('selected', el.getAttribute('data-value') === value);
});
}
}
function wsSelectAgentMode(mode) {
try { localStorage.setItem('cyberstrike-chat-agent-mode', mode); } catch (e) { /* */ }
wsSyncAgentMode(mode);
wsCloseAgentModePanel();
// 同步主页模式选择器
if (typeof syncAgentModeFromValue === 'function') try { syncAgentModeFromValue(mode); } catch (e) { /* */ }
}
function wsToggleAgentModePanel() {
var panel = document.getElementById('ws-agent-mode-panel');
if (!panel) return;
var isOpen = panel.style.display === 'flex';
if (isOpen) { wsCloseAgentModePanel(); return; }
wsCloseRolePanel();
panel.style.display = 'flex';
}
function wsCloseAgentModePanel() {
var panel = document.getElementById('ws-agent-mode-panel');
if (panel) panel.style.display = 'none';
}
/** 当 WebShell AI Tab 可见时刷新选择器显示(同步主页可能的更改) */
function wsRefreshSelectors() {
wsUpdateRoleSelectorDisplay();
wsRenderRoleList();
var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'react';
wsSyncAgentMode(stored);
}
// 点击面板外部关闭
document.addEventListener('click', function (e) {
var rolePanel = document.getElementById('ws-role-selection-panel');
var roleBtn = document.getElementById('ws-role-selector-btn');
if (rolePanel && rolePanel.style.display !== 'none' && roleBtn && !rolePanel.contains(e.target) && !roleBtn.contains(e.target)) {
wsCloseRolePanel();
}
var modePanel = document.getElementById('ws-agent-mode-panel');
var modeBtn = document.getElementById('ws-agent-mode-btn');
if (modePanel && modePanel.style.display !== 'none' && modeBtn && !modePanel.contains(e.target) && !modeBtn.contains(e.target)) {
wsCloseAgentModePanel();
}
});
// ─── end WebShell AI 选择器 ───
/** 停止当前 WebShell AI 流式请求 */
function wsStopAiStream(conn) {
// 1. Abort the fetch
if (webshellAiAbortController) {
try { webshellAiAbortController.abort(); } catch (e) { /* */ }
webshellAiAbortController = null;
}
// 2. Cancel the reader
if (webshellAiStreamReader) {
try { webshellAiStreamReader.cancel(); } catch (e) { /* */ }
webshellAiStreamReader = null;
}
// 3. Call backend cancel API if we have a conversation
var convId = conn && conn.id ? (webshellAiConvMap[conn.id] || '') : '';
if (convId && typeof apiFetch === 'function') {
apiFetch('/api/agent-loop/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ conversationId: convId })
}).catch(function () { /* ignore */ });
}
// 4. Reset UI state
wsSetAiSendingState(false);
}
/** 切换发送/停止按钮状态 */
function wsSetAiSendingState(sending) {
webshellAiSending = sending;
var sendBtn = document.getElementById('webshell-ai-send');
var stopBtn = document.getElementById('webshell-ai-stop');
if (sendBtn) {
sendBtn.disabled = sending;
sendBtn.style.display = sending ? 'none' : '';
}
if (stopBtn) {
stopBtn.style.display = sending ? '' : 'none';
}
}
// 从服务端(SQLite)拉取连接列表
function getWebshellConnections() {
if (typeof apiFetch === 'undefined') {
@@ -1441,7 +1674,7 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
el.classList.toggle('active', el.dataset.convId === convId);
});
if (typeof apiFetch !== 'function') return;
apiFetch('/api/conversations/' + encodeURIComponent(convId), { method: 'GET' })
apiFetch('/api/conversations/' + encodeURIComponent(convId) + '?include_process_details=1', { method: 'GET' })
.then(function (r) { return r.json(); })
.then(function (data) {
messagesContainer.innerHTML = '';
@@ -1572,9 +1805,45 @@ function selectWebshell(id, stateReady) {
'</div>' +
'<div class="webshell-ai-main">' +
'<div id="webshell-ai-messages" class="webshell-ai-messages"></div>' +
'<div class="webshell-ai-input-area">' +
'<div class="webshell-ai-selectors-row">' +
'<div class="ws-role-selector-wrapper">' +
'<button type="button" class="role-selector-btn ws-role-selector-btn" id="ws-role-selector-btn" onclick="wsToggleRolePanel()">' +
'<span id="ws-role-selector-icon" class="role-selector-icon">\ud83d\udd35</span>' +
'<span id="ws-role-selector-text" class="role-selector-text">' + (wsT('chat.defaultRole') || '默认') + '</span>' +
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
'</button>' +
'<div id="ws-role-selection-panel" class="role-selection-panel" style="display:none;">' +
'<div class="role-selection-panel-header"><h3 class="role-selection-panel-title">' + (wsT('chatGroup.rolePanelTitle') || '选择角色') + '</h3>' +
'<button type="button" class="role-selection-panel-close" onclick="wsCloseRolePanel()"><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 id="ws-role-selection-list" class="role-selection-list-main"></div></div>' +
'</div>' +
'<div class="ws-agent-mode-wrapper" id="ws-agent-mode-wrapper" style="display:none;">' +
'<div class="agent-mode-inner">' +
'<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-text" class="role-selector-text">' + (wsT('chat.agentModeReactNative') || '原生 ReAct') + '</span>' +
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
'</button>' +
'<div id="ws-agent-mode-panel" class="agent-mode-panel" style="display:none;" role="listbox">' +
'<div class="role-selection-panel-header agent-mode-panel-header"><h3 class="role-selection-panel-title">' + (wsT('chat.agentModePanelTitle') || '对话模式') + '</h3>' +
'<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 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="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') || 'DeepDeepAgent') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeDeepHint') || 'Eino DeepAgenttask 调度子代理') + '</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="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>' +
'<input type="hidden" id="ws-agent-mode-select" value="react" autocomplete="off" />' +
'</div>' +
'</div>' +
'<div class="webshell-ai-input-row">' +
'<textarea id="webshell-ai-input" class="webshell-ai-input form-control" rows="2" placeholder="' + (wsT('webshell.aiPlaceholder') || '例如:列出当前目录下的文件') + '"></textarea>' +
'<button type="button" class="btn-primary" id="webshell-ai-send">' + (wsT('webshell.aiSend') || '发送') + '</button>' +
'<button type="button" class="btn-danger webshell-ai-stop-btn" id="webshell-ai-stop" style="display:none;">' + wsTOr('webshell.aiStop', '停止') + '</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
@@ -1635,6 +1904,9 @@ function selectWebshell(id, stateReady) {
if (tab === 'terminal' && webshellTerminalInstance && webshellTerminalFitAddon) {
try { webshellTerminalFitAddon.fit(); } catch (e) {}
}
if (tab === 'ai') {
try { wsRefreshSelectors(); } catch (e) {}
}
});
});
@@ -1710,6 +1982,10 @@ function selectWebshell(id, stateReady) {
var aiMessages = document.getElementById('webshell-ai-messages');
var aiNewConvBtn = document.getElementById('webshell-ai-new-conv');
var aiConvListEl = document.getElementById('webshell-ai-conv-list');
// 初始化角色 + 模式选择器
wsLoadRoles();
wsInitAgentMode();
var aiMemoInput = document.getElementById('webshell-ai-memo-input');
var aiMemoStatus = document.getElementById('webshell-ai-memo-status');
var aiMemoClearBtn = document.getElementById('webshell-ai-memo-clear');
@@ -1770,7 +2046,11 @@ function selectWebshell(id, stateReady) {
});
}
if (aiSendBtn && aiInput && aiMessages) {
var aiStopBtn = document.getElementById('webshell-ai-stop');
aiSendBtn.addEventListener('click', function () { runWebshellAiSend(conn, aiInput, aiSendBtn, aiMessages); });
if (aiStopBtn) {
aiStopBtn.addEventListener('click', function () { wsStopAiStream(conn); });
}
aiInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -2347,8 +2627,8 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
return;
}
webshellAiSending = true;
if (sendBtn) sendBtn.disabled = true;
webshellAiAbortController = new AbortController();
wsSetAiSendingState(true);
var userDiv = document.createElement('div');
userDiv.className = 'webshell-ai-msg user';
@@ -2427,14 +2707,18 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
}
var einoSubReplyStreams = new Map();
var wsThinkingStreams = new Map(); // streamId → { el, buf }
var wsToolResultStreams = new Map(); // toolCallId → { el, buf }
if (inputEl) inputEl.value = '';
var convId = webshellAiConvMap[conn.id] || '';
var wsRole = (typeof getCurrentRole === 'function') ? getCurrentRole() : (localStorage.getItem('currentRole') || '');
var body = {
message: message,
webshellConnectionId: conn.id,
conversationId: convId
conversationId: convId,
role: wsRole
};
// 流式输出:支持 progress 实时更新、response 打字机效果;若后端发送多段 response 则追加
@@ -2448,7 +2732,8 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
return apiFetch(info.path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
body: JSON.stringify(body),
signal: webshellAiAbortController ? webshellAiAbortController.signal : undefined
});
}).then(function (response) {
if (!response.ok) {
@@ -2458,6 +2743,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
return response.body.getReader();
}).then(function (reader) {
if (!reader) return;
webshellAiStreamReader = reader;
var decoder = new TextDecoder();
var buffer = '';
return reader.read().then(function processChunk(result) {
@@ -2470,9 +2756,12 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
if (line.indexOf('data: ') !== 0) continue;
try {
var eventData = JSON.parse(line.slice(6));
if (eventData.type === 'conversation' && eventData.data && eventData.data.conversationId) {
// 先把 conversationId 拿出来,避免后续异步回调里 eventData 被后续事件覆盖导致 undefined 报错
var convId = eventData.data.conversationId;
var _et = eventData.type;
var _ed = eventData.data || {};
var _em = eventData.message || '';
if (_et === 'conversation' && _ed.conversationId) {
var convId = _ed.conversationId;
webshellAiConvMap[conn.id] = convId;
var listEl = document.getElementById('webshell-ai-conv-list');
if (listEl) fetchAndRenderWebshellAiConvList(conn, listEl).then(function () {
@@ -2480,100 +2769,219 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
el.classList.toggle('active', el.dataset.convId === convId);
});
});
} else if (eventData.type === 'response_start') {
// ─── Response streaming ───
} else if (_et === 'response_start') {
streamingTarget = '';
webshellStreamingTypingId += 1;
streamingTypingId = webshellStreamingTypingId;
assistantDiv.textContent = '…';
messagesContainer.scrollTop = messagesContainer.scrollHeight;
} else if (eventData.type === 'response_delta') {
var deltaText = (eventData.message != null && eventData.message !== '') ? String(eventData.message) : '';
} else if (_et === 'response_delta') {
var deltaText = (_em != null && _em !== '') ? String(_em) : '';
if (deltaText) {
streamingTarget += deltaText;
webshellStreamingTypingId += 1;
streamingTypingId = webshellStreamingTypingId;
runWebshellAiStreamingTyping(assistantDiv, streamingTarget, streamingTypingId, messagesContainer);
}
} else if (eventData.type === 'response') {
var text = (eventData.message != null && eventData.message !== '') ? eventData.message : (eventData.data && typeof eventData.data === 'string' ? eventData.data : '');
} else if (_et === 'response') {
var text = (_em != null && _em !== '') ? _em : (typeof _ed === 'string' ? _ed : '');
if (text) {
// response 为最终完整内容:避免与增量重复拼接
streamingTarget = String(text);
webshellStreamingTypingId += 1;
streamingTypingId = webshellStreamingTypingId;
runWebshellAiStreamingTyping(assistantDiv, streamingTarget, streamingTypingId, messagesContainer);
}
} else if (eventData.type === 'error' && eventData.message) {
// ─── Terminal events ───
} else if (_et === 'error' && _em) {
streamingTypingId += 1;
var errLabel = (typeof window.t === 'function') ? window.t('chat.error') : '错误';
appendTimelineItem('error', '❌ ' + errLabel, eventData.message, eventData.data);
renderWebshellAiErrorMessage(assistantDiv, errLabel + ': ' + eventData.message);
} else if (eventData.type === 'progress' && eventData.message) {
var errLabel = wsTOr('chat.error', '错误');
appendTimelineItem('error', '❌ ' + errLabel, _em, _ed);
renderWebshellAiErrorMessage(assistantDiv, errLabel + ': ' + _em);
} else if (_et === 'cancelled') {
streamingTypingId += 1;
var cancelLabel = wsTOr('chat.taskCancelled', '任务已取消');
appendTimelineItem('cancelled', '⛔ ' + cancelLabel, _em, _ed);
if (!streamingTarget && !assistantDiv.dataset.hasContent) {
assistantDiv.textContent = cancelLabel;
}
} else if (_et === 'done') {
// 清理流式状态
wsThinkingStreams.clear();
wsToolResultStreams.clear();
einoSubReplyStreams.clear();
// ─── Iteration / Progress ───
} else if (_et === 'progress' && _em) {
var progressMsg = (typeof window.translateProgressMessage === 'function')
? window.translateProgressMessage(eventData.message)
: eventData.message;
appendTimelineItem('progress', '🔍 ' + progressMsg, '', eventData.data);
? window.translateProgressMessage(_em) : _em;
appendTimelineItem('progress', '🔍 ' + progressMsg, '', _ed);
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'iteration') {
var iterN = (eventData.data && eventData.data.iteration) || 0;
var iterTitle = (typeof window.t === 'function')
? window.t('chat.iterationRound', { n: iterN || 1 })
: (iterN ? ('第 ' + iterN + ' 轮迭代') : (eventData.message || '迭代'));
var iterMessage = eventData.message || '';
} else if (_et === 'iteration') {
var iterN = _ed.iteration || 0;
var iterTitle = wsTOr('chat.iterationRound', '') || (iterN ? ('第 ' + iterN + ' 轮迭代') : (_em || '迭代'));
if (typeof window.t === 'function' && iterN) {
iterTitle = window.t('chat.iterationRound', { n: iterN });
}
var iterMessage = _em || '';
if (iterMessage && typeof window.translateProgressMessage === 'function') {
iterMessage = window.translateProgressMessage(iterMessage);
}
appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, eventData.data);
appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, _ed);
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'thinking' && eventData.message) {
var thinkLabel = (typeof window.t === 'function') ? window.t('chat.aiThinking') : 'AI 思考';
var thinkD = eventData.data || {};
appendTimelineItem('thinking', webshellAgentPx(thinkD) + '🤔 ' + thinkLabel, eventData.message, thinkD);
// ─── Thinking (non-stream + stream) ───
} else if (_et === 'thinking_stream_start' && _ed.streamId) {
var thinkSLabel = wsTOr('chat.aiThinking', 'AI 思考');
var thinkSItem = document.createElement('div');
thinkSItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-thinking';
thinkSItem.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + '🤔 ' + thinkSLabel) + '</span>';
var thinkSPre = document.createElement('div');
thinkSPre.className = 'webshell-ai-timeline-msg webshell-thinking-stream-body';
thinkSItem.appendChild(thinkSPre);
timelineContainer.appendChild(thinkSItem);
timelineContainer.classList.add('has-items');
wsThinkingStreams.set(_ed.streamId, { el: thinkSItem, body: thinkSPre, buf: '' });
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'tool_calls_detected' && eventData.data) {
var count = eventData.data.count || 0;
var detectedLabel = (typeof window.t === 'function')
? window.t('chat.toolCallsDetected', { count: count })
: ('检测到 ' + count + ' 个工具调用');
appendTimelineItem('tool_calls_detected', webshellAgentPx(eventData.data) + '🔧 ' + detectedLabel, eventData.message || '', eventData.data);
} else if (_et === 'thinking_stream_delta' && _ed.streamId) {
var tsD = wsThinkingStreams.get(_ed.streamId);
if (tsD) {
tsD.buf += (_em || '');
if (typeof formatMarkdown === 'function') {
tsD.body.innerHTML = formatMarkdown(tsD.buf);
} else {
tsD.body.textContent = tsD.buf;
}
}
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'tool_call' && eventData.data) {
var d = eventData.data;
var tn = d.toolName || '未知工具';
var idx = d.index || 0;
var total = d.total || 0;
var callTitle = (typeof window.t === 'function')
? window.t('chat.callTool', { name: tn, index: idx, total: total })
: ('调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : ''));
var title = webshellAgentPx(d) + '🔧 ' + callTitle;
appendTimelineItem('tool_call', title, eventData.message || '', eventData.data);
} else if (_et === 'thinking_stream_end' && _ed.streamId) {
var tsE = wsThinkingStreams.get(_ed.streamId);
if (tsE) {
var fullThink = (_em != null && _em !== '') ? String(_em) : tsE.buf;
if (typeof formatMarkdown === 'function') {
tsE.body.innerHTML = formatMarkdown(fullThink);
} else {
tsE.body.textContent = fullThink;
}
wsThinkingStreams.delete(_ed.streamId);
}
} else if (_et === 'thinking' && _em) {
// 如果有 streamId 且已存在流式条目,跳过避免重复
if (_ed.streamId && wsThinkingStreams.has(_ed.streamId)) {
// 已由 thinking_stream_* 处理
} else {
var thinkLabel = wsTOr('chat.aiThinking', 'AI 思考');
appendTimelineItem('thinking', webshellAgentPx(_ed) + '🤔 ' + thinkLabel, _em, _ed);
}
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'tool_result' && eventData.data) {
var dr = eventData.data;
var success = dr.success !== false;
var tname = dr.toolName || '工具';
var titleText = (typeof window.t === 'function')
? (success ? window.t('chat.toolExecComplete', { name: tname }) : window.t('chat.toolExecFailed', { name: tname }))
: (tname + (success ? ' 执行完成' : ' 执行失败'));
var title = webshellAgentPx(dr) + (success ? '✅ ' : '❌ ') + titleText;
var sub = eventData.message || (dr.result ? String(dr.result).slice(0, 300) : '');
appendTimelineItem('tool_result', title, sub, eventData.data);
// ─── Warning ───
} else if (_et === 'warning') {
appendTimelineItem('warning', '⚠️ ' + (_em || ''), '', _ed);
// ─── Eino recovery ───
} else if (_et === 'eino_recovery') {
var runIdx = _ed.runIndex != null ? _ed.runIndex : (_ed.einoRetry != null ? _ed.einoRetry + 1 : 1);
var maxRuns = _ed.maxRuns != null ? _ed.maxRuns : 3;
var recTitle = wsTOr('chat.einoRecoveryTitle', '') ||
('🔄 工具参数无效 · 第 ' + runIdx + '/' + maxRuns + ' 轮(已追加提示)');
if (typeof window.t === 'function') {
try { recTitle = window.t('chat.einoRecoveryTitle', { n: runIdx, max: maxRuns }); } catch (e) { /* */ }
}
appendTimelineItem('eino_recovery', recTitle, _em, _ed);
// ─── Tool calls ───
} else if (_et === 'tool_calls_detected' && _ed) {
var count = _ed.count || 0;
var detectedLabel = wsTOr('chat.toolCallsDetected', '') || ('检测到 ' + count + ' 个工具调用');
if (typeof window.t === 'function') {
try { detectedLabel = window.t('chat.toolCallsDetected', { count: count }); } catch (e) { /* */ }
}
appendTimelineItem('tool_calls_detected', webshellAgentPx(_ed) + '🔧 ' + detectedLabel, _em || '', _ed);
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'eino_agent_reply_stream_start' && eventData.data && eventData.data.streamId) {
var rdS = eventData.data;
var repTS = (typeof window.t === 'function') ? window.t('chat.einoAgentReplyTitle') : '子代理回复';
var runTS = (typeof window.t === 'function') ? window.t('timeline.running') : '执行中...';
} else if (_et === 'tool_call' && _ed) {
var tn = _ed.toolName || '未知工具';
var idx = _ed.index || 0;
var total = _ed.total || 0;
var callTitle = wsTOr('chat.callTool', '') || ('调用工具: ' + tn + (total ? ' (' + idx + '/' + total + ')' : ''));
if (typeof window.t === 'function') {
try { callTitle = window.t('chat.callTool', { name: tn, index: idx, total: total }); } catch (e) { /* */ }
}
appendTimelineItem('tool_call', webshellAgentPx(_ed) + '🔧 ' + callTitle, _em || '', _ed);
if (!streamingTarget) assistantDiv.textContent = '…';
// ─── Tool result delta (streaming output) ───
} else if (_et === 'tool_result_delta' && _ed.toolCallId) {
var trdKey = _ed.toolCallId;
var trdDelta = _em || '';
if (trdDelta) {
var trdState = wsToolResultStreams.get(trdKey);
if (!trdState) {
var trdName = _ed.toolName || '工具';
var runLabel = wsTOr('timeline.running', '执行中...');
var trdItem = document.createElement('div');
trdItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-tool_result';
trdItem.innerHTML = '<span class="webshell-ai-timeline-title">' +
escapeHtml(webshellAgentPx(_ed) + '⏳ ' + runLabel + ' ' + trdName) +
'</span><div class="webshell-ai-timeline-msg"><div class="tool-result-section success">' +
'<pre class="tool-result"></pre></div></div>';
timelineContainer.appendChild(trdItem);
timelineContainer.classList.add('has-items');
trdState = { el: trdItem, buf: '' };
wsToolResultStreams.set(trdKey, trdState);
}
trdState.buf += trdDelta;
var trdPre = trdState.el.querySelector('pre.tool-result');
if (trdPre) trdPre.textContent = trdState.buf;
}
if (!streamingTarget) assistantDiv.textContent = '…';
// ─── Tool result (final) ───
} else if (_et === 'tool_result' && _ed) {
var success = _ed.success !== false;
var tname = _ed.toolName || '工具';
var titleText = wsTOr(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', '') ||
(tname + (success ? ' 执行完成' : ' 执行失败'));
if (typeof window.t === 'function') {
try { titleText = window.t(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', { name: tname }); } catch (e) { /* */ }
}
// 如果有流式占位条目,更新标题
var trdExist = _ed.toolCallId ? wsToolResultStreams.get(_ed.toolCallId) : null;
if (trdExist) {
var trdTitleEl = trdExist.el.querySelector('.webshell-ai-timeline-title');
if (trdTitleEl) trdTitleEl.textContent = webshellAgentPx(_ed) + (success ? '✅ ' : '❌ ') + titleText;
// 更新结果内容
var resultText = _ed.result ? String(_ed.result) : (_em || '');
var trdPreEl = trdExist.el.querySelector('pre.tool-result');
if (trdPreEl && resultText) trdPreEl.textContent = resultText;
// 更新 section class
var trdSection = trdExist.el.querySelector('.tool-result-section');
if (trdSection) { trdSection.className = 'tool-result-section ' + (success ? 'success' : 'error'); }
wsToolResultStreams.delete(_ed.toolCallId);
} else {
var title = webshellAgentPx(_ed) + (success ? '✅ ' : '❌ ') + titleText;
var sub = _em || (_ed.result ? String(_ed.result).slice(0, 300) : '');
appendTimelineItem('tool_result', title, sub, _ed);
}
if (!streamingTarget) assistantDiv.textContent = '…';
// ─── Eino sub-agent reply streaming ───
} else if (_et === 'eino_agent_reply_stream_start' && _ed.streamId) {
var repTS = wsTOr('chat.einoAgentReplyTitle', '子代理回复');
var runTS = wsTOr('timeline.running', '执行中...');
var itemS = document.createElement('div');
itemS.className = 'webshell-ai-timeline-item webshell-ai-timeline-eino_agent_reply';
itemS.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(rdS) + '💬 ' + repTS + ' · ' + runTS) + '</span>';
itemS.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + '💬 ' + repTS + ' · ' + runTS) + '</span>';
timelineContainer.appendChild(itemS);
timelineContainer.classList.add('has-items');
einoSubReplyStreams.set(rdS.streamId, { el: itemS, buf: '' });
einoSubReplyStreams.set(_ed.streamId, { el: itemS, buf: '' });
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'eino_agent_reply_stream_delta' && eventData.data && eventData.data.streamId) {
var stD = einoSubReplyStreams.get(eventData.data.streamId);
} else if (_et === 'eino_agent_reply_stream_delta' && _ed.streamId) {
var stD = einoSubReplyStreams.get(_ed.streamId);
if (stD) {
stD.buf += (eventData.message || '');
stD.buf += (_em || '');
var preD = stD.el.querySelector('.webshell-eino-reply-stream-body');
if (!preD) {
preD = document.createElement('pre');
@@ -2581,17 +2989,20 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
preD.style.whiteSpace = 'pre-wrap';
stD.el.appendChild(preD);
}
preD.textContent = stD.buf;
if (typeof formatMarkdown === 'function') {
preD.innerHTML = formatMarkdown(stD.buf);
} else {
preD.textContent = stD.buf;
}
}
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'eino_agent_reply_stream_end' && eventData.data && eventData.data.streamId) {
var stE = einoSubReplyStreams.get(eventData.data.streamId);
} else if (_et === 'eino_agent_reply_stream_end' && _ed.streamId) {
var stE = einoSubReplyStreams.get(_ed.streamId);
if (stE) {
var fullE = (eventData.message != null && eventData.message !== '') ? String(eventData.message) : stE.buf;
stE.buf = fullE;
var repTE = (typeof window.t === 'function') ? window.t('chat.einoAgentReplyTitle') : '子代理回复';
var fullE = (_em != null && _em !== '') ? String(_em) : stE.buf;
var repTE = wsTOr('chat.einoAgentReplyTitle', '子代理回复');
var titE = stE.el.querySelector('.webshell-ai-timeline-title');
if (titE) titE.textContent = webshellAgentPx(eventData.data) + '💬 ' + repTE;
if (titE) titE.textContent = webshellAgentPx(_ed) + '💬 ' + repTE;
var preE = stE.el.querySelector('.webshell-eino-reply-stream-body');
if (!preE) {
preE = document.createElement('pre');
@@ -2599,14 +3010,17 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
preE.style.whiteSpace = 'pre-wrap';
stE.el.appendChild(preE);
}
preE.textContent = fullE;
einoSubReplyStreams.delete(eventData.data.streamId);
if (typeof formatMarkdown === 'function') {
preE.innerHTML = formatMarkdown(fullE);
} else {
preE.textContent = fullE;
}
einoSubReplyStreams.delete(_ed.streamId);
}
if (!streamingTarget) assistantDiv.textContent = '…';
} else if (eventData.type === 'eino_agent_reply' && eventData.message) {
var rd = eventData.data || {};
var replyT = (typeof window.t === 'function') ? window.t('chat.einoAgentReplyTitle') : '子代理回复';
appendTimelineItem('eino_agent_reply', webshellAgentPx(rd) + '💬 ' + replyT, eventData.message, rd);
} else if (_et === 'eino_agent_reply' && _em) {
var replyT = wsTOr('chat.einoAgentReplyTitle', '子代理回复');
appendTimelineItem('eino_agent_reply', webshellAgentPx(_ed) + '💬 ' + replyT, _em, _ed);
if (!streamingTarget) assistantDiv.textContent = '…';
}
} catch (e) { /* ignore parse error */ }
@@ -2615,10 +3029,15 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
return reader.read().then(processChunk);
});
}).catch(function (err) {
renderWebshellAiErrorMessage(assistantDiv, '请求异常: ' + (err && err.message ? err.message : String(err)));
var msg = err && err.message ? err.message : String(err);
var isAbort = /abort/i.test(msg);
if (!isAbort) {
renderWebshellAiErrorMessage(assistantDiv, '请求异常: ' + msg);
}
}).then(function () {
webshellAiSending = false;
if (sendBtn) sendBtn.disabled = false;
webshellAiAbortController = null;
webshellAiStreamReader = null;
wsSetAiSendingState(false);
if (assistantDiv.textContent === '…' && !streamingTarget) {
// 没有任何 response 内容,保持纯文本提示
assistantDiv.textContent = '无回复内容';
+10 -31
View File
@@ -159,7 +159,7 @@
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="mcp">
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')" data-i18n="nav.mcp" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-item-content" data-title="MCP" onclick="window.toggleSubmenu('mcp')" data-i18n="nav.mcp" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
</svg>
@@ -178,7 +178,7 @@
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="knowledge">
<div class="nav-item-content" data-title="知识" onclick="toggleSubmenu('knowledge')" data-i18n="nav.knowledge" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-item-content" data-title="知识" onclick="window.toggleSubmenu('knowledge')" data-i18n="nav.knowledge" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
@@ -198,7 +198,7 @@
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="skills">
<div class="nav-item-content" data-title="Skills" onclick="toggleSubmenu('skills')" data-i18n="nav.skills" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-item-content" data-title="Skills" onclick="window.toggleSubmenu('skills')" data-i18n="nav.skills" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
@@ -221,7 +221,7 @@
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="agents">
<div class="nav-item-content" data-title="Agents" onclick="toggleSubmenu('agents')" data-i18n="nav.agents" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-item-content" data-title="Agents" onclick="window.toggleSubmenu('agents')" data-i18n="nav.agents" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
@@ -239,7 +239,7 @@
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="roles">
<div class="nav-item-content" data-title="角色" onclick="toggleSubmenu('roles')" data-i18n="nav.roles" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-item-content" data-title="角色" onclick="window.toggleSubmenu('roles')" data-i18n="nav.roles" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
@@ -2699,6 +2699,11 @@
<div class="role-tools-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleTools()" data-i18n="roleModal.selectAll">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleTools()" data-i18n="roleModal.deselectAll">全不选</button>
<div id="role-tools-status-filter" class="tools-status-filter">
<button type="button" class="btn-filter active" data-filter="" onclick="filterRoleToolsByStatus('')" data-i18n="roleModal.filterRoleAll">全部</button>
<button type="button" class="btn-filter" data-filter="role_on" onclick="filterRoleToolsByStatus('role_on')" data-i18n="roleModal.filterRoleOn">本角色已开</button>
<button type="button" class="btn-filter" data-filter="role_off" onclick="filterRoleToolsByStatus('role_off')" data-i18n="roleModal.filterRoleOff">本角色已关</button>
</div>
<div class="role-tools-search-box">
<input type="text" id="role-tools-search" data-i18n="roleModal.searchToolsPlaceholder" data-i18n-attr="placeholder" placeholder="搜索工具..."
oninput="searchRoleTools(this.value)"
@@ -2719,32 +2724,6 @@
</div>
<small class="form-hint" data-i18n="roleModal.relatedToolsHint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small>
</div>
<div class="form-group" id="role-skills-section">
<label data-i18n="roleModal.relatedSkills">关联的Skills(可选)</label>
<div class="role-skills-controls">
<div class="role-skills-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleSkills()" data-i18n="roleModal.selectAll">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleSkills()" data-i18n="roleModal.deselectAll">全不选</button>
<div class="role-skills-search-box">
<input type="text" id="role-skills-search" data-i18n="roleModal.searchSkillsPlaceholder" data-i18n-attr="placeholder" placeholder="搜索skill..."
oninput="searchRoleSkills(this.value)"
onkeypress="if(event.key === 'Enter') searchRoleSkills(this.value)" />
<button class="role-skills-search-clear" id="role-skills-search-clear"
onclick="clearRoleSkillsSearch()" style="display: none;" data-i18n="common.clearSearch" data-i18n-attr="title" title="清除搜索">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<div id="role-skills-stats" class="role-skills-stats"></div>
</div>
<div id="role-skills-list" class="role-skills-list">
<div class="skills-loading" data-i18n="roleModal.loadingSkills">正在加载skills列表...</div>
</div>
<small class="form-hint" data-i18n="roleModal.relatedSkillsHint">勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="role-enabled" class="modern-checkbox" checked />