Quellcode durchsuchen

fix:调整plane v2 api解决不可用问题,并测试通过

Jerry Liang vor 2 Monaten
Ursprung
Commit
36e03d30b7

+ 99 - 167
service/cmd/mcp/tools/data/plane_v2_api.go

@@ -4,51 +4,42 @@ import (
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/server"
 
-	"fwh-toolbox-server/config"
 	plane_v2 "fwh-toolbox-server/repositories/api/plane_v2"
 )
 
-// planeV2 通用 PlaneService 实例(MCP 专用,不绑定 Feedback 标签)
-var planeV2 *plane_v2.PlaneService
-
-// planeV2Workspace / planeV2Project 从配置读取的默认 workspace/project
-var planeV2Workspace string
-var planeV2Project string
+// planeV2 通用 PlaneService 实例
+var planeV2Svc *plane_v2.PlaneService
 
 func registerPlaneV2ApiTools(s *server.MCPServer) {
-	// 初始化通用 PlaneService(PlaneClient 已在 main.go 中初始化)
-	planeV2 = plane_v2.NewPlaneService()
+	planeV2Svc = plane_v2.NewPlaneService()
 
-	// 从配置读取默认 workspace/project
-	cfg, _ := config.LoadConfig()
-	if cfg != nil {
-		planeV2Workspace = cfg.PlaneWebApi.WorkspaceSlug
-		planeV2Project = cfg.PlaneWebApi.ProjectId
-	}
 	// ============ Workspace (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_list_workspaces",
-			mcp.WithDescription("列出所有工作空间(V2 Web API)"),
+		mcp.NewTool("plane_list_workspaces",
+			mcp.WithDescription("列出当前用户有权访问的所有工作空间"),
 		),
 		handlePlaneV2ListWorkspaces,
 	)
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_get_workspace",
-			mcp.WithDescription("获取当前工作空间详情(V2 Web API)"),
+		mcp.NewTool("plane_get_workspace",
+			mcp.WithDescription("获取工作空间详情"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
 		),
 		handlePlaneV2GetWorkspace,
 	)
 
 	// ============ Project (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_list_projects",
