openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
252 lines (249 loc) • 12.9 kB
JavaScript
import { a as defineChannelAliasMigration, b as hasLegacyAccountStreamingAliases, f as normalizeOptionalAccountId, k as asNullableRecord, t as collectChannelAccountScopes } from "./runtime-doctor-migrations-BhouWAUC.js";
import { o as createAccountListHelpers } from "./account-core-bZmYRKLc.js";
import { a as resolveOAuthDir } from "./boundary-file-read-D-7q9BJU.js";
import "node:fs/promises";
import "@openclaw/fs-safe/path";
import path from "node:path";
import fs$1 from "node:fs";
import "@openclaw/fs-safe/root";
import "@openclaw/fs-safe/advanced";
import "@openclaw/fs-safe/durability";
import "@openclaw/fs-safe/errors";
import { createHash } from "node:crypto";
import "@openclaw/fs-safe/permissions";
import "@openclaw/fs-safe/watch";
import { compileFunction } from "node:vm";
//#region extensions/whatsapp/src/account-ids.ts
const { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId: resolveDefaultWhatsAppAccountId } = createAccountListHelpers("whatsapp", { implicitDefaultAccount: { channelKeys: ["authDir"] } });
//#endregion
//#region src/media/staged-inputs.ts
const STAGED_INPUT_DIRECTORY_PREFIX = "media/inbound/openclaw-staged-";
const STAGED_INPUT_GITIGNORE = "# Raw task inputs remain private; copy outputs into the project to publish.\n*\n";
const STAGED_INPUT_GITIGNORE_SHA256 = createHash("sha256").update(STAGED_INPUT_GITIGNORE).digest("hex");
const STAGED_INPUT_PATHS_JS = String.raw`
const STAGED_INPUT_DIRECTORY_PREFIX = ${JSON.stringify(STAGED_INPUT_DIRECTORY_PREFIX)};
const STAGED_INPUT_GITIGNORE = ${JSON.stringify(STAGED_INPUT_GITIGNORE)};
const STAGED_INPUT_GITIGNORE_SHA256 = ${JSON.stringify(STAGED_INPUT_GITIGNORE_SHA256)};
// A producer-shaped name is only a candidate; the marker establishes ownership.
function stagedInputPathDirectory(relativePath) {
if (!relativePath.startsWith(STAGED_INPUT_DIRECTORY_PREFIX)) {
return undefined;
}
const identity = relativePath.slice(STAGED_INPUT_DIRECTORY_PREFIX.length).split("/")[0];
const match =
/^(?:[a-f0-9]{64}|[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$/u.exec(
identity,
);
return match?.[0] === identity ? STAGED_INPUT_DIRECTORY_PREFIX + identity : undefined;
}
function isStagedInputPath(relativePath, directories) {
const directory = stagedInputPathDirectory(relativePath);
return directory !== undefined && directories.has(directory);
}
// Complete manifests bind the regular marker's exact bytes through their file digest.
function stagedInputDirectoriesFromEntries(entries) {
const directories = new Set();
for (const entry of entries) {
const directory = stagedInputPathDirectory(entry.path);
if (
directory &&
entry.path === directory + "/.gitignore" &&
entry.type === "file" &&
entry.size === STAGED_INPUT_GITIGNORE.length &&
entry.sha256 === STAGED_INPUT_GITIGNORE_SHA256
) {
directories.add(directory);
}
}
return directories;
}`;
const { stagedInputPathDirectory, isStagedInputPath, stagedInputDirectoriesFromEntries } = compileFunction(`${STAGED_INPUT_PATHS_JS}\nreturn { stagedInputPathDirectory, isStagedInputPath, stagedInputDirectoriesFromEntries };`)();
String.raw`
try {
const fs = require("node:fs"), path = require("node:path");
const [file, maxText, offsetText, chunkText, workspace] = process.argv.slice(1);
const max = Number(maxText), offset = Number(offsetText), chunk = Number(chunkText);
if (!Number.isSafeInteger(max) || max < 0) throw Error("invalid media byte limit");
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(chunk) || chunk <= 0) throw Error("invalid media chunk");
if (fs.lstatSync(file).isSymbolicLink()) throw Error("symbolic links are not allowed");
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
try {
const before = fs.fstatSync(fd);
if (!before.isFile()) throw Error("not a regular file");
if (workspace) {
const descriptor = process.platform === "linux" ? fs.realpathSync("/proc/self/fd/" + fd) : fs.realpathSync(file);
const relative = path.relative(fs.realpathSync(workspace), descriptor);
if (!relative || relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) throw Error("file escapes remote workspace");
const verified = fs.statSync(descriptor);
if (verified.dev !== before.dev || verified.ino !== before.ino) throw Error("file changed while being opened");
}
if (before.size > max) throw Error("file exceeds limit of " + max + " bytes");
if (offset > before.size) throw Error("file changed while being read");
const expected = Math.min(chunk, before.size - offset), buffer = Buffer.allocUnsafe(expected);
let total = 0;
while (total < buffer.length) {
const count = fs.readSync(fd, buffer, total, buffer.length - total, offset + total);
if (count === 0) break;
total += count;
}
if (total !== expected) throw Error("file changed while being read");
const after = fs.fstatSync(fd);
const revision = stat => [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
if (!after.isFile() || revision(after) !== revision(before)) throw Error("file changed while being read");
process.stdout.write(JSON.stringify({ dataBase64: buffer.toString("base64"), size: before.size, revision: revision(before) }));
} finally { fs.closeSync(fd); }
} catch (error) { process.stderr.write(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }
`;
//#endregion
//#region extensions/whatsapp/src/creds-files.ts
const BAILEYS_SIGNAL_AUTH_CATEGORIES = {
"app-state-sync-key": true,
"app-state-sync-version": true,
"device-list": true,
"identity-key": true,
"lid-mapping": true,
"pre-key": true,
"sender-key": true,
"sender-key-memory": true,
session: true,
tctoken: true
};
function isWhatsAppBaileysAuthFileName(name) {
if (name === "creds.json" || name === "creds.json.bak") return true;
return name.endsWith(".json") && Object.keys(BAILEYS_SIGNAL_AUTH_CATEGORIES).some((category) => name.startsWith(`${category}-`));
}
//#endregion
//#region extensions/whatsapp/src/doctor.ts
function resolveLegacyAckScope(ack) {
const direct = ack.direct !== false;
const group = ack.group ?? "mentions";
if (direct) return group === "always" ? "all" : group === "never" ? "direct" : void 0;
return group === "always" ? "group-all" : group === "mentions" ? "group-mentions" : group === "never" ? "off" : void 0;
}
function resolveDefaultAgentEmoji(cfg) {
const entries = asNullableRecord(asNullableRecord(asNullableRecord(cfg)?.agents)?.entries);
const agents = entries ? Object.values(entries).flatMap((value) => {
const entry = asNullableRecord(value);
return entry ? [entry] : [];
}) : [];
const agent = agents.find((entry) => entry.default === true) ?? agents[0];
const emoji = asNullableRecord(agent?.identity)?.emoji;
return typeof emoji === "string" ? emoji : void 0;
}
function normalizeCompatibilityConfig$1({ cfg }) {
const changes = [];
const sources = [];
for (const scope of collectChannelAccountScopes({
cfg,
channelId: "whatsapp"
})) {
const ack = asNullableRecord(scope.account.ackReaction);
if (!ack) continue;
sources.push({
path: `${scope.prefix}.ackReaction`,
account: scope.account,
...typeof ack.emoji === "string" ? { emoji: ack.emoji } : {},
scope: resolveLegacyAckScope(ack),
unrepresentableScope: ack.direct !== false && (ack.group ?? "mentions") === "mentions"
});
}
if (sources.length === 0) return {
config: cfg,
changes
};
const messages = cfg.messages ??= {};
const identityEmoji = resolveDefaultAgentEmoji(cfg) ?? "👀";
for (const source of sources) {
messages.ackReaction ??= source.emoji ?? identityEmoji;
messages.ackReactionScope ??= source.scope;
delete source.account.ackReaction;
changes.push(`Moved translatable ${source.path} settings to messages ack settings.`);
}
const finalEmoji = messages.ackReaction.trim();
const finalScope = messages.ackReactionScope ?? "group-mentions";
const comparableFinalScope = finalScope === "none" ? "off" : finalScope;
for (const source of sources) {
const sourceEmoji = source.emoji?.trim();
if (source.emoji === void 0) changes.push(`${source.path} used a route-dependent agent identity acknowledgement emoji before migration; the final messages.ackReaction is ${JSON.stringify(finalEmoji)}. Review messages.ackReaction.`);
else if (sourceEmoji !== finalEmoji) changes.push(`${source.path} requested acknowledgement emoji ${JSON.stringify(sourceEmoji)}, but the final messages.ackReaction is ${JSON.stringify(finalEmoji)}. Review messages.ackReaction.`);
if (source.unrepresentableScope) changes.push(`${source.path} migration cannot preserve both direct-message and mentioned-group acknowledgements; the final messages.ackReactionScope is ${JSON.stringify(finalScope)}. Review messages.ackReactionScope.`);
else if (source.scope && source.scope !== comparableFinalScope) changes.push(`${source.path} requested acknowledgement scope ${JSON.stringify(source.scope)}, but the final messages.ackReactionScope is ${JSON.stringify(finalScope)}. Review messages.ackReactionScope.`);
}
return {
config: cfg,
changes
};
}
//#endregion
//#region extensions/whatsapp/src/doctor-contract.ts
const streamingAliasMigration = defineChannelAliasMigration({
channelId: "whatsapp",
streaming: {
defaultMode: "partial",
deliveryOnly: true
},
accountStreamingInheritsDefaultAccount: true
});
const hasAckReaction = (value) => Boolean(asNullableRecord(asNullableRecord(value)?.ackReaction));
const legacyDefaultPolicyKeys = /* @__PURE__ */ new Set([
"dmPolicy",
"allowFrom",
"groupPolicy",
"groupAllowFrom"
]);
function hasPossibleLeftoverDefault(cfg) {
const channel = asNullableRecord(cfg.channels?.whatsapp);
const accounts = asNullableRecord(channel?.accounts);
const fallback = asNullableRecord(accounts?.default);
if (!channel || !accounts || !fallback || channel.authDir !== void 0 || typeof channel.defaultAccount === "string" && channel.defaultAccount.trim().toLowerCase() === "default" || Object.keys(accounts).length < 2) return false;
const keys = Object.keys(fallback);
if (keys.length === 0 || keys.some((key) => !legacyDefaultPolicyKeys.has(key)) || Object.values(accounts).some((account) => !asNullableRecord(account)) || cfg.bindings?.some((binding) => binding.match.channel.trim().toLowerCase() === "whatsapp" && binding.match.accountId?.trim().toLowerCase() === "default")) return false;
const oauthDir = resolveOAuthDir();
for (const dir of [oauthDir, path.join(oauthDir, "whatsapp", "default")]) try {
if (fs$1.readdirSync(dir).some(isWhatsAppBaileysAuthFileName)) return false;
} catch (error) {
if (asNullableRecord(error)?.code !== "ENOENT") return false;
}
return true;
}
function collectDefaultAccountWarnings(cfg) {
if (!hasPossibleLeftoverDefault(cfg)) return [];
const namedAccountId = listAccountIds(cfg).find((id) => id !== "default" && normalizeOptionalAccountId(id) === id);
if (!namedAccountId) return [];
const currentAccountId = resolveDefaultWhatsAppAccountId(cfg);
return [`channels.whatsapp.accounts.default contains only shared policy and has no detected credentials. It may be a leftover of an earlier Doctor migration or an intentional account awaiting login. Unqualified WhatsApp operations currently select "${currentAccountId}". Doctor left all accounts and routing unchanged. To select a named account while keeping all accounts and shared policy, run \`openclaw config set channels.whatsapp.defaultAccount '${JSON.stringify(currentAccountId === "default" ? namedAccountId : currentAccountId)}' --strict-json\`. Only if "default" is unwanted, preserve any shared policy you still need, then run \`openclaw channels remove --channel whatsapp --account default --delete\`.`];
}
const legacyConfigRules = [
...streamingAliasMigration.legacyConfigRules,
{
path: [
"channels",
"whatsapp",
"ackReaction"
],
message: "channels.whatsapp.ackReaction moved to global message acknowledgement settings. Run \"openclaw doctor --fix\"."
},
{
path: [
"channels",
"whatsapp",
"accounts"
],
message: "channels.whatsapp.accounts.<id>.ackReaction moved to global message acknowledgement settings. Run \"openclaw doctor --fix\".",
match: (value) => hasLegacyAccountStreamingAliases(value, hasAckReaction)
}
];
function normalizeCompatibilityConfig({ cfg }) {
const ackReaction = normalizeCompatibilityConfig$1({ cfg });
const normalized = streamingAliasMigration.normalizeChannelConfig({
cfg: ackReaction.config,
changes: ackReaction.changes
});
const warnings = collectDefaultAccountWarnings(normalized.config);
return {
...normalized,
...warnings.length ? { warnings } : {}
};
}
//#endregion
export { legacyConfigRules, normalizeCompatibilityConfig };