Compare commits

...

35 Commits

Author SHA1 Message Date
geekgeekrun
c4ebdb0882 Merge branch 'feature/ui' 2026-05-02 22:27:32 +08:00
geekgeekrun
f0f01ae317 ui-v0.17.4 2026-05-02 22:27:05 +08:00
geekgeekrun
9d3408d1f3 adjust rerun interval 2026-05-02 22:21:48 +08:00
geekgeekrun
432ac9c298 remove cookie file when find cookie invalid for gascwb 2026-05-02 22:20:11 +08:00
geekgeekrun
eb90a825a0 update laodeng 2026-05-02 21:31:19 +08:00
geekgeekrun
345d75ff98 show dock icon before call enter loginWithCookieAssistant 2026-04-05 15:56:39 +08:00
geekgeekrun
60ac324603 close browser when find bad cookie 2026-04-05 15:31:58 +08:00
geekgeekrun
dd76a856e8 show running overlay when enter config page 2026-04-05 14:44:18 +08:00
geekgeekrun
15e566b343 add stepStatusMapByStepId in runtimeStorage and logic for update related status on ui 2026-04-05 13:47:56 +08:00
geekgeekrun
e09e7ee936 add runtimeStorage 2026-04-05 12:50:51 +08:00
geekgeekrun
6c865252b2 enable main process hot update 2026-04-05 12:29:31 +08:00
geekgeekrun
3db4905cf6 shorten prerequisite-step-by-step-checkstep-by-step-check 2026-04-05 12:25:54 +08:00
geekgeekrun
a7e57c08f6 Merge branch 'master' into feature/ui 2026-04-05 12:25:46 +08:00
geekgeekrun
bf28af126b ui-v0.17.3 2026-04-03 15:35:32 +08:00
geekgeekrun
09a1b4b918 Merge commit '675f77a408ef5e0bcc95b8825e6c7aff69a5b2d2' 2026-04-03 15:34:31 +08:00
geekgeekrun
9400d2bd6f Merge branch 'feature/ui' 2026-04-03 15:31:19 +08:00
geekgeekrun
6c80147be2 enhance compatibility for page jumped after chat started; regard activeTimeDesc with empty string as 半年前活跃 2026-04-03 15:27:33 +08:00
geekgeekrun-maintainer
b5b4cbe259 Enhance README with job stability discussion
Added clarification on job stability concerns and value creation.
2026-03-25 11:09:52 +08:00
geekgeekrun-maintainer
45c860ddbd Enhance interview feedback and add new sections
Add sections on interviewer biases and interview process feedback.
2026-03-25 11:08:58 +08:00
geekgeekrun-maintainer
7a5c199889 Revise interview and process feedback text
Reworded interview invitation and process feedback sections for clarity.
2026-03-21 00:26:46 +08:00
geekgeekrun-maintainer
1447299354 Update README.md 2026-03-21 00:24:01 +08:00
geekgeekrun-maintainer
775b3467e5 Revise interview comments for improved expression
Updated interview feedback and comments for clarity and detail.
2026-03-21 00:22:39 +08:00
geekgeekrun-maintainer
882645771d Enhance README with interview insights and usage
Added sections for interview feedback and application process.
2026-03-21 00:18:14 +08:00
geekgeekrun
675f77a408 ui-v0.17.2 2026-03-20 01:41:35 +08:00
geekgeekrun
435fb170a2 fix sameSite property in cookies not supported by newer puppeteer 2026-03-20 01:40:12 +08:00
geekgeekrun
4048e3b323 ui-v0.17.1 2026-02-27 10:30:30 +08:00
geekgeekrun
e6489db6c4 Merge branch 'feature/ui' 2026-02-27 10:30:09 +08:00
geekgeekrun
3f08481657 fix style of user agreement 2026-02-27 10:29:46 +08:00
geekgeekrun
97c0f3dfaf ui-v0.17.0 2026-02-27 01:03:58 +08:00
geekgeekrun
80158682b0 Merge branch 'feature/ui' 2026-02-27 01:03:06 +08:00
geekgeekrun
bf1d9023a6 Merge commit 'fff8ca7dff95454ffba05566c5252a1625cf1bc2' 2026-02-27 01:02:49 +08:00
geekgeekrun
b7ffe30ee0 enhance style of first run readme window 2026-02-27 00:47:46 +08:00
geekgeekrun
f82987bd76 add frame for almost window 2026-02-27 00:47:15 +08:00
geekgeekrun
57bab1900c enhance gtag 2026-02-27 00:13:57 +08:00
geekgeekrun
1c7d3d992a add feature for customize open message 2026-02-26 23:14:38 +08:00
34 changed files with 1096 additions and 503 deletions

View File

@@ -9,9 +9,34 @@
各行各业,无论你是小白还是大佬,都能通过几步简单的配置,快速开始求职!
## TO 有求职之外其他用途的朋友
## For 有求职之外其他用途的朋友
本程序的目标,是帮助求职者调研求职市场行情或寻找工作。如果有其它用途,请勿下载使用。
## 近期面试吐槽
### 面评
To 写下这段话的面试官 - 我从事前端开发近7年十万级~亿级 toC 产品),就因为我写了几个月爬虫相关项目,就得出这种结论,你不觉得你的面评很搞笑吗?
我们面试中有关爬虫的内容一点儿没聊,你一定要用这种低级的方式证明我不适合这个职位吗?
<img width="320" alt="image" src="https://github.com/user-attachments/assets/96a20b03-fce4-46c2-a8ac-e9c032418944" />
### 面试方式
To 一些需要现场面试的公司 - 我1小时地铁吭呲吭呲赶到贵司和贵司面试官聊天20分钟面试结束把我打发走我又1小时吭呲吭呲回家。你邀约我面试的目是什么完成到面率KPI非要浪费大家的时间吗
### 流程推进
To 我所遇到的大大小小的公司 - 能不能少一些横向对比一个流程20天没反馈HR 经常不接电话玩 Ghosting请问这到底是在干什么就不能有个痛快话吗
### 面试官的刻板印象
#### “你既然写出了这个项目,那你是不是很好找工作啊?”
对不起不是既然你说我好找工作那给我Offer啊
#### “你写出了这个项目,同时我也看到你几份工作时间不长;那你是不是不稳定啊?难道你来了以后天天就想着跑?”
近期离职原因部门解散、业务HC缩减
你以为我真的想不稳定吗?难道我入职就是为了离职而不是在劳动岗位上创造价值?
## 程序有哪些功能?运行逻辑是什么?怎样使用?
### 自动开聊

View File

