This commit is contained in:
吴红兵
2026-03-07 01:34:48 +08:00
parent adc511cfdc
commit 94c3473958
1211 changed files with 599405 additions and 322105 deletions

View File

@@ -1,127 +1,124 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible"
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="用户ID" prop="userId">
<el-input v-model="form.userId" placeholder="请输入用户ID"/>
</el-form-item>
</el-col>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="用户ID" prop="userId">
<el-input v-model="form.userId" placeholder="请输入用户ID" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="提示令牌数量" prop="promptTokens">
<el-input v-model="form.promptTokens" placeholder="请输入提示令牌数量"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="提示令牌数量" prop="promptTokens">
<el-input v-model="form.promptTokens" placeholder="请输入提示令牌数量" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="补全令牌数量" prop="completionTokens">
<el-input v-model="form.completionTokens" placeholder="请输入补全令牌数量"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="补全令牌数量" prop="completionTokens">
<el-input v-model="form.completionTokens" placeholder="请输入补全令牌数量" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="模型名称" prop="model">
<el-input v-model="form.model" placeholder="请输入模型名称"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="模型名称" prop="model">
<el-input v-model="form.model" placeholder="请输入模型名称" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="请求ID" prop="reqid">
<el-input v-model="form.reqid" placeholder="请输入请求ID"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="请求ID" prop="reqid">
<el-input v-model="form.reqid" placeholder="请输入请求ID" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="IP地址" prop="ip">
<el-input v-model="form.ip" placeholder="请输入IP地址"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="IP地址" prop="ip">
<el-input v-model="form.ip" placeholder="请输入IP地址" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="备注" prop="note">
<el-input v-model="form.note" placeholder="请输入备注"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="备注" prop="note">
<el-input v-model="form.note" placeholder="请输入备注" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="令牌ID" prop="tokenId">
<el-input v-model="form.tokenId" placeholder="请输入令牌ID"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="令牌ID" prop="tokenId">
<el-input v-model="form.tokenId" placeholder="请输入令牌ID" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="令牌数量" prop="tokens">
<el-input v-model="form.tokens" placeholder="请输入令牌数量"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="令牌类型 0 系统 1 用户" prop="tokenType">
<el-input v-model="form.tokenType" placeholder="请输入令牌类型 0 系统 1 用户"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="令牌数量" prop="tokens">
<el-input v-model="form.tokens" placeholder="请输入令牌数量" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="令牌类型 0 系统 1 用户" prop="tokenType">
<el-input v-model="form.tokenType" placeholder="请输入令牌类型 0 系统 1 用户" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="AiBillDialog">
import { useDict } from '/@/hooks/dict';
import { useMessage } from "/@/hooks/message";
import { getObj, addObj, putObj } from '/@/api/knowledge/aiBill'
import { useMessage } from '/@/hooks/message';
import { getObj, addObj, putObj } from '/@/api/knowledge/aiBill';
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { yes_no_type } = useDict('yes_no_type')
const { yes_no_type } = useDict('yes_no_type');
// 提交表单数据
const form = reactive({
id:'',
userId: '',
promptTokens: '',
completionTokens: '',
model: '',
reqid: '',
ip: '',
note: '',
tokenId: '',
tokens: '',
tokenType: '',
id: '',
userId: '',
promptTokens: '',
completionTokens: '',
model: '',
reqid: '',
ip: '',
note: '',
tokenId: '',
tokens: '',
tokenType: '',
});
// 定义校验规则
const dataRules = ref({
})
const dataRules = ref({});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true
form.id = ''
visible.value = true;
form.id = '';
// 重置表单数据
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取aiBill信息
if (id) {
form.id = id
getaiBillData(id)
}
// 获取aiBill信息
if (id) {
form.id = id;
getaiBillData(id);
}
};
// 提交
@@ -130,7 +127,7 @@ const onSubmit = async () => {
if (!valid) return false;
try {
loading.value = true;
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(form.id ? '修改成功' : '添加成功');
visible.value = false;
@@ -138,24 +135,25 @@ const onSubmit = async () => {
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
loading.value = false;
}
};
// 初始化表单数据
const getaiBillData = (id: string) => {
// 获取数据
loading.value = true
getObj(id).then((res: any) => {
Object.assign(form, res.data)
}).finally(() => {
loading.value = false
})
// 获取数据
loading.value = true;
getObj(id)
.then((res: any) => {
Object.assign(form, res.data);
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
openDialog
openDialog,
});
</script>
</script>

View File

@@ -1,82 +1,82 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 顶部折线图-->
<bill-line-chart />
<!-- 顶部折线图-->
<bill-line-chart/>
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="用户名" prop="username">
<el-input placeholder="请输入用户名" v-model="state.queryForm.username" />
</el-form-item>
<el-form-item label="系统调用" prop="tokenType">
<el-radio-group v-model="state.queryForm.tokenType">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in yes_no_type">{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList"> 查询 </el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'knowledge_aiBill_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'knowledge_aiBill_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="createBy" label="用户名" show-overflow-tooltip />
<el-table-column prop="model" label="模型名称" show-overflow-tooltip />
<el-table-column prop="tokens" label="tokens" sortable="custom" show-overflow-tooltip />
<el-table-column prop="promptTokens" label="prompt tokens" show-overflow-tooltip />
<el-table-column prop="completionTokens" label="completion" show-overflow-tooltip />
<el-table-column prop="ip" label="IP地址" show-overflow-tooltip />
<el-table-column prop="tokenType" label="系统调用" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.tokenType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="调用时间" show-overflow-tooltip />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="delete" text type="primary" v-auth="'knowledge_aiBill_del'" @click="handleDelete([scope.row.id])">删除 </el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="用户名" prop="username">
<el-input placeholder="请输入用户名" v-model="state.queryForm.username"/>
</el-form-item>
<el-form-item label="系统调用" prop="tokenType">
<el-radio-group v-model="state.queryForm.tokenType">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in yes_no_type">{{
item.label
}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
查询
</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button plain :disabled="multiple" icon="Delete" type="primary"
v-auth="'knowledge_aiBill_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="'knowledge_aiBill_export'"
@exportExcel="exportExcel" class="ml10 mr20" style="float: right;"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table :data="state.dataList" v-loading="state.loading" border
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center"/>
<el-table-column type="index" label="#" width="40"/>
<el-table-column prop="createBy" label="用户名" show-overflow-tooltip/>
<el-table-column prop="model" label="模型名称" show-overflow-tooltip/>
<el-table-column prop="tokens" label="tokens" sortable="custom" show-overflow-tooltip/>
<el-table-column prop="promptTokens" label="prompt tokens" show-overflow-tooltip/>
<el-table-column prop="completionTokens" label="completion" show-overflow-tooltip/>
<el-table-column prop="ip" label="IP地址" show-overflow-tooltip/>
<el-table-column prop="tokenType" label="系统调用" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.tokenType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="调用时间" show-overflow-tooltip/>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="delete" text type="primary" v-auth="'knowledge_aiBill_del'"
@click="handleDelete([scope.row.id])">删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination"/>
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)"/>
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="systemAiBill">
import {BasicTableProps, useTable} from "/@/hooks/table";
import {fetchList, delObjs} from "/@/api/knowledge/aiBill";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useDict} from '/@/hooks/dict';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/knowledge/aiBill';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
@@ -84,66 +84,59 @@ const BillLineChart = defineAsyncComponent(() => import('./line-chart.vue'));
// 定义查询字典
const {yes_no_type} = useDict('yes_no_type')
const { yes_no_type } = useDict('yes_no_type');
// 定义变量内容
const formDialogRef = ref()
const formDialogRef = ref();
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
descs: ['create_time']
})
queryForm: {},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile,
tableStyle
} = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
// 清空多选
selectObjs.value = []
getDataList()
}
// 清空搜索条件
queryRef.value?.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/knowledge/aiBill/export', Object.assign(state.queryForm, {ids: selectObjs}), 'aiBill.xlsx')
}
downBlobFile('/knowledge/aiBill/export', Object.assign(state.queryForm, { ids: selectObjs }), 'aiBill.xlsx');
};
// 多选事件
const selectionChangHandle = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({id}) => id);
multiple.value = !objs.length;
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -208,8 +208,8 @@ const createNewChat = () => {
const currentRoute = router.currentRoute.value;
// 从路由查询参数中获取mcpId和dataId
const mcpId = currentRoute.query.mcpId as string || '';
const dataId = currentRoute.query.dataId as string || '';
const mcpId = (currentRoute.query.mcpId as string) || '';
const dataId = (currentRoute.query.dataId as string) || '';
// 格式化当前日期和时间作为标题
const now = new Date();

View File

@@ -86,11 +86,7 @@
collapseStates['tool_' + index] === true ? 'collapse-open' : 'collapse-close',
]"
>
<input
type="checkbox"
class="peer"
@change="collapseStates['tool_' + index] = !collapseStates['tool_' + index]"
/>
<input type="checkbox" class="peer" @change="collapseStates['tool_' + index] = !collapseStates['tool_' + index]" />
<div
class="text-sm font-medium text-gray-700 collapse-title dark:text-gray-300 peer-checked:bg-gray-100 dark:peer-checked:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700/50"
>
@@ -108,12 +104,7 @@
stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
工具调用{{ message.toolInfo.name }}
</div>

