dsh-bing-search 1.1.3 → 1.1.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.5] - 2026-09-29
4
+
5
+ ### Fixed
6
+ - 安装被 DSH `0.2.0-rc.1` 拒绝:`@deepseek-ai/dsh-web` 与 `@deepseek-ai/dsh-settings` 的 peer 上限从 `<0.2.0-0` 放宽到 `<0.3.0-0`。运行时只使用 `WebError`、`ctx.web.registerSearchProvider`,以及在存在时才调用的 `settings.installSection`。
7
+
8
+ ## [1.1.4] - 2026-09-28
9
+
10
+ ### Added
11
+ - DSH `>= 0.1.6` 的插件页配置表单(`client.js`):编辑搜索入口、语言和解析上限。旧版本仍走 `settings.installSection`。
12
+
13
+ ### Changed
14
+ - 配置字段在运行时支持 `volatile` 时标记为 volatile,搜索时解开热更新后的值;没有该方法的旧运行时保持原样。
15
+
3
16
  ## [1.1.3] - 2026-09-13
4
17
 
5
18
  ### Changed
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-bing-search
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![DSH Compatible](https://img.shields.io/badge/DSH-0.1.5--rc.1-brightgreen)](https://github.com/deepseek-ai/deepseek-harness)
4
+ [![DSH Compatible](https://img.shields.io/badge/DSH-0.1.5--rc.1%20..%20%3C0.3.0-brightgreen)](https://github.com/deepseek-ai/deepseek-harness)
5
5
  [![npm](https://img.shields.io/npm/v/dsh-bing-search.svg)](https://www.npmjs.com/package/dsh-bing-search)
6
6
 
7
7
  > DeepSeek Harness (DSH) 的免费 Bing 搜索 provider。无需 API Key,不消耗模型搜索配额。
@@ -10,7 +10,7 @@
10
10
  <a href="README_EN.md">English</a> | <b>中文</b>
11
11
  </p>
12
12
 
13
- > 本仓库是 [godchen520/dsh-web-search-bing](https://github.com/godchen520/dsh-web-search-bing) 的维护 fork,已适配 DSH `0.1.5-rc.1`。包名用 `dsh-bing-search`,因为 `dsh-web-search-bing` 已被占用,`dsh-websearch-bing` 又被 npm 判定过于相似。
13
+ > 本仓库是 [godchen520/dsh-web-search-bing](https://github.com/godchen520/dsh-web-search-bing) 的维护 fork,适配 DSH `>=0.1.5-rc.1 <0.3.0`(含 `0.2.0-rc.1`)。包名用 `dsh-bing-search`,因为 `dsh-web-search-bing` 已被占用,`dsh-websearch-bing` 又被 npm 判定过于相似。
14
14
 
15
15
  ## 特点
16
16
 
package/README_EN.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-bing-search
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![DSH Compatible](https://img.shields.io/badge/DSH-0.1.5--rc.1-brightgreen)](https://github.com/deepseek-ai/deepseek-harness)
4
+ [![DSH Compatible](https://img.shields.io/badge/DSH-0.1.5--rc.1%20..%20%3C0.3.0-brightgreen)](https://github.com/deepseek-ai/deepseek-harness)
5
5
  [![npm](https://img.shields.io/npm/v/dsh-bing-search.svg)](https://www.npmjs.com/package/dsh-bing-search)
6
6
 
7
7
  > Free Bing-backed web search provider for DeepSeek Harness (DSH). No API key needed, no search quota consumed.
@@ -10,7 +10,7 @@
10
10
  <b>English</b> | <a href="README.md">中文</a>
11
11
  </p>
12
12
 
13
- > This is a maintenance fork of [godchen520/dsh-web-search-bing](https://github.com/godchen520/dsh-web-search-bing), updated for DSH `0.1.5-rc.1`. The package is `dsh-bing-search` because `dsh-web-search-bing` is taken on npm and `dsh-websearch-bing` was rejected as too similar.
13
+ > This is a maintenance fork of [godchen520/dsh-web-search-bing](https://github.com/godchen520/dsh-web-search-bing), compatible with DSH `>=0.1.5-rc.1 <0.3.0` (including `0.2.0-rc.1`). The package is `dsh-bing-search` because `dsh-web-search-bing` is taken on npm and `dsh-websearch-bing` was rejected as too similar.
14
14
 
15
15
  ## Features
16
16
 
package/client.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * dsh-bing-search — Client half (browser bundle, DSH >= 0.1.6 only).
3
+ *
4
+ * On DSH <= 0.1.5 the provider's settings UI is the auto-generated section the
5
+ * Host installs through `settings.installSection`, so this half stays inert
6
+ * (the `configForms` service does not exist there).
7
+ *
8
+ * On DSH >= 0.1.6 the Settings forms are driven by the plugin's Cordis Config
9
+ * volatile fields instead; this half registers a small editor into the
10
+ * Plugins page's `plugins.bundle.config` slot (keyed by the package name),
11
+ * reading and writing the `bing-search` profile entry through the shared
12
+ * `configForms` service. The registration lives only while the Host serves
13
+ * the entry, so a profile without the plugin shows no trace of the page.
14
+ *
15
+ * This file is a classic script registered through the client module loader:
16
+ * `window.__ModuleLoader__.load({ id, factory })` with a lazy CJS factory.
17
+ */
18
+ window.__ModuleLoader__.load({
19
+ id: 'dsh-bing-search',
20
+ factory: (require) => {
21
+ var module = { exports: {} };
22
+ var exports = module.exports;
23
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
24
+
25
+ const React = require('react');
26
+
27
+ /** Profile entry id of the provider row (see cordis.patch.yml). */
28
+ const ENTRY_ID = 'bing-search';
29
+ /** Bundle package name the Plugins page keys its config slot by. */
30
+ const BUNDLE_KEY = 'dsh-bing-search';
31
+ const LOCALE_NS = 'settings.bing-search';
32
+
33
+ const zh = {
34
+ title: 'Bing 免费搜索',
35
+ summary: '配置 Bing HTML 搜索入口、语言与解析上限。',
36
+ endpoint: '搜索入口 URL',
37
+ endpointHint: '默认 https://cn.bing.com/search;任何返回 HTML 结果页的 Bing 镜像都可以。',
38
+ maxResults: '解析上限',
39
+ maxResultsHint: '每次搜索最多解析的结果条数(正整数)。',
40
+ language: '结果语言',
41
+ languageZh: '中文(ensearch=0)',
42
+ languageEn: '英文(ensearch=1)',
43
+ save: '保存',
44
+ saving: '保存中…',
45
+ saved: '已保存。',
46
+ discard: '放弃修改',
47
+ readOnly: '当前部署的配置为只读。',
48
+ unavailable: 'Host 未提供 Bing 搜索配置。',
49
+ saveFailed: '保存失败,已保留你的修改。',
50
+ };
51
+ const en = {
52
+ title: 'Bing free search',
53
+ summary: 'Configure the Bing HTML endpoint, language, and parse cap.',
54
+ endpoint: 'Search endpoint URL',
55
+ endpointHint: 'Defaults to https://cn.bing.com/search; any Bing mirror serving an HTML results page works.',
56
+ maxResults: 'Parse cap',
57
+ maxResultsHint: 'Maximum results parsed per search (positive integer).',
58
+ language: 'Result language',
59
+ languageZh: 'Chinese (ensearch=0)',
60
+ languageEn: 'English (ensearch=1)',
61
+ save: 'Save',
62
+ saving: 'Saving…',
63
+ saved: 'Saved.',
64
+ discard: 'Discard',
65
+ readOnly: 'Configuration is read-only in this deployment.',
66
+ unavailable: 'The Host does not serve Bing search configuration.',
67
+ saveFailed: 'The deployment did not accept these values.',
68
+ };
69
+
70
+ function translateFactory(dict) {
71
+ return (key) => dict[key] ?? en[key] ?? key;
72
+ }
73
+
74
+ const css = {
75
+ section: { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 640, color: 'var(--dsw-alias-label-primary)' },
76
+ title: { margin: 0, fontSize: 18, fontWeight: 600, lineHeight: '26px' },
77
+ meta: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
78
+ field: { display: 'flex', flexDirection: 'column', gap: 4 },
79
+ label: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
80
+ input: {
81
+ height: 32, width: '100%', boxSizing: 'border-box',
82
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
83
+ padding: '0 10px', font: 'inherit', fontSize: 14, lineHeight: '22px',
84
+ background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
85
+ },
86
+ button: {
87
+ boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
88
+ height: 28, padding: '0 10px', borderRadius: 14,
89
+ border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent',
90
+ color: 'var(--dsw-alias-label-primary)', font: 'inherit', fontSize: 12, lineHeight: '18px',
91
+ cursor: 'pointer',
92
+ },
93
+ primary: {
94
+ boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
95
+ height: 32, padding: '0 14px', borderRadius: 16, border: 'none',
96
+ background: 'var(--dsw-alias-button-primary-fill, #3b82f6)',
97
+ color: 'var(--dsw-alias-label-primary-foreground, #fff)',
98
+ font: 'inherit', fontSize: 13, cursor: 'pointer',
99
+ },
100
+ actions: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center' },
101
+ error: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
102
+ ok: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-success-primary)' },
103
+ };
104
+
105
+ function BingSearchConfigPage(props) {
106
+ const scope = props.scope;
107
+ const t = typeof props.t === 'function' ? props.t : translateFactory(props.dict ?? zh);
108
+ const [snap, setSnap] = React.useState(() => scope.getSnapshot());
109
+ const [draft, setDraft] = React.useState(null);
110
+ const [busy, setBusy] = React.useState(false);
111
+ const [failed, setFailed] = React.useState(false);
112
+ const [savedFlash, setSavedFlash] = React.useState(false);
113
+
114
+ React.useEffect(() => {
115
+ setSnap(scope.getSnapshot());
116
+ return scope.subscribe(() => setSnap(scope.getSnapshot()));
117
+ }, [scope]);
118
+
119
+ const value = snap.value ?? {};
120
+ const current = {
121
+ endpoint: typeof value.endpoint === 'string' ? value.endpoint : '',
122
+ maxResults: Number.isInteger(value.maxResults) ? value.maxResults : 20,
123
+ ensearch: Number.isInteger(value.ensearch) ? value.ensearch : 0,
124
+ };
125
+ const form = draft ?? current;
126
+ const dirty = draft !== null
127
+ && (draft.endpoint !== current.endpoint
128
+ || String(draft.maxResults) !== String(current.maxResults)
129
+ || String(draft.ensearch) !== String(current.ensearch));
130
+
131
+ if (props.view === 'summary') {
132
+ return React.createElement('p', { style: css.meta }, t('summary'));
133
+ }
134
+ if (snap.status === 'unavailable') {
135
+ return React.createElement('div', { style: css.section },
136
+ React.createElement('h2', { style: css.title }, t('title')),
137
+ React.createElement('p', { style: css.error }, t('unavailable')),
138
+ );
139
+ }
140
+
141
+ const writable = snap.writable !== false && snap.status === 'ready';
142
+
143
+ async function save() {
144
+ const endpoint = String(form.endpoint ?? '').trim();
145
+ const maxResults = Number.parseInt(String(form.maxResults), 10);
146
+ const ensearch = Number.parseInt(String(form.ensearch), 10);
147
+ setBusy(true);
148
+ setFailed(false);
149
+ setSavedFlash(false);
150
+ try {
151
+ const accepted = await scope.mutate([
152
+ { op: 'set', path: ['endpoint'], value: endpoint },
153
+ { op: 'set', path: ['maxResults'], value: Number.isInteger(maxResults) && maxResults > 0 ? maxResults : 20 },
154
+ { op: 'set', path: ['ensearch'], value: ensearch === 1 ? 1 : 0 },
155
+ ], snap.revision);
156
+ if (accepted === false) throw new Error('refused');
157
+ setDraft(null);
158
+ setSavedFlash(true);
159
+ } catch {
160
+ setFailed(true);
161
+ } finally {
162
+ setBusy(false);
163
+ }
164
+ }
165
+
166
+ return React.createElement('div', { style: css.section },
167
+ React.createElement('h2', { style: css.title }, t('title')),
168
+ !writable ? React.createElement('p', { style: css.meta }, t('readOnly')) : null,
169
+ React.createElement('div', { style: css.field },
170
+ React.createElement('span', { style: css.label }, t('endpoint')),
171
+ React.createElement('input', {
172
+ style: css.input,
173
+ value: form.endpoint,
174
+ placeholder: 'https://cn.bing.com/search',
175
+ 'aria-label': t('endpoint'),
176
+ disabled: !writable || busy,
177
+ onChange: (event) => setDraft({ ...form, endpoint: event.target.value }),
178
+ }),
179
+ React.createElement('p', { style: css.meta }, t('endpointHint')),
180
+ ),
181
+ React.createElement('div', { style: css.field },
182
+ React.createElement('span', { style: css.label }, t('maxResults')),
183
+ React.createElement('input', {
184
+ style: { ...css.input, maxWidth: 120 },
185
+ value: String(form.maxResults),
186
+ inputMode: 'numeric',
187
+ 'aria-label': t('maxResults'),
188
+ disabled: !writable || busy,
189
+ onChange: (event) => setDraft({ ...form, maxResults: event.target.value }),
190
+ }),
191
+ React.createElement('p', { style: css.meta }, t('maxResultsHint')),
192
+ ),
193
+ React.createElement('div', { style: css.field },
194
+ React.createElement('span', { style: css.label }, t('language')),
195
+ React.createElement('select', {
196
+ style: { ...css.input, maxWidth: 240 },
197
+ value: String(form.ensearch),
198
+ 'aria-label': t('language'),
199
+ disabled: !writable || busy,
200
+ onChange: (event) => setDraft({ ...form, ensearch: event.target.value }),
201
+ },
202
+ React.createElement('option', { value: '0' }, t('languageZh')),
203
+ React.createElement('option', { value: '1' }, t('languageEn')),
204
+ ),
205
+ ),
206
+ failed ? React.createElement('p', { style: css.error, role: 'alert' }, t('saveFailed')) : null,
207
+ savedFlash && !dirty ? React.createElement('p', { style: css.ok, role: 'status' }, t('saved')) : null,
208
+ React.createElement('div', { style: css.actions },
209
+ React.createElement('button', {
210
+ type: 'button', style: css.button, disabled: !dirty || busy,
211
+ onClick: () => { setDraft(null); setFailed(false); },
212
+ }, t('discard')),
213
+ React.createElement('button', {
214
+ type: 'button', style: css.primary, disabled: !writable || !dirty || busy, onClick: save,
215
+ }, busy ? t('saving') : t('save')),
216
+ ),
217
+ );
218
+ }
219
+
220
+ function apply(ctx) {
221
+ // DSH <= 0.1.5 has no configForms service: the Host-installed settings
222
+ // section covers the UI there, so this half stays inert.
223
+ const configForms = typeof ctx.get === 'function' ? ctx.get('configForms') : undefined;
224
+ const slots = typeof ctx.get === 'function' ? ctx.get('slots') : undefined;
225
+ if (!configForms || typeof configForms.get !== 'function' || !slots) return;
226
+
227
+ const locale = typeof ctx.get === 'function' ? ctx.get('locale') : undefined;
228
+ let t = translateFactory(zh);
229
+ if (locale && typeof locale.register === 'function' && typeof locale.bind === 'function') {
230
+ ctx.effect(() => locale.register(LOCALE_NS, { zh, en }), 'dsh-bing-search: dictionaries');
231
+ t = locale.bind(LOCALE_NS);
232
+ }
233
+
234
+ const scope = configForms.get(ENTRY_ID);
235
+ ctx.effect(
236
+ () => configForms.whileServed([ENTRY_ID], () => slots.inject('plugins.bundle.config', () => slots.register(
237
+ { name: 'plugins.bundle.config', key: BUNDLE_KEY, locale: LOCALE_NS },
238
+ (props) => React.createElement(BingSearchConfigPage, { ...props, scope, t }),
239
+ ))),
240
+ 'dsh-bing-search: plugins page',
241
+ );
242
+ }
243
+
244
+ exports.apply = apply;
245
+ exports.inject = [];
246
+ return module.exports;
247
+ },
248
+ });
package/lib/index.js CHANGED
@@ -273,15 +273,40 @@ const name = "bing-search";
273
273
  /** The web seam this provider registers into. */
274
274
  const inject = ["web"];
275
275
 
276
+ /**
277
+ * Mark a schema field volatile when the runtime's schemastery supports it
278
+ * (DSH >= 0.1.6). Volatile fields are editable through the client's
279
+ * configuration forms and hot-committed into the running fiber; on older
280
+ * runtimes the method does not exist and the field stays a plain default.
281
+ */
282
+ function maybeVolatile(schema) {
283
+ return typeof schema?.volatile === "function" ? schema.volatile() : schema;
284
+ }
285
+
276
286
  const Config = z.object({
277
- endpoint: z.string().default(DEFAULT_ENDPOINT),
278
- maxResults: z.number().step(1).min(1).default(DEFAULT_MAX_RESULTS),
279
- ensearch: z.number().step(1).min(0).max(1).default(DEFAULT_ENSEARCH)
287
+ endpoint: maybeVolatile(z.string().default(DEFAULT_ENDPOINT)),
288
+ maxResults: maybeVolatile(z.number().step(1).min(1).default(DEFAULT_MAX_RESULTS)),
289
+ ensearch: maybeVolatile(z.number().step(1).min(0).max(1).default(DEFAULT_ENSEARCH))
280
290
  });
281
291
 
282
292
  /** Settings namespace carrying this provider's endpoint and parse cap. */
283
293
  const WEB_SEARCH_BING_SETTINGS_NAMESPACE = "bing-search";
284
294
 
295
+ /**
296
+ * Unwrap one config field that may be a live Volatile reference (DSH >= 0.1.6
297
+ * loader commits volatile writes into the box in place) or a plain value
298
+ * (older runtimes / settings-section sources).
299
+ *
300
+ * @param {unknown} value - the raw config field.
301
+ * @returns {unknown} the current plain value.
302
+ */
303
+ function unwrapVolatile(value) {
304
+ if (value && typeof value === "object" && typeof value.get === "function") {
305
+ try { return value.get(); } catch { return undefined; }
306
+ }
307
+ return value;
308
+ }
309
+
285
310
  /**
286
311
  * Project one resolved section into the options the provider serves its next
287
312
  * search with: every value it reads is already fully defaulted.
@@ -291,9 +316,9 @@ const WEB_SEARCH_BING_SETTINGS_NAMESPACE = "bing-search";
291
316
  */
292
317
  function resolveOptions(config) {
293
318
  return {
294
- endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,
295
- maxResults: config?.maxResults ?? DEFAULT_MAX_RESULTS,
296
- ensearch: config?.ensearch ?? DEFAULT_ENSEARCH
319
+ endpoint: unwrapVolatile(config?.endpoint) ?? DEFAULT_ENDPOINT,
320
+ maxResults: unwrapVolatile(config?.maxResults) ?? DEFAULT_MAX_RESULTS,
321
+ ensearch: unwrapVolatile(config?.ensearch) ?? DEFAULT_ENSEARCH
297
322
  };
298
323
  }
299
324
 
@@ -301,6 +326,12 @@ function resolveOptions(config) {
301
326
  function apply(ctx, config) {
302
327
  let current = () => config;
303
328
  ctx.inject(["settings"], (settingsCtx) => {
329
+ // DSH <= 0.1.5: the settings service installs a schema-driven section and
330
+ // streams live sources through setSource. DSH >= 0.1.6 removed
331
+ // installSection: the same values ride the plugin's own Cordis Config
332
+ // (volatile fields above), which the loader hot-commits into the running
333
+ // fiber — resolveOptions unwraps the boxes on every search.
334
+ if (typeof settingsCtx.settings?.installSection !== "function") return;
304
335
  settingsCtx.settings.installSection(ctx, WEB_SEARCH_BING_SETTINGS_NAMESPACE, Config, config, {
305
336
  setSource: (source) => {
306
337
  current = source;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-bing-search",
3
3
  "description": "Free Bing-backed web search provider for DeepSeek Harness (DSH). No API key needed, uses cn.bing.com for mainland China accessibility.",
4
- "version": "1.1.3",
4
+ "version": "1.1.5",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -19,12 +19,14 @@
19
19
  "types": "./lib/types/index.d.ts",
20
20
  "default": "./lib/index.js"
21
21
  },
22
+ "./client": "./client.js",
22
23
  "./cordis.patch.yml": "./cordis.patch.yml",
23
24
  "./package.json": "./package.json"
24
25
  },
25
26
  "files": [
26
27
  "lib/index.js",
27
28
  "lib/types",
29
+ "client.js",
28
30
  "cordis.patch.yml",
29
31
  "README.md",
30
32
  "README_EN.md",
@@ -37,12 +39,15 @@
37
39
  "dsh": {
38
40
  "bundle": {
39
41
  "patch": "./cordis.patch.yml"
42
+ },
43
+ "client": {
44
+ "platform": "web"
40
45
  }
41
46
  },
42
47
  "peerDependencies": {
43
- "@deepseek-ai/dsh-settings": ">=0.1.5-rc.1 <0.2.0-0",
44
- "@deepseek-ai/dsh-web": ">=0.1.5-rc.1 <0.2.0-0",
45
- "@deepseek-ai/cordis": "^4.0.2"
48
+ "@deepseek-ai/cordis": "^4.0.2",
49
+ "@deepseek-ai/dsh-settings": ">=0.1.5-rc.1 <0.3.0-0",
50
+ "@deepseek-ai/dsh-web": ">=0.1.5-rc.1 <0.3.0-0"
46
51
  },
47
52
  "dependencies": {
48
53
  "@deepseek-ai/schemastery": "^3.18.2"