agent-hub-runner 0.3.2 → 0.4.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.
- package/README.md +2 -1
- package/autostart.mjs +1 -1
- package/package.json +2 -1
- package/runner.mjs +32 -2
- package/search.mjs +174 -0
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ npx -y agent-hub-runner@latest browser install # 给 bot 装它自己的浏览
|
|
|
57
57
|
npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
开机自启时,程序复制到 `~/.agent-runner/bin/<版本>/`,日志在 `~/.agent-runner/logs/runner.log`(超过 5MB
|
|
60
|
+
开机自启时,程序复制到 `~/.agent-runner/bin/<版本>/`,日志在 `~/.agent-runner/logs/runner.log`(超过 5MB 自动清空)。网页上提示「有新版本」时,0.3.2 起可以直接点「升级」;更早的版本按提示运行那一条升级命令。
|
|
61
61
|
|
|
62
62
|
仓库开发者(R699):要让仓库里的源码 `runner/runner.mjs` 直接连测试服、开机自启,用 `agent-hub/scripts/runner-test.sh autostart on|off | start | stop | status | log`(launchd 服务 `com.agenthub.runner.src`,日志 `~/.agent-runner/logs/runner-src.log`,不复制程序,改了代码 `start` 一下就生效)。普通用户不用管这个。
|
|
63
63
|
|
|
@@ -83,6 +83,7 @@ npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
|
83
83
|
- 开机自启报「Bootstrap failed: 5: Input/output error」:这个后台任务以前被 macOS 记成了「已禁用」。0.2.1 起 `autostart on` 会自己解除并重试,直接再运行一次 `npx -y agent-hub-runner@latest autostart on` 就行;还不行就按它给的那条 `launchctl enable …` 命令运行后再试,或重启电脑再试。
|
|
84
84
|
- 升级后网页还说「运行器版本太旧」:旧版本的运行器还在后台跑着(升级前开的终端,或者旧的开机自启),新版一看有人连着就退出了。0.2.2 起新版会自己把旧版本停掉、接上服务器;从 0.2.1 或更早升上来的,运行一次 `npx -y agent-hub-runner@latest autostart on` 就行,它会先停掉正在跑的那个,等新版本真连上了才说「已设开机自启」。
|
|
85
85
|
- 网页上点「升级」(0.3.2 起):抽屉里有新版本时点一下,运行器自己去 npm 拉最新版、校验后换上并重启,不用开终端。0.3.1 及更早不认这个按钮,仍用上面那条命令升一次。
|
|
86
|
+
- 联网搜索(0.4.0 起):bot 设置里打开「允许联网搜索」后,bot 可以上网查资料。默认用 DuckDuckGo,不用注册、不用 key。想更稳可以在「我的电脑」抽屉里换成 Tavily 或 Brave(去官网注册,把 key 粘进来),或者填一个 SearXNG 地址。key 只存在你电脑上,服务器不留。0.3.x 及更早没有这项,抽屉里会给升级命令。
|
|
86
87
|
|
|
87
88
|
## 断开、卸载
|
|
88
89
|
|
package/autostart.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export const THROTTLE_S = 10;
|
|
|
20
20
|
/** 复制到 ~/.agent-runner/bin/<版本>/ 的文件(package.json 用来读版本号) */
|
|
21
21
|
// @意图#R729(2026-11-04): 多复制 browser.mjs(runner.mjs 要 import 它)
|
|
22
22
|
// @意图#R690(2026-11-01): 多复制 changes.mjs(runner.mjs 要 import 它,少了开机自启的副本起不来)
|
|
23
|
-
export const COPY_FILES = ["runner.mjs", "ops.mjs", "project-context.mjs", "edit.mjs", "changes.mjs", "browser.mjs", "autostart.mjs", "package.json"];
|
|
23
|
+
export const COPY_FILES = ["runner.mjs", "ops.mjs", "project-context.mjs", "edit.mjs", "changes.mjs", "browser.mjs", "autostart.mjs", "search.mjs", "package.json"]; // search.mjs:R751 联网搜索(零依赖,拷文件即可)
|
|
24
24
|
/** 优先用的稳定 node 路径(Homebrew 升级 node 后这两个软链还在;nvm 的带版本号路径卸了就没了) */
|
|
25
25
|
export const STABLE_NODES = ["/opt/homebrew/bin/node", "/usr/local/bin/node"];
|
|
26
26
|
export const ONLY_MAC = "开机自启目前只支持 Mac 和 Windows";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "agent-hub 的电脑运行器:配对后让你的 bot 在这台电脑上读写授权目录里的文件、跑命令(跑命令要你在网页上批准),还能用它自己的独立浏览器",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"changes.mjs",
|
|
15
15
|
"browser.mjs",
|
|
16
16
|
"autostart.mjs",
|
|
17
|
+
"search.mjs",
|
|
17
18
|
"bin/agent-hub-runner.mjs",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
package/runner.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { fileURLToPath } from "node:url";
|
|
|
28
28
|
import { runOp } from "./ops.mjs";
|
|
29
29
|
import { buildChangeRecord, changesDirFor, CHANGE_ID_RE, createChangeStore } from "./changes.mjs";
|
|
30
30
|
import { browserStatus, createBrowserManager, installBrowser } from "./browser.mjs";
|
|
31
|
+
import { searchWeb, cleanSearchConfig, searchConfigStatus } from "./search.mjs"; // R751
|
|
31
32
|
import { autostartOff, autostartOffWindows, autostartOn, autostartOnWindows, autostartStatus, autostartStatusWindows, COPY_FILES, installCopy, integrityOk, logFileFor, ONLY_MAC, otherAutostarts, runnerHome, truncateLogIfBig } from "./autostart.mjs";
|
|
32
33
|
|
|
33
34
|
export const PROTOCOL_VERSION = 1;
|
|
@@ -306,7 +307,8 @@ const wsUrlOf = (server) => `${server.replace(/^http/, "ws")}/devices/ws`;
|
|
|
306
307
|
// 新消息 change_get {id, change_id} → change_result {id, ok:true, found, record},不看这个 bot 有没有目录(换了目录也能看旧记录)
|
|
307
308
|
// @意图#R729(2026-11-04): 新消息 browser {id, bot_id, action, args} → result(截图或元素清单)、browser_open {id, bot_id} → result(网页点「打开 bot 的浏览器」);
|
|
308
309
|
// 浏览器不看这个 bot 有没有选目录(资料目录在 ~/.agent-runner/browser-profiles/<bot_id>/,和授权目录无关);断线不关浏览器(重连后接着用)
|
|
309
|
-
export function createSession(cfg, { log = say, platform = process.platform, pickDir = platform === "win32" ? pickDirWindows : pickDirMac, file = configPath(), changes, browser } = {}) {
|
|
310
|
+
export function createSession(cfg, { log = say, platform = process.platform, pickDir = platform === "win32" ? pickDirWindows : pickDirMac, file = configPath(), changes, browser, search } = {}) {
|
|
311
|
+
const searchImpl = search ?? searchWeb; // R751:测试注入,默认真搜
|
|
310
312
|
let dirs = botDirsOf(cfg); // bot_id → 授权目录
|
|
311
313
|
let browsers = browser; // R729:每个 bot 一只浏览器,用到时才建
|
|
312
314
|
const browserMgr = () => (browsers ??= createBrowserManager({ log }));
|
|
@@ -362,6 +364,32 @@ export function createSession(cfg, { log = say, platform = process.platform, pic
|
|
|
362
364
|
send({ type: "result", id, ok: false, error: e?.message ?? String(e) });
|
|
363
365
|
}
|
|
364
366
|
}
|
|
367
|
+
// @意图#R751(2026-11-07): 联网搜索:不看 bot 有没有选目录(搜索不碰文件);按本机配置选 provider,key 只在这台电脑上
|
|
368
|
+
async function handleSearch(send, msg) {
|
|
369
|
+
const { id } = msg;
|
|
370
|
+
const bot = typeof msg.bot_id === "string" ? msg.bot_id : "";
|
|
371
|
+
const query = typeof msg.args?.query === "string" ? msg.args.query : "";
|
|
372
|
+
log(`[${bot}] 搜索:${query}`);
|
|
373
|
+
try {
|
|
374
|
+
const r = await searchImpl(query, { count: msg.args?.count, search: cfg.search });
|
|
375
|
+
send({ type: "result", id, ok: true, provider: r.provider, results: r.results, cached: r.cached === true });
|
|
376
|
+
} catch (e) {
|
|
377
|
+
send({ type: "result", id, ok: false, error: e?.message ?? String(e) });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// 网页保存搜索配置:key 只写本机配置文件,不回显;服务器只拿 provider/hasKey 状态
|
|
381
|
+
async function handleSearchConfig(send, msg) {
|
|
382
|
+
const { id } = msg;
|
|
383
|
+
try {
|
|
384
|
+
cfg.search = cleanSearchConfig(msg.config);
|
|
385
|
+
saveConfig(cfg, file);
|
|
386
|
+
log(`搜索配置已保存:${cfg.search.provider}${cfg.search.apiKey ? "(有 key)" : ""}`);
|
|
387
|
+
send({ type: "result", id, ok: true, ...searchConfigStatus(cfg.search) });
|
|
388
|
+
} catch (e) {
|
|
389
|
+
send({ type: "result", id, ok: false, error: e?.message ?? String(e) });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
365
393
|
// @意图#R729(2026-11-04): 网页点「打开 bot 的浏览器」:在这台电脑上弹出有窗口的浏览器(后台的换成有窗口的,恢复打开的网址)
|
|
366
394
|
async function handleBrowserOpen(send, msg) {
|
|
367
395
|
const { id } = msg;
|
|
@@ -412,6 +440,8 @@ export function createSession(cfg, { log = say, platform = process.platform, pic
|
|
|
412
440
|
if (msg.type === "change_get") return handleChangeGet(send, msg);
|
|
413
441
|
if (msg.type === "browser") return handleBrowser(send, msg); // R729
|
|
414
442
|
if (msg.type === "browser_open") return handleBrowserOpen(send, msg); // R729
|
|
443
|
+
if (msg.type === "search") return handleSearch(send, msg); // R751
|
|
444
|
+
if (msg.type === "search_config") return handleSearchConfig(send, msg); // R751
|
|
415
445
|
if (msg.type === "abort") { jobs.get(msg.id)?.abort(); picks.get(msg.id)?.abort(); }
|
|
416
446
|
return undefined;
|
|
417
447
|
},
|
|
@@ -462,7 +492,7 @@ export function startRunner(cfg, { log = say, retryMs = 2000, platform = process
|
|
|
462
492
|
ws = sock;
|
|
463
493
|
sock.addEventListener("open", () => {
|
|
464
494
|
send(sock, { type: "hello", version: PROTOCOL_VERSION, device_id: cfg.device_id, token: cfg.token, bot_dirs: session.botDirs,
|
|
465
|
-
name: cfg.name ?? os.hostname(), os: `${process.platform} ${os.release()}`, runner_version: RUNNER_VERSION, autostart: background, browser: browserInfo() });
|
|
495
|
+
name: cfg.name ?? os.hostname(), os: `${process.platform} ${os.release()}`, runner_version: RUNNER_VERSION, autostart: background, browser: browserInfo(), search: searchConfigStatus(cfg.search) });
|
|
466
496
|
beat = setInterval(() => send(sock, { type: "ping", browser: browserInfo() }), HEARTBEAT_MS);
|
|
467
497
|
});
|
|
468
498
|
sock.addEventListener("message", (ev) => {
|
package/search.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// @意图#R751(2026-11-07): king 确认 R751:运行器内置联网搜索,网页开关可选。
|
|
2
|
+
// 默认 DuckDuckGo 抓公开结果页(免注册免 key),可选 Tavily / Brave key、SearXNG 实例地址。
|
|
3
|
+
// key 只存本机 config.json(600),由网页下发(runner.mjs 的 search_config),服务器不存。
|
|
4
|
+
// 零依赖:DDG 的 HTML 解析自己写——开机自启安装只拷文件不装依赖(autostart.mjs 的 COPY_FILES),引第三方库会让自启那份跑不起来。
|
|
5
|
+
// 参考 pi-web-access 的 duckduckgo.ts 的做法(抓 html.duckduckgo.com/html/ 再解析);DDG 是非官方接口,改版/限流会失败,报错要说人话。
|
|
6
|
+
|
|
7
|
+
export const SEARCH_PROVIDERS = ["duckduckgo", "tavily", "brave", "searxng"];
|
|
8
|
+
export const SEARCH_CACHE_MS = 5 * 60_000; // 同一个查询 5 分钟内不重查,防模型刷搜索
|
|
9
|
+
export const SEARCH_TIMEOUT_MS = 20_000;
|
|
10
|
+
const DDG_URL = "https://html.duckduckgo.com/html/";
|
|
11
|
+
const UA = "Mozilla/5.0 (compatible; agent-hub-runner/1.0; +https://github.com/cs1145994973/agent-hub-runner)";
|
|
12
|
+
const CACHE_MAX = 50;
|
|
13
|
+
const cache = new Map(); // key → { at, results }
|
|
14
|
+
|
|
15
|
+
export function normalizeCount(n) {
|
|
16
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return 5;
|
|
17
|
+
return Math.max(1, Math.min(Math.floor(n), 10));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 去标签 + 解码常见 HTML 实体(DDG 的标题/摘要里有 <b> 和实体) */
|
|
21
|
+
export function decodeHtml(s) {
|
|
22
|
+
return String(s ?? "")
|
|
23
|
+
.replace(/<[^>]*>/g, "")
|
|
24
|
+
.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (m, ent) => {
|
|
25
|
+
const lower = ent.toLowerCase();
|
|
26
|
+
if (lower.startsWith("#x")) return String.fromCodePoint(parseInt(lower.slice(2), 16));
|
|
27
|
+
if (lower.startsWith("#")) return String.fromCodePoint(parseInt(lower.slice(1), 10));
|
|
28
|
+
const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
|
|
29
|
+
return named[lower] ?? m;
|
|
30
|
+
})
|
|
31
|
+
.replace(/\s+/g, " ")
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resultUrl(href) {
|
|
36
|
+
try {
|
|
37
|
+
const link = new URL(href, DDG_URL);
|
|
38
|
+
const dest = link.searchParams.get("uddg") ?? link.href;
|
|
39
|
+
const url = new URL(dest);
|
|
40
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.href : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 解析 DDG 结果页:按 class 含独立单词 result 的 div 分块,跳过广告块,提 result__a / result__snippet */
|
|
47
|
+
export function parseDuckDuckGo(html, count = 5) {
|
|
48
|
+
const starts = [...String(html).matchAll(/<div[^>]*class="[^"]*\bresult\b[^"]*"[^>]*>/g)];
|
|
49
|
+
const results = [];
|
|
50
|
+
let parseable = 0;
|
|
51
|
+
for (let i = 0; i < starts.length && results.length < count; i++) {
|
|
52
|
+
const begin = starts[i].index;
|
|
53
|
+
const end = i + 1 < starts.length ? starts[i + 1].index : html.length;
|
|
54
|
+
const block = html.slice(begin, end);
|
|
55
|
+
if (/result--ad/.test(starts[i][0])) continue;
|
|
56
|
+
const a = block.match(/<a\b[^>]*result__a[^>]*>([\s\S]*?)<\/a>/);
|
|
57
|
+
if (!a) continue;
|
|
58
|
+
const href = a[0].match(/href="([^"]+)"/)?.[1] ?? "";
|
|
59
|
+
const title = decodeHtml(a[1]);
|
|
60
|
+
const url = href ? resultUrl(href) : null;
|
|
61
|
+
if (!title || !url) continue;
|
|
62
|
+
parseable++;
|
|
63
|
+
const snip = block.match(/<(?:a|div)\b[^>]*result__snippet[^>]*>([\s\S]*?)<\/(?:a|div)>/);
|
|
64
|
+
results.push({ title, url, snippet: snip ? decodeHtml(snip[1]) : "" });
|
|
65
|
+
}
|
|
66
|
+
if (parseable === 0) throw new Error("DuckDuckGo 这次没解析到结果:可能被限流或页面结构变了。可以换个说法再试、稍后再试,或在「我的电脑」里改配 Tavily / Brave key。");
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function request(fetchImpl, url, init, signal) {
|
|
71
|
+
const res = await fetchImpl(url, {
|
|
72
|
+
...init,
|
|
73
|
+
signal: signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
77
|
+
throw new Error(`搜索请求失败(HTTP ${res.status})${res.status === 403 || res.status === 429 ? ":可能被限流,稍后再试" : ""}${body ? `:${body}` : ""}`);
|
|
78
|
+
}
|
|
79
|
+
return res;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function searchDuckDuckGo(query, { count, fetchImpl, signal }) {
|
|
83
|
+
const url = new URL(DDG_URL);
|
|
84
|
+
url.searchParams.set("q", query);
|
|
85
|
+
const res = await request(fetchImpl, url, { headers: { Accept: "text/html", "User-Agent": UA } }, signal);
|
|
86
|
+
return parseDuckDuckGo(await res.text(), count);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function searchTavily(query, { count, apiKey, fetchImpl, signal }) {
|
|
90
|
+
const res = await request(fetchImpl, "https://api.tavily.com/search", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
93
|
+
body: JSON.stringify({ query, max_results: count }),
|
|
94
|
+
}, signal);
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
return (data.results ?? []).slice(0, count).map((r) => ({ title: String(r.title ?? ""), url: String(r.url ?? ""), snippet: String(r.content ?? "") }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function searchBrave(query, { count, apiKey, fetchImpl, signal }) {
|
|
100
|
+
const url = new URL("https://api.search.brave.com/res/v1/web/search");
|
|
101
|
+
url.searchParams.set("q", query);
|
|
102
|
+
url.searchParams.set("count", String(count));
|
|
103
|
+
const res = await request(fetchImpl, url, { headers: { Accept: "application/json", "X-Subscription-Token": apiKey } }, signal);
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
return (data.web?.results ?? []).slice(0, count).map((r) => ({ title: String(r.title ?? ""), url: String(r.url ?? ""), snippet: String(r.description ?? "") }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function searchSearxng(query, { count, searxngUrl, fetchImpl, signal }) {
|
|
109
|
+
const base = String(searxngUrl).replace(/\/+$/, "");
|
|
110
|
+
const url = new URL(`${base}/search`);
|
|
111
|
+
url.searchParams.set("q", query);
|
|
112
|
+
url.searchParams.set("format", "json");
|
|
113
|
+
const res = await request(fetchImpl, url, { headers: { Accept: "application/json", "User-Agent": UA } }, signal);
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
return (data.results ?? []).slice(0, count).map((r) => ({ title: String(r.title ?? ""), url: String(r.url ?? ""), snippet: String(r.content ?? "") }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 网页下发的配置写本地前的校验(runner.mjs 的 search_config 也用) */
|
|
119
|
+
export function cleanSearchConfig(raw) {
|
|
120
|
+
const provider = SEARCH_PROVIDERS.includes(raw?.provider) ? raw.provider : "duckduckgo";
|
|
121
|
+
const out = { provider };
|
|
122
|
+
if (provider === "tavily" || provider === "brave") {
|
|
123
|
+
const key = typeof raw?.apiKey === "string" ? raw.apiKey.trim() : "";
|
|
124
|
+
if (!key) throw new Error(`${provider} 要填 API key`);
|
|
125
|
+
if (key.length > 200) throw new Error("API key 太长了,检查一下是不是复制错了");
|
|
126
|
+
out.apiKey = key;
|
|
127
|
+
}
|
|
128
|
+
if (provider === "searxng") {
|
|
129
|
+
const url = String(raw?.searxngUrl ?? "").trim();
|
|
130
|
+
const u = new URL(url); // 不合法直接抛
|
|
131
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("SearXNG 地址要以 http(s):// 开头");
|
|
132
|
+
out.searxngUrl = url;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function searchConfigStatus(search) {
|
|
138
|
+
const provider = SEARCH_PROVIDERS.includes(search?.provider) ? search.provider : "duckduckgo";
|
|
139
|
+
return { provider, hasKey: typeof search?.apiKey === "string" && search.apiKey.length > 0 };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 搜一次。opts: { count, search(config.json 的 search 节), fetchImpl(测试注入), signal }
|
|
144
|
+
* 返回 { provider, results: [{ title, url, snippet }] };失败抛中文错误。
|
|
145
|
+
*/
|
|
146
|
+
export async function searchWeb(query, opts = {}) {
|
|
147
|
+
const q = String(query ?? "").trim();
|
|
148
|
+
if (!q) throw new Error("搜索词不能为空");
|
|
149
|
+
const search = opts.search ?? {};
|
|
150
|
+
const provider = SEARCH_PROVIDERS.includes(search.provider) ? search.provider : "duckduckgo";
|
|
151
|
+
const count = normalizeCount(opts.count);
|
|
152
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
153
|
+
const cacheKey = `${provider}|${provider === "searxng" ? search.searxngUrl ?? "" : ""}|${count}|${q}`;
|
|
154
|
+
const hit = cache.get(cacheKey);
|
|
155
|
+
if (hit && Date.now() - hit.at < SEARCH_CACHE_MS) return { provider, results: hit.results, cached: true };
|
|
156
|
+
|
|
157
|
+
const args = { count, apiKey: search.apiKey, searxngUrl: search.searxngUrl, fetchImpl, signal: opts.signal };
|
|
158
|
+
let results;
|
|
159
|
+
if (provider === "tavily") {
|
|
160
|
+
if (!args.apiKey) throw new Error("选了 Tavily 但没配 key:在「我的电脑」里填上,或切回 DuckDuckGo。");
|
|
161
|
+
results = await searchTavily(q, args);
|
|
162
|
+
} else if (provider === "brave") {
|
|
163
|
+
if (!args.apiKey) throw new Error("选了 Brave 但没配 key:在「我的电脑」里填上,或切回 DuckDuckGo。");
|
|
164
|
+
results = await searchBrave(q, args);
|
|
165
|
+
} else if (provider === "searxng") {
|
|
166
|
+
if (!args.searxngUrl) throw new Error("选了 SearXNG 但没填实例地址:在「我的电脑」里填上,或切回 DuckDuckGo。");
|
|
167
|
+
results = await searchSearxng(q, args);
|
|
168
|
+
} else {
|
|
169
|
+
results = await searchDuckDuckGo(q, args);
|
|
170
|
+
}
|
|
171
|
+
cache.set(cacheKey, { at: Date.now(), results });
|
|
172
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
173
|
+
return { provider, results };
|
|
174
|
+
}
|