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
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "20"
# The locked jsdom/undici test dependencies require Node >= 22.19.
node-version: "24"
cache: pnpm
cache-dependency-path: front/pnpm-lock.yaml

Expand All @@ -106,6 +107,10 @@ jobs:
working-directory: front
run: pnpm typecheck

- name: Test Zhilian modern DOM collection
working-directory: front
run: pnpm exec vitest run lib/zhilian-modern-collector.test.ts

- name: Build frontend
working-directory: front
run: pnpm build
Expand Down
7 changes: 4 additions & 3 deletions chrome-extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const PLATFORM_CONFIG = {
contentScript: "zhilian-content.js",
contentScripts: [
"zhilian-scan-support.js",
"zhilian-modern-collector.js",
"zhilian-content.js"
]
}
Expand All @@ -38,13 +39,13 @@ const PLATFORM_SHARED_SCAN_KEYS = {
boss: ["__GET_JOBS_BOSS_SHARED_SCAN_TASK__", "__GET_JOBS_BOSS_SHARED_SCAN_CANCEL__"],
zhilian: ["__GET_JOBS_ZHILIAN_SHARED_SCAN_TASK__", "__GET_JOBS_ZHILIAN_SHARED_SCAN_CANCEL__"]
};
const BACKGROUND_VERSION = "2026-09-07-zhilian-page-status";
const BACKGROUND_VERSION = "2026-09-07-modern-collection";
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 = "2026-09-06-hr-profile-guard";
const REQUIRED_ZHILIAN_CONTENT_VERSION = "2026-09-07-zhilian-page-status";
const REQUIRED_ZHILIAN_CONTENT_VERSION = "2026-09-07-modern-collection";
const LOCAL_API_BASE_URLS = ["http://127.0.0.1:6866"];
const BOSS_LOCAL_API_MAX_ATTEMPTS = 3;
const BOSS_LOCAL_API_TIMEOUT_MS = 30000;
Expand Down Expand Up @@ -2483,7 +2484,7 @@ function isZhilianSearchUrl(url) {
const parsed = new URL(url);
return parsed.protocol === "https:"
&& isZhilianHost(parsed.hostname)
&& /^\/sou(?:\/|$)/i.test(parsed.pathname);
&& /^(?:\/sou(?:\/|$)|\/jobs\/?$)/i.test(parsed.pathname);
} catch {
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions 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.6.6",
"version": "1.6.7",
"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 Expand Up @@ -45,7 +45,7 @@
},
{
"matches": ["https://www.zhaopin.com/*", "https://*.zhaopin.com/*"],
"js": ["zhilian-scan-support.js", "zhilian-content.js"],
"js": ["zhilian-scan-support.js", "zhilian-modern-collector.js", "zhilian-content.js"],
"run_at": "document_idle"
},
{
Expand Down
2 changes: 2 additions & 0 deletions chrome-extension/tests/background-tab-routing.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ test("injects all Zhilian dependencies when the content script is missing", asyn
assert.equal(executedScripts.length, 1);
assert.deepEqual(Array.from(executedScripts[0].files), [
"zhilian-scan-support.js",
"zhilian-modern-collector.js",
"zhilian-content.js"
]);
});
Expand All @@ -262,6 +263,7 @@ test("reinjects all Zhilian dependencies when the content script is stale", asyn
assert.equal(executedScripts.length, 1);
assert.deepEqual(Array.from(executedScripts[0].files), [
"zhilian-scan-support.js",
"zhilian-modern-collector.js",
"zhilian-content.js"
]);
assert.equal(await context.isContentScriptReady(1, "zhilian-content.js"), true);
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.some((value) => value.includes("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.6.6");
assert.equal(manifest.version, "1.6.7");
});

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.6.6');
assert.equal(manifest.version, '1.6.7');
assert.equal(extensionIdFromKey(manifest.key), EXPECTED_EXTENSION_ID);

const publicKey = crypto.createPublicKey({
Expand Down
8 changes: 4 additions & 4 deletions chrome-extension/tests/profile-scoped-scan-contract.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ 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.6.6");
assert.match(background, /BACKGROUND_VERSION = "2026-09-07-zhilian-page-status"/);
assert.equal(manifest.version, "1.6.7");
assert.match(background, /BACKGROUND_VERSION = "2026-09-07-modern-collection"/);
assert.match(background, /REQUIRED_BOSS_CONTENT_VERSION = "2026-09-06-hr-profile-guard"/);
assert.match(boss, /EXTENSION_VERSION = "2026-09-06-hr-profile-guard"/);
assert.match(zhilian, /EXTENSION_VERSION = "2026-09-07-zhilian-page-status"/);
assert.match(background, /REQUIRED_ZHILIAN_CONTENT_VERSION = "2026-09-07-zhilian-page-status"/);
assert.match(zhilian, /EXTENSION_VERSION = "2026-09-07-modern-collection"/);
assert.match(background, /REQUIRED_ZHILIAN_CONTENT_VERSION = "2026-09-07-modern-collection"/);
});

