feat: v3.0 Tool Calling — AI 本地文件操作系统
新增文件: - src/main/tool-security.ts: 路径白名单/黑名单、命令安全检查 - src/main/tool-handlers.ts: 7个工具实现(read/write/list/search/create/delete/run) - src/renderer/services/tool-registry.ts: 工具注册调度中心 - src/renderer/services/agent-engine.ts: Agent Loop 流式多轮工具调用引擎 - src/renderer/components/tool-confirm-modal.ts: 高风险操作确认对话框 修改文件: - types.d.ts: 新增 ToolCall/ToolResult/ToolCallRecord 等类型 - ollama.ts: chatStream 支持 tools 参数 - ipc.ts: 新增 tool:execute/getConfig/setAllowedDirs IPC - preload.ts: 暴露 tool API - chat-area.ts: 渲染工具调用卡片 - input-area.ts: 集成 Agent Loop 引擎 - settings-modal.ts: 工具调用开关设置 - index.html: 工具调用设置面板 + 确认对话框 HTML - style.css: 工具调用卡片、确认对话框样式 - main.ts: 初始化工具调用配置
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* ToolConfirmModal - 工具调用确认对话框
|
||||
*/
|
||||
|
||||
import type { ToolCall } from '../types.js';
|
||||
import { getToolIcon, formatToolName } from '../services/tool-registry.js';
|
||||
|
||||
let modalEl: HTMLElement | null = null;
|
||||
let resolveConfirm: ((confirmed: boolean) => void) | null = null;
|
||||
|
||||
export function initToolConfirmModal(): void {
|
||||
modalEl = document.querySelector('#toolConfirmModal')!;
|
||||
if (!modalEl) return;
|
||||
|
||||
modalEl.addEventListener('click', (e) => {
|
||||
if (e.target === modalEl) {
|
||||
cancelConfirm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function showToolConfirm(call: ToolCall): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
if (!modalEl) {
|
||||
modalEl = document.querySelector('#toolConfirmModal')!;
|
||||
if (!modalEl) { resolve(false); return; }
|
||||
modalEl.addEventListener('click', (e) => {
|
||||
if (e.target === modalEl) cancelConfirm();
|
||||
});
|
||||
}
|
||||
|
||||
resolveConfirm = resolve;
|
||||
|
||||
const icon = getToolIcon(call.function.name);
|
||||
const name = formatToolName(call.function.name);
|
||||
|
||||
const titleEl = modalEl.querySelector('#toolConfirmTitle')!;
|
||||
const bodyEl = modalEl.querySelector('#toolConfirmBody')!;
|
||||
const confirmBtn = modalEl.querySelector('#toolConfirmBtn')! as HTMLButtonElement;
|
||||
const cancelBtn = modalEl.querySelector('#toolCancelBtn')! as HTMLButtonElement;
|
||||
|
||||
titleEl.textContent = `${icon} 确认操作:${name}`;
|
||||
|
||||
let bodyHtml = `<div class="tool-confirm-info">`;
|
||||
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">操作:</span><span>${name}</span></div>`;
|
||||
|
||||
const args = call.function.arguments || {};
|
||||
if (call.function.name === 'read_file' || call.function.name === 'write_file' ||
|
||||
call.function.name === 'list_directory' || call.function.name === 'search_files' ||
|
||||
call.function.name === 'create_directory' || call.function.name === 'delete_file') {
|
||||
if (args.path) {
|
||||
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">路径:</span><code>${escapeHtml(String(args.path))}</code></div>`;
|
||||
}
|
||||
}
|
||||
if (call.function.name === 'run_command') {
|
||||
if (args.command) {
|
||||
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">命令:</span><code>${escapeHtml(String(args.command))}</code></div>`;
|
||||
}
|
||||
}
|
||||
if (call.function.name === 'write_file' && args.content) {
|
||||
const preview = String(args.content).slice(0, 500);
|
||||
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">内容预览:</span></div>`;
|
||||
bodyHtml += `<pre class="tool-confirm-preview">${escapeHtml(preview)}${String(args.content).length > 500 ? '\n...' : ''}</pre>`;
|
||||
}
|
||||
|
||||
bodyHtml += `</div>`;
|
||||
bodyEl.innerHTML = bodyHtml;
|
||||
|
||||
modalEl.style.display = '';
|
||||
|
||||
const onConfirm = () => {
|
||||
cleanup();
|
||||
resolveConfirm?.(true);
|
||||
};
|
||||
const onCancel = () => {
|
||||
cleanup();
|
||||
resolveConfirm?.(false);
|
||||
};
|
||||
const cleanup = () => {
|
||||
confirmBtn.removeEventListener('click', onConfirm);
|
||||
cancelBtn.removeEventListener('click', onCancel);
|
||||
if (modalEl) modalEl.style.display = 'none';
|
||||
resolveConfirm = null;
|
||||
};
|
||||
|
||||
confirmBtn.addEventListener('click', onConfirm);
|
||||
cancelBtn.addEventListener('click', onCancel);
|
||||
});
|
||||
}
|
||||
|
||||
function cancelConfirm(): void {
|
||||
if (resolveConfirm) {
|
||||
const r = resolveConfirm;
|
||||
resolveConfirm = null;
|
||||
if (modalEl) modalEl.style.display = 'none';
|
||||
r(false);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
const map: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
||||
return str.replace(/[&<>"']/g, c => map[c]);
|
||||
}
|
||||
Reference in New Issue
Block a user