u
This commit is contained in:
@@ -684,7 +684,7 @@ _none_
|
|||||||
- Returns: `200 str`
|
- Returns: `200 str`
|
||||||
#### Execute a virtual filesystem applet — `POST /vfs/{*path}`
|
#### Execute a virtual filesystem applet — `POST /vfs/{*path}`
|
||||||
- Params: `path: str, body?: { input?: {}, timeout?: int, max_output?: int }`
|
- Params: `path: str, body?: { input?: {}, timeout?: int, max_output?: int }`
|
||||||
- Returns: `200 { ok: bool, result: any|null, stdout: str, consoleLines: int, durationMs: int, truncated?: bool, truncationNote?: str }`
|
- Returns: `200 { ok: bool, result: str, columns: { name: str, type: str }[], rows: int, stdout: str, consoleLines: int, durationMs: int, warnings?: str[], truncated?: bool, truncationNote?: str }`
|
||||||
#### Create or overwrite a virtual filesystem file — `PUT /vfs/{*path}`
|
#### Create or overwrite a virtual filesystem file — `PUT /vfs/{*path}`
|
||||||
- Params: `path: str, body: { content: str, mime?: str }`
|
- Params: `path: str, body: { content: str, mime?: str }`
|
||||||
- Returns: `200 { success: bool, file: { name: str, size: int, mime: str, updated_at: str } }`
|
- Returns: `200 { success: bool, file: { name: str, size: int, mime: str, updated_at: str } }`
|
||||||
|
|||||||
@@ -9,10 +9,17 @@ and returns the exported function's result.
|
|||||||
|
|
||||||
The applet file must begin with two metadata lines:
|
The applet file must begin with two metadata lines:
|
||||||
`// @applet-input-schema {JSON Schema}` followed by
|
`// @applet-input-schema {JSON Schema}` followed by
|
||||||
`// @applet-output-schema {JSON Schema}`. The file exports a function that receives the posted `input`
|
`// @applet-output-schema {name:type;name:type}`. The input schema describes the GUI form posted as
|
||||||
as `applet_input`, and execution runs in the same isolated worker sandbox as the `javascript_execute`
|
`input`. The output line declares the RESULT COLUMNS as typed CSV — `name:type` pairs separated by `;`
|
||||||
tool — including authenticated `fetch()` against this API, `fs`/`vfs`, `sharp`, ExcelJS, PDFKit and
|
(e.g. `id:integer;name:string;active:boolean`); allowed types are `string`, `integer`, `number`,
|
||||||
WebCrypto globals.
|
`boolean` and `date`. The applet exports a function that receives the posted `input` as `applet_input`
|
||||||
|
and returns a row object (or an array of row objects) keyed by those columns.
|
||||||
|
|
||||||
|
Applets return typed CSV, not JSON: `result` is a `;`-separated CSV string with a header row, and the
|
||||||
|
response also carries `columns` (name/type) and `rows` (data row count). Only declared columns are
|
||||||
|
emitted, in header order; missing values become empty cells. Execution runs in the same isolated worker
|
||||||
|
sandbox as the `javascript_execute` tool — including authenticated `fetch()` against this API,
|
||||||
|
`fs`/`vfs`, `sharp`, ExcelJS, PDFKit and WebCrypto globals.
|
||||||
|
|
||||||
The path is taken from the URL (like `DELETE /vfs/{*path}`); the run arguments are the JSON body.
|
The path is taken from the URL (like `DELETE /vfs/{*path}`); the run arguments are the JSON body.
|
||||||
Path traversal (`..`), null bytes, and backslashes are rejected with HTTP 400, as is a path that does
|
Path traversal (`..`), null bytes, and backslashes are rejected with HTTP 400, as is a path that does
|
||||||
@@ -52,7 +59,17 @@ Applet executed successfully
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"result": null,
|
"result": "id;name\n1;Kaffee",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "…",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rows": 1,
|
||||||
|
"warnings": [
|
||||||
|
"…"
|
||||||
|
],
|
||||||
"stdout": "…",
|
"stdout": "…",
|
||||||
"consoleLines": 1,
|
"consoleLines": 1,
|
||||||
"durationMs": 1,
|
"durationMs": 1,
|
||||||
|
|||||||
@@ -72,10 +72,17 @@ export function register(app, basePath = '') {
|
|||||||
*
|
*
|
||||||
* The applet file must begin with two metadata lines:
|
* The applet file must begin with two metadata lines:
|
||||||
* `// @applet-input-schema {JSON Schema}` followed by
|
* `// @applet-input-schema {JSON Schema}` followed by
|
||||||
* `// @applet-output-schema {JSON Schema}`. The file exports a function that receives the posted `input`
|
* `// @applet-output-schema {name:type;name:type}`. The input schema describes the GUI form posted as
|
||||||
* as `applet_input`, and execution runs in the same isolated worker sandbox as the `javascript_execute`
|
* `input`. The output line declares the RESULT COLUMNS as typed CSV — `name:type` pairs separated by `;`
|
||||||
* tool — including authenticated `fetch()` against this API, `fs`/`vfs`, `sharp`, ExcelJS, PDFKit and
|
* (e.g. `id:integer;name:string;active:boolean`); allowed types are `string`, `integer`, `number`,
|
||||||
* WebCrypto globals.
|
* `boolean` and `date`. The applet exports a function that receives the posted `input` as `applet_input`
|
||||||
|
* and returns a row object (or an array of row objects) keyed by those columns.
|
||||||
|
*
|
||||||
|
* Applets return typed CSV, not JSON: `result` is a `;`-separated CSV string with a header row, and the
|
||||||
|
* response also carries `columns` (name/type) and `rows` (data row count). Only declared columns are
|
||||||
|
* emitted, in header order; missing values become empty cells. Execution runs in the same isolated worker
|
||||||
|
* sandbox as the `javascript_execute` tool — including authenticated `fetch()` against this API,
|
||||||
|
* `fs`/`vfs`, `sharp`, ExcelJS, PDFKit and WebCrypto globals.
|
||||||
*
|
*
|
||||||
* The path is taken from the URL (like `DELETE /vfs/{*path}`); the run arguments are the JSON body.
|
* The path is taken from the URL (like `DELETE /vfs/{*path}`); the run arguments are the JSON body.
|
||||||
* Path traversal (`..`), null bytes, and backslashes are rejected with HTTP 400, as is a path that does
|
* Path traversal (`..`), null bytes, and backslashes are rejected with HTTP 400, as is a path that does
|
||||||
@@ -115,13 +122,34 @@ export function register(app, basePath = '') {
|
|||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* required: [ok, result, stdout, consoleLines, durationMs]
|
* required: [ok, result, columns, rows, stdout, consoleLines, durationMs]
|
||||||
* properties:
|
* properties:
|
||||||
* ok:
|
* ok:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* result:
|
* result:
|
||||||
* description: The value returned by the applet's exported function
|
* type: string
|
||||||
* nullable: true
|
* description: Typed CSV table (`;`-separated, header row first) built from the returned rows and the declared output columns
|
||||||
|
* example: "id;name\n1;Kaffee"
|
||||||
|
* columns:
|
||||||
|
* type: array
|
||||||
|
* description: Declared output columns, in header order
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* required: [name, type]
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* type:
|
||||||
|
* type: string
|
||||||
|
* enum: [string, integer, number, boolean, date]
|
||||||
|
* rows:
|
||||||
|
* type: integer
|
||||||
|
* description: Number of data rows in `result` (excludes the header row)
|
||||||
|
* warnings:
|
||||||
|
* type: array
|
||||||
|
* description: Present when cells were dropped for not matching their declared type
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
* stdout:
|
* stdout:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Captured console output
|
* description: Captured console output
|
||||||
|
|||||||
@@ -10,6 +10,171 @@ const DEFAULT_TIMEOUT_MS = 20_000;
|
|||||||
const MAX_TIMEOUT_MS = 60_000;
|
const MAX_TIMEOUT_MS = 60_000;
|
||||||
const DEFAULT_MAX_OUTPUT = 20_000;
|
const DEFAULT_MAX_OUTPUT = 20_000;
|
||||||
|
|
||||||
|
// Column types an applet may declare in its `@applet-output-schema` header. Kept deliberately
|
||||||
|
// small: each one maps to a JS-coercion rule the applet author can reason about, and the
|
||||||
|
// frontend can render without guessing.
|
||||||
|
const APPLET_COLUMN_TYPES = new Set(['string', 'integer', 'number', 'boolean', 'date']);
|
||||||
|
|
||||||
|
// Semicolon-separated columns, matching the spreadsheet exports in `src/util/*Csv.js`.
|
||||||
|
const APPLET_CSV_SEPARATOR = ';';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the applet output header into typed CSV columns.
|
||||||
|
*
|
||||||
|
* The header carries the COLUMN NAMES AND TYPES, e.g. `id:integer;name:string;active:boolean`.
|
||||||
|
* A bare `name` (no `:type`) is accepted and defaults to `string`. Types are limited to
|
||||||
|
* {@link APPLET_COLUMN_TYPES} so a typo surfaces here instead of at render time.
|
||||||
|
*
|
||||||
|
* @param {string} header raw text after the `// @applet-output-schema ` marker
|
||||||
|
* @returns {Array<{ name: string, type: string }>}
|
||||||
|
*/
|
||||||
|
function parseTypedCsvColumns(header) {
|
||||||
|
const text = String(header || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
throw new ToolError(
|
||||||
|
'Applet output schema must declare typed CSV columns, e.g. ' +
|
||||||
|
'`@applet-output-schema id:integer;name:string`.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const raw of text.split(APPLET_CSV_SEPARATOR)) {
|
||||||
|
const field = raw.trim();
|
||||||
|
if (!field) continue;
|
||||||
|
|
||||||
|
const sep = field.lastIndexOf(':');
|
||||||
|
const name = (sep === -1 ? field : field.slice(0, sep)).trim();
|
||||||
|
const type = (sep === -1 ? 'string' : field.slice(sep + 1).trim().toLowerCase());
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
throw new ToolError(`Applet output column name is empty in "${field}".`);
|
||||||
|
}
|
||||||
|
if (!APPLET_COLUMN_TYPES.has(type)) {
|
||||||
|
throw new ToolError(
|
||||||
|
`Applet output column "${name}" has unknown type "${type}". ` +
|
||||||
|
`Use one of: ${[...APPLET_COLUMN_TYPES].join(', ')}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (seen.has(name)) {
|
||||||
|
throw new ToolError(`Applet output column "${name}" is declared twice.`);
|
||||||
|
}
|
||||||
|
seen.add(name);
|
||||||
|
columns.push({ name, type });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columns.length === 0) {
|
||||||
|
throw new ToolError('Applet output schema must declare at least one column.');
|
||||||
|
}
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote one CSV field (RFC 4180): wrap when it contains the separator, a quote, or a newline.
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function encodeCsvField(value) {
|
||||||
|
const text = value == null ? '' : String(value);
|
||||||
|
if (text.includes(APPLET_CSV_SEPARATOR) || text.includes('"') || /[\r\n]/.test(text)) {
|
||||||
|
return `"${text.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a returned cell to its declared type and report what happened.
|
||||||
|
*
|
||||||
|
* Returns `null` when the value cannot be coerced (`undefined`, `null`, `NaN`, unparseable
|
||||||
|
* date). The caller turns that into an empty CSV cell, so a missing value never aborts the run.
|
||||||
|
*
|
||||||
|
* @param {unknown} value
|
||||||
|
* @param {string} type
|
||||||
|
* @returns {{ ok: true, value: string | number | boolean } | { ok: false }}
|
||||||
|
*/
|
||||||
|
function coerceAppletCell(value, type) {
|
||||||
|
if (value === undefined || value === null) return { ok: false };
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
return { ok: true, value: typeof value === 'object' ? JSON.stringify(value) : String(value) };
|
||||||
|
case 'boolean':
|
||||||
|
if (typeof value === 'boolean') return { ok: true, value };
|
||||||
|
if (value === 1 || value === 0) return { ok: true, value: value === 1 };
|
||||||
|
if (typeof value === 'string' && /^(true|false)$/i.test(value.trim())) {
|
||||||
|
return { ok: true, value: value.trim().toLowerCase() === 'true' };
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
case 'integer':
|
||||||
|
case 'number': {
|
||||||
|
const num = typeof value === 'number' ? value : Number(String(value).trim());
|
||||||
|
if (!Number.isFinite(num)) return { ok: false };
|
||||||
|
return { ok: true, value: type === 'integer' ? Math.trunc(num) : num };
|
||||||
|
}
|
||||||
|
case 'date': {
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return { ok: false };
|
||||||
|
return { ok: true, value: date.toISOString() };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize an applet's return value into typed CSV keyed by its declared columns.
|
||||||
|
*
|
||||||
|
* The applet returns rows as objects (or arrays). Only declared columns are emitted, in header
|
||||||
|
* order; unknown keys are dropped and missing ones become empty cells. Scalars and key
|
||||||
|
* information that belong to the row itself are surfaced as `warnings` rather than failing.
|
||||||
|
*
|
||||||
|
* @param {unknown} result value returned by the applet's exported function
|
||||||
|
* @param {Array<{ name: string, type: string }>} columns
|
||||||
|
* @returns {{ csv: string, rows: number, columns: Array<{ name: string, type: string }>, warnings: string[] }}
|
||||||
|
*/
|
||||||
|
function toTypedCsv(result, columns) {
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
let rows;
|
||||||
|
if (result === undefined || result === null) {
|
||||||
|
rows = [];
|
||||||
|
} else if (Array.isArray(result)) {
|
||||||
|
rows = result;
|
||||||
|
} else if (typeof result === 'object') {
|
||||||
|
// A single object is one row — the common "return the totals" case.
|
||||||
|
rows = [result];
|
||||||
|
} else {
|
||||||
|
rows = [result];
|
||||||
|
warnings.push(
|
||||||
|
`Applet returned a ${typeof result}, which has no columns to map onto; ` +
|
||||||
|
'the value was placed in the first column.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let coercions = 0;
|
||||||
|
const body = rows.map((row) => {
|
||||||
|
const isContainer = row && typeof row === 'object' && !Array.isArray(row);
|
||||||
|
return columns
|
||||||
|
.map((column, index) => {
|
||||||
|
const value = isContainer ? row[column.name] : index === 0 ? row : undefined;
|
||||||
|
const coerced = coerceAppletCell(value, column.type);
|
||||||
|
if (!coerced.ok) {
|
||||||
|
if (value !== undefined && value !== null) coercions += 1;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return encodeCsvField(coerced.value);
|
||||||
|
})
|
||||||
|
.join(APPLET_CSV_SEPARATOR);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (coercions > 0) {
|
||||||
|
warnings.push(`${coercions} cell(s) did not match their declared column type and were left empty.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const csv = [columns.map((c) => encodeCsvField(c.name)).join(APPLET_CSV_SEPARATOR), ...body].join('\n');
|
||||||
|
return { csv, rows: body.length, columns, warnings };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs one snippet/applet on the shared sandbox pool.
|
* Runs one snippet/applet on the shared sandbox pool.
|
||||||
*
|
*
|
||||||
@@ -69,27 +234,24 @@ function readApplet(vfsRoot, appletvfsfile) {
|
|||||||
throw new ToolError(
|
throw new ToolError(
|
||||||
'Applet metadata must be on the first two lines: ' +
|
'Applet metadata must be on the first two lines: ' +
|
||||||
'`// @applet-input-schema {JSON Schema}` followed by ' +
|
'`// @applet-input-schema {JSON Schema}` followed by ' +
|
||||||
'`// @applet-output-schema {JSON Schema}`.'
|
'`// @applet-output-schema name:type;name:type` (typed CSV columns).'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputSchema;
|
let inputSchema;
|
||||||
let outputSchema;
|
|
||||||
try {
|
try {
|
||||||
inputSchema = JSON.parse(inputHeader[1]);
|
inputSchema = JSON.parse(inputHeader[1]);
|
||||||
outputSchema = JSON.parse(outputHeader[1]);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ToolError(`Applet metadata must contain valid JSON schemas: ${err.message}`);
|
throw new ToolError(`Applet input schema must be valid JSON: ${err.message}`);
|
||||||
}
|
}
|
||||||
if (!inputSchema || typeof inputSchema !== 'object' || Array.isArray(inputSchema) ||
|
if (!inputSchema || typeof inputSchema !== 'object' || Array.isArray(inputSchema) ||
|
||||||
inputSchema.type !== 'object') {
|
inputSchema.type !== 'object') {
|
||||||
throw new ToolError('Applet input schema must be a JSON Schema with type "object".');
|
throw new ToolError('Applet input schema must be a JSON Schema with type "object".');
|
||||||
}
|
}
|
||||||
if (!outputSchema || typeof outputSchema !== 'object' || Array.isArray(outputSchema)) {
|
|
||||||
throw new ToolError('Applet output schema must be a JSON Schema object.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return source;
|
const outputColumns = parseTypedCsvColumns(outputHeader[1]);
|
||||||
|
|
||||||
|
return { source, outputColumns };
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSandboxError(code, report) {
|
function formatSandboxError(code, report) {
|
||||||
@@ -112,7 +274,9 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
|
|||||||
description:
|
description:
|
||||||
'Execute JS in an isolated worker sandbox using either inline `code` or a VFS applet. Provide exactly one of `code` and `appletvfsfile`. ' +
|
'Execute JS in an isolated worker sandbox using either inline `code` or a VFS applet. Provide exactly one of `code` and `appletvfsfile`. ' +
|
||||||
'Applets are the preferred method when the first execution may throw: save a `.applet.js` file in the VFS so it can be inspected and further edited with `patch`, then call it by VFS path. ' +
|
'Applets are the preferred method when the first execution may throw: save a `.applet.js` file in the VFS so it can be inspected and further edited with `patch`, then call it by VFS path. ' +
|
||||||
'Applet files must start with `// @applet-input-schema {JSON Schema}` and `// @applet-output-schema {JSON Schema}` on their first two lines. The input schema describes the GUI form passed as `applet_input`; the output schema describes the exported function result. Export a function that receives `applet_input`. ' +
|
'Applet files must start with `// @applet-input-schema {JSON Schema}` on line 1 and `// @applet-output-schema <columns>` on line 2. ' +
|
||||||
|
'The input schema describes the GUI form passed as `applet_input`. The output line declares the RESULT COLUMNS as typed CSV: `name:type` pairs separated by `;` (e.g. `id:integer;name:string;active:boolean`); a bare `name` defaults to string, and the allowed types are string, integer, number, boolean and date. ' +
|
||||||
|
'Applets return typed CSV, NOT JSON: export a function that receives `applet_input` and returns a row object, or an array of row objects, whose keys are the declared columns (e.g. `return [{ id: 1, name: "Kaffee" }]`). Only declared columns are emitted, in header order; missing keys become empty cells. The tool result then carries `result` as a `;`-separated CSV string with a header row, plus `columns` (name/type) and `rows` (data row count). Returning a single object yields one data row. ' +
|
||||||
'Inline code: assign `module.exports` to return value (sync value or Promise; wrap async in IIFE: `module.exports = (async () => { ... })()`). ' +
|
'Inline code: assign `module.exports` to return value (sync value or Promise; wrap async in IIFE: `module.exports = (async () => { ... })()`). ' +
|
||||||
'REST API: `await fetch("/path")` (authenticated). ' +
|
'REST API: `await fetch("/path")` (authenticated). ' +
|
||||||
'VFS: directly use global `fs.readFileSync`/`writeFileSync` for binary artifacts/buffers/images (`image/` is read-only; paths must be relative paths, root is not allowed). VFS can host a static website (HTML, CSS, JS, images, etc.). ' +
|
'VFS: directly use global `fs.readFileSync`/`writeFileSync` for binary artifacts/buffers/images (`image/` is read-only; paths must be relative paths, root is not allowed). VFS can host a static website (HTML, CSS, JS, images, etc.). ' +
|
||||||
@@ -133,11 +297,11 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
|
|||||||
},
|
},
|
||||||
appletvfsfile: {
|
appletvfsfile: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Optional VFS-relative path to a .applet.js file. Provide exactly one of this and code. The file must begin with the two applet JSON Schema metadata lines described in the tool description.',
|
description: 'Optional VFS-relative path to a .applet.js file. Provide exactly one of this and code. The file must begin with the two applet metadata lines (`@applet-input-schema` JSON Schema, then `@applet-output-schema` typed CSV columns like `id:integer;name:string`).',
|
||||||
},
|
},
|
||||||
applet_input: {
|
applet_input: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Input values passed to the exported function when running appletvfsfile. Omit to pass an empty object.',
|
description: 'Input values passed to the exported function when running appletvfsfile. Omit to pass an empty object. The applet returns typed CSV rows mapped onto its `@applet-output-schema` columns.',
|
||||||
additionalProperties: true,
|
additionalProperties: true,
|
||||||
},
|
},
|
||||||
timeout: {
|
timeout: {
|
||||||
@@ -181,7 +345,8 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
|
|||||||
|
|
||||||
const vfsRoot = await prepareVfs({ googleAuthId });
|
const vfsRoot = await prepareVfs({ googleAuthId });
|
||||||
const isApplet = hasApplet;
|
const isApplet = hasApplet;
|
||||||
const source = isApplet ? readApplet(vfsRoot, appletvfsfile) : code;
|
const applet = isApplet ? readApplet(vfsRoot, appletvfsfile) : null;
|
||||||
|
const source = isApplet ? applet.source : code;
|
||||||
let report;
|
let report;
|
||||||
try {
|
try {
|
||||||
report = await runInWorker({
|
report = await runInWorker({
|
||||||
@@ -238,6 +403,15 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
|
|||||||
// Always present, even when empty, so callers can rely on the shape.
|
// Always present, even when empty, so callers can rely on the shape.
|
||||||
stdout,
|
stdout,
|
||||||
};
|
};
|
||||||
|
// Applets return typed CSV, not JSON: the declared columns turn the returned rows
|
||||||
|
// into a `;`-separated table. Inline code keeps returning its raw value.
|
||||||
|
if (isApplet) {
|
||||||
|
const table = toTypedCsv(report.result, applet.outputColumns);
|
||||||
|
result.result = table.csv;
|
||||||
|
result.rows = table.rows;
|
||||||
|
result.columns = table.columns;
|
||||||
|
if (table.warnings.length) result.warnings = table.warnings;
|
||||||
|
}
|
||||||
if (truncated) {
|
if (truncated) {
|
||||||
result.truncated = true;
|
result.truncated = true;
|
||||||
result.truncationNote = `Console output longer than ${maxOutput} chars was capped.`;
|
result.truncationNote = `Console output longer than ${maxOutput} chars was capped.`;
|
||||||
|
|||||||
Reference in New Issue
Block a user