diff --git a/chrome-extension/background.js b/chrome-extension/background.js index eb06ff1..679151a 100644 --- a/chrome-extension/background.js +++ b/chrome-extension/background.js @@ -39,7 +39,8 @@ 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-modern-collection"; +const BACKGROUND_VERSION = "2026-09-08-scan-controls"; +let zhilianPagePreparation = null; const CONTENT_READY_RETRIES = 12; const CONTENT_READY_INTERVAL_MS = 250; const TAB_LOAD_TIMEOUT_MS = 10000; @@ -1873,6 +1874,13 @@ async function postPlatformProgress(pageTabId, payload) { } async function queryZhilianPageStatus(message, pageTabId) { + if (message.openIfMissing === true) { + if (!zhilianPagePreparation) { + zhilianPagePreparation = prepareZhilianPageStatus(message, pageTabId) + .finally(() => { zhilianPagePreparation = null; }); + } + return await zhilianPagePreparation; + } const tabs = (await chrome.tabs.query({})) .filter(tab => isSupportedUrl(tab.url || tab.pendingUrl || "", PLATFORM_CONFIG.zhilian)) .sort((a, b) => Number(b.lastAccessed || 0) - Number(a.lastAccessed || 0)); @@ -1889,6 +1897,31 @@ async function queryZhilianPageStatus(message, pageTabId) { || statuses[0]; } +async function prepareZhilianPageStatus(message, pageTabId) { + const passiveMessage = { ...message, openIfMissing: false }; + try { + let tabs = (await chrome.tabs.query({})) + .filter(tab => isSupportedUrl(tab.url || tab.pendingUrl || "", PLATFORM_CONFIG.zhilian)); + if (!tabs.length) { + // Use a fixed official URL; page messages cannot choose an arbitrary destination. + tabs = [await chrome.tabs.create({ url: "https://www.zhaopin.com/jobs?jl=489", active: true })]; + } + await Promise.all(tabs.filter(tab => tab.status === "loading").map(tab => + waitForSupportedTab(tab.id, PLATFORM_CONFIG.zhilian).catch(() => null) + )); + const startedAt = Date.now(); + let status; + do { + status = await queryZhilianPageStatus(passiveMessage, pageTabId); + if (status.chromePageReady || status.hasLoginPrompt || status.hasSecurityPrompt || status.pageState === "NO_TAB") return status; + await sleep(CONTENT_READY_INTERVAL_MS); + } while (Date.now() - startedAt < 8000); + return { ...status, message: status.message || "智联页面已打开,但尚未就绪,请等待页面加载完成后重试" }; + } catch (error) { + return { success: false, chromePageReady: false, message: `自动打开智联页面失败:${error?.message || String(error)}` }; + } +} + async function queryPassivePlatformStatus(tabId, platform, message, pageTabId) { if (message?.type === "BOSS_PAGE_STATUS" || platform === "zhilian") { try { diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 10d8588..477a0bc 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "投递牛马 Chrome Bridge", - "version": "1.6.7", + "version": "1.6.8", "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": { diff --git a/chrome-extension/tests/background-tab-routing.test.cjs b/chrome-extension/tests/background-tab-routing.test.cjs index 250a84e..8376632 100644 --- a/chrome-extension/tests/background-tab-routing.test.cjs +++ b/chrome-extension/tests/background-tab-routing.test.cjs @@ -1081,6 +1081,41 @@ test("Zhilian missing and loading pages remain unavailable without opening a tab assert.equal(loading.sentMessages.length, 0); }); +test("explicit Zhilian preflight opens one official page for concurrent requests and reuses it", async () => { + const harness = loadBackground({ tabs: [], statuses: { 1: { success: true, chromePageReady: true } } }); + const message = { type: "ZHILIAN_PAGE_STATUS", platform: "zhilian", openIfMissing: true, startUrl: "https://example.com/" }; + const results = await Promise.all([harness.context.queryZhilianPageStatus(message, 20), harness.context.queryZhilianPageStatus(message, 20)]); + assert.ok(results.every(result => result.chromePageReady)); + assert.equal(harness.tabList.length, 1); + assert.equal(harness.tabList[0].url, "https://www.zhaopin.com/jobs?jl=489"); + await harness.context.queryZhilianPageStatus(message, 20); + assert.equal(harness.tabList.length, 1); + assert.equal(harness.tabUpdates.length, 0); +}); + +test("explicit Zhilian preflight waits for loading pages without navigating or bypassing login", async () => { + const harness = loadBackground({ tabs: [{ id: 1, url: "https://www.zhaopin.com/jobs", status: "loading" }], statuses: { 1: { success: true, chromePageReady: false, hasLoginPrompt: true } } }); + let waited = false; + harness.context.waitForSupportedTab = async () => { waited = true; harness.tabList[0].status = "complete"; }; + const status = await harness.context.queryZhilianPageStatus({ type: "ZHILIAN_PAGE_STATUS", openIfMissing: true }, 20); + assert.equal(waited, true); + assert.equal(status.hasLoginPrompt, true); + assert.equal(status.chromePageReady, false); + assert.equal(harness.tabList.length, 1); + assert.equal(harness.tabUpdates.length, 0); +}); + +test("auto-opened Zhilian page retains security checks and returns creation failures", async () => { + const harness = loadBackground({ tabs: [], statuses: { 1: { success: true, chromePageReady: false, hasSecurityPrompt: true } } }); + const message = { type: "ZHILIAN_PAGE_STATUS", openIfMissing: true }; + assert.equal((await harness.context.queryZhilianPageStatus(message, 20)).hasSecurityPrompt, true); + const failed = loadBackground({ tabs: [] }); + failed.context.chrome.tabs.create = async () => { throw new Error("cannot create tab"); }; + const status = await failed.context.queryZhilianPageStatus(message, 20); + assert.equal(status.success, false); + assert.match(status.message, /cannot create tab/); +}); + test("rejects job navigation originating from a chat tab", async () => { const { context, tabUpdates }=loadBackground({tabs:[{id:7,url:"https://www.zhipin.com/web/geek/chat",status:"complete"}]}); diff --git a/chrome-extension/tests/boss-hr-assistant.test.cjs b/chrome-extension/tests/boss-hr-assistant.test.cjs index 2957f8c..9b4dd9f 100644 --- a/chrome-extension/tests/boss-hr-assistant.test.cjs +++ b/chrome-extension/tests/boss-hr-assistant.test.cjs @@ -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.7"); + assert.equal(manifest.version, "1.6.8"); }); test("assistant exposes policy-gated dedicated watch and preserves explicit manual send", () => { diff --git a/chrome-extension/tests/manifest-id.test.cjs b/chrome-extension/tests/manifest-id.test.cjs index d9d166a..5ad1fea 100644 --- a/chrome-extension/tests/manifest-id.test.cjs +++ b/chrome-extension/tests/manifest-id.test.cjs @@ -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.7'); + assert.equal(manifest.version, '1.6.8'); assert.equal(extensionIdFromKey(manifest.key), EXPECTED_EXTENSION_ID); const publicKey = crypto.createPublicKey({ diff --git a/chrome-extension/tests/profile-scoped-scan-contract.test.cjs b/chrome-extension/tests/profile-scoped-scan-contract.test.cjs index 0462393..8c87ae5 100644 --- a/chrome-extension/tests/profile-scoped-scan-contract.test.cjs +++ b/chrome-extension/tests/profile-scoped-scan-contract.test.cjs @@ -15,8 +15,8 @@ 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.7"); - assert.match(background, /BACKGROUND_VERSION = "2026-09-07-modern-collection"/); + assert.equal(manifest.version, "1.6.8"); + assert.match(background, /BACKGROUND_VERSION = "2026-09-08-scan-controls"/); 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-modern-collection"/); diff --git a/docs/zhilian-scan-controls-repair.md b/docs/zhilian-scan-controls-repair.md new file mode 100644 index 0000000..1c4d03b --- /dev/null +++ b/docs/zhilian-scan-controls-repair.md @@ -0,0 +1,34 @@ +# 智联扫描配置与自动开页修复 + +## 行为变化 + +- 共用 Select 原先固定在按钮下方展开。现在根据可见视口上下空间决定方向,限制菜单宽高,并在滚动、窗口与可见视口变化时重新定位。 +- 每关键词后台 AI 分析岗位数改为 5 到 200、步长 5 的下拉菜单。原先保存的非 5 倍数值以“已保存”选项保留,选择新值后随扫描请求提交。 +- 仅“开始扫描”预检发送 `openIfMissing: true`。扩展没有智联标签页时打开固定官方搜索页,等待加载和页面状态;有页面则复用,不重定向已有页面。并发预检复用同一 Promise,避免重复开页。 +- 首页和定时状态检查仍为被动检查。登录提示、安全验证、未知页面状态仍阻止扫描。 +- Chrome Bridge 版本升为 1.6.8。没有数据库、配置迁移或投递逻辑变更。 + +## 验证 + +在本修复工作树根目录执行: + +```powershell +node scripts/validate-chrome-extension.mjs +node --test chrome-extension/tests/*.test.cjs +pnpm --dir front test +pnpm --dir front build +``` + +- 扩展静态校验:20 个引用文件、16 个 JavaScript 文件通过。 +- 扩展测试:121 项通过,包括无页面自动开页、并发去重、已有页面复用、加载等待、登录和安全验证拦截、开页失败,以及被动检查不创建标签页。 +- 前端测试:25 个文件、84 项通过,包括菜单底部翻转、小视口限制、选择 25 后扫描请求携带 25 和主动预检标记。 +- Next.js 生产构建及 TypeScript 检查通过。 +- 独立只读数据预览(18666)使用构建产物,在 1280 × 720 浏览器视口验证底部薪资菜单完全位于视口内,并实际选择岗位数 25。该预览没有连接生产数据库或扩展,不能代替更新后 Chrome 自动开页的实机验收。 + +## 发布与回滚 + +本修复以已部署的 PR #53 内容为基线,PR 延续其集成分支 `codex/fix-zhilian-analysis-login-20260907`,避免把未相关的历史功能带入 main。 + +合并和部署须由用户针对本次修复明确授权。部署时保留现有 RunDock 托管记录及完整配置;更新前端和扩展后,需重新加载 Chrome Bridge 1.6.8 并刷新工作台。在无智联页面的情况下点击开始扫描,验证自动开页后仍通过真实登录检查。 + +回滚可 revert 本次修复提交,并恢复之前前端产物和 Chrome Bridge 1.6.7;此修复不改数据库,无需回滚岗位或投递记录。 diff --git a/front/app/zhilian/page.test.tsx b/front/app/zhilian/page.test.tsx index 4e131b5..4b2273c 100644 --- a/front/app/zhilian/page.test.tsx +++ b/front/app/zhilian/page.test.tsx @@ -2,9 +2,10 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, expect, it, vi } from 'vitest' import Page from './page' import { sendChromeBridgeMessage } from '@/lib/chromeBridge' +import { validateSetupForPlatform } from '@/lib/setupChecklist' vi.mock('@/lib/zhilian-page-status', () => ({ getZhilianPageStatus: async () => ({ connected: true, ready: true, message: 'Chrome 智联可用' }) })) -vi.mock('@/lib/setupChecklist', () => ({ validateSetupForPlatform: async () => ({ ready: true, missing: [] }), formatSetupMissingMessage: () => '' })) +vi.mock('@/lib/setupChecklist', () => ({ validateSetupForPlatform: vi.fn(async () => ({ ready: true, missing: [] })), formatSetupMissingMessage: () => '' })) vi.mock('@/lib/chromeBridge', () => ({ sendChromeBridgeMessage: vi.fn(async () => ({ success: true })), subscribeChromeBridgeEvents: () => () => {} })) afterEach(() => vi.unstubAllGlobals()) @@ -22,8 +23,14 @@ it('配置页不再嵌入分析;启动后提供带档案与批次的独立结 expect(screen.getByRole('link', { name: '智联分析' })).toHaveAttribute('href', '/zhilian/analysis') const start = await screen.findByRole('button', { name: '开始扫描' }) await waitFor(() => expect(start).toBeEnabled()) + fireEvent.click(screen.getByRole('button', { name: '20' })) + const counts = screen.getAllByRole('option').map(option => Number(option.textContent)) + expect(counts).toEqual(Array.from({ length: 40 }, (_, index) => (index + 1) * 5)) + fireEvent.click(screen.getByRole('option', { name: '25' })) fireEvent.click(start) const link = await screen.findByRole('link', { name: '查看本次扫描结果' }) expect(link.getAttribute('href')).toMatch(/\/zhilian\/analysis\?profileId=4&scanRunId=zhilian-/) expect(vi.mocked(sendChromeBridgeMessage).mock.calls.filter(([message]) => message.type === 'ZHILIAN_SCAN_START')).toHaveLength(1) + expect(validateSetupForPlatform).toHaveBeenCalledWith('zhilian', { openPlatformPageIfMissing: true }) + expect(sendChromeBridgeMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'ZHILIAN_SCAN_START', config: expect.objectContaining({ searchJobLimit: 25 }) })) }) diff --git a/front/app/zhilian/page.tsx b/front/app/zhilian/page.tsx index a8cde9b..06c1308 100644 --- a/front/app/zhilian/page.tsx +++ b/front/app/zhilian/page.tsx @@ -7,7 +7,6 @@ import { API_BASE } from '@/lib/api' import { BiSave, BiBriefcase, BiPlay, BiStop, BiLinkExternal, BiCodeAlt } from 'react-icons/bi' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select } from '@/components/ui/select' import Link from 'next/link' @@ -423,7 +422,7 @@ export default function ZhilianPage() { appendProgressLog({ type: 'error', message: '当前档案 ID 无效,请刷新档案后重试。' }) return } - const setup = await validateSetupForPlatform('zhilian') + const setup = await validateSetupForPlatform('zhilian', { openPlatformPageIfMissing: true }) if (!setup.ready) { const message = formatSetupMissingMessage('智联招聘', setup.missing) appendProgressLog({ type: 'error', message }) @@ -775,24 +774,18 @@ export default function ZhilianPage() {
- { - const rawValue = e.target.value - setSearchJobLimitInput(rawValue) - const parsed = Number(rawValue) - if (Number.isFinite(parsed) && parsed >= 1) { - setConfig((c) => ({ ...c, searchJobLimit: Math.min(Math.floor(parsed), 200) })) - } - }} - onBlur={() => commitSearchJobLimit(searchJobLimitInput)} + onChange={(e) => commitSearchJobLimit(e.target.value)} disabled={!hasProfile} - /> + > + {Number(searchJobLimitInput) % 5 !== 0 && ( + + )} + {Array.from({ length: 40 }, (_, index) => (index + 1) * 5).map((limit) => ( + + ))} +
diff --git a/front/components/ui/select.test.tsx b/front/components/ui/select.test.tsx new file mode 100644 index 0000000..3f15a82 --- /dev/null +++ b/front/components/ui/select.test.tsx @@ -0,0 +1,38 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { afterEach, expect, it, vi } from 'vitest' +import { Select } from './select' + +afterEach(() => vi.unstubAllGlobals()) + +it('底部菜单向上展开,并随窗口变化重新定位;选择选项后关闭', () => { + vi.stubGlobal('innerHeight', 600) + vi.stubGlobal('innerWidth', 800) + const onChange = vi.fn() + render() + const trigger = screen.getByRole('button') + vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue({ top: 540, bottom: 580, left: 20, width: 500 } as DOMRect) + fireEvent.click(trigger) + const panel = screen.getByRole('listbox').parentElement! + expect(Number.parseFloat(panel.style.top) + Number.parseFloat(panel.style.maxHeight)).toBeLessThanOrEqual(540) + vi.stubGlobal('innerHeight', 900) + fireEvent.resize(window) + expect(panel.style.top).toBe('588px') + fireEvent.click(screen.getByRole('option', { name: '25' })) + expect(onChange).toHaveBeenCalledWith({ target: { value: '25' } }) + expect(screen.queryByRole('listbox')).not.toBeInTheDocument() +}) + +it('小窗口菜单限制高度和宽度,并支持 Escape 关闭', () => { + vi.stubGlobal('innerHeight', 180) + vi.stubGlobal('innerWidth', 300) + render() + const trigger = screen.getByRole('button') + vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue({ top: 50, bottom: 90, left: 260, width: 500 } as DOMRect) + fireEvent.click(trigger) + const panel = screen.getByRole('listbox').parentElement! + expect(panel.style.maxHeight).toBe('74px') + expect(panel.style.width).toBe('284px') + expect(panel.style.left).toBe('8px') + fireEvent.keyDown(document, { key: 'Escape' }) + expect(screen.queryByRole('listbox')).not.toBeInTheDocument() +}) diff --git a/front/components/ui/select.tsx b/front/components/ui/select.tsx index cd20f36..9b2aa2a 100644 --- a/front/components/ui/select.tsx +++ b/front/components/ui/select.tsx @@ -21,7 +21,7 @@ const Select = React.forwardRef( const wrapperRef = React.useRef(null) const buttonRef = React.useRef(null) const dropdownRef = React.useRef(null) - const [dropdownPosition, setDropdownPosition] = React.useState({ top: 0, left: 0, width: 0 }) + const [dropdownPosition, setDropdownPosition] = React.useState({ top: 0, left: 0, width: 0, maxHeight: 224 }) // 确保组件已挂载(解决 SSR 问题) React.useEffect(() => { @@ -42,25 +42,43 @@ const Select = React.forwardRef( const updatePosition = React.useCallback(() => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect() + const margin = 8 + const viewport = window.visualViewport + const viewportTop = viewport?.offsetTop ?? 0 + const viewportLeft = viewport?.offsetLeft ?? 0 + const viewportHeight = viewport?.height ?? window.innerHeight + const viewportWidth = viewport?.width ?? window.innerWidth + const below = Math.max(0, viewportTop + viewportHeight - rect.bottom - margin * 2) + const above = Math.max(0, rect.top - viewportTop - margin * 2) + const preferredHeight = Math.min(224, dropdownRef.current?.scrollHeight || 224) + const openAbove = below < preferredHeight && above > below + const maxHeight = Math.min(224, openAbove ? above : below) + const height = Math.min(preferredHeight, maxHeight) + const width = Math.min(rect.width, Math.max(0, viewportWidth - margin * 2)) setDropdownPosition({ - top: rect.bottom + 8, - left: rect.left, - width: rect.width, + top: Math.max(viewportTop + margin, Math.min(openAbove ? rect.top - margin - height : rect.bottom + margin, viewportTop + viewportHeight - margin - height)), + left: Math.max(viewportLeft + margin, Math.min(rect.left, viewportLeft + viewportWidth - width - margin)), + width, + maxHeight, }) } }, []) // 打开时计算位置 - React.useEffect(() => { + React.useLayoutEffect(() => { if (open) { updatePosition() // 监听滚动和窗口大小变化,更新位置 const handleUpdate = () => updatePosition() window.addEventListener('scroll', handleUpdate, true) window.addEventListener('resize', handleUpdate) + window.visualViewport?.addEventListener('resize', handleUpdate) + window.visualViewport?.addEventListener('scroll', handleUpdate) return () => { window.removeEventListener('scroll', handleUpdate, true) window.removeEventListener('resize', handleUpdate) + window.visualViewport?.removeEventListener('resize', handleUpdate) + window.visualViewport?.removeEventListener('scroll', handleUpdate) } } }, [open, updatePosition]) @@ -85,11 +103,8 @@ const Select = React.forwardRef( } if (open) { - // 使用 setTimeout 确保 DOM 已更新 - setTimeout(() => { - document.addEventListener('mousedown', handleClickOutside) - document.addEventListener('keydown', handleEscape) - }, 0) + document.addEventListener('mousedown', handleClickOutside) + document.addEventListener('keydown', handleEscape) } return () => { @@ -106,6 +121,8 @@ const Select = React.forwardRef( id={id as string} type="button" disabled={disabled} + aria-haspopup="listbox" + aria-expanded={open} onClick={() => setOpen((v) => !v)} className={cn( "flex h-10 w-full rounded-lg border border-slate-200 bg-white/90 px-4 py-2 pr-8 text-sm text-slate-800 shadow-[0_1px_2px_rgba(15,23,42,0.03)] transition-all duration-200 hover:border-blue-200 hover:bg-white dark:border-white/10 dark:bg-white/5 dark:text-slate-100", @@ -126,14 +143,17 @@ const Select = React.forwardRef( top: `${dropdownPosition.top}px`, left: `${dropdownPosition.left}px`, width: `${dropdownPosition.width}px`, + maxHeight: `${dropdownPosition.maxHeight}px`, }} > -
    +
      {options.map((o) => { const active = String(value ?? '') === String(o.value) return (
    • { } } -async function checkLogin(platform: "boss" | "zhilian"): Promise { +async function checkLogin(platform: "boss" | "zhilian", openIfMissing = false): Promise { const title = platform === "boss" ? "Boss登录状态" : "智联登录状态" const href = platform === "boss" ? "/boss" : "/zhilian" @@ -170,7 +171,7 @@ async function checkLogin(platform: "boss" | "zhilian"): Promise } } - const status = await getZhilianPageStatus() + const status = await getZhilianPageStatus({ openIfMissing }) return item("zhilianLogin", title, status.ready, status.message, "检查智联页面", href, !status.connected) } @@ -197,7 +198,7 @@ export async function validateSetupForPlatform(platform: "boss" | "zhilian", opt checkResume(), ] if (requirePlatformLogin) { - checkers.push(checkLogin(platform)) + checkers.push(checkLogin(platform, options.openPlatformPageIfMissing === true)) } const items = await Promise.all(checkers) const requiredKeys: SetupCheckKey[] = ["backend", "chromeBridge", "aiConfig", "resume"] diff --git a/front/lib/zhilian-page-status.test.ts b/front/lib/zhilian-page-status.test.ts index 954f75f..fb92b19 100644 --- a/front/lib/zhilian-page-status.test.ts +++ b/front/lib/zhilian-page-status.test.ts @@ -12,6 +12,11 @@ beforeEach(() => { }) describe('智联 Chrome 状态', () => { + it('主动启动允许自动开页,并留出加载时间;登录和安全验证仍阻止扫描', async () => { + vi.mocked(sendChromeBridgeMessage).mockResolvedValue({ success: true, chromePageReady: true, hasLoginPrompt: true }) + expect((await getZhilianPageStatus({ openIfMissing: true })).ready).toBe(false) + expect(sendChromeBridgeMessage).toHaveBeenCalledWith({ type: 'ZHILIAN_PAGE_STATUS', platform: 'zhilian', openIfMissing: true }, 35000) + }) it('Chrome 可用时不再读取后端浏览器登录状态', async () => { const fetcher = vi.fn(async (url: string) => ({ ok: true, json: async () => url.endsWith('/api/ready') ? { ready: true, status: "UP" } : { success: true, data: { introduce: '简介', prompt: '分析', resumeText: '简历' } } })) diff --git a/front/lib/zhilian-page-status.ts b/front/lib/zhilian-page-status.ts index a040859..ca5f76c 100644 --- a/front/lib/zhilian-page-status.ts +++ b/front/lib/zhilian-page-status.ts @@ -6,11 +6,14 @@ export type ZhilianPageStatus = { message: string } -export async function getZhilianPageStatus(): Promise { +export async function getZhilianPageStatus(options: { openIfMissing?: boolean } = {}): Promise { try { const bridge = await getChromeBridgeStatus() if (!bridge.success) return { connected: false, ready: false, message: bridge.message || 'Chrome扩展未连接,请加载或重新加载扩展。' } - const status = await sendChromeBridgeMessage({ type: 'ZHILIAN_PAGE_STATUS', platform: 'zhilian' }, 8000) + const status = await sendChromeBridgeMessage({ + type: 'ZHILIAN_PAGE_STATUS', platform: 'zhilian', + ...(options.openIfMissing ? { openIfMissing: true } : {}), + }, options.openIfMissing ? 35000 : 8000) const ready = status.success === true && status.chromePageReady === true && !status.hasLoginPrompt && !status.hasSecurityPrompt return {