adminizer 5.0.0-build.8 → 5.1.0-build.24
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/assets/abnfDiagram-VCTEODGH-DFY3Pztx.js +1 -0
- package/assets/add-31tS-g4V.js +1 -0
- package/assets/add-group-Cf3zT7WE.js +1 -0
- package/assets/add-user-C62wOna-.js +1 -0
- package/assets/ai-assistant/agent.es.js +28464 -0
- package/assets/app-DPmI-MvN.css +1 -0
- package/assets/app-vdHbjOQX.js +138 -0
- package/assets/arc-DP4o1zz3.js +1 -0
- package/assets/architectureDiagram-5GKGNRK7-BjXW5Zu0.js +36 -0
- package/assets/asmcrypto-DZeQH0LC.js +1 -0
- package/assets/blockDiagram-I7D4REHJ-BUPnDwN0.js +129 -0
- package/assets/c4Diagram-7LVT6UL2-Cmb_dAhd.js +38 -0
- package/assets/{catalog-BEvJZYJP.js → catalog-C-y5rMaG.js} +3 -3
- package/assets/channel-CkJRUsq_.js +1 -0
- package/assets/chunk-2Q5K7J3B-CmS-dWwQ.js +1 -0
- package/assets/chunk-5VM5RSS4-Cqs2Ow64.js +15 -0
- package/assets/chunk-F27PBJKO-BoMCpJA1.js +1 -0
- package/assets/chunk-IMKFNOWR-DlDcPk1x.js +231 -0
- package/assets/chunk-JWPE2WC7-DzL_SU8s.js +1 -0
- package/assets/chunk-POPQ4Y6H-Cwq2-Vb8.js +1 -0
- package/assets/chunk-SVP7TREG-BfRHY5FU.js +88 -0
- package/assets/chunk-TICWLB2K-CRP7rHkW.js +206 -0
- package/assets/chunk-XXDRQBXY-Bg2jg-98.js +1 -0
- package/assets/classDiagram-ZZMXUADV-D4EF3HGj.js +1 -0
- package/assets/classDiagram-v2-VYDZK3BY-D4EF3HGj.js +1 -0
- package/assets/controls/ckeditor.es.js +9382 -9320
- package/assets/controls/handsontable.es.js +11971 -11938
- package/assets/controls/jsoneditor.es.js +10442 -10233
- package/assets/controls/toast-ui.es.js +1308 -1279
- package/assets/cose-bilkent-JH36ORCC-CCJ0SGyO.js +1 -0
- package/assets/cynefin-OW5HDTMX-B2Jx5fw3.js +166 -0
- package/assets/cynefinDiagram-5FMLGOSQ-DuK0Lb62.js +62 -0
- package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
- package/assets/dagre-GXQ25YYZ-DnOYexET.js +4 -0
- package/assets/dashboard-X8k4X6Sw.js +3 -0
- package/assets/debounce-B3mXI_sx.js +1 -0
- package/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagram-S7CK7UJ4-DDgPBHsK.js +30 -0
- package/assets/diagram-UQ7AKVKN-qlL0GZs8.js +41 -0
- package/assets/diagram-VSXAHHWV-CQbiU1_c.js +3 -0
- package/assets/diagram-VX7I27RA-DFDI9_au.js +24 -0
- package/assets/diagram-Z3DM3KII-B5C9dtmJ.js +24 -0
- package/assets/docs-BiZOD7PP.js +1 -0
- package/assets/ebnfDiagram-PWID7BFC-CLMUsXEI.js +1 -0
- package/assets/erDiagram-RLTQ6QDP-ax9Xol5-.js +99 -0
- package/assets/flowDiagram-HODETNUW-CvaH7l3S.js +168 -0
- package/assets/ganttDiagram-EL5Y4UJY-Cga5U19j.js +292 -0
- package/assets/gitGraphDiagram-WWUBYQGX-B6o1T3-d.js +106 -0
- package/assets/history-CVBJUNSQ.js +1 -0
- package/assets/infoDiagram-27XIBGKW-BpWxgBCE.js +2 -0
- package/assets/init-Gi6I4Gst.js +1 -0
- package/assets/init-user-xSvnFTF0.js +1 -0
- package/assets/isObject-DL-BuaBl.js +1 -0
- package/assets/ishikawaDiagram-5VMMS53U-zE83db4H.js +70 -0
- package/assets/journeyDiagram-3NMN7TZE-DZNVFqrX.js +139 -0
- package/assets/kanban-definition-UXKFOSKX-CqxNgk27.js +89 -0
- package/assets/katex-HP8lGamR.js +257 -0
- package/assets/layout-CN3kt_jt.js +1 -0
- package/assets/linear-nvkaQ6BA.js +1 -0
- package/assets/{list-DhK0McVn.js → list-CrIU2eZz.js} +2 -2
- package/assets/login-CX9xVjCn.js +1 -0
- package/assets/manifest.json +834 -58
- package/assets/media-manager-BfYftLIH.js +6 -0
- package/assets/mermaid.core-C0V1NaVJ.js +310 -0
- package/assets/mindmap-definition-YA3MSWOX-Bh7_d9f1.js +96 -0
- package/assets/module-DPEvnad3.js +1 -0
- package/assets/notification-Dqpt5AIZ.js +20 -0
- package/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/pegDiagram-XKGWAZYB-DMnjm6Z1.js +1 -0
- package/assets/pieDiagram-E7YTZNPT-D-RWCgYb.js +39 -0
- package/assets/quadrantDiagram-AXDQQJYC-BNBGP7RM.js +7 -0
- package/assets/railroadDiagram-O6MQD6OU-BIjVPT01.js +1 -0
- package/assets/register-CF4KQ97e.js +1 -0
- package/assets/requirementDiagram-BXWQKSXE-VPrtGdjM.js +84 -0
- package/assets/ru-CFJ4fysR.js +1 -0
- package/assets/sankeyDiagram-P5KCCOFB-ct9mLF3U.js +40 -0
- package/assets/sequenceDiagram-WJ2MYXX4-TC06Mbmw.js +162 -0
- package/assets/sizeCapture-INFHLROL-BMMwriCf.js +1 -0
- package/assets/stateDiagram-D77RDMKH-DtwYRPCX.js +1 -0
- package/assets/stateDiagram-v2-MP3YSRHH-C1yB0O5U.js +1 -0
- package/assets/swimlanes-42K2YHIH-DE3hRbbr.js +1 -0
- package/assets/swimlanesDiagram-VR7AAH4N-Ce4l1sQD.js +8 -0
- package/assets/timeline-definition-24CTP7MA-N7s_hfjj.js +120 -0
- package/assets/toNumber-DZb-r9sy.js +1 -0
- package/assets/use-appearance-DYTIKpT2.js +1 -0
- package/assets/user-filters-list-fR0YNJ3l.js +1 -0
- package/assets/vennDiagram-4TSXK5OY-Dj5bFQkR.js +34 -0
- package/assets/wardleyDiagram-VM6X3IG4-83ax25Z6.js +78 -0
- package/assets/welcome-v11MTsRK.js +1 -0
- package/assets/with-app-layout-CH6RIib1.js +31 -0
- package/assets/xychartDiagram-S5SC5T6Z-CW9Br5Xp.js +7 -0
- package/controllers/access-rights/permissionOptions.d.ts +2 -0
- package/controllers/access-rights/permissionOptions.js +23 -0
- package/controllers/add.js +2 -2
- package/controllers/addGroup.js +15 -6
- package/controllers/ai/AiAgentController.d.ts +66 -0
- package/controllers/ai/AiAgentController.js +435 -0
- package/controllers/ai/AiAssistantController.d.ts +0 -4
- package/controllers/ai/AiAssistantController.js +4 -78
- package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
- package/controllers/docs/index.d.ts +22 -0
- package/controllers/docs/index.js +126 -0
- package/controllers/edit.js +2 -2
- package/controllers/editGroup.js +15 -6
- package/controllers/filter-fields/savedFilters.js +1 -1
- package/controllers/history-actions/HistoryController.js +7 -7
- package/controllers/inlineUpdate.js +10 -3
- package/controllers/list.js +1 -1
- package/controllers/login.js +1 -1
- package/controllers/notifications/NotificationController.js +14 -8
- package/controllers/view.js +3 -3
- package/controllers/widgets/filterInfo.js +1 -1
- package/helpers/accessRightsHelper.d.ts +30 -3
- package/helpers/accessRightsHelper.js +140 -12
- package/helpers/assetVersionHelper.d.ts +14 -0
- package/helpers/assetVersionHelper.js +42 -0
- package/helpers/configHelper.js +19 -18
- package/helpers/controllerHelper.d.ts +1 -0
- package/helpers/controllerHelper.js +25 -15
- package/helpers/fieldsHelper.d.ts +2 -1
- package/helpers/fieldsHelper.js +11 -5
- package/helpers/inertiaActionsHelper.d.ts +1 -1
- package/helpers/inertiaActionsHelper.js +4 -5
- package/helpers/inertiaAddHelper.d.ts +1 -1
- package/helpers/inertiaAddHelper.js +7 -6
- package/helpers/inertiaGroupHelper.js +10 -2
- package/helpers/inertiaListHelper.d.ts +1 -1
- package/helpers/inertiaListHelper.js +7 -8
- package/helpers/inertiaMenuHelper.d.ts +7 -2
- package/helpers/inertiaMenuHelper.js +12 -27
- package/helpers/menuHelper.d.ts +11 -1
- package/helpers/menuHelper.js +12 -0
- package/helpers/modelResourceHelper.d.ts +9 -0
- package/helpers/modelResourceHelper.js +43 -0
- package/helpers/navigationAccessHelper.d.ts +13 -0
- package/helpers/navigationAccessHelper.js +45 -0
- package/index.d.ts +11 -0
- package/index.js +11 -0
- package/interfaces/adminpanelConfig.d.ts +160 -2
- package/interfaces/queryCriteria.d.ts +13 -0
- package/interfaces/types.d.ts +169 -0
- package/lib/Adminizer.d.ts +8 -0
- package/lib/Adminizer.js +29 -2
- package/lib/DataAccessor.d.ts +100 -3
- package/lib/DataAccessor.js +454 -245
- package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
- package/lib/access-graph/AccessGraphResolver.js +605 -0
- package/lib/access-graph/RecordAccessCache.d.ts +23 -0
- package/lib/access-graph/RecordAccessCache.js +30 -0
- package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
- package/lib/access-graph/RecordAccessResolver.js +295 -0
- package/lib/access-graph/shared.d.ts +60 -0
- package/lib/access-graph/shared.js +121 -0
- package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
- package/lib/admin-links/AdminLinkHandler.js +183 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
- package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
- package/lib/ai-assistant/AbstractAiModelService.js +154 -1
- package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
- package/lib/ai-assistant/AiAgentRunManager.js +109 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
- package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
- package/lib/ai-assistant/AiAssistantHandler.js +4 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
- package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
- package/lib/ai-assistant/builtinAgentSkills.js +259 -0
- package/lib/ai-assistant/jsonSafe.d.ts +13 -0
- package/lib/ai-assistant/jsonSafe.js +81 -0
- package/lib/app-manager/AdminizerApp.d.ts +31 -2
- package/lib/app-manager/AppManager.js +43 -42
- package/lib/app-manager/ConfigLayerHandler.js +4 -0
- package/lib/docs/AbstractDocumentation.d.ts +100 -0
- package/lib/docs/AbstractDocumentation.js +108 -0
- package/lib/docs/DocumentationHandler.d.ts +100 -0
- package/lib/docs/DocumentationHandler.js +198 -0
- package/lib/docs/FileDocumentation.d.ts +47 -0
- package/lib/docs/FileDocumentation.js +189 -0
- package/lib/docs/docsAgentSkills.d.ts +10 -0
- package/lib/docs/docsAgentSkills.js +93 -0
- package/lib/filters/FilterService.js +4 -3
- package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
- package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
- package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
- package/lib/model/AbstractModel.d.ts +48 -0
- package/lib/model/AbstractModel.js +14 -4
- package/lib/model/ModelHandler.d.ts +50 -2
- package/lib/model/ModelHandler.js +117 -8
- package/lib/model/adapter/sequelize.d.ts +43 -3
- package/lib/model/adapter/sequelize.js +317 -138
- package/lib/model/adapter/typeorm.d.ts +16 -0
- package/lib/model/adapter/typeorm.js +136 -9
- package/lib/notifications/GeneralNotificationService.js +6 -5
- package/lib/notifications/SystemNotificationService.js +1 -1
- package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
- package/lib/ui-i18n/uiTranslationKeys.js +42 -1
- package/lib/widgets/widgetHandler.js +10 -6
- package/models/Group.d.ts +2 -1
- package/package.json +35 -23
- package/policies/authPolicies.js +6 -6
- package/policies/permissionResolvers.d.ts +1 -0
- package/policies/permissionResolvers.js +3 -0
- package/system/Router.js +21 -3
- package/system/bindAccessRights.js +13 -3
- package/system/bindDocs.d.ts +13 -0
- package/system/bindDocs.js +77 -0
- package/system/bindInertia.js +30 -7
- package/system/buildInternalModelAccess.d.ts +8 -0
- package/system/buildInternalModelAccess.js +36 -4
- package/system/defaults.js +7 -1
- package/system/validateSystemModels.js +29 -9
- package/assets/add-DED_wpga.js +0 -1
- package/assets/add-group-CL80RdYb.js +0 -1
- package/assets/add-user-BRmwvQB8.js +0 -1
- package/assets/app-DNXiApCk.css +0 -1
- package/assets/app-layout-86yxiZOy.js +0 -1
- package/assets/app.js +0 -138
- package/assets/dashboard-ZP6K2s_8.js +0 -3
- package/assets/debounce-BMmXVQ06.js +0 -1
- package/assets/history-M61Bcv02.js +0 -1
- package/assets/init-user-8aUohVDq.js +0 -1
- package/assets/login-CO4laf6m.js +0 -1
- package/assets/media-manager-BDbpSSjH.js +0 -6
- package/assets/module-DfBneaUl.js +0 -1
- package/assets/notification-CVnQf3ef.js +0 -20
- package/assets/register-DBYIMXGQ.js +0 -1
- package/assets/ru-rYKdQbWF.js +0 -1
- package/assets/use-appearance-_EbbjHTh.js +0 -1
- package/assets/user-filters-list-DXhGLFTn.js +0 -1
- package/assets/welcome-CLxvXkcg.js +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/** Segments no template may point at: opening one mutates data. */
|
|
2
|
+
const DESTRUCTIVE_PATH = /\/model\/[^/]+\/remove(\/|$)/i;
|
|
3
|
+
/** Server-side registry of standalone admin pages contributed by applications. */
|
|
4
|
+
export class AdminLinkHandler {
|
|
5
|
+
adminizer;
|
|
6
|
+
links = new Map();
|
|
7
|
+
templates = new Map();
|
|
8
|
+
constructor(adminizer) {
|
|
9
|
+
this.adminizer = adminizer;
|
|
10
|
+
}
|
|
11
|
+
add(link, owner = 'host') {
|
|
12
|
+
const type = link.type.trim();
|
|
13
|
+
const name = link.name.trim();
|
|
14
|
+
const href = link.link.trim();
|
|
15
|
+
if (!type || !name || !href)
|
|
16
|
+
throw new Error('Admin link requires type, name and link');
|
|
17
|
+
if (!href.startsWith('/'))
|
|
18
|
+
throw new Error(`Admin link "${name}" must use an absolute admin path`);
|
|
19
|
+
const id = `${this.slug(type)}:${this.slug(name)}`;
|
|
20
|
+
if (this.links.has(id))
|
|
21
|
+
throw new Error(`Admin link "${id}" is already registered`);
|
|
22
|
+
this.links.set(id, {
|
|
23
|
+
...link, id, type, name, link: href, owner,
|
|
24
|
+
title: link.title?.trim() || name,
|
|
25
|
+
section: link.section?.trim() || undefined,
|
|
26
|
+
});
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
remove(id, owner) {
|
|
30
|
+
const link = this.links.get(id);
|
|
31
|
+
if (!link || (owner && link.owner !== owner))
|
|
32
|
+
return false;
|
|
33
|
+
return this.links.delete(id);
|
|
34
|
+
}
|
|
35
|
+
async list(user, type) {
|
|
36
|
+
const wanted = type ? this.slug(type) : null;
|
|
37
|
+
const accessible = [];
|
|
38
|
+
for (const link of this.links.values()) {
|
|
39
|
+
if (wanted && this.slug(link.type) !== wanted)
|
|
40
|
+
continue;
|
|
41
|
+
if (link.accessRightsToken && !await this.adminizer.accessRightsHelper.hasPermission(link.accessRightsToken, user))
|
|
42
|
+
continue;
|
|
43
|
+
accessible.push(link);
|
|
44
|
+
}
|
|
45
|
+
return accessible;
|
|
46
|
+
}
|
|
47
|
+
async resolve(user, type, name) {
|
|
48
|
+
const wanted = this.slug(name);
|
|
49
|
+
return (await this.list(user, type))
|
|
50
|
+
.find((link) => this.slug(link.name) === wanted || this.slug(link.title) === wanted);
|
|
51
|
+
}
|
|
52
|
+
addTemplate(template, owner = 'host') {
|
|
53
|
+
const id = template.id.trim();
|
|
54
|
+
const title = template.title.trim();
|
|
55
|
+
const path = template.template.trim();
|
|
56
|
+
if (!id || !title || !path)
|
|
57
|
+
throw new Error('Admin link template requires id, title and template');
|
|
58
|
+
if (!path.startsWith('/'))
|
|
59
|
+
throw new Error(`Admin link template "${id}" must use an absolute admin path`);
|
|
60
|
+
if (this.templates.has(id))
|
|
61
|
+
throw new Error(`Admin link template "${id}" is already registered`);
|
|
62
|
+
this.templates.set(id, {
|
|
63
|
+
...template, id, title, template: path, owner,
|
|
64
|
+
description: template.description?.trim() || undefined,
|
|
65
|
+
section: template.section?.trim() || undefined,
|
|
66
|
+
params: this.describeParams(path, template.params),
|
|
67
|
+
});
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
70
|
+
removeTemplate(id, owner) {
|
|
71
|
+
const template = this.templates.get(id);
|
|
72
|
+
if (!template || (owner && template.owner !== owner))
|
|
73
|
+
return false;
|
|
74
|
+
return this.templates.delete(id);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Every parametrized page this user may open: model record pages, catalog
|
|
78
|
+
* items, templates registered by apps and any navigation link that carries
|
|
79
|
+
* placeholders of its own.
|
|
80
|
+
*/
|
|
81
|
+
async listTemplates(user) {
|
|
82
|
+
const templates = [];
|
|
83
|
+
const push = async (template) => {
|
|
84
|
+
if (DESTRUCTIVE_PATH.test(template.template))
|
|
85
|
+
return;
|
|
86
|
+
if (template.accessRightsToken && !await this.adminizer.accessRightsHelper.hasPermission(template.accessRightsToken, user))
|
|
87
|
+
return;
|
|
88
|
+
if (templates.some((existing) => existing.template === template.template || existing.id === template.id))
|
|
89
|
+
return;
|
|
90
|
+
templates.push(template);
|
|
91
|
+
};
|
|
92
|
+
for (const template of this.builtinTemplates(user))
|
|
93
|
+
await push(template);
|
|
94
|
+
for (const template of this.templates.values())
|
|
95
|
+
await push(template);
|
|
96
|
+
return templates;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Turns a template id (or its title) plus placeholder values into a real
|
|
100
|
+
* admin URL, rejecting templates this user may not open.
|
|
101
|
+
*/
|
|
102
|
+
async resolveTemplate(user, id, params = {}) {
|
|
103
|
+
const wanted = this.slug(id);
|
|
104
|
+
const available = await this.listTemplates(user);
|
|
105
|
+
const template = available.find((candidate) => this.slug(candidate.id) === wanted)
|
|
106
|
+
?? available.find((candidate) => this.slug(candidate.title) === wanted || this.slug(candidate.template) === wanted);
|
|
107
|
+
if (!template)
|
|
108
|
+
throw new Error(`Admin link template "${id}" is not available`);
|
|
109
|
+
return this.buildHref(template, params);
|
|
110
|
+
}
|
|
111
|
+
/** Fills the placeholders of a template with url-encoded values. */
|
|
112
|
+
buildHref(template, params = {}) {
|
|
113
|
+
return template.template.replace(/:([A-Za-z0-9_]+)/g, (_match, name) => {
|
|
114
|
+
const value = params[name];
|
|
115
|
+
if (value === undefined || value === null || value === '') {
|
|
116
|
+
throw new Error(`Admin link template "${template.id}" requires the "${name}" parameter`);
|
|
117
|
+
}
|
|
118
|
+
return encodeURIComponent(String(value));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** True for paths that change data and therefore must never be navigated to. */
|
|
122
|
+
isDestructivePath(path) {
|
|
123
|
+
return DESTRUCTIVE_PATH.test(path);
|
|
124
|
+
}
|
|
125
|
+
/** Templates derived from the current configuration, catalogs and navigation. */
|
|
126
|
+
builtinTemplates(user) {
|
|
127
|
+
const prefix = (this.adminizer.config.routePrefix || '').replace(/\/+$/, '');
|
|
128
|
+
const templates = [];
|
|
129
|
+
const builtin = (id, title, path, accessRightsToken, description, section, params = []) => {
|
|
130
|
+
templates.push({
|
|
131
|
+
id, title, template: path, accessRightsToken, description, section, owner: 'host',
|
|
132
|
+
params: this.describeParams(path, params),
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const recordId = [
|
|
136
|
+
{ name: 'id', description: 'Identifier of the record, as returned when reading its data.' },
|
|
137
|
+
];
|
|
138
|
+
for (const [model, config] of Object.entries(this.adminizer.config.models ?? {})) {
|
|
139
|
+
const title = typeof config === 'object' && config?.title ? config.title : model;
|
|
140
|
+
const section = typeof config === 'object' ? config?.navbar?.section : undefined;
|
|
141
|
+
builtin(`model-${model}-edit`, `${title}: open record`, `${prefix}/model/${model}/edit/:id`, `update-${model}-model`, `Open the edit page of a single ${title} record by its id.`, section, recordId);
|
|
142
|
+
builtin(`model-${model}-add`, `${title}: create record`, `${prefix}/model/${model}/add`, `create-${model}-model`, `Open the create form of ${title}.`, section);
|
|
143
|
+
}
|
|
144
|
+
for (const catalog of this.adminizer.catalogHandler.getAll()) {
|
|
145
|
+
if (!catalog?.slug)
|
|
146
|
+
continue;
|
|
147
|
+
builtin(`catalog-${catalog.slug}-item`, `${catalog.name || catalog.slug}: open item`, `${prefix}/catalog/${catalog.slug}/:id`, `catalog-${catalog.slug}`, `Open a single item of the ${catalog.name || catalog.slug} catalog by its id.`, undefined, recordId);
|
|
148
|
+
}
|
|
149
|
+
// Navigation links may carry placeholders themselves (app pages, custom
|
|
150
|
+
// tools). Such a link cannot be opened as-is, so it is a template.
|
|
151
|
+
const fromNavigation = (item, section) => {
|
|
152
|
+
const path = typeof item?.link === 'string' ? item.link : '';
|
|
153
|
+
if (path.startsWith('/') && /:[A-Za-z0-9_]+/.test(path) && item.title) {
|
|
154
|
+
templates.push({
|
|
155
|
+
id: `link-${this.slug(String(item.id || item.title))}`,
|
|
156
|
+
title: String(item.title),
|
|
157
|
+
template: path,
|
|
158
|
+
section: item.section || section,
|
|
159
|
+
accessRightsToken: item.accessRightsToken || undefined,
|
|
160
|
+
owner: 'host',
|
|
161
|
+
params: this.describeParams(path),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const child of item?.actions ?? item?.subItems ?? [])
|
|
165
|
+
fromNavigation(child, item?.section || section);
|
|
166
|
+
};
|
|
167
|
+
for (const item of this.adminizer.menuHelper.getMenuItems(user))
|
|
168
|
+
fromNavigation(item, item.section);
|
|
169
|
+
for (const link of this.links.values())
|
|
170
|
+
fromNavigation(link, link.section);
|
|
171
|
+
return templates;
|
|
172
|
+
}
|
|
173
|
+
describeParams(path, described = []) {
|
|
174
|
+
const names = [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]);
|
|
175
|
+
return [...new Set(names)].map((name) => ({
|
|
176
|
+
name,
|
|
177
|
+
description: described.find((param) => param.name === name)?.description,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
slug(value) {
|
|
181
|
+
return String(value).trim().toLowerCase().replace(/[\s_-]+/g, '-');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Adminizer } from '../Adminizer.js';
|
|
2
|
+
import type { AbstractAiModelService } from './AbstractAiModelService.js';
|
|
3
|
+
import type { User } from '../../models/User.js';
|
|
4
|
+
export interface AiConversation {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
messages: Array<Record<string, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Storage contract for an agent's dialogs. Applications can extend this class
|
|
13
|
+
* to persist conversations in their own database instead of process memory.
|
|
14
|
+
*/
|
|
15
|
+
export declare abstract class AbstractAiConversationHistoryService {
|
|
16
|
+
protected agent: AbstractAiModelService;
|
|
17
|
+
protected adminizer: Adminizer;
|
|
18
|
+
initialize(agent: AbstractAiModelService, adminizer: Adminizer): void;
|
|
19
|
+
abstract list(user: User): AiConversation[];
|
|
20
|
+
abstract getActive(user: User): AiConversation;
|
|
21
|
+
abstract create(user: User, title?: string): AiConversation;
|
|
22
|
+
abstract select(user: User, conversationId: string): AiConversation | undefined;
|
|
23
|
+
abstract remove(user: User, conversationId: string): boolean;
|
|
24
|
+
abstract saveActive(user: User, messages: Array<Record<string, unknown>>, title?: string): AiConversation;
|
|
25
|
+
}
|
|
26
|
+
/** Default storage used when an agent does not provide a database-backed one. */
|
|
27
|
+
export declare class InMemoryAiConversationHistoryService extends AbstractAiConversationHistoryService {
|
|
28
|
+
private readonly conversations;
|
|
29
|
+
private readonly activeConversationIds;
|
|
30
|
+
list(user: User): AiConversation[];
|
|
31
|
+
getActive(user: User): AiConversation;
|
|
32
|
+
create(user: User, title?: string): AiConversation;
|
|
33
|
+
select(user: User, conversationId: string): AiConversation | undefined;
|
|
34
|
+
remove(user: User, conversationId: string): boolean;
|
|
35
|
+
saveActive(user: User, messages: Array<Record<string, unknown>>, title?: string): AiConversation;
|
|
36
|
+
private getConversations;
|
|
37
|
+
private getKey;
|
|
38
|
+
private findFirstUserText;
|
|
39
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage contract for an agent's dialogs. Applications can extend this class
|
|
3
|
+
* to persist conversations in their own database instead of process memory.
|
|
4
|
+
*/
|
|
5
|
+
export class AbstractAiConversationHistoryService {
|
|
6
|
+
agent;
|
|
7
|
+
adminizer;
|
|
8
|
+
initialize(agent, adminizer) {
|
|
9
|
+
this.agent = agent;
|
|
10
|
+
this.adminizer = adminizer;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Default storage used when an agent does not provide a database-backed one. */
|
|
14
|
+
export class InMemoryAiConversationHistoryService extends AbstractAiConversationHistoryService {
|
|
15
|
+
conversations = new Map();
|
|
16
|
+
activeConversationIds = new Map();
|
|
17
|
+
list(user) {
|
|
18
|
+
return [...this.getConversations(user)]
|
|
19
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
20
|
+
}
|
|
21
|
+
getActive(user) {
|
|
22
|
+
const key = this.getKey(user);
|
|
23
|
+
const conversations = this.getConversations(user);
|
|
24
|
+
const activeId = this.activeConversationIds.get(key);
|
|
25
|
+
const active = conversations.find((conversation) => conversation.id === activeId);
|
|
26
|
+
if (active)
|
|
27
|
+
return active;
|
|
28
|
+
const conversation = this.create(user);
|
|
29
|
+
this.activeConversationIds.set(key, conversation.id);
|
|
30
|
+
return conversation;
|
|
31
|
+
}
|
|
32
|
+
create(user, title = 'New conversation') {
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
const conversation = {
|
|
35
|
+
id: `conversation-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
36
|
+
title,
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
messages: [],
|
|
40
|
+
};
|
|
41
|
+
this.getConversations(user).push(conversation);
|
|
42
|
+
this.activeConversationIds.set(this.getKey(user), conversation.id);
|
|
43
|
+
return conversation;
|
|
44
|
+
}
|
|
45
|
+
select(user, conversationId) {
|
|
46
|
+
const conversation = this.getConversations(user).find((item) => item.id === conversationId);
|
|
47
|
+
if (conversation)
|
|
48
|
+
this.activeConversationIds.set(this.getKey(user), conversation.id);
|
|
49
|
+
return conversation;
|
|
50
|
+
}
|
|
51
|
+
remove(user, conversationId) {
|
|
52
|
+
const conversations = this.getConversations(user);
|
|
53
|
+
const index = conversations.findIndex((conversation) => conversation.id === conversationId);
|
|
54
|
+
if (index < 0)
|
|
55
|
+
return false;
|
|
56
|
+
conversations.splice(index, 1);
|
|
57
|
+
if (this.activeConversationIds.get(this.getKey(user)) === conversationId) {
|
|
58
|
+
this.activeConversationIds.delete(this.getKey(user));
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
saveActive(user, messages, title) {
|
|
63
|
+
const conversation = this.getActive(user);
|
|
64
|
+
conversation.messages = messages;
|
|
65
|
+
conversation.updatedAt = new Date().toISOString();
|
|
66
|
+
if (title)
|
|
67
|
+
conversation.title = title;
|
|
68
|
+
else if (conversation.title === 'New conversation') {
|
|
69
|
+
const firstText = this.findFirstUserText(messages);
|
|
70
|
+
if (firstText)
|
|
71
|
+
conversation.title = firstText.slice(0, 80);
|
|
72
|
+
}
|
|
73
|
+
return conversation;
|
|
74
|
+
}
|
|
75
|
+
getConversations(user) {
|
|
76
|
+
const key = this.getKey(user);
|
|
77
|
+
let conversations = this.conversations.get(key);
|
|
78
|
+
if (!conversations) {
|
|
79
|
+
conversations = [];
|
|
80
|
+
this.conversations.set(key, conversations);
|
|
81
|
+
}
|
|
82
|
+
return conversations;
|
|
83
|
+
}
|
|
84
|
+
getKey(user) {
|
|
85
|
+
return `${user.id}:${this.agent.id}`;
|
|
86
|
+
}
|
|
87
|
+
findFirstUserText(messages) {
|
|
88
|
+
const message = messages.find((item) => item.role === 'user');
|
|
89
|
+
const content = message?.content;
|
|
90
|
+
if (typeof content === 'string')
|
|
91
|
+
return content.trim();
|
|
92
|
+
if (Array.isArray(content)) {
|
|
93
|
+
const text = content.find((part) => part && typeof part === 'object' && part.type === 'text');
|
|
94
|
+
return typeof text?.text === 'string' ? text.text.trim() : undefined;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,14 +1,151 @@
|
|
|
1
|
-
import { AiAssistantMessage, AiAssistantModelInfo } from '../../interfaces/types.js';
|
|
1
|
+
import { AiAgentConnectionStatus, AiAgentLimits, AiAgentModelOption, AiAgentPublish, AiAgentSavedFile, AiAgentSessionMeta, AiAgentUiHints, AiAgentUiSchema, AiAgentUploadedFile, AiAssistantMessage, AiAssistantModelInfo } from '../../interfaces/types.js';
|
|
2
2
|
import { User } from '../../models/User.js';
|
|
3
|
+
import type { Adminizer } from '../Adminizer.js';
|
|
4
|
+
import { AbstractAiConversationHistoryService } from './AbstractAiConversationHistoryService.js';
|
|
5
|
+
import type { AiAssistantAdminLink, AiAssistantNavigationTarget, AiAssistantUiMethod } from './AiAssistantUiMethodHandler.js';
|
|
6
|
+
import type { AiAssistantAgentSkillDescriptor, AiAssistantSkillUser } from './AiAssistantAgentSkillHandler.js';
|
|
7
|
+
export interface AiAgentChatCommand {
|
|
8
|
+
/** Command name without the leading slash, e.g. `summarize`. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Short explanation rendered in the command picker. */
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AiAgentChatCommandContext {
|
|
14
|
+
command: AiAgentChatCommand;
|
|
15
|
+
arguments: string;
|
|
16
|
+
input: string | Array<Record<string, unknown>>;
|
|
17
|
+
user: User;
|
|
18
|
+
publish: AiAgentPublish;
|
|
19
|
+
}
|
|
20
|
+
export interface AiAgentChatCommandResult {
|
|
21
|
+
/** Text rendered as the command response and stored in the dialog. */
|
|
22
|
+
text?: string;
|
|
23
|
+
/** Set false for ephemeral commands that must not enter agent history. */
|
|
24
|
+
persist?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export type AiAgentChatCommandHandler = (context: AiAgentChatCommandContext) => Promise<AiAgentChatCommandResult | void> | AiAgentChatCommandResult | void;
|
|
3
27
|
/**
|
|
4
28
|
* Base class for AI assistant models. App modules register access rights
|
|
5
29
|
* and expose model metadata through this service.
|
|
30
|
+
*
|
|
31
|
+
* Two levels of capability:
|
|
32
|
+
*
|
|
33
|
+
* 1. `generateReply` — required. A plain request/response chat model; the
|
|
34
|
+
* assistant panel runs it through the same streaming transport and shows
|
|
35
|
+
* the answer as a single text part.
|
|
36
|
+
* 2. The optional agent members below. A service that implements `streamReply`
|
|
37
|
+
* is driven as a real agent: tool calls, reasoning, attachments and a
|
|
38
|
+
* server-side per-user session (history, compaction, model switching) are
|
|
39
|
+
* surfaced by the panel. Whatever the service does not implement is simply
|
|
40
|
+
* not rendered, so partial implementations are fine.
|
|
6
41
|
*/
|
|
7
42
|
export declare abstract class AbstractAiModelService {
|
|
8
43
|
readonly id: string;
|
|
9
44
|
readonly name: string;
|
|
10
45
|
readonly description?: string;
|
|
11
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Set when the model is registered. Agent implementations need it to read
|
|
48
|
+
* the panel config, the model registry and the user's permissions.
|
|
49
|
+
*/
|
|
50
|
+
protected adminizer: Adminizer;
|
|
51
|
+
readonly conversationHistory: AbstractAiConversationHistoryService;
|
|
52
|
+
private readonly chatCommands;
|
|
53
|
+
protected constructor(metadata: AiAssistantModelInfo, conversationHistory?: AbstractAiConversationHistoryService);
|
|
54
|
+
/** Called by AiAssistantHandler on registration; not part of the model API. */
|
|
55
|
+
attachAdminizer(adminizer: Adminizer): void;
|
|
12
56
|
getMetadata(): AiAssistantModelInfo;
|
|
57
|
+
/**
|
|
58
|
+
* Registers a slash command owned by this agent. The panel only displays
|
|
59
|
+
* it and sends its raw `/command arguments` text back to this service;
|
|
60
|
+
* command handling never lives in the browser.
|
|
61
|
+
*/
|
|
62
|
+
registerChatCommand(command: AiAgentChatCommand, handler?: AiAgentChatCommandHandler): void;
|
|
63
|
+
/** Commands available for autocomplete in the panel. */
|
|
64
|
+
getChatCommands(): AiAgentChatCommand[];
|
|
65
|
+
/** One declarative contract for the shared agent UI. */
|
|
66
|
+
getUiSchema(locale?: string): AiAgentUiSchema;
|
|
67
|
+
/**
|
|
68
|
+
* Browser capabilities available to a particular user. Tool-capable
|
|
69
|
+
* services can turn these descriptors into their provider's tool format.
|
|
70
|
+
*/
|
|
71
|
+
protected getUiMethods(user: User): Promise<AiAssistantUiMethod[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Server-side implementation of the built-in `search-admin-links` skill.
|
|
74
|
+
* An OpenHarness/AI SDK adapter can return this value directly from its
|
|
75
|
+
* tool's `execute` callback.
|
|
76
|
+
*/
|
|
77
|
+
protected searchAdminLinks(user: User, query?: string): Promise<AiAssistantAdminLink[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Built-in and app-contributed server skills this user may call. Data
|
|
80
|
+
* skills are already scoped to that user's model permissions, so an agent
|
|
81
|
+
* exposing them can be offered to non-administrator accounts.
|
|
82
|
+
*/
|
|
83
|
+
protected getAgentSkills(user: User): Promise<AiAssistantAgentSkillDescriptor[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Executes an app-owned server skill after its permission check. The result
|
|
86
|
+
* is normalized to plain JSON: it becomes a tool result inside the agent's
|
|
87
|
+
* message history, which every later turn re-validates.
|
|
88
|
+
*/
|
|
89
|
+
protected executeAgentSkill(id: string, input: Record<string, unknown>, user: User): Promise<unknown>;
|
|
90
|
+
/** Identity of the user an agent turn is running for. */
|
|
91
|
+
protected describeUser(user: User): AiAssistantSkillUser;
|
|
92
|
+
/**
|
|
93
|
+
* Opens an admin page for the user: either a concrete `href` or a link
|
|
94
|
+
* template plus `params` (a record page, a catalog item…). The target is
|
|
95
|
+
* validated against that user's permissions and the resolved URL is
|
|
96
|
+
* returned, so it can be reported back as the tool result.
|
|
97
|
+
*/
|
|
98
|
+
protected openAdminLink(target: AiAssistantNavigationTarget, user: User, publish: AiAgentPublish): Promise<string>;
|
|
99
|
+
/**
|
|
100
|
+
* Ask the shared panel to execute a registered browser method. The
|
|
101
|
+
* registry and permission check stay server-side, so an LLM cannot invoke
|
|
102
|
+
* an arbitrary client event.
|
|
103
|
+
*/
|
|
104
|
+
protected publishUiMethod(id: string, input: Record<string, unknown>, user: User, publish: AiAgentPublish): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Invoked by the agent transport for a registered `/command`. Returning
|
|
107
|
+
* null intentionally lets an unhandled command flow into `streamReply`
|
|
108
|
+
* or `generateReply` as regular agent input. A handler should return text
|
|
109
|
+
* instead of publishing a plain text frame itself, so its response can be
|
|
110
|
+
* persisted in the active conversation.
|
|
111
|
+
*/
|
|
112
|
+
executeChatCommand(prompt: string, input: string | Array<Record<string, unknown>>, user: User, publish: AiAgentPublish): Promise<AiAgentChatCommandResult | null>;
|
|
13
113
|
abstract generateReply(prompt: string, history: AiAssistantMessage[], user: User): Promise<string>;
|
|
114
|
+
/** True when the service streams runs itself instead of answering in one shot. */
|
|
115
|
+
isStreamingAgent(): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Run one turn and report progress through `publish`. `input` is either the
|
|
118
|
+
* plain prompt or an ai-sdk style ModelMessage[] when files are attached.
|
|
119
|
+
* The closing `done` frame is emitted by the run manager, not by the service.
|
|
120
|
+
*/
|
|
121
|
+
streamReply?(input: string | Array<Record<string, unknown>>, user: User, publish: AiAgentPublish): Promise<void>;
|
|
122
|
+
/** Current model, context usage and turn count of this user's session. */
|
|
123
|
+
getSessionMeta?(user: User): AiAgentSessionMeta;
|
|
124
|
+
/** Session history as ai-sdk ModelMessage[]; the panel rebuilds the thread from it. */
|
|
125
|
+
getSessionHistory?(user: User): Array<Record<string, unknown>>;
|
|
126
|
+
/** Restores a stored dialog when the user switches conversations. */
|
|
127
|
+
restoreSessionHistory?(user: User, messages: Array<Record<string, unknown>>): Promise<void> | void;
|
|
128
|
+
/** Drop the session. Returns whether there was anything to drop. */
|
|
129
|
+
resetSession?(user: User): boolean;
|
|
130
|
+
/** Summarize the session to free context. Returns e.g. tokensBefore/tokensAfter. */
|
|
131
|
+
compactSession?(user: User): Promise<Record<string, unknown>>;
|
|
132
|
+
/** LLMs the user may switch between inside this service. */
|
|
133
|
+
getModelChoices?(): Promise<AiAgentModelOption[]>;
|
|
134
|
+
/** Switch the session model. Returns whether the model changed. */
|
|
135
|
+
setCurrentModel?(user: User, model: string): Promise<boolean>;
|
|
136
|
+
/** Persist uploads before the turn, so tools can refer to them by id. */
|
|
137
|
+
saveUploadedFiles?(user: User, files: AiAgentUploadedFile[]): Promise<AiAgentSavedFile[]>;
|
|
138
|
+
/**
|
|
139
|
+
* Provisioning state. Omit when the service is always usable; when present
|
|
140
|
+
* and not `ready`, the panel shows a loader instead of the composer.
|
|
141
|
+
*/
|
|
142
|
+
getConnectionStatus?(): Promise<AiAgentConnectionStatus> | AiAgentConnectionStatus;
|
|
143
|
+
/** Budget / rate limits of the provider key, when the provider exposes them. */
|
|
144
|
+
getLimits?(forceRefresh: boolean): Promise<AiAgentLimits | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Panel copy for this service: title, welcome hint, composer placeholder,
|
|
147
|
+
* starter prompts and connection screens. The locale is resolved from the
|
|
148
|
+
* current admin user before this method is called.
|
|
149
|
+
*/
|
|
150
|
+
getUiHints?(locale?: string): AiAgentUiHints;
|
|
14
151
|
}
|
|
@@ -1,15 +1,41 @@
|
|
|
1
|
+
import { InMemoryAiConversationHistoryService } from './AbstractAiConversationHistoryService.js';
|
|
2
|
+
import { toJsonSafe } from './jsonSafe.js';
|
|
1
3
|
/**
|
|
2
4
|
* Base class for AI assistant models. App modules register access rights
|
|
3
5
|
* and expose model metadata through this service.
|
|
6
|
+
*
|
|
7
|
+
* Two levels of capability:
|
|
8
|
+
*
|
|
9
|
+
* 1. `generateReply` — required. A plain request/response chat model; the
|
|
10
|
+
* assistant panel runs it through the same streaming transport and shows
|
|
11
|
+
* the answer as a single text part.
|
|
12
|
+
* 2. The optional agent members below. A service that implements `streamReply`
|
|
13
|
+
* is driven as a real agent: tool calls, reasoning, attachments and a
|
|
14
|
+
* server-side per-user session (history, compaction, model switching) are
|
|
15
|
+
* surfaced by the panel. Whatever the service does not implement is simply
|
|
16
|
+
* not rendered, so partial implementations are fine.
|
|
4
17
|
*/
|
|
5
18
|
export class AbstractAiModelService {
|
|
6
19
|
id;
|
|
7
20
|
name;
|
|
8
21
|
description;
|
|
9
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Set when the model is registered. Agent implementations need it to read
|
|
24
|
+
* the panel config, the model registry and the user's permissions.
|
|
25
|
+
*/
|
|
26
|
+
adminizer;
|
|
27
|
+
conversationHistory;
|
|
28
|
+
chatCommands = new Map();
|
|
29
|
+
constructor(metadata, conversationHistory = new InMemoryAiConversationHistoryService()) {
|
|
10
30
|
this.id = metadata.id;
|
|
11
31
|
this.name = metadata.name;
|
|
12
32
|
this.description = metadata.description;
|
|
33
|
+
this.conversationHistory = conversationHistory;
|
|
34
|
+
}
|
|
35
|
+
/** Called by AiAssistantHandler on registration; not part of the model API. */
|
|
36
|
+
attachAdminizer(adminizer) {
|
|
37
|
+
this.adminizer = adminizer;
|
|
38
|
+
this.conversationHistory.initialize(this, adminizer);
|
|
13
39
|
}
|
|
14
40
|
getMetadata() {
|
|
15
41
|
return {
|
|
@@ -18,4 +44,131 @@ export class AbstractAiModelService {
|
|
|
18
44
|
description: this.description,
|
|
19
45
|
};
|
|
20
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Registers a slash command owned by this agent. The panel only displays
|
|
49
|
+
* it and sends its raw `/command arguments` text back to this service;
|
|
50
|
+
* command handling never lives in the browser.
|
|
51
|
+
*/
|
|
52
|
+
registerChatCommand(command, handler) {
|
|
53
|
+
const id = command.id.trim().replace(/^\/+/, '').toLowerCase();
|
|
54
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(id)) {
|
|
55
|
+
throw new Error(`Invalid AI agent command id: ${command.id}`);
|
|
56
|
+
}
|
|
57
|
+
this.chatCommands.set(id, {
|
|
58
|
+
command: { id, description: command.description?.trim() || undefined },
|
|
59
|
+
handler,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Commands available for autocomplete in the panel. */
|
|
63
|
+
getChatCommands() {
|
|
64
|
+
const commands = [...this.chatCommands.values()].map(({ command }) => ({ ...command }));
|
|
65
|
+
if (typeof this.compactSession === 'function' && !this.chatCommands.has('compact')) {
|
|
66
|
+
commands.unshift({
|
|
67
|
+
id: 'compact',
|
|
68
|
+
description: 'Prune or summarize older messages to free context.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return commands;
|
|
72
|
+
}
|
|
73
|
+
/** One declarative contract for the shared agent UI. */
|
|
74
|
+
getUiSchema(locale) {
|
|
75
|
+
return {
|
|
76
|
+
...(this.getUiHints?.(locale) ?? {}),
|
|
77
|
+
commands: this.getChatCommands(),
|
|
78
|
+
panels: {
|
|
79
|
+
history: true,
|
|
80
|
+
models: typeof this.getModelChoices === 'function' && typeof this.setCurrentModel === 'function',
|
|
81
|
+
limits: typeof this.getLimits === 'function',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Browser capabilities available to a particular user. Tool-capable
|
|
87
|
+
* services can turn these descriptors into their provider's tool format.
|
|
88
|
+
*/
|
|
89
|
+
getUiMethods(user) {
|
|
90
|
+
return this.adminizer.aiAssistantUiMethodHandler.getAvailable(user);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Server-side implementation of the built-in `search-admin-links` skill.
|
|
94
|
+
* An OpenHarness/AI SDK adapter can return this value directly from its
|
|
95
|
+
* tool's `execute` callback.
|
|
96
|
+
*/
|
|
97
|
+
searchAdminLinks(user, query) {
|
|
98
|
+
return this.adminizer.aiAssistantUiMethodHandler.searchAdminLinks(user, query);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Built-in and app-contributed server skills this user may call. Data
|
|
102
|
+
* skills are already scoped to that user's model permissions, so an agent
|
|
103
|
+
* exposing them can be offered to non-administrator accounts.
|
|
104
|
+
*/
|
|
105
|
+
getAgentSkills(user) {
|
|
106
|
+
return this.adminizer.aiAssistantAgentSkillHandler.getAvailable(user);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Executes an app-owned server skill after its permission check. The result
|
|
110
|
+
* is normalized to plain JSON: it becomes a tool result inside the agent's
|
|
111
|
+
* message history, which every later turn re-validates.
|
|
112
|
+
*/
|
|
113
|
+
async executeAgentSkill(id, input, user) {
|
|
114
|
+
const result = await this.adminizer.aiAssistantAgentSkillHandler.execute(id, input, user);
|
|
115
|
+
return toJsonSafe(result) ?? null;
|
|
116
|
+
}
|
|
117
|
+
/** Identity of the user an agent turn is running for. */
|
|
118
|
+
describeUser(user) {
|
|
119
|
+
return this.adminizer.aiAssistantAgentSkillHandler.describeUser(user);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Opens an admin page for the user: either a concrete `href` or a link
|
|
123
|
+
* template plus `params` (a record page, a catalog item…). The target is
|
|
124
|
+
* validated against that user's permissions and the resolved URL is
|
|
125
|
+
* returned, so it can be reported back as the tool result.
|
|
126
|
+
*/
|
|
127
|
+
async openAdminLink(target, user, publish) {
|
|
128
|
+
const href = await this.adminizer.aiAssistantUiMethodHandler.resolveNavigation(user, target);
|
|
129
|
+
await this.publishUiMethod('navigate', { href }, user, publish);
|
|
130
|
+
return href;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Ask the shared panel to execute a registered browser method. The
|
|
134
|
+
* registry and permission check stay server-side, so an LLM cannot invoke
|
|
135
|
+
* an arbitrary client event.
|
|
136
|
+
*/
|
|
137
|
+
async publishUiMethod(id, input, user, publish) {
|
|
138
|
+
const method = (await this.getUiMethods(user)).find((candidate) => candidate.id === id);
|
|
139
|
+
if (!method)
|
|
140
|
+
throw new Error(`AI assistant UI method "${id}" is not available`);
|
|
141
|
+
publish({ type: 'ui.method', method: method.id, action: method.action, input });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Invoked by the agent transport for a registered `/command`. Returning
|
|
145
|
+
* null intentionally lets an unhandled command flow into `streamReply`
|
|
146
|
+
* or `generateReply` as regular agent input. A handler should return text
|
|
147
|
+
* instead of publishing a plain text frame itself, so its response can be
|
|
148
|
+
* persisted in the active conversation.
|
|
149
|
+
*/
|
|
150
|
+
async executeChatCommand(prompt, input, user, publish) {
|
|
151
|
+
const match = /^\/([a-z][a-z0-9_-]*)(?:\s+([\s\S]*))?$/i.exec(prompt.trim());
|
|
152
|
+
if (!match)
|
|
153
|
+
return null;
|
|
154
|
+
const entry = this.chatCommands.get(match[1].toLowerCase());
|
|
155
|
+
if (entry?.handler) {
|
|
156
|
+
const result = await entry.handler({ command: entry.command, arguments: match[2] ?? '', input, user, publish });
|
|
157
|
+
return { persist: true, ...result };
|
|
158
|
+
}
|
|
159
|
+
if (match[1].toLowerCase() !== 'compact' || typeof this.compactSession !== 'function')
|
|
160
|
+
return null;
|
|
161
|
+
const result = await this.compactSession(user);
|
|
162
|
+
const compacted = result.compacted === true;
|
|
163
|
+
const before = Number(result.tokensBefore ?? 0);
|
|
164
|
+
const after = Number(result.tokensAfter ?? 0);
|
|
165
|
+
const text = compacted
|
|
166
|
+
? `Context compacted${before > 0 ? `: ${before} → ${after} tokens.` : '.'}`
|
|
167
|
+
: 'Nothing to compact yet.';
|
|
168
|
+
return { text, persist: true };
|
|
169
|
+
}
|
|
170
|
+
/** True when the service streams runs itself instead of answering in one shot. */
|
|
171
|
+
isStreamingAgent() {
|
|
172
|
+
return typeof this.streamReply === 'function';
|
|
173
|
+
}
|
|
21
174
|
}
|