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