# Plane REST API v1 完整使用文档 > **版本**: v0.0.1 > **Base URL**: `/api/v1/` > **官方文档**: [developers.plane.so](https://developers.plane.so/api-reference/introduction) > 本文档覆盖 Plane API v1 的所有端点,包含详细参数说明和 Go 语言代码示例。 --- ## 目录 1. [认证与客户端基础](#1-认证与客户端基础) 2. [用户 (Users)](#2-用户-users) 3. [项目 (Projects)](#3-项目-projects) 4. [工作项 (Work Items)](#4-工作项-work-items) 5. [工作项链接 (Links)](#5-工作项链接-links) 6. [工作项评论 (Comments)](#6-工作项评论-comments) 7. [工作项活动 (Activities)](#7-工作项活动-activities) 8. [工作项附件 (Attachments)](#8-工作项附件-attachments) 9. [工作项关系 (Relations)](#9-工作项关系-relations) 10. [工作项搜索 (Search)](#10-工作项搜索-search) 11. [迭代 (Cycles)](#11-迭代-cycles) 12. [模块 (Modules)](#12-模块-modules) 13. [状态 (States)](#13-状态-states) 14. [标签 (Labels)](#14-标签-labels) 15. [成员 (Members)](#15-成员-members) 16. [收件箱/Intake](#16-收件箱intake) 17. [资源 (Assets)](#17-资源-assets) 18. [邀请 (Invitations)](#18-邀请-invitations) 19. [便签 (Stickies)](#19-便签-stickies) 20. [附录](#20-附录) --- ## 1. 认证与客户端基础 ### 1.1 认证方式 API v1 使用 **API Key** 进行认证,在 HTTP Header 中携带: ``` X-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 通用分页响应格式 ```json { "total_count": 100, "next_cursor": "...", "prev_cursor": "...", "next_page_results": true, "prev_page_results": false, "total_pages": 10, "results": [...] } ``` ### 1.4 Go 客户端基础代码 ```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 使用示例 ```go 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 示例 ```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 示例 ```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 示例 ```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 示例 ```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 示例 ```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 示例 ```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 示例 ```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` | 当前在目标之后完成 | ### 列出关系响应 按类型分组的对象: ```json { "blocking": [{"project_id": "...", "issue_id": "..."}], "blocked_by": [...], "duplicate": [...], "relates_to": [...], "start_after": [...], "start_before": [...], "finish_after": [...], "finish_before": [...] } ``` ### Go 示例 ```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 时生效) | ### 响应 ```json { "issues": [ { "id": "uuid", "name": "工作项名称", "sequence_id": 42, "project__identifier": "PROJ", "project_id": "uuid", "workspace__slug": "my-workspace" } ] } ``` ### Go 示例 ```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 示例 ```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 } ``` ```go // 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 示例 ```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 } ``` ```go // 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 示例 ```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 示例 ```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 示例 ```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 示例 ```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 示例 ```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 示例 ```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 示例 ```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. 完整使用示例 ```go 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": "

用户反馈登录页面在手机端无法正常显示

", "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, "

已确认复现,在 iOS Safari 上出现

") 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/` |