From d9a6676db4afa26e6e6ee6c4dee8e44cfddcb1dd Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:16:09 +0100 Subject: [PATCH 01/30] Create README.md --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d552ac6 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +请首先配置好openai的api,随后将pdf简历上传到文件夹auto_job_find里,命名为“my_cover".随后执行write_response.py即可 +会自动生成openai的assistant,并在本地产生一个.json文件,只有第一次运行的时候才会产生,后面每次运行如果检测到这个json,就会调用已有的assistant + + +关于openai部分的包: +openai + +About RPA +tutorial video about how to learn rpa: https://www.youtube.com/watch?v=65OPFmEgCbM&list=PLx4LEkEdFArgrdD_lvXe_hYBy8zM0Sp3b&index=1 +Package of RPA +selenium +robotframework +robotframework-seleniumlibrary +robotframework-pythonlibcore + +Plugin: Intellibot@Selenium Library From 6b58a8c55eab48ca5553a581b4a95cc888e939bb Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:18:38 +0100 Subject: [PATCH 02/30] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index d552ac6..e74f7e9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +这是一个完全免费的脚本,只需要你们自己配置好openai的api即可 +希望您能给我点个star +如果在这个寒冷的招聘季,这个脚本能给您一些帮助,带来一些温暖,将让我非常荣幸 + +希望不要有人拿着我的脚本去割韭菜,都已经被逼到用这种脚本投简历的地步了,身上也没啥油水可榨了吧。可当个人吧 + + 请首先配置好openai的api,随后将pdf简历上传到文件夹auto_job_find里,命名为“my_cover".随后执行write_response.py即可 会自动生成openai的assistant,并在本地产生一个.json文件,只有第一次运行的时候才会产生,后面每次运行如果检测到这个json,就会调用已有的assistant @@ -14,3 +21,5 @@ robotframework-seleniumlibrary robotframework-pythonlibcore Plugin: Intellibot@Selenium Library + + From adab14cbc836d387f9f85a2de21339e6ffacfb1c Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:15:49 +0100 Subject: [PATCH 03/30] Update README.md add the tutorial link in bilibili and youtube --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e74f7e9..cd67290 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,7 @@ robotframework-pythonlibcore Plugin: Intellibot@Selenium Library +------------------下面是简单的教学视频--------------------- +B站链接:【赛博投简历脚本教程】 https://www.bilibili.com/video/BV1UC4y1N78v/?share_source=copy_web&vd_source=b2608434484091fcc64d4eb85233122d +油管链接:https://youtu.be/TlnytEi2lD8?si=jfcDj2MZqBptziZc From e92f485bb2fcc2c284ff0f7fbf03da86a97b6f60 Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:16:38 +0100 Subject: [PATCH 04/30] Update README.md Optimize display format --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cd67290..5861722 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ robotframework-pythonlibcore Plugin: Intellibot@Selenium Library ------------------下面是简单的教学视频--------------------- + B站链接:【赛博投简历脚本教程】 https://www.bilibili.com/video/BV1UC4y1N78v/?share_source=copy_web&vd_source=b2608434484091fcc64d4eb85233122d + 油管链接:https://youtu.be/TlnytEi2lD8?si=jfcDj2MZqBptziZc From d72ff53196364152493e9956f48c5ad551dc4042 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 5 Jan 2024 00:34:03 +0900 Subject: [PATCH 05/30] Update functions.py modifiy -> modify --- auto_job_find/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auto_job_find/functions.py b/auto_job_find/functions.py index d409b14..2ef5f91 100644 --- a/auto_job_find/functions.py +++ b/auto_job_find/functions.py @@ -24,7 +24,7 @@ def create_assistant(client): else: # If no assistant.json is present, create a new assistant using the below specifications - # To change the knowledge document, modifiy the file name below to match your document + # To change the knowledge document, modify the file name below to match your document # If you want to add multiple files, paste this function into ChatGPT and ask for it to add support for multiple files file = client.files.create(file=open("my_cover.pdf", "rb"), purpose='assistants') From 84adad88736da2399206012815aab3197bafa090 Mon Sep 17 00:00:00 2001 From: Winson Lee <100707097+Winson-030@users.noreply.github.com> Date: Fri, 5 Jan 2024 19:15:46 +0800 Subject: [PATCH 06/30] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 简单整理了下readme 的格式 --- README.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5861722..e7f5119 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,39 @@ 这是一个完全免费的脚本,只需要你们自己配置好openai的api即可 -希望您能给我点个star + +希望您能给我点个 **star** + 如果在这个寒冷的招聘季,这个脚本能给您一些帮助,带来一些温暖,将让我非常荣幸 -希望不要有人拿着我的脚本去割韭菜,都已经被逼到用这种脚本投简历的地步了,身上也没啥油水可榨了吧。可当个人吧 +希望不要有人拿着我的脚本去割韭菜,都已经被逼到用这种脚本投简历的地步了,身上也没啥油水可榨了吧。 + +## 操作步骤 + +1. 请首先配置好openai的api(使用.env文件或者在代码中配置) +2. 将pdf简历上传到文件夹auto_job_find里,命名为 **“my_cover.pdf"** +3. 将需要的包安装好 +4. 执行write_response.py +## 关于 assistant +会自动生成openai的asistant,并在本地产生一个.json文件,只有第一次运行的时候才会产生,后面每次运行如果检测到这个json,就会调用已有的asistant。 -请首先配置好openai的api,随后将pdf简历上传到文件夹auto_job_find里,命名为“my_cover".随后执行write_response.py即可 -会自动生成openai的assistant,并在本地产生一个.json文件,只有第一次运行的时候才会产生,后面每次运行如果检测到这个json,就会调用已有的assistant +## 使用到的包 +- `python-dotenv` +- `openai` +- `selenium` +- `robotframework` +- `robotframework-seleniumlibrary` +- `robotframework-pythonlibcore` -关于openai部分的包: -openai +## About RPA -About RPA -tutorial video about how to learn rpa: https://www.youtube.com/watch?v=65OPFmEgCbM&list=PLx4LEkEdFArgrdD_lvXe_hYBy8zM0Sp3b&index=1 -Package of RPA -selenium -robotframework -robotframework-seleniumlibrary -robotframework-pythonlibcore +tutorial video about how to learn [rpa](https://www.youtube.com/watch?v=65OPFmEgCbM&list=PLx4LEkEdFArgrdD_lvXe_hYBy8zM0Sp3b&index=1) Plugin: Intellibot@Selenium Library ------------------下面是简单的教学视频--------------------- -B站链接:【赛博投简历脚本教程】 https://www.bilibili.com/video/BV1UC4y1N78v/?share_source=copy_web&vd_source=b2608434484091fcc64d4eb85233122d +[B站链接](https://www.bilibili.com/video/BV1UC4y1N78v/?share_source=copy_web&vd_source=b2608434484091fcc64d4eb85233122d) -油管链接:https://youtu.be/TlnytEi2lD8?si=jfcDj2MZqBptziZc +[油管链接](https://youtu.be/TlnytEi2lD8?si=jfcDj2MZqBptziZc) From 82320dbb162b43b263858136a9803ff8584e11cc Mon Sep 17 00:00:00 2001 From: Winson Lee <100707097+Winson-030@users.noreply.github.com> Date: Fri, 5 Jan 2024 19:19:26 +0800 Subject: [PATCH 07/30] Update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e7f5119..3cb83a0 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ ## 操作步骤 -1. 请首先配置好openai的api(使用.env文件或者在代码中配置) -2. 将pdf简历上传到文件夹auto_job_find里,命名为 **“my_cover.pdf"** +1. 请首先配置好 openai 的 api(使用.env文件或者在代码中配置) +2. 将pdf简历上传到文件夹 auto_job_find 里,命名为 **“my_cover.pdf"** 3. 将需要的包安装好 -4. 执行write_response.py +4. 执行 write_response.py -## 关于 assistant -会自动生成openai的asistant,并在本地产生一个.json文件,只有第一次运行的时候才会产生,后面每次运行如果检测到这个json,就会调用已有的asistant。 +## 关于 asistant + +会自动生成 openai 的 asistant,并在本地产生一个 .json 文件,只有第一次运行的时候才会产生,后面每次运行如果检测到这个 json ,就会调用已有的 asistant。 ## 使用到的包 From 085a4e811cdd9c4a1b4e9c8f491617f2c4ad1fa8 Mon Sep 17 00:00:00 2001 From: Sidi Li Date: Fri, 5 Jan 2024 19:38:41 +0800 Subject: [PATCH 08/30] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86langchain?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E6=9D=A5=E8=BF=90=E8=A1=8C=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=E6=8E=A8=E8=8D=90=E9=A1=B5=E9=9D=A2=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E4=B8=8B=E6=8B=89=E6=8C=89=E9=92=AE=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++ auto_job_find/.env | 3 + auto_job_find/finding_jobs.py | 32 ++++++-- auto_job_find/functions.py | 4 +- auto_job_find/langchain_functions.py | 82 +++++++++++++++++++ auto_job_find/requirements.txt | 5 ++ ...6\346\224\276\350\277\231\351\207\214.txt" | 0 auto_job_find/write_response.py | 30 ++++--- 8 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 auto_job_find/.env create mode 100644 auto_job_find/langchain_functions.py create mode 100644 auto_job_find/requirements.txt create mode 100644 "auto_job_find/resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" diff --git a/README.md b/README.md index 5861722..3c9a685 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,19 @@ B站链接:【赛博投简历脚本教程】 https://www.bilibili.com/video/BV 油管链接:https://youtu.be/TlnytEi2lD8?si=jfcDj2MZqBptziZc +## 运行方式 +先将该项目clone到本地,然后在项目根目录下执行 +```bash +pip install -r requirements.txt +``` + +### assistant方式运行 +打开.env文件,在里面配置好OpenAI的API key +随后将pdf简历上传到文件夹auto_job_find里,命名为“my_cover".随后执行write_response.py即可 +这种方式不支持使用自定义api,优势是执行速度更快 +如果需要使用自定义api,请使用下面的方式运行 + +### langchain方式 +同样打开.env文件,在里面配置好OpenAI的API key和你想要请求的api地址 +随后将pdf简历放到文件夹resume里 +最后执行write_response.py即可 diff --git a/auto_job_find/.env b/auto_job_find/.env new file mode 100644 index 0000000..e3b1ec5 --- /dev/null +++ b/auto_job_find/.env @@ -0,0 +1,3 @@ +# OPENAI_BASE_URL=https://api.openai.com/v1 + +# OPENAI_API_KEY='' \ No newline at end of file diff --git a/auto_job_find/finding_jobs.py b/auto_job_find/finding_jobs.py index 50835c4..55dc4f5 100644 --- a/auto_job_find/finding_jobs.py +++ b/auto_job_find/finding_jobs.py @@ -81,25 +81,39 @@ def get_job_description(): return job_description def select_dropdown_option(driver, label): - # 确保触发下拉列表的元素可见并且可点击 + # 尝试在具有特定类的元素中找到文本 + trigger_elements = driver.find_elements(By.XPATH, "//*[@class='recommend-job-btn has-tooltip']") + + # 标记是否找到元素 + found = False + + for element in trigger_elements: + if label in element.text: + # 确保元素可见并且可点击 + WebDriverWait(driver, 10).until(EC.element_to_be_clickable(element)) + element.click() # 点击找到的元素 + found = True + break + + # 如果在按钮中找到了文本,就不再继续下面的操作 + if found: + return + + # 如果在按钮中没有找到文本,执行原来的下拉列表操作 trigger_selector = "//*[@id='wrap']/div[2]/div[1]/div/div[1]/div" - trigger_element = WebDriverWait(driver, 10).until( + WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.XPATH, trigger_selector)) - ) - trigger_element.click() # 打开下拉菜单 + ).click() # 打开下拉菜单 - # 等待下拉列表元素变为可见 dropdown_selector = "ul.dropdown-expect-list" WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, dropdown_selector)) ) - # 现在点击下拉列表中的具体选项 option_selector = f"//li[contains(text(), '{label}')]" - option_element = WebDriverWait(driver, 10).until( + WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.XPATH, option_selector)) - ) - option_element.click() # 选择下拉菜单中的选项 + ).click() # 选择下拉菜单中的选项 def get_job_description_by_index(index): diff --git a/auto_job_find/functions.py b/auto_job_find/functions.py index 2ef5f91..ee73c72 100644 --- a/auto_job_find/functions.py +++ b/auto_job_find/functions.py @@ -6,10 +6,10 @@ load_dotenv() OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') - +OPENAI_BASE_URL = os.getenv('OPENAI_BASE_URL') # Init OpenAI Client -client = OpenAI(api_key=OPENAI_API_KEY) +client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL) # Create or load assistant def create_assistant(client): diff --git a/auto_job_find/langchain_functions.py b/auto_job_find/langchain_functions.py new file mode 100644 index 0000000..62b78f4 --- /dev/null +++ b/auto_job_find/langchain_functions.py @@ -0,0 +1,82 @@ +from langchain.document_loaders import DirectoryLoader, PyPDFLoader +from langchain.text_splitter import CharacterTextSplitter +from langchain.embeddings import OpenAIEmbeddings +from langchain.vectorstores import FAISS +from langchain.chains import RetrievalQA +from langchain.chat_models import ChatOpenAI +from langchain.prompts import PromptTemplate +import os +from dotenv import load_dotenv +load_dotenv() + +OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') +OPENAI_BASE_URL = os.getenv('OPENAI_BASE_URL') + +# 判断是否使用langchain。如果设置了自定义的api地址,则使用langchain +def should_use_langchain(): + should_use_langchain = OPENAI_BASE_URL is not None + return should_use_langchain + +# 读取简历数据 +def read_resumes(): + # 读取resume文件夹中的所有文件 + d_loader = DirectoryLoader("./resume", glob="*.pdf",loader_cls=PyPDFLoader) + + # 获取 PDF 文本,返回一个列表,列表中的每个元素都是一个 PDF 文档的页 + pdf_pages = d_loader.load() + + resume_text = "" + + for page in pdf_pages: + # 建立路径,用于区分是哪一份简历 + # print(page.metadata.get('source')) + + page_text = page.page_content + resume_text += page_text + + return resume_text + +# 文本分割 +def get_text_chunks(text): + text_splitter = CharacterTextSplitter( + separator="\n", + chunk_size=2000, + chunk_overlap=200, + length_function=len + ) + chunks = text_splitter.split_text(text) + return chunks + +# 向量化,并且返回向量存储库 +def get_vectorstore(text_chunks): + embeddings = OpenAIEmbeddings() + # embeddings = HuggingFaceInstructEmbeddings(model_name="hkunlp/instructor-xl") + vectorstore = FAISS.from_texts(texts=text_chunks, embedding=embeddings) + return vectorstore + +# 生成求职信 +def generate_letter(vectorstore, job_description): + langchain_prompt_template = f""" + 你将扮演一位求职者的角色,根据上下文里的简历内容以及应聘工作的描述,来直接给HR写一个礼貌专业的求职新消息,要求能够用专业的语言结合简历中的经历和技能,并结合应聘工作的描述,来阐述自己的优势,尽最大可能打动招聘者。 + 工作描述 + {job_description}"""+""" + 简历内容: + {context} + 要求: + {question} + """ + + question = "并且请您始终使用中文来进行消息的编写,开头是招聘负责人,结尾是真诚的。这是一封完整的求职信,不要包含求职信内容以外的东西,例如“根据您上传的求职要求和个人简历,我来帮您起草一封求职邮件:”这一类的内容,以便于我直接自动化复制粘贴发送" + + PROMPT = PromptTemplate.from_template(langchain_prompt_template) + llm = ChatOpenAI(temperature=0, openai_api_base=OPENAI_BASE_URL, openai_api_key=OPENAI_API_KEY) + qa_chain = RetrievalQA.from_chain_type( + llm, + retriever=vectorstore.as_retriever(), + # return_source_documents=True, + chain_type_kwargs={"prompt": PROMPT} + ) + + result = qa_chain({"query": question}) + + return result['result'] diff --git a/auto_job_find/requirements.txt b/auto_job_find/requirements.txt new file mode 100644 index 0000000..955cce4 --- /dev/null +++ b/auto_job_find/requirements.txt @@ -0,0 +1,5 @@ +langchain==0.0.354 +openai==1.6.1 +packaging==23.2 +python-dotenv==1.0.0 +selenium==4.16.0 diff --git "a/auto_job_find/resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" "b/auto_job_find/resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" new file mode 100644 index 0000000..e69de29 diff --git a/auto_job_find/write_response.py b/auto_job_find/write_response.py index 3eb0c8c..cc8b074 100644 --- a/auto_job_find/write_response.py +++ b/auto_job_find/write_response.py @@ -8,11 +8,11 @@ from selenium.webdriver import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait +from langchain_functions import get_text_chunks, get_vectorstore, read_resumes, should_use_langchain, generate_letter import functions import finding_jobs - # Check OpenAI version compatibility from packaging import version from dotenv import load_dotenv @@ -31,9 +31,10 @@ # Initialize OpenAI client client = OpenAI(api_key=OPENAI_API_KEY) -# Create or load assistant -assistant_id = functions.create_assistant( - client) # this function comes from "functions.py" +if not should_use_langchain(): + # Create or load assistant + assistant_id = functions.create_assistant( + client) # this function comes from "functions.py" def create_thread(client): # Function to create a new thread and return its ID @@ -125,13 +126,11 @@ def send_response_and_go_back(driver, response): driver.back() time.sleep(3) -def send_job_descriptions_to_chat(assistant_id, url, browser_type, label): +def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, vectorstore=None): # 开始浏览并获取工作描述 finding_jobs.open_browser_with_options(url, browser_type) finding_jobs.log_in() - - job_index = 1 # 开始的索引 while True: try: @@ -144,7 +143,10 @@ def send_job_descriptions_to_chat(assistant_id, url, browser_type, label): job_description = finding_jobs.get_job_description_by_index(job_index) if job_description: # 发送描述到聊天并打印响应 - response = chat(job_description, assistant_id) + if should_use_langchain(): + response = generate_letter(vectorstore, job_description) + else: + response = chat(job_description, assistant_id) print(response) time.sleep(1) # 点击沟通按钮 @@ -172,9 +174,15 @@ def send_job_descriptions_to_chat(assistant_id, url, browser_type, label): if __name__ == '__main__': - assistant_id = functions.create_assistant(client) url = "https://www.zhipin.com/web/geek/job-recommend?ka=header-job-recommend" browser_type = "chrome" - label = "互联网产品经理(上海)" # 想要选择的下拉菜单项 - send_job_descriptions_to_chat(assistant_id, url, browser_type,label) + label = "iOS(深圳)" # 想要选择的下拉菜单项 + if should_use_langchain(): + text = read_resumes() + chunks = get_text_chunks(text) + vectorstore = get_vectorstore(chunks) + send_job_descriptions_to_chat(url, browser_type,label, vectorstore=vectorstore) + else: + assistant_id = functions.create_assistant(client) + send_job_descriptions_to_chat(url, browser_type,label, assistant_id=assistant_id) From d774af54287258f7a709fdbb574edee3e37df7af Mon Sep 17 00:00:00 2001 From: Winson Lee <100707097+Winson-030@users.noreply.github.com> Date: Fri, 5 Jan 2024 20:13:17 +0800 Subject: [PATCH 09/30] Create .env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了环境变量的文件 add .env file for environment variables --- auto_job_find/.env | 1 + 1 file changed, 1 insertion(+) create mode 100644 auto_job_find/.env diff --git a/auto_job_find/.env b/auto_job_find/.env new file mode 100644 index 0000000..dfb9d72 --- /dev/null +++ b/auto_job_find/.env @@ -0,0 +1 @@ +OPENAI_API_KEY=sk-xxxx From 7dc5cefefca4659f5bf5cb0b190f8b3d48f96147 Mon Sep 17 00:00:00 2001 From: Winson Lee <100707097+Winson-030@users.noreply.github.com> Date: Fri, 5 Jan 2024 20:50:44 +0800 Subject: [PATCH 10/30] Create requirements.txt added packages --- auto_job_find/requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 auto_job_find/requirements.txt diff --git a/auto_job_find/requirements.txt b/auto_job_find/requirements.txt new file mode 100644 index 0000000..bf1a004 --- /dev/null +++ b/auto_job_find/requirements.txt @@ -0,0 +1,6 @@ +openai +python-dotenv +selenium +robotframework +robotframework-seleniumlibrary +robotframework-pythonlibcore From 0120dfdf4af9f3748efe3ff845613c1e0e340615 Mon Sep 17 00:00:00 2001 From: Sidi Li Date: Fri, 5 Jan 2024 21:00:52 +0800 Subject: [PATCH 11/30] =?UTF-8?q?=E4=BC=98=E5=8C=96prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto_job_find/langchain_functions.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/auto_job_find/langchain_functions.py b/auto_job_find/langchain_functions.py index 62b78f4..33e6e21 100644 --- a/auto_job_find/langchain_functions.py +++ b/auto_job_find/langchain_functions.py @@ -57,7 +57,7 @@ def get_vectorstore(text_chunks): # 生成求职信 def generate_letter(vectorstore, job_description): langchain_prompt_template = f""" - 你将扮演一位求职者的角色,根据上下文里的简历内容以及应聘工作的描述,来直接给HR写一个礼貌专业的求职新消息,要求能够用专业的语言结合简历中的经历和技能,并结合应聘工作的描述,来阐述自己的优势,尽最大可能打动招聘者。 + 你将扮演一位求职者的角色,根据上下文里的简历内容以及应聘工作的描述,来直接给HR写一个礼貌专业的求职新消息,要求能够用专业的语言结合简历中的经历和技能,并结合应聘工作的描述,来阐述自己的优势,尽最大可能打动招聘者。始终使用中文来进行消息的编写,字数限制在300字以内。开头是招聘负责人, 结尾附上求职者联系方式。这是一份求职消息,不要包含求职内容以外的东西,例如“根据您上传的求职要求和个人简历,我来帮您起草一封求职邮件:”这一类的内容,以便于我直接自动化复制粘贴发送。 工作描述 {job_description}"""+""" 简历内容: @@ -66,10 +66,10 @@ def generate_letter(vectorstore, job_description): {question} """ - question = "并且请您始终使用中文来进行消息的编写,开头是招聘负责人,结尾是真诚的。这是一封完整的求职信,不要包含求职信内容以外的东西,例如“根据您上传的求职要求和个人简历,我来帮您起草一封求职邮件:”这一类的内容,以便于我直接自动化复制粘贴发送" + question = "根据工作描述,寻找出简历里最合适的技能都有哪些?求职者的优势是什么?" PROMPT = PromptTemplate.from_template(langchain_prompt_template) - llm = ChatOpenAI(temperature=0, openai_api_base=OPENAI_BASE_URL, openai_api_key=OPENAI_API_KEY) + llm = ChatOpenAI(temperature=3, openai_api_base=OPENAI_BASE_URL, openai_api_key=OPENAI_API_KEY) qa_chain = RetrievalQA.from_chain_type( llm, retriever=vectorstore.as_retriever(), @@ -78,5 +78,9 @@ def generate_letter(vectorstore, job_description): ) result = qa_chain({"query": question}) + letter = result['result'] - return result['result'] + #去掉所有换行符,防止分成多段消息 + letter = letter.replace('\n', ' ') + + return letter From d394f0b2ba69413dd8befbaa71e15328e5295da1 Mon Sep 17 00:00:00 2001 From: Sidi Li Date: Fri, 5 Jan 2024 21:10:04 +0800 Subject: [PATCH 12/30] =?UTF-8?q?=E5=86=8D=E6=AC=A1=E4=BC=98=E5=8C=96promp?= =?UTF-8?q?t=EF=BC=8C=E5=81=9A=E5=87=BA=E5=AD=97=E6=95=B0=E9=99=90?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto_job_find/langchain_functions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/auto_job_find/langchain_functions.py b/auto_job_find/langchain_functions.py index 33e6e21..fa1fefe 100644 --- a/auto_job_find/langchain_functions.py +++ b/auto_job_find/langchain_functions.py @@ -56,8 +56,10 @@ def get_vectorstore(text_chunks): # 生成求职信 def generate_letter(vectorstore, job_description): + character_limit = 300 + langchain_prompt_template = f""" - 你将扮演一位求职者的角色,根据上下文里的简历内容以及应聘工作的描述,来直接给HR写一个礼貌专业的求职新消息,要求能够用专业的语言结合简历中的经历和技能,并结合应聘工作的描述,来阐述自己的优势,尽最大可能打动招聘者。始终使用中文来进行消息的编写,字数限制在300字以内。开头是招聘负责人, 结尾附上求职者联系方式。这是一份求职消息,不要包含求职内容以外的东西,例如“根据您上传的求职要求和个人简历,我来帮您起草一封求职邮件:”这一类的内容,以便于我直接自动化复制粘贴发送。 + 你将扮演一位求职者的角色,根据上下文里的简历内容以及应聘工作的描述,来直接给HR写一个礼貌专业, 且字数严格限制在{character_limit}以内的求职消息,要求能够用专业的语言结合简历中的经历和技能,并结合应聘工作的描述,来阐述自己的优势,尽最大可能打动招聘者。始终使用中文来进行消息的编写。开头是招聘负责人, 结尾附上求职者联系方式。这是一份求职消息,不要包含求职内容以外的东西,例如“根据您上传的求职要求和个人简历,我来帮您起草一封求职邮件:”这一类的内容,以便于我直接自动化复制粘贴发送。 工作描述 {job_description}"""+""" 简历内容: @@ -82,5 +84,5 @@ def generate_letter(vectorstore, job_description): #去掉所有换行符,防止分成多段消息 letter = letter.replace('\n', ' ') - + return letter From 1497adf19e2bd1822928231225a428f56d994d6e Mon Sep 17 00:00:00 2001 From: Sidi Li Date: Fri, 5 Jan 2024 21:10:32 +0800 Subject: [PATCH 13/30] =?UTF-8?q?=E9=99=8D=E4=BD=8E=E5=AD=97=E6=95=B0?= =?UTF-8?q?=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto_job_find/langchain_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/auto_job_find/langchain_functions.py b/auto_job_find/langchain_functions.py index fa1fefe..5e65fe9 100644 --- a/auto_job_find/langchain_functions.py +++ b/auto_job_find/langchain_functions.py @@ -56,6 +56,7 @@ def get_vectorstore(text_chunks): # 生成求职信 def generate_letter(vectorstore, job_description): + # 字数限制 character_limit = 300 langchain_prompt_template = f""" From 128d52de2be9b71b5cc052618e61bf0d4d2df74c Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Sat, 6 Jan 2024 08:26:03 +0100 Subject: [PATCH 14/30] Update README.md Add others js script --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 3cb83a0..7c1f224 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,12 @@ Plugin: Intellibot@Selenium Library [油管链接](https://youtu.be/TlnytEi2lD8?si=jfcDj2MZqBptziZc) +------------下面是其他朋友基于js构建的更加易于使用的代码--------------- + +我一直也在考虑如何可以降低各位的使用门槛,基于现在项目的热度,我发现很多朋友都需要这个东西来帮助自己,但是我相信对于更多的人而言,甚至vpn都是一个障碍 + +下面这位朋友基于js实现了一个更加简易的版本,虽然因为调用的免费api,无法使用assistant进行retrival,需要自己对简历进行简单的处理,但我依然认为这是个很棒的项目 + +感谢朋友的贡献,以下是链接: + +[https://github.com/noBaldAaa](https://github.com/noBaldAaa/find-job)https://github.com/noBaldAaa/find-job From 4dc552419e721cd567430cc1e7076fc5610f4629 Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:58:21 +0100 Subject: [PATCH 15/30] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 田间了关于faiss-cpu的注意事项 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4b88252..a7e2a39 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - `robotframework` - `robotframework-seleniumlibrary` - `robotframework-pythonlibcore` +- `faiss-cpu不支持3.12(faiss-gpu不清楚)。建议大家用3.11及以下版本的python运行脚本。` from @[huanmit](https://github.com/huanmit) ## About RPA From 4eef2005bb6d8ba5974cd5a335135b43c5e491cd Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Mon, 5 Feb 2024 07:45:01 +0800 Subject: [PATCH 16/30] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 推荐微软云版本 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a7e2a39..f5e4a11 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,6 @@ pip install -r requirements.txt 感谢朋友的贡献,以下是链接: [https://github.com/noBaldAaa](https://github.com/noBaldAaa/find-job)https://github.com/noBaldAaa/find-job + +------------下面是其他朋友基于azure的openai api构建的版本的更加易于使用的代码--------------- +https://github.com/LouisCaixuran/auto_job_find_azure From cd9d8072b84b92821c0ae11e4bde4fc544d705d3 Mon Sep 17 00:00:00 2001 From: yilin-gong <78230512+yilin-gong@users.noreply.github.com> Date: Fri, 9 Feb 2024 02:09:59 -0600 Subject: [PATCH 17/30] Update write_response.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 只发送消息给新HR,不会重复发送 --- auto_job_find/write_response.py | 43 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/auto_job_find/write_response.py b/auto_job_find/write_response.py index cc8b074..dc65ebd 100644 --- a/auto_job_find/write_response.py +++ b/auto_job_find/write_response.py @@ -142,25 +142,30 @@ def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, v # 调用 finding_jobs.py 中的函数来获取描述 job_description = finding_jobs.get_job_description_by_index(job_index) if job_description: - # 发送描述到聊天并打印响应 - if should_use_langchain(): - response = generate_letter(vectorstore, job_description) - else: - response = chat(job_description, assistant_id) - print(response) - time.sleep(1) - # 点击沟通按钮 - contact_button = driver.find_element(By.XPATH, "//*[@id='wrap']/div[2]/div[2]/div/div/div[2]/div/div[1]/div[2]/a[2]") - contact_button.click() - - # 等待回复框出现 - xpath_locator_chat_box = "//*[@id='chat-input']" - chat_box = WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.XPATH, xpath_locator_chat_box)) - ) - - # 调用函数发送响应 - send_response_and_go_back(driver, response) + element = driver.find_element(By.CSS_SELECTOR, '.op-btn.op-btn-chat').text + print(element) + if element == '立即沟通': + # 发送描述到聊天并打印响应 + if should_use_langchain(): + response = generate_letter(vectorstore, job_description) + else: + response = chat(job_description, assistant_id) + print(response) + time.sleep(1) + # 点击沟通按钮 + + contact_button = driver.find_element(By.XPATH, "//*[@id='wrap']/div[2]/div[2]/div/div/div[2]/div/div[1]/div[2]/a[2]") + + contact_button.click() + + # 等待回复框出现 + xpath_locator_chat_box = "//*[@id='chat-input']" + chat_box = WebDriverWait(driver, 50).until( + EC.presence_of_element_located((By.XPATH, xpath_locator_chat_box)) + ) + + # 调用函数发送响应 + send_response_and_go_back(driver, response) From 0c86222e844ecc7385db34653eb7b22f00038cbf Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:16:49 +0800 Subject: [PATCH 18/30] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加了简化付费版说明 --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index f5e4a11..def032a 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,15 @@ pip install -r requirements.txt 随后将pdf简历放到文件夹resume里 最后执行write_response.py即可 +## 简化付费版 + +可以一键下载直接运行,里面甚至不需要自己配置API,如果当前的免费版本让您头痛,难以使用,您也可以在下面链接找到一个更加易用的版本,抛除API和服务器我几乎 +不赚钱,就当请我喝一杯咖啡吧,感谢各位的支持 +https://www.123pan.com/s/0kebjv-gQIZ3.html + + + + ------------下面是其他朋友基于js构建的更加易于使用的代码--------------- 我一直也在考虑如何可以降低各位的使用门槛,基于现在项目的热度,我发现很多朋友都需要这个东西来帮助自己,但是我相信对于更多的人而言,甚至vpn都是一个障碍 @@ -68,3 +77,5 @@ pip install -r requirements.txt ------------下面是其他朋友基于azure的openai api构建的版本的更加易于使用的代码--------------- https://github.com/LouisCaixuran/auto_job_find_azure + + From b3b901991ed0a43fefdf48c31381921d3df67986 Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:00:55 +0800 Subject: [PATCH 19/30] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 各位见谅,宣传一下产品 --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index def032a..d3a6ba6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ +作者已经去打工了,现在在sider做产品经理,义务为我们的产品打一下广告,各位见谅 +[![banner](https://github.com/Frrrrrrrrank/auto_job__find__chatgpt__rpa/assets/82270228/0fff88f5-670c-4126-b6aa-3df1763fe757)](https://sider.ai/ad-land-redirect?source=github&p1=mi&p2=kk) + +欢迎大家使用 + +## 更加便于操作的版本 +另外,如果一些用户确实布置这个项目遇到很多问题,我提供一个收费的版本,加了美工服务器,跑的是我自己的api,一键无脑使用,网站上也有教学视频和使用手册 + + +www.aijobpathfinder.com + + +差不多就是成本价+服务器还有美工的成本,当然各位要是技术ok,免费版本和付费版本是完全一样的,甚至这里还支持langchian + + 这是一个完全免费的脚本,只需要你们自己配置好openai的api即可 希望您能给我点个 **star** From 5e04c616cd07e32f3a0e961f3a6acc248a166b67 Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:02:59 +0800 Subject: [PATCH 20/30] Update README.md --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d3a6ba6..b2c70e1 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@ 欢迎大家使用 -## 更加便于操作的版本 +## 更加便于操作的付费版本 另外,如果一些用户确实布置这个项目遇到很多问题,我提供一个收费的版本,加了美工服务器,跑的是我自己的api,一键无脑使用,网站上也有教学视频和使用手册 www.aijobpathfinder.com -差不多就是成本价+服务器还有美工的成本,当然各位要是技术ok,免费版本和付费版本是完全一样的,甚至这里还支持langchian - +差不多就是成本价+服务器还有美工的成本,就当请我喝一杯咖啡吧~当然各位要是技术ok,免费版本和付费版本是完全一样的,甚至这里还支持langchian +## 正文 这是一个完全免费的脚本,只需要你们自己配置好openai的api即可 希望您能给我点个 **star** @@ -71,11 +71,7 @@ pip install -r requirements.txt 随后将pdf简历放到文件夹resume里 最后执行write_response.py即可 -## 简化付费版 -可以一键下载直接运行,里面甚至不需要自己配置API,如果当前的免费版本让您头痛,难以使用,您也可以在下面链接找到一个更加易用的版本,抛除API和服务器我几乎 -不赚钱,就当请我喝一杯咖啡吧,感谢各位的支持 -https://www.123pan.com/s/0kebjv-gQIZ3.html From c695b1ac15e58d2a471e6cd4ce218d7e2a456d44 Mon Sep 17 00:00:00 2001 From: Loong Loong Date: Wed, 22 May 2024 13:55:31 +0800 Subject: [PATCH 21/30] 1. `finding_jobs.py` It can provide more time to let users select the tags by cancel the comment `time.sleep(20)` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2. `write_response.py` function `create_thread`, add timeout time to avoid stuck. 3. `write_response.py` fix: no more "【" or "】" appear in the content now --- auto_job_find/finding_jobs.py | 16 ++++++++++++---- auto_job_find/write_response.py | 32 ++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/auto_job_find/finding_jobs.py b/auto_job_find/finding_jobs.py index 55dc4f5..1ea8ac3 100644 --- a/auto_job_find/finding_jobs.py +++ b/auto_job_find/finding_jobs.py @@ -24,6 +24,12 @@ def open_browser_with_options(url, browser): if browser == "chrome": driver = webdriver.Chrome(options=options) driver.maximize_window() + elif browser == "edge": + driver = webdriver.Edge() + driver.maximize_window() + elif browser == "safari": + driver = webdriver.Safari() + driver.maximize_window() else: raise ValueError("Browser type not supported") @@ -35,6 +41,7 @@ def open_browser_with_options(url, browser): EC.presence_of_element_located((By.XPATH, xpath_locator)) ) + def log_in(): global driver @@ -61,8 +68,8 @@ def log_in(): EC.presence_of_element_located((By.XPATH, xpath_locator_login_success)) ) -def get_job_description(): +def get_job_description(): global driver # 使用给定的 XPath 定位职位描述元素 @@ -80,6 +87,7 @@ def get_job_description(): # 返回职位描述文本,如果函数需要 return job_description + def select_dropdown_option(driver, label): # 尝试在具有特定类的元素中找到文本 trigger_elements = driver.find_elements(By.XPATH, "//*[@class='recommend-job-btn has-tooltip']") @@ -97,6 +105,8 @@ def select_dropdown_option(driver, label): # 如果在按钮中找到了文本,就不再继续下面的操作 if found: + # 取消注释,提供选择更多tag的时间 + # time.sleep(20) return # 如果在按钮中没有找到文本,执行原来的下拉列表操作 @@ -133,9 +143,7 @@ def get_job_description_by_index(index): print(f"No job found at index {index}.") return None + # Variables url = "https://www.zhipin.com/web/geek/job-recommend?ka=header-job-recommend" browser_type = "chrome" - - - diff --git a/auto_job_find/write_response.py b/auto_job_find/write_response.py index dc65ebd..2dfdc1e 100644 --- a/auto_job_find/write_response.py +++ b/auto_job_find/write_response.py @@ -16,17 +16,18 @@ # Check OpenAI version compatibility from packaging import version from dotenv import load_dotenv + load_dotenv() required_version = version.parse("1.1.1") current_version = version.parse(openai.__version__) OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') if current_version < required_version: - raise ValueError( - f"Error: OpenAI version {openai.__version__} is less than the required version 1.1.1" - ) + raise ValueError( + f"Error: OpenAI version {openai.__version__} is less than the required version 1.1.1" + ) else: - print("OpenAI version is compatible.") + print("OpenAI version is compatible.") # Initialize OpenAI client client = OpenAI(api_key=OPENAI_API_KEY) @@ -36,6 +37,7 @@ assistant_id = functions.create_assistant( client) # this function comes from "functions.py" + def create_thread(client): # Function to create a new thread and return its ID try: @@ -74,7 +76,8 @@ def chat(user_input, assistant_id, thread_id=None): while True: run_status = client.beta.threads.runs.retrieve( thread_id=thread_id, - run_id=run.id + run_id=run.id, + timeout=60 # 设置超时时间为60秒 ) if run_status.status == 'completed': @@ -89,7 +92,10 @@ def chat(user_input, assistant_id, thread_id=None): assistant_message = messages.data[0].content[0].text.value # 将换行符替换为一个空格 - formatted_message = assistant_message.replace("\n", " ") + formatted_message = assistant_message.replace("\n", " ").replace(" ", "").replace("真诚的,龙思卓", "") + import re + formatted_message = re.sub(r'【.*?】', '', formatted_message) + # response_data = json.dumps({"response": assistant_message, "thread_id": thread_id}) return formatted_message @@ -116,7 +122,6 @@ def send_response_to_chat_box(driver, response): time.sleep(1) - def send_response_and_go_back(driver, response): # 调用函数发送响应 send_response_to_chat_box(driver, response) @@ -126,6 +131,7 @@ def send_response_and_go_back(driver, response): driver.back() time.sleep(3) + def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, vectorstore=None): # 开始浏览并获取工作描述 finding_jobs.open_browser_with_options(url, browser_type) @@ -154,8 +160,9 @@ def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, v time.sleep(1) # 点击沟通按钮 - contact_button = driver.find_element(By.XPATH, "//*[@id='wrap']/div[2]/div[2]/div/div/div[2]/div/div[1]/div[2]/a[2]") - + contact_button = driver.find_element(By.XPATH, + "//*[@id='wrap']/div[2]/div[2]/div/div/div[2]/div/div[1]/div[2]/a[2]") + contact_button.click() # 等待回复框出现 @@ -167,8 +174,6 @@ def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, v # 调用函数发送响应 send_response_and_go_back(driver, response) - - # 等待一定时间后处理下一个工作描述 time.sleep(3) # job_index += 1 @@ -186,8 +191,7 @@ def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, v text = read_resumes() chunks = get_text_chunks(text) vectorstore = get_vectorstore(chunks) - send_job_descriptions_to_chat(url, browser_type,label, vectorstore=vectorstore) + send_job_descriptions_to_chat(url, browser_type, label, vectorstore=vectorstore) else: assistant_id = functions.create_assistant(client) - send_job_descriptions_to_chat(url, browser_type,label, assistant_id=assistant_id) - + send_job_descriptions_to_chat(url, browser_type, label, assistant_id=assistant_id) From 0c4c39e8a2bd34b1c5174e18b9a65f8ce900019d Mon Sep 17 00:00:00 2001 From: Loong Loong Date: Sat, 31 Aug 2024 18:16:11 +1000 Subject: [PATCH 22/30] =?UTF-8?q?Update=20README.md,=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86chatGPT4=E5=8F=8A=E4=BB=A5=E4=B8=8A=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### chatgpt4 及以上运行方式 如果尝试使用更新的chatGPT则不能保持最新版本为`v1.1.1`,同时如果报错信息为`An error occurred: Error code: 400 - {'error': {'message': "The requested model 'gpt-4o-mini' cannot be used with the Assistants API in v1. Follow the migration guide to upgrade to v2: https://platform.openai.com/docs/assistants/migration.", 'type': 'invalid_request_error', 'param': 'model', 'code': 'unsupported_model'}}` 需要手动将chatgpt更新到最新版,以及更改`create_assistant`中的结构体,详细参考[迁移模型](https://platform.openai.com/docs/assistants/migration)中的描述。建议直接在[平台](https://platform.openai.com/assistants/)上手动添加最新的assist然后复制代码到`assistant.json`中最为方便 ```json {"assistant_ --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2c70e1..e4b89a4 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,13 @@ pip install -r requirements.txt 最后执行write_response.py即可 +### chatgpt4 及以上运行方式 +如果尝试使用更新的chatGPT则不能保持最新版本为`v1.1.1`,同时如果报错信息为`An error occurred: Error code: 400 - {'error': {'message': "The requested model 'gpt-4o-mini' cannot be used with the Assistants API in v1. Follow the migration guide to upgrade to v2: https://platform.openai.com/docs/assistants/migration.", 'type': 'invalid_request_error', 'param': 'model', 'code': 'unsupported_model'}}` - +需要手动将chatgpt更新到最新版,以及更改`create_assistant`中的结构体,详细参考[迁移模型](https://platform.openai.com/docs/assistants/migration)中的描述。建议直接在[平台](https://platform.openai.com/assistants/)上手动添加最新的assist然后复制代码到`assistant.json`中最为方便 +```json +{"assistant_id": "asst_token"} +``` ------------下面是其他朋友基于js构建的更加易于使用的代码--------------- From 05f136592173539733f3abfc333c2afad54f6ef8 Mon Sep 17 00:00:00 2001 From: Loong Loong Date: Sat, 31 Aug 2024 18:20:53 +1000 Subject: [PATCH 23/30] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4b89a4..2bfb6f8 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,13 @@ pip install -r requirements.txt ### chatgpt4 及以上运行方式 如果尝试使用更新的chatGPT则不能保持最新版本为`v1.1.1`,同时如果报错信息为`An error occurred: Error code: 400 - {'error': {'message': "The requested model 'gpt-4o-mini' cannot be used with the Assistants API in v1. Follow the migration guide to upgrade to v2: https://platform.openai.com/docs/assistants/migration.", 'type': 'invalid_request_error', 'param': 'model', 'code': 'unsupported_model'}}` -需要手动将chatgpt更新到最新版,以及更改`create_assistant`中的结构体,详细参考[迁移模型](https://platform.openai.com/docs/assistants/migration)中的描述。建议直接在[平台](https://platform.openai.com/assistants/)上手动添加最新的assist然后复制代码到`assistant.json`中最为方便 +1. 需要手动将chatgpt更新到最新版, + +```shell +pip install --upgrade openai +``` + +2. 以及更改`create_assistant`中的结构体,详细参考[迁移模型](https://platform.openai.com/docs/assistants/migration)中的描述。建议直接在[平台](https://platform.openai.com/assistants/)上手动添加最新的assist然后复制代码到`assistant.json`中最为方便. ```json {"assistant_id": "asst_token"} ``` From 2506f2c1974d3c35790831bf340a59dd6fb7ef94 Mon Sep 17 00:00:00 2001 From: Frrrrrrrrank <82270228+Frrrrrrrrank@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:45:26 +0800 Subject: [PATCH 24/30] Update README.md --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 2bfb6f8..e54cafb 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,9 @@ 欢迎大家使用 ## 更加便于操作的付费版本 -另外,如果一些用户确实布置这个项目遇到很多问题,我提供一个收费的版本,加了美工服务器,跑的是我自己的api,一键无脑使用,网站上也有教学视频和使用手册 +目前该版本已经下架,您可以在微软商店中下载sider windows客户端,找到求职大师功能,每天可以免费使用三十次(目前已经推出);同时也将加入linkedin和Boss职位筛选功能(最晚在十月底前推出); -www.aijobpathfinder.com - - -差不多就是成本价+服务器还有美工的成本,就当请我喝一杯咖啡吧~当然各位要是技术ok,免费版本和付费版本是完全一样的,甚至这里还支持langchian ## 正文 这是一个完全免费的脚本,只需要你们自己配置好openai的api即可 From 1af396ec16f621f10d6b7abe422f68fc5a89d528 Mon Sep 17 00:00:00 2001 From: Loong Loong Date: Sun, 16 Mar 2025 18:49:16 +1100 Subject: [PATCH 25/30] Changed the structure --- auto_job_find/README => README | 0 auto_job_find/.env | 3 --- auto_job_find/test.py | 21 ------------------- .../finding_jobs.py => finding_jobs.py | 0 auto_job_find/functions.py => functions.py | 0 ...ain_functions.py => langchain_functions.py | 0 auto_job_find/prompts.py => prompts.py | 0 .../requirements.txt => requirements.txt | 2 +- ...6\346\224\276\350\277\231\351\207\214.txt" | 0 .../write_response.py => write_response.py | 0 10 files changed, 1 insertion(+), 25 deletions(-) rename auto_job_find/README => README (100%) delete mode 100644 auto_job_find/.env delete mode 100644 auto_job_find/test.py rename auto_job_find/finding_jobs.py => finding_jobs.py (100%) rename auto_job_find/functions.py => functions.py (100%) rename auto_job_find/langchain_functions.py => langchain_functions.py (100%) rename auto_job_find/prompts.py => prompts.py (100%) rename auto_job_find/requirements.txt => requirements.txt (90%) rename "auto_job_find/resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" => "resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" (100%) rename auto_job_find/write_response.py => write_response.py (100%) diff --git a/auto_job_find/README b/README similarity index 100% rename from auto_job_find/README rename to README diff --git a/auto_job_find/.env b/auto_job_find/.env deleted file mode 100644 index fc46129..0000000 --- a/auto_job_find/.env +++ /dev/null @@ -1,3 +0,0 @@ -# OPENAI_BASE_URL=https://api.openai.com/v1 - -# OPENAI_API_KEY='' diff --git a/auto_job_find/test.py b/auto_job_find/test.py deleted file mode 100644 index 6118426..0000000 --- a/auto_job_find/test.py +++ /dev/null @@ -1,21 +0,0 @@ -from selenium import webdriver -from selenium.webdriver.chrome.options import Options - -def open_browser_with_options(url, browser): - options = Options() - options.add_experimental_option("detach", True) - - if browser == "chrome": - driver = webdriver.Chrome(options=options) - driver.maximize_window() - else: - raise ValueError("Browser type not supported") - - driver.get(url) - -# Variables -url = "https://www.zhipin.com/web/geek/job-recommend?ka=header-job-recommend" -browser_type = "chrome" - -# Test case -open_browser_with_options(url, browser_type) diff --git a/auto_job_find/finding_jobs.py b/finding_jobs.py similarity index 100% rename from auto_job_find/finding_jobs.py rename to finding_jobs.py diff --git a/auto_job_find/functions.py b/functions.py similarity index 100% rename from auto_job_find/functions.py rename to functions.py diff --git a/auto_job_find/langchain_functions.py b/langchain_functions.py similarity index 100% rename from auto_job_find/langchain_functions.py rename to langchain_functions.py diff --git a/auto_job_find/prompts.py b/prompts.py similarity index 100% rename from auto_job_find/prompts.py rename to prompts.py diff --git a/auto_job_find/requirements.txt b/requirements.txt similarity index 90% rename from auto_job_find/requirements.txt rename to requirements.txt index 3f05a0e..6a18f01 100644 --- a/auto_job_find/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ langchain==0.0.354 -openai==1.6.1 +openai==1.66.3 packaging==23.2 python-dotenv==1.0.0 selenium==4.16.0 diff --git "a/auto_job_find/resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" "b/resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" similarity index 100% rename from "auto_job_find/resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" rename to "resume/\346\212\212\347\256\200\345\216\206\346\224\276\350\277\231\351\207\214.txt" diff --git a/auto_job_find/write_response.py b/write_response.py similarity index 100% rename from auto_job_find/write_response.py rename to write_response.py From af333f23a4530709cbae655801ba0d263266ef42 Mon Sep 17 00:00:00 2001 From: Loong Loong Date: Sun, 16 Mar 2025 18:51:02 +1100 Subject: [PATCH 26/30] Made it support OPEN AI ChatGPT-O3 model --- functions.py | 84 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/functions.py b/functions.py index ee73c72..b1af920 100644 --- a/functions.py +++ b/functions.py @@ -1,8 +1,10 @@ import json import os + from openai import OpenAI from prompts import assistant_instructions from dotenv import load_dotenv + load_dotenv() OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') @@ -11,40 +13,52 @@ # Init OpenAI Client client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL) + # Create or load assistant def create_assistant(client): - assistant_file_path = 'assistant.json' - - # If there is an assistant.json file already, then load that assistant - if os.path.exists(assistant_file_path): - with open(assistant_file_path, 'r') as file: - assistant_data = json.load(file) - assistant_id = assistant_data['assistant_id'] - print("Loaded existing assistant ID.") - else: - # If no assistant.json is present, create a new assistant using the below specifications - - # To change the knowledge document, modify the file name below to match your document - # If you want to add multiple files, paste this function into ChatGPT and ask for it to add support for multiple files - file = client.files.create(file=open("my_cover.pdf", "rb"), - purpose='assistants') - - assistant = client.beta.assistants.create( - # Getting assistant prompt from "prompts.py" file, edit on left panel if you want to change the prompt - instructions=assistant_instructions, - model="gpt-3.5-turbo-1106", - tools=[ - { - "type": "retrieval" # This adds the knowledge base as a tool - }, - ], - file_ids=[file.id]) - - # Create a new assistant.json file to load on future runs - with open(assistant_file_path, 'w') as file: - json.dump({'assistant_id': assistant.id}, file) - print("Created a new assistant and saved the ID.") - - assistant_id = assistant.id - - return assistant_id + assistant_file_path = 'assistant.json' + + # If there is an assistant.json file already, then load that assistant + if os.path.exists(assistant_file_path): + with open(assistant_file_path, 'r') as file: + assistant_data = json.load(file) + assistant_id = assistant_data['assistant_id'] + print("Loaded existing assistant ID.") + else: + # If no assistant.json is present, create a new assistant using the below specifications + + # To change the knowledge document, modify the file name below to match your document If you want to add + # multiple files, paste this function into ChatGPT and ask for it to add support for multiple files + # file = client.files.create(file=open("resume/my_cover.pdf", "rb"), + # purpose='assistants') + vector_store = client.vector_stores.create(name="My Resume") + print(vector_store.id) + file_streams = [open("resume/my_cover.pdf", "rb")] + # Use the upload and poll SDK helper to upload the files, add them to the vector store, + # and poll the status of the file batch for completion. + file_batch = client.vector_stores.file_batches.upload_and_poll( + vector_store_id=vector_store.id, files=file_streams + ) + + print(file_batch.status) + print(file_batch.file_counts) + # New version of the assistant + assistant = client.beta.assistants.create( + instructions=assistant_instructions, + model="gpt-4o", + tools=[{"type": "file_search"}], + ) + + assistant = client.beta.assistants.update( + assistant_id=assistant.id, + tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}}, + ) + + # Create a new assistant.json file to load on future runs + with open(assistant_file_path, 'w') as file: + json.dump({'assistant_id': assistant.id}, file) + print("Created a new assistant and saved the ID.") + + assistant_id = assistant.id + + return assistant_id From d9782d59c577d18f00cb91f94f86ce8d24ba8f0e Mon Sep 17 00:00:00 2001 From: Siz Long Date: Sun, 16 Mar 2025 19:05:53 +1100 Subject: [PATCH 27/30] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Btw, in my opinion this version is still runable, the description of "该项目已下架" may lead to unnecessary misleading --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e54cafb..0c32278 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## 更加便于操作的付费版本 -目前该版本已经下架,您可以在微软商店中下载sider windows客户端,找到求职大师功能,每天可以免费使用三十次(目前已经推出);同时也将加入linkedin和Boss职位筛选功能(最晚在十月底前推出); +您可以在微软商店中下载sider windows客户端,找到求职大师功能,每天可以免费使用三十次(目前已经推出);同时也将加入linkedin和Boss职位筛选功能(最晚在十月底前推出); ## 正文 From d99e8eafa899cce1382be3cc6857c140f04691ae Mon Sep 17 00:00:00 2001 From: Siz Long Date: Sun, 16 Mar 2025 19:53:38 +1100 Subject: [PATCH 28/30] Update README.md --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index 0c32278..bbc524c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,3 @@ -作者已经去打工了,现在在sider做产品经理,义务为我们的产品打一下广告,各位见谅 -[![banner](https://github.com/Frrrrrrrrank/auto_job__find__chatgpt__rpa/assets/82270228/0fff88f5-670c-4126-b6aa-3df1763fe757)](https://sider.ai/ad-land-redirect?source=github&p1=mi&p2=kk) - -欢迎大家使用 - -## 更加便于操作的付费版本 - -您可以在微软商店中下载sider windows客户端,找到求职大师功能,每天可以免费使用三十次(目前已经推出);同时也将加入linkedin和Boss职位筛选功能(最晚在十月底前推出); - - ## 正文 这是一个完全免费的脚本,只需要你们自己配置好openai的api即可 From d6a9cd29d6cbd7258657b8b6e61868ace545097a Mon Sep 17 00:00:00 2001 From: Loong Loong Date: Mon, 31 Mar 2025 01:08:33 +1100 Subject: [PATCH 29/30] NEW_FRONTEND --- .gitignore | 4 + README | 16 - TECH.md | 335 ++++++++++++++++++ electron/job_service.log | 13 + electron/package.json | 27 ++ electron/src/main.js | 200 +++++++++++ electron/src/renderer/app.js | 139 ++++++++ electron/src/renderer/index.html | 69 ++++ electron/src/renderer/styles.css | 150 ++++++++ langchain_functions.py | 89 ----- finding_jobs.py => python/finding_jobs.py | 0 functions.py => python/functions.py | 23 +- python/generate_grpc.py | 20 ++ python/grpc_server.py | 173 +++++++++ python/job_service_pb2.py | 42 +++ python/job_service_pb2_grpc.py | 172 +++++++++ prompts.py => python/prompts.py | 0 python/proto/job_service.proto | 81 +++++ python/proto/job_service_pb2.py | 46 +++ python/proto/job_service_pb2_grpc.py | 206 +++++++++++ requirements.txt => python/requirements.txt | 5 +- write_response.py => python/write_response.py | 55 +-- 22 files changed, 1732 insertions(+), 133 deletions(-) delete mode 100644 README create mode 100644 TECH.md create mode 100644 electron/job_service.log create mode 100644 electron/package.json create mode 100644 electron/src/main.js create mode 100644 electron/src/renderer/app.js create mode 100644 electron/src/renderer/index.html create mode 100644 electron/src/renderer/styles.css delete mode 100644 langchain_functions.py rename finding_jobs.py => python/finding_jobs.py (100%) rename functions.py => python/functions.py (79%) create mode 100644 python/generate_grpc.py create mode 100644 python/grpc_server.py create mode 100644 python/job_service_pb2.py create mode 100644 python/job_service_pb2_grpc.py rename prompts.py => python/prompts.py (100%) create mode 100644 python/proto/job_service.proto create mode 100644 python/proto/job_service_pb2.py create mode 100644 python/proto/job_service_pb2_grpc.py rename requirements.txt => python/requirements.txt (67%) rename write_response.py => python/write_response.py (79%) diff --git a/.gitignore b/.gitignore index 9959d04..8a1b181 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,8 @@ *.pyc .idea/ .vs/ +/.venv + +# grpc + diff --git a/README b/README deleted file mode 100644 index c150e1f..0000000 --- a/README +++ /dev/null @@ -1,16 +0,0 @@ -请首先配置好openai的api,随后将pdf简历上传到文件夹auto_job_find里,命名为“my_cover".随后执行write_response.py即可 -会自动生成openai的assistant,并在本地产生一个.json文件,只有第一次运行的时候才会产生,后面每次运行如果检测到这个json,就会调用已有的assistant - - -关于openai部分的包: -openai - -About RPA -tutorial video about how to learn rpa: https://www.youtube.com/watch?v=65OPFmEgCbM&list=PLx4LEkEdFArgrdD_lvXe_hYBy8zM0Sp3b&index=1 -Package of RPA -selenium -robotframework -robotframework-seleniumlibrary -robotframework-pythonlibcore - -Plugin: Intellibot@Selenium Library \ No newline at end of file diff --git a/TECH.md b/TECH.md new file mode 100644 index 0000000..2248dd1 --- /dev/null +++ b/TECH.md @@ -0,0 +1,335 @@ +# Boss直聘自动投递助手 - 技术文档 + +## 项目概述 + +这是一个基于 Electron 和 gRPC 的桌面应用,用于自动化 Boss 直聘的职位投递流程。项目采用前后端分离架构,使用 Python 处理后端逻辑,Electron 提供图形界面。 + +## 技术栈 + +- **前端**:Electron + HTML/CSS/JavaScript +- **后端**:Python + gRPC +- **构建工具**:electron-builder +- **包管理器**:npm/pnpm + +## 项目结构 + +```plaintext +BossZhiPin_Job_Search/ +├── electron/ # Electron 前端 +│ ├── src/ +│ │ ├── main.js # 主进程 +│ │ └── renderer/ # 渲染进程 +│ │ ├── index.html # 主界面 +│ │ ├── styles.css # 样式 +│ │ └── app.js # 渲染进程逻辑 +│ └── package.json # 项目配置 +└── python/ # Python 后端 + ├── grpc_server.py # gRPC 服务器 + └── write_response.py # 业务逻辑处理 +``` + +## 核心功能实现 + +### 1. 主进程 (main.js) + +主进程负责创建应用窗口、启动 Python gRPC 服务器,并处理与渲染进程的通信。 + +```javascript +// 创建应用窗口 +function createWindow() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); +} + +// 启动 Python gRPC 服务器 +function startPythonServer() { + pythonProcess = spawn('python', ['grpc_server.py']); + // 设置 gRPC 客户端 + setupGrpcClient(); +} + +// 处理 IPC 通信 +ipcMain.handle('start-job', async (event, data) => { + // 处理任务启动逻辑 +}); +``` + +### 2. 渲染进程 (app.js) + +渲染进程处理用户界面交互,包括文件上传、配置管理和任务控制。 + +```javascript +// 文件上传处理 +async function handleFileUpload(file) { + const buffer = await file.arrayBuffer(); + await ipcRenderer.invoke('upload-resume', new Uint8Array(buffer)); +} + +// 任务控制 +startJobBtn.addEventListener('click', async () => { + await ipcRenderer.invoke('start-job', { jobType, browserType }); +}); +``` + +### 3. gRPC 通信 + +使用 gRPC 实现前后端通信: + +```javascript +// 设置 gRPC 客户端 +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); + +// 加载 proto 文件 +const PROTO_PATH = path.join(__dirname, '../protos/job_service.proto'); +const packageDefinition = protoLoader.loadSync(PROTO_PATH); +``` + +## 用户界面设计 + +### 1. 布局结构 + +```html +
+

