feat: 工具列表独立为工具面板模态框

- Header 新增 🔧 工具按钮,点击打开工具面板
- 新增 tools-modal.ts 组件
- 工具面板以卡片网格展示全部 12 个工具
- 每个工具显示名称、描述、自动/需确认标签
- run_command 开关内嵌在工具卡片中
- 设置面板简化为「查看全部工具」按钮
- 设置面板保留 run_command 开关同步
This commit is contained in:
OpenClaw Agent
2026-04-16 09:42:26 +08:00
parent 4099b5180f
commit 5a3b88f232
4 changed files with 229 additions and 21 deletions
+65
View File
@@ -0,0 +1,65 @@
/**
* ToolsModal - 工具面板模态框
* 展示所有可用工具的详细信息
*/
import { state, KEYS } from '../state/state.js';
import { setToolEnabled } from '../services/tool-registry.js';
import { showToast } from './toast.js';
import { logInfo } from '../services/log-service.js';
import type { ChatDB } from '../types.js';
let modalEl: HTMLElement;
export function initToolsModal(): void {
modalEl = document.querySelector('#toolsModal')!;
// Header 按钮
document.querySelector('#btnTools')!.addEventListener('click', openToolsModal);
document.querySelector('#btnCloseTools')!.addEventListener('click', closeToolsModal);
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) closeToolsModal();
});
// 设置中"查看全部工具"按钮
document.querySelector('#btnOpenToolsFromSettings')?.addEventListener('click', () => {
(document.querySelector('#settingsModal') as HTMLElement).style.display = 'none';
openToolsModal();
});
// run_command 开关(模态框中)
const toggle2 = document.querySelector('#toggleRunCommand2') as HTMLInputElement;
if (toggle2) {
toggle2.addEventListener('change', async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
state.set('runCommandEnabled', toggle2.checked);
if (db) await db.saveSetting('runCommandEnabled', toggle2.checked);
setToolEnabled('run_command', toggle2.checked);
// 同步设置面板的开关
const toggle1 = document.querySelector('#toggleRunCommand') as HTMLInputElement;
if (toggle1) toggle1.checked = toggle2.checked;
if (toggle2.checked) {
showToast('命令执行已开启(每次执行需确认)', 'info');
logInfo('命令执行已开启');
} else {
showToast('命令执行已关闭', 'info');
logInfo('命令执行已关闭');
}
});
}
}
export function openToolsModal(): void {
// 同步 run_command 开关状态
const runCmdEnabled = state.get<boolean>('runCommandEnabled', true);
const toggle2 = document.querySelector('#toggleRunCommand2') as HTMLInputElement;
if (toggle2) toggle2.checked = runCmdEnabled;
modalEl.style.display = '';
}
export function closeToolsModal(): void {
modalEl.style.display = 'none';
}
+83 -16
View File
@@ -52,6 +52,11 @@
<line x1="9" y1="21" x2="15" y2="21"/> <line x1="9" y1="21" x2="15" y2="21"/>
</svg> </svg>
</button> </button>
<button class="icon-btn" id="btnTools" title="工具面板">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>
</button>
<button class="icon-btn" id="btnHistory" title="历史记录"> <button class="icon-btn" id="btnHistory" title="历史记录">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/> <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
@@ -270,20 +275,9 @@
</label> </label>
</label> </label>
<p class="text-muted" style="font-size:11px;margin-bottom:8px;">启用后,AI 可以在对话中主动调用本地工具(读写文件、搜索、创建目录等)。需要模型支持 Tool Calling。</p> <p class="text-muted" style="font-size:11px;margin-bottom:8px;">启用后,AI 可以在对话中主动调用本地工具(读写文件、搜索、创建目录等)。需要模型支持 Tool Calling。</p>
<div class="tool-settings" id="toolSettings"> <div style="display:flex;gap:8px;align-items:center;">
<div class="tool-list"> <button class="btn btn-sm btn-outline" id="btnOpenToolsFromSettings">🔧 查看全部工具(12 个)</button>
<div class="tool-item"><span>📄 读取文件</span><span class="text-muted">自动</span></div> <div class="tool-item" style="flex:1;margin:0;">
<div class="tool-item"><span>✏️ 写入文件</span><span class="text-muted">需确认</span></div>
<div class="tool-item"><span>📁 列出目录</span><span class="text-muted">自动</span></div>
<div class="tool-item"><span>🔍 搜索文件</span><span class="text-muted">自动</span></div>
<div class="tool-item"><span>📂 创建目录</span><span class="text-muted">需确认</span></div>
<div class="tool-item"><span>🗑️ 删除文件</span><span class="text-muted">需确认</span></div>
<div class="tool-item"><span>📦 移动文件</span><span class="text-muted">需确认</span></div>
<div class="tool-item"><span>📋 复制文件</span><span class="text-muted">需确认</span></div>
<div class="tool-item"><span>🌐 网页抓取</span><span class="text-muted">自动</span></div>
<div class="tool-item"><span> 追加文件</span><span class="text-muted">需确认</span></div>
<div class="tool-item"><span>✂️ 编辑文件</span><span class="text-muted">需确认</span></div>
<div class="tool-item">
<span>💻 执行命令</span> <span>💻 执行命令</span>
<label class="toggle-label" style="margin:0;"> <label class="toggle-label" style="margin:0;">
<input type="checkbox" id="toggleRunCommand"> <input type="checkbox" id="toggleRunCommand">
@@ -291,8 +285,7 @@
</label> </label>
</div> </div>
</div> </div>
<p class="text-muted" style="font-size:11px;margin-top:8px;">⚠️ 高风险操作(写入/删除/命令执行)会弹出确认对话框。系统关键路径(/etc, /sys, ~/.ssh 等)已自动屏蔽。</p> <p class="text-muted" style="font-size:11px;margin-top:8px;">⚠️ 高风险操作会弹出确认对话框。系统关键路径(/etc, /sys, ~/.ssh 等)已自动屏蔽。</p>
</div>
</div> </div>
<div class="setting-group"> <div class="setting-group">
<label class="setting-label">🖥️ 工作空间目录</label> <label class="setting-label">🖥️ 工作空间目录</label>
@@ -387,6 +380,80 @@
</div> </div>
</div> </div>
<!-- ═══════════════ 工具面板模态框 ═══════════════ -->
<div class="modal-overlay" id="toolsModal" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3>🔧 工具面板(12 个)</h3>
<button class="icon-btn" id="btnCloseTools">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body">
<p class="text-muted" style="font-size:12px;margin-bottom:12px;">启用工具调用后,AI 可在对话中自主调用以下工具。高风险操作需用户确认。</p>
<div class="tools-grid">
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📄</span><span class="tool-card-name">read_file</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">读取文件内容,支持指定行范围</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">✏️</span><span class="tool-card-name">write_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">写入文件,自动创建父目录</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📁</span><span class="tool-card-name">list_directory</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">列出目录内容,支持递归和隐藏文件</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔍</span><span class="tool-card-name">search_files</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">按文件名或内容搜索文件</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📂</span><span class="tool-card-name">create_directory</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">创建目录,自动创建父级</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🗑️</span><span class="tool-card-name">delete_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">删除文件或目录</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📦</span><span class="tool-card-name">move_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">移动或重命名文件/目录</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📋</span><span class="tool-card-name">copy_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">复制文件或目录到新位置</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🌐</span><span class="tool-card-name">web_fetch</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">抓取网页内容,自动提取文本</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon"></span><span class="tool-card-name">append_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">追加内容到文件末尾</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">✂️</span><span class="tool-card-name">edit_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">文件内查找替换,支持全部替换</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">💻</span><span class="tool-card-name">run_command</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">执行 shell 命令,无超时限制</div>
<div style="margin-top:6px;display:flex;align-items:center;gap:8px;">
<label class="toggle-label" style="margin:0;font-size:11px;">
<input type="checkbox" id="toggleRunCommand2">
<span class="toggle-slider"></span>
<span style="margin-left:4px;">启用命令执行</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════ 图片预览 Lightbox ═══════════════ --> <!-- ═══════════════ 图片预览 Lightbox ═══════════════ -->
<div class="lightbox-overlay" id="lightbox" style="display:none;"> <div class="lightbox-overlay" id="lightbox" style="display:none;">
<button class="lightbox-close" id="lightboxClose"></button> <button class="lightbox-close" id="lightboxClose"></button>
+3
View File
@@ -20,6 +20,7 @@ import { initInputArea } from './components/input-area.js';
import { initSettingsModal, closeSettingsModal } from './components/settings-modal.js'; import { initSettingsModal, closeSettingsModal } from './components/settings-modal.js';
import { initHistoryModal, closeHistoryModal } from './components/history-modal.js'; import { initHistoryModal, closeHistoryModal } from './components/history-modal.js';
import { initMemoryModal } from './components/memory-modal.js'; import { initMemoryModal } from './components/memory-modal.js';
import { initToolsModal, closeToolsModal } from './components/tools-modal.js';
import { initMemoryManager } from './services/memory-manager.js'; import { initMemoryManager } from './services/memory-manager.js';
import { setToolEnabled } from './services/tool-registry.js'; import { setToolEnabled } from './services/tool-registry.js';
import { initToolConfirmModal } from './components/tool-confirm-modal.js'; import { initToolConfirmModal } from './components/tool-confirm-modal.js';
@@ -205,6 +206,7 @@ async function init(): Promise<void> {
initHelpModal(); initHelpModal();
initToolConfirmModal(); initToolConfirmModal();
initMemoryModal(); initMemoryModal();
initToolsModal();
initWorkspacePanel(); initWorkspacePanel();
setupDesktopIntegration(); setupDesktopIntegration();
@@ -286,6 +288,7 @@ function bindGlobalEvents(): void {
closeSettingsModal(); closeSettingsModal();
closeHistoryModal(); closeHistoryModal();
closeLightbox(); closeLightbox();
closeToolsModal();
(document.querySelector('#helpModal') as HTMLElement).style.display = 'none'; (document.querySelector('#helpModal') as HTMLElement).style.display = 'none';
(document.querySelector('#memoryModal') as HTMLElement).style.display = 'none'; (document.querySelector('#memoryModal') as HTMLElement).style.display = 'none';
} }
+73
View File
@@ -2869,3 +2869,76 @@ html, body {
} }
工作空间面板已改为 flex 子元素自然占据宽度 */ 工作空间面板已改为 flex 子元素自然占据宽度 */
/* ═══════════════ 工具面板模态框 ═══════════════ */
#toolsModal .modal {
max-width: 720px;
width: 95vw;
max-height: 85vh;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 8px;
}
.tool-card {
padding: 10px 14px;
background: var(--bg-layer);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-control);
transition: var(--transition);
}
.tool-card:hover {
border-color: var(--border-strong);
background: var(--bg-layer-alt);
}
.tool-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.tool-card-icon {
font-size: 15px;
flex-shrink: 0;
}
.tool-card-name {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
color: var(--accent);
}
.tool-card-badge {
margin-left: auto;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
}
.tool-card-badge.auto {
color: var(--success);
background: var(--success-bg);
border: 1px solid rgba(108, 203, 95, 0.15);
}
.tool-card-badge.confirm {
color: var(--caution);
background: var(--caution-bg);
border: 1px solid rgba(252, 225, 0, 0.15);
}
.tool-card-desc {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}