Controller:依赖装配与 Task 工厂
职责
Controller 是 extension.ts 和 Task 之间的中间层。activate() 跑完 initialize() 后拿到的 webview.controller 就是它 (class Controller:70)。它身上挂着 Task 实例 (this.task),管着 StateManager、AuthService、OcaAuthService、ClineAccountService、BannerService、McpHub 这几个长生命周期服务,还负责把 task history、telemetry、远程配置等横向能力收口。一句话:Controller 是「Task 的工厂 + 跨任务的共享服务容器」。
它对外暴露的方法大致分三类。一类是任务生命周期:initTask、cancelTask、clearTask、reinitExistingTaskFromId。一类是状态同步:postStateToWebview、getStateToPostToWebview,这两个方法把当前 task 的 clineMessages、taskHistory、apiConfiguration 等一大坨状态打包成 ExtensionState 推给 webview。还有一类是设置和账号:handleSignOut、setUserInfo、updateTelemetrySetting、toggleActModeForYoloMode 等。webview 发来的 gRPC 请求最终都落到 controller 上某个具体方法。
设计动机
- Controller 不持有 LLM 流:Controller 不管 LLM 调用、不管 block 解析、不管工具执行,这些都在 Task 里。Controller 只负责「创建 Task、给 Task 提供依赖、Task 跑完回收」。这种分层让单测 Task 时不需要 mock Controller。
- Task 工厂模式:
initTask是唯一创建 Task 的入口 (initTask:231),先clearTask防止并存,再读 autoApproval、shell integration timeout 等设置,再抢 task lock,最后new Task({...})把所有依赖通过TaskParams注入。Task 不自己 new 依赖,全靠 Controller 传进来。 - postStateToWebview 集中状态推送:Controller 不让 Task 直接 postMessage,而是把
postStateToWebview: () => this.postStateToWebview()作为回调传给 Task (postStateToWebview callback:311)。这样 Task 只管改messageStateHandler里的 clineMessages,Controller 负责把状态序列化成ExtensionState推给 webview。 - 远程配置定时拉取:
startRemoteConfigTimer在构造函数最后异步启动 (startRemoteConfigTimer:114),立即拉一次 + 每小时一次,让企业策略 (yoloModeAllowed、allowedMCPServers 等) 能热更新。 - cancelTask 防重复:
cancelInProgress标志避免用户狂点取消按钮时多次进入 (cancelInProgress:428)。取消流程要abortTask+ 等 stream 停 + 重新 initTask 从 history 恢复,耗时可能数秒,期间必须幂等。 - 回调注入而非继承:Controller 把
updateTaskHistory、reinitExistingTaskFromId、cancelTask等方法作为箭头函数塞进TaskParams(TaskParams callbacks:310),Task 通过this.cancelTask()调用时拿到的是 Controller 的this。这比让 Task 持有 Controller 引用更松耦合。
关键文件
class Controller:70— 类声明,字段包括task?、mcpHub、accountService、authService、stateManager、workspaceManager。constructor:121— 装配 StateManager、AuthService、OcaAuthService、BannerService、McpHub,跑cleanupLegacyCheckpoints、checkCliInstallation。startRemoteConfigTimer:114— 立即拉一次远程配置,然后每小时一次。initTask:231— Task 工厂入口,读设置、抢 lock、new Task、调 startTask 或 resumeTaskFromHistory。tryAcquireTaskLockWithRetry:286— 防止同一 task 被两个 Cline 实例同时跑,跨窗口冲突时报错。new Task:307— 把所有依赖通过TaskParams注入 Task,Task 自己不 new 任何横向服务。cancelTask:426— 分阶段取消:防重入 → abortTask → 等 stream 停 → 从 history 恢复或 clearTask。postStateToWebview:846— 调getStateToPostToWebview拼状态,然后调sendStateUpdate推给 webview。getStateToPostToWebview:851— 把 30+ 个 stateKey 拼成ExtensionState,包括 clineMessages、taskHistory、apiConfiguration 等。clearTask:1016— 清 task settings cache,调task.abortTask,置this.task = undefined让 GC 回收。dispose:168— 清 remote config timer,清 task,mcpHub.dispose。new Controller:19— WebviewProvider 构造时同步 new Controller,二者 1:1 绑定。
数据流
initTask 是 Controller 最核心的方法,流程是「读设置 → 抢 lock → new Task → 启动」:
// apps/vscode/src/core/controller/index.ts
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
// ...更多 settings 读取
const taskId = historyItem?.id || Date.now().toString()
// Acquire task lock
const lockResult: FolderLockWithRetryResult = await tryAcquireTaskLockWithRetry(taskId)
if (!lockResult.acquired && !lockResult.skipped) {
throw new Error(errorMessage) // Prevents task initialization
}
this.task = new Task({
controller: this,
mcpHub: this.mcpHub,
updateTaskHistory: (historyItem) => this.updateTaskHistory(historyItem),
postStateToWebview: () => this.postStateToWebview(),
reinitExistingTaskFromId: (taskId) => this.reinitExistingTaskFromId(taskId),
cancelTask: () => this.cancelTask(),
// ...还有 shellIntegrationTimeout、terminalReuseEnabled、cwd、taskId 等
})
if (historyItem) {
this.task.resumeTaskFromHistory()
} else if (task || images || files) {
this.task.startTask(task, images, files)
}这段在 initTask body:247 附近。注意 Task 拿到的是 controller: this 引用,但实际用到的只是 controller.stateManager 和 controller.mcpHub 等少数字段,且改 state 都通过回 postStateToWebview 回调走。Task 跑完后调 cancelTask 或者用户在 webview 点取消,Controller 进 cancelTask:先 abortTask 让递归循环自己退出,再 pWaitFor 等 stream 真正停 (pWaitFor stream stop:449),最后从 history 恢复出一个新的 Task 实例挂在 UI 上(让用户能看到 resume 按钮),或者直接 clearTask。
边界与失败
- task lock 抢不到:
tryAcquireTaskLockWithRetry返回acquired=false且skipped=false时直接抛错 (lock fail throw:288),Task 不会创建。skipped=true是 VSCode 单实例场景下的回退,允许继续。 - new user 阈值:任务历史 ≥ 10 条时把
isNewUser置假 (new user threshold:261),触发 UI 上的新手引导收起。 - autoApproval 版本号自增:每次
initTask都把autoApprovalSettings.version加 1 (autoApproval version bump:267),让旧版本的自动批准配置失效,强制用户重新确认。 - cancelTask 超时 3 秒:
pWaitFor等 stream 停最多 3 秒 (cancel timeout:456),超时只记 error 不阻塞,后续把 taskabandoned = true让它不再影响 UI。 - StateManager 持久化错误不打断:
onPersistenceError回调只记日志 (onPersistenceError:127),不调reInitialize()(那会把isInitialized=false把正在跑的 task 弄崩),也不弹警告(数据在内存里安全,下次 debounced persistence 会重试)。 - dispose 顺序:Controller dispose 先清 remote config timer,再
clearTask,再mcpHub.dispose(dispose:168),顺序不能反,否则 McpHub 在 task 还引用它时被 dispose 会崩。
小结
Controller 是 Cline 把「跨任务的共享服务」和「单任务状态机」分开的关键。它自己不碰 LLM 流,只负责装配依赖、工厂化 Task、集中状态推送。要继续看 webview 和扩展之间怎么把消息发到 Controller 上,转 /startup/webview-bridge;看 Task 本身怎么跑递归循环,转 /agent-loop/task-class 和 /agent-loop/recursion。