From a7a83f6d6830448a9e90ac63ffe5f0377c1a1a34 Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Sun, 3 Nov 2024 15:46:52 +0800 Subject: [PATCH] add main logic of read no reply auto reminder --- .../src/main/flow/LAUNCH_BOSS_SITE/index.ts | 22 +-- .../flow/OPEN_SETTING_WINDOW/ipc/index.ts | 60 +++++++ .../READ_NO_REPLY_AUTO_REMINDER/bootstrap.ts | 40 +++++ .../boss-operation.ts | 17 ++ .../flow/READ_NO_REPLY_AUTO_REMINDER/index.ts | 161 ++++++++++++++++++ .../flow/READ_NO_REPLY_AUTO_REMINDER/types.ts | 40 +++++ packages/ui/src/main/index.ts | 5 + ...attachListenerForKillSelfOnParentExited.ts | 23 +++ .../GeekAutoStartChatWithBoss.vue | 5 +- .../Configuration/ReadNoReplyReminder.vue | 58 +++++++ .../renderer/src/page/Configuration/index.vue | 2 + .../RunningStatusForReadNoReplyReminder.vue | 80 +++++++++ .../page/GeekAutoStartChatWithBoss/index.vue | 21 ++- packages/ui/src/renderer/src/router/index.ts | 17 ++ 14 files changed, 528 insertions(+), 23 deletions(-) create mode 100644 packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/bootstrap.ts create mode 100644 packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts create mode 100644 packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts create mode 100644 packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/types.ts create mode 100644 packages/ui/src/main/utils/attachListenerForKillSelfOnParentExited.ts create mode 100644 packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue create mode 100644 packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue diff --git a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts index deb560d..479748a 100644 --- a/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts +++ b/packages/ui/src/main/flow/LAUNCH_BOSS_SITE/index.ts @@ -25,7 +25,7 @@ import { pipeWriteRegardlessError } from '../utils/pipe' import * as JSONStream from 'JSONStream' import { ChatStartupFrom } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatStartupLog' import gtag from '../../utils/gtag' -import { sleep } from '@geekgeekrun/utils/sleep.mjs' +import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE) @@ -254,22 +254,4 @@ export async function launchBossSite() { page = tempPage } -// #region period check is parent process existed -// Store the parent process ID -const parentPID = process.ppid -// Function to check if the parent process is alive -async function periodCheckParentProcess() { - // eslint-disable-next-line no-constant-condition - while (true) { - try { - // Try sending signal 0 to the parent process (this does not terminate the process) - process.kill(parentPID, 0) - } catch (err) { - // If an error is thrown, the parent process doesn't exist anymore - process.exit(0) - } - await sleep(1000) - } -} -periodCheckParentProcess() -// #endregion +attachListenerForKillSelfOnParentExited() 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 253538a..238c7db 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 @@ -149,6 +149,66 @@ export default function initIpc() { // TODO: }) + ipcMain.handle('run-read-no-reply-auto-reminder', async () => { + if (subProcessOfPuppeteer) { + return + } + const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable() + if (!puppeteerExecutable) { + return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES') + } + const subProcessEnv = { + ...process.env, + MAIN_BOSSGEEKGO_UI_RUN_MODE: 'readNoReplyAutoReminder', + PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath + } + subProcessOfPuppeteer = childProcess.spawn(process.argv[0], process.argv.slice(1), { + env: subProcessEnv, + stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'] + }) + // console.log(subProcessOfPuppeteer) + return new Promise((resolve, reject) => { + // subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => { + // const data = raw + // switch (data.type) { + // case 'AUTO_START_CHAT_DAEMON_PROCESS_STARTUP': { + // subProcessOfPuppeteer!.stdio[3]!.write( + // JSON.stringify({ + // type: 'GEEK_AUTO_START_CHAT_CAN_BE_RUN' + // }) + // ) + // break + // } + // case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': { + // resolve(data) + // break + // } + // case 'LOGIN_STATUS_INVALID': { + // await sleep(500) + // mainWindow?.webContents.send('check-boss-zhipin-cookie-file') + // return + // } + // default: { + // return + // } + // } + // }) + + subProcessOfPuppeteer!.once('exit', (exitCode) => { + subProcessOfPuppeteer = null + if (exitCode === AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) { + // means cannot find downloaded puppeteer + reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES') + } else { + mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped') + } + }) + + resolve(undefined) + }) + // TODO: + }) + ipcMain.handle('check-dependencies', async () => { const [anyAvailablePuppeteerExecutable] = await Promise.all([ getAnyAvailablePuppeteerExecutable() diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/bootstrap.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/bootstrap.ts new file mode 100644 index 0000000..18b1e72 --- /dev/null +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/bootstrap.ts @@ -0,0 +1,40 @@ +import { Browser } from 'puppeteer' +import puppeteer from 'puppeteer-extra' +import StealthPlugin from 'puppeteer-extra-plugin-stealth' +import { pageMapByName } from './index' + +import { readStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' +import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs' +const localStoragePageUrl = `https://www.zhipin.com/desktop/` +const bossChatUiUrl = `https://www.zhipin.com/web/geek/chat` +const bossCookies = readStorageFile('boss-cookies.json') +const bossLocalStorage = readStorageFile('boss-local-storage.json') + +puppeteer.use(StealthPlugin()) + +export async function bootstrap() { + const browser = await puppeteer.launch({ + headless: false, + ignoreHTTPSErrors: true, + defaultViewport: { + width: 1440, + height: 800 + }, + devtools: true + }) + + return browser +} + +export async function launchBoss(browser: Browser) { + const page = await browser.newPage() + //set cookies + for (let i = 0; i < bossCookies.length; i++) { + await page.setCookie(bossCookies[i]) + } + await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage) + await Promise.all([page.goto(bossChatUiUrl, { timeout: 0 }), page.waitForNavigation()]) + pageMapByName['boss'] = page + page.once('close', () => (pageMapByName['boss'] = null)) + return page +} diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts new file mode 100644 index 0000000..475115e --- /dev/null +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/boss-operation.ts @@ -0,0 +1,17 @@ +import { Page } from 'puppeteer' +import { sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs' + +export const sendLookForwardReplyEmotion = async (page: Page) => { + const emotionEntryButtonProxy = await page.$('.chat-conversation .message-controls .btn-emotion') + await emotionEntryButtonProxy!.click() + await sleepWithRandomDelay(1000) + const duckEmotionTabEntryProxy = await page.$( + '.chat-conversation .message-controls .emotion .emotion-tab .emotion-sort:nth-child(3)' + ) + await duckEmotionTabEntryProxy!.click() + await sleepWithRandomDelay(1500) + const lookForwardReplyEmojiProxy = await page.$( + `.chat-conversation .message-controls .emotion .emotion-box img[title=盼回复]` + ) + await lookForwardReplyEmojiProxy!.click() +} diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts new file mode 100644 index 0000000..0bb7724 --- /dev/null +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts @@ -0,0 +1,161 @@ +import { bootstrap, launchBoss } from './bootstrap' +import { MsgStatus, type ChatListItem } from './types' +import { Page } from 'puppeteer' +import { sendLookForwardReplyEmotion } from './boss-operation' +import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs' +import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited' + +export const pageMapByName: { + boss?: Page | null +} = {} + +export const runEntry = async () => { + try { + const canNotConfirmIfHasReadMsgTemplateList = [ + 'Boss还没查看你的消息', + '你与该职位竞争者PK情况', + '简历诊断提醒', + '附件简历还没准备好', + '开场问题,期待你的回答' + ].map((it) => new RegExp(it)) + const browser = await bootstrap() + await Promise.all([launchBoss(browser)]) + + await sleep(1000) + pageMapByName.boss!.bringToFront() + await sleep(2000) + + // check set security question tip modal + let setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$( + '.dialog-wrap.dialog-account-safe' + ) + if (setSecurityQuestionTipModelProxy) { + await sleep(1000) + setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$( + '.dialog-wrap.dialog-account-safe' + ) + const closeButtonProxy = await setSecurityQuestionTipModelProxy?.$('.close') + + if (setSecurityQuestionTipModelProxy && closeButtonProxy) { + await closeButtonProxy.click() + } + } + + let cursorToContinueFind = 0 + + // eslint-disable-next-line no-constant-condition + while (true) { + // find target boss - with unread icon, or recommend system message + const friendListData = (await pageMapByName.boss!.evaluate( + ` + document.querySelector('.main-wrap .chat-user')?.__vue__?.list + ` + )) as Array + const toCheckItemAtIndex = friendListData.findIndex( + (it, index) => + index >= cursorToContinueFind && + ((it.lastIsSelf && it.lastMsgStatus === MsgStatus.HAS_READ) || + canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) && + !it.unreadCount + ) + + if (toCheckItemAtIndex < 0) { + const isFinished = await pageMapByName.boss!.evaluate( + `(document.querySelector( + '.main-wrap .chat-user .user-list-content div[role=tfoot] .finished' + )?.textContent ?? '').includes('没有')` + ) + if (isFinished) { + // list has all loaded and no more target job + // go back to first job + cursorToContinueFind = 0 + await pageMapByName.boss?.evaluate(() => { + ;(() => { + document + .querySelector('.chat-content .user-list .user-list-content') + ?.__vue__.scrollToIndex(0) + })() + }) + await sleep(10000) + } else { + cursorToContinueFind = friendListData.length - 1 + await pageMapByName.boss?.evaluate(() => { + ;(() => { + document + .querySelector('.chat-content .user-list .user-list-content') + ?.__vue__.scrollToBottom() + })() + }) + await sleep(3000) + } + continue + } else { + cursorToContinueFind = toCheckItemAtIndex + await pageMapByName.boss?.evaluate((toCheckItemAtIndex) => { + ;(() => { + document + .querySelector('.chat-content .user-list .user-list-content') + ?.__vue__.scrollToIndex(toCheckItemAtIndex) + })() + }, toCheckItemAtIndex) + await sleep(3000) + + const targetElProxy = await (async () => { + const jsHandle = ( + await pageMapByName.boss?.evaluateHandle((encryptJobId) => { + const jobLiEls = document.querySelectorAll( + '.main-wrap .chat-user .user-list-content ul[role=group] li[role=listitem]' + ) + return [...jobLiEls].find((it) => { + return it.__vue__.source.encryptJobId === encryptJobId + }) + }, friendListData[toCheckItemAtIndex].encryptJobId) + )?.asElement() + return jsHandle + })() + await targetElProxy?.click() + await pageMapByName.boss!.waitForResponse((response) => { + if (response.url().startsWith('https://www.zhipin.com/wapi/zpchat/geek/historyMsg')) { + return true + } + return false + }) + } + await sleepWithRandomDelay(1500) + const bossInfo = await pageMapByName.boss?.evaluate(() => { + return document.querySelector('.chat-conversation')?.__vue__['bossInfo$'] + }) + + const historyMessageList = + ( + await pageMapByName.boss?.evaluate(() => { + return ( + document.querySelector('.main-wrap .chat-conversation .chat-record')?.__vue__ + ?.records$ ?? [] + ) + }) + )?.filter((msg) => ['received', 'sent'].includes(msg.style)) ?? [] + + const lastGeekMessageSendTime = + historyMessageList.findLast((it) => it.style === 'sent')?.time ?? 0 + if ( + historyMessageList[historyMessageList.length - 1].style === 'sent' && + historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ && + (!bossInfo.bothTalked || + !historyMessageList.filter((it) => it.style === 'received').length) && + // don't disturb too much + Date.now() - lastGeekMessageSendTime >= 8 * 60 * 60 * 1000 + ) { + await sleepWithRandomDelay(3250) + await sendLookForwardReplyEmotion(pageMapByName.boss!) + } else { + cursorToContinueFind += 1 + } + await sleep(3000) + } + } catch (err) { + console.error(err) + } +} + +attachListenerForKillSelfOnParentExited() diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/types.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/types.ts new file mode 100644 index 0000000..0ef6ebb --- /dev/null +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/types.ts @@ -0,0 +1,40 @@ +enum GoldGeekStatus { + WITHOUT = 0, + WITH = 1 +} +export enum MsgStatus { + BOSS_MESSAGE_OR_SYSTEM_MESSAGE = 0, + HAS_NOT_READ = 1, + HAS_READ = 2, + HAS_REVOKE = 3 +} +enum TipType { + EMPTY = 0 +} + +export interface ChatListItem { + name: string + avatar: string + encryptBossId: string + securityId: string + encryptJobId: string + brandName: string + friendSource: number + friendId: number + uniqueId: `${ChatListItem['friendId']}-${number}` + isTop: number // enum + isFiltered: boolean + relationType: number + sourceTitle: string + goldGeekStatus: GoldGeekStatus // enum + lastText: string + lastMessageId: string + unreadCount: number + lastMsgStatus: MsgStatus + lastTS: number + updateTime: number + filterReasonList: null | unknown + title: string + tipType: TipType + lastIsSelf: boolean +} diff --git a/packages/ui/src/main/index.ts b/packages/ui/src/main/index.ts index 3b22657..4d1665d 100644 --- a/packages/ui/src/main/index.ts +++ b/packages/ui/src/main/index.ts @@ -35,6 +35,11 @@ const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE launchBossSite() break } + case 'readNoReplyAutoReminder': { + const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER/index') + runEntry() + break + } default: { const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index') openSettingWindow() diff --git a/packages/ui/src/main/utils/attachListenerForKillSelfOnParentExited.ts b/packages/ui/src/main/utils/attachListenerForKillSelfOnParentExited.ts new file mode 100644 index 0000000..3b1a35c --- /dev/null +++ b/packages/ui/src/main/utils/attachListenerForKillSelfOnParentExited.ts @@ -0,0 +1,23 @@ +import { sleep } from '@geekgeekrun/utils/sleep.mjs' + +export default function attachListenerForKillSelfOnParentExited() { + // #region period check is parent process existed + // Store the parent process ID + const parentPID = process.ppid + // Function to check if the parent process is alive + async function periodCheckParentProcess() { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + // Try sending signal 0 to the parent process (this does not terminate the process) + process.kill(parentPID, 0) + } catch (err) { + // If an error is thrown, the parent process doesn't exist anymore + process.exit(0) + } + await sleep(1000) + } + } + periodCheckParentProcess() + // #endregion +} diff --git a/packages/ui/src/renderer/src/page/Configuration/GeekAutoStartChatWithBoss.vue b/packages/ui/src/renderer/src/page/Configuration/GeekAutoStartChatWithBoss.vue index edf9ffb..9768b38 100644 --- a/packages/ui/src/renderer/src/page/Configuration/GeekAutoStartChatWithBoss.vue +++ b/packages/ui/src/renderer/src/page/Configuration/GeekAutoStartChatWithBoss.vue @@ -92,7 +92,10 @@ const handleSubmit = async () => { await formRef.value!.validate() await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value)) - router.replace('/geekAutoStartChatWithBoss/prepareRun') + router.replace({ + path: '/geekAutoStartChatWithBoss/prepareRun', + query: { flow: 'geek-auto-start-chat-with-boss' } + }) } const handleSave = async () => { await formRef.value!.validate() diff --git a/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue new file mode 100644 index 0000000..fc4fe21 --- /dev/null +++ b/packages/ui/src/renderer/src/page/Configuration/ReadNoReplyReminder.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/ui/src/renderer/src/page/Configuration/index.vue b/packages/ui/src/renderer/src/page/Configuration/index.vue index 52cfa91..d1a257b 100644 --- a/packages/ui/src/renderer/src/page/Configuration/index.vue +++ b/packages/ui/src/renderer/src/page/Configuration/index.vue @@ -3,6 +3,8 @@