소스 검색

feat:新增问题&需求反馈模块

Jerry Liang 3 달 전
부모
커밋
5186d02fca
56개의 변경된 파일과 11574개의 추가작업 그리고 281개의 파일을 삭제
  1. 0 3
      package.json
  2. 43 7
      service/.env
  3. 59 8
      service/config/config.go
  4. 155 0
      service/controllers/feedbackControllers.go
  5. 16 0
      service/dto/feedbackDTO.go
  6. 6 0
      service/main.go
  7. 5 0
      service/middleware/auth.go
  8. 2 1
      service/repositories/api/fwh_pro/fwhHttpClient.go
  9. 2101 0
      service/repositories/api/plane_v1/API_DOCUMENTATION.md
  10. 246 0
      service/repositories/api/plane_v1/planeHttpClient.go
  11. 45 0
      service/repositories/api/plane_v1/plane_activity.go
  12. 228 0
      service/repositories/api/plane_v1/plane_asset.go
  13. 138 0
      service/repositories/api/plane_v1/plane_attachment.go
  14. 101 0
      service/repositories/api/plane_v1/plane_comment.go
  15. 191 0
      service/repositories/api/plane_v1/plane_cycle.go
  16. 187 0
      service/repositories/api/plane_v1/plane_intake.go
  17. 100 0
      service/repositories/api/plane_v1/plane_invitation.go
  18. 100 0
      service/repositories/api/plane_v1/plane_label.go
  19. 100 0
      service/repositories/api/plane_v1/plane_link.go
  20. 119 0
      service/repositories/api/plane_v1/plane_member.go
  21. 172 0
      service/repositories/api/plane_v1/plane_module.go
  22. 156 0
      service/repositories/api/plane_v1/plane_project.go
  23. 50 0
      service/repositories/api/plane_v1/plane_relation.go
  24. 105 0
      service/repositories/api/plane_v1/plane_state.go
  25. 99 0
      service/repositories/api/plane_v1/plane_sticky.go
  26. 25 0
      service/repositories/api/plane_v1/plane_user.go
  27. 148 0
      service/repositories/api/plane_v1/plane_workitem.go
  28. 1910 0
      service/repositories/api/plane_v2/WEB_API_DOCUMENTATION.md
  29. 410 0
      service/repositories/api/plane_v2/planeWebClient.go
  30. 367 0
      service/repositories/api/plane_v2/plane_asset.go
  31. 111 0
      service/repositories/api/plane_v2/plane_comment.go
  32. 279 0
      service/repositories/api/plane_v2/plane_cycle.go
  33. 210 0
      service/repositories/api/plane_v2/plane_intake.go
  34. 187 0
      service/repositories/api/plane_v2/plane_issue.go
  35. 114 0
      service/repositories/api/plane_v2/plane_label.go
  36. 91 0
      service/repositories/api/plane_v2/plane_link.go
  37. 243 0
      service/repositories/api/plane_v2/plane_member.go
  38. 297 0
      service/repositories/api/plane_v2/plane_module.go
  39. 258 0
      service/repositories/api/plane_v2/plane_page.go
  40. 151 0
      service/repositories/api/plane_v2/plane_project.go
  41. 75 0
      service/repositories/api/plane_v2/plane_relation.go
  42. 134 0
      service/repositories/api/plane_v2/plane_state.go
  43. 110 0
      service/repositories/api/plane_v2/plane_sticky.go
  44. 128 0
      service/repositories/api/plane_v2/plane_workspace.go
  45. 315 0
      service/repositories/repositories.go
  46. 34 0
      service/routes/feedbackRoutes.go
  47. 1 0
      service/routes/routes.go
  48. 418 0
      service/services/feedbackServices.go
  49. 11 11
      src/router/record.ts
  50. 73 0
      src/service/feedback/index.ts
  51. 68 0
      src/service/feedback/interface.ts
  52. 14 5
      src/service/fetchClient.ts
  53. 292 0
      src/views/feedback/component/BugReportForm.vue
  54. 263 0
      src/views/feedback/component/FeatureForm.vue
  55. 284 0
      src/views/feedback/component/SubmitHistory.vue
  56. 29 246
      src/views/feedback/index.vue

+ 0 - 3
package.json

@@ -106,8 +106,5 @@
     "overrides": {
       "vite-plugin-vue-devtools>vite-plugin-inspect": "^11.3.0"
     }
-  },
-  "overrides": {
-    "vite-plugin-vue-devtools>vite-plugin-inspect": "^11.3.0"
   }
 }

+ 43 - 7
service/.env

@@ -1,16 +1,52 @@
+# APP CONFIG
 APP_ENV="dev"
 APP_PORT="8301"
-FWH_USER="FMU006"
-FWH_PASSWORD="78728578"
-# FWH_ENERGY="energy.franklinwh.com"
-FWH_ENERGY="energy.fwh-toolbox.top"
-# FWH_OSS="oss.franklinwh.com"
-FWH_OSS="energy-oss.fwh-toolbox.top"
+REDIS_ADDR="localhost:6379"
 MYSQL_DSN="admin:V1hdkLzRoleI0YG6If24@tcp(34.211.183.235:3308)/home_energy_storage_server?charset=utf8mb4&parseTime=True&loc=Local"
 POSTGRES_DSN="host=localhost user=fwh_toolbox password=fwh_toolbox dbname=fwh_toolbox_db port=5432 sslmode=disable"
 MONGO_DSN="mongodb://127.0.0.1:27017/FwhToolboxDbDev"
-REDIS_ADDR="localhost:6379"
 MINIO_ENDPOINT="localhost:9198"
 MINIO_ACCESS_KEY="fwh-toolbox"
 MINIO_SECRET_KEY="fwh-toolbox"
 MINIO_BUCKET_NAME="fwh-toolbox"
+ASYNQMON_PORT=""
+
+# FWH API
+# PRO server
+FWH_ENERGY="energy.franklinwh.com"
+FWH_OSS="oss.franklinwh.com"
+FWH_USER="FMU006"
+FWH_PASSWORD="xxxxxxxx"
+# TEST server
+FWH_TEST_ENERGY="test.franklinwh.com"
+FWH_TEST_OSS="test.franklinwh.com"
+FWH_TEST_USER="SuperAdmin"
+FWH_TEST_PASSWORD="xxxxxxxx"
+
+# FWH MQTT
+# PRO server
+FWH_MQTT_BROKER="tls://a6f63crce3ufk-ats.iot.us-east-2.amazonaws.com:8883"
+FWH_MQTT_CA="./cert/test/AmazonRootCA1.pem"
+FWH_MQTT_CERT="./cert/test/01.certificate.pem.crt"
+FWH_MQTT_KEY="./cert/test/01.private.pem.key"
+# TEST server
+FWH_TEST_MQTT_BROKER="tls://a35sger20yyec4-ats.iot.us-west-2.amazonaws.com:8883"
+FWH_TEST_MQTT_CA="./cert/test/AmazonRootCA1.pem"
+FWH_TEST_MQTT_CERT="./cert/test/01.certificate.pem.crt"
+FWH_TEST_MQTT_KEY="./cert/test/01.private.pem.key"
+
+# PLANE CONFIG
+PLANE_HOST="http://localhost:8600"
+PLANE_API_KEY="plane_api_xxxxx"
+PLANE_WORKSPACE_SLUG="fwh"
+PLANE_PROJECT_ID="55efdd5f-214d-43b3-bed5-a6b71bd04673"
+
+# PLANE WEB CONFIG
+PLANE_WEB_HOST="http://localhost:8600"
+PLANE_WEB_EMAIL="liang_jlyx@163.com"
+PLANE_WEB_PASSWORD="Ljd@163.com"
+PLANE_WEB_WORKSPACE_SLUG="fwh"
+PLANE_WEB_PROJECT_ID="55efdd5f-214d-43b3-bed5-a6b71bd04673"
+PLANE_WEB_LABEL_BUG="415ede7f-a698-426d-b385-ab875a5725dd"
+PLANE_WEB_LABEL_FEATURE="11c51a92-6a76-40d0-aa6c-82f6b52b27eb"
+PLANE_WEB_LABEL_FEEDBACK="f85c7c59-bb11-4035-bce1-3bc417655b70"

+ 59 - 8
service/config/config.go

@@ -36,11 +36,34 @@ type FwhMqttConfig struct {
 	FwhMqttKeyUrl  string
 }
 
+type PlaneApiConfig struct {
+	Host           string
+	ApiKey         string
+	WorkspaceSlug  string
+	ProjectId      string
+	LabelBugId     string
+	LabelFeatureId string
+}
+
+type PlaneWebApiConfig struct {
+	Host           string
+	Email          string
+	Password       string
+	WorkspaceSlug  string
+	ProjectId      string
+	LabelBugId     string
+	LabelFeatureId string
+	LabelFeedbackId string
+}
+
 type Config struct {
 	App         AppConfig
 	FwhApi      FwhApiConfig
+	FwhTestApi	FwhApiConfig
 	FwhMqtt     FwhMqttConfig
-	FwhMqttTest FwhMqttConfig
+	FwhTestMqtt FwhMqttConfig
+	PlaneApi    PlaneApiConfig
+	PlaneWebApi PlaneWebApiConfig
 }
 
 // 需先执行LoadConfig后才能读取ENV
