fix(doctor): preserve discord streaming downgrade compatibility

This commit is contained in:
Vincent Koc
2026-04-12 17:07:38 +01:00
parent 1f0431cd11
commit 43cb94a39a
8 changed files with 82 additions and 259 deletions

View File

@@ -3,18 +3,7 @@ import type {
ChannelDoctorLegacyConfigRule,
} from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
asObjectRecord,
hasLegacyAccountStreamingAliases,
hasLegacyStreamingAliases,
normalizeLegacyDmAliases,
normalizeLegacyStreamingAliases,
} from "openclaw/plugin-sdk/runtime-doctor";
import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js";
function hasLegacyDiscordStreamingAliases(value: unknown): boolean {
return hasLegacyStreamingAliases(value, { includePreviewChunk: true });
}
import { asObjectRecord, normalizeLegacyDmAliases } from "openclaw/plugin-sdk/runtime-doctor";
const LEGACY_TTS_PROVIDER_KEYS = ["openai", "elevenlabs", "microsoft", "edge"] as const;
@@ -114,18 +103,6 @@ function migrateLegacyTtsConfig(
}
export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = [
{
path: ["channels", "discord"],
message:
"channels.discord.streamMode, channels.discord.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}.",
match: hasLegacyDiscordStreamingAliases,
},
{
path: ["channels", "discord", "accounts"],
message:
"channels.discord.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.accounts.<id>.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}.",
match: (value) => hasLegacyAccountStreamingAliases(value, hasLegacyDiscordStreamingAliases),
},
{
path: ["channels", "discord", "voice", "tts"],
message:
@@ -164,18 +141,6 @@ export function normalizeCompatibilityConfig({
updated = dm.entry;
changed = changed || dm.changed;
const streaming = normalizeLegacyStreamingAliases({
entry: updated,
pathPrefix: "channels.discord",
changes,
includePreviewChunk: true,
resolvedMode: resolveDiscordPreviewStreamMode(updated),
offModeLegacyNotice: (pathPrefix) =>
`${pathPrefix}.streaming remains off by default to avoid Discord preview-edit rate limits; set ${pathPrefix}.streaming.mode="partial" to opt in explicitly.`,
});
updated = streaming.entry;
changed = changed || streaming.changed;
const rawAccounts = asObjectRecord(updated.accounts);
if (rawAccounts) {
let accountsChanged = false;
@@ -194,17 +159,6 @@ export function normalizeCompatibilityConfig({
});
accountEntry = accountDm.entry;
accountChanged = accountDm.changed;
const accountStreaming = normalizeLegacyStreamingAliases({
entry: accountEntry,
pathPrefix: `channels.discord.accounts.${accountId}`,
changes,
includePreviewChunk: true,
resolvedMode: resolveDiscordPreviewStreamMode(accountEntry),
offModeLegacyNotice: (pathPrefix) =>
`${pathPrefix}.streaming remains off by default to avoid Discord preview-edit rate limits; set ${pathPrefix}.streaming.mode="partial" to opt in explicitly.`,
});
accountEntry = accountStreaming.entry;
accountChanged = accountChanged || accountStreaming.changed;
const accountVoice = asObjectRecord(accountEntry.voice);
if (
accountVoice &&

View File

@@ -1,46 +1,5 @@
import type { ChannelDoctorLegacyConfigRule } from "openclaw/plugin-sdk/channel-contract";
function asObjectRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function hasLegacyDiscordStreamingAliases(value: unknown): boolean {
const entry = asObjectRecord(value);
if (!entry) {
return false;
}
return (
entry.streamMode !== undefined ||
typeof entry.streaming === "boolean" ||
typeof entry.streaming === "string" ||
entry.chunkMode !== undefined ||
entry.blockStreaming !== undefined ||
entry.draftChunk !== undefined ||
entry.blockStreamingCoalesce !== undefined
);
}
function hasLegacyDiscordAccountStreamingAliases(value: unknown): boolean {
const accounts = asObjectRecord(value);
if (!accounts) {
return false;
}
return Object.values(accounts).some((account) => hasLegacyDiscordStreamingAliases(account));
}
export const DISCORD_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [
{
path: ["channels", "discord"],
message:
"channels.discord.streamMode, channels.discord.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}.",
match: hasLegacyDiscordStreamingAliases,
},
{
path: ["channels", "discord", "accounts"],
message:
"channels.discord.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.accounts.<id>.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}.",
match: hasLegacyDiscordAccountStreamingAliases,
},
];
// Runtime config loading already normalizes these aliases without rewriting the
// source file. Keep doctor non-destructive so downgrade paths remain recoverable.
export const DISCORD_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [];

View File

@@ -8,7 +8,7 @@ import {
} from "./doctor.js";
describe("discord doctor", () => {
it("normalizes legacy discord streaming aliases into the nested streaming shape", () => {
it("leaves legacy discord streaming aliases untouched during doctor normalization", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
@@ -38,62 +38,23 @@ describe("discord doctor", () => {
} as never,
});
expect(result.config.channels?.discord?.streaming).toEqual({
mode: "block",
expect(result.config.channels?.discord).toEqual({
streamMode: "block",
chunkMode: "newline",
block: {
enabled: true,
blockStreaming: true,
draftChunk: {
minChars: 120,
},
preview: {
chunk: {
minChars: 120,
},
},
});
expect(result.config.channels?.discord?.accounts?.work?.streaming).toEqual({
mode: "off",
block: {
coalesce: {
idleMs: 250,
},
},
});
expect(result.changes).toEqual(
expect.arrayContaining([
"Moved channels.discord.streamMode → channels.discord.streaming.mode (block).",
"Moved channels.discord.chunkMode → channels.discord.streaming.chunkMode.",
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
"Moved channels.discord.draftChunk → channels.discord.streaming.preview.chunk.",
"Moved channels.discord.accounts.work.streaming (boolean) → channels.discord.accounts.work.streaming.mode (off).",
"Moved channels.discord.accounts.work.blockStreamingCoalesce → channels.discord.accounts.work.streaming.block.coalesce.",
]),
);
});
it("does not duplicate streaming.mode change messages when streamMode wins over boolean streaming", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const result = normalize({
cfg: {
channels: {
discord: {
streamMode: "block",
streaming: false,
accounts: {
work: {
streaming: false,
blockStreamingCoalesce: {
idleMs: 250,
},
},
} as never,
},
});
expect(result.config.channels?.discord?.streaming).toEqual({
mode: "block",
});
expect(
result.changes.filter((change) => change.includes("channels.discord.streaming.mode")),
).toEqual(["Moved channels.discord.streamMode → channels.discord.streaming.mode (block)."]);
expect(result.changes).toEqual([]);
});
it("moves account voice.tts.edge into providers.microsoft", () => {