feat: 工作空间与 AI Tool Calling 深度集成
核心变更: - 新增 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
This commit is contained in:
@@ -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('命令执行已关闭');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,7 +191,7 @@ async function init(): Promise<void> {
|
||||
|
||||
// ── 防御性重置:确保首次启动不会遗留脏状态 ──
|
||||
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<void> {
|
||||
|
||||
// ── 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<HTMLInputElement>('#toggleToolCalling');
|
||||
if (toggleTC) toggleTC.checked = toolCallingEnabled;
|
||||
const toggleRC = document.querySelector<HTMLInputElement>('#toggleRunCommand');
|
||||
|
||||
@@ -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<ToolResult>((_, 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<ToolResult>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
|
||||
)
|
||||
]);
|
||||
}
|
||||
const record: ToolCallRecord = {
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
|
||||
@@ -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<string> = 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<string, unknown
|
||||
|
||||
try {
|
||||
logToolStart(toolName, JSON.stringify(args));
|
||||
// IPC 调用 + 25 秒硬超时(比 agent-engine 的 30 秒先触发)
|
||||
|
||||
// run_command 走 workspace 无超时 IPC,其他工具走标准 IPC
|
||||
if (toolName === 'run_command') {
|
||||
const command = args.command as string;
|
||||
const cwd = args.cwd as string | undefined;
|
||||
if (!command) {
|
||||
return { success: false, error: '缺少 command 参数' };
|
||||
}
|
||||
logInfo(`Workspace 命令执行: ${command.slice(0, 100)}`);
|
||||
const result = await bridge.workspace.execTool(command, cwd);
|
||||
logToolResult('run_command', result.success, result.success ? undefined : result.stderr?.slice(0, 200));
|
||||
return {
|
||||
success: result.success,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
exitCode: result.exitCode,
|
||||
duration: result.duration,
|
||||
userTerminated: result.userTerminated,
|
||||
truncated: result.truncated
|
||||
};
|
||||
}
|
||||
|
||||
// 其他工具:IPC 调用 + 25 秒硬超时
|
||||
const result = await Promise.race([
|
||||
bridge.tool.execute(toolName, args),
|
||||
new Promise<ToolResult>((_, reject) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+2
@@ -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 }>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user