@@ -68,8 +91,8 @@ func LoadConfig() (*Config, error) {
 	v.SetDefault("MINIO_BUCKET_NAME", "fwh-toolbox")
 	v.SetDefault("ASYNQMON_PORT", "8081")
 	// FWH API
-	v.SetDefault("FWH_ENERGY", "energy.fwh-toolbox.top")
-	v.SetDefault("FWH_OSS", "energy-oss.fwh-toolbox.top")
+	v.SetDefault("FWH_ENERGY", "energy.franklinwh.com")
+	v.SetDefault("FWH_OSS", "oss.franklinwh.com")
 	// FWH MQTT
 
 	// 多文件路径支持
@@ -104,6 +127,10 @@ func LoadConfig() (*Config, error) {
 	h.Write([]byte(v.GetString("FWH_PASSWORD")))
 	fwhApiPassword := hex.EncodeToString(h.Sum(nil))
 
+	h = md5.New()
+	h.Write([]byte(v.GetString("FWH_TEST_PASSWORD")))
+	fwhTestApiPassword := hex.EncodeToString(h.Sum(nil))
+
 	return &Config{
 		App: AppConfig{
 			AppEnv:          v.GetString("APP_ENV"),
@@ -124,17 +151,41 @@ func LoadConfig() (*Config, error) {
 			FwhUser:     v.GetString("FWH_USER"),
 			FwhPassword: fwhApiPassword,
 		},
+		FwhTestApi: FwhApiConfig{
+			FwhEnergy:   v.GetString("FWH_TEST_ENERGY"),
+			FwhOss:      v.GetString("FWH_TEST_OSS"),
+			FwhUser:     v.GetString("FWH_TEST_USER"),
+			FwhPassword: fwhTestApiPassword,
+		},
 		FwhMqtt: FwhMqttConfig{
 			FwhMqttBroker:  v.GetString("FWH_MQTT_BROKER"),
 			FwhMqttCaUrl:   v.GetString("FWH_MQTT_CA"),
 			FwhMqttCertUrl: v.GetString("FWH_MQTT_CERT"),
 			FwhMqttKeyUrl:  v.GetString("FWH_MQTT_KEY"),
 		},
-		FwhMqttTest: FwhMqttConfig{
-			FwhMqttBroker:  v.GetString("FWH_MQTT_BROKER"),
-			FwhMqttCaUrl:   v.GetString("FWH_MQTT_CA"),
-			FwhMqttCertUrl: v.GetString("FWH_MQTT_CERT"),
-			FwhMqttKeyUrl:  v.GetString("FWH_MQTT_KEY"),
+		FwhTestMqtt: FwhMqttConfig{
+			FwhMqttBroker:  v.GetString("FWH_TEST_MQTT_BROKER"),
+			FwhMqttCaUrl:   v.GetString("FWH_TEST_MQTT_CA"),
+			FwhMqttCertUrl: v.GetString("FWH_TEST_MQTT_CERT"),
+			FwhMqttKeyUrl:  v.GetString("FWH_TEST_MQTT_KEY"),
+		},
+		PlaneApi: PlaneApiConfig{
+			Host:           v.GetString("PLANE_HOST"),
+			ApiKey:         v.GetString("PLANE_API_KEY"),
+			WorkspaceSlug:  v.GetString("PLANE_WORKSPACE_SLUG"),
+			ProjectId:      v.GetString("PLANE_PROJECT_ID"),
+			LabelBugId:     v.GetString("PLANE_LABEL_BUG"),
+			LabelFeatureId: v.GetString("PLANE_LABEL_FEATURE"),
+		},
+		PlaneWebApi: PlaneWebApiConfig{
+			Host:            v.GetString("PLANE_WEB_HOST"),
+			Email:           v.GetString("PLANE_WEB_EMAIL"),
+			Password:        v.GetString("PLANE_WEB_PASSWORD"),
+			WorkspaceSlug:   v.GetString("PLANE_WEB_WORKSPACE_SLUG"),
+			ProjectId:       v.GetString("PLANE_WEB_PROJECT_ID"),
+			LabelBugId:      v.GetString("PLANE_WEB_LABEL_BUG"),
+			LabelFeatureId:  v.GetString("PLANE_WEB_LABEL_FEATURE"),
+			LabelFeedbackId: v.GetString("PLANE_WEB_LABEL_FEEDBACK"),
 		},
 	}, nil
 }

+ 155 - 0
service/controllers/feedbackControllers.go

@@ -0,0 +1,155 @@
+/*
+ * Feedback API List
+ * /feedback/submit       - POST   提交问题反馈或需求
+ * /feedback/history      - GET    查询提交历史
+ * /feedback/upload       - POST   上传附件到 Plane (返回 assetId)
+ * /feedback/asset/:id    - DELETE 删除已上传的附件
+ */
+package controllers
+
+import (
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/middleware"
+	"fwh-toolbox-server/services"
+
+	"github.com/gofiber/fiber/v2"
+	"go.uber.org/zap"
+)
+
+// 提交反馈
+// /feedback/submit
+func SubmitFeedback(c *fiber.Ctx) error {
+	var body dto.FeedbackSubmitDTO
+	if err := c.BodyParser(&body); err != nil {
+		zap.L().Warn("Invalid request body", zap.String("err", err.Error()))
+		return Fail(c, "Invalid request body")
+	}
+
+	// 参数校验
+	if body.Title == "" {
+		return Fail(c, "标题不能为空")
+	}
+	if body.Type != "bug" && body.Type != "feature" {
+		return Fail(c, "类型必须是 bug 或 feature")
+	}
+
+	// 从 JWT 中获取用户名
+	userName := middleware.AuthGetDisplayName(c)
+
+	result, err := services.SubmitFeedback(body, userName)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+
+	return Success(c, result)
+}
+
+// 查询提交历史
+// /feedback/history
+func GetFeedbackHistory(c *fiber.Ctx) error {
+	// 从 JWT 中获取用户名
+	userName := ""
+	if name, ok := c.Locals("userName").(string); ok && name != "" {
+		userName = name
+	}
+
+	result, err := services.GetFeedbackHistory(userName)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+
+	return Success(c, result)
+}
+
+// 上传附件到 Plane(立即上传,返回 assetId 供后续提交使用)
+// /feedback/upload
+func UploadFeedbackAsset(c *fiber.Ctx) error {
+	file, err := c.FormFile("file")
+	if err != nil {
+		return Fail(c, "请选择文件")
+	}
+
+	// 限制文件大小 10MB
+	if file.Size > 10*1024*1024 {
+		return Fail(c, "文件大小不能超过 10MB")
+	}
+
+	// 读取文件内容
+	src, err := file.Open()
+	if err != nil {
+		return Fail(c, "文件读取失败")
+	}
+	defer src.Close()
+
+	fileData := make([]byte, file.Size)
+	if _, err := src.Read(fileData); err != nil {
+		return Fail(c, "文件读取失败")
+	}
+
+	// 获取 Content-Type
+	contentType := file.Header.Get("Content-Type")
+	if contentType == "" {
+		contentType = "application/octet-stream"
+	}
+
+	result, err := services.UploadFeedbackAsset(file.Filename, contentType, file.Size, fileData)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+
+	// 返回 assetId 和 assetUrl
+	return Success(c, fiber.Map{
+		"assetId":  result.AssetID,
+		"assetUrl": result.AssetURL,
+	})
+}
+
+// 获取反馈详情(含描述内容)
+// /feedback/detail/:id
+func GetFeedbackDetail(c *fiber.Ctx) error {
+	issueId := c.Params("id")
+	if issueId == "" {
+		return Fail(c, "issue id 不能为空")
+	}
+
+	result, err := services.GetFeedbackDetail(issueId)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+
+	return Success(c, result)
+}
+
+// 代理获取附件文件(从 Plane 下载后转发给前端)
+// /feedback/asset/view/:id
+func ViewFeedbackAsset(c *fiber.Ctx) error {
+	assetId := c.Params("id")
+	if assetId == "" {
+		return Fail(c, "asset id 不能为空")
+	}
+
+	contentType, data, err := services.ProxyFeedbackAsset(assetId)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+
+	c.Set("Content-Type", contentType)
+	c.Set("Cache-Control", "private, max-age=3600")
+	return c.Send(data)
+}
+
+// 删除已上传的附件
+// /feedback/asset/:id
+func DeleteFeedbackAsset(c *fiber.Ctx) error {
+	assetId := c.Params("id")
+	if assetId == "" {
+		return Fail(c, "asset id 不能为空")
+	}
+
+	err := services.DeleteFeedbackAsset(assetId)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+
+	return Success[any](c, nil, "删除成功")
+}

+ 16 - 0
service/dto/feedbackDTO.go

@@ -0,0 +1,16 @@
+package dto
+
+// FeedbackSubmitDTO 问题反馈/需求提交请求体
+type FeedbackSubmitDTO struct {
+	Title       string   `json:"title"`       // 标题(必填)
+	Type        string   `json:"type"`        // 类型: bug / feature
+	Priority    string   `json:"priority"`    // 优先级: urgent / high / medium / low / none
+	Category    string   `json:"category"`    // 分类: bug类-"bug","ui","performance","other" / feature类-"new","improvement","experience"
+	Description string   `json:"description"` // 详细描述
+	Steps       *string  `json:"steps"`       // 复现步骤(Bug 专用)
+	Expected    *string  `json:"expected"`    // 期望结果(Bug 专用)
+	Actual      *string  `json:"actual"`      // 实际结果(Bug 专用)
+	Scenario    *string  `json:"scenario"`    // 使用场景(Feature 专用)
+	RelatedPage *string  `json:"relatedPage"` // 相关页面
+	AssetIds    []string `json:"assetIds"`    // 已上传附件的 Asset ID 列表(Plane V2 Asset)
+}

+ 6 - 0
service/main.go

@@ -59,6 +59,12 @@ func main() {
 	// 初始化FwhApi
 	repositories.FwhHttpInit(&cfg.FwhApi)
 
+	// 初始化PlaneApi
+	repositories.PlaneHttpInit(&cfg.PlaneApi)
+
+	// 初始化PlaneWebApi
+	repositories.PlaneWebHttpInit(&cfg.PlaneWebApi)
+
 	// 初始化计划任务
 	scheduler.Init()
 

+ 5 - 0
service/middleware/auth.go

@@ -146,6 +146,11 @@ func AuthGetUserName(c *fiber.Ctx) string {
 	return userInfo.Name
 }
 
+func AuthGetDisplayName(c *fiber.Ctx) string {
+	userInfo := AuthGetUserInfo(c)
+	return userInfo.DisplayName
+}
+
 func AuthIsAdmin(c *fiber.Ctx) bool {
 	userInfo := AuthGetUserInfo(c)
 	return userInfo.IsAdmin

+ 2 - 1
service/repositories/api/fwh_pro/fwhHttpClient.go

@@ -40,7 +40,8 @@ func getFullEnergyUrl(relativeURL string) string {
 
 // 获取完整OssURL
 func getFullOssUrl(relativeURL string) string {
-	return "https://" + fwhOss + relativeURL
+	// return "https://" + fwhOss + relativeURL
+	return "https://" + fwhEnergy + relativeURL
 }
 
 // 仅用户登录或者不需要token的请求

+ 2101 - 0
service/repositories/api/plane_v1/API_DOCUMENTATION.md

@@ -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/` |

+ 246 - 0
service/repositories/api/plane_v1/planeHttpClient.go

@@ -0,0 +1,246 @@
+package plane_v1
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/config"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+// 官方API手册
+// https://developers.plane.so/api-reference/introduction
+
+const (
+	timeout time.Duration = 60 * time.Second // Plane请求超时60s
+)
+
+var (
+	planeHost          string
+	planeApiKey        string
+	planeWorkspaceSlug string
+	planeProjectId     string
+	planeLabelBugId    string
+	planeLabelFeatureId string
+)
+
+// PlaneConfig Plane配置结构
+type PlaneConfig struct {
+	Host           string
+	ApiKey         string
+	WorkspaceSlug  string
+	ProjectId      string
+	LabelBugId     string
+	LabelFeatureId string
+}
+
+// PlaneHttpInit 初始化Plane HTTP客户端配置
+func PlaneHttpInit(cfg *config.PlaneApiConfig) {
+	planeHost = strings.TrimRight(cfg.Host, "/")
+	planeApiKey = cfg.ApiKey
+	planeWorkspaceSlug = cfg.WorkspaceSlug
+	planeProjectId = cfg.ProjectId
+	planeLabelBugId = cfg.LabelBugId
+	planeLabelFeatureId = cfg.LabelFeatureId
+}
+
+// GetLabelBugId 获取Bug标签ID
+func GetLabelBugId() string {
+	return planeLabelBugId
+}
+
+// GetLabelFeatureId 获取Feature标签ID
+func GetLabelFeatureId() string {
+	return planeLabelFeatureId
+}
+
+// planeResponse 封装Plane HTTP响应
+type planeResponse struct {
+	StatusCode int
+	Status     string
+	Body       []byte
+}
+
+// planeHttpGet 发送GET请求到Plane API
+func planeHttpGet(relativeURL string) (*planeResponse, error) {
+	url := planeHost + relativeURL
+
+	zap.L().Debug("[Plane] HttpGet", zap.String("url", url))
+
+	req, err := http.NewRequest(http.MethodGet, url, nil)
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	req.Header.Set("X-API-Key", planeApiKey)
+	req.Header.Set("Accept", "application/json")
+
+	client := &http.Client{Timeout: timeout}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	return &planeResponse{
+		StatusCode: resp.StatusCode,
+		Status:     resp.Status,
+		Body:       body,
+	}, nil
+}
+
+// planeHttpPost 发送POST请求到Plane API
+func planeHttpPost(relativeURL string, reqBody any) (*planeResponse, error) {
+	url := planeHost + relativeURL
+
+	zap.L().Debug("[Plane] HttpPost", zap.String("url", url))
+
+	jsonData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("序列化请求体失败: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonData))
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	req.Header.Set("X-API-Key", planeApiKey)
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/json")
+
+	client := &http.Client{Timeout: timeout}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	return &planeResponse{
+		StatusCode: resp.StatusCode,
+		Status:     resp.Status,
+		Body:       body,
+	}, nil
+}
+
+// planeHttpPatch 发送PATCH请求到Plane API
+func planeHttpPatch(relativeURL string, reqBody any) (*planeResponse, error) {
+	url := planeHost + relativeURL
+
+	zap.L().Debug("[Plane] HttpPatch", zap.String("url", url))
+
+	jsonData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("序列化请求体失败: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader(jsonData))
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	req.Header.Set("X-API-Key", planeApiKey)
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/json")
+
+	client := &http.Client{Timeout: timeout}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	return &planeResponse{
+		StatusCode: resp.StatusCode,
+		Status:     resp.Status,
+		Body:       body,
+	}, nil
+}
+
+// planeHttpPut 发送PUT请求(用于上传文件到presigned URL,不带API Key)
+func planeHttpPut(url string, data []byte, contentType string) (*planeResponse, error) {
+	zap.L().Debug("[Plane] HttpPut", zap.String("url", url))
+
+	req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	req.Header.Set("Content-Type", contentType)
+
+	client := &http.Client{Timeout: timeout}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	return &planeResponse{
+		StatusCode: resp.StatusCode,
+		Status:     resp.Status,
+		Body:       body,
+	}, nil
+}
+
+// isSuccess 判断HTTP状态码是否表示成功(2xx)
+func isSuccess(statusCode int) bool {
+	return statusCode >= 200 && statusCode < 300
+}
+
+// planeHttpDelete 发送DELETE请求到Plane API
+func planeHttpDelete(relativeURL string) (*planeResponse, error) {
+	url := planeHost + relativeURL
+
+	zap.L().Debug("[Plane] HttpDelete", zap.String("url", url))
+
+	req, err := http.NewRequest(http.MethodDelete, url, nil)
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	req.Header.Set("X-API-Key", planeApiKey)
+	req.Header.Set("Accept", "application/json")
+
+	client := &http.Client{Timeout: timeout}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	return &planeResponse{
+		StatusCode: resp.StatusCode,
+		Status:     resp.Status,
+		Body:       body,
+	}, nil
+}

+ 45 - 0
service/repositories/api/plane_v1/plane_activity.go

@@ -0,0 +1,45 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Work Item Activity API ====================
+
+// ListWorkItemActivities 列出工作项活动历史
+func ListWorkItemActivities(workItemID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/activities/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListWorkItemActivities 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListWorkItemActivities 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkItemActivities 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetWorkItemActivity 获取单条活动详情
+func GetWorkItemActivity(workItemID, activityID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/activities/%s/", planeWorkspaceSlug, planeProjectId, workItemID, activityID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetWorkItemActivity 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetWorkItemActivity 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetWorkItemActivity 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}

+ 228 - 0
service/repositories/api/plane_v1/plane_asset.go

@@ -0,0 +1,228 @@
+package plane_v1
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"strings"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Asset 请求/响应结构 ====================
+
+// CreateAssetResponse Plane Asset创建响应(presigned POST 凭据)
+type CreateAssetResponse struct {
+	UploadData struct {
+		URL    string            `json:"url"`
+		Fields map[string]string `json:"fields"`
+	} `json:"upload_data"`
+	AssetId  string `json:"asset_id"`
+	AssetUrl string `json:"asset_url"`
+}
+
+// AssetUploadResult Asset上传最终结果(返回给前端)
+type AssetUploadResult struct {
+	AssetId  string `json:"assetId"`
+	AssetUrl string `json:"assetUrl"`
+}
+
+// ==================== Asset API操作 ====================
+
+// UploadAsset 通过 Plane Assets API 上传文件(3步流程)
+// Step 1: 获取上传凭据 → Step 2: Presigned POST 上传文件 → Step 3: 确认上传完成
+func UploadAsset(fileName string, contentType string, fileSize int64, fileData []byte) (*AssetUploadResult, error) {
+	// Step 1: 获取上传凭据
+	assetInfo, err := createAsset(fileName, contentType, fileSize)
+	if err != nil {
+		return nil, err
+	}
+
+	zap.L().Debug("[Plane] Asset Create 成功",
+		zap.String("assetId", assetInfo.AssetId),
+		zap.String("uploadURL", assetInfo.UploadData.URL),
+	)
+
+	// Step 2: Presigned POST 上传文件
+	if err := uploadToPresignedPost(assetInfo, fileData, contentType); err != nil {
+		return nil, err
+	}
+
+	// Step 3: 确认上传完成 (PATCH)
+	completeResult, err := completeAssetUpload(assetInfo.AssetId)
+	if err != nil {
+		return nil, err
+	}
+
+	// 解析最终 asset URL
+	assetURL := ""
+	if completeResult != nil && completeResult.AssetUrl != "" {
+		assetURL = completeResult.AssetUrl
+	}
+	if assetURL == "" {
+		assetURL = assetInfo.AssetUrl
+	}
+	if assetURL == "" {
+		assetURL = fmt.Sprintf("%s/api/v1/workspaces/%s/assets/%s/", planeHost, planeWorkspaceSlug, assetInfo.AssetId)
+	}
+
+	zap.L().Info("[Plane] Asset 上传完成",
+		zap.String("assetId", assetInfo.AssetId),
+		zap.String("fileName", fileName),
+		zap.String("assetUrl", assetURL),
+	)
+
+	return &AssetUploadResult{
+		AssetId:  assetInfo.AssetId,
+		AssetUrl: assetURL,
+	}, nil
+}
+
+// createAsset Step 1: 获取上传凭据
+func createAsset(fileName string, contentType string, fileSize int64) (*CreateAssetResponse, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/assets/", planeWorkspaceSlug)
+	body := map[string]interface{}{
+		"name": fileName,
+		"type": contentType,
+		"size": fileSize,
+	}
+
+	resp, err := planeHttpPost(url, body)
+	if err != nil {
+		zap.L().Error("[Plane] Asset Create 请求失败", zap.String("err", err.Error()))
+		return nil, fmt.Errorf("获取上传凭据失败: %w", err)
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] Asset Create 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("获取上传凭据失败: %s", resp.Status)
+	}
+
+	var assetInfo CreateAssetResponse
+	if err := json.Unmarshal(resp.Body, &assetInfo); err != nil {
+		return nil, fmt.Errorf("解析上传凭据失败: %w (body: %s)", err, string(resp.Body))
+	}
+
+	return &assetInfo, nil
+}
+
+// uploadToPresignedPost Step 2: 使用 Presigned POST 上传文件到 S3/MinIO
+// Plane 返回的是 presigned POST(带 fields),需要 multipart/form-data 方式提交
+func uploadToPresignedPost(assetInfo *CreateAssetResponse, fileData []byte, contentType string) error {
+	uploadURL := assetInfo.UploadData.URL
+	if uploadURL == "" {
+		return fmt.Errorf("upload URL 为空")
+	}
+
+	// Plane 容器内使用 host.docker.internal 访问宿主机,
+	// 但后端服务运行在宿主机上,需要替换为 localhost
+	uploadURL = strings.ReplaceAll(uploadURL, "host.docker.internal", "localhost")
+
+	// 构造 multipart/form-data body
+	var buf bytes.Buffer
+	writer := multipart.NewWriter(&buf)
+
+	// 先写入 Plane 返回的所有 fields(必须在 file 之前)
+	for key, value := range assetInfo.UploadData.Fields {
+		if err := writer.WriteField(key, value); err != nil {
+			return fmt.Errorf("写入 field %s 失败: %w", key, err)
+		}
+	}
+
+	// 最后写入文件(field name 必须为 "file")
+	part, err := writer.CreateFormFile("file", "upload")
+	if err != nil {
+		return fmt.Errorf("创建 form file 失败: %w", err)
+	}
+	if _, err := part.Write(fileData); err != nil {
+		return fmt.Errorf("写入文件数据失败: %w", err)
+	}
+
+	if err := writer.Close(); err != nil {
+		return fmt.Errorf("关闭 multipart writer 失败: %w", err)
+	}
+
+	// 发送请求
+	req, err := http.NewRequest(http.MethodPost, uploadURL, &buf)
+	if err != nil {
+		return fmt.Errorf("创建上传请求失败: %w", err)
+	}
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+
+	client := &http.Client{Timeout: 60 * time.Second}
+	resp, err := client.Do(req)
+	if err != nil {
+		zap.L().Error("[Plane] Asset Upload 请求失败", zap.String("err", err.Error()))
+		return fmt.Errorf("文件上传失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		respBody, _ := io.ReadAll(resp.Body)
+		zap.L().Error("[Plane] Asset Upload 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(respBody)),
+		)
+		return fmt.Errorf("文件上传失败: %s", resp.Status)
+	}
+
+	zap.L().Debug("[Plane] Asset Upload 成功", zap.String("url", uploadURL))
+	return nil
+}
+
+// completeAssetUpload Step 3: 确认上传完成
+func completeAssetUpload(assetId string) (*CreateAssetResponse, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/assets/%s/", planeWorkspaceSlug, assetId)
+	body := map[string]interface{}{
+		"is_uploaded": true,
+	}
+
+	resp, err := planeHttpPatch(url, body)
+	if err != nil {
+		zap.L().Error("[Plane] Asset Complete 请求失败", zap.String("err", err.Error()))
+		return nil, fmt.Errorf("确认上传失败: %w", err)
+	}
+
+	// 官方文档: 返回 204 No Content,无 body
+	if resp.StatusCode != 204 && !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] Asset Complete 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("确认上传失败: %s", resp.Status)
+	}
+
+	return &CreateAssetResponse{}, nil
+}
+
+// DeleteAsset 删除 Plane Workspace Asset
+// DELETE /api/v1/workspaces/{workspace_slug}/assets/{asset_id}/
+func DeleteAsset(assetId string) error {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/assets/%s/", planeWorkspaceSlug, assetId)
+
+	resp, err := planeHttpDelete(url)
+	if err != nil {
+		zap.L().Error("[Plane] Asset Delete 请求失败", zap.String("assetId", assetId), zap.String("err", err.Error()))
+		return fmt.Errorf("删除附件失败: %w", err)
+	}
+
+	// 官方文档: 返回 204 No Content
+	if resp.StatusCode != 204 && !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] Asset Delete 返回错误",
+			zap.String("assetId", assetId),
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return fmt.Errorf("删除附件失败: %s", resp.Status)
+	}
+
+	zap.L().Info("[Plane] Asset 删除成功", zap.String("assetId", assetId))
+	return nil
+}

+ 138 - 0
service/repositories/api/plane_v1/plane_attachment.go

@@ -0,0 +1,138 @@
+package plane_v1
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Work Item Attachment API ====================
+
+// AttachmentCreateResponse 附件创建响应
+type AttachmentCreateResponse struct {
+	UploadData struct {
+		URL    string            `json:"url"`
+		Fields map[string]string `json:"fields"`
+	} `json:"upload_data"`
+	AssetId  string `json:"asset_id"`
+	AssetUrl string `json:"asset_url"`
+}
+
+// ListWorkItemAttachments 列出工作项附件
+func ListWorkItemAttachments(workItemID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListWorkItemAttachments 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListWorkItemAttachments 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkItemAttachments 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateWorkItemAttachment 创建附件(获取预签名 URL)
+func CreateWorkItemAttachment(workItemID, fileName, mimeType string, size int64) (*AttachmentCreateResponse, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/", planeWorkspaceSlug, planeProjectId, workItemID)
+	payload := map[string]interface{}{
+		"name": fileName,
+		"type": mimeType,
+		"size": size,
+	}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] CreateWorkItemAttachment 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] CreateWorkItemAttachment 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateWorkItemAttachment 失败: %s", resp.Status)
+	}
+
+	var result AttachmentCreateResponse
+	if err := json.Unmarshal(resp.Body, &result); err != nil {
+		return nil, fmt.Errorf("解析附件创建响应失败: %w", err)
+	}
+
+	return &result, nil
+}
+
+// ConfirmWorkItemAttachment 确认附件上传完成
+func ConfirmWorkItemAttachment(workItemID, assetID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/%s/", planeWorkspaceSlug, planeProjectId, workItemID, assetID)
+	payload := map[string]interface{}{"is_uploaded": true}
+
+	resp, err := planeHttpPatch(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] ConfirmWorkItemAttachment 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 && !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ConfirmWorkItemAttachment 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ConfirmWorkItemAttachment 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// DeleteWorkItemAttachment 删除附件
+func DeleteWorkItemAttachment(workItemID, assetID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/attachments/%s/", planeWorkspaceSlug, planeProjectId, workItemID, assetID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteWorkItemAttachment 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteWorkItemAttachment 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteWorkItemAttachment 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// UploadWorkItemAttachment 完整附件上传流程(获取URL → 上传S3 → 确认)
+func UploadWorkItemAttachment(workItemID, fileName, mimeType string, fileData []byte) (*AssetUploadResult, error) {
+	fileSize := int64(len(fileData))
+
+	// Step 1: 获取预签名 URL
+	createResp, err := CreateWorkItemAttachment(workItemID, fileName, mimeType, fileSize)
+	if err != nil {
+		return nil, fmt.Errorf("获取上传凭据失败: %w", err)
+	}
+
+	// Step 2: 上传到 S3
+	if err := uploadToPresignedPost(&CreateAssetResponse{
+		UploadData: createResp.UploadData,
+		AssetId:    createResp.AssetId,
+		AssetUrl:   createResp.AssetUrl,
+	}, fileData, mimeType); err != nil {
+		return nil, fmt.Errorf("文件上传失败: %w", err)
+	}
+
+	// Step 3: 确认上传完成
+	if err := ConfirmWorkItemAttachment(workItemID, createResp.AssetId); err != nil {
+		return nil, fmt.Errorf("确认上传失败: %w", err)
+	}
+
+	zap.L().Info("[Plane] WorkItem Attachment 上传完成",
+		zap.String("assetId", createResp.AssetId),
+		zap.String("fileName", fileName),
+	)
+
+	return &AssetUploadResult{
+		AssetId:  createResp.AssetId,
+		AssetUrl: createResp.AssetUrl,
+	}, nil
+}

+ 101 - 0
service/repositories/api/plane_v1/plane_comment.go

@@ -0,0 +1,101 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Work Item Comment API ====================
+
+// ListWorkItemComments 列出工作项评论
+func ListWorkItemComments(workItemID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListWorkItemComments 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListWorkItemComments 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkItemComments 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateWorkItemComment 创建评论
+func CreateWorkItemComment(workItemID, commentHTML string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/", planeWorkspaceSlug, planeProjectId, workItemID)
+	payload := map[string]interface{}{"comment_html": commentHTML}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] CreateWorkItemComment 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateWorkItemComment 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateWorkItemComment 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetWorkItemComment 获取评论详情
+func GetWorkItemComment(workItemID, commentID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/%s/", planeWorkspaceSlug, planeProjectId, workItemID, commentID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetWorkItemComment 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetWorkItemComment 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetWorkItemComment 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateWorkItemComment 更新评论
+func UpdateWorkItemComment(workItemID, commentID, commentHTML string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/%s/", planeWorkspaceSlug, planeProjectId, workItemID, commentID)
+	payload := map[string]interface{}{"comment_html": commentHTML}
+
+	resp, err := planeHttpPatch(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateWorkItemComment 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateWorkItemComment 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateWorkItemComment 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteWorkItemComment 删除评论
+func DeleteWorkItemComment(workItemID, commentID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/comments/%s/", planeWorkspaceSlug, planeProjectId, workItemID, commentID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteWorkItemComment 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteWorkItemComment 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteWorkItemComment 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 191 - 0
service/repositories/api/plane_v1/plane_cycle.go

@@ -0,0 +1,191 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Cycle API ====================
+
+// ListCycles 列出迭代
+func ListCycles() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListCycles 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListCycles 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListCycles 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateCycle 创建迭代
+func CreateCycle(data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpPost(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] CreateCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateCycle 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetCycle 获取迭代详情
+func GetCycle(cycleID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/", planeWorkspaceSlug, planeProjectId, cycleID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetCycle 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateCycle 更新迭代
+func UpdateCycle(cycleID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/", planeWorkspaceSlug, planeProjectId, cycleID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateCycle 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteCycle 删除迭代
+func DeleteCycle(cycleID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/", planeWorkspaceSlug, planeProjectId, cycleID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteCycle 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteCycle 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteCycle 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListCycleIssues 列出迭代中的工作项
+func ListCycleIssues(cycleID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", planeWorkspaceSlug, planeProjectId, cycleID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListCycleIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListCycleIssues 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListCycleIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AddIssuesToCycle 添加工作项到迭代
+func AddIssuesToCycle(cycleID string, issueIDs []string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", planeWorkspaceSlug, planeProjectId, cycleID)
+	payload := map[string]interface{}{"issues": issueIDs}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] AddIssuesToCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] AddIssuesToCycle 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("AddIssuesToCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveIssueFromCycle 从迭代移除工作项
+func RemoveIssueFromCycle(cycleID, issueID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/cycle-issues/%s/", planeWorkspaceSlug, planeProjectId, cycleID, issueID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] RemoveIssueFromCycle 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] RemoveIssueFromCycle 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveIssueFromCycle 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// TransferCycleIssues 转移未完成工作项到另一迭代
+func TransferCycleIssues(cycleID, newCycleID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/transfer-issues/", planeWorkspaceSlug, planeProjectId, cycleID)
+	payload := map[string]interface{}{"new_cycle_id": newCycleID}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] TransferCycleIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] TransferCycleIssues 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("TransferCycleIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// ArchiveCycle 归档迭代
+func ArchiveCycle(cycleID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/cycles/%s/archive/", planeWorkspaceSlug, planeProjectId, cycleID)
+
+	resp, err := planeHttpPost(path, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[Plane] ArchiveCycle 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ArchiveCycle 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchiveCycle 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 187 - 0
service/repositories/api/plane_v1/plane_intake.go

@@ -0,0 +1,187 @@
+package plane_v1
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Intake Issue 请求/响应结构 ====================
+
+// IntakeIssueRequest Intake Issue创建请求体
+type IntakeIssueRequest struct {
+	Issue  IntakeIssue `json:"issue"`
+	Source string      `json:"source"`
+}
+
+// IntakeIssue Issue详情
+type IntakeIssue struct {
+	Name            string   `json:"name"`
+	DescriptionHTML string   `json:"description_html"`
+	Priority        string   `json:"priority"`
+	LabelIds        []string `json:"label_ids,omitempty"`
+}
+
+// IntakeIssueResponse Intake Issue创建响应
+type IntakeIssueResponse struct {
+	Id        string `json:"id"`
+	IssueId   string `json:"issue"`
+	Status    int    `json:"status"`
+	Source    string `json:"source"`
+	CreatedAt string `json:"created_at"`
+}
+
+// IntakeListResponse Intake列表响应
+type IntakeListResponse struct {
+	Results    []IntakeListItem `json:"results"`
+	TotalCount int             `json:"total_count"`
+	NextCursor string          `json:"next_cursor"`
+}
+
+// IntakeListItem Intake列表项
+type IntakeListItem struct {
+	Id        string         `json:"id"`
+	Status    int            `json:"status"`
+	Source    string         `json:"source"`
+	CreatedAt string         `json:"created_at"`
+	Issue     IntakeIssueInfo `json:"issue_detail"`
+}
+
+// IntakeIssueInfo Issue基础信息
+type IntakeIssueInfo struct {
+	Id       string `json:"id"`
+	Name     string `json:"name"`
+	Priority string `json:"priority"`
+}
+
+// ==================== Intake Issue API操作 ====================
+
+// CreateIntakeIssue 创建Intake Issue
+func CreateIntakeIssue(req IntakeIssueRequest) (*IntakeIssueResponse, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/",
+		planeWorkspaceSlug,
+		planeProjectId,
+	)
+
+	resp, err := planeHttpPost(url, req)
+	if err != nil {
+		zap.L().Error("[Plane] CreateIntakeIssue 请求失败", zap.String("err", err.Error()))
+		return nil, fmt.Errorf("提交失败: %w", err)
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] CreateIntakeIssue 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("提交失败: Plane 返回 %s", resp.Status)
+	}
+
+	var result IntakeIssueResponse
+	if err := json.Unmarshal(resp.Body, &result); err != nil {
+		zap.L().Error("[Plane] CreateIntakeIssue 响应解析失败", zap.String("err", err.Error()))
+		return nil, fmt.Errorf("响应解析失败: %w", err)
+	}
+
+	zap.L().Info("[Plane] CreateIntakeIssue 成功", zap.String("id", result.Id))
+	return &result, nil
+}
+
+// GetIntakeIssueList 查询Intake Issue列表
+func GetIntakeIssueList() (*IntakeListResponse, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/?per_page=100&expand=issue_detail",
+		planeWorkspaceSlug,
+		planeProjectId,
+	)
+
+	resp, err := planeHttpGet(url)
+	if err != nil {
+		zap.L().Error("[Plane] GetIntakeIssueList 请求失败", zap.String("err", err.Error()))
+		return nil, fmt.Errorf("查询失败: %w", err)
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetIntakeIssueList 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("查询失败: Plane 返回 %s", resp.Status)
+	}
+
+	var listResp IntakeListResponse
+	if err := json.Unmarshal(resp.Body, &listResp); err != nil {
+		zap.L().Error("[Plane] GetIntakeIssueList 响应解析失败", zap.String("err", err.Error()))
+		return nil, fmt.Errorf("响应解析失败: %w", err)
+	}
+
+	zap.L().Debug("[Plane] GetIntakeIssueList", zap.Int("count", len(listResp.Results)))
+	return &listResp, nil
+}
+
+// GetIntakeIssueDetail 获取收件箱工作项详情
+func GetIntakeIssueDetail(issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/%s/",
+		planeWorkspaceSlug, planeProjectId, issueID)
+
+	resp, err := planeHttpGet(url)
+	if err != nil {
+		zap.L().Error("[Plane] GetIntakeIssueDetail 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetIntakeIssueDetail 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetIntakeIssueDetail 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateIntakeIssue 更新收件箱工作项
+func UpdateIntakeIssue(issueID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/%s/",
+		planeWorkspaceSlug, planeProjectId, issueID)
+
+	resp, err := planeHttpPatch(url, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateIntakeIssue 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateIntakeIssue 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateIntakeIssue 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AcceptIntakeIssue 接受收件箱工作项
+func AcceptIntakeIssue(issueID string) ([]byte, error) {
+	return UpdateIntakeIssue(issueID, map[string]interface{}{"status": 1})
+}
+
+// RejectIntakeIssue 拒绝收件箱工作项
+func RejectIntakeIssue(issueID string) ([]byte, error) {
+	return UpdateIntakeIssue(issueID, map[string]interface{}{"status": -1})
+}
+
+// DeleteIntakeIssue 删除收件箱工作项
+func DeleteIntakeIssue(issueID string) error {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/intake-issues/%s/",
+		planeWorkspaceSlug, planeProjectId, issueID)
+
+	resp, err := planeHttpDelete(url)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteIntakeIssue 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteIntakeIssue 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteIntakeIssue 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 100 - 0
service/repositories/api/plane_v1/plane_invitation.go

@@ -0,0 +1,100 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Invitation API ====================
+
+// ListInvitations 列出工作空间邀请
+func ListInvitations() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/", planeWorkspaceSlug)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListInvitations 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListInvitations 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListInvitations 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateInvitation 创建邀请
+func CreateInvitation(email string, role int) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/", planeWorkspaceSlug)
+	payload := map[string]interface{}{"email": email, "role": role}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] CreateInvitation 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateInvitation 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateInvitation 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetInvitation 获取邀请详情
+func GetInvitation(invitationID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/%s/", planeWorkspaceSlug, invitationID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetInvitation 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetInvitation 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetInvitation 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateInvitation 更新邀请
+func UpdateInvitation(invitationID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/%s/", planeWorkspaceSlug, invitationID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateInvitation 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateInvitation 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateInvitation 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteInvitation 撤销邀请
+func DeleteInvitation(invitationID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/invitations/%s/", planeWorkspaceSlug, invitationID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteInvitation 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteInvitation 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteInvitation 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 100 - 0
service/repositories/api/plane_v1/plane_label.go

@@ -0,0 +1,100 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Label API ====================
+
+// ListLabels 列出标签
+func ListLabels() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListLabels 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListLabels 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListLabels 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateLabel 创建标签
+func CreateLabel(name, color string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/", planeWorkspaceSlug, planeProjectId)
+	payload := map[string]interface{}{"name": name, "color": color}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] CreateLabel 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateLabel 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateLabel 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetLabel 获取标签详情
+func GetLabel(labelID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/%s/", planeWorkspaceSlug, planeProjectId, labelID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetLabel 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetLabel 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetLabel 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateLabel 更新标签
+func UpdateLabel(labelID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/%s/", planeWorkspaceSlug, planeProjectId, labelID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateLabel 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateLabel 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateLabel 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteLabel 删除标签
+func DeleteLabel(labelID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/labels/%s/", planeWorkspaceSlug, planeProjectId, labelID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteLabel 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteLabel 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteLabel 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 100 - 0
service/repositories/api/plane_v1/plane_link.go

@@ -0,0 +1,100 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Work Item Link API ====================
+
+// ListWorkItemLinks 列出工作项链接
+func ListWorkItemLinks(workItemID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListWorkItemLinks 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListWorkItemLinks 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkItemLinks 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateWorkItemLink 创建工作项链接
+func CreateWorkItemLink(workItemID, linkURL, title string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/", planeWorkspaceSlug, planeProjectId, workItemID)
+	payload := map[string]interface{}{"url": linkURL, "title": title}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] CreateWorkItemLink 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateWorkItemLink 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateWorkItemLink 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetWorkItemLink 获取链接详情
+func GetWorkItemLink(workItemID, linkID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/%s/", planeWorkspaceSlug, planeProjectId, workItemID, linkID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetWorkItemLink 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetWorkItemLink 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetWorkItemLink 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateWorkItemLink 更新链接
+func UpdateWorkItemLink(workItemID, linkID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/%s/", planeWorkspaceSlug, planeProjectId, workItemID, linkID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateWorkItemLink 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateWorkItemLink 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateWorkItemLink 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteWorkItemLink 删除链接
+func DeleteWorkItemLink(workItemID, linkID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/links/%s/", planeWorkspaceSlug, planeProjectId, workItemID, linkID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteWorkItemLink 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteWorkItemLink 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteWorkItemLink 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 119 - 0
service/repositories/api/plane_v1/plane_member.go

@@ -0,0 +1,119 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Member API ====================
+
+// ListWorkspaceMembers 列出工作空间成员
+func ListWorkspaceMembers() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/members/", planeWorkspaceSlug)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListWorkspaceMembers 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListWorkspaceMembers 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkspaceMembers 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// ListProjectMembers 列出项目成员
+func ListProjectMembers() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListProjectMembers 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListProjectMembers 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListProjectMembers 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AddProjectMember 添加项目成员
+func AddProjectMember(memberID string, role int) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/", planeWorkspaceSlug, planeProjectId)
+	payload := map[string]interface{}{"member": memberID, "role": role}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] AddProjectMember 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] AddProjectMember 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("AddProjectMember 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetProjectMember 获取项目成员详情
+func GetProjectMember(memberRecordID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/%s/", planeWorkspaceSlug, planeProjectId, memberRecordID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetProjectMember 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetProjectMember 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetProjectMember 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateProjectMemberRole 更新项目成员角色
+func UpdateProjectMemberRole(memberRecordID string, role int) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/%s/", planeWorkspaceSlug, planeProjectId, memberRecordID)
+	payload := map[string]interface{}{"role": role}
+
+	resp, err := planeHttpPatch(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateProjectMemberRole 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateProjectMemberRole 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateProjectMemberRole 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveProjectMember 移除项目成员
+func RemoveProjectMember(memberRecordID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/members/%s/", planeWorkspaceSlug, planeProjectId, memberRecordID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] RemoveProjectMember 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] RemoveProjectMember 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveProjectMember 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 172 - 0
service/repositories/api/plane_v1/plane_module.go

@@ -0,0 +1,172 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Module API ====================
+
+// ListModules 列出模块
+func ListModules() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListModules 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListModules 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListModules 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateModule 创建模块
+func CreateModule(data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpPost(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] CreateModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateModule 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetModule 获取模块详情
+func GetModule(moduleID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/", planeWorkspaceSlug, planeProjectId, moduleID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetModule 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateModule 更新模块
+func UpdateModule(moduleID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/", planeWorkspaceSlug, planeProjectId, moduleID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateModule 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteModule 删除模块
+func DeleteModule(moduleID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/", planeWorkspaceSlug, planeProjectId, moduleID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteModule 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteModule 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteModule 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListModuleIssues 列出模块中的工作项
+func ListModuleIssues(moduleID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/module-issues/", planeWorkspaceSlug, planeProjectId, moduleID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListModuleIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListModuleIssues 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListModuleIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AddIssuesToModule 添加工作项到模块
+func AddIssuesToModule(moduleID string, issueIDs []string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/module-issues/", planeWorkspaceSlug, planeProjectId, moduleID)
+	payload := map[string]interface{}{"issues": issueIDs}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] AddIssuesToModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] AddIssuesToModule 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("AddIssuesToModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveIssueFromModule 从模块移除工作项
+func RemoveIssueFromModule(moduleID, issueID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/module-issues/%s/", planeWorkspaceSlug, planeProjectId, moduleID, issueID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] RemoveIssueFromModule 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] RemoveIssueFromModule 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveIssueFromModule 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ArchiveModule 归档模块
+func ArchiveModule(moduleID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/modules/%s/archive/", planeWorkspaceSlug, planeProjectId, moduleID)
+
+	resp, err := planeHttpPost(path, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[Plane] ArchiveModule 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ArchiveModule 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchiveModule 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 156 - 0
service/repositories/api/plane_v1/plane_project.go

@@ -0,0 +1,156 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Project API ====================
+
+// ListProjects 列出项目
+func ListProjects() ([]byte, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/", planeWorkspaceSlug)
+
+	resp, err := planeHttpGet(url)
+	if err != nil {
+		zap.L().Error("[Plane] ListProjects 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListProjects 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListProjects 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateProject 创建项目
+func CreateProject(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/", planeWorkspaceSlug)
+
+	resp, err := planeHttpPost(url, data)
+	if err != nil {
+		zap.L().Error("[Plane] CreateProject 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateProject 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateProject 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetProject 获取项目详情
+func GetProject(projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/", planeWorkspaceSlug, projectID)
+
+	resp, err := planeHttpGet(url)
+	if err != nil {
+		zap.L().Error("[Plane] GetProject 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetProject 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetProject 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateProject 更新项目
+func UpdateProject(projectID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/", planeWorkspaceSlug, projectID)
+
+	resp, err := planeHttpPatch(url, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateProject 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateProject 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateProject 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteProject 删除项目
+func DeleteProject(projectID string) error {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/", planeWorkspaceSlug, projectID)
+
+	resp, err := planeHttpDelete(url)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteProject 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteProject 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteProject 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ArchiveProject 归档项目
+func ArchiveProject(projectID string) error {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/archive/", planeWorkspaceSlug, projectID)
+
+	resp, err := planeHttpPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[Plane] ArchiveProject 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] ArchiveProject 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchiveProject 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// UnarchiveProject 取消归档项目
+func UnarchiveProject(projectID string) error {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/archive/", planeWorkspaceSlug, projectID)
+
+	resp, err := planeHttpDelete(url)
+	if err != nil {
+		zap.L().Error("[Plane] UnarchiveProject 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] UnarchiveProject 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("UnarchiveProject 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetProjectSummary 获取项目摘要
+func GetProjectSummary(projectID, fields string) ([]byte, error) {
+	url := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/summary/", planeWorkspaceSlug, projectID)
+	if fields != "" {
+		url += "?fields=" + fields
+	}
+
+	resp, err := planeHttpGet(url)
+	if err != nil {
+		zap.L().Error("[Plane] GetProjectSummary 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetProjectSummary 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetProjectSummary 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}

+ 50 - 0
service/repositories/api/plane_v1/plane_relation.go

@@ -0,0 +1,50 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Work Item Relation API ====================
+
+// ListWorkItemRelations 列出工作项关系
+func ListWorkItemRelations(workItemID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/relations/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListWorkItemRelations 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListWorkItemRelations 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkItemRelations 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateWorkItemRelation 创建工作项关系
+// relationType: blocking / blocked_by / duplicate / relates_to / start_before / start_after / finish_before / finish_after
+func CreateWorkItemRelation(workItemID, relationType string, targetIDs []string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/relations/", planeWorkspaceSlug, planeProjectId, workItemID)
+	payload := map[string]interface{}{
+		"relation_type": relationType,
+		"issues":        targetIDs,
+	}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] CreateWorkItemRelation 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateWorkItemRelation 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateWorkItemRelation 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}

+ 105 - 0
service/repositories/api/plane_v1/plane_state.go

@@ -0,0 +1,105 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== State API ====================
+
+// ListStates 列出状态
+func ListStates() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListStates 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListStates 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListStates 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateState 创建状态
+// group: backlog / unstarted / started / completed / cancelled
+func CreateState(name, color, group string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/", planeWorkspaceSlug, planeProjectId)
+	payload := map[string]interface{}{
+		"name":  name,
+		"color": color,
+		"group": group,
+	}
+
+	resp, err := planeHttpPost(path, payload)
+	if err != nil {
+		zap.L().Error("[Plane] CreateState 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateState 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateState 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetState 获取状态详情
+func GetState(stateID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/%s/", planeWorkspaceSlug, planeProjectId, stateID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetState 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetState 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetState 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateState 更新状态
+func UpdateState(stateID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/%s/", planeWorkspaceSlug, planeProjectId, stateID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateState 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateState 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateState 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteState 删除状态
+func DeleteState(stateID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/states/%s/", planeWorkspaceSlug, planeProjectId, stateID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteState 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteState 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteState 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 99 - 0
service/repositories/api/plane_v1/plane_sticky.go

@@ -0,0 +1,99 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Sticky (便签) API ====================
+
+// ListStickies 列出便签
+func ListStickies() ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/", planeWorkspaceSlug)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListStickies 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListStickies 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListStickies 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateSticky 创建便签
+func CreateSticky(data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/", planeWorkspaceSlug)
+
+	resp, err := planeHttpPost(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] CreateSticky 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateSticky 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateSticky 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetSticky 获取便签详情
+func GetSticky(stickyID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/%s/", planeWorkspaceSlug, stickyID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetSticky 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetSticky 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetSticky 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateSticky 更新便签
+func UpdateSticky(stickyID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/%s/", planeWorkspaceSlug, stickyID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateSticky 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateSticky 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateSticky 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteSticky 删除便签
+func DeleteSticky(stickyID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/stickies/%s/", planeWorkspaceSlug, stickyID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteSticky 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteSticky 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteSticky 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 25 - 0
service/repositories/api/plane_v1/plane_user.go

@@ -0,0 +1,25 @@
+package plane_v1
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== User API ====================
+
+// GetMe 获取当前用户信息
+func GetMe() ([]byte, error) {
+	resp, err := planeHttpGet("/api/v1/users/me/")
+	if err != nil {
+		zap.L().Error("[Plane] GetMe 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetMe 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetMe 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}

+ 148 - 0
service/repositories/api/plane_v1/plane_workitem.go

@@ -0,0 +1,148 @@
+package plane_v1
+
+import (
+	"fmt"
+	"net/url"
+	"strings"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Work Item API ====================
+
+// ListWorkItems 列出工作项
+func ListWorkItems(params map[string]string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/", planeWorkspaceSlug, planeProjectId)
+	if len(params) > 0 {
+		parts := []string{}
+		for k, v := range params {
+			parts = append(parts, url.QueryEscape(k)+"="+url.QueryEscape(v))
+		}
+		path += "?" + strings.Join(parts, "&")
+	}
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] ListWorkItems 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] ListWorkItems 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkItems 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateWorkItem 创建工作项
+func CreateWorkItem(data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/", planeWorkspaceSlug, planeProjectId)
+
+	resp, err := planeHttpPost(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] CreateWorkItem 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[Plane] CreateWorkItem 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("CreateWorkItem 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetWorkItem 获取工作项详情
+func GetWorkItem(workItemID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetWorkItem 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetWorkItem 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetWorkItem 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateWorkItem 更新工作项
+func UpdateWorkItem(workItemID string, data map[string]interface{}) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpPatch(path, data)
+	if err != nil {
+		zap.L().Error("[Plane] UpdateWorkItem 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] UpdateWorkItem 返回错误", zap.String("status", resp.Status), zap.String("body", string(resp.Body)))
+		return nil, fmt.Errorf("UpdateWorkItem 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteWorkItem 删除工作项
+func DeleteWorkItem(workItemID string) error {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/projects/%s/work-items/%s/", planeWorkspaceSlug, planeProjectId, workItemID)
+
+	resp, err := planeHttpDelete(path)
+	if err != nil {
+		zap.L().Error("[Plane] DeleteWorkItem 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[Plane] DeleteWorkItem 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteWorkItem 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetWorkItemByIdentifier 通过标识符获取工作项(如 PROJ-42)
+func GetWorkItemByIdentifier(projectIdentifier string, sequenceID int) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/work-items/%s-%d/", planeWorkspaceSlug, projectIdentifier, sequenceID)
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] GetWorkItemByIdentifier 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] GetWorkItemByIdentifier 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetWorkItemByIdentifier 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// SearchWorkItems 搜索工作项
+func SearchWorkItems(query string, limit int, projectID string) ([]byte, error) {
+	path := fmt.Sprintf("/api/v1/workspaces/%s/work-items/search/?search=%s&limit=%d",
+		planeWorkspaceSlug, url.QueryEscape(query), limit)
+	if projectID != "" {
+		path += "&project_id=" + projectID
+	}
+
+	resp, err := planeHttpGet(path)
+	if err != nil {
+		zap.L().Error("[Plane] SearchWorkItems 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isSuccess(resp.StatusCode) {
+		zap.L().Error("[Plane] SearchWorkItems 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("SearchWorkItems 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}

+ 1910 - 0
service/repositories/api/plane_v2/WEB_API_DOCUMENTATION.md

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

+ 410 - 0
service/repositories/api/plane_v2/planeWebClient.go

@@ -0,0 +1,410 @@
+package plane_v2
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/config"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"net/http/cookiejar"
+	"net/url"
+	"strings"
+	"sync"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+// Plane v2 Web API客户端 - 使用Session+CSRF认证(Django SessionAuthentication)
+
+const (
+	webTimeout time.Duration = 60 * time.Second // 请求超时60s
+)
+
+var (
+	webBaseURL      string
+	webEmail        string
+	webPassword      string
+	webWorkspaceSlug string
+	webProjectId     string
+	webLabelBugId      string
+	webLabelFeatureId  string
+	webLabelFeedbackId string
+	webCsrfToken    string
+	webClient       *http.Client
+	webLoginMu      sync.Mutex
+)
+
+// planeWebResponse 封装Plane Web HTTP响应
+type planeWebResponse struct {
+	StatusCode int
+	Status     string
+	Body       []byte
+}
+
+// PlaneWebHttpInit 初始化Plane Web HTTP客户端配置
+func PlaneWebHttpInit(cfg *config.PlaneWebApiConfig) {
+	webBaseURL = strings.TrimRight(cfg.Host, "/")
+	webEmail = cfg.Email
+	webPassword = cfg.Password
+	webWorkspaceSlug = cfg.WorkspaceSlug
+	webProjectId = cfg.ProjectId
+	webLabelBugId = cfg.LabelBugId
+	webLabelFeatureId = cfg.LabelFeatureId
+	webLabelFeedbackId = cfg.LabelFeedbackId
+
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		zap.L().Fatal("[PlaneWeb] 创建CookieJar失败", zap.Error(err))
+	}
+
+	webClient = &http.Client{
+		Timeout: webTimeout,
+		Jar:     jar,
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+
+	// 初始化时执行登录
+	if err := webLogin(); err != nil {
+		zap.L().Error("[PlaneWeb] 初始化登录失败", zap.Error(err))
+	}
+}
+
+// GetWorkspaceSlug 获取WorkspaceSlug
+func GetWorkspaceSlug() string {
+	return webWorkspaceSlug
+}
+
+// GetProjectId 获取ProjectId
+func GetProjectId() string {
+	return webProjectId
+}
+
+// GetLabelBugId 获取Bug标签ID
+func GetWebLabelBugId() string {
+	return webLabelBugId
+}
+
+// GetLabelFeatureId 获取Feature标签ID
+func GetWebLabelFeatureId() string {
+	return webLabelFeatureId
+}
+
+// GetLabelFeedbackId 获取Feedback标签ID
+func GetWebLabelFeedbackId() string {
+	return webLabelFeedbackId
+}
+
+// isWebSuccess 判断HTTP状态码是否表示成功(2xx或3xx重定向)
+func isWebSuccess(statusCode int) bool {
+	return statusCode >= 200 && statusCode < 300
+}
+
+// webLogin 执行登录流程:获取CSRF Token → 登录获取Session
+func webLogin() error {
+	webLoginMu.Lock()
+	defer webLoginMu.Unlock()
+
+	zap.L().Info("[PlaneWeb] 开始登录流程")
+
+	// Step 1: GET /auth/get-csrf-token/ 获取CSRF Token
+	csrfURL := webBaseURL + "/auth/get-csrf-token/"
+	req, err := http.NewRequest(http.MethodGet, csrfURL, nil)
+	if err != nil {
+		return fmt.Errorf("创建CSRF请求失败: %w", err)
+	}
+	req.Header.Set("Referer", webBaseURL)
+
+	resp, err := webClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("获取CSRF Token请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("读取CSRF响应失败: %w", err)
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("获取CSRF Token失败, status=%d, body=%s", resp.StatusCode, string(body))
+	}
+
+	// 解析CSRF Token
+	var csrfResp struct {
+		CsrfToken string `json:"csrf_token"`
+	}
+	if err := json.Unmarshal(body, &csrfResp); err != nil {
+		return fmt.Errorf("解析CSRF Token响应失败: %w", err)
+	}
+	webCsrfToken = csrfResp.CsrfToken
+
+	zap.L().Debug("[PlaneWeb] 获取CSRF Token成功", zap.String("token", webCsrfToken[:8]+"..."))
+
+	// Step 2: POST /auth/sign-in/ 登录
+	signInURL := webBaseURL + "/auth/sign-in/"
+	formData := url.Values{}
+	formData.Set("email", webEmail)
+	formData.Set("password", webPassword)
+
+	req, err = http.NewRequest(http.MethodPost, signInURL, strings.NewReader(formData.Encode()))
+	if err != nil {
+		return fmt.Errorf("创建登录请求失败: %w", err)
+	}
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("Referer", webBaseURL)
+	req.Header.Set("X-CSRFToken", webCsrfToken)
+	req.AddCookie(&http.Cookie{Name: "csrftoken", Value: webCsrfToken})
+
+	resp, err = webClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("登录请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// 登录成功返回302
+	if resp.StatusCode != http.StatusFound {
+		loginBody, _ := io.ReadAll(resp.Body)
+		return fmt.Errorf("登录失败, status=%d, body=%s", resp.StatusCode, string(loginBody))
+	}
+
+	// 验证session-id cookie是否已设置
+	parsedURL, _ := url.Parse(webBaseURL)
+	cookies := webClient.Jar.Cookies(parsedURL)
+	hasSession := false
+	for _, c := range cookies {
+		if c.Name == "session-id" {
+			hasSession = true
+			break
+		}
+	}
+
+	if !hasSession {
+		return fmt.Errorf("登录后未获取到session-id cookie")
+	}
+
+	zap.L().Info("[PlaneWeb] 登录成功")
+	return nil
+}
+
+// webEnsureAuth 确保已认证,如果收到401/403则重新登录并重试
+func webEnsureAuth(doRequest func() (*planeWebResponse, error)) (*planeWebResponse, error) {
+	resp, err := doRequest()
+	if err != nil {
+		return nil, err
+	}
+
+	// 如果是401或403,尝试重新登录后重试
+	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
+		zap.L().Warn("[PlaneWeb] 收到认证错误,尝试重新登录", zap.Int("statusCode", resp.StatusCode))
+
+		if loginErr := webLogin(); loginErr != nil {
+			return nil, fmt.Errorf("重新登录失败: %w", loginErr)
+		}
+
+		// 重试请求
+		return doRequest()
+	}
+
+	return resp, nil
+}
+
+// planeWebGet 发送GET请求到Plane Web API
+func planeWebGet(relativeURL string) (*planeWebResponse, error) {
+	return webEnsureAuth(func() (*planeWebResponse, error) {
+		reqURL := webBaseURL + relativeURL
+
+		zap.L().Debug("[PlaneWeb] HttpGet", zap.String("url", reqURL))
+
+		req, err := http.NewRequest(http.MethodGet, reqURL, nil)
+		if err != nil {
+			return nil, fmt.Errorf("创建请求失败: %w", err)
+		}
+
+		req.Header.Set("Accept", "application/json")
+		req.Header.Set("Referer", webBaseURL)
+
+		resp, err := webClient.Do(req)
+		if err != nil {
+			return nil, fmt.Errorf("请求失败: %w", err)
+		}
+		defer resp.Body.Close()
+
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("读取响应失败: %w", err)
+		}
+
+		return &planeWebResponse{
+			StatusCode: resp.StatusCode,
+			Status:     resp.Status,
+			Body:       body,
+		}, nil
+	})
+}
+
+// planeWebPost 发送POST请求到Plane Web API(JSON)
+func planeWebPost(relativeURL string, reqBody any) (*planeWebResponse, error) {
+	return webEnsureAuth(func() (*planeWebResponse, error) {
+		reqURL := webBaseURL + relativeURL
+
+		zap.L().Debug("[PlaneWeb] HttpPost", zap.String("url", reqURL))
+
+		jsonData, err := json.Marshal(reqBody)
+		if err != nil {
+			return nil, fmt.Errorf("序列化请求体失败: %w", err)
+		}
+
+		req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(jsonData))
+		if err != nil {
+			return nil, fmt.Errorf("创建请求失败: %w", err)
+		}
+
+		req.Header.Set("Content-Type", "application/json")
+		req.Header.Set("Accept", "application/json")
+		req.Header.Set("Referer", webBaseURL)
+		req.Header.Set("X-CSRFToken", webCsrfToken)
+
+		resp, err := webClient.Do(req)
+		if err != nil {
+			return nil, fmt.Errorf("请求失败: %w", err)
+		}
+		defer resp.Body.Close()
+
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("读取响应失败: %w", err)
+		}
+
+		return &planeWebResponse{
+			StatusCode: resp.StatusCode,
+			Status:     resp.Status,
+			Body:       body,
+		}, nil
+	})
+}
+
+// planeWebPatch 发送PATCH请求到Plane Web API(JSON)
+func planeWebPatch(relativeURL string, reqBody any) (*planeWebResponse, error) {
+	return webEnsureAuth(func() (*planeWebResponse, error) {
+		reqURL := webBaseURL + relativeURL
+
+		zap.L().Debug("[PlaneWeb] HttpPatch", zap.String("url", reqURL))
+
+		jsonData, err := json.Marshal(reqBody)
+		if err != nil {
+			return nil, fmt.Errorf("序列化请求体失败: %w", err)
+		}
+
+		req, err := http.NewRequest(http.MethodPatch, reqURL, bytes.NewReader(jsonData))
+		if err != nil {
+			return nil, fmt.Errorf("创建请求失败: %w", err)
+		}
+
+		req.Header.Set("Content-Type", "application/json")
+		req.Header.Set("Accept", "application/json")
+		req.Header.Set("Referer", webBaseURL)
+		req.Header.Set("X-CSRFToken", webCsrfToken)
+
+		resp, err := webClient.Do(req)
+		if err != nil {
+			return nil, fmt.Errorf("请求失败: %w", err)
+		}
+		defer resp.Body.Close()
+
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("读取响应失败: %w", err)
+		}
+
+		return &planeWebResponse{
+			StatusCode: resp.StatusCode,
+			Status:     resp.Status,
+			Body:       body,
+		}, nil
+	})
+}
+
+// planeWebDelete 发送DELETE请求到Plane Web API
+func planeWebDelete(relativeURL string) (*planeWebResponse, error) {
+	return webEnsureAuth(func() (*planeWebResponse, error) {
+		reqURL := webBaseURL + relativeURL
+
+		zap.L().Debug("[PlaneWeb] HttpDelete", zap.String("url", reqURL))
+
+		req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
+		if err != nil {
+			return nil, fmt.Errorf("创建请求失败: %w", err)
+		}
+
+		req.Header.Set("Accept", "application/json")
+		req.Header.Set("Referer", webBaseURL)
+		req.Header.Set("X-CSRFToken", webCsrfToken)
+
+		resp, err := webClient.Do(req)
+		if err != nil {
+			return nil, fmt.Errorf("请求失败: %w", err)
+		}
+		defer resp.Body.Close()
+
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("读取响应失败: %w", err)
+		}
+
+		return &planeWebResponse{
+			StatusCode: resp.StatusCode,
+			Status:     resp.Status,
+			Body:       body,
+		}, nil
+	})
+}
+
+// planeWebPostForm 发送multipart/form-data POST请求到Plane Web API
+func planeWebPostForm(relativeURL string, writeForm func(w *multipart.Writer) error) (*planeWebResponse, error) {
+	return webEnsureAuth(func() (*planeWebResponse, error) {
+		reqURL := webBaseURL + relativeURL
+
+		zap.L().Debug("[PlaneWeb] HttpPostForm", zap.String("url", reqURL))
+
+		var buf bytes.Buffer
+		writer := multipart.NewWriter(&buf)
+
+		if err := writeForm(writer); err != nil {
+			return nil, fmt.Errorf("写入表单数据失败: %w", err)
+		}
+		writer.Close()
+
+		req, err := http.NewRequest(http.MethodPost, reqURL, &buf)
+		if err != nil {
+			return nil, fmt.Errorf("创建请求失败: %w", err)
+		}
+
+		req.Header.Set("Content-Type", writer.FormDataContentType())
+		req.Header.Set("Accept", "application/json")
+		req.Header.Set("Referer", webBaseURL)
+		req.Header.Set("X-CSRFToken", webCsrfToken)
+
+		resp, err := webClient.Do(req)
+		if err != nil {
+			return nil, fmt.Errorf("请求失败: %w", err)
+		}
+		defer resp.Body.Close()
+
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("读取响应失败: %w", err)
+		}
+
+		return &planeWebResponse{
+			StatusCode: resp.StatusCode,
+			Status:     resp.Status,
+			Body:       body,
+		}, nil
+	})
+}

+ 367 - 0
service/repositories/api/plane_v2/plane_asset.go

@@ -0,0 +1,367 @@
+package plane_v2
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"strings"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Asset v2 API ====================
+
+// Entity Type 常量
+const (
+	EntityIssueDescription      = "ISSUE_DESCRIPTION"
+	EntityIssueAttachment       = "ISSUE_ATTACHMENT"
+	EntityCommentDescription    = "COMMENT_DESCRIPTION"
+	EntityPageDescription       = "PAGE_DESCRIPTION"
+	EntityDraftIssueDescription = "DRAFT_ISSUE_DESCRIPTION"
+	EntityProjectCover          = "PROJECT_COVER"
+	EntityUserAvatar            = "USER_AVATAR"
+	EntityUserCover             = "USER_COVER"
+	EntityWorkspaceLogo         = "WORKSPACE_LOGO"
+)
+
+// WebAssetUploadResult 资源上传结果
+type WebAssetUploadResult struct {
+	UploadData struct {
+		URL    string            `json:"url"`
+		Fields map[string]string `json:"fields"`
+	} `json:"upload_data"`
+	AssetID  string `json:"asset_id"`
+	AssetURL string `json:"asset_url"`
+}
+
+// CreateProjectAsset 创建项目资源(获取预签名 URL)
+func CreateProjectAsset(slug, projectID, fileName, mimeType string, size int64, entityType string) (*WebAssetUploadResult, error) {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/", slug, projectID)
+	payload := map[string]interface{}{
+		"name":              fileName,
+		"type":              mimeType,
+		"size":              size,
+		"entity_type":       entityType,
+		"entity_identifier": "",
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateProjectAsset 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] CreateProjectAsset 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateProjectAsset 失败: %s", resp.Status)
+	}
+
+	var result WebAssetUploadResult
+	if err := json.Unmarshal(resp.Body, &result); err != nil {
+		return nil, fmt.Errorf("解析资源上传响应失败: %w", err)
+	}
+
+	return &result, nil
+}
+
+// UploadToS3 上传文件到 S3(使用预签名 POST URL)
+func UploadToS3(uploadURL string, fields map[string]string, fileData []byte, fileName string) error {
+	// 替换容器内地址为宿主机地址
+	uploadURL = strings.ReplaceAll(uploadURL, "host.docker.internal", "localhost")
+
+	var buf bytes.Buffer
+	writer := multipart.NewWriter(&buf)
+
+	// 先写入所有 fields(必须在 file 之前)
+	for k, v := range fields {
+		if err := writer.WriteField(k, v); err != nil {
+			return fmt.Errorf("写入 field %s 失败: %w", k, err)
+		}
+	}
+
+	// 写入文件
+	part, err := writer.CreateFormFile("file", fileName)
+	if err != nil {
+		return fmt.Errorf("创建 form file 失败: %w", err)
+	}
+	if _, err := part.Write(fileData); err != nil {
+		return fmt.Errorf("写入文件数据失败: %w", err)
+	}
+	writer.Close()
+
+	req, err := http.NewRequest(http.MethodPost, uploadURL, &buf)
+	if err != nil {
+		return fmt.Errorf("创建上传请求失败: %w", err)
+	}
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+
+	client := &http.Client{Timeout: 60 * time.Second}
+	resp, err := client.Do(req)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] S3 Upload 请求失败", zap.Error(err))
+		return fmt.Errorf("文件上传失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		respBody, _ := io.ReadAll(resp.Body)
+		zap.L().Error("[PlaneWeb] S3 Upload 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(respBody)),
+		)
+		return fmt.Errorf("文件上传失败: %s", resp.Status)
+	}
+
+	zap.L().Debug("[PlaneWeb] S3 Upload 成功")
+	return nil
+}
+
+// ConfirmProjectAssetUpload 确认项目资源上传完成
+func ConfirmProjectAssetUpload(slug, projectID, assetID string) error {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", slug, projectID, assetID)
+
+	// Plane 期望空对象 {} 作为请求体(不能传 nil,否则序列化为 null 导致 500)
+	resp, err := planeWebPatch(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ConfirmProjectAssetUpload 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 && !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ConfirmProjectAssetUpload 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ConfirmProjectAssetUpload 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetProjectAsset 下载项目资源(返回302重定向URL)
+func GetProjectAsset(slug, projectID, assetID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", slug, projectID, assetID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetProjectAsset 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	return resp.Body, nil
+}
+
+// GetProjectAssetURL 获取项目资源的预签名访问URL(从302 Location中提取)
+func GetProjectAssetURL(slug, projectID, assetID string) (string, error) {
+	reqURL := webBaseURL + fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", slug, projectID, assetID)
+
+	req, err := http.NewRequest(http.MethodGet, reqURL, nil)
+	if err != nil {
+		return "", fmt.Errorf("创建请求失败: %w", err)
+	}
+	req.Header.Set("Accept", "application/json")
+	req.Header.Set("Referer", webBaseURL)
+
+	resp, err := webClient.Do(req)
+	if err != nil {
+		return "", fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently {
+		location := resp.Header.Get("Location")
+		if location != "" {
+			return location, nil
+		}
+	}
+
+	if isWebSuccess(resp.StatusCode) {
+		return reqURL, nil
+	}
+
+	return "", fmt.Errorf("获取资源URL失败: status=%d", resp.StatusCode)
+}
+
+// DeleteProjectAsset 删除项目资源
+func DeleteProjectAsset(slug, projectID, assetID string) error {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", slug, projectID, assetID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteProjectAsset 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteProjectAsset 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteProjectAsset 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// BindAssetsToEntity 批量绑定资源到实体
+func BindAssetsToEntity(slug, projectID, entityID string, assetIDs []string) error {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/bulk/", slug, projectID, entityID)
+	payload := map[string]interface{}{
+		"asset_ids": assetIDs,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] BindAssetsToEntity 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 && !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] BindAssetsToEntity 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("BindAssetsToEntity 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// CreateWorkspaceAsset 创建工作空间资源
+func CreateWorkspaceAsset(slug, fileName, mimeType string, size int64, entityType string) (*WebAssetUploadResult, error) {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/", slug)
+	payload := map[string]interface{}{
+		"name":              fileName,
+		"type":              mimeType,
+		"size":              size,
+		"entity_type":       entityType,
+		"entity_identifier": "",
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateWorkspaceAsset 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] CreateWorkspaceAsset 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateWorkspaceAsset 失败: %s", resp.Status)
+	}
+
+	var result WebAssetUploadResult
+	if err := json.Unmarshal(resp.Body, &result); err != nil {
+		return nil, fmt.Errorf("解析资源上传响应失败: %w", err)
+	}
+
+	return &result, nil
+}
+
+// ConfirmWorkspaceAssetUpload 确认工作空间资源上传完成
+func ConfirmWorkspaceAssetUpload(slug, assetID string) error {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/%s/", slug, assetID)
+
+	resp, err := planeWebPatch(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ConfirmWorkspaceAssetUpload 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 && !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ConfirmWorkspaceAssetUpload 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ConfirmWorkspaceAssetUpload 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// CreateUserAsset 创建用户资源
+func CreateUserAsset(fileName, mimeType string, size int64, entityType string) (*WebAssetUploadResult, error) {
+	payload := map[string]interface{}{
+		"name":              fileName,
+		"type":              mimeType,
+		"size":              size,
+		"entity_type":       entityType,
+		"entity_identifier": "",
+	}
+
+	resp, err := planeWebPost("/api/assets/v2/user-assets/", payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateUserAsset 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] CreateUserAsset 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateUserAsset 失败: %s", resp.Status)
+	}
+
+	var result WebAssetUploadResult
+	if err := json.Unmarshal(resp.Body, &result); err != nil {
+		return nil, fmt.Errorf("解析资源上传响应失败: %w", err)
+	}
+
+	return &result, nil
+}
+
+// ConfirmUserAssetUpload 确认用户资源上传完成
+func ConfirmUserAssetUpload(assetID string) error {
+	url := fmt.Sprintf("/api/assets/v2/user-assets/%s/", assetID)
+
+	resp, err := planeWebPatch(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ConfirmUserAssetUpload 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 && !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ConfirmUserAssetUpload 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ConfirmUserAssetUpload 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// UploadProjectAsset 完整上传项目资源流程(组合方法)
+// 1. 创建资源获取预签名URL → 2. 上传到S3 → 3. 确认上传完成
+func UploadProjectAsset(slug, projectID, fileName, mimeType string, fileData []byte, entityType string) (*WebAssetUploadResult, error) {
+	fileSize := int64(len(fileData))
+
+	// Step 1: 创建资源
+	result, err := CreateProjectAsset(slug, projectID, fileName, mimeType, fileSize, entityType)
+	if err != nil {
+		return nil, fmt.Errorf("创建资源失败: %w", err)
+	}
+
+	zap.L().Debug("[PlaneWeb] Asset Create 成功",
+		zap.String("assetId", result.AssetID),
+		zap.String("uploadURL", result.UploadData.URL),
+	)
+
+	// Step 2: 上传到 S3
+	if err := UploadToS3(result.UploadData.URL, result.UploadData.Fields, fileData, fileName); err != nil {
+		return nil, fmt.Errorf("上传文件到S3失败: %w", err)
+	}
+
+	// Step 3: 确认上传完成
+	if err := ConfirmProjectAssetUpload(slug, projectID, result.AssetID); err != nil {
+		return nil, fmt.Errorf("确认上传失败: %w", err)
+	}
+
+	zap.L().Info("[PlaneWeb] Asset 上传完成",
+		zap.String("assetId", result.AssetID),
+		zap.String("fileName", fileName),
+		zap.String("assetUrl", result.AssetURL),
+	)
+
+	return result, nil
+}
+
+// UploadCurrentProjectAsset 在当前项目上传资源
+func UploadCurrentProjectAsset(fileName, mimeType string, fileData []byte, entityType string) (*WebAssetUploadResult, error) {
+	return UploadProjectAsset(webWorkspaceSlug, webProjectId, fileName, mimeType, fileData, entityType)
+}

+ 111 - 0
service/repositories/api/plane_v2/plane_comment.go

@@ -0,0 +1,111 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Comment API ====================
+
+// ListComments 列出工作项评论
+func ListComments(slug, projectID, issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", slug, projectID, issueID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListComments 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListComments 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListComments 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateComment 创建评论
+func CreateComment(slug, projectID, issueID, commentHTML string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", slug, projectID, issueID)
+	payload := map[string]interface{}{
+		"comment_html": commentHTML,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateComment 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateComment 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateComment 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetComment 获取评论详情
+func GetComment(slug, projectID, issueID, commentID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", slug, projectID, issueID, commentID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetComment 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetComment 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetComment 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateComment 更新评论
+func UpdateComment(slug, projectID, issueID, commentID, commentHTML string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", slug, projectID, issueID, commentID)
+	payload := map[string]interface{}{
+		"comment_html": commentHTML,
+	}
+
+	resp, err := planeWebPatch(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateComment 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateComment 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateComment 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteComment 删除评论
+func DeleteComment(slug, projectID, issueID, commentID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", slug, projectID, issueID, commentID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteComment 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteComment 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteComment 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 279 - 0
service/repositories/api/plane_v2/plane_cycle.go

@@ -0,0 +1,279 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Cycle API ====================
+
+// ListCycles 列出迭代
+func ListCycles(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListCycles 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListCycles 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListCycles 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateCycle 创建迭代
+func CreateCycle(slug, projectID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", slug, projectID)
+
+	resp, err := planeWebPost(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateCycle 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetCycle 获取迭代详情
+func GetCycle(slug, projectID, cycleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetCycle 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateCycle 更新迭代
+func UpdateCycle(slug, projectID, cycleID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateCycle 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteCycle 删除迭代
+func DeleteCycle(slug, projectID, cycleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", slug, projectID, cycleID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteCycle 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteCycle 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteCycle 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListCycleIssues 列出迭代中的工作项
+func ListCycleIssues(slug, projectID, cycleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", slug, projectID, cycleID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListCycleIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListCycleIssues 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListCycleIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AddIssuesToCycle 添加工作项到迭代
+func AddIssuesToCycle(slug, projectID, cycleID string, issueIDs []string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", slug, projectID, cycleID)
+	payload := map[string]interface{}{
+		"issues": issueIDs,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] AddIssuesToCycle 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] AddIssuesToCycle 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("AddIssuesToCycle 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveIssueFromCycle 从迭代移除工作项
+func RemoveIssueFromCycle(slug, projectID, cycleID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/%s/", slug, projectID, cycleID, issueID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] RemoveIssueFromCycle 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] RemoveIssueFromCycle 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveIssueFromCycle 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// TransferCycleIssues 转移未完成工作项到另一迭代
+func TransferCycleIssues(slug, projectID, cycleID, newCycleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/transfer-issues/", slug, projectID, cycleID)
+	payload := map[string]interface{}{
+		"new_cycle_id": newCycleID,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] TransferCycleIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] TransferCycleIssues 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("TransferCycleIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// ArchiveCycle 归档迭代
+func ArchiveCycle(slug, projectID, cycleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", slug, projectID, cycleID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ArchiveCycle 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ArchiveCycle 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchiveCycle 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListArchivedCycles 列出已归档迭代
+func ListArchivedCycles(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archived-cycles/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListArchivedCycles 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListArchivedCycles 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListArchivedCycles 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UnarchiveCycle 取消归档迭代
+func UnarchiveCycle(slug, projectID, cycleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archived-cycles/%s/", slug, projectID, cycleID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UnarchiveCycle 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UnarchiveCycle 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("UnarchiveCycle 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetCycleProgress 获取迭代进度
+func GetCycleProgress(slug, projectID, cycleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/progress/", slug, projectID, cycleID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetCycleProgress 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetCycleProgress 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetCycleProgress 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetCycleAnalytics 获取迭代分析
+func GetCycleAnalytics(slug, projectID, cycleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/analytics/", slug, projectID, cycleID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetCycleAnalytics 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetCycleAnalytics 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetCycleAnalytics 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}

+ 210 - 0
service/repositories/api/plane_v2/plane_intake.go

@@ -0,0 +1,210 @@
+package plane_v2
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Intake (收件箱) API ====================
+
+// WebIntakeIssueRequest 创建Intake工作项的请求结构
+type WebIntakeIssueRequest struct {
+	Name            string   `json:"name"`
+	DescriptionHTML string   `json:"description_html,omitempty"`
+	Priority        string   `json:"priority,omitempty"`
+	StateID         string   `json:"state_id,omitempty"`
+	AssigneeIDs     []string `json:"assignee_ids,omitempty"`
+	LabelIDs        []string `json:"label_ids,omitempty"`
+	StartDate       string   `json:"start_date,omitempty"`
+	TargetDate      *string  `json:"target_date"`
+	ParentID        string   `json:"parent_id,omitempty"`
+}
+
+// WebIntakeIssueResponse Intake创建响应
+type WebIntakeIssueResponse struct {
+	Issue struct {
+		ID              string `json:"id"`
+		Name            string `json:"name"`
+		DescriptionHTML string `json:"description_html"`
+		Priority        string `json:"priority"`
+	} `json:"issue"`
+	Status    int    `json:"status"`
+	CreatedAt string `json:"created_at"`
+}
+
+// ListIntakeIssues 列出收件箱工作项
+// status: 逗号分隔的状态值,如 "-2,-1" (可为空表示不过滤)
+func ListIntakeIssues(slug, projectID, status string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", slug, projectID)
+	if status != "" {
+		url += "?status=" + status
+	}
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListIntakeIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListIntakeIssues 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListIntakeIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateIntakeIssue 创建收件箱工作项
+func CreateIntakeIssue(slug, projectID string, issue WebIntakeIssueRequest) (*WebIntakeIssueResponse, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", slug, projectID)
+	payload := map[string]interface{}{
+		"source": "IN_APP",
+		"issue":  issue,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateIntakeIssue 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] CreateIntakeIssue 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateIntakeIssue 失败: %s", resp.Status)
+	}
+
+	var result WebIntakeIssueResponse
+	if err := json.Unmarshal(resp.Body, &result); err != nil {
+		zap.L().Error("[PlaneWeb] CreateIntakeIssue 响应解析失败", zap.Error(err))
+		return nil, fmt.Errorf("响应解析失败: %w", err)
+	}
+
+	zap.L().Info("[PlaneWeb] CreateIntakeIssue 成功", zap.String("issueId", result.Issue.ID))
+	return &result, nil
+}
+
+// CreateIntakeIssueRaw 创建收件箱工作项(原始map方式)
+func CreateIntakeIssueRaw(slug, projectID string, issueData map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", slug, projectID)
+	payload := map[string]interface{}{
+		"issue": issueData,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateIntakeIssueRaw 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] CreateIntakeIssueRaw 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateIntakeIssueRaw 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetIntakeIssue 获取收件箱工作项详情
+func GetIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetIntakeIssue 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetIntakeIssue 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetIntakeIssue 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateIntakeIssue 更新收件箱工作项(修改状态或内容)
+func UpdateIntakeIssue(slug, projectID, issueID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateIntakeIssue 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateIntakeIssue 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateIntakeIssue 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AcceptIntakeIssue 接受收件箱工作项
+func AcceptIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
+	return UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{
+		"status": 1,
+	})
+}
+
+// RejectIntakeIssue 拒绝收件箱工作项
+func RejectIntakeIssue(slug, projectID, issueID string) ([]byte, error) {
+	return UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{
+		"status": -1,
+	})
+}
+
+// SnoozeIntakeIssue 暂缓收件箱工作项
+func SnoozeIntakeIssue(slug, projectID, issueID, snoozedTill string) ([]byte, error) {
+	return UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{
+		"status":       0,
+		"snoozed_till": snoozedTill,
+	})
+}
+
+// DuplicateIntakeIssue 标记收件箱工作项为重复
+func DuplicateIntakeIssue(slug, projectID, issueID, duplicateTo string) ([]byte, error) {
+	return UpdateIntakeIssue(slug, projectID, issueID, map[string]interface{}{
+		"status":       2,
+		"duplicate_to": duplicateTo,
+	})
+}
+
+// DeleteIntakeIssue 删除收件箱工作项
+func DeleteIntakeIssue(slug, projectID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", slug, projectID, issueID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteIntakeIssue 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteIntakeIssue 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteIntakeIssue 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListCurrentIntakeIssues 列出当前项目的收件箱工作项
+func ListCurrentIntakeIssues(status string) ([]byte, error) {
+	return ListIntakeIssues(webWorkspaceSlug, webProjectId, status)
+}
+
+// CreateCurrentIntakeIssue 在当前项目创建收件箱工作项
+func CreateCurrentIntakeIssue(issue WebIntakeIssueRequest) (*WebIntakeIssueResponse, error) {
+	return CreateIntakeIssue(webWorkspaceSlug, webProjectId, issue)
+}

+ 187 - 0
service/repositories/api/plane_v2/plane_issue.go

@@ -0,0 +1,187 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Issue (Work Item) API ====================
+
+// ListIssues 列出工作项
+func ListIssues(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListIssues 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateIssue 创建工作项
+func CreateIssue(slug, projectID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", slug, projectID)
+
+	resp, err := planeWebPost(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateIssue 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateIssue 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateIssue 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetIssue 获取工作项详情
+func GetIssue(slug, projectID, issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", slug, projectID, issueID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetIssue 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetIssue 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetIssue 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateIssue 更新工作项
+func UpdateIssue(slug, projectID, issueID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", slug, projectID, issueID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateIssue 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateIssue 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateIssue 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteIssue 删除工作项
+func DeleteIssue(slug, projectID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", slug, projectID, issueID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteIssue 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteIssue 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteIssue 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetSubIssues 获取子工作项
+func GetSubIssues(slug, projectID, issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/sub-issues/", slug, projectID, issueID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetSubIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetSubIssues 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetSubIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// ArchiveIssue 归档工作项
+func ArchiveIssue(slug, projectID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", slug, projectID, issueID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ArchiveIssue 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ArchiveIssue 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchiveIssue 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// UnarchiveIssue 取消归档工作项
+func UnarchiveIssue(slug, projectID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", slug, projectID, issueID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UnarchiveIssue 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UnarchiveIssue 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("UnarchiveIssue 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetIssueActivity 获取工作项活动历史
+func GetIssueActivity(slug, projectID, issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/history/", slug, projectID, issueID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetIssueActivity 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetIssueActivity 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetIssueActivity 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// ListCurrentIssues 列出当前项目下的工作项
+func ListCurrentIssues() ([]byte, error) {
+	return ListIssues(webWorkspaceSlug, webProjectId)
+}
+
+// CreateCurrentIssue 在当前项目下创建工作项
+func CreateCurrentIssue(data map[string]interface{}) ([]byte, error) {
+	return CreateIssue(webWorkspaceSlug, webProjectId, data)
+}

+ 114 - 0
service/repositories/api/plane_v2/plane_label.go

@@ -0,0 +1,114 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Label API ====================
+
+// ListLabels 列出标签
+func ListLabels(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListLabels 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListLabels 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListLabels 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateLabel 创建标签
+func CreateLabel(slug, projectID, name, color string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", slug, projectID)
+	payload := map[string]interface{}{
+		"name":  name,
+		"color": color,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateLabel 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateLabel 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateLabel 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetLabel 获取标签详情
+func GetLabel(slug, projectID, labelID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", slug, projectID, labelID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetLabel 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetLabel 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetLabel 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateLabel 更新标签
+func UpdateLabel(slug, projectID, labelID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", slug, projectID, labelID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateLabel 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateLabel 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateLabel 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteLabel 删除标签
+func DeleteLabel(slug, projectID, labelID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", slug, projectID, labelID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteLabel 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteLabel 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteLabel 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListCurrentLabels 列出当前项目下的标签
+func ListCurrentLabels() ([]byte, error) {
+	return ListLabels(webWorkspaceSlug, webProjectId)
+}

+ 91 - 0
service/repositories/api/plane_v2/plane_link.go

@@ -0,0 +1,91 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Issue Link API ====================
+
+// ListIssueLinks 列出工作项链接
+func ListIssueLinks(slug, projectID, issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-links/", slug, projectID, issueID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListIssueLinks 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListIssueLinks 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListIssueLinks 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateIssueLink 创建工作项链接
+func CreateIssueLink(slug, projectID, issueID, linkURL, title string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-links/", slug, projectID, issueID)
+	payload := map[string]interface{}{
+		"url":   linkURL,
+		"title": title,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateIssueLink 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateIssueLink 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateIssueLink 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateIssueLink 更新工作项链接
+func UpdateIssueLink(slug, projectID, issueID, linkID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-links/%s/", slug, projectID, issueID, linkID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateIssueLink 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateIssueLink 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateIssueLink 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteIssueLink 删除工作项链接
+func DeleteIssueLink(slug, projectID, issueID, linkID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-links/%s/", slug, projectID, issueID, linkID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteIssueLink 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteIssueLink 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteIssueLink 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 243 - 0
service/repositories/api/plane_v2/plane_member.go

@@ -0,0 +1,243 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Member API ====================
+
+// Member 角色常量
+const (
+	RoleAdmin  = 20
+	RoleMember = 15
+	RoleGuest  = 5
+)
+
+// ListWorkspaceMembers 列出工作空间成员
+func ListWorkspaceMembers(slug string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/members/", slug)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListWorkspaceMembers 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListWorkspaceMembers 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkspaceMembers 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetWorkspaceMember 获取工作空间成员详情
+func GetWorkspaceMember(slug, memberID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/members/%s/", slug, memberID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetWorkspaceMember 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetWorkspaceMember 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetWorkspaceMember 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateWorkspaceMemberRole 更新工作空间成员角色
+func UpdateWorkspaceMemberRole(slug, memberID string, role int) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/members/%s/", slug, memberID)
+	payload := map[string]interface{}{
+		"role": role,
+	}
+
+	resp, err := planeWebPatch(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateWorkspaceMemberRole 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateWorkspaceMemberRole 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateWorkspaceMemberRole 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveWorkspaceMember 移除工作空间成员
+func RemoveWorkspaceMember(slug, memberID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/members/%s/", slug, memberID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] RemoveWorkspaceMember 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] RemoveWorkspaceMember 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveWorkspaceMember 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListProjectMembers 列出项目成员
+func ListProjectMembers(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListProjectMembers 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListProjectMembers 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListProjectMembers 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AddProjectMember 添加项目成员
+func AddProjectMember(slug, projectID, memberID string, role int) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", slug, projectID)
+	payload := map[string]interface{}{
+		"members": []map[string]interface{}{
+			{"member_id": memberID, "role": role},
+		},
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] AddProjectMember 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] AddProjectMember 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("AddProjectMember 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetProjectMember 获取项目成员详情
+func GetProjectMember(slug, projectID, memberID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", slug, projectID, memberID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetProjectMember 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetProjectMember 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetProjectMember 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateProjectMemberRole 更新项目成员角色
+func UpdateProjectMemberRole(slug, projectID, memberID string, role int) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", slug, projectID, memberID)
+	payload := map[string]interface{}{
+		"role": role,
+	}
+
+	resp, err := planeWebPatch(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateProjectMemberRole 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateProjectMemberRole 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateProjectMemberRole 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveProjectMember 移除项目成员
+func RemoveProjectMember(slug, projectID, memberID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", slug, projectID, memberID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] RemoveProjectMember 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] RemoveProjectMember 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveProjectMember 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetCurrentUser 获取当前用户信息
+func GetCurrentUser() ([]byte, error) {
+	resp, err := planeWebGet("/api/users/me/")
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetCurrentUser 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetCurrentUser 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetCurrentUser 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateCurrentUser 更新当前用户信息
+func UpdateCurrentUser(data map[string]interface{}) ([]byte, error) {
+	resp, err := planeWebPatch("/api/users/me/", data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateCurrentUser 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateCurrentUser 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateCurrentUser 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// ListCurrentWorkspaceMembers 列出当前工作空间的成员
+func ListCurrentWorkspaceMembers() ([]byte, error) {
+	return ListWorkspaceMembers(webWorkspaceSlug)
+}
+
+// ListCurrentProjectMembers 列出当前项目的成员
+func ListCurrentProjectMembers() ([]byte, error) {
+	return ListProjectMembers(webWorkspaceSlug, webProjectId)
+}

+ 297 - 0
service/repositories/api/plane_v2/plane_module.go

@@ -0,0 +1,297 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Module API ====================
+
+// ListModules 列出模块
+func ListModules(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListModules 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListModules 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListModules 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateModule 创建模块
+func CreateModule(slug, projectID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", slug, projectID)
+
+	resp, err := planeWebPost(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateModule 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetModule 获取模块详情
+func GetModule(slug, projectID, moduleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetModule 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateModule 更新模块
+func UpdateModule(slug, projectID, moduleID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateModule 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteModule 删除模块
+func DeleteModule(slug, projectID, moduleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", slug, projectID, moduleID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteModule 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteModule 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteModule 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListModuleIssues 列出模块中的工作项
+func ListModuleIssues(slug, projectID, moduleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/issues/", slug, projectID, moduleID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListModuleIssues 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListModuleIssues 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListModuleIssues 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// AddIssuesToModule 添加工作项到模块
+func AddIssuesToModule(slug, projectID, moduleID string, issueIDs []string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/issues/", slug, projectID, moduleID)
+	payload := map[string]interface{}{
+		"issues": issueIDs,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] AddIssuesToModule 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] AddIssuesToModule 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("AddIssuesToModule 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveIssueFromModule 从模块移除工作项
+func RemoveIssueFromModule(slug, projectID, moduleID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/issues/%s/", slug, projectID, moduleID, issueID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] RemoveIssueFromModule 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] RemoveIssueFromModule 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveIssueFromModule 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListModuleLinks 列出模块链接
+func ListModuleLinks(slug, projectID, moduleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-links/", slug, projectID, moduleID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListModuleLinks 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListModuleLinks 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListModuleLinks 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateModuleLink 创建模块链接
+func CreateModuleLink(slug, projectID, moduleID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-links/", slug, projectID, moduleID)
+
+	resp, err := planeWebPost(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateModuleLink 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateModuleLink 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateModuleLink 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateModuleLink 更新模块链接
+func UpdateModuleLink(slug, projectID, moduleID, linkID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-links/%s/", slug, projectID, moduleID, linkID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateModuleLink 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateModuleLink 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateModuleLink 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteModuleLink 删除模块链接
+func DeleteModuleLink(slug, projectID, moduleID, linkID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-links/%s/", slug, projectID, moduleID, linkID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteModuleLink 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteModuleLink 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteModuleLink 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ArchiveModule 归档模块
+func ArchiveModule(slug, projectID, moduleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", slug, projectID, moduleID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ArchiveModule 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ArchiveModule 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchiveModule 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListArchivedModules 列出已归档模块
+func ListArchivedModules(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archived-modules/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListArchivedModules 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListArchivedModules 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListArchivedModules 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UnarchiveModule 取消归档模块
+func UnarchiveModule(slug, projectID, moduleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archived-modules/%s/", slug, projectID, moduleID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UnarchiveModule 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UnarchiveModule 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("UnarchiveModule 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 258 - 0
service/repositories/api/plane_v2/plane_page.go

@@ -0,0 +1,258 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Page API ====================
+
+// ListPages 列出页面
+func ListPages(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListPages 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListPages 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListPages 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreatePage 创建页面
+func CreatePage(slug, projectID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", slug, projectID)
+
+	resp, err := planeWebPost(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreatePage 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreatePage 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreatePage 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetPage 获取页面详情
+func GetPage(slug, projectID, pageID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", slug, projectID, pageID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetPage 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetPage 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetPage 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdatePage 更新页面
+func UpdatePage(slug, projectID, pageID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", slug, projectID, pageID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdatePage 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdatePage 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdatePage 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetPageDescription 获取页面描述内容
+func GetPageDescription(slug, projectID, pageID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/description/", slug, projectID, pageID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetPageDescription 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetPageDescription 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetPageDescription 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdatePageDescription 更新页面描述内容
+func UpdatePageDescription(slug, projectID, pageID, descHTML string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/description/", slug, projectID, pageID)
+	payload := map[string]interface{}{
+		"description_html": descHTML,
+	}
+
+	resp, err := planeWebPatch(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdatePageDescription 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdatePageDescription 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdatePageDescription 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeletePage 删除页面
+func DeletePage(slug, projectID, pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", slug, projectID, pageID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeletePage 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeletePage 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeletePage 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ArchivePage 归档页面
+func ArchivePage(slug, projectID, pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", slug, projectID, pageID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ArchivePage 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ArchivePage 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchivePage 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// UnarchivePage 取消归档页面
+func UnarchivePage(slug, projectID, pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", slug, projectID, pageID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UnarchivePage 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UnarchivePage 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("UnarchivePage 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// LockPage 锁定页面
+func LockPage(slug, projectID, pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/lock/", slug, projectID, pageID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] LockPage 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] LockPage 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("LockPage 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// UnlockPage 解锁页面
+func UnlockPage(slug, projectID, pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/lock/", slug, projectID, pageID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UnlockPage 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UnlockPage 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("UnlockPage 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// SetPageAccess 设置页面访问权限 (access: 0=私密, 1=公开)
+func SetPageAccess(slug, projectID, pageID string, access int) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/access/", slug, projectID, pageID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{"access": access})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] SetPageAccess 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] SetPageAccess 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("SetPageAccess 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// DuplicatePage 复制页面
+func DuplicatePage(slug, projectID, pageID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/duplicate/", slug, projectID, pageID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DuplicatePage 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] DuplicatePage 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("DuplicatePage 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}

+ 151 - 0
service/repositories/api/plane_v2/plane_project.go

@@ -0,0 +1,151 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Project API ====================
+
+// ListProjects 列出项目
+func ListProjects(slug string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/", slug)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListProjects 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListProjects 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListProjects 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateProject 创建项目
+func CreateProject(slug string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/", slug)
+
+	resp, err := planeWebPost(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateProject 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateProject 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateProject 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetProject 获取项目详情
+func GetProject(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetProject 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetProject 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetProject 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateProject 更新项目
+func UpdateProject(slug, projectID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", slug, projectID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateProject 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateProject 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateProject 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteProject 删除项目
+func DeleteProject(slug, projectID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", slug, projectID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteProject 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteProject 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteProject 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ArchiveProject 归档项目
+func ArchiveProject(slug, projectID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", slug, projectID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ArchiveProject 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] ArchiveProject 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("ArchiveProject 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// UnarchiveProject 取消归档项目
+func UnarchiveProject(slug, projectID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", slug, projectID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UnarchiveProject 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UnarchiveProject 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("UnarchiveProject 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListCurrentProjects 列出当前配置的工作空间下的项目
+func ListCurrentProjects() ([]byte, error) {
+	return ListProjects(webWorkspaceSlug)
+}
+
+// GetCurrentProject 获取当前配置的项目详情
+func GetCurrentProject() ([]byte, error) {
+	return GetProject(webWorkspaceSlug, webProjectId)
+}

+ 75 - 0
service/repositories/api/plane_v2/plane_relation.go

@@ -0,0 +1,75 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Issue Relation API ====================
+
+// ListIssueRelations 列出工作项关系
+func ListIssueRelations(slug, projectID, issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-relation/", slug, projectID, issueID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListIssueRelations 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListIssueRelations 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListIssueRelations 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateIssueRelation 创建工作项关系
+// relationType: blocking / blocked_by / duplicate / relates_to / start_before / start_after / finish_before / finish_after
+func CreateIssueRelation(slug, projectID, issueID, relationType string, relatedIDs []string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/issue-relation/", slug, projectID, issueID)
+	payload := map[string]interface{}{
+		"relation_type": relationType,
+		"related_list":  relatedIDs,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateIssueRelation 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateIssueRelation 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateIssueRelation 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// RemoveIssueRelation 移除工作项关系
+func RemoveIssueRelation(slug, projectID, issueID, relationType, relatedID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/remove-relation/", slug, projectID, issueID)
+	payload := map[string]interface{}{
+		"relation_type": relationType,
+		"related_list":  []string{relatedID},
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] RemoveIssueRelation 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] RemoveIssueRelation 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("RemoveIssueRelation 失败: %s", resp.Status)
+	}
+
+	return nil
+}

+ 134 - 0
service/repositories/api/plane_v2/plane_state.go

@@ -0,0 +1,134 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== State API ====================
+
+// ListStates 列出状态
+func ListStates(slug, projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", slug, projectID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListStates 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListStates 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListStates 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateState 创建状态
+// group: backlog / unstarted / started / completed / cancelled
+func CreateState(slug, projectID, name, color, group string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", slug, projectID)
+	payload := map[string]interface{}{
+		"name":  name,
+		"color": color,
+		"group": group,
+	}
+
+	resp, err := planeWebPost(url, payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateState 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateState 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateState 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetState 获取状态详情
+func GetState(slug, projectID, stateID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", slug, projectID, stateID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetState 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetState 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetState 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateState 更新状态
+func UpdateState(slug, projectID, stateID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", slug, projectID, stateID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateState 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateState 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateState 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteState 删除状态
+func DeleteState(slug, projectID, stateID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", slug, projectID, stateID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteState 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteState 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteState 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// MarkStateDefault 设为默认状态
+func MarkStateDefault(slug, projectID, stateID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/mark-default/", slug, projectID, stateID)
+
+	resp, err := planeWebPost(url, map[string]interface{}{})
+	if err != nil {
+		zap.L().Error("[PlaneWeb] MarkStateDefault 请求失败", zap.Error(err))
+		return err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] MarkStateDefault 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("MarkStateDefault 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListCurrentStates 列出当前项目下的状态
+func ListCurrentStates() ([]byte, error) {
+	return ListStates(webWorkspaceSlug, webProjectId)
+}

+ 110 - 0
service/repositories/api/plane_v2/plane_sticky.go

@@ -0,0 +1,110 @@
+package plane_v2
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Sticky (便签) API ====================
+
+// ListStickies 列出便签
+func ListStickies(slug string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/", slug)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListStickies 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListStickies 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListStickies 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateSticky 创建便签
+func CreateSticky(slug string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/", slug)
+
+	resp, err := planeWebPost(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateSticky 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateSticky 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateSticky 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetSticky 获取便签详情
+func GetSticky(slug, stickyID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", slug, stickyID)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetSticky 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetSticky 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetSticky 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateSticky 更新便签
+func UpdateSticky(slug, stickyID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", slug, stickyID)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateSticky 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateSticky 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateSticky 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteSticky 删除便签
+func DeleteSticky(slug, stickyID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", slug, stickyID)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteSticky 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteSticky 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteSticky 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// ListCurrentStickies 列出当前工作空间的便签
+func ListCurrentStickies() ([]byte, error) {
+	return ListStickies(webWorkspaceSlug)
+}

+ 128 - 0
service/repositories/api/plane_v2/plane_workspace.go

@@ -0,0 +1,128 @@
+package plane_v2
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// ==================== Workspace API ====================
+
+// ListWorkspaces 列出当前用户的工作空间
+func ListWorkspaces() ([]byte, error) {
+	resp, err := planeWebGet("/api/workspaces/")
+	if err != nil {
+		zap.L().Error("[PlaneWeb] ListWorkspaces 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] ListWorkspaces 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("ListWorkspaces 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// CreateWorkspace 创建工作空间
+func CreateWorkspace(name, slug string) ([]byte, error) {
+	payload := map[string]interface{}{
+		"name": name,
+		"slug": slug,
+	}
+
+	resp, err := planeWebPost("/api/workspaces/", payload)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] CreateWorkspace 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if resp.StatusCode != 201 {
+		zap.L().Error("[PlaneWeb] CreateWorkspace 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("CreateWorkspace 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// GetWorkspace 获取工作空间详情
+func GetWorkspace(slug string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/", slug)
+
+	resp, err := planeWebGet(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] GetWorkspace 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] GetWorkspace 返回错误", zap.String("status", resp.Status))
+		return nil, fmt.Errorf("GetWorkspace 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// UpdateWorkspace 更新工作空间
+func UpdateWorkspace(slug string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/", slug)
+
+	resp, err := planeWebPatch(url, data)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] UpdateWorkspace 请求失败", zap.Error(err))
+		return nil, err
+	}
+
+	if !isWebSuccess(resp.StatusCode) {
+		zap.L().Error("[PlaneWeb] UpdateWorkspace 返回错误",
+			zap.String("status", resp.Status),
+			zap.String("body", string(resp.Body)),
+		)
+		return nil, fmt.Errorf("UpdateWorkspace 失败: %s", resp.Status)
+	}
+
+	return resp.Body, nil
+}
+
+// DeleteWorkspace 删除工作空间
+func DeleteWorkspace(slug string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/", slug)
+
+	resp, err := planeWebDelete(url)
+	if err != nil {
+		zap.L().Error("[PlaneWeb] DeleteWorkspace 请求失败", zap.Error(err))
+		return err
+	}
+
+	if resp.StatusCode != 204 {
+		zap.L().Error("[PlaneWeb] DeleteWorkspace 返回错误", zap.String("status", resp.Status))
+		return fmt.Errorf("DeleteWorkspace 失败: %s", resp.Status)
+	}
+
+	return nil
+}
+
+// GetCurrentWorkspace 获取当前配置的工作空间详情(使用配置中的slug)
+func GetCurrentWorkspace() ([]byte, error) {
+	return GetWorkspace(webWorkspaceSlug)
+}
+
+// WorkspaceInfo 工作空间基础信息
+type WorkspaceInfo struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+	Slug string `json:"slug"`
+}
+
+// ParseWorkspaceList 解析工作空间列表
+func ParseWorkspaceList(data []byte) ([]WorkspaceInfo, error) {
+	var workspaces []WorkspaceInfo
+	if err := json.Unmarshal(data, &workspaces); err != nil {
+		return nil, fmt.Errorf("解析工作空间列表失败: %w", err)
+	}
+	return workspaces, nil
+}

+ 315 - 0
service/repositories/repositories.go

@@ -3,6 +3,8 @@ package repositories
 import (
 	"fwh-toolbox-server/repositories/api/acceleronix"
 	"fwh-toolbox-server/repositories/api/fwh_pro"
+	"fwh-toolbox-server/repositories/api/plane_v1"
+	"fwh-toolbox-server/repositories/api/plane_v2"
 	"fwh-toolbox-server/repositories/api/smartHub"
 	"fwh-toolbox-server/repositories/events"
 	"fwh-toolbox-server/repositories/mongo"
@@ -76,6 +78,319 @@ var QueryAcceleronixBatchSimCardDataInfo = acceleronix.QueryBatchSimCardDataInfo
 var QueryAcceleronixDataPoolList = acceleronix.QueryDataPoolList                 // 查询流量池列表
 var QueryAcceleronixDataPoolSimInfo = acceleronix.QueryDataPoolSimInfo           // 查询流量池SIM详情
 
+// Plane_V1
+type PlaneConfig = plane_v1.PlaneConfig
+type PlaneIntakeIssueRequest = plane_v1.IntakeIssueRequest
+type PlaneIntakeIssue = plane_v1.IntakeIssue
+type PlaneIntakeIssueResponse = plane_v1.IntakeIssueResponse
+type PlaneIntakeListResponse = plane_v1.IntakeListResponse
+type PlaneIntakeListItem = plane_v1.IntakeListItem
+type PlaneIntakeIssueInfo = plane_v1.IntakeIssueInfo
+type PlaneAssetUploadResult = plane_v1.AssetUploadResult
+type PlaneAttachmentCreateResponse = plane_v1.AttachmentCreateResponse
+
+var PlaneHttpInit = plane_v1.PlaneHttpInit              // 初始化Plane HTTP客户端
+var PlaneGetLabelBugId = plane_v1.GetLabelBugId         // 获取Bug标签ID
+var PlaneGetLabelFeatureId = plane_v1.GetLabelFeatureId // 获取Feature标签ID
+
+// Plane_V1 - User
+var PlaneGetMe = plane_v1.GetMe // 获取当前用户信息
+
+// Plane_V1 - Project
+var PlaneListProjects = plane_v1.ListProjects           // 列出项目
+var PlaneCreateProject = plane_v1.CreateProject         // 创建项目
+var PlaneGetProject = plane_v1.GetProject               // 获取项目详情
+var PlaneUpdateProject = plane_v1.UpdateProject         // 更新项目
+var PlaneDeleteProject = plane_v1.DeleteProject         // 删除项目
+var PlaneArchiveProject = plane_v1.ArchiveProject       // 归档项目
+var PlaneUnarchiveProject = plane_v1.UnarchiveProject   // 取消归档项目
+var PlaneGetProjectSummary = plane_v1.GetProjectSummary // 获取项目摘要
+
+// Plane_V1 - Work Item
+var PlaneListWorkItems = plane_v1.ListWorkItems                     // 列出工作项
+var PlaneCreateWorkItem = plane_v1.CreateWorkItem                   // 创建工作项
+var PlaneGetWorkItem = plane_v1.GetWorkItem                         // 获取工作项详情
+var PlaneUpdateWorkItem = plane_v1.UpdateWorkItem                   // 更新工作项
+var PlaneDeleteWorkItem = plane_v1.DeleteWorkItem                   // 删除工作项
+var PlaneGetWorkItemByIdentifier = plane_v1.GetWorkItemByIdentifier // 通过标识符获取工作项
+var PlaneSearchWorkItems = plane_v1.SearchWorkItems                 // 搜索工作项
+
+// Plane_V1 - Work Item Link
+var PlaneListWorkItemLinks = plane_v1.ListWorkItemLinks   // 列出链接
+var PlaneCreateWorkItemLink = plane_v1.CreateWorkItemLink // 创建链接
+var PlaneGetWorkItemLink = plane_v1.GetWorkItemLink       // 获取链接详情
+var PlaneUpdateWorkItemLink = plane_v1.UpdateWorkItemLink // 更新链接
+var PlaneDeleteWorkItemLink = plane_v1.DeleteWorkItemLink // 删除链接
+
+// Plane_V1 - Work Item Comment
+var PlaneListWorkItemComments = plane_v1.ListWorkItemComments   // 列出评论
+var PlaneCreateWorkItemComment = plane_v1.CreateWorkItemComment // 创建评论
+var PlaneGetWorkItemComment = plane_v1.GetWorkItemComment       // 获取评论详情
+var PlaneUpdateWorkItemComment = plane_v1.UpdateWorkItemComment // 更新评论
+var PlaneDeleteWorkItemComment = plane_v1.DeleteWorkItemComment // 删除评论
+
+// Plane_V1 - Work Item Activity
+var PlaneListWorkItemActivities = plane_v1.ListWorkItemActivities // 列出活动历史
+var PlaneGetWorkItemActivity = plane_v1.GetWorkItemActivity       // 获取单条活动
+
+// Plane_V1 - Work Item Attachment
+var PlaneListWorkItemAttachments = plane_v1.ListWorkItemAttachments     // 列出附件
+var PlaneCreateWorkItemAttachment = plane_v1.CreateWorkItemAttachment   // 创建附件(获取预签名URL)
+var PlaneConfirmWorkItemAttachment = plane_v1.ConfirmWorkItemAttachment // 确认附件上传
+var PlaneDeleteWorkItemAttachment = plane_v1.DeleteWorkItemAttachment   // 删除附件
+var PlaneUploadWorkItemAttachment = plane_v1.UploadWorkItemAttachment   // 完整附件上传流程
+
+// Plane_V1 - Work Item Relation
+var PlaneListWorkItemRelations = plane_v1.ListWorkItemRelations   // 列出关系
+var PlaneCreateWorkItemRelation = plane_v1.CreateWorkItemRelation // 创建关系
+
+// Plane_V1 - Intake
+var PlaneCreateIntakeIssue = plane_v1.CreateIntakeIssue         // 创建Intake Issue
+var PlaneGetIntakeIssueList = plane_v1.GetIntakeIssueList       // 查询Intake Issue列表
+var PlaneGetIntakeIssueDetail = plane_v1.GetIntakeIssueDetail   // 获取Intake Issue详情
+var PlaneUpdateIntakeIssue = plane_v1.UpdateIntakeIssue         // 更新Intake Issue
+var PlaneAcceptIntakeIssue = plane_v1.AcceptIntakeIssue         // 接受Intake Issue
+var PlaneRejectIntakeIssue = plane_v1.RejectIntakeIssue         // 拒绝Intake Issue
+var PlaneDeleteIntakeIssue = plane_v1.DeleteIntakeIssue         // 删除Intake Issue
+
+// Plane_V1 - Asset
+var PlaneUploadAsset = plane_v1.UploadAsset // 上传Asset文件
+var PlaneDeleteAsset = plane_v1.DeleteAsset // 删除Asset文件
+
+// Plane_V1 - Cycle
+var PlaneListCycles = plane_v1.ListCycles                     // 列出迭代
+var PlaneCreateCycle = plane_v1.CreateCycle                   // 创建迭代
+var PlaneGetCycle = plane_v1.GetCycle                         // 获取迭代详情
+var PlaneUpdateCycle = plane_v1.UpdateCycle                   // 更新迭代
+var PlaneDeleteCycle = plane_v1.DeleteCycle                   // 删除迭代
+var PlaneListCycleIssues = plane_v1.ListCycleIssues           // 列出迭代工作项
+var PlaneAddIssuesToCycle = plane_v1.AddIssuesToCycle         // 添加工作项到迭代
+var PlaneRemoveIssueFromCycle = plane_v1.RemoveIssueFromCycle // 从迭代移除工作项
+var PlaneTransferCycleIssues = plane_v1.TransferCycleIssues   // 转移迭代工作项
+var PlaneArchiveCycle = plane_v1.ArchiveCycle                 // 归档迭代
+
+// Plane_V1 - Module
+var PlaneListModules = plane_v1.ListModules                     // 列出模块
+var PlaneCreateModule = plane_v1.CreateModule                   // 创建模块
+var PlaneGetModule = plane_v1.GetModule                         // 获取模块详情
+var PlaneUpdateModule = plane_v1.UpdateModule                   // 更新模块
+var PlaneDeleteModule = plane_v1.DeleteModule                   // 删除模块
+var PlaneListModuleIssues = plane_v1.ListModuleIssues           // 列出模块工作项
+var PlaneAddIssuesToModule = plane_v1.AddIssuesToModule         // 添加工作项到模块
+var PlaneRemoveIssueFromModule = plane_v1.RemoveIssueFromModule // 从模块移除工作项
+var PlaneArchiveModule = plane_v1.ArchiveModule                 // 归档模块
+
+// Plane_V1 - State
+var PlaneListStates = plane_v1.ListStates   // 列出状态
+var PlaneCreateState = plane_v1.CreateState // 创建状态
+var PlaneGetState = plane_v1.GetState       // 获取状态详情
+var PlaneUpdateState = plane_v1.UpdateState // 更新状态
+var PlaneDeleteState = plane_v1.DeleteState // 删除状态
+
+// Plane_V1 - Label
+var PlaneListLabels = plane_v1.ListLabels   // 列出标签
+var PlaneCreateLabel = plane_v1.CreateLabel // 创建标签
+var PlaneGetLabel = plane_v1.GetLabel       // 获取标签详情
+var PlaneUpdateLabel = plane_v1.UpdateLabel // 更新标签
+var PlaneDeleteLabel = plane_v1.DeleteLabel // 删除标签
+
+// Plane_V1 - Member
+var PlaneListWorkspaceMembers = plane_v1.ListWorkspaceMembers       // 列出工作空间成员
+var PlaneListProjectMembers = plane_v1.ListProjectMembers           // 列出项目成员
+var PlaneAddProjectMember = plane_v1.AddProjectMember               // 添加项目成员
+var PlaneGetProjectMember = plane_v1.GetProjectMember               // 获取项目成员详情
+var PlaneUpdateProjectMemberRole = plane_v1.UpdateProjectMemberRole // 更新项目成员角色
+var PlaneRemoveProjectMember = plane_v1.RemoveProjectMember         // 移除项目成员
+
+// Plane_V1 - Invitation
+var PlaneListInvitations = plane_v1.ListInvitations   // 列出邀请
+var PlaneCreateInvitation = plane_v1.CreateInvitation // 创建邀请
+var PlaneGetInvitation = plane_v1.GetInvitation       // 获取邀请详情
+var PlaneUpdateInvitation = plane_v1.UpdateInvitation // 更新邀请
+var PlaneDeleteInvitation = plane_v1.DeleteInvitation // 撤销邀请
+
+// Plane_V1 - Sticky
+var PlaneListStickies = plane_v1.ListStickies   // 列出便签
+var PlaneCreateSticky = plane_v1.CreateSticky   // 创建便签
+var PlaneGetSticky = plane_v1.GetSticky         // 获取便签详情
+var PlaneUpdateSticky = plane_v1.UpdateSticky   // 更新便签
+var PlaneDeleteSticky = plane_v1.DeleteSticky   // 删除便签
+
+// Plane_V2 Web API (Session + CSRF 认证)
+type PlaneWebIntakeIssueRequest = plane_v2.WebIntakeIssueRequest
+type PlaneWebIntakeIssueResponse = plane_v2.WebIntakeIssueResponse
+type PlaneWebAssetUploadResult = plane_v2.WebAssetUploadResult
+type PlaneWebWorkspaceInfo = plane_v2.WorkspaceInfo
+
+var PlaneWebHttpInit = plane_v2.PlaneWebHttpInit             // 初始化Plane Web HTTP客户端
+var PlaneWebGetWorkspaceSlug = plane_v2.GetWorkspaceSlug     // 获取WorkspaceSlug
+var PlaneWebGetProjectId = plane_v2.GetProjectId             // 获取ProjectId
+var PlaneWebGetLabelBugId = plane_v2.GetWebLabelBugId        // 获取Bug标签ID
+var PlaneWebGetLabelFeatureId = plane_v2.GetWebLabelFeatureId // 获取Feature标签ID
+var PlaneWebGetLabelFeedbackId = plane_v2.GetWebLabelFeedbackId // 获取Feedback标签ID
+
+// Plane_V2 - Workspace
+var PlaneWebListWorkspaces = plane_v2.ListWorkspaces         // 列出工作空间
+var PlaneWebCreateWorkspace = plane_v2.CreateWorkspace       // 创建工作空间
+var PlaneWebGetWorkspace = plane_v2.GetWorkspace             // 获取工作空间详情
+var PlaneWebUpdateWorkspace = plane_v2.UpdateWorkspace       // 更新工作空间
+var PlaneWebDeleteWorkspace = plane_v2.DeleteWorkspace       // 删除工作空间
+
+// Plane_V2 - Project
+var PlaneWebListProjects = plane_v2.ListProjects             // 列出项目
+var PlaneWebCreateProject = plane_v2.CreateProject           // 创建项目
+var PlaneWebGetProject = plane_v2.GetProject                 // 获取项目详情
+var PlaneWebUpdateProject = plane_v2.UpdateProject           // 更新项目
+var PlaneWebDeleteProject = plane_v2.DeleteProject           // 删除项目
+var PlaneWebArchiveProject = plane_v2.ArchiveProject         // 归档项目
+var PlaneWebUnarchiveProject = plane_v2.UnarchiveProject     // 取消归档项目
+
+// Plane_V2 - Issue
+var PlaneWebListIssues = plane_v2.ListIssues                 // 列出工作项
+var PlaneWebCreateIssue = plane_v2.CreateIssue               // 创建工作项
+var PlaneWebGetIssue = plane_v2.GetIssue                     // 获取工作项详情
+var PlaneWebUpdateIssue = plane_v2.UpdateIssue               // 更新工作项
+var PlaneWebDeleteIssue = plane_v2.DeleteIssue               // 删除工作项
+var PlaneWebGetSubIssues = plane_v2.GetSubIssues             // 获取子工作项
+var PlaneWebArchiveIssue = plane_v2.ArchiveIssue             // 归档工作项
+var PlaneWebUnarchiveIssue = plane_v2.UnarchiveIssue         // 取消归档工作项
+var PlaneWebGetIssueActivity = plane_v2.GetIssueActivity     // 获取工作项活动历史
+
+// Plane_V2 - Comment
+var PlaneWebListComments = plane_v2.ListComments             // 列出评论
+var PlaneWebCreateComment = plane_v2.CreateComment           // 创建评论
+var PlaneWebGetComment = plane_v2.GetComment                 // 获取评论详情
+var PlaneWebUpdateComment = plane_v2.UpdateComment           // 更新评论
+var PlaneWebDeleteComment = plane_v2.DeleteComment           // 删除评论
+
+// Plane_V2 - Link
+var PlaneWebListIssueLinks = plane_v2.ListIssueLinks         // 列出工作项链接
+var PlaneWebCreateIssueLink = plane_v2.CreateIssueLink       // 创建工作项链接
+var PlaneWebUpdateIssueLink = plane_v2.UpdateIssueLink       // 更新工作项链接
+var PlaneWebDeleteIssueLink = plane_v2.DeleteIssueLink       // 删除工作项链接
+
+// Plane_V2 - Relation
+var PlaneWebListIssueRelations = plane_v2.ListIssueRelations       // 列出工作项关系
+var PlaneWebCreateIssueRelation = plane_v2.CreateIssueRelation     // 创建工作项关系
+var PlaneWebRemoveIssueRelation = plane_v2.RemoveIssueRelation     // 移除工作项关系
+
+// Plane_V2 - Cycle
+var PlaneWebListCycles = plane_v2.ListCycles                       // 列出迭代
+var PlaneWebCreateCycle = plane_v2.CreateCycle                     // 创建迭代
+var PlaneWebGetCycle = plane_v2.GetCycle                           // 获取迭代详情
+var PlaneWebUpdateCycle = plane_v2.UpdateCycle                     // 更新迭代
+var PlaneWebDeleteCycle = plane_v2.DeleteCycle                     // 删除迭代
+var PlaneWebListCycleIssues = plane_v2.ListCycleIssues             // 列出迭代工作项
+var PlaneWebAddIssuesToCycle = plane_v2.AddIssuesToCycle           // 添加工作项到迭代
+var PlaneWebRemoveIssueFromCycle = plane_v2.RemoveIssueFromCycle   // 从迭代移除工作项
+var PlaneWebTransferCycleIssues = plane_v2.TransferCycleIssues     // 转移迭代工作项
+var PlaneWebArchiveCycle = plane_v2.ArchiveCycle                   // 归档迭代
+var PlaneWebListArchivedCycles = plane_v2.ListArchivedCycles       // 列出已归档迭代
+var PlaneWebUnarchiveCycle = plane_v2.UnarchiveCycle               // 取消归档迭代
+var PlaneWebGetCycleProgress = plane_v2.GetCycleProgress           // 获取迭代进度
+var PlaneWebGetCycleAnalytics = plane_v2.GetCycleAnalytics         // 获取迭代分析
+
+// Plane_V2 - Module
+var PlaneWebListModules = plane_v2.ListModules                     // 列出模块
+var PlaneWebCreateModule = plane_v2.CreateModule                   // 创建模块
+var PlaneWebGetModule = plane_v2.GetModule                         // 获取模块详情
+var PlaneWebUpdateModule = plane_v2.UpdateModule                   // 更新模块
+var PlaneWebDeleteModule = plane_v2.DeleteModule                   // 删除模块
+var PlaneWebListModuleIssues = plane_v2.ListModuleIssues           // 列出模块工作项
+var PlaneWebAddIssuesToModule = plane_v2.AddIssuesToModule         // 添加工作项到模块
+var PlaneWebRemoveIssueFromModule = plane_v2.RemoveIssueFromModule // 从模块移除工作项
+var PlaneWebListModuleLinks = plane_v2.ListModuleLinks             // 列出模块链接
+var PlaneWebCreateModuleLink = plane_v2.CreateModuleLink           // 创建模块链接
+var PlaneWebUpdateModuleLink = plane_v2.UpdateModuleLink           // 更新模块链接
+var PlaneWebDeleteModuleLink = plane_v2.DeleteModuleLink           // 删除模块链接
+var PlaneWebArchiveModule = plane_v2.ArchiveModule                 // 归档模块
+var PlaneWebListArchivedModules = plane_v2.ListArchivedModules     // 列出已归档模块
+var PlaneWebUnarchiveModule = plane_v2.UnarchiveModule             // 取消归档模块
+
+// Plane_V2 - State
+var PlaneWebListStates = plane_v2.ListStates                       // 列出状态
+var PlaneWebCreateState = plane_v2.CreateState                     // 创建状态
+var PlaneWebGetState = plane_v2.GetState                           // 获取状态详情
+var PlaneWebUpdateState = plane_v2.UpdateState                     // 更新状态
+var PlaneWebDeleteState = plane_v2.DeleteState                     // 删除状态
+var PlaneWebMarkStateDefault = plane_v2.MarkStateDefault           // 设为默认状态
+
+// Plane_V2 - Label
+var PlaneWebListLabels = plane_v2.ListLabels                       // 列出标签
+var PlaneWebCreateLabel = plane_v2.CreateLabel                     // 创建标签
+var PlaneWebGetLabel = plane_v2.GetLabel                           // 获取标签详情
+var PlaneWebUpdateLabel = plane_v2.UpdateLabel                     // 更新标签
+var PlaneWebDeleteLabel = plane_v2.DeleteLabel                     // 删除标签
+
+// Plane_V2 - Member
+var PlaneWebListWorkspaceMembers = plane_v2.ListWorkspaceMembers         // 列出工作空间成员
+var PlaneWebGetWorkspaceMember = plane_v2.GetWorkspaceMember             // 获取工作空间成员详情
+var PlaneWebUpdateWorkspaceMemberRole = plane_v2.UpdateWorkspaceMemberRole // 更新工作空间成员角色
+var PlaneWebRemoveWorkspaceMember = plane_v2.RemoveWorkspaceMember       // 移除工作空间成员
+var PlaneWebListProjectMembers = plane_v2.ListProjectMembers             // 列出项目成员
+var PlaneWebAddProjectMember = plane_v2.AddProjectMember                 // 添加项目成员
+var PlaneWebGetProjectMember = plane_v2.GetProjectMember                 // 获取项目成员详情
+var PlaneWebUpdateProjectMemberRole = plane_v2.UpdateProjectMemberRole   // 更新项目成员角色
+var PlaneWebRemoveProjectMember = plane_v2.RemoveProjectMember           // 移除项目成员
+var PlaneWebGetCurrentUser = plane_v2.GetCurrentUser                     // 获取当前用户信息
+var PlaneWebUpdateCurrentUser = plane_v2.UpdateCurrentUser               // 更新当前用户信息
+
+// Plane_V2 - Intake
+var PlaneWebListIntakeIssues = plane_v2.ListIntakeIssues             // 列出收件箱工作项
+var PlaneWebCreateIntakeIssue = plane_v2.CreateIntakeIssue           // 创建收件箱工作项
+var PlaneWebCreateIntakeIssueRaw = plane_v2.CreateIntakeIssueRaw     // 创建收件箱工作项(Raw)
+var PlaneWebGetIntakeIssue = plane_v2.GetIntakeIssue                 // 获取收件箱工作项详情
+var PlaneWebUpdateIntakeIssue = plane_v2.UpdateIntakeIssue           // 更新收件箱工作项
+var PlaneWebAcceptIntakeIssue = plane_v2.AcceptIntakeIssue           // 接受收件箱工作项
+var PlaneWebRejectIntakeIssue = plane_v2.RejectIntakeIssue           // 拒绝收件箱工作项
+var PlaneWebSnoozeIntakeIssue = plane_v2.SnoozeIntakeIssue           // 暂缓收件箱工作项
+var PlaneWebDuplicateIntakeIssue = plane_v2.DuplicateIntakeIssue     // 标记重复收件箱工作项
+var PlaneWebDeleteIntakeIssue = plane_v2.DeleteIntakeIssue           // 删除收件箱工作项
+var PlaneWebListCurrentIntakeIssues = plane_v2.ListCurrentIntakeIssues   // 列出当前项目收件箱工作项
+var PlaneWebCreateCurrentIntakeIssue = plane_v2.CreateCurrentIntakeIssue // 在当前项目创建收件箱工作项
+
+// Plane_V2 - Asset
+const PlaneWebEntityIssueDescription = plane_v2.EntityIssueDescription
+const PlaneWebEntityIssueAttachment = plane_v2.EntityIssueAttachment
+const PlaneWebEntityCommentDescription = plane_v2.EntityCommentDescription
+const PlaneWebEntityPageDescription = plane_v2.EntityPageDescription
+
+var PlaneWebCreateProjectAsset = plane_v2.CreateProjectAsset             // 创建项目资源
+var PlaneWebUploadToS3 = plane_v2.UploadToS3                             // 上传文件到S3
+var PlaneWebConfirmProjectAssetUpload = plane_v2.ConfirmProjectAssetUpload // 确认项目资源上传
+var PlaneWebGetProjectAsset = plane_v2.GetProjectAsset                   // 下载项目资源
+var PlaneWebGetProjectAssetURL = plane_v2.GetProjectAssetURL             // 获取项目资源预签名URL
+var PlaneWebDeleteProjectAsset = plane_v2.DeleteProjectAsset             // 删除项目资源
+var PlaneWebBindAssetsToEntity = plane_v2.BindAssetsToEntity             // 批量绑定资源到实体
+var PlaneWebCreateWorkspaceAsset = plane_v2.CreateWorkspaceAsset         // 创建工作空间资源
+var PlaneWebConfirmWorkspaceAssetUpload = plane_v2.ConfirmWorkspaceAssetUpload // 确认工作空间资源上传
+var PlaneWebCreateUserAsset = plane_v2.CreateUserAsset                   // 创建用户资源
+var PlaneWebConfirmUserAssetUpload = plane_v2.ConfirmUserAssetUpload     // 确认用户资源上传
+var PlaneWebUploadProjectAsset = plane_v2.UploadProjectAsset             // 完整上传项目资源流程
+var PlaneWebUploadCurrentProjectAsset = plane_v2.UploadCurrentProjectAsset // 在当前项目上传资源
+
+// Plane_V2 - Page
+var PlaneWebListPages = plane_v2.ListPages                     // 列出页面
+var PlaneWebCreatePage = plane_v2.CreatePage                   // 创建页面
+var PlaneWebGetPage = plane_v2.GetPage                         // 获取页面详情
+var PlaneWebUpdatePage = plane_v2.UpdatePage                   // 更新页面
+var PlaneWebGetPageDescription = plane_v2.GetPageDescription   // 获取页面描述
+var PlaneWebUpdatePageDescription = plane_v2.UpdatePageDescription // 更新页面描述
+var PlaneWebDeletePage = plane_v2.DeletePage                   // 删除页面
+var PlaneWebArchivePage = plane_v2.ArchivePage                 // 归档页面
+var PlaneWebUnarchivePage = plane_v2.UnarchivePage             // 取消归档页面
+var PlaneWebLockPage = plane_v2.LockPage                       // 锁定页面
+var PlaneWebUnlockPage = plane_v2.UnlockPage                   // 解锁页面
+var PlaneWebSetPageAccess = plane_v2.SetPageAccess             // 设置页面访问权限
+var PlaneWebDuplicatePage = plane_v2.DuplicatePage             // 复制页面
+
+// Plane_V2 - Sticky
+var PlaneWebListStickies = plane_v2.ListStickies               // 列出便签
+var PlaneWebCreateSticky = plane_v2.CreateSticky               // 创建便签
+var PlaneWebGetSticky = plane_v2.GetSticky                     // 获取便签详情
+var PlaneWebUpdateSticky = plane_v2.UpdateSticky               // 更新便签
+var PlaneWebDeleteSticky = plane_v2.DeleteSticky               // 删除便签
+
 /*************** EVENTS ***************/
 // Xlog
 var GetXlogManageList = events.GetXlogManageList

+ 34 - 0
service/routes/feedbackRoutes.go

@@ -0,0 +1,34 @@
+/*
+ * Feedback API List
+ * /feedback/submit       - POST   提交问题反馈或需求
+ * /feedback/history      - GET    查询提交历史
+ * /feedback/upload       - POST   上传附件到 Plane
+ * /feedback/asset/:id    - DELETE 删除已上传的附件
+ */
+package routes
+
+import (
+	"fwh-toolbox-server/controllers"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func FeedbackSetup(api fiber.Router) {
+	// 提交问题反馈或需求
+	api.Post("/feedback/submit", controllers.SubmitFeedback)
+
+	// 查询提交历史
+	api.Get("/feedback/history", controllers.GetFeedbackHistory)
+
+	// 获取反馈详情
+	api.Get("/feedback/detail/:id", controllers.GetFeedbackDetail)
+
+	// 代理查看附件(从 Plane 下载后转发)
+	api.Get("/feedback/asset/view/:id", controllers.ViewFeedbackAsset)
+
+	// 上传附件到 Plane
+	api.Post("/feedback/upload", controllers.UploadFeedbackAsset)
+
+	// 删除已上传的附件
+	api.Delete("/feedback/asset/:id", controllers.DeleteFeedbackAsset)
+}

+ 1 - 0
service/routes/routes.go

@@ -33,6 +33,7 @@ func Setup(app *fiber.App) {
 	DespatchConfigSetup(api)
 	FaultRecordSetup(api)
 	SimCardManageSetup(api)
+	FeedbackSetup(api)
 
 	app.Get("/swagger/*", swagger.HandlerDefault)
 

+ 418 - 0
service/services/feedbackServices.go

@@ -0,0 +1,418 @@
+package services
+
+import (
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/repositories"
+	"io"
+	"net/http"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/google/uuid"
+	"go.uber.org/zap"
+)
+
+// FeedbackHistoryItem 返回给前端的历史记录条目
+type FeedbackHistoryItem struct {
+	Id        string `json:"id"`
+	Title     string `json:"title"`
+	Type      string `json:"type"`      // bug / feature
+	Status    int    `json:"status"`    // -2 Pending, -1 Rejected, 0 Snoozed, 1 Accepted, 2 Duplicate
+	Priority  string `json:"priority"`
+	CreatedAt string `json:"createdAt"`
+	CreatedBy string `json:"createdBy"` // 提交人用户名
+}
+
+// SubmitFeedback 提交反馈到 Plane Intake (V2 Web API)
+// 流程:1. 构造HTML(含image-component) → 2. 创建IntakeIssue → 3. 绑定Asset到Issue
+func SubmitFeedback(body dto.FeedbackSubmitDTO, userName string) (*repositories.PlaneWebIntakeIssueResponse, error) {
+	// 构造标题(包含提交人标识,供历史查询时解析)
+	var prefix string
+	if body.Type == "bug" {
+		prefix = "[Bug]"
+	} else {
+		prefix = "[Feature]"
+	}
+	title := fmt.Sprintf("%s %s 「%s」", prefix, body.Title, userName)
+
+	// 构造描述 HTML(附件使用 image-component 格式)
+	descHTML := buildDescriptionHTML(body, userName)
+
+	// 选择 Label(始终添加 Feedback 标签 + 类型标签)
+	var labelIds []string
+	if repositories.PlaneWebGetLabelFeedbackId() != "" {
+		labelIds = append(labelIds, repositories.PlaneWebGetLabelFeedbackId())
+	}
+	if body.Type == "bug" && repositories.PlaneWebGetLabelBugId() != "" {
+		labelIds = append(labelIds, repositories.PlaneWebGetLabelBugId())
+	} else if body.Type == "feature" && repositories.PlaneWebGetLabelFeatureId() != "" {
+		labelIds = append(labelIds, repositories.PlaneWebGetLabelFeatureId())
+	}
+
+	// 映射优先级
+	priority := mapPriority(body.Priority)
+
+	// 构造请求体 (Plane V2 Web API)
+	req := repositories.PlaneWebIntakeIssueRequest{
+		Name:            title,
+		DescriptionHTML: descHTML,
+		Priority:        priority,
+		LabelIDs:        labelIds,
+	}
+
+	// 调用 Plane V2 Web Intake API 创建Issue
+	result, err := repositories.PlaneWebCreateCurrentIntakeIssue(req)
+	if err != nil {
+		return nil, err
+	}
+
+	issueId := result.Issue.ID
+
+	// 创建成功后,如果有附件,将 Asset 绑定到 Issue Entity
+	if len(body.AssetIds) > 0 {
+		slug := repositories.PlaneWebGetWorkspaceSlug()
+		projectId := repositories.PlaneWebGetProjectId()
+
+		if err := repositories.PlaneWebBindAssetsToEntity(slug, projectId, issueId, body.AssetIds); err != nil {
+			// 绑定失败不阻塞整体提交,但记录警告
+			zap.L().Warn("[Feedback] 绑定附件到Issue失败",
+				zap.String("issueId", issueId),
+				zap.Strings("assetIds", body.AssetIds),
+				zap.Error(err),
+			)
+		} else {
+			zap.L().Info("[Feedback] 附件绑定成功",
+				zap.String("issueId", issueId),
+				zap.Int("assetCount", len(body.AssetIds)),
+			)
+		}
+	}
+
+	zap.L().Info("[Feedback] 提交成功",
+		zap.String("issueId", issueId),
+		zap.String("user", userName),
+		zap.String("type", body.Type),
+		zap.String("title", body.Title),
+	)
+
+	return result, nil
+}
+
+// buildDescriptionHTML 构造提交描述的 HTML
+// 附件使用 Plane 原生 <image-component> 格式,放在文本最后
+func buildDescriptionHTML(body dto.FeedbackSubmitDTO, userName string) string {
+	var sb strings.Builder
+
+	sb.WriteString(fmt.Sprintf(`<p class="editor-paragraph-block"><strong>提交人:</strong>%s</p>`, userName))
+	sb.WriteString(fmt.Sprintf(`<p class="editor-paragraph-block"><strong>分类:</strong>%s</p>`, body.Category))
+
+	if body.RelatedPage != nil && *body.RelatedPage != "" {
+		sb.WriteString(fmt.Sprintf(`<p class="editor-paragraph-block"><strong>相关页面:</strong>%s</p>`, *body.RelatedPage))
+	}
+
+	sb.WriteString("<hr/>")
+
+	if body.Type == "bug" {
+		// Bug Report
+		if body.Steps != nil && *body.Steps != "" {
+			sb.WriteString(fmt.Sprintf(`<h3>复现步骤</h3><p class="editor-paragraph-block">%s</p>`, strings.ReplaceAll(*body.Steps, "\n", "<br/>")))
+		}
+		if body.Expected != nil && *body.Expected != "" {
+			sb.WriteString(fmt.Sprintf(`<h3>期望结果</h3><p class="editor-paragraph-block">%s</p>`, strings.ReplaceAll(*body.Expected, "\n", "<br/>")))
+		}
+		if body.Actual != nil && *body.Actual != "" {
+			sb.WriteString(fmt.Sprintf(`<h3>实际结果</h3><p class="editor-paragraph-block">%s</p>`, strings.ReplaceAll(*body.Actual, "\n", "<br/>")))
+		}
+		if body.Description != "" {
+			sb.WriteString(fmt.Sprintf(`<h3>补充说明</h3><p class="editor-paragraph-block">%s</p>`, strings.ReplaceAll(body.Description, "\n", "<br/>")))
+		}
+	} else {
+		// Feature Request
+		if body.Description != "" {
+			sb.WriteString(fmt.Sprintf(`<h3>需求描述</h3><p class="editor-paragraph-block">%s</p>`, strings.ReplaceAll(body.Description, "\n", "<br/>")))
+		}
+		if body.Scenario != nil && *body.Scenario != "" {
+			sb.WriteString(fmt.Sprintf(`<h3>使用场景</h3><p class="editor-paragraph-block">%s</p>`, strings.ReplaceAll(*body.Scenario, "\n", "<br/>")))
+		}
+	}
+
+	// 附件放在最后,带"附件"大标题
+	if len(body.AssetIds) > 0 {
+		sb.WriteString(`<h3>附件</h3>`)
+		for _, assetId := range body.AssetIds {
+			dataId := uuid.New().String()
+			sb.WriteString(fmt.Sprintf(
+				`<image-component data-id="%s" src="%s" id="%s" width="35%%" alignment="left" status="uploaded"></image-component>`,
+				dataId, assetId, dataId,
+			))
+		}
+	}
+
+	return sb.String()
+}
+
+// GetFeedbackHistory 查询 Plane Intake 列表 (V2 Web API)
+// 只返回带有 Feedback 标签的记录,包含所有状态(Pending/Accepted/Rejected/Snoozed/Duplicate)
+func GetFeedbackHistory(userName string) ([]FeedbackHistoryItem, error) {
+	// 查询所有状态的 Intake 记录:-2 Pending, -1 Rejected, 0 Snoozed, 1 Accepted, 2 Duplicate
+	data, err := repositories.PlaneWebListCurrentIntakeIssues("-2,-1,0,1,2")
+	if err != nil {
+		return nil, err
+	}
+
+	feedbackLabelId := repositories.PlaneWebGetLabelFeedbackId()
+
+	// V2 返回的是 raw JSON,直接解析
+	type intakeItem struct {
+		Status int `json:"status"`
+		Issue  struct {
+			ID        string   `json:"id"`
+			Name      string   `json:"name"`
+			Priority  string   `json:"priority"`
+			LabelIDs  []string `json:"label_ids"`
+			CreatedAt string   `json:"created_at"`
+			CreatedBy string   `json:"created_by"`
+		} `json:"issue"`
+	}
+
+	var items []intakeItem
+	if err := json.Unmarshal(data, &items); err != nil {
+		// 可能是分页格式
+		var paged struct {
+			Results []intakeItem `json:"results"`
+		}
+		if err2 := json.Unmarshal(data, &paged); err2 != nil {
+			return nil, fmt.Errorf("解析Intake列表失败: %w", err)
+		}
+		items = paged.Results
+	}
+
+	result := []FeedbackHistoryItem{}
+	for _, item := range items {
+		// 只显示带有 Feedback 标签的记录
+		if feedbackLabelId != "" && !containsLabel(item.Issue.LabelIDs, feedbackLabelId) {
+			continue
+		}
+		result = append(result, FeedbackHistoryItem{
+			Id:        item.Issue.ID,
+			Title:     cleanTitle(item.Issue.Name),
+			Type:      extractType(item.Issue.Name),
+			Status:    item.Status,
+			Priority:  item.Issue.Priority,
+			CreatedAt: item.Issue.CreatedAt,
+			CreatedBy: extractCreator(item.Issue.Name),
+		})
+	}
+
+	return result, nil
+}
+
+// extractCreator 从标题中提取提交人(格式:[Type] title 「username」)
+func extractCreator(title string) string {
+	start := strings.LastIndex(title, "「")
+	end := strings.LastIndex(title, "」")
+	if start >= 0 && end > start {
+		return title[start+len("「") : end]
+	}
+	return ""
+}
+
+// extractType 从标题中提取类型
+func extractType(title string) string {
+	if strings.HasPrefix(title, "[Bug]") {
+		return "bug"
+	}
+	if strings.HasPrefix(title, "[Feature]") {
+		return "feature"
+	}
+	return ""
+}
+
+// cleanTitle 从标题中移除类型前缀和提交人后缀,返回纯标题
+func cleanTitle(title string) string {
+	// 移除 [Bug] 或 [Feature] 前缀
+	title = strings.TrimPrefix(title, "[Bug] ")
+	title = strings.TrimPrefix(title, "[Feature] ")
+	// 移除末尾的 「username」
+	if idx := strings.LastIndex(title, " 「"); idx >= 0 {
+		title = title[:idx]
+	}
+	return title
+}
+
+// containsLabel 判断标签列表中是否包含指定标签
+func containsLabel(labels []string, target string) bool {
+	for _, l := range labels {
+		if l == target {
+			return true
+		}
+	}
+	return false
+}
+
+// FeedbackDetailResult 反馈详情
+type FeedbackDetailResult struct {
+	Id              string                `json:"id"`
+	Title           string                `json:"title"`
+	DescriptionHTML string                `json:"descriptionHtml"`
+	Priority        string                `json:"priority"`
+	Status          int                   `json:"status"`
+	CreatedAt       string                `json:"createdAt"`
+	Attachments     []FeedbackAttachment  `json:"attachments"`
+}
+
+// FeedbackAttachment 附件信息
+type FeedbackAttachment struct {
+	AssetId string `json:"assetId"`
+}
+
+// GetFeedbackDetail 获取单条反馈详情(含 description_html 和附件 ID 列表)
+func GetFeedbackDetail(issueId string) (*FeedbackDetailResult, error) {
+	slug := repositories.PlaneWebGetWorkspaceSlug()
+	projectId := repositories.PlaneWebGetProjectId()
+
+	data, err := repositories.PlaneWebGetIntakeIssue(slug, projectId, issueId)
+	if err != nil {
+		return nil, err
+	}
+
+	var detail struct {
+		Status int `json:"status"`
+		Issue  struct {
+			ID              string `json:"id"`
+			Name            string `json:"name"`
+			DescriptionHTML string `json:"description_html"`
+			Priority        string `json:"priority"`
+			CreatedAt       string `json:"created_at"`
+		} `json:"issue"`
+	}
+	if err := json.Unmarshal(data, &detail); err != nil {
+		return nil, fmt.Errorf("解析反馈详情失败: %w", err)
+	}
+
+	// 从 description_html 中提取 image-component 的 asset ID
+	attachments := extractAttachmentIds(detail.Issue.DescriptionHTML)
+
+	// 清理 description_html 中的 image-component 标签(附件单独展示)
+	cleanedHTML := removeImageComponents(detail.Issue.DescriptionHTML)
+
+	return &FeedbackDetailResult{
+		Id:              detail.Issue.ID,
+		Title:           cleanTitle(detail.Issue.Name),
+		DescriptionHTML: cleanedHTML,
+		Priority:        detail.Issue.Priority,
+		Status:          detail.Status,
+		CreatedAt:       detail.Issue.CreatedAt,
+		Attachments:     attachments,
+	}, nil
+}
+
+// imageComponentSrcRegex 匹配 <image-component ... src="assetId" ...>...</image-component>
+var imageComponentSrcRegex = regexp.MustCompile(`<image-component[^>]+src="([0-9a-f-]{36})"[^>]*>.*?</image-component>`)
+
+// extractAttachmentIds 从 description_html 中提取附件 asset ID 列表
+func extractAttachmentIds(html string) []FeedbackAttachment {
+	matches := imageComponentSrcRegex.FindAllStringSubmatch(html, -1)
+	if len(matches) == 0 {
+		return nil
+	}
+
+	var attachments []FeedbackAttachment
+	for _, match := range matches {
+		attachments = append(attachments, FeedbackAttachment{AssetId: match[1]})
+	}
+	return attachments
+}
+
+// removeImageComponents 移除 description_html 中的 image-component 标签
+func removeImageComponents(html string) string {
+	return imageComponentSrcRegex.ReplaceAllString(html, "")
+}
+
+// mapPriority 映射优先级到 Plane 格式
+func mapPriority(priority string) string {
+	switch priority {
+	case "urgent":
+		return "urgent"
+	case "high":
+		return "high"
+	case "medium":
+		return "medium"
+	case "low":
+		return "low"
+	default:
+		return "none"
+	}
+}
+
+// UploadFeedbackAsset 通过 Plane V2 Assets API 上传文件
+func UploadFeedbackAsset(fileName string, contentType string, fileSize int64, fileData []byte) (*repositories.PlaneWebAssetUploadResult, error) {
+	return repositories.PlaneWebUploadCurrentProjectAsset(fileName, contentType, fileData, repositories.PlaneWebEntityIssueDescription)
+}
+
+// ProxyFeedbackAsset 代理获取附件文件(后端从 Plane 下载后返回给前端)
+func ProxyFeedbackAsset(assetId string) (string, []byte, error) {
+	slug := repositories.PlaneWebGetWorkspaceSlug()
+	projectId := repositories.PlaneWebGetProjectId()
+
+	// 获取预签名 URL
+	presignedURL, err := repositories.PlaneWebGetProjectAssetURL(slug, projectId, assetId)
+	if err != nil {
+		return "", nil, fmt.Errorf("获取附件URL失败: %w", err)
+	}
+
+	// 下载文件
+	resp, err := httpGet(presignedURL)
+	if err != nil {
+		return "", nil, fmt.Errorf("下载附件失败: %w", err)
+	}
+
+	return resp.ContentType, resp.Body, nil
+}
+
+// DeleteFeedbackAsset 删除已上传的附件 (V2)
+func DeleteFeedbackAsset(assetId string) error {
+	return repositories.PlaneWebDeleteProjectAsset(
+		repositories.PlaneWebGetWorkspaceSlug(),
+		repositories.PlaneWebGetProjectId(),
+		assetId,
+	)
+}
+
+// httpGetResponse HTTP GET 响应
+type httpGetResponse struct {
+	ContentType string
+	Body        []byte
+}
+
+// httpGet 简单 HTTP GET 请求(用于下载预签名 URL 的文件)
+func httpGet(url string) (*httpGetResponse, error) {
+	client := &http.Client{Timeout: 60 * time.Second}
+	resp, err := client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return nil, fmt.Errorf("HTTP GET 失败: status=%d", resp.StatusCode)
+	}
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	contentType := resp.Header.Get("Content-Type")
+	if contentType == "" {
+		contentType = "application/octet-stream"
+	}
+
+	return &httpGetResponse{
+		ContentType: contentType,
+		Body:        body,
+	}, nil
+}

+ 11 - 11
src/router/record.ts

@@ -268,17 +268,6 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
   //   component: 'dynamic-route/index',
   // },
   // {
-  //   path: 'feedback',
-  //   name: 'feedback',
-  //   icon: 'i-ph:messenger-logo',
-  //   label: '反馈组件',
-  //   meta: {
-  //     componentName: 'Feedback',
-  //     showTab: true,
-  //   },
-  //   component: 'feedback/index',
-  // },
-  // {
   //   path: 'drag-drop',
   //   name: 'dragDrop',
   //   icon: 'i-pixelarticons:drag-and-drop',
@@ -505,6 +494,17 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
       },
     ],
   },
+  {
+    path: 'feedback',
+    name: 'feedback',
+    icon: 'i-ph:messenger-logo',
+    label: '问题&需求',
+    meta: {
+      componentName: 'Feedback',
+      showTab: true,
+    },
+    component: 'feedback/index',
+  },
   {
     path: 'about',
     key: 'about',

+ 73 - 0
src/service/feedback/index.ts

@@ -0,0 +1,73 @@
+// API List
+// /feedback/submit       - POST   提交问题反馈或需求
+// /feedback/history      - GET    查询提交历史
+// /feedback/upload       - POST   上传附件到 Plane (返回 assetId)
+// /feedback/asset/:id    - DELETE 删除已上传的附件
+
+import http from '@/service/serverReq'
+import type {
+  FeedbackSubmitParams,
+  FeedbackSubmitResult,
+  FeedbackHistoryItem,
+  FeedbackDetailResult,
+  FeedbackUploadResult,
+} from './interface'
+
+const API_List = {
+  submit: '/feedback/submit',
+  history: '/feedback/history',
+  detail: '/feedback/detail',
+  upload: '/feedback/upload',
+  asset: '/feedback/asset',
+}
+
+// POST 提交反馈
+export const submitFeedback = async (
+  params: FeedbackSubmitParams,
+): Promise<FeedbackSubmitResult> => {
+  const res = await http.post<FeedbackSubmitResult>(API_List.submit, params)
+  return res
+}
+
+// GET 查询提交历史
+export const getFeedbackHistory = async (): Promise<FeedbackHistoryItem[]> => {
+  const res = await http.get<FeedbackHistoryItem[]>(API_List.history)
+  return res
+}
+
+// GET 获取反馈详情(含描述内容)
+export const getFeedbackDetail = async (issueId: string): Promise<FeedbackDetailResult> => {
+  const res = await http.get<FeedbackDetailResult>(`${API_List.detail}/${issueId}`)
+  return res
+}
+
+// POST 上传附件(FormData),立即上传到 Plane,返回 assetId
+export const uploadFeedbackAsset = async (file: File): Promise<FeedbackUploadResult> => {
+  const formData = new FormData()
+  formData.append('file', file)
+
+  const res = await http.post<FeedbackUploadResult>(API_List.upload, formData)
+  return res
+}
+
+// DELETE 删除已上传的附件
+export const deleteFeedbackAsset = async (assetId: string): Promise<void> => {
+  await http.delete(`${API_List.asset}/${assetId}`)
+}
+
+// GET 获取附件文件 Blob(通过后端代理,带 token)
+export const getFeedbackAssetBlob = async (assetId: string): Promise<Blob | undefined> => {
+  return http.downloadToBlob(`/feedback/asset/view/${assetId}`)
+}
+
+// 批量上传文件,返回上传结果列表(逐个上传)
+export const uploadFeedbackAssets = async (
+  files: File[],
+): Promise<FeedbackUploadResult[]> => {
+  const results: FeedbackUploadResult[] = []
+  for (const file of files) {
+    const result = await uploadFeedbackAsset(file)
+    results.push(result)
+  }
+  return results
+}

+ 68 - 0
src/service/feedback/interface.ts

@@ -0,0 +1,68 @@
+// 反馈提交类型
+export type FeedbackType = 'bug' | 'feature'
+
+// Bug 分类
+export type BugCategory = 'bug' | 'ui' | 'performance' | 'other'
+
+// Feature 分类
+export type FeatureCategory = 'new' | 'improvement' | 'experience'
+
+// 优先级
+export type FeedbackPriority = 'urgent' | 'high' | 'medium' | 'low' | 'none'
+
+// 提交请求参数
+export interface FeedbackSubmitParams {
+  title: string
+  type: FeedbackType
+  priority: FeedbackPriority
+  category: string
+  description: string
+  steps?: string       // Bug 专用
+  expected?: string    // Bug 专用
+  actual?: string      // Bug 专用
+  scenario?: string    // Feature 专用
+  relatedPage?: string
+  assetIds?: string[]  // 已上传附件的 Asset ID 列表
+}
+
+// Plane Intake 响应
+export interface FeedbackSubmitResult {
+  id: string
+  issue: string
+  status: number
+  source: string
+  created_at: string
+}
+
+// 上传结果
+export interface FeedbackUploadResult {
+  assetId: string
+  assetUrl: string
+}
+
+// 提交历史条目
+export interface FeedbackHistoryItem {
+  id: string
+  title: string
+  type: string       // bug / feature
+  status: number     // -2 Pending, -1 Rejected, 0 Snoozed, 1 Accepted, 2 Duplicate
+  priority: string
+  createdAt: string
+  createdBy: string  // 提交人用户名
+}
+
+// 反馈详情
+export interface FeedbackDetailResult {
+  id: string
+  title: string
+  descriptionHtml: string
+  priority: string
+  status: number
+  createdAt: string
+  attachments: FeedbackAttachment[]
+}
+
+// 附件信息
+export interface FeedbackAttachment {
+  assetId: string
+}

+ 14 - 5
src/service/fetchClient.ts

@@ -117,15 +117,24 @@ export class FetchClient {
     // 构造请求配置
     const config: RequestInit = {
       method,
-      headers: {
-        'Content-Type': 'application/json',
-        ...headers,
-      },
+      // headers: {
+      //   'Content-Type': 'application/json',
+      //   ...headers,
+      // },
       signal: mergedSignal,
     }
 
     if (data) {
-      config.body = JSON.stringify(data)
+      if (data instanceof FormData) {
+        config.body = data
+        config.headers = headers
+      } else {
+        config.body = JSON.stringify(data)
+        config.headers = {
+          'Content-Type': 'application/json',
+          ...headers,
+        }
+      }
     }
 
     // 请求拦截器

+ 292 - 0
src/views/feedback/component/BugReportForm.vue

@@ -0,0 +1,292 @@
+<script setup lang="ts">
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NRadioGroup,
+  NRadio,
+  NButton,
+  NUpload,
+  useMessage,
+} from 'naive-ui'
+import { ref, reactive } from 'vue'
+
+import { submitFeedback, uploadFeedbackAssets, deleteFeedbackAsset } from '@/service/feedback'
+import type { FeedbackPriority, BugCategory, FeedbackUploadResult } from '@/service/feedback/interface'
+import type { UploadFileInfo } from 'naive-ui'
+
+defineOptions({
+  name: 'BugReportForm',
+})
+
+const emit = defineEmits<{
+  submitted: []
+}>()
+
+const message = useMessage()
+const loading = ref(false)
+
+const formData = reactive({
+  title: '',
+  category: 'bug' as BugCategory,
+  priority: 'medium' as FeedbackPriority,
+  steps: '',
+  expected: '',
+  actual: '',
+  description: '',
+  relatedPage: '',
+})
+
+// 暂存的本地文件列表(提交时才上传)
+interface PendingFile {
+  id: string        // 用于列表 key
+  file: File        // 本地 File 对象
+  name: string      // 文件名
+}
+const pendingFiles = ref<PendingFile[]>([])
+
+const categoryOptions = [
+  { label: 'Bug(功能异常)', value: 'bug' },
+  { label: 'UI 问题', value: 'ui' },
+  { label: '性能问题', value: 'performance' },
+  { label: '其他', value: 'other' },
+]
+
+const rules = {
+  title: { required: true, message: '请输入标题', trigger: 'blur' },
+  steps: { required: true, message: '请描述复现步骤', trigger: 'blur' },
+}
+
+const formRef = ref()
+
+// 选择文件 → 暂存到本地,不立即上传
+function handleFileSelect({ file }: { file: UploadFileInfo }) {
+  if (!file.file) return
+
+  // 检查文件大小
+  if (file.file.size > 10 * 1024 * 1024) {
+    message.warning('文件大小不能超过 10MB')
+    return
+  }
+
+  pendingFiles.value.push({
+    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+    file: file.file,
+    name: file.name || file.file.name,
+  })
+}
+
+// 粘贴截图 → 暂存到本地
+function handlePaste(event: ClipboardEvent) {
+  const items = event.clipboardData?.items
+  if (!items) return
+
+  for (const item of items) {
+    if (item.type.startsWith('image/')) {
+      const file = item.getAsFile()
+      if (file) {
+        event.preventDefault()
+        const fileName = `screenshot_${Date.now()}.png`
+        const renamedFile = new File([file], fileName, { type: file.type })
+        pendingFiles.value.push({
+          id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+          file: renamedFile,
+          name: fileName,
+        })
+        message.info('截图已添加')
+      }
+      break
+    }
+  }
+}
+
+// 移除暂存文件(未上传,直接删除引用)
+function removePendingFile(index: number) {
+  pendingFiles.value.splice(index, 1)
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value?.validate()
+  } catch {
+    return
+  }
+
+  loading.value = true
+  let uploadedResults: FeedbackUploadResult[] = []
+
+  try {
+    // 1. 先上传所有暂存文件
+    if (pendingFiles.value.length > 0) {
+      const files = pendingFiles.value.map((p) => p.file)
+      uploadedResults = await uploadFeedbackAssets(files)
+    }
+
+    // 2. 提交反馈(附带已上传的 assetId 列表)
+    await submitFeedback({
+      title: formData.title,
+      type: 'bug',
+      priority: formData.priority,
+      category: formData.category,
+      description: formData.description,
+      steps: formData.steps || undefined,
+      expected: formData.expected || undefined,
+      actual: formData.actual || undefined,
+      relatedPage: formData.relatedPage || undefined,
+      assetIds: uploadedResults.length > 0
+        ? uploadedResults.map((r) => r.assetId)
+        : undefined,
+    })
+
+    message.success('问题已提交,感谢反馈!')
+    resetForm()
+    emit('submitted')
+  } catch (err: any) {
+    // 提交失败时,清理已上传的文件
+    if (uploadedResults.length > 0) {
+      for (const result of uploadedResults) {
+        try {
+          await deleteFeedbackAsset(result.assetId)
+        } catch {
+          // 删除失败忽略
+        }
+      }
+    }
+    message.error(err?.message || '提交失败,请稍后重试')
+  } finally {
+    loading.value = false
+  }
+}
+
+function resetForm() {
+  formData.title = ''
+  formData.category = 'bug'
+  formData.priority = 'medium'
+  formData.steps = ''
+  formData.expected = ''
+  formData.actual = ''
+  formData.description = ''
+  formData.relatedPage = ''
+  pendingFiles.value = []
+}
+</script>
+
+<template>
+  <NForm ref="formRef" :model="formData" :rules="rules" label-placement="top" label-width="auto">
+    <NFormItem label="标题" path="title">
+      <NInput v-model:value="formData.title" placeholder="简要描述问题" maxlength="100" show-count />
+    </NFormItem>
+
+    <div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
+      <NFormItem label="问题分类" path="category">
+        <NSelect v-model:value="formData.category" :options="categoryOptions" />
+      </NFormItem>
+
+      <NFormItem label="优先级" path="priority">
+        <NRadioGroup v-model:value="formData.priority">
+          <NRadio value="urgent">紧急</NRadio>
+          <NRadio value="high">高</NRadio>
+          <NRadio value="medium">中</NRadio>
+          <NRadio value="low">低</NRadio>
+        </NRadioGroup>
+      </NFormItem>
+    </div>
+
+    <NFormItem label="复现步骤" path="steps">
+      <NInput
+        v-model:value="formData.steps"
+        type="textarea"
+        placeholder="1. 打开某页面&#10;2. 点击某按钮&#10;3. 观察到..."
+        :rows="4"
+      />
+    </NFormItem>
+
+    <div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
+      <NFormItem label="期望结果">
+        <NInput
+          v-model:value="formData.expected"
+          type="textarea"
+          placeholder="描述正确的行为应该是什么"
+          :rows="3"
+        />
+      </NFormItem>
+
+      <NFormItem label="实际结果">
+        <NInput
+          v-model:value="formData.actual"
+          type="textarea"
+          placeholder="描述实际发生了什么"
+          :rows="3"
+        />
+      </NFormItem>
+    </div>
+
+    <NFormItem label="相关页面">
+      <NInput v-model:value="formData.relatedPage" placeholder="可选,如:站点监控 / 批量下载" />
+    </NFormItem>
+
+    <NFormItem label="补充说明">
+      <NInput
+        v-model:value="formData.description"
+        type="textarea"
+        placeholder="其他补充信息(可选)"
+        :rows="3"
+      />
+    </NFormItem>
+
+    <!-- 截图/附件 -->
+    <NFormItem label="截图 / 附件">
+      <div class="w-full" @paste="handlePaste">
+        <NUpload
+          :show-file-list="false"
+          :custom-request="({ file }) => handleFileSelect({ file: file as UploadFileInfo })"
+          accept="image/*,.pdf,.log,.txt,.xlsx,.xls,.csv,.zip"
+          :multiple="true"
+        >
+          <NButton dashed class="w-full">
+            <template #icon>
+              <span class="i-ph:upload-simple" />
+            </template>
+            点击选择文件或粘贴截图(Ctrl+V)
+          </NButton>
+        </NUpload>
+
+        <!-- 暂存文件列表 -->
+        <div v-if="pendingFiles.length > 0" class="mt-3 space-y-2">
+          <div
+            v-for="(pf, index) in pendingFiles"
+            :key="pf.id"
+            class="flex items-center justify-between rounded bg-neutral-50 px-3 py-2 dark:bg-neutral-800"
+          >
+            <span class="text-sm text-neutral-600 dark:text-neutral-400">
+              📎 {{ pf.name }}
+              <span class="ml-1 text-xs text-neutral-400">
+                ({{ (pf.file.size / 1024).toFixed(1) }}KB)
+              </span>
+            </span>
+            <NButton size="tiny" quaternary type="error" @click="removePendingFile(index)">
+              <template #icon>
+                <span class="i-ph:x" />
+              </template>
+            </NButton>
+          </div>
+        </div>
+
+        <p class="mt-1 text-xs text-neutral-400">
+          支持图片、PDF、日志文件等,单文件最大 10MB。文件将在提交时上传。
+        </p>
+      </div>
+    </NFormItem>
+
+    <div class="flex justify-end gap-3">
+      <NButton @click="resetForm">重置</NButton>
+      <NButton type="primary" :loading="loading" @click="handleSubmit">
+        <template #icon>
+          <span class="i-ph:paper-plane-tilt" />
+        </template>
+        提交反馈
+      </NButton>
+    </div>
+  </NForm>
+</template>

+ 263 - 0
src/views/feedback/component/FeatureForm.vue

@@ -0,0 +1,263 @@
+<script setup lang="ts">
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NRadioGroup,
+  NRadio,
+  NButton,
+  NUpload,
+  useMessage,
+} from 'naive-ui'
+import { ref, reactive } from 'vue'
+
+import { submitFeedback, uploadFeedbackAssets, deleteFeedbackAsset } from '@/service/feedback'
+import type { FeedbackPriority, FeatureCategory, FeedbackUploadResult } from '@/service/feedback/interface'
+import type { UploadFileInfo } from 'naive-ui'
+
+defineOptions({
+  name: 'FeatureForm',
+})
+
+const emit = defineEmits<{
+  submitted: []
+}>()
+
+const message = useMessage()
+const loading = ref(false)
+
+const formData = reactive({
+  title: '',
+  category: 'new' as FeatureCategory,
+  priority: 'medium' as FeedbackPriority,
+  description: '',
+  scenario: '',
+  relatedPage: '',
+})
+
+// 暂存的本地文件列表(提交时才上传)
+interface PendingFile {
+  id: string
+  file: File
+  name: string
+}
+const pendingFiles = ref<PendingFile[]>([])
+
+const categoryOptions = [
+  { label: '新功能', value: 'new' },
+  { label: '功能改进', value: 'improvement' },
+  { label: '体验优化', value: 'experience' },
+]
+
+const rules = {
+  title: { required: true, message: '请输入标题', trigger: 'blur' },
+  description: { required: true, message: '请描述需求', trigger: 'blur' },
+}
+
+const formRef = ref()
+
+// 选择文件 → 暂存
+function handleFileSelect({ file }: { file: UploadFileInfo }) {
+  if (!file.file) return
+
+  if (file.file.size > 10 * 1024 * 1024) {
+    message.warning('文件大小不能超过 10MB')
+    return
+  }
+
+  pendingFiles.value.push({
+    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+    file: file.file,
+    name: file.name || file.file.name,
+  })
+}
+
+// 粘贴截图 → 暂存
+function handlePaste(event: ClipboardEvent) {
+  const items = event.clipboardData?.items
+  if (!items) return
+
+  for (const item of items) {
+    if (item.type.startsWith('image/')) {
+      const file = item.getAsFile()
+      if (file) {
+        event.preventDefault()
+        const fileName = `screenshot_${Date.now()}.png`
+        const renamedFile = new File([file], fileName, { type: file.type })
+        pendingFiles.value.push({
+          id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+          file: renamedFile,
+          name: fileName,
+        })
+        message.info('截图已添加')
+      }
+      break
+    }
+  }
+}
+
+function removePendingFile(index: number) {
+  pendingFiles.value.splice(index, 1)
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value?.validate()
+  } catch {
+    return
+  }
+
+  loading.value = true
+  let uploadedResults: FeedbackUploadResult[] = []
+
+  try {
+    // 1. 提交时才上传文件
+    if (pendingFiles.value.length > 0) {
+      const files = pendingFiles.value.map((p) => p.file)
+      uploadedResults = await uploadFeedbackAssets(files)
+    }
+
+    // 2. 提交需求(附带已上传的 assetId 列表)
+    await submitFeedback({
+      title: formData.title,
+      type: 'feature',
+      priority: formData.priority,
+      category: formData.category,
+      description: formData.description,
+      scenario: formData.scenario || undefined,
+      relatedPage: formData.relatedPage || undefined,
+      assetIds: uploadedResults.length > 0
+        ? uploadedResults.map((r) => r.assetId)
+        : undefined,
+    })
+
+    message.success('需求已提交,感谢建议!')
+    resetForm()
+    emit('submitted')
+  } catch (err: any) {
+    // 提交失败 → 清理已上传的文件
+    if (uploadedResults.length > 0) {
+      for (const result of uploadedResults) {
+        try {
+          await deleteFeedbackAsset(result.assetId)
+        } catch {
+          // 忽略
+        }
+      }
+    }
+    message.error(err?.message || '提交失败,请稍后重试')
+  } finally {
+    loading.value = false
+  }
+}
+
+function resetForm() {
+  formData.title = ''
+  formData.category = 'new'
+  formData.priority = 'medium'
+  formData.description = ''
+  formData.scenario = ''
+  formData.relatedPage = ''
+  pendingFiles.value = []
+}
+</script>
+
+<template>
+  <NForm ref="formRef" :model="formData" :rules="rules" label-placement="top" label-width="auto">
+    <NFormItem label="标题" path="title">
+      <NInput v-model:value="formData.title" placeholder="简要描述需求" maxlength="100" show-count />
+    </NFormItem>
+
+    <div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
+      <NFormItem label="需求类型" path="category">
+        <NSelect v-model:value="formData.category" :options="categoryOptions" />
+      </NFormItem>
+
+      <NFormItem label="优先级" path="priority">
+        <NRadioGroup v-model:value="formData.priority">
+          <NRadio value="urgent">紧急</NRadio>
+          <NRadio value="high">高</NRadio>
+          <NRadio value="medium">中</NRadio>
+          <NRadio value="low">低</NRadio>
+        </NRadioGroup>
+      </NFormItem>
+    </div>
+
+    <NFormItem label="需求描述" path="description">
+      <NInput
+        v-model:value="formData.description"
+        type="textarea"
+        placeholder="详细描述你期望的功能或改进"
+        :rows="5"
+      />
+    </NFormItem>
+
+    <NFormItem label="使用场景">
+      <NInput
+        v-model:value="formData.scenario"
+        type="textarea"
+        placeholder="描述什么情况下需要这个功能(可选)"
+        :rows="3"
+      />
+    </NFormItem>
+
+    <NFormItem label="相关页面">
+      <NInput v-model:value="formData.relatedPage" placeholder="可选,如:站点监控 / 批量下载" />
+    </NFormItem>
+
+    <!-- 截图/附件 -->
+    <NFormItem label="截图 / 附件">
+      <div class="w-full" @paste="handlePaste">
+        <NUpload
+          :show-file-list="false"
+          :custom-request="({ file }) => handleFileSelect({ file: file as UploadFileInfo })"
+          accept="image/*,.pdf,.log,.txt,.xlsx,.xls,.csv,.zip"
+          :multiple="true"
+        >
+          <NButton dashed class="w-full">
+            <template #icon>
+              <span class="i-ph:upload-simple" />
+            </template>
+            点击选择文件或粘贴截图(Ctrl+V)
+          </NButton>
+        </NUpload>
+
+        <!-- 暂存文件列表 -->
+        <div v-if="pendingFiles.length > 0" class="mt-3 space-y-2">
+          <div
+            v-for="(pf, index) in pendingFiles"
+            :key="pf.id"
+            class="flex items-center justify-between rounded bg-neutral-50 px-3 py-2 dark:bg-neutral-800"
+          >
+            <span class="text-sm text-neutral-600 dark:text-neutral-400">
+              📎 {{ pf.name }}
+              <span class="ml-1 text-xs text-neutral-400">
+                ({{ (pf.file.size / 1024).toFixed(1) }}KB)
+              </span>
+            </span>
+            <NButton size="tiny" quaternary type="error" @click="removePendingFile(index)">
+              <template #icon>
+                <span class="i-ph:x" />
+              </template>
+            </NButton>
+          </div>
+        </div>
+
+        <p class="mt-1 text-xs text-neutral-400">
+          支持图片、PDF、日志文件等,单文件最大 10MB。文件将在提交时上传。
+        </p>
+      </div>
+    </NFormItem>
+
+    <div class="flex justify-end gap-3">
+      <NButton @click="resetForm">重置</NButton>
+      <NButton type="primary" :loading="loading" @click="handleSubmit">
+        <template #icon>
+          <span class="i-ph:paper-plane-tilt" />
+        </template>
+        提交需求
+      </NButton>
+    </div>
+  </NForm>
+</template>

+ 284 - 0
src/views/feedback/component/SubmitHistory.vue

@@ -0,0 +1,284 @@
+<script setup lang="ts">
+import {
+  NTimeline,
+  NTimelineItem,
+  NEmpty,
+  NTag,
+  NSpin,
+  NButton,
+  NSwitch,
+  NSelect,
+  NModal,
+  NCard,
+} from 'naive-ui'
+import { ref, computed, onMounted } from 'vue'
+
+import { getFeedbackHistory, getFeedbackDetail, getFeedbackAssetBlob } from '@/service/feedback'
+import type { FeedbackHistoryItem, FeedbackDetailResult } from '@/service/feedback/interface'
+import { useUserStore } from '@/stores/user'
+
+defineOptions({
+  name: 'SubmitHistory',
+})
+
+const userStore = useUserStore()
+const loading = ref(false)
+const historyList = ref<FeedbackHistoryItem[]>([])
+const errorMsg = ref('')
+const onlyMine = ref(false)
+const statusFilter = ref<number[]>([-2, 1]) // 默认显示待审核和已采纳
+
+const statusOptions = [
+  { label: '待审核', value: -2 },
+  { label: '已拒绝', value: -1 },
+  { label: '已暂缓', value: 0 },
+  { label: '已采纳', value: 1 },
+  { label: '重复', value: 2 },
+]
+
+// 详情预览
+const showDetail = ref(false)
+const detailLoading = ref(false)
+const detailData = ref<FeedbackDetailResult | null>(null)
+// 附件 blob URL 映射:assetId -> objectURL | 'loading' | 'error'
+const assetBlobUrls = ref<Record<string, string>>({})
+
+// 加载附件图片(通过框架 HTTP 请求带 token)
+async function loadAssetBlob(assetId: string) {
+  assetBlobUrls.value[assetId] = 'loading'
+  try {
+    const blob = await getFeedbackAssetBlob(assetId)
+    if (blob) {
+      assetBlobUrls.value[assetId] = URL.createObjectURL(blob)
+    } else {
+      assetBlobUrls.value[assetId] = 'error'
+    }
+  } catch {
+    assetBlobUrls.value[assetId] = 'error'
+  }
+}
+
+const sortedList = computed(() => {
+  let list = [...historyList.value]
+
+  // 过滤:只看自己的提交
+  if (onlyMine.value) {
+    const myName = userStore.getUserInfo()?.name || ''
+    if (myName) {
+      list = list.filter((item) => item.createdBy === myName)
+    }
+  }
+
+  // 过滤:按状态(多选)
+  if (statusFilter.value.length > 0) {
+    list = list.filter((item) => statusFilter.value.includes(item.status))
+  }
+
+  return list.sort(
+    (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
+  )
+})
+
+async function fetchHistory() {
+  loading.value = true
+  errorMsg.value = ''
+  try {
+    historyList.value = await getFeedbackHistory()
+  } catch (err: any) {
+    errorMsg.value = err?.message || '加载失败'
+  } finally {
+    loading.value = false
+  }
+}
+
+async function openDetail(issueId: string) {
+  showDetail.value = true
+  detailLoading.value = true
+  detailData.value = null
+  // 释放旧的 blob URLs
+  Object.values(assetBlobUrls.value).forEach((url) => {
+    if (url && url !== 'loading' && url !== 'error') URL.revokeObjectURL(url)
+  })
+  assetBlobUrls.value = {}
+  try {
+    detailData.value = await getFeedbackDetail(issueId)
+    // 并行加载所有附件
+    if (detailData.value?.attachments?.length) {
+      detailData.value.attachments.forEach((att) => loadAssetBlob(att.assetId))
+    }
+  } catch {
+    detailData.value = null
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+onMounted(fetchHistory)
+
+defineExpose({ fetchHistory })
+
+function getStatusTag(status: number) {
+  const map: Record<number, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }> = {
+    [-2]: { label: '待审核', type: 'default' },
+    [-1]: { label: '已拒绝', type: 'error' },
+    [0]: { label: '已暂缓', type: 'warning' },
+    [1]: { label: '已采纳', type: 'success' },
+    [2]: { label: '重复', type: 'info' },
+  }
+  return map[status] || { label: `未知(${status})`, type: 'default' as const }
+}
+
+function getPriorityTag(priority: string) {
+  const map: Record<string, { label: string; type: 'error' | 'warning' | 'info' | 'default' }> = {
+    urgent: { label: '紧急', type: 'error' },
+    high: { label: '高', type: 'warning' },
+    medium: { label: '中', type: 'info' },
+    low: { label: '低', type: 'default' },
+    none: { label: '-', type: 'default' },
+  }
+  return map[priority] || { label: priority, type: 'default' as const }
+}
+
+function getTypeTag(type: string) {
+  if (type === 'bug') return { label: 'Bug', type: 'error' as const }
+  if (type === 'feature') return { label: 'Feature', type: 'info' as const }
+  return { label: '其他', type: 'default' as const }
+}
+
+function formatTime(time: string) {
+  if (!time) return ''
+  const d = new Date(time)
+  return d.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+  })
+}
+</script>
+
+<template>
+  <div>
+    <div class="mb-3 flex flex-wrap items-center justify-between gap-2">
+      <div class="flex flex-wrap items-center gap-3">
+        <span class="text-sm text-neutral-500">
+          共 {{ sortedList.length }} 条
+        </span>
+        <label class="flex cursor-pointer items-center gap-2 text-sm text-neutral-500">
+          <NSwitch v-model:value="onlyMine" size="small" />
+          只看我的
+        </label>
+        <NSelect
+          v-model:value="statusFilter"
+          :options="statusOptions"
+          multiple
+          size="small"
+          class="min-w-40"
+          placeholder="按状态过滤"
+          clearable
+        />
+      </div>
+      <NButton size="small" :loading="loading" @click="fetchHistory">
+        <template #icon>
+          <span class="i-ph:arrows-clockwise" />
+        </template>
+        刷新
+      </NButton>
+    </div>
+
+    <NSpin :show="loading">
+      <NEmpty v-if="!loading && sortedList.length === 0" :description="errorMsg || '暂无提交记录'" />
+      <NTimeline v-else>
+        <NTimelineItem v-for="item in sortedList" :key="item.id" :time="formatTime(item.createdAt)">
+          <template #default>
+            <div class="flex flex-wrap items-center gap-2">
+              <NTag :type="getTypeTag(item.type).type" size="small" round>
+                {{ getTypeTag(item.type).label }}
+              </NTag>
+              <NTag :type="getStatusTag(item.status).type" size="small" :bordered="false">
+                {{ getStatusTag(item.status).label }}
+              </NTag>
+              <NTag :type="getPriorityTag(item.priority).type" size="small" :bordered="false">
+                {{ getPriorityTag(item.priority).label }}
+              </NTag>
+              <span
+                class="cursor-pointer text-sm hover:text-blue-500 hover:underline"
+                @click="openDetail(item.id)"
+              >
+                {{ item.title }}
+              </span>
+              <span v-if="item.createdBy" class="text-xs text-neutral-400">
+                — {{ item.createdBy }}
+              </span>
+            </div>
+          </template>
+        </NTimelineItem>
+      </NTimeline>
+    </NSpin>
+
+    <!-- 详情预览弹窗 -->
+    <NModal v-model:show="showDetail" preset="card" class="max-w-2xl" title="反馈详情">
+      <NSpin :show="detailLoading">
+        <div v-if="detailData" class="space-y-3">
+          <div class="flex items-center gap-2">
+            <NTag :type="getStatusTag(detailData.status).type" size="small">
+              {{ getStatusTag(detailData.status).label }}
+            </NTag>
+            <NTag :type="getPriorityTag(detailData.priority).type" size="small" :bordered="false">
+              {{ getPriorityTag(detailData.priority).label }}
+            </NTag>
+            <span class="text-xs text-neutral-400">{{ formatTime(detailData.createdAt) }}</span>
+          </div>
+          <h3 class="text-base font-medium">{{ detailData.title }}</h3>
+          <div
+            class="prose prose-sm max-w-none dark:prose-invert rounded border border-neutral-200 p-4 dark:border-neutral-700"
+            v-html="detailData.descriptionHtml"
+          />
+          <!-- 附件展示 -->
+          <div v-if="detailData.attachments && detailData.attachments.length > 0">
+            <h4 class="mb-2 text-sm font-medium text-neutral-600 dark:text-neutral-400">
+              📎 附件({{ detailData.attachments.length }})
+            </h4>
+            <div class="grid grid-cols-2 gap-3 md:grid-cols-3">
+              <div
+                v-for="att in detailData.attachments"
+                :key="att.assetId"
+                class="group relative h-32 overflow-hidden rounded border border-neutral-200 dark:border-neutral-700"
+              >
+                <!-- Loading 状态 -->
+                <div
+                  v-if="!assetBlobUrls[att.assetId] || assetBlobUrls[att.assetId] === 'loading'"
+                  class="flex h-full w-full items-center justify-center bg-neutral-100 dark:bg-neutral-800"
+                >
+                  <NSpin size="small" />
+                </div>
+                <!-- 加载失败 -->
+                <div
+                  v-else-if="assetBlobUrls[att.assetId] === 'error'"
+                  class="flex h-full w-full items-center justify-center bg-neutral-100 text-xs text-neutral-400 dark:bg-neutral-800"
+                >
+                  加载失败
+                </div>
+                <!-- 图片展示 -->
+                <a
+                  v-else
+                  :href="assetBlobUrls[att.assetId]"
+                  target="_blank"
+                  class="block h-full"
+                >
+                  <img
+                    :src="assetBlobUrls[att.assetId]"
+                    :alt="att.assetId"
+                    class="h-full w-full object-cover transition group-hover:scale-105"
+                  />
+                </a>
+              </div>
+            </div>
+          </div>
+        </div>
+        <NEmpty v-else-if="!detailLoading" description="加载失败" />
+      </NSpin>
+    </NModal>
+  </div>
+</template>

+ 29 - 246
src/views/feedback/index.vue

@@ -1,263 +1,46 @@
 <script setup lang="ts">
-import { NAlert, NCard, useMessage, NButton, useModal, NModal, useNotification } from 'naive-ui'
-import { reactive } from 'vue'
+import { NCard, NTabs, NTabPane } from 'naive-ui'
+import { ref } from 'vue'
 
-import type { ModalProps } from 'naive-ui'
+import BugReportForm from './component/BugReportForm.vue'
+import FeatureForm from './component/FeatureForm.vue'
+import SubmitHistory from './component/SubmitHistory.vue'
 
 defineOptions({
   name: 'Feedback',
 })
 
-const APP_NAME = import.meta.env.VITE_GLOB_APP_NAME
+const activeTab = ref('bug')
+const historyRef = ref<InstanceType<typeof SubmitHistory>>()
 
-const message = useMessage()
-
-const modal = useModal()
-
-import { useComponentModifier } from '@/composable/comp/useComponentModifier'
-
-import { useMessageDiscrete, useModalDiscrete, useNotificationDiscrete } from './discreteApi'
-
-const { getModalModifier } = useComponentModifier()
-
-const notification = useNotification()
-
-const showModal = reactive({
-  modal1: false,
-  modal2: false,
-  modal3: false,
-  modal4: false,
-})
-
-const changeMessageState = () => {
-  let counter = 5
-
-  let timer: ReturnType<typeof setInterval> | null = null
-
-  const messageInstance = message.create('5秒后 根据状态更换图标', {
-    type: 'info',
-    duration: 0,
-    closable: true,
-  })
-
-  timer = setInterval(() => {
-    counter -= 1
-    if (counter <= 0) {
-      if (timer) {
-        clearInterval(timer)
-      }
-      messageInstance.type = 'error'
-      messageInstance.content = '切换图标了'
-      return
-    }
-    messageInstance.content = `${counter}秒后 根据状态更换图标`
-  }, 1000)
-}
-
-const createDialogApi = (type: ModalProps['type'] = 'success') => {
-  const title = {
-    success: '成功的Dialog',
-    warning: '警告的Dialog',
-    error: '失败的Dialog',
-    info: '信息的Dialog',
-    default: '默认的Dialog',
-  } as const
-
-  modal.create({
-    ...getModalModifier(),
-    title: `命令式${title[type]}`,
-    content: `命令式${title[type]}`,
-    preset: 'dialog',
-    type,
-    positiveText: '确认',
-    negativeText: '算了',
-  })
+function onSubmitted() {
+  // 提交成功后刷新历史列表
+  historyRef.value?.fetchHistory()
 }
 </script>
+
 <template>
-  <div class="flex flex-col gap-y-2 p-4">
-    <NAlert type="info" closable>
-      {{ APP_NAME }} 修饰了一些反馈组件的默认设计,这似乎还不错
-    </NAlert>
-    <NAlert type="success" closable>
-      {{ APP_NAME }} 修饰了一些反馈组件的默认设计,这似乎还不错
-    </NAlert>
-    <NAlert type="error" closable>
-      {{ APP_NAME }} 修饰了一些反馈组件的默认设计,这似乎还不错
-    </NAlert>
-    <NAlert type="warning" closable>
-      {{ APP_NAME }} 修饰了一些反馈组件的默认设计,这似乎还不错
-    </NAlert>
-    <NCard title="Message 消息">
-      <div class="flex flex-wrap gap-4">
-        <NButton type="success" @click="message.success('成功Message', { closable: true })">
-          成功Message
-        </NButton>
-        <NButton type="warning" secondary @click="message.warning('警告Message', { closable: true })">
-          警告Message
-        </NButton>
-        <NButton type="error" tertiary @click="message.error('失败Message', { closable: true })">
-          失败Message
-        </NButton>
-        <NButton type="info" dashed @click="message.info('信息Message', { closable: true })">
-          信息Message
-        </NButton>
-        <NButton type="primary" ghost @click="message.loading('加载Message', { closable: true })">
-          加载Message
-        </NButton>
-        <NButton @click="changeMessageState"> 根据状态切换图标 </NButton>
-        <NCard size="small" title="Setup 外使用" :bordered="false">
-          <div class="flex flex-wrap gap-4">
-            <NButton type="success" @click="useMessageDiscrete('success')" size="small">
-              成功Message
-            </NButton>
-            <NButton type="warning" secondary @click="useMessageDiscrete('warning')" size="small">
-              警告Message
-            </NButton>
-            <NButton type="error" tertiary @click="useMessageDiscrete('error')" size="small">
-              Message失败
-            </NButton>
-            <NButton type="info" dashed @click="useMessageDiscrete('info')" size="small">
-              信息Message
-            </NButton>
-            <NButton type="primary" ghost @click="useMessageDiscrete('loading')" size="small">
-              加载Message
-            </NButton>
+  <div class="p-4">
+    <NCard>
+      <NTabs v-model:value="activeTab" type="line" animated>
+        <NTabPane name="bug" tab="🐛 问题反馈">
+          <div class="max-w-3xl pt-2">
+            <BugReportForm @submitted="onSubmitted" />
           </div>
-        </NCard>
-      </div>
-    </NCard>
-    <NCard title="Modal 模态框">
-      <div class="flex flex-wrap gap-4">
-        <NCard size="small" title="Dialog 预设" :bordered="false">
-          <div class="flex flex-wrap gap-4">
-            <NButton type="success" secondary @click="showModal.modal1 = true">
-              成功Dialog
-            </NButton>
-            <NButton type="warning" tertiary @click="showModal.modal2 = true">
-              警告Dialog
-            </NButton>
-            <NButton type="error" dashed @click="showModal.modal3 = true">
-              失败Dialog
-            </NButton>
-            <NButton type="info" ghost @click="showModal.modal4 = true">
-              信息Dialog
-            </NButton>
-          </div>
-        </NCard>
-        <NCard size="small" title="命令式 Dialog 预设" :bordered="false">
-          <div class="flex flex-wrap gap-4">
-            <NButton type="success" size="small" secondary @click="createDialogApi('success')">
-              成功Dialog
-            </NButton>
-            <NButton type="warning" size="small" tertiary @click="createDialogApi('warning')">
-              警告Dialog
-            </NButton>
-            <NButton type="error" size="small" dashed @click="createDialogApi('error')">
-              失败Dialog
-            </NButton>
-            <NButton type="info" size="small" ghost @click="createDialogApi('info')">
-              信息Dialog
-            </NButton>
+        </NTabPane>
+
+        <NTabPane name="feature" tab="💡 需求提交">
+          <div class="max-w-3xl pt-2">
+            <FeatureForm @submitted="onSubmitted" />
           </div>
-        </NCard>
-        <NCard size="small" title="Setup 外使用 Dialog 预设" :bordered="false">
-          <div class="flex flex-wrap gap-4">
-            <NButton type="success" size="small" secondary @click="useModalDiscrete('success')">
-              成功Dialog
-            </NButton>
-            <NButton type="warning" size="small" tertiary @click="useModalDiscrete('warning')">
-              警告Dialog
-            </NButton>
-            <NButton type="error" size="small" dashed @click="useModalDiscrete('error')">
-              失败Dialog
-            </NButton>
-            <NButton type="info" size="small" ghost @click="useModalDiscrete('info')">
-              信息Dialog
-            </NButton>
+        </NTabPane>
+
+        <NTabPane name="history" tab="📋 提交记录">
+          <div class="max-w-3xl pt-2">
+            <SubmitHistory ref="historyRef" />
           </div>
-        </NCard>
-      </div>
-    </NCard>
-    <NCard title="Notification 通知">
-      <div class="flex flex-wrap gap-4">
-        <NButton type="success" @click="
-          notification.success({
-            content: '说点啥呢',
-            meta: '我不到啊',
-            duration: 0,
-          })
-          ">
-          成功Notification
-        </NButton>
-        <NButton type="warning" secondary @click="
-          notification.warning({
-            content: '说点啥呢',
-            meta: '我不到啊',
-            duration: 5000,
-          })
-          ">
-          警告Notification
-        </NButton>
-        <NButton type="error" tertiary @click="
-          notification.error({
-            content: '说点啥呢',
-            meta: '我不到啊',
-            duration: 5000,
-          })
-          ">
-          错误Notification
-        </NButton>
-        <NButton type="info" ghost @click="
-          notification.info({
-            content: '说点啥呢',
-            meta: '我不到啊',
-            duration: 5000,
-          })
-          ">
-          信息Notification
-        </NButton>
-      </div>
-      <NCard size="small" :bordered="false" title="Setup 外使用 Notification 通知">
-        <div class="flex flex-wrap gap-4">
-          <NButton type="success" size="small" secondary @click="useNotificationDiscrete('success')">
-            成功Notification
-          </NButton>
-          <NButton type="warning" secondary size="small" @click="useNotificationDiscrete('warning')">
-            警告Notification
-          </NButton>
-          <NButton type="error" tertiary size="small" @click="useNotificationDiscrete('error')">
-            错误Notification
-          </NButton>
-          <NButton type="info" ghost size="small" @click="useNotificationDiscrete('info')">
-            信息Notification
-          </NButton>
-        </div>
-      </NCard>
+        </NTabPane>
+      </NTabs>
     </NCard>
-    <NModal v-bind="getModalModifier({
-      type: 'success',
-    })
-      " v-model:show="showModal.modal1" preset="dialog" title="确认" content="你确认?" positive-text="确认"
-      negative-text="算了">
-    </NModal>
-    <NModal v-bind="getModalModifier({
-      type: 'info',
-    })
-      " v-model:show="showModal.modal2" preset="dialog" title="确认" content="你确认?" positive-text="确认"
-      negative-text="算了">
-    </NModal>
-    <NModal v-bind="getModalModifier({
-      type: 'error',
-    })
-      " v-model:show="showModal.modal3" type="error" preset="dialog" title="确认" content="你确认?" positive-text="确认"
-      negative-text="算了">
-    </NModal>
-    <NModal v-bind="getModalModifier({
-      type: 'info',
-    })
-      " v-model:show="showModal.modal4" preset="dialog" title="确认" content="你确认?" positive-text="确认"
-      negative-text="算了">
-    </NModal>
   </div>
 </template>