feat: selectable pairing-notify provider (discord optional, add fabric)
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>
This commit is contained in:
@@ -1,7 +1,22 @@
|
||||
export type NotifyProvider = "discord" | "fabric";
|
||||
|
||||
export interface FabricNotifyConfig {
|
||||
centerApiBase: string;
|
||||
apiKey: string;
|
||||
guildNodeId: string;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
export interface YonexusServerConfig {
|
||||
followerIdentifiers: string[];
|
||||
notifyBotToken: string;
|
||||
adminUserId: 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;
|
||||
@@ -67,14 +82,45 @@ export function validateYonexusServerConfig(raw: unknown): YonexusServerConfig {
|
||||
issues.push("followerIdentifiers must not contain duplicates");
|
||||
}
|
||||
|
||||
const rawNotifyBotToken = source.notifyBotToken;
|
||||
if (!isNonEmptyString(rawNotifyBotToken)) {
|
||||
issues.push("notifyBotToken is required");
|
||||
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 (!isNonEmptyString(rawAdminUserId)) {
|
||||
issues.push("adminUserId is required");
|
||||
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;
|
||||
@@ -93,14 +139,18 @@ export function validateYonexusServerConfig(raw: unknown): YonexusServerConfig {
|
||||
throw new YonexusServerConfigError(issues);
|
||||
}
|
||||
|
||||
const notifyBotToken = rawNotifyBotToken as string;
|
||||
const adminUserId = rawAdminUserId as string;
|
||||
const listenPort = rawListenPort as number;
|
||||
|
||||
return {
|
||||
followerIdentifiers,
|
||||
notifyBotToken: notifyBotToken.trim(),
|
||||
adminUserId: adminUserId.trim(),
|
||||
notifyProvider,
|
||||
notifyBotToken: isNonEmptyString(rawNotifyBotToken)
|
||||
? rawNotifyBotToken.trim()
|
||||
: undefined,
|
||||
adminUserId: isNonEmptyString(rawAdminUserId)
|
||||
? rawAdminUserId.trim()
|
||||
: undefined,
|
||||
fabric,
|
||||
listenHost,
|
||||
listenPort,
|
||||
publicWsUrl
|
||||
|
||||
@@ -42,10 +42,8 @@ import { verifySignature } from "../../../Yonexus.Protocol/src/crypto.js";
|
||||
import type { YonexusServerStore } from "./store.js";
|
||||
import { type ClientConnection, type ServerTransport } from "./transport.js";
|
||||
import { createPairingService, type PairingService } from "../services/pairing.js";
|
||||
import {
|
||||
createDiscordNotificationService,
|
||||
type DiscordNotificationService
|
||||
} from "../notifications/discord.js";
|
||||
import type { PairingNotificationService } from "../notifications/types.js";
|
||||
import { createNotificationService } from "../notifications/factory.js";
|
||||
import { safeErrorMessage } from "./logging.js";
|
||||
import type { ServerRuleRegistry } from "./rules.js";
|
||||
|
||||
@@ -53,7 +51,7 @@ export interface YonexusServerRuntimeOptions {
|
||||
config: YonexusServerConfig;
|
||||
store: YonexusServerStore;
|
||||
transport: ServerTransport;
|
||||
notificationService?: DiscordNotificationService;
|
||||
notificationService?: PairingNotificationService;
|
||||
ruleRegistry?: ServerRuleRegistry;
|
||||
onClientAuthenticated?: (identifier: string) => void;
|
||||
now?: () => number;
|
||||
@@ -70,7 +68,7 @@ export class YonexusServerRuntime {
|
||||
private readonly now: () => number;
|
||||
private readonly registry: ServerRegistry;
|
||||
private readonly pairingService: PairingService;
|
||||
private readonly notificationService: DiscordNotificationService;
|
||||
private readonly notificationService: PairingNotificationService;
|
||||
private readonly sweepIntervalMs: number;
|
||||
private sweepTimer: NodeJS.Timeout | null = null;
|
||||
private started = false;
|
||||
@@ -86,10 +84,7 @@ export class YonexusServerRuntime {
|
||||
this.pairingService = createPairingService({ now: this.now });
|
||||
this.notificationService =
|
||||
options.notificationService ??
|
||||
createDiscordNotificationService({
|
||||
botToken: options.config.notifyBotToken,
|
||||
adminUserId: options.config.adminUserId
|
||||
});
|
||||
createNotificationService(options.config);
|
||||
}
|
||||
|
||||
get state(): ServerLifecycleState {
|
||||
|
||||
Reference in New Issue
Block a user