mirror of https://github.com/openclaw/openclaw.git
253 lines
6.9 KiB
TypeScript
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;
|
|
}
|