@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,592 @@
1
+ import * as z from "zod/v4";
2
+ import { PingCodeError } from "../../pingcode/errors.js";
3
+ import { contentHashSchema } from "../../domain/content-hash.js";
4
+ import { isSupportedRelationWritePair, relationPrincipalResourceId, relationTargetResourceId, traceableResourceTypes, } from "../../sources/relation-source.js";
5
+ import { requestHash as calculateRequestHash } from "../../write/plan.js";
6
+ import { executeTool, requestIdForLog } from "../tool-runtime.js";
7
+ import { paginationGuidance } from "../pagination-guidance.js";
8
+ import { approvalTokenSchema, commitWithApproval, formatPreviewToolResult, formatWriteConfirmationMessage, formatWriteToolResult, requestClientWriteConfirmation, resolveCommitVerification, } from "../write-approval.js";
9
+ const identifier = z.string().trim().min(1).max(200);
10
+ const optionalIdentifier = z.preprocess((value) => typeof value === "string" && value.trim().length === 0 ? undefined : value, identifier.optional());
11
+ const contentHash = contentHashSchema;
12
+ const approvalContextId = z.string().uuid();
13
+ const agentSessionId = z.string().trim().min(8).max(200).optional();
14
+ const relationTypeSchema = z.enum(traceableResourceTypes).describe("官方 Relation 资源类型;读取可见 ticket、test_plan、test_run,写入仍受 MCP 受控组合限制");
15
+ const relationListPrincipalTypeSchema = z.enum(traceableResourceTypes).describe("官方 Relation 查询主体类型;必须与 principal_id、target_type 组合使用");
16
+ const relationListTargetTypeSchema = z.enum(["ticket", "work_item", "test_case", "idea", "page"]).describe("官方 Relation 查询目标类型;必须与 principal_type、principal_id 组合使用");
17
+ const writablePrincipalTypeSchema = z.enum(["idea", "work_item", "test_case"]);
18
+ const sourceReferenceSchema = z.object({
19
+ type: z.enum(["wiki", "task", "test_case", "spec", "plan", "code", "project", "ship_idea"]),
20
+ id: identifier,
21
+ content_hash: contentHash.optional(),
22
+ }).strict();
23
+ const relationCreateFieldsSchema = z.object({
24
+ principal_type: writablePrincipalTypeSchema,
25
+ principal_id: optionalIdentifier.describe("主体 ID;与 principal_name 二选一"),
26
+ principal_name: optionalIdentifier.describe("主体名称或标识;与 principal_id 二选一"),
27
+ target_type: relationTypeSchema,
28
+ target_id: optionalIdentifier.describe("目标 ID;与 target_name 二选一"),
29
+ target_name: optionalIdentifier.describe("目标名称或标识;与 target_id 二选一"),
30
+ }).strict().superRefine((value, context) => {
31
+ const requireExactlyOne = (left, right, path, message) => {
32
+ if ((left === undefined) === (right === undefined)) {
33
+ context.addIssue({ code: "custom", path: [path], message });
34
+ }
35
+ };
36
+ requireExactlyOne(value.principal_id, value.principal_name, "principal_id", "principal_id 与 principal_name 必须且只能提供一个");
37
+ requireExactlyOne(value.target_id, value.target_name, "target_id", "target_id 与 target_name 必须且只能提供一个");
38
+ });
39
+ // Keep delete inputs in one object. A union of eight mutually-exclusive objects
40
+ // produces `not: {}` in the public JSON Schema, which some MCP clients reject
41
+ // before the request reaches the server.
42
+ const relationDeleteFieldsSchema = z.object({
43
+ relation_id: optionalIdentifier.describe("Relation 内部 ID;直接使用 pingcode_relation_get 或 pingcode_relation_list 返回的 id"),
44
+ relation_name: optionalIdentifier.describe("兼容别名;官方没有独立 Relation 名称时请传 Relation ID"),
45
+ // 官方 Relation 没有独立的可复用名称;省略端点期望值时由详情回读补齐,
46
+ // Preview/Commit 仍会绑定并核对端点快照。
47
+ expected_principal_type: writablePrincipalTypeSchema.optional(),
48
+ expected_principal_id: optionalIdentifier.describe("预期主体 ID;与 expected_principal_name 二选一"),
49
+ expected_principal_name: optionalIdentifier.describe("预期主体名称或标识;与 expected_principal_id 二选一"),
50
+ expected_target_type: relationTypeSchema.optional(),
51
+ expected_target_id: optionalIdentifier.describe("预期目标 ID;与 expected_target_name 二选一"),
52
+ expected_target_name: optionalIdentifier.describe("预期目标名称或标识;与 expected_target_id 二选一"),
53
+ }).strict().superRefine((value, context) => {
54
+ const requireAtMostOne = (left, right, path, message) => {
55
+ if (left !== undefined && right !== undefined) {
56
+ context.addIssue({ code: "custom", path: [path], message });
57
+ }
58
+ };
59
+ if (value.relation_id === undefined && value.relation_name === undefined) {
60
+ context.addIssue({ code: "custom", path: ["relation_id"], message: "relation_id 必须提供;官方 Relation 没有独立名称" });
61
+ }
62
+ requireAtMostOne(value.relation_id, value.relation_name, "relation_id", "relation_id 与 relation_name 不得同时提供");
63
+ requireAtMostOne(value.expected_principal_id, value.expected_principal_name, "expected_principal_id", "expected_principal_id 与 expected_principal_name 不得同时提供");
64
+ requireAtMostOne(value.expected_target_id, value.expected_target_name, "expected_target_id", "expected_target_id 与 expected_target_name 不得同时提供");
65
+ if ((value.expected_principal_id !== undefined || value.expected_principal_name !== undefined) && value.expected_principal_type === undefined) {
66
+ context.addIssue({ code: "custom", path: ["expected_principal_type"], message: "提供预期主体引用时必须同时提供 expected_principal_type" });
67
+ }
68
+ if ((value.expected_target_id !== undefined || value.expected_target_name !== undefined) && value.expected_target_type === undefined) {
69
+ context.addIssue({ code: "custom", path: ["expected_target_type"], message: "提供预期目标引用时必须同时提供 expected_target_type" });
70
+ }
71
+ });
72
+ const createOperationSchema = z.object({
73
+ operation_id: identifier.describe("关联计划内唯一操作 ID"),
74
+ action: z.literal("create"),
75
+ fields: relationCreateFieldsSchema,
76
+ source_references: z.array(sourceReferenceSchema).min(1).max(20),
77
+ }).strict();
78
+ const deleteOperationSchema = z.object({
79
+ operation_id: identifier.describe("关联计划内唯一操作 ID"),
80
+ action: z.literal("delete"),
81
+ fields: relationDeleteFieldsSchema,
82
+ source_references: z.array(sourceReferenceSchema).min(1).max(20),
83
+ }).strict();
84
+ // Create and delete stay separate so the public JSON Schema has a clear action discriminator.
85
+ const relationOperationSchema = z.union([createOperationSchema, deleteOperationSchema]);
86
+ const relationPlanSchema = z.object({
87
+ plan_id: identifier.describe("关联同步计划 ID;Preview 与 Commit 必须完全相同"),
88
+ operations: z.array(relationOperationSchema).length(1).describe("一次计划只处理一个 Relation,避免非原子批量写入造成部分成功"),
89
+ }).strict();
90
+ const readAnnotations = {
91
+ readOnlyHint: true,
92
+ destructiveHint: false,
93
+ idempotentHint: true,
94
+ openWorldHint: true,
95
+ };
96
+ export function registerRelationTools(server, services, logger, runtime, writeApprovalMode) {
97
+ server.registerTool("pingcode_relation_list", {
98
+ title: "列出 PingCode 资源关联",
99
+ description: "按官方 Relation 的强关联参数读取关联。必须提供 principal_type 和 target_type,并在 principal_id 与 principal_name 中恰好选择一个;不要同时传入 ID 和名称。名称解析仅对当前已接入的 Idea、Work Item、Test Case 和 Wiki Page 提供;ticket、test_plan、test_run 只能使用已取得的资源 ID。分页参数可选;只返回可读摘要,不展示原始 JSON。",
100
+ inputSchema: {
101
+ principal_type: relationListPrincipalTypeSchema.optional().describe("Relation 主体资源类型;缺少时先明确主体类型,例如 work_item、test_case 或 idea"),
102
+ principal_id: identifier.optional().describe("Relation 主体资源 ID;来自主体资源列表/详情,名称入口使用 principal_name"),
103
+ principal_name: identifier.optional(),
104
+ target_type: relationListTargetTypeSchema.optional().describe("Relation 目标资源类型;缺少时先明确目标类型,例如 test_case、idea 或 page"),
105
+ page_index: z.number().int().min(0).optional(),
106
+ page_size: z.number().int().min(1).max(100).optional(),
107
+ },
108
+ annotations: readAnnotations,
109
+ }, async (input, extra) => executeTool(logger, "pingcode_relation_list", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { resourceId: input.principal_id ?? "", targetType: input.target_type ?? "" }, async () => {
110
+ if (input.principal_type === undefined || input.target_type === undefined) {
111
+ throw new PingCodeError("缺少 principal_type 或 target_type;请先明确 Relation 主体和目标资源类型,再调用本 Tool", "BAD_REQUEST");
112
+ }
113
+ if (input.principal_id === undefined && input.principal_name === undefined) {
114
+ throw new PingCodeError("principal_id 与 principal_name 必须提供一个;并且必须同时提供 target_type", "BAD_REQUEST");
115
+ }
116
+ if (input.principal_id !== undefined && input.principal_name !== undefined) {
117
+ throw new PingCodeError("principal_id 与 principal_name 不得同时提供", "BAD_REQUEST");
118
+ }
119
+ const principalId = input.principal_id ?? await resolveResourceReference(input.principal_type, undefined, input.principal_name, services);
120
+ const page = await services.getRelationSource().listRelations({
121
+ principalType: input.principal_type,
122
+ principalId,
123
+ targetType: input.target_type,
124
+ pageIndex: input.page_index,
125
+ pageSize: input.page_size,
126
+ });
127
+ return {
128
+ pageSize: page.page_size,
129
+ pageIndex: page.page_index,
130
+ total: page.total,
131
+ relations: page.values.map(mapRelation),
132
+ };
133
+ }, { formatResult: formatRelationList }));
134
+ server.registerTool("pingcode_relation_get", {
135
+ title: "读取 PingCode 资源关联",
136
+ description: "读取一个明确的 PingCode Relation,并返回官方响应中的主体、目标类型及资源 ID。此操作为只读。",
137
+ inputSchema: {
138
+ relation_id: optionalIdentifier.describe("必须来自 pingcode_relation_list 或 Relation Commit 返回的 Relation ID;缺少时先调用 pingcode_relation_list,Relation 没有独立名称入口"),
139
+ },
140
+ annotations: readAnnotations,
141
+ }, async (input, extra) => executeTool(logger, "pingcode_relation_get", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { resourceId: input.relation_id ?? "" }, async () => {
142
+ if (input.relation_id === undefined) {
143
+ throw new PingCodeError("缺少 relation_id;请先调用 pingcode_relation_list 获取关联候选,再传入用户选定的 Relation ID", "BAD_REQUEST");
144
+ }
145
+ return mapRelation(await services.getRelationSource().getRelation(input.relation_id));
146
+ }, { formatResult: formatRelation }));
147
+ server.registerTool("pingcode_relation_sync_preview", {
148
+ title: "预览同步 PingCode 资源关联",
149
+ description: "预览建立或移除当前 MCP 已批准组合中的 Idea、工作项、Test Case 与 Wiki 页面 Relation,不写入 PingCode。创建 Relation 的最小输入是 plan_id、operations[action=create]、fields.principal_type、fields.target_type、主体/目标各自的 id 或 name(二选一)以及 operation 内的 source_references。每个 operations 元素必须是 JSON 对象;fields 也必须是对象,例如 create 的 fields 为 {principal_type, principal_name, target_type, target_name},不能把 fields 序列化成字符串。建立关系时 principal_id 与 principal_name 二选一,target_id 与 target_name 二选一,不能同时提供;名称应来自对应列表/详情候选,无法唯一匹配时返回候选并等待用户选择。删除时 MCP 以 pingcode_relation_get 返回的 principal -> target 为 canonical 方向;若客户端传入了相反端点,MCP 会先按 Relation 详情规范化后再校验。source_references 是 operations 元素内与 fields 同级的必填数组,不放在 fields 或计划顶层。官方还存在 ticket、test_plan、test_run 类型,但本 Tool 不未经验证开放这些类型写入;页面仅可作为新关联的目标。一次计划只允许一个操作。不得在正常用户回复中展示内部 hash、Approval Context、会话绑定、evidence 或幂等字段。",
150
+ inputSchema: {
151
+ ...relationPlanSchema.shape,
152
+ plan_id: relationPlanSchema.shape.plan_id.optional().describe("业务上必填;省略仅用于返回缺参引导"),
153
+ operations: relationPlanSchema.shape.operations.optional().describe("业务上必填且一次只处理一个 Relation;省略仅用于返回缺参引导"),
154
+ agent_session_id: agentSessionId,
155
+ },
156
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
157
+ }, async (input, extra) => executeTool(logger, "pingcode_relation_sync_preview", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id ?? "" }, () => {
158
+ if (input.plan_id === undefined || input.operations === undefined) {
159
+ throw new PingCodeError("缺少 plan_id 或 operations;请补充 Relation 同步计划后重试 Preview", "BAD_REQUEST");
160
+ }
161
+ return previewRelationPlan({ plan_id: input.plan_id, operations: input.operations }, services, runtime, input.agent_session_id, extra.authInfo);
162
+ }, { formatResult: formatPreviewToolResult }));
163
+ server.registerTool("pingcode_relation_sync_commit", {
164
+ title: "提交同步 PingCode 资源关联",
165
+ description: "依已核准的 Preview 建立或移除一个 PingCode Relation。Commit 必须原样复用 Preview 请求中的计划和内部验证字段;删除前会再次核对 Relation 的主体与目标,写入后由 MCP 回读验证。",
166
+ inputSchema: {
167
+ ...relationPlanSchema.shape,
168
+ agent_session_id: agentSessionId,
169
+ preview_id: approvalContextId.optional().describe("可选。省略时 Server 依相同 plan_id 与 operations 自动匹配刚才的 Preview,并据此恢复 request hash、evidence hash、session binding 和幂等边界;仅当客户端确实读到 Preview 的 approvalContextId 时才需要传入"),
170
+ approval_context_id: approvalContextId.optional().describe("已弃用,等同 preview_id;新调用只传 preview_id"),
171
+ approval_token: approvalTokenSchema(writeApprovalMode),
172
+ idempotency_key: identifier.min(16).max(200).optional(),
173
+ },
174
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
175
+ }, async (input, extra) => executeTool(logger, "pingcode_relation_sync_commit", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id }, async () => {
176
+ runtime.assertMcpWriteScope(extra.authInfo?.scopes);
177
+ const verification = await resolveCommitVerification(runtime, {
178
+ previewId: input.preview_id,
179
+ approvalContextId: input.approval_context_id,
180
+ idempotencyKey: input.idempotency_key,
181
+ requestHash: calculateRequestHash({ plan_id: input.plan_id, operations: input.operations }),
182
+ agentSessionId: input.agent_session_id,
183
+ authInfo: extra.authInfo,
184
+ }, "relation");
185
+ const resolvedInput = {
186
+ ...input,
187
+ request_hash: verification.requestHash,
188
+ approval_context_id: verification.approvalContextId,
189
+ session_binding: verification.sessionBinding,
190
+ evidence_hash: verification.evidenceHash,
191
+ idempotency_key: verification.idempotencyKey,
192
+ };
193
+ const prepared = await prepareOperation(await resolveRelationOperation(resolvedInput.operations[0], services), services);
194
+ await requestClientWriteConfirmation(server, logger, writeApprovalMode, input.approval_token, formatRelationConfirmation(prepared), runtime.requiresConfirmation([prepared.operation.action]));
195
+ return commitRelationPlan(resolvedInput, prepared, services, runtime, writeApprovalMode, extra.authInfo);
196
+ }, { formatResult: formatWriteToolResult }));
197
+ }
198
+ async function previewRelationPlan(input, services, runtime, agentSessionIdValue, authInfo) {
199
+ const operation = input.operations[0];
200
+ if (!operation) {
201
+ throw new PingCodeError("Relation 计划不能为空", "BAD_REQUEST");
202
+ }
203
+ const prepared = await prepareOperation(await resolveRelationOperation(operation, services), services);
204
+ const displayOperation = relationDisplayOperation(prepared);
205
+ const plan = { plan_id: input.plan_id, operations: input.operations };
206
+ const requestHash = calculateRequestHash(plan);
207
+ const evidenceHash = calculateRequestHash({ planId: input.plan_id, requestHash, operations: [displayOperation] });
208
+ const context = await runtime.createApprovalContext({
209
+ resourceType: "relation",
210
+ requestHash,
211
+ evidenceHash,
212
+ ...(agentSessionIdValue === undefined ? {} : { agentSessionId: agentSessionIdValue }),
213
+ ...(authInfo === undefined ? {} : { authInfo }),
214
+ });
215
+ return {
216
+ planId: input.plan_id,
217
+ requestHash,
218
+ approvalContextId: context.approvalContextId,
219
+ sessionBinding: context.sessionBinding,
220
+ evidenceHash,
221
+ approvalExpiresAt: context.expiresAt,
222
+ counts: { creates: 0, updates: 0, relations: 1 },
223
+ warnings: prepared.target.type === "page"
224
+ ? ["Wiki 页面是关联目标;官方 Open API 不开放以页面为主体创建反向关联"]
225
+ : [],
226
+ operations: [displayOperation],
227
+ };
228
+ }
229
+ async function commitRelationPlan(input, prepared, services, runtime, writeApprovalMode, authInfo) {
230
+ const plan = { plan_id: input.plan_id, operations: input.operations };
231
+ if (calculateRequestHash(plan) !== input.request_hash) {
232
+ throw new PingCodeError("Commit 内容与 Relation Preview request hash 不一致", "CONFLICT");
233
+ }
234
+ const displayOperation = relationDisplayOperation(prepared);
235
+ const evidenceHash = calculateRequestHash({
236
+ planId: input.plan_id,
237
+ requestHash: input.request_hash,
238
+ operations: [displayOperation],
239
+ });
240
+ if (evidenceHash !== input.evidence_hash) {
241
+ throw new PingCodeError("Relation Preview 绑定的资源内容或端点已发生变化,当前 evidence hash 已过期;请重新执行 pingcode_relation_sync_preview 后再 Commit", "CONFLICT");
242
+ }
243
+ const committed = await commitWithApproval(runtime, writeApprovalMode, {
244
+ resourceType: "relation",
245
+ requestHash: input.request_hash,
246
+ evidenceHash: input.evidence_hash,
247
+ approvalContextId: input.approval_context_id,
248
+ sessionBinding: input.session_binding,
249
+ ...(input.agent_session_id === undefined ? {} : { agentSessionId: input.agent_session_id }),
250
+ approvalToken: input.approval_token,
251
+ idempotencyKey: input.idempotency_key,
252
+ ...(authInfo === undefined ? {} : { authInfo }),
253
+ actions: [input.operations[0]?.action ?? "create"],
254
+ action: async () => {
255
+ if (prepared.operation.action === "create") {
256
+ const fields = prepared.operation.fields;
257
+ if (!fields.principal_id || !fields.target_id)
258
+ throw new PingCodeError("Relation 主体或目标尚未解析", "BAD_REQUEST");
259
+ const created = await services.getRelationSource().createRelation({
260
+ principalType: fields.principal_type,
261
+ principalId: fields.principal_id,
262
+ targetType: fields.target_type,
263
+ targetId: fields.target_id,
264
+ });
265
+ const verified = await services.getRelationSource().getRelation(created.id).catch((error) => {
266
+ throw new PingCodeError(`Relation ${created.id} 已创建,但 MCP 回读失败;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN", { cause: error });
267
+ });
268
+ try {
269
+ assertRelationMatches(verified, fields.principal_type, fields.principal_id, fields.target_type, fields.target_id);
270
+ }
271
+ catch (error) {
272
+ throw new PingCodeError(`Relation ${created.id} 已创建,但 MCP 回读结果与计划不一致;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN", { cause: error });
273
+ }
274
+ return relationCommitResult(input.plan_id, prepared, created.id);
275
+ }
276
+ const relationId = prepared.operation.fields.relation_id;
277
+ if (!relationId)
278
+ throw new PingCodeError("Relation ID 尚未解析", "BAD_REQUEST");
279
+ await services.getRelationSource().deleteRelation(relationId);
280
+ try {
281
+ await services.getRelationSource().getRelation(relationId);
282
+ }
283
+ catch (error) {
284
+ // Some PingCode tenants return BAD_REQUEST rather than NOT_FOUND for
285
+ // a Relation that was just deleted. The DELETE already succeeded, so
286
+ // a 400 on the same resource is a confirmed absence, not uncertainty.
287
+ if (error instanceof PingCodeError &&
288
+ (error.code === "NOT_FOUND" || (error.code === "BAD_REQUEST" && error.options.status === 400))) {
289
+ return relationCommitResult(input.plan_id, prepared, relationId);
290
+ }
291
+ throw new PingCodeError(`Relation ${relationId} 的删除请求已发送,但 MCP 无法确认删除结果;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN", { cause: error });
292
+ }
293
+ throw new PingCodeError(`Relation ${relationId} 的删除请求已发送,但回读时仍存在;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN");
294
+ },
295
+ });
296
+ return {
297
+ ...committed.result,
298
+ replayed: committed.replayed,
299
+ approvedBy: committed.approval.approvedBy,
300
+ auditId: committed.auditId,
301
+ };
302
+ }
303
+ async function prepareOperation(operation, services) {
304
+ if (operation.action === "create") {
305
+ const fields = operation.fields;
306
+ if (!fields.principal_id || !fields.target_id)
307
+ throw new PingCodeError("Relation 主体或目标尚未解析", "BAD_REQUEST");
308
+ assertSupportedPair(fields.principal_type, fields.target_type);
309
+ const [principal, target, existing] = await Promise.all([
310
+ services.getRelationSource().readResource(fields.principal_type, fields.principal_id),
311
+ services.getRelationSource().readResource(fields.target_type, fields.target_id),
312
+ services.getRelationSource().findExactRelation({
313
+ principalType: fields.principal_type,
314
+ principalId: fields.principal_id,
315
+ targetType: fields.target_type,
316
+ targetId: fields.target_id,
317
+ }),
318
+ ]);
319
+ if (existing) {
320
+ throw new PingCodeError(`Relation ${existing.id} 已存在,不会重复建立`, "CONFLICT");
321
+ }
322
+ assertSupportedWorkItemTypes(principal, target);
323
+ return { operation, existing: null, principal, target };
324
+ }
325
+ const fields = operation.fields;
326
+ if (!fields.relation_id || !fields.expected_principal_type || !fields.expected_principal_id || !fields.expected_target_type || !fields.expected_target_id)
327
+ throw new PingCodeError("Relation 删除引用尚未解析", "BAD_REQUEST");
328
+ assertSupportedPair(fields.expected_principal_type, fields.expected_target_type);
329
+ const existing = await services.getRelationSource().getRelation(fields.relation_id);
330
+ assertRelationMatches(existing, fields.expected_principal_type, fields.expected_principal_id, fields.expected_target_type, fields.expected_target_id);
331
+ const [principal, target] = await Promise.all([
332
+ services.getRelationSource().readResource(fields.expected_principal_type, fields.expected_principal_id),
333
+ services.getRelationSource().readResource(fields.expected_target_type, fields.expected_target_id),
334
+ ]);
335
+ assertSupportedWorkItemTypes(principal, target);
336
+ return { operation, existing, principal, target };
337
+ }
338
+ function assertSupportedWorkItemTypes(principal, target) {
339
+ for (const [role, resource] of [["主体", principal], ["目标", target]]) {
340
+ if (resource.type !== "work_item")
341
+ continue;
342
+ const workItemType = resource.workItemType?.trim().toLocaleLowerCase();
343
+ if (workItemType === "story" || workItemType === "bug")
344
+ continue;
345
+ throw new PingCodeError(`Relation ${role}工作项类型 ${resource.workItemType ?? "未知"} 不受支持;仅允许 story 或 bug`, "BAD_REQUEST");
346
+ }
347
+ }
348
+ async function resolveRelationOperation(operation, services) {
349
+ if (operation.action === "create") {
350
+ const fields = { ...operation.fields };
351
+ fields.principal_id = await resolveResourceReference(fields.principal_type, fields.principal_id, fields.principal_name, services);
352
+ fields.target_id = await resolveResourceReference(fields.target_type, fields.target_id, fields.target_name, services);
353
+ delete fields.principal_name;
354
+ delete fields.target_name;
355
+ return { ...operation, fields };
356
+ }
357
+ const fields = { ...operation.fields };
358
+ // A Relation has no separate name in the official response. Use its ID as
359
+ // the canonical selector and hydrate omitted endpoint expectations from the
360
+ // same server-side detail response.
361
+ fields.relation_id ??= fields.relation_name;
362
+ if (fields.relation_id === undefined) {
363
+ throw new PingCodeError("Relation 缺少 relation_id", "BAD_REQUEST");
364
+ }
365
+ const relation = await services.getRelationSource().getRelation(fields.relation_id);
366
+ normalizeRelationEndpointDirection(fields, relation);
367
+ fields.expected_principal_type ??= relation.principal_type;
368
+ fields.expected_target_type ??= relation.target_type;
369
+ fields.expected_principal_id ??= relationPrincipalResourceId(relation);
370
+ fields.expected_target_id ??= relationTargetResourceId(relation);
371
+ if (!fields.expected_principal_type || !fields.expected_target_type || !fields.expected_principal_id || !fields.expected_target_id) {
372
+ throw new PingCodeError("Relation 详情未返回完整主体或目标,不能安全删除", "INVALID_RESPONSE");
373
+ }
374
+ fields.expected_principal_id = await resolveResourceReference(fields.expected_principal_type, fields.expected_principal_id, fields.expected_principal_name, services);
375
+ fields.expected_target_id = await resolveResourceReference(fields.expected_target_type, fields.expected_target_id, fields.expected_target_name, services);
376
+ delete fields.relation_name;
377
+ delete fields.expected_principal_name;
378
+ delete fields.expected_target_name;
379
+ return { ...operation, fields };
380
+ }
381
+ function normalizeRelationEndpointDirection(fields, relation) {
382
+ const principalType = relation.principal_type;
383
+ const targetType = relation.target_type;
384
+ const principalId = relationPrincipalResourceId(relation);
385
+ const targetId = relationTargetResourceId(relation);
386
+ if (fields.expected_principal_type === targetType &&
387
+ fields.expected_target_type === principalType &&
388
+ fields.expected_principal_id === targetId &&
389
+ fields.expected_target_id === principalId) {
390
+ fields.expected_principal_type = principalType;
391
+ fields.expected_principal_id = principalId;
392
+ fields.expected_target_type = targetType;
393
+ fields.expected_target_id = targetId;
394
+ }
395
+ }
396
+ async function resolveResourceReference(type, value, name, services) {
397
+ if (value !== undefined) {
398
+ const resource = await services.getRelationSource().readResource(type, value);
399
+ return resource.id;
400
+ }
401
+ if (name === undefined)
402
+ throw new PingCodeError(`${type} 缺少名称或 ID`, "BAD_REQUEST");
403
+ const candidates = [];
404
+ if (type === "idea") {
405
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
406
+ const page = await services.getShipSource().listIdeas({ keywords: name, pageIndex, pageSize: 100 });
407
+ candidates.push(...page.values.map((item) => ({ id: item.id, name: item.title, identifier: item.identifier })));
408
+ if (candidates.length >= page.total || page.values.length === 0)
409
+ break;
410
+ }
411
+ }
412
+ else if (type === "work_item") {
413
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
414
+ const page = await services.getTaskSource().listWorkItems({ keywords: name, pageIndex, pageSize: 100 });
415
+ candidates.push(...page.values.map((item) => ({ id: item.id, name: item.title, identifier: item.identifier })));
416
+ if (candidates.length >= page.total || page.values.length === 0)
417
+ break;
418
+ }
419
+ }
420
+ else if (type === "test_case") {
421
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
422
+ const page = await services.getTestCaseSource().listTestCases({ keywords: name, pageIndex, pageSize: 100 });
423
+ candidates.push(...page.values.map((item) => ({ id: item.id, name: item.title, identifier: item.identifier })));
424
+ if (candidates.length >= page.total || page.values.length === 0)
425
+ break;
426
+ }
427
+ }
428
+ else if (type === "page") {
429
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
430
+ const page = await services.getWikiSource().listPages({ pageIndex, pageSize: 100 });
431
+ candidates.push(...page.values.map((item) => ({ id: item.id, name: item.name, identifier: item.short_id })));
432
+ if (candidates.length >= page.total || page.values.length === 0)
433
+ break;
434
+ }
435
+ }
436
+ else {
437
+ throw new PingCodeError(`${type} 尚未接入 MCP 资源目录,不能按名称解析;请提供前序 Relation Tool 返回的资源 ID`, "BAD_REQUEST");
438
+ }
439
+ return resolveReferenceName(type, name, candidates);
440
+ }
441
+ function resolveReferenceName(field, name, candidates) {
442
+ const normalized = name.trim().toLocaleLowerCase();
443
+ const matches = candidates.filter((candidate) => candidate.name.trim().toLocaleLowerCase() === normalized || candidate.identifier?.trim().toLocaleLowerCase() === normalized);
444
+ if (matches.length === 1)
445
+ return matches[0].id;
446
+ if (matches.length > 1)
447
+ throw new PingCodeError(`${field} 名称匹配多个候选,请选择一个`, "CONFLICT");
448
+ throw new PingCodeError(`${field} 名称没有匹配结果:${name}`, "NOT_FOUND");
449
+ }
450
+ function assertSupportedPair(principalType, targetType) {
451
+ if (!isSupportedRelationWritePair(principalType, targetType)) {
452
+ throw new PingCodeError(`官方 Open API 不支持或本 MCP 未批准建立 ${principalType} -> ${targetType} 关联;页面只能作为目标`, "BAD_REQUEST");
453
+ }
454
+ }
455
+ function assertRelationMatches(relation, principalType, principalId, targetType, targetId) {
456
+ if (relation.principal_type !== principalType ||
457
+ relationPrincipalResourceId(relation) !== principalId ||
458
+ relation.target_type !== targetType ||
459
+ relationTargetResourceId(relation) !== targetId) {
460
+ throw new PingCodeError(`Relation ${relation.id} 的主体或目标与 Preview 计划不一致,未执行写入`, "CONFLICT");
461
+ }
462
+ }
463
+ function relationDisplayOperation(prepared) {
464
+ const create = prepared.operation.action === "create";
465
+ return {
466
+ operationId: prepared.operation.operation_id,
467
+ action: prepared.operation.action,
468
+ targetId: prepared.existing?.id ?? null,
469
+ changes: {
470
+ relation: {
471
+ before: create ? null : `Relation ${prepared.existing?.id ?? ""}:${relationLabel(prepared.principal, prepared.target)}`,
472
+ after: create ? relationLabel(prepared.principal, prepared.target) : null,
473
+ },
474
+ principal: { before: resourceLabel(prepared.principal), after: resourceLabel(prepared.principal) },
475
+ target: { before: resourceLabel(prepared.target), after: resourceLabel(prepared.target) },
476
+ },
477
+ sourceReferences: prepared.operation.source_references,
478
+ };
479
+ }
480
+ function relationCommitResult(planId, prepared, relationId) {
481
+ return {
482
+ planId,
483
+ resources: [{
484
+ operationId: prepared.operation.operation_id,
485
+ action: prepared.operation.action,
486
+ id: relationId,
487
+ title: relationLabel(prepared.principal, prepared.target),
488
+ verified: true,
489
+ }],
490
+ };
491
+ }
492
+ function mapRelation(relation) {
493
+ const principalType = relationTypeSchema.parse(relation.principal_type);
494
+ const targetType = relationTypeSchema.parse(relation.target_type);
495
+ const principalId = relationPrincipalResourceId(relation);
496
+ const targetId = relationTargetResourceId(relation);
497
+ if (!principalId || !targetId) {
498
+ throw new PingCodeError(`Relation ${relation.id} 未返回完整主体或目标 ID`, "INVALID_RESPONSE");
499
+ }
500
+ return {
501
+ id: relation.id,
502
+ principalType,
503
+ principalId,
504
+ principal: relationResourceLabel(relation.principal),
505
+ targetType,
506
+ targetId,
507
+ target: relationResourceLabel(relation.target),
508
+ };
509
+ }
510
+ function relationResourceLabel(value) {
511
+ if (value === undefined)
512
+ return null;
513
+ return {
514
+ id: value.id,
515
+ name: typeof value.name === "string"
516
+ ? value.name
517
+ : typeof value.title === "string"
518
+ ? value.title
519
+ : null,
520
+ identifier: typeof value.identifier === "string"
521
+ ? value.identifier
522
+ : typeof value.short_id === "string"
523
+ ? value.short_id
524
+ : null,
525
+ };
526
+ }
527
+ function formatRelationList(result) {
528
+ const lines = [`PingCode 资源关联:${result.total} 个`];
529
+ for (const relation of result.relations) {
530
+ const principalLabel = relationDisplayLabel(relation.principal, relation.principalId);
531
+ const targetLabel = relationDisplayLabel(relation.target, relation.targetId);
532
+ lines.push(`- ${relation.id}:${typeLabel(relation.principalType)} ${principalLabel} -> ${typeLabel(relation.targetType)} ${targetLabel}`);
533
+ }
534
+ lines.push(...paginationGuidance({
535
+ pageIndex: result.pageIndex ?? 0,
536
+ pageSize: result.pageSize ?? Math.max(result.relations.length, 1),
537
+ total: result.total,
538
+ returned: result.relations.length,
539
+ searchable: false,
540
+ }));
541
+ return lines.join("\n");
542
+ }
543
+ function formatRelation(result) {
544
+ const principalLabel = relationDisplayLabel(result.principal, result.principalId);
545
+ const targetLabel = relationDisplayLabel(result.target, result.targetId);
546
+ return [
547
+ "已读取 PingCode 资源关联",
548
+ `- Relation ID:${result.id}`,
549
+ `- 主体:${typeLabel(result.principalType)} ${principalLabel}`,
550
+ `- 目标:${typeLabel(result.targetType)} ${targetLabel}`,
551
+ ].join("\n");
552
+ }
553
+ function relationDisplayLabel(resource, fallbackId) {
554
+ if (resource === null)
555
+ return fallbackId;
556
+ const displayName = resource.name ?? resource.identifier;
557
+ if (displayName === null)
558
+ return fallbackId;
559
+ const identifier = resource.name !== null && resource.identifier !== null
560
+ ? ` (${resource.identifier})`
561
+ : "";
562
+ return `${displayName}${identifier} [${resource.id}]`;
563
+ }
564
+ function formatRelationConfirmation(prepared) {
565
+ return formatWriteConfirmationMessage(prepared.operation.action === "create" ? "即将在 PingCode 建立资源关联" : "即将在 PingCode 移除资源关联", [
566
+ `操作:${prepared.operation.action === "create" ? "建立 Relation" : "移除已确认 Relation"}`,
567
+ `主体:${resourceLabel(prepared.principal)}`,
568
+ `目标:${resourceLabel(prepared.target)}`,
569
+ ]);
570
+ }
571
+ function relationLabel(principal, target) {
572
+ return `${resourceLabel(principal)} -> ${resourceLabel(target)}`;
573
+ }
574
+ function resourceLabel(resource) {
575
+ return `${typeLabel(resource.type)}「${resource.title}」${resource.identifier ? ` (${resource.identifier})` : ""}`;
576
+ }
577
+ function typeLabel(type) {
578
+ if (type === "idea")
579
+ return "产品需求";
580
+ if (type === "ticket")
581
+ return "工单";
582
+ if (type === "work_item")
583
+ return "工作项";
584
+ if (type === "test_plan")
585
+ return "测试计划";
586
+ if (type === "test_run")
587
+ return "测试执行";
588
+ if (type === "test_case")
589
+ return "测试用例";
590
+ return "Wiki 页面";
591
+ }
592
+ //# sourceMappingURL=relation.js.map