* fix(extension): prevent Jike post content and source mismatches * fix(extension): normalize existing mention prefixes on Jike mobile
This commit is contained in:
1 parent
862132acc7
commit
3cc42871e0
6 files changed
+105
-13
No files matched your search
@@ -1,18 +1,28 @@
|
||||
import { prepareJikeWebDetail } from "./jike-web-test-state.mjs";
|
||||
import { readJikeWebStateInPage } from "./src/jike-web-state.ts";
|
||||
import { runInNewContext } from "node:vm";
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createDocument } from "@mixmark-io/domino";
|
||||
import { readArticle } from "./src/article-clip.ts";
|
||||
import { readJike, jikeExpandButton } from "./src/jike-clip.ts";
|
||||
import { readJike as readJikeSource, jikeExpandButton } from "./src/jike-clip.ts";
|
||||
import { readDouban, doubanCommentTarget } from "./src/douban-clip.ts";
|
||||
import { isPlatformClip, platformTarget, socialPlatform } from "./src/platform-clip.ts";
|
||||
import { markdownToDoc } from "../../packages/shared/src/content.ts";
|
||||
|
||||
const detailState=(doc,pageUrl,id)=>runInNewContext(`(${readJikeWebStateInPage.toString()})(id)`,{document:doc,location:{href:pageUrl},URL,id});
|
||||
const readJike=(doc,url,target=platformTarget(url),state)=>readJikeSource(doc,url,target,state ?? (url.includes('/u/') ? detailState(doc,url,target?.id) : undefined));
|
||||
|
||||
const urls = {
|
||||
wechat:"https://mp.weixin.qq.com/s/TC-ONpL3PGklsD0yhtcuBQ",sspai:"https://sspai.com/post/92653",juejin:"https://juejin.cn/post/6972051526416072717",
|
||||
jike:"https://m.okjike.com/originalPosts/66ac89b0314f5cced41012d3","jike-grid":"https://m.okjike.com/originalPosts/6715e824d9dc81724c2ec74d","jike-long":"https://m.okjike.com/originalPosts/64c9dd9a2edae6abdc610605",
|
||||
"douban-book":"https://book.douban.com/review/16093028/","douban-movie":"https://movie.douban.com/review/17798662/","douban-music":"https://music.douban.com/review/17855287/","douban-note":"https://www.douban.com/note/814698224/",
|
||||
};
|
||||
const fixture=(name)=>createDocument(readFileSync(new URL(`./fixtures/platforms/${name}.html`,import.meta.url),"utf8"));
|
||||
const fixture=(name)=>{
|
||||
const doc=createDocument(readFileSync(new URL(`./fixtures/platforms/${name}.html`,import.meta.url),"utf8"));
|
||||
if(name==='jike-web-detail'||name==='jike-web-grid')prepareJikeWebDetail(doc,name==='jike-web-grid'?'6715e824d9dc81724c2ec74d':undefined);
|
||||
return doc;
|
||||
};
|
||||
const read=(name,doc=fixture(name))=>name.startsWith("jike") ? readJike(doc,urls[name]) : name.startsWith("douban") ? readDouban(doc,platformTarget(urls[name]),urls[name]) : readArticle(doc,urls[name],"公众号");
|
||||
|
||||
test("article URLs normalize aliases and tracking, retaining only WeChat query identity/signature",()=>{
|
||||
@@ -488,3 +498,17 @@ test('Medium nested quote code and pre paragraph identity preserve language and
|
||||
expect(blocks.map(n=>n.textContent)).toEqual([code,'print(1)\nprint(2)']);expect(blocks.map(n=>n.className)).toEqual(['language-javascript','language-python']);expect(d.documentElement.outerHTML).toBe(before);
|
||||
const saved=[];const walk=n=>{if(n.type==='codeBlock')saved.push(n);for(const child of n.content||[])walk(child)};walk(markdownToDoc(platformHtmlMarkdown(r.body,'https://medium.com/@writer/story-abcdef123456')));expect(saved.map(n=>({language:n.attrs.language,text:n.content.map(c=>c.text).join('')}))).toEqual([{language:'javascript',text:code},{language:'python',text:'print(1)\nprint(2)'}]);
|
||||
});
|
||||
|
||||
for(const invalid of ['old-post','missing-props','stale-marker'])test(`Jike Web detail rejects ${invalid} identity under a same-author new route`,()=>{
|
||||
const d=fixture('jike-web-detail'),card=d.querySelector('[class*="_postCard_"]'),url=jikeWebUrl.replace('64c9dd9a2edae6abdc610605','6715e824d9dc81724c2ec74d');
|
||||
if(invalid==='missing-props')delete card.__reactProps$fixture;
|
||||
if(invalid==='stale-marker')card.setAttribute('data-edgeever-jike-id','6715e824d9dc81724c2ec74d');
|
||||
expect(detailState(d,url,'6715e824d9dc81724c2ec74d')).toBeNull();
|
||||
expect(readJike(d,url)).toEqual({ok:false,reason:'changed'});
|
||||
});
|
||||
|
||||
test('Jike Web mobile mentions already containing @ match the same public post text',()=>{
|
||||
const d=fixture('jike-web-detail');
|
||||
for(const mention of d.querySelectorAll('[class*="_mentionUser_"]'))mention.textContent='@'+mention.textContent.trim();
|
||||
const r=readJike(d,jikeWebUrl);expect(r.ok).toBe(true);expect(r.clip.markdown).toContain('凌空小鱼儿');expect(r.clip.markdown).not.toContain('@@');
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createDocument } from "@mixmark-io/domino";
|
||||
|
||||
// HTML snapshots cannot serialize React host props. Restore the same post's
|
||||
// independent public share-page data rather than deriving it from the Web DOM.
|
||||
export const prepareJikeWebDetail = (doc, id = "64c9dd9a2edae6abdc610605") => {
|
||||
const grid=id==='6715e824d9dc81724c2ec74d',name=grid?'jike-grid':'jike-long';
|
||||
const share=createDocument(readFileSync(new URL(`./fixtures/platforms/${name}.html`,import.meta.url),'utf8'));
|
||||
const post=JSON.parse(share.querySelector('#__NEXT_DATA__').textContent).props.pageProps.post;
|
||||
const card=doc.querySelector('[data-clickable-feedback][class*="_postCard_"]');
|
||||
const username=grid?'af47ed7e-38d4-4798-acd6-04e3cc8d131c':'5e6497e7-a69f-422b-af6e-baf0fc07b512';
|
||||
card.__reactProps$fixture={children:{props:{data:{...post,id,type:"ORIGINAL_POST",user:{username}}}}};
|
||||
return card;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { prepareJikeWebDetail } from "./jike-web-test-state.mjs";
|
||||
import { clipDateTimeText } from "./src/clip-time.ts";
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -787,7 +788,7 @@ test('stable rich selection preserves code blank lines and archives its selected
|
||||
|
||||
for(const navigate of [false,true])test(`current signed-in Jike Web detail ${navigate?'cancels navigation':'saves through the platform route'}`,async()=>{
|
||||
const pageUrl='https://web.okjike.com/u/5e6497e7-a69f-422b-af6e-baf0fc07b512/post/64c9dd9a2edae6abdc610605';
|
||||
const h=harness({platform:'jike',pageUrl,fixture:'jike-web-detail',navigateDuringNotebookLookup:navigate});
|
||||
const h=harness({platform:'jike',pageUrl,fixture:'jike-web-detail',prepare:doc=>prepareJikeWebDetail(doc),navigateDuringNotebookLookup:navigate});
|
||||
expect((await h.toolbar()).ok).toBe(!navigate);expect(h.calls.creates).toHaveLength(navigate?0:1);
|
||||
if(!navigate){expect(h.calls.creates[0].contentMarkdown).toContain('https://m.okjike.com/originalPosts/64c9dd9a2edae6abdc610605');expect(h.calls.creates[0].contentMarkdown).not.toContain('Do not capture comments');expect(h.calls.uploads).toHaveLength(1);}
|
||||
});
|
||||
@@ -818,3 +819,46 @@ test('Jike Web card identity recycling during notebook lookup cancels before cre
|
||||
cards[0].__reactProps$fixture.children.props.data.id='6715e824d9dc81724c2ec74d';release();
|
||||
expect(await pending).toMatchObject({kind:'error',message:'platformPageChanged'});expect(h.calls.creates).toHaveLength(0);
|
||||
});
|
||||
|
||||
for(const stage of ['before-capture','during-notebook-lookup'])test(`Jike Web detail same-author post mismatch ${stage} creates no memo`,async()=>{
|
||||
const a='64c9dd9a2edae6abdc610605',b='6715e824d9dc81724c2ec74d';
|
||||
let card,release,entered;const started=new Promise(r=>entered=r),gate=new Promise(r=>release=r);
|
||||
const h=harness({platform:'jike',fixture:'jike-web-detail',pageUrl:`https://web.okjike.com/u/5e6497e7-a69f-422b-af6e-baf0fc07b512/post/${stage==='before-capture'?b:a}`,
|
||||
prepare:doc=>{card=prepareJikeWebDetail(doc);card.setAttribute('data-edgeever-jike-id',b);},
|
||||
...(stage==='during-notebook-lookup'?{notebookRequested:entered,notebookGate:gate}:{})});
|
||||
const pending=h.toolbar();
|
||||
if(stage==='during-notebook-lookup'){await started;card.__reactProps$fixture.children.props.data.id=b;release();}
|
||||
expect(await pending).toMatchObject({ok:false,message:'platformPageChanged'});expect(h.calls.creates).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('legacy Jike Web share detail retains its H5 data identity checks',async()=>{
|
||||
const h=harness({platform:'jike',fixture:'jike',pageUrl:'https://web.okjike.com/originalPost/66ac89b0314f5cced41012d3'});
|
||||
expect((await h.toolbar()).ok).toBe(true);expect(h.calls.creates).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Jike Web detail rejects DOM from another post even when current props match the route',async()=>{
|
||||
const h=harness({platform:'jike',fixture:'jike-web-detail',pageUrl:'https://web.okjike.com/u/5e6497e7-a69f-422b-af6e-baf0fc07b512/post/64c9dd9a2edae6abdc610605',prepare:doc=>{
|
||||
const card=prepareJikeWebDetail(doc);card.querySelector('[class*="_contentWrapper_"] [class^="_content_"]').textContent='Another post still being rendered';
|
||||
}});
|
||||
expect((await h.toolbar()).ok).toBe(false);expect(h.calls.creates).toHaveLength(0);
|
||||
});
|
||||
|
||||
for(const variant of ['normal','images-only','changed-image'])test(`Jike Web gallery detail ${variant} verifies content and retains nine images`,async()=>{
|
||||
const h=harness({platform:'jike',fixture:'jike-web-grid',pageUrl:'https://web.okjike.com/u/af47ed7e-38d4-4798-acd6-04e3cc8d131c/post/6715e824d9dc81724c2ec74d',prepare:doc=>{
|
||||
const card=prepareJikeWebDetail(doc,'6715e824d9dc81724c2ec74d');
|
||||
if(variant==='images-only'){card.querySelector('[class*="_contentWrapper_"] [class^="_content_"]').remove();card.__reactProps$fixture.children.props.data.content='';}
|
||||
if(variant==='changed-image')card.querySelector('img[data-anonymous-gallery="true"]').setAttribute('src','https://cdnv2.ruguoapp.com/another-post.jpg');
|
||||
}});
|
||||
expect((await h.toolbar()).ok).toBe(variant!=='changed-image');expect(h.calls.creates).toHaveLength(variant==='changed-image'?0:1);
|
||||
if(variant!=='changed-image')expect(h.calls.uploads).toHaveLength(9);
|
||||
});
|
||||
|
||||
test('Jike Web detail expands its selected card before verifying the full public body',async()=>{
|
||||
const h=harness({platform:'jike',fixture:'jike-web-detail',pageUrl:'https://web.okjike.com/u/5e6497e7-a69f-422b-af6e-baf0fc07b512/post/64c9dd9a2edae6abdc610605',prepare:(doc,window)=>{
|
||||
const card=prepareJikeWebDetail(doc),body=card.querySelector('[class*="_contentWrapper_"]'),text=body.querySelector('[class^="_content_"]'),full=text.innerHTML;
|
||||
text.textContent='我在即刻写东西的核心原则是…';const button=doc.createElement('button');button.textContent='展开全文';body.appendChild(button);
|
||||
let notify=()=>{};window.MutationObserver=class{constructor(fn){notify=fn;}observe(){}disconnect(){}};
|
||||
button.addEventListener('click',()=>{text.innerHTML=full;button.remove();notify();});
|
||||
}});
|
||||
expect((await h.toolbar()).ok).toBe(true);expect(h.calls.creates).toHaveLength(1);expect(h.calls.creates[0].contentMarkdown).toContain('《斯坦福大学人生设计课》');
|
||||
});
|
||||
@@ -1323,6 +1323,8 @@ const describePlatformError = (error: unknown) => {
|
||||
};
|
||||
|
||||
const isJikeWebFeed = (url: string) => url.startsWith("https://web.okjike.com/") && !platformTarget(url);
|
||||
const usesJikeWebState = (url: string) => isJikeWebFeed(url)
|
||||
|| /^https:\/\/web\.okjike\.com\/u\/[^/]+\/post\/[a-f0-9]{24}\/?(?:[?#]|$)/.test(url);
|
||||
const readJikeWebState = async (tabId: number, id?: string) => (await chrome.scripting.executeScript({
|
||||
target: { tabId, frameIds: [0] }, world: "MAIN", func: readJikeWebStateInPage, args: id ? [id] : [],
|
||||
}))[0]?.result;
|
||||
@@ -1343,7 +1345,7 @@ const readPlatformFromPage = async (tabId: number, target: PlatformTarget, selec
|
||||
const xiaoyuzhouState = target.platform === "xiaoyuzhou"
|
||||
? (await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, world: "MAIN", func: readXiaoyuzhouStateInPage }))[0]?.result
|
||||
: undefined;
|
||||
const jikeState = target.platform === "jike" && isJikeWebFeed(sourceUrl) ? await readJikeWebState(tabId,target.id) : undefined;
|
||||
const jikeState = target.platform === "jike" && usesJikeWebState(sourceUrl) ? await readJikeWebState(tabId,target.id) : undefined;
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId, frameIds: [0] },
|
||||
func: (payload: unknown) => {
|
||||
@@ -1387,7 +1389,7 @@ const performPlatformSave = async (tabId: number, pageUrl: string, selectionFirs
|
||||
"---", clip.markdown,
|
||||
].filter(Boolean).join("\n\n") : toMarkdown(selection!);
|
||||
const notebookId = await notebookForClip(settings);
|
||||
if (target.platform === "jike" && isJikeWebFeed(pageUrl) && clip) {
|
||||
if (target.platform === "jike" && usesJikeWebState(pageUrl) && clip) {
|
||||
const live = await readJikeWebState(tabId,target.id);
|
||||
if (live?.pageUrl !== pageUrl || live.post.id !== target.id) throw new Error(t("platformPageChanged"));
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ const bodyInCard = (card: Element) => Array.from(card.querySelectorAll(".text,.i
|
||||
const comparisonText = (body: Element, base: string) => {
|
||||
const comparison=body.cloneNode(true) as Element;
|
||||
for (const n of Array.from(comparison.querySelectorAll('button,[role="button"]'))) n.remove();
|
||||
for (const mention of Array.from(comparison.querySelectorAll('[class*="_mentionUser_"]'))) mention.textContent = `@${platformText(mention)}`;
|
||||
for (const mention of Array.from(comparison.querySelectorAll('[class*="_mentionUser_"]'))) {
|
||||
const text=platformText(mention);
|
||||
mention.textContent = text.startsWith("@") ? text : `@${text}`;
|
||||
}
|
||||
// Share pages shorten URL labels to hostnames. Mentions remain source text.
|
||||
for (const link of Array.from(comparison.querySelectorAll("a[href]"))) {
|
||||
const href=safePlatformUrl(link.getAttribute("href") || "",base);
|
||||
@@ -103,7 +106,7 @@ export const jikePostCard = (doc: Document, target: PlatformTarget, pageUrl: str
|
||||
return Array.from(doc.querySelectorAll(JIKE_CARD_SELECTOR)).find(card=>jikeCardTarget(card,pageUrl)?.id === target.id) || null;
|
||||
};
|
||||
|
||||
/** Signed-in Web detail: bind the single visible post card to its route owner. */
|
||||
/** Read the identified Web card only when its visible body matches current public data. */
|
||||
const readWebDetail = (doc: Document, card: Element, target: PlatformTarget, post?: JikePost): PlatformRead => {
|
||||
const body = card.querySelector(WEB_BODY);
|
||||
if (!body) return {ok:false,reason:"unreadable"};
|
||||
@@ -131,7 +134,7 @@ const readWebDetail = (doc: Document, card: Element, target: PlatformTarget, pos
|
||||
if (!text && !clean.querySelector("img")) return {ok:false,reason:"unreadable"};
|
||||
if (post) {
|
||||
const content = ownText(clean);
|
||||
if (!content || typeof post.content !== "string" || normalizedText(comparisonText(content,target.url),target.url) !== normalizedText(post.content,target.url)) return {ok:false,reason:"incomplete"};
|
||||
if (typeof post.content !== "string" || normalizedText(content ? comparisonText(content,target.url) : "",target.url) !== normalizedText(post.content,target.url)) return {ok:false,reason:"incomplete"};
|
||||
const images = Array.from(clean.querySelectorAll('img[data-anonymous-gallery="true"]'));
|
||||
if (!Array.isArray(post.pictures) || images.length !== post.pictures.length) return {ok:false,reason:"incomplete"};
|
||||
for (const [index,image] of images.entries()) {
|
||||
@@ -166,12 +169,11 @@ const readWebDetail = (doc: Document, card: Element, target: PlatformTarget, pos
|
||||
export const readJike = (doc: Document, pageUrl: string, target: PlatformTarget | null = platformTarget(pageUrl), currentState?: unknown): PlatformRead => {
|
||||
if (target?.platform !== "jike") return {ok:false,reason:"not-target"};
|
||||
const card=jikePostCard(doc,target,pageUrl);
|
||||
if (!platformTarget(pageUrl) && card && isJikeWebCard(card)) {
|
||||
if (card && isJikeWebCard(card)) {
|
||||
const state=currentState as {pageUrl?: unknown;post?: JikePost} | undefined;
|
||||
if (state?.pageUrl !== pageUrl || state.post?.id !== target.id) return {ok:false,reason:"changed"};
|
||||
return readWebDetail(doc,card,target,state.post);
|
||||
}
|
||||
if (card?.matches(WEB_DETAIL_CARD)) return readWebDetail(doc,card,target);
|
||||
const body=card && bodyInCard(card);
|
||||
if (!card || !body) return {ok:false,reason:"unreadable"};
|
||||
const state=pageData(doc), detail=platformTarget(pageUrl)?.platform === "jike";
|
||||
|
||||
@@ -40,14 +40,20 @@ export function readJikeWebStateInPage(expectedId?: string) {
|
||||
return postsList.length === 1 && postsList[0].type === "ORIGINAL_POST" ? postsList[0] : null;
|
||||
};
|
||||
const matches = [];
|
||||
const selector=expectedId ? `[data-edgeever-jike-id="${expectedId}"]` : '[data-edgeever-jike-selected]';
|
||||
const url=new URL(location.href);
|
||||
const route = /^(\/u\/[^/]+)\/post\/([a-f0-9]{24})\/?$/.exec(url.pathname);
|
||||
if (route && expectedId && route[2] !== expectedId) return null;
|
||||
const selector=route ? '[data-clickable-feedback][class*="_postCard_"]' : expectedId ? `[data-edgeever-jike-id="${expectedId}"]` : '[data-edgeever-jike-selected]';
|
||||
for (const card of Array.from(document.querySelectorAll(selector))) {
|
||||
const header = card.querySelector("header"), body = card.querySelector('[class*="_contentWrapper_"]');
|
||||
if (!card.matches('[data-clickable-feedback]') || !header || !body || body.contains(header) || header.closest('[data-clickable-feedback]') !== card || body.closest('[data-clickable-feedback]') !== card || !visible(card)) continue;
|
||||
if (route && !Array.from(header.querySelectorAll('a[href]')).some(link => {
|
||||
try { return visible(link) && new URL(link.getAttribute('href') || '',url.origin).href === `${url.origin}${route[1]}`; } catch { return false; }
|
||||
})) continue;
|
||||
const post = read(card);
|
||||
if (post && (!expectedId || post.id === expectedId)) matches.push({ card, post });
|
||||
if (post && (!expectedId || post.id === expectedId) && (!route || post.id === route[2])) matches.push({ card, post });
|
||||
}
|
||||
if (matches.length !== 1) return null;
|
||||
matches[0].card.setAttribute("data-edgeever-jike-id", matches[0].post.id);
|
||||
if (!route) matches[0].card.setAttribute("data-edgeever-jike-id", matches[0].post.id);
|
||||
return { pageUrl: location.href, post: matches[0].post };
|
||||
}
|
||||
Reference in new issue
Block a user