mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
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:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
ee1e6999d7
commit
0c9823ef5c
@@ -2,9 +2,12 @@
|
||||
* Exa client: direct JSON-RPC over the hosted endpoint https://mcp.exa.ai/mcp.
|
||||
* This is a plain HTTPS POST client (no MCP adapter, no SDK): initialize once,
|
||||
* cache the `mcp-session-id` response header, then POST `tools/call` per call.
|
||||
* Keyless works; EXA_API_KEY is sent as the `x-api-key` header when present.
|
||||
* Keyless works; the `exa` api_key credential from auth.json is sent as the
|
||||
* `x-api-key` header when present.
|
||||
*/
|
||||
|
||||
import { getSearchApiKey } from "../auth.ts";
|
||||
|
||||
export class ExaError extends Error {}
|
||||
|
||||
const EXA_MCP_URL = "https://mcp.exa.ai/mcp?tools=web_search_exa,web_search_advanced_exa,web_fetch_exa";
|
||||
@@ -41,7 +44,7 @@ function baseHeaders(): Record<string, string> {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json, text/event-stream",
|
||||
};
|
||||
const key = process.env.EXA_API_KEY?.trim();
|
||||
const key = getSearchApiKey("exa");
|
||||
if (key) headers["x-api-key"] = key;
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* Tavily REST client: POST https://api.tavily.com/search|extract with a Bearer
|
||||
* key. Requires TAVILY_API_KEY — there is no keyless tier.
|
||||
* key from auth.json (`tavily` api_key credential) — there is no keyless tier.
|
||||
*/
|
||||
|
||||
import { getSearchApiKey } from "../auth.ts";
|
||||
|
||||
export class TavilyError extends Error {}
|
||||
|
||||
const TAVILY_BASE_URL = "https://api.tavily.com";
|
||||
@@ -30,8 +32,8 @@ export interface TavilySearchResult {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function tavilyAvailable(env: Record<string, string | undefined> = process.env): boolean {
|
||||
return Boolean(env.TAVILY_API_KEY?.trim());
|
||||
export function tavilyAvailable(resolve: (provider: "tavily") => string | undefined = getSearchApiKey): boolean {
|
||||
return Boolean(resolve("tavily"));
|
||||
}
|
||||
|
||||
function withTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
|
||||
@@ -46,9 +48,9 @@ async function post(
|
||||
signal: AbortSignal | undefined,
|
||||
fetchImpl: FetchLike,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const apiKey = process.env.TAVILY_API_KEY?.trim();
|
||||
const apiKey = getSearchApiKey("tavily");
|
||||
if (!apiKey) {
|
||||
throw new TavilyError("Tavily is not configured: TAVILY_API_KEY is missing.");
|
||||
throw new TavilyError('Tavily is not configured: no "tavily" api_key credential in auth.json.');
|
||||
}
|
||||
let response: Response;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user