openclaw/extensions/qq/src/config-schema.ts
taki 9d036cae3a feat: add QQ channel plugin via NapCatQQ/OneBot v11
- Complete ChannelPlugin implementation for QQ messaging
- WebSocket client with auto-reconnect and heartbeat
- Support private chat and group messages
- OneBot v11 API wrapper (send, receive, group management)
- Onboarding wizard for CLI configuration
- Multi-account support
- 156 unit tests (accounts, client, api, send, monitor, normalize, connection)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:38:24 +08:00

180 lines
6.2 KiB
TypeScript

/**
* QQ Channel Configuration Schema (Zod)
*
* Reference: TelegramConfigSchema in src/config/zod-schema.providers-core.ts
*/
import { z } from "zod";
import {
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
ToolPolicySchema,
normalizeAllowFrom,
} from "clawdbot/plugin-sdk";
// ============================================================================
// Local Schema Definitions (not exported from plugin-sdk)
// ============================================================================
/** Reply threading mode */
const ReplyToModeSchema = z.union([z.literal("off"), z.literal("first"), z.literal("all")]);
/** Retry configuration */
const RetryConfigSchema = z
.object({
attempts: z.number().int().min(1).optional(),
minDelayMs: z.number().int().min(0).optional(),
maxDelayMs: z.number().int().min(0).optional(),
jitter: z.number().min(0).max(1).optional(),
})
.strict()
.optional();
// ============================================================================
// Group Configuration
// ============================================================================
export const QQGroupSchema = z
.object({
/** Whether the group is enabled */
enabled: z.boolean().optional(),
/** Whether to require @mention to trigger */
requireMention: z.boolean().optional(),
/** Tool policy for this group */
tools: ToolPolicySchema,
/** Tool policy by sender */
toolsBySender: z.record(z.string(), ToolPolicySchema).optional(),
/** Skills to enable for this group */
skills: z.array(z.string()).optional(),
/** Allowed senders in this group */
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
/** Custom system prompt for this group */
systemPrompt: z.string().optional(),
})
.strict();
// ============================================================================
// Account Configuration (Base)
// ============================================================================
export const QQAccountSchemaBase = z
.object({
/** Display name for this account */
name: z.string().optional(),
/** Whether this account is enabled */
enabled: z.boolean().optional(),
// Connection settings
/** WebSocket URL for OneBot connection (e.g., "ws://127.0.0.1:3001") */
wsUrl: z.string().optional(),
/** HTTP URL for OneBot API (optional, for hybrid mode) */
httpUrl: z.string().optional(),
/** Access token for authentication */
accessToken: z.string().optional(),
// DM settings
/** DM access policy */
dmPolicy: DmPolicySchema.optional().default("pairing"),
/** Allowed users for DM (when dmPolicy is "allowlist" or "open") */
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
/** DM history limit */
dmHistoryLimit: z.number().int().min(0).optional(),
// Group settings
/** Group access policy */
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
/** Allowed users in groups */
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
/** Group-specific configurations */
groups: z.record(z.string(), QQGroupSchema.optional()).optional(),
/** Group history limit */
historyLimit: z.number().int().min(0).optional(),
// Message settings
/** Reply mode */
replyToMode: ReplyToModeSchema.optional(),
/** Markdown rendering config */
markdown: MarkdownConfigSchema,
/** Max characters per message chunk */
textChunkLimit: z.number().int().positive().optional(),
// Media settings
/** Max media file size in MB */
mediaMaxMb: z.number().positive().optional(),
// Network settings
/** Request timeout in seconds */
timeoutSeconds: z.number().int().positive().optional(),
/** Retry configuration */
retry: RetryConfigSchema,
/** Reconnect interval in ms */
reconnectIntervalMs: z.number().int().positive().optional(),
/** Heartbeat interval in ms */
heartbeatIntervalMs: z.number().int().positive().optional(),
})
.strict();
// ============================================================================
// Validation Helper
// ============================================================================
const requireOpenAllowFrom = (params: {
policy?: string;
allowFrom?: Array<string | number>;
ctx: z.RefinementCtx;
path: Array<string | number>;
message: string;
}) => {
if (params.policy !== "open") return;
const allow = normalizeAllowFrom(params.allowFrom);
if (allow.includes("*")) return;
params.ctx.addIssue({
code: z.ZodIssueCode.custom,
path: params.path,
message: params.message,
});
};
// ============================================================================
// Account Configuration (with validation)
// ============================================================================
export const QQAccountSchema = QQAccountSchemaBase.superRefine((value, ctx) => {
requireOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
path: ["allowFrom"],
message: 'channels.qq.dmPolicy="open" requires channels.qq.allowFrom to include "*"',
});
});
// ============================================================================
// Full QQ Configuration (supports multi-account)
// ============================================================================
export const QQConfigSchema = QQAccountSchemaBase.extend({
/** Named accounts for multi-account support */
accounts: z.record(z.string(), QQAccountSchema.optional()).optional(),
}).superRefine((value, ctx) => {
requireOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
path: ["allowFrom"],
message: 'channels.qq.dmPolicy="open" requires channels.qq.allowFrom to include "*"',
});
});
// ============================================================================
// Default Values
// ============================================================================
export const QQ_DEFAULT_WS_URL = "ws://127.0.0.1:3001";
export const QQ_DEFAULT_TEXT_CHUNK_LIMIT = 4500;
export const QQ_DEFAULT_MEDIA_MAX_MB = 30;
export const QQ_DEFAULT_TIMEOUT_SECONDS = 30;
export const QQ_DEFAULT_RECONNECT_INTERVAL_MS = 5000;
export const QQ_DEFAULT_HEARTBEAT_INTERVAL_MS = 30000;