Boss直聘自动投递助手

+ + +
+

上传简历

+
+ +
+
+ + +
+

OpenAI 配置

+ +
+ + +
+

任务配置

+ +
+
+``` + +### 2. 响应式设计 + +```css +@media (max-width: 600px) { + .container { + padding: 10px; + } + + .section { + padding: 15px; + } + + button { + width: 100%; + margin-bottom: 10px; + } +} +``` + +## 通信流程 + +1. **用户界面交互**: + - 用户在渲染进程中操作界面 + - 触发事件通过 IPC 发送到主进程 + +2. **主进程处理**: + - 主进程接收 IPC 消息 + - 通过 gRPC 调用 Python 后端服务 + +3. **后端处理**: + - Python 后端处理业务逻辑 + - 返回结果通过 gRPC 发送回主进程 + +4. **结果展示**: + - 主进程通过 IPC 将结果发送到渲染进程 + - 渲染进程更新界面显示 + +## 依赖管理 + +主要依赖包: +```json +{ + "dependencies": { + "electron": "^29.4.6", + "@grpc/grpc-js": "^1.13.2", + "@grpc/proto-loader": "^0.7.13" + } +} +``` + +## 开发指南 + +### 1. 环境设置 + +1. 安装 Node.js 和 Python +2. 安装项目依赖: + ```bash + # 安装前端依赖 + cd electron + npm install + + # 安装后端依赖 + cd ../python + pip install -r requirements.txt + ``` + +### 2. 开发流程 + +1. **前端开发**: + - 修改 `src/renderer` 下的文件 + - 使用 `npm start` 启动应用 + +2. **后端开发**: + - 修改 Python 后端代码 + - 重启 gRPC 服务器 + +### 3. 调试技巧 + +1. **前端调试**: + - 使用 Chrome DevTools 调试渲染进程 + - 使用 `console.log` 输出调试信息 + +2. **后端调试**: + - 使用 Python 调试器 + - 查看 gRPC 服务器日志 + +## 注意事项 + +1. 确保 Python 环境正确配置 +2. 保持 gRPC 服务正常运行 +3. 注意文件路径的正确性 +4. 处理异步操作和错误情况 + +## 常见问题 + +1. **gRPC 连接失败**: + - 检查 Python 服务是否运行 + - 验证端口配置是否正确 + +2. **文件上传失败**: + - 检查文件大小限制 + - 验证文件格式 + +3. **界面无响应**: + - 检查 IPC 通信是否正常 + - 查看主进程日志 + +## 服务器启动和连接检查 + +1. **服务器启动**: + - 检查 Python 服务是否启动 + - 确认端口 50051 未被占用 + - 查看服务器输出日志 + +2. **连接测试**: + - 检查网络连接 + - 确认防火墙设置 + - 验证服务器地址和端口 + +3. **超时处理**: + - 设置合理的超时时间 + - 处理连接超时情况 + +4. **错误处理**: + - 捕获并处理连接错误 + - 提供用户友好的错误提示 + +## 启动流程 + +1. **用户启动**: + - 用户启动 Electron 应用 + - 主进程创建应用窗口 + - 主进程启动 Python gRPC 服务器 + +2. **等待服务器**: + - 等待服务器输出启动成功标志 + - 建立 gRPC 连接并测试 + +3. **连接成功**: + - 连接成功后通知渲染进程 + - 用户界面准备就绪,可以开始操作 + +## 错误处理 + +1. **服务器启动失败**: + - 检查 Python 环境 + - 确认端口 50051 未被占用 + - 查看服务器错误输出 + +2. **连接超时**: + - 检查网络连接 + - 确认防火墙设置 + - 验证服务器地址和端口 + +3. **文件上传失败**: + - 检查文件权限 + - 确认磁盘空间 + - 验证文件格式 + +4. **任务执行错误**: + - 捕获并处理任务执行错误 + - 提供用户友好的错误提示 + +## 依赖管理 + +主要依赖包: +```json +{ + "dependencies": { + "@grpc/grpc-js": "^1.9.13", + "@grpc/proto-loader": "^0.7.0" + }, + "devDependencies": { + "electron": "^28.1.0", + "electron-builder": "^24.9.1" + } +} +``` + +## 开发指南 + +1. **环境设置**: + - 安装 Node.js 和 Python + - 安装项目依赖 + - 配置 Python 环境 + +2. **开发流程**: + - 修改 proto 文件后需要重新生成代码 + - 启动 Python 服务器进行测试 + - 使用 `npm start` 启动 Electron 应用 + +3. **调试提示**: + - 查看主进程日志了解服务器状态 + - 使用 Chrome DevTools 调试渲染进程 + - 检查 Python 服务器输出排查问题 + +## 常见问题 + +1. **服务器启动失败**: + - 检查 Python 环境 + - 确认端口 50051 未被占用 + - 查看服务器错误输出 + +2. **连接超时**: + - 检查网络连接 + - 确认防火墙设置 + - 验证服务器地址和端口 + +3. **文件上传失败**: + - 检查文件权限 + - 确认磁盘空间 + - 验证文件格式 \ No newline at end of file diff --git a/electron/job_service.log b/electron/job_service.log new file mode 100644 index 0000000..0ee4d73 --- /dev/null +++ b/electron/job_service.log @@ -0,0 +1,13 @@ +2025-03-31 00:57:14,083 - INFO - Resume directory: C:\Users\longs\AppData\Local\BossZhiPin\BossZhiPin_Job_Search\resume +2025-03-31 00:57:14,083 - INFO - Created resume directory: C:\Users\longs\AppData\Local\BossZhiPin\BossZhiPin_Job_Search\resume +2025-03-31 00:57:17,102 - INFO - Saving resume to: C:\Users\longs\AppData\Local\BossZhiPin\BossZhiPin_Job_Search\resume\ +2025-03-31 00:57:17,102 - INFO - File content length: 349907 bytes +2025-03-31 00:57:17,103 - ERROR - Error saving resume: [Errno 2] No such file or directory: 'C:\\Users\\longs\\AppData\\Local\\BossZhiPin\\BossZhiPin_Job_Search\\resume\\' +2025-03-31 00:59:58,384 - INFO - Resume directory: C:\Users\longs\AppData\Local\BossZhiPin\BossZhiPin_Job_Search\resume +2025-03-31 00:59:58,386 - INFO - Created resume directory: C:\Users\longs\AppData\Local\BossZhiPin\BossZhiPin_Job_Search\resume +2025-03-31 01:00:49,274 - INFO - Empty filename received, using default: resume_1743343249.pdf +2025-03-31 01:00:49,274 - INFO - Saving resume to: C:\Users\longs\AppData\Local\BossZhiPin\BossZhiPin_Job_Search\resume\resume_1743343249.pdf +2025-03-31 01:00:49,274 - INFO - File content length: 349907 bytes +2025-03-31 01:00:49,276 - INFO - Resume saved successfully at: C:\Users\longs\AppData\Local\BossZhiPin\BossZhiPin_Job_Search\resume\resume_1743343249.pdf +2025-03-31 01:00:49,276 - INFO - File exists with size: 349907 bytes +2025-03-31 01:00:49,277 - INFO - Resume also saved to desktop: C:\Users\longs\Desktop\BossZhiPin_Resume_resume_1743343249.pdf diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 0000000..0d0f22a --- /dev/null +++ b/electron/package.json @@ -0,0 +1,27 @@ +{ + "name": "boss-zhipin-job-search", + "version": "1.0.0", + "description": "Boss直聘自动投递助手", + "main": "src/main.js", + "scripts": { + "start": "electron .", + "build": "electron-builder" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "electron": "^35.1.2", + "electron-builder": "^26.0.12" + }, + "dependencies": { + "@grpc/grpc-js": "^1.13.2", + "@grpc/proto-loader": "^0.7.13" + }, + "build": { + "appId": "com.boss.zhipin.job.search", + "win": { + "target": "nsis" + } + } +} diff --git a/electron/src/main.js b/electron/src/main.js new file mode 100644 index 0000000..71b8d90 --- /dev/null +++ b/electron/src/main.js @@ -0,0 +1,200 @@ +const { app, BrowserWindow, ipcMain } = require('electron'); +const path = require('path'); +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const { spawn, exec } = require('child_process'); +const util = require('util'); +const execPromise = util.promisify(exec); + +let mainWindow; +let pythonProcess = null; +let jobServiceClient = null; + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + mainWindow.loadFile(path.join(__dirname, 'renderer/index.html')); +} + +async function killProcessOnPort(port) { + try { + // 在 Windows 上查找占用端口的进程 + const { stdout } = await execPromise(`netstat -ano | findstr :${port}`); + if (stdout) { + // 提取 PID + const match = stdout.match(/\s+(\d+)$/); + if (match) { + const pid = match[1]; + console.log(`Found process ${pid} using port ${port}`); + // 终止进程 + await execPromise(`taskkill /F /PID ${pid}`); + console.log(`Successfully killed process ${pid}`); + } + } + } catch (error) { + // 如果端口没有被占用,netstat 会返回错误,这是正常的 + if (!error.message.includes('no connections')) { + console.error('Error checking port:', error); + } + } +} + +async function startPythonServer() { + try { + // 先检查并关闭占用 50051 端口的进程 + await killProcessOnPort(50051); + + // 等待一小段时间确保端口完全释放 + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 使用 python 命令启动服务器 + pythonProcess = spawn('python', ['../python/grpc_server.py'], { + stdio: ['inherit', 'pipe', 'pipe'] + }); + + if (!pythonProcess) { + throw new Error('Failed to start Python process'); + } + + if (pythonProcess.stdout) { + pythonProcess.stdout.on('data', (data) => { + console.log(`Python server: ${data}`); + if (data.toString().includes('gRPC server started')) { + setupGrpcClient(); + } + }); + } + + if (pythonProcess.stderr) { + pythonProcess.stderr.on('data', (data) => { + console.error(`Python server error: ${data}`); + }); + } + + pythonProcess.on('error', (error) => { + console.error('Failed to start Python process:', error); + }); + + } catch (error) { + console.error('Error in startPythonServer:', error); + throw error; + } +} + +function setupGrpcClient() { + const PROTO_PATH = path.join(__dirname, '../../python/proto/job_service.proto'); + const packageDefinition = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); + + const jobService = grpc.loadPackageDefinition(packageDefinition).job_service; + jobServiceClient = new jobService.JobService( + 'localhost:50051', + grpc.credentials.createInsecure() + ); + + // 测试连接 + jobServiceClient.TestConnection({}, (err, response) => { + if (err) { + console.error('Failed to connect to Python server:', err); + mainWindow.webContents.send('server-error', 'Failed to connect to Python server'); + } else { + console.log('Successfully connected to Python server'); + mainWindow.webContents.send('server-ready'); + } + }); +} + +// Initialize the application +async function initializeApp() { + try { + await startPythonServer(); + await setupGrpcClient(); + createWindow(); + } catch (error) { + console.error('Failed to initialize application:', error); + app.quit(); + } +} + +app.whenReady().then(initializeApp); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('before-quit', () => { + if (pythonProcess) { + pythonProcess.kill(); + } +}); + +// IPC 事件处理 +ipcMain.handle('upload-resume', async (event, fileContent, fileName) => { + if (!jobServiceClient) { + throw new Error('Job service client not initialized'); + } + + return new Promise((resolve, reject) => { + jobServiceClient.UploadResume({ file_content: fileContent, file_name: fileName }, (err, response) => { + if (err) reject(err); + else resolve(response); + }); + }); +}); + +ipcMain.handle('set-api-key', async (event, apiKey, baseUrl) => { + return new Promise((resolve, reject) => { + jobServiceClient.SetApiKey({ api_key: apiKey, base_url: baseUrl }, (err, response) => { + if (err) reject(err); + else resolve(response); + }); + }); +}); + +ipcMain.handle('start-job', async (event, jobConfig) => { + if (!jobServiceClient) { + throw new Error('gRPC client not initialized'); + } + + // 在这里才开始执行主要业务逻辑 + const stream = jobServiceClient.StartJob({ + url: jobConfig.url, + browser_type: jobConfig.browserType, + job_type: jobConfig.jobType + }); + + stream.on('data', (status) => { + mainWindow.webContents.send('job-status', status); + }); + + stream.on('error', (error) => { + mainWindow.webContents.send('job-error', error.message); + }); + + stream.on('end', () => { + mainWindow.webContents.send('job-completed'); + }); +}); + +ipcMain.handle('stop-job', async () => { + return new Promise((resolve, reject) => { + jobServiceClient.StopJob({}, (err, response) => { + if (err) reject(err); + else resolve(response); + }); + }); +}); \ No newline at end of file diff --git a/electron/src/renderer/app.js b/electron/src/renderer/app.js new file mode 100644 index 0000000..4a7357b --- /dev/null +++ b/electron/src/renderer/app.js @@ -0,0 +1,139 @@ +const { ipcRenderer } = require('electron'); + +// DOM 元素 +const dropZone = document.getElementById('dropZone'); +const fileInput = document.getElementById('fileInput'); +const uploadStatus = document.getElementById('uploadStatus'); +const apiKeyInput = document.getElementById('apiKey'); +const baseUrlInput = document.getElementById('baseUrl'); +const saveApiKeyBtn = document.getElementById('saveApiKey'); +const jobTypeInput = document.getElementById('jobType'); +const browserTypeSelect = document.getElementById('browserType'); +const startJobBtn = document.getElementById('startJob'); +const stopJobBtn = document.getElementById('stopJob'); +const statusArea = document.getElementById('status'); +const progressBar = document.getElementById('progress'); + +// 文件拖放处理 +dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.style.borderColor = '#2c3e50'; +}); + +dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.style.borderColor = '#ccc'; +}); + +dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.style.borderColor = '#ccc'; + + const file = e.dataTransfer.files[0]; + if (file && file.type === 'application/pdf') { + handleFileUpload(file); + } else { + uploadStatus.textContent = '请上传 PDF 格式的简历文件'; + } +}); + +dropZone.addEventListener('click', () => { + fileInput.click(); +}); + +fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + handleFileUpload(file); + } +}); + +async function handleFileUpload(file) { + uploadStatus.textContent = '正在上传简历...'; + try { + const buffer = await file.arrayBuffer(); + await ipcRenderer.invoke('upload-resume', new Uint8Array(buffer)); + uploadStatus.textContent = '简历上传成功!'; + } catch (error) { + uploadStatus.textContent = `上传失败: ${error.message}`; + } +} + +// API Key 配置 +saveApiKeyBtn.addEventListener('click', async () => { + const apiKey = apiKeyInput.value.trim(); + const baseUrl = baseUrlInput.value.trim(); + + if (!apiKey) { + alert('请输入 OpenAI API Key'); + return; + } + + try { + await ipcRenderer.invoke('set-api-key', { apiKey, baseUrl }); + alert('配置保存成功!'); + } catch (error) { + alert(`配置保存失败: ${error.message}`); + } +}); + +// 任务控制 +startJobBtn.addEventListener('click', async () => { + const jobType = jobTypeInput.value.trim(); + const browserType = browserTypeSelect.value; + + if (!jobType) { + alert('请输入职位类型'); + return; + } + + try { + startJobBtn.disabled = true; + stopJobBtn.disabled = false; + statusArea.textContent = '正在启动任务...'; + progressBar.style.width = '0%'; + + await ipcRenderer.invoke('start-job', { jobType, browserType }); + } catch (error) { + statusArea.textContent = `任务启动失败: ${error.message}`; + startJobBtn.disabled = false; + stopJobBtn.disabled = true; + } +}); + +stopJobBtn.addEventListener('click', async () => { + try { + await ipcRenderer.invoke('stop-job'); + statusArea.textContent = '任务已停止'; + startJobBtn.disabled = false; + stopJobBtn.disabled = true; + progressBar.style.width = '0%'; + } catch (error) { + alert(`停止任务失败: ${error.message}`); + } +}); + +// 监听任务状态更新 +ipcRenderer.on('job-status', (event, data) => { + statusArea.textContent = data.message; + if (data.progress !== undefined) { + progressBar.style.width = `${data.progress}%`; + } +}); + +ipcRenderer.on('job-error', (event, error) => { + statusArea.textContent = `错误: ${error}`; + startJobBtn.disabled = false; + stopJobBtn.disabled = true; + progressBar.style.width = '0%'; +}); + +ipcRenderer.on('job-complete', () => { + statusArea.textContent = '任务完成!'; + startJobBtn.disabled = false; + stopJobBtn.disabled = true; + progressBar.style.width = '100%'; +}); \ No newline at end of file diff --git a/electron/src/renderer/index.html b/electron/src/renderer/index.html new file mode 100644 index 0000000..9f5a748 --- /dev/null +++ b/electron/src/renderer/index.html @@ -0,0 +1,69 @@ + + + + + Boss直聘自动投递助手 + + + +
+

