make prompt can be read from local file

This commit is contained in:
geekgeekrun
2025-04-12 19:37:44 +08:00
parent f91daf7cab
commit 01ab679376
4 changed files with 158 additions and 24 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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({

View File

@@ -14,7 +14,7 @@
>
</div>
</el-form-item>
<el-form-item class="mb-0">
<el-form-item class="mb0">
<div>
<div>跟进话术 - 当发现已读不回的Boss时将要向Boss发出</div>
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
@@ -37,7 +37,7 @@
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<el-form-item>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickConfigLlm">
配置大语言模型
@@ -59,11 +59,20 @@
style="background-color: #462ac4"
>Qwen2.5</span
>
模型;支持多个服务商提供的多个模型组合使用
模型,通过<a
class="font-size-12px pt0 pb0"
:style="{
color: 'var(--el-color-primary)',
}"
type="text"
href="javascript:void(0)"
@click.prevent="openIntroOfCompletion"
>对话补全</a
>接口实现消息生成;支持多个“服务商-模型”组合按权重搭配使用
</div>
</div>
</el-form-item>
<el-form-item>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
@@ -73,6 +82,22 @@
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑 Prompt 模板
</el-button>
<el-button size="small" type="primary" @click="restoreDefaultTemplate">
还原默认 Prompt 模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑 Prompt 模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
@@ -126,7 +151,7 @@
<script setup lang="ts">
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { dayjs, ElForm } from 'element-plus'
import { dayjs, ElForm, ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { RECHAT_CONTENT_SOURCE } from '../../../../common/enums/auto-start-chat'
const router = useRouter()
@@ -205,6 +230,34 @@ watch(
const handleSubmit = async () => {
await formRef.value!.validate()
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
try {
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid')
} catch (err) {
if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) {
console.log(`Prompt 模板无效`, err)
ElMessageBox.confirm(
'Prompt 模板缺少简历内容占位符:<br /><b>__REPLACE_REAL_RESUME_HERE__</b><br /><br />您是否希望还原默认的 Prompt 模板?',
'',
{
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
closeOnClickModal: false,
dangerouslyUseHTMLString: true
}
)
.then(async () => {
await restoreDefaultTemplate()
})
.catch(() => {})
} else {
ElMessage({
type: 'error',
message: '用于生成自动提醒消息的 Prompt 检查未通过,请重试'
})
}
return
}
router.replace({
path: '/geekAutoStartChatWithBoss/prepareRun',
query: { flow: 'read-no-reply-reminder' }
@@ -219,6 +272,14 @@ function handleThrottleIntervalMinutesBlur() {
)
}
const restoreDefaultTemplate = async () => {
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default')
ElMessage({
type: 'success',
message: '模板还原成功'
})
}
const handleClickLaunchLogin = () => {
router.replace('/cookieAssistant')
}
@@ -255,6 +316,17 @@ const handleClickEditResume = async () => {
console.log(err)
}
}
const handleClickEditPrompt = async () => {
await electron.ipcRenderer.send('no-reply-reminder-prompt-edit')
}
const openIntroOfCompletion = () => {
electron.ipcRenderer.send(
'open-external-link',
'https://api-docs.deepseek.com/zh-cn/api/create-chat-completion'
)
}
</script>
<style scoped lang="scss">