release: bump version to 0.58.0
Release 0.58.0 includes the Earth high-precision boundary PMTiles/MVT pipeline, standardized Earth boundary source collectors, China POV boundary configuration templates, and removal of the legacy low-precision GeoJSON fallback. It also adds Earth news target-location queueing/archive support, fixes datasource task status visibility, documents the Earth surface depth-spacing rules that prevent far-zoom z-fighting snow/black blocks, and updates bilingual operations/developer docs.
This commit is contained in:
@@ -303,7 +303,7 @@ export default function AISettings() {
|
||||
nextSecret === savedPreview ||
|
||||
nextSecret === revealedSecret ||
|
||||
nextSecret.startsWith('••••') ||
|
||||
nextSecret.includes('*')
|
||||
Array.from(nextSecret).every((char) => char === '*' || char === '•' || /\s/.test(char))
|
||||
)
|
||||
|
||||
const buildAiProviderDraftPayload = (values: any) => {
|
||||
@@ -381,9 +381,11 @@ export default function AISettings() {
|
||||
setFeedback({ type: 'success', message: 'AI 配置已保存' })
|
||||
message.success('AI 配置已保存')
|
||||
await fetchAISettings()
|
||||
} catch {
|
||||
setFeedback({ type: 'error', message: 'AI 配置保存失败' })
|
||||
message.error('AI 配置保存失败')
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string; message?: string } } }
|
||||
const errorMessage = err.response?.data?.message || err.response?.data?.detail || 'AI 配置保存失败'
|
||||
setFeedback({ type: 'error', message: errorMessage })
|
||||
message.error(errorMessage)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -591,12 +593,8 @@ export default function AISettings() {
|
||||
buildAiProviderDraftPayload(values),
|
||||
)
|
||||
if (response.data.success || response.data.connected) {
|
||||
if (response.data.integrations) {
|
||||
setIntegrations(response.data.integrations)
|
||||
await fetchAISettings()
|
||||
}
|
||||
setFeedback({ type: 'success', message: response.data.message || 'AI Provider 连接成功' })
|
||||
message.success(response.data.message || 'AI Provider 连接成功')
|
||||
setFeedback({ type: 'success', message: response.data.message || '连接测试通过' })
|
||||
message.success(response.data.message || '连接测试通过')
|
||||
} else {
|
||||
setFeedback({ type: 'error', message: response.data.message || 'AI Provider 连接失败' })
|
||||
message.error(response.data.message || 'AI Provider 连接失败')
|
||||
@@ -680,6 +678,9 @@ export default function AISettings() {
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ marginTop: -16, marginBottom: 12 }}>
|
||||
<Text type="secondary">测试不保存,保存才生效。</Text>
|
||||
</div>
|
||||
<Form.Item name={['ai_provider', 'provider_api']} label="协议适配">
|
||||
<Select>
|
||||
<Select.Option value="openai-completions">OpenAI Chat Completions</Select.Option>
|
||||
@@ -819,6 +820,9 @@ export default function AISettings() {
|
||||
onTest={() => { void testWebSearchConnection() }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ marginTop: -16, marginBottom: 12 }}>
|
||||
<Text type="secondary">测试不保存,保存才生效。</Text>
|
||||
</div>
|
||||
<Form.Item label="WebSearch API Key">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
|
||||
@@ -38,6 +38,8 @@ import { formatPhaseMetric, getPhaseDisplay, getPhaseSummary } from '../../utils
|
||||
|
||||
const { Text } = Typography
|
||||
const COLLECTION_REFRESH_DELAY_MS = 800
|
||||
const COLLECTION_STATUS_POLL_MS = 900
|
||||
const COLLECTION_STATUS_MAX_POLLS = 30
|
||||
|
||||
interface BuiltInDataSource {
|
||||
id: number
|
||||
@@ -217,6 +219,7 @@ type DatasourceTaskStatus = {
|
||||
records_processed?: number | null
|
||||
total_records?: number | null
|
||||
status?: string | null
|
||||
error_message?: string | null
|
||||
}
|
||||
|
||||
type ActiveFilter = 'enabled' | 'disabled' | 'all'
|
||||
@@ -228,6 +231,7 @@ const PRODUCT_LABELS: Record<string, string> = {
|
||||
cables: '海底光缆',
|
||||
satellites: '卫星',
|
||||
bgp: 'BGP',
|
||||
earth: 'Earth',
|
||||
compute: '算力',
|
||||
ai: 'AI',
|
||||
media: '媒体',
|
||||
@@ -239,6 +243,7 @@ const PRODUCT_TAG_COLORS: Record<string, string> = {
|
||||
cables: 'geekblue',
|
||||
satellites: 'purple',
|
||||
bgp: 'volcano',
|
||||
earth: 'lime',
|
||||
compute: 'blue',
|
||||
ai: 'magenta',
|
||||
media: 'green',
|
||||
@@ -412,11 +417,44 @@ function DataSources() {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchDatasourceTaskStatus = async (id: number) => {
|
||||
const res = await axios.get<DatasourceTaskStatus>(`/api/v1/datasources/${id}/task-status`)
|
||||
const fetchDatasourceTaskStatus = async (id: number, taskId?: number | null) => {
|
||||
const res = await axios.get<DatasourceTaskStatus>(`/api/v1/datasources/${id}/task-status`, {
|
||||
params: taskId ? { task_id: taskId } : undefined,
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
const pollDatasourceAfterTrigger = (id: number, taskId?: number | null) => {
|
||||
let polls = 0
|
||||
let lastTaskId = taskId ?? null
|
||||
|
||||
const poll = async () => {
|
||||
polls += 1
|
||||
try {
|
||||
const status = await fetchDatasourceTaskStatus(id, lastTaskId)
|
||||
lastTaskId = status.task_id ?? lastTaskId
|
||||
await fetchData()
|
||||
|
||||
if (status.status === 'failed') {
|
||||
messageApi.error(status.error_message || status.phase_message || '采集失败')
|
||||
return
|
||||
}
|
||||
if (status.status && status.status !== 'running' && status.status !== 'idle') {
|
||||
return
|
||||
}
|
||||
if (polls < COLLECTION_STATUS_MAX_POLLS) {
|
||||
window.setTimeout(poll, COLLECTION_STATUS_POLL_MS)
|
||||
}
|
||||
} catch (error) {
|
||||
if (polls < COLLECTION_STATUS_MAX_POLLS) {
|
||||
window.setTimeout(poll, COLLECTION_STATUS_POLL_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.setTimeout(poll, COLLECTION_REFRESH_DELAY_MS)
|
||||
}
|
||||
|
||||
const triggerDatasource = async (id: number, options?: { force?: boolean }) => {
|
||||
const res = await axios.post(`/api/v1/datasources/${id}/trigger`, null, {
|
||||
params: { force: options?.force ?? false },
|
||||
@@ -427,11 +465,8 @@ function DataSources() {
|
||||
return { ok: false, status: res.status, detail: res.data?.detail } satisfies TriggerDatasourceResult
|
||||
}
|
||||
|
||||
if (res.data.task_id) {
|
||||
void fetchData()
|
||||
} else {
|
||||
window.setTimeout(fetchData, COLLECTION_REFRESH_DELAY_MS)
|
||||
}
|
||||
void fetchData()
|
||||
pollDatasourceAfterTrigger(id, res.data.task_id)
|
||||
return { ok: true, response: res } satisfies TriggerDatasourceResult
|
||||
}
|
||||
|
||||
@@ -916,6 +951,7 @@ function DataSources() {
|
||||
{ value: 'cables', label: '海底光缆' },
|
||||
{ value: 'satellites', label: '卫星' },
|
||||
{ value: 'bgp', label: 'BGP' },
|
||||
{ value: 'earth', label: 'Earth' },
|
||||
{ value: 'compute', label: '算力' },
|
||||
{ value: 'ai', label: 'AI' },
|
||||
{ value: 'media', label: '媒体' },
|
||||
|
||||
@@ -417,6 +417,11 @@ function Settings() {
|
||||
const collectorOptions = useMemo(() => [...collectors, ...customCollectors], [collectors, customCollectors])
|
||||
const selectedCollector = collectorOptions.find((collector) => collector.source === selectedCollectorSource)
|
||||
const selectedCollectorConfig = [...collectorConfigs, ...customSourceConfigs].find((config) => config.name === selectedCollectorSource)
|
||||
const isEarthBoundarySourceCollector = Boolean(
|
||||
selectedCollector?.source &&
|
||||
['earth_admin0_boundaries', 'earth_coastline', 'earth_claim_lines'].includes(selectedCollector.source),
|
||||
)
|
||||
const isEarthPmtilesBuilder = selectedCollector?.source === 'earth_boundary_tiles'
|
||||
const [customStreamStatus, setCustomStreamStatus] = useState<{ running: boolean; done: boolean } | null>(null)
|
||||
const [customStreamBusy, setCustomStreamBusy] = useState(false)
|
||||
const selectedCollectorHealth = selectedCollector
|
||||
@@ -561,7 +566,11 @@ function Settings() {
|
||||
}, [integrationForm, integrations, loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !selectedCollectorConfig) return
|
||||
if (loading) return
|
||||
if (!selectedCollectorConfig) {
|
||||
collectorConfigForm.resetFields()
|
||||
return
|
||||
}
|
||||
const config = selectedCollectorConfig.config || {}
|
||||
const boundingBoxes = config.bounding_boxes ?? [[[-90, -180], [90, 180]]]
|
||||
if (selectedCollector?.is_custom) {
|
||||
@@ -591,6 +600,10 @@ function Settings() {
|
||||
config: {
|
||||
timeout: config.timeout ?? 30,
|
||||
retry: config.retry ?? 3,
|
||||
method: config.method || 'GET',
|
||||
target_schema: config.target_schema || (isEarthBoundarySourceCollector ? 'earth_boundary_source' : undefined),
|
||||
license: config.license || '',
|
||||
mapping_json_text: JSON.stringify(config.mapping_json || {}, null, 2),
|
||||
max_messages: config.max_messages ?? 500,
|
||||
receive_timeout_seconds: config.receive_timeout_seconds ?? 30,
|
||||
message_types: config.message_types ?? ['PositionReport', 'ShipStaticData'],
|
||||
@@ -598,7 +611,7 @@ function Settings() {
|
||||
bounding_boxes_json: stringifyBoundingBoxes(boundingBoxes),
|
||||
},
|
||||
})
|
||||
}, [collectorConfigForm, loading, selectedCollector, selectedCollectorConfig])
|
||||
}, [collectorConfigForm, isEarthBoundarySourceCollector, loading, selectedCollector, selectedCollectorConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestedCollector || !collectorOptions.some((collector) => collector.source === requestedCollector)) return
|
||||
@@ -706,10 +719,12 @@ function Settings() {
|
||||
setWebSearchSaveFeedback({ type: 'success', message: 'WebSearch 配置已保存' })
|
||||
message.success('外部集成配置已保存')
|
||||
await fetchSettings()
|
||||
} catch {
|
||||
setAiProviderSaveFeedback({ type: 'error', message: 'AI 配置保存失败' })
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string; message?: string } } }
|
||||
const errorMessage = err.response?.data?.message || err.response?.data?.detail || '外部集成配置保存失败'
|
||||
setAiProviderSaveFeedback({ type: 'error', message: errorMessage })
|
||||
setWebSearchSaveFeedback({ type: 'error', message: 'WebSearch 配置保存失败' })
|
||||
message.error('外部集成配置保存失败')
|
||||
message.error(errorMessage)
|
||||
} finally {
|
||||
setSavingIntegrations(false)
|
||||
}
|
||||
@@ -724,7 +739,7 @@ function Settings() {
|
||||
nextSecret === savedPreview ||
|
||||
nextSecret === revealedSecret ||
|
||||
nextSecret.startsWith('••••') ||
|
||||
nextSecret.includes('*')
|
||||
Array.from(nextSecret).every((char) => char === '*' || char === '•' || /\s/.test(char))
|
||||
)
|
||||
|
||||
const buildAiProviderDraftPayload = (values: any) => {
|
||||
@@ -899,31 +914,8 @@ function Settings() {
|
||||
buildAiProviderDraftPayload(values),
|
||||
)
|
||||
if (response.data.success || response.data.connected) {
|
||||
if (response.data.integrations) {
|
||||
setIntegrations(response.data.integrations)
|
||||
integrationForm.setFieldsValue({
|
||||
ai_provider: {
|
||||
service_url: response.data.integrations.ai_provider.service_url,
|
||||
service_token: response.data.integrations.ai_provider.service_token.configured
|
||||
? response.data.integrations.ai_provider.service_token.preview
|
||||
: '',
|
||||
default_provider: response.data.integrations.ai_provider.default_provider || response.data.integrations.ai_provider.provider,
|
||||
provider: response.data.integrations.ai_provider.provider,
|
||||
provider_api: response.data.integrations.ai_provider.provider_api,
|
||||
base_url: response.data.integrations.ai_provider.base_url,
|
||||
model: response.data.integrations.ai_provider.model,
|
||||
api_key: response.data.integrations.ai_provider.api_key.configured
|
||||
? response.data.integrations.ai_provider.api_key.preview
|
||||
: '',
|
||||
max_tokens: response.data.integrations.ai_provider.max_tokens,
|
||||
anthropic_version: response.data.integrations.ai_provider.anthropic_version,
|
||||
timeout_seconds: response.data.integrations.ai_provider.timeout_seconds,
|
||||
retry_attempts: response.data.integrations.ai_provider.retry_attempts,
|
||||
},
|
||||
})
|
||||
}
|
||||
setAiProviderSaveFeedback({ type: 'success', message: response.data.message || 'AI Provider 连接成功,已保存为全局默认配置' })
|
||||
message.success(response.data.message || 'AI Provider 连接成功')
|
||||
setAiProviderSaveFeedback({ type: 'success', message: response.data.message || '连接测试通过' })
|
||||
message.success(response.data.message || '连接测试通过')
|
||||
} else {
|
||||
setAiProviderSaveFeedback({ type: 'error', message: response.data.message || 'AI Provider 连接失败' })
|
||||
message.error(response.data.message || 'AI Provider 连接失败')
|
||||
@@ -996,12 +988,23 @@ function Settings() {
|
||||
delete configValues.bounding_boxes_json
|
||||
delete configValues.bounding_box_preset
|
||||
}
|
||||
if (['earth_admin0_boundaries', 'earth_coastline', 'earth_claim_lines'].includes(selectedCollector.source)) {
|
||||
try {
|
||||
configValues.mapping_json = JSON.parse(configValues.mapping_json_text || '{}')
|
||||
} catch {
|
||||
message.error('Earth 边界 mapping_json 必须是合法 JSON')
|
||||
return
|
||||
}
|
||||
configValues.target_schema = configValues.target_schema || 'earth_boundary_source'
|
||||
configValues.method = configValues.method || 'GET'
|
||||
delete configValues.mapping_json_text
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
name: selectedCollector.source,
|
||||
description: `内置采集器覆盖配置:${selectedCollector.name}`,
|
||||
source_type: selectedCollector.source === 'aisstream_vessels' ? 'websocket' : 'http',
|
||||
endpoint: baseValues.endpoint,
|
||||
source_type: selectedCollector.source === 'aisstream_vessels' ? 'websocket' : isEarthPmtilesBuilder ? 'internal' : 'http',
|
||||
endpoint: baseValues.endpoint || '',
|
||||
auth_type: selectedCollector.source === 'aisstream_vessels' ? 'api_key' : 'none',
|
||||
headers,
|
||||
config: configValues,
|
||||
@@ -2026,6 +2029,9 @@ function Settings() {
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ marginTop: -16, marginBottom: 12 }}>
|
||||
<Text type="secondary">测试不保存,保存才生效。</Text>
|
||||
</div>
|
||||
|
||||
<Form.Item name={['ai_provider', 'provider_api']} label="协议适配">
|
||||
<Select>
|
||||
@@ -2151,6 +2157,9 @@ function Settings() {
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ marginTop: -16, marginBottom: 12 }}>
|
||||
<Text type="secondary">测试不保存,保存才生效。</Text>
|
||||
</div>
|
||||
|
||||
<Form.Item label="WebSearch API Key">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
@@ -2481,14 +2490,39 @@ function Settings() {
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
<Form.Item name="endpoint" label="Endpoint" rules={[{ required: true, message: '请输入 Endpoint' }]}>
|
||||
<Input placeholder={selectedCollectorConfig?.default_url || 'https://api.example.com'} />
|
||||
<Form.Item
|
||||
name="endpoint"
|
||||
label="Endpoint"
|
||||
rules={isEarthPmtilesBuilder ? [] : [{ required: true, message: '请输入 Endpoint' }]}
|
||||
>
|
||||
<Input
|
||||
disabled={isEarthPmtilesBuilder}
|
||||
placeholder={isEarthPmtilesBuilder ? '内部构建器不需要 Endpoint' : selectedCollectorConfig?.default_url || 'https://api.example.com'}
|
||||
/>
|
||||
</Form.Item>
|
||||
{!selectedCollector?.is_custom ? (
|
||||
<Form.Item label="默认 Endpoint">
|
||||
<Input value={selectedCollectorConfig?.default_url || '-'} disabled />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isEarthBoundarySourceCollector ? (
|
||||
<>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||||
<Form.Item name={['config', 'method']} label="请求方法">
|
||||
<Select options={[{ value: 'GET', label: 'GET' }, { value: 'POST', label: 'POST' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['config', 'target_schema']} label="目标 Schema">
|
||||
<Select options={[{ value: 'earth_boundary_source', label: 'earth_boundary_source' }]} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name={['config', 'license']} label="License">
|
||||
<Input placeholder="例如 ODbL / source license / internal" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['config', 'mapping_json_text']} label="Mapping JSON">
|
||||
<Input.TextArea rows={8} />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
<Form.List name="headers">
|
||||
{(fields, { add, remove }) => (
|
||||
<Form.Item label="请求头">
|
||||
|
||||
Reference in New Issue
Block a user