Merge branch 'feature/ui'

This commit is contained in:
geekgeekrun
2026-02-27 01:03:06 +08:00
16 changed files with 652 additions and 312 deletions
+5
View File
@@ -1,2 +1,7 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1 export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154' export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
export const DEFAULT_CONSTANT_OPEN_CONTENT_SEGS = [
`您好,我对贵公司岗位很感兴趣,希望能有机会进一步沟通,期待您的回复`,
`接下来的聊天,我将根据我简历里的内容,向您进行自我介绍`
]
@@ -18,6 +18,16 @@ export enum RECHAT_LLM_FALLBACK {
EXIT_REMINDER_PROGRAM = 2 EXIT_REMINDER_PROGRAM = 2
} }
export enum OPEN_CONTENT_SOURCE {
CONSTANT_CONTENT = 1,
GEMINI_WITH_CHAT_CONTEXT = 2
}
// export enum OPEN_LLM_FALLBACK {
// SEND_CONSTANT_CONTENT = 1,
// EXIT_REMINDER_PROGRAM = 2
// }
export enum RUNNING_STATUS_ENUM { export enum RUNNING_STATUS_ENUM {
RUNNING = 0, RUNNING = 0,
NORMAL_EXITED = 1, NORMAL_EXITED = 1,
@@ -32,7 +32,7 @@ import {
requestNewMessageContent requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation' } from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import { import {
autoReminderPromptTemplateFileName, defaultPromptMap,
writeDefaultAutoRemindPrompt writeDefaultAutoRemindPrompt
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation' } from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import { import {
@@ -466,17 +466,19 @@ export default function initIpc() {
const res = (await readConfigFile('resumes.json'))?.[0] const res = (await readConfigFile('resumes.json'))?.[0]
return res?.content ?? null return res?.content ?? null
}) })
ipcMain.on('no-reply-reminder-prompt-edit', async () => { ipcMain.on('no-reply-reminder-prompt-edit', async (_, { type }) => {
const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false }) const template = await readStorageFile(defaultPromptMap[type].fileName, {
isJson: false
})
if (!template) { if (!template) {
await writeDefaultAutoRemindPrompt() await writeDefaultAutoRemindPrompt({ type })
} }
const filePath = path.join(storageFilePath, autoReminderPromptTemplateFileName) const filePath = path.join(storageFilePath, defaultPromptMap[type].fileName)
shell.openPath(filePath) shell.openPath(filePath)
}) })
ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close()) ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close())
ipcMain.handle('check-if-auto-remind-prompt-valid', async () => { ipcMain.handle('check-if-auto-remind-prompt-valid', async (_, { type }) => {
await getValidTemplate() await getValidTemplate({ type })
}) })
ipcMain.handle('check-is-resume-content-valid', async () => { ipcMain.handle('check-is-resume-content-valid', async () => {
const res = (await readConfigFile('resumes.json'))?.[0] const res = (await readConfigFile('resumes.json'))?.[0]
@@ -486,8 +488,8 @@ export default function initIpc() {
const res = (await readConfigFile('resumes.json'))?.[0] const res = (await readConfigFile('resumes.json'))?.[0]
return resumeContentEnoughDetect(res) return resumeContentEnoughDetect(res)
}) })
ipcMain.handle('overwrite-auto-remind-prompt-with-default', async () => { ipcMain.handle('overwrite-auto-remind-prompt-with-default', async (_, { type }) => {
await writeDefaultAutoRemindPrompt() await writeDefaultAutoRemindPrompt({ type })
}) })
ipcMain.handle('check-if-llm-config-list-valid', async () => { ipcMain.handle('check-if-llm-config-list-valid', async () => {
const llmConfigList = await readConfigFile('llm.json') const llmConfigList = await readConfigFile('llm.json')
@@ -60,7 +60,10 @@ const pickLlmConfigFromList = (llmConfigList, blockModelSet) => {
// let _index = 0 // let _index = 0
const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__` const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__`
const defaultPrompt = `**核心指令:** export const defaultPromptMap = {
rechat: {
fileName: 'auto-reminder-resume-system-message-template.md',
content: `**核心指令:**
你是一个智能求职助手,需要根据用户简历生成30字左右的提醒消息,满足以下要求: 你是一个智能求职助手,需要根据用户简历生成30字左右的提醒消息,满足以下要求:
1. 每次生成需满足: 1. 每次生成需满足:
- √ 包含1个核心技能 + 1个成果量化 - √ 包含1个核心技能 + 1个成果量化
@@ -94,15 +97,21 @@ const defaultPrompt = `**核心指令:**
**输出格式:** **输出格式:**
请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:\`{"response": "这里是将会发送给招聘者的内容"}\`` 请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:\`{"response": "这里是将会发送给招聘者的内容"}\``
},
export const autoReminderPromptTemplateFileName = 'auto-reminder-resume-system-message-template.md' open: {
export const getValidTemplate = async () => { fileName: 'auto-reminder-open-message-template.md',
let template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false }) content:
if (!template) { '请根据我的简历,帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容,但需要表达出应聘意向。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`'
await writeDefaultAutoRemindPrompt()
template = defaultPrompt
} }
if (!template.includes(RESUME_PLACEHOLDER)) { }
export const getValidTemplate = async ({ type }) => {
let template = await readStorageFile(defaultPromptMap[type].fileName, { isJson: false })
if (!template) {
await writeDefaultAutoRemindPrompt({ type })
template = defaultPromptMap[type].content
}
if (type === 'rechat' && !template.includes(RESUME_PLACEHOLDER)) {
const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`) const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`)
e.name = `RESUME_PLACEHOLDER_NOT_EXIST` e.name = `RESUME_PLACEHOLDER_NOT_EXIST`
throw e throw e
@@ -110,8 +119,19 @@ export const getValidTemplate = async () => {
return template return template
} }
export const writeDefaultAutoRemindPrompt = async () => { export const writeDefaultAutoRemindPrompt = async ({ type }) => {
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false }) switch (type) {
case 'rechat':
await writeStorageFile(defaultPromptMap[type].fileName, defaultPromptMap[type].content, {
isJson: false
})
break
case 'open':
await writeStorageFile(defaultPromptMap[type].fileName, defaultPromptMap[type].content, {
isJson: false
})
break
}
} }
export const requestNewMessageContent = async ( export const requestNewMessageContent = async (
@@ -119,24 +139,26 @@ export const requestNewMessageContent = async (
{ {
requestScene, requestScene,
llmConfigIdForPick llmConfigIdForPick
}: { requestScene?: RequestSceneEnum; llmConfigIdForPick?: string[] } = {} }: {
requestScene?: RequestSceneEnum
llmConfigIdForPick?: string[]
} = {}
) => { ) => {
const template = await getValidTemplate() const systemMessageTemplate = await getValidTemplate({ type: 'rechat' })
const resumeObject = (await readConfigFile('resumes.json'))?.[0] const resumeObject = (await readConfigFile('resumes.json'))?.[0]
const resumeContent = formatResumeJsonToMarkdown(resumeObject) const resumeContent = formatResumeJsonToMarkdown(resumeObject)
const chatList = [ const chatList = [
{ {
role: 'system', role: 'system',
content: template.replace(RESUME_PLACEHOLDER, resumeContent) content: systemMessageTemplate.replace(RESUME_PLACEHOLDER, resumeContent)
} }
] ]
const openMessageTemplate = await getValidTemplate({ type: 'open' })
chatList.push({ chatList.push({
role: 'user', role: 'user',
content: content: openMessageTemplate
'请根据我的简历,帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容,但需要表达出应聘意向。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`'
}) })
// chatRecords = chatRecords.slice(chatRecords.length - _index) // chatRecords = chatRecords.slice(chatRecords.length - _index)
// debugger
for (const record of chatRecords) { for (const record of chatRecords) {
const assistantJsonContent = JSON.stringify({ const assistantJsonContent = JSON.stringify({
response: record.text response: record.text
@@ -246,14 +268,18 @@ export const requestNewMessageContent = async (
} }
} }
export async function sendGptContent(page: Page, chatRecords) { export async function getGptContent(chatRecords) {
const textToSend = ( const textToSend = (
await requestNewMessageContent(chatRecords, { await requestNewMessageContent(chatRecords, {
requestScene: RequestSceneEnum.readNoReplyAutoReminder requestScene: RequestSceneEnum.readNoReplyAutoReminder
}) })
).responseText ).responseText
return textToSend
}
export async function sendMessage(page: Page, textToSend: string) {
const chatInputSelector = `.chat-conversation .message-controls .chat-input` const chatInputSelector = `.chat-conversation .message-controls .chat-input`
const chatInputHandle = await page.$(chatInputSelector) const chatInputHandle = (await page.$(chatInputSelector))!
await chatInputHandle.click() await chatInputHandle.click()
await sleep(500) await sleep(500)
await chatInputHandle.click() await chatInputHandle.click()
@@ -1,7 +1,7 @@
import { bootstrap, launchBoss } from './bootstrap' import { bootstrap, launchBoss } from './bootstrap'
import { MsgStatus, type ChatListItem } from './types' import { MsgStatus, type ChatListItem } from './types'
import { Browser, Page } from 'puppeteer' import { Browser, Page } from 'puppeteer'
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation' import { getGptContent, sendLookForwardReplyEmotion, sendMessage } from './boss-operation'
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs' import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs' import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
import { app, dialog } from 'electron' import { app, dialog } from 'electron'
@@ -24,6 +24,7 @@ import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
import { messageForSaveFilter } from '../../../common/utils/chat-list' import { messageForSaveFilter } from '../../../common/utils/chat-list'
import { import {
AUTO_CHAT_ERROR_EXIT_CODE, AUTO_CHAT_ERROR_EXIT_CODE,
OPEN_CONTENT_SOURCE,
RECHAT_CONTENT_SOURCE, RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK RECHAT_LLM_FALLBACK
} from '../../../common/enums/auto-start-chat' } from '../../../common/enums/auto-start-chat'
@@ -40,6 +41,7 @@ import { loginWithCookieAssistant } from '../../features/login-with-cookie-assis
import initPublicIpc from '../../utils/initPublicIpc' import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history' import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant' import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../common/constant'
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
console.log('收到SIGTERM信号,正在退出') console.log('收到SIGTERM信号,正在退出')
@@ -87,6 +89,21 @@ const onlyRemindBossWithoutBlockCompanyName =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithoutBlockCompanyName ?? readConfigFile('boss.json').autoReminder?.onlyRemindBossWithoutBlockCompanyName ??
!!blockCompanyNameRegExp !!blockCompanyNameRegExp
const openContentSource = readConfigFile('boss.json').autoReminder?.openContentSource ?? OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
const constantOpenContent = (() => {
let constantOpenContent = readConfigFile('boss.json').autoReminder?.constantOpenContent ?? ''
if (constantOpenContent?.trim?.()) {
return constantOpenContent
} else {
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
constantOpenContent = DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join(``)
} else {
constantOpenContent = DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
}
}
return constantOpenContent
})()
const dbInitPromise = initDb(getPublicDbFilePath()) const dbInitPromise = initDb(getPublicDbFilePath())
export const pageMapByName: { export const pageMapByName: {
@@ -582,30 +599,50 @@ const mainLoop = async () => {
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000 (throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
) { ) {
await sleepWithRandomDelay(3250) await sleepWithRandomDelay(3250)
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) { const messageList = historyMessageList
try { .filter((it) => it.bizType !== 101 && it.isSelf)
const messageListForGpt = historyMessageList .slice(-recentMessageQuantityForLlm)
.filter((it) => it.bizType !== 101 && it.isSelf) if (!messageList?.length) {
.slice(-recentMessageQuantityForLlm) if (openContentSource === OPEN_CONTENT_SOURCE.CONSTANT_CONTENT) {
await sendGptContent(pageMapByName.boss!, messageListForGpt) await sendMessage(pageMapByName.boss!, constantOpenContent)
gtag('rnrr_llm_content_sent') gtag('rnrr_llm_content_sent')
} catch (err) { } else {
console.log(err) try {
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) { const textToSend = await getGptContent(messageList)
await sendLookForwardReplyEmotion(pageMapByName.boss!) await sendMessage(pageMapByName.boss!, textToSend)
gtag('rnrr_llm_content_sent')
} catch (err) {
console.log(err)
await sendMessage(pageMapByName.boss!, constantOpenContent)
gtag('rnrr_look_forward_reply_emotion_sent', { gtag('rnrr_look_forward_reply_emotion_sent', {
fallback: true fallback: true
}) })
} else {
gtag('rnrr_encounter_error', {
error: err
})
throw err
} }
} }
} else { } else {
await sendLookForwardReplyEmotion(pageMapByName.boss!) if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
gtag('rnrr_look_forward_reply_emotion_sent') try {
const textToSend = await getGptContent(messageList)
await sendMessage(pageMapByName.boss!, textToSend)
gtag('rnrr_llm_content_sent')
} catch (err) {
console.log(err)
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
await sendLookForwardReplyEmotion(pageMapByName.boss!)
gtag('rnrr_look_forward_reply_emotion_sent', {
fallback: true
})
} else {
gtag('rnrr_encounter_error', {
error: err
})
throw err
}
}
} else {
await sendLookForwardReplyEmotion(pageMapByName.boss!)
gtag('rnrr_look_forward_reply_emotion_sent')
}
} }
} else { } else {
cursorToContinueFind += 1 cursorToContinueFind += 1
@@ -31,6 +31,7 @@ export function createBrowserAssistantWindow(
minWidth: 800, minWidth: 800,
height: 400, height: 400,
resizable: true, resizable: true,
frame: true,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
@@ -16,6 +16,7 @@ export function createCommonJobConditionConfigWindow(
resizable: false, resizable: false,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
frame: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, '../preload/index.js'), preload: path.join(__dirname, '../preload/index.js'),
sandbox: false sandbox: false
@@ -20,6 +20,7 @@ export function createCookieAssistantWindow(
resizable: true, resizable: true,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
frame: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, '../preload/index.js'), preload: path.join(__dirname, '../preload/index.js'),
sandbox: false sandbox: false
@@ -8,11 +8,12 @@ export function createFirstLaunchNoticeWindow(
// Create the browser window. // Create the browser window.
firstLaunchNoticeWindow = new BrowserWindow({ firstLaunchNoticeWindow = new BrowserWindow({
width: 960, width: 960,
height: 640, maxWidth: 960,
resizable: false, minHeight: 320,
resizable: true,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
frame: false, frame: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, '../preload/index.js'), preload: path.join(__dirname, '../preload/index.js'),
sandbox: false sandbox: false
@@ -15,6 +15,7 @@ export function createLlmConfigWindow(
resizable: false, resizable: false,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
frame: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, '../preload/index.js'), preload: path.join(__dirname, '../preload/index.js'),
sandbox: false sandbox: false
@@ -12,6 +12,7 @@ export function createMainWindow(): BrowserWindow {
minWidth: 1280, minWidth: 1280,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
frame: true,
...(process.platform === 'linux' ...(process.platform === 'linux'
? { ? {
/* icon */ /* icon */
@@ -17,6 +17,7 @@ export function createReadNoReplyReminderLlmMockWindow(
resizable: false, resizable: false,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
frame: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, '../preload/index.js'), preload: path.join(__dirname, '../preload/index.js'),
sandbox: false sandbox: false
@@ -15,6 +15,7 @@ export function createResumeEditorWindow(
resizable: true, resizable: true,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
frame: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, '../preload/index.js'), preload: path.join(__dirname, '../preload/index.js'),
sandbox: false sandbox: false
@@ -1,114 +1,118 @@
<template> <template>
<div ref="componentRootEl" class="first-run-readme"> <div ref="componentRootEl" class="first-run-readme">
<div class="first-run-readme__inner"> <div class="first-run-readme__inner-outer">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div> <div class="first-run-readme__inner">
<div class="readme-desc"> <div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
如下是使用必读请您逐条阅读如果已经了解且接受请在每一条前面打勾 <div class="readme-desc">
</div> 如下是使用必读请您逐条阅读如果已经了解且接受请在每一条前面打勾
<article class="readme-article"> </div>
<ElCheckboxGroup <article class="readme-article">
v-model="readmeItemCheckStatusList" <ElCheckboxGroup
@change="handleReadmeItemCheckStatusListChange" v-model="readmeItemCheckStatusList"
> @change="handleReadmeItemCheckStatusListChange"
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']"> >
本程序从某种程度上说属于辅助工具<el-link <ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
color-blue 本程序从某种程度上说属于辅助工具<el-link
@click.stop.prevent=" color-blue
() => { @click.stop.prevent="
electron.ipcRenderer.send( () => {
'open-external-link', electron.ipcRenderer.send(
'https://about.zhipin.com/agreement/' 'open-external-link',
) 'https://about.zhipin.com/agreement/'
gtagRenderer('view_boss_agreement_clicked') )
} gtagRenderer('view_boss_agreement_clicked')
" }
>BOSS直聘用户协议</el-link "
>2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款 >BOSS直聘用户协议</el-link
<i>用户的平台使用义务</i><i>违约责任</i> >2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red" <i>用户的平台使用义务</i><i>违约责任</i>
>账号被强制退出登录账号被限制使用账号被封禁</b 章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>等对您不利的风险因此使用本程序即意味着<b class="color-red">您愿意接受以上风险</b >账号被强制退出登录账号被限制使用账号被封禁</b
>且如果相关风险发生<b class="color-red">需要自行承担相关后果本程序不负责</b> >等对您不利的风险因此使用本程序即意味着<b class="color-red">愿意接受以上风险</b
</ElCheckbox> >且如果相关风险发生<b class="color-red">您需要自行承担相关后果本程序不负责</b>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']"> </ElCheckbox>
本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b <ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
class="color-red" 本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b
>不会泄露给第三方</b class="color-red"
>也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b >不会泄露给第三方</b
> >也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
</ElCheckbox> >
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']"> </ElCheckbox>
本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b <ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
class="color-red" 本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b
>注意节制</b class="color-red"
>建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red" >注意节制</b
>注册一个本程序专用的新的BOSS直聘账号</b >建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red"
>进行求职 >注册一个本程序专用的新的BOSS直聘账号</b
</ElCheckbox> >进行求职
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']"> </ElCheckbox>
本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b <ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
class="color-red" 本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
>浏览器重复闪退重新启动</b class="color-red"
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈 >浏览器重复闪退重新启动</b
</ElCheckbox> >如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']"> </ElCheckbox>
您所在公司可能会采购上网行为监控工具或网关例如奇安信深信服绿盟等厂商的产品对您的计算机终端或网络进行<b <ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
color-red 您所在公司可能会采购上网行为监控工具或网关例如奇安信深信服绿盟等厂商的产品对您的计算机终端或网络进行<b
>监控</b color-red
>从而<b color-red>审计跟踪</b>您的行为您的上级/IT/HR >监控</b
可能会获取到监控数据从而了解团队成员离职倾向如果您不希望您的上级/IT/HR >从而<b color-red>审计跟踪</b>您的行为您的上级/IT/HR
了解到您正在求职建议您<b color-red 可能会获取到监控数据从而了解团队成员离职倾向如果您不希望您的上级/IT/HR
>不要在您所在公司提供的计算机终端或网络上使用本程序</b 了解到您正在求职建议您<b color-red
> >不要在您所在公司提供的计算机终端或网络上使用本程序</b
</ElCheckbox> >
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']"> </ElCheckbox>
本程序尊重您的隐私<b color-red <ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
>不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及您的上级/IT/HR报告您的求职行为不会向猎头公司泄露您的信息</b 本程序尊重您的隐私<b color-red
>但由于本程序开源<b color-red >不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及您的上级/IT/HR报告您的求职行为不会向猎头公司泄露您的信息</b
>任何人均可更改本程序源码并重新发布这一过程中其它开发者是可以加入恶意程序的因此请从你信任的源下载本程序</b >但由于本程序开源<b color-red
> >任何人均可更改本程序源码并重新发布这一过程中其它开发者是可以加入恶意程序的因此请从你信任的源下载本程序</b
</ElCheckbox> >
</ElCheckbox>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']"> <ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red" 本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red"
>下载使用是免费的</b >下载使用是免费的</b
>任何人可以<b class="color-red">免费获得免费使用</b><b class="color-red" >任何人可以<b class="color-red">免费获得免费使用</b><b class="color-red"
>作者没有利用本程序赚到过任何收入</b >作者没有利用本程序赚到过任何收入</b
>如果您是从GitHub以外的地方付费后购买的本程序或您被提示必须付费后才能使用本程序<b >如果您是从GitHub以外的地方付费后购买的本程序或您被提示必须付费后才能使用本程序<b
class="color-red" class="color-red"
>那您大概率被骗了</b >那您大概率被骗了</b
>或者<b class="color-red">您下载到了本程序修改版</b><b class="color-red" >或者<b class="color-red">您下载到了本程序修改版</b><b class="color-red"
>本程序对此概不负责请勿找作者商讨退款售后事宜相关事宜请咨询卖方</b >本程序对此概不负责请勿找作者商讨退款售后事宜相关事宜请咨询卖方</b
> >
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']"> <ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
本程序<b class="color-red">不对您的求职过程与结果负责</b 本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b >为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b
class="color-red" class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b >自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
> >
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']"> <ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b 请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b >如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b
class="color-red" class="color-red"
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b >在BOSS直聘上屏蔽当前公司及与之关联的公司</b
> >
</ElCheckbox> </ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']"> <ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过 本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过
VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V
等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈 等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox> </ElCheckbox>
</ElCheckboxGroup> </ElCheckboxGroup>
</article> </article>
<footer flex mt20px pb20px flex-justify-end> </div>
</div>
<footer pt10px pb10px>
<div flex flex-justify-end w-880px ml-auto mr-auto>
<el-button type="text" @click="handleCancel">退出程序</el-button> <el-button type="text" @click="handleCancel">退出程序</el-button>
<el-button type="primary" @click="handleSubmit" <el-button type="primary" @click="handleSubmit"
>我已经阅读并接受上方所提及的相关风险并决定继续使用本程序</el-button >我已经阅读并接受上方所提及的相关风险并决定继续使用本程序</el-button
> >
</footer> </div>
</div> </footer>
</div> </div>
</template> </template>
@@ -163,30 +167,27 @@ const handleReadmeItemCheckStatusListChange = (value: number[]) => {
}) })
} }
const componentRootEl = ref<HTMLElement>() const componentRootEl = ref<HTMLElement>()
onMounted(() => {
const ro = new ResizeObserver(() => {
electron.ipcRenderer.send('update-window-size', {
width: componentRootEl.value!.offsetWidth,
height: componentRootEl.value!.offsetHeight
})
})
ro.observe(componentRootEl.value!)
onBeforeMount(() => {
ro.disconnect()
})
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.first-run-readme { .first-run-readme {
box-sizing: border-box; box-sizing: border-box;
width: 960px; width: 960px;
height: fit-content; height: 100vh;
overflow: auto;
user-select: none; user-select: none;
&__inner { display: flex;
flex-direction: column;
overflow: hidden;
.first-run-readme__inner-outer {
flex: 1;
overflow: auto;
}
.first-run-readme__inner {
width: 880px; width: 880px;
margin: 0 auto; margin: 0 auto;
padding-top: 30px; padding-top: 30px;
padding-bottom: 30px;
.readme-title { .readme-title {
} }
.readme-desc { .readme-desc {
@@ -222,5 +223,9 @@ onMounted(() => {
} }
} }
} }
footer {
flex: 0;
background-color: #f0f0f0;
}
} }
</style> </style>
@@ -54,7 +54,77 @@
</template> </template>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的BOSS时,将要向BOSS发出:"> <el-form-item label="开场白话术">
<el-radio-group v-model="formContent.autoReminder.openContentSource" w-full>
<div w-full>
<el-radio :label="OPEN_CONTENT_SOURCE.CONSTANT_CONTENT"> 固定文案 </el-radio>
<el-input
v-if="
formContent.autoReminder.openContentSource ===
OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
"
v-model="formContent.autoReminder.constantOpenContent"
w-full
:autosize="{ minRows: 3 }"
max-h-8lh
type="textarea"
:placeholder="defaultConstantOpenContent"
class="pl-30px mb10px"
:style="{
boxSizing: 'border-box'
}"
/>
</div>
<div>
<el-radio :label="OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型生成的内容
</el-radio>
<div
v-if="
formContent.autoReminder.openContentSource ===
OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
ml30px
>
<el-form-item class="mb4px">
<div>
<div>
<el-button
size="small"
type="primary"
@click="
handleClickEditPrompt({
type: 'open'
})
"
>
使用外部编辑器编辑“开场白话术”提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
restoreDefaultTemplate({
type: 'open',
gaEvName: 'reset_template_clicked_in_main_form'
})
}
"
>
还原默认“开场白话术”提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑“开场白话术”提示词模板。
</div>
</div>
</el-form-item>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item label="跟进话术 - 当发现已读不回的BOSS时,将要向BOSS发出:">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource"> <el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div> <div>
<el-tooltip <el-tooltip
@@ -71,91 +141,117 @@
</el-radio> </el-radio>
</el-tooltip> </el-tooltip>
<br /> <br />
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT"> <div>
由大语言模型(根据简历及当前聊天上下文)生成的内容 <el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
</el-radio> 由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
<div
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
ml30px
>
<el-form-item class="mb4px">
<div>
<div>
<el-button
size="small"
type="primary"
@click="
handleClickEditPrompt({
type: 'rechat'
})
"
>
使用外部编辑器编辑“跟进话术”提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
restoreDefaultTemplate({
type: 'rechat',
gaEvName: 'reset_template_clicked_in_main_form'
})
}
"
>
还原默认“跟进话术”提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑“跟进话术”提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
</div>
</div>
</div> </div>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<div class="ml-30px"> <div mt10px>
<el-form-item mb0 label="大语言模型公共设置及效果预览" />
<template <template
v-if=" v-if="
formContent.autoReminder.rechatContentSource === [
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT formContent.autoReminder.rechatContentSource,
formContent.autoReminder.openContentSource
].includes(RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT)
" "
> >
<el-form-item class="mb4px"> <div ml-30px>
<div> <el-form-item class="mb4px">
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div> <div>
<el-button size="small" type="primary" @click="handleClickEditPrompt"> <el-button size="small" type="primary" @click="handleClickEditResume">
使用外部编辑器编辑提示词模板 (Markdown) 编辑简历
</el-button> </el-button>
<el-button <div class="font-size-12px color-#666">
size="small" 简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
type="primary" </div>
@click=" </div>
() => { </el-form-item>
gtagRenderer('reset_template_clicked_in_main_form') <el-form-item prop="recentMessageQuantityForLlm">
restoreDefaultTemplate() <div>
} 携带最近
" <el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item label="当配置的所有大模型均不可使用时">
<div class="flex flex-items-center">
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
> >
还原默认提示词模板 <el-option
</el-button> v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div> </div>
<div class="font-size-12px color-#666"> </el-form-item>
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入 </div>
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div class="flex flex-items-center">
<span class="whitespace-nowrap">当所有模型均不可使用时&nbsp;</span>
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div>
</el-form-item>
</template> </template>
<el-form-item ml-30px>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
><span text-orange ml10px
>&lt;- 正式运行前建议在这里先试一试大模型生成效果是否符合预期哦</span
>
</el-form-item>
</div> </div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes"> <el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
<el-input-number <el-input-number
@@ -259,6 +355,8 @@ import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus' import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { import {
OPEN_CONTENT_SOURCE,
// OPEN_LLM_FALLBACK,
RECHAT_CONTENT_SOURCE, RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK, RECHAT_LLM_FALLBACK,
RUNNING_STATUS_ENUM RUNNING_STATUS_ENUM
@@ -267,6 +365,7 @@ import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import mittBus from '../../utils/mitt' import mittBus from '../../utils/mitt'
import { QuestionFilled } from '@element-plus/icons-vue' import { QuestionFilled } from '@element-plus/icons-vue'
import RunningOverlay from '@renderer/features/RunningOverlay/index.vue' import RunningOverlay from '@renderer/features/RunningOverlay/index.vue'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../../common/constant'
const gtagRenderer = (name, params?: object) => { const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, { return baseGtagRenderer(name, {
scene: 'rnrr-config', scene: 'rnrr-config',
@@ -282,7 +381,10 @@ const formContent = ref({
recentMessageQuantityForLlm: 8, recentMessageQuantityForLlm: 8,
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION, rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
onlyRemindBossWithExpectJobType: true, onlyRemindBossWithExpectJobType: true,
onlyRemindBossWithoutBlockCompanyName: true onlyRemindBossWithoutBlockCompanyName: true,
openContentSource: OPEN_CONTENT_SOURCE.CONSTANT_CONTENT,
// openLlmFallback: OPEN_LLM_FALLBACK.SEND_CONSTANT_CONTENT,
constantOpenContent: ''
} }
}) })
@@ -315,7 +417,10 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
: parseInt(conf.recentMessageQuantityForLlm) : parseInt(conf.recentMessageQuantityForLlm)
: 8 : 8
conf.onlyRemindBossWithExpectJobType = conf.onlyRemindBossWithExpectJobType ?? true conf.onlyRemindBossWithExpectJobType = conf.onlyRemindBossWithExpectJobType ?? true
conf.onlyRemindBossWithoutBlockCompanyName = conf.onlyRemindBossWithoutBlockCompanyName ?? true
conf.rechatLlmFallback = conf.rechatLlmFallback ?? RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION conf.rechatLlmFallback = conf.rechatLlmFallback ?? RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
conf.openContentSource = conf.openContentSource ?? OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
conf.constantOpenContent = conf.constantOpenContent ?? ''
formContent.value.autoReminder = conf formContent.value.autoReminder = conf
}) })
@@ -462,8 +567,9 @@ async function checkIsCanRun() {
return false return false
} }
try { try {
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid') await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid', { type: 'rechat' })
} catch (err) { } catch (err) {
console.log(err)
if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) { if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) {
gtagRenderer('cannot_launch_for_no_resume_placehold') gtagRenderer('cannot_launch_for_no_resume_placehold')
console.log(`提示词模板无效`, err) console.log(`提示词模板无效`, err)
@@ -479,8 +585,10 @@ async function checkIsCanRun() {
} }
) )
.then(async () => { .then(async () => {
gtagRenderer('confirm_invalid_rt_tip_dialog') await restoreDefaultTemplate({
await restoreDefaultTemplate() type: 'rechat',
gaEvName: 'confirm_invalid_rt_tip_dialog'
})
}) })
.catch(() => { .catch(() => {
gtagRenderer('close_invalid_rt_tip_dialog') gtagRenderer('close_invalid_rt_tip_dialog')
@@ -504,14 +612,23 @@ const handleSubmit = async () => {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes, throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
rechat_limit_day: formContent.value.autoReminder.rechatLimitDay, rechat_limit_day: formContent.value.autoReminder.rechatLimitDay,
rechat_content_source: formContent.value.autoReminder.rechatContentSource, rechat_content_source: formContent.value.autoReminder.rechatContentSource,
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm,
only_remind_boss_with_expect_job_type:
formContent.value.autoReminder.onlyRemindBossWithExpectJobType,
only_remind_boss_without_block_company_name:
formContent.value.autoReminder.onlyRemindBossWithoutBlockCompanyName,
rechat_llm_fallback: formContent.value.autoReminder.rechatLlmFallback,
open_content_source: formContent.value.autoReminder.openContentSource,
constant_open_content_text_length: formContent.value.autoReminder.constantOpenContent.length ?? 0
}) })
await formRef.value!.validate() await formRef.value!.validate()
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value)) await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
gtagRenderer('config_saved') gtagRenderer('config_saved')
if ( if (
formContent.value.autoReminder?.rechatContentSource === formContent.value.autoReminder?.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT ||
formContent.value.autoReminder?.openContentSource ===
OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) { ) {
if (!(await checkIsCanRun())) { if (!(await checkIsCanRun())) {
return return
@@ -568,8 +685,9 @@ function handleThrottleIntervalMinutesBlur() {
) )
} }
const restoreDefaultTemplate = async () => { const restoreDefaultTemplate = async ({ type, gaEvName }) => {
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default') gtagRenderer(gaEvName)
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default', { type })
ElMessage({ ElMessage({
type: 'success', type: 'success',
message: '模板还原成功' message: '模板还原成功'
@@ -603,11 +721,12 @@ const handleClickEditResume = async () => {
} }
} }
const handleClickEditPrompt = async () => { const handleClickEditPrompt = async ({ type }) => {
gtagRenderer('edit_prompt_clicked') gtagRenderer('edit_prompt_clicked', { type })
await electron.ipcRenderer.send('no-reply-reminder-prompt-edit') await electron.ipcRenderer.send('no-reply-reminder-prompt-edit', { type })
} }
// for 跟进话术
const rechatLlmFallbackOptions = [ const rechatLlmFallbackOptions = [
{ {
name: '发送“[盼回复]”表情', name: '发送“[盼回复]”表情',
@@ -619,6 +738,18 @@ const rechatLlmFallbackOptions = [
} }
] ]
// // for 开场白话术
// const openLlmFallbackOptions = [
// {
// name: '发送固定文案',
// value: OPEN_LLM_FALLBACK.SEND_CONSTANT_CONTENT
// },
// {
// name: '退出已读不回自动复聊',
// value: OPEN_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
// }
// ]
async function handleTestEffectClicked() { async function handleTestEffectClicked() {
gtagRenderer('goto_mock_chat_clicked') gtagRenderer('goto_mock_chat_clicked')
if (!(await checkIsCanRun())) { if (!(await checkIsCanRun())) {
@@ -651,6 +782,16 @@ const handleStopButtonClick = async () => {
isStopButtonLoading.value = false isStopButtonLoading.value = false
} }
} }
const defaultConstantOpenContent = computed(() => {
if (
formContent.value.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join('')
}
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
})
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -22,39 +22,65 @@
<div class="pb20px"></div> <div class="pb20px"></div>
<div v-for="(item, index) in messageList" :key="index" flex flex-col flex-items-end> <div v-for="(item, index) in messageList" :key="index" flex flex-col flex-items-end>
<div class="message-item-wrap flex flex-col"> <div class="message-item-wrap flex flex-col">
<div <template v-if="item.type === 'text'">
class="message-item" <div
:class="{ class="message-item"
'will-enter-context': getIsEnterContent(index) :class="{
}" 'will-enter-context': getIsEnterContent(index)
> }"
{{ item.text }} >
</div> {{ item.text }}
<div </div>
:style="{ </template>
width: 'fit-content', <template v-else>
alignSelf: 'flex-end' <div
}" class="message-item image-message-item"
font-size-10px :class="{
> 'will-enter-context': getIsEnterContent(index)
{{ item.usedLlmConfig.model }} }"
</div> >
<div <img :src="item.imageUrl" alt="" />
v-if="item?.usedLlmConfig?.providerCompleteApiUrl?.trim()" </div>
:style="{ </template>
width: 'fit-content', <!-- eslint-disable-next-line prettier/prettier -->
overflow: 'hidden', <template v-if="(typeof item.usedLlmConfig !== 'string')">
whiteSpace: 'nowrap', <div
textOverflow: 'ellipsis', :style="{
alignSelf: 'flex-end', width: 'fit-content',
color: '#bbb' alignSelf: 'flex-end'
}" }"
font-size-10px font-size-10px
w-fit-content >
max-w-20em {{ item.usedLlmConfig.model }}
> </div>
{{ item.usedLlmConfig.providerCompleteApiUrl }} <div
</div> v-if="item?.usedLlmConfig?.providerCompleteApiUrl?.trim()"
:style="{
width: 'fit-content',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
alignSelf: 'flex-end',
color: '#bbb'
}"
font-size-10px
w-fit-content
max-w-20em
>
{{ item.usedLlmConfig.providerCompleteApiUrl }}
</div>
</template>
<template v-else>
<div
:style="{
width: 'fit-content',
alignSelf: 'flex-end'
}"
font-size-10px
>
{{ item.usedLlmConfig }}
</div>
</template>
</div> </div>
</div> </div>
<div class="pb20px"></div> <div class="pb20px"></div>
@@ -147,15 +173,27 @@ import { computed, ref, watch } from 'vue'
import { sleep } from '@geekgeekrun/utils/sleep.mjs' import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { gtagRenderer } from '@renderer/utils/gtag' import { gtagRenderer } from '@renderer/utils/gtag'
import {
OPEN_CONTENT_SOURCE,
RECHAT_CONTENT_SOURCE
} from '../../../../common/enums/auto-start-chat'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../../common/constant'
import lookForwardReplyEmotion from '../MainLayout/resources/look-forward-reply-emotion.gif'
type MessageItem = { type MessageItem = {
text: string text: string
usedLlmConfig: string usedLlmConfig: string
type: 'text'
// recordInfo: any // recordInfo: any
} }
const messageList = ref<MessageItem[]>([]) type ImageMessageItem = MessageItem & {
type: 'image'
imageUrl: string
}
const messageList = ref<(MessageItem | ImageMessageItem)[]>([])
const searchParams = Object.fromEntries(new URL(location.href).searchParams)
const recentMessageQuantityForLlm = const recentMessageQuantityForLlm = Number(searchParams.recentMessageQuantityForLlm) || 8
Number(new URL(location.href).searchParams.get('recentMessageQuantityForLlm')) || 8
function getIsEnterContent(index) { function getIsEnterContent(index) {
return messageList.value.length - index - 1 < recentMessageQuantityForLlm return messageList.value.length - index - 1 < recentMessageQuantityForLlm
} }
@@ -188,32 +226,93 @@ watch(
const scrollElRef = ref(null) const scrollElRef = ref(null)
const isLoading = ref(false) const isLoading = ref(false)
const openContentSource = Number(searchParams.openContentSource)
const constantOpenContent = (() => {
if (searchParams.constantOpenContent?.trim()) {
return searchParams.constantOpenContent.trim()
}
if (Number(searchParams.rechatContentSource) === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join(``)
} else {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
}
})()
const rechatContentSource = Number(searchParams.rechatContentSource)
async function sendLlmGeneratedContent() { async function sendLlmGeneratedContent() {
gtagRenderer('click_mock_chat_send') gtagRenderer('click_mock_chat_send')
isLoading.value = true if (!(messageList.value ?? []).length) {
try { // send open content
const response = await electron.ipcRenderer.invoke('request-llm-for-test', { if (openContentSource === OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
messageList: JSON.parse(JSON.stringify((messageList.value ?? []).slice(-8))), isLoading.value = true
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null try {
}) const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
console.log(response) messageList: [],
messageList.value.push({ llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
text: response.responseText, })
usedLlmConfig: response.usedLlmConfig console.log(response)
}) messageList.value.push({
await sleep(50) type: 'text',
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({ text: response.responseText,
top: scrollElRef.value?.scrollHeight, usedLlmConfig: response.usedLlmConfig
behavior: 'smooth' })
}) await sleep(50)
} catch (err) { ;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
ElMessage.error({ top: scrollElRef.value?.scrollHeight,
dangerouslyUseHTMLString: true, behavior: 'smooth'
grouping: true, })
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>` } catch (err) {
}) ElMessage.error({
} finally { dangerouslyUseHTMLString: true,
isLoading.value = false grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
}
} else {
messageList.value.push({
type: 'text',
text: constantOpenContent,
usedLlmConfig: '未使用大模型'
})
}
} else {
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: JSON.parse(JSON.stringify((messageList.value ?? []).slice(-8))),
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
})
console.log(response)
messageList.value.push({
type: 'text',
text: response.responseText,
usedLlmConfig: response.usedLlmConfig
})
await sleep(50)
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
top: scrollElRef.value?.scrollHeight,
behavior: 'smooth'
})
} catch (err) {
ElMessage.error({
dangerouslyUseHTMLString: true,
grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
}
} else {
messageList.value.push({
type: 'image',
text: `[盼回复] 表情`,
imageUrl: lookForwardReplyEmotion,
usedLlmConfig: '未使用大模型'
})
}
} }
} }
@@ -264,5 +363,12 @@ gtagRenderer('enter_mock_chat_page')
} }
} }
} }
.message-item.image-message-item {
background-color: transparent;
width: 128px;
img {
width: 100%;
}
}
} }
</style> </style>