diff --git a/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs b/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs index 83c9756..dd9512a 100644 --- a/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs +++ b/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs @@ -118,7 +118,8 @@ export const ensureStorageFileExist = () => { ) } -export const readStorageFile = (fileName) => { +export const readStorageFile = (fileName, { isJson } = {}) => { + isJson = isJson ?? true const joinedPath = path.join(storageFilePath, fileName) if (!fs.existsSync( @@ -129,21 +130,36 @@ export const readStorageFile = (fileName) => { let o try { - o = JSON.parse( - fs.readFileSync(joinedPath) - ) + const content = fs.readFileSync(joinedPath) + if (isJson) { + o = JSON.parse(content) + } + else { + o = content.toString() + } } catch { fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath) ensureStorageFileExist() - o = JSON.parse(defaultStorageFileContentMap[fileName]) + if (isJson) { + o = JSON.parse(defaultStorageFileContentMap[fileName] ?? 'null') + } + else { + o = defaultStorageFileContentMap[fileName] ?? null + } } return o } -export const writeStorageFile = async (fileName, content) => { +export const writeStorageFile = async (fileName, content, { isJson } = {}) => { + isJson = isJson ?? true const filePath = path.join(storageFilePath, fileName) - const fileContent = JSON.stringify(content) + let fileContent + if (isJson) { + fileContent = JSON.stringify(content) + } else { + fileContent = content + } return fsPromise.writeFile( filePath, fileContent diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts index 2f96878..3804e49 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts @@ -1,5 +1,5 @@ import { ipcMain, shell, app } from 'electron' - +import path from 'path' import * as childProcess from 'node:child_process' import { ensureConfigFileExist, @@ -8,7 +8,8 @@ import { readConfigFile, writeConfigFile, readStorageFile, - writeStorageFile + writeStorageFile, + storageFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' import { ChildProcess } from 'child_process' import * as JSONStream from 'JSONStream' @@ -32,6 +33,11 @@ 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 { + autoReminderPromptTemplateFileName, + writeDefaultAutoRemindPrompt +} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation' export default function initIpc() { ipcMain.on('open-external-link', (_, link) => { @@ -490,7 +496,21 @@ export default function initIpc() { }) return defer.promise }) + ipcMain.on('no-reply-reminder-prompt-edit', async () => { + const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false }) + if (!template) { + await writeDefaultAutoRemindPrompt() + } + const filePath = path.join(storageFilePath, autoReminderPromptTemplateFileName) + shell.openPath(filePath) + }) ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close()) + ipcMain.handle('check-if-auto-remind-prompt-valid', async () => { + await getValidTemplate() + }) + ipcMain.handle('overwrite-auto-remind-prompt-with-default', async () => { + await writeDefaultAutoRemindPrompt() + }) ipcMain.handle('exit-app-immediately', () => { app.exit(0) diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts index 7a6379d..3febf6b 100644 --- a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts @@ -1,7 +1,11 @@ import { Page } from 'puppeteer' import { sleepWithRandomDelay, sleep } from '@geekgeekrun/utils/sleep.mjs' import { completes } from '@geekgeekrun/utils/gpt-request.mjs' -import { readConfigFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' +import { + readConfigFile, + readStorageFile, + writeStorageFile +} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' import { formatResumeJsonToMarkdown } from '../../../common/utils/format-resume-json-to-markdown' export const sendLookForwardReplyEmotion = async (page: Page) => { @@ -38,14 +42,8 @@ const pickLlmConfigFromList = (llmConfigList) => { // let _index = 0 -export const sendGptContent = async (page: Page, chatRecords) => { - const resumeObject = (await readConfigFile('resumes.json'))?.[0] - const resumeContent = formatResumeJsonToMarkdown(resumeObject) - const chatList = [ - { - role: 'system', - content: ` -**核心指令:** +const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__` +const defaultPrompt = `**核心指令:** 你是一个智能求职助手,需要根据用户简历生成30字左右的提醒消息,满足以下要求: 1. 每次生成需满足: - √ 包含1个核心技能 + 1个成果量化 @@ -56,7 +54,7 @@ export const sendGptContent = async (page: Page, chatRecords) => { - ✗ 严禁包含最近8条已经发过的内容(包括但不限于职位名称) **简历分析层:** -请从以下简历内容中提取关键要素:\n\`\`\`markdown\n${resumeContent}\n\`\`\`\n +请从以下简历内容中提取关键要素:\n\`\`\`markdown\n${RESUME_PLACEHOLDER}\n\`\`\`\n --- 要求提取: @@ -79,6 +77,34 @@ export const sendGptContent = async (page: Page, chatRecords) => { **输出格式:** 请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:\`{"response": "这里是将会发送给招聘者的内容"}\`` + +export const autoReminderPromptTemplateFileName = 'auto-reminder-resume-system-message-template.md' +export const getValidTemplate = async () => { + let template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false }) + if (!template) { + await writeDefaultAutoRemindPrompt() + template = defaultPrompt + } + if (!template.includes(RESUME_PLACEHOLDER)) { + const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`) + e.name = `RESUME_PLACEHOLDER_NOT_EXIST` + throw e + } + return template +} + +export const writeDefaultAutoRemindPrompt = async () => { + await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false }) +} + +export const sendGptContent = async (page: Page, chatRecords) => { + const template = await getValidTemplate() + const resumeObject = (await readConfigFile('resumes.json'))?.[0] + const resumeContent = formatResumeJsonToMarkdown(resumeObject) + const chatList = [ + { + role: 'system', + content: template.replace(RESUME_PLACEHOLDER, resumeContent) } ] chatList.push({ diff --git a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue index f6297dc..aec1f88 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue @@ -14,7 +14,7 @@ > - +
跟进话术 - 当发现已读不回的Boss时,将要向Boss发出:
@@ -37,7 +37,7 @@ RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT " > - +
配置大语言模型 @@ -59,11 +59,20 @@ style="background-color: #462ac4" >Qwen2.5 - 模型;支持多个服务商提供的多个模型组合使用 + 模型,通过对话补全接口实现消息生成;支持多个“服务商-模型”组合按权重搭配使用
- +
编辑简历 @@ -73,6 +82,22 @@
+ +
+
+ + 使用外部编辑器编辑 Prompt 模板 + + + 还原默认 Prompt 模板 + +
+
+ 对生成效果不够满意?可在此查看、编辑 Prompt 模板。请在模板中需要插入简历的位置插入 + __REPLACE_REAL_RESUME_HERE__ +
+
+
携带最近 @@ -126,7 +151,7 @@