openclaw/src/discord/monitor/thread-bindings.lifecycle.ts

253 lines
6.9 KiB
TypeScript

import { normalizeAccountId } from "../../routing/session-key.js";
import { parseDiscordTarget } from "../targets.js";
import { resolveChannelIdForBinding } from "./thread-bindings.discord-api.js";
import { getThreadBindingManager } from "./thread-bindings.manager.js";
import {
resolveThreadBindingIntroText,
resolveThreadBindingThreadName,
} from "./thread-bindings.messages.js";
import {
BINDINGS_BY_THREAD_ID,
MANAGERS_BY_ACCOUNT_ID,
ensureBindingsLoaded,
getThreadBindingToken,
normalizeThreadId,
rememberRecentUnboundWebhookEcho,
removeBindingRecord,
resolveBindingIdsForSession,
saveBindingsToDisk,
setBindingRecord,
shouldPersistBindingMutations,
} from "./thread-bindings.state.js";
import type { ThreadBindingRecord, ThreadBindingTargetKind } from "./thread-bindings.types.js";
function normalizeNonNegativeMs(raw: number): number {
if (!Number.isFinite(raw)) {
return 0;
}
return Math.max(0, Math.floor(raw));
}
function resolveBindingIdsForTargetSession(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}) {
ensureBindingsLoaded();
const targetSessionKey = params.targetSessionKey.trim();
if (!targetSessionKey) {
return [];
}
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
return resolveBindingIdsForSession({
targetSessionKey,
accountId,
targetKind: params.targetKind,
});
}
export function listThreadBindingsForAccount(accountId?: string): ThreadBindingRecord[] {
const manager = getThreadBindingManager(accountId);
if (!manager) {
return [];
}
return manager.listBindings();
}
export function listThreadBindingsBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}): ThreadBindingRecord[] {
const ids = resolveBindingIdsForTargetSession(params);
return ids
.map((bindingKey) => BINDINGS_BY_THREAD_ID.get(bindingKey))
.filter((entry): entry is ThreadBindingRecord => Boolean(entry));
}
export async function autoBindSpawnedDiscordSubagent(params: {
accountId?: string;
channel?: string;
to?: string;
threadId?: string | number;
childSessionKey: string;
agentId: string;
label?: string;
boundBy?: string;
}): Promise<ThreadBindingRecord | null> {
const channel = params.channel?.trim().toLowerCase();
if (channel !== "discord") {
return null;
}
const manager = getThreadBindingManager(params.accountId);
if (!manager) {
return null;
}
const managerToken = getThreadBindingToken(manager.accountId);
const requesterThreadId = normalizeThreadId(params.threadId);
let channelId = "";
if (requesterThreadId) {
const existing = manager.getByThreadId(requesterThreadId);
if (existing?.channelId?.trim()) {
channelId = existing.channelId.trim();
} else {
channelId =
(await resolveChannelIdForBinding({
accountId: manager.accountId,
token: managerToken,
threadId: requesterThreadId,
})) ?? "";
}
}
if (!channelId) {
const to = params.to?.trim() || "";
if (!to) {
return null;
}
try {
const target = parseDiscordTarget(to, { defaultKind: "channel" });
if (!target || target.kind !== "channel") {
return null;
}
channelId =
(await resolveChannelIdForBinding({
accountId: manager.accountId,
token: managerToken,
threadId: target.id,
})) ?? "";
} catch {
return null;
}
}
return await manager.bindTarget({
threadId: undefined,
channelId,
createThread: true,
threadName: resolveThreadBindingThreadName({
agentId: params.agentId,
label: params.label,
}),
targetKind: "subagent",
targetSessionKey: params.childSessionKey,
agentId: params.agentId,
label: params.label,
boundBy: params.boundBy ?? "system",
introText: resolveThreadBindingIntroText({
agentId: params.agentId,
label: params.label,
idleTimeoutMs: manager.getIdleTimeoutMs(),
maxAgeMs: manager.getMaxAgeMs(),
}),
});
}
export function unbindThreadBindingsBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
reason?: string;
sendFarewell?: boolean;
farewellText?: string;
}): ThreadBindingRecord[] {
const ids = resolveBindingIdsForTargetSession(params);
if (ids.length === 0) {
return [];
}
const removed: ThreadBindingRecord[] = [];
for (const bindingKey of ids) {
const record = BINDINGS_BY_THREAD_ID.get(bindingKey);
if (!record) {
continue;
}
const manager = MANAGERS_BY_ACCOUNT_ID.get(record.accountId);
if (manager) {
const unbound = manager.unbindThread({
threadId: record.threadId,
reason: params.reason,
sendFarewell: params.sendFarewell,
farewellText: params.farewellText,
});
if (unbound) {
removed.push(unbound);
}
continue;
}
const unbound = removeBindingRecord(bindingKey);
if (unbound) {
rememberRecentUnboundWebhookEcho(unbound);
removed.push(unbound);
}
}
if (removed.length > 0 && shouldPersistBindingMutations()) {
saveBindingsToDisk({ force: true });
}
return removed;
}
export function setThreadBindingIdleTimeoutBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
idleTimeoutMs: number;
}): ThreadBindingRecord[] {
const ids = resolveBindingIdsForTargetSession(params);
if (ids.length === 0) {
return [];
}
const idleTimeoutMs = normalizeNonNegativeMs(params.idleTimeoutMs);
const now = Date.now();
const updated: ThreadBindingRecord[] = [];
for (const bindingKey of ids) {
const existing = BINDINGS_BY_THREAD_ID.get(bindingKey);
if (!existing) {
continue;
}
const nextRecord: ThreadBindingRecord = {
...existing,
idleTimeoutMs,
lastActivityAt: now,
};
setBindingRecord(nextRecord);
updated.push(nextRecord);
}
if (updated.length > 0 && shouldPersistBindingMutations()) {
saveBindingsToDisk({ force: true });
}
return updated;
}
export function setThreadBindingMaxAgeBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
maxAgeMs: number;
}): ThreadBindingRecord[] {
const ids = resolveBindingIdsForTargetSession(params);
if (ids.length === 0) {
return [];
}
const maxAgeMs = normalizeNonNegativeMs(params.maxAgeMs);
const now = Date.now();
const updated: ThreadBindingRecord[] = [];
for (const bindingKey of ids) {
const existing = BINDINGS_BY_THREAD_ID.get(bindingKey);
if (!existing) {
continue;
}
const nextRecord: ThreadBindingRecord = {
...existing,
maxAgeMs,
boundAt: now,
lastActivityAt: now,
};
setBindingRecord(nextRecord);
updated.push(nextRecord);
}
if (updated.length > 0 && shouldPersistBindingMutations()) {
saveBindingsToDisk({ force: true });
}
return updated;
}