Browse Source

feat: AI工作流

Lesan 4 months ago
parent
commit
ce3b95d1ec

+ 25 - 0
src/api/ai/workflow/index.ts

@@ -0,0 +1,25 @@
+import request from '@/config/axios'
+
+export const getWorkflowPage = async (params) => {
+  return await request.get({ url: '/ai/workflow/page', params })
+}
+
+export const getWorkflow = async (id) => {
+  return await request.get({ url: '/ai/workflow/get?id=' + id })
+}
+
+export const createWorkflow = async (data) => {
+  return await request.post({ url: '/ai/workflow/create', data })
+}
+
+export const updateWorkflow = async (data) => {
+  return await request.put({ url: '/ai/workflow/update', data })
+}
+
+export const deleteWorkflow = async (id) => {
+  return await request.delete({ url: '/ai/workflow/delete?id=' + id })
+}
+
+export const updateWorkflowModel = async (data) => {
+  return await request.put({ url: '/ai/workflow/updateWorkflowModel', data })
+}

+ 63 - 0
src/components/Tinyflow/Tinyflow.vue

@@ -0,0 +1,63 @@
+<template>
+  <div ref="divRef" :class="['tinyflow', className]" :style="style" style="height: 100%"> </div>
+</template>
+
+<script setup lang="ts">
+import { Item, Tinyflow as TinyflowNative } from './ui'
+import './ui/index.css'
+import { onMounted, onUnmounted, ref } from 'vue'
+
+const props = defineProps<{
+  className?: string
+  style?: Record<string, string>
+  data?: Record<string, any>
+  provider?: {
+    llm?: () => Item[] | Promise<Item[]>
+    knowledge?: () => Item[] | Promise<Item[]>
+    internal?: () => Item[] | Promise<Item[]>
+  }
+}>()
+
+const divRef = ref<HTMLDivElement | null>(null)
+let tinyflow: TinyflowNative | null = null
+// 定义默认的 provider 方法
+const defaultProvider = {
+  llm: () => [] as Item[],
+  knowledge: () => [] as Item[],
+  internal: () => [] as Item[]
+}
+
+onMounted(() => {
+  if (divRef.value) {
+    // 合并默认 provider 和传入的 props.provider
+    const mergedProvider = {
+      ...defaultProvider,
+      ...props.provider
+    }
+    tinyflow = new TinyflowNative({
+      element: divRef.value as Element,
+      data: props.data || {},
+      provider: mergedProvider
+    })
+  }
+})
+
+onUnmounted(() => {
+  if (tinyflow) {
+    tinyflow.destroy()
+    tinyflow = null
+  }
+})
+
+const getData = () => {
+  if (tinyflow) {
+    return tinyflow.getData()
+  }
+  console.warn('Tinyflow instance is not initialized')
+  return null
+}
+
+defineExpose({
+  getData
+})
+</script>

File diff suppressed because it is too large
+ 0 - 0
src/components/Tinyflow/ui/index.css


+ 41 - 0
src/components/Tinyflow/ui/index.d.ts

@@ -0,0 +1,41 @@
+import { Edge } from '@xyflow/svelte';
+import { Node as Node_2 } from '@xyflow/svelte';
+import { useSvelteFlow } from '@xyflow/svelte';
+import { Viewport } from '@xyflow/svelte';
+
+export declare type Item = {
+    value: number | string;
+    label: string;
+    children?: Item[];
+};
+
+export declare class Tinyflow {
+    private options;
+    private rootEl;
+    private svelteFlowInstance;
+    constructor(options: TinyflowOptions);
+    private _init;
+    private _setOptions;
+    getOptions(): TinyflowOptions;
+    getData(): {
+        nodes: Node_2[];
+        edges: Edge[];
+        viewport: Viewport;
+    };
+    setData(data: TinyflowData): void;
+    destroy(): void;
+}
+
+export declare type TinyflowData = Partial<ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>>;
+
+export declare type TinyflowOptions = {
+    element: string | Element;
+    data?: TinyflowData;
+    provider?: {
+        llm?: () => Item[] | Promise<Item[]>;
+        knowledge?: () => Item[] | Promise<Item[]>;
+        internal?: () => Item[] | Promise<Item[]>;
+    };
+};
+
+export { }

File diff suppressed because it is too large
+ 10206 - 0
src/components/Tinyflow/ui/index.js


File diff suppressed because it is too large
+ 0 - 0
src/components/Tinyflow/ui/index.umd.js


+ 12 - 0
src/router/modules/remaining.ts

@@ -667,6 +667,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           activeMenu: '/ai/knowledge'
         }
+      },
+      {
+        path: 'console/workflow/:type/:id',
+        component: () => import('@/views/ai/workflow/manager/WorkflowModelForm.vue'),
+        name: 'AiWorkflowUpdate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '修改AI工作流',
+          activeMenu: '/ai/console/workflow'
+        }
       }
     ]
   },

