mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 08:45:50 +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:
158
packages/acp-link/src/server/handlers-agent.ts
Normal file
158
packages/acp-link/src/server/handlers-agent.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Writable, Readable } from 'node:stream'
|
||||
import { spawn } from 'node:child_process'
|
||||
import * as acp from '@agentclientprotocol/sdk'
|
||||
import type { WSContext } from 'hono/ws'
|
||||
import { send, sendJsonRpcError } from './client-send.js'
|
||||
import { cancelPendingPermissions, createClient } from './acp-client.js'
|
||||
import { buildAgentEnv } from './permission-mode.js'
|
||||
import { clients, getAgentConfig, logAgent } from './runtime-state.js'
|
||||
import {
|
||||
JSONRPC_INTERNAL_ERROR,
|
||||
type AgentCapabilities,
|
||||
type ClientState,
|
||||
} from './types.js'
|
||||
|
||||
export async function handleConnect(ws: WSContext): Promise<void> {
|
||||
const state = clients.get(ws)
|
||||
if (!state) return
|
||||
|
||||
const {
|
||||
command: AGENT_COMMAND,
|
||||
args: AGENT_ARGS,
|
||||
cwd: AGENT_CWD,
|
||||
} = getAgentConfig()
|
||||
|
||||
// If already connected to a running agent, just resend status
|
||||
// This handles frontend reconnections without restarting the agent process
|
||||
// Check both .killed and .exitCode to detect crashed processes
|
||||
if (
|
||||
state.connection &&
|
||||
state.process &&
|
||||
!state.process.killed &&
|
||||
state.process.exitCode === null
|
||||
) {
|
||||
logAgent.info('already connected, resending status')
|
||||
send(ws, 'status', {
|
||||
connected: true,
|
||||
agentInfo: state.agentInfo ?? { name: AGENT_COMMAND },
|
||||
capabilities: state.agentCapabilities,
|
||||
protocolVersion: state.protocolVersion,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Kill existing process if any (only if not healthy)
|
||||
if (state.process) {
|
||||
cancelPendingPermissions(state)
|
||||
state.process.kill()
|
||||
state.process = null
|
||||
state.connection = null
|
||||
}
|
||||
|
||||
try {
|
||||
logAgent.info({ command: AGENT_COMMAND, args: AGENT_ARGS }, 'spawning')
|
||||
|
||||
const agentProcess = spawn(AGENT_COMMAND, AGENT_ARGS, {
|
||||
cwd: AGENT_CWD,
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
env: buildAgentEnv(),
|
||||
})
|
||||
|
||||
state.process = agentProcess
|
||||
|
||||
// Clean up state when agent process exits unexpectedly
|
||||
agentProcess.on('exit', code => {
|
||||
logAgent.info({ exitCode: code }, 'agent process exited')
|
||||
// Only clear if this is still the current process
|
||||
if (state.process === agentProcess) {
|
||||
state.process = null
|
||||
state.connection = null
|
||||
state.sessionId = null
|
||||
}
|
||||
})
|
||||
|
||||
const input = Writable.toWeb(
|
||||
agentProcess.stdin!,
|
||||
) as unknown as WritableStream<Uint8Array>
|
||||
const output = Readable.toWeb(
|
||||
agentProcess.stdout!,
|
||||
) as unknown as ReadableStream<Uint8Array>
|
||||
|
||||
const stream = acp.ndJsonStream(input, output)
|
||||
const connection = new acp.ClientSideConnection(
|
||||
_agent => createClient(ws, state),
|
||||
stream,
|
||||
)
|
||||
|
||||
state.connection = connection
|
||||
|
||||
const initResult = await connection.initialize({
|
||||
protocolVersion: acp.PROTOCOL_VERSION,
|
||||
// Forward the real client identity/capabilities (audit §8.7). Falls back
|
||||
// to the Zed defaults only when the client did not provide any.
|
||||
clientInfo: state.clientInfo,
|
||||
clientCapabilities: state.clientCapabilities,
|
||||
})
|
||||
|
||||
// Pass the raw agentCapabilities through unchanged so present and future
|
||||
// capability fields (auth, terminal, ...) reach the client (audit §8.6).
|
||||
const agentCaps = initResult.agentCapabilities
|
||||
state.agentCapabilities = (agentCaps as AgentCapabilities | null) ?? null
|
||||
state.promptCapabilities = agentCaps?.promptCapabilities ?? null
|
||||
// Remember the negotiated protocolVersion + agentInfo so reconnects and
|
||||
// JSON-RPC initialize responses can forward them to the client (§8.13).
|
||||
state.protocolVersion = initResult.protocolVersion
|
||||
state.agentInfo =
|
||||
(initResult.agentInfo as ClientState['agentInfo'] | null | undefined) ??
|
||||
null
|
||||
|
||||
logAgent.info(
|
||||
{
|
||||
protocolVersion: initResult.protocolVersion,
|
||||
loadSession: !!state.agentCapabilities?.loadSession,
|
||||
sessionList: !!state.agentCapabilities?.sessionCapabilities?.list,
|
||||
sessionResume: !!state.agentCapabilities?.sessionCapabilities?.resume,
|
||||
hasMcp: !!state.agentCapabilities?.mcpCapabilities,
|
||||
},
|
||||
'initialized',
|
||||
)
|
||||
|
||||
send(ws, 'status', {
|
||||
connected: true,
|
||||
agentInfo: initResult.agentInfo,
|
||||
capabilities: state.agentCapabilities,
|
||||
// Surface the negotiated protocolVersion to downstream clients (audit §8.13).
|
||||
protocolVersion: initResult.protocolVersion,
|
||||
})
|
||||
|
||||
connection.closed.then(() => {
|
||||
logAgent.info('connection closed')
|
||||
state.connection = null
|
||||
state.sessionId = null
|
||||
send(ws, 'status', { connected: false })
|
||||
})
|
||||
} catch (error) {
|
||||
logAgent.error({ error: (error as Error).message }, 'connect failed')
|
||||
sendJsonRpcError(
|
||||
ws,
|
||||
state,
|
||||
null,
|
||||
JSONRPC_INTERNAL_ERROR,
|
||||
`Failed to connect: ${(error as Error).message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDisconnect(ws: WSContext): void {
|
||||
const state = clients.get(ws)
|
||||
if (!state) return
|
||||
|
||||
if (state.process) {
|
||||
state.process.kill()
|
||||
state.process = null
|
||||
}
|
||||
state.connection = null
|
||||
state.sessionId = null
|
||||
|
||||
send(ws, 'status', { connected: false })
|
||||
}
|
||||
Reference in New Issue
Block a user