跳到主要内容

看板与任务 · 按行为看 API

这一页覆盖 udctl API 的看板与任务面,按「你想做什么」组织,不按 swagger tag 平铺。每个端点带一次完整的 HTTP 调用:请求给 HTTP 原文和 curl 两种写法,响应按状态码分开。

已上线 契约 · 未实现 — 契约先行:本页所有路径都是目标契约。Live 表示操作已实现 —— 在改名落地前(/todolist/* 收敛到 /tasks/*,/kanban/boards/* 收敛到 /boards/*),它在下方标注的「今天可用路径」上应答。Draft 表示端点只发布了契约、尚未实现,上线前仍可能调整。当前线上实况的机器真值仍是 OpenAPI 规范。
基础地址: https://api.oatnil.com(托管版),自部署则是你自己的地址认证: 以下所有端点都需要 Authorization: Bearer <access_token>机器可读的完整真值: OpenAPI · openapi.json

看板日常

看列、在列里建卡、移卡、重排 —— 每天都在做的那一圈。

POST/api/v1/boards/{boardId}/columns/{columnId}/query契约 · 未实现

看某列有什么卡

语义化列读。你只说要哪一列、sprint 怎么算;查询由服务端拼装 —— 列查询、板 default_tags、active sprint 展开、你的临时过滤、缺省排序 —— 并把结果回显在 effective_query 里,你能看到实际查了什么。

请求体

字段类型说明
sprintstring"active"(缺省)按板的进行中 sprint 过滤;"none" 不过滤;或指定 sprint id。
filterstring临时过滤片段,服务端 AND 到列查询上。
page / page_sizeint每列独立分页,缺省 1 / 20。

请求

POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/columns/col-doing/query HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"sprint": "active",
"filter": "title ~ \"callback\"",
"page": 1,
"page_size": 20
}

响应

{
"data": [
{
"id": "7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10",
"title": "Retry payment callback on timeout",
"status": "doing",
"tags": ["dev", "urgent"],
"metadata": {"ud.sprint": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31", "order": 3}
}
],
"total": 2,
"page": 1,
"page_size": 20,
"effective_query": "(status = 'doing') AND ('q4' IN tags) AND ud.sprint = '3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31' AND (title ~ \"callback\") ORDER BY metadata.order ASC"
}

高亮行:服务端算出来的,不是你传的 —— 这一行就是过去每个客户端各自要拼的那份查询。

错误

400过滤片段不合法,message 指到出错位置。
404列 id 不在这块板上。
列 id 从 GET /api/v1/boards/{id} 拿(板对象带列定义);「列名翻 id」是留在客户端的唯一一件事。本端点上线前,列读走下面那扇 raw 查询门。
POST/api/v1/boards/{boardId}/query已上线

今天可用路径: POST /api/v1/kanban/boards/{boardId}/query

板范围自由查询

板范围的 raw 查询门。你发一条拼好的完整查询,服务端只负责板的可见性范围。自由查询是产品能力,会一直在;上面的语义化列读只收编最常用的那一种形状。

请求体

字段类型说明
querystring完整查询串(语法与 /api/v1/tasks/query 相同)。
pageint从 1 开始。
page_sizeint每页条数。

请求

POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/query HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"query": "(status = 'doing') ORDER BY metadata.order ASC",
"page": 1,
"page_size": 20
}

响应

{
"data": [
{
"id": "7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10",
"title": "Retry payment callback on timeout",
"description": "",
"status": "doing",
"path": "",
"tags": ["dev", "urgent"],
"checkInCount": 0,
"metadata": {"ud.sprint": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31", "order": 3},
"created_at": "2026-09-22T08:01:44Z",
"created_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502",
"updated_at": "2026-09-25T10:12:03Z",
"updated_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502"
}
],
"total": 9,
"page": 1
}
POST/api/v1/boards/{boardId}/tasks已上线

今天可用路径: POST /api/v1/kanban/boards/{boardId}/tasks

在板上建一张卡

在板上建卡;服务端会合并板的 default_tags。今天初值(status、tags)由客户端自己定。Draft 的 column_id 字段改变这一点:指定列,初值由服务端按列的 enter actions 计算(你显式传的字段永远优先)。

请求体

字段类型说明
title必填string卡标题。
descriptionstringMarkdown 正文。
statusstring初始状态。
tagsstring[]服务端与板 default_tags 合并。
metadataobject扩展键(cf.*、ud.sprint、order 等)。
assignee / derived_from_id / linked_to_ids / resourceIdsvarious可选;精确形状见 OpenAPI。
column_id契约string契约。目标列;服务端按该列 enter actions 算初值,并默认给 ud.sprint 盖当前 active sprint 的章(metadata 里显式传 "ud.sprint": null 退出)。

请求

POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/tasks HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"title": "Export support tickets",
"description": "CSV and XLSX",
"status": "todo",
"tags": ["dev"]
}

响应

{
"id": "b7e02c4d-9a31-4f6e-8c25-d10f4a7b3e92",
"title": "Export support tickets",
"description": "CSV and XLSX",
"status": "todo",
"path": "",
"tags": ["dev", "q4"],
"checkInCount": 0,
"created_at": "2026-09-26T09:14:03Z",
"created_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502",
"updated_at": "2026-09-26T09:14:03Z",
"updated_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502"
}

示例里的 "q4" 来自板的 default_tags —— 这是今天服务端就已经在合并的那一份初值。

POST/api/v1/boards/{boardId}/tasks/{taskId}/move已上线

把卡移到另一列

单事务移卡。服务端计算 merge(exit(from), enter(to)) 一次写完 —— 取代今天客户端「1 笔更新加 N 笔 metadata PATCH、中途失败停在半路」的写法。dry_run: true 只算不写,给确认弹层当数据源。

请求体

字段类型说明
from_column_id必填string你眼里卡所在的列 —— 服务端会校验。
to_column_id必填string目标列。
dry_runbooltrue 只算不写,响应形状同 200。

请求

POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10/move HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"from_column_id": "col-todo",
"to_column_id": "col-doing",
"dry_run": false
}

响应

{
"task": {
"id": "7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10",
"title": "Retry payment callback on timeout",
"status": "doing",
"tags": ["dev", "urgent"]
},
"applied_actions": [
{"op": "set", "field": "status", "from": "todo", "to": "doing"},
{"op": "add_tags", "value": ["dev"]}
]
}

高亮行:服务端实际做了什么 —— 也正是 dry_run 模式给确认弹层的内容。

错误

409你的视图过期了:卡已不匹配源列查询。响应带卡的当前快照 —— 刷新后重试。服务端绝不按过期视图写字段。
整个操作幂等(set / add / remove 都幂等),重放安全,不需要幂等 key。别与 PATCH /api/v1/tasks/{id}/move 混淆 —— 那条移动的是目录路径。
GET/api/v1/boards/{boardId}/summary契约 · 未实现

扫一眼板概览

一次调用拿到每列计数和各列前几张卡 —— 不用为了看板况逐列翻页。

查询参数

字段类型说明
sprintstring语义同列读端点:缺省 "active"。
previewint每列露几张;缺省 3,0 = 只要计数。

请求

GET /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/summary?preview=2 HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>

响应

{
"board_id": "7d55a212-30f5-4a41-a5da-b2f7f01f3bb9",
"name": "Product launch",
"active_sprint_id": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31",
"columns": [
{"column_id": "col-todo", "name": "To Do", "total": 24,
"preview": ["Deep links broken on mobile", "Sign-up page A/B test"]},
{"column_id": "col-doing", "name": "Doing", "total": 9,
"preview": ["Retry payment callback on timeout", "Report export encoding bug"]},
{"column_id": "col-done", "name": "Done", "total": 132,
"preview": ["Login page refresh", "Notification grouping"]}
]
}
PATCH/api/v1/tasks/{taskId}/metadata已上线

今天可用路径: PATCH /api/v1/todolist/{taskId}/metadata

列内重排 / 排进 sprint

一次调用改一个 metadata key。列内重排写 "order";把卡排进 sprint 写 "ud.sprint"。删 key(把卡移出 sprint)走孪生路由:DELETE /api/v1/tasks/{taskId}/metadata/{key}。

请求体

字段类型说明
key必填stringmetadata 键,如 "order"、"ud.sprint"、"cf.priority"。
valueany任意 JSON 值。要移除这个 key 用 DELETE 路由,不要传 null。

请求

PATCH /api/v1/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10/metadata HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"key": "order",
"value": 12
}
200 响应是完整任务对象(形状同 GET /api/v1/tasks/{id})。
GET/api/v1/tasks/{taskId}已上线

今天可用路径: GET | POST | DELETE /api/v1/todolist/{taskId}

打开、编辑、删除一张卡

通用任务详情一族:GET 读(带 notes 与关联),PATCH /api/v1/tasks/{taskId} 改,DELETE 软删。更新是部分更新 —— 所有字段可选,没传的字段不动;assignee、kickoff、deadline 三个字段显式传空串表示「清除」。注意契约里的动词变化:今天更新在旧路径上应答的是 POST;目标动词是 PATCH —— 这本来就是它的语义。

请求

PATCH /api/v1/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10 HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"status": "done",
"deadline": ""
}
这个示例一次调用把卡标记为完成并清掉截止日。完整字段表(title、description、status、path、tags、assignee、kickoff、deadline、derivedFromId、metadata)见 OpenAPI 的 updateTodolistItem。

Sprint 节奏

sprint 本身是任务(ud.type = sprint);归属关系是 metadata 里的 ud.sprint 键。读走自由查询门。

POST/api/v1/tasks/query已上线

今天可用路径: POST /api/v1/todolist/query

看 backlog、sprint 成员,任意自由查询

全局查询门:SQL 风格语法,可查内建字段、tags、ud.* 与 cf.* metadata。backlog 视图、sprint 成员列表和一切临时切片都走这里。自由查询是产品能力,会一直在 —— 上面的语义端点只收编「按列取」这一种最常用形状。

请求体

字段类型说明
querystring查询串。
sortobject可选排序;形状见 OpenAPI(SortDTO)。
page / pageSizeint注意这里是驼峰 pageSize(板查询门用的是 page_size)。
viewstring"" 或 "full" 返回完整历史载荷;"lite" 去掉 description、notes、分享链接,给只画卡片的调用方。

请求

POST /api/v1/tasks/query HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"query": "(status = 'todo') AND ('q4' IN tags) ORDER BY metadata.order ASC",
"page": 1,
"pageSize": 20,
"view": "lite"
}

响应

{
"data": [
{
"id": "4de19b0c-2f6a-47d3-b8a1-9c05e2f7d614",
"title": "Sign-up page A/B test",
"status": "todo",
"path": "",
"tags": ["q4", "growth"],
"checkInCount": 0,
"metadata": {"order": 5},
"created_at": "2026-09-18T02:11:09Z",
"created_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502",
"updated_at": "2026-09-24T11:40:52Z",
"updated_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502"
}
],
"total": 14,
"page": 1,
"pageSize": 20,
"totalPages": 1
}

"lite" 视图下 description 字段整个缺席(不是空串)—— 缺席的含义是「要正文就取任务详情」,绝不是「这卡没有正文」。

POST/api/v1/tasks/{sprintId}/close-sprint已上线

今天可用路径: POST /api/v1/todolist/{sprintId}/close-sprint

完成一个 sprint

完成仪式在服务端一个事务里:未完成员滚动到目标、结算 velocity、写回顾 note、把 sprint 标记为 done。里程碑有序 —— 所有滚动成功之后 sprint 才翻成 done,重试安全。

请求体

字段类型说明
rolloverstring"backlog" 或一个未关闭的 sprint id。不传表示「没有选择」—— 还有未完成员时服务端会拒绝,而不是悄悄扫进 backlog。sprint 已经干净时,空请求体是合法的。

请求

POST /api/v1/tasks/3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31/close-sprint HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json

{
"rollover": "backlog"
}

响应

{
"sprintId": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31",
"target": "backlog",
"completed": 11,
"rolledOver": 4,
"rotatedBoardIds": ["7d55a212-30f5-4a41-a5da-b2f7f01f3bb9"],
"skippedBoardIds": [],
"velocity": 23.5
}

高亮行:被这次 close 移动了 active-sprint 指针的板(rotatedBoardIds),以及留给 group admin 的板(skippedBoardIds)。

错误

400不是 sprint 任务、rollover 目标不合法,或还有未完成员却没有给 rollover。
close 同时把每一块指向这个 sprint 的板的 activeSprintId 指过去 —— 指向目标 sprint,或 backlog 关闭时清空 —— 并在响应里报告:rotatedBoardIds 是被移动的板,skippedBoardIds 是调用方无权写的分享板,其指针留给 group admin 校正(看板视图偏好不该让 close 失败)。

板生命周期

板的增删改查与分享;改名落地前这些路由在 /api/v1/kanban/boards/* 下应答。一件事值得知道:PUT /api/v1/boards/{id} 是列定义的唯一写入口 —— 每次写板,服务端都会物化列 actions 并补齐缺失的列 id。字段形状见 OpenAPI。

POST/api/v1/boards建板(name 与 board_type "private" | "shared" 必填;columns 可选)。
GET/api/v1/boards列出你可见的板。
GET/api/v1/boards/{id}取一块板,含列定义与设置。
PUT/api/v1/boards/{id}改名字、列、default_tags、metadata —— 列定义的写路径。
DELETE/api/v1/boards/{id}删板;板上的卡保留。
POST/api/v1/boards/{id}/share分享给 group(group_id,permission "r" | "rw")。
DELETE/api/v1/boards/{id}/share取消分享。
POST/api/v1/boards/preview-actions预览一条列查询会物化出什么 actions —— 只读,编列时用。

示例数据均为虚构。Draft 示例是已定向的契约,上线前可能调整。本页对照后端 DTO 手工维护;Live 端点若与 /api/openapi.json 不一致,以 OpenAPI 为准,并请报告这处不一致。