This commit is contained in:
seb
2026-10-04 11:25:17 +02:00
parent e5ff4ef307
commit 43fa8ea105
24 changed files with 1016 additions and 214 deletions

View File

@@ -217,6 +217,20 @@
* format: date-time
* text:
* type: string
* CompanyHistoryEntry:
* type: object
* required: [id, date, text]
* properties:
* id:
* type: integer
* companyuserId:
* type: integer
* nullable: true
* date:
* type: string
* format: date-time
* text:
* type: string
* ArticleSupplierPrice:
* type: object
* required: [net, taxId]

View File

@@ -84,26 +84,11 @@ export function register(app, basePath = '') {
* content:
* application/json:
* schema:
* type: object
* required: [attributevalues]
* properties:
* attributevalues:
* type: array
* description: Array of attribute value IDs (numbers) or objects with id or attributeValueId
* items:
* oneOf:
* - type: integer
* description: Attribute value ID
* - type: object
* properties:
* id:
* type: integer
* description: Attribute value ID
* attributeValueId:
* type: integer
* description: Attribute value ID
* sort:
* type: integer
* type: array
* description: Array of attribute value IDs. Array order defines sort.
* items:
* type: integer
* example: [166]
* responses:
* 200:
* description: Updated article attribute values
@@ -147,13 +132,10 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: skuParsed.error });
}
const valuesInput = req.body?.attributevalues ?? req.body?.attributeValues ?? req.body?.attribute_values ?? req.body?.values ?? req.body;
if (!Array.isArray(valuesInput)) {
return res.status(400).json({ error: 'attributevalues array required' });
}
if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' });
try {
const attributevalues = await db.setByArticleSku(req.user.id, skuParsed.sku, valuesInput);
const attributevalues = await db.setByArticleSku(req.user.id, skuParsed.sku, req.body);
emitArticleSignal(req, skuParsed.sku);
res.json({ attributevalues });
} catch (error) {

View File

@@ -17,6 +17,7 @@ function attributeErrorStatus(error) {
if (error.message === 'Name required') return 400;
if (error.message === 'Invalid attribute name order') return 400;
if (error.message === 'Invalid id') return 400;
if (error.message === 'Attribute name in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500;
}
@@ -200,6 +201,42 @@ export function register(app, basePath = '') {
* responses:
* 200:
* description: Deleted successfully
* 400:
* description: Bad request or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Attribute name not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Attribute name in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.patch(`${basePath}/attributename/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
@@ -213,7 +250,9 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ attributename });
} catch (error) {
res.status(attributeErrorStatus(error)).json({ error: error.message });
const status = attributeErrorStatus(error);
if (status === 500) console.error('Update attribute name failed:', error);
res.status(status).json({ error: error.message });
}
});
@@ -229,7 +268,12 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ ok: true });
} catch (error) {
res.status(attributeErrorStatus(error)).json({ error: error.message });
const status = attributeErrorStatus(error);
const message = (error.message === 'Attribute name in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Attribute name in use'
: error.message;
if (status === 500) console.error('Delete attribute name failed:', error);
res.status(status).json({ error: message });
}
});
}

View File

@@ -16,6 +16,7 @@ function attributeValueErrorStatus(error) {
if (error.message === 'Value required') return 400;
if (error.message === 'Invalid attribute value order') return 400;
if (error.message === 'Invalid id' || error.message === 'Invalid attributeId') return 400;
if (error.message === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500;
}
@@ -216,6 +217,42 @@ export function register(app, basePath = '') {
* responses:
* 200:
* description: Deleted successfully
* 400:
* description: Bad request or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Attribute value not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Attribute value in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.patch(`${basePath}/attributevalue/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
@@ -229,7 +266,9 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ attributevalue });
} catch (error) {
res.status(attributeValueErrorStatus(error)).json({ error: error.message });
const status = attributeValueErrorStatus(error);
if (status === 500) console.error('Update attribute value failed:', error);
res.status(status).json({ error: error.message });
}
});
@@ -245,7 +284,12 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId });
res.json({ ok: true });
} catch (error) {
res.status(attributeValueErrorStatus(error)).json({ error: error.message });
const status = attributeValueErrorStatus(error);
const message = (error.message === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Attribute value in use'
: error.message;
if (status === 500) console.error('Delete attribute value failed:', error);
res.status(status).json({ error: message });
}
});
}

View File

