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。