feat(web): 新增集成工具生成与健康检查优化功能

1. 重构健康检查错误处理,将错误提示交由调用方自定义
2. 系统详情页新增生成工具快捷按钮
3. 新增集成生成器资源多选、厂商集成跳转支持
4. 系统表单新增数据源自动补全与动态认证选项
5. 支持通过厂商集成目录快速创建系统
This commit is contained in:
Kris 2026-07-18 04:53:42 +08:00
parent 59cc1daee1
commit 1f47786dc8
8 changed files with 432 additions and 35 deletions

View File

@ -24,6 +24,10 @@
<template #icon><Copy :size="14" /></template>
克隆
</a-button>
<a-button type="primary" @click="handleNavigateToGenerator">
<template #icon><Wand2 :size="14" /></template>
生成工具
</a-button>
<a-button danger @click="openBucketCleanupModal">
<template #icon><Trash2 :size="14" /></template>
清理指标桶
@ -484,7 +488,8 @@ import {
RotateCw,
KeyRound,
GitBranch,
Trash2
Trash2,
Wand2
} from 'lucide-vue-next'
import { useUserStore } from '@/stores/user'
import { useExternalSystem } from '@/composables/external-systems/useExternalSystem'
@ -644,6 +649,14 @@ function handleClone() {
cloneModalVisible.value = true
}
function handleNavigateToGenerator() {
if (!systemId.value) return
router.push({
name: 'EsIntegrationGenerator',
query: { system_id: String(systemId.value) }
})
}
function handleOpenGovernance() {
governanceDrawerVisible.value = true
}

View File

@ -60,7 +60,13 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="数据源" name="source_type">
<a-input v-model:value="formData.basic.source_type" placeholder="数据源标识" />
<a-auto-complete
v-model:value="formData.basic.source_type"
:options="sourceTypeOptions"
placeholder="数据源标识,如 salesforce"
:filter-option="(input, option) => option.value.toLowerCase().includes(input.toLowerCase())"
@change="handleSourceTypeChange"
/>
</a-form-item>
</a-col>
<a-col :span="12">
@ -96,11 +102,11 @@
description="请先在「基础信息」步骤选择适配器类型"
/>
<LoadingState v-else-if="adapterSchemaLoading" type="skeleton" :rows="6" />
<div v-else-if="adapterSchemaData">
<div v-else-if="mergedAdapterSchema">
<p class="es-system-form__hint">适配器{{ formData.basic.adapter_type }}连接配置</p>
<DynamicForm
v-model="formData.connection_config"
:schema="adapterSchemaData"
:schema="mergedAdapterSchema"
:hide-actions="true"
layout="vertical"
/>
@ -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)

View File

@ -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) {

View File

@ -118,14 +118,72 @@
<a-textarea
v-model:value="discoveryOptionsStr"
:rows="6"
placeholder='{ "include_objects": ["Account", "Contact"] }'
:placeholder="discoveryOptionsPlaceholder"
class="code-textarea"
@blur="validateJson"
/>
<div v-if="jsonError" class="json-error">{{ jsonError }}</div>
<div class="discovery-options-hint">
不同厂商支持的发现选项字段不同Salesforce 使用
<code>selected_sobjects</code>HubSpot 使用 <code>selected_objects</code>
</div>
</a-form-item>
</a-form>
<!-- discover 阶段 resource 多选表格 -->
<div v-if="resourceList.length" class="resource-selector">
<div class="resource-selector__header">
<span class="resource-selector__title">
已发现 {{ resourceList.length }} 个资源已选 {{ selectedResources.length }}
</span>
<a-input-search
v-model:value="resourceSearchKeyword"
placeholder="搜索 name 或 label"
size="small"
style="width: 240px"
allow-clear
/>
</div>
<a-table
:data-source="filteredResourceList"
:columns="resourceColumns"
:row-selection="{
selectedRowKeys: selectedResources,
onChange: (keys) => (selectedResources = keys)
}"
:row-key="(record) => record.name"
:pagination="{ pageSize: 20, size: 'small' }"
size="small"
:scroll="{ y: 360 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<span class="mono-text">{{ record.name }}</span>
</template>
<template v-else-if="column.key === 'queryable'">
<a-tag :color="record.queryable ? 'success' : 'default'">
{{ record.queryable ? '是' : '否' }}
</a-tag>
</template>
<template v-else-if="column.key === 'createable'">
<a-tag :color="record.createable ? 'success' : 'default'">
{{ record.createable ? '是' : '否' }}
</a-tag>
</template>
<template v-else-if="column.key === 'updateable'">
<a-tag :color="record.updateable ? 'success' : 'default'">
{{ record.updateable ? '是' : '否' }}
</a-tag>
</template>
<template v-else-if="column.key === 'deletable'">
<a-tag :color="record.deletable ? 'success' : 'default'">
{{ record.deletable ? '是' : '否' }}
</a-tag>
</template>
</template>
</a-table>
</div>
<!-- 发现结果空状态 -->
<EmptyState
v-if="discoverResult && !discoverResult.resources?.length"
@ -237,6 +295,15 @@
>
发现资源
</a-button>
<a-button
v-if="currentStep === 0"
type="primary"
:loading="previewLoading"
:disabled="selectedResources.length === 0"
@click="handlePreview"
>
预览工具
</a-button>
<a-button
v-if="currentStep === 1"
type="primary"
@ -286,6 +353,7 @@
<script setup>
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;
}

