Compare commits

...

404 Commits

Author SHA1 Message Date
geekgeekrun
fff8ca7dff ui-v0.16.0 2026-02-22 14:57:05 +08:00
geekgeekrun
98ffc3a143 Merge branch 'feature/support-other-platform-and-add-global-setting' into feature/ui 2026-02-22 14:54:25 +08:00
geekgeekrun
70d6453e61 enhance ui text 2026-02-22 14:54:02 +08:00
geekgeekrun
679090c9af update README 2026-02-22 14:35:17 +08:00
geekgeekrun
fb7b545d10 remove debugger 2026-02-22 14:28:25 +08:00
geekgeekrun
88c3d63cb6 Merge remote-tracking branch 'origin/master' into feature/support-other-platform-and-add-global-setting 2026-02-22 14:24:08 +08:00
geekgeekrun
40328a24ef update README 2026-02-22 14:16:49 +08:00
geekgeekrun
b1282c8169 don't init props in fieldsForUseCommonConfig with true in boss.json 2026-02-22 13:29:35 +08:00
geekgeekrun
b3c7b287ab fix salary assert method 2026-02-22 13:14:01 +08:00
geekgeekrun
0ffab64653 add button to fill common config field 2026-02-22 10:35:06 +08:00
geekgeekrun
b94520e00a fix salaryMarkAsNotSuitLabelText 2026-02-22 10:11:58 +08:00
geekgeekrun
cac32b934a consume fieldsForUseCommonConfig in real run 2026-02-21 20:58:16 +08:00
geekgeekrun
0eb64a1c69 fix expectCityList in config is not fill back 2026-02-21 20:20:26 +08:00
geekgeekrun
ce651c8f7e move logic of mainWindow?.webContents.send 'common-job-condition-config-updated' 2026-02-21 16:27:34 +08:00
geekgeekrun
013d76726b add listener for common-job-condition-config-updated; enhance common config show in ghosting reminder ui 2026-02-21 10:20:36 +08:00
geekgeekrun-maintainer
4847e8e8d1 更新 README.md 2026-02-21 04:15:04 +08:00
geekgeekrun
2980aebf8d fix when change the checkbox of fieldsForUseCommonConfig, validate error message still visible 2026-02-20 20:42:59 +08:00
geekgeekrun
31236acda4 add common-job-condition-config migrate logic; add commonJobConditionConfig fill back logic 2026-02-20 16:08:41 +08:00
geekgeekrun
8506921ef2 enhance show logic of template dropdown 2026-02-20 12:43:09 +08:00
geekgeekrun
7561412d18 add logic to show common-job-condition-config window in ui 2026-02-20 12:33:12 +08:00
geekgeekrun
a8a6bd9174 enhance show logic of mark as not suit 2026-02-20 11:23:23 +08:00
geekgeekrun
64d10876c3 add checkbox and readonly input box about commonJobConditionConfig 2026-02-20 10:54:06 +08:00
geekgeekrun-maintainer
d0101cfdce 更新 README.md 2026-02-20 08:26:46 +08:00
geekgeekrun
ba7c61f22b add ui of commonJobConditionConfig 2026-02-19 19:16:33 +08:00
geekgeekrun
18b9868ff0 split parts in side nav bar 2026-02-14 20:13:02 +08:00
geekgeekrun
6743649f04 ui-v0.15.2 2026-02-12 08:00:32 +08:00
geekgeekrun
81f990084d Merge branch 'feature/ui' 2026-02-12 07:36:26 +08:00
geekgeekrun
7aa2cc1485 enhance ui text 2026-02-12 07:33:15 +08:00
geekgeekrun
60928b618c enhance log util about fail to JSON.stringify 2026-02-12 07:33:04 +08:00
geekgeekrun
01ce8c996d fix in production build process.env.NODE_ENV is still development 2026-02-12 07:00:30 +08:00
geekgeekrun
b8f6042f36 print log when launch with process.env.NODE_ENV === 'development' 2026-02-12 06:06:41 +08:00
geekgeekrun
1dad234dbc fix some feature cannot use on production build 2026-02-12 05:54:31 +08:00
geekgeekrun
3155017e73 ui-v0.15.1 2026-02-12 00:23:54 +08:00
geekgeekrun
b3dfd827ea Merge branch 'feature/ui' 2026-02-12 00:19:06 +08:00
geekgeekrun
960d937100 fix if blockCompanyNameRegMatchStrategy field show check; 2026-02-12 00:17:07 +08:00
geekgeekrun
69e5516898 ui-v0.15.0 2026-02-11 23:13:44 +08:00
geekgeekrun
333b9a4558 Merge branch 'feature/ui' 2026-02-11 23:12:52 +08:00
geekgeekrun
7e55641d86 Merge branch 'feature/multi-process' into feature/ui 2026-02-11 23:12:12 +08:00
geekgeekrun
58b836deb3 add isExtractDoneFlagFilePath to mark chrome extension extract done 2026-02-11 23:10:52 +08:00
geekgeekrun
56fc7e8aef change ui text 2026-02-11 22:59:28 +08:00
geekgeekrun
51920e54eb enhance ping 2026-02-11 22:58:50 +08:00
geekgeekrun
38296e5b5b change / page title 2026-02-11 22:13:43 +08:00
geekgeekrun
e52015ccb9 Merge branch 'feature/block-company-config' into feature/multi-process 2026-02-11 18:52:07 +08:00
geekgeekrun
8831ae4b17 add blockCompanyNameRegExpTemplateList 2026-02-11 03:11:03 +08:00
geekgeekrun
19a80cf18d enhance ui of running overlay 2026-02-11 01:52:43 +08:00
geekgeekrun
26a3beaced fix runing misspell 2026-02-10 20:52:51 +08:00
geekgeekrun
10075826dd fix VSERION misspell 2026-02-10 20:51:00 +08:00
geekgeekrun
efcb0f0783 skip saveLastUsedAndAvailableBrowserInfo after download done 2026-02-10 20:50:11 +08:00
geekgeekrun
b4ca041592 Merge remote-tracking branch 'origin/feature/block-company-config' into feature/multi-process 2026-02-10 18:51:18 +08:00
geekgeekrun
35f7e36ab7 Merge branch 'feature/multi-process' of github.com-geekgeekrun:geekgeekrun/geekgeekrun into feature/multi-process 2026-02-10 18:51:03 +08:00
geekgeekrun
7e4f044909 enhance display in mark as not suit record 2026-02-10 06:09:53 +08:00
geekgeekrun
a1f8b6c6f0 consume add block company config in ghosting reminder and auto start chat 2026-02-10 06:09:21 +08:00
geekgeekrun
c0326605a9 remove debugger 2026-02-10 05:30:35 +08:00
geekgeekrun
0460cb331d add block company config field in ui of ghosting reminder 2026-02-10 04:06:49 +08:00
geekgeekrun
85acb599d5 add block company config field in ui of auto start chat 2026-02-10 04:06:28 +08:00
geekgeekrun
f88e295138 enhance ui text 2026-02-09 20:53:35 +08:00
geekgeekrun
81ba666e27 enhance of aside nav ui with box-shadow 2026-02-09 12:09:05 +08:00
geekgeekrun
e33d77c57c change the default value of expectCompanies 2026-02-09 11:39:11 +08:00
geekgeekrun
1d8e99303d enhance ui text about 已读不回自动复聊 2026-02-09 11:20:08 +08:00
geekgeekrun
baef48752f enhance side nav ui 2026-02-09 08:38:03 +08:00
geekgeekrun
46c0bcba42 replace Boss in ui with BOSS 2026-02-09 08:14:39 +08:00
geekgeekrun
6ab40f759a move public setting entry to left side nav; rename feature ui text 2026-02-09 07:52:33 +08:00
geekgeekrun
bfb4fa6e8d enhance ux 2026-02-09 07:11:53 +08:00
geekgeekrun
55c1032ad6 add node-gyp 2026-02-09 04:22:04 +08:00
geekgeekrun
37ad84ff46 add loading status for auto detect browser 2026-02-09 00:03:53 +08:00
geekgeekrun
3077a0f68f add browser check and process continue run logic 2026-02-08 22:47:51 +08:00
geekgeekrun
68170e8b60 add pre-run-check handler 2026-02-08 19:39:37 +08:00
geekgeekrun
c298904d25 fix when worker process exited, ui no reflection 2026-02-08 18:22:12 +08:00
geekgeekrun
3eeea2e5c7 don't open start editthiscookie start page 2026-02-08 02:22:36 +08:00
geekgeekrun
e746bf70e9 add browser auto find logic to first run 2026-02-08 01:58:44 +08:00
geekgeekrun
8cc94baa18 add executable file basic check 2026-02-08 00:11:07 +08:00
geekgeekrun
3f2b709097 add browserDownloadProgress relevant logic 2026-02-07 23:34:50 +08:00
geekgeekrun
ebf512c86a add browser assistant entry on ui 2026-02-07 20:04:49 +08:00
geekgeekrun
9f97622da9 Merge branch 'master' into feature/multi-process 2026-02-07 04:53:51 +08:00
geekgeekrun
e2ed83c421 Merge remote-tracking branch 'origin/master' into feature/multi-process 2026-02-07 04:43:52 +08:00
geekgeekrun
8105f9933a add socketToWorkerIdSetMap to fix when user process exit, worker is still running; add isReset for ensureIpcPipeName to fix daemon may not be connected 2026-02-07 04:43:19 +08:00
geekgeekrun
f2cc87d660 make daemon keep running in background 2026-02-06 22:02:25 +08:00
geekgeekrun
dcf4706eb6 Merge remote-tracking branch 'origin/feature/ui' into feature/multi-process 2026-02-06 05:45:11 +08:00
geekgeekrun
eac79a9661 refactor code about collect job info 2026-02-06 05:43:53 +08:00
geekgeekrun
52094a4ce3 ui-v0.14.2 2026-02-05 21:49:55 +08:00
geekgeekrun
1d5fdb9d54 Merge branch 'feature/ui' 2026-02-05 21:45:20 +08:00
geekgeekrun
9fc16106cd enhance default html title 2026-02-05 21:42:54 +08:00
geekgeekrun
aff310b51a fix includes in not a function 2026-02-05 21:39:02 +08:00
geekgeekrun
3a9dcf13c7 Merge remote-tracking branch 'origin/feature/ui' into feature/ui 2026-02-05 21:09:09 +08:00
geekgeekrun
27652824d9 enhance perf of check if it is hiring of ghosting reminder 2026-02-05 20:57:35 +08:00
geekgeekrun
fb7f2bd0b1 add uncompressed laodeng plugin 2026-01-31 23:28:47 +08:00
geekgeekrun-maintainer
a70a316800 Refine audience section to target job seekers only
Updated section to specify the program's focus on job seekers and discourage use for other purposes.
2026-01-28 16:26:36 +08:00
geekgeekrun-maintainer
4d1c1c4f4b Add disclaimer for intended use of the program
Added a disclaimer to clarify the intended use of the program, emphasizing that it is designed to assist job seekers in finding meaningful employment and should not be used for other purposes.
2026-01-28 16:25:01 +08:00
geekgeekrun
c663b1be1c change timeout of get user info 2026-01-27 16:55:32 +08:00
geekgeekrun
f04d3843be Merge branch 'master' into feature/multi-process 2026-01-26 22:13:25 +08:00
geekgeekrun
0e60819df1 Merge branch 'feature/ui' 2026-01-26 22:06:21 +08:00
geekgeekrun
5d46607e0c Merge branch 'master' into feature/multi-process 2026-01-26 21:46:18 +08:00
geekgeekrun
9889182082 Merge branch 'master' into feature/ui 2026-01-26 21:43:18 +08:00
geekgeekrun
8a45c1bf04 Merge remote-tracking branch 'origin/master' into feature/ui 2026-01-26 21:42:08 +08:00
geekgeekrun
838d4e2da1 Merge remote-tracking branch 'origin/master' 2026-01-26 21:41:44 +08:00
geekgeekrun
097ada6bda ui-v0.14.1 2026-01-26 20:00:38 +08:00
geekgeekrun
a92b9ce040 Merge branch 'feature/add-city-filter-condition-constraint' into feature/ui 2026-01-26 19:59:22 +08:00
geekgeekrun
aa6527432d update app logo 2026-01-26 19:56:12 +08:00
geekgeekrun
64f9ddfb76 enhance gt of city chooser 2026-01-26 18:00:31 +08:00
geekgeekrun
6b131e708c enhance ui of city chooser 2026-01-26 17:42:15 +08:00
geekgeekrun
1e9f436633 update figure 2026-01-26 02:40:08 +08:00
geekgeekrun
0c815f2557 enhance ux of city chooser 2026-01-26 02:31:35 +08:00
geekgeekrun
b8753597da Merge branch 'feature/ui' into feature/add-city-filter-condition-constraint 2026-01-26 02:31:22 +08:00
geekgeekrun
98300ddf66 ui-v0.14.0 2026-01-25 15:26:59 +08:00
geekgeekrun
962dcb1125 update ci 2026-01-25 15:26:15 +08:00
geekgeekrun
a9823f1e24 Merge branch 'feature/add-city-filter-condition-constraint' into feature/ui 2026-01-25 15:14:31 +08:00
geekgeekrun
57eb09ed49 add ui of city filter setting; add slot for city chooser; fix sometimes filter not show 2026-01-25 15:13:37 +08:00
geekgeekrun
a8454cbaac add city filter condition in script 2026-01-25 15:07:33 +08:00
geekgeekrun
c5789679ba refactor login logic in ghosting reminder 2026-01-24 05:08:04 +08:00
geekgeekrun
204e15b5d8 enhance ux login flow of auto start chat 2026-01-24 04:26:51 +08:00
geekgeekrun
60dee6f435 migrate cookie assistant logic to standalone window make it promisify, and call it with ghosting remainder; disable pre-check when program start run. 2026-01-23 14:37:52 +08:00
geekgeekrun
5ab7fc2ee2 remove period screenshot 2026-01-22 13:24:43 +08:00
geekgeekrun
afe8a820c1 change sqlite backend; upgrade electron; fix ipc file cannot listen in macOS 2026-01-21 02:56:42 +08:00
geekgeekrun
c23d590052 pin pnpm version 2026-01-21 01:42:49 +08:00
geekgeekrun
0b50d2767a fix ipc file cannot listen in linux 2026-01-21 01:07:11 +08:00
geekgeekrun
a1f6dceedb hide task manager entry 2026-01-18 13:15:20 +08:00
geekgeekrun
f09db784fa enhance ux of stop button 2026-01-18 12:35:58 +08:00
geekgeekrun
0df3174ad9 visualize prerequisite check steps 2026-01-18 03:43:58 +08:00
geekgeekrun
ce32dc4c2f move find browser logic into util process 2026-01-17 23:32:19 +08:00
geekgeekrun
351f30b824 move stop button into dialog 2026-01-17 22:53:40 +08:00
geekgeekrun
754f942c86 file rename 2026-01-17 18:27:18 +08:00
geekgeekrun
5063f9c2aa fix screenshot error when page closed; catch EPIPE globally; rigister gui process 2026-01-17 18:07:50 +08:00
geekgeekrun
9639151fdd make task can be run standalone via arg 2026-01-17 15:37:54 +08:00
geekgeekrun
4cf6a7e36f use file instead of port to listen server 2026-01-17 12:36:03 +08:00
geekgeekrun
a05c73cbdf enhance daemon lifecycle logic like connect ensurance 2026-01-17 11:34:28 +08:00
geekgeekrun
199a472ec3 change config of externalizeDepsPlugin 2026-01-17 11:34:22 +08:00
geekgeekrun
5634ac0260 remove wrong import way 2026-01-17 11:34:13 +08:00
geekgeekrun
55a04bb1ad store job info when response 2026-01-16 06:11:35 +08:00
geekgeekrun
492538e663 don't make browser to be like windows; check if page closed before push screenshot 2026-01-15 17:20:10 +08:00
geekgeekrun
6f02160f28 add and patch AnonymizeUaPlugin; make version of find-chrome-bin fixed 2026-01-15 13:17:51 +08:00
geekgeekrun
860847963b add screenshot show 2026-01-15 12:46:16 +08:00
geekgeekrun
69ec879967 add page to show running task 2026-01-14 17:44:42 +08:00
geekgeekrun-maintainer
76985b8c7c update readme 2026-01-14 15:49:10 +08:00
geekgeekrun
45397bcc74 Merge branch 'feature/ui' into feature/multi-process 2026-01-14 10:26:01 +08:00
geekgeekrun-maintainer
8b41f479cf 更新 README.md 2026-01-13 09:44:49 +08:00
geekgeekrun
ddc8a81c7c ui-v0.13.3 2026-01-12 22:04:06 +08:00
geekgeekrun
ca37e258c6 change the way to load extension 2026-01-12 22:03:31 +08:00
geekgeekrun
454ee421bf Merge remote-tracking branch 'origin/feature/ui' into feature/ui 2026-01-12 21:29:41 +08:00
geekgeekrun
69bd774640 Merge remote-tracking branch 'origin/feature/ui' into feature/ui 2026-01-12 21:27:28 +08:00
geekgeekrun
20f081a245 ui-v0.13.2 2026-01-12 21:26:08 +08:00
geekgeekrun
252869576b restore find logic of find pre installed chrome 2026-01-12 21:19:58 +08:00
geekgeekrun
98ab2acf50 ui-v0.13.1 2026-01-12 17:37:39 +08:00
geekgeekrun
dbc127be06 add retry for browser download error 2026-01-12 17:36:05 +08:00
geekgeekrun
82f19d772d ui-v0.13.0 2026-01-12 16:39:49 +08:00
geekgeekrun
94de507c9a don't use local browser anymore 2026-01-12 16:37:52 +08:00
geekgeekrun
c1faecaaed compress code of laodeng 2026-01-12 16:28:03 +08:00
geekgeekrun
880557c1ad update ci 2026-01-11 15:06:52 +08:00
geekgeekrun
3b318262a4 update ci 2026-01-11 14:52:43 +08:00
geekgeekrun
f9786d5d82 update ci 2026-01-11 14:16:09 +08:00
geekgeekrun
b583951cb5 Revert "update ci"
This reverts commit d95abd3ad1.
2026-01-11 14:02:01 +08:00
geekgeekrun
d95abd3ad1 update ci 2026-01-11 13:54:27 +08:00
geekgeekrun
2e9d3af276 update ci 2026-01-11 13:45:14 +08:00
geekgeekrun
fbeb01ab48 update github actions token 2026-01-11 11:18:27 +08:00
geekgeekrun
6db6ef4972 update github actions token and permission 2026-01-11 09:48:51 +08:00
geekgeekrun
7b3390745a update github actions token and permission 2026-01-11 09:31:00 +08:00
geekgeekrun
b2b1cecfa9 update github actions token 2026-01-11 09:07:14 +08:00
geekgeekrun
61d7d968a1 edit release ci 2026-01-11 01:14:29 +08:00
geekgeekrun
c18930f76e edit release ci 2026-01-11 01:12:09 +08:00
geekgeekrun
fc13742d52 change dependencies of laodeng 2026-01-11 01:06:25 +08:00
geekgeekrun
af05a5555e encode browser code with emotion text 2026-01-10 23:14:17 +08:00
geekgeekrun
bc57a07a0a run laodeng when any page created 2026-01-10 21:45:16 +08:00
geekgeekrun
265f2f89e5 Merge remote-tracking branch 'origin/master' into feature/ui 2026-01-10 17:16:32 +08:00
geekgeekrun
206d50ca61 bypass anti debug of target site 2026-01-10 17:16:23 +08:00
geekgeekrun-maintainer
9324f20ad3 Update job search timeline and experiences 2026-01-09 16:49:07 +08:00
geekgeekrun-maintainer
632a11b105 更新 README.md 2026-01-09 13:57:08 +08:00
geekgeekrun
933caaff2a add run-record-id arg 2026-01-08 00:07:59 +08:00
geekgeekrun
8739a70277 migrate daemon to new daemon 2026-01-07 23:21:05 +08:00
geekgeekrun
18af99f10e sendResponse pass callbackUuid directly 2026-01-04 10:54:51 +08:00
geekgeekrun
81e40230d1 WIP: migrate to mulit process manager 2026-01-04 10:32:43 +08:00
geekgeekrun
b730901775 launch daemon in main process 2025-12-31 15:15:24 +08:00
geekgeekrun
f4ca2ebbbd add @geekgeekrun/pm 2025-12-29 23:38:37 +08:00
geekgeekrun
418ca66a30 ui-v0.12.0 2025-12-24 23:43:32 +08:00
geekgeekrun
0928b402fb Merge remote-tracking branch 'origin/master' into feature/ui 2025-12-24 21:48:36 +08:00
geekgeekrun-maintainer
373e45dee8 update readme 2025-12-24 16:35:17 +08:00
geekgeekrun
8bbfa5b41e add job status check for ghosting reminder 2025-12-24 15:44:15 +08:00
geekgeekrun
aa410cfc60 add job hire status record table and relevant save logic 2025-12-23 17:05:21 +08:00
geekgeekrun
e986fddbfb do not remind chat manual revoked by user 2025-12-23 13:24:52 +08:00
geekgeekrun
cf7a0d3902 Merge branch 'feature/ui' 2025-12-17 14:32:56 +08:00
geekgeekrun
c95349c037 Merge remote-tracking branch 'origin/master' into feature/ui 2025-12-16 23:07:02 +08:00
geekgeekrun-maintainer
8197782102 Replace old image with new one in README
Updated image in README.md to a new source.
2025-12-15 22:59:44 +08:00
geekgeekrun
f17d626ea0 ui-v0.11.0 2025-12-15 18:02:41 +08:00
geekgeekrun
2b4b620977 fix match rule for multi line in testIfJobTitleOrDescriptionSuit 2025-12-15 18:01:52 +08:00
geekgeekrun
03e3c3661f adjust default config of sage time 2025-12-14 20:59:50 +08:00
geekgeekrun
5d787fdd50 add sage time intro 2025-12-14 13:22:56 +08:00
geekgeekrun
b68ff63b6b add sage time gtag in renderer process 2025-12-14 12:08:11 +08:00
geekgeekrun
2aee76307f add sage time gtag in main process 2025-12-14 11:15:22 +08:00
geekgeekrun
e2040caa38 add sage time ui, make it configurable 2025-12-14 10:46:19 +08:00
geekgeekrun
fba45e420f add sage time for pausing when request count to a time 2025-12-13 15:15:09 +08:00
geekgeekrun
fe629eaa22 ui-v0.10.2 2025-12-12 22:10:32 +08:00
geekgeekrun
056ec7a0a6 handle dialog about /猎头/ during startup chat 2025-12-12 22:03:56 +08:00
geekgeekrun
f27ca4145a handle dialog about /剩\d+次沟通机会/ during startup chat 2025-12-12 22:02:57 +08:00
geekgeekrun
cd09210017 add the tip dialog of LOGIN_STATUS_INVALID 2025-12-12 20:14:04 +08:00
geekgeekrun
aec72c28f0 ui-v0.10.1 2025-12-10 14:44:21 +08:00
geekgeekrun
69cf5fd34f upgrade macOS version in build ci workflow file 2025-12-10 14:44:01 +08:00
geekgeekrun
5f47e47448 upgrade macOS version in build ci workflow file 2025-12-10 13:26:29 +08:00
geekgeekrun
4e9b44d04c fix for misspell of scale property. 2025-12-10 13:16:15 +08:00
geekgeekrun
d206d45242 fix when return value of formatStaticCombineFilters is a empty array, program cannot run correctly 2025-12-10 13:15:55 +08:00
geekgeekrun
0aef2af350 ui-v0.10.0 2025-11-01 23:45:16 +08:00
geekgeekrun
c3dc75b503 ui-v0.9.5 2025-11-01 23:15:13 +08:00
geekgeekrun
7a1ae48aa7 Merge branch 'feature/static-combine-job-filter' into feature/ui 2025-11-01 23:14:47 +08:00
geekgeekrun
e5225006cc form validate stage change 2025-11-01 23:13:24 +08:00
geekgeekrun
fdf2c61620 support multi line regexp 2025-11-01 00:39:44 +08:00
geekgeekrun
1f140121fb handle duplicate static combine conditions 2025-10-26 19:12:13 +08:00
geekgeekrun
57cf631b86 add static combine job filter 2025-10-26 18:00:35 +08:00
geekgeekrun
36aef1c16d add more position presets 2025-10-24 01:42:30 +08:00
geekgeekrun
d21972020e ui-v0.9.4 2025-10-14 01:52:07 +08:00
geekgeekrun
51ca5dd71d fix when choose "应届生" in "工作经验", every job is marked as not suit 2025-10-14 01:49:57 +08:00
geekgeekrun
e7d5ef382e ui-v0.9.3 2025-10-13 00:44:31 +08:00
geekgeekrun
59a36dc7bd Merge remote-tracking branch 'origin/master' into feature/ui 2025-10-13 00:43:05 +08:00
geekgeekrun
73b57e21e4 fix tip of daily chat startup chance used out. fix noOpCondition is not take effect. 2025-10-13 00:42:48 +08:00
geekgeekrun
6f0d98653c enhance ui text 2025-10-13 00:39:31 +08:00
geekgeekrun
3bf6d4c358 enhance electron.vite.config.ts 2025-10-04 01:24:18 +08:00
geekgeekrun
2c7ecafee2 fix issue will cause app crash when mark job not suit due to salary; fix salary compare logic 2025-10-04 01:23:01 +08:00
geekgeekrun
f88a5a2889 Merge branch 'feature/ui' 2025-08-17 13:51:09 +08:00
geekgeekrun
ac19ae3832 ui-v0.9.2 2025-08-17 11:16:42 +08:00
geekgeekrun
a25b047f99 fix ci error  ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with packages/ui/package.json 2025-08-17 11:16:14 +08:00
geekgeekrun
42fc66a0f4 fix cannot run due to lodash-es is not bundled; fix logic about set security question tip modal 2025-08-17 11:11:15 +08:00
geekgeekrun
203fbba9a0 ui-v0.9.1 2025-08-17 01:53:49 +08:00
geekgeekrun
b821bc500c fix error for cannot find chooseReasonDialogProxy 2025-08-17 01:50:51 +08:00
geekgeekrun
0032a356e5 fix: reduce argument getBossIdsByJobIds count to fix error: Error: SQLITE_ERROR: Expression tree is too large (maximum depth 1000) 2025-08-17 00:04:05 +08:00
geekgeekrun
b9c4976291 fix: cannot run due to QueryFailedError: SQLITE_ERROR: near ")": syntax error - when one of jobNotMatchStrategy/jobNotActiveStrategy/expectCityNotMatchStrategy set to MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL and chattedJobIds.length === 0, application will crash 2025-08-17 00:00:34 +08:00
geekgeekrun
558891ad9f Merge branch 'master' into feature/ui 2025-08-16 16:38:34 +08:00
geekgeekrun-maintainer
d2da9f5a7d Update README.md 2025-07-31 01:18:29 +08:00
geekgeekrun-maintainer
71068be21f Update README.md 2025-07-25 02:27:45 +08:00
geekgeekrun-maintainer
2b0cf2a2fa Update README.md 2025-07-15 13:44:41 +08:00
geekgeekrun-maintainer
ceaf68e4eb Update README.md 2025-07-15 13:28:39 +08:00
geekgeekrun-maintainer
10cc248784 Update README.md 2025-07-15 13:23:04 +08:00
geekgeekrun
1d155e498a ui-v0.9.0 2025-07-13 21:12:01 +08:00
geekgeekrun
d3e086f321 Merge branch 'feature/ui' 2025-07-13 21:10:34 +08:00
geekgeekrun
5f97b3ccec Merge remote-tracking branch 'origin/master' into feature/ui 2025-07-13 21:10:07 +08:00
geekgeekrun
7dd0c675d5 enhance agreement 2025-07-13 21:09:40 +08:00
geekgeekrun-maintainer
c1978417da Update README.md 2025-07-13 20:16:12 +08:00
geekgeekrun
e0670a9f30 enhance gtag 2025-07-13 19:53:25 +08:00
geekgeekrun
b05031be8a adjust job-source-dragged gtag value 2025-07-13 18:48:05 +08:00
geekgeekrun
7b10e97fa8 collect jobSource when manual startup chat running; add jobSource in gtag both auto or manual 2025-07-13 18:40:59 +08:00
geekgeekrun
e867acb39d collect jobSource when auto startup chat running 2025-07-13 14:27:43 +08:00
geekgeekrun
b31224a81c add job source column migration for db 2025-07-13 14:19:23 +08:00
geekgeekrun
ddb0cb97d7 add gtag 2025-07-13 11:32:54 +08:00
geekgeekrun
f338dcffc8 enhance ui text for SearchSourceTipText 2025-07-13 01:32:36 +08:00
geekgeekrun
84a79c9bae enhance job-detail-filter error style 2025-07-13 00:55:43 +08:00
geekgeekrun
44b0e44e2e enhance ux of job source 2025-07-13 00:34:57 +08:00
geekgeekrun
08028f37dd add fallback logic when find no JobSource enabled 2025-07-12 20:04:32 +08:00
geekgeekrun
a0b09ebae4 Merge remote-tracking branch 'origin/master' into feature/ui 2025-07-12 19:32:50 +08:00
geekgeekrun
f078500c14 add core logic for job source 2025-07-12 19:31:14 +08:00
geekgeekrun
b89d6e264d fix wrong call way for mainFlowWillLaunch hook 2025-07-12 18:50:40 +08:00
geekgeekrun
78ddc949f4 enhance ui text 2025-07-12 13:01:48 +08:00
geekgeekrun
a208ffe7d4 fix TypeError: notSuitConditionHandleMap[markOnBossCondition] is not a function 2025-07-08 10:07:18 +08:00
geekgeekrun
bc70031e2b add desc of position filter 2025-07-06 04:33:34 +08:00
geekgeekrun
cc821f46f5 add logic to skip chatted boss in 30 days for auto-startup-chat 2025-07-06 02:51:12 +08:00
geekgeekrun
f668b3e5f5 add the logic to mark unsuitable job on local 2025-07-06 02:00:53 +08:00
geekgeekrun
d1441b5761 WIP: add the feature to apply job from recommend list and search list - TODO: handle mark as not suit 2025-07-05 20:09:17 +08:00
geekgeekrun-maintainer
e88b4da53a Update README.md 2025-07-05 12:25:27 +08:00
geekgeekrun-maintainer
d218778690 Update README.md 2025-07-05 12:14:12 +08:00
geekgeekrun-maintainer
f903ef9175 Update README.md 2025-07-05 03:08:29 +08:00
geekgeekrun-maintainer
d114685116 Update README.md 2025-07-05 02:52:25 +08:00
geekgeekrun
bb30329d6c move backup filter condition to top 2025-07-04 21:44:50 +08:00
geekgeekrun
3683ead1d4 allow skip empty filter condition when set backup filter condition to search job 2025-07-04 21:25:47 +08:00
geekgeekrun
cac3e480e5 enhance intro text about job detail filter; adjust expectJobFilterTemplateList 2025-06-29 15:27:48 +08:00
geekgeekrun
52163342c3 add jobDetailRegExpMatchLogic option related flow in core script 2025-06-29 14:48:40 +08:00
geekgeekrun
8efce15fd5 enhance ui after add jobDetailRegExpMatchLogic option; change layout of job detail RegExp option 2025-06-29 05:30:39 +08:00
geekgeekrun
d5dcdb4bd1 add jobDetailRegExpMatchLogic option in UI 2025-06-29 04:32:28 +08:00
geekgeekrun
98b99c5d14 Merge branch 'master' into feature/ui 2025-06-29 02:19:54 +08:00
geekgeekrun-maintainer
d4fa72a28e Update README.md 2025-06-28 12:46:55 +08:00
geekgeekrun
b024c6b517 ui-v0.8.0 2025-06-08 10:23:32 +08:00
geekgeekrun
9b64f05cb6 Merge branch 'feature/ui' 2025-06-08 10:22:48 +08:00
geekgeekrun
eefb25a3da enhance ui appearance 2025-06-08 10:20:07 +08:00
geekgeekrun
2dad39c8e4 disable 在校生 option of work exp 2025-06-03 00:55:56 +08:00
geekgeekrun
699bdd2641 add filter logic of job salary in script 2025-06-03 00:55:44 +08:00
geekgeekrun
48f1232f7d adjust priority of mark as not suit strategy when a job match more than one condition 2025-06-02 21:53:12 +08:00
geekgeekrun
2409699ccf add filter logic of job work exp in script 2025-06-02 16:26:31 +08:00
geekgeekrun
64c007066e fix ui display 2025-06-02 11:05:43 +08:00
geekgeekrun
bd503eb268 add work experience filter ui 2025-06-01 23:20:18 +08:00
geekgeekrun
8dc8ca825d add salary filter ui 2025-06-01 21:41:09 +08:00
geekgeekrun-maintainer
8e8fb6d331 Update README.md 2025-06-01 14:59:54 +08:00
geekgeekrun
2f3a557fb5 update element-plus 2025-05-31 15:52:15 +08:00
geekgeekrun
780cd73e46 Merge remote-tracking branch 'origin/master' 2025-05-30 23:40:07 +08:00
geekgeekrun-maintainer
6c05128e89 Update README.md 2025-05-29 23:42:23 +08:00
geekgeekrun
e235ae099b ui-v0.7.1 2025-05-29 03:09:57 +08:00
geekgeekrun
d479687b8b Merge branch 'feature/ui' 2025-05-29 03:08:01 +08:00
geekgeekrun
4627b4218e fix commandline args 2025-05-29 02:41:38 +08:00
geekgeekrun
b64f411e4a add edge to block list due to https://github.com/geekgeekrun/geekgeekrun/issues/5 2025-05-29 01:55:35 +08:00
geekgeekrun
12ff9dc4ba adjust commandline; adjust base url of download browser 2025-05-29 01:42:23 +08:00
geekgeekrun
959c32eb63 Merge branch 'master' into feature/ui 2025-05-28 23:52:42 +08:00
geekgeekrun
ce38ecea55 ui-v0.7.0 2025-05-28 03:54:22 +08:00
geekgeekrun
340f8946b9 Merge branch 'feature/ui' 2025-05-28 03:50:26 +08:00
geekgeekrun
59002f73e9 add intro for how to fill detail filter 2025-05-28 03:49:05 +08:00
geekgeekrun
5626cf6721 add guide for stop trace one boss 2025-05-28 02:23:50 +08:00
geekgeekrun
fe1c85b38d add comment for expectCityNotMatchStrategy === MarkAsNotSuitOp.NO_OP 2025-05-27 02:04:50 +08:00
geekgeekrun
24018f63b5 enhance ui text 2025-05-27 01:38:30 +08:00
geekgeekrun
5effcc27ab add strategyScopeOptionWhenMarkJobCityNotMatch option; change default value of expectCityNotMatchStrategy to noop; enhance ui 2025-05-27 01:25:39 +08:00
geekgeekrun
aee6340c6a show mark as not suit for city reason in not suit record table 2025-05-25 22:45:13 +08:00
geekgeekrun
03df5f8d5a add city not suit mark in main process 2025-05-25 13:30:44 +08:00
geekgeekrun
7d3a0e80d4 enhance ui appearance of auto start chat config 2025-05-25 01:30:32 +08:00
geekgeekrun
10f1f150bf enhance ui appearance of auto start chat config 2025-05-25 00:41:09 +08:00
geekgeekrun
64249593b8 add basic ui for expectCityList config 2025-05-24 18:30:48 +08:00
geekgeekrun
217b2bbf2a move runMode from env to args. remove support of cli mode. 2025-05-24 14:09:57 +08:00
geekgeekrun-maintainer
02667fe2f8 Update README.md 2025-05-24 14:09:57 +08:00
geekgeekrun
5539842392 Merge branch 'feature/ui' of github.com-geekgeekrun:geekgeekrun/geekgeekrun into feature/ui 2025-05-21 02:17:38 +08:00
geekgeekrun
b57e646147 move runMode from env to args. remove support of cli mode. 2025-05-21 02:17:27 +08:00
geekgeekrun-maintainer
6982f6028d Update README.md 2025-05-17 17:38:40 +08:00
geekgeekrun
f4cd4b884b ui-v0.6.1 2025-05-10 23:39:43 +08:00
geekgeekrun
61c28ae164 fix error popped when first run and init db 2025-05-10 23:04:07 +08:00
geekgeekrun
ece7fc61e3 ui-v0.6.0 2025-05-08 03:35:30 +08:00
geekgeekrun
97793adb0f check message list when find last message is 开场问题,期待你的回答 2025-05-08 03:33:23 +08:00
geekgeekrun
b5c3116c01 add onlyRemindBossWithExpectJobType option for autoReminder 2025-05-06 06:31:14 +08:00
geekgeekrun
10cd1cf11e ui-v0.5.0 2025-05-03 16:50:54 +08:00
geekgeekrun
7d2e60dfd0 update company list 2025-05-03 16:23:37 +08:00
geekgeekrun
6986fbdc3c disable synchronize when typeorm db init, all schema change depend on migration 2025-05-03 15:03:25 +08:00
geekgeekrun
5174da2c6c add tip about wrong mark as not suit 2025-05-03 03:48:52 +08:00
geekgeekrun
043d782a05 add logic support customize job no active time range. add standalone logic to handle no active job. fix logic of set tempTargetJobIndexToCheckDetail 2025-05-03 03:31:39 +08:00
geekgeekrun
67e4b94f9e add logic to handle if mark as not suit when job detail is not match 2025-05-01 22:01:05 +08:00
geekgeekrun-maintainer
45d14e70fb Update README.md 2025-04-30 00:23:40 +08:00
geekgeekrun-maintainer
44202b4685 Update README.md 2025-04-29 23:51:41 +08:00
geekgeekrun-maintainer
00c699d1a6 Update README.md 2025-04-29 23:33:28 +08:00
geekgeekrun
a49124510b ui-v0.4.0 2025-04-29 13:11:58 +08:00
geekgeekrun-maintainer
25e5253022 Update README.md 2025-04-29 13:06:55 +08:00
geekgeekrun
cd4a2e9694 remove logic to scroll top before start chat 2025-04-29 03:01:32 +08:00
geekgeekrun
cc6e7e9444 enhance icon style 2025-04-29 02:29:55 +08:00
geekgeekrun
b650c7d86b fix java regexp to distinguish javascript 2025-04-29 02:22:18 +08:00
geekgeekrun
e616b6668e enhance readme 2025-04-29 02:05:57 +08:00
geekgeekrun
1803b852db change ui text 2025-04-29 01:59:28 +08:00
geekgeekrun-maintainer
bde36317a1 Update README.md 2025-04-29 01:57:53 +08:00
geekgeekrun
28d2d34763 enhance page_view gtag 2025-04-28 01:28:43 +08:00
geekgeekrun
05702e773f add more companies 2025-04-28 00:57:23 +08:00
geekgeekrun
6605c75ad7 expand time for wait recommendJobPageUrl navigation 2025-04-27 13:27:43 +08:00
geekgeekrun
1527a823c8 add page_view gtag 2025-04-27 13:08:38 +08:00
geekgeekrun
4dbc39008e fix the logic of get bothTalked 2025-04-27 00:15:42 +08:00
geekgeekrun
1d604fc364 show frame for readNoReplyReminderLlmMockWindow 2025-04-26 22:15:16 +08:00
geekgeekrun
f6e1b799ce fix update-window-size is no effect on windows 2025-04-26 22:10:34 +08:00
geekgeekrun
6f9533c2b7 fix checkIsResumeContentValid 2025-04-26 21:27:05 +08:00
geekgeekrun
7868f100ce chang url of recommend job page 2025-04-26 19:21:09 +08:00
geekgeekrun
a22ac20b3a fix default value of rechatLlmFallback in ui 2025-04-26 18:30:32 +08:00
geekgeekrun
e2c94ded13 Merge branch 'feature/update-notify' into feature/ui 2025-04-26 17:15:11 +08:00
geekgeekrun
84c30fc9a7 add gtag for mock chat 2025-04-26 17:05:25 +08:00
geekgeekrun
b88879a3bc add pinia; call check update in ui 2025-04-26 16:13:19 +08:00
geekgeekrun
33f6837147 add check update logic 2025-04-26 14:40:55 +08:00
geekgeekrun
9128865bbc Merge branch 'feature/auto-reminder-gpt' into feature/ui 2025-04-26 12:53:15 +08:00
geekgeekrun
8347fb0f77 add delay for no position when have traversed all job 2025-04-26 11:08:51 +08:00
geekgeekrun
7f21ea8c95 remove null in value of gtag payload 2025-04-26 11:00:06 +08:00
geekgeekrun
353fc60891 fix style column of recorded message is null 2025-04-26 10:52:42 +08:00
geekgeekrun
f770e7fe06 fix setFilterCondition related logic in start chat since website upgrade 2025-04-26 02:04:58 +08:00
geekgeekrun
ccbc717a45 fix basic start chat logic since website upgrade 2025-04-25 00:31:57 +08:00
geekgeekrun
5dc7250634 fix gtag name 2025-04-24 01:17:06 +08:00
geekgeekrun
5a1daf034f add template for expect company and job info filter 2025-04-24 01:05:34 +08:00
geekgeekrun
128ccfa93d add mark for message will be used as context in llm test dialog 2025-04-23 02:57:26 +08:00
geekgeekrun
582eae0900 add error tip in llm test dialog 2025-04-23 02:14:07 +08:00
geekgeekrun
d23b0475dc change the scope of blockModelSet from during process to during one time request; add the feature to specify a fixed model in test dialog 2025-04-23 02:00:03 +08:00
geekgeekrun
bbd670b674 change providerApiSecretMd5 column to providerApiSecret for LlmModelUsageRecord table 2025-04-22 13:54:54 +08:00
geekgeekrun
3d148e8e01 add basic llm testing window 2025-04-22 03:20:37 +08:00
geekgeekrun
954553d8d8 enhance gtag name and args 2025-04-22 00:18:45 +08:00
geekgeekrun
011c22e0fe ignore case sensitive of expectCompanies 2025-04-20 14:12:40 +08:00
geekgeekrun
4a82e9e345 enhance handle the scene resume might not exist - show dialog to lead user edit 2025-04-20 13:00:03 +08:00
geekgeekrun
f182ee2c5b handle the scene resume might not exist 2025-04-20 06:13:26 +08:00
geekgeekrun
3fc7662f7d fix inserted completionTokens is null 2025-04-20 00:52:06 +08:00
geekgeekrun
e8d3313a7b add union index for LlmModelUsageRecord 2025-04-20 00:30:08 +08:00
geekgeekrun
e823590e6d add gtag when message sent 2025-04-19 16:40:49 +08:00
geekgeekrun
04c02e148f add gtag for parse llm response error 2025-04-19 15:22:51 +08:00
geekgeekrun
dc293f4f91 remove the tailing 。for text generated by ai 2025-04-19 14:52:13 +08:00
geekgeekrun
f7fb551582 change order for getAutoStartChatRecord, getMarkAsNotSuitRecord 2025-04-19 12:28:28 +08:00
geekgeekrun
703b229bf4 add gtag for tooltip 2025-04-19 11:35:17 +08:00
geekgeekrun
64e9d3068c add more gtag for stop button 2025-04-19 03:02:28 +08:00
geekgeekrun
1d99cde602 add waitForFunction to fix chat list might be undefined 2025-04-19 02:51:45 +08:00
geekgeekrun
7ffe89c41f add check llm config logic before run read no reply reminder 2025-04-19 02:30:56 +08:00
geekgeekrun
bf0518036c add more help and intro text 2025-04-19 00:59:21 +08:00
geekgeekrun
f4634c5305 add split filter for expectJobName/expectJobType/expectJobDesc 2025-04-18 03:11:39 +08:00
geekgeekrun
e838f48b89 add LlmModelUsageRecord table 2025-04-16 03:00:32 +08:00
geekgeekrun
94e237fee8 constrain edge value of serveWeight for llm 2025-04-16 00:32:06 +08:00
geekgeekrun
76339ebc5c add rechatLlmFallback config for auto reminder 2025-04-15 03:25:51 +08:00
geekgeekrun
6dc3948d12 add block list to save unavailable model and prevent use again 2025-04-15 01:13:15 +08:00
geekgeekrun
764d278c23 make rechatContentSource take effect; change window title 2025-04-14 23:41:35 +08:00
geekgeekrun
73d03cb76a adjust the notice text of resume content not enough 2025-04-14 00:43:07 +08:00
geekgeekrun
d42c651425 add form validator for resume editor 2025-04-14 00:11:06 +08:00
geekgeekrun
7b5ff9951b add logic to check if resume content rich enough 2025-04-13 23:47:25 +08:00
geekgeekrun
0e85942de5 add form validator for llm config 2025-04-13 21:30:26 +08:00
geekgeekrun
462276cf6e fix wrong display of JobInfoSnapshot 2025-04-13 19:48:01 +08:00
geekgeekrun
1605103ee9 adjust params of renderer gtag 2025-04-13 19:34:23 +08:00
geekgeekrun
21c1f4284f add gtag ping for ReadNoReplyReminder 2025-04-13 19:32:22 +08:00
geekgeekrun
4208996a83 add gtag for renderer 2025-04-13 18:55:19 +08:00
geekgeekrun
3c951407ce adjust ui for ul in alert 2025-04-13 13:51:27 +08:00
geekgeekrun
438bc973a2 adjust ui text; add more intro; add 3rd list about apply a secret 2025-04-13 13:47:01 +08:00
geekgeekrun
b7f149032a revert: remove ChangeJobLibraryView1744467048874 2025-04-13 13:24:10 +08:00
geekgeekrun
a8a1856527 add latestLogDate column for job library; fix wrong time displayed 2025-04-13 11:08:31 +08:00
geekgeekrun
1c616e5b90 enhance ui style; fix devtools cannot popup 2025-04-12 21:39:37 +08:00
geekgeekrun
e162bc4948 add example of free.v36.cm 2025-04-12 19:52:17 +08:00
geekgeekrun
01ab679376 make prompt can be read from local file 2025-04-12 19:37:44 +08:00
geekgeekrun
f91daf7cab enhance intro of read no reply reminder 2025-04-12 15:08:14 +08:00
geekgeekrun
f72a5c014b add logic to random pick a llm config 2025-04-12 14:36:40 +08:00
geekgeekrun
c5ac63b030 add ui for multi llm config. dynamic adjust llm window height 2025-04-12 14:36:37 +08:00
geekgeekrun
924febd5e0 make llm config supporting array config 2025-04-12 14:36:33 +08:00
geekgeekrun
17c5a40f8d upgrade EditThisCookie.extract ensureEditThisCookie 2025-04-12 14:36:30 +08:00
geekgeekrun
09c5ce97a7 change providerCompleteApiUrl of deepseek 2025-04-12 14:36:27 +08:00
geekgeekrun
901c3a51a0 feat: enhance resume editor and formatResumeJsonToMarkdown; add the logic to send resume read from local to llm 2025-04-12 14:36:24 +08:00
geekgeekrun
0714b187bd add work/project description 2025-04-12 14:36:20 +08:00
geekgeekrun
c40fbde5b2 add ui for resume editor 2025-04-12 14:36:16 +08:00
geekgeekrun
27bc63f63f feat: adjust prompt; make gpt request can read config from file;add recentMessageQuantityForLlm config; 2025-04-12 14:36:11 +08:00
geekgeekrun
85e27dc6ef add basic code to call deepseek 2025-04-12 14:35:58 +08:00
geekgeekrun
7458067708 add standalone window for llm config 2025-04-06 23:27:56 +08:00
geekgeekrun
5b78dc5d63 refactor: change router for main-layout 2025-04-06 00:44:19 +08:00
geekgeekrun
7ccf4303d3 Merge branch 'feature/ui' into feature/aigc 2025-04-06 00:27:01 +08:00
geekgeekrun
7ee8baa311 extract messageForSaveFilter 2025-04-05 03:41:52 +08:00
geekgeekrun
acd8227740 ui-v0.3.2 2025-03-31 23:40:47 +08:00
geekgeekrun
482ea006be fix auto reminder cannot work due to chat ui modified 2025-03-31 23:39:50 +08:00
geekgeekrun
653173e1a3 refactor the ui of ReadNoReplyReminder 2025-01-05 09:52:19 +08:00
geekgeekrun
639ea6ac76 add the config of autoReminder.rechatContentSource in UI 2025-01-01 13:31:53 +08:00
174 changed files with 24604 additions and 3219 deletions

