diff --git a/CLAUDE.md b/CLAUDE.md index 905e05f29..43ac55499 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ DeepChat is a feature-rich open-source AI chat platform built with Electron + Vu ## Development Commands ### Package Management + Use `pnpm` as the package manager (required Node.js >= 20.12.2, pnpm >= 10.11.0): ```bash @@ -20,6 +21,7 @@ pnpm run installRuntime ``` ### Development + ```bash # Start development server pnpm run dev @@ -32,6 +34,7 @@ pnpm run dev:linux ``` ### Code Quality + ```bash # Lint with OxLint pnpm run lint @@ -47,6 +50,7 @@ pnpm run typecheck:web # Renderer process ``` ### Testing + ```bash # Run all tests pnpm run test @@ -66,6 +70,7 @@ pnpm run test:renderer # Renderer process tests ``` ### Building + ```bash # Build for development preview pnpm run build @@ -85,6 +90,7 @@ pnpm run build:linux:arm64 ``` ### Internationalization + ```bash # Check i18n completeness (Chinese as source) pnpm run i18n @@ -96,6 +102,7 @@ pnpm run i18n:en ## Architecture Overview ### Multi-Process Architecture + - **Main Process**: Core business logic, system integration, window management - **Renderer Process**: UI components, user interactions, frontend state management - **Preload Scripts**: Secure IPC bridge between main and renderer processes @@ -103,7 +110,9 @@ pnpm run i18n:en ### Key Architectural Patterns #### Presenter Pattern + Each functional domain has a dedicated Presenter class in `src/main/presenter/`: + - **WindowPresenter**: BrowserWindow lifecycle management - **TabPresenter**: WebContentsView management with cross-window tab dragging - **ThreadPresenter**: Conversation session management and LLM coordination @@ -112,16 +121,19 @@ Each functional domain has a dedicated Presenter class in `src/main/presenter/`: - **LLMProviderPresenter**: LLM provider abstraction with Agent Loop architecture #### Multi-Window Multi-Tab Architecture + - **Window Shell** (`src/renderer/shell/`): Lightweight tab bar UI management - **Tab Content** (`src/renderer/src/`): Complete application functionality - **Independent Vue Instances**: Separation of concerns for better performance #### Event-Driven Communication + - **EventBus** (`src/main/eventbus.ts`): Decoupled inter-process communication - **Standard Event Patterns**: Consistent naming and responsibility separation - **IPC Integration**: EventBus bridges main process events to renderer via IPC ### LLM Provider Architecture + The LLM system follows a two-layer architecture: 1. **Agent Loop Layer** (`llmProviderPresenter/index.ts`): @@ -136,6 +148,7 @@ The LLM system follows a two-layer architecture: - Supports both native and prompt-wrapped tool calling ### MCP Integration + - **Server Management**: Lifecycle management of MCP servers - **Tool Execution**: Seamless integration with LLM providers - **Format Conversion**: Bridges MCP tools with various LLM provider formats @@ -144,16 +157,19 @@ The LLM system follows a two-layer architecture: ## Code Structure ### Main Process (`src/main/`) + - `presenter/`: Core business logic organized by functional domain - `eventbus.ts`: Central event coordination system - `index.ts`: Application entry point and lifecycle management ### Renderer Process (`src/renderer/`) + - `src/`: Main application UI (Vue 3 + Composition API) - `shell/`: Tab management UI shell - `floating/`: Floating button interface ### Shared Code (`src/shared/`) + - Type definitions shared between main and renderer processes - Common utilities and constants - IPC contract definitions @@ -161,6 +177,7 @@ The LLM system follows a two-layer architecture: ## Development Guidelines ### Code Standards + - **Language**: Use English for logs and comments - **TypeScript**: Strict type checking enabled - **Vue 3**: Use Composition API for all components @@ -168,16 +185,19 @@ The LLM system follows a two-layer architecture: - **Styling**: Tailwind CSS with scoped styles ### IPC Communication + - **Renderer to Main**: Use `usePresenter.ts` composable for direct presenter method calls - **Main to Renderer**: Use EventBus to broadcast events via `mainWindow.webContents.send()` - **Security**: Context isolation enabled with preload scripts ### Testing + - **Framework**: Vitest for unit and integration tests - **Test Files**: Place in `test/` directory with corresponding structure - **Coverage**: Run tests with coverage reporting ### File Organization + - **Presenters**: One presenter per functional domain - **Components**: Organize by feature in `src/renderer/src/` - **Types**: Shared types in `src/shared/` @@ -186,23 +206,27 @@ The LLM system follows a two-layer architecture: ## Common Development Tasks ### Adding New LLM Provider + 1. Create provider file in `src/main/presenter/llmProviderPresenter/providers/` 2. Implement `coreStream` method following standardized event interface 3. Add provider configuration in `configPresenter/providers.ts` 4. Update UI in renderer provider settings ### Adding New MCP Tool + 1. Implement tool in `src/main/presenter/mcpPresenter/inMemoryServers/` 2. Register in `mcpPresenter/index.ts` 3. Add tool configuration UI if needed ### Creating New UI Components + 1. Follow existing component patterns in `src/renderer/src/` 2. Use Composition API with proper TypeScript typing 3. Implement responsive design with Tailwind CSS 4. Add proper error handling and loading states ### Debugging + - **Main Process**: Use VSCode debugger with breakpoints - **Renderer Process**: Chrome DevTools (F12) - **MCP Tools**: Built-in MCP debugging window @@ -211,26 +235,31 @@ The LLM system follows a two-layer architecture: ## Key Dependencies ### Core Framework + - **Electron**: Desktop application framework - **Vue 3**: Progressive web framework - **TypeScript**: Type-safe JavaScript - **Vite**: Fast build tool via electron-vite ### State & Routing + - **Pinia**: Vue state management - **Vue Router**: SPA routing ### UI & Styling + - **Tailwind CSS**: Utility-first CSS - **Radix Vue**: Accessible UI components - **Monaco Editor**: Code editor integration ### LLM Integration + - **Multiple SDK**: OpenAI, Anthropic, Google AI, etc. - **Ollama**: Local model support - **MCP SDK**: Model Context Protocol support ### Development Tools + - **OxLint**: Fast linting - **Prettier**: Code formatting - **Vitest**: Testing framework @@ -255,13 +284,20 @@ The LLM system follows a two-layer architecture: ## Platform-Specific Notes ### Windows + - Enable Developer Mode or use admin account for symlink creation - Install Visual Studio Build Tools for native dependencies ### macOS + - Code signing configuration in `scripts/notarize.js` - Platform-specific build configurations ### Linux + - AppImage and deb package support -- Sandbox considerations for development \ No newline at end of file +- Sandbox considerations for development + +## Git Commit + +- Do not include author information other than human authors in the Commit, such as Co-Authored-By related information diff --git a/docs/mcp-permission-implementation-guide.md b/docs/mcp-permission-implementation-guide.md new file mode 100644 index 000000000..981a89759 --- /dev/null +++ b/docs/mcp-permission-implementation-guide.md @@ -0,0 +1,549 @@ +# MCP Permission System Implementation Guide + +## Overview + +This document provides step-by-step implementation instructions for the MCP Tool Permission Request System in DeepChat. + +## Prerequisites + +- Understanding of DeepChat's architecture (LLMProviderPresenter, ThreadPresenter, ToolManager) +- Familiarity with Vue 3 Composition API +- Knowledge of TypeScript and event-driven programming + +## Implementation Steps + +### Step 1: Update Type Definitions + +#### 1.1 Add Permission Block Type + +In `src/shared/chat.d.ts`, add the new permission block type: + +```typescript +export type AssistantMessageBlock = { + type: + | 'content' + | 'search' + | 'reasoning_content' + | 'error' + | 'tool_call' + | 'action' + | 'tool_call_permission' // NEW: Permission request block + | 'image' + | 'artifact-thinking' + // ... existing fields +} +``` + +#### 1.2 Update Tool Response Types + +In `src/shared/presenter.d.ts`, add permission-related types: + +```typescript +export interface MCPToolResponse { + // ... existing fields + requiresPermission?: boolean + permissionRequest?: { + toolName: string + serverName: string + permissionType: 'read' | 'write' | 'all' + description: string + } +} +``` + +### Step 2: Modify ToolManager + +#### 2.1 Update Permission Check Logic + +In `src/main/presenter/mcpPresenter/toolManager.ts`: + +```typescript +async callTool(toolCall: MCPToolCall): Promise { + // ... existing code for tool resolution and argument parsing + + // Check permissions + const hasPermission = this.checkToolPermission(originalName, toolServerName, autoApprove) + + if (!hasPermission) { + const permissionType = this.determinePermissionType(originalName) + + return { + toolCallId: toolCall.id, + content: `Permission required: The '${originalName}' operation requires ${permissionType} permissions.`, + isError: false, + requiresPermission: true, + permissionRequest: { + toolName: originalName, + serverName: toolServerName, + permissionType, + description: `Allow ${originalName} to perform ${permissionType} operations on ${toolServerName}?` + } + } + } + + // ... existing tool execution code +} +``` + +#### 2.2 Add Permission Management Methods + +```typescript +export class ToolManager { + // ... existing code + + async grantPermission(serverName: string, permissionType: 'read' | 'write' | 'all', remember: boolean = false): Promise { + if (remember) { + await this.updateServerPermissions(serverName, permissionType) + } + } + + private async updateServerPermissions(serverName: string, permissionType: 'read' | 'write' | 'all'): Promise { + const servers = await this.configPresenter.getMcpServers() + const serverConfig = servers[serverName] + + if (serverConfig) { + const autoApprove = [...(serverConfig.autoApprove || [])] + + if (permissionType === 'all') { + autoApprove.length = 0 // Clear existing permissions + autoApprove.push('all') + } else if (!autoApprove.includes(permissionType) && !autoApprove.includes('all')) { + autoApprove.push(permissionType) + } + + await this.configPresenter.updateMcpServer(serverName, { + ...serverConfig, + autoApprove + }) + } + } +} +``` + +### Step 3: Update LLMProviderPresenter Agent Loop + +#### 3.1 Handle Permission Requests + +In `src/main/presenter/llmProviderPresenter/index.ts`, modify the tool execution section: + +```typescript +// Inside the agent loop, in tool execution section +try { + const toolResponse = await presenter.mcpPresenter.callTool(mcpToolInput) + + if (abortController.signal.aborted) break + + // Check if permission is required + if (toolResponse.requiresPermission) { + // Create permission request block + yield { + type: 'response', + data: { + eventId, + tool_call: 'permission-required', + tool_call_id: toolCall.id, + tool_call_name: toolCall.name, + tool_call_params: toolCall.arguments, + tool_call_server_name: toolResponse.permissionRequest?.serverName, + tool_call_server_icons: toolDef.server.icons, + tool_call_server_description: toolDef.server.description, + tool_call_response: toolResponse.content, + permission_request: toolResponse.permissionRequest + } + } + + // Pause the agent loop + needContinueConversation = false + break + } + + // ... continue with normal tool response handling +} catch (toolError) { + // ... existing error handling +} +``` + +### Step 4: Update ThreadPresenter + +#### 4.1 Handle Permission Blocks + +In `src/main/presenter/threadPresenter/index.ts`, add permission block handling: + +```typescript +// In handleLLMAgentResponse method +if (tool_call === 'permission-required') { + finalizeLastBlock() + + const { permission_request } = msg + + state.message.content.push({ + type: 'tool_call_permission', + content: tool_call_response || 'Permission required for this operation', + status: 'pending', + timestamp: currentTime, + tool_call: { + id: tool_call_id, + name: tool_call_name, + params: tool_call_params || '', + server_name: tool_call_server_name, + server_icons: tool_call_server_icons, + server_description: tool_call_server_description + }, + extra: { + permissionType: permission_request?.permissionType || 'write', + serverName: permission_request?.serverName || tool_call_server_name, + toolName: permission_request?.toolName || tool_call_name, + needsUserAction: true + } + }) +} +``` + +#### 4.2 Add Permission Response Handler + +```typescript +export class ThreadPresenter implements IThreadPresenter { + // ... existing code + + async handlePermissionResponse( + messageId: string, + toolCallId: string, + granted: boolean, + permissionType: 'read' | 'write' | 'all', + remember: boolean = false + ): Promise { + const state = this.generatingMessages.get(messageId) + if (!state) { + throw new Error('Message not found or not in generating state') + } + + // Update the permission block + const permissionBlock = state.message.content.find( + block => block.type === 'tool_call_permission' && + block.tool_call?.id === toolCallId + ) + + if (permissionBlock) { + permissionBlock.status = granted ? 'granted' : 'denied' + if (permissionBlock.extra) { + permissionBlock.extra.needsUserAction = false + if (granted) { + permissionBlock.extra.grantedPermissions = [permissionType] + } + } + + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + } + + if (granted) { + // Grant permission in ToolManager + const serverName = permissionBlock?.extra?.serverName as string + if (serverName) { + await presenter.mcpPresenter.grantPermission(serverName, permissionType, remember) + } + + // Continue the agent loop + await this.continueWithPermission(messageId, toolCallId) + } + } + + private async continueWithPermission(messageId: string, toolCallId: string): Promise { + // Resume the agent loop for this specific tool call + // This involves re-executing the tool and continuing the conversation + const state = this.generatingMessages.get(messageId) + if (!state) return + + // Find the tool call that needs to be re-executed + const permissionBlock = state.message.content.find( + block => block.type === 'tool_call_permission' && + block.tool_call?.id === toolCallId + ) + + if (!permissionBlock?.tool_call) return + + // Re-execute the tool call + try { + const mcpToolInput: MCPToolCall = { + id: permissionBlock.tool_call.id!, + type: 'function', + function: { + name: permissionBlock.tool_call.name!, + arguments: permissionBlock.tool_call.params! + }, + server: { + name: permissionBlock.tool_call.server_name!, + icons: permissionBlock.tool_call.server_icons!, + description: permissionBlock.tool_call.server_description! + } + } + + const toolResponse = await presenter.mcpPresenter.callTool(mcpToolInput) + + // Send tool result and continue conversation + eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { + eventId: messageId, + tool_call: 'end', + tool_call_id: toolCallId, + tool_call_name: permissionBlock.tool_call.name, + tool_call_response: typeof toolResponse.content === 'string' + ? toolResponse.content + : JSON.stringify(toolResponse.content), + tool_call_response_raw: toolResponse + }) + + // Resume agent loop by restarting stream completion + await this.resumeStreamCompletion(state.conversationId, messageId) + + } catch (error) { + console.error('Failed to continue with permission:', error) + } + } +} +``` + +### Step 5: Create Frontend Permission Component + +#### 5.1 Create Permission Request Component + +Create `src/renderer/src/components/message/MessageBlockPermissionRequest.vue`: + +```vue + + + + + +``` + +#### 5.2 Register Component in MessageItemAssistant + +In `src/renderer/src/components/message/MessageItemAssistant.vue`: + +```vue + + + +``` + +### Step 6: Add Presenter Interface + +#### 6.1 Update ThreadPresenter Interface + +In `src/shared/presenter.d.ts`: + +```typescript +export interface IThreadPresenter { + // ... existing methods + + handlePermissionResponse( + messageId: string, + toolCallId: string, + granted: boolean, + permissionType: 'read' | 'write' | 'all', + remember?: boolean + ): Promise +} +``` + +#### 6.2 Update MCPPresenter Interface + +```typescript +export interface IMcpPresenter { + // ... existing methods + + grantPermission( + serverName: string, + permissionType: 'read' | 'write' | 'all', + remember?: boolean + ): Promise +} +``` + +## Testing the Implementation + +### Test Cases + +1. **Permission Request Flow** + - Trigger a tool call that requires permissions + - Verify permission block is created + - Test grant and deny actions + - Verify conversation continues after grant + +2. **Permission Persistence** + - Grant permission with "remember" checked + - Verify subsequent calls don't require permission + - Test permission management in settings + +3. **Error Handling** + - Test permission denial + - Test network errors during permission check + - Test malformed permission requests + +### Manual Testing + +1. Set up an MCP server without auto-approve permissions +2. Start a conversation that requires tool usage +3. Verify permission request UI appears +4. Test granting and denying permissions +5. Verify conversation flow continues correctly + +## Rollout Plan + +1. **Phase 1**: Implement core backend logic (Steps 1-4) +2. **Phase 2**: Implement frontend UI (Step 5) +3. **Phase 3**: Add presenter interfaces (Step 6) +4. **Phase 4**: Testing and refinement +5. **Phase 5**: Documentation and deployment + +This implementation provides a robust, user-friendly permission system that integrates seamlessly with DeepChat's existing architecture while maintaining security and usability. \ No newline at end of file diff --git a/docs/mcp-permission-system-design.md b/docs/mcp-permission-system-design.md new file mode 100644 index 000000000..2f3e06cff --- /dev/null +++ b/docs/mcp-permission-system-design.md @@ -0,0 +1,215 @@ +# MCP Tool Permission Request System Design + +## Overview + +This document describes the redesigned permission request system for MCP (Model Context Protocol) tool calls in DeepChat. The system provides a user-friendly way to request and manage permissions for tool executions while maintaining the conversational flow. + +## Goals + +1. **Non-intrusive Flow**: Permission requests should pause the agent loop naturally, similar to maximum tool calls behavior +2. **Clear User Interface**: Users should clearly understand what permissions are being requested and why +3. **Persistent Choices**: Users can choose to remember their permission decisions +4. **Seamless Continuation**: After permission is granted, the agent loop continues smoothly +5. **Flexible Permission Types**: Support for read, write, and all permission levels + +## Architecture Overview + +``` +User Input → LLM → Tool Call → Permission Check → [Permission Required] + ↓ + Create Permission Block + ↓ + Display Permission UI + ↓ + User Decision (Allow/Deny) + ↓ + [If Allowed] → Continue Tool Execution → Agent Loop Continues +``` + +## Component Design + +### 1. Message Block System + +#### New Permission Block Type +```typescript +type PermissionRequestBlock = { + type: 'tool_call_permission' + content: string // Description of permission needed + status: 'pending' | 'granted' | 'denied' + timestamp: number + tool_call: { + id: string + name: string + params: string + server_name: string + server_icons: string + server_description: string + } + extra: { + permissionType: 'read' | 'write' | 'all' + serverName: string + toolName: string + needsUserAction: boolean // Whether user action is still needed + grantedPermissions?: string[] // What permissions were granted + } +} +``` + +### 2. Tool Manager Changes + +The `ToolManager.callTool()` method will return a special response when permissions are required: + +```typescript +interface PermissionRequiredResponse extends MCPToolResponse { + toolCallId: string + content: string + isError: false + requiresPermission: true + permissionRequest: { + toolName: string + serverName: string + permissionType: 'read' | 'write' | 'all' + description: string + } +} +``` + +### 3. Agent Loop Modifications + +The LLMProviderPresenter agent loop will: + +1. Detect permission-required responses from tool calls +2. Create a permission block and yield it to ThreadPresenter +3. Pause the agent loop (set `needContinueConversation = false`) +4. Wait for user permission decision via a continuation mechanism + +### 4. Permission Management + +#### ThreadPresenter Extensions +- Add `continueWithPermission(messageId: string, toolCallId: string, granted: boolean)` method +- Handle permission responses and resume agent loops + +#### ConfigPresenter Integration +- Update server configurations when users choose to remember permissions +- Manage persistent permission settings + +### 5. Frontend Components + +#### Permission Request UI Component +```vue + +``` + +Features: +- Clear permission type display (read/write/all) +- Tool and server information +- "Remember this choice" checkbox +- Allow/Deny buttons +- Loading states during processing + +## Implementation Flow + +### 1. Permission Check Flow + +```mermaid +sequenceDiagram + participant LLM as LLMProviderPresenter + participant TM as ToolManager + participant TP as ThreadPresenter + participant UI as Frontend + + LLM->>TM: callTool(toolCall) + TM->>TM: checkPermission() + alt Permission Required + TM-->>LLM: PermissionRequiredResponse + LLM->>TP: yield permission block + TP->>UI: render permission UI + UI->>UI: user clicks Allow/Deny + UI->>TP: permissionResponse + TP->>LLM: continueWithPermission() + LLM->>TM: callTool(toolCall) // retry + TM-->>LLM: success response + else Permission Granted + TM-->>LLM: normal response + end +``` + +### 2. State Management + +#### Message State +Each permission request creates a persistent message block that tracks: +- Current permission status +- User's decision +- Whether the request is still active + +#### Conversation State +The ThreadPresenter maintains: +- Active permission requests per conversation +- Continuation callbacks for resumed agent loops + +### 3. Permission Types + +#### Read Permissions +For tools that only read data: +- `list_directory` +- `read_file` +- `get_information` + +#### Write Permissions +For tools that modify data: +- `write_file` +- `create_directory` +- `delete_file` +- `execute_command` + +#### All Permissions +Grant both read and write access to the server. + +## Security Considerations + +1. **Default Deny**: All tool calls require explicit permission by default +2. **Server Isolation**: Permissions are granted per-server, not globally +3. **Permission Escalation**: Users must explicitly grant higher-level permissions +4. **Audit Trail**: All permission decisions are logged in message history +5. **Session Scope**: Temporary permissions last only for the current session + +## User Experience + +### Permission Request Display +- Clear visual distinction from regular tool calls +- Prominent permission type indicator +- Detailed explanation of what the tool will do +- Server trust indicators (icons, descriptions) + +### Permission Management +- Settings page to review and modify saved permissions +- Bulk permission operations for trusted servers +- Permission history and audit log + +### Error Handling +- Clear error messages when permissions are denied +- Guidance on how to grant permissions if needed +- Fallback options when tools are unavailable + +## Future Enhancements + +1. **Granular Permissions**: File-path or operation-specific permissions +2. **Temporary Permissions**: Time-limited or usage-limited grants +3. **Permission Templates**: Pre-configured permission sets for common workflows +4. **Server Reputation**: Community-driven trust scores for MCP servers +5. **Permission Analytics**: Usage statistics and security insights + +## Migration Plan + +1. **Phase 1**: Implement core permission blocking system +2. **Phase 2**: Add UI components and user interaction +3. **Phase 3**: Implement permission persistence and management +4. **Phase 4**: Add advanced features and optimizations + +This design ensures a secure, user-friendly permission system that maintains the conversational flow while giving users full control over tool execution permissions. \ No newline at end of file diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index 5e55624fa..a427a0e27 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -299,7 +299,7 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { temperature: number = 0.6, maxTokens: number = 4096 ): AsyncGenerator { - console.log('Starting agent loop for event:', eventId, 'with model:', modelId) + console.log(`[Agent Loop] Starting agent loop for event: ${eventId} with model: ${modelId}`) if (!this.canStartNewStream()) { // Instead of throwing, yield an error event yield { type: 'error', data: { eventId, error: '已达到最大并发流数量限制' } } @@ -370,7 +370,7 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { const currentToolChunks: Record = {} try { - console.log(`Loop iteration ${toolCallCount + 1} for event ${eventId}`) + console.log(`[Agent Loop] Iteration ${toolCallCount + 1} for event: ${eventId}`) const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions() // Call the provider's core stream method, expecting LLMCoreStreamEvent const stream = provider.coreStream( @@ -649,6 +649,37 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { if (abortController.signal.aborted) break // Check after tool call returns + // Check if permission is required + if (toolResponse.rawData.requiresPermission) { + console.log( + `[Agent Loop] Permission required for tool ${toolCall.name}, creating permission request` + ) + + // Yield permission request event + yield { + type: 'response', + data: { + eventId, + tool_call: 'permission-required', + tool_call_id: toolCall.id, + tool_call_name: toolCall.name, + tool_call_params: toolCall.arguments, + tool_call_server_name: toolResponse.rawData.permissionRequest?.serverName, + tool_call_server_icons: toolDef.server.icons, + tool_call_server_description: toolDef.server.description, + tool_call_response: toolResponse.content, + permission_request: toolResponse.rawData.permissionRequest + } + } + + // End the agent loop here - permission handling will trigger a new agent loop + console.log( + `[Agent Loop] Ending agent loop for permission request, event: ${eventId}` + ) + needContinueConversation = false + break + } + // Add tool call and response to conversation history for the next LLM iteration const supportsFunctionCall = modelConfig?.functionCall || false @@ -900,6 +931,10 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { needContinueConversation = false // Stop loop on inner error } } // --- End of Agent Loop (while) --- + + console.log( + `[Agent Loop] Agent loop completed for event: ${eventId}, iterations: ${toolCallCount}` + ) } catch (error) { // Catch errors from the generator setup phase (before the loop) if (abortController.signal.aborted) { diff --git a/src/main/presenter/mcpPresenter/index.ts b/src/main/presenter/mcpPresenter/index.ts index ceaf6d286..e23fedd0a 100644 --- a/src/main/presenter/mcpPresenter/index.ts +++ b/src/main/presenter/mcpPresenter/index.ts @@ -1109,4 +1109,23 @@ export class McpPresenter implements IMCPPresenter { }) return openaiTools } + + async grantPermission( + serverName: string, + permissionType: 'read' | 'write' | 'all', + remember: boolean = false + ): Promise { + try { + console.log( + `[MCP] Granting ${permissionType} permission for server: ${serverName}, remember: ${remember}` + ) + await this.toolManager.grantPermission(serverName, permissionType, remember) + console.log( + `[MCP] Successfully granted ${permissionType} permission for server: ${serverName}` + ) + } catch (error) { + console.error(`[MCP] Failed to grant permission for server ${serverName}:`, error) + throw error + } + } } diff --git a/src/main/presenter/mcpPresenter/toolManager.ts b/src/main/presenter/mcpPresenter/toolManager.ts index 9f66c2b54..35abd4879 100644 --- a/src/main/presenter/mcpPresenter/toolManager.ts +++ b/src/main/presenter/mcpPresenter/toolManager.ts @@ -25,6 +25,7 @@ export class ToolManager { this.configPresenter = configPresenter this.serverManager = serverManager eventBus.on(MCP_EVENTS.CLIENT_LIST_UPDATED, this.handleServerListUpdate) + eventBus.on(MCP_EVENTS.CONFIG_CHANGED, this.handleConfigChange) } private handleServerListUpdate = (): void => { @@ -33,6 +34,12 @@ export class ToolManager { this.toolNameToTargetMap = null } + private handleConfigChange = (): void => { + console.info('MCP configuration changed, clearing cached data.') + this.cachedToolDefinitions = null + this.toolNameToTargetMap = null + } + public async getRunningClients(): Promise { return this.serverManager.getRunningClients() } @@ -196,33 +203,84 @@ export class ToolManager { return this.cachedToolDefinitions } + // 确定权限类型的新方法 + private determinePermissionType(toolName: string): 'read' | 'write' | 'all' { + const lowerToolName = toolName.toLowerCase() + + // Read operations + if ( + lowerToolName.includes('read') || + lowerToolName.includes('list') || + lowerToolName.includes('get') || + lowerToolName.includes('show') || + lowerToolName.includes('view') || + lowerToolName.includes('fetch') || + lowerToolName.includes('search') || + lowerToolName.includes('find') || + lowerToolName.includes('query') + ) { + return 'read' + } + + // Write operations + if ( + lowerToolName.includes('write') || + lowerToolName.includes('create') || + lowerToolName.includes('update') || + lowerToolName.includes('delete') || + lowerToolName.includes('modify') || + lowerToolName.includes('edit') || + lowerToolName.includes('remove') || + lowerToolName.includes('add') || + lowerToolName.includes('insert') || + lowerToolName.includes('save') || + lowerToolName.includes('execute') || + lowerToolName.includes('run') || + lowerToolName.includes('call') || + lowerToolName.includes('move') || + lowerToolName.includes('copy') || + lowerToolName.includes('mkdir') || + lowerToolName.includes('rmdir') + ) { + return 'write' + } + + // Default to write for safety (unknown operations require higher permissions) + return 'write' + } + // 检查工具调用权限 private checkToolPermission( originalToolName: string, serverName: string, autoApprove: string[] ): boolean { - console.log('checkToolPermission', originalToolName, serverName, autoApprove) + console.log( + `[ToolManager] Checking permissions for tool '${originalToolName}' on server '${serverName}' with autoApprove:`, + autoApprove + ) + // 如果有 'all' 权限,则允许所有操作 if (autoApprove.includes('all')) { + console.log(`[ToolManager] Permission granted: server '${serverName}' has 'all' permissions`) return true } - if ( - originalToolName.includes('read') || - originalToolName.includes('list') || - originalToolName.includes('get') - ) { - return autoApprove.includes('read') - } - if ( - originalToolName.includes('write') || - originalToolName.includes('create') || - originalToolName.includes('update') || - originalToolName.includes('delete') - ) { - return autoApprove.includes('write') + + const permissionType = this.determinePermissionType(originalToolName) + console.log(`[ToolManager] Tool '${originalToolName}' requires '${permissionType}' permission`) + + // Check if the specific permission type is approved + if (autoApprove.includes(permissionType)) { + console.log( + `[ToolManager] Permission granted: server '${serverName}' has '${permissionType}' permission` + ) + return true } - return true + + console.log( + `[ToolManager] Permission required for tool '${originalToolName}' on server '${serverName}'.` + ) + return false } async callTool(toolCall: MCPToolCall): Promise { @@ -230,6 +288,13 @@ export class ToolManager { const finalName = toolCall.function.name const argsString = toolCall.function.arguments + console.log(`[ToolManager] Calling tool:`, { + requestedName: finalName, + originalName: finalName, + serverName: toolCall.server?.name || 'unknown', + rawArguments: argsString + }) + // Ensure definitions and map are loaded/cached await this.getAllToolDefinitions() @@ -303,11 +368,24 @@ export class ToolManager { const hasPermission = this.checkToolPermission(originalName, toolServerName, autoApprove) if (!hasPermission) { - console.warn(`Permission denied for tool '${originalName}' on server '${toolServerName}'.`) + console.warn( + `Permission required for tool '${originalName}' on server '${toolServerName}'.` + ) + + const permissionType = this.determinePermissionType(originalName) + + // Return permission request instead of error return { toolCallId: toolCall.id, - content: `Error: Operation not permitted. The '${originalName}' operation on server '${toolServerName}' requires appropriate permissions.`, - isError: true // Indicate error + content: `components.messageBlockPermissionRequest.description.${permissionType}`, + isError: false, + requiresPermission: true, + permissionRequest: { + toolName: originalName, + serverName: toolServerName, + permissionType, + description: `Allow ${originalName} to perform ${permissionType} operations on ${toolServerName}?` + } } } @@ -406,7 +484,89 @@ export class ToolManager { } } + // 权限管理方法 + async grantPermission( + serverName: string, + permissionType: 'read' | 'write' | 'all', + remember: boolean = true + ): Promise { + console.log( + `[ToolManager] Granting permission: ${permissionType} for server: ${serverName}, remember: ${remember}` + ) + + if (remember) { + // Persist to configuration + await this.updateServerPermissions(serverName, permissionType) + } else { + // Store in temporary session storage + // TODO: Implement temporary permission storage + console.log(`[ToolManager] Temporary permission granted (session-scoped)`) + } + } + + private async updateServerPermissions( + serverName: string, + permissionType: 'read' | 'write' | 'all' + ): Promise { + try { + console.log(`[ToolManager] Updating server ${serverName} permissions: ${permissionType}`) + const servers = await this.configPresenter.getMcpServers() + const serverConfig = servers[serverName] + + if (serverConfig) { + let autoApprove = [...(serverConfig.autoApprove || [])] + + // If 'all' permission already exists, no need to add specific permissions + if (autoApprove.includes('all')) { + console.log(`Server ${serverName} already has 'all' permissions`) + return + } + + // If requesting 'all' permission, remove specific permissions and add 'all' + if (permissionType === 'all') { + autoApprove = autoApprove.filter((p) => p !== 'read' && p !== 'write') + autoApprove.push('all') + } else { + // Add the specific permission if not already present + if (!autoApprove.includes(permissionType)) { + autoApprove.push(permissionType) + } + } + + console.log( + `[ToolManager] Before update - Server ${serverName} permissions:`, + serverConfig.autoApprove || [] + ) + console.log(`[ToolManager] After update - Server ${serverName} permissions:`, autoApprove) + + // Update server configuration + await this.configPresenter.updateMcpServer(serverName, { + ...serverConfig, + autoApprove + }) + + console.log( + `[ToolManager] Successfully updated server ${serverName} permissions to:`, + autoApprove + ) + + // Verify the update by reading back + const updatedServers = await this.configPresenter.getMcpServers() + const updatedConfig = updatedServers[serverName] + console.log( + `[ToolManager] Verification - Server ${serverName} current permissions:`, + updatedConfig?.autoApprove || [] + ) + } else { + console.error(`[ToolManager] Server configuration not found for: ${serverName}`) + } + } catch (error) { + console.error('[ToolManager] Failed to update server permissions:', error) + } + } + public destroy(): void { eventBus.off(MCP_EVENTS.CLIENT_LIST_UPDATED, this.handleServerListUpdate) + eventBus.off(MCP_EVENTS.CONFIG_CHANGED, this.handleConfigChange) } } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 4b0756d3a..15b2881e7 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -127,119 +127,160 @@ export class ThreadPresenter implements IThreadPresenter { const { eventId, userStop } = msg const state = this.generatingMessages.get(eventId) if (state) { - state.message.content.forEach((block) => { - block.status = 'success' - }) - // 计算completion tokens - let completionTokens = 0 - if (state.totalUsage) { - completionTokens = state.totalUsage.completion_tokens - } else { - for (const block of state.message.content) { - if ( - block.type === 'content' || - block.type === 'reasoning_content' || - block.type === 'tool_call' - ) { - completionTokens += approximateTokenSize(block.content) - } - } - } + console.log( + `[ThreadPresenter] Handling LLM agent end for message: ${eventId}, userStop: ${userStop}` + ) - // 检查是否有内容块 - const hasContentBlock = state.message.content.some( - (block) => - block.type === 'content' || - block.type === 'reasoning_content' || - block.type === 'tool_call' || - block.type === 'image' + // 检查是否有未处理的权限请求 + const hasPendingPermissions = state.message.content.some( + (block) => block.type === 'tool_call_permission' && block.status === 'pending' ) - // 如果没有内容块,添加错误信息 - if (!hasContentBlock && !userStop) { - state.message.content.push({ - type: 'error', - content: 'common.error.noModelResponse', - status: 'error', - timestamp: Date.now() + if (hasPendingPermissions) { + console.log( + `[ThreadPresenter] Message ${eventId} has pending permissions, keeping in generating state` + ) + // 保持消息在generating状态,等待权限响应 + // 但是要更新非权限块为success状态 + state.message.content.forEach((block) => { + if (block.type !== 'tool_call_permission' && block.status === 'loading') { + block.status = 'success' + } }) + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + return } - const totalTokens = state.promptTokens + completionTokens - const generationTime = Date.now() - (state.firstTokenTime ?? state.startTime) - const tokensPerSecond = completionTokens / (generationTime / 1000) - const contextUsage = state?.totalUsage?.context_length - ? (totalTokens / state.totalUsage.context_length) * 100 - : 0 + console.log(`[ThreadPresenter] Finalizing message ${eventId} - no pending permissions`) - // 如果有reasoning_content,记录结束时间 - const metadata: Partial = { - totalTokens, - inputTokens: state.promptTokens, - outputTokens: completionTokens, - generationTime, - firstTokenTime: state.firstTokenTime ? state.firstTokenTime - state.startTime : 0, - tokensPerSecond, - contextUsage - } + // 正常完成流程 + await this.finalizeMessage(state, eventId, userStop || false) + } - if (state.reasoningStartTime !== null && state.lastReasoningTime !== null) { - metadata.reasoningStartTime = state.reasoningStartTime - state.startTime - metadata.reasoningEndTime = state.lastReasoningTime - state.startTime - } + eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, msg) + } - // 更新消息的usage信息 - await this.messageManager.updateMessageMetadata(eventId, metadata) - await this.messageManager.updateMessageStatus(eventId, 'sent') - await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) - this.generatingMessages.delete(eventId) + // 完成消息的通用方法 + private async finalizeMessage( + state: GeneratingMessageState, + eventId: string, + userStop: boolean + ): Promise { + // 将所有块设为success状态,但保留权限块的状态 + state.message.content.forEach((block) => { + if (block.type === 'tool_call_permission') { + // 权限块保持其当前状态(granted/denied/error) + return + } + block.status = 'success' + }) - // 检查是否需要总结标题 - const conversation = await this.sqlitePresenter.getConversation(state.conversationId) - let titleUpdated = false - if (conversation.is_new === 1) { - try { - // 注意:第二个参数直接传入 conversationId - this.summaryTitles(undefined, state.conversationId).then((title) => { - if (title) { - // renameConversation 会更新标题和updatedAt,并广播 - this.renameConversation(state.conversationId, title).then(() => { - titleUpdated = true - }) - } - }) - } catch (e) { - console.error('Failed to summarize title in main process:', e) + // 计算completion tokens + let completionTokens = 0 + if (state.totalUsage) { + completionTokens = state.totalUsage.completion_tokens + } else { + for (const block of state.message.content) { + if ( + block.type === 'content' || + block.type === 'reasoning_content' || + block.type === 'tool_call' + ) { + completionTokens += approximateTokenSize(block.content) } } + } - // 如果标题没有被更新(即不是新会话,或生成标题失败), - // 我们仍然需要更新updatedAt并广播 - if (!titleUpdated) { - this.sqlitePresenter - .updateConversation(state.conversationId, { - updatedAt: Date.now() - }) - .then(() => { - console.log('updated conv time', state.conversationId) - }) - // 手动触发一次广播,因为这次更新没有经过其他会触发广播的方法 - await this.broadcastThreadListUpdate() - } - - // --- 新增逻辑:广播消息生成完成事件 --- - // 在所有数据库和状态更新完成后,获取最终的消息对象 - const finalMessage = await this.messageManager.getMessage(eventId) - if (finalMessage) { - // 该事件仅在主进程内部流通,用于通知其他监听者(如MCP会议主持人) - eventBus.sendToMain(CONVERSATION_EVENTS.MESSAGE_GENERATED, { - conversationId: finalMessage.conversationId, - message: finalMessage + // 检查是否有内容块 + const hasContentBlock = state.message.content.some( + (block) => + block.type === 'content' || + block.type === 'reasoning_content' || + block.type === 'tool_call' || + block.type === 'image' + ) + + // 如果没有内容块,添加错误信息 + if (!hasContentBlock && !userStop) { + state.message.content.push({ + type: 'error', + content: 'common.error.noModelResponse', + status: 'error', + timestamp: Date.now() + }) + } + + const totalTokens = state.promptTokens + completionTokens + const generationTime = Date.now() - (state.firstTokenTime ?? state.startTime) + const tokensPerSecond = completionTokens / (generationTime / 1000) + const contextUsage = state?.totalUsage?.context_length + ? (totalTokens / state.totalUsage.context_length) * 100 + : 0 + + // 如果有reasoning_content,记录结束时间 + const metadata: Partial = { + totalTokens, + inputTokens: state.promptTokens, + outputTokens: completionTokens, + generationTime, + firstTokenTime: state.firstTokenTime ? state.firstTokenTime - state.startTime : 0, + tokensPerSecond, + contextUsage + } + + if (state.reasoningStartTime !== null && state.lastReasoningTime !== null) { + metadata.reasoningStartTime = state.reasoningStartTime - state.startTime + metadata.reasoningEndTime = state.lastReasoningTime - state.startTime + } + + // 更新消息的usage信息 + await this.messageManager.updateMessageMetadata(eventId, metadata) + await this.messageManager.updateMessageStatus(eventId, 'sent') + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + this.generatingMessages.delete(eventId) + + // 处理标题更新和会话更新 + await this.handleConversationUpdates(state) + + // 广播消息生成完成事件 + const finalMessage = await this.messageManager.getMessage(eventId) + if (finalMessage) { + eventBus.sendToMain(CONVERSATION_EVENTS.MESSAGE_GENERATED, { + conversationId: finalMessage.conversationId, + message: finalMessage + }) + } + } + + // 处理会话更新和标题生成 + private async handleConversationUpdates(state: GeneratingMessageState): Promise { + const conversation = await this.sqlitePresenter.getConversation(state.conversationId) + let titleUpdated = false + + if (conversation.is_new === 1) { + try { + this.summaryTitles(undefined, state.conversationId).then((title) => { + if (title) { + this.renameConversation(state.conversationId, title).then(() => { + titleUpdated = true + }) + } }) + } catch (e) { + console.error('Failed to summarize title in main process:', e) } } - eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, msg) + if (!titleUpdated) { + this.sqlitePresenter + .updateConversation(state.conversationId, { + updatedAt: Date.now() + }) + .then(() => { + console.log('updated conv time', state.conversationId) + }) + await this.broadcastThreadListUpdate() + } } async handleLLMAgentResponse(msg: LLMAgentEventData) { @@ -270,6 +311,10 @@ export class ThreadPresenter implements IThreadPresenter { ? state.message.content[state.message.content.length - 1] : undefined if (lastBlock) { + if (lastBlock.type === 'tool_call_permission' && lastBlock.status === 'pending') { + lastBlock.status = 'granted' + return + } // 只有当上一个块不是一个正在等待结果的工具调用时,才将其标记为成功 if (!(lastBlock.type === 'tool_call' && lastBlock.status === 'loading')) { lastBlock.status = 'success' @@ -331,13 +376,16 @@ export class ThreadPresenter implements IThreadPresenter { if (tool_call_response_raw && tool_call === 'end') { try { // 检查返回的内容中是否有deepchat-webpage类型的资源 - const hasSearchResults = tool_call_response_raw.content?.some( - (item: { type: string; resource?: { mimeType: string } }) => - item?.type === 'resource' && - item?.resource?.mimeType === 'application/deepchat-webpage' - ) + // 确保content是数组才调用some方法 + const hasSearchResults = + Array.isArray(tool_call_response_raw.content) && + tool_call_response_raw.content.some( + (item: { type: string; resource?: { mimeType: string } }) => + item?.type === 'resource' && + item?.resource?.mimeType === 'application/deepchat-webpage' + ) - if (hasSearchResults) { + if (hasSearchResults && Array.isArray(tool_call_response_raw.content)) { // 解析搜索结果 const searchResults = tool_call_response_raw.content .filter( @@ -470,6 +518,44 @@ export class ThreadPresenter implements IThreadPresenter { toolCallBlock.tool_call.server_description = tool_call_server_description } } + } else if (tool_call === 'permission-required') { + // 处理权限请求:创建权限请求块 + // 注意:不调用finalizeLastBlock,因为工具调用还没有完成,在等待权限 + + // 从 msg 中获取权限请求信息 + const { permission_request } = msg + + state.message.content.push({ + type: 'tool_call_permission', + content: + typeof tool_call_response === 'string' + ? tool_call_response + : 'Permission required for this operation', + status: 'pending', + timestamp: currentTime, + tool_call: { + id: tool_call_id, + name: tool_call_name, + params: tool_call_params || '', + server_name: tool_call_server_name, + server_icons: tool_call_server_icons, + server_description: tool_call_server_description + }, + extra: { + permissionType: permission_request?.permissionType || 'write', + serverName: permission_request?.serverName || tool_call_server_name || '', + toolName: permission_request?.toolName || tool_call_name || '', + needsUserAction: true, + permissionRequest: JSON.stringify( + permission_request || { + toolName: tool_call_name || '', + serverName: tool_call_server_name || '', + permissionType: 'write' as const, + description: 'Permission required for this operation' + } + ) + } + }) } else if (tool_call === 'end' || tool_call === 'error') { // 查找对应的工具调用块 const toolCallBlock = state.message.content.find( @@ -1601,16 +1687,31 @@ export class ThreadPresenter implements IThreadPresenter { const conversation = await this.getConversation(conversationId) let contextMessages: Message[] = [] let userMessage: Message | null = null + if (queryMsgId) { // 处理指定消息ID的情况 const queryMessage = await this.getMessage(queryMsgId) - if (!queryMessage || !queryMessage.parentId) { + if (!queryMessage) { throw new Error('找不到指定的消息') } - userMessage = await this.getMessage(queryMessage.parentId) - if (!userMessage) { - throw new Error('找不到触发消息') + + // 修复:根据消息类型确定如何获取用户消息 + if (queryMessage.role === 'user') { + // 如果 queryMessage 就是用户消息,直接使用 + userMessage = queryMessage + } else if (queryMessage.role === 'assistant') { + // 如果 queryMessage 是助手消息,获取它的 parentId(用户消息) + if (!queryMessage.parentId) { + throw new Error('助手消息缺少 parentId') + } + userMessage = await this.getMessage(queryMessage.parentId) + if (!userMessage) { + throw new Error('找不到触发消息') + } + } else { + throw new Error('不支持的消息类型') } + contextMessages = await this.getMessageHistory( userMessage.id, conversation.settings.contextLength @@ -2655,4 +2756,527 @@ export class ThreadPresenter implements IThreadPresenter { finalGroupedList ) } + + // 权限响应处理方法 - 重新设计为基于消息数据的流程 + async handlePermissionResponse( + messageId: string, + toolCallId: string, + granted: boolean, + permissionType: 'read' | 'write' | 'all', + remember: boolean = true + ): Promise { + console.log(`[ThreadPresenter] Handling permission response:`, { + messageId, + toolCallId, + granted, + permissionType, + remember + }) + + try { + // 1. 获取消息并更新权限块状态 + const message = await this.messageManager.getMessage(messageId) + if (!message || message.role !== 'assistant') { + const errorMsg = `Message not found or not an assistant message (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + const content = message.content as AssistantMessageBlock[] + const permissionBlock = content.find( + (block) => block.type === 'tool_call_permission' && block.tool_call?.id === toolCallId + ) + + if (!permissionBlock) { + const errorMsg = `Permission block not found (messageId: ${messageId}, toolCallId: ${toolCallId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + console.error( + `[ThreadPresenter] Available blocks:`, + content.map((block) => ({ + type: block.type, + toolCallId: block.tool_call?.id + })) + ) + throw new Error(errorMsg) + } + + console.log( + `[ThreadPresenter] Found permission block for tool: ${permissionBlock.tool_call?.name}` + ) + + // 2. 更新权限块状态 + permissionBlock.status = granted ? 'granted' : 'denied' + if (permissionBlock.extra) { + permissionBlock.extra.needsUserAction = false + if (granted) { + permissionBlock.extra.grantedPermissions = permissionType + } + } + + // 3. 保存消息更新 + await this.messageManager.editMessage(messageId, JSON.stringify(content)) + console.log(`[ThreadPresenter] Updated permission block status to: ${permissionBlock.status}`) + + if (granted) { + // 4. 权限授予流程 + const serverName = permissionBlock?.extra?.serverName as string + if (!serverName) { + const errorMsg = `Server name not found in permission block (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + console.log( + `[ThreadPresenter] Granting permission: ${permissionType} for server: ${serverName}` + ) + console.log( + `[ThreadPresenter] Waiting for permission configuration to complete before restarting agent loop...` + ) + + try { + // 等待权限配置完成 + await presenter.mcpPresenter.grantPermission(serverName, permissionType, remember) + console.log(`[ThreadPresenter] Permission granted successfully`) + + // 等待MCP服务重启完成 + console.log( + `[ThreadPresenter] Permission configuration completed, waiting for MCP service restart...` + ) + await this.waitForMcpServiceReady(serverName) + + console.log( + `[ThreadPresenter] MCP service ready, now restarting agent loop for message: ${messageId}` + ) + } catch (permissionError) { + console.error(`[ThreadPresenter] Failed to grant permission:`, permissionError) + // 权限授予失败,将状态更新为错误 + permissionBlock.status = 'error' + await this.messageManager.editMessage(messageId, JSON.stringify(content)) + throw permissionError + } + + // 5. 现在重启agent loop + await this.restartAgentLoopAfterPermission(messageId) + } else { + console.log( + `[ThreadPresenter] Permission denied, ending generation for message: ${messageId}` + ) + // 6. 权限被拒绝 - 正常结束消息 + await this.finalizeMessageAfterPermissionDenied(messageId) + } + } catch (error) { + console.error(`[ThreadPresenter] Failed to handle permission response:`, error) + + // 确保消息状态正确更新 + try { + const message = await this.messageManager.getMessage(messageId) + if (message) { + await this.messageManager.handleMessageError(messageId, String(error)) + } + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } + + throw error + } + } + + // 重新启动agent loop (权限授予后) + private async restartAgentLoopAfterPermission(messageId: string): Promise { + console.log( + `[ThreadPresenter] Restarting agent loop after permission for message: ${messageId}` + ) + + try { + // 获取消息和会话信息 + const message = await this.messageManager.getMessage(messageId) + if (!message) { + const errorMsg = `Message not found (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + const conversationId = message.conversationId + console.log(`[ThreadPresenter] Found message in conversation: ${conversationId}`) + + // 验证权限是否生效 - 获取最新的服务器配置 + const content = message.content as AssistantMessageBlock[] + const permissionBlock = content.find( + (block) => block.type === 'tool_call_permission' && block.status === 'granted' + ) + + if (!permissionBlock) { + const errorMsg = `No granted permission block found (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + console.error( + `[ThreadPresenter] Available blocks:`, + content.map((block) => ({ + type: block.type, + status: block.status, + toolCallId: block.tool_call?.id + })) + ) + throw new Error(errorMsg) + } + + if (permissionBlock?.extra?.serverName) { + console.log( + `[ThreadPresenter] Verifying permission is active for server: ${permissionBlock.extra.serverName}` + ) + try { + const servers = await this.configPresenter.getMcpServers() + const serverConfig = servers[permissionBlock.extra.serverName as string] + console.log( + `[ThreadPresenter] Current server permissions:`, + serverConfig?.autoApprove || [] + ) + } catch (configError) { + console.warn(`[ThreadPresenter] Failed to verify server permissions:`, configError) + } + } + + // 如果消息还在generating状态,直接继续 + const state = this.generatingMessages.get(messageId) + if (state) { + console.log(`[ThreadPresenter] Message still in generating state, resuming from memory`) + await this.resumeStreamCompletion(conversationId, messageId) + return + } + + // 否则重新启动完整的agent loop + console.log(`[ThreadPresenter] Message not in generating state, starting fresh agent loop`) + + // 重新创建生成状态 + const assistantMessage = message as AssistantMessage + + this.generatingMessages.set(messageId, { + message: assistantMessage, + conversationId, + startTime: Date.now(), + firstTokenTime: null, + promptTokens: 0, + reasoningStartTime: null, + reasoningEndTime: null, + lastReasoningTime: null + }) + + console.log(`[ThreadPresenter] Created new generating state for message: ${messageId}`) + + // 启动新的流式完成 + await this.startStreamCompletion(conversationId, messageId) + } catch (error) { + console.error(`[ThreadPresenter] Failed to restart agent loop:`, error) + + // 确保清理生成状态 + this.generatingMessages.delete(messageId) + + try { + await this.messageManager.handleMessageError(messageId, String(error)) + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } + + throw error + } + } + + // 权限被拒绝后完成消息 + private async finalizeMessageAfterPermissionDenied(messageId: string): Promise { + console.log(`[ThreadPresenter] Finalizing message after permission denied: ${messageId}`) + + try { + const message = await this.messageManager.getMessage(messageId) + if (!message) return + + const content = message.content as AssistantMessageBlock[] + + // 将所有loading状态的块设为success,但保留权限块的状态 + content.forEach((block) => { + if (block.type === 'tool_call_permission') { + // 权限块保持其当前状态(granted/denied/error) + return + } + if (block.status === 'loading') { + block.status = 'success' + } + }) + + // 添加权限被拒绝的提示 + content.push({ + type: 'error', + content: 'Permission denied by user', + status: 'error', + timestamp: Date.now() + }) + + await this.messageManager.editMessage(messageId, JSON.stringify(content)) + await this.messageManager.updateMessageStatus(messageId, 'sent') + + // 清理生成状态 + this.generatingMessages.delete(messageId) + + // 发送结束事件 + eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, { + eventId: messageId, + userStop: false + }) + + console.log(`[ThreadPresenter] Message finalized after permission denial: ${messageId}`) + } catch (error) { + console.error(`[ThreadPresenter] Failed to finalize message after permission denial:`, error) + } + } + + // 恢复流式完成 (用于内存状态存在的情况) + private async resumeStreamCompletion(conversationId: string, messageId: string): Promise { + const state = this.generatingMessages.get(messageId) + if (!state) { + console.log( + `[ThreadPresenter] No generating state found for ${messageId}, starting fresh agent loop` + ) + await this.startStreamCompletion(conversationId) + return + } + + try { + console.log(`[ThreadPresenter] Resuming stream completion for message: ${messageId}`) + + // 关键修复:重新构建上下文,确保包含被中断的工具调用信息 + const conversation = await this.getConversation(conversationId) + if (!conversation) { + const errorMsg = `Conversation not found (conversationId: ${conversationId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + const { providerId, modelId, temperature, maxTokens } = conversation.settings + const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) + + if (!modelConfig) { + console.warn( + `[ThreadPresenter] Model config not found for ${modelId} (${providerId}), using default` + ) + } + + // 查找被权限中断的工具调用 + const pendingToolCall = this.findPendingToolCallAfterPermission(state.message.content) + + if (!pendingToolCall) { + console.warn( + `[ThreadPresenter] No pending tool call found after permission grant, using normal context` + ) + // 如果没有找到待执行的工具调用,使用正常流程 + await this.startStreamCompletion(conversationId, messageId) + return + } + + console.log( + `[ThreadPresenter] Found pending tool call: ${pendingToolCall.name} with ID: ${pendingToolCall.id}` + ) + + // 获取对话上下文(基于助手消息,它会自动找到相应的用户消息) + const { contextMessages, userMessage } = await this.prepareConversationContext( + conversationId, + messageId // 使用助手消息ID,让prepareConversationContext自动解析 + ) + + console.log( + `[ThreadPresenter] Prepared conversation context with ${contextMessages.length} messages` + ) + + // 构建专门的继续执行上下文 + const finalContent = await this.buildContinueToolCallContext( + conversation, + contextMessages, + userMessage, + pendingToolCall, + modelConfig + ) + + console.log(`[ThreadPresenter] Built continue context for tool: ${pendingToolCall.name}`) + + // Continue the agent loop with the correct context + const stream = this.llmProviderPresenter.startStreamCompletion( + providerId, + finalContent, + modelId, + messageId, + temperature, + maxTokens + ) + + for await (const event of stream) { + const msg = event.data + if (event.type === 'response') { + await this.handleLLMAgentResponse(msg) + } else if (event.type === 'error') { + await this.handleLLMAgentError(msg) + } else if (event.type === 'end') { + await this.handleLLMAgentEnd(msg) + } + } + } catch (error) { + console.error('[ThreadPresenter] Failed to resume stream completion:', error) + + // 确保清理生成状态 + this.generatingMessages.delete(messageId) + + try { + await this.messageManager.handleMessageError(messageId, String(error)) + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } + + throw error + } + } + + // 等待MCP服务重启完成并准备就绪 + private async waitForMcpServiceReady( + serverName: string, + maxWaitTime: number = 3000 + ): Promise { + console.log(`[ThreadPresenter] Waiting for MCP service ${serverName} to be ready...`) + + const startTime = Date.now() + const checkInterval = 100 // 100ms + + return new Promise((resolve) => { + const checkReady = async () => { + try { + // 检查服务是否正在运行 + const isRunning = await presenter.mcpPresenter.isServerRunning(serverName) + + if (isRunning) { + // 服务正在运行,再等待一下确保完全初始化 + setTimeout(() => { + console.log(`[ThreadPresenter] MCP service ${serverName} is ready`) + resolve() + }, 200) + return + } + + // 检查是否超时 + if (Date.now() - startTime > maxWaitTime) { + console.warn( + `[ThreadPresenter] Timeout waiting for MCP service ${serverName} to be ready` + ) + resolve() // 超时也继续,避免阻塞 + return + } + + // 继续等待 + setTimeout(checkReady, checkInterval) + } catch (error) { + console.error(`[ThreadPresenter] Error checking MCP service status:`, error) + resolve() // 出错也继续,避免阻塞 + } + } + + checkReady() + }) + } + + // 查找权限授予后待执行的工具调用 + private findPendingToolCallAfterPermission( + content: AssistantMessageBlock[] + ): { id: string; name: string; params: string } | null { + // 查找已授权的权限块 + const grantedPermissionBlock = content.find( + (block) => block.type === 'tool_call_permission' && block.status === 'granted' + ) + + if (!grantedPermissionBlock?.tool_call) { + return null + } + + const { id, name, params } = grantedPermissionBlock.tool_call + if (!id || !name || !params) { + console.warn( + `[ThreadPresenter] Incomplete tool call info in permission block:`, + grantedPermissionBlock.tool_call + ) + return null + } + + return { id, name, params } + } + + // 构建继续工具调用执行的上下文 + private async buildContinueToolCallContext( + conversation: any, + contextMessages: any[], + userMessage: any, + pendingToolCall: { id: string; name: string; params: string }, + modelConfig: any + ): Promise { + const { systemPrompt } = conversation.settings + const formattedMessages: ChatMessage[] = [] + + // 1. 添加系统提示 + if (systemPrompt) { + formattedMessages.push({ + role: 'system', + content: systemPrompt + }) + } + + // 2. 添加上下文消息 + const contextChatMessages = this.addContextMessages( + contextMessages, + false, + modelConfig.functionCall + ) + formattedMessages.push(...contextChatMessages) + + // 3. 添加当前用户消息 + const userContent = userMessage.content + const msgText = userContent.content + ? this.formatUserMessageContent(userContent.content) + : userContent.text + const finalUserContent = `${msgText}${getFileContext(userContent.files || [])}` + + formattedMessages.push({ + role: 'user', + content: finalUserContent + }) + + // 4. 添加助手消息,说明需要执行工具调用 + if (modelConfig.functionCall) { + // 对于原生支持函数调用的模型,添加tool_calls + formattedMessages.push({ + role: 'assistant', + tool_calls: [ + { + id: pendingToolCall.id, + type: 'function', + function: { + name: pendingToolCall.name, + arguments: pendingToolCall.params + } + } + ] + }) + + // 添加一个虚拟的工具响应,说明权限已经授予 + formattedMessages.push({ + role: 'tool', + tool_call_id: pendingToolCall.id, + content: `Permission granted. Please proceed with executing the ${pendingToolCall.name} function.` + }) + } else { + // 对于非原生支持的模型,使用文本提示 + formattedMessages.push({ + role: 'assistant', + content: `I need to call the ${pendingToolCall.name} function with the following parameters: ${pendingToolCall.params}` + }) + + formattedMessages.push({ + role: 'user', + content: `Permission has been granted for the ${pendingToolCall.name} function. Please proceed with the execution.` + }) + } + + return formattedMessages + } } diff --git a/src/renderer/src/components/message/MessageBlockPermissionRequest.vue b/src/renderer/src/components/message/MessageBlockPermissionRequest.vue new file mode 100644 index 000000000..daa6f8e8e --- /dev/null +++ b/src/renderer/src/components/message/MessageBlockPermissionRequest.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/src/renderer/src/components/message/MessageItemAssistant.vue b/src/renderer/src/components/message/MessageItemAssistant.vue index e2433379d..e70be06eb 100644 --- a/src/renderer/src/components/message/MessageItemAssistant.vue +++ b/src/renderer/src/components/message/MessageItemAssistant.vue @@ -29,7 +29,7 @@ @@ -40,25 +40,31 @@ /> + @@ -113,6 +119,7 @@ import MessageBlockThink from './MessageBlockThink.vue' import MessageBlockSearch from './MessageBlockSearch.vue' import MessageBlockToolCall from './MessageBlockToolCall.vue' import MessageBlockError from './MessageBlockError.vue' +import MessageBlockPermissionRequest from './MessageBlockPermissionRequest.vue' import MessageToolbar from './MessageToolbar.vue' import MessageInfo from './MessageInfo.vue' import { useChatStore } from '@/stores/chat' @@ -238,7 +245,7 @@ const cancelFork = () => { const confirmFork = async () => { try { // 执行fork操作 - await chatStore.forkThread(props.message.id, t('dialog.fork.tag')) + await chatStore.forkThread(currentMessage.value.id, t('dialog.fork.tag')) isForkDialogOpen.value = false } catch (error) { console.error('创建对话分支失败:', error) @@ -249,9 +256,9 @@ const handleAction = ( action: 'retry' | 'delete' | 'copy' | 'prev' | 'next' | 'copyImage' | 'copyImageFromTop' | 'fork' ) => { if (action === 'retry') { - chatStore.retryMessage(props.message.id) + chatStore.retryMessage(currentMessage.value.id) } else if (action === 'delete') { - chatStore.deleteMessage(props.message.id) + chatStore.deleteMessage(currentMessage.value.id) } else if (action === 'copy') { window.api.copyText( currentContent.value @@ -286,12 +293,12 @@ const handleAction = ( currentVariantIndex.value++ } } else if (action === 'copyImage') { - emit('copyImage', props.message.id, props.message.parentId, false, { + emit('copyImage', currentMessage.value.id, currentMessage.value.parentId, false, { model_name: currentMessage.value.model_name, model_provider: currentMessage.value.model_provider }) } else if (action === 'copyImageFromTop') { - emit('copyImage', props.message.id, props.message.parentId, true, { + emit('copyImage', currentMessage.value.id, currentMessage.value.parentId, true, { model_name: currentMessage.value.model_name, model_provider: currentMessage.value.model_provider }) diff --git a/src/renderer/src/components/settings/ShortcutSettings.vue b/src/renderer/src/components/settings/ShortcutSettings.vue index 216a663c3..9dc37b04e 100644 --- a/src/renderer/src/components/settings/ShortcutSettings.vue +++ b/src/renderer/src/components/settings/ShortcutSettings.vue @@ -17,20 +17,36 @@
-
- +
+ {{ t(shortcut.label) }}
- - + -
@@ -407,10 +433,10 @@ const clearShortcut = async (shortcutId: string) => { // 设置为空字符串 const shortcutKey = shortcutId as keyof typeof shortcutKeys.value shortcutKeys.value[shortcutKey] = '' - + // 保存更改 await saveChanges() - + console.log(`Shortcut ${shortcutId} cleared`) } catch (error) { console.error('Clear shortcut error:', error) diff --git a/src/renderer/src/i18n/en-US/components.json b/src/renderer/src/i18n/en-US/components.json index 7f43c5231..d0754b7fd 100644 --- a/src/renderer/src/i18n/en-US/components.json +++ b/src/renderer/src/i18n/en-US/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "Continue", "continued": "Continued" + }, + "messageBlockPermissionRequest": { + "title": "Permission Required", + "allow": "Allow", + "deny": "Deny", + "rememberChoice": "Remember this choice", + "granted": "Permission granted", + "denied": "Permission denied", + "type": { + "read": "Read permissions", + "write": "Write permissions", + "all": "Full permissions" + }, + "description": { + "read": "Allow '{toolName}' of '{serverName}' to perform read operations?", + "write": "Allow '{toolName}' of '{serverName}' to perform write operations?", + "all": "Allow '{toolName}' of '{serverName}' to perform read and write operations?" + } } } diff --git a/src/renderer/src/i18n/fa-IR/components.json b/src/renderer/src/i18n/fa-IR/components.json index bdbaea1e1..c2831ee41 100644 --- a/src/renderer/src/i18n/fa-IR/components.json +++ b/src/renderer/src/i18n/fa-IR/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "ادامه", "continued": "ادامه یافت" + }, + "messageBlockPermissionRequest": { + "title": "مجوز مورد نیاز", + "allow": "اجازه دادن", + "deny": "رد کردن", + "rememberChoice": "این انتخاب را به خاطر بسپار", + "granted": "مجوز داده شد", + "denied": "مجوز رد شد", + "type": { + "read": "مجوزهای خواندن", + "write": "مجوزهای نوشتن", + "all": "تمام مجوزها" + }, + "description": { + "all": "اجازه دهید \"{نام ابزار\" از \"{servername}\" عملیات خواندن و نوشتن را انجام دهد؟", + "read": "اجازه دهید \"{نام ابزار\" از \"{servername}\" انجام عملیات خواندن را انجام دهد؟", + "write": "اجازه می دهد \"{ابزار\" از \"{servername}\" انجام عملیات نوشتن؟" + } } } diff --git a/src/renderer/src/i18n/fr-FR/components.json b/src/renderer/src/i18n/fr-FR/components.json index 88895f8ec..a1aa7134a 100644 --- a/src/renderer/src/i18n/fr-FR/components.json +++ b/src/renderer/src/i18n/fr-FR/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "Continuer", "continued": "Suite" + }, + "messageBlockPermissionRequest": { + "title": "Autorisation requise", + "allow": "Autoriser", + "deny": "Refuser", + "rememberChoice": "Se souvenir de ce choix", + "granted": "Autorisation accordée", + "denied": "Autorisation refusée", + "type": { + "read": "Autorisations de lecture", + "write": "Autorisations d'écriture", + "all": "Toutes les autorisations" + }, + "description": { + "all": "Autoriser '{ToolName}' de '{servername}' pour effectuer des opérations de lecture et d'écriture?", + "read": "Autoriser '{ToolName}' de '{servername}' pour effectuer des opérations de lecture?", + "write": "Autoriser '{ToolName}' de '{servername}' pour effectuer des opérations d'écriture?" + } } } diff --git a/src/renderer/src/i18n/ja-JP/components.json b/src/renderer/src/i18n/ja-JP/components.json index 30d42b9cd..29c22c9d8 100644 --- a/src/renderer/src/i18n/ja-JP/components.json +++ b/src/renderer/src/i18n/ja-JP/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "続く", "continued": "続く" + }, + "messageBlockPermissionRequest": { + "title": "権限が必要", + "allow": "許可", + "deny": "拒否", + "rememberChoice": "この選択を記憶する", + "granted": "権限が許可されました", + "denied": "権限が拒否されました", + "type": { + "read": "読み取り権限", + "write": "書き込み権限", + "all": "全ての権限" + }, + "description": { + "all": "'{servername}'の '{toolname}'を読み取り操作を実行することを許可しますか?", + "read": "'{servername}'の '{toolname}'を読み取り操作を実行することを許可しますか?", + "write": "'{servername}'の '{toolname}'を許可しますか?" + } } } diff --git a/src/renderer/src/i18n/ko-KR/components.json b/src/renderer/src/i18n/ko-KR/components.json index 0fae1d2b5..3abebfff9 100644 --- a/src/renderer/src/i18n/ko-KR/components.json +++ b/src/renderer/src/i18n/ko-KR/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "계속", "continued": "계속됨" + }, + "messageBlockPermissionRequest": { + "title": "권한 필요", + "allow": "허용", + "deny": "거부", + "rememberChoice": "이 선택을 기억하기", + "granted": "권한 부여됨", + "denied": "권한 거부됨", + "type": { + "read": "읽기 권한", + "write": "쓰기 권한", + "all": "모든 권한" + }, + "description": { + "all": "읽기 및 쓰기 작업을 수행하려면 '{servername}'의 '{toolname}'을 허용합니까?", + "read": "읽기 작업을 수행하려면 '{servername}'의 '{toolname}'을 허용합니까?", + "write": "쓰기 작업을 수행하려면 '{servername}'의 '{toolname}'을 허용합니까?" + } } } diff --git a/src/renderer/src/i18n/ru-RU/components.json b/src/renderer/src/i18n/ru-RU/components.json index bd10e6888..30d260a7d 100644 --- a/src/renderer/src/i18n/ru-RU/components.json +++ b/src/renderer/src/i18n/ru-RU/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "продолжать", "continued": "Продолжать" + }, + "messageBlockPermissionRequest": { + "title": "Требуется разрешение", + "allow": "Разрешить", + "deny": "Отклонить", + "rememberChoice": "Запомнить этот выбор", + "granted": "Разрешение предоставлено", + "denied": "Разрешение отклонено", + "type": { + "read": "Разрешения на чтение", + "write": "Разрешения на запись", + "all": "Полные разрешения" + }, + "description": { + "all": "Разрешить '{ToolName}' of '{ServerName}' для выполнения операций чтения и записи?", + "read": "Разрешить '{ToolName}' of '{ServerName}' для выполнения операций чтения?", + "write": "Разрешить '{ToolName}' of '{ServerName}' для выполнения операций записи?" + } } } diff --git a/src/renderer/src/i18n/zh-CN/components.json b/src/renderer/src/i18n/zh-CN/components.json index cd4ad5af6..c4c660c02 100644 --- a/src/renderer/src/i18n/zh-CN/components.json +++ b/src/renderer/src/i18n/zh-CN/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "继续", "continued": "已继续" + }, + "messageBlockPermissionRequest": { + "title": "需要权限", + "allow": "允许", + "deny": "拒绝", + "rememberChoice": "记住此选择", + "granted": "权限已授予", + "denied": "权限已拒绝", + "type": { + "read": "读取权限", + "write": "写入权限", + "all": "完全权限" + }, + "description": { + "read": "允许 '{serverName}' 的 '{toolName}' 执行读取操作?", + "write": "允许 '{serverName}' 的 '{toolName}' 执行写入操作?", + "all": "允许 '{serverName}' 的 '{toolName}' 执行读取和写入操作?" + } } } diff --git a/src/renderer/src/i18n/zh-HK/components.json b/src/renderer/src/i18n/zh-HK/components.json index 726cc88f3..83af4729c 100644 --- a/src/renderer/src/i18n/zh-HK/components.json +++ b/src/renderer/src/i18n/zh-HK/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "繼續", "continued": "已繼續" + }, + "messageBlockPermissionRequest": { + "title": "需要權限", + "allow": "允許", + "deny": "拒絕", + "rememberChoice": "記住此選擇", + "granted": "權限已授予", + "denied": "權限已拒絕", + "type": { + "read": "讀取權限", + "write": "寫入權限", + "all": "完整權限" + }, + "description": { + "all": "允許 '{serverName}' 的 '{toolName}' 執行讀取和寫入操作?", + "read": "允許 '{serverName}' 的 '{toolName}' 執行讀取操作?", + "write": "允許 '{serverName}' 的 '{toolName}' 執行寫入操作?" + } } } diff --git a/src/renderer/src/i18n/zh-TW/components.json b/src/renderer/src/i18n/zh-TW/components.json index bfaeb1ddf..9556f0f03 100644 --- a/src/renderer/src/i18n/zh-TW/components.json +++ b/src/renderer/src/i18n/zh-TW/components.json @@ -14,5 +14,23 @@ "messageBlockAction": { "continue": "繼續", "continued": "已繼續" + }, + "messageBlockPermissionRequest": { + "title": "需要權限", + "allow": "允許", + "deny": "拒絕", + "rememberChoice": "記住此選擇", + "granted": "權限已授予", + "denied": "權限已拒絕", + "type": { + "read": "讀取權限", + "write": "寫入權限", + "all": "完整權限" + }, + "description": { + "all": "允許 '{serverName}' 的 '{toolName}' 執行讀取和寫入操作?", + "read": "允許 '{serverName}' 的 '{toolName}' 執行讀取操作?", + "write": "允許 '{serverName}' 的 '{toolName}' 執行寫入操作?" + } } } diff --git a/src/shared/chat.d.ts b/src/shared/chat.d.ts index a9d53f2f9..53a50012a 100644 --- a/src/shared/chat.d.ts +++ b/src/shared/chat.d.ts @@ -89,11 +89,21 @@ export type AssistantMessageBlock = { | 'error' | 'tool_call' | 'action' + | 'tool_call_permission' // NEW: Dedicated permission request block type | 'image' | 'artifact-thinking' content?: string extra?: Record - status: 'success' | 'loading' | 'cancel' | 'error' | 'reading' | 'optimizing' | 'pending' + status: + | 'success' + | 'loading' + | 'cancel' + | 'error' + | 'reading' + | 'optimizing' + | 'pending' + | 'granted' + | 'denied' timestamp: number artifact?: { identifier: string diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index c09548b4c..289148a5b 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -626,6 +626,15 @@ export interface IThreadPresenter { continueStreamCompletion(conversationId: string, queryMsgId: string): Promise toggleConversationPinned(conversationId: string, isPinned: boolean): Promise findTabForConversation(conversationId: string): Promise + + // Permission handling + handlePermissionResponse( + messageId: string, + toolCallId: string, + granted: boolean, + permissionType: 'read' | 'write' | 'all', + remember?: boolean + ): Promise } export type MESSAGE_STATUS = 'sent' | 'pending' | 'error' @@ -927,6 +936,17 @@ export interface MCPToolResponse { /** 当使用兼容模式时,可能直接返回工具结果 */ toolResult?: unknown + + /** 是否需要权限 */ + requiresPermission?: boolean + + /** 权限请求信息 */ + permissionRequest?: { + toolName: string + serverName: string + permissionType: 'read' | 'write' | 'all' + description: string + } } /** 内容项类型 */ @@ -987,6 +1007,13 @@ export interface IMCPPresenter { setMcpEnabled(enabled: boolean): Promise getMcpEnabled(): Promise resetToDefaultServers(): Promise + + // Permission management + grantPermission( + serverName: string, + permissionType: 'read' | 'write' | 'all', + remember?: boolean + ): Promise } export interface IDeeplinkPresenter { @@ -1101,7 +1128,16 @@ export interface LLMAgentEventData { tool_call_server_description?: string tool_call_response_raw?: any - tool_call?: 'start' | 'running' | 'end' | 'error' | 'update' + tool_call?: 'start' | 'running' | 'end' | 'error' | 'update' | 'permission-required' + + // Permission request related fields + permission_request?: { + toolName: string + serverName: string + permissionType: 'read' | 'write' | 'all' + description: string + } + totalUsage?: { prompt_tokens: number completion_tokens: number