openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
554 lines (553 loc) • 21.7 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js";
import { _ as uniqueStrings, l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { w as pathExists } from "./fs-safe-aqmM_n6V.js";
import { c as isRecord } from "./utils-CCC-BEJH.js";
import { o as withFileLock$1 } from "./file-lock-BOaqUSu6.js";
import { i as writeJsonFileAtomically } from "./json-store-CWaMsrLM.js";
import "./number-runtime-DBLVDypr.js";
import "./security-runtime-CQm7DD1u.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { t as getMSTeamsRuntime } from "./runtime-6g-cPOGH.js";
import path from "node:path";
import crypto from "node:crypto";
//#region extensions/msteams/src/conversation-store-helpers.ts
function normalizeStoredConversationId(raw) {
return raw.split(";")[0] ?? raw;
}
function parseStoredConversationTimestamp(value) {
if (!value) return null;
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) return null;
return parsed;
}
function toConversationStoreEntries(entries) {
return Array.from(entries, ([conversationId, reference]) => ({
conversationId,
reference
}));
}
function mergeStoredConversationReference(existing, incoming, nowIso) {
return {
...existing?.timezone && !incoming.timezone ? { timezone: existing.timezone } : {},
...existing?.graphChatId && !incoming.graphChatId ? { graphChatId: existing.graphChatId } : {},
...existing?.tenantId && !incoming.tenantId ? { tenantId: existing.tenantId } : {},
...existing?.aadObjectId && !incoming.aadObjectId ? { aadObjectId: existing.aadObjectId } : {},
...incoming,
lastSeenAt: nowIso
};
}
function findPreferredDmConversationByUserId(entries, id) {
const target = id.trim();
if (!target) return null;
const personalMatches = [];
const unknownTypeMatches = [];
for (const entry of entries) {
if (entry.reference.user?.aadObjectId !== target && entry.reference.user?.id !== target) continue;
const convType = normalizeLowercaseStringOrEmpty(entry.reference.conversation?.conversationType ?? "");
if (convType === "personal") personalMatches.push(entry);
else if (convType === "channel" || convType === "groupchat") {} else unknownTypeMatches.push(entry);
}
const candidates = personalMatches.length > 0 ? personalMatches : unknownTypeMatches;
if (candidates.length === 0) return null;
if (candidates.length > 1) candidates.sort((a, b) => (parseStoredConversationTimestamp(b.reference.lastSeenAt) ?? 0) - (parseStoredConversationTimestamp(a.reference.lastSeenAt) ?? 0));
return candidates[0] ?? null;
}
//#endregion
//#region extensions/msteams/src/store-fs.ts
const STORE_LOCK_OPTIONS = {
retries: {
retries: 10,
factor: 2,
minTimeout: 100,
maxTimeout: 1e4,
randomize: true
},
stale: 3e4
};
async function writeJsonFile(filePath, value) {
await writeJsonFileAtomically(filePath, value);
}
async function ensureJsonFile(filePath, fallback) {
if (!await pathExists(filePath)) await writeJsonFile(filePath, fallback);
}
async function withFileLock(filePath, fallback, fn) {
await ensureJsonFile(filePath, fallback);
return await withFileLock$1(filePath, STORE_LOCK_OPTIONS, async () => {
return await fn();
});
}
//#endregion
//#region extensions/msteams/src/sqlite-state.ts
function resolveStateDirOverride(options) {
if (!options) return;
if (options.stateDir) return options.stateDir;
if (options.storePath) return path.dirname(options.storePath);
if (options.homedir) return getMSTeamsRuntime().state.resolveStateDir(options.env ?? process.env, options.homedir);
return options.env?.OPENCLAW_STATE_DIR?.trim() || void 0;
}
function resolveMSTeamsSqliteStateEnv(options) {
const stateDir = resolveStateDirOverride(options);
if (!stateDir) return options?.env;
return {
...options?.env ?? process.env,
OPENCLAW_STATE_DIR: stateDir
};
}
function toPluginJsonValue(value) {
const serialized = JSON.stringify(value);
return JSON.parse(serialized);
}
function resolveMSTeamsSqliteStateDir(options) {
return resolveStateDirOverride(options) ?? getMSTeamsRuntime().state.resolveStateDir(options?.env ?? process.env, options?.homedir);
}
const sqliteMutationLocks = /* @__PURE__ */ new Map();
async function withProcessMutationLock(lockPath, fn) {
const previous = sqliteMutationLocks.get(lockPath) ?? Promise.resolve();
let release = () => {};
const next = new Promise((resolve) => {
release = resolve;
});
const chained = previous.then(() => next, () => next);
sqliteMutationLocks.set(lockPath, chained);
await previous.catch(() => void 0);
try {
return await fn();
} finally {
release();
if (sqliteMutationLocks.get(lockPath) === chained) sqliteMutationLocks.delete(lockPath);
}
}
async function withMSTeamsSqliteMutationLock(options, lockFilename, fn) {
const lockPath = path.join(resolveMSTeamsSqliteStateDir(options), lockFilename);
return await withProcessMutationLock(lockPath, async () => {
return await withFileLock(lockPath, { version: 1 }, fn);
});
}
//#endregion
//#region extensions/msteams/src/conversation-store-state.ts
const MSTEAMS_CONVERSATIONS_LEGACY_FILENAME = "msteams-conversations.json";
const MSTEAMS_CONVERSATIONS_NAMESPACE = "conversations";
const MSTEAMS_MAX_CONVERSATIONS = 1e3;
const MSTEAMS_SQLITE_MAX_CONVERSATION_ROWS = 2e3;
const MSTEAMS_CONVERSATION_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
const CONVERSATION_LOCK_FILENAME = "msteams-conversations.sqlite.lock";
function createConversationStateStore(params) {
return getMSTeamsRuntime().state.openKeyedStore({
namespace: MSTEAMS_CONVERSATIONS_NAMESPACE,
maxEntries: MSTEAMS_SQLITE_MAX_CONVERSATION_ROWS,
env: resolveMSTeamsSqliteStateEnv(params)
});
}
function normalizeMSTeamsLegacyConversationStore(value) {
if (value.version !== 1 || !value.conversations || typeof value.conversations !== "object" || Array.isArray(value.conversations)) return {
version: 1,
conversations: {}
};
return value;
}
function buildMSTeamsConversationStateKey(conversationId) {
return crypto.createHash("sha256").update(conversationId).digest("hex");
}
function prepareMSTeamsConversationReferenceForStorage(conversationId, reference) {
return {
...reference,
conversation: {
...reference.conversation,
id: conversationId
}
};
}
function getStoredConversationId(reference) {
const rawId = reference.conversation?.id;
return rawId ? normalizeStoredConversationId(rawId) : null;
}
function selectRetainedMSTeamsConversations(conversations, ttlMs = MSTEAMS_CONVERSATION_TTL_MS) {
const retained = Object.entries(conversations).filter(([, reference]) => {
const lastSeenAt = parseStoredConversationTimestamp(reference.lastSeenAt);
return lastSeenAt == null || Date.now() - lastSeenAt <= ttlMs;
});
if (retained.length <= 1e3) return retained;
retained.sort((a, b) => {
return (parseStoredConversationTimestamp(a[1].lastSeenAt) ?? 0) - (parseStoredConversationTimestamp(b[1].lastSeenAt) ?? 0) || a[0].localeCompare(b[0]);
});
return retained.slice(retained.length - MSTEAMS_MAX_CONVERSATIONS);
}
function createMSTeamsConversationStoreState(params) {
const ttlMs = params?.ttlMs ?? 31536e6;
const conversationStore = createConversationStateStore(params);
const isExpired = (reference) => {
const lastSeenAt = parseStoredConversationTimestamp(reference.lastSeenAt);
return lastSeenAt != null && Date.now() - lastSeenAt > ttlMs;
};
const lookupStored = async (conversationId) => {
const normalizedId = normalizeStoredConversationId(conversationId);
const value = await conversationStore.lookup(buildMSTeamsConversationStateKey(normalizedId));
if (!value) return null;
if (isExpired(value)) return null;
return value;
};
const entries = async () => {
const rows = await conversationStore.entries();
const kept = [];
for (const row of rows) {
if (isExpired(row.value)) continue;
const conversationId = getStoredConversationId(row.value);
if (conversationId) kept.push([conversationId, row.value]);
}
return kept;
};
const lookup = async (conversationId) => {
return await lookupStored(conversationId);
};
const register = async (conversationId, reference) => {
const normalizedId = normalizeStoredConversationId(conversationId);
await conversationStore.register(buildMSTeamsConversationStateKey(normalizedId), toPluginJsonValue(prepareMSTeamsConversationReferenceForStorage(normalizedId, reference)));
const rows = [];
for (const row of await conversationStore.entries()) {
if (isExpired(row.value)) {
await conversationStore.delete(row.key);
continue;
}
rows.push(row);
}
if (rows.length <= 1e3) return;
const sorted = rows.toSorted((a, b) => {
const aTs = parseStoredConversationTimestamp(a.value.lastSeenAt) ?? 0;
const bTs = parseStoredConversationTimestamp(b.value.lastSeenAt) ?? 0;
const aId = getStoredConversationId(a.value) ?? a.key;
const bId = getStoredConversationId(b.value) ?? b.key;
return aTs - bTs || aId.localeCompare(bId);
});
for (const row of sorted.slice(0, rows.length - MSTEAMS_MAX_CONVERSATIONS)) await conversationStore.delete(row.key);
};
const list = async () => {
return toConversationStoreEntries(await entries());
};
const get = async (conversationId) => {
return await lookup(conversationId);
};
const findPreferredDmByUserId = async (id) => {
return findPreferredDmConversationByUserId(await list(), id);
};
const upsert = async (conversationId, reference) => {
const normalizedId = normalizeStoredConversationId(conversationId);
await withMSTeamsSqliteMutationLock(params, CONVERSATION_LOCK_FILENAME, async () => {
await register(normalizedId, mergeStoredConversationReference(await lookupStored(normalizedId) ?? void 0, reference, (/* @__PURE__ */ new Date()).toISOString()));
});
};
const remove = async (conversationId) => {
const normalizedId = normalizeStoredConversationId(conversationId);
return await withMSTeamsSqliteMutationLock(params, CONVERSATION_LOCK_FILENAME, async () => {
return await conversationStore.delete(buildMSTeamsConversationStateKey(normalizedId));
});
};
return {
upsert,
get,
list,
remove,
findPreferredDmByUserId,
findByUserId: findPreferredDmByUserId
};
}
//#endregion
//#region extensions/msteams/src/polls.ts
const MSTEAMS_POLLS_LEGACY_FILENAME = "msteams-polls.json";
const MSTEAMS_POLLS_NAMESPACE = "polls";
const MSTEAMS_POLL_VOTE_BUCKETS_NAMESPACE = "poll-vote-buckets";
const MSTEAMS_MAX_POLLS = 1e3;
const MSTEAMS_SQLITE_MAX_POLL_ROWS = 2e3;
const MSTEAMS_MAX_POLL_VOTE_BUCKET_ROWS = 1001 * 32;
const MSTEAMS_POLL_TTL_MS = 720 * 60 * 60 * 1e3;
const POLL_LOCK_FILENAME = "msteams-polls.sqlite.lock";
function normalizeChoiceValue(value) {
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
if (typeof value === "number" && Number.isFinite(value)) return String(value);
return null;
}
function extractSelections(value) {
if (Array.isArray(value)) return value.map(normalizeChoiceValue).filter((entry) => Boolean(entry));
const normalized = normalizeChoiceValue(value);
if (!normalized) return [];
if (normalized.includes(",")) return normalizeStringEntries(normalized.split(","));
return [normalized];
}
function readNestedValue(value, keys) {
let current = value;
for (const key of keys) {
if (!isRecord(current)) return;
current = current[key];
}
return current;
}
function readNestedString(value, keys) {
return normalizeOptionalString(readNestedValue(value, keys));
}
function extractMSTeamsPollVote(activity) {
const value = activity?.value;
if (!value || !isRecord(value)) return null;
const pollId = readNestedString(value, ["openclawPollId"]) ?? readNestedString(value, ["pollId"]) ?? readNestedString(value, ["openclaw", "pollId"]) ?? readNestedString(value, [
"openclaw",
"poll",
"id"
]) ?? readNestedString(value, ["data", "openclawPollId"]) ?? readNestedString(value, ["data", "pollId"]) ?? readNestedString(value, [
"data",
"openclaw",
"pollId"
]) ?? readNestedString(value, [
"action",
"data",
"openclawPollId"
]) ?? readNestedString(value, [
"action",
"data",
"pollId"
]);
if (!pollId) return null;
const directSelections = extractSelections(value.choices);
const nestedSelections = extractSelections(readNestedValue(value, ["choices"]));
const dataSelections = extractSelections(readNestedValue(value, ["data", "choices"]));
const actionDataSelections = extractSelections(readNestedValue(value, [
"action",
"data",
"choices"
]));
const selections = directSelections.length > 0 ? directSelections : nestedSelections.length > 0 ? nestedSelections : dataSelections.length > 0 ? dataSelections : actionDataSelections;
if (selections.length === 0) return null;
return {
pollId,
selections
};
}
function buildMSTeamsPollCard(params) {
const pollId = params.pollId ?? crypto.randomUUID();
const maxSelections = typeof params.maxSelections === "number" && params.maxSelections > 1 ? Math.floor(params.maxSelections) : 1;
const cappedMaxSelections = Math.min(Math.max(1, maxSelections), params.options.length);
const choices = params.options.map((option, index) => ({
title: option,
value: String(index)
}));
const hint = cappedMaxSelections > 1 ? `Select up to ${cappedMaxSelections} option${cappedMaxSelections === 1 ? "" : "s"}.` : "Select one option.";
const card = {
type: "AdaptiveCard",
version: "1.5",
body: [
{
type: "TextBlock",
text: params.question,
wrap: true,
weight: "Bolder",
size: "Medium"
},
{
type: "Input.ChoiceSet",
id: "choices",
isMultiSelect: cappedMaxSelections > 1,
style: "expanded",
choices
},
{
type: "TextBlock",
text: hint,
wrap: true,
isSubtle: true,
spacing: "Small"
}
],
actions: [{
type: "Action.Execute",
title: "Vote",
verb: "openclaw.poll.vote",
data: {
openclawPollId: pollId,
pollId
}
}]
};
const fallbackLines = [`Poll: ${params.question}`, ...params.options.map((option, index) => `${index + 1}. ${option}`)];
return {
pollId,
question: params.question,
options: params.options,
maxSelections: cappedMaxSelections,
card,
fallbackText: fallbackLines.join("\n")
};
}
function createPollStateStore(params) {
return getMSTeamsRuntime().state.openKeyedStore({
namespace: MSTEAMS_POLLS_NAMESPACE,
maxEntries: MSTEAMS_SQLITE_MAX_POLL_ROWS,
env: resolveMSTeamsSqliteStateEnv(params)
});
}
function createPollVoteBucketStateStore(params) {
return getMSTeamsRuntime().state.openKeyedStore({
namespace: MSTEAMS_POLL_VOTE_BUCKETS_NAMESPACE,
maxEntries: MSTEAMS_MAX_POLL_VOTE_BUCKET_ROWS,
env: resolveMSTeamsSqliteStateEnv(params)
});
}
function parseTimestamp(value) {
if (!value) return null;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
}
function pruneExpired(polls) {
const cutoff = Date.now() - MSTEAMS_POLL_TTL_MS;
const entries = Object.entries(polls).filter(([, poll]) => {
return (parseTimestamp(poll.updatedAt ?? poll.createdAt) ?? 0) >= cutoff;
});
return Object.fromEntries(entries);
}
function selectRetainedMSTeamsPolls(polls) {
const retained = Object.entries(pruneExpired(polls));
if (retained.length <= 1e3) return retained;
retained.sort((a, b) => {
return (parseTimestamp(a[1].updatedAt ?? a[1].createdAt) ?? 0) - (parseTimestamp(b[1].updatedAt ?? b[1].createdAt) ?? 0) || a[0].localeCompare(b[0]);
});
return retained.slice(retained.length - MSTEAMS_MAX_POLLS);
}
function normalizeMSTeamsPollSelections(poll, selections) {
const maxSelections = Math.max(1, poll.maxSelections);
const mapped = selections.map((entry) => parseStrictNonNegativeInteger(entry)).filter((value) => value !== void 0).filter((value) => value >= 0 && value < poll.options.length).map((value) => String(value));
return uniqueStrings(maxSelections > 1 ? mapped.slice(0, maxSelections) : mapped.slice(0, 1));
}
function splitMSTeamsPoll(poll) {
const { votes, ...metadata } = poll;
return {
metadata,
votes
};
}
function hashMSTeamsPollVote(pollId, voterId) {
return crypto.createHash("sha256").update(pollId).update("\0").update(voterId).digest("hex");
}
function buildMSTeamsPollStateKey(pollId) {
return crypto.createHash("sha256").update(pollId).digest("hex");
}
function selectMSTeamsPollVoteBucket(pollId, voterId) {
const bucket = Number.parseInt(hashMSTeamsPollVote(pollId, voterId).slice(0, 8), 16);
return String(bucket % 32).padStart(4, "0");
}
function buildMSTeamsPollVoteBucketKey(pollId, bucket) {
return `${crypto.createHash("sha256").update(pollId).digest("hex")}:${bucket}`;
}
function createMSTeamsPollStoreState(params) {
const pollStore = createPollStateStore(params);
const voteBucketStore = createPollVoteBucketStateStore(params);
const readPollVotes = async (pollId) => {
const votes = {};
for (const row of await voteBucketStore.entries()) if (row.value.pollId === pollId) Object.assign(votes, row.value.votes);
return votes;
};
const deletePollVotes = async (pollId) => {
for (const row of await voteBucketStore.entries()) if (row.value.pollId === pollId) await voteBucketStore.delete(row.key);
};
const registerPollVotes = async (pollId, votes, updatedAt) => {
const buckets = /* @__PURE__ */ new Map();
for (const [voterId, selections] of Object.entries(votes)) {
const bucket = selectMSTeamsPollVoteBucket(pollId, voterId);
const bucketVotes = buckets.get(bucket) ?? {};
bucketVotes[voterId] = selections;
buckets.set(bucket, bucketVotes);
}
for (const [bucket, bucketVotes] of buckets) {
const key = buildMSTeamsPollVoteBucketKey(pollId, bucket);
const existing = await voteBucketStore.lookup(key);
await voteBucketStore.register(key, toPluginJsonValue({
pollId,
bucket,
votes: {
...bucketVotes,
...existing?.votes
},
updatedAt
}));
}
};
const registerPollVote = async (pollId, voterId, selections, updatedAt) => {
const bucket = selectMSTeamsPollVoteBucket(pollId, voterId);
const key = buildMSTeamsPollVoteBucketKey(pollId, bucket);
const existing = await voteBucketStore.lookup(key);
await voteBucketStore.register(key, toPluginJsonValue({
pollId,
bucket,
votes: {
...existing?.votes,
[voterId]: selections
},
updatedAt
}));
};
const reconstructPoll = async (metadata) => {
return {
...metadata,
votes: await readPollVotes(metadata.id)
};
};
const prunePollStoreToLimit = async () => {
const rows = [];
for (const row of await pollStore.entries()) {
if (!pruneExpired({ [row.key]: row.value })[row.key]) {
await pollStore.delete(row.key);
await deletePollVotes(row.value.id);
continue;
}
rows.push(row);
}
if (rows.length <= 1e3) return;
const sorted = rows.toSorted((a, b) => {
return (parseTimestamp(a.value.updatedAt ?? a.value.createdAt) ?? 0) - (parseTimestamp(b.value.updatedAt ?? b.value.createdAt) ?? 0) || a.key.localeCompare(b.key);
});
for (const row of sorted.slice(0, rows.length - MSTEAMS_MAX_POLLS)) {
await pollStore.delete(row.key);
await deletePollVotes(row.value.id);
}
};
const createPoll = async (poll) => {
await withMSTeamsSqliteMutationLock(params, POLL_LOCK_FILENAME, async () => {
const { metadata, votes } = splitMSTeamsPoll(poll);
await pollStore.register(buildMSTeamsPollStateKey(poll.id), toPluginJsonValue(metadata));
await deletePollVotes(poll.id);
await registerPollVotes(poll.id, votes, poll.updatedAt ?? poll.createdAt);
await prunePollStoreToLimit();
});
};
const getPoll = async (pollId) => {
const poll = await pollStore.lookup(buildMSTeamsPollStateKey(pollId));
if (!poll) return null;
if (!pruneExpired({ [pollId]: poll })[pollId]) return null;
return await reconstructPoll(poll);
};
const recordVote = async (vote) => {
return await withMSTeamsSqliteMutationLock(params, POLL_LOCK_FILENAME, async () => {
const pollKey = buildMSTeamsPollStateKey(vote.pollId);
const poll = await pollStore.lookup(pollKey);
if (!poll) return null;
if (!pruneExpired({ [vote.pollId]: poll })[vote.pollId]) {
await pollStore.delete(pollKey);
await deletePollVotes(vote.pollId);
return null;
}
const normalized = normalizeMSTeamsPollSelections(await reconstructPoll(poll), vote.selections);
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
poll.updatedAt = updatedAt;
await pollStore.register(pollKey, toPluginJsonValue(poll));
await registerPollVote(vote.pollId, vote.voterId, normalized, updatedAt);
await prunePollStoreToLimit();
return await reconstructPoll(poll);
});
};
return {
createPoll,
getPoll,
recordVote
};
}
//#endregion
export { toPluginJsonValue as C, resolveMSTeamsSqliteStateEnv as S, normalizeStoredConversationId as T, buildMSTeamsConversationStateKey as _, MSTEAMS_SQLITE_MAX_POLL_ROWS as a, prepareMSTeamsConversationReferenceForStorage as b, buildMSTeamsPollVoteBucketKey as c, selectMSTeamsPollVoteBucket as d, selectRetainedMSTeamsPolls as f, MSTEAMS_SQLITE_MAX_CONVERSATION_ROWS as g, MSTEAMS_CONVERSATIONS_NAMESPACE as h, MSTEAMS_POLL_VOTE_BUCKETS_NAMESPACE as i, createMSTeamsPollStoreState as l, MSTEAMS_CONVERSATIONS_LEGACY_FILENAME as m, MSTEAMS_POLLS_LEGACY_FILENAME as n, buildMSTeamsPollCard as o, splitMSTeamsPoll as p, MSTEAMS_POLLS_NAMESPACE as r, buildMSTeamsPollStateKey as s, MSTEAMS_MAX_POLL_VOTE_BUCKET_ROWS as t, extractMSTeamsPollVote as u, createMSTeamsConversationStoreState as v, withMSTeamsSqliteMutationLock as w, selectRetainedMSTeamsConversations as x, normalizeMSTeamsLegacyConversationStore as y };