View File

@@ -60,9 +60,9 @@ jobs:
strategy:
matrix:
platform:
- os: macos-13 # for x64 build
- os: macos-15-intel # for x64 build
arch: x64
- os: macos-14 # for arm64 build
- os: macos-15 # for arm64 build
arch: arm64
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
@@ -149,9 +149,11 @@ jobs:
- name: Display structure of downloaded files
run: ls -llR geekgeekrun-ui@${{ github.sha }}
- name: Create release
- name: Create release for public
uses: ncipollo/release-action@v1
with:
repo: geekgeekrun
prerelease: true
allowUpdates: true
artifacts: geekgeekrun-ui@${{ github.sha }}/*
tag: ${{ github.ref }}

164
README.md
View File

@@ -1,15 +1,100 @@
# 牛人快跑 - GeekGeekRun
一款可以帮助你在Boss直聘上**自动批量开聊Boss**的脚本基于Puppeteer。
一款可以帮助你在BOSS直聘上**自动批量开聊BOSS**的脚本基于Puppeteer。
与每一位牛人站在一起
- 使命:帮你找到让你满意的工作
- 愿景:天下牛人都有满意的工作
- 使命:用科技让复杂的求职过程变简单
- 愿景:天下没有难找的工作,天下牛人都有满意的工作
# GUI 版本
各行各业,无论你是小白还是大佬,都能通过几步简单的配置,快速开始求职!
## TO 有求职之外其他用途的朋友
本程序的目标,是帮助求职者调研求职市场行情或寻找工作。如果有其它用途,请勿下载使用。
## 程序有哪些功能?运行逻辑是什么?怎样使用?
### 自动开聊
扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的BOSS。可以帮你开聊推荐列表中的职位、根据设置的求职期望推荐的职位以及根据你设置的关键词通过搜索找到的职位可以通过拖拽的方式自定义搜索顺序哦~
<img width="1309" alt="image" src="https://github.com/user-attachments/assets/fda33e53-347b-453d-a791-5a1dcd506605" />
#### 匹配步骤
1. 选择职位来源、职位筛选条件
<img width="1680" height="1050" alt="image" src="https://github.com/user-attachments/assets/cbdea071-ec28-49b0-af12-1018aa9579a9" />
2. 按照公司名称在列表中查找职位,查找到目标职位后,自动点击这个职位,右侧将会展示职位详情
3. 对工作地、薪资、工作经验、职位名称、职位类型、职位描述、BOSS活跃度进行匹配
- 如果可以匹配,则自动点击开聊按钮
- 不匹配则根据你设置的标记不合适的策略将职位标记为不合适
- 首先查看是否命中策略为“在BOSS直聘上标记不合适”的配置如果命中则在BOSS直聘上点击相关UI标记不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位对于“推荐列表中的职位”、“通过搜索找到的职位”此项会回落到“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”
- 然后查看是否命中策略为“在本地数据库中标记不合适且7天内再遇到这个职位时直接跳过”的配置如果命中则仅在本地数据库中记录且本次运行将无视这个职位
- 最后查看是否命中策略为了“仅在本次运行中记录不合适,且本次运行再遇到这个职位时直接跳过”的配置,如果命中则本次运行将无视这个职位,但下次运行可能还会查看这个职位
#### 异常情况
- 当前页面筛选条件下,如果没有更多可投递职位,则自动切换筛选条件,以获取更多新职位;如果所有筛选条件搜索完没有更多可投递职位,则切换其它职位来源继续查找更多新职位;如果所有职位来源筛选完没有更多可投递职位,则等候一段时间后,会回到第一个职位来源的第一个筛选条件,从头开始查找职位
- 如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
#### 运行效果
> BOSS直聘界面已改版如下是旧版BOSS直聘界面
https://github.com/user-attachments/assets/672f1637-dd54-416c-922e-1ecf09b0874a
### 已读不回自动复聊
BOSS不明原因已读不回简历就是投不出去
已读不回自动复聊,有事没事提醒一下已读不回的 Ta助力沟通双向奔赴
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/51a43b0f-118d-4b0a-957c-40a5367e29e4" />
#### 匹配逻辑
1. 在聊天列表中查找对你消息已读不回的BOSS再发一条消息多次复聊同时
- 如果设置了“跟进时限”,那么在这个时间之前活跃的聊天将不会被检查
- 如果设置了“跟进间隔”且再次检查时发现BOSS已读不回且距离上次提醒时间间隔小于这个时间那么聊天将暂时不会跟进直到下次检查时距离上次提醒时间间隔大于这个时间
#### 发送内容
以下二者之一:
1. “[盼回复]”表情
2. 由大语言模型(根据简历及当前聊天上下文)生成的内容
## 辅助功能介绍
### BOSS 登录助手
帮你用十分简单的方式,登录到 BOSS 直聘以进行自动开聊、已读不回提醒避免和乱七八糟的技术名词类似Cookie、登录凭证、JSON……打交道
<img width="1680" alt="image" src="https://github.com/user-attachments/assets/b5d02385-f1c0-4045-b01a-66c5f747fec5" />
### 大语言模型设置
设置本程序运行过程中可能会使用的大模型,有大语言模型加持,让求职更智能。支持配置多个备用模型,确保生成内容更随机,同时确保当某个模型不可用时也能继续让其他模型继续为你服务
<img width="576" alt="image" src="https://github.com/user-attachments/assets/ef91642f-b0b0-438f-8c66-e650f0de139b" />
### 已读不回自动复聊提示词模板编辑
对生成的提醒消息不够满意?您不妨可以自己进行编辑提示词,来获得您期望的结果
<img width="1139" alt="image" src="https://github.com/user-attachments/assets/db956573-61c0-44d4-b588-5c04fe941f84" />
### 已读不回自动复聊提醒过程模拟
想要测试配置的某一款模型可不可用?想测试使用当前配置生成的内容效果如何?那就来这里试试吧
<img width="600" alt="image" src="https://github.com/user-attachments/assets/3b2776bd-ae5b-48d1-a01c-0819509d96a1" />
### 配置模板选项
初次见面,不会配置本程序?当心配置出现问题?本程序为一些关键的功能配置(例如:自动开聊中的期望公司设置、职位详情筛选设置;大语言模型设置)加入了配置模板,让本程序更易用,助你轻松完成配置,开始求职。不妨来试试吧
<img width="576" alt="image" src="https://github.com/user-attachments/assets/2cccfece-17e7-4cc2-bf33-67f0ce500625" />
<img width="1024" alt="image" src="https://github.com/user-attachments/assets/f0c7453d-454e-494a-8c03-246107b1384f" />
## 系统要求
- 操作系统及处理器
- Windowsx86_64最低 Windows 10 1507如果你的电脑是2016年后出厂、默认安装 Window 10 的电脑,一般都可以使用)
@@ -36,16 +121,63 @@
xattr -cr /Applications/GeekGeekRun.app
```
1. 双击应用程序图标以启动程序;启动后,将引导你为初次使用进行一些配置
<!--
# CLI 版本
## 使用方式
1. 确保你已经安装 Node.js 18+、pnpm 8.15.9
1. `pnpm i` 安装依赖
1. 打开 Chrome / Edge安装 EditThisCookie 扩展程序
1. 打开 [Boss直聘](https://www.zhipin.com) 网站,在浏览器右上角找到 EditThisCookie 扩展程序图标,并点击
1. 按下 EditThisCookie 扩展程序弹出气泡中的“导出Cookie”按钮左数第三个按钮此时将会把你在 Boss直聘 网站下所有Cookie复制到剪切板上
1. 打开 `~/.geekgeekrun/storage/boss-cookies.json` 文件(如果没有相关路径请自行创建),把文件清空,并粘贴刚刚复制的 Cookie
1. 执行`pnpm start`。开聊 Boss
-->
祝求职成功~
## 关于软件不能正常启动、不能按预期运行的一些说明
本程序一直都在不断地更新迭代中包括新功能的增加以及Bug的修复。
然而作者本人只有 1台 macOS Apple Silicon设备、1台 Windows 设备且只有作者1人自测白天工作晚上熬夜开发、测试测试条件不是太好人力不足、设备不足测试时只要确保两台设备能够正常运行本程序主要流程以及新增逻辑时即认为可以准出、发版。
所以不能确保任何设备一定能够正确运行本程序;因此期望朋友们使用过程中,可以帮忙**参与测试**并通过Issue区**反馈问题**。Issue区任何内容我都会看问题也会尝试进行修复。
如果部分朋友超级喜欢写代码,或者本身工作就是写代码,可以帮忙下载 [Debugtron](https://github.com/pd4d10/debugtron/releases/tag/v1.0.0-alpha.0) 来调试本程序,查看反馈本程序的日志(如下图)。
![image](https://github.com/user-attachments/assets/4ee9955c-e1c0-4399-9d00-ce89f6657bba)
在此先谢过了~
## 为什么要写这个程序?
根据日常使用求职平台BOSS直聘的经验在已经开聊很多职位的情况下经常会推荐
1. 一些长时间不活跃的“僵尸”职位 - 活跃信息默认隐藏,需要点开职位详情才能看到
2. 牛头不对马嘴,不符合求职期望的职位
人工筛选这些职位会有很大的心智负担,真的会吐……
因此,我在自动开聊代码中,加入了清理机制,将通过标记不合适的方式,尝试让这些不活跃职位、不合适职位消失;保证只开聊符合你口味的职位。
## 使用必读及免责声明
如下是使用必读及免责声明,请您务必逐条阅读;本程序首次启动时,您还会再看到一次;若您不接受如下提到的任何内容,请不要使用本程序。
- 本程序从某种程度上说属于辅助工具,与《<a href="https://about.zhipin.com/agreement/?id=registerprotocol_30" rel="noreferer noopener" target="blank">BOSS直聘用户协议</a>》2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款`七、用户的平台使用义务`、`八、违约责任` 章节,如果一些非正常用户行为被风控监测到,您需要承受包括不仅限于**账号被强制退出登录**、**账号被限制使用**、**账号被封禁**等对您不利的风险;因此使用本程序即意味着**您愿意接受以上风险**,且如果相关风险发生,您需要自行承担相关后果,**本程序概不负责** 。
- 本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘**不会泄露给第三方**也不会进行除自动开聊BOSS以外的行为**请勿向他人泄漏您的Cookie**。
- 本程序会通过尽可能模仿用户行为来规避相关风险,但并不能保证可以完全规避。建议您使用本程序时**注意节制**,建议当天开聊次数用尽后,隔几天再使用。建议您**注册一个本程序专用的新的BOSS直聘账号**进行求职。
- 本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常**发生改版**,且有可能**包含A/B实验**,这将导致本程序相关脚本失效(典型表现为本程序运行到某一步骤后,浏览器重复“闪退、重新启动”)。如果您在使用过程中遇上程序未按照预期执行的情况,请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
- 您所在公司可能会采购上网行为监控工具或网关(例如奇安信、深信服、绿盟等厂商的产品),对您的计算机终端或网络进行**监控**,从而**审计**、**跟踪**您的行为;您的上级/IT/HR 可能会获取到监控数据,从而了解团队成员离职倾向。如果您不希望您的上级/IT/HR 了解到您正在求职,建议您**不要在您所在公司提供的计算机终端或网络上使用本程序**。
- 本程序尊重您的隐私,**不会参与任何钓鱼活动**、**不会上报能够识别出您身份的信息**、**不会向您所在公司及您的上级/IT/HR 报告您的求职行为**、**不会向任何猎头公司泄露您的个人信息**。但由于本程序开源,任何人均可更改本程序源码并重新发布,这一过程中其它开发者是可以加入恶意程序的,因此请从你信任的源下载本程序。
- 本程序**没有内置任何付费功能****下载**、**使用**是**免费**的,任何人可以**免费获得**、**免费使用**。**作者没有利用本程序赚到过任何收入**。如果您是从GitHub以外的地方付费后“购买”的本程序或您被提示“必须付费后才能使用本程序”那**您大概率被骗了**,或者**您下载到了本程序修改版**。**本程序对此概不负责,请勿找作者商讨退款、售后事宜,相关事宜请咨询卖方**。
- 本程序**不对您的求职过程与结果负责**为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责请**自行甄别为您开聊的公司**、**认真决定是否参加面试**、**慎重选择Offer**。
- 请在BOSS直聘上自行**屏蔽您不期望投递的公司**如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃请**在BOSS直聘上屏蔽当前公司及与之关联的公司**。
- 本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过VMware Workstation/Fusion、Oracle VirtualBox、Microsoft Hyper-V等虚拟化技术运行本程序。如果您在使用过程中遇上程序未按照预期执行的情况请[点击这里](https://github.com/geekgeekrun/geekgeekrun/issues/new)进行反馈。
## 想联系作者?想反馈使用体验上的问题?想找求职搭子?
<img width="360" alt="QQ Group QRCode" src="https://github.com/user-attachments/assets/67560948-467b-44f5-a3e3-c8713255d8b7" />
欢迎进群聊天(刚建的群,人有点少,大胆聊就好)
--------
以上
更多功能还在开发中~
祝你求职成功,事业顺利,事事顺心
## Star
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=geekgeekrun/geekgeekrun&type=Date" />
</picture>
感谢支持

View File

@@ -11,11 +11,14 @@
"author": "geekgeekrun",
"license": "ISC",
"dependencies": {
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
"cheerio": "1.0.0-rc.12",
"dayjs": "^1.11.10",
"json5": "^2.2.3",
"minimist": "^1.2.8",
"puppeteer": "20.1.0",
"puppeteer": "24.19.0",
"puppeteer-extra": "3.3.6",
"puppeteer-extra-plugin-anonymize-ua": "2.4.6",
"puppeteer-extra-plugin-stealth": "2.11.2",
"rimraf": "^3.0.2",
"tapable": "^2.2.1"
@@ -27,6 +30,12 @@
"node": "20.16.0"
},
"engines": {
"pnpm": "=8.15.9"
"pnpm": ">=8.15.9 <9.0.0"
},
"pnpm": {
"patchedDependencies": {
"find-chrome-bin@2.0.4": "patches/find-chrome-bin@2.0.4.patch",
"puppeteer-extra-plugin-anonymize-ua@2.4.6": "patches/puppeteer-extra-plugin-anonymize-ua@2.4.6.patch"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,9 @@ function combine(arr, min, max) {
// 生成符合"0"限制的组合
function combineWithZero(arr, min, max) {
let combineResult;
if (!Array.isArray(arr)) {
arr = []
}
if (arr.includes(0)) {
// 如果包含 00不参与组合
combineResult = [].concat(
@@ -56,10 +59,11 @@ function combineWithZero(arr, min, max) {
}
export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
const { salaryList, experienceList, degreeList, scaleList, industryList } =
const { cityList, salaryList, experienceList, degreeList, scaleList, industryList } =
selectedFilters;
// 生成符合限制条件的组合
const cityComb = combineWithZero(cityList, 0, 1) // Salary: 0-1 个
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
@@ -67,17 +71,20 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
// 通过迭代生成所有组合
for (const salary of salaryComb) {
for (const experience of experienceComb) {
for (const degree of degreeComb) {
for (const scale of scaleComb) {
for (const industry of industryComb) {
yield {
salaryList: salary,
experienceList: experience,
degreeList: degree,
scaleList: scale,
industryList: industry
for (const city of cityComb) {
for (const salary of salaryComb) {
for (const experience of experienceComb) {
for (const degree of degreeComb) {
for (const scale of scaleComb) {
for (const industry of industryComb) {
yield {
cityList: city,
salaryList: salary,
experienceList: experience,
degreeList: degree,
scaleList: scale,
industryList: industry
}
}
}
}
@@ -90,8 +97,9 @@ export function* combineFiltersWithConstraintsGenerator(selectedFilters) {
//#region get count of combinations
// 计算符合限制条件的组合数量
export function calculateTotalCombinations(selectedFilters) {
export function calculateTotalCombinations(selectedFilters, includeEmptyCondition) {
const {
cityList = [],
salaryList = [],
experienceList = [],
degreeList = [],
@@ -100,14 +108,70 @@ export function calculateTotalCombinations(selectedFilters) {
} = selectedFilters
// 生成符合限制条件的组合
const cityComb = combineWithZero(cityList, 0, 1) // City: 0-1 个
const salaryComb = combineWithZero(salaryList, 0, 1) // Salary: 0-1 个
const experienceComb = combineWithZero(experienceList, 0, experienceList.length) // Experience: 0 个或更多
const degreeComb = combineWithZero(degreeList, 0, degreeList.length) // Degree: 0 个或更多
const scaleComb = combineWithZero(scaleList, 0, scaleList.length) // Scale: 0 个或更多
const industryComb = combineWithZero(industryList, 0, 3) // Industry: 0-3 个
return [salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
let result = [cityComb, salaryComb, experienceComb, degreeComb, scaleComb, industryComb].reduce((accu, cur) => {
return accu * cur.length
}, 1)
if (!includeEmptyCondition) {
result -= 1
}
return result
}
//#endregion
export function checkAnyCombineBossRecommendFilterHasCondition(value) {
if (!Object.keys(value ?? {}).length) {
return false
}
return Object.keys(value).some((k) => {
return !!value[k]?.length
})
}
export function getStaticCombineFilterKey(condition) {
const kAsO = {}
for (const key of Object.keys(condition ?? []).sort()) {
if (condition[key] === null || condition[key] === undefined) {
continue
}
kAsO[key] = condition[key]
}
return JSON.stringify(kAsO)
}
export function formatStaticCombineFilters(rawStaticCombineRecommendJobFilterConditions) {
rawStaticCombineRecommendJobFilterConditions = JSON.parse(JSON.stringify(rawStaticCombineRecommendJobFilterConditions))
const map = new Map()
for (const condition of rawStaticCombineRecommendJobFilterConditions ?? []) {
const key = getStaticCombineFilterKey(condition)
map.set(key, condition)
}
const conditions = Array.from(map.values())
const result = conditions.map((condition) => {
return {
cityList: condition.city ? [condition.city] : [],
salaryList: condition.salary ? [condition.salary] : [],
experienceList: condition.experience ? [condition.experience] : [],
degreeList: condition.degree ? [condition.degree] : [],
scaleList: condition.scale ? [condition.scale] : [],
industryList: condition.industry ? [condition.industry] : []
}
})
if (!result.length) {
result.push({
cityList: [],
salaryList: [],
experienceList: [],
degreeList: [],
scaleList: [],
industryList: []
})
}
return result
}

View File

@@ -0,0 +1,7 @@
export const activeDescList = [
'', "半年前活跃","近半年活跃","5月内活跃","4月内活跃","3月内活跃","2月内活跃","本月活跃","2周内活跃","本周活跃","3日内活跃","昨日活跃","今日活跃","刚刚活跃"
]
export const RECOMMEND_JOB_ENTRY_SELECTOR = `.c-expect-select a[ka="jobs_recommend_tab_click"]`
export const USER_SET_EXPECT_JOB_ENTRIES_SELECTOR = `.c-expect-select .expect-list .expect-item`
export const SEARCH_BOX_SELECTOR = `.c-search-input .search-input-box`

View File

@@ -1,14 +1,44 @@
{
"combineRecommendJobFilterType": 1,
"anyCombineRecommendJobFilter": {
"cityList": [],
"salaryList": [],
"experienceList": [],
"degreeList": [],
"scaleList": [],
"industryList": []
},
"staticCombineRecommendJobFilterConditions": [],
"isSkipEmptyConditionForCombineRecommendJobFilter": false,
"expectJobRegExpStr": "",
"expectJobNameRegExpStr": "",
"expectJobTypeRegExpStr": "",
"expectJobDescRegExpStr": "",
"jobNotMatchStrategy": 1,
"jobNotActiveStrategy": 1,
"markAsNotActiveSelectedTimeRange": 7,
"expectCityList": [],
"expectCityNotMatchStrategy": 3,
"strategyScopeOptionWhenMarkJobCityNotMatch": 2,
"jobDetailRegExpMatchLogic": 1,
"autoReminder": {
"throttleIntervalMinutes": 10,
"rechatLimitDay": 21
}
"rechatLimitDay": 21,
"geminiApiKey": "",
"rechatContentSource": 1,
"recentMessageQuantityForLlm": 8,
"rechatLlmFallback": 1,
"onlyRemindBossWithExpectJobType": true
},
"jobSourceList": [
{
"type": "expect",
"enabled": true
}
],
"isSageTimeEnabled": true,
"sageTimeOpTimes": 100,
"sageTimePauseMinute": 15,
"blockCompanyNameRegExpStr": "",
"fieldsForUseCommonConfig": {}
}

View File

@@ -0,0 +1,12 @@
{
"expectCityList": [],
"expectJobNameRegExpStr": "",
"expectJobTypeRegExpStr": "",
"expectJobDescRegExpStr": "",
"expectCompanies": [],
"blockCompanyNameRegExpStr": "",
"jobDetailRegExpMatchLogic": 1,
"expectSalaryCalculateWay": 1,
"expectSalaryLow": null,
"expectSalaryHigh": null
}

View File

@@ -0,0 +1,8 @@
[{
"providerCompleteApiUrl": "",
"providerApiSecret": "",
"model": "",
"enabled": true,
"serveWeight": 100,
"_extra": {}
}]

View File

@@ -0,0 +1,4 @@
[
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技","华为","德科","FESCO","科锐","科之锐",
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","巨量引擎","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","小鹅通","富途","京东","沃东天骏","达达","达冠","百度","昆仑芯","小度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","智图","长地万方","瑞图万方","道道通","小熊博望","理想","蔚来","顺丰","丰巢","中通","圆通","申通","跨越","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","淘宝","淘麦郎","天猫","盒马","口碑","优视","夸克","UC","蚂蚁","高德","LAZADA","来赞达","飞猪","菜鸟","哈啰","钉钉","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","51talk","爱学习","高思","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","从鲸","TEMU","得物","有赞","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","虾皮","首汽租车","GoFun","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技","迅雷","TP","锐捷","Tenda","腾达","斐讯","希音","SHEIN","稀宇","深言","百川智能","与爱为舞","牵手","Grab","爱回收","洋钱罐","瓴岳","得到","思维造物","地平线","咪咕","翼支付","电信","天翼","联通","蓝湖","墨刀","海尔","美的","米哈游","传音","同花顺","国美","TCL"
]

View File

@@ -1,4 +1 @@
[
"青钱","软通动力","南天","睿服","中电金信","佰钧成","云链","博彦","汉克时代","柯莱特","拓保","亿达信息","纬创","微创","微澜","诚迈科技","法本","兆尹","诚迈","联合永道","新致软件","宇信科技",
"抖音","字节","字跳","有竹居","脸萌","头条","懂车帝","滴滴","嘀嘀","小桔","网易","有道","腾讯","酷狗","酷我","阅文","搜狗","京东","沃东天骏","达达","百度","度小满","爱奇艺","携程","趣拿","去哪儿","集度","理想","蔚来","顺丰","讯飞","同程","艺龙","马蜂窝","贝壳","自如","链家","我爱我家","相寓","多点","金山","小米","猎豹","新浪","微博","阿里","蚂蚁","飞猪","高德","乌鸫","饿了么","美团","三快","猫眼","快手","映客","小红书","行吟","奇虎","360","三六零","鸿盈","奇富","奇元","亚信","启明星辰","奇安信","深信服","长亭","绿盟","天融信","商汤","SenseTime","大华","海康威视","hikvision","汽车之家","车好多","瓜子","易车","昆仑万维","昆仑天工","闲徕","趣加","FunPlus","完美","马上消费","轻松","水滴","白龙马","58","车欢欢","五八","红布林","致美","快狗","天鹅到家","转转","美餐","知乎","智者四海","易点云","搜狐","用友","畅捷通","猿辅导","小猿","猿力","好未来","学而思","希望学","新东方","东方甄选","东方优选","作业帮","高途","跟谁学","学科网","天学网","一起教育","一起作业","美术宝","火花思维","粉笔","老虎国际","一心向上","向上一意","联想","拉勾","乐视","欢聚","竞技世界","拼多多","寻梦","得物","Moka","希瑞亚斯","北森","OPPO","欧珀","vivo","维沃","小天才","步步高","读书郎","货拉拉","陌陌","探探","Shopee","首汽租车","神州租车","天眼查","旷视","小冰","美图","智谱华章","MiniMax","石头科技"
]
[]

File diff suppressed because it is too large Load Diff

View File

@@ -6,19 +6,119 @@ 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']
import defaultCommonJobConditionConfig from './default-config-file/common-job-condition-config.json' assert { type: 'json' }
export const configFileNameList = ['boss.json', 'dingtalk.json', 'target-company-list.json', 'llm.json', 'common-job-condition-config.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),
'common-job-condition-config.json': JSON.stringify(defaultCommonJobConditionConfig)
}
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
export const configFolderPath = path.join(
runtimeFolderPath,
'config'
)
export const writeConfigFile = async (fileName, content, { isSync } = {}) => {
const filePath = path.join(configFolderPath, fileName)
const fileContent = JSON.stringify(content)
if (isSync) {
fs.writeFileSync(
filePath,
fileContent
)
}
else {
return fsPromise.writeFile(
filePath,
fileContent
)
}
}
if (
!fs.existsSync(
path.join(configFolderPath, 'common-job-condition-config.json')
)
) {
let bossConfig = null
if (
fs.existsSync(
path.join(configFolderPath, 'boss.json')
)
) {
fs.existsSync(
path.join(configFolderPath, 'boss.json')
)
try {
bossConfig = JSON.parse(
fs.readFileSync(
path.join(configFolderPath, 'boss.json')
)
)
}
catch {}
}
if (bossConfig) {
Object.keys(defaultCommonJobConditionConfig).forEach(
key => {
if (Object.hasOwn(bossConfig, key)) {
defaultCommonJobConditionConfig[key] = bossConfig[key]
}
}
)
let {
expectJobRegExpStr,
expectJobNameRegExpStr,
expectJobTypeRegExpStr,
expectJobDescRegExpStr,
} = bossConfig
if (
expectJobRegExpStr &&
!expectJobNameRegExpStr &&
!expectJobTypeRegExpStr &&
!expectJobDescRegExpStr
) {
expectJobNameRegExpStr = expectJobRegExpStr
expectJobTypeRegExpStr = expectJobRegExpStr
expectJobDescRegExpStr = expectJobRegExpStr
}
Object.assign(defaultCommonJobConditionConfig, {
expectJobNameRegExpStr,
expectJobTypeRegExpStr,
expectJobDescRegExpStr
})
}
let targetCompanyList = null
if (
fs.existsSync(
path.join(configFolderPath, 'target-company-list.json')
)
) {
targetCompanyList = JSON.parse(
fs.readFileSync(
path.join(configFolderPath, 'target-company-list.json')
)
)
}
if (targetCompanyList) {
defaultCommonJobConditionConfig.expectCompanies = targetCompanyList ?? []
}
writeConfigFile('common-job-condition-config.json', defaultCommonJobConditionConfig, { isSync: true })
if (bossConfig) {
if (!bossConfig.fieldsForUseCommonConfig) {
bossConfig.fieldsForUseCommonConfig = {}
}
writeConfigFile('boss.json', bossConfig, { isSync: true })
}
}
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
const ensureRuntimeFolderPathExist = () => {
if (!fs.existsSync(runtimeFolderPath)) {
fs.mkdirSync(runtimeFolderPath)
@@ -33,11 +133,6 @@ const ensureRuntimeFolderPathExist = () => {
}
})
}
export const configFolderPath = path.join(
runtimeFolderPath,
'config'
)
export const ensureConfigFileExist = () => {
ensureRuntimeFolderPathExist()
;configFileNameList.forEach(
@@ -69,22 +164,17 @@ export const readConfigFile = (fileName) => {
)
} catch {
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
ensureConfigFileExist()
o = JSON.parse(defaultConfigFileContentMap[fileName])
if (defaultConfigFileContentMap[fileName]) {
ensureConfigFileExist()
o = JSON.parse(defaultConfigFileContentMap[fileName])
} else {
o = null
}
}
return o
}
export const writeConfigFile = async (fileName, content) => {
const filePath = path.join(configFolderPath, fileName)
const fileContent = JSON.stringify(content)
return fsPromise.writeFile(
filePath,
fileContent
)
}
export const storageFilePath = path.join(
runtimeFolderPath,
'storage'
@@ -112,7 +202,8 @@ export const ensureStorageFileExist = () => {
)
}
export const readStorageFile = (fileName) => {
export const readStorageFile = (fileName, { isJson } = {}) => {
isJson = isJson ?? true
const joinedPath = path.join(storageFilePath, fileName)
if (!fs.existsSync(
@@ -123,21 +214,36 @@ export const readStorageFile = (fileName) => {
let o
try {
o = JSON.parse(
fs.readFileSync(joinedPath)
)
const content = fs.readFileSync(joinedPath)
if (isJson) {
o = JSON.parse(content)
}
else {
o = content.toString()
}
} catch {
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
ensureStorageFileExist()
o = JSON.parse(defaultStorageFileContentMap[fileName])
if (isJson) {
o = JSON.parse(defaultStorageFileContentMap[fileName] ?? 'null')
}
else {
o = defaultStorageFileContentMap[fileName] ?? null
}
}
return o
}
export const writeStorageFile = async (fileName, content) => {
export const writeStorageFile = async (fileName, content, { isJson } = {}) => {
isJson = isJson ?? true
const filePath = path.join(storageFilePath, fileName)
const fileContent = JSON.stringify(content)
let fileContent
if (isJson) {
fileContent = JSON.stringify(content)
} else {
fileContent = content
}
return fsPromise.writeFile(
filePath,
fileContent

View File

@@ -0,0 +1,54 @@
import { readConfigFile } from './runtime-file-utils.mjs'
import { sleep } from "@geekgeekrun/utils/sleep.mjs"
let {
isSageTimeEnabled,
sageTimeOpTimes,
sageTimePauseMinute,
} = readConfigFile('boss.json')
isSageTimeEnabled = isSageTimeEnabled ?? true
sageTimeOpTimes =
isNaN(parseInt(sageTimeOpTimes)) ||
parseInt(sageTimeOpTimes) < 1
? 100
: parseInt(sageTimeOpTimes)
sageTimePauseMinute =
isNaN(parseFloat(sageTimePauseMinute)) ||
parseFloat(sageTimePauseMinute) < 0
? 15
: parseFloat(sageTimePauseMinute)
if (parseFloat(sageTimePauseMinute) === 0) {
isSageTimeEnabled = false
}
let totalEnabledTimes = 0
let recordedOpCount = 0
export const waitForSageTimeOrJustContinue = async ({
tag,
hooks,
} = {}) => {
if (!isSageTimeEnabled) {
return
}
const tagText = tag ? ` ${tag}` : ''
if (recordedOpCount > sageTimeOpTimes) {
totalEnabledTimes++
console.log(`[SageTime${tagText}] 请求已达限制,开启;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次开启`)
await hooks?.sageTimeEnter?.promise({
tag,
totalEnabledTimes,
recordedOpCount,
})
await sleep(sageTimePauseMinute * 60 * 1000)
console.log(`[SageTime${tagText}] 请求限制已解除,关闭;当前记录次数 ${recordedOpCount};第 ${totalEnabledTimes} 次关闭`)
await hooks?.sageTimeExit?.promise({
tag,
totalEnabledTimes,
recordedOpCount,
})
recordedOpCount = 0
}
else {
console.log(`[SageTime${tagText}] 请求未达限制;当前记录次数 ${recordedOpCount};已开启过 ${totalEnabledTimes}`)
recordedOpCount++
}
}

148
packages/laodeng/index.js Normal file
View File

@@ -0,0 +1,148 @@
"use strict";
const { PuppeteerExtraPlugin } = require("puppeteer-extra-plugin");
async function handle(p) {
await p.evaluateOnNewDocument(() => {
(() => {
"use strict";
/* -------------------------------------------------------
* 1. 保存原生 Function.prototype.toString
* ----------------------------------------------------- */
const nativeFunctionToString = Function.prototype.toString;
/* -------------------------------------------------------
* 2. WeakMap函数 → 伪原生源码
* ----------------------------------------------------- */
const nativeSourceMap = new WeakMap();
/* -------------------------------------------------------
* 3. 注册伪原生源码
* ----------------------------------------------------- */
const registerNativeSource = (fn, source) => {
try {
nativeSourceMap.set(fn, source);
} catch (_) {}
};
/* -------------------------------------------------------
* 4. 劫持 Function.prototype.toString
* ----------------------------------------------------- */
Object.defineProperty(Function.prototype, "toString", {
configurable: true,
writable: true,
value: function toString() {
if (nativeSourceMap.has(this)) {
return nativeSourceMap.get(this);
}
return nativeFunctionToString.call(this);
},
});
/* -------------------------------------------------------
* 5. 伪装 Function.prototype.toString 自身
* ----------------------------------------------------- */
registerNativeSource(
Function.prototype.toString,
nativeFunctionToString.toString(),
);
/* -------------------------------------------------------
* 6. stealthify包装函数但保持“原生外观”
* ----------------------------------------------------- */
const stealthify = (obj, prop, handler) => {
const original = obj[prop];
if (typeof original !== "function") return;
const wrapped = function (...args) {
return handler.call(this, original, args);
};
const namePropertyDescriptor = Object.getOwnPropertyDescriptor(
wrapped,
"name",
);
// 处理函数 name 属性
Object.defineProperty(wrapped, "name", {
...namePropertyDescriptor,
value: prop,
});
// 保留 prototype某些函数有
try {
Object.setPrototypeOf(wrapped, Object.getPrototypeOf(original));
} catch (_) {}
// 注册伪原生源码(直接复用原函数的 native 表现)
registerNativeSource(wrapped, nativeFunctionToString.call(original));
// 用 defineProperty 保持 descriptor 接近原生
const desc = Object.getOwnPropertyDescriptor(obj, prop);
Object.defineProperty(obj, prop, {
...desc,
value: wrapped,
});
};
/* -------------------------------------------------------
* 7. 示例stealth console.log / debug / info
* ----------------------------------------------------- */
const filterConsoleArgs = (args) =>
args.map((arg) => {
if (arg && typeof arg === "object") {
// 防止 getter / Proxy / 大对象触发
return {};
}
return arg;
});
[
"log",
"debug",
"info",
"warn",
"error",
"dir",
"table",
"debug",
].forEach((name) => {
stealthify(console, name, (original, args) => {
// ❗不传递原始对象,避免 DevTools / CDP 展开
return original.apply(console, filterConsoleArgs(args));
});
});
/* -------------------------------------------------------
* 8. 防御性补丁(可选但强烈建议)
* ----------------------------------------------------- */
// 防止检测 toString 被替换
registerNativeSource(
registerNativeSource,
"function registerNativeSource() { [native code] }",
);
})();
});
}
class Plugin extends PuppeteerExtraPlugin {
constructor() {
super();
}
get name() {
return "laodeng";
}
async onBrowser(browser) {
const pages = await browser.pages();
for (const p of pages) {
await handle(p);
}
}
async onPageCreated(p) {
await handle(p);
}
}
module.exports = function (pluginConfig) {
return new Plugin(pluginConfig);
};

