feat(publish): add bilibili and qiehao desktop adapters
Wire bilibili and qiehao (om.qq.com) into the publish adapter registry and auth-failure classifier. Bilibili bind uses WBI-signed space-info fallback to retrieve the avatar when nav lacks a face URL, and the account list image renders with a no-referrer policy so bilibili CDN serves the avatar cross-origin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,17 @@ type BilibiliNavResponse = {
|
||||
mid?: string | number;
|
||||
uname?: string;
|
||||
face?: string;
|
||||
wbi_img?: {
|
||||
img_url?: string;
|
||||
sub_url?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type BilibiliSpaceInfoResponse = {
|
||||
code?: number;
|
||||
data?: {
|
||||
face?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -218,6 +229,17 @@ const MAX_CONCURRENT_BIND_WINDOWS = 2;
|
||||
const TOUTIAO_LOGIN_URL = "https://mp.toutiao.com/auth/page/login";
|
||||
const TOUTIAO_CONSOLE_HOME_URL = "https://mp.toutiao.com/profile_v4/index";
|
||||
const TOUTIAO_WORKBENCH_URL_PATTERN = /^https:\/\/mp\.toutiao\.com\/profile_v4\//i;
|
||||
const BILIBILI_API_ORIGIN = "https://api.bilibili.com";
|
||||
const BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32,
|
||||
15, 50, 10, 31, 58, 3, 45, 35,
|
||||
27, 43, 5, 49, 33, 9, 42, 19,
|
||||
29, 28, 14, 39, 12, 38, 41, 13,
|
||||
37, 48, 7, 16, 24, 55, 40, 61,
|
||||
26, 17, 0, 1, 60, 51, 30, 4,
|
||||
22, 25, 54, 21, 56, 59, 6, 63,
|
||||
57, 62, 11, 36, 20, 34, 44, 52,
|
||||
];
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -276,6 +298,59 @@ function hashText(value: string): string {
|
||||
return createHash("sha1").update(value).digest("hex").slice(0, 20);
|
||||
}
|
||||
|
||||
function buildBilibiliMixinKey(imgKey: string, subKey: string): string {
|
||||
const raw = imgKey + subKey;
|
||||
return BILIBILI_WBI_MIXIN_KEY_ENC_TAB
|
||||
.map((index) => raw.charAt(index))
|
||||
.filter(Boolean)
|
||||
.join("")
|
||||
.slice(0, 32);
|
||||
}
|
||||
|
||||
function bilibiliKeyFromWbiURL(value: string | undefined): string {
|
||||
const source = value?.trim() || "";
|
||||
if (!source) {
|
||||
return "";
|
||||
}
|
||||
const slashIndex = source.lastIndexOf("/");
|
||||
const dotIndex = source.lastIndexOf(".");
|
||||
if (slashIndex < 0 || dotIndex <= slashIndex) {
|
||||
return "";
|
||||
}
|
||||
return source.slice(slashIndex + 1, dotIndex);
|
||||
}
|
||||
|
||||
function buildBilibiliWbiQuery(
|
||||
nav: BilibiliNavResponse | null | undefined,
|
||||
params: Record<string, string | number>,
|
||||
): string | null {
|
||||
const imgKey = bilibiliKeyFromWbiURL(nav?.data?.wbi_img?.img_url);
|
||||
const subKey = bilibiliKeyFromWbiURL(nav?.data?.wbi_img?.sub_url);
|
||||
if (!imgKey || !subKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mixinKey = buildBilibiliMixinKey(imgKey, subKey);
|
||||
const next: Record<string, string | number> = {
|
||||
...params,
|
||||
wts: Math.round(Date.now() / 1000),
|
||||
};
|
||||
const illegalChars = /[!'()*]/g;
|
||||
const query = Object.keys(next)
|
||||
.sort()
|
||||
.map((key) => {
|
||||
const value = String(next[key]).replace(illegalChars, "");
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join("&");
|
||||
const wRid = createHash("md5").update(`${query}${mixinKey}`).digest("hex");
|
||||
return `${query}&w_rid=${wRid}`;
|
||||
}
|
||||
|
||||
function normalizeBilibiliAvatarUrl(value?: string | null): string | null {
|
||||
return normalizeRemoteUrl(value)?.replace(/^http:\/\//i, "https://") ?? null;
|
||||
}
|
||||
|
||||
function boundedIdentity(prefix: string, raw: string): string {
|
||||
const normalizedPrefix = prefix.trim().replace(/:+$/, "");
|
||||
const normalizedRaw = raw.trim();
|
||||
@@ -2242,11 +2317,56 @@ async function detectJianshu({ session }: DetectContext): Promise<DetectedAccoun
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchBilibiliSpaceAvatar(
|
||||
session: Session,
|
||||
uid: string,
|
||||
nav: BilibiliNavResponse | null,
|
||||
cookie: string,
|
||||
): Promise<string | null> {
|
||||
const query = buildBilibiliWbiQuery(nav, {
|
||||
mid: uid,
|
||||
platform: "web",
|
||||
token: "",
|
||||
web_location: 1550101,
|
||||
});
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 1800);
|
||||
|
||||
try {
|
||||
const response = await sessionFetchJson<BilibiliSpaceInfoResponse>(
|
||||
session,
|
||||
`${BILIBILI_API_ORIGIN}/x/space/wbi/acc/info?${query}`,
|
||||
{
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
accept: "application/json, text/plain, */*",
|
||||
referer: `https://space.bilibili.com/${encodeURIComponent(uid)}`,
|
||||
...(cookie ? { cookie } : {}),
|
||||
},
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
if (response?.code !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeBilibiliAvatarUrl(response.data?.face);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function detectBilibili({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||
const cookie = await sessionCookieHeader(session, "bilibili.com");
|
||||
const response = await sessionFetchJson<BilibiliNavResponse>(
|
||||
session,
|
||||
"https://api.bilibili.com/x/web-interface/nav?build=0&mobi_app=web",
|
||||
`${BILIBILI_API_ORIGIN}/x/web-interface/nav?build=0&mobi_app=web`,
|
||||
{
|
||||
headers: {
|
||||
accept: "application/json, text/plain, */*",
|
||||
@@ -2261,10 +2381,14 @@ async function detectBilibili({ session }: DetectContext): Promise<DetectedAccou
|
||||
return null;
|
||||
}
|
||||
|
||||
const avatarUrl =
|
||||
normalizeBilibiliAvatarUrl(user.face)
|
||||
?? await fetchBilibiliSpaceAvatar(session, uid, response, cookie);
|
||||
|
||||
return {
|
||||
platformUid: uid,
|
||||
displayName: user.uname,
|
||||
avatarUrl: normalizeRemoteUrl(user.face),
|
||||
avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user