API_DOCUMENTATION.md 71 KB

Plane REST API v1 完整使用文档

版本: v0.0.1
Base URL: /api/v1/
官方文档: developers.plane.so
本文档覆盖 Plane API v1 的所有端点,包含详细参数说明和 Go 语言代码示例。


目录

  1. 认证与客户端基础
  2. 用户 (Users)
  3. 项目 (Projects)
  4. 工作项 (Work Items)
  5. 工作项链接 (Links)
  6. 工作项评论 (Comments)
  7. 工作项活动 (Activities)
  8. 工作项附件 (Attachments)
  9. 工作项关系 (Relations)
  10. 工作项搜索 (Search)
  11. 迭代 (Cycles)
  12. 模块 (Modules)
  13. 状态 (States)
  14. 标签 (Labels)
  15. 成员 (Members)
  16. 收件箱/Intake
  17. 资源 (Assets)
  18. 邀请 (Invitations)
  19. 便签 (Stickies)
  20. 附录

1. 认证与客户端基础

1.1 认证方式

API v1 使用 API Key 进行认证,在 HTTP Header 中携带:

X-Api-Key: <your-api-key>

API Key 在 Plane 工作空间设置 → API Tokens 中生成。支持设置过期时间。

1.2 通用分页参数

所有列表接口支持以下查询参数:

参数 类型 说明
cursor string 分页游标(从上一页响应的 next_cursor 获取)
per_page integer 每页数量
order_by string 排序字段,前缀 - 表示倒序
fields string 返回字段过滤(逗号分隔)
expand string 展开关联对象(逗号分隔),如 assignees,labels

1.3 通用分页响应格式

{
  "total_count": 100,
  "next_cursor": "...",
  "prev_cursor": "...",
  "next_page_results": true,
  "prev_page_results": false,
  "total_pages": 10,
  "results": [...]
}

1.4 Go 客户端基础代码

package plane

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

// V1Client Plane API v1 客户端
type V1Client struct {
	BaseURL string
	APIKey  string
	HTTP    *http.Client
}

// NewV1Client 创建 API v1 客户端
func NewV1Client(baseURL, apiKey string) *V1Client {
	return &V1Client{
		BaseURL: strings.TrimRight(baseURL, "/"),
		APIKey:  apiKey,
		HTTP:    &http.Client{},
	}
}

// request 执行 HTTP 请求
func (c *V1Client) 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
	}
	req.Header.Set("X-Api-Key", c.APIKey)
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}
	return c.HTTP.Do(req)
}

// jsonRequest 执行 JSON 请求并返回响应体
func (c *V1Client) 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
}

1.5 使用示例

func main() {
	client := NewV1Client("http://localhost:8000", "your-api-key-here")

	// 获取当前用户
	body, status, _ := client.jsonRequest("GET", "/api/v1/users/me/", nil)
	fmt.Printf("Status: %d, Body: %s\n", status, string(body))
}

2. 用户 (Users)

端点

方法 路径 说明
GET /api/v1/users/me/ 获取当前用户信息

响应字段

字段 类型 说明
id uuid 用户 ID
first_name string 名
last_name string 姓
email string 邮箱
avatar string 头像路径
avatar_url string 头像完整 URL
display_name string 显示名称

Go 示例

// GetMe 获取当前用户信息
func (c *V1Client) GetMe() ([]byte, error) {
	body, status, err := c.jsonRequest("GET", "/api/v1/users/me/", nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, err: %v", status, err)
	}
	return body, nil
}

3. 项目 (Projects)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/projects/ 列出项目
POST /api/v1/workspaces/{slug}/projects/ 创建项目
GET /api/v1/workspaces/{slug}/projects/{id}/ 获取详情
PATCH /api/v1/workspaces/{slug}/projects/{id}/ 更新项目
DELETE /api/v1/workspaces/{slug}/projects/{id}/ 删除项目
POST /api/v1/workspaces/{slug}/projects/{id}/archive/ 归档项目
DELETE /api/v1/workspaces/{slug}/projects/{id}/archive/ 取消归档
GET /api/v1/workspaces/{slug}/projects/{id}/summary/ 获取项目摘要

创建项目参数

字段 类型 必填 说明
name string 是 项目名称(最大 255 字符,不可含特殊字符)
identifier string 是 标识符(最大 12 字符,自动转大写)
description string 否 描述
network integer 否 0=私密, 2=公开(默认 2)
project_lead uuid 否 负责人(须为工作空间成员)
default_assignee uuid 否 默认指派人
icon_prop object 否 图标属性
emoji string 否 表情图标
cover_image string 否 封面图 URL
module_view boolean 否 启用模块(默认 false)
cycle_view boolean 否 启用迭代(默认 false)
issue_views_view boolean 否 启用自定义视图(默认 false)
page_view boolean 否 启用页面(默认 true)
intake_view boolean 否 启用收件箱(默认 false)
archive_in integer 否 自动归档天数(0-12,默认 0)
close_in integer 否 自动关闭天数(0-12,默认 0)
timezone string 否 时区(默认 UTC)
external_source string 否 外部来源
external_id string 否 外部 ID

更新项目额外参数

字段 类型 说明
default_state uuid 默认状态 ID(须为本项目状态)
estimate uuid 估算配置 ID

响应字段

字段 类型 说明
id uuid 项目 ID
name string 名称
identifier string 标识符
network integer 可见性
total_members integer 成员总数
total_cycles integer 迭代总数
total_modules integer 模块总数
is_member boolean 当前用户是否为成员
member_role integer 当前用户角色 (20/15/5)
is_deployed boolean 是否已部署
cover_image_url string 封面图完整 URL
created_at datetime 创建时间
updated_at datetime 更新时间

项目摘要查询参数

参数 类型 说明
fields string 逗号分隔,可选:members,states,labels,cycles,modules,issues,intakes,pages

Go 示例

