fix(check): repair latest type drift batch

This commit is contained in:
Peter Steinberger
2026-04-06 15:03:49 +01:00
parent 380a396266
commit 732c18cd06
10 changed files with 152 additions and 83 deletions

View File

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

View File

@@ -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> = {},
): 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();

View File

@@ -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 () => {

View File

@@ -5,6 +5,10 @@ type TlonScryApi = {
scry: (path: string) => Promise<unknown>;
};
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : 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;

View File

@@ -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<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
function formatErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export async function fetchGroupChanges(
api: { scry: (path: string) => Promise<unknown> },
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<InitData> {
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<string, unknown>)) {
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 };
}
}

View File

@@ -1,6 +1,14 @@
import type { RuntimeEnv } from "../../api.js";
import { extractMessageText } from "./utils.js";
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : 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<string, unknown>);
} 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<string, unknown>);
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 [];
}

View File

@@ -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<void> {
const core = getTlonRuntime();
const cfg = core.config.loadConfig();
@@ -89,7 +93,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
return await authenticate(accountUrl, accountCode, { ssrfPolicy });
} catch (error: unknown) {
runtime.error?.(
`[tlon] Failed to authenticate (attempt ${attempt}): ${error?.message ?? String(error)}`,
`[tlon] Failed to authenticate (attempt ${attempt}): ${formatErrorMessage(error)}`,
);
if (attempt >= maxAttempts) {
throw error;
@@ -169,7 +173,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
}
} catch (error: unknown) {
runtime.log?.(`[tlon] Could not fetch nickname: ${error?.message ?? String(error)}`);
runtime.log?.(`[tlon] Could not fetch nickname: ${formatErrorMessage(error)}`);
}
// Store init foreigns for processing after settings are loaded
@@ -236,7 +240,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
initForeigns = initData.foreigns;
} catch (error: unknown) {
runtime.error?.(`[tlon] Auto-discovery failed: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Auto-discovery failed: ${formatErrorMessage(error)}`);
}
}
@@ -302,8 +306,8 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
senderShip,
isGroup,
channelNest,
_hostShip,
_channelName,
hostShip: _hostShip,
channelName: _channelName,
timestamp,
parentId,
isThreadReply,
@@ -321,7 +325,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
runtime.log?.(`[tlon] Downloaded ${attachments.length} image(s) from message`);
}
} catch (error: unknown) {
runtime.log?.(`[tlon] Failed to download images: ${error?.message ?? String(error)}`);
runtime.log?.(`[tlon] Failed to download images: ${formatErrorMessage(error)}`);
}
}
@@ -344,7 +348,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
);
}
} catch (error: unknown) {
runtime?.log?.(`[tlon] Could not fetch thread context: ${error?.message ?? String(error)}`);
runtime?.log?.(`[tlon] Could not fetch thread context: ${formatErrorMessage(error)}`);
// Continue without thread context - not critical
}
}
@@ -391,7 +395,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
"3. Action items if any\n" +
"4. Notable participants";
} catch (error: unknown) {
const errorMsg = `Sorry, I encountered an error while fetching the channel history: ${error?.message ?? String(error)}`;
const errorMsg = `Sorry, I encountered an error while fetching the channel history: ${formatErrorMessage(error)}`;
if (isGroup && groupChannel) {
const parsed = parseChannelNest(groupChannel);
if (parsed) {
@@ -807,9 +811,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
isThreadReply,
});
} catch (error: any) {
runtime.error?.(
`[tlon] Error handling channel firehose event: ${error?.message ?? String(error)}`,
);
runtime.error?.(`[tlon] Error handling channel firehose event: ${formatErrorMessage(error)}`);
}
};
@@ -989,9 +991,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
timestamp: essay.sent || Date.now(),
});
} catch (error: any) {
runtime.error?.(
`[tlon] Error handling chat firehose event: ${error?.message ?? String(error)}`,
);
runtime.error?.(`[tlon] Error handling chat firehose event: ${formatErrorMessage(error)}`);
}
};
@@ -1044,9 +1044,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
}
} catch (error: any) {
runtime.error?.(
`[tlon] Error handling contacts event: ${error?.message ?? String(error)}`,
);
runtime.error?.(`[tlon] Error handling contacts event: ${formatErrorMessage(error)}`);
}
},
err: (error) => {
@@ -1201,9 +1199,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
}
} catch (error: any) {
runtime.error?.(
`[tlon] Error handling groups-ui event: ${error?.message ?? String(error)}`,
);
runtime.error?.(`[tlon] Error handling groups-ui event: ${formatErrorMessage(error)}`);
}
},
err: (error) => {
@@ -1335,7 +1331,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
await processPendingInvites(data as Foreigns);
} catch (error: any) {
runtime.error?.(
`[tlon] Error handling foreigns event: ${error?.message ?? String(error)}`,
`[tlon] Error handling foreigns event: ${formatErrorMessage(error)}`,
);
}
})();
@@ -1388,7 +1384,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
}
} catch (error: any) {
runtime.error?.(`[tlon] Channel refresh error: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Channel refresh error: ${formatErrorMessage(error)}`);
}
}
},
@@ -1414,7 +1410,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
try {
await api?.close();
} catch (error: any) {
runtime.error?.(`[tlon] Cleanup error: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Cleanup error: ${formatErrorMessage(error)}`);
}
}
}

View File

@@ -242,7 +242,7 @@ export function markdownToStory(markdown: string): Story {
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headerMatch) {
const level = headerMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6;
const tag = `h${level}`;
const tag = `h${level}` as const;
story.push({
block: {
header: {

View File

@@ -432,7 +432,6 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr
config,
runtime,
core,
_text,
mediaPath,
mediaType,
statusSink,

View File

@@ -84,6 +84,11 @@ export function defineImportedCommandGroupSpecs<TRegisterArgs, TModule>(
}
type ProgramCommandRegistrar = (program: Command) => Promise<void> | void;
type AnyImportedProgramCommandGroupDefinition = {
commandNames: readonly string[];
loadModule: () => Promise<Record<string, unknown>>;
exportName: string;
};
export type ImportedProgramCommandGroupDefinition<
TModule extends Record<TKey, ProgramCommandRegistrar>,
@@ -108,10 +113,17 @@ export function defineImportedProgramCommandGroupSpec<
}
export function defineImportedProgramCommandGroupSpecs<
TModule extends Record<TKey, ProgramCommandRegistrar>,
TKey extends keyof TModule & string,
>(
definitions: readonly ImportedProgramCommandGroupDefinition<TModule, TKey>[],
): CommandGroupDescriptorSpec<(program: Command) => Promise<void>>[] {
return definitions.map((definition) => defineImportedProgramCommandGroupSpec(definition));
const TDefinitions extends readonly AnyImportedProgramCommandGroupDefinition[],
>(definitions: TDefinitions): CommandGroupDescriptorSpec<(program: Command) => Promise<void>>[] {
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);
},
}));
}