工单生命周期
工单状态
标题为“工单状态”的部分| 状态 | 存储字符串 | 说明 |
|---|---|---|
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 工具和对账器。在那里,非法转换会被记录并忽略。UI 中每一次由用户驱动的状态变更都会传入 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 |
源工单是目标工单的重复项 |
UI 和 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 时生效 |