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
+1
View File
@@ -7,6 +7,7 @@ import {
decodeAcpxRuntimeHandleState, decodeAcpxRuntimeHandleState,
encodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState,
type AcpAgentRegistry, type AcpAgentRegistry,
type AcpSessionRecord,
type AcpRuntimeDoctorReport, type AcpRuntimeDoctorReport,
type AcpRuntimeEvent, type AcpRuntimeEvent,
type AcpRuntimeHandle, type AcpRuntimeHandle,
@@ -118,7 +118,7 @@ function sanitizeRecentModels(models: string[] | undefined, limit: number): stri
async function readPreferencesStore(filePath: string): Promise<ModelPickerPreferencesStore> { async function readPreferencesStore(filePath: string): Promise<ModelPickerPreferencesStore> {
const { value } = await readJsonFileWithFallback(filePath, { const { value } = await readJsonFileWithFallback(filePath, {
version: 1, version: 1,
entries: {}, entries: {} as Record<string, ModelPickerPreferencesEntry>,
}); });
if (!value || typeof value !== "object" || value.version !== 1) { if (!value || typeof value !== "object" || value.version !== 1) {
return { version: 1, entries: {} }; return { version: 1, entries: {} };
+4 -10
View File
@@ -770,12 +770,9 @@ describe("discord component interactions", () => {
const acknowledge = vi.fn().mockResolvedValue(undefined); const acknowledge = vi.fn().mockResolvedValue(undefined);
const reply = vi.fn().mockResolvedValue(undefined); const reply = vi.fn().mockResolvedValue(undefined);
const update = vi.fn().mockResolvedValue(undefined); const update = vi.fn().mockResolvedValue(undefined);
const baseInteraction = createComponentButtonInteraction().interaction as unknown as Record< const baseInteraction = createComponentButtonInteraction().interaction;
string,
unknown
>;
const interaction = { const interaction = {
...baseInteraction, ...(baseInteraction as object),
acknowledge, acknowledge,
reply, reply,
update, update,
@@ -825,12 +822,9 @@ describe("discord component interactions", () => {
const button = createDiscordComponentButton(createComponentContext()); const button = createDiscordComponentButton(createComponentContext());
const acknowledge = vi.fn().mockResolvedValue(undefined); const acknowledge = vi.fn().mockResolvedValue(undefined);
const followUp = vi.fn().mockResolvedValue(undefined); const followUp = vi.fn().mockResolvedValue(undefined);
const baseInteraction = createComponentButtonInteraction().interaction as unknown as Record< const baseInteraction = createComponentButtonInteraction().interaction;
string,
unknown
>;
const interaction = { const interaction = {
...baseInteraction, ...(baseInteraction as object),
acknowledge, acknowledge,
followUp, followUp,
} as unknown as ButtonInteraction; } as unknown as ButtonInteraction;
@@ -43,7 +43,10 @@ const defaultOptions = {
maxQueueSize: 1000, maxQueueSize: 1000,
runtimeProfile: "persistent", runtimeProfile: "persistent",
scheduler: {}, scheduler: {},
} satisfies Omit<ProxyRequestClientOptions, "fetch">; } satisfies Omit<ProxyRequestClientOptions, "fetch"> & {
runtimeProfile: string;
scheduler: object;
};
function isRecord(value: unknown): value is Record<string, unknown> { function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null; return typeof value === "object" && value !== null;
@@ -17,6 +17,7 @@ describe("duckduckgo web search provider", () => {
({ createDuckDuckGoWebSearchProvider } = await import("./ddg-search-provider.js")); ({ createDuckDuckGoWebSearchProvider } = await import("./ddg-search-provider.js"));
({ __testing: ddgClientTesting } = ({ __testing: ddgClientTesting } =
await vi.importActual<typeof import("./ddg-client.js")>("./ddg-client.js")); await vi.importActual<typeof import("./ddg-client.js")>("./ddg-client.js"));
await import("../index.js");
}); });
beforeEach(() => { beforeEach(() => {
@@ -1,3 +1,4 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const { const {
@@ -109,7 +110,7 @@ async function runAction(params: {
params: params.params ?? {}, params: params.params ?? {},
mediaLocalRoots: params.mediaLocalRoots, mediaLocalRoots: params.mediaLocalRoots,
toolContext: params.toolContext, toolContext: params.toolContext,
} as unknown); } as Parameters<ReturnType<typeof requireMSTeamsHandleAction>>[0]);
} }
async function expectActionError( async function expectActionError(
@@ -226,7 +227,7 @@ describe("msteamsPlugin message actions", () => {
tenantId: "tenant-id", tenantId: "tenant-id",
}, },
}, },
} as unknown, } as OpenClawConfig,
})?.actions, })?.actions,
).toContain("upload-file"); ).toContain("upload-file");
}); });
+2 -9
View File
@@ -132,7 +132,7 @@ export async function handleStructuredPayload(
replyText: string, replyText: string,
recordActivity: () => void, recordActivity: () => void,
): Promise<boolean> { ): Promise<boolean> {
const { _target, account, _cfg, log } = ctx; const { account, log } = ctx;
const payloadResult = parseQQBotPayload(replyText); const payloadResult = parseQQBotPayload(replyText);
if (!payloadResult.isPayload) { if (!payloadResult.isPayload) {
@@ -195,14 +195,7 @@ export async function handleStructuredPayload(
return true; return true;
} }
const payloadType = log?.error(`[qqbot:${account.accountId}] Unknown payload type in structured payload`);
parsedPayload &&
typeof parsedPayload === "object" &&
"type" in parsedPayload &&
typeof parsedPayload.type === "string"
? parsedPayload.type
: "unknown";
log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${payloadType}`);
return true; return true;
} }
+35 -1
View File
@@ -14,8 +14,42 @@ export interface STTConfig {
model: string; 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 { export function resolveSTTConfig(cfg: Record<string, unknown>): STTConfig | null {
const c = cfg as unknown; const c = cfg as QQBotSttConfigRoot;
// Prefer plugin-specific STT config. // Prefer plugin-specific STT config.
const channelStt = c?.channels?.qqbot?.stt; const channelStt = c?.channels?.qqbot?.stt;
+18 -7
View File
@@ -44,6 +44,10 @@ let _log:
| { info: (msg: string) => void; error: (msg: string) => void; debug?: (msg: string) => void } | { info: (msg: string) => void; error: (msg: string) => void; debug?: (msg: string) => void }
| undefined; | undefined;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function fetchJson(url: string, timeoutMs: number): Promise<unknown> { function fetchJson(url: string, timeoutMs: number): Promise<unknown> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = https.get( const req = https.get(
@@ -80,12 +84,18 @@ async function fetchDistTags(): Promise<Record<string, string>> {
for (const url of REGISTRIES) { for (const url of REGISTRIES) {
try { try {
const json = await fetchJson(url, 10_000); const json = await fetchJson(url, 10_000);
const tags = json["dist-tags"]; const tags = isRecord(json) ? json["dist-tags"] : undefined;
if (tags && typeof tags === "object") { if (isRecord(tags)) {
return tags; return Object.fromEntries(
Object.entries(tags).filter((entry): entry is [string, string] => {
return typeof entry[1] === "string";
}),
);
} }
} catch (e: unknown) { } 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"); throw new Error("all registries failed");
@@ -141,7 +151,8 @@ export async function getUpdateInfo(): Promise<UpdateInfo> {
const tags = await fetchDistTags(); const tags = await fetchDistTags();
return buildUpdateInfo(tags); return buildUpdateInfo(tags);
} catch (err: unknown) { } 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 { return {
current: CURRENT_VERSION, current: CURRENT_VERSION,
latest: null, latest: null,
@@ -149,7 +160,7 @@ export async function getUpdateInfo(): Promise<UpdateInfo> {
alpha: null, alpha: null,
hasUpdate: false, hasUpdate: false,
checkedAt: Date.now(), checkedAt: Date.now(),
error: err.message, error: message,
}; };
} }
} }
@@ -162,7 +173,7 @@ export async function checkVersionExists(version: string): Promise<boolean> {
try { try {
const url = `${baseUrl}/${version}`; const url = `${baseUrl}/${version}`;
const json = await fetchJson(url, 10_000); const json = await fetchJson(url, 10_000);
if (json && json.version === version) { if (isRecord(json) && json.version === version) {
return true; return true;
} }
} catch { } catch {
+37 -4
View File
@@ -194,9 +194,42 @@ export interface TTSConfig {
speed?: number; 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( function resolveTTSFromBlock(
block: Record<string, unknown>, block: QQBotTtsBlock,
providerCfg: Record<string, unknown> | undefined, providerCfg: QQBotTtsProviderConfig | undefined,
): TTSConfig | null { ): TTSConfig | null {
const baseUrl: string | undefined = block?.baseUrl || providerCfg?.baseUrl; const baseUrl: string | undefined = block?.baseUrl || providerCfg?.baseUrl;
const apiKey: string | undefined = block?.apiKey || providerCfg?.apiKey; const apiKey: string | undefined = block?.apiKey || providerCfg?.apiKey;
@@ -228,7 +261,7 @@ function resolveTTSFromBlock(
} }
export function resolveTTSConfig(cfg: Record<string, unknown>): TTSConfig | null { 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. // Prefer plugin-specific TTS config first.
const channelTts = c?.channels?.qqbot?.tts; const channelTts = c?.channels?.qqbot?.tts;
@@ -245,7 +278,7 @@ export function resolveTTSConfig(cfg: Record<string, unknown>): TTSConfig | null
const msgTts = c?.messages?.tts; const msgTts = c?.messages?.tts;
if (msgTts && msgTts.auto !== "off" && msgTts.auto !== "disabled") { if (msgTts && msgTts.auto !== "off" && msgTts.auto !== "disabled") {
const providerId: string = msgTts?.provider || "openai"; const providerId: string = msgTts?.provider || "openai";
const providerBlock = msgTts?.[providerId]; const providerBlock = msgTts?.[providerId] as QQBotTtsBlock | undefined;
const providerCfg = c?.models?.providers?.[providerId]; const providerCfg = c?.models?.providers?.[providerId];
const result = resolveTTSFromBlock(providerBlock ?? {}, providerCfg); const result = resolveTTSFromBlock(providerBlock ?? {}, providerCfg);
if (result) { if (result) {
@@ -18,7 +18,7 @@ const { waitForTransportReadyMock, spawnSignalDaemonMock, streamMock } =
getSignalToolResultTestMocks(); getSignalToolResultTestMocks();
const SIGNAL_BASE_URL = "http://127.0.0.1:8080"; 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() { function createMonitorRuntime() {
return { return {
@@ -28,7 +28,7 @@ const {
} = getSignalToolResultTestMocks(); } = getSignalToolResultTestMocks();
const SIGNAL_BASE_URL = "http://127.0.0.1:8080"; 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) { async function runMonitorWithMocks(opts: MonitorSignalProviderOptions) {
return monitorSignalProvider({ return monitorSignalProvider({
+42 -30
View File
@@ -1,4 +1,5 @@
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import type { ClientRequest, IncomingMessage, RequestOptions } from "node:http";
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
// Mock http and https modules before importing the client // 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 fetchChatUsers: typeof import("./client.js").fetchChatUsers;
let resolveLegacyWebhookNameToChatUserId: typeof import("./client.js").resolveLegacyWebhookNameToChatUserId; 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> { async function settleTimers<T>(promise: Promise<T>): Promise<T> {
await Promise.resolve(); await Promise.resolve();
await vi.runAllTimersAsync(); await vi.runAllTimersAsync();
@@ -29,20 +51,16 @@ async function settleTimers<T>(promise: Promise<T>): Promise<T> {
function mockResponse(statusCode: number, body: string) { function mockResponse(statusCode: number, body: string) {
const httpsRequest = vi.mocked(https.request); const httpsRequest = vi.mocked(https.request);
httpsRequest.mockImplementation((_url: unknown, _opts: unknown, callback: unknown) => { httpsRequest.mockImplementation(((...args) => {
const res = new EventEmitter() as unknown; const callback = args[2];
res.statusCode = statusCode; const res = createMockResponseEmitter(statusCode);
process.nextTick(() => { process.nextTick(() => {
callback(res); callback?.(res);
res.emit("data", Buffer.from(body)); res.emit("data", Buffer.from(body));
res.emit("end"); res.emit("end");
}); });
const req = new EventEmitter() as unknown; return createMockRequestEmitter();
req.write = vi.fn(); }) as MockRequestHandler);
req.end = vi.fn();
req.destroy = vi.fn();
return req;
});
} }
function mockSuccessResponse() { function mockSuccessResponse() {
@@ -156,18 +174,15 @@ function mockUserListResponseImpl(
users: Array<{ user_id: number; username: string; nickname: string }>, users: Array<{ user_id: number; username: string; nickname: string }>,
once: boolean, once: boolean,
) { ) {
const httpsGet = vi.mocked((https as unknown).get); const httpsGet = vi.mocked(https.get);
const impl = (_url: unknown, _opts: unknown, callback: unknown) => { const impl: MockRequestHandler = (_url, _opts, callback) => {
const res = new EventEmitter() as unknown; const res = createMockResponseEmitter(200);
res.statusCode = 200;
process.nextTick(() => { process.nextTick(() => {
callback(res); callback?.(res);
res.emit("data", Buffer.from(JSON.stringify({ success: true, data: { users } }))); res.emit("data", Buffer.from(JSON.stringify({ success: true, data: { users } })));
res.emit("end"); res.emit("end");
}); });
const req = new EventEmitter() as unknown; return createMockRequestEmitter();
req.destroy = vi.fn();
return req;
}; };
if (once) { if (once) {
httpsGet.mockImplementationOnce(impl); httpsGet.mockImplementationOnce(impl);
@@ -251,7 +266,7 @@ describe("resolveLegacyWebhookNameToChatUserId", () => {
incomingUrl: baseUrl, incomingUrl: baseUrl,
mutableWebhookUsername: "anyone", mutableWebhookUsername: "anyone",
}); });
const httpsGet = vi.mocked((https as unknown).get); const httpsGet = vi.mocked(https.get);
expect(httpsGet).toHaveBeenCalledWith( expect(httpsGet).toHaveBeenCalledWith(
expect.stringContaining("method=user_list"), expect.stringContaining("method=user_list"),
expect.any(Object), expect.any(Object),
@@ -274,7 +289,7 @@ describe("resolveLegacyWebhookNameToChatUserId", () => {
expect(result1).toBe(4); expect(result1).toBe(4);
expect(result2).toBe(9); expect(result2).toBe(9);
const httpsGet = vi.mocked((https as unknown).get); const httpsGet = vi.mocked(https.get);
expect(httpsGet).toHaveBeenCalledTimes(2); expect(httpsGet).toHaveBeenCalledTimes(2);
}); });
}); });
@@ -283,12 +298,11 @@ describe("fetchChatUsers", () => {
installFakeTimerHarness(); installFakeTimerHarness();
it("filters malformed user entries while keeping valid ones", async () => { it("filters malformed user entries while keeping valid ones", async () => {
const httpsGet = vi.mocked((https as unknown).get); const httpsGet = vi.mocked(https.get);
httpsGet.mockImplementation((_url: unknown, _opts: unknown, callback: unknown) => { httpsGet.mockImplementation(((_url, _opts, callback) => {
const res = new EventEmitter() as unknown; const res = createMockResponseEmitter(200);
res.statusCode = 200;
process.nextTick(() => { process.nextTick(() => {
callback(res); callback?.(res);
res.emit( res.emit(
"data", "data",
Buffer.from( Buffer.from(
@@ -305,10 +319,8 @@ describe("fetchChatUsers", () => {
); );
res.emit("end"); res.emit("end");
}); });
const req = new EventEmitter() as unknown; return createMockRequestEmitter();
req.destroy = vi.fn(); }) as MockRequestHandler);
return req;
});
const users = await fetchChatUsers( const users = await fetchChatUsers(
"https://nas.example.com/webapi/entry.cgi?api=SYNO.Chat.External&method=chatbot&version=2&token=%22test%22", "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); 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 }); expect(httpsGet.mock.calls[0]?.[1]).toMatchObject({ rejectUnauthorized: true });
}); });
}); });
+3 -1
View File
@@ -167,9 +167,11 @@ export async function fetchChatUsers(
return; return;
} }
const transport = parsedUrl.protocol === "https:" ? https : http; const transport = parsedUrl.protocol === "https:" ? https : http;
const requestOptions: http.RequestOptions | https.RequestOptions =
parsedUrl.protocol === "https:" ? { rejectUnauthorized: !allowInsecureSsl } : {};
transport transport
.get(listUrl, { rejectUnauthorized: !allowInsecureSsl } as unknown, (res) => { .get(listUrl, requestOptions, (res) => {
let data = ""; let data = "";
res.on("data", (c: Buffer) => { res.on("data", (c: Buffer) => {
data += c.toString(); data += c.toString();
@@ -1,3 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "./api.js"; import type { OpenClawPluginApi } from "./api.js";
import register from "./index.js"; import register from "./index.js";
+1
View File
@@ -248,6 +248,7 @@ export function createXaiWebSearchProvider(): WebSearchProviderPlugin {
), ),
inlineCitations: resolveXaiInlineCitations(searchConfig), inlineCitations: resolveXaiInlineCitations(searchConfig),
cacheTtlMs: resolveCacheTtlMs(searchConfig?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES), cacheTtlMs: resolveCacheTtlMs(searchConfig?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
cacheTtlMs: resolveCacheTtlMs(searchConfig?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
}); });
}, },
}; };
+4 -1
View File
@@ -617,7 +617,10 @@ function buildMessageToolDescription(options?: {
desc += ` Other configured channels: ${otherChannels.join(", ")}.`; desc += ` Other configured channels: ${otherChannels.join(", ")}.`;
} }
return appendMessageToolReadHint(desc, allActions); return appendMessageToolReadHint(
desc,
Array.from(allActions) as Iterable<ChannelMessageActionName | "send">,
);
} }
} }
+2
View File
@@ -21,6 +21,8 @@
"openclaw/extension-api": ["./src/extensionAPI.ts"], "openclaw/extension-api": ["./src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"], "openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"],
"openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.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/plugin-sdk/account-id": ["./src/plugin-sdk/account-id.ts"],
"@openclaw/*": ["./extensions/*"] "@openclaw/*": ["./extensions/*"]
} }