-			mcp.WithDescription("列出工作空间下的所有项目(V2 Web API)"),
+		mcp.NewTool("plane_list_projects",
+			mcp.WithDescription("列出工作空间下的所有项目"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
 		),
 		handlePlaneV2ListProjects,
 	)
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_get_project",
-			mcp.WithDescription("获取项目详情(V2 Web API)"),
+		mcp.NewTool("plane_get_project",
+			mcp.WithDescription("获取项目详情"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
 			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 		),
 		handlePlaneV2GetProject,
@@ -56,199 +47,142 @@ func registerPlaneV2ApiTools(s *server.MCPServer) {
 
 	// ============ Issue (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_list_issues",
-			mcp.WithDescription("列出项目工作项列表(V2 Web API)"),
+		mcp.NewTool("plane_list_issues",
+			mcp.WithDescription("列出项目工作项列表"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 		),
 		handlePlaneV2ListIssues,
 	)
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_get_issue",
-			mcp.WithDescription("获取工作项详情(V2 Web API)"),
+		mcp.NewTool("plane_get_issue",
+			mcp.WithDescription("获取工作项详情"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 			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.NewTool("plane_get_sub_issues",
+			mcp.WithDescription("获取子工作项列表"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 			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.NewTool("plane_get_issue_activity",
+			mcp.WithDescription("获取工作项活动历史"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
 		),
 		handlePlaneV2GetIssueActivity,
 	)
 
-	// ============ Cycle (读) ============
+	// ============ Intake (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_list_cycles",
-			mcp.WithDescription("列出项目周期列表(V2 Web API)"),
+		mcp.NewTool("plane_list_intake_issues",
+			mcp.WithDescription("列出收件箱工作项"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
+			mcp.WithString("status", mcp.Description("状态过滤,如 -2,-1,0,1,2")),
 		),
-		handlePlaneV2ListCycles,
+		handlePlaneV2ListIntakeIssues,
 	)
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_get_cycle",
-			mcp.WithDescription("获取周期详情(V2 Web API)"),
-			mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期 UUID")),
+		mcp.NewTool("plane_get_intake_issue",
+			mcp.WithDescription("获取收件箱工作项详情"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
+			mcp.WithString("issue_id", mcp.Required(), mcp.Description("工作项 UUID")),
 		),
-		handlePlaneV2GetCycle,
+		handlePlaneV2GetIntakeIssue,
 	)
+
+	// ============ Cycle (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_list_cycle_issues",
-			mcp.WithDescription("列出周期中的工作项(V2 Web API)"),
+		mcp.NewTool("plane_list_cycles",
+			mcp.WithDescription("列出项目周期列表"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
+		),
+		handlePlaneV2ListCycles,
+	)
+	s.AddTool(
+		mcp.NewTool("plane_get_cycle",
+			mcp.WithDescription("获取周期详情"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 			mcp.WithString("cycle_id", mcp.Required(), mcp.Description("周期 UUID")),
 		),
-		handlePlaneV2ListCycleIssues,
+		handlePlaneV2GetCycle,
 	)
 
 	// ============ Module (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_list_modules",
-			mcp.WithDescription("列出项目模块列表(V2 Web API)"),
+		mcp.NewTool("plane_list_modules",
+			mcp.WithDescription("列出项目模块列表"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 		),
 		handlePlaneV2ListModules,
 	)
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_get_module",
-			mcp.WithDescription("获取模块详情(V2 Web API)"),
+		mcp.NewTool("plane_get_module",
+			mcp.WithDescription("获取模块详情"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 			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)"),
+		mcp.NewTool("plane_list_labels",
+			mcp.WithDescription("列出项目标签列表"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 		),
 		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)"),
+		mcp.NewTool("plane_list_states",
+			mcp.WithDescription("列出项目状态列表"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 		),
 		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)"),
+		mcp.NewTool("plane_list_pages",
+			mcp.WithDescription("列出项目页面列表"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 		),
 		handlePlaneV2ListPages,
 	)
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_get_page",
-			mcp.WithDescription("获取页面详情(V2 Web API)"),
+		mcp.NewTool("plane_get_page",
+			mcp.WithDescription("获取页面详情"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 			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.NewTool("plane_get_page_description",
+			mcp.WithDescription("获取页面描述内容"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 			mcp.WithString("page_id", mcp.Required(), mcp.Description("页面 UUID")),
 		),
 		handlePlaneV2GetPageDescription,
@@ -256,29 +190,27 @@ func registerPlaneV2ApiTools(s *server.MCPServer) {
 
 	// ============ Member (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_list_workspace_members",
-			mcp.WithDescription("列出工作空间成员(V2 Web API)"),
+		mcp.NewTool("plane_list_workspace_members",
+			mcp.WithDescription("列出工作空间成员"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
 		),
 		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)"),
+		mcp.NewTool("plane_list_project_members",
+			mcp.WithDescription("列出项目成员"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
+			mcp.WithString("project_id", mcp.Required(), mcp.Description("项目 UUID")),
 		),
 		handlePlaneV2ListProjectMembers,
 	)
+
+	// ============ Sticky (读) ============
 	s.AddTool(
-		mcp.NewTool("data_plane_v2_get_project_member",
-			mcp.WithDescription("获取项目成员详情(V2 Web API)"),
-			mcp.WithString("member_id", mcp.Required(), mcp.Description("成员 UUID")),
+		mcp.NewTool("plane_list_stickies",
+			mcp.WithDescription("列出工作空间便签"),
+			mcp.WithString("workspace_slug", mcp.Required(), mcp.Description("工作空间 slug")),
 		),
-		handlePlaneV2GetProjectMember,
+		handlePlaneV2ListStickies,
 	)
 }

+ 98 - 172
service/cmd/mcp/tools/data/plane_v2_api_handlers.go

@@ -11,291 +11,217 @@ import (
 // === Workspace Handlers ===
 
 func handlePlaneV2ListWorkspaces(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListWorkspaces()
+	data, err := planeV2Svc.ListWorkspaces()
 	return planeRawResult(data, err, "列出工作空间")
 }
 
 func handlePlaneV2GetWorkspace(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.GetWorkspace(planeV2Workspace)
-	return planeRawResult(data, err, "获取工作空间详情")
+	slug, err := req.RequireString("workspace_slug")
+	if err != nil {
+		return helper.ParamError("workspace_slug"), nil
+	}
+	data, apiErr := planeV2Svc.GetWorkspace(slug)
+	return planeRawResult(data, apiErr, "获取工作空间详情")
 }
 
 // === Project Handlers ===
 
 func handlePlaneV2ListProjects(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListProjects(planeV2Workspace)
-	return planeRawResult(data, err, "列出项目")
+	slug, err := req.RequireString("workspace_slug")
+	if err != nil {
+		return helper.ParamError("workspace_slug"), nil
+	}
+	data, apiErr := planeV2Svc.ListProjects(slug)
+	return planeRawResult(data, apiErr, "列出项目")
 }
 
 func handlePlaneV2GetProject(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("project_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, err := req.RequireString("project_id")
 	if err != nil {
 		return helper.ParamError("project_id"), nil
 	}
-	data, apiErr := planeV2.GetProject(planeV2Workspace, id)
+	data, apiErr := planeV2Svc.GetProject(slug, projectId)
 	return planeRawResult(data, apiErr, "获取项目详情")
 }
 
 // === Issue Handlers ===
 
 func handlePlaneV2ListIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListIssues(planeV2Workspace, planeV2Project)
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	data, err := planeV2Svc.ListIssues(slug, projectId)
 	return planeRawResult(data, err, "列出工作项")
 }
 
 func handlePlaneV2GetIssue(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("issue_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	issueId, err := req.RequireString("issue_id")
 	if err != nil {
 		return helper.ParamError("issue_id"), nil
 	}
-	data, apiErr := planeV2.GetIssue(planeV2Workspace, planeV2Project, id)
+	data, apiErr := planeV2Svc.GetIssue(slug, projectId, issueId)
 	return planeRawResult(data, apiErr, "获取工作项详情")
 }
 
 func handlePlaneV2GetSubIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("issue_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	issueId, err := req.RequireString("issue_id")
 	if err != nil {
 		return helper.ParamError("issue_id"), nil
 	}
-	data, apiErr := planeV2.GetSubIssues(planeV2Workspace, planeV2Project, id)
+	data, apiErr := planeV2Svc.GetSubIssues(slug, projectId, issueId)
 	return planeRawResult(data, apiErr, "获取子工作项")
 }
 
 func handlePlaneV2GetIssueActivity(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("issue_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	issueId, err := req.RequireString("issue_id")
 	if err != nil {
 		return helper.ParamError("issue_id"), nil
 	}
-	data, apiErr := planeV2.GetIssueActivity(planeV2Workspace, planeV2Project, id)
+	data, apiErr := planeV2Svc.GetIssueActivity(slug, projectId, issueId)
 	return planeRawResult(data, apiErr, "获取工作项活动历史")
 }
 
+// === Intake Handlers ===
+
+func handlePlaneV2ListIntakeIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	status := req.GetString("status", "")
+	data, err := planeV2Svc.ListIntakeIssues(slug, projectId, status)
+	return planeRawResult(data, err, "列出收件箱工作项")
+}
+
+func handlePlaneV2GetIntakeIssue(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	issueId, err := req.RequireString("issue_id")
+	if err != nil {
+		return helper.ParamError("issue_id"), nil
+	}
+	data, apiErr := planeV2Svc.GetIntakeIssue(slug, projectId, issueId)
+	return planeRawResult(data, apiErr, "获取收件箱工作项详情")
+}
+
 // === Cycle Handlers ===
 
 func handlePlaneV2ListCycles(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListCycles(planeV2Workspace, planeV2Project)
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	data, err := planeV2Svc.ListCycles(slug, projectId)
 	return planeRawResult(data, err, "列出周期")
 }
 
 func handlePlaneV2GetCycle(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("cycle_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	cycleId, err := req.RequireString("cycle_id")
 	if err != nil {
 		return helper.ParamError("cycle_id"), nil
 	}
-	data, apiErr := planeV2.GetCycle(planeV2Workspace, planeV2Project, id)
+	data, apiErr := planeV2Svc.GetCycle(slug, projectId, cycleId)
 	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 := planeV2.ListCycleIssues(planeV2Workspace, planeV2Project, id)
-	return planeRawResult(data, apiErr, "列出周期工作项")
-}
-
 // === Module Handlers ===
 
 func handlePlaneV2ListModules(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListModules(planeV2Workspace, planeV2Project)
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	data, err := planeV2Svc.ListModules(slug, projectId)
 	return planeRawResult(data, err, "列出模块")
 }
 
 func handlePlaneV2GetModule(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("module_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	moduleId, err := req.RequireString("module_id")
 	if err != nil {
 		return helper.ParamError("module_id"), nil
 	}
-	data, apiErr := planeV2.GetModule(planeV2Workspace, planeV2Project, id)
+	data, apiErr := planeV2Svc.GetModule(slug, projectId, moduleId)
 	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 := planeV2.ListModuleIssues(planeV2Workspace, planeV2Project, id)
-	return planeRawResult(data, apiErr, "列出模块工作项")
-}
-
 // === Label Handlers ===
 
 func handlePlaneV2ListLabels(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListLabels(planeV2Workspace, planeV2Project)
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	data, err := planeV2Svc.ListLabels(slug, projectId)
 	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 := planeV2.GetLabel(planeV2Workspace, planeV2Project, id)
-	return planeRawResult(data, apiErr, "获取标签详情")
-}
-
 // === State Handlers ===
 
 func handlePlaneV2ListStates(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListStates(planeV2Workspace, planeV2Project)
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	data, err := planeV2Svc.ListStates(slug, projectId)
 	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 := planeV2.GetState(planeV2Workspace, planeV2Project, 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 := planeV2.ListComments(planeV2Workspace, planeV2Project, 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 := planeV2.GetComment(planeV2Workspace, planeV2Project, 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 := planeV2.ListIssueLinks(planeV2Workspace, planeV2Project, 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 := planeV2.ListIssueRelations(planeV2Workspace, planeV2Project, id)
-	return planeRawResult(data, apiErr, "列出工作项关联")
-}
-
-// === Sticky Handlers ===
-
-func handlePlaneV2ListStickies(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListStickies(planeV2Workspace)
-	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 := planeV2.GetSticky(planeV2Workspace, id)
-	return planeRawResult(data, apiErr, "获取便签详情")
-}
-
-// === Intake Handlers ===
-
-func handlePlaneV2ListIntakeIssues(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	status := req.GetString("status", "")
-	data, err := planeV2.ListIntakeIssues(planeV2Workspace, planeV2Project, 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 := planeV2.GetIntakeIssue(planeV2Workspace, planeV2Project, 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 := planeV2.GetProjectAssetURL(planeV2Workspace, planeV2Project, 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 := planeV2.ListPages(planeV2Workspace, planeV2Project)
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	data, err := planeV2Svc.ListPages(slug, projectId)
 	return planeRawResult(data, err, "列出页面")
 }
 
 func handlePlaneV2GetPage(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("page_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	pageId, err := req.RequireString("page_id")
 	if err != nil {
 		return helper.ParamError("page_id"), nil
 	}
-	data, apiErr := planeV2.GetPage(planeV2Workspace, planeV2Project, id)
+	data, apiErr := planeV2Svc.GetPage(slug, projectId, pageId)
 	return planeRawResult(data, apiErr, "获取页面详情")
 }
 
 func handlePlaneV2GetPageDescription(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("page_id")
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	pageId, err := req.RequireString("page_id")
 	if err != nil {
 		return helper.ParamError("page_id"), nil
 	}
-	data, apiErr := planeV2.GetPageDescription(planeV2Workspace, planeV2Project, id)
-	return planeRawResult(data, apiErr, "获取页面描述")
+	data, apiErr := planeV2Svc.GetPageDescription(slug, projectId, pageId)
+	return planeRawResult(data, apiErr, "获取页面描述内容")
 }
 
 // === Member Handlers ===
 
 func handlePlaneV2ListWorkspaceMembers(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListWorkspaceMembers(planeV2Workspace)
-	return planeRawResult(data, err, "列出工作空间成员")
-}
-
-func handlePlaneV2GetWorkspaceMember(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("member_id")
+	slug, err := req.RequireString("workspace_slug")
 	if err != nil {
-		return helper.ParamError("member_id"), nil
+		return helper.ParamError("workspace_slug"), nil
 	}
-	data, apiErr := planeV2.GetWorkspaceMember(planeV2Workspace, id)
-	return planeRawResult(data, apiErr, "获取工作空间成员详情")
+	data, apiErr := planeV2Svc.ListWorkspaceMembers(slug)
+	return planeRawResult(data, apiErr, "列出工作空间成员")
 }
 
 func handlePlaneV2ListProjectMembers(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	data, err := planeV2.ListProjectMembers(planeV2Workspace, planeV2Project)
+	slug, _ := req.RequireString("workspace_slug")
+	projectId, _ := req.RequireString("project_id")
+	data, err := planeV2Svc.ListProjectMembers(slug, projectId)
 	return planeRawResult(data, err, "列出项目成员")
 }
 
-func handlePlaneV2GetProjectMember(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	id, err := req.RequireString("member_id")
+// === Sticky Handlers ===
+
+func handlePlaneV2ListStickies(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	slug, err := req.RequireString("workspace_slug")
 	if err != nil {
-		return helper.ParamError("member_id"), nil
+		return helper.ParamError("workspace_slug"), nil
 	}
-	data, apiErr := planeV2.GetProjectMember(planeV2Workspace, planeV2Project, id)
-	return planeRawResult(data, apiErr, "获取项目成员详情")
+	data, apiErr := planeV2Svc.ListStickies(slug)
+	return planeRawResult(data, apiErr, "列出便签")
 }

+ 148 - 0
service/cmd/test_plane_v2/main.go

@@ -0,0 +1,148 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"fwh-toolbox-server/logger"
+	"fwh-toolbox-server/repositories/api/plane_v2"
+)
+
+// 打印模式:true=完整JSON,false=简洁摘要
+var verbose = false
+
+func main() {
+	// 初始化
+	logger.Init("dev")
+
+	slug := "au-version"
+	projectId := "b44a29fc-496b-4767-bbf1-1912f4c922ab"
+
+	host := "http://localhost:8600"
+	email := "liang_jlyx@163.com"
+	password := "Ljd@163.com"
+
+	fmt.Println("=== Plane V2 API 测试 ===")
+	fmt.Printf("Host: %s\n", host)
+	fmt.Printf("Workspace: %s\n", slug)
+	fmt.Printf("Project: %s\n", projectId)
+	fmt.Println()
+
+	// 初始化 Client
+	fmt.Print("1. 初始化 PlaneClient... ")
+	plane_v2.InitPlaneClient(host, email, password)
+	svc := plane_v2.NewPlaneService()
+	fmt.Println("✅ 登录成功")
+	fmt.Println()
+
+	// 测试 Workspace API
+	fmt.Println("=== Workspace ===")
+	testGet("ListWorkspaces", func() ([]byte, error) {
+		return svc.ListWorkspaces()
+	})
+	testGet("GetWorkspace", func() ([]byte, error) {
+		return svc.GetWorkspace(slug)
+	})
+
+	// 测试 Project API
+	fmt.Println("\n=== Project ===")
+	testGet("ListProjects", func() ([]byte, error) {
+		return svc.ListProjects(slug)
+	})
+	testGet("GetProject", func() ([]byte, error) {
+		return svc.GetProject(slug, projectId)
+	})
+
+	// 测试 Member API
+	fmt.Println("\n=== Member ===")
+	testGet("ListWorkspaceMembers", func() ([]byte, error) {
+		return svc.ListWorkspaceMembers(slug)
+	})
+	testGet("ListProjectMembers", func() ([]byte, error) {
+		return svc.ListProjectMembers(slug, projectId)
+	})
+
+	// 测试 Label API
+	fmt.Println("\n=== Label ===")
+	testGet("ListLabels", func() ([]byte, error) {
+		return svc.ListLabels(slug, projectId)
+	})
+
+	// 测试 State API
+	fmt.Println("\n=== State ===")
+	testGet("ListStates", func() ([]byte, error) {
+		return svc.ListStates(slug, projectId)
+	})
+
+	// 测试 Issue API
+	fmt.Println("\n=== Issue ===")
+	testGet("ListIssues", func() ([]byte, error) {
+		return svc.ListIssues(slug, projectId)
+	})
+
+	// 测试 Intake API
+	fmt.Println("\n=== Intake ===")
+	testGet("ListIntakeIssues (all)", func() ([]byte, error) {
+		return svc.ListIntakeIssues(slug, projectId, "-2,-1,0,1,2")
+	})
+
+	// 测试 Cycle API
+	fmt.Println("\n=== Cycle ===")
+	testGet("ListCycles", func() ([]byte, error) {
+		return svc.ListCycles(slug, projectId)
+	})
+
+	// 测试 Module API
+	fmt.Println("\n=== Module ===")
+	testGet("ListModules", func() ([]byte, error) {
+		return svc.ListModules(slug, projectId)
+	})
+
+	// 测试 Page API
+	fmt.Println("\n=== Page ===")
+	testGet("ListPages", func() ([]byte, error) {
+		return svc.ListPages(slug, projectId)
+	})
+
+	// 测试 Sticky API
+	fmt.Println("\n=== Sticky ===")
+	testGet("ListStickies", func() ([]byte, error) {
+		return svc.ListStickies(slug)
+	})
+
+	fmt.Println("\n=== 测试完成 ===")
+}
+
+func testGet(name string, fn func() ([]byte, error)) {
+	fmt.Printf("  %s... ", name)
+	data, err := fn()
+	if err != nil {
+		fmt.Printf("❌ %v\n", err)
+		return
+	}
+
+	var parsed any
+	if err := json.Unmarshal(data, &parsed); err == nil {
+		if verbose {
+			// 完整打印模式
+			pretty, _ := json.MarshalIndent(parsed, "    ", "  ")
+			fmt.Printf("✅\n    %s\n", string(pretty))
+		} else {
+			// 简洁打印模式
+			switch v := parsed.(type) {
+			case []any:
+				fmt.Printf("✅ (%d items)\n", len(v))
+			case map[string]any:
+				if results, ok := v["results"].([]any); ok {
+					fmt.Printf("✅ (%d results)\n", len(results))
+				} else {
+					fmt.Printf("✅ (object, %d keys)\n", len(v))
+				}
+			default:
+				fmt.Printf("✅ (%d bytes)\n", len(data))
+			}
+		}
+	} else {
+		fmt.Printf("✅ (%d bytes, non-JSON)\n", len(data))
+	}
+}

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

@@ -224,14 +224,16 @@ func (c *PlaneClient) ensureAuth(doRequest func() (*Response, error)) (*Response
 		return nil, err
 	}
 
-	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
-		zap.L().Warn("[PlaneClient] 认证失败,尝试重新登录", zap.Int("status", resp.StatusCode))
+	// 401 Unauthorized = session 过期,重新登录后重试
+	if resp.StatusCode == http.StatusUnauthorized {
+		zap.L().Warn("[PlaneClient] Session 过期,重新登录")
 		if loginErr := c.login(); loginErr != nil {
 			return nil, fmt.Errorf("重新登录失败: %w", loginErr)
 		}
 		return doRequest()
 	}
 
+	// 403 Forbidden = 权限不足(不是认证问题),直接返回
 	return resp, nil
 }
 

+ 6 - 6
service/repositories/api/plane_v2/service.go

@@ -21,7 +21,7 @@ func NewPlaneService() *PlaneService {
 // ============ Workspace 方法 ============
 
 func (s *PlaneService) ListWorkspaces() ([]byte, error) {
-	resp, err := s.Client.Get("/api/workspaces/")
+	resp, err := s.Client.Get("/api/users/me/workspaces/")
 	if err != nil {
 		return nil, err
 	}
@@ -706,7 +706,7 @@ func (s *PlaneService) UnarchiveModule(workspaceSlug, projectId, moduleID string
 // ============ Label 方法 ============
 
 func (s *PlaneService) ListLabels(workspaceSlug, projectId string) ([]byte, error) {
-	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/", workspaceSlug, projectId)
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", workspaceSlug, projectId)
 	resp, err := s.Client.Get(url)
 	if err != nil {
 		return nil, err
@@ -718,7 +718,7 @@ func (s *PlaneService) ListLabels(workspaceSlug, projectId string) ([]byte, erro
 }
 
 func (s *PlaneService) GetLabel(workspaceSlug, projectId, labelID string) ([]byte, error) {
-	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", workspaceSlug, projectId, labelID)
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", workspaceSlug, projectId, labelID)
 	resp, err := s.Client.Get(url)
 	if err != nil {
 		return nil, err
@@ -730,7 +730,7 @@ func (s *PlaneService) GetLabel(workspaceSlug, projectId, labelID string) ([]byt
 }
 
 func (s *PlaneService) CreateLabel(workspaceSlug, projectId, name, color string) ([]byte, error) {
-	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/", workspaceSlug, projectId)
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", workspaceSlug, projectId)
 	payload := map[string]interface{}{"name": name, "color": color}
 	resp, err := s.Client.Post(url, payload)
 	if err != nil {
@@ -743,7 +743,7 @@ func (s *PlaneService) CreateLabel(workspaceSlug, projectId, name, color string)
 }
 
 func (s *PlaneService) UpdateLabel(workspaceSlug, projectId, labelID string, data map[string]interface{}) ([]byte, error) {
-	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", workspaceSlug, projectId, labelID)
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", workspaceSlug, projectId, labelID)
 	resp, err := s.Client.Patch(url, data)
 	if err != nil {
 		return nil, err
@@ -755,7 +755,7 @@ func (s *PlaneService) UpdateLabel(workspaceSlug, projectId, labelID string, dat
 }
 
 func (s *PlaneService) DeleteLabel(workspaceSlug, projectId, labelID string) error {
-	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", workspaceSlug, projectId, labelID)
+	url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", workspaceSlug, projectId, labelID)
 	resp, err := s.Client.Delete(url)
 	if err != nil {
 		return err