Pairing-code delivery was hardwired to Discord DM (notifyBotToken +
adminUserId required). Make the provider config-selectable.
- core/config.ts: add notifyProvider ("discord"|"fabric", default
"discord" for back-compat); discord fields required only for discord;
add fabric block (centerApiBase/apiKey/guildNodeId/channelId) required
only for fabric
- notifications/types.ts: neutral PairingNotificationService interface
(DiscordNotificationService kept as back-compat alias)
- notifications/fabric.ts: post the pairing message to a Fabric channel
(agent/login -> guild token -> POST messages); self-contained, no
Fabric plugin dependency
- notifications/factory.ts: select provider from config
- core/runtime.ts: wire via factory
- openclaw.plugin.json: notifyProvider enum + fabric object; drop
notifyBotToken/adminUserId from required (conditional in code)
- tests: fabric notifier + provider-selection config (80 passing)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
export type NotifyProvider = "discord" | "fabric";
|
|
|
|
export interface FabricNotifyConfig {
|
|
centerApiBase: string;
|
|
apiKey: string;
|
|
guildNodeId: string;
|
|
channelId: string;
|
|
}
|
|
|
|
export interface YonexusServerConfig {
|
|
followerIdentifiers: string[];
|
|
/** Pairing-code delivery provider. Default: "discord" (back-compat). */
|
|
notifyProvider: NotifyProvider;
|
|
/** Required only when notifyProvider === "discord". */
|
|
notifyBotToken?: string;
|
|
/** Required only when notifyProvider === "discord". */
|
|
adminUserId?: string;
|
|
/** Required only when notifyProvider === "fabric". */
|
|
fabric?: FabricNotifyConfig;
|
|
listenHost?: string;
|
|
listenPort: number;
|
|
publicWsUrl?: string;
|
|
}
|
|
|
|
export class YonexusServerConfigError extends Error {
|
|
readonly issues: string[];
|
|
|
|
constructor(issues: string[]) {
|
|
super(`Invalid Yonexus.Server config: ${issues.join("; ")}`);
|
|
this.name = "YonexusServerConfigError";
|
|
this.issues = issues;
|
|
}
|
|
}
|
|
|
|
function isNonEmptyString(value: unknown): value is string {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
|
|
function normalizeOptionalString(value: unknown): string | undefined {
|
|
if (value === undefined || value === null) {
|
|
return undefined;
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
function isValidPort(value: unknown): value is number {
|
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
}
|
|
|
|
function isValidWsUrl(value: string): boolean {
|
|
try {
|
|
const url = new URL(value);
|
|
return url.protocol === "ws:" || url.protocol === "wss:";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function validateYonexusServerConfig(raw: unknown): YonexusServerConfig {
|
|
const source = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
|
const issues: string[] = [];
|
|
|
|
const rawIdentifiers = source.followerIdentifiers;
|
|
const followerIdentifiers = Array.isArray(rawIdentifiers)
|
|
? rawIdentifiers
|
|
.filter((value): value is string => typeof value === "string")
|
|
.map((value) => value.trim())
|
|
.filter((value) => value.length > 0)
|
|
: [];
|
|
|
|
if (!Array.isArray(rawIdentifiers)) {
|
|
issues.push("followerIdentifiers must be an array");
|
|
}
|
|
|
|
if (new Set(followerIdentifiers).size !== followerIdentifiers.length) {
|
|
issues.push("followerIdentifiers must not contain duplicates");
|
|
}
|
|
|
|
const rawNotifyProvider = source.notifyProvider;
|
|
let notifyProvider: NotifyProvider = "discord";
|
|
if (rawNotifyProvider === undefined || rawNotifyProvider === null) {
|
|
notifyProvider = "discord";
|
|
} else if (rawNotifyProvider === "discord" || rawNotifyProvider === "fabric") {
|
|
notifyProvider = rawNotifyProvider;
|
|
} else {
|
|
issues.push('notifyProvider must be "discord" or "fabric"');
|
|
}
|
|
|
|
// Discord fields — required only for the discord provider.
|
|
const rawNotifyBotToken = source.notifyBotToken;
|
|
const rawAdminUserId = source.adminUserId;
|
|
if (notifyProvider === "discord") {
|
|
if (!isNonEmptyString(rawNotifyBotToken)) {
|
|
issues.push('notifyBotToken is required when notifyProvider is "discord"');
|
|
}
|
|
if (!isNonEmptyString(rawAdminUserId)) {
|
|
issues.push('adminUserId is required when notifyProvider is "discord"');
|
|
}
|
|
}
|
|
|
|
// Fabric fields — required only for the fabric provider.
|
|
let fabric: FabricNotifyConfig | undefined;
|
|
if (notifyProvider === "fabric") {
|
|
const f = (source.fabric && typeof source.fabric === "object"
|
|
? source.fabric
|
|
: {}) as Record<string, unknown>;
|
|
const centerApiBase = normalizeOptionalString(f.centerApiBase);
|
|
const apiKey = normalizeOptionalString(f.apiKey);
|
|
const guildNodeId = normalizeOptionalString(f.guildNodeId);
|
|
const channelId = normalizeOptionalString(f.channelId);
|
|
if (!centerApiBase) issues.push("fabric.centerApiBase is required when notifyProvider is \"fabric\"");
|
|
if (!apiKey) issues.push("fabric.apiKey is required when notifyProvider is \"fabric\"");
|
|
if (!guildNodeId) issues.push("fabric.guildNodeId is required when notifyProvider is \"fabric\"");
|
|
if (!channelId) issues.push("fabric.channelId is required when notifyProvider is \"fabric\"");
|
|
if (centerApiBase && apiKey && guildNodeId && channelId) {
|
|
fabric = { centerApiBase, apiKey, guildNodeId, channelId };
|
|
}
|
|
}
|
|
|
|
const rawListenPort = source.listenPort;
|
|
if (!isValidPort(rawListenPort)) {
|
|
issues.push("listenPort must be an integer between 1 and 65535");
|
|
}
|
|
|
|
const listenHost = normalizeOptionalString(source.listenHost) ?? "0.0.0.0";
|
|
const publicWsUrl = normalizeOptionalString(source.publicWsUrl);
|
|
|
|
if (publicWsUrl !== undefined && !isValidWsUrl(publicWsUrl)) {
|
|
issues.push("publicWsUrl must be a valid ws:// or wss:// URL when provided");
|
|
}
|
|
|
|
if (issues.length > 0) {
|
|
throw new YonexusServerConfigError(issues);
|
|
}
|
|
|
|
const listenPort = rawListenPort as number;
|
|
|
|
return {
|
|
followerIdentifiers,
|
|
notifyProvider,
|
|
notifyBotToken: isNonEmptyString(rawNotifyBotToken)
|
|
? rawNotifyBotToken.trim()
|
|
: undefined,
|
|
adminUserId: isNonEmptyString(rawAdminUserId)
|
|
? rawAdminUserId.trim()
|
|
: undefined,
|
|
fabric,
|
|
listenHost,
|
|
listenPort,
|
|
publicWsUrl
|
|
};
|
|
}
|