@dcrays/web-search 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/web-search",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DeepSeek Harness web search plugin for Mobook (production)",
5
5
  "type": "module",
6
6
  "main": "plugin/index.js",
@@ -28,12 +28,12 @@
28
28
  "engines": {
29
29
  "node": ">=24.0.0"
30
30
  },
31
- "dependencies": {
32
- "@deepseek-ai/schemastery": "^3.18.1"
33
- },
31
+ "dependencies": {},
34
32
  "peerDependencies": {
35
- "@deepseek-ai/cordis": "^4.0.1",
36
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
33
+ "@deepseek-ai/cordis": "^4.0.2",
34
+ "@deepseek-ai/dsh-home-paths": "0.1.2-rc.1",
35
+ "@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
36
+ "@deepseek-ai/schemastery": "^3.18.1"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
package/plugin/index.d.ts CHANGED
@@ -27,4 +27,5 @@ export declare class MbhWebSearchService extends Service {
27
27
  isEnabled(): boolean;
28
28
  search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
29
29
  }
30
+ export declare function resolveMetadataPath(configuredPath: string): string;
30
31
  export declare function apply(ctx: Context, config: Config): void;
package/plugin/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { createRequire as __mobookCreateRequire } from 'node:module'; const require = __mobookCreateRequire(import.meta.url);
2
2
 
3
- // plugins/web-search/dist-npm/plugin/index.js
4
- import { homedir } from "node:os";
3
+ // dist-npm/plugin/index.js
5
4
  import { resolve } from "node:path";
6
5
  import { Service } from "@deepseek-ai/cordis";
6
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
 
9
- // plugins/web-search/dist-npm/src/api-client.js
9
+ // dist-npm/src/api-client.js
10
10
  import { createHmac, randomUUID } from "node:crypto";
11
11
  import { readFile } from "node:fs/promises";
12
12
  var WebSearchApiError = class extends Error {
@@ -17,6 +17,10 @@ var WebSearchApiError = class extends Error {
17
17
  this.name = "WebSearchApiError";
18
18
  }
19
19
  };
20
+ var RESPONSE_TOO_LARGE_MESSAGE = "\u641C\u7D22\u670D\u52A1\u8FD4\u56DE\u7684\u5185\u5BB9\u8FC7\u5927\uFF0C\u6682\u65F6\u65E0\u6CD5\u5904\u7406\u3002\u8BF7\u7F29\u5C0F\u641C\u7D22\u8303\u56F4\u540E\u91CD\u8BD5\u3002";
21
+ function searchServiceLabel(service) {
22
+ return service === "tavily_search" ? "Tavily \u641C\u7D22" : "\u535A\u67E5\u641C\u7D22";
23
+ }
20
24
  function record(value) {
21
25
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
22
26
  }
@@ -35,7 +39,7 @@ function resolveSearchCredentials(metadata, channelId, legacyChannelId) {
35
39
  if (userId && botToken && deviceId)
36
40
  return { userId, botToken, deviceId };
37
41
  }
38
- throw new WebSearchApiError("MISSING_CREDENTIALS", `mobook metadata must contain plugins.${channelId}.userId, botToken, and hardware.fingerprint`);
42
+ 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");
39
43
  }
