mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
三个搜索 key 改为 auth.json 中的 tavily/exa/keenable api_key 凭据, pi-web-search/auth.ts 复刻上游 resolve-config-value 语义(!command、 $VAR 插值、credential env 优先、$$/$! 转义)并在每次调用时读取, 进程环境变量与 search.env 不再被直接读取。search_config.sh 重写为 scripts/search-auth.mjs 的薄封装:加锁原子写、权限 600、保留其他 凭据条目,支持 --migrate 从旧 search.env 一次性导入。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
155 lines
5.4 KiB
TypeScript
155 lines
5.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
|
import webSearchExtension, {
|
|
createFetchRunners,
|
|
createProviderRunners,
|
|
dispatchFetch,
|
|
dispatchSearch,
|
|
normalizeUrls,
|
|
toExaParams,
|
|
toKeenableParams,
|
|
toTavilyParams,
|
|
type WebSearchParams,
|
|
} from "../index.ts";
|
|
|
|
const PARAMS: WebSearchParams = { query: "release notes" };
|
|
|
|
function ctxAnswering(answer: string, calls: { count: number } = { count: 0 }) {
|
|
return {
|
|
model: { provider: "test", id: "main" },
|
|
modelRegistry: {
|
|
complete: async () => {
|
|
calls.count += 1;
|
|
return { content: [{ type: "text", text: answer }] };
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const WITH_TAVILY = () => "k";
|
|
const KEYLESS = () => undefined;
|
|
|
|
test("extension registers only web_search and web_fetch, with no provider in the schema", () => {
|
|
const tools = new Map<string, { parameters?: { properties?: Record<string, unknown> }; description?: string }>();
|
|
const pi = {
|
|
registerTool(definition: { name: string }) {
|
|
tools.set(definition.name, definition as never);
|
|
},
|
|
on() {},
|
|
} as unknown as ExtensionAPI;
|
|
|
|
webSearchExtension(pi);
|
|
assert.deepEqual([...tools.keys()].sort(), ["web_fetch", "web_search"]);
|
|
const schemaText = JSON.stringify([...tools.values()].map((t) => t.parameters));
|
|
assert.doesNotMatch(schemaText, /tavily|exa|keenable|"provider"/iu);
|
|
});
|
|
|
|
test("dispatchSearch runs the model-picked provider first and records details", async () => {
|
|
const ran: string[] = [];
|
|
const runners = createProviderRunners({
|
|
exa: async () => {
|
|
ran.push("exa");
|
|
return "exa results";
|
|
},
|
|
});
|
|
const result = await dispatchSearch(PARAMS, ctxAnswering("exa"), new Map(), undefined, runners, WITH_TAVILY);
|
|
assert.deepEqual(ran, ["exa"]);
|
|
assert.equal(result.content[0]!.type === "text" && result.content[0]!.text, "exa results");
|
|
assert.deepEqual(result.details, { provider: "exa", routedBy: "model" });
|
|
});
|
|
|
|
test("dispatchSearch falls back across remaining providers when the pick fails", async () => {
|
|
const ran: string[] = [];
|
|
const runners = createProviderRunners({
|
|
tavily: async () => {
|
|
ran.push("tavily");
|
|
throw new Error("tavily down");
|
|
},
|
|
exa: async () => {
|
|
ran.push("exa");
|
|
return "exa results";
|
|
},
|
|
});
|
|
const result = await dispatchSearch(PARAMS, ctxAnswering("tavily"), new Map(), undefined, runners, WITH_TAVILY);
|
|
assert.deepEqual(ran, ["tavily", "exa"]);
|
|
assert.equal((result.details as Record<string, unknown>).provider, "exa");
|
|
assert.equal((result.details as Record<string, unknown>).routedBy, "provider-fallback");
|
|
});
|
|
|
|
test("dispatchSearch reports an error result when every provider fails", async () => {
|
|
const runners = createProviderRunners({
|
|
tavily: async () => { throw new Error("t"); },
|
|
exa: async () => { throw new Error("e"); },
|
|
keenable: async () => { throw new Error("k"); },
|
|
});
|
|
const result = await dispatchSearch(PARAMS, ctxAnswering("exa"), new Map(), undefined, runners, WITH_TAVILY);
|
|
assert.equal((result.details as Record<string, unknown>).error, true);
|
|
assert.match(result.content[0]!.type === "text" ? result.content[0]!.text : "", /failed on every available provider/);
|
|
});
|
|
|
|
test("normalizeUrls accepts a string or array, trims, and deduplicates", () => {
|
|
assert.deepEqual(normalizeUrls(" https://a.example "), ["https://a.example"]);
|
|
assert.deepEqual(normalizeUrls(["https://a", " https://a ", "https://b"]), ["https://a", "https://b"]);
|
|
});
|
|
|
|
test("dispatchFetch prefers exa and degrades to keenable without a model call", async () => {
|
|
const fetchers = createFetchRunners({
|
|
exa: async (urls) => {
|
|
throw new Error(`exa down for ${urls.length}`);
|
|
},
|
|
keenable: async (urls) => `keenable pages: ${urls.join(",")}`,
|
|
});
|
|
const result = await dispatchFetch({ urls: ["https://a.example", "https://b.example"] }, undefined, fetchers, KEYLESS);
|
|
assert.equal((result.details as Record<string, unknown>).provider, "keenable");
|
|
assert.match(result.content[0]!.type === "text" ? result.content[0]!.text : "", /keenable pages: https:\/\/a\.example,https:\/\/b\.example/);
|
|
});
|
|
|
|
test("dispatchFetch validates input and caps the URL count", async () => {
|
|
const empty = await dispatchFetch({ urls: [] }, undefined);
|
|
assert.equal((empty.details as Record<string, unknown>).error, true);
|
|
|
|
const tooMany = await dispatchFetch({ urls: Array.from({ length: 11 }, (_, i) => `https://e${i}.example`) }, undefined);
|
|
assert.equal((tooMany.details as Record<string, unknown>).error, true);
|
|
assert.match(tooMany.content[0]!.type === "text" ? tooMany.content[0]!.text : "", /at most 10/);
|
|
});
|
|
|
|
test("unified params map onto each provider's native arguments", () => {
|
|
const params: WebSearchParams = {
|
|
query: "q",
|
|
max_results: 4,
|
|
site: "a.dev",
|
|
published_after: "2025-01-01",
|
|
published_before: "2025-06-01",
|
|
depth: "advanced",
|
|
};
|
|
assert.deepEqual(toTavilyParams(params), {
|
|
query: "q",
|
|
max_results: 4,
|
|
search_depth: "advanced",
|
|
include_domains: ["a.dev"],
|
|
start_date: "2025-01-01",
|
|
end_date: "2025-06-01",
|
|
});
|
|
assert.deepEqual(toExaParams(params), {
|
|
query: "q",
|
|
numResults: 4,
|
|
includeDomains: ["a.dev"],
|
|
startPublishedDate: "2025-01-01",
|
|
endPublishedDate: "2025-06-01",
|
|
});
|
|
assert.deepEqual(toKeenableParams(params), {
|
|
query: "q",
|
|
site: "a.dev",
|
|
published_after: "2025-01-01",
|
|
published_before: "2025-06-01",
|
|
});
|
|
|
|
const defaults = toTavilyParams({ query: "q" });
|
|
assert.equal(defaults.max_results, 8);
|
|
assert.equal(defaults.search_depth, "basic");
|
|
assert.equal(defaults.include_domains, undefined);
|
|
});
|