feat(pi-web-search): 搜索凭据迁移到 Pi auth.json

三个搜索 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>
This commit is contained in:
云服务部-叶林立
2026-10-19 20:39:13 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent ee1e6999d7
commit 0c9823ef5c
20 changed files with 909 additions and 335 deletions
+6 -7
View File
@@ -9,7 +9,7 @@
import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { loadSearchConfigEnv } from "../extensions/search-config.ts";
import { getSearchApiKey, type SearchKeyResolver } from "./auth.ts";
import { exaFetchPages, exaSearch } from "./clients/exa.ts";
import { keenableFetch, keenableSearch } from "./clients/keenable.ts";
import { tavilyAvailable, tavilyExtract, tavilySearch } from "./clients/tavily.ts";
@@ -108,9 +108,10 @@ export async function dispatchSearch(
routeCache: Map<string, SearchProvider>,
signal?: AbortSignal,
runners: Record<SearchProvider, ProviderRunner> = createProviderRunners(),
resolve: SearchKeyResolver = getSearchApiKey,
): Promise<AgentToolResult> {
const decision = await routeSearch(params, ctx, routeCache, signal);
const order = [decision.provider, ...availableProviders().filter((p) => p !== decision.provider)];
const decision = await routeSearch(params, ctx, routeCache, signal, resolve);
const order = [decision.provider, ...availableProviders(resolve).filter((p) => p !== decision.provider)];
let lastError: unknown;
for (const provider of order) {
try {
@@ -161,6 +162,7 @@ export async function dispatchFetch(
params: WebFetchParams,
signal?: AbortSignal,
fetchers: Record<SearchProvider, FetchRunner> = createFetchRunners(),
resolve: SearchKeyResolver = getSearchApiKey,
): Promise<AgentToolResult> {
const urls = normalizeUrls(params.urls);
if (!urls.length) {
@@ -171,7 +173,7 @@ export async function dispatchFetch(
}
const maxCharacters = params.max_characters ?? DEFAULT_FETCH_MAX_CHARACTERS;
const order: SearchProvider[] = ["exa", "keenable"];
if (tavilyAvailable()) order.push("tavily");
if (tavilyAvailable(resolve)) order.push("tavily");
let lastError: unknown;
for (const provider of order) {
@@ -186,9 +188,6 @@ export async function dispatchFetch(
}
export default function webSearchExtension(pi: ExtensionAPI): void {
// Keys may already be loaded by extensions/search-config.ts; this is a
// harmless idempotent safety net for standalone loading.
loadSearchConfigEnv();
const routeCache = new Map<string, SearchProvider>();
pi.registerTool({