fix(docs): validate channel config snippets

This commit is contained in:
Vincent Koc
2026-05-03 15:20:11 -07:00
parent 940487e20f
commit 9ae93179e2
5 changed files with 51 additions and 10 deletions

View File

@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
- Google Meet: refresh realtime browser state during status and retry delayed speech after Meet finishes joining, so a just-opened in-call tab no longer leaves speech stuck behind stale `not-in-call` health.
- Plugins/install: recover the install ledger from the managed npm root when `plugins/installs.json` is empty or partial, so reinstalling Discord and Codex no longer makes the other installed plugin disappear.
- Google Meet: grant Meet media permissions through the Playwright browser context when CDP grants do not affect the attached Chrome page, and report in-call microphone/speaker permission problems instead of marking realtime speech ready.
- Channel docs: keep JSON5 channel config examples parseable and schema-valid, fixing BlueBubbles and QQ Bot snippets that could not be copied into config as shown. Thanks @vincentkoc.
- Tlon: expose `groupInviteAllowlist` in the channel config schema and clarify that group invite auto-accept fails closed without an invite allowlist. Thanks @vincentkoc.
- Google Chat: update the setup example to use the accepted `groups.<space>.enabled` key instead of the legacy `allow` alias, with a schema regression for the documented group shape. Thanks @vincentkoc.
- Control UI/WebChat: collapse duplicate in-flight internal text sends onto the active Gateway run so rapid repeat submits do not start fresh `agent:main:main` dispatches. Fixes #75737. Thanks @dsdsddd1 and @BunsDev.

View File

@@ -267,15 +267,7 @@ With the BlueBubbles Private API enabled, inbound messages arrive with short mes
bluebubbles: {
groups: {
"iMessage;+;chat-family": {
systemPrompt: [
"When replying in this group, always call action=reply with the",
"[[reply_to:N]] messageId from context so your response threads",
"under the triggering message. Never send a new unlinked message.",
"",
"For short acknowledgements ('ok', 'got it', 'on it'), use",
"action=react with an appropriate tapback emoji (❤️, 👍, 😂, ‼️, ❓)",
"instead of sending a text reply.",
].join(" "),
systemPrompt: "When replying in this group, always call action=reply with the [[reply_to:N]] messageId from context so your response threads under the triggering message. Never send a new unlinked message. For short acknowledgements ('ok', 'got it', 'on it'), use action=react with an appropriate tapback emoji (❤️, 👍, 😂, ‼️, ❓) instead of sending a text reply.",
},
},
},

View File

@@ -209,7 +209,7 @@ STT and TTS support two-level configuration with priority fallback:
voice: "your-voice",
},
accounts: {
qq-main: {
"qq-main": {
tts: {
providers: {
openai: { voice: "shimmer" },

View File

@@ -0,0 +1,47 @@
import fs from "node:fs";
import path from "node:path";
import JSON5 from "json5";
import { describe, expect, it } from "vitest";
import { OpenClawSchema } from "../config/zod-schema.js";
const CHANNEL_DOCS_DIR = path.join(process.cwd(), "docs", "channels");
function lineNumberAt(source: string, index: number): number {
return source.slice(0, index).split("\n").length;
}
describe("channel docs config examples", () => {
it("keeps OpenClaw channel config snippets parseable and schema-valid", () => {
const failures: string[] = [];
for (const fileName of fs
.readdirSync(CHANNEL_DOCS_DIR)
.filter((entry) => entry.endsWith(".md"))) {
const docPath = path.join(CHANNEL_DOCS_DIR, fileName);
const markdown = fs.readFileSync(docPath, "utf8");
const blocks = markdown.matchAll(/```(?:json5|json)\n([\s\S]*?)```/g);
for (const match of blocks) {
const code = match[1] ?? "";
if (!/(^|\n)\s*(?:"channels"|channels)\s*:/.test(code)) {
continue;
}
const location = `${fileName}:${lineNumberAt(markdown, match.index ?? 0)}`;
let parsed: unknown;
try {
parsed = JSON5.parse(code);
} catch (error) {
failures.push(`${location} JSON5 parse failed: ${String(error)}`);
continue;
}
const result = OpenClawSchema.safeParse(parsed);
if (!result.success) {
const issues = result.error.issues
.slice(0, 3)
.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
.join("; ");
failures.push(`${location} schema failed: ${issues}`);
}
}
}
expect(failures).toEqual([]);
});
});

View File

@@ -92,6 +92,7 @@ export const forcedUnitFastTestFiles = [
"src/context-engine/context-engine.test.ts",
"src/canvas-host/server.state-dir.test.ts",
"src/docs/clawhub-plugin-docs.test.ts",
"src/docs/channel-config-examples.test.ts",
"src/docs/install-cloud-secrets.test.ts",
"src/docker-build-cache.test.ts",
"src/docker-image-digests.test.ts",