fix(mattermost): accept streaming config

This commit is contained in:
Vincent Koc
2026-05-03 14:35:30 -07:00
parent bdd68a75ea
commit 35f6071d8d
13 changed files with 309 additions and 11 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- 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.
- 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.
- Mattermost: accept the documented `channels.mattermost.streaming` config and honor `streaming: "off"` by disabling draft preview posts. Thanks @vincentkoc.
- Channels/streaming: expose `streaming.progress.label`, `labels`, `maxLines`, and `toolProgress` in bundled channel config metadata so progress draft settings appear in config, docs, and control surfaces. Thanks @vincentkoc.
- Channels/streaming: normalize whitespace and case for `streaming.progress.label: "auto"` so progress draft labels keep using the built-in label pool instead of rendering a literal `auto` title. Thanks @vincentkoc.
- Gateway/install: prefer supported system Node over nvm/fnm/volta/asdf/mise when regenerating managed gateway services, so `gateway install --force` no longer recreates service definitions that doctor immediately flags as version-manager-backed. Fixes #76339. Thanks @brokemac79.

View File

@@ -1,4 +1,4 @@
056760c0a86627641d8e2993cc0cc987820dc4289c40c67dc8c2c1e8970c1849 config-baseline.json
5603f93164f1bed3b39714b813c7597e188321fff07cfbb6980d7198a69da162 config-baseline.json
5b5ebd95939d75496597d9858a375e27544812d0f79dc3b4bf87c794ada2ba08 config-baseline.core.json
7b207901b595ad527026b1f357f63a5cd33123a72eeb66bdac24a8f2e8bb1ac8 config-baseline.channel.json
c83a29196d34b4aff4849f63ae8850298441c367811d928e1ab2efe787eae520 config-baseline.channel.json
055fae0d0067a751dc10125af7421da45633f73519c94c982d02b0c4eb2bdf67 config-baseline.plugin.json

View File

