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/README.md +68 -14
- package/README.zh-CN.md +68 -14
- package/dist/index.cjs +457 -16
- package/dist/index.mjs +457 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/dev-workflow/SKILL.md +186 -123
- package/skills/dev-workflow/references/task-types.md +148 -97
- package/skills/dev-workflow/references/templates/code-dev-task.md +34 -21
- package/skills/dev-workflow/references/templates/code-fix-task.md +34 -20
- package/skills/dev-workflow/references/templates/code-refactor-task.md +34 -22
- package/skills/dev-workflow/references/templates/doc-write-task.md +33 -21
- package/skills/dev-workflow/references/templates/research-task.md +34 -22
- package/skills/dev-workflow/references/templates/test-task.md +33 -22
- package/skills/dev-workflow/references/workflow.md +290 -118
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
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/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
|
-
|
|
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
|
|
605
|
-
const
|
|
606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|