Skip to content

Controller:依赖装配与 Task 工厂

源码版本v4.0.10

职责

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 的工厂 + 跨任务的共享服务容器」。

它对外暴露的方法大致分三类。一类是任务生命周期:initTaskcancelTaskclearTaskreinitExistingTaskFromId。一类是状态同步:postStateToWebviewgetStateToPostToWebview,这两个方法把当前 task 的 clineMessages、taskHistory、apiConfiguration 等一大坨状态打包成 ExtensionState 推给 webview。还有一类是设置和账号:handleSignOutsetUserInfoupdateTelemetrySettingtoggleActModeForYoloMode 等。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 把 updateTaskHistoryreinitExistingTaskFromIdcancelTask 等方法作为箭头函数塞进 TaskParams (TaskParams callbacks:310),Task 通过 this.cancelTask() 调用时拿到的是 Controller 的 this。这比让 Task 持有 Controller 引用更松耦合。

关键文件

  • class Controller:70 — 类声明,字段包括 task?mcpHubaccountServiceauthServicestateManagerworkspaceManager
  • constructor:121 — 装配 StateManager、AuthService、OcaAuthService、BannerService、McpHub,跑 cleanupLegacyCheckpointscheckCliInstallation
  • 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 → 启动」:

typescript
// 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.stateManagercontroller.mcpHub 等少数字段,且改 state 都通过回 postStateToWebview 回调走。Task 跑完后调 cancelTask 或者用户在 webview 点取消,Controller 进 cancelTask:先 abortTask 让递归循环自己退出,再 pWaitFor 等 stream 真正停 (pWaitFor stream stop:449),最后从 history 恢复出一个新的 Task 实例挂在 UI 上(让用户能看到 resume 按钮),或者直接 clearTask。

边界与失败

  • task lock 抢不到:tryAcquireTaskLockWithRetry 返回 acquired=falseskipped=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 不阻塞,后续把 task abandoned = 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

对照官方资料:Cline 文档 · README