add basic llm testing window

This commit is contained in:
geekgeekrun
2025-04-22 03:20:37 +08:00
parent 954553d8d8
commit 3d148e8e01
6 changed files with 319 additions and 95 deletions

View File

@@ -33,7 +33,10 @@ import { WriteStream } from 'node:fs'
import { hasOwn } from '@vue/shared'
import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow'
import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow'
import { getValidTemplate } from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
import {
getValidTemplate,
requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
import {
autoReminderPromptTemplateFileName,
writeDefaultAutoRemindPrompt
@@ -42,6 +45,11 @@ import {
checkIsResumeContentValid,
resumeContentEnoughDetect
} from '../../../../common/utils/resume'
import {
createReadNoReplyReminderLlmMockWindow,
readNoReplyReminderLlmMockWindow
} from '../../../window/readNoReplyReminderLlmMockWindow'
import { RequestSceneEnum } from '../../../features/llm-request-log'
export default function initIpc() {
ipcMain.handle('fetch-config-file-content', async () => {
@@ -539,6 +547,33 @@ export default function initIpc() {
}
}
})
ipcMain.on('test-llm-config-effect', () => {
createReadNoReplyReminderLlmMockWindow({
parent: mainWindow!,
modal: true,
show: true
})
async function requestLlm(_, requestPayload) {
return await requestNewMessageContent(requestPayload.messageList, {
requestScene: RequestSceneEnum.testing
})
}
ipcMain.handle('request-llm-for-test', requestLlm)
readNoReplyReminderLlmMockWindow?.once('closed', () => {
ipcMain.removeHandler('request-llm-for-test')
})
async function getLlmConfigList() {
return await readConfigFile('llm.json')
}
ipcMain.handle('get-llm-config-for-test', getLlmConfigList)
readNoReplyReminderLlmMockWindow?.once('closed', () => {
ipcMain.removeHandler('get-llm-config-for-test')
})
})
ipcMain.on('close-read-no-reply-reminder-llm-mock-window', () =>
readNoReplyReminderLlmMockWindow?.close()
)
ipcMain.handle('exit-app-immediately', () => {
app.exit(0)
})

View File

@@ -115,7 +115,7 @@ export const writeDefaultAutoRemindPrompt = async () => {
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false })
}
export const sendGptContent = async (page: Page, chatRecords) => {
export const requestNewMessageContent = async (chatRecords, { requestScene } = {}) => {
const template = await getValidTemplate()
const resumeObject = (await readConfigFile('resumes.json'))?.[0]
const resumeContent = formatResumeJsonToMarkdown(resumeObject)
@@ -148,6 +148,9 @@ export const sendGptContent = async (page: Page, chatRecords) => {
}
console.log(chatList)
let res, llmConfig
const llmRequestRecord: Omit<LlmModelUsageRecord, 'id' | 'providerApiSecretMd5'> & {
providerApiSecret: string
} = {}
while (!res) {
const llmConfigList = await readConfigFile('llm.json')
llmConfig = pickLlmConfigFromList(llmConfigList)
@@ -155,17 +158,15 @@ export const sendGptContent = async (page: Page, chatRecords) => {
throw new Error(`CANNOT_FIND_A_USABLE_MODEL`)
}
console.log(llmConfig.providerCompleteApiUrl)
const llmRequestRecord: Omit<LlmModelUsageRecord, 'id' | 'providerApiSecretMd5'> & {
providerApiSecret: string
} = {
Object.assign(llmRequestRecord, {
providerCompleteApiUrl: llmConfig.providerCompleteApiUrl,
model: llmConfig.model,
providerApiSecret: llmConfig.providerApiSecret,
requestStartTime: new Date(),
hasError: false,
errorMessage: '',
requestScene: RequestSceneEnum.readNoReplyAutoReminder
}
requestScene
})
try {
const completion = await completes(
{
@@ -227,6 +228,19 @@ export const sendGptContent = async (page: Page, chatRecords) => {
} catch (err) {
throw new Error(`fail to parse response. ${err?.message} ${res?.message?.content}`)
}
return {
responseText: textToSend,
usedLlmConfig: llmConfig,
recordInfo: llmRequestRecord
}
}
export async function sendGptContent(page: Page, chatRecords) {
const textToSend = (
await requestNewMessageContent(chatRecords, {
requestScene: RequestSceneEnum.readNoReplyAutoReminder
})
).responseText
const chatInputSelector = `.chat-conversation .message-controls .chat-input`
const chatInputHandle = await page.$(chatInputSelector)
await chatInputHandle.click()

View File

@@ -0,0 +1,47 @@
import { BrowserWindow } from 'electron'
import path from 'path'
export let readNoReplyReminderLlmMockWindow: BrowserWindow | null = null
export function createReadNoReplyReminderLlmMockWindow(
opt?: Electron.BrowserWindowConstructorOptions
): BrowserWindow {
// Create the browser window.
if (readNoReplyReminderLlmMockWindow) {
readNoReplyReminderLlmMockWindow!.show()
}
readNoReplyReminderLlmMockWindow = new BrowserWindow({
width: 600,
height: 800,
resizable: false,
show: false,
autoHideMenuBar: true,
frame: false,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
readNoReplyReminderLlmMockWindow.on('ready-to-show', () => {
readNoReplyReminderLlmMockWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
readNoReplyReminderLlmMockWindow.loadURL(
process.env['ELECTRON_RENDERER_URL'] + '#/readNoReplyReminderLlmMock'
)
} else {
readNoReplyReminderLlmMockWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/readNoReplyReminderLlmMock'
)
}
readNoReplyReminderLlmMockWindow!.once('closed', () => {
readNoReplyReminderLlmMockWindow = null
})
return readNoReplyReminderLlmMockWindow!
}

View File

@@ -81,7 +81,7 @@
<div>
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板
使用外部编辑器编辑提示词模板 (Markdown)
</el-button>
<el-button
size="small"
@@ -133,6 +133,11 @@
</el-select>
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回复聊过程</el-button
>
</el-form-item>
</template>
</div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
@@ -256,6 +261,100 @@ watch(
}
)
async function checkIsCanRun() {
if (!(await electron.ipcRenderer.invoke('check-is-resume-content-valid'))) {
gtagRenderer('cannot_launch_for_invalid_rc_dialog_show')
try {
await ElMessageBox.confirm(`简历内容无效;您需要编辑一下您的简历`, {
cancelButtonText: '取消',
confirmButtonText: '好的,去编辑我的简历',
dangerouslyUseHTMLString: true
})
gtagRenderer('invalid_rc_dialog_click_confirm')
try {
await electron.ipcRenderer.invoke('resume-edit')
} catch (err) {
console.log(err)
}
} catch {
gtagRenderer('invalid_rc_dialog_click_cancel')
}
return false
}
try {
await electron.ipcRenderer.invoke('check-if-llm-config-list-valid')
} catch (err) {
if (err?.message?.includes(`CANNOT_FIND_VALID_CONFIG`)) {
gtagRenderer('cannot_launch_for_invalid_llm_config')
console.log(`大模型配置无效`, err)
ElMessageBox.confirm(
'大模型配置不存在或者包含无效配置<br />您是否希望查看并修正当前大模型配置?',
'',
{
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
closeOnClickModal: false,
dangerouslyUseHTMLString: true
}
)
.then(async () => {
gtagRenderer('invalid_llm_config_tip_dialog_confirm')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
})
.catch(() => {
gtagRenderer('invalid_llm_config_tip_dialog_cancel')
})
} else {
gtagRenderer('cannot_launch_for_check_llm_config_error', { err })
ElMessage({
type: 'error',
message: '大模型配置检查未通过,请重试'
})
}
return false
}
try {
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid')
} catch (err) {
if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) {
gtagRenderer('cannot_launch_for_no_resume_placehold')
console.log(`提示词模板无效`, err)
ElMessageBox.confirm(
'提示词模板缺少简历内容占位符:<br /><b>__REPLACE_REAL_RESUME_HERE__</b><br /><br />您是否希望还原默认的提示词模板?',
'',
{
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
closeOnClickModal: false,
dangerouslyUseHTMLString: true
}
)
.then(async () => {
gtagRenderer('confirm_invalid_rt_tip_dialog')
await restoreDefaultTemplate()
})
.catch(() => {
gtagRenderer('close_invalid_rt_tip_dialog')
})
} else {
gtagRenderer('cannot_launch_for_check_prompt_error', { err })
ElMessage({
type: 'error',
message: '用于生成自动提醒消息的提示词检查未通过,请重试'
})
}
return false
}
return true
}
const handleSubmit = async () => {
gtagRenderer('run_read_no_reply_reminder_clicked', {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
@@ -270,93 +369,7 @@ const handleSubmit = async () => {
formContent.value.autoReminder?.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
if (!(await electron.ipcRenderer.invoke('check-is-resume-content-valid'))) {
gtagRenderer('cannot_launch_for_invalid_rc_dialog_show')
try {
await ElMessageBox.confirm(`简历内容无效;您需要编辑一下您的简历`, {
cancelButtonText: '取消',
confirmButtonText: '好的,去编辑我的简历',
dangerouslyUseHTMLString: true
})
gtagRenderer('invalid_rc_dialog_click_confirm')
try {
await electron.ipcRenderer.invoke('resume-edit')
} catch (err) {
console.log(err)
}
} catch {
gtagRenderer('invalid_rc_dialog_click_cancel')
}
return
}
try {
await electron.ipcRenderer.invoke('check-if-llm-config-list-valid')
} catch (err) {
if (err?.message?.includes(`CANNOT_FIND_VALID_CONFIG`)) {
gtagRenderer('cannot_launch_for_invalid_llm_config')
console.log(`大模型配置无效`, err)
ElMessageBox.confirm(
'大模型配置不存在或者包含无效配置<br />您是否希望查看并修正当前大模型配置?',
'',
{
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
closeOnClickModal: false,
dangerouslyUseHTMLString: true
}
)
.then(async () => {
gtagRenderer('invalid_llm_config_tip_dialog_confirm')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
})
.catch(() => {
gtagRenderer('invalid_llm_config_tip_dialog_cancel')
})
} else {
gtagRenderer('cannot_launch_for_check_llm_config_error', { err })
ElMessage({
type: 'error',
message: '大模型配置检查未通过,请重试'
})
}
return
}
try {
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid')
} catch (err) {
if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) {
gtagRenderer('cannot_launch_for_no_resume_placehold')
console.log(`提示词模板无效`, err)
ElMessageBox.confirm(
'提示词模板缺少简历内容占位符:<br /><b>__REPLACE_REAL_RESUME_HERE__</b><br /><br />您是否希望还原默认的提示词模板?',
'',
{
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
closeOnClickModal: false,
dangerouslyUseHTMLString: true
}
)
.then(async () => {
gtagRenderer('confirm_invalid_rt_tip_dialog')
await restoreDefaultTemplate()
})
.catch(() => {
gtagRenderer('close_invalid_rt_tip_dialog')
})
} else {
gtagRenderer('cannot_launch_for_check_prompt_error', { err })
ElMessage({
type: 'error',
message: '用于生成自动提醒消息的提示词检查未通过,请重试'
})
}
if (!(await checkIsCanRun())) {
return
}
if (!(await electron.ipcRenderer.invoke('resume-content-enough-detect'))) {
@@ -455,6 +468,13 @@ const rechatLlmFallbackOptions = [
value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
}
]
async function handleTestEffectClicked() {
if (!(await checkIsCanRun())) {
return
}
electron.ipcRenderer.send('test-llm-config-effect')
}
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,101 @@
<template>
<div class="h100vh flex flex-col">
<div
:style="{
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: `auto`,
margin: `0 auto`,
alignItems: `flex-end`
}"
>
<div class="pb20px"></div>
<div v-for="(item, index) in messageList" :key="index" class="message-item">
{{ item.text }}
</div>
<div class="pb20px"></div>
</div>
<div
:style="{
display: 'grid',
gridTemplateColumns: '100px 1fr',
height: `fit-content`,
paddingTop: `10px`,
paddingBottom: `10px`,
backgroundColor: `#f0f0f0`
}"
>
<!-- <el-select v-model="selectedLlmConfig">
<el-option v-for="(it, index) in llmConfigList" :key="index" :label="it">
<div>{{ it.model }}</div>
<div>{{ it.providerCompleteApiUrl }}</div>
</el-option>
</el-select> -->
<el-button ml20px type="text" @click="closeWindow">关闭对话框</el-button>
<el-button
:loading="isLoading"
mr10px
width-fit-content
type="primary"
@click="sendLlmGeneratedContent"
>
<template v-if="isLoading">正在生成消息请稍候...</template>
<template v-else-if="!messageList.length">发送开场白</template>
<template v-else>发送下一句提醒内容</template>
</el-button>
<div></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
type MessageItem = {
text: string
generatedBy: string
}
const messageList = ref<MessageItem[]>([])
// const llmConfigList = ref([])
// async function getLlmConfigList() {
// debugger
// llmConfigList.value = await electron.ipcRenderer.invoke('get-llm-config-for-test')
// }
// getLlmConfigList().catch(() => {})
// const selectedLlmConfig = ref()
const isLoading = ref(false)
async function sendLlmGeneratedContent() {
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: JSON.parse(JSON.stringify(messageList.value ?? []))
})
console.log(response)
messageList.value.push({
text: response.responseText,
generatedBy: response.usedLlmConfig
})
} finally {
isLoading.value = false
}
}
function closeWindow() {
electron.ipcRenderer.send(`close-read-no-reply-reminder-llm-mock-window`)
}
</script>
<style lang="scss" scoped>
.message-item {
line-height: 1.25em;
font-size: 14px;
background-color: #d1f0ef;
color: #333;
padding: 10px;
border-radius: 8px 8px 0 8px;
margin-top: 20px;
max-width: 420px;
}
</style>

View File

@@ -31,6 +31,13 @@ const routes: Array<RouteRecordRaw> = [
title: '简历编辑'
}
},
{
path: '/readNoReplyReminderLlmMock',
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
meta: {
title: '已读不回提醒器 大语言模型测试'
}
},
{
path: '/main-layout',
component: () => import('@renderer/page/MainLayout/index.vue'),