@dsh-plus/web-search-services 0.1.2 → 0.1.4

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/lib/index.d.ts CHANGED
@@ -1,49 +1,53 @@
1
- import z from "@deepseek-ai/schemastery";
1
+ import { UnwrapVolatile } from "@dsh-plus/shared";
2
2
  import { WebSearchProvider, WebSearchRequest, WebSearchResult } from "@deepseek-ai/dsh-web";
3
3
  import { spawn } from "node:child_process";
4
+ import z from "@deepseek-ai/schemastery";
4
5
  import { Context } from "@deepseek-ai/cordis";
5
6
  //#region src/config.d.ts
6
7
  declare const SETTINGS_NS = "dsh-plus-web-search-services";
7
- declare const Config: z<Schemastery.ObjectS<{
8
- scriptPath: z<string, string>;
9
- envFile: z<string, string>;
10
- python: z<string, string>;
11
- priority: z<("exa" | "openai-chat" | "tavily")[], ("exa" | "openai-chat" | "tavily")[]>;
12
- timeoutMs: z<number, number>;
13
- keys: z<Schemastery.ObjectS<{
14
- tavily: z<string, string>;
15
- exa: z<string, string>;
16
- openaiApiKey: z<string, string>;
17
- openaiBaseUrl: z<string, string>;
18
- openaiModel: z<string, string>;
19
- }>, Schemastery.ObjectT<{
20
- tavily: z<string, string>;
21
- exa: z<string, string>;
22
- openaiApiKey: z<string, string>;
23
- openaiBaseUrl: z<string, string>;
24
- openaiModel: z<string, string>;
25
- }>>;
26
- }>, Schemastery.ObjectT<{
27
- scriptPath: z<string, string>;
28
- envFile: z<string, string>;
29
- python: z<string, string>;
30
- priority: z<("exa" | "openai-chat" | "tavily")[], ("exa" | "openai-chat" | "tavily")[]>;
31
- timeoutMs: z<number, number>;
32
- keys: z<Schemastery.ObjectS<{
33
- tavily: z<string, string>;
34
- exa: z<string, string>;
35
- openaiApiKey: z<string, string>;
36
- openaiBaseUrl: z<string, string>;
37
- openaiModel: z<string, string>;
38
- }>, Schemastery.ObjectT<{
39
- tavily: z<string, string>;
40
- exa: z<string, string>;
41
- openaiApiKey: z<string, string>;
42
- openaiBaseUrl: z<string, string>;
43
- openaiModel: z<string, string>;
44
- }>>;
45
- }>>;
46
- type WebSearchServicesConfig = Schemastery.TypeT<typeof Config>;
8
+ declare const Config: z<Schemastery.ObjectS<NoInfer<{
9
+ scriptPath: z<string, string, "volatile-defined">;
10
+ envFile: z<string, string, "volatile-defined">;
11
+ python: z<string, string, "volatile-defined">;
12
+ priority: z<NoInfer<("exa" | "openai-chat" | "tavily")[]>, NoInfer<("exa" | "openai-chat" | "tavily")[]>, "volatile-defined">;
13
+ timeoutMs: z<number, number, "volatile-defined">;
14
+ keys: z<NoInfer<Schemastery.ObjectS<NoInfer<{
15
+ tavily: z<string, string, "defined">;
16
+ exa: z<string, string, "defined">;
17
+ openaiApiKey: z<string, string, "defined">;
18
+ openaiBaseUrl: z<string, string, "defined">;
19
+ openaiModel: z<string, string, "defined">;
20
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
21
+ tavily: z<string, string, "defined">;
22
+ exa: z<string, string, "defined">;
23
+ openaiApiKey: z<string, string, "defined">;
24
+ openaiBaseUrl: z<string, string, "defined">;
25
+ openaiModel: z<string, string, "defined">;
26
+ }>>>, "volatile-defined">;
27
+ }>>, Schemastery.ObjectT<NoInfer<{
28
+ scriptPath: z<string, string, "volatile-defined">;
29
+ envFile: z<string, string, "volatile-defined">;
30
+ python: z<string, string, "volatile-defined">;
31
+ priority: z<NoInfer<("exa" | "openai-chat" | "tavily")[]>, NoInfer<("exa" | "openai-chat" | "tavily")[]>, "volatile-defined">;
32
+ timeoutMs: z<number, number, "volatile-defined">;
33
+ keys: z<NoInfer<Schemastery.ObjectS<NoInfer<{
34
+ tavily: z<string, string, "defined">;
35
+ exa: z<string, string, "defined">;
36
+ openaiApiKey: z<string, string, "defined">;
37
+ openaiBaseUrl: z<string, string, "defined">;
38
+ openaiModel: z<string, string, "defined">;
39
+ }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
40
+ tavily: z<string, string, "defined">;
41
+ exa: z<string, string, "defined">;
42
+ openaiApiKey: z<string, string, "defined">;
43
+ openaiBaseUrl: z<string, string, "defined">;
44
+ openaiModel: z<string, string, "defined">;
45
+ }>>>, "volatile-defined">;
46
+ }>>, "plain">;
47
+ /** 活动字段形态(0.1.7 loader 解析产物:volatile 字段为活动引用)。 */
48
+ type WebSearchServicesConfigFields = Schemastery.TypeT<typeof Config>;
49
+ /** 平面配置形态(消费面的读取形态,由活动引用解包得到)。 */
50
+ type WebSearchServicesConfig = UnwrapVolatile<WebSearchServicesConfigFields>;
47
51
  //#endregion
