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
11 changes: 7 additions & 4 deletions chrome-extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,13 @@ chrome.runtime.onInstalled?.addListener?.(() => {

const scanObserver = GetJobsScanObserver.create({storage:chrome.storage.local, request:requestLocalApi, version:chrome.runtime.getManifest().version});
const bossHrHost = GetJobsBossHrHost.create({ chrome, request: requestLocalApi,
ensureContent: async tabId => {
await ensureContentScript(tabId, "boss-content.js");
await chrome.scripting.executeScript({ target:{tabId}, world:"MAIN", files:["boss-hr-identity.js"] });
} });
ensureContent: ensureBossHrContent });
async function ensureBossHrContent(tabId) {
// HR has its own idempotent builds. Reconnect them in place after an extension
// update without restarting the unrelated job scanner or reloading the page.
await chrome.scripting.executeScript({ target:{tabId}, world:"MAIN", files:["boss-hr-identity.js"] });
await chrome.scripting.executeScript({ target:{tabId}, files:["boss-hr-support.js","boss-hr-bridge.js","boss-hr-assistant.js"] });
}
bossHrHost.initialize().catch(()=>{});
chrome.runtime.onStartup?.addListener?.(()=>bossHrHost.initialize(true).catch(()=>{}));
chrome.storage.local.get('__GET_JOBS_SCAN_OBSERVER_V1__').then(data=>{if(Object.keys(data.__GET_JOBS_SCAN_OBSERVER_V1__||{}).length) chrome.alarms?.create?.('scan-observer-sync',{periodInMinutes:0.5});}).catch(()=>{});
Expand Down
2 changes: 1 addition & 1 deletion chrome-extension/boss-hr-assistant.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(function () {
"use strict";

const PANEL_VERSION = "2026-10-08-hr-background-ui-v4";
const PANEL_VERSION = "2026-10-20-hr-background-ui-v5";
if (window.top !== window.self || window.__GET_JOBS_BOSS_HR_ASSISTANT__ === PANEL_VERSION) return;
window.__GET_JOBS_BOSS_HR_ASSISTANT_CLEANUP__?.();
window.__GET_JOBS_BOSS_HR_ASSISTANT__ = PANEL_VERSION;
Expand Down
48 changes: 32 additions & 16 deletions chrome-extension/boss-hr-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"use strict";

const CONTENT_VERSION = "2026-09-30-hr-background-v1";
const SCRIPT_BUILD = "2026-10-11-hr-read-recovery-v1";
const SCRIPT_BUILD = "2026-10-20-hr-send-readiness-v1";
if (window.top !== window.self || window.__GET_JOBS_BOSS_HR_BRIDGE__ === SCRIPT_BUILD) return;
window.__GET_JOBS_BOSS_HR_BRIDGE__ = SCRIPT_BUILD;
const support = globalThis.GetJobsBossHrSupport;
Expand Down Expand Up @@ -598,9 +598,10 @@
return { success: true, outcome: "FAILED_SAFE", evidence: "输入框内容复核失败" };
}

// Let the reactive editor update its native send control before checking readiness.
await wait(50);
const sendButtons=findTextSendControls(input).filter(sendControlEnabled);
// Background rendering can take longer than one frame and replace the editor.
// Only observe readiness here; never rewrite a changed draft or retry a send.
const readiness=await waitForTextSendControl(command);
if (!readiness.button) return {success:true,outcome:readiness.stale?"STALE":"FAILED_SAFE",evidence:readiness.evidence};
if (Date.now() >= command.deadlineAt) return { success: true, outcome: "FAILED_SAFE", evidence: "发送命令已过期" };
await guard();
const recheck=support.currentSession(document,{uid:command.uid});
Expand All @@ -611,18 +612,10 @@
|| (command.expectedInboundRound?.length && (finalRound.length!==command.expectedInboundRound.length || finalRound.some((m,i)=>!support.messagesMatch(m,command.expectedInboundRound[i])))))
return {success:true,outcome:"STALE",evidence:"点击发送前会话或消息发生变化"};
if(Date.now()>=command.deadlineAt) return {success:true,outcome:"FAILED_SAFE",evidence:"授权复核后租约已过期"};
let dispatched = false;
if (sendButtons.length === 1) {
sendDispatched=true;
sendButtons[0].click();
dispatched = true;
} else if (sendButtons.length === 0) {
return { success: true, outcome: "FAILED_SAFE", evidence: "真实发送按钮不可用,未触发发送动作" };
} else {
return { success: true, outcome: "FAILED_SAFE", evidence: "发送按钮命中多个候选项" };
}

