Преглед изворни кода

feat:补充完整Plane v2的API

Jerry Liang пре 2 месеци
родитељ
комит
6c355ed11d

+ 3 - 2
service/cmd/mcp/main.go

@@ -4,9 +4,9 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/mark3labs/mcp-go/server"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
-	"github.com/mark3labs/mcp-go/server"
 
 	"fwh-toolbox-server/cache"
 	"fwh-toolbox-server/config"
@@ -44,7 +44,8 @@ func main() {
 	}
 	cache.Init(cfg.App.RedisAddr)
 	minioUtil.InitClient(&cfg.App)
-	repositories.InitPlane(&cfg.PlaneWebApi)
+	planeCfg := &cfg.PlaneWebApi
+	repositories.InitPlaneClient(planeCfg.Host, planeCfg.Email, planeCfg.Password)
 
 	// 初始化 MCP FileStore
 	mcpFS := filestore.NewFileStore("download")

+ 1 - 23
service/cmd/mcp/tools/data/plane_api.go

@@ -203,27 +203,5 @@ func registerPlaneApiTools(s *server.MCPServer) {
 		handlePlaneGetIntakeIssueDetail,
 	)
 
-	// === V2 Web API: Page ===
-	s.AddTool(
-		mcp.NewTool("data_plane_list_pages",
-			mcp.WithDescription("列出 Plane 页面列表(V2 Web API)"),
-		),
-		handlePlaneListPages,
-	)
-
-	s.AddTool(
-		mcp.NewTool("data_plane_get_page",
-			mcp.WithDescription("获取 Plane 页面详情(V2 Web API)"),
-			mcp.WithString("page_id", mcp.Required(), mcp.Description("页面 UUID")),
-		),
-		handlePlaneGetPage,
-	)
-
-	s.AddTool(
-		mcp.NewTool("data_plane_get_page_description",
-			mcp.WithDescription("获取 Plane 页面描述内容(V2 Web API)"),
-			mcp.WithString("page_id", mcp.Required(), mcp.Description("页面 UUID")),
-		),
-		handlePlaneGetPageDescription,
-	)
+	// === V2 Web API: Page (已迁移到 plane_v2_api.go) ===
 }

+ 1 - 24
service/cmd/mcp/tools/data/plane_api_handlers.go

@@ -240,27 +240,4 @@ func handlePlaneGetIntakeIssueDetail(ctx context.Context, req mcp.CallToolReques
 	return planeRawResult(data, apiErr, "获取Intake详情")
 }
 
-// === V2 Web API: Page Handlers ===
-
-func handlePlaneListPages(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := repositories.Plane.ListPages()
-	return planeRawResult(data, err, "列出页面")
-}
-
-func handlePlaneGetPage(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("page_id")
-	if err != nil {
-		return helper.ParamError("page_id"), nil
-	}
-	data, apiErr := repositories.Plane.GetPage(id)
-	return planeRawResult(data, apiErr, "获取页面详情")
-}
-
-func handlePlaneGetPageDescription(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("page_id")
-	if err != nil {
-		return helper.ParamError("page_id"), nil
-	}
-	data, apiErr := repositories.Plane.GetPageDescription(id)
-	return planeRawResult(data, apiErr, "获取页面描述")
-}
+// === V2 Web API: Page Handlers (已迁移到 plane_v2_api_handlers.go) ===

+ 265 - 0
service/cmd/mcp/tools/data/plane_v2_api.go

