kagg886 3 miesięcy temu
rodzic
commit
c7bf292ab7

+ 27 - 1
src/api/assist/index.ts

@@ -1,8 +1,34 @@
-import { ChatRequest, ChatResponse, ErrorResponse } from '/@/api/assist/type'
+import {
+	ChatRequest,
+	ChatResponse,
+	ErrorResponse,
+	LmConfigListParams,
+	LmConfigAddReq,
+	LmConfigEditReq,
+	LmConfigStatusReq,
+	LmConfigDeleteParams,
+	LmConfigGetParams
+} from '/@/api/assist/type'
+import { get, post, del, put } from '/@/utils/request'
 import getOrigin from '/@/utils/origin'
 import { getToken } from "/@/utils/auth";
 
 export default {
+	model: {
+		// 大语言模型配置列表
+		getList: (params?: LmConfigListParams) => get('/system/lmconfig/list', params),
+		// 获取大语言模型配置信息
+		detail: (params: LmConfigGetParams) => get('/system/lmconfig/get', params),
+		// 添加大语言模型配置
+		add: (data: LmConfigAddReq) => post('/system/lmconfig/add', data),
+		// 修改大语言模型配置
+		edit: (data: LmConfigEditReq) => put('/system/lmconfig/edit', data),
+		// 删除大语言模型配置
+		del: (params: LmConfigDeleteParams) => del('/system/lmconfig/delete', params),
+		// 设置大语言模型配置状态
+		setStatus: (data: LmConfigStatusReq) => put('/system/lmconfig/status', data),
+	},
+
 	// SSE聊天方法
 	chat: (
 		data:

+ 60 - 0
src/api/assist/type.ts

@@ -61,3 +61,63 @@ export type MetaResponse = ChatResponseBase<'meta'> & {
 }
 
 export type ChatResponse = Text | ToolCallRequest | ToolCallResponse | ErrorResponse
+
+// 大语言模型配置相关类型定义
+
+// 大语言模型配置列表查询参数
+export type LmConfigListParams = {
+	keyWord?: string // 搜索关键字
+	dateRange?: string[] // 日期范围
+	OrderBy?: string // 排序
+	pageNum?: number // 分页号码,默认1
+	pageSize?: number // 分页数量,最大500,默认10
+	modelClass?: string // 模型分类
+	modelName?: string // 模型名称
+	modelType?: string // 模型类型
+	status?: string // 是否启用
+	createdAt?: string // 创建时间
+}
+
+// 大语言模型配置基础信息
+export type LmConfigInfo = {
+	id?: number
+	modelClass?: string // 模型分类
+	modelName?: string // 模型名称
+	apiKey?: string // API密钥
+	baseUrl?: string // 基础URL
+	modelType?: string // 模型类型
+	isCallFun?: boolean // 是否调用函数
+	maxToken?: number // 最大令牌数
+	status: boolean // 是否启用
+	createdAt?: string // 创建时间
+	updatedAt?: string // 更新时间
+	createdBy?: number // 创建者ID
+	updatedBy?: number // 更新者ID
+	createdUser?: any // 创建用户信息
+	actionBtn?: any // 操作按钮
+	[key: string]: any // 允许其他字段
+}
+
+// 大语言模型配置添加请求
+export type LmConfigAddReq = Omit<LmConfigInfo, 'id' | 'createdAt' | 'updatedAt'>
+
+// 大语言模型配置编辑请求
+export type LmConfigEditReq = LmConfigInfo & {
+	id: number // 编辑时ID必须
+}
+
+// 大语言模型配置状态设置请求
+export type LmConfigStatusReq = {
+	id: number
+	status: string
+}
+
+// 删除请求参数
+export type LmConfigDeleteParams = {
+	ids: number[]
+}
+
+// 获取单个配置参数
+export type LmConfigGetParams = {
+	id: number
+}

+ 53 - 33
src/views/assistant/index.vue

@@ -7,7 +7,10 @@ import EChartsPlugin from '/@/components/markdown/plugins/echarts'
 import ToolsLoadingPlugin from '/@/components/markdown/plugins/tools-loading'
 import Markdown from '/@/components/markdown/Markdown.vue'
 import assist from '/@/api/assist'
-import { ChatResponse, Message } from '/@/api/assist/type'
+import { ChatResponse, LmConfigInfo, Message } from '/@/api/assist/type'
+import { useLoading } from '/@/utils/loading-util'
+import { Setting as EleSetting } from '@element-plus/icons-vue'
+import { useRouter } from 'vue-router'
 
 const plugins: Array<MarkdownPlugin<any>> = [EChartsPlugin(), ToolsLoadingPlugin()]
 
@@ -19,6 +22,8 @@ const messages = ref<Message[]>([])
 const inputMessage = ref('')
 const messagesContainer = ref<HTMLElement>()
 
+// 选中的工具和模型
+const selectedTool = ref([])
 // 工具选择
 const toolOptions = ref([
 	{
@@ -73,16 +78,23 @@ const prompt = ref<string>('')
 const openPromptDialog = ref(false)
 
 // 模型选择
-const modelOptions = ref([
-	{ label: 'Claude Sonnet 4', value: 'claude-sonnet-4' },
-	{ label: 'GPT-4', value: 'gpt-4' },
-	{ label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' },
-	{ label: 'Gemini Pro', value: 'gemini-pro' },
-])
+const modelOptions = ref<LmConfigInfo[]>([])
+
+const {loading: loadingModels, doLoading: loadModel} = useLoading(async ()=> {
+	const data: {list: LmConfigInfo[],total: number} = await assist.model.getList().catch(() => {
+		return {
+			list: []
+		}
+	})
+
+	modelOptions.value = data.list
+	selectedModel.value = data.list[0]?.modelName ?? undefined
+})
+
+onMounted(loadModel)
+
+const selectedModel = ref<string | undefined>(undefined)
 
-// 选中的工具和模型
-const selectedTool = ref([])
-const selectedModel = ref('claude-sonnet-4')
 
 const chatInstance = ref<(() => void) | undefined>(undefined)
 onUnmounted(() => chatInstance.value?.())
@@ -255,6 +267,9 @@ const getUserInfos = ref<{
 	avatar: string
 	userName: string
 }>(Local.get('userInfo') || {})
+
+const router = useRouter()
+const redirectToModelManager = () => router.push('manage/model')
 </script>
 
 <template>
@@ -263,6 +278,7 @@ const getUserInfos = ref<{
 		<el-aside width="300px" class="chat-sidebar">
 			<div class="sidebar-header">
 				<h3>对话历史</h3>
+				<el-button round :icon="EleSetting" size="small" @click="redirectToModelManager"></el-button>
 			</div>
 			<el-scrollbar class="conversation-list">
 				<div
@@ -428,8 +444,8 @@ const getUserInfos = ref<{
 
 					<!-- 模型选择 -->
 					<div class="model-selector">
-						<el-select v-model="selectedModel" placeholder="选择模型" size="small" style="width: 200px">
-							<el-option v-for="item in modelOptions" :key="item.value" :label="item.label" :value="item.value" />
+						<el-select :loading="loadingModels" v-model="selectedModel" placeholder="选择模型" size="small" style="width: 200px">
+							<el-option v-for="item in modelOptions" :key="item.id" :label="item.modelName" :value="item.id" />
 						</el-select>
 					</div>
 
@@ -487,29 +503,31 @@ const getUserInfos = ref<{
 				</div>
 			</div>
 		</el-main>
+
+		<!-- 提示词设置对话框 -->
+		<el-dialog
+			v-model="openPromptDialog"
+			title="设置提示词"
+			width="600px"
+			:before-close="() => { openPromptDialog = false }"
+		>
+			<el-input
+				v-model="prompt"
+				type="textarea"
+				placeholder="请输入提示词..."
+				:rows="8"
+				resize="none"
+			/>
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="openPromptDialog = false">取消</el-button>
+					<el-button type="primary" @click="openPromptDialog = false">确定</el-button>
+				</div>
+			</template>
+		</el-dialog>
 	</el-container>
 
-	<!-- 提示词设置对话框 -->
-	<el-dialog
-		v-model="openPromptDialog"
-		title="设置提示词"
-		width="600px"
-		:before-close="() => { openPromptDialog = false }"
-	>
-		<el-input
-			v-model="prompt"
-			type="textarea"
-			placeholder="请输入提示词..."
-			:rows="8"
-			resize="none"
-		/>
-		<template #footer>
-			<div class="dialog-footer">
-				<el-button @click="openPromptDialog = false">取消</el-button>
-				<el-button type="primary" @click="openPromptDialog = false">确定</el-button>
-			</div>
-		</template>
-	</el-dialog>
+
 </template>
 
 <style scoped lang="scss">
@@ -537,6 +555,8 @@ const getUserInfos = ref<{
 .sidebar-header {
 	padding: 20px;
 	border-bottom: 1px solid var(--el-border-color-light);
+	display: flex;
+	justify-content: space-between;
 
 	h3 {
 		margin: 0;

+ 448 - 0
src/views/assistant/manage/model.vue

@@ -0,0 +1,448 @@
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search as EleSearch, Refresh as EleRefresh, Plus as ElePlus, Delete as EleDelete, Edit as EleEdit } from '@element-plus/icons-vue'
+import { useLoading } from '/@/utils/loading-util'
+import api from '/@/api/assist'
+import type { LmConfigInfo, LmConfigListParams } from '/@/api/assist/type'
+
+// 数据搜索部分
+const searchParam = reactive<LmConfigListParams>({
+	pageNum: 1,
+	pageSize: 10,
+	keyWord: '',
+	modelClass: '',
+	modelName: '',
+	modelType: '',
+	status: '',
+	dateRange: [],
+})
+
+const total = ref<number>(0)
+const data = ref<Array<LmConfigInfo>>([])
+const ids = ref<number[]>([])
+
+// 加载列表数据
+const { loading, doLoading: doListLoad } = useLoading(async () => {
+	try {
+		const res: {
+			list: LmConfigInfo[]
+			total: number
+		} = await api.model.getList(searchParam)
+		total.value = res.total
+		data.value = res.list
+	} catch (error) {
+		console.error('获取模型列表失败:', error)
+		data.value = []
+		total.value = 0
+	}
+})
+
+// 重置搜索条件
+const reset = () => {
+	Object.assign(searchParam, {
+		pageNum: 1,
+		pageSize: 10,
+		keyWord: '',
+		modelClass: '',
+		modelName: '',
+		modelType: '',
+		status: '',
+		dateRange: [],
+	})
+	doListLoad()
+}
+
+// 选择删除项
+const onDeleteItemSelected = (selection: LmConfigInfo[]) => {
+	ids.value = selection.map((item) => item.id!).filter(Boolean)
+}
+
+// 批量删除
+const del = async () => {
+	if (ids.value.length === 0) {
+		ElMessage.error('请选择要删除的数据')
+		return
+	}
+
+	try {
+		await ElMessageBox.confirm('您确定要删除所选数据吗?', '提示', {
+			confirmButtonText: '确认',
+			cancelButtonText: '取消',
+			type: 'warning',
+		})
+
+		await api.model.del({ ids: ids.value })
+		ElMessage.success('删除成功')
+		await doListLoad()
+	} catch (error) {
+		if (error !== 'cancel') {
+			console.error('删除失败:', error)
+			ElMessage.error('删除失败')
+		}
+	}
+}
+
+// 单个删除
+const delSingle = async (id: number) => {
+	try {
+		await ElMessageBox.confirm('您确定要删除这条数据吗?', '提示', {
+			confirmButtonText: '确认',
+			cancelButtonText: '取消',
+			type: 'warning',
+		})
+
+		await api.model.del({ ids: [id] })
+		ElMessage.success('删除成功')
+		await doListLoad()
+	} catch (error) {
+		if (error !== 'cancel') {
+			console.error('删除失败:', error)
+			ElMessage.error('删除失败')
+		}
+	}
+}
+
+// 切换状态
+const toggleStatus = async (row: LmConfigInfo) => {
+	const newStatus = !row.status
+	const statusText = newStatus ? '启用' : '禁用'
+
+	try {
+		await ElMessageBox.confirm(`您确定要${statusText}这个模型配置吗?`, '提示', {
+			confirmButtonText: '确认',
+			cancelButtonText: '取消',
+			type: 'warning',
+		})
+
+		await api.model.setStatus({ id: row.id!, status: newStatus.toString() })
+		ElMessage.success(`${statusText}成功`)
+		await doListLoad()
+	} catch (error) {
+		if (error !== 'cancel') {
+			console.error(`${statusText}失败:`, error)
+			ElMessage.error(`${statusText}失败`)
+		}
+	}
+}
+
+// 编辑/新增对话框相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const isEdit = ref(false)
+const formRef = ref()
+
+const formData = reactive<LmConfigInfo>({
+	id: undefined,
+	modelClass: '',
+	modelName: '',
+	apiKey: '',
+	baseUrl: '',
+	modelType: '',
+	isCallFun: false,
+	maxToken: 0,
+	status: true,
+})
+
+// 表单验证规则
+const formRules = {
+	modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
+	modelClass: [{ required: true, message: '请输入模型分类', trigger: 'blur' }],
+	modelType: [{ required: true, message: '请输入模型类型', trigger: 'blur' }],
+	apiKey: [{ required: true, message: '请输入API密钥', trigger: 'blur' }],
+	baseUrl: [{ required: true, message: '请输入基础URL', trigger: 'blur' }],
+	maxToken: [{ type: 'number', message: '最大令牌数必须为数字', trigger: 'blur' },],
+}
+
+// 重置表单
+const resetForm = () => {
+	Object.assign(formData, {
+		id: undefined,
+		modelClass: '',
+		modelName: '',
+		apiKey: '',
+		baseUrl: '',
+		modelType: '',
+		isCallFun: false,
+		maxToken: undefined,
+		status: true,
+	})
+	formRef.value?.clearValidate()
+}
+
+// 打开新增对话框
+const openAddDialog = () => {
+	resetForm()
+	dialogTitle.value = '新增模型配置'
+	isEdit.value = false
+	dialogVisible.value = true
+}
+
+// 打开编辑对话框
+const openEditDialog = async (row: LmConfigInfo) => {
+	try {
+		const res = await api.model.detail({ id: row.id! })
+		Object.assign(formData, res)
+		dialogTitle.value = '编辑模型配置'
+		isEdit.value = true
+		dialogVisible.value = true
+	} catch (error) {
+		console.error('获取模型详情失败:', error)
+		ElMessage.error('获取模型详情失败')
+	}
+}
+
+// 保存表单
+const { loading: saveLoading, doLoading: doSave } = useLoading(async () => {
+	try {
+		await formRef.value?.validate()
+
+		if (isEdit.value) {
+			await api.model.edit(formData as any)
+			ElMessage.success('编辑成功')
+		} else {
+			await api.model.add(formData as any)
+			ElMessage.success('新增成功')
+		}
+
+		dialogVisible.value = false
+		await doListLoad()
+	} catch (error) {
+		console.error('保存失败:', error)
+		if (error !== false) {
+			// 表单验证失败时不显示错误消息
+			ElMessage.error('保存失败')
+		}
+	}
+})
+
+// 组件挂载时加载数据
+onMounted(() => {
+	doListLoad()
+})
+</script>
+
+<template>
+	<el-card shadow="never" class="page">
+		<!-- 搜索表单 -->
+		<el-form :model="searchParam" inline>
+			<el-form-item label="" prop="keyWord">
+				<el-input style="width: 200px" v-model="searchParam.keyWord" placeholder="搜索关键字" clearable />
+			</el-form-item>
+			<el-form-item label="" prop="modelClass">
+				<el-input style="width: 150px" v-model="searchParam.modelClass" placeholder="模型分类" clearable />
+			</el-form-item>
+			<el-form-item label="" prop="modelName">
+				<el-input style="width: 150px" v-model="searchParam.modelName" placeholder="模型名称" clearable />
+			</el-form-item>
+			<el-form-item label="" prop="modelType">
+				<el-input style="width: 150px" v-model="searchParam.modelType" placeholder="模型类型" clearable />
+			</el-form-item>
+			<el-form-item label="" prop="status">
+				<el-select style="width: 125px" v-model="searchParam.status" placeholder="状态" clearable>
+					<el-option label="全部" value="" />
+					<el-option label="启用" value="true" />
+					<el-option label="禁用" value="false" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="" prop="dateRange">
+				<el-date-picker
+					v-model="searchParam.dateRange"
+					style="width: 220px"
+					value-format="YYYY-MM-DD"
+					type="daterange"
+					range-separator="-"
+					start-placeholder="开始时间"
+					end-placeholder="结束时间"
+				/>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="doListLoad">
+					<el-icon>
+						<EleSearch />
+					</el-icon>
+					查询
+				</el-button>
+				<el-button @click="reset">
+					<el-icon>
+						<EleRefresh />
+					</el-icon>
+					重置
+				</el-button>
+				<el-button type="primary" @click="openAddDialog">
+					<el-icon>
+						<ElePlus />
+					</el-icon>
+					新增模型
+				</el-button>
+				<el-button type="danger" @click="del" :disabled="ids.length === 0">
+					<el-icon>
+						<EleDelete />
+					</el-icon>
+					批量删除
+				</el-button>
+			</el-form-item>
+		</el-form>
+
+		<!-- 数据表格 -->
+		<el-table :data="data" style="width: 100%" v-loading="loading" @selection-change="onDeleteItemSelected">
+			<el-table-column type="selection" width="50" align="center" />
+			<el-table-column label="ID" prop="id" width="80" align="center" />
+			<el-table-column label="模型名称" prop="modelName" align="center" show-overflow-tooltip />
+			<el-table-column label="模型分类" prop="modelClass" align="center" show-overflow-tooltip />
+			<el-table-column label="模型类型" prop="modelType" align="center" show-overflow-tooltip />
+			<el-table-column label="状态" prop="status" width="100" align="center">
+				<template #default="scope">
+					<el-tag :type="scope.row.status ? 'success' : 'danger'" size="small">
+						{{ scope.row.status ? '启用' : '禁用' }}
+					</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column label="创建时间" prop="createdAt" width="180" align="center" />
+			<el-table-column label="更新时间" prop="updatedAt" width="180" align="center" />
+			<el-table-column label="操作" width="200" align="center" fixed="right">
+				<template #default="scope">
+					<el-button text type="primary" size="small" @click="openEditDialog(scope.row)">
+						编辑
+					</el-button>
+					<el-button text :type="scope.row.status ? 'warning' : 'success'" size="small" @click="toggleStatus(scope.row)">
+						{{ scope.row.status ? '禁用' : '启用' }}
+					</el-button>
+					<el-button text type="danger" size="small" @click="delSingle(scope.row.id)">
+						删除
+					</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<!-- 分页 -->
+		<div class="pagination-container">
+			<el-pagination
+				v-show="total > 0"
+				:current-page="searchParam.pageNum"
+				:page-size="searchParam.PageSize"
+				:page-sizes="[10, 20, 50, 100]"
+				:total="total"
+				layout="total, sizes, prev, pager, next, jumper"
+				@size-change="(size: number) => { searchParam.PageSize = size; doListLoad(); }"
+				@current-change="(page: number) => { searchParam.pageNum = page; doListLoad(); }"
+			/>
+		</div>
+
+		<!-- 编辑/新增对话框 -->
+		<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close @close="resetForm">
+			<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
+				<el-form-item label="模型名称" prop="modelName">
+					<el-input v-model="formData.modelName" placeholder="请输入模型名称" clearable />
+				</el-form-item>
+				<el-form-item label="模型分类" prop="modelClass">
+					<el-input v-model="formData.modelClass" placeholder="请输入模型分类" clearable />
+				</el-form-item>
+				<el-form-item label="模型类型" prop="modelType">
+					<el-input v-model="formData.modelType" placeholder="请输入模型类型" clearable />
+				</el-form-item>
+				<el-form-item label="API密钥" prop="apiKey">
+					<el-input v-model="formData.apiKey" type="password" placeholder="请输入API密钥" clearable show-password />
+				</el-form-item>
+				<el-form-item label="基础URL" prop="baseUrl">
+					<el-input v-model="formData.baseUrl" placeholder="请输入基础URL" clearable />
+				</el-form-item>
+				<el-form-item label="最大令牌数" prop="maxToken">
+					<el-input-number v-model="formData.maxToken" :min="1" :max="999999" placeholder="请输入最大令牌数" style="width: 100%" />
+				</el-form-item>
+				<el-form-item label="调用函数" prop="isCallFun">
+					<el-radio-group v-model="formData.isCallFun">
+						<el-radio :label="true">是</el-radio>
+						<el-radio :label="false">否</el-radio>
+					</el-radio-group>
+				</el-form-item>
+				<el-form-item label="状态" prop="status">
+					<el-radio-group v-model="formData.status">
+						<el-radio :label="true">启用</el-radio>
+						<el-radio :label="false">禁用</el-radio>
+					</el-radio-group>
+				</el-form-item>
+			</el-form>
+
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="dialogVisible = false">取消</el-button>
+					<el-button type="primary" @click="doSave" :loading="saveLoading"> 确定 </el-button>
+				</div>
+			</template>
+		</el-dialog>
+	</el-card>
+</template>
+
+<style scoped lang="scss">
+.page {
+	margin: 20px;
+
+	.el-form {
+		margin-bottom: 20px;
+
+		.el-form-item {
+			margin-bottom: 20px;
+		}
+	}
+
+	.pagination-container {
+		margin-top: 20px;
+		display: flex;
+		justify-content: center;
+	}
+
+	.dialog-footer {
+		text-align: right;
+
+		.el-button {
+			margin-left: 10px;
+		}
+	}
+}
+
+// 表格样式优化
+.el-table {
+	.el-table__header {
+		th {
+			background-color: var(--el-bg-color-page);
+			color: var(--el-text-color-primary);
+			font-weight: 600;
+		}
+	}
+
+	.el-table__row {
+		&:hover {
+			background-color: var(--el-bg-color-page);
+		}
+	}
+}
+
+// 按钮组样式
+.el-form-item:last-child {
+	.el-button {
+		margin-right: 10px;
+
+		&:last-child {
+			margin-right: 0;
+		}
+	}
+}
+
+// 状态标签样式
+.el-tag {
+	font-weight: 500;
+}
+
+// 操作按钮样式
+.el-table__fixed-right {
+	.el-button {
+		margin: 0 2px;
+
+		&.el-button--small {
+			padding: 5px 8px;
+			font-size: 12px;
+		}
+	}
+}
+</style>