feat(publish): add seven publish adapters for new media platforms
Wire sohuhao, wangyihao, juejin, smzdm, weixin-gzh, zol, and dongchedi into the publish runtime: each ships its own publish protocol and auth-failure classifier (login/token/challenge codes) plus registry entries in selectPublishAdapter and the auth adapter map. Smzdm bind detection now triple-falls-back across page-context fetch, session fetch, cookie fetch, and a final cookie-derived account so the nickname-less guest profile still resolves to a stable platform UID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -205,6 +205,18 @@ type SmzdmCurrentUser = {
|
||||
nickname?: string;
|
||||
audit_nickname?: string;
|
||||
avatar?: string;
|
||||
data?: {
|
||||
smzdm_id?: string | number;
|
||||
nickname?: string;
|
||||
audit_nickname?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type SmzdmPageState = {
|
||||
displayName: string | null;
|
||||
avatarUrl: string | null;
|
||||
loggedInPage: boolean;
|
||||
};
|
||||
|
||||
type ZolUserInfoResponse = {
|
||||
@@ -294,6 +306,15 @@ function stringId(value: unknown): string {
|
||||
return normalizeText(value) ?? "";
|
||||
}
|
||||
|
||||
function decodeCookieText(value: string): string {
|
||||
const source = value.replace(/\+/g, " ");
|
||||
try {
|
||||
return decodeURIComponent(source);
|
||||
} catch {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
function hashText(value: string): string {
|
||||
return createHash("sha1").update(value).digest("hex").slice(0, 20);
|
||||
}
|
||||
@@ -2399,25 +2420,261 @@ async function detectJuejin({ session }: DetectContext): Promise<DetectedAccount
|
||||
};
|
||||
}
|
||||
|
||||
async function detectSmzdm({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||
const response = await sessionFetchJson<SmzdmCurrentUser>(
|
||||
session,
|
||||
"https://zhiyou.smzdm.com/user/info/jsonp_get_current",
|
||||
).catch(() => null);
|
||||
function normalizeSmzdmUid(value: unknown): string {
|
||||
const uid = stringId(value);
|
||||
return uid && uid !== "0" ? uid : "";
|
||||
}
|
||||
|
||||
const uid = stringId(response?.smzdm_id);
|
||||
const nickname = response?.nickname || response?.audit_nickname || "";
|
||||
if (!uid || !nickname) {
|
||||
function smzdmAccountFromCurrentUser(
|
||||
response: SmzdmCurrentUser | null | undefined,
|
||||
fallback?: {
|
||||
displayName?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
} | null,
|
||||
): DetectedAccount | null {
|
||||
const user = response?.data ?? response;
|
||||
const uid = normalizeSmzdmUid(user?.smzdm_id);
|
||||
if (!uid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nickname =
|
||||
normalizeText(user?.nickname)
|
||||
?? normalizeText(user?.audit_nickname)
|
||||
?? normalizeText(fallback?.displayName)
|
||||
?? `值友${uid}`;
|
||||
return {
|
||||
platformUid: uid,
|
||||
displayName: nickname,
|
||||
avatarUrl: normalizeRemoteUrl(response?.avatar),
|
||||
avatarUrl: normalizeRemoteUrl(user?.avatar) ?? normalizeRemoteUrl(fallback?.avatarUrl),
|
||||
};
|
||||
}
|
||||
|
||||
async function readSmzdmPageState(
|
||||
webContents: WebContents | null | undefined,
|
||||
): Promise<SmzdmPageState | null> {
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serialized = await webContents.executeJavaScript(`(() => {
|
||||
try {
|
||||
const normalize = (value) => {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim().replace(/\\s+/g, " ");
|
||||
return trimmed || null;
|
||||
};
|
||||
const compact = (value) => normalize(value)?.replace(/\\s+/g, "") || null;
|
||||
const isVisible = (node) => {
|
||||
if (!(node instanceof Element)) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
||||
return false;
|
||||
}
|
||||
const rect = node.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const ignored = /^(首页|好价|社区|百科|集团官网|商业合作|爆料投稿|个人中心|我的消息|兑换记录|账户设置|关注|粉丝|社区达人榜|查看更多|搜索|文章|笔记|视频|商品|众测|评论|收藏|品牌认领|金币|碎银子|经验|登录|注册|扫码登录|手机号登录)$/;
|
||||
const scoreName = (text, rect) => {
|
||||
let score = 0;
|
||||
if (/^值友[0-9A-Za-z_-]{4,}$/.test(text)) score += 80;
|
||||
if (rect.top >= 120 && rect.top <= 360 && rect.left >= 160 && rect.left <= 820) score += 40;
|
||||
if (rect.top >= 40 && rect.top <= 140 && rect.left >= window.innerWidth * 0.72) score += 20;
|
||||
return score;
|
||||
};
|
||||
const pickDisplayName = () => {
|
||||
const candidates = [];
|
||||
const selectors = [
|
||||
"[class*=nickname]",
|
||||
"[class*=user] [class*=name]",
|
||||
"[class*=account] [class*=name]",
|
||||
"[class*=profile] [class*=name]",
|
||||
".name",
|
||||
];
|
||||
for (const selector of selectors) {
|
||||
for (const node of document.querySelectorAll(selector)) {
|
||||
if (!isVisible(node)) continue;
|
||||
const text = compact(node.textContent || "");
|
||||
if (!text || text.length < 2 || text.length > 40 || ignored.test(text)) continue;
|
||||
const rect = node.getBoundingClientRect();
|
||||
candidates.push({ text, score: scoreName(text, rect) + 30 });
|
||||
}
|
||||
}
|
||||
for (const node of document.querySelectorAll("body *")) {
|
||||
if (!isVisible(node)) continue;
|
||||
const text = compact(node.textContent || "");
|
||||
if (!text || text.length < 2 || text.length > 40 || ignored.test(text)) continue;
|
||||
const rect = node.getBoundingClientRect();
|
||||
if (rect.top < 0 || rect.top > 420 || rect.left < 80) continue;
|
||||
const score = scoreName(text, rect);
|
||||
if (score > 0) {
|
||||
candidates.push({ text, score });
|
||||
}
|
||||
}
|
||||
candidates.sort((left, right) => right.score - left.score);
|
||||
if (candidates[0]?.text) {
|
||||
return candidates[0].text;
|
||||
}
|
||||
const bodyText = document.body?.innerText || "";
|
||||
return compact(bodyText.match(/值友[0-9A-Za-z_-]{4,}/)?.[0] || "");
|
||||
};
|
||||
const pickAvatar = () => {
|
||||
const images = Array.from(document.images)
|
||||
.map((image) => ({ src: image.currentSrc || image.src || "", rect: image.getBoundingClientRect() }))
|
||||
.filter(({ src, rect }) =>
|
||||
src
|
||||
&& !/(logo|favicon|sprite|blank)/i.test(src)
|
||||
&& rect.top >= 100
|
||||
&& rect.top <= 420
|
||||
&& rect.left >= 80
|
||||
&& rect.left <= 520
|
||||
&& rect.width >= 40
|
||||
&& rect.height >= 40
|
||||
)
|
||||
.sort((left, right) => {
|
||||
const leftArea = left.rect.width * left.rect.height;
|
||||
const rightArea = right.rect.width * right.rect.height;
|
||||
return rightArea - leftArea;
|
||||
});
|
||||
return images[0]?.src || null;
|
||||
};
|
||||
const displayName = pickDisplayName();
|
||||
const bodyText = document.body?.innerText || "";
|
||||
const path = window.location.pathname || "";
|
||||
const loggedInPage =
|
||||
/(?:^|\\.)smzdm\\.com$/i.test(window.location.hostname)
|
||||
&& !/\\/user\\/login/i.test(path)
|
||||
&& Boolean(displayName)
|
||||
&& (
|
||||
/个人中心/.test(bodyText)
|
||||
|| /账户设置/.test(bodyText)
|
||||
|| /我的消息/.test(bodyText)
|
||||
|| /^值友[0-9A-Za-z_-]{4,}$/.test(displayName)
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
displayName,
|
||||
avatarUrl: pickAvatar(),
|
||||
loggedInPage,
|
||||
});
|
||||
} catch {
|
||||
return JSON.stringify(null);
|
||||
}
|
||||
})()`, true).catch(() => null);
|
||||
|
||||
if (typeof serialized !== "string" || !serialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(serialized) as Partial<SmzdmPageState> | null;
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
displayName: normalizeText(parsed.displayName),
|
||||
avatarUrl: normalizeRemoteUrl(parsed.avatarUrl),
|
||||
loggedInPage: parsed.loggedInPage === true,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function deriveSmzdmCookieAccount(
|
||||
session: Session,
|
||||
pageState?: SmzdmPageState | null,
|
||||
): Promise<DetectedAccount | null> {
|
||||
const uid = normalizeSmzdmUid(await sessionCookieValue(session, "smzdm.com", "smzdm_id").catch(() => ""));
|
||||
const userCookie = await sessionCookieValue(session, "smzdm.com", "user").catch(() => "");
|
||||
const sessCookie = await sessionCookieValue(session, "smzdm.com", "sess").catch(() => "");
|
||||
const decodedUser = normalizeText(decodeCookieText(userCookie));
|
||||
const displayName =
|
||||
decodedUser
|
||||
?? normalizeText(pageState?.displayName)
|
||||
?? (uid ? `值友${uid}` : null);
|
||||
|
||||
if (uid && displayName) {
|
||||
return {
|
||||
platformUid: uid,
|
||||
displayName,
|
||||
avatarUrl: normalizeRemoteUrl(pageState?.avatarUrl),
|
||||
};
|
||||
}
|
||||
|
||||
const authFingerprint = normalizeText(sessCookie);
|
||||
if (authFingerprint && displayName) {
|
||||
return {
|
||||
platformUid: boundedIdentity("smzdm-sess", authFingerprint),
|
||||
displayName,
|
||||
avatarUrl: normalizeRemoteUrl(pageState?.avatarUrl),
|
||||
};
|
||||
}
|
||||
|
||||
if (pageState?.loggedInPage && pageState.displayName) {
|
||||
const cookieHeader = await sessionCookieHeader(session, "smzdm.com").catch(() => "");
|
||||
return {
|
||||
platformUid: boundedIdentity("smzdm-page", cookieHeader || pageState.displayName),
|
||||
displayName: pageState.displayName,
|
||||
avatarUrl: normalizeRemoteUrl(pageState.avatarUrl),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function detectSmzdm({ session, webContents }: DetectContext): Promise<DetectedAccount | null> {
|
||||
const currentUserUrl = "https://zhiyou.smzdm.com/user/info/jsonp_get_current";
|
||||
const pageState = await readSmzdmPageState(webContents).catch(() => null);
|
||||
const pageResponse = await pageFetchJson<SmzdmCurrentUser>(
|
||||
webContents,
|
||||
currentUserUrl,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
accept: "application/json, text/plain, */*",
|
||||
},
|
||||
},
|
||||
).catch(() => null);
|
||||
const fromPage = smzdmAccountFromCurrentUser(pageResponse, pageState);
|
||||
if (fromPage) {
|
||||
return fromPage;
|
||||
}
|
||||
|
||||
const sessionResponse = await sessionFetchJson<SmzdmCurrentUser>(
|
||||
session,
|
||||
currentUserUrl,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: {
|
||||
accept: "application/json, text/plain, */*",
|
||||
referer: "https://zhiyou.smzdm.com/user",
|
||||
origin: "https://zhiyou.smzdm.com",
|
||||
},
|
||||
},
|
||||
).catch(() => null);
|
||||
const fromSession = smzdmAccountFromCurrentUser(sessionResponse, pageState);
|
||||
if (fromSession) {
|
||||
return fromSession;
|
||||
}
|
||||
|
||||
const cookieResponse = await sessionCookieFetchJson<SmzdmCurrentUser>(
|
||||
session,
|
||||
currentUserUrl,
|
||||
{
|
||||
headers: {
|
||||
accept: "application/json, text/plain, */*",
|
||||
},
|
||||
},
|
||||
).catch(() => null);
|
||||
const fromCookieRequest = smzdmAccountFromCurrentUser(cookieResponse, pageState);
|
||||
if (fromCookieRequest) {
|
||||
return fromCookieRequest;
|
||||
}
|
||||
|
||||
return await deriveSmzdmCookieAccount(session, pageState);
|
||||
}
|
||||
|
||||
async function detectWeixinGzh({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||
const html = await sessionFetchText(session, "https://mp.weixin.qq.com/").catch(() => "");
|
||||
if (!html) {
|
||||
|
||||
Reference in New Issue
Block a user