@dcrays/web-search 0.1.1 → 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 +2 -2
- package/package.json +2 -2
- package/plugin/index.d.ts +1 -4
- package/plugin/index.js +50 -60
- package/src/api-client.d.ts +2 -5
- package/src/api-client.js +41 -27
- package/src/config/env-config.d.ts +1 -10
- package/src/config/env-config.js +1 -16
- package/src/tools.js +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# @dcrays/web-search
|
|
2
2
|
|
|
3
|
-
书灵墨宝的 DSH 联网搜索插件。加载后注册 `web_search`
|
|
3
|
+
书灵墨宝的 DSH 联网搜索插件。加载后注册 `web_search` 工具,把模型搜索请求映射到搜索网关。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
单一发布包同时适用于生产与测试桌面:运行时从 `mobook.json` 根级 `servers.apiGatewayBaseUrl` 和 `account` 读取搜索网关与签名身份。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/web-search",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DeepSeek Harness web search plugin for Mobook
|
|
3
|
+
"version": "0.1.3-beta.0",
|
|
4
|
+
"description": "DeepSeek Harness web search plugin for Mobook",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "plugin/index.js",
|
|
7
7
|
"types": "plugin/index.d.ts",
|
package/plugin/index.d.ts
CHANGED
|
@@ -2,13 +2,10 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import { Service } from '@deepseek-ai/cordis';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
4
|
import { type WebSearchRequest, type WebSearchResult } from '../src/index.js';
|
|
5
|
-
export { DEFAULT_BUILD_ENV } from '../src/config/env-config.js';
|
|
6
5
|
export declare const name = "mbh-web-search";
|
|
7
6
|
export declare const inject: string[];
|
|
8
7
|
export interface Config {
|
|
9
8
|
enabled: boolean;
|
|
10
|
-
endpointUrl: string;
|
|
11
|
-
metadataPath: string;
|
|
12
9
|
timeoutMs: number;
|
|
13
10
|
maxResponseBytes: number;
|
|
14
11
|
defaultCount: number;
|
|
@@ -27,5 +24,5 @@ export declare class MbhWebSearchService extends Service {
|
|
|
27
24
|
isEnabled(): boolean;
|
|
28
25
|
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
29
26
|
}
|
|
30
|
-
export declare function resolveMetadataPath(
|
|
27
|
+
export declare function resolveMetadataPath(): string;
|
|
31
28
|
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
|
-
// dist
|
|
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
|
|
9
|
+
// dist/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 {
|
|
@@ -27,20 +27,26 @@ function record(value) {
|
|
|
27
27
|
function nonEmptyString(value) {
|
|
28
28
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
29
29
|
}
|
|
30
|
-
function resolveSearchCredentials(metadata
|
|
30
|
+
function resolveSearchCredentials(metadata) {
|
|
31
31
|
const root = record(metadata);
|
|
32
|
-
const plugins = record(root?.plugins);
|
|
33
32
|
const hardware = record(root?.hardware);
|
|
33
|
+
const account = record(root?.account);
|
|
34
34
|
const deviceId = nonEmptyString(hardware?.fingerprint);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return { userId, botToken, deviceId };
|
|
41
|
-
}
|
|
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 };
|
|
42
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");
|
|
43
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
|
+
}
|
|
44
50
|
function requestSignal(signal, timeoutMs) {
|
|
45
51
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
46
52
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
@@ -125,23 +131,26 @@ function parseResponse(text, fallbackQuery, service) {
|
|
|
125
131
|
}
|
|
126
132
|
return result;
|
|
127
133
|
}
|
|
134
|
+
function toEndpoint(endpointUrl) {
|
|
135
|
+
let endpoint;
|
|
136
|
+
try {
|
|
137
|
+
endpoint = new URL(endpointUrl);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
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 });
|
|
140
|
+
}
|
|
141
|
+
if (!["http:", "https:"].includes(endpoint.protocol)) {
|
|
142
|
+
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");
|
|
143
|
+
}
|
|
144
|
+
return endpoint;
|
|
145
|
+
}
|
|
128
146
|
var WebSearchApiClient = class {
|
|
129
147
|
config;
|
|
130
|
-
endpoint;
|
|
131
148
|
fetchImpl;
|
|
132
149
|
readFileImpl;
|
|
133
150
|
nowSeconds;
|
|
134
151
|
nonce;
|
|
135
152
|
constructor(config, dependencies = {}) {
|
|
136
153
|
this.config = config;
|
|
137
|
-
try {
|
|
138
|
-
this.endpoint = new URL(config.endpointUrl);
|
|
139
|
-
} catch (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 });
|
|
141
|
-
}
|
|
142
|
-
if (!["http:", "https:"].includes(this.endpoint.protocol)) {
|
|
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");
|
|
144
|
-
}
|
|
145
154
|
for (const [name2, value] of [
|
|
146
155
|
["timeoutMs", config.timeoutMs],
|
|
147
156
|
["maxResponseBytes", config.maxResponseBytes],
|
|
@@ -176,9 +185,12 @@ var WebSearchApiClient = class {
|
|
|
176
185
|
try {
|
|
177
186
|
metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, "utf8"));
|
|
178
187
|
} catch (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", {
|
|
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
|
+
});
|
|
180
191
|
}
|
|
181
|
-
const
|
|
192
|
+
const endpoint = resolveSearchEndpoint(metadata);
|
|
193
|
+
const credentials = resolveSearchCredentials(metadata);
|
|
182
194
|
const call = async (service) => {
|
|
183
195
|
const timestamp = String(this.nowSeconds());
|
|
184
196
|
const nonce = this.nonce();
|
|
@@ -207,7 +219,7 @@ ${nonce}`;
|
|
|
207
219
|
const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
|
|
208
220
|
let response;
|
|
209
221
|
try {
|
|
210
|
-
response = await this.fetchImpl(
|
|
222
|
+
response = await this.fetchImpl(endpoint, {
|
|
211
223
|
method: "POST",
|
|
212
224
|
headers: { accept: "application/json", "content-type": "application/json" },
|
|
213
225
|
body: JSON.stringify(body),
|
|
@@ -221,7 +233,9 @@ ${nonce}`;
|
|
|
221
233
|
cause: error
|
|
222
234
|
});
|
|
223
235
|
}
|
|
224
|
-
throw new WebSearchApiError("NETWORK_ERROR", `\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5${searchServiceLabel(service)}\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002`, {
|
|
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
|
+
});
|
|
225
239
|
}
|
|
226
240
|
const text = await responseText(response, this.config.maxResponseBytes);
|
|
227
241
|
if (!response.ok) {
|
|
@@ -230,23 +244,23 @@ ${nonce}`;
|
|
|
230
244
|
return parseResponse(text, query, service);
|
|
231
245
|
};
|
|
232
246
|
try {
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
return
|
|
247
|
+
const bocha = await call("bocha_search");
|
|
248
|
+
if (bocha.results.length > 0)
|
|
249
|
+
return bocha;
|
|
236
250
|
} catch (error) {
|
|
237
251
|
if (signal?.aborted)
|
|
238
252
|
throw error;
|
|
239
253
|
}
|
|
240
|
-
return call("
|
|
254
|
+
return call("tavily_search");
|
|
241
255
|
}
|
|
242
256
|
};
|
|
243
257
|
|
|
244
|
-
// dist
|
|
258
|
+
// dist/src/tools.js
|
|
245
259
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
246
260
|
function createWebSearchTool(search) {
|
|
247
261
|
return defineTool({
|
|
248
262
|
name: "web_search",
|
|
249
|
-
description: "Search the public web with
|
|
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.",
|
|
250
264
|
parameters: {
|
|
251
265
|
query: { type: "string", required: true, description: "A focused web search query." },
|
|
252
266
|
count: { type: "integer", description: "Number of results to return. Defaults to 5; maximum 20." },
|
|
@@ -274,34 +288,14 @@ function createWebSearchTool(search) {
|
|
|
274
288
|
});
|
|
275
289
|
}
|
|
276
290
|
|
|
277
|
-
// dist
|
|
291
|
+
// dist/src/index.js
|
|
278
292
|
var MBH_WEB_SEARCH_SERVICE_KEY = "mbhWebSearch";
|
|
279
293
|
|
|
280
|
-
// dist
|
|
281
|
-
var ENV_CONFIG = {
|
|
282
|
-
production: {
|
|
283
|
-
packageName: "@dcrays/web-search",
|
|
284
|
-
channelId: "mbh-chat",
|
|
285
|
-
legacyChannelId: "mbhchat",
|
|
286
|
-
endpointUrl: "https://api-gateway.shuwenda.com/apikey-manage/api/search"
|
|
287
|
-
},
|
|
288
|
-
test: {
|
|
289
|
-
packageName: "@dcrays/web-search-test",
|
|
290
|
-
channelId: "mbh-chat-test",
|
|
291
|
-
legacyChannelId: "mbhchat-test",
|
|
292
|
-
endpointUrl: "https://api-gateway.shuwenda.icu/apikey-manage/api/search"
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
var DEFAULT_BUILD_ENV = "production";
|
|
296
|
-
var ACTIVE_ENV_CONFIG = ENV_CONFIG[DEFAULT_BUILD_ENV];
|
|
297
|
-
|
|
298
|
-
// dist-npm/plugin/index.js
|
|
294
|
+
// dist/plugin/index.js
|
|
299
295
|
var name = "@dcrays/web-search";
|
|
300
296
|
var inject = ["tools"];
|
|
301
297
|
var Config = z.object({
|
|
302
298
|
enabled: z.boolean().default(true),
|
|
303
|
-
endpointUrl: z.string().default(""),
|
|
304
|
-
metadataPath: z.string().default(""),
|
|
305
299
|
timeoutMs: z.natural().min(1).default(3e4),
|
|
306
300
|
maxResponseBytes: z.natural().min(1).default(1024 * 1024),
|
|
307
301
|
defaultCount: z.natural().min(1).default(5),
|
|
@@ -316,10 +310,7 @@ var MbhWebSearchService = class extends Service {
|
|
|
316
310
|
if (!config.enabled)
|
|
317
311
|
return;
|
|
318
312
|
this.operations = new WebSearchApiClient({
|
|
319
|
-
|
|
320
|
-
metadataPath: resolveMetadataPath(config.metadataPath),
|
|
321
|
-
channelId: ACTIVE_ENV_CONFIG.channelId,
|
|
322
|
-
legacyChannelId: ACTIVE_ENV_CONFIG.legacyChannelId,
|
|
313
|
+
metadataPath: resolveMetadataPath(),
|
|
323
314
|
timeoutMs: config.timeoutMs,
|
|
324
315
|
maxResponseBytes: config.maxResponseBytes,
|
|
325
316
|
defaultCount: config.defaultCount,
|
|
@@ -336,8 +327,8 @@ var MbhWebSearchService = class extends Service {
|
|
|
336
327
|
return this.operations.search(request, signal);
|
|
337
328
|
}
|
|
338
329
|
};
|
|
339
|
-
function resolveMetadataPath(
|
|
340
|
-
return
|
|
330
|
+
function resolveMetadataPath() {
|
|
331
|
+
return resolve(resolveDshHome(), "mobook.json");
|
|
341
332
|
}
|
|
342
333
|
function apply(ctx, config) {
|
|
343
334
|
const service = new MbhWebSearchService(ctx, config);
|
|
@@ -345,13 +336,12 @@ function apply(ctx, config) {
|
|
|
345
336
|
if (!service.isEnabled())
|
|
346
337
|
return () => void 0;
|
|
347
338
|
const unregister = ctx.tools.register(createWebSearchTool(service));
|
|
348
|
-
ctx.logger(name).info("%s plugin loaded with web_search tool
|
|
339
|
+
ctx.logger(name).info("%s plugin loaded with web_search tool", name);
|
|
349
340
|
return unregister;
|
|
350
341
|
}, `${name} lifecycle`);
|
|
351
342
|
}
|
|
352
343
|
export {
|
|
353
344
|
Config,
|
|
354
|
-
DEFAULT_BUILD_ENV,
|
|
355
345
|
MbhWebSearchService,
|
|
356
346
|
apply,
|
|
357
347
|
inject,
|
package/src/api-client.d.ts
CHANGED
|
@@ -24,10 +24,7 @@ export interface WebSearchOperations {
|
|
|
24
24
|
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
25
25
|
}
|
|
26
26
|
export interface WebSearchApiClientConfig {
|
|
27
|
-
endpointUrl: string;
|
|
28
27
|
metadataPath: string;
|
|
29
|
-
channelId: string;
|
|
30
|
-
legacyChannelId: string;
|
|
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
|
|
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 endpoint;
|
|
57
54
|
private readonly fetchImpl;
|
|
58
55
|
private readonly readFileImpl;
|
|
59
56
|
private readonly nowSeconds;
|
package/src/api-client.js
CHANGED
|
@@ -18,20 +18,26 @@ function record(value) {
|
|
|
18
18
|
function nonEmptyString(value) {
|
|
19
19
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
20
20
|
}
|
|
21
|
-
export function resolveSearchCredentials(metadata
|
|
21
|
+
export function resolveSearchCredentials(metadata) {
|
|
22
22
|
const root = record(metadata);
|
|
23
|
-
const plugins = record(root?.plugins);
|
|
24
23
|
const hardware = record(root?.hardware);
|
|
24
|
+
const account = record(root?.account);
|
|
25
25
|
const deviceId = nonEmptyString(hardware?.fingerprint);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return { userId, botToken, deviceId };
|
|
32
|
-
}
|
|
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 };
|
|
33
31
|
throw new WebSearchApiError('MISSING_CREDENTIALS', '搜索服务尚未准备好,请确认已登录墨宝并完成客户端初始化后重试。');
|
|
34
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
|
+
}
|
|
35
41
|
function requestSignal(signal, timeoutMs) {
|
|
36
42
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
37
43
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
@@ -118,24 +124,27 @@ function parseResponse(text, fallbackQuery, service) {
|
|
|
118
124
|
}
|
|
119
125
|
return result;
|
|
120
126
|
}
|
|
127
|
+
function toEndpoint(endpointUrl) {
|
|
128
|
+
let endpoint;
|
|
129
|
+
try {
|
|
130
|
+
endpoint = new URL(endpointUrl);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址配置不正确,请联系管理员检查插件配置。', { cause: error });
|
|
134
|
+
}
|
|
135
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)) {
|
|
136
|
+
throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址仅支持 HTTP 或 HTTPS,请联系管理员检查插件配置。');
|
|
137
|
+
}
|
|
138
|
+
return endpoint;
|
|
139
|
+
}
|
|
121
140
|
export class WebSearchApiClient {
|
|
122
141
|
config;
|
|
123
|
-
endpoint;
|
|
124
142
|
fetchImpl;
|
|
125
143
|
readFileImpl;
|
|
126
144
|
nowSeconds;
|
|
127
145
|
nonce;
|
|
128
146
|
constructor(config, dependencies = {}) {
|
|
129
147
|
this.config = config;
|
|
130
|
-
try {
|
|
131
|
-
this.endpoint = new URL(config.endpointUrl);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址配置不正确,请联系管理员检查插件配置。', { cause: error });
|
|
135
|
-
}
|
|
136
|
-
if (!['http:', 'https:'].includes(this.endpoint.protocol)) {
|
|
137
|
-
throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址仅支持 HTTP 或 HTTPS,请联系管理员检查插件配置。');
|
|
138
|
-
}
|
|
139
148
|
for (const [name, value] of [
|
|
140
149
|
['timeoutMs', config.timeoutMs],
|
|
141
150
|
['maxResponseBytes', config.maxResponseBytes],
|
|
@@ -171,9 +180,12 @@ export class WebSearchApiClient {
|
|
|
171
180
|
metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, 'utf8'));
|
|
172
181
|
}
|
|
173
182
|
catch (error) {
|
|
174
|
-
throw new WebSearchApiError('MISSING_CREDENTIALS', '暂时无法读取墨宝运行配置,请确认客户端已正常初始化后重试。', {
|
|
183
|
+
throw new WebSearchApiError('MISSING_CREDENTIALS', '暂时无法读取墨宝运行配置,请确认客户端已正常初始化后重试。', {
|
|
184
|
+
cause: error
|
|
185
|
+
});
|
|
175
186
|
}
|
|
176
|
-
const
|
|
187
|
+
const endpoint = resolveSearchEndpoint(metadata);
|
|
188
|
+
const credentials = resolveSearchCredentials(metadata);
|
|
177
189
|
const call = async (service) => {
|
|
178
190
|
const timestamp = String(this.nowSeconds());
|
|
179
191
|
const nonce = this.nonce();
|
|
@@ -199,7 +211,7 @@ export class WebSearchApiClient {
|
|
|
199
211
|
const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
|
|
200
212
|
let response;
|
|
201
213
|
try {
|
|
202
|
-
response = await this.fetchImpl(
|
|
214
|
+
response = await this.fetchImpl(endpoint, {
|
|
203
215
|
method: 'POST',
|
|
204
216
|
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
205
217
|
body: JSON.stringify(body),
|
|
@@ -214,7 +226,9 @@ export class WebSearchApiClient {
|
|
|
214
226
|
cause: error
|
|
215
227
|
});
|
|
216
228
|
}
|
|
217
|
-
throw new WebSearchApiError('NETWORK_ERROR',
|
|
229
|
+
throw new WebSearchApiError('NETWORK_ERROR', `暂时无法连接 ${searchServiceLabel(service)},请检查网络后重试。`, {
|
|
230
|
+
cause: error
|
|
231
|
+
});
|
|
218
232
|
}
|
|
219
233
|
const text = await responseText(response, this.config.maxResponseBytes);
|
|
220
234
|
if (!response.ok) {
|
|
@@ -223,14 +237,14 @@ export class WebSearchApiClient {
|
|
|
223
237
|
return parseResponse(text, query, service);
|
|
224
238
|
};
|
|
225
239
|
try {
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
return
|
|
240
|
+
const bocha = await call('bocha_search');
|
|
241
|
+
if (bocha.results.length > 0)
|
|
242
|
+
return bocha;
|
|
229
243
|
}
|
|
230
244
|
catch (error) {
|
|
231
245
|
if (signal?.aborted)
|
|
232
246
|
throw error;
|
|
233
247
|
}
|
|
234
|
-
return call('
|
|
248
|
+
return call('tavily_search');
|
|
235
249
|
}
|
|
236
250
|
}
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export interface EnvConfig {
|
|
3
|
-
packageName: string;
|
|
4
|
-
channelId: string;
|
|
5
|
-
legacyChannelId: string;
|
|
6
|
-
endpointUrl: string;
|
|
7
|
-
}
|
|
8
|
-
export declare const ENV_CONFIG: Record<BuildEnv, EnvConfig>;
|
|
9
|
-
export declare const DEFAULT_BUILD_ENV: BuildEnv;
|
|
10
|
-
export declare const ACTIVE_ENV_CONFIG: EnvConfig;
|
|
1
|
+
export declare const PACKAGE_NAME = "@dcrays/web-search";
|
package/src/config/env-config.js
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
export const
|
|
2
|
-
production: {
|
|
3
|
-
packageName: '@dcrays/web-search',
|
|
4
|
-
channelId: 'mbh-chat',
|
|
5
|
-
legacyChannelId: 'mbhchat',
|
|
6
|
-
endpointUrl: 'https://api-gateway.shuwenda.com/apikey-manage/api/search'
|
|
7
|
-
},
|
|
8
|
-
test: {
|
|
9
|
-
packageName: '@dcrays/web-search-test',
|
|
10
|
-
channelId: 'mbh-chat-test',
|
|
11
|
-
legacyChannelId: 'mbhchat-test',
|
|
12
|
-
endpointUrl: 'https://api-gateway.shuwenda.icu/apikey-manage/api/search'
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
export const DEFAULT_BUILD_ENV = 'production';
|
|
16
|
-
export const ACTIVE_ENV_CONFIG = ENV_CONFIG[DEFAULT_BUILD_ENV];
|
|
1
|
+
export const PACKAGE_NAME = '@dcrays/web-search';
|
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
|
|
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.' },
|