mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-26 10:05: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:
291
src/services/acp/agent/createSessionMethod.ts
Normal file
291
src/services/acp/agent/createSessionMethod.ts
Normal 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,
|
||||
})
|
||||
Reference in New Issue
Block a user