View File

@@ -0,0 +1,27 @@
{
"name": "@geekgeekrun/puppeteer-extra-plugin-laodeng",
"version": "0.0.1",
"description": "laodeng",
"main": "./index.js",
"author": "geekgeekrun",
"productName": "GeekGeekRun",
"homepage": "https://github.com/geekgeekrun",
"scripts": {},
"dependencies": {
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin": "^3.2.3"
},
"peerDependencies": {
"playwright-extra": "*",
"puppeteer": "^24.19.0",
"puppeteer-extra": "*"
},
"peerDependenciesMeta": {
"puppeteer-extra": {
"optional": true
},
"playwright-extra": {
"optional": true
}
}
}

View File

@@ -6,74 +6,32 @@ import {
sleep,
sleepWithRandomDelay
} from '@geekgeekrun/utils/sleep.mjs'
import extractZip from 'extract-zip'
import { blockNavigation } from '@geekgeekrun/utils/puppeteer/block-navigation.mjs'
import {
writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path';
import JSON5 from 'json5'
import url from 'url';
import packageJson from './package.json' assert {type: 'json'}
import {
runtimeFolderPath,
ensureEditThisCookie,
editThisCookieExtensionPath,
} from './utils.mjs'
import { EventEmitter } from 'node:events'
export const loginEventBus = new EventEmitter()
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
const isUiDev = process.env.NODE_ENV === 'development'
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
const extensionDir = path.join(
runtimeFolderPath,
'chrome-extensions'
)
if (!fs.existsSync(
runtimeFolderPath
)) {
fs.mkdirSync(runtimeFolderPath)
}
if (!fs.existsSync(extensionDir)) {
fs.mkdirSync(extensionDir)
}
const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
let editThisCookieZipPath
async function getEditThisCookieZipPath () {
if (editThisCookieZipPath) {
return editThisCookieZipPath
}
if (isRunFromUi) {
const { app } = await import('electron')
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
} else {
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
}
return editThisCookieZipPath
}
export async function main() {
if (!fs.existsSync(
path.join(editThisCookieExtensionPath, 'manifest.json')
)) {
await extractZip(
await getEditThisCookieZipPath(),
{
dir: extensionDir
}
)
}
await ensureEditThisCookie()
const { puppeteer } = await initPuppeteer()
const browser = await puppeteer.launch({
headless: false,
args: [
`--load-extension=${editThisCookieExtensionPath}`
]
pipe: true,
enableExtensions: [editThisCookieExtensionPath]
})
const closeAttachedSet = new WeakSet()
@@ -96,10 +54,8 @@ export async function main() {
page.once('close', async () => {
browser.close()
if (isRunFromUi) {
const electron = await import('electron')
electron.app.quit()
}
const electron = await import('electron')
electron.app.quit()
})
const { dispose: disposeNavigationLock } = await blockNavigation(page, (req) => !req.url().startsWith('https://www.zhipin.com'))

View File

@@ -0,0 +1,82 @@
import path from 'node:path';
import fs from 'node:fs'
import os from 'node:os'
import extractZip from 'extract-zip'
import packageJson from './package.json' assert {type: 'json'}
const isUiDev = process.env.NODE_ENV === 'development'
export const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
const extensionDir = path.join(
runtimeFolderPath,
'chrome-extensions'
)
if (!fs.existsSync(
runtimeFolderPath
)) {
fs.mkdirSync(runtimeFolderPath)
}
if (!fs.existsSync(extensionDir)) {
fs.mkdirSync(extensionDir)
}
export const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
let editThisCookieZipPath
async function getEditThisCookieZipPath () {
if (editThisCookieZipPath) {
return editThisCookieZipPath
}
const { app } = await import('electron')
editThisCookieZipPath = path.join(app.getAppPath(), './node_modules', packageJson.name, 'extensions', 'EditThisCookie.zip')
return editThisCookieZipPath
}
const APP_GEEKGEEKRUN_EDIT_VERSION = 1
export async function ensureEditThisCookie () {
let isNeedExtractEditThisCookie = false
const GEEKGEEKRUN_EDIT_VERSION_FILE_PATH = path.join(editThisCookieExtensionPath, 'GEEKGEEKRUN_EDIT_VERSION')
let geekgeekrunEditVersion
try {
const fileContent = fs.readFileSync(GEEKGEEKRUN_EDIT_VERSION_FILE_PATH, { encoding: 'utf-8' })
geekgeekrunEditVersion = Number(fileContent) || 0
}
catch (err) {
geekgeekrunEditVersion = 0
}
if (geekgeekrunEditVersion < APP_GEEKGEEKRUN_EDIT_VERSION) {
isNeedExtractEditThisCookie = true
}
const isExtractDoneFlagFilePath = path.join(editThisCookieExtensionPath, 'EXTRACT_DONE')
if (
!isNeedExtractEditThisCookie &&
!fs.existsSync(isExtractDoneFlagFilePath)
) {
isNeedExtractEditThisCookie = true
}
if (isNeedExtractEditThisCookie) {
if (
fs.existsSync(
editThisCookieExtensionPath
)
) {
fs.rmSync(
editThisCookieExtensionPath,
{
recursive: true,
force: true
}
)
}
await extractZip(
await getEditThisCookieZipPath(),
{
dir: extensionDir
}
)
await fs.promises.writeFile(
isExtractDoneFlagFilePath,
''
)
}
}

486
packages/pm/daemon.js Normal file
View File

@@ -0,0 +1,486 @@
// 如果是通过 Electron 运行,禁用 GUI 和 Dock 图标
if (typeof require !== 'undefined') {
try {
const { app } = require('electron');
if (app) {
// 隐藏 Dock 图标macOS
if (process.platform === 'darwin') {
app.dock?.hide();
}
// 防止显示窗口
app.on('ready', () => {
// 不创建任何窗口,保持后台运行
});
// 防止在没有窗口时退出
app.on('window-all-closed', (e) => {
e.preventDefault();
// 不退出应用,保持后台运行
});
}
} catch (e) {
// 不在 Electron 环境中,忽略
}
}
const net = require('net');
const { spawn } = require('child_process');
const path = require('path');
const split2 = require('split2');
const fs = require('fs')
const { tmpdir } = require('os')
const { randomUUID } = require('crypto')
const ipcWritePipe = fs.createWriteStream(null, { fd: 3 })
let ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
if (!ipcSocketName) {
process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}`
ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
}
const ipcSocketPath = process.platform === 'win32'
? `\\\\.\\pipe\\${ipcSocketName}`
: path.join(tmpdir(), `${ipcSocketName}.sock`)
const workers = new Map(); // workerId -> { process, status, restartCount, socket, latestScreenshot, latestScreenshotAt }
const userProcessClients = new Set(); // GUI客户端连接集合
const stoppedWorkers = new Set(); // 被用户主动停止的workerId集合用于防止竞态条件
const pidToProcessInfoMap = new Map()
const socketToWorkerIdSetMap = new WeakMap()
// 创建TCP服务器
const server = net.createServer((socket) => {
console.log('客户端已连接');
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON
// split2 会自动处理 TCP 分包问题,确保每条完整的消息(以换行符结尾)才会触发
const splitStream = split2();
socket.pipe(splitStream).on('data', (line) => {
const trimmedLine = line.toString().trim();
if (!trimmedLine) {
return; // 跳过空行
}
let _callbackUuid
try {
const message = JSON.parse(trimmedLine);
_callbackUuid = message._callbackUuid
handleMessage(socket, message);
} catch (parseError) {
console.error('解析JSON消息失败:', parseError.message);
console.error('原始数据:', trimmedLine.substring(0, 100)); // 只打印前100个字符
sendResponse(socket, _callbackUuid, { error: '无效的JSON格式', details: parseError.message });
}
});
splitStream.on('error', (err) => {
console.error('split2 流处理错误:', err);
sendResponse(socket, null, { error: '流处理失败' });
});
socket.on('error', (err) => {
console.error('Socket错误:', err);
});
socket.on('close', () => {
console.log('客户端已断开连接');
// 清理GUI客户端连接
userProcessClients.delete(socket);
// 清理工具进程连接
for (const [workerId, workerInfo] of workers.entries()) {
if (workerInfo.socket === socket) {
console.log(`工具进程 ${workerId} 的连接已断开`);
workerInfo.socket = null;
}
}
const workerIdSet = socketToWorkerIdSetMap.get(socket) || new Set()
;[...workerIdSet].forEach(workerId => {
stopWorker(workerId);
})
});
});
// 处理消息
function handleMessage(socket, message) {
console.log('收到消息:', message);
const _callbackUuid = message._callbackUuid
if (message.type === 'ping') {
sendResponse(socket, _callbackUuid, {
success: true,
message: 'pong'
});
return
}
if (message.type === 'user-process-register') {
// GUI客户端的控制消息
// 标记为GUI客户端
if (!userProcessClients.has(socket)) {
userProcessClients.add(socket);
}
sendResponse(socket, _callbackUuid, {
success: true,
});
return;
}
const workerId = message.workerId;
const workerInfo = workers.get(workerId);
switch (message.type) {
// 从工具进程发送的消息(数据、心跳、截图等)
case 'check-should-exit': {
const workerId = message.workerId;
const shouldExit = stoppedWorkers.has(workerId) || !workers.has(workerId);
sendResponse(socket, _callbackUuid, {
workerId: workerId,
shouldExit: shouldExit
});
if (shouldExit) {
console.log(`工具进程 ${workerId} 查询是否应该退出,返回: 是`);
}
return
}
case 'worker-to-gui-message': {
// 转发工具进程消息到GUI客户端
broadcastToGUI({
type: 'worker-to-gui-message',
workerId: workerId,
data: message.data || message,
timestamp: Date.now()
});
return
}
case 'worker-screenshot': {
if (workerInfo && message.data && message.data.screenshot /* && workerInfo.socket === socket */) {
// 如果携带截图信息,则在守护进程内缓存一份,供 get-status 使用
try {
workerInfo.latestScreenshot = message.data.screenshot;
workerInfo.latestScreenshotAt = Date.now();
} catch (e) {
console.error('缓存 worker 截图信息失败:', e);
}
}
return
}
// 从GUI进程发送的消息数据、心跳、截图等
case 'start-worker': {
const {
workerId,
command,
args,
env
} = message
if (workers.has(workerId)) {
console.log(`工具进程 ${workerId} 已在运行`);
return;
}
startWorker({
workerId,
command,
args,
env
});
sendResponse(socket, _callbackUuid, {
success: true,
message: `工具进程 ${message.workerId} 已启动`,
workerId: message.workerId
});
let socketToWorkerIdSet = socketToWorkerIdSetMap.get(socket)
if (
!(socketToWorkerIdSet instanceof Set)
) {
socketToWorkerIdSet = new Set()
socketToWorkerIdSetMap.set(
socket,
socketToWorkerIdSet
)
}
socketToWorkerIdSet.add(workerId)
return
}
case 'stop-worker': {
stopWorker(message.workerId);
sendResponse(socket, _callbackUuid, {
success: true,
message: `工具进程 ${message.workerId} 已停止`,
workerId: message.workerId
});
return
}
case 'get-status': {
const status = getWorkersStatus();
sendResponse(socket, _callbackUuid, {
success: true,
workers: status
});
return
}
default: {
sendResponse(socket, _callbackUuid, { error: '未知的消息类型' });
}
}
}
// 启动工具进程
function startWorker({ workerId, command, args, env }, restartCount = 0) {
const noAutoRestartExitCodeSet = new Set([0]);
(env.GEEKGEEKRUND_NO_AUTO_RESTART_EXIT_CODE ?? '')
.split(',')
.map(n => parseInt(n))
.forEach(n => noAutoRestartExitCodeSet.add(n))
console.log(`启动工具进程: ${workerId}${restartCount > 0 ? ` (重启第${restartCount}次)` : ''}`);
// 添加参数使工具进程在后台运行,不显示 UI
const workerProcess = spawn(command, args, {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
...env,
GEEKGEEKRUND_WORKER_ID: workerId,
GEEKGEEKRUND_RESTART_COUNT: restartCount.toString(),
}
});
let output = '';
workerProcess.stdout.on('data', (data) => {
output += data.toString();
console.log(`工具进程 ${workerId} 输出:`, data.toString().trim());
});
workerProcess.stderr.on('data', (data) => {
console.error(`工具进程 ${workerId} 错误:`, data.toString().trim());
});
workerProcess.on('exit', (code, signal) => {
console.log(`工具进程 ${workerId} 退出,代码: ${code}, 信号: ${signal}`);
const workerInfo = pidToProcessInfoMap.get(workerProcess.pid)
if (workerInfo) {
pidToProcessInfoMap.delete(workerProcess.pid)
// 关闭工具进程的TCP连接
if (workerInfo.socket) {
workerInfo.socket.destroy();
}
const shouldRestart = !noAutoRestartExitCodeSet.has(code) // && code !== null;
// 使用当前的 restartCount 加1而不是从 workerInfo 中取(因为可能已经被删除)
const restartCount = (workerInfo.restartCount || 0) + 1;
workers.delete(workerId);
// 通知GUI客户端工具进程已退出
broadcastToGUI({
type: 'worker-exited',
workerId: workerId,
code: code,
signal: signal,
restarting: shouldRestart && !stoppedWorkers.has(workerId),
restartCount: restartCount
});
// 如果进程意外退出(非正常停止),且不在停止列表中,自动重启
if (shouldRestart && !stoppedWorkers.has(workerId)) {
console.log(`工具进程 ${workerId} 意外退出,准备重启 (第${restartCount}次)`);
// 延迟重启,避免频繁重启
setTimeout(() => {
// 再次检查确保worker不在停止列表中且当前没有运行
if (!workers.has(workerId) && !stoppedWorkers.has(workerId)) {
startWorker({ workerId, command, args, env }, restartCount);
} else if (stoppedWorkers.has(workerId)) {
console.log(`工具进程 ${workerId} 在重启前被标记为停止,取消重启`);
// 从停止列表中移除,因为已经处理完毕
stoppedWorkers.delete(workerId);
broadcastToGUI({
type: 'worker-exited',
workerId: workerId,
code: code,
signal: signal,
restarting: false,
restartCount: restartCount
});
}
}, 2000);
} else if (stoppedWorkers.has(workerId)) {
// 如果是在停止列表中,清理标记
console.log(`工具进程 ${workerId} 已停止,清理停止标记`);
stoppedWorkers.delete(workerId);
}
} else {
// 如果workerInfo不存在可能是已经被stopWorker删除
// 检查停止列表,如果在则清理
if (stoppedWorkers.has(workerId)) {
stoppedWorkers.delete(workerId);
}
}
});
workerProcess.on('error', (err) => {
console.log(err)
})
const workerInfo = {
process: workerProcess,
status: 'running',
startTime: Date.now(),
restartCount, // 使用传入的重启次数
// socket: null, // 工具进程的TCP连接稍后由工具进程注册
// lastHeartbeat: null,
command,
args,
env,
workerId,
}
workers.set(workerId, workerInfo);
pidToProcessInfoMap.set(workerProcess.pid, workerInfo);
// 定期发送状态更新
broadcastStatus();
}
// 停止工具进程
function stopWorker(workerId) {
const workerInfo = workers.get(workerId);
// 无论workerInfo是否存在都添加到停止列表防止竞态条件
stoppedWorkers.add(workerId);
console.log(`停止工具进程: ${workerId} (已添加到停止列表)`);
if (!workerInfo) {
console.log(`工具进程 ${workerId} 不存在,但已标记为停止(防止重启)`);
// 通知GUI客户端
broadcastToGUI({
type: 'worker-disconnected',
workerId: workerId,
message: `工具进程 ${workerId} 已停止`
});
// 延迟发送状态更新
setTimeout(() => broadcastStatus(), 500);
return;
}
// 关闭工具进程的TCP连接
if (workerInfo.socket) {
workerInfo.socket.destroy();
}
workerInfo.process.kill('SIGTERM');
workers.delete(workerId);
// 通知GUI客户端
broadcastToGUI({
type: 'worker-disconnected',
workerId: workerId,
message: `工具进程 ${workerId} 已断开`
});
// 延迟发送状态更新
setTimeout(() => broadcastStatus(), 500);
}
// 获取所有工具进程状态
function getWorkersStatus() {
const status = [];
for (const [workerId, workerInfo] of workers.entries()) {
status.push({
workerId,
status: workerInfo.status,
uptime: Date.now() - workerInfo.startTime,
restartCount: workerInfo.restartCount || 0,
// connected: workerInfo.socket !== null && !workerInfo.socket.destroyed,
// lastHeartbeat: workerInfo.lastHeartbeat,
command: workerInfo.command,
args: workerInfo.args,
pid: workerInfo.process?.pid,
// 最新截图(通常是 data URL 或 base64 字符串),以及截图时间
screenshot: workerInfo.latestScreenshot ?? null,
screenshotAt: workerInfo.latestScreenshotAt ?? null,
});
}
return status;
}
// 广播状态更新给所有GUI客户端
function broadcastStatus() {
const status = getWorkersStatus();
broadcastToGUI({
type: 'status',
workers: status
});
}
// 广播消息给所有GUI客户端
function broadcastToGUI(message) {
userProcessClients.forEach(socket => {
if (!socket.destroyed) {
try {
sendResponse(socket, null, message);
} catch (e) {
console.error('广播消息失败:', e);
userProcessClients.delete(socket);
}
}
});
}
// 发送响应
function sendResponse(socket, _callbackUuid, response) {
try {
socket.write(JSON.stringify({
...response,
_callbackUuid
}) + '\n');
} catch (e) {
console.error('发送响应失败:', e);
}
}
// 启动服务器
new Promise((resolve, reject) => {
server.once('error', (err) => {
ipcWritePipe.write(
JSON.stringify({ type: 'DAEMON_FATAL', error: err }),
(err) => void err
)
reject(err)
})
server.listen(ipcSocketPath, (err) => {
console.log(`守护进程服务器运行在端口 ${ipcSocketPath}`);
ipcWritePipe.write(
JSON.stringify({ type: 'DAEMON_READY' }),
(err) => void err
)
if (!err) {
resolve(true)
}
else {
reject(err)
}
});
})
// 优雅关闭
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在关闭所有工具进程...');
for (const [workerId, workerInfo] of workers.entries()) {
workerInfo.process.kill('SIGTERM');
}
server.close(() => {
console.log('守护进程已关闭');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('收到SIGINT信号正在关闭所有工具进程...');
for (const [workerId, workerInfo] of workers.entries()) {
workerInfo.process.kill('SIGTERM');
}
server.close(() => {
console.log('守护进程已关闭');
process.exit(0);
});
});

25
packages/pm/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@geekgeekrun/pm",
"version": "0.0.1",
"description": "Electron多进程架构",
"main": "main.js",
"scripts": {
"start:daemon": "electron ./daemon",
"dev:daemon": "electron ./daemon --dev",
"start:worker": "electron ./worker",
"dev:worker": "electron ./worker --dev"
},
"keywords": [
"electron",
"multi-process"
],
"author": "",
"license": "MIT",
"devDependencies": {
"electron": "28.2.0"
},
"dependencies": {
"JSONStream": "^1.3.5",
"split2": "^4.2.0"
}
}

View File

@@ -47,15 +47,20 @@ const main = async () => {
}
const hooks = {
daemonInitialized: new AsyncSeriesHook(),
puppeteerLaunched: new SyncHook(),
puppeteerLaunched: new SyncHook(['browser']),
pageGotten: new SyncHook(['page']),
pageLoaded: new SyncHook(),
cookieWillSet: new SyncHook(['cookies']),
userInfoResponse: new AsyncSeriesHook(['userInfo']),
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo'])
errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
sageTimeEnter: new AsyncSeriesHook(['args']),
sageTimeExit: new AsyncSeriesHook(['args'])
}
initPlugins(hooks)
await hooks.daemonInitialized.callAsync()

View File

@@ -1,15 +1,18 @@
{
"name": "@geekgeekrun/sqlite-plugin",
"version": "0.0.1",
"version": "0.0.2",
"description": "",
"main": "dist/index.js",
"dependencies": {
"better-sqlite3": "12.6.2",
"cli-highlight": "^2.1.11",
"lodash": "^4.17.21",
"reflect-metadata": "^0.2.1",
"sqlite3": "5.1.6",
"typeorm": "0.3.11"
"typeorm": "0.3.28"
},
"devDependencies": {
"@types/lodash": "^4.17.23",
"node-gyp": "^12.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
@Entity()
export class AutoStartChatRunRecord {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm;
@Entity()
export class BossActiveStatusRecord {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm;
@Entity()
export class BossInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
@Entity()
export class BossInfoChangeLog {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
@Entity()
export class ChatMessageRecord {
@@ -25,7 +25,7 @@ export class ChatMessageRecord {
@Column({
nullable: true
})
style?: 'sent' | 'receive';
style?: 'sent' | 'received';
@Column({
nullable: true

View File

@@ -1,5 +1,6 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
import { JobSource } from "../enums";
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
export enum ChatStartupFrom {
AutoFromRecommendList = null,
@@ -29,4 +30,9 @@ export class ChatStartupLog {
nullable: true
})
autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
}

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm
@Entity()
export class CompanyInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
@Entity()
export class CompanyInfoChangeLog {

View File

@@ -0,0 +1,15 @@
import { JobHireStatus } from "../enums";
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm;
@Entity()
export class JobHireStatusRecord {
@PrimaryColumn()
encryptJobId: string;
@Column()
hireStatus: JobHireStatus;
@Column()
lastSeenDate: Date;
}

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm
@Entity()
export class JobInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, PrimaryGeneratedColumn, Column } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, PrimaryGeneratedColumn, Column } = typeorm
@Entity()
export class JobInfoChangeLog {

View File

@@ -0,0 +1,64 @@
import * as typeorm from 'typeorm';
const { Entity, PrimaryGeneratedColumn, Column, Index } = typeorm
@Entity()
@Index(["providerCompleteApiUrl", "model", "providerApiSecret"])
export class LlmModelUsageRecord {
@PrimaryGeneratedColumn()
id: number;
@Column()
providerCompleteApiUrl: string
@Column()
model: string
@Column({
nullable: true
})
providerApiSecret: string
@Column({
nullable: true
})
completionTokens?: number;
@Column({
nullable: true
})
promptTokens?: number;
@Column({
nullable: true
})
promptCacheHitTokens?: number
@Column({
nullable: true
})
promptCacheMissTokens?: number
@Column({
nullable: true
})
totalTokens?: number;
@Column()
requestStartTime: Date
@Column({
nullable: true
})
requestEndTime?: Date
@Column()
hasError: boolean
@Column()
errorMessage: string
@Column({
nullable: true
})
requestScene?: number
}

View File

@@ -1,7 +1,7 @@
import { MarkAsNotSuitReason } from "../enums";
import { requireTypeorm } from "../utils/module-loader";
import { JobSource, MarkAsNotSuitOp, MarkAsNotSuitReason } from "../enums";
import * as typeorm from 'typeorm';
import { ChatStartupFrom } from "./ChatStartupLog";
const { Entity, Column, PrimaryGeneratedColumn } = requireTypeorm()
const { Entity, Column, PrimaryGeneratedColumn } = typeorm
@Entity()
export class MarkAsNotSuitLog {
@@ -27,6 +27,11 @@ export class MarkAsNotSuitLog {
})
markReason?: MarkAsNotSuitReason
@Column({
nullable: true
})
markOp?: MarkAsNotSuitOp
@Column({
nullable: true
})
@@ -36,4 +41,9 @@ export class MarkAsNotSuitLog {
nullable: true
})
autoStartupChatRecordId?: number;
@Column({
nullable: true
})
jobSource?: JobSource;
}

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { Entity, Column, PrimaryColumn } = requireTypeorm()
import * as typeorm from 'typeorm';
const { Entity, Column, PrimaryColumn } = typeorm
@Entity()
export class UserInfo {

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
boss_info.encryptBossId,

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
job_info.*,

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
company_info.*

View File

@@ -1,5 +1,5 @@
import { requireTypeorm } from "../utils/module-loader";
const { ViewEntity, ViewColumn } = requireTypeorm();
import * as typeorm from 'typeorm';
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
job_info.*,

View File

@@ -1,7 +1,7 @@
import { requireTypeorm } from "../utils/module-loader";
import * as typeorm from 'typeorm';
import { ChatStartupFrom } from "./ChatStartupLog";
import { MarkAsNotSuitReason } from "../enums";
const { ViewEntity, ViewColumn } = requireTypeorm();
const { ViewEntity, ViewColumn } = typeorm;
@ViewEntity({
expression: `SELECT
job_info.*,

View File

@@ -3,4 +3,46 @@ export enum MarkAsNotSuitReason {
BOSS_INACTIVE = 1,
USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON = 2,
JOB_NOT_SUIT = 3,
JOB_CITY_NOT_SUIT = 4,
JOB_WORK_EXP_NOT_SUIT = 5,
JOB_SALARY_NOT_SUIT = 6,
COMPANY_NAME_NOT_SUIT = 7,
}
export enum MarkAsNotSuitOp {
MARK_AS_NOT_SUIT_ON_BOSS = 1,
MARK_AS_NOT_SUIT_ON_LOCAL = 2,
NO_OP = 3
}
export enum StrategyScopeOptionWhenMarkJobNotMatch {
ALL_JOB = 1,
ONLY_COMPANY_MATCHED_JOB = 2
}
export enum SalaryCalculateWay {
MONTH_SALARY = 1,
ANNUAL_PACKAGE = 2,
}
export enum JobDetailRegExpMatchLogic {
EVERY = 1,
SOME = 2,
}
export enum JobSource {
expect = 1,
recommend = 2,
search = 3,
}
export enum CombineRecommendJobFilterType {
ANY_COMBINE = 1,
STATIC_COMBINE = 2,
}
export enum JobHireStatus {
HIRING = 1,
CLOSED = 2,
DELETED = 3,
}

View File

@@ -1,4 +1,4 @@
import { DataSource } from "typeorm";
import { DataSource, Raw } from "typeorm";
import { BossActiveStatusRecord } from "./entity/BossActiveStatusRecord";
import { BossInfo } from "./entity/BossInfo";
import { CompanyInfo } from "./entity/CompanyInfo";
@@ -10,6 +10,8 @@ import { CompanyInfoChangeLog } from "./entity/CompanyInfoChangeLog";
import { JobInfoChangeLog } from "./entity/JobInfoChangeLog";
import { MarkAsNotSuitLog } from "./entity/MarkAsNotSuitLog";
import { ChatMessageRecord } from "./entity/ChatMessageRecord";
import { LlmModelUsageRecord } from "./entity/LlmModelUsageRecord";
import { JobHireStatusRecord } from "./entity/JobHireStatusRecord";
function getBossInfoIfIsEqual (savedOne, currentOne) {
if (savedOne === currentOne) {
@@ -244,7 +246,7 @@ export async function saveChatStartupRecord(
ds: DataSource,
_jobInfo,
{ encryptUserId },
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined } = {}
{ autoStartupChatRecordId = undefined, chatStartupFrom = undefined, jobSource = undefined } = {}
) {
const { jobInfo } = _jobInfo;
@@ -255,7 +257,8 @@ export async function saveChatStartupRecord(
encryptCurrentUserId: encryptUserId,
encryptJobId: jobInfo.encryptId,
autoStartupChatRecordId,
chatStartupFrom
chatStartupFrom,
jobSource,
}
Object.assign(chatStartupLog, chatStartupLogPayload)
@@ -269,7 +272,7 @@ export async function saveMarkAsNotSuitRecord(
ds: DataSource,
_jobInfo,
{ encryptUserId },
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined } = {}
{ autoStartupChatRecordId = undefined, markFrom = undefined, extInfo = undefined, markReason = undefined, markOp = undefined, jobSource = undefined } = {}
) {
const { jobInfo } = _jobInfo;
@@ -282,7 +285,9 @@ export async function saveMarkAsNotSuitRecord(
autoStartupChatRecordId,
markFrom,
markReason,
extInfo: extInfo ? JSON.stringify(extInfo) : undefined
extInfo: extInfo ? JSON.stringify(extInfo) : undefined,
markOp,
jobSource,
}
Object.assign(markAsNotSuitLog, markAsNotSuitLogPayload)
@@ -307,3 +312,79 @@ export async function saveChatMessageRecord(
//#endregion
return
}
export async function saveGptCompletionRequestRecord(
ds: DataSource,
records: LlmModelUsageRecord[]
) {
//#region mark-as-not-suit-log
const list = records.map(it => {
const o = new LlmModelUsageRecord()
for (const k of Object.keys(it)) {
o[k] = it[k]
}
return o
})
const chatMessageRecordRepository = ds.getRepository(LlmModelUsageRecord);
await chatMessageRecordRepository.save(list);
//#endregion
return
}
export async function getNotSuitMarkRecordsInLastSomeDays (ds: DataSource, days = 0) {
const repo = ds.getRepository(MarkAsNotSuitLog)
const result = await repo.findBy({
date: Raw(alias => `DATE(${alias}) >= DATE('${
new Date(
Number(new Date()) - days * 24 * 60 * 60 * 1000
).toISOString()
}')`)
})
return result
}
export async function getChatStartupRecordsInLastSomeDays (ds: DataSource, days = 0) {
const repo = ds.getRepository(ChatStartupLog)
const result = await repo.findBy({
date: Raw(alias => `DATE(${alias}) >= DATE('${
new Date(
Number(new Date()) - days * 24 * 60 * 60 * 1000
).toISOString()
}')`)
})
return result
}
export async function getBossIdsByJobIds (ds: DataSource, jobIds: string[] = []) {
const repo = ds.getRepository(JobInfo)
const result = await repo.find({
where: jobIds.map(
id => ({
encryptJobId: id
})
)
})
return result
}
export async function saveJobHireStatusRecord(
ds: DataSource,
record: JobHireStatusRecord
) {
const jobHireStatusRecordRepository = ds.getRepository(JobHireStatusRecord);
await jobHireStatusRecordRepository.save(record);
return
}
export async function getJobHireStatusRecord(
ds: DataSource,
encryptJobId: string
) {
const repo = ds.getRepository(JobHireStatusRecord)
const result = await repo.findOne({
where: {
encryptJobId
}
})
return result
}

View File

@@ -1,6 +1,5 @@
import "reflect-metadata";
import { type DataSource } from "typeorm";
import { requireTypeorm } from "./utils/module-loader";
import { BossInfo } from "./entity/BossInfo";
import { BossInfoChangeLog } from "./entity/BossInfoChangeLog";
@@ -19,22 +18,37 @@ import { VJobLibrary } from "./entity/VJobLibrary";
import { VCompanyLibrary } from "./entity/VCompanyLibrary"
import { VMarkAsNotSuitLog } from "./entity/VMarkAsNotSuitLog"
import { ChatMessageRecord } from './entity/ChatMessageRecord'
import { LlmModelUsageRecord } from './entity/LlmModelUsageRecord'
import { JobHireStatusRecord } from './entity/JobHireStatusRecord'
import sqlite3 from 'sqlite3';
import { saveChatStartupRecord, saveJobInfoFromRecommendPage, saveMarkAsNotSuitRecord } from "./handlers";
import {
saveChatStartupRecord,
saveJobInfoFromRecommendPage,
saveMarkAsNotSuitRecord,
getNotSuitMarkRecordsInLastSomeDays,
getChatStartupRecordsInLastSomeDays,
getBossIdsByJobIds,
saveJobHireStatusRecord
} from "./handlers";
import { UpdateChatStartupLogTable1729182577167 } from "./migrations/1729182577167-UpdateChatStartupLogTable";
import minimist from 'minimist'
import { UpdateBossInfoTable1732032381304 } from "./migrations/1732032381304-UpdateBossInfoTable";
import { JobHireStatus, MarkAsNotSuitOp, MarkAsNotSuitReason } from "./enums";
import { AddColumnForMarkAsNotSuitLog1746092370665 } from "./migrations/1746092370665-AddColumnForMarkAsNotSuitLog";
import { Init1000000000000 } from "./migrations/1000000000000-Init";
import { AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 } from "./migrations/1752380078526-AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog";
import { AddJobHireStatusTable1766466476822 } from "./migrations/1766466476822-AddJobHireStatusTable";
import chunk from 'lodash/chunk'
import * as typeorm from 'typeorm'
export function initDb(dbFilePath) {
const { DataSource } = requireTypeorm()
const { DataSource } = typeorm
const appDataSource = new DataSource({
type: "sqlite",
synchronize: true,
type: "better-sqlite3",
synchronize: false,
logging: true,
logger: "simple-console",
database: dbFilePath,
driver: sqlite3, // The important line
entities: [
ChatStartupLog,
BossInfo,
@@ -53,10 +67,16 @@ export function initDb(dbFilePath) {
MarkAsNotSuitLog,
VMarkAsNotSuitLog,
ChatMessageRecord,
LlmModelUsageRecord,
JobHireStatusRecord,
],
migrations: [
Init1000000000000,
UpdateChatStartupLogTable1729182577167,
UpdateBossInfoTable1732032381304
UpdateBossInfoTable1732032381304,
AddColumnForMarkAsNotSuitLog1746092370665,
AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526,
AddJobHireStatusTable1766466476822
],
migrationsRun: true
});
@@ -75,10 +95,29 @@ export default class SqlitePlugin {
userInfo = null
apply(hooks) {
hooks.pageGotten.tap(
'SqlitePlugin',
(page) => {
page.on('response', async (response) => {
const ds = await this.initPromise;
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
const data = await response.json()
if (data.code === 0) {
await saveJobInfoFromRecommendPage(await ds, data.zpData)
await saveJobHireStatusRecord(await ds, {
encryptJobId: data.zpData.jobInfo.encryptId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
})
}
)
hooks.userInfoResponse.tapPromise(
"SqlitePlugin",
async (userInfoResponse) => {
if (userInfoResponse.code !== 0) {
if (!userInfoResponse || userInfoResponse.code !== 0) {
return;
}
const { zpData: userInfo } = userInfoResponse;
@@ -95,27 +134,117 @@ export default class SqlitePlugin {
return await userInfoRepository.save(user);
}
);
hooks.mainFlowWillLaunch.tapPromise(
"SqlitePlugin",
async ({
jobNotMatchStrategy,
jobNotActiveStrategy,
expectCityNotMatchStrategy,
blockJobNotSuit,
blockBossNotActive,
blockBossNotNewChat
}) => {
if (
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL
) {
const ds = await this.initPromise;
const last7DayMarkRecords = (await getNotSuitMarkRecordsInLastSomeDays(ds, 7)) ?? [];
if (
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
jobNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it =>
[
MarkAsNotSuitReason.JOB_NOT_SUIT,
MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON
].includes(it.markReason)
)
.map(
it => it.encryptJobId
)
.forEach(
id => blockJobNotSuit.add(id)
)
}
if (
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
jobNotActiveStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it => it.markReason === MarkAsNotSuitReason.BOSS_INACTIVE)
.map(
it => it.encryptJobId
)
.forEach(
id => blockJobNotSuit.add(id)
)
}
if (
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_LOCAL ||
expectCityNotMatchStrategy === MarkAsNotSuitOp.MARK_AS_NOT_SUIT_ON_BOSS
) {
last7DayMarkRecords
.filter(it => it.markReason === MarkAsNotSuitReason.JOB_CITY_NOT_SUIT)
.map(
it => it.encryptJobId
)
.forEach(
id => blockJobNotSuit.add(id)
)
}
const last30DayChatStartupRecords = (await getChatStartupRecordsInLastSomeDays(ds, 30)) ?? [];
const chattedJobIds = last30DayChatStartupRecords.map(it => it.encryptJobId)
if (chattedJobIds.length === 0) {
return
}
const chattedJobIdChunks = chunk(chattedJobIds, 200)
const chattedBossIds = [];
for (const chattedJobIdChunk of chattedJobIdChunks) {
const chattedBossIdChunk = ((await getBossIdsByJobIds(ds, chattedJobIdChunk)) ?? []).map(it => it.encryptBossId)
chattedBossIds.push(...chattedBossIdChunk)
}
for (const id of chattedBossIds) {
blockBossNotNewChat.add(id)
}
}
}
);
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async (_jobInfo) => {
const ds = await this.initPromise;
await saveJobInfoFromRecommendPage(ds, _jobInfo);
});
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList } = {}) => {
hooks.jobDetailIsGetFromRecommendList.tapPromise("SqlitePlugin", async ({ jobInfo }) => {
const ds = await this.initPromise;
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
chatStartupFrom
return await saveJobHireStatusRecord(ds, {
encryptJobId: jobInfo.encryptId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
});
});
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined } = {}) => {
hooks.newChatStartup.tapPromise("SqlitePlugin", async (_jobInfo, { chatStartupFrom = ChatStartupFrom.AutoFromRecommendList, jobSource = undefined } = {}) => {
const ds = await this.initPromise;
return await saveChatStartupRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
chatStartupFrom,
jobSource
});
});
hooks.jobMarkedAsNotSuit.tapPromise("SqlitePlugin", async (_jobInfo, { markFrom = ChatStartupFrom.AutoFromRecommendList, markReason = undefined, extInfo = undefined, markOp = undefined, jobSource = undefined } = {}) => {
const ds = await this.initPromise;
return await saveMarkAsNotSuitRecord(ds, _jobInfo, this.userInfo, {
autoStartupChatRecordId: this.runRecordId,
markFrom,
markReason,
extInfo
extInfo,
markOp,
jobSource
});
});
}

View File

@@ -0,0 +1,24 @@
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
export class Init1000000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const tableSchemaStatements = `CREATE TABLE IF NOT EXISTS "boss_info" ("encryptBossId" varchar PRIMARY KEY NOT NULL, "encryptCompanyId" varchar, "name" varchar NOT NULL, "date" datetime NOT NULL, "title" varchar NOT NULL);
CREATE TABLE IF NOT EXISTS "boss_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptBossId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
CREATE TABLE IF NOT EXISTS "chat_startup_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "encryptCurrentUserId" varchar NOT NULL, "date" datetime NOT NULL, "chatStartupFrom" integer, "autoStartupChatRecordId" integer);
CREATE TABLE IF NOT EXISTS "company_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptCompanyId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
CREATE TABLE IF NOT EXISTS "company_info" ("encryptCompanyId" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "brandName" varchar NOT NULL, "scaleLow" integer, "scaleHigh" integer, "stageName" varchar, "industryName" varchar);
CREATE TABLE IF NOT EXISTS "job_info" ("encryptJobId" varchar PRIMARY KEY NOT NULL, "jobName" varchar NOT NULL, "positionName" varchar NOT NULL, "salaryLow" integer, "salaryHigh" integer, "salaryMonth" integer, "experienceName" varchar NOT NULL, "publishDate" datetime, "degreeName" varchar, "address" varchar, "description" varchar NOT NULL, "encryptBossId" varchar NOT NULL, "encryptCompanyId" varchar NOT NULL);
CREATE TABLE IF NOT EXISTS "job_info_change_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "updateTime" datetime NOT NULL, "dataAsJson" varchar NOT NULL);
CREATE TABLE IF NOT EXISTS "boss_active_status_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptBossId" varchar NOT NULL, "lastActiveStatus" varchar, "updateTime" datetime NOT NULL);
CREATE TABLE IF NOT EXISTS "user_info" ("encryptUserId" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL);
CREATE TABLE IF NOT EXISTS "auto_start_chat_run_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "date" datetime NOT NULL);
CREATE TABLE IF NOT EXISTS "mark_as_not_suit_log" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptJobId" varchar NOT NULL, "encryptCurrentUserId" varchar NOT NULL, "date" datetime NOT NULL, "markFrom" integer, "markReason" integer, "extInfo" varchar, "autoStartupChatRecordId" integer);
CREATE TABLE IF NOT EXISTS "chat_message_record" ("mid" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "encryptFromUserId" varchar NOT NULL, "encryptToUserId" varchar NOT NULL, "time" datetime, "type" varchar, "style" varchar, "text" varchar, "imageUrl" varchar, "imageWidth" integer, "imageHeight" integer);
CREATE TABLE IF NOT EXISTS "llm_model_usage_record" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "providerCompleteApiUrl" varchar NOT NULL, "model" varchar NOT NULL, "providerApiSecret" varchar, "completionTokens" integer, "promptTokens" integer, "promptCacheHitTokens" integer, "promptCacheMissTokens" integer, "totalTokens" integer, "requestStartTime" datetime NOT NULL, "requestEndTime" datetime, "hasError" boolean NOT NULL, "errorMessage" varchar NOT NULL, "requestScene" integer);`.split('\n')
for(const statement of tableSchemaStatements) {
await queryRunner.query(statement);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View File

@@ -1,19 +1,24 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { VBossLibrary } from "../entity/VBossLibrary";
import { VChatStartupLog } from "../entity/VChatStartupLog";
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
import { VJobLibrary } from "../entity/VJobLibrary";
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
const viewNames = [
"v_boss_library",
"v_chat_startup_log",
"v_company_library",
"v_job_library",
"v_mark_as_not_suit_log"
];
const ViewEntities = [
VBossLibrary,
VChatStartupLog,
VCompanyLibrary,
VJobLibrary,
VMarkAsNotSuitLog,
]
export class UpdateChatStartupLogTable1729182577167
implements MigrationInterface
{
export class UpdateChatStartupLogTable1729182577167 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
for (const viewName of viewNames) {
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
}
if (await queryRunner.hasTable("boss_active_status_record")) {
if (await queryRunner.hasColumn("boss_active_status_record", "updateDate")) {
@@ -40,14 +45,17 @@ export class UpdateChatStartupLogTable1729182577167
})
)
}
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
let expression = viewMetadata.expression;
if (typeof expression === 'function') {
expression = expression(dataSource).getQuery();
}
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
for (const viewName of viewNames) {
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
}
await queryRunner.query(
`ALTER TABLE boss_active_status_record RENAME COLUMN updateTime TO updateDate`
);
}
}

View File

@@ -1,35 +1,48 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { VBossLibrary } from "../entity/VBossLibrary";
import { VChatStartupLog } from "../entity/VChatStartupLog";
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
import { VJobLibrary } from "../entity/VJobLibrary";
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
const viewNames = [
"v_boss_library",
"v_chat_startup_log",
"v_company_library",
"v_job_library",
"v_mark_as_not_suit_log"
];
const ViewEntities = [
VBossLibrary,
VChatStartupLog,
VCompanyLibrary,
VJobLibrary,
VMarkAsNotSuitLog,
]
export class UpdateBossInfoTable1732032381304 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
for (const viewName of viewNames) {
await queryRunner.query(`DROP VIEW IF EXISTS "${viewName}"`);
}
if (await queryRunner.hasTable("boss_info")) {
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
await queryRunner.changeColumn(
'boss_info',
'encryptCompanyId',
new TableColumn({
name: 'encryptCompanyId',
type: 'varchar',
isNullable: true
})
)
debugger
}
}
public async up(queryRunner: QueryRunner): Promise<void> {
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
if (await queryRunner.hasTable("boss_info")) {
if (await queryRunner.hasColumn("boss_info", "encryptCompanyId")) {
await queryRunner.changeColumn(
"boss_info",
"encryptCompanyId",
new TableColumn({
name: "encryptCompanyId",
type: "varchar",
isNullable: true,
})
);
}
}
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
let expression = viewMetadata.expression;
if (typeof expression === 'function') {
expression = expression(dataSource).getQuery();
}
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View File

@@ -0,0 +1,47 @@
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { VBossLibrary } from "../entity/VBossLibrary";
import { VChatStartupLog } from "../entity/VChatStartupLog";
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
import { VJobLibrary } from "../entity/VJobLibrary";
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
const ViewEntities = [
VBossLibrary,
VChatStartupLog,
VCompanyLibrary,
VJobLibrary,
VMarkAsNotSuitLog,
]
export class AddColumnForMarkAsNotSuitLog1746092370665 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
}
if (await queryRunner.hasTable("mark_as_not_suit_log")) {
if (!await queryRunner.hasColumn("mark_as_not_suit_log", "markOp")) {
await queryRunner.addColumn(
"mark_as_not_suit_log",
new TableColumn({
name: "markOp",
type: "number",
isNullable: true,
})
);
}
}
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
let expression = viewMetadata.expression;
if (typeof expression === 'function') {
expression = expression(dataSource).getQuery();
}
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View File

@@ -0,0 +1,64 @@
import { DataSource, MigrationInterface, QueryRunner, TableColumn } from "typeorm"
import { VBossLibrary } from "../entity/VBossLibrary";
import { VChatStartupLog } from "../entity/VChatStartupLog";
import { VCompanyLibrary } from "../entity/VCompanyLibrary";
import { VJobLibrary } from "../entity/VJobLibrary";
import { VMarkAsNotSuitLog } from "../entity/VMarkAsNotSuitLog";
import { JobSource } from "../enums";
const ViewEntities = [
VBossLibrary,
VChatStartupLog,
VCompanyLibrary,
VJobLibrary,
VMarkAsNotSuitLog,
]
export class AddJobSourceColumnForChatStartupLogAndMarkAsNotSuitLog1752380078526 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
await queryRunner.query(`DROP VIEW IF EXISTS "${viewMetadata.tableName}"`);
}
if (await queryRunner.hasTable("mark_as_not_suit_log")) {
if (!await queryRunner.hasColumn("mark_as_not_suit_log", "jobSource")) {
await queryRunner.addColumn(
"mark_as_not_suit_log",
new TableColumn({
name: "jobSource",
type: "number",
isNullable: true,
})
);
await queryRunner.query(`UPDATE mark_as_not_suit_log SET jobSource=?`, [JobSource.expect]);
}
}
if (await queryRunner.hasTable("chat_startup_log")) {
if (!await queryRunner.hasColumn("chat_startup_log", "jobSource")) {
await queryRunner.addColumn(
"chat_startup_log",
new TableColumn({
name: "jobSource",
type: "number",
isNullable: true,
})
);
await queryRunner.query(`UPDATE chat_startup_log SET jobSource=?`, [JobSource.expect]);
}
}
for (const EntityDefinition of ViewEntities) {
const dataSource = queryRunner.connection as DataSource;
const viewMetadata = dataSource.getMetadata(EntityDefinition);
let expression = viewMetadata.expression;
if (typeof expression === 'function') {
expression = expression(dataSource).getQuery();
}
await queryRunner.query(`CREATE VIEW "${viewMetadata.tableName}" AS ${expression}`);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View File

@@ -0,0 +1,12 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class AddJobHireStatusTable1766466476822 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS "job_hire_status_record" ("encryptJobId" varchar PRIMARY KEY NOT NULL, "hireStatus" integer NOT NULL, "lastSeenDate" datetime NOT NULL);`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View File

@@ -1,8 +0,0 @@
import * as path from 'node:path';
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
const isUiDev = process.env.NODE_ENV === 'development'
export function requireTypeorm () {
const importResult = require('typeorm')
return importResult
}

View File

@@ -3,43 +3,74 @@ import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import { presetUno, presetAttributify, presetIcons } from 'unocss'
import transformerDirective from "@unocss/transformer-directives";
import transformerDirective from '@unocss/transformer-directives'
import Replace from 'unplugin-replace/vite'
process.env = { ...process.env, ...loadEnv(process.env.NODE_ENV!, process.cwd()) }
const mainPlugins = [
externalizeDepsPlugin({
exclude: [
'@geekgeekrun/utils',
'find-chrome-bin',
'@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
]
}),
Replace({
delimiters: ['', ''],
sourcemap: true,
include: ['**/src/main/utils/gtag/Analytics.ts'],
values: [
{
find: /<measurement_id>/g,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string
},
{
find: /<api_secret>/g,
replacement: process.env.VITE_APP_GTAG_API_SECRET as string
}
]
})
]
const preloadPlugins = [externalizeDepsPlugin()]
const rendererPlugins = [
vue(),
UnoCSS({
presets: [presetUno(), presetAttributify(), presetIcons()],
transformers: [transformerDirective()]
})
]
if (process.env.NODE_ENV) {
;[mainPlugins, preloadPlugins, rendererPlugins].forEach((pluginList) => {
pluginList.push(
Replace({
delimiters: ['', ''],
sourcemap: true,
include: ['**'],
values: [
{
find: /process.env.NODE_ENV/g,
replacement: `'${process.env.NODE_ENV}'` as string
}
]
})
)
})
}
export default defineConfig({
main: {
build: {
rollupOptions: {
external: []
},
minify: 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
},
plugins: [
externalizeDepsPlugin({
exclude: ['@geekgeekrun/geek-auto-start-chat-with-boss', '@geekgeekrun/dingtalk-plugin', '@geekgeekrun/utils', 'find-chrome-bin', '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension']
}),
Replace({
delimiters: ['', ''],
sourcemap: true,
include: ['**/src/main/utils/gtag/Analytics.ts'],
values: [
{
find: /<measurement_id>/g,
replacement: process.env.VITE_APP_GTAG_MEASUREMENT_ID as string,
},
{
find: /<api_secret>/g,
replacement: process.env.VITE_APP_GTAG_API_SECRET as string,
}
]
})
]
plugins: mainPlugins
},
preload: {
plugins: [externalizeDepsPlugin()],
plugins: preloadPlugins,
build: {
minify: 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
}
},
renderer: {
@@ -48,15 +79,9 @@ export default defineConfig({
'@renderer': resolve('src/renderer/src')
}
},
plugins: [
vue(),
UnoCSS({
presets: [presetUno(), presetAttributify(), presetIcons()],
transformers: [transformerDirective()],
})
],
plugins: rendererPlugins,
build: {
minify: 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
}
}
})

View File

@@ -1,7 +1,7 @@
{
"name": "geekgeekrun-ui",
"version": "0.3.1",
"description": "Boss 炸弹 - 自动开聊Boss,助力每位打工人求职!",
"version": "0.16.0",
"description": "BOSS 炸弹 - 自动开聊BOSS,助力每位打工人求职!",
"main": "./out/main/index.js",
"author": "geekgeekrun",
"productName": "GeekGeekRun",
@@ -9,10 +9,6 @@
"scripts": {
"start": "node scripts/run-build-sqlite-plugin.mjs && electron-vite preview",
"dev": "node scripts/run-build-sqlite-plugin.mjs && electron-vite dev",
"dev:geek-auto-start-chat-with-boss-main-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=geekAutoStartWithBossMain pnpm run dev",
"dev:geek-auto-start-chat-with-boss-daemon-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=geekAutoStartWithBossDaemon pnpm run dev",
"dev:check-and-download-dependencies-for-init-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=checkAndDownloadDependenciesForInit pnpm run dev",
"dev:launch-bosszhipin-login-page-with-preload-extension-only": "cross-env MAIN_BOSSGEEKGO_UI_RUN_MODE=launchBossZhipinLoginPageWithPreloadExtension pnpm run dev",
"build": "node scripts/run-build-sqlite-plugin.mjs && electron-vite build",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
@@ -30,27 +26,35 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@geekgeekrun/dingtalk-plugin": "workspace:*",
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
"@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension": "workspace:*",
"@geekgeekrun/pm": "workspace:*",
"@geekgeekrun/puppeteer-extra-plugin-laodeng": "workspace:*",
"@geekgeekrun/sqlite-plugin": "workspace:*",
"@geekgeekrun/utils": "workspace:*",
"JSONStream": "^1.3.5",
"diff": "^7.0.0",
"electron-updater": "^6.1.7",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"mitt": "^3.0.1",
"node-machine-id": "^1.1.12",
"puppeteer": "20.1.0",
"puppeteer-extra-plugin-stealth": "2.11.2"
"pinia": "^3.0.2",
"puppeteer": "24.19.0",
"puppeteer-extra-plugin-stealth": "2.11.2",
"uuid": "^11.1.0",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@geekgeekrun/dingtalk-plugin": "workspace:*",
"@geekgeekrun/geek-auto-start-chat-with-boss": "workspace:*",
"@inquirer/prompts": "^7.0.1",
"@inquirer/select": "^4.0.1",
"@puppeteer/browsers": "^2.0.0",
"@puppeteer/browsers": "^2.11.0",
"@rushstack/eslint-patch": "^1.7.1",
"@types/lodash": "^4.17.23",
"@types/node": "^18.19.9",
"@unocss/preset-attributify": "^0.58.5",
"@unocss/preset-icons": "^0.58.5",
@@ -60,18 +64,18 @@
"@vue/eslint-config-typescript": "^12.0.0",
"animate.css": "^4.1.1",
"cross-env": "^7.0.3",
"electron": "28.2.0",
"electron": "39.2.7",
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"element-plus": "^2.5.5",
"element-plus": "^2.8.4",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.1",
"find-chrome-bin": "^2.0.1",
"find-chrome-bin": "2.0.4",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"normalize.css": "^8.0.1",
"prettier": "^3.2.4",
"sass": "^1.70.0",
"spark-md5": "^3.0.2",
"terser": "^5.37.0",
"typescript": "^5.3.3",
"unocss": "^0.58.5",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -1,7 +1,7 @@
{
"version": "0.3.1",
"buildVersion": 7,
"buildTime": 1736688071781,
"buildHash": "537208b855436d26e6f9f5e396b05360b57cb0b4",
"version": "0.16.0",
"buildVersion": 36,
"buildTime": 1771743424925,
"buildHash": "98ffc3a143fc2c0ed5a949e441f8159f95ed6afa",
"name": "geekgeekrun-ui"
}

View File

@@ -0,0 +1,2 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'

View File

@@ -5,6 +5,21 @@ export enum AUTO_CHAT_ERROR_EXIT_CODE {
ERR_INTERNET_DISCONNECTED = 83,
ACCESS_IS_DENIED = 84,
PUPPETEER_IS_NOT_EXECUTABLE = 85,
AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86,
AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87,
LLM_UNAVAILABLE = 86,
}
export enum RECHAT_CONTENT_SOURCE {
LOOK_FORWARD_EMOTION = 1,
GEMINI_WITH_CHAT_CONTEXT = 2
}
export enum RECHAT_LLM_FALLBACK {
SEND_LOOK_FORWARD_EMOTION = 1,
EXIT_REMINDER_PROGRAM = 2
}
export enum RUNNING_STATUS_ENUM {
RUNNING = 0,
NORMAL_EXITED = 1,
ERROR_EXITED = 2
}

View File

@@ -0,0 +1,18 @@
export const getAutoStartChatSteps = () => [
{
id: 'worker-launch',
describe: '启动子进程'
},
{
id: 'puppeteer-executable-check',
describe: 'Puppeteer 可执行程序检查'
},
{
id: 'basic-cookie-check',
describe: 'Cookie 格式检查'
},
{
id: 'login-status-check',
describe: '登录状态检查'
}
]

View File

@@ -0,0 +1,7 @@
export interface NewReleaseInfo {
releaseVersion: string
releasePageUrl: string
assetUrl: string
assetName: string
releaseNote: string
}

View File

@@ -0,0 +1,9 @@
export function messageForSaveFilter(it) {
return (
it.status !== 3 && // filter system notification out
it.templateId === 1 && // filter system notification out
((['text', 'sticker', 'image', 'sound', 'comDesc'].includes(it.messageType) &&
!it.extend?.greetingQuestionAnswer) || // include those message, filter out auto ask
(it.messageType === 'dialog' && [0, 1, 2, 8, 11, 12, 14, 17, 33].includes(it?.dialog?.type))) // include message like resume, phone, map, etc., filter out auto ask
)
}

View File

@@ -0,0 +1,91 @@
export interface ResumeContent {
name: string
workYearDesc: string
expectJob: string
userDescription: string
geekWorkExpList: Array<{
company: string
positionName: string
startYearMon: string | null
endYearMon: string | null
performance: string
workDescription: string
}>
geekProjExpList: Array<{
name: string
startYearMon: string
endYearMon: string
roleName: string
projectDescription: string
performance: string
}>
expectSalary: [string, string]
}
export function formatResumeJsonToMarkdown(resume) {
const basicInfoText = [
['# 姓名', resume.content.name],
['# 工作年限', resume.content.workYearDesc],
['# 期望职位', resume.content.expectJob],
['# 个人优势', resume.content.userDescription]
]
.filter((it) => {
return Boolean(it[1]?.trim())
})
.map((it) => it.join('\n'))
.join('\n\n')
let formattedWorkExpText = resume.content.geekWorkExpList
.filter((it) => Boolean(it.company?.trim()))
.map((it) => {
const info = [
[`职务`, it.positionName],
[`任职时间`],
[`工作描述`, it.workDescription],
[`工作业绩`, it.performance]
].filter((it) => {
return Boolean(it[1]?.trim())
})
return [[`## ${it.company}`], ...info].map((it) => it.join('\n')).join('\n\n')
})
.join('\n')
if (formattedWorkExpText?.trim()) {
formattedWorkExpText = '# 工作经历\n' + formattedWorkExpText
}
let formattedProjWorkExpText = resume.content.geekProjExpList
.filter((it) => Boolean(it.name?.trim()))
.map((it) => {
const info = [
[`## ${it.name}`],
[`项目角色`, it.roleName],
[`项目时间`],
[`工作描述`, it.projectDescription],
[`工作业绩`, it.performance]
].filter((it) => {
return Boolean(it[1]?.trim())
})
return [[`## ${it.name}`], ...info].map((it) => it.join('\n')).join('\n\n')
})
.join('\n')
if (formattedProjWorkExpText?.trim()) {
formattedProjWorkExpText = '# 项目经历\n' + formattedProjWorkExpText
}
const result = `${basicInfoText}\n\n${formattedWorkExpText}\n\n${formattedProjWorkExpText}`
return result
}
export function checkIsResumeContentValid(resumeItem: { content: ResumeContent }) {
return (
!!resumeItem?.content &&
resumeItem.content.geekProjExpList?.[0]?.name?.trim() &&
resumeItem.content.geekWorkExpList?.[0]?.company?.trim()
)
}
export function resumeContentEnoughDetect(resumeItem: { content: ResumeContent }) {
return resumeItem?.content && formatResumeJsonToMarkdown(resumeItem)?.length > 800
}

