Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion chrome-extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const CONTENT_READY_RETRIES = 12;
const CONTENT_READY_INTERVAL_MS = 250;
const TAB_LOAD_TIMEOUT_MS = 10000;
const DELIVERY_NAVIGATION_TIMEOUT_MS = 15000;
const REQUIRED_BOSS_CONTENT_VERSION = "1.8.19";
const REQUIRED_BOSS_CONTENT_VERSION = "1.8.20";
const REQUIRED_ZHILIAN_CONTENT_VERSION = "1.8.16";
const LOCAL_API_BASE_URLS = ["http://127.0.0.1:6866"];
const BOSS_LOCAL_API_MAX_ATTEMPTS = 3;
Expand Down
2 changes: 1 addition & 1 deletion chrome-extension/boss-content.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(function () {
const EXTENSION_VERSION = "1.8.19";
const EXTENSION_VERSION = "1.8.20";
// Manifest injection and a readiness probe can meet in the same document.
// Reuse its runner instead of leaving the first runner alive without a listener.
if (window.__GET_JOBS_BOSS_CONTENT_VERSION__ === EXTENSION_VERSION) return;
Expand Down
57 changes: 38 additions & 19 deletions chrome-extension/boss-delivery-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const url = new URL(value);
if (url.protocol !== "https:" || !/(^|\.)zhipin\.com$/.test(url.hostname)) return "";
if (/\/(?:verify|captcha)(?:[/.]|$)/i.test(url.pathname)) return "Boss页面出现安全验证,已暂停投递,请手动处理";
if (/\/(?:passport|login)(?:[/.]|$)/i.test(url.pathname)) return "Boss登录状态失效,已暂停投递,请手动登录";
if (/\/(?:passport|login)(?:[/.]|$)|^\/web\/user(?:\/|$)/i.test(url.pathname)) return "Boss登录状态失效,已暂停投递,请手动登录";
} catch {}
return "";
}
Expand All @@ -14,57 +14,76 @@
const common = /permission|Cannot access|站点权限|脚本未就绪|扩展版本|登录|安全验证|验证码/i.test(message);
return { success: false, outcome: "FAILED", evidence: "PRE_ACTION_ERROR",
greetingOutcome: "NOT_SENT", greetingEvidence: "PRE_ACTION_ERROR",
actionStarted: false, haltBatch: common, message,
actionStarted: false, haltBatch: common || error?.code === "BOSS_PAGE_NOT_READY", message,
...(error?.code ? { errorCode: error.code } : {}),
failureType: /安全验证|验证码/.test(message) ? "PLATFORM_VERIFICATION"
: /登录/.test(message) ? "LOGIN_EXPIRED"
: /permission|Cannot access|站点权限/i.test(message) ? "EXTENSION_PERMISSION" : "PRE_ACTION_ERROR" };
}

