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
74 changes: 61 additions & 13 deletions chrome-extension/fixture-export.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
(function (root) {
const VERSION = "structural-fixture/1";
const REDACTION_VERSION = "structural-fixture/2";
const errors = Object.freeze({
UNSUPPORTED_PAGE: "当前页面不是受支持的 BOSS / 智联 HTTPS 招聘页。",
CHAT_PAGE: "聊天页面不支持导出,请切换到岗位搜索列表或详情。",
INVALID_TYPE: "样本类型不支持,请重新选择。",
NO_STRUCTURE: "未找到该类型的白名单结构。请确认岗位已加载;页面结构可能尚未适配,不会改为保存整页。",
TOO_LARGE: "页面结构过大,已停止导出。请减少加载的岗位后重试,不会输出截断样本。",
NO_ACTIVE_TAB: "未找到当前标签页,请保持招聘页面在前台。",
PAGE_CHANGED: "页面已变化或未返回有效样本,请在页面稳定后重新生成。",
CAPTURE_FAILED: "无法读取页面,请刷新招聘标签页并重新加载扩展后再试。"
});
const warningMessages = Object.freeze({
MISSING_JOB_CARDS: "缺少可识别的岗位卡片结构",
MISSING_TITLE: "缺少岗位标题",
MISSING_COMPANY: "缺少公司名称",
MISSING_DESCRIPTION: "缺少职位描述(JD)",
MISSING_JOB_IDENTITY: "缺少岗位 ID 或详情链接,不能验证岗位关联"
});
function fail(code) { const error = new Error(errors[code]); error.code = code; throw error; }
const roots = Object.freeze({
boss: {
SEARCH: ".job-list-box, .search-job-result, .pagination",
JOB_DETAIL: ".job-banner, .job-detail-header, .job-description, .job-detail-section, .job-sec, .company-info, .company-name, .job-address",
// Bounded patterns already supported by boss-selectors.js. Never use body/main or generic content roots.
SEARCH: ".job-list-box, .search-job-result, .pagination, .job-card-wrapper, .job-card-body, li.job-card-box, [class*='job-card'], [class*='search-list'], [class*='result-list'], [class*='job-list']",
JOB_DETAIL: ".job-banner, .job-detail-header, .job-description, .job-detail-section, .job-sec, .job-sec-text, .job-detail, .detail-content, [class*='job-detail'], [class*='job-sec'], [class*='description'], .company-info, .company-name, .job-address",
BLOCKER: ".dialog, .modal, .dialog-wrap, .login-dialog, .verify-dialog, .verify-box"
},
zhilian: {
Expand All @@ -14,7 +34,7 @@
});
const classes = new Set(("job-list-box search-job-result pagination next prev disabled empty loading " +
"job-card-box job-card-wrapper job-card-body job-card job-name job-title company-name salary job-salary job-area tag-list " +
"job-banner job-detail-header job-description job-detail-section job-sec job-sec-text text company-info job-address " +
"job-banner job-detail-header job-description job-detail-section job-sec job-sec-text job-detail detail-content text company-info job-address " +
"boss-name boss-title boss-active-time dialog modal dialog-wrap login-dialog verify-dialog verify-box " +
"job-list-panel job-split-layout__right job-card--active job-card__title-clamp job-card__salary job-card__skill-tags " +
"job-card__company-name job-card__location job-detail-summary__title-text job-detail-summary__salary " +
Expand All @@ -34,24 +54,25 @@
];
function platformFor(href) {
const url = new URL(href);
if (url.protocol !== "https:") throw new Error("仅支持招聘网站 HTTPS 页面");
if (url.protocol !== "https:") fail("UNSUPPORTED_PAGE");
if (/(^|\.)zhipin\.com$/i.test(url.hostname)) return "boss";
if (/(^|\.)zhaopin\.com$/i.test(url.hostname)) return "zhilian";
throw new Error("当前页面不支持 Fixture 导出");
fail("UNSUPPORTED_PAGE");
}
// No outerHTML of the source, storage, network, page click or form reads.
// Free text and attributes are substituted, not regex-scrubbed and retained.
function capture(document, href, pageType) {
const platform = platformFor(href);
if (/\/(chat|im|message)(\/|$)/i.test(new URL(href).pathname)) throw new Error("不导出聊天页面");
if (/\/(chat|im|message)(\/|$)/i.test(new URL(href).pathname)) fail("CHAT_PAGE");
const selector = roots[platform][pageType];
if (!selector) throw new Error("不支持的样本类型");
if (!selector) fail("INVALID_TYPE");
const candidates = Array.from(document.querySelectorAll(selector));
if (candidates.length > 200) throw new Error("匹配结构过多,已停止导出");
if (candidates.length > 1000) fail("TOO_LARGE");
const selected = candidates
.filter(node => !node.closest(excluded))
.filter(node => tags.has(node.localName) && !node.closest(excluded))
.filter((node, _, all) => !all.some(parent => parent !== node && parent.contains(node)));
if (!selected.length) throw new Error("未找到白名单结构;不会退回导出整个页面");
if (selected.length > 200) fail("TOO_LARGE");
if (!selected.length) fail("NO_STRUCTURE");
const out = document.implementation.createHTMLDocument("脱敏结构样本");
const identities = new Map(), titles = new Map(), companies = new Map();
let visited = 0;
Expand All @@ -78,7 +99,7 @@
return "脱敏文本";
}
function copy(node, depth = 0) {
if (++visited > 5000 || depth > 60) throw new Error("结构过大,已停止导出;不会输出不完整样本");
if (++visited > 5000 || depth > 60) fail("TOO_LARGE");
if (node.nodeType === 3) return out.createTextNode(substitute(node.textContent, node.parentElement));
if (node.nodeType !== 1 || node.matches(excluded)) return null;
if (!tags.has(node.localName)) return null;
Expand Down Expand Up @@ -114,13 +135,40 @@
const result = copy(node);
if (result) out.body.appendChild(result);
}
if (!out.body.children.length) fail("NO_STRUCTURE");
// Coverage describes retained structural markers, never claims the real text or platform flow was verified.
const count = selector => out.body.querySelectorAll(selector).length;
const coverage = {
jobCards: count(".job-card-box,.job-card-wrapper,.job-card-body,.job-card"),
titles: count(".job-name,.job-title,.job-card__title-clamp,.job-detail-summary__title-text"),
companies: count(".company-name,.job-card__company-name"),
descriptions: count(".job-description,.job-sec-text,.job-detail-section .text,.job-description__content"),
jobIdentities: count("[data-jobid],[data-job-id],[data-jid],[data-position-id],a[href*='/job_detail/'],a[href*='/jobdetail/']")
};
const warnings = [];
if (pageType === "SEARCH" && !coverage.jobCards) warnings.push("MISSING_JOB_CARDS");
if (pageType !== "BLOCKER") {
if (!coverage.titles) warnings.push("MISSING_TITLE");
if (!coverage.companies) warnings.push("MISSING_COMPANY");
if (!coverage.jobIdentities) warnings.push("MISSING_JOB_IDENTITY");
}
if (pageType === "JOB_DETAIL" && !coverage.descriptions) warnings.push("MISSING_DESCRIPTION");
return {
format: VERSION, platform, pageType,
provenance: { kind: "structural-redacted", capturedAt: new Date().toISOString(), source: "user-triggered-extension-export", content: "synthetic-replacements", redactionVersion: VERSION },
provenance: { kind: "structural-redacted", capturedAt: new Date().toISOString(), source: "user-triggered-extension-export", content: "synthetic-replacements", redactionVersion: REDACTION_VERSION },
html: out.body.innerHTML, rootCount: selected.length, nodeCount: visited,
coverage, warnings,
reviewRequired: true,
limitations: ["自由文本与身份已替换,不能证明真实正文解析正确", "只保留白名单类名和有限样式,不是完整视觉快照", "岗位别名仅在本次导出内部保持关联", "不采集聊天,未知结构需要另写合成样本"]
};
}
root.GetJobsFixtureExporter = Object.freeze({ capture, platformFor, version: VERSION });
function captureSafely(document, href, pageType) {
try { return { ok: true, bundle: capture(document, href, pageType) }; }
catch (error) { return { ok: false, errorCode: Object.hasOwn(errors, error?.code) ? error.code : "CAPTURE_FAILED" }; }
}
root.GetJobsFixtureExporter = Object.freeze({
capture, captureSafely, platformFor, version: REDACTION_VERSION,
errorMessage: code => Object.hasOwn(errors, code) ? errors[code] : errors.CAPTURE_FAILED,
warningMessage: code => Object.hasOwn(warningMessages, code) ? warningMessages[code] : "部分结构未识别"
});
})(typeof window !== "undefined" ? window : globalThis);
16 changes: 10 additions & 6 deletions chrome-extension/fixture-popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@
status.textContent = "正在生成脱敏结构预览…";
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id || !tab.url) throw new Error("未找到当前页面");
if (!tab?.id || !tab.url) throw { code: "NO_ACTIVE_TAB" };
const platform = window.GetJobsFixtureExporter.platformFor(tab.url);
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ["fixture-export.js"] });
if (generation !== current) return;
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, args: [type], func: type => window.GetJobsFixtureExporter.capture(document, window.location.href, type) });
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, args: [type], func: type => window.GetJobsFixtureExporter.captureSafely(document, window.location.href, type) });
if (generation !== current) return;
if (result.length !== 1 || !result[0].result || result[0].result.platform !== platform) throw new Error("页面已变化或未产生样本,请重新检查");
bundle = result[0].result;
if (result.length !== 1 || !result[0].result) throw { code: "PAGE_CHANGED" };
const captureResult = result[0].result;
if (!captureResult.ok) throw { code: captureResult.errorCode };
if (captureResult.bundle?.platform !== platform || captureResult.bundle.pageType !== type) throw { code: "PAGE_CHANGED" };
bundle = captureResult.bundle;
preview.value = JSON.stringify(bundle, null, 2); reviewed.disabled = false;
status.textContent = `已生成 ${bundle.rootCount} 个脱敏结构。请检查预览后再下载;不代表真实网站测试通过。`;
} catch {
if (generation === current) status.textContent = "无法导出:请确认当前为支持的招聘页面、样本类型正确且扩展已重载。没有生成文件。";
if (bundle.warnings?.length) status.textContent += ` 当前是部分结构样本:${bundle.warnings.map(code => window.GetJobsFixtureExporter.warningMessage(code)).join(";")}。可保留用于诊断,但不能作为完整采集验收。`;
} catch (error) {
if (generation === current) status.textContent = `无法导出:${window.GetJobsFixtureExporter.errorMessage(error?.code)} 没有生成文件。`;
} finally { capture.disabled = false; }
});
download.addEventListener("click", () => {
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.12",
"version": "1.8.13",
"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
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.12");
assert.equal(manifest.version, "1.8.13");
});