@@ -699,7 +699,7 @@ async function toRecommendPage (hooks) {
hooks.pageLoaded?.call()
let userInfoResponse = await userInfoPromise
await hooks.userInfoResponse?.promise(userInfoResponse)
await hooks.userInfoResponse?.promise({ userInfoResponse, browser })
if (userInfoResponse?.code !== 0) {
autoStartChatEventBus.emit('LOGIN_STATUS_INVALID', {
userInfoResponse
@@ -1417,7 +1417,12 @@ async function toRecommendPage (hooks) {
// 刚刚活跃 // 今日活跃 // 昨日活跃 // 3日内活跃 // 本周活跃 // 2周内活跃
// 本月活跃 // 2月内活跃 // 3月内活跃 // 4月内活跃 // 5月内活跃 // 近半年活跃 // 半年前活跃
//#endregion
const indexOfActiveText = activeDescList.indexOf(targetJobData.bossInfo.activeTimeDesc)
let activeTimeDescForCompare = targetJobData.bossInfo.activeTimeDesc
// handle empty string case
if (activeTimeDescForCompare === '') {
activeTimeDescForCompare = '半年前活跃'
}
const indexOfActiveText = activeDescList.indexOf(activeTimeDescForCompare)
if (
markAsNotActiveSelectedTimeRange > 0 &&
indexOfActiveText > 0 && indexOfActiveText <= markAsNotActiveSelectedTimeRange
@@ -1532,10 +1537,23 @@ async function toRecommendPage (hooks) {
return false
}
);
const res = await addFriendResponse.json()
return res
await sleepWithRandomDelay(3000)
let res
try {
res = await addFriendResponse.json()
return res
}
catch(err) {
await sleep(2000)
if (page.url().startsWith('https://www.zhipin.com/web/geek/chat')) {
throw new Error('PAGE_JUMPED_TO_CHAT_PAGE')
}
else {
throw err
}
}
}
const waitAndHandleChatSuccess = async () => {
const waitAndHandleChatSuccess = async ({ hasGoToChatPage = false } = {}) => {
await hooks.newChatStartup?.promise(
targetJobData,
{
@@ -1547,13 +1565,22 @@ async function toRecommendPage (hooks) {
await storeStorage(page).catch(() => void 0)
await sleepWithRandomDelay(1500)
if (hasGoToChatPage) {
await page.goBack()
await page.waitForFunction(() => {
return location.href.startsWith(`https://www.zhipin.com/web/geek/jobs`) && document.readyState === 'complete'
})
await sleepWithRandomDelay(2000)
}
const closeDialogButtonProxy = await page.$('.greet-boss-dialog .greet-boss-footer .cancel-btn')
await closeDialogButtonProxy.click()
await sleepWithRandomDelay(2000)
if (closeDialogButtonProxy) {
await closeDialogButtonProxy.click()
await sleepWithRandomDelay(2000)
}
}
const handleAddFriendResponse = async (res) => {
const handleAddFriendResponse = async (res, { hasGoToChatPage = false } = {}) => {
if (res.code === 0) {
await waitAndHandleChatSuccess()
await waitAndHandleChatSuccess({ hasGoToChatPage })
}
else if (
res.zpData.bizCode === 1 &&
@@ -1601,9 +1628,21 @@ async function toRecommendPage (hooks) {
throw new Error('STARTUP_CHAT_ERROR_WITH_UNKNOWN_ERROR')
}
}
const res = await waitAddFriendResponse()
await handleAddFriendResponse(res)
// #endregion
let res
try {
res = await waitAddFriendResponse()
await handleAddFriendResponse(res)
}
catch (err) {
if (err instanceof Error && err.message === 'PAGE_JUMPED_TO_CHAT_PAGE') {
await handleAddFriendResponse({
code: 0
}, { hasGoToChatPage: true })
}
else {
throw err
}
}
} catch (err) {
if (err instanceof Error) {
switch (err.message) {
@@ -1674,8 +1713,11 @@ export async function mainLoop (hooks) {
//set cookies
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
await hooks.cookieWillSet?.promise(bossCookies)
await hooks.cookieWillSet?.promise({ cookies: bossCookies, browser })
for(let i = 0; i < bossCookies.length; i++){
if (Object.hasOwn(bossCookies[i], 'sameSite')) {
bossCookies[i].sameSite = 'unspecified'
}
await page.setCookie(bossCookies[i]);
}
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)

View File

@@ -1,125 +1,134 @@
"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();
// 定义脚本内容,避免重复定义
const stealthScript = () => {
"use strict";
/* -------------------------------------------------------
* 1. 保存原生 Function.prototype.toString
* ----------------------------------------------------- */
const nativeFunctionToString = Function.prototype.toString;
/* -------------------------------------------------------
* 3. 注册伪原生源码
* ----------------------------------------------------- */
const registerNativeSource = (fn, source) => {
try {
nativeSourceMap.set(fn, source);
} catch (_) {}
};
/* -------------------------------------------------------
* 2. WeakMap函数 → 伪原生源码
* ----------------------------------------------------- */
const nativeSourceMap = new WeakMap();
/* -------------------------------------------------------
* 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);
},
});
/* -------------------------------------------------------
* 3. 注册伪原生源码
* ----------------------------------------------------- */
const registerNativeSource = (fn, source) => {
try {
nativeSourceMap.set(fn, source);
} catch (_) {}
};
/* -------------------------------------------------------
* 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] }",
);
})();
/* -------------------------------------------------------
* 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] }",
);
};
async function handle(p) {
// 1. 立即执行一次(针对当前已有的文档,防止错过)
try {
await p.evaluate(stealthScript);
} catch (e) {
// 如果当前页面还没准备好,忽略错误
}
// 2. 注册为新文档脚本(针对未来的导航)
await p.evaluateOnNewDocument(stealthScript);
}
class Plugin extends PuppeteerExtraPlugin {

View File

@@ -1,6 +1,6 @@
{
"name": "@geekgeekrun/puppeteer-extra-plugin-laodeng",
"version": "0.0.1",
"version": "0.0.2",
"description": "laodeng",
"main": "./index.js",
"author": "geekgeekrun",

View File

@@ -103,7 +103,7 @@ const server = net.createServer((socket) => {
// 处理消息
function handleMessage(socket, message) {
console.log('收到消息:', message);
message.type !== 'get-status' && console.log('收到消息:', message);
const _callbackUuid = message._callbackUuid
if (message.type === 'ping') {
sendResponse(socket, _callbackUuid, {
@@ -143,6 +143,23 @@ function handleMessage(socket, message) {
return
}
case 'worker-to-gui-message': {
// 将 prerequisite step 状态写入 worker 的 runtimeStorage
if (
workerInfo &&
message.data?.type === 'prerequisite-step-by-step-check' &&
message.data?.step?.id
) {
workerInfo.runtimeStorage = workerInfo.runtimeStorage || {}
workerInfo.runtimeStorage.stepStatusMapByStepId =
workerInfo.runtimeStorage.stepStatusMapByStepId || {}
workerInfo.runtimeStorage.stepStatusMapByStepId[
message.data.step.id
] = {
step: message.data.step,
runRecordId: message.data.runRecordId
}
}
// 转发工具进程消息到GUI客户端
broadcastToGUI({
type: 'worker-to-gui-message',
@@ -329,6 +346,9 @@ function startWorker({ workerId, command, args, env }, restartCount = 0) {
status: 'running',
startTime: Date.now(),
restartCount, // 使用传入的重启次数
runtimeStorage: {
stepStatusMapByStepId: {}
},
// socket: null, // 工具进程的TCP连接稍后由工具进程注册
// lastHeartbeat: null,
command,
@@ -390,6 +410,7 @@ function getWorkersStatus() {
status: workerInfo.status,
uptime: Date.now() - workerInfo.startTime,
restartCount: workerInfo.restartCount || 0,
runtimeStorage: workerInfo.runtimeStorage || {},
// connected: workerInfo.socket !== null && !workerInfo.socket.destroyed,
// lastHeartbeat: workerInfo.lastHeartbeat,
command: workerInfo.command,

View File

@@ -7,7 +7,7 @@ import { AUTO_CHAT_ERROR_EXIT_CODE } from './enums.mjs'
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
v = 5000
}
return v

View File

@@ -22,7 +22,7 @@ const {
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
v = 5000
}
return v

View File

@@ -116,7 +116,7 @@ export default class SqlitePlugin {
)
hooks.userInfoResponse.tapPromise(
"SqlitePlugin",
async (userInfoResponse) => {
async ({ userInfoResponse } = {}) => {
if (!userInfoResponse || userInfoResponse.code !== 0) {
return;
}

View File

@@ -63,14 +63,16 @@ export default defineConfig({
rollupOptions: {
external: []
},
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser',
watch: process.env.NODE_ENV === 'development' ? {} : undefined
},
plugins: mainPlugins
},
preload: {
plugins: preloadPlugins,
build: {
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser'
minify: process.env.NODE_ENV === 'development' ? undefined : 'terser',
watch: process.env.NODE_ENV === 'development' ? {} : undefined
}
},
renderer: {

View File

@@ -1,6 +1,6 @@
{
"name": "geekgeekrun-ui",
"version": "0.16.0",
"version": "0.17.4",
"description": "BOSS 炸弹 - 自动开聊BOSS助力每位打工人求职",
"main": "./out/main/index.js",
"author": "geekgeekrun",

View File

@@ -1,7 +1,7 @@
{
"version": "0.16.0",
"buildVersion": 36,
"buildTime": 1771743424925,
"buildHash": "98ffc3a143fc2c0ed5a949e441f8159f95ed6afa",
"version": "0.17.4",
"buildVersion": 41,
"buildTime": 1777732024939,
"buildHash": "9d3408d1f3fd5bba0e44736233408e2be2e48536",
"name": "geekgeekrun-ui"
}

View File

@@ -1,2 +1,7 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
export const DEFAULT_CONSTANT_OPEN_CONTENT_SEGS = [
`您好,我对贵公司岗位很感兴趣,希望能有机会进一步沟通,期待您的回复`,
`接下来的聊天,我将根据我简历里的内容,向您进行自我介绍`
]

View File

@@ -18,6 +18,16 @@ export enum RECHAT_LLM_FALLBACK {
EXIT_REMINDER_PROGRAM = 2
}
export enum OPEN_CONTENT_SOURCE {
CONSTANT_CONTENT = 1,
GEMINI_WITH_CHAT_CONTEXT = 2
}
// export enum OPEN_LLM_FALLBACK {
// SEND_CONSTANT_CONTENT = 1,
// EXIT_REMINDER_PROGRAM = 2
// }
export enum RUNNING_STATUS_ENUM {
RUNNING = 0,
NORMAL_EXITED = 1,

View File

@@ -3,17 +3,37 @@ 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'
import {
readStorageFile,
writeStorageFile
} 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) => {
hooks.cookieWillSet.tapPromise('CookieInvalidHandlePlugin', async ({ cookies, browser } = {}) => {
let isValid = checkCookieListFormat(cookies)
while (!isValid) {
try {
browser && (await browser.close())
} catch (err) {
console.log(`close browser failed`, err)
}
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
let app
try {
app = (await import('electron')).app
} catch {
//
}
// popup login dialog, then update login status
try {
await app?.dock?.show()
await loginWithCookieAssistant()
} finally {
await app?.dock?.hide()
}
await sleep(2000)
const newCookies = readStorageFile('boss-cookies.json')
isValid = checkCookieListFormat(newCookies)
@@ -27,8 +47,9 @@ export class CookieInvalidHandlePlugin {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
@@ -42,8 +63,9 @@ export class CookieInvalidHandlePlugin {
}
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
@@ -52,12 +74,13 @@ export class CookieInvalidHandlePlugin {
}
})
})
hooks.userInfoResponse.tapPromise('CookieInvalidHandlePlugin', async (userInfoResponse) => {
hooks.userInfoResponse.tapPromise('CookieInvalidHandlePlugin', async ({ userInfoResponse, browser } = {}) => {
if (userInfoResponse.code === 0) {
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
@@ -67,15 +90,34 @@ export class CookieInvalidHandlePlugin {
})
return
}
try {
browser && (await browser.close())
} catch (err) {
console.log(`close browser failed`, err)
}
await writeStorageFile('boss-cookies.json', [])
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
let app
try {
app = (await import('electron')).app
} catch {
//
}
// popup login dialog, then update login status
try {
await app?.dock?.show()
await loginWithCookieAssistant()
} finally {
await app?.dock?.hide()
}
} catch (e) {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'

View File

@@ -30,7 +30,7 @@ process.on('SIGTERM', () => {
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
v = 5000
}
return v
@@ -66,8 +66,9 @@ const runAutoChat = async () => {
})
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
@@ -80,8 +81,9 @@ const runAutoChat = async () => {
}
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'
@@ -183,8 +185,9 @@ export const waitForProcessHandShakeAndRunAutoChat = async () => {
)
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'

View File

@@ -374,6 +374,9 @@ export async function launchBossSite() {
})
let [page] = await browser.pages()
for (let i = 0; i < bossCookies.length; i++) {
if (Object.hasOwn(bossCookies[i], 'sameSite')) {
bossCookies[i].sameSite = 'unspecified'
}
await page.setCookie(bossCookies[i])
}

View File

@@ -32,7 +32,7 @@ import {
requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
autoReminderPromptTemplateFileName,
defaultPromptMap,
writeDefaultAutoRemindPrompt
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
@@ -466,17 +466,19 @@ export default function initIpc() {
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 })
ipcMain.on('no-reply-reminder-prompt-edit', async (_, { type }) => {
const template = await readStorageFile(defaultPromptMap[type].fileName, {
isJson: false
})
if (!template) {
await writeDefaultAutoRemindPrompt()
await writeDefaultAutoRemindPrompt({ type })
}
const filePath = path.join(storageFilePath, autoReminderPromptTemplateFileName)
const filePath = path.join(storageFilePath, defaultPromptMap[type].fileName)
shell.openPath(filePath)
})
ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close())
ipcMain.handle('check-if-auto-remind-prompt-valid', async () => {
await getValidTemplate()
ipcMain.handle('check-if-auto-remind-prompt-valid', async (_, { type }) => {
await getValidTemplate({ type })
})
ipcMain.handle('check-is-resume-content-valid', async () => {
const res = (await readConfigFile('resumes.json'))?.[0]
@@ -486,8 +488,8 @@ export default function initIpc() {
const res = (await readConfigFile('resumes.json'))?.[0]
return resumeContentEnoughDetect(res)
})
ipcMain.handle('overwrite-auto-remind-prompt-with-default', async () => {
await writeDefaultAutoRemindPrompt()
ipcMain.handle('overwrite-auto-remind-prompt-with-default', async (_, { type }) => {
await writeDefaultAutoRemindPrompt({ type })
})
ipcMain.handle('check-if-llm-config-list-valid', async () => {
const llmConfigList = await readConfigFile('llm.json')

View File

@@ -30,6 +30,9 @@ export async function launchBoss(browser: Browser) {
const bossLocalStorage = readStorageFile('boss-local-storage.json')
//set cookies
for (let i = 0; i < bossCookies.length; i++) {
if (Object.hasOwn(bossCookies[i], 'sameSite')) {
bossCookies[i].sameSite = 'unspecified'
}
await page.setCookie(bossCookies[i])
}
await setDomainLocalStorage(browser, localStoragePageUrl, bossLocalStorage)

View File

@@ -60,7 +60,10 @@ const pickLlmConfigFromList = (llmConfigList, blockModelSet) => {
// let _index = 0
const RESUME_PLACEHOLDER = `__REPLACE_REAL_RESUME_HERE__`
const defaultPrompt = `**核心指令:**
export const defaultPromptMap = {
rechat: {
fileName: 'auto-reminder-resume-system-message-template.md',
content: `**核心指令:**
你是一个智能求职助手需要根据用户简历生成30字左右的提醒消息满足以下要求
1. 每次生成需满足:
- √ 包含1个核心技能 + 1个成果量化
@@ -94,15 +97,21 @@ const defaultPrompt = `**核心指令:**
**输出格式:**
请确保仅回复一句话以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
},
open: {
fileName: 'auto-reminder-open-message-template.md',
content:
'请根据我的简历帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容但需要表达出应聘意向。请确保仅响应一句话以JSON响应数据结构参考`{"response": "这里是将会发送给招聘者的内容"}`'
}
if (!template.includes(RESUME_PLACEHOLDER)) {
}
export const getValidTemplate = async ({ type }) => {
let template = await readStorageFile(defaultPromptMap[type].fileName, { isJson: false })
if (!template) {
await writeDefaultAutoRemindPrompt({ type })
template = defaultPromptMap[type].content
}
if (type === 'rechat' && !template.includes(RESUME_PLACEHOLDER)) {
const e = new Error(`简历内容占位符字符串不存在。占位字符串是 ${RESUME_PLACEHOLDER}`)
e.name = `RESUME_PLACEHOLDER_NOT_EXIST`
throw e
@@ -110,8 +119,19 @@ export const getValidTemplate = async () => {
return template
}
export const writeDefaultAutoRemindPrompt = async () => {
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false })
export const writeDefaultAutoRemindPrompt = async ({ type }) => {
switch (type) {
case 'rechat':
await writeStorageFile(defaultPromptMap[type].fileName, defaultPromptMap[type].content, {
isJson: false
})
break
case 'open':
await writeStorageFile(defaultPromptMap[type].fileName, defaultPromptMap[type].content, {
isJson: false
})
break
}
}
export const requestNewMessageContent = async (
@@ -119,24 +139,26 @@ export const requestNewMessageContent = async (
{
requestScene,
llmConfigIdForPick
}: { requestScene?: RequestSceneEnum; llmConfigIdForPick?: string[] } = {}
}: {
requestScene?: RequestSceneEnum
llmConfigIdForPick?: string[]
} = {}
) => {
const template = await getValidTemplate()
const systemMessageTemplate = await getValidTemplate({ type: 'rechat' })
const resumeObject = (await readConfigFile('resumes.json'))?.[0]
const resumeContent = formatResumeJsonToMarkdown(resumeObject)
const chatList = [
{
role: 'system',
content: template.replace(RESUME_PLACEHOLDER, resumeContent)
content: systemMessageTemplate.replace(RESUME_PLACEHOLDER, resumeContent)
}
]
const openMessageTemplate = await getValidTemplate({ type: 'open' })
chatList.push({
role: 'user',
content:
'请根据我的简历帮我写一句谦逊有礼貌的开场白。开头包含“您好”等类似敬语、结尾包含“期待回复”等类似话术。不必包含简历中的具体内容但需要表达出应聘意向。请确保仅响应一句话以JSON响应数据结构参考`{"response": "这里是将会发送给招聘者的内容"}`'
content: openMessageTemplate
})
// chatRecords = chatRecords.slice(chatRecords.length - _index)
// debugger
for (const record of chatRecords) {
const assistantJsonContent = JSON.stringify({
response: record.text
@@ -246,14 +268,18 @@ export const requestNewMessageContent = async (
}
}
export async function sendGptContent(page: Page, chatRecords) {
export async function getGptContent(chatRecords) {
const textToSend = (
await requestNewMessageContent(chatRecords, {
requestScene: RequestSceneEnum.readNoReplyAutoReminder
})
).responseText
return textToSend
}
export async function sendMessage(page: Page, textToSend: string) {
const chatInputSelector = `.chat-conversation .message-controls .chat-input`
const chatInputHandle = await page.$(chatInputSelector)
const chatInputHandle = (await page.$(chatInputSelector))!
await chatInputHandle.click()
await sleep(500)
await chatInputHandle.click()

View File

@@ -1,7 +1,7 @@
import { bootstrap, launchBoss } from './bootstrap'
import { MsgStatus, type ChatListItem } from './types'
import { Browser, Page } from 'puppeteer'
import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation'
import { getGptContent, sendLookForwardReplyEmotion, sendMessage } from './boss-operation'
import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs'
import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs'
import { app, dialog } from 'electron'
@@ -24,6 +24,7 @@ import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo'
import { messageForSaveFilter } from '../../../common/utils/chat-list'
import {
AUTO_CHAT_ERROR_EXIT_CODE,
OPEN_CONTENT_SOURCE,
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK
} from '../../../common/enums/auto-start-chat'
@@ -40,6 +41,7 @@ import { loginWithCookieAssistant } from '../../features/login-with-cookie-assis
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../common/constant'
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在退出')
@@ -87,6 +89,23 @@ const onlyRemindBossWithoutBlockCompanyName =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithoutBlockCompanyName ??
!!blockCompanyNameRegExp
const openContentSource =
readConfigFile('boss.json').autoReminder?.openContentSource ??
OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
const constantOpenContent = (() => {
let constantOpenContent = readConfigFile('boss.json').autoReminder?.constantOpenContent ?? ''
if (constantOpenContent?.trim?.()) {
return constantOpenContent
} else {
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
constantOpenContent = DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join(``)
} else {
constantOpenContent = DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
}
}
return constantOpenContent
})()
const dbInitPromise = initDb(getPublicDbFilePath())
export const pageMapByName: {
@@ -286,7 +305,17 @@ const mainLoop = async () => {
let cookieCheckResult = checkCookieListFormat(bossCookies)
while (!cookieCheckResult) {
try {
await loginWithCookieAssistant()
browser && (await browser.close())
} catch (err) {
console.log(`close browser failed`, err)
}
try {
try {
await app.dock?.show()
await loginWithCookieAssistant()
} finally {
await app.dock?.hide()
}
bossCookies = readStorageFile('boss-cookies.json')
cookieCheckResult = checkCookieListFormat(bossCookies)
} catch (err) {
@@ -297,8 +326,9 @@ const mainLoop = async () => {
})
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
@@ -311,8 +341,9 @@ const mainLoop = async () => {
}
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
@@ -339,8 +370,17 @@ const mainLoop = async () => {
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
writeStorageFile('boss-cookies.json', [])
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
browser && (await browser.close())
} catch (err) {
console.log(`close browser failed`, err)
}
try {
try {
await app.dock?.show()
await loginWithCookieAssistant()
} finally {
await app.dock?.hide()
}
} catch (err) {
await dialog.showMessageBox({
type: `error`,
@@ -349,8 +389,9 @@ const mainLoop = async () => {
})
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
@@ -368,8 +409,9 @@ const mainLoop = async () => {
) {
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
@@ -401,8 +443,9 @@ const mainLoop = async () => {
await storeStorage(pageMapByName.boss)
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
@@ -415,8 +458,9 @@ const mainLoop = async () => {
}
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
@@ -582,30 +626,50 @@ const mainLoop = async () => {
(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)
const messageList = historyMessageList
.filter((it) => it.bizType !== 101 && it.isSelf)
.slice(-recentMessageQuantityForLlm)
if (!messageList?.length) {
if (openContentSource === OPEN_CONTENT_SOURCE.CONSTANT_CONTENT) {
await sendMessage(pageMapByName.boss!, constantOpenContent)
gtag('rnrr_llm_content_sent')
} catch (err) {
console.log(err)
if (rechatLlmFallback === RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION) {
await sendLookForwardReplyEmotion(pageMapByName.boss!)
} else {
try {
const textToSend = await getGptContent(messageList)
await sendMessage(pageMapByName.boss!, textToSend)
gtag('rnrr_llm_content_sent')
} catch (err) {
console.log(err)
await sendMessage(pageMapByName.boss!, constantOpenContent)
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')
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
try {
const textToSend = await getGptContent(messageList)
await sendMessage(pageMapByName.boss!, textToSend)
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
@@ -619,7 +683,7 @@ const mainLoop = async () => {
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
v = 3000
v = 5000
}
return v
@@ -644,8 +708,9 @@ export async function runEntry() {
)
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'
@@ -670,8 +735,9 @@ export async function runEntry() {
})
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
@@ -683,8 +749,9 @@ export async function runEntry() {
}
sendToDaemon({
type: 'worker-to-gui-message',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
type: 'prerequisite-step-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'

View File

@@ -31,6 +31,7 @@ export function createBrowserAssistantWindow(
minWidth: 800,
height: 400,
resizable: true,
frame: true,
show: false,
autoHideMenuBar: true,
webPreferences: {
@@ -39,7 +40,9 @@ export function createBrowserAssistantWindow(
},
...opt
})
browserAssistantWindow!.setAlwaysOnTop(true, 'normal')
browserAssistantWindow!.focus()
browserAssistantWindow!.setAlwaysOnTop(false)
browserAssistantWindow.on('ready-to-show', () => {
browserAssistantWindow!.show()
})

View File

@@ -16,6 +16,7 @@ export function createCommonJobConditionConfigWindow(
resizable: false,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -20,13 +20,16 @@ export function createCookieAssistantWindow(
resizable: true,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
cookieAssistantWindow!.setAlwaysOnTop(true, 'normal')
cookieAssistantWindow!.focus()
cookieAssistantWindow!.setAlwaysOnTop(false)
cookieAssistantWindow.on('ready-to-show', () => {
cookieAssistantWindow!.show()
})

View File

@@ -8,11 +8,12 @@ export function createFirstLaunchNoticeWindow(
// Create the browser window.
firstLaunchNoticeWindow = new BrowserWindow({
width: 960,
height: 640,
resizable: false,
maxWidth: 960,
minHeight: 320,
resizable: true,
show: false,
autoHideMenuBar: true,
frame: false,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -15,6 +15,7 @@ export function createLlmConfigWindow(
resizable: false,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -12,6 +12,7 @@ export function createMainWindow(): BrowserWindow {
minWidth: 1280,
show: false,
autoHideMenuBar: true,
frame: true,
...(process.platform === 'linux'
? {
/* icon */

View File

@@ -17,6 +17,7 @@ export function createReadNoReplyReminderLlmMockWindow(
resizable: false,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -15,6 +15,7 @@ export function createResumeEditorWindow(
resizable: true,
show: false,
autoHideMenuBar: true,
frame: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false

View File

@@ -55,7 +55,7 @@
</template>
<script lang="ts" setup>
// import { useTaskManagerStore } from '@renderer/store'
import { useTaskManagerStore } from '@renderer/store'
import { getAutoStartChatSteps } from '../../../../common/prerequisite-step-by-step-check'
import { computed, onUnmounted, ref, watch } from 'vue'
import {
@@ -71,12 +71,12 @@ const props = defineProps({
type: Number
}
})
// const taskManagerStore = useTaskManagerStore()
// const runningTaskInfo = computed(() => {
// return taskManagerStore.runningTasks?.find((it) => {
// return it.workerId === props.workerId
// })
// })
const taskManagerStore = useTaskManagerStore()
const runningTaskInfo = computed(() => {
return taskManagerStore.runningTasks?.find((it) => {
return it.workerId === props.workerId
})
})
const steps = ref([])
const stepsForRender = computed(() => {
const clonedSteps = JSON.parse(JSON.stringify(steps.value))
@@ -101,9 +101,43 @@ function fillEmptySteps() {
steps.value = arr
currentRunningStatus.value = RUNNING_STATUS_ENUM.RUNNING
}
watch(() => props.runRecordId, fillEmptySteps, {
immediate: true
})
function applyRuntimeStepStatus() {
const task = runningTaskInfo.value
if (!task?.runtimeStorage?.stepStatusMapByStepId) {
return
}
const stepMap = task.runtimeStorage.stepStatusMapByStepId
steps.value = getAutoStartChatSteps().map((step) => {
const saved = stepMap[step.id]
if (!saved || !saved.step) {
return step
}
return {
...step,
status: saved.step.status
}
})
}
watch(
() => props.runRecordId,
() => {
fillEmptySteps()
applyRuntimeStepStatus()
},
{
immediate: true
}
)
watch(
() => taskManagerStore.runningTasks,
() => {
applyRuntimeStepStatus()
},
{ deep: true }
)
watch(
() => stepsForRender.value,
(v) => {
@@ -121,10 +155,7 @@ watch(
const { ipcRenderer } = electron
function messageHandler(ev, { data }) {
if (
data.type !== 'prerequisite-step-by-step-checkstep-by-step-check' ||
data.runRecordId !== props.runRecordId
) {
if (data.type !== 'prerequisite-step-by-step-check' || data.runRecordId !== props.runRecordId) {
return
}
const { id: stepId, status: stepStatus } = data.step

View File

@@ -1,114 +1,118 @@
<template>
<div ref="componentRootEl" class="first-run-readme">
<div class="first-run-readme__inner">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
<div class="readme-desc">
如下是使用必读请您逐条阅读如果已经了解且接受请在每一条前面打勾
</div>
<article class="readme-article">
<ElCheckboxGroup
v-model="readmeItemCheckStatusList"
@change="handleReadmeItemCheckStatusListChange"
>
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
本程序从某种程度上说属于辅助工具<el-link
color-blue
@click.stop.prevent="
() => {
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
gtagRenderer('view_boss_agreement_clicked')
}
"
>BOSS直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
>等对您不利的风险因此使用本程序即意味着<b class="color-red">您愿意接受以上风险</b
>且如果相关风险发生<b class="color-red">需要自行承担相关后果本程序不负责</b>
</ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b
class="color-red"
>注意节制</b
>建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red"
>注册一个本程序专用的新的BOSS直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
您所在公司可能会采购上网行为监控工具或网关例如奇安信深信服绿盟等厂商的产品对您的计算机终端或网络进行<b
color-red
>监控</b
>从而<b color-red>审计跟踪</b>您的行为您的上级/IT/HR
可能会获取到监控数据从而了解团队成员离职倾向如果您不希望您的上级/IT/HR
了解到您正在求职建议您<b color-red
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
>
</ElCheckbox>
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
本程序尊重您的隐私<b color-red
>不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及您的上级/IT/HR报告您的求职行为不会向猎头公司泄露您的信息</b
>但由于本程序开源<b color-red
>任何人均可更改本程序源码并重新发布这一过程中其它开发者是可以加入恶意程序的因此请从你信任的源下载本程序</b
>
</ElCheckbox>
<div class="first-run-readme__inner-outer">
<div class="first-run-readme__inner">
<div class="readme-title">欢迎使用GeekGeekRun祝您求职顺利~</div>
<div class="readme-desc">
如下是使用必读请您逐条阅读如果已经了解且接受请在每一条前面打勾
</div>
<article class="readme-article">
<ElCheckboxGroup
v-model="readmeItemCheckStatusList"
@change="handleReadmeItemCheckStatusListChange"
>
<ElCheckbox :label="0" :class="[unreadItemsAfterClickSubmit[0] ? 'unread' : '']">
本程序从某种程度上说属于辅助工具<el-link
color-blue
@click.stop.prevent="
() => {
electron.ipcRenderer.send(
'open-external-link',
'https://about.zhipin.com/agreement/'
)
gtagRenderer('view_boss_agreement_clicked')
}
"
>BOSS直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
>等对您不利的风险因此使用本程序即意味着<b class="color-red">愿意接受以上风险</b
>且如果相关风险发生<b class="color-red">您需要自行承担相关后果本程序不负责</b>
</ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
本程序会通过尽可能模仿用户行为来规避相关风险但并不能保证可以完全规避建议您使用本程序时<b
class="color-red"
>注意节制</b
>建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red"
>注册一个本程序专用的新的BOSS直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
<ElCheckbox :label="4" :class="[unreadItemsAfterClickSubmit[4] ? 'unread' : '']">
您所在公司可能会采购上网行为监控工具或网关例如奇安信深信服绿盟等厂商的产品对您的计算机终端或网络进行<b
color-red
>监控</b
>从而<b color-red>审计跟踪</b>您的行为您的上级/IT/HR
可能会获取到监控数据从而了解团队成员离职倾向如果您不希望您的上级/IT/HR
了解到您正在求职建议您<b color-red
>不要在您所在公司提供的计算机终端或网络上使用本程序</b
>
</ElCheckbox>
<ElCheckbox :label="5" :class="[unreadItemsAfterClickSubmit[5] ? 'unread' : '']">
本程序尊重您的隐私<b color-red
>不会上报能够识别出您身份的信息不会参与任何钓鱼活动不会向您所在公司及您的上级/IT/HR报告您的求职行为不会向猎头公司泄露您的信息</b
>但由于本程序开源<b color-red
>任何人均可更改本程序源码并重新发布这一过程中其它开发者是可以加入恶意程序的因此请从你信任的源下载本程序</b
>
</ElCheckbox>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red"
>下载使用是免费的</b
>任何人可以<b class="color-red">免费获得免费使用</b><b class="color-red"
>作者没有利用本程序赚到过任何收入</b
>如果您是从GitHub以外的地方付费后购买的本程序或您被提示必须付费后才能使用本程序<b
class="color-red"
>那您大概率被骗了</b
>或者<b class="color-red">您下载到了本程序修改版</b><b class="color-red"
>本程序对此概不负责请勿找作者商讨退款售后事宜相关事宜请咨询卖方</b
>
</ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b
class="color-red"
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过
VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V
等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
</ElCheckboxGroup>
</article>
<footer flex mt20px pb20px flex-justify-end>
<ElCheckbox :label="6" :class="[unreadItemsAfterClickSubmit[6] ? 'unread' : '']">
本程序<b class="color-red">没有内置任何付费功能</b><b class="color-red"
>下载使用是免费的</b
>任何人可以<b class="color-red">免费获得免费使用</b><b class="color-red"
>作者没有利用本程序赚到过任何收入</b
>如果您是从GitHub以外的地方付费后购买的本程序或您被提示必须付费后才能使用本程序<b
class="color-red"
>那您大概率被骗了</b
>或者<b class="color-red">您下载到了本程序修改版</b><b class="color-red"
>本程序对此概不负责请勿找作者商讨退款售后事宜相关事宜请咨询卖方</b
>
</ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b
class="color-red"
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
本程序经历过了多次测试理论上来说大部分情况下可以正常运行但可能也会出现测试用例覆盖不到位导致程序不按预期运行的情况如果您有顾虑建议通过
VMware Workstation / FusionOracle VirtualBoxMicrosoft Hyper-V
等虚拟化技术运行本程序如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
</ElCheckbox>
</ElCheckboxGroup>
</article>
</div>
</div>
<footer pt10px pb10px>
<div flex flex-justify-end w-880px ml-auto mr-auto>
<el-button type="text" @click="handleCancel">退出程序</el-button>
<el-button type="primary" @click="handleSubmit"
>我已经阅读并接受上方所提及的相关风险并决定继续使用本程序</el-button
>
</footer>
</div>
</div>
</footer>
</div>
</template>
@@ -163,30 +167,27 @@ const handleReadmeItemCheckStatusListChange = (value: number[]) => {
})
}
const componentRootEl = ref<HTMLElement>()
onMounted(() => {
const ro = new ResizeObserver(() => {
electron.ipcRenderer.send('update-window-size', {
width: componentRootEl.value!.offsetWidth,
height: componentRootEl.value!.offsetHeight
})
})
ro.observe(componentRootEl.value!)
onBeforeMount(() => {
ro.disconnect()
})
})
</script>
<style lang="scss" scoped>
.first-run-readme {
box-sizing: border-box;
width: 960px;
height: fit-content;
width: 100vw;
height: 100vh;
overflow: auto;
user-select: none;
&__inner {
display: flex;
flex-direction: column;
overflow: hidden;
.first-run-readme__inner-outer {
flex: 1;
overflow: auto;
}
.first-run-readme__inner {
width: 880px;
margin: 0 auto;
padding-top: 30px;
padding-bottom: 30px;
.readme-title {
}
.readme-desc {
@@ -222,5 +223,9 @@ onMounted(() => {
}
}
}
footer {
flex: 0;
background-color: #f0f0f0;
}
}
</style>

View File

@@ -1681,7 +1681,7 @@
>
<RunningOverlay
ref="runningOverlayRef"
worker-id="geekAutoStartWithBossMain"
:worker-id="CURRENT_WORKER_ID"
:run-record-id="runRecordId"
>
<template #op-buttons="{ currentRunningStatus }">
@@ -1714,7 +1714,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch, nextTick, onUnmounted } from 'vue'
import { computed, onBeforeUnmount, ref, watch, onUnmounted, onMounted } from 'vue'
import { ElForm, ElMessage } from 'element-plus'
import { QuestionFilled, ArrowDown } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
@@ -1742,6 +1742,7 @@ import JobSourceDragOrderer from '../../../features/JobSourceDragOrderer/index.v
import expectJobFilterTemplateList from './expectJobFilterTemplateList'
import RunningOverlay from '@renderer/features/RunningOverlay/index.vue'
import { RUNNING_STATUS_ENUM } from '../../../../../common/enums/auto-start-chat'
import { useTaskManagerStore } from '@renderer/store'
import {
getJobDetailRegExpMatchLogicConfig,
isJobDetailRegExpEmpty,
@@ -2084,6 +2085,24 @@ const formRules = {
const formRef = ref<InstanceType<typeof ElForm>>()
const runRecordId = ref(null)
const runningOverlayRef = ref(null)
const taskManagerStore = useTaskManagerStore()
const CURRENT_WORKER_ID = 'geekAutoStartWithBossMain'
onMounted(async () => {
await taskManagerStore.getRunningTasks()
const existingWorker = taskManagerStore.runningTasks?.find(
(it) => it.workerId === CURRENT_WORKER_ID
)
if (existingWorker) {
runRecordId.value = existingWorker.runtimeStorage?.stepStatusMapByStepId
? Object.values(existingWorker.runtimeStorage.stepStatusMapByStepId)[0]?.runRecordId
: null
if (runRecordId.value) {
runningOverlayRef.value?.show()
}
}
})
const handleSubmit = async () => {
gtagRenderer('save_config_and_launch_clicked', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken,

View File

@@ -54,7 +54,77 @@
</template>
</div>
</el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的BOSS时将要向BOSS发出">
<el-form-item label="开场白话术">
<el-radio-group v-model="formContent.autoReminder.openContentSource" w-full>
<div w-full>
<el-radio :label="OPEN_CONTENT_SOURCE.CONSTANT_CONTENT"> 固定文案 </el-radio>
<el-input
v-if="
formContent.autoReminder.openContentSource ===
OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
"
v-model="formContent.autoReminder.constantOpenContent"
w-full
:autosize="{ minRows: 3 }"
max-h-8lh
type="textarea"
:placeholder="defaultConstantOpenContent"
class="pl-30px mb10px"
:style="{
boxSizing: 'border-box'
}"
/>
</div>
<div>
<el-radio :label="OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型生成的内容
</el-radio>
<div
v-if="
formContent.autoReminder.openContentSource ===
OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
ml30px
>
<el-form-item class="mb4px">
<div>
<div>
<el-button
size="small"
type="primary"
@click="
handleClickEditPrompt({
type: 'open'
})
"
>
使用外部编辑器编辑“开场白话术”提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
restoreDefaultTemplate({
type: 'open',
gaEvName: 'reset_template_clicked_in_main_form'
})
}
"
>
还原默认“开场白话术”提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑“开场白话术”提示词模板。
</div>
</div>
</el-form-item>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item label="跟进话术 - 当发现已读不回的BOSS时将要向BOSS发出">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div>
<el-tooltip
@@ -71,91 +141,117 @@
</el-radio>
</el-tooltip>
<br />
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
<div>
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
<div
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
ml30px
>
<el-form-item class="mb4px">
<div>
<div>
<el-button
size="small"
type="primary"
@click="
handleClickEditPrompt({
type: 'rechat'
})
"
>
使用外部编辑器编辑“跟进话术”提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
restoreDefaultTemplate({
type: 'rechat',
gaEvName: 'reset_template_clicked_in_main_form'
})
}
"
>
还原默认“跟进话术”提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑“跟进话术”提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
</div>
</div>
</div>
</el-radio-group>
</el-form-item>
<div class="ml-30px">
<div mt10px>
<el-form-item mb0 label="大语言模型公共设置及效果预览" />
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
[
formContent.autoReminder.rechatContentSource,
formContent.autoReminder.openContentSource
].includes(RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT)
"
>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div ml-30px>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板 (Markdown)
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
gtagRenderer('reset_template_clicked_in_main_form')
restoreDefaultTemplate()
}
"
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item label="当配置的所有大模型均不可使用时">
<div class="flex flex-items-center">
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
>
还原默认提示词模板
</el-button>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div class="flex flex-items-center">
<span class="whitespace-nowrap">当所有模型均不可使用时&nbsp;</span>
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div>
</el-form-item>
</el-form-item>
</div>
</template>
<el-form-item ml-30px>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
><span text-orange ml10px
>&lt;- 正式运行前建议在这里先试一试大模型生成效果是否符合预期哦</span
>
</el-form-item>
</div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
<el-input-number
@@ -222,7 +318,7 @@
>
<RunningOverlay
ref="runningOverlayRef"
worker-id="readNoReplyAutoReminderMain"
:worker-id="CURRENT_WORKER_ID"
:run-record-id="runRecordId"
>
<template #op-buttons="{ currentRunningStatus }">
@@ -255,10 +351,12 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus'
import { useRouter } from 'vue-router'
import {
OPEN_CONTENT_SOURCE,
// OPEN_LLM_FALLBACK,
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK,
RUNNING_STATUS_ENUM
@@ -267,6 +365,8 @@ import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import mittBus from '../../utils/mitt'
import { QuestionFilled } from '@element-plus/icons-vue'
import RunningOverlay from '@renderer/features/RunningOverlay/index.vue'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../../common/constant'
import { useTaskManagerStore } from '@renderer/store'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'rnrr-config',
@@ -282,7 +382,10 @@ const formContent = ref({
recentMessageQuantityForLlm: 8,
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
onlyRemindBossWithExpectJobType: true,
onlyRemindBossWithoutBlockCompanyName: true
onlyRemindBossWithoutBlockCompanyName: true,
openContentSource: OPEN_CONTENT_SOURCE.CONSTANT_CONTENT,
// openLlmFallback: OPEN_LLM_FALLBACK.SEND_CONSTANT_CONTENT,
constantOpenContent: ''
}
})
@@ -315,7 +418,10 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
: parseInt(conf.recentMessageQuantityForLlm)
: 8
conf.onlyRemindBossWithExpectJobType = conf.onlyRemindBossWithExpectJobType ?? true
conf.onlyRemindBossWithoutBlockCompanyName = conf.onlyRemindBossWithoutBlockCompanyName ?? true
conf.rechatLlmFallback = conf.rechatLlmFallback ?? RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
conf.openContentSource = conf.openContentSource ?? OPEN_CONTENT_SOURCE.CONSTANT_CONTENT
conf.constantOpenContent = conf.constantOpenContent ?? ''
formContent.value.autoReminder = conf
})
@@ -462,8 +568,9 @@ async function checkIsCanRun() {
return false
}
try {
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid')
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid', { type: 'rechat' })
} catch (err) {
console.log(err)
if (err?.message?.includes(`RESUME_PLACEHOLDER_NOT_EXIST`)) {
gtagRenderer('cannot_launch_for_no_resume_placehold')
console.log(`提示词模板无效`, err)
@@ -479,8 +586,10 @@ async function checkIsCanRun() {
}
)
.then(async () => {
gtagRenderer('confirm_invalid_rt_tip_dialog')
await restoreDefaultTemplate()
await restoreDefaultTemplate({
type: 'rechat',
gaEvName: 'confirm_invalid_rt_tip_dialog'
})
})
.catch(() => {
gtagRenderer('close_invalid_rt_tip_dialog')
@@ -499,19 +608,46 @@ async function checkIsCanRun() {
}
const runRecordId = ref(null)
const runningOverlayRef = ref(null)
const taskManagerStore = useTaskManagerStore()
const CURRENT_WORKER_ID = 'readNoReplyAutoReminderMain'
onMounted(async () => {
await taskManagerStore.getRunningTasks()
const existingWorker = taskManagerStore.runningTasks?.find(
(it) => it.workerId === CURRENT_WORKER_ID
)
if (existingWorker) {
runRecordId.value = existingWorker.runtimeStorage?.stepStatusMapByStepId
? Object.values(existingWorker.runtimeStorage.stepStatusMapByStepId)[0]?.runRecordId
: null
if (runRecordId.value) {
runningOverlayRef.value?.show()
}
}
})
const handleSubmit = async () => {
gtagRenderer('run_read_no_reply_reminder_clicked', {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
rechat_limit_day: formContent.value.autoReminder.rechatLimitDay,
rechat_content_source: formContent.value.autoReminder.rechatContentSource,
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm,
only_remind_boss_with_expect_job_type:
formContent.value.autoReminder.onlyRemindBossWithExpectJobType,
only_remind_boss_without_block_company_name:
formContent.value.autoReminder.onlyRemindBossWithoutBlockCompanyName,
rechat_llm_fallback: formContent.value.autoReminder.rechatLlmFallback,
open_content_source: formContent.value.autoReminder.openContentSource,
constant_open_content_text_length: formContent.value.autoReminder.constantOpenContent.length ?? 0
})
await formRef.value!.validate()
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
gtagRenderer('config_saved')
if (
formContent.value.autoReminder?.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT ||
formContent.value.autoReminder?.openContentSource ===
OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
if (!(await checkIsCanRun())) {
return
@@ -568,8 +704,9 @@ function handleThrottleIntervalMinutesBlur() {
)
}
const restoreDefaultTemplate = async () => {
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default')
const restoreDefaultTemplate = async ({ type, gaEvName }) => {
gtagRenderer(gaEvName)
await electron.ipcRenderer.invoke('overwrite-auto-remind-prompt-with-default', { type })
ElMessage({
type: 'success',
message: '模板还原成功'
@@ -603,11 +740,12 @@ const handleClickEditResume = async () => {
}
}
const handleClickEditPrompt = async () => {
gtagRenderer('edit_prompt_clicked')
await electron.ipcRenderer.send('no-reply-reminder-prompt-edit')
const handleClickEditPrompt = async ({ type }) => {
gtagRenderer('edit_prompt_clicked', { type })
await electron.ipcRenderer.send('no-reply-reminder-prompt-edit', { type })
}
// for 跟进话术
const rechatLlmFallbackOptions = [
{
name: '发送“[盼回复]”表情',
@@ -619,6 +757,18 @@ const rechatLlmFallbackOptions = [
}
]
// // for 开场白话术
// const openLlmFallbackOptions = [
// {
// name: '发送固定文案',
// value: OPEN_LLM_FALLBACK.SEND_CONSTANT_CONTENT
// },
// {
// name: '退出已读不回自动复聊',
// value: OPEN_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
// }
// ]
async function handleTestEffectClicked() {
gtagRenderer('goto_mock_chat_clicked')
if (!(await checkIsCanRun())) {
@@ -651,6 +801,16 @@ const handleStopButtonClick = async () => {
isStopButtonLoading.value = false
}
}
const defaultConstantOpenContent = computed(() => {
if (
formContent.value.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join('')
}
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
})
</script>
<style lang="scss">

View File

@@ -22,39 +22,65 @@
<div class="pb20px"></div>
<div v-for="(item, index) in messageList" :key="index" flex flex-col flex-items-end>
<div class="message-item-wrap flex flex-col">
<div
class="message-item"
:class="{
'will-enter-context': getIsEnterContent(index)
}"
>
{{ item.text }}
</div>
<div
:style="{
width: 'fit-content',
alignSelf: 'flex-end'
}"
font-size-10px
>
{{ item.usedLlmConfig.model }}
</div>
<div
v-if="item?.usedLlmConfig?.providerCompleteApiUrl?.trim()"
:style="{
width: 'fit-content',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
alignSelf: 'flex-end',
color: '#bbb'
}"
font-size-10px
w-fit-content
max-w-20em
>
{{ item.usedLlmConfig.providerCompleteApiUrl }}
</div>
<template v-if="item.type === 'text'">
<div
class="message-item"
:class="{
'will-enter-context': getIsEnterContent(index)
}"
>
{{ item.text }}
</div>
</template>
<template v-else>
<div
class="message-item image-message-item"
:class="{
'will-enter-context': getIsEnterContent(index)
}"
>
<img :src="item.imageUrl" alt="" />
</div>
</template>
<!-- eslint-disable-next-line prettier/prettier -->
<template v-if="(typeof item.usedLlmConfig !== 'string')">
<div
:style="{
width: 'fit-content',
alignSelf: 'flex-end'
}"
font-size-10px
>
{{ item.usedLlmConfig.model }}
</div>
<div
v-if="item?.usedLlmConfig?.providerCompleteApiUrl?.trim()"
:style="{
width: 'fit-content',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
alignSelf: 'flex-end',
color: '#bbb'
}"
font-size-10px
w-fit-content
max-w-20em
>
{{ item.usedLlmConfig.providerCompleteApiUrl }}
</div>
</template>
<template v-else>
<div
:style="{
width: 'fit-content',
alignSelf: 'flex-end'
}"
font-size-10px
>
{{ item.usedLlmConfig }}
</div>
</template>
</div>
</div>
<div class="pb20px"></div>
@@ -147,15 +173,27 @@ import { computed, ref, watch } from 'vue'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { ElMessage } from 'element-plus'
import { gtagRenderer } from '@renderer/utils/gtag'
import {
OPEN_CONTENT_SOURCE,
RECHAT_CONTENT_SOURCE
} from '../../../../common/enums/auto-start-chat'
import { DEFAULT_CONSTANT_OPEN_CONTENT_SEGS } from '../../../../common/constant'
import lookForwardReplyEmotion from '../MainLayout/resources/look-forward-reply-emotion.gif'
type MessageItem = {
text: string
usedLlmConfig: string
type: 'text'
// recordInfo: any
}
const messageList = ref<MessageItem[]>([])
type ImageMessageItem = MessageItem & {
type: 'image'
imageUrl: string
}
const messageList = ref<(MessageItem | ImageMessageItem)[]>([])
const searchParams = Object.fromEntries(new URL(location.href).searchParams)
const recentMessageQuantityForLlm =
Number(new URL(location.href).searchParams.get('recentMessageQuantityForLlm')) || 8
const recentMessageQuantityForLlm = Number(searchParams.recentMessageQuantityForLlm) || 8
function getIsEnterContent(index) {
return messageList.value.length - index - 1 < recentMessageQuantityForLlm
}
@@ -188,32 +226,93 @@ watch(
const scrollElRef = ref(null)
const isLoading = ref(false)
const openContentSource = Number(searchParams.openContentSource)
const constantOpenContent = (() => {
if (searchParams.constantOpenContent?.trim()) {
return searchParams.constantOpenContent.trim()
}
if (Number(searchParams.rechatContentSource) === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS.join(``)
} else {
return DEFAULT_CONSTANT_OPEN_CONTENT_SEGS[0]
}
})()
const rechatContentSource = Number(searchParams.rechatContentSource)
async function sendLlmGeneratedContent() {
gtagRenderer('click_mock_chat_send')
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: JSON.parse(JSON.stringify((messageList.value ?? []).slice(-8))),
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
})
console.log(response)
messageList.value.push({
text: response.responseText,
usedLlmConfig: response.usedLlmConfig
})
await sleep(50)
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
top: scrollElRef.value?.scrollHeight,
behavior: 'smooth'
})
} catch (err) {
ElMessage.error({
dangerouslyUseHTMLString: true,
grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
if (!(messageList.value ?? []).length) {
// send open content
if (openContentSource === OPEN_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: [],
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
})
console.log(response)
messageList.value.push({
type: 'text',
text: response.responseText,
usedLlmConfig: response.usedLlmConfig
})
await sleep(50)
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
top: scrollElRef.value?.scrollHeight,
behavior: 'smooth'
})
} catch (err) {
ElMessage.error({
dangerouslyUseHTMLString: true,
grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
}
} else {
messageList.value.push({
type: 'text',
text: constantOpenContent,
usedLlmConfig: '未使用大模型'
})
}
} else {
if (rechatContentSource === RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT) {
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: JSON.parse(JSON.stringify((messageList.value ?? []).slice(-8))),
llmConfigIdForPick: selectedLlmConfig.value ? [selectedLlmConfig.value] : null
})
console.log(response)
messageList.value.push({
type: 'text',
text: response.responseText,
usedLlmConfig: response.usedLlmConfig
})
await sleep(50)
;(scrollElRef.value as any as HTMLDivElement)?.scrollTo({
top: scrollElRef.value?.scrollHeight,
behavior: 'smooth'
})
} catch (err) {
ElMessage.error({
dangerouslyUseHTMLString: true,
grouping: true,
message: `<div>本次测试所使用的模型不可用</div><div style="margin-top: 10px; white-space: nowrap;">建议在大语言模型配置中关闭相关模型</div>`
})
} finally {
isLoading.value = false
}
} else {
messageList.value.push({
type: 'image',
text: `[盼回复] 表情`,
imageUrl: lookForwardReplyEmotion,
usedLlmConfig: '未使用大模型'
})
}
}
}
@@ -264,5 +363,12 @@ gtagRenderer('enter_mock_chat_page')
}
}
}
.message-item.image-message-item {
background-color: transparent;
width: 128px;
img {
width: 100%;
}
}
}
</style>

View File

@@ -22,13 +22,13 @@ export const useUpdateStore = defineStore('update', () => {
export const useTaskManagerStore = defineStore('taskManager', () => {
const runningTasks = ref<unknown[]>([])
function getRunningTasks() {
async function getRunningTasks() {
const { ipcRenderer } = electron
ipcRenderer.invoke('get-task-manager-list').then(res => {
runningTasks.value = res.workers ?? []
})
const res = await ipcRenderer.invoke('get-task-manager-list')
runningTasks.value = res.workers ?? []
}
const throttledGetRunningTasks = throttle(getRunningTasks, 2000)
setInterval(throttledGetRunningTasks, 2 * 1000)
getRunningTasks()
return { runningTasks, getRunningTasks: throttledGetRunningTasks }
})