Merge pull request #1 from GhostDragon124/fix/eliminate-as-any

fix: 消除 8 个 as any — MCP 通知、结构化输出、流事件的类型安全修复
This commit is contained in:
James F 2026-06-07 16:47:38 +08:00 committed by GitHub
commit faa03d37b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 79 additions and 41 deletions

View File

@ -146,7 +146,7 @@ async function main(): Promise<void> {
shutdown1PEventLogging, shutdown1PEventLogging,
logForDebugging, logForDebugging,
registerPermissionHandler(server, handler) { registerPermissionHandler(server, handler) {
server.setNotificationHandler(ChannelPermissionRequestNotificationSchema() as any, async notification => server.setNotificationHandler(ChannelPermissionRequestNotificationSchema(), async notification =>
handler(notification.params), handler(notification.params),
); );
}, },

View File

@ -3,9 +3,10 @@ import { logEvent } from 'src/services/analytics/index.js'
import { z } from 'zod/v4' import { z } from 'zod/v4'
import type { MCPServerConnection } from '../services/mcp/types.js' import type { MCPServerConnection } from '../services/mcp/types.js'
import { getConnectedIdeClient } from '../utils/ide.js' import { getConnectedIdeClient } from '../utils/ide.js'
import type { AnyObjectSchema } from '@modelcontextprotocol/sdk/server/zod-compat.js'
import { lazySchema } from '../utils/lazySchema.js' import { lazySchema } from '../utils/lazySchema.js'
const LogEventSchema = lazySchema(() => const LogEventSchema: () => AnyObjectSchema = lazySchema(() =>
z.object({ z.object({
method: z.literal('log_event'), method: z.literal('log_event'),
params: z.object({ params: z.object({
@ -27,7 +28,7 @@ export function useIdeLogging(mcpClients: MCPServerConnection[]): void {
if (ideClient) { if (ideClient) {
// Register the log event handler // Register the log event handler
ideClient.client.setNotificationHandler( ideClient.client.setNotificationHandler(
LogEventSchema() as any, LogEventSchema(),
notification => { notification => {
const { eventName, eventData } = notification.params const { eventName, eventData } = notification.params
logEvent( logEvent(

View File

@ -6,6 +6,7 @@ import type {
MCPServerConnection, MCPServerConnection,
} from '../services/mcp/types.js' } from '../services/mcp/types.js'
import { getConnectedIdeClient } from '../utils/ide.js' import { getConnectedIdeClient } from '../utils/ide.js'
import type { AnyObjectSchema } from '@modelcontextprotocol/sdk/server/zod-compat.js'
import { lazySchema } from '../utils/lazySchema.js' import { lazySchema } from '../utils/lazySchema.js'
export type SelectionPoint = { export type SelectionPoint = {
line: number line: number
@ -29,7 +30,7 @@ export type IDESelection = {
} }
// Define the selection changed notification schema // Define the selection changed notification schema
const SelectionChangedSchema = lazySchema(() => const SelectionChangedSchema: () => AnyObjectSchema = lazySchema(() =>
z.object({ z.object({
method: z.literal('selection_changed'), method: z.literal('selection_changed'),
params: z.object({ params: z.object({
@ -110,7 +111,7 @@ export function useIdeSelection(
// Register notification handler for selection_changed events // Register notification handler for selection_changed events
ideClient.client.setNotificationHandler( ideClient.client.setNotificationHandler(
SelectionChangedSchema() as any, SelectionChangedSchema(),
notification => { notification => {
if (currentIDERef.current !== ideClient) { if (currentIDERef.current !== ideClient) {
return return

View File

@ -6,11 +6,12 @@ import { callIdeRpc } from '../services/mcp/client.js';
import type { ConnectedMCPServer, MCPServerConnection } from '../services/mcp/types.js'; import type { ConnectedMCPServer, MCPServerConnection } from '../services/mcp/types.js';
import type { PermissionMode } from '../types/permissions.js'; import type { PermissionMode } from '../types/permissions.js';
import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, isTrackedClaudeInChromeTabId } from '../utils/claudeInChrome/common.js'; import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, isTrackedClaudeInChromeTabId } from '../utils/claudeInChrome/common.js';
import type { AnyObjectSchema } from '@modelcontextprotocol/sdk/server/zod-compat.js';
import { lazySchema } from '../utils/lazySchema.js'; import { lazySchema } from '../utils/lazySchema.js';
import { enqueuePendingNotification } from '../utils/messageQueueManager.js'; import { enqueuePendingNotification } from '../utils/messageQueueManager.js';
// Schema for the prompt notification from Chrome extension (JSON-RPC 2.0 format) // Schema for the prompt notification from Chrome extension (JSON-RPC 2.0 format)
const ClaudeInChromePromptNotificationSchema = lazySchema(() => const ClaudeInChromePromptNotificationSchema: () => AnyObjectSchema = lazySchema(() =>
z.object({ z.object({
method: z.literal('notifications/message'), method: z.literal('notifications/message'),
params: z.object({ params: z.object({
@ -48,7 +49,7 @@ export function usePromptsFromClaudeInChrome(
} }
if (mcpClient) { if (mcpClient) {
mcpClient.client.setNotificationHandler(ClaudeInChromePromptNotificationSchema() as any, notification => { mcpClient.client.setNotificationHandler(ClaudeInChromePromptNotificationSchema(), notification => {
if (mcpClientRef.current !== mcpClient) { if (mcpClientRef.current !== mcpClient) {
return; return;
} }

View File

@ -17,6 +17,7 @@
*/ */
import type { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js' import type { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js'
import type { AnyObjectSchema } from '@modelcontextprotocol/sdk/server/zod-compat.js'
import { z } from 'zod/v4' import { z } from 'zod/v4'
import { type ChannelEntry, getAllowedChannels } from '../../bootstrap/state.js' import { type ChannelEntry, getAllowedChannels } from '../../bootstrap/state.js'
import { CHANNEL_TAG } from '../../constants/xml.js' import { CHANNEL_TAG } from '../../constants/xml.js'
@ -96,23 +97,24 @@ export type ChannelPermissionRequestParams = {
} }
} }
export const ChannelPermissionRequestNotificationSchema = lazySchema(() => export const ChannelPermissionRequestNotificationSchema: () => AnyObjectSchema =
z.object({ lazySchema(() =>
method: z.literal(CHANNEL_PERMISSION_REQUEST_METHOD), z.object({
params: z.object({ method: z.literal(CHANNEL_PERMISSION_REQUEST_METHOD),
request_id: z.string(), params: z.object({
tool_name: z.string(), request_id: z.string(),
description: z.string(), tool_name: z.string(),
input_preview: z.string(), description: z.string(),
channel_context: z input_preview: z.string(),
.object({ channel_context: z
source_server: z.string().optional(), .object({
chat_id: z.string().optional(), source_server: z.string().optional(),
}) chat_id: z.string().optional(),
.optional(), })
.optional(),
}),
}), }),
}), )
)
/** /**
* Meta keys become XML attribute NAMES a crafted key like * Meta keys become XML attribute NAMES a crafted key like

View File

@ -20,10 +20,14 @@ import {
} from '../services/analytics/index.js' } from '../services/analytics/index.js'
import { accumulateUsage, updateUsage } from '../services/api/claude.js' import { accumulateUsage, updateUsage } from '../services/api/claude.js'
import { EMPTY_USAGE, type NonNullableUsage } from '@ant/model-provider' import { EMPTY_USAGE, type NonNullableUsage } from '@ant/model-provider'
import type {
BetaRawMessageDeltaEvent,
BetaRawMessageStreamEvent,
} from '@anthropic-ai/sdk/resources/beta/messages/messages.js'
import type { ToolUseContext } from '../Tool.js' import type { ToolUseContext } from '../Tool.js'
import type { AgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js' import type { AgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import type { AgentId } from '../types/ids.js' import type { AgentId } from '../types/ids.js'
import type { Message } from '../types/message.js' import type { Message, StreamEvent } from '../types/message.js'
import { createChildAbortController } from './abortController.js' import { createChildAbortController } from './abortController.js'
import { logForDebugging } from './debug.js' import { logForDebugging } from './debug.js'
import { cloneFileStateCache } from './fileStateCache.js' import { cloneFileStateCache } from './fileStateCache.js'
@ -492,6 +496,24 @@ export function createSubagentContext(
* }) * })
* ``` * ```
*/ */
type StreamEventMessage = StreamEvent & {
type: 'stream_event'
event: BetaRawMessageStreamEvent
}
function isMessageDeltaStreamEvent(
message: Message | StreamEvent,
): message is StreamEventMessage & { event: BetaRawMessageDeltaEvent } {
return (
message.type === 'stream_event' &&
typeof (message as StreamEventMessage).event === 'object' &&
(message as StreamEventMessage).event !== null &&
'type' in (message as StreamEventMessage).event &&
(message as StreamEventMessage).event.type === 'message_delta'
)
}
export async function runForkedAgent({ export async function runForkedAgent({
promptMessages, promptMessages,
cacheSafeParams, cacheSafeParams,
@ -562,15 +584,8 @@ export async function runForkedAgent({
})) { })) {
// Extract real usage from message_delta stream events (final usage per API call) // Extract real usage from message_delta stream events (final usage per API call)
if (message.type === 'stream_event') { if (message.type === 'stream_event') {
if ( if (isMessageDeltaStreamEvent(message)) {
'event' in message && const turnUsage = updateUsage({ ...EMPTY_USAGE }, message.event.usage)
(message as any).event?.type === 'message_delta' &&
(message as any).event.usage
) {
const turnUsage = updateUsage(
{ ...EMPTY_USAGE },
(message as any).event.usage,
)
totalUsage = accumulateUsage(totalUsage, turnUsage) totalUsage = accumulateUsage(totalUsage, turnUsage)
} }
continue continue

View File

@ -8,7 +8,12 @@ import { type Tool, toolMatchesName } from '../../Tool.js'
import { SYNTHETIC_OUTPUT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SyntheticOutputTool/SyntheticOutputTool.js' import { SYNTHETIC_OUTPUT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SyntheticOutputTool/SyntheticOutputTool.js'
import { ALL_AGENT_DISALLOWED_TOOLS } from '../../tools.js' import { ALL_AGENT_DISALLOWED_TOOLS } from '../../tools.js'
import { asAgentId } from '../../types/ids.js' import { asAgentId } from '../../types/ids.js'
import type { Message } from '../../types/message.js' import type {
AttachmentMessage,
Message,
RequestStartEvent,
StreamEvent,
} from '../../types/message.js'
import { createAbortController } from '../abortController.js' import { createAbortController } from '../abortController.js'
import { createAttachmentMessage } from '../attachments.js' import { createAttachmentMessage } from '../attachments.js'
import { createCombinedAbortSignal } from '../combinedAbortSignal.js' import { createCombinedAbortSignal } from '../combinedAbortSignal.js'
@ -30,6 +35,24 @@ import {
} from './hookHelpers.js' } from './hookHelpers.js'
import { clearSessionHooks } from './sessionHooks.js' import { clearSessionHooks } from './sessionHooks.js'
type QueryMessage = Message | StreamEvent | RequestStartEvent
type StructuredOutputAttachment = {
type: 'structured_output'
data: unknown
[key: string]: unknown
}
type StructuredOutputAttachmentMessage =
AttachmentMessage<StructuredOutputAttachment>
function isStructuredOutputAttachmentMessage(
message: QueryMessage,
): message is StructuredOutputAttachmentMessage {
if (message.type !== 'attachment') return false
return (message as Message).attachment?.type === 'structured_output'
}
/** /**
* Execute an agent-based hook using a multi-turn LLM query * Execute an agent-based hook using a multi-turn LLM query
*/ */
@ -209,13 +232,8 @@ When done, return your result using the ${SYNTHETIC_OUTPUT_TOOL_NAME} tool with:
} }
// Check for structured output in attachments // Check for structured output in attachments
if ( if (isStructuredOutputAttachmentMessage(message)) {
message.type === 'attachment' && const parsed = hookResponseSchema().safeParse(message.attachment.data)
(message as any).attachment.type === 'structured_output'
) {
const parsed = hookResponseSchema().safeParse(
(message as any).attachment.data,
)
if (parsed.success) { if (parsed.success) {
structuredOutputResult = parsed.data structuredOutputResult = parsed.data
logForDebugging( logForDebugging(