mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-16 11:41:08 +00:00
refactor: dedupe channel doctor compat helpers
This commit is contained in:
@@ -3,6 +3,11 @@ import type {
|
||||
ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
hasLegacyStreamingAliases,
|
||||
normalizeLegacyDmAliases,
|
||||
normalizeLegacyStreamingAliases,
|
||||
} from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js";
|
||||
|
||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
@@ -11,226 +16,8 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const existing = asObjectRecord(owner[key]);
|
||||
if (existing) {
|
||||
return { ...existing };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function normalizeDiscordDmAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; changed: boolean } {
|
||||
let changed = false;
|
||||
let updated: Record<string, unknown> = params.entry;
|
||||
const rawDm = updated.dm;
|
||||
const dm = asObjectRecord(rawDm) ? (structuredClone(rawDm) as Record<string, unknown>) : null;
|
||||
let dmChanged = false;
|
||||
const shouldPromoteLegacyAllowFrom = !(
|
||||
params.pathPrefix === "channels.discord" && asObjectRecord(updated.accounts)
|
||||
);
|
||||
|
||||
const allowFromEqual = (a: unknown, b: unknown): boolean => {
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
const na = a.map((v) => String(v).trim()).filter(Boolean);
|
||||
const nb = b.map((v) => String(v).trim()).filter(Boolean);
|
||||
if (na.length !== nb.length) {
|
||||
return false;
|
||||
}
|
||||
return na.every((v, i) => v === nb[i]);
|
||||
};
|
||||
|
||||
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).`);
|
||||
}
|
||||
}
|
||||
|
||||
const topAllowFrom = updated.allowFrom;
|
||||
const legacyAllowFrom = dm?.allowFrom;
|
||||
if (shouldPromoteLegacyAllowFrom) {
|
||||
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 &&
|
||||
allowFromEqual(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 normalizeDiscordStreamingAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.draftChunk !== undefined ||
|
||||
params.entry.blockStreamingCoalesce !== undefined;
|
||||
const resolved = resolveDiscordPreviewStreamMode(params.entry);
|
||||
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 = resolved;
|
||||
if (hadLegacyStreamMode) {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
}
|
||||
if (typeof beforeStreaming === "boolean") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "string") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
}
|
||||
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;
|
||||
if (
|
||||
params.pathPrefix.startsWith("channels.discord") &&
|
||||
resolved === "off" &&
|
||||
hadLegacyStreamMode
|
||||
) {
|
||||
params.changes.push(
|
||||
`${params.pathPrefix}.streaming remains off by default to avoid Discord preview-edit rate limits; set ${params.pathPrefix}.streaming.mode="partial" to opt in explicitly.`,
|
||||
);
|
||||
}
|
||||
return { entry: updated, changed };
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
return hasLegacyStreamingAliases(value, { includePreviewChunk: true });
|
||||
}
|
||||
|
||||
function hasLegacyDiscordAccountStreamingAliases(value: unknown): boolean {
|
||||
@@ -378,19 +165,25 @@ export function normalizeCompatibilityConfig({
|
||||
const changes: string[] = [];
|
||||
let updated = rawEntry;
|
||||
let changed = false;
|
||||
const shouldPromoteRootDmAllowFrom = !asObjectRecord(updated.accounts);
|
||||
|
||||
const dm = normalizeDiscordDmAliases({
|
||||
const dm = normalizeLegacyDmAliases({
|
||||
entry: updated,
|
||||
pathPrefix: "channels.discord",
|
||||
changes,
|
||||
promoteAllowFrom: shouldPromoteRootDmAllowFrom,
|
||||
});
|
||||
updated = dm.entry;
|
||||
changed = changed || dm.changed;
|
||||
|
||||
const streaming = normalizeDiscordStreamingAliases({
|
||||
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;
|
||||
@@ -406,17 +199,21 @@ export function normalizeCompatibilityConfig({
|
||||
}
|
||||
let accountEntry = account;
|
||||
let accountChanged = false;
|
||||
const accountDm = normalizeDiscordDmAliases({
|
||||
const accountDm = normalizeLegacyDmAliases({
|
||||
entry: accountEntry,
|
||||
pathPrefix: `channels.discord.accounts.${accountId}`,
|
||||
changes,
|
||||
});
|
||||
accountEntry = accountDm.entry;
|
||||
accountChanged = accountDm.changed;
|
||||
const accountStreaming = normalizeDiscordStreamingAliases({
|
||||
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;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
type ChannelDoctorAdapter,
|
||||
type ChannelDoctorConfigMutation,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import { type ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract";
|
||||
import { type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import { normalizeCompatibilityConfig as normalizeDiscordCompatibilityConfig } from "./doctor-contract.js";
|
||||
import { DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared.js";
|
||||
import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js";
|
||||
|
||||
type DiscordNumericIdHit = { path: string; entry: number; safe: boolean };
|
||||
|
||||
@@ -21,14 +18,6 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const existing = asObjectRecord(owner[key]);
|
||||
if (existing) {
|
||||
return { ...existing };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function sanitizeForLog(value: string): string {
|
||||
return value.replace(/\p{Cc}+/gu, " ").trim();
|
||||
}
|
||||
@@ -54,275 +43,6 @@ function isDiscordMutableAllowEntry(raw: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeDiscordDmAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; changed: boolean } {
|
||||
let changed = false;
|
||||
let updated: Record<string, unknown> = params.entry;
|
||||
const rawDm = updated.dm;
|
||||
const dm = asObjectRecord(rawDm) ? (structuredClone(rawDm) as Record<string, unknown>) : null;
|
||||
let dmChanged = false;
|
||||
|
||||
const allowFromEqual = (a: unknown, b: unknown): boolean => {
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
const na = a.map((v) => String(v).trim()).filter(Boolean);
|
||||
const nb = b.map((v) => String(v).trim()).filter(Boolean);
|
||||
if (na.length !== nb.length) {
|
||||
return false;
|
||||
}
|
||||
return na.every((v, i) => v === nb[i]);
|
||||
};
|
||||
|
||||
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).`);
|
||||
}
|
||||
}
|
||||
|
||||
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 &&
|
||||
allowFromEqual(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 normalizeDiscordStreamingAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.draftChunk !== undefined ||
|
||||
params.entry.blockStreamingCoalesce !== undefined;
|
||||
const resolved = resolveDiscordPreviewStreamMode(params.entry);
|
||||
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 = resolved;
|
||||
if (hadLegacyStreamMode) {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "boolean") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "string") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
}
|
||||
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;
|
||||
if (
|
||||
params.pathPrefix.startsWith("channels.discord") &&
|
||||
resolved === "off" &&
|
||||
hadLegacyStreamMode
|
||||
) {
|
||||
params.changes.push(
|
||||
`${params.pathPrefix}.streaming remains off by default to avoid Discord preview-edit rate limits; set ${params.pathPrefix}.streaming.mode="partial" to opt in explicitly.`,
|
||||
);
|
||||
}
|
||||
return { entry: updated, changed };
|
||||
}
|
||||
|
||||
function normalizeDiscordCompatibilityConfig(cfg: OpenClawConfig): ChannelDoctorConfigMutation {
|
||||
const rawEntry = asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.discord);
|
||||
if (!rawEntry) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
let updated = rawEntry;
|
||||
let changed = false;
|
||||
|
||||
const base = normalizeDiscordDmAliases({
|
||||
entry: rawEntry,
|
||||
pathPrefix: "channels.discord",
|
||||
changes,
|
||||
});
|
||||
updated = base.entry;
|
||||
changed = base.changed;
|
||||
|
||||
const streaming = normalizeDiscordStreamingAliases({
|
||||
entry: updated,
|
||||
pathPrefix: "channels.discord",
|
||||
changes,
|
||||
});
|
||||
updated = streaming.entry;
|
||||
changed = changed || streaming.changed;
|
||||
|
||||
const rawAccounts = asObjectRecord(updated.accounts);
|
||||
if (rawAccounts) {
|
||||
let accountsChanged = false;
|
||||
const accounts = { ...rawAccounts };
|
||||
for (const [accountId, rawAccount] of Object.entries(rawAccounts)) {
|
||||
const account = asObjectRecord(rawAccount);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
let accountEntry = account;
|
||||
let accountChanged = false;
|
||||
const dm = normalizeDiscordDmAliases({
|
||||
entry: account,
|
||||
pathPrefix: `channels.discord.accounts.${accountId}`,
|
||||
changes,
|
||||
});
|
||||
accountEntry = dm.entry;
|
||||
accountChanged = dm.changed;
|
||||
const accountStreaming = normalizeDiscordStreamingAliases({
|
||||
entry: accountEntry,
|
||||
pathPrefix: `channels.discord.accounts.${accountId}`,
|
||||
changes,
|
||||
});
|
||||
accountEntry = accountStreaming.entry;
|
||||
accountChanged = accountChanged || accountStreaming.changed;
|
||||
if (accountChanged) {
|
||||
accounts[accountId] = accountEntry;
|
||||
accountsChanged = true;
|
||||
}
|
||||
}
|
||||
if (accountsChanged) {
|
||||
updated = { ...updated, accounts };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
discord: updated,
|
||||
} as OpenClawConfig["channels"],
|
||||
},
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
||||
function collectDiscordAccountScopes(
|
||||
cfg: OpenClawConfig,
|
||||
): Array<{ prefix: string; account: Record<string, unknown> }> {
|
||||
@@ -604,7 +324,7 @@ export const discordDoctor: ChannelDoctorAdapter = {
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
legacyConfigRules: DISCORD_LEGACY_CONFIG_RULES,
|
||||
normalizeCompatibilityConfig: ({ cfg }) => normalizeDiscordCompatibilityConfig(cfg),
|
||||
normalizeCompatibilityConfig: normalizeDiscordCompatibilityConfig,
|
||||
collectPreviewWarnings: ({ cfg, doctorFixCommand }) =>
|
||||
collectDiscordNumericIdWarnings({
|
||||
hits: scanDiscordNumericIdEntries(cfg),
|
||||
|
||||
@@ -3,6 +3,11 @@ import type {
|
||||
ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
hasLegacyStreamingAliases,
|
||||
normalizeLegacyDmAliases,
|
||||
normalizeLegacyStreamingAliases,
|
||||
} from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import { resolveSlackNativeStreaming, resolveSlackStreamingMode } from "./streaming-compat.js";
|
||||
|
||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
@@ -11,130 +16,8 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const existing = asObjectRecord(owner[key]);
|
||||
if (existing) {
|
||||
return { ...existing };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function normalizeSlackStreamingAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.nativeStreaming !== undefined;
|
||||
const resolvedStreaming = resolveSlackStreamingMode(params.entry);
|
||||
const resolvedNativeStreaming = resolveSlackNativeStreaming(params.entry);
|
||||
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");
|
||||
|
||||
if (
|
||||
(hadLegacyStreamMode ||
|
||||
typeof beforeStreaming === "boolean" ||
|
||||
typeof beforeStreaming === "string") &&
|
||||
streaming.mode === undefined
|
||||
) {
|
||||
streaming.mode = resolvedStreaming;
|
||||
if (hadLegacyStreamMode) {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${resolvedStreaming}).`,
|
||||
);
|
||||
}
|
||||
if (typeof beforeStreaming === "boolean") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${resolvedStreaming}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "string") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${resolvedStreaming}).`,
|
||||
);
|
||||
}
|
||||
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.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) {
|
||||
streaming.nativeTransport = resolvedNativeStreaming;
|
||||
delete updated.nativeStreaming;
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.nativeStreaming → ${params.pathPrefix}.streaming.nativeTransport.`,
|
||||
);
|
||||
changed = true;
|
||||
} else if (typeof beforeStreaming === "boolean" && streaming.nativeTransport === undefined) {
|
||||
streaming.nativeTransport = resolvedNativeStreaming;
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.nativeTransport.`,
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (Object.keys(block).length > 0) {
|
||||
streaming.block = block;
|
||||
}
|
||||
updated.streaming = streaming;
|
||||
|
||||
return { entry: updated, changed };
|
||||
}
|
||||
|
||||
function hasLegacySlackStreamingAliases(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.blockStreamingCoalesce !== undefined ||
|
||||
entry.nativeStreaming !== undefined
|
||||
);
|
||||
return hasLegacyStreamingAliases(value, { includeNativeTransport: true });
|
||||
}
|
||||
|
||||
function hasLegacySlackAccountStreamingAliases(value: unknown): boolean {
|
||||
@@ -174,13 +57,23 @@ export function normalizeCompatibilityConfig({
|
||||
let updated = rawEntry;
|
||||
let changed = false;
|
||||
|
||||
const baseStreaming = normalizeSlackStreamingAliases({
|
||||
const dm = normalizeLegacyDmAliases({
|
||||
entry: updated,
|
||||
pathPrefix: "channels.slack",
|
||||
changes,
|
||||
});
|
||||
updated = baseStreaming.entry;
|
||||
changed = changed || baseStreaming.changed;
|
||||
updated = dm.entry;
|
||||
changed = changed || dm.changed;
|
||||
|
||||
const streaming = normalizeLegacyStreamingAliases({
|
||||
entry: updated,
|
||||
pathPrefix: "channels.slack",
|
||||
changes,
|
||||
resolvedMode: resolveSlackStreamingMode(updated),
|
||||
resolvedNativeTransport: resolveSlackNativeStreaming(updated),
|
||||
});
|
||||
updated = streaming.entry;
|
||||
changed = changed || streaming.changed;
|
||||
|
||||
const rawAccounts = asObjectRecord(updated.accounts);
|
||||
if (rawAccounts) {
|
||||
@@ -191,13 +84,26 @@ export function normalizeCompatibilityConfig({
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const streaming = normalizeSlackStreamingAliases({
|
||||
entry: account,
|
||||
let accountEntry = account;
|
||||
let accountChanged = false;
|
||||
const accountDm = normalizeLegacyDmAliases({
|
||||
entry: accountEntry,
|
||||
pathPrefix: `channels.slack.accounts.${accountId}`,
|
||||
changes,
|
||||
});
|
||||
if (streaming.changed) {
|
||||
accounts[accountId] = streaming.entry;
|
||||
accountEntry = accountDm.entry;
|
||||
accountChanged = accountDm.changed;
|
||||
const accountStreaming = normalizeLegacyStreamingAliases({
|
||||
entry: accountEntry,
|
||||
pathPrefix: `channels.slack.accounts.${accountId}`,
|
||||
changes,
|
||||
resolvedMode: resolveSlackStreamingMode(accountEntry),
|
||||
resolvedNativeTransport: resolveSlackNativeStreaming(accountEntry),
|
||||
});
|
||||
accountEntry = accountStreaming.entry;
|
||||
accountChanged = accountChanged || accountStreaming.changed;
|
||||
if (accountChanged) {
|
||||
accounts[accountId] = accountEntry;
|
||||
accountsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
type ChannelDoctorAdapter,
|
||||
type ChannelDoctorConfigMutation,
|
||||
type ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import { type ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract";
|
||||
import { type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import {
|
||||
legacyConfigRules as SLACK_LEGACY_CONFIG_RULES,
|
||||
normalizeCompatibilityConfig as normalizeSlackCompatibilityConfig,
|
||||
} from "./doctor-contract.js";
|
||||
import { isSlackMutableAllowEntry } from "./security-doctor.js";
|
||||
import { resolveSlackNativeStreaming, resolveSlackStreamingMode } from "./streaming-compat.js";
|
||||
|
||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
@@ -14,283 +13,9 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const existing = asObjectRecord(owner[key]);
|
||||
if (existing) {
|
||||
return { ...existing };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function sanitizeForLog(value: string): string {
|
||||
return value.replace(/\p{Cc}+/gu, " ").trim();
|
||||
}
|
||||
|
||||
function normalizeSlackDmAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; changed: boolean } {
|
||||
let changed = false;
|
||||
let updated: Record<string, unknown> = params.entry;
|
||||
const rawDm = updated.dm;
|
||||
const dm = asObjectRecord(rawDm) ? (structuredClone(rawDm) as Record<string, unknown>) : null;
|
||||
let dmChanged = false;
|
||||
|
||||
const allowFromEqual = (a: unknown, b: unknown): boolean => {
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
const na = a.map((v) => String(v).trim()).filter(Boolean);
|
||||
const nb = b.map((v) => String(v).trim()).filter(Boolean);
|
||||
if (na.length !== nb.length) {
|
||||
return false;
|
||||
}
|
||||
return na.every((v, i) => v === nb[i]);
|
||||
};
|
||||
|
||||
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).`);
|
||||
}
|
||||
}
|
||||
|
||||
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 &&
|
||||
allowFromEqual(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 normalizeSlackStreamingAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.nativeStreaming !== undefined;
|
||||
const resolvedStreaming = resolveSlackStreamingMode(params.entry);
|
||||
const resolvedNativeStreaming = resolveSlackNativeStreaming(params.entry);
|
||||
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");
|
||||
|
||||
if (
|
||||
(hadLegacyStreamMode ||
|
||||
typeof beforeStreaming === "boolean" ||
|
||||
typeof beforeStreaming === "string") &&
|
||||
streaming.mode === undefined
|
||||
) {
|
||||
streaming.mode = resolvedStreaming;
|
||||
if (hadLegacyStreamMode) {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${resolvedStreaming}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "boolean") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${resolvedStreaming}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "string") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${resolvedStreaming}).`,
|
||||
);
|
||||
}
|
||||
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.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) {
|
||||
streaming.nativeTransport = resolvedNativeStreaming;
|
||||
delete updated.nativeStreaming;
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.nativeStreaming → ${params.pathPrefix}.streaming.nativeTransport.`,
|
||||
);
|
||||
changed = true;
|
||||
} else if (typeof beforeStreaming === "boolean" && streaming.nativeTransport === undefined) {
|
||||
streaming.nativeTransport = resolvedNativeStreaming;
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.nativeTransport.`,
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (Object.keys(block).length > 0) {
|
||||
streaming.block = block;
|
||||
}
|
||||
updated.streaming = streaming;
|
||||
|
||||
return { entry: updated, changed };
|
||||
}
|
||||
|
||||
function normalizeSlackCompatibilityConfig(cfg: OpenClawConfig): ChannelDoctorConfigMutation {
|
||||
const rawEntry = asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.slack);
|
||||
if (!rawEntry) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
let updated = rawEntry;
|
||||
let changed = false;
|
||||
|
||||
const base = normalizeSlackDmAliases({
|
||||
entry: rawEntry,
|
||||
pathPrefix: "channels.slack",
|
||||
changes,
|
||||
});
|
||||
updated = base.entry;
|
||||
changed = base.changed;
|
||||
|
||||
const baseStreaming = normalizeSlackStreamingAliases({
|
||||
entry: updated,
|
||||
pathPrefix: "channels.slack",
|
||||
changes,
|
||||
});
|
||||
updated = baseStreaming.entry;
|
||||
changed = changed || baseStreaming.changed;
|
||||
|
||||
const rawAccounts = asObjectRecord(updated.accounts);
|
||||
if (rawAccounts) {
|
||||
let accountsChanged = false;
|
||||
const accounts = { ...rawAccounts };
|
||||
for (const [accountId, rawAccount] of Object.entries(rawAccounts)) {
|
||||
const account = asObjectRecord(rawAccount);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
let accountEntry = account;
|
||||
let accountChanged = false;
|
||||
const dm = normalizeSlackDmAliases({
|
||||
entry: account,
|
||||
pathPrefix: `channels.slack.accounts.${accountId}`,
|
||||
changes,
|
||||
});
|
||||
accountEntry = dm.entry;
|
||||
accountChanged = dm.changed;
|
||||
const streaming = normalizeSlackStreamingAliases({
|
||||
entry: accountEntry,
|
||||
pathPrefix: `channels.slack.accounts.${accountId}`,
|
||||
changes,
|
||||
});
|
||||
accountEntry = streaming.entry;
|
||||
accountChanged = accountChanged || streaming.changed;
|
||||
if (accountChanged) {
|
||||
accounts[accountId] = accountEntry;
|
||||
accountsChanged = true;
|
||||
}
|
||||
}
|
||||
if (accountsChanged) {
|
||||
updated = { ...updated, accounts };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: updated as unknown as NonNullable<OpenClawConfig["channels"]>["slack"],
|
||||
} as OpenClawConfig["channels"],
|
||||
},
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
||||
export function collectSlackMutableAllowlistWarnings(cfg: OpenClawConfig): string[] {
|
||||
const hits: Array<{ path: string; entry: string }> = [];
|
||||
const addHits = (pathLabel: string, list: unknown) => {
|
||||
@@ -344,51 +69,12 @@ export function collectSlackMutableAllowlistWarnings(cfg: OpenClawConfig): strin
|
||||
];
|
||||
}
|
||||
|
||||
function hasLegacySlackStreamingAliases(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.blockStreamingCoalesce !== undefined ||
|
||||
entry.nativeStreaming !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function hasLegacySlackAccountStreamingAliases(value: unknown): boolean {
|
||||
const accounts = asObjectRecord(value);
|
||||
if (!accounts) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(accounts).some((account) => hasLegacySlackStreamingAliases(account));
|
||||
}
|
||||
|
||||
const SLACK_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [
|
||||
{
|
||||
path: ["channels", "slack"],
|
||||
message:
|
||||
"channels.slack.streamMode, channels.slack.streaming (scalar), chunkMode, blockStreaming, blockStreamingCoalesce, and nativeStreaming are legacy; use channels.slack.streaming.{mode,chunkMode,block.enabled,block.coalesce,nativeTransport}.",
|
||||
match: hasLegacySlackStreamingAliases,
|
||||
},
|
||||
{
|
||||
path: ["channels", "slack", "accounts"],
|
||||
message:
|
||||
"channels.slack.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, blockStreamingCoalesce, and nativeStreaming are legacy; use channels.slack.accounts.<id>.streaming.{mode,chunkMode,block.enabled,block.coalesce,nativeTransport}.",
|
||||
match: hasLegacySlackAccountStreamingAliases,
|
||||
},
|
||||
];
|
||||
|
||||
export const slackDoctor: ChannelDoctorAdapter = {
|
||||
dmAllowFromMode: "topOrNested",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
legacyConfigRules: SLACK_LEGACY_CONFIG_RULES,
|
||||
normalizeCompatibilityConfig: ({ cfg }) => normalizeSlackCompatibilityConfig(cfg),
|
||||
normalizeCompatibilityConfig: normalizeSlackCompatibilityConfig,
|
||||
collectMutableAllowlistWarnings: ({ cfg }) => collectSlackMutableAllowlistWarnings(cfg),
|
||||
};
|
||||
|
||||
@@ -3,6 +3,10 @@ import type {
|
||||
ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
hasLegacyStreamingAliases,
|
||||
normalizeLegacyStreamingAliases,
|
||||
} from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js";
|
||||
|
||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
@@ -11,125 +15,8 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const existing = asObjectRecord(owner[key]);
|
||||
if (existing) {
|
||||
return { ...existing };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function normalizeTelegramStreamingAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.draftChunk !== undefined ||
|
||||
params.entry.blockStreamingCoalesce !== undefined;
|
||||
const resolved = resolveTelegramPreviewStreamMode(params.entry);
|
||||
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 = resolved;
|
||||
if (hadLegacyStreamMode) {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "boolean") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "string") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
function hasLegacyTelegramStreamingAliases(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
|
||||
);
|
||||
return hasLegacyStreamingAliases(value, { includePreviewChunk: true });
|
||||
}
|
||||
|
||||
function hasLegacyTelegramAccountStreamingAliases(value: unknown): boolean {
|
||||
@@ -218,13 +105,15 @@ export function normalizeCompatibilityConfig({
|
||||
}
|
||||
}
|
||||
|
||||
const base = normalizeTelegramStreamingAliases({
|
||||
const streaming = normalizeLegacyStreamingAliases({
|
||||
entry: updated,
|
||||
pathPrefix: "channels.telegram",
|
||||
changes,
|
||||
includePreviewChunk: true,
|
||||
resolvedMode: resolveTelegramPreviewStreamMode(updated),
|
||||
});
|
||||
updated = base.entry;
|
||||
changed = changed || base.changed;
|
||||
updated = streaming.entry;
|
||||
changed = changed || streaming.changed;
|
||||
|
||||
const rawAccounts = asObjectRecord(updated.accounts);
|
||||
if (rawAccounts) {
|
||||
@@ -235,10 +124,12 @@ export function normalizeCompatibilityConfig({
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountStreaming = normalizeTelegramStreamingAliases({
|
||||
const accountStreaming = normalizeLegacyStreamingAliases({
|
||||
entry: account,
|
||||
pathPrefix: `channels.telegram.accounts.${accountId}`,
|
||||
changes,
|
||||
includePreviewChunk: true,
|
||||
resolvedMode: resolveTelegramPreviewStreamMode(account),
|
||||
});
|
||||
if (accountStreaming.changed) {
|
||||
accounts[accountId] = accountStreaming.entry;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {
|
||||
type ChannelDoctorAdapter,
|
||||
type ChannelDoctorConfigMutation,
|
||||
type ChannelDoctorEmptyAllowlistAccountContext,
|
||||
type ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import { type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { inspectTelegramAccount } from "./account-inspect.js";
|
||||
import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js";
|
||||
import { isNumericTelegramUserId, normalizeTelegramAllowFromEntry } from "./allow-from.js";
|
||||
import { lookupTelegramChatId } from "./api-fetch.js";
|
||||
import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js";
|
||||
import {
|
||||
legacyConfigRules as TELEGRAM_LEGACY_CONFIG_RULES,
|
||||
normalizeCompatibilityConfig as normalizeTelegramCompatibilityConfig,
|
||||
} from "./doctor-contract.js";
|
||||
|
||||
type TelegramAllowFromUsernameHit = { path: string; entry: string };
|
||||
type DoctorAllowFromList = Array<string | number>;
|
||||
@@ -27,14 +28,6 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const existing = asObjectRecord(owner[key]);
|
||||
if (existing) {
|
||||
return { ...existing };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function sanitizeForLog(value: string): string {
|
||||
return value.replace(/\p{Cc}+/gu, " ").trim();
|
||||
}
|
||||
@@ -43,161 +36,6 @@ function describeUnknownError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function normalizeTelegramStreamingAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { entry: Record<string, unknown>; 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.draftChunk !== undefined ||
|
||||
params.entry.blockStreamingCoalesce !== undefined;
|
||||
const resolved = resolveTelegramPreviewStreamMode(params.entry);
|
||||
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 = resolved;
|
||||
if (hadLegacyStreamMode) {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "boolean") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
} else if (typeof beforeStreaming === "string") {
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${resolved}).`,
|
||||
);
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
function normalizeTelegramCompatibilityConfig(cfg: OpenClawConfig): ChannelDoctorConfigMutation {
|
||||
const rawEntry = asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.telegram);
|
||||
if (!rawEntry) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
let updated = rawEntry;
|
||||
let changed = false;
|
||||
|
||||
const base = normalizeTelegramStreamingAliases({
|
||||
entry: rawEntry,
|
||||
pathPrefix: "channels.telegram",
|
||||
changes,
|
||||
});
|
||||
updated = base.entry;
|
||||
changed = base.changed;
|
||||
|
||||
const rawAccounts = asObjectRecord(updated.accounts);
|
||||
if (rawAccounts) {
|
||||
let accountsChanged = false;
|
||||
const accounts = { ...rawAccounts };
|
||||
for (const [accountId, rawAccount] of Object.entries(rawAccounts)) {
|
||||
const account = asObjectRecord(rawAccount);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountStreaming = normalizeTelegramStreamingAliases({
|
||||
entry: account,
|
||||
pathPrefix: `channels.telegram.accounts.${accountId}`,
|
||||
changes,
|
||||
});
|
||||
if (accountStreaming.changed) {
|
||||
accounts[accountId] = accountStreaming.entry;
|
||||
accountsChanged = true;
|
||||
}
|
||||
}
|
||||
if (accountsChanged) {
|
||||
updated = { ...updated, accounts };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
telegram: updated as unknown as NonNullable<OpenClawConfig["channels"]>["telegram"],
|
||||
} as OpenClawConfig["channels"],
|
||||
},
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
||||
function hasAllowFromEntries(values?: DoctorAllowFromList): boolean {
|
||||
return Array.isArray(values) && values.some((entry) => String(entry).trim());
|
||||
}
|
||||
@@ -515,48 +353,9 @@ export function collectTelegramEmptyAllowlistExtraWarnings(
|
||||
: [];
|
||||
}
|
||||
|
||||
function hasLegacyTelegramStreamingAliases(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 hasLegacyTelegramAccountStreamingAliases(value: unknown): boolean {
|
||||
const accounts = asObjectRecord(value);
|
||||
if (!accounts) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(accounts).some((account) => hasLegacyTelegramStreamingAliases(account));
|
||||
}
|
||||
|
||||
const TELEGRAM_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [
|
||||
{
|
||||
path: ["channels", "telegram"],
|
||||
message:
|
||||
"channels.telegram.streamMode, channels.telegram.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.telegram.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}.",
|
||||
match: hasLegacyTelegramStreamingAliases,
|
||||
},
|
||||
{
|
||||
path: ["channels", "telegram", "accounts"],
|
||||
message:
|
||||
"channels.telegram.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.telegram.accounts.<id>.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}.",
|
||||
match: hasLegacyTelegramAccountStreamingAliases,
|
||||
},
|
||||
];
|
||||
|
||||
export const telegramDoctor: ChannelDoctorAdapter = {
|
||||
legacyConfigRules: TELEGRAM_LEGACY_CONFIG_RULES,
|
||||
normalizeCompatibilityConfig: ({ cfg }) => normalizeTelegramCompatibilityConfig(cfg),
|
||||
normalizeCompatibilityConfig: normalizeTelegramCompatibilityConfig,
|
||||
collectPreviewWarnings: ({ cfg, doctorFixCommand }) =>
|
||||
collectTelegramAllowFromUsernameWarnings({
|
||||
hits: scanTelegramAllowFromUsernameEntries(cfg),
|
||||
|
||||
262
src/config/channel-compat-normalization.ts
Normal file
262
src/config/channel-compat-normalization.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
type CompatMutationResult = {
|
||||
entry: Record<string, unknown>;
|
||||
changed: boolean;
|
||||
};
|
||||
|
||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
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 = left.map((value) => String(value).trim()).filter(Boolean);
|
||||
const normalizedRight = right.map((value) => String(value).trim()).filter(Boolean);
|
||||
if (normalizedLeft.length !== normalizedRight.length) {
|
||||
return false;
|
||||
}
|
||||
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
||||
}
|
||||
|
||||
export function normalizeLegacyDmAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
promoteAllowFrom?: boolean;
|
||||
}): CompatMutationResult {
|
||||
let changed = false;
|
||||
let updated: Record<string, unknown> = params.entry;
|
||||
const rawDm = updated.dm;
|
||||
const dm = asObjectRecord(rawDm) ? (structuredClone(rawDm) as Record<string, unknown>) : 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 };
|
||||
}
|
||||
|
||||
export function normalizeLegacyStreamingAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
resolvedMode: string;
|
||||
includePreviewChunk?: boolean;
|
||||
resolvedNativeTransport?: unknown;
|
||||
offModeLegacyNotice?: (pathPrefix: string) => string;
|
||||
}): CompatMutationResult {
|
||||
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 };
|
||||
}
|
||||
|
||||
export function hasLegacyStreamingAliases(
|
||||
value: unknown,
|
||||
options?: { includePreviewChunk?: boolean; includeNativeTransport?: boolean },
|
||||
): 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.blockStreamingCoalesce !== undefined ||
|
||||
(options?.includePreviewChunk === true && entry.draftChunk !== undefined) ||
|
||||
(options?.includeNativeTransport === true && entry.nativeStreaming !== undefined)
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
export { collectProviderDangerousNameMatchingScopes } from "../config/dangerous-name-matching.js";
|
||||
export {
|
||||
hasLegacyStreamingAliases,
|
||||
normalizeLegacyDmAliases,
|
||||
normalizeLegacyStreamingAliases,
|
||||
} from "../config/channel-compat-normalization.js";
|
||||
export {
|
||||
detectPluginInstallPathIssue,
|
||||
formatPluginInstallPathIssue,
|
||||
|
||||
Reference in New Issue
Block a user