mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
通过 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>
147 lines
5.2 KiB
TypeScript
147 lines
5.2 KiB
TypeScript
// 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]' }
|
|
}
|
|
}
|
|
}
|