import fs from "node:fs"; import path from "node:path"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { initTurnOrder } from "../turn-manager.js"; import { fetchVisibleChannelBotAccountIds } from "./channel-members.js"; const channelSeenAccounts = new Map>(); const channelBootstrapTried = new Set(); let cacheLoaded = false; function cachePath(api: OpenClawPluginApi): string { return api.resolvePath("~/.openclaw/dirigent-channel-members.json"); } function loadCache(api: OpenClawPluginApi): void { if (cacheLoaded) return; cacheLoaded = true; const p = cachePath(api); try { if (!fs.existsSync(p)) return; const raw = fs.readFileSync(p, "utf8"); const parsed = JSON.parse(raw) as Record; for (const [channelId, rec] of Object.entries(parsed || {})) { const ids = Array.isArray(rec?.botAccountIds) ? rec.botAccountIds.filter(Boolean) : []; if (ids.length > 0) channelSeenAccounts.set(channelId, new Set(ids)); } } catch (err) { api.logger.warn?.(`dirigent: failed loading channel member cache: ${String(err)}`); } } function inferGuildIdFromChannelId(api: OpenClawPluginApi, channelId: string): string | undefined { const root = (api.config as Record) || {}; const channels = (root.channels as Record) || {}; const discord = (channels.discord as Record) || {}; const accounts = (discord.accounts as Record>) || {}; for (const rec of Object.values(accounts)) { const chMap = (rec?.channels as Record> | undefined) || undefined; if (!chMap) continue; const direct = chMap[channelId]; const prefixed = chMap[`channel:${channelId}`]; const found = (direct || prefixed) as Record | undefined; if (found && typeof found.guildId === "string" && found.guildId.trim()) return found.guildId.trim(); } return undefined; } function persistCache(api: OpenClawPluginApi): void { const p = cachePath(api); const out: Record = {}; for (const [channelId, set] of channelSeenAccounts.entries()) { out[channelId] = { botAccountIds: [...set], updatedAt: new Date().toISOString(), source: "dirigent/turn-bootstrap", guildId: inferGuildIdFromChannelId(api, channelId), }; } try { fs.mkdirSync(path.dirname(p), { recursive: true }); const tmp = `${p}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(out, null, 2)}\n`, "utf8"); fs.renameSync(tmp, p); } catch (err) { api.logger.warn?.(`dirigent: failed persisting channel member cache: ${String(err)}`); } } function getAllBotAccountIds(api: OpenClawPluginApi): string[] { const root = (api.config as Record) || {}; const bindings = root.bindings as Array> | undefined; if (!Array.isArray(bindings)) return []; const ids: string[] = []; for (const b of bindings) { const match = b.match as Record | undefined; if (match?.channel === "discord" && typeof match.accountId === "string") { ids.push(match.accountId); } } return ids; } function getChannelBotAccountIds(api: OpenClawPluginApi, channelId: string): string[] { const allBots = new Set(getAllBotAccountIds(api)); const seen = channelSeenAccounts.get(channelId); if (!seen) return []; return [...seen].filter((id) => allBots.has(id)); } export function recordChannelAccount(api: OpenClawPluginApi, channelId: string, accountId: string): boolean { loadCache(api); let seen = channelSeenAccounts.get(channelId); if (!seen) { seen = new Set(); channelSeenAccounts.set(channelId, seen); } if (seen.has(accountId)) return false; seen.add(accountId); persistCache(api); return true; } export async function ensureTurnOrder(api: OpenClawPluginApi, channelId: string): Promise { loadCache(api); let botAccounts = getChannelBotAccountIds(api, channelId); if (botAccounts.length === 0 && !channelBootstrapTried.has(channelId)) { channelBootstrapTried.add(channelId); const discovered = await fetchVisibleChannelBotAccountIds(api, channelId).catch(() => [] as string[]); for (const aid of discovered) recordChannelAccount(api, channelId, aid); botAccounts = getChannelBotAccountIds(api, channelId); } if (botAccounts.length > 0) { initTurnOrder(channelId, botAccounts); } }