@joohw/boss-cli 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +25 -25
- package/README.md +36 -16
- package/dist/browser/agent_operating_indicator.d.ts +9 -0
- package/dist/browser/agent_operating_indicator.d.ts.map +1 -0
- package/dist/browser/agent_operating_indicator.js +92 -0
- package/dist/browser/agent_operating_indicator.js.map +1 -0
- package/dist/browser/c_resume_capture.d.ts +11 -0
- package/dist/browser/c_resume_capture.d.ts.map +1 -0
- package/dist/browser/c_resume_capture.js +76 -0
- package/dist/browser/c_resume_capture.js.map +1 -0
- package/dist/browser/chat.d.ts +3 -3
- package/dist/browser/chat.d.ts.map +1 -1
- package/dist/browser/chat.js +20 -4
- package/dist/browser/chat.js.map +1 -1
- package/dist/browser/human_delay.d.ts +5 -0
- package/dist/browser/human_delay.d.ts.map +1 -1
- package/dist/browser/human_delay.js +5 -0
- package/dist/browser/human_delay.js.map +1 -1
- package/dist/browser/index.d.ts +3 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +3 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/viewport_temp.d.ts +15 -0
- package/dist/browser/viewport_temp.d.ts.map +1 -0
- package/dist/browser/viewport_temp.js +78 -0
- package/dist/browser/viewport_temp.js.map +1 -0
- package/dist/cli/banner.d.ts +1 -1
- package/dist/cli/banner.d.ts.map +1 -1
- package/dist/cli/banner.js +40 -1
- package/dist/cli/banner.js.map +1 -1
- package/dist/cli/cliRouter.d.ts.map +1 -1
- package/dist/cli/cliRouter.js +116 -59
- package/dist/cli/cliRouter.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/version.d.ts +7 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +59 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/common/boss_paywall_popup.d.ts +16 -0
- package/dist/common/boss_paywall_popup.d.ts.map +1 -0
- package/dist/common/boss_paywall_popup.js +140 -0
- package/dist/common/boss_paywall_popup.js.map +1 -0
- package/dist/common/boss_sidebar_nav.d.ts +6 -0
- package/dist/common/boss_sidebar_nav.d.ts.map +1 -0
- package/dist/common/boss_sidebar_nav.js +37 -0
- package/dist/common/boss_sidebar_nav.js.map +1 -0
- package/dist/common/c_resume_capture.d.ts +11 -0
- package/dist/common/c_resume_capture.d.ts.map +1 -0
- package/dist/common/c_resume_capture.js +76 -0
- package/dist/common/c_resume_capture.js.map +1 -0
- package/dist/toolset/action.d.ts +1 -1
- package/dist/toolset/action.d.ts.map +1 -1
- package/dist/toolset/action.js +311 -228
- package/dist/toolset/action.js.map +1 -1
- package/dist/toolset/c_resume_capture.d.ts +11 -0
- package/dist/toolset/c_resume_capture.d.ts.map +1 -0
- package/dist/toolset/c_resume_capture.js +76 -0
- package/dist/toolset/c_resume_capture.js.map +1 -0
- package/dist/toolset/chat.d.ts.map +1 -1
- package/dist/toolset/chat.js +4 -3
- package/dist/toolset/chat.js.map +1 -1
- package/dist/toolset/deep-search.d.ts +24 -0
- package/dist/toolset/deep-search.d.ts.map +1 -0
- package/dist/toolset/deep-search.js +1039 -0
- package/dist/toolset/deep-search.js.map +1 -0
- package/dist/toolset/greet.d.ts +2 -0
- package/dist/toolset/greet.d.ts.map +1 -0
- package/dist/toolset/greet.js +47 -0
- package/dist/toolset/greet.js.map +1 -0
- package/dist/toolset/index.d.ts +15 -5
- package/dist/toolset/index.d.ts.map +1 -1
- package/dist/toolset/index.js +19 -10
- package/dist/toolset/index.js.map +1 -1
- package/dist/toolset/jd.d.ts.map +1 -1
- package/dist/toolset/jd.js +29 -35
- package/dist/toolset/jd.js.map +1 -1
- package/dist/toolset/list.d.ts +6 -0
- package/dist/toolset/list.d.ts.map +1 -1
- package/dist/toolset/list.js +36 -60
- package/dist/toolset/list.js.map +1 -1
- package/dist/toolset/login.d.ts.map +1 -1
- package/dist/toolset/login.js +2 -5
- package/dist/toolset/login.js.map +1 -1
- package/dist/toolset/recommend.d.ts +34 -0
- package/dist/toolset/recommend.d.ts.map +1 -0
- package/dist/toolset/recommend.js +445 -0
- package/dist/toolset/recommend.js.map +1 -0
- package/dist/toolset/recommend_greet.d.ts +2 -0
- package/dist/toolset/recommend_greet.d.ts.map +1 -0
- package/dist/toolset/recommend_greet.js +27 -0
- package/dist/toolset/recommend_greet.js.map +1 -0
- package/dist/toolset/search.d.ts +23 -1
- package/dist/toolset/search.d.ts.map +1 -1
- package/dist/toolset/search.js +556 -48
- package/dist/toolset/search.js.map +1 -1
- package/dist/toolset/send.d.ts +0 -6
- package/dist/toolset/send.d.ts.map +1 -1
- package/dist/toolset/send.js +22 -190
- package/dist/toolset/send.js.map +1 -1
- package/package.json +2 -2
- package/skills/README.md +0 -11
- package/skills/boss-cli/SKILL.md +0 -58
package/dist/toolset/action.js
CHANGED
|
@@ -1,41 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { isBossChatIndexUrl, ONLINE_RESUME_IFRAME_APPEAR_MS, ONLINE_RESUME_IFRAME_SETTLE_MS, ONLINE_RESUME_IFRAME_WAIT_MAX_MS, sleepRandom, snapshotBossPageViewport, } from '../browser/index.js';
|
|
3
|
+
import { closeBossPaywallPopupIfPresent, describeBossPaywallPopupIfPresent, waitForCResumeIframeOrPaywall, } from '../common/boss_paywall_popup.js';
|
|
4
|
+
import { captureCResumeIframeToFile, closeCResumePanel, safeResumeScreenshotFileBase, } from '../common/c_resume_capture.js';
|
|
3
5
|
import { ensureAppDataLayout, RESUME_SCREENSHOTS_DIR } from '../config.js';
|
|
4
6
|
import { isResumeOcrEnabled, ocrResumePngToTextFile } from '../ocr/index.js';
|
|
5
7
|
import { runGetCommunicationHistory } from './chat.js';
|
|
6
|
-
/** 在线简历截图前临时拉高的视口高度(CSS px)。可用 `BOSS_RESUME_SCREENSHOT_VIEWPORT_HEIGHT` 覆盖。 */
|
|
7
|
-
const ONLINE_RESUME_SNAPSHOT_VIEWPORT_HEIGHT_PX = 5000;
|
|
8
|
-
function safeResumeFileBase(name) {
|
|
9
|
-
const t = name.replace(/[/\\?%*:|"<>]/g, '_').trim().slice(0, 64);
|
|
10
|
-
return t.length > 0 ? t : 'candidate';
|
|
11
|
-
}
|
|
12
|
-
function viewportForOnlineResumeSnapshot(prev) {
|
|
13
|
-
const envH = Number.parseInt(process.env.BOSS_RESUME_SCREENSHOT_VIEWPORT_HEIGHT?.trim() ?? '', 10);
|
|
14
|
-
const height = Number.isFinite(envH) && envH > 0 ? envH : ONLINE_RESUME_SNAPSHOT_VIEWPORT_HEIGHT_PX;
|
|
15
|
-
const base = defaultViewportFromEnv();
|
|
16
|
-
return {
|
|
17
|
-
width: prev?.width ?? base.width,
|
|
18
|
-
height,
|
|
19
|
-
deviceScaleFactor: prev?.deviceScaleFactor ?? 1,
|
|
20
|
-
isMobile: prev?.isMobile ?? false,
|
|
21
|
-
hasTouch: prev?.hasTouch ?? false,
|
|
22
|
-
isLandscape: prev?.isLandscape ?? false,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function viewportRestoreAfterResumeSnapshot(prev) {
|
|
26
|
-
if (prev) {
|
|
27
|
-
return prev;
|
|
28
|
-
}
|
|
29
|
-
const d = defaultViewportFromEnv();
|
|
30
|
-
return {
|
|
31
|
-
width: d.width,
|
|
32
|
-
height: d.height,
|
|
33
|
-
deviceScaleFactor: 1,
|
|
34
|
-
isMobile: false,
|
|
35
|
-
hasTouch: false,
|
|
36
|
-
isLandscape: false,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
8
|
function ensureInCandidateChat(page, actionLabel) {
|
|
40
9
|
return (async () => {
|
|
41
10
|
const currentUrl = page.url();
|
|
@@ -49,73 +18,235 @@ function ensureInCandidateChat(page, actionLabel) {
|
|
|
49
18
|
})();
|
|
50
19
|
}
|
|
51
20
|
/**
|
|
52
|
-
*
|
|
53
|
-
* 仅点击入口与底部确认按钮,不选择任何原因。
|
|
21
|
+
* 在聊天页右侧操作区执行「不合适」:该入口依赖 hover 后才响应真实点击,先派发 hover 再点一次。
|
|
54
22
|
*/
|
|
55
23
|
async function markCandidateNotFitWithoutReason(page) {
|
|
56
24
|
await ensureInCandidateChat(page, '不合适');
|
|
57
|
-
const
|
|
58
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, "").trim();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
25
|
+
const hovered = (await page.evaluate(`(() => {
|
|
26
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "").trim();
|
|
27
|
+
function fireHover(el) {
|
|
28
|
+
if (!(el instanceof HTMLElement)) return;
|
|
29
|
+
const r = el.getBoundingClientRect();
|
|
30
|
+
const cx = r.left + r.width / 2;
|
|
31
|
+
const cy = r.top + r.height / 2;
|
|
32
|
+
["mouseover", "mouseenter", "mousemove"].forEach((type) => {
|
|
33
|
+
el.dispatchEvent(
|
|
34
|
+
new MouseEvent(type, { bubbles: true, cancelable: true, clientX: cx, clientY: cy, view: window }),
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const roots = Array.from(document.querySelectorAll(".operate-exchange-right .operate-icon-item, .operate-icon-item"));
|
|
39
|
+
const target = roots.find((el) => {
|
|
40
|
+
const t = norm(el.querySelector(".operate-btn")?.textContent || el.textContent || "");
|
|
41
|
+
return t.includes("不合适");
|
|
42
|
+
});
|
|
43
|
+
if (!target) return false;
|
|
44
|
+
target.scrollIntoView({ block: "center", inline: "nearest" });
|
|
45
|
+
fireHover(target);
|
|
46
|
+
const btn = target.querySelector(".operate-btn");
|
|
47
|
+
if (btn instanceof HTMLElement) fireHover(btn);
|
|
48
|
+
return true;
|
|
70
49
|
})()`));
|
|
71
|
-
if (!
|
|
50
|
+
if (!hovered) {
|
|
72
51
|
throw new Error('未找到“不合适”按钮,无法执行操作。');
|
|
73
52
|
}
|
|
74
|
-
await sleepRandom(
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
53
|
+
await sleepRandom(200, 450);
|
|
54
|
+
const clicked = (await page.evaluate(`(() => {
|
|
55
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "").trim();
|
|
56
|
+
const roots = Array.from(document.querySelectorAll(".operate-exchange-right .operate-icon-item, .operate-icon-item"));
|
|
57
|
+
const target = roots.find((el) => {
|
|
58
|
+
const t = norm(el.querySelector(".operate-btn")?.textContent || el.textContent || "");
|
|
59
|
+
return t.includes("不合适");
|
|
60
|
+
});
|
|
61
|
+
if (!target) return false;
|
|
62
|
+
const btn = target.querySelector(".operate-btn");
|
|
63
|
+
const host = btn instanceof HTMLElement ? btn : target;
|
|
64
|
+
host.scrollIntoView({ block: "center", inline: "nearest" });
|
|
65
|
+
host.click();
|
|
66
|
+
return true;
|
|
67
|
+
})()`));
|
|
68
|
+
if (!clicked) {
|
|
69
|
+
throw new Error('未找到“不合适”按钮,无法执行点击。');
|
|
70
|
+
}
|
|
71
|
+
await sleepRandom(320, 780);
|
|
72
|
+
return '已点击「不合适」。';
|
|
73
|
+
}
|
|
74
|
+
/** 在聊天页右侧操作区点击「换微信」。 */
|
|
75
|
+
async function runExchangeWechat(page) {
|
|
76
|
+
await ensureInCandidateChat(page, '换微信');
|
|
77
|
+
const availability = (await page.evaluate(`(() => {
|
|
78
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
79
|
+
const items = Array.from(
|
|
80
|
+
document.querySelectorAll(".operate-exchange-left .operate-icon-item, .operate-icon-item"),
|
|
81
|
+
);
|
|
82
|
+
const target = items.find((el) => norm(el.querySelector(".operate-btn")?.textContent).includes("换微信"));
|
|
83
|
+
if (!target) return { found: false, available: false };
|
|
84
|
+
const btn = target.querySelector(".operate-btn");
|
|
85
|
+
const className = [target.className ?? "", btn?.className ?? ""].join(" ");
|
|
86
|
+
const disabled = /disabled|forbid|ban/i.test(className) || btn?.getAttribute("disabled") !== null;
|
|
87
|
+
return { found: true, available: !disabled };
|
|
88
|
+
})()`));
|
|
89
|
+
if (!availability.found) {
|
|
90
|
+
throw new Error('未找到“换微信”按钮,当前页面可能不支持该操作。');
|
|
91
|
+
}
|
|
92
|
+
if (!availability.available) {
|
|
93
|
+
throw new Error('当前“换微信”按钮不可用,请先确认会话状态是否满足交换条件。');
|
|
94
|
+
}
|
|
95
|
+
await sleepRandom(220, 620);
|
|
96
|
+
const clicked = (await page.evaluate(`(() => {
|
|
97
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
98
|
+
const items = Array.from(
|
|
99
|
+
document.querySelectorAll(".operate-exchange-left .operate-icon-item, .operate-icon-item"),
|
|
100
|
+
);
|
|
101
|
+
const target = items.find((el) => norm(el.querySelector(".operate-btn")?.textContent).includes("换微信"));
|
|
102
|
+
if (!target) return false;
|
|
103
|
+
const btn = target.querySelector(".operate-btn");
|
|
104
|
+
const host = btn instanceof HTMLElement ? btn : target;
|
|
105
|
+
host.scrollIntoView({ block: "center", inline: "nearest" });
|
|
106
|
+
host.click();
|
|
107
|
+
return true;
|
|
108
|
+
})()`));
|
|
109
|
+
if (!clicked) {
|
|
110
|
+
throw new Error('点击“换微信”失败,请确认当前会话是否仍处于可操作状态。');
|
|
111
|
+
}
|
|
112
|
+
await sleepRandom(280, 520);
|
|
113
|
+
await page.waitForFunction(`(() => {
|
|
114
|
+
function isVisible(el) {
|
|
115
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
116
|
+
const st = window.getComputedStyle(el);
|
|
117
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
118
|
+
const r = el.getBoundingClientRect();
|
|
119
|
+
return r.width > 0 && r.height > 0;
|
|
120
|
+
}
|
|
121
|
+
const tips = Array.from(document.querySelectorAll(".exchange-tooltip"));
|
|
122
|
+
for (const tip of tips) {
|
|
123
|
+
if (!isVisible(tip)) continue;
|
|
124
|
+
const raw = (tip.textContent ?? "").replace(/\\s+/g, "");
|
|
125
|
+
if (!raw.includes("交换微信")) continue;
|
|
126
|
+
const primary = tip.querySelector(".btn-box .boss-btn-primary");
|
|
127
|
+
return primary instanceof HTMLElement;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
})()`, { timeout: 12_000 });
|
|
131
|
+
const confirmed = (await page.evaluate(`(() => {
|
|
132
|
+
function isVisible(el) {
|
|
133
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
134
|
+
const st = window.getComputedStyle(el);
|
|
135
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
136
|
+
const r = el.getBoundingClientRect();
|
|
137
|
+
return r.width > 0 && r.height > 0;
|
|
138
|
+
}
|
|
139
|
+
function norm(v) {
|
|
140
|
+
return (v ?? "").replace(/\\s+/g, "").trim();
|
|
141
|
+
}
|
|
142
|
+
const tips = Array.from(document.querySelectorAll(".exchange-tooltip"));
|
|
143
|
+
for (const tip of tips) {
|
|
144
|
+
if (!isVisible(tip)) continue;
|
|
145
|
+
if (!norm(tip.textContent).includes("交换微信")) continue;
|
|
146
|
+
const primary = tip.querySelector(".btn-box .boss-btn-primary.boss-btn, .btn-box .boss-btn-primary");
|
|
147
|
+
if (!(primary instanceof HTMLElement)) continue;
|
|
148
|
+
if (!norm(primary.textContent).includes("确定")) continue;
|
|
149
|
+
primary.scrollIntoView({ block: "center", inline: "nearest" });
|
|
150
|
+
primary.click();
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
113
154
|
})()`));
|
|
114
155
|
if (!confirmed) {
|
|
115
|
-
throw new Error('
|
|
156
|
+
throw new Error('已弹出交换微信确认框,但未点到「确定」按钮。');
|
|
116
157
|
}
|
|
117
|
-
await sleepRandom(320,
|
|
118
|
-
return '
|
|
158
|
+
await sleepRandom(320, 780);
|
|
159
|
+
return '已点击「换微信」并在弹窗中确认。';
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 在聊天页左侧工具栏点击「求简历」,并在确认弹窗中点「确定」。
|
|
163
|
+
* 平台规则:双方需各至少发送一条消息后该入口才可点;否则按钮为禁用态。
|
|
164
|
+
*/
|
|
165
|
+
async function runRequestAttachmentResume(page) {
|
|
166
|
+
await ensureInCandidateChat(page, '求简历');
|
|
167
|
+
const availability = (await page.evaluate(`(() => {
|
|
168
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
169
|
+
const items = Array.from(
|
|
170
|
+
document.querySelectorAll(".operate-exchange-left .operate-icon-item, .operate-icon-item"),
|
|
171
|
+
);
|
|
172
|
+
const target = items.find((el) => norm(el.querySelector(".operate-btn")?.textContent).includes("求简历"));
|
|
173
|
+
if (!target) return { found: false, available: false };
|
|
174
|
+
const btn = target.querySelector(".operate-btn");
|
|
175
|
+
const className = [target.className ?? "", btn?.className ?? ""].join(" ");
|
|
176
|
+
const disabled = /disabled|forbid|ban/i.test(className) || btn?.getAttribute("disabled") !== null;
|
|
177
|
+
return { found: true, available: !disabled };
|
|
178
|
+
})()`));
|
|
179
|
+
if (!availability.found) {
|
|
180
|
+
throw new Error('未找到「求简历」按钮,当前页面可能不支持该操作。');
|
|
181
|
+
}
|
|
182
|
+
if (!availability.available) {
|
|
183
|
+
throw new Error('当前「求简历」不可用。Boss 要求双方各至少发送一条消息后才可以向对方请求附件简历,请先与对方互发消息后再试。');
|
|
184
|
+
}
|
|
185
|
+
await sleepRandom(220, 620);
|
|
186
|
+
const clicked = (await page.evaluate(`(() => {
|
|
187
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
188
|
+
const items = Array.from(
|
|
189
|
+
document.querySelectorAll(".operate-exchange-left .operate-icon-item, .operate-icon-item"),
|
|
190
|
+
);
|
|
191
|
+
const target = items.find((el) => norm(el.querySelector(".operate-btn")?.textContent).includes("求简历"));
|
|
192
|
+
if (!target) return false;
|
|
193
|
+
const btn = target.querySelector(".operate-btn");
|
|
194
|
+
const host = btn instanceof HTMLElement ? btn : target;
|
|
195
|
+
host.scrollIntoView({ block: "center", inline: "nearest" });
|
|
196
|
+
host.click();
|
|
197
|
+
return true;
|
|
198
|
+
})()`));
|
|
199
|
+
if (!clicked) {
|
|
200
|
+
throw new Error('点击「求简历」失败,请确认当前会话是否仍处于可操作状态。');
|
|
201
|
+
}
|
|
202
|
+
await sleepRandom(280, 520);
|
|
203
|
+
await page.waitForFunction(`(() => {
|
|
204
|
+
function isVisible(el) {
|
|
205
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
206
|
+
const st = window.getComputedStyle(el);
|
|
207
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
208
|
+
const r = el.getBoundingClientRect();
|
|
209
|
+
return r.width > 0 && r.height > 0;
|
|
210
|
+
}
|
|
211
|
+
const tips = Array.from(document.querySelectorAll(".exchange-tooltip"));
|
|
212
|
+
for (const tip of tips) {
|
|
213
|
+
if (!isVisible(tip)) continue;
|
|
214
|
+
const raw = (tip.textContent ?? "").replace(/\\s+/g, "");
|
|
215
|
+
if (!raw.includes("请求简历")) continue;
|
|
216
|
+
const primary = tip.querySelector(".btn-box .boss-btn-primary");
|
|
217
|
+
return primary instanceof HTMLElement;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
})()`, { timeout: 12_000 });
|
|
221
|
+
const confirmed = (await page.evaluate(`(() => {
|
|
222
|
+
function isVisible(el) {
|
|
223
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
224
|
+
const st = window.getComputedStyle(el);
|
|
225
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
226
|
+
const r = el.getBoundingClientRect();
|
|
227
|
+
return r.width > 0 && r.height > 0;
|
|
228
|
+
}
|
|
229
|
+
function norm(v) {
|
|
230
|
+
return (v ?? "").replace(/\\s+/g, "").trim();
|
|
231
|
+
}
|
|
232
|
+
const tips = Array.from(document.querySelectorAll(".exchange-tooltip"));
|
|
233
|
+
for (const tip of tips) {
|
|
234
|
+
if (!isVisible(tip)) continue;
|
|
235
|
+
if (!norm(tip.textContent).includes("请求简历")) continue;
|
|
236
|
+
const primary = tip.querySelector(".btn-box .boss-btn-primary.boss-btn, .btn-box .boss-btn-primary");
|
|
237
|
+
if (!(primary instanceof HTMLElement)) continue;
|
|
238
|
+
if (!norm(primary.textContent).includes("确定")) continue;
|
|
239
|
+
primary.scrollIntoView({ block: "center", inline: "nearest" });
|
|
240
|
+
primary.click();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
})()`));
|
|
245
|
+
if (!confirmed) {
|
|
246
|
+
throw new Error('已弹出求简历确认框,但未点到「确定」按钮。');
|
|
247
|
+
}
|
|
248
|
+
await sleepRandom(320, 780);
|
|
249
|
+
return '已点击「求简历」并在弹窗中确认(将发送默认话术「方便发一份你的简历过来吗?」)。';
|
|
119
250
|
}
|
|
120
251
|
/**
|
|
121
252
|
* 在聊天页通过「更多 -> 备注」更新候选人备注,并点击确认保存。
|
|
@@ -129,50 +260,50 @@ async function updateCandidateRemark(page, remarkText) {
|
|
|
129
260
|
throw new Error(`备注内容过长(${nextRemark.length}/120),请缩短后重试。`);
|
|
130
261
|
}
|
|
131
262
|
await ensureInCandidateChat(page, '备注');
|
|
132
|
-
const openedMoreMenu = (await page.evaluate(`(() => {
|
|
133
|
-
function isVisible(el) {
|
|
134
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
135
|
-
const st = window.getComputedStyle(el);
|
|
136
|
-
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
137
|
-
const r = el.getBoundingClientRect();
|
|
138
|
-
return r.width > 0 && r.height > 0;
|
|
139
|
-
}
|
|
140
|
-
const popovers = Array.from(document.querySelectorAll(".rightbar-item .popover"))
|
|
141
|
-
.filter((el) => !!el.querySelector(".popover-wrap.rightbar-more-tooltip"));
|
|
142
|
-
if (popovers.length === 0) return false;
|
|
143
|
-
const popover = popovers[popovers.length - 1];
|
|
144
|
-
const host = popover.querySelector(".icon") || popover;
|
|
145
|
-
host.scrollIntoView({ block: "center", inline: "nearest" });
|
|
146
|
-
host.click();
|
|
147
|
-
const wrap = popover.querySelector(".popover-wrap.rightbar-more-tooltip");
|
|
148
|
-
return !!wrap && isVisible(wrap) || !!wrap;
|
|
263
|
+
const openedMoreMenu = (await page.evaluate(`(() => {
|
|
264
|
+
function isVisible(el) {
|
|
265
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
266
|
+
const st = window.getComputedStyle(el);
|
|
267
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
268
|
+
const r = el.getBoundingClientRect();
|
|
269
|
+
return r.width > 0 && r.height > 0;
|
|
270
|
+
}
|
|
271
|
+
const popovers = Array.from(document.querySelectorAll(".rightbar-item .popover"))
|
|
272
|
+
.filter((el) => !!el.querySelector(".popover-wrap.rightbar-more-tooltip"));
|
|
273
|
+
if (popovers.length === 0) return false;
|
|
274
|
+
const popover = popovers[popovers.length - 1];
|
|
275
|
+
const host = popover.querySelector(".icon") || popover;
|
|
276
|
+
host.scrollIntoView({ block: "center", inline: "nearest" });
|
|
277
|
+
host.click();
|
|
278
|
+
const wrap = popover.querySelector(".popover-wrap.rightbar-more-tooltip");
|
|
279
|
+
return !!wrap && isVisible(wrap) || !!wrap;
|
|
149
280
|
})()`));
|
|
150
281
|
if (!openedMoreMenu) {
|
|
151
282
|
throw new Error('未找到右侧“更多”按钮(rightbar-more),无法打开备注菜单。');
|
|
152
283
|
}
|
|
153
284
|
await sleepRandom(120, 300);
|
|
154
|
-
const clickedRemarkItem = (await page.evaluate(`(() => {
|
|
155
|
-
function norm(v) {
|
|
156
|
-
return (v ?? "").replace(/\\s+/g, "").trim();
|
|
157
|
-
}
|
|
158
|
-
function isVisible(el) {
|
|
159
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
160
|
-
const st = window.getComputedStyle(el);
|
|
161
|
-
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
162
|
-
const r = el.getBoundingClientRect();
|
|
163
|
-
return r.width > 0 && r.height > 0;
|
|
164
|
-
}
|
|
165
|
-
const wraps = Array.from(document.querySelectorAll(".popover-wrap.rightbar-more-tooltip"))
|
|
166
|
-
.filter((el) => isVisible(el));
|
|
167
|
-
if (wraps.length === 0) return false;
|
|
168
|
-
const wrap = wraps[wraps.length - 1];
|
|
169
|
-
const items = Array.from(wrap.querySelectorAll(".more-list .item"))
|
|
170
|
-
.filter((el) => isVisible(el));
|
|
171
|
-
const remark = items.find((el) => norm(el.textContent).includes("备注"));
|
|
172
|
-
if (!remark) return false;
|
|
173
|
-
remark.scrollIntoView({ block: "center", inline: "nearest" });
|
|
174
|
-
remark.click();
|
|
175
|
-
return true;
|
|
285
|
+
const clickedRemarkItem = (await page.evaluate(`(() => {
|
|
286
|
+
function norm(v) {
|
|
287
|
+
return (v ?? "").replace(/\\s+/g, "").trim();
|
|
288
|
+
}
|
|
289
|
+
function isVisible(el) {
|
|
290
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
291
|
+
const st = window.getComputedStyle(el);
|
|
292
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
293
|
+
const r = el.getBoundingClientRect();
|
|
294
|
+
return r.width > 0 && r.height > 0;
|
|
295
|
+
}
|
|
296
|
+
const wraps = Array.from(document.querySelectorAll(".popover-wrap.rightbar-more-tooltip"))
|
|
297
|
+
.filter((el) => isVisible(el));
|
|
298
|
+
if (wraps.length === 0) return false;
|
|
299
|
+
const wrap = wraps[wraps.length - 1];
|
|
300
|
+
const items = Array.from(wrap.querySelectorAll(".more-list .item"))
|
|
301
|
+
.filter((el) => isVisible(el));
|
|
302
|
+
const remark = items.find((el) => norm(el.textContent).includes("备注"));
|
|
303
|
+
if (!remark) return false;
|
|
304
|
+
remark.scrollIntoView({ block: "center", inline: "nearest" });
|
|
305
|
+
remark.click();
|
|
306
|
+
return true;
|
|
176
307
|
})()`));
|
|
177
308
|
if (!clickedRemarkItem) {
|
|
178
309
|
throw new Error('未找到“备注”菜单项,无法打开备注弹窗。');
|
|
@@ -194,36 +325,36 @@ async function updateCandidateRemark(page, remarkText) {
|
|
|
194
325
|
await sleepRandom(120, 260);
|
|
195
326
|
await page.type(textareaSel, nextRemark, { delay: 24 });
|
|
196
327
|
await sleepRandom(200, 360);
|
|
197
|
-
const filledOk = (await page.evaluate(`((selector, expected) => {
|
|
198
|
-
const el = document.querySelector(selector);
|
|
199
|
-
if (!(el instanceof HTMLTextAreaElement)) return false;
|
|
200
|
-
return (el.value ?? "").trim() === expected;
|
|
328
|
+
const filledOk = (await page.evaluate(`((selector, expected) => {
|
|
329
|
+
const el = document.querySelector(selector);
|
|
330
|
+
if (!(el instanceof HTMLTextAreaElement)) return false;
|
|
331
|
+
return (el.value ?? "").trim() === expected;
|
|
201
332
|
})`, textareaSel, nextRemark));
|
|
202
333
|
if (!filledOk) {
|
|
203
334
|
throw new Error('备注输入未生效,请重试。');
|
|
204
335
|
}
|
|
205
|
-
const confirmed = (await page.evaluate(`(() => {
|
|
206
|
-
function norm(v) {
|
|
207
|
-
return (v ?? "").replace(/\\s+/g, "").trim();
|
|
208
|
-
}
|
|
209
|
-
function isVisible(el) {
|
|
210
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
211
|
-
const st = window.getComputedStyle(el);
|
|
212
|
-
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
213
|
-
const r = el.getBoundingClientRect();
|
|
214
|
-
return r.width > 0 && r.height > 0;
|
|
215
|
-
}
|
|
216
|
-
const wrappers = Array.from(document.querySelectorAll(".dialog-default-v2"))
|
|
217
|
-
.filter((el) => isVisible(el) && !!el.querySelector(".dialog-geek-remark"));
|
|
218
|
-
if (wrappers.length === 0) return false;
|
|
219
|
-
const wrapper = wrappers[wrappers.length - 1];
|
|
220
|
-
const buttons = Array.from(wrapper.querySelectorAll(".boss-dialog__footer .boss-dialog__button, .boss-btn"))
|
|
221
|
-
.filter((el) => isVisible(el));
|
|
222
|
-
const confirmBtn = buttons.find((el) => norm(el.textContent).includes("确认"));
|
|
223
|
-
if (!confirmBtn) return false;
|
|
224
|
-
confirmBtn.scrollIntoView({ block: "center", inline: "nearest" });
|
|
225
|
-
confirmBtn.click();
|
|
226
|
-
return true;
|
|
336
|
+
const confirmed = (await page.evaluate(`(() => {
|
|
337
|
+
function norm(v) {
|
|
338
|
+
return (v ?? "").replace(/\\s+/g, "").trim();
|
|
339
|
+
}
|
|
340
|
+
function isVisible(el) {
|
|
341
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
342
|
+
const st = window.getComputedStyle(el);
|
|
343
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
344
|
+
const r = el.getBoundingClientRect();
|
|
345
|
+
return r.width > 0 && r.height > 0;
|
|
346
|
+
}
|
|
347
|
+
const wrappers = Array.from(document.querySelectorAll(".dialog-default-v2"))
|
|
348
|
+
.filter((el) => isVisible(el) && !!el.querySelector(".dialog-geek-remark"));
|
|
349
|
+
if (wrappers.length === 0) return false;
|
|
350
|
+
const wrapper = wrappers[wrappers.length - 1];
|
|
351
|
+
const buttons = Array.from(wrapper.querySelectorAll(".boss-dialog__footer .boss-dialog__button, .boss-btn"))
|
|
352
|
+
.filter((el) => isVisible(el));
|
|
353
|
+
const confirmBtn = buttons.find((el) => norm(el.textContent).includes("确认"));
|
|
354
|
+
if (!confirmBtn) return false;
|
|
355
|
+
confirmBtn.scrollIntoView({ block: "center", inline: "nearest" });
|
|
356
|
+
confirmBtn.click();
|
|
357
|
+
return true;
|
|
227
358
|
})()`));
|
|
228
359
|
if (!confirmed) {
|
|
229
360
|
throw new Error('已填写备注,但未找到“确认”按钮。');
|
|
@@ -231,36 +362,6 @@ async function updateCandidateRemark(page, remarkText) {
|
|
|
231
362
|
await sleepRandom(220, 580);
|
|
232
363
|
return `已更新备注: ${nextRemark}`;
|
|
233
364
|
}
|
|
234
|
-
async function closeOnlineResumePanel(page) {
|
|
235
|
-
try {
|
|
236
|
-
await page.evaluate(() => {
|
|
237
|
-
const wraps = Array.from(document.querySelectorAll('.boss-popup__wrapper'));
|
|
238
|
-
for (const w of wraps) {
|
|
239
|
-
if (w.querySelector('iframe[src*="c-resume"], iframe[src*="frame/c-resume"]')) {
|
|
240
|
-
const c = w.querySelector('.boss-popup__close');
|
|
241
|
-
if (c) {
|
|
242
|
-
c.click();
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const iframe = document.querySelector('iframe[src*="c-resume"], iframe[src*="frame/c-resume"]');
|
|
248
|
-
let node = iframe?.parentElement ?? null;
|
|
249
|
-
for (let i = 0; i < 12 && node; i++) {
|
|
250
|
-
const c = node.querySelector('.boss-popup__close, .drawer-close, .icon-close');
|
|
251
|
-
if (c) {
|
|
252
|
-
c.click();
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
node = node.parentElement;
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
await sleepRandom(200, 450);
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
/* ignore */
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
365
|
/**
|
|
265
366
|
* 对方「附件简历」确认卡片上点击「同意」。
|
|
266
367
|
* 对应按钮 disabled 时视为已处理。
|
|
@@ -325,10 +426,10 @@ async function runIncomingResumeCardAction(page, which) {
|
|
|
325
426
|
: '已点击「拒绝」,拒绝接收对方附件简历。';
|
|
326
427
|
}
|
|
327
428
|
async function getCandidateLabelForResumeShot(page) {
|
|
328
|
-
const name = (await page.evaluate(`(() => {
|
|
329
|
-
const node = document.querySelector(".base-info-single-container .name-box");
|
|
330
|
-
const text = node?.textContent ?? "";
|
|
331
|
-
return text.replace(/\\s+/g, " ").trim();
|
|
429
|
+
const name = (await page.evaluate(`(() => {
|
|
430
|
+
const node = document.querySelector(".base-info-single-container .name-box");
|
|
431
|
+
const text = node?.textContent ?? "";
|
|
432
|
+
return text.replace(/\\s+/g, " ").trim();
|
|
332
433
|
})()`));
|
|
333
434
|
return name || 'candidate';
|
|
334
435
|
}
|
|
@@ -336,11 +437,11 @@ async function getCandidateLabelForResumeShot(page) {
|
|
|
336
437
|
* 点击「在线简历」,对 `iframe` 元素整框截图(含视口外部分,见 `captureBeyondViewport`)。
|
|
337
438
|
* 不依赖 `contentFrame()`,与内页是否 canvas / 跨域无关。
|
|
338
439
|
*
|
|
339
|
-
*
|
|
440
|
+
* 进入前记录视口(`snapshotBossPageViewport`,见 {@link captureCResumeIframeToFile})。
|
|
340
441
|
*/
|
|
341
442
|
async function captureOnlineResumeScreenshot(page, candidateLabel) {
|
|
342
443
|
ensureAppDataLayout();
|
|
343
|
-
const savedViewport = await page
|
|
444
|
+
const savedViewport = await snapshotBossPageViewport(page);
|
|
344
445
|
const opened = await page.evaluate(() => {
|
|
345
446
|
const a = document.querySelector('a.resume-btn-online');
|
|
346
447
|
if (!a || a.classList.contains('disabled'))
|
|
@@ -353,46 +454,24 @@ async function captureOnlineResumeScreenshot(page, candidateLabel) {
|
|
|
353
454
|
return null;
|
|
354
455
|
}
|
|
355
456
|
await sleepRandom(ONLINE_RESUME_IFRAME_APPEAR_MS.min, ONLINE_RESUME_IFRAME_APPEAR_MS.max);
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
457
|
+
const outcome = await waitForCResumeIframeOrPaywall(page, ONLINE_RESUME_IFRAME_WAIT_MAX_MS);
|
|
458
|
+
if (outcome !== 'iframe') {
|
|
459
|
+
const paywall = await describeBossPaywallPopupIfPresent(page);
|
|
460
|
+
await closeBossPaywallPopupIfPresent(page);
|
|
461
|
+
if (paywall) {
|
|
462
|
+
throw new Error(paywall);
|
|
463
|
+
}
|
|
360
464
|
return null;
|
|
361
465
|
}
|
|
362
466
|
await sleepRandom(ONLINE_RESUME_IFRAME_SETTLE_MS.min, ONLINE_RESUME_IFRAME_SETTLE_MS.max);
|
|
363
|
-
const fileName = `online-resume-${
|
|
467
|
+
const fileName = `online-resume-${safeResumeScreenshotFileBase(candidateLabel)}-${Date.now()}.png`;
|
|
364
468
|
const absPath = join(RESUME_SCREENSHOTS_DIR, fileName);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
await
|
|
368
|
-
|
|
369
|
-
if (!iframe) {
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
await iframe.evaluate((el) => {
|
|
373
|
-
el.scrollIntoView({ block: 'start', inline: 'nearest' });
|
|
374
|
-
});
|
|
375
|
-
const box = await iframe.boundingBox();
|
|
376
|
-
if (!box) {
|
|
377
|
-
await iframe.dispose();
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
try {
|
|
381
|
-
await iframe.screenshot({
|
|
382
|
-
path: absPath,
|
|
383
|
-
type: 'png',
|
|
384
|
-
captureBeyondViewport: true,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
finally {
|
|
388
|
-
await iframe.dispose();
|
|
389
|
-
}
|
|
390
|
-
await closeOnlineResumePanel(page);
|
|
391
|
-
return absPath;
|
|
392
|
-
}
|
|
393
|
-
finally {
|
|
394
|
-
await page.setViewport(viewportRestoreAfterResumeSnapshot(savedViewport));
|
|
469
|
+
const ok = await captureCResumeIframeToFile(page, savedViewport, absPath);
|
|
470
|
+
if (!ok) {
|
|
471
|
+
await closeCResumePanel(page);
|
|
472
|
+
return null;
|
|
395
473
|
}
|
|
474
|
+
return absPath;
|
|
396
475
|
}
|
|
397
476
|
export async function runChatActionOnCurrentConversation(page, options) {
|
|
398
477
|
const action = options.action;
|
|
@@ -422,6 +501,10 @@ export async function runChatActionOnCurrentConversation(page, options) {
|
|
|
422
501
|
}
|
|
423
502
|
case 'agree-resume':
|
|
424
503
|
return runIncomingResumeCardAction(page, 'agree');
|
|
504
|
+
case 'request-attachment-resume':
|
|
505
|
+
return runRequestAttachmentResume(page);
|
|
506
|
+
case 'exchange-wechat':
|
|
507
|
+
return runExchangeWechat(page);
|
|
425
508
|
case 'history':
|
|
426
509
|
return runGetCommunicationHistory(page);
|
|
427
510
|
default: {
|