ai-dev-requirements 0.1.8 → 0.1.10

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.
package/dist/index.cjs CHANGED
@@ -93,6 +93,9 @@ const TASK_DETAIL_QUERY = `
93
93
  query Task($key: Key) {
94
94
  task(key: $key) {
95
95
  key uuid number name
96
+ description
97
+ descriptionText
98
+ desc_rich: description
96
99
  issueType { uuid name }
97
100
  status { uuid name category }
98
101
  priority { value }
@@ -202,6 +205,55 @@ const DEFAULT_STATUS_NOT_IN = [
202
205
  "Dn3k8ffK",
203
206
  "TbmY2So5"
204
207
  ];
208
+ const TESTCASE_LIBRARY_LIST_QUERY = `
209
+ query Q {
210
+ testcaseLibraries {
211
+ uuid name key
212
+ testcaseCaseCount
213
+ }
214
+ }
215
+ `;
216
+ const TESTCASE_MODULE_SEARCH_QUERY = `
217
+ query Q($filter: Filter) {
218
+ testcaseModules(filter: $filter) {
219
+ uuid name key
220
+ parent { uuid name }
221
+ }
222
+ }
223
+ `;
224
+ const TESTCASE_LIST_PAGED_QUERY = `
225
+ query PAGED_LIBRARY_TESTCASE_LIST($testCaseFilter: Filter, $pagination: Pagination) {
226
+ buckets(groupBy: {testcaseCases: {}}, pagination: $pagination) {
227
+ testcaseCases(filterGroup: $testCaseFilter, limit: 10000) {
228
+ uuid name key id
229
+ priority { uuid value }
230
+ type { uuid value }
231
+ assign { uuid name }
232
+ testcaseModule { uuid }
233
+ }
234
+ key
235
+ pageInfo { count totalCount hasNextPage endCursor }
236
+ }
237
+ }
238
+ `;
239
+ const TESTCASE_DETAIL_QUERY = `
240
+ query QUERY_TESTCASES_DETAIL($testCaseFilter: Filter, $stepFilter: Filter) {
241
+ testcaseCases(filter: $testCaseFilter) {
242
+ uuid name key id condition desc path
243
+ assign { uuid name }
244
+ priority { uuid value }
245
+ type { uuid value }
246
+ testcaseLibrary { uuid }
247
+ testcaseModule { uuid }
248
+ relatedTasks { uuid name number }
249
+ }
250
+ testcaseCaseSteps(filter: $stepFilter, orderBy: { index: ASC }) {
251
+ key uuid
252
+ testcaseCase { uuid }
253
+ desc result index
254
+ }
255
+ }
256
+ `;
205
257
  function parseOnesSearchIntent(query) {
206
258
  if (!query) return "keyword";
207
259
  const normalized = query.toLowerCase();
@@ -273,7 +325,120 @@ function getSetCookies(response) {
273
325
  const raw = response.headers.get("set-cookie");
274
326
  return raw ? [raw] : [];
275
327
  }
276
- function toRequirement(task, description = "") {
328
+ function extractWikiPageUuidsFromText(text) {
329
+ if (!text) return [];
330
+ const uuids = /* @__PURE__ */ new Set();
331
+ for (const pattern of [/\/page\/([\w-]+)/g, /page=([\w-]+)/g]) for (const match of text.matchAll(pattern)) if (match[1]) uuids.add(match[1]);
332
+ return [...uuids];
333
+ }
334
+ function htmlToPlainText(html) {
335
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\n{3,}/g, "\n\n").trim();
336
+ }
337
+ function getTaskDetailText(task) {
338
+ return task.descriptionText?.trim() || htmlToPlainText(task.desc_rich ?? task.description ?? "");
339
+ }
340
+ function isRecord(value) {
341
+ return value !== null && typeof value === "object" && !Array.isArray(value);
342
+ }
343
+ function parseJsonRecord(value) {
344
+ try {
345
+ const parsed = JSON.parse(value);
346
+ return isRecord(parsed) ? parsed : null;
347
+ } catch {
348
+ return null;
349
+ }
350
+ }
351
+ function asWikiBlocks(value) {
352
+ if (!Array.isArray(value)) return [];
353
+ return value.filter(isRecord);
354
+ }
355
+ function renderWikiTextRuns(value) {
356
+ if (!Array.isArray(value)) return "";
357
+ return value.map((run) => {
358
+ if (!isRecord(run)) return "";
359
+ const attributes = isRecord(run.attributes) ? run.attributes : {};
360
+ const insert = typeof run.insert === "string" ? run.insert.replace(/\u00A0/g, " ") : "";
361
+ const link = typeof attributes.link === "string" ? attributes.link : "";
362
+ if (link && insert.trim()) return `[${insert}](${link})`;
363
+ const taskName = typeof attributes.taskName === "string" ? attributes.taskName : "";
364
+ if (link && taskName) return `[${taskName}](${link})`;
365
+ return insert;
366
+ }).join("").replace(/\n{3,}/g, "\n\n").trim();
367
+ }
368
+ function renderWikiHeading(text, heading) {
369
+ if (!heading) return text;
370
+ const level = Math.min(Math.max(Math.trunc(heading), 1), 6);
371
+ return `${"#".repeat(level)} ${text}`;
372
+ }
373
+ function getWikiImageSource(block) {
374
+ const embedData = isRecord(block.embedData) ? block.embedData : {};
375
+ return typeof embedData.src === "string" ? embedData.src.trim() : "";
376
+ }
377
+ function renderWikiEmbed(block, context) {
378
+ if (block.embedType === "image") {
379
+ const src = getWikiImageSource(block);
380
+ if (src && !context.imageSources.includes(src)) context.imageSources.push(src);
381
+ return src ? `[Image: ${src}]` : "[Image]";
382
+ }
383
+ return block.embedType ? `[Embed: ${block.embedType}]` : "";
384
+ }
385
+ function escapeWikiTableCell(value) {
386
+ return value.replace(/\|/g, "\\|").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
387
+ }
388
+ function renderWikiCell(value, document, context) {
389
+ const blocks = asWikiBlocks(value);
390
+ if (!blocks.length) return "";
391
+ return blocks.map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join(" ").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
392
+ }
393
+ function renderWikiTable(block, document, context) {
394
+ const cols = typeof block.cols === "number" && block.cols > 0 ? Math.trunc(block.cols) : 0;
395
+ const children = Array.isArray(block.children) ? block.children.filter((child) => typeof child === "string") : [];
396
+ if (!cols || !children.length) return "";
397
+ const rows = [];
398
+ for (let index = 0; index < children.length; index += cols) {
399
+ const cells = children.slice(index, index + cols).map((childId) => escapeWikiTableCell(renderWikiCell(document[childId], document, context)));
400
+ while (cells.length < cols) cells.push("");
401
+ rows.push(`| ${cells.join(" | ")} |`);
402
+ }
403
+ if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
404
+ return rows.join("\n");
405
+ }
406
+ function renderWikiBlock(block, document, context) {
407
+ if (block.type === "table") return renderWikiTable(block, document, context);
408
+ if (block.type === "embed") return renderWikiEmbed(block, context);
409
+ const text = renderWikiTextRuns(block.text);
410
+ if (!text) return "";
411
+ if (block.type === "list") {
412
+ const level = typeof block.level === "number" ? Math.max(Math.trunc(block.level), 1) : 1;
413
+ return `${" ".repeat(level - 1)}${block.ordered ? `${block.start ?? 1}.` : "-"} ${text}`;
414
+ }
415
+ return renderWikiHeading(text, block.heading);
416
+ }
417
+ function renderWikiContent(content, context = { imageSources: [] }) {
418
+ const trimmed = content.trim();
419
+ if (!trimmed) return "";
420
+ const document = parseJsonRecord(trimmed);
421
+ if (!document) return trimmed;
422
+ if (!("blocks" in document)) return trimmed;
423
+ return asWikiBlocks(document.blocks).map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join("\n\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
424
+ }
425
+ function mimeTypeFromFileName(fileName) {
426
+ const normalized = fileName.toLowerCase();
427
+ if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg";
428
+ if (normalized.endsWith(".gif")) return "image/gif";
429
+ if (normalized.endsWith(".webp")) return "image/webp";
430
+ if (normalized.endsWith(".svg")) return "image/svg+xml";
431
+ return "image/png";
432
+ }
433
+ function attachmentNameFromPath(path) {
434
+ const name = path.split("/").pop() || path;
435
+ try {
436
+ return decodeURIComponent(name);
437
+ } catch {
438
+ return name;
439
+ }
440
+ }
441
+ function toRequirement(task, description = "", attachments = []) {
277
442
  return {
278
443
  id: task.uuid,
279
444
  source: "ones",
@@ -288,7 +453,7 @@ function toRequirement(task, description = "") {
288
453
  createdAt: "",
289
454
  updatedAt: "",
290
455
  dueDate: null,
291
- attachments: [],
456
+ attachments,
292
457
  raw: task
293
458
  };
294
459
  }
@@ -568,12 +733,51 @@ var OnesAdapter = class extends BaseAdapter {
568
733
  * Fetch wiki page content via REST API.
569
734
  * Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
570
735
  */
736
+ async fetchWikiPageDetail(wikiUuid) {
737
+ const session = await this.login();
738
+ const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/page/${wikiUuid}/detail`;
739
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
740
+ if (!response.ok) return {};
741
+ return response.json();
742
+ }
743
+ buildWikiImageUrl(session, refUuid, source, token) {
744
+ const encodedRefUuid = encodeURIComponent(refUuid);
745
+ const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
746
+ const encodedToken = encodeURIComponent(token);
747
+ return `${this.config.apiBase}/wiki/api/wiki/editor/${session.teamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
748
+ }
571
749
  async fetchWikiContent(wikiUuid) {
572
750
  const session = await this.login();
573
751
  const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
574
752
  const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
575
- if (!response.ok) return "";
576
- return (await response.json()).content ?? "";
753
+ if (!response.ok) return {
754
+ content: "",
755
+ attachments: []
756
+ };
757
+ const data = await response.json();
758
+ const renderContext = { imageSources: [] };
759
+ const content = renderWikiContent(typeof data.content === "string" ? data.content : "", renderContext);
760
+ const token = typeof data.token === "string" ? data.token : "";
761
+ if (!renderContext.imageSources.length || !token) return {
762
+ content,
763
+ attachments: []
764
+ };
765
+ const detail = await this.fetchWikiPageDetail(wikiUuid);
766
+ const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
767
+ if (!refUuid) return {
768
+ content,
769
+ attachments: []
770
+ };
771
+ return {
772
+ content,
773
+ attachments: renderContext.imageSources.map((source, index) => ({
774
+ id: `${wikiUuid}-image-${index + 1}`,
775
+ name: attachmentNameFromPath(source),
776
+ url: this.buildWikiImageUrl(session, refUuid, source, token),
777
+ mimeType: mimeTypeFromFileName(source),
778
+ size: 0
779
+ }))
780
+ };
577
781
  }
578
782
  /**
579
783
  * Fetch a single task by UUID or number (e.g. "#95945" or "95945").
@@ -601,13 +805,27 @@ var OnesAdapter = class extends BaseAdapter {
601
805
  }
602
806
  const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
603
807
  if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
604
- const wikiPages = task.relatedWikiPages ?? [];
605
- const wikiContents = await Promise.all(wikiPages.filter((w) => !w.errorMessage).map(async (wiki) => {
606
- const content = await this.fetchWikiContent(wiki.uuid);
808
+ const wikiRefs = /* @__PURE__ */ new Map();
809
+ for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
810
+ title: wiki.title,
811
+ uuid: wiki.uuid
812
+ });
813
+ const detailForLinkExtraction = [
814
+ task.description,
815
+ task.descriptionText,
816
+ task.desc_rich
817
+ ].filter(Boolean).join("\n");
818
+ for (const wikiUuid of extractWikiPageUuidsFromText(detailForLinkExtraction)) if (!wikiRefs.has(wikiUuid)) wikiRefs.set(wikiUuid, {
819
+ title: `Wiki ${wikiUuid}`,
820
+ uuid: wikiUuid
821
+ });
822
+ const wikiContents = await Promise.all([...wikiRefs.values()].map(async (wiki) => {
823
+ const rendered = await this.fetchWikiContent(wiki.uuid);
607
824
  return {
608
825
  title: wiki.title,
609
826
  uuid: wiki.uuid,
610
- content
827
+ content: rendered.content,
828
+ attachments: rendered.attachments
611
829
  };
612
830
  }));
613
831
  const parts = [];
@@ -646,7 +864,18 @@ var OnesAdapter = class extends BaseAdapter {
646
864
  else parts.push("(No content available)");
647
865
  }
648
866
  }
649
- return toRequirement(task, parts.join("\n"));
867
+ const detailText = getTaskDetailText(task);
868
+ const hasWikiContent = wikiContents.some((wiki) => wiki.content.trim());
869
+ if (detailText && !hasWikiContent) {
870
+ parts.push("");
871
+ parts.push("---");
872
+ parts.push("");
873
+ parts.push("## Requirement Detail");
874
+ parts.push("");
875
+ parts.push(detailText);
876
+ }
877
+ const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
878
+ return toRequirement(task, parts.join("\n"), wikiAttachments);
650
879
  }
651
880
  /**
652
881
  * Search tasks assigned to current user via GraphQL.
@@ -783,6 +1012,108 @@ var OnesAdapter = class extends BaseAdapter {
783
1012
  raw: task
784
1013
  };
785
1014
  }
1015
+ async getTestcases(params) {
1016
+ let libraryUuid = params.libraryUuid ?? this.config.options?.testcaseLibraryUuid;
1017
+ if (!libraryUuid) {
1018
+ const libs = (await this.graphql(TESTCASE_LIBRARY_LIST_QUERY, {}, "library-select")).data?.testcaseLibraries ?? [];
1019
+ if (libs.length === 0) throw new Error("ONES: No testcase libraries found for this team");
1020
+ libs.sort((a, b) => b.testcaseCaseCount - a.testcaseCaseCount);
1021
+ libraryUuid = libs[0].uuid;
1022
+ }
1023
+ const task = ((await this.graphql(SEARCH_TASKS_QUERY, {
1024
+ groupBy: { tasks: {} },
1025
+ groupOrderBy: null,
1026
+ orderBy: { createTime: "DESC" },
1027
+ filterGroup: [{ number_in: [params.taskNumber] }],
1028
+ search: null,
1029
+ pagination: {
1030
+ limit: 10,
1031
+ preciseCount: false
1032
+ },
1033
+ limit: 10
1034
+ }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((t) => t.number === params.taskNumber);
1035
+ if (!task) throw new Error(`ONES: Task #${params.taskNumber} not found`);
1036
+ const modules = (await this.graphql(TESTCASE_MODULE_SEARCH_QUERY, { filter: {
1037
+ testcaseLibrary_in: [libraryUuid],
1038
+ name_match: `#${params.taskNumber}`
1039
+ } }, "find-testcase-module")).data?.testcaseModules ?? [];
1040
+ if (modules.length === 0) throw new Error(`ONES: No testcase module matching "#${params.taskNumber}" in library ${libraryUuid}`);
1041
+ const mod = modules[0];
1042
+ const caseList = [];
1043
+ let cursor = "";
1044
+ let totalCount = 0;
1045
+ while (true) {
1046
+ const bucket = (await this.graphql(TESTCASE_LIST_PAGED_QUERY, {
1047
+ testCaseFilter: [{
1048
+ testcaseLibrary_in: [libraryUuid],
1049
+ path_match: mod.uuid
1050
+ }],
1051
+ pagination: {
1052
+ limit: 50,
1053
+ after: cursor,
1054
+ preciseCount: true
1055
+ }
1056
+ }, "testcase-list-paged")).data?.buckets?.[0];
1057
+ if (!bucket) break;
1058
+ caseList.push(...bucket.testcaseCases ?? []);
1059
+ totalCount = bucket.pageInfo.totalCount;
1060
+ if (!bucket.pageInfo.hasNextPage) break;
1061
+ cursor = bucket.pageInfo.endCursor;
1062
+ }
1063
+ if (caseList.length === 0) return {
1064
+ taskNumber: params.taskNumber,
1065
+ taskName: task.name,
1066
+ moduleName: mod.name,
1067
+ moduleUuid: mod.uuid,
1068
+ totalCount: 0,
1069
+ cases: []
1070
+ };
1071
+ const allCases = [];
1072
+ const BATCH_SIZE = 20;
1073
+ for (let i = 0; i < caseList.length; i += BATCH_SIZE) {
1074
+ const uuids = caseList.slice(i, i + BATCH_SIZE).map((c) => c.uuid);
1075
+ const detailData = await this.graphql(TESTCASE_DETAIL_QUERY, {
1076
+ testCaseFilter: { uuid_in: [...uuids, null] },
1077
+ stepFilter: { testcaseCase_in: uuids }
1078
+ }, "library-testcase-detail");
1079
+ const cases = detailData.data?.testcaseCases ?? [];
1080
+ const steps = detailData.data?.testcaseCaseSteps ?? [];
1081
+ const stepsByCase = /* @__PURE__ */ new Map();
1082
+ for (const step of steps) {
1083
+ const caseUuid = step.testcaseCase.uuid;
1084
+ if (!stepsByCase.has(caseUuid)) stepsByCase.set(caseUuid, []);
1085
+ stepsByCase.get(caseUuid).push({
1086
+ uuid: step.uuid,
1087
+ index: step.index,
1088
+ desc: step.desc ?? "",
1089
+ result: step.result ?? ""
1090
+ });
1091
+ }
1092
+ for (const c of cases) {
1093
+ const freshDesc = c.desc ? await this.refreshImageUrls(c.desc) : "";
1094
+ allCases.push({
1095
+ uuid: c.uuid,
1096
+ id: c.id,
1097
+ name: c.name,
1098
+ priority: c.priority?.value ?? "N/A",
1099
+ type: c.type?.value ?? "Unknown",
1100
+ assignName: c.assign?.name ?? null,
1101
+ condition: c.condition ?? "",
1102
+ desc: freshDesc,
1103
+ steps: (stepsByCase.get(c.uuid) ?? []).sort((a, b) => a.index - b.index),
1104
+ modulePath: c.path ?? ""
1105
+ });
1106
+ }
1107
+ }
1108
+ return {
1109
+ taskNumber: params.taskNumber,
1110
+ taskName: task.name,
1111
+ moduleName: mod.name,
1112
+ moduleUuid: mod.uuid,
1113
+ totalCount,
1114
+ cases: allCases
1115
+ };
1116
+ }
786
1117
  };
