mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-06 16:07:17 +08:00
feat: enhance resume editor and formatResumeJsonToMarkdown; add the logic to send resume read from local to llm
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -2,6 +2,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 { completes } from '@geekgeekrun/utils/gpt-request.mjs'
|
||||||
import { readConfigFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.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) => {
|
export const sendLookForwardReplyEmotion = async (page: Page) => {
|
||||||
const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion')
|
const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion')
|
||||||
@@ -18,10 +19,11 @@ export const sendLookForwardReplyEmotion = async (page: Page) => {
|
|||||||
await lookForwardReplyEmojiProxy!.click()
|
await lookForwardReplyEmojiProxy!.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const resumeContent = ``
|
|
||||||
// let _index = 0
|
// let _index = 0
|
||||||
|
|
||||||
export const sendGptContent = async (page: Page, chatRecords) => {
|
export const sendGptContent = async (page: Page, chatRecords) => {
|
||||||
|
const resumeObject = (await readConfigFile('resumes.json'))?.[0]
|
||||||
|
const resumeContent = formatResumeJsonToMarkdown(resumeObject)
|
||||||
const chatList = [
|
const chatList = [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
@@ -32,12 +34,12 @@ export const sendGptContent = async (page: Page, chatRecords) => {
|
|||||||
- √ 包含1个核心技能 + 1个成果量化
|
- √ 包含1个核心技能 + 1个成果量化
|
||||||
- √ 使用不同句式模板(至少准备5种)
|
- √ 使用不同句式模板(至少准备5种)
|
||||||
- √ 谦虚一些,头衔、工作年限等在历史记录信息中出现一次就好
|
- √ 谦虚一些,头衔、工作年限等在历史记录信息中出现一次就好
|
||||||
- ✗ 禁止与最近发送的几条相似或雷同
|
- ✗ 严禁与最近发送的几条相似或雷同
|
||||||
- ✗ 禁止使用专业术语堆砌
|
|
||||||
- ✗ 严禁出现简历之外的词语
|
- ✗ 严禁出现简历之外的词语
|
||||||
|
- ✗ 严禁包含最近8条已经发过的内容(包括但不限于职位名称)
|
||||||
|
|
||||||
**简历分析层:**
|
**简历分析层:**
|
||||||
请从以下简历内容中提取关键要素:\n${resumeContent}\n
|
请从以下简历内容中提取关键要素:\n\`\`\`markdown\n${resumeContent}\n\`\`\`\n
|
||||||
|
|
||||||
---
|
---
|
||||||
要求提取:
|
要求提取:
|
||||||
@@ -53,7 +55,7 @@ export const sendGptContent = async (page: Page, chatRecords) => {
|
|||||||
每次生成前执行:
|
每次生成前执行:
|
||||||
1. 检查历史记录
|
1. 检查历史记录
|
||||||
2. 确保技能/成果组合未重复
|
2. 确保技能/成果组合未重复
|
||||||
3. 所生成的新消息,严禁包含最近8条已经发过的内容(包括但不限于职位名称)
|
3. 确保所生成的新消息不包含最近8条已经发过的内容(包括但不限于职位名称)
|
||||||
4. 字数严格控制在10-40字
|
4. 字数严格控制在10-40字
|
||||||
5. 避免感叹号等激进符号
|
5. 避免感叹号等激进符号
|
||||||
6. 减少头衔“资深”、“高级”出现的频率,严禁出现“专家”、“老兵”;减少工作年限“x年”出现的频率
|
6. 减少头衔“资深”、“高级”出现的频率,严禁出现“专家”、“老兵”;减少工作年限“x年”出现的频率
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
<div class="mt1em mb1em flex flex-items-center flex-justify-between">
|
<div class="mt1em mb1em flex flex-items-center flex-justify-between">
|
||||||
<span>简历编辑器</span>
|
<span>简历编辑器</span>
|
||||||
</div>
|
</div>
|
||||||
|
<el-alert type="info" :closable="false" mb20px line-height-1.25em>
|
||||||
|
<ul pl16px>
|
||||||
|
<li>
|
||||||
|
此简历将作为提示词的一部分提交给语言大模型,仅在匹配职位、生成已读不回提醒消息时使用;大部分信息非必填,但在不填写的情况下,可能会匹配到不准确的职位或生成预料之外的已读不回提醒消息
|
||||||
|
</li>
|
||||||
|
<li>期望薪资仅作匹配职位使用,不会用作生成已读不回提醒消息</li>
|
||||||
|
</ul>
|
||||||
|
</el-alert>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formContent"
|
:model="formContent"
|
||||||
@@ -15,39 +23,33 @@
|
|||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '1fr 1fr 1fr',
|
gridTemplateColumns: '1fr 1fr',
|
||||||
gap: '10px'
|
gap: '10px'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<el-form-item prop="providerCompleteApiUrl" label="姓名">
|
<el-form-item label="姓名">
|
||||||
<el-input v-model="formContent.name" font-size-12px></el-input>
|
<el-input v-model="formContent.name" font-size-12px></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="providerCompleteApiUrl" label="性别">
|
<el-form-item label="工作年限">
|
||||||
<el-input v-model="formContent.gender" font-size-12px></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="providerCompleteApiUrl" label="年龄">
|
|
||||||
<el-input-number
|
|
||||||
v-model="formContent.age"
|
|
||||||
w-full
|
|
||||||
font-size-12px
|
|
||||||
:min="0"
|
|
||||||
:max="200"
|
|
||||||
:precision="0"
|
|
||||||
:step="1"
|
|
||||||
></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="providerCompleteApiUrl" label="学历">
|
|
||||||
<el-input v-model="formContent.degree" font-size-12px></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="providerCompleteApiUrl" label="工作年限">
|
|
||||||
<el-input v-model="formContent.workYearDesc" font-size-12px></el-input>
|
<el-input v-model="formContent.workYearDesc" font-size-12px></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="providerCompleteApiUrl" label="期望职位">
|
<el-form-item label="期望职位">
|
||||||
<el-input v-model="formContent.expectJob" font-size-12px></el-input>
|
<el-input v-model="formContent.expectJob" font-size-12px></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="期望薪资(k)">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<el-input v-model="formContent.expectSalary[0]" placeholder="下限" />
|
||||||
|
<el-input v-model="formContent.expectSalary[1]" placeholder="上限" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form-item prop="providerCompleteApiUrl" label="个人优势">
|
<el-form-item label="个人优势">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formContent.userDescription"
|
v-model="formContent.userDescription"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -293,28 +295,27 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<footer flex pt10px pb10px pr20px flex-justify-between>
|
<footer pt10px pb10px flex flex-justify-center>
|
||||||
<div>
|
<div w768px flex flex-justify-between>
|
||||||
<!-- <el-button type="text" @click="handleTestAvailability">测试可用性</el-button> -->
|
<div>
|
||||||
</div>
|
<!-- <el-button type="text" @click="handleTestAvailability">测试可用性</el-button> -->
|
||||||
<div>
|
</div>
|
||||||
<el-button @click="handleCancel">取消</el-button>
|
<div>
|
||||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
<el-button @click="handleCancel">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ElForm, ElButton } from 'element-plus'
|
import { ElForm, ElButton, ElAlert } from 'element-plus'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
interface ResumeContent {
|
interface ResumeContent {
|
||||||
name: string
|
name: string
|
||||||
gender: string
|
|
||||||
age: string
|
|
||||||
degree: string
|
|
||||||
workYearDesc: string
|
workYearDesc: string
|
||||||
expectJob: string
|
expectJob: string
|
||||||
userDescription: string
|
userDescription: string
|
||||||
@@ -334,19 +335,18 @@ interface ResumeContent {
|
|||||||
projectDescription: string
|
projectDescription: string
|
||||||
performance: string
|
performance: string
|
||||||
}>
|
}>
|
||||||
|
expectSalary: [string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||||
|
|
||||||
const getEmptyFormContent = () => {
|
const getEmptyFormContent = () => {
|
||||||
const o: any = {
|
const o: any = {
|
||||||
age: '',
|
|
||||||
degree: '',
|
|
||||||
expectJob: '',
|
expectJob: '',
|
||||||
gender: '',
|
|
||||||
name: '',
|
name: '',
|
||||||
userDescription: '',
|
userDescription: '',
|
||||||
workYearDesc: '',
|
workYearDesc: '',
|
||||||
|
expectSalary: ['', ''],
|
||||||
geekWorkExpList: [],
|
geekWorkExpList: [],
|
||||||
geekProjExpList: []
|
geekProjExpList: []
|
||||||
}
|
}
|
||||||
@@ -375,6 +375,15 @@ onMounted(async () => {
|
|||||||
for (const k of Object.keys(formContent.value)) {
|
for (const k of Object.keys(formContent.value)) {
|
||||||
formContent.value[k] = savedFileContent[k]
|
formContent.value[k] = savedFileContent[k]
|
||||||
}
|
}
|
||||||
|
if (!formContent.value.expectSalary) {
|
||||||
|
formContent.value.expectSalary = ['', '']
|
||||||
|
}
|
||||||
|
if (!formContent.value.expectSalary?.[0] || /\D/.test(formContent.value.expectSalary?.[0])) {
|
||||||
|
formContent.value.expectSalary[0] = ''
|
||||||
|
}
|
||||||
|
if (!formContent.value.expectSalary?.[1] || /\D/.test(formContent.value.expectSalary?.[1])) {
|
||||||
|
formContent.value.expectSalary[1] = ''
|
||||||
|
}
|
||||||
if (!formContent.value.geekProjExpList?.length) {
|
if (!formContent.value.geekProjExpList?.length) {
|
||||||
formContent.value.geekProjExpList = [getNewProjExpItem()]
|
formContent.value.geekProjExpList = [getNewProjExpItem()]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user