UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

256 lines (255 loc) • 11.1 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { i as executeSqliteQuerySync, o as getNodeSqliteKysely } from "./kysely-sync-DUWzQvCU.mjs"; import { s as requestSqliteWorkerOperationAdmission } from "./sqlite-worker-operation-admission-C5lBzzTk.mjs"; import { u as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-ywwvuxdQ.mjs"; import { n as resolveAllowFromAccountId, r as resolvePairingRequestAccountId, t as dedupePreserveOrder } from "./pairing-store-keys-cRW5JPmq.mjs"; import { i as readChannelPairingSnapshotFromDatabase, r as readChannelPairingRequests, t as normalizePersistedPairingRequest } from "./pairing-store-sqlite-D_sqw3gG.mjs"; import { a as requestMatchesAccountId, i as pruneExpiredRequests, n as generateUniqueCode, o as resolveChannelPairingRequestId, r as pruneExcessRequestsByAccount } from "./pairing-store-model-B2kD2hdd.mjs"; import { isDeepStrictEqual } from "node:util"; import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; //#region src/pairing/pairing-store.worker.ts function selectedRequest(requests, accountId, selector) { return requests.find((request) => requestMatchesAccountId(request, accountId) && ("code" in selector ? request.code.toUpperCase() === selector.code : resolveChannelPairingRequestId(selector.channel, request) === selector.requestId)); } /** Preserve canonical snapshot normalization without rewriting unchanged rows. */ function writeChanges(database, channel, snapshot) { const db = getNodeSqliteKysely(database.db); const requests = snapshot.state.requests.flatMap((value) => { const request = normalizePersistedPairingRequest(value); return request ? [{ channel_key: channel, account_id: resolvePairingRequestAccountId(request), request_id: request.id, code: request.code, created_at: request.createdAt, last_seen_at: request.lastSeenAt, meta_json: request.meta ? JSON.stringify(request.meta) : null }] : []; }); const requestKey = (row) => JSON.stringify([row.account_id, row.request_id]); const oldRequests = new Map(snapshot.requestRows.map((row) => [requestKey(row), row])); const newRequests = new Set(requests.map(requestKey)); if (newRequests.size !== requests.length) throw new Error("Duplicate normalized channel pairing request"); const removedRequests = snapshot.requestRows.filter((row) => !newRequests.has(requestKey(row))); for (let offset = 0; offset < removedRequests.length; offset += 100) { const removed = removedRequests.slice(offset, offset + 100); executeSqliteQuerySync(database.db, db.deleteFrom("channel_pairing_requests").where("channel_key", "=", channel).where((eb) => eb.or(removed.map((row) => eb.and([eb("account_id", "=", row.account_id), eb("request_id", "=", row.request_id)]))))); } const changedRequests = requests.filter((row) => !isDeepStrictEqual(row, { ...oldRequests.get(requestKey(row)) })); for (let offset = 0; offset < changedRequests.length; offset += 100) executeSqliteQuerySync(database.db, db.insertInto("channel_pairing_requests").values(changedRequests.slice(offset, offset + 100)).onConflict((oc) => oc.columns([ "channel_key", "account_id", "request_id" ]).doUpdateSet((eb) => ({ code: eb.ref("excluded.code"), created_at: eb.ref("excluded.created_at"), last_seen_at: eb.ref("excluded.last_seen_at"), meta_json: eb.ref("excluded.meta_json") })))); const now = Date.now(); const entries = Object.entries(snapshot.state.allowFrom ?? {}).flatMap(([account, values]) => dedupePreserveOrder(values.map((value) => normalizeOptionalString(value) ?? "").filter((value) => value && value !== "*")).map((entry, sort_order) => ({ channel_key: channel, account_id: resolveAllowFromAccountId(account), entry, sort_order }))); const entryKey = (row) => JSON.stringify([row.account_id, row.entry]); const oldEntries = new Map(snapshot.allowRows.map((row) => [entryKey(row), row])); const newEntries = new Set(entries.map(entryKey)); const removedEntries = snapshot.allowRows.filter((row) => !newEntries.has(entryKey(row))); for (let offset = 0; offset < removedEntries.length; offset += 100) executeSqliteQuerySync(database.db, db.deleteFrom("channel_pairing_allow_entries").where("channel_key", "=", channel).where((eb) => eb.or(removedEntries.slice(offset, offset + 100).map((row) => eb.and([eb("account_id", "=", row.account_id), eb("entry", "=", row.entry)]))))); const changedEntries = entries.filter((row) => oldEntries.get(entryKey(row))?.sort_order !== row.sort_order); for (let offset = 0; offset < changedEntries.length; offset += 100) executeSqliteQuerySync(database.db, db.insertInto("channel_pairing_allow_entries").values(changedEntries.slice(offset, offset + 100).map((row) => ({ channel_key: row.channel_key, account_id: row.account_id, entry: row.entry, sort_order: row.sort_order, updated_at: now }))).onConflict((oc) => oc.columns([ "channel_key", "account_id", "entry" ]).doUpdateSet((eb) => ({ sort_order: eb.ref("excluded.sort_order"), updated_at: now })))); } function prepareApproval(channel, entry) { const { port1, port2 } = new MessageChannel(); try { requestSqliteWorkerOperationAdmission({ stage: "prepare", facts: { kind: "channel-pairing-approval", channel, entry, replyPort: port2 } }, [port2]); const normalized = receiveMessageOnPort(port1)?.message; if (typeof normalized !== "string") throw new Error("Channel pairing approval preparation did not return an entry"); return normalized; } finally { port1.close(); port2.close(); } } const channelPairingOperations = { "channelPairing.mutate": ({ channel, mutation }, context) => { for (;;) { let prepared; let normalizedApproval = ""; if (mutation.action === "resolve" && mutation.approval === "host") { const pruned = pruneExpiredRequests(withExistingOpenClawStateDatabaseReadOnly(({ db }) => readChannelPairingRequests(db, channel), context.stateOptions()) ?? [], Date.now()); prepared = selectedRequest(pruned.requests, mutation.accountId, mutation.selector); if (!prepared && !pruned.removed) { context.open(); requestSqliteWorkerOperationAdmission({ stage: "commit", facts: { kind: "channel-pairing", channel } }); return { action: "resolve", result: null }; } if (prepared) normalizedApproval = prepareApproval(channel, prepared); } const outcome = context.write((database) => { requestSqliteWorkerOperationAdmission({ stage: "transaction", facts: { kind: "channel-pairing", channel } }); const snapshot = readChannelPairingSnapshotFromDatabase(database, channel); const state = snapshot.state; let changed; let result; if (mutation.action === "allow") { const current = (state.allowFrom?.[mutation.accountId] ?? []).slice(); const next = mutation.remove ? current.filter((entry) => normalizeOptionalString(entry) !== mutation.entry) : current.includes(mutation.entry) ? current : [...current, mutation.entry]; changed = Boolean(mutation.entry) && !isDeepStrictEqual(current, next); if (changed) { state.allowFrom ??= {}; state.allowFrom[mutation.accountId] = next; } result = { action: "allow", changed, allowFrom: changed ? next : current }; } else { const nowMs = Date.now(); const expired = pruneExpiredRequests(state.requests, nowMs); state.requests = expired.requests; changed = expired.removed; if (mutation.action === "list") { const capped = pruneExcessRequestsByAccount(state.requests); state.requests = capped.requests; changed ||= capped.removed; result = { action: "list", requests: state.requests.filter((entry) => requestMatchesAccountId(entry, mutation.accountId)).toSorted((left, right) => left.createdAt.localeCompare(right.createdAt) || resolvePairingRequestAccountId(left).localeCompare(resolvePairingRequestAccountId(right)) || left.id.localeCompare(right.id)) }; } else if (mutation.action === "upsert") { const existingCodes = new Set(state.requests.map((request) => request.code.toUpperCase())); const existing = state.requests.findIndex((request) => request.id === mutation.id && requestMatchesAccountId(request, mutation.accountId)); const now = new Date(nowMs).toISOString(); if (existing >= 0) { const previous = state.requests[existing]; state.requests[existing] = { id: mutation.id, code: previous.code, createdAt: previous.createdAt, lastSeenAt: now, meta: mutation.meta }; state.requests = pruneExcessRequestsByAccount(state.requests).requests; changed = true; result = { action: "upsert", code: previous.code, created: false }; } else { const capped = pruneExcessRequestsByAccount(state.requests); state.requests = capped.requests; changed ||= capped.removed; if (state.requests.filter((request) => requestMatchesAccountId(request, mutation.accountId)).length >= 3) result = { action: "upsert", code: "", created: false }; else { const code = generateUniqueCode(existingCodes); state.requests.push({ id: mutation.id, code, createdAt: now, lastSeenAt: now, meta: mutation.meta }); changed = true; result = { action: "upsert", code, created: true }; } } } else { const entry = selectedRequest(state.requests, mutation.accountId, mutation.selector); if (mutation.approval === "host" && !isDeepStrictEqual(entry, prepared)) { requestSqliteWorkerOperationAdmission({ stage: "commit", facts: { kind: "channel-pairing", channel } }); return; } if (entry) { state.requests = state.requests.filter((request) => request !== entry); changed = true; if (mutation.approval === "sender") { normalizedApproval = normalizeOptionalString(entry.id) ?? ""; if (normalizedApproval === "*") normalizedApproval = ""; } if (mutation.approval !== "dismiss" && normalizedApproval) { const accountId = resolveAllowFromAccountId(mutation.accountId || entry.meta?.accountId); const current = state.allowFrom?.[accountId] ?? []; if (!current.includes(normalizedApproval)) { state.allowFrom ??= {}; state.allowFrom[accountId] = [...current, normalizedApproval]; } } } result = { action: "resolve", result: entry ? { id: entry.id, entry } : null }; } } if (changed) writeChanges(database, channel, snapshot); requestSqliteWorkerOperationAdmission({ stage: "commit", facts: { kind: "channel-pairing", channel } }); return result; }, { operationLabel: "channel-pairing.mutate" }); if (outcome) return outcome; } } }; //#endregion export { channelPairingOperations };