Compare commits

...

1 Commits

Author SHA1 Message Date
jackwener dd2eed7743 refactor: replace hardcoded skipPreNav with declarative navigateBefore field
Browser adapters using COOKIE/HEADER strategy need the page on the target
domain so credentialed fetch() carries cookies. Previously, execution.ts
hardcoded `cmd.site === 'boss'` to skip this pre-navigation for adapters
that handle their own goto().

Now each adapter self-declares via `navigateBefore: false` on CliCommand.
This is more extensible — new sites that manage their own navigation just
add the field instead of editing execution.ts.

Changes:
- Add `navigateBefore?: boolean | string` to CliCommand interface
- Add `resolvePreNav()` helper in execution.ts (replaces hardcoded check)
- All 14 boss adapters declare `navigateBefore: false`
- Wire through discovery.ts (YAML + manifest) and build-manifest.ts
2026-03-21 23:50:50 +08:00
18 changed files with 59 additions and 8 deletions
+7
View File
@@ -41,6 +41,8 @@ interface ManifestEntry {
type: 'yaml' | 'ts';
/** Relative path from clis/ dir, e.g. 'bilibili/hot.yaml' or 'bilibili/search.js' */
modulePath?: string;
/** Pre-navigation control — see CliCommand.navigateBefore */
navigateBefore?: boolean | string;
}
function extractBalancedBlock(
@@ -189,6 +191,7 @@ function scanYaml(filePath: string, site: string): ManifestEntry | null {
pipeline: def.pipeline,
timeout: def.timeout,
type: 'yaml',
navigateBefore: def.navigateBefore,
};
} catch (err: any) {
process.stderr.write(`Warning: failed to parse ${filePath}: ${err.message}\n`);
@@ -248,6 +251,10 @@ function scanTs(filePath: string, site: string): ManifestEntry | null {
entry.args = parseTsArgsBlock(argsBlock);
}
// Extract navigateBefore: false
const navMatch = src.match(/navigateBefore\s*:\s*(true|false)/);
if (navMatch) entry.navigateBefore = navMatch[1] === 'true' ? true : false;
return entry;
} catch (err: any) {
// If parsing fails, log a warning (matching scanYaml behaviour) and skip the entry.
+1
View File
@@ -13,6 +13,7 @@ cli({
description: 'BOSS直聘批量向推荐候选人发送招呼',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'job-id', default: '', help: 'Filter by encrypted job ID (greet all jobs if empty)' },
+1
View File
@@ -7,6 +7,7 @@ cli({
description: 'BOSS直聘查看聊天列表(招聘端)',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
+1
View File
@@ -7,6 +7,7 @@ cli({
description: 'BOSS直聘查看与候选人的聊天消息',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'uid', required: true, help: 'Encrypted UID (from chatlist)' },
+1
View File
@@ -10,6 +10,7 @@ cli({
description: 'BOSS直聘查看职位详情',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'security-id', required: true, help: 'Security ID from search results (securityId field)' },
+1
View File
@@ -10,6 +10,7 @@ cli({
description: 'BOSS直聘交换联系方式(请求手机/微信)',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate' },
+1
View File
@@ -13,6 +13,7 @@ cli({
description: 'BOSS直聘向新候选人发送招呼(开始聊天)',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate (from recommend)' },
+1
View File
@@ -10,6 +10,7 @@ cli({
description: 'BOSS直聘发送面试邀请',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate' },
+1
View File
@@ -10,6 +10,7 @@ cli({
description: 'BOSS直聘查看我发布的职位列表',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [],
columns: ['job_name', 'salary', 'city', 'status', 'encrypt_job_id'],
+1
View File
@@ -19,6 +19,7 @@ cli({
description: 'BOSS直聘给候选人添加标签',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate' },
+1
View File
@@ -10,6 +10,7 @@ cli({
description: 'BOSS直聘查看推荐候选人(新招呼列表)',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'limit', type: 'int', default: 20, help: 'Number of results to return' },
+1
View File
@@ -18,6 +18,7 @@ cli({
description: 'BOSS直聘查看候选人简历(招聘端)',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate (from chatlist)' },
+1
View File
@@ -69,6 +69,7 @@ cli({
description: 'BOSS直聘搜索职位',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'query', required: true, positional: true, help: 'Search keyword (e.g. AI agent, 前端)' },
+1
View File
@@ -16,6 +16,7 @@ cli({
description: 'BOSS直聘发送聊天消息',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'uid', required: true, help: 'Encrypted UID of the candidate (from chatlist)' },
+1
View File
@@ -10,6 +10,7 @@ cli({
description: 'BOSS直聘职位数据统计',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
browser: true,
args: [
{ name: 'job-id', default: '', help: 'Encrypted job ID (show all if empty)' },
+3
View File
@@ -62,6 +62,7 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
pipeline: entry.pipeline,
timeoutSeconds: entry.timeout,
source: `manifest:${entry.site}/${entry.name}`,
navigateBefore: entry.navigateBefore,
};
registerCommand(cmd);
} else if (entry.type === 'ts' && entry.modulePath) {
@@ -80,6 +81,7 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
columns: entry.columns,
timeoutSeconds: entry.timeout,
source: modulePath,
navigateBefore: entry.navigateBefore,
_lazy: true,
_modulePath: modulePath,
};
@@ -162,6 +164,7 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
pipeline: def.pipeline,
timeoutSeconds: def.timeout,
source: filePath,
navigateBefore: def.navigateBefore,
};
registerCommand(cmd);
+23 -8
View File
@@ -101,6 +101,24 @@ async function runCommand(
throw new Error(`Command ${fullName(cmd)} has no func or pipeline`);
}
/**
* Resolve the pre-navigation URL for a command, or null to skip.
*
* COOKIE/HEADER strategies need the browser on the target domain so
* `fetch(url, { credentials: 'include' })` carries cookies.
* Adapters that handle their own navigation set `navigateBefore: false`.
*/
function resolvePreNav(cmd: CliCommand): string | null {
if (cmd.navigateBefore === false) return null;
if (typeof cmd.navigateBefore === 'string') return cmd.navigateBefore;
// Default: pre-navigate for COOKIE/HEADER strategies with a domain
if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
return `https://${cmd.domain}`;
}
return null;
}
/**
* Execute a CLI command. Automatically manages browser sessions when needed.
*
@@ -122,14 +140,11 @@ export async function executeCommand(
if (shouldUseBrowserSession(cmd)) {
const BrowserFactory = getBrowserFactory();
return browserSession(BrowserFactory, async (page) => {
// Cookie/header strategies require same-origin context for credentialed fetch.
// TS adapters that manage their own navigation (via common.ts helpers like
// navigateToChat/navigateTo) should skip this pre-navigation to avoid
// redundant double page loads. Currently only boss adapters do this.
// TODO: replace site check with an adapter-level `skipPreNav` flag.
const skipPreNav = cmd.site === 'boss';
if (!skipPreNav && (cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
// Pre-navigate to target domain for cookie/header context if needed.
// Each adapter controls this via `navigateBefore` (see CliCommand docs).
const preNavUrl = resolvePreNav(cmd);
if (preNavUrl) {
try { await page.goto(preNavUrl); await page.wait(2); } catch {}
}
return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT,
+12
View File
@@ -36,6 +36,17 @@ export interface CliCommand {
timeoutSeconds?: number;
source?: string;
footerExtra?: (kwargs: Record<string, any>) => string | undefined;
/**
* Control pre-navigation for cookie/header context before command execution.
*
* Browser adapters using COOKIE/HEADER strategy need the page to be on the
* target domain so that `fetch(url, { credentials: 'include' })` carries cookies.
*
* - `undefined` / `true`: navigate to `https://${domain}` (default)
* - `false`: skip — adapter handles its own navigation (e.g. boss common.ts)
* - `string`: navigate to this specific URL instead of the domain root
*/
navigateBefore?: boolean | string;
}
/** Internal extension for lazy-loaded TS modules (not exposed in public API) */
@@ -73,6 +84,7 @@ export function cli(opts: CliOptions): CliCommand {
pipeline: opts.pipeline,
timeoutSeconds: opts.timeoutSeconds,
footerExtra: opts.footerExtra,
navigateBefore: opts.navigateBefore,
};
const key = fullName(cmd);