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:
claude-code-best
2026-06-19 15:39:01 +08:00
parent 35768837a7
commit 65f81de52b
32 changed files with 5481 additions and 4591 deletions

View File

@@ -0,0 +1,291 @@
/**
* AcpAgent.prototype.createSession implementation, attached via Object.assign.
* Extracted from sessionLifecycle.ts to keep that module under the 500-line
* budget. The barrel (./index.ts) imports this module for its side effect.
*/
import { randomUUID } from 'node:crypto'
import type {
NewSessionRequest,
NewSessionResponse,
SessionModeState,
SessionModelState,
} from '@agentclientprotocol/sdk'
import type { Message } from '../../../types/message.js'
import { QueryEngine } from '../../../QueryEngine.js'
import type { QueryEngineConfig } from '../../../QueryEngine.js'
import type { Tools } from '../../../Tool.js'
import { getTools } from '../../../tools.js'
import { getEmptyToolPermissionContext } from '../../../Tool.js'
import type { PermissionMode } from '../../../types/permissions.js'
import { getCommands } from '../../../commands.js'
import { getAgentDefinitionsWithOverrides } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import {
setOriginalCwd,
switchSession,
getSessionProjectDir,
} from '../../../bootstrap/state.js'
import type { SessionId } from '../../../types/ids.js'
import { enableConfigs } from '../../../utils/config.js'
import { FileStateCache } from '../../../utils/fileStateCache.js'
import { getDefaultAppState } from '../../../state/AppStateStore.js'
import type { AppState } from '../../../state/AppStateStore.js'
import { createAcpCanUseTool } from '../permissions.js'
import { computeSessionFingerprint } from '../utils.js'
import { getMainLoopModel } from '../../../utils/model/model.js'
import { getModelOptions } from '../../../utils/model/modelOptions.js'
import { getSettings_DEPRECATED } from '../../../utils/settings/settings.js'
import { AcpAgent } from './AcpAgent.js'
import type { AcpSession } from './sessionTypes.js'
import {
resolveSessionPermissionMode,
isAcpBypassPermissionModeAvailable,
hasOwnField,
} from './permissionMode.js'
import { buildConfigOptions } from './configOptions.js'
import { readClientCapabilities } from './internalAccessors.js'
/**
* Resolve the effective `permissions.defaultMode` setting by walking the
* settings object. Lives here so createSession can read it without depending
* on AcpAgent.getSetting (which is a private instance method on the shell).
*/
function readSettingsPermissionMode(): unknown {
const settings = getSettings_DEPRECATED() as Record<string, unknown>
const perms = settings.permissions as Record<string, unknown> | undefined
return perms?.defaultMode
}
// ── createSession ────────────────────────────────────────────────
async function createSession(
this: AcpAgent,
params: NewSessionRequest,
opts: {
forceNewId?: boolean
sessionId?: string
initialMessages?: Message[]
} = {},
): Promise<NewSessionResponse> {
enableConfigs()
const sessionId = opts.sessionId ?? randomUUID()
const cwd = params.cwd
// Align the global session state so that transcript persistence,
// analytics, and cost tracking use the ACP session ID.
// Preserve the projectDir set by getOrCreateSession so that
// getSessionProjectDir() continues to resolve correctly.
const currentProjectDir = getSessionProjectDir()
switchSession(sessionId as SessionId, currentProjectDir)
// Set CWD for the session
setOriginalCwd(cwd)
const previousProcessCwd = process.cwd()
let processCwdChanged = false
try {
process.chdir(cwd)
processCwdChanged = true
} catch {
// CWD may not exist yet; best-effort
}
try {
// Build tools with a permissive permission context.
const permissionContext = getEmptyToolPermissionContext()
const tools: Tools = getTools(permissionContext)
// Parse permission mode from _meta (passed by RCS/acp-link) or settings.
const meta = params._meta as Record<string, unknown> | null | undefined
const hasMetaPermissionMode = hasOwnField(meta, 'permissionMode')
const metaPermissionMode = hasMetaPermissionMode
? meta?.permissionMode
: undefined
const settingsPermissionMode = readSettingsPermissionMode()
const permissionMode = resolveSessionPermissionMode(
metaPermissionMode,
hasMetaPermissionMode,
settingsPermissionMode,
)
// The clientCapabilities field on the shell is private; access it via
// the public initialize() side effect. Since createSession is only ever
// called after initialize() has run (per ACP protocol), this accessor
// is safe.
const clientCapabilities = readClientCapabilities(this)
// Create the permission bridge canUseTool function. The connection field
// is private on the shell; access it through the internal accessor.
const conn = (
this as unknown as {
conn: import('@agentclientprotocol/sdk').AgentSideConnection
}
).conn
const canUseTool = createAcpCanUseTool(
conn,
sessionId,
() => this.sessions.get(sessionId)?.modes.currentModeId ?? 'default',
clientCapabilities,
cwd,
(modeId: string) => {
this.applySessionMode(sessionId, modeId)
},
() =>
this.sessions.get(sessionId)?.appState.toolPermissionContext
.isBypassPermissionsModeAvailable ?? false,
)
// Parse MCP servers from ACP params
// MCP server config is handled separately in the tools system
// ACP clients can expose bypass only when both the process and local config allow it.
const isBypassAvailable = isAcpBypassPermissionModeAvailable(
settingsPermissionMode,
)
// Create a mutable AppState for the session
const appState: AppState = {
...getDefaultAppState(),
toolPermissionContext: {
...permissionContext,
mode: permissionMode as PermissionMode,
isBypassPermissionsModeAvailable: isBypassAvailable,
},
}
// Load commands and agent definitions for subagent support
const [commands, agentDefinitionsResult] = await Promise.all([
getCommands(cwd),
getAgentDefinitionsWithOverrides(cwd),
])
// Inject agent definitions into appState
appState.agentDefinitions = agentDefinitionsResult
// Build QueryEngine config
const engineConfig: QueryEngineConfig = {
cwd,
tools,
commands,
mcpClients: [],
agents: agentDefinitionsResult.activeAgents,
canUseTool,
getAppState: () => appState,
setAppState: (updater: (prev: AppState) => AppState) => {
const updated = updater(appState)
Object.assign(appState, updated)
},
readFileCache: new FileStateCache(500, 50 * 1024 * 1024),
includePartialMessages: true,
replayUserMessages: true,
initialMessages: opts.initialMessages,
}
const queryEngine = new QueryEngine(engineConfig)
// Build modes — bypassPermissions is opt-in for ACP clients.
const availableModes = [
{
id: 'default',
name: 'Default',
description: 'Standard behavior, prompts for dangerous operations',
},
{
id: 'acceptEdits',
name: 'Accept Edits',
description: 'Auto-accept file edit operations',
},
{
id: 'plan',
name: 'Plan Mode',
description: 'Planning mode, no actual tool execution',
},
{
id: 'auto',
name: 'Auto',
description:
'Use a model classifier to approve/deny permission prompts.',
},
...(isBypassAvailable
? [
{
id: 'bypassPermissions' as const,
name: 'Bypass Permissions',
description: 'Skip all permission checks',
},
]
: []),
{
id: 'dontAsk',
name: "Don't Ask",
description: "Don't prompt for permissions, deny if not pre-approved",
},
]
const modes: SessionModeState = {
currentModeId: permissionMode,
availableModes,
}
// Build models
const modelOptions = getModelOptions()
const currentModel = getMainLoopModel()
const models: SessionModelState = {
availableModels: modelOptions.map(m => ({
modelId: String(m.value ?? ''),
name: m.label ?? String(m.value ?? ''),
description: m.description ?? undefined,
})),
currentModelId: currentModel,
}
// Set the model on the engine
queryEngine.setModel(currentModel)
// Build config options
const configOptions = buildConfigOptions(modes, models)
const session: AcpSession = {
queryEngine,
cancelled: false,
cancelGeneration: 0,
cwd,
modes,
models,
configOptions,
promptRunning: false,
pendingMessages: new Map(),
pendingQueue: [],
pendingQueueHead: 0,
toolUseCache: {},
clientCapabilities,
appState,
commands,
sessionFingerprint: computeSessionFingerprint({
cwd,
mcpServers: params.mcpServers as
| Array<{ name: string; [key: string]: unknown }>
| undefined,
}),
}
this.sessions.set(sessionId, session)
// Stable v1 NewSessionResponse only defines sessionId/modes/configOptions.
// `models` is a draft/unstable field — omit it for v1 compliance.
return {
sessionId,
modes,
configOptions,
}
} finally {
if (processCwdChanged) {
process.chdir(previousProcessCwd)
}
}
}
// ── Prototype attachment ─────────────────────────────────────────
Object.assign(AcpAgent.prototype, {
createSession,
})