mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-21 10:16:32 +02:00
Add files via upload
This commit is contained in:
@@ -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/<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 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.
|
||||
|
||||
+3
-6
@@ -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` 定义命令、参数、提示词与元数据,可热加载。
|
||||
|
||||
@@ -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` 工具按需加载。 |
|
||||
|
||||
+2
-3
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+21
-14
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user