diff --git a/CHANGELOG.md b/CHANGELOG.md index c701260ce25..0992099c092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Config/messages: coerce boolean `messages.visibleReplies` and `messages.groupChat.visibleReplies` values to the documented enum modes so an intuitive toggle no longer invalidates config and drops channel startup. Fixes #75390. Thanks @scottgl9. - Feishu: accept and honor `channels.feishu.blockStreaming` at the top level and per account, while keeping the legacy default off so Feishu cards no longer reject documented config or silently drop block replies. Fixes #75555. Thanks @vincentkoc. - Google Chat: normalize custom Google auth transport headers before google-auth/gaxios interceptors run, restoring webhook token verification when certificate retrieval expects Fetch `Headers`. Fixes #76742. Thanks @donbowman. - Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index cebf2cf93e7..3f928e64247 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -d9dbaace82aff4445be6ed11e52e69b4548239e3a4e659538f96dfb3ed3c57ac config-baseline.json -9d4d4ab553dadca237d837f71dc7fc13e4ea65d33a564c2ea6775180c413e86a config-baseline.core.json -f2a1aad257c570b497865680c331568a6775369528749826dfa35c1f644483fc config-baseline.channel.json -858f82733d9828b28bf88bc226e155d8417c494215eb3f808f15799daa42a7d7 config-baseline.plugin.json +c1de046645b03b1ec47ec41811b67c0e7ad5460842b54416a47757ef22b9b17e config-baseline.json +f945a060012b3e7c675fb3ea0c5f18996cdcc06c9ec6cead389e04791a529ce9 config-baseline.core.json +76979aba007500abc52b970da76b6512291916739c29d6a3f4218772d1a31186 config-baseline.channel.json +245aa98aabc6c2e3c57a69e639c2fb10d84a7e1e1b3bcdadc340fa61ca998287 config-baseline.plugin.json diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index b3ce72b3784..fd4549d1de2 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -7388,8 +7388,15 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { maximum: 9007199254740991, }, visibleReplies: { - type: "string", - enum: ["automatic", "message_tool"], + anyOf: [ + { + type: "string", + enum: ["automatic", "message_tool"], + }, + { + type: "boolean", + }, + ], }, }, additionalProperties: false, @@ -19019,8 +19026,15 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { "Prefix text prepended to inbound user messages before they are handed to the agent runtime. Use this sparingly for channel context markers and keep it stable across sessions.", }, visibleReplies: { - type: "string", - enum: ["automatic", "message_tool"], + anyOf: [ + { + type: "string", + enum: ["automatic", "message_tool"], + }, + { + type: "boolean", + }, + ], title: "Visible Replies", description: 'Controls visible source replies across direct, group, and channel conversations. "message_tool" keeps normal final replies private and requires message(action=send) for visible output; "automatic" posts normal replies as before.', @@ -19052,8 +19066,15 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { "Maximum number of prior group messages loaded as context per turn for group sessions. Use higher values for richer continuity, or lower values for faster and cheaper responses.", }, visibleReplies: { - type: "string", - enum: ["automatic", "message_tool"], + anyOf: [ + { + type: "string", + enum: ["automatic", "message_tool"], + }, + { + type: "boolean", + }, + ], title: "Group Visible Replies", description: 'Overrides visible source replies for group/channel conversations. Defaults to "message_tool" when no global visible reply policy is set. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.', diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 137d7a4b2fd..9e7885537b6 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -386,11 +386,25 @@ export const ModelsConfigSchema = z .strict() .optional(); +const VisibleRepliesValueSchema = z.enum(["automatic", "message_tool"]); + +export const VisibleRepliesSchema = z + .union([VisibleRepliesValueSchema, z.boolean()]) + .overwrite((value) => { + if (value === true) { + return "automatic"; + } + if (value === false) { + return "message_tool"; + } + return value; + }); + export const GroupChatSchema = z .object({ mentionPatterns: z.array(z.string()).optional(), historyLimit: z.number().int().positive().optional(), - visibleReplies: z.enum(["automatic", "message_tool"]).optional(), + visibleReplies: VisibleRepliesSchema.optional(), }) .strict() .optional(); diff --git a/src/config/zod-schema.session.ts b/src/config/zod-schema.session.ts index 98c6e801044..e9bb3befa91 100644 --- a/src/config/zod-schema.session.ts +++ b/src/config/zod-schema.session.ts @@ -11,6 +11,7 @@ import { QueueSchema, TypingModeSchema, TtsConfigSchema, + VisibleRepliesSchema, } from "./zod-schema.core.js"; import { sensitive } from "./zod-schema.sensitive.js"; @@ -152,7 +153,7 @@ export const SessionSchema = z export const MessagesSchema = z .object({ messagePrefix: z.string().optional(), - visibleReplies: z.enum(["automatic", "message_tool"]).optional(), + visibleReplies: VisibleRepliesSchema.optional(), responsePrefix: z.string().optional(), groupChat: GroupChatSchema, queue: QueueSchema, diff --git a/src/config/zod-schema.visible-replies.test.ts b/src/config/zod-schema.visible-replies.test.ts new file mode 100644 index 00000000000..e0030460302 --- /dev/null +++ b/src/config/zod-schema.visible-replies.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { validateConfigObjectRaw } from "./validation.js"; + +describe("visible reply config schema", () => { + it("coerces boolean global visibleReplies values to the enum contract", () => { + const automatic = validateConfigObjectRaw({ + messages: { + visibleReplies: true, + }, + }); + const toolOnly = validateConfigObjectRaw({ + messages: { + visibleReplies: false, + }, + }); + + expect(automatic.ok).toBe(true); + expect(toolOnly.ok).toBe(true); + if (automatic.ok) { + expect(automatic.config.messages?.visibleReplies).toBe("automatic"); + } + if (toolOnly.ok) { + expect(toolOnly.config.messages?.visibleReplies).toBe("message_tool"); + } + }); + + it("coerces boolean groupChat visibleReplies values to the enum contract", () => { + const automatic = validateConfigObjectRaw({ + messages: { + groupChat: { + visibleReplies: true, + }, + }, + }); + const toolOnly = validateConfigObjectRaw({ + messages: { + groupChat: { + visibleReplies: false, + }, + }, + }); + + expect(automatic.ok).toBe(true); + expect(toolOnly.ok).toBe(true); + if (automatic.ok) { + expect(automatic.config.messages?.groupChat?.visibleReplies).toBe("automatic"); + } + if (toolOnly.ok) { + expect(toolOnly.config.messages?.groupChat?.visibleReplies).toBe("message_tool"); + } + }); + + it("keeps invalid visibleReplies values rejected", () => { + const result = validateConfigObjectRaw({ + messages: { + visibleReplies: "visible", + }, + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.issues).toContainEqual( + expect.objectContaining({ + path: "messages.visibleReplies", + }), + ); + } + }); +});