223 lines
8.7 KiB
JavaScript
223 lines
8.7 KiB
JavaScript
import { registerSessionBindingAdapter, resolveThreadBindingConversationIdFromBindingId, resolveThreadBindingIdleTimeoutMsForChannel, resolveThreadBindingMaxAgeMsForChannel, unregisterSessionBindingAdapter } from "openclaw/plugin-sdk/conversation-runtime";
|
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
|
//#region extensions/feishu/src/thread-bindings.ts
|
|
const FEISHU_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.feishuThreadBindingsState");
|
|
let state;
|
|
function getState() {
|
|
if (!state) {
|
|
const globalStore = globalThis;
|
|
state = globalStore[FEISHU_THREAD_BINDINGS_STATE_KEY] ?? {
|
|
managersByAccountId: /* @__PURE__ */ new Map(),
|
|
bindingsByAccountConversation: /* @__PURE__ */ new Map()
|
|
};
|
|
globalStore[FEISHU_THREAD_BINDINGS_STATE_KEY] = state;
|
|
}
|
|
return state;
|
|
}
|
|
function resolveBindingKey(params) {
|
|
return `${params.accountId}:${params.conversationId}`;
|
|
}
|
|
function toSessionBindingTargetKind(raw) {
|
|
return raw === "subagent" ? "subagent" : "session";
|
|
}
|
|
function toFeishuTargetKind(raw) {
|
|
return raw === "subagent" ? "subagent" : "acp";
|
|
}
|
|
function toSessionBindingRecord(record, defaults) {
|
|
const idleExpiresAt = defaults.idleTimeoutMs > 0 ? record.lastActivityAt + defaults.idleTimeoutMs : void 0;
|
|
const maxAgeExpiresAt = defaults.maxAgeMs > 0 ? record.boundAt + defaults.maxAgeMs : void 0;
|
|
const expiresAt = idleExpiresAt != null && maxAgeExpiresAt != null ? Math.min(idleExpiresAt, maxAgeExpiresAt) : idleExpiresAt ?? maxAgeExpiresAt;
|
|
return {
|
|
bindingId: resolveBindingKey({
|
|
accountId: record.accountId,
|
|
conversationId: record.conversationId
|
|
}),
|
|
targetSessionKey: record.targetSessionKey,
|
|
targetKind: toSessionBindingTargetKind(record.targetKind),
|
|
conversation: {
|
|
channel: "feishu",
|
|
accountId: record.accountId,
|
|
conversationId: record.conversationId,
|
|
parentConversationId: record.parentConversationId
|
|
},
|
|
status: "active",
|
|
boundAt: record.boundAt,
|
|
expiresAt,
|
|
metadata: {
|
|
agentId: record.agentId,
|
|
label: record.label,
|
|
boundBy: record.boundBy,
|
|
deliveryTo: record.deliveryTo,
|
|
deliveryThreadId: record.deliveryThreadId,
|
|
lastActivityAt: record.lastActivityAt,
|
|
idleTimeoutMs: defaults.idleTimeoutMs,
|
|
maxAgeMs: defaults.maxAgeMs
|
|
}
|
|
};
|
|
}
|
|
function createFeishuThreadBindingManager(params) {
|
|
const accountId = normalizeAccountId(params.accountId);
|
|
const existing = getState().managersByAccountId.get(accountId);
|
|
if (existing) return existing;
|
|
const idleTimeoutMs = resolveThreadBindingIdleTimeoutMsForChannel({
|
|
cfg: params.cfg,
|
|
channel: "feishu",
|
|
accountId
|
|
});
|
|
const maxAgeMs = resolveThreadBindingMaxAgeMsForChannel({
|
|
cfg: params.cfg,
|
|
channel: "feishu",
|
|
accountId
|
|
});
|
|
const manager = {
|
|
accountId,
|
|
getByConversationId: (conversationId) => getState().bindingsByAccountConversation.get(resolveBindingKey({
|
|
accountId,
|
|
conversationId
|
|
})),
|
|
listBySessionKey: (targetSessionKey) => [...getState().bindingsByAccountConversation.values()].filter((record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey),
|
|
bindConversation: ({ conversationId, parentConversationId, targetKind, targetSessionKey, metadata }) => {
|
|
const normalizedConversationId = conversationId.trim();
|
|
const normalizedTargetSessionKey = targetSessionKey.trim();
|
|
if (!normalizedConversationId || !normalizedTargetSessionKey) return null;
|
|
const existingLocal = getState().bindingsByAccountConversation.get(resolveBindingKey({
|
|
accountId,
|
|
conversationId: normalizedConversationId
|
|
}));
|
|
const now = Date.now();
|
|
const record = {
|
|
accountId,
|
|
conversationId: normalizedConversationId,
|
|
parentConversationId: normalizeOptionalString(parentConversationId) ?? existingLocal?.parentConversationId,
|
|
deliveryTo: typeof metadata?.deliveryTo === "string" && metadata.deliveryTo.trim() ? metadata.deliveryTo.trim() : existingLocal?.deliveryTo,
|
|
deliveryThreadId: typeof metadata?.deliveryThreadId === "string" && metadata.deliveryThreadId.trim() ? metadata.deliveryThreadId.trim() : existingLocal?.deliveryThreadId,
|
|
targetKind: toFeishuTargetKind(targetKind),
|
|
targetSessionKey: normalizedTargetSessionKey,
|
|
agentId: typeof metadata?.agentId === "string" && metadata.agentId.trim() ? metadata.agentId.trim() : existingLocal?.agentId ?? resolveAgentIdFromSessionKey(normalizedTargetSessionKey),
|
|
label: typeof metadata?.label === "string" && metadata.label.trim() ? metadata.label.trim() : existingLocal?.label,
|
|
boundBy: typeof metadata?.boundBy === "string" && metadata.boundBy.trim() ? metadata.boundBy.trim() : existingLocal?.boundBy,
|
|
boundAt: now,
|
|
lastActivityAt: now
|
|
};
|
|
getState().bindingsByAccountConversation.set(resolveBindingKey({
|
|
accountId,
|
|
conversationId: normalizedConversationId
|
|
}), record);
|
|
return record;
|
|
},
|
|
touchConversation: (conversationId, at = Date.now()) => {
|
|
const key = resolveBindingKey({
|
|
accountId,
|
|
conversationId
|
|
});
|
|
const existingRecord = getState().bindingsByAccountConversation.get(key);
|
|
if (!existingRecord) return null;
|
|
const updated = {
|
|
...existingRecord,
|
|
lastActivityAt: at
|
|
};
|
|
getState().bindingsByAccountConversation.set(key, updated);
|
|
return updated;
|
|
},
|
|
unbindConversation: (conversationId) => {
|
|
const key = resolveBindingKey({
|
|
accountId,
|
|
conversationId
|
|
});
|
|
const existingRecord = getState().bindingsByAccountConversation.get(key);
|
|
if (!existingRecord) return null;
|
|
getState().bindingsByAccountConversation.delete(key);
|
|
return existingRecord;
|
|
},
|
|
unbindBySessionKey: (targetSessionKey) => {
|
|
const removed = [];
|
|
for (const record of getState().bindingsByAccountConversation.values()) {
|
|
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) continue;
|
|
getState().bindingsByAccountConversation.delete(resolveBindingKey({
|
|
accountId,
|
|
conversationId: record.conversationId
|
|
}));
|
|
removed.push(record);
|
|
}
|
|
return removed;
|
|
},
|
|
stop: () => {
|
|
for (const key of getState().bindingsByAccountConversation.keys()) if (key.startsWith(`${accountId}:`)) getState().bindingsByAccountConversation.delete(key);
|
|
getState().managersByAccountId.delete(accountId);
|
|
unregisterSessionBindingAdapter({
|
|
channel: "feishu",
|
|
accountId,
|
|
adapter: sessionBindingAdapter
|
|
});
|
|
}
|
|
};
|
|
const sessionBindingAdapter = {
|
|
channel: "feishu",
|
|
accountId,
|
|
capabilities: { placements: ["current"] },
|
|
bind: async (input) => {
|
|
if (input.conversation.channel !== "feishu" || input.placement === "child") return null;
|
|
const bound = manager.bindConversation({
|
|
conversationId: input.conversation.conversationId,
|
|
parentConversationId: input.conversation.parentConversationId,
|
|
targetKind: input.targetKind,
|
|
targetSessionKey: input.targetSessionKey,
|
|
metadata: input.metadata
|
|
});
|
|
return bound ? toSessionBindingRecord(bound, {
|
|
idleTimeoutMs,
|
|
maxAgeMs
|
|
}) : null;
|
|
},
|
|
listBySession: (targetSessionKey) => manager.listBySessionKey(targetSessionKey).map((entry) => toSessionBindingRecord(entry, {
|
|
idleTimeoutMs,
|
|
maxAgeMs
|
|
})),
|
|
resolveByConversation: (ref) => {
|
|
if (ref.channel !== "feishu") return null;
|
|
const found = manager.getByConversationId(ref.conversationId);
|
|
return found ? toSessionBindingRecord(found, {
|
|
idleTimeoutMs,
|
|
maxAgeMs
|
|
}) : null;
|
|
},
|
|
touch: (bindingId, at) => {
|
|
const conversationId = resolveThreadBindingConversationIdFromBindingId({
|
|
accountId,
|
|
bindingId
|
|
});
|
|
if (conversationId) manager.touchConversation(conversationId, at);
|
|
},
|
|
unbind: async (input) => {
|
|
if (input.targetSessionKey?.trim()) return manager.unbindBySessionKey(input.targetSessionKey.trim()).map((entry) => toSessionBindingRecord(entry, {
|
|
idleTimeoutMs,
|
|
maxAgeMs
|
|
}));
|
|
const conversationId = resolveThreadBindingConversationIdFromBindingId({
|
|
accountId,
|
|
bindingId: input.bindingId
|
|
});
|
|
if (!conversationId) return [];
|
|
const removed = manager.unbindConversation(conversationId);
|
|
return removed ? [toSessionBindingRecord(removed, {
|
|
idleTimeoutMs,
|
|
maxAgeMs
|
|
})] : [];
|
|
}
|
|
};
|
|
registerSessionBindingAdapter(sessionBindingAdapter);
|
|
getState().managersByAccountId.set(accountId, manager);
|
|
return manager;
|
|
}
|
|
function getFeishuThreadBindingManager(accountId) {
|
|
return getState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
|
|
}
|
|
const testing = { resetFeishuThreadBindingsForTests() {
|
|
for (const manager of getState().managersByAccountId.values()) manager.stop();
|
|
getState().managersByAccountId.clear();
|
|
getState().bindingsByAccountConversation.clear();
|
|
} };
|
|
//#endregion
|
|
export { getFeishuThreadBindingManager as n, testing as r, createFeishuThreadBindingManager as t };
|