View File

@@ -296,16 +296,12 @@ watch(
// 如果有参数且没有选择对话字段,自动选择第一个适合的参数作为对话字段
if (flowStartParams.value.length > 0 && !selectedChatField.value) {
// 优先选择名称包含 prompt、message、input、content 的参数
const preferredField = flowStartParams.value.find(param =>
/prompt|message|input|content|text/i.test(param.name)
);
const preferredField = flowStartParams.value.find((param) => /prompt|message|input|content|text/i.test(param.name));
if (preferredField) {
selectedChatField.value = preferredField.name;
} else {
// 如果没有找到优先字段,选择第一个字符串类型的参数
const firstStringField = flowStartParams.value.find(param =>
param.inputType === 'input' || param.inputType === 'textarea'
);
const firstStringField = flowStartParams.value.find((param) => param.inputType === 'input' || param.inputType === 'textarea');
if (firstStringField) {
selectedChatField.value = firstStringField.name;
}
@@ -348,7 +344,7 @@ const sendFlowChatMessage = async (content: string) => {
// 动态构建执行参数,基于流程开始节点的参数配置
const executeParams: Record<string, any> = {};
if (flowStartParams.value && flowStartParams.value.length > 0) {
// 根据参数配置构建执行参数
flowStartParams.value.forEach((param: any) => {
@@ -383,7 +379,7 @@ const sendFlowChatMessage = async (content: string) => {
Local.set(conversationKey.value, JSON.stringify(messageList.value));
});
}
// 滚动到底部
debouncedScrollToBottom();
},
@@ -393,7 +389,7 @@ const sendFlowChatMessage = async (content: string) => {
onError: (error: string) => {
isFinish.value = true;
useMessage().error(error || '流程执行失败');
}
},
};
// 执行流程SSE聊天
@@ -403,7 +399,7 @@ const sendFlowChatMessage = async (content: string) => {
conversationId: flowConversationId,
params: executeParams,
envs: {},
stream: true
stream: true,
},
callbacks
);
@@ -420,12 +416,11 @@ const sendFlowChatMessage = async (content: string) => {
// @ts-ignore - 流式完成标识
currentMessage.stream = false;
currentMessage.time = parseTime(new Date());
// 保存到本地存储
Local.set(conversationKey.value, JSON.stringify(messageList.value));
}
});
} catch (error: any) {
// 更新错误消息
const aiMessageIndex = messageList.value.length - 1;
@@ -435,11 +430,11 @@ const sendFlowChatMessage = async (content: string) => {
// @ts-ignore - 流式完成标识
currentMessage.stream = false;
currentMessage.time = parseTime(new Date());
// 保存到本地存储
Local.set(conversationKey.value, JSON.stringify(messageList.value));
}
isFinish.value = true;
useMessage().error(error.message || '流程执行失败');
}
@@ -622,7 +617,7 @@ const appendLastMessageContent = (result: any) => {
const sendOrSave = async () => {
if (!messageContent.value.length || !isFinish.value) return;
// 如果是流程编排模式,使用流程参数配置组件进行验证
if (props.flowId && flowStartParams.value.length > 0 && flowParamsConfigRef.value) {
const validation = flowParamsConfigRef.value.validateRequiredParams();
@@ -631,7 +626,7 @@ const sendOrSave = async () => {
return;
}
}
await sendChatMessage();
};
@@ -797,5 +792,4 @@ defineExpose({
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -1,11 +1,13 @@
<template>
<div
v-if="flowId && flowStartParams.length > 0"
class="mx-auto mb-4 bg-white border-t border-gray-200 shadow-lg flow-params-container dark:border-gray-700 dark:bg-gray-900"
style="width: 50%;"
<div
v-if="flowId && flowStartParams.length > 0"
class="mx-auto mb-4 bg-white border-t border-gray-200 shadow-lg flow-params-container dark:border-gray-700 dark:bg-gray-900"
style="width: 50%"
>
<!-- 配置区头部 -->
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-800 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-800">
<div
class="flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-800 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-800"
>
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200">流程参数配置</h3>
@@ -41,8 +43,12 @@
>
<div v-show="!isCollapsed" class="p-4 space-y-4 overflow-y-auto max-h-96">
<!-- 对话字段选择 -->
<div class="p-3 rounded-lg bg-gradient-to-r from-amber-50 dark:from-amber-900/20 dark:to-yellow-900/20 border-amber-200 dark:border-amber-800">
<div class="p-3 transition-colors border rounded-lg border-amber-200 param-item bg-amber-50/50 dark:bg-amber-900/10 dark:border-amber-800 hover:border-amber-300 dark:hover:border-amber-600">
<div
class="p-3 rounded-lg bg-gradient-to-r from-amber-50 dark:from-amber-900/20 dark:to-yellow-900/20 border-amber-200 dark:border-amber-800"
>
<div
class="p-3 transition-colors border rounded-lg border-amber-200 param-item bg-amber-50/50 dark:bg-amber-900/10 dark:border-amber-800 hover:border-amber-300 dark:hover:border-amber-600"
>
<label class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-amber-800 dark:text-amber-200">对话字段</span>
<span class="px-1.5 py-0.5 text-xs bg-amber-100 text-amber-600 dark:bg-amber-900 dark:text-amber-300 rounded">必选</span>
@@ -59,7 +65,11 @@
</select>
<div class="flex items-start mt-2 space-x-2 text-xs text-amber-700 dark:text-amber-300">
<svg class="w-3 h-3 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
<span>选择的字段值将自动使用底部输入框的内容</span>
</div>
@@ -70,16 +80,24 @@
<div v-if="displayParams.length > 0">
<div class="flex items-center mb-3 space-x-2">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM15 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4z" />
<path
d="M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM15 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4z"
/>
</svg>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">其他参数</span>
<span class="text-xs text-gray-500 dark:text-gray-400">({{ displayParams.length }} )</span>
</div>
<div class="space-y-4">
<div v-for="(param, index) in displayParams" :key="index" class="p-3 transition-colors border border-gray-200 rounded-lg param-item bg-gray-50 dark:bg-gray-800 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600">
<div
v-for="(param, index) in displayParams"
:key="index"
class="p-3 transition-colors border border-gray-200 rounded-lg param-item bg-gray-50 dark:bg-gray-800 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600"
>
<label class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ param.name }}</span>
<span v-if="param.required" class="px-1.5 py-0.5 text-xs bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300 rounded">必填</span>
<span v-if="param.required" class="px-1.5 py-0.5 text-xs bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300 rounded"
>必填</span
>
</label>
<input
v-if="param.inputType === 'input'"
@@ -116,9 +134,16 @@
{{ option.label }}
</option>
</select>
<div v-if="param.type === 'prompt' || param.type === 'message'" class="flex items-center mt-2 space-x-1 text-xs text-blue-600 dark:text-blue-400">
<div
v-if="param.type === 'prompt' || param.type === 'message'"
class="flex items-center mt-2 space-x-1 text-xs text-blue-600 dark:text-blue-400"
>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
<span>此参数将自动使用您的消息内容</span>
</div>
@@ -143,19 +168,19 @@ const props = defineProps<{
const emit = defineEmits<{
'update:selectedChatField': [value: string];
'update:isCollapsed': [value: boolean];
'paramsChange': [params: FlowParam[]];
paramsChange: [params: FlowParam[]];
}>();
// 计算属性:过滤掉对话字段的参数列表
const displayParams = computed(() => {
return props.flowStartParams.filter(param => param.name !== props.selectedChatField);
return props.flowStartParams.filter((param) => param.name !== props.selectedChatField);
});
// 计算属性:可用作对话字段的参数选项
const chatFieldOptions = computed((): ChatFieldOption[] => {
return props.flowStartParams.map(param => ({
return props.flowStartParams.map((param) => ({
label: param.name,
value: param.name
value: param.name,
}));
});
@@ -178,23 +203,24 @@ const handleParamChange = () => {
// 验证必填参数
const validateRequiredParams = (): FlowParamsValidationResult => {
const missingParams = props.flowStartParams.filter(param =>
param.required &&
param.type !== 'prompt' &&
param.type !== 'message' &&
param.name !== props.selectedChatField && // 排除对话字段
!param.value
const missingParams = props.flowStartParams.filter(
(param) =>
param.required &&
param.type !== 'prompt' &&
param.type !== 'message' &&
param.name !== props.selectedChatField && // 排除对话字段
!param.value
);
return {
isValid: missingParams.length === 0,
missingParams: missingParams.map(p => p.name)
missingParams: missingParams.map((p) => p.name),
};
};
// 对外暴露方法
defineExpose({
validateRequiredParams
validateRequiredParams,
});
</script>

View File

@@ -1,75 +1,74 @@
<template>
<el-drawer title="提示词" v-model="promptVisible" close-on-click-modal>
<div class="max-w-xl rounded-lg h-full py-4 dark:border-gray-700">
<div class="mx-2">
<input
id="search-chats"
type="text"
class="w-full rounded-lg border border-slate-300 dark:border-gray-700 bg-slate-50 dark:bg-gray-800 p-3 pr-10 text-sm text-slate-800 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-600"
placeholder="查询提示词"
v-model="queryString"
@keydown.enter="querySearchAsync"
required
/>
</div>
<el-drawer title="提示词" v-model="promptVisible" close-on-click-modal>
<div class="max-w-xl rounded-lg h-full py-4 dark:border-gray-700">
<div class="mx-2">
<input
id="search-chats"
type="text"
class="w-full rounded-lg border border-slate-300 dark:border-gray-700 bg-slate-50 dark:bg-gray-800 p-3 pr-10 text-sm text-slate-800 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-600"
placeholder="查询提示词"
v-model="queryString"
@keydown.enter="querySearchAsync"
required
/>
</div>
<ul role="list" class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-2">
<li class="col-span-2 rounded-lg mt-2 shadow dark:bg-gray-800" v-for="prompt in promptList">
<div class="flex w-full items-center justify-between space-x-6 p-6">
<button class="group flex-1 truncate" @click="selectPrompt(prompt)">
<div class="flex items-center space-x-3">
<h3 class="truncate text-sm font-medium text-slate-900 dark:text-gray-200 transition-colors group-hover:text-blue-600">
{{ prompt.act }}
</h3>
</div>
<p class="mt-1 truncate text-sm text-slate-500 dark:text-gray-400">
{{ prompt.prompt }}
</p>
</button>
</div>
</li>
</ul>
</div>
</el-drawer>
<ul role="list" class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-2">
<li class="col-span-2 rounded-lg mt-2 shadow dark:bg-gray-800" v-for="prompt in promptList">
<div class="flex w-full items-center justify-between space-x-6 p-6">
<button class="group flex-1 truncate" @click="selectPrompt(prompt)">
<div class="flex items-center space-x-3">
<h3 class="truncate text-sm font-medium text-slate-900 dark:text-gray-200 transition-colors group-hover:text-blue-600">
{{ prompt.act }}
</h3>
</div>
<p class="mt-1 truncate text-sm text-slate-500 dark:text-gray-400">
{{ prompt.prompt }}
</p>
</button>
</div>
</li>
</ul>
</div>
</el-drawer>
</template>
<script setup lang="ts" name="AiPromptsDialog">
import {list} from "/@/api/knowledge/aiPrompt";
import { list } from '/@/api/knowledge/aiPrompt';
const emit = defineEmits(['refresh']);
const promptVisible = ref(false)
const promptList = ref([])
const queryString = ref('')
const promptVisible = ref(false);
const promptList = ref([]);
const queryString = ref('');
/**
* 选中提示词
* @param prompt
*/
const selectPrompt = (prompt: any) => {
// Close the prompt dialog
promptVisible.value = false
// Emit a 'refresh' event with the selected prompt
emit('refresh', prompt.prompt);
}
// Close the prompt dialog
promptVisible.value = false;
// Emit a 'refresh' event with the selected prompt
emit('refresh', prompt.prompt);
};
/**
* 查询所有提示词.
*/
const querySearchAsync = async () => {
const {data} = await list({act: queryString.value})
promptList.value = data
}
const { data } = await list({ act: queryString.value });
promptList.value = data;
};
/**
* 打开提示词选择界面.
*/
const openDialog = () => {
querySearchAsync()
promptVisible.value = true
}
querySearchAsync();
promptVisible.value = true;
};
// Expose the openDialog function
defineExpose({
openDialog
openDialog,
});
</script>

View File

@@ -80,7 +80,7 @@ const handleRowDblClick = (row: any) => {
selectedDataId.value = row.dataId;
selectedDatasetName.value = row.datasetName;
// Emit a 'refresh' event with the selected data
emit('refresh', row.dataId );
emit('refresh', row.dataId);
useMessage().success('已选择数据集: ' + row.datasetName);
};

View File

@@ -5,16 +5,16 @@
</template>
<script setup>
import {genTts} from "/@/api/knowledge/aiGen";
import {ref} from 'vue';
import {useMessage} from '/@/hooks/message';
import { genTts } from '/@/api/knowledge/aiGen';
import { ref } from 'vue';
import { useMessage } from '/@/hooks/message';
// 接收传入的文本属性
const props = defineProps({
text: {
type: String,
required: true
}
text: {
type: String,
required: true,
},
});
// 定义状态变量
@@ -25,92 +25,102 @@ const isLocked = ref(false); // 按钮锁控制,防止重复点击
// 切换播放/暂停的主函数
const togglePlayPause = () => {
if (isLocked.value) return; // 如果按钮已锁定,直接返回
isLocked.value = true; // 锁定按钮,防止重复点击
if (isLocked.value) return; // 如果按钮已锁定,直接返回
isLocked.value = true; // 锁定按钮,防止重复点击
if (isPlaying.value) {
pauseAudio(); // 如果正在播放,则暂停
} else {
playAudio(props.text); // 如果未播放,则生成或播放音频
}
if (isPlaying.value) {
pauseAudio(); // 如果正在播放,则暂停
} else {
playAudio(props.text); // 如果未播放,则生成或播放音频
}
};
// 播放音频的函数
const playAudio = (text) => {
if (audioSrc.value) {
resumeAudio(); // 如果音频已生成,则直接恢复播放
} else {
genTts(text).then((res) => {
const audioBlob = base64ToBlob(res.data, 'audio/wav');
const audioUrl = URL.createObjectURL(audioBlob);
audioSrc.value = audioUrl; // 存储生成的音频 URL
handleAudioPlayPause(audioElement, audioUrl); // 处理音频的播放和暂停
}).catch((error) => {
if (typeof error === 'object' && error !== null && 'msg' in error) {
useMessage().error(error.msg);
} else {
useMessage().error('An error occurred');
}
})
.finally(() => {
isLocked.value = false; // 操作完成后释放锁
});
}
if (audioSrc.value) {
resumeAudio(); // 如果音频已生成,则直接恢复播放
} else {
genTts(text)
.then((res) => {
const audioBlob = base64ToBlob(res.data, 'audio/wav');
const audioUrl = URL.createObjectURL(audioBlob);
audioSrc.value = audioUrl; // 存储生成的音频 URL
handleAudioPlayPause(audioElement, audioUrl); // 处理音频的播放和暂停
})
.catch((error) => {
if (typeof error === 'object' && error !== null && 'msg' in error) {
useMessage().error(error.msg);
} else {
useMessage().error('An error occurred');
}
})
.finally(() => {
isLocked.value = false; // 操作完成后释放锁
});
}
};
// 恢复播放音频的函数
const resumeAudio = () => {
if (audioElement.value && audioElement.value.paused) {
audioElement.value.play().then(() => {
isPlaying.value = true; // 更新状态为播放中
}).catch(error => {
console.error("恢复播放音频时出错:", error); // 捕获并打印错误
}).finally(() => {
isLocked.value = false; // 操作完成后释放锁
});
}
if (audioElement.value && audioElement.value.paused) {
audioElement.value
.play()
.then(() => {
isPlaying.value = true; // 更新状态为播放中
})
.catch((error) => {
console.error('恢复播放音频时出错:', error); // 捕获并打印错误
})
.finally(() => {
isLocked.value = false; // 操作完成后释放锁
});
}
};
// 暂停音频的函数
const pauseAudio = () => {
if (audioElement.value && !audioElement.value.paused) {
audioElement.value.pause(); // 暂停音频
isPlaying.value = false; // 更新状态为暂停
isLocked.value = false; // 释放锁
}
if (audioElement.value && !audioElement.value.paused) {
audioElement.value.pause(); // 暂停音频
isPlaying.value = false; // 更新状态为暂停
isLocked.value = false; // 释放锁
}
};
// 处理音频播放和暂停的核心函数
const handleAudioPlayPause = (audioRef, src) => {
if (audioRef.value) {
if (!audioRef.value.paused && audioRef.value.src === src) {
audioRef.value.pause(); // 如果正在播放且 URL 相同,则暂停
isPlaying.value = false; // 更新状态为暂停
isLocked.value = false; // 释放锁
} else {
audioRef.value.src = src; // 设置音频 URL
audioRef.value.load(); // 重新加载音频
audioRef.value.onloadedmetadata = () => {
audioRef.value.play().then(() => {
isPlaying.value = true; // 更新状态为播放中
}).catch(error => {
console.error("播放音频时出错:", error); // 捕获并打印错误
}).finally(() => {
isLocked.value = false; // 操作完成后释放锁
});
};
}
}
if (audioRef.value) {
if (!audioRef.value.paused && audioRef.value.src === src) {
audioRef.value.pause(); // 如果正在播放且 URL 相同,则暂停
isPlaying.value = false; // 更新状态为暂停
isLocked.value = false; // 释放锁
} else {
audioRef.value.src = src; // 设置音频 URL
audioRef.value.load(); // 重新加载音频
audioRef.value.onloadedmetadata = () => {
audioRef.value
.play()
.then(() => {
isPlaying.value = true; // 更新状态为播放中
})
.catch((error) => {
console.error('播放音频时出错:', error); // 捕获并打印错误
})
.finally(() => {
isLocked.value = false; // 操作完成后释放锁
});
};
}
}
};
// 将 base64 编码转换为 Blob 对象的辅助函数
function base64ToBlob(base64, fileType) {
const byteCharacters = atob(base64); // 解码 base64 字符串
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i); // 将字符转换为字节
}
const byteArray = new Uint8Array(byteNumbers); // 创建字节数组
return new Blob([byteArray], {type: fileType}); // 创建并返回 Blob 对象
const byteCharacters = atob(base64); // 解码 base64 字符串
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i); // 将字符转换为字节
}
const byteArray = new Uint8Array(byteNumbers); // 创建字节数组
return new Blob([byteArray], { type: fileType }); // 创建并返回 Blob 对象
}
</script>

