@xurenxiang1219/pingcode-mcp 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/.env.example +115 -0
  2. package/.env.stdio.local.example +13 -0
  3. package/README.md +222 -0
  4. package/dist/approve-write.d.ts +3 -0
  5. package/dist/approve-write.d.ts.map +1 -0
  6. package/dist/approve-write.js +81 -0
  7. package/dist/approve-write.js.map +1 -0
  8. package/dist/auth/cimd-client-resolver.d.ts +35 -0
  9. package/dist/auth/cimd-client-resolver.d.ts.map +1 -0
  10. package/dist/auth/cimd-client-resolver.js +293 -0
  11. package/dist/auth/cimd-client-resolver.js.map +1 -0
  12. package/dist/auth/memory-store.d.ts +15 -0
  13. package/dist/auth/memory-store.d.ts.map +1 -0
  14. package/dist/auth/memory-store.js +46 -0
  15. package/dist/auth/memory-store.js.map +1 -0
  16. package/dist/auth/pingcode-oauth-client.d.ts +23 -0
  17. package/dist/auth/pingcode-oauth-client.d.ts.map +1 -0
  18. package/dist/auth/pingcode-oauth-client.js +103 -0
  19. package/dist/auth/pingcode-oauth-client.js.map +1 -0
  20. package/dist/auth/pingcode-oauth-provider.d.ts +70 -0
  21. package/dist/auth/pingcode-oauth-provider.d.ts.map +1 -0
  22. package/dist/auth/pingcode-oauth-provider.js +414 -0
  23. package/dist/auth/pingcode-oauth-provider.js.map +1 -0
  24. package/dist/auth/redis-store.d.ts +16 -0
  25. package/dist/auth/redis-store.d.ts.map +1 -0
  26. package/dist/auth/redis-store.js +54 -0
  27. package/dist/auth/redis-store.js.map +1 -0
  28. package/dist/auth/runtime.d.ts +10 -0
  29. package/dist/auth/runtime.d.ts.map +1 -0
  30. package/dist/auth/runtime.js +57 -0
  31. package/dist/auth/runtime.js.map +1 -0
  32. package/dist/auth/store.d.ts +35 -0
  33. package/dist/auth/store.d.ts.map +1 -0
  34. package/dist/auth/store.js +55 -0
  35. package/dist/auth/store.js.map +1 -0
  36. package/dist/auth/token-cipher.d.ts +7 -0
  37. package/dist/auth/token-cipher.d.ts.map +1 -0
  38. package/dist/auth/token-cipher.js +36 -0
  39. package/dist/auth/token-cipher.js.map +1 -0
  40. package/dist/config.d.ts +54 -0
  41. package/dist/config.d.ts.map +1 -0
  42. package/dist/config.js +270 -0
  43. package/dist/config.js.map +1 -0
  44. package/dist/domain/content-hash.d.ts +5 -0
  45. package/dist/domain/content-hash.d.ts.map +1 -0
  46. package/dist/domain/content-hash.js +17 -0
  47. package/dist/domain/content-hash.js.map +1 -0
  48. package/dist/domain/requirement-context.d.ts +29 -0
  49. package/dist/domain/requirement-context.d.ts.map +1 -0
  50. package/dist/domain/requirement-context.js +2 -0
  51. package/dist/domain/requirement-context.js.map +1 -0
  52. package/dist/domain/test-case-context.d.ts +56 -0
  53. package/dist/domain/test-case-context.d.ts.map +1 -0
  54. package/dist/domain/test-case-context.js +2 -0
  55. package/dist/domain/test-case-context.js.map +1 -0
  56. package/dist/domain/work-item-context.d.ts +47 -0
  57. package/dist/domain/work-item-context.d.ts.map +1 -0
  58. package/dist/domain/work-item-context.js +2 -0
  59. package/dist/domain/work-item-context.js.map +1 -0
  60. package/dist/environment.d.ts +2 -0
  61. package/dist/environment.d.ts.map +1 -0
  62. package/dist/environment.js +21 -0
  63. package/dist/environment.js.map +1 -0
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +52 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/inspect-wiki.d.ts +3 -0
  69. package/dist/inspect-wiki.d.ts.map +1 -0
  70. package/dist/inspect-wiki.js +49 -0
  71. package/dist/inspect-wiki.js.map +1 -0
  72. package/dist/logger.d.ts +9 -0
  73. package/dist/logger.d.ts.map +1 -0
  74. package/dist/logger.js +40 -0
  75. package/dist/logger.js.map +1 -0
  76. package/dist/mcp/create-server.d.ts +10 -0
  77. package/dist/mcp/create-server.d.ts.map +1 -0
  78. package/dist/mcp/create-server.js +40 -0
  79. package/dist/mcp/create-server.js.map +1 -0
  80. package/dist/mcp/pagination-guidance.d.ts +19 -0
  81. package/dist/mcp/pagination-guidance.d.ts.map +1 -0
  82. package/dist/mcp/pagination-guidance.js +36 -0
  83. package/dist/mcp/pagination-guidance.js.map +1 -0
  84. package/dist/mcp/reference-resolution.d.ts +19 -0
  85. package/dist/mcp/reference-resolution.d.ts.map +1 -0
  86. package/dist/mcp/reference-resolution.js +160 -0
  87. package/dist/mcp/reference-resolution.js.map +1 -0
  88. package/dist/mcp/tool-registry.d.ts +2 -0
  89. package/dist/mcp/tool-registry.d.ts.map +1 -0
  90. package/dist/mcp/tool-registry.js +55 -0
  91. package/dist/mcp/tool-registry.js.map +1 -0
  92. package/dist/mcp/tool-runtime.d.ts +22 -0
  93. package/dist/mcp/tool-runtime.d.ts.map +1 -0
  94. package/dist/mcp/tool-runtime.js +214 -0
  95. package/dist/mcp/tool-runtime.js.map +1 -0
  96. package/dist/mcp/tools/member-role.d.ts +8 -0
  97. package/dist/mcp/tools/member-role.d.ts.map +1 -0
  98. package/dist/mcp/tools/member-role.js +451 -0
  99. package/dist/mcp/tools/member-role.js.map +1 -0
  100. package/dist/mcp/tools/metadata.d.ts +5 -0
  101. package/dist/mcp/tools/metadata.d.ts.map +1 -0
  102. package/dist/mcp/tools/metadata.js +606 -0
  103. package/dist/mcp/tools/metadata.js.map +1 -0
  104. package/dist/mcp/tools/project-context.d.ts +4 -0
  105. package/dist/mcp/tools/project-context.d.ts.map +1 -0
  106. package/dist/mcp/tools/project-context.js +111 -0
  107. package/dist/mcp/tools/project-context.js.map +1 -0
  108. package/dist/mcp/tools/project.d.ts +7 -0
  109. package/dist/mcp/tools/project.d.ts.map +1 -0
  110. package/dist/mcp/tools/project.js +370 -0
  111. package/dist/mcp/tools/project.js.map +1 -0
  112. package/dist/mcp/tools/relation.d.ts +7 -0
  113. package/dist/mcp/tools/relation.d.ts.map +1 -0
  114. package/dist/mcp/tools/relation.js +592 -0
  115. package/dist/mcp/tools/relation.js.map +1 -0
  116. package/dist/mcp/tools/runtime-info.d.ts +4 -0
  117. package/dist/mcp/tools/runtime-info.d.ts.map +1 -0
  118. package/dist/mcp/tools/runtime-info.js +49 -0
  119. package/dist/mcp/tools/runtime-info.js.map +1 -0
  120. package/dist/mcp/tools/ship-suite.d.ts +7 -0
  121. package/dist/mcp/tools/ship-suite.d.ts.map +1 -0
  122. package/dist/mcp/tools/ship-suite.js +636 -0
  123. package/dist/mcp/tools/ship-suite.js.map +1 -0
  124. package/dist/mcp/tools/ship.d.ts +7 -0
  125. package/dist/mcp/tools/ship.d.ts.map +1 -0
  126. package/dist/mcp/tools/ship.js +617 -0
  127. package/dist/mcp/tools/ship.js.map +1 -0
  128. package/dist/mcp/tools/sync.d.ts +10 -0
  129. package/dist/mcp/tools/sync.d.ts.map +1 -0
  130. package/dist/mcp/tools/sync.js +1110 -0
  131. package/dist/mcp/tools/sync.js.map +1 -0
  132. package/dist/mcp/tools/task.d.ts +5 -0
  133. package/dist/mcp/tools/task.d.ts.map +1 -0
  134. package/dist/mcp/tools/task.js +236 -0
  135. package/dist/mcp/tools/task.js.map +1 -0
  136. package/dist/mcp/tools/test-case-bulk.d.ts +7 -0
  137. package/dist/mcp/tools/test-case-bulk.d.ts.map +1 -0
  138. package/dist/mcp/tools/test-case-bulk.js +297 -0
  139. package/dist/mcp/tools/test-case-bulk.js.map +1 -0
  140. package/dist/mcp/tools/test-case-delete.d.ts +7 -0
  141. package/dist/mcp/tools/test-case-delete.d.ts.map +1 -0
  142. package/dist/mcp/tools/test-case-delete.js +247 -0
  143. package/dist/mcp/tools/test-case-delete.js.map +1 -0
  144. package/dist/mcp/tools/test-case.d.ts +5 -0
  145. package/dist/mcp/tools/test-case.d.ts.map +1 -0
  146. package/dist/mcp/tools/test-case.js +155 -0
  147. package/dist/mcp/tools/test-case.js.map +1 -0
  148. package/dist/mcp/tools/test-library.d.ts +7 -0
  149. package/dist/mcp/tools/test-library.d.ts.map +1 -0
  150. package/dist/mcp/tools/test-library.js +302 -0
  151. package/dist/mcp/tools/test-library.js.map +1 -0
  152. package/dist/mcp/tools/wiki.d.ts +5 -0
  153. package/dist/mcp/tools/wiki.d.ts.map +1 -0
  154. package/dist/mcp/tools/wiki.js +266 -0
  155. package/dist/mcp/tools/wiki.js.map +1 -0
  156. package/dist/mcp/write-approval.d.ts +48 -0
  157. package/dist/mcp/write-approval.d.ts.map +1 -0
  158. package/dist/mcp/write-approval.js +321 -0
  159. package/dist/mcp/write-approval.js.map +1 -0
  160. package/dist/observability/metrics.d.ts +46 -0
  161. package/dist/observability/metrics.d.ts.map +1 -0
  162. package/dist/observability/metrics.js +79 -0
  163. package/dist/observability/metrics.js.map +1 -0
  164. package/dist/pingcode/access-token-provider.d.ts +5 -0
  165. package/dist/pingcode/access-token-provider.d.ts.map +1 -0
  166. package/dist/pingcode/access-token-provider.js +2 -0
  167. package/dist/pingcode/access-token-provider.js.map +1 -0
  168. package/dist/pingcode/client.d.ts +43 -0
  169. package/dist/pingcode/client.d.ts.map +1 -0
  170. package/dist/pingcode/client.js +300 -0
  171. package/dist/pingcode/client.js.map +1 -0
  172. package/dist/pingcode/errors.d.ts +20 -0
  173. package/dist/pingcode/errors.d.ts.map +1 -0
  174. package/dist/pingcode/errors.js +14 -0
  175. package/dist/pingcode/errors.js.map +1 -0
  176. package/dist/pingcode/schemas.d.ts +465 -0
  177. package/dist/pingcode/schemas.d.ts.map +1 -0
  178. package/dist/pingcode/schemas.js +450 -0
  179. package/dist/pingcode/schemas.js.map +1 -0
  180. package/dist/pingcode/token-provider.d.ts +20 -0
  181. package/dist/pingcode/token-provider.d.ts.map +1 -0
  182. package/dist/pingcode/token-provider.js +97 -0
  183. package/dist/pingcode/token-provider.js.map +1 -0
  184. package/dist/pingcode/write-response.d.ts +9 -0
  185. package/dist/pingcode/write-response.d.ts.map +1 -0
  186. package/dist/pingcode/write-response.js +39 -0
  187. package/dist/pingcode/write-response.js.map +1 -0
  188. package/dist/services.d.ts +22 -0
  189. package/dist/services.d.ts.map +1 -0
  190. package/dist/services.js +84 -0
  191. package/dist/services.js.map +1 -0
  192. package/dist/sources/metadata-source.d.ts +445 -0
  193. package/dist/sources/metadata-source.d.ts.map +1 -0
  194. package/dist/sources/metadata-source.js +167 -0
  195. package/dist/sources/metadata-source.js.map +1 -0
  196. package/dist/sources/project-context-source.d.ts +61 -0
  197. package/dist/sources/project-context-source.d.ts.map +1 -0
  198. package/dist/sources/project-context-source.js +224 -0
  199. package/dist/sources/project-context-source.js.map +1 -0
  200. package/dist/sources/relation-source.d.ts +83 -0
  201. package/dist/sources/relation-source.d.ts.map +1 -0
  202. package/dist/sources/relation-source.js +129 -0
  203. package/dist/sources/relation-source.js.map +1 -0
  204. package/dist/sources/requirement-source.d.ts +5 -0
  205. package/dist/sources/requirement-source.d.ts.map +1 -0
  206. package/dist/sources/requirement-source.js +2 -0
  207. package/dist/sources/requirement-source.js.map +1 -0
  208. package/dist/sources/ship-source.d.ts +67 -0
  209. package/dist/sources/ship-source.d.ts.map +1 -0
  210. package/dist/sources/ship-source.js +135 -0
  211. package/dist/sources/ship-source.js.map +1 -0
  212. package/dist/sources/test-case-source.d.ts +30 -0
  213. package/dist/sources/test-case-source.d.ts.map +1 -0
  214. package/dist/sources/test-case-source.js +206 -0
  215. package/dist/sources/test-case-source.js.map +1 -0
  216. package/dist/sources/wiki-source.d.ts +34 -0
  217. package/dist/sources/wiki-source.d.ts.map +1 -0
  218. package/dist/sources/wiki-source.js +136 -0
  219. package/dist/sources/wiki-source.js.map +1 -0
  220. package/dist/sources/work-item-source.d.ts +33 -0
  221. package/dist/sources/work-item-source.d.ts.map +1 -0
  222. package/dist/sources/work-item-source.js +159 -0
  223. package/dist/sources/work-item-source.js.map +1 -0
  224. package/dist/sources/write-source.d.ts +461 -0
  225. package/dist/sources/write-source.d.ts.map +1 -0
  226. package/dist/sources/write-source.js +174 -0
  227. package/dist/sources/write-source.js.map +1 -0
  228. package/dist/transports/bearer-auth.d.ts +20 -0
  229. package/dist/transports/bearer-auth.d.ts.map +1 -0
  230. package/dist/transports/bearer-auth.js +79 -0
  231. package/dist/transports/bearer-auth.js.map +1 -0
  232. package/dist/transports/http.d.ts +11 -0
  233. package/dist/transports/http.d.ts.map +1 -0
  234. package/dist/transports/http.js +203 -0
  235. package/dist/transports/http.js.map +1 -0
  236. package/dist/transports/metrics-auth.d.ts +3 -0
  237. package/dist/transports/metrics-auth.d.ts.map +1 -0
  238. package/dist/transports/metrics-auth.js +22 -0
  239. package/dist/transports/metrics-auth.js.map +1 -0
  240. package/dist/transports/stdio.d.ts +5 -0
  241. package/dist/transports/stdio.d.ts.map +1 -0
  242. package/dist/transports/stdio.js +11 -0
  243. package/dist/transports/stdio.js.map +1 -0
  244. package/dist/uat/run-label.d.ts +2 -0
  245. package/dist/uat/run-label.d.ts.map +1 -0
  246. package/dist/uat/run-label.js +12 -0
  247. package/dist/uat/run-label.js.map +1 -0
  248. package/dist/verify.d.ts +3 -0
  249. package/dist/verify.d.ts.map +1 -0
  250. package/dist/verify.js +59 -0
  251. package/dist/verify.js.map +1 -0
  252. package/dist/write/approval.d.ts +35 -0
  253. package/dist/write/approval.d.ts.map +1 -0
  254. package/dist/write/approval.js +66 -0
  255. package/dist/write/approval.js.map +1 -0
  256. package/dist/write/plan.d.ts +2 -0
  257. package/dist/write/plan.d.ts.map +1 -0
  258. package/dist/write/plan.js +17 -0
  259. package/dist/write/plan.js.map +1 -0
  260. package/dist/write/runtime.d.ts +90 -0
  261. package/dist/write/runtime.d.ts.map +1 -0
  262. package/dist/write/runtime.js +244 -0
  263. package/dist/write/runtime.js.map +1 -0
  264. package/dist/write/store.d.ts +101 -0
  265. package/dist/write/store.d.ts.map +1 -0
  266. package/dist/write/store.js +201 -0
  267. package/dist/write/store.js.map +1 -0
  268. package/package.json +58 -0
