ForcePilot/web/src/composables/external-systems/useIntegrationGenerator.js

523 lines
15 KiB
JavaScript
Raw Normal View History

/**
* 集成工具生成 - 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
}
}