dsh-free-search 0.5.0 → 0.5.6

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 CHANGED
@@ -186,7 +186,9 @@ dsh web
186
186
 
187
187
  #### 配置文件
188
188
 
189
- DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config`。旧版 `~/.dsh/settings.yaml` 的 `free-search:` 段只会在启动时自动导入一次,随后原文件被重命名为 `settings.yaml.imported`。
189
+ DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config`。
190
+
191
+ 旧版 `~/.dsh/settings.yaml` 的 `free-search:` 段**不会被 DSH 核心自动导入**(核心的 `importLegacyDocument` 只为 `ui-developer-tools` / `ui-onboarding` / `shell` 三个段提供了映射),原文件在导入其它段后会被改名为 `settings.yaml.imported`,该段的值只留在那里。插件会在启动时检测 `settings.yaml.imported`(或仍存在的 `settings.yaml`)中的 `free-search:` 段,把可识别的字段**一次性补种**进当前 profile 的条目 `config`(写一次后不再重复,可在启动日志看到 `free-search: migrated N field(s)…`)。如果你想手动处理,也可以照着 `settings.yaml.imported` 里的值在插件页行配置中填一遍。
190
192
 
191
193
  ```yaml
192
194
  # profiles/<profile>/cordis.patch.yml 中该条目的 config:
@@ -495,7 +497,9 @@ The command only changes the preferred engine; search still goes through `web_se
495
497
 
496
498
  #### Configuration File
497
499
 
498
- Since DSH 0.1.7-rc.1 the configuration is stored with the profile's plugin entry: the settings page and `/free-search-engine` both write the `config` of the `web-search-free` (`dsh-free-search`) entry in the active profile's `cordis.patch.yml`. The old `free-search:` section of `~/.dsh/settings.yaml` is imported once at startup; the file is then renamed to `settings.yaml.imported`.
500
+ Since DSH 0.1.7-rc.1 the configuration is stored with the profile's plugin entry: the settings page and `/free-search-engine` both write the `config` of the `web-search-free` (`dsh-free-search`) entry in the active profile's `cordis.patch.yml`.
501
+
502
+ The old `free-search:` section of `~/.dsh/settings.yaml` is **not** imported by the DSH core (`importLegacyDocument` only maps `ui-developer-tools` / `ui-onboarding` / `shell`); the file is renamed to `settings.yaml.imported` after the other sections are imported, and the section's values stay there. The plugin detects that section in `settings.yaml.imported` (or in a still-present `settings.yaml`) at startup and seeds the recognized fields into this entry's `config` **once** (watch for `free-search: migrated N field(s)…` in the startup log). You can also migrate manually by copying the values from `settings.yaml.imported` into the plugin row config.
499
503
 