@@ -0,0 +1,265 @@
+package data
+
+import (
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+)
+
+func registerPlaneV2ApiTools(s *server.MCPServer) {
+	// ============ Workspace (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_workspaces",
+			mcp.WithDescription("列出所有工作空间(V2 Web API)"),
+		),
+		handlePlaneV2ListWorkspaces,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_workspace",
+			mcp.WithDescription("获取当前工作空间详情(V2 Web API)"),
+		),
+		handlePlaneV2GetWorkspace,
+	)
+
+	// ============ Project (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_projects",
+			mcp.WithDescription("列出工作空间下的所有项目(V2 Web API)"),
+		),
+		handlePlaneV2ListProjects,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_project",
+			mcp.WithDescription("获取项目详情(V2 Web API)"),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
+		),
+		handlePlaneV2GetProject,
+	)
+
+	// ============ Issue (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_issues",
+			mcp.WithDescription("列出项目工作项列表(V2 Web API)"),
+		),
+		handlePlaneV2ListIssues,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_issue",
+			mcp.WithDescription("获取工作项详情(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
+		),
+		handlePlaneV2GetIssue,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_sub_issues",
+			mcp.WithDescription("获取子工作项列表(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("父工作项 UUID")),
+		),
+		handlePlaneV2GetSubIssues,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_issue_activity",
+			mcp.WithDescription("获取工作项活动历史(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
+		),
+		handlePlaneV2GetIssueActivity,
+	)
+
+	// ============ Cycle (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_cycles",
+			mcp.WithDescription("列出项目周期列表(V2 Web API)"),
+		),
+		handlePlaneV2ListCycles,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_cycle",
+			mcp.WithDescription("获取周期详情(V2 Web API)"),
+			mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期 UUID")),
+		),
+		handlePlaneV2GetCycle,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_cycle_issues",
+			mcp.WithDescription("列出周期中的工作项(V2 Web API)"),
+			mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期 UUID")),
+		),
+		handlePlaneV2ListCycleIssues,
+	)
+
+	// ============ Module (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_modules",
+			mcp.WithDescription("列出项目模块列表(V2 Web API)"),
+		),
+		handlePlaneV2ListModules,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_module",
+			mcp.WithDescription("获取模块详情(V2 Web API)"),
+			mcp.WithString("module_id", mcp.Required(), mcp.Description("模块 UUID")),
+		),
+		handlePlaneV2GetModule,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_module_issues",
+			mcp.WithDescription("列出模块中的工作项(V2 Web API)"),
+			mcp.WithString("module_id", mcp.Required(), mcp.Description("模块 UUID")),
+		),
+		handlePlaneV2ListModuleIssues,
+	)
+
+	// ============ Label (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_labels",
+			mcp.WithDescription("列出项目标签列表(V2 Web API)"),
+		),
+		handlePlaneV2ListLabels,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_label",
+			mcp.WithDescription("获取标签详情(V2 Web API)"),
+			mcp.WithString("label_id", mcp.Required(), mcp.Description("标签 UUID")),
+		),
+		handlePlaneV2GetLabel,
+	)
+
+	// ============ State (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_states",
+			mcp.WithDescription("列出项目状态列表(V2 Web API)"),
+		),
+		handlePlaneV2ListStates,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_state",
+			mcp.WithDescription("获取状态详情(V2 Web API)"),
+			mcp.WithString("state_id", mcp.Required(), mcp.Description("状态 UUID")),
+		),
+		handlePlaneV2GetState,
+	)
+
+	// ============ Comment (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_comments",
+			mcp.WithDescription("列出工作项评论(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
+		),
+		handlePlaneV2ListComments,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_comment",
+			mcp.WithDescription("获取评论详情(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
+			mcp.WithString("comment_id", mcp.Required(), mcp.Description("评论 UUID")),
+		),
+		handlePlaneV2GetComment,
+	)
+
+	// ============ Link (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_issue_links",
+			mcp.WithDescription("列出工作项链接(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
+		),
+		handlePlaneV2ListIssueLinks,
+	)
+
+	// ============ Relation (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_issue_relations",
+			mcp.WithDescription("列出工作项关联关系(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
+		),
+		handlePlaneV2ListIssueRelations,
+	)
+
+	// ============ Sticky (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_stickies",
+			mcp.WithDescription("列出工作空间便签(V2 Web API)"),
+		),
+		handlePlaneV2ListStickies,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_sticky",
+			mcp.WithDescription("获取便签详情(V2 Web API)"),
+			mcp.WithString("sticky_id", mcp.Required(), mcp.Description("便签 UUID")),
+		),
+		handlePlaneV2GetSticky,
+	)
+
+	// ============ Intake (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_intake_issues",
+			mcp.WithDescription("列出收件箱工作项(V2 Web API)"),
+			mcp.WithString("status", mcp.Description("状态过滤")),
+		),
+		handlePlaneV2ListIntakeIssues,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_intake_issue",
+			mcp.WithDescription("获取收件箱工作项详情(V2 Web API)"),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
+		),
+		handlePlaneV2GetIntakeIssue,
+	)
+
+	// ============ Asset (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_project_asset_url",
+			mcp.WithDescription("获取项目资源的预签名访问 URL(V2 Web API)"),
+			mcp.WithString("asset_id", mcp.Required(), mcp.Description("资源 UUID")),
+		),
+		handlePlaneV2GetProjectAssetURL,
+	)
+
+	// ============ Page (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_pages",
+			mcp.WithDescription("列出项目页面列表(V2 Web API)"),
+		),
+		handlePlaneV2ListPages,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_page",
+			mcp.WithDescription("获取页面详情(V2 Web API)"),
+			mcp.WithString("page_id", mcp.Required(), mcp.Description("页面 UUID")),
+		),
+		handlePlaneV2GetPage,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_page_description",
+			mcp.WithDescription("获取页面描述内容(V2 Web API)"),
+			mcp.WithString("page_id", mcp.Required(), mcp.Description("页面 UUID")),
+		),
+		handlePlaneV2GetPageDescription,
+	)
+
+	// ============ Member (读) ============
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_workspace_members",
+			mcp.WithDescription("列出工作空间成员(V2 Web API)"),
+		),
+		handlePlaneV2ListWorkspaceMembers,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_workspace_member",
+			mcp.WithDescription("获取工作空间成员详情(V2 Web API)"),
+			mcp.WithString("member_id", mcp.Required(), mcp.Description("成员 UUID")),
+		),
+		handlePlaneV2GetWorkspaceMember,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_list_project_members",
+			mcp.WithDescription("列出项目成员(V2 Web API)"),
+		),
+		handlePlaneV2ListProjectMembers,
+	)
+	s.AddTool(
+		mcp.NewTool("data_plane_v2_get_project_member",
+			mcp.WithDescription("获取项目成员详情(V2 Web API)"),
+			mcp.WithString("member_id", mcp.Required(), mcp.Description("成员 UUID")),
+		),
+		handlePlaneV2GetProjectMember,
+	)
+}

+ 302 - 0
service/cmd/mcp/tools/data/plane_v2_api_handlers.go

