From 639ea6ac76afb5047db769b71cfd99f73bb94888 Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Wed, 1 Jan 2025 13:31:53 +0800 Subject: [PATCH 01/63] add the config of autoReminder.rechatContentSource in UI --- .../default-config-file/boss.json | 4 +- .../ui/src/common/enums/auto-start-chat.ts | 5 ++ .../Configuration/ReadNoReplyReminder.vue | 46 +++++++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json index 435e5ca..174a5e9 100644 --- a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json +++ b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json @@ -9,6 +9,8 @@ "expectJobRegExpStr": "", "autoReminder": { "throttleIntervalMinutes": 10, - "rechatLimitDay": 21 + "rechatLimitDay": 21, + "geminiApiKey": "", + "rechatContentSource": 1 } } \ No newline at end of file diff --git a/packages/ui/src/common/enums/auto-start-chat.ts b/packages/ui/src/common/enums/auto-start-chat.ts index a0e001a..ede72cc 100644 --- a/packages/ui/src/common/enums/auto-start-chat.ts +++ b/packages/ui/src/common/enums/auto-start-chat.ts @@ -8,3 +8,8 @@ export enum AUTO_CHAT_ERROR_EXIT_CODE { AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86, AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87, } + +export enum RECHAT_CONTENT_SOURCE { + LOOK_FORWARD_EMOTION = 1, + GEMINI_WITH_CHAT_CONTEXT = 2 +} diff --git a/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue index efa25f0..043f52e 100644 --- a/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue @@ -11,8 +11,34 @@ >编辑Cookie - - 当发现已读不回的Boss时,将向Boss发出“[盼回复]”表情 + + +
+ + “[盼回复]” 表情 + +
+ + 由 AI(根据你的简历及和当前Boss聊天的上下文)生成的内容 + +
+
+
+ + + 没有密钥?点击此处申请一个吧 + + { const conf = res.config['boss.json']?.autoReminder || {} conf.throttleIntervalMinutes = conf.throttleIntervalMinutes ?? 10 conf.rechatLimitDay = conf.rechatLimitDay ?? 21 + conf.geminiApiKey = conf.geminiApiKey ?? '' + conf.rechatContentSource = conf.rechatContentSource ?? 1 formContent.value.autoReminder = conf }) @@ -100,6 +130,10 @@ const formRules = { cb() } } + }, + geminiApiKey: { + required: true, + message: '请输入 Gemini API Key' } } @@ -153,6 +187,10 @@ const rechatLimitDateString = computed(() => { +currentStamp.value - formContent.value.autoReminder.rechatLimitDay * 24 * 60 * 60 * 1000 ).format('YYYY-MM-DD HH:mm:ss') }) + +const goToGeminiNanoApiKeyPage = () => { + electron.ipcRenderer.send('open-external-link', 'https://aistudio.google.com/app/apikey') +} + + diff --git a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue index 16a81eb..b3a0e26 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue @@ -23,7 +23,7 @@ v-model="formContent.expectCompanies" :autosize="{ minRows: 4 }" type="textarea" - @blur="handleExpectCompaniesInputBlur" + @blur="normalizeExpectCompanies" /> { }) } const handleSave = async () => { + normalizeExpectCompanies() await formRef.value!.validate() await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value)) ElMessage.success('Configuration saved.') } -const handleExpectCompaniesInputBlur = (event) => { - event.target.value = (event.target?.value ?? '') +const normalizeExpectCompanies = () => { + formContent.value.expectCompanies = formContent.value.expectCompanies .split(/,|,/) .map((it) => it.trim()) .filter(Boolean) diff --git a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue index b066c06..d8128d7 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue @@ -9,7 +9,7 @@
BOSS直聘 Cookie
- 编辑Cookie
@@ -24,14 +24,37 @@
- 由 AI(根据你的简历及和当前Boss聊天的上下文)生成的内容 + 由大语言模型(根据简历及当前聊天上下文)生成的内容
- + +
+ + 配置大语言模型 + +
+ 支持 + DeepSeek-V3 + 模型 +
+
+
+ +
 天
- 不再跟进 ({{ rechatLimitDateString }})之前列表中没有进展的聊天 + 不再跟进 ({{ rechatLimitDateString }})之前列表中没有进展的聊天
这将会跟进列表中所有聊天(不建议
@@ -209,8 +233,12 @@ const rechatLimitDateString = computed(() => { ).format('YYYY-MM-DD HH:mm:ss') }) -const goToGeminiNanoApiKeyPage = () => { - electron.ipcRenderer.send('open-external-link', 'https://aistudio.google.com/app/apikey') +const handleClickConfigLlm = async () => { + try { + await electron.ipcRenderer.invoke('llm-config') + } catch (err) { + console.log(err) + } } diff --git a/packages/ui/src/renderer/src/router/index.ts b/packages/ui/src/renderer/src/router/index.ts index a36e7ef..a3a1726 100644 --- a/packages/ui/src/renderer/src/router/index.ts +++ b/packages/ui/src/renderer/src/router/index.ts @@ -16,6 +16,13 @@ const routes: Array = [ title: 'Cookie 助手' } }, + { + path: '/llmConfig', + component: () => import('@renderer/page/LlmConfig/index.vue'), + meta: { + title: '大语言模型设置' + } + }, { path: '/main-layout', component: () => import('@renderer/page/MainLayout/index.vue'), From 85e27dc6ef1526bcf2a1e439612d732fa9076f5a Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Sat, 12 Apr 2025 14:35:58 +0800 Subject: [PATCH 05/63] add basic code to call deepseek --- .../boss-operation.ts | 64 +++++++++++++++- .../flow/READ_NO_REPLY_AUTO_REMINDER/index.ts | 16 +++- packages/utils/gpt-request.mjs | 21 ++++++ packages/utils/package.json | 7 +- pnpm-lock.yaml | 73 ++++++++++++++++--- 5 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 packages/utils/gpt-request.mjs 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 475115e..2ccc043 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,5 +1,6 @@ import { Page } from 'puppeteer' -import { sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs' +import { sleepWithRandomDelay, sleep } from '@geekgeekrun/utils/sleep.mjs'; +import { completes } from '@geekgeekrun/utils/gpt-request.mjs' export const sendLookForwardReplyEmotion = async (page: Page) => { const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion') @@ -15,3 +16,64 @@ export const sendLookForwardReplyEmotion = async (page: Page) => { ) await lookForwardReplyEmojiProxy!.click() } + +export const sendGptContent = async (page: Page, chatRecords) => { + const chatList = [ + { + role: 'system', + content: + '你是一个求职消息发送机器人,正在帮助一位求职者在Boss直聘上寻找一份工作。求职者需要向招聘者聊天,得到招聘者的回复后,方能获得一次投递简历的机会。你需要“从求职者简历中提取到的信息,来生成一个要发送给招聘者的消息”。同时你需要注意,每次发送的内容一定要接续之前发送的内容,最好不要和已发过的内容重复。求职者简历信息如下:' + + `` + } + ] + chatList.push({ + role: 'user', + content: + '请帮我写一句开场白。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`' + }) + for (const record of chatRecords) { + const assistantJsonContent = JSON.stringify({ + response: record.text + }) + chatList.push({ + role: 'assistant', + content: `\`\`\`json\n${assistantJsonContent}\n\`\`\`` + }) + chatList.push({ + role: 'user', + content: + '请根据接续之前你所回复的内容,根据我的简历,写一句自我介绍,注意尽量不要和之前的聊天内容重复。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`' + }) + } + + console.log(chatList) + debugger + const res = await completes(chatList) + console.log(res) + let textToSend + try { + const rawMarkdownText = res?.message?.content + textToSend = JSON.parse( + rawMarkdownText.replace(/^```json/m, '').replace(/```$/m, '') + )?.response + if (!textToSend) { + throw new Error(`empty content. ${err?.message} ${res?.message?.content}`) + } + } catch (err) { + throw new Error(`fail to parse response. ${err?.message} ${res?.message?.content}`) + } + const chatInputSelector = `.chat-conversation .message-controls .chat-input` + const chatInputHandle = await page.$(chatInputSelector) + await chatInputHandle.click() + await sleep(500) + await chatInputHandle.click() + await chatInputHandle.type( + textToSend, + { + delay: 50 + } + ) + await sleep(1000) + const sendButtonSelector = `.chat-conversation .message-controls .chat-op .btn-send:not(.disabled)` + await page.click(sendButtonSelector) +} diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts index 063c76a..8655c47 100644 --- a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts @@ -1,7 +1,7 @@ import { bootstrap, launchBoss } from './bootstrap' import { MsgStatus, type ChatListItem } from './types' import { Browser, Page } from 'puppeteer' -import { sendLookForwardReplyEmotion } from './boss-operation' +import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation' import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs' import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited' import { app } from 'electron' @@ -277,7 +277,19 @@ const mainLoop = async () => { (throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000 ) { await sleepWithRandomDelay(3250) - await sendLookForwardReplyEmotion(pageMapByName.boss!) + // execute reply + // eslint-disable-next-line no-constant-condition + if (1 + 1 === 2) { + try { + const messageListForGpt = historyMessageList.filter(it => it.bizType !== 101 && it.isSelf) + debugger + await sendGptContent(pageMapByName.boss!, messageListForGpt) + } catch (err) { + await sendLookForwardReplyEmotion(pageMapByName.boss!) + } + } else { + await sendLookForwardReplyEmotion(pageMapByName.boss!) + } } else { cursorToContinueFind += 1 } diff --git a/packages/utils/gpt-request.mjs b/packages/utils/gpt-request.mjs new file mode 100644 index 0000000..f2243d3 --- /dev/null +++ b/packages/utils/gpt-request.mjs @@ -0,0 +1,21 @@ +import OpenAI from "openai"; + + +const GPT_API_KEY = `sk-40fdef46fee24402bc05311304fce7a1` +export async function completes(messages) { + const openai = new OpenAI({ + baseURL: 'https://api.deepseek.com', + apiKey: GPT_API_KEY + }); + + const completion = await openai.chat.completions.create({ + messages, + model: "deepseek-chat", + frequency_penalty: 0, + max_tokens: 100, + temperature: 0.2 + }); + + console.log(completion.choices[0].message.content); + return completion.choices?.[0] ?? null; +} \ No newline at end of file diff --git a/packages/utils/package.json b/packages/utils/package.json index 8c1e536..b6a5017 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -3,9 +3,10 @@ "private": true, "version": "1.0.0", "description": "utils", - "scripts": { - }, + "scripts": {}, "author": "geekgeekrun", "license": "ISC", - "dependencies": {} + "dependencies": { + "openai": "^4.91.1" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9243387..419555d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,11 @@ importers: specifier: ^1.8.27 version: 1.8.27(typescript@5.3.3) - packages/utils: {} + packages/utils: + dependencies: + openai: + specifier: ^4.91.1 + version: 4.91.1 packages: @@ -1679,6 +1683,13 @@ packages: /@types/ms@0.7.34: resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + /@types/node-fetch@2.6.12: + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + dependencies: + '@types/node': 18.19.15 + form-data: 4.0.0 + dev: false + /@types/node@18.19.15: resolution: {integrity: sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==} dependencies: @@ -2302,6 +2313,13 @@ packages: requiresBuild: true dev: false + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2349,7 +2367,6 @@ packages: dependencies: humanize-ms: 1.2.1 dev: false - optional: true /aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} @@ -2546,7 +2563,6 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true /at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} @@ -2904,7 +2920,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: true /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3108,7 +3123,6 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: true /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -3607,6 +3621,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3776,6 +3795,10 @@ packages: signal-exit: 4.1.0 dev: true + /form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + dev: false + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -3783,7 +3806,14 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true + + /formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: false /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} @@ -4162,7 +4192,6 @@ packages: dependencies: ms: 2.1.2 dev: false - optional: true /iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} @@ -4643,14 +4672,12 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: true /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 - dev: true /mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} @@ -5002,6 +5029,29 @@ packages: mimic-fn: 2.1.0 dev: true + /openai@4.91.1: + resolution: {integrity: sha512-DbjrR0hIMQFbxz8+3qBsfPJnh3+I/skPgoSlT7f9eiZuhGBUissPQULNgx6gHNkLoZ3uS0uYS6eXPUdtg4nHzw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + dependencies: + '@types/node': 18.19.15 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: false + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -6499,6 +6549,11 @@ packages: engines: {node: '>= 8'} dev: false + /web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false From 27bc63f63fdf52441a0532ed1a504895b670555e Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Fri, 11 Apr 2025 23:42:32 +0800 Subject: [PATCH 06/63] feat: adjust prompt; make gpt request can read config from file;add recentMessageQuantityForLlm config; --- .../default-config-file/boss.json | 3 +- .../boss-operation.ts | 76 ++++++++++++++----- .../flow/READ_NO_REPLY_AUTO_REMINDER/index.ts | 7 +- .../src/renderer/src/page/LlmConfig/index.vue | 19 ++--- .../page/MainLayout/ReadNoReplyReminder.vue | 61 ++++++--------- packages/utils/gpt-request.mjs | 19 +++-- 6 files changed, 111 insertions(+), 74 deletions(-) diff --git a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json index 174a5e9..8324fec 100644 --- a/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json +++ b/packages/geek-auto-start-chat-with-boss/default-config-file/boss.json @@ -11,6 +11,7 @@ "throttleIntervalMinutes": 10, "rechatLimitDay": 21, "geminiApiKey": "", - "rechatContentSource": 1 + "rechatContentSource": 1, + "recentMessageQuantityForLlm": 8 } } \ No newline at end of file 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 2ccc043..7f4007b 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,6 +1,7 @@ import { Page } from 'puppeteer' -import { sleepWithRandomDelay, sleep } from '@geekgeekrun/utils/sleep.mjs'; +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' export const sendLookForwardReplyEmotion = async (page: Page) => { const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion') @@ -17,20 +18,57 @@ export const sendLookForwardReplyEmotion = async (page: Page) => { await lookForwardReplyEmojiProxy!.click() } +const resumeContent = `` +// let _index = 0 + export const sendGptContent = async (page: Page, chatRecords) => { const chatList = [ { role: 'system', - content: - '你是一个求职消息发送机器人,正在帮助一位求职者在Boss直聘上寻找一份工作。求职者需要向招聘者聊天,得到招聘者的回复后,方能获得一次投递简历的机会。你需要“从求职者简历中提取到的信息,来生成一个要发送给招聘者的消息”。同时你需要注意,每次发送的内容一定要接续之前发送的内容,最好不要和已发过的内容重复。求职者简历信息如下:' + - `` + content: ` +**核心指令:** +你是一个智能求职助手,需要根据用户简历生成30字左右的提醒消息,满足以下要求: +1. 每次生成需满足: + - √ 包含1个核心技能 + 1个成果量化 + - √ 使用不同句式模板(至少准备5种) + - √ 谦虚一些,头衔、工作年限等在历史记录信息中出现一次就好 + - ✗ 禁止与最近发送的几条相似或雷同 + - ✗ 禁止使用专业术语堆砌 + - ✗ 严禁出现简历之外的词语 + +**简历分析层:** +请从以下简历内容中提取关键要素:\n${resumeContent}\n + +--- +要求提取: +1. 硬技能:编程语言/技术栈/工具证书等(至少提取5项) +2. 项目经历与成果:业绩、带量化数据的结果(至少3条) +3. 软技能:沟通/管理等(至少2项) +4. 特殊成就:奖项/专利等(可选) + +**消息生成层:** +根据上述要素随机组合生成消息 + +**质量控制层:** +每次生成前执行: +1. 检查历史记录 +2. 确保技能/成果组合未重复 +3. 所生成的新消息,严禁包含最近8条已经发过的内容(包括但不限于职位名称) +4. 字数严格控制在10-40字 +5. 避免感叹号等激进符号 +6. 减少头衔“资深”、“高级”出现的频率,严禁出现“专家”、“老兵”;减少工作年限“x年”出现的频率 + +**输出格式:** +请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:\`{"response": "这里是将会发送给招聘者的内容"}\`` } ] chatList.push({ role: 'user', content: - '请帮我写一句开场白。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`' + '请根据我的简历,帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容,但需要表达出应聘意向。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`' }) + // chatRecords = chatRecords.slice(chatRecords.length - _index) + // debugger for (const record of chatRecords) { const assistantJsonContent = JSON.stringify({ response: record.text @@ -42,20 +80,25 @@ export const sendGptContent = async (page: Page, chatRecords) => { chatList.push({ role: 'user', content: - '请根据接续之前你所回复的内容,根据我的简历,写一句自我介绍,注意尽量不要和之前的聊天内容重复。请确保仅响应一句话,以JSON响应;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`' + '围绕我简历中关于自我介绍、技术栈、工作经历、项目描述、项目业绩等内容,写一句自我介绍。开头不必包含“您好”、结尾不必包含“期待回复”;务必确保本次所回复的内容不能与之前所回复的内容雷同或相似。请确保仅回复一句话,以JSON响应,不要包含其他解释或内容;数据结构参考:`{"response": "这里是将会发送给招聘者的内容"}`' }) } - console.log(chatList) - debugger - const res = await completes(chatList) + const llmConfig = await readConfigFile('llm.json') + const res = await completes( + { + baseURL: llmConfig.providerCompleteApiUrl, + apiKey: llmConfig.providerApiSecret, + model: llmConfig.model + }, + chatList + ) console.log(res) + // _index++ let textToSend try { const rawMarkdownText = res?.message?.content - textToSend = JSON.parse( - rawMarkdownText.replace(/^```json/m, '').replace(/```$/m, '') - )?.response + textToSend = JSON.parse(rawMarkdownText.replace(/^```json/m, '').replace(/```$/m, ''))?.response if (!textToSend) { throw new Error(`empty content. ${err?.message} ${res?.message?.content}`) } @@ -67,12 +110,9 @@ export const sendGptContent = async (page: Page, chatRecords) => { await chatInputHandle.click() await sleep(500) await chatInputHandle.click() - await chatInputHandle.type( - textToSend, - { - delay: 50 - } - ) + await chatInputHandle.type(textToSend, { + delay: 50 + }) await sleep(1000) const sendButtonSelector = `.chat-conversation .message-controls .chat-op .btn-send:not(.disabled)` await page.click(sendButtonSelector) diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts index 8655c47..545598c 100644 --- a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts @@ -21,6 +21,9 @@ import { messageForSaveFilter } from '../../../common/utils/chat-list' const throttleIntervalMinutes = readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10 const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21 +const recentMessageQuantityForLlm = + readConfigFile('boss.json').autoReminder?.recentMessageQuantityForLlm ?? 8 + const dbInitPromise = initDb(getPublicDbFilePath()) export const pageMapByName: { @@ -281,10 +284,10 @@ const mainLoop = async () => { // eslint-disable-next-line no-constant-condition if (1 + 1 === 2) { try { - const messageListForGpt = historyMessageList.filter(it => it.bizType !== 101 && it.isSelf) - debugger + const messageListForGpt = historyMessageList.filter(it => it.bizType !== 101 && it.isSelf).slice(-recentMessageQuantityForLlm) await sendGptContent(pageMapByName.boss!, messageListForGpt) } catch (err) { + console.log(err) await sendLookForwardReplyEmotion(pageMapByName.boss!) } } else { diff --git a/packages/ui/src/renderer/src/page/LlmConfig/index.vue b/packages/ui/src/renderer/src/page/LlmConfig/index.vue index 0f1cfa4..44ad1ba 100644 --- a/packages/ui/src/renderer/src/page/LlmConfig/index.vue +++ b/packages/ui/src/renderer/src/page/LlmConfig/index.vue @@ -115,20 +115,21 @@ const llmPresetList: { providerCompleteApiUrl: 'https://api.deepseek.com' } }, - { - name: '通过 Ollama 部署的 DeepSeek-R1(14B)模型', - config: { - model: 'deepseek-r1:14b', - providerApiSecret: 'ollama', - providerCompleteApiUrl: 'http://127.0.0.1:11434' - } - }, + // TODO: + // { + // name: '通过 Ollama 部署的 DeepSeek-R1(14B)模型', + // config: { + // model: 'deepseek-r1:14b', + // providerApiSecret: 'ollama', + // providerCompleteApiUrl: 'http://127.0.0.1:11434/v1' + // } + // }, { name: '通过 Ollama 部署的 Qwen2.5(7B)模型', config: { model: 'qwen2.5:7b', providerApiSecret: 'ollama', - providerCompleteApiUrl: 'http://127.0.0.1:11434' + providerCompleteApiUrl: 'http://127.0.0.1:11434/v1' } } ] diff --git a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue index d8128d7..7591df4 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue @@ -37,7 +37,7 @@ RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT " > - +
配置大语言模型 @@ -53,34 +53,21 @@
+ +
+ 携带最近 + + 次聊天内容作为上下文生成新消息 +
+
- { const conf = res.config['boss.json']?.autoReminder || {} conf.throttleIntervalMinutes = conf.throttleIntervalMinutes ?? 10 conf.rechatLimitDay = conf.rechatLimitDay ?? 21 - conf.geminiApiKey = conf.geminiApiKey ?? '' conf.rechatContentSource = conf.rechatContentSource ?? 1 - conf.resumeAbstract = conf.resumeAbstract ?? '' - + conf.recentMessageQuantityForLlm = + typeof conf.recentMessageQuantityForLlm === 'number' + ? conf.recentMessageQuantityForLlm > 20 + ? 20 + : conf.recentMessageQuantityForLlm < 8 + ? 8 + : parseInt(conf.recentMessageQuantityForLlm) + : 8 formContent.value.autoReminder = conf }) @@ -175,10 +166,6 @@ const formRules = { cb() } } - }, - geminiApiKey: { - required: true, - message: '请输入 Gemini API Key' } } diff --git a/packages/utils/gpt-request.mjs b/packages/utils/gpt-request.mjs index f2243d3..ff5a48a 100644 --- a/packages/utils/gpt-request.mjs +++ b/packages/utils/gpt-request.mjs @@ -1,19 +1,24 @@ import OpenAI from "openai"; - -const GPT_API_KEY = `sk-40fdef46fee24402bc05311304fce7a1` -export async function completes(messages) { +export async function completes( + { + baseURL, + apiKey, + model + }, + messages +) { const openai = new OpenAI({ - baseURL: 'https://api.deepseek.com', - apiKey: GPT_API_KEY + baseURL, + apiKey, }); const completion = await openai.chat.completions.create({ messages, - model: "deepseek-chat", + model, frequency_penalty: 0, max_tokens: 100, - temperature: 0.2 + temperature: 0.1 }); console.log(completion.choices[0].message.content); From c40fbde5b2e3afd715d53019237c895cd9045e89 Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Thu, 10 Apr 2025 02:39:34 +0800 Subject: [PATCH 07/63] add ui for resume editor --- .../runtime-file-utils.mjs | 8 +- .../flow/OPEN_SETTING_WINDOW/ipc/index.ts | 34 ++ .../ui/src/main/window/resumeEditorWindow.ts | 45 ++ .../page/MainLayout/ReadNoReplyReminder.vue | 15 + .../renderer/src/page/ResumeEditor/index.vue | 460 ++++++++++++++++++ packages/ui/src/renderer/src/router/index.ts | 7 + 6 files changed, 567 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/main/window/resumeEditorWindow.ts create mode 100644 packages/ui/src/renderer/src/page/ResumeEditor/index.vue 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 65d8780..83c9756 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 @@ -71,8 +71,12 @@ export const readConfigFile = (fileName) => { ) } catch { fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath) - ensureConfigFileExist() - o = JSON.parse(defaultConfigFileContentMap[fileName]) + if (defaultConfigFileContentMap[fileName]) { + ensureConfigFileExist() + o = JSON.parse(defaultConfigFileContentMap[fileName]) + } else { + o = null + } } return o 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 72c03c4..1b59306 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 @@ -31,6 +31,7 @@ import { WriteStream } from 'node:fs' // eslint-disable-next-line vue/prefer-import-from-vue import { hasOwn } from '@vue/shared' import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow' +import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow' export default function initIpc() { ipcMain.on('open-external-link', (_, link) => { @@ -462,6 +463,39 @@ export default function initIpc() { }) ipcMain.on('close-llm-config', () => llmConfigWindow?.close()) + ipcMain.handle('resume-edit', async () => { + createResumeEditorWindow({ + parent: mainWindow!, + modal: true, + show: true + }) + const defer = Promise.withResolvers() + async function saveResumeHandler(_, resumeContent) { + await writeConfigFile('resumes.json', [ + { + name: '默认简历', + updateTime: Number(new Date()), + content: resumeContent + } + ]) + defer.resolve() + resumeEditorWindow?.close() + } + ipcMain.handle('save-resume-content', saveResumeHandler) + resumeEditorWindow?.once('closed', () => { + ipcMain.removeHandler('save-resume-content') + ipcMain.removeHandler('fetch-resume-content') + defer.reject(new Error('cancel')) + }) + + ipcMain.handle('fetch-resume-content', async () => { + const res = (await readConfigFile('resumes.json'))?.[0] + return res?.content ?? null + }) + return defer.promise + }) + ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close()) + ipcMain.handle('exit-app-immediately', () => { app.exit(0) }) diff --git a/packages/ui/src/main/window/resumeEditorWindow.ts b/packages/ui/src/main/window/resumeEditorWindow.ts new file mode 100644 index 0000000..3ccb453 --- /dev/null +++ b/packages/ui/src/main/window/resumeEditorWindow.ts @@ -0,0 +1,45 @@ +import { BrowserWindow, ipcMain } from 'electron' +import path from 'path' + +export let resumeEditorWindow: BrowserWindow | null = null +export function createResumeEditorWindow( + opt?: Electron.BrowserWindowConstructorOptions +): BrowserWindow { + // Create the browser window. + if (resumeEditorWindow) { + resumeEditorWindow!.show() + } + resumeEditorWindow = new BrowserWindow({ + width: 960, + height: 720, + resizable: true, + show: false, + autoHideMenuBar: true, + // frame: false, + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + sandbox: false + }, + ...opt + }) + + resumeEditorWindow.on('ready-to-show', () => { + resumeEditorWindow!.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']) { + resumeEditorWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/resumeEditor') + } else { + resumeEditorWindow.loadURL( + 'file://' + path.join(__dirname, '../renderer/index.html') + '#/resumeEditor' + ) + } + + resumeEditorWindow!.once('closed', () => { + resumeEditorWindow = null + }) + + return resumeEditorWindow! +} diff --git a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue index 7591df4..ba25c6f 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue @@ -53,6 +53,13 @@ + +
+ + 编辑简历 + +
+
携带最近 @@ -227,6 +234,14 @@ const handleClickConfigLlm = async () => { console.log(err) } } + +const handleClickEditResume = async () => { + try { + await electron.ipcRenderer.invoke('resume-edit') + } catch (err) { + console.log(err) + } +} + + diff --git a/packages/ui/src/renderer/src/router/index.ts b/packages/ui/src/renderer/src/router/index.ts index a3a1726..8b4dc97 100644 --- a/packages/ui/src/renderer/src/router/index.ts +++ b/packages/ui/src/renderer/src/router/index.ts @@ -23,6 +23,13 @@ const routes: Array = [ title: '大语言模型设置' } }, + { + path: '/resumeEditor', + component: () => import('@renderer/page/ResumeEditor/index.vue'), + meta: { + title: '简历编辑' + } + }, { path: '/main-layout', component: () => import('@renderer/page/MainLayout/index.vue'), From 0714b187bdae98e150bf9d6ef77e14efc153355c Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Thu, 10 Apr 2025 03:00:40 +0800 Subject: [PATCH 08/63] add work/project description --- .../renderer/src/page/ResumeEditor/index.vue | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/renderer/src/page/ResumeEditor/index.vue b/packages/ui/src/renderer/src/page/ResumeEditor/index.vue index 63c57fc..c910217 100644 --- a/packages/ui/src/renderer/src/page/ResumeEditor/index.vue +++ b/packages/ui/src/renderer/src/page/ResumeEditor/index.vue @@ -143,6 +143,17 @@
+ + + + + + geekProjExpList: Array<{ name: string @@ -372,7 +395,8 @@ function getNewWorkExpItem() { endYearMon: '', positionName: '', startYearMon: '', - performance: '' + performance: '', + workDescription: '' } } function addWorkExp() { From 901c3a51a078181dfe5e326fc6bd65d951007f29 Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Fri, 11 Apr 2025 23:36:55 +0800 Subject: [PATCH 09/63] feat: enhance resume editor and `formatResumeJsonToMarkdown`; add the logic to send resume read from local to llm --- .../utils/format-resume-json-to-markdown.ts | 55 +++++++++++++ .../boss-operation.ts | 12 +-- .../renderer/src/page/ResumeEditor/index.vue | 81 ++++++++++--------- 3 files changed, 107 insertions(+), 41 deletions(-) create mode 100644 packages/ui/src/common/utils/format-resume-json-to-markdown.ts diff --git a/packages/ui/src/common/utils/format-resume-json-to-markdown.ts b/packages/ui/src/common/utils/format-resume-json-to-markdown.ts new file mode 100644 index 0000000..b972112 --- /dev/null +++ b/packages/ui/src/common/utils/format-resume-json-to-markdown.ts @@ -0,0 +1,55 @@ +export function formatResumeJsonToMarkdown(resume) { + const basicInfoText = [ + ['# 姓名', resume.content.name], + ['# 工作年限', resume.content.workYearDesc], + ['# 期望职位', resume.content.expectJob], + ['# 个人优势', resume.content.userDescription] + ] + .filter((it) => { + return Boolean(it[1]?.trim()) + }) + .map((it) => it.join('\n')) + .join('\n\n') + + let formattedWorkExpText = resume.content.geekWorkExpList + .filter((it) => Boolean(it.company?.trim())) + .map((it) => { + const info = [ + [`职务`, it.positionName], + [`任职时间`], + [`工作描述`, it.workDescription], + [`工作业绩`, it.performance] + ].filter((it) => { + return Boolean(it[1]?.trim()) + }) + return [[`## ${it.company}`], ...info].map((it) => it.join('\n')).join('\n\n') + }) + .join('\n') + if (formattedWorkExpText?.trim()) { + formattedWorkExpText = '# 工作经历\n' + formattedWorkExpText + } + + let formattedProjWorkExpText = resume.content.geekProjExpList + .filter((it) => Boolean(it.name?.trim())) + .map((it) => { + const info = [ + [`## ${it.name}`], + [`项目角色`, it.roleName], + [`项目时间`], + [`工作描述`, it.projectDescription], + [`工作业绩`, it.performance] + ].filter((it) => { + return Boolean(it[1]?.trim()) + }) + + return [[`## ${it.name}`], ...info].map((it) => it.join('\n')).join('\n\n') + }) + .join('\n') + if (formattedProjWorkExpText?.trim()) { + formattedProjWorkExpText = '# 项目经历\n' + formattedProjWorkExpText + } + + const result = `${basicInfoText}\n\n${formattedWorkExpText}\n\n${formattedProjWorkExpText}` + + return result +} 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 7f4007b..ffcf13c 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 @@ -2,6 +2,7 @@ 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 { formatResumeJsonToMarkdown } from '../../../common/utils/format-resume-json-to-markdown' export const sendLookForwardReplyEmotion = async (page: Page) => { const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion') @@ -18,10 +19,11 @@ export const sendLookForwardReplyEmotion = async (page: Page) => { await lookForwardReplyEmojiProxy!.click() } -const resumeContent = `` // 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', @@ -32,12 +34,12 @@ export const sendGptContent = async (page: Page, chatRecords) => { - √ 包含1个核心技能 + 1个成果量化 - √ 使用不同句式模板(至少准备5种) - √ 谦虚一些,头衔、工作年限等在历史记录信息中出现一次就好 - - ✗ 禁止与最近发送的几条相似或雷同 - - ✗ 禁止使用专业术语堆砌 + - ✗ 严禁与最近发送的几条相似或雷同 - ✗ 严禁出现简历之外的词语 + - ✗ 严禁包含最近8条已经发过的内容(包括但不限于职位名称) **简历分析层:** -请从以下简历内容中提取关键要素:\n${resumeContent}\n +请从以下简历内容中提取关键要素:\n\`\`\`markdown\n${resumeContent}\n\`\`\`\n --- 要求提取: @@ -53,7 +55,7 @@ export const sendGptContent = async (page: Page, chatRecords) => { 每次生成前执行: 1. 检查历史记录 2. 确保技能/成果组合未重复 -3. 所生成的新消息,严禁包含最近8条已经发过的内容(包括但不限于职位名称) +3. 确保所生成的新消息不包含最近8条已经发过的内容(包括但不限于职位名称) 4. 字数严格控制在10-40字 5. 避免感叹号等激进符号 6. 减少头衔“资深”、“高级”出现的频率,严禁出现“专家”、“老兵”;减少工作年限“x年”出现的频率 diff --git a/packages/ui/src/renderer/src/page/ResumeEditor/index.vue b/packages/ui/src/renderer/src/page/ResumeEditor/index.vue index c910217..5a8a319 100644 --- a/packages/ui/src/renderer/src/page/ResumeEditor/index.vue +++ b/packages/ui/src/renderer/src/page/ResumeEditor/index.vue @@ -5,6 +5,14 @@
简历编辑器
+ +
    +
  • + 此简历将作为提示词的一部分提交给语言大模型,仅在匹配职位、生成已读不回提醒消息时使用;大部分信息非必填,但在不填写的情况下,可能会匹配到不准确的职位或生成预料之外的已读不回提醒消息 +
  • +
  • 期望薪资仅作匹配职位使用,不会用作生成已读不回提醒消息
  • +
+
- + - - - - - - - - - - + - + + +
+ + +
+
- + -