Files
Dirigent/plugin/core/channel-members.ts
zhi 5b05e91d4e fix: don't block one-shot openclaw subcommands; migrate to current plugin SDK
Sidecar lifecycle:
- Move startSideCar() out of register() into an api.on("gateway_start", ...)
  handler. register() runs in every CLI subprocess that loads plugins
  (e.g. `openclaw completion`, `openclaw doctor`); eagerly spawning a
  long-lived process there hung `openclaw update`'s post-update steps.
- Spawn the sidecar with detached: true, stdio routed to a log file fd,
  and call .unref() so the host's event loop is never held by the child.
  Even if a future caller invokes startSideCar in a non-gateway context,
  it can no longer block that host from exiting.
- Sidecar logs now go to ~/.openclaw/logs/dirigent-sidecar.log instead of
  being piped through the host logger.

Plugin SDK convention update:
- Wrap default export with definePluginEntry({ id, name, description, register })
  per the current openclaw plugin authoring contract.
- Switch all imports from the deprecated root barrel "openclaw/plugin-sdk"
  to focused subpaths "openclaw/plugin-sdk/core" and
  "openclaw/plugin-sdk/plugin-entry".
- Modernize openclaw.plugin.json: drop entry/version, add description,
  declare contracts.tools[] for the 6 tools, set activation.onStartup: true
  so gateway_start fires for this plugin at boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:36:00 +00:00

158 lines
5.2 KiB
TypeScript

import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import type { IdentityRegistry } from "./identity-registry.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 getDiscoveryToken(api: OpenClawPluginApi): string | undefined {
// Prefer moderator bot token from pluginConfig — it has guild member access
const pluginCfg = (api.pluginConfig as Record<string, unknown>) || {};
const moderatorToken = pluginCfg.moderatorBotToken;
if (typeof moderatorToken === "string" && moderatorToken) {
return moderatorToken;
}
// Fall back to any discord account token
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;
}
/**
* Returns agentIds for all agents visible in the channel, resolved via the identity registry.
*/
export async function fetchVisibleChannelBotAccountIds(
api: OpenClawPluginApi,
channelId: string,
identityRegistry?: IdentityRegistry,
): Promise<string[]> {
const token = getDiscoveryToken(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 out = new Set<string>();
if (identityRegistry) {
const discordToAgent = identityRegistry.buildDiscordToAgentMap();
for (const uid of visibleUserIds) {
const aid = discordToAgent.get(uid);
if (aid) out.add(aid);
}
}
return [...out];
}