View File

@@ -0,0 +1,4 @@
import path from 'node:path'
import os from 'node:os'
export const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')

View File

@@ -0,0 +1,28 @@
import { ipcMain } from 'electron'
import { createCommonJobConditionConfigWindow } from '../window/commonJobConditionConfigWindow'
import { mainWindow } from '../window/mainWindow'
let commonJobConditionConfigWindow = null
export async function waitForCommonJobConditionDone() {
return new Promise((resolve, reject) => {
commonJobConditionConfigWindow = createCommonJobConditionConfigWindow({
parent: mainWindow!,
modal: true,
show: true
})
let processDone = false
function handler() {
processDone = true
commonJobConditionConfigWindow.close()
}
ipcMain.once('common-job-condition-config-done', handler)
commonJobConditionConfigWindow.on('closed', async () => {
ipcMain.off('common-job-condition-config-done', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED'))
}
})
})
}

View File

@@ -0,0 +1,26 @@
import { ipcMain } from 'electron'
import {
createBrowserAssistantWindow,
browserAssistantWindow
} from '../window/browserAssistantWindow'
export async function configWithBrowserAssistant({ windowOption, autoFind } = {}) {
return new Promise((resolve, reject) => {
createBrowserAssistantWindow({ ...windowOption }, { autoFind })
let processDone = false
function handler() {
processDone = true
browserAssistantWindow.close()
}
ipcMain.once('browser-config-saved', handler)
browserAssistantWindow.once('closed', () => {
ipcMain.off('browser-config-saved', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED_CONFIG_BROWSER'))
}
})
})
}

