fix: restore check gate

This commit is contained in:
Peter Steinberger
2026-04-06 15:12:00 +01:00
parent 979c81d9dd
commit 8d095147b4
18 changed files with 161 additions and 69 deletions

View File

@@ -7,6 +7,7 @@ import {
decodeAcpxRuntimeHandleState,
encodeAcpxRuntimeHandleState,
type AcpAgentRegistry,
type AcpSessionRecord,
type AcpRuntimeDoctorReport,
type AcpRuntimeEvent,
type AcpRuntimeHandle,

View File

@@ -118,7 +118,7 @@ function sanitizeRecentModels(models: string[] | undefined, limit: number): stri
async function readPreferencesStore(filePath: string): Promise<ModelPickerPreferencesStore> {
const { value } = await readJsonFileWithFallback(filePath, {
version: 1,
entries: {},
entries: {} as Record<string, ModelPickerPreferencesEntry>,
});
if (!value || typeof value !== "object" || value.version !== 1) {
return { version: 1, entries: {} };

View File

@@ -770,12 +770,9 @@ describe("discord component interactions", () => {
const acknowledge = vi.fn().mockResolvedValue(undefined);
const reply = vi.fn().mockResolvedValue(undefined);
const update = vi.fn().mockResolvedValue(undefined);
const baseInteraction = createComponentButtonInteraction().interaction as unknown as Record<
string,
unknown
>;
const baseInteraction = createComponentButtonInteraction().interaction;
const interaction = {
...baseInteraction,
...(baseInteraction as object),
acknowledge,
reply,
update,
@@ -825,12 +822,9 @@ describe("discord component interactions", () => {
const button = createDiscordComponentButton(createComponentContext());
const acknowledge = vi.fn().mockResolvedValue(undefined);
const followUp = vi.fn().mockResolvedValue(undefined);
const baseInteraction = createComponentButtonInteraction().interaction as unknown as Record<
string,
unknown
>;
const baseInteraction = createComponentButtonInteraction().interaction;
const interaction = {
...baseInteraction,
...(baseInteraction as object),
acknowledge,
followUp,
} as unknown as ButtonInteraction;

View File

@@ -43,7 +43,10 @@ const defaultOptions = {
maxQueueSize: 1000,
runtimeProfile: "persistent",
scheduler: {},
} satisfies Omit<ProxyRequestClientOptions, "fetch">;
} satisfies Omit<ProxyRequestClientOptions, "fetch"> & {
runtimeProfile: string;
scheduler: object;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;

View File

@@ -17,6 +17,7 @@ describe("duckduckgo web search provider", () => {
({ createDuckDuckGoWebSearchProvider } = await import("./ddg-search-provider.js"));
({ __testing: ddgClientTesting } =
await vi.importActual<typeof import("./ddg-client.js")>("./ddg-client.js"));
await import("../index.js");
});
beforeEach(() => {

View File

@@ -1,3 +1,4 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
const {
@@ -109,7 +110,7 @@ async function runAction(params: {
params: params.params ?? {},
mediaLocalRoots: params.mediaLocalRoots,
toolContext: params.toolContext,
} as unknown);
} as Parameters<ReturnType<typeof requireMSTeamsHandleAction>>[0]);
}
async function expectActionError(
@@ -226,7 +227,7 @@ describe("msteamsPlugin message actions", () => {
tenantId: "tenant-id",
},
},
} as unknown,
} as OpenClawConfig,
})?.actions,
).toContain("upload-file");
});

View File

@@ -132,7 +132,7 @@ export async function handleStructuredPayload(
replyText: string,
recordActivity: () => void,
): Promise<boolean> {
const { _target, account, _cfg, log } = ctx;
const { account, log } = ctx;
const payloadResult = parseQQBotPayload(replyText);
if (!payloadResult.isPayload) {
@@ -195,14 +195,7 @@ export async function handleStructuredPayload(
return true;
}
const payloadType =
parsedPayload &&
typeof parsedPayload === "object" &&
"type" in parsedPayload &&
typeof parsedPayload.type === "string"
? parsedPayload.type
: "unknown";
log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${payloadType}`);
log?.error(`[qqbot:${account.accountId}] Unknown payload type in structured payload`);
return true;
}

View File

@@ -14,8 +14,42 @@ export interface STTConfig {
model: string;
}
type QQBotSttProviderConfig = {
baseUrl?: string;
apiKey?: string;
};
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[];
};
};
};
};
export function resolveSTTConfig(cfg: Record<string, unknown>): STTConfig | null {
const c = cfg as unknown;
const c = cfg as QQBotSttConfigRoot;
// Prefer plugin-specific STT config.
const channelStt = c?.channels?.qqbot?.stt;

View File

@@ -44,6 +44,10 @@ 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 fetchJson(url: string, timeoutMs: number): Promise<unknown> {
return new Promise((resolve, reject) => {
const req = https.get(
@@ -80,12 +84,18 @@ async function fetchDistTags(): Promise<Record<string, string>> {
for (const url of REGISTRIES) {
try {
const json = await fetchJson(url, 10_000);
const tags = json["dist-tags"];
if (tags && typeof tags === "object") {
return tags;
const tags = isRecord(json) ? json["dist-tags"] : undefined;
if (isRecord(tags)) {
return Object.fromEntries(
Object.entries(tags).filter((entry): entry is [string, string] => {
return typeof entry[1] === "string";
}),
);
}
} catch (e: unknown) {
_log?.debug?.(`[qqbot:update-checker] ${url} failed: ${e.message}`);
_log?.debug?.(
`[qqbot:update-checker] ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
);
}
}
throw new Error("all registries failed");
@@ -141,7 +151,8 @@ export async function getUpdateInfo(): Promise<UpdateInfo> {
const tags = await fetchDistTags();
return buildUpdateInfo(tags);
} catch (err: unknown) {
_log?.debug?.(`[qqbot:update-checker] check failed: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
_log?.debug?.(`[qqbot:update-checker] check failed: ${message}`);
return {
current: CURRENT_VERSION,
latest: null,
@@ -149,7 +160,7 @@ export async function getUpdateInfo(): Promise<UpdateInfo> {
alpha: null,
hasUpdate: false,
checkedAt: Date.now(),
error: err.message,
error: message,
};
}
}
@@ -162,7 +173,7 @@ export async function checkVersionExists(version: string): Promise<boolean> {
try {
const url = `${baseUrl}/${version}`;
const json = await fetchJson(url, 10_000);
if (json && json.version === version) {
if (isRecord(json) && json.version === version) {
return true;
}
} catch {

View File

@@ -194,9 +194,42 @@ export interface TTSConfig {
speed?: number;
}
type QQBotTtsProviderConfig = {
baseUrl?: string;
apiKey?: string;
authStyle?: string;
queryParams?: Record<string, string>;
};
type QQBotTtsBlock = QQBotTtsProviderConfig & {
model?: string;
voice?: string;
speed?: number;
};
type QQBotMessagesTtsConfig = {
auto?: string;
enabled?: boolean;
provider?: string;
} & Record<string, unknown>;
type QQBotTtsConfigRoot = {
channels?: {
qqbot?: {
tts?: QQBotTtsBlock & { enabled?: boolean; provider?: string };
};
};
models?: {
providers?: Record<string, QQBotTtsProviderConfig>;
};
messages?: {
tts?: QQBotMessagesTtsConfig;
};
};
function resolveTTSFromBlock(
block: Record<string, unknown>,
providerCfg: Record<string, unknown> | undefined,
block: QQBotTtsBlock,
providerCfg: QQBotTtsProviderConfig | undefined,
): TTSConfig | null {
const baseUrl: string | undefined = block?.baseUrl || providerCfg?.baseUrl;
const apiKey: string | undefined = block?.apiKey || providerCfg?.apiKey;
@@ -228,7 +261,7 @@ function resolveTTSFromBlock(
}
export function resolveTTSConfig(cfg: Record<string, unknown>): TTSConfig | null {
const c = cfg as unknown;
const c = cfg as QQBotTtsConfigRoot;
// Prefer plugin-specific TTS config first.
const channelTts = c?.channels?.qqbot?.tts;
@@ -245,7 +278,7 @@ export function resolveTTSConfig(cfg: Record<string, unknown>): TTSConfig | null
const msgTts = c?.messages?.tts;
if (msgTts && msgTts.auto !== "off" && msgTts.auto !== "disabled") {
const providerId: string = msgTts?.provider || "openai";
const providerBlock = msgTts?.[providerId];
const providerBlock = msgTts?.[providerId] as QQBotTtsBlock | undefined;
const providerCfg = c?.models?.providers?.[providerId];
const result = resolveTTSFromBlock(providerBlock ?? {}, providerCfg);
if (result) {

View File

@@ -18,7 +18,7 @@ const { waitForTransportReadyMock, spawnSignalDaemonMock, streamMock } =
getSignalToolResultTestMocks();
const SIGNAL_BASE_URL = "http://127.0.0.1:8080";
type MonitorSignalProviderOptions = Parameters<typeof monitorSignalProvider>[0];
type MonitorSignalProviderOptions = NonNullable<Parameters<typeof monitorSignalProvider>[0]>;
function createMonitorRuntime() {
return {

View File

@@ -28,7 +28,7 @@ const {
} = getSignalToolResultTestMocks();
const SIGNAL_BASE_URL = "http://127.0.0.1:8080";
type MonitorSignalProviderOptions = Parameters<typeof monitorSignalProvider>[0];
type MonitorSignalProviderOptions = NonNullable<Parameters<typeof monitorSignalProvider>[0]>;
async function runMonitorWithMocks(opts: MonitorSignalProviderOptions) {
return monitorSignalProvider({

View File

@@ -1,4 +1,5 @@
import { EventEmitter } from "node:events";
import type { ClientRequest, IncomingMessage, RequestOptions } from "node:http";
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
// Mock http and https modules before importing the client
@@ -21,6 +22,27 @@ let sendFileUrl: typeof import("./client.js").sendFileUrl;
let fetchChatUsers: typeof import("./client.js").fetchChatUsers;
let resolveLegacyWebhookNameToChatUserId: typeof import("./client.js").resolveLegacyWebhookNameToChatUserId;
type RequestCallback = (res: IncomingMessage) => void;
type MockRequestHandler = (
url: string | URL,
options: RequestOptions,
callback?: RequestCallback,
) => ClientRequest;
function createMockResponseEmitter(statusCode: number): IncomingMessage {
const res = new EventEmitter() as Partial<IncomingMessage>;
res.statusCode = statusCode;
return res as IncomingMessage;
}
function createMockRequestEmitter(): ClientRequest {
const req = new EventEmitter() as Partial<ClientRequest>;
req.write = vi.fn() as ClientRequest["write"];
req.end = vi.fn() as ClientRequest["end"];
req.destroy = vi.fn() as ClientRequest["destroy"];
return req as ClientRequest;
}
async function settleTimers<T>(promise: Promise<T>): Promise<T> {
await Promise.resolve();
await vi.runAllTimersAsync();
@@ -29,20 +51,16 @@ async function settleTimers<T>(promise: Promise<T>): Promise<T> {
function mockResponse(statusCode: number, body: string) {
const httpsRequest = vi.mocked(https.request);
httpsRequest.mockImplementation((_url: unknown, _opts: unknown, callback: unknown) => {
const res = new EventEmitter() as unknown;
res.statusCode = statusCode;
httpsRequest.mockImplementation(((...args) => {
const callback = args[2];
const res = createMockResponseEmitter(statusCode);
process.nextTick(() => {
callback(res);
callback?.(res);
res.emit("data", Buffer.from(body));
res.emit("end");
});
const req = new EventEmitter() as unknown;
req.write = vi.fn();
req.end = vi.fn();
req.destroy = vi.fn();
return req;
});
return createMockRequestEmitter();
}) as MockRequestHandler);
}
function mockSuccessResponse() {
@@ -156,18 +174,15 @@ function mockUserListResponseImpl(
users: Array<{ user_id: number; username: string; nickname: string }>,
once: boolean,
) {
const httpsGet = vi.mocked((https as unknown).get);
const impl = (_url: unknown, _opts: unknown, callback: unknown) => {
const res = new EventEmitter() as unknown;
res.statusCode = 200;
const httpsGet = vi.mocked(https.get);
const impl: MockRequestHandler = (_url, _opts, callback) => {
const res = createMockResponseEmitter(200);
process.nextTick(() => {
callback(res);
callback?.(res);
res.emit("data", Buffer.from(JSON.stringify({ success: true, data: { users } })));
res.emit("end");
});
const req = new EventEmitter() as unknown;
req.destroy = vi.fn();
return req;
return createMockRequestEmitter();
};
if (once) {
httpsGet.mockImplementationOnce(impl);
@@ -251,7 +266,7 @@ describe("resolveLegacyWebhookNameToChatUserId", () => {
incomingUrl: baseUrl,
mutableWebhookUsername: "anyone",
});
const httpsGet = vi.mocked((https as unknown).get);
const httpsGet = vi.mocked(https.get);
expect(httpsGet).toHaveBeenCalledWith(
expect.stringContaining("method=user_list"),
expect.any(Object),
@@ -274,7 +289,7 @@ describe("resolveLegacyWebhookNameToChatUserId", () => {
expect(result1).toBe(4);
expect(result2).toBe(9);
const httpsGet = vi.mocked((https as unknown).get);
const httpsGet = vi.mocked(https.get);
expect(httpsGet).toHaveBeenCalledTimes(2);
});
});
@@ -283,12 +298,11 @@ describe("fetchChatUsers", () => {
installFakeTimerHarness();
it("filters malformed user entries while keeping valid ones", async () => {
const httpsGet = vi.mocked((https as unknown).get);
httpsGet.mockImplementation((_url: unknown, _opts: unknown, callback: unknown) => {
const res = new EventEmitter() as unknown;
res.statusCode = 200;
const httpsGet = vi.mocked(https.get);
httpsGet.mockImplementation(((_url, _opts, callback) => {
const res = createMockResponseEmitter(200);
process.nextTick(() => {
callback(res);
callback?.(res);
res.emit(
"data",
Buffer.from(
@@ -305,10 +319,8 @@ describe("fetchChatUsers", () => {
);
res.emit("end");
});
const req = new EventEmitter() as unknown;
req.destroy = vi.fn();
return req;
});
return createMockRequestEmitter();
}) as MockRequestHandler);
const users = await fetchChatUsers(
"https://nas.example.com/webapi/entry.cgi?api=SYNO.Chat.External&method=chatbot&version=2&token=%22test%22",
@@ -324,7 +336,7 @@ describe("fetchChatUsers", () => {
await fetchChatUsers(freshUrl);
const httpsGet = vi.mocked((https as unknown).get);
const httpsGet = vi.mocked(https.get);
expect(httpsGet.mock.calls[0]?.[1]).toMatchObject({ rejectUnauthorized: true });
});
});

View File

@@ -167,9 +167,11 @@ export async function fetchChatUsers(
return;
}
const transport = parsedUrl.protocol === "https:" ? https : http;
const requestOptions: http.RequestOptions | https.RequestOptions =
parsedUrl.protocol === "https:" ? { rejectUnauthorized: !allowInsecureSsl } : {};
transport
.get(listUrl, { rejectUnauthorized: !allowInsecureSsl } as unknown, (res) => {
.get(listUrl, requestOptions, (res) => {
let data = "";
res.on("data", (c: Buffer) => {
data += c.toString();

View File

@@ -1,3 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "./api.js";
import register from "./index.js";

View File

@@ -248,6 +248,7 @@ export function createXaiWebSearchProvider(): WebSearchProviderPlugin {
),
inlineCitations: resolveXaiInlineCitations(searchConfig),
cacheTtlMs: resolveCacheTtlMs(searchConfig?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
cacheTtlMs: resolveCacheTtlMs(searchConfig?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
});
},
};

View File

@@ -617,7 +617,10 @@ function buildMessageToolDescription(options?: {
desc += ` Other configured channels: ${otherChannels.join(", ")}.`;
}
return appendMessageToolReadHint(desc, allActions);
return appendMessageToolReadHint(
desc,
Array.from(allActions) as Iterable<ChannelMessageActionName | "send">,
);
}
}

View File

@@ -21,6 +21,8 @@
"openclaw/extension-api": ["./src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"],
"openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"],
"@openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"],
"@openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"],
"openclaw/plugin-sdk/account-id": ["./src/plugin-sdk/account-id.ts"],
"@openclaw/*": ["./extensions/*"]
}