ai-dev-requirements 0.1.9 → 0.1.11
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 +288 -21
- package/dist/index.mjs +288 -21
- 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 }
|
|
@@ -322,7 +325,139 @@ function getSetCookies(response) {
|
|
|
322
325
|
const raw = response.headers.get("set-cookie");
|
|
323
326
|
return raw ? [raw] : [];
|
|
324
327
|
}
|
|
325
|
-
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 parseOnesWikiPageRoute(input) {
|
|
335
|
+
if (!input.includes("/wiki/")) return null;
|
|
336
|
+
const match = (() => {
|
|
337
|
+
try {
|
|
338
|
+
const parsed = new URL(input);
|
|
339
|
+
return `${parsed.pathname}${parsed.hash}${parsed.search}`;
|
|
340
|
+
} catch {
|
|
341
|
+
return input;
|
|
342
|
+
}
|
|
343
|
+
})().match(/\/team\/([^/?#]+)\/space\/[^/?#]+\/page\/([^/?#]+)/);
|
|
344
|
+
if (!match?.[1] || !match[2]) return null;
|
|
345
|
+
return {
|
|
346
|
+
teamUuid: decodeURIComponent(match[1]),
|
|
347
|
+
wikiUuid: decodeURIComponent(match[2])
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function isOnesWikiUrlInput(input) {
|
|
351
|
+
return input.includes("/wiki/");
|
|
352
|
+
}
|
|
353
|
+
function htmlToPlainText(html) {
|
|
354
|
+
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();
|
|
355
|
+
}
|
|
356
|
+
function getTaskDetailText(task) {
|
|
357
|
+
return task.descriptionText?.trim() || htmlToPlainText(task.desc_rich ?? task.description ?? "");
|
|
358
|
+
}
|
|
359
|
+
function isRecord(value) {
|
|
360
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
361
|
+
}
|
|
362
|
+
function parseJsonRecord(value) {
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(value);
|
|
365
|
+
return isRecord(parsed) ? parsed : null;
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function asWikiBlocks(value) {
|
|
371
|
+
if (!Array.isArray(value)) return [];
|
|
372
|
+
return value.filter(isRecord);
|
|
373
|
+
}
|
|
374
|
+
function renderWikiTextRuns(value) {
|
|
375
|
+
if (!Array.isArray(value)) return "";
|
|
376
|
+
return value.map((run) => {
|
|
377
|
+
if (!isRecord(run)) return "";
|
|
378
|
+
const attributes = isRecord(run.attributes) ? run.attributes : {};
|
|
379
|
+
const insert = typeof run.insert === "string" ? run.insert.replace(/\u00A0/g, " ") : "";
|
|
380
|
+
const link = typeof attributes.link === "string" ? attributes.link : "";
|
|
381
|
+
if (link && insert.trim()) return `[${insert}](${link})`;
|
|
382
|
+
const taskName = typeof attributes.taskName === "string" ? attributes.taskName : "";
|
|
383
|
+
if (link && taskName) return `[${taskName}](${link})`;
|
|
384
|
+
return insert;
|
|
385
|
+
}).join("").replace(/\n{3,}/g, "\n\n").trim();
|
|
386
|
+
}
|
|
387
|
+
function renderWikiHeading(text, heading) {
|
|
388
|
+
if (!heading) return text;
|
|
389
|
+
const level = Math.min(Math.max(Math.trunc(heading), 1), 6);
|
|
390
|
+
return `${"#".repeat(level)} ${text}`;
|
|
391
|
+
}
|
|
392
|
+
function getWikiImageSource(block) {
|
|
393
|
+
const embedData = isRecord(block.embedData) ? block.embedData : {};
|
|
394
|
+
return typeof embedData.src === "string" ? embedData.src.trim() : "";
|
|
395
|
+
}
|
|
396
|
+
function renderWikiEmbed(block, context) {
|
|
397
|
+
if (block.embedType === "image") {
|
|
398
|
+
const src = getWikiImageSource(block);
|
|
399
|
+
if (src && !context.imageSources.includes(src)) context.imageSources.push(src);
|
|
400
|
+
return src ? `[Image: ${src}]` : "[Image]";
|
|
401
|
+
}
|
|
402
|
+
return block.embedType ? `[Embed: ${block.embedType}]` : "";
|
|
403
|
+
}
|
|
404
|
+
function escapeWikiTableCell(value) {
|
|
405
|
+
return value.replace(/\|/g, "\\|").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
406
|
+
}
|
|
407
|
+
function renderWikiCell(value, document, context) {
|
|
408
|
+
const blocks = asWikiBlocks(value);
|
|
409
|
+
if (!blocks.length) return "";
|
|
410
|
+
return blocks.map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join(" ").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
411
|
+
}
|
|
412
|
+
function renderWikiTable(block, document, context) {
|
|
413
|
+
const cols = typeof block.cols === "number" && block.cols > 0 ? Math.trunc(block.cols) : 0;
|
|
414
|
+
const children = Array.isArray(block.children) ? block.children.filter((child) => typeof child === "string") : [];
|
|
415
|
+
if (!cols || !children.length) return "";
|
|
416
|
+
const rows = [];
|
|
417
|
+
for (let index = 0; index < children.length; index += cols) {
|
|
418
|
+
const cells = children.slice(index, index + cols).map((childId) => escapeWikiTableCell(renderWikiCell(document[childId], document, context)));
|
|
419
|
+
while (cells.length < cols) cells.push("");
|
|
420
|
+
rows.push(`| ${cells.join(" | ")} |`);
|
|
421
|
+
}
|
|
422
|
+
if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
|
|
423
|
+
return rows.join("\n");
|
|
424
|
+
}
|
|
425
|
+
function renderWikiBlock(block, document, context) {
|
|
426
|
+
if (block.type === "table") return renderWikiTable(block, document, context);
|
|
427
|
+
if (block.type === "embed") return renderWikiEmbed(block, context);
|
|
428
|
+
const text = renderWikiTextRuns(block.text);
|
|
429
|
+
if (!text) return "";
|
|
430
|
+
if (block.type === "list") {
|
|
431
|
+
const level = typeof block.level === "number" ? Math.max(Math.trunc(block.level), 1) : 1;
|
|
432
|
+
return `${" ".repeat(level - 1)}${block.ordered ? `${block.start ?? 1}.` : "-"} ${text}`;
|
|
433
|
+
}
|
|
434
|
+
return renderWikiHeading(text, block.heading);
|
|
435
|
+
}
|
|
436
|
+
function renderWikiContent(content, context = { imageSources: [] }) {
|
|
437
|
+
const trimmed = content.trim();
|
|
438
|
+
if (!trimmed) return "";
|
|
439
|
+
const document = parseJsonRecord(trimmed);
|
|
440
|
+
if (!document) return trimmed;
|
|
441
|
+
if (!("blocks" in document)) return trimmed;
|
|
442
|
+
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();
|
|
443
|
+
}
|
|
444
|
+
function mimeTypeFromFileName(fileName) {
|
|
445
|
+
const normalized = fileName.toLowerCase();
|
|
446
|
+
if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg";
|
|
447
|
+
if (normalized.endsWith(".gif")) return "image/gif";
|
|
448
|
+
if (normalized.endsWith(".webp")) return "image/webp";
|
|
449
|
+
if (normalized.endsWith(".svg")) return "image/svg+xml";
|
|
450
|
+
return "image/png";
|
|
451
|
+
}
|
|
452
|
+
function attachmentNameFromPath(path) {
|
|
453
|
+
const name = path.split("/").pop() || path;
|
|
454
|
+
try {
|
|
455
|
+
return decodeURIComponent(name);
|
|
456
|
+
} catch {
|
|
457
|
+
return name;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function toRequirement(task, description = "", attachments = []) {
|
|
326
461
|
return {
|
|
327
462
|
id: task.uuid,
|
|
328
463
|
source: "ones",
|
|
@@ -337,7 +472,7 @@ function toRequirement(task, description = "") {
|
|
|
337
472
|
createdAt: "",
|
|
338
473
|
updatedAt: "",
|
|
339
474
|
dueDate: null,
|
|
340
|
-
attachments
|
|
475
|
+
attachments,
|
|
341
476
|
raw: task
|
|
342
477
|
};
|
|
343
478
|
}
|
|
@@ -617,18 +752,86 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
617
752
|
* Fetch wiki page content via REST API.
|
|
618
753
|
* Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
|
|
619
754
|
*/
|
|
620
|
-
async
|
|
755
|
+
async fetchWikiPageDetail(wikiUuid, teamUuid) {
|
|
756
|
+
const session = await this.login();
|
|
757
|
+
const wikiTeamUuid = teamUuid ?? session.teamUuid;
|
|
758
|
+
const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/page/${wikiUuid}/detail`;
|
|
759
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
760
|
+
if (!response.ok) return {};
|
|
761
|
+
return response.json();
|
|
762
|
+
}
|
|
763
|
+
buildWikiImageUrl(session, refUuid, source, token, teamUuid) {
|
|
764
|
+
const encodedRefUuid = encodeURIComponent(refUuid);
|
|
765
|
+
const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
766
|
+
const encodedToken = encodeURIComponent(token);
|
|
767
|
+
const wikiTeamUuid = teamUuid ?? session.teamUuid;
|
|
768
|
+
return `${this.config.apiBase}/wiki/api/wiki/editor/${wikiTeamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
|
|
769
|
+
}
|
|
770
|
+
async fetchWikiContent(wikiUuid, teamUuid) {
|
|
621
771
|
const session = await this.login();
|
|
622
|
-
const
|
|
772
|
+
const wikiTeamUuid = teamUuid ?? session.teamUuid;
|
|
773
|
+
const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/online_page/${wikiUuid}/content`;
|
|
623
774
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
624
|
-
if (!response.ok) return
|
|
625
|
-
|
|
775
|
+
if (!response.ok) return {
|
|
776
|
+
content: "",
|
|
777
|
+
attachments: []
|
|
778
|
+
};
|
|
779
|
+
const data = await response.json();
|
|
780
|
+
const renderContext = { imageSources: [] };
|
|
781
|
+
const content = renderWikiContent(typeof data.content === "string" ? data.content : "", renderContext);
|
|
782
|
+
const token = typeof data.token === "string" ? data.token : "";
|
|
783
|
+
if (!renderContext.imageSources.length || !token) return {
|
|
784
|
+
content,
|
|
785
|
+
attachments: []
|
|
786
|
+
};
|
|
787
|
+
const detail = await this.fetchWikiPageDetail(wikiUuid, wikiTeamUuid);
|
|
788
|
+
const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
|
|
789
|
+
if (!refUuid) return {
|
|
790
|
+
content,
|
|
791
|
+
attachments: []
|
|
792
|
+
};
|
|
793
|
+
return {
|
|
794
|
+
content,
|
|
795
|
+
attachments: renderContext.imageSources.map((source, index) => ({
|
|
796
|
+
id: `${wikiUuid}-image-${index + 1}`,
|
|
797
|
+
name: attachmentNameFromPath(source),
|
|
798
|
+
url: this.buildWikiImageUrl(session, refUuid, source, token, wikiTeamUuid),
|
|
799
|
+
mimeType: mimeTypeFromFileName(source),
|
|
800
|
+
size: 0
|
|
801
|
+
}))
|
|
802
|
+
};
|
|
626
803
|
}
|
|
627
804
|
/**
|
|
628
|
-
* Fetch a single task by UUID or number (e.g. "#
|
|
805
|
+
* Fetch a single task by UUID or number (e.g. "#1001" or "1001").
|
|
629
806
|
* If a number is given, searches first to resolve the UUID.
|
|
630
807
|
*/
|
|
631
808
|
async getRequirement(params) {
|
|
809
|
+
const wikiRoute = parseOnesWikiPageRoute(params.id);
|
|
810
|
+
if (wikiRoute) {
|
|
811
|
+
const rendered = await this.fetchWikiContent(wikiRoute.wikiUuid, wikiRoute.teamUuid);
|
|
812
|
+
return {
|
|
813
|
+
id: wikiRoute.wikiUuid,
|
|
814
|
+
source: "ones",
|
|
815
|
+
title: `Wiki ${wikiRoute.wikiUuid}`,
|
|
816
|
+
description: rendered.content,
|
|
817
|
+
status: "open",
|
|
818
|
+
priority: "medium",
|
|
819
|
+
type: "feature",
|
|
820
|
+
labels: [],
|
|
821
|
+
reporter: "",
|
|
822
|
+
assignee: null,
|
|
823
|
+
createdAt: "",
|
|
824
|
+
updatedAt: "",
|
|
825
|
+
dueDate: null,
|
|
826
|
+
attachments: rendered.attachments,
|
|
827
|
+
raw: {
|
|
828
|
+
input: params.id,
|
|
829
|
+
teamUuid: wikiRoute.teamUuid,
|
|
830
|
+
wikiUuid: wikiRoute.wikiUuid
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (isOnesWikiUrlInput(params.id)) throw new Error("ONES: Unsupported wiki page URL. Expected /wiki/#/team/{teamUuid}/space/{spaceUuid}/page/{wikiUuid}");
|
|
632
835
|
let taskUuid = params.id;
|
|
633
836
|
const numMatch = taskUuid.match(/^#?(\d+)$/);
|
|
634
837
|
if (numMatch) {
|
|
@@ -650,13 +853,27 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
650
853
|
}
|
|
651
854
|
const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
|
|
652
855
|
if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
|
|
856
|
+
const wikiRefs = /* @__PURE__ */ new Map();
|
|
857
|
+
for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
|
|
858
|
+
title: wiki.title,
|
|
859
|
+
uuid: wiki.uuid
|
|
860
|
+
});
|
|
861
|
+
const detailForLinkExtraction = [
|
|
862
|
+
task.description,
|
|
863
|
+
task.descriptionText,
|
|
864
|
+
task.desc_rich
|
|
865
|
+
].filter(Boolean).join("\n");
|
|
866
|
+
for (const wikiUuid of extractWikiPageUuidsFromText(detailForLinkExtraction)) if (!wikiRefs.has(wikiUuid)) wikiRefs.set(wikiUuid, {
|
|
867
|
+
title: `Wiki ${wikiUuid}`,
|
|
868
|
+
uuid: wikiUuid
|
|
869
|
+
});
|
|
870
|
+
const wikiContents = await Promise.all([...wikiRefs.values()].map(async (wiki) => {
|
|
871
|
+
const rendered = await this.fetchWikiContent(wiki.uuid);
|
|
656
872
|
return {
|
|
657
873
|
title: wiki.title,
|
|
658
874
|
uuid: wiki.uuid,
|
|
659
|
-
content
|
|
875
|
+
content: rendered.content,
|
|
876
|
+
attachments: rendered.attachments
|
|
660
877
|
};
|
|
661
878
|
}));
|
|
662
879
|
const parts = [];
|
|
@@ -695,7 +912,18 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
695
912
|
else parts.push("(No content available)");
|
|
696
913
|
}
|
|
697
914
|
}
|
|
698
|
-
|
|
915
|
+
const detailText = getTaskDetailText(task);
|
|
916
|
+
const hasWikiContent = wikiContents.some((wiki) => wiki.content.trim());
|
|
917
|
+
if (detailText && !hasWikiContent) {
|
|
918
|
+
parts.push("");
|
|
919
|
+
parts.push("---");
|
|
920
|
+
parts.push("");
|
|
921
|
+
parts.push("## Requirement Detail");
|
|
922
|
+
parts.push("");
|
|
923
|
+
parts.push(detailText);
|
|
924
|
+
}
|
|
925
|
+
const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
|
|
926
|
+
return toRequirement(task, parts.join("\n"), wikiAttachments);
|
|
699
927
|
}
|
|
700
928
|
/**
|
|
701
929
|
* Search tasks assigned to current user via GraphQL.
|
|
@@ -1109,14 +1337,14 @@ function loadConfig(startDir) {
|
|
|
1109
1337
|
//#endregion
|
|
1110
1338
|
//#region src/tools/get-issue-detail.ts
|
|
1111
1339
|
const GetIssueDetailSchema = zod_v4.z.object({
|
|
1112
|
-
issueId: zod_v4.z.string().describe("The issue task ID or key (e.g. \"
|
|
1340
|
+
issueId: zod_v4.z.string().describe("The issue task ID or key (e.g. \"mock-issue-uuid\" or \"task-mock-issue-uuid\")"),
|
|
1113
1341
|
source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1114
1342
|
});
|
|
1115
1343
|
/**
|
|
1116
1344
|
* Download an image from URL and return as base64 data URI.
|
|
1117
1345
|
* Returns null if download fails.
|
|
1118
1346
|
*/
|
|
1119
|
-
async function downloadImageAsBase64(url) {
|
|
1347
|
+
async function downloadImageAsBase64$1(url) {
|
|
1120
1348
|
try {
|
|
1121
1349
|
const res = await fetch(url, { redirect: "follow" });
|
|
1122
1350
|
if (!res.ok) return null;
|
|
@@ -1142,7 +1370,7 @@ async function handleGetIssueDetail(input, adapters, defaultSource) {
|
|
|
1142
1370
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1143
1371
|
const detail = await adapter.getIssueDetail({ issueId: input.issueId });
|
|
1144
1372
|
const imageUrls = detail.descriptionRich ? extractImageUrls(detail.descriptionRich) : [];
|
|
1145
|
-
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64(url)));
|
|
1373
|
+
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64$1(url)));
|
|
1146
1374
|
const content = [{
|
|
1147
1375
|
type: "text",
|
|
1148
1376
|
text: formatIssueDetail(detail)
|
|
@@ -1184,7 +1412,7 @@ function formatIssueDetail(detail) {
|
|
|
1184
1412
|
//#endregion
|
|
1185
1413
|
//#region src/tools/get-related-issues.ts
|
|
1186
1414
|
const GetRelatedIssuesSchema = zod_v4.z.object({
|
|
1187
|
-
taskId: zod_v4.z.string().describe("The parent task ID or key (e.g. \"
|
|
1415
|
+
taskId: zod_v4.z.string().describe("The parent task ID or key (e.g. \"mock-task-uuid\" or \"task-mock-task-uuid\")"),
|
|
1188
1416
|
source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1189
1417
|
});
|
|
1190
1418
|
async function handleGetRelatedIssues(input, adapters, defaultSource) {
|
|
@@ -1225,18 +1453,57 @@ function formatRelatedIssues(issues) {
|
|
|
1225
1453
|
//#endregion
|
|
1226
1454
|
//#region src/tools/get-requirement.ts
|
|
1227
1455
|
const GetRequirementSchema = zod_v4.z.object({
|
|
1228
|
-
id: zod_v4.z.string().describe("The requirement/issue ID"),
|
|
1456
|
+
id: zod_v4.z.string().describe("The requirement/issue ID, task number, or ONES wiki page URL"),
|
|
1229
1457
|
source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1230
1458
|
});
|
|
1459
|
+
async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
|
|
1460
|
+
try {
|
|
1461
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1462
|
+
if (!res.ok) return null;
|
|
1463
|
+
const mimeType = (res.headers.get("content-type") ?? fallbackMimeType).split(";")[0].trim() || fallbackMimeType;
|
|
1464
|
+
return {
|
|
1465
|
+
base64: Buffer.from(await res.arrayBuffer()).toString("base64"),
|
|
1466
|
+
mimeType
|
|
1467
|
+
};
|
|
1468
|
+
} catch {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function isImageAttachment(attachment) {
|
|
1473
|
+
if (attachment.mimeType.startsWith("image/")) return true;
|
|
1474
|
+
return /\.(?:png|jpe?g|gif|webp|svg)$/i.test(attachment.url);
|
|
1475
|
+
}
|
|
1476
|
+
function displayAttachmentUrl(url) {
|
|
1477
|
+
try {
|
|
1478
|
+
const parsed = new URL(url);
|
|
1479
|
+
parsed.search = "";
|
|
1480
|
+
parsed.hash = "";
|
|
1481
|
+
return parsed.toString();
|
|
1482
|
+
} catch {
|
|
1483
|
+
return url.replace(/[?#].*$/, "");
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1231
1486
|
async function handleGetRequirement(input, adapters, defaultSource) {
|
|
1232
1487
|
const sourceType = input.source ?? defaultSource;
|
|
1233
1488
|
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
1234
1489
|
const adapter = adapters.get(sourceType);
|
|
1235
1490
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1236
|
-
|
|
1491
|
+
const requirement = await adapter.getRequirement({ id: input.id });
|
|
1492
|
+
const imageAttachments = requirement.attachments.filter(isImageAttachment);
|
|
1493
|
+
const imageResults = await Promise.all(imageAttachments.map((attachment) => downloadImageAsBase64(attachment.url, attachment.mimeType)));
|
|
1494
|
+
const content = [{
|
|
1237
1495
|
type: "text",
|
|
1238
|
-
text: formatRequirement(
|
|
1239
|
-
}]
|
|
1496
|
+
text: formatRequirement(requirement)
|
|
1497
|
+
}];
|
|
1498
|
+
for (const image of imageResults) {
|
|
1499
|
+
if (!image) continue;
|
|
1500
|
+
content.push({
|
|
1501
|
+
type: "image",
|
|
1502
|
+
data: image.base64,
|
|
1503
|
+
mimeType: image.mimeType
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
return { content };
|
|
1240
1507
|
}
|
|
1241
1508
|
function formatRequirement(req) {
|
|
1242
1509
|
const lines = [
|
|
@@ -1257,7 +1524,7 @@ function formatRequirement(req) {
|
|
|
1257
1524
|
lines.push("", "## Description", "", req.description || "_No description_");
|
|
1258
1525
|
if (req.attachments.length > 0) {
|
|
1259
1526
|
lines.push("", "## Attachments");
|
|
1260
|
-
for (const att of req.attachments) lines.push(`- [${att.name}](${att.url}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1527
|
+
for (const att of req.attachments) lines.push(`- [${att.name}](${displayAttachmentUrl(att.url)}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1261
1528
|
}
|
|
1262
1529
|
return lines.join("\n");
|
|
1263
1530
|
}
|