|
|
@@ -0,0 +1,2101 @@
|
|
|
+# 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: <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 通用分页响应格式
|
|
|
+
|
|
|
+```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": "<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/` |
|