@dcrays/web-search 0.1.0 → 0.1.2
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 +7 -7
- package/plugin/index.d.ts +1 -1
- package/plugin/index.js +160 -114
- package/src/api-client.d.ts +6 -4
- package/src/api-client.js +127 -81
- package/src/config/env-config.d.ts +10 -5
- package/src/config/env-config.js +10 -4
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` 中记录的环境选择搜索网关与 MBHChat 凭据通道;`endpointUrl` 可在 Cordis 插件配置中覆盖。
|
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.2",
|
|
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",
|
|
@@ -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.
|
|
36
|
-
"@deepseek-ai/dsh-
|
|
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
|
@@ -2,7 +2,6 @@ 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 {
|
|
@@ -27,4 +26,5 @@ export declare class MbhWebSearchService extends Service {
|
|
|
27
26
|
isEnabled(): boolean;
|
|
28
27
|
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
29
28
|
}
|
|
29
|
+
export declare function resolveMetadataPath(configuredPath: string): string;
|
|
30
30
|
export declare function apply(ctx: Context, config: Config): void;
|
package/plugin/index.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { createRequire as __mobookCreateRequire } from 'node:module'; const require = __mobookCreateRequire(import.meta.url);
|
|
2
2
|
|
|
3
|
-
//
|
|
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
|
-
//
|
|
9
|
+
// dist-npm/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
|
|
12
31
|
var WebSearchApiError = class extends Error {
|
|
13
32
|
code;
|
|
14
33
|
constructor(code, message, options) {
|
|
@@ -17,6 +36,10 @@ var WebSearchApiError = class extends Error {
|
|
|
17
36
|
this.name = "WebSearchApiError";
|
|
18
37
|
}
|
|
19
38
|
};
|
|
39
|
+
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";
|
|
40
|
+
function searchServiceLabel(service) {
|
|
41
|
+
return service === "tavily_search" ? "Tavily \u641C\u7D22" : "\u535A\u67E5\u641C\u7D22";
|
|
42
|
+
}
|
|
20
43
|
function record(value) {
|
|
21
44
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
22
45
|
}
|
|
@@ -35,7 +58,7 @@ function resolveSearchCredentials(metadata, channelId, legacyChannelId) {
|
|
|
35
58
|
if (userId && botToken && deviceId)
|
|
36
59
|
return { userId, botToken, deviceId };
|
|
37
60
|
}
|
|
38
|
-
throw new WebSearchApiError("MISSING_CREDENTIALS",
|
|
61
|
+
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
62
|
}
|
|
40
63
|
function requestSignal(signal, timeoutMs) {
|
|
41
64
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
@@ -45,7 +68,7 @@ async function responseText(response, maxBytes) {
|
|
|
45
68
|
const declared = Number(response.headers.get("content-length"));
|
|
46
69
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
47
70
|
await response.body?.cancel().catch(() => void 0);
|
|
48
|
-
throw new WebSearchApiError("RESPONSE_TOO_LARGE",
|
|
71
|
+
throw new WebSearchApiError("RESPONSE_TOO_LARGE", RESPONSE_TOO_LARGE_MESSAGE);
|
|
49
72
|
}
|
|
50
73
|
if (!response.body)
|
|
51
74
|
return "";
|
|
@@ -61,7 +84,7 @@ async function responseText(response, maxBytes) {
|
|
|
61
84
|
size += item.value.byteLength;
|
|
62
85
|
if (size > maxBytes) {
|
|
63
86
|
await reader.cancel().catch(() => void 0);
|
|
64
|
-
throw new WebSearchApiError("RESPONSE_TOO_LARGE",
|
|
87
|
+
throw new WebSearchApiError("RESPONSE_TOO_LARGE", RESPONSE_TOO_LARGE_MESSAGE);
|
|
65
88
|
}
|
|
66
89
|
text += decoder.decode(item.value, { stream: true });
|
|
67
90
|
}
|
|
@@ -70,66 +93,92 @@ async function responseText(response, maxBytes) {
|
|
|
70
93
|
reader.releaseLock();
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
|
-
function
|
|
96
|
+
function searchItem(value, snippetKeys, publishedAtKeys) {
|
|
97
|
+
const item = record(value);
|
|
98
|
+
const url = nonEmptyString(item?.url);
|
|
99
|
+
if (!item || !url)
|
|
100
|
+
return [];
|
|
101
|
+
const result = {
|
|
102
|
+
title: nonEmptyString(item.title) ?? nonEmptyString(item.name) ?? url,
|
|
103
|
+
url,
|
|
104
|
+
snippet: snippetKeys.map((key) => nonEmptyString(item[key])).find(Boolean) ?? ""
|
|
105
|
+
};
|
|
106
|
+
const siteName = nonEmptyString(item.siteName);
|
|
107
|
+
const publishedAt = publishedAtKeys.map((key) => nonEmptyString(item[key])).find(Boolean);
|
|
108
|
+
if (siteName)
|
|
109
|
+
result.siteName = siteName;
|
|
110
|
+
if (publishedAt)
|
|
111
|
+
result.publishedAt = publishedAt;
|
|
112
|
+
return [result];
|
|
113
|
+
}
|
|
114
|
+
function parseResponse(text, fallbackQuery, service) {
|
|
74
115
|
let payload;
|
|
75
116
|
try {
|
|
76
117
|
payload = JSON.parse(text);
|
|
77
118
|
} catch (error) {
|
|
78
|
-
throw new WebSearchApiError("INVALID_RESPONSE", "
|
|
119
|
+
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
120
|
}
|
|
80
121
|
const wrapper = record(payload);
|
|
81
122
|
const upstream = record(wrapper?.data);
|
|
82
123
|
if (wrapper?.success !== true || typeof upstream?.code === "number" && upstream.code !== 200) {
|
|
83
|
-
throw new WebSearchApiError("API_ERROR", "
|
|
124
|
+
throw new WebSearchApiError("API_ERROR", "\u641C\u7D22\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
|
|
125
|
+
}
|
|
126
|
+
if (service === "tavily_search") {
|
|
127
|
+
const values = Array.isArray(upstream?.results) ? upstream.results : [];
|
|
128
|
+
return {
|
|
129
|
+
query: nonEmptyString(upstream?.query) ?? fallbackQuery,
|
|
130
|
+
provider: service,
|
|
131
|
+
results: values.flatMap((value) => searchItem(value, ["content", "snippet"], ["published_date", "publishedAt"]))
|
|
132
|
+
};
|
|
84
133
|
}
|
|
85
134
|
const searchData = record(upstream?.data);
|
|
86
135
|
const pages = record(searchData?.webPages);
|
|
87
136
|
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
137
|
const result = {
|
|
108
138
|
query: nonEmptyString(queryContext?.originalQuery) ?? fallbackQuery,
|
|
109
|
-
|
|
139
|
+
provider: service,
|
|
140
|
+
results: (Array.isArray(pages?.value) ? pages.value : []).flatMap((value) => searchItem(value, ["snippet"], ["datePublished"]))
|
|
110
141
|
};
|
|
111
142
|
if (typeof pages?.totalEstimatedMatches === "number" && Number.isFinite(pages.totalEstimatedMatches)) {
|
|
112
143
|
result.totalEstimatedMatches = pages.totalEstimatedMatches;
|
|
113
144
|
}
|
|
114
145
|
return result;
|
|
115
146
|
}
|
|
147
|
+
function toEndpoint(endpointUrl) {
|
|
148
|
+
let endpoint;
|
|
149
|
+
try {
|
|
150
|
+
endpoint = new URL(endpointUrl);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
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 });
|
|
153
|
+
}
|
|
154
|
+
if (!["http:", "https:"].includes(endpoint.protocol)) {
|
|
155
|
+
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");
|
|
156
|
+
}
|
|
157
|
+
return endpoint;
|
|
158
|
+
}
|
|
116
159
|
var WebSearchApiClient = class {
|
|
117
160
|
config;
|
|
118
|
-
|
|
161
|
+
environments;
|
|
119
162
|
fetchImpl;
|
|
120
163
|
readFileImpl;
|
|
121
164
|
nowSeconds;
|
|
122
165
|
nonce;
|
|
123
166
|
constructor(config, dependencies = {}) {
|
|
124
167
|
this.config = config;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
};
|
|
133
182
|
for (const [name2, value] of [
|
|
134
183
|
["timeoutMs", config.timeoutMs],
|
|
135
184
|
["maxResponseBytes", config.maxResponseBytes],
|
|
@@ -137,11 +186,11 @@ var WebSearchApiClient = class {
|
|
|
137
186
|
["maxCount", config.maxCount]
|
|
138
187
|
]) {
|
|
139
188
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
140
|
-
throw new WebSearchApiError("INVALID_CONFIG",
|
|
189
|
+
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
190
|
}
|
|
142
191
|
}
|
|
143
192
|
if (config.defaultCount > config.maxCount) {
|
|
144
|
-
throw new WebSearchApiError("INVALID_CONFIG", "
|
|
193
|
+
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
194
|
}
|
|
146
195
|
this.fetchImpl = dependencies.fetch ?? fetch;
|
|
147
196
|
this.readFileImpl = dependencies.readFile ?? readFile;
|
|
@@ -151,73 +200,87 @@ var WebSearchApiClient = class {
|
|
|
151
200
|
async search(request, signal) {
|
|
152
201
|
const query = request.query.trim();
|
|
153
202
|
if (!query)
|
|
154
|
-
throw new WebSearchApiError("INVALID_REQUEST", "
|
|
203
|
+
throw new WebSearchApiError("INVALID_REQUEST", "\u8BF7\u8F93\u5165\u8981\u641C\u7D22\u7684\u5185\u5BB9\u540E\u91CD\u8BD5\u3002");
|
|
155
204
|
const count = request.count ?? this.config.defaultCount;
|
|
156
205
|
const offset = request.offset ?? 0;
|
|
157
206
|
if (!Number.isSafeInteger(count) || count < 1 || count > this.config.maxCount) {
|
|
158
|
-
throw new WebSearchApiError("INVALID_REQUEST",
|
|
207
|
+
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
208
|
}
|
|
160
209
|
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
161
|
-
throw new WebSearchApiError("INVALID_REQUEST", "
|
|
210
|
+
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
211
|
}
|
|
163
212
|
let metadata;
|
|
164
213
|
try {
|
|
165
214
|
metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, "utf8"));
|
|
166
215
|
} catch (error) {
|
|
167
|
-
throw new WebSearchApiError("MISSING_CREDENTIALS", "
|
|
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 });
|
|
168
217
|
}
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
const
|
|
218
|
+
const environment = resolveWebSearchEnvironment(record(metadata)?.environment);
|
|
219
|
+
const resolved = this.environments[environment];
|
|
220
|
+
const credentials = resolveSearchCredentials(metadata, resolved.channelId, resolved.legacyChannelId);
|
|
221
|
+
const call = async (service) => {
|
|
222
|
+
const timestamp = String(this.nowSeconds());
|
|
223
|
+
const nonce = this.nonce();
|
|
224
|
+
const message = `${credentials.userId}
|
|
173
225
|
${credentials.deviceId}
|
|
174
226
|
${timestamp}
|
|
175
227
|
${nonce}`;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
228
|
+
const signature = createHmac("sha256", credentials.botToken).update(message, "utf8").digest("hex");
|
|
229
|
+
const body = {
|
|
230
|
+
user_id: credentials.userId,
|
|
231
|
+
bot_token: credentials.botToken,
|
|
232
|
+
device_id: credentials.deviceId,
|
|
233
|
+
signature,
|
|
234
|
+
timestamp,
|
|
235
|
+
nonce,
|
|
236
|
+
service,
|
|
237
|
+
query,
|
|
238
|
+
count,
|
|
239
|
+
offset,
|
|
240
|
+
language: request.language?.trim() || "zh-CN"
|
|
241
|
+
};
|
|
242
|
+
if (request.includeDomains?.length)
|
|
243
|
+
body.include_domains = request.includeDomains;
|
|
244
|
+
if (request.excludeDomains?.length)
|
|
245
|
+
body.exclude_domains = request.excludeDomains;
|
|
246
|
+
const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = await this.fetchImpl(resolved.endpoint, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
252
|
+
body: JSON.stringify(body),
|
|
253
|
+
signal: effectiveSignal
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (signal?.aborted)
|
|
257
|
+
throw error;
|
|
258
|
+
if (effectiveSignal.aborted) {
|
|
259
|
+
throw new WebSearchApiError("TIMEOUT", `${searchServiceLabel(service)}\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002`, {
|
|
260
|
+
cause: error
|
|
261
|
+
});
|
|
262
|
+
}
|
|
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 });
|
|
264
|
+
}
|
|
265
|
+
const text = await responseText(response, this.config.maxResponseBytes);
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
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`);
|
|
268
|
+
}
|
|
269
|
+
return parseResponse(text, query, service);
|
|
189
270
|
};
|
|
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
271
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
body: JSON.stringify(body),
|
|
201
|
-
signal: effectiveSignal
|
|
202
|
-
});
|
|
272
|
+
const tavily = await call("tavily_search");
|
|
273
|
+
if (tavily.results.length > 0)
|
|
274
|
+
return tavily;
|
|
203
275
|
} catch (error) {
|
|
204
276
|
if (signal?.aborted)
|
|
205
277
|
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
278
|
}
|
|
213
|
-
|
|
214
|
-
if (!response.ok)
|
|
215
|
-
throw new WebSearchApiError("HTTP_ERROR", `web search request failed with HTTP ${response.status}`);
|
|
216
|
-
return parseResponse(text, query);
|
|
279
|
+
return call("bocha_search");
|
|
217
280
|
}
|
|
218
281
|
};
|
|
219
282
|
|
|
220
|
-
//
|
|
283
|
+
// dist-npm/src/tools.js
|
|
221
284
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
222
285
|
function createWebSearchTool(search) {
|
|
223
286
|
return defineTool({
|
|
@@ -250,28 +313,10 @@ function createWebSearchTool(search) {
|
|
|
250
313
|
});
|
|
251
314
|
}
|
|
252
315
|
|
|
253
|
-
//
|
|
316
|
+
// dist-npm/src/index.js
|
|
254
317
|
var MBH_WEB_SEARCH_SERVICE_KEY = "mbhWebSearch";
|
|
255
318
|
|
|
256
|
-
//
|
|
257
|
-
var ENV_CONFIG = {
|
|
258
|
-
production: {
|
|
259
|
-
packageName: "@dcrays/web-search",
|
|
260
|
-
channelId: "mbh-chat",
|
|
261
|
-
legacyChannelId: "mbhchat",
|
|
262
|
-
endpointUrl: "https://api-gateway.shuwenda.com/apikey-manage/api/search"
|
|
263
|
-
},
|
|
264
|
-
test: {
|
|
265
|
-
packageName: "@dcrays/web-search-test",
|
|
266
|
-
channelId: "mbh-chat-test",
|
|
267
|
-
legacyChannelId: "mbhchat-test",
|
|
268
|
-
endpointUrl: "https://api-gateway.shuwenda.icu/apikey-manage/api/search"
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
var DEFAULT_BUILD_ENV = "production";
|
|
272
|
-
var ACTIVE_ENV_CONFIG = ENV_CONFIG[DEFAULT_BUILD_ENV];
|
|
273
|
-
|
|
274
|
-
// plugins/web-search/dist-npm/plugin/index.js
|
|
319
|
+
// dist-npm/plugin/index.js
|
|
275
320
|
var name = "@dcrays/web-search";
|
|
276
321
|
var inject = ["tools"];
|
|
277
322
|
var Config = z.object({
|
|
@@ -291,12 +336,9 @@ var MbhWebSearchService = class extends Service {
|
|
|
291
336
|
this.config = config;
|
|
292
337
|
if (!config.enabled)
|
|
293
338
|
return;
|
|
294
|
-
const dshHome = process.env.DSH_HOME?.trim() || resolve(homedir(), ".mobook-harness");
|
|
295
339
|
this.operations = new WebSearchApiClient({
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
channelId: ACTIVE_ENV_CONFIG.channelId,
|
|
299
|
-
legacyChannelId: ACTIVE_ENV_CONFIG.legacyChannelId,
|
|
340
|
+
metadataPath: resolveMetadataPath(config.metadataPath),
|
|
341
|
+
...config.endpointUrl.trim() ? { endpointOverride: config.endpointUrl.trim() } : {},
|
|
300
342
|
timeoutMs: config.timeoutMs,
|
|
301
343
|
maxResponseBytes: config.maxResponseBytes,
|
|
302
344
|
defaultCount: config.defaultCount,
|
|
@@ -307,26 +349,30 @@ var MbhWebSearchService = class extends Service {
|
|
|
307
349
|
return this.config.enabled;
|
|
308
350
|
}
|
|
309
351
|
search(request, signal) {
|
|
310
|
-
if (!this.operations)
|
|
311
|
-
return Promise.reject(new Error("
|
|
352
|
+
if (!this.operations) {
|
|
353
|
+
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"));
|
|
354
|
+
}
|
|
312
355
|
return this.operations.search(request, signal);
|
|
313
356
|
}
|
|
314
357
|
};
|
|
358
|
+
function resolveMetadataPath(configuredPath) {
|
|
359
|
+
return configuredPath.trim() || resolve(resolveDshHome(), "mobook.json");
|
|
360
|
+
}
|
|
315
361
|
function apply(ctx, config) {
|
|
316
362
|
const service = new MbhWebSearchService(ctx, config);
|
|
317
363
|
ctx.effect(() => {
|
|
318
364
|
if (!service.isEnabled())
|
|
319
365
|
return () => void 0;
|
|
320
366
|
const unregister = ctx.tools.register(createWebSearchTool(service));
|
|
321
|
-
ctx.logger(name).info("%s plugin loaded with web_search tool
|
|
367
|
+
ctx.logger(name).info("%s plugin loaded with web_search tool", name);
|
|
322
368
|
return unregister;
|
|
323
369
|
}, `${name} lifecycle`);
|
|
324
370
|
}
|
|
325
371
|
export {
|
|
326
372
|
Config,
|
|
327
|
-
DEFAULT_BUILD_ENV,
|
|
328
373
|
MbhWebSearchService,
|
|
329
374
|
apply,
|
|
330
375
|
inject,
|
|
331
|
-
name
|
|
376
|
+
name,
|
|
377
|
+
resolveMetadataPath
|
|
332
378
|
};
|
package/src/api-client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EnvConfig, type WebSearchEnvironment } from './config/env-config.js';
|
|
1
2
|
export interface WebSearchRequest {
|
|
2
3
|
query: string;
|
|
3
4
|
count?: number;
|
|
@@ -15,17 +16,18 @@ export interface WebSearchItem {
|
|
|
15
16
|
}
|
|
16
17
|
export interface WebSearchResult {
|
|
17
18
|
query: string;
|
|
19
|
+
provider: SearchService;
|
|
18
20
|
totalEstimatedMatches?: number;
|
|
19
21
|
results: WebSearchItem[];
|
|
20
22
|
}
|
|
23
|
+
export type SearchService = 'tavily_search' | 'bocha_search';
|
|
21
24
|
export interface WebSearchOperations {
|
|
22
25
|
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
23
26
|
}
|
|
24
27
|
export interface WebSearchApiClientConfig {
|
|
25
|
-
endpointUrl: string;
|
|
26
28
|
metadataPath: string;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
endpointOverride?: string;
|
|
30
|
+
environments?: Record<WebSearchEnvironment, EnvConfig>;
|
|
29
31
|
timeoutMs: number;
|
|
30
32
|
maxResponseBytes: number;
|
|
31
33
|
defaultCount: number;
|
|
@@ -51,7 +53,7 @@ interface SearchCredentials {
|
|
|
51
53
|
export declare function resolveSearchCredentials(metadata: unknown, channelId: string, legacyChannelId: string): SearchCredentials;
|
|
52
54
|
export declare class WebSearchApiClient implements WebSearchOperations {
|
|
53
55
|
private readonly config;
|
|
54
|
-
private readonly
|
|
56
|
+
private readonly environments;
|
|
55
57
|
private readonly fetchImpl;
|
|
56
58
|
private readonly readFileImpl;
|
|
57
59
|
private readonly nowSeconds;
|
package/src/api-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
3
4
|
export class WebSearchApiError extends Error {
|
|
4
5
|
code;
|
|
5
6
|
constructor(code, message, options) {
|
|
@@ -8,6 +9,10 @@ export class WebSearchApiError extends Error {
|
|
|
8
9
|
this.name = 'WebSearchApiError';
|
|
9
10
|
}
|
|
10
11
|
}
|
|
12
|
+
const RESPONSE_TOO_LARGE_MESSAGE = '搜索服务返回的内容过大,暂时无法处理。请缩小搜索范围后重试。';
|
|
13
|
+
function searchServiceLabel(service) {
|
|
14
|
+
return service === 'tavily_search' ? 'Tavily 搜索' : '博查搜索';
|
|
15
|
+
}
|
|
11
16
|
function record(value) {
|
|
12
17
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
13
18
|
}
|
|
@@ -26,7 +31,7 @@ export function resolveSearchCredentials(metadata, channelId, legacyChannelId) {
|
|
|
26
31
|
if (userId && botToken && deviceId)
|
|
27
32
|
return { userId, botToken, deviceId };
|
|
28
33
|
}
|
|
29
|
-
throw new WebSearchApiError('MISSING_CREDENTIALS',
|
|
34
|
+
throw new WebSearchApiError('MISSING_CREDENTIALS', '搜索服务尚未准备好,请确认已登录墨宝并完成客户端初始化后重试。');
|
|
30
35
|
}
|
|
31
36
|
function requestSignal(signal, timeoutMs) {
|
|
32
37
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
@@ -36,7 +41,7 @@ async function responseText(response, maxBytes) {
|
|
|
36
41
|
const declared = Number(response.headers.get('content-length'));
|
|
37
42
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
38
43
|
await response.body?.cancel().catch(() => undefined);
|
|
39
|
-
throw new WebSearchApiError('RESPONSE_TOO_LARGE',
|
|
44
|
+
throw new WebSearchApiError('RESPONSE_TOO_LARGE', RESPONSE_TOO_LARGE_MESSAGE);
|
|
40
45
|
}
|
|
41
46
|
if (!response.body)
|
|
42
47
|
return '';
|
|
@@ -52,7 +57,7 @@ async function responseText(response, maxBytes) {
|
|
|
52
57
|
size += item.value.byteLength;
|
|
53
58
|
if (size > maxBytes) {
|
|
54
59
|
await reader.cancel().catch(() => undefined);
|
|
55
|
-
throw new WebSearchApiError('RESPONSE_TOO_LARGE',
|
|
60
|
+
throw new WebSearchApiError('RESPONSE_TOO_LARGE', RESPONSE_TOO_LARGE_MESSAGE);
|
|
56
61
|
}
|
|
57
62
|
text += decoder.decode(item.value, { stream: true });
|
|
58
63
|
}
|
|
@@ -62,68 +67,94 @@ async function responseText(response, maxBytes) {
|
|
|
62
67
|
reader.releaseLock();
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
|
-
function
|
|
70
|
+
function searchItem(value, snippetKeys, publishedAtKeys) {
|
|
71
|
+
const item = record(value);
|
|
72
|
+
const url = nonEmptyString(item?.url);
|
|
73
|
+
if (!item || !url)
|
|
74
|
+
return [];
|
|
75
|
+
const result = {
|
|
76
|
+
title: nonEmptyString(item.title) ?? nonEmptyString(item.name) ?? url,
|
|
77
|
+
url,
|
|
78
|
+
snippet: snippetKeys.map((key) => nonEmptyString(item[key])).find(Boolean) ?? ''
|
|
79
|
+
};
|
|
80
|
+
const siteName = nonEmptyString(item.siteName);
|
|
81
|
+
const publishedAt = publishedAtKeys.map((key) => nonEmptyString(item[key])).find(Boolean);
|
|
82
|
+
if (siteName)
|
|
83
|
+
result.siteName = siteName;
|
|
84
|
+
if (publishedAt)
|
|
85
|
+
result.publishedAt = publishedAt;
|
|
86
|
+
return [result];
|
|
87
|
+
}
|
|
88
|
+
function parseResponse(text, fallbackQuery, service) {
|
|
66
89
|
let payload;
|
|
67
90
|
try {
|
|
68
91
|
payload = JSON.parse(text);
|
|
69
92
|
}
|
|
70
93
|
catch (error) {
|
|
71
|
-
throw new WebSearchApiError('INVALID_RESPONSE', '
|
|
94
|
+
throw new WebSearchApiError('INVALID_RESPONSE', '搜索服务返回的数据格式异常,请稍后重试。', { cause: error });
|
|
72
95
|
}
|
|
73
96
|
const wrapper = record(payload);
|
|
74
97
|
const upstream = record(wrapper?.data);
|
|
75
98
|
if (wrapper?.success !== true || (typeof upstream?.code === 'number' && upstream.code !== 200)) {
|
|
76
|
-
throw new WebSearchApiError('API_ERROR', '
|
|
99
|
+
throw new WebSearchApiError('API_ERROR', '搜索服务暂时不可用,请稍后重试。');
|
|
100
|
+
}
|
|
101
|
+
if (service === 'tavily_search') {
|
|
102
|
+
const values = Array.isArray(upstream?.results) ? upstream.results : [];
|
|
103
|
+
return {
|
|
104
|
+
query: nonEmptyString(upstream?.query) ?? fallbackQuery,
|
|
105
|
+
provider: service,
|
|
106
|
+
results: values.flatMap((value) => searchItem(value, ['content', 'snippet'], ['published_date', 'publishedAt']))
|
|
107
|
+
};
|
|
77
108
|
}
|
|
78
109
|
const searchData = record(upstream?.data);
|
|
79
110
|
const pages = record(searchData?.webPages);
|
|
80
111
|
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
112
|
const result = {
|
|
101
113
|
query: nonEmptyString(queryContext?.originalQuery) ?? fallbackQuery,
|
|
102
|
-
|
|
114
|
+
provider: service,
|
|
115
|
+
results: (Array.isArray(pages?.value) ? pages.value : []).flatMap((value) => searchItem(value, ['snippet'], ['datePublished']))
|
|
103
116
|
};
|
|
104
117
|
if (typeof pages?.totalEstimatedMatches === 'number' && Number.isFinite(pages.totalEstimatedMatches)) {
|
|
105
118
|
result.totalEstimatedMatches = pages.totalEstimatedMatches;
|
|
106
119
|
}
|
|
107
120
|
return result;
|
|
108
121
|
}
|
|
122
|
+
function toEndpoint(endpointUrl) {
|
|
123
|
+
let endpoint;
|
|
124
|
+
try {
|
|
125
|
+
endpoint = new URL(endpointUrl);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址配置不正确,请联系管理员检查插件配置。', { cause: error });
|
|
129
|
+
}
|
|
130
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)) {
|
|
131
|
+
throw new WebSearchApiError('INVALID_CONFIG', '搜索服务地址仅支持 HTTP 或 HTTPS,请联系管理员检查插件配置。');
|
|
132
|
+
}
|
|
133
|
+
return endpoint;
|
|
134
|
+
}
|
|
109
135
|
export class WebSearchApiClient {
|
|
110
136
|
config;
|
|
111
|
-
|
|
137
|
+
environments;
|
|
112
138
|
fetchImpl;
|
|
113
139
|
readFileImpl;
|
|
114
140
|
nowSeconds;
|
|
115
141
|
nonce;
|
|
116
142
|
constructor(config, dependencies = {}) {
|
|
117
143
|
this.config = config;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
};
|
|
127
158
|
for (const [name, value] of [
|
|
128
159
|
['timeoutMs', config.timeoutMs],
|
|
129
160
|
['maxResponseBytes', config.maxResponseBytes],
|
|
@@ -131,11 +162,11 @@ export class WebSearchApiClient {
|
|
|
131
162
|
['maxCount', config.maxCount]
|
|
132
163
|
]) {
|
|
133
164
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
134
|
-
throw new WebSearchApiError('INVALID_CONFIG',
|
|
165
|
+
throw new WebSearchApiError('INVALID_CONFIG', `搜索插件配置项 ${name} 必须是大于 0 的整数,请联系管理员检查配置。`);
|
|
135
166
|
}
|
|
136
167
|
}
|
|
137
168
|
if (config.defaultCount > config.maxCount) {
|
|
138
|
-
throw new WebSearchApiError('INVALID_CONFIG', '
|
|
169
|
+
throw new WebSearchApiError('INVALID_CONFIG', '默认搜索结果数不能大于最大结果数,请联系管理员检查插件配置。');
|
|
139
170
|
}
|
|
140
171
|
this.fetchImpl = dependencies.fetch ?? fetch;
|
|
141
172
|
this.readFileImpl = dependencies.readFile ?? readFile;
|
|
@@ -145,67 +176,82 @@ export class WebSearchApiClient {
|
|
|
145
176
|
async search(request, signal) {
|
|
146
177
|
const query = request.query.trim();
|
|
147
178
|
if (!query)
|
|
148
|
-
throw new WebSearchApiError('INVALID_REQUEST', '
|
|
179
|
+
throw new WebSearchApiError('INVALID_REQUEST', '请输入要搜索的内容后重试。');
|
|
149
180
|
const count = request.count ?? this.config.defaultCount;
|
|
150
181
|
const offset = request.offset ?? 0;
|
|
151
182
|
if (!Number.isSafeInteger(count) || count < 1 || count > this.config.maxCount) {
|
|
152
|
-
throw new WebSearchApiError('INVALID_REQUEST',
|
|
183
|
+
throw new WebSearchApiError('INVALID_REQUEST', `搜索结果数量需设置为 1 到 ${this.config.maxCount} 之间的整数。`);
|
|
153
184
|
}
|
|
154
185
|
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
155
|
-
throw new WebSearchApiError('INVALID_REQUEST', '
|
|
186
|
+
throw new WebSearchApiError('INVALID_REQUEST', '搜索结果偏移量必须是大于或等于 0 的整数。');
|
|
156
187
|
}
|
|
157
188
|
let metadata;
|
|
158
189
|
try {
|
|
159
190
|
metadata = JSON.parse(await this.readFileImpl(this.config.metadataPath, 'utf8'));
|
|
160
191
|
}
|
|
161
192
|
catch (error) {
|
|
162
|
-
throw new WebSearchApiError('MISSING_CREDENTIALS', '
|
|
193
|
+
throw new WebSearchApiError('MISSING_CREDENTIALS', '暂时无法读取墨宝运行配置,请确认客户端已正常初始化后重试。', { cause: error });
|
|
163
194
|
}
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
const environment = resolveWebSearchEnvironment(record(metadata)?.environment);
|
|
196
|
+
const resolved = this.environments[environment];
|
|
197
|
+
const credentials = resolveSearchCredentials(metadata, resolved.channelId, resolved.legacyChannelId);
|
|
198
|
+
const call = async (service) => {
|
|
199
|
+
const timestamp = String(this.nowSeconds());
|
|
200
|
+
const nonce = this.nonce();
|
|
201
|
+
const message = `${credentials.userId}\n${credentials.deviceId}\n${timestamp}\n${nonce}`;
|
|
202
|
+
const signature = createHmac('sha256', credentials.botToken).update(message, 'utf8').digest('hex');
|
|
203
|
+
const body = {
|
|
204
|
+
user_id: credentials.userId,
|
|
205
|
+
bot_token: credentials.botToken,
|
|
206
|
+
device_id: credentials.deviceId,
|
|
207
|
+
signature,
|
|
208
|
+
timestamp,
|
|
209
|
+
nonce,
|
|
210
|
+
service,
|
|
211
|
+
query,
|
|
212
|
+
count,
|
|
213
|
+
offset,
|
|
214
|
+
language: request.language?.trim() || 'zh-CN'
|
|
215
|
+
};
|
|
216
|
+
if (request.includeDomains?.length)
|
|
217
|
+
body.include_domains = request.includeDomains;
|
|
218
|
+
if (request.excludeDomains?.length)
|
|
219
|
+
body.exclude_domains = request.excludeDomains;
|
|
220
|
+
const effectiveSignal = requestSignal(signal, this.config.timeoutMs);
|
|
221
|
+
let response;
|
|
222
|
+
try {
|
|
223
|
+
response = await this.fetchImpl(resolved.endpoint, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
226
|
+
body: JSON.stringify(body),
|
|
227
|
+
signal: effectiveSignal
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (signal?.aborted)
|
|
232
|
+
throw error;
|
|
233
|
+
if (effectiveSignal.aborted) {
|
|
234
|
+
throw new WebSearchApiError('TIMEOUT', `${searchServiceLabel(service)}响应超时,请稍后重试。`, {
|
|
235
|
+
cause: error
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
throw new WebSearchApiError('NETWORK_ERROR', `暂时无法连接${searchServiceLabel(service)},请检查网络后重试。`, { cause: error });
|
|
239
|
+
}
|
|
240
|
+
const text = await responseText(response, this.config.maxResponseBytes);
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
throw new WebSearchApiError('HTTP_ERROR', `${searchServiceLabel(service)}暂时不可用(服务状态 ${response.status}),请稍后重试。`);
|
|
243
|
+
}
|
|
244
|
+
return parseResponse(text, query, service);
|
|
181
245
|
};
|
|
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
246
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
body: JSON.stringify(body),
|
|
193
|
-
signal: effectiveSignal
|
|
194
|
-
});
|
|
247
|
+
const tavily = await call('tavily_search');
|
|
248
|
+
if (tavily.results.length > 0)
|
|
249
|
+
return tavily;
|
|
195
250
|
}
|
|
196
251
|
catch (error) {
|
|
197
252
|
if (signal?.aborted)
|
|
198
253
|
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
254
|
}
|
|
206
|
-
|
|
207
|
-
if (!response.ok)
|
|
208
|
-
throw new WebSearchApiError('HTTP_ERROR', `web search request failed with HTTP ${response.status}`);
|
|
209
|
-
return parseResponse(text, query);
|
|
255
|
+
return call('bocha_search');
|
|
210
256
|
}
|
|
211
257
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type WebSearchEnvironment = 'production' | 'test';
|
|
2
2
|
export interface EnvConfig {
|
|
3
|
-
packageName: string;
|
|
4
3
|
channelId: string;
|
|
5
4
|
legacyChannelId: string;
|
|
6
5
|
endpointUrl: string;
|
|
7
6
|
}
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const
|
|
10
|
-
|
|
7
|
+
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;
|
package/src/config/env-config.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
export const PACKAGE_NAME = '@dcrays/web-search';
|
|
1
2
|
export const ENV_CONFIG = {
|
|
2
3
|
production: {
|
|
3
|
-
packageName: '@dcrays/web-search',
|
|
4
4
|
channelId: 'mbh-chat',
|
|
5
5
|
legacyChannelId: 'mbhchat',
|
|
6
6
|
endpointUrl: 'https://api-gateway.shuwenda.com/apikey-manage/api/search'
|
|
7
7
|
},
|
|
8
8
|
test: {
|
|
9
|
-
packageName: '@dcrays/web-search-test',
|
|
10
9
|
channelId: 'mbh-chat-test',
|
|
11
10
|
legacyChannelId: 'mbhchat-test',
|
|
12
11
|
endpointUrl: 'https://api-gateway.shuwenda.icu/apikey-manage/api/search'
|
|
13
12
|
}
|
|
14
13
|
};
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|