From d9e9416d3548f939add378f5c0b1e76b28f1faab Mon Sep 17 00:00:00 2001 From: thzxx Date: Tue, 7 Apr 2026 22:24:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E4=B8=8E=20AI=20Tool=20Calling=20=E6=B7=B1=E5=BA=A6=E9=9B=86?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心变更: - 新增 execWorkspaceCommand() 无超时进程执行(workspace.ts) - 新增 workspace:execTool IPC 通道(无超时,5MB 输出上限) - run_command 工具改用 workspace IPC,移除 30s 超时限制 - run_command 默认启用(需用户确认),AI 上下文注入工作空间目录 - Agent Loop 工具执行:run_command 无超时,其他工具保持 30s - Agent 系统提示词新增工作空间规则(userTerminated/truncated 反馈) UI 修复: - Header z-index 提升至 50,Model Bar 提升至 40,Input Area 提升至 30 - 工作空间面板 top 调整为 90px(header + model-bar),不再遮挡菜单 - Input Area 增加 position: relative 确保 z-index 生效 其他: - 新增 docs/DEVELOPMENT.md 开发规范文档 - 设置面板 run_command 开关文案优化,默认值改为 true --- docs/DEVELOPMENT.md | 277 ++++++++++++++++++++++ src/main/ipc.ts | 8 +- src/main/preload.ts | 4 +- src/main/workspace.ts | 134 +++++++++++ src/renderer/components/settings-modal.ts | 5 +- src/renderer/main.ts | 6 +- src/renderer/services/agent-engine.ts | 39 ++- src/renderer/services/tool-registry.ts | 32 ++- src/renderer/styles/style.css | 10 +- src/renderer/types.d.ts | 2 + 10 files changed, 492 insertions(+), 25 deletions(-) create mode 100644 docs/DEVELOPMENT.md diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..0160bac --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,277 @@ +# Metona Ollama Desktop — 开发规范 + +> 版本: 1.0 | 更新: 2026-04-07 | 维护: 项目团队 + +--- + +## 一、项目概述 + +Metona Ollama Desktop 是基于 TypeScript + Electron 的本地 Ollama AI 桌面客户端,面向 Windows 平台。项目采用零外部依赖策略,所有核心功能(Markdown 解析、SHA-256、HTML 净化器等)均内联实现。 + +--- + +## 二、技术栈规范 + +| 层级 | 技术 | 版本约束 | +|------|------|----------| +| 语言 | TypeScript | ≥5.7,严格模式 (`strict: true`) | +| 桌面框架 | Electron | ≥33 | +| 构建工具 | Vite (渲染进程) + tsc (主进程) | Vite ≥5 | +| 数据存储 | IndexedDB | 原生 API,无 ORM | +| 打包 | electron-builder | NSIS 格式 | + +--- + +## 三、目录结构 + +``` +src/ +├── main/ # Electron 主进程 +│ ├── main.ts # 入口、窗口管理 +│ ├── preload.ts # contextBridge API 暴露 +│ ├── ipc.ts # IPC 处理器(invoke/handle + on/send) +│ ├── workspace.ts # 子进程管理、流式输出 +│ ├── tool-handlers.ts # Tool Calling 工具实现 +│ ├── tool-security.ts # 路径/命令安全检查 +│ ├── menu.ts # 原生菜单 +│ ├── tray.ts # 系统托盘 +│ └── utils.ts # 工具函数 +├── renderer/ # 渲染进程 +│ ├── main.ts # 入口、全局初始化 +│ ├── types.d.ts # 完整类型定义 +│ ├── index.html # 入口 HTML +│ ├── api/ # API 封装 +│ ├── components/ # UI 组件 +│ ├── services/ # 核心服务 +│ ├── utils/ # 工具函数 +│ ├── db/ # IndexedDB 封装 +│ ├── state/ # 状态管理 +│ └── styles/ # 样式 +└── ... +``` + +--- + +## 四、代码规范 + +### 4.1 TypeScript 规范 + +- **严格模式**:所有文件必须通过 `strict: true` 编译 +- **类型定义**:所有接口/类型在 `types.d.ts` 中集中定义,禁止 `any` 滥用 +- **命名约定**: + - 文件名:`kebab-case`(如 `tool-registry.ts`) + - 类型/接口:`PascalCase`(如 `ToolCallRecord`) + - 变量/函数:`camelCase`(如 `getEnabledToolDefinitions`) + - 常量:`UPPER_SNAKE_CASE`(如 `MAX_LOOPS`) + - 私有成员:`_` 前缀(如 `_workspaceDir`) + +### 4.2 日志规范 + +**核心原则:项目中严禁使用 `console.log/error/warn/debug`。所有日志必须通过 `log-service.ts` 输出。** + +日志服务提供以下级别: + +```typescript +import { logInfo, logSuccess, logWarn, logError, logDebug } from './services/log-service.js'; + +logInfo('操作描述', '可选详情'); +logSuccess('成功描述', '可选详情'); +logWarn('警告描述', '可选详情'); +logError('错误描述', '错误详情'); +logDebug('调试信息', '可选详情'); +``` + +专用日志函数: + +```typescript +import { + logInit, // 初始化日志 + logSetting, // 设置变更日志 + logToolStart, // 工具开始执行 + logToolResult, // 工具执行结果 + logStream, // 流式输出日志 + logAgentLoop, // Agent Loop 进度 + logModelResponse,// 模型响应日志 + logSession, // 会话操作日志 + logThink // 思考过程日志 +} from './services/log-service.js'; +``` + +**日志详细度要求:** + +- 每个 IPC 调用必须记录发起方、参数摘要、结果 +- 文件操作记录路径(脱敏)、大小、操作类型 +- 工具调用记录工具名、参数摘要、执行时长、结果状态 +- 错误日志必须包含错误消息和上下文信息 +- 进程管理记录进程 ID、命令摘要、退出码 + +**主进程日志**:通过 `mainWindow?.webContents.send('main:log', ...)` 发送到渲染进程日志面板。 + +### 4.3 IPC 规范 + +项目使用两种 IPC 模式: + +| 模式 | 用途 | 超时 | +|------|------|------| +| `invoke/handle` | 请求-响应,同步等待结果 | 有(默认) | +| `on/send` | 单向推送,流式通信 | 无 | + +**选择原则:** + +- 需要返回值 → `invoke/handle` +- 流式输出/实时推送 → `on/send` +- 长时间运行的命令 → `on/send`(避免超时问题) +- Tool Calling 工具执行 → `invoke/handle`(通过 workspace IPC 实现无超时) + +### 4.4 安全规范 + +#### 文件系统安全 + +- 所有文件路径通过 `tool-security.ts` 的 `checkPathAllowed()` 验证 +- 写操作仅允许在用户目录下进行 +- 路径黑名单:`/etc`, `/sys`, `/proc`, `~/.ssh`, `~/.gnupg` 等 + +#### 命令执行安全 + +- 所有命令通过 `tool-security.ts` 的 `checkCommandAllowed()` 验证 +- 命令黑名单:`rm -rf /`, `mkfs`, `dd`, `shutdown`, 反弹 shell 检测等 +- 写操作类工具(`write_file`, `delete_file`, `run_command`, `create_directory`)必须用户确认 + +#### 前端安全 + +- 内置 HTML 净化器(白名单标签 + 属性过滤 + URI 协议检查) +- Markdown 链接仅允许 `http:` / `https:` / `mailto:` / `tel:` +- 阻止 `javascript:` / `vbscript:` / `data:` 协议注入 +- `contextIsolation: true` + IPC 白名单 + +--- + +## 五、架构规范 + +### 5.1 四大子系统 + +``` +① Agent 系统 → agent-engine.ts + tool-registry.ts +② 记忆系统 → memory-manager.ts +③ RAG 知识库 → rag.ts + vector-store.ts + document-processor.ts +④ 工作空间 → workspace.ts (主进程) + workspace-panel.ts (渲染进程) +``` + +### 5.2 Tool Calling 流程 + +``` +用户消息 → Agent Loop → 模型 tool_calls → 工具确认 → 执行 → 结果回传 → 循环 +``` + +- 最大循环次数:10 +- 全局超时:5 分钟 +- run_command:无超时(通过 workspace IPC 执行) +- 其他工具:30 秒超时 + +### 5.3 工作空间与 AI 集成 + +- AI 通过 `run_command` 工具调用工作空间进程管理 +- 执行路径:`tool-registry.ts` → `bridge.workspace.execTool()` → IPC → `workspace.ts execWorkspaceCommand()` +- 无超时限制,进程自然结束后返回完整输出 +- 输出上限:5MB(超出标记 `truncated=true`) +- 用户可随时通过设置禁用 `run_command` + +--- + +## 六、UI/UX 规范 + +### 6.1 设计语言 + +- **风格**:Windows 11 Fluent Design 暗色主题 +- **材质**:Mica 毛玻璃 + Acrylic 亚克力 +- **字体**:Segoe UI Variable (正文) + Cascadia Mono (代码) +- **圆角**:控件 4px,卡片 8px,弹框 12-16px + +### 6.2 Z-Index 层级规范 + +| 层级 | 组件 | 值 | +|------|------|----| +| 最高层 | Toast 通知 | 200 | +| 高层 | 模态框 | 100 | +| 中层 | Header | 50 | +| 中层 | Model Bar | 40 | +| 中层 | Input Area | 30 | +| 低层 | 工作空间面板 | 10 | +| 基础 | 日志面板 | 5 | + +**规则**:新增固定定位元素必须指定 z-index 并在此表中记录。 + +### 6.3 响应式规则 + +- 工作空间面板宽度:300-700px 可调 +- 主内容区开启工作空间时:`margin-right: 面板宽度` +- 模态框宽度:基础 480px,大号 860px + +--- + +## 七、构建与发布 + +### 7.1 构建命令 + +```bash +npm run build # 完整构建 +npm run build:renderer # 仅渲染进程 (Vite) +npm run build:main # 仅主进程 (tsc) +npm start # 构建并运行 +npm run dist # 构建 Windows 安装包 +``` + +### 7.2 发布流程 + +1. 更新 `package.json` 版本号 +2. 更新 `docs/CHANGELOG.md` +3. 更新 `README.md`(如需) +4. 构建并测试:`npm start` +5. 构建安装包:`npm run dist` +6. 推送到 Gitee:`git add -A && git commit && git push origin develop` +7. 创建 Release Tag + +--- + +## 八、Git 规范 + +### 分支模型 + +- `master` — 稳定发布版本 +- `develop` — 开发主分支 +- `feature/*` — 功能开发 +- `fix/*` — Bug 修复 + +### Commit 规范 + +``` +: <简要描述> + +类型 (type): +- feat: 新功能 +- fix: Bug 修复 +- refactor: 重构 +- style: 样式调整 +- docs: 文档更新 +- chore: 构建/工具变更 +- perf: 性能优化 +``` + +示例: +``` +feat: 工作空间与 AI Tool Calling 集成 +fix: 工作空间面板遮挡 Header 的 z-index 问题 +refactor: run_command 改用 workspace IPC 无超时执行 +docs: 新增开发规范文档 +``` + +--- + +## 九、注意事项 + +1. **零外部依赖**:新增功能优先使用内联实现,避免引入 npm 包 +2. **安全优先**:所有文件/命令操作必须经过安全检查层 +3. **日志完整**:关键操作必须记录到执行日志面板 +4. **无超时设计**:工作空间相关操作不设超时,由用户控制生命周期 +5. **用户确认**:写操作类工具必须弹出确认对话框 +6. **桌面优先**:API 调用必须检查 `bridge.isDesktop`,非桌面环境优雅降级 diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 5dfe504..b6ad987 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -22,7 +22,7 @@ import { handleRunCommand } from './tool-handlers.js'; import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js'; -import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js'; +import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir, execWorkspaceCommand } from './workspace.js'; export function setupIPC(): void { ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => { @@ -171,4 +171,10 @@ export function setupIPC(): void { sendLog('info', `🖥️ workspace:kill`, `ID: ${id}`); killProcess(id); }); + + // 无超时工具命令执行(AI Tool Calling 集成) + ipcMain.handle('workspace:execTool', async (_, command: string, cwd?: string) => { + sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`); + return execWorkspaceCommand(command, cwd); + }); } diff --git a/src/main/preload.ts b/src/main/preload.ts index 317e082..b3146ac 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -58,6 +58,8 @@ contextBridge.exposeInMainWorld('metonaDesktop', { }, onExit: (callback: (data: { id: string; code: number | null }) => void) => { ipcRenderer.on('workspace:exit', (_: unknown, data: { id: string; code: number | null }) => callback(data)); - } + }, + /** 无超时命令执行,用于 AI Tool Calling */ + execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd) } }); diff --git a/src/main/workspace.ts b/src/main/workspace.ts index 3139455..f33f262 100644 --- a/src/main/workspace.ts +++ b/src/main/workspace.ts @@ -200,3 +200,137 @@ export function isProcessAlive(id: string): boolean { export function getActiveProcessCount(): number { return activeProcesses.size; } + +/** + * 执行命令并收集完整输出(无超时限制) + * 用于 AI Tool Calling 集成,通过 IPC invoke/handle 调用 + * 返回完整的 stdout + stderr + exitCode + */ +export function execWorkspaceCommand( + command: string, + cwd?: string +): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }> { + return new Promise((resolve) => { + const workDir = cwd ? path.resolve(cwd) : _workspaceDir; + const cmdId = `_tool_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + + // 安全检查 + const cmdCheck = checkCommandAllowed(command); + if (!cmdCheck.ok) { + sendLog('warn', `🔧 工具命令被拦截`, `${command.slice(0, 100)} | ${cmdCheck.reason}`); + resolve({ success: false, stdout: '', stderr: cmdCheck.reason!, exitCode: 1, duration: 0, userTerminated: false, truncated: false }); + return; + } + + const dirCheck = checkPathAllowed(workDir, 'read'); + if (!dirCheck.ok) { + sendLog('warn', `🔧 工具目录被拦截`, `${workDir} | ${dirCheck.reason}`); + resolve({ success: false, stdout: '', stderr: dirCheck.reason!, exitCode: 1, duration: 0, userTerminated: false, truncated: false }); + return; + } + + if (!fs.existsSync(workDir)) { + resolve({ success: false, stdout: '', stderr: `工作目录不存在: ${workDir}`, exitCode: 1, duration: 0, userTerminated: false, truncated: false }); + return; + } + + sendLog('info', `🔧 execWorkspaceCommand`, `${command.slice(0, 200)} | cwd: ${workDir}`); + + const startTime = Date.now(); + const MAX_OUTPUT = 5 * 1024 * 1024; // 5MB 输出上限 + let stdoutBuf = ''; + let stderrBuf = ''; + let truncated = false; + + try { + const isWin = process.platform === 'win32'; + const shell = isWin ? 'cmd.exe' : 'bash'; + const shellArgs = isWin ? ['/c', command] : ['-c', command]; + const proc = spawn(shell, shellArgs, { + cwd: workDir, + env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) }, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + activeProcesses.set(cmdId, proc); + + proc.stdout.on('data', (data: Buffer) => { + if (stdoutBuf.length < MAX_OUTPUT) { + stdoutBuf += data.toString(); + if (stdoutBuf.length > MAX_OUTPUT) { + stdoutBuf = stdoutBuf.slice(0, MAX_OUTPUT); + truncated = true; + } + } else { + truncated = true; + } + }); + + proc.stderr.on('data', (data: Buffer) => { + if (stderrBuf.length < MAX_OUTPUT) { + stderrBuf += data.toString(); + if (stderrBuf.length > MAX_OUTPUT) { + stderrBuf = stderrBuf.slice(0, MAX_OUTPUT); + truncated = true; + } + } else { + truncated = true; + } + }); + + proc.on('close', (code) => { + activeProcesses.delete(cmdId); + const duration = Date.now() - startTime; + sendLog( + code === 0 ? 'success' : 'warn', + `🔧 execWorkspaceCommand 完成`, + `code: ${code} | ${duration}ms | stdout: ${stdoutBuf.length}B | stderr: ${stderrBuf.length}B` + ); + resolve({ + success: code === 0, + stdout: stdoutBuf, + stderr: stderrBuf, + exitCode: code, + duration, + userTerminated: false, + truncated + }); + }); + + proc.on('error', (err) => { + activeProcesses.delete(cmdId); + const duration = Date.now() - startTime; + sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`); + resolve({ + success: false, + stdout: stdoutBuf, + stderr: stderrBuf + (stderrBuf ? '\n' : '') + `进程启动失败: ${err.message}`, + exitCode: 1, + duration, + userTerminated: false, + truncated + }); + }); + } catch (err) { + const duration = Date.now() - startTime; + sendLog('error', `🔧 execWorkspaceCommand 异常`, (err as Error).message); + resolve({ + success: false, + stdout: stdoutBuf, + stderr: (err as Error).message, + exitCode: 1, + duration, + userTerminated: false, + truncated: false + }); + } + }); +} + +/** + * 终止 execWorkspaceCommand 启动的进程 + * @returns 是否成功终止 + */ +export function terminateWorkspaceCommand(commandId: string): boolean { + return killProcess(commandId); +} diff --git a/src/renderer/components/settings-modal.ts b/src/renderer/components/settings-modal.ts index 8f4cb07..56ae6fe 100644 --- a/src/renderer/components/settings-modal.ts +++ b/src/renderer/components/settings-modal.ts @@ -150,9 +150,10 @@ export function initSettingsModal(): void { if (db) await db.saveSetting('runCommandEnabled', toggleRunCommand.checked); setToolEnabled('run_command', toggleRunCommand.checked); if (toggleRunCommand.checked) { - showToast('⚠️ 命令执行已开启,请谨慎使用', 'warning'); - logWarn('命令执行已开启(高风险)'); + showToast('命令执行已开启(每次执行需确认)', 'info'); + logInfo('命令执行已开启'); } else { + showToast('命令执行已关闭', 'info'); logInfo('命令执行已关闭'); } }); diff --git a/src/renderer/main.ts b/src/renderer/main.ts index 580e2a3..a3c57cc 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -191,7 +191,7 @@ async function init(): Promise { // ── 防御性重置:确保首次启动不会遗留脏状态 ── state.set('toolCallingEnabled', false); - state.set('runCommandEnabled', false); + state.set('runCommandEnabled', true); // 默认开启(需确认) state.set('memoryEnabled', true); state.set('thinkEnabled', false); @@ -245,10 +245,10 @@ async function init(): Promise { // ── Tool Calling 设置 ── const toolCallingEnabled = await db.getSetting('toolCallingEnabled', false); - const runCommandEnabled = await db.getSetting('runCommandEnabled', false); + const runCommandEnabled = await db.getSetting('runCommandEnabled', true); // 默认开启(需确认) state.set('toolCallingEnabled', toolCallingEnabled); state.set('runCommandEnabled', runCommandEnabled); - if (runCommandEnabled) setToolEnabled('run_command', true); + if (!runCommandEnabled) setToolEnabled('run_command', false); // 仅在用户明确关闭时禁用 const toggleTC = document.querySelector('#toggleToolCalling'); if (toggleTC) toggleTC.checked = toolCallingEnabled; const toggleRC = document.querySelector('#toggleRunCommand'); diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index 93f0e70..01b7605 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -13,6 +13,7 @@ import { import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js'; import { showToast } from '../components/toast.js'; import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logStream, logAgentLoop, logModelResponse } from './log-service.js'; +import { getWorkspaceDirPath } from '../components/workspace-panel.js'; import type { OllamaMessage, OllamaStreamChunk, @@ -31,7 +32,9 @@ const TOOL_USAGE_GUIDE = `【工具使用规则】 2. 工具调用结果中的 path 字段是权威的文件路径,优先使用它。 3. 如果对话中从未出现过相关路径,应先用 search_files 或 list_directory 定位,不要凭空猜测路径。 4. 对同一文件的连续操作(读取→修改、读取→删除),路径必须一致。 -5. 当用户要求执行命令时,使用 run_command 工具执行。执行完成后,将 stdout/stderr 的结果告知用户。对于需要实时查看进度的长时间运行命令(如 npm install、ollama pull),建议用户在工作空间面板中手动执行。`; +5. 当用户要求执行命令时,使用 run_command 工具执行。run_command 通过工作空间进程管理执行,无超时限制,支持长时间运行的命令。执行完成后,将 stdout/stderr 的结果告知用户。 +6. 如果 run_command 返回 userTerminated=true,说明用户手动终止了命令,请据此告知用户。 +7. 如果返回 truncated=true,说明命令输出超过 5MB 被截断,应告知用户完整输出可在工作空间面板查看。`; export interface AgentCallbacks { onThinking: (text: string) => void; @@ -83,6 +86,15 @@ export async function runAgentLoop( } } + // 注入工作空间上下文 + const workspaceDir = getWorkspaceDirPath(); + if (workspaceDir) { + systemPromptParts.push(`【工作空间】 +当前工作空间目录: ${workspaceDir} +你可以使用 run_command 工具在此目录下执行命令,所有命令通过工作空间进程管理执行,无超时限制。 +文件操作工具(read_file、write_file 等)的相对路径基于此目录解析。`); + } + if (systemPromptParts.length > 0) { messages.push({ role: 'system', content: systemPromptParts.join('\n\n') + '\n\n' + TOOL_USAGE_GUIDE }); } else { @@ -287,16 +299,23 @@ export async function runAgentLoop( } try { - // 工具执行超时保护 + // 工具执行(run_command 无超时,其他工具 30 秒超时) logInfo(`执行工具: ${call.function.name}`, JSON.stringify(call.function.arguments)); - const result = await Promise.race([ - executeTool(call.function.name, call.function.arguments).catch(err => - ({ success: false, error: err?.message || String(err) }) as ToolResult - ), - new Promise((_, reject) => - setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT) - ) - ]); + let result: ToolResult; + if (call.function.name === 'run_command') { + // run_command 通过 workspace IPC 执行,无超时限制 + result = await executeTool(call.function.name, call.function.arguments); + } else { + // 其他工具保持 30 秒超时保护 + result = await Promise.race([ + executeTool(call.function.name, call.function.arguments).catch(err => + ({ success: false, error: err?.message || String(err) }) as ToolResult + ), + new Promise((_, reject) => + setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT) + ) + ]); + } const record: ToolCallRecord = { name: call.function.name, arguments: call.function.arguments, diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index 7e3e350..1058adf 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -108,14 +108,13 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: 'function', function: { name: 'run_command', - description: 'Execute a shell command. DANGEROUS: disabled by default, must be enabled in settings.', + description: 'Execute a shell command via workspace. No timeout limit — runs until the process exits. Requires user confirmation. Output is streamed in real-time through the workspace process.', parameters: { type: 'object', required: ['command'], properties: { command: { type: 'string', description: 'Shell command to execute.' }, - cwd: { type: 'string', description: 'Working directory.' }, - timeout: { type: 'integer', description: 'Timeout ms. Max 120000.' } + cwd: { type: 'string', description: 'Working directory. Defaults to workspace directory.' } } } } @@ -130,7 +129,8 @@ export function needsConfirmation(toolName: string): boolean { let enabledTools: Set = new Set([ 'read_file', 'list_directory', 'search_files', - 'write_file', 'create_directory', 'delete_file' + 'write_file', 'create_directory', 'delete_file', + 'run_command' // 默认启用,需要用户确认 ]); export function setToolEnabled(toolName: string, enabled: boolean): void { @@ -160,7 +160,29 @@ export async function executeTool(toolName: string, args: Record((_, reject) => diff --git a/src/renderer/styles/style.css b/src/renderer/styles/style.css index 5d63af2..a7a1b9b 100644 --- a/src/renderer/styles/style.css +++ b/src/renderer/styles/style.css @@ -199,7 +199,7 @@ html, body { -webkit-backdrop-filter: saturate(180%) blur(20px); border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; - z-index: 10; + z-index: 50; } .header-left { @@ -392,6 +392,8 @@ html, body { background: var(--bg-layer-alt); border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; + z-index: 40; + position: relative; } .model-bar-icon { @@ -1046,7 +1048,8 @@ html, body { backdrop-filter: saturate(180%) blur(20px); -webkit-backdrop-filter: saturate(180%) blur(20px); border-top: 1px solid var(--border-subtle); - z-index: 20; + z-index: 30; + position: relative; pointer-events: auto; } @@ -2664,12 +2667,13 @@ html, body { .workspace-panel { position: fixed; - top: 0; + top: 90px; /* header(44px) + model-bar(46px) */ right: 0; bottom: 0; width: 420px; background: var(--bg-solid, #202020); border-left: 1px solid var(--border-default, rgba(255,255,255,0.08)); + border-top: 1px solid var(--border-default, rgba(255,255,255,0.08)); display: flex; flex-direction: column; z-index: 10; diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 5eaee97..475d749 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -239,6 +239,8 @@ export interface MetonaDesktopAPI { kill: (id: string) => void; onOutput: (callback: (data: { id: string; type: 'stdout' | 'stderr'; data: string }) => void) => void; onExit: (callback: (data: { id: string; code: number | null }) => void) => void; + /** 无超时工具命令执行,用于 AI Tool Calling 集成 */ + execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }>; }; }