fix(check): finish extension type cleanup

This commit is contained in:
Vincent Koc
2026-04-06 15:26:47 +01:00
parent fa67ab2358
commit f92ef361ae
5 changed files with 121 additions and 84 deletions

View File

@@ -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;
}

View File

@@ -14,64 +14,48 @@ export interface STTConfig {
model: string;
}
type QQBotSttProviderConfig = {
baseUrl?: string;
apiKey?: string;
};
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null
? (value as Record<string, unknown>)
: 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<string, QQBotSttProviderConfig>;
};
tools?: {
media?: {
audio?: {
models?: QQBotSttToolAudioModel[];
};
};
};
};
function readString(record: Record<string, unknown> | undefined, key: string): string | undefined {
const value = record?.[key];
return typeof value === "string" ? value : undefined;
}
export function resolveSTTConfig(cfg: Record<string, unknown>): 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 };
}

View File

@@ -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<string, unknown> {
return typeof value === "object" && value !== null;
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null
? (value as Record<string, unknown>)
: undefined;
}
function readString(record: Record<string, unknown> | 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<unknown> {
@@ -84,18 +95,16 @@ async function fetchDistTags(): Promise<Record<string, string>> {
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<UpdateInfo> {
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<UpdateInfo> {
alpha: null,
hasUpdate: false,
checkedAt: Date.now(),
error: message,
error: errorMessage,
};
}
}
@@ -173,7 +182,7 @@ export async function checkVersionExists(version: string): Promise<boolean> {
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 {

View File

@@ -227,27 +227,55 @@ type QQBotTtsConfigRoot = {
};
};
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null
? (value as Record<string, unknown>)
: undefined;
}
function readString(record: Record<string, unknown> | undefined, key: string): string | undefined {
const value = record?.[key];
return typeof value === "string" ? value : undefined;
}
function readNumber(record: Record<string, unknown> | undefined, key: string): number | undefined {
const value = record?.[key];
return typeof value === "number" ? value : undefined;
}
function readStringMap(value: unknown): Record<string, string> {
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<string, string> = {
...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<string, unknown>): 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<string, unknown>): 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;
}

View File

@@ -752,10 +752,13 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
return;
}
const contentBody = content.content;
const sentAt = readNumber(content, "sent") ?? Date.now();
cacheMessage(nest, {
author: senderShip,
content: rawText,
timestamp: readNumber(content, "sent") ?? Date.now(),
timestamp: sentAt,
id: messageId,
});
@@ -797,8 +800,8 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
originalMessage: {
messageId: messageId ?? "",
messageText: rawText,
messageContent: content.content,
timestamp: readNumber(content, "sent") ?? Date.now(),
messageContent: contentBody,
timestamp: sentAt,
parentId: parentId ?? undefined,
isThreadReply,
},
@@ -816,7 +819,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
const messageText = await resolveAuthorizedMessageText({
rawText,
content: content.content,
content: contentBody,
authorizedForCites: true,
resolveAllCites,
});
@@ -826,12 +829,12 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
messageId: messageId ?? "",
senderShip,
messageText,
messageContent: content.content, // Pass raw content for media extraction
messageContent: contentBody, // Pass raw content for media extraction
isGroup: true,
channelNest: nest,
hostShip: parsed?.hostShip,
channelName: parsed?.channelName,
timestamp: readNumber(content, "sent") ?? Date.now(),
timestamp: sentAt,
parentId,
isThreadReply,
});