@dcrays/web-search 0.1.2 → 0.1.3-beta.0

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
@@ -2,4 +2,4 @@
2
2
 
3
3
  书灵墨宝的 DSH 联网搜索插件。加载后注册 `web_search` 工具,把模型搜索请求映射到搜索网关。
4
4
 
5
- 单一发布包同时适用于生产与测试桌面:运行时根据 `mobook.json` 中记录的环境选择搜索网关与 MBHChat 凭据通道;`endpointUrl` 可在 Cordis 插件配置中覆盖。
5
+ 单一发布包同时适用于生产与测试桌面:运行时从 `mobook.json` 根级 `servers.apiGatewayBaseUrl` `account` 读取搜索网关与签名身份。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/web-search",
3
- "version": "0.1.2",
3
+ "version": "0.1.3-beta.0",
4
4
  "description": "DeepSeek Harness web search plugin for Mobook",
5
5
  "type": "module",
6
6
  "main": "plugin/index.js",
package/plugin/index.d.ts CHANGED
@@ -6,8 +6,6 @@ export declare const name = "mbh-web-search";
6
6
  export declare const inject: string[];
7
7
  export interface Config {
8
8
  enabled: boolean;
9
- endpointUrl: string;
10
- metadataPath: string;
11
9
  timeoutMs: number;
12
10
  maxResponseBytes: number;
13
11
  defaultCount: number;
@@ -26,5 +24,5 @@ export declare class MbhWebSearchService extends Service {
26
24
  isEnabled(): boolean;
27
25
  search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
28
26
  }
29
- export declare function resolveMetadataPath(configuredPath: string): string;
27
+ export declare function resolveMetadataPath(): string;
30
28
  export declare function apply(ctx: Context, config: Config): void;
package/plugin/index.js CHANGED
@@ -1,33 +1,14 @@
1
1
  import { createRequire as __mobookCreateRequire } from 'node:module'; const require = __mobookCreateRequire(import.meta.url);
2
2
 
3
- // dist-npm/plugin/index.js
3
+ // dist/plugin/index.js
4
4
  import { resolve } from "node:path";
5
5
  import { Service } from "@deepseek-ai/cordis";
6
6
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
 
9
- // dist-npm/src/api-client.js
9
+ // dist/src/api-client.js
10
10
  import { createHmac, randomUUID } from "node:crypto";
11
11
  import { readFile } from "node:fs/promises";
12
-
13
- // dist-npm/src/config/env-config.js
14
- var ENV_CONFIG = {
15
- production: {
16
- channelId: "mbh-chat",
17
- legacyChannelId: "mbhchat",
18
- endpointUrl: "https://api-gateway.shuwenda.com/apikey-manage/api/search"
19
- },
20
- test: {
21
- channelId: "mbh-chat-test",
22
- legacyChannelId: "mbhchat-test",
23
- endpointUrl: "https://api-gateway.shuwenda.icu/apikey-manage/api/search"
24
- }
25
- };
26
- function resolveWebSearchEnvironment(value) {
27
- return value === "production" ? "production" : "test";
28
- }
29
-
30
- // dist-npm/src/api-client.js
31
12
  var WebSearchApiError = class extends Error {
32
13
  code;
33
14
  constructor(code, message, options) {
@@ -46,20 +27,26 @@ function record(value) {
46
27
  function nonEmptyString(value) {
47
28
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
48
29
  }
49
- function resolveSearchCredentials(metadata, channelId, legacyChannelId) {
30
+ function resolveSearchCredentials(metadata) {
50
31
  const root = record(metadata);
51
- const plugins = record(root?.plugins);
52
32
  const hardware = record(root?.hardware);
33
+ const account = record(root?.account);
53
34
  const deviceId = nonEmptyString(hardware?.fingerprint);
54
- for (const plugin of [record(plugins?.[channelId]), record(plugins?.[legacyChannelId])]) {
55
- const userIdValue = plugin?.userId;
56
- const userId = typeof userIdValue === "number" && Number.isSafeInteger(userIdValue) ? String(userIdValue) : nonEmptyString(userIdValue);
57
- const botToken = nonEmptyString(plugin?.botToken);
58
- if (userId && botToken && deviceId)
59
- return { userId, botToken, deviceId };
60
- }
35
+ const userIdValue = account?.userId;
36
+ const userId = typeof userIdValue === "number" && Number.isSafeInteger(userIdValue) ? String(userIdValue) : nonEmptyString(userIdValue);
37
+ const botToken = nonEmptyString(account?.botToken);
38
+ if (userId && botToken && deviceId)
39
+ return { userId, botToken, deviceId };
61
40
  throw new WebSearchApiError("MISSING_CREDENTIALS", "\u641C\u7D22\u670D\u52A1\u5C1A\u672A\u51C6\u5907\u597D\uFF0C\u8BF7\u786E\u8BA4\u5DF2\u767B\u5F55\u58A8\u5B9D\u5E76\u5B8C\u6210\u5BA2\u6237\u7AEF\u521D\u59CB\u5316\u540E\u91CD\u8BD5\u3002");
62
41
  }
42
+ function resolveSearchEndpoint(metadata) {
43
+ const baseUrl = nonEmptyString(record(record(metadata)?.servers)?.apiGatewayBaseUrl);
44
+ if (!baseUrl) {
45
+ throw new WebSearchApiError("INVALID_CONFIG", "\u641C\u7D22\u670D\u52A1\u5730\u5740\u5C1A\u672A\u914D\u7F6E\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u5BA2\u6237\u7AEF\u914D\u7F6E\u3002");
46
+ }
47
+ const base = toEndpoint(baseUrl);
48
+ return new URL("/apikey-manage/api/search", base);
49
+ }
63
50
  function requestSignal(signal, timeoutMs) {
64
51
  const timeout = AbortSignal.timeout(timeoutMs);
65
52
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
@@ -158,27 +145,12 @@ function toEndpoint(endpointUrl) {
158
145
  }
159
146
  var WebSearchApiClient = class {
160
147
  config;
161
- environments;
162
148
  fetchImpl;
163
149
  readFileImpl;
164
150
  nowSeconds;
165
151
  nonce;
166
152
  constructor(config, dependencies = {}) {
167
153
  this.config = config;
168
- const environments = config.environments ?? ENV_CONFIG;
169
- const override = config.endpointOverride?.trim();
170
- this.environments = {
171
- production: {
172
- endpoint: toEndpoint(override || environments.production.endpointUrl),
173
- channelId: environments.production.channelId,
174
- legacyChannelId: environments.production.legacyChannelId
175
- },
176
- test: {
177
- endpoint: toEndpoint(override || environments.test.endpointUrl),
178
- channelId: environments.test.channelId,
179
- legacyChannelId: environments.test.legacyChannelId
180
- }
181
- };
182
154
  for (const [name2, value] of [
183
155
  ["timeoutMs", config.timeoutMs],
184
156
  ["maxResponseBytes", config.maxResponseBytes],
@@ -213,11 +185,12 @@ var WebSearchApiClient = class {
213
185
  try {
214
186
  metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, "utf8"));
215
187
  } catch (error) {
216
- throw new WebSearchApiError("MISSING_CREDENTIALS", "\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u58A8\u5B9D\u8FD0\u884C\u914D\u7F6E\uFF0C\u8BF7\u786E\u8BA4\u5BA2\u6237\u7AEF\u5DF2\u6B63\u5E38\u521D\u59CB\u5316\u540E\u91CD\u8BD5\u3002", { cause: error });
188
+ throw new WebSearchApiError("MISSING_CREDENTIALS", "\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u58A8\u5B9D\u8FD0\u884C\u914D\u7F6E\uFF0C\u8BF7\u786E\u8BA4\u5BA2\u6237\u7AEF\u5DF2\u6B63\u5E38\u521D\u59CB\u5316\u540E\u91CD\u8BD5\u3002", {
189
+ cause: error
190
+ });
217
191
  }
218
- const environment = resolveWebSearchEnvironment(record(metadata)?.environment);
219
- const resolved = this.environments[environment];
220
- const credentials = resolveSearchCredentials(metadata, resolved.channelId, resolved.legacyChannelId);
192
+ const endpoint = resolveSearchEndpoint(metadata);
193
+ const credentials = resolveSearchCredentials(metadata);
221
194
  const call = async (service) => {
222
195
  const timestamp = String(this.nowSeconds());
223
196
  const nonce = this.nonce();
@@ -246,7 +219,7 @@ ${nonce}`;
246
219
  const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
247
220
  let response;
248
221
  try {
249
- response = await this.fetchImpl(resolved.endpoint, {
222
+ response = await this.fetchImpl(endpoint, {
250
223
  method: "POST",
251
224
  headers: { accept: "application/json", "content-type": "application/json" },
252
225
  body: JSON.stringify(body),
@@ -260,7 +233,9 @@ ${nonce}`;
260
233
  cause: error
261
234
  });
262
235
  }
263
- throw new WebSearchApiError("NETWORK_ERROR", `\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5${searchServiceLabel(service)}\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002`, { cause: error });
236
+ throw new WebSearchApiError("NETWORK_ERROR", `\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5 ${searchServiceLabel(service)}\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002`, {
237
+ cause: error
238
+ });
264
239
  }
265
240
  const text = await responseText(response, this.config.maxResponseBytes);
266
241
  if (!response.ok) {
@@ -269,23 +244,23 @@ ${nonce}`;
269
244
  return parseResponse(text, query, service);
270
245
  };
271
246
  try {
272
- const tavily = await call("tavily_search");
273
- if (tavily.results.length > 0)
274
- return tavily;
247
+ const bocha = await call("bocha_search");
248
+ if (bocha.results.length > 0)
249
+ return bocha;
275
250
  } catch (error) {
276
251
  if (signal?.aborted)
277
252
  throw error;
278
253
  }
279
- return call("bocha_search");
254
+ return call("tavily_search");
280
255
  }
281
256
  };
282
257
 
283
- // dist-npm/src/tools.js
258
+ // dist/src/tools.js
284
259
  import { defineTool } from "@deepseek-ai/dsh-tools";
285
260
  function createWebSearchTool(search) {
286
261
  return defineTool({
287
262
  name: "web_search",
288
- description: "Search the public web with the configured Mobook Bocha search service. Use this for current information, source discovery, fact verification, news, weather, prices, schedules, or any question that requires internet access. Cite result URLs when answering.",
263
+ description: "Search the public web with Mobook search services. Use Bocha first and fall back to Tavily when Bocha fails or returns no results. Use this for current information, source discovery, fact verification, news, weather, prices, schedules, or any question that requires internet access. In the final answer, summarize first, then present useful sources as a numbered list with Markdown links in the form [title | siteName](url), followed by a concise snippet-based explanation. Never expose the raw JSON or internal provider fields.",
289
264
  parameters: {
290
265
  query: { type: "string", required: true, description: "A focused web search query." },
291
266
  count: { type: "integer", description: "Number of results to return. Defaults to 5; maximum 20." },
@@ -313,16 +288,14 @@ function createWebSearchTool(search) {
313
288
  });
314
289
  }
315
290
 
316
- // dist-npm/src/index.js
291
+ // dist/src/index.js
317
292
  var MBH_WEB_SEARCH_SERVICE_KEY = "mbhWebSearch";
318
293
 
319
- // dist-npm/plugin/index.js
294
+ // dist/plugin/index.js
320
295
  var name = "@dcrays/web-search";
321
296
  var inject = ["tools"];
322
297
  var Config = z.object({
323
298
  enabled: z.boolean().default(true),
324
- endpointUrl: z.string().default(""),
325
- metadataPath: z.string().default(""),
326
299
  timeoutMs: z.natural().min(1).default(3e4),
327
300
  maxResponseBytes: z.natural().min(1).default(1024 * 1024),
328
301
  defaultCount: z.natural().min(1).default(5),
@@ -337,8 +310,7 @@ var MbhWebSearchService = class extends Service {
337
310
  if (!config.enabled)
338
311
  return;
339
312
  this.operations = new WebSearchApiClient({
340
- metadataPath: resolveMetadataPath(config.metadataPath),
341
- ...config.endpointUrl.trim() ? { endpointOverride: config.endpointUrl.trim() } : {},
313
+ metadataPath: resolveMetadataPath(),
342
314
  timeoutMs: config.timeoutMs,
343
315
  maxResponseBytes: config.maxResponseBytes,
344
316
  defaultCount: config.defaultCount,
@@ -355,8 +327,8 @@ var MbhWebSearchService = class extends Service {
355
327
  return this.operations.search(request, signal);
356
328
  }
357
329
  };
358
- function resolveMetadataPath(configuredPath) {
359
- return configuredPath.trim() || resolve(resolveDshHome(), "mobook.json");
330
+ function resolveMetadataPath() {
331
+ return resolve(resolveDshHome(), "mobook.json");
360
332
  }
361
333
  function apply(ctx, config) {
362
334
  const service = new MbhWebSearchService(ctx, config);
@@ -1,4 +1,3 @@
1
- import { type EnvConfig, type WebSearchEnvironment } from './config/env-config.js';
2
1
  export interface WebSearchRequest {
3
2
  query: string;
4
3
  count?: number;
@@ -26,8 +25,6 @@ export interface WebSearchOperations {
26
25
  }
27
26
  export interface WebSearchApiClientConfig {
28
27
  metadataPath: string;
29
- endpointOverride?: string;
30
- environments?: Record<WebSearchEnvironment, EnvConfig>;
31
28
  timeoutMs: number;
32
29
  maxResponseBytes: number;
33
30
  defaultCount: number;
@@ -50,10 +47,10 @@ interface SearchCredentials {
50
47
  botToken: string;
51
48
  deviceId: string;
52
49
  }
53
- export declare function resolveSearchCredentials(metadata: unknown, channelId: string, legacyChannelId: string): SearchCredentials;
50
+ export declare function resolveSearchCredentials(metadata: unknown): SearchCredentials;
51
+ export declare function resolveSearchEndpoint(metadata: unknown): URL;
54
52
  export declare class WebSearchApiClient implements WebSearchOperations {
55
53
  private readonly config;
56
- private readonly environments;
57
54
  private readonly fetchImpl;
58
55
  private readonly readFileImpl;
59
56
  private readonly nowSeconds;
package/src/api-client.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { createHmac, randomUUID } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { ENV_CONFIG, resolveWebSearchEnvironment } from './config/env-config.js';
4
3
  export class WebSearchApiError extends Error {
5
4
  code;
6
5
  constructor(code, message, options) {
@@ -19,20 +18,26 @@ function record(value) {
19
18
  function nonEmptyString(value) {
20
19
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
21
20
  }
22
- export function resolveSearchCredentials(metadata, channelId, legacyChannelId) {
21
+ export function resolveSearchCredentials(metadata) {
23
22
  const root = record(metadata);
24
- const plugins = record(root?.plugins);
25
23
  const hardware = record(root?.hardware);
24
+ const account = record(root?.account);
26
25
  const deviceId = nonEmptyString(hardware?.fingerprint);
27
- for (const plugin of [record(plugins?.[channelId]), record(plugins?.[legacyChannelId])]) {
28
- const userIdValue = plugin?.userId;
29
- const userId = typeof userIdValue === 'number' && Number.isSafeInteger(userIdValue) ? String(userIdValue) : nonEmptyString(userIdValue);
30
- const botToken = nonEmptyString(plugin?.botToken);
31
- if (userId && botToken && deviceId)
32
- return { userId, botToken, deviceId };
33
- }
26
+ const userIdValue = account?.userId;
27
+ const userId = typeof userIdValue === 'number' && Number.isSafeInteger(userIdValue) ? String(userIdValue) : nonEmptyString(userIdValue);
28
+ const botToken = nonEmptyString(account?.botToken);
29
+ if (userId && botToken && deviceId)
30
+ return { userId, botToken, deviceId };
34
31
  throw new WebSearchApiError('MISSING_CREDENTIALS', '搜索服务尚未准备好,请确认已登录墨宝并完成客户端初始化后重试。');
35
32
  }
33
+ export function resolveSearchEndpoint(metadata) {
34
+ const baseUrl = nonEmptyString(record(record(metadata)?.servers)?.apiGatewayBaseUrl);
35
+ if (!baseUrl) {
36
+ throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址尚未配置,请联系管理员检查客户端配置。');
37
+ }
38
+ const base = toEndpoint(baseUrl);
39
+ return new URL('/apikey-manage/api/search', base);
40
+ }
36
41
  function requestSignal(signal, timeoutMs) {
37
42
  const timeout = AbortSignal.timeout(timeoutMs);
38
43
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
@@ -134,27 +139,12 @@ function toEndpoint(endpointUrl) {
134
139
  }
135
140
  export class WebSearchApiClient {
136
141
  config;
137
- environments;
138
142
  fetchImpl;
139
143
  readFileImpl;
140
144
  nowSeconds;
141
145
  nonce;
142
146
  constructor(config, dependencies = {}) {
143
147
  this.config = config;
144
- const environments = config.environments ?? ENV_CONFIG;
145
- const override = config.endpointOverride?.trim();
146
- this.environments = {
147
- production: {
148
- endpoint: toEndpoint(override || environments.production.endpointUrl),
149
- channelId: environments.production.channelId,
150
- legacyChannelId: environments.production.legacyChannelId
151
- },
152
- test: {
153
- endpoint: toEndpoint(override || environments.test.endpointUrl),
154
- channelId: environments.test.channelId,
155
- legacyChannelId: environments.test.legacyChannelId
156
- }
157
- };
158
148
  for (const [name, value] of [
159
149
  ['timeoutMs', config.timeoutMs],
160
150
  ['maxResponseBytes', config.maxResponseBytes],
@@ -190,11 +180,12 @@ export class WebSearchApiClient {
190
180
  metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, 'utf8'));
191
181
  }
192
182
  catch (error) {
193
- throw new WebSearchApiError('MISSING_CREDENTIALS', '暂时无法读取墨宝运行配置,请确认客户端已正常初始化后重试。', { cause: error });
183
+ throw new WebSearchApiError('MISSING_CREDENTIALS', '暂时无法读取墨宝运行配置,请确认客户端已正常初始化后重试。', {
184
+ cause: error
185
+ });
194
186
  }
195
- const environment = resolveWebSearchEnvironment(record(metadata)?.environment);
196
- const resolved = this.environments[environment];
197
- const credentials = resolveSearchCredentials(metadata, resolved.channelId, resolved.legacyChannelId);
187
+ const endpoint = resolveSearchEndpoint(metadata);
188
+ const credentials = resolveSearchCredentials(metadata);
198
189
  const call = async (service) => {
199
190
  const timestamp = String(this.nowSeconds());
200
191
  const nonce = this.nonce();
@@ -220,7 +211,7 @@ export class WebSearchApiClient {
220
211
  const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
221
212
  let response;
222
213
  try {
223
- response = await this.fetchImpl(resolved.endpoint, {
214
+ response = await this.fetchImpl(endpoint, {
224
215
  method: 'POST',
225
216
  headers: { accept: 'application/json', 'content-type': 'application/json' },
226
217
  body: JSON.stringify(body),
@@ -235,7 +226,9 @@ export class WebSearchApiClient {
235
226
  cause: error
236
227
  });
237
228
  }
238
- throw new WebSearchApiError('NETWORK_ERROR', `暂时无法连接${searchServiceLabel(service)},请检查网络后重试。`, { cause: error });
229
+ throw new WebSearchApiError('NETWORK_ERROR', `暂时无法连接 ${searchServiceLabel(service)},请检查网络后重试。`, {
230
+ cause: error
231
+ });
239
232
  }
240
233
  const text = await responseText(response, this.config.maxResponseBytes);
241
234
  if (!response.ok) {
@@ -244,14 +237,14 @@ export class WebSearchApiClient {
244
237
  return parseResponse(text, query, service);
245
238
  };
246
239
  try {
247
- const tavily = await call('tavily_search');
248
- if (tavily.results.length > 0)
249
- return tavily;
240
+ const bocha = await call('bocha_search');
241
+ if (bocha.results.length > 0)
242
+ return bocha;
250
243
  }
251
244
  catch (error) {
252
245
  if (signal?.aborted)
253
246
  throw error;
254
247
  }
255
- return call('bocha_search');
248
+ return call('tavily_search');
256
249
  }
257
250
  }
@@ -1,15 +1 @@
1
- export type WebSearchEnvironment = 'production' | 'test';
2
- export interface EnvConfig {
3
- channelId: string;
4
- legacyChannelId: string;
5
- endpointUrl: string;
6
- }
7
1
  export declare const PACKAGE_NAME = "@dcrays/web-search";
8
- export declare const ENV_CONFIG: Record<WebSearchEnvironment, EnvConfig>;
9
- /**
10
- * Map the desktop runtime environment recorded in mobook.json to the search
11
- * environment. Development desktops share the test gateway and MBHChat channel.
12
- * Missing/unknown values fall back to test so a misconfigured desktop never
13
- * silently reaches the production gateway with test credentials.
14
- */
15
- export declare function resolveWebSearchEnvironment(value: unknown): WebSearchEnvironment;
@@ -1,22 +1 @@
1
1
  export const PACKAGE_NAME = '@dcrays/web-search';
2
- export const ENV_CONFIG = {
3
- production: {
4
- channelId: 'mbh-chat',
5
- legacyChannelId: 'mbhchat',
6
- endpointUrl: 'https://api-gateway.shuwenda.com/apikey-manage/api/search'
7
- },
8
- test: {
9
- channelId: 'mbh-chat-test',
10
- legacyChannelId: 'mbhchat-test',
11
- endpointUrl: 'https://api-gateway.shuwenda.icu/apikey-manage/api/search'
12
- }
13
- };
14
- /**
15
- * Map the desktop runtime environment recorded in mobook.json to the search
16
- * environment. Development desktops share the test gateway and MBHChat channel.
17
- * Missing/unknown values fall back to test so a misconfigured desktop never
18
- * silently reaches the production gateway with test credentials.
19
- */
20
- export function resolveWebSearchEnvironment(value) {
21
- return value === 'production' ? 'production' : 'test';
22
- }
package/src/tools.js CHANGED
@@ -2,7 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  export function createWebSearchTool(search) {
3
3
  return defineTool({
4
4
  name: 'web_search',
5
- description: 'Search the public web with the configured Mobook Bocha search service. Use this for current information, source discovery, fact verification, news, weather, prices, schedules, or any question that requires internet access. Cite result URLs when answering.',
5
+ description: 'Search the public web with Mobook search services. Use Bocha first and fall back to Tavily when Bocha fails or returns no results. Use this for current information, source discovery, fact verification, news, weather, prices, schedules, or any question that requires internet access. In the final answer, summarize first, then present useful sources as a numbered list with Markdown links in the form [title | siteName](url), followed by a concise snippet-based explanation. Never expose the raw JSON or internal provider fields.',
6
6
  parameters: {
7
7
  query: { type: 'string', required: true, description: 'A focused web search query.' },
8
8
  count: { type: 'integer', description: 'Number of results to return. Defaults to 5; maximum 20.' },