From 5800aaedd2692fb9f4e57043076822ac5655f204 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 12:40:21 +0800 Subject: [PATCH 01/13] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20BUILD.md?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=AD=A3=E4=BB=93=E5=BA=93=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=20v3.2.3=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BUILD.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/BUILD.md b/docs/BUILD.md index 264c999..bc6f13f 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -12,8 +12,9 @@ ```bash # 1. 克隆并安装依赖 -git clone https://gitee.com/thzxx/metona-ollama.git -cd metona-ollama +git clone https://gitee.com/thzxx/metona-ollama-desktop.git +cd metona-ollama-desktop +git checkout metona-ollama-desktop-v3.2.3 npm config set registry https://registry.npmmirror.com npm install @@ -175,6 +176,13 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi ## 构建日志 +### v3.2.3(2026-04-08) — metona-ollama-desktop 仓库 + +- ✅ 工作空间面板改为常驻显示,固定宽度 480px +- ✅ 修复 tool-registry.ts 缺少 logInfo 导入导致 run_command 异常 +- ✅ Gitee Release v3.2.3-desktop(仓库 thzxx/metona-ollama-desktop) +- ✅ 分支 `metona-ollama-desktop-v3.2.3` 已创建并推送 + ### v3.2.2(2026-04-08) — metona-ollama-desktop 仓库 - ✅ `Metona Ollama Setup 3.2.2.exe`(78MB) From d5f4112d978389f9710a54d700a31788583d36df Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 12:47:28 +0800 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20AI=20Tool=20Calling=20run=5Fcomman?= =?UTF-8?q?d=20=E8=BE=93=E5=87=BA=E5=90=8C=E6=AD=A5=E5=88=B0=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E7=A9=BA=E9=97=B4=E7=BB=88=E7=AB=AF=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleRunCommand 改用 execWorkspaceCommand 替代 Node exec() - 执行完成后通过 workspace:toolOutput IPC 事件推送命令和输出 - 渲染进程监听 onToolOutput,在活跃终端显示 AI 命令及结果 - 清理 tool-handlers.ts 中不再需要的 child_process 导入 --- src/main/preload.ts | 6 ++- src/main/tool-handlers.ts | 46 +++++++++++----------- src/renderer/components/workspace-panel.ts | 40 +++++++++++++++++++ src/renderer/types.d.ts | 2 + 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/main/preload.ts b/src/main/preload.ts index b3146ac..1c6293a 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -60,6 +60,10 @@ contextBridge.exposeInMainWorld('metonaDesktop', { 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) + execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd), + /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ + onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => { + ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => callback(data)); + } } }); diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 05f8be6..5189687 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -5,11 +5,9 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import { exec } from 'child_process'; -import { promisify } from 'util'; import { checkPathAllowed, checkCommandAllowed } from './tool-security.js'; - -const execAsync = promisify(exec); +import { mainWindow } from './main.js'; +import { execWorkspaceCommand } from './workspace.js'; export interface ToolResult { success: boolean; @@ -270,33 +268,33 @@ export async function handleRunCommand(params: { command: string; cwd?: string; const cmdCheck = checkCommandAllowed(params.command); if (!cmdCheck.ok) return { success: false, error: cmdCheck.reason }; - const timeout = Math.min(params.timeout || 30000, 120000); - const cwd = params.cwd ? path.resolve(params.cwd) : process.env.HOME || '/'; + // 通过 workspace 子进程执行,复用安全检查和进程管理 + const result = await execWorkspaceCommand(params.command, params.cwd); - const cwdAllowed = checkPathAllowed(cwd, 'read'); - if (!cwdAllowed.ok) return { success: false, error: cwdAllowed.reason }; - - const start = Date.now(); - const { stdout, stderr } = await execAsync(params.command, { - cwd, - timeout, - maxBuffer: 100 * 1024 - }); + // 将命令和输出同步推送到工作空间终端面板 + if (mainWindow) { + mainWindow.webContents.send('workspace:toolOutput', { + command: params.command, + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode + }); + } return { - success: true, - stdout: stdout.slice(0, 100 * 1024), - stderr: stderr.slice(0, 100 * 1024), - exitCode: 0, - duration: Date.now() - start + success: result.success, + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + duration: result.duration }; } catch (err) { - const error = err as { stdout?: string; stderr?: string; code?: number; message: string }; + const error = err as { message: string }; return { success: false, - stdout: error.stdout?.slice(0, 100 * 1024) || '', - stderr: error.stderr?.slice(0, 100 * 1024) || error.message, - exitCode: error.code || 1, + stdout: '', + stderr: error.message, + exitCode: 1, error: error.message }; } diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index 8d9b9a6..6162681 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -171,6 +171,12 @@ export async function initWorkspacePanel(): Promise { handleProcessExit(data.id, data.code); }); + // 监听 AI Tool Calling 命令输出,同步到终端面板 + bridge.workspace.onToolOutput((data) => { + logDebug('工作空间收到工具输出', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); + appendToolOutput(data.command, data.stdout, data.stderr, data.exitCode); + }); + // 绑定 UI 事件 bindEvents(); @@ -293,6 +299,40 @@ function executeCommand(command: string): void { bridge.workspace.exec({ id: session.id, command, cwd }); } +/** AI Tool Calling 命令输出 → 追加到活跃终端 */ +function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void { + const session = getActiveSession(); + if (!session) return; + + // 显示执行的命令 + session.lines.push({ type: 'system', text: `$ ${command} (AI)` }); + + // 显示 stdout + if (stdout) { + for (const line of stdout.split('\n')) { + if (line) session.lines.push({ type: 'stdout', text: line }); + } + } + + // 显示 stderr + if (stderr) { + for (const line of stderr.split('\n')) { + if (line) session.lines.push({ type: 'stderr', text: line }); + } + } + + // 显示退出状态 + const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`; + session.lines.push({ type: 'system', text: exitMsg }); + + // 限制最大行数 + if (session.lines.length > 3000) { + session.lines = session.lines.slice(-2000); + } + + renderTerminal(); +} + function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void { const session = terminalSessions.find(s => s.id === sessionId); if (!session) { diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 475d749..2bf89ec 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -241,6 +241,8 @@ export interface MetonaDesktopAPI { 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 }>; + /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ + onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => void; }; } From d150b531169b5332890f3c986c3e212addd49f59 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 12:57:12 +0800 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20=E5=B7=A5=E4=BD=9C=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E5=8E=BB=E6=8E=89=E6=89=8B=E5=8A=A8=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=BE=93=E5=85=A5=EF=BC=8C=E7=BB=88=E6=AD=A2=E6=97=B6=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8F=8D=E9=A6=88=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除工作空间终端的命令输入框和执行按钮 - 用户只能通过终止按钮中断卡住的 AI 命令 - 终止后自动通过 killTool IPC 终止进程,结果自动回传 AI - 新增 workspace:killTool IPC 和 terminateCurrentToolCommand - 新增终端底部状态提示(等待/执行中) - 修复工作空间面板顶部对齐(90px → 92px) --- src/main/ipc.ts | 9 +- src/main/preload.ts | 2 + src/main/workspace.ts | 17 +++ src/renderer/components/workspace-panel.ts | 125 +++++++++------------ src/renderer/index.html | 41 ++++--- src/renderer/styles/style.css | 44 +++++--- src/renderer/types.d.ts | 2 + 7 files changed, 130 insertions(+), 110 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b6ad987..f4eba75 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, execWorkspaceCommand } from './workspace.js'; +import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir, execWorkspaceCommand, terminateCurrentToolCommand } from './workspace.js'; export function setupIPC(): void { ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => { @@ -177,4 +177,11 @@ export function setupIPC(): void { sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`); return execWorkspaceCommand(command, cwd); }); + + // 终止当前 AI 工具命令 + ipcMain.handle('workspace:killTool', async () => { + const killed = terminateCurrentToolCommand(); + sendLog('info', `🔧 workspace:killTool`, killed ? '已终止' : '无正在运行的工具命令'); + return { killed }; + }); } diff --git a/src/main/preload.ts b/src/main/preload.ts index 1c6293a..67fc4bd 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -61,6 +61,8 @@ contextBridge.exposeInMainWorld('metonaDesktop', { }, /** 无超时命令执行,用于 AI Tool Calling */ execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd), + /** 终止当前 AI 工具命令 */ + killTool: () => ipcRenderer.invoke('workspace:killTool'), /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => { ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => callback(data)); diff --git a/src/main/workspace.ts b/src/main/workspace.ts index f33f262..3578964 100644 --- a/src/main/workspace.ts +++ b/src/main/workspace.ts @@ -20,6 +20,9 @@ export const DEFAULT_WORKSPACE_DIR = path.join(app.getPath('userData'), 'workspa /** 活跃进程 Map */ const activeProcesses = new Map(); +/** 当前 AI 工具命令进程 ID(用于用户手动终止) */ +let _currentToolProcessId: string | null = null; + /** 确保工作空间目录存在 */ export function ensureWorkspaceDir(): void { if (!fs.existsSync(DEFAULT_WORKSPACE_DIR)) { @@ -253,6 +256,7 @@ export function execWorkspaceCommand( }); activeProcesses.set(cmdId, proc); + _currentToolProcessId = cmdId; proc.stdout.on('data', (data: Buffer) => { if (stdoutBuf.length < MAX_OUTPUT) { @@ -280,6 +284,7 @@ export function execWorkspaceCommand( proc.on('close', (code) => { activeProcesses.delete(cmdId); + if (_currentToolProcessId === cmdId) _currentToolProcessId = null; const duration = Date.now() - startTime; sendLog( code === 0 ? 'success' : 'warn', @@ -299,6 +304,7 @@ export function execWorkspaceCommand( proc.on('error', (err) => { activeProcesses.delete(cmdId); + if (_currentToolProcessId === cmdId) _currentToolProcessId = null; const duration = Date.now() - startTime; sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`); resolve({ @@ -334,3 +340,14 @@ export function execWorkspaceCommand( export function terminateWorkspaceCommand(commandId: string): boolean { return killProcess(commandId); } + +/** + * 终止当前正在运行的 AI 工具命令 + * @returns 是否成功终止 + */ +export function terminateCurrentToolCommand(): boolean { + if (!_currentToolProcessId) return false; + const id = _currentToolProcessId; + _currentToolProcessId = null; + return killProcess(id); +} diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index 6162681..697a3d9 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -39,6 +39,17 @@ let fileTree: FileNode[] = []; let previewFile: { name: string; content: string; path: string } | null = null; let _counter = 0; +/** 当前正在运行的 AI 命令(用于用户手动终止时通知 AI) */ +let currentAiCommand: string | null = null; + +/** 终止通知回调(由外部设置) */ +let onToolTerminated: ((command: string) => void) | null = null; + +/** 设置终止通知回调 */ +export function setOnToolTerminated(cb: (command: string) => void): void { + onToolTerminated = cb; +} + function genId(): string { return `ws_${Date.now()}_${++_counter}`; } @@ -191,28 +202,6 @@ function bindEvents(): void { document.querySelector('#wsTabTerminal')?.addEventListener('click', () => switchTab('terminal')); document.querySelector('#wsTabFiles')?.addEventListener('click', () => switchTab('files')); - // 终端输入 - const termInput = document.querySelector('#wsTermInput') as HTMLInputElement; - termInput?.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const cmd = termInput.value.trim(); - if (cmd) { - executeCommand(cmd); - termInput.value = ''; - } - } - }); - - // 执行按钮 - document.querySelector('#wsExecBtn')?.addEventListener('click', () => { - const cmd = termInput?.value.trim(); - if (cmd) { - executeCommand(cmd); - termInput.value = ''; - } - }); - // 停止按钮 document.querySelector('#wsStopBtn')?.addEventListener('click', killCurrentProcess); @@ -274,36 +263,16 @@ function getActiveSession(): TerminalSession | undefined { return terminalSessions.find(s => s.id === activeTerminalId); } -function executeCommand(command: string): void { - const session = getActiveSession(); - if (!session) { - logError('工作空间执行失败', '无活跃终端会话'); - return; - } - - const bridge = window.metonaDesktop; - if (!bridge?.isDesktop) { - logError('工作空间执行失败', '非桌面环境'); - return; - } - - // 显示命令 - session.lines.push({ type: 'system', text: `$ ${command}` }); - session.running = true; - session.autoScroll = true; - - renderTerminal(); - - const cwd = currentFileDir || workspaceDir; - logInfo('工作空间执行命令', `ID: ${session.id} | ${command.slice(0, 100)} | cwd: ${cwd}`); - bridge.workspace.exec({ id: session.id, command, cwd }); -} - /** AI Tool Calling 命令输出 → 追加到活跃终端 */ function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void { const session = getActiveSession(); if (!session) return; + // 记录当前 AI 命令 + currentAiCommand = command; + session.running = true; + updateHint(); + // 显示执行的命令 session.lines.push({ type: 'system', text: `$ ${command} (AI)` }); @@ -325,12 +294,18 @@ function appendToolOutput(command: string, stdout: string, stderr: string, exitC const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`; session.lines.push({ type: 'system', text: exitMsg }); + // 命令结束 + session.running = false; + currentAiCommand = null; + // 限制最大行数 if (session.lines.length > 3000) { session.lines = session.lines.slice(-2000); } renderTerminal(); + updateStopBtnState(); + updateHint(); } function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void { @@ -389,14 +364,32 @@ function handleProcessExit(sessionId: string, code: number | null): void { function killCurrentProcess(): void { const session = getActiveSession(); - if (!session || !session.running) return; + if (!session) return; const bridge = window.metonaDesktop; if (!bridge?.isDesktop) return; - bridge.workspace.kill(session.id); - session.lines.push({ type: 'system', text: '■ 已发送终止信号' }); + // 终止用户终端进程(如果有) + if (session.running) { + bridge.workspace.kill(session.id); + } + + // 终止当前 AI 工具命令 + bridge.workspace.killTool(); + + const cmd = currentAiCommand; + session.lines.push({ type: 'system', text: '■ 命令已手动终止' }); + session.running = false; + currentAiCommand = null; + renderTerminal(); + updateStopBtnState(); + updateHint(); + + // 通知 AI 命令被用户终止 + if (cmd && onToolTerminated) { + onToolTerminated(cmd); + } } function clearTerminal(): void { @@ -649,12 +642,23 @@ function renderPreview(): void { function updateStopBtnState(): void { const stopBtn = document.querySelector('#wsStopBtn') as HTMLButtonElement; if (!stopBtn) return; - const session = getActiveSession(); - const running = session?.running || false; + const running = currentAiCommand !== null; stopBtn.disabled = !running; stopBtn.classList.toggle('active', running); } +function updateHint(): void { + const hint = document.querySelector('#wsTermHint') as HTMLElement; + if (!hint) return; + if (currentAiCommand) { + hint.textContent = `⏳ AI 正在执行: ${currentAiCommand.slice(0, 40)}${currentAiCommand.length > 40 ? '...' : ''}`; + hint.classList.add('running'); + } else { + hint.textContent = '等待 AI 执行命令...'; + hint.classList.remove('running'); + } +} + // ── 工具函数 ── function getFileIcon(name: string): string { const ext = name.split('.').pop()?.toLowerCase() || ''; @@ -676,23 +680,6 @@ function formatSize(bytes: number): string { // ── 外部调用接口 ── -/** 从聊天区域调用:在工作空间执行命令 */ -export function execInWorkspace(command: string): void { - // 切换到终端 tab - activeTab = 'terminal'; - - // 使用当前活跃终端或创建新的 - const session = getActiveSession(); - if (session?.running) { - // 当前终端有进程在跑,新建一个 - const num = terminalSessions.length + 1; - createTerminalSession(`终端 ${num}`); - } - - renderPanel(); - executeCommand(command); -} - /** 获取工作空间目录 */ export function getWorkspaceDirPath(): string { return workspaceDir; diff --git a/src/renderer/index.html b/src/renderer/index.html index 95c689a..57c41b8 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -182,28 +182,25 @@
- - - - - + 等待 AI 执行命令... +
+ + + +
diff --git a/src/renderer/styles/style.css b/src/renderer/styles/style.css index 11ec21a..4a9368c 100644 --- a/src/renderer/styles/style.css +++ b/src/renderer/styles/style.css @@ -2667,7 +2667,7 @@ html, body { .workspace-panel { position: fixed; - top: 90px; /* header(44px) + model-bar(46px) */ + top: 92px; /* header(44px) + border(1px) + model-bar(46px) + border(1px) */ right: 0; bottom: 0; width: 480px; @@ -2838,31 +2838,39 @@ html, body { .ws-term-toolbar { display: flex; align-items: center; + justify-content: space-between; gap: 4px; padding: 6px 8px; border-top: 1px solid var(--border-default, rgba(255,255,255,0.08)); background: var(--bg-layer-alt, #2a2a2a); } -.ws-term-input { +.ws-term-hint { flex: 1; - background: var(--bg-layer, #383838); - border: 1px solid var(--border-default, rgba(255,255,255,0.08)); - border-radius: 4px; - color: var(--text-primary, #fff); - padding: 6px 10px; - font-size: 12px; - font-family: 'Cascadia Code', 'Consolas', 'Courier New', monospace; - outline: none; - transition: border-color 0.15s; -} - -.ws-term-input:focus { - border-color: var(--accent, #60CDFF); -} - -.ws-term-input::placeholder { + font-size: 11px; color: var(--text-tertiary, #666); + font-family: 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 4px; +} + +.ws-term-hint.running { + color: var(--accent, #60CDFF); + animation: hint-pulse 1.5s ease-in-out infinite; +} + +@keyframes hint-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +.ws-term-toolbar-btns { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; } .ws-toolbar-btn { diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 2bf89ec..d2a1926 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -243,6 +243,8 @@ export interface MetonaDesktopAPI { execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }>; /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => void; + /** 终止当前 AI 工具命令 */ + killTool: () => Promise<{ killed: boolean }>; }; } From d5e5f5f58a8c0b3288917b105c9dc4f158da4ec8 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 13:06:57 +0800 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20AI=20=E5=91=BD=E4=BB=A4=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=AE=9E=E6=97=B6=E6=B5=81=E5=BC=8F=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E5=88=B0=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4=E7=BB=88=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - execWorkspaceCommand 新增 onOutput 回调,stdout/stderr 实时推送 - ipc.ts workspace:execTool 实时转发输出 + 发送 toolDone 完成通知 - 渲染进程 appendToolStreamOutput 流式显示,handleToolDone 显示退出状态 - 去掉 tool-handlers.ts 中的一次性完整输出推送 - 完整流程:用户确认 → 实时输出 → 完成通知 → AI 回复结果 --- src/main/ipc.ts | 12 +++- src/main/preload.ts | 10 +++- src/main/tool-handlers.ts | 11 ---- src/main/workspace.ts | 13 +++- src/renderer/components/workspace-panel.ts | 69 ++++++++++++---------- src/renderer/types.d.ts | 6 +- 6 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f4eba75..b53293d 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -173,9 +173,17 @@ export function setupIPC(): void { }); // 无超时工具命令执行(AI Tool Calling 集成) - ipcMain.handle('workspace:execTool', async (_, command: string, cwd?: string) => { + ipcMain.handle('workspace:execTool', async (event, command: string, cwd?: string) => { sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`); - return execWorkspaceCommand(command, cwd); + + const result = await execWorkspaceCommand(command, cwd, (type, data) => { + // 实时推送到工作空间终端 + event.sender.send('workspace:toolOutput', { command, type, data }); + }); + + // 通知命令执行完毕 + event.sender.send('workspace:toolDone', { command, exitCode: result.exitCode }); + return result; }); // 终止当前 AI 工具命令 diff --git a/src/main/preload.ts b/src/main/preload.ts index 67fc4bd..b4186fe 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -63,9 +63,13 @@ contextBridge.exposeInMainWorld('metonaDesktop', { execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd), /** 终止当前 AI 工具命令 */ killTool: () => ipcRenderer.invoke('workspace:killTool'), - /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ - onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => { - ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => callback(data)); + /** AI Tool Calling 实时输出推送到工作空间终端 */ + onToolOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => { + ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; type: 'stdout' | 'stderr'; data: string }) => callback(data)); + }, + /** AI Tool Calling 命令执行完毕 */ + onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => { + ipcRenderer.on('workspace:toolDone', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data)); } } }); diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 5189687..30a308b 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -6,7 +6,6 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { checkPathAllowed, checkCommandAllowed } from './tool-security.js'; -import { mainWindow } from './main.js'; import { execWorkspaceCommand } from './workspace.js'; export interface ToolResult { @@ -271,16 +270,6 @@ export async function handleRunCommand(params: { command: string; cwd?: string; // 通过 workspace 子进程执行,复用安全检查和进程管理 const result = await execWorkspaceCommand(params.command, params.cwd); - // 将命令和输出同步推送到工作空间终端面板 - if (mainWindow) { - mainWindow.webContents.send('workspace:toolOutput', { - command: params.command, - stdout: result.stdout, - stderr: result.stderr, - exitCode: result.exitCode - }); - } - return { success: result.success, stdout: result.stdout, diff --git a/src/main/workspace.ts b/src/main/workspace.ts index 3578964..3fd171b 100644 --- a/src/main/workspace.ts +++ b/src/main/workspace.ts @@ -211,7 +211,8 @@ export function getActiveProcessCount(): number { */ export function execWorkspaceCommand( command: string, - cwd?: string + cwd?: string, + onOutput?: (type: 'stdout' | 'stderr', data: string) => void ): 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; @@ -259,8 +260,9 @@ export function execWorkspaceCommand( _currentToolProcessId = cmdId; proc.stdout.on('data', (data: Buffer) => { + const str = data.toString(); if (stdoutBuf.length < MAX_OUTPUT) { - stdoutBuf += data.toString(); + stdoutBuf += str; if (stdoutBuf.length > MAX_OUTPUT) { stdoutBuf = stdoutBuf.slice(0, MAX_OUTPUT); truncated = true; @@ -268,11 +270,14 @@ export function execWorkspaceCommand( } else { truncated = true; } + // 实时推送到渲染进程 + onOutput?.('stdout', str); }); proc.stderr.on('data', (data: Buffer) => { + const str = data.toString(); if (stderrBuf.length < MAX_OUTPUT) { - stderrBuf += data.toString(); + stderrBuf += str; if (stderrBuf.length > MAX_OUTPUT) { stderrBuf = stderrBuf.slice(0, MAX_OUTPUT); truncated = true; @@ -280,6 +285,8 @@ export function execWorkspaceCommand( } else { truncated = true; } + // 实时推送到渲染进程 + onOutput?.('stderr', str); }); proc.on('close', (code) => { diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index 697a3d9..7a8ca34 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -182,10 +182,15 @@ export async function initWorkspacePanel(): Promise { handleProcessExit(data.id, data.code); }); - // 监听 AI Tool Calling 命令输出,同步到终端面板 + // 监听 AI Tool Calling 实时输出,流式推送到终端面板 bridge.workspace.onToolOutput((data) => { - logDebug('工作空间收到工具输出', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); - appendToolOutput(data.command, data.stdout, data.stderr, data.exitCode); + appendToolStreamOutput(data.command, data.type, data.data); + }); + + // 监听 AI Tool Calling 命令执行完毕 + bridge.workspace.onToolDone((data) => { + logDebug('工作空间工具命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); + handleToolDone(data.command, data.exitCode); }); // 绑定 UI 事件 @@ -263,46 +268,50 @@ function getActiveSession(): TerminalSession | undefined { return terminalSessions.find(s => s.id === activeTerminalId); } -/** AI Tool Calling 命令输出 → 追加到活跃终端 */ -function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void { +/** AI Tool Calling 实时输出 → 流式追加到活跃终端 */ +function appendToolStreamOutput(command: string, type: 'stdout' | 'stderr', data: string): void { const session = getActiveSession(); if (!session) return; - // 记录当前 AI 命令 - currentAiCommand = command; - session.running = true; - updateHint(); - - // 显示执行的命令 - session.lines.push({ type: 'system', text: `$ ${command} (AI)` }); - - // 显示 stdout - if (stdout) { - for (const line of stdout.split('\n')) { - if (line) session.lines.push({ type: 'stdout', text: line }); - } + // 首次输出时显示命令头 + if (!currentAiCommand) { + currentAiCommand = command; + session.running = true; + session.lines.push({ type: 'system', text: `$ ${command} (AI)` }); + updateHint(); + updateStopBtnState(); } - // 显示 stderr - if (stderr) { - for (const line of stderr.split('\n')) { - if (line) session.lines.push({ type: 'stderr', text: line }); + // 流式追加输出(按行分割,保留最后一行用于后续追加) + const lines = data.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (i < lines.length - 1 || line) { + const cleanLine = line.replace(/\r/g, ''); + if (cleanLine) session.lines.push({ type, text: cleanLine }); } } - // 显示退出状态 - const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`; - session.lines.push({ type: 'system', text: exitMsg }); - - // 命令结束 - session.running = false; - currentAiCommand = null; - // 限制最大行数 if (session.lines.length > 3000) { session.lines = session.lines.slice(-2000); } + renderTerminal(); +} + +/** AI Tool Calling 命令执行完毕 */ +function handleToolDone(command: string, exitCode: number | null): void { + const session = getActiveSession(); + if (!session) return; + + // 显示退出状态 + const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`; + session.lines.push({ type: 'system', text: exitMsg }); + + session.running = false; + currentAiCommand = null; + renderTerminal(); updateStopBtnState(); updateHint(); diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index d2a1926..9caf7d2 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -241,8 +241,10 @@ export interface MetonaDesktopAPI { 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 }>; - /** AI Tool Calling 命令输出 → 同步到工作空间终端 */ - onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => void; + /** AI Tool Calling 实时输出推送到工作空间终端 */ + onToolOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => void; + /** AI Tool Calling 命令执行完毕 */ + onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => void; /** 终止当前 AI 工具命令 */ killTool: () => Promise<{ killed: boolean }>; }; From 0431ed56c04c942e6d4afc096db1ef738e854752 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 13:12:18 +0800 Subject: [PATCH 05/13] =?UTF-8?q?refactor:=20=E5=91=BD=E4=BB=A4=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E8=B5=B0=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=EF=BC=8CAI=20=E4=B8=8D=E5=86=8D=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 流程改为:AI 提出命令 → 用户确认 → 工作空间终端执行(实时显示)→ 结果反馈 AI → AI 回复 - handleRunCommand 直接 spawn 子进程,stdout/stderr 实时通过 cmd:output 推送 - 进程结束后通过 cmd:done 通知渲染进程 - 停止按钮通过 cmd:kill 终止进程 - 清理 workspace.ts 中不再需要的 execWorkspaceCommand 工具集成代码 - 去掉 workspace:execTool/killTool,改为 cmd:output/cmd:done/cmd:kill --- src/main/ipc.ts | 25 ++---- src/main/preload.ts | 22 +++--- src/main/tool-handlers.ts | 91 +++++++++++++++++----- src/main/workspace.ts | 17 ---- src/renderer/components/workspace-panel.ts | 14 ++-- src/renderer/services/tool-registry.ts | 8 +- src/renderer/types.d.ts | 14 ++-- 7 files changed, 107 insertions(+), 84 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b53293d..a42c95c 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -19,10 +19,11 @@ import { handleSearchFiles, handleCreateDir, handleDeleteFile, - handleRunCommand + handleRunCommand, + killToolProcess } from './tool-handlers.js'; import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js'; -import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir, execWorkspaceCommand, terminateCurrentToolCommand } from './workspace.js'; +import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js'; export function setupIPC(): void { ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => { @@ -172,24 +173,10 @@ export function setupIPC(): void { killProcess(id); }); - // 无超时工具命令执行(AI Tool Calling 集成) - ipcMain.handle('workspace:execTool', async (event, command: string, cwd?: string) => { - sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`); - - const result = await execWorkspaceCommand(command, cwd, (type, data) => { - // 实时推送到工作空间终端 - event.sender.send('workspace:toolOutput', { command, type, data }); - }); - - // 通知命令执行完毕 - event.sender.send('workspace:toolDone', { command, exitCode: result.exitCode }); - return result; - }); - // 终止当前 AI 工具命令 - ipcMain.handle('workspace:killTool', async () => { - const killed = terminateCurrentToolCommand(); - sendLog('info', `🔧 workspace:killTool`, killed ? '已终止' : '无正在运行的工具命令'); + ipcMain.handle('cmd:kill', async () => { + const killed = killToolProcess(); + sendLog('info', `🔧 cmd:kill`, killed ? '已终止' : '无正在运行的工具命令'); return { killed }; }); } diff --git a/src/main/preload.ts b/src/main/preload.ts index b4186fe..0ba6b14 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -59,17 +59,17 @@ 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), - /** 终止当前 AI 工具命令 */ - killTool: () => ipcRenderer.invoke('workspace:killTool'), - /** AI Tool Calling 实时输出推送到工作空间终端 */ - onToolOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => { - ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; type: 'stdout' | 'stderr'; data: string }) => callback(data)); + /** 无超时工具命令执行,用于 AI Tool Calling */ + execTool: (command: string, cwd?: string) => ipcRenderer.invoke('tool:execute', 'run_command', { command, cwd }), + /** AI 命令实时输出推送到工作空间终端 */ + onCmdOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => { + ipcRenderer.on('cmd:output', (_: unknown, data: { command: string; type: 'stdout' | 'stderr'; data: string }) => callback(data)); }, - /** AI Tool Calling 命令执行完毕 */ - onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => { - ipcRenderer.on('workspace:toolDone', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data)); - } + /** AI 命令执行完毕 */ + onCmdDone: (callback: (data: { command: string; exitCode: number | null }) => void) => { + ipcRenderer.on('cmd:done', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data)); + }, + /** 终止当前 AI 命令 */ + cmdKill: () => ipcRenderer.invoke('cmd:kill') } }); diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 30a308b..dbbd55d 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -5,8 +5,10 @@ import * as fs from 'fs/promises'; import * as path from 'path'; +import { spawn } from 'child_process'; import { checkPathAllowed, checkCommandAllowed } from './tool-security.js'; -import { execWorkspaceCommand } from './workspace.js'; +import { mainWindow } from './main.js'; +import { getWorkspaceDir } from './workspace.js'; export interface ToolResult { success: boolean; @@ -262,29 +264,82 @@ export async function handleDeleteFile(params: { path: string; recursive?: boole } } +/** 当前工具命令进程(用于用户手动终止) */ +let _toolProc: ReturnType | null = null; + export async function handleRunCommand(params: { command: string; cwd?: string; timeout?: number }): Promise { try { const cmdCheck = checkCommandAllowed(params.command); if (!cmdCheck.ok) return { success: false, error: cmdCheck.reason }; - // 通过 workspace 子进程执行,复用安全检查和进程管理 - const result = await execWorkspaceCommand(params.command, params.cwd); + const cwd = params.cwd ? path.resolve(params.cwd) : getWorkspaceDir(); + const dirCheck = checkPathAllowed(cwd, 'read'); + if (!dirCheck.ok) return { success: false, error: dirCheck.reason }; - return { - success: result.success, - stdout: result.stdout, - stderr: result.stderr, - exitCode: result.exitCode, - duration: result.duration - }; + return new Promise((resolve) => { + const isWin = process.platform === 'win32'; + const shell = isWin ? 'cmd.exe' : 'bash'; + const shellArgs = isWin ? ['/c', params.command] : ['-c', params.command]; + const startTime = Date.now(); + + _toolProc = spawn(shell, shellArgs, { + cwd, + env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }) }, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + // 实时推送到工作空间终端 + _toolProc.stdout?.on('data', (data: Buffer) => { + const str = data.toString(); + stdout += str; + mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stdout', data: str }); + }); + + _toolProc.stderr?.on('data', (data: Buffer) => { + const str = data.toString(); + stderr += str; + mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stderr', data: str }); + }); + + _toolProc.on('close', (code) => { + _toolProc = null; + const duration = Date.now() - startTime; + mainWindow?.webContents.send('cmd:done', { command: params.command, exitCode: code }); + resolve({ + success: code === 0, + stdout, + stderr, + exitCode: code, + duration + }); + }); + + _toolProc.on('error', (err) => { + _toolProc = null; + const duration = Date.now() - startTime; + mainWindow?.webContents.send('cmd:done', { command: params.command, exitCode: 1 }); + resolve({ + success: false, + stdout, + stderr: stderr + (stderr ? '\n' : '') + err.message, + exitCode: 1, + error: err.message, + duration + }); + }); + }); } catch (err) { - const error = err as { message: string }; - return { - success: false, - stdout: '', - stderr: error.message, - exitCode: 1, - error: error.message - }; + return { success: false, stdout: '', stderr: (err as Error).message, exitCode: 1, error: (err as Error).message }; } } + +/** 终止当前工具命令进程 */ +export function killToolProcess(): boolean { + if (!_toolProc) return false; + try { _toolProc.kill('SIGTERM'); } catch { /* ignore */ } + _toolProc = null; + return true; +} diff --git a/src/main/workspace.ts b/src/main/workspace.ts index 3fd171b..c8c11eb 100644 --- a/src/main/workspace.ts +++ b/src/main/workspace.ts @@ -20,9 +20,6 @@ export const DEFAULT_WORKSPACE_DIR = path.join(app.getPath('userData'), 'workspa /** 活跃进程 Map */ const activeProcesses = new Map(); -/** 当前 AI 工具命令进程 ID(用于用户手动终止) */ -let _currentToolProcessId: string | null = null; - /** 确保工作空间目录存在 */ export function ensureWorkspaceDir(): void { if (!fs.existsSync(DEFAULT_WORKSPACE_DIR)) { @@ -257,7 +254,6 @@ export function execWorkspaceCommand( }); activeProcesses.set(cmdId, proc); - _currentToolProcessId = cmdId; proc.stdout.on('data', (data: Buffer) => { const str = data.toString(); @@ -291,7 +287,6 @@ export function execWorkspaceCommand( proc.on('close', (code) => { activeProcesses.delete(cmdId); - if (_currentToolProcessId === cmdId) _currentToolProcessId = null; const duration = Date.now() - startTime; sendLog( code === 0 ? 'success' : 'warn', @@ -311,7 +306,6 @@ export function execWorkspaceCommand( proc.on('error', (err) => { activeProcesses.delete(cmdId); - if (_currentToolProcessId === cmdId) _currentToolProcessId = null; const duration = Date.now() - startTime; sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`); resolve({ @@ -347,14 +341,3 @@ export function execWorkspaceCommand( export function terminateWorkspaceCommand(commandId: string): boolean { return killProcess(commandId); } - -/** - * 终止当前正在运行的 AI 工具命令 - * @returns 是否成功终止 - */ -export function terminateCurrentToolCommand(): boolean { - if (!_currentToolProcessId) return false; - const id = _currentToolProcessId; - _currentToolProcessId = null; - return killProcess(id); -} diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts index 7a8ca34..d69dc1b 100644 --- a/src/renderer/components/workspace-panel.ts +++ b/src/renderer/components/workspace-panel.ts @@ -182,14 +182,14 @@ export async function initWorkspacePanel(): Promise { handleProcessExit(data.id, data.code); }); - // 监听 AI Tool Calling 实时输出,流式推送到终端面板 - bridge.workspace.onToolOutput((data) => { + // 监听 AI 命令实时输出,流式推送到终端面板 + bridge.workspace.onCmdOutput((data) => { appendToolStreamOutput(data.command, data.type, data.data); }); - // 监听 AI Tool Calling 命令执行完毕 - bridge.workspace.onToolDone((data) => { - logDebug('工作空间工具命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); + // 监听 AI 命令执行完毕 + bridge.workspace.onCmdDone((data) => { + logDebug('工作空间 AI 命令完成', `command: ${data.command.slice(0, 50)} | exitCode: ${data.exitCode}`); handleToolDone(data.command, data.exitCode); }); @@ -383,8 +383,8 @@ function killCurrentProcess(): void { bridge.workspace.kill(session.id); } - // 终止当前 AI 工具命令 - bridge.workspace.killTool(); + // 终止当前 AI 命令 + bridge.workspace.cmdKill(); const cmd = currentAiCommand; session.lines.push({ type: 'system', text: '■ 命令已手动终止' }); diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index b66c2fe..4213c5e 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -161,14 +161,14 @@ export async function executeTool(toolName: string, args: Record 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 }>; - /** AI Tool Calling 实时输出推送到工作空间终端 */ - onToolOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => void; - /** AI Tool Calling 命令执行完毕 */ - onToolDone: (callback: (data: { command: string; exitCode: number | null }) => void) => void; - /** 终止当前 AI 工具命令 */ - killTool: () => Promise<{ killed: boolean }>; + execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number }>; + /** AI 命令实时输出推送到工作空间终端 */ + onCmdOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => void; + /** AI 命令执行完毕 */ + onCmdDone: (callback: (data: { command: string; exitCode: number | null }) => void) => void; + /** 终止当前 AI 命令 */ + cmdKill: () => Promise<{ killed: boolean }>; }; } From 83fe7ba4ceadd2bd4f8534fe5b08b76f35bacf93 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 13:16:34 +0800 Subject: [PATCH 06/13] =?UTF-8?q?fix:=20=E6=B8=85=E7=90=86=20TOOL=5FUSAGE?= =?UTF-8?q?=5FGUIDE=20=E4=B8=AD=E5=BC=95=E7=94=A8=E5=B7=B2=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=AD=97=E6=AE=B5=E7=9A=84=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/services/agent-engine.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index 01b7605..2030887 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -32,9 +32,7 @@ const TOOL_USAGE_GUIDE = `【工具使用规则】 2. 工具调用结果中的 path 字段是权威的文件路径,优先使用它。 3. 如果对话中从未出现过相关路径,应先用 search_files 或 list_directory 定位,不要凭空猜测路径。 4. 对同一文件的连续操作(读取→修改、读取→删除),路径必须一致。 -5. 当用户要求执行命令时,使用 run_command 工具执行。run_command 通过工作空间进程管理执行,无超时限制,支持长时间运行的命令。执行完成后,将 stdout/stderr 的结果告知用户。 -6. 如果 run_command 返回 userTerminated=true,说明用户手动终止了命令,请据此告知用户。 -7. 如果返回 truncated=true,说明命令输出超过 5MB 被截断,应告知用户完整输出可在工作空间面板查看。`; +5. 当用户要求执行命令时,使用 run_command 工具执行。命令通过工作空间终端实时执行并显示,执行完成后将 stdout/stderr 结果告知用户。`; export interface AgentCallbacks { onThinking: (text: string) => void; From 01b1b0e9c5a92f916fa7d78de66acb78f9cb1ca7 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 13:22:39 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20=E8=AE=BE=E7=BD=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E4=BF=AE=E6=94=B9=E5=9C=B0=E5=9D=80=E5=90=8E=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=9B=B4=E6=96=B0=E9=A1=B6=E9=83=A8=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/components/settings-modal.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/settings-modal.ts b/src/renderer/components/settings-modal.ts index 56ae6fe..f2c2a22 100644 --- a/src/renderer/components/settings-modal.ts +++ b/src/renderer/components/settings-modal.ts @@ -8,7 +8,7 @@ import { encryptData, decryptData } from '../services/crypto.js'; import { setMemoryEnabled } from '../services/memory-manager.js'; import { setToolEnabled } from '../services/tool-registry.js'; import { logSetting, logInfo, logWarn, logError, logSuccess } from '../services/log-service.js'; -import { updateConnectionInfo, updateRunningModels } from './header.js'; +import { checkConnection, updateConnectionInfo, updateRunningModels } from './header.js'; import { loadModels } from './model-bar.js'; import { showToast } from './toast.js'; import { showConfirm } from './prompt-modal.js'; @@ -35,6 +35,7 @@ export function initSettingsModal(): void { state.set(KEYS.API, api); if (db) await db.saveSetting('serverUrl', url); updateConnectionInfo(); + checkConnection(); loadModels(); }, 500); document.querySelector('#inputServerUrl')!.addEventListener('input', saveServerUrl); From f0bad05494e69ca24248411c2728f5ed95a6a473 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 8 Apr 2026 13:26:14 +0800 Subject: [PATCH 08/13] =?UTF-8?q?chore:=20=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=87=B3=203.2.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - package.json version 3.2.4 - README.md 版本徽章、下载说明、构建分支 - docs/CHANGELOG.md 新增 v3.2.4 版本说明,版本链补全 v3.2.3 - docs/BUILD.md 新增 v3.2.4 构建日志,构建分支引用 - src/renderer/index.html 应用标题版本号 --- README.md | 6 +++--- docs/BUILD.md | 8 +++++++- docs/CHANGELOG.md | 19 ++++++++++++++++++- package.json | 2 +- src/renderer/index.html | 2 +- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9472f94..df3f1a4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > 💻 桌面版仓库 | 🌐 [Web 版历史存档](https://gitee.com/thzxx/metona-ollama-web) -![版本](https://img.shields.io/badge/version-3.2.3--desktop-brightgreen) +![版本](https://img.shields.io/badge/version-3.2.4--desktop-brightgreen) ![平台](https://img.shields.io/badge/platform-Windows%20x64-blue) ![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6) ![Electron](https://img.shields.io/badge/Electron-33-47848f) @@ -190,7 +190,7 @@ v3.2 四大子系统协调运作: | 文件 | 类型 | |------|------| -| `Metona Ollama Setup 3.2.3.exe` | NSIS 安装包(可选目录、创建快捷方式) | +| `Metona Ollama Setup 3.2.4.exe` | NSIS 安装包(可选目录、创建快捷方式) | > ⚠️ v3.2.0 起不再提供便携版,仅保留 NSIS 安装包。 > @@ -210,7 +210,7 @@ v3.2 四大子系统协调运作: ```bash git clone https://gitee.com/thzxx/metona-ollama-desktop.git cd metona-ollama-desktop -git checkout metona-ollama-desktop-v3.2.3 +git checkout metona-ollama-desktop-v3.2.4 # 安装依赖(国内使用 npmmirror 加速) npm config set registry https://registry.npmmirror.com diff --git a/docs/BUILD.md b/docs/BUILD.md index bc6f13f..b55d852 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -14,7 +14,7 @@ # 1. 克隆并安装依赖 git clone https://gitee.com/thzxx/metona-ollama-desktop.git cd metona-ollama-desktop -git checkout metona-ollama-desktop-v3.2.3 +git checkout metona-ollama-desktop-v3.2.4 npm config set registry https://registry.npmmirror.com npm install @@ -176,6 +176,12 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi ## 构建日志 +### v3.2.4(2026-04-08) — metona-ollama-desktop 仓库 + +- 🔧 AI 命令执行走工作空间终端,实时流式显示 +- 🔧 设置面板修改地址后同步更新顶部导航连接状态 +- ✅ 分支 `metona-ollama-desktop-v3.2.4` 已创建并推送 + ### v3.2.3(2026-04-08) — metona-ollama-desktop 仓库 - ✅ 工作空间面板改为常驻显示,固定宽度 480px diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2353412..ec57d07 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,7 +3,7 @@ ## 分支结构 **桌面版主线**(`master`): -- `metona-ollama-desktop-v1` → `metona-ollama-desktop-v1.1` → `metona-ollama-desktop-v2` → `metona-ollama-desktop-v3` → `metona-ollama-desktop-v3.2.0` → `metona-ollama-desktop-v3.2.1` → `metona-ollama-desktop-v3.2.2` → `metona-ollama-desktop-v3.2.3`(当前) +- `metona-ollama-desktop-v1` → `metona-ollama-desktop-v1.1` → `metona-ollama-desktop-v2` → `metona-ollama-desktop-v3` → `metona-ollama-desktop-v3.2.0` → `metona-ollama-desktop-v3.2.1` → `metona-ollama-desktop-v3.2.2` → `metona-ollama-desktop-v3.2.3` → `metona-ollama-desktop-v3.2.4`(当前) **Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。 @@ -11,6 +11,23 @@ ## 桌面版(Desktop) +### Desktop v3.2.4 — AI 命令执行走工作空间终端 & Bug 修复 + +- 🔧 **AI 命令执行走工作空间终端** + - `run_command` 工具不再在主进程内部执行,改为通过工作空间终端实时执行并显示 + - stdout/stderr 实时推送到工作空间终端面板,流式展示 + - 用户可通过停止按钮终止卡住的命令,结果自动回传 AI + - 新增 `cmd:output` / `cmd:done` / `cmd:kill` IPC 通道 +- 🖥️ **工作空间面板优化** + - 去掉用户手动命令输入,所有命令由 AI 执行 + - 终端底部状态提示(等待/执行中) + - 修复面板顶部对齐(90px → 92px) +- 🔧 **设置面板修复** + - 修改 Ollama 服务地址后同步更新顶部导航连接状态和模型列表 +- 📦 **构建产物** + - `Metona Ollama Setup 3.2.4.exe` + - Gitee Release v3.2.4-desktop + ### Desktop v3.2.3 — 工作空间面板常驻显示 & Bug 修复 - 🖥️ **工作空间面板改为常驻显示** diff --git a/package.json b/package.json index f9a3a57..a864ec8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metona-ollama-desktop", - "version": "3.2.3", + "version": "3.2.4", "description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端", "main": "dist/main/main.js", "author": "thzxx", diff --git a/src/renderer/index.html b/src/renderer/index.html index 57c41b8..ecd85f3 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -27,7 +27,7 @@
Metona Ollama - v3.2.2 + v3.2.4