if (!dispatched) return { success: true, outcome: "FAILED_SAFE", evidence: "未触发发送动作" };
const finalControl=textSendState(command.draft);
if (!finalControl.button) return {success:true,outcome:finalControl.stale?"STALE":"FAILED_SAFE",evidence:finalControl.evidence};
sendDispatched=true;
finalControl.button.click();
const confirmed = await waitForOutbound(command.draft, before.length);
return confirmed
? { success: true, outcome: "SENT", evidence: "DOM_OUTBOUND_EXACT_MATCH", observedLatestInbound: latest }
Expand Down Expand Up @@ -831,6 +824,29 @@
.filter(element=>visible(element) && /^发送(?:[((]Enter[))])?$/.test(support.normalizeText(element.textContent)));
}

function textSendState(draft) {
const inputs=Array.from(document.querySelectorAll("#chat-input,textarea,[contenteditable='true']")).filter(visible);
if(inputs.length!==1) return {evidence:"聊天输入框未唯一命中,未触发发送动作"};
const input=inputs[0];
if(support.normalizeText(inputValue(input))!==support.normalizeText(draft))
return {stale:true,evidence:"等待发送按钮时草稿已变化,保留当前输入,未触发发送动作"};
const controls=findTextSendControls(input);
if(controls.length>1) return {ambiguous:true,evidence:"发送按钮命中多个候选项"};
const button=controls.length===1 && sendControlEnabled(controls[0]) ? controls[0] : null;
return {button,evidence:"真实发送按钮不可用,未触发发送动作"};
}

async function waitForTextSendControl(command) {
const deadline=Math.min(command.deadlineAt,Date.now()+2500);
for(;;) {
await guard();
const state=textSendState(command.draft);
const remaining=deadline-Date.now();
if(state.button || state.stale || state.ambiguous || remaining<=0) return state;
await wait(Math.min(100,remaining));
}
}

