Compare commits

..

3 Commits

Author SHA1 Message Date
seb
06dc4629da u 2026-10-12 14:18:54 +02:00
seb
88ecf5bf5c u 2026-10-12 14:07:40 +02:00
seb
280ed1b2b8 u 2026-10-12 13:52:13 +02:00
11 changed files with 221 additions and 54 deletions

View File

@@ -801,7 +801,7 @@ _none_
- Returns: `200 str`
#### Execute a virtual filesystem applet — `POST /vfs/{*path}`
- Params: `path: str, body?: { input?: {}, timeout?: int, max_output?: int }`
- 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 }`
- Returns: `200 { ok: bool, result: str, columns: { name: str, type: str }[], rows: int, stdout: str, consoleLines: int, durationMs: int, description?: str, warnings?: str[], truncated?: bool, truncationNote?: str }`
#### Create or overwrite a virtual filesystem file — `PUT /vfs/{*path}`
- Params: `path: str, body: { content: str, mime?: str }`
- Returns: `200 { success: bool, file: { name: str, size: int, mime: str, updated_at: str } }`

View File

@@ -7,9 +7,10 @@ Execute a virtual filesystem applet
Runs a `.applet.js` file from the virtual filesystem for the authenticated user's selected company
and returns the exported function's result.
The applet file must begin with two metadata lines:
The applet file must begin with three metadata lines:
`// @applet-input-schema {JSON Schema}` followed by
`// @applet-output-schema {name:type;name:type}`. The input schema describes the GUI form posted as
`// @applet-output-schema {name:type;name:type}` and
`// @applet-description "description"`. The input schema describes the GUI form posted as
`input`. The output line declares the RESULT COLUMNS as typed CSV — `name:type` pairs separated by `;`
(e.g. `id:integer;name:string;active:boolean`); allowed types are `string`, `integer`, `number`,
`boolean` and `date`. The applet exports a function that receives the posted `input` as `applet_input`
@@ -59,6 +60,7 @@ Applet executed successfully
```json
{
"ok": true,
"description": "…",
"result": "id;name\n1;Kaffee",
"columns": [
{

View File

@@ -41,7 +41,7 @@ function appletExecErrorStatus(error) {
if (message.includes('must be an object')) return 400;
if (message.includes('Unable to read applet') || message.includes('ENOENT')) return 404;
if (message.includes('is not a file')) return 400;
if (message.includes('Applet metadata') || message.includes('Applet input schema') || message.includes('Applet output schema')) return 400;
if (message.includes('Applet metadata') || message.includes('Applet input schema') || message.includes('Applet output schema') || message.includes('Applet description')) return 400;
if (message.includes('Sandbox exceeded')) return 504;
if (message.includes('Sandbox worker did not report a result payload')) return 500;
return 400;
@@ -71,9 +71,10 @@ export function register(app, basePath = '') {
* Runs a `.applet.js` file from the virtual filesystem for the authenticated user's selected company
* and returns the exported function's result.
*
* The applet file must begin with two metadata lines:
* The applet file must begin with three metadata lines:
* `// @applet-input-schema {JSON Schema}` followed by
* `// @applet-output-schema {name:type;name:type}`. The input schema describes the GUI form posted as
* `// @applet-output-schema {name:type;name:type}` and
* `// @applet-description "description"`. The input schema describes the GUI form posted as
* `input`. The output line declares the RESULT COLUMNS as typed CSV — `name:type` pairs separated by `;`
* (e.g. `id:integer;name:string;active:boolean`); allowed types are `string`, `integer`, `number`,
* `boolean` and `date`. The applet exports a function that receives the posted `input` as `applet_input`
@@ -127,6 +128,9 @@ export function register(app, basePath = '') {
* properties:
* ok:
* type: boolean
* description:
* type: string
* description: Applet description declared in the @applet-description header
* result:
* type: string
* description: Typed CSV table (`;`-separated, header row first) built from the returned rows and the declared output columns

View File

@@ -1,5 +1,6 @@
// @applet-input-schema {"type":"object","properties":{"query":{"type":"string","title":"Article name contains","default":"","description":"Case-insensitive substring match on the article name. Leave empty to include every article."},"limit":{"type":"integer","title":"Max rows","default":200,"minimum":1,"maximum":1000},"onlyTracked":{"type":"boolean","title":"Only stock-tracked articles","default":false,"description":"When true, articles with track_stock=false are omitted."}}}
// @applet-output-schema sku:string;name:string;net:number;taxRate:number;track_stock:boolean;categoryId:integer
// @applet-description "Article catalogue — articles with their net price, resolved by walking the category tree."
//
// Article catalogue — articles with their net price, resolved by walking the category tree.
//

View File

@@ -1,5 +1,6 @@
// @applet-input-schema {"type":"object","properties":{"sku":{"type":"string","title":"Article SKU","description":"Exact SKU to inspect (e.g. \"42000009\")."}},"required":["sku"]}
// @applet-output-schema key:string;value:string
// @applet-description "Article lookup — the raw GET /article/{sku} payload as a key/value sheet."
//
// Article lookup — the raw `GET /article/{sku}` payload as a key/value sheet.
//

View File

@@ -1,5 +1,6 @@
// @applet-input-schema {"type":"object","properties":{"query":{"type":"string","title":"Category name contains","default":"","description":"Case-insensitive substring match on the category name. Leave empty to include every category."},"limit":{"type":"integer","title":"Max rows","default":100,"minimum":1,"maximum":500},"sortBy":{"type":"string","title":"Sort by","default":"articles","enum":["articles","name","depth"],"description":"articles = most articles first, name = A–Z, depth = tree depth then name."}}}
// @applet-output-schema id:integer;name:string;depth:integer;articles:integer;children:integer
// @applet-description "Category inventory — every category with its article count and child count."
//
// Category inventory — every category with its article count and child count.
//

View File

@@ -1,5 +1,6 @@
// @applet-input-schema {"type":"object","properties":{"dateFrom":{"type":"string","title":"From date (YYYY-MM-DD)","default":"","description":"Only include documents dated on or after this day. Leave empty for no lower bound."},"dateTo":{"type":"string","title":"To date (YYYY-MM-DD)","default":"","description":"Only include documents dated on or before this day. Leave empty for no upper bound."},"limit":{"type":"integer","title":"Max rows","default":100,"minimum":1,"maximum":500}}}
// @applet-output-schema number:string;date:string;supplier:string;lines:integer;net:number
// @applet-description "Goods receipt register — deliveries with their line-item count and net total."
//
// Goods receipt register — deliveries with their line-item count and net total.
//

View File

@@ -1,5 +1,6 @@
// @applet-input-schema {"type":"object","properties":{"month":{"type":"string","title":"Month (YYYY-MM)","default":"","description":"Only include orders placed in this month. Leave empty for all months."},"limit":{"type":"integer","title":"Max rows","default":50,"minimum":1,"maximum":500}}}
// @applet-output-schema month:string;orders:integer;positions:integer;net:number
// @applet-description "Order revenue by month — orders grouped per calendar month."
//
// Order revenue by month — orders grouped per calendar month.
//

View File

@@ -0,0 +1,129 @@
Applet Guide: Sandboxed Execution, Typed CSV Output
# Overview
An applet is a flat, reusable `.applet.js` script stored in the VFS that queries Dashback REST endpoints and computes tabular data.
Applets return structured, semicolon-separated (`;`) typed CSV conforming to output metadata headers.
Applets can be executed by:
1. `javascript_execute` tool using `{ "appletvfsfile": "<name>.applet.js", "applet_input": { ... } }`.
2. REST API endpoint `POST /vfs/<name>.applet.js` with `{ "input": { ... } }`.
---
# Every `.applet.js` file MUST start with three metadata lines:
```
// @applet-input-schema {"type":"object","properties":{...},"required":[...]}
// @applet-output-schema name:type;name:type
// @applet-description "Lorem ipsum"
```
`@applet-input-schema`
- JSON Schema object (`"type": "object"`).
- Parameter descriptions in the schema are optional
`@applet-output-schema`
- Semicolon-separated (`col1:type;col2:type;...`) column declarations.
- Bare column names without `:type` default to `string`.
- Allowed data types:
- `string`: plain text or serialized object.
- `integer`: truncated integer number.
- `number`: floating-point number.
- `boolean`: boolean value (`true`, `false`, `1`, `0`, `"true"`, `"false"`).
- `date`: Date object or ISO string.
`@applet-description`
- Short human-readable summary of what the applet computes (e.g. `"Article catalogue with net prices"`).
---
# Execution Environment & Globals
Applets run in an isolated worker thread pool sandbox.
- **Entry point**: Export an async function taking `applet_input`:
```javascript
module.exports = async function (applet_input) { ... };
```
- **No `require()` or `import`**: Attempting to import modules throws an error.
- **Pre-injected Globals**:
- `fetch(path, options)`: Authenticated `fetch` pre-configured with a JWT scoped to the current user and selected company. Use root-relative API routes, e.g. `fetch('/article/42000009')`.
- `fs` / `vfs`: Sandboxed VFS filesystem interface for reading/writing local company files.
- `ExcelJS`: Pre-injected spreadsheet library.
- `PDFDocument`: Pre-injected PDFKit library.
- `sharp`: Pre-injected image manipulation library.
- `crypto`: Node / WebCrypto API.
- `console.log(...)`: returned in the tool response
---
# Return Value & Typed CSV Mapping
The exported function must return:
- An **Array of row objects**: `[ { col1: 'val1', col2: 10 }, { col1: 'val2', col2: 20 } ]`
- Or a **Single row object**: `{ col1: 'val1', col2: 10 }` (treated as a 1-row table)
---
# Standard Applet Template
```javascript
// @applet-input-schema {"type":"object","properties":{"sku":{"type":"string","title":"Article SKU","description":"SKU to lookup"}},"required":["sku"]}
// @applet-output-schema sku:string;name:string;netPrice:number;tracked:boolean;brand:string
// @applet-description "Lorem ipsum"
module.exports = async function (applet_input) {
const sku = String(applet_input?.sku || '').trim();
if (!sku) {
throw new Error('Missing required parameter: sku');
}
const res = await fetch(`/article/${encodeURIComponent(sku)}`);
if (res.status === 404) throw new Error(`Article "${sku}" not found.`);
if (!res.ok) throw new Error(`GET /article/${sku} failed with status ${res.status}`);
const { article } = await res.json();
const price = (article.prices || [])[0];
console.log(`Found article ${article.sku}: ${article.name}`);
return [{
sku: article.sku,
name: article.name || '',
netPrice: price?.net ?? '',
tracked: Boolean(article.track_stock),
brand: article.brand?.name || ''
}];
};
```
---
# Common Implementation Patterns
### 1. Paging List Endpoints
Page using `offset` and `limit` until all items are loaded or `limit` is reached:
```javascript
const items = [];
let offset = 0;
while (true) {
const res = await fetch(`/order?offset=${offset}&limit=100`);
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
const data = await res.json();
const page = data.orders || [];
items.push(...page);
offset += page.length;
if (page.length === 0 || offset >= (data.total ?? items.length)) break;
}
```
---
# Shipped Reference Applets
Pre-installed examples in the VFS root provide working reference code:
- `v1-article-lookup.applet.js`: Single entity lookup and key/value sheet generation.
- `v1-article-catalogue.applet.js`: Walking category trees and joining category article lists.
- `v1-category-inventory.applet.js`: Recursive tree flattening and depth calculation.
- `v1-delivery-register.applet.js`: Paged list fetching with batched detail requests.
- `v1-order-revenue-by-month.applet.js`: Paged order retrieval with calendar month aggregation.

View File

@@ -197,6 +197,32 @@ function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appl
});
}
/**
* Parse the applet description header.
*
* Accepts either a quoted string (e.g. `"Lorem ipsum"` or `'Lorem ipsum'`) or bare text.
* Strips surrounding quotes if present and trims whitespace.
*
* @param {string} header raw text after the `// @applet-description ` marker
* @returns {string}
*/
function parseAppletDescription(header) {
const text = String(header || '').trim();
if (!text) {
throw new ToolError('Applet description cannot be empty.');
}
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
try {
const parsed = JSON.parse(text);
if (typeof parsed === 'string' && parsed.trim()) return parsed.trim();
} catch {
const unquoted = text.slice(1, -1).trim();
if (unquoted) return unquoted;
}
}
return text;
}
function readApplet(vfsRoot, appletvfsfile) {
if (typeof appletvfsfile !== 'string' || !appletvfsfile.trim()) {
throw new ToolError('"appletvfsfile" must be a non-empty VFS-relative file path.');
@@ -228,14 +254,16 @@ function readApplet(vfsRoot, appletvfsfile) {
throw new ToolError(`Unable to read applet "${appletPath}" from the VFS: ${err.message}`);
}
const lines = source.replace(/^\uFEFF/, '').split(/\r?\n/, 3);
const lines = source.replace(/^\uFEFF/, '').split(/\r?\n/, 4);
const inputHeader = lines[0]?.match(/^\/\/ @applet-input-schema (.+)$/);
const outputHeader = lines[1]?.match(/^\/\/ @applet-output-schema (.+)$/);
if (!inputHeader || !outputHeader) {
const descHeader = lines[2]?.match(/^\/\/ @applet-description (.+)$/);
if (!inputHeader || !outputHeader || !descHeader) {
throw new ToolError(
'Applet metadata must be on the first two lines: ' +
'Applet metadata must be on the first three lines: ' +
'`// @applet-input-schema {JSON Schema}` followed by ' +
'`// @applet-output-schema name:type;name:type` (typed CSV columns).'
'`// @applet-output-schema name:type;name:type` (typed CSV columns) and ' +
'`// @applet-description "description"`.'
);
}
@@ -251,8 +279,9 @@ function readApplet(vfsRoot, appletvfsfile) {
}
const outputColumns = parseTypedCsvColumns(outputHeader[1]);
const description = parseAppletDescription(descHeader[1]);
return { source, outputColumns };
return { source, outputColumns, description, inputSchema };
}
function formatSandboxError(code, report) {
@@ -401,6 +430,7 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
result.result = table.csv;
result.rows = table.rows;
result.columns = table.columns;
result.description = applet.description;
if (table.warnings.length) result.warnings = table.warnings;
}
if (truncated) {

View File

@@ -1,9 +1,39 @@
import * as dbCompanySetting from '../db/companysetting.js';
import { listSkillFiles } from './skill.js';
// #region Tool Prompts & Descriptions
// #region System Prompt
const NOTE_DESC = 'Status message shown to the user during execution.';
export const SYSTEM_PROMPT_RULES = [
'RULES & VFS:',
'- VFS: Persistent root-relative paths ("labels/sheet1.png"). Uploads land in "attachments/..." ([ATTACHMENT name= path= type=]); inspect via read_file, pdf_read, analyze_image, or javascript_execute + ExcelJS. Can host static sites.',
'- Links: Web links use "https://..." or domains; relative paths format as [label](relative/path). Inline images: ![alt](images/path.avif). Other files require download links.',
'- SKILLS: Flat root "<slug>.SKILL" markdown files. ALWAYS read_file matching skill BEFORE executing covered tasks. Exempt from reset_vfs.',
'- API: Routes are not inlined. BEFORE dashback_request, read_file the api-<section>.SKILL for parameters and response structures. api-binary-streaming-routes.SKILL covers fetch()-only upload/stream endpoints.',
'- Types: T<n> refers to globally shared object schemas across skills.',
'- Images: Pipeline: search_images -> analyze_image -> upload via fetch() in javascript_execute. "image/" is read-only; write generated assets to "images/...".',
'- Labels: Use Dashback JSON `{ "objects": [ ... ] }` for /labeltemplate. Never use ZPL.',
'- Navigation: Call set_user_route when the user asks to open or view any page/entity.',
].join('\n');
export const SYSTEM_PROMPT_ROUTE_MATRIX = [
'ROUTE MATRIX (client German, API skills = api-<section>):',
'- Search: /artikel?target={query}&filter={filter}&offset={n} -> article, category, brand',
'- Article Detail: /artikel/${sku} -> article, category, brand, attributename, propertyname, unit, supplier, printer, labeltemplate, ledger',
'- Warehouse / Stock: /artikel/lager -> warehouse, stock, ledger',
'- Brands: /artikel/marken -> brand',
'- Label Templates: /artikel/vorlagen -> labeltemplate, printer',
'- Attributes: /artikel/merkmale -> attributename, attributevalue, propertyname',
'- Categories: /kategorien | /kategorien/${slug1}/${slug2} -> category, article',
'- Deliveries: /lieferung | /lieferung/neu | /lieferung/${deliveryId} | /lieferung/lieferanten -> delivery, supplier, article, warehouse, stock, ledger',
'- Quotes: /auftrag | /auftrag/neu | /auftrag/${quoteId} -> quote, entity, article',
'- Orders: /auftrag/bestellung | /auftrag/bestellung/neu | /auftrag/bestellung/${orderId} -> order, entity, account, payment, shipping, package',
'- Customers: /kunde | /kunde/neu | /kunde/${entityId} | /kunde/gruppen -> entity, customergroup, number',
'- History & Stats: /infos/articlehistory | /infos/globalhistory | /infos/statistics -> article, company, order, delivery, ledger',
].join('\n');
// #endregion
// #region Tool Prompts
/**
* Descriptions for all tools registered in the agent's toolbelt.
@@ -18,7 +48,7 @@ export const TOOL_DESCRIPTIONS = {
javascript_execute:
'Execute JS in an isolated sandbox via inline `code` (assign to `module.exports`) or `appletvfsfile` ' +
'(.applet.js returning typed CSV per @applet-input-schema and @applet-output-schema; details in applet.SKILL). ' +
'(.applet.js returning typed CSV per @applet-input-schema, @applet-output-schema, and @applet-description; details in applet.SKILL). ' +
'Pre-injected globals: fs/vfs, sharp, ExcelJS, PDFDocument, crypto, and fetch("/path"). No require/import.',
find_files:
@@ -58,7 +88,7 @@ export const TOOL_DESCRIPTIONS = {
'Search articles by name, brand, EAN, SKU, or supplier SKU (AND logic, ordered by name).',
edit_image:
'Generate or edit images via modelslugimage. Supports reference image attachments, size tier, ratio, and VFS AVIF output.',
'Generate or edit images via a vision model. Supports reference image attachments, size tier, ratio, and VFS AVIF output.',
ean_checksum:
'Validate or calculate check digits for EAN-13, EAN-8, UPC-A, and GTIN-14 barcodes.',
@@ -70,7 +100,11 @@ export const TOOL_DESCRIPTIONS = {
'Navigate user to a frontend route when asked to view/open an entity or page (see ROUTE MATRIX in prompt).',
};
// #region Tool Parameter Descriptions
// #endregion
// #region Tool Parameters
const NOTE_DESC = 'Status message shown to the user during execution.';
/**
* Descriptions for tool input parameters across src/tools/*.
@@ -213,47 +247,10 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
export const PATCH_PARAMETER_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch;
export const PATCH_PARAM_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch;
// #endregion
// #region Tool-specific Subprompts
export const ANALYZE_IMAGE_VISION_COMPARE_PROMPT =
'Compare these images, evaluate visual quality, flag watermarks/overlays/defects, and state the best candidate with rationale.';
export const ANALYZE_IMAGE_VISION_DESCRIBE_PROMPT =
'Describe this image, evaluate visual quality, flag watermarks/overlays/defects, and verify suitability as a product/category image.';
// #endregion
// #endregion
// #region System Prompt Components
export const SYSTEM_PROMPT_RULES = [
'RULES & VFS:',
'- VFS: Persistent root-relative paths ("labels/sheet1.png"). Uploads land in "attachments/..." ([ATTACHMENT name= path= type=]); inspect via read_file, pdf_read, analyze_image, or javascript_execute + ExcelJS. Can host static sites.',
'- Links: Web links use "https://..." or domains; relative paths format as [label](relative/path). Inline images: ![alt](images/path.avif). Other files require download links.',
'- SKILLS: Flat root "<slug>.SKILL" markdown files. ALWAYS read_file matching skill BEFORE executing covered tasks. Exempt from reset_vfs.',
'- API: Routes are not inlined. BEFORE dashback_request, read_file the api-<section>.SKILL for parameters and response structures. api-binary-streaming-routes.SKILL covers fetch()-only upload/stream endpoints.',
'- Types: T<n> refers to globally shared object schemas across skills.',
'- Images: Pipeline: search_images -> analyze_image -> upload via fetch() in javascript_execute. "image/" is read-only; write generated assets to "images/...".',
'- Labels: Use Dashback JSON `{ "objects": [ ... ] }` for /labeltemplate. Never use ZPL.',
'- Navigation: Call set_user_route when the user asks to open or view any page/entity.',
].join('\n');
export const SYSTEM_PROMPT_ROUTE_MATRIX = [
'ROUTE MATRIX (client German, API skills = api-<section>):',
'- Search: /artikel?target={query}&filter={filter}&offset={n} -> article, category, brand',
'- Article Detail: /artikel/${sku} -> article, category, brand, attributename, propertyname, unit, supplier, printer, labeltemplate, ledger',
'- Warehouse / Stock: /artikel/lager -> warehouse, stock, ledger',
'- Brands: /artikel/marken -> brand',
'- Label Templates: /artikel/vorlagen -> labeltemplate, printer',
'- Attributes: /artikel/merkmale -> attributename, attributevalue, propertyname',
'- Categories: /kategorien | /kategorien/${slug1}/${slug2} -> category, article',
'- Deliveries: /lieferung | /lieferung/neu | /lieferung/${deliveryId} | /lieferung/lieferanten -> delivery, supplier, article, warehouse, stock, ledger',
'- Quotes: /auftrag | /auftrag/neu | /auftrag/${quoteId} -> quote, entity, article',
'- Orders: /auftrag/bestellung | /auftrag/bestellung/neu | /auftrag/bestellung/${orderId} -> order, entity, account, payment, shipping, package',
'- Customers: /kunde | /kunde/neu | /kunde/${entityId} | /kunde/gruppen -> entity, customergroup, number',
'- History & Stats: /infos/articlehistory | /infos/globalhistory | /infos/statistics -> article, company, order, delivery, ledger',
].join('\n');
// #endregion
@@ -290,7 +287,7 @@ export function buildSkillIndex(vfsRoot) {
// #endregion
// #region System Prompt
// #region Factory
/**
* System prompt: base instructions and contextual index.