@jiggai/recipes 0.4.57 → 0.4.59
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
CHANGED
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
asRecord, asString,
|
|
14
14
|
ensureDir, fileExists,
|
|
15
15
|
moveRunTicket, appendRunLog, nodeLabel,
|
|
16
|
-
loadNodeStatesFromRun, sanitizeDraftOnlyText, templateReplace,
|
|
16
|
+
loadNodeStatesFromRun, sanitizeDraftOnlyText, templateReplace, expandFileIncludes,
|
|
17
17
|
} from './workflow-utils';
|
|
18
18
|
|
|
19
19
|
export async function resolveApprovalBindingTarget(api: OpenClawPluginApi, bindingId: string): Promise<{ channel: string; target: string; accountId?: string }> {
|
|
@@ -180,7 +180,10 @@ export async function executeWorkflowNodes(opts: {
|
|
|
180
180
|
}
|
|
181
181
|
await ensureDir(path.dirname(nodeOutputAbs));
|
|
182
182
|
|
|
183
|
-
const
|
|
183
|
+
const promptRaw = promptTemplateInline ? promptTemplateInline : await readTextFile(promptPathAbs);
|
|
184
|
+
// Inline `{{file:<relative-path>}}` contents so LLM nodes can see workspace files
|
|
185
|
+
// they cannot fetch themselves (llm-task is one-shot, no tool loop).
|
|
186
|
+
const prompt = await expandFileIncludes(promptRaw, teamDir);
|
|
184
187
|
const task = [
|
|
185
188
|
`You are executing a workflow run for teamId=${teamId}.`,
|
|
186
189
|
`Workflow: ${workflow.name ?? workflow.id ?? workflowFile}`,
|
|
@@ -158,6 +158,81 @@ export function templateReplace(input: string, vars: Record<string, string>) {
|
|
|
158
158
|
return out;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
export const FILE_INCLUDE_MAX_BYTES_DEFAULT = 256 * 1024;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Expand `{{file:<relative-path>}}` markers by inlining the contents of files
|
|
165
|
+
* under the team workspace. Intended for LLM prompts where the model cannot
|
|
166
|
+
* call a read_file tool itself (workflow llm-task is one-shot, no tool loop).
|
|
167
|
+
*
|
|
168
|
+
* Safety:
|
|
169
|
+
* - paths are treated as RELATIVE TO teamDir
|
|
170
|
+
* - rejects absolute paths, paths with `..`, and paths that escape teamDir (incl. via symlink)
|
|
171
|
+
* - enforces a per-include byte cap (default 256KB)
|
|
172
|
+
*
|
|
173
|
+
* On any rejection the marker is replaced with a short `[[file-include …]]`
|
|
174
|
+
* note so the LLM sees the failure context instead of the pipeline blowing up.
|
|
175
|
+
*/
|
|
176
|
+
export async function expandFileIncludes(
|
|
177
|
+
input: string,
|
|
178
|
+
teamDir: string,
|
|
179
|
+
opts: { maxBytes?: number } = {},
|
|
180
|
+
): Promise<string> {
|
|
181
|
+
const text = String(input ?? '');
|
|
182
|
+
const pattern = /\{\{\s*file:([^}]+?)\s*\}\}/g;
|
|
183
|
+
const matches = Array.from(text.matchAll(pattern));
|
|
184
|
+
if (!matches.length) return text;
|
|
185
|
+
|
|
186
|
+
const maxBytes = opts.maxBytes ?? FILE_INCLUDE_MAX_BYTES_DEFAULT;
|
|
187
|
+
const teamDirResolvedRaw = path.resolve(teamDir);
|
|
188
|
+
// Resolve symlinks on teamDir too so comparisons below are apples-to-apples.
|
|
189
|
+
// On macOS, os.tmpdir() returns /var/... but realpath yields /private/var/... .
|
|
190
|
+
let teamDirResolved = teamDirResolvedRaw;
|
|
191
|
+
try { teamDirResolved = await fs.realpath(teamDirResolvedRaw); } catch { /* fall back to resolved */ }
|
|
192
|
+
const teamDirPrefix = teamDirResolved + path.sep;
|
|
193
|
+
|
|
194
|
+
const resolved = new Map<string, string>();
|
|
195
|
+
for (const m of matches) {
|
|
196
|
+
const rawPath = m[1].trim();
|
|
197
|
+
if (resolved.has(rawPath)) continue;
|
|
198
|
+
|
|
199
|
+
if (!rawPath || path.isAbsolute(rawPath) || rawPath.split('/').includes('..')) {
|
|
200
|
+
resolved.set(rawPath, `[[file-include rejected: unsafe path "${rawPath}"]]`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const candidate = path.resolve(teamDirResolved, rawPath);
|
|
205
|
+
if (candidate !== teamDirResolved && !candidate.startsWith(teamDirPrefix)) {
|
|
206
|
+
resolved.set(rawPath, `[[file-include rejected: outside team workspace "${rawPath}"]]`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const real = await fs.realpath(candidate);
|
|
212
|
+
if (real !== teamDirResolved && !real.startsWith(teamDirPrefix)) {
|
|
213
|
+
resolved.set(rawPath, `[[file-include rejected: symlink escapes team workspace "${rawPath}"]]`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const stat = await fs.stat(real);
|
|
217
|
+
if (!stat.isFile()) {
|
|
218
|
+
resolved.set(rawPath, `[[file-include rejected: not a regular file "${rawPath}"]]`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (stat.size > maxBytes) {
|
|
222
|
+
resolved.set(rawPath, `[[file-include rejected: "${rawPath}" size ${stat.size}B exceeds ${maxBytes}B cap]]`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const content = await fs.readFile(real, 'utf8');
|
|
226
|
+
resolved.set(rawPath, content);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
resolved.set(rawPath, `[[file-include failed: "${rawPath}" — ${msg}]]`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return text.replace(pattern, (_full, raw) => resolved.get(String(raw).trim()) ?? '');
|
|
234
|
+
}
|
|
235
|
+
|
|
161
236
|
export function sanitizeDraftOnlyText(text: string): string {
|
|
162
237
|
// Back-compat: older workflow nodes mention 'draft only'.
|
|
163
238
|
// New canonical sanitizer also strips other internal-only disclaimer lines.
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
moveRunTicket, appendRunLog, writeRunFile, loadRunFile,
|
|
23
23
|
runFilePathFor, nodeLabel,
|
|
24
24
|
loadNodeStatesFromRun, pickNextRunnableNodeIndex,
|
|
25
|
-
sanitizeDraftOnlyText, templateReplace,
|
|
25
|
+
sanitizeDraftOnlyText, templateReplace, expandFileIncludes,
|
|
26
26
|
} from './workflow-utils';
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -780,7 +780,10 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
780
780
|
const promptRaw = promptTemplateInline ? promptTemplateInline : await readTextFile(promptPathAbs);
|
|
781
781
|
|
|
782
782
|
const vars = await buildTemplateVars(teamDir, runsDir, runId, workflowFile, workflow);
|
|
783
|
-
const
|
|
783
|
+
const promptVarsResolved = templateReplace(promptRaw, vars);
|
|
784
|
+
// Inline `{{file:<relative-path>}}` contents so LLM nodes can see workspace files
|
|
785
|
+
// they cannot fetch themselves (llm-task is one-shot, no tool loop).
|
|
786
|
+
const prompt = await expandFileIncludes(promptVarsResolved, teamDir);
|
|
784
787
|
|
|
785
788
|
// Build output format instructions from outputFields when defined
|
|
786
789
|
const nodeConfig = asRecord((node as unknown as Record<string, unknown>)['config']);
|
|
@@ -1240,7 +1243,8 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
1240
1243
|
const vars = await buildTemplateVars(teamDir, runsDir, task.runId, workflowFile, workflow);
|
|
1241
1244
|
// Add node-level vars that templateReplace doesn't normally include
|
|
1242
1245
|
vars['node.id'] = node.id;
|
|
1243
|
-
const
|
|
1246
|
+
const promptVarsResolved = templateReplace(promptTemplateRaw, vars);
|
|
1247
|
+
const prompt = await expandFileIncludes(promptVarsResolved, teamDir);
|
|
1244
1248
|
const outputRelPath = templateReplace(outputPathRaw, vars);
|
|
1245
1249
|
|
|
1246
1250
|
const defaultNodeOutputRel = path.join('node-outputs', `${String(nodeIdx).padStart(3, '0')}-${node.id}.json`);
|