dsh-free-search 0.4.9 → 0.4.10
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 -2
- package/lib/client.js +4 -4
- package/lib/index.js +58 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
36
36
|
- **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
|
|
37
37
|
- **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
|
|
38
38
|
- **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
|
|
39
|
-
- **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.
|
|
39
|
+
- **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.10),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
|
|
40
40
|
- **结果缓存** —— 相同查询(含引擎/时间过滤参数)5 分钟内命中缓存(LRU 50 条),防免费引擎限流、省付费额度;时长可在设置页 0-5 分钟自由配置(0 关闭)
|
|
41
41
|
- **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
|
|
42
42
|
- **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
|
|
@@ -271,7 +271,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
271
271
|
- **Unified Engine Fallback** — Any engine failure (paid or free, missing key, 401, rate limit, network error) automatically tries the next engine: the configured engine first, then other engines (exa/tavily/keenable are tried even without a key because they have built-in keyless quota), then the remaining free engines (Bing/AnySearch etc.) — with a note attached to the results naming the engine that actually served them (e.g. `Note: perplexity unavailable or failed, using exa.`). Search never fails outright.
|
|
272
272
|
- **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
|
|
273
273
|
- **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
|
|
274
|
-
- **Version + Update Check** — The settings card shows the current version (v0.4.
|
|
274
|
+
- **Version + Update Check** — The settings card shows the current version (v0.4.10), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
|
|
275
275
|
- **Result Caching** — Identical queries (same engine / time-filter args) hit an LRU cache (50 entries) for up to 5 minutes, protecting free engines from rate-limiting and saving paid quota; the TTL is configurable from 0-5 minutes in the settings UI (0 disables caching)
|
|
276
276
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
277
277
|
- **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
|
package/lib/client.js
CHANGED
|
@@ -142,7 +142,7 @@ window.__ModuleLoader__.load({
|
|
|
142
142
|
};
|
|
143
143
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
144
144
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
145
|
-
const PLUGIN_VERSION = "0.4.
|
|
145
|
+
const PLUGIN_VERSION = "0.4.10";
|
|
146
146
|
const ENGINES = [
|
|
147
147
|
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
148
148
|
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
@@ -201,7 +201,7 @@ window.__ModuleLoader__.load({
|
|
|
201
201
|
const [keenableKey, setKeenableKey] = react.useState("");
|
|
202
202
|
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
203
203
|
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
204
|
-
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili"]);
|
|
204
|
+
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit"]);
|
|
205
205
|
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
206
206
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
207
207
|
const [lang, setLang] = react.useState("zh");
|
|
@@ -227,7 +227,7 @@ window.__ModuleLoader__.load({
|
|
|
227
227
|
setKeenableKey(v.keenableApiKey ?? "");
|
|
228
228
|
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
229
229
|
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
230
|
-
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili"]);
|
|
230
|
+
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit"]);
|
|
231
231
|
setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
|
|
232
232
|
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
233
233
|
const configured = {};
|
|
@@ -515,7 +515,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
515
515
|
react_jsx_runtime.jsx("div", {
|
|
516
516
|
className: "dshfs-platforms",
|
|
517
517
|
children: [
|
|
518
|
-
["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"],
|
|
518
|
+
["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"], ["reddit", "Reddit"],
|
|
519
519
|
].map(([id, label]) =>
|
|
520
520
|
react_jsx_runtime.jsx("label", {
|
|
521
521
|
className: "dshfs-platform",
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
|
|
|
18
18
|
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "perplexity", "deepseek-official"];
|
|
19
19
|
|
|
20
20
|
// 当前插件版本(发布时与 package.json 同步)
|
|
21
|
-
const PLUGIN_VERSION = "0.4.
|
|
21
|
+
const PLUGIN_VERSION = "0.4.10";
|
|
22
22
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
23
23
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
24
24
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -115,6 +115,8 @@ function decodeEntities(text) {
|
|
|
115
115
|
|
|
116
116
|
//#region 结果缓存(防限流/省额度,LRU 50 条,TTL 可配置 0-5 分钟)
|
|
117
117
|
const CACHE_MAX_ENTRIES = 50;
|
|
118
|
+
// fallback 条目(实际引擎 ≠ 首选引擎时)TTL = 配置 TTL 的 1/5(默认 5 分钟 → 60s):
|
|
119
|
+
// 首选引擎恢复后最多 1 分钟即可拿到新结果,避免回退结果被完整 TTL 钉死;首选成功条目仍用完整 TTL。
|
|
118
120
|
|
|
119
121
|
function buildCacheKey(query, maxResults, timeRangeLabel, preferred) {
|
|
120
122
|
return [query ?? "", maxResults ?? 5, timeRangeLabel ?? "", preferred].join("\u0000");
|
|
@@ -302,7 +304,8 @@ async function searchSearxng(query, maxResults, options, signal) {
|
|
|
302
304
|
const instances = options?.searxngInstances?.length
|
|
303
305
|
? options.searxngInstances
|
|
304
306
|
: SEARXNG_INSTANCES;
|
|
305
|
-
|
|
307
|
+
// 聚合所有实例的失败原因,避免只显示最后一个实例的错误
|
|
308
|
+
const errors = [];
|
|
306
309
|
for (const base of instances) {
|
|
307
310
|
try {
|
|
308
311
|
const params = new URLSearchParams({ q: query, format: "json" });
|
|
@@ -322,12 +325,12 @@ async function searchSearxng(query, maxResults, options, signal) {
|
|
|
322
325
|
clearTimeout(timer);
|
|
323
326
|
signal?.removeEventListener("abort", onAbort);
|
|
324
327
|
if (!response.ok) {
|
|
325
|
-
|
|
328
|
+
errors.push(`${base}: HTTP ${response.status}`);
|
|
326
329
|
continue;
|
|
327
330
|
}
|
|
328
331
|
const data = await response.json().catch(() => null);
|
|
329
332
|
if (!data || !Array.isArray(data.results)) {
|
|
330
|
-
|
|
333
|
+
errors.push(`${base}: invalid JSON`);
|
|
331
334
|
continue;
|
|
332
335
|
}
|
|
333
336
|
const sources = data.results
|
|
@@ -340,12 +343,16 @@ async function searchSearxng(query, maxResults, options, signal) {
|
|
|
340
343
|
if (sources.length > 0) {
|
|
341
344
|
return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
|
|
342
345
|
}
|
|
343
|
-
|
|
346
|
+
errors.push(`${base}: 0 results`);
|
|
344
347
|
} catch (error) {
|
|
345
|
-
|
|
348
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
349
|
+
errors.push(`${base}: ${message}`);
|
|
346
350
|
}
|
|
347
351
|
}
|
|
348
|
-
|
|
352
|
+
// 空实例列表兜底:避免 "all SearXNG instances failed: " 尾巴悬空
|
|
353
|
+
const detail = errors.length > 0 ? errors.join(", ") : "no instances configured";
|
|
354
|
+
// Note 会引用这个错误消息,截断避免 6 实例全挂时刷屏
|
|
355
|
+
throw new Error(`all SearXNG instances failed: ${detail.slice(0, 300)}`);
|
|
349
356
|
}
|
|
350
357
|
//#endregion
|
|
351
358
|
|
|
@@ -450,7 +457,8 @@ async function searchExaMCP(query, maxResults, signal) {
|
|
|
450
457
|
url,
|
|
451
458
|
...(title ? { title } : {}),
|
|
452
459
|
...(highlights ? { snippet: highlights.slice(0, 300) } : {}),
|
|
453
|
-
|
|
460
|
+
// 只保留日期形态(ISO 或 YYYY-MM-DD),过滤 "N/A" 等占位符
|
|
461
|
+
...(published && /^\d{4}-\d{2}-\d{2}/.test(published) ? { publishedAt: published } : {}),
|
|
454
462
|
});
|
|
455
463
|
}
|
|
456
464
|
return { sources, truncated: false };
|
|
@@ -462,6 +470,7 @@ const PLATFORMS = {
|
|
|
462
470
|
github: { name: "GitHub" },
|
|
463
471
|
v2ex: { name: "V2EX" },
|
|
464
472
|
bilibili: { name: "Bilibili" },
|
|
473
|
+
reddit: { name: "Reddit" },
|
|
465
474
|
};
|
|
466
475
|
|
|
467
476
|
async function searchGithub(query, maxResults, signal) {
|
|
@@ -700,7 +709,8 @@ function extractKeenableSources(text, maxResults) {
|
|
|
700
709
|
url,
|
|
701
710
|
...(title ? { title } : {}),
|
|
702
711
|
...(snippets ? { snippet: snippets.slice(0, 300) } : {}),
|
|
703
|
-
|
|
712
|
+
// 与 Exa MCP 一致:只保留日期形态,过滤 "N/A" 等占位符
|
|
713
|
+
...(published && /^\d{4}-\d{2}-\d{2}/.test(published) ? { publishedAt: published } : {}),
|
|
704
714
|
});
|
|
705
715
|
}
|
|
706
716
|
return uniqueSources(sources, maxResults ?? 10);
|
|
@@ -799,6 +809,7 @@ async function searchKeenable(query, maxResults, apiKey, timeRange, signal) {
|
|
|
799
809
|
|
|
800
810
|
async function searchPerplexity(query, maxResults, apiKey, signal) {
|
|
801
811
|
if (!apiKey) throw new Error("Perplexity search requires PERPLEXITY_API_KEY");
|
|
812
|
+
// 内置 20s 超时(与外部 signal 组合):调用方不传 signal 时也不会永久卡住
|
|
802
813
|
const response = await fetch("https://api.perplexity.ai/chat/completions", {
|
|
803
814
|
method: "POST",
|
|
804
815
|
redirect: "error",
|
|
@@ -812,7 +823,7 @@ async function searchPerplexity(query, maxResults, apiKey, signal) {
|
|
|
812
823
|
max_tokens: 1024,
|
|
813
824
|
messages: [{ role: "user", content: query }],
|
|
814
825
|
}),
|
|
815
|
-
...(signal !== undefined ?
|
|
826
|
+
signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(20000)]),
|
|
816
827
|
});
|
|
817
828
|
if (!response.ok) {
|
|
818
829
|
const detail = await response.text().catch(() => "");
|
|
@@ -834,6 +845,7 @@ async function searchPerplexity(query, maxResults, apiKey, signal) {
|
|
|
834
845
|
|
|
835
846
|
async function searchDeepSeekOfficial(query, maxResults, apiKey, signal) {
|
|
836
847
|
if (!apiKey) throw new Error("DeepSeek search requires DEEPSEEK_API_KEY");
|
|
848
|
+
// 内置 20s 超时(与外部 signal 组合):调用方不传 signal 时也不会永久卡住
|
|
837
849
|
const response = await fetch("https://api.deepseek.com/anthropic/v1/messages", {
|
|
838
850
|
method: "POST",
|
|
839
851
|
redirect: "error",
|
|
@@ -856,7 +868,7 @@ async function searchDeepSeekOfficial(query, maxResults, apiKey, signal) {
|
|
|
856
868
|
],
|
|
857
869
|
tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 1 }],
|
|
858
870
|
}),
|
|
859
|
-
...(signal !== undefined ?
|
|
871
|
+
signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(20000)]),
|
|
860
872
|
});
|
|
861
873
|
if (!response.ok) {
|
|
862
874
|
const detail = await response.text().catch(() => "");
|
|
@@ -1140,7 +1152,7 @@ const Config = z.object({
|
|
|
1140
1152
|
region: z.string(),
|
|
1141
1153
|
bingMarket: z.string().default("zh-CN"),
|
|
1142
1154
|
searxngInstances: z.array(z.string()),
|
|
1143
|
-
platforms: z.array(z.string()).default(["github", "v2ex", "bilibili"]),
|
|
1155
|
+
platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit"]),
|
|
1144
1156
|
exaApiKey: z.string().role("secret"),
|
|
1145
1157
|
tavilyApiKey: z.string().role("secret"),
|
|
1146
1158
|
keenableApiKey: z.string().role("secret"),
|
|
@@ -1182,6 +1194,10 @@ function apply(ctx, config) {
|
|
|
1182
1194
|
},
|
|
1183
1195
|
// 单 query 结果缓存:key=query+maxResults+timeRangeLabel+preferred,Map 天然 LRU
|
|
1184
1196
|
async search(request, signal) {
|
|
1197
|
+
// 公共咽喉校验:web_search / advanced_search / raw-search 三条路径都经过这里
|
|
1198
|
+
if (request === null || typeof request !== "object" || typeof request.query !== "string" || request.query.trim().length === 0) {
|
|
1199
|
+
throw new Error("query is required");
|
|
1200
|
+
}
|
|
1185
1201
|
const cfg = current();
|
|
1186
1202
|
// 首选引擎:free_search 工具显式指定(request.engine)优先于设置(cfg.provider)
|
|
1187
1203
|
const preferred =
|
|
@@ -1205,9 +1221,8 @@ function apply(ctx, config) {
|
|
|
1205
1221
|
if (signal?.aborted) throw new Error("search aborted");
|
|
1206
1222
|
searchCache.delete(cacheKey);
|
|
1207
1223
|
searchCache.set(cacheKey, hit);
|
|
1208
|
-
// 浅拷贝 +
|
|
1209
|
-
|
|
1210
|
-
return { ...hit.value, _cache: "hit" };
|
|
1224
|
+
// 浅拷贝 + 私有标记:sources 数组也复制一层,彻底隔离缓存对象(调用方 push/改元素不影响缓存)
|
|
1225
|
+
return { ...hit.value, sources: hit.value.sources?.slice(), _cache: "hit" };
|
|
1211
1226
|
}
|
|
1212
1227
|
if (hit) searchCache.delete(cacheKey);
|
|
1213
1228
|
}
|
|
@@ -1243,35 +1258,44 @@ function apply(ctx, config) {
|
|
|
1243
1258
|
let usedEngine = null;
|
|
1244
1259
|
// 首选引擎若被尝试后失败,记录失败详情(用于 Note)
|
|
1245
1260
|
let preferredFailure = null;
|
|
1261
|
+
// 总超时预算:串行回退时限制整条引擎链的总时长,防止各引擎超时累加达分钟级
|
|
1262
|
+
const BUDGET_MS = 30000;
|
|
1263
|
+
const deadline = Date.now() + BUDGET_MS;
|
|
1246
1264
|
for (const engine of chain) {
|
|
1265
|
+
const remaining = deadline - Date.now();
|
|
1266
|
+
if (remaining <= 0) {
|
|
1267
|
+
throw new Error(`search timed out after ${BUDGET_MS / 1000}s`);
|
|
1268
|
+
}
|
|
1269
|
+
// 组合外部取消 signal + 剩余预算超时:官方 web_search 取消、引擎超时、总预算都能触发
|
|
1270
|
+
const effSignal = AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(remaining)]);
|
|
1247
1271
|
try {
|
|
1248
1272
|
let result;
|
|
1249
1273
|
if (engine === "ddg") {
|
|
1250
|
-
result = await searchDdgHtml(request.query, request.maxResults, { ...cfg, timeRange },
|
|
1274
|
+
result = await searchDdgHtml(request.query, request.maxResults, { ...cfg, timeRange }, effSignal);
|
|
1251
1275
|
} else if (engine === "ddg-lite") {
|
|
1252
|
-
result = await searchDdgLite(request.query, request.maxResults, { ...cfg, timeRange },
|
|
1276
|
+
result = await searchDdgLite(request.query, request.maxResults, { ...cfg, timeRange }, effSignal);
|
|
1253
1277
|
} else if (engine === "bing") {
|
|
1254
|
-
result = await searchBing(request.query, request.maxResults, cfg,
|
|
1278
|
+
result = await searchBing(request.query, request.maxResults, cfg, effSignal);
|
|
1255
1279
|
} else if (engine === "searxng") {
|
|
1256
|
-
result = await searchSearxng(request.query, request.maxResults, { ...cfg, timeRange },
|
|
1280
|
+
result = await searchSearxng(request.query, request.maxResults, { ...cfg, timeRange }, effSignal);
|
|
1257
1281
|
} else if (engine === "anysearch") {
|
|
1258
|
-
result = await searchAnysearch(request.query, request.maxResults,
|
|
1282
|
+
result = await searchAnysearch(request.query, request.maxResults, effSignal);
|
|
1259
1283
|
} else if (engine === "exa") {
|
|
1260
1284
|
// exa:有 key 走 REST,无 key 走 keyless MCP(免费)
|
|
1261
1285
|
const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
|
|
1262
1286
|
if (key) {
|
|
1263
|
-
result = await searchExa(request.query, request.maxResults, key, timeRange,
|
|
1287
|
+
result = await searchExa(request.query, request.maxResults, key, timeRange, effSignal);
|
|
1264
1288
|
} else {
|
|
1265
|
-
result = await searchExaMCP(request.query, request.maxResults,
|
|
1289
|
+
result = await searchExaMCP(request.query, request.maxResults, effSignal);
|
|
1266
1290
|
}
|
|
1267
1291
|
} else if (engine === "tavily") {
|
|
1268
1292
|
// tavily:有 key 走账号档,无 key 走 keyless(免费匿名额度)
|
|
1269
1293
|
const key = await resolveApiKey("TAVILY_API_KEY", "tavilyApiKey");
|
|
1270
|
-
result = await searchTavily(request.query, request.maxResults, key, timeRange,
|
|
1294
|
+
result = await searchTavily(request.query, request.maxResults, key, timeRange, effSignal);
|
|
1271
1295
|
} else if (engine === "keenable") {
|
|
1272
1296
|
// keenable:有 key 走 REST,无 key 走 keyless MCP(免费)
|
|
1273
1297
|
const key = await resolveApiKey("KEENABLE_API_KEY", "keenableApiKey");
|
|
1274
|
-
result = await searchKeenable(request.query, request.maxResults, key, timeRange,
|
|
1298
|
+
result = await searchKeenable(request.query, request.maxResults, key, timeRange, effSignal);
|
|
1275
1299
|
} else if (engine === "perplexity") {
|
|
1276
1300
|
const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
|
|
1277
1301
|
if (!key) {
|
|
@@ -1280,7 +1304,7 @@ function apply(ctx, config) {
|
|
|
1280
1304
|
logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
|
|
1281
1305
|
continue; // 无 key 跳过
|
|
1282
1306
|
}
|
|
1283
|
-
result = await searchPerplexity(request.query, request.maxResults, key,
|
|
1307
|
+
result = await searchPerplexity(request.query, request.maxResults, key, effSignal);
|
|
1284
1308
|
} else if (engine === "deepseek-official") {
|
|
1285
1309
|
const key = await resolveApiKey("DEEPSEEK_API_KEY", "deepseekApiKey");
|
|
1286
1310
|
if (!key) {
|
|
@@ -1289,7 +1313,7 @@ function apply(ctx, config) {
|
|
|
1289
1313
|
logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
|
|
1290
1314
|
continue; // 无 key 跳过
|
|
1291
1315
|
}
|
|
1292
|
-
result = await searchDeepSeekOfficial(request.query, request.maxResults, key,
|
|
1316
|
+
result = await searchDeepSeekOfficial(request.query, request.maxResults, key, effSignal);
|
|
1293
1317
|
} else {
|
|
1294
1318
|
continue;
|
|
1295
1319
|
}
|
|
@@ -1313,9 +1337,11 @@ function apply(ctx, config) {
|
|
|
1313
1337
|
// 写入缓存(只缓存成功结果,失败走 throw 天然不缓存)
|
|
1314
1338
|
const cached = { ...result, provider: engine, engine: engine };
|
|
1315
1339
|
if (cacheKey !== null) {
|
|
1340
|
+
// fallback 条目(实际引擎≠首选)用配置 TTL 的 1/5,首选成功保持完整 TTL
|
|
1341
|
+
const entryTtlMs = engine !== preferred ? Math.max(cacheTtlMs / 5, 1000) : cacheTtlMs;
|
|
1316
1342
|
searchCache.set(cacheKey, {
|
|
1317
1343
|
value: cached,
|
|
1318
|
-
expiresAt: Date.now() +
|
|
1344
|
+
expiresAt: Date.now() + entryTtlMs,
|
|
1319
1345
|
});
|
|
1320
1346
|
if (searchCache.size > CACHE_MAX_ENTRIES) {
|
|
1321
1347
|
const oldest = searchCache.keys().next().value;
|
|
@@ -1514,18 +1540,18 @@ function apply(ctx, config) {
|
|
|
1514
1540
|
}, "free-search: test engines tool");
|
|
1515
1541
|
});
|
|
1516
1542
|
|
|
1517
|
-
// 平台搜索工具:GitHub / V2EX / Bilibili(公开 API,零依赖)
|
|
1543
|
+
// 平台搜索工具:GitHub / V2EX / Bilibili / Reddit(公开 API,零依赖)
|
|
1518
1544
|
ctx.inject(["tools"], (sctx) => {
|
|
1519
1545
|
sctx.effect(() => {
|
|
1520
1546
|
const dispose = sctx.tools.register(
|
|
1521
1547
|
defineTool({
|
|
1522
1548
|
name: "platform_search",
|
|
1523
1549
|
description:
|
|
1524
|
-
"Search a specific platform (GitHub / V2EX / Bilibili) for a query. Returns source URLs with titles and snippets. Use this when the user asks about repos, code, forum threads, or
|
|
1550
|
+
"Search a specific platform (GitHub / V2EX / Bilibili / Reddit) for a query. Returns source URLs with titles and snippets. Use this when the user asks about repos, code, forum threads, videos, or discussions.",
|
|
1525
1551
|
parameters: {
|
|
1526
1552
|
platform: {
|
|
1527
1553
|
type: "string",
|
|
1528
|
-
description: "Platform to search: github, v2ex, bilibili",
|
|
1554
|
+
description: "Platform to search: github, v2ex, bilibili, reddit",
|
|
1529
1555
|
},
|
|
1530
1556
|
query: {
|
|
1531
1557
|
type: "string",
|
|
@@ -1567,7 +1593,7 @@ function apply(ctx, config) {
|
|
|
1567
1593
|
throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
1568
1594
|
}
|
|
1569
1595
|
// 平台开关:settings 里禁用某平台时,工具明确告知
|
|
1570
|
-
const enabled = current().platforms ?? ["github", "v2ex", "bilibili"];
|
|
1596
|
+
const enabled = current().platforms ?? ["github", "v2ex", "bilibili", "reddit"];
|
|
1571
1597
|
if (!enabled.includes(platform)) {
|
|
1572
1598
|
throw new Error(
|
|
1573
1599
|
`platform "${platform}" is disabled in Free Search settings - enable it in Settings > Plugins > Free Search to use it`
|
|
@@ -1653,6 +1679,7 @@ function apply(ctx, config) {
|
|
|
1653
1679
|
},
|
|
1654
1680
|
},
|
|
1655
1681
|
async execute(args) {
|
|
1682
|
+
if (!args.query || !String(args.query).trim()) throw new Error("query is required");
|
|
1656
1683
|
const request = {
|
|
1657
1684
|
query: args.query,
|
|
1658
1685
|
maxResults: Math.min(args.maxResults ?? 5, 10),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"description": "Free web search for DeepSeek Harness: 10 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable keyless) + time filtering + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|