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.
Files changed (232) hide show
  1. package/assets/abnfDiagram-VCTEODGH-DFY3Pztx.js +1 -0
  2. package/assets/add-31tS-g4V.js +1 -0
  3. package/assets/add-group-Cf3zT7WE.js +1 -0
  4. package/assets/add-user-C62wOna-.js +1 -0
  5. package/assets/ai-assistant/agent.es.js +28464 -0
  6. package/assets/app-DPmI-MvN.css +1 -0
  7. package/assets/app-vdHbjOQX.js +138 -0
  8. package/assets/arc-DP4o1zz3.js +1 -0
  9. package/assets/architectureDiagram-5GKGNRK7-BjXW5Zu0.js +36 -0
  10. package/assets/asmcrypto-DZeQH0LC.js +1 -0
  11. package/assets/blockDiagram-I7D4REHJ-BUPnDwN0.js +129 -0
  12. package/assets/c4Diagram-7LVT6UL2-Cmb_dAhd.js +38 -0
  13. package/assets/{catalog-BEvJZYJP.js → catalog-C-y5rMaG.js} +3 -3
  14. package/assets/channel-CkJRUsq_.js +1 -0
  15. package/assets/chunk-2Q5K7J3B-CmS-dWwQ.js +1 -0
  16. package/assets/chunk-5VM5RSS4-Cqs2Ow64.js +15 -0
  17. package/assets/chunk-F27PBJKO-BoMCpJA1.js +1 -0
  18. package/assets/chunk-IMKFNOWR-DlDcPk1x.js +231 -0
  19. package/assets/chunk-JWPE2WC7-DzL_SU8s.js +1 -0
  20. package/assets/chunk-POPQ4Y6H-Cwq2-Vb8.js +1 -0
  21. package/assets/chunk-SVP7TREG-BfRHY5FU.js +88 -0
  22. package/assets/chunk-TICWLB2K-CRP7rHkW.js +206 -0
  23. package/assets/chunk-XXDRQBXY-Bg2jg-98.js +1 -0
  24. package/assets/classDiagram-ZZMXUADV-D4EF3HGj.js +1 -0
  25. package/assets/classDiagram-v2-VYDZK3BY-D4EF3HGj.js +1 -0
  26. package/assets/controls/ckeditor.es.js +9382 -9320
  27. package/assets/controls/handsontable.es.js +11971 -11938
  28. package/assets/controls/jsoneditor.es.js +10442 -10233
  29. package/assets/controls/toast-ui.es.js +1308 -1279
  30. package/assets/cose-bilkent-JH36ORCC-CCJ0SGyO.js +1 -0
  31. package/assets/cynefin-OW5HDTMX-B2Jx5fw3.js +166 -0
  32. package/assets/cynefinDiagram-5FMLGOSQ-DuK0Lb62.js +62 -0
  33. package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
  34. package/assets/dagre-GXQ25YYZ-DnOYexET.js +4 -0
  35. package/assets/dashboard-X8k4X6Sw.js +3 -0
  36. package/assets/debounce-B3mXI_sx.js +1 -0
  37. package/assets/defaultLocale-DX6XiGOO.js +1 -0
  38. package/assets/diagram-S7CK7UJ4-DDgPBHsK.js +30 -0
  39. package/assets/diagram-UQ7AKVKN-qlL0GZs8.js +41 -0
  40. package/assets/diagram-VSXAHHWV-CQbiU1_c.js +3 -0
  41. package/assets/diagram-VX7I27RA-DFDI9_au.js +24 -0
  42. package/assets/diagram-Z3DM3KII-B5C9dtmJ.js +24 -0
  43. package/assets/docs-BiZOD7PP.js +1 -0
  44. package/assets/ebnfDiagram-PWID7BFC-CLMUsXEI.js +1 -0
  45. package/assets/erDiagram-RLTQ6QDP-ax9Xol5-.js +99 -0
  46. package/assets/flowDiagram-HODETNUW-CvaH7l3S.js +168 -0
  47. package/assets/ganttDiagram-EL5Y4UJY-Cga5U19j.js +292 -0
  48. package/assets/gitGraphDiagram-WWUBYQGX-B6o1T3-d.js +106 -0
  49. package/assets/history-CVBJUNSQ.js +1 -0
  50. package/assets/infoDiagram-27XIBGKW-BpWxgBCE.js +2 -0
  51. package/assets/init-Gi6I4Gst.js +1 -0
  52. package/assets/init-user-xSvnFTF0.js +1 -0
  53. package/assets/isObject-DL-BuaBl.js +1 -0
  54. package/assets/ishikawaDiagram-5VMMS53U-zE83db4H.js +70 -0
  55. package/assets/journeyDiagram-3NMN7TZE-DZNVFqrX.js +139 -0
  56. package/assets/kanban-definition-UXKFOSKX-CqxNgk27.js +89 -0
  57. package/assets/katex-HP8lGamR.js +257 -0
  58. package/assets/layout-CN3kt_jt.js +1 -0
  59. package/assets/linear-nvkaQ6BA.js +1 -0
  60. package/assets/{list-DhK0McVn.js → list-CrIU2eZz.js} +2 -2
  61. package/assets/login-CX9xVjCn.js +1 -0
  62. package/assets/manifest.json +834 -58
  63. package/assets/media-manager-BfYftLIH.js +6 -0
  64. package/assets/mermaid.core-C0V1NaVJ.js +310 -0
  65. package/assets/mindmap-definition-YA3MSWOX-Bh7_d9f1.js +96 -0
  66. package/assets/module-DPEvnad3.js +1 -0
  67. package/assets/notification-Dqpt5AIZ.js +20 -0
  68. package/assets/ordinal-Cboi1Yqb.js +1 -0
  69. package/assets/pegDiagram-XKGWAZYB-DMnjm6Z1.js +1 -0
  70. package/assets/pieDiagram-E7YTZNPT-D-RWCgYb.js +39 -0
  71. package/assets/quadrantDiagram-AXDQQJYC-BNBGP7RM.js +7 -0
  72. package/assets/railroadDiagram-O6MQD6OU-BIjVPT01.js +1 -0
  73. package/assets/register-CF4KQ97e.js +1 -0
  74. package/assets/requirementDiagram-BXWQKSXE-VPrtGdjM.js +84 -0
  75. package/assets/ru-CFJ4fysR.js +1 -0
  76. package/assets/sankeyDiagram-P5KCCOFB-ct9mLF3U.js +40 -0
  77. package/assets/sequenceDiagram-WJ2MYXX4-TC06Mbmw.js +162 -0
  78. package/assets/sizeCapture-INFHLROL-BMMwriCf.js +1 -0
  79. package/assets/stateDiagram-D77RDMKH-DtwYRPCX.js +1 -0
  80. package/assets/stateDiagram-v2-MP3YSRHH-C1yB0O5U.js +1 -0
  81. package/assets/swimlanes-42K2YHIH-DE3hRbbr.js +1 -0
  82. package/assets/swimlanesDiagram-VR7AAH4N-Ce4l1sQD.js +8 -0
  83. package/assets/timeline-definition-24CTP7MA-N7s_hfjj.js +120 -0
  84. package/assets/toNumber-DZb-r9sy.js +1 -0
  85. package/assets/use-appearance-DYTIKpT2.js +1 -0
  86. package/assets/user-filters-list-fR0YNJ3l.js +1 -0
  87. package/assets/vennDiagram-4TSXK5OY-Dj5bFQkR.js +34 -0
  88. package/assets/wardleyDiagram-VM6X3IG4-83ax25Z6.js +78 -0
  89. package/assets/welcome-v11MTsRK.js +1 -0
  90. package/assets/with-app-layout-CH6RIib1.js +31 -0
  91. package/assets/xychartDiagram-S5SC5T6Z-CW9Br5Xp.js +7 -0
  92. package/controllers/access-rights/permissionOptions.d.ts +2 -0
  93. package/controllers/access-rights/permissionOptions.js +23 -0
  94. package/controllers/add.js +2 -2
  95. package/controllers/addGroup.js +15 -6
  96. package/controllers/ai/AiAgentController.d.ts +66 -0
  97. package/controllers/ai/AiAgentController.js +435 -0
  98. package/controllers/ai/AiAssistantController.d.ts +0 -4
  99. package/controllers/ai/AiAssistantController.js +4 -78
  100. package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
  101. package/controllers/docs/index.d.ts +22 -0
  102. package/controllers/docs/index.js +126 -0
  103. package/controllers/edit.js +2 -2
  104. package/controllers/editGroup.js +15 -6
  105. package/controllers/filter-fields/savedFilters.js +1 -1
  106. package/controllers/history-actions/HistoryController.js +7 -7
  107. package/controllers/inlineUpdate.js +10 -3
  108. package/controllers/list.js +1 -1
  109. package/controllers/login.js +1 -1
  110. package/controllers/notifications/NotificationController.js +14 -8
  111. package/controllers/view.js +3 -3
  112. package/controllers/widgets/filterInfo.js +1 -1
  113. package/helpers/accessRightsHelper.d.ts +30 -3
  114. package/helpers/accessRightsHelper.js +140 -12
  115. package/helpers/assetVersionHelper.d.ts +14 -0
  116. package/helpers/assetVersionHelper.js +42 -0
  117. package/helpers/configHelper.js +19 -18
  118. package/helpers/controllerHelper.d.ts +1 -0
  119. package/helpers/controllerHelper.js +25 -15
  120. package/helpers/fieldsHelper.d.ts +2 -1
  121. package/helpers/fieldsHelper.js +11 -5
  122. package/helpers/inertiaActionsHelper.d.ts +1 -1
  123. package/helpers/inertiaActionsHelper.js +4 -5
  124. package/helpers/inertiaAddHelper.d.ts +1 -1
  125. package/helpers/inertiaAddHelper.js +7 -6
  126. package/helpers/inertiaGroupHelper.js +10 -2
  127. package/helpers/inertiaListHelper.d.ts +1 -1
  128. package/helpers/inertiaListHelper.js +7 -8
  129. package/helpers/inertiaMenuHelper.d.ts +7 -2
  130. package/helpers/inertiaMenuHelper.js +12 -27
  131. package/helpers/menuHelper.d.ts +11 -1
  132. package/helpers/menuHelper.js +12 -0
  133. package/helpers/modelResourceHelper.d.ts +9 -0
  134. package/helpers/modelResourceHelper.js +43 -0
  135. package/helpers/navigationAccessHelper.d.ts +13 -0
  136. package/helpers/navigationAccessHelper.js +45 -0
  137. package/index.d.ts +11 -0
  138. package/index.js +11 -0
  139. package/interfaces/adminpanelConfig.d.ts +160 -2
  140. package/interfaces/queryCriteria.d.ts +13 -0
  141. package/interfaces/types.d.ts +169 -0
  142. package/lib/Adminizer.d.ts +8 -0
  143. package/lib/Adminizer.js +29 -2
  144. package/lib/DataAccessor.d.ts +100 -3
  145. package/lib/DataAccessor.js +454 -245
  146. package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
  147. package/lib/access-graph/AccessGraphResolver.js +605 -0
  148. package/lib/access-graph/RecordAccessCache.d.ts +23 -0
  149. package/lib/access-graph/RecordAccessCache.js +30 -0
  150. package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
  151. package/lib/access-graph/RecordAccessResolver.js +295 -0
  152. package/lib/access-graph/shared.d.ts +60 -0
  153. package/lib/access-graph/shared.js +121 -0
  154. package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
  155. package/lib/admin-links/AdminLinkHandler.js +183 -0
  156. package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
  157. package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
  158. package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
  159. package/lib/ai-assistant/AbstractAiModelService.js +154 -1
  160. package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
  161. package/lib/ai-assistant/AiAgentRunManager.js +109 -0
  162. package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
  163. package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
  164. package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
  165. package/lib/ai-assistant/AiAssistantHandler.js +4 -0
  166. package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
  167. package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
  168. package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
  169. package/lib/ai-assistant/builtinAgentSkills.js +259 -0
  170. package/lib/ai-assistant/jsonSafe.d.ts +13 -0
  171. package/lib/ai-assistant/jsonSafe.js +81 -0
  172. package/lib/app-manager/AdminizerApp.d.ts +31 -2
  173. package/lib/app-manager/AppManager.js +43 -42
  174. package/lib/app-manager/ConfigLayerHandler.js +4 -0
  175. package/lib/docs/AbstractDocumentation.d.ts +100 -0
  176. package/lib/docs/AbstractDocumentation.js +108 -0
  177. package/lib/docs/DocumentationHandler.d.ts +100 -0
  178. package/lib/docs/DocumentationHandler.js +198 -0
  179. package/lib/docs/FileDocumentation.d.ts +47 -0
  180. package/lib/docs/FileDocumentation.js +189 -0
  181. package/lib/docs/docsAgentSkills.d.ts +10 -0
  182. package/lib/docs/docsAgentSkills.js +93 -0
  183. package/lib/filters/FilterService.js +4 -3
  184. package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
  185. package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
  186. package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
  187. package/lib/model/AbstractModel.d.ts +48 -0
  188. package/lib/model/AbstractModel.js +14 -4
  189. package/lib/model/ModelHandler.d.ts +50 -2
  190. package/lib/model/ModelHandler.js +117 -8
  191. package/lib/model/adapter/sequelize.d.ts +43 -3
  192. package/lib/model/adapter/sequelize.js +317 -138
  193. package/lib/model/adapter/typeorm.d.ts +16 -0
  194. package/lib/model/adapter/typeorm.js +136 -9
  195. package/lib/notifications/GeneralNotificationService.js +6 -5
  196. package/lib/notifications/SystemNotificationService.js +1 -1
  197. package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
  198. package/lib/ui-i18n/uiTranslationKeys.js +42 -1
  199. package/lib/widgets/widgetHandler.js +10 -6
  200. package/models/Group.d.ts +2 -1
  201. package/package.json +35 -23
  202. package/policies/authPolicies.js +6 -6
  203. package/policies/permissionResolvers.d.ts +1 -0
  204. package/policies/permissionResolvers.js +3 -0
  205. package/system/Router.js +21 -3
  206. package/system/bindAccessRights.js +13 -3
  207. package/system/bindDocs.d.ts +13 -0
  208. package/system/bindDocs.js +77 -0
  209. package/system/bindInertia.js +30 -7
  210. package/system/buildInternalModelAccess.d.ts +8 -0
  211. package/system/buildInternalModelAccess.js +36 -4
  212. package/system/defaults.js +7 -1
  213. package/system/validateSystemModels.js +29 -9
  214. package/assets/add-DED_wpga.js +0 -1
  215. package/assets/add-group-CL80RdYb.js +0 -1
  216. package/assets/add-user-BRmwvQB8.js +0 -1
  217. package/assets/app-DNXiApCk.css +0 -1
  218. package/assets/app-layout-86yxiZOy.js +0 -1
  219. package/assets/app.js +0 -138
  220. package/assets/dashboard-ZP6K2s_8.js +0 -3
  221. package/assets/debounce-BMmXVQ06.js +0 -1
  222. package/assets/history-M61Bcv02.js +0 -1
  223. package/assets/init-user-8aUohVDq.js +0 -1
  224. package/assets/login-CO4laf6m.js +0 -1
  225. package/assets/media-manager-BDbpSSjH.js +0 -6
  226. package/assets/module-DfBneaUl.js +0 -1
  227. package/assets/notification-CVnQf3ef.js +0 -20
  228. package/assets/register-DBYIMXGQ.js +0 -1
  229. package/assets/ru-rYKdQbWF.js +0 -1
  230. package/assets/use-appearance-_EbbjHTh.js +0 -1
  231. package/assets/user-filters-list-DXhGLFTn.js +0 -1
  232. 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
- protected constructor(metadata: AiAssistantModelInfo);
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
- constructor(metadata) {
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
  }