- 重构useRequestGuard移除多余逗号,修复语法细节 - 优化API调用格式,统一代码缩进与可读性 - 新增多个共享composable与工具函数:useAccountOptions、useEnvironmentOptions、useSystemOptions、directoryUtils等 - 统一状态枚举与常量定义,对齐后端接口规范 - 修复请求竞态问题,新增批量操作支持 - 优化错误处理与加载状态管理 - 移除重复代码,合并共享逻辑避免多处维护差异
523 lines
15 KiB
JavaScript
523 lines
15 KiB
JavaScript
/**
|
||
* 集成工具生成 - 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-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
|
||
}
|
||
}
|
||
|
||
// ===== 弹窗控制 =====
|
||
|
||
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
|
||
}
|
||
}
|