@@ -78,6 +78,40 @@ const MattermostNetworkSchema = z
.strict()
.optional();
const MattermostStreamingModeSchema = z.enum(["off", "partial", "block", "progress"]);
const MattermostStreamingProgressSchema = z
.object({
label: z.union([z.string(), z.literal(false)]).optional(),
labels: z.array(z.string()).optional(),
maxLines: z.number().int().positive().optional(),
toolProgress: z.boolean().optional(),
})
.strict();
const MattermostStreamingPreviewSchema = z
.object({
toolProgress: z.boolean().optional(),
})
.strict();
const MattermostStreamingBlockSchema = z
.object({
enabled: z.boolean().optional(),
coalesce: BlockStreamingCoalesceSchema.optional(),
})
.strict();
const MattermostStreamingSchema = z.union([
MattermostStreamingModeSchema,
z.boolean(),
z
.object({
mode: MattermostStreamingModeSchema.optional(),
chunkMode: z.enum(["length", "newline"]).optional(),
preview: MattermostStreamingPreviewSchema.optional(),
progress: MattermostStreamingProgressSchema.optional(),
block: MattermostStreamingBlockSchema.optional(),
})
.strict(),
]);
const MattermostAccountSchemaBase = z
.object({
name: z.string().optional(),
@@ -97,6 +131,7 @@ const MattermostAccountSchemaBase = z
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
textChunkLimit: z.number().int().positive().optional(),
chunkMode: z.enum(["length", "newline"]).optional(),
streaming: MattermostStreamingSchema.optional(),
blockStreaming: z.boolean().optional(),
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
replyToMode: z.enum(["off", "first", "all", "batched"]).optional(),

View File

@@ -29,6 +29,25 @@ describe("MattermostConfigSchema", () => {
expect(result.success).toBe(true);
});
it("accepts documented streaming modes and progress config", () => {
const result = MattermostConfigSchema.safeParse({
streaming: {
mode: "progress",
progress: {
label: "Shelling",
maxLines: 4,
toolProgress: false,
},
},
accounts: {
quiet: {
streaming: "off",
},
},
});
expect(result.success).toBe(true);
});
it("accepts groups with requireMention", () => {
const result = MattermostConfigSchema.safeParse({
groups: {

View File

@@ -135,4 +135,24 @@ describe("resolveMattermostReplyToMode", () => {
callbackPath: "/hooks/work",
});
});
it("resolves documented streaming mode from account config", () => {
const account = resolveMattermostAccount({
cfg: {
channels: {
mattermost: {
streaming: "partial",
accounts: {
work: {
streaming: "off",
},
},
},
},
},
accountId: "work",
});
expect(account.streamingMode).toBe("off");
});
});

View File

@@ -5,6 +5,8 @@ import {
resolveChannelStreamingBlockCoalesce,
resolveChannelStreamingBlockEnabled,
resolveChannelStreamingChunkMode,
resolveChannelPreviewStreamMode,
type StreamingMode,
} from "openclaw/plugin-sdk/channel-streaming";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "../secret-input.js";
@@ -34,6 +36,7 @@ export type ResolvedMattermostAccount = {
requireMention?: boolean;
textChunkLimit?: number;
chunkMode?: MattermostAccountConfig["chunkMode"];
streamingMode: StreamingMode;
blockStreaming?: boolean;
blockStreamingCoalesce?: MattermostAccountConfig["blockStreamingCoalesce"];
};
@@ -120,6 +123,7 @@ export function resolveMattermostAccount(params: {
requireMention,
textChunkLimit: merged.textChunkLimit,
chunkMode: resolveChannelStreamingChunkMode(merged) ?? merged.chunkMode,
streamingMode: resolveChannelPreviewStreamMode(merged, "partial"),
blockStreaming: resolveChannelStreamingBlockEnabled(merged) ?? merged.blockStreaming,
blockStreamingCoalesce:
resolveChannelStreamingBlockCoalesce(merged) ?? merged.blockStreamingCoalesce,

View File

@@ -13,6 +13,7 @@ const accountFixture: ResolvedMattermostAccount = {
baseUrl: "https://chat.example.com",
botTokenSource: "config",
baseUrlSource: "config",
streamingMode: "partial",
config: {},
};

View File

@@ -281,6 +281,20 @@ type MattermostDraftPreviewState = {
finalizedViaPreviewPost: boolean;
};
function createDisabledMattermostDraftStream(): ReturnType<typeof createMattermostDraftStream> {
const noopAsync = async () => {};
return {
update: () => {},
flush: noopAsync,
postId: () => undefined,
clear: noopAsync,
discardPending: noopAsync,
seal: noopAsync,
stop: noopAsync,
forceNewMessage: () => {},
};
}
type MattermostDraftPreviewDeliverParams = {
payload: ReplyPayload;
info: { kind: "tool" | "block" | "final" };
@@ -1619,14 +1633,17 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
},
},
});
const draftStream = createMattermostDraftStream({
client,
channelId,
rootId: effectiveReplyToId,
throttleMs: 1200,
log: logVerboseMessage,
warn: logVerboseMessage,
});
const draftPreviewEnabled = account.streamingMode !== "off";
const draftStream = draftPreviewEnabled
? createMattermostDraftStream({
client,
channelId,
rootId: effectiveReplyToId,
throttleMs: 1200,
log: logVerboseMessage,
warn: logVerboseMessage,
})
: createDisabledMattermostDraftStream();
let lastPartialText = "";
const previewState: MattermostDraftPreviewState = {
finalizedViaPreviewPost: false,
@@ -1815,7 +1832,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
disableBlockStreaming: true,
onModelSelected,
onPartialReply: (payload) => {
updateDraftFromPartial(payload.text);
if (account.streamingMode !== "progress") {
updateDraftFromPartial(payload.text);
}
},
onAssistantMessageStart: () => {
lastPartialText = "";

View File

@@ -205,6 +205,7 @@ const accountFixture: ResolvedMattermostAccount = {
baseUrl: "https://chat.example.com",
botTokenSource: "config",
baseUrlSource: "config",
streamingMode: "partial",
config: {},
};

View File

@@ -68,6 +68,7 @@ const accountFixture: ResolvedMattermostAccount = {
baseUrl: "https://chat.example.com",
botTokenSource: "config",
baseUrlSource: "config",
streamingMode: "partial",
config: {},
};

View File

@@ -15,6 +15,7 @@ function createResolvedMattermostAccount(accountId: string): ResolvedMattermostA
enabled: true,
botTokenSource: "config",
baseUrlSource: "config",
streamingMode: "partial",
config: {},
};
}

View File

@@ -1,3 +1,7 @@
import type {
ChannelPreviewStreamingConfig,
StreamingMode,
} from "openclaw/plugin-sdk/channel-streaming";
import type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "./runtime-api.js";
import type { SecretInput } from "./secret-input.js";
@@ -51,6 +55,8 @@ export type MattermostAccountConfig = {
textChunkLimit?: number;
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
chunkMode?: "length" | "newline";
/** Preview streaming mode/config. */
streaming?: StreamingMode | boolean | ChannelPreviewStreamingConfig;
/** Disable block streaming for this account. */
blockStreaming?: boolean;
/** Merge streamed block replies before sending. */

View File

@@ -8198,6 +8198,101 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["length", "newline"],
},
streaming: {
anyOf: [
{
type: "string",
enum: ["off", "partial", "block", "progress"],
},
{
type: "boolean",
},
{
type: "object",
properties: {
mode: {
type: "string",
enum: ["off", "partial", "block", "progress"],
},
chunkMode: {
type: "string",
enum: ["length", "newline"],
},
preview: {
type: "object",
properties: {
toolProgress: {
type: "boolean",
},
},
additionalProperties: false,
},
progress: {
type: "object",
properties: {
label: {
anyOf: [
{
type: "string",
},
{
type: "boolean",
const: false,
},
],
},
labels: {
type: "array",
items: {
type: "string",
},
},
maxLines: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
toolProgress: {
type: "boolean",
},
},
additionalProperties: false,
},
block: {
type: "object",
properties: {
enabled: {
type: "boolean",
},
coalesce: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
idleMs: {
type: "integer",
minimum: 0,
maximum: 9007199254740991,
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
],
},
blockStreaming: {
type: "boolean",
},
@@ -8500,6 +8595,101 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["length", "newline"],
},
streaming: {
anyOf: [
{
type: "string",
enum: ["off", "partial", "block", "progress"],
},
{
type: "boolean",
},
{
type: "object",
properties: {
mode: {
type: "string",
enum: ["off", "partial", "block", "progress"],
},
chunkMode: {
type: "string",
enum: ["length", "newline"],
},
preview: {
type: "object",
properties: {
toolProgress: {
type: "boolean",
},
},
additionalProperties: false,
},
progress: {
type: "object",
properties: {
label: {
anyOf: [
{
type: "string",
},
{
type: "boolean",
const: false,
},
],
},
labels: {
type: "array",
items: {
type: "string",
},
},
maxLines: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
toolProgress: {
type: "boolean",
},
},
additionalProperties: false,
},
block: {
type: "object",
properties: {
enabled: {
type: "boolean",
},
coalesce: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
idleMs: {
type: "integer",
minimum: 0,
maximum: 9007199254740991,
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
],
},
blockStreaming: {
type: "boolean",
},