View File

@@ -1,62 +1,65 @@
<template>
<el-dialog v-model="audioVisible" destroy-on-close>
<tapir-widget :time="0.2" :backendEndpoint="backendEndpoint"
title="音频交互"
instructionMessageStart="点击开始录音"
instructionMessageStop="点击停止录音"
listenInstructions="播放您的录音"
successMessageRecorded="录音成功"
successMessageSubmitted="上传成功"
:successfulUpload="successfulUpload"
submitLabel="点击识别"
buttonColor="border border-primary"/>
</el-dialog>
<el-dialog v-model="audioVisible" destroy-on-close>
<tapir-widget
:time="0.2"
:backendEndpoint="backendEndpoint"
title="音频交互"
instructionMessageStart="点击开始录音"
instructionMessageStop="点击停止录音"
listenInstructions="播放您的录音"
successMessageRecorded="录音成功"
successMessageSubmitted="上传成功"
:successfulUpload="successfulUpload"
submitLabel="点击识别"
buttonColor="border border-primary"
/>
</el-dialog>
</template>
<script setup lang="ts" name="AiPromptsDialog">
// @ts-ignore
import TapirWidget from 'vue-audio-record';
import 'vue-audio-record/dist/vue-audio-tapir.css';
import {Session} from "/@/utils/storage";
import { Session } from '/@/utils/storage';
const emit = defineEmits(['refresh']);
const audioVisible = ref(false)
const audioVisible = ref(false);
/**
* 从会话存储中获取访问令牌
* @returns {string} 访问令牌
*/
const token = computed(() => {
return Session.getToken();
return Session.getToken();
});
/**
* 从会话存储中获取访问租户
* @returns {string} 租户
*/
const tenant = computed(() => {
return Session.getTenant();
return Session.getTenant();
});
const backendEndpoint = computed(() => {
return `${import.meta.env.VITE_API_URL}${import.meta.env.VITE_IS_MICRO == 'false' ? '/admin' : '/knowledge'}/chat/audio?access_token=${token.value}&TENANT-ID=${tenant.value}`
})
return `${import.meta.env.VITE_API_URL}${import.meta.env.VITE_IS_MICRO == 'false' ? '/admin' : '/knowledge'}/chat/audio?access_token=${
token.value
}&TENANT-ID=${tenant.value}`;
});
const successfulUpload = async (response: any) => {
audioVisible.value = false
const {data} = await response.json()
emit('refresh', JSON.parse(data).text);
}
audioVisible.value = false;
const { data } = await response.json();
emit('refresh', JSON.parse(data).text);
};
/**
* 打开提示词选择界面.
*/
const openDialog = () => {
audioVisible.value = true
}
audioVisible.value = true;
};
// Expose the openDialog function
defineExpose({
openDialog
openDialog,
});
</script>