500
504
  ```yaml
501
505
  # config of that entry in profiles/<profile>/cordis.patch.yml:
package/cordis.patch.yml CHANGED
@@ -1,16 +1,19 @@
1
1
  # dsh-free-search — free web search providers (no API key needed)
2
- # Registers the free-search plugin into ctx.web + a settings bridge
3
- # (/api/dsh-free-search-settings) that serves the "free-search" namespace.
4
- # The engine (ddg / ddg-lite / bing) is chosen from the web Settings page:
5
- # DSH 0.1.6-alpha.2+: sidebar Plugins page -> Installed -> free-search -> row config
6
- # DSH 0.1.5 and earlier: Settings > Plugins > Configurable > Free Search
7
- # or from settings.yaml:
8
- # free-search:
9
- # provider: bing
10
- # region: cn-zh # DuckDuckGo region
11
- # bingMarket: zh-CN # Bing market
2
+ # Registers the plugin into ctx.web + a settings bridge served at
3
+ # /api/dsh-free-search-settings.
4
+ # The engine is chosen from the sidebar Plugins page -> Free Search row config
5
+ # (plugins.row.config, DSH 0.1.7-rc.1+), from the /free-search-engine command,
6
+ # or directly via this entry's config, e.g.:
7
+ # provider: bing # engine id: ddg / bing / baidu / kimi / aliyun / auto / ...
8
+ # region: cn-zh # DuckDuckGo region
9
+ # bingMarket: zh-CN # Bing market
10
+ # A legacy ~/.dsh/settings.yaml "free-search:" section is migrated into this
11
+ # entry's config once at startup (see README).
12
12
  # Failed/empty engines automatically fall back to the next free engine.
13
- # web.searchProvider must point at this plugin's provider id (ddg).
13
+ # web.searchProvider must point at this plugin's provider id (ddg); the runtime
14
+ # also takes over when it is unset or still the shipped default
15
+ # (deepseek-official), and only warns when another provider was chosen
16
+ # explicitly.
14
17
  - insert:
15
18
  - id: web-search-free
16
19
  name: dsh-free-search
package/lib/client.js CHANGED
@@ -265,7 +265,7 @@ window.__ModuleLoader__.load({
265
265
  };
266
266
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
267
267
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
268
- const PLUGIN_VERSION = "0.5.0";
268
+ const PLUGIN_VERSION = "0.5.6";
269
269
  const ENGINES = [
270
270
  { id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
271
271
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@ import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import fs from "node:fs";
5
+ import os from "node:os";
5
6
  import path from "node:path";
6
7
  import { exec } from "node:child_process";
7
8
 
@@ -81,7 +82,7 @@ function routeEngines(query, { timeRange } = {}) {
81
82
  }
82
83
 
83
84
  // 当前插件版本(发布时与 package.json 同步)
84
- const PLUGIN_VERSION = "0.5.0";
85
+ const PLUGIN_VERSION = "0.5.6";
85
86
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
86
87
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
87
88
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -2181,8 +2182,104 @@ const Config = z.object({
2181
2182
  baiduApiKey: z.string().role("secret").volatile(),
2182
2183
  kimiApiKey: z.string().role("secret").volatile(),
2183
2184
  aliyunApiKey: z.string().role("secret").volatile(),
2185
+ // 旧版 settings.yaml `free-search:` 段的一次性迁移标记(见 readLegacyFreeSearchSection)
2186
+ legacyYamlMigrated: z.boolean().default(false).volatile(),
2184
2187
  });
2185
2188
 
2189
+ // ── 旧版 settings.yaml `free-search:` 段的迁移 ─────────────────────────────────
2190
+ // DSH 核心的 importLegacyDocument 只为 ui-developer-tools / ui-onboarding / shell
2191
+ // 三个段提供了映射;`free-search:` 段无法自动导入,值只会留在启动时被改名的
2192
+ // settings.yaml.imported 里。这里解析该段,并在首次启动时补种进本条目 config,
2193
+ // 用 Config.legacyYamlMigrated 去重(只写一次)。
2194
+ const LEGACY_MIGRATABLE_KEYS = [
2195
+ "provider",
2196
+ "cache",
2197
+ "cacheTtl",
2198
+ "keyStorage",
2199
+ "lang",
2200
+ "region",
2201
+ "bingMarket",
2202
+ "safeSearch",
2203
+ "searxngInstances",
2204
+ "platforms",
2205
+ "anysearchApiKey",
2206
+ "exaApiKey",
2207
+ "tavilyApiKey",
2208
+ "keenableApiKey",
2209
+ "firecrawlApiKey",
2210
+ "parallelApiKey",
2211
+ "perplexityApiKey",
2212
+ "serpbaseApiKey",
2213
+ "deepseekApiKey",
2214
+ "youcomApiKey",
2215
+ "baiduApiKey",
2216
+ "kimiApiKey",
2217
+ "aliyunApiKey",
2218
+ ];
2219
+
2220
+ function dshHomeDir() {
2221
+ return process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
2222
+ }
2223
+
2224
+ /** 解析 YAML 标量:引号字符串、布尔、null、数字、内联数组;其余按原样字符串。 */
2225
+ function parseLegacyScalar(raw) {
2226
+ if (raw === "true") return true;
2227
+ if (raw === "false") return false;
2228
+ if (raw === "null" || raw === "~") return null;
2229
+ const quoted = raw.match(/^(['"])([\s\S]*)\1$/);
2230
+ if (quoted) return quoted[2];
2231
+ if (/^-?\d+$/.test(raw)) return Number(raw);
2232
+ if (/^-?\d+\.\d+$/.test(raw)) return Number(raw);
2233
+ if (raw.startsWith("[") && raw.endsWith("]")) {
2234
+ return raw
2235
+ .slice(1, -1)
2236
+ .split(",")
2237
+ .map((part) => part.trim())
2238
+ .filter(Boolean)
2239
+ .map((part) => parseLegacyScalar(part))
2240
+ .filter((value) => value !== null);
2241
+ }
2242
+ return raw;
2243
+ }
2244
+
2245
+ /** 从 settings.yaml 文本里抽出顶层 `free-search:` 段的扁平键值(忽略嵌套块/块状列表)。 */
2246
+ function parseLegacyFreeSearchSection(text) {
2247
+ const lines = String(text).split(/\r?\n/);
2248
+ let start = -1;
2249
+ for (let i = 0; i < lines.length; i++) {
2250
+ if (/^free-search:\s*(#.*)?$/.test(lines[i])) {
2251
+ start = i;
2252
+ break;
2253
+ }
2254
+ }
2255
+ if (start === -1) return null;
2256
+ const section = {};
2257
+ for (let i = start + 1; i < lines.length; i++) {
2258
+ const line = lines[i];
2259
+ if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
2260
+ if (/^\S/.test(line)) break; // 下一个顶层键
2261
+ const kv = line.match(/^\s+([A-Za-z0-9_-]+):\s*(.*)$/);
2262
+ if (!kv) continue;
2263
+ const value = kv[2].replace(/\s+#.*$/, "").trim();
2264
+ if (value === "") continue; // 嵌套块(map / 块状列表):跳过,保留人工处理
2265
+ section[kv[1]] = parseLegacyScalar(value);
2266
+ }
2267
+ return section;
2268
+ }
2269
+
2270
+ /** 依次尝试 settings.yaml.imported(导入后的遗留段)与 settings.yaml。 */
2271
+ function readLegacyFreeSearchSection() {
2272
+ const home = dshHomeDir();
2273
+ for (const name of ["settings.yaml.imported", "settings.yaml"]) {
2274
+ try {
2275
+ const text = fs.readFileSync(path.join(home, name), "utf8");
2276
+ const section = parseLegacyFreeSearchSection(text);
2277
+ if (section !== null && Object.keys(section).length > 0) return { file: name, section };
2278
+ } catch {}
2279
+ }
2280
+ return null;
2281
+ }
2282
+
2186
2283
  // Unwrap the live `Volatile<T>` references the Loader passes for `.volatile()`
2187
2284
  // fields into a plain object, once per read.
2188
2285
  function resolveConfig(config) {
@@ -2206,6 +2303,27 @@ function apply(ctx, config) {
2206
2303
  // 单 query 结果缓存(provider.search 内闭包持有):LRU 50 条 / TTL 可配置
2207
2304
  const searchCache = new Map(); // key -> { value, expiresAt }
2208
2305
 
2306
+ // 旧版 settings.yaml `free-search:` 段的一次性补种:核心不会导入它(只映射 3 个特例),
2307
+ // 这里在首次启动时把可识别的字段写进本条目 config,之后靠 legacyYamlMigrated 去重。
2308
+ const migrateLegacySettingsFile = async (settings) => {
2309
+ if (current().legacyYamlMigrated) return;
2310
+ const legacy = readLegacyFreeSearchSection();
2311
+ if (legacy === null) return;
2312
+ const cfg = current();
2313
+ const ops = [];
2314
+ for (const [key, value] of Object.entries(legacy.section)) {
2315
+ if (!LEGACY_MIGRATABLE_KEYS.includes(key)) continue;
2316
+ if (JSON.stringify(cfg[key]) === JSON.stringify(value)) continue;
2317
+ ops.push({ op: "set", path: [key], value });
2318
+ }
2319
+ if (ops.length === 0) return;
2320
+ ops.push({ op: "set", path: ["legacyYamlMigrated"], value: true });
2321
+ await settings.mutate(FREE_SEARCH_NS, ops);
2322
+ logger.info(
2323
+ `free-search: migrated ${ops.length - 1} field(s) from legacy ${legacy.file} "free-search:" section into this entry's config`
2324
+ );
2325
+ };
2326
+
2209
2327
  // key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
