Plane Web API 完整使用文档
本文档覆盖 Plane v1.3.1 内部 Web API 的所有主要端点,包括认证、工作空间、项目、工作项、迭代、模块、状态、标签、成员、页面、收件箱、资源上传等,并为每个接口提供 Go 语言代码示例。
目录
- 认证与客户端基础
- 工作空间 (Workspaces)
- 项目 (Projects)
- 工作项 (Issues)
- 工作项评论 (Comments)
- 工作项链接 (Links)
- 工作项关系 (Relations)
- 迭代 (Cycles)
- 模块 (Modules)
- 状态 (States)
- 标签 (Labels)
- 成员 (Members)
- 收件箱/Intake
- 资源上传 (Assets v2)
- 页面 (Pages)
- 便签 (Stickies)
- 附录
1. 认证与客户端基础
1.1 认证机制
| 项目 |
值 |
| 认证方式 |
Django SessionAuthentication (Cookie) |
| Session Cookie 名 |
session-id |
| Session 有效期 |
7 天 (604800秒) |
| CSRF Cookie 名 |
csrftoken |
| CSRF Header |
X-CSRFToken |
| 写操作要求 |
Session Cookie + CSRF Token |
| 读操作要求 |
Session Cookie |
1.2 登录流程
步骤 1: 获取 CSRF Token
GET /auth/get-csrf-token/
响应: {"csrf_token": "abc123..."}
Set-Cookie: csrftoken=abc123...
步骤 2: 邮箱密码登录
POST /auth/sign-in/
Content-Type: application/x-www-form-urlencoded
Cookie: csrftoken=abc123...
X-CSRFToken: abc123...
Body: email=user@example.com&password=secret
响应: 302 (成功)
Set-Cookie: session-id=xyz789...
1.3 Go 客户端基础代码
package plane
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"strings"
)
// Client Plane Web API 客户端
type Client struct {
BaseURL string
httpClient *http.Client
csrfToken string
}
// NewClient 创建客户端实例
func NewClient(baseURL string) (*Client, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
return &Client{
BaseURL: strings.TrimRight(baseURL, "/"),
httpClient: &http.Client{
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
},
}, nil
}
// request 执行 HTTP 请求
func (c *Client) request(method, path string, body io.Reader, contentType string) (*http.Response, error) {
req, err := http.NewRequest(method, c.BaseURL+path, body)
if err != nil {
return nil, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if method != http.MethodGet && c.csrfToken != "" {
req.Header.Set("X-CSRFToken", c.csrfToken)
}
req.Header.Set("Referer", c.BaseURL)
return c.httpClient.Do(req)
}
// jsonRequest 执行 JSON 请求并返回响应
func (c *Client) jsonRequest(method, path string, payload interface{}) ([]byte, int, error) {
var body io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return nil, 0, err
}
body = bytes.NewReader(data)
}
resp, err := c.request(method, path, body, "application/json")
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
return respBody, resp.StatusCode, nil
}
// Login 登录获取 Session
func (c *Client) Login(email, password string) error {
// 获取 CSRF Token
resp, err := c.request(http.MethodGet, "/auth/get-csrf-token/", nil, "")
if err != nil {
return fmt.Errorf("获取 CSRF 失败: %w", err)
}
defer resp.Body.Close()
var csrf struct {
CSRFToken string `json:"csrf_token"`
}
json.NewDecoder(resp.Body).Decode(&csrf)
c.csrfToken = csrf.CSRFToken
// 登录
form := url.Values{"email": {email}, "password": {password}}
loginResp, err := c.request(http.MethodPost, "/auth/sign-in/",
strings.NewReader(form.Encode()), "application/x-www-form-urlencoded")
if err != nil {
return fmt.Errorf("登录失败: %w", err)
}
defer loginResp.Body.Close()
if loginResp.StatusCode != http.StatusFound {
return fmt.Errorf("登录失败, 状态码: %d", loginResp.StatusCode)
}
return nil
}
1.4 登录示例
func main() {
client, _ := NewClient("http://localhost:8000")
err := client.Login("admin@example.com", "password123")
if err != nil {
panic(err)
}
fmt.Println("登录成功")
}
2. 工作空间 (Workspaces)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/ |
列出当前用户的工作空间 |
| POST |
/api/workspaces/ |
创建工作空间 |
| GET |
/api/workspaces/{slug}/ |
获取工作空间详情 |
| PATCH |
/api/workspaces/{slug}/ |
更新工作空间 |
| DELETE |
/api/workspaces/{slug}/ |
删除工作空间 |
创建工作空间参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
工作空间名称 |
slug |
string |
是 |
URL 标识符(全局唯一) |
organization_size |
string |
否 |
组织规模 |
Go 示例
// ListWorkspaces 列出工作空间
func (c *Client) ListWorkspaces() ([]byte, error) {
body, status, err := c.jsonRequest(http.MethodGet, "/api/workspaces/", nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, err: %v", status, err)
}
return body, nil
}
// CreateWorkspace 创建工作空间
func (c *Client) CreateWorkspace(name, slug string) ([]byte, error) {
payload := map[string]interface{}{
"name": name,
"slug": slug,
}
body, status, err := c.jsonRequest(http.MethodPost, "/api/workspaces/", payload)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// GetWorkspace 获取工作空间详情
func (c *Client) GetWorkspace(slug string) ([]byte, error) {
body, status, err := c.jsonRequest(http.MethodGet, fmt.Sprintf("/api/workspaces/%s/", slug), nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// UpdateWorkspace 更新工作空间
func (c *Client) UpdateWorkspace(slug string, data map[string]interface{}) ([]byte, error) {
body, status, err := c.jsonRequest(http.MethodPatch, fmt.Sprintf("/api/workspaces/%s/", slug), data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
3. 项目 (Projects)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/ |
列出项目 |
| POST |
/api/workspaces/{slug}/projects/ |
创建项目 |
| GET |
/api/workspaces/{slug}/projects/{id}/ |
获取项目详情 |
| PATCH |
/api/workspaces/{slug}/projects/{id}/ |
更新项目 |
| DELETE |
/api/workspaces/{slug}/projects/{id}/ |
删除项目 |
| POST |
/api/workspaces/{slug}/projects/{id}/archive/ |
归档项目 |
| DELETE |
/api/workspaces/{slug}/projects/{id}/archive/ |
取消归档 |
创建项目参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
项目名称(最大 255 字符) |
identifier |
string |
是 |
项目标识符(最大 12 字符,自动转大写) |
description |
string |
否 |
描述 |
network |
integer |
否 |
0=私密, 2=公开(默认 2) |
project_lead |
uuid |
否 |
负责人 ID |
default_assignee |
uuid |
否 |
默认指派人 |
module_view |
boolean |
否 |
启用模块(默认 false) |
cycle_view |
boolean |
否 |
启用迭代(默认 false) |
page_view |
boolean |
否 |
启用页面(默认 true) |
intake_view |
boolean |
否 |
启用收件箱(默认 false) |
timezone |
string |
否 |
时区(默认 UTC) |
Go 示例
// ListProjects 列出项目
func (c *Client) ListProjects(slug string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/", slug)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// CreateProject 创建项目
func (c *Client) CreateProject(slug string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/", slug)
body, status, err := c.jsonRequest(http.MethodPost, path, data)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// GetProject 获取项目详情
func (c *Client) GetProject(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// UpdateProject 更新项目
func (c *Client) UpdateProject(slug, projectID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteProject 删除项目
func (c *Client) DeleteProject(slug, projectID string) error {
_, status, err := c.jsonRequest(http.MethodDelete,
fmt.Sprintf("/api/workspaces/%s/projects/%s/", slug, projectID), nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// ArchiveProject 归档项目
func (c *Client) ArchiveProject(slug, projectID string) error {
_, status, err := c.jsonRequest(http.MethodPost,
fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", slug, projectID), map[string]interface{}{})
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
4. 工作项 (Issues)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/{pid}/issues/ |
列出工作项 |
| POST |
/api/workspaces/{slug}/projects/{pid}/issues/ |
创建工作项 |
| GET |
/api/workspaces/{slug}/projects/{pid}/issues/{id}/ |
获取详情 |
| PATCH |
/api/workspaces/{slug}/projects/{pid}/issues/{id}/ |
更新工作项 |
| DELETE |
/api/workspaces/{slug}/projects/{pid}/issues/{id}/ |
删除工作项 |
| GET |
/api/workspaces/{slug}/projects/{pid}/issues/{id}/sub-issues/ |
列出子工作项 |
| POST |
/api/workspaces/{slug}/projects/{pid}/issues/{id}/archive/ |
归档 |
| DELETE |
/api/workspaces/{slug}/projects/{pid}/issues/{id}/archive/ |
取消归档 |
| GET |
/api/workspaces/{slug}/projects/{pid}/issues/{id}/history/ |
活动历史 |
创建工作项参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
名称(最大 255 字符) |
description_html |
string |
否 |
HTML 描述 |
priority |
string |
否 |
urgent/high/medium/low/none(默认 none) |
state_id |
uuid |
否 |
状态 ID |
parent_id |
uuid |
否 |
父工作项 ID |
assignee_ids |
array[uuid] |
否 |
指派人 ID 列表 |
label_ids |
array[uuid] |
否 |
标签 ID 列表 |
start_date |
date |
否 |
开始日期 (YYYY-MM-DD) |
target_date |
date |
否 |
目标日期 (YYYY-MM-DD) |
estimate_point |
uuid |
否 |
估算点 ID |
Go 示例
// ListIssues 列出工作项
func (c *Client) ListIssues(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// CreateIssue 创建工作项
func (c *Client) CreateIssue(slug, projectID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodPost, path, data)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// GetIssue 获取工作项详情
func (c *Client) GetIssue(slug, projectID, issueID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", slug, projectID, issueID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// UpdateIssue 更新工作项
func (c *Client) UpdateIssue(slug, projectID, issueID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", slug, projectID, issueID)
body, status, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteIssue 删除工作项
func (c *Client) DeleteIssue(slug, projectID, issueID string) error {
_, status, err := c.jsonRequest(http.MethodDelete,
fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", slug, projectID, issueID), nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// ArchiveIssue 归档工作项
func (c *Client) ArchiveIssue(slug, projectID, issueID string) error {
_, status, err := c.jsonRequest(http.MethodPost,
fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", slug, projectID, issueID),
map[string]interface{}{})
if err != nil || status != 200 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// GetIssueActivity 获取工作项活动历史
func (c *Client) GetIssueActivity(slug, projectID, issueID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/history/", slug, projectID, issueID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// GetSubIssues 获取子工作项
func (c *Client) GetSubIssues(slug, projectID, issueID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/sub-issues/", slug, projectID, issueID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
5. 工作项评论 (Comments)
端点列表
| 方法 |
路径 |
说明 |
| GET |
.../issues/{issue_id}/comments/ |
列出评论 |
| POST |
.../issues/{issue_id}/comments/ |
创建评论 |
| GET |
.../issues/{issue_id}/comments/{id}/ |
获取评论 |
| PATCH |
.../issues/{issue_id}/comments/{id}/ |
更新评论 |
| DELETE |
.../issues/{issue_id}/comments/{id}/ |
删除评论 |
创建评论参数
| 字段 |
类型 |
必填 |
说明 |
comment_html |
string |
是 |
评论 HTML 内容 |
comment_json |
object |
否 |
评论 JSON 结构 |
access |
string |
否 |
INTERNAL(默认) / EXTERNAL |
Go 示例
// ListComments 列出评论
func (c *Client) ListComments(slug, projectID, issueID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", slug, projectID, issueID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// CreateComment 创建评论
func (c *Client) CreateComment(slug, projectID, issueID, commentHTML string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", slug, projectID, issueID)
payload := map[string]interface{}{
"comment_html": commentHTML,
}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// UpdateComment 更新评论
func (c *Client) UpdateComment(slug, projectID, issueID, commentID, commentHTML string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", slug, projectID, issueID, commentID)
payload := map[string]interface{}{"comment_html": commentHTML}
body, status, err := c.jsonRequest(http.MethodPatch, path, payload)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteComment 删除评论
func (c *Client) DeleteComment(slug, projectID, issueID, commentID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", slug, projectID, issueID, commentID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
6. 工作项链接 (Links)
端点列表
| 方法 |
路径 |
说明 |
| GET |
.../issues/{issue_id}/issue-links/ |
列出链接 |
| POST |
.../issues/{issue_id}/issue-links/ |
创建链接 |
| PATCH |
.../issues/{issue_id}/issue-links/{id}/ |
更新链接 |
| DELETE |
.../issues/{issue_id}/issue-links/{id}/ |
删除链接 |
创建链接参数
| 字段 |
类型 |
必填 |
说明 |
url |
string |
是 |
链接 URL (http/https) |
title |
string |
否 |
链接标题 |
Go 示例
// CreateIssueLink 创建链接
func (c *Client) CreateIssueLink(slug, projectID, issueID, linkURL, title string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-links/", slug, projectID, issueID)
payload := map[string]interface{}{"url": linkURL, "title": title}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteIssueLink 删除链接
func (c *Client) DeleteIssueLink(slug, projectID, issueID, linkID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-links/%s/", slug, projectID, issueID, linkID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
7. 工作项关系 (Relations)
端点列表
| 方法 |
路径 |
说明 |
| GET |
.../issues/{issue_id}/issue-relation/ |
列出关系 |
| POST |
.../issues/{issue_id}/issue-relation/ |
创建关系 |
| POST |
.../issues/{issue_id}/remove-relation/ |
移除关系 |
创建关系参数
| 字段 |
类型 |
必填 |
说明 |
relation_type |
string |
是 |
blocking/blocked_by/duplicate/relates_to/start_before/start_after/finish_before/finish_after |
related_list |
array[uuid] |
是 |
关联工作项 ID 列表 |
Go 示例
// CreateIssueRelation 创建工作项关系
func (c *Client) CreateIssueRelation(slug, projectID, issueID, relationType string, relatedIDs []string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-relation/", slug, projectID, issueID)
payload := map[string]interface{}{
"relation_type": relationType,
"related_list": relatedIDs,
}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// RemoveIssueRelation 移除关系
func (c *Client) RemoveIssueRelation(slug, projectID, issueID, relationType, relatedID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/remove-relation/", slug, projectID, issueID)
payload := map[string]interface{}{
"relation_type": relationType,
"related_list": []string{relatedID},
}
_, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
8. 迭代 (Cycles)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/{pid}/cycles/ |
列出迭代 |
| POST |
/api/workspaces/{slug}/projects/{pid}/cycles/ |
创建迭代 |
| GET |
/api/workspaces/{slug}/projects/{pid}/cycles/{id}/ |
获取迭代详情 |
| PATCH |
/api/workspaces/{slug}/projects/{pid}/cycles/{id}/ |
更新迭代 |
| DELETE |
/api/workspaces/{slug}/projects/{pid}/cycles/{id}/ |
删除迭代 |
| GET |
.../cycles/{id}/cycle-issues/ |
列出迭代中的工作项 |
| POST |
.../cycles/{id}/cycle-issues/ |
添加工作项到迭代 |
| DELETE |
.../cycles/{id}/cycle-issues/{issue_id}/ |
从迭代移除工作项 |
| POST |
.../cycles/{id}/transfer-issues/ |
转移未完成工作项到另一迭代 |
| POST |
.../cycles/{id}/archive/ |
归档迭代 |
| GET |
.../archived-cycles/ |
列出已归档迭代 |
| DELETE |
.../archived-cycles/{id}/ |
取消归档 |
| GET |
.../cycles/{id}/progress/ |
获取迭代进度 |
| GET |
.../cycles/{id}/analytics/ |
获取迭代分析 |
创建迭代参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
迭代名称 |
description |
string |
否 |
描述 |
start_date |
datetime |
否 |
开始时间(不可晚于 end_date) |
end_date |
datetime |
否 |
结束时间 |
owned_by |
uuid |
否 |
负责人 ID(默认当前用户) |
添加工作项到迭代参数
| 字段 |
类型 |
必填 |
说明 |
issues |
array[uuid] |
是 |
工作项 ID 列表 |
转移工作项参数
| 字段 |
类型 |
必填 |
说明 |
new_cycle_id |
uuid |
是 |
目标迭代 ID |
Go 示例
// ListCycles 列出迭代
func (c *Client) ListCycles(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// CreateCycle 创建迭代
func (c *Client) CreateCycle(slug, projectID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodPost, path, data)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// GetCycle 获取迭代详情
func (c *Client) GetCycle(slug, projectID, cycleID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// UpdateCycle 更新迭代
func (c *Client) UpdateCycle(slug, projectID, cycleID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
body, status, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteCycle 删除迭代
func (c *Client) DeleteCycle(slug, projectID, cycleID string) error {
_, status, err := c.jsonRequest(http.MethodDelete,
fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID), nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// AddIssuesToCycle 添加工作项到迭代
func (c *Client) AddIssuesToCycle(slug, projectID, cycleID string, issueIDs []string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", slug, projectID, cycleID)
payload := map[string]interface{}{"issues": issueIDs}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// RemoveIssueFromCycle 从迭代移除工作项
func (c *Client) RemoveIssueFromCycle(slug, projectID, cycleID, issueID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/%s/", slug, projectID, cycleID, issueID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// TransferCycleIssues 转移迭代工作项
func (c *Client) TransferCycleIssues(slug, projectID, cycleID, newCycleID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/transfer-issues/", slug, projectID, cycleID)
payload := map[string]interface{}{"new_cycle_id": newCycleID}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// ArchiveCycle 归档迭代
func (c *Client) ArchiveCycle(slug, projectID, cycleID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", slug, projectID, cycleID)
_, status, err := c.jsonRequest(http.MethodPost, path, map[string]interface{}{})
if err != nil || status != 200 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// GetCycleProgress 获取迭代进度
func (c *Client) GetCycleProgress(slug, projectID, cycleID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/progress/", slug, projectID, cycleID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
9. 模块 (Modules)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/{pid}/modules/ |
列出模块 |
| POST |
/api/workspaces/{slug}/projects/{pid}/modules/ |
创建模块 |
| GET |
.../modules/{id}/ |
获取模块详情 |
| PATCH |
.../modules/{id}/ |
更新模块 |
| DELETE |
.../modules/{id}/ |
删除模块 |
| GET |
.../modules/{id}/issues/ |
列出模块中的工作项 |
| POST |
.../modules/{id}/issues/ |
添加工作项到模块 |
| DELETE |
.../modules/{id}/issues/{issue_id}/ |
从模块移除工作项 |
| GET |
.../modules/{id}/module-links/ |
列出模块链接 |
| POST |
.../modules/{id}/module-links/ |
创建模块链接 |
| PATCH |
.../modules/{id}/module-links/{link_id}/ |
更新模块链接 |
| DELETE |
.../modules/{id}/module-links/{link_id}/ |
删除模块链接 |
| POST |
.../modules/{id}/archive/ |
归档模块 |
| GET |
.../archived-modules/ |
列出已归档模块 |
| DELETE |
.../archived-modules/{id}/ |
取消归档 |
创建模块参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
模块名称(同项目内不可重复) |
description |
string |
否 |
描述 |
start_date |
date |
否 |
开始日期 (YYYY-MM-DD) |
target_date |
date |
否 |
目标日期 |
status |
string |
否 |
backlog/planned(默认)/in-progress/paused/completed/cancelled |
lead |
uuid |
否 |
负责人 ID |
members |
array[uuid] |
否 |
成员 ID 列表 |
添加工作项到模块参数
| 字段 |
类型 |
必填 |
说明 |
issues |
array[uuid] |
是 |
工作项 ID 列表 |
Go 示例
// ListModules 列出模块
func (c *Client) ListModules(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// CreateModule 创建模块
func (c *Client) CreateModule(slug, projectID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodPost, path, data)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// GetModule 获取模块详情
func (c *Client) GetModule(slug, projectID, moduleID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// UpdateModule 更新模块
func (c *Client) UpdateModule(slug, projectID, moduleID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
body, status, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteModule 删除模块
func (c *Client) DeleteModule(slug, projectID, moduleID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// AddIssuesToModule 添加工作项到模块
func (c *Client) AddIssuesToModule(slug, projectID, moduleID string, issueIDs []string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/issues/", slug, projectID, moduleID)
payload := map[string]interface{}{"issues": issueIDs}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// RemoveIssueFromModule 从模块移除工作项
func (c *Client) RemoveIssueFromModule(slug, projectID, moduleID, issueID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/issues/%s/", slug, projectID, moduleID, issueID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 200 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// ArchiveModule 归档模块
func (c *Client) ArchiveModule(slug, projectID, moduleID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", slug, projectID, moduleID)
_, status, err := c.jsonRequest(http.MethodPost, path, map[string]interface{}{})
if err != nil || status != 200 {
return fmt.Errorf("status: %d", status)
}
return nil
}
10. 状态 (States)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/{pid}/states/ |
列出状态 |
| POST |
/api/workspaces/{slug}/projects/{pid}/states/ |
创建状态 |
| GET |
.../states/{id}/ |
获取状态详情 |
| PATCH |
.../states/{id}/ |
更新状态 |
| DELETE |
.../states/{id}/ |
删除状态 |
| POST |
.../states/{id}/mark-default/ |
设为默认状态 |
创建状态参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
状态名称(项目内唯一) |
color |
string |
是 |
颜色值(如 #46A758) |
group |
string |
是 |
分组:backlog/unstarted/started/completed/cancelled |
description |
string |
否 |
描述 |
sequence |
float |
否 |
排序序号 |
default |
boolean |
否 |
是否默认(设 true 会重置其他状态) |
Go 示例
// ListStates 列出状态
func (c *Client) ListStates(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// CreateState 创建状态
func (c *Client) CreateState(slug, projectID, name, color, group string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", slug, projectID)
payload := map[string]interface{}{
"name": name,
"color": color,
"group": group,
}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// UpdateState 更新状态
func (c *Client) UpdateState(slug, projectID, stateID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", slug, projectID, stateID)
body, status, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteState 删除状态
func (c *Client) DeleteState(slug, projectID, stateID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", slug, projectID, stateID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// MarkStateDefault 设为默认状态
func (c *Client) MarkStateDefault(slug, projectID, stateID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/mark-default/", slug, projectID, stateID)
_, status, err := c.jsonRequest(http.MethodPost, path, map[string]interface{}{})
if err != nil || status != 200 {
return fmt.Errorf("status: %d", status)
}
return nil
}
11. 标签 (Labels)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/{pid}/issue-labels/ |
列出标签 |
| POST |
/api/workspaces/{slug}/projects/{pid}/issue-labels/ |
创建标签 |
| GET |
.../issue-labels/{id}/ |
获取标签详情 |
| PATCH |
.../issue-labels/{id}/ |
更新标签 |
| DELETE |
.../issue-labels/{id}/ |
删除标签 |
创建标签参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
标签名称(项目内唯一) |
color |
string |
否 |
颜色值 |
description |
string |
否 |
描述 |
parent |
uuid |
否 |
父标签 ID |
Go 示例
// ListLabels 列出标签
func (c *Client) ListLabels(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// CreateLabel 创建标签
func (c *Client) CreateLabel(slug, projectID, name, color string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", slug, projectID)
payload := map[string]interface{}{"name": name, "color": color}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// UpdateLabel 更新标签
func (c *Client) UpdateLabel(slug, projectID, labelID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", slug, projectID, labelID)
body, status, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// DeleteLabel 删除标签
func (c *Client) DeleteLabel(slug, projectID, labelID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", slug, projectID, labelID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
12. 成员 (Members)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/members/ |
列出工作空间成员 |
| GET |
/api/workspaces/{slug}/members/{id}/ |
获取成员详情 |
| PATCH |
/api/workspaces/{slug}/members/{id}/ |
更新成员角色 |
| DELETE |
/api/workspaces/{slug}/members/{id}/ |
移除成员 |
| GET |
/api/workspaces/{slug}/projects/{pid}/members/ |
列出项目成员 |
| POST |
/api/workspaces/{slug}/projects/{pid}/members/ |
添加项目成员 |
| GET |
.../projects/{pid}/members/{id}/ |
获取项目成员详情 |
| PATCH |
.../projects/{pid}/members/{id}/ |
更新项目成员角色 |
| DELETE |
.../projects/{pid}/members/{id}/ |
移除项目成员 |
| GET |
/api/users/me/ |
获取当前用户信息 |
| PATCH |
/api/users/me/ |
更新当前用户信息 |
角色值
| 值 |
名称 |
| 20 |
Admin |
| 15 |
Member |
| 5 |
Guest |
添加项目成员参数
| 字段 |
类型 |
必填 |
说明 |
members |
array |
是 |
成员列表 |
members[].member_id |
uuid |
是 |
用户 ID(须为工作空间成员) |
members[].role |
integer |
是 |
角色:20/15/5 |
Go 示例
// ListWorkspaceMembers 列出工作空间成员
func (c *Client) ListWorkspaceMembers(slug string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/members/", slug)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// ListProjectMembers 列出项目成员
func (c *Client) ListProjectMembers(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", slug, projectID)
body, status, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// AddProjectMember 添加项目成员
func (c *Client) AddProjectMember(slug, projectID, memberID string, role int) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", slug, projectID)
payload := map[string]interface{}{
"members": []map[string]interface{}{
{"member_id": memberID, "role": role},
},
}
body, status, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || status != 201 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// UpdateProjectMemberRole 更新项目成员角色
func (c *Client) UpdateProjectMemberRole(slug, projectID, memberID string, role int) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", slug, projectID, memberID)
payload := map[string]interface{}{"role": role}
body, status, err := c.jsonRequest(http.MethodPatch, path, payload)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
// RemoveProjectMember 移除项目成员
func (c *Client) RemoveProjectMember(slug, projectID, memberID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", slug, projectID, memberID)
_, status, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || status != 204 {
return fmt.Errorf("status: %d", status)
}
return nil
}
// GetCurrentUser 获取当前用户信息
func (c *Client) GetCurrentUser() ([]byte, error) {
body, status, err := c.jsonRequest(http.MethodGet, "/api/users/me/", nil)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d", status)
}
return body, nil
}
// UpdateCurrentUser 更新当前用户信息
func (c *Client) UpdateCurrentUser(data map[string]interface{}) ([]byte, error) {
body, status, err := c.jsonRequest(http.MethodPatch, "/api/users/me/", data)
if err != nil || status != 200 {
return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
}
return body, nil
}
13. 收件箱/Intake
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/{pid}/intake-issues/ |
列出收件箱工作项 |
| POST |
/api/workspaces/{slug}/projects/{pid}/intake-issues/ |
创建收件箱工作项 |
| GET |
.../intake-issues/{issue_id}/ |
获取收件箱工作项详情 |
| PATCH |
.../intake-issues/{issue_id}/ |
更新收件箱工作项 |
| DELETE |
.../intake-issues/{issue_id}/ |
删除收件箱工作项 |
创建收件箱工作项参数
请求体为嵌套 JSON,外层包含 issue 对象:
| 字段 |
类型 |
必填 |
说明 |
issue.name |
string |
是 |
工作项名称 |
issue.description_html |
string |
否 |
HTML 描述(支持内联图片) |
issue.priority |
string |
否 |
urgent/high/medium/low/none(默认 none) |
issue.assignee_ids |
array[uuid] |
否 |
指派人列表 |
issue.label_ids |
array[uuid] |
否 |
标签列表 |
issue.start_date |
date |
否 |
开始日期 |
issue.target_date |
date |
否 |
目标日期 |
issue.parent_id |
uuid |
否 |
父工作项 ID |
注意:state_id 会被强制设为 Triage 状态,无需传入。
更新收件箱工作项参数
| 字段 |
类型 |
必填 |
说明 |
status |
integer |
否 |
-2=待处理, -1=拒绝, 0=暂缓, 1=接受, 2=标记重复 |
snoozed_till |
datetime |
否 |
暂缓截止时间 |
duplicate_to |
uuid |
否 |
重复目标工作项 ID |
issue |
object |
否 |
更新工作项内容(字段同创建) |
列表查询参数
| 参数 |
类型 |
说明 |
status |
string |
按状态过滤(逗号分隔,如 -2,-1) |
order_by |
string |
排序(默认 -issue__created_at) |
Go 示例
// ListIntakeIssues 列出收件箱工作项
func (c *Client) ListIntakeIssues(slug, projectID string, status string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", slug, projectID)
if status != "" {
path += "?status=" + status
}
body, code, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d", code)
}
return body, nil
}
// CreateIntakeIssue 创建收件箱工作项
func (c *Client) CreateIntakeIssue(slug, projectID string, issue map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", slug, projectID)
payload := map[string]interface{}{"issue": issue}
body, code, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
// GetIntakeIssue 获取收件箱工作项详情
func (c *Client) GetIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
body, code, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d", code)
}
return body, nil
}
// UpdateIntakeIssue 更新收件箱工作项(修改状态或内容)
func (c *Client) UpdateIntakeIssue(slug, projectID, issueID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
body, code, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
// AcceptIntakeIssue 接受收件箱工作项
func (c *Client) AcceptIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
return c.UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{
"status": 1,
})
}
// RejectIntakeIssue 拒绝收件箱工作项
func (c *Client) RejectIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
return c.UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{
"status": -1,
})
}
// DeleteIntakeIssue 删除收件箱工作项
func (c *Client) DeleteIntakeIssue(slug, projectID, issueID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
_, code, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || code != 204 {
return fmt.Errorf("status: %d", code)
}
return nil
}
14. 资源上传 (Assets v2)
端点列表
| 方法 |
路径 |
说明 |
| POST |
/api/assets/v2/workspaces/{slug}/projects/{pid}/ |
创建项目资源(获取预签名 URL) |
| PATCH |
/api/assets/v2/workspaces/{slug}/projects/{pid}/{asset_id}/ |
确认上传完成 |
| GET |
/api/assets/v2/workspaces/{slug}/projects/{pid}/{asset_id}/ |
下载资源(302 重定向) |
| DELETE |
/api/assets/v2/workspaces/{slug}/projects/{pid}/{asset_id}/ |
删除资源 |
| POST |
/api/assets/v2/workspaces/{slug}/projects/{pid}/{entity_id}/bulk/ |
批量绑定资源到实体 |
| POST |
/api/assets/v2/workspaces/{slug}/ |
创建工作空间资源 |
| PATCH |
/api/assets/v2/workspaces/{slug}/{asset_id}/ |
确认工作空间资源上传 |
| POST |
/api/assets/v2/user-assets/ |
创建用户资源 |
| PATCH |
/api/assets/v2/user-assets/{asset_id}/ |
确认用户资源上传 |
创建项目资源参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
文件名 |
type |
string |
是 |
MIME 类型:image/jpeg/image/png/image/webp/image/jpg/image/gif |
size |
integer |
是 |
文件大小(字节),上限 5MB |
entity_type |
string |
是 |
资源类型(见下表) |
entity_identifier |
string |
否 |
关联实体 ID(新建时传 "" 空字符串) |
entity_type 值
| 值 |
说明 |
ISSUE_DESCRIPTION |
工作项描述内联资源 |
ISSUE_ATTACHMENT |
工作项附件 |
COMMENT_DESCRIPTION |
评论内联资源 |
PAGE_DESCRIPTION |
页面内联资源 |
DRAFT_ISSUE_DESCRIPTION |
草稿工作项描述 |
PROJECT_COVER |
项目封面 |
USER_AVATAR |
用户头像 |
USER_COVER |
用户封面 |
WORKSPACE_LOGO |
工作空间 Logo |
创建资源响应
| 字段 |
类型 |
说明 |
upload_data.url |
string |
S3 上传目标 URL |
upload_data.fields |
object |
上传表单字段 |
asset_id |
uuid |
资源 ID |
asset_url |
string |
资源访问路径(嵌入 HTML 用) |
批量绑定参数
| 字段 |
类型 |
必填 |
说明 |
asset_ids |
array[uuid] |
是 |
资源 ID 列表 |
上传流程
POST .../projects/{pid}/ → 获取 upload_data + asset_id
POST {upload_data.url} (multipart/form-data) → 上传文件到 S3
PATCH .../projects/{pid}/{asset_id}/ → 确认上传
- 将
asset_url 嵌入 description_html
- 创建工作项后
POST .../{issue_id}/bulk/ → 绑定资源
Go 示例
// AssetUploadResult 资源上传结果
type AssetUploadResult struct {
UploadData struct {
URL string `json:"url"`
Fields map[string]string `json:"fields"`
} `json:"upload_data"`
AssetID string `json:"asset_id"`
AssetURL string `json:"asset_url"`
}
// CreateProjectAsset 创建项目资源并获取预签名 URL
func (c *Client) CreateProjectAsset(slug, projectID, fileName, mimeType string, size int64, entityType string) (*AssetUploadResult, error) {
path := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/", slug, projectID)
payload := map[string]interface{}{
"name": fileName,
"type": mimeType,
"size": size,
"entity_type": entityType,
"entity_identifier": "",
}
body, code, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
var result AssetUploadResult
json.Unmarshal(body, &result)
return &result, nil
}
// UploadToS3 上传文件到 S3(使用预签名 URL)
func (c *Client) UploadToS3(uploadURL string, fields map[string]string, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
for k, v := range fields {
writer.WriteField(k, v)
}
part, _ := writer.CreateFormFile("file", filepath.Base(filePath))
io.Copy(part, file)
writer.Close()
req, _ := http.NewRequest(http.MethodPost, uploadURL, &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := (&http.Client{}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 204 {
return fmt.Errorf("S3 upload failed: %d", resp.StatusCode)
}
return nil
}
// ConfirmAssetUpload 确认资源上传完成
func (c *Client) ConfirmAssetUpload(slug, projectID, assetID string) error {
path := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", slug, projectID, assetID)
_, code, err := c.jsonRequest(http.MethodPatch, path, nil)
if err != nil || code != 204 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// BindAssetsToEntity 批量绑定资源到实体
func (c *Client) BindAssetsToEntity(slug, projectID, entityID string, assetIDs []string) error {
path := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/bulk/", slug, projectID, entityID)
payload := map[string]interface{}{"asset_ids": assetIDs}
_, code, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || code != 204 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// DeleteAsset 删除资源
func (c *Client) DeleteAsset(slug, projectID, assetID string) error {
path := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", slug, projectID, assetID)
_, code, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || code != 204 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// UploadInlineImage 完整上传内联图片流程(组合方法)
func (c *Client) UploadInlineImage(slug, projectID, filePath, mimeType string) (assetID, assetURL string, err error) {
info, err := os.Stat(filePath)
if err != nil {
return "", "", err
}
// 1. 创建资源
result, err := c.CreateProjectAsset(slug, projectID, info.Name(), mimeType, info.Size(), "ISSUE_DESCRIPTION")
if err != nil {
return "", "", err
}
// 2. 上传到 S3
if err := c.UploadToS3(result.UploadData.URL, result.UploadData.Fields, filePath); err != nil {
return "", "", err
}
// 3. 确认
if err := c.ConfirmAssetUpload(slug, projectID, result.AssetID); err != nil {
return "", "", err
}
return result.AssetID, result.AssetURL, nil
}
15. 页面 (Pages)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/projects/{pid}/pages/ |
列出页面 |
| POST |
/api/workspaces/{slug}/projects/{pid}/pages/ |
创建页面 |
| GET |
.../pages/{page_id}/ |
获取页面详情 |
| PATCH |
.../pages/{page_id}/ |
更新页面 |
| DELETE |
.../pages/{page_id}/ |
删除页面 |
| GET |
.../pages/{page_id}/description/ |
获取页面描述 |
| PATCH |
.../pages/{page_id}/description/ |
更新页面描述 |
| POST |
.../pages/{page_id}/archive/ |
归档页面 |
| DELETE |
.../pages/{page_id}/archive/ |
取消归档 |
| POST |
.../pages/{page_id}/lock/ |
锁定页面 |
| DELETE |
.../pages/{page_id}/lock/ |
解锁页面 |
| POST |
.../pages/{page_id}/access/ |
设置访问权限 |
| POST |
.../pages/{page_id}/duplicate/ |
复制页面 |
创建页面参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
是 |
页面标题 |
description_html |
string |
否 |
HTML 内容 |
access |
integer |
否 |
0=私密, 1=公开(默认 0) |
color |
string |
否 |
页面颜色 |
label_ids |
array[uuid] |
否 |
标签列表 |
设置访问权限参数
| 字段 |
类型 |
必填 |
说明 |
access |
integer |
是 |
0=私密, 1=公开 |
Go 示例
// ListPages 列出页面
func (c *Client) ListPages(slug, projectID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", slug, projectID)
body, code, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d", code)
}
return body, nil
}
// CreatePage 创建页面
func (c *Client) CreatePage(slug, projectID, name, descriptionHTML string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", slug, projectID)
payload := map[string]interface{}{
"name": name,
"description_html": descriptionHTML,
}
body, code, err := c.jsonRequest(http.MethodPost, path, payload)
if err != nil || code != 201 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
// GetPage 获取页面详情
func (c *Client) GetPage(slug, projectID, pageID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", slug, projectID, pageID)
body, code, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d", code)
}
return body, nil
}
// UpdatePage 更新页面
func (c *Client) UpdatePage(slug, projectID, pageID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", slug, projectID, pageID)
body, code, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
// UpdatePageDescription 更新页面描述内容
func (c *Client) UpdatePageDescription(slug, projectID, pageID, descHTML string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/description/", slug, projectID, pageID)
payload := map[string]interface{}{"description_html": descHTML}
body, code, err := c.jsonRequest(http.MethodPatch, path, payload)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
// DeletePage 删除页面
func (c *Client) DeletePage(slug, projectID, pageID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", slug, projectID, pageID)
_, code, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || code != 204 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// ArchivePage 归档页面
func (c *Client) ArchivePage(slug, projectID, pageID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", slug, projectID, pageID)
_, code, err := c.jsonRequest(http.MethodPost, path, map[string]interface{}{})
if err != nil || code != 200 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// LockPage 锁定页面
func (c *Client) LockPage(slug, projectID, pageID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/lock/", slug, projectID, pageID)
_, code, err := c.jsonRequest(http.MethodPost, path, map[string]interface{}{})
if err != nil || code != 200 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// UnlockPage 解锁页面
func (c *Client) UnlockPage(slug, projectID, pageID string) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/lock/", slug, projectID, pageID)
_, code, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || code != 200 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// SetPageAccess 设置页面访问权限
func (c *Client) SetPageAccess(slug, projectID, pageID string, access int) error {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/access/", slug, projectID, pageID)
_, code, err := c.jsonRequest(http.MethodPost, path, map[string]interface{}{"access": access})
if err != nil || code != 200 {
return fmt.Errorf("status: %d", code)
}
return nil
}
// DuplicatePage 复制页面
func (c *Client) DuplicatePage(slug, projectID, pageID string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/duplicate/", slug, projectID, pageID)
body, code, err := c.jsonRequest(http.MethodPost, path, map[string]interface{}{})
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
16. 便签 (Stickies)
端点列表
| 方法 |
路径 |
说明 |
| GET |
/api/workspaces/{slug}/stickies/ |
列出便签 |
| POST |
/api/workspaces/{slug}/stickies/ |
创建便签 |
| GET |
/api/workspaces/{slug}/stickies/{id}/ |
获取便签 |
| PATCH |
/api/workspaces/{slug}/stickies/{id}/ |
更新便签 |
| DELETE |
/api/workspaces/{slug}/stickies/{id}/ |
删除便签 |
创建便签参数
| 字段 |
类型 |
必填 |
说明 |
name |
string |
否 |
标题 |
description_html |
string |
否 |
HTML 内容 |
color |
string |
否 |
前景色 |
background_color |
string |
否 |
背景色 |
Go 示例
// ListStickies 列出便签
func (c *Client) ListStickies(slug string) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/stickies/", slug)
body, code, err := c.jsonRequest(http.MethodGet, path, nil)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d", code)
}
return body, nil
}
// CreateSticky 创建便签
func (c *Client) CreateSticky(slug string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/stickies/", slug)
body, code, err := c.jsonRequest(http.MethodPost, path, data)
if err != nil || code != 201 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
// UpdateSticky 更新便签
func (c *Client) UpdateSticky(slug, stickyID string, data map[string]interface{}) ([]byte, error) {
path := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", slug, stickyID)
body, code, err := c.jsonRequest(http.MethodPatch, path, data)
if err != nil || code != 200 {
return nil, fmt.Errorf("status: %d, body: %s", code, string(body))
}
return body, nil
}
// DeleteSticky 删除便签
func (c *Client) DeleteSticky(slug, stickyID string) error {
path := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", slug, stickyID)
_, code, err := c.jsonRequest(http.MethodDelete, path, nil)
if err != nil || code != 204 {
return fmt.Errorf("status: %d", code)
}
return nil
}
17. 附录
A. 完整使用示例:创建带内联图片的 Intake 工作项
package main
import (
"fmt"
"log"
)
func main() {
// 初始化客户端
client, err := NewClient("http://localhost:8000")
if err != nil {
log.Fatal(err)
}
// 登录
if err := client.Login("admin@example.com", "password123"); err != nil {
log.Fatal(err)
}
fmt.Println("✓ 登录成功")
slug := "my-workspace"
projectID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
// 上传内联图片
assetID, assetURL, err := client.UploadInlineImage(slug, projectID, "./bug-screenshot.png", "image/png")
if err != nil {
log.Fatal(err)
}
fmt.Printf("✓ 图片上传成功: %s\n", assetID)
// 创建 Intake 工作项
descHTML := fmt.Sprintf(`<p>发现 UI 问题,详见截图:</p><img src="%s" /><p>请优先处理。</p>`, assetURL)
issueData := map[string]interface{}{
"name": "UI Bug - 按钮错位",
"description_html": descHTML,
"priority": "high",
"label_ids": []string{},
"assignee_ids": []string{},
}
respBody, err := client.CreateIntakeIssue(slug, projectID, issueData)
if err != nil {
log.Fatal(err)
}
// 从响应中提取 issue ID
var resp struct {
Issue struct {
ID string `json:"id"`
} `json:"issue"`
}
json.Unmarshal(respBody, &resp)
fmt.Printf("✓ Intake 工作项创建成功: %s\n", resp.Issue.ID)
// 绑定资源
if err := client.BindAssetsToEntity(slug, projectID, resp.Issue.ID, []string{assetID}); err != nil {
log.Fatal(err)
}
fmt.Println("✓ 资源绑定成功")
}
B. 完整使用示例:项目管理全流程
func projectWorkflow() {
client, _ := NewClient("http://localhost:8000")
client.Login("admin@example.com", "password123")
slug := "my-workspace"
// 创建项目
project, _ := client.CreateProject(slug, map[string]interface{}{
"name": "Backend Refactor",
"identifier": "BR",
"network": 2,
"cycle_view": true,
"module_view": true,
"intake_view": true,
})
fmt.Println("项目已创建:", string(project))
projectID := "从响应中解析"
// 创建状态
client.CreateState(slug, projectID, "Review", "#9333EA", "started")
// 创建标签
client.CreateLabel(slug, projectID, "backend", "#3B82F6")
// 创建工作项
issue, _ := client.CreateIssue(slug, projectID, map[string]interface{}{
"name": "重构用户认证模块",
"description_html": "<p>将认证逻辑抽象为独立服务</p>",
"priority": "high",
"assignee_ids": []string{},
"label_ids": []string{},
})
fmt.Println("工作项已创建:", string(issue))
}
C. 完整使用示例:迭代管理
func cycleWorkflow() {
client, _ := NewClient("http://localhost:8000")
client.Login("admin@example.com", "password123")
slug := "my-workspace"
projectID := "project-uuid"
// 创建迭代
cycle, _ := client.CreateCycle(slug, projectID, map[string]interface{}{
"name": "Sprint 1",
"start_date": "2024-01-15T00:00:00Z",
"end_date": "2024-01-29T23:59:59Z",
})
fmt.Println("迭代已创建:", string(cycle))
cycleID := "从响应解析"
issueID := "已有工作项ID"
// 添加工作项到迭代
client.AddIssuesToCycle(slug, projectID, cycleID, []string{issueID})
// 查看进度
progress, _ := client.GetCycleProgress(slug, projectID, cycleID)
fmt.Println("迭代进度:", string(progress))
}
D. 错误码参考
| 状态码 |
含义 |
处理方式 |
| 200 |
成功 |
正常处理 |
| 201 |
创建成功 |
正常处理 |
| 204 |
无内容(删除/确认成功) |
无需解析 Body |
| 302 |
重定向(登录成功/资源下载) |
提取 Cookie 或跟随重定向 |
| 400 |
参数错误 |
检查请求体 |
| 403 |
权限不足或 CSRF 失败 |
检查角色/重新获取 CSRF |
| 404 |
资源不存在 |
检查路径参数 |
| 409 |
冲突(名称重复等) |
检查唯一性约束 |
E. 与 API v1 (X-Api-Key) 对比
| 特性 |
Web API (本文档) |
API v1 |
| 认证 |
Session + CSRF |
X-Api-Key Header |
| Intake 创建 |
完整字段 (assignees, labels, dates) |
仅 name, description, priority |
| 描述内联图片 |
支持 |
不支持 |
| 资源上传 |
Assets v2 全功能 |
仅附件 |
| 工作项字段 |
assignee_ids / label_ids / state_id |
assignees / labels / state |
| URL 前缀 |
/api/ |
/api/v1/ |
| Labels 路径 |
/issue-labels/ |
/labels/ |
| 适用场景 |
后端自动化、脚本、完整功能集成 |
第三方集成、外部 Webhook |
F. Session 管理建议
// 带自动重试的请求包装
func (c *Client) withRetry(email, password string, fn func() error) error {
err := fn()
if err != nil && (strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "401")) {
// Session 过期,重新登录
if loginErr := c.Login(email, password); loginErr != nil {
return fmt.Errorf("重新登录失败: %w", loginErr)
}
return fn()
}
return err
}
G. 所有端点快速参考
| 模块 |
列出 |
创建 |
详情 |
更新 |
删除 |
| 工作空间 |
GET /api/workspaces/ |
POST /api/workspaces/ |
GET /api/workspaces/{slug}/ |
PATCH /api/workspaces/{slug}/ |
DELETE /api/workspaces/{slug}/ |
| 项目 |
GET .../projects/ |
POST .../projects/ |
GET .../projects/{id}/ |
PATCH .../projects/{id}/ |
DELETE .../projects/{id}/ |
| 工作项 |
GET .../issues/ |
POST .../issues/ |
GET .../issues/{id}/ |
PATCH .../issues/{id}/ |
DELETE .../issues/{id}/ |
| 评论 |
GET .../comments/ |
POST .../comments/ |
GET .../comments/{id}/ |
PATCH .../comments/{id}/ |
DELETE .../comments/{id}/ |
| 链接 |
GET .../issue-links/ |
POST .../issue-links/ |
GET .../issue-links/{id}/ |
PATCH .../issue-links/{id}/ |
DELETE .../issue-links/{id}/ |
| 迭代 |
GET .../cycles/ |
POST .../cycles/ |
GET .../cycles/{id}/ |
PATCH .../cycles/{id}/ |
DELETE .../cycles/{id}/ |
| 模块 |
GET .../modules/ |
POST .../modules/ |
GET .../modules/{id}/ |
PATCH .../modules/{id}/ |
DELETE .../modules/{id}/ |
| 状态 |
GET .../states/ |
POST .../states/ |
GET .../states/{id}/ |
PATCH .../states/{id}/ |
DELETE .../states/{id}/ |
| 标签 |
GET .../issue-labels/ |
POST .../issue-labels/ |
GET .../issue-labels/{id}/ |
PATCH .../issue-labels/{id}/ |
DELETE .../issue-labels/{id}/ |
| 成员 |
GET .../members/ |
POST .../members/ |
GET .../members/{id}/ |
PATCH .../members/{id}/ |
DELETE .../members/{id}/ |
| 收件箱 |
GET .../intake-issues/ |
POST .../intake-issues/ |
GET .../intake-issues/{id}/ |
PATCH .../intake-issues/{id}/ |
DELETE .../intake-issues/{id}/ |
| 页面 |
GET .../pages/ |
POST .../pages/ |
GET .../pages/{id}/ |
PATCH .../pages/{id}/ |
DELETE .../pages/{id}/ |
| 便签 |
GET .../stickies/ |
POST .../stickies/ |
GET .../stickies/{id}/ |
PATCH .../stickies/{id}/ |
DELETE .../stickies/{id}/ |