refactor(plugin): extract common utils and moderator discord helpers
This commit is contained in:
49
plugin/core/moderator-discord.ts
Normal file
49
plugin/core/moderator-discord.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
function userIdFromToken(token: string): string | undefined {
|
||||||
|
try {
|
||||||
|
const segment = token.split(".")[0];
|
||||||
|
const padded = segment + "=".repeat((4 - (segment.length % 4)) % 4);
|
||||||
|
return Buffer.from(padded, "base64").toString("utf8");
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDiscordUserId(api: OpenClawPluginApi, accountId: string): 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>>) || {};
|
||||||
|
const acct = accounts[accountId];
|
||||||
|
if (!acct?.token || typeof acct.token !== "string") return undefined;
|
||||||
|
return userIdFromToken(acct.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendModeratorMessage(
|
||||||
|
token: string,
|
||||||
|
channelId: string,
|
||||||
|
content: string,
|
||||||
|
logger: { info: (msg: string) => void; warn: (msg: string) => void },
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bot ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content }),
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const text = await r.text();
|
||||||
|
logger.warn(`dirigent: moderator send failed (${r.status}): ${text}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logger.info(`dirigent: moderator message sent to channel=${channelId}`);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`dirigent: moderator send error: ${String(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
plugin/core/utils.ts
Normal file
45
plugin/core/utils.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
type DebugConfig = {
|
||||||
|
enableDebugLogs?: boolean;
|
||||||
|
debugLogChannelIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pickDefined(input: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(input)) {
|
||||||
|
if (v !== undefined) out[k] = v;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldDebugLog(cfg: DebugConfig, channelId?: string): boolean {
|
||||||
|
if (!cfg.enableDebugLogs) return false;
|
||||||
|
const allow = Array.isArray(cfg.debugLogChannelIds) ? cfg.debugLogChannelIds : [];
|
||||||
|
if (allow.length === 0) return true;
|
||||||
|
if (!channelId) return true;
|
||||||
|
return allow.includes(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debugCtxSummary(ctx: Record<string, unknown>, event: Record<string, unknown>) {
|
||||||
|
const meta = ((ctx.metadata || event.metadata || {}) as Record<string, unknown>) || {};
|
||||||
|
return {
|
||||||
|
sessionKey: typeof ctx.sessionKey === "string" ? ctx.sessionKey : undefined,
|
||||||
|
commandSource: typeof ctx.commandSource === "string" ? ctx.commandSource : undefined,
|
||||||
|
messageProvider: typeof ctx.messageProvider === "string" ? ctx.messageProvider : undefined,
|
||||||
|
channel: typeof ctx.channel === "string" ? ctx.channel : undefined,
|
||||||
|
channelId: typeof ctx.channelId === "string" ? ctx.channelId : undefined,
|
||||||
|
senderId: typeof ctx.senderId === "string" ? ctx.senderId : undefined,
|
||||||
|
from: typeof ctx.from === "string" ? ctx.from : undefined,
|
||||||
|
metaSenderId:
|
||||||
|
typeof meta.senderId === "string"
|
||||||
|
? meta.senderId
|
||||||
|
: typeof meta.sender_id === "string"
|
||||||
|
? meta.sender_id
|
||||||
|
: undefined,
|
||||||
|
metaUserId:
|
||||||
|
typeof meta.userId === "string"
|
||||||
|
? meta.userId
|
||||||
|
: typeof meta.user_id === "string"
|
||||||
|
? meta.user_id
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ import { ensurePolicyStateLoaded, persistPolicies, policyState } from "./policy/
|
|||||||
import { buildAgentIdentity, buildUserIdToAccountIdMap, resolveAccountId } from "./core/identity.js";
|
import { buildAgentIdentity, buildUserIdToAccountIdMap, resolveAccountId } from "./core/identity.js";
|
||||||
import { extractMentionedUserIds, getModeratorUserId } from "./core/mentions.js";
|
import { extractMentionedUserIds, getModeratorUserId } from "./core/mentions.js";
|
||||||
import { ensureTurnOrder, recordChannelAccount } from "./core/turn-bootstrap.js";
|
import { ensureTurnOrder, recordChannelAccount } from "./core/turn-bootstrap.js";
|
||||||
|
import { debugCtxSummary, pickDefined, shouldDebugLog } from "./core/utils.js";
|
||||||
|
import { resolveDiscordUserId, sendModeratorMessage } from "./core/moderator-discord.js";
|
||||||
|
|
||||||
// ── No-Reply API child process lifecycle ──────────────────────────────
|
// ── No-Reply API child process lifecycle ──────────────────────────────
|
||||||
let noReplyProcess: ChildProcess | null = null;
|
let noReplyProcess: ChildProcess | null = null;
|
||||||
@@ -110,96 +112,6 @@ function pruneDecisionMap(now = Date.now()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Moderator bot helpers ---
|
|
||||||
|
|
||||||
/** Extract Discord user ID from a bot token (base64-encoded in first segment) */
|
|
||||||
function userIdFromToken(token: string): string | undefined {
|
|
||||||
try {
|
|
||||||
const segment = token.split(".")[0];
|
|
||||||
// Add padding
|
|
||||||
const padded = segment + "=".repeat((4 - (segment.length % 4)) % 4);
|
|
||||||
return Buffer.from(padded, "base64").toString("utf8");
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolve accountId → Discord user ID by reading the account's bot token from config */
|
|
||||||
function resolveDiscordUserId(api: OpenClawPluginApi, accountId: string): 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>>) || {};
|
|
||||||
const acct = accounts[accountId];
|
|
||||||
if (!acct?.token || typeof acct.token !== "string") return undefined;
|
|
||||||
return userIdFromToken(acct.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Send a message as the moderator bot via Discord REST API */
|
|
||||||
async function sendModeratorMessage(token: string, channelId: string, content: string, logger: { info: (msg: string) => void; warn: (msg: string) => void }): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const r = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bot ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ content }),
|
|
||||||
});
|
|
||||||
if (!r.ok) {
|
|
||||||
const text = await r.text();
|
|
||||||
logger.warn(`dirigent: moderator send failed (${r.status}): ${text}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
logger.info(`dirigent: moderator message sent to channel=${channelId}`);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(`dirigent: moderator send error: ${String(err)}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickDefined(input: Record<string, unknown>) {
|
|
||||||
const out: Record<string, unknown> = {};
|
|
||||||
for (const [k, v] of Object.entries(input)) {
|
|
||||||
if (v !== undefined) out[k] = v;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldDebugLog(cfg: DebugConfig, channelId?: string): boolean {
|
|
||||||
if (!cfg.enableDebugLogs) return false;
|
|
||||||
const allow = Array.isArray(cfg.debugLogChannelIds) ? cfg.debugLogChannelIds : [];
|
|
||||||
if (allow.length === 0) return true;
|
|
||||||
if (!channelId) return true;
|
|
||||||
return allow.includes(channelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function debugCtxSummary(ctx: Record<string, unknown>, event: Record<string, unknown>) {
|
|
||||||
const meta = ((ctx.metadata || event.metadata || {}) as Record<string, unknown>) || {};
|
|
||||||
return {
|
|
||||||
sessionKey: typeof ctx.sessionKey === "string" ? ctx.sessionKey : undefined,
|
|
||||||
commandSource: typeof ctx.commandSource === "string" ? ctx.commandSource : undefined,
|
|
||||||
messageProvider: typeof ctx.messageProvider === "string" ? ctx.messageProvider : undefined,
|
|
||||||
channel: typeof ctx.channel === "string" ? ctx.channel : undefined,
|
|
||||||
channelId: typeof ctx.channelId === "string" ? ctx.channelId : undefined,
|
|
||||||
senderId: typeof ctx.senderId === "string" ? ctx.senderId : undefined,
|
|
||||||
from: typeof ctx.from === "string" ? ctx.from : undefined,
|
|
||||||
metaSenderId:
|
|
||||||
typeof meta.senderId === "string"
|
|
||||||
? meta.senderId
|
|
||||||
: typeof meta.sender_id === "string"
|
|
||||||
? meta.sender_id
|
|
||||||
: undefined,
|
|
||||||
metaUserId:
|
|
||||||
typeof meta.userId === "string"
|
|
||||||
? meta.userId
|
|
||||||
: typeof meta.user_id === "string"
|
|
||||||
? meta.user_id
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "dirigent",
|
id: "dirigent",
|
||||||
name: "Dirigent",
|
name: "Dirigent",
|
||||||
|
|||||||
Reference in New Issue
Block a user