// ListProjects 列出项目
func (c *V1Client) ListProjects(slug string) ([]byte, error) {
	body, status, err := c.jsonRequest("GET", fmt.Sprintf("/api/v1/workspaces/%s/projects/", slug), nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateProject 创建项目
func (c *V1Client) CreateProject(slug string, data map[string]interface{}) ([]byte, error) {
	body, status, err := c.jsonRequest("POST", fmt.Sprintf("/api/v1/workspaces/%s/projects/", slug), data)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// GetProject 获取项目详情
func (c *V1Client) GetProject(slug, projectID string) ([]byte, error) {
	body, status, err := c.jsonRequest("GET", fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/", slug, projectID), nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// UpdateProject 更新项目
func (c *V1Client) UpdateProject(slug, projectID string, data map[string]interface{}) ([]byte, error) {
	body, status, err := c.jsonRequest("PATCH", fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/", slug, projectID), data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteProject 删除项目
func (c *V1Client) DeleteProject(slug, projectID string) error {
	_, status, err := c.jsonRequest("DELETE", fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/", slug, projectID), nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// ArchiveProject 归档项目
func (c *V1Client) ArchiveProject(slug, projectID string) error {
	_, status, err := c.jsonRequest("POST",
		fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/archive/", slug, projectID), map[string]interface{}{})
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// UnarchiveProject 取消归档项目
func (c *V1Client) UnarchiveProject(slug, projectID string) error {
	_, status, err := c.jsonRequest("DELETE",
		fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/archive/", slug, projectID), nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// GetProjectSummary 获取项目摘要
func (c *V1Client) GetProjectSummary(slug, projectID, fields string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/summary/", slug, projectID)
	if fields != "" {
		path += "?fields=" + fields
	}
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

4. 工作项 (Work Items)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/projects/{pid}/work-items/ 列出工作项
POST /api/v1/workspaces/{slug}/projects/{pid}/work-items/ 创建工作项
GET /api/v1/workspaces/{slug}/projects/{pid}/work-items/{id}/ 获取详情
PATCH /api/v1/workspaces/{slug}/projects/{pid}/work-items/{id}/ 更新工作项
PUT /api/v1/workspaces/{slug}/projects/{pid}/work-items/{id}/ Upsert(通过 external_id)
DELETE /api/v1/workspaces/{slug}/projects/{pid}/work-items/{id}/ 删除工作项
GET /api/v1/workspaces/{slug}/work-items/{proj_identifier}-{seq}/ 通过标识符获取

创建工作项参数

字段 类型 必填 说明
name string 是 名称(最大 255 字符)
description_html string 否 HTML 描述(会进行安全校验)
priority string 否 urgent/high/medium/low/none(默认 none)
state uuid 否 状态 ID(须为本项目状态,不填用默认)
parent uuid 否 父工作项 ID
assignees array[uuid] 否 指派人 ID 列表(须为项目中 role>=15 活跃成员)
labels array[uuid] 否 标签 ID 列表(须为本项目标签)
start_date date 否 开始日期 (YYYY-MM-DD,不可晚于 target_date)
target_date date 否 目标日期 (YYYY-MM-DD)
type_id uuid 否 工作项类型 ID
estimate_point uuid 否 估算点 ID
external_source string 否 外部来源标识
external_id string 否 外部 ID(同项目内不可重复)
created_at datetime 否 自定义创建时间(导入用)
created_by uuid 否 自定义创建人(导入用)

列表查询参数

参数 类型 说明
order_by string 排序:-created_at(默认),priority,-priority,state__group,-state__group,assignees__first_name
external_id string 按外部 ID 精确查找
external_source string 按外部来源查找(需配合 external_id)
cursor string 分页游标
per_page integer 每页数量
fields string 返回字段过滤
expand string 展开:assignees,labels

响应字段

字段 类型 说明
id uuid 工作项 ID
name string 名称
description_html string HTML 描述
priority string 优先级
state uuid 状态 ID
parent uuid/null 父工作项 ID
assignees array[uuid] 指派人(expand 时返回用户对象)
labels array[uuid] 标签(expand 时返回标签对象)
start_date date/null 开始日期
target_date date/null 目标日期
sequence_id integer 序列号
sort_order float 排序权重
type_id uuid/null 工作项类型
completed_at datetime/null 完成时间
sub_issues_count integer 子工作项数量
link_count integer 链接数量
attachment_count integer 附件数量
cycle_id uuid/null 所属迭代
external_source string/null 外部来源
external_id string/null 外部 ID
project uuid 项目 ID
workspace uuid 工作空间 ID
created_at datetime 创建时间
updated_at datetime 更新时间

Go 示例

// ListWorkItems 列出工作项
func (c *V1Client) ListWorkItems(slug, projectID string, params map[string]string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/", slug, projectID)
	if len(params) > 0 {
		parts := []string{}
		for k, v := range params {
			parts = append(parts, k+"="+v)
		}
		path += "?" + strings.Join(parts, "&")
	}
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateWorkItem 创建工作项
func (c *V1Client) CreateWorkItem(slug, projectID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/", slug, projectID)
	body, status, err := c.jsonRequest("POST", path, data)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// GetWorkItem 获取工作项详情
func (c *V1Client) GetWorkItem(slug, projectID, workItemID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/", slug, projectID, workItemID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// UpdateWorkItem 更新工作项
func (c *V1Client) UpdateWorkItem(slug, projectID, workItemID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/", slug, projectID, workItemID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// UpsertWorkItem 通过 external_id Upsert 工作项
func (c *V1Client) UpsertWorkItem(slug, projectID string, data map[string]interface{}) ([]byte, int, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/", slug, projectID)
	// PUT 请求需要包含 external_id 和 external_source
	body, status, err := c.jsonRequest("PUT", path, data)
	return body, status, err // 200=更新, 201=创建
}

// DeleteWorkItem 删除工作项(仅 Admin 或创建者)
func (c *V1Client) DeleteWorkItem(slug, projectID, workItemID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/", slug, projectID, workItemID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// GetWorkItemByIdentifier 通过标识符获取(如 PROJ-42)
func (c *V1Client) GetWorkItemByIdentifier(slug, projectIdentifier string, sequenceID int) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/work-items/%s-%d/", slug, projectIdentifier, sequenceID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

5. 工作项链接 (Links)

端点列表

方法 路径 说明
GET .../work-items/{issue_id}/links/ 列出链接
POST .../work-items/{issue_id}/links/ 创建链接
GET .../work-items/{issue_id}/links/{id}/ 获取链接详情
PATCH .../work-items/{issue_id}/links/{id}/ 更新链接
DELETE .../work-items/{issue_id}/links/{id}/ 删除链接

路径前缀: /api/v1/workspaces/{slug}/projects/{project_id}

创建链接参数

字段 类型 必填 说明
url string 是 链接 URL(http/https,同工作项不可重复)
title string 否 链接标题(不填则系统自动爬取)

响应字段

字段 类型 说明
id uuid 链接 ID
title string/null 标题
url string URL
metadata object 元数据
issue uuid 所属工作项 ID
created_by uuid 创建人
created_at datetime 创建时间
updated_at datetime 更新时间

Go 示例

// ListWorkItemLinks 列出链接
func (c *V1Client) ListWorkItemLinks(slug, projectID, issueID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/", slug, projectID, issueID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateWorkItemLink 创建链接
func (c *V1Client) CreateWorkItemLink(slug, projectID, issueID, linkURL, title string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/", slug, projectID, issueID)
	payload := map[string]interface{}{"url": linkURL, "title": title}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// GetWorkItemLink 获取链接详情
func (c *V1Client) GetWorkItemLink(slug, projectID, issueID, linkID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/%s/", slug, projectID, issueID, linkID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// UpdateWorkItemLink 更新链接
func (c *V1Client) UpdateWorkItemLink(slug, projectID, issueID, linkID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/%s/", slug, projectID, issueID, linkID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteWorkItemLink 删除链接
func (c *V1Client) DeleteWorkItemLink(slug, projectID, issueID, linkID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/%s/", slug, projectID, issueID, linkID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

6. 工作项评论 (Comments)

端点列表

方法 路径 说明
GET .../work-items/{issue_id}/comments/ 列出评论
POST .../work-items/{issue_id}/comments/ 创建评论
GET .../work-items/{issue_id}/comments/{id}/ 获取评论详情
PATCH .../work-items/{issue_id}/comments/{id}/ 更新评论
DELETE .../work-items/{issue_id}/comments/{id}/ 删除评论

创建评论参数

字段 类型 必填 说明
comment_html string 是 HTML 内容(会校验安全性)
comment_json object 否 JSON 富文本结构
access string 否 INTERNAL(默认) / EXTERNAL
external_source string 否 外部来源
external_id string 否 外部 ID(同项目不可重复)
created_at datetime 否 自定义创建时间(导入用)
created_by uuid 否 自定义创建人(导入用)

响应字段

字段 类型 说明
id uuid 评论 ID
comment_html string HTML 内容
access string 可见性
actor uuid 评论者 ID
is_member boolean 当前用户是否为项目成员
external_source string/null 外部来源
external_id string/null 外部 ID
edited_at datetime/null 最后编辑时间
created_at datetime 创建时间

Go 示例

// ListWorkItemComments 列出评论
func (c *V1Client) ListWorkItemComments(slug, projectID, issueID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/", slug, projectID, issueID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateWorkItemComment 创建评论
func (c *V1Client) CreateWorkItemComment(slug, projectID, issueID, commentHTML string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/", slug, projectID, issueID)
	payload := map[string]interface{}{"comment_html": commentHTML}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// GetWorkItemComment 获取评论详情
func (c *V1Client) GetWorkItemComment(slug, projectID, issueID, commentID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/%s/", slug, projectID, issueID, commentID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// UpdateWorkItemComment 更新评论
func (c *V1Client) UpdateWorkItemComment(slug, projectID, issueID, commentID, commentHTML string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/%s/", slug, projectID, issueID, commentID)
	payload := map[string]interface{}{"comment_html": commentHTML}
	body, status, err := c.jsonRequest("PATCH", path, payload)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteWorkItemComment 删除评论
func (c *V1Client) DeleteWorkItemComment(slug, projectID, issueID, commentID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/%s/", slug, projectID, issueID, commentID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

7. 工作项活动 (Activities)

端点列表

方法 路径 说明
GET .../work-items/{issue_id}/activities/ 列出活动历史
GET .../work-items/{issue_id}/activities/{id}/ 获取单条活动

响应字段

字段 类型 说明
id uuid 活动 ID
verb string 操作类型(created, updated)
field string/null 变更字段名(state, priority, name 等)
old_value string/null 变更前的值
new_value string/null 变更后的值
old_identifier uuid/null 变更前关联 ID
new_identifier uuid/null 变更后关联 ID
comment string 备注
actor uuid/null 操作人
epoch float/null 时间戳
created_at datetime 创建时间

Go 示例

// ListWorkItemActivities 列出活动历史
func (c *V1Client) ListWorkItemActivities(slug, projectID, issueID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/activities/", slug, projectID, issueID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// GetWorkItemActivity 获取单条活动
func (c *V1Client) GetWorkItemActivity(slug, projectID, issueID, activityID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/activities/%s/", slug, projectID, issueID, activityID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

8. 工作项附件 (Attachments)

端点列表

方法 路径 说明
GET .../work-items/{issue_id}/attachments/ 列出附件
POST .../work-items/{issue_id}/attachments/ 创建附件(获取预签名 URL)
GET .../work-items/{issue_id}/attachments/{id}/ 下载附件(302 重定向)
PATCH .../work-items/{issue_id}/attachments/{id}/ 确认上传
DELETE .../work-items/{issue_id}/attachments/{id}/ 删除附件

创建附件参数(获取预签名 URL)

字段 类型 必填 说明
name string 是 文件名
type string 是 MIME 类型(须为服务端允许的类型)
size integer 是 文件大小(字节)
external_id string 否 外部 ID
external_source string 否 外部来源

创建附件响应

字段 类型 说明
upload_data.url string S3 上传目标 URL
upload_data.fields object 上传表单字段
asset_id uuid 资源 ID
asset_url string 资源访问 URL
attachment object 附件详情

确认上传参数

字段 类型 必填 说明
is_uploaded boolean 是 设为 true 确认上传完成

附件响应字段

字段 类型 说明
id uuid 附件 ID
attributes object {"name":"...", "type":"...", "size":...}
size integer 文件大小
is_uploaded boolean 是否已上传
external_id string/null 外部 ID
external_source string/null 外部来源
created_at datetime 创建时间

上传流程

  1. POST 获取预签名 URL + asset_id
  2. POST 文件到 S3(multipart/form-data)
  3. PATCH 确认上传完成

Go 示例

// ListWorkItemAttachments 列出附件
func (c *V1Client) ListWorkItemAttachments(slug, projectID, issueID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/", slug, projectID, issueID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateWorkItemAttachment 创建附件(获取预签名 URL)
func (c *V1Client) CreateWorkItemAttachment(slug, projectID, issueID, fileName, mimeType string, size int64) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/", slug, projectID, issueID)
	payload := map[string]interface{}{
		"name": fileName,
		"type": mimeType,
		"size": size,
	}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// UploadToS3 上传文件到 S3
func (c *V1Client) 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("POST", 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
}

// ConfirmAttachmentUpload 确认附件上传
func (c *V1Client) ConfirmAttachmentUpload(slug, projectID, issueID, assetID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/%s/", slug, projectID, issueID, assetID)
	payload := map[string]interface{}{"is_uploaded": true}
	_, status, err := c.jsonRequest("PATCH", path, payload)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// DeleteWorkItemAttachment 删除附件
func (c *V1Client) DeleteWorkItemAttachment(slug, projectID, issueID, assetID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/%s/", slug, projectID, issueID, assetID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// UploadAttachment 完整附件上传流程
func (c *V1Client) UploadAttachment(slug, projectID, issueID, filePath, mimeType string) (string, error) {
	info, err := os.Stat(filePath)
	if err != nil {
		return "", err
	}
	// 1. 获取预签名 URL
	respBody, err := c.CreateWorkItemAttachment(slug, projectID, issueID, info.Name(), mimeType, info.Size())
	if err != nil {
		return "", err
	}
	var result struct {
		UploadData struct {
			URL    string            `json:"url"`
			Fields map[string]string `json:"fields"`
		} `json:"upload_data"`
		AssetID string `json:"asset_id"`
	}
	json.Unmarshal(respBody, &result)

	// 2. 上传到 S3
	if err := c.UploadToS3(result.UploadData.URL, result.UploadData.Fields, filePath); err != nil {
		return "", err
	}
	// 3. 确认
	if err := c.ConfirmAttachmentUpload(slug, projectID, issueID, result.AssetID); err != nil {
		return "", err
	}
	return result.AssetID, nil
}

9. 工作项关系 (Relations)

端点列表

方法 路径 说明
GET .../work-items/{issue_id}/relations/ 列出关系
POST .../work-items/{issue_id}/relations/ 创建关系

创建关系参数

字段 类型 必填 说明
relation_type string 是 关系类型
issues array[uuid] 是 关联工作项 ID 列表(至少 1 个)

relation_type 可选值

值 说明
blocking 当前工作项阻塞目标
blocked_by 当前工作项被目标阻塞
duplicate 重复
relates_to 相关
start_before 当前在目标之前开始
start_after 当前在目标之后开始
finish_before 当前在目标之前完成
finish_after 当前在目标之后完成

列出关系响应

按类型分组的对象:

{
  "blocking": [{"project_id": "...", "issue_id": "..."}],
  "blocked_by": [...],
  "duplicate": [...],
  "relates_to": [...],
  "start_after": [...],
  "start_before": [...],
  "finish_after": [...],
  "finish_before": [...]
}

Go 示例

// ListWorkItemRelations 列出关系
func (c *V1Client) ListWorkItemRelations(slug, projectID, issueID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/relations/", slug, projectID, issueID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateWorkItemRelation 创建关系
func (c *V1Client) CreateWorkItemRelation(slug, projectID, issueID, relationType string, targetIDs []string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/relations/", slug, projectID, issueID)
	payload := map[string]interface{}{
		"relation_type": relationType,
		"issues":        targetIDs,
	}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

10. 工作项搜索 (Search)

端点

方法 路径 说明
GET /api/v1/workspaces/{slug}/work-items/search/ 搜索工作项

查询参数

参数 类型 必填 说明
search string 是 搜索关键词(匹配名称、序列号、项目标识符)
limit integer 否 结果数量限制(默认 10)
workspace_search string 否 跨项目搜索:"true"/"false"(默认 "false")
project_id uuid 否 限定项目(仅 workspace_search=false 时生效)

响应

{
  "issues": [
    {
      "id": "uuid",
      "name": "工作项名称",
      "sequence_id": 42,
      "project__identifier": "PROJ",
      "project_id": "uuid",
      "workspace__slug": "my-workspace"
    }
  ]
}

Go 示例

// SearchWorkItems 搜索工作项
func (c *V1Client) SearchWorkItems(slug, query string, limit int, projectID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/work-items/search/?search=%s&limit=%d", slug, query, limit)
	if projectID != "" {
		path += "&project_id=" + projectID
	}
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

11. 迭代 (Cycles)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/projects/{pid}/cycles/ 列出迭代
POST /api/v1/workspaces/{slug}/projects/{pid}/cycles/ 创建迭代
GET .../cycles/{id}/ 获取详情
PATCH .../cycles/{id}/ 更新迭代
DELETE .../cycles/{id}/ 删除迭代
GET .../cycles/{id}/cycle-issues/ 列出迭代中的工作项
POST .../cycles/{id}/cycle-issues/ 添加工作项到迭代
GET .../cycles/{id}/cycle-issues/{issue_id}/ 获取迭代工作项
DELETE .../cycles/{id}/cycle-issues/{issue_id}/ 移除工作项
POST .../cycles/{id}/transfer-issues/ 转移工作项
POST .../cycles/{id}/archive/ 归档
GET .../archived-cycles/ 列出已归档
DELETE .../archived-cycles/{id}/unarchive/ 取消归档

创建迭代参数

字段 类型 必填 说明
name string 是 名称(最大 255 字符)
description string 否 描述
start_date datetime 否 开始时间(不可晚于 end_date)
end_date datetime 否 结束时间
owned_by uuid 否 负责人(默认当前用户)
timezone string 否 时区
external_source string 否 外部来源
external_id string 否 外部 ID

迭代响应字段

字段 类型 说明
id uuid 迭代 ID
name string 名称
description string 描述
start_date datetime/null 开始时间
end_date datetime/null 结束时间
owned_by uuid 负责人
total_issues integer 工作项总数
completed_issues integer 已完成数
started_issues integer 进行中数
unstarted_issues integer 未开始数
backlog_issues integer Backlog 数
cancelled_issues integer 已取消数
total_estimates float 总估算点
completed_estimates float 已完成估算
started_estimates float 进行中估算
archived_at datetime/null 归档时间

添加工作项参数

字段 类型 必填 说明
issues array[uuid] 是 工作项 ID 列表

转移工作项参数

字段 类型 必填 说明
new_cycle_id uuid 是 目标迭代 ID

Go 示例

// ListCycles 列出迭代
func (c *V1Client) ListCycles(slug, projectID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/", slug, projectID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateCycle 创建迭代
func (c *V1Client) CreateCycle(slug, projectID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/", slug, projectID)
	body, status, err := c.jsonRequest("POST", path, data)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}
// GetCycle 获取迭代详情
func (c *V1Client) GetCycle(slug, projectID, cycleID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// UpdateCycle 更新迭代
func (c *V1Client) UpdateCycle(slug, projectID, cycleID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteCycle 删除迭代
func (c *V1Client) DeleteCycle(slug, projectID, cycleID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// AddIssuesToCycle 添加工作项到迭代
func (c *V1Client) AddIssuesToCycle(slug, projectID, cycleID string, issueIDs []string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", slug, projectID, cycleID)
	payload := map[string]interface{}{"issues": issueIDs}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// RemoveIssueFromCycle 从迭代移除工作项
func (c *V1Client) RemoveIssueFromCycle(slug, projectID, cycleID, issueID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/cycle-issues/%s/", slug, projectID, cycleID, issueID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// TransferCycleIssues 转移未完成工作项
func (c *V1Client) TransferCycleIssues(slug, projectID, cycleID, newCycleID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/transfer-issues/", slug, projectID, cycleID)
	payload := map[string]interface{}{"new_cycle_id": newCycleID}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// ArchiveCycle 归档迭代
func (c *V1Client) ArchiveCycle(slug, projectID, cycleID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/archive/", slug, projectID, cycleID)
	_, status, err := c.jsonRequest("POST", path, map[string]interface{}{})
	if err != nil || status != 200 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

12. 模块 (Modules)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/projects/{pid}/modules/ 列出模块
POST /api/v1/workspaces/{slug}/projects/{pid}/modules/ 创建模块
GET .../modules/{id}/ 获取详情
PATCH .../modules/{id}/ 更新模块
DELETE .../modules/{id}/ 删除模块
GET .../modules/{id}/module-issues/ 列出模块工作项
POST .../modules/{id}/module-issues/ 添加工作项
DELETE .../modules/{id}/module-issues/{issue_id}/ 移除工作项
POST .../modules/{id}/archive/ 归档
GET .../archived-modules/ 列出已归档
DELETE .../archived-modules/{id}/unarchive/ 取消归档

创建模块参数

字段 类型 必填 说明
name string 是 名称(项目内唯一)
description string 否 描述
start_date date 否 开始日期 (YYYY-MM-DD)
target_date date 否 目标日期
status string 否 backlog/planned(默认)/in-progress/paused/completed/cancelled
lead uuid 否 负责人
members array[uuid] 否 成员列表(须为项目成员)
external_source string 否 外部来源
external_id string 否 外部 ID

模块响应字段

字段 类型 说明
id uuid 模块 ID
name string 名称
status string 状态
lead uuid/null 负责人
members array[uuid] 成员列表
total_issues integer 工作项总数
completed_issues integer 已完成数
started_issues integer 进行中数
unstarted_issues integer 未开始数
backlog_issues integer Backlog 数
cancelled_issues integer 已取消数
start_date date/null 开始日期
target_date date/null 目标日期

添加工作项参数

字段 类型 必填 说明
issues array[uuid] 是 工作项 ID 列表

Go 示例

// ListModules 列出模块
func (c *V1Client) ListModules(slug, projectID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/", slug, projectID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateModule 创建模块
func (c *V1Client) CreateModule(slug, projectID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/", slug, projectID)
	body, status, err := c.jsonRequest("POST", path, data)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}
// GetModule 获取模块详情
func (c *V1Client) GetModule(slug, projectID, moduleID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// UpdateModule 更新模块
func (c *V1Client) UpdateModule(slug, projectID, moduleID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteModule 删除模块
func (c *V1Client) DeleteModule(slug, projectID, moduleID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// AddIssuesToModule 添加工作项到模块
func (c *V1Client) AddIssuesToModule(slug, projectID, moduleID string, issueIDs []string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/module-issues/", slug, projectID, moduleID)
	payload := map[string]interface{}{"issues": issueIDs}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// RemoveIssueFromModule 从模块移除工作项
func (c *V1Client) RemoveIssueFromModule(slug, projectID, moduleID, issueID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/module-issues/%s/", slug, projectID, moduleID, issueID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

// ArchiveModule 归档模块
func (c *V1Client) ArchiveModule(slug, projectID, moduleID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/archive/", slug, projectID, moduleID)
	_, status, err := c.jsonRequest("POST", path, map[string]interface{}{})
	if err != nil || status != 200 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

13. 状态 (States)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/projects/{pid}/states/ 列出状态
POST /api/v1/workspaces/{slug}/projects/{pid}/states/ 创建状态
GET .../states/{state_id}/ 获取详情
PATCH .../states/{state_id}/ 更新状态
DELETE .../states/{state_id}/ 删除状态

创建状态参数

字段 类型 必填 说明
name string 是 名称(项目内唯一)
color string 是 颜色值(如 #46A758)
group string 是 分组:backlog/unstarted/started/completed/cancelled(不可创建 triage)
description string 否 描述
sequence float 否 排序(默认 65535)
default boolean 否 设为默认(true 时重置其他状态)
external_source string 否 外部来源
external_id string 否 外部 ID

响应字段

字段 类型 说明
id uuid 状态 ID
name string 名称
color string 颜色
group string 分组
sequence float 排序
default boolean 是否默认
slug string 自动生成的 slug
is_triage boolean 是否 Triage 状态

Go 示例

// ListStates 列出状态
func (c *V1Client) ListStates(slug, projectID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/", slug, projectID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateState 创建状态
func (c *V1Client) CreateState(slug, projectID, name, color, group string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/", slug, projectID)
	payload := map[string]interface{}{"name": name, "color": color, "group": group}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// UpdateState 更新状态
func (c *V1Client) UpdateState(slug, projectID, stateID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/%s/", slug, projectID, stateID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteState 删除状态
func (c *V1Client) DeleteState(slug, projectID, stateID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/%s/", slug, projectID, stateID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

14. 标签 (Labels)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/projects/{pid}/labels/ 列出标签
POST /api/v1/workspaces/{slug}/projects/{pid}/labels/ 创建标签
GET .../labels/{id}/ 获取详情
PATCH .../labels/{id}/ 更新标签
DELETE .../labels/{id}/ 删除标签

创建标签参数

字段 类型 必填 说明
name string 是 名称(项目内唯一)
color string 否 颜色值
description string 否 描述
parent uuid 否 父标签 ID
sort_order float 否 排序权重
external_source string 否 外部来源
external_id string 否 外部 ID

响应字段

字段 类型 说明
id uuid 标签 ID
name string 名称
color string 颜色
description string 描述
parent uuid/null 父标签
sort_order float 排序
project uuid 项目 ID
created_at datetime 创建时间

Go 示例

// ListLabels 列出标签
func (c *V1Client) ListLabels(slug, projectID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/", slug, projectID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateLabel 创建标签
func (c *V1Client) CreateLabel(slug, projectID, name, color string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/", slug, projectID)
	payload := map[string]interface{}{"name": name, "color": color}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// UpdateLabel 更新标签
func (c *V1Client) UpdateLabel(slug, projectID, labelID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/%s/", slug, projectID, labelID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteLabel 删除标签
func (c *V1Client) DeleteLabel(slug, projectID, labelID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/%s/", slug, projectID, labelID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

15. 成员 (Members)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/members/ 列出工作空间成员
GET /api/v1/workspaces/{slug}/projects/{pid}/members/ 列出项目成员
POST /api/v1/workspaces/{slug}/projects/{pid}/members/ 添加项目成员
GET .../members/{id}/ 获取成员详情
PATCH .../members/{id}/ 更新角色
DELETE .../members/{id}/ 移除成员

角色值

值 名称 说明
20 Admin 管理员
15 Member 成员
5 Guest 访客

添加项目成员参数

字段 类型 必填 说明
member uuid 是 用户 ID(须为工作空间成员)
role integer 是 角色:20/15/5

响应字段

字段 类型 说明
id uuid 成员记录 ID
member uuid 用户 ID
role integer 角色值

Go 示例

// ListWorkspaceMembers 列出工作空间成员
func (c *V1Client) ListWorkspaceMembers(slug string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/members/", slug)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// ListProjectMembers 列出项目成员
func (c *V1Client) ListProjectMembers(slug, projectID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/", slug, projectID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// AddProjectMember 添加项目成员
func (c *V1Client) AddProjectMember(slug, projectID, memberID string, role int) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/", slug, projectID)
	payload := map[string]interface{}{"member": memberID, "role": role}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// UpdateProjectMemberRole 更新成员角色
func (c *V1Client) UpdateProjectMemberRole(slug, projectID, memberRecordID string, role int) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/%s/", slug, projectID, memberRecordID)
	payload := map[string]interface{}{"role": role}
	body, status, err := c.jsonRequest("PATCH", path, payload)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// RemoveProjectMember 移除项目成员
func (c *V1Client) RemoveProjectMember(slug, projectID, memberRecordID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/%s/", slug, projectID, memberRecordID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

16. 收件箱/Intake

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/projects/{pid}/intake-issues/ 列出收件箱工作项
POST /api/v1/workspaces/{slug}/projects/{pid}/intake-issues/ 创建收件箱工作项
GET .../intake-issues/{issue_id}/ 获取详情
PATCH .../intake-issues/{issue_id}/ 更新
DELETE .../intake-issues/{issue_id}/ 删除

创建收件箱工作项参数

请求体结构:{"issue": {...}}

字段 类型 必填 说明
issue.name string 是 名称
issue.description_html string 否 HTML 描述
issue.description_json object 否 JSON 描述(也接受 description 字段名)
issue.priority string 否 urgent/high/medium/low/none(默认 none)

注意: API v1 的 Intake 接口仅接受 name、description、priority。
如需完整字段(assignees、labels 等),请使用 Web API(Session 认证)。

更新收件箱工作项参数

字段 类型 必填 说明
status integer 否 -2=待处理, -1=拒绝, 0=暂缓, 1=接受, 2=重复
snoozed_till datetime 否 暂缓截止时间
duplicate_to uuid 否 重复目标
source string 否 来源
source_email string 否 来源邮箱
issue object 否 更新工作项内容(name, description_html, priority)

响应字段

字段 类型 说明
id uuid Intake 记录 ID
status integer 状态
source string 来源
snoozed_till datetime/null 暂缓时间
duplicate_to uuid/null 重复目标
issue uuid 工作项 ID
issue_detail object 工作项详情(展开)
inbox uuid 收件箱 ID
created_at datetime 创建时间

Go 示例

// ListIntakeIssues 列出收件箱工作项
func (c *V1Client) ListIntakeIssues(slug, projectID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/", slug, projectID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateIntakeIssue 创建收件箱工作项
func (c *V1Client) CreateIntakeIssue(slug, projectID, name, descHTML, priority string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/", slug, projectID)
	payload := map[string]interface{}{
		"issue": map[string]interface{}{
			"name":             name,
			"description_html": descHTML,
			"priority":         priority,
		},
	}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// GetIntakeIssue 获取收件箱工作项详情
func (c *V1Client) GetIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// UpdateIntakeIssue 更新收件箱工作项
func (c *V1Client) UpdateIntakeIssue(slug, projectID, issueID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// AcceptIntakeIssue 接受收件箱工作项
func (c *V1Client) AcceptIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
	return c.UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{"status": 1})
}

// RejectIntakeIssue 拒绝收件箱工作项
func (c *V1Client) RejectIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
	return c.UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{"status": -1})
}

// DeleteIntakeIssue 删除收件箱工作项
func (c *V1Client) DeleteIntakeIssue(slug, projectID, issueID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

17. 资源 (Assets)

端点列表

方法 路径 说明
POST /api/v1/assets/user-assets/ 上传用户资源
PATCH /api/v1/assets/user-assets/{asset_id}/ 更新用户资源
DELETE /api/v1/assets/user-assets/{asset_id}/ 删除用户资源
POST /api/v1/assets/user-assets/server/ 服务端直传用户资源
POST /api/v1/workspaces/{slug}/assets/ 上传工作空间资源
GET /api/v1/workspaces/{slug}/assets/{asset_id}/ 获取资源
PATCH /api/v1/workspaces/{slug}/assets/{asset_id}/ 更新资源

上传用户资源参数

字段 类型 必填 说明
name string 是 文件名
type string 是 image/jpeg/image/png/image/webp/image/jpg/image/gif
size integer 是 字节数
entity_type string 是 USER_AVATAR / USER_COVER

上传工作空间资源参数

字段 类型 必填 说明
name string 是 文件名
type string 否 MIME 类型
size integer 是 字节数
project_id uuid 否 关联项目
external_id string 否 外部 ID
external_source string 否 外部来源

资源上传响应

字段 类型 说明
upload_data.url string S3 上传 URL
upload_data.fields object 表单字段
asset_id uuid 资源 ID
asset_url string 访问 URL

Go 示例

// UploadUserAsset 上传用户资源
func (c *V1Client) UploadUserAsset(name, mimeType string, size int64, entityType string) ([]byte, error) {
	payload := map[string]interface{}{
		"name":        name,
		"type":        mimeType,
		"size":        size,
		"entity_type": entityType,
	}
	body, status, err := c.jsonRequest("POST", "/api/v1/assets/user-assets/", payload)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// UploadWorkspaceAsset 上传工作空间资源
func (c *V1Client) UploadWorkspaceAsset(slug string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/assets/", slug)
	body, status, err := c.jsonRequest("POST", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

18. 邀请 (Invitations)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/invitations/ 列出邀请
POST /api/v1/workspaces/{slug}/invitations/ 创建邀请
GET .../invitations/{id}/ 获取邀请详情
PATCH .../invitations/{id}/ 更新邀请
DELETE .../invitations/{id}/ 撤销邀请

创建邀请参数

字段 类型 必填 说明
email string 是 邮箱(有效格式,同工作空间不可重复)
role integer 是 角色:20=管理员, 15=成员, 5=访客

响应字段

字段 类型 说明
id uuid 邀请 ID
email string 邮箱
role integer 角色
accepted boolean 是否已接受
responded_at datetime/null 响应时间
created_at datetime 创建时间

Go 示例

// ListInvitations 列出邀请
func (c *V1Client) ListInvitations(slug string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/", slug)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateInvitation 创建邀请
func (c *V1Client) CreateInvitation(slug, email string, role int) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/", slug)
	payload := map[string]interface{}{"email": email, "role": role}
	body, status, err := c.jsonRequest("POST", path, payload)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteInvitation 撤销邀请
func (c *V1Client) DeleteInvitation(slug, invitationID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/%s/", slug, invitationID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

19. 便签 (Stickies)

端点列表

方法 路径 说明
GET /api/v1/workspaces/{slug}/stickies/ 列出便签
POST /api/v1/workspaces/{slug}/stickies/ 创建便签
GET .../stickies/{id}/ 获取便签
PATCH .../stickies/{id}/ 更新便签
DELETE .../stickies/{id}/ 删除便签

创建便签参数

字段 类型 必填 说明
name string 否 标题
description_html string 否 HTML 内容
color string 否 前景色
background_color string 否 背景色
sort_order float 否 排序权重

响应字段

字段 类型 说明
id uuid 便签 ID
name string/null 标题
description_html string HTML 内容
color string/null 前景色
background_color string/null 背景色
sort_order float 排序
workspace uuid 工作空间 ID
owner uuid 所有者
created_at datetime 创建时间

Go 示例

// ListStickies 列出便签
func (c *V1Client) ListStickies(slug string) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/", slug)
	body, status, err := c.jsonRequest("GET", path, nil)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d", status)
	}
	return body, nil
}

// CreateSticky 创建便签
func (c *V1Client) CreateSticky(slug string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/", slug)
	body, status, err := c.jsonRequest("POST", path, data)
	if err != nil || status != 201 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// UpdateSticky 更新便签
func (c *V1Client) UpdateSticky(slug, stickyID string, data map[string]interface{}) ([]byte, error) {
	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/%s/", slug, stickyID)
	body, status, err := c.jsonRequest("PATCH", path, data)
	if err != nil || status != 200 {
		return nil, fmt.Errorf("status: %d, body: %s", status, string(body))
	}
	return body, nil
}

// DeleteSticky 删除便签
func (c *V1Client) DeleteSticky(slug, stickyID string) error {
	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/%s/", slug, stickyID)
	_, status, err := c.jsonRequest("DELETE", path, nil)
	if err != nil || status != 204 {
		return fmt.Errorf("status: %d", status)
	}
	return nil
}

20. 附录

A. 完整使用示例

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	client := NewV1Client("http://localhost:8000", "your-api-key")

	slug := "my-workspace"
	projectID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

	// 获取当前用户
	me, _ := client.GetMe()
	fmt.Println("当前用户:", string(me))

	// 列出项目
	projects, _ := client.ListProjects(slug)
	fmt.Println("项目列表:", string(projects))

	// 创建工作项
	issue, _ := client.CreateWorkItem(slug, projectID, map[string]interface{}{
		"name":             "修复登录页面 Bug",
		"description_html": "<p>用户反馈登录页面在手机端无法正常显示</p>",
		"priority":         "high",
		"assignees":        []string{},
		"labels":           []string{},
	})
	fmt.Println("创建工作项:", string(issue))

	// 解析工作项 ID
	var issueResp struct {
		ID string `json:"id"`
	}
	json.Unmarshal(issue, &issueResp)

	// 添加评论
	comment, _ := client.CreateWorkItemComment(slug, projectID, issueResp.ID,
		"<p>已确认复现,在 iOS Safari 上出现</p>")
	fmt.Println("添加评论:", string(comment))

	// 上传附件
	assetID, _ := client.UploadAttachment(slug, projectID, issueResp.ID, "./screenshot.png", "image/png")
	fmt.Println("附件上传成功:", assetID)

	// 搜索工作项
	results, _ := client.SearchWorkItems(slug, "登录", 5, projectID)
	fmt.Println("搜索结果:", string(results))
}

B. 错误码参考

状态码 含义 处理方式
200 成功 正常解析
201 创建成功 正常解析
204 无内容(删除/确认成功) 无需解析 Body
302 重定向(附件下载) 跟随重定向获取文件
400 参数错误 检查请求体字段
401 未认证(API Key 无效或缺失) 检查 X-Api-Key
403 权限不足 检查用户角色
404 资源不存在 检查路径参数
409 冲突(名称重复、external_id 重复) 检查唯一性

C. expand 参数说明

工作项接口支持 expand 查询参数:

值 效果
assignees 展开为完整用户对象(id, first_name, last_name, email, avatar_url, display_name)
labels 展开为完整标签对象(id, name, color)

未展开时返回 UUID 数组。

示例:GET /api/v1/.../work-items/?expand=assignees,labels


D. 外部集成字段

多数资源支持 external_source + external_id:

用途 说明
导入标识 记录资源在外部系统(如 GitHub/Jira)中的 ID
Upsert PUT 接口通过这两个字段识别已有资源
唯一约束 同一 (project, external_source, external_id) 不可重复

E. 旧版路径兼容

旧路径 (仍可用) 新路径 (推荐)
.../projects/{pid}/issues/ .../projects/{pid}/work-items/
.../issues/{id}/issue-attachments/ .../work-items/{id}/attachments/
.../issues/search/ .../work-items/search/
.../issues/{proj}-{seq}/ .../work-items/{proj}-{seq}/

F. 所有端点快速参考

模块 列出 创建 详情 更新 删除
用户 - - GET /users/me/ - -
项目 GET .../projects/ POST .../projects/ GET .../projects/{id}/ PATCH .../projects/{id}/ DELETE .../projects/{id}/
工作项 GET .../work-items/ POST .../work-items/ GET .../work-items/{id}/ PATCH .../work-items/{id}/ DELETE .../work-items/{id}/
链接 GET .../links/ POST .../links/ GET .../links/{id}/ PATCH .../links/{id}/ DELETE .../links/{id}/
评论 GET .../comments/ POST .../comments/ GET .../comments/{id}/ PATCH .../comments/{id}/ DELETE .../comments/{id}/
活动 GET .../activities/ - GET .../activities/{id}/ - -
附件 GET .../attachments/ POST .../attachments/ GET .../attachments/{id}/ PATCH .../attachments/{id}/ DELETE .../attachments/{id}/
关系 GET .../relations/ POST .../relations/ - - -
搜索 GET .../search/ - - - -
迭代 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 .../labels/ POST .../labels/ GET .../labels/{id}/ PATCH .../labels/{id}/ DELETE .../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 .../invitations/ POST .../invitations/ GET .../invitations/{id}/ PATCH .../invitations/{id}/ DELETE .../invitations/{id}/
便签 GET .../stickies/ POST .../stickies/ GET .../stickies/{id}/ PATCH .../stickies/{id}/ DELETE .../stickies/{id}/

G. Swagger 交互文档

若服务端启用了 ENABLE_DRF_SPECTACULAR:

文档 URL
OpenAPI Schema GET /api/schema/
Swagger UI GET /api/schema/swagger-ui/
ReDoc GET /api/schema/redoc/