@jiggai/recipes 0.4.39 → 0.4.40
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/lib/workflows/media-drivers/generic.driver.ts +2 -1
- package/src/lib/workflows/media-drivers/kling-video.driver.ts +2 -1
- package/src/lib/workflows/media-drivers/luma-video.driver.ts +3 -2
- package/src/lib/workflows/media-drivers/nano-banana-pro.driver.ts +2 -1
- package/src/lib/workflows/media-drivers/openai-image-gen.driver.ts +2 -1
- package/src/lib/workflows/media-drivers/runway-video.driver.ts +3 -2
- package/src/lib/workflows/media-drivers/types.ts +3 -0
- package/src/lib/workflows/media-drivers/utils.ts +75 -23
- package/src/lib/workflows/workflow-error-classify.ts +69 -0
- package/src/lib/workflows/workflow-worker.ts +20 -13
- package/src/toolsInvoke.ts +14 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -43,7 +43,8 @@ export class GenericDriver implements MediaDriver {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Execute the script with stdin input (most common interface)
|
|
46
|
-
const scriptOutput = runScript({
|
|
46
|
+
const scriptOutput = await runScript({
|
|
47
|
+
api: opts.api,
|
|
47
48
|
runner,
|
|
48
49
|
script: scriptPath,
|
|
49
50
|
stdin: prompt,
|
|
@@ -78,7 +78,8 @@ export class KlingVideo implements MediaDriver {
|
|
|
78
78
|
// The official skill is a Node.js script (not Python)
|
|
79
79
|
const runner = 'node';
|
|
80
80
|
|
|
81
|
-
const scriptOutput = runScript({
|
|
81
|
+
const scriptOutput = await runScript({
|
|
82
|
+
api: opts.api,
|
|
82
83
|
runner,
|
|
83
84
|
script: scriptPath,
|
|
84
85
|
args: [
|
|
@@ -26,7 +26,8 @@ export class LumaVideo implements MediaDriver {
|
|
|
26
26
|
const runner = await findVenvPython(skillDir);
|
|
27
27
|
|
|
28
28
|
// Execute the script with stdin input
|
|
29
|
-
const scriptOutput = runScript({
|
|
29
|
+
const scriptOutput = await runScript({
|
|
30
|
+
api: opts.api,
|
|
30
31
|
runner,
|
|
31
32
|
script: scriptPath,
|
|
32
33
|
stdin: prompt,
|
|
@@ -42,7 +43,7 @@ export class LumaVideo implements MediaDriver {
|
|
|
42
43
|
|
|
43
44
|
// Parse the MEDIA: output
|
|
44
45
|
const filePath = parseMediaOutput(scriptOutput);
|
|
45
|
-
|
|
46
|
+
|
|
46
47
|
if (!filePath) {
|
|
47
48
|
throw new Error(`No MEDIA: path found in script output. Output: ${scriptOutput}`);
|
|
48
49
|
}
|
|
@@ -39,7 +39,8 @@ export class NanoBananaPro implements MediaDriver {
|
|
|
39
39
|
const resolution = maxDim >= 3840 ? '4K' : maxDim >= 1792 ? '2K' : '1K';
|
|
40
40
|
|
|
41
41
|
// Execute the script with argparse CLI interface
|
|
42
|
-
const scriptOutput = runScript({
|
|
42
|
+
const scriptOutput = await runScript({
|
|
43
|
+
api: opts.api,
|
|
43
44
|
runner,
|
|
44
45
|
script: scriptPath,
|
|
45
46
|
args: ['--prompt', prompt, '--filename', filename, '--resolution', resolution],
|
|
@@ -28,7 +28,8 @@ export class OpenAIImageGen implements MediaDriver {
|
|
|
28
28
|
const size = String(config?.size ?? '1024x1024');
|
|
29
29
|
|
|
30
30
|
// Execute the script with stdin input
|
|
31
|
-
const scriptOutput = runScript({
|
|
31
|
+
const scriptOutput = await runScript({
|
|
32
|
+
api: opts.api,
|
|
32
33
|
runner,
|
|
33
34
|
script: scriptPath,
|
|
34
35
|
stdin: prompt,
|
|
@@ -26,7 +26,8 @@ export class RunwayVideo implements MediaDriver {
|
|
|
26
26
|
const runner = await findVenvPython(skillDir);
|
|
27
27
|
|
|
28
28
|
// Execute the script with stdin input
|
|
29
|
-
const scriptOutput = runScript({
|
|
29
|
+
const scriptOutput = await runScript({
|
|
30
|
+
api: opts.api,
|
|
30
31
|
runner,
|
|
31
32
|
script: scriptPath,
|
|
32
33
|
stdin: prompt,
|
|
@@ -42,7 +43,7 @@ export class RunwayVideo implements MediaDriver {
|
|
|
42
43
|
|
|
43
44
|
// Parse the MEDIA: output
|
|
44
45
|
const filePath = parseMediaOutput(scriptOutput);
|
|
45
|
-
|
|
46
|
+
|
|
46
47
|
if (!filePath) {
|
|
47
48
|
throw new Error(`No MEDIA: path found in script output. Output: ${scriptOutput}`);
|
|
48
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Find a skill directory by searching common skill roots
|
|
@@ -33,7 +33,7 @@ export async function findSkillDir(slug: string): Promise<string | null> {
|
|
|
33
33
|
*/
|
|
34
34
|
export async function findVenvPython(skillDir: string): Promise<string> {
|
|
35
35
|
const venvPython = path.join(skillDir, '.venv', 'bin', 'python');
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
try {
|
|
38
38
|
await fs.access(venvPython);
|
|
39
39
|
return venvPython;
|
|
@@ -48,7 +48,7 @@ export async function findVenvPython(skillDir: string): Promise<string> {
|
|
|
48
48
|
export async function loadConfigEnv(): Promise<Record<string, string>> {
|
|
49
49
|
const homedir = process.env.HOME || '/home/control';
|
|
50
50
|
const configPath = path.join(homedir, '.openclaw', 'openclaw.json');
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
try {
|
|
53
53
|
const cfgRaw = await fs.readFile(configPath, 'utf8');
|
|
54
54
|
const cfgParsed = JSON.parse(cfgRaw);
|
|
@@ -82,9 +82,12 @@ export function parseMediaOutput(stdout: string): string {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Execute a script
|
|
85
|
+
* Execute a script via the OpenClaw exec tool so this plugin package does not
|
|
86
|
+
* directly import child_process. We still pass argv as discrete args and feed
|
|
87
|
+
* prompt text via stdin through a small Python wrapper script.
|
|
86
88
|
*/
|
|
87
89
|
export interface RunScriptOpts {
|
|
90
|
+
api: OpenClawPluginApi;
|
|
88
91
|
runner: string;
|
|
89
92
|
script: string;
|
|
90
93
|
args?: string[];
|
|
@@ -94,26 +97,75 @@ export interface RunScriptOpts {
|
|
|
94
97
|
timeout: number;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
function buildPythonExecSnippet(opts: RunScriptOpts): string {
|
|
98
101
|
const { runner, script, args = [], stdin, env, cwd, timeout } = opts;
|
|
102
|
+
const mergedEnv = {
|
|
103
|
+
...env,
|
|
104
|
+
MEDIA_OUTPUT_DIR: cwd,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const payload = {
|
|
108
|
+
runner,
|
|
109
|
+
script,
|
|
110
|
+
args,
|
|
111
|
+
stdin: stdin ?? '',
|
|
112
|
+
env: mergedEnv,
|
|
113
|
+
cwd,
|
|
114
|
+
timeoutMs: timeout,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Base64-encode the payload to avoid shell injection and heredoc delimiter collisions.
|
|
118
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
`python3 -c '`,
|
|
122
|
+
`import base64, json, os, subprocess, sys;`,
|
|
123
|
+
`payload = json.loads(base64.b64decode("${payloadB64}").decode());`,
|
|
124
|
+
`env = os.environ.copy();`,
|
|
125
|
+
`env.update({k: str(v) for k, v in payload["env"].items()});`,
|
|
126
|
+
`res = subprocess.run(`,
|
|
127
|
+
` [payload["runner"], payload["script"], *payload.get("args", [])],`,
|
|
128
|
+
` input=payload.get("stdin", ""),`,
|
|
129
|
+
` text=True,`,
|
|
130
|
+
` capture_output=True,`,
|
|
131
|
+
` cwd=payload["cwd"],`,
|
|
132
|
+
` env=env,`,
|
|
133
|
+
` timeout=max(1, int(payload.get("timeoutMs", 1000) / 1000))`,
|
|
134
|
+
`);`,
|
|
135
|
+
`sys.stdout.write(res.stdout);`,
|
|
136
|
+
`sys.stderr.write(res.stderr);`,
|
|
137
|
+
`raise SystemExit(res.returncode)`,
|
|
138
|
+
`'`,
|
|
139
|
+
].join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function runScript(opts: RunScriptOpts): Promise<string> {
|
|
143
|
+
const { api, timeout } = opts;
|
|
144
|
+
const timeoutMs = Math.max(1000, timeout + 5000);
|
|
145
|
+
const command = buildPythonExecSnippet(opts);
|
|
99
146
|
|
|
100
147
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
// Use the plugin SDK's runtime exec — available to all plugins without
|
|
149
|
+
// gateway tool permissions (unlike toolsInvoke('exec') which is session-gated).
|
|
150
|
+
const result = await api.runtime.system.runCommandWithTimeout(
|
|
151
|
+
['bash', '-c', command],
|
|
152
|
+
{ timeoutMs, cwd: opts.cwd },
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (result.code !== 0) {
|
|
156
|
+
const msg = [
|
|
157
|
+
`Script execution failed with exit code ${result.code}`,
|
|
158
|
+
result.stdout ? `\n--- stdout ---\n${result.stdout.trim()}` : '',
|
|
159
|
+
result.stderr ? `\n--- stderr ---\n${result.stderr.trim()}` : '',
|
|
160
|
+
].filter(Boolean).join('');
|
|
161
|
+
throw new Error(msg);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (result.stdout || '').trim();
|
|
112
165
|
} catch (err) {
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const stderr = typeof e?.stderr === 'string' ? e.stderr : (Buffer.isBuffer(e?.stderr) ? e.stderr.toString('utf8') : '');
|
|
166
|
+
const e = err as Error & { stdout?: string; stderr?: string };
|
|
167
|
+
const stdout = e?.stdout ?? '';
|
|
168
|
+
const stderr = e?.stderr ?? '';
|
|
117
169
|
const msg = [
|
|
118
170
|
e?.message ? String(e.message) : 'Script execution failed',
|
|
119
171
|
stdout ? `\n--- stdout ---\n${stdout.trim()}` : '',
|
|
@@ -128,7 +180,7 @@ export function runScript(opts: RunScriptOpts): string {
|
|
|
128
180
|
*/
|
|
129
181
|
export async function findScriptInSkill(skillDir: string, scriptCandidates: string[]): Promise<string | null> {
|
|
130
182
|
const searchDirs = [skillDir, path.join(skillDir, 'scripts')];
|
|
131
|
-
|
|
183
|
+
|
|
132
184
|
for (const dir of searchDirs) {
|
|
133
185
|
for (const candidate of scriptCandidates) {
|
|
134
186
|
const scriptPath = path.join(dir, candidate);
|
|
@@ -140,6 +192,6 @@ export async function findScriptInSkill(skillDir: string, scriptCandidates: stri
|
|
|
140
192
|
}
|
|
141
193
|
}
|
|
142
194
|
}
|
|
143
|
-
|
|
195
|
+
|
|
144
196
|
return null;
|
|
145
|
-
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ToolsInvokeError } from '../../toolsInvoke.js';
|
|
2
|
+
|
|
3
|
+
export type ErrorCategory = 'funding' | 'rate-limit' | 'auth' | 'timeout' | 'unknown';
|
|
4
|
+
|
|
5
|
+
const FUNDING_PATTERNS = [
|
|
6
|
+
/insufficient.*(credits?|funds?|balance)/i,
|
|
7
|
+
/billing/i,
|
|
8
|
+
/payment\s+required/i,
|
|
9
|
+
/quota\s+exceeded/i,
|
|
10
|
+
/out\s+of\s+credits/i,
|
|
11
|
+
/budget\s+(exceeded|limit)/i,
|
|
12
|
+
/no\s+(active\s+)?subscription/i,
|
|
13
|
+
/plan\s+(limit|exceeded)/i,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const RATE_LIMIT_PATTERNS = [
|
|
17
|
+
/rate\s+limit/i,
|
|
18
|
+
/too\s+many\s+requests/i,
|
|
19
|
+
/throttl/i,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const AUTH_PATTERNS = [
|
|
23
|
+
/unauthorized/i,
|
|
24
|
+
/invalid.*api.?key/i,
|
|
25
|
+
/forbidden/i,
|
|
26
|
+
/authentication\s+failed/i,
|
|
27
|
+
/access\s+denied/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function classifyByHttpStatus(status: number): ErrorCategory | null {
|
|
31
|
+
if (status === 402) return 'funding';
|
|
32
|
+
if (status === 429) return 'rate-limit';
|
|
33
|
+
if (status === 401 || status === 403) return 'auth';
|
|
34
|
+
if (status === 408 || status === 504) return 'timeout';
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function classifyByMessage(message: string, error: unknown): ErrorCategory | null {
|
|
39
|
+
if (FUNDING_PATTERNS.some((p) => p.test(message))) return 'funding';
|
|
40
|
+
if (RATE_LIMIT_PATTERNS.some((p) => p.test(message))) return 'rate-limit';
|
|
41
|
+
if (AUTH_PATTERNS.some((p) => p.test(message))) return 'auth';
|
|
42
|
+
if (error instanceof Error && error.name === 'AbortError') return 'timeout';
|
|
43
|
+
if (/timed?\s*out/i.test(message)) return 'timeout';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Classify an error into a category based on HTTP status and message content.
|
|
49
|
+
* Returns 'unknown' if the error doesn't match any known pattern.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyError(error: unknown): ErrorCategory {
|
|
52
|
+
const httpStatus = error instanceof ToolsInvokeError ? error.httpStatus : 0;
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
54
|
+
|
|
55
|
+
return classifyByHttpStatus(httpStatus) ?? classifyByMessage(message, error) ?? 'unknown';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const CATEGORY_LABELS: Record<ErrorCategory, string> = {
|
|
59
|
+
'funding': 'Funding issue — the model provider may be out of credits or require payment',
|
|
60
|
+
'rate-limit': 'Rate limit — the model provider is throttling requests',
|
|
61
|
+
'auth': 'Authentication failure — the API key may be invalid or expired',
|
|
62
|
+
'timeout': 'Timeout — the request took too long to complete',
|
|
63
|
+
'unknown': 'Unknown error',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Human-readable label for an error category. */
|
|
67
|
+
export function errorCategoryLabel(category: ErrorCategory): string {
|
|
68
|
+
return CATEGORY_LABELS[category] ?? CATEGORY_LABELS['unknown'];
|
|
69
|
+
}
|
|
@@ -4,6 +4,7 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
5
5
|
import type { ToolTextResult } from '../../toolsInvoke';
|
|
6
6
|
import { toolsInvoke } from '../../toolsInvoke';
|
|
7
|
+
import { classifyError, errorCategoryLabel } from './workflow-error-classify';
|
|
7
8
|
import { resolveTeamDir } from '../workspace';
|
|
8
9
|
import { getDriver } from './media-drivers/registry';
|
|
9
10
|
import { GenericDriver } from './media-drivers/generic.driver';
|
|
@@ -911,6 +912,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
911
912
|
text = JSON.stringify(payload, null, 2);
|
|
912
913
|
} catch (e) {
|
|
913
914
|
const eRec = asRecord(e);
|
|
915
|
+
const errorCategory = classifyError(e);
|
|
914
916
|
const errorDetails = {
|
|
915
917
|
message: e instanceof Error ? e.message : String(e),
|
|
916
918
|
name: e instanceof Error ? e.name : undefined,
|
|
@@ -919,6 +921,8 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
919
921
|
details: eRec['details'],
|
|
920
922
|
data: eRec['data'],
|
|
921
923
|
cause: e instanceof Error && 'cause' in e ? (e as Error & { cause?: unknown }).cause : undefined,
|
|
924
|
+
errorCategory,
|
|
925
|
+
errorCategoryLabel: errorCategory !== 'unknown' ? errorCategoryLabel(errorCategory) : undefined,
|
|
922
926
|
};
|
|
923
927
|
const errMsg = `LLM execution failed for node ${nodeLabel(node)}: ${errorDetails.message}`;
|
|
924
928
|
const errorTs = new Date().toISOString();
|
|
@@ -928,18 +932,18 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
928
932
|
updatedAt: errorTs,
|
|
929
933
|
nodeStates: {
|
|
930
934
|
...(cur.nodeStates ?? {}),
|
|
931
|
-
[node.id]: { status: 'error', ts: errorTs, error: errMsg, details: errorDetails },
|
|
935
|
+
[node.id]: { status: 'error', ts: errorTs, error: errMsg, details: errorDetails, errorCategory },
|
|
932
936
|
},
|
|
933
937
|
events: [
|
|
934
938
|
...cur.events,
|
|
935
|
-
{ ts: errorTs, type: 'node.error', nodeId: node.id, kind: node.kind, message: errMsg, details: errorDetails },
|
|
939
|
+
{ ts: errorTs, type: 'node.error', nodeId: node.id, kind: node.kind, message: errMsg, details: errorDetails, errorCategory },
|
|
936
940
|
],
|
|
937
941
|
nodeResults: [
|
|
938
942
|
...(cur.nodeResults ?? []),
|
|
939
|
-
{ nodeId: node.id, kind: node.kind, agentId: agentIdExec, error: errMsg, details: errorDetails },
|
|
943
|
+
{ nodeId: node.id, kind: node.kind, agentId: agentIdExec, error: errMsg, details: errorDetails, errorCategory },
|
|
940
944
|
],
|
|
941
945
|
}));
|
|
942
|
-
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'error' });
|
|
946
|
+
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'error', errorCategory });
|
|
943
947
|
continue;
|
|
944
948
|
}
|
|
945
949
|
|
|
@@ -1356,16 +1360,17 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
1356
1360
|
nodeResults: [...(cur.nodeResults ?? []), { nodeId: node.id, kind: node.kind, tool: toolName, artifactPath: path.relative(teamDir, artifactPath), nodeOutputPath: path.relative(teamDir, nodeOutputAbs) }],
|
|
1357
1361
|
}));
|
|
1358
1362
|
} catch (e) {
|
|
1359
|
-
|
|
1363
|
+
const errorCategory = classifyError(e);
|
|
1364
|
+
await fs.writeFile(artifactPath, JSON.stringify({ ok: false, tool: toolName, error: (e as Error).message, errorCategory }, null, 2) + '\n', 'utf8');
|
|
1360
1365
|
const errorTs = new Date().toISOString();
|
|
1361
1366
|
await appendRunLog(runPath, (cur) => ({
|
|
1362
1367
|
...cur,
|
|
1363
1368
|
status: 'error',
|
|
1364
|
-
nodeStates: { ...(cur.nodeStates ?? {}), [node.id]: { status: 'error', ts: errorTs } },
|
|
1365
|
-
events: [...cur.events, { ts: errorTs, type: 'node.error', nodeId: node.id, kind: node.kind, tool: toolName, message: (e as Error).message, artifactPath: path.relative(teamDir, artifactPath) }],
|
|
1366
|
-
nodeResults: [...(cur.nodeResults ?? []), { nodeId: node.id, kind: node.kind, tool: toolName, error: (e as Error).message, artifactPath: path.relative(teamDir, artifactPath) }],
|
|
1369
|
+
nodeStates: { ...(cur.nodeStates ?? {}), [node.id]: { status: 'error', ts: errorTs, errorCategory } },
|
|
1370
|
+
events: [...cur.events, { ts: errorTs, type: 'node.error', nodeId: node.id, kind: node.kind, tool: toolName, message: (e as Error).message, artifactPath: path.relative(teamDir, artifactPath), errorCategory }],
|
|
1371
|
+
nodeResults: [...(cur.nodeResults ?? []), { nodeId: node.id, kind: node.kind, tool: toolName, error: (e as Error).message, artifactPath: path.relative(teamDir, artifactPath), errorCategory }],
|
|
1367
1372
|
}));
|
|
1368
|
-
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'error', error: (e as Error).message });
|
|
1373
|
+
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'error', error: (e as Error).message, errorCategory });
|
|
1369
1374
|
continue;
|
|
1370
1375
|
}
|
|
1371
1376
|
} else if (kind === 'media-image' || kind === 'media-video' || kind === 'media-audio') {
|
|
@@ -1477,6 +1482,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
1477
1482
|
let payload: Record<string, unknown>;
|
|
1478
1483
|
if (driver) {
|
|
1479
1484
|
const result = await driver.invoke({
|
|
1485
|
+
api,
|
|
1480
1486
|
prompt: refinedPrompt,
|
|
1481
1487
|
outputDir: mediaDir,
|
|
1482
1488
|
env: mergedEnv,
|
|
@@ -1503,6 +1509,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
1503
1509
|
}
|
|
1504
1510
|
text = JSON.stringify(payload, null, 2);
|
|
1505
1511
|
} catch (e) {
|
|
1512
|
+
const errorCategory = classifyError(e);
|
|
1506
1513
|
const errDetails = e instanceof Error
|
|
1507
1514
|
? { message: e.message, name: e.name, stack: e.stack?.split('\n').slice(0, 5).join(' | ') }
|
|
1508
1515
|
: { message: String(e) };
|
|
@@ -1512,11 +1519,11 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
1512
1519
|
...cur,
|
|
1513
1520
|
status: 'error',
|
|
1514
1521
|
updatedAt: errorTs,
|
|
1515
|
-
nodeStates: { ...(cur.nodeStates ?? {}), [node.id]: { status: 'error', ts: errorTs, error: errMsg } },
|
|
1516
|
-
events: [...cur.events, { ts: errorTs, type: 'node.error', nodeId: node.id, kind: node.kind, message: errMsg }],
|
|
1517
|
-
nodeResults: [...(cur.nodeResults ?? []), { nodeId: node.id, kind: node.kind, agentId: agentIdMedia || agentId, error: errMsg }],
|
|
1522
|
+
nodeStates: { ...(cur.nodeStates ?? {}), [node.id]: { status: 'error', ts: errorTs, error: errMsg, errorCategory } },
|
|
1523
|
+
events: [...cur.events, { ts: errorTs, type: 'node.error', nodeId: node.id, kind: node.kind, message: errMsg, errorCategory }],
|
|
1524
|
+
nodeResults: [...(cur.nodeResults ?? []), { nodeId: node.id, kind: node.kind, agentId: agentIdMedia || agentId, error: errMsg, errorCategory }],
|
|
1518
1525
|
}));
|
|
1519
|
-
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'error' });
|
|
1526
|
+
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'error', errorCategory });
|
|
1520
1527
|
continue;
|
|
1521
1528
|
}
|
|
1522
1529
|
|
package/src/toolsInvoke.ts
CHANGED
|
@@ -6,6 +6,19 @@ export const TOOLS_INVOKE_TIMEOUT_MS = 120_000;
|
|
|
6
6
|
export const RETRY_DELAY_BASE_MS = 150;
|
|
7
7
|
export const GATEWAY_DEFAULT_PORT = 18789;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Custom error class that preserves HTTP status from gateway responses.
|
|
11
|
+
* Used downstream to classify errors (e.g. 402 → funding, 429 → rate-limit).
|
|
12
|
+
*/
|
|
13
|
+
export class ToolsInvokeError extends Error {
|
|
14
|
+
httpStatus: number;
|
|
15
|
+
constructor(message: string, httpStatus: number) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ToolsInvokeError';
|
|
18
|
+
this.httpStatus = httpStatus;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export type ToolTextResult = { content?: Array<{ type: string; text?: string }> };
|
|
10
23
|
|
|
11
24
|
export type ToolsInvokeRequest = {
|
|
@@ -43,7 +56,7 @@ async function doSingleToolsInvoke<T>(url: string, token: string, req: ToolsInvo
|
|
|
43
56
|
}).finally(() => clearTimeout(t));
|
|
44
57
|
|
|
45
58
|
const json = (await res.json()) as ToolsInvokeResponse;
|
|
46
|
-
if (!res.ok || !json.ok) throw new
|
|
59
|
+
if (!res.ok || !json.ok) throw new ToolsInvokeError(parseToolsInvokeError(json, res.status), res.status);
|
|
47
60
|
return json.result as T;
|
|
48
61
|
}
|
|
49
62
|
|