diff --git a/CHANGELOG.md b/CHANGELOG.md index f661dbdf973..6959016b7c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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..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. diff --git a/docs/channels/bluebubbles.md b/docs/channels/bluebubbles.md index b2ba7584c65..f67ddf32d80 100644 --- a/docs/channels/bluebubbles.md +++ b/docs/channels/bluebubbles.md @@ -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.", }, }, }, diff --git a/docs/channels/qqbot.md b/docs/channels/qqbot.md index 9e15e6c59b6..d914cd0f642 100644 --- a/docs/channels/qqbot.md +++ b/docs/channels/qqbot.md @@ -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" }, diff --git a/src/docs/channel-config-examples.test.ts b/src/docs/channel-config-examples.test.ts new file mode 100644 index 00000000000..711c58d6590 --- /dev/null +++ b/src/docs/channel-config-examples.test.ts @@ -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(".") || ""}: ${issue.message}`) + .join("; "); + failures.push(`${location} schema failed: ${issues}`); + } + } + } + expect(failures).toEqual([]); + }); +}); diff --git a/test/vitest/vitest.unit-fast-paths.mjs b/test/vitest/vitest.unit-fast-paths.mjs index c1ee6a2f517..4ce9cd9fa6b 100644 --- a/test/vitest/vitest.unit-fast-paths.mjs +++ b/test/vitest/vitest.unit-fast-paths.mjs @@ -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",