mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 16:55:51 +00:00
refactor: 拆分 3 个过大 ACP 文件为模块化子文件(每个 <500 行)
通过 4 阶段 workflow(分析 → 计划 → 重构 → 验证)将 3 个超大的 ACP 源文件拆分为 28 个模块化子文件,每个均严格小于 500 行,且完整保留 所有公共 API(barrel 模式重导出)。 变更概要: - packages/acp-link/src/server.ts: 1800 → 20 行(barrel),新增 11 个子模块 (server/types、payload-decode、permission-mode、runtime-state、dispatch、 handlers-agent、handlers-session、acp-client、client-send、start-server、 testing-internals) - src/services/acp/agent.ts: 1297 → 33 行(barrel),新增 9 个子模块 (agent/AcpAgent、sessionTypes、permissionMode、configOptions、promptQueue、 internalAccessors、createSessionMethod、sessionLifecycle、promptFlow) - src/services/acp/bridge.ts: 1516 → 29 行(barrel),新增 8 个子模块 (bridge/types、paths、contentBlocks、toolInfo、toolResults、modelUsage、 notifications、forwarding) 验证: - bun run precheck 全通过(typecheck + lint + 5851 tests) - ACP service tests: 176 pass / 0 fail - ACP link tests: 47 pass / 0 fail - 所有外部消费者(entry.ts、permissions.ts、__tests__/)的 import 路径不变 - 测试文件零修改 迁移计划详见 docs/acp-refactor-plan.md。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
146
src/services/acp/bridge/contentBlocks.ts
Normal file
146
src/services/acp/bridge/contentBlocks.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
// Low-level conversion of Claude content block shapes into ACP ContentBlock values.
|
||||
import type { ContentBlock, ToolCallContent } from './types.js'
|
||||
|
||||
/**
|
||||
* Wraps a string or array of content blocks into a `{ content: ToolCallContent[] }`
|
||||
* update object. Used by `toolUpdateFromToolResult` for the default / error paths.
|
||||
*/
|
||||
export function toAcpContentUpdate(
|
||||
content: unknown,
|
||||
isError: boolean,
|
||||
): { content?: ToolCallContent[] } {
|
||||
if (Array.isArray(content) && content.length > 0) {
|
||||
return {
|
||||
content: content.map((c: Record<string, unknown>) => ({
|
||||
type: 'content' as const,
|
||||
content: toAcpContentBlock(c, isError),
|
||||
})),
|
||||
}
|
||||
}
|
||||
if (typeof content === 'string' && content.length > 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'content' as const,
|
||||
content: {
|
||||
type: 'text' as const,
|
||||
text: isError ? `\`\`\`\n${content}\n\`\`\`` : content,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function toAcpContentBlock(
|
||||
content: Record<string, unknown>,
|
||||
isError: boolean,
|
||||
): ContentBlock {
|
||||
const wrapText = (text: string): ContentBlock => ({
|
||||
type: 'text',
|
||||
text: isError ? `\`\`\`\n${text}\n\`\`\`` : text,
|
||||
})
|
||||
|
||||
const type = content.type as string
|
||||
switch (type) {
|
||||
case 'text': {
|
||||
const text = content.text as string
|
||||
return { type: 'text', text: isError ? `\`\`\`\n${text}\n\`\`\`` : text }
|
||||
}
|
||||
case 'image': {
|
||||
const source = content.source as Record<string, unknown> | undefined
|
||||
if (source?.type === 'base64') {
|
||||
return {
|
||||
type: 'image',
|
||||
data: source.data as string,
|
||||
mimeType: source.media_type as string,
|
||||
}
|
||||
}
|
||||
return wrapText(
|
||||
source?.type === 'url'
|
||||
? `[image: ${source.url as string}]`
|
||||
: '[image: file reference]',
|
||||
)
|
||||
}
|
||||
case 'resource_link': {
|
||||
// ACP v1 ResourceLink requires name + uri. Name falls back to uri when
|
||||
// absent so the client always has a display label. mimeType is optional.
|
||||
const uri = content.uri as string | undefined
|
||||
const name =
|
||||
(content.name as string | undefined) ?? (uri as string | undefined)
|
||||
return {
|
||||
type: 'resource_link',
|
||||
uri: uri as string,
|
||||
name: name as string,
|
||||
mimeType: content.mimeType as string | undefined,
|
||||
}
|
||||
}
|
||||
case 'resource': {
|
||||
// ACP v1 EmbeddedResource wraps an optional TextResource / BlobResource
|
||||
// shape. Forward the standard fields the client knows how to render.
|
||||
const r = content.resource as Record<string, unknown> | undefined
|
||||
// Construct a TextResource or BlobResource payload depending on what is
|
||||
// present. Cast through unknown because not every source shape satisfies
|
||||
// the full union contract.
|
||||
const resourcePayload = {
|
||||
uri: (r?.uri as string | undefined) ?? '',
|
||||
mimeType: r?.mimeType as string | null | undefined,
|
||||
...(typeof r?.text === 'string' ? { text: r.text as string } : {}),
|
||||
...(typeof r?.blob === 'string' ? { blob: r.blob as string } : {}),
|
||||
}
|
||||
return {
|
||||
type: 'resource',
|
||||
resource: resourcePayload,
|
||||
} as unknown as ContentBlock
|
||||
}
|
||||
case 'tool_reference':
|
||||
return wrapText(`Tool: ${content.tool_name as string}`)
|
||||
case 'tool_search_tool_search_result': {
|
||||
const refs = content.tool_references as
|
||||
| Array<{ tool_name: string }>
|
||||
| undefined
|
||||
return wrapText(
|
||||
`Tools found: ${refs?.map(r => r.tool_name).join(', ') || 'none'}`,
|
||||
)
|
||||
}
|
||||
case 'tool_search_tool_result_error':
|
||||
return wrapText(
|
||||
`Error: ${content.error_code as string}${content.error_message ? ` - ${content.error_message as string}` : ''}`,
|
||||
)
|
||||
case 'web_search_result':
|
||||
return wrapText(`${content.title as string} (${content.url as string})`)
|
||||
case 'web_search_tool_result_error':
|
||||
return wrapText(`Error: ${content.error_code as string}`)
|
||||
case 'web_fetch_result':
|
||||
return wrapText(`Fetched: ${content.url as string}`)
|
||||
case 'web_fetch_tool_result_error':
|
||||
return wrapText(`Error: ${content.error_code as string}`)
|
||||
case 'code_execution_result':
|
||||
case 'bash_code_execution_result':
|
||||
return wrapText(
|
||||
`Output: ${(content.stdout as string) || (content.stderr as string) || ''}`,
|
||||
)
|
||||
case 'code_execution_tool_result_error':
|
||||
case 'bash_code_execution_tool_result_error':
|
||||
return wrapText(`Error: ${content.error_code as string}`)
|
||||
case 'text_editor_code_execution_view_result':
|
||||
return wrapText(content.content as string)
|
||||
case 'text_editor_code_execution_create_result':
|
||||
return wrapText(content.is_file_update ? 'File updated' : 'File created')
|
||||
case 'text_editor_code_execution_str_replace_result': {
|
||||
const lines = content.lines as string[] | undefined
|
||||
return wrapText(lines?.join('\n') || '')
|
||||
}
|
||||
case 'text_editor_code_execution_tool_result_error':
|
||||
return wrapText(
|
||||
`Error: ${content.error_code as string}${content.error_message ? ` - ${content.error_message as string}` : ''}`,
|
||||
)
|
||||
default:
|
||||
try {
|
||||
return { type: 'text', text: JSON.stringify(content) }
|
||||
} catch {
|
||||
return { type: 'text', text: '[content]' }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user