test("assistant exposes policy-gated dedicated watch and preserves explicit manual send", () => {
Expand Down
4 changes: 3 additions & 1 deletion chrome-extension/tests/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# 招聘 DOM Fixture

`catalog.json` 是样本索引,每条包含平台、页面类型、来源、HTML 路径及期望结果。当前样本全部是根据已有测试和采集器编写的 **synthetic** 样本,未访问招聘账号,`capturedAt` 为 null。不要将其改名为真实网站验收记录。
`catalog.json` 是样本索引,每条包含平台、页面类型、来源、HTML 路径及期望结果。保留 23 份根据已有测试和采集器编写的 **synthetic** 样本(`capturedAt` 为 null),另有用户主动导出的 **structural-redacted** 样本;二者不能互相冒充,也不等于真实投递验收。

- `boss/detail/redacted-header-only-20260915`:用户使用 1.8.12 导出的真实结构,正文和身份已替换。只含标题区域与地址,缺 JD、公司和岗位身份,是导出不完整的故障证据。禁止用标题/地址的文本回退结果证明完整 JD 已采集。原始截图未提交。

- BOSS 列表覆盖去重、外部链接、导航伪链接、空列表及替换后的卡片;详情覆盖岗位与公司信息。
- 智联复用现代采集器,覆盖无岗位链接的列表卡片、独立详情身份、过期详情和空列表。
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="job-detail-header" style="display: flex; visibility: visible; position: static; opacity: 1;"><div style="display: block; visibility: visible; position: static; opacity: 1;"><div style="display: flex; visibility: visible; position: static; opacity: 1;"><span class="job-name" style="display: block; visibility: visible; position: static; opacity: 1;">示例产品运营001</span><span class="job-salary" style="display: block; visibility: visible; position: static; opacity: 1;">15-25K</span></div><ul class="tag-list" style="display: flex; visibility: visible; position: static; opacity: 1;"><li style="display: flex; visibility: visible; position: relative; opacity: 1;"><a style="display: block; visibility: visible; position: static; opacity: 1;">3-5年 本科</a></li><li style="display: flex; visibility: visible; position: relative; opacity: 1;"><span style="display: block; visibility: visible; position: static; opacity: 1;">3-5年 本科</span></li><li style="display: flex; visibility: visible; position: relative; opacity: 1;"><span style="display: block; visibility: visible; position: static; opacity: 1;">3-5年 本科</span></li></ul></div><div style="display: block; visibility: visible; position: static; opacity: 1;"><a style="display: flex; visibility: visible; position: static; opacity: 1;">脱敏文本</a><a style="display: block; visibility: visible; position: static; opacity: 1;">立即沟通</a></div></div><div class="job-address" style="display: block; visibility: visible; position: static; opacity: 1;"><span style="display: inline; visibility: visible; position: static; opacity: 1;">示例办公地址</span><p style="display: flex; visibility: visible; position: static; opacity: 1;">示例办公地址</p><div style="display: block; visibility: visible; position: relative; opacity: 1;"><p style="display: block; visibility: visible; position: absolute; opacity: 1;">示例办公地址</p></div></div>
30 changes: 30 additions & 0 deletions chrome-extension/tests/fixtures/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -401,5 +401,35 @@
"marker": "已向对方发送简历和打招呼语",
"detectorCoverage": "existing-delivery-confirmation-dom"
}
},
{
"id": "boss/detail/redacted-header-only-20260915",
"platform": "boss",
"pageType": "JOB_DETAIL",
"file": "boss/detail/redacted-header-only-20260915.html",
"provenance": {
"kind": "structural-redacted",
"capturedAt": "2026-09-15T01:19:39.4290000Z",
"reference": "user-provided boss-job_detail-fixture.json",
"redactionVersion": "structural-fixture/1",
"limitations": [
"1.8.12 仅导出标题和地址,缺少 JD、公司及岗位身份;是故障样本,不代表完整页面或采集通过。",
"截图只用于人工核对,不纳入仓库;原文已由用户导出器替换。"
]
},
"expected": {
"detail": {
"title": "示例产品运营001",
"company": "",
"salary": "15-25K",
"companyAddress": "示例办公地址示例办公地址示例办公地址"
},
"absentSelectors": [
".job-description",
".job-sec-text",
".job-detail-section",
".company-name"
]
}
}
]
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.12');
assert.equal(manifest.version, '1.8.13');
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.8.12");
assert.equal(manifest.version, "1.8.13");
assert.match(background, /BACKGROUND_VERSION = "2026-09-14-runtime-adapters"/);
assert.match(background, /REQUIRED_BOSS_CONTENT_VERSION = "2026-09-14-boss-adapter"/);
assert.match(boss, /EXTENSION_VERSION = "2026-09-14-boss-adapter"/);
Expand Down
Loading
Loading