├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── README.user.md ├── auto-imports.d.ts ├── boos-helper-config.json ├── components.d.ts ├── docs └── img │ ├── shot_2024-04-02_22-25-25.png │ ├── shot_2024-04-02_22-26-54.png │ ├── shot_2024-04-02_22-32-25.png │ ├── shot_2024-04-14_23-08-03.png │ └── shot_2024-04-14_23-09-05.png ├── package.json ├── pnpm-lock.yaml ├── src ├── App.vue ├── assets │ ├── chat.proto │ └── helper.svg ├── components │ ├── chat │ │ └── chat.vue │ ├── conf │ │ ├── log.vue │ │ ├── store.vue │ │ └── user.vue │ ├── form │ │ ├── formItem.vue │ │ ├── formSelect.vue │ │ └── formSwitch.vue │ ├── icon │ │ ├── info.vue │ │ └── settings.vue │ ├── llms │ │ ├── configLLM.vue │ │ ├── createLLM.vue │ │ ├── lForm.vue │ │ ├── lFormItem.vue │ │ └── selectLLM.vue │ └── logger.vue ├── hooks │ ├── useApplying │ │ ├── api.ts │ │ ├── handles.ts │ │ ├── index.ts │ │ ├── type.ts │ │ └── utils.ts │ ├── useChat │ │ ├── index.ts │ │ └── type.ts │ ├── useCommon.ts │ ├── useConfForm.ts │ ├── useLog.ts │ ├── useMap.ts │ ├── useModel │ │ ├── common.ts │ │ ├── index.ts │ │ ├── llms │ │ │ ├── coze.ts │ │ │ ├── moonshot.ts │ │ │ ├── openai.ts │ │ │ └── user.ts │ │ └── type.ts │ ├── useStatistics.ts │ ├── useStore.ts │ ├── useVue.ts │ └── useWebSocket │ │ ├── index.ts │ │ ├── mqtt.ts │ │ ├── protobuf.ts │ │ ├── type.json │ │ └── type.ts ├── main.scss ├── main.ts ├── pages │ └── geek │ │ └── job │ │ ├── about.vue │ │ ├── ai.vue │ │ ├── card.vue │ │ ├── config.vue │ │ ├── hooks │ │ ├── useDeliver.ts │ │ ├── useJobList.ts │ │ └── usePager.ts │ │ ├── index.scss │ │ ├── index.ts │ │ ├── logs.vue │ │ ├── statistics.vue │ │ └── ui.vue ├── types │ ├── boosData.d.ts │ ├── deliverError.ts │ ├── formData.ts │ ├── jobData.d.ts │ ├── mitem.d.ts │ └── vueVirtualScroller.d.ts ├── utils │ ├── conf.ts │ ├── deepmerge.ts │ ├── elmGetter.ts │ ├── index.ts │ ├── logger.ts │ ├── parse.ts │ └── request.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── update.log └── vite.config.ts /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | workflow_dispatch: 7 | #schedule: 8 | # - cron: "30 0 * * *" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [21] 16 | outputs: 17 | current_time: ${{ steps.current_time.outputs.current_time }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - uses: pnpm/action-setup@v3 22 | with: 23 | version: 8 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: "pnpm" 29 | 30 | - name: Install dependencies 31 | run: pnpm install 32 | 33 | - name: Get current time 34 | id: current_time 35 | run: echo "current_time=$(date +%Y-%m-%d)" >> "$GITHUB_OUTPUT" 36 | 37 | - name: Set Release Environment 38 | if: startsWith(github.ref, 'refs/tags/') 39 | run: | 40 | echo "VITE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV 41 | echo "VITE_RELEASE_MODE=release" >> $GITHUB_ENV 42 | 43 | - name: Set Prerelease Environment 44 | if: ${{ ! startsWith(github.ref, 'refs/tags/') }} 45 | run: | 46 | echo "VITE_VERSION=${{ steps.current_time.outputs.current_time }}.alpha" >> $GITHUB_ENV 47 | echo "VITE_RELEASE_MODE=alpha" >> $GITHUB_ENV 48 | 49 | - name: Watch Environment 50 | run: echo "version=$VITE_VERSION,mode=$VITE_RELEASE_MODE" 51 | 52 | - name: Build 53 | run: pnpm run build:noTsc 54 | 55 | - name: Upload Artifacts 56 | uses: actions/upload-artifact@v4 57 | with: 58 | path: dist/* 59 | 60 | release: 61 | needs: build 62 | runs-on: ubuntu-latest 63 | permissions: 64 | contents: write 65 | discussions: write 66 | steps: 67 | - name: Download Artifacts 68 | uses: actions/download-artifact@v4 69 | with: 70 | path: dist 71 | - name: ls dist 72 | run: ls -la dist/* 73 | 74 | - name: Release 75 | uses: softprops/action-gh-release@v2.0.4 76 | if: startsWith(github.ref, 'refs/tags/') 77 | with: 78 | fail_on_unmatched_files: true 79 | files: dist/** 80 | 81 | - name: Prerelease 82 | uses: softprops/action-gh-release@v2.0.4 83 | if: ${{ ! startsWith(github.ref, 'refs/tags/') }} 84 | with: 85 | name: ${{needs.build.outputs.current_time}} 86 | tag_name: ${{needs.build.outputs.current_time}} 87 | fail_on_unmatched_files: true 88 | prerelease: true 89 | files: dist/** 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | !/update.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | 11 | node_modules 12 | dist 13 | other 14 | dist-ssr 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | logapi.zhipin.*.pem 29 | 30 | boos-helper-config.local.json 31 | /local -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ocyss(git@ocyss.icu) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > 本项目仅供学习交流,禁止用于商业用途 3 | > 4 | > 使用该脚本有一定风险(如黑号,封号,权重降低等),本项目不承担任何责任 5 | 6 | # 重要提示 7 | 8 | 本项目从0.3版本开始重构成浏览器扩展,油猴脚本不再更新,扩展等代码整理之后再开源 9 | 10 | | Chrome | Crx搜搜 | Edge | FireFox | Github | 11 | |----------|----------|----------|----------|----------| 12 | | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/ogkmgjbagackkdlcibcailacnncgonbn?label=Chrome插件商店)](https://chrome.google.com/webstore/detail/ogkmgjbagackkdlcibcailacnncgonbn) |[![Crx 搜搜](https://img.shields.io/badge/Crx搜索-v%3F.%3F.%3F-EF7C3D)](https://www.crxsoso.com/webstore/detail/ogkmgjbagackkdlcibcailacnncgonbn) | [![Edge Web Store](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fjcllnbjfeamhihjpfjlclhdnjmggbgal&query=version&prefix=v&label=Edge插件商店&color=EF7C3D)](https://microsoftedge.microsoft.com/addons/detail/jcllnbjfeamhihjpfjlclhdnjmggbgal) | [![Firefox](https://img.shields.io/amo/v/boss-helper?label=Mozilla插件商店)](https://addons.mozilla.org/zh-TW/firefox/addon/boss-helper/) | [![GitHub Release](https://img.shields.io/github/v/release/Ocyss/boos-helper)](https://github.com/Ocyss/boos-helper/releases/latest/) | 13 | 14 | > **国内**: 如果无法访问 `Chrome插件商店` , 请使用 `Crx搜搜` 或 `Edge插件商店` 安装 15 | 16 | ## 项目介绍 17 | 18 | Boos直聘助手, 皆在减少投递简历的麻烦, 和提高投递简历的效率,技术栈使用VIte + Vue3 + element-plus, 代码由 Vite 打包无加密混淆最小化,开源在 Github 欢迎前来Pr 19 | 20 | > 本项目处于积极维护状态 21 | 22 | ## 相关链接 23 | 24 | Github开源地址: 25 | 26 | greasyfork地址: 27 | 28 | 飞书反馈问卷(匿名): 29 | 30 | 飞书问卷结果: 31 | 32 | 飞书交流群: 33 | 34 | 35 | ## 项目预览 36 | 37 | [![卡片状态](docs/img/shot_2024-04-14_23-08-03.png)](docs/img/shot_2024-04-14_23-08-03.png) 38 | [![账户配置](docs/img/shot_2024-04-14_23-09-05.png)](docs/img/shot_2024-04-14_23-09-05.png) 39 | [![统计界面](docs/img/shot_2024-04-02_22-25-25.png)](docs/img/shot_2024-04-02_22-25-25.png) 40 | [![配置界面](docs/img/shot_2024-04-02_22-26-54.png)](docs/img/shot_2024-04-02_22-26-54.png) 41 | [![日志界面](docs/img/shot_2024-04-02_22-32-25.png)](docs/img/shot_2024-04-02_22-32-25.png) 42 | 43 | ## 未来计划 44 | 45 | - [x] 优化UI去除广告 46 | - [x] 批量投递简历 47 | - 高级筛选 48 | - [x] 薪资,公司名,工作名,人数,内容简单筛选 49 | - [ ] 通勤时间 50 | - [ ] 公司风险评控 51 | - [x] GPT筛选 52 | - 自动打招呼 53 | - [x] 模板语言 54 | - [x] 支持chatGPT,自定义http调用 55 | - GPT赋能 56 | - [ ] 自动回复聊天 57 | - [x] 多模型管理 58 | - 额外功能(有时间会写) 59 | - [x] 暗黑模式 60 | - [x] 自适应UI适配手机 61 | - [ ] 黑名单 62 | - [x] 多账号管理 63 | - [ ] 聊天屏蔽已读消息 64 | - [ ] boss消息弹窗 65 | 66 | 67 | ## 参与贡献 68 | 69 | 1. Fork 本仓库并克隆到本地。 70 | 2. 在新分支上进行您的更改:`git checkout -b 您的分支名称` 71 | 3. 提交更改:`git commit -am '描述您的更改'` 72 | 4. 推送更改到您的 Fork:`git push origin 您的分支名称` 73 | 5. 提交 Pull 请求。 74 | 75 | - 在开发时server模式会注入脚本,也可能导致跨域问题请禁用以下两个策略 76 | 77 | chrome 用户: 78 | 79 | - chrome://flags/#block-insecure-private-network-requests 80 | - chrome://flags/#private-network-access-respect-preflight-results 81 | 82 | edge 用户: 83 | 84 | - edge://flags/#block-insecure-private-network-requests 85 | - edge://flags/#private-network-access-respect-preflight-results 86 | 87 | ## 鸣谢 88 | 89 | - 90 | - 91 | - 92 | 93 | - 94 | - 95 | 96 | ## 类似项目 97 | 98 | - 99 | - 100 | 101 | ## 最后 102 | 103 | 嗯... 104 | 105 | ## Star 趋势 106 | 107 | 108 | 109 | 110 | 111 | Star History Chart 112 | 113 | -------------------------------------------------------------------------------- /README.user.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > 本项目仅供学习交流,禁止用于商业用途 3 | > 4 | > 使用该脚本有一定风险(如黑号,封号,权重降低等),本项目不承担任何责任 5 | 6 | # 重要提示 7 | 8 | 本项目从0.3版本开始重构成浏览器扩展,油猴脚本不再更新,扩展等代码整理之后再开源 9 | 10 | | Chrome | Crx搜搜 | Edge | FireFox | Github | 11 | |----------|----------|----------|----------|----------| 12 | | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/ogkmgjbagackkdlcibcailacnncgonbn?label=Chrome插件商店)](https://chrome.google.com/webstore/detail/ogkmgjbagackkdlcibcailacnncgonbn) |[![Crx 搜搜](https://img.shields.io/badge/Crx搜索-v%3F.%3F.%3F-EF7C3D)](https://www.crxsoso.com/webstore/detail/ogkmgjbagackkdlcibcailacnncgonbn) | [![Edge Web Store](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fjcllnbjfeamhihjpfjlclhdnjmggbgal&query=version&prefix=v&label=Edge插件商店&color=EF7C3D)](https://microsoftedge.microsoft.com/addons/detail/jcllnbjfeamhihjpfjlclhdnjmggbgal) | [![Firefox](https://img.shields.io/amo/v/boss-helper?label=Mozilla插件商店)](https://addons.mozilla.org/zh-TW/firefox/addon/boss-helper/) | [![GitHub Release](https://img.shields.io/github/v/release/Ocyss/boos-helper)](https://github.com/Ocyss/boos-helper/releases/latest/) | 13 | 14 | > **国内**: 如果无法访问 `Chrome插件商店` , 请使用 `Crx搜搜` 或 `Edge插件商店` 安装 15 | 16 | # 项目介绍 17 | 18 | Boos直聘助手, 皆在减少投递简历的麻烦, 和提高投递简历的效率,技术栈使用VIte + Vue3 + element-plus, 代码由 Vite 打包无加密混淆最小化, 开源在 Github 欢迎前来Pr 19 | 20 | ## 相关链接 21 | 22 | Github开源地址: 23 | 24 | greasyfork地址: 25 | 26 | 飞书反馈问卷(匿名): 27 | 28 | 飞书问卷结果: 29 | 30 | 飞书交流群: 31 | 32 | ### 问题解答/已知问题 33 | 34 | - 暗黑模式不完整,有些地方还是白的 35 | - 最好还是让官方去适配,一个个适配有点麻烦,后续会更加专注默认主题、删除暗黑模式 36 | 37 | ## 未来计划 38 | 39 | - [x] 优化UI去除广告 40 | - [x] 批量投递简历 41 | - 高级筛选 42 | - [x] 薪资,公司名,工作名,人数,内容简单筛选 43 | - [ ] 通勤时间 44 | - [ ] 公司风险评控 45 | - [x] GPT筛选 46 | - 自动打招呼 47 | - [x] 模板语言 48 | - [x] 支持chatGPT,自定义http调用 49 | - GPT赋能 50 | - [ ] 自动回复聊天 51 | - [x] 多模型管理 52 | - 额外功能(有时间会写) 53 | - [x] 暗黑模式 54 | - [x] 自适应UI适配手机 55 | - [ ] 黑名单 56 | - [x] 多账号管理 57 | - [ ] 聊天屏蔽已读消息 58 | - [ ] boos消息弹窗 59 | 60 | 61 | 62 | 63 | ## 项目预览 64 | 65 | [![卡片状态](https://s21.ax1x.com/2024/04/14/pFvtxGF.png)](https://imgse.com/i/pFvtxGF) 66 | [![账户配置](https://s21.ax1x.com/2024/04/14/pFvtvPU.png)](https://imgse.com/i/pFvtvPU) 67 | [![统计界面](https://s21.ax1x.com/2024/04/02/pFHa3ZD.png)](https://imgse.com/i/pFHa3ZD) 68 | [![配置界面](https://s21.ax1x.com/2024/04/02/pFHa8de.png)](https://imgse.com/i/pFHa8de) 69 | [![日志界面](https://s21.ax1x.com/2024/04/02/pFHalqO.png)](https://imgse.com/i/pFHalqO) 70 | 71 | 72 | # 鸣谢 73 | 74 | - 75 | - 76 | 77 | - 78 | 79 | ## Star 趋势 80 | 81 | 82 | 83 | 84 | 85 | Star History Chart 86 | 87 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const ElMessage: typeof import('element-plus/es')['ElMessage'] 9 | const GM: typeof import('vite-plugin-monkey/dist/client')['GM'] 10 | const GM_addElement: typeof import('vite-plugin-monkey/dist/client')['GM_addElement'] 11 | const GM_addStyle: typeof import('vite-plugin-monkey/dist/client')['GM_addStyle'] 12 | const GM_addValueChangeListener: typeof import('vite-plugin-monkey/dist/client')['GM_addValueChangeListener'] 13 | const GM_cookie: typeof import('vite-plugin-monkey/dist/client')['GM_cookie'] 14 | const GM_deleteValue: typeof import('vite-plugin-monkey/dist/client')['GM_deleteValue'] 15 | const GM_download: typeof import('vite-plugin-monkey/dist/client')['GM_download'] 16 | const GM_getResourceText: typeof import('vite-plugin-monkey/dist/client')['GM_getResourceText'] 17 | const GM_getResourceURL: typeof import('vite-plugin-monkey/dist/client')['GM_getResourceURL'] 18 | const GM_getTab: typeof import('vite-plugin-monkey/dist/client')['GM_getTab'] 19 | const GM_getTabs: typeof import('vite-plugin-monkey/dist/client')['GM_getTabs'] 20 | const GM_getValue: typeof import('vite-plugin-monkey/dist/client')['GM_getValue'] 21 | const GM_info: typeof import('vite-plugin-monkey/dist/client')['GM_info'] 22 | const GM_listValues: typeof import('vite-plugin-monkey/dist/client')['GM_listValues'] 23 | const GM_log: typeof import('vite-plugin-monkey/dist/client')['GM_log'] 24 | const GM_notification: typeof import('vite-plugin-monkey/dist/client')['GM_notification'] 25 | const GM_openInTab: typeof import('vite-plugin-monkey/dist/client')['GM_openInTab'] 26 | const GM_registerMenuCommand: typeof import('vite-plugin-monkey/dist/client')['GM_registerMenuCommand'] 27 | const GM_removeValueChangeListener: typeof import('vite-plugin-monkey/dist/client')['GM_removeValueChangeListener'] 28 | const GM_saveTab: typeof import('vite-plugin-monkey/dist/client')['GM_saveTab'] 29 | const GM_setClipboard: typeof import('vite-plugin-monkey/dist/client')['GM_setClipboard'] 30 | const GM_setValue: typeof import('vite-plugin-monkey/dist/client')['GM_setValue'] 31 | const GM_unregisterMenuCommand: typeof import('vite-plugin-monkey/dist/client')['GM_unregisterMenuCommand'] 32 | const GM_webRequest: typeof import('vite-plugin-monkey/dist/client')['GM_webRequest'] 33 | const GM_xmlhttpRequest: typeof import('vite-plugin-monkey/dist/client')['GM_xmlhttpRequest'] 34 | const monkeyWindow: typeof import('vite-plugin-monkey/dist/client')['monkeyWindow'] 35 | const unsafeWindow: typeof import('vite-plugin-monkey/dist/client')['unsafeWindow'] 36 | } 37 | -------------------------------------------------------------------------------- /boos-helper-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.2", 3 | "notification": [ 4 | { 5 | "key": "", 6 | "type": "notification", 7 | "data": { 8 | "title": "点我反馈", 9 | "message": "来自云端的通知测试,脚本完全开源无后台请放心,本通知为被动触发,请求固定配置文件触发", 10 | "type": "info", 11 | "duration": 9999999999, 12 | "url": "https://gai06vrtbc0.feishu.cn/share/base/form/shrcnmEq2fxH9hM44hqEnoeaj8g" 13 | } 14 | } 15 | ], 16 | "feedback": "https://gai06vrtbc0.feishu.cn/share/base/form/shrcnmEq2fxH9hM44hqEnoeaj8g" 17 | } 18 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | Chat: typeof import('./src/components/chat/chat.vue')['default'] 11 | ConfigLLM: typeof import('./src/components/llms/configLLM.vue')['default'] 12 | CreateLLM: typeof import('./src/components/llms/createLLM.vue')['default'] 13 | ElAlert: typeof import('element-plus/es')['ElAlert'] 14 | ElAvatar: typeof import('element-plus/es')['ElAvatar'] 15 | ElBadge: typeof import('element-plus/es')['ElBadge'] 16 | ElButton: typeof import('element-plus/es')['ElButton'] 17 | ElCollapse: typeof import('element-plus/es')['ElCollapse'] 18 | ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] 19 | ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] 20 | ElDialog: typeof import('element-plus/es')['ElDialog'] 21 | ElForm: typeof import('element-plus/es')['ElForm'] 22 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 23 | ElIcon: typeof import('element-plus/es')['ElIcon'] 24 | ElInput: typeof import('element-plus/es')['ElInput'] 25 | ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 26 | ElLink: typeof import('element-plus/es')['ElLink'] 27 | ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] 28 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 29 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 30 | ElSegmented: typeof import('element-plus/es')['ElSegmented'] 31 | ElSpace: typeof import('element-plus/es')['ElSpace'] 32 | ElTable: typeof import('element-plus/es')['ElTable'] 33 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 34 | ElTag: typeof import('element-plus/es')['ElTag'] 35 | ElText: typeof import('element-plus/es')['ElText'] 36 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 37 | FormItem: typeof import('./src/components/form/formItem.vue')['default'] 38 | FormSelect: typeof import('./src/components/form/formSelect.vue')['default'] 39 | FormSwitch: typeof import('./src/components/form/formSwitch.vue')['default'] 40 | Info: typeof import('./src/components/icon/info.vue')['default'] 41 | LForm: typeof import('./src/components/llms/lForm.vue')['default'] 42 | LFormItem: typeof import('./src/components/llms/lFormItem.vue')['default'] 43 | Log: typeof import('./src/components/conf/log.vue')['default'] 44 | Logger: typeof import('./src/components/logger.vue')['default'] 45 | SelectLLM: typeof import('./src/components/llms/selectLLM.vue')['default'] 46 | Settings: typeof import('./src/components/icon/settings.vue')['default'] 47 | Store: typeof import('./src/components/conf/store.vue')['default'] 48 | User: typeof import('./src/components/conf/user.vue')['default'] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/img/shot_2024-04-02_22-25-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocyss/boos-helper/6b0733a3894136d6a21d4f0a65841bba8451679d/docs/img/shot_2024-04-02_22-25-25.png -------------------------------------------------------------------------------- /docs/img/shot_2024-04-02_22-26-54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocyss/boos-helper/6b0733a3894136d6a21d4f0a65841bba8451679d/docs/img/shot_2024-04-02_22-26-54.png -------------------------------------------------------------------------------- /docs/img/shot_2024-04-02_22-32-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocyss/boos-helper/6b0733a3894136d6a21d4f0a65841bba8451679d/docs/img/shot_2024-04-02_22-32-25.png -------------------------------------------------------------------------------- /docs/img/shot_2024-04-14_23-08-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocyss/boos-helper/6b0733a3894136d6a21d4f0a65841bba8451679d/docs/img/shot_2024-04-14_23-08-03.png -------------------------------------------------------------------------------- /docs/img/shot_2024-04-14_23-09-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocyss/boos-helper/6b0733a3894136d6a21d4f0a65841bba8451679d/docs/img/shot_2024-04-14_23-09-05.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boos-helper", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "build:noTsc": "vite build", 10 | "preview": "vite preview", 11 | "test": "vue-tsc --noEmit && vite build && vite preview --open", 12 | "check": "vue-tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@types/nanoassert": "^2.0.3", 16 | "axios": "^1.7.7", 17 | "element-plus": "^2.8.1", 18 | "fetch-event-stream": "^0.1.5", 19 | "mitem": "^1.0.9", 20 | "nanoassert": "^2.0.0", 21 | "partial-json": "^0.1.7", 22 | "vue": "^3.4.38", 23 | "vue-virtual-scroller": "2.0.0-beta.8" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20.16.3", 27 | "@vitejs/plugin-vue": "^5.1.3", 28 | "@vue/tsconfig": "^0.5.1", 29 | "@vueuse/core": "^10.11.1", 30 | "proto2api": "^0.0.8", 31 | "protobufjs": "^7.4.0", 32 | "sass": "^1.77.8", 33 | "ts-proto": "^1.181.2", 34 | "typescript": "^5.5.4", 35 | "unplugin-auto-import": "^0.17.8", 36 | "unplugin-vue-components": "^0.26.0", 37 | "vite": "^5.4.2", 38 | "vite-plugin-monkey": "^3.5.2", 39 | "vue-tsc": "^1.8.27" 40 | }, 41 | "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" 42 | } 43 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 103 | 104 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/components/chat/chat.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 118 | 119 | 242 | -------------------------------------------------------------------------------- /src/components/conf/log.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/conf/store.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/conf/user.vue: -------------------------------------------------------------------------------- 1 | 122 | 123 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /src/components/form/formItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/form/formSelect.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/form/formSwitch.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/icon/info.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/icon/settings.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/llms/configLLM.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/components/llms/createLLM.vue: -------------------------------------------------------------------------------- 1 | 161 | 162 | 260 | 261 | 276 | -------------------------------------------------------------------------------- /src/components/llms/lForm.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/llms/lFormItem.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/components/llms/selectLLM.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 190 | 191 | 202 | -------------------------------------------------------------------------------- /src/components/logger.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocyss/boos-helper/6b0733a3894136d6a21d4f0a65841bba8451679d/src/components/logger.vue -------------------------------------------------------------------------------- /src/hooks/useApplying/api.ts: -------------------------------------------------------------------------------- 1 | import { GreetError, PublishError } from "@/types/deliverError"; 2 | import axios from "axios"; 3 | import { useStore } from "../useStore"; 4 | import { ElMessage } from "element-plus"; 5 | import { unsafeWindow } from "$"; 6 | 7 | const { userInfo } = useStore(); 8 | 9 | export function requestCard(params: { securityId: string; lid: string }) { 10 | return axios.get<{ 11 | code: number; 12 | message: string; 13 | zpData: { 14 | jobCard: JobCard; 15 | }; 16 | }>("https://www.zhipin.com/wapi/zpgeek/job/card.json", { 17 | params, 18 | timeout: 5000, 19 | }); 20 | } 21 | 22 | export async function sendPublishReq( 23 | data: JobListData, 24 | errorMsg?: string, 25 | retries = 3 26 | ) { 27 | if (retries === 0) { 28 | throw new PublishError(errorMsg || "重试多次失败"); 29 | } 30 | const url = "https://www.zhipin.com/wapi/zpgeek/friend/add.json"; 31 | let params: { 32 | securityId: string | null; 33 | jobId: string | null; 34 | lid: string | null; 35 | }; 36 | params = { 37 | securityId: data.securityId, 38 | jobId: data.encryptJobId, 39 | lid: data.lid, 40 | }; 41 | const token = unsafeWindow?.Cookie.get("bst"); 42 | if (!token) { 43 | ElMessage.error("没有获取到token,请刷新重试"); 44 | throw new PublishError("没有获取到token"); 45 | } 46 | try { 47 | const res = await axios({ 48 | url, 49 | params, 50 | method: "POST", 51 | headers: { Zp_token: token }, 52 | }); 53 | if ( 54 | res.data.code === 1 && 55 | res.data?.zpData?.bizData?.chatRemindDialog?.content 56 | ) { 57 | throw new PublishError( 58 | res.data?.zpData?.bizData?.chatRemindDialog?.content 59 | ); 60 | } 61 | if (res.data.code !== 0) { 62 | throw new PublishError("状态错误:" + res.data.message); 63 | } 64 | return res.data; 65 | } catch (e: any) { 66 | if (e instanceof PublishError) { 67 | throw e; 68 | } 69 | return sendPublishReq(data, e.message, retries - 1); 70 | } 71 | } 72 | 73 | export async function requestBossData( 74 | card: JobCard, 75 | errorMsg?: string, 76 | retries = 3 77 | ): Promise { 78 | if (retries === 0) { 79 | throw new GreetError(errorMsg || "重试多次失败"); 80 | } 81 | const url = "https://www.zhipin.com/wapi/zpchat/geek/getBossData"; 82 | // userInfo.value?.token 不相等! 83 | const token = unsafeWindow?.Cookie.get("bst"); 84 | if (!token) { 85 | ElMessage.error("没有获取到token,请刷新重试"); 86 | throw new GreetError("没有获取到token"); 87 | } 88 | try { 89 | const data = new FormData(); 90 | data.append("bossId", card.encryptUserId); 91 | data.append("securityId", card.securityId); 92 | data.append("bossSrc", "0"); 93 | const res = await axios<{ 94 | code: number; 95 | message: string; 96 | zpData: BoosData; 97 | }>({ 98 | url, 99 | data: data, 100 | method: "POST", 101 | headers: { Zp_token: token }, 102 | }); 103 | if (res.data.code !== 0 && res.data.message !== "非好友关系") { 104 | throw new GreetError("状态错误:" + res.data.message); 105 | } 106 | if (res.data.code !== 0) 107 | return requestBossData(card, "非好友关系", retries - 1); 108 | return res.data.zpData; 109 | } catch (e: any) { 110 | if (e instanceof GreetError) { 111 | throw e; 112 | } 113 | return requestBossData(card, e.message, retries - 1); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/hooks/useApplying/index.ts: -------------------------------------------------------------------------------- 1 | import { UnknownError, errMap } from "@/types/deliverError"; 2 | import { handleFn } from "./type"; 3 | import { requestCard } from "./api"; 4 | import { useConfFormData } from "../useConfForm"; 5 | import * as h from "./handles"; 6 | import { logData } from "../useLog"; 7 | export * from "./api"; 8 | const { formData } = useConfFormData(); 9 | 10 | export function createHandle(): { 11 | before: handleFn; 12 | after: handleFn; 13 | record: (ctx: logData) => Promise; 14 | } { 15 | // 无需调用接口 16 | const handles: handleFn[] = []; 17 | // 需要调用接口 18 | const handlesRes: handleFn[] = []; 19 | // 投递后调用 20 | const handlesAfter: handleFn[] = []; 21 | 22 | // 已沟通过滤 23 | h.communicated(handles); 24 | // 岗位名筛选 25 | if (formData.jobTitle.enable) h.jobTitle(handles); 26 | // 公司名筛选 27 | if (formData.company.enable) h.company(handles); 28 | // 薪资筛选 29 | if (formData.salaryRange.enable) h.salaryRange(handles); 30 | // 公司规模筛选 31 | if (formData.companySizeRange.enable) h.companySizeRange(handles); 32 | // 猎头过滤 33 | if (formData.goldHunterFilter.value) h.goldHunterFilter(handles); 34 | // 好友状态过滤 35 | if (formData.friendStatus.value) h.jobFriendStatus(handlesRes); 36 | // 工作内容筛选 37 | if (formData.jobContent.enable) h.jobContent(handlesRes); 38 | // Hr职位筛选 39 | if (formData.hrPosition.enable) h.hrPosition(handlesRes); 40 | // 活跃度过滤 41 | if (formData.activityFilter.value) h.activityFilter(handlesRes); 42 | // AI过滤 43 | if (formData.aiFiltering.enable) h.aiFiltering(handlesRes); 44 | 45 | if (formData.aiGreeting.enable) { 46 | // AI招呼语 47 | h.aiGreeting(handlesAfter); 48 | } else if (formData.customGreeting.enable) { 49 | // 自定义招呼语 50 | h.customGreeting(handlesAfter); 51 | } 52 | 53 | return { 54 | before: async (args, ctx) => { 55 | try { 56 | // 异步运行 card 请求前的筛选 57 | await Promise.all(handles.map((handle) => handle(args, ctx))); 58 | 59 | if (handlesRes.length > 0) { 60 | const cardResp = await requestCard({ 61 | lid: args.data.lid, 62 | securityId: args.data.securityId, 63 | }); 64 | if (cardResp.data.code == 0) { 65 | ctx.card = cardResp.data.zpData.jobCard; 66 | // 异步运行 card 请求后的筛选 67 | await Promise.all(handlesRes.map((handle) => handle(args, ctx))); 68 | } else { 69 | throw new UnknownError("请求响应错误:" + cardResp.data.message); 70 | } 71 | } 72 | } catch (e: any) { 73 | if (errMap.has(e.name)) { 74 | throw e; 75 | } 76 | throw new UnknownError("预期外:" + e.message); 77 | } 78 | }, 79 | after: async (args, ctx) => { 80 | if (handlesAfter.length === 0) return; 81 | try { 82 | if (!ctx.card) { 83 | const cardResp = await requestCard({ 84 | lid: args.data.lid, 85 | securityId: args.data.securityId, 86 | }); 87 | if (cardResp.data.code == 0) { 88 | ctx.card = cardResp.data.zpData.jobCard; 89 | } else { 90 | throw new UnknownError("请求响应错误:" + cardResp.data.message); 91 | } 92 | } 93 | await Promise.all(handlesAfter.map((handle) => handle(args, ctx))); 94 | } catch (e: any) { 95 | if (errMap.has(e.name)) { 96 | throw e; 97 | } 98 | throw new UnknownError("预期外:" + e.message); 99 | } 100 | }, 101 | record: (ctx) => { 102 | if (formData.record.enable) return h.record(ctx); 103 | return Promise.resolve(); 104 | }, 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /src/hooks/useApplying/type.ts: -------------------------------------------------------------------------------- 1 | import { logData } from "../useLog"; 2 | 3 | export type handleArgs = { 4 | data: JobListData; 5 | }; 6 | 7 | export type handleFn = (args: handleArgs, ctx: logData) => Promise; 8 | export type handleCFn = (handles: handleFn[]) => void; 9 | -------------------------------------------------------------------------------- /src/hooks/useApplying/utils.ts: -------------------------------------------------------------------------------- 1 | // 匹配范围 2 | export function rangeMatch( 3 | rangeStr: string, 4 | input?: string, 5 | mode: "intersection" | "subset" = "subset" // 交集、子集,默认: 子集 6 | ): [boolean, string] { 7 | if (!rangeStr) { 8 | return [false, "无内容"]; 9 | } 10 | // 匹配定义范围的正则表达式 11 | const reg = /(\d+)(?:-(\d+))?/; 12 | const match = rangeStr.match(reg); 13 | let err = "预期之外"; 14 | if (match && match.length > 0) { 15 | err = match[0]; 16 | } 17 | if (match && input) { 18 | let start = parseInt(match[1]); 19 | let end = parseInt(match[2] || match[1]); 20 | 21 | // 如果输入只有一个数字的情况 22 | if (/^\d+$/.test(input)) { 23 | let number = parseInt(input); 24 | 25 | return [number >= start && number <= end, err]; 26 | } 27 | 28 | // 如果输入有两个数字的情况 29 | let inputReg = /^(\d+)(?:-(\d+))?/; 30 | let inputMatch = input.match(inputReg); 31 | if (inputMatch) { 32 | let inputStart = parseInt(inputMatch[1]); 33 | let inputEnd = parseInt(inputMatch[2] || inputMatch[1]); 34 | return [ 35 | // start-end: 15-29 用户输入: inputStart-inputEnd 16-20 36 | mode == "subset" 37 | ? // 子集 38 | (start >= inputStart && start <= inputEnd) || 39 | (end >= inputStart && end <= inputEnd) 40 | : // 交集 41 | !(end < inputStart || inputEnd < start), 42 | err, 43 | ]; 44 | } 45 | } 46 | 47 | // 其他情况均视为不匹配 48 | return [false, err]; 49 | } 50 | -------------------------------------------------------------------------------- /src/hooks/useChat/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, reactive, toRaw } from "vue"; 2 | import { ChatInput, ChatMessages } from "./type"; 3 | import { llmsIcons, modelData } from "../useModel"; 4 | import { getCurDay, getCurTime } from "@/utils"; 5 | 6 | const chatMessages = ref([]); 7 | 8 | const chatInput = reactive({ 9 | role: "user", 10 | content: "", 11 | input: false, 12 | }); 13 | 14 | const chatInputInit = (model: modelData) => { 15 | chatInput.content = ""; 16 | chatInput.input = true; 17 | chatInput.role = "assistant"; 18 | chatInput.name = model.name; 19 | chatInput.avatar = { 20 | icon: llmsIcons[model.data?.mode || ""], 21 | color: model.color, 22 | }; 23 | let end = false; 24 | return { 25 | handle: (s: string) => { 26 | chatInput.content += s; 27 | }, 28 | end: (s: string) => { 29 | if (end) return; 30 | end = true; 31 | chatInput.input = false; 32 | chatInput.content = s; 33 | const d = new Date(); 34 | chatMessages.value.push({ 35 | id: d.getTime(), 36 | role: "assistant", 37 | content: s, 38 | date: [getCurDay(d), getCurTime(d)], 39 | name: chatInput.name, 40 | avatar: toRaw(chatInput.avatar!), 41 | }); 42 | chatInput.content = ""; 43 | }, 44 | }; 45 | }; 46 | 47 | export const useChat = () => { 48 | return { 49 | chatMessages, 50 | chatInput, 51 | chatInputInit, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /src/hooks/useChat/type.ts: -------------------------------------------------------------------------------- 1 | export type ChatMessages = ChatMessage[]; 2 | 3 | export type ChatMessage = { 4 | id: number; 5 | role: "boos" | "user" | "assistant"; 6 | name?: string; 7 | content: string; 8 | date: [string, string]; 9 | avatar: string | ChatAvatar; 10 | url?: string; 11 | data?: Record; 12 | }; 13 | 14 | export type ChatInput = { 15 | role: "user" | "assistant"; 16 | name?: string; 17 | content: string; 18 | input: boolean; 19 | avatar?: ChatAvatar; 20 | }; 21 | export type ChatAvatar = { 22 | icon?: string; 23 | color?: string; 24 | }; 25 | -------------------------------------------------------------------------------- /src/hooks/useCommon.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | const deliverLock = ref(false); 4 | const deliverStop = ref(false); 5 | 6 | export const useCommon = () => { 7 | return { 8 | deliverLock, 9 | deliverStop, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/hooks/useLog.ts: -------------------------------------------------------------------------------- 1 | import { Column, ElButton, ElTag } from "element-plus"; 2 | import { h, ref } from "vue"; 3 | import { 4 | PublishError, 5 | JobTitleError, 6 | CompanyNameError, 7 | SalaryError, 8 | CompanySizeError, 9 | JobDescriptionError, 10 | UnknownError, 11 | errMap, 12 | } from "@/types/deliverError"; 13 | 14 | type logErr = 15 | | null 16 | | undefined 17 | | PublishError 18 | | JobTitleError 19 | | CompanyNameError 20 | | SalaryError 21 | | CompanySizeError 22 | | JobDescriptionError 23 | | UnknownError; 24 | 25 | export type logData = { 26 | listData: JobListData; 27 | el?: Element; 28 | card?: JobCard; 29 | boosData?: BoosData; 30 | message?: string; 31 | state?: string; 32 | err?: string; 33 | aiFilteringQ?: string; 34 | aiFilteringAraw?: string; 35 | aiFilteringAjson?: object; 36 | aiFilteringAtext?: string; 37 | aiGreetingQ?: string; 38 | aiGreetingA?: string; 39 | }; 40 | 41 | type logState = "info" | "success" | "warning" | "danger"; 42 | 43 | type log = { 44 | title: string; // 标题 45 | state: logState; // 信息,成功,过滤,出错 46 | state_name: string; // 标签文本 47 | message?: string; // 显示消息 48 | data?: logData; 49 | }; 50 | 51 | const columns: Column[] = [ 52 | { 53 | key: "title", 54 | title: "标题", 55 | dataKey: "title", 56 | width: 200, 57 | }, 58 | { 59 | key: "state", 60 | title: "状态", 61 | width: 150, 62 | align: "center", 63 | cellRenderer: ({ rowData }) => 64 | h(ElTag, { type: rowData.state ?? "primary" }, () => rowData.state_name), 65 | }, 66 | { 67 | key: "message", 68 | title: "信息", 69 | dataKey: "message", 70 | width: 360, 71 | minWidth: 360, 72 | align: "left", 73 | }, 74 | ]; 75 | 76 | const dataOld = ref([]); 77 | const data = ref([ 78 | { 79 | title: "嘿嘿嘿", 80 | state: "info", 81 | state_name: "消息", 82 | message: "目前没有投递日志啦", 83 | }, 84 | { 85 | title: "啦啦啦", 86 | state: "success", 87 | state_name: "消息", 88 | message: "要查看其他日志请点击右上角的悬浮按钮", 89 | }, 90 | ]); 91 | 92 | export const useLog = () => { 93 | const add = (title: string, err: logErr, logdata?: logData, msg?: string) => { 94 | const state = !err ? "success" : err.state; 95 | const message = msg ?? (err ? err.message : undefined); 96 | 97 | data.value.push({ 98 | title, 99 | state, 100 | state_name: err?.name ?? "投递成功", 101 | message, 102 | data: logdata, 103 | }); 104 | }; 105 | const info = (title: string, message: string) => { 106 | data.value.push({ 107 | title, 108 | state: "info", 109 | state_name: "消息", 110 | message, 111 | data: undefined, 112 | }); 113 | }; 114 | const clear = () => { 115 | dataOld.value = []; 116 | data.value = []; 117 | }; 118 | const reset = () => { 119 | dataOld.value = data.value; 120 | data.value = []; 121 | }; 122 | 123 | const Row = ({ cells, rowData }: { cells: any; rowData: log }) => { 124 | // if (rowData.data) return h("div", {}, rowData.data); 125 | return cells; 126 | }; 127 | 128 | Row.inheritAttrs = false; 129 | return { 130 | columns, 131 | data, 132 | dataOld, 133 | clear, 134 | reset, 135 | add, 136 | info, 137 | Row, 138 | }; 139 | }; 140 | -------------------------------------------------------------------------------- /src/hooks/useMap.ts: -------------------------------------------------------------------------------- 1 | // 来自互联网 2 | import { ref, Ref, markRaw } from "vue"; 3 | export type MapValue = Iterable; 4 | export type Actions = { 5 | set: (key: K, value: T) => void; 6 | get: (key: K) => T | undefined; 7 | remove: (key: K) => void; 8 | has: (key: K) => boolean; 9 | clear: () => void; 10 | setAll: (newMap: MapValue) => void; 11 | reset: () => void; 12 | }; 13 | 14 | export function useMap( 15 | initialValue?: MapValue 16 | ): { state: Ref>; actions: Actions } { 17 | const initialMap = initialValue ? new Map(initialValue) : new Map(); 18 | const state = ref(initialMap) as Ref>; 19 | const actions: Actions = { 20 | set: (key, value) => { 21 | state.value.set(key, value); 22 | }, 23 | get: (key) => { 24 | return state.value.get(key); 25 | }, 26 | remove: (key) => { 27 | state.value.delete(key); 28 | }, 29 | has: (key) => state.value.has(key), 30 | clear: () => state.value.clear(), 31 | setAll: (newMap) => { 32 | state.value = new Map(newMap); 33 | }, 34 | reset: () => (state.value = initialMap), 35 | }; 36 | return { 37 | state, 38 | actions: markRaw(actions), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/useModel/common.ts: -------------------------------------------------------------------------------- 1 | import { llmConf, llmInfo } from "./type"; 2 | 3 | export type other = { 4 | other: { 5 | timeout?: number; 6 | }; 7 | }; 8 | 9 | export const other: llmInfo["other"] = { 10 | value: { 11 | timeout: { 12 | value: 120, 13 | type: "inputNumber", 14 | desc: "GPT请求的超时时间,超时后不会进行重试将跳过岗位,默认120s", 15 | }, 16 | }, 17 | alert: "warning", 18 | label: "其他配置", 19 | }; 20 | 21 | export const desc = { 22 | stream: "推荐开启,可以实时查看gpt返回的响应,但如果你的模型不支持,请关闭", 23 | max_tokens: "用处不大一般不需要调整", 24 | temperature: "较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定", 25 | top_p: "影响输出文本的多样性,取值越大,生成文本的多样性越强", 26 | }; 27 | -------------------------------------------------------------------------------- /src/hooks/useModel/index.ts: -------------------------------------------------------------------------------- 1 | import { GM_getValue, GM_setValue } from "$"; 2 | import { logger } from "@/utils/logger"; 3 | import { ElMessage } from "element-plus"; 4 | import { ref, toRaw } from "vue"; 5 | import { openai, openaiLLMConf } from "./llms/openai"; 6 | import { user, userLLMConf } from "./llms/user"; 7 | import { moonshot, moonshotLLMConf } from "./llms/moonshot"; 8 | import { coze, cozeLLMConf } from "./llms/coze"; 9 | import { llm, prompt } from "./type"; 10 | 11 | export const confModelKey = "conf-model"; 12 | export const llms = [openai.info, moonshot.info, coze.info]; 13 | 14 | export const llmsIcons = llms.reduce((acc, cur) => { 15 | if (cur.mode.icon) acc[cur.mode.mode] = cur.mode.icon; 16 | return acc; 17 | }, {} as Record); 18 | const modelData = ref(GM_getValue(confModelKey, [])); 19 | logger.debug("ai模型数据", toRaw(modelData.value)); 20 | 21 | export type modelData = { 22 | key: string; 23 | name: string; 24 | color?: string; 25 | data?: moonshotLLMConf | userLLMConf | openaiLLMConf | cozeLLMConf; 26 | }; 27 | 28 | function getGpt(model: modelData, template: string | prompt): llm { 29 | if (!model.data) { 30 | throw new Error("GPT数据不存在"); 31 | } 32 | if (Array.isArray(template)) { 33 | template = [...template].map((v) => ({ ...v })); 34 | } 35 | try { 36 | switch (model.data.mode) { 37 | case "openai": 38 | return new openai.gpt(model.data, template); 39 | case "moonshot": 40 | return new moonshot.gpt(model.data, template); 41 | case "coze": 42 | return new coze.gpt(model.data, template); 43 | case "user": 44 | break; 45 | } 46 | } catch (e) { 47 | throw new Error("GPT构建错误"); 48 | } 49 | throw new Error(`GPT不存在, 可能已删除停止维护, ${model.data.mode}`); 50 | } 51 | 52 | function save() { 53 | GM_setValue(confModelKey, toRaw(modelData.value)); 54 | ElMessage.success("保存成功"); 55 | } 56 | 57 | export const useModel = () => { 58 | return { 59 | modelData, 60 | save, 61 | getGpt, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /src/hooks/useModel/llms/moonshot.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { llm, llmConf, llmInfo, messageReps, prompt } from "../type"; 3 | import { openai } from "./openai"; 4 | import { other } from "../common"; 5 | import { OnStream, request } from "@/utils/request"; 6 | 7 | export type moonshotLLMConf = llmConf< 8 | "moonshot", 9 | { 10 | url: string; 11 | model: string; 12 | api_key: string; 13 | advanced: { 14 | json?: boolean; 15 | stream?: boolean; 16 | temperature?: number; 17 | top_p?: number; 18 | presence_penalty?: number; 19 | frequency_penalty?: number; 20 | }; 21 | } & other 22 | >; 23 | 24 | const info: llmInfo = { 25 | ...openai.info, 26 | mode: { 27 | mode: "moonshot", 28 | label: "Kimi", 29 | // disabled: true, 30 | icon: ``, 31 | desc: `注册就送15,国产模型对中文支持应该好一些 开通文档`, 32 | }, 33 | model: { 34 | config: { 35 | placeholder: "moonshot-v1-8k", 36 | options: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"].map( 37 | (item) => ({ 38 | label: item, 39 | value: item, 40 | }) 41 | ), 42 | 43 | allowCreate: true, 44 | filterable: true, 45 | defaultFirstOption: true, 46 | }, 47 | value: "moonshot-v1-8k", 48 | type: "select", 49 | required: true, 50 | }, 51 | url: { 52 | config: { 53 | placeholder: "https://api.moonshot.cn", 54 | }, 55 | value: "https://api.moonshot.cn", 56 | type: "input", 57 | required: true, 58 | }, 59 | }; 60 | 61 | // TODO: 原以为最好写的模型,tm的调用就卡死 62 | class gpt extends openai.gpt { 63 | constructor(conf: moonshotLLMConf, template: string | prompt) { 64 | super(conf as never, template); 65 | } 66 | } 67 | 68 | export const moonshot = { 69 | info, 70 | gpt, 71 | }; 72 | -------------------------------------------------------------------------------- /src/hooks/useModel/llms/openai.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { llm, llmConf, llmInfo, messageReps, prompt } from "../type"; 3 | import { desc, other } from "../common"; 4 | import { GM_xmlhttpRequest } from "$"; 5 | import { OnStream, request } from "@/utils/request"; 6 | import { logger } from "@/utils/logger"; 7 | 8 | export type openaiLLMConf = llmConf< 9 | "openai", 10 | { 11 | url: string; 12 | raw_url?: boolean; 13 | model: string; 14 | api_key: string; 15 | advanced: { 16 | json?: boolean; 17 | stream?: boolean; 18 | temperature?: number; 19 | top_p?: number; 20 | presence_penalty?: number; 21 | frequency_penalty?: number; 22 | }; 23 | } & other 24 | >; 25 | 26 | const info: llmInfo = { 27 | mode: { 28 | mode: "openai", 29 | label: "ChatGPT", 30 | desc: `gpt-4o效果较好但价格更贵,推荐使用gpt-4o-mini 价格便宜性能好,需要根据自身情况去优化提示词也能达到良好效果`, 31 | icon: ``, 32 | }, 33 | url: { 34 | desc: "可使用中转/代理API,前提是符合openai的规范,只需要填域名", 35 | type: "input", 36 | config: { 37 | placeholder: "https://api.openai.com", 38 | }, 39 | value: "https://api.openai.com", 40 | required: true, 41 | }, 42 | raw_url: { 43 | desc: "需要填写完整api地址使用:如: https://example.cn/v1/chat/completions", 44 | type: "switch", 45 | value: false, 46 | }, 47 | model: { 48 | config: { 49 | placeholder: "gpt-4o-mini", 50 | options: ["gpt-4o-mini", "gpt-3.5-turbo", "gpt-4o", "gpt-4"].map( 51 | (item) => ({ 52 | label: item, 53 | value: item, 54 | }) 55 | ), 56 | allowCreate: true, 57 | filterable: true, 58 | defaultFirstOption: true, 59 | }, 60 | value: "gpt-4o-mini", 61 | type: "select", 62 | required: true, 63 | }, 64 | api_key: { type: "input", required: true }, 65 | advanced: { 66 | label: "高级配置", 67 | alert: "warning", 68 | desc: "小白勿动", 69 | value: { 70 | json: { 71 | label: "json", 72 | value: true, 73 | type: "switch", 74 | desc: "仅支持较新的模型,会强制gpt返回json格式,效果好一点,能有效减少响应解析错误", 75 | }, 76 | stream: { 77 | value: false, 78 | type: "switch", 79 | desc: desc.stream, 80 | }, 81 | temperature: { 82 | value: 0.65, 83 | type: "slider", 84 | config: { 85 | min: 0, 86 | max: 2, 87 | step: 0.05, 88 | }, 89 | desc: "较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定性。
我们通常建议更改此项或 top_p ,但不要同时更改两者。", 90 | }, 91 | top_p: { 92 | value: 1, 93 | type: "slider", 94 | config: { 95 | min: 0, 96 | max: 1, 97 | step: 0.05, 98 | }, 99 | desc: "温度采样的替代方法称为核采样,其中模型考虑具有 top_p 概率质量的标记的结果。因此 0.1 意味着仅考虑包含前 10% 概率质量的标记。
我们通常建议更改此项或 temperature ,但不要同时更改两者。", 100 | }, 101 | presence_penalty: { 102 | value: 0, 103 | type: "slider", 104 | config: { 105 | min: -2, 106 | max: 2, 107 | step: 0.1, 108 | }, 109 | desc: "正值根据新标记是否出现在文本中来对其进行惩罚,从而增加模型讨论新主题的可能性。", 110 | }, 111 | frequency_penalty: { 112 | value: 0, 113 | type: "slider", 114 | config: { 115 | min: -2, 116 | max: 2, 117 | step: 0.1, 118 | }, 119 | desc: "正值根据迄今为止文本中的现有频率对新标记进行惩罚,从而降低模型逐字重复同一行的可能性。", 120 | }, 121 | }, 122 | }, 123 | other, 124 | }; 125 | 126 | class gpt extends llm { 127 | constructor(conf: openaiLLMConf, template: string | prompt) { 128 | super(conf, template); 129 | } 130 | async chat(message: string) { 131 | const res = await this.post({ prompt: this.buildPrompt(message) }); 132 | return res.choices.pop()?.message?.content || ""; 133 | } 134 | async message({ 135 | data = {}, 136 | onPrompt = (s: string) => {}, 137 | onStream = (s: string) => {}, 138 | json = false, 139 | }): Promise { 140 | const prompts = this.buildPrompt(data); 141 | const prompt = prompts[prompts.length - 1].content; 142 | onPrompt(prompt); 143 | const decoder = new TextDecoder("utf-8"); 144 | let stream = ""; 145 | const ans: messageReps = { prompt }; 146 | const res = await this.post({ 147 | prompt: prompts, 148 | json, 149 | onStream: async (reader) => { 150 | // TODO: 处理 stream 输出 151 | }, 152 | }); 153 | if (!this.conf.advanced.stream) { 154 | ans.content = res?.choices.pop()?.message?.content; 155 | ans.usage = { 156 | input_tokens: res?.usage?.prompt_tokens, 157 | output_tokens: res?.usage?.completion_tokens, 158 | total_tokens: res?.usage?.total_tokens, 159 | }; 160 | } else { 161 | ans.content = stream; 162 | } 163 | return ans; 164 | } 165 | private async post({ 166 | prompt, 167 | onStream, 168 | json = false, 169 | }: { 170 | prompt: prompt; 171 | onStream?: OnStream; 172 | json?: boolean; 173 | }): Promise { 174 | const res = await request.post({ 175 | // 兼容特殊的api中转站 176 | url: this.conf.url + (this.conf.raw_url ? "" : "/v1/chat/completions"), 177 | data: JSON.stringify({ 178 | messages: prompt, 179 | model: this.conf.model, 180 | stream: this.conf.advanced.stream, 181 | temperature: this.conf.advanced.temperature, 182 | top_p: this.conf.advanced.top_p, 183 | presence_penalty: this.conf.advanced.presence_penalty, 184 | frequency_penalty: this.conf.advanced.frequency_penalty, 185 | response_format: 186 | this.conf.advanced.json && json ? { type: "json_object" } : undefined, 187 | }), 188 | headers: { 189 | Authorization: `Bearer ${this.conf.api_key}`, 190 | "Content-Type": "application/json", 191 | }, 192 | timeout: this.conf.other.timeout, 193 | // TODO: 暂时禁用 stream 输出 194 | responseType: false && this.conf.advanced.stream ? "stream" : "json", 195 | onStream, 196 | isFetch: true, 197 | }); 198 | 199 | return res; 200 | } 201 | } 202 | 203 | export const openai = { 204 | gpt, 205 | info, 206 | }; 207 | -------------------------------------------------------------------------------- /src/hooks/useModel/llms/user.ts: -------------------------------------------------------------------------------- 1 | import { miTem } from "mitem"; 2 | import { modelData } from ".."; 3 | import { llmConf, llmInfo } from "../type"; 4 | 5 | export type userLLMConf = llmConf< 6 | "user", 7 | { 8 | url: string; 9 | header: string; 10 | data: string; 11 | req: string; 12 | } 13 | >; 14 | 15 | const info: llmInfo = { 16 | mode: { 17 | mode: "user", 18 | label: "自定义", 19 | disabled: true, 20 | icon: ``, 21 | desc: "通过模板语言来手动构建请求,获取数据,门槛较高,如果模型不是小众,且效果良好,价格便宜,可联系作者适配", 22 | }, 23 | url: { 24 | type: "input", 25 | required: true, 26 | }, 27 | header: { type: "input", required: true }, 28 | data: { type: "input", required: true }, 29 | req: { type: "input", required: true }, 30 | }; 31 | // export const send = (m: conf) => { 32 | // const template = miTem.compile(m.data.data); 33 | // const msg = template({ 34 | // message: JSON.stringify(message).replace(/^(\s|")+|(\s|")+$/g, ""), 35 | // raw: JSON.stringify(message), 36 | // }); 37 | // const req = await axios.post(m.data.url, JSON.parse(msg), { 38 | // headers: m.data.header ? JSON.parse(m.data.header) : undefined, 39 | // timeout, 40 | // }); 41 | // if (m.data.mode === "自定义") { 42 | // const reqTemplate = miTem.compile(`{{${m.data.req}}}`); 43 | // return reqTemplate(req); 44 | // } 45 | // }; 46 | export const user = { 47 | info, 48 | }; 49 | -------------------------------------------------------------------------------- /src/hooks/useModel/type.ts: -------------------------------------------------------------------------------- 1 | import { miTem } from "mitem"; 2 | import { 3 | ElInput, 4 | ElInputNumber, 5 | ElSelectV2, 6 | ElSlider, 7 | ElSwitch, 8 | } from "element-plus"; 9 | 10 | export type llmMessageData = { 11 | data?: JobListData; 12 | boos?: BoosData; 13 | card?: JobCard; 14 | }; 15 | 16 | export type llmMessageArgs = { 17 | data: llmMessageData; 18 | onPrompt?: (s: string) => void; 19 | onStream?: (s: string) => void; 20 | json?: boolean; 21 | }; 22 | 23 | export abstract class llm = Array> { 24 | conf: C; 25 | tem: (object: any) => string; 26 | template: string | prompt; 27 | constructor(conf: C, template: string | prompt) { 28 | this.conf = conf; 29 | this.template = template; 30 | 31 | if (typeof template === "string") { 32 | this.tem = miTem.compile(template); 33 | } else { 34 | if (template.length === 0) { 35 | throw new Error("多对话提示词不能为空"); 36 | } 37 | this.tem = miTem.compile(template[template.length - 1].content); 38 | } 39 | } 40 | buildPrompt(data: object | string): prompt { 41 | if (typeof data === "string") { 42 | return [ 43 | { 44 | content: data, 45 | role: "user", 46 | }, 47 | ]; 48 | } else if (Array.isArray(this.template)) { 49 | const temp = this.template; 50 | temp[temp.length - 1].content = this.tem(data); 51 | return temp; 52 | } else { 53 | return [ 54 | { 55 | content: this.tem(data), 56 | role: "user", 57 | }, 58 | ]; 59 | } 60 | } 61 | abstract chat(message: string): Promise; 62 | abstract message(args: llmMessageArgs): Promise; 63 | } 64 | 65 | export type messageReps = { 66 | content?: T; 67 | prompt?: string; 68 | usage?: { total_tokens: number; input_tokens: number; output_tokens: number }; 69 | }; 70 | export type llmConf = { mode: M } & T; 71 | 72 | export type formElm = 73 | | { type: "input"; config?: InstanceType["$props"] } 74 | | { 75 | type: "inputNumber"; 76 | config?: InstanceType["$props"]; 77 | } 78 | | { type: "slider"; config?: InstanceType["$props"] } 79 | | { type: "switch"; config?: InstanceType["$props"] } 80 | | { type: "select"; config?: InstanceType["$props"] }; 81 | 82 | export type llmInfoVal = T extends Record 83 | ? { 84 | value: llmInfo>; 85 | label?: string; 86 | desc?: string; 87 | alert: "success" | "warning" | "info" | "error"; 88 | } 89 | : { 90 | value?: T; 91 | label?: string; 92 | desc?: string; 93 | } & formElm & { [K in keyof R]: R[K] }; 94 | 95 | export type llmInfo> = { 96 | [K in keyof T]-?: K extends "mode" 97 | ? { 98 | mode: T[K]; 99 | label?: string; 100 | icon?: string; 101 | desc?: string; 102 | disabled?: boolean; 103 | } 104 | : llmInfoVal; 105 | }; 106 | 107 | export type prompt = Array<{ 108 | role: "system" | "user" | "assistant"; 109 | content: string; 110 | }>; 111 | -------------------------------------------------------------------------------- /src/hooks/useStatistics.ts: -------------------------------------------------------------------------------- 1 | import { GM_getValue, GM_setValue } from "$"; 2 | import { Statistics } from "@/types/formData"; 3 | import { getCurDay } from "@/utils"; 4 | import { reactiveComputed, watchThrottled } from "@vueuse/core"; 5 | import { todayKey, statisticsKey } from "./useConfForm"; 6 | import { logger } from "@/utils/logger"; 7 | 8 | const todayData = reactiveComputed(() => { 9 | const date = getCurDay(); 10 | const current = { 11 | date, 12 | success: 0, 13 | total: 0, 14 | company: 0, 15 | jobTitle: 0, 16 | jobContent: 0, 17 | hrPosition: 0, 18 | salaryRange: 0, 19 | companySizeRange: 0, 20 | activityFilter: 0, 21 | goldHunterFilter: 0, 22 | repeat: 0, 23 | }; 24 | const g = GM_getValue(todayKey, current); 25 | logger.debug("统计数据:", g); 26 | 27 | if (g.date === date) { 28 | return g; 29 | } 30 | const statistics = GM_getValue(statisticsKey, []); 31 | GM_setValue(statisticsKey, [g, ...statistics]); 32 | GM_setValue(todayKey, current); 33 | return current; 34 | }); 35 | 36 | const statisticsData = GM_getValue(statisticsKey, []); 37 | 38 | watchThrottled( 39 | todayData, 40 | (v) => { 41 | GM_setValue(todayKey, v); 42 | }, 43 | { throttle: 200 } 44 | ); 45 | 46 | export const useStatistics = () => { 47 | return { 48 | todayData, 49 | statisticsData, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/hooks/useStore.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { getRootVue, useHookVueData } from "./useVue"; 3 | import { logger } from "@/utils/logger"; 4 | import { unsafeWindow } from "$"; 5 | 6 | const userInfo = ref<{ 7 | userId: number; 8 | identity: number; 9 | encryptUserId: string; 10 | name: string; 11 | showName: string; 12 | tinyAvatar: string; 13 | largeAvatar: string; 14 | token: string; 15 | isHunter: boolean; 16 | clientIP: string; 17 | email: any; 18 | phone: any; 19 | brandName: any; 20 | doubleIdentity: boolean; 21 | recruit: boolean; 22 | agentRecruit: boolean; 23 | industryCostTag: number; 24 | gender: number; 25 | trueMan: boolean; 26 | studentFlag: boolean; 27 | completeDayStatus: boolean; 28 | complete: boolean; 29 | multiExpect: boolean; 30 | }>(); 31 | 32 | const storeInit = async () => { 33 | const v = await getRootVue(); 34 | const store = v?.$store?.state; 35 | userInfo.value = store?.userInfo; 36 | logger.debug("userInfo: ", userInfo.value); 37 | }; 38 | 39 | export const useStore = () => { 40 | return { 41 | storeInit, 42 | userInfo, 43 | }; 44 | }; 45 | export const useUserId = () => { 46 | return ( 47 | userInfo.value?.userId || 48 | unsafeWindow?._PAGE?.uid || 49 | unsafeWindow?._PAGE?.userId 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/hooks/useVue.ts: -------------------------------------------------------------------------------- 1 | import elmGetter from "@/utils/elmGetter"; 2 | import { ref, Ref } from "vue"; 3 | 4 | const rootVue = ref(); 5 | 6 | export const getRootVue = async () => { 7 | if (rootVue.value) return rootVue.value; 8 | 9 | let wrap = await elmGetter.get("#wrap"); 10 | 11 | if (wrap.__vue__) rootVue.value = wrap.__vue__; 12 | else { 13 | // ElMessage.error("未找到vue根组件"); 14 | throw new Error("未找到vue根组件"); 15 | } 16 | return rootVue.value; 17 | }; 18 | export const useHookVueData = ( 19 | selectors: string, 20 | key: string, 21 | data: Ref 22 | ) => { 23 | return () => { 24 | const jobVue = document.querySelector(selectors).__vue__; 25 | data.value = jobVue[key]; 26 | let originalSet = jobVue.__lookupSetter__(key); 27 | Object.defineProperty(jobVue, key, { 28 | set(val) { 29 | data.value = val; 30 | originalSet.call(this, val); 31 | }, 32 | }); 33 | }; 34 | }; 35 | export const useHookVueFn = (selectors: string, key: string) => { 36 | return () => { 37 | const jobVue = document.querySelector(selectors).__vue__; 38 | return jobVue[key]; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/hooks/useWebSocket/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./protobuf"; 2 | -------------------------------------------------------------------------------- /src/hooks/useWebSocket/mqtt.ts: -------------------------------------------------------------------------------- 1 | // 因为window.ChatWebsocket的出现,暂时用不到了 2 | 3 | import { logger } from "@/utils/logger"; 4 | 5 | export function encodeLength(len: number) { 6 | const output = []; 7 | let x = len; 8 | do { 9 | let encodedByte = x % 128; 10 | x = Math.floor(x / 128); 11 | if (x > 0) { 12 | encodedByte |= 128; 13 | } 14 | output.push(encodedByte); 15 | } while (x > 0); 16 | return output; 17 | } 18 | 19 | export interface UTF8Encoder { 20 | encode: (str: string) => Uint8Array; 21 | } 22 | export interface UTF8Decoder { 23 | decode: (bytes: Uint8Array) => string; 24 | } 25 | export function encodeUTF8String(str: string, encoder: UTF8Encoder) { 26 | const bytes = encoder.encode(str); 27 | return [bytes.length >> 8, bytes.length & 0xff, ...bytes]; 28 | } 29 | export function decodeUTF8String( 30 | buffer: Uint8Array, 31 | startIndex: number, 32 | utf8Decoder: UTF8Decoder 33 | ) { 34 | const bytes = decodeUint8Array(buffer, startIndex); 35 | if (bytes === undefined) { 36 | return undefined; 37 | } 38 | const value = utf8Decoder.decode(bytes); 39 | 40 | return { 41 | length: bytes.length + 2, 42 | value, 43 | }; 44 | } 45 | // https://www.npmjs.com/package/@esutils/mqtt-packet 46 | export const mqtt = { 47 | encode(packet: { 48 | messageId?: number; // only for qos1 and qos2 49 | payload: Uint8Array; 50 | }): Uint8Array { 51 | const utf8 = new TextEncoder(); 52 | const variableHeader = [...encodeUTF8String("chat", utf8)]; 53 | if (packet.messageId) { 54 | variableHeader.push(packet.messageId >> 8, packet.messageId & 0xff); 55 | } 56 | let { payload } = packet; 57 | 58 | if (typeof payload === "string") { 59 | payload = utf8.encode(payload); 60 | } 61 | 62 | const fixedHeader = [ 63 | (3 << 4) | 3, // 0x00110011 qos1消息,非重传、保留消息 64 | ...encodeLength(variableHeader.length + payload.length), 65 | ]; 66 | 67 | return Uint8Array.from([...fixedHeader, ...variableHeader, ...payload]); 68 | }, 69 | decode(buffer: Uint8Array, flags = 3) { 70 | const dup = !!(flags & 8); 71 | const qos = (flags & 6) >> 1; 72 | const { length: remainingLength, bytesUsedToEncodeLength } = decodeLength( 73 | buffer, 74 | 1 75 | ); 76 | const retain = !!(flags & 1); 77 | const utf = new TextDecoder("utf-8"); 78 | const topicStart = bytesUsedToEncodeLength + 1; 79 | const decodedTopic = decodeUTF8String(buffer, topicStart, utf); 80 | if (decodedTopic === undefined) { 81 | throw new Error("Cannot parse topic"); 82 | } 83 | const topic = decodedTopic.value; 84 | 85 | let id = 0; 86 | let payloadStart = topicStart + decodedTopic.length; 87 | 88 | if (qos > 0) { 89 | const idStart = payloadStart; 90 | try { 91 | id = parseMessageId(buffer, idStart); 92 | } catch { 93 | logger.error(`错的id?: `, { 94 | payloadStart, 95 | topicStart, 96 | topic, 97 | dup, 98 | qos, 99 | remainingLength, 100 | retain, 101 | }); 102 | } 103 | payloadStart += 2; 104 | } 105 | 106 | const payload = buffer.subarray(payloadStart); 107 | 108 | const returnPacket = { 109 | topic, 110 | payload, 111 | dup, 112 | retain, 113 | qos, 114 | messageId: id, 115 | }; 116 | return returnPacket; 117 | }, 118 | }; 119 | export function decodeLength(buffer: Uint8Array, startIndex: number) { 120 | let i = startIndex; 121 | let encodedByte = 0; 122 | let value = 0; 123 | let multiplier = 1; 124 | 125 | do { 126 | encodedByte = buffer[i]; 127 | i += 1; 128 | 129 | value += (encodedByte & 127) * multiplier; 130 | 131 | if (multiplier > 128 * 128 * 128) { 132 | throw Error("malformed length"); 133 | } 134 | 135 | multiplier *= 128; 136 | } while ((encodedByte & 128) !== 0); 137 | 138 | return { length: value, bytesUsedToEncodeLength: i - startIndex }; 139 | } 140 | export function parseMessageId(buffer: Uint8Array, startIndex: number): number { 141 | if (startIndex + 2 > buffer.length) { 142 | throw new Error("Cannot parse messageId"); 143 | } 144 | return (buffer[startIndex] << 8) | buffer[startIndex + 1]; 145 | } 146 | export function decodeUint8Array( 147 | buffer: Uint8Array, 148 | startIndex: number 149 | ): Uint8Array | undefined { 150 | if (startIndex >= buffer.length || startIndex + 2 > buffer.length) { 151 | return undefined; 152 | } 153 | const length = (buffer[startIndex] << 8) + buffer[startIndex + 1]; 154 | const bytes = buffer.subarray(startIndex + 2, startIndex + 2 + length); 155 | return bytes; 156 | } 157 | -------------------------------------------------------------------------------- /src/hooks/useWebSocket/protobuf.ts: -------------------------------------------------------------------------------- 1 | import protobuf from "protobufjs"; 2 | import { TechwolfChatProtocol, AwesomeMessage } from "./type"; 3 | import { unsafeWindow } from "$"; 4 | 5 | export class Message { 6 | msg: Uint8Array; 7 | hex: string; 8 | constructor(args: { 9 | form_uid: string; 10 | to_uid: string; 11 | to_name: string; // encryptBossId 擦,是boos的id不是岗位的 12 | content?: string; 13 | image?: string; // url 14 | }) { 15 | const r = new Date().getTime(); 16 | const d = r + 68256432452609; 17 | const data: TechwolfChatProtocol = { 18 | messages: [ 19 | { 20 | from: { 21 | uid: args.form_uid, 22 | source: 0, 23 | }, 24 | to: { 25 | uid: args.to_uid, 26 | name: args.to_name, 27 | source: 0, 28 | }, 29 | type: 1, 30 | mid: d.toString(), 31 | time: r.toString(), 32 | body: { 33 | type: 1, 34 | templateId: 1, 35 | text: args.content, 36 | // image: {}, 37 | }, 38 | cmid: d.toString(), 39 | }, 40 | ], 41 | type: 1, 42 | }; 43 | 44 | this.msg = AwesomeMessage.encode(data).finish().slice(); 45 | this.hex = [...this.msg] 46 | .map((b) => b.toString(16).padStart(2, "0")) 47 | .join(""); 48 | } 49 | toArrayBuffer(): ArrayBuffer { 50 | return this.msg.buffer.slice(0, this.msg.byteLength); 51 | } 52 | send() { 53 | unsafeWindow.ChatWebsocket.send(this); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/hooks/useWebSocket/type.json: -------------------------------------------------------------------------------- 1 | { 2 | "nested": { 3 | "TechwolfUser": { 4 | "fields": { 5 | "uid": { 6 | "type": "string", 7 | "id": 1 8 | }, 9 | "name": { 10 | "type": "string", 11 | "id": 2 12 | }, 13 | "source": { 14 | "type": "int32", 15 | "id": 7 16 | } 17 | } 18 | }, 19 | "TechwolfMessageBody": { 20 | "fields": { 21 | "type": { 22 | "type": "int32", 23 | "id": 1 24 | }, 25 | "templateId": { 26 | "type": "int32", 27 | "id": 2 28 | }, 29 | "headTitle": { 30 | "type": "string", 31 | "id": 11 32 | }, 33 | "text": { 34 | "type": "string", 35 | "id": 3 36 | } 37 | } 38 | }, 39 | "TechwolfMessage": { 40 | "fields": { 41 | "from": { 42 | "type": "TechwolfUser", 43 | "id": 1 44 | }, 45 | "to": { 46 | "type": "TechwolfUser", 47 | "id": 2 48 | }, 49 | "type": { 50 | "type": "int32", 51 | "id": 3 52 | }, 53 | "mid": { 54 | "type": "string", 55 | "id": 4 56 | }, 57 | "time": { 58 | "type": "string", 59 | "id": 5 60 | }, 61 | "body": { 62 | "type": "TechwolfMessageBody", 63 | "id": 6 64 | }, 65 | "cmid": { 66 | "type": "string", 67 | "id": 11 68 | } 69 | } 70 | }, 71 | "TechwolfChatProtocol": { 72 | "fields": { 73 | "type": { 74 | "type": "int32", 75 | "id": 1 76 | }, 77 | "messages": { 78 | "rule": "repeated", 79 | "type": "TechwolfMessage", 80 | "id": 3 81 | } 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/hooks/useWebSocket/type.ts: -------------------------------------------------------------------------------- 1 | import protobuf from "protobufjs"; 2 | 3 | export interface TechwolfUser { 4 | // @int64 5 | uid: string; // 1 6 | name?: string; // 2 7 | // @int32 8 | source?: number; // 7 9 | } 10 | 11 | export interface TechwolfImageInfo { 12 | url: string; // 1 13 | // @int32 14 | width: number; // 1 15 | // @int32 16 | height: number; // 1 17 | } 18 | 19 | export interface TechwolfImage { 20 | // @int64 21 | iid?: number; // 1 测试为空 22 | tinyImage?: TechwolfImageInfo; // 2 23 | originImage?: TechwolfImageInfo; // 3 24 | } 25 | 26 | export interface TechwolfMessageBody { 27 | // @int32 28 | type: number; // 1 29 | // @int32 30 | templateId: number; // 2 31 | headTitle?: string; // 11 32 | text?: string; // 3 33 | image?: TechwolfImage; // 5 34 | } 35 | 36 | export interface TechwolfMessage { 37 | from: TechwolfUser; // 1 38 | to: TechwolfUser; // 2 39 | // @int32 40 | type?: number; // 3 41 | // @int64 42 | mid?: string; // 4 43 | // @int64 44 | time?: string; // 5 45 | body: TechwolfMessageBody; // 6 46 | // @int64 47 | cmid?: string; // 11 48 | } 49 | 50 | export interface TechwolfChatProtocol { 51 | // @int32 52 | type: number; // 1 53 | messages: TechwolfMessage[]; // 3 54 | } 55 | 56 | var Root = protobuf.Root, 57 | Type = protobuf.Type, 58 | Field = protobuf.Field; 59 | 60 | // "double" | "float" | "int32" | "uint32" | "sint32" | 61 | // "fixed32" | "sfixed32" | "int64" | "uint64" | 62 | // "sint64" | "fixed64" | "sfixed64" | "string" | 63 | // "bool" | "bytes" | Object 64 | const root = new Root() 65 | .define("cn.techwolf.boss.chat") 66 | .add( 67 | new Type("TechwolfUser") 68 | .add(new Field("uid", 1, "int64")) 69 | .add(new Field("name", 2, "string", "optional")) 70 | .add(new Field("source", 7, "int32", "optional")) 71 | ) 72 | .add( 73 | new Type("TechwolfImageInfo") 74 | .add(new Field("url", 1, "string")) 75 | .add(new Field("width", 2, "int32")) 76 | .add(new Field("height", 3, "int32")) 77 | ) 78 | .add( 79 | new Type("TechwolfImage") 80 | .add(new Field("iid", 1, "int64", "optional")) 81 | .add(new Field("tinyImage", 2, "TechwolfImageInfo", "optional")) 82 | ) 83 | .add( 84 | new Type("TechwolfMessageBody") 85 | .add(new Field("type", 1, "int32")) 86 | .add(new Field("templateId", 2, "int32", "optional")) 87 | .add(new Field("headTitle", 11, "string")) 88 | .add(new Field("text", 3, "string")) 89 | .add(new Field("image", 5, "TechwolfImage", "optional")) 90 | ) 91 | .add( 92 | new Type("TechwolfMessage") 93 | .add(new Field("from", 1, "TechwolfUser")) 94 | .add(new Field("to", 2, "TechwolfUser")) 95 | .add(new Field("type", 3, "int32")) 96 | .add(new Field("mid", 4, "int64", "optional")) 97 | .add(new Field("time", 5, "int64", "optional")) 98 | .add(new Field("body", 6, "TechwolfMessageBody")) 99 | .add(new Field("cmid", 11, "int64", "optional")) 100 | ) 101 | .add( 102 | new Type("TechwolfChatProtocol") 103 | .add(new Field("type", 1, "int32")) 104 | .add(new Field("messages", 3, "TechwolfMessage", "repeated")) 105 | ); 106 | 107 | export const AwesomeMessage = root.lookupType("TechwolfChatProtocol"); 108 | -------------------------------------------------------------------------------- /src/main.scss: -------------------------------------------------------------------------------- 1 | #boos-helper { 2 | position: fixed; 3 | top: 55px; 4 | right: 10px; 5 | z-index: 999; 6 | } 7 | .el-dropdown .el-avatar { 8 | border: 2px solid #fff; 9 | &:hover { 10 | border: 3px solid #c413e7; 11 | } 12 | } 13 | .el-dropdown-menu__item { 14 | justify-content: center; 15 | } 16 | 17 | ::-webkit-scrollbar { 18 | width: 11px; 19 | height: 11px; 20 | } 21 | 22 | ::-webkit-scrollbar-thumb { 23 | border-radius: 10px; 24 | background-color: #4b5563; 25 | border: 2px solid transparent; 26 | background-clip: padding-box; 27 | } 28 | 29 | #loader { 30 | width: 0; 31 | height: 4.8px; 32 | display: inline-block; 33 | position: relative; 34 | background: #54f98d; 35 | box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); 36 | box-sizing: border-box; 37 | top: -14px; 38 | &::after, 39 | &::before { 40 | content: ""; 41 | width: 10px; 42 | height: 1px; 43 | background: #fff; 44 | position: absolute; 45 | top: 9px; 46 | right: -2px; 47 | opacity: 0; 48 | transform: rotate(-45deg) translateX(0px); 49 | box-sizing: border-box; 50 | animation: coli1 0.3s linear infinite; 51 | } 52 | 53 | &::before { 54 | top: -4px; 55 | transform: rotate(45deg); 56 | animation: coli2 0.3s linear infinite; 57 | } 58 | 59 | @keyframes coli1 { 60 | 0% { 61 | transform: rotate(-45deg) translateX(0px); 62 | opacity: 0.7; 63 | } 64 | 65 | 100% { 66 | transform: rotate(-45deg) translateX(-45px); 67 | opacity: 0; 68 | } 69 | } 70 | 71 | @keyframes coli2 { 72 | 0% { 73 | transform: rotate(45deg) translateX(0px); 74 | opacity: 1; 75 | } 76 | 77 | 100% { 78 | transform: rotate(45deg) translateX(-45px); 79 | opacity: 0.7; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage, ElMessageBox } from "element-plus"; 2 | import "element-plus/dist/index.css"; 3 | import "element-plus/theme-chalk/dark/css-vars.css"; 4 | import "./main.scss"; 5 | import { logger } from "@/utils/logger"; 6 | import App from "./App.vue"; 7 | import { createApp, ref, watch } from "vue"; 8 | import { delay, loader } from "./utils"; 9 | import { getRootVue } from "./hooks/useVue"; 10 | import { GM_getValue } from "$"; 11 | import axios from "axios"; 12 | 13 | logger.debug("初始化"); 14 | let t: NodeJS.Timeout; 15 | // function Hook() { 16 | // logger.info("加载/web/geek/chat页面Hook"); 17 | // const OldSocket = WebSocket; 18 | // window.WebSocket = function (url: string | URL, protocols?: string[]) { 19 | // logger.info("捕获到新Socket,", url); 20 | // let s: WebSocket; 21 | // if ( 22 | // !url.toString().includes("HOOK::") && 23 | // url.toString().includes("chatws") 24 | // ) { 25 | // logger.info("劫持到新的chatSocket,", url); 26 | // const a = new TextDecoder("utf-8"); 27 | // const d = useWebSocket("HOOK::" + url, { 28 | // protocols, 29 | // // immediate:false, 30 | // heartbeat: false, 31 | // autoReconnect: false, 32 | // }); 33 | // s = d.ws.value!; 34 | // const oldSend = s.send.bind(s); 35 | // s.send = (data: Uint8Array) => { 36 | // let udata = new Uint8Array(data); 37 | // try { 38 | // if (udata[0] === 0x33) { 39 | // const ma = mqtt.decode(udata); 40 | // logger.warn("sendMa", data, ma); 41 | // const m = AwesomeMessage.decode(ma.payload); 42 | // AwesomeMessage; 43 | // logger.warn("send", data, JSON.stringify(m)); 44 | // } else throw new Error("不是消息"); 45 | // } catch (e) { 46 | // logger.error("解析失败", data, e); 47 | // } 48 | // oldSend(data); 49 | // }; 50 | // if (window.top) window.top.socket = s; 51 | // else window.socket = s; 52 | // } else { 53 | // s = new OldSocket(url.toString().replace("HOOK::", ""), protocols); 54 | // } 55 | // return s; 56 | // } as never; 57 | // } 58 | 59 | // if (window.top !== window.self) 60 | // Hook(); 61 | 62 | async function main(router: any) { 63 | let module = { 64 | run() { 65 | logger.info("BoosHelper加载成功"); 66 | logger.warn("当前页面无对应hook脚本", router.path); 67 | }, 68 | }; 69 | switch (router.path) { 70 | case "/web/geek/job": 71 | module = await import("./pages/geek/job"); 72 | break; 73 | case "/web/geek/chat": 74 | // module = await import("./pages/web/geek/chat"); 75 | break; 76 | } 77 | module.run(); 78 | const helper = document.querySelector("#boos-helper"); 79 | if (!helper) { 80 | const app = createApp(App); 81 | const appEl = document.createElement("div"); 82 | appEl.id = "boos-helper"; 83 | document.body.append(appEl); 84 | app.mount(appEl); 85 | } 86 | } 87 | 88 | async function start() { 89 | document.documentElement.classList.toggle( 90 | "dark", 91 | GM_getValue("theme-dark", false) 92 | ); 93 | 94 | const v = await getRootVue(); 95 | v.$router.afterHooks.push(main); 96 | main(v.$route); 97 | let axiosLoad: () => void; 98 | axios.interceptors.request.use( 99 | function (config) { 100 | if (config.timeout) { 101 | axiosLoad = loader({ ms: config.timeout, color: "#F79E63" }); 102 | } 103 | return config; 104 | }, 105 | function (error) { 106 | if (axiosLoad) axiosLoad(); 107 | return Promise.reject(error); 108 | } 109 | ); 110 | axios.interceptors.response.use( 111 | function (response) { 112 | if (axiosLoad) axiosLoad(); 113 | return response; 114 | }, 115 | function (error) { 116 | if (axiosLoad) axiosLoad(); 117 | return Promise.reject(error); 118 | } 119 | ); 120 | } 121 | 122 | logger.debug("开始运行"); 123 | start(); 124 | 125 | declare global { 126 | interface Window { 127 | socket: WebSocket; 128 | ChatWebsocket: { 129 | send(e: { toArrayBuffer(): ArrayBuffer }): void; 130 | }; 131 | _PAGE: { 132 | isGeekChat: boolean; 133 | // zp_token: string; 7.18 寄! 134 | userId: number; 135 | identity: number; 136 | encryptUserId: string; 137 | name: string; 138 | showName: string; 139 | tinyAvatar: string; 140 | largeAvatar: string; 141 | token: string; 142 | isHunter: boolean; 143 | clientIP: string; 144 | email: any; 145 | phone: any; 146 | brandName: any; 147 | doubleIdentity: boolean; 148 | recruit: boolean; 149 | agentRecruit: boolean; 150 | industryCostTag: number; 151 | gender: number; 152 | trueMan: boolean; 153 | studentFlag: boolean; 154 | completeDayStatus: boolean; 155 | complete: boolean; 156 | multiExpect: boolean; 157 | uid: number; 158 | }; 159 | Cookie: { 160 | get(key: string): string; 161 | }; 162 | parseGptJson: (json: any) => any; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/pages/geek/job/about.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/pages/geek/job/ai.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 87 | 88 | 96 | -------------------------------------------------------------------------------- /src/pages/geek/job/card.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 120 | 121 | 337 | -------------------------------------------------------------------------------- /src/pages/geek/job/config.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 189 | 190 | 195 | -------------------------------------------------------------------------------- /src/pages/geek/job/hooks/useDeliver.ts: -------------------------------------------------------------------------------- 1 | import { delay, notification } from "@/utils"; 2 | import { logData, useLog } from "@/hooks/useLog"; 3 | import { useCommon } from "@/hooks/useCommon"; 4 | import { useStatistics } from "@/hooks/useStatistics"; 5 | 6 | import { ref } from "vue"; 7 | import { createHandle, sendPublishReq } from "@/hooks/useApplying"; 8 | import { Actions } from "@/hooks/useMap"; 9 | import { logger } from "@/utils/logger"; 10 | import { useConfFormData } from "@/hooks/useConfForm"; 11 | import { ElMessage } from "element-plus"; 12 | 13 | const total = ref(0); 14 | const current = ref(0); 15 | const log = useLog(); 16 | const { todayData } = useStatistics(); 17 | const { deliverStop } = useCommon(); 18 | const { formData } = useConfFormData(); 19 | async function jobListHandle( 20 | jobList: JobList, 21 | jobMap: Actions< 22 | string, 23 | { 24 | state: string; 25 | msg: string; 26 | } 27 | > 28 | ) { 29 | log.info("获取岗位", `本次获取到 ${jobList.length} 个`); 30 | total.value = jobList.length; 31 | const h = createHandle(); 32 | jobList.forEach((v) => { 33 | if (!jobMap.has(v.encryptJobId)) 34 | jobMap.set(v.encryptJobId, { 35 | state: "wait", 36 | msg: "等待中", 37 | }); 38 | }); 39 | for (const [index, data] of jobList.entries()) { 40 | current.value = index; 41 | if (deliverStop.value) { 42 | log.info("暂停投递", `剩余 ${jobList.length - index} 个未处理`); 43 | return; 44 | } 45 | if (jobMap.get(data.encryptJobId)?.state !== "wait") continue; 46 | 47 | try { 48 | jobMap.set(data.encryptJobId, { 49 | state: "running", 50 | msg: "处理中", 51 | }); 52 | const ctx: logData = { listData: JSON.parse(JSON.stringify(data)) }; 53 | try { 54 | await h.before({ data }, ctx); 55 | await sendPublishReq(data); 56 | await h.after({ data }, ctx); 57 | log.add(data.jobName, null, ctx, ctx.message); 58 | todayData.success++; 59 | jobMap.set(data.encryptJobId, { 60 | state: "success", 61 | msg: "投递成功", 62 | }); 63 | logger.warn("成功", ctx); 64 | ctx.state = "成功"; 65 | if (todayData.success >= formData.deliveryLimit.value) { 66 | if (formData.notification.value) { 67 | notification(`投递到达上限 ${formData.deliveryLimit.value},已暂停投递`); 68 | } else { 69 | ElMessage.info(`投递到达上限 ${formData.deliveryLimit.value},已暂停投递`); 70 | } 71 | deliverStop.value = true; 72 | return; 73 | } 74 | } catch (e: any) { 75 | jobMap.set(data.encryptJobId, { 76 | state: e.state === "warning" ? "warn" : "error", 77 | msg: e.name || "没有消息", 78 | }); 79 | log.add(data.jobName, e, ctx); 80 | logger.warn("过滤", ctx); 81 | ctx.state = "过滤"; 82 | ctx.err = e.message || ""; 83 | } finally { 84 | await h.record(ctx); 85 | } 86 | } catch (e) { 87 | jobMap.set(data.encryptJobId, { 88 | state: "error", 89 | msg: "未知报错", 90 | }); 91 | logger.error("未知报错", e, data); 92 | if (formData.notification.value) { 93 | notification("未知报错"); 94 | } 95 | } finally { 96 | todayData.total++; 97 | await delay(formData.delay.deliveryInterval); 98 | } 99 | } 100 | } 101 | 102 | export const useDeliver = () => { 103 | return { 104 | createHandle, 105 | jobListHandle, 106 | total, 107 | current, 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /src/pages/geek/job/hooks/useJobList.ts: -------------------------------------------------------------------------------- 1 | import { useHookVueData } from "@/hooks/useVue"; 2 | import { useMap } from "@/hooks/useMap"; 3 | import { ref } from "vue"; 4 | 5 | const jobList = ref([]); 6 | const jobMap = useMap(); // encryptJobId 7 | const init = useHookVueData("#wrap .page-job-wrapper", "jobList", jobList); 8 | 9 | export const useJobList = () => { 10 | return { 11 | jobList, 12 | jobMap, 13 | initJobList: init, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/pages/geek/job/hooks/usePager.ts: -------------------------------------------------------------------------------- 1 | import { useHookVueData, useHookVueFn } from "@/hooks/useVue"; 2 | import { ref } from "vue"; 3 | 4 | const page = ref({ page: 1, pageSize: 30 }); 5 | const pageChange = ref((v: number) => {}); 6 | 7 | const initPage = useHookVueData("#wrap .page-job-wrapper", "pageVo", page); 8 | const initChange = useHookVueFn("#wrap .page-job-wrapper", "pageChangeAction"); 9 | 10 | const next = () => { 11 | if (page.value.page >= 10) return; 12 | pageChange.value(page.value.page + 1); 13 | }; 14 | const prev = () => { 15 | if (page.value.page <= 1) return; 16 | pageChange.value(page.value.page - 1); 17 | }; 18 | 19 | export const usePager = () => { 20 | return { 21 | page, 22 | pageChange, 23 | next, 24 | prev, 25 | initPager: () => { 26 | initPage(); 27 | pageChange.value = initChange(); 28 | }, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/pages/geek/job/index.scss: -------------------------------------------------------------------------------- 1 | #wrap { 2 | min-width: unset; 3 | .inner { 4 | width: unset; 5 | } 6 | .page-job-wrapper { 7 | padding-top: 0 !important; 8 | width: 100%; 9 | position: relative; 10 | 11 | .page-job-inner, 12 | .job-search-wrapper { 13 | width: 65%; 14 | max-width: 870px; 15 | min-width: 320px; 16 | margin: 20px auto 20px 30%; 17 | &.fix-top { 18 | position: unset; 19 | margin-top: unset; 20 | box-shadow: unset; 21 | } 22 | } 23 | } 24 | .page-job-content, 25 | .job-list-wrapper, 26 | .job-card-wrapper { 27 | width: 100% !important; 28 | } 29 | .page-job-inner .page-job-content { 30 | display: flex; 31 | flex-direction: column; 32 | order: 2; 33 | } 34 | .job-card-wrapper { 35 | border: 3px solid transparent; 36 | .job-card-footer, 37 | .job-card-right, 38 | .job-card-body { 39 | display: flex; 40 | } 41 | .job-card-body { 42 | border-radius: 12px 12px 0 0; 43 | } 44 | 45 | .job-card-left .job-title, 46 | .salary, 47 | .job-card-right .company-name { 48 | font-size: clamp(0.625rem, 0.407rem + 1.09vw, 1rem) !important; 49 | } 50 | .tag-list li, 51 | .company-tag-list li, 52 | .info-desc { 53 | font-size: clamp(0.531rem, 0.368rem + 0.82vw, 0.813rem) !important; 54 | } 55 | .job-card-left { 56 | height: unset; 57 | padding: 16px 24px 12px; 58 | .job-name { 59 | margin-right: 12px; 60 | } 61 | .job-area-wrapper { 62 | margin-left: 0 !important; 63 | } 64 | .start-chat-btn, 65 | .info-public { 66 | display: inline-block; 67 | } 68 | .job-info { 69 | height: unset; 70 | overflow: unset; 71 | > * { 72 | margin: 3px; 73 | } 74 | } 75 | } 76 | .job-card-right { 77 | flex-wrap: wrap; 78 | .company-logo { 79 | margin-right: 12px; 80 | width: unset; 81 | height: unset; 82 | border: unset; 83 | border-radius: 15px; 84 | img { 85 | object-fit: contain; 86 | width: clamp(4.063rem, 3.699rem + 1.82vw, 4.688rem); 87 | } 88 | } 89 | .company-info { 90 | margin-left: 0; 91 | } 92 | } 93 | .job-card-footer { 94 | padding: 8px 12px 14px 12px; 95 | } 96 | 97 | .job-card-left .tag-list, 98 | .company-tag-list { 99 | height: unset; 100 | border: unset; 101 | } 102 | } 103 | 104 | .search-job-result .job-list-box { 105 | display: flex; 106 | flex-direction: column; 107 | 108 | .job-card-wrapper { 109 | margin: 16px auto; 110 | } 111 | } 112 | .job-search-box .job-search-form { 113 | width: 100% !important; 114 | display: flex; 115 | .city-label, 116 | .search-input-box { 117 | width: unset; 118 | } 119 | .search-input-box { 120 | flex: 1; 121 | } 122 | .search-btn { 123 | margin: 0 15px; 124 | } 125 | } 126 | } 127 | 128 | html { 129 | --body-bg-color: #f6f6f8; 130 | body { 131 | background-color: var(--body-bg-color); 132 | } 133 | .page-job:before { 134 | background: unset; 135 | } 136 | } 137 | 138 | .el-input .el-input__inner { 139 | background-color: unset !important; 140 | border: unset !important; 141 | } 142 | html.dark { 143 | --el-bg-color: #212020; 144 | --body-bg-color: #212121; 145 | 146 | #header .inner:before, 147 | .page-job:before { 148 | background: unset; 149 | } 150 | .job-search-wrapper, 151 | .job-card-wrapper, 152 | .satisfaction-feedback-wrapper, 153 | .job-search-box .city-label, 154 | .job-search-box .search-input-box, 155 | .job-search-box .search-input-box input, 156 | .hot-link-wrapper, 157 | .filter-select-dropdown li { 158 | background-color: #292929 !important; 159 | } 160 | .filter-select-dropdown { 161 | &, 162 | ul, 163 | .condition-position-detail { 164 | background-color: #292929 !important; 165 | border: 1px solid #5a5a5a !important; 166 | } 167 | * { 168 | color: #cfd3dc !important; 169 | } 170 | .active { 171 | color: #00a6a7 !important; 172 | } 173 | } 174 | .job-title, 175 | .info-desc, 176 | .tag-list li, 177 | .company-name a, 178 | .satisfaction-feedback-wrapper h3, 179 | .fast-next-btn, 180 | .search-map-btn, 181 | .city-label, 182 | .city-area-select .area-dropdown-item li, 183 | .city-area-select .city-area-tab li, 184 | .subway-select-wrapper .subway-line-list li, 185 | .condition-filter-select .current-select, 186 | .el-vl__wrapper, 187 | .el-checkbox__label, 188 | .placeholder-text, 189 | #boos-helper-job h2 { 190 | color: #cfd3dc !important; 191 | } 192 | .city-area-select .area-select-wrapper, 193 | .condition-filter-select, 194 | .condition-position-select.is-select .current-select, 195 | .job-card-body, 196 | .condition-industry-select { 197 | background-color: #434141; 198 | } 199 | .job-card-wrapper { 200 | transition: all 0.3s ease; 201 | position: relative; 202 | .job-card-footer { 203 | background: linear-gradient(90deg, #373737, #4d4b4b); 204 | } 205 | .job-card-left .start-chat-btn { 206 | background: rgb(0 190 189 / 70%); 207 | } 208 | .job-info .tag-list li, 209 | .info-public, 210 | .company-tag-list li { 211 | color: #cfd3dc !important; 212 | background: #44e1e326 !important; 213 | border: 0.5px solid #e5e6e678 !important; 214 | } 215 | .info-public em:before { 216 | height: 70%; 217 | } 218 | } 219 | .job-loading-wrapper .job-loading-list li { 220 | filter: invert(83%); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/pages/geek/job/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@/utils/logger"; 2 | import { createApp } from "vue"; 3 | import uiVue from "./ui.vue"; 4 | import elmGetter from "@/utils/elmGetter"; 5 | import "./index.scss"; 6 | 7 | async function mountVue() { 8 | const jobSearchWrapper = await elmGetter.get(".job-search-wrapper"); 9 | if (document.querySelector("#boos-helper-job")) { 10 | return; 11 | } 12 | const app = createApp(uiVue); 13 | const jobEl = document.createElement("div"); 14 | jobEl.id = "boos-helper-job"; 15 | jobSearchWrapper.insertBefore(jobEl, jobSearchWrapper.firstElementChild); 16 | jobSearchWrapper.setAttribute("help", "出界了哇!"); 17 | app.mount(jobEl); 18 | } 19 | 20 | function removeAd() { 21 | // 新职位发布时通知我 22 | elmGetter.rm(".job-list-wrapper .subscribe-weixin-wrapper"); 23 | // 侧栏 24 | elmGetter.rm(".job-side-wrapper"); 25 | // 侧边悬浮框 26 | elmGetter.rm(".side-bar-box"); 27 | // 搜索栏登录框 28 | elmGetter.rm(".go-login-btn"); 29 | // 底部页脚 30 | // elmGetter.rm("#footer-wrapper"); 31 | } 32 | 33 | export async function run() { 34 | logger.info("加载/web/geek/job页面Hook"); 35 | removeAd(); 36 | mountVue(); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/geek/job/logs.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | 37 | 48 | -------------------------------------------------------------------------------- /src/pages/geek/job/statistics.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /src/pages/geek/job/ui.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 209 | 210 | 255 | -------------------------------------------------------------------------------- /src/types/boosData.d.ts: -------------------------------------------------------------------------------- 1 | interface BoosData { 2 | data: Data; 3 | job: Job; 4 | } 5 | 6 | interface Data { 7 | bossFreezeStatus: number; 8 | companyName: string; 9 | tinyUrl: string; 10 | title: string; 11 | mobileVisible: number; 12 | encryptJobId: string; 13 | regionCode: string; 14 | bothTalked: boolean; 15 | encryptBossId: string; 16 | warningTips: any[]; 17 | bossId: number; 18 | weixinVisible: number; 19 | mobile: string; 20 | securityId: string; 21 | bossPreFreezeStatus: number; 22 | weixin: string; 23 | curTime: number; 24 | isTop: number; 25 | name: string; 26 | hasInterview: boolean; 27 | bossSource: number; 28 | } 29 | 30 | interface Job { 31 | jobName: string; 32 | salaryDesc: string; 33 | brandName: string; 34 | locationName: string; 35 | proxyJob: number; 36 | proxyType: number; 37 | jobSource: number; 38 | degreeName: string; 39 | lowSalary: number; 40 | highSalary: number; 41 | experienceName: string; 42 | } 43 | -------------------------------------------------------------------------------- /src/types/deliverError.ts: -------------------------------------------------------------------------------- 1 | export const errMap = new Map(); 2 | function createCustomError( 3 | name: string, 4 | state = "warning" as "warning" | "danger" 5 | ) { 6 | errMap.set(name, true); 7 | return class CustomError extends Error { 8 | static message: any; 9 | state: "warning" | "danger"; 10 | constructor( 11 | message: string 12 | // cause?: any 13 | // options?: ErrorOptions 14 | ) { 15 | super(message); 16 | this.name = name; 17 | this.state = state; 18 | Object.setPrototypeOf(this, CustomError.prototype); 19 | } 20 | }; 21 | } 22 | export const RepeatError = createCustomError("重复沟通"); 23 | export type RepeatError = InstanceType; 24 | export const JobTitleError = createCustomError("岗位名筛选"); 25 | export type JobTitleError = InstanceType; 26 | export const CompanyNameError = createCustomError("公司名筛选"); 27 | export type CompanyNameError = InstanceType; 28 | export const SalaryError = createCustomError("薪资筛选"); 29 | export type SalaryError = InstanceType; 30 | export const CompanySizeError = createCustomError("公司规模筛选"); 31 | export type CompanySizeError = InstanceType; 32 | export const JobDescriptionError = createCustomError("工作内容筛选"); 33 | export type JobDescriptionError = InstanceType; 34 | export const HrPositionError = createCustomError("Hr职位筛选"); 35 | export type HrPositionError = InstanceType; 36 | export const AIFilteringError = createCustomError("AI筛选"); 37 | export type AIFilteringError = InstanceType; 38 | export const FriendStatusError = createCustomError("好友状态"); 39 | export type FriendStatusError = InstanceType; 40 | export const ActivityError = createCustomError("活跃度过滤"); 41 | export type ActivityError = InstanceType; 42 | export const GoldHunterError = createCustomError("猎头过滤"); 43 | export type GoldHunterError = InstanceType; 44 | export const UnknownError = createCustomError("未知错误", "danger"); 45 | export type UnknownError = InstanceType; 46 | export const PublishError = createCustomError("投递出错", "danger"); 47 | export type PublishError = InstanceType; 48 | export const GreetError = createCustomError("打招呼出错", "danger"); 49 | export type GreetError = InstanceType; 50 | -------------------------------------------------------------------------------- /src/types/formData.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from "@/hooks/useModel/type"; 2 | 3 | export interface Statistics { 4 | date: string; 5 | success: number; 6 | total: number; 7 | company: number; 8 | jobTitle: number; 9 | jobContent: number; 10 | hrPosition: number; 11 | salaryRange: number; 12 | companySizeRange: number; 13 | activityFilter: number; 14 | goldHunterFilter: number; 15 | repeat: number; 16 | } 17 | 18 | export interface FormData { 19 | company: FormDataSelect; 20 | jobTitle: FormDataSelect; 21 | jobContent: FormDataSelect; 22 | hrPosition: FormDataSelect; 23 | salaryRange: FormDataInput; 24 | companySizeRange: FormDataInput; 25 | customGreeting: FormDataInput; 26 | deliveryLimit: FormDataInputNumber; 27 | greetingVariable: FormDataCheckbox; 28 | activityFilter: FormDataCheckbox; 29 | friendStatus: FormDataCheckbox; 30 | goldHunterFilter: FormDataCheckbox; 31 | notification: FormDataCheckbox; 32 | aiGreeting: FormDataAi; 33 | aiFiltering: FormDataAi; 34 | aiReply: FormDataAi; 35 | record: { model?: string[]; enable: boolean }; 36 | // animation?: "frame" | "card" | "together"; 37 | delay: ConfDelay; 38 | } 39 | 40 | export type FormInfoData = { 41 | [key in keyof Omit]: { 42 | label: string; 43 | help?: string; 44 | }; 45 | } & { 46 | aiGreeting: FormInfoAi; 47 | aiFiltering: FormInfoAi; 48 | delay: ConfInfoDelay; 49 | }; 50 | 51 | export type FormInfoAi = { 52 | label: string; 53 | help?: string; 54 | example: [string, prompt]; 55 | }; 56 | 57 | export interface FormDataSelect { 58 | include: boolean; 59 | value: string[]; 60 | options: string[]; 61 | enable: boolean; 62 | } 63 | 64 | export interface FormDataInput { 65 | value: string; 66 | enable: boolean; 67 | } 68 | 69 | export interface FormDataInputNumber { 70 | value: number; 71 | } 72 | 73 | export interface FormDataCheckbox { 74 | value: boolean; 75 | } 76 | 77 | export interface FormDataAi { 78 | model?: string; 79 | prompt: string | prompt; 80 | enable: boolean; 81 | } 82 | 83 | type ConfDelay = { 84 | deliveryStarts: number; 85 | deliveryInterval: number; 86 | deliveryPageNext: number; 87 | messageSending: number; 88 | }; 89 | 90 | type ConfInfoDelay = { 91 | [Key in keyof ConfDelay]: { 92 | label: string; 93 | help?: string; 94 | disable?: boolean; 95 | }; 96 | }; 97 | -------------------------------------------------------------------------------- /src/types/jobData.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 职位信息,在模板中通常使用 data 获取 3 | * 如 {{ data.jobName }} 获取工作名称 4 | */ 5 | interface JobListData { 6 | /** 安全标识符,例如:'MkH058uX...' */ 7 | securityId: string; 8 | /** 招聘者头像的URL,例如:'https://img.bosszhipin.com/boss/avatar.png' */ 9 | bossAvatar: string; 10 | /** 招聘者认证等级,例如:3 */ 11 | bossCert: number; 12 | /** 加密后的招聘者ID,例如:'4e3d3f84...' */ 13 | encryptBossId: string; 14 | /** 招聘者姓名,例如:'周靖丰' */ 15 | bossName: string; 16 | /** 招聘者职称,例如:'招聘者' */ 17 | bossTitle: string; 18 | /** 是否是金牌猎头(0为否,1为是),例如:0 */ 19 | goldHunter: number; 20 | /** 招聘者是否在线,例如:false */ 21 | bossOnline: boolean; 22 | /** 加密后的工作ID,例如:'ee226d37...' */ 23 | encryptJobId: string; 24 | /** 预期ID,可能用于追踪求职者的应聘活动,例如:89300700003 */ 25 | expectId: number; 26 | /** 工作名称,例如:'PHP' */ 27 | jobName: string; 28 | /** 日志ID,可能用于追踪或日志系统,例如:'6VzQgg0000...' */ 29 | lid: string; 30 | /** 薪资描述,例如:'8-13K' */ 31 | salaryDesc: string; 32 | /** 工作标签,如经验和学历要求,例如:['1-3年', '学历不限'] */ 33 | jobLabels: string[]; 34 | /** 工作有效状态,例如:1 */ 35 | jobValidStatus: number; 36 | /** 图标文字,例如:'' */ 37 | iconWord: string; 38 | /** 技能要求列表,例如:['PHP', 'PHP开发经验', '服务端开发经验', '大数据项目开发经验'] */ 39 | skills: string[]; 40 | /** 工作经验要求,例如:'1-3年' */ 41 | jobExperience: string; 42 | /** 每周工作天数描述,例如:'' */ 43 | daysPerWeekDesc: string; 44 | /** 最短工作月数描述,例如:'' */ 45 | leastMonthDesc: string; 46 | /** 学历要求,例如:'学历不限' */ 47 | jobDegree: string; 48 | /** 城市名称,例如:'成都' */ 49 | cityName: string; 50 | /** 地区/县,例如:'双流区' */ 51 | areaDistrict: string; 52 | /** 商业区域,例如:'华阳' */ 53 | businessDistrict: string; 54 | /** 工作类型标识,例如:0 */ 55 | jobType: number; 56 | /** 是否代理工作,例如:0 */ 57 | proxyJob: number; 58 | /** 代理类型,例如:0 */ 59 | proxyType: number; 60 | /** 是否匿名发布,例如:0 */ 61 | anonymous: number; 62 | /** 是否为境外工作,例如:0 */ 63 | outland: number; 64 | /** 是否为优选职位,例如:0 */ 65 | optimal: number; 66 | /** 图标标记列表,例如:[] */ 67 | iconFlagList: any[]; 68 | /** 物品ID,例如:1 */ 69 | itemId: number; 70 | /** 城市编码,例如:101270100 */ 71 | city: number; 72 | /** 是否被屏蔽,例如:0 */ 73 | isShield: number; 74 | /** 是否直接通过ATS发布,例如:false */ 75 | atsDirectPost: boolean; 76 | /** GPS位置,例如:null */ 77 | gps: any; 78 | /** 最后修改时间(时间戳),例如:1710067550000 */ 79 | lastModifyTime: number; 80 | /** 加密后的品牌ID,例如:'6d13c740...' */ 81 | encryptBrandId: string; 82 | /** 品牌名称,例如:'御坂网络' */ 83 | brandName: string; 84 | /** 品牌Logo的URL,例如:'https://img.bosszhipin.00000098.png.webp' */ 85 | brandLogo: string; 86 | /** 品牌阶段名称,例如:'' */ 87 | brandStageName: string; 88 | /** 品牌所属行业,例如:'互联网' */ 89 | brandIndustry: string; 90 | /** 品牌规模描述,例如:'20-99人' */ 91 | brandScaleName: string; 92 | /** 福利列表,例如:['意外险', '工龄奖', '团建聚餐'] */ 93 | welfareList: string[]; 94 | /** 行业编码,例如:100020 */ 95 | industry: number; 96 | /** 是否允许联系,例如:false */ 97 | contact: boolean; 98 | } 99 | 100 | /** 101 | * 职位卡片信息,在模板中通常使用 card 获取 102 | * 如 {{ card.activeTimeDesc }} 获取活跃时间描述 103 | */ 104 | interface JobCard { 105 | /** 职位名称,例如:"电脑技术员" */ 106 | jobName: string; 107 | /** 岗位描述,例如:"测试电脑配件" */ 108 | postDescription: string; 109 | /** 加密职位ID,例如:"162c879061111t2_GFVZ" */ 110 | encryptJobId: string; 111 | /** 是否直接发布到 ATS,例如:false */ 112 | atsDirectPost: boolean; 113 | /** 是否代理发布,例如:false */ 114 | atsProxyJob: boolean; 115 | /** 薪资描述,例如:"4-5K" */ 116 | salaryDesc: string; 117 | /** 城市名称,例如:"成都" */ 118 | cityName: string; 119 | /** 工作经验要求,例如:"1年以内" */ 120 | experienceName: string; 121 | /** 学历要求,例如:"高中" */ 122 | degreeName: string; 123 | /** 职位标签,例如:["企业内部IT技术支持", "售前/售后技术支持"] */ 124 | jobLabels: string[]; 125 | /** 工作地址,例如:"成都武侯区广和 */ 126 | address: string; 127 | /** 唯一标识ID,例如:"3j51231ut.search.3" */ 128 | lid: string; 129 | /** 会话ID,例如:"" */ 130 | sessionId: string; 131 | /** 安全ID,例如:"QFGEz123CzXQ-111111A9qKWHZZtes8aKQt9StSI655FRNtYx123123123" */ 132 | securityId: string; 133 | /** 加密用户ID,例如:"a111111b05711111111dy1FFJZ" */ 134 | encryptUserId: string; 135 | /** 上级领导姓名,例如:"小明" */ 136 | bossName: string; 137 | /** 上级领导职称,例如:"经理" */ 138 | bossTitle: string; 139 | /** 上级领导头像,例如:"https://img.bosszhipin.com/boss/avatar/avatar_2.png" */ 140 | bossAvatar: string; 141 | /** 是否在线,例如:false */ 142 | online: boolean; 143 | /** 是否认证,例如:true */ 144 | certificated: boolean; 145 | /** 活跃时间描述,例如:"本月活跃" */ 146 | activeTimeDesc: string; 147 | /** 品牌名称,例如:"宇鑫电脑..." */ 148 | brandName: string; 149 | /** 是否可以添加为好友,例如:true */ 150 | canAddFriend: boolean; 151 | /** 好友状态,例如:1 */ 152 | friendStatus: number; 153 | /** 是否感兴趣,例如:0 */ 154 | isInterested: number; 155 | /** 是否登录,例如:true */ 156 | login: boolean; 157 | } 158 | 159 | type JobList = JobListData[]; 160 | -------------------------------------------------------------------------------- /src/types/mitem.d.ts: -------------------------------------------------------------------------------- 1 | declare module "mitem" { 2 | class miTem { 3 | static compile(s: string): (object) => string; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/types/vueVirtualScroller.d.ts: -------------------------------------------------------------------------------- 1 | declare module "vue-virtual-scroller" { 2 | import { 3 | type ObjectEmitsOptions, 4 | type PublicProps, 5 | type SetupContext, 6 | type SlotsType, 7 | type VNode, 8 | } from "vue"; 9 | 10 | interface RecycleScrollerProps { 11 | items: readonly T[]; 12 | direction?: "vertical" | "horizontal"; 13 | itemSize?: number | null; 14 | gridItems?: number; 15 | itemSecondarySize?: number; 16 | minItemSize?: number; 17 | sizeField?: string; 18 | typeField?: string; 19 | keyField?: keyof T; 20 | pageMode?: boolean; 21 | prerender?: number; 22 | buffer?: number; 23 | emitUpdate?: boolean; 24 | updateInterval?: number; 25 | listClass?: string; 26 | itemClass?: string; 27 | listTag?: string; 28 | itemTag?: string; 29 | } 30 | 31 | interface DynamicScrollerProps extends RecycleScrollerProps { 32 | minItemSize: number; 33 | } 34 | 35 | interface RecycleScrollerEmitOptions extends ObjectEmitsOptions { 36 | resize: () => void; 37 | visible: () => void; 38 | hidden: () => void; 39 | update: ( 40 | startIndex: number, 41 | endIndex: number, 42 | visibleStartIndex: number, 43 | visibleEndIndex: number 44 | ) => void; 45 | "scroll-start": () => void; 46 | "scroll-end": () => void; 47 | } 48 | 49 | interface RecycleScrollerSlotProps { 50 | item: T; 51 | index: number; 52 | active: boolean; 53 | } 54 | 55 | interface RecycleScrollerSlots { 56 | default(slotProps: RecycleScrollerSlotProps): unknown; 57 | before(): unknown; 58 | empty(): unknown; 59 | after(): unknown; 60 | } 61 | 62 | export interface RecycleScrollerInstance { 63 | getScroll(): { start: number; end: number }; 64 | scrollToItem(index: number): void; 65 | scrollToPosition(position: number): void; 66 | scrollToBottom(): void; 67 | } 68 | 69 | export const RecycleScroller: ( 70 | props: RecycleScrollerProps & PublicProps, 71 | ctx?: SetupContext< 72 | RecycleScrollerEmitOptions, 73 | SlotsType> 74 | >, 75 | expose?: (exposed: RecycleScrollerInstance) => void 76 | ) => VNode & { 77 | __ctx?: { 78 | props: RecycleScrollerProps & PublicProps; 79 | expose(exposed: RecycleScrollerInstance): void; 80 | slots: RecycleScrollerSlots; 81 | }; 82 | }; 83 | 84 | export const DynamicScroller: ( 85 | props: DynamicScrollerProps & PublicProps, 86 | ctx?: SetupContext< 87 | RecycleScrollerEmitOptions, 88 | SlotsType> 89 | >, 90 | expose?: (exposed: RecycleScrollerInstance) => void 91 | ) => VNode & { 92 | __ctx?: { 93 | props: DynamicScrollerProps & PublicProps; 94 | expose(exposed: RecycleScrollerInstance): void; 95 | slots: RecycleScrollerSlots; 96 | }; 97 | }; 98 | 99 | interface DynamicScrollerItemProps { 100 | item: T; 101 | active: boolean; 102 | sizeDependencies?: unknown[]; 103 | watchData?: boolean; 104 | tag?: string; 105 | emitResize?: boolean; 106 | onResize?: () => void; 107 | } 108 | 109 | interface DynamicScrollerItemEmitOptions extends ObjectEmitsOptions { 110 | resize: () => void; 111 | } 112 | 113 | export const DynamicScrollerItem: ( 114 | props: DynamicScrollerItemProps & PublicProps, 115 | ctx?: SetupContext 116 | ) => VNode; 117 | 118 | export function IdState(options?: { 119 | idProp?: (value: any) => unknown; 120 | }): ComponentOptionsMixin; 121 | } 122 | -------------------------------------------------------------------------------- /src/utils/conf.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { ElMessageBox, ElNotification } from "element-plus"; 3 | import { GM_getValue, GM_setValue } from "$"; 4 | export const netConf = ref(); 5 | 6 | fetch("https://qiu-config.oss-cn-beijing.aliyuncs.com/boos-helper-config.json") 7 | .then((res) => { 8 | return res.json(); 9 | }) 10 | .then((data) => { 11 | netConf.value = data; 12 | const now = new Date().getTime(); 13 | netConf.value?.notification.forEach((item) => { 14 | if (now > GM_getValue(`netConf-${item.key}`, 0)) { 15 | if (item.type === "message") { 16 | ElMessageBox.alert(item.data.content, item.data.title ?? "message", { 17 | confirmButtonText: "OK", 18 | callback: () => { 19 | GM_setValue( 20 | `netConf-${item.key}`, 21 | now + (item.data.duration ?? 86400) * 1000 22 | ); 23 | }, 24 | }); 25 | } else if (item.type === "notification") { 26 | ElNotification({ 27 | ...item.data, 28 | duration: 0, 29 | onClose() { 30 | GM_setValue( 31 | `netConf-${item.key}`, 32 | now + (item.data.duration ?? 86400) * 1000 33 | ); 34 | }, 35 | onClick() { 36 | item.data.url ?? window.open(item.data.url); 37 | }, 38 | }); 39 | } 40 | } 41 | }); 42 | }); 43 | 44 | export interface NetConf { 45 | version: string; 46 | notification: ( 47 | | NotificationAlert 48 | | NotificationMessage 49 | | NotificationNotification 50 | )[]; 51 | feedback: string; 52 | } 53 | 54 | export interface NotificationAlert { 55 | key?: string; 56 | type: "alert"; 57 | data: import("element-plus").AlertProps; 58 | } 59 | 60 | export interface NotificationMessage { 61 | key?: string; 62 | type: "message"; 63 | data: { title?: string; content: string; duration?: number }; 64 | } 65 | 66 | export interface NotificationNotification { 67 | key?: string; 68 | type: "notification"; 69 | data: import("element-plus").NotificationProps & { 70 | url?: string; 71 | duration?: number; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/deepmerge.ts: -------------------------------------------------------------------------------- 1 | // 深度合并 2 | // https://github.com/sindresorhus/is-plain-obj/blob/main/index.js 3 | export function isPlainObject( 4 | item: unknown 5 | ): item is Record { 6 | if (typeof item !== "object" || item === null) { 7 | return false; 8 | } 9 | 10 | const prototype = Object.getPrototypeOf(item); 11 | return ( 12 | (prototype === null || 13 | prototype === Object.prototype || 14 | Object.getPrototypeOf(prototype) === null) && 15 | !(Symbol.toStringTag in item) && 16 | !(Symbol.iterator in item) 17 | ); 18 | } 19 | 20 | export interface DeepmergeOptions { 21 | clone?: boolean; 22 | } 23 | 24 | function deepClone(source: T): T | Record { 25 | if (!isPlainObject(source)) { 26 | return source; 27 | } 28 | 29 | const output: Record = {}; 30 | 31 | Object.keys(source).forEach((key) => { 32 | output[key] = deepClone(source[key]); 33 | }); 34 | 35 | return output; 36 | } 37 | 38 | export default function deepmerge( 39 | target: T, 40 | source: unknown, 41 | options: DeepmergeOptions = { clone: true } 42 | ): T { 43 | const output = options.clone ? { ...target } : target; 44 | 45 | if (isPlainObject(target) && isPlainObject(source)) { 46 | Object.keys(source).forEach((key) => { 47 | // Avoid prototype pollution 48 | if (key === "__proto__") { 49 | return; 50 | } 51 | 52 | if ( 53 | isPlainObject(source[key]) && 54 | key in target && 55 | isPlainObject(target[key]) 56 | ) { 57 | // Since `output` is a clone of `target` and we have narrowed `target` in this block we can cast to the same type. 58 | (output as Record)[key] = deepmerge( 59 | target[key], 60 | source[key], 61 | options 62 | ); 63 | } else if (options.clone) { 64 | (output as Record)[key] = isPlainObject(source[key]) 65 | ? deepClone(source[key]) 66 | : source[key]; 67 | } else { 68 | (output as Record)[key] = source[key]; 69 | } 70 | }); 71 | } 72 | 73 | return output; 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/elmGetter.ts: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name ElementGetter 3 | // @author cxxjackie 4 | // @version 2.0.0 5 | // @supportURL https://bbs.tampermonkey.net.cn/thread-2726-1-1.html 6 | // ==/UserScript== 7 | // @ts-nocheck 8 | import { unsafeWindow } from "$"; 9 | 10 | const win = unsafeWindow || document.defaultView || window; 11 | const doc = win.document; 12 | const listeners = new WeakMap(); 13 | 14 | const elProto = win.Element.prototype; 15 | 16 | const matches = 17 | elProto.matches || 18 | elProto.matchesSelector || 19 | elProto.webkitMatchesSelector || 20 | elProto.mozMatchesSelector || 21 | elProto.oMatchesSelector; 22 | const MutationObs = 23 | win.MutationObserver || win.WebkitMutationObserver || win.MozMutationObserver; 24 | function addObserver(target, callback) { 25 | const observer = new MutationObs((mutations) => { 26 | for (const mutation of mutations) { 27 | if (mutation.type === "attributes") { 28 | callback(mutation.target); 29 | if (observer.canceled) return; 30 | } 31 | for (const node of mutation.addedNodes) { 32 | if (node instanceof Element) callback(node); 33 | if (observer.canceled) return; 34 | } 35 | } 36 | }); 37 | observer.canceled = false; 38 | observer.observe(target, { 39 | childList: true, 40 | subtree: true, 41 | attributes: true, 42 | }); 43 | return () => { 44 | observer.canceled = true; 45 | observer.disconnect(); 46 | }; 47 | } 48 | function addFilter(target, filter) { 49 | let listener = listeners.get(target); 50 | if (!listener) { 51 | listener = { 52 | filters: new Set(), 53 | remove: addObserver(target, (el) => 54 | listener.filters.forEach((f) => f(el)) 55 | ), 56 | }; 57 | listeners.set(target, listener); 58 | } 59 | listener.filters.add(filter); 60 | } 61 | function removeFilter(target, filter) { 62 | const listener = listeners.get(target); 63 | if (!listener) return; 64 | listener.filters.delete(filter); 65 | if (!listener.filters.size) { 66 | listener.remove(); 67 | listeners.delete(target); 68 | } 69 | } 70 | function query(all, selector, parent, includeParent) { 71 | const checkParent = includeParent && matches.call(parent, selector); 72 | if (all) { 73 | const queryAll = parent.querySelectorAll(selector); 74 | return checkParent ? [parent, ...queryAll] : [...queryAll]; 75 | } 76 | return checkParent ? parent : parent.querySelector(selector); 77 | } 78 | 79 | function getOne(selector, parent, timeout) { 80 | return new Promise((resolve) => { 81 | const node = query(false, selector, parent, false); 82 | if (node) return resolve(node); 83 | let timer; 84 | const filter = (el) => { 85 | const node = query(false, selector, el, true); 86 | if (node) { 87 | removeFilter(parent, filter); 88 | timer && clearTimeout(timer); 89 | resolve(node); 90 | } 91 | }; 92 | addFilter(parent, filter); 93 | if (timeout > 0) { 94 | timer = setTimeout(() => { 95 | removeFilter(parent, filter); 96 | resolve(null); 97 | }, timeout); 98 | } 99 | }); 100 | } 101 | 102 | function get( 103 | selector: string[], 104 | ...args: [Element, number] | [number] | [Element] | [] 105 | ): Promise; 106 | function get( 107 | selector: string, 108 | ...args: [Element, number] | [number] | [Element] | [] 109 | ): Promise; 110 | function get( 111 | selector: string | string[], 112 | ...args: [Element, number] | [number] | [Element] | [] 113 | ): Promise { 114 | let parent = (typeof args[0] !== "number" && args.shift()) || doc; 115 | const timeout = args[0] || 0; 116 | if (Array.isArray(selector)) { 117 | return Promise.all( 118 | selector.map((s) => getOne(s, parent, timeout)) 119 | ) as Promise; 120 | } 121 | return getOne(selector, parent, timeout) as Promise; 122 | } 123 | 124 | function each( 125 | selector: string, 126 | ...args: 127 | | [Element, (elm: Element, isInserted: boolean) => boolean] 128 | | [(elm: Element, isInserted: boolean) => boolean] 129 | ) { 130 | let parent = (typeof args[0] !== "function" && args.shift()) || doc; 131 | 132 | const callback = args[0] as (elm: Element, isInserted: boolean) => boolean; 133 | 134 | const refs = new WeakSet(); 135 | for (const node of query(true, selector, parent, false)) { 136 | refs.add(node); 137 | if (callback(node, false) === false) return; 138 | } 139 | const filter = (el: Element) => { 140 | for (const node of query(true, selector, el, true)) { 141 | const _el = node; 142 | if (refs.has(_el)) break; 143 | refs.add(_el); 144 | if (callback(node, true) === false) { 145 | return removeFilter(parent, filter); 146 | } 147 | } 148 | }; 149 | addFilter(parent, filter); 150 | } 151 | 152 | async function rm( 153 | selector: string | string[], 154 | ...args: [Element, number] | [number] | [Element] | [] 155 | ) { 156 | if (Array.isArray(selector)) { 157 | await Promise.all( 158 | selector.map((s) => { 159 | get(s, ...args).then((e) => e.remove()); 160 | }) 161 | ); 162 | } else { 163 | await get(selector, ...args).then((e) => e.remove()); 164 | } 165 | } 166 | export default { 167 | get, 168 | each, 169 | rm, 170 | }; 171 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { GM_notification } from "$"; 2 | import { logger } from "./logger"; 3 | 4 | // 通知 5 | export function notification(content: string) { 6 | GM_notification({ 7 | title: "Boss直聘批量投简历", 8 | image: 9 | "https://img.bosszhipin.com/beijin/mcs/banner/3e9d37e9effaa2b6daf43f3f03f7cb15cfcd208495d565ef66e7dff9f98764da.jpg", 10 | text: content, 11 | highlight: true, // 布尔值,是否突出显示发送通知的选项卡 12 | silent: true, // 布尔值,是否播放声音 13 | timeout: 10000, // 设置通知隐藏时间 14 | onclick: function () { 15 | logger.info("点击了通知"); 16 | }, 17 | ondone() {}, // 在通知关闭(无论这是由超时还是单击触发)或突出显示选项卡时调用 18 | }); 19 | } 20 | 21 | // 动画 22 | export function animate({ 23 | duration, 24 | draw, 25 | timing, 26 | end, 27 | callId, 28 | }: { 29 | duration: number; 30 | draw: (progress: number) => void; 31 | timing: (timeFraction: number) => number; 32 | callId: (id: number) => void; 33 | end?: () => void; 34 | }) { 35 | let start = performance.now(); 36 | 37 | callId( 38 | requestAnimationFrame(function animate(time) { 39 | let timeFraction = (time - start) / duration; 40 | if (timeFraction > 1) timeFraction = 1; 41 | 42 | let progress = timing(timeFraction); 43 | 44 | draw(progress); 45 | 46 | if (timeFraction < 1) { 47 | callId(requestAnimationFrame(animate)); 48 | } else if (end) { 49 | end(); 50 | } 51 | }) 52 | ); 53 | } 54 | let delayLoadId: number | undefined = undefined; 55 | // 延迟 56 | export function delay(s: number) { 57 | loader({ ms: s * 1000 }); 58 | return new Promise((resolve) => setTimeout(resolve, s * 1000)); 59 | } 60 | 61 | // 加载进度条 62 | export function loader({ ms = 10000, color = "#54f98d", onDone = () => {} }) { 63 | let load = document.querySelector("#loader"); 64 | if (!load) { 65 | const l = document.createElement("div"); 66 | l.id = "loader"; 67 | document.querySelector("#header")?.appendChild(l); 68 | load = l; 69 | } 70 | load.style.background = color; 71 | if (delayLoadId) { 72 | cancelAnimationFrame(delayLoadId); 73 | delayLoadId = undefined; 74 | } 75 | if (load) 76 | animate({ 77 | duration: ms, 78 | callId(id) { 79 | delayLoadId = id; 80 | }, 81 | timing(timeFraction) { 82 | return timeFraction; 83 | }, 84 | draw(progress) { 85 | if (load) load.style.width = progress * 100 + "%"; 86 | }, 87 | end() { 88 | if (load) load.style.width = "0%"; 89 | onDone(); 90 | }, 91 | }); 92 | return () => { 93 | if (delayLoadId) cancelAnimationFrame(delayLoadId); 94 | delayLoadId = undefined; 95 | const load = document.querySelector("#loader"); 96 | if (load) load.style.width = "0%"; 97 | }; 98 | } 99 | // 获取当前日期 100 | export function getCurDay(currentDate = new Date()) { 101 | const year = currentDate.getFullYear(); 102 | const month = String(currentDate.getMonth() + 1).padStart(2, "0"); 103 | const day = String(currentDate.getDate()).padStart(2, "0"); 104 | return `${year}-${month}-${day}`; 105 | } 106 | // 获取当前时间 107 | export function getCurTime(currentDate = new Date()) { 108 | const hours = String(currentDate.getHours() + 1).padStart(2, "0"); 109 | const minutes = String(currentDate.getMinutes() + 1).padStart(2, "0"); 110 | const seconds = String(currentDate.getSeconds()).padStart(2, "0"); 111 | return `${hours}:${minutes}:${seconds}`; 112 | } 113 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | // https://bbs.tampermonkey.net.cn/forum.php?mod=redirect&goto=findpost&ptid=5899&pid=77134 2 | const icons = { debug: "🐞", info: "ℹ️", warn: "⚠", error: "❌️" }; 3 | const Color = { 4 | debug: "#42CA8C;", 5 | info: "#37C5D6;", 6 | warn: "#EFC441;", 7 | error: "#FF6257;", 8 | }; 9 | export const logger = { 10 | debug: console.log.bind( 11 | console, 12 | `%c${icons.debug} debug > `, 13 | `color:${Color.debug}; padding-left:1.2em; line-height:1.5em;` 14 | ), 15 | info: console.info.bind( 16 | console, 17 | `%c${icons.info} info > `, 18 | `color:${Color.info}; padding-left:1.2em; line-height:1.5em;` 19 | ), 20 | warn: console.warn.bind( 21 | console, 22 | `%c${icons.warn} warn > `, 23 | `color:${Color.warn}; padding-left:1.2em; line-height:1.5em;` 24 | ), 25 | error: console.error.bind( 26 | console, 27 | `%c${icons.error} error > `, 28 | `color:${Color.error}; padding-left:1.2em; line-height:1.5em;` 29 | ), 30 | group: console.groupCollapsed, 31 | groupEnd: console.groupEnd, 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/parse.ts: -------------------------------------------------------------------------------- 1 | import { unsafeWindow } from "$"; 2 | import { parse, STR, OBJ, NUM, ARR, NULL } from "partial-json"; 3 | 4 | export function parseGptJson(json: string): Partial | null { 5 | return parse( 6 | json.replace(/^```json|```$/g, ""), 7 | STR | OBJ | NUM | ARR | NULL 8 | ); 9 | } 10 | 11 | unsafeWindow.parseGptJson = parseGptJson; 12 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { GM_xmlhttpRequest, GmXhrRequest } from "$"; 2 | import { loader } from "."; 3 | import { events, stream } from "fetch-event-stream"; 4 | 5 | export class RequestError extends Error { 6 | constructor(message: string) { 7 | super(message); 8 | this.name = "请求错误"; 9 | } 10 | } 11 | export type ResponseType = 12 | | "text" 13 | | "json" 14 | | "arraybuffer" 15 | | "blob" 16 | | "document" 17 | | "stream"; 18 | 19 | export type OnStream = (reader: ReturnType) => Promise; 20 | 21 | export type RequestArgs = Partial< 22 | Pick< 23 | GmXhrRequest, 24 | "method" | "url" | "data" | "headers" | "timeout" | "responseType" 25 | > & { 26 | onStream: OnStream; 27 | isFetch: boolean; 28 | } 29 | >; 30 | let axiosLoad: () => void; 31 | 32 | export function request({ 33 | method = "POST", 34 | url = "", 35 | data = "", 36 | headers = {}, 37 | timeout = 5, 38 | responseType = "json" as TResponseType, 39 | onStream = async () => {}, 40 | isFetch = false, 41 | }: RequestArgs) { 42 | if (!isFetch) 43 | return new Promise((resolve, reject) => { 44 | GM_xmlhttpRequest({ 45 | method, 46 | url, 47 | data, 48 | headers, 49 | timeout: timeout * 1000, 50 | responseType, 51 | 52 | ontimeout() { 53 | if (axiosLoad) axiosLoad(); 54 | reject(new RequestError(`超时 ${Math.round(timeout / 1000)}s`)); 55 | }, 56 | onabort() { 57 | if (axiosLoad) axiosLoad(); 58 | reject(new RequestError("用户中止")); 59 | }, 60 | onerror(e) { 61 | const msg = `${e.responseText} | ${e.error}`; 62 | if (axiosLoad) axiosLoad(); 63 | reject(new RequestError(msg)); 64 | }, 65 | // onloadend(e) { 66 | // if (axiosLoad) axiosLoad(); 67 | // resolve(e.response); 68 | // }, 69 | onload(e) { 70 | if (axiosLoad) axiosLoad(); 71 | resolve(e.response); 72 | }, 73 | onloadstart(e) { 74 | axiosLoad = loader({ ms: timeout * 1000, color: "#F79E63" }); 75 | if (responseType === "stream") { 76 | const stream = events(e.response); 77 | onStream(stream); 78 | } 79 | }, 80 | }); 81 | }); 82 | else { 83 | const abortController = new AbortController(); 84 | 85 | return new Promise((resolve, reject) => { 86 | // Start loading indication 87 | axiosLoad = loader({ ms: timeout * 1000, color: "#F79E63" }); 88 | fetch(url, { 89 | method, 90 | headers, 91 | body: data, 92 | signal: abortController.signal, 93 | }) 94 | .then(async (response) => { 95 | if (!response.body) { 96 | reject(new RequestError("没有响应体")); 97 | return; 98 | } 99 | if (!response.ok) { 100 | const errorText = await response.text(); 101 | if (axiosLoad) axiosLoad(); 102 | reject(new RequestError(`${errorText} | ${response.statusText}`)); 103 | return; 104 | } 105 | if (responseType === "stream") { 106 | // const reader = response.body.getReader(); 107 | const stream = events(response, abortController.signal); 108 | await onStream(stream); 109 | return; 110 | } else { 111 | const result = 112 | responseType === "json" 113 | ? await response.json() 114 | : await response.text(); 115 | if (axiosLoad) axiosLoad(); 116 | resolve(result); 117 | } 118 | }) 119 | .catch((e) => { 120 | if (axiosLoad) axiosLoad(); 121 | if (e.name === "AbortError") { 122 | reject(new RequestError("用户中止")); 123 | } else { 124 | const msg = `${e.message}`; 125 | reject(new RequestError(msg)); 126 | } 127 | }); 128 | 129 | // Set timeout 130 | setTimeout(() => { 131 | abortController.abort(); 132 | if (axiosLoad) axiosLoad(); 133 | reject(new RequestError(`超时 ${Math.round(timeout / 1000)}s`)); 134 | }, timeout * 1000); 135 | }); 136 | } 137 | } 138 | 139 | request.post = ( 140 | args: Omit, "method"> 141 | ) => { 142 | return request({ 143 | method: "POST", 144 | ...args, 145 | }); 146 | }; 147 | 148 | request.get = ( 149 | args: Omit, "method"> 150 | ) => { 151 | return request({ 152 | method: "GET", 153 | ...args, 154 | }); 155 | }; 156 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | //// 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "useDefineForClassFields": true, 5 | "strict": true, 6 | "resolveJsonModule": true, 7 | "isolatedModules": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "verbatimModuleSyntax": false, 11 | "types": ["node"], 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": ["src/*"] 15 | } 16 | }, 17 | "include": [ 18 | "src/**/*.ts", 19 | "src/**/*.d.ts", 20 | "src/**/*.tsx", 21 | "src/**/*.vue", 22 | "auto-imports.d.ts", 23 | "components.d.ts" 24 | ], 25 | "exclude": ["node_modules", "other"], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /update.log: -------------------------------------------------------------------------------- 1 | v0.1.4 🌟🤡 卡片状态适配白天,修复将gpt提示发送给boos 2 | v0.1.5 🤡 修复编译后拿不到window数据导致报错的严重Bug 3 | v0.1.6-1 🌟🤡 更改元素查找逻辑更快更准,调整运行时期过渡更加流畅,使用CDN优化大小,修复部分样式问题 4 | v0.1.6-2 🌟 更多优化和提醒,完善记录内容,筛选问题中加入公司福利内容更准 5 | v0.1.6-fix 🤡 修复自定义招呼语不发送,修复配置开关没文字,隐藏没找到vue的提示 6 | v0.1.6-fix2 🤡 修复公司名筛选报错,修复部分功能无法正确筛选 7 | v0.2.0 🤡 修复所有已知的BUG 8 | v0.2.0-1 🌟 使用partial-json来修复json错误。模型存储使用数组,更新版本后请重新配置模型 9 | v0.2.0-2 🌟 适配更多GPT,多种不同的请求方式,更多的参数,更多的筛选,延迟配置,增加聊天气泡(后续会完善) 10 | v0.2.1 🤡🌟 修复无法获取到token的问题, 添加好友状态过滤(未测试) 11 | v0.2.2 🤡🌟 添加云通知,修复chat部分错误,禁用一些功能(解决不了问题就先解决出问题的功能😅) 12 | v0.2.3 🌟🤡 配置的导入导出, stream 禁用, 交集薪资范围, 优化tab名, 多行输入框 13 | v0.2.4 🌟 Hr职位筛选, UI优化, 初始帮助 14 | v0.2.4-fix 🌟🤡 支持完整url调用gpt,修复prompt在gpt请求时和编辑时修改到form中 15 | v0.2.4-fix2 🤡 修复投递限制逻辑,成功投递达到100时自动暂停 16 | v0.2.5 🌟 添加扣子模型(测试),禁用stream, 文心一言, 通义千问 17 | v0.2.6 🌟 添加投递上限配置 18 | v0.2.7 添加扩展通知 -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import monkey, { cdn, util } from "vite-plugin-monkey"; 4 | import AutoImport from "unplugin-auto-import/vite"; 5 | import Components from "unplugin-vue-components/vite"; 6 | import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; 7 | 8 | import process from "process"; 9 | import path from "path"; 10 | import fs from "fs"; 11 | 12 | const rootDir = process.cwd(); 13 | const pathSrc = path.resolve(__dirname, "src"); 14 | 15 | // https://vitejs.dev/config/ 16 | export default defineConfig(() => { 17 | const env = loadEnv("", process.cwd(), ""); 18 | const VITE_RELEASE_MODE = env.VITE_RELEASE_MODE ?? "dev"; 19 | const VITE_VERSION = env.VITE_VERSION ?? "dev"; 20 | return { 21 | plugins: [ 22 | vue(), 23 | AutoImport({ 24 | imports: [util.unimportPreset], 25 | resolvers: [ElementPlusResolver()], 26 | }), 27 | Components({ 28 | resolvers: [ElementPlusResolver()], 29 | }), 30 | monkey({ 31 | entry: "src/main.ts", 32 | format: { 33 | generate(uOptions) { 34 | if (uOptions.mode === "build") { 35 | const filePath = path.join(rootDir, "update.log"); 36 | const fileContent = fs.readFileSync(filePath, "utf-8"); 37 | const lines = fileContent.trim().split("\n"); 38 | const lastTenLines = lines.slice(-30); 39 | const log = lastTenLines 40 | .reverse() 41 | .map((line) => `// ${line}`) 42 | .join("\n"); 43 | return ( 44 | uOptions.userscript + 45 | `\n// 更新日志[只显示最新的10条,🌟🤡 分别代表新功能和bug修复]\n${log}` 46 | ); 47 | } else { 48 | return uOptions.userscript; 49 | } 50 | }, 51 | }, 52 | userscript: { 53 | name: 54 | VITE_RELEASE_MODE === "release" 55 | ? "Boss直聘助手" 56 | : `Boss直聘助手 [${VITE_RELEASE_MODE}]`, 57 | version: VITE_VERSION, 58 | license: "MIT", 59 | description: 60 | "优化UI去除广告,批量投递简历,高级筛选,GPT自动打招呼,多账号管理...", 61 | icon: "https://img.bosszhipin.com/beijin/mcs/banner/3e9d37e9effaa2b6daf43f3f03f7cb15cfcd208495d565ef66e7dff9f98764da.jpg", 62 | namespace: "https://github.com/Ocyss/boos-helper", 63 | homepage: "https://github.com/Ocyss/boos-helper", 64 | match: ["https://*.zhipin.com/*"], 65 | author: "Ocyss", 66 | grant: ["unsafeWindow"], 67 | "run-at": "document-start", 68 | connect: [ 69 | "api.chatanywhere.com.cn", 70 | "api.moonshot.cn", 71 | "aliyuncs.com", 72 | "baidubce.com", 73 | ], 74 | downloadURL: 75 | "https://update.greasyfork.org/scripts/491340/Boss%E7%9B%B4%E8%81%98%E5%8A%A9%E6%89%8B.user.js", 76 | updateURL: 77 | "https://update.greasyfork.org/scripts/491340/Boss%E7%9B%B4%E8%81%98%E5%8A%A9%E6%89%8B.user.js", 78 | }, 79 | build: { 80 | externalGlobals: { 81 | vue: cdn 82 | .jsdelivr("Vue", "dist/vue.global.prod.js") 83 | .concat(util.dataUrl(";window.Vue=Vue;")), 84 | "element-plus": cdn.jsdelivr( 85 | "ElementPlus", 86 | "dist/index.full.min.js" 87 | ), 88 | protobufjs: cdn.jsdelivr("protobuf", "dist/light/protobuf.min.js"), 89 | }, 90 | externalResource: { 91 | "element-plus/dist/index.css": cdn.jsdelivr(), 92 | "element-plus/theme-chalk/dark/css-vars.css": cdn.jsdelivr(), 93 | }, 94 | }, 95 | server: { 96 | prefix: false, 97 | }, 98 | }), 99 | ], 100 | resolve: { 101 | alias: { 102 | "@": pathSrc, 103 | }, 104 | }, 105 | build: { 106 | minify: false, 107 | }, 108 | css: { 109 | preprocessorOptions: { 110 | scss: { 111 | api: "modern-compiler", // or 'modern' 112 | }, 113 | }, 114 | }, 115 | // server: { 116 | // host: "logapi.zhipin.com", 117 | // port: 80, 118 | // https: { 119 | // key: path.resolve(__dirname, "logapi.zhipin.com-key.pem"), 120 | // cert: path.resolve(__dirname, "logapi.zhipin.com.pem"), 121 | // }, 122 | // }, 123 | }; 124 | }); 125 | --------------------------------------------------------------------------------