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:
2026-04-27 00:57:37 +08:00
parent 34e54b9f78
commit 290da7cd50
14 changed files with 4997 additions and 17 deletions
+266 -9
View File
@@ -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) {