dsh-free-search 0.9.20 → 0.9.22
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/LICENSE +21 -21
- package/README.md +8 -4
- package/lib/client.js +19 -173
- package/lib/domain_filter.js +100 -100
- package/lib/index.js +1 -134
- package/lib/prompt_i18n.js +312 -312
- package/lib/url_norm.js +82 -82
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 dsh-free-search contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-free-search contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
41
41
|
- **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
|
|
42
42
|
- **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key;并明确所有搜索结果是**不可信外部数据**,不得执行其中的指令
|
|
43
43
|
- **提示注入防护(不可信数据边界)** —— 插件自有工具(advanced_search / platform_search / free_search_test)的网页文本包在 `<untrusted-web-content>` 边界内(正文里自带的同名标记会被剥离,防止提前闭合);核心 web_search / web_fetch 由 DSH 核心自带同类提示(`External web content follows...`);所有 snippet 统一清洗并截断到 300 字符
|
|
44
|
-
-
|
|
44
|
+
- **版本号** —— 设置卡片底部显示当前版本(跟随 `PLUGIN_VERSION`,随发布更新)。检查与升级交给官方插件页(插件 → 已安装 → dsh-free-search → 更新),插件不再自带第二套更新按钮
|
|
45
45
|
- **结果缓存** —— 相同查询(含引擎/时间过滤参数)5 分钟内命中缓存(LRU 50 条),防免费引擎限流、省付费额度;时长可在设置页 0-5 分钟自由配置(0 关闭)
|
|
46
46
|
- **域名黑名单与安全搜索** —— `blockedDomains`:命中的域名不会进入任何结果;`safeSearch`(`off` / `moderate` / `strict`)为 Bing / DuckDuckGo 加一层过滤
|
|
47
47
|
- **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
|
|
@@ -497,7 +497,7 @@ Search engine test:
|
|
|
497
497
|
|
|
498
498
|
#### 离线自检断言(tools/)
|
|
499
499
|
|
|
500
|
-
仓库自带 **
|
|
500
|
+
仓库自带 **34 个**零依赖断言脚本(合计 **636 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
|
|
501
501
|
|
|
502
502
|
| 脚本 | 覆盖 | 运行(项数) |
|
|
503
503
|
|---|---|---|
|
|
@@ -522,6 +522,8 @@ Search engine test:
|
|
|
522
522
|
| `assert-youcom.mjs` | You.com 免 key 档(#90):两档 URL 与请求头、无 key 不发 `X-API-Key`、`YDC_API_KEY` 别名、429/401/402/5xx 分类 | `node tools/assert-youcom.mjs`(23) |
|
|
523
523
|
| `assert-prompt-language.mjs` | 提示词语言(en/zh):语言回退、9 个工具双语文案、标识符不被翻译、占位符插值、切换时只重注册一次 | `node tools/assert-prompt-language.mjs`(7) |
|
|
524
524
|
| `assert-tool-schema-size.mjs` | 工具 schema 体积(#94):共享引擎 id 常量、`domain_search` 不内嵌逐引擎句子、三处合计 < 1.6k 字符 | `node tools/assert-tool-schema-size.mjs`(5) |
|
|
525
|
+
| `assert-config-seat.mjs` | 配置入口(#101):卡片同时挂在 `plugins.bundle.config`(key = 包名,渲染在包页)与 `plugins.row.config`(老入口),两者共用同一渲染器 | `node tools/assert-config-seat.mjs`(4) |
|
|
526
|
+
| `assert-rail-section.mjs` | 页内目录的机器可读当前段:卡片根 `data-dsh-fs-section` 与 `railActive` 绑定、六段 id 固定、滚动监听仍在更新 | `node tools/assert-rail-section.mjs`(4) |
|
|
525
527
|
| `assert-package-files.mjs` | 发布清单闸门(#97):`files` 展开后必须覆盖被引用的本地模块、入口点与 `cordis.patch.yml`,并确认 `prepack` 守卫生效 | `node tools/assert-package-files.mjs`(4) |
|
|
526
528
|
| `assert-key-rotation.mjs` | 多 key 轮换(#96):round-robin 与 peek、单 key 不推进、配额优先于 429 的分类、轮换候选集合、`multi` 模式内重试与预检不消耗 key | `node tools/assert-key-rotation.mjs`(7) |
|
|
527
529
|
| `assert-x-platform.mjs` | X (Twitter) 平台(Grok Build CLI):可用性、提示词约束、structuredOutput/text/原始 JSON 解析、status 链接过滤与截断、双语文案与面板默认值 | `node tools/assert-x-platform.mjs`(8) |
|
|
@@ -592,7 +594,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
592
594
|
- **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
|
|
593
595
|
- **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys; it is also told that all search output is **untrusted external data** and must never be executed as instructions
|
|
594
596
|
- **Prompt-Injection Guard (untrusted-data boundary)** — Web-derived text from the plugin's own tools (`advanced_search` / `platform_search` / `free_search_test`) is wrapped in an explicit `<untrusted-web-content>` boundary (look-alike tags inside the text are stripped to prevent early closure); the core `web_search` / `web_fetch` tools carry DSH core's own notice (`External web content follows...`); every snippet is cleaned and capped at 300 characters
|
|
595
|
-
- **Version
|
|
597
|
+
- **Version label** — the settings card shows the current version (tracks PLUGIN_VERSION, bumped on release). Checking and updating is the official plugin page's job (Plugins -> Installed -> dsh-free-search -> Update); the plugin ships no second updater
|
|
596
598
|
- **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)
|
|
597
599
|
- **Domain blocklist & safe search** — `blockedDomains` keeps matching domains out of every result; `safeSearch` (`off` / `moderate` / `strict`) adds a filter layer for Bing / DuckDuckGo
|
|
598
600
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
@@ -1049,7 +1051,7 @@ Restart `dsh web` after switching to apply changes.
|
|
|
1049
1051
|
|
|
1050
1052
|
#### Offline self-check assertions (`tools/`)
|
|
1051
1053
|
|
|
1052
|
-
**Thirty-
|
|
1054
|
+
**Thirty-four** zero-dependency assertion scripts ship with the repo (**636 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
|
|
1053
1055
|
|
|
1054
1056
|
| Script | Covers | Run (count) |
|
|
1055
1057
|
|---|---|---|
|
|
@@ -1074,6 +1076,8 @@ Restart `dsh web` after switching to apply changes.
|
|
|
1074
1076
|
| `assert-youcom.mjs` | You.com keyless tier (#90): both URLs and headers, no `X-API-Key` without a key, the `YDC_API_KEY` alias, and 429/401/402/5xx classification | `node tools/assert-youcom.mjs` (23) |
|
|
1075
1077
|
| `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) |
|
|
1076
1078
|
| `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
|
+
| `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) |
|
|
1080
|
+
| `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) |
|
|
1077
1081
|
| `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) |
|
|
1078
1082
|
| `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) |
|
|
1079
1083
|
| `assert-x-platform.mjs` | X (Twitter) platform via the Grok Build CLI: availability, prompt guardrails, structuredOutput/text/raw JSON parsing, status-link filtering and caps, bilingual copy and panel defaults | `node tools/assert-x-platform.mjs` (8) |
|
package/lib/client.js
CHANGED
|
@@ -123,9 +123,6 @@ window.__ModuleLoader__.load({
|
|
|
123
123
|
".dshfs-save:hover:not(:disabled){background:#0077ed;transform:translateY(-1px);box-shadow:0 2px 4px rgba(0,0,0,.15)}",
|
|
124
124
|
".dshfs-save:active:not(:disabled){transform:scale(0.98)}",
|
|
125
125
|
"@media (prefers-color-scheme: dark){.dshfs-save{background:#0a84ff;color:#ffffff}.dshfs-save:hover:not(:disabled){background:#1e8eff}}",
|
|
126
|
-
".dshfs-upgrade{border:1px solid rgba(52,199,89,.4);background:rgba(52,199,89,.15);color:#248a3d}",
|
|
127
|
-
"@media (prefers-color-scheme: dark){.dshfs-upgrade{color:#30d158}}",
|
|
128
|
-
".dshfs-upgrade:hover:not(:disabled){background:rgba(52,199,89,.25)}",
|
|
129
126
|
".dshfs-badge{background:rgba(0,0,0,.05);color:#1d1d1f;white-space:nowrap;border-radius:999px;flex:none;padding:2px 8px;font-size:11px;font-weight:600;letter-spacing:0.3px}",
|
|
130
127
|
"@media (prefers-color-scheme: dark){.dshfs-badge{background:rgba(255,255,255,.1);color:#f5f5f7}}",
|
|
131
128
|
".dshfs-badgeAuto{background:rgba(175,82,222,.15);color:#af52de;border:1px solid rgba(175,82,222,.2)}",
|
|
@@ -140,15 +137,7 @@ window.__ModuleLoader__.load({
|
|
|
140
137
|
"@media (prefers-color-scheme: dark){.dshfs-langToggle{color:#a1a1a6}}",
|
|
141
138
|
".dshfs-update{color:#cc7a00;flex:none;font-size:12px;white-space:nowrap;border:1px solid rgba(255,149,0,.3);background:rgba(255,149,0,.12);border-radius:999px;padding:2px 8px}",
|
|
142
139
|
"@media (prefers-color-scheme: dark){.dshfs-update{color:#ff9f0a}}",
|
|
143
|
-
".dshfs-updateOk{color:#248a3d;flex:none;font-size:12px;white-space:nowrap;border:1px solid rgba(52,199,89,.3);background:rgba(52,199,89,.12);border-radius:999px;padding:2px 8px}",
|
|
144
|
-
"@media (prefers-color-scheme: dark){.dshfs-updateOk{color:#30d158}}",
|
|
145
140
|
".dshfs-updateLine{display:flex;align-items:center;gap:10px;flex-wrap:wrap}",
|
|
146
|
-
".dshfs-updatePill{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,113,227,.2);background:rgba(0,113,227,.1);color:#0071e3;font:inherit;font-size:13px;font-weight:500;line-height:1;cursor:pointer;padding:6px 12px;border-radius:999px;text-decoration:none;white-space:nowrap;transition:all 180ms cubic-bezier(.4,0,.2,1)}",
|
|
147
|
-
"@media (prefers-color-scheme: dark){.dshfs-updatePill{color:#0a84ff;background:rgba(10,132,255,.15);border-color:rgba(10,132,255,.25)}}",
|
|
148
|
-
".dshfs-updatePill:hover:not(:disabled){background:rgba(0,113,227,.15);border-color:rgba(0,113,227,.3)}",
|
|
149
|
-
"@media (prefers-color-scheme: dark){.dshfs-updatePill:hover:not(:disabled){background:rgba(10,132,255,.2);border-color:rgba(10,132,255,.4)}}",
|
|
150
|
-
".dshfs-updatePill:disabled{opacity:.5;cursor:default}",
|
|
151
|
-
".dshfs-updateIcon{flex:none;display:block}",
|
|
152
141
|
".dshfs-version{color:#6e6e73;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}",
|
|
153
142
|
"@media (prefers-color-scheme: dark){.dshfs-version{color:#a1a1a6}}"
|
|
154
143
|
].join("");
|
|
@@ -255,18 +244,6 @@ window.__ModuleLoader__.load({
|
|
|
255
244
|
testOk: (r) => `✓ ${r.count} 条结果(引擎: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · 例如 "${r.sample.slice(0, 40)}"` : ""}`,
|
|
256
245
|
testFail: (e) => `✗ ${e}`,
|
|
257
246
|
toggleLang: "EN",
|
|
258
|
-
checkUpdate: "检查更新",
|
|
259
|
-
checkingUpdate: "检查中…",
|
|
260
|
-
updateAvailable: (c, l) => `发现新版本 v${l}(当前 v${c})`,
|
|
261
|
-
updateLatest: (c) => `已是最新版本 v${c}`,
|
|
262
|
-
updateCheckFailed: "检查更新失败(无法访问 npm registry)",
|
|
263
|
-
updateView: "查看 →",
|
|
264
|
-
hasUpdate: "有更新",
|
|
265
|
-
upgrade: "升级",
|
|
266
|
-
upgrading: "升级中…",
|
|
267
|
-
upgradeLinkMode: "(本地开发模式,升级请用 git pull)",
|
|
268
|
-
upgradeDone: (l) => `升级到 v${l} 完成,重启 dsh 后生效`,
|
|
269
|
-
upgradeFailed: (m) => `升级失败:${m}`,
|
|
270
247
|
safeSearchLabel: "安全搜索过滤 (adlt)",
|
|
271
248
|
safeSearchOff: "关闭 —— 引擎默认(不加参数)",
|
|
272
249
|
safeSearchModerate: "中等 —— Bing 默认",
|
|
@@ -388,18 +365,6 @@ window.__ModuleLoader__.load({
|
|
|
388
365
|
testOk: (r) => `✓ ${r.count} results (engine: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · e.g. "${r.sample.slice(0, 40)}"` : ""}`,
|
|
389
366
|
testFail: (e) => `✗ ${e}`,
|
|
390
367
|
toggleLang: "中文",
|
|
391
|
-
checkUpdate: "Check update",
|
|
392
|
-
checkingUpdate: "Checking…",
|
|
393
|
-
updateAvailable: (c, l) => `New version v${l} available (current v${c})`,
|
|
394
|
-
updateLatest: (c) => `You're on the latest version v${c}`,
|
|
395
|
-
updateCheckFailed: "Update check failed (cannot reach npm registry)",
|
|
396
|
-
updateView: "View →",
|
|
397
|
-
hasUpdate: "Update available",
|
|
398
|
-
upgrade: "Upgrade",
|
|
399
|
-
upgrading: "Upgrading…",
|
|
400
|
-
upgradeLinkMode: "(local dev install - use git pull to update)",
|
|
401
|
-
upgradeDone: (l) => `Upgraded to v${l} - restart dsh to apply`,
|
|
402
|
-
upgradeFailed: (m) => `Upgrade failed: ${m}`,
|
|
403
368
|
safeSearchLabel: "Safe search filter (adlt)",
|
|
404
369
|
safeSearchOff: "Off - engine default (no filtering)",
|
|
405
370
|
safeSearchModerate: "Moderate - Bing default",
|
|
@@ -437,7 +402,7 @@ window.__ModuleLoader__.load({
|
|
|
437
402
|
};
|
|
438
403
|
const tt = (lang) => I18N[String(lang ?? "").trim().toLowerCase() === "zh" ? "zh" : "en"];
|
|
439
404
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
|
|
440
|
-
const PLUGIN_VERSION = "0.9.
|
|
405
|
+
const PLUGIN_VERSION = "0.9.22";
|
|
441
406
|
const PLUGIN_REPO_URL = "https://github.com/DDDMUC/dsh-free-search";
|
|
442
407
|
// 右侧浮动目录(#85):条目顺序即页面顺序,anchor 是页面里对应 field 的 id。
|
|
443
408
|
const NAV_ITEMS = [
|
|
@@ -585,15 +550,6 @@ window.__ModuleLoader__.load({
|
|
|
585
550
|
return response.json();
|
|
586
551
|
}
|
|
587
552
|
|
|
588
|
-
async function bridgeCheckUpdate() {
|
|
589
|
-
const response = await fetch(`${BRIDGE_PREFIX}/check-update`, {
|
|
590
|
-
method: "POST",
|
|
591
|
-
headers: { "content-type": "application/json" },
|
|
592
|
-
body: "{}",
|
|
593
|
-
});
|
|
594
|
-
return response.json();
|
|
595
|
-
}
|
|
596
|
-
|
|
597
553
|
async function bridgeCredentialsStatus() {
|
|
598
554
|
const response = await fetch(`${BRIDGE_PREFIX}/credentials-status`, {
|
|
599
555
|
method: "POST",
|
|
@@ -736,9 +692,6 @@ window.__ModuleLoader__.load({
|
|
|
736
692
|
const [saveError, setSaveError] = react.useState("");
|
|
737
693
|
const [testing, setTesting] = react.useState(false);
|
|
738
694
|
const [testResult, setTestResult] = react.useState(null);
|
|
739
|
-
const [checkingUpdate, setCheckingUpdate] = react.useState(false);
|
|
740
|
-
const [upgrading, setUpgrading] = react.useState(false);
|
|
741
|
-
const [updateInfo, setUpdateInfo] = react.useState(null);
|
|
742
695
|
// 回退优先级/池列表行内 key 输入:引擎 id → [当前值, setter](zhihu 两引擎共享 ZHIHU_API_KEY)
|
|
743
696
|
const KEY_INPUTS = {
|
|
744
697
|
anysearch: [anysearchKey, setAnysearchKey],
|
|
@@ -1040,51 +993,6 @@ window.__ModuleLoader__.load({
|
|
|
1040
993
|
}
|
|
1041
994
|
};
|
|
1042
995
|
|
|
1043
|
-
const runCheckUpdate = async () => {
|
|
1044
|
-
setCheckingUpdate(true);
|
|
1045
|
-
setUpdateInfo(null);
|
|
1046
|
-
setFailed(false);
|
|
1047
|
-
setSaveError("");
|
|
1048
|
-
try {
|
|
1049
|
-
const result = await bridgeCheckUpdate();
|
|
1050
|
-
if (result.ok) {
|
|
1051
|
-
setUpdateInfo({ ok: true, ...result.value });
|
|
1052
|
-
} else {
|
|
1053
|
-
setUpdateInfo({ ok: false });
|
|
1054
|
-
}
|
|
1055
|
-
} catch {
|
|
1056
|
-
setUpdateInfo({ ok: false });
|
|
1057
|
-
} finally {
|
|
1058
|
-
setCheckingUpdate(false);
|
|
1059
|
-
}
|
|
1060
|
-
};
|
|
1061
|
-
|
|
1062
|
-
const runUpdate = async () => {
|
|
1063
|
-
setUpgrading(true);
|
|
1064
|
-
setUpdateInfo(null);
|
|
1065
|
-
setFailed(false);
|
|
1066
|
-
setSaveError("");
|
|
1067
|
-
try {
|
|
1068
|
-
const response = await fetch(`${BRIDGE_PREFIX}/update`, {
|
|
1069
|
-
method: "POST",
|
|
1070
|
-
headers: { "content-type": "application/json" },
|
|
1071
|
-
body: "{}",
|
|
1072
|
-
});
|
|
1073
|
-
const result = await response.json();
|
|
1074
|
-
if (result.ok) {
|
|
1075
|
-
setUpdateInfo({ ok: true, hasUpdate: false, upgraded: true, message: result.value.message, latest: result.value.latest });
|
|
1076
|
-
} else {
|
|
1077
|
-
setUpdateInfo({ ok: false, upgradeFailed: result.message ?? "upgrade failed" });
|
|
1078
|
-
}
|
|
1079
|
-
} catch {
|
|
1080
|
-
setUpdateInfo({ ok: false, upgradeFailed: "request failed" });
|
|
1081
|
-
} finally {
|
|
1082
|
-
setUpgrading(false);
|
|
1083
|
-
}
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
// 浮动目录(#85):放在卡片右侧的页面留白里,绝对定位不参与卡片布局,
|
|
1087
|
-
// 所以正文宽度和 0.9.8 完全一致;留白不足(窄窗口/卡片贴边)时直接隐藏。
|
|
1088
996
|
react.useEffect(() => {
|
|
1089
997
|
if (!open) return undefined;
|
|
1090
998
|
const place = () => {
|
|
@@ -1327,6 +1235,7 @@ window.__ModuleLoader__.load({
|
|
|
1327
1235
|
|
|
1328
1236
|
return react_jsx_runtime.jsx("li", {
|
|
1329
1237
|
className: (open ? "dshfs-card dshfs-cardOpen" : "dshfs-card") + (pageMode ? " dshfs-pageMode" : ""),
|
|
1238
|
+
"data-dsh-fs-section": railActive,
|
|
1330
1239
|
children: [
|
|
1331
1240
|
react_jsx_runtime.jsx("button", {
|
|
1332
1241
|
type: "button",
|
|
@@ -1373,6 +1282,7 @@ window.__ModuleLoader__.load({
|
|
|
1373
1282
|
react_jsx_runtime.jsx("button", {
|
|
1374
1283
|
type: "button",
|
|
1375
1284
|
className: "dshfs-railItem" + (railActive === item.id ? " dshfs-railItemActive" : ""),
|
|
1285
|
+
"data-dsh-fs-section": item.id,
|
|
1376
1286
|
onClick: () => goToSection(item.id),
|
|
1377
1287
|
children: [
|
|
1378
1288
|
react_jsx_runtime.jsx("span", { className: "dshfs-railDot" }),
|
|
@@ -2023,86 +1933,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
2023
1933
|
className: "dshfs-footerLeft",
|
|
2024
1934
|
children: [
|
|
2025
1935
|
react_jsx_runtime.jsx("span", { className: "dshfs-version", children: "v" + PLUGIN_VERSION }),
|
|
2026
|
-
|
|
2027
|
-
? react_jsx_runtime.jsx("a", {
|
|
2028
|
-
className: "dshfs-updatePill",
|
|
2029
|
-
href: updateInfo.updateUrl,
|
|
2030
|
-
target: "_blank",
|
|
2031
|
-
rel: "noopener noreferrer",
|
|
2032
|
-
title: t.updateAvailable(updateInfo.current, updateInfo.latest),
|
|
2033
|
-
children: [
|
|
2034
|
-
react_jsx_runtime.jsx("svg", {
|
|
2035
|
-
className: "dshfs-updateIcon",
|
|
2036
|
-
viewBox: "0 0 16 16",
|
|
2037
|
-
width: 14,
|
|
2038
|
-
height: 14,
|
|
2039
|
-
"aria-hidden": "true",
|
|
2040
|
-
children: react_jsx_runtime.jsx("path", {
|
|
2041
|
-
d: "M8 2.2v6.4M5.2 6.4 8 9.2l2.8-2.8M3 10.8v1.4c0 .9.7 1.6 1.6 1.6h6.8c.9 0 1.6-.7 1.6-1.6v-1.4",
|
|
2042
|
-
fill: "none",
|
|
2043
|
-
stroke: "currentColor",
|
|
2044
|
-
strokeWidth: 1.6,
|
|
2045
|
-
strokeLinecap: "round",
|
|
2046
|
-
strokeLinejoin: "round",
|
|
2047
|
-
}),
|
|
2048
|
-
}),
|
|
2049
|
-
t.hasUpdate,
|
|
2050
|
-
],
|
|
2051
|
-
})
|
|
2052
|
-
: react_jsx_runtime.jsx("button", {
|
|
2053
|
-
className: "dshfs-updatePill",
|
|
2054
|
-
type: "button",
|
|
2055
|
-
title: updateInfo && updateInfo.ok && updateInfo.hasUpdate ? t.updateAvailable(updateInfo.current, updateInfo.latest) : undefined,
|
|
2056
|
-
onClick: updateInfo && updateInfo.ok && updateInfo.hasUpdate ? runUpdate : runCheckUpdate,
|
|
2057
|
-
disabled: upgrading || checkingUpdate || saving || !ready,
|
|
2058
|
-
children: [
|
|
2059
|
-
react_jsx_runtime.jsx("svg", {
|
|
2060
|
-
className: "dshfs-updateIcon",
|
|
2061
|
-
viewBox: "0 0 16 16",
|
|
2062
|
-
width: 14,
|
|
2063
|
-
height: 14,
|
|
2064
|
-
"aria-hidden": "true",
|
|
2065
|
-
children: react_jsx_runtime.jsx("path", {
|
|
2066
|
-
d: "M8 2.2v6.4M5.2 6.4 8 9.2l2.8-2.8M3 10.8v1.4c0 .9.7 1.6 1.6 1.6h6.8c.9 0 1.6-.7 1.6-1.6v-1.4",
|
|
2067
|
-
fill: "none",
|
|
2068
|
-
stroke: "currentColor",
|
|
2069
|
-
strokeWidth: 1.6,
|
|
2070
|
-
strokeLinecap: "round",
|
|
2071
|
-
strokeLinejoin: "round",
|
|
2072
|
-
}),
|
|
2073
|
-
}),
|
|
2074
|
-
upgrading
|
|
2075
|
-
? t.upgrading
|
|
2076
|
-
: checkingUpdate
|
|
2077
|
-
? t.checkingUpdate
|
|
2078
|
-
: updateInfo && updateInfo.ok && updateInfo.hasUpdate
|
|
2079
|
-
? t.hasUpdate
|
|
2080
|
-
: t.checkUpdate,
|
|
2081
|
-
],
|
|
2082
|
-
}),
|
|
2083
|
-
updateInfo && updateInfo.ok
|
|
2084
|
-
? updateInfo.upgraded
|
|
2085
|
-
? react_jsx_runtime.jsx("span", {
|
|
2086
|
-
className: "dshfs-updateOk",
|
|
2087
|
-
children: t.upgradeDone(updateInfo.latest),
|
|
2088
|
-
})
|
|
2089
|
-
: updateInfo.hasUpdate
|
|
2090
|
-
? updateInfo.installable
|
|
2091
|
-
? null
|
|
2092
|
-
: react_jsx_runtime.jsx("span", {
|
|
2093
|
-
className: "dshfs-version",
|
|
2094
|
-
children: t.upgradeLinkMode,
|
|
2095
|
-
})
|
|
2096
|
-
: react_jsx_runtime.jsx("span", {
|
|
2097
|
-
className: "dshfs-updateOk",
|
|
2098
|
-
children: t.updateLatest(updateInfo.current),
|
|
2099
|
-
})
|
|
2100
|
-
: updateInfo && !updateInfo.ok
|
|
2101
|
-
? react_jsx_runtime.jsx("span", {
|
|
2102
|
-
className: "dshfs-failed",
|
|
2103
|
-
children: updateInfo.upgradeFailed ? t.upgradeFailed(updateInfo.upgradeFailed) : t.updateCheckFailed,
|
|
2104
|
-
})
|
|
2105
|
-
: null,
|
|
1936
|
+
|
|
2106
1937
|
],
|
|
2107
1938
|
}),
|
|
2108
1939
|
react_jsx_runtime.jsx("div", {
|
|
@@ -2187,6 +2018,21 @@ react_jsx_runtime.jsx("div", {
|
|
|
2187
2018
|
// description 必须传函数:ui-commands 读回的是 contribution.description(),
|
|
2188
2019
|
// 传字符串会抛 TypeError: contribution.description is not a function;该异常
|
|
2189
2020
|
// 会让整份 "/" 候选列表一起失败,菜单空白、其他命令也一起点不到。
|
|
2021
|
+
// #101: also claim the bundle-level seat (keyed by the package name) so the
|
|
2022
|
+
// card renders on the bundle page itself, between the description and the
|
|
2023
|
+
// rows. The row-level seat stays registered for the older entry point.
|
|
2024
|
+
ctx.slots.inject("plugins.bundle.config", () =>
|
|
2025
|
+
ctx.slots.register(
|
|
2026
|
+
{
|
|
2027
|
+
name: "plugins.bundle.config",
|
|
2028
|
+
key: "dsh-free-search",
|
|
2029
|
+
},
|
|
2030
|
+
(slotProps) =>
|
|
2031
|
+
slotProps && slotProps.view === "summary"
|
|
2032
|
+
? summaryText()
|
|
2033
|
+
: react_jsx_runtime.jsx(FreeSearchCard, { page: true })
|
|
2034
|
+
)
|
|
2035
|
+
);
|
|
2190
2036
|
ctx.inject(["commandUi"], (sctx) => {
|
|
2191
2037
|
const command = sctx.get("commandUi");
|
|
2192
2038
|
sctx.effect(() => {
|
package/lib/domain_filter.js
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
// #72 域名黑名单:在结果进入上下文前过滤内容农场(CSDN 等)。
|
|
2
|
-
// 匹配语义(issue #72 约定):
|
|
3
|
-
// - 域名相等或其子域命中(csdn.net 覆盖 blog.csdn.net)
|
|
4
|
-
// - 忽略大小写;www. 前缀归一;无 scheme / 无路径;
|
|
5
|
-
// - 不做正则(notcsdn.net 不得被 csdn.net 命中)
|
|
6
|
-
// 生效方式:provider.search 入口按当次请求的 blockedDomains 设置"活动黑名单",
|
|
7
|
-
// uniqueSources 在收敛(去重 + limit)之前过滤——被屏蔽条目让位给后续候选,
|
|
8
|
-
// 因此 maxResults 仍能填满,且 truncated 只在真正触及上限时为 true。
|
|
9
|
-
|
|
10
|
-
let active = [];
|
|
11
|
-
/**
|
|
12
|
-
* 配置清洗:把用户随手填的域名/URL 归一成裸域名规则。
|
|
13
|
-
* 处理 trim、scheme、路径/query/hash、端口、www.、大小写、去重(保序)、丢弃空值与非法行。
|
|
14
|
-
*
|
|
15
|
-
* 为什么要服务端兜底:blockedDomains 是 .volatile() 配置,用户可以直接编辑 YAML 绕过 UI,
|
|
16
|
-
* 所以规范化必须在 setActiveBlockedDomains 里做,而不是只做在设置页。
|
|
17
|
-
* isDomainBlocked 的匹配语义不受影响——规则只是被清洗成更可靠的等价形式。
|
|
18
|
-
*/
|
|
19
|
-
export function normalizeDomainList(list) {
|
|
20
|
-
if (!Array.isArray(list)) return [];
|
|
21
|
-
const out = [];
|
|
22
|
-
const seen = new Set();
|
|
23
|
-
for (const raw of list) {
|
|
24
|
-
let s = String(raw ?? "").trim().toLowerCase();
|
|
25
|
-
if (!s) continue;
|
|
26
|
-
// 去 scheme(http:// https:// 或其它的 protocol://)
|
|
27
|
-
s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
28
|
-
// 去路径 / query / hash
|
|
29
|
-
const cut = s.search(/[/?#]/);
|
|
30
|
-
if (cut >= 0) s = s.slice(0, cut);
|
|
31
|
-
if (!s) continue;
|
|
32
|
-
// 去端口(仅当冒号后是纯数字)
|
|
33
|
-
const colon = s.lastIndexOf(":");
|
|
34
|
-
if (colon >= 0 && /^\d+$/.test(s.slice(colon + 1))) s = s.slice(0, colon);
|
|
35
|
-
// 去 www. 前缀与尾随点号
|
|
36
|
-
s = s.replace(/^www\./, "").replace(/\.+$/, "");
|
|
37
|
-
// 只接受"看起来像域名"的行:空格、下划线、中文等直接丢弃(它们永远匹配不到 host)
|
|
38
|
-
if (!s || !/^[a-z0-9][a-z0-9.-]*$/.test(s) || s.startsWith(".")) continue;
|
|
39
|
-
if (seen.has(s)) continue;
|
|
40
|
-
seen.add(s);
|
|
41
|
-
out.push(s);
|
|
42
|
-
}
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** 设置本次搜索生效的黑名单(provider.search / research 入口调用)。空数组 = 不过滤。 */
|
|
47
|
-
export function setActiveBlockedDomains(list) {
|
|
48
|
-
active = normalizeDomainList(list);
|
|
49
|
-
}
|
|
50
|
-
export function getActiveBlockedDomains() {
|
|
51
|
-
return [...active];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normalizeHost(host) {
|
|
55
|
-
return String(host ?? "").toLowerCase().trim().replace(/^www\./, "");
|
|
56
|
-
}
|
|
57
|
-
function normalizeRule(domain) {
|
|
58
|
-
return String(domain ?? "").toLowerCase().trim().replace(/^www\./, "").replace(/\/+$/, "");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 单个 URL 是否命中黑名单;URL 无法解析时返回 false(不误伤)。 */
|
|
62
|
-
export function isDomainBlocked(url, blocked = active) {
|
|
63
|
-
if (!blocked || blocked.length === 0) return false;
|
|
64
|
-
let host;
|
|
65
|
-
try {
|
|
66
|
-
host = normalizeHost(new URL(String(url)).hostname);
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
if (!host) return false;
|
|
71
|
-
return blocked.some((raw) => {
|
|
72
|
-
const rule = normalizeRule(raw);
|
|
73
|
-
if (!rule) return false;
|
|
74
|
-
return host === rule || host.endsWith("." + rule);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 列表过滤:结果条目统一为 { url } 形态,命中黑名的整条丢弃(保持原顺序=让位语义)。 */
|
|
79
|
-
export function filterBlockedDomains(items, blocked = active) {
|
|
80
|
-
if (!blocked || blocked.length === 0) return items;
|
|
81
|
-
return items.filter((item) => !isDomainBlocked(item?.url, blocked));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 结果级收口:引擎返回的 { sources } 在交给上层(provider 返回值 / multi 合并 /
|
|
86
|
-
* runEngineTest / 工具 execute)之前统一过滤。
|
|
87
|
-
*
|
|
88
|
-
* 为什么需要它:uniqueSources 只是 40+ 个引擎里 29 个的"收敛去重"helper,
|
|
89
|
-
* searchAnysearch / searchPlatform 等 11 个引擎并不经过它(各自直接 return sources)。
|
|
90
|
-
* 只在 uniqueSources 里过滤会漏掉这些引擎——#72 首版就是这么漏的。
|
|
91
|
-
* 因此过滤必须落在"结果离开引擎"的所有咽喉点上,而不是某一个 helper 里。
|
|
92
|
-
*
|
|
93
|
-
* 无黑名单 / 无命中时原样返回(零分配),调用方可以直接重新赋值。
|
|
94
|
-
*/
|
|
95
|
-
export function applyBlockedFilterToResult(result) {
|
|
96
|
-
if (!result || !Array.isArray(result.sources)) return result;
|
|
97
|
-
const sources = filterBlockedDomains(result.sources);
|
|
98
|
-
if (sources.length === result.sources.length) return result;
|
|
99
|
-
return { ...result, sources };
|
|
100
|
-
}
|
|
1
|
+
// #72 域名黑名单:在结果进入上下文前过滤内容农场(CSDN 等)。
|
|
2
|
+
// 匹配语义(issue #72 约定):
|
|
3
|
+
// - 域名相等或其子域命中(csdn.net 覆盖 blog.csdn.net)
|
|
4
|
+
// - 忽略大小写;www. 前缀归一;无 scheme / 无路径;
|
|
5
|
+
// - 不做正则(notcsdn.net 不得被 csdn.net 命中)
|
|
6
|
+
// 生效方式:provider.search 入口按当次请求的 blockedDomains 设置"活动黑名单",
|
|
7
|
+
// uniqueSources 在收敛(去重 + limit)之前过滤——被屏蔽条目让位给后续候选,
|
|
8
|
+
// 因此 maxResults 仍能填满,且 truncated 只在真正触及上限时为 true。
|
|
9
|
+
|
|
10
|
+
let active = [];
|
|
11
|
+
/**
|
|
12
|
+
* 配置清洗:把用户随手填的域名/URL 归一成裸域名规则。
|
|
13
|
+
* 处理 trim、scheme、路径/query/hash、端口、www.、大小写、去重(保序)、丢弃空值与非法行。
|
|
14
|
+
*
|
|
15
|
+
* 为什么要服务端兜底:blockedDomains 是 .volatile() 配置,用户可以直接编辑 YAML 绕过 UI,
|
|
16
|
+
* 所以规范化必须在 setActiveBlockedDomains 里做,而不是只做在设置页。
|
|
17
|
+
* isDomainBlocked 的匹配语义不受影响——规则只是被清洗成更可靠的等价形式。
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeDomainList(list) {
|
|
20
|
+
if (!Array.isArray(list)) return [];
|
|
21
|
+
const out = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const raw of list) {
|
|
24
|
+
let s = String(raw ?? "").trim().toLowerCase();
|
|
25
|
+
if (!s) continue;
|
|
26
|
+
// 去 scheme(http:// https:// 或其它的 protocol://)
|
|
27
|
+
s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
28
|
+
// 去路径 / query / hash
|
|
29
|
+
const cut = s.search(/[/?#]/);
|
|
30
|
+
if (cut >= 0) s = s.slice(0, cut);
|
|
31
|
+
if (!s) continue;
|
|
32
|
+
// 去端口(仅当冒号后是纯数字)
|
|
33
|
+
const colon = s.lastIndexOf(":");
|
|
34
|
+
if (colon >= 0 && /^\d+$/.test(s.slice(colon + 1))) s = s.slice(0, colon);
|
|
35
|
+
// 去 www. 前缀与尾随点号
|
|
36
|
+
s = s.replace(/^www\./, "").replace(/\.+$/, "");
|
|
37
|
+
// 只接受"看起来像域名"的行:空格、下划线、中文等直接丢弃(它们永远匹配不到 host)
|
|
38
|
+
if (!s || !/^[a-z0-9][a-z0-9.-]*$/.test(s) || s.startsWith(".")) continue;
|
|
39
|
+
if (seen.has(s)) continue;
|
|
40
|
+
seen.add(s);
|
|
41
|
+
out.push(s);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 设置本次搜索生效的黑名单(provider.search / research 入口调用)。空数组 = 不过滤。 */
|
|
47
|
+
export function setActiveBlockedDomains(list) {
|
|
48
|
+
active = normalizeDomainList(list);
|
|
49
|
+
}
|
|
50
|
+
export function getActiveBlockedDomains() {
|
|
51
|
+
return [...active];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeHost(host) {
|
|
55
|
+
return String(host ?? "").toLowerCase().trim().replace(/^www\./, "");
|
|
56
|
+
}
|
|
57
|
+
function normalizeRule(domain) {
|
|
58
|
+
return String(domain ?? "").toLowerCase().trim().replace(/^www\./, "").replace(/\/+$/, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 单个 URL 是否命中黑名单;URL 无法解析时返回 false(不误伤)。 */
|
|
62
|
+
export function isDomainBlocked(url, blocked = active) {
|
|
63
|
+
if (!blocked || blocked.length === 0) return false;
|
|
64
|
+
let host;
|
|
65
|
+
try {
|
|
66
|
+
host = normalizeHost(new URL(String(url)).hostname);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (!host) return false;
|
|
71
|
+
return blocked.some((raw) => {
|
|
72
|
+
const rule = normalizeRule(raw);
|
|
73
|
+
if (!rule) return false;
|
|
74
|
+
return host === rule || host.endsWith("." + rule);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 列表过滤:结果条目统一为 { url } 形态,命中黑名的整条丢弃(保持原顺序=让位语义)。 */
|
|
79
|
+
export function filterBlockedDomains(items, blocked = active) {
|
|
80
|
+
if (!blocked || blocked.length === 0) return items;
|
|
81
|
+
return items.filter((item) => !isDomainBlocked(item?.url, blocked));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 结果级收口:引擎返回的 { sources } 在交给上层(provider 返回值 / multi 合并 /
|
|
86
|
+
* runEngineTest / 工具 execute)之前统一过滤。
|
|
87
|
+
*
|
|
88
|
+
* 为什么需要它:uniqueSources 只是 40+ 个引擎里 29 个的"收敛去重"helper,
|
|
89
|
+
* searchAnysearch / searchPlatform 等 11 个引擎并不经过它(各自直接 return sources)。
|
|
90
|
+
* 只在 uniqueSources 里过滤会漏掉这些引擎——#72 首版就是这么漏的。
|
|
91
|
+
* 因此过滤必须落在"结果离开引擎"的所有咽喉点上,而不是某一个 helper 里。
|
|
92
|
+
*
|
|
93
|
+
* 无黑名单 / 无命中时原样返回(零分配),调用方可以直接重新赋值。
|
|
94
|
+
*/
|
|
95
|
+
export function applyBlockedFilterToResult(result) {
|
|
96
|
+
if (!result || !Array.isArray(result.sources)) return result;
|
|
97
|
+
const sources = filterBlockedDomains(result.sources);
|
|
98
|
+
if (sources.length === result.sources.length) return result;
|
|
99
|
+
return { ...result, sources };
|
|
100
|
+
}
|