release: bump version to 0.52.0
This commit is contained in:
@@ -3,12 +3,12 @@ import {
|
||||
ApiOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
EyeOutlined,
|
||||
PlayCircleOutlined,
|
||||
SyncOutlined,
|
||||
ToolOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
AutoComplete,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
@@ -21,17 +21,23 @@ import {
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import axios from 'axios'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||
import ConnectionTestInput from '../../components/ConnectionTestInput/ConnectionTestInput'
|
||||
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||||
import { PlaygroundWorkspace } from '../Playground/Playground'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const ANTHROPIC_MESSAGES_MAX_TOKENS = 1200
|
||||
const DEFAULT_PROVIDER_MAX_TOKENS = 4096
|
||||
const TOOL_OPTIONS = [
|
||||
{ value: 'web_search', label: 'WebSearch 证据层' },
|
||||
{ value: 'ocr', label: 'OCR 识别' },
|
||||
]
|
||||
|
||||
interface SecretStatus {
|
||||
configured: boolean
|
||||
@@ -115,6 +121,18 @@ interface ExternalIntegrations {
|
||||
scrape_formats: string[]
|
||||
source: string
|
||||
}
|
||||
ocr: {
|
||||
enabled: boolean
|
||||
provider: string
|
||||
base_url: string
|
||||
api_key: SecretStatus
|
||||
model: string
|
||||
languages: string[]
|
||||
timeout_seconds: number
|
||||
max_file_size_mb: number
|
||||
output_format: string
|
||||
source: string
|
||||
}
|
||||
}
|
||||
|
||||
interface AIProviderPreset {
|
||||
@@ -170,15 +188,20 @@ export default function AISettings() {
|
||||
const [refreshingAiPreset, setRefreshingAiPreset] = useState(false)
|
||||
const [testingAiProviderConnection, setTestingAiProviderConnection] = useState(false)
|
||||
const [testingWebSearchConnection, setTestingWebSearchConnection] = useState(false)
|
||||
const [selectedToolKey, setSelectedToolKey] = useState('web_search')
|
||||
const [feedback, setFeedback] = useState<{ type: 'success' | 'error'; message: string } | null>(null)
|
||||
const [revealedAiProviderSecrets, setRevealedAiProviderSecrets] = useState<Record<string, { api_key: string; service_token: string }>>({})
|
||||
const [revealedWebSearchSecrets, setRevealedWebSearchSecrets] = useState<Record<string, { api_key: string }>>({})
|
||||
const [revealedOcrSecrets, setRevealedOcrSecrets] = useState<{ api_key: string } | null>(null)
|
||||
const [aiProviderApiKeyRevealed, setAiProviderApiKeyRevealed] = useState(false)
|
||||
const [serviceTokenRevealed, setServiceTokenRevealed] = useState(false)
|
||||
const [webSearchApiKeyRevealed, setWebSearchApiKeyRevealed] = useState(false)
|
||||
const [ocrApiKeyRevealed, setOcrApiKeyRevealed] = useState(false)
|
||||
|
||||
const selectedAiProvider = Form.useWatch(['ai_provider', 'provider'], form)
|
||||
const selectedWebSearchProvider = Form.useWatch(['web_search', 'provider'], form)
|
||||
const webSearchEnabled = Form.useWatch(['web_search', 'enabled'], form)
|
||||
const ocrEnabled = Form.useWatch(['ocr', 'enabled'], form)
|
||||
const selectedAiProviderSecret = selectedAiProvider
|
||||
? integrations?.ai_provider.providers?.[selectedAiProvider]?.api_key || integrations?.ai_provider.api_key
|
||||
: integrations?.ai_provider.api_key
|
||||
@@ -255,6 +278,19 @@ export default function AISettings() {
|
||||
scrape_path: integrations.web_search.scrape_path,
|
||||
scrape_formats: integrations.web_search.scrape_formats,
|
||||
},
|
||||
ocr: {
|
||||
enabled: integrations.ocr.enabled,
|
||||
provider: integrations.ocr.provider,
|
||||
base_url: integrations.ocr.base_url,
|
||||
api_key: integrations.ocr.api_key.configured
|
||||
? integrations.ocr.api_key.preview
|
||||
: '',
|
||||
model: integrations.ocr.model,
|
||||
languages: integrations.ocr.languages,
|
||||
timeout_seconds: integrations.ocr.timeout_seconds,
|
||||
max_file_size_mb: integrations.ocr.max_file_size_mb,
|
||||
output_format: integrations.ocr.output_format,
|
||||
},
|
||||
})
|
||||
}, [form, integrations, loading])
|
||||
|
||||
@@ -313,9 +349,23 @@ export default function AISettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const buildOcrDraftPayload = (values: any) => {
|
||||
const nextApiKey = String(values.ocr?.api_key || '').trim()
|
||||
const apiKeyUnchanged = isSecretDraftUnchanged(
|
||||
nextApiKey,
|
||||
integrations?.ocr.api_key.preview,
|
||||
revealedOcrSecrets?.api_key,
|
||||
)
|
||||
return {
|
||||
...values.ocr,
|
||||
api_key: apiKeyUnchanged ? '' : nextApiKey,
|
||||
}
|
||||
}
|
||||
|
||||
const buildIntegrationsPayload = (values: any) => ({
|
||||
ai_provider: buildAiProviderDraftPayload(values),
|
||||
web_search: buildWebSearchDraftPayload(values),
|
||||
ocr: buildOcrDraftPayload(values),
|
||||
barentswatch: {
|
||||
endpoint: integrations?.barentswatch.endpoint || '',
|
||||
client_id: integrations?.barentswatch.client_id || '',
|
||||
@@ -366,6 +416,16 @@ export default function AISettings() {
|
||||
return secrets
|
||||
}
|
||||
|
||||
const revealOcrSecrets = async () => {
|
||||
if (revealedOcrSecrets) return revealedOcrSecrets
|
||||
const response = await axios.get('/api/v1/settings/integrations/ocr/secrets')
|
||||
const secrets = {
|
||||
api_key: String(response.data.api_key || ''),
|
||||
}
|
||||
setRevealedOcrSecrets(secrets)
|
||||
return secrets
|
||||
}
|
||||
|
||||
const handleAiProviderApiKeyVisibleChange = async (visible: boolean) => {
|
||||
const provider = String(form.getFieldValue(['ai_provider', 'provider']) || integrations?.ai_provider.provider || 'minimax')
|
||||
const providerSecret = integrations?.ai_provider.providers?.[provider]?.api_key || integrations?.ai_provider.api_key
|
||||
@@ -428,6 +488,25 @@ export default function AISettings() {
|
||||
setWebSearchApiKeyRevealed(false)
|
||||
}
|
||||
|
||||
const handleOcrApiKeyVisibleChange = async (visible: boolean) => {
|
||||
if (visible) {
|
||||
try {
|
||||
const secrets = await revealOcrSecrets()
|
||||
if (secrets.api_key) form.setFieldValue(['ocr', 'api_key'], secrets.api_key)
|
||||
setOcrApiKeyRevealed(true)
|
||||
} catch {
|
||||
message.error('读取 OCR API Key 失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
const currentValue = String(form.getFieldValue(['ocr', 'api_key']) || '')
|
||||
const revealedValue = revealedOcrSecrets?.api_key
|
||||
if (revealedValue && currentValue === revealedValue) {
|
||||
form.setFieldValue(['ocr', 'api_key'], integrations?.ocr.api_key.preview || '')
|
||||
}
|
||||
setOcrApiKeyRevealed(false)
|
||||
}
|
||||
|
||||
const applyAiProviderSelection = (provider: string, presetOverride?: AIProviderPreset) => {
|
||||
const preset = presetOverride || aiProviderPresets.find((item) => item.provider === provider)
|
||||
const savedProvider = integrations?.ai_provider.providers?.[provider]
|
||||
@@ -567,31 +646,40 @@ export default function AISettings() {
|
||||
<Form form={form} layout="vertical" onFinish={saveAISettings} onValuesChange={() => setFeedback(null)}>
|
||||
<Card size="small" title={<Space><ApiOutlined />LLM Provider</Space>}>
|
||||
<Form.Item name={['ai_provider', 'provider']} label="Provider">
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
<AutoComplete
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
options={aiProviderPresets.map((preset) => ({
|
||||
value: preset.provider,
|
||||
label: `${preset.label} · ${preset.provider_api}`,
|
||||
}))}
|
||||
onChange={(value) => applyAiProviderSelection(value)}
|
||||
onSelect={(value) => applyAiProviderSelection(value)}
|
||||
placeholder="选择或输入 provider id,例如 openai"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['ai_provider', 'base_url']} label="LLM Base URL">
|
||||
<ConnectionTestInput
|
||||
placeholder="https://api.example.com/v1"
|
||||
testing={testingAiProviderConnection}
|
||||
testDisabled={!selectedAiProvider}
|
||||
onTest={() => { void testAiProviderConnection() }}
|
||||
extraSuffix={(
|
||||
<Tooltip title="刷新当前 Provider 的模型配置">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<SyncOutlined spin={refreshingAiPreset} />}
|
||||
disabled={!selectedAiProvider}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void refreshSelectedAiProviderPreset()
|
||||
}}
|
||||
aria-label="刷新当前 Provider 的模型配置"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '0 12px', alignItems: 'end' }}>
|
||||
<Form.Item name={['ai_provider', 'base_url']} label="LLM Base URL">
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
<Form.Item label=" ">
|
||||
<Space>
|
||||
<Button icon={<SyncOutlined />} loading={refreshingAiPreset} onClick={refreshSelectedAiProviderPreset}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button icon={<PlayCircleOutlined />} loading={testingAiProviderConnection} onClick={() => { void testAiProviderConnection() }}>
|
||||
测试连接
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name={['ai_provider', 'provider_api']} label="协议适配">
|
||||
<Select>
|
||||
<Select.Option value="openai-completions">OpenAI Chat Completions</Select.Option>
|
||||
@@ -600,12 +688,12 @@ export default function AISettings() {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name={['ai_provider', 'model']} label="默认模型">
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
<AutoComplete
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
options={(
|
||||
aiProviderPresets.find((preset) => preset.provider === selectedAiProvider)?.models || []
|
||||
).map((model) => ({ value: model, label: model }))}
|
||||
placeholder="选择或输入模型名,例如 gpt-5.1"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="LLM API Key">
|
||||
@@ -621,13 +709,15 @@ export default function AISettings() {
|
||||
autoComplete="new-password"
|
||||
placeholder="输入新的 LLM API key"
|
||||
suffix={(
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={aiProviderApiKeyRevealed ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => { void handleAiProviderApiKeyVisibleChange(!aiProviderApiKeyRevealed) }}
|
||||
aria-label={aiProviderApiKeyRevealed ? '隐藏 LLM API key' : '显示 LLM API key'}
|
||||
/>
|
||||
<Tooltip title={aiProviderApiKeyRevealed ? '隐藏 LLM API key' : '显示 LLM API key'}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={aiProviderApiKeyRevealed ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => { void handleAiProviderApiKeyVisibleChange(!aiProviderApiKeyRevealed) }}
|
||||
aria-label={aiProviderApiKeyRevealed ? '隐藏 LLM API key' : '显示 LLM API key'}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -664,13 +754,15 @@ export default function AISettings() {
|
||||
autoComplete="new-password"
|
||||
placeholder="输入新的代理 token"
|
||||
suffix={(
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={serviceTokenRevealed ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => { void handleServiceTokenVisibleChange(!serviceTokenRevealed) }}
|
||||
aria-label={serviceTokenRevealed ? '隐藏代理 token' : '显示代理 token'}
|
||||
/>
|
||||
<Tooltip title={serviceTokenRevealed ? '隐藏代理 token' : '显示代理 token'}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={serviceTokenRevealed ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => { void handleServiceTokenVisibleChange(!serviceTokenRevealed) }}
|
||||
aria-label={serviceTokenRevealed ? '隐藏代理 token' : '显示代理 token'}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -693,97 +785,196 @@ export default function AISettings() {
|
||||
children: (
|
||||
<AISettingsPanel loading={loading}>
|
||||
<Form form={form} layout="vertical" onFinish={saveAISettings} onValuesChange={() => setFeedback(null)}>
|
||||
<Card size="small" title={<Space><ToolOutlined />WebSearch 证据层</Space>}>
|
||||
<Form.Item name={['web_search', 'enabled']} label="启用 WebSearch" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'provider']} label="WebSearch Provider">
|
||||
<Card size="small" title={<Space><ToolOutlined />AI Tools</Space>}>
|
||||
<Form.Item label="工具">
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={webSearchPresets.map((preset) => ({
|
||||
value: preset.provider,
|
||||
label: preset.label,
|
||||
}))}
|
||||
onChange={(value) => applyWebSearchProviderSelection(value)}
|
||||
value={selectedToolKey}
|
||||
options={TOOL_OPTIONS}
|
||||
onChange={setSelectedToolKey}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '0 12px', alignItems: 'end' }}>
|
||||
<Form.Item name={['web_search', 'base_url']} label="API Base URL">
|
||||
<Input placeholder="https://api.tavily.com" />
|
||||
</Form.Item>
|
||||
<Form.Item label=" ">
|
||||
<Button icon={<PlayCircleOutlined />} loading={testingWebSearchConnection} onClick={() => { void testWebSearchConnection() }}>
|
||||
测试连接
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item label="WebSearch API Key">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Tag color={selectedWebSearchSecret?.configured ? 'green' : 'default'}>
|
||||
{selectedWebSearchSecret?.configured ? '已配置' : '未配置'}
|
||||
</Tag>
|
||||
<Text type="secondary">保存时只会更新当前 WebSearch Provider 的 key。</Text>
|
||||
</Space>
|
||||
<Form.Item name={['web_search', 'api_key']} noStyle>
|
||||
<Input
|
||||
autoComplete="new-password"
|
||||
placeholder="输入新的 WebSearch API key;SearXNG 可留空"
|
||||
suffix={(
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={webSearchApiKeyRevealed ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => { void handleWebSearchApiKeyVisibleChange(!webSearchApiKeyRevealed) }}
|
||||
aria-label={webSearchApiKeyRevealed ? '隐藏 WebSearch API key' : '显示 WebSearch API key'}
|
||||
/>
|
||||
)}
|
||||
{selectedToolKey === 'web_search' ? (
|
||||
<>
|
||||
<Form.Item name={['web_search', 'enabled']} label="启用 WebSearch" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'provider']} label="WebSearch Provider">
|
||||
<Select
|
||||
showSearch
|
||||
disabled={!webSearchEnabled}
|
||||
optionFilterProp="label"
|
||||
options={webSearchPresets.map((preset) => ({
|
||||
value: preset.provider,
|
||||
label: preset.label,
|
||||
}))}
|
||||
onChange={(value) => applyWebSearchProviderSelection(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||||
<Form.Item name={['web_search', 'max_results']} label="最大结果数">
|
||||
<InputNumber min={1} max={20} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'timeout_seconds']} label="超时(秒)">
|
||||
<InputNumber min={3} max={120} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Card size="small" type="inner" title="高级选项" style={{ marginTop: 8 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||||
<Form.Item name={['web_search', 'endpoint_path']} label="Endpoint Path">
|
||||
<Input placeholder="/search" />
|
||||
<Form.Item name={['web_search', 'base_url']} label="API Base URL">
|
||||
<ConnectionTestInput
|
||||
placeholder="https://api.tavily.com"
|
||||
disabled={!webSearchEnabled}
|
||||
testing={testingWebSearchConnection}
|
||||
testDisabled={!webSearchEnabled || !selectedWebSearchProvider}
|
||||
onTest={() => { void testWebSearchConnection() }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'search_depth']} label="Search Depth">
|
||||
<Input placeholder="basic" />
|
||||
<Form.Item label="WebSearch API Key">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Tag color={selectedWebSearchSecret?.configured ? 'green' : 'default'}>
|
||||
{selectedWebSearchSecret?.configured ? '已配置' : '未配置'}
|
||||
</Tag>
|
||||
<Text type="secondary">保存时只会更新当前 WebSearch Provider 的 key。</Text>
|
||||
</Space>
|
||||
<Form.Item name={['web_search', 'api_key']} noStyle>
|
||||
<Input
|
||||
autoComplete="new-password"
|
||||
disabled={!webSearchEnabled}
|
||||
placeholder="输入新的 WebSearch API key;SearXNG 可留空"
|
||||
suffix={(
|
||||
<Tooltip title={webSearchApiKeyRevealed ? '隐藏 WebSearch API key' : '显示 WebSearch API key'}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
disabled={!webSearchEnabled}
|
||||
icon={webSearchApiKeyRevealed ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => { void handleWebSearchApiKeyVisibleChange(!webSearchApiKeyRevealed) }}
|
||||
aria-label={webSearchApiKeyRevealed ? '隐藏 WebSearch API key' : '显示 WebSearch API key'}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'engine']} label="SerpAPI Engine">
|
||||
<Input placeholder="google" />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||||
<Form.Item name={['web_search', 'max_results']} label="最大结果数">
|
||||
<InputNumber min={1} max={20} disabled={!webSearchEnabled} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'timeout_seconds']} label="超时(秒)">
|
||||
<InputNumber min={3} max={120} disabled={!webSearchEnabled} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Card size="small" type="inner" title="高级选项" style={{ marginTop: 8 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||||
<Form.Item name={['web_search', 'endpoint_path']} label="Endpoint Path">
|
||||
<Input disabled={!webSearchEnabled} placeholder="/search" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'search_depth']} label="Search Depth">
|
||||
<Input disabled={!webSearchEnabled} placeholder="basic" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'engine']} label="SerpAPI Engine">
|
||||
<Input disabled={!webSearchEnabled} placeholder="google" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'categories']} label="SearXNG Categories">
|
||||
<Input disabled={!webSearchEnabled} placeholder="general" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'search_path']} label="Firecrawl Search Path">
|
||||
<Input disabled={!webSearchEnabled} placeholder="/v2/search" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'scrape_path']} label="Firecrawl Scrape Path">
|
||||
<Input disabled={!webSearchEnabled} placeholder="/v2/scrape" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Form.Item name={['web_search', 'include_answer']} valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox disabled={!webSearchEnabled}>包含 Answer</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'include_raw_content']} valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox disabled={!webSearchEnabled}>包含 Raw Content</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'include_text']} valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox disabled={!webSearchEnabled}>包含 Result Text</Checkbox>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
) : null}
|
||||
{selectedToolKey === 'ocr' ? (
|
||||
<>
|
||||
<Form.Item name={['ocr', 'enabled']} label="启用 OCR" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'categories']} label="SearXNG Categories">
|
||||
<Input placeholder="general" />
|
||||
<Form.Item name={['ocr', 'provider']} label="OCR Provider">
|
||||
<Select
|
||||
disabled={!ocrEnabled}
|
||||
options={[
|
||||
{ value: 'paddleocr', label: 'PaddleOCR / Local' },
|
||||
{ value: 'tesseract', label: 'Tesseract / Local' },
|
||||
{ value: 'azure', label: 'Azure AI Vision' },
|
||||
{ value: 'google_vision', label: 'Google Cloud Vision' },
|
||||
{ value: 'custom', label: 'Custom HTTP OCR' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'search_path']} label="Firecrawl Search Path">
|
||||
<Input placeholder="/v2/search" />
|
||||
<Form.Item name={['ocr', 'base_url']} label="OCR Base URL">
|
||||
<Input disabled={!ocrEnabled} placeholder="http://localhost:8020 或云 OCR endpoint" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'scrape_path']} label="Firecrawl Scrape Path">
|
||||
<Input placeholder="/v2/scrape" />
|
||||
<Form.Item label="OCR API Key">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Tag color={integrations?.ocr.api_key.configured ? 'green' : 'default'}>
|
||||
{integrations?.ocr.api_key.configured ? '已配置' : '未配置'}
|
||||
</Tag>
|
||||
<Text type="secondary">本地 OCR 可留空;云 OCR 或自定义 HTTP 服务按需填写。</Text>
|
||||
</Space>
|
||||
<Form.Item name={['ocr', 'api_key']} noStyle>
|
||||
<Input
|
||||
autoComplete="new-password"
|
||||
disabled={!ocrEnabled}
|
||||
placeholder="输入新的 OCR API key"
|
||||
suffix={(
|
||||
<Tooltip title={ocrApiKeyRevealed ? '隐藏 OCR API key' : '显示 OCR API key'}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
disabled={!ocrEnabled}
|
||||
icon={ocrApiKeyRevealed ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => { void handleOcrApiKeyVisibleChange(!ocrApiKeyRevealed) }}
|
||||
aria-label={ocrApiKeyRevealed ? '隐藏 OCR API key' : '显示 OCR API key'}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Form.Item name={['web_search', 'include_answer']} valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox>包含 Answer</Checkbox>
|
||||
<Form.Item name={['ocr', 'model']} label="模型 / Engine">
|
||||
<Input disabled={!ocrEnabled} placeholder="例如:PP-OCRv5、tesseract-default、prebuilt-read" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'include_raw_content']} valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox>包含 Raw Content</Checkbox>
|
||||
<Form.Item name={['ocr', 'languages']} label="识别语言">
|
||||
<Select
|
||||
mode="tags"
|
||||
disabled={!ocrEnabled}
|
||||
options={[
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['web_search', 'include_text']} valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox>包含 Result Text</Checkbox>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||||
<Form.Item name={['ocr', 'timeout_seconds']} label="超时(秒)">
|
||||
<InputNumber min={3} max={300} disabled={!ocrEnabled} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['ocr', 'max_file_size_mb']} label="最大文件(MB)">
|
||||
<InputNumber min={1} max={200} disabled={!ocrEnabled} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name={['ocr', 'output_format']} label="输出格式">
|
||||
<Select
|
||||
disabled={!ocrEnabled}
|
||||
options={[
|
||||
{ value: 'markdown', label: 'Markdown' },
|
||||
{ value: 'text', label: 'Plain Text' },
|
||||
{ value: 'json', label: 'JSON Blocks' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
) : null}
|
||||
</Card>
|
||||
<Button type="primary" htmlType="submit" loading={saving} style={{ marginTop: 16 }}>
|
||||
保存 Tool 配置
|
||||
|
||||
Reference in New Issue
Block a user