工單生命週期
工單狀態
標題為「工單狀態」的章節| 狀態 | 儲存字串 | 說明 |
|---|---|---|
backlog |
backlog |
已擷取,尚未分流進使用中的佇列 |
open |
open |
已分流,可以開始處理,但尚未動手 |
inProgress |
inProgress |
正在處理中 |
blocked |
blocked |
已開始,但被外部因素阻塞 |
inReview |
inReview |
工作已完成,等待審查 |
done |
done |
成功完成(終端狀態) |
failed |
failed |
因錯誤結束(終端狀態) |
cancelled |
cancelled |
已放棄(終端狀態) |
狀態值以小駝峰(camelCase)形式儲存(inProgress、inReview),絕不會是 in_progress。儲存值為 null 時會解析為 open;遇到未知值會拋出例外,而不是強行轉換。
tryParseLoose 接受代理提供的別名,忽略大小寫以及 - / _ / 空格:
| 狀態 | 接受的別名 |
|---|---|
open |
todo, ready, pending, new |
inProgress |
doing, started, active, wip |
blocked |
stuck |
inReview |
review, reviewing |
done |
closed, complete, completed, finished |
failed |
error, errored |
cancelled |
canceled, abandoned, dropped |
狀態轉換
標題為「狀態轉換」的章節backlog ──→ open ──→ inProgress ──→ inReview ──→ done │ │ │ │ └──→ failed ┘ └──→ blocked ──→ inProgress
Any non-terminal status ──→ cancelled; inReview ──→ failed這張圖只約束代理與自動化。 當 force 為 false 時,TicketWorkflowService.transitionStatus 會檢查它——也就是 MCP 工具與對帳器。在那裏,不合法的轉換會被記錄下來並忽略。介面中每一次由使用者驅動的狀態變更都會傳入 force: true,完全繞過這張圖,因此人可以將工單移到選擇器所列出的任何狀態(failed 以外的每個狀態),包括重新開啟已是終端狀態的工單。代理會以 fail_ticket 標記 failed。
狀態永遠可以轉換到自身。終端狀態沒有任何出邊。
| 轉換自 | 允許的目標 |
|---|---|
backlog |
open, cancelled |
open |
inProgress, blocked, cancelled |
inProgress |
blocked, inReview, done, failed, cancelled |
blocked |
inProgress, cancelled |
inReview |
inProgress, done, failed, cancelled |
done |
—(終端狀態) |
failed |
—(終端狀態) |
cancelled |
—(終端狀態) |
工單優先級
標題為「工單優先級」的章節以整數形式按 Linear 原生的 0–4 刻度儲存。未知值會解析為 none。
| 優先級 | 儲存為 |
|---|---|
none |
0 |
urgent |
1 |
high |
2 |
medium |
3 |
low |
4 |
工單供應商
標題為「工單供應商」的章節未知或為 null 的儲存值會解析為 local。
| 供應商 | 狀態 |
|---|---|
local |
完全儲存在工作區資料庫中;預設值 |
linear |
由遠端持有,在本機鏡像 |
同步配接器位於 packages/cc_infra/lib/src/tickets/sync/ 之下,涵蓋 Linear、
GitHub Issues、Jira Cloud 與 ClickUp。GitHub Issues 只是同步配接器意義上的
廠商(vendorId: github)——它不是 TicketProvider 的取值,因此拉取回來的
GitHub issue 會以 originKind: externalSync 和預設的
provider: local 儲存。廠商對應關係保存在 ticket_sync_links 上。
工單來源
標題為「工單來源」的章節createTicket 會寫入 manual。廠商拉取會寫入 externalSync。被委派的
子工單會保持 manual。
| 取值 | 含義 |
|---|---|
manual |
createTicket 上的預設值(人、代理與被委派的資料列) |
externalSync |
廠商拉取時由 TicketSyncEngine 設定 |
工單欄位
標題為「工單欄位」的章節這個聚合把兩類關注點放在同一列資料上。鏡像(供應商、索引鍵、標題、描述、優先級、標籤、狀態、時間戳)是遠端供應商的快取,一次重新整理會整體改寫它。覆蓋層(指派、委派、空間、父項、項目、已連結的 PR)只屬於 Control Center,重新整理永遠不會觸及它。
| 欄位 | 類型 | 說明 |
|---|---|---|
id |
String |
UUID v4 |
workspaceId |
String |
所屬工作區 |
provider |
TicketProvider |
擁有標準資料的後端(預設 local) |
externalKey |
String? |
供應商原生的索引鍵(例如 LIN-123)。本機 createTicket 會在這裏寫入一個獨立的 UUID,而不是資料列的 id |
url |
String? |
遠端供應商上的 Web URL |
title |
String |
簡短摘要;不得為空 |
description |
String? |
較長的 Markdown 正文 |
priority |
TicketPriority |
預設 none |
labels |
List<String> |
自由形式的標籤 |
status |
TicketStatus |
正規化後的標準狀態 |
rawStatus |
String? |
遠端供應商原生的狀態名稱,保留下來以便無損顯示 |
externalSyncedAt |
DateTime? |
最後一次多廠商同步;首次同步之前為 null。逐廠商的對應關係保存在 ticket_sync_links 中 |
parentTicketId |
String? |
委派/拆解樹中的父項 |
projectId |
String? |
所屬項目;永遠不會推送到遠端 |
assignedAgentId |
String? |
被指派主體的 id(僅中繼資料——指派不會派發任何東西) |
assigneeType |
PrincipalType |
assignedAgentId 所指的主體種類——user 或 agent(預設 agent) |
createdByType |
PrincipalType? |
建立該工單的主體種類(user 或 agent);對舊有/系統資料列為 null |
createdById |
String? |
建立該工單的主體 id |
assignedTeamId |
String? |
被指派的團隊(僅中繼資料) |
delegatedByAgentId |
String? |
委派這張工單的代理 |
delegationDepth |
int |
委派鏈中的深度;根為 0,會與最大深度上限比對 |
delegationRootTicketId |
String? |
委派鏈的根;根工單為 null |
spaceId |
String? |
這張工單被拆分出來的對話,或委派它的代理所在的空間 |
errorMessage |
String? |
當 status 為 failed 時設定 |
linkedPrIds |
List<String> |
這張工單所連結的 PR 節點 id |
metadata |
Map<String, dynamic> |
自由形式的容器 |
createdAt |
DateTime |
建立時間 |
startedAt |
DateTime? |
工作開始的時間 |
blockedAt |
DateTime? |
工單被阻塞的時間 |
cancelledAt |
DateTime? |
工單被取消的時間 |
completedAt |
DateTime? |
工單成功完成的時間 |
finishedAt |
DateTime? |
工單進入任一終端狀態的時間 |
updatedAt |
DateTime |
最後一次變更(鏡像重新整理或覆蓋層改動) |
version |
int |
樂觀並行計數器,每次變更時遞增 |
originKind |
TicketOriginKind |
工單的建立方式(預設 manual) |
collaborators |
List<TicketCollaborator> |
由儲存庫按需填充 |
衍生欄位:isTerminal、isRemote(provider != local)、isAssignedToUser 與 displayKey(externalKey ?? id——因此本機工單顯示的是建立時那個獨立的 UUID,而不是資料列的 id)。
工單關聯
標題為「工單關聯」的章節儲存的邊是 ticket_links 中的有向資料列:
| 類型 | 儲存字串 | 含義 |
|---|---|---|
blocks |
blocks |
來源封鎖目標 |
relatesTo |
relates_to |
對稱關聯 |
duplicateOf |
duplicate_of |
來源是目標的重複項 |
介面與 ticket_relation 所使用的詞彙是 TicketRelationKind,它由一條儲存的邊,加上主體位於哪一個端點推導而來:
| 種類 | 推導自 |
|---|---|
blockedBy |
一條 blocks 資料列,其中主體是目標 |
blocking |
一條 blocks 資料列,其中主體是來源 |
relatedTo |
一條 relates_to 資料列(對稱) |
duplicateOf |
一條 duplicate_of 資料列,其中主體是來源 |
duplicatedBy |
一條 duplicate_of 資料列,其中主體是目標 |
subIssueOf |
主體上的 tickets.parent_ticket_id |
parentOf |
另一張工單上的 tickets.parent_ticket_id |
父項與子問題關聯不是 ticket_links 資料列——它們保存在 tickets.parent_ticket_id 上。
領域事件
標題為「領域事件」的章節這八個類別位於 packages/cc_domain/lib/core/domain/events/ticketing_events.dart。不存在 TicketStarted、TicketDelegated、TicketCollaboratorAdded,或 ExternalTicketWebhookReceived。
| 事件 | 觸發時機 |
|---|---|
TicketCreated |
工單被建立 |
TicketAssigned |
工單被指派給某個主體或團隊 |
TicketCompleted |
工作成功完成 |
TicketFailed |
工作失敗 |
TicketCancelled |
工單被取消 |
TicketStatusChanged |
任何狀態變更 |
TicketReassigned |
工單被重新指派 |
TicketDetailsUpdated |
標題、描述、優先級或標籤變更 |
TicketAssigned 既是稽核與通知訊號,也是管線觸發器。將工單指派給代理不會啟動任何執行——沒有任何工單派發器會監看 assignedAgentId。指派給團隊是例外:TeamRoutingService 會派發團隊領導人。新增協作者只會寫入一列,不會發佈任何東西。廠商 webhook 經由 TicketSyncEngine 套用,不會產生領域事件。
變更關卡
標題為「變更關卡」的章節TicketWorkflowService 掌管所有變更。
| 機制 | 細節 |
|---|---|
| 單一關卡 | 所有變更都經由 _mutate 路由,它會在套用之前載入該列並呼叫 _assertWorkspace |
| 工作區隔離 | 當載入到的列的 workspaceId 與呼叫者的不符時,_assertWorkspace 會拋出 WorkspaceMismatchException |
| 樂觀式並行 | _mutate 寫入時把 expectedVersion 設為自己剛讀到的 version,並在遇到 ConcurrencyConflictException 時重新讀取最新狀態後重試 |
| 獨佔認領 | tryCheckout / releaseCheckout;第二個代理認領一張已被簽出的工單時,會得到 CheckoutConflictException |
| 轉換守衛 | canTransitionTo,僅在 force 為 false 時套用 |