web-access-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/README_CN.md +268 -0
  4. package/THIRD_PARTY_NOTICES.md +48 -0
  5. package/dist/cli.js +197 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config/config.js +463 -0
  8. package/dist/config/config.js.map +1 -0
  9. package/dist/config/edit.js +126 -0
  10. package/dist/config/edit.js.map +1 -0
  11. package/dist/core/diagnostics.js +94 -0
  12. package/dist/core/diagnostics.js.map +1 -0
  13. package/dist/core/errors.js +47 -0
  14. package/dist/core/errors.js.map +1 -0
  15. package/dist/core/router.js +320 -0
  16. package/dist/core/router.js.map +1 -0
  17. package/dist/core/schema.js +120 -0
  18. package/dist/core/schema.js.map +1 -0
  19. package/dist/core/types.js +22 -0
  20. package/dist/core/types.js.map +1 -0
  21. package/dist/providers/anysearch.js +225 -0
  22. package/dist/providers/anysearch.js.map +1 -0
  23. package/dist/providers/common.js +210 -0
  24. package/dist/providers/common.js.map +1 -0
  25. package/dist/providers/extract.js +155 -0
  26. package/dist/providers/extract.js.map +1 -0
  27. package/dist/providers/registry.js +31 -0
  28. package/dist/providers/registry.js.map +1 -0
  29. package/dist/providers/rsc.js +109 -0
  30. package/dist/providers/rsc.js.map +1 -0
  31. package/dist/providers/search.js +178 -0
  32. package/dist/providers/search.js.map +1 -0
  33. package/dist/providers/xcrawl.js +170 -0
  34. package/dist/providers/xcrawl.js.map +1 -0
  35. package/dist/transport/http.js +175 -0
  36. package/dist/transport/http.js.map +1 -0
  37. package/dist/version.js +2 -0
  38. package/dist/version.js.map +1 -0
  39. package/package.json +61 -0
  40. package/schemas/config.schema.json +210 -0
  41. package/schemas/extractRequest.schema.json +21 -0
  42. package/schemas/output.schema.json +435 -0
  43. package/schemas/searchRequest.schema.json +57 -0
