qwen-code/packages/webui/src/components/toolcalls/WebFetchToolCall.tsx
顾盼 aeeb2976d6
feat(web-search): remove built-in web_search tool, replace with MCP-based approach (#3502)
* feat(web-search): add GLM (ZhipuAI) web search provider

- Add GlmProvider class implementing BaseWebSearchProvider using the
  ZhipuAI Web Search API (https://open.bigmodel.cn/api/paas/v4/web_search)
- Support multiple search engines: search_std, search_pro, search_pro_sogou,
  search_pro_quark
- Support optional config: maxResults, searchIntent, searchRecencyFilter,
  contentSize, searchDomainFilter
- Truncate query to 70 characters per API limit
- Register 'glm' in the provider discriminated union (types.ts) and
  createProvider() switch (index.ts)
- Add GlmProviderConfig to settingsSchema, ConfigParams, and Config class
- Add --glm-api-key CLI flag and GLM_API_KEY env var support in webSearch.ts
- Forward GLM_API_KEY in sandbox environment
- Update provider priority list: Tavily > Google > GLM > DashScope
- Add 17 unit tests for GlmProvider and 4 integration tests in index.test.ts
- Update docs/developers/tools/web-search.md with GLM configuration,
  env vars, CLI args, pricing, and corrected DashScope billing info
- Fix stale OAuth/free-tier references in web-search.md

Closes #3496

* docs(web-search): fix DashScope note and add GLM server-side limitations

* fix(web-search): make DashScope provider work with standard API key, remove qwen-oauth dependency

- DashScopeProvider.isAvailable() now checks config.apiKey instead of authType
- Remove OAuth credential file reading and resource_url requirement
- Use standard DashScope endpoint: dashscope.aliyuncs.com/api/v1/indices/plugin/web_search
- Read DASHSCOPE_API_KEY env var and --dashscope-api-key CLI flag
- Forward DASHSCOPE_API_KEY into sandbox environment
- Update integration test to detect DASHSCOPE_API_KEY
- Update docs to reflect new API key based configuration

* feat(web-search): remove built-in web search tool

The web_search tool and all related provider implementations are removed.
Web search functionality will be provided via MCP integrations instead,
which is the direction the broader agent ecosystem is moving.

Removed:
- packages/core/src/tools/web-search/ (entire directory)
- packages/cli/src/config/webSearch.ts
- integration-tests/cli/web_search.test.ts
- ToolNames.WEB_SEARCH, ToolErrorCode.WEB_SEARCH_FAILED
- webSearch config in ConfigParams, Config class, settingsSchema
- CLI options: --tavily-api-key, --google-api-key, --google-search-engine-id,
  --glm-api-key, --dashscope-api-key, --web-search-default
- Sandbox env forwarding for TAVILY/GLM/DASHSCOPE/GOOGLE search keys
- web_search from rule-parser, permission-manager, speculation gate,
  microcompact tool set, and builtin-agents tool list

* fix: remove websearch reference

* docs: remove websearch tool

* docs: add break change guide

* fix review
2026-04-24 11:29:02 +08:00

196 lines
5.8 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* WebFetch/WebSearch tool call component
* Displays web fetch and search operations with URL/query and output
*/
import { useState, type FC } from 'react';
import {
ToolCallContainer,
safeTitle,
groupContent,
mapToolStatusToContainerStatus,
} from './shared/index.js';
import type { BaseToolCallProps } from './shared/index.js';
import { getToolDisplayLabel } from './labelUtils.js';
import { MarkdownRenderer } from '../messages/MarkdownRenderer/MarkdownRenderer.js';
type WebVariant = 'fetch' | 'search';
/** Default collapsed height in pixels */
const COLLAPSED_HEIGHT = 120;
/** Threshold for showing expand button (content longer than this will be collapsible) */
const EXPAND_THRESHOLD = 300;
/**
* Get the URL or query from tool call data
* @param variant - 'fetch' or 'search'
* @param title - Tool call title
* @param rawInput - Raw input object
* @returns URL or query string
*/
const getWebTarget = (
variant: WebVariant,
title: unknown,
rawInput?: unknown,
): string => {
// Try to extract URL or query from rawInput
if (rawInput && typeof rawInput === 'object') {
const input = rawInput as Record<string, unknown>;
if (variant === 'fetch' && input['url']) {
return String(input['url']);
}
if (variant === 'search' && input['query']) {
return String(input['query']);
}
}
return safeTitle(title);
};
/**
* Output card component with expand/collapse functionality
* @param props - Component props
* @returns JSX element
*/
const OutputCard: FC<{
content: string;
isError?: boolean;
}> = ({ content, isError = false }) => {
const [isExpanded, setIsExpanded] = useState(false);
const isLongContent = content.length > EXPAND_THRESHOLD;
return (
<div className="border-[0.5px] border-[var(--app-input-border)] rounded-[5px] bg-[var(--app-tool-background)] my-2 max-w-full text-[1em] items-start">
<div className="flex flex-col gap-[3px] p-1">
<div className="grid grid-cols-[max-content_1fr] p-1">
<div className="text-[var(--app-secondary-foreground)] text-left opacity-50 py-1 px-2 pl-1 font-mono text-[0.85em]">
OUT
</div>
<div
className={`break-words m-0 p-1 overflow-hidden ${
isError ? 'whitespace-pre-wrap' : ''
}`}
style={
!isExpanded && isLongContent
? {
maxHeight: `${COLLAPSED_HEIGHT}px`,
maskImage: `linear-gradient(to bottom, var(--app-primary-background) 80px, transparent ${COLLAPSED_HEIGHT}px)`,
WebkitMaskImage: `linear-gradient(to bottom, var(--app-primary-background) 80px, transparent ${COLLAPSED_HEIGHT}px)`,
}
: undefined
}
>
{isError ? (
<pre className="m-0 overflow-hidden font-mono text-[0.85em] text-[#c74e39]">
{content}
</pre>
) : (
<div className="text-[0.85em]">
<MarkdownRenderer content={content} enableFileLinks={false} />
</div>
)}
</div>
</div>
{/* Expand/Collapse button */}
{isLongContent && (
<div className="flex justify-center border-t border-[var(--app-input-border)] pt-1">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="text-[var(--app-secondary-foreground)] text-[0.8em] hover:text-[var(--app-primary-foreground)] cursor-pointer bg-transparent border-none px-2 py-1 rounded hover:bg-[var(--app-input-background)] transition-colors"
>
{isExpanded ? '▲ Collapse' : '▼ Show more'}
</button>
</div>
)}
</div>
</div>
);
};
/**
* WebFetch/WebSearch tool call implementation
* @param props - Component props including toolCall, variant, isFirst, isLast
* @returns JSX element
*/
const WebFetchToolCallImpl: FC<BaseToolCallProps & { variant: WebVariant }> = ({
toolCall,
variant,
isFirst,
isLast,
}) => {
const { title, content, rawInput, toolCallId } = toolCall;
const webTarget = getWebTarget(variant, title, rawInput);
const label = getToolDisplayLabel({ kind: toolCall.kind, title });
// Group content by type
const { textOutputs, errors } = groupContent(content);
// Map tool status to container status
const containerStatus =
errors.length > 0
? 'error'
: mapToolStatusToContainerStatus(toolCall.status);
// Error case
if (errors.length > 0) {
return (
<ToolCallContainer
label={label}
status={containerStatus}
toolCallId={toolCallId}
isFirst={isFirst}
isLast={isLast}
labelSuffix={webTarget}
>
<OutputCard content={errors.join('\n')} isError />
</ToolCallContainer>
);
}
// Success with output
if (textOutputs.length > 0) {
const output = textOutputs.join('\n');
return (
<ToolCallContainer
label={label}
status={containerStatus}
toolCallId={toolCallId}
isFirst={isFirst}
isLast={isLast}
labelSuffix={webTarget}
>
<OutputCard content={output} />
</ToolCallContainer>
);
}
// No output yet - show just the URL/query (loading state)
return (
<ToolCallContainer
label={label}
status={containerStatus}
toolCallId={toolCallId}
isFirst={isFirst}
isLast={isLast}
labelSuffix={webTarget}
/>
);
};
/**
* WebFetchToolCall - displays web fetch/search tool calls
* Shows URL/query and output with OUT card
* @param props - Component props
* @returns JSX element
*/
export const WebFetchToolCall: FC<BaseToolCallProps> = (props) => (
<WebFetchToolCallImpl {...props} variant={'fetch'} />
);