View File

@@ -0,0 +1,93 @@
import { sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
import minimist from 'minimist'
import { loginWithCookieAssistant } from './login-with-cookie-assistant'
import { checkCookieListFormat } from '../../common/utils/cookie'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { readStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export class CookieInvalidHandlePlugin {
apply(hooks) {
hooks.cookieWillSet.tapPromise('CookieInvalidHandlePlugin', async (cookies) => {
let isValid = checkCookieListFormat(cookies)
while (!isValid) {
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
await sleep(2000)
const newCookies = readStorageFile('boss-cookies.json')
isValid = checkCookieListFormat(newCookies)
if (isValid) {
cookies.length = 0
for (const cookie of newCookies) {
cookies.push(cookie)
}
}
} catch (e) {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
},
runRecordId
}
})
})
hooks.userInfoResponse.tapPromise('CookieInvalidHandlePlugin', async (userInfoResponse) => {
if (userInfoResponse.code === 0) {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
},
runRecordId
}
})
return
}
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
} catch (e) {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
// throw new Error('THROW_FOR_RETRY')
return Promise.reject(new Error('THROW_FOR_RETRY'))
})
}
}

View File

@@ -3,6 +3,11 @@ import os from 'os'
import path from 'path'
import buildInfo from '../../common/build-info.json'
import { ensureStorageFileExist } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import {
createFirstLaunchNoticeWindow,
firstLaunchNoticeWindow
} from '../window/firstLaunchNoticeWindow'
import { ipcMain } from 'electron'
export const firstLaunchNoticeApproveFlagPath = path.join(
os.homedir(),
@@ -10,8 +15,28 @@ export const firstLaunchNoticeApproveFlagPath = path.join(
'ui-first-launch-notice-flag'
)
export const isFirstLaunchNoticeApproveFlagExist = () => fs.existsSync(firstLaunchNoticeApproveFlagPath)
export const isFirstLaunchNoticeApproveFlagExist = () =>
fs.existsSync(firstLaunchNoticeApproveFlagPath)
export const createFirstLaunchNoticeApproveFlag = () => {
ensureStorageFileExist()
fs.writeFileSync(firstLaunchNoticeApproveFlagPath, buildInfo.version)
}
export async function waitForUserApproveAgreement({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createFirstLaunchNoticeWindow({ ...windowOption })
let processDone = false
function handler() {
processDone = true
firstLaunchNoticeWindow.close()
}
ipcMain.once('first-launch-notice-approve', handler)
firstLaunchNoticeWindow.once('closed', () => {
ipcMain.off('first-launch-notice-approve', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED'))
}
})
})
}

View File

@@ -0,0 +1,22 @@
import { saveGptCompletionRequestRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
export enum RequestSceneEnum {
testing = 1,
readNoReplyAutoReminder = 2,
geekAutoStartChatWithBoss = 3
}
let dbInitPromise
export const recordGptCompletionRequest = async (payload) => {
const { getPublicDbFilePath } = await import(
'@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
)
const { initDb } = await import('@geekgeekrun/sqlite-plugin')
if (!dbInitPromise) {
dbInitPromise = initDb(getPublicDbFilePath())
}
const ds = await dbInitPromise
const o = { ...payload }
await saveGptCompletionRequestRecord(ds, [o])
}

View File

@@ -0,0 +1,23 @@
import { ipcMain } from 'electron'
import { createCookieAssistantWindow, cookieAssistantWindow } from '../window/cookieAssistantWindow';
export async function loginWithCookieAssistant({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createCookieAssistantWindow({ ...windowOption })
let processDone = false
function handler() {
processDone = true
cookieAssistantWindow.close()
}
ipcMain.once('cookie-saved', handler)
cookieAssistantWindow.once('closed', () => {
ipcMain.off('cookie-saved', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED_LOGIN'))
}
})
})
}

View File

@@ -0,0 +1,28 @@
import { ipcMain } from 'electron'
import {
createBrowserDownloadProgressWindow,
browserDownloadProgressWindow
} from '../window/browserDownloadProgressWindow'
export async function openBrowserDownloadWindow({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createBrowserDownloadProgressWindow({ ...windowOption })
let processDone = false
let pathOfDownloadedBrowser = null
function handler(_, executablePath) {
pathOfDownloadedBrowser = executablePath
processDone = true
browserDownloadProgressWindow.close()
}
ipcMain.once('browser-download-done', handler)
browserDownloadProgressWindow.once('closed', () => {
ipcMain.off('browser-download-done', handler)
if (processDone) {
resolve(pathOfDownloadedBrowser)
} else {
reject(new Error('USER_CANCELLED_CONFIG_BROWSER'))
}
})
})
}

View File

@@ -0,0 +1,74 @@
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat'
import { daemonEE, sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
import { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db'
import minimist from 'minimist'
export async function runCommon({ mode }) {
await sendToDaemon(
{
type: 'user-process-register'
},
{
needCallback: true
}
)
const taskList = (
await sendToDaemon(
{
type: 'get-status'
},
{
needCallback: true
}
)
)?.workers
const runningTask = taskList?.find((it) => it.workerId === mode)
if (runningTask) {
const commandlineArgs = minimist(runningTask.args ?? [])
const runRecordId = Number(commandlineArgs['run-record-id'])
console.log('任务已在运行中')
return {
runRecordId,
isAlreadyRunning: true
}
}
const currentRunRecord = (await saveAndGetCurrentRunRecord())?.data
const subProcessEnv = {
...process.env,
GEEKGEEKRUND_NO_AUTO_RESTART_EXIT_CODE: [
AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE,
AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID,
AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE
].join(',')
}
const args =
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
: [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
await sendToDaemon(
{
type: 'start-worker',
workerId: mode,
command: process.argv[0],
args,
env: subProcessEnv
},
{
needCallback: true
}
)
daemonEE.on('message', (message) => {
if (message.type === 'worker-exited') {
if (
message.workerId === mode &&
!message.restarting &&
globalThis.GEEKGEEKRUN_PROCESS_ROLE !== 'ui'
) {
process.exit(0)
}
}
})
return {
runRecordId: currentRunRecord?.id
}
}

View File

@@ -0,0 +1,103 @@
import semver from 'semver'
import packageJson from '../../../package.json'
import os from 'node:os'
import gtag from '../utils/gtag'
import { NewReleaseInfo } from '../../common/types/update'
export const currentOsPlatform = os.platform()
const RELEASE_LIST_URL = `https://api.github.com/repos/geekgeekrun/geekgeekrun/releases`
export interface GitHubReleaseItem {
tag_name: string
prerelease: boolean
draft: boolean
html_url: string
body: string
assets: Array<{
browser_download_url: string
name: string
}>
}
// find by arch, appendix
function findTargetAsset(
assets: GitHubReleaseItem['assets']
): null | GitHubReleaseItem['assets'][number] {
let assetsFilteredByPlatform: GitHubReleaseItem['assets'] = []
switch (os.platform()) {
case 'win32': {
assetsFilteredByPlatform = assets.filter((it) => it.name.endsWith('.exe'))
break
}
case 'darwin': {
assetsFilteredByPlatform = assets.filter((it) => it.name.endsWith('.dmg'))
break
}
case 'linux': {
assetsFilteredByPlatform = assets.filter((it) => it.name.endsWith('.deb'))
break
}
}
if (!assetsFilteredByPlatform?.length) {
return null
}
if (assetsFilteredByPlatform.length === 1) {
return assetsFilteredByPlatform[0]
}
let targetAsset
if (os.platform() === 'darwin') {
targetAsset = assetsFilteredByPlatform.find((it) => it.name.includes(`_${os.arch()}`))
}
return targetAsset
}
export const checkUpdateForUi = async (): Promise<NewReleaseInfo | null> => {
let releaseList: GitHubReleaseItem[] = []
try {
const headers = new Headers()
headers.append('Accept', 'application/vnd.github+json')
headers.append('X-GitHub-Api-Version', '2022-11-28')
await fetch(RELEASE_LIST_URL, {
method: 'GET',
headers: headers,
redirect: 'follow'
})
.then((res) => res.json())
.then((res) => {
if (Array.isArray(res)) {
releaseList = res
} else {
throw res
}
})
} catch (err) {
gtag('check_update_error', { err: JSON.stringify(err) })
console.log(err)
}
console.log(releaseList)
const availableRelease = releaseList.find((it) => !it.draft && !it.prerelease)
if (!availableRelease) {
return null
}
const availableReleaseVersion = availableRelease.tag_name.replace(/^(ui-v)/, '')
if (!semver.gt(availableReleaseVersion, packageJson.version)) {
return null
}
const { assets } = availableRelease
const targetAsset = findTargetAsset(assets ?? [])
if (targetAsset) {
gtag('update_found', {
currentVersion: packageJson.version,
newVersion: availableReleaseVersion
})
console.log(targetAsset)
return {
releaseVersion: availableReleaseVersion,
releasePageUrl: availableRelease.html_url,
assetUrl: targetAsset.browser_download_url,
assetName: targetAsset.name,
releaseNote: availableRelease.body
}
}
return null
}

View File

@@ -9,8 +9,9 @@ export enum DOWNLOAD_ERROR_EXIT_CODE {
DOWNLOAD_ERROR = 80
}
export const checkAndDownloadDependenciesForInit = async () => {
export const downloadDependenciesForInit = async () => {
process.on('disconnect', () => app.exit())
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
app.dock?.hide()
let pipe: null | fs.WriteStream = null
try {
@@ -49,7 +50,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
timeoutTimer = setTimeout(() => {
// will encounter this when network disconnected when downloading
promiseWithResolver.reject(new Error('PROGRESS_NOT_CHANGED_TOO_LONG'))
}, 5 * 1000)
}, 10 * 1000)
} else {
clearTimeout(throttleProgressTimer)
throttleProgressTimer = null
@@ -72,7 +73,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
)
throttleProgressTimer = setTimeout(() => {
throttleProgressTimer = null
}, 2500)
}, 500)
}
}
})
@@ -93,11 +94,13 @@ export const checkAndDownloadDependenciesForInit = async () => {
pipe,
JSON.stringify({
type: 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR',
...err instanceof Error ? {
name: err.name,
message: err.message,
stack: err.stack
} : null
...(err instanceof Error
? {
name: err.name,
message: err.message,
stack: err.stack
}
: null)
}) + '\r\n'
)
await sleep(1000)

View File

