* feat: per-channel responsePrefix override
Add responsePrefix field to all channel config types and Zod schemas,
enabling per-channel and per-account outbound response prefix overrides.
Resolution cascade (most specific wins):
L1: channels.<ch>.accounts.<id>.responsePrefix
L2: channels.<ch>.responsePrefix
L3: (reserved for channels.defaults)
L4: messages.responsePrefix (existing global)
Semantics:
- undefined -> inherit from parent level
- empty string -> explicitly no prefix (stops cascade)
- "auto" -> derive [identity.name] from routed agent
Changes:
- Core logic: resolveResponsePrefix() in identity.ts accepts
optional channel/accountId and walks the cascade
- resolveEffectiveMessagesConfig() passes channel context through
- Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack,
Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs
- Zod schemas: responsePrefix added for config validation
- All channel handlers wired: telegram, discord, slack, signal,
imessage, line, heartbeat runner, route-reply, native commands
- 23 new tests covering backward compat, channel/account levels,
full cascade, auto keyword, empty string stops, unknown fallthrough
Fully backward compatible - no existing config is affected.
Fixes #8857
* fix: address CI lint + review feedback
- Replace Record<string, any> with proper typed helpers (no-explicit-any)
- Add curly braces to single-line if returns (eslint curly)
- Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types
- Extract getChannelConfig() helper for type-safe dynamic key access
* fix: finish responsePrefix overrides (#9001) (thanks @mudrii)
* fix: normalize prefix wiring and types (#9001) (thanks @mudrii)
---------
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
85 lines
3.0 KiB
TypeScript
85 lines
3.0 KiB
TypeScript
import { MarkdownConfigSchema } from "openclaw/plugin-sdk";
|
|
import { z } from "zod";
|
|
|
|
/**
|
|
* Twitch user roles that can be allowed to interact with the bot
|
|
*/
|
|
const TwitchRoleSchema = z.enum(["moderator", "owner", "vip", "subscriber", "all"]);
|
|
|
|
/**
|
|
* Twitch account configuration schema
|
|
*/
|
|
const TwitchAccountSchema = z.object({
|
|
/** Twitch username */
|
|
username: z.string(),
|
|
/** Twitch OAuth access token (requires chat:read and chat:write scopes) */
|
|
accessToken: z.string(),
|
|
/** Twitch client ID (from Twitch Developer Portal or twitchtokengenerator.com) */
|
|
clientId: z.string().optional(),
|
|
/** Channel name to join */
|
|
channel: z.string().min(1),
|
|
/** Enable this account */
|
|
enabled: z.boolean().optional(),
|
|
/** Allowlist of Twitch user IDs who can interact with the bot (use IDs for safety, not usernames) */
|
|
allowFrom: z.array(z.string()).optional(),
|
|
/** Roles allowed to interact with the bot (e.g., ["moderator", "vip", "subscriber"]) */
|
|
allowedRoles: z.array(TwitchRoleSchema).optional(),
|
|
/** Require @mention to trigger bot responses */
|
|
requireMention: z.boolean().optional(),
|
|
/** Outbound response prefix override for this channel/account. */
|
|
responsePrefix: z.string().optional(),
|
|
/** Twitch client secret (required for token refresh via RefreshingAuthProvider) */
|
|
clientSecret: z.string().optional(),
|
|
/** Refresh token (required for automatic token refresh) */
|
|
refreshToken: z.string().optional(),
|
|
/** Token expiry time in seconds (optional, for token refresh tracking) */
|
|
expiresIn: z.number().nullable().optional(),
|
|
/** Timestamp when token was obtained (optional, for token refresh tracking) */
|
|
obtainmentTimestamp: z.number().optional(),
|
|
});
|
|
|
|
/**
|
|
* Base configuration properties shared by both single and multi-account modes
|
|
*/
|
|
const TwitchConfigBaseSchema = z.object({
|
|
name: z.string().optional(),
|
|
enabled: z.boolean().optional(),
|
|
markdown: MarkdownConfigSchema.optional(),
|
|
});
|
|
|
|
/**
|
|
* Simplified single-account configuration schema
|
|
*
|
|
* Use this for single-account setups. Properties are at the top level,
|
|
* creating an implicit "default" account.
|
|
*/
|
|
const SimplifiedSchema = z.intersection(TwitchConfigBaseSchema, TwitchAccountSchema);
|
|
|
|
/**
|
|
* Multi-account configuration schema
|
|
*
|
|
* Use this for multi-account setups. Each key is an account ID (e.g., "default", "secondary").
|
|
*/
|
|
const MultiAccountSchema = z.intersection(
|
|
TwitchConfigBaseSchema,
|
|
z
|
|
.object({
|
|
/** Per-account configuration (for multi-account setups) */
|
|
accounts: z.record(z.string(), TwitchAccountSchema),
|
|
})
|
|
.refine((val) => Object.keys(val.accounts || {}).length > 0, {
|
|
message: "accounts must contain at least one entry",
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* Twitch plugin configuration schema
|
|
*
|
|
* Supports two mutually exclusive patterns:
|
|
* 1. Simplified single-account: username, accessToken, clientId, channel at top level
|
|
* 2. Multi-account: accounts object with named account configs
|
|
*
|
|
* The union ensures clear discrimination between the two modes.
|
|
*/
|
|
export const TwitchConfigSchema = z.union([SimplifiedSchema, MultiAccountSchema]);
|