diff --git a/extensions/discord/src/doctor-contract.ts b/extensions/discord/src/doctor-contract.ts index c9a6699cc5e..f31bca06e68 100644 --- a/extensions/discord/src/doctor-contract.ts +++ b/extensions/discord/src/doctor-contract.ts @@ -3,275 +3,17 @@ import type { ChannelDoctorLegacyConfigRule, } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime"; +import { + asObjectRecord, + hasLegacyAccountStreamingAliases, + hasLegacyStreamingAliases, + normalizeLegacyDmAliases, + normalizeLegacyStreamingAliases, +} from "openclaw/plugin-sdk/runtime-doctor"; import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js"; -function asObjectRecord(value: unknown): Record | null { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : null; -} - -function ensureNestedRecord(owner: Record, key: string): Record { - const existing = asObjectRecord(owner[key]); - if (existing) { - return { ...existing }; - } - return {}; -} - -function allowFromListsMatch(left: unknown, right: unknown): boolean { - if (!Array.isArray(left) || !Array.isArray(right)) { - return false; - } - const normalizedLeft = normalizeStringEntries(left); - const normalizedRight = normalizeStringEntries(right); - if (normalizedLeft.length !== normalizedRight.length) { - return false; - } - return normalizedLeft.every((value, index) => value === normalizedRight[index]); -} - -function normalizeLegacyDmAliases(params: { - entry: Record; - pathPrefix: string; - changes: string[]; - promoteAllowFrom?: boolean; -}): { entry: Record; changed: boolean } { - let changed = false; - let updated: Record = params.entry; - const rawDm = updated.dm; - const dm = asObjectRecord(rawDm) ? (structuredClone(rawDm) as Record) : null; - let dmChanged = false; - - const topDmPolicy = updated.dmPolicy; - const legacyDmPolicy = dm?.policy; - if (topDmPolicy === undefined && legacyDmPolicy !== undefined) { - updated = { ...updated, dmPolicy: legacyDmPolicy }; - changed = true; - if (dm) { - delete dm.policy; - dmChanged = true; - } - params.changes.push(`Moved ${params.pathPrefix}.dm.policy → ${params.pathPrefix}.dmPolicy.`); - } else if ( - topDmPolicy !== undefined && - legacyDmPolicy !== undefined && - topDmPolicy === legacyDmPolicy - ) { - if (dm) { - delete dm.policy; - dmChanged = true; - params.changes.push(`Removed ${params.pathPrefix}.dm.policy (dmPolicy already set).`); - } - } - - if (params.promoteAllowFrom !== false) { - const topAllowFrom = updated.allowFrom; - const legacyAllowFrom = dm?.allowFrom; - if (topAllowFrom === undefined && legacyAllowFrom !== undefined) { - updated = { ...updated, allowFrom: legacyAllowFrom }; - changed = true; - if (dm) { - delete dm.allowFrom; - dmChanged = true; - } - params.changes.push( - `Moved ${params.pathPrefix}.dm.allowFrom → ${params.pathPrefix}.allowFrom.`, - ); - } else if ( - topAllowFrom !== undefined && - legacyAllowFrom !== undefined && - allowFromListsMatch(topAllowFrom, legacyAllowFrom) - ) { - if (dm) { - delete dm.allowFrom; - dmChanged = true; - params.changes.push(`Removed ${params.pathPrefix}.dm.allowFrom (allowFrom already set).`); - } - } - } - - if (dm && asObjectRecord(rawDm) && dmChanged) { - const keys = Object.keys(dm); - if (keys.length === 0) { - if (updated.dm !== undefined) { - const { dm: _ignored, ...rest } = updated; - updated = rest; - changed = true; - params.changes.push(`Removed empty ${params.pathPrefix}.dm after migration.`); - } - } else { - updated = { ...updated, dm }; - changed = true; - } - } - - return { entry: updated, changed }; -} - -function normalizeLegacyStreamingAliases(params: { - entry: Record; - pathPrefix: string; - changes: string[]; - resolvedMode: string; - includePreviewChunk?: boolean; - resolvedNativeTransport?: unknown; - offModeLegacyNotice?: (pathPrefix: string) => string; -}): { entry: Record; changed: boolean } { - const beforeStreaming = params.entry.streaming; - const hadLegacyStreamMode = params.entry.streamMode !== undefined; - const hasLegacyFlatFields = - params.entry.chunkMode !== undefined || - params.entry.blockStreaming !== undefined || - params.entry.blockStreamingCoalesce !== undefined || - (params.includePreviewChunk === true && params.entry.draftChunk !== undefined) || - params.entry.nativeStreaming !== undefined; - const shouldNormalize = - hadLegacyStreamMode || - typeof beforeStreaming === "boolean" || - typeof beforeStreaming === "string" || - hasLegacyFlatFields; - if (!shouldNormalize) { - return { entry: params.entry, changed: false }; - } - - let updated = { ...params.entry }; - let changed = false; - const streaming = ensureNestedRecord(updated, "streaming"); - const block = ensureNestedRecord(streaming, "block"); - const preview = ensureNestedRecord(streaming, "preview"); - - if ( - (hadLegacyStreamMode || - typeof beforeStreaming === "boolean" || - typeof beforeStreaming === "string") && - streaming.mode === undefined - ) { - streaming.mode = params.resolvedMode; - if (hadLegacyStreamMode) { - params.changes.push( - `Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`, - ); - } else if (typeof beforeStreaming === "boolean") { - params.changes.push( - `Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`, - ); - } else if (typeof beforeStreaming === "string") { - params.changes.push( - `Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`, - ); - } - changed = true; - } - if (hadLegacyStreamMode) { - delete updated.streamMode; - changed = true; - } - if (updated.chunkMode !== undefined && streaming.chunkMode === undefined) { - streaming.chunkMode = updated.chunkMode; - delete updated.chunkMode; - params.changes.push( - `Moved ${params.pathPrefix}.chunkMode → ${params.pathPrefix}.streaming.chunkMode.`, - ); - changed = true; - } - if (updated.blockStreaming !== undefined && block.enabled === undefined) { - block.enabled = updated.blockStreaming; - delete updated.blockStreaming; - params.changes.push( - `Moved ${params.pathPrefix}.blockStreaming → ${params.pathPrefix}.streaming.block.enabled.`, - ); - changed = true; - } - if ( - params.includePreviewChunk === true && - updated.draftChunk !== undefined && - preview.chunk === undefined - ) { - preview.chunk = updated.draftChunk; - delete updated.draftChunk; - params.changes.push( - `Moved ${params.pathPrefix}.draftChunk → ${params.pathPrefix}.streaming.preview.chunk.`, - ); - changed = true; - } - if (updated.blockStreamingCoalesce !== undefined && block.coalesce === undefined) { - block.coalesce = updated.blockStreamingCoalesce; - delete updated.blockStreamingCoalesce; - params.changes.push( - `Moved ${params.pathPrefix}.blockStreamingCoalesce → ${params.pathPrefix}.streaming.block.coalesce.`, - ); - changed = true; - } - if ( - updated.nativeStreaming !== undefined && - streaming.nativeTransport === undefined && - params.resolvedNativeTransport !== undefined - ) { - streaming.nativeTransport = params.resolvedNativeTransport; - delete updated.nativeStreaming; - params.changes.push( - `Moved ${params.pathPrefix}.nativeStreaming → ${params.pathPrefix}.streaming.nativeTransport.`, - ); - changed = true; - } else if ( - typeof beforeStreaming === "boolean" && - streaming.nativeTransport === undefined && - params.resolvedNativeTransport !== undefined - ) { - streaming.nativeTransport = params.resolvedNativeTransport; - params.changes.push( - `Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.nativeTransport.`, - ); - changed = true; - } - - if (Object.keys(preview).length > 0) { - streaming.preview = preview; - } - if (Object.keys(block).length > 0) { - streaming.block = block; - } - updated.streaming = streaming; - if ( - hadLegacyStreamMode && - params.resolvedMode === "off" && - params.offModeLegacyNotice !== undefined - ) { - params.changes.push(params.offModeLegacyNotice(params.pathPrefix)); - } - return { entry: updated, changed }; -} - function hasLegacyDiscordStreamingAliases(value: unknown): boolean { - const entry = asObjectRecord(value); - if (!entry) { - return false; - } - if ( - typeof entry.streamMode === "string" || - typeof entry.chunkMode === "string" || - typeof entry.blockStreaming === "boolean" || - typeof entry.blockStreamingCoalesce === "boolean" || - typeof entry.draftChunk === "boolean" || - (entry.draftChunk && typeof entry.draftChunk === "object") - ) { - return true; - } - const streaming = entry.streaming; - return typeof streaming === "string" || typeof streaming === "boolean"; -} - -function hasLegacyAccountStreamingAliases( - value: unknown, - match: (entry: unknown) => boolean, -): boolean { - const accounts = asObjectRecord(value); - if (!accounts) { - return false; - } - return Object.values(accounts).some((account) => match(account)); + return hasLegacyStreamingAliases(value, { includePreviewChunk: true }); } const LEGACY_TTS_PROVIDER_KEYS = ["openai", "elevenlabs", "microsoft", "edge"] as const; diff --git a/extensions/telegram/src/doctor-contract.ts b/extensions/telegram/src/doctor-contract.ts index 0a15a940bfb..dd4153b520b 100644 --- a/extensions/telegram/src/doctor-contract.ts +++ b/extensions/telegram/src/doctor-contract.ts @@ -3,149 +3,16 @@ import type { ChannelDoctorLegacyConfigRule, } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { + asObjectRecord, + hasLegacyAccountStreamingAliases, + hasLegacyStreamingAliases, + normalizeLegacyStreamingAliases, +} from "openclaw/plugin-sdk/runtime-doctor"; import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js"; -function asObjectRecord(value: unknown): Record | null { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : null; -} - function hasLegacyTelegramStreamingAliases(value: unknown): boolean { - const entry = asObjectRecord(value); - if (!entry) { - return false; - } - if ( - typeof entry.streamMode === "string" || - typeof entry.chunkMode === "string" || - typeof entry.blockStreaming === "boolean" || - typeof entry.blockStreamingCoalesce === "boolean" || - typeof entry.draftChunk === "boolean" - ) { - return true; - } - const streaming = entry.streaming; - return typeof streaming === "string" || typeof streaming === "boolean"; -} - -function hasLegacyAccountStreamingAliases( - value: unknown, - match: (entry: unknown) => boolean, -): boolean { - const accounts = asObjectRecord(value); - if (!accounts) { - return false; - } - return Object.values(accounts).some((account) => match(account)); -} - -function ensureNestedRecord(owner: Record, key: string): Record { - const existing = asObjectRecord(owner[key]); - if (existing) { - return { ...existing }; - } - return {}; -} - -function normalizeLegacyTelegramStreamingAliases(params: { - entry: Record; - pathPrefix: string; - changes: string[]; - resolvedMode: string; -}): { - entry: Record; - changed: boolean; -} { - const beforeStreaming = params.entry.streaming; - const hadLegacyStreamMode = params.entry.streamMode !== undefined; - const hasLegacyFlatFields = - params.entry.chunkMode !== undefined || - params.entry.blockStreaming !== undefined || - params.entry.blockStreamingCoalesce !== undefined || - params.entry.draftChunk !== undefined; - const shouldNormalize = - hadLegacyStreamMode || - typeof beforeStreaming === "boolean" || - typeof beforeStreaming === "string" || - hasLegacyFlatFields; - if (!shouldNormalize) { - return { entry: params.entry, changed: false }; - } - - let updated = { ...params.entry }; - let changed = false; - const streaming = ensureNestedRecord(updated, "streaming"); - const block = ensureNestedRecord(streaming, "block"); - const preview = ensureNestedRecord(streaming, "preview"); - - if ( - (hadLegacyStreamMode || - typeof beforeStreaming === "boolean" || - typeof beforeStreaming === "string") && - streaming.mode === undefined - ) { - streaming.mode = params.resolvedMode; - if (hadLegacyStreamMode) { - params.changes.push( - `Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`, - ); - } else if (typeof beforeStreaming === "boolean") { - params.changes.push( - `Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`, - ); - } else if (typeof beforeStreaming === "string") { - params.changes.push( - `Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`, - ); - } - changed = true; - } - if (hadLegacyStreamMode) { - delete updated.streamMode; - changed = true; - } - if (updated.chunkMode !== undefined && streaming.chunkMode === undefined) { - streaming.chunkMode = updated.chunkMode; - delete updated.chunkMode; - params.changes.push( - `Moved ${params.pathPrefix}.chunkMode → ${params.pathPrefix}.streaming.chunkMode.`, - ); - changed = true; - } - if (updated.blockStreaming !== undefined && block.enabled === undefined) { - block.enabled = updated.blockStreaming; - delete updated.blockStreaming; - params.changes.push( - `Moved ${params.pathPrefix}.blockStreaming → ${params.pathPrefix}.streaming.block.enabled.`, - ); - changed = true; - } - if (updated.draftChunk !== undefined && preview.chunk === undefined) { - preview.chunk = updated.draftChunk; - delete updated.draftChunk; - params.changes.push( - `Moved ${params.pathPrefix}.draftChunk → ${params.pathPrefix}.streaming.preview.chunk.`, - ); - changed = true; - } - if (updated.blockStreamingCoalesce !== undefined && block.coalesce === undefined) { - block.coalesce = updated.blockStreamingCoalesce; - delete updated.blockStreamingCoalesce; - params.changes.push( - `Moved ${params.pathPrefix}.blockStreamingCoalesce → ${params.pathPrefix}.streaming.block.coalesce.`, - ); - changed = true; - } - - if (Object.keys(preview).length > 0) { - streaming.preview = preview; - } - if (Object.keys(block).length > 0) { - streaming.block = block; - } - updated.streaming = streaming; - return { entry: updated, changed }; + return hasLegacyStreamingAliases(value, { includePreviewChunk: true }); } function resolveCompatibleDefaultGroupEntry(section: Record): { @@ -226,11 +93,12 @@ export function normalizeCompatibilityConfig({ } } - const streaming = normalizeLegacyTelegramStreamingAliases({ + const streaming = normalizeLegacyStreamingAliases({ entry: updated, pathPrefix: "channels.telegram", changes, resolvedMode: resolveTelegramPreviewStreamMode(updated), + includePreviewChunk: true, }); updated = streaming.entry; changed = changed || streaming.changed; @@ -244,11 +112,12 @@ export function normalizeCompatibilityConfig({ if (!account) { continue; } - const accountStreaming = normalizeLegacyTelegramStreamingAliases({ + const accountStreaming = normalizeLegacyStreamingAliases({ entry: account, pathPrefix: `channels.telegram.accounts.${accountId}`, changes, resolvedMode: resolveTelegramPreviewStreamMode(account), + includePreviewChunk: true, }); if (accountStreaming.changed) { accounts[accountId] = accountStreaming.entry;