@@ -8,6 +8,8 @@ export interface BrowserInfo {
executablePath: string
}
const CONFIG_VERSION = 2
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
export const lastUsedBrowserRecordFilePath = path.join(
runtimeFolderPath,
@@ -28,8 +30,13 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
}
try {
const fileContent = (await fsPromise.readFile(lastUsedBrowserRecordFilePath)).toString()
const [path, browser] = fileContent.split('\n').map((it) => it.trim())
if (!path || !fs.existsSync(path)) {
const [path, browser, configVersion] = fileContent.split('\n').map((it) => it.trim())
if (
!path ||
!fs.existsSync(path) ||
!Number(configVersion) ||
Number(configVersion) < CONFIG_VERSION
) {
await removeLastUsedAndAvailableBrowserPath()
return null
}
@@ -50,7 +57,7 @@ export const saveLastUsedAndAvailableBrowserInfo = async (browserInfo: BrowserIn
}
await fsPromise.writeFile(
lastUsedBrowserRecordFilePath,
[browserInfo.executablePath, browserInfo.browser].join('\n')
[browserInfo.executablePath, browserInfo.browser, CONFIG_VERSION].join('\n')
)
} catch {
console.warn('lastUsedBrowserRecordFile write error')

View File

@@ -1,5 +1,3 @@
import * as path from 'node:path'
import * as os from 'node:os'
import * as fs from 'node:fs'
import type { InstalledBrowser } from '@puppeteer/browsers'
import {
@@ -8,18 +6,16 @@ import {
getLastUsedAndAvailableBrowser,
removeLastUsedAndAvailableBrowserPath
} from '../browser-history'
import { getExecutableFileVersion } from '@geekgeekrun/utils/windows-only/file.mjs'
import gtag from '../../../../utils/gtag'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../../common/constant'
import { cacheDir } from '../../../../constant'
const getPuppeteerManagerModule = async () => {
let puppeteerManager
puppeteerManager = await import('@puppeteer/browsers')
const puppeteerManager = await import('@puppeteer/browsers')
return puppeteerManager
}
const EXPECT_CHROMIUM_BUILD_ID = '113.0.5672.63'
const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
const getExpectCachedPuppeteerExecutable = async (): Promise<BrowserInfo> => {
const puppeteerManager = await getPuppeteerManagerModule()
@@ -76,7 +72,8 @@ export const checkAndDownloadPuppeteerExecutable = async (
browser: puppeteerManager.Browser.CHROME,
cacheDir,
buildId: EXPECT_CHROMIUM_BUILD_ID,
downloadProgressCallback: options.downloadProgressCallback
downloadProgressCallback: options.downloadProgressCallback,
baseUrl: `https://registry.npmmirror.com/-/binary/chrome-for-testing`
})
} else {
gtag('use_installed_browser')
@@ -86,41 +83,48 @@ export const checkAndDownloadPuppeteerExecutable = async (
})
).find((it) => it.buildId === EXPECT_CHROMIUM_BUILD_ID)!
}
await saveLastUsedAndAvailableBrowserInfo({
executablePath: installedBrowser.executablePath,
browser:
installedBrowser.browser[0].toUpperCase() +
installedBrowser.browser.slice(1) +
' ' +
EXPECT_CHROMIUM_BUILD_ID
})
// await saveLastUsedAndAvailableBrowserInfo({
// executablePath: installedBrowser.executablePath,
// browser:
// installedBrowser.browser[0].toUpperCase() +
// installedBrowser.browser.slice(1) +
// ' ' +
// EXPECT_CHROMIUM_BUILD_ID
// })
return installedBrowser
}
export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo | null> => {
const lastUsedOne = await getLastUsedAndAvailableBrowser()
if (lastUsedOne) {
return lastUsedOne
export const getAnyAvailablePuppeteerExecutable = async ({
ignoreCached = false,
noSave = false
}: {
ignoreCached?: boolean
noSave?: boolean
} = {}): Promise<BrowserInfo | null> => {
if (!ignoreCached) {
const lastUsedOne = await getLastUsedAndAvailableBrowser()
if (lastUsedOne) {
return lastUsedOne
}
}
// find existed browser - the fallback one
if (await checkCachedPuppeteerExecutable()) {
const cachedOne = await getExpectCachedPuppeteerExecutable()
!noSave && (await saveLastUsedAndAvailableBrowserInfo(cachedOne))
return cachedOne
}
// find existed browser - the one maybe actively installed by user or ship with os like Edge on windows
try {
const existedOne = await findAndLocateUserInstalledChromiumExecutableSync()
await saveLastUsedAndAvailableBrowserInfo(existedOne)
!noSave && (await saveLastUsedAndAvailableBrowserInfo(existedOne))
// save its path
return existedOne
} catch (err) {
console.error(err)
console.log('no existed browser path found')
}
// find existed browser - the fallback one
if (await checkCachedPuppeteerExecutable()) {
const cachedOne = await getExpectCachedPuppeteerExecutable()
await saveLastUsedAndAvailableBrowserInfo(cachedOne)
return cachedOne
}
// if no one available, then return null and remove last used browser
await removeLastUsedAndAvailableBrowserPath()
return null
@@ -128,35 +132,10 @@ export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo
export async function findAndLocateUserInstalledChromiumExecutableSync(): Promise<BrowserInfo> {
const exceptChromiumMainVersion = Number(EXPECT_CHROMIUM_BUILD_ID.split('.')[0])
// For windows, try to find Edge(chromium)
if (os.platform() === 'win32') {
// TODO: handle windows
const edgeExecutableLocation = path.join(
process.env['ProgramFiles(x86)']!,
'Microsoft/Edge/Application',
'msedge.exe'
)
if (fs.existsSync(edgeExecutableLocation)) {
try {
const version = await getExecutableFileVersion(edgeExecutableLocation)
const mainVersion = Number(version.split('.')[0])
if (mainVersion >= exceptChromiumMainVersion) {
return {
executablePath: edgeExecutableLocation,
browser: `Edge ${version}`
}
}
} catch (err) {
console.log(err)
}
}
}
// For other, use findChrome
let findChrome: typeof import('find-chrome-bin').findChrome
findChrome = (await import('find-chrome-bin')).findChrome
const findChrome: typeof import('find-chrome-bin').findChrome = (await import('find-chrome-bin'))
.findChrome
const targetBrowser = await findChrome({
min: exceptChromiumMainVersion
min: exceptChromiumMainVersion + 1
})
if (!targetBrowser?.executablePath) {
throw new Error('NO_EXPECT_CHROMIUM_FOUND')

View File

@@ -1,133 +0,0 @@
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import childProcess from 'node:child_process'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
import { app, dialog } from 'electron'
import fs, { WriteStream } from 'node:fs'
import { pipeWriteRegardlessError } from '../utils/pipe'
import * as JSONStream from 'JSONStream'
import { initPowerSaveBlocker } from './power-saver-blocker'
import gtag from '../../utils/gtag'
import { initDb } from '@geekgeekrun/sqlite-plugin'
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord'
import minimist from 'minimist'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
const isUiDev = process.env.NODE_ENV === 'development'
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
}
return v
})()
function runWithDaemon({ runRecordId, runMode, parentProcessPipe }) {
const subProcessOfCore = childProcess.spawn(
process.argv[0],
isUiDev
? [...process.argv.slice(1), `--run-record-id=${runRecordId}`]
: [`--run-record-id=${runRecordId}`],
{
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'],
env: {
...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: runMode
}
}
)
subProcessOfCore!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
const data = raw
switch (data.type) {
case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': {
pipeWriteRegardlessError(
parentProcessPipe as WriteStream,
JSON.stringify({
type: data.type
})
)
break
}
default: {
return
}
}
})
subProcessOfCore.once('exit', async (exitCode: number) => {
if (
[...Object.values(AUTO_CHAT_ERROR_EXIT_CODE)]
.filter((it) => typeof it === 'number')
.includes(exitCode)
) {
console.log(
`[Run core daemon] Child process exit with reason ${AUTO_CHAT_ERROR_EXIT_CODE[exitCode]}.`
)
process.exit(exitCode)
return
}
console.log(
`[Run core daemon] Child process exit with code ${exitCode}, an internal error may not be caught, and will be restarted in ${rerunInterval}ms.`
)
await sleep(rerunInterval)
runWithDaemon({ runRecordId, runMode, parentProcessPipe })
})
}
export async function runAutoChatWithDaemon() {
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
if (!['geekAutoStartWithBossMain'].includes(commandlineArgs['mode-to-daemon'])) {
await new Promise((resolve) => {
app.once('ready', () => resolve(undefined))
})
dialog.showMessageBoxSync({
type: 'error',
message: `守护进程不支持 ${commandlineArgs['mode-to-daemon'] ?? '(默认)'} 模式`
})
app.exit()
return
}
app.dock?.hide()
process.on('disconnect', () => {
app.exit()
})
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.error('pipe is not available')
app.exit(1)
}
const disposePowerSaveBlocker = initPowerSaveBlocker()
app.once('quit', disposePowerSaveBlocker)
process.on('SIGINT', () => {
process.exit()
})
const ds = await initDb(getPublicDbFilePath())
const autoStartChatRunRecord = new AutoStartChatRunRecord()
autoStartChatRunRecord.date = new Date()
const autoStartChatRunRecordRepository = ds.getRepository(AutoStartChatRunRecord)
const result = await autoStartChatRunRecordRepository.save(autoStartChatRunRecord)
runWithDaemon({
runRecordId: result.id,
runMode: commandlineArgs['mode-to-daemon'],
parentProcessPipe: pipe
})
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'AUTO_START_CHAT_DAEMON_PROCESS_STARTUP'
})
)
gtag('daemon_ready', { mode: commandlineArgs['mode-to-daemon'] ?? '' })
}
attachListenerForKillSelfOnParentExited()

View File

@@ -1,10 +0,0 @@
import { powerSaveBlocker } from 'electron'
export const initPowerSaveBlocker = (
type: 'prevent-app-suspension' | 'prevent-display-sleep' = 'prevent-app-suspension'
) => {
const id = powerSaveBlocker.start(type)
return function disposePowerSaveBlocker() {
return powerSaveBlocker.stop(id)
}
}

View File