View File

@ -325,7 +325,7 @@
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { Empty } from 'ant-design-vue'
import { Empty, Modal, message } from 'ant-design-vue'
import { Search, AlertTriangle, HelpCircle, Trash2, Zap } from 'lucide-vue-next'
import dayjs from 'dayjs'
import PageContainer from '@/components/external-systems/layout/PageContainer.vue'
@ -338,6 +338,7 @@ import { formatRelative, formatFullDateTime } from '@/utils/time'
import { formatCount } from '@/composables/external-systems/format'
import { HEALTH_STATUS_OPTIONS } from '@/composables/external-systems/constants'
import { useHealthCheck } from '@/composables/external-systems/useHealthCheck'
import { normalizeApiError } from '@/composables/external-systems/utils'
defineOptions({ name: 'EsHealthChecks' })
@ -370,7 +371,7 @@ const {
fetchFailing,
fetchUnchecked,
handleCleanup,
handleTrigger,
handleTrigger: triggerHealthCheck,
startPolling,
stopPolling,
restoreStateFromQuery
@ -469,6 +470,32 @@ function openTriggerModal() {
triggerModal.envKey = undefined
}
//
// 400
function isNoToolsError(err) {
const msg = normalizeApiError(err)?.message || ''
return msg.includes('未配置任何工具') || msg.includes('均为工具级')
}
//
// message.error
async function handleTrigger() {
try {
await triggerHealthCheck()
} catch (err) {
if (isNoToolsError(err)) {
Modal.info({
title: '该系统尚未生成工具',
content: '请先通过集成工具生成器创建工具,再触发健康检查。',
okText: '去生成工具',
onOk: () => router.push({ name: 'EsIntegrationGenerator' })
})
} else {
message.error(normalizeApiError(err)?.message || '触发健康检查失败')
}
}
}
// ---- ----
onMounted(() => {
restoreStateFromQuery()

View File

@ -48,6 +48,10 @@ export function useAdapterMetadata() {
const schemaLoading = ref(false)
const schemaError = ref(null)
// ===== 认证插件列表(用于 SystemFormModal 动态填充认证类型选项)=====
const authPlugins = ref([])
const authPluginsLoading = ref(false)
// ===== 校验 =====
const validateResult = ref(null)
const validating = ref(false)
@ -152,6 +156,27 @@ export function useAdapterMetadata() {
}
}
// ===== 认证插件列表 =====
async function fetchAuthPlugins(type) {
if (!type) {
authPlugins.value = []
return
}
authPluginsLoading.value = true
try {
const data = unwrap(await getAdapterAuthPluginsApi(type))
authPlugins.value = data?.items || []
} catch {
authPlugins.value = []
} finally {
authPluginsLoading.value = false
}
}
function resetAuthPlugins() {
authPlugins.value = []
}
// ===== 校验配置 =====
async function validateConfig(type, config) {
validating.value = true
@ -208,15 +233,19 @@ export function useAdapterMetadata() {
schema,
schemaLoading,
schemaError,
authPlugins,
authPluginsLoading,
validateResult,
validating,
fetchOverview,
fetchHealthForAdapters,
fetchDetail,
fetchSchema,
fetchAuthPlugins,
validateConfig,
resetDetail,
resetSchema,
resetAuthPlugins,
resetValidate
}
}

View File

@ -383,7 +383,9 @@ export function useHealthCheck() {
fetchStats()
fetchList()
} catch (e) {
message.error(normalizeApiError(e)?.message || '触发健康检查失败')
// 不在此处统一展示错误,交由调用方根据错误类型决定如何提示
// (例如 HealthCheckView 需识别「未配置任何工具」/「均为工具级」并展示引导式 Modal
throw e
} finally {
triggerModal.loading = false
}

View File

@ -68,6 +68,10 @@ export function useIntegrationGenerator() {
// ===== 选中的工具 slug 列表 =====
const selectedToolSlugs = ref([])
// ===== discover 阶段的 resource 多选 =====
const selectedResources = ref([])
const resourceList = ref([])
// ===== 加载状态 =====
const discoverLoading = ref(false)
const previewLoading = ref(false)
@ -274,7 +278,8 @@ export function useIntegrationGenerator() {
/**
* 步骤①发现资源
* 成功且有资源 自动进入步骤②并加载预览
* 成功后填充 resourceList停留在步骤①让用户勾选 resource
* 再由组件触发 preview() 进入步骤②
* 成功但无资源 停留步骤①展示空状态
*/
async function discover() {
@ -288,18 +293,9 @@ export function useIntegrationGenerator() {
error.value = null
try {
discoverResult.value = unwrap(await discoverIntegrationToolsApi(buildFlowRequest()))
const resources = discoverResult.value?.resources || []
if (resources.length === 0) {
// 无资源,停留步骤①
return false
}
// 进入步骤②,加载预览工具列表
// 先加载预览,成功后再推进步骤,避免 preview 失败时步骤已推进
const previewOk = await preview()
if (previewOk) {
currentStep.value = STEP_PREVIEW
}
return previewOk
resourceList.value = discoverResult.value?.resources || []
selectedResources.value = []
return resourceList.value.length > 0
} catch (e) {
error.value = e
return false
@ -319,7 +315,20 @@ export function useIntegrationGenerator() {
previewLoading.value = true
error.value = null
try {
previewResult.value = unwrap(await previewIntegrationToolsApi(buildFlowRequest()))
const so = buildFlowRequest()
if (selectedResources.value.length > 0) {
const fieldName =
form.source_type === 'salesforce'
? 'selected_sobjects'
: form.source_type === 'hubspot'
? 'selected_objects'
: 'selected_resources'
so.discovery_options = {
...(so.discovery_options || {}),
[fieldName]: selectedResources.value
}
}
previewResult.value = unwrap(await previewIntegrationToolsApi(so))
// 默认勾选所有非冲突项
const tools = previewResult.value?.tools || []
const existing = existingToolSlugs.value
@ -423,10 +432,10 @@ export function useIntegrationGenerator() {
// ===== 弹窗控制 =====
function openModal() {
async function openModal() {
resetFlow()
modalVisible.value = true
loadInitData()
await loadInitData()
}
/**
@ -465,6 +474,8 @@ export function useIntegrationGenerator() {
previewResult.value = null
draftResult.value = null
selectedToolSlugs.value = []
selectedResources.value = []
resourceList.value = []
environmentOptions.value = []
existingToolSlugs.value = new Set()
}
@ -497,6 +508,8 @@ export function useIntegrationGenerator() {
hasUnpersistedDrafts,
// 选中
selectedToolSlugs,
selectedResources,
resourceList,
// 加载状态
discoverLoading,
previewLoading,