From c801a97add773f7e8a82eb55d12429e0f0ccf96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 21 Apr 2026 01:24:01 +0800 Subject: [PATCH] Add files via upload --- README.md | 11 +- README_CN.md | 9 +- docs/MULTI_AGENT_EINO.md | 1 + roles/README.md | 5 +- web/static/css/style.css | 170 ++++------ web/static/i18n/en-US.json | 35 +- web/static/i18n/zh-CN.json | 35 +- web/static/js/roles.js | 634 ++++++++++++++++++------------------- web/static/js/router.js | 10 +- web/templates/index.html | 41 +-- 10 files changed, 430 insertions(+), 521 deletions(-) diff --git a/README.md b/README.md index 086baec7..815e63b6 100644 --- a/README.md +++ b/README.md @@ -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 Eino’s 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/` and add standard `SKILL.md` (+ any optional files), or drop in an open-source skill folder as-is. -2. Reference `` in a role’s `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. diff --git a/README_CN.md b/README_CN.md index cbefcc1e..9d88917d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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.md`(及任意可选文件),或直接解压开源技能包到该目录。 -2. 在 `roles/*.yaml` 的 `skills` 列表中引用该 ``。 +2. 启用 **`multi_agent.eino_skills`** 并使用 **多代理** 会话,由模型通过 **`skill`** 工具按包 **name** 加载。 ### 工具编排与扩展 - `tools/*.yaml` 定义命令、参数、提示词与元数据,可热加载。 diff --git a/docs/MULTI_AGENT_EINO.md b/docs/MULTI_AGENT_EINO.md index 1487debb..464ce019 100644 --- a/docs/MULTI_AGENT_EINO.md +++ b/docs/MULTI_AGENT_EINO.md @@ -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` 仅继承 tools;Skills 仅通过 Eino `skill` 工具按需加载。 | diff --git a/roles/README.md b/roles/README.md index 7d0f4dbd..85a1ee0e 100644 --- a/roles/README.md +++ b/roles/README.md @@ -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) ## 示例 diff --git a/web/static/css/style.css b/web/static/css/style.css index e1efd181..f4300451 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -14188,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); @@ -14197,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; } @@ -14448,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; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 05e9dd10..2ba3f1ba 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1784,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 role’s 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:", @@ -1816,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" } } diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 5c2018a3..40831985 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1784,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": "工具:", @@ -1816,6 +1818,11 @@ "prevPage": "上一页", "pageOf": "第 {{page}} / {{total}} 页", "nextPage": "下一页", - "lastPage": "末页" + "lastPage": "末页", + "mcpDisabledBadge": "MCP已关", + "mcpDisabledBadgeTitle": "MCP 管理里该工具为关闭;勾选只表示想关联到本角色,实际调用需先在 MCP 中打开", + "roleFilterOnBanner": "以下为「已勾选、关联到本角色」的工具(与 MCP 管理里全局开/关无关)。", + "roleFilterOffBanner": "以下为「未勾选、未关联到本角色」的工具。", + "checkboxLinkTitle": "勾选表示本角色关联使用该工具" } } diff --git a/web/static/js/roles.js b/web/static/js/roles.js index e316b710..925b6a88 100644 --- a/web/static/js/roles.js +++ b/web/static/js/roles.js @@ -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 = `${badgeText}`; } - + let mcpDisabledBadge = ''; + if (tool.enabled === false) { + mcpDisabledBadge = `${escapeHtml(_t('roleModal.mcpDisabledBadge'))}`; + } // 生成唯一的checkbox id const checkboxId = `role-tool-${escapeHtml(toolKey).replace(/::/g, '--')}`; toolItem.innerHTML = `
${escapeHtml(tool.name)} ${externalBadge} + ${mcpDisabledBadge}
${escapeHtml(tool.description || '无描述')}
@@ -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 = `
${paginationShowText}
+
+ + +
- - + + ${_t('roleModal.pageOf', { page: page, total: totalPages })} - - + +
`; 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 = ` - ✅ ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })} - 📊 ${_t('roleModal.totalSelected', { current: totalEnabled, total: totalTools })} ${_t('roleModal.usingAllEnabledTools')} +
+ ✅ ${_t('roleModal.statsPageLinked', { current: pageChecked, total: pageTotal })} +
+
+ 📊 ${_t('roleModal.statsRoleUsesAll', { mcpOn: mcpOnMax, all: grandAll })} +
+
📋 ${escapeHtml(scopeLine)}
`; 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 + ? `📊 ${_t('roleModal.statsRoleLinked', { current: roleLinked, max: mcpOnMax })}` + : `📊 ${_t('roleModal.statsRoleLinkedNoMax', { current: roleLinked })}`; + statsEl.innerHTML = ` - ✅ ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })} - 📊 ${_t('roleModal.totalSelected', { current: totalSelected, total: totalTools })} +
+ ✅ ${_t('roleModal.statsPageLinked', { current: pageChecked, total: pageTotal })} +
+
${roleRow}
+
📋 ${escapeHtml(scopeLine)}
`; } @@ -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 = '
' + _t('roleModal.loadSkillsFailed') + ': ' + error.message + '
'; - } - } -} - -// 渲染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 = '
' + - (roleSkillsSearchKeyword ? _t('roleModal.noMatchingSkills') : _t('roleModal.noSkillsAvailable')) + - '
'; - updateRoleSkillsStats(); - return; - } - - // 渲染skills列表 - skillsList.innerHTML = filteredSkills.map(skill => { - const isSelected = roleSelectedSkills.has(skill); - return ` -
- -
- `; - }).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; -} diff --git a/web/static/js/router.js b/web/static/js/router.js index 5a91b79b..aa1496d7 100644 --- a/web/static/js/router.js +++ b/web/static/js/router.js @@ -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; }; diff --git a/web/templates/index.html b/web/templates/index.html index e05cb13e..239c292e 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -159,7 +159,7 @@