dsh-free-search 0.9.22 → 0.9.23
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 +42 -4
- package/lib/client.js +166 -6
- package/lib/index.js +74 -33
- package/lib/prompt_i18n.js +366 -312
- package/lib/research.js +5 -4
- package/lib/vertical.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,22 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
31
31
|
- **零成本** —— 多个免费引擎,无需 key、无需注册
|
|
32
32
|
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、Serply、DeepSeek 官方、DeepSeek 原生搜索 API、You.com、百度千帆、Kimi、阿里云百炼、火山联网搜索(豆包搜索)、知乎全网/站内(`zhihu_global`/`zhihu_site`,经知乎开放平台 MCP)、OpenAI / Gemini / Claude 模型内置搜索
|
|
33
33
|
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换;入口在左侧「插件」页的组件行配置(`plugins.row.config`,DSH 0.1.7-rc.1+)。远程访问(VPS / 反向代理 / 自定义域名)下也能用:桥接接口沿用宿主的 Host 围栏,`--trusted-host` 与 `--public-url` 声明的地址会放行(#84)
|
|
34
|
-
- **右侧浮动目录** —— 卡片右侧的页面留白里浮出「引擎 / 回退 / 语言池 / 搜索行为 / 抓取 /
|
|
34
|
+
- **右侧浮动目录** —— 卡片右侧的页面留白里浮出「引擎 / 回退 / 语言池 / 搜索行为 / 抓取 / 性能 / 提示词」七个快捷跳转,滚动自动高亮当前项、点击平滑跳转;不占用正文宽度(正文布局与 0.9.6 一致),右侧留白不足时自动隐藏;每项都是真链接(`#/panel/plugins/dsh-free-search/<分区>`),可复制、可开新标签,见下方「分区链接契约」
|
|
35
|
+
|
|
36
|
+
### 提示词分页
|
|
37
|
+
|
|
38
|
+
设置页第七段「提示词」里,页 1 是内置英文,页 2 是内置中文,页 3 起是你自己写的页。注入的就是你停的那一页,停在空页等于不注入。内置页由配置实时生成、不入盘,不能删也不能改。工具开关关掉的工具不注册,schema 一起从上下文消失。
|
|
39
|
+
|
|
40
|
+
### 分区链接契约(与 dsh-url-router 对接)
|
|
41
|
+
|
|
42
|
+
规则见 `dsh-url-router/docs/panel-sections.md`(数量不限,当前 7 段:engines / fallback / pools / behavior / fetch / performance / prompt):
|
|
43
|
+
|
|
44
|
+
- 页面根发布**恰好一个** `data-dsh-panel-section="<id>"` = 读者正在看的那一节(跟着滚动走,不做“到底=最后一节”的猜测)
|
|
45
|
+
- 每个分区容器带 `data-dsh-section="<id>"`(滚动目标)
|
|
46
|
+
- 侧栏每项是 `<a data-dsh-section="<id>" href="#/panel/plugins/dsh-free-search/<id>">`
|
|
47
|
+
- 本插件**不写 URL**:`location.hash` / `history.pushState` / `replaceState` 一律不碰,地址栏唯一写者是 dsh-url-router;地址栏里出现本页分区链接时(`hashchange`)插件负责把它滚进视野
|
|
48
|
+
|
|
49
|
+
加新分区只需三步:容器加 `data-dsh-section="<新id>"`、`NAV_ITEMS` 加一条(侧栏锚点自动生成,href 由 `item.id` 拼)、`assert-rail-section` 的 id 列表加一项。
|
|
35
50
|
- **多 key 轮换** —— 任何 key 字段(或对应环境变量)都可以用逗号 / 分号 / 换行分隔多把 key;平时按请求轮流使用(round-robin,避免单 key 撞 QPS/RPM 风控),遇到 401/402/429(鉴权失败、额度耗尽、限流)先轮换同一引擎的下一把,再走引擎回退;`multi` 模式同样生效(#96)
|
|
36
51
|
- **弹出式切换命令** —— 聊天框输入 `/free-search-engine`,弹出引擎选择窗口,点选即切换(等效设置页 + 保存)
|
|
37
52
|
- **引擎测试** —— `free_search_test` 工具让 agent 一键测试所有引擎;设置页也有"测试引擎"按钮(直测当前引擎,不走回退链,付费引擎无 key 会明确报错)
|
|
@@ -314,6 +329,9 @@ lang: zh # 搜索语言池/本地化(zh / en / ru …,不
|
|
|
314
329
|
# web_fetch(fs-quality-fetch)的 Accept-Language 同样跟随 lang,不再固定中文(#92)。
|
|
315
330
|
uiLang: zh # 设置页界面语言(zh / en);留空则跟随 lang
|
|
316
331
|
promptLanguage: en # 注入给模型的系统提示词与工具描述语言(en / zh);只翻叙述文字,工具名/参数/枚举保持英文
|
|
332
|
+
promptPages: [] # 自定义页文本(只存这些;页 1 内置英文、页 2 内置中文都不入盘)
|
|
333
|
+
promptPageIndex: 0 # 当前页(0 = 内置英文,1 = 内置中文,2 起 = promptPages[i-2]);当前页即注入内容
|
|
334
|
+
disabledTools: [] # 关掉的工具不再注册(例如 [domain_search]),schema 一并从上下文消失
|
|
317
335
|
bingMarket: zh-CN # Bing 市场;留空 = 自动(不发送 mkt)
|
|
318
336
|
fetchProviderTakeover: true # false = 保留你显式 pin 的 fetchProvider: http
|
|
319
337
|
region: cn-zh # DuckDuckGo 区域(可选)
|
|
@@ -497,7 +515,7 @@ Search engine test:
|
|
|
497
515
|
|
|
498
516
|
#### 离线自检断言(tools/)
|
|
499
517
|
|
|
500
|
-
仓库自带 **
|
|
518
|
+
仓库自带 **35 个**零依赖断言脚本(合计 **648 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
|
|
501
519
|
|
|
502
520
|
| 脚本 | 覆盖 | 运行(项数) |
|
|
503
521
|
|---|---|---|
|
|
@@ -523,6 +541,7 @@ Search engine test:
|
|
|
523
541
|
| `assert-prompt-language.mjs` | 提示词语言(en/zh):语言回退、9 个工具双语文案、标识符不被翻译、占位符插值、切换时只重注册一次 | `node tools/assert-prompt-language.mjs`(7) |
|
|
524
542
|
| `assert-tool-schema-size.mjs` | 工具 schema 体积(#94):共享引擎 id 常量、`domain_search` 不内嵌逐引擎句子、三处合计 < 1.6k 字符 | `node tools/assert-tool-schema-size.mjs`(5) |
|
|
525
543
|
| `assert-config-seat.mjs` | 配置入口(#101):卡片同时挂在 `plugins.bundle.config`(key = 包名,渲染在包页)与 `plugins.row.config`(老入口),两者共用同一渲染器 | `node tools/assert-config-seat.mjs`(4) |
|
|
544
|
+
| `assert-prompt-injection.mjs` | 提示词分页(#102):两页内置在前、自定义页在后、当前页即注入、空页不注入、内置页分状态/工具/约束三块、内置随 describe 下发不再单独请求、工具开关落到每个注册点、第七段 UI | `node tools/assert-prompt-injection.mjs`(10) |
|
|
526
545
|
| `assert-rail-section.mjs` | 页内目录的机器可读当前段:卡片根 `data-dsh-fs-section` 与 `railActive` 绑定、六段 id 固定、滚动监听仍在更新 | `node tools/assert-rail-section.mjs`(4) |
|
|
527
546
|
| `assert-package-files.mjs` | 发布清单闸门(#97):`files` 展开后必须覆盖被引用的本地模块、入口点与 `cordis.patch.yml`,并确认 `prepack` 守卫生效 | `node tools/assert-package-files.mjs`(4) |
|
|
528
547
|
| `assert-key-rotation.mjs` | 多 key 轮换(#96):round-robin 与 peek、单 key 不推进、配额优先于 429 的分类、轮换候选集合、`multi` 模式内重试与预检不消耗 key | `node tools/assert-key-rotation.mjs`(7) |
|
|
@@ -584,7 +603,22 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
584
603
|
- **Zero Cost** — Multiple free engines with no API key or registration required
|
|
585
604
|
- **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, SerpBase, Serply, DeepSeek Official, You.com, Baidu Qianfan, Kimi, Aliyun Bailian, Volcano Doubao, Zhihu web-wide/site search (`zhihu_global`/`zhihu_site`, via the Zhihu open-platform MCP), and the model-based OpenAI / Gemini / Claude searches
|
|
586
605
|
- **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle; the entry is the component-row config (`plugins.row.config`) on the sidebar Plugins page (DSH 0.1.7-rc.1+). Remote access (VPS, reverse proxy, custom domain) works too: the bridge reuses the host's own Host fence, so addresses declared with `--trusted-host` or `--public-url` are accepted (#84)
|
|
587
|
-
- **Right-hand section rail** — A floating list (Engines / Fallback / Language pools / Search behaviour / Fetch / Performance) sits in the page margin to the right of the card: it highlights the section you are in while scrolling and jumps smoothly on click. It is rendered in a portal and never narrows the card, so the body layout is unchanged; it hides itself when the margin is too tight.
|
|
606
|
+
- **Right-hand section rail** — A floating list (Engines / Fallback / Language pools / Search behaviour / Fetch / Performance / Prompt) sits in the page margin to the right of the card: it highlights the section you are in while scrolling and jumps smoothly on click. It is rendered in a portal and never narrows the card, so the body layout is unchanged; it hides itself when the margin is too tight. Every entry is a real link (`#/panel/plugins/dsh-free-search/<section>`), so it can be copied or opened in a tab - see the section-link contract below.
|
|
607
|
+
|
|
608
|
+
### Paged prompt
|
|
609
|
+
|
|
610
|
+
In the seventh section, page 1 is the built-in English prompt, page 2 the built-in Chinese one, and page 3 onwards are pages you wrote. The page you are on is what gets injected; parking on an empty page injects nothing. The built-in pages are generated from the live config, are never stored, and cannot be deleted or edited. A tool switched off in the tool list is not registered, so its schema leaves the context too.
|
|
611
|
+
|
|
612
|
+
### Section-link contract (with dsh-url-router)
|
|
613
|
+
|
|
614
|
+
Rules live in `dsh-url-router/docs/panel-sections.md` (no fixed count; seven sections today: engines / fallback / pools / behavior / fetch / performance / prompt):
|
|
615
|
+
|
|
616
|
+
- the page root publishes **exactly one** `data-dsh-panel-section="<id>"` = the section the reader is looking at (it follows scrolling; it never guesses "at the bottom = last section")
|
|
617
|
+
- every section container carries `data-dsh-section="<id>"` (the scroll target)
|
|
618
|
+
- every rail entry is `<a data-dsh-section="<id>" href="#/panel/plugins/dsh-free-search/<id>">`
|
|
619
|
+
- this plugin **never writes the URL**: `location.hash` / `history.pushState` / `history.replaceState` are untouched because dsh-url-router owns the address bar; when a section link appears there, a `hashchange` listener scrolls that section into view
|
|
620
|
+
|
|
621
|
+
Adding a section takes three steps: put `data-dsh-section="<new id>"` on the container, add one `NAV_ITEMS` entry (the rail anchor is generated from `item.id`), and add the id to `assert-rail-section`.
|
|
588
622
|
- **Multi-key rotation** — Any key field (or its env var) accepts several keys separated by commas, semicolons or newlines. Keys are used round-robin per request (so a single key never absorbs every call), and on 401/402/429 (bad key, quota exhausted, rate limited) the same engine retries with the next key before the engine-level fallback runs. This also applies inside `multi` mode (#96).
|
|
589
623
|
- **Popup Switch Command** — Type `/free-search-engine` in the chat: a picker opens with all engines; click one to switch (equivalent to the settings page + save)
|
|
590
624
|
- **Engine Testing** — `free_search_test` for the agent to check all engines in one call; the settings UI also has a "Test engine" button that tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error)
|
|
@@ -868,6 +902,9 @@ lang: zh # search language pools / localization (zh / en / ru
|
|
|
868
902
|
# web_fetch (fs-quality-fetch) sends the same Accept-Language instead of a fixed Chinese one (#92).
|
|
869
903
|
uiLang: zh # settings UI language (zh / en); empty = follow lang
|
|
870
904
|
promptLanguage: en # language of the system prompt section and tool descriptions (en / zh); prose only - tool names, params and enums stay English
|
|
905
|
+
promptPages: [] # your own page texts (only these are stored; the built-in pages are not)
|
|
906
|
+
promptPageIndex: 0 # current page (0 = built-in English, 1 = built-in Chinese, 2+ = promptPages[i-2]); the current page is what gets injected
|
|
907
|
+
disabledTools: [] # tools that are not registered at all (e.g. [domain_search]); their schemas leave the context too
|
|
871
908
|
bingMarket: zh-CN # Bing market; empty = automatic (no mkt sent)
|
|
872
909
|
fetchProviderTakeover: true # false = keep the fetchProvider: http you pinned yourself
|
|
873
910
|
region: cn-zh # DuckDuckGo region (optional)
|
|
@@ -1051,7 +1088,7 @@ Restart `dsh web` after switching to apply changes.
|
|
|
1051
1088
|
|
|
1052
1089
|
#### Offline self-check assertions (`tools/`)
|
|
1053
1090
|
|
|
1054
|
-
**Thirty-
|
|
1091
|
+
**Thirty-five** zero-dependency assertion scripts ship with the repo (**648 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
|
|
1055
1092
|
|
|
1056
1093
|
| Script | Covers | Run (count) |
|
|
1057
1094
|
|---|---|---|
|
|
@@ -1077,6 +1114,7 @@ Restart `dsh web` after switching to apply changes.
|
|
|
1077
1114
|
| `assert-prompt-language.mjs` | Prompt language (en/zh): fallback rules, bilingual copy for all 9 tools, identifiers left untranslated, placeholder interpolation, and one re-registration per language change | `node tools/assert-prompt-language.mjs` (7) |
|
|
1078
1115
|
| `assert-tool-schema-size.mjs` | Tool schema budget (#94): one shared engine-id constant, no per-engine sentences in `domain_search`, the three schemas under 1.6k chars combined | `node tools/assert-tool-schema-size.mjs` (5) |
|
|
1079
1116
|
| `assert-config-seat.mjs` | Config entry point (#101): the card is registered on `plugins.bundle.config` (keyed by the package name, rendered on the bundle page) and on `plugins.row.config` (older entry), both sharing one renderer | `node tools/assert-config-seat.mjs` (4) |
|
|
1117
|
+
| `assert-prompt-injection.mjs` | Paged prompt (#102): two built-in pages first, user pages after, the current page is what gets injected, an empty page injects nothing, the generated page split into State/Tools/Constraints blocks, built-ins delivered with describe instead of a second request, tool switches wired into every registrar, and the seventh section UI | `node tools/assert-prompt-injection.mjs` (10) |
|
|
1080
1118
|
| `assert-rail-section.mjs` | Machine-readable active section for the in-page rail: the card root's `data-dsh-fs-section` tracks `railActive`, the six section ids are fixed, and the scroll spy still updates it | `node tools/assert-rail-section.mjs` (4) |
|
|
1081
1119
|
| `assert-package-files.mjs` | Packaging gate (#97): after expanding `files`, every imported local module, the entry points and `cordis.patch.yml` must be published, and the `prepack` guard must be wired | `node tools/assert-package-files.mjs` (4) |
|
|
1082
1120
|
| `assert-key-rotation.mjs` | Multi-key rotation (#96): round-robin and peek, single keys never advancing, quota classification ahead of 429, the rotation-candidate set, and in-engine retries that keep multi-mode preflight from consuming keys | `node tools/assert-key-rotation.mjs` (7) |
|
package/lib/client.js
CHANGED
|
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
".dshfs-body{flex-direction:column;gap:20px;padding:0 20px 20px;display:flex;animation:dshfsFadeIn 240ms cubic-bezier(.4,0,.2,1) forwards}",
|
|
32
32
|
"@media (prefers-reduced-motion: reduce){.dshfs-body{animation:none}}",
|
|
33
33
|
".dshfs-rail{display:none;position:fixed;top:50%;transform:translateY(-50%);flex-direction:column;gap:2px;max-height:70vh;overflow:auto;z-index:5;padding:2px}",
|
|
34
|
-
".dshfs-railItem{display:flex;align-items:center;gap:6px;padding:3px 2px;border:0;background:none;font:inherit;font-size:12px;line-height:1.5;color:inherit;opacity:.42;text-align:left;cursor:pointer;transition:opacity 120ms}",
|
|
34
|
+
".dshfs-railItem{display:flex;align-items:center;gap:6px;padding:3px 2px;border:0;background:none;font:inherit;font-size:12px;line-height:1.5;color:inherit;opacity:.42;text-align:left;cursor:pointer;transition:opacity 120ms;text-decoration:none}",
|
|
35
35
|
".dshfs-railItem:hover{opacity:.85}",
|
|
36
36
|
".dshfs-railItemActive{opacity:.95;font-weight:600}",
|
|
37
37
|
".dshfs-railDot{flex:none;width:4px;height:4px;border-radius:50%;background:currentColor;opacity:.45}",
|
|
@@ -53,6 +53,9 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
"@media (prefers-color-scheme: dark){.dshfs-testOk{color:#30d158}}",
|
|
54
54
|
".dshfs-resultRow{display:flex;flex-direction:column;align-items:flex-start;gap:6px;min-width:0;margin-top:-4px}",
|
|
55
55
|
".dshfs-field{flex-direction:column;gap:8px;min-width:0;display:flex}",
|
|
56
|
+
".dshfs-preview{margin:0;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:12px;line-height:1.5;padding:10px 12px;border:1px solid rgba(0,0,0,.1);border-radius:10px;background:rgba(0,0,0,.03)}",
|
|
57
|
+
"@media (prefers-color-scheme: dark){.dshfs-preview{border-color:rgba(255,255,255,.1);background:rgba(255,255,255,.04)}}",
|
|
58
|
+
|
|
56
59
|
".dshfs-label{color:inherit;font-size:14px;font-weight:500;display:flex;align-items:center;gap:8px}",
|
|
57
60
|
".dshfs-select, .dshfs-input{border:1px solid rgba(0,0,0,.1);font:inherit;font-variant-numeric:tabular-nums;color:inherit;background:rgba(0,0,0,.03);border-radius:10px;padding:0 12px;height:38px;font-size:14px;transition:all 160ms cubic-bezier(.4,0,.2,1);width:100%;box-sizing:border-box}",
|
|
58
61
|
"@media (prefers-color-scheme: dark){.dshfs-select, .dshfs-input{border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05)}}",
|
|
@@ -265,6 +268,11 @@ window.__ModuleLoader__.load({
|
|
|
265
268
|
navBehavior: "搜索行为",
|
|
266
269
|
navFetch: "抓取",
|
|
267
270
|
navPerformance: "性能",
|
|
271
|
+
navPrompt: "提示词",
|
|
272
|
+
promptLabel: "提示词",
|
|
273
|
+
promptPreviewToggle: "预览",
|
|
274
|
+
|
|
275
|
+
toolsLabel: "工具开关",
|
|
268
276
|
marketZhCN: "zh-CN —— 中国大陆(默认)",
|
|
269
277
|
marketZhTW: "zh-TW —— 台湾",
|
|
270
278
|
marketEnUS: "en-US —— 美国",
|
|
@@ -386,6 +394,11 @@ window.__ModuleLoader__.load({
|
|
|
386
394
|
navBehavior: "Search behaviour",
|
|
387
395
|
navFetch: "Fetch",
|
|
388
396
|
navPerformance: "Performance",
|
|
397
|
+
navPrompt: "Prompt",
|
|
398
|
+
promptLabel: "Prompt",
|
|
399
|
+
promptPreviewToggle: "Preview",
|
|
400
|
+
|
|
401
|
+
toolsLabel: "Tools",
|
|
389
402
|
marketZhCN: "zh-CN - China (default)",
|
|
390
403
|
marketZhTW: "zh-TW - Taiwan",
|
|
391
404
|
marketEnUS: "en-US - United States",
|
|
@@ -402,7 +415,20 @@ window.__ModuleLoader__.load({
|
|
|
402
415
|
};
|
|
403
416
|
const tt = (lang) => I18N[String(lang ?? "").trim().toLowerCase() === "zh" ? "zh" : "en"];
|
|
404
417
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
|
|
405
|
-
const
|
|
418
|
+
const PROMPT_BUILTIN_PAGES = 2;
|
|
419
|
+
// #102:可开关的工具(web_search / web_fetch 属于 DSH 内核,不在我们这边注册)
|
|
420
|
+
const TOOL_TOGGLES = [
|
|
421
|
+
"free_search_test",
|
|
422
|
+
"platform_search",
|
|
423
|
+
"video_search",
|
|
424
|
+
"weather",
|
|
425
|
+
"advanced_search",
|
|
426
|
+
"multi_search",
|
|
427
|
+
"research",
|
|
428
|
+
"cache_search",
|
|
429
|
+
"domain_search",
|
|
430
|
+
];
|
|
431
|
+
const PLUGIN_VERSION = "0.9.23";
|
|
406
432
|
const PLUGIN_REPO_URL = "https://github.com/DDDMUC/dsh-free-search";
|
|
407
433
|
// 右侧浮动目录(#85):条目顺序即页面顺序,anchor 是页面里对应 field 的 id。
|
|
408
434
|
const NAV_ITEMS = [
|
|
@@ -412,6 +438,7 @@ window.__ModuleLoader__.load({
|
|
|
412
438
|
{ id: "behavior", anchor: "dshfs-anchor-behavior" },
|
|
413
439
|
{ id: "fetch", anchor: "dshfs-anchor-fetch" },
|
|
414
440
|
{ id: "performance", anchor: "dshfs-anchor-performance" },
|
|
441
|
+
{ id: "prompt", anchor: "dshfs-anchor-prompt" },
|
|
415
442
|
];
|
|
416
443
|
// 目录跟着真正滚动的祖先走:DSH 页面通常不是 window 在滚。
|
|
417
444
|
const findScrollParent = (element) => {
|
|
@@ -685,6 +712,11 @@ window.__ModuleLoader__.load({
|
|
|
685
712
|
const [lang, setLang] = react.useState("zh");
|
|
686
713
|
const [uiLang, setUiLang] = react.useState("");
|
|
687
714
|
const [promptLanguage, setPromptLanguage] = react.useState("en");
|
|
715
|
+
const [promptPages, setPromptPages] = react.useState([]);
|
|
716
|
+
const [promptPageIndex, setPromptPageIndex] = react.useState(0);
|
|
717
|
+
const [disabledTools, setDisabledTools] = react.useState([]);
|
|
718
|
+
const [builtinPrompts, setBuiltinPrompts] = react.useState({ en: "", zh: "" });
|
|
719
|
+
const [previewOpen, setPreviewOpen] = react.useState(false);
|
|
688
720
|
const [dirty, setDirty] = react.useState(false);
|
|
689
721
|
const [saving, setSaving] = react.useState(false);
|
|
690
722
|
const [failed, setFailed] = react.useState(false);
|
|
@@ -745,6 +777,10 @@ window.__ModuleLoader__.load({
|
|
|
745
777
|
try {
|
|
746
778
|
const result = await bridgeDescribe();
|
|
747
779
|
if (result.ok) {
|
|
780
|
+
const builtin = result.value.promptBuiltin;
|
|
781
|
+
if (builtin && typeof builtin === "object") {
|
|
782
|
+
setBuiltinPrompts({ en: String(builtin.en ?? ""), zh: String(builtin.zh ?? "") });
|
|
783
|
+
}
|
|
748
784
|
const view = result.value.namespaces.find((n) => n.ns === NS);
|
|
749
785
|
if (view) {
|
|
750
786
|
const v = view.value ?? {};
|
|
@@ -766,6 +802,9 @@ window.__ModuleLoader__.load({
|
|
|
766
802
|
setLang(v.lang ?? "zh");
|
|
767
803
|
setUiLang(typeof v.uiLang === "string" ? v.uiLang : "");
|
|
768
804
|
setPromptLanguage(v.promptLanguage === "zh" ? "zh" : "en");
|
|
805
|
+
setPromptPages(Array.isArray(v.promptPages) ? v.promptPages.map((page) => String(page ?? "")) : []);
|
|
806
|
+
setPromptPageIndex(Number.isFinite(v.promptPageIndex) ? Math.max(0, Math.floor(v.promptPageIndex)) : 0);
|
|
807
|
+
setDisabledTools(Array.isArray(v.disabledTools) ? v.disabledTools.map((id) => String(id)) : []);
|
|
769
808
|
setAnysearchKey(v.anysearchApiKey ?? "");
|
|
770
809
|
setExaKey(v.exaApiKey ?? "");
|
|
771
810
|
setTavilyKey(v.tavilyApiKey ?? "");
|
|
@@ -914,6 +953,9 @@ window.__ModuleLoader__.load({
|
|
|
914
953
|
ops.push({ op: "set", path: ["blockedDomains"], value: parseBlockedDomains(blockedDomains).domains });
|
|
915
954
|
ops.push({ op: "set", path: ["uiLang"], value: uiLang });
|
|
916
955
|
ops.push({ op: "set", path: ["promptLanguage"], value: promptLanguage });
|
|
956
|
+
ops.push({ op: "set", path: ["promptPages"], value: promptPages });
|
|
957
|
+
ops.push({ op: "set", path: ["promptPageIndex"], value: Math.max(0, Math.min(promptPageIndex, PROMPT_BUILTIN_PAGES + promptPages.length - 1)) });
|
|
958
|
+
ops.push({ op: "set", path: ["disabledTools"], value: disabledTools });
|
|
917
959
|
ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
|
|
918
960
|
ops.push({ op: "set", path: ["safeSearch"], value: safeSearch });
|
|
919
961
|
ops.push({ op: "set", path: ["bingMarket"], value: bingMarket });
|
|
@@ -1013,6 +1055,31 @@ window.__ModuleLoader__.load({
|
|
|
1013
1055
|
return () => window.removeEventListener("resize", place);
|
|
1014
1056
|
}, [open, state.status]);
|
|
1015
1057
|
|
|
1058
|
+
// 分区链接:地址栏里出现 #/panel/plugins/dsh-free-search/<分区>(或任意 …/<分区>)就滚过去。
|
|
1059
|
+
// 本插件**只读不写** URL —— 地址栏的唯一写者是 dsh-url-router,写这里会跟它抢。
|
|
1060
|
+
react.useEffect(() => {
|
|
1061
|
+
if (!pageMode) return undefined;
|
|
1062
|
+
const sectionOfHash = () => {
|
|
1063
|
+
const hash = String((typeof window !== "undefined" && window.location && window.location.hash) || "");
|
|
1064
|
+
const parts = decodeURIComponent(hash.replace(/^#\/?/, "")).split("/").filter(Boolean);
|
|
1065
|
+
const index = parts.lastIndexOf("plugins");
|
|
1066
|
+
if (index >= 0) return parts[index + 2] || "";
|
|
1067
|
+
return parts.length === 1 ? parts[0] : "";
|
|
1068
|
+
};
|
|
1069
|
+
const apply = () => {
|
|
1070
|
+
const id = sectionOfHash();
|
|
1071
|
+
if (!id || !NAV_ITEMS.some((entry) => entry.id === id)) return;
|
|
1072
|
+
const node = document.querySelector(`[data-dsh-section="${id}"]`)
|
|
1073
|
+
|| (NAV_ITEMS.find((entry) => entry.id === id) ? document.getElementById(NAV_ITEMS.find((entry) => entry.id === id).anchor) : null);
|
|
1074
|
+
if (!node || typeof node.scrollIntoView !== "function") return;
|
|
1075
|
+
setRailActive(id);
|
|
1076
|
+
node.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1077
|
+
};
|
|
1078
|
+
apply();
|
|
1079
|
+
window.addEventListener("hashchange", apply);
|
|
1080
|
+
return () => window.removeEventListener("hashchange", apply);
|
|
1081
|
+
}, [pageMode]);
|
|
1082
|
+
|
|
1016
1083
|
// 当前分组:分组顶部越过 35% 激活线就算当前;滚到底高亮最后一组。
|
|
1017
1084
|
react.useEffect(() => {
|
|
1018
1085
|
if (!open) return undefined;
|
|
@@ -1021,9 +1088,16 @@ window.__ModuleLoader__.load({
|
|
|
1021
1088
|
if (!target) return undefined;
|
|
1022
1089
|
const compute = () => {
|
|
1023
1090
|
const viewportHeight = scroller ? scroller.clientHeight : window.innerHeight;
|
|
1091
|
+
// "At the bottom" 必须建立在"这个容器真的能滚"之上。原来只看 scrollTop + clientHeight
|
|
1092
|
+
// >= scrollHeight - 8:一旦 findScrollParent 挑中一个不滚动的祖先(scrollHeight ===
|
|
1093
|
+
// clientHeight),这个判断恒为真,于是当前分区**永远**是最后一节,地址栏永远是
|
|
1094
|
+
// performance(用户 2026-10-31 实测)。契约见 dsh-url-router/docs/panel-sections.md 五.1:
|
|
1095
|
+
// 当前分区是"读者正在看的那一节",不是"猜最后一节"。
|
|
1024
1096
|
const atBottom = scroller
|
|
1025
|
-
? scroller.
|
|
1026
|
-
|
|
1097
|
+
? scroller.scrollHeight > scroller.clientHeight + 8
|
|
1098
|
+
&& scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 8
|
|
1099
|
+
: document.documentElement.scrollHeight > window.innerHeight + 8
|
|
1100
|
+
&& window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 8;
|
|
1027
1101
|
if (atBottom) {
|
|
1028
1102
|
setRailActive(NAV_ITEMS[NAV_ITEMS.length - 1].id);
|
|
1029
1103
|
return;
|
|
@@ -1063,6 +1137,7 @@ window.__ModuleLoader__.load({
|
|
|
1063
1137
|
behavior: t.navBehavior,
|
|
1064
1138
|
fetch: t.navFetch,
|
|
1065
1139
|
performance: t.navPerformance,
|
|
1140
|
+
prompt: t.navPrompt,
|
|
1066
1141
|
};
|
|
1067
1142
|
const goToSection = (id) => {
|
|
1068
1143
|
const item = NAV_ITEMS.find((entry) => entry.id === id);
|
|
@@ -1071,6 +1146,15 @@ window.__ModuleLoader__.load({
|
|
|
1071
1146
|
setRailActive(id);
|
|
1072
1147
|
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1073
1148
|
};
|
|
1149
|
+
// #102:页 1 内置英文、页 2 内置中文、页 3 起自定义;当前页即注入内容。
|
|
1150
|
+
const promptPageTotal = PROMPT_BUILTIN_PAGES + promptPages.length;
|
|
1151
|
+
const promptPageKind = (index) => (index === 0 ? "builtin-en" : index === 1 ? "builtin-zh" : "user");
|
|
1152
|
+
const promptPageTextAt = (index) => {
|
|
1153
|
+
const kind = promptPageKind(index);
|
|
1154
|
+
if (kind === "builtin-en") return builtinPrompts.en;
|
|
1155
|
+
if (kind === "builtin-zh") return builtinPrompts.zh;
|
|
1156
|
+
return promptPages[index - PROMPT_BUILTIN_PAGES] ?? "";
|
|
1157
|
+
};
|
|
1074
1158
|
const title = "Free Search";
|
|
1075
1159
|
const description = t.description;
|
|
1076
1160
|
const enabledEngineCount = ENGINES.filter((e) => e.id !== "auto" && e.id !== "multi" && !disabledEngines.includes(e.id)).length;
|
|
@@ -1106,6 +1190,7 @@ window.__ModuleLoader__.load({
|
|
|
1106
1190
|
return react_jsx_runtime.jsx("div", {
|
|
1107
1191
|
className: "dshfs-field",
|
|
1108
1192
|
id: poolKey === "zh" ? "dshfs-anchor-pools" : undefined,
|
|
1193
|
+
"data-dsh-section": poolKey === "zh" ? "pools" : undefined,
|
|
1109
1194
|
children: [
|
|
1110
1195
|
react_jsx_runtime.jsx("div", {
|
|
1111
1196
|
className: "dshfs-label",
|
|
@@ -1236,6 +1321,9 @@ window.__ModuleLoader__.load({
|
|
|
1236
1321
|
return react_jsx_runtime.jsx("li", {
|
|
1237
1322
|
className: (open ? "dshfs-card dshfs-cardOpen" : "dshfs-card") + (pageMode ? " dshfs-pageMode" : ""),
|
|
1238
1323
|
"data-dsh-fs-section": railActive,
|
|
1324
|
+
// 通用契约:当前分区。侧栏每一项也有 data-dsh-fs-section(值是它自己的 id),
|
|
1325
|
+
// 所以读的人分不清"当前"和"某一项" —— 根上这个才是状态。
|
|
1326
|
+
"data-dsh-panel-section": railActive,
|
|
1239
1327
|
children: [
|
|
1240
1328
|
react_jsx_runtime.jsx("button", {
|
|
1241
1329
|
type: "button",
|
|
@@ -1279,10 +1367,14 @@ window.__ModuleLoader__.load({
|
|
|
1279
1367
|
ref: railRef,
|
|
1280
1368
|
"aria-label": t.navLabel,
|
|
1281
1369
|
children: NAV_ITEMS.map((item) =>
|
|
1282
|
-
|
|
1283
|
-
|
|
1370
|
+
// 真链接:右键能"复制链接"、能开新标签、地址栏里就是分区。
|
|
1371
|
+
// 本插件不写 URL(地址栏的唯一写者是 dsh-url-router);href 只是给出
|
|
1372
|
+
// 这条分区的地址,点击时浏览器自己改 hash,滚动仍由 goToSection 立即完成。
|
|
1373
|
+
react_jsx_runtime.jsx("a", {
|
|
1284
1374
|
className: "dshfs-railItem" + (railActive === item.id ? " dshfs-railItemActive" : ""),
|
|
1375
|
+
href: `#/panel/plugins/dsh-free-search/${item.id}`,
|
|
1285
1376
|
"data-dsh-fs-section": item.id,
|
|
1377
|
+
"data-dsh-section": item.id,
|
|
1286
1378
|
onClick: () => goToSection(item.id),
|
|
1287
1379
|
children: [
|
|
1288
1380
|
react_jsx_runtime.jsx("span", { className: "dshfs-railDot" }),
|
|
@@ -1296,6 +1388,7 @@ window.__ModuleLoader__.load({
|
|
|
1296
1388
|
react_jsx_runtime.jsx("div", {
|
|
1297
1389
|
className: "dshfs-field",
|
|
1298
1390
|
id: "dshfs-anchor-engines",
|
|
1391
|
+
"data-dsh-section": "engines",
|
|
1299
1392
|
children: [
|
|
1300
1393
|
react_jsx_runtime.jsx("div", {
|
|
1301
1394
|
className: "dshfs-label",
|
|
@@ -1383,6 +1476,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
1383
1476
|
react_jsx_runtime.jsx("div", {
|
|
1384
1477
|
className: "dshfs-field",
|
|
1385
1478
|
id: "dshfs-anchor-fallback",
|
|
1479
|
+
"data-dsh-section": "fallback",
|
|
1386
1480
|
children: [
|
|
1387
1481
|
react_jsx_runtime.jsx("div", {
|
|
1388
1482
|
className: "dshfs-label",
|
|
@@ -1557,6 +1651,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
1557
1651
|
react_jsx_runtime.jsx("div", {
|
|
1558
1652
|
className: "dshfs-field",
|
|
1559
1653
|
id: "dshfs-anchor-behavior",
|
|
1654
|
+
"data-dsh-section": "behavior",
|
|
1560
1655
|
children: [
|
|
1561
1656
|
react_jsx_runtime.jsx("div", {
|
|
1562
1657
|
className: "dshfs-label",
|
|
@@ -1681,6 +1776,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
1681
1776
|
react_jsx_runtime.jsx("div", {
|
|
1682
1777
|
className: "dshfs-field",
|
|
1683
1778
|
id: "dshfs-anchor-fetch",
|
|
1779
|
+
"data-dsh-section": "fetch",
|
|
1684
1780
|
children: [
|
|
1685
1781
|
react_jsx_runtime.jsx("label", {
|
|
1686
1782
|
className: "dshfs-check",
|
|
@@ -1787,6 +1883,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
1787
1883
|
react_jsx_runtime.jsx("div", {
|
|
1788
1884
|
className: "dshfs-field",
|
|
1789
1885
|
id: "dshfs-anchor-performance",
|
|
1886
|
+
"data-dsh-section": "performance",
|
|
1790
1887
|
children: [
|
|
1791
1888
|
react_jsx_runtime.jsx("div", {
|
|
1792
1889
|
className: "dshfs-label",
|
|
@@ -1901,6 +1998,69 @@ react_jsx_runtime.jsx("div", {
|
|
|
1901
1998
|
}),
|
|
1902
1999
|
],
|
|
1903
2000
|
}),
|
|
2001
|
+
react_jsx_runtime.jsx("div", {
|
|
2002
|
+
className: "dshfs-field",
|
|
2003
|
+
id: "dshfs-anchor-prompt",
|
|
2004
|
+
"data-dsh-section": "prompt",
|
|
2005
|
+
children: [
|
|
2006
|
+
react_jsx_runtime.jsx("div", { className: "dshfs-label", children: t.promptLabel }),
|
|
2007
|
+
react_jsx_runtime.jsx("div", {
|
|
2008
|
+
className: "dshfs-presetRow",
|
|
2009
|
+
children: [
|
|
2010
|
+
react_jsx_runtime.jsx("button", { type: "button", className: "dshfs-btn", disabled: promptPageIndex <= 0, onClick: () => setPromptPageIndex((i) => Math.max(0, i - 1)), children: "‹" }, "prev"),
|
|
2011
|
+
react_jsx_runtime.jsx("span", { className: "dshfs-hint", children: `${promptPageIndex + 1} / ${promptPageTotal}` }),
|
|
2012
|
+
react_jsx_runtime.jsx("button", { type: "button", className: "dshfs-btn", disabled: promptPageIndex >= promptPageTotal - 1, onClick: () => setPromptPageIndex((i) => Math.min(promptPageTotal - 1, i + 1)), children: "›" }, "next"),
|
|
2013
|
+
react_jsx_runtime.jsx("button", { type: "button", className: "dshfs-btn", disabled: !ready || saving, onClick: () => { setPromptPages((prev) => [...prev, ""]); setPromptPageIndex(promptPageTotal); setDirty(true); setFailed(false); setSaveError(""); }, children: "+" }, "add"),
|
|
2014
|
+
react_jsx_runtime.jsx("button", { type: "button", className: "dshfs-btn", disabled: !ready || saving || promptPageKind(promptPageIndex) !== "user", onClick: () => { const at = promptPageIndex - PROMPT_BUILTIN_PAGES; setPromptPages((prev) => prev.filter((_, i) => i !== at)); setPromptPageIndex((i) => Math.max(0, i - 1)); setDirty(true); setFailed(false); setSaveError(""); }, children: "−" }, "remove"),
|
|
2015
|
+
],
|
|
2016
|
+
}),
|
|
2017
|
+
react_jsx_runtime.jsx("textarea", {
|
|
2018
|
+
className: "dshfs-textarea",
|
|
2019
|
+
rows: 8,
|
|
2020
|
+
spellCheck: false,
|
|
2021
|
+
readOnly: promptPageKind(promptPageIndex) !== "user",
|
|
2022
|
+
value: promptPageTextAt(promptPageIndex),
|
|
2023
|
+
disabled: !ready || saving,
|
|
2024
|
+
onChange: (e) => {
|
|
2025
|
+
const at = promptPageIndex - PROMPT_BUILTIN_PAGES;
|
|
2026
|
+
const next = promptPages.slice();
|
|
2027
|
+
next[at] = e.target.value;
|
|
2028
|
+
setPromptPages(next);
|
|
2029
|
+
setDirty(true);
|
|
2030
|
+
setFailed(false);
|
|
2031
|
+
setSaveError("");
|
|
2032
|
+
},
|
|
2033
|
+
}),
|
|
2034
|
+
|
|
2035
|
+
react_jsx_runtime.jsx("button", { type: "button", className: "dshfs-btn", onClick: () => setPreviewOpen((open) => !open), children: (previewOpen ? "▾ " : "▸ ") + t.promptPreviewToggle }, "preview"),
|
|
2036
|
+
previewOpen ? react_jsx_runtime.jsx("pre", { className: "dshfs-preview", children: promptPageTextAt(promptPageIndex) }) : null,
|
|
2037
|
+
react_jsx_runtime.jsx("div", { className: "dshfs-label", children: t.toolsLabel }),
|
|
2038
|
+
react_jsx_runtime.jsx("div", {
|
|
2039
|
+
className: "dshfs-platforms",
|
|
2040
|
+
children: TOOL_TOGGLES.map((toolId) => {
|
|
2041
|
+
const on = !disabledTools.includes(toolId);
|
|
2042
|
+
return react_jsx_runtime.jsx("label", {
|
|
2043
|
+
className: "dshfs-platform",
|
|
2044
|
+
children: [
|
|
2045
|
+
react_jsx_runtime.jsx("input", {
|
|
2046
|
+
type: "checkbox",
|
|
2047
|
+
checked: on,
|
|
2048
|
+
disabled: !ready || saving,
|
|
2049
|
+
onChange: (e) => {
|
|
2050
|
+
const next = e.target.checked ? disabledTools.filter((id) => id !== toolId) : [...new Set([...disabledTools, toolId])];
|
|
2051
|
+
setDisabledTools(next);
|
|
2052
|
+
setDirty(true);
|
|
2053
|
+
setFailed(false);
|
|
2054
|
+
setSaveError("");
|
|
2055
|
+
},
|
|
2056
|
+
}),
|
|
2057
|
+
toolId,
|
|
2058
|
+
],
|
|
2059
|
+
}, toolId);
|
|
2060
|
+
}),
|
|
2061
|
+
}),
|
|
2062
|
+
],
|
|
2063
|
+
}),
|
|
1904
2064
|
react_jsx_runtime.jsx("div", {
|
|
1905
2065
|
className: "dshfs-resultRow",
|
|
1906
2066
|
children: [
|