function sendControlEnabled(element) {
return !element.disabled && element.getAttribute("aria-disabled")!=="true"
&& !element.hasAttribute("disabled") && !element.classList.contains("disabled")
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.10.16",
"version": "1.10.17",
"minimum_chrome_version": "132",
"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 投递牛马.",
Expand Down
12 changes: 12 additions & 0 deletions chrome-extension/tests/background-tab-routing.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ test('overlay pause changes the persistent background host instead of the legacy
const BOSS_CONTENT_VERSION = readContentVersion("boss-content.js");
const ZHILIAN_CONTENT_VERSION = readContentVersion("zhilian-content.js");

test('HR reconnects its own scripts on the existing tab without the legacy scanner or page navigation',async()=>{
const h=loadBackground({tabs:[{id:7,windowId:8,url:'https://www.zhipin.com/web/geek/chat?getjobs-autopilot=1',status:'complete'}],contentReady:false});
h.context.ensureContentScript=async()=>{throw new Error('legacy content context is unavailable after extension update');};
await h.context.ensureBossHrContent(7);
assert.deepEqual(h.executedScripts.map(item=>({tab:item.target.tabId,world:item.world,files:Array.from(item.files)})),[
{tab:7,world:'MAIN',files:['boss-hr-identity.js']},
{tab:7,world:undefined,files:['boss-hr-support.js','boss-hr-bridge.js','boss-hr-assistant.js']}
]);
assert.equal(h.tabUpdates.length,0);assert.equal(h.windowUpdates.length,0);assert.equal(h.windowCreates.length,0);
assert.equal(h.tabList.length,1);
});

function jsonResponse(body, { ok = true, status = 200 } = {}) {
return {
ok,
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.10.16");
assert.equal(manifest.version, "1.10.17");
});

test("assistant uses the shared background host and preserves explicit manual send", () => {
Expand Down
19 changes: 15 additions & 4 deletions chrome-extension/tests/boss-hr-panel.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class Node {
attachShadow(){this.shadow=new Node('shadow');return this.shadow;}
}
const flush=()=>new Promise(resolve=>setImmediate(resolve));
async function harness({mismatch=false,hostState={},statusState={},policyState={}}={}){
async function harness({mismatch=false,hostState={},statusState={},policyState={},previousBuild}={}){
const document={documentElement:new Node('html'),getElementById:()=>null,createElement:tag=>new Node(tag)};
let timer,fail=false,held=new Set(),clock=0,timeoutId=0;
let timer,fail=false,held=new Set(),clock=0,timeoutId=0,oldCleanups=0;
const operations=[],pending=[],timeouts=new Map();
const binding={watchSessionId:'watch',hostGeneration:'generation',pageDocumentId:'document'};
const host={transport:'CHROME_BACKGROUND',state:'RUNNING',intentEnabled:true,profileId:4,...binding,...hostState};
Expand All @@ -30,14 +30,25 @@ async function harness({mismatch=false,hostState={},statusState={},policyState={
const window={setInterval:fn=>{timer=fn;return 1;},clearInterval:()=>{},
setTimeout:(fn,ms)=>{const id=++timeoutId;timeouts.set(id,{fn,at:clock+ms});return id;},clearTimeout:id=>timeouts.delete(id)};
window.top=window;window.self=window;
vm.runInNewContext(fs.readFileSync(require.resolve('../boss-hr-assistant.js'),'utf8'),{document,window,chrome:{runtime},location:{pathname:'/web/geek/chat'},Date});
if(previousBuild){window.__GET_JOBS_BOSS_HR_ASSISTANT__=previousBuild;window.__GET_JOBS_BOSS_HR_ASSISTANT_CLEANUP__=()=>oldCleanups++;}
const source=fs.readFileSync(require.resolve('../boss-hr-assistant.js'),'utf8');
const context=vm.createContext({document,window,chrome:{runtime},location:{pathname:'/web/geek/chat'},Date});
vm.runInContext(source,context);
await flush();const root=document.documentElement.childNodes[0].shadow;
const nodes=()=>{const result=[];const walk=node=>{result.push(node);node.childNodes.forEach(walk);};walk(root);return result;};
return {nodes,operations,tick:async()=>{timer();await flush();},fail:()=>{fail=true;},
return {nodes,operations,reinject:()=>vm.runInContext(source,context),oldCleanups:()=>oldCleanups,panels:()=>document.documentElement.childNodes.length,
tick:async()=>{timer();await flush();},fail:()=>{fail=true;},
hold:operations=>{held=new Set(operations);},setHost:value=>Object.assign(host,value),setStatus:value=>Object.assign(status,value),
release:async response=>{pending.splice(0).forEach(item=>item.reply(response || item.response));await flush();},
expire:async ms=>{clock+=ms;for(const [id,timeout] of timeouts)if(timeout.at<=clock){timeouts.delete(id);timeout.fn();}await flush();}};
}

test('an extension update replaces the prior panel connection in place and repeated injection stays single',async()=>{
const h=await harness({previousBuild:'2026-10-08-hr-background-ui-v4'});
assert.equal(h.oldCleanups(),1);assert.equal(h.panels(),1);
assert.ok(h.nodes().some(node=>node.textContent==='后台托管:运行中'));
h.reinject();await flush();assert.equal(h.oldCleanups(),1);assert.equal(h.panels(),1);
});
test('panel shows the saved 15 day policy and shared status, with collapsed records and same-host pause',async()=>{
const h=await harness();
assert.ok(h.nodes().some(node=>node.textContent==='后台托管:运行中'));
Expand Down
92 changes: 92 additions & 0 deletions chrome-extension/tests/boss-hr-send-readiness.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const test=require('node:test');
const assert=require('node:assert/strict');
const vm=require('node:vm');
const {readFileSync}=require('node:fs');
const source=readFileSync(require.resolve('../boss-hr-bridge.js'),'utf8');

function harness({readyAt=350,replaceAt=Infinity,changeDraftAt=Infinity,ambiguousAt=Infinity,pauseAt=Infinity,changeRoundAt=Infinity,disableOnFinalGuard=false}={}) {
let clock=1000,clicks=0,writtenAt=null,readyObserved=false,finalDisabled=false;
const inbound={from:'对方',type:'文本',text:'合成问候',time:'今天',messageId:'inbound'};
const messages=[inbound];
const normalize=value=>String(value??'').trim();
const button={textContent:'发送',classList:{contains:()=>false},getAttribute:()=>null,hasAttribute:()=>false,
getBoundingClientRect:()=>({width:60,height:28}),click:()=>{clicks++;messages.push({from:'本人',type:'文本',text:'合成回复',messageId:'outbound'});}};
Object.defineProperty(button,'disabled',{get:()=>{
const disabled=writtenAt===null || clock-writtenAt<readyAt || finalDisabled;
if(!disabled)readyObserved=true;
return disabled;
}});
const makeInput=()=>({textContent:'',focus:()=>{},dispatchEvent:()=>{if(writtenAt===null)writtenAt=clock;},
getBoundingClientRect:()=>({width:600,height:80}),closest(){return {querySelectorAll:()=>this===input?(clock-writtenAt>=ambiguousAt?[button,{...button}]:[button]):[]};}});
let input=makeInput(),replaced=false;
const advance=ms=>{
clock+=ms;
if(writtenAt!==null && clock-writtenAt>=replaceAt && !replaced){const text=input.textContent;input=makeInput();input.textContent=text;replaced=true;}
if(writtenAt!==null && clock-writtenAt>=changeDraftAt)input.textContent='本人未发送草稿';
if(writtenAt!==null && clock-writtenAt>=changeRoundAt)messages.splice(0,messages.length,{...inbound,text:'新的HR提问'});
};
const document={querySelectorAll:()=>[input]};
const window={addEventListener:()=>{}};window.top=window;window.self=window;
class ClockDate extends Date {static now(){return clock;}}
const support={normalizeText:normalize,pageSafety:()=>({safe:true}),currentSession:()=>({uid:'uid',title:'合成HR',observedCompanyName:'合成公司'}),
readMessages:()=>messages.map(x=>({...x})),latestInbound:rows=>rows.findLast(x=>x.from==='对方'),messagesMatch:(a,b)=>a?.text===b?.text};
const code=source.replace(/\}\)\(\);\s*$/,`window.test={executeSend};
guard=async()=>hooks.guard();wait=async ms=>hooks.wait(ms);
locateByUid=async()=>({matches:[{}],unique:{}});openConversation=async()=>({success:true});
readContext=async()=>({messages:GetJobsBossHrSupport.readMessages(),complete:true});})();`);
vm.runInNewContext(code,{window,document,Date:ClockDate,location:{pathname:'/web/geek/chat'},
crypto:{randomUUID:()=> 'document'},sessionStorage:{getItem:()=>null},
HTMLTextAreaElement:class {},HTMLInputElement:class {},InputEvent:class {},Event:class {},
getComputedStyle:()=>({display:'block',visibility:'visible'}),GetJobsBossHrSupport:support,
chrome:{runtime:{onMessage:{addListener:()=>{}}}},hooks:{wait:advance,guard:()=>{
if(writtenAt!==null && clock-writtenAt>=pauseAt)throw new Error('USER_PAUSED');
if(disableOnFinalGuard && readyObserved)finalDisabled=true;
}}});
return {send:async(deadline=20000)=>window.test.executeSend({commandId:'command',uid:'uid',draft:'合成回复',hrName:'合成HR',companyName:'合成公司',
expectedLatestInbound:inbound,expectedInboundRound:[inbound],deadlineAt:1000+deadline}),
clicks:()=>clicks,draft:()=>input.textContent,elapsed:()=>clock-1000,replaced:()=>replaced};
}

test('waits for delayed editor readiness and clicks once after the native control enables',async()=>{
const h=harness();const result=await h.send();
assert.equal(result.outcome,'SENT');assert.equal(h.clicks(),1);assert.ok(h.elapsed()>=350);
});

test('reacquires an editor replaced during reactive rendering without rewriting its draft',async()=>{
const h=harness({replaceAt:100});const result=await h.send();
assert.equal(h.replaced(),true);assert.equal(result.outcome,'SENT');assert.equal(h.clicks(),1);
});

test('keeps a changed user draft and never sends it',async()=>{
const h=harness({changeDraftAt:100});const result=await h.send();
assert.equal(result.outcome,'STALE');assert.equal(h.clicks(),0);assert.equal(h.draft(),'本人未发送草稿');
});

test('a permanently disabled control stops safely within the readiness budget',async()=>{
const h=harness({readyAt:Infinity});const result=await h.send();
assert.equal(result.outcome,'FAILED_SAFE');assert.equal(h.clicks(),0);assert.equal(h.elapsed(),2500);
});

test('readiness waiting respects the command deadline',async()=>{
const h=harness({readyAt:Infinity});const result=await h.send(250);
assert.equal(result.outcome,'FAILED_SAFE');assert.equal(h.clicks(),0);assert.ok(h.elapsed()<=250);
});

test('ambiguous native send controls fail closed without clicking either',async()=>{
const h=harness({ambiguousAt:100});const result=await h.send();
assert.equal(result.outcome,'FAILED_SAFE');assert.equal(h.clicks(),0);assert.match(result.evidence,/多个/);
});

test('manual pause during readiness waiting aborts before a send',async()=>{
const h=harness({pauseAt:100});await assert.rejects(h.send(),/USER_PAUSED/);assert.equal(h.clicks(),0);
});

test('a changed inbound round is rechecked after readiness',async()=>{
const h=harness({changeRoundAt:100});const result=await h.send();
assert.equal(result.outcome,'STALE');assert.equal(h.clicks(),0);
});

test('the native control is checked again after the final authorization guard',async()=>{
const h=harness({disableOnFinalGuard:true});const result=await h.send();
assert.equal(result.outcome,'FAILED_SAFE');assert.equal(h.clicks(),0);
});
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.10.16');
assert.equal(manifest.version, '1.10.17');
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,7 +15,7 @@ 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.10.16");
assert.equal(manifest.version, "1.10.17");
assert.match(background, /BACKGROUND_VERSION = "2026-09-14-runtime-adapters"/);
assert.match(background, /REQUIRED_BOSS_CONTENT_VERSION = "1.10.5"/);
assert.match(boss, /EXTENSION_VERSION = "1.10.5"/);
Expand Down
Loading