From f070597378d2c211ddf160e24258402d633fd718 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 30 Jul 2026 21:55:22 +0800 Subject: [PATCH] fix(tool-web): share one source projection between search execute and meta The web_search execute result and searchMetaFromValue each spread the same {url, title?, snippet?, publishedAt?} projection over a seam source, which the duplication gate flags as a clone. Extract projectSource, typed on the seam's WebSearchSource, so both sites carry a byte-identical shape from one definition. --- packages/web/tool-web/src/search.ts | 38 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/web/tool-web/src/search.ts b/packages/web/tool-web/src/search.ts index 95016af29f..20979e4035 100644 --- a/packages/web/tool-web/src/search.ts +++ b/packages/web/tool-web/src/search.ts @@ -8,7 +8,7 @@ import type { Context } from 'cordis' import { defineTool } from '@deepseek-ai/dsh-tools' import type { GenericCallView, JsonValue, ToolResult, WebSearchResultView, WebSource } from '@deepseek-ai/dsh-tools' -import type { WebSearchResult } from '@deepseek-ai/dsh-web' +import type { WebSearchResult, WebSearchSource } from '@deepseek-ai/dsh-web' import type {} from '@deepseek-ai/dsh-system-prompt' /** @@ -101,6 +101,28 @@ export interface WebSearchMeta { answer?: string } +/** + * Project one seam source into a plain object that omits every absent optional + * field. Shared by the canonical `execute` result and its replayable + * presentation meta so both carry byte-identical source shapes. + * + * @param source - one source from the `ctx.web` search outcome. + * @returns `{ url }` plus each present optional field. + */ +function projectSource(source: WebSearchSource): { + url: string + title?: string + snippet?: string + publishedAt?: string +} { + return { + url: source.url, + ...source.title !== undefined ? { title: source.title } : {}, + ...source.snippet !== undefined ? { snippet: source.snippet } : {}, + ...source.publishedAt !== undefined ? { publishedAt: source.publishedAt } : {}, + } +} + /** * Project a validated `web_search` output value into its replayable * presentation meta ({@link WebSearchMeta} as opaque JSON). @@ -110,12 +132,7 @@ export interface WebSearchMeta { */ export function searchMetaFromValue(value: WebSearchResult): JsonValue { return { - sources: value.sources.map(source => ({ - url: source.url, - ...source.title !== undefined ? { title: source.title } : {}, - ...source.snippet !== undefined ? { snippet: source.snippet } : {}, - ...source.publishedAt !== undefined ? { publishedAt: source.publishedAt } : {}, - })), + sources: value.sources.map(projectSource), truncated: value.truncated, ...value.content !== undefined ? { answer: value.content } : {}, } @@ -238,12 +255,7 @@ export function applyWebSearchTool(ctx: Context, maxResults: number, timeoutMs: ) return { ...result.content !== undefined ? { content: result.content } : {}, - sources: result.sources.map(source => ({ - url: source.url, - ...source.title !== undefined ? { title: source.title } : {}, - ...source.snippet !== undefined ? { snippet: source.snippet } : {}, - ...source.publishedAt !== undefined ? { publishedAt: source.publishedAt } : {}, - })), + sources: result.sources.map(projectSource), truncated: result.truncated, } },