@frockbot/plugin-settings 0.0.0 → 0.1.0
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/frockbot.json +55 -0
- package/package.json +43 -6
- package/src/backend.test.ts +397 -0
- package/src/backend.ts +262 -0
- package/src/client/BotPanel.vue +57 -0
- package/src/client/BotSettingsSurface.vue +1076 -0
- package/src/client/BotSettingsTrigger.vue +26 -0
- package/src/client/ConnectionsSurface.vue +813 -0
- package/src/client/ModelsSurface.vue +419 -0
- package/src/client/PackageAccounts.vue +330 -0
- package/src/client/PackageCatalogSurface.vue +584 -0
- package/src/client/PackageSettingsForm.vue +150 -0
- package/src/client/PackageSettingsSection.vue +66 -0
- package/src/client/PluginsSurface.vue +412 -0
- package/src/client/PluginsTrigger.vue +62 -0
- package/src/client/UserProfileTrigger.vue +223 -0
- package/src/client/UserSettingsSurface.vue +242 -0
- package/src/client/assignment-operations.ts +22 -0
- package/src/client/bot-settings.test.ts +218 -0
- package/src/client/bot-settings.ts +150 -0
- package/src/client/index.test.ts +113 -0
- package/src/client/index.ts +78 -0
- package/src/client/package-settings.test.ts +63 -0
- package/src/client/package-settings.ts +77 -0
- package/src/client/package-surfaces.test.ts +147 -0
- package/src/client/package-surfaces.ts +93 -0
- package/src/client/user-display-name.test.ts +44 -0
- package/src/client/user-display-name.ts +16 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +3 -0
- package/src/user.test.ts +1277 -0
- package/src/user.ts +1221 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +30 -0
- package/README.md +0 -3
package/src/backend.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeBotIdV1,
|
|
3
|
+
decodeCompositionCommandReceiptV1,
|
|
4
|
+
decodeCompositionGenerationIdV1,
|
|
5
|
+
decodeCompositionGenerationListViewV1,
|
|
6
|
+
decodeCompositionGenerationViewV1,
|
|
7
|
+
decodeRevertCompositionCommandV1,
|
|
8
|
+
isPublicIdentifier,
|
|
9
|
+
MAX_COMPOSITION_GENERATION_PAGE_V1,
|
|
10
|
+
type CompositionCommandReceiptV1,
|
|
11
|
+
type CompositionGenerationListViewV1,
|
|
12
|
+
type CompositionGenerationViewV1,
|
|
13
|
+
type RevertCompositionCommandV1,
|
|
14
|
+
} from "@frockbot/configuration-core";
|
|
15
|
+
import {
|
|
16
|
+
decodeConnectionCommandIdV1,
|
|
17
|
+
decodeConnectionCommandReceiptV1,
|
|
18
|
+
decodeConnectionCommandV1,
|
|
19
|
+
type ConnectionCommandReceiptV1,
|
|
20
|
+
type ConnectionCommandV1,
|
|
21
|
+
} from "@frockbot/connection-core";
|
|
22
|
+
import type { Plugin } from "cordis";
|
|
23
|
+
|
|
24
|
+
export interface SettingsConnectionGatewayHost {
|
|
25
|
+
executeConnection(
|
|
26
|
+
userId: string,
|
|
27
|
+
command: ConnectionCommandV1,
|
|
28
|
+
): Promise<ConnectionCommandReceiptV1>;
|
|
29
|
+
lookupConnectionCommand(
|
|
30
|
+
userId: string,
|
|
31
|
+
packageId: string,
|
|
32
|
+
commandId: string,
|
|
33
|
+
): Promise<ConnectionCommandReceiptV1 | undefined>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The Bot Durable Object is the authority for its Composition generations; the
|
|
38
|
+
* gateway only carries the request. Every method is Bot-scoped and the
|
|
39
|
+
* authority proves directory membership before it answers.
|
|
40
|
+
*/
|
|
41
|
+
export interface SettingsCompositionGatewayHost {
|
|
42
|
+
listCompositionGenerations(
|
|
43
|
+
userId: string,
|
|
44
|
+
botId: string,
|
|
45
|
+
query: { limit: number; cursor?: string },
|
|
46
|
+
): Promise<CompositionGenerationListViewV1>;
|
|
47
|
+
getCompositionGeneration(
|
|
48
|
+
userId: string,
|
|
49
|
+
botId: string,
|
|
50
|
+
generationId: string,
|
|
51
|
+
): Promise<CompositionGenerationViewV1 | undefined>;
|
|
52
|
+
revertComposition(
|
|
53
|
+
userId: string,
|
|
54
|
+
botId: string,
|
|
55
|
+
command: RevertCompositionCommandV1,
|
|
56
|
+
): Promise<CompositionCommandReceiptV1>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type SettingsGatewayHost = SettingsConnectionGatewayHost &
|
|
60
|
+
SettingsCompositionGatewayHost;
|
|
61
|
+
|
|
62
|
+
export interface SettingsBackendRouteContribution {
|
|
63
|
+
packageId: string;
|
|
64
|
+
route(
|
|
65
|
+
request: Request,
|
|
66
|
+
url: URL,
|
|
67
|
+
context: { userId?: string; client: "browser" | "desktop" },
|
|
68
|
+
): Promise<Response | undefined>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function jsonError(status: number, message: string): Response {
|
|
72
|
+
return Response.json({ error: message }, { status });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const COMPOSITION_GENERATIONS =
|
|
76
|
+
/^\/api\/bots\/([^/]+)\/composition\/generations$/;
|
|
77
|
+
const COMPOSITION_GENERATION =
|
|
78
|
+
/^\/api\/bots\/([^/]+)\/composition\/generations\/([^/]+)$/;
|
|
79
|
+
const COMPOSITION_REVERT = /^\/api\/bots\/([^/]+)\/composition\/revert$/;
|
|
80
|
+
|
|
81
|
+
function pathSegment(value: string): string {
|
|
82
|
+
try {
|
|
83
|
+
return decodeURIComponent(value);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error("request path is invalid");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function compositionListQuery(url: URL): { limit: number; cursor?: string } {
|
|
90
|
+
const allowed = new Set(["limit", "cursor"]);
|
|
91
|
+
if (
|
|
92
|
+
[...url.searchParams.keys()].some((field) => !allowed.has(field)) ||
|
|
93
|
+
url.searchParams.getAll("limit").length > 1 ||
|
|
94
|
+
url.searchParams.getAll("cursor").length > 1
|
|
95
|
+
) {
|
|
96
|
+
throw new Error("Composition generation query is invalid");
|
|
97
|
+
}
|
|
98
|
+
const rawLimit = url.searchParams.get("limit");
|
|
99
|
+
const limit =
|
|
100
|
+
rawLimit === null ? MAX_COMPOSITION_GENERATION_PAGE_V1 : Number(rawLimit);
|
|
101
|
+
if (
|
|
102
|
+
!Number.isSafeInteger(limit) ||
|
|
103
|
+
limit < 1 ||
|
|
104
|
+
limit > MAX_COMPOSITION_GENERATION_PAGE_V1
|
|
105
|
+
) {
|
|
106
|
+
throw new Error("Composition generation query is invalid");
|
|
107
|
+
}
|
|
108
|
+
const cursor = url.searchParams.get("cursor");
|
|
109
|
+
if (cursor !== null && (cursor.length === 0 || cursor.length > 512)) {
|
|
110
|
+
throw new Error("Composition generation query is invalid");
|
|
111
|
+
}
|
|
112
|
+
return { limit, ...(cursor === null ? {} : { cursor }) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function routeComposition(
|
|
116
|
+
host: SettingsCompositionGatewayHost,
|
|
117
|
+
request: Request,
|
|
118
|
+
url: URL,
|
|
119
|
+
userId: string,
|
|
120
|
+
): Promise<Response | undefined> {
|
|
121
|
+
const list = url.pathname.match(COMPOSITION_GENERATIONS);
|
|
122
|
+
const single = url.pathname.match(COMPOSITION_GENERATION);
|
|
123
|
+
const revert = url.pathname.match(COMPOSITION_REVERT);
|
|
124
|
+
if (!list && !single && !revert) return undefined;
|
|
125
|
+
try {
|
|
126
|
+
const botId = decodeBotIdV1(
|
|
127
|
+
pathSegment((list ?? single ?? revert)![1]!),
|
|
128
|
+
"botId",
|
|
129
|
+
);
|
|
130
|
+
if (revert) {
|
|
131
|
+
if (request.method !== "POST") {
|
|
132
|
+
return jsonError(405, "method not allowed");
|
|
133
|
+
}
|
|
134
|
+
const command = decodeRevertCompositionCommandV1(await request.json());
|
|
135
|
+
if (command.botId !== botId) {
|
|
136
|
+
return jsonError(
|
|
137
|
+
400,
|
|
138
|
+
"Composition revert command does not match the request path",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return Response.json(
|
|
142
|
+
decodeCompositionCommandReceiptV1(
|
|
143
|
+
await host.revertComposition(userId, botId, command),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (request.method !== "GET") return jsonError(405, "method not allowed");
|
|
148
|
+
if (single) {
|
|
149
|
+
const generationId = decodeCompositionGenerationIdV1(
|
|
150
|
+
pathSegment(single[2]!),
|
|
151
|
+
);
|
|
152
|
+
const generation = await host.getCompositionGeneration(
|
|
153
|
+
userId,
|
|
154
|
+
botId,
|
|
155
|
+
generationId,
|
|
156
|
+
);
|
|
157
|
+
if (!generation) {
|
|
158
|
+
return jsonError(404, "Composition generation is unknown");
|
|
159
|
+
}
|
|
160
|
+
return Response.json(decodeCompositionGenerationViewV1(generation));
|
|
161
|
+
}
|
|
162
|
+
return Response.json(
|
|
163
|
+
decodeCompositionGenerationListViewV1(
|
|
164
|
+
await host.listCompositionGenerations(
|
|
165
|
+
userId,
|
|
166
|
+
botId,
|
|
167
|
+
compositionListQuery(url),
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return jsonError(
|
|
173
|
+
400,
|
|
174
|
+
error instanceof Error ? error.message : "Composition request failed",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createSettingsBackendContribution(
|
|
180
|
+
host: SettingsGatewayHost,
|
|
181
|
+
): SettingsBackendRouteContribution {
|
|
182
|
+
return {
|
|
183
|
+
packageId: "settings",
|
|
184
|
+
async route(request, url, context) {
|
|
185
|
+
if (!context.userId) return undefined;
|
|
186
|
+
const composition = await routeComposition(
|
|
187
|
+
host,
|
|
188
|
+
request,
|
|
189
|
+
url,
|
|
190
|
+
context.userId,
|
|
191
|
+
);
|
|
192
|
+
if (composition) return composition;
|
|
193
|
+
if (
|
|
194
|
+
url.pathname !== "/api/connections" &&
|
|
195
|
+
url.pathname !== "/api/connection-commands"
|
|
196
|
+
) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
if (url.pathname === "/api/connection-commands") {
|
|
200
|
+
if (request.method !== "GET")
|
|
201
|
+
return jsonError(405, "method not allowed");
|
|
202
|
+
const queryFields = [...url.searchParams.keys()];
|
|
203
|
+
const packageId = url.searchParams.get("packageId");
|
|
204
|
+
const commandId = url.searchParams.get("commandId");
|
|
205
|
+
if (
|
|
206
|
+
queryFields.length !== 2 ||
|
|
207
|
+
url.searchParams.getAll("packageId").length !== 1 ||
|
|
208
|
+
url.searchParams.getAll("commandId").length !== 1 ||
|
|
209
|
+
queryFields.some(
|
|
210
|
+
(field) => field !== "packageId" && field !== "commandId",
|
|
211
|
+
) ||
|
|
212
|
+
!isPublicIdentifier(packageId)
|
|
213
|
+
) {
|
|
214
|
+
return jsonError(400, "invalid Connection command lookup");
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const receipt = await host.lookupConnectionCommand(
|
|
218
|
+
context.userId,
|
|
219
|
+
packageId,
|
|
220
|
+
decodeConnectionCommandIdV1(commandId),
|
|
221
|
+
);
|
|
222
|
+
return Response.json(
|
|
223
|
+
receipt === undefined
|
|
224
|
+
? null
|
|
225
|
+
: decodeConnectionCommandReceiptV1(receipt),
|
|
226
|
+
);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return jsonError(
|
|
229
|
+
400,
|
|
230
|
+
error instanceof Error
|
|
231
|
+
? error.message
|
|
232
|
+
: "Connection command lookup failed",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (request.method !== "POST")
|
|
237
|
+
return jsonError(405, "method not allowed");
|
|
238
|
+
try {
|
|
239
|
+
const command = decodeConnectionCommandV1(await request.json());
|
|
240
|
+
return Response.json(
|
|
241
|
+
decodeConnectionCommandReceiptV1(
|
|
242
|
+
await host.executeConnection(context.userId, command),
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return jsonError(
|
|
247
|
+
400,
|
|
248
|
+
error instanceof Error ? error.message : "Connection command failed",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export namespace createSettingsBackendContribution {
|
|
256
|
+
export function plugin(
|
|
257
|
+
host: SettingsGatewayHost,
|
|
258
|
+
lifecycle: { mount(value: SettingsBackendRouteContribution): () => void },
|
|
259
|
+
): Plugin {
|
|
260
|
+
return () => lifecycle.mount(createSettingsBackendContribution(host));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
3
|
+
import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
|
|
4
|
+
import { UiAnchor } from "@frockbot/client-ui";
|
|
5
|
+
import { computed, inject, onMounted } from "vue";
|
|
6
|
+
|
|
7
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
8
|
+
if (!providedWeb) throw new Error("settings client services were not provided");
|
|
9
|
+
const web = providedWeb;
|
|
10
|
+
const botName = computed(
|
|
11
|
+
() => web.value.botSettings?.profile.name ?? "This Bot",
|
|
12
|
+
);
|
|
13
|
+
const computerLink = computed(() =>
|
|
14
|
+
settingsLinkV1({
|
|
15
|
+
anchor: "bot-info-computer",
|
|
16
|
+
botId: web.value.activeBotId,
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
onMounted(() => void web.value.loadBotSettings());
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<section class="bot-panel" aria-label="Bot panel">
|
|
25
|
+
<UiAnchor
|
|
26
|
+
anchor="bot-info-computer"
|
|
27
|
+
label="Computer"
|
|
28
|
+
:href="computerLink"
|
|
29
|
+
class="bot-panel__computer"
|
|
30
|
+
>
|
|
31
|
+
<k-slot name="frockbot.computer" />
|
|
32
|
+
<p>{{ botName }}'s screen</p>
|
|
33
|
+
</UiAnchor>
|
|
34
|
+
<k-slot name="frockbot.bot-panel-sections" />
|
|
35
|
+
</section>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<style scoped>
|
|
39
|
+
.bot-panel {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: 22px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bot-panel__computer {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
gap: 8px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.bot-panel__computer p {
|
|
52
|
+
margin: 0;
|
|
53
|
+
color: var(--frock-text-muted);
|
|
54
|
+
font-size: var(--frock-text-sm);
|
|
55
|
+
text-align: center;
|
|
56
|
+
}
|
|
57
|
+
</style>
|