跳转到内容

工单生命周期

状态 存储字符串 说明
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 时生效