From f92ef361aeb197eaac9cca7930c632149eae49e7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 6 Apr 2026 15:26:47 +0100 Subject: [PATCH] fix(check): finish extension type cleanup --- extensions/qqbot/src/reply-dispatcher.ts | 10 ++- extensions/qqbot/src/stt.ts | 74 ++++++++------------- extensions/qqbot/src/update-checker.ts | 37 +++++++---- extensions/qqbot/src/utils/audio-convert.ts | 69 ++++++++++++++----- extensions/tlon/src/monitor/index.ts | 15 +++-- 5 files changed, 121 insertions(+), 84 deletions(-) diff --git a/extensions/qqbot/src/reply-dispatcher.ts b/extensions/qqbot/src/reply-dispatcher.ts index 9c21c4c9c0d..42c155491c1 100644 --- a/extensions/qqbot/src/reply-dispatcher.ts +++ b/extensions/qqbot/src/reply-dispatcher.ts @@ -149,6 +149,7 @@ export async function handleStructuredPayload( } const parsedPayload = payloadResult.payload; + const unknownPayload = payloadResult.payload as unknown; log?.info( `[qqbot:${account.accountId}] Detected structured payload, type: ${parsedPayload.type}`, ); @@ -195,7 +196,14 @@ export async function handleStructuredPayload( return true; } - log?.error(`[qqbot:${account.accountId}] Unknown payload type in structured payload`); + const payloadType = + typeof unknownPayload === "object" && + unknownPayload !== null && + "type" in unknownPayload && + typeof unknownPayload.type === "string" + ? unknownPayload.type + : "unknown"; + log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${payloadType}`); return true; } diff --git a/extensions/qqbot/src/stt.ts b/extensions/qqbot/src/stt.ts index 2d635ff48e3..529b3d13e26 100644 --- a/extensions/qqbot/src/stt.ts +++ b/extensions/qqbot/src/stt.ts @@ -14,64 +14,48 @@ export interface STTConfig { model: string; } -type QQBotSttProviderConfig = { - baseUrl?: string; - apiKey?: string; -}; +function asRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null + ? (value as Record) + : undefined; +} -type QQBotSttChannelConfig = QQBotSttProviderConfig & { - enabled?: boolean; - provider?: string; - model?: string; -}; - -type QQBotSttToolAudioModel = QQBotSttProviderConfig & { - provider?: string; - model?: string; -}; - -type QQBotSttConfigRoot = { - channels?: { - qqbot?: { - stt?: QQBotSttChannelConfig; - }; - }; - models?: { - providers?: Record; - }; - tools?: { - media?: { - audio?: { - models?: QQBotSttToolAudioModel[]; - }; - }; - }; -}; +function readString(record: Record | undefined, key: string): string | undefined { + const value = record?.[key]; + return typeof value === "string" ? value : undefined; +} export function resolveSTTConfig(cfg: Record): STTConfig | null { - const c = cfg as QQBotSttConfigRoot; + const channels = asRecord(cfg.channels); + const qqbot = asRecord(channels?.qqbot); + const channelStt = asRecord(qqbot?.stt); + const models = asRecord(cfg.models); + const providers = asRecord(models?.providers); // Prefer plugin-specific STT config. - const channelStt = c?.channels?.qqbot?.stt; if (channelStt && channelStt.enabled !== false) { - const providerId: string = channelStt?.provider || "openai"; - const providerCfg = c?.models?.providers?.[providerId]; - const baseUrl: string | undefined = channelStt?.baseUrl || providerCfg?.baseUrl; - const apiKey: string | undefined = channelStt?.apiKey || providerCfg?.apiKey; - const model: string = channelStt?.model || "whisper-1"; + const providerId = readString(channelStt, "provider") ?? "openai"; + const providerCfg = asRecord(providers?.[providerId]); + const baseUrl = readString(channelStt, "baseUrl") ?? readString(providerCfg, "baseUrl"); + const apiKey = readString(channelStt, "apiKey") ?? readString(providerCfg, "apiKey"); + const model = readString(channelStt, "model") ?? "whisper-1"; if (baseUrl && apiKey) { return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey, model }; } } // Fall back to framework-level audio model config. - const audioModelEntry = c?.tools?.media?.audio?.models?.[0]; + const tools = asRecord(cfg.tools); + const media = asRecord(tools?.media); + const audio = asRecord(media?.audio); + const audioModels = audio?.models; + const audioModelEntry = Array.isArray(audioModels) ? asRecord(audioModels[0]) : undefined; if (audioModelEntry) { - const providerId: string = audioModelEntry?.provider || "openai"; - const providerCfg = c?.models?.providers?.[providerId]; - const baseUrl: string | undefined = audioModelEntry?.baseUrl || providerCfg?.baseUrl; - const apiKey: string | undefined = audioModelEntry?.apiKey || providerCfg?.apiKey; - const model: string = audioModelEntry?.model || "whisper-1"; + const providerId = readString(audioModelEntry, "provider") ?? "openai"; + const providerCfg = asRecord(providers?.[providerId]); + const baseUrl = readString(audioModelEntry, "baseUrl") ?? readString(providerCfg, "baseUrl"); + const apiKey = readString(audioModelEntry, "apiKey") ?? readString(providerCfg, "apiKey"); + const model = readString(audioModelEntry, "model") ?? "whisper-1"; if (baseUrl && apiKey) { return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey, model }; } diff --git a/extensions/qqbot/src/update-checker.ts b/extensions/qqbot/src/update-checker.ts index 2e66455238d..a75e36cf7cc 100644 --- a/extensions/qqbot/src/update-checker.ts +++ b/extensions/qqbot/src/update-checker.ts @@ -44,8 +44,19 @@ let _log: | { info: (msg: string) => void; error: (msg: string) => void; debug?: (msg: string) => void } | undefined; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; +function asRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null + ? (value as Record) + : undefined; +} + +function readString(record: Record | undefined, key: string): string | undefined { + const value = record?.[key]; + return typeof value === "string" ? value : undefined; +} + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); } function fetchJson(url: string, timeoutMs: number): Promise { @@ -84,18 +95,16 @@ async function fetchDistTags(): Promise> { for (const url of REGISTRIES) { try { const json = await fetchJson(url, 10_000); - const tags = isRecord(json) ? json["dist-tags"] : undefined; - if (isRecord(tags)) { + const tags = asRecord(asRecord(json)?.["dist-tags"]); + if (tags) { return Object.fromEntries( - Object.entries(tags).filter((entry): entry is [string, string] => { - return typeof entry[1] === "string"; - }), + Object.entries(tags).flatMap(([key, value]) => + typeof value === "string" ? [[key, value]] : [], + ), ); } } catch (e: unknown) { - _log?.debug?.( - `[qqbot:update-checker] ${url} failed: ${e instanceof Error ? e.message : String(e)}`, - ); + _log?.debug?.(`[qqbot:update-checker] ${url} failed: ${getErrorMessage(e)}`); } } throw new Error("all registries failed"); @@ -151,8 +160,8 @@ export async function getUpdateInfo(): Promise { const tags = await fetchDistTags(); return buildUpdateInfo(tags); } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); - _log?.debug?.(`[qqbot:update-checker] check failed: ${message}`); + const errorMessage = getErrorMessage(err); + _log?.debug?.(`[qqbot:update-checker] check failed: ${errorMessage}`); return { current: CURRENT_VERSION, latest: null, @@ -160,7 +169,7 @@ export async function getUpdateInfo(): Promise { alpha: null, hasUpdate: false, checkedAt: Date.now(), - error: message, + error: errorMessage, }; } } @@ -173,7 +182,7 @@ export async function checkVersionExists(version: string): Promise { try { const url = `${baseUrl}/${version}`; const json = await fetchJson(url, 10_000); - if (isRecord(json) && json.version === version) { + if (readString(asRecord(json), "version") === version) { return true; } } catch { diff --git a/extensions/qqbot/src/utils/audio-convert.ts b/extensions/qqbot/src/utils/audio-convert.ts index 33988b98f89..3406e5c5e5d 100644 --- a/extensions/qqbot/src/utils/audio-convert.ts +++ b/extensions/qqbot/src/utils/audio-convert.ts @@ -227,27 +227,55 @@ type QQBotTtsConfigRoot = { }; }; +function asRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null + ? (value as Record) + : undefined; +} + +function readString(record: Record | undefined, key: string): string | undefined { + const value = record?.[key]; + return typeof value === "string" ? value : undefined; +} + +function readNumber(record: Record | undefined, key: string): number | undefined { + const value = record?.[key]; + return typeof value === "number" ? value : undefined; +} + +function readStringMap(value: unknown): Record { + const record = asRecord(value); + if (!record) { + return {}; + } + return Object.fromEntries( + Object.entries(record).flatMap(([key, entryValue]) => + typeof entryValue === "string" ? [[key, entryValue]] : [], + ), + ); +} + function resolveTTSFromBlock( block: QQBotTtsBlock, providerCfg: QQBotTtsProviderConfig | undefined, ): TTSConfig | null { - const baseUrl: string | undefined = block?.baseUrl || providerCfg?.baseUrl; - const apiKey: string | undefined = block?.apiKey || providerCfg?.apiKey; - const model: string = block?.model || "tts-1"; - const voice: string = block?.voice || "alloy"; + const baseUrl = readString(block, "baseUrl") ?? readString(providerCfg, "baseUrl"); + const apiKey = readString(block, "apiKey") ?? readString(providerCfg, "apiKey"); + const model = readString(block, "model") ?? "tts-1"; + const voice = readString(block, "voice") ?? "alloy"; if (!baseUrl || !apiKey) { return null; } const authStyle = - (block?.authStyle || providerCfg?.authStyle) === "api-key" + (readString(block, "authStyle") ?? readString(providerCfg, "authStyle")) === "api-key" ? ("api-key" as const) : ("bearer" as const); const queryParams: Record = { - ...providerCfg?.queryParams, - ...block?.queryParams, + ...readStringMap(providerCfg?.queryParams), + ...readStringMap(block.queryParams), }; - const speed: number | undefined = block?.speed; + const speed = readNumber(block, "speed"); return { baseUrl: baseUrl.replace(/\/+$/, ""), @@ -261,13 +289,16 @@ function resolveTTSFromBlock( } export function resolveTTSConfig(cfg: Record): TTSConfig | null { - const c = cfg as QQBotTtsConfigRoot; + const models = asRecord(cfg.models); + const providers = asRecord(models?.providers); // Prefer plugin-specific TTS config first. - const channelTts = c?.channels?.qqbot?.tts; + const channels = asRecord(cfg.channels); + const qqbot = asRecord(channels?.qqbot); + const channelTts = asRecord(qqbot?.tts); if (channelTts && channelTts.enabled !== false) { - const providerId: string = channelTts?.provider || "openai"; - const providerCfg = c?.models?.providers?.[providerId]; + const providerId = readString(channelTts, "provider") ?? "openai"; + const providerCfg = asRecord(providers?.[providerId]); const result = resolveTTSFromBlock(channelTts, providerCfg); if (result) { return result; @@ -275,12 +306,14 @@ export function resolveTTSConfig(cfg: Record): TTSConfig | null } // Fall back to framework-level TTS config. - const msgTts = c?.messages?.tts; - if (msgTts && msgTts.auto !== "off" && msgTts.auto !== "disabled") { - const providerId: string = msgTts?.provider || "openai"; - const providerBlock = msgTts?.[providerId] as QQBotTtsBlock | undefined; - const providerCfg = c?.models?.providers?.[providerId]; - const result = resolveTTSFromBlock(providerBlock ?? {}, providerCfg); + const messages = asRecord(cfg.messages); + const msgTts = asRecord(messages?.tts); + const autoMode = readString(msgTts, "auto"); + if (msgTts && autoMode !== "off" && autoMode !== "disabled") { + const providerId = readString(msgTts, "provider") ?? "openai"; + const providerBlock = asRecord(msgTts[providerId]) ?? {}; + const providerCfg = asRecord(providers?.[providerId]); + const result = resolveTTSFromBlock(providerBlock, providerCfg); if (result) { return result; } diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index 39df3f3d3fb..90d316813e1 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -752,10 +752,13 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise