mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 20:46:57 +02:00
fix(discord): normalize legacy streaming aliases
This commit is contained in:
@@ -3,7 +3,12 @@ import type {
|
||||
ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { asObjectRecord, normalizeLegacyDmAliases } from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import {
|
||||
asObjectRecord,
|
||||
normalizeLegacyDmAliases,
|
||||
normalizeLegacyStreamingAliases,
|
||||
} from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js";
|
||||
|
||||
const LEGACY_TTS_PROVIDER_KEYS = ["openai", "elevenlabs", "microsoft", "edge"] as const;
|
||||
|
||||
@@ -141,6 +146,16 @@ export function normalizeCompatibilityConfig({
|
||||
updated = dm.entry;
|
||||
changed = changed || dm.changed;
|
||||
|
||||
const streaming = normalizeLegacyStreamingAliases({
|
||||
entry: updated,
|
||||
pathPrefix: "channels.discord",
|
||||
changes,
|
||||
resolvedMode: resolveDiscordPreviewStreamMode(updated),
|
||||
includePreviewChunk: true,
|
||||
});
|
||||
updated = streaming.entry;
|
||||
changed = changed || streaming.changed;
|
||||
|
||||
const rawAccounts = asObjectRecord(updated.accounts);
|
||||
if (rawAccounts) {
|
||||
let accountsChanged = false;
|
||||
@@ -159,6 +174,15 @@ export function normalizeCompatibilityConfig({
|
||||
});
|
||||
accountEntry = accountDm.entry;
|
||||
accountChanged = accountDm.changed;
|
||||
const accountStreaming = normalizeLegacyStreamingAliases({
|
||||
entry: accountEntry,
|
||||
pathPrefix: `channels.discord.accounts.${accountId}`,
|
||||
changes,
|
||||
resolvedMode: resolveDiscordPreviewStreamMode(accountEntry),
|
||||
includePreviewChunk: true,
|
||||
});
|
||||
accountEntry = accountStreaming.entry;
|
||||
accountChanged = accountChanged || accountStreaming.changed;
|
||||
const accountVoice = asObjectRecord(accountEntry.voice);
|
||||
if (
|
||||
accountVoice &&
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "./doctor.js";
|
||||
|
||||
describe("discord doctor", () => {
|
||||
it("leaves legacy discord streaming aliases untouched during doctor normalization", () => {
|
||||
it("normalizes legacy discord streaming aliases for runtime config", () => {
|
||||
const normalize = discordDoctor.normalizeCompatibilityConfig;
|
||||
expect(normalize).toBeDefined();
|
||||
if (!normalize) {
|
||||
@@ -39,22 +39,39 @@ describe("discord doctor", () => {
|
||||
});
|
||||
|
||||
expect(result.config.channels?.discord).toEqual({
|
||||
streamMode: "block",
|
||||
chunkMode: "newline",
|
||||
blockStreaming: true,
|
||||
draftChunk: {
|
||||
minChars: 120,
|
||||
streaming: {
|
||||
mode: "block",
|
||||
chunkMode: "newline",
|
||||
block: {
|
||||
enabled: true,
|
||||
},
|
||||
preview: {
|
||||
chunk: {
|
||||
minChars: 120,
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
streaming: false,
|
||||
blockStreamingCoalesce: {
|
||||
idleMs: 250,
|
||||
streaming: {
|
||||
mode: "off",
|
||||
block: {
|
||||
coalesce: {
|
||||
idleMs: 250,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.changes).toEqual([]);
|
||||
expect(result.changes).toEqual([
|
||||
"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("moves account voice.tts.edge into providers.microsoft", () => {
|
||||
|
||||
@@ -77,6 +77,122 @@ vi.mock("../plugins/doctor-contract-registry.js", () => {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDiscordStreamMode(entry: Record<string, unknown>): string {
|
||||
if (
|
||||
entry.streamMode === "block" ||
|
||||
entry.streamMode === "partial" ||
|
||||
entry.streamMode === "off"
|
||||
) {
|
||||
return entry.streamMode;
|
||||
}
|
||||
if (entry.streaming === true) {
|
||||
return "partial";
|
||||
}
|
||||
if (entry.streaming === false) {
|
||||
return "off";
|
||||
}
|
||||
return "off";
|
||||
}
|
||||
|
||||
function normalizeDiscordStreamingEntry(
|
||||
entry: Record<string, unknown>,
|
||||
pathPrefix: string,
|
||||
changes: string[],
|
||||
): boolean {
|
||||
const hasLegacyStreaming =
|
||||
"streamMode" in entry ||
|
||||
typeof entry.streaming === "boolean" ||
|
||||
typeof entry.streaming === "string" ||
|
||||
"chunkMode" in entry ||
|
||||
"blockStreaming" in entry ||
|
||||
"draftChunk" in entry ||
|
||||
"blockStreamingCoalesce" in entry;
|
||||
if (!hasLegacyStreaming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const streaming = asRecord(entry.streaming) ?? {};
|
||||
if (!("mode" in streaming) && ("streamMode" in entry || typeof entry.streaming !== "object")) {
|
||||
const mode = resolveDiscordStreamMode(entry);
|
||||
streaming.mode = mode;
|
||||
changes.push(
|
||||
"streamMode" in entry
|
||||
? `Moved ${pathPrefix}.streamMode → ${pathPrefix}.streaming.mode (${mode}).`
|
||||
: `Moved ${pathPrefix}.streaming (boolean) → ${pathPrefix}.streaming.mode (${mode}).`,
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
if ("streamMode" in entry) {
|
||||
delete entry.streamMode;
|
||||
changed = true;
|
||||
}
|
||||
if ("chunkMode" in entry && !("chunkMode" in streaming)) {
|
||||
streaming.chunkMode = entry.chunkMode;
|
||||
delete entry.chunkMode;
|
||||
changes.push(`Moved ${pathPrefix}.chunkMode → ${pathPrefix}.streaming.chunkMode.`);
|
||||
changed = true;
|
||||
}
|
||||
const block = asRecord(streaming.block) ?? {};
|
||||
if ("blockStreaming" in entry && !("enabled" in block)) {
|
||||
block.enabled = entry.blockStreaming;
|
||||
delete entry.blockStreaming;
|
||||
changes.push(`Moved ${pathPrefix}.blockStreaming → ${pathPrefix}.streaming.block.enabled.`);
|
||||
changed = true;
|
||||
}
|
||||
if ("blockStreamingCoalesce" in entry && !("coalesce" in block)) {
|
||||
block.coalesce = entry.blockStreamingCoalesce;
|
||||
delete entry.blockStreamingCoalesce;
|
||||
changes.push(
|
||||
`Moved ${pathPrefix}.blockStreamingCoalesce → ${pathPrefix}.streaming.block.coalesce.`,
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
if (Object.keys(block).length > 0) {
|
||||
streaming.block = block;
|
||||
}
|
||||
const preview = asRecord(streaming.preview) ?? {};
|
||||
if ("draftChunk" in entry && !("chunk" in preview)) {
|
||||
preview.chunk = entry.draftChunk;
|
||||
delete entry.draftChunk;
|
||||
changes.push(`Moved ${pathPrefix}.draftChunk → ${pathPrefix}.streaming.preview.chunk.`);
|
||||
changed = true;
|
||||
}
|
||||
if (Object.keys(preview).length > 0) {
|
||||
streaming.preview = preview;
|
||||
}
|
||||
entry.streaming = streaming;
|
||||
return changed;
|
||||
}
|
||||
|
||||
function normalizeDiscordStreamingAliasesForTest(cfg: unknown): {
|
||||
config: unknown;
|
||||
changes: string[];
|
||||
} {
|
||||
const root = asRecord(cfg);
|
||||
const discord = asRecord(asRecord(root?.channels)?.discord);
|
||||
if (!root || !discord) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const next = structuredClone(root);
|
||||
const nextDiscord = asRecord(asRecord(next.channels)?.discord);
|
||||
if (!nextDiscord) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
normalizeDiscordStreamingEntry(nextDiscord, "channels.discord", changes);
|
||||
const accounts = asRecord(nextDiscord.accounts);
|
||||
for (const [accountId, accountRaw] of Object.entries(accounts ?? {})) {
|
||||
const account = asRecord(accountRaw);
|
||||
if (account) {
|
||||
normalizeDiscordStreamingEntry(account, `channels.discord.accounts.${accountId}`, changes);
|
||||
}
|
||||
}
|
||||
return changes.length > 0 ? { config: next, changes } : { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
collectRelevantDoctorPluginIds: (raw: unknown): string[] => {
|
||||
const ids = new Set<string>();
|
||||
@@ -92,7 +208,7 @@ vi.mock("../plugins/doctor-contract-registry.js", () => {
|
||||
}
|
||||
return [...ids].toSorted();
|
||||
},
|
||||
applyPluginDoctorCompatibilityMigrations: (cfg: unknown) => ({ config: cfg, changes: [] }),
|
||||
applyPluginDoctorCompatibilityMigrations: normalizeDiscordStreamingAliasesForTest,
|
||||
listPluginDoctorLegacyConfigRules: () => [
|
||||
{
|
||||
path: ["channels", "telegram", "groupMentionsOnly"],
|
||||
@@ -404,6 +520,10 @@ vi.mock("./doctor-config-preflight.js", async () => {
|
||||
await import("../plugins/doctor-contract-registry.js");
|
||||
const { findLegacyConfigIssues }: typeof import("../config/legacy.js") =
|
||||
await import("../config/legacy.js");
|
||||
const {
|
||||
applyRuntimeLegacyConfigMigrations,
|
||||
}: typeof import("./doctor/shared/runtime-compat-api.js") =
|
||||
await import("./doctor/shared/runtime-compat-api.js");
|
||||
|
||||
function resolveConfigPath() {
|
||||
const stateDir =
|
||||
@@ -430,18 +550,20 @@ vi.mock("./doctor-config-preflight.js", async () => {
|
||||
pluginIds: collectRelevantDoctorPluginIds(parsed),
|
||||
}),
|
||||
);
|
||||
const compat = applyRuntimeLegacyConfigMigrations(parsed);
|
||||
const effectiveConfig = compat.next ?? parsed;
|
||||
return {
|
||||
snapshot: {
|
||||
exists,
|
||||
path: configPath,
|
||||
parsed,
|
||||
config: parsed,
|
||||
sourceConfig: parsed,
|
||||
config: effectiveConfig,
|
||||
sourceConfig: effectiveConfig,
|
||||
valid: legacyIssues.length === 0,
|
||||
warnings: [],
|
||||
legacyIssues,
|
||||
},
|
||||
baseConfig: parsed,
|
||||
baseConfig: effectiveConfig,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user