适配器「{{ formData.basic.adapter_type }}」连接配置:
@@ -282,6 +288,9 @@ import { ref, reactive, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import { useAdapterMetadata } from '@/composables/external-systems/useAdapterMetadata'
import { useAuthPlugin } from '@/composables/external-systems/useAuthPlugin'
+import { useVendorIntegration } from '@/composables/external-systems/useVendorIntegration'
+import { unwrap } from '@/composables/external-systems/utils'
+import { getVendorConnectionSchemaApi } from '@/apis/external-systems/vendor_integration_api'
import { SYSTEM_CATEGORY_MAP as categoryDisplayMap } from '@/composables/external-systems/constants'
import DynamicForm from '@/components/external-systems/common/DynamicForm.vue'
import LoadingState from '@/components/external-systems/common/LoadingState.vue'
@@ -290,6 +299,7 @@ const props = defineProps({
visible: { type: Boolean, default: false },
systemId: { type: [String, Number], default: null },
initialData: { type: Object, default: null },
+ initialVendorMeta: { type: Object, default: null },
submitting: { type: Boolean, default: false }
})
@@ -302,7 +312,10 @@ const {
fetchSchema: fetchAdapterSchema,
resetSchema: resetAdapterSchema,
overview: adapterOverview,
- fetchOverview: fetchAdapterOverview
+ fetchOverview: fetchAdapterOverview,
+ authPlugins,
+ fetchAuthPlugins,
+ resetAuthPlugins
} = useAdapterMetadata()
const {
@@ -311,11 +324,15 @@ const {
resetDetailSchema: resetAuthSchema
} = useAuthPlugin()
+const { sourceTypes, fetchListMeta } = useVendorIntegration()
+
// ===== Reactive State =====
const currentStep = ref(0)
const basicFormRef = ref(null)
const authSchemaLoading = ref(false)
const secretRefsText = ref('')
+const vendorSupportedAuthTypes = ref(null)
+const vendorExtraSchema = ref(null)
const formData = reactive({
basic: {
@@ -360,7 +377,14 @@ const formData = reactive({
// ===== Computed =====
const isEdit = computed(() => !!props.systemId)
const adapterSchemaData = computed(() => adapterSchemaRef.value)
-const authSchemaData = computed(() => authSchemaState.data)
+const authSchemaData = computed(() => authSchemaState.data?.schema)
+const mergedAdapterSchema = computed(() =>
+ mergeSchemas(adapterSchemaRef.value, vendorExtraSchema.value)
+)
+const sourceTypeOptions = computed(() => {
+ const items = sourceTypes.value || []
+ return items.map((s) => ({ label: s.source_type, value: s.source_type }))
+})
// ===== Constants =====
@@ -387,13 +411,17 @@ const adapterTypeOptions = computed(() => {
]
})
-const authTypeOptions = [
- { label: '无认证', value: 'none' },
- { label: 'Basic Auth', value: 'basic' },
- { label: 'Bearer Token', value: 'bearer' },
- { label: 'API Key', value: 'api_key' },
- { label: 'OAuth2', value: 'oauth2' }
-]
+const authTypeOptions = computed(() => {
+ let items = authPlugins.value.map((p) => ({ label: p.display_name, value: p.type }))
+ if (vendorSupportedAuthTypes.value) {
+ const allowed = vendorSupportedAuthTypes.value
+ items = items.filter((i) => allowed.includes(i.value))
+ }
+ if (!items.some((i) => i.value === 'none')) {
+ items.unshift({ label: '无认证', value: 'none' })
+ }
+ return items
+})
const backoffOptions = [
{ label: '固定间隔', value: 'fixed' },
@@ -442,6 +470,9 @@ function resetForm() {
currentStep.value = 0
resetAdapterSchema()
resetAuthSchema()
+ resetAuthPlugins()
+ vendorSupportedAuthTypes.value = null
+ vendorExtraSchema.value = null
}
function fillForm(data) {
@@ -494,17 +525,68 @@ async function loadAuthSchema(authType, adapterType) {
authSchemaLoading.value = true
try {
await fetchAuthSchema(authType, adapterType)
+ if (authSchemaState.data?.schema) {
+ authSchemaState.data = {
+ ...authSchemaState.data,
+ schema: normalizeAuthSchema(authSchemaState.data.schema)
+ }
+ }
} finally {
authSchemaLoading.value = false
}
}
+async function loadVendorExtraSchema(sourceType) {
+ if (!sourceType) {
+ vendorExtraSchema.value = null
+ return
+ }
+ try {
+ const data = unwrap(await getVendorConnectionSchemaApi(sourceType))
+ vendorExtraSchema.value = data?.connection_extra_schema || null
+ } catch {
+ // 404 等错误静默处理:该厂商无额外连接配置
+ vendorExtraSchema.value = null
+ }
+}
+
+function mergeSchemas(base, extra) {
+ if (!base) return extra
+ if (!extra) return base
+ return {
+ ...base,
+ properties: { ...(base.properties || {}), ...(extra.properties || {}) },
+ required: [...new Set([...(base.required || []), ...(extra.required || [])])]
+ }
+}
+
+function normalizeAuthSchema(schema) {
+ if (!schema || !schema.properties) return schema
+ const normalized = { ...schema, properties: { ...schema.properties } }
+ for (const [name, field] of Object.entries(normalized.properties)) {
+ if (/private_key/i.test(name)) {
+ normalized.properties[name] = { ...field, format: 'multiline' }
+ } else if (!/_url$/.test(name) && /^(password|secret|token)$/i.test(name)) {
+ normalized.properties[name] = { ...field, format: 'password' }
+ }
+ }
+ return normalized
+}
+
async function handleAdapterTypeChange(value) {
formData.basic.adapter_type = value
formData.connection_config = {}
+ formData.auth_type = undefined
+ formData.auth_config = {}
+ await fetchAuthPlugins(value)
await loadAdapterSchema(value)
}
+function handleSourceTypeChange(value) {
+ formData.connection_config = {}
+ loadVendorExtraSchema(value)
+}
+
async function handleAuthTypeChange(value) {
formData.auth_type = value
formData.auth_config = {}
@@ -619,13 +701,33 @@ watch(
if (val) {
resetForm()
// 加载适配器概览(动态填充适配器类型选项)
- if (!adapterOverview.value) {
+ // adapterOverview 默认为空数组 [](truthy),必须用 .length 判断
+ if (!adapterOverview.value?.length) {
fetchAdapterOverview()
}
- if (props.initialData) {
+ // authPlugins 无需预加载,等 adapter_type 选择后再 fetchAuthPlugins
+ // 加载厂商列表元数据(用于 source_type 自动补全)
+ if (!sourceTypes.value?.length) {
+ fetchListMeta()
+ }
+ if (props.initialVendorMeta) {
+ vendorSupportedAuthTypes.value = props.initialVendorMeta.supported_auth_types || null
+ fillForm({
+ source_type: props.initialVendorMeta.source_type,
+ adapter_type: props.initialVendorMeta.adapter_type
+ })
+ if (formData.basic.adapter_type) {
+ await loadAdapterSchema(formData.basic.adapter_type)
+ await fetchAuthPlugins(formData.basic.adapter_type)
+ }
+ if (formData.basic.source_type) {
+ await loadVendorExtraSchema(formData.basic.source_type)
+ }
+ } else if (props.initialData) {
fillForm(props.initialData)
if (formData.basic.adapter_type) {
await loadAdapterSchema(formData.basic.adapter_type)
+ await fetchAuthPlugins(formData.basic.adapter_type)
}
if (formData.auth_type) {
await loadAuthSchema(formData.auth_type, formData.basic.adapter_type)
diff --git a/web/src/components/external-systems/asset-management/system/SystemListView.vue b/web/src/components/external-systems/asset-management/system/SystemListView.vue
index 34feaf39..c358b8f2 100644
--- a/web/src/components/external-systems/asset-management/system/SystemListView.vue
+++ b/web/src/components/external-systems/asset-management/system/SystemListView.vue
@@ -189,6 +189,7 @@
v-model:visible="formModalVisible"
:system-id="editingId"
:initial-data="editingData"
+ :initial-vendor-meta="initialVendorMeta"
:submitting="submitting"
@submit="handleSubmit"
/>
@@ -280,6 +281,7 @@ import {
} from 'lucide-vue-next'
import { useUserStore } from '@/stores/user'
import { useExternalSystem } from '@/composables/external-systems/useExternalSystem'
+import { useVendorIntegration } from '@/composables/external-systems/useVendorIntegration'
import {
SYSTEM_HEALTH_STATUS_MAP as healthStatusMap,
SYSTEM_CATEGORY_MAP as categoryDisplayMap
@@ -342,12 +344,15 @@ const {
batchEnabled
} = useExternalSystem()
+const { detail: vendorIntegrationDetail, fetchDetail: fetchVendorDetail } = useVendorIntegration()
+
// ===== Reactive State =====
const selectedRowKeys = ref([])
const selectedSystems = ref([])
const formModalVisible = ref(false)
const editingId = ref(null)
const editingData = ref(null)
+const initialVendorMeta = ref(null)
const cloneModalVisible = ref(false)
const cloningSource = ref(null)
const batchModalVisible = ref(false)
@@ -684,15 +689,40 @@ function handleDetail(record) {
function handleCreate() {
editingId.value = null
editingData.value = null
+ initialVendorMeta.value = null
formModalVisible.value = true
}
function handleEdit(record) {
editingId.value = record.id
editingData.value = record
+ initialVendorMeta.value = null
formModalVisible.value = true
}
+// 通过厂商集成目录的 integration_key 跳转过来时,预填适配器与数据源
+async function handleIntegrationKeyQuery(key) {
+ try {
+ await fetchVendorDetail(key)
+ const detail = vendorIntegrationDetail.value
+ if (!detail) {
+ message.warning('未找到对应的厂商集成')
+ return
+ }
+ initialVendorMeta.value = {
+ source_type: key,
+ adapter_type: detail.adapter_type,
+ connection_extra_schema: detail.connection_extra_schema,
+ supported_auth_types: detail.supported_auth_types
+ }
+ editingId.value = null
+ editingData.value = null
+ formModalVisible.value = true
+ } catch (err) {
+ message.error(err?.message || '加载厂商集成失败')
+ }
+}
+
function handleClone(record) {
cloningSource.value = record
cloneModalVisible.value = true
@@ -861,6 +891,12 @@ onMounted(() => {
restoreFromRouteQuery()
loadAdapterOptions()
loadAll()
+ // 通过厂商集成目录跳转:携带 integration_key 时预填表单
+ const integrationKey = route.query.integration_key
+ if (integrationKey) {
+ handleIntegrationKeyQuery(String(integrationKey))
+ router.replace({ query: { ...route.query, integration_key: undefined } })
+ }
})
// 浏览器前进/后退时恢复状态
@@ -873,6 +909,19 @@ watch(
}
)
+// 厂商集成目录跳转:integration_key 变化时预填表单
+watch(
+ () => route.query.integration_key,
+ (key) => {
+ if (key) handleIntegrationKeyQuery(String(key))
+ }
+)
+
+// 弹窗关闭时清空厂商预填元数据,避免下次打开残留
+watch(formModalVisible, (val) => {
+ if (!val) initialVendorMeta.value = null
+})
+
// 统一错误提示:create/update/batchClone/batchEnabled 失败时由 composable 写入 submitError
watch(submitError, (val) => {
if (val) {
diff --git a/web/src/components/external-systems/integration-config/integration-generator/IntegrationGeneratorView.vue b/web/src/components/external-systems/integration-config/integration-generator/IntegrationGeneratorView.vue
index 63179b62..6bff2fb0 100644
--- a/web/src/components/external-systems/integration-config/integration-generator/IntegrationGeneratorView.vue
+++ b/web/src/components/external-systems/integration-config/integration-generator/IntegrationGeneratorView.vue
@@ -118,14 +118,72 @@
{{ jsonError }}
+
+ 不同厂商支持的发现选项字段不同:Salesforce 使用
+ selected_sobjects,HubSpot 使用 selected_objects
+
+
+
+
+
+
+
+ {{ record.name }}
+
+
+
+ {{ record.queryable ? '是' : '否' }}
+
+
+
+
+ {{ record.createable ? '是' : '否' }}
+
+
+
+
+ {{ record.updateable ? '是' : '否' }}
+
+
+
+
+ {{ record.deletable ? '是' : '否' }}
+
+
+
+
+
+
发现资源
+
+ 预览工具
+
import { h, ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { Wand2, Trash2 } from 'lucide-vue-next'
import { useUserStore } from '@/stores/user'
@@ -317,6 +385,8 @@ const {
previewStats,
draftTools,
selectedToolSlugs,
+ selectedResources,
+ resourceList,
discoverLoading,
previewLoading,
generateLoading,
@@ -325,6 +395,7 @@ const {
error,
handleSystemChange,
discover,
+ preview,
generate,
persist,
deleteTool,
@@ -334,6 +405,10 @@ const {
back
} = useIntegrationGenerator()
+// ===== 路由(system_id 自动打开弹窗) =====
+const route = useRoute()
+const router = useRouter()
+
// ===== 权限 =====
const userStore = useUserStore()
const isAdmin = computed(() => userStore.isAdmin)
@@ -408,6 +483,34 @@ const deleteContent = computed(() => {
return `确认删除工具「${name}」?此操作不可恢复,删除后相关配置将永久丢失。`
})
+// 发现选项 placeholder:按 source_type 动态生成,提示各厂商支持的字段
+const discoveryOptionsPlaceholder = computed(() => {
+ const st = form.source_type
+ if (st === 'salesforce') return '{"selected_sobjects": ["Account", "Contact"]}'
+ if (st === 'hubspot') return '{"selected_objects": ["contacts", "companies"]}'
+ return '{"key": "value"}'
+})
+
+// ===== discover 阶段 resource 多选 =====
+const resourceSearchKeyword = ref('')
+const resourceColumns = [
+ { title: 'Name', key: 'name', dataIndex: 'name', width: 200 },
+ { title: 'Label', key: 'label', dataIndex: 'label', width: 200 },
+ { title: '查询', key: 'queryable', width: 80, align: 'center' },
+ { title: '创建', key: 'createable', width: 80, align: 'center' },
+ { title: '更新', key: 'updateable', width: 80, align: 'center' },
+ { title: '删除', key: 'deletable', width: 80, align: 'center' }
+]
+const filteredResourceList = computed(() => {
+ const kw = resourceSearchKeyword.value.trim().toLowerCase()
+ if (!kw) return resourceList.value
+ return resourceList.value.filter(
+ (r) =>
+ String(r.name || '').toLowerCase().includes(kw) ||
+ String(r.label || '').toLowerCase().includes(kw)
+ )
+})
+
// ===== Watch =====
watch(error, (err) => {
if (err) {
@@ -472,6 +575,17 @@ async function handleDiscover() {
}
}
+async function handlePreview() {
+ if (selectedResources.value.length === 0) {
+ message.warning('请至少选择 1 个资源')
+ return
+ }
+ const ok = await preview()
+ if (ok) {
+ currentStep.value = 1
+ }
+}
+
async function handleGenerate() {
if (selectedToolSlugs.value.length === 0) {
message.warning('请至少选择 1 个工具')
@@ -537,12 +651,30 @@ async function confirmDeleteTool() {
})
}
+// ===== 路由 system_id 自动打开弹窗 =====
+async function handleSystemIdQuery(systemId) {
+ await openModal()
+ form.system_id = Number(systemId)
+ await handleSystemChange(Number(systemId))
+ router.replace({ query: { ...route.query, system_id: undefined } })
+}
+
// ===== Lifecycle =====
onMounted(() => {
fetchToolList({ limit: listPagination.value.pageSize, offset: 0 })
window.addEventListener('resize', handleResize)
+ if (route.query.system_id) {
+ handleSystemIdQuery(route.query.system_id)
+ }
})
+watch(
+ () => route.query.system_id,
+ (systemId) => {
+ if (systemId) handleSystemIdQuery(systemId)
+ }
+)
+
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (resizeTimer) clearTimeout(resizeTimer)
@@ -600,6 +732,36 @@ onBeforeUnmount(() => {
font-size: 12px;
}
+.discovery-options-hint {
+ margin-top: 4px;
+ color: #888;
+ font-size: 12px;
+
+ code {
+ background: #f5f5f5;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: monospace;
+ }
+}
+
+.resource-selector {
+ margin-top: 16px;
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ }
+
+ &__title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--gray-800);
+ }
+}
+
.draft-alert {
margin-bottom: 12px;
}
diff --git a/web/src/components/external-systems/runtime-monitoring/health-check/HealthCheckView.vue b/web/src/components/external-systems/runtime-monitoring/health-check/HealthCheckView.vue
index 93ec7421..b30bfaad 100644
--- a/web/src/components/external-systems/runtime-monitoring/health-check/HealthCheckView.vue
+++ b/web/src/components/external-systems/runtime-monitoring/health-check/HealthCheckView.vue
@@ -325,7 +325,7 @@