test("both platforms bind cursors, dedupe, submissions and progress to profileId", () => {
Expand Down
31 changes: 26 additions & 5 deletions chrome-extension/tests/zhilian-scan-support.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test("replaces a stale Zhilian support module after extension reload", () => {
const support = loadSupport(staleSupport);

assert.notEqual(support, staleSupport);
assert.equal(support.version, "2026-09-07-page-status");
assert.equal(support.version, "2026-09-07-modern-collection");
assert.equal(typeof support.isZhilianUrl, "function");
});

Expand Down Expand Up @@ -174,7 +174,7 @@ test("builds a Zhilian search URL with official city and salary params", () => {

assert.equal(
support.buildSearchUrl("Java", { cityCode: "765", salary: "10001,15000" }),
"https://www.zhaopin.com/sou/jl765/?kw=Java&sl=10001%2C15000"
"https://www.zhaopin.com/jobs?jl=765&kw=Java&sl=10001%2C15000"
);
});

Expand All @@ -183,15 +183,15 @@ test("omits sl when salary is unlimited", () => {

assert.equal(
support.buildSearchUrl("Java", { cityCode: "765", salary: "0" }),
"https://www.zhaopin.com/sou/jl765/?kw=Java"
"https://www.zhaopin.com/jobs?jl=765&kw=Java"
);
assert.equal(
support.buildSearchUrl("Java", { cityCode: "765", salary: "\u4e0d\u9650" }),
"https://www.zhaopin.com/sou/jl765/?kw=Java"
"https://www.zhaopin.com/jobs?jl=765&kw=Java"
);
assert.equal(
support.buildSearchUrl("Java", { cityCode: "765", salary: "0000,9999999" }),
"https://www.zhaopin.com/sou/jl765/?kw=Java"
"https://www.zhaopin.com/jobs?jl=765&kw=Java"
);
});

Expand All @@ -217,3 +217,24 @@ test("page readiness distinguishes login, security and loading from a usable pag
}
assert.equal(support.pageStatus({ hasSecurityPrompt: true, hasLoginPrompt: true }).pageState, "SECURITY_REQUIRED");
});

test("accepts redirected jobs searches only with matching keyword, city, salary and page", () => {
const support = loadSupport();
const config = { cityCode: "489" };
for (const url of ["https://www.zhaopin.com/jobs?jl=489&kw=AI产品运营", "https://www.zhaopin.com/jobs/?jl=489&kw=AI产品运营", "https://www.zhaopin.com/sou/jl489/?kw=AI产品运营"]) {
assert.equal(support.matchesSearchUrl(url, "AI产品运营", config), true);
}
for (const url of ["https://www.zhaopin.com/jobs?jl=765&kw=AI产品运营", "https://www.zhaopin.com/jobs?jl=489&kw=Java", "https://www.zhaopin.com/jobs?jl=489&kw=AI产品运营&sl=10001,15000", "https://www.zhaopin.com/jobs/?pageMode=recommend", "https://evilzhaopin.com/jobs?jl=489&kw=AI产品运营"]) {
assert.equal(support.matchesSearchUrl(url, "AI产品运营", config), false);
}
assert.equal(support.matchesSearchUrl("https://www.zhaopin.com/sou/jl489/?kw=Java&p=2", "Java", config, 2), true);
assert.equal(support.matchesSearchUrl("https://www.zhaopin.com/sou/jl489/?kw=Java&p=2", "Java", config, 1), false);
});

test("normalizes official HTTP detail links without relaxing the origin or protocol boundary", () => {
const support = loadSupport();
assert.equal(support.normalizeJobUrl("http://www.zhaopin.com/jobdetail/CC100J200.htm"), "https://www.zhaopin.com/jobdetail/CC100J200.htm");
for (const value of ["http://evilzhaopin.com/jobdetail/CC100J200.htm", "https://www.zhaopin.com.evil.test/jobdetail/CC100J200.htm", "javascript:alert(1)", "http://www.zhaopin.com/companydetail/CC100.htm", "https://user:password@www.zhaopin.com/jobdetail/CC100.htm"]) {
assert.equal(support.normalizeJobUrl(value), "");
}
});
Loading
Loading