Add files via upload

This commit is contained in:
公明
2026-04-21 01:24:01 +08:00
committed by GitHub
parent 68978b82e9
commit c801a97add
10 changed files with 430 additions and 521 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
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` 工具按需加载。 |
+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)
## 示例
+57 -113
View File
@@ -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;
+21 -14
View File
@@ -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 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:",
@@ -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"
}
}
+21 -14
View File
@@ -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": "勾选表示本角色关联使用该工具"
}
}
+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; };
+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 />