clawmoney 0.17.16 → 0.17.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hub/executor.js +126 -4
- package/package.json +1 -1
package/dist/hub/executor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
3
|
import { isProcessed, markProcessed } from "./dedup.js";
|
|
3
4
|
import { replaceLocalPaths, uploadFile } from "./media.js";
|
|
4
5
|
import { logger } from "./logger.js";
|
|
@@ -20,13 +21,108 @@ function buildPrompt(call, config) {
|
|
|
20
21
|
`Input: ${JSON.stringify(call.input, null, 2)}`,
|
|
21
22
|
"",
|
|
22
23
|
];
|
|
23
|
-
// Category-specific instructions
|
|
24
|
+
// Category-specific instructions. Skill names differ per CLI runtime —
|
|
25
|
+
// openclaw exposes nano-banana-pro, codex exposes built-in imagegen,
|
|
26
|
+
// claude/gemini surface image generation via their own tools. We pick
|
|
27
|
+
// the right hint so the model goes straight to the real tool instead
|
|
28
|
+
// of trying to "be helpful" by writing PIL code or hallucinating a path.
|
|
24
29
|
if (call.category?.startsWith("generation/image")) {
|
|
25
|
-
|
|
30
|
+
const cli = config.provider.cli_command;
|
|
31
|
+
const skillHint = cli === "codex"
|
|
32
|
+
? "Use the imagegen skill (the built-in image_gen tool — do NOT use shell/python to draw an image)"
|
|
33
|
+
: cli === "openclaw"
|
|
34
|
+
? "Use the nano-banana-pro skill"
|
|
35
|
+
: "Use your native image generation tool";
|
|
36
|
+
lines.push(`IMPORTANT: ${skillHint} to generate a real PNG/JPG image.`, "Do NOT write SVG, HTML, Python (PIL), or any code to fake an image.", "If no image generation tool is available in this environment, return {\"success\": false, \"error\": \"No image generation tool available\"}.", "Save the generated image and include the absolute file path in your JSON output as \"image_path\".");
|
|
26
37
|
}
|
|
27
38
|
lines.push("Execute this task and return the result as JSON.", "If you generate any files (images, videos, etc.), save them and include their file paths in the output.", "Return ONLY the JSON result, no other text.");
|
|
28
39
|
return lines.join("\n");
|
|
29
40
|
}
|
|
41
|
+
// ── Image-output validation ──
|
|
42
|
+
//
|
|
43
|
+
// Codex / Gemini / generic-claude paths don't have a structured response
|
|
44
|
+
// schema like OpenClaw, so providers using those CLIs can return
|
|
45
|
+
// `{"image_path":"/tmp/whatever.png"}` even when the model never actually
|
|
46
|
+
// generated an image — either by writing Python that fakes a bitmap, or
|
|
47
|
+
// (worse) by hallucinating the path entirely without spawning any tool.
|
|
48
|
+
//
|
|
49
|
+
// `extractClaimedPaths` walks an output object and collects every local
|
|
50
|
+
// file path the provider is claiming to have produced. Used so we can
|
|
51
|
+
// physically verify each file exists before we deliver to the buyer.
|
|
52
|
+
const FILE_PATH_KEYS = ["image_path", "video_path", "audio_path", "file_path", "primary_file"];
|
|
53
|
+
const IMAGE_EXT_RE = /\.(png|jpg|jpeg|webp|gif)$/i;
|
|
54
|
+
const CDN_URL_RE = /^https?:\/\//i;
|
|
55
|
+
function extractClaimedPaths(output) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const visit = (node) => {
|
|
58
|
+
if (!node || typeof node !== "object")
|
|
59
|
+
return;
|
|
60
|
+
const obj = node;
|
|
61
|
+
for (const key of FILE_PATH_KEYS) {
|
|
62
|
+
const v = obj[key];
|
|
63
|
+
if (typeof v === "string" && v.startsWith("/"))
|
|
64
|
+
out.push(v);
|
|
65
|
+
}
|
|
66
|
+
const files = obj.files;
|
|
67
|
+
if (Array.isArray(files)) {
|
|
68
|
+
for (const f of files) {
|
|
69
|
+
if (typeof f === "string" && f.startsWith("/"))
|
|
70
|
+
out.push(f);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Recurse into nested .result so providers wrapping output in
|
|
74
|
+
// { result: { image_path: ... } } don't bypass the check.
|
|
75
|
+
if (obj.result && typeof obj.result === "object")
|
|
76
|
+
visit(obj.result);
|
|
77
|
+
};
|
|
78
|
+
visit(output);
|
|
79
|
+
return Array.from(new Set(out));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* After `replaceLocalPaths` has run, every successful image upload is
|
|
83
|
+
* promoted from `image_path` (local) to `image_url` (CDN). If the upload
|
|
84
|
+
* silently failed (file didn't exist), `image_path` stays in the output
|
|
85
|
+
* and we deliver a broken result — UNLESS we catch it here.
|
|
86
|
+
*
|
|
87
|
+
* Returns null when the delivery is OK for a `generation/image` call,
|
|
88
|
+
* otherwise an error string explaining why it isn't.
|
|
89
|
+
*/
|
|
90
|
+
function validateImageDelivery(output) {
|
|
91
|
+
// CDN URL anywhere in the output? Then a real file got uploaded.
|
|
92
|
+
const top = output;
|
|
93
|
+
if (typeof top.image_url === "string" && CDN_URL_RE.test(top.image_url))
|
|
94
|
+
return null;
|
|
95
|
+
if (Array.isArray(top.files)) {
|
|
96
|
+
const hasCdnImage = top.files.some((f) => typeof f === "string" && CDN_URL_RE.test(f) && IMAGE_EXT_RE.test(f));
|
|
97
|
+
if (hasCdnImage)
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// No real image URL delivered. Why?
|
|
101
|
+
const claimedPaths = extractClaimedPaths(output);
|
|
102
|
+
if (claimedPaths.length === 0) {
|
|
103
|
+
return "Provider returned no image (model likely lacks an image-generation tool).";
|
|
104
|
+
}
|
|
105
|
+
const missing = claimedPaths.filter((p) => !existsSync(p));
|
|
106
|
+
if (missing.length > 0) {
|
|
107
|
+
return `Provider claimed image at ${missing[0]} but the file does not exist (hallucinated path).`;
|
|
108
|
+
}
|
|
109
|
+
// Files exist but none are images, or upload failed for a different reason.
|
|
110
|
+
const hasImagePath = claimedPaths.some((p) => IMAGE_EXT_RE.test(p));
|
|
111
|
+
if (!hasImagePath) {
|
|
112
|
+
return "Provider produced non-image files only for a generation/image task.";
|
|
113
|
+
}
|
|
114
|
+
// File exists and is .png/.jpg/.webp/.gif but upload still failed.
|
|
115
|
+
// Most likely cause: file is empty or permission denied. Surface bytes.
|
|
116
|
+
try {
|
|
117
|
+
const stats = claimedPaths
|
|
118
|
+
.filter((p) => IMAGE_EXT_RE.test(p))
|
|
119
|
+
.map((p) => `${p} (${statSync(p).size}B)`);
|
|
120
|
+
return `Image file exists but R2 upload failed: ${stats.join(", ")}`;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return "Image upload to R2 failed.";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
30
126
|
// ── CLI execution (openclaw agent / claude -p) ──
|
|
31
127
|
function runCli(command, prompt, timeoutMs, orderId) {
|
|
32
128
|
return new Promise((resolve) => {
|
|
@@ -37,8 +133,15 @@ function runCli(command, prompt, timeoutMs, orderId) {
|
|
|
37
133
|
args = ["agent", "--message", prompt, "--session-id", orderId || "hub-task", "--json"];
|
|
38
134
|
}
|
|
39
135
|
else if (command === "codex") {
|
|
40
|
-
//
|
|
41
|
-
|
|
136
|
+
// -s workspace-write is required so the built-in image_gen tool can
|
|
137
|
+
// write files under $CODEX_HOME/generated_images and so the model
|
|
138
|
+
// can mv/cp the result to the user-named path. With the default
|
|
139
|
+
// read-only sandbox, image_gen silently degrades — the model falls
|
|
140
|
+
// back to either drawing the image with Python in /tmp (slow, ugly)
|
|
141
|
+
// or hallucinating an image_path with no file behind it (worst
|
|
142
|
+
// case, since the buyer pays for a nonexistent file). Verified on
|
|
143
|
+
// codex 0.128.0 + gpt-5.5 xhigh, 2026-05-12.
|
|
144
|
+
args = ["exec", "-s", "workspace-write", prompt, "--json", "--skip-git-repo-check"];
|
|
42
145
|
}
|
|
43
146
|
else if (command === "gemini") {
|
|
44
147
|
// gemini -p "..." -o json --yolo
|
|
@@ -501,6 +604,25 @@ export class Executor {
|
|
|
501
604
|
// Upload local files via generic path replacement
|
|
502
605
|
output = await replaceLocalPaths(output, this.config);
|
|
503
606
|
}
|
|
607
|
+
// Image-output validation. The OpenClaw branch above does this inline
|
|
608
|
+
// (using its richer parseOpenClawResponse files list); for everything
|
|
609
|
+
// else we re-run the check on the post-upload output so the buyer
|
|
610
|
+
// never gets `{image_path: "/tmp/elon.png"}` when the file doesn't
|
|
611
|
+
// exist and uploadFile silently returned null. Skip the check for the
|
|
612
|
+
// OpenClaw branch — it already validated and may have set _meta etc.
|
|
613
|
+
if (command !== "openclaw" &&
|
|
614
|
+
call.category?.startsWith("generation/image")) {
|
|
615
|
+
const reason = validateImageDelivery(output);
|
|
616
|
+
if (reason) {
|
|
617
|
+
logger.error(`Image validation failed for order=${call.order_id} (${command}): ${reason}`);
|
|
618
|
+
this.send({
|
|
619
|
+
event: "deliver",
|
|
620
|
+
order_id: call.order_id,
|
|
621
|
+
error: reason,
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
504
626
|
const sent = this.send({
|
|
505
627
|
event: "deliver",
|
|
506
628
|
order_id: call.order_id,
|