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
+16 -9
View File
@@ -1,17 +1,19 @@
/**
* Keenable REST client, adapted from @keenable/pi-search (keenable-client.ts).
* Keyless by default (`/v1/*\/public`), authenticated when KEENABLE_API_KEY is
* present (`/v1/*`), HTTPS-only base URL, `X-Keenable-Title` attribution.
* Keyless by default (`/v1/*\/public`), authenticated when the `keenable`
* api_key credential exists in auth.json (`/v1/*`), HTTPS-only base URL,
* `X-Keenable-Title` attribution.
*/
import { isIP } from "node:net";
import { getSearchApiKey, getSearchCredentialEnv } from "../auth.ts";
export class KeenableError extends Error {}
const CLIENT_VERSION = "0.1.0";
const DEFAULT_BASE_URL = "https://api.keenable.ai";
const BASE_URL_ENV = "KEENABLE_API_URL";
const API_KEY_ENV = "KEENABLE_API_KEY";
const ATTRIBUTION_TITLE = "Pi";
const USER_AGENT = `pi-web-search/${CLIENT_VERSION}`;
const DEFAULT_TIMEOUT_MS = 30_000;
@@ -45,9 +47,15 @@ export interface KeenableFetchResult {
[key: string]: unknown;
}
/** Resolve the API base URL from `KEENABLE_API_URL`, enforcing HTTPS (http only for loopback). */
/**
* Resolve the API base URL from `KEENABLE_API_URL` (process env first, then
* the keenable credential's `env` map), enforcing HTTPS (http only for loopback).
*/
function resolveBaseUrl(): string {
const base = (process.env[BASE_URL_ENV] || DEFAULT_BASE_URL).replace(/\/+$/u, "");
const base = (process.env[BASE_URL_ENV] || getSearchCredentialEnv("keenable", BASE_URL_ENV) || DEFAULT_BASE_URL).replace(
/\/+$/u,
"",
);
let parsed: URL;
try {
parsed = new URL(base);
@@ -63,10 +71,9 @@ function resolveBaseUrl(): string {
throw new KeenableError(`${BASE_URL_ENV} must be an https:// URL with a host, got ${base}`);
}
/** Non-blank API key from the env, or undefined to use the keyless public tier. */
/** API key from auth.json, or undefined to use the keyless public tier. */
function resolveApiKey(): string | undefined {
const key = (process.env[API_KEY_ENV] ?? "").trim();
return key || undefined;
return getSearchApiKey("keenable");
}
function buildHeaders(apiKey: string | undefined): Record<string, string> {
@@ -129,7 +136,7 @@ async function raiseForStatus(response: Response, keyed: boolean): Promise<void>
if (response.ok) return;
if (!keyed && response.status === 429) {
throw new KeenableError(
"Keenable keyless requests hit their rate limit. Set KEENABLE_API_KEY to raise the limits.",
'Keenable keyless requests hit their rate limit. Add a "keenable" api_key credential to auth.json to raise the limits.',
);
}
let detail = "";