48
52
  //#region src/runner.d.ts
49
53
  type SpawnFn = typeof spawn;
@@ -67,6 +71,6 @@ declare class SearchServicesProvider implements WebSearchProvider {
67
71
  declare const name = "dsh-plus-web-search-services";
68
72
  /** 行级 inject:dsh-web 服务激活后才应用本行(provider 注册即需 ctx.web)。 */
69
73
  declare const inject: readonly ['web'];
70
- declare function apply(ctx: Context, config: WebSearchServicesConfig): void;
74
+ declare function apply(ctx: Context, config: WebSearchServicesConfig | WebSearchServicesConfigFields): void;
71
75
  //#endregion
72
76
  export { Config, PROVIDER_ID, SETTINGS_NS, SearchServicesProvider, apply, inject, name };
package/lib/index.js CHANGED
@@ -1,55 +1,11 @@
1
- import z from "@deepseek-ai/schemastery";
1
+ import { unwrapVolatile } from "@dsh-plus/shared";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { WebError } from "@deepseek-ai/dsh-web";
5
5
  import { homedir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { spawn } from "node:child_process";
8
- //#endregion
9
- //#region src/config.ts
10
- /**
11
- * 配置单一事实源:cordis 行级 Config(组合默认值,patch 层可覆盖)与 settings
12
- * namespace(用户层 $DSH_HOME/settings.yaml,热生效)共用同一 schemastery schema。
13
- *
14
- * 密钥主来源是 search-services skill 的 env 文件(envFile,默认
15
- * ~/.config/search-services/env),保持 skill 单事实源、零搬迁。行级 keys 仅为
16
- * 覆盖逃生舱;按存储规范 role('secret') 字段只允许行级注入,settings 用户层勿填。
17
- * @module web-search-services/config
18
- */
19
- const SETTINGS_NS = "dsh-plus-web-search-services";
20
- const DEFAULT_ENV_FILE = "~/.config/search-services/env";
21
- const DEFAULT_TIMEOUT_MS = 55e3;
22
- const DEFAULT_PRIORITY = [
23
- "tavily",
24
- "exa",
25
- "openai-chat"
26
- ];
27
- const PRIORITY_ENTRY = z.union([
28
- "tavily",
29
- "exa",
30
- "openai-chat"
31
- ]);
32
- const KeysSchema = z.object({
33
- tavily: z.string().role("secret").description("Tavily API key(覆盖 envFile 的 TAVILY_API_KEY;行级注入专用)").default(""),
34
- exa: z.string().role("secret").description("Exa API key(覆盖 envFile 的 EXA_API_KEY;行级注入专用)").default(""),
35
- openaiApiKey: z.string().role("secret").description("OpenAI-compatible API key(覆盖 SEARCH_OPENAI_API_KEY;行级注入专用)").default(""),
36
- openaiBaseUrl: z.string().description("OpenAI-compatible base URL(覆盖 SEARCH_OPENAI_BASE_URL)").default(""),
37
- openaiModel: z.string().description("OpenAI-compatible 搜索模型名(覆盖 SEARCH_OPENAI_MODEL)").default("")
38
- });
39
- const Config = z.object({
40
- scriptPath: z.string().description("search.py 路径(支持 ~ 展开);空串 = 使用随包内置脚本副本").default(""),
41
- envFile: z.string().description("skill 密钥 env 文件路径(支持 ~ 展开);空串表示不读 envFile").default(DEFAULT_ENV_FILE),
42
- python: z.string().description("python 可执行文件").default("python3"),
43
- priority: z.array(PRIORITY_ENTRY).description("后端优先级:靠前的先尝试,失败自动回退到下一个").default([...DEFAULT_PRIORITY]),
44
- timeoutMs: z.natural().min(1e3).description("单次搜索子进程超时(毫秒),应低于 dsh-tool-web 的 searchTimeoutMs").default(DEFAULT_TIMEOUT_MS),
45
- keys: KeysSchema.default({
46
- tavily: "",
47
- exa: "",
48
- openaiApiKey: "",
49
- openaiBaseUrl: "",
50
- openaiModel: ""
51
- })
52
- });
8
+ import z from "@deepseek-ai/schemastery";
53
9
  /** 由 web 请求 + 插件配置生成 search.py 的 argv(不含解释器与脚本路径)。 */
54
10
  function buildSearchArgv(cfg, request) {
55
11
  const query = request.query.trim();
@@ -118,6 +74,12 @@ function asNonEmptyString(value) {
118
74
  function asArray(value) {
119
75
  return Array.isArray(value) ? value : [];
120
76
  }
77
+ /** snippet 截断上限:官方 deepseek provider 的摘录是短引文;Tavily content /
78
+ * Exa highlights 原始值可达整页文本,直接透传会撑爆工具结果上下文。 */
79
+ const SNIPPET_MAX_CHARS = 500;
80
+ function truncateSnippet(text) {
81
+ return text.length <= SNIPPET_MAX_CHARS ? text : `${text.slice(0, SNIPPET_MAX_CHARS)}…`;
82
+ }
121
83
  function makeSource(url, title, snippet, publishedAt) {
122
84
  return {
123
85
  url,
@@ -157,8 +119,9 @@ function normalizeTavily(raw) {
157
119
  const payload = payloadOf(raw);
158
120
  const sources = sourcesFromResults(payload.results, (item) => {
159
121
  const url = asNonEmptyString(item.url);
122
+ const content = asNonEmptyString(item.content);
160
123
  if (url === void 0) return void 0;
161
- return makeSource(url, asNonEmptyString(item.title), asNonEmptyString(item.content), asNonEmptyString(item.published_date));
124
+ return makeSource(url, asNonEmptyString(item.title), content !== void 0 ? truncateSnippet(content) : void 0, asNonEmptyString(item.published_date));
162
125
  });
163
126
  return finish(asNonEmptyString(payload.answer), sources);
164
127
  }
@@ -167,7 +130,7 @@ function normalizeExa(raw) {
167
130
  const url = asNonEmptyString(item.url) ?? asNonEmptyString(item.id);
168
131
  if (url === void 0) return void 0;
169
132
  const highlights = asArray(item.highlights).map((h) => asNonEmptyString(h)).filter((h) => h !== void 0);
170
- return makeSource(url, asNonEmptyString(item.title), highlights.length > 0 ? highlights.join(" ") : void 0, asNonEmptyString(item.publishedDate));
133
+ return makeSource(url, asNonEmptyString(item.title), highlights.length > 0 ? truncateSnippet(highlights.join(" ")) : void 0, asNonEmptyString(item.publishedDate));
171
134
  }));
172
135
  }
173
136
  const MARKDOWN_LINK = /\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g;
@@ -406,29 +369,58 @@ var SearchServicesProvider = class {
406
369
  }
407
370
  };
408
371
  //#endregion
372
+ //#region src/config.ts
373
+ /**
374
+ * 配置单一事实源:cordis 行级 Config(组合默认值,patch 层可覆盖)与 settings
375
+ * namespace(用户层 $DSH_HOME/settings.yaml,热生效)共用同一 schemastery schema。
376
+ *
377
+ * 密钥主来源是 search-services skill 的 env 文件(envFile,默认
378
+ * ~/.config/search-services/env),保持 skill 单事实源、零搬迁。行级 keys 仅为
379
+ * 覆盖逃生舱;按存储规范 role('secret') 字段只允许行级注入,settings 用户层勿填。
380
+ * @module web-search-services/config
381
+ */
382
+ const SETTINGS_NS = "dsh-plus-web-search-services";
383
+ const DEFAULT_ENV_FILE = "~/.config/search-services/env";
384
+ const DEFAULT_TIMEOUT_MS = 55e3;
385
+ const DEFAULT_PRIORITY = [
386
+ "tavily",
387
+ "exa",
388
+ "openai-chat"
389
+ ];
390
+ const PRIORITY_ENTRY = z.union([
391
+ "tavily",
392
+ "exa",
393
+ "openai-chat"
394
+ ]);
395
+ const KeysSchema = z.object({
396
+ tavily: z.string().role("secret").description("Tavily API key(覆盖 envFile 的 TAVILY_API_KEY;行级注入专用)").default(""),
397
+ exa: z.string().role("secret").description("Exa API key(覆盖 envFile 的 EXA_API_KEY;行级注入专用)").default(""),
398
+ openaiApiKey: z.string().role("secret").description("OpenAI-compatible API key(覆盖 SEARCH_OPENAI_API_KEY;行级注入专用)").default(""),
399
+ openaiBaseUrl: z.string().description("OpenAI-compatible base URL(覆盖 SEARCH_OPENAI_BASE_URL)").default(""),
400
+ openaiModel: z.string().description("OpenAI-compatible 搜索模型名(覆盖 SEARCH_OPENAI_MODEL)").default("")
401
+ });
402
+ const Config = z.object({
403
+ scriptPath: z.string().description("search.py 路径(支持 ~ 展开);空串 = 使用随包内置脚本副本").default("").volatile(),
404
+ envFile: z.string().description("skill 密钥 env 文件路径(支持 ~ 展开);空串表示不读 envFile").default(DEFAULT_ENV_FILE).volatile(),
405
+ python: z.string().description("python 可执行文件").default("python3").volatile(),
406
+ priority: z.array(PRIORITY_ENTRY).description("后端优先级:靠前的先尝试,失败自动回退到下一个").default([...DEFAULT_PRIORITY]).volatile(),
407
+ timeoutMs: z.natural().min(1e3).description("单次搜索子进程超时(毫秒),应低于 dsh-tool-web 的 searchTimeoutMs").default(DEFAULT_TIMEOUT_MS).volatile(),
408
+ keys: KeysSchema.default({
409
+ tavily: "",
410
+ exa: "",
411
+ openaiApiKey: "",
412
+ openaiBaseUrl: "",
413
+ openaiModel: ""
414
+ }).volatile()
415
+ });
416
+ //#endregion
409
417
  //#region src/index.ts
410
418
  const name = "dsh-plus-web-search-services";
411
419
  /** 行级 inject:dsh-web 服务激活后才应用本行(provider 注册即需 ctx.web)。 */
412
420
  const inject = ["web"];
413
421
  function apply(ctx, config) {
414
- const provider = new SearchServicesProvider(configSource(ctx, config));
422
+ const provider = new SearchServicesProvider(() => unwrapVolatile(config));
415
423
  ctx.effect(() => ctx.web.registerSearchProvider(provider), "dsh-plus-web-search-services: register search provider");
416
424
  }
417
- /**
418
- * 配置源:settings 用户层($DSH_HOME/settings.yaml,热生效)在时优先生效,
419
- * 缺席/detach 回落 cordis 行级 config(官方 installSection 可选消费范式)。
420
- */
421
- function configSource(ctx, rowConfig) {
422
- let current = () => rowConfig;
423
- ctx.inject(["settings"], (settingsCtx) => {
424
- settingsCtx.settings.installSection(ctx, SETTINGS_NS, Config, rowConfig, {
425
- setSource: (source) => {
426
- current = source;
427
- },
428
- onChange: () => {}
429
- });
430
- });
431
- return () => current();
432
- }
433
425
  //#endregion
434
426
  export { Config, PROVIDER_ID, SETTINGS_NS, SearchServicesProvider, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/web-search-services",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "dsh-plus service plugin: web_search 免费后端聚合——经官方 ctx.web provider 扩展点桥接 search-services skill(Tavily/Exa/OpenAI Chat 免费额度,优先级+自动回退+key 配置),官方 web_search 工具与输出格式零修改",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -18,17 +18,20 @@
18
18
  "src",
19
19
  "scripts"
20
20
  ],
21
+ "dependencies": {
22
+ "@dsh-plus/shared": "0.1.22"
23
+ },
21
24
  "peerDependencies": {
22
- "@deepseek-ai/cordis": "^4.0.2",
23
- "@deepseek-ai/dsh-settings": "^0.1.6-alpha.2",
24
- "@deepseek-ai/dsh-web": "^0.1.6-alpha.2",
25
- "@deepseek-ai/schemastery": "^3.18.2"
25
+ "@deepseek-ai/cordis": "^4.0.4",
26
+ "@deepseek-ai/dsh-settings": "^0.1.7-alpha.2",
27
+ "@deepseek-ai/dsh-web": "^0.1.7-alpha.2",
28
+ "@deepseek-ai/schemastery": "^3.18.4"
26
29
  },
27
30
  "devDependencies": {
28
- "@deepseek-ai/cordis": "4.0.2",
29
- "@deepseek-ai/dsh-settings": "0.1.6-alpha.2",
30
- "@deepseek-ai/dsh-web": "0.1.6-alpha.2",
31
- "@deepseek-ai/schemastery": "3.18.2"
31
+ "@deepseek-ai/cordis": "4.0.4",
32
+ "@deepseek-ai/dsh-settings": "0.1.7-alpha.2",
33
+ "@deepseek-ai/dsh-web": "0.1.7-alpha.2",
34
+ "@deepseek-ai/schemastery": "3.18.4"
32
35
  },
33
36
  "publishConfig": {
34
37
  "access": "public"
package/src/config.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * @module web-search-services/config
9
9
  */
10
10
  import z from '@deepseek-ai/schemastery'
11
+ import type { UnwrapVolatile } from '@dsh-plus/shared'
11
12
 
12
13
  import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
13
14
 
@@ -49,33 +50,43 @@ const KeysSchema = z.object({
49
50
  .default(''),
50
51
  })
51
52
 
53
+ // 0.1.7:全字段 `.volatile()`——条目进入 settings describe 视图(卡片可读写;
54
+ // keys 内 role('secret') 字段由 describe/write 层做密文遮蔽,用户层勿填),
55
+ // loader 原位提交活动引用,current() 现取即热。
52
56
  export const Config = z.object({
53
57
  scriptPath: z
54
58
  .string()
55
59
  .description('search.py 路径(支持 ~ 展开);空串 = 使用随包内置脚本副本')
56
- .default(DEFAULT_SCRIPT_PATH),
60
+ .default(DEFAULT_SCRIPT_PATH)
61
+ .volatile(),
57
62
  envFile: z
58
63
  .string()
59
64
  .description('skill 密钥 env 文件路径(支持 ~ 展开);空串表示不读 envFile')
60
- .default(DEFAULT_ENV_FILE),
61
- python: z.string().description('python 可执行文件').default('python3'),
65
+ .default(DEFAULT_ENV_FILE)
66
+ .volatile(),
67
+ python: z.string().description('python 可执行文件').default('python3').volatile(),
62
68
  priority: z
63
69
  .array(PRIORITY_ENTRY)
64
70
  .description('后端优先级:靠前的先尝试,失败自动回退到下一个')
65
- .default([...DEFAULT_PRIORITY]),
71
+ .default([...DEFAULT_PRIORITY])
72
+ .volatile(),
66
73
  timeoutMs: z
67
74
  .natural()
68
75
  .min(1000)
69
76
  .description('单次搜索子进程超时(毫秒),应低于 dsh-tool-web 的 searchTimeoutMs')
70
- .default(DEFAULT_TIMEOUT_MS),
77
+ .default(DEFAULT_TIMEOUT_MS)
78
+ .volatile(),
71
79
  keys: KeysSchema.default({
72
80
  tavily: '',
73
81
  exa: '',
74
82
  openaiApiKey: '',
75
83
  openaiBaseUrl: '',
76
84
  openaiModel: '',
77
- }),
85
+ }).volatile(),
78
86
  })
79
87
 
80
- export type WebSearchServicesConfig = Schemastery.TypeT<typeof Config>
88
+ /** 活动字段形态(0.1.7 loader 解析产物:volatile 字段为活动引用)。 */
89
+ export type WebSearchServicesConfigFields = Schemastery.TypeT<typeof Config>
90
+ /** 平面配置形态(消费面的读取形态,由活动引用解包得到)。 */
91
+ export type WebSearchServicesConfig = UnwrapVolatile<WebSearchServicesConfigFields>
81
92
  export type KeysConfig = WebSearchServicesConfig['keys']
package/src/index.ts CHANGED
@@ -10,8 +10,9 @@
10
10
  import type { Context } from '@deepseek-ai/cordis'
11
11
  import type {} from '@deepseek-ai/dsh-settings'
12
12
  import type {} from '@deepseek-ai/dsh-web'
13
+ import { unwrapVolatile } from '@dsh-plus/shared'
13
14
 
14
- import { Config, SETTINGS_NS, type WebSearchServicesConfig } from './config.ts'
15
+ import type { WebSearchServicesConfig, WebSearchServicesConfigFields } from './config.ts'
15
16
  import { SearchServicesProvider } from './provider.ts'
16
17
 
17
18
  export const name = 'dsh-plus-web-search-services'
@@ -19,33 +20,18 @@ export const name = 'dsh-plus-web-search-services'
19
20
  /** 行级 inject:dsh-web 服务激活后才应用本行(provider 注册即需 ctx.web)。 */
20
21
  export const inject = ['web'] as const
21
22
 
23
+ export { Config, SETTINGS_NS } from './config.ts'
22
24
  export { PROVIDER_ID, SearchServicesProvider } from './provider.ts'
23
- export { Config, SETTINGS_NS }
24
25
 
25
- export function apply(ctx: Context, config: WebSearchServicesConfig): void {
26
- const provider = new SearchServicesProvider(configSource(ctx, config))
26
+ // config 运行期为 loader 解析的 volatile 活动字段(用户层 override 并入行级
27
+ // config,写入原位提交)——每次读现取平面快照,替代 0.1.6 installSection/setSource。
28
+ export function apply(
29
+ ctx: Context,
30
+ config: WebSearchServicesConfig | WebSearchServicesConfigFields,
31
+ ): void {
32
+ const provider = new SearchServicesProvider(() => unwrapVolatile(config))
27
33
  ctx.effect(
28
34
  () => ctx.web.registerSearchProvider(provider),
29
35
  'dsh-plus-web-search-services: register search provider',
30
36
  )
31
37
  }
32
-
33
- /**
34
- * 配置源:settings 用户层($DSH_HOME/settings.yaml,热生效)在时优先生效,
35
- * 缺席/detach 回落 cordis 行级 config(官方 installSection 可选消费范式)。
36
- */
37
- function configSource(
38
- ctx: Context,
39
- rowConfig: WebSearchServicesConfig,
40
- ): () => WebSearchServicesConfig {
41
- let current: () => WebSearchServicesConfig = () => rowConfig
42
- ctx.inject(['settings'], (settingsCtx) => {
43
- settingsCtx.settings.installSection(ctx, SETTINGS_NS, Config, rowConfig, {
44
- setSource: (source) => {
45
- current = source
46
- },
47
- onChange: () => {},
48
- })
49
- })
50
- return () => current()
51
- }
package/src/normalize.ts CHANGED
@@ -25,6 +25,14 @@ function asArray(value: unknown): unknown[] {
25
25
  return Array.isArray(value) ? value : []
26
26
  }
27
27
 
28
+ /** snippet 截断上限:官方 deepseek provider 的摘录是短引文;Tavily content /
29
+ * Exa highlights 原始值可达整页文本,直接透传会撑爆工具结果上下文。 */
30
+ const SNIPPET_MAX_CHARS = 500
31
+
32
+ export function truncateSnippet(text: string): string {
33
+ return text.length <= SNIPPET_MAX_CHARS ? text : `${text.slice(0, SNIPPET_MAX_CHARS)}…`
34
+ }
35
+
28
36
  function makeSource(
29
37
  url: string,
30
38
  title?: string,
@@ -76,11 +84,12 @@ function normalizeTavily(raw: JsonObject): WebSearchResult {
76
84
  const payload = payloadOf(raw)
77
85
  const sources = sourcesFromResults(payload.results, (item) => {
78
86
  const url = asNonEmptyString(item.url)
87
+ const content = asNonEmptyString(item.content)
79
88
  if (url === undefined) return undefined
80
89
  return makeSource(
81
90
  url,
82
91
  asNonEmptyString(item.title),
83
- asNonEmptyString(item.content),
92
+ content !== undefined ? truncateSnippet(content) : undefined,
84
93
  asNonEmptyString(item.published_date),
85
94
  )
86
95
  })
@@ -98,7 +107,7 @@ function normalizeExa(raw: JsonObject): WebSearchResult {
98
107
  return makeSource(
99
108
  url,
100
109
  asNonEmptyString(item.title),
101
- highlights.length > 0 ? highlights.join(' ') : undefined,
110
+ highlights.length > 0 ? truncateSnippet(highlights.join(' ')) : undefined,
102
111
  asNonEmptyString(item.publishedDate),
103
112
  )
104
113
  })