fix(config): coerce visible replies booleans

This commit is contained in:
Vincent Koc
2026-05-03 11:52:06 -07:00
parent 03e35b1d83
commit 69b66dd548
6 changed files with 118 additions and 12 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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.',

View File

@@ -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();

View File

@@ -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,

View File

@@ -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",
}),
);
}
});
});