openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
349 lines (348 loc) • 14.7 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { a as updateConfigMachineState, r as readConfigMachineState } from "./config-machine-state-BCereLZr.js";
import { o as parseRegistryNpmSpec } from "./npm-registry-spec-CM_p1_uq.js";
import { _ as requestClawHub, a as fetchClawHubJson, d as readClawHubStringArrayField, f as readClawHubStringField, g as readRequiredClawHubStringField, i as decodeClawHubResponseBody, l as readClawHubBooleanField, m as readRequiredClawHubNumberField, p as readRequiredClawHubBooleanField, r as createClawHubError, t as CLAWHUB_JSON_MAX_BYTES, u as readClawHubBytes } from "./clawhub-client-I4w9Lg5d.js";
import { n as hasValidIsoCalendarComponents } from "./iso-time-C2akJNwm.js";
//#region src/infra/clawhub-promotions.ts
const CLAWHUB_PROMOTION_MODEL_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
function parseClawHubPromotionModel(value, context) {
if (!isRecord(value)) throw new Error(`Malformed ClawHub ${context}: expected each model to be an object.`);
const modelRef = readRequiredClawHubStringField(value, "modelRef", context);
if (!CLAWHUB_PROMOTION_MODEL_REF_RE.test(modelRef)) throw new Error(`Malformed ClawHub ${context}: modelRef contains unsupported characters.`);
const model = { modelRef };
const alias = readClawHubStringField(value, "alias", context);
if (alias) model.alias = alias;
const suggestedDefault = readClawHubBooleanField(value, "suggestedDefault", context);
if (suggestedDefault !== void 0) model.suggestedDefault = suggestedDefault;
return model;
}
const CLAWHUB_PROMOTION_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
const CLAWHUB_PROMOTION_IDENTIFIER_RE = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
function parseClawHubPromotionCore(value, context) {
const modelsRaw = value.models;
if (!Array.isArray(modelsRaw) || modelsRaw.length === 0) throw new Error(`Malformed ClawHub ${context}: expected models to be a non-empty array.`);
const slug = readRequiredClawHubStringField(value, "slug", context);
if (!CLAWHUB_PROMOTION_SLUG_RE.test(slug)) throw new Error(`Malformed ClawHub ${context}: slug must be lowercase [a-z0-9-].`);
const startsAt = readRequiredClawHubNumberField(value, "startsAt", context);
const endsAt = readRequiredClawHubNumberField(value, "endsAt", context);
if (endsAt <= startsAt) throw new Error(`Malformed ClawHub ${context}: promotion window must end after it starts.`);
const promotion = {
slug,
title: readRequiredClawHubStringField(value, "title", context),
blurb: readRequiredClawHubStringField(value, "blurb", context),
startsAt,
endsAt,
models: modelsRaw.map((entry) => parseClawHubPromotionModel(entry, context))
};
for (const field of [
"sponsor",
"signupUrl",
"docsUrl",
"launchPageUrl"
]) {
const parsed = readClawHubStringField(value, field, context);
if (parsed) promotion[field] = parsed;
}
for (const field of ["provider", "authChoiceId"]) {
const parsed = readClawHubStringField(value, field, context);
if (!parsed) continue;
if (!CLAWHUB_PROMOTION_IDENTIFIER_RE.test(parsed)) throw new Error(`Malformed ClawHub ${context}: ${field} contains unsupported characters.`);
promotion[field] = parsed;
}
const pluginNames = readClawHubStringArrayField(value, "pluginNames", context);
if (pluginNames && pluginNames.length > 0) {
for (const name of pluginNames) {
const parsed = parseRegistryNpmSpec(name);
if (!parsed || parsed.selectorKind !== "none" || parsed.name !== name) throw new Error(`Malformed ClawHub ${context}: pluginNames must contain npm package names.`);
}
promotion.pluginNames = pluginNames;
}
return promotion;
}
function parseClawHubPromotion(value) {
const context = "promotion";
if (!isRecord(value)) throw new Error(`Malformed ClawHub ${context}: expected an object.`);
return {
...parseClawHubPromotionCore(value, context),
status: readRequiredClawHubStringField(value, "status", context),
active: readRequiredClawHubBooleanField(value, "active", context)
};
}
async function fetchClawHubPromotions(params = {}) {
const response = await fetchClawHubJson({
baseUrl: params.baseUrl,
path: "/api/v1/promotions",
timeoutMs: params.timeoutMs,
fetchImpl: params.fetchImpl
});
if (!isRecord(response) || !Array.isArray(response.promotions)) throw new Error("Malformed ClawHub promotions response: expected a promotions array.");
return response.promotions.map((entry) => parseClawHubPromotion(entry));
}
async function fetchClawHubPromotion(params) {
return parseClawHubPromotion(await fetchClawHubJson({
baseUrl: params.baseUrl,
path: `/api/v1/promotions/${encodeURIComponent(params.slug)}`,
timeoutMs: params.timeoutMs,
fetchImpl: params.fetchImpl
}));
}
const CLAWHUB_PROMOTIONS_FEED_ID = "clawhub-promotions";
const CLAWHUB_PROMOTIONS_FEED_SCHEMA_VERSION = 1;
function parseClawHubPromotionsFeed(value) {
const context = "promotions feed";
if (!isRecord(value)) throw new Error(`Malformed ClawHub ${context}: expected an object.`);
const id = readRequiredClawHubStringField(value, "id", context);
if (id !== CLAWHUB_PROMOTIONS_FEED_ID) throw new Error(`Malformed ClawHub ${context}: unexpected feed id.`);
const schemaVersion = readRequiredClawHubNumberField(value, "schemaVersion", context);
if (schemaVersion !== CLAWHUB_PROMOTIONS_FEED_SCHEMA_VERSION) throw new Error(`Unsupported ClawHub ${context} schema version ${schemaVersion}.`);
const sequence = readRequiredClawHubNumberField(value, "sequence", context);
if (!Number.isSafeInteger(sequence) || sequence < 0) throw new Error(`Malformed ClawHub ${context}: sequence must be a non-negative integer.`);
const generatedAt = readRequiredClawHubStringField(value, "generatedAt", context);
const expiresAt = readRequiredClawHubStringField(value, "expiresAt", context);
const generatedAtMs = Date.parse(generatedAt);
const expiresAtMs = Date.parse(expiresAt);
if (!Number.isFinite(generatedAtMs) || !Number.isFinite(expiresAtMs) || !hasValidIsoCalendarComponents(generatedAt) || !hasValidIsoCalendarComponents(expiresAt)) throw new Error(`Malformed ClawHub ${context}: timestamps must be ISO dates.`);
if (expiresAtMs <= generatedAtMs) throw new Error(`Malformed ClawHub ${context}: expiresAt must be after generatedAt.`);
const entriesRaw = value.entries;
if (!Array.isArray(entriesRaw)) throw new Error(`Malformed ClawHub ${context}: expected an entries array.`);
return {
schemaVersion,
id,
generatedAt,
sequence,
expiresAt,
entries: entriesRaw.map((entry) => {
if (!isRecord(entry)) throw new Error(`Malformed ClawHub ${context}: expected each entry to be an object.`);
if (readRequiredClawHubStringField(entry, "type", context) !== "promotion") throw new Error(`Malformed ClawHub ${context}: unexpected entry type.`);
return parseClawHubPromotionCore(entry, context);
})
};
}
async function fetchClawHubPromotionsFeed(params = {}) {
const { response, url } = await requestClawHub({
baseUrl: params.baseUrl,
path: "/api/v1/feeds/promotions",
timeoutMs: params.timeoutMs,
fetchImpl: params.fetchImpl,
retryTransientReads: false,
skipAuth: true,
...params.etag ? { headers: { "If-None-Match": params.etag } } : {}
});
if (response.status === 304) return { status: "not-modified" };
if (!response.ok) throw await createClawHubError(response, url, false, params.timeoutMs);
const buffer = await readClawHubBytes({
response,
maxBytes: CLAWHUB_JSON_MAX_BYTES,
timeoutMs: params.timeoutMs,
resourceLabel: "promotions feed"
});
const payload = decodeClawHubResponseBody(buffer);
let parsedJson;
try {
parsedJson = JSON.parse(payload);
} catch (cause) {
throw new Error(`ClawHub ${url.pathname} returned malformed JSON`, { cause });
}
const feed = parseClawHubPromotionsFeed(parsedJson);
const etag = response.headers.get("etag") ?? void 0;
return {
status: "ok",
feed,
payload,
...etag ? { etag } : {}
};
}
//#endregion
//#region src/infra/promotions-feed.ts
const PROMOTIONS_FEED_STATE_KEY = "clawhub.promotionsFeed";
const PROMOTIONS_FEED_CHECK_INTERVAL_MS = 864e5;
const PROMOTIONS_FEED_FETCH_TIMEOUT_MS = 2500;
const EMPTY_STATE = {
entries: [],
notifiedSlugs: /* @__PURE__ */ new Set()
};
function readPromotionsFeedStateWithMetadata() {
try {
const stored = readConfigMachineState(PROMOTIONS_FEED_STATE_KEY);
if (!stored) return {
state: {
...EMPTY_STATE,
notifiedSlugs: /* @__PURE__ */ new Set()
},
payloadInvalid: false
};
let entries = [];
let expiresAtMs;
let payloadInvalid = false;
if (stored.payloadJson) try {
const feed = parseClawHubPromotionsFeed(JSON.parse(stored.payloadJson));
entries = feed.entries;
expiresAtMs = Date.parse(feed.expiresAt);
} catch {
payloadInvalid = true;
}
return {
state: {
...!payloadInvalid && stored.etag ? { etag: stored.etag } : {},
...!payloadInvalid && typeof stored.sequence === "number" ? { sequence: stored.sequence } : {},
...!payloadInvalid && expiresAtMs !== void 0 ? { expiresAtMs } : {},
entries,
...typeof stored.lastCheckedAtMs === "number" ? { lastCheckedAtMs: stored.lastCheckedAtMs } : {},
notifiedSlugs: new Set(stored.notifiedSlugs)
},
payloadInvalid
};
} catch {
return {
state: {
...EMPTY_STATE,
notifiedSlugs: /* @__PURE__ */ new Set()
},
payloadInvalid: false
};
}
}
function readPromotionsFeedState() {
return readPromotionsFeedStateWithMetadata().state;
}
function writePromotionsFeedState(params) {
updateConfigMachineState(PROMOTIONS_FEED_STATE_KEY, (existing) => ({
etag: params.etag === void 0 ? existing?.etag ?? null : params.etag,
payloadJson: params.payloadJson === void 0 ? existing?.payloadJson ?? null : params.payloadJson,
sequence: params.sequence === void 0 ? existing?.sequence ?? null : params.sequence,
lastCheckedAtMs: params.lastCheckedAtMs ?? existing?.lastCheckedAtMs ?? null,
notifiedSlugs: params.notifiedSlugs ? [.../* @__PURE__ */ new Set([...existing?.notifiedSlugs ?? [], ...params.notifiedSlugs])].toSorted() : existing?.notifiedSlugs ?? []
}));
}
function markPromotionSlugsNotified(slugs) {
try {
const state = readPromotionsFeedState();
const merged = new Set(state.notifiedSlugs);
let changed = false;
for (const slug of slugs) if (!merged.has(slug)) {
merged.add(slug);
changed = true;
}
if (changed) writePromotionsFeedState({ notifiedSlugs: merged });
} catch {}
}
function isPromotionWindowLive(entry, nowMs) {
return entry.startsAt <= nowMs && nowMs <= entry.endsAt;
}
function listLivePromotionEntries(state, nowMs) {
if (state.expiresAtMs !== void 0 && nowMs >= state.expiresAtMs) return [];
return state.entries.filter((entry) => isPromotionWindowLive(entry, nowMs));
}
/**
* Cadence-gated, fail-silent feed refresh. At most one conditional GET per
* check interval; offline or malformed responses leave the cached state
* untouched (aside from the attempt timestamp, so failures do not retry on
* every command). Returns the freshest available state.
*/
async function maybeRefreshPromotionsFeed(params = {}) {
const { state, payloadInvalid } = readPromotionsFeedStateWithMetadata();
const nowMs = params.nowMs ?? Date.now();
const skipForTests = !params.fetchImpl && (process.env.VITEST !== void 0 || false);
const checkedBeforeSnapshotExpired = state.expiresAtMs !== void 0 && state.lastCheckedAtMs !== void 0 && state.lastCheckedAtMs < state.expiresAtMs;
const fresh = !payloadInvalid && state.lastCheckedAtMs !== void 0 && nowMs - state.lastCheckedAtMs < PROMOTIONS_FEED_CHECK_INTERVAL_MS && (!checkedBeforeSnapshotExpired || state.expiresAtMs === void 0 || nowMs < state.expiresAtMs);
if (skipForTests || fresh && !params.force) return state;
try {
const result = await fetchClawHubPromotionsFeed({
...state.etag ? { etag: state.etag } : {},
...params.fetchImpl ? { fetchImpl: params.fetchImpl } : {},
timeoutMs: params.timeoutMs ?? PROMOTIONS_FEED_FETCH_TIMEOUT_MS
});
if (result.status === "not-modified") {
writePromotionsFeedState({ lastCheckedAtMs: nowMs });
return {
...state,
lastCheckedAtMs: nowMs
};
}
if (state.sequence !== void 0 && result.feed.sequence < state.sequence) {
writePromotionsFeedState({ lastCheckedAtMs: nowMs });
return {
...state,
lastCheckedAtMs: nowMs
};
}
writePromotionsFeedState({
etag: result.etag ?? null,
sequence: result.feed.sequence,
payloadJson: result.payload,
lastCheckedAtMs: nowMs
});
return {
...result.etag ? { etag: result.etag } : {},
sequence: result.feed.sequence,
expiresAtMs: Date.parse(result.feed.expiresAt),
entries: result.feed.entries,
lastCheckedAtMs: nowMs,
notifiedSlugs: state.notifiedSlugs
};
} catch {
try {
writePromotionsFeedState({
...payloadInvalid ? {
etag: null,
sequence: null,
payloadJson: null
} : {},
lastCheckedAtMs: nowMs
});
} catch {}
return {
...state,
lastCheckedAtMs: nowMs
};
}
}
function recordPromotionClaim(record) {
try {
runOpenClawStateWriteTransaction((database) => {
const db = getNodeSqliteKysely(database.db);
const values = {
slug: record.slug,
provider: record.provider ?? null,
model_keys_json: JSON.stringify(record.modelKeys),
ends_at_ms: record.endsAtMs,
claimed_at_ms: record.claimedAtMs
};
executeSqliteQuerySync(database.db, db.insertInto("clawhub_promotion_claims").values(values).onConflict((conflict) => conflict.column("slug").doUpdateSet(values)));
});
} catch {}
}
function readPromotionClaims() {
try {
const database = openOpenClawStateDatabase();
const db = getNodeSqliteKysely(database.db);
const { rows } = executeSqliteQuerySync(database.db, db.selectFrom("clawhub_promotion_claims").select([
"slug",
"provider",
"model_keys_json",
"ends_at_ms",
"claimed_at_ms"
]));
return rows.map((row) => {
let modelKeys = [];
try {
const parsed = JSON.parse(row.model_keys_json);
if (Array.isArray(parsed)) modelKeys = parsed.filter((entry) => typeof entry === "string");
} catch {}
const record = {
slug: row.slug,
modelKeys,
endsAtMs: row.ends_at_ms,
claimedAtMs: row.claimed_at_ms
};
if (row.provider) record.provider = row.provider;
return record;
});
} catch {
return [];
}
}
//#endregion
export { recordPromotionClaim as a, readPromotionClaims as i, markPromotionSlugsNotified as n, fetchClawHubPromotion as o, maybeRefreshPromotionsFeed as r, fetchClawHubPromotions as s, listLivePromotionEntries as t };