ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

2086 lines
81 KiB

'use strict';
const path = require('path');
const request = require('superagent');
const { chromium } = require('playwright-core');
const CNIPA_CPQUERY_BASE_URL = 'https://cpquery.cponline.cnipa.gov.cn';
const CNIPA_AM_BASE_URL = 'https://tysf.cponline.cnipa.gov.cn/am';
const CPQUERY_COOKIE_DOMAINS = [
'tysf.cponline.cnipa.gov.cn',
'tysfjk.cponline.cnipa.gov.cn',
'sso.cponline.cnipa.gov.cn',
'cpquery.cponline.cnipa.gov.cn',
'cponline.cnipa.gov.cn',
'.cponline.cnipa.gov.cn',
];
let latestQueryAuth = null;
let persistentCpqueryContext = null;
let persistentCpqueryHeadless = null;
let persistentLoginPage = null;
let latestLoginFlow = null;
let cnipaOpQueue = Promise.resolve();
const BROWSER_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36';
const CNIPA_READY_COOKIE_PATTERN = /(?:^|;\s*)(?:enable_dX1xbeyMT58W|dX1xbeyMT58WO|dX1xbeyMT58WP)=/;
const CNIPA_AUTH_REDIS_KEY = 'cnipa:auth:v1:shared-account';
const CNIPA_AUTH_REDIS_TTL_SECONDS = 12 * 60 * 60;
const readCnipaConfig = (ctx) => ctx.app.fs.config?.cnipa || {};
const getRedisClient = (ctx) => ctx?.app?.fs?.redis || null;
const loadCnipaAuthState = async (ctx) => {
const redis = getRedisClient(ctx);
if (!redis) return;
const raw = await redis.get(CNIPA_AUTH_REDIS_KEY);
if (!raw) return;
try {
const parsed = JSON.parse(raw);
latestQueryAuth = Object.prototype.hasOwnProperty.call(parsed || {}, 'latestQueryAuth')
? parsed.latestQueryAuth
: latestQueryAuth;
latestLoginFlow = Object.prototype.hasOwnProperty.call(parsed || {}, 'latestLoginFlow')
? parsed.latestLoginFlow
: latestLoginFlow;
} catch (error) {
// Ignore invalid cached auth state.
}
};
const saveCnipaAuthState = async (ctx) => {
const redis = getRedisClient(ctx);
if (!redis) return;
const payload = JSON.stringify({
latestQueryAuth: latestQueryAuth || null,
latestLoginFlow: latestLoginFlow || null,
updatedAt: Date.now(),
});
await redis.set(CNIPA_AUTH_REDIS_KEY, payload, 'EX', CNIPA_AUTH_REDIS_TTL_SECONDS);
};
const withCnipaSerializedOp = async (fn) => {
const prev = cnipaOpQueue;
let release;
cnipaOpQueue = new Promise(resolve => { release = resolve; });
await prev;
try {
return await fn();
} finally {
release();
}
};
const getCookieNames = (cookieHeader) => String(cookieHeader || '')
.split(';')
.map(item => item.trim().split('=')[0])
.filter(Boolean);
const hasCnipaReadyCookie = (cookieHeader) => CNIPA_READY_COOKIE_PATTERN.test(String(cookieHeader || ''));
const getCookieDiagnostics = (cookieHeader) => ({
hasSession: /SESSION=/.test(String(cookieHeader || '')),
hasEnableFlag: hasCnipaReadyCookie(cookieHeader),
hasLegacyEnableFlag: /enable_dX1xbeyMT58W=/.test(String(cookieHeader || '')),
hasWafCookie: /(?:^|;\s*)dX1xbeyMT58W[OP]=/.test(String(cookieHeader || '')),
cookieLength: String(cookieHeader || '').length,
cookieNames: getCookieNames(cookieHeader),
});
const isCnipaAuthExpiredError = (error) => {
const status = Number(error?.status || error?.response?.status || 0);
const message = [
error?.message,
error?.response?.body?.message,
error?.response?.body?.msg,
error?.response?.body?.diagnostics?.currentUrl,
error?.response?.body?.diagnostics?.bodyText,
].filter(Boolean).join(' ');
return status === 401 ||
status === 403 ||
status === 412 ||
/未登录|请登录|登录态|授权|认证|Precondition|前置条件|oauth|chrome-error|ERR_TOO_MANY_REDIRECTS/i.test(message);
};
const invalidateCnipaAuthState = () => {
latestQueryAuth = null;
latestLoginFlow = null;
};
const logCnipaDebug = (ctx, message, detail = {}) => {
const logger = ctx?.logger;
if (!logger?.log) return;
logger.log(`[CNIPA] ${message}`, detail);
};
const launchChromiumWithFallback = async (launchOptions = {}) => {
// 优先使用系统 Chromium(Docker 环境)
const systemChromium = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
if (systemChromium) {
try {
return await chromium.launch({
...launchOptions,
executablePath: systemChromium,
});
} catch (error) {
// 系统 Chromium 失败,继续尝试其他方式
}
}
const attempts = [
{ ...launchOptions, channel: 'chrome' },
{ ...launchOptions, channel: 'msedge' },
{ ...launchOptions },
];
let lastError = null;
for (const options of attempts) {
try {
return await chromium.launch(options);
} catch (error) {
lastError = error;
}
}
const error = new Error('未检测到可用浏览器。请安装 Chrome/Edge,或在服务器安装 Playwright Chromium 浏览器。');
error.cause = lastError;
throw error;
};
const launchPersistentChromiumWithFallback = async (userDataDir, launchOptions = {}) => {
// 优先使用系统 Chromium(Docker 环境)
const systemChromium = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
if (systemChromium) {
try {
return await chromium.launchPersistentContext(userDataDir, {
...launchOptions,
executablePath: systemChromium,
});
} catch (error) {
// 系统 Chromium 失败,继续尝试其他方式
}
}
const attempts = [
{ ...launchOptions, channel: 'chrome' },
{ ...launchOptions, channel: 'msedge' },
{ ...launchOptions },
];
let lastError = null;
for (const options of attempts) {
try {
return await chromium.launchPersistentContext(userDataDir, options);
} catch (error) {
lastError = error;
}
}
const error = new Error('未检测到可用浏览器。请安装 Chrome/Edge,或在服务器安装 Playwright Chromium 浏览器。');
error.cause = lastError;
throw error;
};
const mergeCookieHeaders = (...cookieHeaders) => {
const cookies = new Map();
cookieHeaders.filter(Boolean).forEach(header => {
String(header).split(';').map(item => item.trim()).filter(Boolean).forEach(item => {
const index = item.indexOf('=');
if (index <= 0) return;
const name = item.substring(0, index);
const value = item.substring(index + 1);
if (/^(path|domain|expires|max-age|secure|httponly|samesite)$/i.test(name)) return;
cookies.set(name, value);
});
});
return [...cookies.entries()].map(([name, value]) => `${name}=${value}`).join('; ');
};
const cookieHeaderToPlaywrightCookies = (cookieHeader, domains) => {
const pairs = String(cookieHeader || '')
.split(';')
.map(item => item.trim())
.filter(Boolean)
.map(item => {
const index = item.indexOf('=');
return index > 0
? { name: item.substring(0, index), value: item.substring(index + 1) }
: null;
})
.filter(Boolean);
const uniquePairs = [...new Map(pairs.map(item => [item.name, item])).values()];
return domains.flatMap(domain => uniquePairs.map(item => ({
name: item.name,
value: item.value,
domain,
path: '/',
secure: true,
httpOnly: false,
sameSite: 'Lax',
})));
};
const collectSetCookieHeader = (response) => {
const setCookie = response?.headers?.['set-cookie'];
if (Array.isArray(setCookie)) return setCookie.join('; ');
return setCookie || '';
};
const buildAmHeaders = (cookieHeader) => ({
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'Content-Type': 'application/json;charset=UTF-8',
Origin: 'https://tysf.cponline.cnipa.gov.cn',
Referer: `${CNIPA_AM_BASE_URL}/`,
'User-Agent': BROWSER_USER_AGENT,
'sec-ch-ua': '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
...(cookieHeader ? { Cookie: cookieHeader } : {}),
});
const postCnipaAm = async (pathName, body, cookieHeader) => {
const response = await request
.post(`${CNIPA_AM_BASE_URL}${pathName}`)
.set(buildAmHeaders(cookieHeader))
.send(body)
.timeout({ response: 20000, deadline: 30000 });
const nextCookie = mergeCookieHeaders(cookieHeader, collectSetCookieHeader(response));
return {
body: response.body,
text: response.text,
cookie: nextCookie,
status: response.status,
};
};
const getCnipaAm = async (pathName = '/', cookieHeader) => {
const response = await request
.get(`${CNIPA_AM_BASE_URL}${pathName}`)
.set({
...buildAmHeaders(cookieHeader),
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Content-Type': 'text/plain;charset=UTF-8',
})
.timeout({ response: 20000, deadline: 30000 });
const nextCookie = mergeCookieHeaders(cookieHeader, collectSetCookieHeader(response));
return {
body: response.body,
text: response.text,
cookie: nextCookie,
status: response.status,
};
};
const getCnipaPage = async (url, cookieHeader, referer) => {
const response = await request
.get(url)
.set({
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'User-Agent': BROWSER_USER_AGENT,
...(referer ? { Referer: referer } : {}),
...(cookieHeader ? { Cookie: cookieHeader } : {}),
})
.redirects(5)
.timeout({ response: 20000, deadline: 30000 });
const nextCookie = mergeCookieHeaders(cookieHeader, collectSetCookieHeader(response));
return {
body: response.body,
text: response.text,
cookie: nextCookie,
status: response.status,
url: response.redirects?.[response.redirects.length - 1] || url,
};
};
const bridgeCnipaPortalSession = async (cookieHeader, redirectURL) => {
let cookie = cookieHeader || '';
const diagnostics = [];
const redirectTarget = String(redirectURL || '').startsWith('//')
? `https:${redirectURL}`
: (String(redirectURL || '').startsWith('http') ? redirectURL : 'https://tysfjk.cponline.cnipa.gov.cn/portal');
const steps = [
{ url: redirectTarget, referer: `${CNIPA_AM_BASE_URL}/` },
{ url: 'https://tysfjk.cponline.cnipa.gov.cn/portal', referer: redirectTarget },
{ url: `${CNIPA_CPQUERY_BASE_URL}/chinesepatent/index`, referer: 'https://tysfjk.cponline.cnipa.gov.cn/portal' },
];
for (const step of steps) {
try {
const res = await getCnipaPage(step.url, cookie, step.referer);
cookie = mergeCookieHeaders(cookie, res.cookie);
diagnostics.push({ step: step.url, status: res.status, finalUrl: res.url });
} catch (error) {
diagnostics.push({ step: step.url, error: error.message });
}
}
return { cookie, diagnostics };
};
const bridgeCnipaPortalSessionByBrowser = async (cookieHeader, redirectURL, cnipaConfig = {}) => {
const diagnostics = [];
const redirectTarget = String(redirectURL || '').startsWith('//')
? `https:${redirectURL}`
: (String(redirectURL || '').startsWith('http') ? redirectURL : 'https://tysfjk.cponline.cnipa.gov.cn/portal');
const browser = await launchChromiumWithFallback({
headless: cnipaConfig.browserQueryHeadless !== false,
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-software-rasterizer',
],
});
try {
const context = await browser.newContext({
userAgent: BROWSER_USER_AGENT,
locale: 'zh-CN',
viewport: { width: 1366, height: 768 },
});
if (cookieHeader) {
await context.addCookies(cookieHeaderToPlaywrightCookies(cookieHeader, CPQUERY_COOKIE_DOMAINS));
}
const page = await context.newPage();
const steps = [
{ url: redirectTarget, referer: `${CNIPA_AM_BASE_URL}/` },
{ url: 'https://tysfjk.cponline.cnipa.gov.cn/portal', referer: redirectTarget },
{ url: `${CNIPA_CPQUERY_BASE_URL}/chinesepatent/index`, referer: 'https://tysfjk.cponline.cnipa.gov.cn/portal' },
];
for (const step of steps) {
try {
const resp = await page.goto(step.url, {
waitUntil: 'domcontentloaded',
timeout: 25000,
referer: step.referer,
});
await page.waitForTimeout(1200).catch(() => {});
const status = resp?.status?.() || null;
diagnostics.push({
step: step.url,
status,
finalUrl: page.url(),
});
if (status === 412) {
await retryCpqueryAfterPrecondition(page, context, step, diagnostics);
}
} catch (error) {
diagnostics.push({ step: step.url, error: error.message });
}
}
const cookie = await collectContextCookieHeader(context);
return {
cookie: mergeCookieHeaders(cookieHeader, cookie),
diagnostics,
};
} finally {
await browser.close().catch(() => {});
}
};
const collectBrowserDiagnostics = async (page, networkTrace = []) => {
let title = '';
let bodyText = '';
let resources = [];
try {
title = await page.title();
} catch (error) {
title = '';
}
try {
bodyText = await page.locator('body').innerText({ timeout: 1500 });
} catch (error) {
bodyText = '';
}
try {
resources = await page.evaluate(() => performance.getEntriesByType('resource').map(item => item.name).slice(-30));
} catch (error) {
resources = [];
}
return {
currentUrl: page.url(),
title,
bodyText: String(bodyText || '').slice(0, 500),
recentNetwork: networkTrace.slice(-30),
recentResources: resources,
challengeDiagnostics: page.__cnipaCpqueryChallengeDiagnostics || [],
};
};
const collectContextCookieHeader = async (context) => {
const cookies = await context.cookies().catch(() => []);
return cookies
.filter(item => item.domain && item.name)
.map(item => `${item.name}=${item.value}`)
.join('; ');
};
const retryCpqueryAfterPrecondition = async (page, context, step, diagnostics) => {
if (!String(step.url || '').includes('cpquery.cponline.cnipa.gov.cn')) return null;
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
await page.waitForTimeout(2500).catch(() => {});
const retryResp = await page.reload({
waitUntil: 'domcontentloaded',
timeout: 25000,
}).catch(error => {
diagnostics.push({
step: `${step.url}#after-412-reload`,
error: error.message,
...getCookieDiagnostics(''),
});
return null;
});
await page.waitForTimeout(1200).catch(() => {});
const retryCookie = await collectContextCookieHeader(context);
diagnostics.push({
step: `${step.url}#after-412-reload`,
status: retryResp?.status?.() || null,
finalUrl: page.url(),
...getCookieDiagnostics(retryCookie),
});
return retryResp;
};
const createCpqueryBrowser = async (cnipaConfig = {}, options = {}) => {
const headless = options.headless ?? cnipaConfig.browserHeadless === true;
if (persistentCpqueryContext) {
if (persistentCpqueryHeadless !== headless) {
await persistentCpqueryContext.close().catch(() => {});
persistentCpqueryContext = null;
persistentCpqueryHeadless = null;
} else {
return {
context: persistentCpqueryContext,
close: async () => {},
};
}
}
if (persistentCpqueryContext) {
return {
context: persistentCpqueryContext,
close: async () => {},
};
}
const userDataDir = cnipaConfig.browserUserDataDir || path.join(process.cwd(), '.cnipa-browser');
persistentCpqueryContext = await launchPersistentChromiumWithFallback(userDataDir, {
headless,
userAgent: BROWSER_USER_AGENT,
locale: 'zh-CN',
viewport: { width: 1366, height: 768 },
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-software-rasterizer',
],
});
persistentCpqueryHeadless = headless;
persistentCpqueryContext.on('close', () => {
persistentCpqueryContext = null;
persistentCpqueryHeadless = null;
});
return {
context: persistentCpqueryContext,
close: async () => {},
};
};
const getCnipaUserDataDir = (cnipaConfig = {}) => cnipaConfig.browserUserDataDir || path.join(process.cwd(), '.cnipa-browser');
const getCpqueryPage = async (cnipaConfig = {}, options = {}) => {
const browser = await createCpqueryBrowser(cnipaConfig, options);
const context = browser.context;
const page = persistentLoginPage && !persistentLoginPage.isClosed()
? persistentLoginPage
: context.pages().find(item => !item.isClosed()) || await context.newPage();
persistentLoginPage = page;
return { browser, context, page };
};
const isLoginStateReady = async (page) => {
try {
const bodyText = await page.locator('body').innerText({ timeout: 2500 });
const text = String(bodyText || '').replace(/\s+/g, '');
if (/欢迎您|退出登录/.test(text) && !/用户登录|账号登录|密码登录|请登录/.test(text)) return true;
if (/案件查询|申请号\/专利号|共查询到/.test(text)) return true;
} catch (error) {
// Treat unreadable pages as not ready.
}
return false;
};
const refreshLatestBrowserCookies = async (context, page) => {
const browserCookies = await context.cookies().catch(() => []);
const cookie = browserCookies
.filter(item => item.domain && item.name)
.map(item => `${item.name}=${item.value}`)
.join('; ');
latestQueryAuth = {
...(latestQueryAuth || {}),
cookie: mergeCookieHeaders(latestQueryAuth?.cookie, cookie) || null,
redirectURL: page.url(),
cpqueryReferer: page.url(),
browserReady: true,
updatedAt: Date.now(),
};
return latestQueryAuth.cookie;
};
const ensureCnipaAmSession = async (ctx) => {
if (!latestLoginFlow?.cookie || !/SESSION=/.test(latestLoginFlow.cookie)) {
const initRes = await getCnipaAm('/', latestLoginFlow?.cookie || latestQueryAuth?.cookie || '').catch(() => null);
const cookie = initRes?.cookie || latestLoginFlow?.cookie || latestQueryAuth?.cookie || '';
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: mergeCookieHeaders(latestLoginFlow?.cookie, cookie),
updatedAt: Date.now(),
};
}
return latestLoginFlow?.cookie || latestQueryAuth?.cookie || '';
};
const createCaptchaClientUid = () => `slider-${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
const normalizeCaptchaPointJson = (requestBody = {}) => {
if (requestBody.pointJson) return requestBody.pointJson;
const x = Number(requestBody.x);
const y = Number(requestBody.y);
if (Number.isFinite(x)) {
return JSON.stringify({
x: Math.round(x),
y: Number.isFinite(y) ? Math.round(y) : 5,
});
}
return '';
};
const buildCnipaLoginPayload = (cnipaConfig = {}, requestBody = {}) => {
const loginName = requestBody.loginName || requestBody.username || cnipaConfig.loginName || cnipaConfig.username;
const encryptedPassword = requestBody.encryptedPassword || cnipaConfig.encryptedPassword || cnipaConfig.password;
if (!loginName || !encryptedPassword) {
const error = new Error('未配置 CNIPA_LOGIN_NAME 或 CNIPA_ENCRYPTED_PASSWORD,无法调用 CNIPA 登录接口');
error.status = 400;
throw error;
}
return {
callbackVOList: [
{
callback: {
loginName,
moduleName: 'PasswordModule',
prompt: '用户名',
step: 1,
subjectNeedFill: true,
},
callbackType: 'cn.org.bjca.am.authframework.callback.LoginNameCallback',
order: 0,
},
{
callback: {
encoding: false,
moduleName: 'PasswordModule',
password: encryptedPassword,
prompt: '密码',
step: 1,
subjectNeedFill: true,
},
callbackType: 'cn.org.bjca.am.authframework.callback.PasswordCallback',
order: 1,
},
{
callback: {
choices: { 1: '个人用户', 2: '单位用户', 3: '代办人' },
defaultChoice: '',
moduleName: 'PasswordModule',
multipleSelectionsAllowed: false,
prompt: '111',
selectedKey: String(requestBody.userType || cnipaConfig.loginUserType || '1'),
step: 1,
subjectNeedFill: true,
},
callbackType: 'cn.org.bjca.am.authframework.callback.ChoiceCallback',
order: 2,
},
{
callback: {
moduleName: 'PasswordModule',
prompt: '验证码',
step: 1,
subjectNeedFill: true,
validateCode: requestBody.validateCode || '',
},
callbackType: 'cn.org.bjca.am.authframework.callback.ValidateCodeCallback',
order: 3,
},
{
callback: {
moduleName: 'PasswordModule',
step: 1,
subjectNeedFill: true,
type: requestBody.appType || '',
},
callbackType: 'cn.org.bjca.am.authframework.callback.AppTypeCallback',
order: 4,
},
],
indexName: 'initService',
indexType: 'service',
moduleName: 'PasswordModule',
step: 1,
};
};
const syncLoginCookieToBrowser = async (ctx, cookieHeader) => {
const cnipaConfig = readCnipaConfig(ctx);
const { context, page } = await getCpqueryPage(cnipaConfig, {
headless: cnipaConfig.browserQueryHeadless !== false,
});
await context.clearCookies().catch(() => {});
if (cookieHeader) {
await context.addCookies(cookieHeaderToPlaywrightCookies(cookieHeader, CPQUERY_COOKIE_DOMAINS));
}
const redirectURL = latestLoginFlow?.loginResponse?.data?.callback?.redirectURL
|| latestLoginFlow?.loginResponse?.data?.redirectURL
|| '';
const redirectTarget = String(redirectURL || '').startsWith('//')
? `https:${redirectURL}`
: (String(redirectURL || '').startsWith('http') ? redirectURL : 'https://tysfjk.cponline.cnipa.gov.cn/portal');
await page.goto(redirectTarget, {
waitUntil: 'domcontentloaded',
timeout: 25000,
referer: `${CNIPA_AM_BASE_URL}/`,
}).catch(() => {});
await page.waitForTimeout(1500).catch(() => {});
// 先经过统一身份入口,完成 SSO 会话桥接,再进入 cpquery。
await page.goto('https://tysfjk.cponline.cnipa.gov.cn/portal', {
waitUntil: 'domcontentloaded',
timeout: 20000,
referer: `${CNIPA_AM_BASE_URL}/`,
}).catch(() => {});
await page.waitForTimeout(1200).catch(() => {});
await page.goto(`${CNIPA_CPQUERY_BASE_URL}/chinesepatent/index`, {
waitUntil: 'domcontentloaded',
timeout: 20000,
referer: 'https://tysfjk.cponline.cnipa.gov.cn/portal',
}).catch(() => {});
try {
await page.waitForLoadState('networkidle', { timeout: 6000 });
} catch (error) {
// some pages keep long polling
}
await page.waitForTimeout(1200).catch(() => {});
const cookie = await refreshLatestBrowserCookies(context, page);
return {
loggedIn: await isLoginStateReady(page),
cookie,
diagnostics: await collectBrowserDiagnostics(page),
};
};
const getCnipaLoginCredentials = (cnipaConfig = {}, requestBody = {}) => ({
loginName: requestBody.loginName
|| requestBody.username
|| cnipaConfig.loginName
|| cnipaConfig.username
|| '',
password: requestBody.password
|| requestBody.encryptedPassword
|| cnipaConfig.password
|| cnipaConfig.encryptedPassword
|| '',
});
const collectLoginPanelState = async (page) => page.evaluate(() => {
const normalize = (text) => String(text || '').replace(/\s+/g, '');
const visible = element => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const checkedTab = [...document.querySelectorAll('.title-group label, label[role="radio"], .el-radio')]
.filter(visible)
.find(element => element.classList.contains('is-checked') || element.getAttribute('aria-checked') === 'true');
return {
checkedTabText: normalize(checkedTab?.innerText || checkedTab?.textContent),
placeholders: [...document.querySelectorAll('input')]
.filter(visible)
.map(element => element.placeholder || element.getAttribute('aria-label') || element.type || ''),
};
}).catch(() => ({
checkedTabText: '',
placeholders: [],
}));
const selectLegalLoginTab = async (page) => {
const tabSelectors = [
'label:has-text("法人登录")',
'label[role="radio"]:has-text("法人登录")',
'.title-group label:has-text("法人登录")',
'.title-group .el-radio:has-text("法人登录")',
'span:has-text("法人登录")',
'input.el-radio__original[value="2"]',
];
for (const selector of tabSelectors) {
const locator = page.locator(selector).first();
try {
await locator.waitFor({ state: 'visible', timeout: 4000 });
await locator.click({ timeout: 4000, force: true });
return true;
} catch (error) {
// Try the next selector; the login page has changed markup before.
}
}
return page.evaluate(() => {
const normalize = (text) => String(text || '').replace(/\s+/g, '');
const target = [...document.querySelectorAll('label,button,div,span,a,li')]
.find(element => /法人登录/.test(normalize(element.textContent)));
if (!target) return false;
const rect = target.getBoundingClientRect();
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
};
target.dispatchEvent(new PointerEvent('pointerdown', eventInit));
target.dispatchEvent(new MouseEvent('mousedown', eventInit));
target.dispatchEvent(new PointerEvent('pointerup', eventInit));
target.dispatchEvent(new MouseEvent('mouseup', eventInit));
target.dispatchEvent(new MouseEvent('click', eventInit));
if (typeof target.click === 'function') target.click();
return true;
}).catch(() => false);
};
const fillVisibleInputByPattern = async (page, patterns, value) => page.evaluate(({ patterns: patternSources, value: inputValue }) => {
const regexList = patternSources.map(source => new RegExp(source));
const visible = element => {
if (!element || element.disabled || element.readOnly) return false;
const type = String(element.type || '').toLowerCase();
if (['hidden', 'radio', 'checkbox', 'button', 'submit'].includes(type)) return false;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const textOf = element => [
element.placeholder,
element.getAttribute('name'),
element.getAttribute('id'),
element.getAttribute('aria-label'),
element.closest('.el-form-item')?.innerText,
element.closest('form')?.innerText,
element.parentElement?.innerText,
].filter(Boolean).join(' ');
const target = [...document.querySelectorAll('input')]
.filter(visible)
.find(element => regexList.some(regex => regex.test(textOf(element))));
if (!target) return false;
target.focus();
const prototype = Object.getPrototypeOf(target);
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
if (descriptor?.set) descriptor.set.call(target, inputValue);
else target.value = inputValue;
target.dispatchEvent(new Event('input', { bubbles: true }));
target.dispatchEvent(new Event('change', { bubbles: true }));
target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: '0' }));
target.blur();
return true;
}, {
patterns: patterns.map(pattern => pattern.source),
value,
}).catch(() => false);
const clickVisibleLoginButton = async (page) => {
const loginButton = page.locator('form button:has-text("登录"), button.login-btn:has-text("登录")').first();
try {
await loginButton.waitFor({ state: 'visible', timeout: 4000 });
await loginButton.click({ timeout: 4000 });
return true;
} catch (error) {
return page.evaluate(() => {
const normalize = (text) => String(text || '').replace(/\s+/g, '');
const visible = element => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const button = [...document.querySelectorAll('form button,button')]
.filter(visible)
.find(element => /^登录$/.test(normalize(element.textContent)));
if (!button) return false;
button.click();
return true;
}).catch(() => false);
}
};
const autoFillLegalLogin = async (page, cnipaConfig = {}, requestBody = {}) => {
const credentials = getCnipaLoginCredentials(cnipaConfig, requestBody);
const result = {
legalTabClicked: false,
legalTabReady: false,
loginFilled: false,
passwordFilled: false,
loginClicked: false,
missingLoginName: !credentials.loginName,
missingPassword: !credentials.password,
beforeState: await collectLoginPanelState(page),
afterState: null,
};
for (let attempt = 0; attempt < 3; attempt += 1) {
const clicked = await selectLegalLoginTab(page);
if (clicked) result.legalTabClicked = true;
await page.waitForTimeout(120).catch(() => {});
const state = await collectLoginPanelState(page);
if ((state.checkedTabText || '').includes('法人登录')) break;
}
try {
await page.locator('input[placeholder*="统一社会信用代码"]').first().waitFor({ state: 'visible', timeout: 1500 });
result.legalTabReady = true;
} catch (error) {
const state = await collectLoginPanelState(page);
result.legalTabReady = state.placeholders.some(item => /统一社会信用代码/.test(item));
}
if (credentials.loginName) {
try {
await page.locator('input[placeholder*="统一社会信用代码"]').first().fill(credentials.loginName, { timeout: 4000 });
result.loginFilled = true;
} catch (error) {
result.loginFilled = await fillVisibleInputByPattern(page, [/统一社会信用代码|用户名|账号|登录名/], credentials.loginName);
}
}
if (credentials.password) {
try {
await page.locator('input[type="password"], input[placeholder*="密码"]').first().fill(credentials.password, { timeout: 4000 });
result.passwordFilled = true;
} catch (error) {
result.passwordFilled = await fillVisibleInputByPattern(page, [/密码/], credentials.password);
}
}
if (result.loginFilled && result.passwordFilled) {
result.loginClicked = await clickVisibleLoginButton(page);
}
result.afterState = await collectLoginPanelState(page);
return result;
};
const openPreparedCpqueryBrowser = async (ctx, url = `${CNIPA_CPQUERY_BASE_URL}/chinesepatent/index`) => {
const cnipaConfig = readCnipaConfig(ctx);
const requestBody = ctx.request.body || {};
const browser = await createCpqueryBrowser(cnipaConfig, {
headless: requestBody.headless === true || cnipaConfig.browserPrepareHeadless === true,
});
const context = browser.context;
const page = context.pages()[0] || await context.newPage();
await page.bringToFront();
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
const autoLogin = await autoFillLegalLogin(page, cnipaConfig, requestBody).catch(() => ({
legalTabClicked: false,
legalTabReady: false,
loginFilled: false,
passwordFilled: false,
loginClicked: false,
missingLoginName: !getCnipaLoginCredentials(cnipaConfig, requestBody).loginName,
missingPassword: !getCnipaLoginCredentials(cnipaConfig, requestBody).password,
}));
const browserCookies = await context.cookies().catch(() => []);
const cookie = browserCookies
.filter(item => item.domain && item.name)
.map(item => `${item.name}=${item.value}`)
.join('; ');
latestQueryAuth = {
...(latestQueryAuth || {}),
cookie: mergeCookieHeaders(latestQueryAuth?.cookie, cookie) || null,
redirectURL: page.url(),
cpqueryReferer: page.url(),
browserReady: true,
updatedAt: Date.now(),
};
return {
url: page.url(),
diagnostics: await collectBrowserDiagnostics(page),
userDataDir: cnipaConfig.browserUserDataDir || path.join(process.cwd(), '.cnipa-browser'),
headless: persistentCpqueryHeadless === true,
autoLogin,
};
};
const setSearchInputValue = async (page, value) => {
for (const frame of page.frames()) {
try {
const success = await frame.evaluate((patentNumber) => {
const hiddenTypes = ['hidden', 'radio', 'checkbox', 'button', 'submit'];
const isUsable = element => {
const style = window.getComputedStyle(element);
if (style.visibility === 'hidden' || style.display === 'none') return false;
if (element.disabled || element.readOnly) return false;
if (hiddenTypes.includes(String(element.type || '').toLowerCase())) return false;
return true;
};
const center = rect => ({
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
});
const findLabelRect = () => {
const elements = [...document.querySelectorAll('label,span,div,p,td,th')]
.filter(element => /申请号\s*\/\s*专利号|申请号|专利号/.test(String(element.textContent || '')))
.map(element => ({ element, rect: element.getBoundingClientRect() }))
.filter(item => item.rect.width > 0 && item.rect.height > 0);
return elements[0]?.rect || null;
};
const textOf = element => [
element.placeholder,
element.getAttribute('aria-label'),
element.getAttribute('name'),
element.getAttribute('id'),
element.closest('.el-form-item')?.innerText,
element.closest('.ant-form-item')?.innerText,
element.parentElement?.innerText,
].filter(Boolean).join(' ');
const controls = [...document.querySelectorAll('input,textarea,[contenteditable="true"],[role="textbox"]')]
.filter(isUsable)
.map((element, index) => {
const text = textOf(element);
let score = 0;
const rect = element.getBoundingClientRect();
if (/2010|申请号|专利号|申请/.test(text)) score += 10;
if (/请输入|输入/.test(text)) score += 2;
if (rect.width > 80 && rect.height > 15) score += 1;
return { element, index, score, rect };
})
.sort((a, b) => b.score - a.score || a.index - b.index);
const labelRect = findLabelRect();
const coordinateTarget = labelRect
? controls
.map(item => {
const labelCenter = center(labelRect);
const itemCenter = center(item.rect);
const sameRowPenalty = Math.abs(itemCenter.y - labelCenter.y) * 20;
const rightPenalty = itemCenter.x >= labelRect.right ? 0 : 10000;
const distance = rightPenalty + sameRowPenalty + Math.max(0, itemCenter.x - labelRect.right);
return { ...item, distance };
})
.sort((a, b) => a.distance - b.distance)[0]
: null;
const target = coordinateTarget?.distance < 4000
? coordinateTarget.element
: controls[0]?.element;
if (!target) return false;
target.setAttribute('data-cnipa-query-input', 'true');
target.focus();
if (target.isContentEditable) {
target.textContent = patentNumber;
} else {
const prototype = Object.getPrototypeOf(target);
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
if (descriptor?.set) {
descriptor.set.call(target, patentNumber);
} else {
target.value = patentNumber;
}
}
target.dispatchEvent(new Event('input', { bubbles: true }));
target.dispatchEvent(new Event('change', { bubbles: true }));
target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: '0' }));
const buttonCandidates = [...document.querySelectorAll('button,.q-btn,.el-button,.ant-btn,[role="button"]')]
.filter(element => isUsable(element))
.map((element, index) => ({
element,
index,
text: String(element.innerText || element.textContent || element.getAttribute('aria-label') || '').trim(),
rect: element.getBoundingClientRect(),
}))
.filter(item => {
const compactText = item.text.replace(/\s+/g, '');
if (!/查询/.test(compactText)) return false;
if (/中国|多国|审查信息|重置|更多|展开|案件查询|申请方式查询/.test(compactText)) return false;
if (item.rect.top < target.getBoundingClientRect().bottom) return false;
return compactText.length <= 12 || item.element.matches('button,.q-btn,.el-button,.ant-btn');
})
.map(item => {
const targetRect = target.getBoundingClientRect();
const itemCenter = center(item.rect);
const targetCenter = center(targetRect);
const verticalDistance = Math.abs(itemCenter.y - (targetRect.bottom + 95));
const horizontalDistance = Math.abs(itemCenter.x - targetCenter.x);
return {
...item,
distance: verticalDistance * 4 + horizontalDistance,
};
})
.sort((a, b) => a.distance - b.distance || a.index - b.index);
const searchButton = buttonCandidates[0]?.element;
if (searchButton) {
searchButton.setAttribute('data-cnipa-query-button', 'true');
searchButton.setAttribute('title', 'CNIPA 自动查询按钮');
searchButton.style.outline = '2px solid #ef4444';
}
return true;
}, value);
if (success) return frame;
} catch (error) {
// Try the next frame.
}
}
return null;
};
const findDetailUrlInPage = async (page) => page.evaluate(() => {
const resolveUrl = (value) => {
if (!value) return null;
try {
return new URL(value, location.href).toString();
} catch (error) {
return null;
}
};
const anchors = [...document.querySelectorAll('a[href]')];
for (const anchor of anchors) {
const url = resolveUrl(anchor.getAttribute('href'));
if (url && url.includes('/detail/index') && url.includes('zhuanlisqh=')) return url;
}
const html = document.documentElement.outerHTML;
const patterns = [
/https:\/\/cpquery\.cponline\.cnipa\.gov\.cn\/detail\/index\?[^"'<>\s]+/i,
/\/detail\/index\?[^"'<>\s]+zhuanlisqh=[^"'<>\s]+/i,
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match?.[0]) return resolveUrl(match[0].replace(/&amp;/g, '&'));
}
return null;
});
const isCpqueryDetailPage = async (page) => {
for (const frame of page.frames()) {
try {
const isDetail = await frame.evaluate(() => {
const text = String(document.body?.innerText || '');
return /国内案件信息详情|申请信息|发文信息|申请号\/专利号:\d{8,}/.test(text)
&& /class="[^"]*(detailBox|boxRow|el-tree)/.test(document.documentElement.outerHTML);
});
if (isDetail) return true;
} catch (error) {
// Keep scanning other frames.
}
}
return false;
};
const clickFirstSearchResult = async (frame, number) => frame.evaluate((patentNumber) => {
const visible = element => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const clickElement = element => {
element.scrollIntoView({ block: 'center', inline: 'center' });
element.setAttribute('title', 'CNIPA 自动点击详情');
element.style.outline = '2px solid #22c55e';
const rect = element.getBoundingClientRect();
const eventInit = {
bubbles: true,
cancelable: true,
view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
};
element.dispatchEvent(new MouseEvent('mouseover', eventInit));
element.dispatchEvent(new MouseEvent('mousedown', eventInit));
element.dispatchEvent(new MouseEvent('mouseup', eventInit));
element.dispatchEvent(new MouseEvent('click', eventInit));
return true;
};
const patentNumberText = String(patentNumber || '');
const resultCards = [
...document.querySelectorAll('.tabele_list,.q-pa-sm,.table_info,tbody tr,.el-table__row,[role="row"]'),
]
.filter(visible)
.map(element => ({
element,
text: String(element.innerText || element.textContent || '').trim(),
}))
.filter(item => item.text.includes(patentNumberText) || /申请号\s*\/\s*专利号/.test(item.text));
const matchedCard = resultCards.find(item => item.text.includes(patentNumberText))?.element || resultCards[0]?.element;
if (matchedCard) {
const detailTarget = matchedCard.querySelector('.title.hover_active,.hover_active.title,a[href*="/detail/index"],span.hover_active');
if (detailTarget && visible(detailTarget)) return clickElement(detailTarget);
}
const isHeaderLike = text => /申请号\/专利号\s*发明名称|发明名称\s*申请人\s*专利类型|共查询到|主办单位|案件查询/.test(text);
const candidates = [
...document.querySelectorAll('tbody tr, .el-table__row, .ant-table-row, .mat-row, [role="row"], .v-data-table__tr'),
]
.filter(visible)
.map(element => ({ element, text: String(element.innerText || element.textContent || '').trim() }))
.filter(item => item.text.length > 8 && !isHeaderLike(item.text));
const row = candidates.find(item => /发明|实用新型|外观设计|申请|公布|授权/.test(item.text))?.element || candidates[0]?.element;
if (!row) return false;
const clickable = row.querySelector('.title.hover_active,.hover_active.title,a[href*="/detail/index"],a,button,[role="button"],td,div,span') || row;
clickElement(clickable);
return true;
}, number);
const clickSearchButton = async (frame) => {
const markedButton = frame.locator('[data-cnipa-query-button="true"]').first();
try {
await markedButton.scrollIntoViewIfNeeded({ timeout: 3000 });
await markedButton.click({ timeout: 5000, force: true });
return true;
} catch (error) {
// Fall through to a constrained DOM search.
}
const clicked = await frame.evaluate(() => {
const visible = element => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const center = rect => ({
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
});
const input = document.querySelector('[data-cnipa-query-input="true"]');
const inputRect = input?.getBoundingClientRect();
if (!inputRect) return false;
const buttons = [...document.querySelectorAll('button,.q-btn,.el-button,.ant-btn,[role="button"]')]
.filter(visible)
.map((element, index) => ({
element,
index,
text: String(element.innerText || element.textContent || element.getAttribute('aria-label') || '').trim(),
rect: element.getBoundingClientRect(),
}))
.filter(item => {
const compactText = item.text.replace(/\s+/g, '');
if (!/查询/.test(compactText)) return false;
if (/中国|多国|审查信息|重置|更多|展开|案件查询|申请方式查询/.test(compactText)) return false;
if (item.rect.top < inputRect.bottom) return false;
return compactText.length <= 12 || item.element.matches('button,.q-btn,.el-button,.ant-btn');
})
.map(item => {
const inputCenter = center(inputRect);
const itemCenter = center(item.rect);
return {
...item,
distance: Math.abs(itemCenter.y - (inputRect.bottom + 95)) * 4 + Math.abs(itemCenter.x - inputCenter.x),
};
})
.sort((a, b) => a.distance - b.distance || a.index - b.index);
const button = buttons[0]?.element;
if (!button) return false;
button.setAttribute('title', 'CNIPA 自动查询按钮');
button.style.outline = '2px solid #ef4444';
button.scrollIntoView({ block: 'center', inline: 'center' });
const rect = button.getBoundingClientRect();
const eventInit = {
bubbles: true,
cancelable: true,
view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
};
button.dispatchEvent(new MouseEvent('mouseover', eventInit));
button.dispatchEvent(new MouseEvent('mousedown', eventInit));
button.dispatchEvent(new MouseEvent('mouseup', eventInit));
button.dispatchEvent(new MouseEvent('click', eventInit));
if (typeof button.click === 'function') button.click();
return true;
});
if (clicked) return true;
try {
const input = frame.locator('[data-cnipa-query-input="true"]').first();
await input.press('Enter', { timeout: 3000 });
return true;
} catch (error) {
return false;
}
};
const getSearchResultSummary = async (frame) => frame.evaluate(() => {
const text = String(document.body?.innerText || '');
const countMatch = text.match(/共查询到\s*(\d+)\s*条结果/);
const input = document.querySelector('[data-cnipa-query-input="true"]');
return {
count: countMatch ? Number(countMatch[1]) : null,
inputValue: input ? (input.value || input.textContent || '') : '',
};
});
const normalizePageDate = (value = '') => {
const match = String(value || '').match(/\d{4}[-./年]\d{1,2}[-./月]\d{1,2}/);
return match ? match[0].replace(/[年月/.]/g, '-').replace(/日/g, '') : '';
};
const normalizePageText = (value = '') => String(value || '')
.replace(/\r/g, '\n')
.replace(/[ \t]+/g, ' ')
.replace(/\n{2,}/g, '\n')
.trim();
const extractNoticeNameFromText = (value = '') => {
const text = normalizePageText(value).replace(/\s+/g, '');
const explicitNames = [
'第一次审查意见通知书',
'第二次审查意见通知书',
'第三次审查意见通知书',
'第四次审查意见通知书',
'第五次审查意见通知书',
'办理登记手续通知书',
'授予发明专利权通知书',
'授予实用新型专利权通知书',
'授予外观设计专利权通知书',
'驳回决定',
'视为撤回通知书',
'补正通知书',
'手续合格通知书',
'专利证书',
];
const explicit = explicitNames.find(name => text.includes(name));
if (explicit) return explicit;
const match = text.match(/[\u4e00-\u9fa5A-Za-z0-9()()、-]{2,40}(?:通知书|决定书|专利证书)/);
return match?.[0] || '';
};
const buildNoticeRecordFromText = (value = '') => {
const text = normalizePageText(value);
const name = extractNoticeNameFromText(text);
if (!name) return null;
const date = normalizePageDate(text);
return {
tongzhismc: name,
fawenr: date || '-',
xiazaisj: date || '-',
rawText: text,
};
};
const mergeNoticeRecords = (records = []) => {
const unique = new Map();
records.filter(Boolean).forEach(record => {
const key = `${record.tongzhismc || ''}|${record.fawenr || ''}|${record.rawText || ''}`;
if (!unique.has(key)) unique.set(key, record);
});
return [...unique.values()];
};
const isIssueInfoPageLoaded = async (page) => {
for (const frame of page.frames()) {
try {
const loaded = await frame.evaluate(() => {
const text = String(document.body?.innerText || '');
const title = [...document.querySelectorAll('.table .title,p.title,.tableTile')]
.map(element => String(element.innerText || element.textContent || '').replace(/\s+/g, ''))
.find(Boolean);
return title === '发文信息' || /通知书发文/.test(text);
});
if (loaded) return true;
} catch (error) {
// Keep scanning other frames.
}
}
return false;
};
const clickIssueInfoTreeNode = async (page) => {
await page.waitForTimeout(1200);
const diagnostics = [];
for (let attempt = 0; attempt < 5; attempt += 1) {
for (const frame of page.frames()) {
try {
const clickResult = await frame.evaluate(() => {
const normalize = value => String(value || '').replace(/\s+/g, '');
const visible = element => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const title = [...document.querySelectorAll('.table .title,p.title,.tableTile')]
.map(element => normalize(element.innerText || element.textContent))
.find(Boolean) || '';
const treeItems = [...document.querySelectorAll('.tree [role="treeitem"],[role="treeitem"].el-tree-node')]
.filter(visible)
.map((element, index) => ({
element,
index,
text: normalize(element.innerText || element.textContent),
current: element.classList.contains('is-current'),
}));
const targetItem = treeItems.find(item => item.text === '发文信息');
if (!targetItem) {
return {
clicked: false,
reason: '发文信息 treeitem not found',
title,
treeTexts: treeItems.map(item => item.text),
};
}
const treeNode = targetItem.element;
const content = treeNode.querySelector('.el-tree-node__content') || treeNode;
const textTarget = [...treeNode.querySelectorAll('.custom-tree-node span,.custom-tree-node,span')]
.find(element => normalize(element.innerText || element.textContent) === '发文信息') || content;
const rect = content.getBoundingClientRect();
const textRect = textTarget.getBoundingClientRect();
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: textRect.left + textRect.width / 2,
clientY: textRect.top + textRect.height / 2,
};
[textTarget, content, treeNode].forEach(element => {
element.setAttribute('title', 'CNIPA 自动点击发文信息');
element.style.outline = '4px solid #f59e0b';
element.dispatchEvent(new PointerEvent('pointerdown', eventInit));
element.dispatchEvent(new MouseEvent('mousedown', eventInit));
element.dispatchEvent(new PointerEvent('pointerup', eventInit));
element.dispatchEvent(new MouseEvent('mouseup', eventInit));
element.dispatchEvent(new MouseEvent('click', eventInit));
if (typeof element.click === 'function') element.click();
});
return {
clicked: true,
title,
treeTexts: treeItems.map(item => `${item.current ? '*' : ''}${item.text}`),
x: textRect.left + textRect.width / 2,
y: textRect.top + textRect.height / 2,
contentX: rect.left + rect.width / 2,
contentY: rect.top + rect.height / 2,
};
});
diagnostics.push({ attempt, frameUrl: frame.url(), ...clickResult });
if (clickResult?.clicked) {
await page.mouse.click(clickResult.x, clickResult.y);
await page.waitForTimeout(300);
await page.mouse.click(clickResult.contentX, clickResult.contentY);
await page.waitForTimeout(1200);
if (await isIssueInfoPageLoaded(page)) return true;
}
} catch (error) {
diagnostics.push({ attempt, frameUrl: frame.url(), error: error.message });
}
}
}
const loaded = await isIssueInfoPageLoaded(page);
page.__cnipaIssueInfoDiagnostics = diagnostics.slice(-10);
return loaded;
};
const scrapeNoticeInfoFromPage = async (page, number) => {
const issueInfoClicked = await clickIssueInfoTreeNode(page);
const snapshots = [];
for (const frame of page.frames()) {
try {
snapshots.push(await frame.evaluate(() => {
const visible = element => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const rowSelectors = [
'tbody tr',
'.el-table__row',
'.ant-table-row',
'[role="row"]',
'.el-descriptions__row',
'.ant-descriptions-row',
'li',
];
const rows = rowSelectors.flatMap(selector => [...document.querySelectorAll(selector)])
.filter(visible)
.map(element => String(element.innerText || element.textContent || '').trim())
.filter(Boolean);
return {
bodyText: String(document.body?.innerText || '').trim(),
rows,
};
}));
} catch (error) {
// Ignore inaccessible frames.
}
}
const rowRecords = snapshots.flatMap(snapshot => snapshot.rows || [])
.filter(text => /通知书|决定书|专利证书|办理登记手续|审查意见/.test(text))
.map(buildNoticeRecordFromText);
const bodyRecords = snapshots.flatMap(snapshot => normalizePageText(snapshot.bodyText).split('\n'))
.filter(text => /通知书|决定书|专利证书|办理登记手续|审查意见/.test(text))
.map(buildNoticeRecordFromText);
const bodyText = normalizePageText(snapshots.map(snapshot => snapshot.bodyText).filter(Boolean).join('\n'));
const hasFirstOfficeAction = bodyText.includes('第一次审查意见通知书');
const records = mergeNoticeRecords([
...rowRecords,
...bodyRecords,
hasFirstOfficeAction
? {
tongzhismc: '第一次审查意见通知书',
fawenr: normalizePageDate(bodyText) || '-',
xiazaisj: normalizePageDate(bodyText) || '-',
rawText: '页面存在第一次审查意见通知书',
}
: null,
]);
const certificateList = records.filter(item => /证书|授权|授予|办理登记手续/.test(item.tongzhismc || item.rawText || ''));
const noticeList = records.filter(item => !certificateList.includes(item));
return {
code: 200,
msg: 'success',
data: {
zhuanlisqh: number,
tongzhishufw: {
tongzhishufwList: noticeList,
},
zhuanlizsfw: {
zhuanlizsfwList: certificateList,
},
},
rawPage: {
notices: noticeList,
certificates: certificateList,
issueInfoClicked,
issueInfoDiagnostics: page.__cnipaIssueInfoDiagnostics || [],
hasFirstOfficeAction,
bodyText: bodyText.slice(0, 2000),
},
};
};
const openCpqueryDetailFromSearch = async (page, number, referer) => {
const searchUrl = `${CNIPA_CPQUERY_BASE_URL}/chinesepatent/index`;
if (referer && !String(referer).includes('cpquery.cponline.cnipa.gov.cn')) {
try {
await page.goto(referer, {
waitUntil: 'domcontentloaded',
timeout: 20000,
});
await page.waitForTimeout(1000);
} catch (error) {
// Continue with the cpquery search page; it will redirect if the login state is insufficient.
}
}
const searchResp = await page.goto(searchUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000,
referer: referer || 'https://tysfjk.cponline.cnipa.gov.cn/portal',
});
if (searchResp?.status?.() === 412) {
page.__cnipaCpqueryChallengeDiagnostics = page.__cnipaCpqueryChallengeDiagnostics || [];
await retryCpqueryAfterPrecondition(
page,
page.context(),
{ url: searchUrl },
page.__cnipaCpqueryChallengeDiagnostics
);
}
try {
await page.waitForLoadState('networkidle', { timeout: 10000 });
} catch (error) {
// The page can keep background requests open.
}
const searchFrame = await setSearchInputValue(page, number);
if (!searchFrame) {
const error = new Error('cpquery 查询页未找到申请号输入框,请先确认浏览器已登录并已进入查询页');
error.response = { body: { diagnostics: await collectBrowserDiagnostics(page) } };
throw error;
}
const beforeUrl = page.url();
try {
await searchFrame.locator('[data-cnipa-query-input="true"]').first().fill(number, { timeout: 5000 });
await searchFrame.locator('[data-cnipa-query-input="true"]').first().dispatchEvent('input', {}, { timeout: 3000 });
await searchFrame.locator('[data-cnipa-query-input="true"]').first().dispatchEvent('change', {}, { timeout: 3000 });
} catch (error) {
// setSearchInputValue already populated the field through DOM events.
}
const clickedQuery = await clickSearchButton(searchFrame);
if (!clickedQuery) {
const error = new Error('cpquery 查询页未找到查询按钮');
error.response = { body: { diagnostics: await collectBrowserDiagnostics(page) } };
throw error;
}
try {
await page.waitForLoadState('networkidle', { timeout: 12000 });
} catch (error) {
// Results can render before all requests are idle.
}
await page.waitForTimeout(1000);
let resultSummary = await getSearchResultSummary(searchFrame).catch(() => null);
if (resultSummary?.count === 0 && resultSummary.inputValue === number) {
await clickSearchButton(searchFrame).catch(() => false);
try {
await searchFrame.locator('[data-cnipa-query-input="true"]').first().press('Enter', { timeout: 3000 });
} catch (error) {
// Some inputs do not submit on Enter.
}
try {
await page.waitForLoadState('networkidle', { timeout: 8000 });
} catch (error) {
// Results can render before all requests are idle.
}
await page.waitForTimeout(1000);
resultSummary = await getSearchResultSummary(searchFrame).catch(() => resultSummary);
}
if (resultSummary?.count === 0) {
const error = new Error(`cpquery 查询结果为 0,可能是申请号未正确填入或该号码无记录。输入框当前值:${resultSummary.inputValue || '空'}`);
error.response = {
body: {
searchUrl,
beforeUrl,
resultSummary,
diagnostics: await collectBrowserDiagnostics(page),
},
};
throw error;
}
let detailUrl = await findDetailUrlInPage(page) || await findDetailUrlInPage(searchFrame);
if (!detailUrl) {
const detailPagePromise = page.context().waitForEvent('page', { timeout: 8000 }).catch(() => null);
const detailRequestPromise = page.waitForRequest(
request => request.url().includes('/detail/index') && request.url().includes('zhuanlisqh='),
{ timeout: 8000 }
).catch(() => null);
const clicked = await clickFirstSearchResult(searchFrame, number).catch(() => false);
if (!clicked) {
const row = searchFrame.locator('tbody tr, .el-table__body tr, .el-table__row, [role="row"], tr').first();
try {
await row.dblclick({ timeout: 5000 });
} catch (error) {
try {
await row.click({ timeout: 5000 });
} catch (innerError) {
// Some result lists render clickable cells instead of rows.
}
}
}
try {
await page.waitForURL(url => String(url).includes('/detail/index'), { timeout: 8000 });
} catch (error) {
// Fall back to parsing the page HTML below.
}
const openedPage = await detailPagePromise;
if (openedPage) {
await openedPage.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => {});
await openedPage.bringToFront().catch(() => {});
page = openedPage;
}
const detailRequest = await detailRequestPromise;
detailUrl = page.url().includes('/detail/index')
? page.url()
: detailRequest?.url() || (await findDetailUrlInPage(page) || await findDetailUrlInPage(searchFrame));
if (!detailUrl && await isCpqueryDetailPage(page)) {
detailUrl = page.url();
}
}
if (!detailUrl) {
const error = new Error('cpquery 查询页未找到详情链接');
error.response = {
body: {
searchUrl,
beforeUrl,
diagnostics: await collectBrowserDiagnostics(page),
},
};
throw error;
}
if (page.url() !== detailUrl && !(await isCpqueryDetailPage(page))) {
await page.goto(detailUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000,
referer: searchUrl,
});
}
try {
await page.waitForLoadState('networkidle', { timeout: 10000 });
} catch (error) {
// Captured requests and rendered page state are sufficient.
}
return { detailUrl, page };
};
const queryGrantNoticeByBrowser = async ({ number, cookieHeader, referer, cnipaConfig }) => {
let browser;
const networkTrace = [];
try {
browser = await createCpqueryBrowser(cnipaConfig, { headless: cnipaConfig.browserQueryHeadless !== false });
const context = browser.context;
// 清除旧 cookies,避免与新 cookie 冲突导致 HTTP 412
await context.clearCookies().catch(() => {});
if (cookieHeader) {
await context.addCookies(cookieHeaderToPlaywrightCookies(cookieHeader, CPQUERY_COOKIE_DOMAINS));
}
let page = context.pages()[0] || await context.newPage();
page.on('request', request => {
const url = request.url();
if (/cponline\.cnipa\.gov\.cn/.test(url)) {
networkTrace.push({ type: 'request', method: request.method(), url });
}
});
page.on('response', response => {
const url = response.url();
if (/cponline\.cnipa\.gov\.cn/.test(url)) {
networkTrace.push({ type: 'response', status: response.status(), url });
}
});
const detailResult = await openCpqueryDetailFromSearch(page, number, referer);
const detailUrl = detailResult.detailUrl;
page = detailResult.page;
const pageBody = await scrapeNoticeInfoFromPage(page, number);
if (!pageBody.rawPage.issueInfoClicked) {
const error = new Error('浏览器详情页未找到或未能点击“发文信息”节点,无法判断是否一次性授权');
error.response = {
body: {
detailUrl,
diagnostics: await collectBrowserDiagnostics(page, networkTrace),
rawPage: pageBody.rawPage,
},
};
throw error;
}
const browserCookies = await context.cookies();
const nextCookieHeader = browserCookies
.filter(item => item.domain && item.name)
.map(item => `${item.name}=${item.value}`)
.join('; ');
return {
detailUrl,
cookie: nextCookieHeader,
response: {
ok: true,
status: 200,
statusText: 'OK',
body: pageBody,
text: JSON.stringify(pageBody),
url: detailUrl,
},
diagnostics: await collectBrowserDiagnostics(page, networkTrace),
};
} finally {
if (browser) await browser.close();
}
};
module.exports.prepareBrowser = async (ctx) => {
const targetUrl = ctx.request.body?.url || `${CNIPA_CPQUERY_BASE_URL}/chinesepatent/index`;
try {
const result = await openPreparedCpqueryBrowser(ctx, targetUrl);
ctx.status = 200;
ctx.body = {
success: true,
message: '已打开 CNIPA 浏览器,系统已自动执行“法人登录 + 账号密码填写 + 登录按钮点击”。请仅完成滑块验证。',
...result,
};
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
success: false,
message: error.message || '打开 CNIPA 查询浏览器失败',
detail: error.response?.body || error.response?.text || null,
};
}
};
module.exports.prepareBrowserInternal = async (ctx, targetUrl = `${CNIPA_CPQUERY_BASE_URL}/chinesepatent/index`) => {
return openPreparedCpqueryBrowser(ctx, targetUrl);
};
module.exports.getCaptcha = async (ctx) => {
try {
const cookie = await ensureCnipaAmSession(ctx);
const requestBody = ctx.request.body || {};
const payload = {
captchaType: requestBody.captchaType || 'blockPuzzle',
clientUid: requestBody.clientUid || createCaptchaClientUid(),
ts: requestBody.ts || Date.now(),
};
const response = await postCnipaAm('/captcha/get', payload, cookie);
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: response.cookie,
captchaType: payload.captchaType,
captchaClientUid: payload.clientUid,
captchaGetResponse: response.body,
updatedAt: Date.now(),
};
ctx.status = 200;
ctx.body = {
success: true,
data: response.body,
cookie: response.cookie,
};
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
success: false,
message: error.message || '获取 CNIPA 验证码失败',
detail: error.response?.body || error.response?.text || null,
};
}
};
module.exports.checkCaptcha = async (ctx) => {
try {
const cookie = await ensureCnipaAmSession(ctx);
const requestBody = ctx.request.body || {};
const payload = {
captchaType: requestBody.captchaType || latestLoginFlow?.captchaType || 'blockPuzzle',
pointJson: normalizeCaptchaPointJson(requestBody),
token: requestBody.token || latestLoginFlow?.captchaGetResponse?.repData?.token || '',
};
if (!payload.pointJson || !payload.token) {
const error = new Error('缺少参数 pointJson 或 token');
error.status = 400;
throw error;
}
const response = await postCnipaAm('/captcha/check', payload, cookie);
const repCode = String(response.body?.repCode || '');
const passed = repCode === '0000';
if (!passed) {
// 验证失败时立即刷新一张新验证码,直接回传给前端,避免重走登录流程。
let nextCaptcha = null;
try {
const refreshPayload = {
captchaType: payload.captchaType,
clientUid: latestLoginFlow?.captchaClientUid || createCaptchaClientUid(),
ts: Date.now(),
};
const refreshResponse = await postCnipaAm('/captcha/get', refreshPayload, response.cookie || cookie);
nextCaptcha = refreshResponse.body;
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: refreshResponse.cookie || response.cookie || cookie,
captchaType: payload.captchaType,
captchaClientUid: refreshPayload.clientUid,
captchaGetResponse: refreshResponse.body,
captchaCheckResponse: response.body,
updatedAt: Date.now(),
};
} catch (refreshError) {
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: response.cookie || cookie,
captchaCheckResponse: response.body,
updatedAt: Date.now(),
};
}
ctx.status = 200;
ctx.body = {
success: false,
message: response.body?.repMsg || '滑块验证码校验失败',
code: 'CAPTCHA_CHECK_FAILED',
data: response.body,
nextCaptcha,
cookie: latestLoginFlow?.cookie || response.cookie || cookie,
};
return;
}
const captchaVerification = requestBody.captchaVerification
|| requestBody.validateCode
|| `${payload.token}---${payload.pointJson}`;
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: response.cookie,
captchaCheckResponse: response.body,
validateCode: captchaVerification,
updatedAt: Date.now(),
};
ctx.status = 200;
ctx.body = {
success: true,
data: response.body,
validateCode: captchaVerification,
cookie: response.cookie,
};
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
success: false,
message: error.message || '校验 CNIPA 验证码失败',
detail: error.response?.body || error.response?.text || null,
};
}
};
module.exports.submitLogin = async (ctx) => {
try {
const cookie = await ensureCnipaAmSession(ctx);
const cnipaConfig = readCnipaConfig(ctx);
const requestBody = {
...(ctx.request.body || {}),
userType: String((ctx.request.body || {}).userType || cnipaConfig.loginUserType || '2'),
validateCode: (ctx.request.body || {}).validateCode
|| (ctx.request.body || {}).captchaVerification
|| latestLoginFlow?.validateCode
|| '',
};
const payload = buildCnipaLoginPayload(cnipaConfig, requestBody);
const response = await postCnipaAm('/login', payload, cookie);
const loginCode = Number(response.body?.code);
const loginRepCode = String(response.body?.repCode || '');
const loginSuccess = (
(!Number.isFinite(loginCode) || loginCode === 200)
&& (!loginRepCode || loginRepCode === '0000')
&& !/失败|错误|验证码/.test(String(response.body?.msg || response.body?.message || ''))
);
if (!loginSuccess) {
let nextCaptcha = null;
try {
const refreshPayload = {
captchaType: latestLoginFlow?.captchaType || 'blockPuzzle',
clientUid: latestLoginFlow?.captchaClientUid || createCaptchaClientUid(),
ts: Date.now(),
};
const refreshResponse = await postCnipaAm('/captcha/get', refreshPayload, response.cookie || cookie);
nextCaptcha = refreshResponse.body;
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: refreshResponse.cookie || response.cookie || cookie,
captchaType: refreshPayload.captchaType,
captchaClientUid: refreshPayload.clientUid,
captchaGetResponse: refreshResponse.body,
loginResponse: response.body,
updatedAt: Date.now(),
};
} catch (refreshError) {
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: response.cookie || cookie,
loginResponse: response.body,
updatedAt: Date.now(),
};
}
ctx.status = 200;
ctx.body = {
success: false,
message: response.body?.msg || response.body?.message || 'CNIPA 登录失败',
code: 'LOGIN_FAILED',
data: response.body,
nextCaptcha,
cookie: latestLoginFlow?.cookie || response.cookie || cookie,
};
return;
}
latestLoginFlow = {
...(latestLoginFlow || {}),
cookie: response.cookie,
loginResponse: response.body,
updatedAt: Date.now(),
};
const redirectURL = response.body?.data?.callback?.redirectURL || response.body?.data?.redirectURL || '';
const baseCookie = mergeCookieHeaders(latestLoginFlow.cookie, response.cookie);
logCnipaDebug(ctx, 'login success, start HTTP bridge', {
redirectURL,
responseCode: response.body?.code,
responseRepCode: response.body?.repCode,
...getCookieDiagnostics(baseCookie),
});
const bridgeHttpResult = await bridgeCnipaPortalSession(
baseCookie,
redirectURL
);
logCnipaDebug(ctx, 'HTTP bridge finished', {
diagnostics: bridgeHttpResult?.diagnostics || [],
...getCookieDiagnostics(bridgeHttpResult?.cookie),
});
let bridgeResult = bridgeHttpResult;
if (!hasCnipaReadyCookie(bridgeHttpResult?.cookie)) {
logCnipaDebug(ctx, 'HTTP bridge missing enable flag, start browser bridge', {
redirectURL,
...getCookieDiagnostics(mergeCookieHeaders(baseCookie, bridgeHttpResult?.cookie)),
});
const bridgeBrowserResult = await bridgeCnipaPortalSessionByBrowser(
mergeCookieHeaders(baseCookie, bridgeHttpResult?.cookie),
redirectURL,
cnipaConfig
).catch(error => ({
cookie: mergeCookieHeaders(baseCookie, bridgeHttpResult?.cookie),
diagnostics: [{ step: 'browser-bridge', error: error.message }],
}));
bridgeResult = {
cookie: mergeCookieHeaders(bridgeHttpResult?.cookie, bridgeBrowserResult?.cookie),
diagnostics: [
...(bridgeHttpResult?.diagnostics || []),
...(bridgeBrowserResult?.diagnostics || []),
],
};
logCnipaDebug(ctx, 'browser bridge finished', {
diagnostics: bridgeBrowserResult?.diagnostics || [],
...getCookieDiagnostics(bridgeBrowserResult?.cookie),
});
}
latestLoginFlow.cookie = mergeCookieHeaders(latestLoginFlow.cookie, bridgeResult.cookie);
latestQueryAuth = {
...(latestQueryAuth || {}),
cookie: mergeCookieHeaders(latestQueryAuth?.cookie, response.cookie, latestLoginFlow?.cookie, bridgeResult.cookie),
browserReady: true,
updatedAt: Date.now(),
};
logCnipaDebug(ctx, 'latest query auth updated', {
...getCookieDiagnostics(latestQueryAuth.cookie),
diagnostics: bridgeResult?.diagnostics || [],
});
ctx.status = 200;
ctx.body = {
success: true,
data: response.body,
cookie: response.cookie,
syncResult: {
loggedIn: true,
bridgeDiagnostics: bridgeResult.diagnostics,
},
};
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
success: false,
message: error.message || 'CNIPA 登录失败',
detail: error.response?.body || error.response?.text || null,
};
}
};
module.exports.queryGrantNoticeByBrowser = async (ctx, number, options = {}) => {
if (!latestQueryAuth) {
latestQueryAuth = {
browserReady: true,
updatedAt: Date.now(),
};
}
const cookieHeader = mergeCookieHeaders(latestQueryAuth.cookie, latestLoginFlow?.cookie);
const referer = latestQueryAuth.cpqueryReferer || latestQueryAuth.redirectURL;
logCnipaDebug(ctx, 'grant notice query start', {
number,
referer,
...getCookieDiagnostics(cookieHeader),
});
let browserResult;
try {
browserResult = await queryGrantNoticeByBrowser({
number,
cookieHeader,
referer,
cnipaConfig: readCnipaConfig(ctx),
});
} catch (error) {
logCnipaDebug(ctx, 'grant notice query failed', {
number,
message: error.message,
status: error?.status || error?.response?.status || null,
diagnostics: error?.response?.body?.diagnostics || error?.response?.diagnostics || null,
detailUrl: error?.response?.body?.detailUrl || error?.response?.detailUrl || null,
...getCookieDiagnostics(cookieHeader),
});
if (isCnipaAuthExpiredError(error)) {
invalidateCnipaAuthState();
logCnipaDebug(ctx, 'shared auth invalidated after query failure', {
number,
message: error.message,
status: error?.status || error?.response?.status || null,
});
}
throw error;
}
logCnipaDebug(ctx, 'grant notice query browser result', {
number,
detailUrl: browserResult.detailUrl,
responseStatus: browserResult.response?.status,
responseOk: browserResult.response?.ok,
diagnostics: browserResult.diagnostics,
...getCookieDiagnostics(browserResult.cookie),
});
if (browserResult.cookie) latestQueryAuth.cookie = mergeCookieHeaders(latestQueryAuth.cookie, browserResult.cookie);
if (browserResult.detailUrl) latestQueryAuth.cpqueryReferer = browserResult.detailUrl;
logCnipaDebug(ctx, 'grant notice auth refreshed', {
number,
cpqueryReferer: latestQueryAuth.cpqueryReferer,
...getCookieDiagnostics(latestQueryAuth.cookie),
});
const fetchResponse = browserResult.response;
if (!fetchResponse?.ok) {
const error = new Error(`CNIPA 浏览器页面查询失败: HTTP ${fetchResponse?.status || ''} ${fetchResponse?.statusText || ''}`.trim());
error.status = fetchResponse?.status;
error.response = {
status: fetchResponse?.status,
body: fetchResponse?.body,
text: fetchResponse?.text,
detailUrl: browserResult.detailUrl,
diagnostics: browserResult.diagnostics,
};
throw error;
}
if (fetchResponse.body?.code !== undefined && Number(fetchResponse.body.code) !== 200) {
const error = new Error(fetchResponse.body?.msg || fetchResponse.body?.message || 'CNIPA 浏览器页面查询返回失败');
error.response = { body: fetchResponse.body };
throw error;
}
return fetchResponse.body;
};
const buildAuthDiagnostics = () => {
const cookie = mergeCookieHeaders(latestQueryAuth?.cookie, latestLoginFlow?.cookie);
return {
hasSession: /SESSION=/.test(cookie),
hasEnableFlag: hasCnipaReadyCookie(cookie),
hasLegacyEnableFlag: /enable_dX1xbeyMT58W=/.test(cookie),
hasWafCookie: /(?:^|;\s*)dX1xbeyMT58W[OP]=/.test(cookie),
cookieLength: String(cookie || '').length,
cookieNames: getCookieNames(cookie),
updatedAt: latestQueryAuth?.updatedAt || latestLoginFlow?.updatedAt || 0,
};
};
const withAuthState = (handler) => async (...args) => withCnipaSerializedOp(async () => {
const ctx = args[0];
if (ctx) await loadCnipaAuthState(ctx);
try {
return await handler(...args);
} finally {
if (ctx) await saveCnipaAuthState(ctx).catch(() => {});
}
});
module.exports.prepareBrowser = withAuthState(module.exports.prepareBrowser);
module.exports.prepareBrowserInternal = withAuthState(module.exports.prepareBrowserInternal);
module.exports.getCaptcha = withAuthState(module.exports.getCaptcha);
module.exports.checkCaptcha = withAuthState(module.exports.checkCaptcha);
module.exports.submitLogin = withAuthState(module.exports.submitLogin);
module.exports.queryGrantNoticeByBrowser = withAuthState(module.exports.queryGrantNoticeByBrowser);
module.exports.__debugCookieHeader = () => mergeCookieHeaders(
latestQueryAuth?.cookie,
latestLoginFlow?.cookie
);
module.exports.__getAuthDiagnostics = buildAuthDiagnostics;
module.exports.getAuthDiagnostics = async (ctx) => withCnipaSerializedOp(async () => {
await loadCnipaAuthState(ctx);
return buildAuthDiagnostics();
});
module.exports.getDebugCookieHeader = async (ctx) => withCnipaSerializedOp(async () => {
await loadCnipaAuthState(ctx);
return mergeCookieHeaders(latestQueryAuth?.cookie, latestLoginFlow?.cookie);
});
module.exports.__hasCnipaReadyCookie = hasCnipaReadyCookie;