@@ -1,23 +1,32 @@
import DingtalkPlugin from '@geekgeekrun/dingtalk-plugin/index.mjs'
import { app } from 'electron'
import { app, dialog } from 'electron'
import { SyncHook, AsyncSeriesHook } from 'tapable'
import {
readConfigFile,
getPublicDbFilePath
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import * as fs from 'fs'
import { pipeWriteRegardlessError } from '../utils/pipe'
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
// import { pipeWriteRegardlessError } from '../utils/pipe'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
import minimist from 'minimist'
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
import gtag from '../../utils/gtag'
import GtagPlugin from '../../utils/gtag/GtagPlugin'
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
// import { PeriodPushCurrentPageScreenshotPlugin } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
import { CookieInvalidHandlePlugin } from '../../features/cookie-invalid-handle-plugin'
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
const { default: SqlitePlugin } = SqlitePluginModule
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在退出')
process.exit(0)
})
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
@@ -33,14 +42,54 @@ const initPlugins = (hooks) => {
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
new GtagPlugin().apply(hooks)
// new PeriodPushCurrentPageScreenshotPlugin().apply(hooks)
new CookieInvalidHandlePlugin().apply(hooks)
}
let isParentProcessDisconnect = false
process.once('disconnect', () => {
isParentProcessDisconnect = true
})
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
const runAutoChat = async () => {
app.dock?.hide()
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
},
runRecordId
}
})
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'
},
runRecordId
}
})
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
const { initPuppeteer, mainLoop, closeBrowserWindow, autoStartChatEventBus } = await import(
'@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
)
@@ -48,77 +97,44 @@ const runAutoChat = async () => {
closeBrowserWindow()
app.exit()
})
app.dock?.hide()
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.warn('pipe is not available')
}
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'INITIALIZE_PUPPETEER'
}) + '\r\n'
)
try {
await initPuppeteer()
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
}) + '\r\n'
)
} catch (err) {
console.error(err)
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
const isPuppeteerExecutable = !!(await getAnyAvailablePuppeteerExecutable())
if (!isPuppeteerExecutable) {
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
await initPuppeteer()
const hooks = {
puppeteerLaunched: new SyncHook(),
puppeteerLaunched: new SyncHook(['browser']),
pageGotten: new SyncHook(['page']),
pageLoaded: new SyncHook(),
cookieWillSet: new SyncHook(['cookies']),
cookieWillSet: new AsyncSeriesHook(['cookies']),
userInfoResponse: new AsyncSeriesHook(['userInfo']),
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
newChatWillStartup: new AsyncSeriesHook(['positionInfoDetail']),
newChatStartup: new AsyncSeriesHook(['positionInfoDetail', 'chatRunningContext']),
jobMarkedAsNotSuit: new AsyncSeriesHook(['positionInfoDetail', 'markDetail']),
noPositionFoundForCurrentJob: new SyncHook(),
noPositionFoundAfterTraverseAllJob: new SyncHook(),
errorEncounter: new SyncHook(['errorInfo'])
errorEncounter: new SyncHook(['errorInfo']),
encounterEmptyRecommendJobList: new AsyncSeriesHook(['args']),
sageTimeEnter: new AsyncSeriesHook(['args']),
sageTimeExit: new AsyncSeriesHook(['args'])
}
initPlugins(hooks)
gtag('run_auto_chat_with_boss_main_ready')
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started
}) + '\r\n'
)
autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => {
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started
}) + '\r\n'
)
})
while (![isParentProcessDisconnect].includes(true)) {
while (true) {
try {
await mainLoop(hooks)
} catch (err) {
if (err instanceof Error) {
if (err.message.includes('LOGIN_STATUS_INVALID')) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录BOSS直聘`
})
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
break
}
@@ -130,9 +146,18 @@ const runAutoChat = async () => {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
break
}
if (err.message.includes(`Could not find Chrome`) || err.message.includes(`no executable was found`)) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
break
}
}
closeBrowserWindow?.()
console.error(err)
const shouldExit = await checkShouldExit()
if (shouldExit) {
app.exit()
return
}
console.log(
`[Run core main] An internal error is caught, and browser will be restarted in ${rerunInterval}ms.`
)
@@ -141,14 +166,32 @@ const runAutoChat = async () => {
}
}
export const waitForProcessHandShakeAndRunAutoChat = () => {
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.error('pipe is not available')
app.exit(1)
}
export const waitForProcessHandShakeAndRunAutoChat = async () => {
await app.whenReady()
app.on('window-all-closed', (e) => {
e.preventDefault()
})
initPublicIpc()
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'
},
runRecordId
}
})
runAutoChat()
}

View File

@@ -1,26 +1,31 @@
import { app } from 'electron'
import { initPuppeteer } from '@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
import extractZip from 'extract-zip'
import {
readStorageFile,
writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import {
RECOMMEND_JOB_ENTRY_SELECTOR,
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
} from '@geekgeekrun/geek-auto-start-chat-with-boss/constant.mjs'
import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storage.mjs'
import {
saveJobInfoFromRecommendPage,
saveChatStartupRecord,
saveMarkAsNotSuitRecord,
saveChatMessageRecord
saveChatMessageRecord,
saveJobHireStatusRecord
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
import { initDb } from '@geekgeekrun/sqlite-plugin'
import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { MarkAsNotSuitReason } from '@geekgeekrun/sqlite-plugin/dist/enums'
import {
MarkAsNotSuitReason,
JobSource,
JobHireStatus
} from '@geekgeekrun/sqlite-plugin/dist/enums'
import cheerio from 'cheerio'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import url from 'url'
import packageJson from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension/package.json' assert { type: 'json' }
import { Target } from 'puppeteer'
import { pipeWriteRegardlessError } from '../utils/pipe'
import * as JSONStream from 'JSONStream'
@@ -29,39 +34,13 @@ import gtag from '../../utils/gtag'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
import { type ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/src/entity/ChatMessageRecord'
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
import { messageForSaveFilter } from '../../../common/utils/chat-list'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
import {
ensureEditThisCookie,
editThisCookieExtensionPath
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension/utils.mjs'
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
const extensionDir = path.join(runtimeFolderPath, 'chrome-extensions')
if (!fs.existsSync(runtimeFolderPath)) {
fs.mkdirSync(runtimeFolderPath)
}
if (!fs.existsSync(extensionDir)) {
fs.mkdirSync(extensionDir)
}
const editThisCookieExtensionPath = path.join(extensionDir, 'EditThisCookie')
let editThisCookieZipPath
async function getEditThisCookieZipPath() {
if (editThisCookieZipPath) {
return editThisCookieZipPath
}
if (isRunFromUi) {
const { app } = await import('electron')
editThisCookieZipPath = path.join(
app.getAppPath(),
'./node_modules',
packageJson.name,
'extensions',
'EditThisCookie.zip'
)
} else {
editThisCookieZipPath = path.join(__dirname, 'extensions', 'EditThisCookie.zip')
}
return editThisCookieZipPath
}
const dbInitPromise = initDb(getPublicDbFilePath())
const attachRequestsListener = async (target: Target) => {
@@ -70,13 +49,134 @@ const attachRequestsListener = async (target: Target) => {
return
}
// FIXME: might not work
async function handleJobDetailPage({ encryptJobId } = { encryptJobId: null }) {
if (!encryptJobId) {
return
}
try {
await page.waitForFunction(
({ encryptJobId }) => {
return (
location.href.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
(!!document.querySelector('#main .job-banner') ||
!!document.documentElement.innerText?.includes(`您访问的页面不存在`))
)
},
undefined,
{ encryptJobId }
)
const htmlContent = await page.content()
if (htmlContent) {
const $ = cheerio.load(htmlContent)
const [jobBannerEl] = $('#main .job-banner') ?? []
if (!jobBannerEl) {
console.log(`access might be blocked`)
if (
htmlContent.includes(`您访问的页面不存在`) ||
location.href === `https://www.zhipin.com/`
) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.DELETED,
lastSeenDate: new Date()
})
}
} else {
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
if (jobStatusTextEl) {
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
if ([`职位已关闭`].includes(jobStatusText)) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.CLOSED,
lastSeenDate: new Date()
})
} else {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
}
}
} catch {
//
}
}
if (page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
const encryptJobId = page.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
if (encryptJobId) {
handleJobDetailPage({ encryptJobId })
}
}
async function getCurrentJobSource() {
const methodMap = {
async recommend() {
return await page.evaluate(
({ RECOMMEND_JOB_ENTRY_SELECTOR }) => {
return document.querySelector(RECOMMEND_JOB_ENTRY_SELECTOR).classList.contains('active')
},
{
RECOMMEND_JOB_ENTRY_SELECTOR
}
)
},
async expect() {
return await page.evaluate(
({ USER_SET_EXPECT_JOB_ENTRIES_SELECTOR }) => {
return [...document.querySelectorAll(USER_SET_EXPECT_JOB_ENTRIES_SELECTOR)].some((el) =>
el.classList.contains('active')
)
},
{
USER_SET_EXPECT_JOB_ENTRIES_SELECTOR
}
)
},
async search() {
const elHandle = await page.$(`.page-jobs-main`)
const currentKeyWord = await elHandle?.evaluate((el) => {
return el?.__vue__?.formData?.query
})
return !!currentKeyWord
}
}
for (const [type, func] of Object.entries(methodMap)) {
try {
if (await func()) {
return type
}
} catch (err) {
console.error('encounter error when get job source')
}
}
return null
}
page.on('response', async (response) => {
if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
if (response.url().match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)) {
const encryptJobId = response
.url()
.match(/^https:\/\/www.zhipin.com\/job_detail\/(.+)\.html/)?.[1]
if (encryptJobId) {
handleJobDetailPage({ encryptJobId })
}
} else if (response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/job/detail.json')) {
const data = await response.json()
console.log(data)
if (data.code === 0) {
await saveJobInfoFromRecommendPage(await dbInitPromise, data.zpData)
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId: data.zpData.jobInfo.encryptId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
} else if (
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/reasons.json')
@@ -90,7 +190,7 @@ const attachRequestsListener = async (target: Target) => {
}
await writeStorageFile('job-not-suit-reason-code-to-text-cache.json', reasonCodeToTextMap)
} else if (
page.url().startsWith('https://www.zhipin.com/web/geek/job-recommend') &&
page.url().startsWith('https://www.zhipin.com/web/geek/jobs') &&
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/negativefeedback/save.json')
) {
const currentJobData = await page.evaluate(
@@ -98,7 +198,7 @@ const attachRequestsListener = async (target: Target) => {
)
const requestBody = new URLSearchParams(response.request().postData())
const securityIdInRequest = requestBody.get("securityId")
const securityIdInRequest = requestBody.get('securityId')
const currentJobSecurityId = currentJobData?.securityId
if (securityIdInRequest !== currentJobSecurityId) {
@@ -112,7 +212,7 @@ const attachRequestsListener = async (target: Target) => {
const reasonCodeToTextMap = await readStorageFile(
'job-not-suit-reason-code-to-text-cache.json'
)
const jobSource = await getCurrentJobSource()
const markDetail = {
markFrom: ChatStartupFrom.ManuallyFromRecommendList,
extInfo: {
@@ -121,9 +221,15 @@ const attachRequestsListener = async (target: Target) => {
text: reasonCodeToTextMap[chosenCode]
}
},
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON
markReason: MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON,
jobSource: JobSource[jobSource]
}
gtag('job_marked_as_not_suit', { markFrom: markDetail.markFrom })
gtag('job_marked_as_not_suit', {
markFrom: markDetail.markFrom,
bossActiveTimeDesc: currentJobData?.bossInfo?.activeTimeDesc,
encryptJobId: currentJobData?.jobInfo?.encryptId,
jobSource: JobSource[jobSource]
})
if (reasonCodeToTextMap[chosenCode]?.includes('活跃度低')) {
markDetail.markReason = MarkAsNotSuitReason.BOSS_INACTIVE
markDetail.extInfo.bossActiveTimeDesc = currentJobData?.bossInfo.activeTimeDesc
@@ -137,7 +243,7 @@ const attachRequestsListener = async (target: Target) => {
markDetail
)
} else if (
page.url().startsWith('https://www.zhipin.com/web/geek/job-recommend') &&
page.url().startsWith('https://www.zhipin.com/web/geek/jobs') &&
response.url().startsWith('https://www.zhipin.com/wapi/zpgeek/friend/add.json')
) {
const request = response.request().url()
@@ -158,7 +264,12 @@ const attachRequestsListener = async (target: Target) => {
const currentUserInfo = await page.evaluate(
'document.querySelector(".job-detail-box").__vue__.$store.state.userInfo'
)
gtag('new_chat_startup', { chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList })
const jobSource = await getCurrentJobSource()
gtag('new_chat_startup', {
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
encryptJobId: currentJobData?.jobInfo?.encryptId,
jobSource: JobSource[jobSource]
})
await saveChatStartupRecord(
await dbInitPromise,
currentJobData,
@@ -166,7 +277,8 @@ const attachRequestsListener = async (target: Target) => {
encryptUserId: currentUserInfo.encryptUserId
},
{
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList
chatStartupFrom: ChatStartupFrom.ManuallyFromRecommendList,
jobSource: JobSource[jobSource]
}
)
} else if (
@@ -181,9 +293,14 @@ const attachRequestsListener = async (target: Target) => {
const url = new URL(request)
const encryptBossIdInAddFriendUrl = url.searchParams.get('bossId')
const bossInfo: any = await page.evaluate(
'document.querySelector(".chat-conversation").__vue__.bossInfo$'
)
const bossInfo =
(await page.evaluate(
'document.querySelector(".chat-conversation .chat-record")?.__vue__?.boss'
)) ?? null
if (!bossInfo) {
console.warn('cannot find boss info on page.')
return
}
const ds = await dbInitPromise
// save boss info
const bossInfoRepository = ds.getRepository(BossInfo)
@@ -206,16 +323,20 @@ const attachRequestsListener = async (target: Target) => {
const rawChatRecordList =
(
await page.evaluate(
'document.querySelector(".message-content .chat-record").__vue__.records$'
'document.querySelector(".message-content .chat-record").__vue__.list$'
)
)?.filter((msg) => ['received', 'sent'].includes(msg.style)) ?? []
)?.filter(messageForSaveFilter) ?? []
const chatRecordList = rawChatRecordList.map(it => {
const chatRecordList = rawChatRecordList.map((it) => {
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
mappedItem.mid = it.mid
mappedItem.encryptFromUserId = it.style === 'sent' ? currentUserInfo.encryptUserId : it.style === 'received' ? bossInfo.encryptBossId : ''
mappedItem.encryptToUserId = it.style === 'sent' ? bossInfo.encryptBossId: it.style === 'received' ? currentUserInfo.encryptUserId : ''
mappedItem.style = it.style
mappedItem.encryptFromUserId = it.isSelf
? currentUserInfo.encryptUserId
: bossInfo.encryptBossId
mappedItem.encryptToUserId = it.isSelf
? bossInfo.encryptBossId
: currentUserInfo.encryptUserId
mappedItem.style = it.isSelf ? 'sent' : 'received'
mappedItem.type = it.type
mappedItem.time = it.time ? new Date(it.time) : null
mappedItem.text = it.text
@@ -241,19 +362,15 @@ const attachRequestsListener = async (target: Target) => {
export async function launchBossSite() {
app.dock?.hide()
if (!fs.existsSync(path.join(editThisCookieExtensionPath, 'manifest.json'))) {
await extractZip(await getEditThisCookieZipPath(), {
dir: extensionDir
})
}
await ensureEditThisCookie()
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
const { puppeteer } = await initPuppeteer()
const browser = await puppeteer.launch({
headless: false,
args: [`--load-extension=${editThisCookieExtensionPath}`]
pipe: true,
enableExtensions: [editThisCookieExtensionPath]
})
let [page] = await browser.pages()
for (let i = 0; i < bossCookies.length; i++) {

View File

@@ -1,7 +1,12 @@
import { app } from 'electron'
import { main, loginEventBus } from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
import { app, dialog } from 'electron'
import {
main,
loginEventBus
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
import { pipeWriteRegardlessError } from './utils/pipe'
import fs from "node:fs";
import fs from 'node:fs'
import { getLastUsedAndAvailableBrowser } from './DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../features/config-with-browser-assistant'
export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
process.on('disconnect', () => app.exit())
@@ -18,15 +23,26 @@ export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
type: 'INITIALIZE_PUPPETEER'
}) + '\r\n'
)
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
app.exit(1)
}
const { initPuppeteer } = await import('@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs')
try {
await initPuppeteer()
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
}) + '\r\n'
)
} catch (err) {
console.error(err)
app.exit(1)

View File

@@ -0,0 +1,5 @@
;(async () => {
await import('@geekgeekrun/pm/daemon.js')
})()
export {}

View File

@@ -0,0 +1,145 @@
import { randomUUID } from 'node:crypto'
import { EventEmitter } from 'node:events'
import { tmpdir } from 'node:os'
import path from 'node:path'
import net from 'node:net'
import split2 from 'split2'
let daemonClient = null
export const daemonEE = new EventEmitter()
const waitForCallbackTaskMap = new Map()
export function getSocketPath(socketName) {
const ipcSocketPath =
process.platform === 'win32'
? `\\\\.\\pipe\\${socketName}`
: path.join(tmpdir(), `${socketName}.sock`)
return ipcSocketPath
}
// 连接到守护进程
export async function connectToDaemon() {
daemonClient = new net.Socket()
let isConnected = false
await new Promise((resolve, reject) => {
const ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
const ipcSocketPath = getSocketPath(ipcSocketName)
daemonClient.connect(ipcSocketPath, 'localhost', () => {
isConnected = true
console.log('已连接到守护进程')
daemonEE.emit('connect')
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON
const splitStream = split2()
daemonClient.pipe(splitStream).on('data', (line) => {
const trimmedLine = line.toString().trim()
if (!trimmedLine) {
return // 跳过空行
}
try {
const message = JSON.parse(trimmedLine)
daemonEE.emit('message', message)
// FIXME:
// console.log('收到守护进程消息:', message)
if (message._callbackUuid) {
const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid)
if (callbackInfo) {
const isError = message._isError
if (isError) {
callbackInfo.reject(message)
} else {
callbackInfo.resolve(message)
}
waitForCallbackTaskMap.delete(message._callbackUuid)
}
}
// 转发消息到渲染进程
// if (mainWindow) {
// mainWindow.webContents.send('daemon-message', message);
// }
} catch (parseError) {
console.error('解析守护进程消息失败:', parseError.message)
console.error('原始数据:', trimmedLine.substring(0, 100))
}
})
splitStream.on('error', (err) => {
console.error('split2 流处理错误:', err)
})
daemonClient.on('close', () => {
if (!isConnected) {
return
}
console.log('守护进程连接已关闭')
daemonEE.emit('close')
})
resolve(true)
// 通知渲染进程连接成功
// if (mainWindow) {
// mainWindow.webContents.send('daemon-connected');
// }
})
daemonClient.on('close', () => {
if (isConnected) {
return
}
reject(new Error('连接到守护进程超时'))
})
daemonClient.on('error', (err) => {
console.error('守护进程连接错误:', err)
// daemonEE.emit('error', err)
reject(err)
})
})
}
// 向守护进程发送消息
export function sendToDaemon(message, { needCallback = false, timeout = undefined } = {}) {
const _callbackUuid = randomUUID()
if (daemonClient && !daemonClient.destroyed) {
daemonClient.write(
JSON.stringify({
...message,
_callbackUuid
}) + '\n'
)
if (needCallback) {
let resolve, reject
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
waitForCallbackTaskMap.set(_callbackUuid, { resolve, reject })
promise.finally(() => waitForCallbackTaskMap.delete(_callbackUuid))
let timeoutTimer
if (!isNaN(parseInt(timeout))) {
timeoutTimer = setTimeout(() => {
reject(new Error(`Callback timeout after ${timeout}ms`))
}, timeout)
}
promise.finally(() => {
clearTimeout(timeoutTimer)
})
return promise
}
} else {
console.error('守护进程未连接')
}
return undefined
}
// // IPC处理从渲染进程接收消息并转发到守护进程
// ipcMain.on('send-to-daemon', (event, message) => {
// sendToDaemon(message);
// });
// // IPC处理启动工具进程
// ipcMain.on('start-worker', (event, { workerId, command, args, env }) => {
// sendToDaemon({ type: 'start-worker', workerId, command, args, env });
// });
export function closeDaemonClient() {
daemonClient?.destroy()
}

View File

@@ -4,6 +4,9 @@ import { createMainWindow } from '../../window/mainWindow'
import './app-menu'
import initIpc from './ipc'
import gtag from '../../utils/gtag'
import initPublicIpc from '../../utils/initPublicIpc'
import { sendToDaemon, closeDaemonClient } from './connect-to-daemon'
export function openSettingWindow() {
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
if (!app.requestSingleInstanceLock()) {
@@ -31,6 +34,7 @@ export function openSettingWindow() {
// IPC test
ipcMain.on('ping', () => console.log('pong'))
initPublicIpc()
initIpc()
app.on('activate', function () {
@@ -63,4 +67,25 @@ export function openSettingWindow() {
globalShortcut.unregister('Command+Option+Shift+/')
})
})
whenReadyPromise.then(async () => {
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
await sendToDaemon(
{
type: 'user-process-register'
},
{
needCallback: true
}
)
})
app.on('window-all-closed', closeDaemonClient)
app.on('before-quit', closeDaemonClient)
}

View File

@@ -1,21 +1,16 @@
import { ipcMain, shell, app } from 'electron'
import { ipcMain, shell, app, dialog, BrowserWindow } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import {
ensureConfigFileExist,
ensureStorageFileExist,
configFileNameList,
readConfigFile,
writeConfigFile,
readStorageFile,
writeStorageFile
storageFilePath
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { ChildProcess } from 'child_process'
import * as JSONStream from 'JSONStream'
import { checkCookieListFormat } from '../../../../common/utils/cookie'
import { getAnyAvailablePuppeteerExecutable } from '../../../flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../../common/enums/auto-start-chat'
import { getAnyAvailablePuppeteerExecutable } from '../../DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
import { mainWindow } from '../../../window/mainWindow'
import {
getAutoStartChatRecord,
@@ -30,29 +25,41 @@ 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'
import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow'
import {
getValidTemplate,
requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
autoReminderPromptTemplateFileName,
writeDefaultAutoRemindPrompt
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
checkIsResumeContentValid,
resumeContentEnoughDetect
} from '../../../../common/utils/resume'
import {
createReadNoReplyReminderLlmMockWindow,
readNoReplyReminderLlmMockWindow
} from '../../../window/readNoReplyReminderLlmMockWindow'
import { RequestSceneEnum } from '../../../features/llm-request-log'
import { checkUpdateForUi } from '../../../features/updater'
import gtag from '../../../utils/gtag'
import { daemonEE, sendToDaemon } from '../connect-to-daemon'
import { runCommon } from '../../../features/run-common'
import { loginWithCookieAssistant } from '../../../features/login-with-cookie-assistant'
import { configWithBrowserAssistant } from '../../../features/config-with-browser-assistant'
import {
createFirstLaunchNoticeApproveFlag,
isFirstLaunchNoticeApproveFlagExist,
waitForUserApproveAgreement
} from '../../../features/first-launch-notice-window'
import { getLastUsedAndAvailableBrowser } from '../../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { waitForCommonJobConditionDone } from '../../../features/common-job-condition'
import { ensureConfigFileExist } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
export default function initIpc() {
ipcMain.on('open-external-link', (_, link) => {
shell.openExternal(link, {
activate: true
})
})
ipcMain.handle('fetch-config-file-content', async () => {
const configFileContentList = configFileNameList.map((fileName) => {
return readConfigFile(fileName)
})
const result = {
config: {}
}
configFileNameList.forEach((fileName, index) => {
result.config[fileName] = configFileContentList[index]
})
return result
})
ipcMain.handle('save-config-file-from-ui', async (ev, payload) => {
payload = JSON.parse(payload)
ensureConfigFileExist()
@@ -69,12 +76,106 @@ export default function initIpc() {
if (hasOwn(payload, 'anyCombineRecommendJobFilter')) {
bossConfig.anyCombineRecommendJobFilter = payload.anyCombineRecommendJobFilter
}
if (hasOwn(payload, 'expectJobRegExpStr')) {
bossConfig.expectJobRegExpStr = payload.expectJobRegExpStr
delete bossConfig.expectJobRegExpStr
if (hasOwn(payload, 'expectJobNameRegExpStr')) {
bossConfig.expectJobNameRegExpStr = payload.expectJobNameRegExpStr
}
if (hasOwn(payload, 'expectJobTypeRegExpStr')) {
bossConfig.expectJobTypeRegExpStr = payload.expectJobTypeRegExpStr
}
if (hasOwn(payload, 'expectJobDescRegExpStr')) {
bossConfig.expectJobDescRegExpStr = payload.expectJobDescRegExpStr
}
if (hasOwn(payload, 'jobNotMatchStrategy')) {
bossConfig.jobNotMatchStrategy = payload.jobNotMatchStrategy
}
if (hasOwn(payload, 'markAsNotActiveSelectedTimeRange')) {
bossConfig.markAsNotActiveSelectedTimeRange = payload.markAsNotActiveSelectedTimeRange
}
if (hasOwn(payload, 'jobNotActiveStrategy')) {
bossConfig.jobNotActiveStrategy = payload.jobNotActiveStrategy
}
if (hasOwn(payload, 'autoReminder')) {
bossConfig.autoReminder = payload.autoReminder
}
// city
if (hasOwn(payload, 'expectCityList')) {
bossConfig.expectCityList = payload.expectCityList
}
if (hasOwn(payload, 'expectCityNotMatchStrategy')) {
bossConfig.expectCityNotMatchStrategy = payload.expectCityNotMatchStrategy
}
if (hasOwn(payload, 'strategyScopeOptionWhenMarkJobCityNotMatch')) {
bossConfig.strategyScopeOptionWhenMarkJobCityNotMatch =
payload.strategyScopeOptionWhenMarkJobCityNotMatch
}
// salary
if (hasOwn(payload, 'expectSalaryCalculateWay')) {
bossConfig.expectSalaryCalculateWay = payload.expectSalaryCalculateWay
}
if (hasOwn(payload, 'expectSalaryNotMatchStrategy')) {
bossConfig.expectSalaryNotMatchStrategy = payload.expectSalaryNotMatchStrategy
}
if (hasOwn(payload, 'strategyScopeOptionWhenMarkSalaryNotMatch')) {
bossConfig.strategyScopeOptionWhenMarkSalaryNotMatch =
payload.strategyScopeOptionWhenMarkSalaryNotMatch
}
if (hasOwn(payload, 'expectSalaryLow')) {
bossConfig.expectSalaryLow = payload.expectSalaryLow
}
if (hasOwn(payload, 'expectSalaryHigh')) {
bossConfig.expectSalaryHigh = payload.expectSalaryHigh
}
// work exp
if (hasOwn(payload, 'expectWorkExpList')) {
bossConfig.expectWorkExpList = payload.expectWorkExpList
}
if (hasOwn(payload, 'expectWorkExpNotMatchStrategy')) {
bossConfig.expectWorkExpNotMatchStrategy = payload.expectWorkExpNotMatchStrategy
}
if (hasOwn(payload, 'strategyScopeOptionWhenMarkJobWorkExpNotMatch')) {
bossConfig.strategyScopeOptionWhenMarkJobWorkExpNotMatch =
payload.strategyScopeOptionWhenMarkJobWorkExpNotMatch
}
if (hasOwn(payload, 'jobDetailRegExpMatchLogic')) {
bossConfig.jobDetailRegExpMatchLogic = payload.jobDetailRegExpMatchLogic
}
if (hasOwn(payload, 'isSkipEmptyConditionForCombineRecommendJobFilter')) {
bossConfig.isSkipEmptyConditionForCombineRecommendJobFilter =
payload.isSkipEmptyConditionForCombineRecommendJobFilter
}
if (hasOwn(payload, 'jobSourceList')) {
bossConfig.jobSourceList = payload.jobSourceList
}
if (hasOwn(payload, 'combineRecommendJobFilterType')) {
bossConfig.combineRecommendJobFilterType = payload.combineRecommendJobFilterType
}
if (hasOwn(payload, 'staticCombineRecommendJobFilterConditions')) {
bossConfig.staticCombineRecommendJobFilterConditions =
payload.staticCombineRecommendJobFilterConditions
}
if (hasOwn(payload, 'isSageTimeEnabled')) {
bossConfig.isSageTimeEnabled = payload.isSageTimeEnabled
}
if (hasOwn(payload, 'sageTimeOpTimes')) {
bossConfig.sageTimeOpTimes = payload.sageTimeOpTimes
}
if (hasOwn(payload, 'sageTimePauseMinute')) {
bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute
}
if (hasOwn(payload, 'blockCompanyNameRegExpStr')) {
bossConfig.blockCompanyNameRegExpStr = payload.blockCompanyNameRegExpStr
}
if (hasOwn(payload, 'blockCompanyNameRegMatchStrategy')) {
bossConfig.blockCompanyNameRegMatchStrategy = payload.blockCompanyNameRegMatchStrategy
}
if (hasOwn(payload, 'fieldsForUseCommonConfig')) {
bossConfig.fieldsForUseCommonConfig = payload.fieldsForUseCommonConfig
}
promiseArr.push(writeConfigFile('boss.json', bossConfig))
if (hasOwn(payload, 'expectCompanies')) {
@@ -86,262 +187,111 @@ export default function initIpc() {
return await Promise.all(promiseArr)
})
ipcMain.handle('read-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await readStorageFile(payload.fileName)
})
ipcMain.handle('write-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
})
// const currentExecutablePath = app.getPath('exe')
// console.log(currentExecutablePath)
ipcMain.handle('prepare-run-geek-auto-start-chat-with-boss', async () => {
mainWindow?.webContents.send('locating-puppeteer-executable')
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
}
mainWindow?.webContents.send('puppeteer-executable-is-located')
})
let subProcessOfPuppeteer: ChildProcess | null = null
ipcMain.handle('run-geek-auto-start-chat-with-boss', 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: 'geekAutoStartWithBossDaemon',
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
}
subProcessOfPuppeteer = childProcess.spawn(
process.argv[0],
[...process.argv.slice(1), '--mode-to-daemon=geekAutoStartWithBossMain'],
{
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => {
const mode = 'geekAutoStartWithBossMain'
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler(message) {
if (message.workerId !== mode) {
return
}
if (message.type === 'worker-exited') {
mainWindow?.webContents.send('worker-exited', message)
}
)
// 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 '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')
}
})
})
// TODO:
return { runRecordId }
})
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 'LOGIN_STATUS_INVALID': {
await sleep(500)
mainWindow?.webContents.send('check-boss-zhipin-cookie-file')
return
}
case 'ERR_INTERNET_DISCONNECTED': {
mainWindow?.webContents.send('toast-message', {
type: 'error',
message: '联网失败,请检查网络连接'
})
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(true)
})
// TODO:
})
ipcMain.handle('check-dependencies', async () => {
const [anyAvailablePuppeteerExecutable] = await Promise.all([
getAnyAvailablePuppeteerExecutable()
])
return {
puppeteerExecutableAvailable: !!anyAvailablePuppeteerExecutable
}
})
let subProcessOfCheckAndDownloadDependencies: ChildProcess | null = null
ipcMain.handle('setup-dependencies', async () => {
if (subProcessOfCheckAndDownloadDependencies) {
return
}
const subProcessEnv = {
...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'checkAndDownloadDependenciesForInit'
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0],
process.argv.slice(1),
{
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc']
const mode = 'readNoReplyAutoReminderMain'
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler(message) {
if (message.workerId !== mode) {
return
}
if (message.type === 'worker-exited') {
mainWindow?.webContents.send('worker-exited', message)
}
)
return new Promise((resolve, reject) => {
subProcessOfCheckAndDownloadDependencies!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'NEED_RESETUP_DEPENDENCIES':
case 'PUPPETEER_DOWNLOAD_PROGRESS': {
mainWindow?.webContents.send(data.type, data)
break
}
case 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR': {
console.error(data)
break
}
default: {
return
}
}
}
)
subProcessOfCheckAndDownloadDependencies!.once('exit', (exitCode) => {
switch (exitCode) {
case 0: {
resolve(exitCode)
break
}
default: {
reject('PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR')
break
}
}
subProcessOfCheckAndDownloadDependencies = null
})
})
return { runRecordId }
})
ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => {
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping')
subProcessOfPuppeteer?.kill()
setTimeout(() => {
try {
subProcessOfPuppeteer?.kill('SIGKILL')
} catch {
//
}
}, 1000)
})
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
}
const subProcessEnv = {
...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'launchBossZhipinLoginPageWithPreloadExtension',
PUPPETEER_EXECUTABLE_PATH: (await getAnyAvailablePuppeteerExecutable())!.executablePath
}
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
process.argv[0],
process.argv.slice(1),
{
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc']
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'BOSS_ZHIPIN_COOKIE_COLLECTED': {
mainWindow?.webContents.send(data.type, data)
break
}
default: {
return
}
const p = new Promise((resolve) => {
daemonEE.on('message', function handler(message) {
if (message.workerId !== 'geekAutoStartWithBossMain') {
return
}
if (message.type === 'worker-exited') {
daemonEE.off('message', handler)
resolve(undefined)
}
})
})
await sendToDaemon(
{
type: 'stop-worker',
workerId: 'geekAutoStartWithBossMain'
},
{
needCallback: true
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
mainWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
})
await p
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
})
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
} finally {
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
}
ipcMain.handle('stop-read-no-reply-auto-reminder', async () => {
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopping')
const p = new Promise((resolve) => {
daemonEE.on('message', function handler(message) {
if (message.workerId !== 'readNoReplyAutoReminderMain') {
return
}
if (message.type === 'worker-exited') {
daemonEE.off('message', handler)
resolve(undefined)
}
})
})
await sendToDaemon(
{
type: 'stop-worker',
workerId: 'readNoReplyAutoReminderMain'
},
{
needCallback: true
}
)
await p
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopped')
})
ipcMain.handle('get-task-manager-list', async () => {
const result = await sendToDaemon(
{
type: 'get-status'
},
{
needCallback: true
}
)
return result
})
// IPC处理停止工具进程
ipcMain.handle('stop-task', async (_, workerId) => {
await sendToDaemon(
{
type: 'stop-worker',
workerId
},
{
needCallback: true
}
)
})
ipcMain.handle('check-boss-zhipin-cookie-file', () => {
@@ -372,7 +322,7 @@ export default function initIpc() {
let subProcessOfOpenBossSiteDefer: null | PromiseWithResolvers<ChildProcess> = null
let subProcessOfOpenBossSite: null | ChildProcess = null
ipcMain.handle('open-site-with-boss-cookie', async (_, data) => {
ipcMain.handle('open-site-with-boss-cookie', async (ev, data) => {
const url = data.url
if (
!subProcessOfOpenBossSiteDefer ||
@@ -380,16 +330,45 @@ export default function initIpc() {
subProcessOfOpenBossSite.killed
) {
subProcessOfOpenBossSiteDefer = Promise.withResolvers()
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
const parent = BrowserWindow.fromWebContents(ev.sender) || undefined
await configWithBrowserAssistant({
autoFind: true,
windowOption: {
parent,
modal: !!parent,
show: true
}
})
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
} catch (error) {
//
}
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
return
}
const subProcessEnv = {
...process.env,
MAIN_BOSSGEEKGO_UI_RUN_MODE: 'launchBossSite',
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath
}
subProcessOfOpenBossSite = childProcess.spawn(process.argv[0], process.argv.slice(1), {
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe']
})
subProcessOfOpenBossSite = childProcess.spawn(
process.argv[0],
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=launchBossSite`]
: [`--mode=launchBossSite`],
{
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe']
}
)
subProcessOfOpenBossSite.once('exit', () => {
subProcessOfOpenBossSiteDefer = null
})
@@ -435,6 +414,194 @@ 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) {
await writeConfigFile('llm.json', configToSave)
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('resume-edit', async () => {
createResumeEditorWindow({
parent: mainWindow!,
modal: true,
show: true
})
const defer = Promise.withResolvers()
async function saveResumeHandler(_, resumeContent) {
await writeConfigFile('resumes.json', [
{
name: '默认简历',
updateTime: Number(new Date()),
content: resumeContent
}
])
defer.resolve()
resumeEditorWindow?.close()
}
ipcMain.handle('save-resume-content', saveResumeHandler)
resumeEditorWindow?.once('closed', () => {
ipcMain.removeHandler('save-resume-content')
defer.reject(new Error('cancel'))
})
return defer.promise
})
ipcMain.handle('fetch-resume-content', async () => {
const res = (await readConfigFile('resumes.json'))?.[0]
return res?.content ?? null
})
ipcMain.on('no-reply-reminder-prompt-edit', async () => {
const template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
if (!template) {
await writeDefaultAutoRemindPrompt()
}
const filePath = path.join(storageFilePath, autoReminderPromptTemplateFileName)
shell.openPath(filePath)
})
ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close())
ipcMain.handle('check-if-auto-remind-prompt-valid', async () => {
await getValidTemplate()
})
ipcMain.handle('check-is-resume-content-valid', async () => {
const res = (await readConfigFile('resumes.json'))?.[0]
return checkIsResumeContentValid(res)
})
ipcMain.handle('resume-content-enough-detect', async () => {
const res = (await readConfigFile('resumes.json'))?.[0]
return resumeContentEnoughDetect(res)
})
ipcMain.handle('overwrite-auto-remind-prompt-with-default', async () => {
await writeDefaultAutoRemindPrompt()
})
ipcMain.handle('check-if-llm-config-list-valid', async () => {
const llmConfigList = await readConfigFile('llm.json')
if (!Array.isArray(llmConfigList) || !llmConfigList?.length) {
throw new Error('CANNOT_FIND_VALID_CONFIG')
}
if (llmConfigList.some((it) => !/^http(s)?:\/\//.test(it.providerCompleteApiUrl))) {
throw new Error('CANNOT_FIND_VALID_CONFIG')
}
if (llmConfigList.length > 1) {
const firstEnabledModel = llmConfigList.find((it) => it.enabled)
if (!firstEnabledModel) {
throw new Error('CANNOT_FIND_VALID_CONFIG')
}
}
})
ipcMain.on('test-llm-config-effect', (_, { autoReminderConfig } = {}) => {
createReadNoReplyReminderLlmMockWindow(
{
parent: mainWindow!,
modal: true,
show: true
},
{
autoReminderConfig
}
)
async function requestLlm(_, requestPayload) {
return await requestNewMessageContent(requestPayload.messageList, {
requestScene: RequestSceneEnum.testing,
llmConfigIdForPick: requestPayload.llmConfigIdForPick ?? null
})
}
ipcMain.handle('request-llm-for-test', requestLlm)
readNoReplyReminderLlmMockWindow?.once('closed', () => {
ipcMain.removeHandler('request-llm-for-test')
})
async function getLlmConfigList() {
return await readConfigFile('llm.json')
}
ipcMain.handle('get-llm-config-for-test', getLlmConfigList)
readNoReplyReminderLlmMockWindow?.once('closed', () => {
ipcMain.removeHandler('get-llm-config-for-test')
})
})
ipcMain.on('close-read-no-reply-reminder-llm-mock-window', () => {
readNoReplyReminderLlmMockWindow?.close()
gtag('mock_chat_window_closed')
})
ipcMain.handle('check-update', async () => {
const newRelease = await checkUpdateForUi()
return newRelease
})
ipcMain.handle('login-with-cookie-assistant', async () => {
return await loginWithCookieAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
})
ipcMain.handle('config-with-browser-assistant', async () => {
return await configWithBrowserAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
})
ipcMain.handle('pre-enter-setting-ui', async () => {
if (!isFirstLaunchNoticeApproveFlagExist()) {
try {
await waitForUserApproveAgreement({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
createFirstLaunchNoticeApproveFlag()
} catch {
app.exit(0)
return
}
}
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
const lastBrowser = await getLastUsedAndAvailableBrowser()
if (!lastBrowser) {
try {
await configWithBrowserAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
},
autoFind: true
})
} catch (err) {
void err
}
}
}
})
ipcMain.handle('common-job-condition-config', async () => {
await waitForCommonJobConditionDone()
mainWindow?.webContents.send('common-job-condition-config-updated', {
config: await readConfigFile('common-job-condition-config.json')
})
})
ipcMain.handle('exit-app-immediately', () => {
app.exit(0)
})

View File

@@ -0,0 +1,92 @@
import { spawn } from 'child_process'
import {
ensureStorageFileExist,
writeStorageFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { randomUUID } from 'node:crypto'
import { connectToDaemon } from './connect-to-daemon'
export async function ensureIpcPipeName({ isReset } = {}) {
if (isReset) {
await writeStorageFile('ipc-pipe-name', '', { isJson: false })
}
let ipcPipeName = readStorageFile('ipc-pipe-name', { isJson: false })
if (!ipcPipeName) {
ipcPipeName = `geekgeekrun-d_${randomUUID()}`
ensureStorageFileExist()
await writeStorageFile('ipc-pipe-name', ipcPipeName, { isJson: false })
}
process.env.GEEKGEEKRUND_PIPE_NAME = ipcPipeName
return ipcPipeName
}
export async function launchDaemon() {
let daemonProcess
async function startDaemon() {
console.log('启动守护进程...')
// 添加参数使守护进程在后台运行,不显示 UI
daemonProcess = spawn(
process.argv[0],
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=launchDaemon`]
: [`--mode=launchDaemon`],
{
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
detached: true,
env: {
...process.env
}
}
)
daemonProcess.stdout.on('data', (data) => {
console.log(`守护进程输出: ${data}`)
})
daemonProcess.stderr.on('data', (data) => {
console.error(`守护进程错误: ${data}`)
})
return new Promise((resolve, reject) => {
daemonProcess.stdio[3].on('data', (rawData) => {
let data
try {
data = JSON.parse(rawData.toString())
if (data.type === 'DAEMON_READY') {
resolve(true)
} else if (data.type === 'DAEMON_FATAL') {
reject(new Error(data.error))
}
} catch (err) {
console.error('', err)
}
})
})
}
await ensureIpcPipeName()
try {
await connectToDaemon()
} catch (err) {
let isDaemonLaunched = false
console.log('cannot connect to daemon, try to launch it', err)
// 启动守护进程
try {
await startDaemon()
isDaemonLaunched = true
} catch (err) {
console.log('cannot launch to daemon, try to change port', err)
daemonProcess?.kill('SIGKILL')
await ensureIpcPipeName({ isReset: true })
try {
await startDaemon()
isDaemonLaunched = true
} catch (err) {
console.log('cannot launch to daemon, try to change port failed', err)
}
}
if (isDaemonLaunched) {
await connectToDaemon()
}
}
}

View File

@@ -20,7 +20,7 @@ export const initDbWorker = () => {
// attach more event
worker?.off('message', handler)
} else if (data.type === 'DB_INIT_FAIL') {
reject(undefined)
reject(data.error)
worker?.terminate()
worker?.off('message', handler)
worker = null
@@ -102,3 +102,10 @@ export const getJobHistoryByEncryptId = async (encryptJobId) => {
})
return res
}
export const saveAndGetCurrentRunRecord = async () => {
const res = await createWorkerPromise({
type: 'saveAndGetCurrentRunRecord'
})
return res
}

View File

@@ -11,6 +11,7 @@ import { VMarkAsNotSuitLog } from '@geekgeekrun/sqlite-plugin/dist/entity/VMarkA
import { measureExecutionTime } from '../../../../../../common/utils/performance'
import { PageReq, PagedRes } from '../../../../../../common/types/pagination'
import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/dist/entity/JobInfoChangeLog'
import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord'
const dbInitPromise = initDb(getPublicDbFilePath())
let dataSource: DataSource | null = null
@@ -47,7 +48,10 @@ const payloadHandler = {
const [data, totalItemCount] = await measureExecutionTime(
userRepository.findAndCount({
skip: (pageNo - 1) * pageSize,
take: pageSize
take: pageSize,
order: {
date: 'DESC'
}
})
)
return {
@@ -69,7 +73,10 @@ const payloadHandler = {
const [data, totalItemCount] = await measureExecutionTime(
recordRepository.findAndCount({
skip: (pageNo - 1) * pageSize,
take: pageSize
take: pageSize,
order: {
date: 'DESC'
}
})
)
return {
@@ -155,6 +162,13 @@ const payloadHandler = {
})
)
return data
},
async saveAndGetCurrentRunRecord() {
const autoStartChatRunRecord = new AutoStartChatRunRecord()
autoStartChatRunRecord.date = new Date()
const autoStartChatRunRecordRepository = dataSource!.getRepository(AutoStartChatRunRecord)
const result = await autoStartChatRunRecordRepository.save(autoStartChatRunRecord)
return result
}
}

View File

@@ -1,17 +0,0 @@
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()
}

View File

@@ -1,374 +0,0 @@
import { bootstrap, launchBoss } from './bootstrap'
import { MsgStatus, type ChatListItem } from './types'
import { Browser, Page } from 'puppeteer'
import { sendLookForwardReplyEmotion } from './boss-operation'
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
import { app } from 'electron'
import { initDb } from '@geekgeekrun/sqlite-plugin'
import {
getPublicDbFilePath,
readConfigFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatMessageRecord'
import { saveChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers'
import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import * as fs from 'fs'
import { pipeWriteRegardlessError } from '../utils/pipe'
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
const throttleIntervalMinutes =
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
const dbInitPromise = initDb(getPublicDbFilePath())
export const pageMapByName: {
boss?: Page | null
} = {}
async function saveCurrentChatRecord(page) {
const userInfo = await page.evaluate(
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
)
const bossInfo = await page.evaluate(
'document.querySelector(".chat-conversation").__vue__.bossInfo$'
)
const ds = await dbInitPromise
// save boss info
const bossInfoRepository = ds.getRepository(BossInfo)
let targetBossInfo = await bossInfoRepository.findOneBy({
encryptBossId: bossInfo.encryptBossId
})
if (!targetBossInfo) {
targetBossInfo = new BossInfo()
Object.assign(targetBossInfo, {
encryptBossId: bossInfo.encryptBossId,
name: bossInfo.name,
title: bossInfo.title,
date: new Date()
})
await bossInfoRepository.save(targetBossInfo)
}
const rawChatRecordList =
(
await page.evaluate(
'document.querySelector(".message-content .chat-record").__vue__.records$'
)
)?.filter((msg) => ['received', 'sent'].includes(msg.style)) ?? []
const chatRecordList = rawChatRecordList.map((it) => {
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
mappedItem.mid = it.mid
mappedItem.encryptFromUserId =
it.style === 'sent'
? userInfo.encryptUserId
: it.style === 'received'
? bossInfo.encryptBossId
: ''
mappedItem.encryptToUserId =
it.style === 'sent'
? bossInfo.encryptBossId
: it.style === 'received'
? userInfo.encryptUserId
: ''
mappedItem.style = it.style
mappedItem.type = it.type
mappedItem.time = it.time ? new Date(it.time) : null
mappedItem.text = it.text
if (it.type === 'image') {
mappedItem.imageUrl = it.image?.originImage?.url
mappedItem.imageHeight = it.image?.originImage?.url?.height
mappedItem.imageWidth = it.image?.originImage?.url?.width
}
return mappedItem
})
await saveChatMessageRecord(ds, chatRecordList)
}
let browser: null | Browser = null
const mainLoop = async () => {
if (browser) {
try {
const cp = browser.process()
cp?.kill('SIGKILL')
} catch {
//
} finally {
browser = null
}
}
const canNotConfirmIfHasReadMsgTemplateList = [
'Boss还没查看你的消息',
'你与该职位竞争者PK情况',
'简历诊断提醒',
'附件简历还没准备好',
'开场问题,期待你的回答',
'设置合适的期望薪资范围'
].map((it) => new RegExp(it))
browser = await bootstrap()
await Promise.all([launchBoss(browser)])
await sleep(1000)
pageMapByName.boss!.bringToFront()
await sleep(2000)
const currentPageUrl = pageMapByName.boss!.url() ?? ''
// #region
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
writeStorageFile('boss-cookies.json', [])
throw new Error('LOGIN_STATUS_INVALID')
}
if (
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/error.html')
) {
throw new Error('ACCESS_IS_DENIED')
}
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
const validateRes: any = await pageMapByName
.boss!.waitForResponse(
(response) => {
if (
response.url().startsWith('https://www.zhipin.com/wapi/zpAntispam/v2/geetest/validate')
) {
return true
}
return false
},
{
timeout: 0
}
)
.then((res) => {
return res.json()
})
if (validateRes.code === 0) {
await storeStorage(pageMapByName.boss)
throw new Error('CAPTCHA_PASSED_AND_NEED_RESTART')
}
}
// #endregion
// 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<ChatListItem>
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
return (
index >= cursorToContinueFind &&
(rechatLimitDay && it.updateTime
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
: true) &&
((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 >=
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
) {
await sleepWithRandomDelay(3250)
await sendLookForwardReplyEmotion(pageMapByName.boss!)
} else {
cursorToContinueFind += 1
}
await sleep(1000)
await saveCurrentChatRecord(pageMapByName.boss!)
await sleep(3000)
}
}
let isParentProcessDisconnect = false
process.once('disconnect', () => {
isParentProcessDisconnect = true
})
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
}
return v
})()
let pipe
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.warn('pipe is not available')
}
export async function runEntry() {
process.on('disconnect', () => {
app.exit()
})
app.dock?.hide()
while (!isParentProcessDisconnect) {
try {
await mainLoop()
} catch (err) {
console.error(err)
try {
await pageMapByName['boss']?.close()
} catch {
//
}
// handle error
if (
err instanceof Error &&
['LOGIN_STATUS_INVALID', 'ACCESS_IS_DENIED', 'ERR_INTERNET_DISCONNECTED'].includes(
err.message
)
) {
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: err.message
}) + '\r\n'
)
process.exit(1)
}
} finally {
pageMapByName['boss'] = null
await sleep(rerunInterval)
}
}
process.exit(0)
}
attachListenerForKillSelfOnParentExited()
process.once('uncaughtException', (error) => {
console.error('uncaughtException', error)
process.exit(1)
})
process.once('unhandledRejection', (error) => {
console.log('unhandledRejection', error)
process.exit(1)
})
async function storeStorage(page) {
const [cookies, localStorage] = await Promise.all([
page.cookies(),
page
.evaluate(() => {
return JSON.stringify(window.localStorage)
})
.then((res) => JSON.parse(res))
])
return Promise.all([
writeStorageFile('boss-cookies.json', cookies),
writeStorageFile('boss-local-storage.json', localStorage)
])
}

View File