package/dist/cli.js ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ import { Command, CommanderError, InvalidArgumentError, Option, } from "commander";
5
+ import { loadConfig } from "./config/config.js";
6
+ import { executeConfigEdit } from "./config/edit.js";
7
+ import { executeDoctor, executeProviders } from "./core/diagnostics.js";
8
+ import { asWebAccessError, WebAccessError } from "./core/errors.js";
9
+ import { errorEnvelope, executeExtract, executeSearch } from "./core/router.js";
10
+ import { COMMANDS } from "./core/types.js";
11
+ import { normalizeDomains } from "./providers/common.js";
12
+ import { VERSION } from "./version.js";
13
+ function collect(value, previous) {
14
+ return [...previous, value];
15
+ }
16
+ function integer(value) {
17
+ const parsed = Number(value);
18
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
19
+ throw new InvalidArgumentError("必须是大于 0 的整数");
20
+ return parsed;
21
+ }
22
+ function httpUrl(value) {
23
+ let url;
24
+ try {
25
+ url = new URL(value);
26
+ }
27
+ catch {
28
+ throw new WebAccessError("invalid_input", "url 必须是合法的绝对 URL");
29
+ }
30
+ if (url.protocol !== "http:" && url.protocol !== "https:")
31
+ throw new WebAccessError("invalid_input", "url 必须使用 http 或 https");
32
+ return url.toString();
33
+ }
34
+ function exitCode(envelope) {
35
+ if (envelope.ok)
36
+ return 0;
37
+ if (envelope.error.code === "aborted")
38
+ return 130;
39
+ if ([
40
+ "invalid_input",
41
+ "config_error",
42
+ "provider_unknown",
43
+ "provider_disabled",
44
+ ].includes(envelope.error.code))
45
+ return 2;
46
+ return 1;
47
+ }
48
+ function writeEnvelope(envelope, pretty) {
49
+ process.stdout.write(`${JSON.stringify(envelope, null, pretty ? 2 : undefined)}\n`);
50
+ }
51
+ export function createProgram(run, dependencies = {}) {
52
+ const program = new Command();
53
+ const runConfigEdit = dependencies.executeConfigEdit ?? executeConfigEdit;
54
+ program
55
+ .name("web-access")
56
+ .description("Agent-neutral 的网页搜索与内容提取 CLI")
57
+ .version(VERSION)
58
+ .option("--config <path>", "指定 JSON 配置文件")
59
+ .option("--pretty", "格式化 JSON 输出", false)
60
+ .showSuggestionAfterError();
61
+ program.configureOutput({ writeErr: () => { }, outputError: () => { } });
62
+ program.exitOverride();
63
+ program
64
+ .command("search")
65
+ .description("搜索网页")
66
+ .argument("<query>", "搜索关键词")
67
+ .option("-p, --provider <id>", "provider instance id,或 auto", "auto")
68
+ .option("-l, --limit <number>", "结果数量(1-20)", integer)
69
+ .addOption(new Option("--freshness <window>", "时间范围").choices([
70
+ "day",
71
+ "month",
72
+ "year",
73
+ ]))
74
+ .option("--include-domain <domain>", "仅包含域名,可重复", collect, [])
75
+ .option("--exclude-domain <domain>", "排除域名,可重复", collect, [])
76
+ .option("--timeout <milliseconds>", "总超时毫秒数", integer)
77
+ .action((query, options) => {
78
+ run(async () => {
79
+ const globals = program.opts();
80
+ const loaded = loadConfig(globals.config);
81
+ const request = {
82
+ query: query.trim(),
83
+ provider: options.provider.toLowerCase(),
84
+ limit: options.limit ?? loaded.app.search.limit,
85
+ freshness: options.freshness,
86
+ includeDomains: normalizeDomains(options.includeDomain),
87
+ excludeDomains: normalizeDomains(options.excludeDomain),
88
+ timeoutMs: options.timeout,
89
+ };
90
+ if (!request.query)
91
+ throw new WebAccessError("invalid_input", "query 不能为空");
92
+ if (request.limit > 20)
93
+ throw new WebAccessError("invalid_input", "limit 不能超过 20");
94
+ return executeSearch(request, {
95
+ loaded,
96
+ signal: processSignal.signal,
97
+ });
98
+ });
99
+ });
100
+ program
101
+ .command("extract")
102
+ .description("提取网页正文并转换为 Markdown")
103
+ .argument("<url>", "HTTP(S) URL")
104
+ .option("-p, --provider <id>", "provider instance id,或 auto", "auto")
105
+ .option("--timeout <milliseconds>", "总超时毫秒数", integer)
106
+ .action((url, options) => {
107
+ run(async () => {
108
+ const globals = program.opts();
109
+ const loaded = loadConfig(globals.config);
110
+ const request = {
111
+ url: httpUrl(url),
112
+ provider: options.provider.toLowerCase(),
113
+ timeoutMs: options.timeout,
114
+ };
115
+ return executeExtract(request, {
116
+ loaded,
117
+ signal: processSignal.signal,
118
+ });
119
+ });
120
+ });
121
+ program
122
+ .command("providers")
123
+ .description("列出 provider instance、route 与配置状态")
124
+ .action(() => run(() => executeProviders(loadConfig(program.opts().config))));
125
+ program
126
+ .command("doctor")
127
+ .description("检查本地配置与已启用 provider 的可用性")
128
+ .action(() => run(() => executeDoctor(loadConfig(program.opts().config))));
129
+ const config = program.command("config").description("管理配置文件");
130
+ config
131
+ .command("edit")
132
+ .description("创建默认配置文件并用系统默认应用打开")
133
+ .action(() => run(() => runConfigEdit({
134
+ explicitPath: program.opts().config,
135
+ })));
136
+ return program;
137
+ }
138
+ function envelopeCommand(args) {
139
+ const nested = args.slice(0, 2).join(".");
140
+ if (COMMANDS.includes(nested))
141
+ return nested;
142
+ const command = args[0];
143
+ return COMMANDS.includes(command)
144
+ ? command
145
+ : null;
146
+ }
147
+ const processSignal = new AbortController();
148
+ process.once("SIGINT", () => processSignal.abort());
149
+ async function main() {
150
+ let pending;
151
+ const program = createProgram((task) => {
152
+ pending = Promise.resolve().then(task);
153
+ });
154
+ if (process.argv.length <= 2) {
155
+ const envelope = errorEnvelope(new WebAccessError("invalid_input", "必须指定命令"));
156
+ writeEnvelope(envelope, false);
157
+ process.exitCode = 2;
158
+ return;
159
+ }
160
+ try {
161
+ await program.parseAsync(process.argv);
162
+ if (!pending)
163
+ throw new WebAccessError("invalid_input", "必须指定命令");
164
+ const envelope = await pending;
165
+ writeEnvelope(envelope, !!program.opts().pretty);
166
+ process.exitCode = exitCode(envelope);
167
+ }
168
+ catch (caught) {
169
+ if (caught instanceof CommanderError &&
170
+ caught.code === "commander.helpDisplayed")
171
+ return;
172
+ if (caught instanceof CommanderError && caught.code === "commander.version")
173
+ return;
174
+ const normalized = caught instanceof CommanderError
175
+ ? new WebAccessError("invalid_input", caught.code === "commander.help" && program.args[0] === "config"
176
+ ? "必须指定 config 子命令"
177
+ : caught.message)
178
+ : asWebAccessError(caught);
179
+ const envelope = errorEnvelope(normalized, envelopeCommand(program.args));
180
+ writeEnvelope(envelope, !!program.opts().pretty);
181
+ process.exitCode = exitCode(envelope);
182
+ }
183
+ }
184
+ export function isMainModule(moduleUrl, entryPath) {
185
+ if (!entryPath)
186
+ return false;
187
+ try {
188
+ return moduleUrl === pathToFileURL(realpathSync(entryPath)).href;
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ if (isMainModule(import.meta.url, process.argv[1])) {
195
+ await main();
196
+ }
197
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,463 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { WebAccessError } from "../core/errors.js";
5
+ import { PROVIDER_TYPES, SEARCH_FILTER_MODES, } from "../core/types.js";
6
+ export const CONFIG_ENV = "WEB_ACCESS_CONFIG";
7
+ export const DEFAULT_SEARCH_PROVIDERS = [
8
+ "tavily",
9
+ "exa",
10
+ "brave",
11
+ "searxng",
12
+ "anysearch",
13
+ "xcrawl",
14
+ ];
15
+ export const DEFAULT_EXTRACT_PROVIDERS = [
16
+ "firecrawl",
17
+ "jina",
18
+ "exa",
19
+ "anysearch",
20
+ "xcrawl",
21
+ "http",
22
+ ];
23
+ export const DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
24
+ const DEFAULT_SEARCH_CONFIG = {
25
+ providers: [...DEFAULT_SEARCH_PROVIDERS],
26
+ limit: 5,
27
+ timeoutMs: 60_000,
28
+ attemptTimeoutMs: 20_000,
29
+ maxResponseBytes: DEFAULT_MAX_RESPONSE_BYTES,
30
+ };
31
+ const DEFAULT_EXTRACT_CONFIG = {
32
+ providers: [...DEFAULT_EXTRACT_PROVIDERS],
33
+ timeoutMs: 120_000,
34
+ attemptTimeoutMs: 45_000,
35
+ maxResponseBytes: DEFAULT_MAX_RESPONSE_BYTES,
36
+ minContentCharacters: 500,
37
+ };
38
+ const SEARCH_TYPES = new Set([
39
+ "tavily",
40
+ "exa",
41
+ "brave",
42
+ "searxng",
43
+ "anysearch",
44
+ "xcrawl",
45
+ ]);
46
+ const EXTRACT_TYPES = new Set([
47
+ "firecrawl",
48
+ "jina",
49
+ "exa",
50
+ "http",
51
+ "anysearch",
52
+ "xcrawl",
53
+ ]);
54
+ const DEFAULT_INSTANCE_CONFIGS = [
55
+ { id: "tavily", type: "tavily" },
56
+ { id: "exa", type: "exa" },
57
+ { id: "brave", type: "brave" },
58
+ { id: "searxng", type: "searxng" },
59
+ { id: "firecrawl", type: "firecrawl" },
60
+ { id: "jina", type: "jina" },
61
+ { id: "http", type: "http" },
62
+ { id: "anysearch", type: "anysearch", searchFilterMode: "strict" },
63
+ { id: "xcrawl", type: "xcrawl", searchFilterMode: "strict" },
64
+ ];
65
+ const STANDARD_KEY_ENV = {
66
+ tavily: "TAVILY_API_KEY",
67
+ exa: "EXA_API_KEY",
68
+ brave: "BRAVE_API_KEY",
69
+ firecrawl: "FIRECRAWL_API_KEY",
70
+ jina: "JINA_API_KEY",
71
+ anysearch: "ANYSEARCH_API_KEY",
72
+ xcrawl: "XCRAWL_API_KEY",
73
+ };
74
+ const STANDARD_BASE_ENV = {
75
+ tavily: "TAVILY_BASE_URL",
76
+ exa: "EXA_BASE_URL",
77
+ brave: "BRAVE_BASE_URL",
78
+ searxng: "SEARXNG_BASE_URL",
79
+ firecrawl: "FIRECRAWL_BASE_URL",
80
+ jina: "JINA_BASE_URL",
81
+ anysearch: "ANYSEARCH_BASE_URL",
82
+ xcrawl: "XCRAWL_BASE_URL",
83
+ };
84
+ const SEARCH_FILTER_TYPES = new Set(["anysearch", "xcrawl"]);
85
+ const ALLOWED_INSTANCE_KEYS = new Set([
86
+ "id",
87
+ "type",
88
+ "apiKey",
89
+ "apiKeyEnv",
90
+ "baseUrl",
91
+ "baseUrlEnv",
92
+ "headers",
93
+ "searchFilterMode",
94
+ ]);
95
+ const ALLOWED_SEARCH_KEYS = new Set([
96
+ "providers",
97
+ "limit",
98
+ "timeoutMs",
99
+ "attemptTimeoutMs",
100
+ "maxResponseBytes",
101
+ ]);
102
+ const ALLOWED_EXTRACT_KEYS = new Set([
103
+ "providers",
104
+ "timeoutMs",
105
+ "attemptTimeoutMs",
106
+ "maxResponseBytes",
107
+ "minContentCharacters",
108
+ ]);
109
+ export class ConfigError extends WebAccessError {
110
+ constructor(message, details) {
111
+ super("config_error", message, { details });
112
+ this.name = "ConfigError";
113
+ }
114
+ }
115
+ function isRecord(value) {
116
+ return !!value && typeof value === "object" && !Array.isArray(value);
117
+ }
118
+ function assertRecord(value, path) {
119
+ if (!isRecord(value))
120
+ throw new ConfigError(`${path} 必须是 JSON 对象`);
121
+ }
122
+ function assertKnownKeys(value, allowed, path) {
123
+ for (const key of Object.keys(value)) {
124
+ if (!allowed.has(key))
125
+ throw new ConfigError(`${path}.${key} 不是受支持的配置字段`);
126
+ }
127
+ }
128
+ function parsePositiveInt(value, path, defaultValue) {
129
+ if (value === undefined)
130
+ return defaultValue;
131
+ if (!Number.isInteger(value) || value < 1)
132
+ throw new ConfigError(`${path} 必须是大于 0 的整数`);
133
+ return value;
134
+ }
135
+ function parseLimit(value) {
136
+ const limit = parsePositiveInt(value, "search.limit", DEFAULT_SEARCH_CONFIG.limit);
137
+ if (limit > 20)
138
+ throw new ConfigError("search.limit 不能超过 20");
139
+ return limit;
140
+ }
141
+ function parseString(value, path) {
142
+ if (typeof value !== "string" || value.trim() === "")
143
+ throw new ConfigError(`${path} 必须是非空字符串`);
144
+ return value.trim();
145
+ }
146
+ function parseOptionalString(value, path) {
147
+ if (value === undefined)
148
+ return undefined;
149
+ return parseString(value, path);
150
+ }
151
+ function validateEnvName(value, path) {
152
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value))
153
+ throw new ConfigError(`${path} 不是合法环境变量名`);
154
+ return value;
155
+ }
156
+ function normalizeHeaders(value, path) {
157
+ if (value === undefined)
158
+ return undefined;
159
+ assertRecord(value, path);
160
+ const headers = {};
161
+ for (const [name, headerValue] of Object.entries(value)) {
162
+ if (!name.trim() || /[\r\n]/.test(name))
163
+ throw new ConfigError(`${path} 包含非法 header 名称`);
164
+ if (typeof headerValue !== "string" || /[\r\n]/.test(headerValue))
165
+ throw new ConfigError(`${path}.${name} 必须是无换行字符串`);
166
+ headers[name] = headerValue;
167
+ }
168
+ return headers;
169
+ }
170
+ function normalizeBaseUrl(value, path) {
171
+ let url;
172
+ try {
173
+ url = new URL(value);
174
+ }
175
+ catch {
176
+ throw new ConfigError(`${path} 必须是 HTTP(S) URL`);
177
+ }
178
+ if (url.protocol !== "http:" && url.protocol !== "https:")
179
+ throw new ConfigError(`${path} 必须使用 http 或 https`);
180
+ if (url.username || url.password)
181
+ throw new ConfigError(`${path} 不允许包含 URL 用户名或密码`);
182
+ url.search = "";
183
+ url.hash = "";
184
+ url.pathname = url.pathname.replace(/\/+$/, "");
185
+ return url.toString().replace(/\/$/, "");
186
+ }
187
+ function parseInstance(value, index) {
188
+ const path = `providers[${index}]`;
189
+ assertRecord(value, path);
190
+ assertKnownKeys(value, ALLOWED_INSTANCE_KEYS, path);
191
+ const id = parseString(value.id, `${path}.id`).toLowerCase();
192
+ if (!/^[a-z][a-z0-9_-]{0,63}$/.test(id) || id === "auto")
193
+ throw new ConfigError(`${path}.id 不是合法 instance id`);
194
+ const type = parseString(value.type, `${path}.type`).toLowerCase();
195
+ if (!PROVIDER_TYPES.includes(type))
196
+ throw new ConfigError(`${path}.type 不受支持: ${type}`);
197
+ const apiKey = parseOptionalString(value.apiKey, `${path}.apiKey`);
198
+ const apiKeyEnvRaw = parseOptionalString(value.apiKeyEnv, `${path}.apiKeyEnv`);
199
+ const baseUrlRaw = parseOptionalString(value.baseUrl, `${path}.baseUrl`);
200
+ const baseUrl = baseUrlRaw
201
+ ? normalizeBaseUrl(baseUrlRaw, `${path}.baseUrl`)
202
+ : undefined;
203
+ const baseUrlEnvRaw = parseOptionalString(value.baseUrlEnv, `${path}.baseUrlEnv`);
204
+ const headers = normalizeHeaders(value.headers, `${path}.headers`);
205
+ const searchFilterMode = value.searchFilterMode === undefined
206
+ ? undefined
207
+ : parseString(value.searchFilterMode, `${path}.searchFilterMode`).toLowerCase();
208
+ if (searchFilterMode !== undefined &&
209
+ !SEARCH_FILTER_MODES.includes(searchFilterMode))
210
+ throw new ConfigError(`${path}.searchFilterMode 必须是 strict 或 best_effort`);
211
+ if (searchFilterMode !== undefined &&
212
+ !SEARCH_FILTER_TYPES.has(type))
213
+ throw new ConfigError(`${path}.searchFilterMode 仅适用于 anysearch 或 xcrawl`);
214
+ return {
215
+ id,
216
+ type: type,
217
+ ...(apiKey ? { apiKey } : {}),
218
+ ...(apiKeyEnvRaw
219
+ ? { apiKeyEnv: validateEnvName(apiKeyEnvRaw, `${path}.apiKeyEnv`) }
220
+ : {}),
221
+ ...(baseUrl ? { baseUrl } : {}),
222
+ ...(baseUrlEnvRaw
223
+ ? { baseUrlEnv: validateEnvName(baseUrlEnvRaw, `${path}.baseUrlEnv`) }
224
+ : {}),
225
+ ...(headers ? { headers } : {}),
226
+ ...(searchFilterMode
227
+ ? { searchFilterMode: searchFilterMode }
228
+ : {}),
229
+ };
230
+ }
231
+ function parseRoute(value, path) {
232
+ if (value === undefined)
233
+ return undefined;
234
+ if (!Array.isArray(value))
235
+ throw new ConfigError(`${path} 必须是 instance id 数组`);
236
+ const result = [];
237
+ for (const [index, item] of value.entries()) {
238
+ const id = parseString(item, `${path}[${index}]`).toLowerCase();
239
+ if (id === "auto" || result.includes(id))
240
+ throw new ConfigError(`${path} 包含重复或保留 id: ${id}`);
241
+ result.push(id);
242
+ }
243
+ return result;
244
+ }
245
+ function parseSearch(value) {
246
+ if (value === undefined)
247
+ return {};
248
+ assertRecord(value, "search");
249
+ assertKnownKeys(value, ALLOWED_SEARCH_KEYS, "search");
250
+ return {
251
+ providers: parseRoute(value.providers, "search.providers"),
252
+ limit: parseLimit(value.limit),
253
+ timeoutMs: parsePositiveInt(value.timeoutMs, "search.timeoutMs", DEFAULT_SEARCH_CONFIG.timeoutMs),
254
+ attemptTimeoutMs: parsePositiveInt(value.attemptTimeoutMs, "search.attemptTimeoutMs", DEFAULT_SEARCH_CONFIG.attemptTimeoutMs),
255
+ maxResponseBytes: parsePositiveInt(value.maxResponseBytes, "search.maxResponseBytes", DEFAULT_SEARCH_CONFIG.maxResponseBytes),
256
+ };
257
+ }
258
+ function parseExtract(value) {
259
+ if (value === undefined)
260
+ return {};
261
+ assertRecord(value, "extract");
262
+ assertKnownKeys(value, ALLOWED_EXTRACT_KEYS, "extract");
263
+ return {
264
+ providers: parseRoute(value.providers, "extract.providers"),
265
+ timeoutMs: parsePositiveInt(value.timeoutMs, "extract.timeoutMs", DEFAULT_EXTRACT_CONFIG.timeoutMs),
266
+ attemptTimeoutMs: parsePositiveInt(value.attemptTimeoutMs, "extract.attemptTimeoutMs", DEFAULT_EXTRACT_CONFIG.attemptTimeoutMs),
267
+ maxResponseBytes: parsePositiveInt(value.maxResponseBytes, "extract.maxResponseBytes", DEFAULT_EXTRACT_CONFIG.maxResponseBytes),
268
+ minContentCharacters: parsePositiveInt(value.minContentCharacters, "extract.minContentCharacters", DEFAULT_EXTRACT_CONFIG.minContentCharacters),
269
+ };
270
+ }
271
+ function parseRawConfig(value) {
272
+ assertRecord(value, "config");
273
+ assertKnownKeys(value, new Set(["$schema", "providers", "search", "extract"]), "config");
274
+ if (value.$schema !== undefined && typeof value.$schema !== "string")
275
+ throw new ConfigError("$schema 必须是字符串");
276
+ const rawInstances = value.providers === undefined ? [] : value.providers;
277
+ if (!Array.isArray(rawInstances))
278
+ throw new ConfigError("providers 必须是数组");
279
+ const instances = rawInstances.map(parseInstance);
280
+ const ids = new Set();
281
+ for (const instance of instances) {
282
+ if (ids.has(instance.id))
283
+ throw new ConfigError(`providers 中存在重复 id: ${instance.id}`);
284
+ ids.add(instance.id);
285
+ }
286
+ return {
287
+ instances,
288
+ search: parseSearch(value.search),
289
+ extract: parseExtract(value.extract),
290
+ };
291
+ }
292
+ function mergeInstances(overrides) {
293
+ const merged = DEFAULT_INSTANCE_CONFIGS.map((instance) => ({ ...instance }));
294
+ const byId = new Map(merged.map((instance) => [instance.id, instance]));
295
+ for (const override of overrides) {
296
+ const existing = byId.get(override.id);
297
+ if (existing) {
298
+ if (existing.type !== override.type)
299
+ throw new ConfigError(`默认 instance ${override.id} 的 type 不能改为 ${override.type}`);
300
+ Object.assign(existing, override);
301
+ }
302
+ else {
303
+ const copy = { ...override };
304
+ merged.push(copy);
305
+ byId.set(copy.id, copy);
306
+ }
307
+ }
308
+ return merged;
309
+ }
310
+ function validateRoutes(instances, route, capability) {
311
+ const allowed = capability === "search" ? SEARCH_TYPES : EXTRACT_TYPES;
312
+ const byId = new Map(instances.map((instance) => [instance.id, instance]));
313
+ const chosen = route ??
314
+ (capability === "search"
315
+ ? [...DEFAULT_SEARCH_PROVIDERS]
316
+ : [...DEFAULT_EXTRACT_PROVIDERS]);
317
+ for (const id of chosen) {
318
+ const instance = byId.get(id);
319
+ if (!instance)
320
+ throw new ConfigError(`${capability}.providers 引用了不存在的 instance: ${id}`);
321
+ if (!allowed.has(instance.type))
322
+ throw new ConfigError(`${capability}.providers 的 instance ${id} 不支持 ${capability}`);
323
+ }
324
+ return chosen;
325
+ }
326
+ export function getDefaultConfigPath() {
327
+ return join(homedir(), ".config", "web-access-cli", "config.json");
328
+ }
329
+ export function resolveConfigPath(explicitPath, env = process.env) {
330
+ const raw = explicitPath || env[CONFIG_ENV] || getDefaultConfigPath();
331
+ return isAbsolute(raw) ? raw : resolve(raw);
332
+ }
333
+ function resolveEnvValue(name, fallback, env) {
334
+ const custom = name ? env[name]?.trim() : undefined;
335
+ if (custom)
336
+ return { value: custom, source: "custom_env" };
337
+ const standard = fallback ? env[fallback]?.trim() : undefined;
338
+ if (standard)
339
+ return { value: standard, source: "standard_env" };
340
+ return { value: null, source: "missing" };
341
+ }
342
+ function resolveProvider(instance, env) {
343
+ const standardKeyEnv = instance.id === instance.type ? STANDARD_KEY_ENV[instance.type] : undefined;
344
+ const keyEnv = resolveEnvValue(instance.apiKeyEnv, standardKeyEnv, env);
345
+ const apiKey = keyEnv.value ?? instance.apiKey ?? null;
346
+ const credentialSource = apiKey
347
+ ? keyEnv.value
348
+ ? keyEnv.source
349
+ : "config"
350
+ : "missing";
351
+ const standardBaseEnv = STANDARD_BASE_ENV[instance.type];
352
+ const baseEnv = resolveEnvValue(instance.baseUrlEnv, instance.id === instance.type ? standardBaseEnv : undefined, env);
353
+ const rawBaseUrl = baseEnv.value ??
354
+ instance.baseUrl ??
355
+ DEFAULT_BASE_URLS[instance.type] ??
356
+ null;
357
+ const baseUrl = rawBaseUrl
358
+ ? normalizeBaseUrl(rawBaseUrl, baseEnv.value
359
+ ? (instance.baseUrlEnv ??
360
+ STANDARD_BASE_ENV[instance.type] ??
361
+ "baseUrl 环境变量")
362
+ : `providers.${instance.id}.baseUrl`)
363
+ : null;
364
+ const baseUrlSource = baseEnv.value
365
+ ? baseEnv.source
366
+ : instance.baseUrl
367
+ ? "config"
368
+ : DEFAULT_BASE_URLS[instance.type]
369
+ ? "default"
370
+ : "missing";
371
+ return {
372
+ id: instance.id,
373
+ type: instance.type,
374
+ apiKey,
375
+ credentialSource,
376
+ baseUrl,
377
+ baseUrlSource,
378
+ headers: { ...(instance.headers ?? {}) },
379
+ searchFilterMode: instance.searchFilterMode ??
380
+ (SEARCH_FILTER_TYPES.has(instance.type) ? "strict" : null),
381
+ };
382
+ }
383
+ const DEFAULT_BASE_URLS = {
384
+ tavily: "https://api.tavily.com",
385
+ exa: "https://api.exa.ai",
386
+ brave: "https://api.search.brave.com",
387
+ firecrawl: "https://api.firecrawl.dev",
388
+ jina: "https://r.jina.ai",
389
+ anysearch: "https://api.anysearch.com",
390
+ xcrawl: "https://run.xcrawl.com",
391
+ };
392
+ export function createDefaultAppConfig() {
393
+ return {
394
+ providers: DEFAULT_INSTANCE_CONFIGS.map((instance) => ({ ...instance })),
395
+ search: {
396
+ ...DEFAULT_SEARCH_CONFIG,
397
+ providers: [...DEFAULT_SEARCH_CONFIG.providers],
398
+ },
399
+ extract: {
400
+ ...DEFAULT_EXTRACT_CONFIG,
401
+ providers: [...DEFAULT_EXTRACT_CONFIG.providers],
402
+ },
403
+ };
404
+ }
405
+ export function loadConfig(explicitPath, env = process.env) {
406
+ const path = resolveConfigPath(explicitPath, env);
407
+ const defaults = createDefaultAppConfig();
408
+ let parsed = {};
409
+ const exists = existsSync(path);
410
+ if (exists) {
411
+ try {
412
+ parsed = JSON.parse(readFileSync(path, "utf8"));
413
+ }
414
+ catch (error) {
415
+ throw new ConfigError(`无法解析 ${path}: ${error instanceof Error ? error.message : String(error)}`);
416
+ }
417
+ }
418
+ else if (explicitPath || env[CONFIG_ENV]) {
419
+ throw new ConfigError(`配置文件不存在: ${path}`);
420
+ }
421
+ const raw = parseRawConfig(parsed);
422
+ const instanceConfigs = mergeInstances(raw.instances);
423
+ const searchProviders = validateRoutes(instanceConfigs, raw.search.providers, "search");
424
+ const extractProviders = validateRoutes(instanceConfigs, raw.extract.providers, "extract");
425
+ const search = {
426
+ providers: searchProviders,
427
+ limit: raw.search.limit ?? defaults.search.limit,
428
+ timeoutMs: raw.search.timeoutMs ?? defaults.search.timeoutMs,
429
+ attemptTimeoutMs: raw.search.attemptTimeoutMs ?? defaults.search.attemptTimeoutMs,
430
+ maxResponseBytes: raw.search.maxResponseBytes ?? defaults.search.maxResponseBytes,
431
+ };
432
+ const extract = {
433
+ providers: extractProviders,
434
+ timeoutMs: raw.extract.timeoutMs ?? defaults.extract.timeoutMs,
435
+ attemptTimeoutMs: raw.extract.attemptTimeoutMs ?? defaults.extract.attemptTimeoutMs,
436
+ maxResponseBytes: raw.extract.maxResponseBytes ?? defaults.extract.maxResponseBytes,
437
+ minContentCharacters: raw.extract.minContentCharacters ?? defaults.extract.minContentCharacters,
438
+ };
439
+ const app = { providers: instanceConfigs, search, extract };
440
+ return {
441
+ path,
442
+ exists,
443
+ app,
444
+ instances: instanceConfigs.map((instance) => resolveProvider(instance, env)),
445
+ };
446
+ }
447
+ export function capabilitySupports(type, capability) {
448
+ return capability === "search"
449
+ ? SEARCH_TYPES.has(type)
450
+ : EXTRACT_TYPES.has(type);
451
+ }
452
+ export function getRoute(config, capability) {
453
+ return capability === "search"
454
+ ? config.search.providers
455
+ : config.extract.providers;
456
+ }
457
+ export function getCapabilityConfig(config, capability) {
458
+ return capability === "search" ? config.search : config.extract;
459
+ }
460
+ export function providerRef(instance) {
461
+ return { id: instance.id, type: instance.type };
462
+ }
463
+ //# sourceMappingURL=config.js.map