2210
2328
  const resolveApiKey = async (envName, settingsKey) => {
2211
2329
  const credentials = getCredentials();
@@ -2472,6 +2590,13 @@ function apply(ctx, config) {
2472
2590
  // page (`auto: false`) so the host does not also generate a schema form. Values
2473
2591
  // persist per profile in `cordis.patch.yml` under this entry (`FREE_SEARCH_NS`).
2474
2592
  sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
2593
+ // 旧版 settings.yaml 的 `free-search:` 段不会被核心自动导入(只映射 3 个特例),
2594
+ // 这里一次性补种进本条目 config;失败只警告,不影响插件启动。
2595
+ void migrateLegacySettingsFile(sctx.settings).catch((error) => {
2596
+ logger.warn(
2597
+ `free-search: legacy settings.yaml migration skipped (${error instanceof Error ? error.message : String(error)})`
2598
+ );
2599
+ });
2475
2600
  // Re-render the dynamic system-prompt engine list whenever this entry's config changes.
2476
2601
  sctx.on("settings/document-updated", (ns) => {
2477
2602
  if (String(ns) === FREE_SEARCH_NS && typeof refreshPrompt === "function") refreshPrompt();
@@ -2508,7 +2633,7 @@ function apply(ctx, config) {
2508
2633
  const previous = ctx.web.searchProviderId;
2509
2634
  ctx.web.searchProviderId = provider.id;
2510
2635
  logger.info(
2511
- previous === undefined
2636
+ previous === undefined || previous === null
2512
2637
  ? `free-search: web.searchProvider was unset, taking over as "${provider.id}"`
2513
2638
  : `free-search: taking over from the default provider "${previous}" as "${provider.id}"`
2514
2639
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.5.0",
3
+ "version": "0.5.6",
4
4
  "description": "Free web search for DeepSeek Harness: 17 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Perplexity/SerpBase/DeepSeek/You.com/Baidu/Kimi/Aliyun with key) + auto smart routing + multi_search cross-source tool + time filtering + platform search + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -54,11 +54,11 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@deepseek-ai/cordis": "^4.0.1",
57
- "@deepseek-ai/dsh-client-ui-commands": "^0.1.7-rc.1",
58
- "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.7-rc.1",
59
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
60
- "@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
61
- "@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
57
+ "@deepseek-ai/dsh-client-ui-commands": "^0.1.7-rc.1 || ^0.2.0-rc.1",
58
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.7-rc.1 || ^0.2.0-rc.1",
59
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1 || ^0.2.0-rc.1",
60
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.1 || ^0.2.0-rc.1",
61
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.1 || ^0.2.0-rc.1",
62
62
  "@deepseek-ai/schemastery": "^3.18.4",
63
63
  "react": "^18.2.0"
64
64
  },