40
44
  function requestSignal(signal, timeoutMs) {
41
45
  const timeout = AbortSignal.timeout(timeoutMs);
@@ -45,7 +49,7 @@ async function responseText(response, maxBytes) {
45
49
  const declared = Number(response.headers.get("content-length"));
46
50
  if (Number.isFinite(declared) && declared > maxBytes) {
47
51
  await response.body?.cancel().catch(() => void 0);
48
- throw new WebSearchApiError("RESPONSE_TOO_LARGE", `web search response exceeds ${maxBytes} bytes`);
52
+ throw new WebSearchApiError("RESPONSE_TOO_LARGE", RESPONSE_TOO_LARGE_MESSAGE);
49
53
  }
50
54
  if (!response.body)
51
55
  return "";
@@ -61,7 +65,7 @@ async function responseText(response, maxBytes) {
61
65
  size += item.value.byteLength;
62
66
  if (size > maxBytes) {
63
67
  await reader.cancel().catch(() => void 0);
64
- throw new WebSearchApiError("RESPONSE_TOO_LARGE", `web search response exceeds ${maxBytes} bytes`);
68
+ throw new WebSearchApiError("RESPONSE_TOO_LARGE", RESPONSE_TOO_LARGE_MESSAGE);
65
69
  }
66
70
  text += decoder.decode(item.value, { stream: true });
67
71
  }
@@ -70,43 +74,51 @@ async function responseText(response, maxBytes) {
70
74
  reader.releaseLock();
71
75
  }
72
76
  }
73
- function parseResponse(text, fallbackQuery) {
77
+ function searchItem(value, snippetKeys, publishedAtKeys) {
78
+ const item = record(value);
79
+ const url = nonEmptyString(item?.url);
80
+ if (!item || !url)
81
+ return [];
82
+ const result = {
83
+ title: nonEmptyString(item.title) ?? nonEmptyString(item.name) ?? url,
84
+ url,
85
+ snippet: snippetKeys.map((key) => nonEmptyString(item[key])).find(Boolean) ?? ""
86
+ };
87
+ const siteName = nonEmptyString(item.siteName);
88
+ const publishedAt = publishedAtKeys.map((key) => nonEmptyString(item[key])).find(Boolean);
89
+ if (siteName)
90
+ result.siteName = siteName;
91
+ if (publishedAt)
92
+ result.publishedAt = publishedAt;
93
+ return [result];
94
+ }
95
+ function parseResponse(text, fallbackQuery, service) {
74
96
  let payload;
75
97
  try {
76
98
  payload = JSON.parse(text);
77
99
  } catch (error) {
78
- throw new WebSearchApiError("INVALID_RESPONSE", "web search API returned invalid JSON", { cause: error });
100
+ throw new WebSearchApiError("INVALID_RESPONSE", "\u641C\u7D22\u670D\u52A1\u8FD4\u56DE\u7684\u6570\u636E\u683C\u5F0F\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", { cause: error });
79
101
  }
80
102
  const wrapper = record(payload);
81
103
  const upstream = record(wrapper?.data);
82
104
  if (wrapper?.success !== true || typeof upstream?.code === "number" && upstream.code !== 200) {
83
- throw new WebSearchApiError("API_ERROR", "web search API reported an unsuccessful response");
105
+ throw new WebSearchApiError("API_ERROR", "\u641C\u7D22\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
106
+ }
107
+ if (service === "tavily_search") {
108
+ const values = Array.isArray(upstream?.results) ? upstream.results : [];
109
+ return {
110
+ query: nonEmptyString(upstream?.query) ?? fallbackQuery,
111
+ provider: service,
112
+ results: values.flatMap((value) => searchItem(value, ["content", "snippet"], ["published_date", "publishedAt"]))
113
+ };
84
114
  }
85
115
  const searchData = record(upstream?.data);
86
116
  const pages = record(searchData?.webPages);
87
117
  const queryContext = record(searchData?.queryContext);
88
- const values = Array.isArray(pages?.value) ? pages.value : [];
89
- const results = values.flatMap((value) => {
90
- const item = record(value);
91
- const url = nonEmptyString(item?.url);
92
- if (!item || !url)
93
- return [];
94
- const result2 = {
95
- title: nonEmptyString(item.name) ?? url,
96
- url,
97
- snippet: nonEmptyString(item.snippet) ?? ""
98
- };
99
- const siteName = nonEmptyString(item.siteName);
100
- const publishedAt = nonEmptyString(item.datePublished);
101
- if (siteName)
102
- result2.siteName = siteName;
103
- if (publishedAt)
104
- result2.publishedAt = publishedAt;
105
- return [result2];
106
- });
107
118
  const result = {
108
119
  query: nonEmptyString(queryContext?.originalQuery) ?? fallbackQuery,
109
- results
120
+ provider: service,
121
+ results: (Array.isArray(pages?.value) ? pages.value : []).flatMap((value) => searchItem(value, ["snippet"], ["datePublished"]))
110
122
  };
111
123
  if (typeof pages?.totalEstimatedMatches === "number" && Number.isFinite(pages.totalEstimatedMatches)) {
112
124
  result.totalEstimatedMatches = pages.totalEstimatedMatches;
@@ -125,10 +137,10 @@ var WebSearchApiClient = class {
125
137
  try {
126
138
  this.endpoint = new URL(config.endpointUrl);
127
139
  } catch (error) {
128
- throw new WebSearchApiError("INVALID_CONFIG", "web search endpointUrl is invalid", { cause: error });
140
+ throw new WebSearchApiError("INVALID_CONFIG", "\u641C\u7D22\u670D\u52A1\u5730\u5740\u914D\u7F6E\u4E0D\u6B63\u786E\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\u3002", { cause: error });
129
141
  }
130
142
  if (!["http:", "https:"].includes(this.endpoint.protocol)) {
131
- throw new WebSearchApiError("INVALID_CONFIG", "web search endpointUrl must use http or https");
143
+ throw new WebSearchApiError("INVALID_CONFIG", "\u641C\u7D22\u670D\u52A1\u5730\u5740\u4EC5\u652F\u6301 HTTP \u6216 HTTPS\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\u3002");
132
144
  }
133
145
  for (const [name2, value] of [
134
146
  ["timeoutMs", config.timeoutMs],
@@ -137,11 +149,11 @@ var WebSearchApiClient = class {
137
149
  ["maxCount", config.maxCount]
138
150
  ]) {
139
151
  if (!Number.isSafeInteger(value) || value <= 0) {
140
- throw new WebSearchApiError("INVALID_CONFIG", `${name2} must be a positive integer`);
152
+ throw new WebSearchApiError("INVALID_CONFIG", `\u641C\u7D22\u63D2\u4EF6\u914D\u7F6E\u9879 ${name2} \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u914D\u7F6E\u3002`);
141
153
  }
142
154
  }
143
155
  if (config.defaultCount > config.maxCount) {
144
- throw new WebSearchApiError("INVALID_CONFIG", "defaultCount must not exceed maxCount");
156
+ throw new WebSearchApiError("INVALID_CONFIG", "\u9ED8\u8BA4\u641C\u7D22\u7ED3\u679C\u6570\u4E0D\u80FD\u5927\u4E8E\u6700\u5927\u7ED3\u679C\u6570\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\u3002");
145
157
  }
146
158
  this.fetchImpl = dependencies.fetch ?? fetch;
147
159
  this.readFileImpl = dependencies.readFile ?? readFile;
@@ -151,73 +163,85 @@ var WebSearchApiClient = class {
151
163
  async search(request, signal) {
152
164
  const query = request.query.trim();
153
165
  if (!query)
154
- throw new WebSearchApiError("INVALID_REQUEST", "web search query must not be empty");
166
+ throw new WebSearchApiError("INVALID_REQUEST", "\u8BF7\u8F93\u5165\u8981\u641C\u7D22\u7684\u5185\u5BB9\u540E\u91CD\u8BD5\u3002");
155
167
  const count = request.count ?? this.config.defaultCount;
156
168
  const offset = request.offset ?? 0;
157
169
  if (!Number.isSafeInteger(count) || count < 1 || count > this.config.maxCount) {
158
- throw new WebSearchApiError("INVALID_REQUEST", `web search count must be between 1 and ${this.config.maxCount}`);
170
+ throw new WebSearchApiError("INVALID_REQUEST", `\u641C\u7D22\u7ED3\u679C\u6570\u91CF\u9700\u8BBE\u7F6E\u4E3A 1 \u5230 ${this.config.maxCount} \u4E4B\u95F4\u7684\u6574\u6570\u3002`);
159
171
  }
160
172
  if (!Number.isSafeInteger(offset) || offset < 0) {
161
- throw new WebSearchApiError("INVALID_REQUEST", "web search offset must be a non-negative integer");
173
+ throw new WebSearchApiError("INVALID_REQUEST", "\u641C\u7D22\u7ED3\u679C\u504F\u79FB\u91CF\u5FC5\u987B\u662F\u5927\u4E8E\u6216\u7B49\u4E8E 0 \u7684\u6574\u6570\u3002");
162
174
  }
163
175
  let metadata;
164
176
  try {
165
177
  metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, "utf8"));
166
178
  } catch (error) {
167
- throw new WebSearchApiError("MISSING_CREDENTIALS", "unable to read Mobook runtime metadata", { cause: error });
179
+ 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 });
168
180
  }
169
181
  const credentials = resolveSearchCredentials(metadata, this.config.channelId, this.config.legacyChannelId);
170
- const timestamp = String(this.nowSeconds());
171
- const nonce = this.nonce();
172
- const message = `${credentials.userId}
182
+ const call = async (service) => {
183
+ const timestamp = String(this.nowSeconds());
184
+ const nonce = this.nonce();
185
+ const message = `${credentials.userId}
173
186
  ${credentials.deviceId}
174
187
  ${timestamp}
175
188
  ${nonce}`;
176
- const signature = createHmac("sha256", credentials.botToken).update(message, "utf8").digest("hex");
177
- const body = {
178
- user_id: credentials.userId,
179
- bot_token: credentials.botToken,
180
- device_id: credentials.deviceId,
181
- signature,
182
- timestamp,
183
- nonce,
184
- service: "bocha_search",
185
- query,
186
- count,
187
- offset,
188
- language: request.language?.trim() || "zh-CN"
189
+ const signature = createHmac("sha256", credentials.botToken).update(message, "utf8").digest("hex");
190
+ const body = {
191
+ user_id: credentials.userId,
192
+ bot_token: credentials.botToken,
193
+ device_id: credentials.deviceId,
194
+ signature,
195
+ timestamp,
196
+ nonce,
197
+ service,
198
+ query,
199
+ count,
200
+ offset,
201
+ language: request.language?.trim() || "zh-CN"
202
+ };
203
+ if (request.includeDomains?.length)
204
+ body.include_domains = request.includeDomains;
205
+ if (request.excludeDomains?.length)
206
+ body.exclude_domains = request.excludeDomains;
207
+ const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
208
+ let response;
209
+ try {
210
+ response = await this.fetchImpl(this.endpoint, {
211
+ method: "POST",
212
+ headers: { accept: "application/json", "content-type": "application/json" },
213
+ body: JSON.stringify(body),
214
+ signal: effectiveSignal
215
+ });
216
+ } catch (error) {
217
+ if (signal?.aborted)
218
+ throw error;
219
+ if (effectiveSignal.aborted) {
220
+ throw new WebSearchApiError("TIMEOUT", `${searchServiceLabel(service)}\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002`, {
221
+ cause: error
222
+ });
223
+ }
224
+ 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 });
225
+ }
226
+ const text = await responseText(response, this.config.maxResponseBytes);
227
+ if (!response.ok) {
228
+ throw new WebSearchApiError("HTTP_ERROR", `${searchServiceLabel(service)}\u6682\u65F6\u4E0D\u53EF\u7528\uFF08\u670D\u52A1\u72B6\u6001 ${response.status}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002`);
229
+ }
230
+ return parseResponse(text, query, service);
189
231
  };
190
- if (request.includeDomains?.length)
191
- body.include_domains = request.includeDomains;
192
- if (request.excludeDomains?.length)
193
- body.exclude_domains = request.excludeDomains;
194
- const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
195
- let response;
196
232
  try {
197
- response = await this.fetchImpl(this.endpoint, {
198
- method: "POST",
199
- headers: { accept: "application/json", "content-type": "application/json" },
200
- body: JSON.stringify(body),
201
- signal: effectiveSignal
202
- });
233
+ const tavily = await call("tavily_search");
234
+ if (tavily.results.length > 0)
235
+ return tavily;
203
236
  } catch (error) {
204
237
  if (signal?.aborted)
205
238
  throw error;
206
- if (effectiveSignal.aborted) {
207
- throw new WebSearchApiError("TIMEOUT", `web search request timed out after ${this.config.timeoutMs}ms`, {
208
- cause: error
209
- });
210
- }
211
- throw new WebSearchApiError("NETWORK_ERROR", "web search request failed", { cause: error });
212
239
  }
213
- const text = await responseText(response, this.config.maxResponseBytes);
214
- if (!response.ok)
215
- throw new WebSearchApiError("HTTP_ERROR", `web search request failed with HTTP ${response.status}`);
216
- return parseResponse(text, query);
240
+ return call("bocha_search");
217
241
  }
218
242
  };
219
243
 
220
- // plugins/web-search/dist-npm/src/tools.js
244
+ // dist-npm/src/tools.js
221
245
  import { defineTool } from "@deepseek-ai/dsh-tools";
222
246
  function createWebSearchTool(search) {
223
247
  return defineTool({
@@ -250,10 +274,10 @@ function createWebSearchTool(search) {
250
274
  });
251
275
  }
252
276
 
253
- // plugins/web-search/dist-npm/src/index.js
277
+ // dist-npm/src/index.js
254
278
  var MBH_WEB_SEARCH_SERVICE_KEY = "mbhWebSearch";
255
279
 
256
- // plugins/web-search/dist-npm/src/config/env-config.js
280
+ // dist-npm/src/config/env-config.js
257
281
  var ENV_CONFIG = {
258
282
  production: {
259
283
  packageName: "@dcrays/web-search",
@@ -271,7 +295,7 @@ var ENV_CONFIG = {
271
295
  var DEFAULT_BUILD_ENV = "production";
272
296
  var ACTIVE_ENV_CONFIG = ENV_CONFIG[DEFAULT_BUILD_ENV];
273
297
 
274
- // plugins/web-search/dist-npm/plugin/index.js
298
+ // dist-npm/plugin/index.js
275
299
  var name = "@dcrays/web-search";
276
300
  var inject = ["tools"];
277
301
  var Config = z.object({
@@ -291,10 +315,9 @@ var MbhWebSearchService = class extends Service {
291
315
  this.config = config;
292
316
  if (!config.enabled)
293
317
  return;
294
- const dshHome = process.env.DSH_HOME?.trim() || resolve(homedir(), ".mobook-harness");
295
318
  this.operations = new WebSearchApiClient({
296
319
  endpointUrl: config.endpointUrl.trim() || ACTIVE_ENV_CONFIG.endpointUrl,
297
- metadataPath: config.metadataPath.trim() || resolve(dshHome, "mobook.json"),
320
+ metadataPath: resolveMetadataPath(config.metadataPath),
298
321
  channelId: ACTIVE_ENV_CONFIG.channelId,
299
322
  legacyChannelId: ACTIVE_ENV_CONFIG.legacyChannelId,
300
323
  timeoutMs: config.timeoutMs,
@@ -307,11 +330,15 @@ var MbhWebSearchService = class extends Service {
307
330
  return this.config.enabled;
308
331
  }
309
332
  search(request, signal) {
310
- if (!this.operations)
311
- return Promise.reject(new Error("mbh-web-search is disabled"));
333
+ if (!this.operations) {
334
+ return Promise.reject(new Error("\u8054\u7F51\u641C\u7D22\u529F\u80FD\u5F53\u524D\u672A\u542F\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\u3002"));
335
+ }
312
336
  return this.operations.search(request, signal);
313
337
  }
314
338
  };
339
+ function resolveMetadataPath(configuredPath) {
340
+ return configuredPath.trim() || resolve(resolveDshHome(), "mobook.json");
341
+ }
315
342
  function apply(ctx, config) {
316
343
  const service = new MbhWebSearchService(ctx, config);
317
344
  ctx.effect(() => {
@@ -328,5 +355,6 @@ export {
328
355
  MbhWebSearchService,
329
356
  apply,
330
357
  inject,
331
- name
358
+ name,
359
+ resolveMetadataPath
332
360
  };
@@ -15,9 +15,11 @@ export interface WebSearchItem {
15
15
  }
16
16
  export interface WebSearchResult {
17
17
  query: string;
18
+ provider: SearchService;
18
19
  totalEstimatedMatches?: number;
19
20
  results: WebSearchItem[];
20
21
  }
22
+ export type SearchService = 'tavily_search' | 'bocha_search';
21
23
  export interface WebSearchOperations {
22
24
  search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
23
25
  }
package/src/api-client.js CHANGED
@@ -8,6 +8,10 @@ export class WebSearchApiError extends Error {
8
8
  this.name = 'WebSearchApiError';
9
9
  }
10
10
  }
11
+ const RESPONSE_TOO_LARGE_MESSAGE = '搜索服务返回的内容过大,暂时无法处理。请缩小搜索范围后重试。';
12
+ function searchServiceLabel(service) {
13
+ return service === 'tavily_search' ? 'Tavily 搜索' : '博查搜索';
14
+ }
11
15
  function record(value) {
12
16
  return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
13
17
  }
@@ -26,7 +30,7 @@ export function resolveSearchCredentials(metadata, channelId, legacyChannelId) {
26
30
  if (userId && botToken && deviceId)
27
31
  return { userId, botToken, deviceId };
28
32
  }
29
- throw new WebSearchApiError('MISSING_CREDENTIALS', `mobook metadata must contain plugins.${channelId}.userId, botToken, and hardware.fingerprint`);
33
+ throw new WebSearchApiError('MISSING_CREDENTIALS', '搜索服务尚未准备好,请确认已登录墨宝并完成客户端初始化后重试。');
30
34
  }
31
35
  function requestSignal(signal, timeoutMs) {
32
36
  const timeout = AbortSignal.timeout(timeoutMs);
@@ -36,7 +40,7 @@ async function responseText(response, maxBytes) {
36
40
  const declared = Number(response.headers.get('content-length'));
37
41
  if (Number.isFinite(declared) && declared > maxBytes) {
38
42
  await response.body?.cancel().catch(() => undefined);
39
- throw new WebSearchApiError('RESPONSE_TOO_LARGE', `web search response exceeds ${maxBytes} bytes`);
43
+ throw new WebSearchApiError('RESPONSE_TOO_LARGE', RESPONSE_TOO_LARGE_MESSAGE);
40
44
  }
41
45
  if (!response.body)
42
46
  return '';
@@ -52,7 +56,7 @@ async function responseText(response, maxBytes) {
52
56
  size += item.value.byteLength;
53
57
  if (size > maxBytes) {
54
58
  await reader.cancel().catch(() => undefined);
55
- throw new WebSearchApiError('RESPONSE_TOO_LARGE', `web search response exceeds ${maxBytes} bytes`);
59
+ throw new WebSearchApiError('RESPONSE_TOO_LARGE', RESPONSE_TOO_LARGE_MESSAGE);
56
60
  }
57
61
  text += decoder.decode(item.value, { stream: true });
58
62
  }
@@ -62,44 +66,52 @@ async function responseText(response, maxBytes) {
62
66
  reader.releaseLock();
63
67
  }
64
68
  }
65
- function parseResponse(text, fallbackQuery) {
69
+ function searchItem(value, snippetKeys, publishedAtKeys) {
70
+ const item = record(value);
71
+ const url = nonEmptyString(item?.url);
72
+ if (!item || !url)
73
+ return [];
74
+ const result = {
75
+ title: nonEmptyString(item.title) ?? nonEmptyString(item.name) ?? url,
76
+ url,
77
+ snippet: snippetKeys.map((key) => nonEmptyString(item[key])).find(Boolean) ?? ''
78
+ };
79
+ const siteName = nonEmptyString(item.siteName);
80
+ const publishedAt = publishedAtKeys.map((key) => nonEmptyString(item[key])).find(Boolean);
81
+ if (siteName)
82
+ result.siteName = siteName;
83
+ if (publishedAt)
84
+ result.publishedAt = publishedAt;
85
+ return [result];
86
+ }
87
+ function parseResponse(text, fallbackQuery, service) {
66
88
  let payload;
67
89
  try {
68
90
  payload = JSON.parse(text);
69
91
  }
70
92
  catch (error) {
71
- throw new WebSearchApiError('INVALID_RESPONSE', 'web search API returned invalid JSON', { cause: error });
93
+ throw new WebSearchApiError('INVALID_RESPONSE', '搜索服务返回的数据格式异常,请稍后重试。', { cause: error });
72
94
  }
73
95
  const wrapper = record(payload);
74
96
  const upstream = record(wrapper?.data);
75
97
  if (wrapper?.success !== true || (typeof upstream?.code === 'number' && upstream.code !== 200)) {
76
- throw new WebSearchApiError('API_ERROR', 'web search API reported an unsuccessful response');
98
+ throw new WebSearchApiError('API_ERROR', '搜索服务暂时不可用,请稍后重试。');
99
+ }
100
+ if (service === 'tavily_search') {
101
+ const values = Array.isArray(upstream?.results) ? upstream.results : [];
102
+ return {
103
+ query: nonEmptyString(upstream?.query) ?? fallbackQuery,
104
+ provider: service,
105
+ results: values.flatMap((value) => searchItem(value, ['content', 'snippet'], ['published_date', 'publishedAt']))
106
+ };
77
107
  }
78
108
  const searchData = record(upstream?.data);
79
109
  const pages = record(searchData?.webPages);
80
110
  const queryContext = record(searchData?.queryContext);
81
- const values = Array.isArray(pages?.value) ? pages.value : [];
82
- const results = values.flatMap((value) => {
83
- const item = record(value);
84
- const url = nonEmptyString(item?.url);
85
- if (!item || !url)
86
- return [];
87
- const result = {
88
- title: nonEmptyString(item.name) ?? url,
89
- url,
90
- snippet: nonEmptyString(item.snippet) ?? ''
91
- };
92
- const siteName = nonEmptyString(item.siteName);
93
- const publishedAt = nonEmptyString(item.datePublished);
94
- if (siteName)
95
- result.siteName = siteName;
96
- if (publishedAt)
97
- result.publishedAt = publishedAt;
98
- return [result];
99
- });
100
111
  const result = {
101
112
  query: nonEmptyString(queryContext?.originalQuery) ?? fallbackQuery,
102
- results
113
+ provider: service,
114
+ results: (Array.isArray(pages?.value) ? pages.value : []).flatMap((value) => searchItem(value, ['snippet'], ['datePublished']))
103
115
  };
104
116
  if (typeof pages?.totalEstimatedMatches === 'number' && Number.isFinite(pages.totalEstimatedMatches)) {
105
117
  result.totalEstimatedMatches = pages.totalEstimatedMatches;
@@ -119,10 +131,10 @@ export class WebSearchApiClient {
119
131
  this.endpoint = new URL(config.endpointUrl);
120
132
  }
121
133
  catch (error) {
122
- throw new WebSearchApiError('INVALID_CONFIG', 'web search endpointUrl is invalid', { cause: error });
134
+ throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址配置不正确,请联系管理员检查插件配置。', { cause: error });
123
135
  }
124
136
  if (!['http:', 'https:'].includes(this.endpoint.protocol)) {
125
- throw new WebSearchApiError('INVALID_CONFIG', 'web search endpointUrl must use http or https');
137
+ throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址仅支持 HTTP HTTPS,请联系管理员检查插件配置。');
126
138
  }
127
139
  for (const [name, value] of [
128
140
  ['timeoutMs', config.timeoutMs],
@@ -131,11 +143,11 @@ export class WebSearchApiClient {
131
143
  ['maxCount', config.maxCount]
132
144
  ]) {
133
145
  if (!Number.isSafeInteger(value) || value <= 0) {
134
- throw new WebSearchApiError('INVALID_CONFIG', `${name} must be a positive integer`);
146
+ throw new WebSearchApiError('INVALID_CONFIG', `搜索插件配置项 ${name} 必须是大于 0 的整数,请联系管理员检查配置。`);
135
147
  }
136
148
  }
137
149
  if (config.defaultCount > config.maxCount) {
138
- throw new WebSearchApiError('INVALID_CONFIG', 'defaultCount must not exceed maxCount');
150
+ throw new WebSearchApiError('INVALID_CONFIG', '默认搜索结果数不能大于最大结果数,请联系管理员检查插件配置。');
139
151
  }
140
152
  this.fetchImpl = dependencies.fetch ?? fetch;
141
153
  this.readFileImpl = dependencies.readFile ?? readFile;
@@ -145,67 +157,80 @@ export class WebSearchApiClient {
145
157
  async search(request, signal) {
146
158
  const query = request.query.trim();
147
159
  if (!query)
148
- throw new WebSearchApiError('INVALID_REQUEST', 'web search query must not be empty');
160
+ throw new WebSearchApiError('INVALID_REQUEST', '请输入要搜索的内容后重试。');
149
161
  const count = request.count ?? this.config.defaultCount;
150
162
  const offset = request.offset ?? 0;
151
163
  if (!Number.isSafeInteger(count) || count < 1 || count > this.config.maxCount) {
152
- throw new WebSearchApiError('INVALID_REQUEST', `web search count must be between 1 and ${this.config.maxCount}`);
164
+ throw new WebSearchApiError('INVALID_REQUEST', `搜索结果数量需设置为 1 ${this.config.maxCount} 之间的整数。`);
153
165
  }
154
166
  if (!Number.isSafeInteger(offset) || offset < 0) {
155
- throw new WebSearchApiError('INVALID_REQUEST', 'web search offset must be a non-negative integer');
167
+ throw new WebSearchApiError('INVALID_REQUEST', '搜索结果偏移量必须是大于或等于 0 的整数。');
156
168
  }
157
169
  let metadata;
158
170
  try {
159
171
  metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, 'utf8'));
160
172
  }
161
173
  catch (error) {
162
- throw new WebSearchApiError('MISSING_CREDENTIALS', 'unable to read Mobook runtime metadata', { cause: error });
174
+ throw new WebSearchApiError('MISSING_CREDENTIALS', '暂时无法读取墨宝运行配置,请确认客户端已正常初始化后重试。', { cause: error });
163
175
  }
164
176
  const credentials = resolveSearchCredentials(metadata, this.config.channelId, this.config.legacyChannelId);
165
- const timestamp = String(this.nowSeconds());
166
- const nonce = this.nonce();
167
- const message = `${credentials.userId}\n${credentials.deviceId}\n${timestamp}\n${nonce}`;
168
- const signature = createHmac('sha256', credentials.botToken).update(message, 'utf8').digest('hex');
169
- const body = {
170
- user_id: credentials.userId,
171
- bot_token: credentials.botToken,
172
- device_id: credentials.deviceId,
173
- signature,
174
- timestamp,
175
- nonce,
176
- service: 'bocha_search',
177
- query,
178
- count,
179
- offset,
180
- language: request.language?.trim() || 'zh-CN'
177
+ const call = async (service) => {
178
+ const timestamp = String(this.nowSeconds());
179
+ const nonce = this.nonce();
180
+ const message = `${credentials.userId}\n${credentials.deviceId}\n${timestamp}\n${nonce}`;
181
+ const signature = createHmac('sha256', credentials.botToken).update(message, 'utf8').digest('hex');
182
+ const body = {
183
+ user_id: credentials.userId,
184
+ bot_token: credentials.botToken,
185
+ device_id: credentials.deviceId,
186
+ signature,
187
+ timestamp,
188
+ nonce,
189
+ service,
190
+ query,
191
+ count,
192
+ offset,
193
+ language: request.language?.trim() || 'zh-CN'
194
+ };
195
+ if (request.includeDomains?.length)
196
+ body.include_domains = request.includeDomains;
197
+ if (request.excludeDomains?.length)
198
+ body.exclude_domains = request.excludeDomains;
199
+ const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
200
+ let response;
201
+ try {
202
+ response = await this.fetchImpl(this.endpoint, {
203
+ method: 'POST',
204
+ headers: { accept: 'application/json', 'content-type': 'application/json' },
205
+ body: JSON.stringify(body),
206
+ signal: effectiveSignal
207
+ });
208
+ }
209
+ catch (error) {
210
+ if (signal?.aborted)
211
+ throw error;
212
+ if (effectiveSignal.aborted) {
213
+ throw new WebSearchApiError('TIMEOUT', `${searchServiceLabel(service)}响应超时,请稍后重试。`, {
214
+ cause: error
215
+ });
216
+ }
217
+ throw new WebSearchApiError('NETWORK_ERROR', `暂时无法连接${searchServiceLabel(service)},请检查网络后重试。`, { cause: error });
218
+ }
219
+ const text = await responseText(response, this.config.maxResponseBytes);
220
+ if (!response.ok) {
221
+ throw new WebSearchApiError('HTTP_ERROR', `${searchServiceLabel(service)}暂时不可用(服务状态 ${response.status}),请稍后重试。`);
222
+ }
223
+ return parseResponse(text, query, service);
181
224
  };
182
- if (request.includeDomains?.length)
183
- body.include_domains = request.includeDomains;
184
- if (request.excludeDomains?.length)
185
- body.exclude_domains = request.excludeDomains;
186
- const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
187
- let response;
188
225
  try {
189
- response = await this.fetchImpl(this.endpoint, {
190
- method: 'POST',
191
- headers: { accept: 'application/json', 'content-type': 'application/json' },
192
- body: JSON.stringify(body),
193
- signal: effectiveSignal
194
- });
226
+ const tavily = await call('tavily_search');
227
+ if (tavily.results.length > 0)
228
+ return tavily;
195
229
  }
196
230
  catch (error) {
197
231
  if (signal?.aborted)
198
232
  throw error;
199
- if (effectiveSignal.aborted) {
200
- throw new WebSearchApiError('TIMEOUT', `web search request timed out after ${this.config.timeoutMs}ms`, {
201
- cause: error
202
- });
203
- }
204
- throw new WebSearchApiError('NETWORK_ERROR', 'web search request failed', { cause: error });
205
233
  }
206
- const text = await responseText(response, this.config.maxResponseBytes);
207
- if (!response.ok)
208
- throw new WebSearchApiError('HTTP_ERROR', `web search request failed with HTTP ${response.status}`);
209
- return parseResponse(text, query);
234
+ return call('bocha_search');
210
235
  }
211
236
  }