787
1118
 
788
1119
  //#endregion
@@ -884,6 +1215,11 @@ function loadConfigFromEnv() {
884
1215
  const account = process.env.ONES_ACCOUNT;
885
1216
  const password = process.env.ONES_PASSWORD;
886
1217
  if (!apiBase || !account || !password) return null;
1218
+ let options;
1219
+ const configPath = findConfigFile(process.cwd());
1220
+ if (configPath) try {
1221
+ options = JSON.parse((0, node_fs.readFileSync)(configPath, "utf-8"))?.sources?.ones?.options;
1222
+ } catch {}
887
1223
  return {
888
1224
  sources: { ones: {
889
1225
  enabled: true,
@@ -892,7 +1228,8 @@ function loadConfigFromEnv() {
892
1228
  type: "ones-pkce",
893
1229
  emailEnv: "ONES_ACCOUNT",
894
1230
  passwordEnv: "ONES_PASSWORD"
895
- }
1231
+ },
1232
+ options
896
1233
  } },
897
1234
  defaultSource: "ones"
898
1235
  };
@@ -959,7 +1296,7 @@ const GetIssueDetailSchema = zod_v4.z.object({
959
1296
  * Download an image from URL and return as base64 data URI.
960
1297
  * Returns null if download fails.
961
1298
  */
962
- async function downloadImageAsBase64(url) {
1299
+ async function downloadImageAsBase64$1(url) {
963
1300
  try {
964
1301
  const res = await fetch(url, { redirect: "follow" });
965
1302
  if (!res.ok) return null;
@@ -985,7 +1322,7 @@ async function handleGetIssueDetail(input, adapters, defaultSource) {
985
1322
  if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
986
1323
  const detail = await adapter.getIssueDetail({ issueId: input.issueId });
987
1324
  const imageUrls = detail.descriptionRich ? extractImageUrls(detail.descriptionRich) : [];
988
- const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64(url)));
1325
+ const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64$1(url)));
989
1326
  const content = [{
990
1327
  type: "text",
991
1328
  text: formatIssueDetail(detail)
@@ -1071,15 +1408,54 @@ const GetRequirementSchema = zod_v4.z.object({
1071
1408
  id: zod_v4.z.string().describe("The requirement/issue ID"),
1072
1409
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1073
1410
  });
1411
+ async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
1412
+ try {
1413
+ const res = await fetch(url, { redirect: "follow" });
1414
+ if (!res.ok) return null;
1415
+ const mimeType = (res.headers.get("content-type") ?? fallbackMimeType).split(";")[0].trim() || fallbackMimeType;
1416
+ return {
1417
+ base64: Buffer.from(await res.arrayBuffer()).toString("base64"),
1418
+ mimeType
1419
+ };
1420
+ } catch {
1421
+ return null;
1422
+ }
1423
+ }
1424
+ function isImageAttachment(attachment) {
1425
+ if (attachment.mimeType.startsWith("image/")) return true;
1426
+ return /\.(?:png|jpe?g|gif|webp|svg)$/i.test(attachment.url);
1427
+ }
1428
+ function displayAttachmentUrl(url) {
1429
+ try {
1430
+ const parsed = new URL(url);
1431
+ parsed.search = "";
1432
+ parsed.hash = "";
1433
+ return parsed.toString();
1434
+ } catch {
1435
+ return url.replace(/[?#].*$/, "");
1436
+ }
1437
+ }
1074
1438
  async function handleGetRequirement(input, adapters, defaultSource) {
1075
1439
  const sourceType = input.source ?? defaultSource;
1076
1440
  if (!sourceType) throw new Error("No source specified and no default source configured");
1077
1441
  const adapter = adapters.get(sourceType);
1078
1442
  if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1079
- return { content: [{
1443
+ const requirement = await adapter.getRequirement({ id: input.id });
1444
+ const imageAttachments = requirement.attachments.filter(isImageAttachment);
1445
+ const imageResults = await Promise.all(imageAttachments.map((attachment) => downloadImageAsBase64(attachment.url, attachment.mimeType)));
1446
+ const content = [{
1080
1447
  type: "text",
1081
- text: formatRequirement(await adapter.getRequirement({ id: input.id }))
1082
- }] };
1448
+ text: formatRequirement(requirement)
1449
+ }];
1450
+ for (const image of imageResults) {
1451
+ if (!image) continue;
1452
+ content.push({
1453
+ type: "image",
1454
+ data: image.base64,
1455
+ mimeType: image.mimeType
1456
+ });
1457
+ }
1458
+ return { content };
1083
1459
  }
1084
1460
  function formatRequirement(req) {
1085
1461
  const lines = [
@@ -1100,7 +1476,59 @@ function formatRequirement(req) {
1100
1476
  lines.push("", "## Description", "", req.description || "_No description_");
1101
1477
  if (req.attachments.length > 0) {
1102
1478
  lines.push("", "## Attachments");
1103
- for (const att of req.attachments) lines.push(`- [${att.name}](${att.url}) (${att.mimeType}, ${att.size} bytes)`);
1479
+ for (const att of req.attachments) lines.push(`- [${att.name}](${displayAttachmentUrl(att.url)}) (${att.mimeType}, ${att.size} bytes)`);
1480
+ }
1481
+ return lines.join("\n");
1482
+ }
1483
+
1484
+ //#endregion
1485
+ //#region src/tools/get-testcases.ts
1486
+ const GetTestcasesSchema = zod_v4.z.object({
1487
+ taskNumber: zod_v4.z.string().describe("Task number (e.g. \"302\" or \"#302\"). Finds all testcases in the matching module."),
1488
+ libraryUuid: zod_v4.z.string().optional().describe("Testcase library UUID. If omitted, uses configured default."),
1489
+ source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1490
+ });
1491
+ async function handleGetTestcases(input, adapters, defaultSource) {
1492
+ const sourceType = input.source ?? defaultSource;
1493
+ if (!sourceType) throw new Error("No source specified and no default source configured");
1494
+ const adapter = adapters.get(sourceType);
1495
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1496
+ const numMatch = input.taskNumber.match(/^#?(\d+)$/);
1497
+ if (!numMatch) throw new Error(`Invalid task number: "${input.taskNumber}". Expected a number like "302" or "#302".`);
1498
+ return { content: [{
1499
+ type: "text",
1500
+ text: formatTestcases(await adapter.getTestcases({
1501
+ taskNumber: Number.parseInt(numMatch[1], 10),
1502
+ libraryUuid: input.libraryUuid
1503
+ }))
1504
+ }] };
1505
+ }
1506
+ function formatTestcases(result) {
1507
+ const lines = [
1508
+ `# ${result.taskName} — 测试用例`,
1509
+ "",
1510
+ `- **模块**: ${result.moduleName}`,
1511
+ `- **共 ${result.totalCount} 个用例**(已加载 ${result.cases.length} 个)`,
1512
+ ""
1513
+ ];
1514
+ for (const tc of result.cases) {
1515
+ lines.push(`## ${tc.id} ${tc.name}`);
1516
+ lines.push("");
1517
+ lines.push(`- 优先级: ${tc.priority} | 类型: ${tc.type}`);
1518
+ if (tc.assignName) lines.push(`- 维护人: ${tc.assignName}`);
1519
+ if (tc.condition) lines.push(`- 前置条件: ${tc.condition}`);
1520
+ if (tc.desc) lines.push(`- 备注: ${tc.desc}`);
1521
+ if (tc.steps.length > 0) {
1522
+ lines.push("");
1523
+ lines.push("| 步骤 | 操作描述 | 预期结果 |");
1524
+ lines.push("|------|----------|----------|");
1525
+ for (const step of tc.steps) {
1526
+ const desc = step.desc.replace(/\n/g, "<br>");
1527
+ const res = step.result.replace(/\n/g, "<br>");
1528
+ lines.push(`| ${step.index + 1} | ${desc} | ${res} |`);
1529
+ }
1530
+ }
1531
+ lines.push("");
1104
1532
  }
1105
1533
  return lines.join("\n");
1106
1534
  }
@@ -1284,6 +1712,19 @@ async function main() {
1284
1712
  };
1285
1713
  }
1286
1714
  });
1715
+ server.tool("get_testcases", "Get all test cases for a task by its number (e.g. 302). Searches the testcase library for a matching module and returns all cases with steps.", GetTestcasesSchema.shape, async (params) => {
1716
+ try {
1717
+ return await handleGetTestcases(params, adapters, config.config.defaultSource);
1718
+ } catch (err) {
1719
+ return {
1720
+ content: [{
1721
+ type: "text",
1722
+ text: `Error: ${err.message}`
1723
+ }],
1724
+ isError: true
1725
+ };
1726
+ }
1727
+ });
1287
1728
  const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
1288
1729
  await server.connect(transport);
1289
1730
  }