feat: split dirigent_tools + human @mention override #14
141
plugin/core/channel-members.ts
Normal file
141
plugin/core/channel-members.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { buildUserIdToAccountIdMap } from "./identity.js";
|
||||
|
||||
const PERM_VIEW_CHANNEL = 1n << 10n;
|
||||
const PERM_ADMINISTRATOR = 1n << 3n;
|
||||
|
||||
function toBigIntPerm(v: unknown): bigint {
|
||||
if (typeof v === "bigint") return v;
|
||||
if (typeof v === "number") return BigInt(Math.trunc(v));
|
||||
if (typeof v === "string" && v.trim()) {
|
||||
try {
|
||||
return BigInt(v.trim());
|
||||
} catch {
|
||||
return 0n;
|
||||
}
|
||||
}
|
||||
return 0n;
|
||||
}
|
||||
|
||||
function roleOrMemberType(v: unknown): number {
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v === "string" && v.toLowerCase() === "member") return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function discordRequest(token: string, method: string, path: string): Promise<{ ok: boolean; status: number; json: any; text: string }> {
|
||||
const r = await fetch(`https://discord.com/api/v10${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const text = await r.text();
|
||||
let json: any = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return { ok: r.ok, status: r.status, json, text };
|
||||
}
|
||||
|
||||
function canViewChannel(member: any, guildId: string, guildRoles: Map<string, bigint>, channelOverwrites: any[]): boolean {
|
||||
const roleIds: string[] = Array.isArray(member?.roles) ? member.roles : [];
|
||||
let perms = guildRoles.get(guildId) || 0n;
|
||||
for (const rid of roleIds) perms |= guildRoles.get(rid) || 0n;
|
||||
|
||||
if ((perms & PERM_ADMINISTRATOR) !== 0n) return true;
|
||||
|
||||
let everyoneAllow = 0n;
|
||||
let everyoneDeny = 0n;
|
||||
for (const ow of channelOverwrites) {
|
||||
if (String(ow?.id || "") === guildId && roleOrMemberType(ow?.type) === 0) {
|
||||
everyoneAllow = toBigIntPerm(ow?.allow);
|
||||
everyoneDeny = toBigIntPerm(ow?.deny);
|
||||
break;
|
||||
}
|
||||
}
|
||||
perms = (perms & ~everyoneDeny) | everyoneAllow;
|
||||
|
||||
let roleAllow = 0n;
|
||||
let roleDeny = 0n;
|
||||
for (const ow of channelOverwrites) {
|
||||
if (roleOrMemberType(ow?.type) !== 0) continue;
|
||||
const id = String(ow?.id || "");
|
||||
if (id !== guildId && roleIds.includes(id)) {
|
||||
roleAllow |= toBigIntPerm(ow?.allow);
|
||||
roleDeny |= toBigIntPerm(ow?.deny);
|
||||
}
|
||||
}
|
||||
perms = (perms & ~roleDeny) | roleAllow;
|
||||
|
||||
for (const ow of channelOverwrites) {
|
||||
if (roleOrMemberType(ow?.type) !== 1) continue;
|
||||
if (String(ow?.id || "") === String(member?.user?.id || "")) {
|
||||
const allow = toBigIntPerm(ow?.allow);
|
||||
const deny = toBigIntPerm(ow?.deny);
|
||||
perms = (perms & ~deny) | allow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (perms & PERM_VIEW_CHANNEL) !== 0n;
|
||||
}
|
||||
|
||||
function getAnyDiscordToken(api: OpenClawPluginApi): string | undefined {
|
||||
const root = (api.config as Record<string, unknown>) || {};
|
||||
const channels = (root.channels as Record<string, unknown>) || {};
|
||||
const discord = (channels.discord as Record<string, unknown>) || {};
|
||||
const accounts = (discord.accounts as Record<string, Record<string, unknown>>) || {};
|
||||
for (const rec of Object.values(accounts)) {
|
||||
if (typeof rec?.token === "string" && rec.token) return rec.token;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function fetchVisibleChannelBotAccountIds(api: OpenClawPluginApi, channelId: string): Promise<string[]> {
|
||||
const token = getAnyDiscordToken(api);
|
||||
if (!token) return [];
|
||||
|
||||
const ch = await discordRequest(token, "GET", `/channels/${channelId}`);
|
||||
if (!ch.ok) return [];
|
||||
const guildId = String(ch.json?.guild_id || "");
|
||||
if (!guildId) return [];
|
||||
|
||||
const rolesResp = await discordRequest(token, "GET", `/guilds/${guildId}/roles`);
|
||||
if (!rolesResp.ok) return [];
|
||||
const rolePerms = new Map<string, bigint>();
|
||||
for (const r of Array.isArray(rolesResp.json) ? rolesResp.json : []) {
|
||||
rolePerms.set(String(r?.id || ""), toBigIntPerm(r?.permissions));
|
||||
}
|
||||
|
||||
const members: any[] = [];
|
||||
let after = "";
|
||||
while (true) {
|
||||
const q = new URLSearchParams({ limit: "1000" });
|
||||
if (after) q.set("after", after);
|
||||
const mResp = await discordRequest(token, "GET", `/guilds/${guildId}/members?${q.toString()}`);
|
||||
if (!mResp.ok) return [];
|
||||
const batch = Array.isArray(mResp.json) ? mResp.json : [];
|
||||
members.push(...batch);
|
||||
if (batch.length < 1000) break;
|
||||
after = String(batch[batch.length - 1]?.user?.id || "");
|
||||
if (!after) break;
|
||||
}
|
||||
|
||||
const overwrites = Array.isArray(ch.json?.permission_overwrites) ? ch.json.permission_overwrites : [];
|
||||
const visibleUserIds = members
|
||||
.filter((m) => canViewChannel(m, guildId, rolePerms, overwrites))
|
||||
.map((m) => String(m?.user?.id || ""))
|
||||
.filter(Boolean);
|
||||
|
||||
const userToAccount = buildUserIdToAccountIdMap(api);
|
||||
const out = new Set<string>();
|
||||
for (const uid of visibleUserIds) {
|
||||
const aid = userToAccount.get(uid);
|
||||
if (aid) out.add(aid);
|
||||
}
|
||||
return [...out];
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { initTurnOrder } from "../turn-manager.js";
|
||||
import { fetchVisibleChannelBotAccountIds } from "./channel-members.js";
|
||||
|
||||
const channelSeenAccounts = new Map<string, Set<string>>();
|
||||
const channelBootstrapTried = new Set<string>();
|
||||
|
||||
function getAllBotAccountIds(api: OpenClawPluginApi): string[] {
|
||||
const root = (api.config as Record<string, unknown>) || {};
|
||||
@@ -35,8 +37,16 @@ export function recordChannelAccount(channelId: string, accountId: string): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
export function ensureTurnOrder(api: OpenClawPluginApi, channelId: string): void {
|
||||
const botAccounts = getChannelBotAccountIds(api, channelId);
|
||||
export async function ensureTurnOrder(api: OpenClawPluginApi, channelId: string): Promise<void> {
|
||||
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(channelId, aid);
|
||||
botAccounts = getChannelBotAccountIds(api, channelId);
|
||||
}
|
||||
|
||||
if (botAccounts.length > 0) {
|
||||
initTurnOrder(channelId, botAccounts);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type BeforeMessageWriteDeps = {
|
||||
ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void;
|
||||
getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig;
|
||||
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
|
||||
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => void;
|
||||
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void;
|
||||
resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined;
|
||||
sendModeratorMessage: (
|
||||
botToken: string,
|
||||
@@ -137,7 +137,7 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
|
||||
return;
|
||||
}
|
||||
|
||||
ensureTurnOrder(api, channelId);
|
||||
void ensureTurnOrder(api, channelId);
|
||||
const nextSpeaker = onSpeakerDone(channelId, accountId, true);
|
||||
sessionAllowed.delete(key);
|
||||
sessionTurnHandled.add(key);
|
||||
@@ -166,7 +166,7 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
|
||||
}
|
||||
}
|
||||
} else if (hasEndSymbol) {
|
||||
ensureTurnOrder(api, channelId);
|
||||
void ensureTurnOrder(api, channelId);
|
||||
const nextSpeaker = onSpeakerDone(channelId, accountId, false);
|
||||
sessionAllowed.delete(key);
|
||||
sessionTurnHandled.add(key);
|
||||
|
||||
@@ -28,7 +28,7 @@ type BeforeModelResolveDeps = {
|
||||
resolveAccountId: (api: OpenClawPluginApi, agentId: string) => string | undefined;
|
||||
pruneDecisionMap: () => void;
|
||||
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
|
||||
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => void;
|
||||
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): void {
|
||||
@@ -111,7 +111,7 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo
|
||||
}
|
||||
|
||||
if (derived.channelId) {
|
||||
ensureTurnOrder(api, derived.channelId);
|
||||
await ensureTurnOrder(api, derived.channelId);
|
||||
const accountId = resolveAccountId(api, ctx.agentId || "");
|
||||
if (accountId) {
|
||||
const turnCheck = checkTurn(derived.channelId, accountId);
|
||||
|
||||
@@ -14,7 +14,7 @@ type MessageReceivedDeps = {
|
||||
getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig;
|
||||
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
|
||||
debugCtxSummary: (ctx: Record<string, unknown>, event: Record<string, unknown>) => Record<string, unknown>;
|
||||
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => void;
|
||||
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void;
|
||||
getModeratorUserId: (cfg: DirigentConfig) => string | undefined;
|
||||
recordChannelAccount: (channelId: string, accountId: string) => boolean;
|
||||
extractMentionedUserIds: (content: string) => string[];
|
||||
@@ -46,7 +46,7 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
|
||||
}
|
||||
|
||||
if (preChannelId) {
|
||||
ensureTurnOrder(api, preChannelId);
|
||||
await ensureTurnOrder(api, preChannelId);
|
||||
const metadata = (e as Record<string, unknown>).metadata as Record<string, unknown> | undefined;
|
||||
const from =
|
||||
(typeof metadata?.senderId === "string" && metadata.senderId) ||
|
||||
@@ -65,7 +65,7 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
|
||||
if (senderAccountId && senderAccountId !== "default") {
|
||||
const isNew = recordChannelAccount(preChannelId, senderAccountId);
|
||||
if (isNew) {
|
||||
ensureTurnOrder(api, preChannelId);
|
||||
await ensureTurnOrder(api, preChannelId);
|
||||
api.logger.info(`dirigent: new account ${senderAccountId} seen in channel=${preChannelId}, turn order updated`);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
|
||||
const mentionedAccountIds = mentionedUserIds.map((uid) => userIdMap.get(uid)).filter((aid): aid is string => !!aid);
|
||||
|
||||
if (mentionedAccountIds.length > 0) {
|
||||
ensureTurnOrder(api, preChannelId);
|
||||
await ensureTurnOrder(api, preChannelId);
|
||||
const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds);
|
||||
if (overrideSet) {
|
||||
api.logger.info(
|
||||
|
||||
Reference in New Issue
Block a user