+ 106 - 0
src/views/ai/workflow/manager/WorkflowForm.vue

@@ -0,0 +1,106 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="流程标识" prop="definitionKey">
+            <el-input v-model="formData.definitionKey" placeholder="请输入流程标识" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="流程名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入流程名称" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as WorkflowApi from '@/api/ai/workflow'
+import { FormRules } from 'element-plus'
+
+defineOptions({ name: 'AiWorkflowForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  definitionKey: '',
+  name: ''
+})
+const formRules = reactive<FormRules>({
+  definitionKey: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await WorkflowApi.getWorkflow(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await WorkflowApi.createWorkflow(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await WorkflowApi.updateWorkflow(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    definitionKey: '',
+    name: ''
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 77 - 0
src/views/ai/workflow/manager/WorkflowModelForm.vue

@@ -0,0 +1,77 @@
+<template>
+  <div style="width: 100%; height: calc(100vh - 160px)">
+    <Tinyflow
+      ref="tinyflowRef"
+      :className="'custom-class'"
+      :style="{ width: '100%', height: '100%' }"
+      v-if="initialData"
+      :data="initialData"
+      :provider="provider"
+    />
+  </div>
+  <div class="absolute top-30px right-30px">
+    <el-button @click="updateWorkflowModel" type="primary" v-hasPermi="['ai:workflow:update']">保存</el-button>
+    <el-button @click="testWorkflowModel" type="primary" v-hasPermi="['ai:workflow:test']">测试</el-button>
+  </div>
+</template>
+
+<script setup lang="ts">
+import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
+import * as WorkflowApi from '@/api/ai/workflow'
+import { ApiKeyApi } from '@/api/ai/model/apiKey'
+
+const route = useRoute()
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const tinyflowRef = ref()
+const provider = ref({ llm: () => [], knowledge: () => [], internal: () => [] })
+const initialData = ref()
+
+const loadData = async () => {
+  try {
+    const [apiKeys, flowData] = await Promise.all([
+      ApiKeyApi.getApiKeySimpleList(),
+      WorkflowApi.getWorkflow(route.params.id)
+    ])
+
+    // 更新 provider
+    provider.value = {
+      llm: () =>
+        apiKeys.map(({ id, name }) => ({
+          value: id,
+          label: name
+        })),
+      knowledge: () => [],
+      internal: () => []
+    }
+
+    // 更新流程图数据
+    initialData.value = JSON.parse(flowData.model)
+  } catch {}
+}
+
+const updateWorkflowModel = async () => {
+  try {
+    const model = tinyflowRef.value.getData()
+    const data = {
+      model: JSON.stringify(model),
+      id: route.params.id
+    }
+    await message.confirm('确认保存流程模型?')
+    await WorkflowApi.updateWorkflowModel(data)
+    message.success(t('common.updateSuccess'))
+    await loadData()
+  } catch {}
+}
+
+const testWorkflowModel = () => {
+  // TODO @lesan 测试
+}
+
+watchEffect(() => {
+  if (route.params.id) {
+    loadData()
+  }
+})
+</script>

+ 193 - 0
src/views/ai/workflow/manager/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="流程标识" prop="definitionKey">
+        <el-input
+          v-model="queryParams.definitionKey"
+          placeholder="请输入流程标识"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="流程名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入流程名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['ai:workflow:create']">
+          <Icon icon="ep:plus" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column
+        label="流程标识"
+        align="center"
+        prop="definitionKey"
+        :show-overflow-tooltip="true"
+      />
+      <el-table-column label="流程名称" align="center" prop="name" :show-overflow-tooltip="true" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="操作" align="center" width="220" fixed="right">
+        <template #default="scope">
+          <el-button
+            type="primary"
+            link
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['ai:workflow:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            type="primary"
+            link
+            @click="openModelForm('update', scope.row.id)"
+            v-hasPermi="['ai:workflow:update']"
+          >
+            流程图
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:workflow:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 添加或修改工作流对话框 -->
+  <WorkflowForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import * as WorkflowApi from '@/api/ai/workflow'
+import { dateFormatter } from '@/utils/formatTime'
+import WorkflowForm from './WorkflowForm.vue'
+
+/** AI 绘画 列表 */
+defineOptions({ name: 'AiWorkflowManager' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  definitionKey: '',
+  name: '',
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await WorkflowApi.getWorkflowPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await WorkflowApi.deleteWorkflow(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 修改流程模型弹窗 */
+const openModelForm = async (type: string, id?: number) => {
+  if (type === 'create') {
+    await push({ name: 'AiWorkflowCreate' })
+  } else {
+    await push({
+      name: 'AiWorkflowUpdate',
+      params: { id, type }
+    })
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  getList()
+})
+</script>

Some files were not shown because too many files changed in this diff