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

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