This commit is contained in:
YunaiV 2025-05-08 19:32:08 +08:00
commit 15f5963e68
21 changed files with 407 additions and 32 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -20,6 +20,6 @@ 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 })
export const testWorkflow = async (data) => {
return await request.post({ url: '/ai/workflow/test', data })
}

View File

@ -41,6 +41,11 @@ export const getTenant = (id: number) => {
return request.get({ url: '/system/tenant/get?id=' + id })
}
// 获取租户精简信息列表
export const getTenantList = () => {
return request.get({ url: '/system/tenant/simple-list' })
}
// 新增租户
export const createTenant = (data: TenantVO) => {
return request.post({ url: '/system/tenant/create', data })

View File

@ -91,7 +91,7 @@ const dialogStyle = computed(() => {
icon="ep:close"
hover-color="var(--el-color-primary)"
color="var(--el-color-info)"
@click="close"
@click.stop="close"
/>
</div>
</div>

View File

@ -237,7 +237,7 @@ const props = defineProps({
const prefix = inject('prefix')
const width = inject('width')
const formKey = ref('')
const formKey = ref(undefined)
const businessKey = ref('')
const optionModelTitle = ref('')
const fieldList = ref<any[]>([])
@ -462,6 +462,7 @@ const updateElementExtensions = () => {
const formList = ref([]) //
onMounted(async () => {
formList.value = await FormApi.getFormSimpleList()
formKey.value = parseInt(formKey.value)
})
watch(

View File

@ -3,7 +3,14 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import qs from 'qs'
import { config } from '@/config/axios/config'
import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
import {
getAccessToken,
getRefreshToken,
getTenantId,
getVisitTenantId,
removeToken,
setToken
} from '@/utils/auth'
import errorCode from './errorCode'
import { resetRouter } from '@/router'
@ -24,7 +31,7 @@ export const isRelogin = { show: false }
let requestList: any[] = []
// 是否正在刷新中
let isRefreshToken = false
// 请求白名单,无须token的接口
// 请求白名单,无须 token 的接口
const whiteList: string[] = ['/login', '/refresh-token']
// 创建axios实例
@ -55,6 +62,11 @@ service.interceptors.request.use(
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
if (tenantId) config.headers['tenant-id'] = tenantId
// 只有登录时,才设置 visit-tenant-id 访问租户
const visitTenantId = getVisitTenantId()
if (config.headers.Authorization && visitTenantId) {
config.headers['visit-tenant-id'] = visitTenantId
}
}
const method = config.method?.toUpperCase()
// 防止 GET 请求缓存

View File

@ -10,6 +10,7 @@ export const CACHE_KEY = {
// 用户相关
ROLE_ROUTERS: 'roleRouters',
USER: 'user',
VisitTenantId: 'visitTenantId',
// 系统设置
IS_DARK: 'isDark',
LANG: 'lang',
@ -35,5 +36,6 @@ export const deleteUserCache = () => {
const { wsCache } = useCache()
wsCache.delete(CACHE_KEY.USER)
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
wsCache.delete(CACHE_KEY.VisitTenantId)
// 注意,不要清理 LoginForm 登录表单
}

View File

@ -0,0 +1,46 @@
<template>
<div>
<el-select
filterable
placeholder="请选择租户"
class="!w-180px"
v-model="value"
@change="handleChange"
clearable
>
<el-option v-for="item in tenants" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import * as TenantApi from '@/api/system/tenant'
import { getVisitTenantId, setVisitTenantId } from '@/utils/auth'
import { useMessage } from '@/hooks/web/useMessage'
import { useTagsView } from '@/hooks/web/useTagsView'
const message = useMessage() //
const tagsView = useTagsView() //
const value = ref(getVisitTenantId()) // ID
const tenants = ref<any[]>([]) //
const handleChange = (id: number) => {
// 访 ID
setVisitTenantId(id)
//
tagsView.closeOther()
//
tagsView.refreshPage()
//
const tenant = tenants.value.find((item) => item.id === id)
if (tenant) {
message.success(`切换当前租户为: ${tenant.name}`)
}
}
onMounted(async () => {
tenants.value = await TenantApi.getTenantList()
})
</script>

View File

@ -8,8 +8,10 @@ import { Breadcrumb } from '@/layout/components/Breadcrumb'
import { SizeDropdown } from '@/layout/components/SizeDropdown'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import RouterSearch from '@/components/RouterSearch/index.vue'
import TenantVisit from '@/layout/components/TenantVisit/index.vue'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
import { checkPermi } from '@/utils/permission'
const { getPrefixCls, variables } = useDesign()
@ -41,6 +43,11 @@ const locale = computed(() => appStore.getLocale)
//
const message = computed(() => appStore.getMessage)
//
const hasTenantVisitPermission = computed(
() => import.meta.env.VITE_APP_TENANT_ENABLE === 'true' && checkPermi(['system:tenant:visit'])
)
export default defineComponent({
name: 'ToolHeader',
setup() {
@ -62,6 +69,7 @@ export default defineComponent({
</div>
) : undefined}
<div class="h-full flex items-center">
{hasTenantVisitPermission.value ? <TenantVisit /> : undefined}
{screenfull.value ? (
<Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
) : undefined}

View File

@ -67,6 +67,14 @@ export const getTenantId = () => {
return wsCache.get(CACHE_KEY.TenantId)
}
export const setTenantId = (username: string) => {
wsCache.set(CACHE_KEY.TenantId, username)
export const setTenantId = (tenantId: number) => {
wsCache.set(CACHE_KEY.TenantId, tenantId)
}
export const getVisitTenantId = () => {
return wsCache.get(CACHE_KEY.VisitTenantId)
}
export const setVisitTenantId = (visitTenantId: number) => {
wsCache.set(CACHE_KEY.VisitTenantId, visitTenantId)
}

View File

@ -462,6 +462,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
(error) => {
message.alert(`对话异常! ${error}`)
stopStream()
//
throw error
},
() => {
stopStream()

View File

@ -80,6 +80,8 @@ const submit = (data: AiMindMapGenerateReqVO) => {
onError(err) {
console.error('生成思维导图失败', err)
stopStream()
//
throw error
},
ctrl: ctrl.value
})

View File

@ -4,7 +4,7 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
label-width="130px"
v-loading="formLoading"
>
<el-form-item label="所属平台" prop="platform">
@ -146,7 +146,10 @@ const formRules = reactive({
platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
type: [{ required: true, message: '模型类型不能为空', trigger: 'blur' }],
sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
temperature: [{ required: true, message: '温度参数不能为空', trigger: 'blur' }],
maxTokens: [{ required: true, message: '回复数 Token 数不能为空', trigger: 'blur' }],
maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const apiKeyList = ref([] as ApiKeyVO[]) // API

View File

@ -13,11 +13,58 @@
测试
</el-button>
</div>
<!-- 测试窗口 -->
<el-drawer v-model="showTestDrawer" title="工作流测试" :modal="false">
<fieldset>
<legend class="ml-15px"><h3>运行参数配置</h3></legend>
<div class="p-20px">
<div
class="flex justify-around mb-10px"
v-for="(param, index) in params4Test"
:key="index"
>
<el-select class="w-200px!" v-model="param.key" placeholder="参数名">
<el-option
v-for="(value, key) in paramsOfStartNode"
:key="key"
:label="value?.description || key"
:value="key"
:disabled="!!value?.disabled"
/>
</el-select>
<el-input class="w-200px!" v-model="param.value" placeholder="参数值" />
<el-button type="danger" plain :icon="Delete" circle @click="removeParam(index)" />
</div>
<!-- TODO @lesan是不是不用添加和删除参数直接把必填和选填列出来然后加上参数校验 -->
<el-button type="primary" plain @click="addParam">添加参数</el-button>
</div>
</fieldset>
<fieldset class="mt-20px bg-#f8f9fa">
<legend class="ml-15px"><h3>运行结果</h3></legend>
<div class="p-20px">
<div v-if="loading"> <el-text type="primary">执行中...</el-text></div>
<div v-else-if="error">
<el-text type="danger">{{ error }}</el-text>
</div>
<pre v-else-if="testResult" class="result-content"
>{{ JSON.stringify(testResult, null, 2) }}
</pre>
<div v-else> <el-text type="info">点击运行查看结果</el-text> </div>
</div>
</fieldset>
<el-button class="mt-20px w-100%" size="large" type="success" @click="goRun">
运行流程
</el-button>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
import * as WorkflowApi from '@/api/ai/workflow'
// TODO @lesan使 ICon
import { Delete } from '@element-plus/icons-vue'
defineProps<{
provider: any
@ -25,9 +72,149 @@ defineProps<{
const tinyflowRef = ref()
const workflowData = inject('workflowData') as Ref
const showTestDrawer = ref(false)
const params4Test = ref([])
const paramsOfStartNode = ref({})
const testResult = ref(null)
const loading = ref(false)
const error = ref(null)
/** 展示工作流测试抽屉 */
const testWorkflowModel = () => {
// TODO @lesan
showTestDrawer.value = !showTestDrawer.value
}
/** 运行流程 */
const goRun = async () => {
try {
const val = tinyflowRef.value.getData()
loading.value = true
error.value = null
testResult.value = null
/// start
const startNode = getStartNode()
//
const parameters = startNode.data?.parameters || []
const paramDefinitions = {}
parameters.forEach((param) => {
paramDefinitions[param.name] = param.dataType
})
//
const convertedParams = {}
for (const { key, value } of params4Test.value) {
const paramKey = key.trim()
if (!paramKey) continue
let dataType = paramDefinitions[paramKey]
if (!dataType) {
dataType = 'String'
}
try {
convertedParams[paramKey] = convertParamValue(value, dataType)
} catch (e) {
throw new Error(`参数 ${paramKey} 转换失败: ${e.message}`)
}
}
const data = {
graph: JSON.stringify(val),
params: convertedParams
}
const response = await WorkflowApi.testWorkflow(data)
testResult.value = response
} catch (err) {
error.value = err.response?.data?.message || '运行失败,请检查参数和网络连接'
} finally {
loading.value = false
}
}
/** 监听测试抽屉的开启,获取开始节点参数列表 */
watch(showTestDrawer, (value) => {
if (!value) return
/// start
const startNode = getStartNode()
//
const parameters = startNode.data?.parameters || []
const paramDefinitions = {}
// 便
parameters.forEach((param) => {
paramDefinitions[param.name] = param
})
function mergeIfRequiredButNotSet(target) {
let needPushList = []
for (let key in paramDefinitions) {
let param = paramDefinitions[key]
if (param.required) {
let item = target.find((item) => item.key === key)
if (!item) {
needPushList.push({ key: param.name, value: param.defaultValue || '' })
}
}
}
target.push(...needPushList)
}
//
mergeIfRequiredButNotSet(params4Test.value)
paramsOfStartNode.value = paramDefinitions
})
/** 获取开始节点 */
const getStartNode = () => {
const val = tinyflowRef.value.getData()
const startNode = val.nodes.find((node) => node.type === 'startNode')
if (!startNode) {
throw new Error('流程缺少开始节点')
}
return startNode
}
/** 添加参数项 */
const addParam = () => {
params4Test.value.push({ key: '', value: '' })
}
/** 删除参数项 */
const removeParam = (index) => {
params4Test.value.splice(index, 1)
}
/** 类型转换函数 */
const convertParamValue = (value, dataType) => {
if (value === '') return null //
switch (dataType) {
case 'String':
return String(value)
case 'Number':
const num = Number(value)
if (isNaN(num)) throw new Error('非数字格式')
return num
case 'Boolean':
if (value.toLowerCase() === 'true') return true
if (value.toLowerCase() === 'false') return false
throw new Error('必须为 true/false')
case 'Object':
case 'Array':
try {
return JSON.parse(value)
} catch (e) {
throw new Error(`JSON格式错误: ${e.message}`)
}
default:
throw new Error(`不支持的类型: ${dataType}`)
}
}
/** 表单校验 */
@ -47,3 +234,17 @@ defineExpose({
validate
})
</script>
<style lang="css" scoped>
.result-content {
background: white;
padding: 12px;
border-radius: 4px;
max-height: 300px;
overflow: auto;
font-family: Monaco, Consolas, monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
}
</style>

View File

@ -59,7 +59,7 @@
<WorkflowDesign
v-if="currentStep === 1"
v-model="formData"
:provider="provider"
:provider="llmProvider"
ref="workflowDesignRef"
/>
</div>
@ -73,7 +73,8 @@ import { CommonStatusEnum } from '@/utils/constants'
import * as WorkflowApi from '@/api/ai/workflow'
import BasicInfo from './BasicInfo.vue'
import WorkflowDesign from './WorkflowDesign.vue'
import { ApiKeyApi } from '@/api/ai/model/apiKey'
import { ModelApi } from '@/api/ai/model/model'
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
const router = useRouter()
const { delView } = useTagsViewStore()
@ -104,31 +105,35 @@ const formData: any = ref({
graph: '',
status: CommonStatusEnum.ENABLE
})
// TODO @lesan
const provider = ref<any>()
const llmProvider = ref<any>([])
const workflowData = ref<any>({})
provide('workflowData', workflowData)
/** 初始化数据 */
const actionType = route.params.type as string
const initData = async () => {
//
if (actionType === 'update') {
const workflowId = route.params.id as string
formData.value = await WorkflowApi.getWorkflow(workflowId)
workflowData.value = JSON.parse(formData.value.graph)
}
const apiKeys = await ApiKeyApi.getApiKeySimpleList()
provider.value = {
//
const models = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
llmProvider.value = {
llm: () =>
apiKeys.map(({ id, name }) => ({
models.map(({ id, name }) => ({
value: id,
label: name
})),
knowledge: () => [],
internal: () => []
}
// TODO @lesan knowledge
// TODO @lesan pr
//
currentStep.value = 0
}
@ -164,17 +169,17 @@ const handleSave = async () => {
//
const data = {
...formData.value
...formData.value,
graph: JSON.stringify(workflowData.value)
}
data.graph = JSON.stringify(workflowData.value)
if (actionType === 'update') {
await WorkflowApi.updateWorkflow(data)
} else {
await WorkflowApi.createWorkflow(data)
}
//
message.success('保存成功')
delView(unref(router.currentRoute))
await router.push({ name: 'AiWorkflow' })
} catch (error: any) {

View File

@ -57,9 +57,11 @@ const submit = (data: WriteVO) => {
},
ctrl: abortController.value,
onClose: stopStream,
onError: (...err) => {
console.error('写作异常', ...err)
onError: (error) => {
console.error('写作异常', error)
stopStream()
//
throw error
}
})
}

View File

@ -1,8 +1,8 @@
import type { UploadProps, UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth'
import { getRefreshToken } from '@/utils/auth'
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
const HEADERS = { Authorization: 'Bearer ' + getRefreshToken() } // 请求头(解决 el-upload 上传过程中,无法刷新令牌的问题)
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
interface UploadData {

View File

@ -71,11 +71,12 @@
>
<el-input
v-model="formData.config.keyContent"
:autosize="{ minRows: 8, maxRows: 8 }"
:autosize="{ minRows: 2, maxRows: 4 }"
:style="{ width: '100%' }"
placeholder="请上传 apiclient_cert.p12 证书"
readonly
type="textarea"
:rows="2"
/>
</el-form-item>
<el-form-item label="" label-width="180px">
@ -108,11 +109,12 @@
>
<el-input
v-model="formData.config.privateKeyContent"
:autosize="{ minRows: 8, maxRows: 8 }"
:autosize="{ minRows: 2, maxRows: 4 }"
:style="{ width: '100%' }"
placeholder="请上传 apiclient_key.pem 证书"
readonly
type="textarea"
:rows="2"
/>
</el-form-item>
<el-form-item label="" label-width="180px" prop="privateKeyContentFile">
@ -145,6 +147,47 @@
前往微信商户平台查看证书序列号
</a>
</el-form-item>
<el-form-item label="public_key.pem 证书" label-width="180px" prop="config.publicKeyContent">
<el-input
v-model="formData.config.publicKeyContent"
:autosize="{ minRows: 2, maxRows: 4 }"
:style="{ width: '100%' }"
placeholder="请上传 public_key.pem 证书"
readonly
type="textarea"
:rows="2"
/>
</el-form-item>
<el-form-item label="" label-width="180px" prop="publicKeyContentFile">
<el-upload
ref="publicKeyContentFile"
:before-upload="pemFileBeforeUpload"
:http-request="publicKeyContentUpload"
:limit="1"
accept=".pem"
action=""
>
<el-button type="primary">
<Icon class="mr-5px" icon="ep:upload" />
点击上传
</el-button>
</el-upload>
</el-form-item>
<el-form-item label="公钥 ID" label-width="180px" prop="config.publicKeyId">
<el-input
v-model="formData.config.publicKeyId"
clearable
placeholder="请输入公钥 ID"
/>
</el-form-item>
<el-form-item label-width="180px">
<a
href="https://pay.weixin.qq.com/doc/v3/merchant/4012153196"
target="_blank"
>
微信支付公钥产品简介及使用说明
</a>
</el-form-item>
</div>
<el-form-item label="备注" label-width="180px" prop="remark">
<el-input v-model="formData.remark" :style="{ width: '100%' }" />
@ -184,7 +227,9 @@ const formData = ref<any>({
keyContent: '',
privateKeyContent: '',
certSerialNo: '',
apiV3Key: ''
apiV3Key: '',
publicKeyContent: '',
publicKeyId: ''
}
})
const formRules = {
@ -201,6 +246,8 @@ const formRules = {
{ required: true, message: '请上传 apiclient_key.pem 证书', trigger: 'blur' }
],
'config.certSerialNo': [{ required: true, message: '请输入证书序列号', trigger: 'blur' }],
'config.publicKeyContent': [{ required: true, message: '请上传 public_key.pem 证书', trigger: 'blur' }],
'config.publicKeyId': [{ required: true, message: '请输入公钥 ID', trigger: 'blur' }],
'config.apiV3Key': [{ required: true, message: '请上传 api V3 密钥值', trigger: 'blur' }]
}
const formRef = ref() // Ref
@ -267,7 +314,9 @@ const resetForm = (appId, code) => {
keyContent: '',
privateKeyContent: '',
certSerialNo: '',
apiV3Key: ''
apiV3Key: '',
publicKeyContent: '',
publicKeyId: ''
}
}
formRef.value?.resetFields()
@ -318,4 +367,15 @@ const keyContentUpload = async (event) => {
}
readFile.readAsDataURL(event.file) // base64
}
/**
* 读取 public_key.pem publicKeyContent 字段
*/
const publicKeyContentUpload = async (event) => {
const readFile = new FileReader()
readFile.onload = (e: any) => {
formData.value.config.publicKeyContent = e.target.result
}
readFile.readAsText(event.file)
}
</script>

View File

@ -6,7 +6,11 @@
</ContentWrap>
</template>
<script lang="ts" setup>
import { getAccessToken, getRefreshToken } from '@/utils/auth'
defineOptions({ name: 'GoView' })
const src = ref(import.meta.env.VITE_GOVIEW_URL)
const src = ref(
`${import.meta.env.VITE_GOVIEW_URL}?accessToken=${getAccessToken()}&refreshToken=${getRefreshToken()}`
)
</script>

View File

@ -0,0 +1,15 @@
<template>
<doc-alert title="大屏设计器" url="https://doc.iocoder.cn/screen/" />
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import { getRefreshToken } from '@/utils/auth'
defineOptions({ name: 'JimuBI' })
// 使 getRefreshToken() 使 getAccessToken() 便访
const src = ref(import.meta.env.VITE_BASE_URL + '/drag/list?token=' + getRefreshToken())
</script>

View File

@ -37,7 +37,6 @@ ${selector}:before {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
background-color: var(--el-border-color);
z-index: 3;