Files
claude-code/packages/acp-link/src/server/handlers-session.ts
claude-code-best 65f81de52b 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>
2026-06-20 12:38:43 +08:00

436 lines
11 KiB
TypeScript

import * as acp from '@agentclientprotocol/sdk'
import type { WSContext } from 'hono/ws'
import { cancelPendingPermissions } from './acp-client.js'
import { send, sendJsonRpcError } from './client-send.js'
import { resolveNewSessionPermissionMode } from './permission-mode.js'
import {
clients,
getAgentConfig,
getDefaultPermissionMode,
logAgent,
logPrompt,
logSession,
logWs,
} from './runtime-state.js'
import {
JSONRPC_INTERNAL_ERROR,
JSONRPC_INVALID_PARAMS,
JSONRPC_INVALID_REQUEST,
JSONRPC_METHOD_NOT_FOUND,
type ContentBlock,
} from './types.js'
export async function handleNewSession(
ws: WSContext,
params: { cwd?: string; permissionMode?: string },
): Promise<void> {
const state = clients.get(ws)
if (!state?.connection) {
logAgent.warn(
{
hasState: !!state,
hasProcess: !!state?.process,
processKilled: state?.process?.killed,
exitCode: state?.process?.exitCode,
},
'handleNewSession: not connected to agent',
)
sendJsonRpcError(
ws,
state,
state?.pendingJsonRpc?.id ?? null,
JSONRPC_INVALID_REQUEST,
'Not connected to agent',
)
return
}
const { cwd: AGENT_CWD } = getAgentConfig()
try {
const sessionCwd = params.cwd || AGENT_CWD
let permissionMode: string | undefined
try {
permissionMode = resolveNewSessionPermissionMode(
params.permissionMode,
getDefaultPermissionMode(),
)
} catch (error) {
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_INVALID_PARAMS,
(error as Error).message,
)
return
}
const result = await state.connection.newSession({
cwd: sessionCwd,
mcpServers: [],
...(permissionMode ? { _meta: { permissionMode } } : {}),
})
state.sessionId = result.sessionId
state.modelState = result.models ?? null
logSession.info(
{
sessionId: result.sessionId,
cwd: sessionCwd,
hasModels: !!result.models,
},
'created',
)
send(ws, 'session_created', {
...result,
promptCapabilities: state.promptCapabilities,
models: state.modelState,
})
} catch (error) {
logSession.error({ error: (error as Error).message }, 'create failed')
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_INTERNAL_ERROR,
`Failed to create session: ${(error as Error).message}`,
)
}
}
// ============================================================================
// Session History Operations
// Reference: Zed's AgentConnection trait - list_sessions, load_session, resume_session
// ============================================================================
export async function handleListSessions(
ws: WSContext,
params: { cwd?: string; cursor?: string },
): Promise<void> {
const state = clients.get(ws)
if (!state?.connection) {
logAgent.warn(
{
hasState: !!state,
hasProcess: !!state?.process,
processKilled: state?.process?.killed,
exitCode: state?.process?.exitCode,
},
'handleListSessions: not connected to agent',
)
sendJsonRpcError(
ws,
state,
state?.pendingJsonRpc?.id ?? null,
JSONRPC_INVALID_REQUEST,
'Not connected to agent',
)
return
}
if (!state.agentCapabilities?.sessionCapabilities?.list) {
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_METHOD_NOT_FOUND,
'Listing sessions is not supported by this agent',
)
return
}
try {
const result = await state.connection.listSessions({
cwd: params.cwd,
cursor: params.cursor,
})
const MAX_SESSIONS = 20
const sessions = result.sessions.slice(0, MAX_SESSIONS)
logSession.info(
{
total: result.sessions.length,
returned: sessions.length,
hasMore: !!result.nextCursor,
},
'listed',
)
send(ws, 'session_list', {
sessions: sessions.map((s: acp.SessionInfo) => ({
_meta: s._meta,
cwd: s.cwd,
sessionId: s.sessionId,
title: s.title,
updatedAt: s.updatedAt,
})),
nextCursor: result.nextCursor,
_meta: result._meta,
})
} catch (error) {
logSession.error({ error: (error as Error).message }, 'list failed')
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_INTERNAL_ERROR,
`Failed to list sessions: ${(error as Error).message}`,
)
}
}
export async function handleLoadSession(
ws: WSContext,
params: { sessionId: string; cwd?: string },
): Promise<void> {
const state = clients.get(ws)
if (!state?.connection) {
logAgent.warn(
{
hasState: !!state,
hasProcess: !!state?.process,
processKilled: state?.process?.killed,
exitCode: state?.process?.exitCode,
},
'handleLoadSession: not connected to agent',
)
sendJsonRpcError(
ws,
state,
state?.pendingJsonRpc?.id ?? null,
JSONRPC_INVALID_REQUEST,
'Not connected to agent',
)
return
}
if (!state.agentCapabilities?.loadSession) {
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_METHOD_NOT_FOUND,
'Loading sessions is not supported by this agent',
)
return
}
const { cwd: AGENT_CWD } = getAgentConfig()
try {
const sessionCwd = params.cwd || AGENT_CWD
const sessionId = params.sessionId
const result = await state.connection.loadSession({
sessionId,
cwd: sessionCwd,
mcpServers: [],
})
state.sessionId = sessionId
state.modelState = result.models ?? null
logSession.info({ sessionId, cwd: sessionCwd }, 'loaded')
send(ws, 'session_loaded', {
sessionId,
promptCapabilities: state.promptCapabilities,
models: state.modelState,
})
} catch (error) {
logSession.error({ error: (error as Error).message }, 'load failed')
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_INTERNAL_ERROR,
`Failed to load session: ${(error as Error).message}`,
)
}
}
export async function handleResumeSession(
ws: WSContext,
params: { sessionId: string; cwd?: string },
): Promise<void> {
const state = clients.get(ws)
if (!state?.connection) {
logAgent.warn(
{
hasState: !!state,
hasProcess: !!state?.process,
processKilled: state?.process?.killed,
exitCode: state?.process?.exitCode,
},
'handleResumeSession: not connected to agent',
)
sendJsonRpcError(
ws,
state,
state?.pendingJsonRpc?.id ?? null,
JSONRPC_INVALID_REQUEST,
'Not connected to agent',
)
return
}
if (!state.agentCapabilities?.sessionCapabilities?.resume) {
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_METHOD_NOT_FOUND,
'Resuming sessions is not supported by this agent',
)
return
}
const { cwd: AGENT_CWD } = getAgentConfig()
try {
const sessionCwd = params.cwd || AGENT_CWD
const sessionId = params.sessionId
const result = await state.connection.unstable_resumeSession({
sessionId,
cwd: sessionCwd,
})
state.sessionId = sessionId
state.modelState = result.models ?? null
logSession.info({ sessionId, cwd: sessionCwd }, 'resumed')
send(ws, 'session_resumed', {
sessionId,
promptCapabilities: state.promptCapabilities,
models: state.modelState,
})
} catch (error) {
logSession.error({ error: (error as Error).message }, 'resume failed')
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_INTERNAL_ERROR,
`Failed to resume session: ${(error as Error).message}`,
)
}
}
// Reference: Zed's AcpThread.send() forwards Vec<acp::ContentBlock> to agent
export async function handlePrompt(
ws: WSContext,
params: { content: ContentBlock[] },
): Promise<void> {
const state = clients.get(ws)
if (!state?.connection || !state.sessionId) {
sendJsonRpcError(
ws,
state,
state?.pendingJsonRpc?.id ?? null,
JSONRPC_INVALID_REQUEST,
'No active session',
)
return
}
try {
const firstText = params.content.find(b => b.type === 'text')?.text
const images = params.content.filter(b => b.type === 'image')
logPrompt.debug(
{
text: firstText?.slice(0, 100),
imageCount: images.length,
blockCount: params.content.length,
},
'sending',
)
const result = await state.connection.prompt({
sessionId: state.sessionId,
prompt: params.content as acp.ContentBlock[],
})
logPrompt.info({ stopReason: result.stopReason }, 'completed')
send(ws, 'prompt_complete', result)
} catch (error) {
logPrompt.error({ error: (error as Error).message }, 'failed')
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_INTERNAL_ERROR,
`Prompt failed: ${(error as Error).message}`,
)
}
}
// Handle cancel request from client
export async function handleCancel(ws: WSContext): Promise<void> {
const state = clients.get(ws)
if (!state?.connection || !state.sessionId) {
logWs.warn('cancel requested but no active session')
return
}
logSession.info({ sessionId: state.sessionId }, 'cancel requested')
cancelPendingPermissions(state)
try {
await state.connection.cancel({ sessionId: state.sessionId })
logSession.info({ sessionId: state.sessionId }, 'cancel sent')
} catch (error) {
logSession.error({ error: (error as Error).message }, 'cancel failed')
}
}
// Reference: Zed's AgentModelSelector.select_model() calls connection.set_session_model()
export async function handleSetSessionModel(
ws: WSContext,
params: { modelId: string },
): Promise<void> {
const state = clients.get(ws)
if (!state?.connection || !state.sessionId) {
sendJsonRpcError(
ws,
state,
state?.pendingJsonRpc?.id ?? null,
JSONRPC_INVALID_REQUEST,
'No active session',
)
return
}
if (!state.modelState) {
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_METHOD_NOT_FOUND,
'Model selection not supported by this agent',
)
return
}
try {
logSession.info(
{ sessionId: state.sessionId, modelId: params.modelId },
'setting model',
)
await state.connection.unstable_setSessionModel({
sessionId: state.sessionId,
modelId: params.modelId,
})
state.modelState = { ...state.modelState, currentModelId: params.modelId }
send(ws, 'model_changed', { modelId: params.modelId })
logSession.info({ modelId: params.modelId }, 'model changed')
} catch (error) {
logSession.error({ error: (error as Error).message }, 'set model failed')
sendJsonRpcError(
ws,
state,
state.pendingJsonRpc?.id ?? null,
JSONRPC_INTERNAL_ERROR,
`Failed to set model: ${(error as Error).message}`,
)
}
}