From ebb301c32d9e2e1ec61baa783564eb38e4296895 Mon Sep 17 00:00:00 2001 From: wangmiao0668000666 Date: Sun, 2 Aug 2026 04:01:40 +0800 Subject: [PATCH] fix(moonshot): forward abort signal through Kimi web search provider (#105125) --- .../src/kimi-web-search-provider.runtime.ts | 4 ++ .../src/kimi-web-search-provider.test.ts | 57 ++++++++++++------- .../moonshot/src/kimi-web-search-provider.ts | 6 +- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/extensions/moonshot/src/kimi-web-search-provider.runtime.ts b/extensions/moonshot/src/kimi-web-search-provider.runtime.ts index c549064ed4f8..7ab5e266f092 100644 --- a/extensions/moonshot/src/kimi-web-search-provider.runtime.ts +++ b/extensions/moonshot/src/kimi-web-search-provider.runtime.ts @@ -216,6 +216,7 @@ async function runKimiSearch(params: { baseUrl: string; model: string; timeoutSeconds: number; + signal?: AbortSignal; }): Promise { const endpoint = `${params.baseUrl.trim().replace(/\/$/, "")}/chat/completions`; const messages: Array> = [{ role: "user", content: params.query }]; @@ -227,6 +228,7 @@ async function runKimiSearch(params: { { url: endpoint, timeoutSeconds: params.timeoutSeconds, + signal: params.signal, init: { method: "POST", headers: { @@ -341,6 +343,7 @@ async function runKimiSearch(params: { export async function executeKimiWebSearchProviderTool( ctx: { config?: OpenClawConfig; searchConfig?: SearchConfigRecord }, args: Record, + opts?: { signal?: AbortSignal }, ): Promise> { const searchConfig = mergeScopedSearchConfig( ctx.searchConfig, @@ -392,6 +395,7 @@ export async function executeKimiWebSearchProviderTool( baseUrl, model, timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig), + signal: opts?.signal, }); if (!result.grounded) { return { diff --git a/extensions/moonshot/src/kimi-web-search-provider.test.ts b/extensions/moonshot/src/kimi-web-search-provider.test.ts index a082aac0bdcb..f349fbbf9a8a 100644 --- a/extensions/moonshot/src/kimi-web-search-provider.test.ts +++ b/extensions/moonshot/src/kimi-web-search-provider.test.ts @@ -1,31 +1,12 @@ // Moonshot tests cover kimi web search provider plugin behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard"; -import { withEnvAsync } from "openclaw/plugin-sdk/test-env"; +import { withEnv, withEnvAsync } from "openclaw/plugin-sdk/test-env"; import { afterEach, describe, expect, it, vi } from "vitest"; import { testing } from "../test-api.js"; import { createKimiWebSearchProvider } from "./kimi-web-search-provider.js"; const kimiApiKeyEnv = ["KIMI_API", "KEY"].join("_"); -function withEnv(overrides: Record, run: () => void): void { - const previous = new Map(); - for (const [key, value] of Object.entries(overrides)) { - previous.set(key, process.env[key]); - process.env[key] = value; - } - try { - run(); - } finally { - for (const [key, value] of previous) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } -} - function jsonResponse(body: unknown): Response { return new Response(JSON.stringify(body), { status: 200, @@ -286,6 +267,42 @@ describe("kimi web search provider", () => { ).toBeUndefined(); }); + it("forwards the execution abort signal to an in-flight Kimi search", async () => { + const fetchMock = vi.fn( + (_url: string, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener( + "abort", + () => reject(new Error(String(init.signal?.reason ?? "Aborted"))), + { + once: true, + }, + ); + }), + ); + vi.stubGlobal("fetch", fetchMock); + + await withEnvAsync({ KIMI_API_KEY: "kimi-test-key" }, async () => { + const controller = new AbortController(); + const tool = createKimiWebSearchProvider().createTool({ config: {}, searchConfig: {} }); + if (!tool) { + throw new Error("Expected tool definition"); + } + + const search = tool.execute( + { query: "unique Kimi abort regression" }, + { + signal: controller.signal, + }, + ); + await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce()); + controller.abort(new Error("Kimi search cancelled")); + + await expect(search).rejects.toThrow("Kimi search cancelled"); + expect(fetchMock.mock.calls[0]?.[1]?.signal?.aborted).toBe(true); + }); + }); + it("uses config apiKey when provided", () => { expect(testing.resolveKimiApiKey({ apiKey: "kimi-test-key" })).toBe("kimi-test-key"); }); diff --git a/extensions/moonshot/src/kimi-web-search-provider.ts b/extensions/moonshot/src/kimi-web-search-provider.ts index f1259b018fc2..8b46898b3cfd 100644 --- a/extensions/moonshot/src/kimi-web-search-provider.ts +++ b/extensions/moonshot/src/kimi-web-search-provider.ts @@ -60,9 +60,11 @@ export function createKimiWebSearchProvider(): WebSearchProviderPlugin { description: "Search the web using Kimi by Moonshot. Returns AI-synthesized answers with citations from native $web_search.", parameters: KimiSearchSchema, - execute: async (args) => { + execute: async (args, context) => { const { executeKimiWebSearchProviderTool } = await loadKimiWebSearchProviderRuntime(); - return await executeKimiWebSearchProviderTool(ctx, args); + return await executeKimiWebSearchProviderTool(ctx, args, { + signal: context?.signal, + }); }, }), };