openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
291 lines (290 loc) • 11.8 kB
JavaScript
import { y as resolveStateDir } from "../../paths-mvMm5bYV.js";
import { _ as uniqueStrings } from "../../string-normalization-WNUDCpXX.js";
import { s as statRegularFileSync } from "../../regular-file-BD2zl6_l.js";
import "../../security-runtime-CQm7DD1u.js";
import "../../string-coerce-runtime-CEGJWkQ_.js";
import "../../state-paths-DzVdErem.js";
import { a as resolveIMessageAccount, i as resolveDefaultIMessageAccountId, r as listIMessageAccountIds } from "../../accounts-DzoymhDS.js";
import { i as IMESSAGE_REPLY_CACHE_NAMESPACE, l as resolveIMessageReplyCacheEntryKey, n as IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE, r as IMESSAGE_REPLY_CACHE_MAX_ENTRIES, t as IMESSAGE_REPLY_CACHE_COUNTER_KEY } from "../../monitor-reply-cache-DUH5oJzE.js";
import { o as resolveIMessageCatchupCursorKey, r as capFailureRetriesMap, t as IMESSAGE_CATCHUP_CURSOR_NAMESPACE } from "../../catchup-BUorPTSs.js";
import { n as IMESSAGE_SENT_ECHOES_TTL_MS, o as resolveIMessageSentEchoEntryKey, t as IMESSAGE_SENT_ECHOES_NAMESPACE } from "../../persisted-echo-cache-C0OpKutp.js";
import fs from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
//#region extensions/imessage/src/state-migrations.ts
const REPLY_CACHE_TTL_MS = 360 * 60 * 1e3;
function fileExists(pathValue) {
try {
return !statRegularFileSync(pathValue).missing;
} catch {
return false;
}
}
function resolveMigrationStateDir(params) {
return params.stateDir ?? resolveStateDir(params.env);
}
function remainingTtlMs(timestamp, ttlMs) {
const remaining = ttlMs - Math.max(0, Date.now() - timestamp);
return remaining > 0 ? remaining : void 0;
}
function readJsonl(pathValue) {
try {
return fs.readFileSync(pathValue, "utf8").split(/\n+/).flatMap((line) => {
if (!line) return [];
try {
return [JSON.parse(line)];
} catch {
return [];
}
});
} catch (err) {
throw new Error(`Failed reading ${pathValue}: ${String(err)}`, { cause: err });
}
}
function parseReplyCacheEntry(raw) {
if (!raw || typeof raw !== "object") return null;
const parsed = raw;
if (typeof parsed.accountId !== "string" || typeof parsed.messageId !== "string" || typeof parsed.shortId !== "string" || typeof parsed.timestamp !== "number") return null;
return {
accountId: parsed.accountId,
messageId: parsed.messageId,
shortId: parsed.shortId,
timestamp: parsed.timestamp,
...typeof parsed.chatGuid === "string" ? { chatGuid: parsed.chatGuid } : {},
...typeof parsed.chatIdentifier === "string" ? { chatIdentifier: parsed.chatIdentifier } : {},
...typeof parsed.chatId === "number" ? { chatId: parsed.chatId } : {},
...typeof parsed.isFromMe === "boolean" ? { isFromMe: parsed.isFromMe } : {}
};
}
function readReplyCacheMaxShortId(sourcePath) {
let max = 0;
for (const raw of readJsonl(sourcePath)) {
if (!raw || typeof raw !== "object") continue;
const shortId = raw.shortId;
if (typeof shortId !== "string") continue;
const numeric = Number.parseInt(shortId, 10);
if (Number.isFinite(numeric) && numeric > max) max = numeric;
}
return max;
}
function readReplyCounterValue(value) {
if (!value || typeof value !== "object") return null;
const counter = value.counter;
return typeof counter === "number" && Number.isFinite(counter) ? counter : null;
}
function shouldReplaceReplyCounter(existingValue, incomingValue) {
const incomingCounter = readReplyCounterValue(incomingValue);
if (incomingCounter === null) return false;
const existingCounter = readReplyCounterValue(existingValue);
return existingCounter === null || incomingCounter > existingCounter;
}
function parseSentEchoEntry(raw) {
if (!raw || typeof raw !== "object") return null;
const parsed = raw;
if (typeof parsed.scope !== "string" || typeof parsed.timestamp !== "number") return null;
return {
scope: parsed.scope,
timestamp: parsed.timestamp,
...typeof parsed.text === "string" ? { text: parsed.text } : {},
...typeof parsed.messageId === "string" ? { messageId: parsed.messageId } : {}
};
}
function listReplyCacheEntries(sourcePath) {
const entriesByKey = /* @__PURE__ */ new Map();
for (const entry of readJsonl(sourcePath).map(parseReplyCacheEntry)) {
if (!entry) continue;
const ttlMs = remainingTtlMs(entry.timestamp, REPLY_CACHE_TTL_MS);
if (!ttlMs) continue;
const key = resolveIMessageReplyCacheEntryKey(entry.messageId);
entriesByKey.delete(key);
entriesByKey.set(key, {
value: entry,
ttlMs
});
}
return [...entriesByKey.entries()].slice(-IMESSAGE_REPLY_CACHE_MAX_ENTRIES).map(([key, entry]) => ({
key,
value: entry.value,
ttlMs: entry.ttlMs
}));
}
function listSentEchoEntries(sourcePath) {
return readJsonl(sourcePath).map(parseSentEchoEntry).filter((entry) => Boolean(entry)).slice(-256).flatMap((entry) => {
const ttlMs = remainingTtlMs(entry.timestamp, IMESSAGE_SENT_ECHOES_TTL_MS);
if (!ttlMs) return [];
return [{
key: resolveIMessageSentEchoEntryKey(entry),
value: entry,
ttlMs
}];
});
}
function resolveLegacyCatchupCursorPath(stateDir, accountId) {
const safePrefix = accountId.replace(/[^a-zA-Z0-9_-]/g, "_") || "account";
const hash = createHash("sha256").update(accountId, "utf8").digest("hex").slice(0, 12);
return path.join(stateDir, "imessage", "catchup", `${safePrefix}__${hash}.json`);
}
function listLegacyCatchupCursorPaths(stateDir) {
const dir = path.join(stateDir, "imessage", "catchup");
try {
return fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path.join(dir, entry.name));
} catch {
return [];
}
}
function normalizeCatchupCursor(raw) {
if (!raw || typeof raw !== "object") return null;
const value = raw;
if (typeof value.lastSeenMs !== "number" || !Number.isFinite(value.lastSeenMs) || typeof value.lastSeenRowid !== "number" || !Number.isFinite(value.lastSeenRowid)) return null;
const failureRetries = sanitizeCatchupFailureRetries(value.failureRetries);
const hasRetries = Object.keys(failureRetries).length > 0;
return {
lastSeenMs: value.lastSeenMs,
lastSeenRowid: value.lastSeenRowid,
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : 0,
...hasRetries ? { failureRetries } : {}
};
}
function readCatchupCursor(sourcePath) {
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
} catch (err) {
throw new Error(`Failed reading ${sourcePath}: ${String(err)}`, { cause: err });
}
const cursor = normalizeCatchupCursor(parsed);
if (!cursor) throw new Error(`Invalid iMessage catchup cursor: ${sourcePath}`);
return cursor;
}
function sanitizeCatchupFailureRetries(raw) {
if (!raw || typeof raw !== "object") return {};
const out = {};
for (const [guid, count] of Object.entries(raw)) if (typeof count === "number" && Number.isFinite(count) && count > 0) out[guid] = Math.floor(count);
return capFailureRetriesMap(out);
}
function shouldReplaceCatchupCursor(existingValue, incomingValue) {
const incoming = normalizeCatchupCursor(incomingValue);
if (!incoming) return false;
const existing = normalizeCatchupCursor(existingValue);
return !existing || incoming.lastSeenRowid > existing.lastSeenRowid || incoming.lastSeenRowid === existing.lastSeenRowid && incoming.lastSeenMs > existing.lastSeenMs;
}
function detectReplyCacheMigration(params) {
const stateDir = resolveMigrationStateDir(params);
const sourcePath = path.join(stateDir, "imessage", "reply-cache.jsonl");
if (!fileExists(sourcePath)) return [];
const plans = [];
plans.push({
kind: "plugin-state-import",
label: "iMessage reply short-id counter",
sourcePath,
targetPath: `plugin state:${IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE}`,
pluginId: "imessage",
namespace: IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE,
maxEntries: 1,
scopeKey: "",
stateDir,
preview: `- iMessage reply short-id counter: ${sourcePath} → plugin state (${IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE})`,
readEntries: () => {
const maxShortId = readReplyCacheMaxShortId(sourcePath);
return maxShortId > 0 ? [{
key: IMESSAGE_REPLY_CACHE_COUNTER_KEY,
value: { counter: maxShortId }
}] : [];
},
shouldReplaceExistingEntry: ({ existingValue, incomingValue }) => shouldReplaceReplyCounter(existingValue, incomingValue)
});
plans.push({
kind: "plugin-state-import",
label: "iMessage reply short-id cache",
sourcePath,
targetPath: `plugin state:${IMESSAGE_REPLY_CACHE_NAMESPACE}`,
pluginId: "imessage",
namespace: IMESSAGE_REPLY_CACHE_NAMESPACE,
maxEntries: IMESSAGE_REPLY_CACHE_MAX_ENTRIES,
scopeKey: "",
stateDir,
cleanupSource: "rename",
cleanupWhenEmpty: true,
preview: `- iMessage reply short-id cache: ${sourcePath} → plugin state (${IMESSAGE_REPLY_CACHE_NAMESPACE})`,
readEntries: () => listReplyCacheEntries(sourcePath)
});
return plans;
}
function detectSentEchoMigration(params) {
const stateDir = resolveMigrationStateDir(params);
const sourcePath = path.join(stateDir, "imessage", "sent-echoes.jsonl");
if (!fileExists(sourcePath)) return [];
return [{
kind: "plugin-state-import",
label: "iMessage sent-echo dedupe cache",
sourcePath,
targetPath: `plugin state:${IMESSAGE_SENT_ECHOES_NAMESPACE}`,
pluginId: "imessage",
namespace: IMESSAGE_SENT_ECHOES_NAMESPACE,
maxEntries: 256,
scopeKey: "",
stateDir,
cleanupSource: "rename",
cleanupWhenEmpty: true,
preview: `- iMessage sent-echo dedupe cache: ${sourcePath} → plugin state (${IMESSAGE_SENT_ECHOES_NAMESPACE})`,
readEntries: () => listSentEchoEntries(sourcePath)
}];
}
function detectCatchupCursorMigrations(params) {
const stateDir = resolveMigrationStateDir(params);
const accountIds = uniqueStrings([resolveDefaultIMessageAccountId(params.cfg), ...listIMessageAccountIds(params.cfg)].map((accountId) => resolveIMessageAccount({
cfg: params.cfg,
accountId
}).accountId));
const configuredPaths = new Set(accountIds.map((accountId) => resolveLegacyCatchupCursorPath(stateDir, accountId)));
const configuredPlans = accountIds.flatMap((accountId) => {
const sourcePath = resolveLegacyCatchupCursorPath(stateDir, accountId);
if (!fileExists(sourcePath)) return [];
return {
kind: "plugin-state-import",
label: "iMessage catchup cursor",
sourcePath,
targetPath: `plugin state:${IMESSAGE_CATCHUP_CURSOR_NAMESPACE}`,
pluginId: "imessage",
namespace: IMESSAGE_CATCHUP_CURSOR_NAMESPACE,
maxEntries: 256,
scopeKey: "",
stateDir,
cleanupSource: "rename",
preview: `- iMessage catchup cursor: ${sourcePath} → plugin state (${IMESSAGE_CATCHUP_CURSOR_NAMESPACE})`,
readEntries: () => {
const cursor = readCatchupCursor(sourcePath);
return [{
key: resolveIMessageCatchupCursorKey(accountId),
value: cursor
}];
},
shouldReplaceExistingEntry: (replaceParams) => shouldReplaceCatchupCursor(replaceParams.existingValue, replaceParams.incomingValue)
};
});
const orphanPlans = listLegacyCatchupCursorPaths(stateDir).filter((sourcePath) => !configuredPaths.has(sourcePath)).map((sourcePath) => ({
kind: "plugin-state-import",
label: "iMessage orphan catchup cursor",
sourcePath,
targetPath: `plugin state:${IMESSAGE_CATCHUP_CURSOR_NAMESPACE}`,
pluginId: "imessage",
namespace: IMESSAGE_CATCHUP_CURSOR_NAMESPACE,
maxEntries: 256,
scopeKey: "",
stateDir,
cleanupSource: "rename",
cleanupWhenEmpty: true,
preview: `- iMessage orphan catchup cursor: ${sourcePath} → archived legacy state`,
readEntries: () => []
}));
return [...configuredPlans, ...orphanPlans];
}
async function detectIMessageLegacyStateMigrations(params) {
return [
...detectCatchupCursorMigrations(params),
...detectReplyCacheMigration(params),
...detectSentEchoMigration(params)
];
}
//#endregion
export { detectIMessageLegacyStateMigrations };