@@ -0,0 +1,302 @@
+package data
+
+import (
+	"context"
+
+	"github.com/mark3labs/mcp-go/mcp"
+
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+	"fwh-toolbox-server/repositories"
+)
+
+// === Workspace Handlers ===
+
+func handlePlaneV2ListWorkspaces(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListWorkspaces()
+	return planeRawResult(data, err, "列出工作空间")
+}
+
+func handlePlaneV2GetWorkspace(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.GetWorkspace()
+	return planeRawResult(data, err, "获取工作空间详情")
+}
+
+// === Project Handlers ===
+
+func handlePlaneV2ListProjects(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListProjects()
+	return planeRawResult(data, err, "列出项目")
+}
+
+func handlePlaneV2GetProject(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("project_id")
+	if err != nil {
+		return helper.ParamError("project_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetProject(id)
+	return planeRawResult(data, apiErr, "获取项目详情")
+}
+
+// === Issue Handlers ===
+
+func handlePlaneV2ListIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListIssues()
+	return planeRawResult(data, err, "列出工作项")
+}
+
+func handlePlaneV2GetIssue(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetIssue(id)
+	return planeRawResult(data, apiErr, "获取工作项详情")
+}
+
+func handlePlaneV2GetSubIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetSubIssues(id)
+	return planeRawResult(data, apiErr, "获取子工作项")
+}
+
+func handlePlaneV2GetIssueActivity(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetIssueActivity(id)
+	return planeRawResult(data, apiErr, "获取工作项活动历史")
+}
+
+// === Cycle Handlers ===
+
+func handlePlaneV2ListCycles(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListCycles()
+	return planeRawResult(data, err, "列出周期")
+}
+
+func handlePlaneV2GetCycle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("cycle_id")
+	if err != nil {
+		return helper.ParamError("cycle_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetCycle(id)
+	return planeRawResult(data, apiErr, "获取周期详情")
+}
+
+func handlePlaneV2ListCycleIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("cycle_id")
+	if err != nil {
+		return helper.ParamError("cycle_id"), nil
+	}
+	data, apiErr := repositories.Plane.ListCycleIssues(id)
+	return planeRawResult(data, apiErr, "列出周期工作项")
+}
+
+// === Module Handlers ===
+
+func handlePlaneV2ListModules(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListModules()
+	return planeRawResult(data, err, "列出模块")
+}
+
+func handlePlaneV2GetModule(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("module_id")
+	if err != nil {
+		return helper.ParamError("module_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetModule(id)
+	return planeRawResult(data, apiErr, "获取模块详情")
+}
+
+func handlePlaneV2ListModuleIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("module_id")
+	if err != nil {
+		return helper.ParamError("module_id"), nil
+	}
+	data, apiErr := repositories.Plane.ListModuleIssues(id)
+	return planeRawResult(data, apiErr, "列出模块工作项")
+}
+
+// === Label Handlers ===
+
+func handlePlaneV2ListLabels(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListLabels()
+	return planeRawResult(data, err, "列出标签")
+}
+
+func handlePlaneV2GetLabel(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("label_id")
+	if err != nil {
+		return helper.ParamError("label_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetLabel(id)
+	return planeRawResult(data, apiErr, "获取标签详情")
+}
+
+// === State Handlers ===
+
+func handlePlaneV2ListStates(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListStates()
+	return planeRawResult(data, err, "列出状态")
+}
+
+func handlePlaneV2GetState(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("state_id")
+	if err != nil {
+		return helper.ParamError("state_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetState(id)
+	return planeRawResult(data, apiErr, "获取状态详情")
+}
+
+// === Comment Handlers ===
+
+func handlePlaneV2ListComments(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := repositories.Plane.ListComments(id)
+	return planeRawResult(data, apiErr, "列出评论")
+}
+
+func handlePlaneV2GetComment(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	issueID, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	commentID, err := req.RequireString("comment_id")
+	if err != nil {
+		return helper.ParamError("comment_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetComment(issueID, commentID)
+	return planeRawResult(data, apiErr, "获取评论详情")
+}
+
+// === Link Handlers ===
+
+func handlePlaneV2ListIssueLinks(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := repositories.Plane.ListIssueLinks(id)
+	return planeRawResult(data, apiErr, "列出工作项链接")
+}
+
+// === Relation Handlers ===
+
+func handlePlaneV2ListIssueRelations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := repositories.Plane.ListIssueRelations(id)
+	return planeRawResult(data, apiErr, "列出工作项关联")
+}
+
+// === Sticky Handlers ===
+
+func handlePlaneV2ListStickies(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListStickies()
+	return planeRawResult(data, err, "列出便签")
+}
+
+func handlePlaneV2GetSticky(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("sticky_id")
+	if err != nil {
+		return helper.ParamError("sticky_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetSticky(id)
+	return planeRawResult(data, apiErr, "获取便签详情")
+}
+
+// === Intake Handlers ===
+
+func handlePlaneV2ListIntakeIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	status := req.GetString("status", "")
+	data, err := repositories.Plane.ListIntakeIssues(status)
+	return planeRawResult(data, err, "列出收件箱工作项")
+}
+
+func handlePlaneV2GetIntakeIssue(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetIntakeIssue(id)
+	return planeRawResult(data, apiErr, "获取收件箱工作项详情")
+}
+
+// === Asset Handlers ===
+
+func handlePlaneV2GetProjectAssetURL(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("asset_id")
+	if err != nil {
+		return helper.ParamError("asset_id"), nil
+	}
+	url, apiErr := repositories.Plane.GetProjectAssetURL(id)
+	if apiErr != nil {
+		return helper.ServiceError("获取资源URL", apiErr), nil
+	}
+	return helper.TextResult(url), nil
+}
+
+// === Page Handlers ===
+
+func handlePlaneV2ListPages(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListPages()
+	return planeRawResult(data, err, "列出页面")
+}
+
+func handlePlaneV2GetPage(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("page_id")
+	if err != nil {
+		return helper.ParamError("page_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetPage(id)
+	return planeRawResult(data, apiErr, "获取页面详情")
+}
+
+func handlePlaneV2GetPageDescription(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("page_id")
+	if err != nil {
+		return helper.ParamError("page_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetPageDescription(id)
+	return planeRawResult(data, apiErr, "获取页面描述")
+}
+
+// === Member Handlers ===
+
+func handlePlaneV2ListWorkspaceMembers(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListWorkspaceMembers()
+	return planeRawResult(data, err, "列出工作空间成员")
+}
+
+func handlePlaneV2GetWorkspaceMember(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("member_id")
+	if err != nil {
+		return helper.ParamError("member_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetWorkspaceMember(id)
+	return planeRawResult(data, apiErr, "获取工作空间成员详情")
+}
+
+func handlePlaneV2ListProjectMembers(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := repositories.Plane.ListProjectMembers()
+	return planeRawResult(data, err, "列出项目成员")
+}
+
+func handlePlaneV2GetProjectMember(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	id, err := req.RequireString("member_id")
+	if err != nil {
+		return helper.ParamError("member_id"), nil
+	}
+	data, apiErr := repositories.Plane.GetProjectMember(id)
+	return planeRawResult(data, apiErr, "获取项目成员详情")
+}

+ 97 - 0
service/cmd/mcp/tools/data/plane_v2_api_write.go

@@ -0,0 +1,97 @@
+package data
+
+// ============================================================
+// Plane V2 写操作工具(暂未启用,需要时取消注释并实现 handler)
+// ============================================================
+
+// --- Workspace 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_workspace", mcp.WithDescription("创建工作空间(V2 Web API)"), mcp.WithString("name", mcp.Required(), mcp.Description("工作空间名称")), mcp.WithString("slug", mcp.Required(), mcp.Description("工作空间标识"))), handlePlaneV2CreateWorkspace)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_workspace", mcp.WithDescription("更新工作空间(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateWorkspace)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_workspace", mcp.WithDescription("删除工作空间(V2 Web API)")), handlePlaneV2DeleteWorkspace)
+
+// --- Project 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_project", mcp.WithDescription("创建项目(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("项目数据JSON"))), handlePlaneV2CreateProject)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_project", mcp.WithDescription("更新项目(V2 Web API)"), mcp.WithString("project_id", mcp.Required(), mcp.Description("项目UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateProject)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_project", mcp.WithDescription("删除项目(V2 Web API)"), mcp.WithString("project_id", mcp.Required(), mcp.Description("项目UUID"))), handlePlaneV2DeleteProject)
+// s.AddTool(mcp.NewTool("data_plane_v2_archive_project", mcp.WithDescription("归档项目(V2 Web API)"), mcp.WithString("project_id", mcp.Required(), mcp.Description("项目UUID"))), handlePlaneV2ArchiveProject)
+// s.AddTool(mcp.NewTool("data_plane_v2_unarchive_project", mcp.WithDescription("取消归档项目(V2 Web API)"), mcp.WithString("project_id", mcp.Required(), mcp.Description("项目UUID"))), handlePlaneV2UnarchiveProject)
+
+// --- Issue 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_issue", mcp.WithDescription("创建工作项(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("工作项数据JSON"))), handlePlaneV2CreateIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_issue", mcp.WithDescription("更新工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_issue", mcp.WithDescription("删除工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2DeleteIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_archive_issue", mcp.WithDescription("归档工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2ArchiveIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_unarchive_issue", mcp.WithDescription("取消归档工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2UnarchiveIssue)
+
+// --- Cycle 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_cycle", mcp.WithDescription("创建周期(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("周期数据JSON"))), handlePlaneV2CreateCycle)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_cycle", mcp.WithDescription("更新周期(V2 Web API)"), mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateCycle)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_cycle", mcp.WithDescription("删除周期(V2 Web API)"), mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期UUID"))), handlePlaneV2DeleteCycle)
+// s.AddTool(mcp.NewTool("data_plane_v2_add_issues_to_cycle", mcp.WithDescription("添加工作项到周期(V2 Web API)"), mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期UUID")), mcp.WithString("issue_ids", mcp.Required(), mcp.Description("工作项UUID列表,逗号分隔"))), handlePlaneV2AddIssuesToCycle)
+// s.AddTool(mcp.NewTool("data_plane_v2_remove_issue_from_cycle", mcp.WithDescription("从周期移除工作项(V2 Web API)"), mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期UUID")), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2RemoveIssueFromCycle)
+// s.AddTool(mcp.NewTool("data_plane_v2_archive_cycle", mcp.WithDescription("归档周期(V2 Web API)"), mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期UUID"))), handlePlaneV2ArchiveCycle)
+// s.AddTool(mcp.NewTool("data_plane_v2_unarchive_cycle", mcp.WithDescription("取消归档周期(V2 Web API)"), mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期UUID"))), handlePlaneV2UnarchiveCycle)
+
+// --- Module 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_module", mcp.WithDescription("创建模块(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("模块数据JSON"))), handlePlaneV2CreateModule)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_module", mcp.WithDescription("更新模块(V2 Web API)"), mcp.WithString("module_id", mcp.Required(), mcp.Description("模块UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateModule)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_module", mcp.WithDescription("删除模块(V2 Web API)"), mcp.WithString("module_id", mcp.Required(), mcp.Description("模块UUID"))), handlePlaneV2DeleteModule)
+// s.AddTool(mcp.NewTool("data_plane_v2_add_issues_to_module", mcp.WithDescription("添加工作项到模块(V2 Web API)"), mcp.WithString("module_id", mcp.Required(), mcp.Description("模块UUID")), mcp.WithString("issue_ids", mcp.Required(), mcp.Description("工作项UUID列表,逗号分隔"))), handlePlaneV2AddIssuesToModule)
+// s.AddTool(mcp.NewTool("data_plane_v2_remove_issue_from_module", mcp.WithDescription("从模块移除工作项(V2 Web API)"), mcp.WithString("module_id", mcp.Required(), mcp.Description("模块UUID")), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2RemoveIssueFromModule)
+// s.AddTool(mcp.NewTool("data_plane_v2_archive_module", mcp.WithDescription("归档模块(V2 Web API)"), mcp.WithString("module_id", mcp.Required(), mcp.Description("模块UUID"))), handlePlaneV2ArchiveModule)
+// s.AddTool(mcp.NewTool("data_plane_v2_unarchive_module", mcp.WithDescription("取消归档模块(V2 Web API)"), mcp.WithString("module_id", mcp.Required(), mcp.Description("模块UUID"))), handlePlaneV2UnarchiveModule)
+
+// --- Label 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_label", mcp.WithDescription("创建标签(V2 Web API)"), mcp.WithString("name", mcp.Required(), mcp.Description("标签名称")), mcp.WithString("color", mcp.Required(), mcp.Description("标签颜色"))), handlePlaneV2CreateLabel)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_label", mcp.WithDescription("更新标签(V2 Web API)"), mcp.WithString("label_id", mcp.Required(), mcp.Description("标签UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateLabel)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_label", mcp.WithDescription("删除标签(V2 Web API)"), mcp.WithString("label_id", mcp.Required(), mcp.Description("标签UUID"))), handlePlaneV2DeleteLabel)
+
+// --- State 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_state", mcp.WithDescription("创建状态(V2 Web API)"), mcp.WithString("name", mcp.Required(), mcp.Description("状态名称")), mcp.WithString("color", mcp.Required(), mcp.Description("状态颜色")), mcp.WithString("group", mcp.Required(), mcp.Description("状态分组"))), handlePlaneV2CreateState)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_state", mcp.WithDescription("更新状态(V2 Web API)"), mcp.WithString("state_id", mcp.Required(), mcp.Description("状态UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateState)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_state", mcp.WithDescription("删除状态(V2 Web API)"), mcp.WithString("state_id", mcp.Required(), mcp.Description("状态UUID"))), handlePlaneV2DeleteState)
+// s.AddTool(mcp.NewTool("data_plane_v2_mark_state_default", mcp.WithDescription("标记状态为默认(V2 Web API)"), mcp.WithString("state_id", mcp.Required(), mcp.Description("状态UUID"))), handlePlaneV2MarkStateDefault)
+
+// --- Comment 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_comment", mcp.WithDescription("创建评论(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("comment_html", mcp.Required(), mcp.Description("评论HTML内容"))), handlePlaneV2CreateComment)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_comment", mcp.WithDescription("更新评论(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("comment_id", mcp.Required(), mcp.Description("评论UUID")), mcp.WithString("comment_html", mcp.Required(), mcp.Description("评论HTML内容"))), handlePlaneV2UpdateComment)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_comment", mcp.WithDescription("删除评论(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("comment_id", mcp.Required(), mcp.Description("评论UUID"))), handlePlaneV2DeleteComment)
+
+// --- Link 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_issue_link", mcp.WithDescription("创建工作项链接(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("url", mcp.Required(), mcp.Description("链接URL")), mcp.WithString("title", mcp.Required(), mcp.Description("链接标题"))), handlePlaneV2CreateIssueLink)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_issue_link", mcp.WithDescription("更新工作项链接(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("link_id", mcp.Required(), mcp.Description("链接UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateIssueLink)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_issue_link", mcp.WithDescription("删除工作项链接(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("link_id", mcp.Required(), mcp.Description("链接UUID"))), handlePlaneV2DeleteIssueLink)
+
+// --- Relation 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_issue_relation", mcp.WithDescription("创建工作项关联(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("relation_type", mcp.Required(), mcp.Description("关联类型")), mcp.WithString("related_ids", mcp.Required(), mcp.Description("关联工作项UUID列表,逗号分隔"))), handlePlaneV2CreateIssueRelation)
+// s.AddTool(mcp.NewTool("data_plane_v2_remove_issue_relation", mcp.WithDescription("移除工作项关联(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("relation_type", mcp.Required(), mcp.Description("关联类型")), mcp.WithString("related_id", mcp.Required(), mcp.Description("关联工作项UUID"))), handlePlaneV2RemoveIssueRelation)
+
+// --- Sticky 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_sticky", mcp.WithDescription("创建便签(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("便签数据JSON"))), handlePlaneV2CreateSticky)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_sticky", mcp.WithDescription("更新便签(V2 Web API)"), mcp.WithString("sticky_id", mcp.Required(), mcp.Description("便签UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateSticky)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_sticky", mcp.WithDescription("删除便签(V2 Web API)"), mcp.WithString("sticky_id", mcp.Required(), mcp.Description("便签UUID"))), handlePlaneV2DeleteSticky)
+
+// --- Intake 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_intake_issue", mcp.WithDescription("创建收件箱工作项(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("工作项数据JSON"))), handlePlaneV2CreateIntakeIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_intake_issue", mcp.WithDescription("更新收件箱工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdateIntakeIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_accept_intake_issue", mcp.WithDescription("接受收件箱工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2AcceptIntakeIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_reject_intake_issue", mcp.WithDescription("拒绝收件箱工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2RejectIntakeIssue)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_intake_issue", mcp.WithDescription("删除收件箱工作项(V2 Web API)"), mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项UUID"))), handlePlaneV2DeleteIntakeIssue)
+
+// --- Page 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_create_page", mcp.WithDescription("创建页面(V2 Web API)"), mcp.WithString("data", mcp.Required(), mcp.Description("页面数据JSON"))), handlePlaneV2CreatePage)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_page", mcp.WithDescription("更新页面(V2 Web API)"), mcp.WithString("page_id", mcp.Required(), mcp.Description("页面UUID")), mcp.WithString("data", mcp.Required(), mcp.Description("更新数据JSON"))), handlePlaneV2UpdatePage)
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_page", mcp.WithDescription("删除页面(V2 Web API)"), mcp.WithString("page_id", mcp.Required(), mcp.Description("页面UUID"))), handlePlaneV2DeletePage)
+// s.AddTool(mcp.NewTool("data_plane_v2_archive_page", mcp.WithDescription("归档页面(V2 Web API)"), mcp.WithString("page_id", mcp.Required(), mcp.Description("页面UUID"))), handlePlaneV2ArchivePage)
+// s.AddTool(mcp.NewTool("data_plane_v2_unarchive_page", mcp.WithDescription("取消归档页面(V2 Web API)"), mcp.WithString("page_id", mcp.Required(), mcp.Description("页面UUID"))), handlePlaneV2UnarchivePage)
+
+// --- Member 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_update_workspace_member_role", mcp.WithDescription("更新工作空间成员角色(V2 Web API)"), mcp.WithString("member_id", mcp.Required(), mcp.Description("成员UUID")), mcp.WithNumber("role", mcp.Required(), mcp.Description("角色值"))), handlePlaneV2UpdateWorkspaceMemberRole)
+// s.AddTool(mcp.NewTool("data_plane_v2_remove_workspace_member", mcp.WithDescription("移除工作空间成员(V2 Web API)"), mcp.WithString("member_id", mcp.Required(), mcp.Description("成员UUID"))), handlePlaneV2RemoveWorkspaceMember)
+// s.AddTool(mcp.NewTool("data_plane_v2_add_project_member", mcp.WithDescription("添加项目成员(V2 Web API)"), mcp.WithString("member_id", mcp.Required(), mcp.Description("成员UUID")), mcp.WithNumber("role", mcp.Required(), mcp.Description("角色值"))), handlePlaneV2AddProjectMember)
+// s.AddTool(mcp.NewTool("data_plane_v2_update_project_member_role", mcp.WithDescription("更新项目成员角色(V2 Web API)"), mcp.WithString("member_id", mcp.Required(), mcp.Description("成员UUID")), mcp.WithNumber("role", mcp.Required(), mcp.Description("角色值"))), handlePlaneV2UpdateProjectMemberRole)
+// s.AddTool(mcp.NewTool("data_plane_v2_remove_project_member", mcp.WithDescription("移除项目成员(V2 Web API)"), mcp.WithString("member_id", mcp.Required(), mcp.Description("成员UUID"))), handlePlaneV2RemoveProjectMember)
+
+// --- Asset 写操作 ---
+// s.AddTool(mcp.NewTool("data_plane_v2_delete_project_asset", mcp.WithDescription("删除项目资源(V2 Web API)"), mcp.WithString("asset_id", mcp.Required(), mcp.Description("资源UUID"))), handlePlaneV2DeleteProjectAsset)
+// s.AddTool(mcp.NewTool("data_plane_v2_bind_assets_to_entity", mcp.WithDescription("绑定资源到实体(V2 Web API)"), mcp.WithString("entity_id", mcp.Required(), mcp.Description("实体UUID")), mcp.WithString("asset_ids", mcp.Required(), mcp.Description("资源UUID列表,逗号分隔"))), handlePlaneV2BindAssetsToEntity)

+ 1 - 0
service/cmd/mcp/tools/data/register.go

@@ -10,5 +10,6 @@ func RegisterDataTools(s *server.MCPServer) {
 	registerEmsparaTools(s)
 	registerFwhApiTools(s)
 	registerPlaneApiTools(s)
+	registerPlaneV2ApiTools(s)
 	registerSimApiTools(s)
 }

+ 2 - 3
service/repositories/api/plane_v2/client.go

@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"fwh-toolbox-server/config"
 	"io"
 	"mime/multipart"
 	"net/http"
@@ -40,13 +39,13 @@ type PlaneClient struct {
 var globalClient *PlaneClient
 
 // InitPlaneClient 初始化全局 PlaneClient(启动时调用一次,失败直接 panic)
-func InitPlaneClient(cfg *config.PlaneWebApiConfig) {
+func InitPlaneClient(host, email, password string) {
 	if globalClient != nil {
 		zap.L().Warn("[PlaneClient] 重复调用 InitPlaneClient,已忽略")
 		return
 	}
 
-	client, err := newPlaneClient(cfg.Host, cfg.Email, cfg.Password)
+	client, err := newPlaneClient(host, email, password)
 	if err != nil {
 		panic(fmt.Sprintf("[PlaneClient] 初始化失败: %v", err))
 	}

+ 1115 - 7
service/repositories/api/plane_v2/service.go

@@ -3,7 +3,6 @@ package plane_v2
 import (
 	"encoding/json"
 	"fmt"
-	"fwh-toolbox-server/config"
 )
 
 // PlaneService 便捷封装(绑定固定的 workspace/project,共享全局 PlaneClient)
@@ -17,18 +16,18 @@ type PlaneService struct {
 }
 
 // NewPlaneService 创建 PlaneService(须先调用 InitPlaneClient)
-func NewPlaneService(cfg *config.PlaneWebApiConfig) *PlaneService {
+func NewPlaneService(workspaceSlug, projectId, labelBugId, labelFeatureId, labelFeedbackId string) *PlaneService {
 	if globalClient == nil {
 		panic("[PlaneService] PlaneClient 未初始化,请先调用 InitPlaneClient")
 	}
 
 	return &PlaneService{
 		Client:          globalClient,
-		WorkspaceSlug:   cfg.WorkspaceSlug,
-		ProjectId:       cfg.ProjectId,
-		LabelBugId:      cfg.LabelBugId,
-		LabelFeatureId:  cfg.LabelFeatureId,
-		LabelFeedbackId: cfg.LabelFeedbackId,
+		WorkspaceSlug:   workspaceSlug,
+		ProjectId:       projectId,
+		LabelBugId:      labelBugId,
+		LabelFeatureId:  labelFeatureId,
+		LabelFeedbackId: labelFeedbackId,
 	}
 }
 
@@ -178,3 +177,1112 @@ func (s *PlaneService) GetPageDescription(pageID string) ([]byte, error) {
 	}
 	return resp.Body, nil
 }
+
+// ============ Workspace 便捷方法 ============
+
+func (s *PlaneService) ListWorkspaces() ([]byte, error) {
+	resp, err := s.Client.Get("/api/workspaces/")
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListWorkspaces 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetWorkspace() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/", s.WorkspaceSlug)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetWorkspace 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateWorkspace(name, slug string) ([]byte, error) {
+	payload := map[string]interface{}{"name": name, "slug": slug}
+	resp, err := s.Client.Post("/api/workspaces/", payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateWorkspace 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateWorkspace(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/", s.WorkspaceSlug)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateWorkspace 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteWorkspace() error {
+	url := fmt.Sprintf("/api/workspaces/%s/", s.WorkspaceSlug)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteWorkspace 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Project 便捷方法 ============
+
+func (s *PlaneService) ListProjects() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/", s.WorkspaceSlug)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListProjects 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetProject(projectID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", s.WorkspaceSlug, projectID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetProject 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateProject(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/", s.WorkspaceSlug)
+	resp, err := s.Client.Post(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateProject 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateProject(projectID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", s.WorkspaceSlug, projectID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateProject 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteProject(projectID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", s.WorkspaceSlug, projectID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteProject 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) ArchiveProject(projectID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", s.WorkspaceSlug, projectID)
+	resp, err := s.Client.Post(url, nil)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("ArchiveProject 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) UnarchiveProject(projectID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", s.WorkspaceSlug, projectID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("UnarchiveProject 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Issue 便捷方法 ============
+
+func (s *PlaneService) ListIssues() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListIssues 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetIssue(issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetIssue 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateIssue(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Post(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateIssue 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateIssue(issueID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateIssue 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteIssue(issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteIssue 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) GetSubIssues(issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/sub-issues/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetSubIssues 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) ArchiveIssue(issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Post(url, nil)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("ArchiveIssue 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) UnarchiveIssue(issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("UnarchiveIssue 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) GetIssueActivity(issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/history/?activity_type=issue-property", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetIssueActivity 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+// ============ Cycle 便捷方法 ============
+
+func (s *PlaneService) ListCycles() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListCycles 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetCycle(cycleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", s.WorkspaceSlug, s.ProjectId, cycleID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetCycle 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateCycle(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Post(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateCycle 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateCycle(cycleID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", s.WorkspaceSlug, s.ProjectId, cycleID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateCycle 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteCycle(cycleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", s.WorkspaceSlug, s.ProjectId, cycleID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteCycle 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) ListCycleIssues(cycleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", s.WorkspaceSlug, s.ProjectId, cycleID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListCycleIssues 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) AddIssuesToCycle(cycleID string, issueIDs []string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", s.WorkspaceSlug, s.ProjectId, cycleID)
+	payload := map[string]interface{}{"issues": issueIDs}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("AddIssuesToCycle 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) RemoveIssueFromCycle(cycleID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/%s/", s.WorkspaceSlug, s.ProjectId, cycleID, issueID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("RemoveIssueFromCycle 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) ArchiveCycle(cycleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", s.WorkspaceSlug, s.ProjectId, cycleID)
+	resp, err := s.Client.Post(url, nil)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("ArchiveCycle 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) UnarchiveCycle(cycleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", s.WorkspaceSlug, s.ProjectId, cycleID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("UnarchiveCycle 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Module 便捷方法 ============
+
+func (s *PlaneService) ListModules() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListModules 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetModule(moduleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", s.WorkspaceSlug, s.ProjectId, moduleID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetModule 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateModule(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Post(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateModule 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateModule(moduleID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", s.WorkspaceSlug, s.ProjectId, moduleID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateModule 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteModule(moduleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", s.WorkspaceSlug, s.ProjectId, moduleID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteModule 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) ListModuleIssues(moduleID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/", s.WorkspaceSlug, s.ProjectId, moduleID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListModuleIssues 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) AddIssuesToModule(moduleID string, issueIDs []string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/", s.WorkspaceSlug, s.ProjectId, moduleID)
+	payload := map[string]interface{}{"issues": issueIDs}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("AddIssuesToModule 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) RemoveIssueFromModule(moduleID, issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/%s/", s.WorkspaceSlug, s.ProjectId, moduleID, issueID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("RemoveIssueFromModule 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) ArchiveModule(moduleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", s.WorkspaceSlug, s.ProjectId, moduleID)
+	resp, err := s.Client.Post(url, nil)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("ArchiveModule 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) UnarchiveModule(moduleID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", s.WorkspaceSlug, s.ProjectId, moduleID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("UnarchiveModule 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Label 便捷方法 ============
+
+func (s *PlaneService) ListLabels() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListLabels 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetLabel(labelID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", s.WorkspaceSlug, s.ProjectId, labelID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetLabel 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateLabel(name, color string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/", s.WorkspaceSlug, s.ProjectId)
+	payload := map[string]interface{}{"name": name, "color": color}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateLabel 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateLabel(labelID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", s.WorkspaceSlug, s.ProjectId, labelID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateLabel 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteLabel(labelID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", s.WorkspaceSlug, s.ProjectId, labelID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteLabel 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ State 便捷方法 ============
+
+func (s *PlaneService) ListStates() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListStates 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetState(stateID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", s.WorkspaceSlug, s.ProjectId, stateID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetState 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateState(name, color, group string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", s.WorkspaceSlug, s.ProjectId)
+	payload := map[string]interface{}{"name": name, "color": color, "group": group}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateState 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateState(stateID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", s.WorkspaceSlug, s.ProjectId, stateID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateState 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteState(stateID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", s.WorkspaceSlug, s.ProjectId, stateID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteState 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) MarkStateDefault(stateID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/default/", s.WorkspaceSlug, s.ProjectId, stateID)
+	resp, err := s.Client.Post(url, nil)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("MarkStateDefault 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Comment 便捷方法 ============
+
+func (s *PlaneService) ListComments(issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListComments 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetComment(issueID, commentID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", s.WorkspaceSlug, s.ProjectId, issueID, commentID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetComment 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateComment(issueID, commentHTML string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", s.WorkspaceSlug, s.ProjectId, issueID)
+	payload := map[string]interface{}{"comment_html": commentHTML}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateComment 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateComment(issueID, commentID, commentHTML string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", s.WorkspaceSlug, s.ProjectId, issueID, commentID)
+	payload := map[string]interface{}{"comment_html": commentHTML}
+	resp, err := s.Client.Patch(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateComment 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteComment(issueID, commentID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", s.WorkspaceSlug, s.ProjectId, issueID, commentID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteComment 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Link 便捷方法 ============
+
+func (s *PlaneService) ListIssueLinks(issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListIssueLinks 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateIssueLink(issueID, linkURL, title string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/", s.WorkspaceSlug, s.ProjectId, issueID)
+	payload := map[string]interface{}{"url": linkURL, "title": title}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateIssueLink 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateIssueLink(issueID, linkID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/%s/", s.WorkspaceSlug, s.ProjectId, issueID, linkID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateIssueLink 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteIssueLink(issueID, linkID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/%s/", s.WorkspaceSlug, s.ProjectId, issueID, linkID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteIssueLink 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Relation 便捷方法 ============
+
+func (s *PlaneService) ListIssueRelations(issueID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListIssueRelations 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateIssueRelation(issueID, relationType string, relatedIDs []string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/", s.WorkspaceSlug, s.ProjectId, issueID)
+	payload := map[string]interface{}{"relation_type": relationType, "related_issues": relatedIDs}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateIssueRelation 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) RemoveIssueRelation(issueID, relationType, relatedID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/%s/%s/", s.WorkspaceSlug, s.ProjectId, issueID, relationType, relatedID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("RemoveIssueRelation 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Sticky 便捷方法 ============
+
+func (s *PlaneService) ListStickies() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/", s.WorkspaceSlug)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListStickies 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetSticky(stickyID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", s.WorkspaceSlug, stickyID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetSticky 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) CreateSticky(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/", s.WorkspaceSlug)
+	resp, err := s.Client.Post(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateSticky 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateSticky(stickyID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", s.WorkspaceSlug, stickyID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateSticky 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeleteSticky(stickyID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", s.WorkspaceSlug, stickyID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteSticky 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Intake 补充方法 ============
+
+func (s *PlaneService) UpdateIntakeIssue(issueID string, data map[string]any) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateIntakeIssue 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) AcceptIntakeIssue(issueID string) ([]byte, error) {
+	return s.UpdateIntakeIssue(issueID, map[string]any{"status": 1})
+}
+
+func (s *PlaneService) RejectIntakeIssue(issueID string) ([]byte, error) {
+	return s.UpdateIntakeIssue(issueID, map[string]any{"status": -1})
+}
+
+func (s *PlaneService) DeleteIntakeIssue(issueID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeleteIntakeIssue 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Asset 补充方法 ============
+
+func (s *PlaneService) CreateProjectAsset(fileName, mimeType string, size int64, entityType string) (*AssetUploadResult, error) {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/", s.WorkspaceSlug, s.ProjectId)
+	payload := map[string]interface{}{
+		"name": fileName, "type": mimeType, "size": size,
+		"entity_type": entityType, "entity_identifier": "",
+	}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreateProjectAsset 失败: %s", resp.Status)
+	}
+	var result AssetUploadResult
+	if err := json.Unmarshal(resp.Body, &result); err != nil {
+		return nil, fmt.Errorf("解析响应失败: %w", err)
+	}
+	return &result, nil
+}
+
+func (s *PlaneService) ConfirmProjectAssetUpload(assetID string) error {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", s.WorkspaceSlug, s.ProjectId, assetID)
+	resp, err := s.Client.Patch(url, map[string]interface{}{})
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 && !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("ConfirmProjectAssetUpload 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) GetProjectAsset(assetID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", s.WorkspaceSlug, s.ProjectId, assetID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	return resp.Body, nil
+}
+
+// ============ Page 补充方法 ============
+
+func (s *PlaneService) CreatePage(data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Post(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("CreatePage 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdatePage(pageID string, data map[string]interface{}) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", s.WorkspaceSlug, s.ProjectId, pageID)
+	resp, err := s.Client.Patch(url, data)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdatePage 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) DeletePage(pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", s.WorkspaceSlug, s.ProjectId, pageID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("DeletePage 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) ArchivePage(pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", s.WorkspaceSlug, s.ProjectId, pageID)
+	resp, err := s.Client.Post(url, nil)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("ArchivePage 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) UnarchivePage(pageID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", s.WorkspaceSlug, s.ProjectId, pageID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return fmt.Errorf("UnarchivePage 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+// ============ Member 便捷方法 ============
+
+func (s *PlaneService) ListWorkspaceMembers() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/members/", s.WorkspaceSlug)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListWorkspaceMembers 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetWorkspaceMember(memberID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/members/%s/", s.WorkspaceSlug, memberID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetWorkspaceMember 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateWorkspaceMemberRole(memberID string, role int) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/members/%s/", s.WorkspaceSlug, memberID)
+	payload := map[string]interface{}{"role": role}
+	resp, err := s.Client.Patch(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateWorkspaceMemberRole 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) RemoveWorkspaceMember(memberID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/members/%s/", s.WorkspaceSlug, memberID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("RemoveWorkspaceMember 失败: %s", resp.Status)
+	}
+	return nil
+}
+
+func (s *PlaneService) ListProjectMembers() ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", s.WorkspaceSlug, s.ProjectId)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("ListProjectMembers 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) GetProjectMember(memberID string) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", s.WorkspaceSlug, s.ProjectId, memberID)
+	resp, err := s.Client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("GetProjectMember 失败: %s", resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) AddProjectMember(memberID string, role int) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", s.WorkspaceSlug, s.ProjectId)
+	payload := map[string]interface{}{"member": memberID, "role": role}
+	resp, err := s.Client.Post(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("AddProjectMember 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) UpdateProjectMemberRole(memberID string, role int) ([]byte, error) {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", s.WorkspaceSlug, s.ProjectId, memberID)
+	payload := map[string]interface{}{"role": role}
+	resp, err := s.Client.Patch(url, payload)
+	if err != nil {
+		return nil, err
+	}
+	if !IsSuccess(resp.StatusCode) {
+		return nil, fmt.Errorf("UpdateProjectMemberRole 失败: %s, body=%s", resp.Status, string(resp.Body))
+	}
+	return resp.Body, nil
+}
+
+func (s *PlaneService) RemoveProjectMember(memberID string) error {
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", s.WorkspaceSlug, s.ProjectId, memberID)
+	resp, err := s.Client.Delete(url)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("RemoveProjectMember 失败: %s", resp.Status)
+	}
+	return nil
+}

+ 8 - 4
service/repositories/repositories.go

@@ -18,15 +18,19 @@ import (
 
 // ============ Plane V2 新架构(PlaneService)============
 
-// Plane 全局 PlaneService 实例(新架构入口)
+// Plane 全局 PlaneService 实例(新架构入口) - 仅用于 fwh-toolbox 的 Feedback 模块
 var Plane *plane_v2.PlaneService
 
-// InitPlane 初始化新架构 Plane(启动时调用)
+// InitPlane 初始化新架构 Plane(启动时调用) - 仅用于 fwh-toolbox 的 Feedback 模块
 func InitPlane(cfg *config.PlaneWebApiConfig) {
-	plane_v2.InitPlaneClient(cfg)
-	Plane = plane_v2.NewPlaneService(cfg)
+	plane_v2.InitPlaneClient(cfg.Host, cfg.Email, cfg.Password)
+	Plane = plane_v2.NewPlaneService(cfg.WorkspaceSlug, cfg.ProjectId, cfg.LabelBugId, cfg.LabelFeatureId, cfg.LabelFeedbackId)
 }
 
+// InitPlane 初始化接口(给MCP Server使用)
+var InitPlaneClient = plane_v2.InitPlaneClient
+var NewPlaneService = plane_v2.NewPlaneService
+
 // 聚合包
 
 /*************** API ***************/