openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
387 lines (386 loc) • 11.5 kB
JavaScript
import { t as createDeferredCore } from "./deferred-D0La5CRk.js";
import { n as WizardCancelledError, t as DEVICE_CODE_PHISHING_WARNING } from "./prompts-DLsO8MlU.js";
import { randomUUID } from "node:crypto";
//#region src/wizard/session.ts
const WIZARD_STEP_INPUT_REQUIREMENT_BY_TYPE = {
note: "never",
select: "always",
text: "always",
confirm: "always",
multiselect: "always",
progress: "never",
action: "client-executor"
};
/** Whether a step needs a user answer instead of client or gateway acknowledgement. */
function wizardStepAwaitsInput(step) {
const requirement = WIZARD_STEP_INPUT_REQUIREMENT_BY_TYPE[step.type];
switch (requirement) {
case "always": return true;
case "never": return false;
case "client-executor": return step.executor === "client";
}
return requirement;
}
/** Remove secret prefill before a wizard step crosses a client boundary. */
function sanitizeWizardStepForClient(step) {
if (step.sensitive !== true || step.initialValue === void 0) return step;
const safe = { ...step };
delete safe.initialValue;
return safe;
}
function normalizeTextAnswer(value) {
if (value === null || value === void 0) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
}
/** Own enumerable, closure-bound methods survive the runtime installer's note adapter. */
function createWizardSessionPrompter(session) {
async function prompt(step) {
return await session.awaitAnswer(createStep(step));
}
function createStep(step) {
const externalUrl = session.consumeExternalUrl();
return {
...step,
...externalUrl ? { externalUrl } : {},
id: randomUUID()
};
}
return {
cancel(message) {
throw new WizardCancelledError(message);
},
async intro(title) {
await prompt({
type: "note",
title,
message: "",
executor: "client"
});
},
async outro(message) {
await prompt({
type: "note",
title: "Done",
message,
executor: "client"
});
},
async note(message, title) {
await prompt({
type: "note",
title,
message,
executor: "client"
});
},
async deviceCode(params) {
const fallbackMessage = [
params.message ?? "Enter this one-time code on the provider's sign-in page.",
`Code: ${params.code}`,
...params.expiresInMinutes ? [`Code expires in ${params.expiresInMinutes} minutes.`] : [],
DEVICE_CODE_PHISHING_WARNING
].join("\n");
await prompt({
type: "note",
title: params.title,
message: fallbackMessage,
deviceCode: {
code: params.code,
...params.expiresInMinutes ? { expiresInMinutes: params.expiresInMinutes } : {},
...params.message ? { message: params.message } : {}
},
executor: "client"
});
},
async plain(message) {
await prompt({
type: "note",
message,
format: "plain",
executor: "client"
});
},
async select(params) {
return await prompt({
type: "select",
message: params.message,
options: params.options.map((opt) => ({
value: opt.value,
label: opt.label,
hint: opt.hint
})),
initialValue: params.initialValue,
executor: "client"
});
},
async multiselect(params) {
const res = await prompt({
type: "multiselect",
message: params.message,
options: params.options.map((opt) => ({
value: opt.value,
label: opt.label,
hint: opt.hint
})),
initialValue: params.initialValues,
executor: "client"
});
return Array.isArray(res) ? res : [];
},
async text(params) {
const res = await session.awaitAnswer(createStep({
type: "text",
message: params.message,
initialValue: params.initialValue,
placeholder: params.placeholder,
sensitive: params.sensitive,
executor: "client"
}), params.validate, params.signal);
return res === null || res === void 0 ? "" : typeof res === "string" ? res : typeof res === "number" || typeof res === "boolean" || typeof res === "bigint" ? String(res) : "";
},
async confirm(params) {
return await prompt({
type: "confirm",
message: params.message,
initialValue: params.initialValue,
executor: "client"
}) === true;
},
progress(label) {
let stopped = false;
session.pushProgress(label);
return {
update: (message) => {
if (!stopped) session.pushProgress(message);
},
stop: (message) => {
if (stopped) return;
stopped = true;
if (message) session.pushProgress(message);
}
};
},
async openUrl(url) {
session.queueExternalUrl(url);
}
};
}
var WizardSession = class {
constructor(runner, options) {
this.runner = runner;
this.abortController = new AbortController();
this.currentStep = null;
this.progressSteps = [];
this.deliveredProgressStepIds = /* @__PURE__ */ new Set();
this.stepDeferred = null;
this.cancellationLocked = false;
this.settled = false;
this.answerDeferred = /* @__PURE__ */ new Map();
this.status = "running";
const prompter = createWizardSessionPrompter(this);
if (options?.timeoutMs !== void 0) {
this.expiryTimer = setTimeout(() => this.cancel(), options.timeoutMs);
this.expiryTimer.unref?.();
}
this.runnerPromise = this.run(prompter);
}
async next() {
if (this.status !== "running") return this.terminalResult();
const progressStep = this.progressSteps.shift();
if (progressStep) {
this.rememberDeliveredProgressStep(progressStep.id);
return {
done: false,
step: progressStep,
status: this.status
};
}
if (this.currentStep) return {
done: false,
step: this.currentStep,
status: this.status
};
if (!this.stepDeferred) this.stepDeferred = createDeferredCore();
const step = await this.stepDeferred.promise;
if (step && this.getStatus() === "running") return {
done: false,
step,
status: "running"
};
return this.terminalResult();
}
/** A non-consuming view for polling clients; retired prompts are never replayed. */
getCurrentStep() {
return this.status === "running" ? this.currentStep ?? this.progressSteps.at(-1) : void 0;
}
terminalResult() {
return {
done: true,
status: this.status,
error: this.error,
...this.configuredAccounts ? {
channels: [...new Set(this.configuredAccounts.map((entry) => entry.channel))],
accounts: this.configuredAccounts.map((entry) => ({ ...entry }))
} : {},
...this.status === "done" && this.preparedModelRef ? { preparedModelRef: this.preparedModelRef } : {},
...this.status === "done" && this.modelActivation ? { modelActivation: this.modelActivation } : {},
...this.status === "error" && this.activationRejection ? { activationRejection: this.activationRejection } : {}
};
}
/** Record what the channels flow actually configured (channels flow only). */
setConfiguredAccounts(accounts) {
this.configuredAccounts = accounts.map((entry) => ({ ...entry }));
}
/** Record the exact provider-owned model prepared by a setup flow. */
setPreparedModelRef(modelRef) {
this.preparedModelRef = modelRef;
}
/** Record the live activation result, distinct from provider preparation. */
setModelActivation(activation) {
this.modelActivation = activation;
}
/** Only the activation owner can distinguish rejection from possibly committed failure. */
setActivationRejection(rejection) {
this.activationRejection = rejection;
}
async answer(stepId, value) {
const pending = this.answerDeferred.get(stepId);
if (!pending) {
if (this.deliveredProgressStepIds.delete(stepId)) return;
throw new Error("wizard: no pending step");
}
const normalizedValue = pending.text ? normalizeTextAnswer(value) : value;
if (pending.text && normalizedValue === void 0) return "wizard: text answer must be a scalar value";
const validationError = pending.validate?.(normalizedValue) ?? void 0;
if (validationError) return validationError;
this.answerDeferred.delete(stepId);
this.currentStep = null;
pending.deferred.resolve(normalizedValue);
}
cancel() {
if (this.status !== "running" || this.cancellationLocked) return false;
this.status = "cancelled";
this.error = "cancelled";
this.abortController.abort(new WizardCancelledError());
this.rejectPendingAnswers();
this.progressSteps = [];
this.deliveredProgressStepIds.clear();
this.resolveStep(null);
return true;
}
/** The underlying mutation crossed its durable commit point and must finish. */
lockCancellation() {
this.signal.throwIfAborted();
this.cancellationLocked = true;
}
get signal() {
return this.abortController.signal;
}
pushStep(step) {
this.currentStep = step;
this.resolveStep(step);
}
pushProgress(message) {
if (this.status !== "running") return;
const step = {
id: randomUUID(),
type: "progress",
message,
executor: "gateway"
};
if (this.stepDeferred) {
this.rememberDeliveredProgressStep(step.id);
this.resolveStep(step);
return;
}
if (this.progressSteps.length >= 2) {
this.progressSteps[this.progressSteps.length - 1] = step;
return;
}
this.progressSteps.push(step);
}
rememberDeliveredProgressStep(stepId) {
this.deliveredProgressStepIds.add(stepId);
if (this.deliveredProgressStepIds.size <= 64) return;
const oldest = this.deliveredProgressStepIds.values().next().value;
if (oldest) this.deliveredProgressStepIds.delete(oldest);
}
queueExternalUrl(url) {
this.pendingExternalUrl = url;
}
consumeExternalUrl() {
const url = this.pendingExternalUrl;
this.pendingExternalUrl = void 0;
return url;
}
async run(prompter) {
try {
await this.runner(prompter, this.signal, this);
if (this.status === "running") this.status = "done";
} catch (err) {
if (this.status !== "running") return;
if (err instanceof WizardCancelledError) {
this.status = "cancelled";
this.error = err.message;
} else {
this.status = "error";
this.error = String(err);
}
} finally {
this.settled = true;
if (this.expiryTimer) clearTimeout(this.expiryTimer);
this.rejectPendingAnswers();
this.resolveStep(null);
}
}
rejectPendingAnswers() {
this.currentStep = null;
for (const pending of this.answerDeferred.values()) pending.deferred.reject(new WizardCancelledError());
this.answerDeferred.clear();
}
async awaitAnswer(step, validate, signal) {
if (this.status !== "running") throw new Error("wizard: session not running");
signal?.throwIfAborted();
const deferred = createDeferredCore();
this.answerDeferred.set(step.id, {
deferred,
text: step.type === "text",
validate
});
const abort = () => {
this.answerDeferred.delete(step.id);
if (this.currentStep?.id === step.id) this.currentStep = null;
deferred.reject(signal?.reason);
};
signal?.addEventListener("abort", abort, { once: true });
this.pushStep(step);
try {
return await deferred.promise;
} finally {
signal?.removeEventListener("abort", abort);
}
}
resolveStep(step) {
if (!this.stepDeferred) return;
const deferred = this.stepDeferred;
this.stepDeferred = null;
deferred.resolve(step);
}
getStatus() {
return this.status;
}
/** Whether the runner has stopped and can no longer mutate setup state. */
isSettled() {
return this.settled;
}
/** Resolves after the runner can no longer mutate setup state. */
whenSettled() {
return this.runnerPromise;
}
getError() {
return this.error;
}
};
//#endregion
export { sanitizeWizardStepForClient as n, wizardStepAwaitsInput as r, WizardSession as t };