feat(ai): 添加 AI 对话聊天和 API 密钥管理功能

- 新增 AI 对话聊天管理页面,包括对话列表和消息列表
- 新增 API 密钥管理页面,包括密钥列表和表单
- 添加相关 API 接口和数据模型
- 集成表单和表格组件,实现基本的 CRUD 操作
pull/145/head
gjd 2025-06-06 17:09:14 +08:00
parent 75c5669a97
commit 3ef362508a
28 changed files with 2509 additions and 105 deletions

View File

@ -0,0 +1,75 @@
import type { PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace AiChatConversationApi {
export interface ChatConversationVO {
id: number; // ID 编号
userId: number; // 用户编号
title: string; // 对话标题
pinned: boolean; // 是否置顶
roleId: number; // 角色编号
modelId: number; // 模型编号
model: string; // 模型标志
temperature: number; // 温度参数
maxTokens: number; // 单条回复的最大 Token 数量
maxContexts: number; // 上下文的最大 Message 数量
createTime?: Date; // 创建时间
// 额外字段
systemMessage?: string; // 角色设定
modelName?: string; // 模型名字
roleAvatar?: string; // 角色头像
modelMaxTokens?: string; // 模型的单条回复的最大 Token 数量
modelMaxContexts?: string; // 模型的上下文的最大 Message 数量
}
}
// 获得【我的】聊天对话
export function getChatConversationMy(id: number) {
return requestClient.get<
PageResult<AiChatConversationApi.ChatConversationVO>
>(`/ai/chat/conversation/get-my?id=${id}`);
}
// 新增【我的】聊天对话
export function createChatConversationMy(
data: AiChatConversationApi.ChatConversationVO,
) {
return requestClient.post('/ai/chat/conversation/create-my', data);
}
// 更新【我的】聊天对话
export function updateChatConversationMy(
data: AiChatConversationApi.ChatConversationVO,
) {
return requestClient.put(`/ai/chat/conversation/update-my`, data);
}
// 删除【我的】聊天对话
export function deleteChatConversationMy(id: string) {
return requestClient.delete(`/ai/chat/conversation/delete-my?id=${id}`);
}
// 删除【我的】所有对话,置顶除外
export function deleteChatConversationMyByUnpinned() {
return requestClient.delete(`/ai/chat/conversation/delete-by-unpinned`);
}
// 获得【我的】聊天对话列表
export function getChatConversationMyList() {
return requestClient.get<AiChatConversationApi.ChatConversationVO[]>(
`/ai/chat/conversation/my-list`,
);
}
// 获得【我的】聊天对话列表
export function getChatConversationPage(params: any) {
return requestClient.get<
PageResult<AiChatConversationApi.ChatConversationVO[]>
>(`/ai/chat/conversation/page`, { params });
}
// 管理员删除消息
export function deleteChatConversationByAdmin(id: number) {
return requestClient.delete(`/ai/chat/conversation/delete-by-admin?id=${id}`);
}

View File

@ -0,0 +1,96 @@
import type { PageResult } from '@vben/request';
import { fetchEventSource } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { requestClient } from '#/api/request';
const accessStore = useAccessStore();
export namespace AiChatMessageApi {
export interface ChatMessageVO {
id: number; // 编号
conversationId: number; // 对话编号
type: string; // 消息类型
userId: string; // 用户编号
roleId: string; // 角色编号
model: number; // 模型标志
modelId: number; // 模型编号
content: string; // 聊天内容
tokens: number; // 消耗 Token 数量
segmentIds?: number[]; // 段落编号
segments?: {
content: string; // 段落内容
documentId: number; // 文档编号
documentName: string; // 文档名称
id: number; // 段落编号
}[];
createTime: Date; // 创建时间
roleAvatar: string; // 角色头像
userAvatar: string; // 用户头像
}
}
// 消息列表
export function getChatMessageListByConversationId(
conversationId: null | number,
) {
return requestClient.get<AiChatMessageApi.ChatMessageVO[]>(
`/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`,
);
}
// 发送 Stream 消息
export function sendChatMessageStream(
conversationId: number,
content: string,
ctrl: any,
enableContext: boolean,
onMessage: any,
onError: any,
onClose: any,
) {
const token = accessStore.accessToken;
return fetchEventSource(
`${import.meta.env.VITE_BASE_URL}/ai/chat/message/send-stream`,
{
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
openWhenHidden: true,
body: JSON.stringify({
conversationId,
content,
useContext: enableContext,
}),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal,
},
);
}
// 删除消息
export function deleteChatMessage(id: string) {
return requestClient.delete(`/ai/chat/message/delete?id=${id}`);
}
// 删除指定对话的消息
export function deleteByConversationId(conversationId: number) {
return requestClient.delete(
`/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`,
);
}
// 获得消息分页
export function getChatMessagePage(params: any) {
return requestClient.get<PageResult<AiChatMessageApi.ChatMessageVO>>(
'/ai/chat/message/page',
{ params },
);
}
// 管理员删除消息
export function deleteChatMessageByAdmin(id: number) {
return requestClient.delete(`/ai/chat/message/delete-by-admin?id=${id}`);
}

View File

@ -0,0 +1,50 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace AiKnowledgeKnowledgeApi {
export interface KnowledgeVO {
id: number; // 编号
name: string; // 知识库名称
description: string; // 知识库描述
embeddingModelId: number; // 嵌入模型编号,高质量模式时维护
topK: number; // topK
similarityThreshold: number; // 相似度阈值
}
}
// 查询知识库分页
export function getKnowledgePage(params: PageParam) {
return requestClient.get<PageResult<AiKnowledgeKnowledgeApi.KnowledgeVO>>(
'/ai/knowledge/page',
{ params },
);
}
// 查询知识库详情
export function getKnowledge(id: number) {
return requestClient.get<AiKnowledgeKnowledgeApi.KnowledgeVO>(
`/ai/knowledge/get?id=${id}`,
);
}
// 新增知识库
export function createKnowledge(data: AiKnowledgeKnowledgeApi.KnowledgeVO) {
return requestClient.post('/ai/knowledge/create', data);
}
// 修改知识库
export function updateKnowledge(data: AiKnowledgeKnowledgeApi.KnowledgeVO) {
return requestClient.put('/ai/knowledge/update', data);
}
// 删除知识库
export function deleteKnowledge(id: number) {
return requestClient.delete(`/ai/knowledge/delete?id=${id}`);
}
// 获取知识库简单列表
export function getSimpleKnowledgeList() {
return requestClient.get<AiKnowledgeKnowledgeApi.KnowledgeVO[]>(
'/ai/knowledge/simple-list',
);
}

View File

@ -0,0 +1,50 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace AiModelApiKeyApi {
export interface ApiKeyVO {
id: number; // 编号
name: string; // 名称
apiKey: string; // 密钥
platform: string; // 平台
url: string; // 自定义 API 地址
status: number; // 状态
}
}
// 查询 API 密钥分页
export function getApiKeyPage(params: PageParam) {
return requestClient.get<PageResult<AiModelApiKeyApi.ApiKeyVO>>(
'/ai/api-key/page',
{ params },
);
}
// 获得 API 密钥列表
export function getApiKeySimpleList() {
return requestClient.get<AiModelApiKeyApi.ApiKeyVO[]>(
'/ai/api-key/simple-list',
);
}
// 查询 API 密钥详情
export function getApiKey(id: number) {
return requestClient.get<AiModelApiKeyApi.ApiKeyVO>(
`/ai/api-key/get?id=${id}`,
);
}
// 新增 API 密钥
export function createApiKey(data: AiModelApiKeyApi.ApiKeyVO) {
return requestClient.post('/ai/api-key/create', data);
}
// 修改 API 密钥
export function updateApiKey(data: AiModelApiKeyApi.ApiKeyVO) {
return requestClient.put('/ai/api-key/update', data);
}
// 删除 API 密钥
export function deleteApiKey(id: number) {
return requestClient.delete(`/ai/api-key/delete?id=${id}`);
}

View File

@ -0,0 +1,85 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace AiModelChatRoleApi {
export interface ChatRoleVO {
id: number; // 角色编号
modelId: number; // 模型编号
name: string; // 角色名称
avatar: string; // 角色头像
category: string; // 角色类别
sort: number; // 角色排序
description: string; // 角色描述
systemMessage: string; // 角色设定
welcomeMessage: string; // 角色设定
publicStatus: boolean; // 是否公开
status: number; // 状态
knowledgeIds?: number[]; // 引用的知识库 ID 列表
toolIds?: number[]; // 引用的工具 ID 列表
}
// AI 聊天角色 分页请求 vo
export interface ChatRolePageReqVO {
name?: string; // 角色名称
category?: string; // 角色类别
publicStatus: boolean; // 是否公开
pageNo: number; // 是否公开
pageSize: number; // 是否公开
}
}
// 查询聊天角色分页
export function getChatRolePage(params: PageParam) {
return requestClient.get<PageResult<AiModelChatRoleApi.ChatRoleVO>>(
'/ai/chat-role/page',
{ params },
);
}
// 查询聊天角色详情
export function getChatRole(id: number) {
return requestClient.get<AiModelChatRoleApi.ChatRoleVO>(
`/ai/chat-role/get?id=${id}`,
);
}
// 新增聊天角色
export function createChatRole(data: AiModelChatRoleApi.ChatRoleVO) {
return requestClient.post('/ai/chat-role/create', data);
}
// 修改聊天角色
export function updateChatRole(data: AiModelChatRoleApi.ChatRoleVO) {
return requestClient.put('/ai/chat-role/update', data);
}
// 删除聊天角色
export function deleteChatRole(id: number) {
return requestClient.delete(`/ai/chat-role/delete?id=${id}`);
}
// ======= chat 聊天
// 获取 my role
export function getMyPage(params: AiModelChatRoleApi.ChatRolePageReqVO) {
return requestClient.get('/ai/chat-role/my-page', { params });
}
// 获取角色分类
export function getCategoryList() {
return requestClient.get('/ai/chat-role/category-list');
}
// 创建角色
export function createMy(data: AiModelChatRoleApi.ChatRoleVO) {
return requestClient.post('/ai/chat-role/create-my', data);
}
// 更新角色
export function updateMy(data: AiModelChatRoleApi.ChatRoleVO) {
return requestClient.put('/ai/chat-role/update', data);
}
// 删除角色 my
export function deleteMy(id: number) {
return requestClient.delete(`/ai/chat-role/delete-my?id=${id}`);
}

View File

@ -0,0 +1,55 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace AiModelModelApi {
export interface ModelVO {
id: number; // 编号
keyId: number; // API 秘钥编号
name: string; // 模型名字
model: string; // 模型标识
platform: string; // 模型平台
type: number; // 模型类型
sort: number; // 排序
status: number; // 状态
temperature?: number; // 温度参数
maxTokens?: number; // 单条回复的最大 Token 数量
maxContexts?: number; // 上下文的最大 Message 数量
}
}
// 查询模型分页
export function getModelPage(params: PageParam) {
return requestClient.get<PageResult<AiModelModelApi.ModelVO>>(
'/ai/model/page',
{ params },
);
}
// 获得模型列表
export function getModelSimpleList(type?: number) {
return requestClient.get<AiModelModelApi.ModelVO[]>('/ai/model/simple-list', {
params: {
type,
},
});
}
// 查询模型详情
export function getModel(id: number) {
return requestClient.get<AiModelModelApi.ModelVO>(`/ai/model/get?id=${id}`);
}
// 新增模型
export function createModel(data: AiModelModelApi.ModelVO) {
return requestClient.post('/ai/model/create', data);
}
// 修改模型
export function updateModel(data: AiModelModelApi.ModelVO) {
return requestClient.put('/ai/model/update', data);
}
// 删除模型
export function deleteModel(id: number) {
return requestClient.delete(`/ai/model/delete?id=${id}`);
}

View File

@ -0,0 +1,43 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace AiModelToolApi {
export interface ToolVO {
id: number; // 工具编号
name: string; // 工具名称
description: string; // 工具描述
status: number; // 状态
}
}
// 查询工具分页
export function getToolPage(params: PageParam) {
return requestClient.get<PageResult<AiModelToolApi.ToolVO>>('/ai/tool/page', {
params,
});
}
// 查询工具详情
export function getTool(id: number) {
return requestClient.get<AiModelToolApi.ToolVO>(`/ai/tool/get?id=${id}`);
}
// 新增工具
export function createTool(data: AiModelToolApi.ToolVO) {
return requestClient.post('/ai/tool/create', data);
}
// 修改工具
export function updateTool(data: AiModelToolApi.ToolVO) {
return requestClient.put('/ai/tool/update', data);
}
// 删除工具
export function deleteTool(id: number) {
return requestClient.delete(`/ai/tool/delete?id=${id}`);
}
// 获取工具简单列表
export function getToolSimpleList() {
return requestClient.get<AiModelToolApi.ToolVO[]>('/ai/tool/simple-list');
}

View File

@ -5,6 +5,14 @@
*
*/
export const AiModelTypeEnum = {
CHAT: 1, // 聊天
IMAGE: 2, // 图像
VOICE: 3, // 音频
VIDEO: 4, // 视频
EMBEDDING: 5, // 向量
RERANK: 6, // 重排
};
// ========== COMMON 模块 ==========
// 全局通用状态枚举
export const CommonStatusEnum = {

View File

@ -143,10 +143,10 @@ export const getBoolDictOptions = (dictType: string) => {
enum DICT_TYPE {
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
// ========== AI - 人工智能模块 ==========
AI_PLATFORM = 'ai_platform', // AI 平台
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度

View File

@ -0,0 +1,196 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSimpleUserList } from '#/api/system/user';
import { DICT_TYPE } from '#/utils';
/** 列表的搜索表单 */
export function useGridFormSchemaConversation(): VbenFormSchema[] {
return [
{
fieldName: 'userId',
label: '用户编号',
component: 'Input',
},
{
fieldName: 'title',
label: '聊天标题',
component: 'Input',
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
placeholder: ['开始时间', '结束时间'],
valueFormat: 'YYYY-MM-DD HH:mm:ss',
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumnsConversation(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '对话编号',
fixed: 'left',
minWidth: 180,
},
{
field: 'title',
title: '对话标题',
minWidth: 180,
fixed: 'left',
},
{
title: '用户',
width: 180,
slots: { default: 'userId' },
},
{
field: 'roleName',
title: '角色',
minWidth: 180,
},
{
field: 'model',
title: '模型标识',
minWidth: 180,
},
{
field: 'messageCount',
title: '消息数',
minWidth: 180,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'temperature',
title: '温度参数',
minWidth: 80,
},
{
title: '回复数 Token 数',
field: 'maxTokens',
minWidth: 120,
},
{
title: '上下文数量',
field: 'maxContexts',
minWidth: 120,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchemaMessage(): VbenFormSchema[] {
return [
{
fieldName: 'conversationId',
label: '对话编号',
component: 'Input',
},
{
fieldName: 'userId',
label: '用户编号',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
labelField: 'nickname',
valueField: 'id',
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
placeholder: ['开始时间', '结束时间'],
valueFormat: 'YYYY-MM-DD HH:mm:ss',
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumnsMessage(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '消息编号',
fixed: 'left',
minWidth: 180,
},
{
field: 'conversationId',
title: '对话编号',
minWidth: 180,
fixed: 'left',
},
{
title: '用户',
width: 180,
slots: { default: 'userId' },
},
{
field: 'roleName',
title: '角色',
minWidth: 180,
},
{
field: 'type',
title: '消息类型',
minWidth: 100,
},
{
field: 'model',
title: '模型标识',
minWidth: 180,
},
{
field: 'content',
title: '消息内容',
minWidth: 300,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'replyId',
title: '回复消息编号',
minWidth: 180,
},
{
title: '携带上下文',
field: 'useContext',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
minWidth: 100,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,31 +1,30 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { Card, TabPane, Tabs } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
import ChatConversationList from './modules/ChatConversationList.vue';
import ChatMessageList from './modules/ChatMessageList.vue';
const activeTabName = ref('conversation');
</script>
<template>
<Page>
<Page auto-content-height>
<DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/chat/manager/index.vue"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/chat/manager/index.vue
代码pull request 贡献给我们
</Button>
<Card>
<Tabs v-model:active-key="activeTabName">
<TabPane tab="对话列表" key="conversation">
<ChatConversationList />
</TabPane>
<TabPane tab="消息列表" key="message">
<ChatMessageList />
</TabPane>
</Tabs>
</Card>
</Page>
</template>

View File

@ -0,0 +1,113 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
import type { SystemUserApi } from '#/api/system/user';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteChatConversationByAdmin,
getChatConversationPage,
} from '#/api/ai/chat/conversation';
import { getSimpleUserList } from '#/api/system/user';
import { $t } from '#/locales';
import {
useGridColumnsConversation,
useGridFormSchemaConversation,
} from '../data';
const userList = ref<SystemUserApi.User[]>([]); //
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 删除 */
async function handleDelete(row: AiChatConversationApi.ChatConversationVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
});
try {
await deleteChatConversationByAdmin(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchemaConversation(),
},
gridOptions: {
columns: useGridColumnsConversation(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getChatConversationPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<AiChatConversationApi.ChatConversationVO>,
});
onMounted(async () => {
//
userList.value = await getSimpleUserList();
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="">
<template #toolbar-tools>
<TableAction :actions="[]" />
</template>
<template #userId="{ row }">
<span>{{
userList.find((item) => item.id === row.userId)?.nickname
}}</span>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:chat-conversation:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,110 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
import type { SystemUserApi } from '#/api/system/user';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteChatMessageByAdmin,
getChatMessagePage,
} from '#/api/ai/chat/message';
import { getSimpleUserList } from '#/api/system/user';
import { $t } from '#/locales';
import { useGridColumnsMessage, useGridFormSchemaMessage } from '../data';
const userList = ref<SystemUserApi.User[]>([]); //
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 删除 */
async function handleDelete(row: AiChatConversationApi.ChatConversationVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
});
try {
await deleteChatMessageByAdmin(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchemaMessage(),
},
gridOptions: {
columns: useGridColumnsMessage(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getChatMessagePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<AiChatConversationApi.ChatConversationVO>,
});
onMounted(async () => {
//
userList.value = await getSimpleUserList();
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="">
<template #toolbar-tools>
<TableAction :actions="[]" />
</template>
<template #userId="{ row }">
<span>{{
userList.find((item) => item.id === row.userId)?.nickname
}}</span>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:chat-message:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,126 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'platform',
label: '所属平台',
component: 'Select',
componentProps: {
placeholder: '请选择所属平台',
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
allowClear: true,
},
rules: z.string().min(1, { message: '请输入平台' }),
},
{
component: 'Input',
fieldName: 'name',
label: '名称',
rules: 'required',
},
{
component: 'Input',
fieldName: 'apiKey',
label: '密钥',
rules: 'required',
},
{
component: 'Input',
fieldName: 'url',
label: '自定义 API URL',
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '名称',
component: 'Input',
},
{
fieldName: 'platform',
label: '平台',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'platform',
title: '所属平台',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.AI_PLATFORM },
},
},
{
field: 'name',
title: '名称',
},
{
field: 'apiKey',
title: '密钥',
},
{
field: 'url',
title: '自定义 API URL',
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,31 +1,129 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteApiKey, getApiKeyPage } from '#/api/ai/model/apiKey';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑 */
function handleEdit(row: AiModelApiKeyApi.ApiKeyVO) {
formModalApi.setData(row).open();
}
/** 删除 */
async function handleDelete(row: AiModelApiKeyApi.ApiKeyVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteApiKey(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getApiKeyPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<AiModelApiKeyApi.ApiKeyVO>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<DocAlert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/apiKey/index.vue"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/apiKey/index.vue
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="API ">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['API 密钥']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['ai:api-key:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['ai:api-key:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:api-key:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createApiKey, getApiKey, updateApiKey } from '#/api/ai/model/apiKey';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AiModelApiKeyApi.ApiKeyVO>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['API 密钥'])
: $t('ui.actionTitle.create', ['API 密钥']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as AiModelApiKeyApi.ApiKeyVO;
try {
await (formData.value?.id ? updateApiKey(data) : createApiKey(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<AiModelApiKeyApi.ApiKeyVO>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getApiKey(data.id as number);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,277 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getSimpleKnowledgeList } from '#/api/ai/knowledge/knowledge';
import { getModelSimpleList } from '#/api/ai/model/model';
import { getToolSimpleList } from '#/api/ai/model/tool';
import {
AiModelTypeEnum,
CommonStatusEnum,
DICT_TYPE,
getDictOptions,
} from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'formType',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'name',
label: '角色名称',
rules: 'required',
},
{
component: 'ImageUpload',
fieldName: 'avatar',
label: '角色头像',
componentProps: {
maxSize: 1,
},
rules: 'required',
},
{
fieldName: 'modelId',
label: '绑定模型',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择绑定模型',
api: () => getModelSimpleList(AiModelTypeEnum.CHAT),
labelField: 'name',
valueField: 'id',
allowClear: true,
},
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
},
{
component: 'Input',
fieldName: 'category',
label: '角色类别',
rules: 'required',
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
},
{
component: 'Textarea',
fieldName: 'description',
label: '角色描述',
componentProps: {
placeholder: '请输入角色描述',
},
rules: 'required',
},
{
fieldName: 'systemMessage',
label: '角色设定',
component: 'Textarea',
componentProps: {
placeholder: '请输入角色设定',
},
rules: 'required',
},
{
fieldName: 'knowledgeIds',
label: '引用知识库',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择引用知识库',
api: getSimpleKnowledgeList,
labelField: 'name',
mode: 'multiple',
valueField: 'id',
allowClear: true,
},
},
{
fieldName: 'toolIds',
label: '引用工具',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择引用工具',
api: getToolSimpleList,
mode: 'multiple',
labelField: 'name',
valueField: 'id',
allowClear: true,
},
},
{
fieldName: 'publicStatus',
label: '是否公开',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
},
defaultValue: true,
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
rules: 'required',
},
{
fieldName: 'sort',
label: '角色排序',
component: 'InputNumber',
componentProps: {
controlsPosition: 'right',
placeholder: '请输入角色排序',
class: 'w-full',
},
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
rules: 'required',
},
{
fieldName: 'status',
label: '开启状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
dependencies: {
triggerFields: ['formType'],
show: (values) => {
return values.formType === 'create' || values.formType === 'update';
},
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '角色名称',
component: 'Input',
},
{
fieldName: 'category',
label: '角色类别',
component: 'Input',
},
{
fieldName: 'publicStatus',
label: '是否公开',
component: 'Select',
componentProps: {
placeholder: '请选择是否公开',
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
allowClear: true,
},
defaultValue: true,
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '角色名称',
minWidth: 100,
},
{
title: '绑定模型',
field: 'modelName',
minWidth: 100,
},
{
title: '角色头像',
slots: { default: 'avatar' },
minWidth: 140,
},
{
title: '角色类别',
field: 'category',
minWidth: 100,
},
{
title: '角色描述',
field: 'description',
minWidth: 100,
},
{
title: '角色设定',
field: 'systemMessage',
minWidth: 100,
},
{
title: '知识库',
slots: { default: 'knowledgeIds' },
minWidth: 100,
},
{
title: '工具',
slots: { default: 'toolIds' },
minWidth: 100,
},
{
field: 'publicStatus',
title: '是否公开',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
minWidth: 80,
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
minWidth: 80,
},
{
title: '角色排序',
field: 'sort',
minWidth: 80,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,31 +1,140 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiModelChatRoleApi } from '#/api/ai/model/chatRole';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Image, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteChatRole, getChatRolePage } from '#/api/ai/model/chatRole';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 编辑 */
function handleEdit(row: AiModelChatRoleApi.ChatRoleVO) {
formModalApi.setData({ formType: 'update', ...row }).open();
}
/** 删除 */
async function handleDelete(row: AiModelChatRoleApi.ChatRoleVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteChatRole(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getChatRolePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<AiModelChatRoleApi.ChatRoleVO>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/chatRole/index.vue"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/chatRole/index.vue
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['聊天角色']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['ai:chat-role:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #knowledgeIds="{ row }">
<span v-if="!row.knowledgeIds || row.knowledgeIds.length === 0">-</span>
<span v-else> {{ row.knowledgeIds.length }} </span>
</template>
<template #toolIds="{ row }">
<span v-if="!row.toolIds || row.toolIds.length === 0">-</span>
<span v-else> {{ row.toolIds.length }} </span>
</template>
<template #avatar="{ row }">
<Image :src="row.avatar" class="w-32px h-32px" />
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['ai:chat-role:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:chat-role:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { AiModelChatRoleApi } from '#/api/ai/model/chatRole';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createChatRole,
getChatRole,
updateChatRole,
} from '#/api/ai/model/chatRole';
import {} from '#/api/bpm/model';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AiModelChatRoleApi.ChatRoleVO>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['聊天角色'])
: $t('ui.actionTitle.create', ['聊天角色']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as AiModelChatRoleApi.ChatRoleVO;
try {
await (formData.value?.id ? updateChatRole(data) : createChatRole(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<AiModelChatRoleApi.ChatRoleVO>();
if (!data || !data.id) {
await formApi.setValues(data);
return;
}
modalApi.lock();
try {
formData.value = await getChatRole(data.id as number);
// values
await formApi.setValues({ ...data, ...formData.value });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,248 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getApiKeySimpleList } from '#/api/ai/model/apiKey';
import {
AiModelTypeEnum,
CommonStatusEnum,
DICT_TYPE,
getDictOptions,
} from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'platform',
label: '所属平台',
component: 'Select',
componentProps: {
placeholder: '请选择所属平台',
options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
allowClear: true,
},
rules: z.string().min(1, { message: '请输入平台' }),
},
{
fieldName: 'type',
label: '模型类型',
component: 'Select',
componentProps: (values) => {
return {
placeholder: '请输入模型类型',
disabled: !!values.id,
options: getDictOptions(DICT_TYPE.AI_MODEL_TYPE, 'number'),
allowClear: true,
};
},
rules: 'required',
},
{
fieldName: 'keyId',
label: 'API 秘钥',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择API 秘钥',
api: getApiKeySimpleList,
labelField: 'name',
valueField: 'id',
allowClear: true,
},
rules: 'required',
},
{
component: 'Input',
fieldName: 'name',
label: '模型名字',
rules: 'required',
},
{
component: 'Input',
fieldName: 'model',
label: '模型标识',
rules: 'required',
},
{
fieldName: 'sort',
label: '模型排序',
component: 'InputNumber',
componentProps: {
controlsPosition: 'right',
placeholder: '请输入模型排序',
class: 'w-full',
},
rules: 'required',
},
{
fieldName: 'status',
label: '开启状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
{
fieldName: 'temperature',
label: '温度参数',
component: 'InputNumber',
componentProps: {
controlsPosition: 'right',
placeholder: '请输入温度参数',
class: 'w-full',
min: 0,
max: 2,
},
dependencies: {
triggerFields: ['type'],
show: (values) => {
return [AiModelTypeEnum.CHAT].includes(values.type);
},
},
rules: 'required',
},
{
fieldName: 'maxTokens',
label: '回复数 Token 数',
component: 'InputNumber',
componentProps: {
min: 0,
max: 8192,
controlsPosition: 'right',
placeholder: '请输入回复数 Token 数',
class: 'w-full',
},
dependencies: {
triggerFields: ['type'],
show: (values) => {
return [AiModelTypeEnum.CHAT].includes(values.type);
},
},
rules: 'required',
},
{
fieldName: 'maxContexts',
label: '上下文数量',
component: 'InputNumber',
componentProps: {
min: 0,
max: 20,
controlsPosition: 'right',
placeholder: '请输入上下文数量',
class: 'w-full',
},
dependencies: {
triggerFields: ['type'],
show: (values) => {
return [AiModelTypeEnum.CHAT].includes(values.type);
},
},
rules: 'required',
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '模型名字',
component: 'Input',
},
{
fieldName: 'model',
label: '模型标识',
component: 'Input',
},
{
fieldName: 'platform',
label: '模型平台',
component: 'Input',
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'platform',
title: '所属平台',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.AI_PLATFORM },
},
minWidth: 100,
},
{
field: 'type',
title: '模型类型',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.AI_MODEL_TYPE },
},
minWidth: 100,
},
{
field: 'name',
title: '模型名字',
minWidth: 180,
},
{
title: '模型标识',
field: 'model',
minWidth: 180,
},
{
title: 'API 秘钥',
slots: { default: 'keyId' },
minWidth: 140,
},
{
title: '排序',
field: 'sort',
minWidth: 80,
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
minWidth: 80,
},
{
field: 'temperature',
title: '温度参数',
minWidth: 80,
},
{
title: '回复数 Token 数',
field: 'maxTokens',
minWidth: 140,
},
{
title: '上下文数量',
field: 'maxContexts',
minWidth: 100,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,31 +1,143 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey';
import type { AiModelModelApi } from '#/api/ai/model/model';
import { Button } from 'ant-design-vue';
import { onMounted, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getApiKeySimpleList } from '#/api/ai/model/apiKey';
import { deleteModel, getModelPage } from '#/api/ai/model/model';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const apiKeyList = ref([] as AiModelApiKeyApi.ApiKeyVO[]);
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑 */
function handleEdit(row: AiModelModelApi.ModelVO) {
formModalApi.setData(row).open();
}
/** 删除 */
async function handleDelete(row: AiModelModelApi.ModelVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteModel(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getModelPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<AiModelModelApi.ModelVO>,
});
onMounted(async () => {
//
apiKeyList.value = await getApiKeySimpleList();
});
</script>
<template>
<Page>
<Page auto-content-height>
<DocAlert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/model/index.vue"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/model/index.vue
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['模型配置']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['ai:model:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #keyId="{ row }">
<span>{{
apiKeyList.find((item) => item.id === row.keyId)?.name
}}</span>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['ai:model:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:model:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { AiModelModelApi } from '#/api/ai/model/model';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createModel, getModel, updateModel } from '#/api/ai/model/model';
import {} from '#/api/bpm/model';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AiModelModelApi.ModelVO>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['模型配置'])
: $t('ui.actionTitle.create', ['模型配置']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as AiModelModelApi.ModelVO;
try {
await (formData.value?.id ? updateModel(data) : createModel(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<AiModelModelApi.ModelVO>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getModel(data.id as number);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,111 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'name',
label: '工具名称',
rules: 'required',
},
{
component: 'Textarea',
fieldName: 'description',
label: '工具描述',
componentProps: {
placeholder: '请输入工具描述',
},
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
defaultValue: CommonStatusEnum.ENABLE,
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '工具名称',
component: 'Input',
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
placeholder: ['开始时间', '结束时间'],
valueFormat: 'YYYY-MM-DD HH:mm:ss',
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '工具编号',
},
{
field: 'name',
title: '工具名称',
},
{
field: 'description',
title: '工具描述',
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,34 +1,132 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiModelToolApi } from '#/api/ai/model/tool';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteTool, getToolPage } from '#/api/ai/model/tool';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑 */
function handleEdit(row: AiModelToolApi.ToolVO) {
formModalApi.setData(row).open();
}
/** 删除 */
async function handleDelete(row: AiModelToolApi.ToolVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteTool(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getToolPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<AiModelToolApi.ToolVO>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<DocAlert
title="AI 工具调用function calling"
url="https://doc.iocoder.cn/ai/tool/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/tool/index.vue"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/tool/index.vue
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['工具']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['ai:tool:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['ai:tool:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['ai:tool:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { AiModelToolApi } from '#/api/ai/model/tool';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createTool, getTool, updateTool } from '#/api/ai/model/tool';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AiModelToolApi.ToolVO>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['工具'])
: $t('ui.actionTitle.create', ['工具']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as AiModelToolApi.ToolVO;
try {
await (formData.value?.id ? updateTool(data) : createTool(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<AiModelToolApi.ToolVO>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getTool(data.id as number);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -20,6 +20,7 @@
}
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"@vben/locales": "workspace:*",
"@vben/utils": "workspace:*",
"axios": "catalog:",

View File

@ -1,2 +1,3 @@
export * from './request-client';
export * from '@microsoft/fetch-event-source';
export * from 'axios';

View File

@ -1824,6 +1824,9 @@ importers:
packages/effects/request:
dependencies:
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
'@vben/locales':
specifier: workspace:*
version: link:../../locales
@ -3915,6 +3918,9 @@ packages:
resolution: {integrity: sha512-cszYIcjiNscDoMB1CIKZ3My61+JOhpERGlGr54i6bocvGLrcL/wo9o+RNXMBrb7XgLtKaizZWUpqRduQuHQLdg==}
hasBin: true
'@microsoft/fetch-event-source@2.0.1':
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
'@microsoft/tsdoc-config@0.17.1':
resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==}
@ -13780,6 +13786,8 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@microsoft/fetch-event-source@2.0.1': {}
'@microsoft/tsdoc-config@0.17.1':
dependencies:
'@microsoft/tsdoc': 0.15.1