/** * 集成工具生成 - 3 步流程状态机 * * 流程: * 步骤① 发现资源 → POST /discover * 步骤② 预览工具 → POST /preview + GET /tools(冲突比对)→ POST /generate * 步骤③ 生成草稿 → POST /integration-tools(持久化,事务型) * * 数据流: * discover 返回 resources → preview 返回 tools → generate 返回 draft.tool_configs * 前端按步骤②勾选项过滤草稿,persist 仅持久化勾选项 * * 冲突处理: * 后端 override_existing 硬编码 False,冲突 slug 仅能跳过 * 前端比对 GET /tools 已有 slug,冲突项禁用勾选 */ import { ref, reactive, computed } from 'vue' import { getIntegrationToolListApi, discoverIntegrationToolsApi, previewIntegrationToolsApi, generateIntegrationToolsApi, persistIntegrationToolsApi } from '@/apis/external-systems/integration_generator_api' import { getSystemListApi } from '@/apis/external-systems/system_api' import { getEnvironmentListApi } from '@/apis/external-systems/environment_api' import { getToolListApi, deleteToolApi } from '@/apis/external-systems/tool_api' import { getVendorSourceTypesApi } from '@/apis/external-systems/vendor_integration_api' import { unwrap } from './utils' // 步骤常量 const STEP_DISCOVER = 0 const STEP_PREVIEW = 1 const STEP_GENERATE = 2 export function useIntegrationGenerator() { // ===== 列表状态(已持久化的集成工具) ===== const toolList = ref([]) const listLoading = ref(false) const listTotal = ref(0) // ===== 弹窗与步骤 ===== const modalVisible = ref(false) const currentStep = ref(STEP_DISCOVER) // ===== 表单状态 ===== const form = reactive({ system_id: null, env_key: undefined, source_type: undefined, discovery_options: {} }) const discoveryOptionsStr = ref('{}') // ===== 选项数据 ===== const systemOptions = ref([]) const environmentOptions = ref([]) const sourceTypeOptions = ref([]) const existingToolSlugs = ref(new Set()) // ===== 流程结果 ===== const discoverResult = ref(null) const previewResult = ref(null) const draftResult = ref(null) // ===== 选中的工具 slug 列表 ===== const selectedToolSlugs = ref([]) // ===== 加载状态 ===== const discoverLoading = ref(false) const previewLoading = ref(false) const generateLoading = ref(false) const persistLoading = ref(false) const deleteLoading = ref(false) const optionsLoading = ref(false) // ===== 错误状态(组件 watch 后显示 message.error) ===== const error = ref(null) // ===== 计算属性 ===== /** * 预览工具列表(带冲突标记) * 冲突定义:preview 返回的 slug 已存在于 GET /tools 结果中 */ const previewTools = computed(() => { const tools = previewResult.value?.tools || [] return tools.map((tool) => ({ ...tool, is_conflict: existingToolSlugs.value.has(tool.slug) })) }) const previewStats = computed(() => { const total = previewTools.value.length const conflictCount = previewTools.value.filter((t) => t.is_conflict).length const newCount = total - conflictCount return { total, newCount, conflictCount } }) /** * 草稿列表(按步骤②勾选过滤) * * 后端 GenerateToolsDraftOutput 结构: * { system_id, source_type, draft: { tool_configs, override_existing } } * 前端按勾选项过滤草稿后展示与持久化 */ const draftTools = computed(() => { const configs = draftResult.value?.draft?.tool_configs || [] return configs.filter((config) => selectedToolSlugs.value.includes(config.slug)) }) const hasUnpersistedDrafts = computed( () => currentStep.value === STEP_GENERATE && draftTools.value.length > 0 ) // ===== 列表加载 ===== async function fetchToolList(params = {}) { listLoading.value = true error.value = null try { const data = unwrap(await getIntegrationToolListApi(params)) toolList.value = data?.items || data || [] listTotal.value = data?.total || toolList.value.length } catch (e) { error.value = e } finally { listLoading.value = false } } // ===== 选项加载 ===== async function loadInitData() { optionsLoading.value = true error.value = null try { const [systemsRes, sourceTypesRes] = await Promise.all([ getSystemListApi({ limit: 500 }), getVendorSourceTypesApi() ]) const systemsData = unwrap(systemsRes) systemOptions.value = systemsData?.items || systemsData || [] // 后端返回 { items: [{ source_type, operations }], total }, // a-select options 需要 { label, value } 格式 const sourceTypesData = unwrap(sourceTypesRes) const sourceTypeItems = sourceTypesData?.items || [] sourceTypeOptions.value = sourceTypeItems.map((item) => ({ label: item.source_type, value: item.source_type })) } catch (e) { error.value = e } finally { optionsLoading.value = false } } async function loadEnvironments(systemId) { if (!systemId) { environmentOptions.value = [] return } error.value = null try { const data = unwrap(await getEnvironmentListApi({ system_id: systemId, limit: 100 })) environmentOptions.value = data?.items || data || [] } catch (e) { error.value = e } } async function loadExistingToolSlugs(systemId) { if (!systemId) { existingToolSlugs.value = new Set() return } error.value = null try { const data = unwrap(await getToolListApi({ system_id: systemId, limit: 500 })) const tools = data?.items || data || [] existingToolSlugs.value = new Set(tools.map((t) => t.slug)) } catch (e) { error.value = e } } async function handleSystemChange(systemId) { form.env_key = undefined await Promise.all([loadEnvironments(systemId), loadExistingToolSlugs(systemId)]) } // ===== 流程请求构造 ===== function parseDiscoveryOptions() { const raw = discoveryOptionsStr.value?.trim() if (!raw) { form.discovery_options = null return true } try { form.discovery_options = JSON.parse(raw) return true } catch { error.value = new Error('发现选项必须为合法 JSON') return false } } function buildFlowRequest() { return { system_id: form.system_id, // 使用 ?? 而非 ||,避免空字符串/0 被误转 null; // discovery_options 为空对象 {} 时应保留(语义为"无特殊选项"),不应转 null source_type: form.source_type ?? null, env_key: form.env_key ?? null, discovery_options: form.discovery_options ?? null } } // 字段校验规则(对齐后端 ExternalToolItemRequest 约束) const SLUG_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$/ /** * 持久化前字段级校验(对齐后端 ExternalToolItemRequest 约束) * - slug: 正则 ^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$,长度 1-128 * - name: 长度 1-128 * - timeout: 1-300 * * @returns {string|null} 首个错误信息,null 表示通过 */ function validateToolsBeforePersist(tools) { for (const tool of tools) { if (!tool.slug || !SLUG_PATTERN.test(tool.slug)) { return `工具 slug 格式不合法:${tool.slug || '(空)'}(需匹配 ^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$)` } if (!tool.name || tool.name.length > 128) { return `工具 name 长度需在 1-128 之间:${tool.name || '(空)'}` } if (tool.timeout === undefined || tool.timeout === null || tool.timeout < 1 || tool.timeout > 300) { return `工具 timeout 需在 1-300 之间:${tool.timeout}` } } return null } // ===== 3 步流程方法 ===== /** * 步骤①:发现资源 * 成功且有资源 → 自动进入步骤②并加载预览 * 成功但无资源 → 停留步骤①展示空状态 */ async function discover() { if (!form.system_id) { error.value = new Error('请选择系统') return false } if (!parseDiscoveryOptions()) return false discoverLoading.value = true 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 } catch (e) { error.value = e return false } finally { discoverLoading.value = false } } /** * 步骤②:预览工具列表 * 调用 POST /preview 获取工具列表 * 默认勾选所有「新增」项,冲突项不可勾选 * * @returns {boolean} 是否成功 */ async function preview() { previewLoading.value = true error.value = null try { previewResult.value = unwrap(await previewIntegrationToolsApi(buildFlowRequest())) // 默认勾选所有非冲突项 const tools = previewResult.value?.tools || [] const existing = existingToolSlugs.value selectedToolSlugs.value = tools.filter((t) => !existing.has(t.slug)).map((t) => t.slug) return true } catch (e) { error.value = e return false } finally { previewLoading.value = false } } /** * 步骤② → 步骤③:生成草稿 * 调用 POST /generate 生成全量草稿(不持久化) * 前端按步骤②勾选项过滤草稿列表 */ async function generate() { if (selectedToolSlugs.value.length === 0) { error.value = new Error('请至少选择 1 个工具') return false } generateLoading.value = true error.value = null try { draftResult.value = unwrap(await generateIntegrationToolsApi(buildFlowRequest())) currentStep.value = STEP_GENERATE return true } catch (e) { error.value = e return false } finally { generateLoading.value = false } } /** * 步骤③:持久化草稿(事务型全量成功/失败) * 调用 POST /integration-tools,body 含 system_id + 过滤后的 tools 列表 * 成功 → 关闭弹窗 + 刷新列表,返回后端实际持久化数量(affected) * 失败 → 错误提示,支持重试 * * 后端 PersistGeneratedToolsOutput:{ persisted: [...], affected: int } */ async function persist() { if (draftTools.value.length === 0) { error.value = new Error('持久化工具列表不能为空') return false } // 字段级校验前置(对齐后端 ExternalToolItemRequest 约束) const validateError = validateToolsBeforePersist(draftTools.value) if (validateError) { error.value = new Error(validateError) return false } persistLoading.value = true error.value = null try { const response = await persistIntegrationToolsApi({ system_id: form.system_id, tools: draftTools.value }) // 使用后端返回的 affected 字段,而非前端计算的 length const data = unwrap(response) const affected = data?.affected ?? draftTools.value.length resetFlow() modalVisible.value = false await fetchToolList() return affected } catch (e) { error.value = e return false } finally { persistLoading.value = false } } /** * 删除已持久化的工具 * 调用 DELETE /tools/{tool_id},system_id 为可选 query * 成功 → 返回后端响应,由组件刷新列表 * 失败 → 设置 error,返回 false */ async function deleteTool(toolId) { deleteLoading.value = true error.value = null try { const result = unwrap(await deleteToolApi(toolId)) return result } catch (e) { error.value = e return false } finally { deleteLoading.value = false } } // ===== 弹窗控制 ===== function openModal() { resetFlow() modalVisible.value = true loadInitData() } /** * 关闭弹窗:若有未持久化草稿,返回 false 由组件触发二次确认 */ function closeModal() { if (hasUnpersistedDrafts.value) { return false } modalVisible.value = false resetFlow() return true } function forceClose() { modalVisible.value = false resetFlow() } function back() { if (currentStep.value > STEP_DISCOVER) { currentStep.value-- } } // ===== 重置 ===== function resetFlow() { currentStep.value = STEP_DISCOVER form.system_id = null form.env_key = undefined form.source_type = undefined form.discovery_options = {} discoveryOptionsStr.value = '{}' discoverResult.value = null previewResult.value = null draftResult.value = null selectedToolSlugs.value = [] environmentOptions.value = [] existingToolSlugs.value = new Set() } return { // 列表 toolList, listLoading, listTotal, fetchToolList, // 弹窗与步骤 modalVisible, currentStep, // 表单 form, discoveryOptionsStr, // 选项 systemOptions, environmentOptions, sourceTypeOptions, existingToolSlugs, optionsLoading, // 流程结果 discoverResult, previewResult, draftResult, previewTools, previewStats, draftTools, hasUnpersistedDrafts, // 选中 selectedToolSlugs, // 加载状态 discoverLoading, previewLoading, generateLoading, persistLoading, deleteLoading, // 错误 error, // 方法 loadInitData, handleSystemChange, discover, preview, generate, persist, deleteTool, openModal, closeModal, forceClose, back, resetFlow } }