wizards 0.4.3 → 0.4.5
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/apps/runtime/dist/agents/architect.js +32 -13
- package/apps/runtime/dist/agents/subscription.js +12 -12
- package/apps/runtime/dist/app.js +9 -5
- package/apps/runtime/dist/assistant/knows.js +2 -2
- package/apps/runtime/dist/authoring/example.js +21 -20
- package/apps/runtime/dist/authoring/guide.js +4 -2
- package/apps/runtime/dist/connectors/imap.js +4 -4
- package/apps/runtime/dist/connectors/index.js +40 -15
- package/apps/runtime/dist/connectors/plugin-connections.js +3 -2
- package/apps/runtime/dist/connectors/via-account.js +8 -6
- package/apps/runtime/dist/docs/dev/plugins/connections.md +1 -1
- package/apps/runtime/dist/docs/dev/plugins/reference.md +5 -1
- package/apps/runtime/dist/docs/dev/plugins/routes.md +35 -1
- package/apps/runtime/dist/docs/dev/plugins/server.md +27 -1
- package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -1
- package/apps/runtime/dist/documents/parse.js +5 -3
- package/apps/runtime/dist/engine/events.js +1 -1
- package/apps/runtime/dist/engine/input.js +28 -8
- package/apps/runtime/dist/engine/prepare.js +2 -2
- package/apps/runtime/dist/engine/requirements.js +12 -3
- package/apps/runtime/dist/engine/runner.js +34 -23
- package/apps/runtime/dist/engine/steps.js +6 -5
- package/apps/runtime/dist/engine/surface.js +2 -1
- package/apps/runtime/dist/engine/template.js +1 -1
- package/apps/runtime/dist/engine/types.js +2 -1
- package/apps/runtime/dist/film/project.js +1 -1
- package/apps/runtime/dist/film/skills.js +2 -2
- package/apps/runtime/dist/film/sounds.js +1 -1
- package/apps/runtime/dist/film/step.js +7 -4
- package/apps/runtime/dist/film/tools.js +1 -1
- package/apps/runtime/dist/geocode.js +1 -1
- package/apps/runtime/dist/harness/apple.js +0 -5
- package/apps/runtime/dist/harness/claude.js +2 -1
- package/apps/runtime/dist/harness/codex.js +2 -1
- package/apps/runtime/dist/harness/model.js +4 -3
- package/apps/runtime/dist/harness/terminal.js +4 -3
- package/apps/runtime/dist/manage.js +29 -0
- package/apps/runtime/dist/model-errors.js +8 -3
- package/apps/runtime/dist/models.js +58 -38
- package/apps/runtime/dist/plugins/channels.js +77 -0
- package/apps/runtime/dist/plugins/loader.js +9 -1
- package/apps/runtime/dist/plugins/public.js +16 -2
- package/apps/runtime/dist/plugins/registry.js +1 -0
- package/apps/runtime/dist/plugins/runs.js +2 -1
- package/apps/runtime/dist/routes/api.js +16 -5
- package/apps/runtime/dist/routes/lang.js +13 -0
- package/apps/runtime/dist/routes/plugin-public.js +65 -12
- package/apps/runtime/dist/routes/runs.js +12 -19
- package/apps/runtime/dist/routes/studio.js +40 -28
- package/apps/runtime/dist/services/access.js +4 -3
- package/apps/runtime/dist/services/architect.js +15 -4
- package/apps/runtime/dist/services/assistant-chats.js +2 -1
- package/apps/runtime/dist/services/brand.js +3 -2
- package/apps/runtime/dist/services/errors.js +7 -5
- package/apps/runtime/dist/services/knowledge-model.js +3 -2
- package/apps/runtime/dist/services/marketplace.js +6 -5
- package/apps/runtime/dist/services/package.js +10 -3
- package/apps/runtime/dist/services/profile.js +12 -0
- package/apps/runtime/dist/services/project-files.js +13 -12
- package/apps/runtime/dist/services/projects.js +11 -6
- package/apps/runtime/dist/services/runs.js +18 -11
- package/apps/runtime/dist/services/shares.js +2 -1
- package/apps/runtime/dist/services/site.js +21 -10
- package/apps/runtime/dist/services/space-categories.js +7 -6
- package/apps/runtime/dist/services/space-data.js +19 -16
- package/apps/runtime/dist/services/spaces.js +8 -7
- package/apps/runtime/dist/services/wizards.js +41 -20
- package/apps/runtime/dist/store/errors.js +2 -1
- package/apps/runtime/dist/store/index.js +21 -4
- package/apps/runtime/dist/store/shared-lists.js +2 -3
- package/apps/runtime/dist/tools/index.js +17 -2
- package/apps/runtime/dist/tools/plugin.js +3 -2
- package/apps/runtime/dist/widgets/render.js +1 -1
- package/apps/runtime/dist/widgets/step.js +2 -1
- package/apps/web/dist/assets/{AboutDialog-DWaBKvI6.js → AboutDialog-BCtnYRj2.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-BcM1gXPM.js → ChangelogDialog-DBjoIIHL.js} +1 -1
- package/apps/web/dist/assets/ChatView-BcyUYN0Q.js +10 -0
- package/apps/web/dist/assets/EditorPage-Ce1QD-DK.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-S1s1yRaP.js → FlowPreview-BtpAdyt-.js} +1 -1
- package/apps/web/dist/assets/{Harness-DILx5Edb.js → Harness-DJJ_yMLY.js} +2 -2
- package/apps/web/dist/assets/NewWizardChannels-BrYLMV6F.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-827zwhhW.js +7 -0
- package/apps/web/dist/assets/{OnboardingPage-xoGrKIJ-.js → OnboardingPage-jC6FidpF.js} +1 -1
- package/apps/web/dist/assets/ProjectPage-BYcM1-1H.js +1 -0
- package/apps/web/dist/assets/{PublicRunner-Cab2jUy6.js → PublicRunner-BofZl0MO.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-B4nIGkoj.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-Dx-BRCkY.js +4 -0
- package/apps/web/dist/assets/RunnerView-CDCp61aq.js +1 -0
- package/apps/web/dist/assets/SettingsPage-B49nLHwv.js +1 -0
- package/apps/web/dist/assets/SetupPage-4tzzHUq0.js +1 -0
- package/apps/web/dist/assets/SharePage-Bs4s73h4.js +1 -0
- package/apps/web/dist/assets/changelog-D-12LwZB.js +1 -0
- package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BueS7qO8.js} +1 -1
- package/apps/web/dist/assets/esm-CjGdsKU4.js +7 -0
- package/apps/web/dist/assets/index-BRDn-5v_.js +14 -0
- package/apps/web/dist/assets/index-mlnAPOdf.css +1 -0
- package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-CkXYnQux.js} +1 -1
- package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CHkEf-Dz.js} +1 -1
- package/apps/web/dist/assets/settings-menu-CrVpArM_.js +1 -0
- package/apps/web/dist/assets/ui-DrdqFaQt.js +134 -0
- package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-CzkkZqAo.js} +1 -1
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/mcp/flow.html +76 -76
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +98 -2
- package/node_modules/@engenty-wizards/plugin-sdk/dist/thread.js +6 -1
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -2
- package/node_modules/@engenty-wizards/shared/dist/definition.js +13 -5
- package/node_modules/@engenty-wizards/shared/dist/knowledge.js +1 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +11 -1
- package/node_modules/@engenty-wizards/shared/dist/run.js +11 -1
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +8 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +8 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/ChatView-D0RRIl15.js +0 -10
- package/apps/web/dist/assets/EditorPage-DrJ6e_2q.js +0 -3
- package/apps/web/dist/assets/NewWizardChannels-CEILDADp.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-DxOtK2E-.js +0 -7
- package/apps/web/dist/assets/ProjectPage-6E5z4nZA.js +0 -1
- package/apps/web/dist/assets/RunDrawer-B3FNYpgZ.js +0 -4
- package/apps/web/dist/assets/RunnerView-D6sGVnRy.js +0 -1
- package/apps/web/dist/assets/SettingsPage-DHtIqmOJ.js +0 -1
- package/apps/web/dist/assets/SetupPage-Cn9iQbJx.js +0 -1
- package/apps/web/dist/assets/SharePage-CMr72PeU.js +0 -1
- package/apps/web/dist/assets/changelog-DfmWTlav.js +0 -1
- package/apps/web/dist/assets/esm-BPfTI3lL.js +0 -7
- package/apps/web/dist/assets/index-B-D8UvL9.js +0 -14
- package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
- package/apps/web/dist/assets/settings-menu-BJm0CwOj.js +0 -1
- package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
|
@@ -19,7 +19,7 @@ export function placeLabel(result) {
|
|
|
19
19
|
* The address of a position, when the server has a geocoder (`GEOCODER_URL`). Calls go out one
|
|
20
20
|
* at a time and at most one a second — what a public Nominatim asks of its users.
|
|
21
21
|
*/
|
|
22
|
-
export function reverseGeocode(lat, lng, language = "
|
|
22
|
+
export function reverseGeocode(lat, lng, language = "en") {
|
|
23
23
|
if (!env.geocoderUrl) {
|
|
24
24
|
return Promise.resolve(null);
|
|
25
25
|
}
|
|
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { release } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { noteText } from "@engenty-wizards/shared/run";
|
|
6
5
|
import { nanoid } from "nanoid";
|
|
7
6
|
import { env } from "../env.js";
|
|
8
7
|
import { ffmpeg, hasFfmpeg } from "../media/ffmpeg.js";
|
|
@@ -103,10 +102,6 @@ export function appleUnavailableNote(status) {
|
|
|
103
102
|
return { code: "appleDevice" };
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
|
-
/** What to tell the person when Apple Intelligence cannot answer, in German (the settings). */
|
|
107
|
-
export function appleUnavailable(status) {
|
|
108
|
-
return noteText(appleUnavailableNote(status), "de");
|
|
109
|
-
}
|
|
110
105
|
/**
|
|
111
106
|
* The failure the helper reports, as the person should read it: a note, so a run shows it in the
|
|
112
107
|
* wizard's language.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
1
2
|
import { resolveEnv, run } from "./env.js";
|
|
2
3
|
import { failureOf, HarnessModel, lastJson, runClient, } from "./model.js";
|
|
3
4
|
/**
|
|
@@ -142,5 +143,5 @@ export const claude = {
|
|
|
142
143
|
},
|
|
143
144
|
login: { args: ["auth", "login"], interactive: false },
|
|
144
145
|
model: (alias, effort) => new ClaudeCodeModel(claude, alias || "sonnet", effort),
|
|
145
|
-
exhausted: "Das Anthropic-Konto, mit dem Claude Code angemeldet ist, hat kein Guthaben. Melde Claude Code mit deinem Abo an: in den Einstellungen unter „Modelle“.",
|
|
146
|
+
exhausted: bilingual("Das Anthropic-Konto, mit dem Claude Code angemeldet ist, hat kein Guthaben. Melde Claude Code mit deinem Abo an: in den Einstellungen unter „Modelle“.", "The Anthropic account Claude Code is signed in with has no credit. Sign Claude Code in with your subscription: under “Models” in the settings."),
|
|
146
147
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
4
5
|
import { resolveEnv, run } from "./env.js";
|
|
5
6
|
import { failureOf, HarnessImageModel, HarnessModel, runClient, } from "./model.js";
|
|
6
7
|
/**
|
|
@@ -224,5 +225,5 @@ export const codex = {
|
|
|
224
225
|
login: { args: ["login"], interactive: false },
|
|
225
226
|
model: (alias, effort) => new CodexModel(codex, alias || "default", effort),
|
|
226
227
|
image: { alias: "image", model: (alias) => new CodexImageModel(codex, alias || "image") },
|
|
227
|
-
exhausted: "Das Kontingent des ChatGPT-Kontos, mit dem Codex angemeldet ist, ist aufgebraucht. Später noch einmal versuchen oder einen anderen Client wählen.",
|
|
228
|
+
exhausted: bilingual("Das Kontingent des ChatGPT-Kontos, mit dem Codex angemeldet ist, ist aufgebraucht. Später noch einmal versuchen oder einen anderen Client wählen.", "The quota of the ChatGPT account Codex is signed in with is used up. Try again later or pick another client."),
|
|
228
229
|
};
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
4
5
|
import { nanoid } from "nanoid";
|
|
5
6
|
import { env } from "../env.js";
|
|
6
7
|
import { openBridge } from "../mcp/bridge.js";
|
|
7
8
|
import { ModelUnavailableError } from "../model-errors.js";
|
|
8
9
|
import { extensionOf, jsonInstruction, parseJsonAnswer, renderPrompt } from "./prompt.js";
|
|
9
10
|
const workDir = join(env.dataDir, "harness");
|
|
10
|
-
export const signedOut = (h) => `${h.name} ist auf diesem Gerät nicht angemeldet. Melde dich in den Einstellungen unter „Modelle“ an
|
|
11
|
-
export const notInstalled = (h) => `${h.name} ist auf diesem Gerät nicht installiert. Installiere es in den Einstellungen unter „Modelle
|
|
11
|
+
export const signedOut = (h) => bilingual(`${h.name} ist auf diesem Gerät nicht angemeldet. Melde dich in den Einstellungen unter „Modelle“ an.`, `${h.name} is not signed in on this device. Sign in under “Models” in the settings.`);
|
|
12
|
+
export const notInstalled = (h) => bilingual(`${h.name} ist auf diesem Gerät nicht installiert. Installiere es in den Einstellungen unter „Modelle“.`, `${h.name} is not installed on this device. Install it under “Models” in the settings.`);
|
|
12
13
|
export const exhausted = (h) => h.exhausted ??
|
|
13
|
-
`Das Konto, mit dem ${h.name} angemeldet ist, hat kein Guthaben oder Kontingent mehr
|
|
14
|
+
bilingual(`Das Konto, mit dem ${h.name} angemeldet ist, hat kein Guthaben oder Kontingent mehr.`, `The account ${h.name} is signed in with has no credit or quota left.`);
|
|
14
15
|
/** The failure a client reports, as the person should read it. */
|
|
15
16
|
export function failureOf(h, detail) {
|
|
16
17
|
// The client's own words stay in the log: the sentence below sums them up for the person.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accessSync, chmodSync, constants } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
4
5
|
import { nanoid } from "nanoid";
|
|
5
6
|
import { ServiceError } from "../services/errors.js";
|
|
6
7
|
const BUFFER = 200_000;
|
|
@@ -36,7 +37,7 @@ async function ptyModule() {
|
|
|
36
37
|
return { spawn };
|
|
37
38
|
}
|
|
38
39
|
catch {
|
|
39
|
-
throw new ServiceError("refused", "Das eingebaute Terminal ist auf diesem Gerät nicht verfügbar. Führe den Befehl in deinem eigenen Terminal aus.");
|
|
40
|
+
throw new ServiceError("refused", bilingual("Das eingebaute Terminal ist auf diesem Gerät nicht verfügbar. Führe den Befehl in deinem eigenen Terminal aus.", "The built-in terminal is not available on this device. Run the command in your own terminal."));
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
function emit(session, event) {
|
|
@@ -84,7 +85,7 @@ export async function openTerminal(opts) {
|
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
catch (err) {
|
|
87
|
-
throw new ServiceError("refused", `Der Befehl konnte nicht starten: ${err.message}`);
|
|
88
|
+
throw new ServiceError("refused", bilingual(`Der Befehl konnte nicht starten: ${err.message}`, `The command could not start: ${err.message}`));
|
|
88
89
|
}
|
|
89
90
|
const session = {
|
|
90
91
|
id,
|
|
@@ -146,7 +147,7 @@ export async function openTerminal(opts) {
|
|
|
146
147
|
function sessionOf(id) {
|
|
147
148
|
const session = sessions.get(id);
|
|
148
149
|
if (!session) {
|
|
149
|
-
throw new ServiceError("not_found", "Dieses Terminal gibt es nicht mehr.");
|
|
150
|
+
throw new ServiceError("not_found", bilingual("Dieses Terminal gibt es nicht mehr.", "This terminal is gone."));
|
|
150
151
|
}
|
|
151
152
|
return session;
|
|
152
153
|
}
|
|
@@ -187,6 +187,35 @@ export async function publicPlans(base) {
|
|
|
187
187
|
export function bookUsage(input) {
|
|
188
188
|
return call("POST", "/v1/usage", input);
|
|
189
189
|
}
|
|
190
|
+
const channels = new Map();
|
|
191
|
+
/** The tenant's numbers, cached for a minute. */
|
|
192
|
+
export async function tenantChannels(id) {
|
|
193
|
+
const hit = channels.get(id);
|
|
194
|
+
if (hit && Date.now() - hit.at < 60_000) {
|
|
195
|
+
return hit.value;
|
|
196
|
+
}
|
|
197
|
+
const value = await call("GET", `/v1/tenants/${encodeURIComponent(id)}/channels`);
|
|
198
|
+
channels.set(id, { at: Date.now(), value });
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
/** Forgets what the Manage-App said of the tenant's numbers: the next `tenantChannels` asks. */
|
|
202
|
+
export function forgetChannels(id) {
|
|
203
|
+
channels.delete(id);
|
|
204
|
+
}
|
|
205
|
+
/** A person of the tenant asks for its number; what the Manage-App answers with is the request. */
|
|
206
|
+
export async function requestChannelNumber(id, input) {
|
|
207
|
+
channels.delete(id);
|
|
208
|
+
return call("POST", `/v1/tenants/${encodeURIComponent(id)}/channels/requests`, input);
|
|
209
|
+
}
|
|
210
|
+
/** The tenant takes its open request back. */
|
|
211
|
+
export async function withdrawChannelRequest(id) {
|
|
212
|
+
channels.delete(id);
|
|
213
|
+
return call("DELETE", `/v1/tenants/${encodeURIComponent(id)}/channels/requests`);
|
|
214
|
+
}
|
|
215
|
+
/** What a tenant's channel used, priced by the Manage-App: texts by segment, calls by minute. */
|
|
216
|
+
export function bookChannelUsage(id, input) {
|
|
217
|
+
return call("POST", `/v1/tenants/${encodeURIComponent(id)}/channels/usage`, input);
|
|
218
|
+
}
|
|
190
219
|
/** A push to the mobile app on one device; the Manage-App holds the APNs and FCM keys. */
|
|
191
220
|
export function pushToDevice(input) {
|
|
192
221
|
return call("POST", "/v1/push", input);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { noteText } from "@engenty-wizards/shared/run";
|
|
2
2
|
/**
|
|
3
|
-
* A failure the person reads, given as a note of `RUN_NOTES`
|
|
4
|
-
* the wizard's language,
|
|
3
|
+
* A failure the person reads, given as a note of `RUN_NOTES` (or `bilingual` words) where it has
|
|
4
|
+
* one: a run shows it in the wizard's language, a response in the asker's, and the message keeps
|
|
5
|
+
* the English line for the log. Free text has no note.
|
|
5
6
|
*/
|
|
6
7
|
export class NotedError extends Error {
|
|
7
8
|
note;
|
|
8
9
|
constructor(said) {
|
|
9
|
-
super(typeof said === "string" ? said : noteText(said, "
|
|
10
|
+
super(typeof said === "string" ? said : noteText(said, "en"));
|
|
10
11
|
this.note = typeof said === "string" ? null : said;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -17,6 +18,10 @@ export class ModelUnavailableError extends NotedError {
|
|
|
17
18
|
export function noteOf(err) {
|
|
18
19
|
return err instanceof NotedError ? err.note : null;
|
|
19
20
|
}
|
|
21
|
+
/** What a failure says, to pass on: its note where it has one, else its message. */
|
|
22
|
+
export function saidOf(err) {
|
|
23
|
+
return noteOf(err) ?? err?.message ?? String(err);
|
|
24
|
+
}
|
|
20
25
|
/** A failure in a language: its note where it has one, else its message as it is. */
|
|
21
26
|
export function errorText(err, lang) {
|
|
22
27
|
const note = noteOf(err);
|
|
@@ -8,15 +8,16 @@ import { createOpenAI } from "@ai-sdk/openai";
|
|
|
8
8
|
import { createReplicate } from "@ai-sdk/replicate";
|
|
9
9
|
import { MODEL_CLASSES, TEXT_CLASSES, } from "@engenty-wizards/shared/definition";
|
|
10
10
|
import { creditsModelOf, isCreditsRef, PROVIDER_IDS, providerInfo, providerOfRef, } from "@engenty-wizards/shared/providers";
|
|
11
|
+
import { bilingual, saidText } from "@engenty-wizards/shared/run";
|
|
11
12
|
import { generateSpeech, generateText, } from "ai";
|
|
12
13
|
import { accountToken, linkedAccount } from "./auth/account.js";
|
|
13
14
|
import { DeviceModel, deviceThinks } from "./engine/device.js";
|
|
14
15
|
import { env } from "./env.js";
|
|
15
|
-
import { appleModel, appleStatus, appleTranscriptionModel,
|
|
16
|
+
import { appleModel, appleStatus, appleTranscriptionModel, appleUnavailableNote, } from "./harness/apple.js";
|
|
16
17
|
import { detectHarness, HarnessModel, harness, isHarnessVendor, notInstalled, signedOut, } from "./harness/index.js";
|
|
17
18
|
import { managed } from "./manage.js";
|
|
18
19
|
import { hasFfmpeg } from "./media/ffmpeg.js";
|
|
19
|
-
import { ModelUnavailableError } from "./model-errors.js";
|
|
20
|
+
import { ModelUnavailableError, saidOf } from "./model-errors.js";
|
|
20
21
|
import { seal, unseal } from "./secrets/crypto.js";
|
|
21
22
|
import { vaultDelete, vaultGet, vaultSet } from "./secrets/vault.js";
|
|
22
23
|
import { readSetting, writeSetting } from "./settings.js";
|
|
@@ -231,27 +232,29 @@ function ownRef(cfg, cls) {
|
|
|
231
232
|
// The defaults name gateway models; without a gateway key nothing reaches them.
|
|
232
233
|
return reachable(cfg, env.models[cls]) ? env.models[cls] : "";
|
|
233
234
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
235
|
+
/** What a class makes, to say that nothing makes it. */
|
|
236
|
+
const MAKES = {
|
|
237
|
+
classifier: { de: "Text", en: "text" },
|
|
238
|
+
standard: { de: "Text", en: "text" },
|
|
239
|
+
high: { de: "Text", en: "text" },
|
|
240
|
+
highest: { de: "Text", en: "text" },
|
|
241
|
+
image: { de: "Bilder", en: "images" },
|
|
242
|
+
video: { de: "Videos", en: "videos" },
|
|
243
|
+
speech: { de: "Sprachausgabe", en: "speech" },
|
|
244
|
+
audio: { de: "Sprachnotizen", en: "voice notes" },
|
|
243
245
|
};
|
|
244
246
|
/** Why a class has no way: the provider picked for it lacks its key, or nothing is set up. */
|
|
245
247
|
function missingText(cfg, cls) {
|
|
246
248
|
const bound = cfg.settings.bindings[cls];
|
|
247
249
|
if (isCreditsRef(bound)) {
|
|
248
|
-
return "Dafür ist das Guthaben gewählt, aber kein Konto angemeldet.";
|
|
250
|
+
return bilingual("Dafür ist das Guthaben gewählt, aber kein Konto angemeldet.", "The credits are picked for this, but no account is signed in.");
|
|
249
251
|
}
|
|
252
|
+
const makes = MAKES[cls];
|
|
250
253
|
const provider = bound ? providerInfo(providerOfRef(bound) ?? "") : null;
|
|
251
254
|
if (provider) {
|
|
252
|
-
return
|
|
255
|
+
return bilingual(`Für ${makes.de} ist ${provider.name} gewählt, aber kein API Key hinterlegt.`, `${provider.name} is picked for ${makes.en}, but no API key is set.`);
|
|
253
256
|
}
|
|
254
|
-
return
|
|
257
|
+
return bilingual(`Für ${makes.de} ist kein Modell eingerichtet.`, `No model is set up for ${makes.en}.`);
|
|
255
258
|
}
|
|
256
259
|
/**
|
|
257
260
|
* The way to the credits. On a Manage-App's runtime always; alone, through the linked account —
|
|
@@ -271,7 +274,7 @@ async function creditAccess(cfg, chosen) {
|
|
|
271
274
|
const token = await accountToken();
|
|
272
275
|
if (!token) {
|
|
273
276
|
if (chosen) {
|
|
274
|
-
throw new ModelUnavailableError("Das Konto ist nicht mehr angemeldet. Bitte in den Einstellungen neu anmelden.");
|
|
277
|
+
throw new ModelUnavailableError(bilingual("Das Konto ist nicht mehr angemeldet. Bitte in den Einstellungen neu anmelden.", "The account is no longer signed in. Please sign in again in the settings."));
|
|
275
278
|
}
|
|
276
279
|
return null;
|
|
277
280
|
}
|
|
@@ -298,7 +301,7 @@ async function routeOf(cls) {
|
|
|
298
301
|
function need(cfg, name) {
|
|
299
302
|
const key = cfg.keys[name];
|
|
300
303
|
if (!key) {
|
|
301
|
-
throw new ModelUnavailableError(`Für ${providerInfo(name)?.name ?? name} ist kein API Key hinterlegt.`);
|
|
304
|
+
throw new ModelUnavailableError(bilingual(`Für ${providerInfo(name)?.name ?? name} ist kein API Key hinterlegt.`, `No API key is set for ${providerInfo(name)?.name ?? name}.`));
|
|
302
305
|
}
|
|
303
306
|
return key;
|
|
304
307
|
}
|
|
@@ -409,7 +412,7 @@ async function ownLanguageModel(cfg, ref, meta) {
|
|
|
409
412
|
};
|
|
410
413
|
}
|
|
411
414
|
}
|
|
412
|
-
throw new ModelUnavailableError(`Für das Modell "${ref}" ist kein Zugang eingerichtet. Bitte in den Einstellungen einen API Key hinterlegen oder ein Konto anmelden.`);
|
|
415
|
+
throw new ModelUnavailableError(bilingual(`Für das Modell "${ref}" ist kein Zugang eingerichtet. Bitte in den Einstellungen einen API Key hinterlegen oder ein Konto anmelden.`, `There is no access set up for the model "${ref}". Please add an API key or sign in to an account in the settings.`));
|
|
413
416
|
}
|
|
414
417
|
/**
|
|
415
418
|
* The language model a text class runs on. A run watched from a device that offers the class
|
|
@@ -489,7 +492,7 @@ export async function imageModel(meta = {}) {
|
|
|
489
492
|
return own(createOpenAI({ apiKey: cfg.keys.openai }).image(aliasOf(id)));
|
|
490
493
|
}
|
|
491
494
|
}
|
|
492
|
-
throw new ModelUnavailableError("Für Bilder ist kein Modell eingerichtet.");
|
|
495
|
+
throw new ModelUnavailableError(bilingual("Für Bilder ist kein Modell eingerichtet.", "No model is set up for images."));
|
|
493
496
|
}
|
|
494
497
|
export async function videoModel(meta = {}) {
|
|
495
498
|
const route = await routeOf("video");
|
|
@@ -518,7 +521,7 @@ export async function videoModel(meta = {}) {
|
|
|
518
521
|
return own(createGateway({ apiKey: cfg.keys.gateway }).videoModel(id), true);
|
|
519
522
|
}
|
|
520
523
|
}
|
|
521
|
-
throw new ModelUnavailableError("Für Videos ist kein Modell eingerichtet.");
|
|
524
|
+
throw new ModelUnavailableError(bilingual("Für Videos ist kein Modell eingerichtet.", "No model is set up for videos."));
|
|
522
525
|
}
|
|
523
526
|
/** The model that reads a text aloud. */
|
|
524
527
|
export async function speechModel(meta = {}) {
|
|
@@ -526,7 +529,7 @@ export async function speechModel(meta = {}) {
|
|
|
526
529
|
if (route.kind === "credits") {
|
|
527
530
|
const resolved = await viaCredits(route, "speech", meta, (c, id) => c.speechModel(id));
|
|
528
531
|
if (!resolved.ref) {
|
|
529
|
-
throw new ModelUnavailableError("Für Sprachausgabe ist kein Modell eingerichtet.");
|
|
532
|
+
throw new ModelUnavailableError(bilingual("Für Sprachausgabe ist kein Modell eingerichtet.", "No model is set up for speech."));
|
|
530
533
|
}
|
|
531
534
|
return resolved;
|
|
532
535
|
}
|
|
@@ -557,7 +560,7 @@ export async function speechModel(meta = {}) {
|
|
|
557
560
|
return own(createOpenAI({ apiKey: cfg.keys.openai }).speech(aliasOf(id)));
|
|
558
561
|
}
|
|
559
562
|
}
|
|
560
|
-
throw new ModelUnavailableError("Für Sprachausgabe ist kein Modell eingerichtet.");
|
|
563
|
+
throw new ModelUnavailableError(bilingual("Für Sprachausgabe ist kein Modell eingerichtet.", "No model is set up for speech."));
|
|
561
564
|
}
|
|
562
565
|
/**
|
|
563
566
|
* What turns a voice note into text: a transcription model (ElevenLabs Scribe, Whisper), or a
|
|
@@ -708,7 +711,7 @@ export function freeTierRefusal(err) {
|
|
|
708
711
|
}
|
|
709
712
|
/** What the person reads when Vercel's free tier leaves a model out, and how to fix it. */
|
|
710
713
|
export function freeTierText(model) {
|
|
711
|
-
return `Dein AI Gateway Key ist bei Vercel im kostenlosen Tarif, und der schließt ${model ? `„${model}“` : "dieses Modell"} aus. Abhilfe: bei Vercel Guthaben aufladen (${GATEWAY_TOP_UP}) oder unter Einstellungen → Modelle dafür einen anderen Weg wählen
|
|
714
|
+
return bilingual(`Dein AI Gateway Key ist bei Vercel im kostenlosen Tarif, und der schließt ${model ? `„${model}“` : "dieses Modell"} aus. Abhilfe: bei Vercel Guthaben aufladen (${GATEWAY_TOP_UP}) oder unter Einstellungen → Modelle dafür einen anderen Weg wählen.`, `Your AI Gateway key is on Vercel's free tier, which leaves out ${model ? `“${model}”` : "this model"}. To fix it: top up at Vercel (${GATEWAY_TOP_UP}) or pick another way for it under Settings → Models.`);
|
|
712
715
|
}
|
|
713
716
|
/** What a key was answered for a model: refusals are asked again soon, as a top-up lifts them. */
|
|
714
717
|
const gatewayProbes = new Map();
|
|
@@ -762,7 +765,7 @@ const CONVERSATION_MODEL = env.conversationModel || "gpt-realtime";
|
|
|
762
765
|
export async function conversationAccess() {
|
|
763
766
|
const cfg = await config();
|
|
764
767
|
if (!cfg.keys.openai) {
|
|
765
|
-
throw new ModelUnavailableError("Für Sprachgespräche ist kein OpenAI API Key hinterlegt (Einstellungen → Modelle).");
|
|
768
|
+
throw new ModelUnavailableError(bilingual("Für Sprachgespräche ist kein OpenAI API Key hinterlegt (Einstellungen → Modelle).", "No OpenAI API key is set for voice conversations (Settings → Models)."));
|
|
766
769
|
}
|
|
767
770
|
return { apiKey: cfg.keys.openai, model: CONVERSATION_MODEL };
|
|
768
771
|
}
|
|
@@ -799,11 +802,11 @@ export async function classProblem(cls) {
|
|
|
799
802
|
const named = creditsModelOf((await config()).settings.bindings[cls]);
|
|
800
803
|
if (named) {
|
|
801
804
|
return catalog?.models && !catalog.models.some((m) => m.id === named)
|
|
802
|
-
? "Dieses Modell gibt es beim Konto nicht."
|
|
805
|
+
? bilingual("Dieses Modell gibt es beim Konto nicht.", "The account has no such model.")
|
|
803
806
|
: null;
|
|
804
807
|
}
|
|
805
808
|
return catalog?.classes && !catalog.classes[cls]
|
|
806
|
-
? "Dafür ist beim Konto kein Modell eingerichtet."
|
|
809
|
+
? bilingual("Dafür ist beim Konto kein Modell eingerichtet.", "The account has no model set up for this.")
|
|
807
810
|
: null;
|
|
808
811
|
}
|
|
809
812
|
const client = harness(resolved.vendor);
|
|
@@ -814,7 +817,7 @@ export async function classProblem(cls) {
|
|
|
814
817
|
}
|
|
815
818
|
catch (err) {
|
|
816
819
|
if (err instanceof ModelUnavailableError) {
|
|
817
|
-
return err
|
|
820
|
+
return saidOf(err);
|
|
818
821
|
}
|
|
819
822
|
throw err;
|
|
820
823
|
}
|
|
@@ -830,7 +833,7 @@ export async function filmClient(cls) {
|
|
|
830
833
|
const client = managed ? null : harness(cfg.settings.source);
|
|
831
834
|
if (!client || !FILM_CLIENTS.has(client.id)) {
|
|
832
835
|
return {
|
|
833
|
-
problem: "Filme macht Claude Code auf diesem Computer mit deinem Abo. Richte es unter „Modelle“ als KI-Abo ein.",
|
|
836
|
+
problem: bilingual("Filme macht Claude Code auf diesem Computer mit deinem Abo. Richte es unter „Modelle“ als KI-Abo ein.", "Films are made by Claude Code on this computer with your subscription. Set it up as an AI subscription under “Models”."),
|
|
834
837
|
};
|
|
835
838
|
}
|
|
836
839
|
const status = await detectHarness(client.id);
|
|
@@ -885,7 +888,7 @@ export async function classWays() {
|
|
|
885
888
|
}
|
|
886
889
|
let problem = null;
|
|
887
890
|
const access = await creditAccess(cfg, creditsChosen(cfg, cls)).catch((err) => {
|
|
888
|
-
problem = err
|
|
891
|
+
problem = saidOf(err);
|
|
889
892
|
return null;
|
|
890
893
|
});
|
|
891
894
|
ways[cls] = access
|
|
@@ -910,25 +913,32 @@ export async function classWays() {
|
|
|
910
913
|
async function appleProblem(cls) {
|
|
911
914
|
const status = await appleStatus();
|
|
912
915
|
if (cls !== "audio") {
|
|
913
|
-
return status.available ? null :
|
|
916
|
+
return status.available ? null : appleUnavailableNote(status);
|
|
914
917
|
}
|
|
915
918
|
if (!status.supported || status.reason === "missing") {
|
|
916
|
-
return
|
|
919
|
+
return appleUnavailableNote(status);
|
|
917
920
|
}
|
|
918
921
|
return (await hasFfmpeg())
|
|
919
922
|
? null
|
|
920
|
-
: "Aufnahmen aus Chrome oder Firefox (WebM, Ogg) schreibt dieser Mac erst mit ffmpeg auf; Safari und die App brauchen es nicht. ffmpeg installieren, z. B. mit „brew install ffmpeg“.";
|
|
923
|
+
: bilingual("Aufnahmen aus Chrome oder Firefox (WebM, Ogg) schreibt dieser Mac erst mit ffmpeg auf; Safari und die App brauchen es nicht. ffmpeg installieren, z. B. mit „brew install ffmpeg“.", "Recordings from Chrome or Firefox (WebM, Ogg) need ffmpeg before this Mac can write them down; Safari and the app do not. Install ffmpeg, e.g. with “brew install ffmpeg”.");
|
|
921
924
|
}
|
|
922
925
|
/** At start the ways are asked once, so a model the key cannot reach shows before a run meets it. */
|
|
923
926
|
export async function checkModelsAtStart() {
|
|
924
927
|
for (const [cls, way] of Object.entries(await classWays())) {
|
|
925
928
|
if (way.kind !== "none" && way.problem) {
|
|
926
|
-
console.warn(`models: ${cls}: ${way.problem}`);
|
|
929
|
+
console.warn(`models: ${cls}: ${saidText(way.problem, "en")}`);
|
|
927
930
|
}
|
|
928
931
|
}
|
|
929
932
|
}
|
|
933
|
+
/** The ways with their problems in a language, for whoever reads them. */
|
|
934
|
+
export function waysIn(ways, lang) {
|
|
935
|
+
return Object.fromEntries(Object.entries(ways).map(([cls, way]) => [
|
|
936
|
+
cls,
|
|
937
|
+
{ ...way, problem: way.problem && saidText(way.problem, lang) },
|
|
938
|
+
]));
|
|
939
|
+
}
|
|
930
940
|
/** What the settings page shows: the choices, which keys are there (never the keys), and the ways. */
|
|
931
|
-
export async function modelSettings() {
|
|
941
|
+
export async function modelSettings(lang = "en") {
|
|
932
942
|
const cfg = await config();
|
|
933
943
|
return {
|
|
934
944
|
source: cfg.settings.source,
|
|
@@ -936,7 +946,7 @@ export async function modelSettings() {
|
|
|
936
946
|
ollamaUrl: cfg.settings.ollamaUrl ?? env.ollamaUrl,
|
|
937
947
|
creditFallback: cfg.settings.creditFallback !== false,
|
|
938
948
|
keys: Object.fromEntries(LOCAL_KEYS.map((k) => [k, Boolean(cfg.keys[k])])),
|
|
939
|
-
ways: await classWays(),
|
|
949
|
+
ways: waysIn(await classWays(), lang),
|
|
940
950
|
/** Apple Intelligence on this Mac: shown where the machine can run it, picked once it answers. */
|
|
941
951
|
apple: managed ? null : await appleStatus(),
|
|
942
952
|
/** What a class costs on the credits, where they are at hand. */
|
|
@@ -947,7 +957,8 @@ export async function modelSettings() {
|
|
|
947
957
|
}
|
|
948
958
|
// --- Checking a key ------------------------------------------------------------------
|
|
949
959
|
/** A cheap authenticated call: whether the provider takes the key. Nothing is generated. */
|
|
950
|
-
export async function checkKey(name, key) {
|
|
960
|
+
export async function checkKey(name, key, lang = "en") {
|
|
961
|
+
const de = lang === "de";
|
|
951
962
|
const requests = {
|
|
952
963
|
openai: ["https://api.openai.com/v1/models", { authorization: `Bearer ${key}` }],
|
|
953
964
|
anthropic: [
|
|
@@ -980,12 +991,21 @@ export async function checkKey(name, key) {
|
|
|
980
991
|
return {
|
|
981
992
|
ok: false,
|
|
982
993
|
message: res.status === 401
|
|
983
|
-
?
|
|
984
|
-
|
|
994
|
+
? de
|
|
995
|
+
? "Der Anbieter kennt diesen Key nicht."
|
|
996
|
+
: "The provider does not know this key."
|
|
997
|
+
: de
|
|
998
|
+
? `Der Anbieter antwortet mit ${res.status}.`
|
|
999
|
+
: `The provider answers with ${res.status}.`,
|
|
985
1000
|
};
|
|
986
1001
|
}
|
|
987
1002
|
catch {
|
|
988
|
-
return {
|
|
1003
|
+
return {
|
|
1004
|
+
ok: false,
|
|
1005
|
+
message: de
|
|
1006
|
+
? "Der Anbieter ist gerade nicht erreichbar."
|
|
1007
|
+
: "The provider cannot be reached right now.",
|
|
1008
|
+
};
|
|
989
1009
|
}
|
|
990
1010
|
}
|
|
991
1011
|
let catalog = new Map();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { bookChannelUsage, ManageError, managed, requestChannelNumber, tenantChannels, withdrawChannelRequest, } from "../manage.js";
|
|
2
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
3
|
+
/**
|
|
4
|
+
* `server.channels`: the numbers the Manage-App gives the current tenant
|
|
5
|
+
* (docs/manage-contract.md, "Numbers of a tenant"). Without a Manage-App there are none.
|
|
6
|
+
*/
|
|
7
|
+
/** A refusal of the Manage-App as a plugin passes it on: its status and its code. */
|
|
8
|
+
const asked = (work) => work.catch((err) => {
|
|
9
|
+
throw err instanceof ManageError
|
|
10
|
+
? Object.assign(new Error(err.message), { status: err.status, code: err.code })
|
|
11
|
+
: err;
|
|
12
|
+
});
|
|
13
|
+
const unmanaged = () => Object.assign(new Error("This runtime has no Manage-App, so it has no numbers."), {
|
|
14
|
+
status: 404,
|
|
15
|
+
code: "not_managed",
|
|
16
|
+
});
|
|
17
|
+
export const channelsApi = {
|
|
18
|
+
async get() {
|
|
19
|
+
if (!managed) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const c = await asked(tenantChannels(currentTenant()));
|
|
23
|
+
return {
|
|
24
|
+
number: c.number
|
|
25
|
+
? {
|
|
26
|
+
number: c.number.number,
|
|
27
|
+
country: c.number.country,
|
|
28
|
+
kinds: c.number.kinds,
|
|
29
|
+
providerRef: c.number.providerRef ?? null,
|
|
30
|
+
}
|
|
31
|
+
: null,
|
|
32
|
+
request: c.request
|
|
33
|
+
? {
|
|
34
|
+
id: c.request.id,
|
|
35
|
+
country: c.request.country,
|
|
36
|
+
wish: c.request.wish,
|
|
37
|
+
createdAt: c.request.createdAt,
|
|
38
|
+
}
|
|
39
|
+
: null,
|
|
40
|
+
shared: c.shared,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
async request(input) {
|
|
44
|
+
if (!managed) {
|
|
45
|
+
throw unmanaged();
|
|
46
|
+
}
|
|
47
|
+
const { request } = await asked(requestChannelNumber(currentTenant(), {
|
|
48
|
+
userId: input.userId,
|
|
49
|
+
country: input.country.toUpperCase(),
|
|
50
|
+
wish: input.wish ?? null,
|
|
51
|
+
lang: input.lang,
|
|
52
|
+
}));
|
|
53
|
+
return {
|
|
54
|
+
id: request.id,
|
|
55
|
+
country: request.country,
|
|
56
|
+
wish: request.wish,
|
|
57
|
+
createdAt: request.createdAt,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
async withdraw() {
|
|
61
|
+
if (!managed) {
|
|
62
|
+
throw unmanaged();
|
|
63
|
+
}
|
|
64
|
+
return (await asked(withdrawChannelRequest(currentTenant()))).withdrawn;
|
|
65
|
+
},
|
|
66
|
+
async book(input) {
|
|
67
|
+
if (!managed || input.quantity <= 0) {
|
|
68
|
+
return { credits: 0 };
|
|
69
|
+
}
|
|
70
|
+
return asked(bookChannelUsage(currentTenant(), {
|
|
71
|
+
kind: input.kind,
|
|
72
|
+
quantity: input.quantity,
|
|
73
|
+
idempotencyKey: input.key,
|
|
74
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
75
|
+
}));
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -20,10 +20,11 @@ import { seal, unseal } from "../secrets/crypto.js";
|
|
|
20
20
|
import { putIndexEntry, removeIndexEntries } from "../services/project-index.js";
|
|
21
21
|
import { originData } from "../services/space-origin.js";
|
|
22
22
|
import { safeFetch } from "../tools/net-guard.js";
|
|
23
|
+
import { channelsApi } from "./channels.js";
|
|
23
24
|
import { discoverPlugins } from "./discovery.js";
|
|
24
25
|
import { migrateOpenTenants, migratePlugins } from "./migrations.js";
|
|
25
26
|
import { proModulesDir, proSources } from "./pro-state.js";
|
|
26
|
-
import { publicUrlOf } from "./public.js";
|
|
27
|
+
import { publicRefOf, publicUrlOf, sharedUrlOf } from "./public.js";
|
|
27
28
|
import { announceChange, compileRoute, dropPlugin, emptyRecord, loadedPlugin, loadedPlugins, setPlugin, } from "./registry.js";
|
|
28
29
|
import { runsApiFor } from "./runs.js";
|
|
29
30
|
/**
|
|
@@ -159,6 +160,12 @@ function apiFor(record) {
|
|
|
159
160
|
});
|
|
160
161
|
},
|
|
161
162
|
publicUrl: (path) => publicUrlOf(source.id, path),
|
|
163
|
+
publicRef: () => publicRefOf(source.id),
|
|
164
|
+
registerSharedRoute(route) {
|
|
165
|
+
record.sharedRoutes.push(compileRoute(route));
|
|
166
|
+
},
|
|
167
|
+
sharedUrl: (path) => sharedUrlOf(source.id, path),
|
|
168
|
+
channels: channelsApi,
|
|
162
169
|
runs: runsApiFor(source.id),
|
|
163
170
|
speech: {
|
|
164
171
|
async conversation() {
|
|
@@ -307,6 +314,7 @@ async function load(source) {
|
|
|
307
314
|
record.assistantTools.clear();
|
|
308
315
|
record.assistantPages = [];
|
|
309
316
|
record.publicRoutes = [];
|
|
317
|
+
record.sharedRoutes = [];
|
|
310
318
|
record.runners.clear();
|
|
311
319
|
record.jobs.clear();
|
|
312
320
|
record.starters.clear();
|
|
@@ -6,9 +6,24 @@ import { currentTenant } from "../tenants/tenant.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* Public addresses of plugins (`server.publicUrl`): `/api/public/plugins/<plugin>/<ref>/…`. The
|
|
8
8
|
* ref names the tenant without giving its id away; one per tenant and plugin, kept for good.
|
|
9
|
+
* A shared route (`server.sharedUrl`) is `/api/public/plugins/<plugin>/shared/…` for every
|
|
10
|
+
* tenant: a ref is 24 characters, so it is never `shared`.
|
|
9
11
|
*/
|
|
12
|
+
export const SHARED = "shared";
|
|
13
|
+
const rest = (path) => {
|
|
14
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
15
|
+
return p === "/" ? "" : p;
|
|
16
|
+
};
|
|
10
17
|
/** The full address of a plugin's public route for the current tenant. */
|
|
11
18
|
export async function publicUrlOf(plugin, path) {
|
|
19
|
+
return `${env.publicUrl}/api/public/plugins/${plugin}/${await publicRefOf(plugin)}${rest(path)}`;
|
|
20
|
+
}
|
|
21
|
+
/** The full address of a plugin's shared route. */
|
|
22
|
+
export function sharedUrlOf(plugin, path) {
|
|
23
|
+
return `${env.publicUrl}/api/public/plugins/${plugin}/${SHARED}${rest(path)}`;
|
|
24
|
+
}
|
|
25
|
+
/** The ref of the current tenant's public address for a plugin, made the first time. */
|
|
26
|
+
export async function publicRefOf(plugin) {
|
|
12
27
|
const tenantId = currentTenant();
|
|
13
28
|
const where = and(eq(control.pluginAddress.tenantId, tenantId), eq(control.pluginAddress.plugin, plugin));
|
|
14
29
|
let row = await controlDb.query.pluginAddress.findFirst({ where });
|
|
@@ -19,8 +34,7 @@ export async function publicUrlOf(plugin, path) {
|
|
|
19
34
|
.onConflictDoNothing();
|
|
20
35
|
row = (await controlDb.query.pluginAddress.findFirst({ where }));
|
|
21
36
|
}
|
|
22
|
-
|
|
23
|
-
return `${env.publicUrl}/api/public/plugins/${plugin}/${row.ref}${rest === "/" ? "" : rest}`;
|
|
37
|
+
return row.ref;
|
|
24
38
|
}
|
|
25
39
|
/** The tenant a plugin's public address belongs to. */
|
|
26
40
|
export async function tenantOfAddress(plugin, ref) {
|
|
@@ -5,6 +5,7 @@ import { and, asc, eq, inArray, isNotNull } from "drizzle-orm";
|
|
|
5
5
|
import { db, schema, withTenant } from "../db/client.js";
|
|
6
6
|
import { answerAsk } from "../engine/asks.js";
|
|
7
7
|
import { watchRun } from "../engine/events.js";
|
|
8
|
+
import { fieldErrorsIn } from "../engine/input.js";
|
|
8
9
|
import { cancel, createRun, goBack, NoCreditsError, RunInputError, retry, reviewStep, submitPage, } from "../engine/runner.js";
|
|
9
10
|
import { saveAsset } from "../files/storage.js";
|
|
10
11
|
import { wizardUnavailable } from "../limits.js";
|
|
@@ -81,7 +82,7 @@ export function runsApiFor(pluginId) {
|
|
|
81
82
|
if (!(err instanceof RunInputError)) {
|
|
82
83
|
throw err;
|
|
83
84
|
}
|
|
84
|
-
refused = err.errors;
|
|
85
|
+
refused = fieldErrorsIn(err.errors, "en");
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
return { runId, ticket: runTicket(runId), refused };
|