Boss直聘自动投递助手

+ +
+

上传简历

+
+

拖拽文件到此处或点击上传

+ +
+

+
+ +
+

OpenAI 配置

+
+ + +
+
+ + +
+ +
+ +
+

任务配置

+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+

任务状态

+
+ 等待任务开始... +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/electron/src/renderer/styles.css b/electron/src/renderer/styles.css new file mode 100644 index 0000000..0d31120 --- /dev/null +++ b/electron/src/renderer/styles.css @@ -0,0 +1,150 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + background-color: #f5f5f5; + color: #333; + line-height: 1.6; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +h1 { + text-align: center; + color: #2c3e50; + margin-bottom: 30px; +} + +.section { + background-color: white; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +h2 { + color: #2c3e50; + margin-bottom: 15px; + font-size: 1.5em; +} + +.upload-area { + border: 2px dashed #ccc; + border-radius: 4px; + padding: 20px; + text-align: center; + cursor: pointer; + transition: border-color 0.3s ease; +} + +.upload-area:hover { + border-color: #2c3e50; +} + +.form-group { + margin-bottom: 15px; +} + +label { + display: block; + margin-bottom: 5px; + color: #666; +} + +input[type="text"], +input[type="password"], +select { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +button { + background-color: #3498db; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #2980b9; +} + +button:disabled { + background-color: #bdc3c7; + cursor: not-allowed; +} + +.status-area { + background-color: #f8f9fa; + border-radius: 4px; + padding: 15px; + margin-bottom: 15px; + min-height: 100px; + max-height: 200px; + overflow-y: auto; +} + +.progress-container { + width: 100%; + height: 20px; + background-color: #f0f0f0; + border-radius: 10px; + overflow: hidden; +} + +.progress-bar { + width: 0%; + height: 100%; + background-color: #2ecc71; + transition: width 0.3s ease; +} + +#uploadStatus { + margin-top: 10px; + color: #666; +} + +#stopJob { + background-color: #e74c3c; + margin-left: 10px; +} + +#stopJob:hover { + background-color: #c0392b; +} + +/* 响应式设计 */ +@media (max-width: 600px) { + .container { + padding: 10px; + } + + .section { + padding: 15px; + } + + button { + width: 100%; + margin-bottom: 10px; + } + + #stopJob { + margin-left: 0; + } +} \ No newline at end of file diff --git a/langchain_functions.py b/langchain_functions.py deleted file mode 100644 index 5e65fe9..0000000 --- a/langchain_functions.py +++ /dev/null @@ -1,89 +0,0 @@ -from langchain.document_loaders import DirectoryLoader, PyPDFLoader -from langchain.text_splitter import CharacterTextSplitter -from langchain.embeddings import OpenAIEmbeddings -from langchain.vectorstores import FAISS -from langchain.chains import RetrievalQA -from langchain.chat_models import ChatOpenAI -from langchain.prompts import PromptTemplate -import os -from dotenv import load_dotenv -load_dotenv() - -OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') -OPENAI_BASE_URL = os.getenv('OPENAI_BASE_URL') - -# 判断是否使用langchain。如果设置了自定义的api地址,则使用langchain -def should_use_langchain(): - should_use_langchain = OPENAI_BASE_URL is not None - return should_use_langchain - -# 读取简历数据 -def read_resumes(): - # 读取resume文件夹中的所有文件 - d_loader = DirectoryLoader("./resume", glob="*.pdf",loader_cls=PyPDFLoader) - - # 获取 PDF 文本,返回一个列表,列表中的每个元素都是一个 PDF 文档的页 - pdf_pages = d_loader.load() - - resume_text = "" - - for page in pdf_pages: - # 建立路径,用于区分是哪一份简历 - # print(page.metadata.get('source')) - - page_text = page.page_content - resume_text += page_text - - return resume_text - -# 文本分割 -def get_text_chunks(text): - text_splitter = CharacterTextSplitter( - separator="\n", - chunk_size=2000, - chunk_overlap=200, - length_function=len - ) - chunks = text_splitter.split_text(text) - return chunks - -# 向量化,并且返回向量存储库 -def get_vectorstore(text_chunks): - embeddings = OpenAIEmbeddings() - # embeddings = HuggingFaceInstructEmbeddings(model_name="hkunlp/instructor-xl") - vectorstore = FAISS.from_texts(texts=text_chunks, embedding=embeddings) - return vectorstore - -# 生成求职信 -def generate_letter(vectorstore, job_description): - # 字数限制 - character_limit = 300 - - langchain_prompt_template = f""" - 你将扮演一位求职者的角色,根据上下文里的简历内容以及应聘工作的描述,来直接给HR写一个礼貌专业, 且字数严格限制在{character_limit}以内的求职消息,要求能够用专业的语言结合简历中的经历和技能,并结合应聘工作的描述,来阐述自己的优势,尽最大可能打动招聘者。始终使用中文来进行消息的编写。开头是招聘负责人, 结尾附上求职者联系方式。这是一份求职消息,不要包含求职内容以外的东西,例如“根据您上传的求职要求和个人简历,我来帮您起草一封求职邮件:”这一类的内容,以便于我直接自动化复制粘贴发送。 - 工作描述 - {job_description}"""+""" - 简历内容: - {context} - 要求: - {question} - """ - - question = "根据工作描述,寻找出简历里最合适的技能都有哪些?求职者的优势是什么?" - - PROMPT = PromptTemplate.from_template(langchain_prompt_template) - llm = ChatOpenAI(temperature=3, openai_api_base=OPENAI_BASE_URL, openai_api_key=OPENAI_API_KEY) - qa_chain = RetrievalQA.from_chain_type( - llm, - retriever=vectorstore.as_retriever(), - # return_source_documents=True, - chain_type_kwargs={"prompt": PROMPT} - ) - - result = qa_chain({"query": question}) - letter = result['result'] - - #去掉所有换行符,防止分成多段消息 - letter = letter.replace('\n', ' ') - - return letter diff --git a/finding_jobs.py b/python/finding_jobs.py similarity index 100% rename from finding_jobs.py rename to python/finding_jobs.py diff --git a/functions.py b/python/functions.py similarity index 79% rename from functions.py rename to python/functions.py index b1af920..52272c4 100644 --- a/functions.py +++ b/python/functions.py @@ -10,9 +10,28 @@ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') OPENAI_BASE_URL = os.getenv('OPENAI_BASE_URL') -# Init OpenAI Client -client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL) +class OpenAIClient: + _instance = None + + def __init__(self): + self.client = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def initialize(self, api_key, base_url=None): + self.client = OpenAI(api_key=api_key, base_url=base_url) + + def get_client(self): + if self.client is None: + raise ValueError("OpenAI client not initialized. Please set API key first.") + return self.client +# 使用单例模式 +openai_client = OpenAIClient.get_instance() # Create or load assistant def create_assistant(client): diff --git a/python/generate_grpc.py b/python/generate_grpc.py new file mode 100644 index 0000000..85d44c8 --- /dev/null +++ b/python/generate_grpc.py @@ -0,0 +1,20 @@ +import os +import subprocess + +def generate_grpc_code(): + # 确保 proto 目录存在 + os.makedirs('proto', exist_ok=True) + + # 生成 Python 代码 + subprocess.run([ + 'python', '-m', 'grpc_tools.protoc', + '-I./proto', + '--python_out=.', + '--grpc_python_out=.', + './proto/job_service.proto' + ]) + + print("gRPC 代码生成完成") + +if __name__ == '__main__': + generate_grpc_code() \ No newline at end of file diff --git a/python/grpc_server.py b/python/grpc_server.py new file mode 100644 index 0000000..a0e1605 --- /dev/null +++ b/python/grpc_server.py @@ -0,0 +1,173 @@ +import grpc +from concurrent import futures +import os +import time +from typing import Iterator +import job_service_pb2 +import job_service_pb2_grpc +from write_response import send_job_descriptions_to_chat +import threading +import queue +import appdirs +import logging + +# 配置日志 +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[logging.FileHandler("job_service.log"), + logging.StreamHandler()]) + +class JobServiceServicer(job_service_pb2_grpc.JobServiceServicer): + def __init__(self): + self.api_key = None + self.base_url = None + self.resume_path = None + self.is_running = False + # 创建应用数据目录 + self.app_data_dir = appdirs.user_data_dir('BossZhiPin_Job_Search', 'BossZhiPin') + self.resume_dir = os.path.join(self.app_data_dir, 'resume') + logging.info(f"Resume directory: {self.resume_dir}") + + try: + os.makedirs(self.resume_dir, exist_ok=True) + logging.info(f"Created resume directory: {self.resume_dir}") + except Exception as e: + logging.error(f"Error creating resume directory: {e}") + + def TestConnection(self, request, context): + """测试与服务器的连接""" + return job_service_pb2.TestConnectionResponse( + success=True, + message="服务器连接正常" + ) + + def UploadResume(self, request, context): + try: + # 检查文件名是否为空,如果为空则使用默认文件名 + file_name = request.file_name + if not file_name or file_name.strip() == '': + file_name = f"resume_{int(time.time())}.pdf" + logging.info(f"Empty filename received, using default: {file_name}") + + # 保存简历文件到应用数据目录 + file_path = os.path.join(self.resume_dir, file_name) + logging.info(f"Saving resume to: {file_path}") + logging.info(f"File content length: {len(request.file_content)} bytes") + + # 确保目录存在 + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + # 保存文件 + with open(file_path, 'wb') as f: + f.write(request.file_content) + + logging.info(f"Resume saved successfully at: {file_path}") + + # 检查文件是否成功保存 + if os.path.exists(file_path): + file_size = os.path.getsize(file_path) + logging.info(f"File exists with size: {file_size} bytes") + else: + logging.error(f"File does not exist after save: {file_path}") + + self.resume_path = file_path + + # 尝试保存到额外的位置(桌面),确保用户可以找到 + try: + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + desktop_resume_path = os.path.join(desktop_path, f"BossZhiPin_Resume_{file_name}") + with open(desktop_resume_path, 'wb') as f: + f.write(request.file_content) + logging.info(f"Resume also saved to desktop: {desktop_resume_path}") + + return job_service_pb2.UploadResumeResponse( + success=True, + message=f"简历已保存到: {file_path} 和 {desktop_resume_path}" + ) + except Exception as e: + logging.error(f"Error saving resume to desktop: {e}") + + return job_service_pb2.UploadResumeResponse( + success=True, + message=f"简历已保存到: {file_path}" + ) + except Exception as e: + logging.error(f"Error saving resume: {e}") + return job_service_pb2.UploadResumeResponse(success=False, message=str(e)) + + def SetApiKey(self, request, context): + try: + self.api_key = request.api_key + self.base_url = request.base_url + return job_service_pb2.SetApiKeyResponse(success=True) + except Exception as e: + return job_service_pb2.SetApiKeyResponse(success=False, message=str(e)) + + def StartJob(self, request, context): + if not all([self.api_key, self.resume_path]): + context.abort(grpc.StatusCode.FAILED_PRECONDITION, + 'API key and resume must be set before starting job') + + try: + # 只在这里初始化 OpenAI 客户端 + os.environ['OPENAI_API_KEY'] = self.api_key + if self.base_url: + os.environ['OPENAI_BASE_URL'] = self.base_url + + # 开始执行主要业务逻辑 + yield job_service_pb2.JobStatus( + status="running", + message="Starting job processing..." + ) + + # 调用 write_response 的主要逻辑 + from write_response import process_job + for status in process_job( + url=request.url, + browser_type=request.browser_type, + job_type=request.job_type, + resume_path=self.resume_path + ): + yield status + + except Exception as e: + yield job_service_pb2.JobStatus( + status="error", + message=str(e) + ) + + def StopJob(self, request, context): + if not self.is_running: + return job_service_pb2.StopJobResponse( + success=False, + message="没有正在运行的任务", + final_status="stopped" + ) + + try: + self.is_running = False + if self.current_thread: + self.current_thread.join(timeout=5) + + return job_service_pb2.StopJobResponse( + success=True, + message="任务已停止", + final_status="stopped" + ) + except Exception as e: + return job_service_pb2.StopJobResponse( + success=False, + message=f"停止任务失败: {str(e)}", + final_status="error" + ) + +def serve(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + job_service_pb2_grpc.add_JobServiceServicer_to_server(JobServiceServicer(), server) + server.add_insecure_port('[::]:50051') + server.start() + print("gRPC server started") # 添加启动成功的标志 + server.wait_for_termination() + +if __name__ == '__main__': + serve() \ No newline at end of file diff --git a/python/job_service_pb2.py b/python/job_service_pb2.py new file mode 100644 index 0000000..858b425 --- /dev/null +++ b/python/job_service_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: job_service.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11job_service.proto\x12\x0bjob_service\">\n\x13UploadResumeRequest\x12\x14\n\x0c\x66ile_content\x18\x01 \x01(\x0c\x12\x11\n\tfile_name\x18\x02 \x01(\t\"K\n\x14UploadResumeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\tfile_path\x18\x03 \x01(\t\"5\n\x10SetApiKeyRequest\x12\x0f\n\x07\x61pi_key\x18\x01 \x01(\t\x12\x10\n\x08\x62\x61se_url\x18\x02 \x01(\t\"5\n\x11SetApiKeyResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"F\n\x0fStartJobRequest\x12\x10\n\x08job_type\x18\x01 \x01(\t\x12\x14\n\x0c\x62rowser_type\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\"m\n\tJobStatus\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x10\n\x08progress\x18\x03 \x01(\x05\x12\x13\n\x0b\x63urrent_job\x18\x04 \x01(\t\x12\x18\n\x10\x63urrent_response\x18\x05 \x01(\t\"\x10\n\x0eStopJobRequest\"I\n\x0fStopJobResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x14\n\x0c\x66inal_status\x18\x03 \x01(\t2\xbf\x02\n\nJobService\x12U\n\x0cUploadResume\x12 .job_service.UploadResumeRequest\x1a!.job_service.UploadResumeResponse\"\x00\x12L\n\tSetApiKey\x12\x1d.job_service.SetApiKeyRequest\x1a\x1e.job_service.SetApiKeyResponse\"\x00\x12\x44\n\x08StartJob\x12\x1c.job_service.StartJobRequest\x1a\x16.job_service.JobStatus\"\x00\x30\x01\x12\x46\n\x07StopJob\x12\x1b.job_service.StopJobRequest\x1a\x1c.job_service.StopJobResponse\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'job_service_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_UPLOADRESUMEREQUEST']._serialized_start=34 + _globals['_UPLOADRESUMEREQUEST']._serialized_end=96 + _globals['_UPLOADRESUMERESPONSE']._serialized_start=98 + _globals['_UPLOADRESUMERESPONSE']._serialized_end=173 + _globals['_SETAPIKEYREQUEST']._serialized_start=175 + _globals['_SETAPIKEYREQUEST']._serialized_end=228 + _globals['_SETAPIKEYRESPONSE']._serialized_start=230 + _globals['_SETAPIKEYRESPONSE']._serialized_end=283 + _globals['_STARTJOBREQUEST']._serialized_start=285 + _globals['_STARTJOBREQUEST']._serialized_end=355 + _globals['_JOBSTATUS']._serialized_start=357 + _globals['_JOBSTATUS']._serialized_end=466 + _globals['_STOPJOBREQUEST']._serialized_start=468 + _globals['_STOPJOBREQUEST']._serialized_end=484 + _globals['_STOPJOBRESPONSE']._serialized_start=486 + _globals['_STOPJOBRESPONSE']._serialized_end=559 + _globals['_JOBSERVICE']._serialized_start=562 + _globals['_JOBSERVICE']._serialized_end=881 +# @@protoc_insertion_point(module_scope) diff --git a/python/job_service_pb2_grpc.py b/python/job_service_pb2_grpc.py new file mode 100644 index 0000000..812ebe8 --- /dev/null +++ b/python/job_service_pb2_grpc.py @@ -0,0 +1,172 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import job_service_pb2 as job__service__pb2 + + +class JobServiceStub(object): + """定义服务 + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.UploadResume = channel.unary_unary( + '/job_service.JobService/UploadResume', + request_serializer=job__service__pb2.UploadResumeRequest.SerializeToString, + response_deserializer=job__service__pb2.UploadResumeResponse.FromString, + ) + self.SetApiKey = channel.unary_unary( + '/job_service.JobService/SetApiKey', + request_serializer=job__service__pb2.SetApiKeyRequest.SerializeToString, + response_deserializer=job__service__pb2.SetApiKeyResponse.FromString, + ) + self.StartJob = channel.unary_stream( + '/job_service.JobService/StartJob', + request_serializer=job__service__pb2.StartJobRequest.SerializeToString, + response_deserializer=job__service__pb2.JobStatus.FromString, + ) + self.StopJob = channel.unary_unary( + '/job_service.JobService/StopJob', + request_serializer=job__service__pb2.StopJobRequest.SerializeToString, + response_deserializer=job__service__pb2.StopJobResponse.FromString, + ) + + +class JobServiceServicer(object): + """定义服务 + """ + + def UploadResume(self, request, context): + """文件上传 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SetApiKey(self, request, context): + """设置 API key + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def StartJob(self, request, context): + """开始任务 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def StopJob(self, request, context): + """停止任务 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_JobServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'UploadResume': grpc.unary_unary_rpc_method_handler( + servicer.UploadResume, + request_deserializer=job__service__pb2.UploadResumeRequest.FromString, + response_serializer=job__service__pb2.UploadResumeResponse.SerializeToString, + ), + 'SetApiKey': grpc.unary_unary_rpc_method_handler( + servicer.SetApiKey, + request_deserializer=job__service__pb2.SetApiKeyRequest.FromString, + response_serializer=job__service__pb2.SetApiKeyResponse.SerializeToString, + ), + 'StartJob': grpc.unary_stream_rpc_method_handler( + servicer.StartJob, + request_deserializer=job__service__pb2.StartJobRequest.FromString, + response_serializer=job__service__pb2.JobStatus.SerializeToString, + ), + 'StopJob': grpc.unary_unary_rpc_method_handler( + servicer.StopJob, + request_deserializer=job__service__pb2.StopJobRequest.FromString, + response_serializer=job__service__pb2.StopJobResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'job_service.JobService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class JobService(object): + """定义服务 + """ + + @staticmethod + def UploadResume(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/job_service.JobService/UploadResume', + job__service__pb2.UploadResumeRequest.SerializeToString, + job__service__pb2.UploadResumeResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SetApiKey(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/job_service.JobService/SetApiKey', + job__service__pb2.SetApiKeyRequest.SerializeToString, + job__service__pb2.SetApiKeyResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def StartJob(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/job_service.JobService/StartJob', + job__service__pb2.StartJobRequest.SerializeToString, + job__service__pb2.JobStatus.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def StopJob(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/job_service.JobService/StopJob', + job__service__pb2.StopJobRequest.SerializeToString, + job__service__pb2.StopJobResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/prompts.py b/python/prompts.py similarity index 100% rename from prompts.py rename to python/prompts.py diff --git a/python/proto/job_service.proto b/python/proto/job_service.proto new file mode 100644 index 0000000..ed248bc --- /dev/null +++ b/python/proto/job_service.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package job_service; + +// 定义服务 +service JobService { + // 测试连接 + rpc TestConnection (TestConnectionRequest) returns (TestConnectionResponse) {} + + // 文件上传 + rpc UploadResume (UploadResumeRequest) returns (UploadResumeResponse) {} + + // 设置 API key + rpc SetApiKey (SetApiKeyRequest) returns (SetApiKeyResponse) {} + + // 开始任务 + rpc StartJob (StartJobRequest) returns (stream JobStatus) {} + + // 停止任务 + rpc StopJob (StopJobRequest) returns (StopJobResponse) {} +} + +// 测试连接请求 +message TestConnectionRequest {} + +// 测试连接响应 +message TestConnectionResponse { + bool success = 1; + string message = 2; +} + +// 文件上传请求 +message UploadResumeRequest { + bytes file_content = 1; + string file_name = 2; +} + +// 文件上传响应 +message UploadResumeResponse { + bool success = 1; + string message = 2; + string file_path = 3; // 保存的文件路径 +} + +// 设置 API key 请求 +message SetApiKeyRequest { + string api_key = 1; + string base_url = 2; // 可选的 OpenAI base URL +} + +// 设置 API key 响应 +message SetApiKeyResponse { + bool success = 1; + string message = 2; +} + +// 开始任务请求 +message StartJobRequest { + string job_type = 1; // 例如:"iOS(深圳)" + string browser_type = 2; // 例如:"chrome" + string url = 3; // 目标网站 URL +} + +// 任务状态(流式响应) +message JobStatus { + string status = 1; // "running", "completed", "error" + string message = 2; + int32 progress = 3; // 0-100 + string current_job = 4; // 当前正在处理的工作描述 + string current_response = 5; // 当前生成的响应 +} + +// 停止任务请求 +message StopJobRequest {} + +// 停止任务响应 +message StopJobResponse { + bool success = 1; + string message = 2; + string final_status = 3; // 任务停止时的最终状态 +} \ No newline at end of file diff --git a/python/proto/job_service_pb2.py b/python/proto/job_service_pb2.py new file mode 100644 index 0000000..5e49087 --- /dev/null +++ b/python/proto/job_service_pb2.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: job_service.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11job_service.proto\x12\x0bjob_service\"\x17\n\x15TestConnectionRequest\":\n\x16TestConnectionResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\">\n\x13UploadResumeRequest\x12\x14\n\x0c\x66ile_content\x18\x01 \x01(\x0c\x12\x11\n\tfile_name\x18\x02 \x01(\t\"K\n\x14UploadResumeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\tfile_path\x18\x03 \x01(\t\"5\n\x10SetApiKeyRequest\x12\x0f\n\x07\x61pi_key\x18\x01 \x01(\t\x12\x10\n\x08\x62\x61se_url\x18\x02 \x01(\t\"5\n\x11SetApiKeyResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"F\n\x0fStartJobRequest\x12\x10\n\x08job_type\x18\x01 \x01(\t\x12\x14\n\x0c\x62rowser_type\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\"m\n\tJobStatus\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x10\n\x08progress\x18\x03 \x01(\x05\x12\x13\n\x0b\x63urrent_job\x18\x04 \x01(\t\x12\x18\n\x10\x63urrent_response\x18\x05 \x01(\t\"\x10\n\x0eStopJobRequest\"I\n\x0fStopJobResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x14\n\x0c\x66inal_status\x18\x03 \x01(\t2\x9c\x03\n\nJobService\x12[\n\x0eTestConnection\x12\".job_service.TestConnectionRequest\x1a#.job_service.TestConnectionResponse\"\x00\x12U\n\x0cUploadResume\x12 .job_service.UploadResumeRequest\x1a!.job_service.UploadResumeResponse\"\x00\x12L\n\tSetApiKey\x12\x1d.job_service.SetApiKeyRequest\x1a\x1e.job_service.SetApiKeyResponse\"\x00\x12\x44\n\x08StartJob\x12\x1c.job_service.StartJobRequest\x1a\x16.job_service.JobStatus\"\x00\x30\x01\x12\x46\n\x07StopJob\x12\x1b.job_service.StopJobRequest\x1a\x1c.job_service.StopJobResponse\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'job_service_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_TESTCONNECTIONREQUEST']._serialized_start=34 + _globals['_TESTCONNECTIONREQUEST']._serialized_end=57 + _globals['_TESTCONNECTIONRESPONSE']._serialized_start=59 + _globals['_TESTCONNECTIONRESPONSE']._serialized_end=117 + _globals['_UPLOADRESUMEREQUEST']._serialized_start=119 + _globals['_UPLOADRESUMEREQUEST']._serialized_end=181 + _globals['_UPLOADRESUMERESPONSE']._serialized_start=183 + _globals['_UPLOADRESUMERESPONSE']._serialized_end=258 + _globals['_SETAPIKEYREQUEST']._serialized_start=260 + _globals['_SETAPIKEYREQUEST']._serialized_end=313 + _globals['_SETAPIKEYRESPONSE']._serialized_start=315 + _globals['_SETAPIKEYRESPONSE']._serialized_end=368 + _globals['_STARTJOBREQUEST']._serialized_start=370 + _globals['_STARTJOBREQUEST']._serialized_end=440 + _globals['_JOBSTATUS']._serialized_start=442 + _globals['_JOBSTATUS']._serialized_end=551 + _globals['_STOPJOBREQUEST']._serialized_start=553 + _globals['_STOPJOBREQUEST']._serialized_end=569 + _globals['_STOPJOBRESPONSE']._serialized_start=571 + _globals['_STOPJOBRESPONSE']._serialized_end=644 + _globals['_JOBSERVICE']._serialized_start=647 + _globals['_JOBSERVICE']._serialized_end=1059 +# @@protoc_insertion_point(module_scope) diff --git a/python/proto/job_service_pb2_grpc.py b/python/proto/job_service_pb2_grpc.py new file mode 100644 index 0000000..0cb9ecd --- /dev/null +++ b/python/proto/job_service_pb2_grpc.py @@ -0,0 +1,206 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import job_service_pb2 as job__service__pb2 + + +class JobServiceStub(object): + """定义服务 + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.TestConnection = channel.unary_unary( + '/job_service.JobService/TestConnection', + request_serializer=job__service__pb2.TestConnectionRequest.SerializeToString, + response_deserializer=job__service__pb2.TestConnectionResponse.FromString, + ) + self.UploadResume = channel.unary_unary( + '/job_service.JobService/UploadResume', + request_serializer=job__service__pb2.UploadResumeRequest.SerializeToString, + response_deserializer=job__service__pb2.UploadResumeResponse.FromString, + ) + self.SetApiKey = channel.unary_unary( + '/job_service.JobService/SetApiKey', + request_serializer=job__service__pb2.SetApiKeyRequest.SerializeToString, + response_deserializer=job__service__pb2.SetApiKeyResponse.FromString, + ) + self.StartJob = channel.unary_stream( + '/job_service.JobService/StartJob', + request_serializer=job__service__pb2.StartJobRequest.SerializeToString, + response_deserializer=job__service__pb2.JobStatus.FromString, + ) + self.StopJob = channel.unary_unary( + '/job_service.JobService/StopJob', + request_serializer=job__service__pb2.StopJobRequest.SerializeToString, + response_deserializer=job__service__pb2.StopJobResponse.FromString, + ) + + +class JobServiceServicer(object): + """定义服务 + """ + + def TestConnection(self, request, context): + """测试连接 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UploadResume(self, request, context): + """文件上传 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SetApiKey(self, request, context): + """设置 API key + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def StartJob(self, request, context): + """开始任务 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def StopJob(self, request, context): + """停止任务 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_JobServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'TestConnection': grpc.unary_unary_rpc_method_handler( + servicer.TestConnection, + request_deserializer=job__service__pb2.TestConnectionRequest.FromString, + response_serializer=job__service__pb2.TestConnectionResponse.SerializeToString, + ), + 'UploadResume': grpc.unary_unary_rpc_method_handler( + servicer.UploadResume, + request_deserializer=job__service__pb2.UploadResumeRequest.FromString, + response_serializer=job__service__pb2.UploadResumeResponse.SerializeToString, + ), + 'SetApiKey': grpc.unary_unary_rpc_method_handler( + servicer.SetApiKey, + request_deserializer=job__service__pb2.SetApiKeyRequest.FromString, + response_serializer=job__service__pb2.SetApiKeyResponse.SerializeToString, + ), + 'StartJob': grpc.unary_stream_rpc_method_handler( + servicer.StartJob, + request_deserializer=job__service__pb2.StartJobRequest.FromString, + response_serializer=job__service__pb2.JobStatus.SerializeToString, + ), + 'StopJob': grpc.unary_unary_rpc_method_handler( + servicer.StopJob, + request_deserializer=job__service__pb2.StopJobRequest.FromString, + response_serializer=job__service__pb2.StopJobResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'job_service.JobService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class JobService(object): + """定义服务 + """ + + @staticmethod + def TestConnection(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/job_service.JobService/TestConnection', + job__service__pb2.TestConnectionRequest.SerializeToString, + job__service__pb2.TestConnectionResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def UploadResume(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/job_service.JobService/UploadResume', + job__service__pb2.UploadResumeRequest.SerializeToString, + job__service__pb2.UploadResumeResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SetApiKey(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/job_service.JobService/SetApiKey', + job__service__pb2.SetApiKeyRequest.SerializeToString, + job__service__pb2.SetApiKeyResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def StartJob(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/job_service.JobService/StartJob', + job__service__pb2.StartJobRequest.SerializeToString, + job__service__pb2.JobStatus.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def StopJob(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/job_service.JobService/StopJob', + job__service__pb2.StopJobRequest.SerializeToString, + job__service__pb2.StopJobResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/requirements.txt b/python/requirements.txt similarity index 67% rename from requirements.txt rename to python/requirements.txt index 6a18f01..5c4b8f6 100644 --- a/requirements.txt +++ b/python/requirements.txt @@ -1,4 +1,3 @@ -langchain==0.0.354 openai==1.66.3 packaging==23.2 python-dotenv==1.0.0 @@ -6,3 +5,7 @@ selenium==4.16.0 robotframework robotframework-seleniumlibrary robotframework-pythonlibcore +grpcio==1.62.0 +grpcio-tools==1.62.0 +protobuf==4.25.3 +appdirs==1.4.4 \ No newline at end of file diff --git a/write_response.py b/python/write_response.py similarity index 79% rename from write_response.py rename to python/write_response.py index 2dfdc1e..73c1d3b 100644 --- a/write_response.py +++ b/python/write_response.py @@ -8,7 +8,6 @@ from selenium.webdriver import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait -from langchain_functions import get_text_chunks, get_vectorstore, read_resumes, should_use_langchain, generate_letter import functions import finding_jobs @@ -29,13 +28,26 @@ else: print("OpenAI version is compatible.") -# Initialize OpenAI client -client = OpenAI(api_key=OPENAI_API_KEY) +class OpenAIClientManager: + _instance = None + _client = None -if not should_use_langchain(): - # Create or load assistant - assistant_id = functions.create_assistant( - client) # this function comes from "functions.py" + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def initialize(self, api_key, base_url=None): + self._client = OpenAI(api_key=api_key, base_url=base_url) + + def get_client(self): + if self._client is None: + raise ValueError("OpenAI client not initialized. Please set API key first.") + return self._client + +# 全局单例 +openai_manager = OpenAIClientManager.get_instance() def create_thread(client): @@ -51,7 +63,7 @@ def create_thread(client): def chat(user_input, assistant_id, thread_id=None): if thread_id is None: - thread_id = create_thread(client) + thread_id = create_thread(openai_manager.get_client()) if thread_id is None: return json.dumps({"error": "Failed to create a new thread"}) @@ -60,21 +72,21 @@ def chat(user_input, assistant_id, thread_id=None): # Run the Assistant try: # Add the user's message to the thread - client.beta.threads.messages.create( + openai_manager.get_client().beta.threads.messages.create( thread_id=thread_id, role="user", content=user_input ) # Start the Assistant Run - run = client.beta.threads.runs.create( + run = openai_manager.get_client().beta.threads.runs.create( thread_id=thread_id, assistant_id=assistant_id ) # Check if the Run requires action (function call) while True: - run_status = client.beta.threads.runs.retrieve( + run_status = openai_manager.get_client().beta.threads.runs.retrieve( thread_id=thread_id, run_id=run.id, timeout=60 # 设置超时时间为60秒 @@ -88,7 +100,7 @@ def chat(user_input, assistant_id, thread_id=None): time.sleep(1) # Wait for a second before checking again # Retrieve and return the latest message from the assistant - messages = client.beta.threads.messages.list(thread_id=thread_id) + messages = openai_manager.get_client().beta.threads.messages.list(thread_id=thread_id) assistant_message = messages.data[0].content[0].text.value # 将换行符替换为一个空格 @@ -132,7 +144,9 @@ def send_response_and_go_back(driver, response): time.sleep(3) -def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, vectorstore=None): +def send_job_descriptions_to_chat(url, browser_type, label): + # 这个函数会在用户点击"开始任务"后被调用 + client = openai_manager.get_client() # 获取已初始化的客户端 # 开始浏览并获取工作描述 finding_jobs.open_browser_with_options(url, browser_type) finding_jobs.log_in() @@ -152,10 +166,7 @@ def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, v print(element) if element == '立即沟通': # 发送描述到聊天并打印响应 - if should_use_langchain(): - response = generate_letter(vectorstore, job_description) - else: - response = chat(job_description, assistant_id) + response = chat(job_description, assistant_id) print(response) time.sleep(1) # 点击沟通按钮 @@ -187,11 +198,5 @@ def send_job_descriptions_to_chat(url, browser_type, label, assistant_id=None, v url = "https://www.zhipin.com/web/geek/job-recommend?ka=header-job-recommend" browser_type = "chrome" label = "iOS(深圳)" # 想要选择的下拉菜单项 - if should_use_langchain(): - text = read_resumes() - chunks = get_text_chunks(text) - vectorstore = get_vectorstore(chunks) - send_job_descriptions_to_chat(url, browser_type, label, vectorstore=vectorstore) - else: - assistant_id = functions.create_assistant(client) - send_job_descriptions_to_chat(url, browser_type, label, assistant_id=assistant_id) + assistant_id = functions.create_assistant(openai_manager.get_client()) + send_job_descriptions_to_chat(url, browser_type, label, assistant_id=assistant_id) From e3e91ce515fc5f959363cd3ea88ad54017b68167 Mon Sep 17 00:00:00 2001 From: Siz Long Date: Sat, 5 Apr 2025 18:50:33 +1100 Subject: [PATCH 30/30] Update README.md --- README.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/README.md b/README.md index bbc524c..8c24137 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,114 @@ +## Main Content +This is a completely free script — all you need to do is configure your own OpenAI API. + +If you find this script helpful during this tough job-hunting season, I would be truly honored if you could give it a **star** ⭐️. + +If this script brings you some warmth in this cold recruitment season, that alone would mean a lot to me. + +Please don’t use this script to exploit others. If someone is already at the point of relying on scripts to apply for jobs, they probably don’t have much left to be squeezed. + +--- + +## Steps to Use + +1. First, configure your OpenAI API (using a `.env` file or set it directly in the code). +2. Upload your PDF resume to the `auto_job_find` folder and name it **"my_cover.pdf"**. +3. Install all required packages. +4. Run `write_response.py`. + +--- + +## About the Assistant + +The script will automatically create an OpenAI Assistant and generate a local `.json` file. This file is only created on the first run. On subsequent runs, the existing Assistant will be reused if the JSON file is detected. + +--- + +## Required Packages + +- `python-dotenv` +- `openai` +- `selenium` +- `robotframework` +- `robotframework-seleniumlibrary` +- `robotframework-pythonlibcore` + +--- + +## About RPA + +Tutorial video on how to get started with [RPA](https://www.youtube.com/watch?v=65OPFmEgCbM&list=PLx4LEkEdFArgrdD_lvXe_hYBy8zM0Sp3b&index=1) + +Recommended Plugin: Intellibot@Selenium Library + +--- + +### 📺 Simple Tutorial Videos + +- [Bilibili](https://www.bilibili.com/video/BV1UC4y1N78v/?share_source=copy_web&vd_source=b2608434484091fcc64d4eb85233122d) +- [YouTube](https://youtu.be/TlnytEi2lD8?si=jfcDj2MZqBptziZc) + +--- + +## How to Run + +Clone the project locally, then run the following in the root directory: +```bash +pip install -r requirements.txt +``` + +### Run with Assistant Mode + +1. Open the `.env` file and configure your OpenAI API key. +2. Upload your resume PDF to the `auto_job_find` folder, named `my_cover.pdf`. +3. Run `write_response.py`. + +Note: This mode does not support custom APIs but runs faster. + +### Run with Langchain Mode + +1. Again, configure your OpenAI API key and your custom API endpoint in the `.env` file. +2. Put your resume in the `resume` folder. +3. Run `write_response.py`. + +### Run with ChatGPT-4 and Above + +If you're using newer ChatGPT models, you may encounter an error if you're not using version `v1.1.1`: + +``` +Error code: 400 - {'error': {'message': "The requested model 'gpt-4o-mini' cannot be used with the Assistants API in v1. Follow the migration guide to upgrade to v2: https://platform.openai.com/docs/assistants/migration."}} +``` + +#### Solution: +1. Upgrade OpenAI package: +```shell +pip install --upgrade openai +``` +2. Modify the `create_assistant` function structure. See the [migration guide](https://platform.openai.com/docs/assistants/migration) for details. + +> Alternatively, manually create an assistant on the [OpenAI Platform](https://platform.openai.com/assistants/), then copy its code into your `assistant.json` file: +```json +{"assistant_id": "asst_token"} +``` + +--- + +## Alternative JavaScript Version + +Some kind contributors have built easier-to-use versions based on JavaScript. Although these versions may not support Assistant-based retrieval and may require manual preprocessing of resumes, they are still very helpful. + +GitHub link: +[https://github.com/noBaldAaa/find-job](https://github.com/noBaldAaa/find-job) + +--- + +## Azure-based Version + +An alternative version using Azure's OpenAI API is available here: +[https://github.com/LouisCaixuran/auto_job_find_azure](https://github.com/LouisCaixuran/auto_job_find_azure) + + + ## 正文 这是一个完全免费的脚本,只需要你们自己配置好openai的api即可