UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

349 lines (348 loc) 14.7 kB
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 };