mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
fix: add missing execution state for approved tool permissions (#11394)
This commit is contained in:
parent
c1f1d7996d
commit
ebfb1c5abf
@ -2,7 +2,14 @@
|
|||||||
import { EventEmitter } from 'node:events'
|
import { EventEmitter } from 'node:events'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
|
|
||||||
import type { CanUseTool, McpHttpServerConfig, Options, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
import type {
|
||||||
|
CanUseTool,
|
||||||
|
HookCallback,
|
||||||
|
McpHttpServerConfig,
|
||||||
|
Options,
|
||||||
|
PreToolUseHookInput,
|
||||||
|
SDKMessage
|
||||||
|
} from '@anthropic-ai/claude-agent-sdk'
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk'
|
import { query } from '@anthropic-ai/claude-agent-sdk'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { config as apiConfigService } from '@main/apiServer/config'
|
import { config as apiConfigService } from '@main/apiServer/config'
|
||||||
@ -157,6 +164,63 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preToolUseHook: HookCallback = async (input, toolUseID, options) => {
|
||||||
|
// Type guard to ensure we're handling PreToolUse event
|
||||||
|
if (input.hook_event_name !== 'PreToolUse') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookInput = input as PreToolUseHookInput
|
||||||
|
const toolName = hookInput.tool_name
|
||||||
|
|
||||||
|
logger.debug('PreToolUse hook triggered', {
|
||||||
|
session_id: hookInput.session_id,
|
||||||
|
tool_name: hookInput.tool_name,
|
||||||
|
tool_use_id: toolUseID,
|
||||||
|
tool_input: hookInput.tool_input,
|
||||||
|
cwd: hookInput.cwd,
|
||||||
|
permission_mode: hookInput.permission_mode,
|
||||||
|
autoAllowTools: autoAllowTools
|
||||||
|
})
|
||||||
|
|
||||||
|
if (options?.signal?.aborted) {
|
||||||
|
logger.debug('PreToolUse hook signal already aborted; skipping tool use', {
|
||||||
|
tool_name: hookInput.tool_name
|
||||||
|
})
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle auto approved tools since it never triggers canUseTool
|
||||||
|
const normalizedToolName = normalizeToolName(toolName)
|
||||||
|
if (toolUseID) {
|
||||||
|
const bypassAll = input.permission_mode === 'bypassPermissions'
|
||||||
|
const autoAllowed = autoAllowTools.has(toolName) || autoAllowTools.has(normalizedToolName)
|
||||||
|
if (bypassAll || autoAllowed) {
|
||||||
|
const namespacedToolCallId = buildNamespacedToolCallId(session.id, toolUseID)
|
||||||
|
logger.debug('handling auto approved tools', {
|
||||||
|
toolName,
|
||||||
|
normalizedToolName,
|
||||||
|
namespacedToolCallId,
|
||||||
|
permission_mode: input.permission_mode,
|
||||||
|
autoAllowTools
|
||||||
|
})
|
||||||
|
const isRecord = (v: unknown): v is Record<string, unknown> => {
|
||||||
|
return !!v && typeof v === 'object' && !Array.isArray(v)
|
||||||
|
}
|
||||||
|
const toolInput = isRecord(input.tool_input) ? input.tool_input : {}
|
||||||
|
|
||||||
|
await promptForToolApproval(toolName, toolInput, {
|
||||||
|
...options,
|
||||||
|
toolCallId: namespacedToolCallId,
|
||||||
|
autoApprove: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return to proceed without modification
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
// Build SDK options from parameters
|
// Build SDK options from parameters
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
abortController,
|
abortController,
|
||||||
@ -180,7 +244,14 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
permissionMode: session.configuration?.permission_mode,
|
permissionMode: session.configuration?.permission_mode,
|
||||||
maxTurns: session.configuration?.max_turns,
|
maxTurns: session.configuration?.max_turns,
|
||||||
allowedTools: session.allowed_tools,
|
allowedTools: session.allowed_tools,
|
||||||
canUseTool
|
canUseTool,
|
||||||
|
hooks: {
|
||||||
|
PreToolUse: [
|
||||||
|
{
|
||||||
|
hooks: [preToolUseHook]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.accessible_paths.length > 1) {
|
if (session.accessible_paths.length > 1) {
|
||||||
|
|||||||
@ -31,6 +31,7 @@ type PendingPermissionRequest = {
|
|||||||
abortListener?: () => void
|
abortListener?: () => void
|
||||||
originalInput: Record<string, unknown>
|
originalInput: Record<string, unknown>
|
||||||
toolName: string
|
toolName: string
|
||||||
|
toolCallId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RendererPermissionRequestPayload = {
|
type RendererPermissionRequestPayload = {
|
||||||
@ -45,6 +46,7 @@ type RendererPermissionRequestPayload = {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
expiresAt: number
|
expiresAt: number
|
||||||
suggestions: PermissionUpdate[]
|
suggestions: PermissionUpdate[]
|
||||||
|
autoApprove?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type RendererPermissionResultPayload = {
|
type RendererPermissionResultPayload = {
|
||||||
@ -52,6 +54,7 @@ type RendererPermissionResultPayload = {
|
|||||||
behavior: ToolPermissionBehavior
|
behavior: ToolPermissionBehavior
|
||||||
message?: string
|
message?: string
|
||||||
reason: 'response' | 'timeout' | 'aborted' | 'no-window'
|
reason: 'response' | 'timeout' | 'aborted' | 'no-window'
|
||||||
|
toolCallId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingRequests = new Map<string, PendingPermissionRequest>()
|
const pendingRequests = new Map<string, PendingPermissionRequest>()
|
||||||
@ -145,7 +148,8 @@ const finalizeRequest = (
|
|||||||
requestId,
|
requestId,
|
||||||
behavior: update.behavior,
|
behavior: update.behavior,
|
||||||
message: update.behavior === 'deny' ? update.message : undefined,
|
message: update.behavior === 'deny' ? update.message : undefined,
|
||||||
reason
|
reason,
|
||||||
|
toolCallId: pending.toolCallId
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatched = broadcastToRenderer(IpcChannel.AgentToolPermission_Result, resultPayload)
|
const dispatched = broadcastToRenderer(IpcChannel.AgentToolPermission_Result, resultPayload)
|
||||||
@ -210,6 +214,7 @@ const ensureIpcHandlersRegistered = () => {
|
|||||||
type PromptForToolApprovalOptions = {
|
type PromptForToolApprovalOptions = {
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
suggestions?: PermissionUpdate[]
|
suggestions?: PermissionUpdate[]
|
||||||
|
autoApprove?: boolean
|
||||||
|
|
||||||
// NOTICE: This ID is namespaced with session ID, not the raw SDK tool call ID.
|
// NOTICE: This ID is namespaced with session ID, not the raw SDK tool call ID.
|
||||||
// Format: `${sessionId}:${rawToolCallId}`, e.g., `session_123:WebFetch_0`
|
// Format: `${sessionId}:${rawToolCallId}`, e.g., `session_123:WebFetch_0`
|
||||||
@ -270,7 +275,8 @@ export async function promptForToolApproval(
|
|||||||
inputPreview,
|
inputPreview,
|
||||||
createdAt,
|
createdAt,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
suggestions: sanitizedSuggestions
|
suggestions: sanitizedSuggestions,
|
||||||
|
autoApprove: options.autoApprove
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultDenyUpdate: PermissionResult = { behavior: 'deny', message: 'Tool request aborted before user decision' }
|
const defaultDenyUpdate: PermissionResult = { behavior: 'deny', message: 'Tool request aborted before user decision' }
|
||||||
@ -299,7 +305,8 @@ export async function promptForToolApproval(
|
|||||||
timeout,
|
timeout,
|
||||||
originalInput: sanitizedInput,
|
originalInput: sanitizedInput,
|
||||||
toolName,
|
toolName,
|
||||||
signal: options?.signal
|
signal: options?.signal,
|
||||||
|
toolCallId: options.toolCallId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.signal) {
|
if (options?.signal) {
|
||||||
|
|||||||
@ -175,14 +175,46 @@ export function useAppInit() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window.electron?.ipcRenderer) return
|
if (!window.electron?.ipcRenderer) return
|
||||||
|
|
||||||
const requestListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionRequestPayload) => {
|
const requestListener = async (_event: Electron.IpcRendererEvent, payload: ToolPermissionRequestPayload) => {
|
||||||
logger.debug('Renderer received tool permission request', {
|
logger.debug('Renderer received tool permission request', {
|
||||||
requestId: payload.requestId,
|
requestId: payload.requestId,
|
||||||
toolName: payload.toolName,
|
toolName: payload.toolName,
|
||||||
expiresAt: payload.expiresAt,
|
expiresAt: payload.expiresAt,
|
||||||
suggestionCount: payload.suggestions.length
|
suggestionCount: payload.suggestions.length,
|
||||||
|
autoApprove: payload.autoApprove
|
||||||
})
|
})
|
||||||
dispatch(toolPermissionsActions.requestReceived(payload))
|
dispatch(toolPermissionsActions.requestReceived(payload))
|
||||||
|
|
||||||
|
// Auto-approve if requested
|
||||||
|
if (payload.autoApprove) {
|
||||||
|
logger.debug('Auto-approving tool permission request', {
|
||||||
|
requestId: payload.requestId,
|
||||||
|
toolName: payload.toolName
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(toolPermissionsActions.submissionSent({ requestId: payload.requestId, behavior: 'allow' }))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.api.agentTools.respondToPermission({
|
||||||
|
requestId: payload.requestId,
|
||||||
|
behavior: 'allow',
|
||||||
|
updatedInput: payload.input,
|
||||||
|
updatedPermissions: payload.suggestions
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response?.success) {
|
||||||
|
throw new Error('Auto-approval response rejected by main process')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Auto-approval acknowledged by main process', {
|
||||||
|
requestId: payload.requestId,
|
||||||
|
toolName: payload.toolName
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send auto-approval response', error as Error)
|
||||||
|
dispatch(toolPermissionsActions.submissionFailed({ requestId: payload.requestId }))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionResultPayload) => {
|
const resultListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionResultPayload) => {
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "Failed to send your decision. Please try again."
|
"sendFailed": "Failed to send your decision. Please try again."
|
||||||
},
|
},
|
||||||
|
"executing": "Executing...",
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
"inputPreview": "Tool input preview",
|
"inputPreview": "Tool input preview",
|
||||||
"pending": "Pending ({{seconds}}s)",
|
"pending": "Pending ({{seconds}}s)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "发送您的决定失败,请重试。"
|
"sendFailed": "发送您的决定失败,请重试。"
|
||||||
},
|
},
|
||||||
|
"executing": "正在执行...",
|
||||||
"expired": "已过期",
|
"expired": "已过期",
|
||||||
"inputPreview": "工具输入预览",
|
"inputPreview": "工具输入预览",
|
||||||
"pending": "等待中 ({{seconds}}秒)",
|
"pending": "等待中 ({{seconds}}秒)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "傳送您的決定失敗,請重試。"
|
"sendFailed": "傳送您的決定失敗,請重試。"
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "已過期",
|
"expired": "已過期",
|
||||||
"inputPreview": "工具輸入預覽",
|
"inputPreview": "工具輸入預覽",
|
||||||
"pending": "等待中 ({{seconds}}秒)",
|
"pending": "等待中 ({{seconds}}秒)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "Ihre Entscheidung konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
|
"sendFailed": "Ihre Entscheidung konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "Abgelaufen",
|
"expired": "Abgelaufen",
|
||||||
"inputPreview": "Vorschau der Werkzeugeingabe",
|
"inputPreview": "Vorschau der Werkzeugeingabe",
|
||||||
"pending": "Ausstehend ({{seconds}}s)",
|
"pending": "Ausstehend ({{seconds}}s)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "Αποτυχία αποστολής της απόφασής σας. Προσπαθήστε ξανά."
|
"sendFailed": "Αποτυχία αποστολής της απόφασής σας. Προσπαθήστε ξανά."
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "Ληγμένο",
|
"expired": "Ληγμένο",
|
||||||
"inputPreview": "Προεπισκόπηση εισόδου εργαλείου",
|
"inputPreview": "Προεπισκόπηση εισόδου εργαλείου",
|
||||||
"pending": "Εκκρεμεί ({{seconds}}δ)",
|
"pending": "Εκκρεμεί ({{seconds}}δ)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "No se pudo enviar tu decisión. Por favor, inténtalo de nuevo."
|
"sendFailed": "No se pudo enviar tu decisión. Por favor, inténtalo de nuevo."
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "Caducado",
|
"expired": "Caducado",
|
||||||
"inputPreview": "Vista previa de entrada de herramienta",
|
"inputPreview": "Vista previa de entrada de herramienta",
|
||||||
"pending": "Pendiente ({{seconds}}s)",
|
"pending": "Pendiente ({{seconds}}s)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "Échec de l'envoi de votre décision. Veuillez réessayer."
|
"sendFailed": "Échec de l'envoi de votre décision. Veuillez réessayer."
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "Expiré",
|
"expired": "Expiré",
|
||||||
"inputPreview": "Aperçu de l'entrée de l'outil",
|
"inputPreview": "Aperçu de l'entrée de l'outil",
|
||||||
"pending": "En attente ({{seconds}}s)",
|
"pending": "En attente ({{seconds}}s)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "決定の送信に失敗しました。もう一度お試しください。"
|
"sendFailed": "決定の送信に失敗しました。もう一度お試しください。"
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "期限切れ",
|
"expired": "期限切れ",
|
||||||
"inputPreview": "ツール入力プレビュー",
|
"inputPreview": "ツール入力プレビュー",
|
||||||
"pending": "保留中({{seconds}}秒)",
|
"pending": "保留中({{seconds}}秒)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "Falha ao enviar sua decisão. Por favor, tente novamente."
|
"sendFailed": "Falha ao enviar sua decisão. Por favor, tente novamente."
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "Expirado",
|
"expired": "Expirado",
|
||||||
"inputPreview": "Pré-visualização da entrada da ferramenta",
|
"inputPreview": "Pré-visualização da entrada da ferramenta",
|
||||||
"pending": "Pendente ({{seconds}}s)",
|
"pending": "Pendente ({{seconds}}s)",
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"sendFailed": "Не удалось отправить ваше решение. Попробуйте ещё раз."
|
"sendFailed": "Не удалось отправить ваше решение. Попробуйте ещё раз."
|
||||||
},
|
},
|
||||||
|
"executing": "[to be translated]:Executing...",
|
||||||
"expired": "Истёк",
|
"expired": "Истёк",
|
||||||
"inputPreview": "Предварительный просмотр ввода инструмента",
|
"inputPreview": "Предварительный просмотр ввода инструмента",
|
||||||
"pending": "Ожидание ({{seconds}}с)",
|
"pending": "Ожидание ({{seconds}}с)",
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { loggerService } from '@logger'
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { selectPendingPermission, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
import { selectPendingPermission, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
||||||
import type { NormalToolResponse } from '@renderer/types'
|
import type { NormalToolResponse } from '@renderer/types'
|
||||||
import { Button } from 'antd'
|
import { Button, Spin } from 'antd'
|
||||||
import { ChevronDown, CirclePlay, CircleX } from 'lucide-react'
|
import { ChevronDown, CirclePlay, CircleX } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -52,6 +52,7 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
|||||||
const isSubmittingAllow = request?.status === 'submitting-allow'
|
const isSubmittingAllow = request?.status === 'submitting-allow'
|
||||||
const isSubmittingDeny = request?.status === 'submitting-deny'
|
const isSubmittingDeny = request?.status === 'submitting-deny'
|
||||||
const isSubmitting = isSubmittingAllow || isSubmittingDeny
|
const isSubmitting = isSubmittingAllow || isSubmittingDeny
|
||||||
|
const isInvoking = request?.status === 'invoking'
|
||||||
|
|
||||||
const handleDecision = useCallback(
|
const handleDecision = useCallback(
|
||||||
async (
|
async (
|
||||||
@ -113,6 +114,53 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isInvoking) {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-xl rounded-xl border border-default-200 bg-default-100 px-4 py-3 shadow-sm">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Spin size="small" />
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="font-semibold text-default-700 text-sm">{request.toolName}</div>
|
||||||
|
<div className="text-default-500 text-xs">{t('agent.toolPermission.executing')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{request.inputPreview && (
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
aria-label={
|
||||||
|
showDetails
|
||||||
|
? t('agent.toolPermission.aria.hideDetails')
|
||||||
|
: t('agent.toolPermission.aria.showDetails')
|
||||||
|
}
|
||||||
|
className="h-8 text-default-600 transition-colors hover:bg-default-200/50 hover:text-default-800"
|
||||||
|
onClick={() => setShowDetails((value) => !value)}
|
||||||
|
icon={<ChevronDown className={`transition-transform ${showDetails ? 'rotate-180' : ''}`} size={16} />}
|
||||||
|
variant="text"
|
||||||
|
style={{ backgroundColor: 'transparent' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showDetails && request.inputPreview && (
|
||||||
|
<div className="flex flex-col gap-3 border-default-200 border-t pt-3">
|
||||||
|
<div className="rounded-md border border-default-200 bg-default-100 p-3">
|
||||||
|
<p className="mb-2 font-medium text-default-400 text-xs uppercase tracking-wide">
|
||||||
|
{t('agent.toolPermission.inputPreview')}
|
||||||
|
</p>
|
||||||
|
<div className="max-h-[192px] overflow-auto font-mono text-xs">
|
||||||
|
<pre className="whitespace-pre-wrap break-all p-2 text-left">{request.inputPreview}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-xl rounded-xl border border-default-200 bg-default-100 px-4 py-3 shadow-sm">
|
<div className="w-full max-w-xl rounded-xl border border-default-200 bg-default-100 px-4 py-3 shadow-sm">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
|||||||
@ -42,7 +42,8 @@ export const createCallbacks = (deps: CallbacksDependencies) => {
|
|||||||
|
|
||||||
const toolCallbacks = createToolCallbacks({
|
const toolCallbacks = createToolCallbacks({
|
||||||
blockManager,
|
blockManager,
|
||||||
assistantMsgId
|
assistantMsgId,
|
||||||
|
dispatch
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageCallbacks = createImageCallbacks({
|
const imageCallbacks = createImageCallbacks({
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import type { AppDispatch } from '@renderer/store'
|
||||||
|
import { toolPermissionsActions } from '@renderer/store/toolPermissions'
|
||||||
import type { MCPToolResponse } from '@renderer/types'
|
import type { MCPToolResponse } from '@renderer/types'
|
||||||
import { WebSearchSource } from '@renderer/types'
|
import { WebSearchSource } from '@renderer/types'
|
||||||
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
||||||
@ -12,10 +14,11 @@ const logger = loggerService.withContext('ToolCallbacks')
|
|||||||
interface ToolCallbacksDependencies {
|
interface ToolCallbacksDependencies {
|
||||||
blockManager: BlockManager
|
blockManager: BlockManager
|
||||||
assistantMsgId: string
|
assistantMsgId: string
|
||||||
|
dispatch: AppDispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createToolCallbacks = (deps: ToolCallbacksDependencies) => {
|
export const createToolCallbacks = (deps: ToolCallbacksDependencies) => {
|
||||||
const { blockManager, assistantMsgId } = deps
|
const { blockManager, assistantMsgId, dispatch } = deps
|
||||||
|
|
||||||
// 内部维护的状态
|
// 内部维护的状态
|
||||||
const toolCallIdToBlockIdMap = new Map<string, string>()
|
const toolCallIdToBlockIdMap = new Map<string, string>()
|
||||||
@ -53,6 +56,9 @@ export const createToolCallbacks = (deps: ToolCallbacksDependencies) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onToolCallComplete: (toolResponse: MCPToolResponse) => {
|
onToolCallComplete: (toolResponse: MCPToolResponse) => {
|
||||||
|
if (toolResponse?.id) {
|
||||||
|
dispatch(toolPermissionsActions.removeByToolCallId({ toolCallId: toolResponse.id }))
|
||||||
|
}
|
||||||
const existingBlockId = toolCallIdToBlockIdMap.get(toolResponse.id)
|
const existingBlockId = toolCallIdToBlockIdMap.get(toolResponse.id)
|
||||||
toolCallIdToBlockIdMap.delete(toolResponse.id)
|
toolCallIdToBlockIdMap.delete(toolResponse.id)
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type ToolPermissionRequestPayload = {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
expiresAt: number
|
expiresAt: number
|
||||||
suggestions: PermissionUpdate[]
|
suggestions: PermissionUpdate[]
|
||||||
|
autoApprove?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolPermissionResultPayload = {
|
export type ToolPermissionResultPayload = {
|
||||||
@ -21,9 +22,10 @@ export type ToolPermissionResultPayload = {
|
|||||||
behavior: 'allow' | 'deny'
|
behavior: 'allow' | 'deny'
|
||||||
message?: string
|
message?: string
|
||||||
reason: 'response' | 'timeout' | 'aborted' | 'no-window'
|
reason: 'response' | 'timeout' | 'aborted' | 'no-window'
|
||||||
|
toolCallId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolPermissionStatus = 'pending' | 'submitting-allow' | 'submitting-deny'
|
export type ToolPermissionStatus = 'pending' | 'submitting-allow' | 'submitting-deny' | 'invoking'
|
||||||
|
|
||||||
export type ToolPermissionEntry = ToolPermissionRequestPayload & {
|
export type ToolPermissionEntry = ToolPermissionRequestPayload & {
|
||||||
status: ToolPermissionStatus
|
status: ToolPermissionStatus
|
||||||
@ -61,8 +63,24 @@ const toolPermissionsSlice = createSlice({
|
|||||||
entry.status = 'pending'
|
entry.status = 'pending'
|
||||||
},
|
},
|
||||||
requestResolved: (state, action: PayloadAction<ToolPermissionResultPayload>) => {
|
requestResolved: (state, action: PayloadAction<ToolPermissionResultPayload>) => {
|
||||||
const { requestId } = action.payload
|
const { requestId, behavior } = action.payload
|
||||||
delete state.requests[requestId]
|
const entry = state.requests[requestId]
|
||||||
|
|
||||||
|
if (!entry) return
|
||||||
|
|
||||||
|
if (behavior === 'allow') {
|
||||||
|
entry.status = 'invoking'
|
||||||
|
} else {
|
||||||
|
delete state.requests[requestId]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeByToolCallId: (state, action: PayloadAction<{ toolCallId: string }>) => {
|
||||||
|
const { toolCallId } = action.payload
|
||||||
|
|
||||||
|
const entryId = Object.keys(state.requests).find((key) => state.requests[key]?.toolCallId === toolCallId)
|
||||||
|
if (entryId) {
|
||||||
|
delete state.requests[entryId]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clearAll: (state) => {
|
clearAll: (state) => {
|
||||||
state.requests = {}
|
state.requests = {}
|
||||||
@ -73,8 +91,8 @@ const toolPermissionsSlice = createSlice({
|
|||||||
export const toolPermissionsActions = toolPermissionsSlice.actions
|
export const toolPermissionsActions = toolPermissionsSlice.actions
|
||||||
|
|
||||||
export const selectActiveToolPermission = (state: ToolPermissionsState): ToolPermissionEntry | null => {
|
export const selectActiveToolPermission = (state: ToolPermissionsState): ToolPermissionEntry | null => {
|
||||||
const activeEntries = Object.values(state.requests).filter(
|
const activeEntries = Object.values(state.requests).filter((entry) =>
|
||||||
(entry) => entry.status === 'pending' || entry.status === 'submitting-allow' || entry.status === 'submitting-deny'
|
['pending', 'submitting-allow', 'submitting-deny', 'invoking'].includes(entry.status)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (activeEntries.length === 0) return null
|
if (activeEntries.length === 0) return null
|
||||||
@ -89,9 +107,7 @@ export const selectPendingPermission = (
|
|||||||
): ToolPermissionEntry | undefined => {
|
): ToolPermissionEntry | undefined => {
|
||||||
const activeEntries = Object.values(state.requests)
|
const activeEntries = Object.values(state.requests)
|
||||||
.filter((entry) => entry.toolCallId === toolCallId)
|
.filter((entry) => entry.toolCallId === toolCallId)
|
||||||
.filter(
|
.filter((entry) => ['pending', 'submitting-allow', 'submitting-deny', 'invoking'].includes(entry.status))
|
||||||
(entry) => entry.status === 'pending' || entry.status === 'submitting-allow' || entry.status === 'submitting-deny'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (activeEntries.length === 0) return undefined
|
if (activeEntries.length === 0) return undefined
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user