add form validator for llm config

This commit is contained in:
geekgeekrun
2025-04-13 21:30:26 +08:00
parent 462276cf6e
commit 0e85942de5
3 changed files with 85 additions and 20 deletions
+1
View File
@@ -0,0 +1 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
@@ -7,6 +7,7 @@ import {
writeStorageFile writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { formatResumeJsonToMarkdown } from '../../../common/utils/format-resume-json-to-markdown' import { formatResumeJsonToMarkdown } from '../../../common/utils/format-resume-json-to-markdown'
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../common/constant'
export const sendLookForwardReplyEmotion = async (page: Page) => { export const sendLookForwardReplyEmotion = async (page: Page) => {
const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion') const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion')
@@ -25,6 +26,8 @@ export const sendLookForwardReplyEmotion = async (page: Page) => {
const pickLlmConfigFromList = (llmConfigList) => { const pickLlmConfigFromList = (llmConfigList) => {
if (llmConfigList.length === 1) { if (llmConfigList.length === 1) {
llmConfigList[0].enabled = true
llmConfigList[0].serveWeight = SINGLE_ITEM_DEFAULT_SERVE_WEIGHT
return llmConfigList[0] return llmConfigList[0]
} }
llmConfigList = llmConfigList.filter((it) => it.enabled) llmConfigList = llmConfigList.filter((it) => it.enabled)
@@ -34,6 +37,9 @@ const pickLlmConfigFromList = (llmConfigList) => {
pool.push(i) pool.push(i)
} }
} }
if (!pool.length) {
throw new Error(`cannot find a usable model`)
}
const index = Math.floor(pool.length * Math.random()) const index = Math.floor(pool.length * Math.random())
return llmConfigList[ return llmConfigList[
pool[index] pool[index]
@@ -16,7 +16,7 @@
" "
> >
<el-button size="small" <el-button size="small"
>申请 API Secret <el-icon class="el-icon--right"><arrow-down /></el-icon >获取 API Secret <el-icon class="el-icon--right"><arrow-down /></el-icon
></el-button> ></el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
@@ -63,17 +63,18 @@
<li><b class="color-red">暂不支持推理模型</b>例如 DeepSeek-R1</li> <li><b class="color-red">暂不支持推理模型</b>例如 DeepSeek-R1</li>
<li> <li>
请自行确保您所接入的服务商能够保护您的隐私<b class="color-red" 请自行确保您所接入的服务商能够保护您的隐私<b class="color-red"
>此处所列举服务商-模型由第三方提供仅供配置参考不能保证它们能够合法使用您的数据不表示本程序认可相关模型</b >此处所列举服务商-模型由第三方提供仅供配置参考本程序不能保证它们能够合法使用您的数据不表示本程序认可相关模型</b
> >
</li> </li>
</ul> </ul>
</el-alert> </el-alert>
<el-form <el-form
ref="formRef" ref="formRef"
:model="formContent" :model="formContentForElForm"
:rules="formRules" :rules="formRulesForElForm"
label-position="top" label-position="top"
class="llm-config-form" class="llm-config-form"
:validate-on-rule-change="false"
> >
<div v-for="(conf, index) in formContent" :key="index" class="flex gap12px"> <div v-for="(conf, index) in formContent" :key="index" class="flex gap12px">
<div <div
@@ -114,12 +115,12 @@
/> />
</div> </div>
<div class="w-full"> <div class="w-full">
<el-form-item prop="providerCompleteApiUrl"> <el-form-item :prop="`${index}_providerCompleteApiUrl`">
<div <div
class="el-form-item__label flex-items-center flex-justify-between w-full pr0px" class="el-form-item__label flex-items-center flex-justify-between w-full pr0px"
:style="{ display: 'flex' }" :style="{ display: 'flex' }"
> >
<div>服务提供商 Base Url</div> <div>服务提供商 Base URL</div>
<el-dropdown @command="(item) => handlePresetClick(item, index)"> <el-dropdown @command="(item) => handlePresetClick(item, index)">
<el-button size="small" <el-button size="small"
>配置模板 <el-icon class="el-icon--right"><arrow-down /></el-icon >配置模板 <el-icon class="el-icon--right"><arrow-down /></el-icon
@@ -166,16 +167,13 @@
font-size-12px font-size-12px
></el-input> ></el-input>
</el-form-item> </el-form-item>
<div v-if="formContent.length > 1" class="serve-weight-config"> <div class="serve-weight-config">
<div class="flex"> <div class="flex">
<el-form-item prop="enabled"> <el-form-item prop="enabled">
<div class="el-form-item__label">启用</div> <div class="el-form-item__label">启用</div>
<el-checkbox <el-checkbox
v-if="formContent.length > 1"
v-model="conf.enabled" v-model="conf.enabled"
:autosize="{
minRows: 10,
maxRows: 10
}"
font-size-12px font-size-12px
@click=" @click="
nextTick(() => nextTick(() =>
@@ -183,15 +181,13 @@
) )
" "
></el-checkbox> ></el-checkbox>
<el-checkbox v-else :model-value="true" font-size-12px disabled />
</el-form-item> </el-form-item>
<el-form-item prop="serveWeight" class="ml40px"> <el-form-item prop="serveWeight" class="ml40px">
<div class="el-form-item__label">权重</div> <div class="el-form-item__label">权重</div>
<el-input-number <el-input-number
v-if="formContent.length > 1"
v-model="conf.serveWeight" v-model="conf.serveWeight"
:autosize="{
minRows: 10,
maxRows: 10
}"
:min="0" :min="0"
:max="100" :max="100"
:step="1" :step="1"
@@ -205,6 +201,12 @@
} }
" "
></el-input-number> ></el-input-number>
<el-input-number
v-else
font-size-12px
:model-value="SINGLE_ITEM_DEFAULT_SERVE_WEIGHT"
disabled
/>
</el-form-item> </el-form-item>
</div> </div>
<div class="flex"> <div class="flex">
@@ -247,11 +249,13 @@ import {
ElDropdownItem, ElDropdownItem,
ElIcon, ElIcon,
ElButton, ElButton,
ElInput ElInput,
ElMessage
} from 'element-plus' } from 'element-plus'
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue' import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
import { ref, onMounted, watch, nextTick } from 'vue' import { ref, onMounted, watch, nextTick, computed } from 'vue'
import { gtagRenderer } from '@renderer/utils/gtag' import { gtagRenderer } from '@renderer/utils/gtag'
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../../common/constant'
interface LlmConfigItem { interface LlmConfigItem {
providerCompleteApiUrl: string providerCompleteApiUrl: string
@@ -273,15 +277,69 @@ function getNewConfigItem(): LlmConfigItem {
const formRef = ref<InstanceType<typeof ElForm>>() const formRef = ref<InstanceType<typeof ElForm>>()
const formContent = ref<LlmConfigItem[]>([getNewConfigItem()]) const formContent = ref<LlmConfigItem[]>([getNewConfigItem()])
const formRules = {} const formContentForElForm = computed(() => {
const valueMap = {}
formContent.value.forEach((configItem, i) => {
valueMap[`${i}_providerCompleteApiUrl`] = configItem.providerCompleteApiUrl
})
return valueMap
})
const formRulesForElForm = computed(() => {
const valueMap = {}
formContent.value.forEach((_, i) => {
valueMap[`${i}_providerCompleteApiUrl`] = [
{
required: true,
message: '请输入服务提供商 Base URL'
},
{
trigger: 'blur',
validator(_, value, cb) {
try {
new URL(value?.trim())
} catch (err) {
cb(`URL 格式无效,请重新输入`)
}
if (/^http(s)?:\/\//.test(value)) {
cb()
return
}
cb(`服务提供商 Base URL 无效,请重新输入`)
}
}
]
})
return valueMap
})
const handleCancel = () => { const handleCancel = () => {
gtagRenderer('cancel_clicked') gtagRenderer('cancel_clicked')
electron.ipcRenderer.send('close-llm-config') electron.ipcRenderer.send('close-llm-config')
gtagRenderer('cancel_done') gtagRenderer('cancel_done')
} }
const handleSubmit = async () => { const handleSubmit = async () => {
gtagRenderer('submit_clicked', { llm_config_length: formContent.value.length }) gtagRenderer('submit_clicked', { llm_config_length: formContent.value.length })
await formRef.value?.validate()
if (!formContent.value.length) {
gtagRenderer('empty_model_list')
ElMessage.warning({
message: '可选模型列表为空,请出现填写'
})
formContent.value = [getNewConfigItem()]
return
} else if (formContent.value.length > 1) {
const firstEnabledModel = formContent.value.find(it => it.enabled)
if (!firstEnabledModel) {
gtagRenderer('no_enabled_model_find_in_model_list')
ElMessage.warning({
dangerouslyUseHTMLString: true,
grouping: true,
message: '<div style="white-space: nowrap">所有模型均被禁用;请至少启用一个模型</div>'
})
return
}
}
electron.ipcRenderer.invoke('save-llm-config', JSON.parse(JSON.stringify(formContent.value))) electron.ipcRenderer.invoke('save-llm-config', JSON.parse(JSON.stringify(formContent.value)))
gtagRenderer('submit_done') gtagRenderer('submit_done')
} }
@@ -403,11 +461,11 @@ function handlePresetClick(selected: (typeof llmPresetList)[number], index) {
const firstInputRefList = ref<InstanceType<typeof ElInput>[]>([]) const firstInputRefList = ref<InstanceType<typeof ElInput>[]>([])
function addConfig() { function addConfig() {
gtagRenderer('new_config_item_added', { config_list_length_before_add: formContent.value.length })
formContent.value.push(getNewConfigItem()) formContent.value.push(getNewConfigItem())
nextTick(() => { nextTick(() => {
firstInputRefList.value[firstInputRefList.value.length - 1]?.focus() firstInputRefList.value[firstInputRefList.value.length - 1]?.focus()
}) })
gtagRenderer('new_config_item_added')
} }
function moveConfigUp(index) { function moveConfigUp(index) {
;[formContent.value[index], formContent.value[index - 1]] = [ ;[formContent.value[index], formContent.value[index - 1]] = [
@@ -436,7 +494,7 @@ watch(
if (nVal <= 1) { if (nVal <= 1) {
electron.ipcRenderer.send('update-window-size', { electron.ipcRenderer.send('update-window-size', {
width: window.innerWidth, width: window.innerWidth,
height: 510 height: 550
}) })
} else { } else {
electron.ipcRenderer.send('update-window-size', { electron.ipcRenderer.send('update-window-size', {