Z ZPI 文档 v0.3.8
介绍页

源码

packages/ui/src/components/web-tool-data.ts

typescript104 行查看原文
import type { WebSearchSource, WebToolDetails } from "../types.ts";

export const webToolLimits = {
  content: 64 * 1024,
  results: 50,
  query: 2000,
  url: 8192,
  title: 512,
  snippet: 4000,
  summary: 4000,
  message: 2000,
} as const;

/** Never route service-controlled destinations through the local-file link resolver. */
export function safeWebUrl(value: unknown): string | undefined {
  if (
    typeof value !== "string" ||
    Array.from(value).length > webToolLimits.url ||
    Array.from(value).some(
      (character) => character.charCodeAt(0) <= 32 || character === "\u007f" || character === "\\",
    ) ||
    !/^https?:\/\//i.test(value)
  )
    return undefined;
  try {
    const url = new URL(value);
    if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password)
      return undefined;
    return value;
  } catch {
    return undefined;
  }
}

const record = (value: unknown): value is Record<string, unknown> =>
  !!value && typeof value === "object" && !Array.isArray(value);

/** Copy only the bounded, typed data that the UI understands, including restored history. */
export function normalizeWebToolDetails(value: unknown, toolName?: string): WebToolDetails | undefined {
  if (
    !record(value) ||
    (value.kind !== "web_search" && value.kind !== "web_fetch") ||
    (toolName !== undefined && value.kind !== toolName) ||
    typeof value.status !== "string" ||
    !["pending", "success", "error", "cancelled"].includes(value.status)
  )
    return undefined;
  let truncated = value.truncated === true;
  const text = (input: unknown, limit: number): string | undefined => {
    if (typeof input !== "string") return undefined;
    if (input.length > limit) truncated = true;
    return input.slice(0, limit);
  };
  const result: WebToolDetails = {
    kind: value.kind,
    status: value.status as WebToolDetails["status"],
  };
  for (const key of ["query", "title", "content", "message"] as const) {
    const field = text(value[key], webToolLimits[key]);
    if (field !== undefined) result[key] = field;
  }
  const url = safeWebUrl(value.url);
  if (url) result.url = url;
  if (Array.isArray(value.results)) {
    if (value.results.length > webToolLimits.results) truncated = true;
    const sources: WebSearchSource[] = [];
    for (const source of value.results.slice(0, webToolLimits.results)) {
      if (!record(source)) continue;
      const url = safeWebUrl(source.url);
      const title = text(source.title, webToolLimits.title);
      const snippet = text(source.snippet, webToolLimits.snippet);
      if (!url || title === undefined || snippet === undefined) continue;
      const summary = text(source.summary, webToolLimits.summary);
      sources.push({
        url,
        title,
        snippet,
        ...(summary !== undefined ? { summary } : {}),
        site_name: text(source.site_name, 256) ?? null,
        site_icon: safeWebUrl(source.site_icon) ?? null,
        date: text(source.date, 128) ?? null,
      });
    }
    result.results = sources;
  }
  if (truncated) result.truncated = true;
  return result;
}

/** SDK preflight cancellations happen before the web tool can emit structured details. */
export function cancelledWebTool(
  name: string,
  output: string,
  isError?: boolean,
): WebToolDetails | undefined {
  if (
    (name === "web_search" || name === "web_fetch") &&
    isError &&
    /^Tool cancelled before execution(?:; effects unknown)?$/.test(output)
  )
    return { kind: name, status: "cancelled", message: "联网工具已取消。" };
  return undefined;
}