feat(auth): add login/whoami for additional sites

Adds site login/whoami coverage for 55 additional auth adapters using the shared site-auth helper, including the final gitee/hf/v2ex/deepseek/quark batch and fixes from live validation.

Review follow-up:
- remove direct email output from ChatGPT/Grok/Gemini/Qwen whoami
- avoid DeepSeek email fallback as display name
- avoid Upwork first/last name output
- avoid leaking Boss wt2 session cookie as user_id
- rebase on latest main and regenerate cli-manifest.json

Validation:
- clean-HOME npm test: 5049 passed, 1 skipped
- npm run check:typed-error-lint: new=0
- npm run check:silent-column-drop: new=0
- npm run build
- npm run docs:build
- git diff --check
- dist list JSON smoke
- GitHub CI green
This commit is contained in:
jakevin
2026-06-06 19:42:51 +08:00
committed by GitHub
parent a25a2836e9
commit 77b29b3d09
56 changed files with 5761 additions and 0 deletions
+2783
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function has12306SessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://kyfw.12306.cn' });
return cookies.some(c => c.name === 'tk' && c.value);
}
async function verify12306Identity(page) {
if (!await has12306SessionCookie(page)) {
throw new AuthRequiredError('12306.cn', '12306 tk auth cookie missing');
}
await page.goto('https://kyfw.12306.cn/otn/view/index.html');
await page.wait(2);
const probe = await page.evaluate(`(async () => {
try {
const r = await fetch('/otn/index/initMy12306Api', {
method: 'POST',
credentials: 'include',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
});
if (/login\\.html/.test(r.url)) {
return { kind: 'auth', detail: '12306 initMy12306Api redirected to login' };
}
const t = await r.text();
let d = null;
try { d = JSON.parse(t); } catch {}
if (!d || d.status === false || /未登录|登录超时|NotLogin/i.test(t)) {
return { kind: 'auth', detail: '12306 initMy12306Api returned NotLogin' };
}
const userName = d.data?.user_name || d.data?.userName || d.user_name || '';
if (!userName) {
return { kind: 'auth', detail: '12306 initMy12306Api 200 but no user_name surface' };
}
return { ok: true, user_name: String(userName) };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('12306.cn', probe.detail);
if (probe?.kind === 'exception') throw new CommandExecutionError(`12306 whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected 12306 probe: ${JSON.stringify(probe)}`);
return { user_name: probe.user_name };
}
registerSiteAuthCommands({
site: '12306',
domain: '12306.cn',
loginUrl: 'https://kyfw.12306.cn/otn/resources/login.html',
columns: ['user_name'],
verify: verify12306Identity,
poll: async (page) => {
if (!await has12306SessionCookie(page)) {
throw new AuthRequiredError('12306.cn', 'Waiting for 12306 tk auth cookie');
}
return verify12306Identity(page);
},
});
+45
View File
@@ -0,0 +1,45 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function has1688LogonCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.1688.com' });
return cookies.some(c => c.name === '__cn_logon__' && c.value === 'true');
}
async function verify1688Identity(page) {
if (!await has1688LogonCookie(page)) {
throw new AuthRequiredError('1688.com', '1688 __cn_logon__=true cookie missing — anonymous');
}
await page.goto('https://www.1688.com/');
await page.wait(2);
const cookies = await page.getCookies({ url: 'https://www.1688.com' });
const cookieMap = Object.fromEntries(cookies.map(c => [c.name, c.value]));
if (cookieMap['__cn_logon__'] !== 'true') {
throw new AuthRequiredError('1688.com', '1688 __cn_logon__ cookie absent after navigation');
}
const unb = cookieMap['unb'] || '';
if (!unb) {
throw new AuthRequiredError('1688.com', '1688 unb cookie missing — partial logged-in state');
}
let name = '';
try {
name = cookieMap['lid'] ? decodeURIComponent(cookieMap['lid']) : '';
} catch {
name = cookieMap['lid'] || '';
}
return { user_id: String(unb), name };
}
registerSiteAuthCommands({
site: '1688',
domain: '1688.com',
loginUrl: 'https://login.1688.com/member/signin.htm',
columns: ['user_id', 'name'],
verify: verify1688Identity,
poll: async (page) => {
if (!await has1688LogonCookie(page)) {
throw new AuthRequiredError('1688.com', 'Waiting for 1688 __cn_logon__=true cookie');
}
return verify1688Identity(page);
},
});
+51
View File
@@ -0,0 +1,51 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function has1Point3AcresAuthCookie(page) {
const host = await page.getCookies({ url: 'https://www.1point3acres.com' });
const root = await page.getCookies({ url: 'https://.1point3acres.com' });
return [...host, ...root].some(c => /_auth$/.test(c.name) && c.value);
}
async function verify1Point3AcresIdentity(page) {
if (!await has1Point3AcresAuthCookie(page)) {
throw new AuthRequiredError('1point3acres.com', '1point3acres Discuz *_auth cookie missing');
}
await page.goto('https://www.1point3acres.com/bbs/');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
if (/auth\\.1point3acres\\.com\\/login/.test(location.href)) {
return { kind: 'auth', detail: '1point3acres bbs redirected to auth login' };
}
const loginLink = document.querySelector('a[href*="auth.1point3acres.com/login"], a[href*="member.php?mod=logging&action=login"]');
if (loginLink && /登录/.test(loginLink.innerText || '')) {
return { kind: 'auth', detail: '1point3acres bbs shows 登录 link — anonymous' };
}
const nameEl = document.querySelector('#um .vwmy h4 a, a.username, .vwmy a');
const username = (nameEl?.innerText || '').trim();
const uid = (nameEl?.getAttribute('href') || '').match(/uid[=-](\\d+)/)?.[1] || '';
if (!uid && !username) {
return { kind: 'auth', detail: '1point3acres bbs rendered but no #um identity — anonymous or shape drifted' };
}
return { ok: true, user_id: uid, username };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('1point3acres.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected 1point3acres probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, username: probe.username };
}
registerSiteAuthCommands({
site: '1point3acres',
domain: '1point3acres.com',
loginUrl: 'https://auth.1point3acres.com/login',
columns: ['user_id', 'username'],
verify: verify1Point3AcresIdentity,
poll: async (page) => {
if (!await has1Point3AcresAuthCookie(page)) {
throw new AuthRequiredError('1point3acres.com', 'Waiting for 1point3acres Discuz *_auth cookie');
}
return verify1Point3AcresIdentity(page);
},
});
+52
View File
@@ -0,0 +1,52 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasAmazonSessionCookies(page) {
const cookies = await page.getCookies({ url: 'https://www.amazon.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('at-main') || names.has('x-main');
}
async function verifyAmazonIdentity(page) {
if (!await hasAmazonSessionCookies(page)) {
throw new AuthRequiredError('amazon.com', 'Amazon auth cookies (at-main / x-main) are missing');
}
await page.goto('https://www.amazon.com/', { waitUntil: 'load' });
await page.wait(3);
const probe = await page.evaluate(`
(() => {
const navLink = document.querySelector('#nav-link-accountList');
if (!navLink) {
return { kind: 'auth', detail: 'Amazon header missing nav-link-accountList — layout changed or robot challenge' };
}
const greeting = (navLink.querySelector('.nav-line-1, #nav-link-accountList-nav-line-1') || {}).textContent || '';
const trimmed = greeting.trim();
if (/sign\\s*in/i.test(trimmed)) {
return { kind: 'auth', detail: 'Amazon header shows "Hello, sign in" — anonymous' };
}
const m = trimmed.match(/^Hello,?\\s+(.+)$/i);
const name = m ? m[1].trim() : '';
if (!name) {
return { kind: 'auth', detail: 'Amazon greeting unparseable: ' + trimmed };
}
return { ok: true, user_name: name };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('amazon.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Amazon probe: ${JSON.stringify(probe)}`);
return { user_name: probe.user_name };
}
registerSiteAuthCommands({
site: 'amazon',
domain: 'amazon.com',
loginUrl: 'https://www.amazon.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.amazon.com%2F&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=usflex&openid.mode=checkid_setup&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0',
columns: ['user_name'],
verify: verifyAmazonIdentity,
poll: async (page) => {
if (!await hasAmazonSessionCookies(page)) {
throw new AuthRequiredError('amazon.com', 'Waiting for Amazon at-main / x-main cookie');
}
return verifyAmazonIdentity(page);
},
});
+61
View File
@@ -0,0 +1,61 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasBandSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.band.us' });
return cookies.some(c => c.name === 'band_session' && c.value);
}
async function verifyBandIdentity(page) {
if (!await hasBandSessionCookie(page)) {
throw new AuthRequiredError('band.us', 'Band band_session cookie missing');
}
await page.goto('https://www.band.us/feed');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
if (/auth\\.band\\.us\\/login/.test(location.href)) {
return { kind: 'auth', detail: 'Band /feed redirected to auth login' };
}
let userId = '';
try {
const stack = [window.__INITIAL_STATE__, window.__BAND_STORE__].filter(Boolean);
const seen = new Set();
while (stack.length) {
const node = stack.pop();
if (!node || typeof node !== 'object' || seen.has(node)) continue;
seen.add(node);
if (Array.isArray(node)) { stack.push(...node); continue; }
const u = node.user || node.me || node.currentUser;
if (u && (u.user_no || u.user_id || u.userId || u.id)) {
userId = String(u.user_no || u.user_id || u.userId || u.id);
break;
}
for (const v of Object.values(node)) if (v && typeof v === 'object') stack.push(v);
}
} catch {}
if (!userId) {
const el = document.querySelector('[data-user-no], [data-user_no]');
userId = el?.getAttribute('data-user-no') || el?.getAttribute('data-user_no') || '';
}
return { ok: true, user_id: userId };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('band.us', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Band probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id };
}
registerSiteAuthCommands({
site: 'band',
domain: 'band.us',
loginUrl: 'https://auth.band.us/login',
columns: ['user_id'],
verify: verifyBandIdentity,
poll: async (page) => {
if (!await hasBandSessionCookie(page)) {
throw new AuthRequiredError('band.us', 'Waiting for Band band_session cookie');
}
return verifyBandIdentity(page);
},
});
+46
View File
@@ -0,0 +1,46 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasBossSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.zhipin.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('wt2') || names.has('t');
}
async function verifyBossIdentity(page) {
if (!await hasBossSessionCookie(page)) {
throw new AuthRequiredError('zhipin.com', 'Boss wt2 / t cookies missing');
}
await page.goto('https://www.zhipin.com/web/geek/job-recommend');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
const path = location.pathname || '';
if (/\\/web\\/user\\/login|\\/login\\.html/.test(location.href)) {
return { kind: 'auth', detail: 'Boss redirected to login page' };
}
const userType = /\\/web\\/geek\\//.test(path) ? 'geek' : /\\/web\\/(boss|recruit|chat\\/boss)/.test(path) ? 'recruiter' : '';
if (!userType) {
return { kind: 'auth', detail: 'Boss path does not look like authenticated geek/recruiter page: ' + path };
}
return { ok: true, user_type: userType };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('zhipin.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Boss probe: ${JSON.stringify(probe)}`);
return { user_type: probe.user_type };
}
registerSiteAuthCommands({
site: 'boss',
domain: 'zhipin.com',
loginUrl: 'https://login.zhipin.com/',
columns: ['user_type'],
verify: verifyBossIdentity,
poll: async (page) => {
if (!await hasBossSessionCookie(page)) {
throw new AuthRequiredError('zhipin.com', 'Waiting for Boss wt2 / t cookies');
}
return verifyBossIdentity(page);
},
});
+53
View File
@@ -0,0 +1,53 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasChaoxingSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://i.chaoxing.com' });
return cookies.some(c => /^(UID|_uid|chaoxinguser|cx_p_token)$/i.test(c.name) && c.value);
}
async function verifyChaoxingIdentity(page) {
if (!await hasChaoxingSessionCookie(page)) {
throw new AuthRequiredError('chaoxing.com', 'Chaoxing session cookies missing');
}
await page.goto('https://i.chaoxing.com/');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
if (/passport2\\.chaoxing\\.com\\/login/.test(location.href)) {
return { kind: 'auth', detail: 'Chaoxing i.chaoxing.com redirected to passport2 login' };
}
const userIdCookie = (document.cookie.split('; ').find(c => /^(_uid|UID)=/.test(c)) || '').split('=')[1] || '';
let userName = '';
const unameCookie = (document.cookie.split('; ').find(c => /^uname=/.test(c)) || '').split('=')[1] || '';
if (unameCookie) {
try { userName = decodeURIComponent(unameCookie); } catch { userName = unameCookie; }
}
if (!userName) {
const el = document.querySelector('.userTitle, .myInfo, .user-name, [class*=userName]');
userName = (el?.innerText || '').trim();
}
if (!userIdCookie && !userName) {
return { kind: 'auth', detail: 'Chaoxing i.chaoxing.com no user identity surface — anonymous' };
}
return { ok: true, user_id: userIdCookie, name: userName };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('chaoxing.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Chaoxing probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'chaoxing',
domain: 'chaoxing.com',
loginUrl: 'https://passport2.chaoxing.com/login?fid=&newversion=true&refer=https%3A%2F%2Fi.chaoxing.com',
columns: ['user_id', 'name'],
verify: verifyChaoxingIdentity,
poll: async (page) => {
if (!await hasChaoxingSessionCookie(page)) {
throw new AuthRequiredError('chaoxing.com', 'Waiting for Chaoxing session cookies');
}
return verifyChaoxingIdentity(page);
},
});
+51
View File
@@ -0,0 +1,51 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasChatgptSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://chatgpt.com' });
return cookies.some(c => c.name === '__Secure-next-auth.session-token' && c.value);
}
async function verifyChatgptIdentity(page) {
if (!await hasChatgptSessionCookie(page)) {
throw new AuthRequiredError('chatgpt.com', 'ChatGPT __Secure-next-auth.session-token cookie missing');
}
await page.goto('https://chatgpt.com/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const res = await fetch('/api/auth/session', { credentials: 'include' });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'ChatGPT /api/auth/session HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
const user = d && d.user;
if (!user || !user.id) {
return { kind: 'auth', detail: 'ChatGPT /api/auth/session has no user — anonymous' };
}
return { ok: true, user_id: String(user.id), name: String(user.name || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('chatgpt.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /api/auth/session`);
if (result?.kind === 'exception') throw new CommandExecutionError(`ChatGPT whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected ChatGPT probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, name: result.name };
}
registerSiteAuthCommands({
site: 'chatgpt',
domain: 'chatgpt.com',
loginUrl: 'https://auth.openai.com/log-in',
columns: ['user_id', 'name'],
verify: verifyChatgptIdentity,
poll: async (page) => {
if (!await hasChatgptSessionCookie(page)) {
throw new AuthRequiredError('chatgpt.com', 'Waiting for ChatGPT session cookie');
}
return verifyChatgptIdentity(page);
},
});
+54
View File
@@ -0,0 +1,54 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasClaudeSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://claude.ai' });
return cookies.some(c => c.name === 'sessionKey' && c.value);
}
async function verifyClaudeIdentity(page) {
if (!await hasClaudeSessionCookie(page)) {
throw new AuthRequiredError('claude.ai', 'Claude sessionKey cookie missing');
}
await page.goto('https://claude.ai/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const res = await fetch('/api/organizations', { credentials: 'include' });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Claude /api/organizations HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
if (!Array.isArray(d) || d.length === 0) {
return { kind: 'auth', detail: 'Claude /api/organizations empty' };
}
const userIdCookie = (document.cookie.split('; ').find(c => c.startsWith('ajs_user_id=')) || '').split('=')[1] || '';
const activeOrgCookie = (document.cookie.split('; ').find(c => c.startsWith('lastActiveOrg=')) || '').split('=')[1] || '';
const activeOrg = d.find(o => o.uuid === activeOrgCookie) || d[0];
return { ok: true, user_id: userIdCookie, org_name: activeOrg.name || '', org_uuid: activeOrg.uuid || '' };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('claude.ai', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /api/organizations`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Claude whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Claude probe: ${JSON.stringify(result)}`);
if (!result.user_id) throw new AuthRequiredError('claude.ai', 'Claude session incomplete — ajs_user_id cookie missing');
return { user_id: String(result.user_id), org_name: String(result.org_name), org_uuid: String(result.org_uuid) };
}
registerSiteAuthCommands({
site: 'claude',
domain: 'claude.ai',
loginUrl: 'https://claude.ai/login',
columns: ['user_id', 'org_name', 'org_uuid'],
verify: verifyClaudeIdentity,
poll: async (page) => {
if (!await hasClaudeSessionCookie(page)) {
throw new AuthRequiredError('claude.ai', 'Waiting for Claude sessionKey cookie');
}
return verifyClaudeIdentity(page);
},
});
+48
View File
@@ -0,0 +1,48 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasCoupangSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.coupang.com' });
return cookies.some(c => /^(AID|MEMBER_ID|LMSESSIONID)$/.test(c.name) && c.value);
}
async function verifyCoupangIdentity(page) {
if (!await hasCoupangSessionCookie(page)) {
throw new AuthRequiredError('coupang.com', 'Coupang session cookies (AID/MEMBER_ID/LMSESSIONID) missing');
}
await page.goto('https://www.coupang.com/np/mypage');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
if (/login\\.coupang\\.com\\/login/.test(location.href)) {
return { kind: 'auth', detail: 'Coupang mypage redirected to login — anonymous' };
}
if (/Access Denied/i.test(document.title)) {
return { kind: 'auth', detail: 'Coupang Access Denied — anti-bot or non-KR IP' };
}
const el = document.querySelector('.my-nickname, .member-name, .mp-user-info-name, [class*=memberName]');
const name = (el?.textContent || '').trim();
if (!name) {
return { kind: 'auth', detail: 'Coupang mypage 200 but no member-name surface' };
}
return { ok: true, name };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('coupang.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Coupang probe: ${JSON.stringify(probe)}`);
return { name: probe.name };
}
registerSiteAuthCommands({
site: 'coupang',
domain: 'coupang.com',
loginUrl: 'https://login.coupang.com/login/login.pang',
columns: ['name'],
verify: verifyCoupangIdentity,
poll: async (page) => {
if (!await hasCoupangSessionCookie(page)) {
throw new AuthRequiredError('coupang.com', 'Waiting for Coupang session cookies');
}
return verifyCoupangIdentity(page);
},
});
+49
View File
@@ -0,0 +1,49 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasCtripLoginUid(page) {
const cookies = await page.getCookies({ url: 'https://www.ctrip.com' });
const loginUid = cookies.find(c => c.name === 'login_uid');
return Boolean(loginUid && loginUid.value);
}
async function verifyCtripIdentity(page) {
if (!await hasCtripLoginUid(page)) {
throw new AuthRequiredError('ctrip.com', 'Ctrip login_uid cookie missing — anonymous');
}
await page.goto('https://my.ctrip.com/myinfo/MyInfoIndex.aspx');
await page.wait(2);
const cookies = await page.getCookies({ url: 'https://www.ctrip.com' });
const cookieMap = Object.fromEntries(cookies.map(c => [c.name, c.value]));
const loginUid = cookieMap['login_uid'] || '';
if (!loginUid) {
throw new AuthRequiredError('ctrip.com', 'Ctrip login_uid cookie absent after navigation');
}
const aheadRaw = cookieMap['AHeadUserInfo'] || '';
const params = new URLSearchParams(aheadRaw);
const userNameRaw = params.get('UserName') || '';
let userName = '';
if (userNameRaw) {
try {
userName = decodeURIComponent(userNameRaw);
} catch {
userName = userNameRaw;
}
}
const vipGrade = params.get('VipGrade') || '';
return { user_id: loginUid, name: userName, vip_grade: vipGrade };
}
registerSiteAuthCommands({
site: 'ctrip',
domain: 'ctrip.com',
loginUrl: 'https://passport.ctrip.com/user/login',
columns: ['user_id', 'name', 'vip_grade'],
verify: verifyCtripIdentity,
poll: async (page) => {
if (!await hasCtripLoginUid(page)) {
throw new AuthRequiredError('ctrip.com', 'Waiting for Ctrip login_uid cookie');
}
return verifyCtripIdentity(page);
},
});
+54
View File
@@ -0,0 +1,54 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
// DeepSeek authenticates via a Bearer token stored in localStorage (userToken),
// not a cookie, so credentials:include alone returns code 40002 (anonymous).
// The probe reads the token and calls the confirmed /api/v0/users/current
// endpoint (anonymous → HTTP 200 + body code 40002).
const WHOAMI_PROBE = `(async () => {
try {
let token = '';
const raw = localStorage.getItem('userToken');
if (raw) { try { token = JSON.parse(raw).value || ''; } catch { token = raw; } }
if (!token) return { kind: 'auth', detail: 'DeepSeek userToken missing from localStorage — anonymous' };
const r = await fetch('/api/v0/users/current', {
headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' },
});
if (r.status === 401 || r.status === 403) return { kind: 'auth', detail: 'DeepSeek users/current HTTP ' + r.status };
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
if (!d || d.code !== 0) return { kind: 'auth', detail: 'DeepSeek users/current code=' + String(d && d.code) + ' — anonymous' };
const u = (d.data && (d.data.biz_data || d.data.user || d.data)) || {};
const userId = String(u.id || u.user_id || u.uuid || '');
const name = String(u.name || u.nickname || u.username || '');
if (!userId && !name) return { kind: 'render-error', detail: 'DeepSeek users/current ok but no id/name field — response shape drift' };
return { ok: true, user_id: userId, name };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`;
async function verifyDeepseekIdentity(page) {
await page.goto('https://chat.deepseek.com/');
await page.wait(2);
const probe = await page.evaluate(WHOAMI_PROBE);
if (probe?.kind === 'auth') throw new AuthRequiredError('chat.deepseek.com', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from DeepSeek users/current`);
if (probe?.kind === 'render-error') throw new CommandExecutionError(probe.detail);
if (probe?.kind === 'exception') throw new CommandExecutionError(`DeepSeek whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected DeepSeek probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'deepseek',
domain: 'chat.deepseek.com',
loginUrl: 'https://chat.deepseek.com/sign_in',
columns: ['user_id', 'name'],
verify: verifyDeepseekIdentity,
poll: async (page) => {
const probe = await page.evaluate(WHOAMI_PROBE);
if (!probe?.ok) throw new AuthRequiredError('chat.deepseek.com', 'Waiting for DeepSeek login');
return { user_id: probe.user_id, name: probe.name };
},
});
+48
View File
@@ -0,0 +1,48 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasDianpingSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.dianping.com' });
return cookies.some(c => c.name === 'dper' && c.value);
}
async function verifyDianpingIdentity(page) {
if (!await hasDianpingSessionCookie(page)) {
throw new AuthRequiredError('dianping.com', 'Dianping dper cookie missing');
}
await page.goto('https://www.dianping.com/member/myinformation');
await page.wait(2);
const finalUrl = await page.evaluate(`location.href`);
if (/account\.dianping\.com\/(pc)?login/.test(String(finalUrl || ''))) {
throw new AuthRequiredError('dianping.com', `Dianping member page redirected to login: ${finalUrl}`);
}
const info = await page.evaluate(`
(() => {
const nicknameEl = document.querySelector('.user-name, .username, .nickname, .user-info .name');
const nickname = (nicknameEl?.textContent || '').trim();
const profileLink = Array.from(document.querySelectorAll('a[href*="/member/"]'))
.map(a => a.getAttribute('href') || '')
.find(h => /\\/member\\/\\d+/.test(h));
const uidMatch = String(profileLink || '').match(/\\/member\\/(\\d+)/);
return { user_id: uidMatch?.[1] || '', nickname };
})()
`);
if (!info?.user_id) {
throw new CommandExecutionError('Dianping member page rendered but no user_id link found — stale dper or layout drift');
}
return { user_id: String(info.user_id), nickname: String(info.nickname || '') };
}
registerSiteAuthCommands({
site: 'dianping',
domain: 'dianping.com',
loginUrl: 'https://account.dianping.com/pclogin',
columns: ['user_id', 'nickname'],
verify: verifyDianpingIdentity,
poll: async (page) => {
if (!await hasDianpingSessionCookie(page)) {
throw new AuthRequiredError('dianping.com', 'Waiting for Dianping dper cookie');
}
return verifyDianpingIdentity(page);
},
});
+49
View File
@@ -0,0 +1,49 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasDoubanSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.douban.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('dbcl2') || names.has('ck');
}
async function verifyDoubanIdentity(page) {
if (!await hasDoubanSessionCookie(page)) {
throw new AuthRequiredError('douban.com', 'Douban dbcl2 / ck cookies missing');
}
await page.goto('https://www.douban.com/');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
const navUser = document.querySelector('.nav-user-account .bn-more, .top-nav-info a.bn-more');
if (!navUser) {
return { kind: 'auth', detail: 'Douban nav-user element missing — not signed in' };
}
const href = navUser.getAttribute('href') || '';
const m = href.match(/people\\/(\\d+)\\/?/);
const user_id = m ? m[1] : '';
const name = (navUser.textContent || '').trim();
if (!user_id) {
return { kind: 'auth', detail: 'Douban user_id parse failed: href=' + href };
}
return { ok: true, user_id, name };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('douban.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Douban probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'douban',
domain: 'douban.com',
loginUrl: 'https://accounts.douban.com/passport/login',
columns: ['user_id', 'name'],
verify: verifyDoubanIdentity,
poll: async (page) => {
if (!await hasDoubanSessionCookie(page)) {
throw new AuthRequiredError('douban.com', 'Waiting for Douban dbcl2 / ck cookies');
}
return verifyDoubanIdentity(page);
},
});
+66
View File
@@ -0,0 +1,66 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasDoubaoSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.doubao.com' });
return cookies.some(c => c.name === 'passport_csrf_token' && c.value);
}
async function verifyDoubaoIdentity(page) {
if (!await hasDoubaoSessionCookie(page)) {
throw new AuthRequiredError('www.doubao.com', 'Doubao passport_csrf_token cookie missing');
}
await page.goto('https://www.doubao.com/chat/');
await page.wait(3);
const result = await page.evaluate(`(async () => {
try {
const res = await fetch('/passport/account/info/v2/', { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Doubao /passport/account/info HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
const data = d && d.data;
if (!data || !data.user_id_str) {
return { kind: 'auth', detail: 'Doubao /passport/account/info returned no user_id_str' };
}
return {
ok: true,
user_id: String(data.user_id_str),
name: String(data.name || data.screen_name || ''),
};
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('www.doubao.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /passport/account/info`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Doubao whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Doubao probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, name: result.name };
}
registerSiteAuthCommands({
site: 'doubao',
domain: 'www.doubao.com',
loginUrl: 'https://www.doubao.com/chat/',
columns: ['user_id', 'name'],
verify: verifyDoubaoIdentity,
// passport_csrf_token is set for anonymous sessions too, so a cookie gate
// would navigate away mid-login. Probe the account API on the current page
// (no goto) and only confirm once a real user_id is present.
poll: async (page) => {
const loggedIn = await page.evaluate(`(async () => {
try {
const r = await fetch('/passport/account/info/v2/', { credentials: 'include', headers: { Accept: 'application/json' } });
if (!r.ok) return false;
const d = await r.json();
return !!(d?.data?.user_id_str);
} catch { return false; }
})()`);
if (!loggedIn) {
throw new AuthRequiredError('www.doubao.com', 'Waiting for Doubao login');
}
return verifyDoubaoIdentity(page);
},
});
+42
View File
@@ -0,0 +1,42 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasFacebookCUserCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.facebook.com' });
return cookies.some(c => c.name === 'c_user' && c.value);
}
async function verifyFacebookIdentity(page) {
if (!await hasFacebookCUserCookie(page)) {
throw new AuthRequiredError('www.facebook.com', 'Facebook c_user cookie missing — anonymous session');
}
const cookies = await page.getCookies({ url: 'https://www.facebook.com' });
const cUser = cookies.find(c => c.name === 'c_user')?.value || '';
await page.goto('https://www.facebook.com/me');
await page.wait(2);
const finalUrl = await page.evaluate(`location.href`);
const vanityMatch = String(finalUrl || '').match(/facebook\.com\/([^/?#]+)\/?(?:$|[?#])/);
const vanity = vanityMatch?.[1] || '';
if (!vanity || vanity === 'login.php' || vanity === 'checkpoint') {
throw new AuthRequiredError('www.facebook.com', `Facebook /me redirected to ${finalUrl} — logged out or in checkpoint`);
}
return {
user_id: String(cUser),
vanity: String(vanity),
profile_url: `https://www.facebook.com/${vanity}/`,
};
}
registerSiteAuthCommands({
site: 'facebook',
domain: 'facebook.com',
loginUrl: 'https://www.facebook.com/login.php',
columns: ['user_id', 'vanity', 'profile_url'],
verify: verifyFacebookIdentity,
poll: async (page) => {
if (!await hasFacebookCUserCookie(page)) {
throw new AuthRequiredError('www.facebook.com', 'Waiting for Facebook c_user cookie');
}
return verifyFacebookIdentity(page);
},
});
+54
View File
@@ -0,0 +1,54 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasFlomoSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://flomoapp.com' });
return cookies.some(c => c.name === 'flomo' && c.value);
}
async function verifyFlomoIdentity(page) {
if (!await hasFlomoSessionCookie(page)) {
throw new AuthRequiredError('flomoapp.com', 'Flomo session cookie missing');
}
await page.goto('https://v.flomoapp.com/mine');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
if (/\\/login(\\b|$|\\?)/.test(location.href)) {
return { kind: 'auth', detail: 'Flomo /mine redirected to /login' };
}
let userId = '';
try {
const stack = [window.__INITIAL_STATE__, window.__NUXT__, window.__PINIA__].filter(Boolean);
const seen = new Set();
while (stack.length) {
const node = stack.pop();
if (!node || typeof node !== 'object' || seen.has(node)) continue;
seen.add(node);
if (Array.isArray(node)) { stack.push(...node); continue; }
const u = node.user || node.userInfo || node.currentUser;
if (u && (u.id || u.user_id || u.uid)) { userId = String(u.id || u.user_id || u.uid); break; }
for (const v of Object.values(node)) if (v && typeof v === 'object') stack.push(v);
}
} catch {}
return { ok: true, user_id: userId };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('flomoapp.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Flomo probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id };
}
registerSiteAuthCommands({
site: 'flomo',
domain: 'flomoapp.com',
loginUrl: 'https://v.flomoapp.com/login',
columns: ['user_id'],
verify: verifyFlomoIdentity,
poll: async (page) => {
if (!await hasFlomoSessionCookie(page)) {
throw new AuthRequiredError('flomoapp.com', 'Waiting for Flomo session cookie');
}
return verifyFlomoIdentity(page);
},
});
+47
View File
@@ -0,0 +1,47 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasGoogleSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://gemini.google.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('SID') || names.has('SAPISID') || names.has('__Secure-1PSID');
}
async function verifyGeminiIdentity(page) {
if (!await hasGoogleSessionCookie(page)) {
throw new AuthRequiredError('gemini.google.com', 'Google session cookies (SID / SAPISID) missing');
}
await page.goto('https://gemini.google.com/app');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
const a = document.querySelector('a[aria-label^="Google Account:"]');
if (!a) {
return { kind: 'auth', detail: 'Gemini account link missing — not signed into Google' };
}
const label = a.getAttribute('aria-label') || '';
const m = label.match(/Google Account:\\s*([^(]+?)\\s*\\(([^)]+)\\)/);
if (!m) {
return { kind: 'auth', detail: 'Gemini aria-label unparseable: ' + label };
}
return { ok: true, name: m[1].trim() };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('gemini.google.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Gemini probe: ${JSON.stringify(probe)}`);
return { name: probe.name };
}
registerSiteAuthCommands({
site: 'gemini',
domain: 'gemini.google.com',
loginUrl: 'https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fgemini.google.com%2F',
columns: ['name'],
verify: verifyGeminiIdentity,
poll: async (page) => {
if (!await hasGoogleSessionCookie(page)) {
throw new AuthRequiredError('gemini.google.com', 'Waiting for Google session cookies');
}
return verifyGeminiIdentity(page);
},
});
+41
View File
@@ -0,0 +1,41 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
// Gitee's logged-in cookie (gitee-session-n) is httpOnly and its exact name
// rotates, so the poll uses a no-navigation API probe instead of a cookie gate.
const WHOAMI_PROBE = `(async () => {
try {
const r = await fetch('/api/v5/user', { credentials: 'include', headers: { Accept: 'application/json' } });
if (r.status === 401 || r.status === 403) return { kind: 'auth', detail: 'Gitee /api/v5/user HTTP ' + r.status };
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
if (!d || !d.id || !d.login) return { kind: 'auth', detail: 'Gitee /api/v5/user has no id/login — anonymous' };
return { ok: true, user_id: String(d.id), username: String(d.login), name: String(d.name || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`;
async function verifyGiteeIdentity(page) {
await page.goto('https://gitee.com/');
await page.wait(1);
const probe = await page.evaluate(WHOAMI_PROBE);
if (probe?.kind === 'auth') throw new AuthRequiredError('gitee.com', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from Gitee /api/v5/user`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Gitee whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Gitee probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, username: probe.username, name: probe.name };
}
registerSiteAuthCommands({
site: 'gitee',
domain: 'gitee.com',
loginUrl: 'https://gitee.com/login',
columns: ['user_id', 'username', 'name'],
verify: verifyGiteeIdentity,
poll: async (page) => {
const probe = await page.evaluate(WHOAMI_PROBE);
if (!probe?.ok) throw new AuthRequiredError('gitee.com', 'Waiting for Gitee login');
return { user_id: probe.user_id, username: probe.username, name: probe.name };
},
});
+51
View File
@@ -0,0 +1,51 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasGrokSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://grok.com' });
return cookies.some(c => c.name === '__Secure-next-auth.session-token' && c.value);
}
async function verifyGrokIdentity(page) {
if (!await hasGrokSessionCookie(page)) {
throw new AuthRequiredError('grok.com', 'Grok __Secure-next-auth.session-token cookie missing');
}
await page.goto('https://grok.com/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const res = await fetch('/api/auth/session', { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Grok /api/auth/session HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
const user = d && d.user;
if (!user || !user.id) {
return { kind: 'auth', detail: 'Grok /api/auth/session has no user — anonymous' };
}
return { ok: true, user_id: String(user.id), name: String(user.name || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('grok.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /api/auth/session`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Grok whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Grok probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, name: result.name };
}
registerSiteAuthCommands({
site: 'grok',
domain: 'grok.com',
loginUrl: 'https://grok.com/auth/sign-in',
columns: ['user_id', 'name'],
verify: verifyGrokIdentity,
poll: async (page) => {
if (!await hasGrokSessionCookie(page)) {
throw new AuthRequiredError('grok.com', 'Waiting for Grok session cookie');
}
return verifyGrokIdentity(page);
},
});
+41
View File
@@ -0,0 +1,41 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
// Hugging Face's `token` cookie is httpOnly; gate the login poll on the
// documented /api/whoami-v2 endpoint (401 when anonymous) via a no-nav probe.
const WHOAMI_PROBE = `(async () => {
try {
const r = await fetch('/api/whoami-v2', { credentials: 'include', headers: { Accept: 'application/json' } });
if (r.status === 401 || r.status === 403) return { kind: 'auth', detail: 'HF /api/whoami-v2 HTTP ' + r.status };
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
if (!d || !d.name || d.type === undefined) return { kind: 'auth', detail: 'HF /api/whoami-v2 has no name — anonymous' };
return { ok: true, username: String(d.name), fullname: String(d.fullname || ''), type: String(d.type || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`;
async function verifyHfIdentity(page) {
await page.goto('https://huggingface.co/');
await page.wait(1);
const probe = await page.evaluate(WHOAMI_PROBE);
if (probe?.kind === 'auth') throw new AuthRequiredError('huggingface.co', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from HF /api/whoami-v2`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`HF whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected HF probe: ${JSON.stringify(probe)}`);
return { username: probe.username, fullname: probe.fullname, type: probe.type };
}
registerSiteAuthCommands({
site: 'hf',
domain: 'huggingface.co',
loginUrl: 'https://huggingface.co/login',
columns: ['username', 'fullname', 'type'],
verify: verifyHfIdentity,
poll: async (page) => {
const probe = await page.evaluate(WHOAMI_PROBE);
if (!probe?.ok) throw new AuthRequiredError('huggingface.co', 'Waiting for Hugging Face login');
return { username: probe.username, fullname: probe.fullname, type: probe.type };
},
});
+46
View File
@@ -0,0 +1,46 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasHupuUserCookie(page) {
const cookies = await page.getCookies({ url: 'https://my.hupu.com' });
return cookies.some(c => c.name === 'u' && c.value);
}
async function verifyHupuIdentity(page) {
if (!await hasHupuUserCookie(page)) {
throw new AuthRequiredError('hupu.com', 'Hupu u cookie missing — anonymous');
}
await page.goto('https://my.hupu.com/');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
if (/passport\\.hupu\\.com\\/.*login/.test(location.href)) {
return { kind: 'auth', detail: 'Hupu my page redirected to passport login' };
}
const uCookie = (document.cookie.split('; ').find(c => c.startsWith('u=')) || '').split('=')[1] || '';
const el = document.querySelector('.user-name, .username, .nick, [class*="userName"]');
const username = (el?.innerText || '').trim();
if (!uCookie) {
return { kind: 'auth', detail: 'Hupu my page rendered but u cookie absent — stale session' };
}
return { ok: true, user_id: uCookie, username };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('hupu.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Hupu probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, username: probe.username };
}
registerSiteAuthCommands({
site: 'hupu',
domain: 'hupu.com',
loginUrl: 'https://passport.hupu.com/pc/login',
columns: ['user_id', 'username'],
verify: verifyHupuIdentity,
poll: async (page) => {
if (!await hasHupuUserCookie(page)) {
throw new AuthRequiredError('hupu.com', 'Waiting for Hupu u cookie');
}
return verifyHupuIdentity(page);
},
});
+56
View File
@@ -0,0 +1,56 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasInstagramSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.instagram.com' });
return cookies.some(c => c.name === 'sessionid' && c.value);
}
async function verifyInstagramIdentity(page) {
if (!await hasInstagramSessionCookie(page)) {
throw new AuthRequiredError('www.instagram.com', 'Instagram sessionid cookie missing');
}
await page.goto('https://www.instagram.com/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const uid = (document.cookie.split('; ').find(c => c.startsWith('ds_user_id=')) || '').split('=')[1] || '';
if (!uid) return { kind: 'auth', detail: 'Instagram ds_user_id cookie missing' };
const r = await fetch('/api/v1/users/' + uid + '/info/', {
credentials: 'include',
headers: { 'X-IG-App-ID': '936619743392459', 'Accept': 'application/json' },
});
if (r.status === 401 || r.status === 403) {
return { kind: 'auth', detail: 'Instagram /users/info HTTP ' + r.status };
}
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
const user = d?.user;
if (!user || !user.pk) {
return { kind: 'auth', detail: 'Instagram /users/info returned no pk — session likely expired' };
}
return { ok: true, user_id: String(user.pk), username: String(user.username || ''), full_name: String(user.full_name || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('www.instagram.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from Instagram /users/info`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Instagram whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Instagram probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, username: result.username, full_name: result.full_name };
}
registerSiteAuthCommands({
site: 'instagram',
domain: 'instagram.com',
loginUrl: 'https://www.instagram.com/accounts/login/',
columns: ['user_id', 'username', 'full_name'],
verify: verifyInstagramIdentity,
poll: async (page) => {
if (!await hasInstagramSessionCookie(page)) {
throw new AuthRequiredError('www.instagram.com', 'Waiting for Instagram sessionid cookie');
}
return verifyInstagramIdentity(page);
},
});
+45
View File
@@ -0,0 +1,45 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasJdSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.jd.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('pin') || names.has('thor');
}
async function verifyJdIdentity(page) {
if (!await hasJdSessionCookie(page)) {
throw new AuthRequiredError('jd.com', 'JD pin / thor cookie missing');
}
await page.goto('https://home.jd.com/');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
const pinCookie = (document.cookie.split('; ').find(c => c.startsWith('pin=')) || '').split('=')[1] || '';
const decoded = pinCookie ? decodeURIComponent(pinCookie) : '';
if (!decoded) {
return { kind: 'auth', detail: 'JD pin cookie empty after decode' };
}
const nickEl = document.querySelector('.user-info, #aliveUserName, .name, .user-name');
const nickname = (nickEl && nickEl.textContent && nickEl.textContent.trim()) || '';
return { ok: true, pin: decoded, nickname };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('jd.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected JD probe: ${JSON.stringify(probe)}`);
return { pin: probe.pin, nickname: probe.nickname };
}
registerSiteAuthCommands({
site: 'jd',
domain: 'jd.com',
loginUrl: 'https://passport.jd.com/new/login.aspx',
columns: ['pin', 'nickname'],
verify: verifyJdIdentity,
poll: async (page) => {
if (!await hasJdSessionCookie(page)) {
throw new AuthRequiredError('jd.com', 'Waiting for JD pin / thor cookie');
}
return verifyJdIdentity(page);
},
});
+61
View File
@@ -0,0 +1,61 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasJianyuUserCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.jianyu360.cn' });
return cookies.some(c => c.name === 'userid_secure' && c.value);
}
async function verifyJianyuIdentity(page) {
if (!await hasJianyuUserCookie(page)) {
throw new AuthRequiredError('jianyu360.cn', 'Jianyu userid_secure cookie missing — anonymous');
}
await page.goto('https://www.jianyu360.cn/');
await page.wait(2);
const probe = await page.evaluate(`(async () => {
try {
const r = await fetch('/swordfish/frontPage/customer/sess/index', { credentials: 'include' });
const text = await r.text();
if (/<title>\\s*登录\\s*[-—]\\s*剑鱼标讯/.test(text)) {
return { kind: 'auth', detail: 'Jianyu protected page returned login page — anonymous' };
}
const userIdMeta = text.match(/<meta[^>]+name=[\"'](?:user-id|userId)[\"'][^>]+content=[\"']([^\"']+)[\"']/)?.[1] || '';
const userScript = text.match(/window\\.__USER__\\s*=\\s*(\\{[^}]+\\})/)?.[1] || '';
let userId = userIdMeta;
let name = '';
if (userScript) {
try {
const u = JSON.parse(userScript);
userId = userId || String(u.id || u.userId || '');
name = String(u.name || u.realName || u.nickName || '');
} catch {}
}
const cookieUid = (document.cookie.split('; ').find(c => c.startsWith('userid_secure=')) || '').split('=')[1] || '';
userId = userId || cookieUid;
if (!userId && !name) {
return { kind: 'auth', detail: 'Jianyu protected page 200 but no user identity surface' };
}
return { ok: true, user_id: userId, name };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('jianyu360.cn', probe.detail);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Jianyu whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Jianyu probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'jianyu',
domain: 'jianyu360.cn',
loginUrl: 'https://www.jianyu360.cn/',
columns: ['user_id', 'name'],
verify: verifyJianyuIdentity,
poll: async (page) => {
if (!await hasJianyuUserCookie(page)) {
throw new AuthRequiredError('jianyu360.cn', 'Waiting for Jianyu userid_secure cookie');
}
return verifyJianyuIdentity(page);
},
});
+46
View File
@@ -0,0 +1,46 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasKeSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.ke.com' });
return cookies.some(c => c.name === 'lianjia_token' && c.value);
}
async function verifyKeIdentity(page) {
if (!await hasKeSessionCookie(page)) {
throw new AuthRequiredError('ke.com', 'Ke lianjia_token cookie missing — anonymous');
}
await page.goto('https://www.ke.com/');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
const loginBtn = document.querySelector('.btn-login, a[class*=actLoginBtn], .login-btn');
if (loginBtn && /登录|登陆/.test(loginBtn.innerText || '')) {
return { kind: 'auth', detail: 'Ke shows 登录 button — anonymous session' };
}
const el = document.querySelector('.userNick, .user-name, .myInfo a, [class*=userNick]');
const username = (el?.innerText || '').trim();
if (!username) {
return { kind: 'auth', detail: 'Ke no user-name DOM anchor — anonymous or SSR failed' };
}
return { ok: true, username };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('ke.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Ke probe: ${JSON.stringify(probe)}`);
return { username: probe.username };
}
registerSiteAuthCommands({
site: 'ke',
domain: 'ke.com',
loginUrl: 'https://clogin.ke.com/login/?service=https%3A%2F%2Fwww.ke.com',
columns: ['username'],
verify: verifyKeIdentity,
poll: async (page) => {
if (!await hasKeSessionCookie(page)) {
throw new AuthRequiredError('ke.com', 'Waiting for Ke lianjia_token cookie');
}
return verifyKeIdentity(page);
},
});
+56
View File
@@ -0,0 +1,56 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasKimiSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.kimi.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('access_token') || names.has('refresh_token');
}
async function verifyKimiIdentity(page) {
// Source the token via CDP getCookies (works even if access_token is httpOnly,
// which document.cookie cannot read).
const cookies = await page.getCookies({ url: 'https://www.kimi.com' });
const token = cookies.find(c => c.name === 'access_token')?.value || '';
if (!token) {
throw new AuthRequiredError('kimi.com', 'Kimi access_token cookie missing');
}
await page.goto('https://www.kimi.com/');
await page.wait(3);
const result = await page.evaluate(`(async () => {
try {
const token = ${JSON.stringify(token)};
const res = await fetch('/api/user', { credentials: 'include', headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Kimi /api/user HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
if (!d || !d.id) {
return { kind: 'auth', detail: 'Kimi /api/user returned no id — anonymous' };
}
return { ok: true, user_id: String(d.id), name: String(d.name || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('kimi.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /api/user`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Kimi whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Kimi probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, name: result.name };
}
registerSiteAuthCommands({
site: 'kimi',
domain: 'kimi.com',
loginUrl: 'https://www.kimi.com/',
columns: ['user_id', 'name'],
verify: verifyKimiIdentity,
poll: async (page) => {
if (!await hasKimiSessionCookie(page)) {
throw new AuthRequiredError('kimi.com', 'Waiting for Kimi auth cookies');
}
return verifyKimiIdentity(page);
},
});
+61
View File
@@ -0,0 +1,61 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasLinkedinSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.linkedin.com' });
return cookies.some(c => c.name === 'li_at' && c.value);
}
async function verifyLinkedinLearningIdentity(page) {
if (!await hasLinkedinSessionCookie(page)) {
throw new AuthRequiredError('linkedin.com', 'LinkedIn li_at cookie missing');
}
await page.goto('https://www.linkedin.com/learning/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const jsessionRaw = (document.cookie.split('; ').find(c => c.startsWith('JSESSIONID=')) || '').split('=')[1] || '';
const csrf = jsessionRaw.replace(/^"|"$/g, '');
if (!csrf) return { kind: 'auth', detail: 'LinkedIn JSESSIONID missing — csrf token unavailable' };
const res = await fetch('/voyager/api/me', { credentials: 'include', headers: { 'csrf-token': csrf, 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'LinkedIn /voyager/api/me HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
const mini = d && d.miniProfile;
if (!mini || !mini.publicIdentifier) {
return { kind: 'auth', detail: 'LinkedIn /voyager/api/me 200 but miniProfile missing' };
}
const firstName = (mini.firstName && (mini.firstName.text || mini.firstName)) || '';
const lastName = (mini.lastName && (mini.lastName.text || mini.lastName)) || '';
return {
ok: true,
public_id: String(mini.publicIdentifier),
plain_id: String(d.plainId || ''),
name: String((firstName + ' ' + lastName).trim()),
};
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('linkedin.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /voyager/api/me`);
if (result?.kind === 'exception') throw new CommandExecutionError(`LinkedIn Learning whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected LinkedIn Learning probe: ${JSON.stringify(result)}`);
return { public_id: result.public_id, plain_id: result.plain_id, name: result.name };
}
registerSiteAuthCommands({
site: 'linkedin-learning',
domain: 'linkedin.com',
loginUrl: 'https://www.linkedin.com/login?session_redirect=%2Flearning%2F',
columns: ['public_id', 'plain_id', 'name'],
verify: verifyLinkedinLearningIdentity,
poll: async (page) => {
if (!await hasLinkedinSessionCookie(page)) {
throw new AuthRequiredError('linkedin.com', 'Waiting for LinkedIn li_at cookie');
}
return verifyLinkedinLearningIdentity(page);
},
});
+61
View File
@@ -0,0 +1,61 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasLinkedinSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.linkedin.com' });
return cookies.some(c => c.name === 'li_at' && c.value);
}
async function verifyLinkedinIdentity(page) {
if (!await hasLinkedinSessionCookie(page)) {
throw new AuthRequiredError('linkedin.com', 'LinkedIn li_at cookie missing');
}
await page.goto('https://www.linkedin.com/feed/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const jsessionRaw = (document.cookie.split('; ').find(c => c.startsWith('JSESSIONID=')) || '').split('=')[1] || '';
const csrf = jsessionRaw.replace(/^"|"$/g, '');
if (!csrf) return { kind: 'auth', detail: 'LinkedIn JSESSIONID missing — csrf token unavailable' };
const res = await fetch('/voyager/api/me', { credentials: 'include', headers: { 'csrf-token': csrf, 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'LinkedIn /voyager/api/me HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
const mini = d && d.miniProfile;
if (!mini || !mini.publicIdentifier) {
return { kind: 'auth', detail: 'LinkedIn /voyager/api/me 200 but miniProfile missing' };
}
const firstName = (mini.firstName && (mini.firstName.text || mini.firstName)) || '';
const lastName = (mini.lastName && (mini.lastName.text || mini.lastName)) || '';
return {
ok: true,
public_id: String(mini.publicIdentifier),
plain_id: String(d.plainId || ''),
name: String((firstName + ' ' + lastName).trim()),
};
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('linkedin.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /voyager/api/me`);
if (result?.kind === 'exception') throw new CommandExecutionError(`LinkedIn whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected LinkedIn probe: ${JSON.stringify(result)}`);
return { public_id: result.public_id, plain_id: result.plain_id, name: result.name };
}
registerSiteAuthCommands({
site: 'linkedin',
domain: 'www.linkedin.com',
loginUrl: 'https://www.linkedin.com/login',
columns: ['public_id', 'plain_id', 'name'],
verify: verifyLinkedinIdentity,
poll: async (page) => {
if (!await hasLinkedinSessionCookie(page)) {
throw new AuthRequiredError('linkedin.com', 'Waiting for LinkedIn li_at cookie');
}
return verifyLinkedinIdentity(page);
},
});
+54
View File
@@ -0,0 +1,54 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasLinuxDoSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://linux.do' });
return cookies.some(c => c.name === '_t' && c.value);
}
async function verifyLinuxDoIdentity(page) {
if (!await hasLinuxDoSessionCookie(page)) {
throw new AuthRequiredError('linux.do', 'Linux.do _t cookie missing — anonymous');
}
await page.goto('https://linux.do/');
await page.wait(2);
const probe = await page.evaluate(`(async () => {
try {
const u = document.querySelector('meta[name="current-user-username"]')?.getAttribute('content') || '';
if (!u) return { kind: 'auth', detail: 'Linux.do meta[current-user-username] missing — anonymous' };
const r = await fetch('/u/' + encodeURIComponent(u) + '.json', {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (r.status === 401 || r.status === 403) {
return { kind: 'auth', detail: 'Linux.do /u/<self>.json HTTP ' + r.status };
}
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
const user = d?.user;
if (!user || !user.id) return { kind: 'auth', detail: 'Linux.do /u/<self>.json missing user.id' };
return { ok: true, user_id: String(user.id), username: String(user.username || u), name: String(user.name || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('linux.do', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from Linux.do /u/<self>.json`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Linux.do whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Linux.do probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, username: probe.username, name: probe.name };
}
registerSiteAuthCommands({
site: 'linux-do',
domain: 'linux.do',
loginUrl: 'https://linux.do/login',
columns: ['user_id', 'username', 'name'],
verify: verifyLinuxDoIdentity,
poll: async (page) => {
if (!await hasLinuxDoSessionCookie(page)) {
throw new AuthRequiredError('linux.do', 'Waiting for Linux.do _t cookie');
}
return verifyLinuxDoIdentity(page);
},
});
+54
View File
@@ -0,0 +1,54 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasManusSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://manus.im' });
return cookies.some(c => /^(auth_session|manus_token|_session|session)$/.test(c.name) && c.value);
}
async function verifyManusIdentity(page) {
if (!await hasManusSessionCookie(page)) {
throw new AuthRequiredError('manus.im', 'Manus session cookies missing — anonymous');
}
await page.goto('https://manus.im/');
await page.wait(3);
const probe = await page.evaluate(`(async () => {
try {
const r = await fetch('/api/auth/session', { credentials: 'include', headers: { Accept: 'application/json' } });
if (r.status === 401 || r.status === 403) {
return { kind: 'auth', detail: 'Manus /api/auth/session HTTP ' + r.status };
}
if (r.status === 503) {
return { kind: 'http', httpStatus: 503 };
}
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
const u = d?.user || d;
if (!u || !(u.id || u.userId)) {
return { kind: 'auth', detail: 'Manus /api/auth/session 200 but no user' };
}
return { ok: true, user_id: String(u.id || u.userId), name: String(u.name || u.displayName || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('manus.im', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from Manus /api/auth/session`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Manus whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Manus probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'manus',
domain: 'manus.im',
loginUrl: 'https://manus.im/login',
columns: ['user_id', 'name'],
verify: verifyManusIdentity,
poll: async (page) => {
if (!await hasManusSessionCookie(page)) {
throw new AuthRequiredError('manus.im', 'Waiting for Manus session cookies');
}
return verifyManusIdentity(page);
},
});
+54
View File
@@ -0,0 +1,54 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasNotebookLmSsoCookies(page) {
const cookies = await page.getCookies({ url: 'https://notebooklm.google.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('SID') && names.has('SAPISID');
}
async function verifyNotebookLmIdentity(page) {
if (!await hasNotebookLmSsoCookies(page)) {
throw new AuthRequiredError('notebooklm.google.com', 'Google SSO cookies (SID + SAPISID) missing');
}
await page.goto('https://notebooklm.google.com/');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
if (/accounts\\.google\\.com\\/ServiceLogin/.test(location.href) || /accounts\\.google\\.com\\/signin/i.test(location.href)) {
return { kind: 'auth', detail: 'NotebookLM redirected to Google sign-in' };
}
const acctEl = document.querySelector('a[aria-label^="Google Account:"], a[aria-label*="Google 账号:"]');
if (!acctEl) {
return { kind: 'auth', detail: 'NotebookLM missing Google Account button' };
}
const label = acctEl.getAttribute('aria-label') || '';
const nameMatch = label.match(/Google Account:\\s*([^\\n\\(]+?)(?:\\s*\\n|\\s*\\()/i) ||
label.match(/Google 账号:\\s*([^\\n\\(]+?)(?:\\s*\\n|\\s*\\()/i);
const name = nameMatch ? nameMatch[1].trim() : '';
const authuserMatch = location.href.match(/[?&]authuser=(\\d+)/);
const authuser = authuserMatch ? Number(authuserMatch[1]) : 0;
if (!name) {
return { kind: 'auth', detail: 'NotebookLM Google Account aria-label found but name unparseable' };
}
return { ok: true, name, authuser };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('notebooklm.google.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected NotebookLM probe: ${JSON.stringify(probe)}`);
return { name: probe.name, authuser: probe.authuser };
}
registerSiteAuthCommands({
site: 'notebooklm',
domain: 'google.com',
loginUrl: 'https://accounts.google.com/ServiceLogin?service=lso&continue=https%3A%2F%2Fnotebooklm.google.com%2F',
columns: ['name', 'authuser'],
verify: verifyNotebookLmIdentity,
poll: async (page) => {
if (!await hasNotebookLmSsoCookies(page)) {
throw new AuthRequiredError('notebooklm.google.com', 'Waiting for Google SSO cookies (SID + SAPISID)');
}
return verifyNotebookLmIdentity(page);
},
});
+63
View File
@@ -0,0 +1,63 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasPixivSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.pixiv.net' });
// Anonymous PHPSESSID is a bare hash; logged-in form is `<userId>_<hash>`.
// Require the numeric uid prefix so the login poll doesn't navigate away
// from accounts.pixiv.net while the user is still signing in.
return cookies.some(c => c.name === 'PHPSESSID' && /^\d+_/.test(c.value || ''));
}
async function verifyPixivIdentity(page) {
if (!await hasPixivSessionCookie(page)) {
throw new AuthRequiredError('pixiv.net', 'Pixiv PHPSESSID cookie missing');
}
await page.goto('https://www.pixiv.net/');
await page.wait(2);
const probe = await page.evaluate(`(async () => {
try {
const meta = document.querySelector('meta[name="global-data"]')?.getAttribute('content') || '';
let userData = null;
if (meta) { try { userData = JSON.parse(meta); } catch {} }
const u = userData?.userData;
if (u?.id) {
return { ok: true, user_id: String(u.id), name: String(u.name || u.account || '') };
}
const r = await fetch('/ajax/user/extra', { credentials: 'include', headers: { Accept: 'application/json' } });
if (r.status === 401 || r.status === 403) {
return { kind: 'auth', detail: 'Pixiv /ajax/user/extra HTTP ' + r.status };
}
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
if (d?.error) return { kind: 'auth', detail: 'Pixiv /ajax/user/extra error=true — anonymous' };
const phpSess = (document.cookie.split('; ').find(c => c.startsWith('PHPSESSID=')) || '').split('=')[1] || '';
const uid = phpSess.split('_')[0] || '';
if (!uid) {
return { kind: 'auth', detail: 'Pixiv PHPSESSID prefix unparseable' };
}
return { ok: true, user_id: uid, name: '' };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('pixiv.net', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from Pixiv ajax`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Pixiv whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Pixiv probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'pixiv',
domain: 'pixiv.net',
loginUrl: 'https://accounts.pixiv.net/login',
columns: ['user_id', 'name'],
verify: verifyPixivIdentity,
poll: async (page) => {
if (!await hasPixivSessionCookie(page)) {
throw new AuthRequiredError('pixiv.net', 'Waiting for Pixiv PHPSESSID cookie');
}
return verifyPixivIdentity(page);
},
});
+51
View File
@@ -0,0 +1,51 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasPowerchinaSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://zhaopin.powerchina.cn' });
// JSESSIONID / SESSION are issued for anonymous Java sessions too; gate only on
// the auth-token cookies so the login poll doesn't navigate away mid-login.
return cookies.some(c => /^(Admin-Token|access_token)$/i.test(c.name) && c.value);
}
async function verifyPowerchinaIdentity(page) {
if (!await hasPowerchinaSessionCookie(page)) {
throw new AuthRequiredError('powerchina.cn', 'Powerchina session cookies missing — anonymous');
}
await page.goto('https://zhaopin.powerchina.cn/index');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
if (/\\/login(\\b|$|\\?)/.test(location.pathname)) {
return { kind: 'auth', detail: 'Powerchina redirected to /login — anonymous' };
}
const bodyText = document.body?.innerText || '';
if (/欢迎登录.*【登录】|请登录|您未登录/.test(bodyText)) {
return { kind: 'auth', detail: 'Powerchina shows 欢迎登录 prompt — anonymous' };
}
const el = document.querySelector('.user-info, .userName, .personalCenter [class*=name]');
const userName = (el?.innerText || '').trim();
if (!userName) {
return { kind: 'auth', detail: 'Powerchina no user name DOM — anonymous or layout changed' };
}
return { ok: true, name: userName };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('powerchina.cn', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Powerchina probe: ${JSON.stringify(probe)}`);
return { name: probe.name };
}
registerSiteAuthCommands({
site: 'powerchina',
domain: 'powerchina.cn',
loginUrl: 'https://zhaopin.powerchina.cn/login',
columns: ['name'],
verify: verifyPowerchinaIdentity,
poll: async (page) => {
if (!await hasPowerchinaSessionCookie(page)) {
throw new AuthRequiredError('powerchina.cn', 'Waiting for Powerchina session cookies');
}
return verifyPowerchinaIdentity(page);
},
});
+47
View File
@@ -0,0 +1,47 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
// Quark's auth cookies (__pus / __kp / __puus) are httpOnly. The account/info
// endpoint returns HTTP 200 with an empty `data` array when anonymous and a
// populated object once logged in, so the poll uses a no-nav probe of it.
const WHOAMI_PROBE = `(async () => {
try {
const r = await fetch('https://pan.quark.cn/account/info?fr=pc&platform=pc', { credentials: 'include', headers: { Accept: 'application/json' } });
if (r.status === 401 || r.status === 403) return { kind: 'auth', detail: 'Quark account/info HTTP ' + r.status };
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
const data = d && d.data;
const isEmpty = !data || Array.isArray(data) || Object.keys(data).length === 0;
if (isEmpty) return { kind: 'auth', detail: 'Quark account/info returned empty data — anonymous' };
const nickname = String(data.nickname || data.nick_name || data.name || '');
if (!nickname) return { kind: 'render-error', detail: 'Quark account/info populated but no nickname field — response shape drift' };
return { ok: true, nickname };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`;
async function verifyQuarkIdentity(page) {
await page.goto('https://pan.quark.cn/');
await page.wait(2);
const probe = await page.evaluate(WHOAMI_PROBE);
if (probe?.kind === 'auth') throw new AuthRequiredError('quark.cn', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from Quark account/info`);
if (probe?.kind === 'render-error') throw new CommandExecutionError(probe.detail);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Quark whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Quark probe: ${JSON.stringify(probe)}`);
return { nickname: probe.nickname };
}
registerSiteAuthCommands({
site: 'quark',
domain: 'quark.cn',
loginUrl: 'https://pan.quark.cn/',
columns: ['nickname'],
verify: verifyQuarkIdentity,
poll: async (page) => {
const probe = await page.evaluate(WHOAMI_PROBE);
if (!probe?.ok) throw new AuthRequiredError('quark.cn', 'Waiting for Quark login');
return { nickname: probe.nickname };
},
});
+55
View File
@@ -0,0 +1,55 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasQwenSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://chat.qwen.ai' });
return cookies.some(c => c.name === 'token' && c.value);
}
async function verifyQwenIdentity(page) {
// Source the token via CDP getCookies (works even if `token` is httpOnly,
// which document.cookie cannot read).
const cookies = await page.getCookies({ url: 'https://chat.qwen.ai' });
const token = cookies.find(c => c.name === 'token')?.value || '';
if (!token) {
throw new AuthRequiredError('qwen.ai', 'Qwen token cookie missing');
}
await page.goto('https://chat.qwen.ai/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const token = ${JSON.stringify(token)};
const res = await fetch('/api/v1/auths/', { credentials: 'include', headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Qwen /api/v1/auths/ HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
if (!d || !d.id) {
return { kind: 'auth', detail: 'Qwen /api/v1/auths/ returned no user id' };
}
return { ok: true, user_id: String(d.id), name: String(d.name || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('qwen.ai', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /api/v1/auths/`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Qwen whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Qwen probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, name: result.name };
}
registerSiteAuthCommands({
site: 'qwen',
domain: 'qwen.ai',
loginUrl: 'https://chat.qwen.ai/auth?action=login',
columns: ['user_id', 'name'],
verify: verifyQwenIdentity,
poll: async (page) => {
if (!await hasQwenSessionCookie(page)) {
throw new AuthRequiredError('qwen.ai', 'Waiting for Qwen token cookie');
}
return verifyQwenIdentity(page);
},
});
+51
View File
@@ -0,0 +1,51 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasRedditSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.reddit.com' });
return cookies.some(c => c.name === 'reddit_session' && c.value);
}
async function verifyRedditIdentity(page) {
if (!await hasRedditSessionCookie(page)) {
throw new AuthRequiredError('reddit.com', 'Reddit reddit_session cookie missing');
}
await page.goto('https://www.reddit.com/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const res = await fetch('/api/me.json', { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Reddit /api/me.json HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
const data = d && d.data;
if (!data || !data.name) {
return { kind: 'auth', detail: 'Reddit /api/me.json 200 but no data.name — anonymous' };
}
return { ok: true, username: String(data.name), id: String(data.id || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('reddit.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /api/me.json`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Reddit whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Reddit probe: ${JSON.stringify(result)}`);
return { username: result.username, id: result.id };
}
registerSiteAuthCommands({
site: 'reddit',
domain: 'reddit.com',
loginUrl: 'https://www.reddit.com/login',
columns: ['username', 'id'],
verify: verifyRedditIdentity,
poll: async (page) => {
if (!await hasRedditSessionCookie(page)) {
throw new AuthRequiredError('reddit.com', 'Waiting for Reddit reddit_session cookie');
}
return verifyRedditIdentity(page);
},
});
+51
View File
@@ -0,0 +1,51 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasRednoteSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.rednote.com' });
return cookies.some(c => c.name === 'web_session' && c.value);
}
async function verifyRednoteIdentity(page) {
if (!await hasRednoteSessionCookie(page)) {
throw new AuthRequiredError('rednote.com', 'Rednote web_session cookie missing');
}
await page.goto('https://www.rednote.com/explore');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
const state = window.__INITIAL_STATE__;
if (!state?.user) {
return { kind: 'auth', detail: 'Rednote __INITIAL_STATE__.user missing' };
}
const loggedIn = state.user.loggedIn?._value;
const userInfo = state.user.userInfo?._value || {};
if (loggedIn !== true) {
return { kind: 'auth', detail: 'Rednote loggedIn._value=' + String(loggedIn) + ' — anonymous' };
}
const userId = String(userInfo.userId || userInfo.user_id || '');
const nickname = String(userInfo.nickname || userInfo.name || '');
if (!userId) {
return { kind: 'auth', detail: 'Rednote logged-in but userId missing — stale session' };
}
return { ok: true, user_id: userId, nickname };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('rednote.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Rednote probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, nickname: probe.nickname };
}
registerSiteAuthCommands({
site: 'rednote',
domain: 'rednote.com',
loginUrl: 'https://www.rednote.com/explore',
columns: ['user_id', 'nickname'],
verify: verifyRednoteIdentity,
poll: async (page) => {
if (!await hasRednoteSessionCookie(page)) {
throw new AuthRequiredError('rednote.com', 'Waiting for Rednote web_session cookie');
}
return verifyRednoteIdentity(page);
},
});
+67
View File
@@ -0,0 +1,67 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function verifyReutersIdentity(page) {
await page.goto('https://www.reuters.com/');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
try {
const subStateRaw = localStorage.getItem('rcom-subscription-state');
let subState = null;
if (subStateRaw) { try { subState = JSON.parse(subStateRaw); } catch {} }
if (!subState || subState.isLoggedIn !== true) {
return { kind: 'auth', detail: 'Reuters rcom-subscription-state.isLoggedIn is not true — anonymous' };
}
let oidcKey = null;
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k && k.startsWith('oidc.user:')) { oidcKey = k; break; }
}
let cuid = '';
if (oidcKey) {
try {
const u = JSON.parse(localStorage.getItem(oidcKey) || '{}');
cuid = String(u?.profile?.cuid || u?.profile?.sub || '');
} catch {}
}
if (!cuid) {
const ajs = localStorage.getItem('ajs_user_id');
if (ajs && ajs !== 'null') cuid = ajs;
}
if (!cuid) {
return { kind: 'auth', detail: 'Reuters logged-in but cuid missing — session shape drifted' };
}
return { ok: true, user_id: cuid, subscribed: Boolean(subState.isSubscribed) };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('reuters.com', probe.detail);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Reuters whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Reuters probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, subscribed: probe.subscribed };
}
registerSiteAuthCommands({
site: 'reuters',
domain: 'reuters.com',
loginUrl: 'https://www.reuters.com/account/sign-in/',
columns: ['user_id', 'subscribed'],
verify: verifyReutersIdentity,
// No-navigation poll: check localStorage on the current page so login-flow
// polling doesn't bounce the user off the sign-in page every interval.
poll: async (page) => {
const loggedIn = await page.evaluate(`(() => {
try {
const raw = localStorage.getItem('rcom-subscription-state');
return raw ? JSON.parse(raw).isLoggedIn === true : false;
} catch { return false; }
})()`);
if (!loggedIn) {
throw new AuthRequiredError('reuters.com', 'Waiting for Reuters login');
}
return verifyReutersIdentity(page);
},
});
+62
View File
@@ -0,0 +1,62 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasSunoClerkCookie(page) {
const cookies = await page.getCookies({ url: 'https://clerk.suno.com' });
// Clerk sets __client for anonymous sessions too; __session (the session JWT)
// is present only when authenticated, so gate on it to avoid navigating away
// mid-login.
return cookies.some(c => c.name === '__session' && c.value);
}
async function verifySunoIdentity(page) {
if (!await hasSunoClerkCookie(page)) {
throw new AuthRequiredError('suno.com', 'Suno Clerk __session/__client cookie missing');
}
await page.goto('https://suno.com/');
await page.wait(2);
const probe = await page.evaluate(`(async () => {
try {
const r = await fetch('https://clerk.suno.com/v1/client?_clerk_js_version=5', {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (r.status === 401 || r.status === 403) {
return { kind: 'auth', detail: 'clerk.suno.com /v1/client HTTP ' + r.status };
}
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
const sessions = d?.response?.sessions || [];
if (!Array.isArray(sessions) || sessions.length === 0) {
return { kind: 'auth', detail: 'clerk.suno.com sessions=[] — anonymous' };
}
const active = sessions.find(s => s.status === 'active') || sessions[0];
const user = active?.user;
if (!user?.id) {
return { kind: 'auth', detail: 'clerk.suno.com session present but no user.id — stale session' };
}
return { ok: true, user_id: String(user.id), name: String(user.username || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('suno.com', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from clerk.suno.com`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`Suno whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Suno probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'suno',
domain: 'suno.com',
loginUrl: 'https://suno.com/?sign-in=true',
columns: ['user_id', 'name'],
verify: verifySunoIdentity,
poll: async (page) => {
if (!await hasSunoClerkCookie(page)) {
throw new AuthRequiredError('suno.com', 'Waiting for Suno Clerk __session/__client cookie');
}
return verifySunoIdentity(page);
},
});
+57
View File
@@ -0,0 +1,57 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasTaobaoSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.taobao.com' });
return cookies.some(c => c.name === 'tracknick' && c.value);
}
async function verifyTaobaoIdentity(page) {
if (!await hasTaobaoSessionCookie(page)) {
throw new AuthRequiredError('taobao.com', 'Taobao tracknick cookie missing — anonymous');
}
await page.goto('https://i.taobao.com/my_itaobao');
await page.wait(2);
const finalUrl = await page.evaluate(`location.href`);
if (/login\.taobao\.com\/member\/login/.test(String(finalUrl || ''))) {
throw new AuthRequiredError('taobao.com', `Taobao my_itaobao redirected to login: ${finalUrl}`);
}
const cookies = await page.getCookies({ url: 'https://www.taobao.com' });
const tracknick = cookies.find(c => c.name === 'tracknick')?.value || '';
if (!tracknick) {
throw new AuthRequiredError('taobao.com', 'Taobao tracknick cookie absent after navigation');
}
const domInfo = await page.evaluate(`
(() => {
const nick = (document.querySelector('.user-nick, .site-nav-user, .user-name')?.innerText || '').trim();
const html = document.body?.innerHTML || '';
const userIdMatch = html.match(/userId[\"'\\s:=]+(\\d+)/i);
return { nickname: nick, user_id: userIdMatch?.[1] || '' };
})()
`);
let decodedTracknick = '';
try {
decodedTracknick = JSON.parse('"' + tracknick.replace(/\\/g, '\\\\') + '"');
} catch {
decodedTracknick = tracknick;
}
const nickname = domInfo.nickname || decodedTracknick;
if (!domInfo.user_id) {
throw new CommandExecutionError('Taobao my_itaobao rendered but no user_id extractable — stale cookie2 or layout drift');
}
return { user_id: String(domInfo.user_id), nickname: String(nickname) };
}
registerSiteAuthCommands({
site: 'taobao',
domain: 'taobao.com',
loginUrl: 'https://login.taobao.com/member/login.jhtml',
columns: ['user_id', 'nickname'],
verify: verifyTaobaoIdentity,
poll: async (page) => {
if (!await hasTaobaoSessionCookie(page)) {
throw new AuthRequiredError('taobao.com', 'Waiting for Taobao tracknick cookie');
}
return verifyTaobaoIdentity(page);
},
});
+64
View File
@@ -0,0 +1,64 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasTiktokSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.tiktok.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('sessionid') || names.has('sid_tt') || names.has('uid_tt');
}
async function verifyTiktokIdentity(page) {
if (!await hasTiktokSessionCookie(page)) {
throw new AuthRequiredError('www.tiktok.com', 'TikTok session cookies (sessionid/sid_tt/uid_tt) missing');
}
await page.goto('https://www.tiktok.com/foryou');
await page.wait(2);
const info = await page.evaluate(`
(() => {
const raw = document.querySelector('script[id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]')?.textContent;
if (!raw) return null;
let data;
try { data = JSON.parse(raw); } catch { return null; }
const scope = data?.['__DEFAULT_SCOPE__'] || {};
const seen = new Set();
const stack = [scope];
while (stack.length) {
const node = stack.pop();
if (!node || typeof node !== 'object' || seen.has(node)) continue;
seen.add(node);
if (Array.isArray(node)) { stack.push(...node); continue; }
const u = node.user;
if (u && typeof u === 'object') {
const isMe = Boolean(u.isOwner || u.is_owner || u.isCurrentUser);
if (isMe && (u.secUid || u.sec_uid)) {
return {
sec_uid: String(u.secUid || u.sec_uid),
username: String(u.uniqueId || u.unique_id || u.username || ''),
nickname: String(u.nickname || u.nickName || ''),
};
}
}
for (const v of Object.values(node)) if (v && typeof v === 'object') stack.push(v);
}
return null;
})()
`);
if (!info?.sec_uid) {
throw new AuthRequiredError('www.tiktok.com', 'TikTok universal data has no owner user — identity not rehydrated');
}
return { sec_uid: info.sec_uid, username: info.username, nickname: info.nickname };
}
registerSiteAuthCommands({
site: 'tiktok',
domain: 'tiktok.com',
loginUrl: 'https://www.tiktok.com/login',
columns: ['sec_uid', 'username', 'nickname'],
verify: verifyTiktokIdentity,
poll: async (page) => {
if (!await hasTiktokSessionCookie(page)) {
throw new AuthRequiredError('www.tiktok.com', 'Waiting for TikTok session cookies');
}
return verifyTiktokIdentity(page);
},
});
+69
View File
@@ -0,0 +1,69 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasToutiaoSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://mp.toutiao.com' });
return cookies.some(c => c.name === 'sessionid' && c.value);
}
async function verifyToutiaoIdentity(page) {
if (!await hasToutiaoSessionCookie(page)) {
throw new AuthRequiredError('toutiao.com', 'Toutiao sessionid cookie missing');
}
await page.goto('https://mp.toutiao.com/');
await page.wait(2);
const probe = await page.evaluate(`
(() => {
if (/\\/auth\\/page\\/login/.test(location.href)) {
return { kind: 'auth', detail: 'mp.toutiao.com redirected to /auth/page/login — anonymous' };
}
let userId = '', nickname = '';
try {
const seen = new Set();
const stack = [window.__INITIAL_STATE__, window.__REDUX_STATE__, window.__SSR_DATA__].filter(Boolean);
while (stack.length) {
const node = stack.pop();
if (!node || typeof node !== 'object' || seen.has(node)) continue;
seen.add(node);
if (Array.isArray(node)) { stack.push(...node); continue; }
const u = node.userInfo || node.user || node.currentUser || node.userBase;
if (u && typeof u === 'object' && (u.user_id || u.userId || u.uid)) {
userId = String(u.user_id || u.userId || u.uid || '');
nickname = String(u.screen_name || u.name || u.nickname || u.user_name || '');
break;
}
for (const v of Object.values(node)) if (v && typeof v === 'object') stack.push(v);
}
} catch {}
if (!userId) {
const ssoUid = (document.cookie.split('; ').find(c => c.startsWith('sso_uid=')) || '').split('=')[1] || '';
if (ssoUid) userId = ssoUid;
}
if (!nickname) {
const el = document.querySelector('.user-name, .header-username, .avatar-name, [class*="userName"]');
nickname = (el?.innerText || '').trim();
}
if (!userId) {
return { kind: 'auth', detail: 'Toutiao dashboard rendered but no user_id surface — stale session' };
}
return { ok: true, user_id: userId, nickname };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('toutiao.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Toutiao probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, nickname: probe.nickname };
}
registerSiteAuthCommands({
site: 'toutiao',
domain: 'toutiao.com',
loginUrl: 'https://mp.toutiao.com/auth/page/login',
columns: ['user_id', 'nickname'],
verify: verifyToutiaoIdentity,
poll: async (page) => {
if (!await hasToutiaoSessionCookie(page)) {
throw new AuthRequiredError('toutiao.com', 'Waiting for Toutiao sessionid cookie');
}
return verifyToutiaoIdentity(page);
},
});
+52
View File
@@ -0,0 +1,52 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasUpworkSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.upwork.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('master_access_token') || names.has('XSRF-TOKEN') && names.has('user_uid');
}
async function verifyUpworkIdentity(page) {
if (!await hasUpworkSessionCookie(page)) {
throw new AuthRequiredError('upwork.com', 'Upwork session cookies missing');
}
await page.goto('https://www.upwork.com/nx/find-work/');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
if (/\\/(ab|account-security\\/login|signup)\\//.test(location.pathname)) {
return { kind: 'auth', detail: 'Upwork redirected to login flow' };
}
const nuxt = (typeof window !== 'undefined' && window.__NUXT__) ? window.__NUXT__ : null;
const state = nuxt && (nuxt.state || (nuxt.data && nuxt.data[0]));
const user = state && (state.user || (state.auth && state.auth.user));
const profile = user && (user.profile || user);
if (!profile || !profile.id) {
return { kind: 'auth', detail: 'Upwork __NUXT__ has no profile id — anonymous' };
}
return {
ok: true,
user_id: String(profile.id || profile.uid || ''),
ciphertext: String(profile.ciphertext || ''),
};
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('upwork.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected Upwork probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, ciphertext: probe.ciphertext };
}
registerSiteAuthCommands({
site: 'upwork',
domain: 'upwork.com',
loginUrl: 'https://www.upwork.com/ab/account-security/login',
columns: ['user_id', 'ciphertext'],
verify: verifyUpworkIdentity,
poll: async (page) => {
if (!await hasUpworkSessionCookie(page)) {
throw new AuthRequiredError('upwork.com', 'Waiting for Upwork session cookies');
}
return verifyUpworkIdentity(page);
},
});
+43
View File
@@ -0,0 +1,43 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasV2exAuthCookie(page) {
// A2 is V2EX's httpOnly logged-in session cookie.
const cookies = await page.getCookies({ url: 'https://www.v2ex.com' });
return cookies.some(c => c.name === 'A2' && c.value);
}
async function verifyV2exIdentity(page) {
if (!await hasV2exAuthCookie(page)) {
throw new AuthRequiredError('v2ex.com', 'V2EX A2 session cookie missing — anonymous');
}
await page.goto('https://www.v2ex.com/');
await page.wait(1);
const probe = await page.evaluate(`
(() => {
const link = document.querySelector('#Top a[href^="/member/"]');
if (!link) return { kind: 'auth', detail: 'V2EX top bar has no member link — anonymous session' };
const href = link.getAttribute('href') || '';
const username = (link.innerText || href.replace('/member/', '')).trim();
if (!username) return { kind: 'auth', detail: 'V2EX member link present but username empty' };
return { ok: true, username };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('v2ex.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected V2EX probe: ${JSON.stringify(probe)}`);
return { username: probe.username };
}
registerSiteAuthCommands({
site: 'v2ex',
domain: 'v2ex.com',
loginUrl: 'https://www.v2ex.com/signin',
columns: ['username'],
verify: verifyV2exIdentity,
poll: async (page) => {
if (!await hasV2exAuthCookie(page)) {
throw new AuthRequiredError('v2ex.com', 'Waiting for V2EX A2 session cookie');
}
return verifyV2exIdentity(page);
},
});
+61
View File
@@ -0,0 +1,61 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasWechatChannelsSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://channels.weixin.qq.com' });
return cookies.some(c => c.name === 'sessionid' && c.value);
}
async function verifyWechatChannelsIdentity(page) {
if (!await hasWechatChannelsSessionCookie(page)) {
throw new AuthRequiredError('channels.weixin.qq.com', 'WeChat Channels sessionid cookie missing');
}
await page.goto('https://channels.weixin.qq.com/platform');
await page.wait(2);
const probe = await page.evaluate(`(async () => {
try {
if (/login\\.html/.test(location.href)) {
return { kind: 'auth', detail: 'WeChat Channels platform redirected to login.html' };
}
const r = await fetch('/cgi-bin/mmfinderassistant-bin/auth/auth_data', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: '{}',
});
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
if (!d || d.base_resp?.ret !== 0) {
return { kind: 'auth', detail: 'WeChat Channels auth_data base_resp.ret=' + String(d?.base_resp?.ret) };
}
const fu = d.data?.finder_user || d.finder_user || {};
const userId = String(fu.uniq_id || fu.username || '');
const name = String(fu.nickname || fu.name || '');
if (!userId && !name) {
return { kind: 'auth', detail: 'WeChat Channels auth_data 200 but finder_user empty' };
}
return { ok: true, user_id: userId, name };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('channels.weixin.qq.com', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from auth_data`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`WeChat Channels whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected WeChat Channels probe: ${JSON.stringify(probe)}`);
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'wechat-channels',
domain: 'channels.weixin.qq.com',
loginUrl: 'https://channels.weixin.qq.com/login.html?from=assistant',
columns: ['user_id', 'name'],
verify: verifyWechatChannelsIdentity,
poll: async (page) => {
if (!await hasWechatChannelsSessionCookie(page)) {
throw new AuthRequiredError('channels.weixin.qq.com', 'Waiting for WeChat Channels sessionid cookie');
}
return verifyWechatChannelsIdentity(page);
},
});
+52
View File
@@ -0,0 +1,52 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasWeiboSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://weibo.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('SUB') && names.has('SUBP');
}
async function verifyWeiboIdentity(page) {
if (!await hasWeiboSessionCookie(page)) {
throw new AuthRequiredError('weibo.com', 'Weibo SUB / SUBP cookies missing');
}
await page.goto('https://weibo.com/');
await page.wait(3);
const result = await page.evaluate(`(async () => {
try {
const res = await fetch('/ajax/profile/info', { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Weibo /ajax/profile/info HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
const user = d && d.data && d.data.user;
if (!user || !user.id) {
return { kind: 'auth', detail: 'Weibo /ajax/profile/info returned no user — anonymous' };
}
return { ok: true, user_id: String(user.id), screen_name: String(user.screen_name || ''), profile_url: String(user.profile_url || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('weibo.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /ajax/profile/info`);
if (result?.kind === 'exception') throw new CommandExecutionError(`Weibo whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected Weibo probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, screen_name: result.screen_name, profile_url: result.profile_url };
}
registerSiteAuthCommands({
site: 'weibo',
domain: 'weibo.com',
loginUrl: 'https://weibo.com/login',
columns: ['user_id', 'screen_name', 'profile_url'],
verify: verifyWeiboIdentity,
poll: async (page) => {
if (!await hasWeiboSessionCookie(page)) {
throw new AuthRequiredError('weibo.com', 'Waiting for Weibo SUB / SUBP cookies');
}
return verifyWeiboIdentity(page);
},
});
+58
View File
@@ -0,0 +1,58 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasWereadSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://weread.qq.com' });
return cookies.some(c => c.name === 'wr_vid' && c.value);
}
async function verifyWereadIdentity(page) {
if (!await hasWereadSessionCookie(page)) {
throw new AuthRequiredError('weread.qq.com', 'WeRead wr_vid cookie missing');
}
await page.goto('https://weread.qq.com/');
await page.wait(2);
const result = await page.evaluate(`(async () => {
try {
const wrVid = (document.cookie.split('; ').find(c => c.startsWith('wr_vid=')) || '').split('=')[1] || '';
if (!wrVid) {
return { kind: 'auth', detail: 'WeRead wr_vid cookie absent in document.cookie' };
}
const res = await fetch('/web/user', { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'WeRead /web/user HTTP ' + res.status };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
if (d && d.errCode && d.errCode !== 0) {
return { kind: 'auth', detail: 'WeRead /web/user errCode=' + d.errCode };
}
return {
ok: true,
user_id: String(d.userVid || wrVid),
name: String(d.name || d.nickName || ''),
};
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') throw new AuthRequiredError('weread.qq.com', result.detail);
if (result?.kind === 'http') throw new CommandExecutionError(`HTTP ${result.httpStatus} from /web/user`);
if (result?.kind === 'exception') throw new CommandExecutionError(`WeRead whoami failed: ${result.detail}`);
if (!result?.ok) throw new CommandExecutionError(`Unexpected WeRead probe: ${JSON.stringify(result)}`);
return { user_id: result.user_id, name: result.name };
}
registerSiteAuthCommands({
site: 'weread',
domain: 'weread.qq.com',
loginUrl: 'https://weread.qq.com/',
columns: ['user_id', 'name'],
verify: verifyWereadIdentity,
poll: async (page) => {
if (!await hasWereadSessionCookie(page)) {
throw new AuthRequiredError('weread.qq.com', 'Waiting for WeRead wr_vid cookie');
}
return verifyWereadIdentity(page);
},
});
+67
View File
@@ -0,0 +1,67 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasXianyuIdentityCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.goofish.com' });
return cookies.some(c => (c.name === 'unb' || c.name === 'tracknick') && c.value);
}
async function verifyXianyuIdentity(page) {
if (!await hasXianyuIdentityCookie(page)) {
throw new AuthRequiredError('goofish.com', 'Xianyu unb/tracknick cookie missing — anonymous');
}
await page.goto('https://www.goofish.com/personal');
await page.wait(2);
const finalUrl = await page.evaluate(`location.href`);
if (/passport\.(taobao|goofish)\.com\/(member\/login|login)/.test(String(finalUrl || ''))) {
throw new AuthRequiredError('goofish.com', `Xianyu /personal redirected to login: ${finalUrl}`);
}
const cookies = await page.getCookies({ url: 'https://www.goofish.com' });
const tracknick = cookies.find(c => c.name === 'tracknick')?.value || '';
const unb = cookies.find(c => c.name === 'unb')?.value || '';
const probe = await page.evaluate(`
(() => {
const bodyText = document.body?.innerText || '';
const requiresAuth = /请先登录|登录后/.test(bodyText);
const blocked = /验证码|安全验证|异常访问/.test(bodyText);
const nick = document.querySelector('.user-name, .user-nick, .nick, [class*="nickname"]')?.innerText?.trim() || '';
const html = document.body?.innerHTML || '';
const userIdMatch = html.match(/['"]?userId['"]?\\s*[:=]\\s*['"]?(\\d+)/i);
return { requiresAuth, blocked, domNick: nick, domUserId: userIdMatch?.[1] || '' };
})()
`);
if (probe.blocked) {
throw new AuthRequiredError('goofish.com', 'Xianyu blocked by verification / risk control');
}
if (probe.requiresAuth) {
throw new AuthRequiredError('goofish.com', 'Xianyu /personal shows login prompt — anonymous');
}
const userId = probe.domUserId || unb;
let decodedTracknick = '';
if (tracknick) {
try {
decodedTracknick = JSON.parse('"' + tracknick.replace(/\\/g, '\\\\') + '"');
} catch {
decodedTracknick = tracknick;
}
}
const nickname = probe.domNick || decodedTracknick;
if (!userId && !nickname) {
throw new CommandExecutionError('Xianyu /personal rendered but no user identity extractable — stale unb or layout drift');
}
return { user_id: String(userId), nickname: String(nickname) };
}
registerSiteAuthCommands({
site: 'xianyu',
domain: 'goofish.com',
loginUrl: 'https://www.goofish.com/login',
columns: ['user_id', 'nickname'],
verify: verifyXianyuIdentity,
poll: async (page) => {
if (!await hasXianyuIdentityCookie(page)) {
throw new AuthRequiredError('goofish.com', 'Waiting for Xianyu unb/tracknick cookie');
}
return verifyXianyuIdentity(page);
},
});
+54
View File
@@ -0,0 +1,54 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasXiaoeAdminCookie(page) {
const cookies = await page.getCookies({ url: 'https://admin.xiaoe-tech.com' });
return cookies.some(c => (c.name === 'XIAOEID' || c.name === 'b_user_token') && c.value);
}
async function verifyXiaoeIdentity(page) {
if (!await hasXiaoeAdminCookie(page)) {
throw new AuthRequiredError('xiaoe-tech.com', 'Xiaoe XIAOEID/b_user_token cookie missing — anonymous');
}
await page.goto('https://admin.xiaoe-tech.com/t/account/muti_index');
await page.wait(3);
const finalUrl = await page.evaluate(`location.href`);
if (/login|signin|#\/wx$/.test(String(finalUrl || ''))) {
throw new AuthRequiredError('xiaoe-tech.com', `Xiaoe admin page redirected to login: ${finalUrl}`);
}
const cookies = await page.getCookies({ url: 'https://admin.xiaoe-tech.com' });
const xiaoeId = cookies.find(c => c.name === 'XIAOEID')?.value || '';
const unionId = cookies.find(c => c.name === 'unionid')?.value || '';
const probe = await page.evaluate(`
(() => {
const bodyText = document.body?.innerText || '';
if (/微信扫码登录|手机号登录|登录小鹅通/.test(bodyText)) {
return { isLoginPage: true };
}
const nick = document.querySelector('.user-name, .nickname, [class*="userName"], [class*="user-info"]')?.innerText?.trim() || '';
return { isLoginPage: false, domNick: nick };
})()
`);
if (probe.isLoginPage) {
throw new AuthRequiredError('xiaoe-tech.com', 'Xiaoe admin page showed login UI — anonymous session');
}
const userId = xiaoeId || unionId;
if (!userId) {
throw new CommandExecutionError('Xiaoe admin page rendered but no user_id cookie extractable — stale session');
}
return { user_id: String(userId), nickname: String(probe.domNick || '') };
}
registerSiteAuthCommands({
site: 'xiaoe',
domain: 'xiaoe-tech.com',
loginUrl: 'https://admin.xiaoe-tech.com/',
columns: ['user_id', 'nickname'],
verify: verifyXiaoeIdentity,
poll: async (page) => {
if (!await hasXiaoeAdminCookie(page)) {
throw new AuthRequiredError('xiaoe-tech.com', 'Waiting for Xiaoe XIAOEID/b_user_token cookie');
}
return verifyXiaoeIdentity(page);
},
});
+62
View File
@@ -0,0 +1,62 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasXueqiuAccessToken(page) {
const cookies = await page.getCookies({ url: 'https://xueqiu.com' });
return cookies.some(c => c.name === 'xq_a_token' && c.value);
}
async function verifyXueqiuIdentity(page) {
if (!await hasXueqiuAccessToken(page)) {
throw new AuthRequiredError('xueqiu.com', 'Xueqiu xq_a_token cookie missing — anonymous');
}
await page.goto('https://xueqiu.com/');
await page.wait(2);
const probe = await page.evaluate(`(async () => {
try {
const res = await fetch(
'https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?size=1&category=1&pid=-1',
{ credentials: 'include' },
);
if (res.status === 403) {
return { kind: 'http', httpStatus: 403, detail: 'xueqiu stock API 403 — anti-bot / rate limit' };
}
if (!res.ok) return { kind: 'http', httpStatus: res.status };
const d = await res.json();
if (d?.error_code === 60201) {
return { kind: 'auth', detail: 'xueqiu portfolio API error_code 60201 用户id无效 — anonymous' };
}
if (d?.error_code) {
return { kind: 'xq-error', errorCode: d.error_code, detail: d.error_description || 'xueqiu API error' };
}
const uCookie = document.cookie.split('; ').find(c => c.startsWith('u='))?.split('=')[1] || '';
const cookiesuCookie = document.cookie.split('; ').find(c => c.startsWith('cookiesu='))?.split('=')[1] || '';
if (!uCookie || uCookie === cookiesuCookie) {
return { kind: 'auth', detail: 'xueqiu u cookie equals cookiesu (device id) — anonymous despite portfolio API 200' };
}
return { ok: true, user_id: uCookie };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (probe?.kind === 'auth') throw new AuthRequiredError('xueqiu.com', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from xueqiu stock API: ${probe.detail || ''}`);
if (probe?.kind === 'xq-error') throw new CommandExecutionError(`xueqiu API error_code ${probe.errorCode}: ${probe.detail}`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`xueqiu whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected xueqiu probe: ${JSON.stringify(probe)}`);
return { user_id: String(probe.user_id) };
}
registerSiteAuthCommands({
site: 'xueqiu',
domain: 'xueqiu.com',
loginUrl: 'https://xueqiu.com/',
columns: ['user_id'],
verify: verifyXueqiuIdentity,
poll: async (page) => {
if (!await hasXueqiuAccessToken(page)) {
throw new AuthRequiredError('xueqiu.com', 'Waiting for Xueqiu xq_a_token cookie');
}
return verifyXueqiuIdentity(page);
},
});
+53
View File
@@ -0,0 +1,53 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasGoogleSessionCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.youtube.com' });
const names = new Set(cookies.map(c => c.name));
return names.has('SID') || names.has('SAPISID') || names.has('__Secure-1PSID');
}
async function verifyYoutubeIdentity(page) {
if (!await hasGoogleSessionCookie(page)) {
throw new AuthRequiredError('www.youtube.com', 'Google session cookies missing');
}
await page.goto('https://www.youtube.com/');
await page.wait(3);
const probe = await page.evaluate(`
(() => {
const cfg = (typeof window !== 'undefined' && window.ytcfg && typeof window.ytcfg.get === 'function') ? window.ytcfg : null;
// ytcfg LOGGED_IN is the reliable signed-in signal; the avatar button is a fallback.
const loggedIn = !!(cfg && cfg.get('LOGGED_IN') === true) || !!document.querySelector('#avatar-btn');
if (!loggedIn) {
return { kind: 'auth', detail: 'YouTube ytcfg LOGGED_IN not true and no avatar — not signed in' };
}
// Name is best-effort: YouTube's masthead avatar exposes a generic
// "Account menu" aria-label, so the channel name is often unavailable
// without opening the menu. Surface it when present, else leave empty.
let name = '';
try { const ctx = cfg && cfg.get('INNERTUBE_CONTEXT'); name = (ctx && ctx.user && ctx.user.identityName) || ''; } catch {}
if (!name) {
const aria = (document.querySelector('#avatar-btn')?.getAttribute('aria-label') || '').trim();
if (aria && !/^account menu$/i.test(aria)) name = aria;
}
return { ok: true, name: String(name || '') };
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('www.youtube.com', probe.detail);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected YouTube probe: ${JSON.stringify(probe)}`);
return { name: probe.name };
}
registerSiteAuthCommands({
site: 'youtube',
domain: 'www.youtube.com',
loginUrl: 'https://accounts.google.com/ServiceLogin?service=youtube&continue=https%3A%2F%2Fwww.youtube.com%2F',
columns: ['name'],
verify: verifyYoutubeIdentity,
poll: async (page) => {
if (!await hasGoogleSessionCookie(page)) {
throw new AuthRequiredError('www.youtube.com', 'Waiting for Google session cookies');
}
return verifyYoutubeIdentity(page);
},
});
+58
View File
@@ -0,0 +1,58 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
import { hasLoginGate, ensureYuanbaoPage, YUANBAO_DOMAIN, YUANBAO_URL } from './shared.js';
async function hasYuanbaoUserCookie(page) {
const cookies = await page.getCookies({ url: 'https://yuanbao.tencent.com' });
return cookies.some(c => c.name === 'hy_user' && c.value);
}
async function verifyYuanbaoIdentity(page) {
await ensureYuanbaoPage(page);
if (await hasLoginGate(page)) {
throw new AuthRequiredError(YUANBAO_DOMAIN, 'Yuanbao showed wx login gate — anonymous session');
}
const cookies = await page.getCookies({ url: 'https://yuanbao.tencent.com' });
const hyUser = cookies.find(c => c.name === 'hy_user')?.value || '';
if (!hyUser) {
throw new AuthRequiredError(YUANBAO_DOMAIN, 'Yuanbao hy_user cookie missing — anonymous session');
}
const probe = await page.evaluate(`
(() => {
const bodyText = document.body?.innerText || '';
const nickMatch = bodyText.match(/用户[a-z0-9]{4,}/i);
const state = window.__INITIAL_STATE__ || {};
const seen = new Set();
const stack = [state];
let stateNick = '';
while (stack.length) {
const node = stack.pop();
if (!node || typeof node !== 'object' || seen.has(node)) continue;
seen.add(node);
if (Array.isArray(node)) { stack.push(...node); continue; }
const u = node.userInfo || node.user;
if (u && typeof u === 'object' && (u.nickname || u.nick)) {
stateNick = String(u.nickname || u.nick);
break;
}
for (const v of Object.values(node)) if (v && typeof v === 'object') stack.push(v);
}
return { nickname: stateNick || (nickMatch ? nickMatch[0] : '') };
})()
`);
return { user_id: String(hyUser), nickname: String(probe.nickname || '') };
}
registerSiteAuthCommands({
site: 'yuanbao',
domain: YUANBAO_DOMAIN,
loginUrl: YUANBAO_URL,
columns: ['user_id', 'nickname'],
verify: verifyYuanbaoIdentity,
poll: async (page) => {
if (!await hasYuanbaoUserCookie(page)) {
throw new AuthRequiredError(YUANBAO_DOMAIN, 'Waiting for Yuanbao hy_user cookie');
}
return verifyYuanbaoIdentity(page);
},
});
+58
View File
@@ -0,0 +1,58 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
async function hasZhihuAuthCookie(page) {
const cookies = await page.getCookies({ url: 'https://www.zhihu.com' });
return cookies.some(c => c.name === 'z_c0' && c.value);
}
async function verifyZhihuIdentity(page) {
if (!await hasZhihuAuthCookie(page)) {
throw new AuthRequiredError('www.zhihu.com', 'Zhihu z_c0 cookie missing — anonymous');
}
await page.goto('https://www.zhihu.com/');
await page.wait(2);
const data = await page.evaluate(`
(async () => {
try {
const r = await fetch('https://www.zhihu.com/api/v4/me?include=url_token', { credentials: 'include' });
if (!r.ok) return { __httpError: r.status };
return await r.json();
} catch (e) {
return { __exception: String(e && e.message || e) };
}
})()
`);
if (data?.__exception) {
throw new CommandExecutionError(`Zhihu whoami failed: ${data.__exception}`);
}
if (!data || data.__httpError) {
const status = data?.__httpError;
if (status === 401 || status === 403) {
throw new AuthRequiredError('www.zhihu.com', `Zhihu /api/v4/me returned HTTP ${status} — anonymous`);
}
throw new CommandExecutionError(`Zhihu identity probe failed (HTTP ${status ?? 'unknown'})`);
}
if (!data.url_token) {
throw new AuthRequiredError('www.zhihu.com', 'Zhihu /api/v4/me returned no url_token — anonymous session');
}
return {
url_token: String(data.url_token),
name: String(data.name ?? ''),
uid: String(data.uid ?? data.id ?? ''),
};
}
registerSiteAuthCommands({
site: 'zhihu',
domain: 'www.zhihu.com',
loginUrl: 'https://www.zhihu.com/signin',
columns: ['url_token', 'name', 'uid'],
verify: verifyZhihuIdentity,
poll: async (page) => {
if (!await hasZhihuAuthCookie(page)) {
throw new AuthRequiredError('www.zhihu.com', 'Waiting for Zhihu z_c0 cookie');
}
return verifyZhihuIdentity(page);
},
});
+68
View File
@@ -0,0 +1,68 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
// zsxq auth cookies are all httpOnly and span multiple subdomains
// (wx.zsxq.com / api.zsxq.com / .zsxq.com), so document.cookie / cookie
// probes are unreliable. Verify via a lightweight API 401 probe instead.
async function verifyZsxqIdentity(page) {
await page.goto('https://wx.zsxq.com/');
await page.wait(2);
const probe = await page.evaluate(`
(async () => {
const url = location.href;
if (/\\/login(\\b|$)/.test(url)) {
return { kind: 'auth', detail: 'zsxq wx page redirected to /login — anonymous session' };
}
try {
const r = await fetch('https://api.zsxq.com/v2/users/self', {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (r.status === 401 || r.status === 403) {
return { kind: 'auth', detail: 'zsxq /v2/users/self returned HTTP ' + r.status };
}
if (!r.ok) return { kind: 'http', httpStatus: r.status };
const d = await r.json();
if (d?.succeeded === false || !d?.resp_data?.user) {
return { kind: 'auth', detail: 'zsxq /v2/users/self returned succeeded=false — anonymous' };
}
const u = d.resp_data.user;
return { ok: true, user_id: String(u.user_id || u.id || ''), name: String(u.name || u.nickname || '') };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()
`);
if (probe?.kind === 'auth') throw new AuthRequiredError('zsxq.com', probe.detail);
if (probe?.kind === 'http') throw new CommandExecutionError(`HTTP ${probe.httpStatus} from zsxq /v2/users/self`);
if (probe?.kind === 'exception') throw new CommandExecutionError(`zsxq whoami failed: ${probe.detail}`);
if (!probe?.ok) throw new CommandExecutionError(`Unexpected zsxq probe: ${JSON.stringify(probe)}`);
if (!probe.user_id) {
throw new AuthRequiredError('zsxq.com', 'zsxq /v2/users/self 200 but user_id missing — incomplete session');
}
return { user_id: probe.user_id, name: probe.name };
}
registerSiteAuthCommands({
site: 'zsxq',
domain: 'zsxq.com',
loginUrl: 'https://wx.zsxq.com/login',
columns: ['user_id', 'name'],
verify: verifyZsxqIdentity,
// No-navigation poll: probe the API from the current page so the login-page
// QR code isn't reset by a goto on every interval.
poll: async (page) => {
const loggedIn = await page.evaluate(`(async () => {
try {
const r = await fetch('https://api.zsxq.com/v2/users/self', { credentials: 'include', headers: { Accept: 'application/json' } });
if (!r.ok) return false;
const d = await r.json();
return !!(d?.resp_data?.user);
} catch { return false; }
})()`);
if (!loggedIn) {
throw new AuthRequiredError('zsxq.com', 'Waiting for zsxq login');
}
return verifyZsxqIdentity(page);
},
});