diff --git a/packages/geek-auto-start-chat-with-boss/default-config-file/llm.json b/packages/geek-auto-start-chat-with-boss/default-config-file/llm.json new file mode 100644 index 0000000..6b7dde8 --- /dev/null +++ b/packages/geek-auto-start-chat-with-boss/default-config-file/llm.json @@ -0,0 +1,6 @@ +{ + "providerCompleteApiUrl": "", + "providerApiSecret": "", + "model": "", + "_extra": {} +} \ No newline at end of file diff --git a/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs b/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs index 091345c..65d8780 100644 --- a/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs +++ b/packages/geek-auto-start-chat-with-boss/runtime-file-utils.mjs @@ -6,16 +6,18 @@ import os from 'node:os' import defaultDingtalkConf from './default-config-file/dingtalk.json' assert {type: 'json'} import defaultBossConf from './default-config-file/boss.json' assert {type: 'json'} import defaultTargetCompanyListConf from './default-config-file/target-company-list.json' assert {type: 'json'} +import defaultLlmConf from './default-config-file/llm.json' assert { type: 'json' } import defaultBossCookieStorage from './default-storage-file/boss-cookies.json' assert { type: 'json' } import defaultBossLocalStorageStorage from './default-storage-file/boss-local-storage.json' assert { type: 'json' } import defaultJobNotSuitReasonCodeToTextCacheStorage from './default-storage-file/job-not-suit-reason-code-to-text-cache.json' assert { type: 'json' } -export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json'] +export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json', 'llm.json'] const defaultConfigFileContentMap = { 'boss.json': JSON.stringify(defaultBossConf), 'dingtalk.json': JSON.stringify(defaultDingtalkConf), - 'target-company-list.json': JSON.stringify(defaultTargetCompanyListConf) + 'target-company-list.json': JSON.stringify(defaultTargetCompanyListConf), + 'llm.json': JSON.stringify(defaultLlmConf) } const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun') diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts index 084f189..72c03c4 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts @@ -30,6 +30,7 @@ import { pipeWriteRegardlessError } from '../../utils/pipe' import { WriteStream } from 'node:fs' // eslint-disable-next-line vue/prefer-import-from-vue import { hasOwn } from '@vue/shared' +import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow' export default function initIpc() { ipcMain.on('open-external-link', (_, link) => { @@ -435,6 +436,32 @@ export default function initIpc() { return await getJobHistoryByEncryptId(encryptJobId) }) + ipcMain.handle('llm-config', async () => { + createLlmConfigWindow({ + parent: mainWindow!, + modal: true, + show: true + }) + const defer = Promise.withResolvers() + async function saveLlmConfigHandler(_, configToSave) { + const configToPatch = await readConfigFile('llm.json') + for (const k of Object.keys(configToSave)) { + configToPatch[k] = configToSave[k] + } + await writeConfigFile('llm.json', configToPatch) + defer.resolve() + ipcMain.removeHandler('save-llm-config') + llmConfigWindow?.close() + } + ipcMain.handle('save-llm-config', saveLlmConfigHandler) + llmConfigWindow?.once('closed', () => { + ipcMain.removeHandler('save-llm-config') + defer.reject(new Error('cancel')) + }) + return defer.promise + }) + ipcMain.on('close-llm-config', () => llmConfigWindow?.close()) + ipcMain.handle('exit-app-immediately', () => { app.exit(0) }) diff --git a/packages/ui/src/main/window/llmConfigWindow.ts b/packages/ui/src/main/window/llmConfigWindow.ts new file mode 100644 index 0000000..0463c02 --- /dev/null +++ b/packages/ui/src/main/window/llmConfigWindow.ts @@ -0,0 +1,45 @@ +import { BrowserWindow, ipcMain } from 'electron' +import path from 'path' + +export let llmConfigWindow: BrowserWindow | null = null +export function createLlmConfigWindow( + opt?: Electron.BrowserWindowConstructorOptions +): BrowserWindow { + // Create the browser window. + if (llmConfigWindow) { + llmConfigWindow!.show() + } + llmConfigWindow = new BrowserWindow({ + width: 540, + height: 400, + resizable: false, + show: false, + autoHideMenuBar: true, + frame: false, + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + sandbox: false + }, + ...opt + }) + + llmConfigWindow.on('ready-to-show', () => { + llmConfigWindow!.show() + }) + + // HMR for renderer base on electron-vite cli. + // Load the remote URL for development or the local html file for production. + if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) { + llmConfigWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/llmConfig') + } else { + llmConfigWindow.loadURL( + 'file://' + path.join(__dirname, '../renderer/index.html') + '#/llmConfig' + ) + } + + llmConfigWindow!.once('closed', () => { + llmConfigWindow = null + }) + + return llmConfigWindow! +} diff --git a/packages/ui/src/renderer/src/page/LlmConfig/index.vue b/packages/ui/src/renderer/src/page/LlmConfig/index.vue new file mode 100644 index 0000000..0f1cfa4 --- /dev/null +++ b/packages/ui/src/renderer/src/page/LlmConfig/index.vue @@ -0,0 +1,162 @@ + + + + + + + diff --git a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue index 16a81eb..b3a0e26 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/GeekAutoStartChatWithBoss.vue @@ -23,7 +23,7 @@ v-model="formContent.expectCompanies" :autosize="{ minRows: 4 }" type="textarea" - @blur="handleExpectCompaniesInputBlur" + @blur="normalizeExpectCompanies" /> { }) } const handleSave = async () => { + normalizeExpectCompanies() await formRef.value!.validate() await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value)) ElMessage.success('Configuration saved.') } -const handleExpectCompaniesInputBlur = (event) => { - event.target.value = (event.target?.value ?? '') +const normalizeExpectCompanies = () => { + formContent.value.expectCompanies = formContent.value.expectCompanies .split(/,|,/) .map((it) => it.trim()) .filter(Boolean) diff --git a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue index b066c06..d8128d7 100644 --- a/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/MainLayout/ReadNoReplyReminder.vue @@ -9,7 +9,7 @@
BOSS直聘 Cookie
- 编辑Cookie
@@ -24,14 +24,37 @@
- 由 AI(根据你的简历及和当前Boss聊天的上下文)生成的内容 + 由大语言模型(根据简历及当前聊天上下文)生成的内容
- + +
+ + 配置大语言模型 + +
+ 支持 + DeepSeek-V3 + 模型 +
+
+
+ +
 天
- 不再跟进 ({{ rechatLimitDateString }})之前列表中没有进展的聊天 + 不再跟进 ({{ rechatLimitDateString }})之前列表中没有进展的聊天
这将会跟进列表中所有聊天(不建议
@@ -209,8 +233,12 @@ const rechatLimitDateString = computed(() => { ).format('YYYY-MM-DD HH:mm:ss') }) -const goToGeminiNanoApiKeyPage = () => { - electron.ipcRenderer.send('open-external-link', 'https://aistudio.google.com/app/apikey') +const handleClickConfigLlm = async () => { + try { + await electron.ipcRenderer.invoke('llm-config') + } catch (err) { + console.log(err) + } } diff --git a/packages/ui/src/renderer/src/router/index.ts b/packages/ui/src/renderer/src/router/index.ts index a36e7ef..a3a1726 100644 --- a/packages/ui/src/renderer/src/router/index.ts +++ b/packages/ui/src/renderer/src/router/index.ts @@ -16,6 +16,13 @@ const routes: Array = [ title: 'Cookie 助手' } }, + { + path: '/llmConfig', + component: () => import('@renderer/page/LlmConfig/index.vue'), + meta: { + title: '大语言模型设置' + } + }, { path: '/main-layout', component: () => import('@renderer/page/MainLayout/index.vue'),