@jiggai/recipes 0.2.23 → 0.2.25
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 +1 -1
- package/docs/CLEANUP_TODO.md +31 -0
- package/docs/CODE_SMELLS_TRACKER.md +42 -0
- package/docs/SMELLS_TODO.md +23 -0
- package/docs/TEST_COVERAGE_PROGRESS.md +37 -0
- package/index.ts +385 -2168
- package/openclaw.plugin.json +1 -1
- package/package.json +20 -2
- package/src/handlers/cron.ts +309 -0
- package/src/handlers/install.ts +160 -0
- package/src/handlers/recipes.ts +119 -0
- package/src/handlers/scaffold.ts +141 -0
- package/src/handlers/team.ts +395 -0
- package/src/handlers/tickets.ts +304 -0
- package/src/lib/agent-config.ts +48 -0
- package/src/lib/bindings.ts +9 -59
- package/src/lib/cleanup-workspaces.ts +4 -4
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +11 -0
- package/src/lib/cron-utils.ts +54 -0
- package/src/lib/fs-utils.ts +33 -0
- package/src/lib/json-utils.ts +17 -0
- package/src/lib/lanes.ts +14 -12
- package/src/lib/prompt.ts +47 -0
- package/src/lib/recipe-frontmatter.ts +65 -21
- package/src/lib/recipe-id.ts +49 -0
- package/src/lib/recipes-config.ts +166 -0
- package/src/lib/recipes.ts +57 -0
- package/src/lib/remove-team.ts +17 -23
- package/src/lib/scaffold-utils.ts +95 -0
- package/src/lib/skill-install.ts +22 -0
- package/src/lib/stable-stringify.ts +21 -0
- package/src/lib/template.ts +10 -0
- package/src/lib/ticket-finder.ts +40 -23
- package/src/lib/ticket-workflow.ts +32 -65
- package/src/lib/workspace.ts +33 -0
- package/src/marketplaceFetch.ts +1 -1
- package/src/toolsInvoke.ts +41 -32
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny, safe template renderer: replaces {{key}} with vars[key].
|
|
3
|
+
* No conditionals, no eval.
|
|
4
|
+
*/
|
|
5
|
+
export function renderTemplate(raw: string, vars: Record<string, string>): string {
|
|
6
|
+
return raw.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_m, key) => {
|
|
7
|
+
const v = vars[key];
|
|
8
|
+
return typeof v === "string" ? v : "";
|
|
9
|
+
});
|
|
10
|
+
}
|
package/src/lib/ticket-finder.ts
CHANGED
|
@@ -1,49 +1,66 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import fs from
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { fileExists } from "./fs-utils";
|
|
4
|
+
import { ticketStageDir, type TicketLane } from "./lanes";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
await fs.access(p);
|
|
7
|
-
return true;
|
|
8
|
-
} catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type TicketLane = 'backlog' | 'in-progress' | 'testing' | 'done';
|
|
6
|
+
export type { TicketLane };
|
|
14
7
|
|
|
15
8
|
export function laneDir(teamDir: string, lane: TicketLane) {
|
|
16
|
-
return
|
|
9
|
+
return ticketStageDir(teamDir, lane);
|
|
17
10
|
}
|
|
18
11
|
|
|
12
|
+
const LANE_SEARCH_ORDER: TicketLane[] = ["backlog", "in-progress", "testing", "done"];
|
|
13
|
+
|
|
19
14
|
export function allLaneDirs(teamDir: string) {
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
return LANE_SEARCH_ORDER.map((lane) => ticketStageDir(teamDir, lane));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compute the next ticket number (max existing + 1) by scanning ticket lane dirs.
|
|
20
|
+
* Used when creating new tickets (e.g. dispatch).
|
|
21
|
+
*/
|
|
22
|
+
export async function computeNextTicketNumber(teamDir: string): Promise<number> {
|
|
23
|
+
let max = 0;
|
|
24
|
+
for (const lane of LANE_SEARCH_ORDER) {
|
|
25
|
+
const dir = ticketStageDir(teamDir, lane);
|
|
26
|
+
if (!(await fileExists(dir))) continue;
|
|
27
|
+
const files = await fs.readdir(dir);
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
const m = f.match(TICKET_FILENAME_REGEX);
|
|
30
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return max + 1;
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
/** Regex for ticket filenames: 0001-slug.md */
|
|
37
|
+
export const TICKET_FILENAME_REGEX = /^(\d{4})-(.+)\.md$/;
|
|
38
|
+
|
|
28
39
|
export function parseTicketArg(ticketArgRaw: string) {
|
|
29
40
|
const raw = String(ticketArgRaw ?? "").trim();
|
|
30
41
|
|
|
31
42
|
// Accept "30" as shorthand for ticket 0030.
|
|
32
43
|
const padded = raw.match(/^\d+$/) && raw.length < 4 ? raw.padStart(4, "0") : raw;
|
|
33
44
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
: (padded.match(/^(\d{4})-/)?.[1] ?? null);
|
|
45
|
+
const idMatch = padded.match(/^(\d{4})-/);
|
|
46
|
+
const ticketNum = padded.match(/^\d{4}$/) ? padded : (idMatch ? idMatch[1] : null);
|
|
37
47
|
|
|
38
48
|
return { ticketArg: padded, ticketNum };
|
|
39
49
|
}
|
|
40
50
|
|
|
51
|
+
/** Parse ticket number and slug from filename. Returns null if not a valid ticket filename. */
|
|
52
|
+
export function parseTicketFilename(filename: string): { ticketNumStr: string; slug: string } | null {
|
|
53
|
+
const m = filename.match(TICKET_FILENAME_REGEX);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
return { ticketNumStr: m[1], slug: m[2] };
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
export async function findTicketFile(opts: {
|
|
42
59
|
teamDir: string;
|
|
43
60
|
ticket: string;
|
|
44
61
|
lanes?: TicketLane[];
|
|
45
62
|
}) {
|
|
46
|
-
const lanes = opts.lanes ??
|
|
63
|
+
const lanes = opts.lanes ?? LANE_SEARCH_ORDER;
|
|
47
64
|
const { ticketArg, ticketNum } = parseTicketArg(String(opts.ticket));
|
|
48
65
|
|
|
49
66
|
for (const lane of lanes) {
|
|
@@ -1,39 +1,36 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return true;
|
|
10
|
-
} catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
4
|
+
import { fileExists } from "./fs-utils";
|
|
5
|
+
import { ensureLaneDir } from "./lanes";
|
|
6
|
+
import { DEFAULT_TICKET_NUMBER } from "./constants";
|
|
7
|
+
import { findTicketFile as findTicketFileFromFinder } from "./ticket-finder";
|
|
8
|
+
import { parseTicketFilename } from "./ticket-finder";
|
|
14
9
|
|
|
15
10
|
async function ensureDir(p: string) {
|
|
16
11
|
await fs.mkdir(p, { recursive: true });
|
|
17
12
|
}
|
|
18
13
|
|
|
14
|
+
function patchTicketFields(
|
|
15
|
+
md: string,
|
|
16
|
+
opts: { ownerSafe: string; status: string; assignmentRel: string }
|
|
17
|
+
): string {
|
|
18
|
+
let out = md;
|
|
19
|
+
if (out.match(/^Owner:\s.*$/m)) out = out.replace(/^Owner:\s.*$/m, `Owner: ${opts.ownerSafe}`);
|
|
20
|
+
else out = out.replace(/^(# .+\n)/, `$1\nOwner: ${opts.ownerSafe}\n`);
|
|
21
|
+
|
|
22
|
+
if (out.match(/^Status:\s.*$/m)) out = out.replace(/^Status:\s.*$/m, `Status: ${opts.status}`);
|
|
23
|
+
else out = out.replace(/^(# .+\n)/, `$1\nStatus: ${opts.status}\n`);
|
|
24
|
+
|
|
25
|
+
if (out.match(/^Assignment:\s.*$/m)) out = out.replace(/^Assignment:\s.*$/m, `Assignment: ${opts.assignmentRel}`);
|
|
26
|
+
else out = out.replace(/^Owner:.*$/m, (line) => `${line}\nAssignment: ${opts.assignmentRel}`);
|
|
27
|
+
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Re-export for callers expecting (teamDir, ticketArg) signature. Delegates to ticket-finder. */
|
|
19
32
|
export async function findTicketFile(teamDir: string, ticketArgRaw: string) {
|
|
20
|
-
|
|
21
|
-
const searchDirs = [stageDir('backlog'), stageDir('in-progress'), stageDir('testing'), stageDir('done')];
|
|
22
|
-
|
|
23
|
-
const ticketArg = String(ticketArgRaw ?? '').trim();
|
|
24
|
-
const padded = ticketArg.match(/^\d+$/) && ticketArg.length < 4 ? ticketArg.padStart(4, '0') : ticketArg;
|
|
25
|
-
const ticketNum = padded.match(/^\d{4}$/) ? padded : (padded.match(/^(\d{4})-/)?.[1] ?? null);
|
|
26
|
-
|
|
27
|
-
for (const dir of searchDirs) {
|
|
28
|
-
if (!(await fileExists(dir))) continue;
|
|
29
|
-
const files = await fs.readdir(dir);
|
|
30
|
-
for (const f of files) {
|
|
31
|
-
if (!f.endsWith('.md')) continue;
|
|
32
|
-
if (ticketNum && f.startsWith(`${ticketNum}-`)) return path.join(dir, f);
|
|
33
|
-
if (!ticketNum && f.replace(/\.md$/, '') === padded) return path.join(dir, f);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
33
|
+
return findTicketFileFromFinder({ teamDir, ticket: ticketArgRaw });
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
export async function takeTicket(opts: { teamDir: string; ticket: string; owner?: string; overwriteAssignment: boolean }) {
|
|
@@ -50,33 +47,18 @@ export async function takeTicket(opts: { teamDir: string; ticket: string; owner?
|
|
|
50
47
|
const filename = path.basename(srcPath);
|
|
51
48
|
const destPath = path.join(inProgressDir, filename);
|
|
52
49
|
|
|
53
|
-
const
|
|
54
|
-
const ticketNumStr
|
|
55
|
-
const slug = m?.[2] ?? 'ticket';
|
|
50
|
+
const parsed = parseTicketFilename(filename) ?? { ticketNumStr: opts.ticket.match(/^\d{4}$/) ? opts.ticket : DEFAULT_TICKET_NUMBER, slug: "ticket" };
|
|
51
|
+
const { ticketNumStr, slug } = parsed;
|
|
56
52
|
|
|
57
53
|
const assignmentsDir = path.join(teamDir, 'work', 'assignments');
|
|
58
54
|
await ensureDir(assignmentsDir);
|
|
59
55
|
const assignmentPath = path.join(assignmentsDir, `${ticketNumStr}-assigned-${ownerSafe}.md`);
|
|
60
56
|
const assignmentRel = path.relative(teamDir, assignmentPath);
|
|
61
57
|
|
|
62
|
-
const patch = (md: string) => {
|
|
63
|
-
let out = md;
|
|
64
|
-
if (out.match(/^Owner:\s.*$/m)) out = out.replace(/^Owner:\s.*$/m, `Owner: ${ownerSafe}`);
|
|
65
|
-
else out = out.replace(/^(# .+\n)/, `$1\nOwner: ${ownerSafe}\n`);
|
|
66
|
-
|
|
67
|
-
if (out.match(/^Status:\s.*$/m)) out = out.replace(/^Status:\s.*$/m, 'Status: in-progress');
|
|
68
|
-
else out = out.replace(/^(# .+\n)/, `$1\nStatus: in-progress\n`);
|
|
69
|
-
|
|
70
|
-
if (out.match(/^Assignment:\s.*$/m)) out = out.replace(/^Assignment:\s.*$/m, `Assignment: ${assignmentRel}`);
|
|
71
|
-
else out = out.replace(/^Owner:.*$/m, (line) => `${line}\nAssignment: ${assignmentRel}`);
|
|
72
|
-
|
|
73
|
-
return out;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
58
|
const alreadyInProgress = srcPath === destPath;
|
|
77
59
|
|
|
78
60
|
const md = await fs.readFile(srcPath, 'utf8');
|
|
79
|
-
const nextMd =
|
|
61
|
+
const nextMd = patchTicketFields(md, { ownerSafe, status: 'in-progress', assignmentRel });
|
|
80
62
|
await fs.writeFile(srcPath, nextMd, 'utf8');
|
|
81
63
|
|
|
82
64
|
if (!alreadyInProgress) {
|
|
@@ -109,33 +91,18 @@ export async function handoffTicket(opts: { teamDir: string; ticket: string; tes
|
|
|
109
91
|
const filename = path.basename(srcPath);
|
|
110
92
|
const destPath = path.join(testingDir, filename);
|
|
111
93
|
|
|
112
|
-
const
|
|
113
|
-
const ticketNumStr
|
|
114
|
-
const slug = m?.[2] ?? 'ticket';
|
|
94
|
+
const parsed = parseTicketFilename(filename) ?? { ticketNumStr: opts.ticket.match(/^\d{4}$/) ? opts.ticket : DEFAULT_TICKET_NUMBER, slug: "ticket" };
|
|
95
|
+
const { ticketNumStr, slug } = parsed;
|
|
115
96
|
|
|
116
97
|
const assignmentsDir = path.join(teamDir, 'work', 'assignments');
|
|
117
98
|
await ensureDir(assignmentsDir);
|
|
118
99
|
const assignmentPath = path.join(assignmentsDir, `${ticketNumStr}-assigned-${testerSafe}.md`);
|
|
119
100
|
const assignmentRel = path.relative(teamDir, assignmentPath);
|
|
120
101
|
|
|
121
|
-
const patch = (md: string) => {
|
|
122
|
-
let out = md;
|
|
123
|
-
if (out.match(/^Owner:\s.*$/m)) out = out.replace(/^Owner:\s.*$/m, `Owner: ${testerSafe}`);
|
|
124
|
-
else out = out.replace(/^(# .+\n)/, `$1\nOwner: ${testerSafe}\n`);
|
|
125
|
-
|
|
126
|
-
if (out.match(/^Status:\s.*$/m)) out = out.replace(/^Status:\s.*$/m, 'Status: testing');
|
|
127
|
-
else out = out.replace(/^(# .+\n)/, `$1\nStatus: testing\n`);
|
|
128
|
-
|
|
129
|
-
if (out.match(/^Assignment:\s.*$/m)) out = out.replace(/^Assignment:\s.*$/m, `Assignment: ${assignmentRel}`);
|
|
130
|
-
else out = out.replace(/^Owner:.*$/m, (line) => `${line}\nAssignment: ${assignmentRel}`);
|
|
131
|
-
|
|
132
|
-
return out;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
102
|
const alreadyInTesting = srcPath === destPath;
|
|
136
103
|
|
|
137
104
|
const md = await fs.readFile(srcPath, 'utf8');
|
|
138
|
-
const nextMd =
|
|
105
|
+
const nextMd = patchTicketFields(md, { ownerSafe: testerSafe, status: 'testing', assignmentRel });
|
|
139
106
|
await fs.writeFile(srcPath, nextMd, 'utf8');
|
|
140
107
|
|
|
141
108
|
if (!alreadyInTesting) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import { ensureDir } from "./fs-utils";
|
|
4
|
+
import { ticketStageDir } from "./lanes";
|
|
5
|
+
|
|
6
|
+
export function resolveWorkspaceRoot(api: OpenClawPluginApi): string {
|
|
7
|
+
const root = api.config.agents?.defaults?.workspace;
|
|
8
|
+
if (!root) throw new Error("agents.defaults.workspace is not set in config");
|
|
9
|
+
return root;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveTeamDir(api: OpenClawPluginApi, teamId: string): string {
|
|
13
|
+
const workspaceRoot = resolveWorkspaceRoot(api);
|
|
14
|
+
return path.resolve(workspaceRoot, "..", "workspace-" + teamId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function ensureTicketStageDirs(teamDir: string): Promise<void> {
|
|
18
|
+
await Promise.all([
|
|
19
|
+
ensureDir(path.join(teamDir, "work")),
|
|
20
|
+
ensureDir(ticketStageDir(teamDir, "backlog")),
|
|
21
|
+
ensureDir(ticketStageDir(teamDir, "in-progress")),
|
|
22
|
+
ensureDir(ticketStageDir(teamDir, "testing")),
|
|
23
|
+
ensureDir(ticketStageDir(teamDir, "done")),
|
|
24
|
+
ensureDir(ticketStageDir(teamDir, "assignments")),
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function resolveTeamContext(api: OpenClawPluginApi, teamId: string): Promise<{ workspaceRoot: string; teamDir: string }> {
|
|
29
|
+
const workspaceRoot = resolveWorkspaceRoot(api);
|
|
30
|
+
const teamDir = path.resolve(workspaceRoot, "..", "workspace-" + teamId);
|
|
31
|
+
await ensureTicketStageDirs(teamDir);
|
|
32
|
+
return { workspaceRoot, teamDir };
|
|
33
|
+
}
|
package/src/marketplaceFetch.ts
CHANGED
|
@@ -17,7 +17,7 @@ export async function fetchMarketplaceRecipeMarkdown(params: {
|
|
|
17
17
|
throw new Error(`Registry lookup failed (${metaRes.status}): ${metaUrl}\n\n${hint}`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const metaData = (await metaRes.json()) as
|
|
20
|
+
const metaData = (await metaRes.json()) as { sourceUrl?: string };
|
|
21
21
|
const recipe = metaData?.recipe;
|
|
22
22
|
const sourceUrl = String(recipe?.sourceUrl ?? "").trim();
|
|
23
23
|
if (!metaData?.ok || !sourceUrl) {
|
package/src/toolsInvoke.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
// flag "file read + network send" when both patterns live in the same file.
|
|
3
3
|
// This module intentionally contains the network call (fetch) but no filesystem reads.
|
|
4
4
|
|
|
5
|
+
export const TOOLS_INVOKE_TIMEOUT_MS = 30_000;
|
|
6
|
+
export const RETRY_DELAY_BASE_MS = 150;
|
|
7
|
+
export const GATEWAY_DEFAULT_PORT = 18789;
|
|
8
|
+
|
|
5
9
|
export type ToolTextResult = { content?: Array<{ type: string; text?: string }> };
|
|
6
10
|
|
|
7
11
|
export type ToolsInvokeRequest = {
|
|
@@ -18,47 +22,52 @@ type ToolsInvokeResponse = {
|
|
|
18
22
|
error?: { message?: string } | string;
|
|
19
23
|
};
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
26
|
+
|
|
27
|
+
function parseToolsInvokeError(json: ToolsInvokeResponse, status: number): string {
|
|
28
|
+
const msg =
|
|
29
|
+
(typeof json.error === "object" && json.error?.message) ||
|
|
30
|
+
(typeof json.error === "string" ? json.error : null) ||
|
|
31
|
+
`tools/invoke failed (${status})`;
|
|
32
|
+
return msg;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function doSingleToolsInvoke<T>(url: string, token: string, req: ToolsInvokeRequest): Promise<T> {
|
|
36
|
+
const ac = new AbortController();
|
|
37
|
+
const t = setTimeout(() => ac.abort(), TOOLS_INVOKE_TIMEOUT_MS);
|
|
38
|
+
const res = await fetch(url, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
signal: ac.signal,
|
|
41
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
42
|
+
body: JSON.stringify(req),
|
|
43
|
+
}).finally(() => clearTimeout(t));
|
|
44
|
+
|
|
45
|
+
const json = (await res.json()) as ToolsInvokeResponse;
|
|
46
|
+
if (!res.ok || !json.ok) throw new Error(parseToolsInvokeError(json, res.status));
|
|
47
|
+
return json.result as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Invoke a tool via gateway /tools/invoke (with retries).
|
|
52
|
+
* @param api - OpenClaw plugin API
|
|
53
|
+
* @param req - Tool name, action, args, optional sessionKey
|
|
54
|
+
* @returns Tool result (typed via generic)
|
|
55
|
+
* @throws On missing token, HTTP error, or after retries
|
|
56
|
+
*/
|
|
57
|
+
export async function toolsInvoke<T = unknown>(api: OpenClawPluginApi, req: ToolsInvokeRequest): Promise<T> {
|
|
58
|
+
const port = api.config.gateway?.port ?? GATEWAY_DEFAULT_PORT;
|
|
23
59
|
const token = api.config.gateway?.auth?.token;
|
|
24
60
|
if (!token) throw new Error("Missing gateway.auth.token in openclaw config (required for tools/invoke)");
|
|
25
61
|
|
|
26
|
-
// We sometimes see transient undici network errors in the CLI environment
|
|
27
|
-
// (ECONNRESET/ECONNREFUSED) even when the Gateway is healthy.
|
|
28
|
-
// A small retry makes recipe cron reconciliation much less flaky.
|
|
29
62
|
const url = `http://127.0.0.1:${port}/tools/invoke`;
|
|
30
|
-
|
|
31
63
|
let lastErr: unknown = null;
|
|
64
|
+
|
|
32
65
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
33
66
|
try {
|
|
34
|
-
|
|
35
|
-
const timeoutMs = 30_000;
|
|
36
|
-
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
37
|
-
|
|
38
|
-
const res = await fetch(url, {
|
|
39
|
-
method: "POST",
|
|
40
|
-
signal: ac.signal,
|
|
41
|
-
headers: {
|
|
42
|
-
"content-type": "application/json",
|
|
43
|
-
authorization: `Bearer ${token}`,
|
|
44
|
-
},
|
|
45
|
-
body: JSON.stringify(req),
|
|
46
|
-
}).finally(() => clearTimeout(t));
|
|
47
|
-
|
|
48
|
-
const json = (await res.json()) as ToolsInvokeResponse;
|
|
49
|
-
if (!res.ok || !json.ok) {
|
|
50
|
-
const msg =
|
|
51
|
-
(typeof json.error === "object" && json.error?.message) ||
|
|
52
|
-
(typeof json.error === "string" ? json.error : null) ||
|
|
53
|
-
`tools/invoke failed (${res.status})`;
|
|
54
|
-
throw new Error(msg);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return json.result as T;
|
|
67
|
+
return await doSingleToolsInvoke<T>(url, token, req);
|
|
58
68
|
} catch (e) {
|
|
59
69
|
lastErr = e;
|
|
60
|
-
if (attempt
|
|
61
|
-
await new Promise((r) => setTimeout(r, 150 * attempt));
|
|
70
|
+
if (attempt < 3) await new Promise((r) => setTimeout(r, RETRY_DELAY_BASE_MS * attempt));
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|