@@ -400,6 +400,7 @@ export function register(app, basePath = '') {
.map((a) => ({
...a,
childCount: Number(a.childCount ?? 0),
isFather: Boolean(a.isFather ?? a.is_father),
}));
res.status(200).json({ articles });
} catch (error) {

View File

@@ -85,26 +85,11 @@ export function register(app, basePath = '') {
* content:
* application/json:
* schema:
* type: object
* required: [attributevalues]
* properties:
* attributevalues:
* type: array
* description: Array of attribute value IDs (numbers) or objects with id or attributeValueId
* items:
* oneOf:
* - type: integer
* description: Attribute value ID
* - type: object
* properties:
* id:
* type: integer
* description: Attribute value ID
* attributeValueId:
* type: integer
* description: Attribute value ID
* sort:
* type: integer
* type: array
* description: Array of attribute value IDs. Array order defines sort.
* items:
* type: integer
* example: [166]
* responses:
* 200:
* description: Updated category attribute values
@@ -148,13 +133,10 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error });
}
const valuesInput = req.body?.attributevalues ?? req.body?.attributeValues ?? req.body?.attribute_values ?? req.body?.values ?? req.body;
if (!Array.isArray(valuesInput)) {
return res.status(400).json({ error: 'attributevalues array required' });
}
if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' });
try {
const attributevalues = await db.setByCategoryId(req.user.id, idParsed.id, valuesInput);
const attributevalues = await db.setByCategoryId(req.user.id, idParsed.id, req.body);
emitCategorySignal(req);
res.json({ attributevalues });
} catch (error) {

View File

@@ -32,10 +32,21 @@ function parseCompanyId(id) {
return { companyId };
}
function requireSelectedCompany(req) {
const id = req.user?.selectedCompanyId;
if (id == null) {
const err = new Error('No company selected');
err.statusCode = 400;
throw err;
}
return id;
}
function companyErrorStatus(error) {
if (error.message === 'Company not found') return 404;
if (error.message === 'Company name not available') return 409;
if (error.message === 'Invalid tax rate') return 400;
if (error.message === 'Invalid tax rate' || error.message === 'Text required' || error.message === 'No company selected') return 400;
if (error.message === 'Not authorized') return 403;
return 500;
}
@@ -167,6 +178,170 @@ export function register(app, basePath = '') {
}
});
/**
* @openapi
* /company/history:
* get:
* tags: [Company]
* summary: Get company history
* description: |
* Returns `companyhistory` rows for the selected company, newest first (`date` DESC, `id` DESC).
* security:
* - cookieAuth: []
* x-markdown: company/history.md
* parameters:
* - in: query
* name: limit
* required: false
* schema:
* type: integer
* minimum: 1
* maximum: 1000
* description: Maximum number of entries to return (newest first). Omit for all entries.
* responses:
* 200:
* description: Company history (newest first)
* content:
* application/json:
* schema:
* type: object
* required: [history, truncated]
* properties:
* history:
* type: array
* items:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* truncated:
* type: boolean
* description: True when a limit was applied and the result set was exactly that size (more entries may exist).
* 400:
* description: Invalid limit or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* post:
* tags: [Company]
* summary: Add company history entry
* description: |
* Appends an entry to `companyhistory` for the selected company.
* security:
* - cookieAuth: []
* x-markdown: company/addHistory.md
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [text]
* properties:
* text:
* type: string
* description: History entry text or note
* responses:
* 201:
* description: History entry created
* content:
* application/json:
* schema:
* type: object
* required: [history]
* properties:
* history:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* 400:
* description: Text required or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.get(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const rawLimit = req.query.limit;
let limit = null;
if (rawLimit !== undefined) {
limit = parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
return res.status(400).json({ error: 'limit must be an integer between 1 and 1000' });
}
}
try {
const history = await db.getCompanyHistory(req.user.id, limit != null ? { limit } : undefined);
const truncated = limit != null && history.length === limit;
res.status(200).json({ history, truncated });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Get company history failed:', error);
res.status(status).json({ error: error.message });
}
});
app.post(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
if (!text) {
return res.status(400).json({ error: 'text is required' });
}
try {
const entry = await db.addCompanyHistory(req.user.id, { text });
res.status(201).json({ history: entry });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Add company history failed:', error);
res.status(status).json({ error: error.message });
}
});
/**
* @openapi
* /company/{id}:

View File

@@ -17,6 +17,7 @@ function propertyErrorStatus(error) {
if (error.message === 'Name required') return 400;
if (error.message === 'Invalid property name order') return 400;
if (error.message === 'Invalid id') return 400;
if (error.message === 'Property name in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500;
}
@@ -214,6 +215,42 @@ export function register(app, basePath = '') {
* responses:
* 200:
* description: Deleted successfully
* 400:
* description: Bad request or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Property name not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Property name in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.patch(`${basePath}/propertyname/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
@@ -227,7 +264,9 @@ export function register(app, basePath = '') {
emit('propertyname', { companyId: selected.companyId });
res.json({ propertyname });
} catch (error) {
res.status(propertyErrorStatus(error)).json({ error: error.message });
const status = propertyErrorStatus(error);
if (status === 500) console.error('Update property name failed:', error);
res.status(status).json({ error: error.message });
}
});
@@ -243,7 +282,12 @@ export function register(app, basePath = '') {
emit('propertyname', { companyId: selected.companyId });
res.json({ ok: true });
} catch (error) {
res.status(propertyErrorStatus(error)).json({ error: error.message });
const status = propertyErrorStatus(error);
const message = (error.message === 'Property name in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Property name in use'
: error.message;
if (status === 500) console.error('Delete property name failed:', error);
res.status(status).json({ error: message });
}
});
}

View File

@@ -24,3 +24,13 @@ export async function deleteCompany(companyId) {
const store = await getStore();
await store.company.delete(companyId);
}
export async function getCompanyHistory(googleAuthId, opts) {
const store = await getStore();
return store.company.getHistory(googleAuthId, opts);
}
export async function addCompanyHistory(googleAuthId, data) {
const store = await getStore();
return store.company.addHistory(googleAuthId, data);
}

View File

@@ -11,6 +11,7 @@ import { createDashbackRequestTool } from '../tools/dashback-request.js';
import { createAskUserTool } from '../tools/ask-user.js';
import { createJavascriptExecuteTool } from '../tools/javascript.js';
import { createResetVfsTool } from '../tools/reset-vfs.js';
import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js';
const KEEP_ALIVE_MS = 20_000;
@@ -38,6 +39,7 @@ function buildSystemPrompt() {
'2. ask_user: Ask clarifying questions or request decisions from the user if their intent is ambiguous or requires choices. When you call ask_user, execution pauses immediately and waits for the user to provide their response.',
'3. javascript_execute: Run JavaScript snippets in a sandbox to compute, aggregate, transform, or analyze data. Assign module.exports to return a value. With allow_network: true, snippets can directly call Dashback API routes using fetch() (e.g. await fetch("/article/2")) and search images using search_for_images(query). A sandboxed virtual filesystem (fs / vfs) is available to store and retrieve artifacts across runs.',
'4. reset_vfs: Delete all files, images, and artifacts stored in the virtual filesystem (VFS) to free up storage or clean up.',
'5. apisuggestionsorissues: Report missing API endpoints, bugs, or issues to be triaged. Automatically writes an entry to the company history table for review.',
'',
'OPERATING GUIDELINES:',
'- Refer to the DASHBACK API ROUTES documentation below to find appropriate endpoints, parameters, and HTTP methods.',
@@ -404,6 +406,7 @@ export class ConversationManager {
createAskUserTool(),
createJavascriptExecuteTool({ googleAuthId, companyId: this.companyId }),
createResetVfsTool({ googleAuthId, companyId: this.companyId }),
createApiSuggestionsOrIssuesTool({ googleAuthId, companyId: this.companyId }),
],
});

View File

@@ -0,0 +1,61 @@
import { defineTool } from 'libllmorbiter';
import * as db from '../db/company.js';
/**
* Creates the apisuggestionsorissues tool allowing the agent to report
* missing API endpoints, bugs, or general issues into the company history table.
* These reports are then visible to triage agents or administrators.
*
* @param {{ googleAuthId: number, companyId?: number }} ctx
*/
export function createApiSuggestionsOrIssuesTool({ googleAuthId, companyId }) {
return defineTool({
name: 'apisuggestionsorissues',
description:
'Report a missing API endpoint, feature request, bug, or general issue. ' +
'The report is recorded in the company history table for review by the triage agent. ' +
'Provide a clear description of the missing functionality or issue.',
strict: false,
parameters: {
type: 'object',
additionalProperties: false,
required: ['text'],
properties: {
text: {
type: 'string',
description: 'Detailed description of the missing API, bug, or issue.',
},
type: {
type: 'string',
enum: ['missing_api', 'bug', 'suggestion', 'issue', 'other'],
description: 'Category/type of the report.',
},
title: {
type: 'string',
description: 'Optional short summary or title.',
},
},
},
invoke: async ({ text, type, title }) => {
const trimmedText = typeof text === 'string' ? text.trim() : '';
if (!trimmedText) {
throw new Error('text is required');
}
const prefixParts = [];
if (type) prefixParts.push(`[${type.toUpperCase()}]`);
if (title) prefixParts.push(title.trim());
const formatted = prefixParts.length > 0
? `${prefixParts.join(' ')}\n${trimmedText}`
: trimmedText;
const entry = await db.addCompanyHistory(googleAuthId, { text: formatted });
return {
success: true,
message: 'Recorded in company history for triage agent review',
entry,
};
},
});
}