This commit is contained in:
seb
2026-10-08 12:44:53 +02:00
parent 79a1b72292
commit e2e7f8d566
11 changed files with 172 additions and 21 deletions

View File

@@ -167,6 +167,12 @@ export function register(app, basePath = '') {
* description: Category ID under which to create the article
* name:
* type: string
* desc:
* type: string
* description: Article description text/HTML
* seo:
* type: string
* description: SEO text
* track_stock:
* type: boolean
* description: Whether to track inventory stock (defaults to true)
@@ -215,7 +221,7 @@ export function register(app, basePath = '') {
* $ref: '#/components/schemas/Error'
*/
app.post(`${basePath}/article`, async (req, res) => {
const rawParent = req.body?.categoryId ?? req.body?.parentId;
const rawParent = req.body?.categoryId;
if (rawParent === undefined || rawParent === null) {
return res.status(400).json({ error: 'categoryId is required' });
}
@@ -229,7 +235,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: 'name is required' });
}
const rawTrack = req.body?.track_stock ?? req.body?.trackStock;
const rawTrack = req.body?.track_stock;
if (rawTrack !== undefined && typeof rawTrack !== 'boolean' && rawTrack !== 0 && rawTrack !== 1) {
return res.status(400).json({ error: 'Invalid track_stock' });
}
@@ -237,12 +243,15 @@ export function register(app, basePath = '') {
const customSku = typeof req.body?.sku === 'string' && req.body.sku.trim() ? req.body.sku.trim() : undefined;
try {
const desc = req.body?.desc;
const created = await db.createUnderParent({
googleAuthId: req.user.id,
parentId,
name,
track_stock,
sku: customSku,
desc: desc !== undefined ? desc : undefined,
seo: req.body?.seo !== undefined ? req.body.seo : undefined,
});
emitArticlesSignal(req);
const article = {
@@ -438,6 +447,12 @@ export function register(app, basePath = '') {
* properties:
* name:
* type: string
* desc:
* type: string
* description: Article description text/HTML
* seo:
* type: string
* description: SEO text
* unitamount:
* type: number
* description: Must be finite and `> 0`
@@ -672,12 +687,23 @@ export function register(app, basePath = '') {
}
if (Object.prototype.hasOwnProperty.call(body, 'track_stock')) {
patch.track_stock = body.track_stock;
} else if (Object.prototype.hasOwnProperty.call(body, 'trackStock')) {
patch.track_stock = body.trackStock;
}
if (Object.prototype.hasOwnProperty.call(body, 'desc')) {
patch.desc = body.desc;
}
if (Object.prototype.hasOwnProperty.call(body, 'seo')) {
patch.seo = body.seo;
}
if (!('name' in patch) && !('unitamount' in patch) && !('unitSi' in patch) && !('track_stock' in patch)) {
return res.status(400).json({ error: 'name, unitamount, unitSi, or track_stock is required' });
if (
!('name' in patch) &&
!('unitamount' in patch) &&
!('unitSi' in patch) &&
!('track_stock' in patch) &&
!('desc' in patch) &&
!('seo' in patch)
) {
return res.status(400).json({ error: 'name, unitamount, unitSi, track_stock, desc, or seo is required' });
}
try {
@@ -1443,7 +1469,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: skuParsed.error });
}
const raw = req.body?.track_stock ?? req.body?.trackStock;
const raw = req.body?.track_stock;
if (raw === undefined || (typeof raw !== 'boolean' && raw !== 0 && raw !== 1)) {
return res.status(400).json({ error: 'Invalid track_stock' });
}
@@ -1783,7 +1809,7 @@ export function register(app, basePath = '') {
if (typeof isParent !== 'boolean') {
return res.status(400).json({ error: 'isParent boolean is required' });
}
const attributeNameId = req.body?.attributeNameId ?? req.body?.parentAttributeNameId;
const attributeNameId = req.body?.attributeNameId;
try {
const result = await db.toggleParent(req.user.id, parsed.sku, {

View File

@@ -1,8 +1,8 @@
import { getStore } from './store.js';
export async function createUnderParent({ googleAuthId, parentId, name, track_stock, sku }) {
export async function createUnderParent(args) {
const store = await getStore();
return store.article.createUnderParent({ googleAuthId, parentId, name, track_stock, sku });
return store.article.createUnderParent(args);
}
export async function getBySku(googleAuthId, sku) {

View File

@@ -70,10 +70,17 @@ function wrapStoreWithTiming(rawStore) {
storeLog.debug(`${namespace}.${method} ${chalk.gray(`(${Date.now() - start}ms)`)}`);
return result;
} catch (err) {
storeLog.error(
`${namespace}.${method} ${chalk.red('FAILED')} ` +
`${chalk.gray(`(${Date.now() - start}ms)`)} ${chalk.red(err.message)}`
);
const isNotFound = /not found/i.test(err?.message);
if (isNotFound) {
storeLog.debug(
`${namespace}.${method} ${chalk.gray(`(${Date.now() - start}ms)`)} ${chalk.gray(err.message)}`
);
} else {
storeLog.error(
`${namespace}.${method} ${chalk.red('FAILED')} ` +
`${chalk.gray(`(${Date.now() - start}ms)`)} ${chalk.red(err.message)}`
);
}
throw err;
}
};

View File

@@ -343,9 +343,17 @@ export async function executeCompanyImport({
jtlArtIdToSku.set(jtlArtId, sku);
let name = '';
let desc = '';
let seo = '';
try {
const desc = JSON.parse(a.descriptions || '[]');
name = desc[0]?.cName || '';
const descriptions = JSON.parse(a.descriptions || '[]');
const descItem = Array.isArray(descriptions) ? descriptions[0] : descriptions;
if (descItem) {
name = String(descItem.cName || '').trim();
desc = String(descItem.cBeschreibung || '').trim();
seo = String(descItem.cKurzBeschreibung || '').trim();
}
} catch {}
if (!name) name = `Artikel ${sku}`;
@@ -388,6 +396,8 @@ export async function executeCompanyImport({
await store.article.updateBySku(googleAuthId, sku, {
name: name.trim(),
track_stock: trackStock,
desc: desc || undefined,
seo: seo || undefined,
});
// Update prices
@@ -432,6 +442,8 @@ export async function executeCompanyImport({
name: name.trim(),
sku,
track_stock: trackStock,
desc: desc || undefined,
seo: seo || undefined,
});
// Set price

View File

@@ -28,6 +28,7 @@ import { createPdfReadTool } from '../tools/pdf-read.js';
import { createSearchArticlesTool } from '../tools/search-articles.js';
import { createEanChecksumTool } from '../tools/ean-checksum.js';
import { createNetGrossCalcTool } from '../tools/net-gross-calc.js';
import { createSetUserRouteTool } from '../tools/set-user-route.js';
import { resolveOpenRouterKey, resolveDefaultModel, resolveModelReasoning, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js';
const KEEP_ALIVE_MS = 20_000;
@@ -132,6 +133,7 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) {
'16. edit_image: Generate or edit images using the configured company image model (modelslugimage). Supports attachments (low/high quality), size tiers (1K/2K/4K), and aspect ratios (1:1/4:3/3:4). Writes output to VFS (always AVIF format).',
'17. ean_checksum: Validate EAN/GTIN/UPC barcode check digits or calculate missing check digit for barcodes (EAN-13, EAN-8, UPC-A, GTIN-14).',
'18. net_gross_calc: Convert between net and gross prices with tax/VAT rate (default 19%). Provides 2-decimal display values and high-precision net prices for API article creation.',
'19. set-user-route: Navigate the user to a frontend route (e.g. when the user says "show me the article", "open quote 1001", or asks to view a page/entity).',
'',
'VIRTUAL FILESYSTEM:',
'- Persistent per-company VFS. Paths are root-relative forward-slash (e.g. "labels/sheet1.png").',
@@ -157,7 +159,37 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) {
'- GROSS vs NET PRICES: User prices are almost always GROSS (incl. VAT); API requires NET. Use net_gross_calc to compute high-precision (4–6 decimals) NET prices for API requests: `net = gross / (1 + taxRate / 100)`.',
'- DISPLAYING GENERATED IMAGES: Inline markdown images supported. Embed written VFS image paths: `![descriptive alt](artifacts/example.png)`. Other files (PDF, JSON, CSV) do not render inline; provide download links.',
'- LABEL TEMPLATES (/labeltemplate): Must use Dashback JSON schema `{ "objects": [ ... ] }`. Do NOT use ZPL.',
'- USER NAVIGATION: When the user says "show me the article", "open this order", or asks to navigate to a screen/entity, call set-user-route with the appropriate path from the Route Construction Matrix.',
'- Always Format output with markdown and emoji.',
'',
'USER PERSPECTIVE ROUTE CONSTRUCTION MATRIX:',
'| Destination | Path Pattern | Parameters / Construction | Example |',
'|:---|:---|:---|:---|',
'| **Article Search / Index** | `/artikel` | Query params: `?target={query}&filter={filter}&offset={n}` | `/artikel?target=kabel` |',
'| **Article Detail** | `/artikel/:sku` | Encode SKU: `/artikel/${encodeURIComponent(sku)}` | `/artikel/ART-10042` |',
'| **Warehouse / Stock** | `/artikel/lager` | Static | `/artikel/lager` |',
'| **Brands** | `/artikel/marken` | Static | `/artikel/marken` |',
'| **Label Templates** | `/artikel/vorlagen` | Static | `/artikel/vorlagen` |',
'| **Attributes & Features** | `/artikel/merkmale` | Static | `/artikel/merkmale` |',
'| **Category Tree Root** | `/kategorien` | Static | `/kategorien` |',
'| **Category Subtree** | `/kategorien/*` | Join encoded slugs: `/kategorien/${slug1}/${slug2}` | `/kategorien/elektronik/kabel` |',
'| **Delivery List** | `/lieferung` | Static | `/lieferung` |',
'| **New Delivery Draft** | `/lieferung/neu` | Static (receipt & draft line editor) | `/lieferung/neu` |',
'| **Delivery Detail** | `/lieferung/:deliveryId` | Delivery document ID: `/lieferung/${deliveryId}` | `/lieferung/42` |',
'| **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` |',
'| **Quote List (Angebote)** | `/auftrag` | Static | `/auftrag` |',
'| **New Quote** | `/auftrag/neu` | Static draft form | `/auftrag/neu` |',
'| **Quote Detail** | `/auftrag/:quoteId` | Quote document ID: `/auftrag/${quoteId}` | `/auftrag/1001` |',
'| **Order List (Bestellungen)**| `/auftrag/bestellung` | Static | `/auftrag/bestellung` |',
'| **New Order** | `/auftrag/bestellung/neu` | Static draft form | `/auftrag/bestellung/neu` |',
'| **Order Detail** | `/auftrag/bestellung/:orderId` | Order document ID: `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` |',
'| **Customer List** | `/kunde` | Static | `/kunde` |',
'| **New Customer** | `/kunde/neu` | Static form | `/kunde/neu` |',
'| **Customer Detail** | `/kunde/:entityId` | Customer entity ID: `/kunde/${entityId}` | `/kunde/312` |',
'| **Customer Groups** | `/kunde/gruppen` | Static (pricing & customer tiers) | `/kunde/gruppen` |',
'| **Article History** | `/infos` or `/infos/articlehistory` | Static | `/infos/articlehistory` |',
'| **Global History** | `/infos/globalhistory` | Static audit log | `/infos/globalhistory` |',
'| **Statistics** | `/infos/statistics` | Static KPIs & charts | `/infos/statistics` |',
...(routesDoc ? [
'',
'DASHBACK API ROUTES:',
@@ -874,6 +906,11 @@ export class ConversationManager {
createSearchArticlesTool({ googleAuthId, companyId: this.companyId }),
createEanChecksumTool(),
createNetGrossCalcTool(),
createSetUserRouteTool({
onRoute: (route) => {
this.broadcast('route', { route });
},
}),
],
});

View File

@@ -0,0 +1,46 @@
import { defineTool } from 'libllmorbiter';
/**
* Creates the set-user-route tool.
* Sends the user to a specific frontend route (e.g. when the user says "show me the article",
* "open quote 1001", "go to orders", etc.).
*
* @param {{ onRoute?: (route: string) => void }} [options]
*/
export function createSetUserRouteTool({ onRoute } = {}) {
return defineTool({
name: 'set-user-route',
description:
'Navigate the user to a frontend route. Use this when the user says "show me the article" or asks to see, open, or view a specific page or entity (e.g. an article detail page, orders, quotes, deliveries, or customer). Refer to the User Perspective Route Construction Matrix for valid route patterns.',
strict: false,
parameters: {
type: 'object',
additionalProperties: false,
required: ['route'],
properties: {
route: {
type: 'string',
description: 'Target frontend route path according to the Route Construction Matrix (e.g. "/artikel/ART-10042", "/kategorien/elektronik/kabel", "/auftrag/1001").',
},
},
},
invoke: async ({ route }) => {
const trimmedRoute = typeof route === 'string' ? route.trim() : '';
if (!trimmedRoute) {
throw new Error('route is required');
}
if (typeof onRoute === 'function') {
try {
onRoute(trimmedRoute);
} catch (err) {
console.error('set-user-route onRoute callback error:', err);
}
}
return {
success: true,
route: trimmedRoute,
message: `Navigated user to ${trimmedRoute}`,
};
},
});
}