/** * 集成工具生成 - 3 步流程状态机 * * 流程: * 步骤① 发现资源 → POST /discover * 步骤② 预览工具 → POST /preview + GET /tools(冲突比对)→ POST /generate * 步骤③ 生成草稿 → POST /integration-tools(持久化,事务型) * * 持久化刷新:persist 不在 composable 内刷新列表,交由组件控制以保持分页状态同步 * * 数据流: * 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([]) // ===== discover 阶段的 resource 多选 ===== const selectedResources = ref([]) const resourceList = 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 ?? [] 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 } try { const data = unwrap(await getEnvironmentListApi({ system_id: systemId, limit: 100 })) environmentOptions.value = data?.items || data || [] } catch (e) { environmentOptions.value = [] throw e } } async function loadExistingToolSlugs(systemId) { if (!systemId) { existingToolSlugs.value = new Set() return } 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) { existingToolSlugs.value = new Set() throw e } } async function handleSystemChange(systemId) { form.env_key = undefined error.value = null const [envResult, slugsResult] = await Promise.allSettled([ loadEnvironments(systemId), loadExistingToolSlugs(systemId) ]) const errors = [] if (envResult.status === 'rejected') { errors.push(envResult.reason) } if (slugsResult.status === 'rejected') { errors.push(slugsResult.reason) } if (errors.length > 0) { error.value = errors.length === 1 ? errors[0] : new Error(errors.map((e) => e.message || String(e)).join(';')) } } // ===== 流程请求构造 ===== function parseDiscoveryOptions() { const raw = discoveryOptionsStr.value?.trim() if (!raw) { // 清空时统一发送空对象,避免下游 handler 对 null 处理不一致 form.discovery_options = {} 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 步流程方法 ===== /** * 步骤①:发现资源 * 成功后填充 resourceList,停留在步骤①让用户勾选 resource, * 再由组件触发 preview() 进入步骤② * 成功但无资源 → 停留步骤①展示空状态 */ 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())) resourceList.value = discoverResult.value?.resources || [] selectedResources.value = [] return resourceList.value.length > 0 } 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 { 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 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 列表 + source_type * 成功 → 关闭弹窗,返回后端实际持久化数量(affected);列表刷新交由组件控制, * 以保持分页状态同步(与 deleteTool 的刷新模式一致) * 失败 → 错误提示,支持重试 * * 后端 PersistGeneratedToolsOutput:{ persisted: [...], affected: int, failed: int, errors: [...] } * source_type 作为三级兜底(tool.source_type → request.source_type → system.source_type), * 透传 form.source_type 保证与 discover/preview/generate 流程一致 */ 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, source_type: form.source_type ?? null }) // 使用后端返回的 affected 字段,而非前端计算的 length const data = unwrap(response) const affected = data?.affected ?? draftTools.value.length resetFlow() modalVisible.value = false 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, systemId) { deleteLoading.value = true error.value = null try { const result = unwrap(await deleteToolApi(toolId, systemId)) return result } catch (e) { error.value = e return false } finally { deleteLoading.value = false } } // ===== 弹窗控制 ===== async function openModal() { resetFlow() modalVisible.value = true await 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 = [] selectedResources.value = [] resourceList.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, selectedResources, resourceList, // 加载状态 discoverLoading, previewLoading, generateLoading, persistLoading, deleteLoading, // 错误 error, // 方法 loadInitData, handleSystemChange, discover, preview, generate, persist, deleteTool, openModal, closeModal, forceClose, back, resetFlow } }