@@ -7,8 +7,6 @@ import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storag
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')
export async function bootstrap() {
const { puppeteer } = await initPuppeteer()
@@ -28,13 +26,18 @@ export async function bootstrap() {
export async function launchBoss(browser: Browser) {
const page = (await browser.pages())[0]
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
//set cookies
for (let i = 0; i < bossCookies.length; i++) {
await page.setCookie(bossCookies[i])
}
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)
try {
await Promise.all([page.goto(bossChatUiUrl, { timeout: 0 }), page.waitForNavigation()])
await Promise.all([
page.goto(bossChatUiUrl, { timeout: 0 }),
page.waitForNavigation({ timeout: 120 * 1000 })
])
} catch (error) {
if (error?.message?.startsWith('net::ERR_INTERNET_DISCONNECTED')) {
throw new Error('ERR_INTERNET_DISCONNECTED')

View File

@@ -0,0 +1,266 @@
import { Page } from 'puppeteer'
import { sleepWithRandomDelay, sleep } from '@geekgeekrun/utils/sleep.mjs'
import { completes } from '@geekgeekrun/utils/gpt-request.mjs'
import { recordGptCompletionRequest, RequestSceneEnum } from '../../features/llm-request-log'
import {
readConfigFile,
readStorageFile,
writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { formatResumeJsonToMarkdown } from '../../../common/utils/resume'
import { SINGLE_ITEM_DEFAULT_SERVE_WEIGHT } from '../../../common/constant'
import { LlmModelUsageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/LlmModelUsageRecord'
import gtag from '../../utils/gtag'
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()
}
const pickLlmConfigFromList = (llmConfigList, blockModelSet) => {
if (llmConfigList.length === 1) {
llmConfigList[0].enabled = true
llmConfigList[0].serveWeight = SINGLE_ITEM_DEFAULT_SERVE_WEIGHT
}
llmConfigList = llmConfigList.filter((it) => it.enabled && !blockModelSet.has(it.id))
if (!llmConfigList.length) {
return null
}
llmConfigList.forEach((conf) => {
if (!Number(conf.serveWeight) || conf.serveWeight < 1) {
conf.serveWeight = 1
}
if (conf.serveWeight > 100) {
conf.serveWeight = 100
}
})
const pool: number[] = []
for (let i = 0; i < llmConfigList.length; i++) {
for (let j = 0; j < Math.floor(llmConfigList[i].serveWeight); j++) {
pool.push(llmConfigList[i].id)
}
}
if (!pool.length) {
return null
}
const index = Math.floor(pool.length * Math.random())
return llmConfigList.find((it) => it.id === pool[index]) ?? null
}
// let _index = 0
const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__`
const defaultPrompt = `**核心指令:**
你是一个智能求职助手需要根据用户简历生成30字左右的提醒消息满足以下要求
1. 每次生成需满足:
- √ 包含1个核心技能 + 1个成果量化
- √ 使用不同句式模板至少准备5种
- √ 谦虚一些,头衔、工作年限等在历史记录信息中出现一次就好
- ✗ 严禁与最近发送的几条相似或雷同
- ✗ 严禁出现简历之外的词语
- ✗ 严禁包含最近8条已经发过的内容包括但不限于职位名称
**简历分析层:**
请从以下简历内容中提取关键要素:\n\`\`\`markdown\n${RESUME_PLACEHOLDER}\n\`\`\`\n
---
要求提取:
1. 硬技能:编程语言/技术栈/工具证书等至少提取5项
2. 项目经历与成果业绩、带量化数据的结果至少3条
3. 软技能:沟通/管理等至少2项
4. 特殊成就:奖项/专利等(可选)
**消息生成层:**
根据上述要素随机组合生成消息
**质量控制层:**
每次生成前执行:
1. 检查历史记录
2. 确保技能/成果组合未重复
3. 确保所生成的新消息不包含最近8条已经发过的内容包括但不限于职位名称
4. 字数严格控制在10-40字
5. 避免感叹号等激进符号
6. 减少头衔“资深”、“高级”出现的频率严禁出现“专家”、“老兵”减少工作年限“x年”出现的频率
**输出格式:**
请确保仅回复一句话以JSON响应不要包含其他解释或内容数据结构参考\`{"response": "这里是将会发送给招聘者的内容"}\``
export const autoReminderPromptTemplateFileName = 'auto-reminder-resume-system-message-template.md'
export const getValidTemplate = async () => {
let template = await readStorageFile(autoReminderPromptTemplateFileName, { isJson: false })
if (!template) {
await writeDefaultAutoRemindPrompt()
template = defaultPrompt
}
if (!template.includes(RESUME_PLACEHOLDER)) {
const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`)
e.name = `RESUME_PLACEHOLDER_NOT_EXIST`
throw e
}
return template
}
export const writeDefaultAutoRemindPrompt = async () => {
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false })
}
export const requestNewMessageContent = async (
chatRecords,
{
requestScene,
llmConfigIdForPick
}: { requestScene?: RequestSceneEnum; llmConfigIdForPick?: string[] } = {}
) => {
const template = await getValidTemplate()
const resumeObject = (await readConfigFile('resumes.json'))?.[0]
const resumeContent = formatResumeJsonToMarkdown(resumeObject)
const chatList = [
{
role: 'system',
content: template.replace(RESUME_PLACEHOLDER, resumeContent)
}
]
chatList.push({
role: 'user',
content:
'请根据我的简历帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容但需要表达出应聘意向。请确保仅响应一句话以JSON响应数据结构参考`{"response": "这里是将会发送给招聘者的内容"}`'
})
// chatRecords = chatRecords.slice(chatRecords.length - _index)
// debugger
for (const record of chatRecords) {
const assistantJsonContent = JSON.stringify({
response: record.text
})
chatList.push({
role: 'assistant',
content: `\`\`\`json\n${assistantJsonContent}\n\`\`\``
})
chatList.push({
role: 'user',
content:
'围绕我简历中关于自我介绍、技术栈、工作经历、项目描述、项目业绩等内容写一句自我介绍。开头不必包含“您好”、结尾不必包含“期待回复”务必确保本次所回复的内容不能与之前所回复的内容雷同或相似。请确保仅回复一句话以JSON响应不要包含其他解释或内容数据结构参考`{"response": "这里是将会发送给招聘者的内容"}`'
})
}
console.log(chatList)
let res, llmConfig
const llmRequestRecord: Omit<LlmModelUsageRecord, 'id' | 'providerApiSecretMd5'> & {
providerApiSecret: string
} = {}
const blockModelSet = new Set()
while (!res) {
let llmConfigList = await readConfigFile('llm.json')
if (llmConfigIdForPick?.length) {
llmConfigList = llmConfigList.filter((it) => {
return llmConfigIdForPick.includes(it.id)
})
}
llmConfig = pickLlmConfigFromList(llmConfigList, blockModelSet)
if (!llmConfig) {
throw new Error(`CANNOT_FIND_A_USABLE_MODEL`)
}
console.log(llmConfig.providerCompleteApiUrl)
Object.assign(llmRequestRecord, {
providerCompleteApiUrl: llmConfig.providerCompleteApiUrl,
model: llmConfig.model,
providerApiSecret: llmConfig.providerApiSecret,
requestStartTime: new Date(),
hasError: false,
errorMessage: '',
requestScene
})
try {
const completion = await completes(
{
baseURL: llmConfig.providerCompleteApiUrl,
apiKey: llmConfig.providerApiSecret,
model: llmConfig.model
},
chatList
)
res = completion?.choices?.[0] ?? null
Object.assign(llmRequestRecord, {
completionTokens: completion.usage?.completion_tokens ?? null,
promptCacheHitTokens: completion.usage?.prompt_cache_hit_tokens ?? null,
promptCacheMissTokens: completion.usage?.prompt_cache_miss_tokens ?? null,
promptTokens: completion.usage?.prompt_tokens ?? null,
totalTokens: completion.usage?.total_tokens ?? null
} as LlmModelUsageRecord)
} catch (err) {
console.log('request failed', err)
blockModelSet.add(llmConfig.id)
Object.assign(llmRequestRecord, {
hasError: true,
errorMessage: err?.message ?? ''
})
} finally {
llmRequestRecord.requestEndTime = new Date()
try {
await recordGptCompletionRequest(llmRequestRecord)
} catch (err) {
console.log('CANNOT_SAVE_LLM_COMPLETION_LOG', err)
}
}
}
console.log(res)
// _index++
let textToSend
try {
const rawMarkdownText = res?.message?.content
try {
textToSend = JSON.parse(
rawMarkdownText.replace(/^```json/m, '').replace(/```$/m, '')
)?.response
} catch (err) {
gtag('encounter_error_when_parse_llm_text', {
err,
model: llmConfig?.model,
providerCompleteApiUrl: llmConfig?.providerCompleteApiUrl
})
throw err
}
textToSend = textToSend?.replace(/。$/, '')
if (!textToSend) {
gtag('llm_respond_text_is_empty', {
model: llmConfig?.model,
providerCompleteApiUrl: llmConfig?.providerCompleteApiUrl
})
throw new Error(`empty content. ${err?.message} ${res?.message?.content}`)
}
} catch (err) {
throw new Error(`fail to parse response. ${err?.message} ${res?.message?.content}`)
}
return {
responseText: textToSend,
usedLlmConfig: llmConfig,
recordInfo: llmRequestRecord
}
}
export async function sendGptContent(page: Page, chatRecords) {
const textToSend = (
await requestNewMessageContent(chatRecords, {
requestScene: RequestSceneEnum.readNoReplyAutoReminder
})
).responseText
const chatInputSelector = `.chat-conversation .message-controls .chat-input`
const chatInputHandle = await page.$(chatInputSelector)
await chatInputHandle.click()
await sleep(500)
await chatInputHandle.click()
await chatInputHandle.type(textToSend, {
delay: 50
})
await sleep(1000)
const sendButtonSelector = `.chat-conversation .message-controls .chat-op .btn-send:not(.disabled)`
await page.click(sendButtonSelector)
}

View File

@@ -0,0 +1,780 @@
import { bootstrap, launchBoss } from './bootstrap'
import { MsgStatus, type ChatListItem } from './types'
import { Browser, Page } from 'puppeteer'
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
import { app, dialog } from 'electron'
import { initDb } from '@geekgeekrun/sqlite-plugin'
import {
getPublicDbFilePath,
readConfigFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { ChatMessageRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatMessageRecord'
import {
saveChatMessageRecord,
getJobHireStatusRecord,
saveJobHireStatusRecord
} from '@geekgeekrun/sqlite-plugin/dist/handlers'
import {
writeStorageFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
import { messageForSaveFilter } from '../../../common/utils/chat-list'
import {
AUTO_CHAT_ERROR_EXIT_CODE,
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK
} from '../../../common/enums/auto-start-chat'
import gtag from '../../utils/gtag'
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums'
import dayjs from 'dayjs'
import cheerio from 'cheerio'
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
// import { pushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
import minimist from 'minimist'
import { checkCookieListFormat } from '../../../common/utils/cookie'
import { loginWithCookieAssistant } from '../../features/login-with-cookie-assistant'
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在退出')
process.exit(0)
})
const throttleIntervalMinutes =
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
const rechatLimitDay = readConfigFile('boss.json').autoReminder?.rechatLimitDay ?? 21
const recentMessageQuantityForLlm =
readConfigFile('boss.json').autoReminder?.recentMessageQuantityForLlm ?? 8
const rechatContentSource =
readConfigFile('boss.json').autoReminder?.rechatContentSource ??
RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION
const rechatLlmFallback =
readConfigFile('boss.json').autoReminder?.rechatLlmFallback ??
RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
const fieldsForUseCommonConfig = readConfigFile('boss.json').fieldsForUseCommonConfig ?? {}
const commonJobConditionConfig = readConfigFile('common-job-condition-config.json') ?? {}
const expectJobTypeRegExpStr =
(!fieldsForUseCommonConfig.jobDetail ? readConfigFile('boss.json') : commonJobConditionConfig)
?.expectJobTypeRegExpStr ?? ''
const onlyRemindBossWithExpectJobType =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ??
!!expectJobTypeRegExpStr
const blockCompanyNameRegExpStr =
(!fieldsForUseCommonConfig.blockCompanyNameRegExpStr
? readConfigFile('boss.json')
: commonJobConditionConfig
)?.blockCompanyNameRegExpStr ?? ''
const blockCompanyNameRegExp = (() => {
if (!blockCompanyNameRegExpStr?.trim()) {
return null
}
try {
return new RegExp(blockCompanyNameRegExpStr, 'im')
} catch {
return null
}
})()
const onlyRemindBossWithoutBlockCompanyName =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithoutBlockCompanyName ??
!!blockCompanyNameRegExp
const dbInitPromise = initDb(getPublicDbFilePath())
export const pageMapByName: {
boss?: Page | null
} = {}
// async function periodPushCurrentPageScreenshot () {
// try {
// if (pageMapByName.boss?.isClosed()) {
// return
// }
// const shouldExit = await checkShouldExit()
// if (shouldExit) {
// return
// }
// try {
// await pushCurrentPageScreenshot(pageMapByName.boss)
// }
// catch (err) {
// if (err?.message?.includes(`PAGE_CLOSED`)) {
// return
// }
// }
// setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
// }
// catch {}
// }
// periodPushCurrentPageScreenshot()
async function saveCurrentChatRecord(page) {
const userInfo = await page.evaluate(
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
)
const bossInfo = await page.evaluate(
'document.querySelector(".chat-conversation .chat-record")?.__vue__?.boss'
)
const ds = await dbInitPromise
// save boss info
const bossInfoRepository = ds.getRepository(BossInfo)
let targetBossInfo = await bossInfoRepository.findOneBy({
encryptBossId: bossInfo.encryptBossId
})
if (!targetBossInfo) {
targetBossInfo = new BossInfo()
Object.assign(targetBossInfo, {
encryptBossId: bossInfo.encryptBossId,
name: bossInfo.name,
title: bossInfo.title,
date: new Date()
})
await bossInfoRepository.save(targetBossInfo)
}
const rawChatRecordList =
(
await page.evaluate(
'document.querySelector(".message-content .chat-record").__vue__.records$'
)
)?.filter((msg) => ['received', 'sent'].includes(msg.style)) ?? []
const chatRecordList = rawChatRecordList.map((it) => {
const mappedItem = {} as InstanceType<typeof ChatMessageRecord>
mappedItem.mid = it.mid
mappedItem.encryptFromUserId =
it.style === 'sent'
? userInfo.encryptUserId
: it.style === 'received'
? bossInfo.encryptBossId
: ''
mappedItem.encryptToUserId =
it.style === 'sent'
? bossInfo.encryptBossId
: it.style === 'received'
? userInfo.encryptUserId
: ''
mappedItem.style = it.style
mappedItem.type = it.type
mappedItem.time = it.time ? new Date(it.time) : null
mappedItem.text = it.text
if (it.type === 'image') {
mappedItem.imageUrl = it.image?.originImage?.url
mappedItem.imageHeight = it.image?.originImage?.url?.height
mappedItem.imageWidth = it.image?.originImage?.url?.width
}
return mappedItem
})
await saveChatMessageRecord(ds, chatRecordList)
}
async function checkJobIsClosed() {
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$
.encryptJobId
})
if (!encryptJobId) {
return false
}
let isJobClosed = false
let record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
// not seen before, or last seen more than 6 hours ago
// fetch new status
if (
!record ||
(record.hireStatus === JobHireStatus.HIRING &&
Date.now() - Number(dayjs(record.lastSeenDate)) > 6 * 60 * 60 * 1000)
) {
const positionNameElHandle = await pageMapByName.boss!.$(
`#main .chat-conversation [ka="geek_chat_job_detail"] .right-content`
)
if (!positionNameElHandle) {
return false
}
positionNameElHandle.click()
try {
const targetPage = await waitForPage(
pageMapByName.boss!.browser(),
async (page) => {
const url = page.url()
if (
url.startsWith(`https://www.zhipin.com/job_detail/${encryptJobId}`) &&
(await page.evaluate(
() =>
!!document.querySelector('#main .job-banner') ||
!!document.documentElement.innerText?.includes(`您访问的页面不存在`)
))
) {
return true
}
return false
},
{ timeout: 15 * 1000 }
)
const htmlContent = await targetPage.content()
if (htmlContent) {
const $ = cheerio.load(htmlContent)
const [jobBannerEl] = $('#main .job-banner') ?? []
if (!jobBannerEl) {
console.log(`access might be blocked`)
if (
htmlContent.includes(`您访问的页面不存在`) ||
location.href === `https://www.zhipin.com/`
) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.DELETED,
lastSeenDate: new Date()
})
}
} else {
const [jobStatusTextEl] = $('#main .job-banner .job-status') ?? []
if (jobStatusTextEl) {
const jobStatusText = $(jobStatusTextEl).text()?.trim() ?? ''
if ([`职位已关闭`].includes(jobStatusText)) {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.CLOSED,
lastSeenDate: new Date()
})
} else {
await saveJobHireStatusRecord(await dbInitPromise, {
encryptJobId,
hireStatus: JobHireStatus.HIRING,
lastSeenDate: new Date()
})
}
}
}
}
targetPage.close().catch(() => void 0)
} catch (err) {
console.log(`checkJobIsClosed error: ${err}`)
}
record = await getJobHireStatusRecord(await dbInitPromise, encryptJobId)
}
if (record.hireStatus !== JobHireStatus.HIRING) {
isJobClosed = true
}
return isJobClosed
}
let browser: null | Browser = null
const mainLoop = async () => {
if (browser) {
try {
const cp = browser.process()
cp?.kill('SIGKILL')
} catch {
//
} finally {
browser = null
}
}
let bossCookies = readStorageFile('boss-cookies.json')
let cookieCheckResult = checkCookieListFormat(bossCookies)
while (!cookieCheckResult) {
try {
await loginWithCookieAssistant()
bossCookies = readStorageFile('boss-cookies.json')
cookieCheckResult = checkCookieListFormat(bossCookies)
} catch (err) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录BOSS直聘`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
},
runRecordId
}
})
const canNotConfirmIfHasReadMsgTemplateList = [
'Boss还没查看你的消息',
'你与该职位竞争者PK情况',
'简历诊断提醒',
'附件简历还没准备好',
'设置合适的期望薪资范围'
].map((it) => new RegExp(it))
browser = await bootstrap()
await Promise.all([launchBoss(browser)])
await sleep(1000)
pageMapByName.boss!.bringToFront()
await sleep(2000)
const currentPageUrl = pageMapByName.boss!.url() ?? ''
// #region
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
writeStorageFile('boss-cookies.json', [])
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
} catch (err) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录BOSS直聘`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
throw new Error('THROW_FOR_RETRY')
}
if (
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/error.html')
) {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('ACCESS_IS_DENIED')
}
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
const validateRes: any = await pageMapByName
.boss!.waitForResponse(
(response) => {
if (
response.url().startsWith('https://www.zhipin.com/wapi/zpAntispam/v2/geetest/validate')
) {
return true
}
return false
},
{
timeout: 0
}
)
.then((res) => {
return res.json()
})
if (validateRes.code === 0) {
await storeStorage(pageMapByName.boss)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('CAPTCHA_PASSED_AND_NEED_RESTART')
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
},
runRecordId
}
})
// #endregion
// 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) {
await pageMapByName.boss?.waitForFunction(() => {
return Array.isArray(document.querySelector('.main-wrap .chat-user')?.__vue__?.list)
})
// 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<ChatListItem>
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
return (
index >= cursorToContinueFind &&
(onlyRemindBossWithoutBlockCompanyName && blockCompanyNameRegExp
? !blockCompanyNameRegExp.test(it.brandName)
: true) &&
(rechatLimitDay && it.updateTime
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
: true) &&
((((it.lastIsSelf &&
it.lastMsgStatus === MsgStatus.HAS_READ &&
!it.lastText.includes('你撤回了')) ||
canNotConfirmIfHasReadMsgTemplateList.some((regExp) => regExp.test(it.lastText))) &&
!it.unreadCount) ||
(!it.lastIsSelf && it.lastText === '开场问题,期待你的回答'))
)
})
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)
// check if expect job type match
let isExpectJobTypeMatch = true
if (onlyRemindBossWithExpectJobType) {
const selectedFriendInfo = await pageMapByName.boss?.evaluate(
`document.querySelector('.chat-conversation')?.__vue__?.selectedFriend$`
)
if (!selectedFriendInfo) {
isExpectJobTypeMatch = false
} else {
const jobType = selectedFriendInfo?.positionName
if (!jobType) {
isExpectJobTypeMatch = false
} else {
const regExp = new RegExp(expectJobTypeRegExpStr)
isExpectJobTypeMatch = regExp.test(jobType)
}
}
}
const conversationInfo = await pageMapByName.boss?.evaluate(
`document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$`
)
const historyMessageList =
(
await pageMapByName.boss?.evaluate(() => {
return document.querySelector('.message-content .chat-record')?.__vue__?.list$ ?? []
})
)?.filter(messageForSaveFilter) ?? []
const lastGeekMessageSendTime = historyMessageList.findLast((it) => it.isSelf)?.time ?? 0
const isJobClosed = await checkJobIsClosed()
if (
!isJobClosed &&
isExpectJobTypeMatch &&
historyMessageList[historyMessageList.length - 1].isSelf &&
historyMessageList[historyMessageList.length - 1].status === MsgStatus.HAS_READ &&
((conversationInfo &&
Object.hasOwn(conversationInfo, 'bothTalked') &&
!conversationInfo.bothTalked) ||
!historyMessageList.filter(
(it) => !it.isSelf // not sent by me
).length) &&
// don't disturb too much
Date.now() - lastGeekMessageSendTime >=
(throttleIntervalMinutes + 4 * Math.random()) * 60 * 1000
) {
await sleepWithRandomDelay(3250)
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
try {
const messageListForGpt = historyMessageList
.filter((it) => it.bizType !== 101 && it.isSelf)
.slice(-recentMessageQuantityForLlm)
await sendGptContent(pageMapByName.boss!, messageListForGpt)
gtag('rnrr_llm_content_sent')
} catch (err) {
console.log(err)
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
await sendLookForwardReplyEmotion(pageMapByName.boss!)
gtag('rnrr_look_forward_reply_emotion_sent', {
fallback: true
})
} else {
gtag('rnrr_encounter_error', {
error: err
})
throw err
}
}
} else {
await sendLookForwardReplyEmotion(pageMapByName.boss!)
gtag('rnrr_look_forward_reply_emotion_sent')
}
} else {
cursorToContinueFind += 1
}
await sleep(1000)
await saveCurrentChatRecord(pageMapByName.boss!)
await sleep(3000)
}
}
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
}
return v
})()
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export async function runEntry() {
app.dock?.hide()
await app.whenReady()
app.on('window-all-closed', (e) => {
e.preventDefault()
})
initPublicIpc()
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'
},
runRecordId
}
})
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error(`PUPPETEER_IS_NOT_EXECUTABLE`)
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'
},
runRecordId
}
})
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
while (true) {
try {
await mainLoop()
} catch (err) {
console.error(err)
try {
await pageMapByName['boss']?.close()
} catch {
//
}
const shouldExit = await checkShouldExit()
if (shouldExit) {
app.exit()
return
}
// handle error
if (err instanceof Error) {
if (err.message.includes('LOGIN_STATUS_INVALID')) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
break
}
if (err.message.includes('ERR_INTERNET_DISCONNECTED')) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ERR_INTERNET_DISCONNECTED)
break
}
if (err.message.includes('ACCESS_IS_DENIED')) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED)
break
}
if (
err.message.includes(`PUPPETEER_IS_NOT_EXECUTABLE`) ||
err.message.includes(`Could not find Chrome`) ||
err.message.includes(`no executable was found`)
) {
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
break
}
if (err.message === 'CANNOT_FIND_A_USABLE_MODEL') {
gtag('cannot_find_a_usable_model')
await dialog.showMessageBox({
type: 'error',
message:
'未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题',
buttons: ['退出']
})
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE)
break
}
}
} finally {
pageMapByName['boss'] = null
await sleep(rerunInterval)
}
}
process.exit(0)
}
process.once('uncaughtException', (error) => {
console.error('uncaughtException', error)
process.exit(1)
})
process.once('unhandledRejection', (error) => {
console.log('unhandledRejection', error)
process.exit(1)
})
process.once('disconnect', () => {
process.exit(0)
})
async function storeStorage(page) {
const [cookies, localStorage] = await Promise.all([
page.cookies(),
page
.evaluate(() => {
return JSON.stringify(window.localStorage)
})
.then((res) => JSON.parse(res))
])
return Promise.all([
writeStorageFile('boss-cookies.json', cookies),
writeStorageFile('boss-local-storage.json', localStorage)
])
}

View File

@@ -1,7 +1,33 @@
const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE
import overrideConsole from './utils/overrideConsole'
import minimist from 'minimist'
import { runCommon } from './features/run-common'
import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'
import { app } from 'electron'
const isUiDev = process.env.NODE_ENV === 'development'
const enableLogToFile = process.env.GEEKGEEKRUN_ENABLE_LOG_TO_FILE === String(1)
if (isUiDev || enableLogToFile) {
overrideConsole()
}
console.log('NODE_ENV:', process.env.NODE_ENV)
// 捕获未处理的 EPIPE 错误
process.on('uncaughtException', (err) => {
if (err?.code === 'EPIPE' || err?.code === 'ERR_STREAM_DESTROYED') {
return
}
throw err
})
console.log('argv:', process.argv)
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
console.log('parsed commandline args:', commandlineArgs)
const runMode = commandlineArgs['mode']
;(async () => {
switch (runMode) {
// #region internal use
case 'geekAutoStartWithBossMain': {
const { waitForProcessHandShakeAndRunAutoChat } = await import(
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index'
@@ -9,18 +35,9 @@ const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE
waitForProcessHandShakeAndRunAutoChat()
break
}
case 'geekAutoStartWithBossDaemon': {
const { runAutoChatWithDaemon } = await import(
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index'
)
runAutoChatWithDaemon()
break
}
case 'checkAndDownloadDependenciesForInit': {
const { checkAndDownloadDependenciesForInit } = await import(
'./flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/index'
)
checkAndDownloadDependenciesForInit()
case 'downloadDependenciesForInit': {
const { downloadDependenciesForInit } = await import('./flow/DOWNLOAD_DEPENDENCIES/index')
downloadDependenciesForInit()
break
}
case 'launchBossZhipinLoginPageWithPreloadExtension': {
@@ -35,15 +52,43 @@ const runMode = process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE
launchBossSite()
break
}
case 'readNoReplyAutoReminder': {
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER/index')
case 'readNoReplyAutoReminderMain': {
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER_MAIN/index')
runEntry()
break
}
case 'launchDaemon': {
await import('./flow/LAUNCH_DAEMON')
break
}
// #endregion
// #region user entry
case 'geekAutoStartWithBoss': {
app.dock?.hide()
await launchDaemon()
const { isAlreadyRunning } = await runCommon({ mode: 'geekAutoStartWithBossMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break
}
case 'readNoReplyAutoReminder': {
app.dock?.hide()
await launchDaemon()
const { isAlreadyRunning } = await runCommon({ mode: 'readNoReplyAutoReminderMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break
}
default: {
globalThis.GEEKGEEKRUN_PROCESS_ROLE = 'ui'
await launchDaemon()
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
openSettingWindow()
break
}
// #region
}
})()

View File

@@ -2,17 +2,40 @@ import gtag from '.'
export default class GtagPlugin {
apply(hooks) {
hooks.newChatStartup.tap('GtagPlugin', (_, { chatStartupFrom }) => {
gtag('new_chat_startup', { chatStartupFrom })
})
hooks.jobMarkedAsNotSuit.tap('GtagPlugin', (_, { markFrom }) => {
gtag('job_marked_as_not_suit', { markFrom })
hooks.newChatStartup.tap('GtagPlugin', (jobData, { chatStartupFrom, jobSource }) => {
gtag('new_chat_startup', {
chatStartupFrom,
encryptJobId: jobData.jobInfo.encryptId,
jobSource
})
})
hooks.jobMarkedAsNotSuit.tap(
'GtagPlugin',
(jobData, { markFrom, markOp, markReason, jobSource }) => {
gtag('job_marked_as_not_suit', {
markFrom,
markOp,
markReason,
bossActiveTimeDesc: jobData.bossInfo.activeTimeDesc,
encryptJobId: jobData.jobInfo.encryptId,
jobSource
})
}
)
hooks.noPositionFoundForCurrentJob.tap('GtagPlugin', () => {
gtag('no_position_found_for_current_job')
})
hooks.noPositionFoundAfterTraverseAllJob.tap('GtagPlugin', () => {
gtag('no_position_found_after_traverse_all_job')
})
hooks.encounterEmptyRecommendJobList.tap('GtagPlugin', ({ pageQuery }) => {
gtag('encounter_empty_rec_job_list', { pageQuery })
})
hooks.sageTimeEnter.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
gtag('sage_time_enter', { tag, totalEnabledTimes, recordedOpCount })
})
hooks.sageTimeExit.tap('GtagPlugin', ({ tag, totalEnabledTimes, recordedOpCount }) => {
gtag('sage_time_exit', { tag, totalEnabledTimes, recordedOpCount })
})
}
}

View File

@@ -1,4 +1,5 @@
import buildInfo from '../../../common/build-info.json'
import os from 'node:os'
type LowercaseLetter =
| 'a'
@@ -55,6 +56,7 @@ function getCommonParams() {
return {
app_version: buildInfo.version,
app_build_hash: buildInfo.buildHash,
os_info: `${os.type()} | ${os.release()} | ${os.arch()}`,
t: Number(new Date())
}
}
@@ -68,9 +70,14 @@ export default async function gtag<T extends string>(
...getCommonParams(),
...params
}
Object.keys(params).forEach((k) => {
if ([null, undefined].includes(params[k])) {
delete params[k]
}
})
// ServiceWorker环境下直接调用上报函数
const reporter = (await import('./Analytics')).default
return reporter.fireEvent(name, params)
return reporter.fireEvent(name.replace(/-/g, '_'), params)
}
// Fire a page view event.

View File

@@ -0,0 +1,130 @@
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
import gtag from './gtag'
import buildInfo from '../../common/build-info.json'
import os from 'node:os'
import fs from 'node:fs'
import {
ensureStorageFileExist,
writeStorageFile,
configFileNameList,
readConfigFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
export default function initPublicIpc() {
ipcMain.on(
'update-window-size',
(
ev,
size: {
width: number
height: number
animate?: boolean
}
) => {
const win = BrowserWindow.fromWebContents(ev.sender)
if (!win) {
return
}
win.setMinimumSize(size.width, size.height)
win.setSize(size.width, size.height, size.animate)
}
)
ipcMain.on('open-external-link', (_, link) => {
shell.openExternal(link, {
activate: true
})
})
ipcMain.on('gtag', (ev, { name, params } = {}) => {
gtag(name, {
...params,
electron_log_source: 'renderer'
})
})
ipcMain.on('send-feed-back-to-github-issue', (ev, payload) => {
const getIssueUrlWithBody = (issueBody: string = '') => {
const baseUrl = `https://github.com/geekgeekrun/geekgeekrun/issues/new`
issueBody = issueBody || ''
if (!issueBody || !issueBody.trim()) {
return baseUrl
}
const urlObj = new URL(baseUrl)
urlObj.searchParams.append('body', issueBody)
return urlObj.toString()
}
shell.openExternal(
getIssueUrlWithBody(`\n\n\n-----
版本号:${buildInfo.version}(${buildInfo.buildVersion})
提交:${buildInfo.buildHash.substring(0, 6)}
操作系统信息: \`${os.type()}\` / \`${os.release()}\` / \`${os.arch()}\``),
{
activate: true
}
)
})
ipcMain.handle('read-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await readStorageFile(payload.fileName)
})
ipcMain.handle('write-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
})
ipcMain.handle('get-os-platform', () => {
return os.platform()
})
ipcMain.handle('choose-file', (ev, { fileChooserConfig }) => {
if (!fileChooserConfig) {
fileChooserConfig = {}
}
const win = BrowserWindow.fromWebContents(ev.sender)
if (!win) {
return dialog.showOpenDialog(fileChooserConfig)
} else {
return dialog.showOpenDialog(win, fileChooserConfig)
}
})
ipcMain.handle('check-executable-file', (ev, filePath: string) => {
if (!filePath?.trim()) {
return {
message: '文件名无效'
}
}
if (!fs.existsSync(filePath)) {
return {
message: '文件不存在'
}
}
if (!fs.statSync(filePath).isFile()) {
const messageSeg = ['文件不是可执行文件']
if (os.platform() === 'darwin') {
messageSeg.push(
'macOS 平台可执行文件位于“App包.app/Contents/MacOS/ 文件夹下”而不是“App包.app”文件夹整体'
)
}
return {
message: messageSeg.join('')
}
}
return null
})
ipcMain.handle('fetch-config-file-content', async () => {
const configFileContentList = configFileNameList.map((fileName) => {
return readConfigFile(fileName)
})
const result = {
config: {}
}
configFileNameList.forEach((fileName, index) => {
result.config[fileName] = configFileContentList[index]
})
return result
})
}

View File

@@ -0,0 +1,75 @@
import path from 'node:path'
import os from 'node:os'
import fs from 'node:fs'
import dayjs from 'dayjs'
export default function overrideConsole() {
const originConsoleLog = console.log.bind(console)
const originConsoleWarn = console.warn.bind(console)
const originConsoleError = console.error.bind(console)
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
const logDirPath = path.join(runtimeFolderPath, 'log')
if (!fs.existsSync(logDirPath)) {
fs.mkdirSync(logDirPath, { recursive: true })
}
const logFileStream = fs.createWriteStream(path.join(logDirPath, `log.log`), {
flags: 'a' // 追加模式
})
const warnFileStream = fs.createWriteStream(path.join(logDirPath, `warn.log`), {
flags: 'a' // 追加模式
})
const errorFileStream = fs.createWriteStream(path.join(logDirPath, `error.log`), {
flags: 'a' // 追加模式
})
console.log = (...args: any[]) => {
const lineHead = `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} [log][PID=${process.pid}]`
originConsoleLog(lineHead, ...args)
logFileStream.write(
[
lineHead,
args.map((arg) => {
try {
return JSON.stringify(arg)
} catch (err) {
return `[[${JSON.stringify(err?.toString())}]]`
}
})
].join(' ') + '\n'
)
}
console.warn = (...args: any[]) => {
const lineHead = `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} [warn][PID=${process.pid}]`
originConsoleWarn(lineHead, ...args)
warnFileStream.write(
[
lineHead,
args.map((arg) => {
try {
return JSON.stringify(arg)
} catch (err) {
return `[[${JSON.stringify(err?.toString())}]]`
}
})
].join(' ') + '\n'
)
}
console.error = (...args: any[]) => {
const lineHead = `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} [warn][PID=${process.pid}]`
originConsoleError(lineHead, ...args)
errorFileStream.write(
[
lineHead,
args.map((arg) => {
try {
return JSON.stringify(arg)
} catch (err) {
return `[[${JSON.stringify(err?.toString())}]]`
}
})
].join(' ') + '\n'
)
}
}

View File

@@ -0,0 +1,67 @@
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
import { checkShouldExit } from "./worker"
export const SCREENSHOT_INTERVAL_MS = 2500
export async function pushCurrentPageScreenshot (page) {
try {
if (!page) {
return
}
// 尝试截图当前页面(压缩为 jpeg + base64避免文件写盘
const screenshotBase64 = await page.screenshot({
type: 'jpeg',
quality: 60,
encoding: 'base64',
fullPage: false
})
const screenshotAt = Date.now()
await sendToDaemon({
type: 'worker-screenshot',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
screenshot: `data:image/jpeg;base64,${screenshotBase64}`,
screenshotAt,
pageUrl: page.url?.() ?? null
}
})
} catch (err) {
if (err?.message?.includes('Session closed')) {
throw new Error(`PAGE_CLOSED`)
}
// 截图失败不应影响主流程
console.warn('[pushCurrentPageScreenshot] error', err)
}
}
export class PeriodPushCurrentPageScreenshotPlugin {
apply(hooks) {
hooks.pageGotten.tap(
'PeriodPushCurrentPageScreenshotPlugin',
(page) => {
async function periodPushCurrentPageScreenshot () {
try {
if (page.isClosed()) {
return
}
const shouldExit = await checkShouldExit()
if (shouldExit) {
return
}
try {
await pushCurrentPageScreenshot(page)
}
catch (err) {
if (err?.message?.includes(`PAGE_CLOSED`)) {
return
}
}
setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
}
catch {}
}
periodPushCurrentPageScreenshot()
}
)
}
}

View File

@@ -0,0 +1,14 @@
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
export async function checkShouldExit () {
const shouldExitResponse = await sendToDaemon(
{
type: 'check-should-exit',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
},
{
needCallback: true
}
)
return shouldExitResponse?.shouldExit
}

Some files were not shown because too many files have changed in this diff Show More