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
-57
View File
@@ -1,57 +0,0 @@
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
export const SEARCH_API_KEY_NAMES = ["TAVILY_API_KEY", "EXA_API_KEY", "KEENABLE_API_KEY"] as const;
type SearchApiKeyName = (typeof SEARCH_API_KEY_NAMES)[number];
type SearchEnvironment = Record<string, string | undefined>;
const SAFE_VALUE = /^[A-Za-z0-9._-]+$/;
export function getSearchConfigPath(env: SearchEnvironment = process.env): string {
if (env.MY_PI_SEARCH_CONFIG?.trim()) {
return env.MY_PI_SEARCH_CONFIG.trim();
}
const configHome = env.XDG_CONFIG_HOME?.trim() || join(env.HOME?.trim() || homedir(), ".config");
return join(configHome, "my-pi", "search.env");
}
export function parseSearchConfig(text: string): Partial<Record<SearchApiKeyName, string>> {
const parsed: Partial<Record<SearchApiKeyName, string>> = {};
for (const rawLine of text.split(/\r?\n/u)) {
const line = rawLine.trim().replace(/^export[ \t]+/u, "");
if (!line || line.startsWith("#")) continue;
const separator = line.indexOf("=");
if (separator < 1) continue;
const name = line.slice(0, separator).trim() as SearchApiKeyName;
const value = line.slice(separator + 1).trim();
if (!SEARCH_API_KEY_NAMES.includes(name) || !SAFE_VALUE.test(value)) continue;
parsed[name] = value;
}
return parsed;
}
/** Load package-managed search credentials without overriding explicit process environment values. */
export function loadSearchConfigEnv(
env: SearchEnvironment = process.env,
configPath = getSearchConfigPath(env),
): Partial<Record<SearchApiKeyName, string>> {
let parsed: Partial<Record<SearchApiKeyName, string>> = {};
try {
parsed = parseSearchConfig(readFileSync(configPath, "utf8"));
} catch {
// Missing or unreadable config is equivalent to an empty package-managed config.
}
const effective: Partial<Record<SearchApiKeyName, string>> = {};
for (const name of SEARCH_API_KEY_NAMES) {
if (!env[name]?.trim() && parsed[name]) env[name] = parsed[name];
if (env[name]?.trim()) effective[name] = env[name]?.trim();
}
return effective;
}
/** Load search API keys before provider extensions initialize. */
export default function searchConfigExtension(): void {
loadSearchConfigEnv();
}