feat: split dirigent_tools + human @mention override #14

Merged
hzhang merged 37 commits from feat/split-tools-and-mention-override into main 2026-03-08 08:02:28 +00:00
5 changed files with 162 additions and 11 deletions
Showing only changes of commit 307235e207 - Show all commits

View 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];
}

View File

@@ -1,7 +1,9 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { initTurnOrder } from "../turn-manager.js"; import { initTurnOrder } from "../turn-manager.js";
import { fetchVisibleChannelBotAccountIds } from "./channel-members.js";
const channelSeenAccounts = new Map<string, Set<string>>(); const channelSeenAccounts = new Map<string, Set<string>>();
const channelBootstrapTried = new Set<string>();
function getAllBotAccountIds(api: OpenClawPluginApi): string[] { function getAllBotAccountIds(api: OpenClawPluginApi): string[] {
const root = (api.config as Record<string, unknown>) || {}; const root = (api.config as Record<string, unknown>) || {};
@@ -35,8 +37,16 @@ export function recordChannelAccount(channelId: string, accountId: string): bool
return true; return true;
} }
export function ensureTurnOrder(api: OpenClawPluginApi, channelId: string): void { export async function ensureTurnOrder(api: OpenClawPluginApi, channelId: string): Promise<void> {
const botAccounts = getChannelBotAccountIds(api, channelId); 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) { if (botAccounts.length > 0) {
initTurnOrder(channelId, botAccounts); initTurnOrder(channelId, botAccounts);
} }

View File

@@ -18,7 +18,7 @@ type BeforeMessageWriteDeps = {
ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void; ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void;
getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig; getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig;
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; 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; resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined;
sendModeratorMessage: ( sendModeratorMessage: (
botToken: string, botToken: string,
@@ -137,7 +137,7 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
return; return;
} }
ensureTurnOrder(api, channelId); void ensureTurnOrder(api, channelId);
const nextSpeaker = onSpeakerDone(channelId, accountId, true); const nextSpeaker = onSpeakerDone(channelId, accountId, true);
sessionAllowed.delete(key); sessionAllowed.delete(key);
sessionTurnHandled.add(key); sessionTurnHandled.add(key);
@@ -166,7 +166,7 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
} }
} }
} else if (hasEndSymbol) { } else if (hasEndSymbol) {
ensureTurnOrder(api, channelId); void ensureTurnOrder(api, channelId);
const nextSpeaker = onSpeakerDone(channelId, accountId, false); const nextSpeaker = onSpeakerDone(channelId, accountId, false);
sessionAllowed.delete(key); sessionAllowed.delete(key);
sessionTurnHandled.add(key); sessionTurnHandled.add(key);

View File

@@ -28,7 +28,7 @@ type BeforeModelResolveDeps = {
resolveAccountId: (api: OpenClawPluginApi, agentId: string) => string | undefined; resolveAccountId: (api: OpenClawPluginApi, agentId: string) => string | undefined;
pruneDecisionMap: () => void; pruneDecisionMap: () => void;
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; 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 { export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): void {
@@ -111,7 +111,7 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo
} }
if (derived.channelId) { if (derived.channelId) {
ensureTurnOrder(api, derived.channelId); await ensureTurnOrder(api, derived.channelId);
const accountId = resolveAccountId(api, ctx.agentId || ""); const accountId = resolveAccountId(api, ctx.agentId || "");
if (accountId) { if (accountId) {
const turnCheck = checkTurn(derived.channelId, accountId); const turnCheck = checkTurn(derived.channelId, accountId);

View File

@@ -14,7 +14,7 @@ type MessageReceivedDeps = {
getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig; getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig;
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
debugCtxSummary: (ctx: Record<string, unknown>, event: Record<string, unknown>) => Record<string, unknown>; 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; getModeratorUserId: (cfg: DirigentConfig) => string | undefined;
recordChannelAccount: (channelId: string, accountId: string) => boolean; recordChannelAccount: (channelId: string, accountId: string) => boolean;
extractMentionedUserIds: (content: string) => string[]; extractMentionedUserIds: (content: string) => string[];
@@ -46,7 +46,7 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
} }
if (preChannelId) { if (preChannelId) {
ensureTurnOrder(api, preChannelId); await ensureTurnOrder(api, preChannelId);
const metadata = (e as Record<string, unknown>).metadata as Record<string, unknown> | undefined; const metadata = (e as Record<string, unknown>).metadata as Record<string, unknown> | undefined;
const from = const from =
(typeof metadata?.senderId === "string" && metadata.senderId) || (typeof metadata?.senderId === "string" && metadata.senderId) ||
@@ -65,7 +65,7 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
if (senderAccountId && senderAccountId !== "default") { if (senderAccountId && senderAccountId !== "default") {
const isNew = recordChannelAccount(preChannelId, senderAccountId); const isNew = recordChannelAccount(preChannelId, senderAccountId);
if (isNew) { if (isNew) {
ensureTurnOrder(api, preChannelId); await ensureTurnOrder(api, preChannelId);
api.logger.info(`dirigent: new account ${senderAccountId} seen in channel=${preChannelId}, turn order updated`); 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); const mentionedAccountIds = mentionedUserIds.map((uid) => userIdMap.get(uid)).filter((aid): aid is string => !!aid);
if (mentionedAccountIds.length > 0) { if (mentionedAccountIds.length > 0) {
ensureTurnOrder(api, preChannelId); await ensureTurnOrder(api, preChannelId);
const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds); const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds);
if (overrideSet) { if (overrideSet) {
api.logger.info( api.logger.info(