@@ -0,0 +1,617 @@
1
+ import * as z from "zod/v4";
2
+ import { PingCodeError } from "../../pingcode/errors.js";
3
+ import { contentHashSchema } from "../../domain/content-hash.js";
4
+ import { requestHash } from "../../write/plan.js";
5
+ import { executeTool, requestIdForLog } from "../tool-runtime.js";
6
+ import { loadWithKeywordFallback, paginationGuidance } from "../pagination-guidance.js";
7
+ import { resolveCandidate, resolveTeamReference, resolveUserReference } from "../reference-resolution.js";
8
+ import { approvalTokenSchema, commitWithApproval, formatPreviewToolResult, formatWriteConfirmationMessage, formatWriteToolResult, requestClientWriteConfirmation, resolveCommitVerification, } from "../write-approval.js";
9
+ const readOnlyAnnotations = {
10
+ readOnlyHint: true,
11
+ destructiveHint: false,
12
+ idempotentHint: true,
13
+ openWorldHint: true,
14
+ };
15
+ const previewAnnotations = {
16
+ readOnlyHint: true,
17
+ destructiveHint: false,
18
+ idempotentHint: false,
19
+ openWorldHint: true,
20
+ };
21
+ const commitAnnotations = {
22
+ readOnlyHint: false,
23
+ destructiveHint: true,
24
+ idempotentHint: true,
25
+ openWorldHint: true,
26
+ };
27
+ const identifier = z.string().trim().min(1).max(200);
28
+ const contentHash = contentHashSchema;
29
+ const sourceReference = z.object({
30
+ type: z.enum(["wiki", "task", "test_case", "spec", "plan", "code", "ship_idea"]),
31
+ id: identifier,
32
+ url: z.url().optional(),
33
+ content_hash: contentHash.optional(),
34
+ });
35
+ const agentSessionId = z.string().trim().min(8).max(200).optional();
36
+ const approvalContextId = z.string().uuid();
37
+ const listInput = {
38
+ keywords: z.string().trim().min(1).max(200).optional(),
39
+ page_index: z.number().int().min(0).max(100000).optional(),
40
+ page_size: z.number().int().min(1).max(100).optional(),
41
+ };
42
+ const ideaListInput = {
43
+ ...listInput,
44
+ product_id: identifier.optional().describe("按产品 ID 筛选需求"),
45
+ };
46
+ const productMemberSchema = z
47
+ .object({
48
+ id: identifier.optional().describe("成员用户/团队 ID;来自 pingcode_user_list/search 或团队目录,名称入口使用 name"),
49
+ name: identifier.optional().describe("成员名称;Tool 会按 type 从用户或团队目录解析"),
50
+ type: z.enum(["user", "user_group"]),
51
+ })
52
+ .strict()
53
+ .refine((value) => value.id !== undefined || value.name !== undefined, "成员必须提供 id 或 name")
54
+ .refine((value) => value.id === undefined || value.name === undefined, "成员 id 与 name 不得同时提供");
55
+ const productCreateFields = z
56
+ .object({
57
+ scope_type: z.enum(["organization", "user_group"]).optional(),
58
+ scope_id: identifier.optional().describe("产品所属团队 ID;仅在 scope_type=user_group 时有效,并由 Tool 从团队目录验证"),
59
+ scope_name: identifier.optional().describe("产品所属团队名称;仅在 scope_type=user_group 时有效,Tool 会解析为 scope_id"),
60
+ name: z.string().trim().min(1).max(32),
61
+ visibility: z.enum(["public", "private"]).optional(),
62
+ identifier: z
63
+ .string()
64
+ .trim()
65
+ .min(1)
66
+ .max(15)
67
+ .regex(/^[A-Z0-9_-]+$/),
68
+ description: z.string().optional(),
69
+ members: z.array(productMemberSchema).optional(),
70
+ })
71
+ .strict()
72
+ .superRefine((value, context) => {
73
+ if (value.scope_type === "user_group" && value.scope_id === undefined && value.scope_name === undefined) {
74
+ context.addIssue({ code: "custom", path: ["scope_id"], message: "scope_type=user_group 时必须提供 scope_id 或 scope_name" });
75
+ }
76
+ if (value.scope_type !== "user_group" && (value.scope_id !== undefined || value.scope_name !== undefined)) {
77
+ context.addIssue({ code: "custom", path: [value.scope_name !== undefined ? "scope_name" : "scope_id"], message: "scope_id/scope_name 仅在 scope_type=user_group 时有效" });
78
+ }
79
+ if (value.scope_id !== undefined && value.scope_name !== undefined) {
80
+ context.addIssue({ code: "custom", path: ["scope_name"], message: "scope_id 与 scope_name 不得同时提供" });
81
+ }
82
+ });
83
+ const productUpdateFields = z
84
+ .object({
85
+ name: z.string().trim().min(1).max(32).optional(),
86
+ identifier: z.string().trim().min(1).max(15).regex(/^[A-Z0-9_-]+$/).optional(),
87
+ description: z.string().optional(),
88
+ })
89
+ .strict()
90
+ .refine((value) => Object.values(value).some((item) => item !== undefined), "至少提供一個更新欄位");
91
+ const ideaCreateFields = z
92
+ .object({
93
+ product_id: identifier.optional().describe("兼容字段;由 Tool 从产品目录验证"),
94
+ product_name: identifier.optional().describe("产品名称或标识;Tool 会从 Ship 产品目录解析 product_id"),
95
+ title: z.string().trim().min(1).max(255),
96
+ assignee_id: identifier.optional().describe("需求负责人用户 ID;来自 pingcode_user_list/search,名称入口使用 assignee_name"),
97
+ assignee_name: identifier.optional().describe("负责人姓名;Tool 会通过用户目录解析 assignee_id"),
98
+ description: z.string().optional(),
99
+ suite_id: identifier.optional().describe("需求模块 ID;来自指定产品的 pingcode_ship_product_suite_list,名称入口使用 suite_name"),
100
+ suite_name: identifier.optional().describe("需求模块名称;Tool 会从指定产品的模块目录解析 suite_id"),
101
+ priority_id: identifier.optional().describe("产品级需求优先级 ID;来自指定产品优先级目录,名称入口使用 priority_name"),
102
+ priority_name: identifier.optional().describe("产品级需求优先级名称;Tool 只从指定产品的需求优先级目录解析(通常为 P0-P4),不得使用工作项的优先级名称"),
103
+ properties: z.record(z.string().trim().min(1).max(100), z.unknown()).optional(),
104
+ })
105
+ .strict()
106
+ .refine((value) => value.product_id !== undefined || value.product_name !== undefined, "product_id 或 product_name 至少提供一个")
107
+ .refine((value) => value.product_id === undefined || value.product_name === undefined, "product_id 与 product_name 不得同时提供")
108
+ .superRefine((value, context) => {
109
+ for (const [idKey, nameKey] of [["assignee_id", "assignee_name"], ["suite_id", "suite_name"], ["priority_id", "priority_name"]]) {
110
+ if (value[idKey] !== undefined && value[nameKey] !== undefined)
111
+ context.addIssue({ code: "custom", path: [nameKey], message: `${idKey} 与 ${nameKey} 不得同时提供` });
112
+ }
113
+ });
114
+ const ideaUpdateFields = z
115
+ .object({
116
+ title: z.string().trim().min(1).max(255).optional(),
117
+ assignee_id: identifier.optional().describe("需求负责人用户 ID;来自 pingcode_user_list/search,名称入口使用 assignee_name"),
118
+ assignee_name: identifier.optional(),
119
+ description: z.string().optional(),
120
+ suite_id: identifier.optional().describe("需求模块 ID;来自指定产品的 pingcode_ship_product_suite_list,名称入口使用 suite_name"),
121
+ suite_name: identifier.optional(),
122
+ priority_id: identifier.optional().describe("产品级需求优先级 ID;来自指定产品优先级目录,名称入口使用 priority_name"),
123
+ priority_name: identifier.optional().describe("产品级需求优先级名称;更新时只接受指定产品目录返回的名称,通常为 P0-P4"),
124
+ properties: z.record(z.string().trim().min(1).max(100), z.unknown()).optional(),
125
+ })
126
+ .strict()
127
+ .refine((value) => Object.values(value).some((item) => item !== undefined), "至少提供一個更新欄位")
128
+ .superRefine((value, context) => {
129
+ for (const [idKey, nameKey] of [["assignee_id", "assignee_name"], ["suite_id", "suite_name"], ["priority_id", "priority_name"]]) {
130
+ if (value[idKey] !== undefined && value[nameKey] !== undefined)
131
+ context.addIssue({ code: "custom", path: [nameKey], message: `${idKey} 与 ${nameKey} 不得同时提供` });
132
+ }
133
+ });
134
+ const productOperation = z.discriminatedUnion("action", [
135
+ z.object({
136
+ operation_id: identifier,
137
+ action: z.literal("create"),
138
+ fields: productCreateFields,
139
+ source_references: z.array(sourceReference).min(1).max(20).describe("本操作的来源引用;必须放在 operations 的每个元素内,不能放在计划顶层"),
140
+ }),
141
+ z.object({
142
+ operation_id: identifier,
143
+ action: z.literal("update"),
144
+ target_id: identifier.describe("待更新 Product/Idea 的内部 ID;来自对应列表或详情 Tool"),
145
+ expected_content_hash: contentHash,
146
+ fields: productUpdateFields,
147
+ source_references: z.array(sourceReference).min(1).max(20).describe("本操作的来源引用;必须放在 operations 的每个元素内,不能放在计划顶层"),
148
+ }),
149
+ ]);
150
+ const ideaOperation = z.discriminatedUnion("action", [
151
+ z.object({
152
+ operation_id: identifier,
153
+ action: z.literal("create"),
154
+ fields: ideaCreateFields,
155
+ source_references: z.array(sourceReference).min(1).max(20).describe("本操作的来源引用;必须放在 operations 的每个元素内,不能放在计划顶层"),
156
+ }),
157
+ z.object({
158
+ operation_id: identifier,
159
+ action: z.literal("update"),
160
+ target_id: identifier.describe("待更新 Product/Idea 的内部 ID;来自对应列表或详情 Tool"),
161
+ expected_content_hash: contentHash,
162
+ fields: ideaUpdateFields,
163
+ source_references: z.array(sourceReference).min(1).max(20).describe("本操作的来源引用;必须放在 operations 的每个元素内,不能放在计划顶层"),
164
+ }),
165
+ ]);
166
+ const productPlan = z.object({ plan_id: identifier.describe("Ship Product 同步计划 ID;由 Agent 生成,Preview 与 Commit 必须完全相同"), operations: z.array(productOperation).min(1).max(50).describe("产品操作数组;每个操作必须包含自己的 source_references") });
167
+ const ideaPlan = z.object({ plan_id: identifier.describe("Ship Idea 同步计划 ID;由 Agent 生成,Preview 与 Commit 必须完全相同"), operations: z.array(ideaOperation).min(1).max(50).describe("需求操作数组;每个操作必须包含自己的 source_references") });
168
+ export function registerShipTools(server, services, logger, writeRuntime, writeApprovalMode) {
169
+ server.registerTool("pingcode_ship_product_list", {
170
+ title: "列出 PingCode Ship 产品",
171
+ description: "读取当前凭据可访问的 Ship 产品列表。",
172
+ inputSchema: listInput,
173
+ annotations: readOnlyAnnotations,
174
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_list", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), {}, async () => {
175
+ const loaded = await loadWithKeywordFallback(input.keywords, (keywords, resetPage) => services.getShipSource().listProducts({ keywords, pageIndex: resetPage ? 0 : input.page_index, pageSize: input.page_size }));
176
+ const result = loaded.page;
177
+ return { pageSize: result.page_size, pageIndex: result.page_index, total: result.total, products: result.values, keywords: loaded.keywords, fallbackFromKeywords: loaded.fallbackFromKeywords };
178
+ }, { formatResult: (result) => formatShipListResult("Ship 产品", result.products, result.total, result.pageIndex, result.pageSize, result.keywords, result.fallbackFromKeywords) }));
179
+ server.registerTool("pingcode_ship_product_get", {
180
+ title: "读取 PingCode Ship 产品",
181
+ description: "读取一个 Ship 产品及其官方返回字段。",
182
+ inputSchema: { product_id: identifier.optional().describe("Ship Product 内部 ID;必须来自 pingcode_ship_product_list 或 Product Commit 返回结果;缺少时先调用 pingcode_ship_product_list") },
183
+ annotations: readOnlyAnnotations,
184
+ }, async ({ product_id }, extra) => executeTool(logger, "pingcode_ship_product_get", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { productId: product_id ?? "" }, async () => {
185
+ if (product_id === undefined) {
186
+ throw new PingCodeError("缺少 product_id;请先调用 pingcode_ship_product_list 获取产品候选并等待用户选择", "BAD_REQUEST");
187
+ }
188
+ const product = await services.getShipSource().getProduct(product_id);
189
+ return { product, source: sourceMeta(product) };
190
+ }, { formatResult: (result) => formatShipResourceResult("Ship 产品", result.product, result.source) }));
191
+ server.registerTool("pingcode_ship_idea_list", {
192
+ title: "列出 PingCode Ship 需求",
193
+ description: "读取当前凭据可访问的 Ship 需求(Idea)列表。",
194
+ inputSchema: ideaListInput,
195
+ annotations: readOnlyAnnotations,
196
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_idea_list", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), {}, async () => {
197
+ const loaded = await loadWithKeywordFallback(input.keywords, (keywords, resetPage) => services.getShipSource().listIdeas({ keywords, productId: input.product_id, pageIndex: resetPage ? 0 : input.page_index, pageSize: input.page_size }));
198
+ const result = loaded.page;
199
+ return { pageSize: result.page_size, pageIndex: result.page_index, total: result.total, ideas: result.values, keywords: loaded.keywords, fallbackFromKeywords: loaded.fallbackFromKeywords };
200
+ }, { formatResult: (result) => formatShipListResult("Ship 需求", result.ideas, result.total, result.pageIndex, result.pageSize, result.keywords, result.fallbackFromKeywords) }));
201
+ server.registerTool("pingcode_ship_product_idea_list", {
202
+ title: "列出 PingCode 产品需求",
203
+ description: "读取指定 Ship 产品下的需求(Idea)列表。此 Tool 只读,不创建、删除或修改需求。",
204
+ inputSchema: {
205
+ product_id: identifier.optional().describe("产品 ID;必须来自 pingcode_ship_product_list 或 Product Commit 返回结果;缺少时先调用 pingcode_ship_product_list"),
206
+ keywords: z.string().trim().min(1).max(200).optional(),
207
+ page_index: z.number().int().min(0).max(100000).optional(),
208
+ page_size: z.number().int().min(1).max(100).optional(),
209
+ },
210
+ annotations: readOnlyAnnotations,
211
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_idea_list", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { productId: input.product_id ?? "" }, async () => {
212
+ if (input.product_id === undefined) {
213
+ throw new PingCodeError("缺少 product_id;请先调用 pingcode_ship_product_list 获取产品候选并等待用户选择", "BAD_REQUEST");
214
+ }
215
+ const loaded = await loadWithKeywordFallback(input.keywords, (keywords, resetPage) => services.getShipSource().listIdeas({ productId: input.product_id, keywords, pageIndex: resetPage ? 0 : input.page_index, pageSize: input.page_size }));
216
+ const result = loaded.page;
217
+ return { pageSize: result.page_size, pageIndex: result.page_index, total: result.total, ideas: result.values, keywords: loaded.keywords, fallbackFromKeywords: loaded.fallbackFromKeywords };
218
+ }, { formatResult: (result) => formatShipListResult("产品需求", result.ideas, result.total, result.pageIndex, result.pageSize, result.keywords, result.fallbackFromKeywords) }));
219
+ server.registerTool("pingcode_ship_idea_get", {
220
+ title: "读取 PingCode Ship 需求",
221
+ description: "读取一个 Ship 需求(Idea)及其官方返回字段。",
222
+ inputSchema: { idea_id: identifier.optional().describe("Ship Idea 内部 ID;必须来自 pingcode_ship_idea_list、pingcode_ship_product_idea_list 或 Idea Commit 返回结果;缺少时先调用对应列表 Tool") },
223
+ annotations: readOnlyAnnotations,
224
+ }, async ({ idea_id }, extra) => executeTool(logger, "pingcode_ship_idea_get", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { ideaId: idea_id ?? "" }, async () => {
225
+ if (idea_id === undefined) {
226
+ throw new PingCodeError("缺少 idea_id;请先调用 pingcode_ship_idea_list 或 pingcode_ship_product_idea_list 获取需求候选并等待用户选择", "BAD_REQUEST");
227
+ }
228
+ const idea = await services.getShipSource().getIdea(idea_id);
229
+ return { idea, source: sourceMeta(idea) };
230
+ }, { formatResult: (result) => formatShipResourceResult("Ship 需求", result.idea, result.source) }));
231
+ registerProductSyncTools(server, services, logger, writeRuntime, writeApprovalMode);
232
+ registerIdeaSyncTools(server, services, logger, writeRuntime, writeApprovalMode);
233
+ }
234
+ function registerProductSyncTools(server, services, logger, runtime, writeApprovalMode) {
235
+ server.registerTool("pingcode_ship_product_sync_preview", {
236
+ title: "预览 PingCode Ship 产品同步",
237
+ description: "验证 Ship 产品创建/更新计划并返回差异,不写入 PingCode。创建的最小输入是 plan_id、operations[action=create]、fields.name、fields.identifier、fields.scope_type(可选)以及 operation 内的 source_references;scope_type=user_group 时还必须提供 scope_name 或 scope_id,正常流程使用 scope_name 由团队目录解析。每个 operations 元素必须是 JSON 对象;fields 必须是 JSON 对象而不是字符串,source_references 是 operation 内与 fields 同级的必填数组。正常用户回复只展示目标产品、操作、字段差异和警告;Commit 是否显示 MCP Client 确认由 PINGCODE_WRITE_CONFIRMATION 决定,不得先在聊天中重复询问。requestHash、approvalContextId、sessionBinding、evidenceHash、agent_session_id 和 idempotency_key 仅供 Agent 内部传递,不得在正常用户回复中展示或要求用户复制。Product 创建支持官方 endpoint 的 scope_type、scope_id、name、visibility、identifier、description 和 members;name 和 identifier 必填,scope_type=user_group 时必须提供并解析真实团队。",
238
+ inputSchema: {
239
+ ...productPlan.shape,
240
+ plan_id: productPlan.shape.plan_id.optional().describe("业务上必填;省略仅用于返回缺参引导"),
241
+ operations: productPlan.shape.operations.optional().describe("业务上必填;省略仅用于返回缺参引导"),
242
+ agent_session_id: agentSessionId,
243
+ },
244
+ annotations: previewAnnotations,
245
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_sync_preview", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id ?? "" }, () => {
246
+ if (input.plan_id === undefined || input.operations === undefined) {
247
+ throw new PingCodeError("缺少 plan_id 或 operations;请补充 Ship Product 同步计划后重试 Preview", "BAD_REQUEST");
248
+ }
249
+ return previewShipPlan("ship_product", { plan_id: input.plan_id, operations: input.operations }, services, runtime, input.agent_session_id, extra.authInfo);
250
+ }, { formatResult: formatPreviewToolResult }));
251
+ server.registerTool("pingcode_ship_product_sync_commit", {
252
+ title: "提交 PingCode Ship 产品同步",
253
+ description: "依已核准的 Preview 建立或更新 Ship 产品。Commit 必须原样复用 Preview 请求中的 plan_id 和 operations;不要复制 Preview 响应里的 camelCase operations 展示对象。产品创建成功并回读确认后,Agent 应询问用户是否将产品的 id、identifier、name 保存到当前工作区;只有用户明确同意才调用 pingcode_project_context_save。是否要求当前 MCP Client 确认由 PINGCODE_WRITE_CONFIRMATION 决定,并原样使用验证信息和唯一幂等键。",
254
+ inputSchema: commitInput(productPlan, writeApprovalMode),
255
+ annotations: commitAnnotations,
256
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_sync_commit", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id }, async () => {
257
+ runtime.assertMcpWriteScope(extra.authInfo?.scopes);
258
+ const verification = await resolveCommitVerification(runtime, { previewId: input.preview_id, approvalContextId: input.approval_context_id, idempotencyKey: input.idempotency_key, requestHash: requestHash({ plan_id: input.plan_id, operations: input.operations }), agentSessionId: input.agent_session_id, authInfo: extra.authInfo }, "ship_product");
259
+ const resolvedInput = { ...input, request_hash: verification.requestHash, approval_context_id: verification.approvalContextId, session_binding: verification.sessionBinding, evidence_hash: verification.evidenceHash, idempotency_key: verification.idempotencyKey };
260
+ await requestClientWriteConfirmation(server, logger, writeApprovalMode, input.approval_token, shipCommitConfirmationMessage("ship_product", resolvedInput), runtime.requiresConfirmation(input.operations.map((operation) => operation.action)));
261
+ return commitShipPlan("ship_product", resolvedInput, services, runtime, writeApprovalMode, extra.authInfo);
262
+ }, { formatResult: formatWriteToolResult }));
263
+ }
264
+ function registerIdeaSyncTools(server, services, logger, runtime, writeApprovalMode) {
265
+ server.registerTool("pingcode_ship_idea_sync_preview", {
266
+ title: "预览 PingCode Ship 需求同步",
267
+ description: "验证 Ship 需求(Idea)创建/更新计划并返回差异,不写入 PingCode。创建的最小输入是 plan_id、operations[action=create]、fields.title、fields.product_id 或 product_name(二选一)以及 operation 内的 source_references。用户未主动指定产品时,先调用 pingcode_project_context_get;上下文中的 Ship 产品只能作为候选展示并询问是否复用,用户同意后才传入 product_name 或 product_id;没有上下文或用户拒绝时调用 pingcode_ship_product_list。正常用户回复只展示目标产品、操作、字段差异和警告;Commit 是否显示 MCP Client 确认由 PINGCODE_WRITE_CONFIRMATION 决定,不得先在聊天中重复询问。requestHash、approvalContextId、sessionBinding、evidenceHash、agent_session_id 和 idempotency_key 仅供 Agent 内部传递,不得在正常用户回复中展示或要求用户复制。建立时 product_id/product_name 和 title 必填且不能同时提供。",
268
+ inputSchema: {
269
+ ...ideaPlan.shape,
270
+ plan_id: ideaPlan.shape.plan_id.optional().describe("业务上必填;省略仅用于返回缺参引导"),
271
+ operations: ideaPlan.shape.operations.optional().describe("业务上必填;省略仅用于返回缺参引导"),
272
+ agent_session_id: agentSessionId,
273
+ },
274
+ annotations: previewAnnotations,
275
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_idea_sync_preview", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id ?? "" }, () => {
276
+ if (input.plan_id === undefined || input.operations === undefined) {
277
+ throw new PingCodeError("缺少 plan_id 或 operations;请补充 Ship Idea 同步计划后重试 Preview", "BAD_REQUEST");
278
+ }
279
+ return previewShipPlan("ship_idea", { plan_id: input.plan_id, operations: input.operations }, services, runtime, input.agent_session_id, extra.authInfo);
280
+ }, { formatResult: formatPreviewToolResult }));
281
+ server.registerTool("pingcode_ship_idea_sync_commit", {
282
+ title: "提交 PingCode Ship 需求同步",
283
+ description: "依已核准的 Preview 建立或更新 Ship 需求(Idea)。Commit 必须原样复用 Preview 请求中的 plan_id 和 operations;不要复制 Preview 响应里的 camelCase operations 展示对象。是否要求当前 MCP Client 确认由 PINGCODE_WRITE_CONFIRMATION 决定,并原样使用验证信息和唯一幂等键。",
284
+ inputSchema: commitInput(ideaPlan, writeApprovalMode),
285
+ annotations: commitAnnotations,
286
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_idea_sync_commit", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id }, async () => {
287
+ runtime.assertMcpWriteScope(extra.authInfo?.scopes);
288
+ const verification = await resolveCommitVerification(runtime, { previewId: input.preview_id, approvalContextId: input.approval_context_id, idempotencyKey: input.idempotency_key, requestHash: requestHash({ plan_id: input.plan_id, operations: input.operations }), agentSessionId: input.agent_session_id, authInfo: extra.authInfo }, "ship_idea");
289
+ const resolvedInput = { ...input, request_hash: verification.requestHash, approval_context_id: verification.approvalContextId, session_binding: verification.sessionBinding, evidence_hash: verification.evidenceHash, idempotency_key: verification.idempotencyKey };
290
+ await requestClientWriteConfirmation(server, logger, writeApprovalMode, input.approval_token, shipCommitConfirmationMessage("ship_idea", resolvedInput), runtime.requiresConfirmation(input.operations.map((operation) => operation.action)));
291
+ return commitShipPlan("ship_idea", resolvedInput, services, runtime, writeApprovalMode, extra.authInfo);
292
+ }, { formatResult: formatWriteToolResult }));
293
+ }
294
+ function commitInput(plan, writeApprovalMode) {
295
+ return {
296
+ ...plan.shape,
297
+ agent_session_id: agentSessionId,
298
+ preview_id: approvalContextId.optional().describe("可选。省略时 Server 依相同 plan_id 与 operations 自动匹配刚才的 Preview,并据此恢复 request hash、evidence hash、session binding 和幂等边界;仅当客户端确实读到 Preview 的 approvalContextId 时才需要传入"),
299
+ approval_context_id: approvalContextId.optional().describe("已弃用,等同 preview_id;新调用只传 preview_id"),
300
+ approval_token: approvalTokenSchema(writeApprovalMode),
301
+ idempotency_key: z.string().trim().min(16).max(200).optional(),
302
+ };
303
+ }
304
+ async function previewShipPlan(resourceType, input, services, runtime, agentSession, authInfo) {
305
+ const resolvedInput = await resolveShipPlanReferences(resourceType, input, services);
306
+ if (resourceType === "ship_product") {
307
+ await assertNoShipProductCreateConflict(resolvedInput, services);
308
+ }
309
+ const operations = [];
310
+ for (const operation of resolvedInput.operations) {
311
+ if (operation.action === "create") {
312
+ operations.push({ operationId: operation.operation_id, action: "create", targetId: null, changes: changesFromCreate(operation.fields), sourceReferences: operation.source_references });
313
+ continue;
314
+ }
315
+ const targetId = operation.target_id;
316
+ const expectedHash = operation.expected_content_hash;
317
+ if (!targetId || !expectedHash) {
318
+ throw new PingCodeError("Ship 更新操作缺少 target_id 或 expected_content_hash", "BAD_REQUEST");
319
+ }
320
+ const current = resourceType === "ship_product"
321
+ ? await services.getShipSource().getProduct(targetId)
322
+ : await services.getShipSource().getIdea(targetId);
323
+ assertContentHash(expectedHash, current);
324
+ operations.push({ operationId: operation.operation_id, action: "update", targetId, changes: selectedChanges(current, operation.fields), sourceReferences: operation.source_references });
325
+ }
326
+ const request = { plan_id: input.plan_id, operations: input.operations };
327
+ const evidence = requestHash(operations);
328
+ const approval = await runtime.createApprovalContext({ resourceType, requestHash: requestHash(request), evidenceHash: evidence, agentSessionId: agentSession, authInfo });
329
+ return { planId: input.plan_id, requestHash: requestHash(request), approvalContextId: approval.approvalContextId, sessionBinding: approval.sessionBinding, evidenceHash: evidence, approvalExpiresAt: approval.expiresAt, counts: { creates: input.operations.filter((item) => item.action === "create").length, updates: input.operations.filter((item) => item.action === "update").length, relations: 0 }, operations };
330
+ }
331
+ async function commitShipPlan(resourceType, input, services, runtime, writeApprovalMode, authInfo) {
332
+ const request = { plan_id: input.plan_id, operations: input.operations };
333
+ if (input.request_hash !== requestHash(request)) {
334
+ throw new PingCodeError("Ship Commit 的计划 Hash 与 Preview 不一致", "CONFLICT");
335
+ }
336
+ const resolvedInput = await resolveShipPlanReferences(resourceType, input, services);
337
+ const committed = await commitWithApproval(runtime, writeApprovalMode, { resourceType, requestHash: input.request_hash, evidenceHash: input.evidence_hash, approvalContextId: input.approval_context_id, sessionBinding: input.session_binding, agentSessionId: input.agent_session_id, approvalToken: input.approval_token, idempotencyKey: input.idempotency_key, authInfo, actions: input.operations.map((operation) => operation.action), action: async () => {
338
+ if (resourceType === "ship_product") {
339
+ await assertNoShipProductCreateConflict(resolvedInput, services);
340
+ }
341
+ const resources = [];
342
+ for (const operation of resolvedInput.operations) {
343
+ if (operation.action === "update" && !operation.target_id) {
344
+ throw new PingCodeError("Ship 更新操作缺少 target_id", "BAD_REQUEST");
345
+ }
346
+ const value = resourceType === "ship_product"
347
+ ? operation.action === "create"
348
+ ? await services.getShipSource().createProduct(toShipProductWriteFields(parseResolvedProductCreateFields(operation.fields)))
349
+ : await services.getShipSource().updateProduct(operation.target_id, toShipProductWriteFields(productUpdateFields.parse(operation.fields)))
350
+ : operation.action === "create"
351
+ ? await services.getShipSource().createIdea(toShipIdeaWriteFields(parseResolvedIdeaCreateFields(operation.fields)))
352
+ : await services.getShipSource().updateIdea(operation.target_id, toShipIdeaWriteFields(parseResolvedIdeaUpdateFields(operation.fields)));
353
+ const title = "name" in value ? String(value.name) : String(value.title);
354
+ resources.push({ operationId: operation.operation_id, action: operation.action, id: value.id, identifier: value.identifier, title, url: value.url, relationIds: [] });
355
+ }
356
+ return resources;
357
+ } });
358
+ return { planId: input.plan_id, replayed: committed.replayed, approvedBy: committed.approval.approvedBy, auditId: committed.auditId, resources: committed.result };
359
+ }
360
+ async function assertNoShipProductCreateConflict(input, services) {
361
+ const creates = input.operations.filter((operation) => operation.action === "create");
362
+ if (creates.length === 0)
363
+ return;
364
+ const products = await listAllProducts(services);
365
+ const seenIdentifiers = new Set();
366
+ const seenNames = new Set();
367
+ for (const operation of creates) {
368
+ const fields = operation.fields;
369
+ const identifier = asString(fields.identifier);
370
+ const name = asString(fields.name);
371
+ if (identifier !== undefined) {
372
+ const normalized = identifier.toLocaleLowerCase();
373
+ if (seenIdentifiers.has(normalized)) {
374
+ throw new PingCodeError(`同一 Preview 中重复创建 Ship 产品 identifier:${identifier}`, "CONFLICT");
375
+ }
376
+ seenIdentifiers.add(normalized);
377
+ }
378
+ if (name !== undefined) {
379
+ const normalized = name.toLocaleLowerCase();
380
+ if (seenNames.has(normalized)) {
381
+ throw new PingCodeError(`同一 Preview 中重复创建 Ship 产品名称:${name}`, "CONFLICT");
382
+ }
383
+ seenNames.add(normalized);
384
+ }
385
+ const matching = products.find((product) => (identifier !== undefined && product.identifier?.toLocaleLowerCase() === identifier.toLocaleLowerCase())
386
+ || (name !== undefined && product.name.toLocaleLowerCase() === name.toLocaleLowerCase()));
387
+ if (matching) {
388
+ throw new PingCodeError(`Ship 产品名称或 identifier 已存在:${matching.name} (${matching.identifier});请换用新的名称和 identifier`, "CONFLICT");
389
+ }
390
+ }
391
+ }
392
+ async function resolveShipPlanReferences(resourceType, input, services) {
393
+ const products = input.operations.some((operation) => resourceType === "ship_idea" && operation.action === "create" && (typeof operation.fields.product_id === "string" || typeof operation.fields.product_name === "string"))
394
+ ? await listAllProducts(services)
395
+ : [];
396
+ const operations = [];
397
+ for (const operation of input.operations) {
398
+ const fields = { ...operation.fields };
399
+ if (resourceType === "ship_product" && operation.action === "create") {
400
+ if (fields.scope_type === "user_group") {
401
+ fields.scope_id = await resolveTeamReference("产品归属团队", asString(fields.scope_id), asString(fields.scope_name), services);
402
+ }
403
+ if (Array.isArray(fields.members)) {
404
+ fields.members = await Promise.all(fields.members.map(async (member) => {
405
+ const value = asRecord(member);
406
+ const type = value.type === "user_group" ? "user_group" : "user";
407
+ const id = type === "user_group"
408
+ ? await resolveTeamReference("产品团队成员", asString(value.id), asString(value.name), services)
409
+ : await resolveUserReference("产品成员", asString(value.id), asString(value.name), services);
410
+ if (id === undefined)
411
+ throw new PingCodeError("产品成员尚未解析为有效目录资源", "BAD_REQUEST");
412
+ return { id, type };
413
+ }));
414
+ }
415
+ }
416
+ if (resourceType === "ship_idea") {
417
+ let productId = asString(fields.product_id);
418
+ if (operation.action === "create") {
419
+ productId = resolveCandidate("产品", productId, asString(fields.product_name), products);
420
+ fields.product_id = productId;
421
+ }
422
+ else if (productId === undefined && operation.target_id !== undefined) {
423
+ const current = await services.getShipSource().getIdea(operation.target_id);
424
+ productId = extractResourceId(current.product);
425
+ }
426
+ if (typeof fields.assignee_id === "string" || typeof fields.assignee_name === "string") {
427
+ fields.assignee_id = await resolveUserReference("需求负责人", asString(fields.assignee_id), asString(fields.assignee_name), services);
428
+ }
429
+ if (typeof fields.suite_id === "string" || typeof fields.suite_name === "string") {
430
+ if (!productId)
431
+ throw new PingCodeError("需求模块解析需要先确定产品", "BAD_REQUEST");
432
+ const suites = await listAllShipSuites(productId, services);
433
+ fields.suite_id = resolveCandidate("需求模块", asString(fields.suite_id), asString(fields.suite_name), suites);
434
+ }
435
+ if (typeof fields.priority_id === "string" || typeof fields.priority_name === "string") {
436
+ if (!productId)
437
+ throw new PingCodeError("需求优先级解析需要先确定产品", "BAD_REQUEST");
438
+ const priorities = await listAllShipIdeaPriorities(productId, services);
439
+ fields.priority_id = resolveCandidate("需求优先级", asString(fields.priority_id), asString(fields.priority_name), priorities);
440
+ }
441
+ }
442
+ operations.push({ ...operation, fields });
443
+ }
444
+ return { ...input, operations };
445
+ }
446
+ async function listAllProducts(services) {
447
+ const values = [];
448
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
449
+ const page = await services.getShipSource().listProducts({ pageIndex, pageSize: 100 });
450
+ values.push(...page.values.map((product) => ({ id: product.id, name: product.name, identifier: product.identifier })));
451
+ if (values.length >= page.total || page.values.length === 0)
452
+ break;
453
+ }
454
+ return values;
455
+ }
456
+ async function listAllShipSuites(productId, services) {
457
+ const values = [];
458
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
459
+ const page = await services.getShipSource().listSuites(productId, { pageIndex, pageSize: 100 });
460
+ values.push(...page.values.map((suite) => ({ id: suite.id, name: suite.name })));
461
+ if (values.length >= page.total || page.values.length === 0)
462
+ break;
463
+ }
464
+ return values;
465
+ }
466
+ async function listAllShipIdeaPriorities(productId, services) {
467
+ const values = [];
468
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
469
+ const page = await services.getMetadataSource().listShipIdeaPriorities(productId, { pageIndex, pageSize: 100 });
470
+ values.push(...page.values.map((priority) => ({ id: priority.id, name: priority.name })));
471
+ if (values.length >= page.total || page.values.length === 0)
472
+ break;
473
+ }
474
+ return values;
475
+ }
476
+ function asRecord(value) {
477
+ return typeof value === "object" && value !== null ? value : {};
478
+ }
479
+ function asString(value) {
480
+ return typeof value === "string" ? value : undefined;
481
+ }
482
+ function extractResourceId(value) {
483
+ const record = asRecord(value);
484
+ return asString(record.id);
485
+ }
486
+ function toShipProductWriteFields(fields) {
487
+ return {
488
+ ...("scope_type" in fields && fields.scope_type !== undefined ? { scopeType: fields.scope_type } : {}),
489
+ ...("scope_id" in fields && fields.scope_id !== undefined ? { scopeId: fields.scope_id } : {}),
490
+ ...(typeof fields.name === "string" ? { name: fields.name } : {}),
491
+ ...("visibility" in fields && fields.visibility !== undefined ? { visibility: fields.visibility } : {}),
492
+ ...(typeof fields.identifier === "string" ? { identifier: fields.identifier } : {}),
493
+ ...(typeof fields.description === "string" ? { description: fields.description } : {}),
494
+ ...("members" in fields && fields.members !== undefined ? { members: resolvedShipMembers(fields.members) } : {}),
495
+ };
496
+ }
497
+ function parseResolvedProductCreateFields(fields) {
498
+ const normalized = { ...fields };
499
+ // Name references remain in the Preview for readability. Once resolved, the
500
+ // write schema must receive only the corresponding server-side ID.
501
+ if (normalized.scope_id !== undefined)
502
+ delete normalized.scope_name;
503
+ return productCreateFields.parse(normalized);
504
+ }
505
+ function parseResolvedIdeaCreateFields(fields) {
506
+ const normalized = { ...fields };
507
+ stripResolvedIdeaReferenceNames(normalized);
508
+ return ideaCreateFields.parse(normalized);
509
+ }
510
+ function parseResolvedIdeaUpdateFields(fields) {
511
+ const normalized = { ...fields };
512
+ stripResolvedIdeaReferenceNames(normalized);
513
+ return ideaUpdateFields.parse(normalized);
514
+ }
515
+ function stripResolvedIdeaReferenceNames(fields) {
516
+ if (fields.product_id !== undefined)
517
+ delete fields.product_name;
518
+ if (fields.assignee_id !== undefined)
519
+ delete fields.assignee_name;
520
+ if (fields.suite_id !== undefined)
521
+ delete fields.suite_name;
522
+ if (fields.priority_id !== undefined)
523
+ delete fields.priority_name;
524
+ }
525
+ function resolvedShipMembers(members) {
526
+ return members.map((member) => {
527
+ if (member.id === undefined)
528
+ throw new PingCodeError("产品成员尚未解析为有效目录资源", "BAD_REQUEST");
529
+ return { id: member.id, type: member.type };
530
+ });
531
+ }
532
+ function toShipIdeaWriteFields(fields) {
533
+ return {
534
+ ...("product_id" in fields && fields.product_id !== undefined ? { productId: fields.product_id } : {}),
535
+ ...(typeof fields.title === "string" ? { title: fields.title } : {}),
536
+ ...("assignee_id" in fields && fields.assignee_id !== undefined ? { assigneeId: fields.assignee_id } : {}),
537
+ ...(typeof fields.description === "string" ? { description: fields.description } : {}),
538
+ ...("suite_id" in fields && fields.suite_id !== undefined ? { suiteId: fields.suite_id } : {}),
539
+ ...("priority_id" in fields && fields.priority_id !== undefined ? { priorityId: fields.priority_id } : {}),
540
+ ...(fields.properties !== undefined ? { properties: fields.properties } : {}),
541
+ };
542
+ }
543
+ function shipCommitConfirmationMessage(resourceType, input) {
544
+ const creates = input.operations.filter((operation) => operation.action === "create").length;
545
+ const updates = input.operations.length - creates;
546
+ const resources = input.operations.map((operation) => String(operation.fields.name ?? operation.fields.title ?? "已确认资源"));
547
+ const productIds = resourceType === "ship_idea"
548
+ ? [...new Set(input.operations.flatMap((operation) => {
549
+ const productId = operation.fields.product_id;
550
+ return typeof productId === "string" ? [productId] : [];
551
+ }))]
552
+ : [];
553
+ return formatWriteConfirmationMessage(resourceType === "ship_product" ? "即将写入 PingCode Ship 产品" : "即将写入 PingCode Ship 需求", [
554
+ ...(productIds.length > 0 ? [`目标产品:${input.operations.map((operation) => operation.fields.product_name ?? "已确认产品").filter((value, index, values) => values.indexOf(value) === index).join("、")}`] : []),
555
+ `操作:创建 ${creates} 个,更新 ${updates} 个`,
556
+ `资源:${resources.slice(0, 5).join("、")}${resources.length > 5 ? `,另有 ${resources.length - 5} 个` : ""}`,
557
+ ]);
558
+ }
559
+ function formatShipListResult(label, resources, total, pageIndex = 0, pageSize = Math.max(resources.length, 1), keywords, fallbackFromKeywords) {
560
+ const lines = [`${label}列表:共 ${total} 个`];
561
+ for (const resource of resources.slice(0, 20)) {
562
+ const item = asVisibleResource(resource);
563
+ lines.push(`- ${item.name}${item.identifier ? ` (${item.identifier})` : ""}${item.id ? `,ID:${item.id}` : ""}`);
564
+ }
565
+ if (total > resources.length) {
566
+ lines.push(`- 其余 ${total - resources.length} 个资源请使用分页继续读取`);
567
+ }
568
+ lines.push(...paginationGuidance({
569
+ pageIndex,
570
+ pageSize,
571
+ total,
572
+ returned: resources.length,
573
+ ...(keywords === undefined ? {} : { keywords }),
574
+ searchable: true,
575
+ ...(fallbackFromKeywords === undefined ? {} : { fallbackFromKeywords }),
576
+ }));
577
+ return lines.join("\n");
578
+ }
579
+ function formatShipResourceResult(label, resource, source) {
580
+ const item = asVisibleResource(resource);
581
+ return [
582
+ `${label}已读取`,
583
+ `名称:${item.name}`,
584
+ item.identifier ? `标识:${item.identifier}` : null,
585
+ item.id ? `资源 ID:${item.id}` : null,
586
+ source.contentHash ? `contentHash:${source.contentHash}` : null,
587
+ ].filter((line) => line !== null).join("\n");
588
+ }
589
+ function asVisibleResource(resource) {
590
+ if (typeof resource !== "object" || resource === null) {
591
+ return { id: null, identifier: null, name: "未命名资源" };
592
+ }
593
+ const value = resource;
594
+ return {
595
+ id: typeof value.id === "string" ? value.id : null,
596
+ identifier: typeof value.identifier === "string" ? value.identifier : null,
597
+ name: typeof value.name === "string"
598
+ ? value.name
599
+ : typeof value.title === "string" ? value.title : "未命名资源",
600
+ };
601
+ }
602
+ function changesFromCreate(fields) {
603
+ return Object.fromEntries(Object.entries(fields).map(([field, after]) => [field, { before: null, after }]));
604
+ }
605
+ function selectedChanges(current, fields) {
606
+ return Object.fromEntries(Object.entries(fields).map(([field, after]) => [field, { before: current[field] ?? null, after }]));
607
+ }
608
+ function assertContentHash(expected, current) {
609
+ const actual = requestHash(current);
610
+ if (expected !== actual) {
611
+ throw new PingCodeError(`Ship 资源内容已变化,无法安全覆盖(expected=${expected}, actual=${actual})`, "CONFLICT");
612
+ }
613
+ }
614
+ function sourceMeta(value) {
615
+ return { contentHash: requestHash(value), retrievedAt: new Date().toISOString() };
616
+ }
617
+ //# sourceMappingURL=ship.js.map