async function prepare({ chrome, tabId, targetUrl, navigate, ensure, sleep }) {
async function prepare({ chrome, tabId, targetUrl, navigate, ensure, sleep, timeoutMs = 20000, now = Date.now }) {
if (targetUrl) {
try {
const target = new URL(targetUrl);
if (target.protocol !== "https:" || !/(^|\.)zhipin\.com$/.test(target.hostname)
|| !/^\/job_detail\/[^/]+\.html$/.test(target.pathname)) throw new Error();
} catch { return preparationFailure(new Error("目标岗位链接无效,尚未执行投递")); }
}
// Navigate once. Readiness polling must not restart a slow load or erase a
// login/verification redirect. Preflight has no target and only observes.
try {
const current = await chrome.tabs.get(tabId);
const blocked = authenticationBlock(current.url) || authenticationBlock(current.pendingUrl);
if (blocked) throw new Error(blocked);
if (targetUrl) await navigate(tabId, targetUrl);
} catch (error) {
const current = await chrome.tabs.get(tabId).catch(() => null);
const blocked = authenticationBlock(current?.url) || authenticationBlock(current?.pendingUrl);
return preparationFailure(blocked ? new Error(blocked) : error);
}
const deadline = now() + timeoutMs;
let lastError;
for (let attempt = 0; attempt < 3; attempt++) {
do {
try {
if (targetUrl) await navigate(tabId, targetUrl);
const before = await chrome.tabs.get(tabId);
const blocked = authenticationBlock(before.url) || authenticationBlock(before.pendingUrl);
if (blocked) throw new Error(blocked);
// A new tab may still report about:blank, with only pendingUrl populated.
if (before.status === "loading" || (before.pendingUrl && before.pendingUrl !== before.url)) {
throw new Error("Boss页面仍在加载,尚未执行投递");
}
const url = new URL(before.url || "");
if (url.protocol !== "https:" || !(url.hostname === "zhipin.com" || url.hostname.endsWith(".zhipin.com"))) {
throw new Error("Boss页面尚未就绪,请打开已登录的Boss岗位页面");
throw new Error("Boss页面尚未就绪,尚未执行投递");
}
const blocked = authenticationBlock(url.href);
if (blocked) throw new Error(blocked);
if (targetUrl && url.pathname !== new URL(targetUrl).pathname) throw new Error("目标岗位ID与当前页面不一致,尚未执行投递");
if (before.status === "loading" || (before.pendingUrl && before.pendingUrl !== before.url)) {
throw new Error("Boss页面仍在跳转,尚未执行投递");
}
if (!await chrome.permissions.contains({ origins: [`${url.origin}/*`] })) {
throw new Error(`Chrome扩展缺少站点权限:${url.origin},请在扩展设置中允许访问该站点`);
if (!await chrome.permissions.contains({ origins: [url.origin + "/*"] })) {
throw new Error("Chrome扩展缺少站点权限:" + url.origin + ",请在扩展设置中允许访问该站点");
}
await ensure(tabId);
const status = await chrome.tabs.sendMessage(tabId, { source: "GET_JOBS_BACKGROUND", type: "BOSS_PAGE_STATUS" });
if (!status?.chromePageReady || !status?.isLoggedIn) throw new Error(status?.message || "Boss页面登录状态未确认");
if (status?.hasSecurityPrompt || status?.hasLoginPrompt) throw new Error(
(status.hasSecurityPrompt ? "Boss页面出现安全验证" : "Boss登录状态失效") + (status.message ? ":" + status.message : ""));
if (!status?.chromePageReady || !status?.isLoggedIn) throw new Error(status?.message || "Boss页面尚未就绪");
const after = await chrome.tabs.get(tabId);
if (after.url !== before.url || after.status === "loading" || (after.pendingUrl && after.pendingUrl !== after.url)) {
throw new Error("Boss页面在准备过程中发生跳转,尚未执行投递");
}
if (status.currentUrl && status.currentUrl !== after.url) throw new Error("Boss页面回执来自旧页面,尚未执行投递");
return { success: true, actionStarted: false, tabId, currentUrl: after.url };
} catch (error) {
lastError = error;
if (/站点权限|登录|安全验证|验证码/.test(error?.message || "")) break;
// Navigation may time out before yielding a verification redirect. Preserve that
// page instead of navigating to the job again on the next preparation attempt.
if (/站点权限|permission|Cannot access|登录|安全验证|验证码|版本不一致|目标岗位ID/i.test(error?.message || "")) break;
const current = await chrome.tabs.get(tabId).catch(() => null);
const blocked = authenticationBlock(current?.url) || authenticationBlock(current?.pendingUrl);
if (blocked) { lastError = new Error(blocked); break; }
if (attempt < 2) await sleep(500);
if (!current || now() >= deadline) break;
await sleep(Math.min(250, deadline - now()));
}
}
} while (now() < deadline);
if (now() >= deadline) lastError = Object.assign(new Error(
"Boss页面等待 " + Math.ceil(timeoutMs / 1000) + " 秒后仍未就绪,尚未执行投递。" + (lastError?.message || "")
), { code: "BOSS_PAGE_NOT_READY" });
return preparationFailure(lastError);
}
const api = { prepare, preparationFailure };
Expand Down
2 changes: 1 addition & 1 deletion chrome-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "投递牛马 Chrome Bridge",
"version": "1.8.19",
"version": "1.8.20",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzzdIlNVOv76Y/cSWrjD5Tg2Vlsha8yWHzsn46PBsg724/2dftOUzIIr2n70VRaRgGwEd8FjO/Y768Ori443zF4pQpWvuxXxm05YO25ILQ/+aJLmUycAEdWbkdhcagr4YXnXJdYlSCGSAToSQBjk+owQOdlBLQn5wofPoshrqayoJjRQ5aAUj1SuSlnNv9iimle8GMA1IaA1l5rw6K/chfcgwMTg6HxRAIoludt5JGbIBryi2Lu1hOJRMaDnL7A57ofBnn3qx3H2HIGWGkkTW9EMkls0XMXwx8+mJVIj5HSYl0EeuCvEoTa1W3i1CbOf3kY2yCPKS3Qz3lOvJiwJ4ZQIDAQAB",
"description": "Use signed-in Chrome tabs to scan jobs, confirm deliveries, and review BOSS HR reply drafts for 投递牛马.",
"icons": {
Expand Down
74 changes: 65 additions & 9 deletions chrome-extension/tests/boss-delivery-preparation.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ const { prepare } = require('../boss-delivery-support.js');
const url = 'https://www.zhipin.com/job_detail/abc.html';
function harness(overrides = {}) {
let injections = 0, sends = 0, navigations = 0;
const options = { tabId: 1, targetUrl: url, sleep: async () => {},
let elapsed = 0;
const options = { tabId: 1, targetUrl: url, now: () => elapsed, sleep: async ms => { elapsed += ms; },
navigate: async () => { navigations++; }, ensure: async () => { injections++; },
chrome: { permissions: { contains: async () => true }, tabs: {
get: async () => ({ id: 1, url, status: 'complete' }),
sendMessage: async (_, message) => { assert.equal(message.type, 'BOSS_PAGE_STATUS'); sends++; return { chromePageReady: true, isLoggedIn: true }; }
} } };
overrides.configure?.(options);
return { run: () => prepare(options), counts: () => ({ injections, sends, navigations }) };
return { run: () => prepare(options), counts: () => ({ injections, sends, navigations }), elapsed: () => elapsed };
}
test('permission denial occurs before any delivery action and halts batch', async () => {
const h = harness({ configure: o => { o.chrome.permissions.contains = async () => false; } });
Expand All @@ -24,10 +25,14 @@ test('the screenshot injection error is not UNKNOWN', async () => {
const h = harness({ configure: o => { o.ensure = async () => { throw new Error('Cannot access contents of the page. Extension manifest must request permission to access the respective host.'); }; } });
const r = await h.run(); assert.equal(r.outcome, 'FAILED'); assert.equal(r.actionStarted, false); assert.equal(r.haltBatch, true);
});
test('loading page is retried twice without injecting or sending', async () => {
test('loading page has a bounded wait without injecting, sending or repeated navigation', async () => {
const h = harness({ configure: o => { o.chrome.tabs.get = async () => ({ url, status: 'loading' }); } });
assert.equal((await h.run()).actionStarted, false);
assert.deepEqual(h.counts(), { injections: 0, sends: 0, navigations: 3 });
const result = await h.run();
assert.equal(result.actionStarted, false);
assert.equal(result.errorCode, 'BOSS_PAGE_NOT_READY');
assert.equal(result.haltBatch, true);
assert.equal(h.elapsed(), 20000);
assert.deepEqual(h.counts(), { injections: 0, sends: 0, navigations: 1 });
});
test('navigation during script readiness does not pass preparation', async () => {
let n = 0;
Expand All @@ -39,28 +44,79 @@ test('ready page only probes status and never sends a delivery command', async (
});

test('verification and login redirects halt immediately without repeated navigation', async () => {
for (const [path, type] of [['/web/passport/zp/verify.html', 'PLATFORM_VERIFICATION'], ['/web/passport/login', 'LOGIN_EXPIRED']]) {
for (const [path, type] of [['/web/passport/zp/verify.html', 'PLATFORM_VERIFICATION'], ['/web/passport/login', 'LOGIN_EXPIRED'], ['/web/user/', 'LOGIN_EXPIRED']]) {
const h = harness({ configure: o => { o.chrome.tabs.get = async () => ({ url: 'https://www.zhipin.com' + path, status: 'complete' }); } });
const result = await h.run();
assert.equal(result.failureType, type);
assert.equal(result.haltBatch, true);
assert.equal(result.actionStarted, false);
assert.deepEqual(h.counts(), { injections: 0, sends: 0, navigations: 1 });
assert.deepEqual(h.counts(), { injections: 0, sends: 0, navigations: 0 });
}
});

test('a navigation timeout on verification preserves the page without retrying navigation', async () => {
const h = harness({ configure: o => {
let redirected = false;
const navigate = o.navigate;
o.navigate = async () => { await navigate(); throw new Error('岗位导航超时'); };
o.chrome.tabs.get = async () => ({url:'https://www.zhipin.com/web/passport/zp/verify.html',status:'complete'});
o.navigate = async () => { await navigate(); redirected = true; throw new Error('岗位导航超时'); };
o.chrome.tabs.get = async () => ({url:redirected?'https://www.zhipin.com/web/passport/zp/verify.html':url,status:'complete'});
} });
const result = await h.run();
assert.equal(result.haltBatch, true);
assert.equal(result.failureType, 'PLATFORM_VERIFICATION');
assert.deepEqual(h.counts(), {injections:0,sends:0,navigations:1});
});

test('cold preflight waits more than one second for the committed page without navigation', async () => {
const h = harness({ configure: o => {
delete o.targetUrl;
o.chrome.tabs.get = async () => o.now() < 3500
? {url:'about:blank',pendingUrl:'https://www.zhipin.com/',status:'loading'}
: {url:'https://www.zhipin.com/',status:'complete'};
} });
assert.equal((await h.run()).success,true);
assert.equal(h.elapsed(),3500);
assert.deepEqual(h.counts(),{injections:1,sends:1,navigations:0});
});

test('pending login redirect is preserved even when the committed job still looks ready', async () => {
const h = harness({ configure: o => { o.chrome.tabs.get = async () => ({url,pendingUrl:'https://www.zhipin.com/web/user/',status:'loading'}); } });
assert.equal((await h.run()).failureType,'LOGIN_EXPIRED');
assert.deepEqual(h.counts(),{injections:0,sends:0,navigations:0});
});

test('a newly created blank tab without pendingUrl waits instead of being classified as logged out', async () => {
const h = harness({ configure: o => {
delete o.targetUrl;
o.chrome.tabs.get = async () => ({url:o.now()<1250?'about:blank':url,status:'complete'});
} });
assert.equal((await h.run()).success,true);
assert.equal(h.elapsed(),1250);
assert.deepEqual(h.counts(),{injections:1,sends:1,navigations:0});
});

test('explicit login and security status halt immediately even with a generic page message', async () => {
for (const [flag, type] of [['hasLoginPrompt', 'LOGIN_EXPIRED'], ['hasSecurityPrompt', 'PLATFORM_VERIFICATION']]) {
const h = harness({ configure: o => {
o.chrome.tabs.sendMessage = async () => ({[flag]:true,message:'请稍后再试'});
} });
const result = await h.run();
assert.equal(result.failureType,type);
assert.equal(result.actionStarted,false);
assert.equal(result.haltBatch,true);
assert.equal(h.elapsed(),0);
}
});

test('readiness from an old document is rejected until its current URL matches', async () => {
const h = harness({ configure: o => {
o.chrome.tabs.sendMessage = async () => ({chromePageReady:true,isLoggedIn:true,currentUrl:o.now()<1500?url.replace('abc','other'):url});
} });
assert.equal((await h.run()).success,true);
assert.equal(h.elapsed(),1500);
assert.equal(h.counts().navigations,1);
});

test('rejects an unsupported target before navigating and rejects a changed job ID', async () => {
const invalid = harness({ configure: o => { o.targetUrl = 'https://example.com/job_detail/abc.html'; } });
assert.equal((await invalid.run()).evidence, 'PRE_ACTION_ERROR');
Expand Down
2 changes: 1 addition & 1 deletion chrome-extension/tests/boss-hr-assistant.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test("manifest loads the direct HR bridge and one-minute alarm capability", () =
const bossScripts = manifest.content_scripts.find((entry) => entry.matches.includes("https://www.zhipin.com/*")).js;
assert.deepEqual(bossScripts.slice(-3), ["boss-hr-support.js", "boss-hr-bridge.js", "boss-hr-assistant.js"]);
assert.ok(manifest.permissions.includes("alarms"));
assert.equal(manifest.version, "1.8.19");
assert.equal(manifest.version, "1.8.20");
});

test("assistant exposes policy-gated dedicated watch and preserves explicit manual send", () => {
Expand Down
2 changes: 1 addition & 1 deletion chrome-extension/tests/manifest-id.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function extensionIdFromKey(key) {
test('manifest public key derives the backend allowlisted extension id', () => {
const manifestPath = path.join(__dirname, '..', 'manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
assert.equal(manifest.version, '1.8.19');
assert.equal(manifest.version, '1.8.20');
assert.equal(extensionIdFromKey(manifest.key), EXPECTED_EXTENSION_ID);

const publicKey = crypto.createPublicKey({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ test("extension release and both content scripts use the profile-scoped contract
const boss = source("boss-content.js");
const zhilian = source("zhilian-content.js");

assert.equal(manifest.version, "1.8.19");
assert.equal(manifest.version, "1.8.20");
assert.match(background, /BACKGROUND_VERSION = "2026-09-14-runtime-adapters"/);
assert.match(background, /REQUIRED_BOSS_CONTENT_VERSION = "1.8.19"/);
assert.match(boss, /EXTENSION_VERSION = "1.8.19"/);
assert.match(background, /REQUIRED_BOSS_CONTENT_VERSION = "1.8.20"/);
assert.match(boss, /EXTENSION_VERSION = "1.8.20"/);
assert.match(zhilian, /EXTENSION_VERSION = "1.8.16"/);
assert.match(background, /REQUIRED_ZHILIAN_CONTENT_VERSION = "1.8.16"/);
const frontendBridge = fs.readFileSync(path.resolve(extensionDir, "../front/lib/chromeBridge.ts"), "utf8");
Expand Down
4 changes: 3 additions & 1 deletion docs/delivery-recovery.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# BOSS / 智联投递恢复

扩展版本 1.8.19,后台与页面协议 `2026-09-14-runtime-adapters`。
扩展版本 1.8.20,后台与页面协议 `2026-09-14-runtime-adapters`。

- BOSS 预检对新建标签页最多等待 20 秒,等待已提交 URL、页面加载及脚本回执同时就绪,避免冷启动超过 1 秒就误报“页面仍在跳转”。岗位导航仅执行一次,重试只读就绪检查;登录、安全验证和权限不足立即暂停,超时仍不触发投递。
- 隔离 Chromium 回归覆盖真实 MV3 后台和页面脚本的完整流程:延迟首页加载、预检、岗位导航、跨文档进入聊天、多行话术只发送一次和结果记录。招聘页面及 API 均为离线合成响应,不代表真实账号投递验收。
- BOSS 富文本输入框保留确认话术中的真实换行,不再用 textContent 覆盖 innerText 生成的换行结构;发送前仍逐字核对原话术。
- BOSS 点击操作只触发一次 click,避免先派发 click 再调用 `.click()` 导致收藏切换两次、重复打开沟通入口或重复发送话术。
- 页面进入前进/后退缓存(BFCache)时立即撤销旧页面执行资格;返回页面后重新注入脚本,旧异步任务不得继续点击或填写话术。
Expand Down
Loading
Loading