ForcePilot/web/src/composables/external-systems/useIntegrationGenerator.js
Kris 371c554aa7 refactor: 整理并优化多个composable代码,统一维护逻辑
- 重构useRequestGuard移除多余逗号,修复语法细节
- 优化API调用格式,统一代码缩进与可读性
- 新增多个共享composable与工具函数:useAccountOptions、useEnvironmentOptions、useSystemOptions、directoryUtils等
- 统一状态枚举与常量定义,对齐后端接口规范
- 修复请求竞态问题,新增批量操作支持
- 优化错误处理与加载状态管理
- 移除重复代码,合并共享逻辑避免多处维护差异
2026-07-11 21:53:27 +08:00

523 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 集成工具生成 - 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([])
// ===== 加载状态 =====
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 步流程方法 =====
/**
* 步骤①:发现资源
* 成功且有资源 → 自动进入步骤②并加载预览
* 成功但无资源 → 停留步骤①展示空状态
*/
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-toolsbody 含 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
}
}
// ===== 弹窗控制 =====
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
}
}