@capybara-hq/kernel 0.1.0-rc.14 → 0.1.0-rc.15
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/dist/admin/admin.schemas.d.ts +5 -13
- package/dist/catalog/admin-catalog.controller.d.ts +27 -4
- package/dist/catalog/admin-catalog.controller.js +53 -5
- package/dist/catalog/appearance.schemas.d.ts +10 -0
- package/dist/catalog/appearance.schemas.js +20 -0
- package/dist/catalog/appearance.service.d.ts +105 -0
- package/dist/catalog/appearance.service.js +306 -0
- package/dist/catalog/catalog-import.module.js +3 -0
- package/dist/catalog/catalog-items.d.ts +11 -0
- package/dist/catalog/catalog-items.js +21 -0
- package/dist/catalog/catalog-scope.service.d.ts +7 -0
- package/dist/catalog/catalog-scope.service.js +55 -15
- package/dist/catalog/catalog.service.js +10 -2
- package/dist/catalog/cli.js +3 -0
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-extension.controller.d.ts +27 -7
- package/dist/catalog/org-extension.controller.js +48 -6
- package/dist/catalog/plugin-import.d.ts +28 -4
- package/dist/catalog/plugin-import.js +95 -19
- package/dist/catalog/plugin-package.d.ts +4 -2
- package/dist/catalog/plugin-package.js +29 -3
- package/dist/client/client-access.service.d.ts +101 -0
- package/dist/client/client-access.service.js +294 -0
- package/dist/client/client-builds.service.d.ts +109 -0
- package/dist/client/client-builds.service.js +229 -0
- package/dist/client/client-delivery.controller.d.ts +25 -0
- package/dist/client/client-delivery.controller.js +66 -0
- package/dist/client/client-files.d.ts +48 -0
- package/dist/client/client-files.js +136 -0
- package/dist/client/client-reports.cron.d.ts +13 -0
- package/dist/client/client-reports.cron.js +42 -0
- package/dist/client/client-reports.service.d.ts +30 -0
- package/dist/client/client-reports.service.js +102 -0
- package/dist/client/client.controller.d.ts +45 -0
- package/dist/client/client.controller.js +121 -0
- package/dist/client/client.module.d.ts +7 -0
- package/dist/client/client.module.js +26 -0
- package/dist/client/client.schemas.d.ts +55 -0
- package/dist/client/client.schemas.js +27 -0
- package/dist/ctx/create-ctx.d.ts +4 -3
- package/dist/ctx/create-ctx.js +4 -3
- package/dist/db/kernel-schema.d.ts +47 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +29 -0
- package/dist/extensions/admin-ops.d.ts +4 -1
- package/dist/extensions/admin-ops.js +22 -1
- package/dist/extensions/admin-view.d.ts +8 -0
- package/dist/extensions/admin-view.js +78 -0
- package/dist/extensions/blobs.d.ts +14 -1
- package/dist/extensions/blobs.js +48 -5
- package/dist/extensions/host.d.ts +6 -1
- package/dist/extensions/host.js +20 -9
- package/dist/extensions/package.d.ts +4 -3
- package/dist/extensions/package.js +5 -1
- package/dist/extensions/permission-set.d.ts +19 -3
- package/dist/extensions/permission-set.js +19 -2
- package/dist/extensions/permission-view.d.ts +38 -7
- package/dist/extensions/permission-view.js +27 -8
- package/dist/extensions/sources.d.ts +6 -0
- package/dist/extensions/sources.js +2 -0
- package/dist/extensions/state.module.js +9 -1
- package/dist/http/api.module.js +10 -0
- package/dist/identity/settings.d.ts +41 -26
- package/dist/identity/settings.js +87 -4
- package/dist/infra/throttler/named-throttlers.d.ts +4 -0
- package/dist/infra/throttler/named-throttlers.js +14 -0
- package/dist/kernel.module.js +3 -0
- package/dist/orgs/settings.service.d.ts +2 -26
- package/dist/platform/exclusions.js +27 -1
- package/dist/platform/ops/admin.ops.js +17 -2
- package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
- package/dist/platform/ops/catalog-admin.ops.js +85 -2
- package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
- package/dist/platform/ops/org-plugins.ops.js +63 -4
- package/dist/platform/ops/org-settings.ops.js +2 -2
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/ui.ops.js +1 -1
- package/dist/platform/platform-ops.module.js +2 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/worker.module.js +4 -0
- package/dist/themes/render-css.d.ts +2 -1
- package/dist/themes/render-css.js +5 -2
- package/dist/themes/theme-delivery.service.d.ts +4 -2
- package/dist/themes/theme-delivery.service.js +8 -3
- package/dist/themes/theme-state.d.ts +28 -7
- package/dist/themes/theme-state.js +24 -6
- package/dist/themes/themes-public.controller.d.ts +7 -2
- package/dist/themes/themes-public.controller.js +11 -5
- package/dist/themes/themes.service.js +2 -2
- package/dist/tools/pipeline.d.ts +3 -2
- package/dist/ui/arrangement.d.ts +41 -50
- package/dist/ui/arrangement.js +88 -15
- package/dist/ui/registry.d.ts +13 -3
- package/dist/ui/registry.js +15 -4
- package/dist/ui/ui.schemas.d.ts +4 -0
- package/dist/ui/views.d.ts +36 -9
- package/dist/ui/views.js +58 -28
- package/package.json +3 -3
- package/src/migrations/0107_extension_client_builds.sql +50 -0
package/dist/ui/arrangement.js
CHANGED
|
@@ -1,22 +1,66 @@
|
|
|
1
|
-
import { UI_ARRANGEABLE_SLOTS, UI_CONTRIBUTION_KEY_RE, uiSlot, } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { UI_ARRANGEABLE_SLOTS, UI_CONTRIBUTION_KEY_RE, UI_SINGLE_SLOTS, uiSlot, } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
/**
|
|
4
|
-
* Arrangement of UI slots (UI slots spec §5): per slot, which
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Arrangement of UI slots (UI slots spec §5, client extensions spec §6.3): per list slot, which
|
|
5
|
+
* contributions are hidden and in which order; per single slot, which contribution replaces the
|
|
6
|
+
* built-in (`use`, null = the built-in). "Hidden" means not listed and `use` only picks what is
|
|
7
|
+
* listed: the access gate never reads an arrangement, so a contribution's view, action and embed
|
|
8
|
+
* routes keep working for an allowed role whatever the arrangement says.
|
|
7
9
|
*/
|
|
8
|
-
const Key = z.string().regex(UI_CONTRIBUTION_KEY_RE);
|
|
10
|
+
const Key = z.string().regex(UI_CONTRIBUTION_KEY_RE, "must be a contribution key <plugin>/<id>");
|
|
9
11
|
const LIST_MAX = 200;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* A list slot's entry: the keys hidden and the keys ordered first. The refusals name the slot and
|
|
14
|
+
* what it takes (actionable for an admin and the Platform tool's model).
|
|
15
|
+
*/
|
|
16
|
+
const listEntrySchema = (slot) => {
|
|
17
|
+
const keys = z
|
|
18
|
+
.array(Key, {
|
|
19
|
+
error: (iss) => iss.input === undefined
|
|
20
|
+
? `${slot} is a list slot: give an array of contribution keys <plugin>/<id>`
|
|
21
|
+
: undefined,
|
|
22
|
+
})
|
|
23
|
+
.max(LIST_MAX);
|
|
24
|
+
return z.strictObject({ hidden: keys, order: keys }, {
|
|
25
|
+
error: (iss) => iss.code === "unrecognized_keys"
|
|
26
|
+
? `${slot} is a list slot: give { hidden, order }; use is only for the single slots (${UI_SINGLE_SLOTS.join(", ")})`
|
|
27
|
+
: undefined,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
/** A single slot's entry: the contribution that replaces the built-in, or null for the built-in. */
|
|
31
|
+
const useEntrySchema = (slot) => z.strictObject({
|
|
32
|
+
use: z.union([Key, z.null()], {
|
|
33
|
+
error: (iss) => iss.input === undefined
|
|
34
|
+
? `${slot} needs use: a contribution key <plugin>/<id>, or null for the built-in`
|
|
35
|
+
: undefined,
|
|
36
|
+
}),
|
|
37
|
+
}, {
|
|
38
|
+
error: (iss) => iss.code === "unrecognized_keys"
|
|
39
|
+
? `${slot} is a single slot: give { use } (a contribution key <plugin>/<id>, or null for the built-in); hidden and order are only for list slots`
|
|
40
|
+
: undefined,
|
|
41
|
+
});
|
|
42
|
+
/** Whether a slot is a single (replaceable) slot. */
|
|
43
|
+
export const isSingle = (slot) => UI_SINGLE_SLOTS.includes(slot);
|
|
44
|
+
/**
|
|
45
|
+
* An object over `slots`, each optional and of its kind's entry (nullable in a patch); unknown
|
|
46
|
+
* slots are refused, and so are `use` on a list slot and `hidden` / `order` on a single slot (400
|
|
47
|
+
* `validation_failed` on the settings routes).
|
|
48
|
+
*/
|
|
49
|
+
function arrangementShape(slots, nullable) {
|
|
50
|
+
const shape = {};
|
|
51
|
+
for (const slot of slots) {
|
|
52
|
+
const entry = isSingle(slot) ? useEntrySchema(slot) : listEntrySchema(slot);
|
|
53
|
+
shape[slot] = (nullable ? entry.nullable() : entry).optional();
|
|
54
|
+
}
|
|
55
|
+
return z.object(shape).strict();
|
|
56
|
+
}
|
|
57
|
+
export const UiArrangementSchema = arrangementShape(UI_ARRANGEABLE_SLOTS, false);
|
|
14
58
|
/** The slots an org arranges: every arrangeable slot but the platform's own. */
|
|
15
59
|
export const ORG_ARRANGEABLE = UI_ARRANGEABLE_SLOTS.filter((s) => s !== "platform.page");
|
|
16
60
|
/** An org's patch: a slot's entry replaces the stored one, null removes it. */
|
|
17
|
-
export const OrgUiArrangementPatchSchema =
|
|
61
|
+
export const OrgUiArrangementPatchSchema = arrangementShape(ORG_ARRANGEABLE, true);
|
|
18
62
|
/** A deployment patch: like {@link OrgUiArrangementPatchSchema} over every arrangeable slot. */
|
|
19
|
-
export const UiArrangementPatchSchema =
|
|
63
|
+
export const UiArrangementPatchSchema = arrangementShape(UI_ARRANGEABLE_SLOTS, true);
|
|
20
64
|
/** Merges a per-slot patch into the stored arrangement (null removes the slot's entry). */
|
|
21
65
|
export function mergeArrangement(current, patch) {
|
|
22
66
|
const merged = { ...current };
|
|
@@ -29,17 +73,46 @@ export function mergeArrangement(current, patch) {
|
|
|
29
73
|
return merged;
|
|
30
74
|
}
|
|
31
75
|
const NONE = { hidden: [], order: [] };
|
|
32
|
-
|
|
76
|
+
const asList = (e) => ("hidden" in e ? e : NONE);
|
|
77
|
+
/**
|
|
78
|
+
* The list entry that applies to a slot: the org's if it arranged the slot, else the platform's
|
|
79
|
+
* (a `use` entry, which only a single slot stores, reads as none).
|
|
80
|
+
*/
|
|
33
81
|
export function effectiveEntry(org, platform, slot) {
|
|
34
82
|
const o = org?.[slot];
|
|
35
83
|
if (o !== undefined)
|
|
36
|
-
return { entry: o, source: "org" };
|
|
84
|
+
return { entry: asList(o), source: "org" };
|
|
37
85
|
const p = platform[slot];
|
|
38
86
|
if (p !== undefined)
|
|
39
|
-
return { entry: p, source: "platform" };
|
|
87
|
+
return { entry: asList(p), source: "platform" };
|
|
40
88
|
return { entry: NONE, source: "none" };
|
|
41
89
|
}
|
|
42
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The winner of a single slot (client extensions spec §6.3): the org's entry if it has one, else
|
|
92
|
+
* the platform's, else none. No entry or `use: null` is the built-in (null); a `use` naming a key
|
|
93
|
+
* not in `available` (extension off in the org, contribution dropped, the viewer's role below the
|
|
94
|
+
* slot's) is the built-in too, never the other level's pick. A list slot, or an entry that is not
|
|
95
|
+
* a `use` entry, has no winner.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveSingleSlot(slot, orgEntry, platformEntry, available) {
|
|
98
|
+
if (!isSingle(slot))
|
|
99
|
+
return null;
|
|
100
|
+
const entry = orgEntry ?? platformEntry;
|
|
101
|
+
if (entry === undefined || !("use" in entry) || entry.use === null)
|
|
102
|
+
return null;
|
|
103
|
+
return available.has(entry.use) ? entry.use : null;
|
|
104
|
+
}
|
|
105
|
+
/** The `use` stored for a single slot: the org's entry if it has one, else the platform's. */
|
|
106
|
+
export function storedUse(org, platform, slot) {
|
|
107
|
+
const o = org?.[slot];
|
|
108
|
+
const e = o ?? platform[slot];
|
|
109
|
+
return {
|
|
110
|
+
use: e !== undefined && "use" in e ? e.use : null,
|
|
111
|
+
followsPlatform: o === undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The arrangement key of a contribution: `<plugin>/<id>`. */
|
|
115
|
+
export const keyOf = (i) => `${i.plugin}/${i.id}`;
|
|
43
116
|
/**
|
|
44
117
|
* Orders the items of one slot (hidden ones stay in place): grouped by anchor in the slot's anchor
|
|
45
118
|
* order, also without an entry (the lists match what the page renders per anchor); within a group
|
package/dist/ui/registry.d.ts
CHANGED
|
@@ -45,19 +45,29 @@ export declare const uiFrame: (m: UiBusMessage) => string;
|
|
|
45
45
|
/**
|
|
46
46
|
* 单个插件在 setup 期间注册的视图与动作:进程内的静态登记(同 `ctx.tools.register`),
|
|
47
47
|
* 不是跨请求的共享状态——各副本加载同一插件得到同样的登记。
|
|
48
|
-
* id
|
|
48
|
+
* The id must be in `permissions.ui.views` or `permissions.ui.components` (a component's server
|
|
49
|
+
* view, client extensions spec §5.2); `defineExtension` already cross-checked them with the
|
|
50
|
+
* manifest `ui`.
|
|
49
51
|
*/
|
|
50
52
|
export declare class PluginUiRegistration {
|
|
51
53
|
private readonly manifest;
|
|
52
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
|
|
56
|
+
* throws ExtensionPermissionError and writes an audit entry.
|
|
57
|
+
*/
|
|
53
58
|
private readonly deny;
|
|
54
59
|
readonly views: Map<string, UiRender>;
|
|
55
60
|
readonly actions: Map<string, UiActionHandler>;
|
|
56
61
|
private readonly declaredViews;
|
|
57
62
|
private readonly contributions;
|
|
63
|
+
/** The ids of the manifest's `component` contributions: their permission is `ui.components`. */
|
|
64
|
+
private readonly components;
|
|
58
65
|
private sealed;
|
|
59
66
|
constructor(manifest: ExtensionManifest,
|
|
60
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
|
|
69
|
+
* throws ExtensionPermissionError and writes an audit entry.
|
|
70
|
+
*/
|
|
61
71
|
deny: (item: string) => never);
|
|
62
72
|
/** setup 结束后调用:此后 view / action 抛错。 */
|
|
63
73
|
seal(): void;
|
package/dist/ui/registry.js
CHANGED
|
@@ -31,7 +31,9 @@ export const uiFrame = (m) => `event: ui\ndata: ${JSON.stringify({ plugin: m.plu
|
|
|
31
31
|
/**
|
|
32
32
|
* 单个插件在 setup 期间注册的视图与动作:进程内的静态登记(同 `ctx.tools.register`),
|
|
33
33
|
* 不是跨请求的共享状态——各副本加载同一插件得到同样的登记。
|
|
34
|
-
* id
|
|
34
|
+
* The id must be in `permissions.ui.views` or `permissions.ui.components` (a component's server
|
|
35
|
+
* view, client extensions spec §5.2); `defineExtension` already cross-checked them with the
|
|
36
|
+
* manifest `ui`.
|
|
35
37
|
*/
|
|
36
38
|
export class PluginUiRegistration {
|
|
37
39
|
manifest;
|
|
@@ -40,14 +42,21 @@ export class PluginUiRegistration {
|
|
|
40
42
|
actions = new Map();
|
|
41
43
|
declaredViews;
|
|
42
44
|
contributions;
|
|
45
|
+
/** The ids of the manifest's `component` contributions: their permission is `ui.components`. */
|
|
46
|
+
components;
|
|
43
47
|
sealed = false;
|
|
44
48
|
constructor(manifest,
|
|
45
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
|
|
51
|
+
* throws ExtensionPermissionError and writes an audit entry.
|
|
52
|
+
*/
|
|
46
53
|
deny) {
|
|
47
54
|
this.manifest = manifest;
|
|
48
55
|
this.deny = deny;
|
|
49
|
-
|
|
56
|
+
const ui = manifest.permissions?.ui;
|
|
57
|
+
this.declaredViews = new Set([...(ui?.views ?? []), ...(ui?.components ?? [])]);
|
|
50
58
|
this.contributions = new Set((manifest.ui ?? []).map((c) => c.id));
|
|
59
|
+
this.components = new Set((manifest.ui ?? []).filter((c) => c.kind === "component").map((c) => c.id));
|
|
51
60
|
}
|
|
52
61
|
/** setup 结束后调用:此后 view / action 抛错。 */
|
|
53
62
|
seal() {
|
|
@@ -99,7 +108,9 @@ export class PluginUiRegistration {
|
|
|
99
108
|
throw new Error(`plugin ${name}: ui.${what} is only allowed during setup; setup has completed`);
|
|
100
109
|
}
|
|
101
110
|
if (typeof id !== "string" || !this.declaredViews.has(id)) {
|
|
102
|
-
|
|
111
|
+
// name the list the id belongs in: a component's server view is declared under components
|
|
112
|
+
const list = typeof id === "string" && this.components.has(id) ? "components" : "views";
|
|
113
|
+
this.deny(`permissions.ui.${list} ${String(id)}`);
|
|
103
114
|
}
|
|
104
115
|
if (typeof fn !== "function") {
|
|
105
116
|
throw new Error(`plugin ${name}: ui.${what} "${id}" handler must be a function`);
|
package/dist/ui/ui.schemas.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const ContributionParams: z.ZodObject<{
|
|
|
9
9
|
export declare const ContributionsQuery: z.ZodObject<{
|
|
10
10
|
slot: z.ZodOptional<z.ZodEnum<{
|
|
11
11
|
"agent.detail.section": "agent.detail.section";
|
|
12
|
+
"conversation.composer.frame": "conversation.composer.frame";
|
|
13
|
+
"conversation.empty": "conversation.empty";
|
|
12
14
|
"conversation.panel": "conversation.panel";
|
|
13
15
|
"conversation.properties.section": "conversation.properties.section";
|
|
14
16
|
"conversation.tool-result.card": "conversation.tool-result.card";
|
|
@@ -16,6 +18,8 @@ export declare const ContributionsQuery: z.ZodObject<{
|
|
|
16
18
|
"org.page": "org.page";
|
|
17
19
|
"org.settings.section": "org.settings.section";
|
|
18
20
|
"platform.page": "platform.page";
|
|
21
|
+
"shell.brand": "shell.brand";
|
|
22
|
+
"shell.overlay": "shell.overlay";
|
|
19
23
|
}>>;
|
|
20
24
|
}, z.core.$strip>;
|
|
21
25
|
export declare const ViewQuery: z.ZodObject<{
|
package/dist/ui/views.d.ts
CHANGED
|
@@ -24,14 +24,31 @@ export interface UiContributionItem {
|
|
|
24
24
|
/** From `UI_ICONS`; `puzzle` when the manifest names none. */
|
|
25
25
|
icon: UiIcon;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
export interface
|
|
27
|
+
/** A list slot of an arrangement listing: where its entry comes from and its items in order. */
|
|
28
|
+
export interface UiArrangementListSlot {
|
|
29
29
|
slot: UiSlotId;
|
|
30
30
|
source: "org" | "platform" | "none";
|
|
31
31
|
items: (UiContributionItem & {
|
|
32
32
|
hidden: boolean;
|
|
33
33
|
})[];
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* A single slot of an arrangement listing (client extensions spec §6.3): the contributions that may
|
|
37
|
+
* replace the built-in, the stored `use` (null = the built-in; it may name a key that is not a
|
|
38
|
+
* candidate any more, which renders the built-in) and whether it is the platform's (the org has no
|
|
39
|
+
* entry; always false on the platform's own listing).
|
|
40
|
+
*/
|
|
41
|
+
export interface UiArrangementSingleSlot {
|
|
42
|
+
slot: UiSlotId;
|
|
43
|
+
candidates: {
|
|
44
|
+
key: string;
|
|
45
|
+
title: UiContribution["title"];
|
|
46
|
+
}[];
|
|
47
|
+
use: string | null;
|
|
48
|
+
followsPlatform: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** One slot of an arrangement listing. */
|
|
51
|
+
export type UiArrangementSlot = UiArrangementListSlot | UiArrangementSingleSlot;
|
|
35
52
|
/** 嵌入应用的签发目标:已核对组织启用与对话访问。 */
|
|
36
53
|
export interface UiEmbedTarget {
|
|
37
54
|
plugin: string;
|
|
@@ -101,6 +118,8 @@ export declare function dropForeignOpens(view: UiView, panels: ReadonlySet<strin
|
|
|
101
118
|
* org; org: the role alone); refused is 404 `ui.not_found`, the same as not existing;
|
|
102
119
|
* - a view takes only the context id of its scope (`conversationId` / `agentId`): a missing one or
|
|
103
120
|
* one of another scope is 400 `validation_failed`;
|
|
121
|
+
* - a component's server view (`ctx.ui.view(<component id>)`) is served like a view of its slot;
|
|
122
|
+
* only its views carry `data` (a plain view's `data` is 502 `ui.invalid_view`);
|
|
104
123
|
* - 调用带 5 秒时限与中止信号:render 抛错 502 `ui.render_failed`、action 抛错 502 `ui.action_failed`、
|
|
105
124
|
* 超时 504 `ui.timeout`、返回非法视图 502
|
|
106
125
|
* `ui.invalid_view`,都记告警日志、不写对话日志(这些不是模型可见的内容)。
|
|
@@ -111,27 +130,35 @@ export declare class UiViews {
|
|
|
111
130
|
constructor(host: ExtensionHost, conversations: ConversationsService, previewers: PreviewerRegistry, db: KernelDb, log: Pick<Logger, "warn">, effective: Pick<EffectiveConfigs, "get" | "platform">);
|
|
112
131
|
/**
|
|
113
132
|
* The contributions the role may use in the org, of one slot or of every org-reachable slot:
|
|
114
|
-
* the extension enabled and loaded, a view's render registered
|
|
115
|
-
* ({@link canUseSlot}, so never `platform.page`);
|
|
116
|
-
* file previewers list those). Base order (spec
|
|
133
|
+
* the extension enabled and loaded, a view's render registered (a component is listed with or
|
|
134
|
+
* without a server view), the slot's `minRole` ({@link canUseSlot}, so never `platform.page`);
|
|
135
|
+
* no alias, no `file.preview` contribution (the file previewers list those). Base order (spec
|
|
136
|
+
* §5): extension name, then manifest order. A single slot lists at most its winner
|
|
137
|
+
* ({@link resolveSingleSlot} over what this role may use: none until one is chosen, and the web
|
|
138
|
+
* renders the built-in); display only, the routes that serve a contribution never read it. The
|
|
139
|
+
* winner is listed whether or not its client code may load (kill switch, per-plugin disable,
|
|
140
|
+
* client API support): the web renders the built-in when the org's client set has no such
|
|
141
|
+
* component, and any other consumer must do the same.
|
|
117
142
|
*/
|
|
118
143
|
contributions(orgId: string, role: OrgRole, slot?: UiSlotId): Promise<UiContributionItem[]>;
|
|
119
144
|
/** {@link contributions} before the arrangement: what the role may use, in base order. */
|
|
120
145
|
private orgBase;
|
|
121
146
|
/**
|
|
122
147
|
* The arrangement page of an org: every org-arrangeable slot with the contributions the role may
|
|
123
|
-
* use
|
|
148
|
+
* use: a list slot in effective order with the hidden ones in place, and where the entry comes
|
|
149
|
+
* from; a single slot with its candidates, the stored `use` and whether it follows the platform.
|
|
124
150
|
*/
|
|
125
151
|
arrangementView(orgId: string, role: OrgRole): Promise<UiArrangementSlot[]>;
|
|
126
152
|
/**
|
|
127
153
|
* The platform's arrangement page: every arrangeable slot with the contributions of every running
|
|
128
|
-
* extension (whatever its org enablement)
|
|
154
|
+
* extension (whatever its org enablement): a list slot ordered by the platform entry, hidden ones
|
|
155
|
+
* in place; a single slot with its candidates and the platform's `use`.
|
|
129
156
|
*/
|
|
130
157
|
platformArrangementView(): Promise<UiArrangementSlot[]>;
|
|
131
158
|
/**
|
|
132
159
|
* The `platform.page` contributions of the running extensions, for the admin console (super
|
|
133
|
-
* admins; no org enablement applies): no alias, a view's render registered
|
|
134
|
-
* {@link contributions}.
|
|
160
|
+
* admins; no org enablement applies): no alias, a view's render registered (a component is
|
|
161
|
+
* listed with or without a server view). Base order like {@link contributions}.
|
|
135
162
|
*/
|
|
136
163
|
platformContributions(): Promise<UiContributionItem[]>;
|
|
137
164
|
/**
|
package/dist/ui/views.js
CHANGED
|
@@ -20,7 +20,7 @@ import { readUiArrangement } from "../identity/settings.js";
|
|
|
20
20
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
21
21
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
22
22
|
import { readOrgSettings } from "../orgs/settings.service.js";
|
|
23
|
-
import { arrange, arrangeBySlot, effectiveEntry, ORG_ARRANGEABLE, orderItems, } from "./arrangement.js";
|
|
23
|
+
import { arrange, arrangeBySlot, effectiveEntry, isSingle, keyOf, ORG_ARRANGEABLE, orderItems, resolveSingleSlot, storedUse, } from "./arrangement.js";
|
|
24
24
|
import { callExtension } from "./call-extension.js";
|
|
25
25
|
import { canUseSlot, UUID_RE } from "./registry.js";
|
|
26
26
|
export { callExtension } from "./call-extension.js";
|
|
@@ -73,12 +73,14 @@ export function dropForeignOpens(view, panels) {
|
|
|
73
73
|
return { ...rest, ...(open !== undefined && ok(open) && { open }), blocks };
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* Lists contributions for a listing route: no alias, a view's render registered
|
|
77
|
-
*
|
|
76
|
+
* Lists contributions for a listing route: no alias, a view's render registered (an embed or a
|
|
77
|
+
* component is listed without one: a component's server view is optional); base order (spec §5):
|
|
78
|
+
* extension name, then manifest order (stable).
|
|
78
79
|
*/
|
|
79
80
|
function listed(entries) {
|
|
80
81
|
return entries
|
|
81
|
-
.filter((c) => c.contribution.aliasOf === undefined &&
|
|
82
|
+
.filter((c) => c.contribution.aliasOf === undefined &&
|
|
83
|
+
(c.contribution.kind === "embed" || c.contribution.kind === "component" || c.render))
|
|
82
84
|
.map((c, i) => ({ c, i }))
|
|
83
85
|
.sort((x, y) => (x.c.plugin === y.c.plugin ? x.i - y.i : x.c.plugin < y.c.plugin ? -1 : 1))
|
|
84
86
|
.map(({ c: { plugin, contribution: c, slot: s, after } }) => ({
|
|
@@ -92,6 +94,8 @@ function listed(entries) {
|
|
|
92
94
|
icon: c.icon ?? "puzzle",
|
|
93
95
|
}));
|
|
94
96
|
}
|
|
97
|
+
/** The candidates of a single slot in an arrangement listing. */
|
|
98
|
+
const candidatesOf = (items) => items.map((i) => ({ key: keyOf(i), title: i.title }));
|
|
95
99
|
/** The items in effective order, each marked hidden or not (hidden ones stay in place). */
|
|
96
100
|
function withHidden(items, entry) {
|
|
97
101
|
const hidden = new Set(entry.hidden);
|
|
@@ -107,12 +111,15 @@ const TOAST_MAX = 500;
|
|
|
107
111
|
const MAX_LOGGED_PROBLEMS = 10;
|
|
108
112
|
const notFound = () => new ApiError("ui.not_found", "UI contribution not found");
|
|
109
113
|
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
110
|
-
/**
|
|
111
|
-
|
|
114
|
+
/**
|
|
115
|
+
* 校验动作结果,返回问题列表(空 = 合法)。A new `view` may carry `data` only for a component
|
|
116
|
+
* (`component`).
|
|
117
|
+
*/
|
|
118
|
+
function actionResultProblems(r, component) {
|
|
112
119
|
if (!isObj(r))
|
|
113
120
|
return ["action result must be an object"];
|
|
114
121
|
if (r.view !== undefined)
|
|
115
|
-
return validateUiView(r.view).map((p) => `view: ${p}`);
|
|
122
|
+
return validateUiView(r.view, { component }).map((p) => `view: ${p}`);
|
|
116
123
|
if (r.toast === undefined)
|
|
117
124
|
return [];
|
|
118
125
|
if (!isObj(r.toast))
|
|
@@ -133,6 +140,8 @@ function actionResultProblems(r) {
|
|
|
133
140
|
* org; org: the role alone); refused is 404 `ui.not_found`, the same as not existing;
|
|
134
141
|
* - a view takes only the context id of its scope (`conversationId` / `agentId`): a missing one or
|
|
135
142
|
* one of another scope is 400 `validation_failed`;
|
|
143
|
+
* - a component's server view (`ctx.ui.view(<component id>)`) is served like a view of its slot;
|
|
144
|
+
* only its views carry `data` (a plain view's `data` is 502 `ui.invalid_view`);
|
|
136
145
|
* - 调用带 5 秒时限与中止信号:render 抛错 502 `ui.render_failed`、action 抛错 502 `ui.action_failed`、
|
|
137
146
|
* 超时 504 `ui.timeout`、返回非法视图 502
|
|
138
147
|
* `ui.invalid_view`,都记告警日志、不写对话日志(这些不是模型可见的内容)。
|
|
@@ -160,9 +169,15 @@ let UiViews = class UiViews {
|
|
|
160
169
|
}
|
|
161
170
|
/**
|
|
162
171
|
* The contributions the role may use in the org, of one slot or of every org-reachable slot:
|
|
163
|
-
* the extension enabled and loaded, a view's render registered
|
|
164
|
-
* ({@link canUseSlot}, so never `platform.page`);
|
|
165
|
-
* file previewers list those). Base order (spec
|
|
172
|
+
* the extension enabled and loaded, a view's render registered (a component is listed with or
|
|
173
|
+
* without a server view), the slot's `minRole` ({@link canUseSlot}, so never `platform.page`);
|
|
174
|
+
* no alias, no `file.preview` contribution (the file previewers list those). Base order (spec
|
|
175
|
+
* §5): extension name, then manifest order. A single slot lists at most its winner
|
|
176
|
+
* ({@link resolveSingleSlot} over what this role may use: none until one is chosen, and the web
|
|
177
|
+
* renders the built-in); display only, the routes that serve a contribution never read it. The
|
|
178
|
+
* winner is listed whether or not its client code may load (kill switch, per-plugin disable,
|
|
179
|
+
* client API support): the web renders the built-in when the org's client set has no such
|
|
180
|
+
* component, and any other consumer must do the same.
|
|
166
181
|
*/
|
|
167
182
|
async contributions(orgId, role, slot) {
|
|
168
183
|
const items = await this.orgBase(orgId, role, slot);
|
|
@@ -172,7 +187,16 @@ let UiViews = class UiViews {
|
|
|
172
187
|
this.d.orgArrangement(orgId),
|
|
173
188
|
this.d.platformArrangement(),
|
|
174
189
|
]);
|
|
175
|
-
|
|
190
|
+
const winners = new Map();
|
|
191
|
+
const winnerOf = (s) => {
|
|
192
|
+
if (!winners.has(s)) {
|
|
193
|
+
const available = new Set(items.filter((i) => i.slot === s).map(keyOf));
|
|
194
|
+
winners.set(s, resolveSingleSlot(s, org?.[s], platform[s], available));
|
|
195
|
+
}
|
|
196
|
+
return winners.get(s) ?? null;
|
|
197
|
+
};
|
|
198
|
+
const shown = items.filter((i) => !isSingle(i.slot) || keyOf(i) === winnerOf(i.slot));
|
|
199
|
+
return arrangeBySlot(shown, (s) => effectiveEntry(org, platform, s).entry);
|
|
176
200
|
}
|
|
177
201
|
/** {@link contributions} before the arrangement: what the role may use, in base order. */
|
|
178
202
|
async orgBase(orgId, role, slot) {
|
|
@@ -187,7 +211,8 @@ let UiViews = class UiViews {
|
|
|
187
211
|
}
|
|
188
212
|
/**
|
|
189
213
|
* The arrangement page of an org: every org-arrangeable slot with the contributions the role may
|
|
190
|
-
* use
|
|
214
|
+
* use: a list slot in effective order with the hidden ones in place, and where the entry comes
|
|
215
|
+
* from; a single slot with its candidates, the stored `use` and whether it follows the platform.
|
|
191
216
|
*/
|
|
192
217
|
async arrangementView(orgId, role) {
|
|
193
218
|
// the page is for people who arrange or at least see the org's UI, not for a guest
|
|
@@ -199,17 +224,18 @@ let UiViews = class UiViews {
|
|
|
199
224
|
this.d.platformArrangement(),
|
|
200
225
|
]);
|
|
201
226
|
return ORG_ARRANGEABLE.map((slot) => {
|
|
227
|
+
const items = base.filter((i) => i.slot === slot);
|
|
228
|
+
if (isSingle(slot)) {
|
|
229
|
+
return { slot, candidates: candidatesOf(items), ...storedUse(org, platform, slot) };
|
|
230
|
+
}
|
|
202
231
|
const { entry, source } = effectiveEntry(org, platform, slot);
|
|
203
|
-
return {
|
|
204
|
-
slot,
|
|
205
|
-
source,
|
|
206
|
-
items: withHidden(base.filter((i) => i.slot === slot), entry),
|
|
207
|
-
};
|
|
232
|
+
return { slot, source, items: withHidden(items, entry) };
|
|
208
233
|
});
|
|
209
234
|
}
|
|
210
235
|
/**
|
|
211
236
|
* The platform's arrangement page: every arrangeable slot with the contributions of every running
|
|
212
|
-
* extension (whatever its org enablement)
|
|
237
|
+
* extension (whatever its org enablement): a list slot ordered by the platform entry, hidden ones
|
|
238
|
+
* in place; a single slot with its candidates and the platform's `use`.
|
|
213
239
|
*/
|
|
214
240
|
async platformArrangementView() {
|
|
215
241
|
const [base, platform] = await Promise.all([
|
|
@@ -217,18 +243,19 @@ let UiViews = class UiViews {
|
|
|
217
243
|
this.d.platformArrangement(),
|
|
218
244
|
]);
|
|
219
245
|
return UI_ARRANGEABLE_SLOTS.map((slot) => {
|
|
246
|
+
const items = base.filter((i) => i.slot === slot);
|
|
247
|
+
if (isSingle(slot)) {
|
|
248
|
+
const { use } = storedUse(undefined, platform, slot);
|
|
249
|
+
return { slot, candidates: candidatesOf(items), use, followsPlatform: false };
|
|
250
|
+
}
|
|
220
251
|
const { entry, source } = effectiveEntry(undefined, platform, slot);
|
|
221
|
-
return {
|
|
222
|
-
slot,
|
|
223
|
-
source,
|
|
224
|
-
items: withHidden(base.filter((i) => i.slot === slot), entry),
|
|
225
|
-
};
|
|
252
|
+
return { slot, source, items: withHidden(items, entry) };
|
|
226
253
|
});
|
|
227
254
|
}
|
|
228
255
|
/**
|
|
229
256
|
* The `platform.page` contributions of the running extensions, for the admin console (super
|
|
230
|
-
* admins; no org enablement applies): no alias, a view's render registered
|
|
231
|
-
* {@link contributions}.
|
|
257
|
+
* admins; no org enablement applies): no alias, a view's render registered (a component is
|
|
258
|
+
* listed with or without a server view). Base order like {@link contributions}.
|
|
232
259
|
*/
|
|
233
260
|
async platformContributions() {
|
|
234
261
|
const items = listed(this.d.host.uiContributions().filter((c) => c.slot.id === "platform.page"));
|
|
@@ -289,7 +316,8 @@ let UiViews = class UiViews {
|
|
|
289
316
|
const { entry, args } = await this.resolve(t, "render");
|
|
290
317
|
const render = entry.render;
|
|
291
318
|
const view = await this.call(t, "render", (signal) => render({ ...args, signal }));
|
|
292
|
-
|
|
319
|
+
// `data` only from a component's view (client extensions spec §5.2)
|
|
320
|
+
this.check(t, "render", validateUiView(view, { component: entry.contribution.kind === "component" }));
|
|
293
321
|
return dropForeignOpens(view, this.panels(t.plugin));
|
|
294
322
|
}
|
|
295
323
|
/** 执行动作:调用插件 `action`(视图里的按钮 / 表单提交),校验返回的提示或新视图。 */
|
|
@@ -303,7 +331,7 @@ let UiViews = class UiViews {
|
|
|
303
331
|
...(input.payload !== undefined && { payload: input.payload }),
|
|
304
332
|
...(input.answer !== undefined && { answer: input.answer }),
|
|
305
333
|
}));
|
|
306
|
-
this.check(t, "action", actionResultProblems(r));
|
|
334
|
+
this.check(t, "action", actionResultProblems(r, entry.contribution.kind === "component"));
|
|
307
335
|
const res = r;
|
|
308
336
|
if (res.view !== undefined)
|
|
309
337
|
return { view: dropForeignOpens(res.view, this.panels(t.plugin)) };
|
|
@@ -338,7 +366,9 @@ let UiViews = class UiViews {
|
|
|
338
366
|
c.contribution.aliasOf === undefined &&
|
|
339
367
|
(need === "embed"
|
|
340
368
|
? c.contribution.kind === "embed"
|
|
341
|
-
:
|
|
369
|
+
: // a view, or a component's server view (served like a view of its slot)
|
|
370
|
+
(c.contribution.kind === "view" || c.contribution.kind === "component") &&
|
|
371
|
+
c[need] !== undefined));
|
|
342
372
|
if (t.scope === "platform") {
|
|
343
373
|
const found = platformEntry(entry);
|
|
344
374
|
return { entry: found, args: this.platformArgs(t, found) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/kernel",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.15",
|
|
4
4
|
"description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"yaml": "^2.9.1",
|
|
66
66
|
"yauzl": "3.4.0",
|
|
67
67
|
"zod": "^4.6.7",
|
|
68
|
-
"@capybara-hq/plugin-sdk": "^0.1.0-rc.
|
|
68
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.11"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@langchain/langgraph-checkpoint-validation": "^1.2.0",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@types/ws": "^8.18.2",
|
|
75
75
|
"@types/yauzl": "3.4.0",
|
|
76
76
|
"ws": "^8.22.0",
|
|
77
|
-
"@capybara-hq/testing": "0.1.0-rc.
|
|
77
|
+
"@capybara-hq/testing": "0.1.0-rc.14"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"build": "tsc -b"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Client extensions (spec 2026-11-09-client-extensions-design.md §9.1, §10.4).
|
|
2
|
+
-- extension_client_builds: the client files of an extension package, keyed by the package sha256
|
|
3
|
+
-- (the build id). The files live in object storage under `deployment/<object_prefix>`; the
|
|
4
|
+
-- integrity values delivered to browsers come from this row, never from the object store.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS extension_client_builds (
|
|
6
|
+
package_sha256 text PRIMARY KEY REFERENCES extension_packages(sha256) ON DELETE CASCADE,
|
|
7
|
+
plugin text NOT NULL,
|
|
8
|
+
entry text,
|
|
9
|
+
styles text[] NOT NULL,
|
|
10
|
+
assets text[] NOT NULL,
|
|
11
|
+
integrity jsonb NOT NULL,
|
|
12
|
+
sizes jsonb NOT NULL,
|
|
13
|
+
api_version int NOT NULL,
|
|
14
|
+
object_prefix text NOT NULL,
|
|
15
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- extension_client_reports: crash and load reports of client code, one row per
|
|
19
|
+
-- (plugin, build, org, contribution, kind); org_id is null for admin pages. Upserted, pruned after
|
|
20
|
+
-- 30 days.
|
|
21
|
+
CREATE TABLE IF NOT EXISTS extension_client_reports (
|
|
22
|
+
plugin text NOT NULL,
|
|
23
|
+
build text NOT NULL,
|
|
24
|
+
org_id text NULL,
|
|
25
|
+
contribution text NULL,
|
|
26
|
+
kind text NOT NULL CHECK (kind IN ('load', 'render', 'event', 'api', 'budget')),
|
|
27
|
+
count int NOT NULL DEFAULT 1,
|
|
28
|
+
first_at timestamptz NOT NULL DEFAULT now(),
|
|
29
|
+
last_at timestamptz NOT NULL DEFAULT now(),
|
|
30
|
+
last_message text NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE UNIQUE INDEX IF NOT EXISTS extension_client_reports_key_uq
|
|
34
|
+
ON extension_client_reports (
|
|
35
|
+
plugin, build, (coalesce(org_id, '')), (coalesce(contribution, '')), kind
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS extension_client_reports_last_at
|
|
39
|
+
ON extension_client_reports (last_at);
|
|
40
|
+
|
|
41
|
+
-- extension_replica_status.serving_sha256: the package a replica actually serves for the plugin
|
|
42
|
+
-- (the row's own sha256 when loaded; the older package still running when the row's version is
|
|
43
|
+
-- pending approval or failed; null when none runs). Client code follows the server code that runs
|
|
44
|
+
-- (spec §9.3): a build is delivered while a live replica serves its package.
|
|
45
|
+
ALTER TABLE extension_replica_status ADD COLUMN IF NOT EXISTS serving_sha256 text NULL;
|
|
46
|
+
|
|
47
|
+
-- catalog_item_versions.appearance_order: the keys of plugin.json's
|
|
48
|
+
-- suggestedAppearance.replacements in the author's order (a jsonb object does not keep key order),
|
|
49
|
+
-- so the suggestion is listed and applied in that order (spec §6.5); null without replacements.
|
|
50
|
+
ALTER TABLE catalog_item_versions ADD COLUMN IF NOT EXISTS appearance_order text[] NULL;
|