From 732c18cd0660e8a968bbb3a79238c103a7cd2bf5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 15:03:49 +0100 Subject: [PATCH] fix(check): repair latest type drift batch --- extensions/feishu/src/accounts.ts | 13 ++- .../synology-chat/src/webhook-handler.test.ts | 10 +- extensions/thread-ownership/index.test.ts | 5 +- extensions/tlon/src/monitor/cites.ts | 11 ++- extensions/tlon/src/monitor/discovery.ts | 28 ++++-- extensions/tlon/src/monitor/history.ts | 99 +++++++++++++------ extensions/tlon/src/monitor/index.ts | 42 ++++---- extensions/tlon/src/urbit/story.ts | 2 +- extensions/zalo/src/monitor.ts | 1 - src/cli/program/command-group-descriptors.ts | 24 +++-- 10 files changed, 152 insertions(+), 83 deletions(-) diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.ts index f7b9e1463e4..edbfa5f846a 100644 --- a/extensions/feishu/src/accounts.ts +++ b/extensions/feishu/src/accounts.ts @@ -15,13 +15,12 @@ import type { ResolvedFeishuAccount, } from "./types.js"; -const { - _listConfiguredAccountIds, - listAccountIds: listFeishuAccountIds, - resolveDefaultAccountId, -} = createAccountListHelpers("feishu", { - allowUnlistedDefaultAccount: true, -}); +const { listAccountIds: listFeishuAccountIds, resolveDefaultAccountId } = createAccountListHelpers( + "feishu", + { + allowUnlistedDefaultAccount: true, + }, +); export { listFeishuAccountIds }; diff --git a/extensions/synology-chat/src/webhook-handler.test.ts b/extensions/synology-chat/src/webhook-handler.test.ts index 466860697b6..896c53164bf 100644 --- a/extensions/synology-chat/src/webhook-handler.test.ts +++ b/extensions/synology-chat/src/webhook-handler.test.ts @@ -10,6 +10,12 @@ const resolveLegacyWebhookNameToChatUserId = vi const { clearSynologyWebhookRateLimiterStateForTest, createWebhookHandler } = await import("./webhook-handler.js"); +type TestLog = { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +}; + function makeAccount( overrides: Partial = {}, ): ResolvedSynologyChatAccount { @@ -40,7 +46,7 @@ const validBody = makeFormBody({ }); async function runDangerousNameMatchReply( - log: { info: unknown; warn: unknown; error: unknown }, + log: TestLog, options: { resolvedChatUserId?: number; accountIdSuffix: string; @@ -73,7 +79,7 @@ async function runDangerousNameMatchReply( } describe("createWebhookHandler", () => { - let log: { info: unknown; warn: unknown; error: unknown }; + let log: TestLog; beforeEach(() => { clearSynologyWebhookRateLimiterStateForTest(); diff --git a/extensions/thread-ownership/index.test.ts b/extensions/thread-ownership/index.test.ts index 1c3273ed0c1..8cb354a96c1 100644 --- a/extensions/thread-ownership/index.test.ts +++ b/extensions/thread-ownership/index.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawPluginApi } from "./api.js"; import register from "./index.js"; describe("thread-ownership plugin", () => { @@ -40,7 +41,7 @@ describe("thread-ownership plugin", () => { describe("message_sending", () => { beforeEach(() => { - register.register(api as unknown); + register.register(api as unknown as OpenClawPluginApi); }); async function sendSlackThreadMessage() { @@ -112,7 +113,7 @@ describe("thread-ownership plugin", () => { describe("message_received @-mention tracking", () => { beforeEach(() => { - register.register(api as unknown); + register.register(api as unknown as OpenClawPluginApi); }); it("tracks @-mentions and skips ownership check for mentioned threads", async () => { diff --git a/extensions/tlon/src/monitor/cites.ts b/extensions/tlon/src/monitor/cites.ts index c88cf2532b1..bb0cc62514b 100644 --- a/extensions/tlon/src/monitor/cites.ts +++ b/extensions/tlon/src/monitor/cites.ts @@ -5,6 +5,10 @@ type TlonScryApi = { scry: (path: string) => Promise; }; +function asRecord(value: unknown): Record | null { + return value && typeof value === "object" ? (value as Record) : null; +} + export function createTlonCitationResolver(params: { api: TlonScryApi; runtime: RuntimeEnv }) { const { api, runtime } = params; @@ -17,9 +21,10 @@ export function createTlonCitationResolver(params: { api: TlonScryApi; runtime: const scryPath = `/channels/v4/${cite.nest}/posts/post/${cite.postId}.json`; runtime.log?.(`[tlon] Fetching cited post: ${scryPath}`); - const data: unknown = await api.scry(scryPath); - if (data?.essay?.content) { - return extractMessageText(data.essay.content) || null; + const data = asRecord(await api.scry(scryPath)); + const essay = asRecord(data?.essay); + if (essay?.content) { + return extractMessageText(essay.content) || null; } return null; diff --git a/extensions/tlon/src/monitor/discovery.ts b/extensions/tlon/src/monitor/discovery.ts index 2e512874f89..1f75b20f1ca 100644 --- a/extensions/tlon/src/monitor/discovery.ts +++ b/extensions/tlon/src/monitor/discovery.ts @@ -2,6 +2,14 @@ import type { RuntimeEnv } from "../../api.js"; import type { Foreigns } from "../urbit/foreigns.js"; import { formatChangesDate } from "./utils.js"; +function asRecord(value: unknown): Record | null { + return value && typeof value === "object" ? (value as Record) : null; +} + +function formatErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + export async function fetchGroupChanges( api: { scry: (path: string) => Promise }, runtime: RuntimeEnv, @@ -18,7 +26,7 @@ export async function fetchGroupChanges( return null; } catch (error: unknown) { runtime.log?.( - `[tlon] Failed to fetch changes (falling back to full init): ${error?.message ?? String(error)}`, + `[tlon] Failed to fetch changes (falling back to full init): ${formatErrorMessage(error)}`, ); return null; } @@ -39,13 +47,16 @@ export async function fetchInitData( ): Promise { try { runtime.log?.("[tlon] Fetching groups-ui init data..."); - const initData = await api.scry("/groups-ui/v6/init.json"); + const initData = asRecord(await api.scry("/groups-ui/v6/init.json")); const channels: string[] = []; - if (initData?.groups) { - for (const groupData of Object.values(initData.groups as Record)) { - if (groupData && typeof groupData === "object" && groupData.channels) { - for (const channelNest of Object.keys(groupData.channels)) { + const groups = asRecord(initData?.groups); + if (groups) { + for (const groupData of Object.values(groups)) { + const typedGroupData = asRecord(groupData); + const groupChannels = asRecord(typedGroupData?.channels); + if (groupChannels) { + for (const channelNest of Object.keys(groupChannels)) { if (channelNest.startsWith("chat/")) { channels.push(channelNest); } @@ -60,7 +71,8 @@ export async function fetchInitData( runtime.log?.("[tlon] No chat channels found via auto-discovery"); } - const foreigns = (initData?.foreigns as Foreigns) || null; + const foreignsValue = asRecord(initData?.foreigns); + const foreigns = foreignsValue ? (foreignsValue as Foreigns) : null; if (foreigns) { const pendingCount = Object.values(foreigns).filter((f) => f.invites?.some((i) => i.valid), @@ -72,7 +84,7 @@ export async function fetchInitData( return { channels, foreigns }; } catch (error: unknown) { - runtime.log?.(`[tlon] Init data fetch failed: ${error?.message ?? String(error)}`); + runtime.log?.(`[tlon] Init data fetch failed: ${formatErrorMessage(error)}`); return { channels: [], foreigns: null }; } } diff --git a/extensions/tlon/src/monitor/history.ts b/extensions/tlon/src/monitor/history.ts index 1d1b0c57b93..a10f76c767c 100644 --- a/extensions/tlon/src/monitor/history.ts +++ b/extensions/tlon/src/monitor/history.ts @@ -1,6 +1,14 @@ import type { RuntimeEnv } from "../../api.js"; import { extractMessageText } from "./utils.js"; +function asRecord(value: unknown): Record | null { + return value && typeof value === "object" ? (value as Record) : null; +} + +function formatErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + /** * Format a number as @ud (with dots every 3 digits from the right) * e.g., 170141184507799509469114119040828178432 -> 170.141.184.507.799.509.469.114.119.040.828.178.432 @@ -62,22 +70,29 @@ export async function fetchChannelHistory( let posts: unknown[] = []; if (Array.isArray(data)) { posts = data; - } else if (data.posts && typeof data.posts === "object") { - posts = Object.values(data.posts); - } else if (typeof data === "object") { - posts = Object.values(data); + } else { + const dataRecord = asRecord(data); + const postMap = asRecord(dataRecord?.posts); + if (postMap) { + posts = Object.values(postMap); + } else if (dataRecord) { + posts = Object.values(dataRecord); + } } const messages = posts .map((item) => { - const essay = item.essay || item["r-post"]?.set?.essay; - const seal = item.seal || item["r-post"]?.set?.seal; + const itemRecord = asRecord(item); + const replyPost = asRecord(itemRecord?.["r-post"]); + const replyPostSet = asRecord(replyPost?.set); + const essay = asRecord(itemRecord?.essay) ?? asRecord(replyPostSet?.essay); + const seal = asRecord(itemRecord?.seal) ?? asRecord(replyPostSet?.seal); return { - author: essay?.author || "unknown", + author: typeof essay?.author === "string" ? essay.author : "unknown", content: extractMessageText(essay?.content || []), - timestamp: essay?.sent || Date.now(), - id: seal?.id, + timestamp: typeof essay?.sent === "number" ? essay.sent : Date.now(), + id: typeof seal?.id === "string" ? seal.id : undefined, } as TlonHistoryEntry; }) .filter((msg) => msg.content); @@ -85,7 +100,7 @@ export async function fetchChannelHistory( runtime?.log?.(`[tlon] Extracted ${messages.length} messages from history`); return messages; } catch (error: unknown) { - runtime?.log?.(`[tlon] Error fetching channel history: ${error?.message ?? String(error)}`); + runtime?.log?.(`[tlon] Error fetching channel history: ${formatErrorMessage(error)}`); return []; } } @@ -138,23 +153,37 @@ export async function fetchThreadHistory( let replies: unknown[] = []; if (Array.isArray(data)) { replies = data; - } else if (data.replies && Array.isArray(data.replies)) { - replies = data.replies; - } else if (typeof data === "object") { - replies = Object.values(data); + } else { + const dataRecord = asRecord(data); + const replyValue = dataRecord?.replies; + if (Array.isArray(replyValue)) { + replies = replyValue; + } else if (typeof replyValue === "object" && replyValue) { + replies = Object.values(replyValue as Record); + } else if (dataRecord) { + replies = Object.values(dataRecord); + } } const messages = replies .map((item) => { // Thread replies use 'memo' structure - const memo = item.memo || item["r-reply"]?.set?.memo || item; - const seal = item.seal || item["r-reply"]?.set?.seal; + const itemRecord = asRecord(item); + const replyRecord = asRecord(itemRecord?.["r-reply"]); + const replySet = asRecord(replyRecord?.set); + const memo = asRecord(itemRecord?.memo) ?? asRecord(replySet?.memo) ?? itemRecord; + const seal = asRecord(itemRecord?.seal) ?? asRecord(replySet?.seal); return { - author: memo?.author || "unknown", + author: typeof memo?.author === "string" ? memo.author : "unknown", content: extractMessageText(memo?.content || []), - timestamp: memo?.sent || Date.now(), - id: seal?.id || item.id, + timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(), + id: + typeof seal?.id === "string" + ? seal.id + : typeof itemRecord?.id === "string" + ? itemRecord.id + : undefined, } as TlonHistoryEntry; }) .filter((msg) => msg.content); @@ -162,29 +191,39 @@ export async function fetchThreadHistory( runtime?.log?.(`[tlon] Extracted ${messages.length} thread replies from history`); return messages; } catch (error: unknown) { - runtime?.log?.(`[tlon] Error fetching thread history: ${error?.message ?? String(error)}`); + runtime?.log?.(`[tlon] Error fetching thread history: ${formatErrorMessage(error)}`); // Fall back to trying alternate path structure try { const altPath = `/channels/v4/${channelNest}/posts/post/id/${formatUd(parentId)}.json`; runtime?.log?.(`[tlon] Trying alternate path: ${altPath}`); - const data: unknown = await api.scry(altPath); + const data = asRecord(await api.scry(altPath)); + const dataSeal = asRecord(data?.seal); + const dataMeta = asRecord(dataSeal?.meta); + const repliesValue = data?.replies; - if (data?.seal?.meta?.replyCount > 0 && data?.replies) { - const replies = Array.isArray(data.replies) ? data.replies : Object.values(data.replies); + if (typeof dataMeta?.replyCount === "number" && dataMeta.replyCount > 0 && repliesValue) { + const replies = Array.isArray(repliesValue) + ? repliesValue + : Object.values(repliesValue as Record); const messages = replies - .map((reply: unknown) => ({ - author: reply.memo?.author || "unknown", - content: extractMessageText(reply.memo?.content || []), - timestamp: reply.memo?.sent || Date.now(), - id: reply.seal?.id, - })) + .map((reply: unknown) => { + const replyRecord = asRecord(reply); + const memo = asRecord(replyRecord?.memo); + const seal = asRecord(replyRecord?.seal); + return { + author: typeof memo?.author === "string" ? memo.author : "unknown", + content: extractMessageText(memo?.content || []), + timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(), + id: typeof seal?.id === "string" ? seal.id : undefined, + }; + }) .filter((msg: TlonHistoryEntry) => msg.content); runtime?.log?.(`[tlon] Extracted ${messages.length} replies from post data`); return messages; } } catch (altError: unknown) { - runtime?.log?.(`[tlon] Alternate path also failed: ${altError?.message ?? String(altError)}`); + runtime?.log?.(`[tlon] Alternate path also failed: ${formatErrorMessage(altError)}`); } return []; } diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index 67edff5ab6e..5b4e03753fa 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -45,6 +45,10 @@ export type MonitorTlonOpts = { accountId?: string | null; }; +function formatErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { const core = getTlonRuntime(); const cfg = core.config.loadConfig(); @@ -89,7 +93,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise= maxAttempts) { throw error; @@ -169,7 +173,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { @@ -1201,9 +1199,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { @@ -1335,7 +1331,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise( } type ProgramCommandRegistrar = (program: Command) => Promise | void; +type AnyImportedProgramCommandGroupDefinition = { + commandNames: readonly string[]; + loadModule: () => Promise>; + exportName: string; +}; export type ImportedProgramCommandGroupDefinition< TModule extends Record, @@ -108,10 +113,17 @@ export function defineImportedProgramCommandGroupSpec< } export function defineImportedProgramCommandGroupSpecs< - TModule extends Record, - TKey extends keyof TModule & string, ->( - definitions: readonly ImportedProgramCommandGroupDefinition[], -): CommandGroupDescriptorSpec<(program: Command) => Promise>[] { - return definitions.map((definition) => defineImportedProgramCommandGroupSpec(definition)); + const TDefinitions extends readonly AnyImportedProgramCommandGroupDefinition[], +>(definitions: TDefinitions): CommandGroupDescriptorSpec<(program: Command) => Promise>[] { + return definitions.map((definition) => ({ + commandNames: definition.commandNames, + register: async (program: Command) => { + const module = await definition.loadModule(); + const register = module[definition.exportName]; + if (typeof register !== "function") { + throw new Error(`Missing program command registrar: ${definition.exportName}`); + } + await register(program); + }, + })); }