diff --git a/apps/extension/article-clip.test.mjs b/apps/extension/article-clip.test.mjs index 71e023b0..d78fdf9b 100644 --- a/apps/extension/article-clip.test.mjs +++ b/apps/extension/article-clip.test.mjs @@ -2,7 +2,7 @@ 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 } from "./src/jike-clip.ts"; +import { readJike, 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"; @@ -319,3 +319,172 @@ test('article metadata retains a visible relative publication value when no mach const {mediumContent}=await import('./src/medium-content.ts'),d=fixture('medium-article');d.querySelector('script').remove();d.querySelector('[data-testid="storyPublishDate"]').textContent='2 days ago'; expect(mediumContent(d,'https://scottbatson.dev/what-should-programmers-do-while-the-ai-writes-code-a2ce2e6de88e').publishedAt).toEqual({value:'2 days ago',precision:'relative'}); }); + +const jikeWebUrl='https://web.okjike.com/u/5e6497e7-a69f-422b-af6e-baf0fc07b512/post/64c9dd9a2edae6abdc610605'; +test('signed-in Jike Web detail keeps its full body, mention text and preview cover without avatars/comments',()=>{ + const d=fixture('jike-web-detail');const r=readJike(d,jikeWebUrl); + expect(platformTarget(jikeWebUrl)).toEqual(platformTarget(urls['jike-long'])); + expect(r.ok).toBe(true);expect(r.clip.author).toBe('泛函');expect(r.clip.publishedAt).toEqual({value:'2023-08-02',precision:'date'}); + expect(r.clip.markdown).toContain('《世界尽头的咖啡馆》和《斯坦福大学人生设计课》'); + expect(r.clip.markdown).toContain('凌空小鱼儿');expect(r.clip.markdown).not.toContain('Do not capture comments'); + expect(r.clip.markdown.match(/!\[/g)).toHaveLength(1);expect(r.clip.markdown).not.toContain('!120x120'); + expect(r.clip.markdown).toContain('https://m.okjike.com/originalPosts/61ed0f92c7f0e70011d194c0'); + expect(r.clip.markdown).not.toContain('?s='); +}); +for(const invalid of ['wrong-author','ambiguous-card','hidden-card','missing-body','wrong-post'])test(`signed-in Jike Web refuses ${invalid}`,()=>{ + const d=fixture('jike-web-detail'),card=d.querySelector('[class*="_postCard_"]'); + if(invalid==='wrong-author')for(const a of card.querySelectorAll('header a'))a.setAttribute('href','/u/other-user'); + if(invalid==='ambiguous-card')card.parentElement.appendChild(card.cloneNode(true)); + if(invalid==='hidden-card')card.setAttribute('hidden',''); + if(invalid==='missing-body')card.querySelector('[class*="_contentWrapper_"]').remove(); + const target=invalid==='wrong-post'?platformTarget(urls['jike-grid']):platformTarget(jikeWebUrl); + expect(readJike(d,jikeWebUrl,target)).toEqual({ok:false,reason:'unreadable'}); +}); + +test('signed-in Jike Web gallery keeps all nine viewer-original images in order',()=>{ + const d=fixture('jike-web-grid'),url='https://web.okjike.com/u/af47ed7e-38d4-4798-acd6-04e3cc8d131c/post/6715e824d9dc81724c2ec74d'; + const expected=[...d.querySelectorAll('img[data-mode="grid-item"]')].map(i=>i.getAttribute('src').split('?')[0]); + const r=readJike(d,url);expect(r.ok).toBe(true);expect(r.clip.author).toBe('彗星咻咻咻');expect(r.clip.publishedAt).toEqual({value:'2024-10-21',precision:'date'}); + expect(r.clip.markdown).not.toContain('Neighbor post');expect([...r.clip.markdown.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)].map(m=>m[1])).toEqual(expected); +}); + +test('Jike Web preview headers cannot impersonate the route author',()=>{ + const d=fixture('jike-web-detail'),card=d.querySelector('[class*="_postCard_"]'); + card.querySelector('header a').setAttribute('href','/u/other-user'); + const nested=d.createElement('header');nested.innerHTML='泛函'; + card.querySelector('[class*="_contentWrapper_"]').appendChild(nested); + expect(readJike(d,jikeWebUrl)).toEqual({ok:false,reason:'unreadable'}); +}); + +for(const hidden of ['hidden','aria-hidden','display']) { + const hide=node=>hidden==='display'?node.setAttribute('style','display:none'):node.setAttribute(hidden,hidden==='aria-hidden'?'true':''); + test(`Jike Web ignores ${hidden} identity links`,()=>{ + const d=fixture('jike-web-detail'),link=d.querySelector('header a'); + const visible=link.cloneNode(true);visible.setAttribute('href','/u/other-user'); + link.parentElement.appendChild(visible);hide(link); + expect(readJike(d,jikeWebUrl)).toEqual({ok:false,reason:'unreadable'}); + }); + test(`Jike Web omits ${hidden} metadata and expansion controls`,()=>{ + const d=fixture('jike-web-detail'),header=d.querySelector('header'),link=header.querySelector('a'); + const stale=link.cloneNode(true);stale.textContent='Hidden author';hide(stale);link.parentElement.insertBefore(stale,link); + hide(header.querySelector('[class*="_descContainer_"]')); + const button=d.createElement('button');button.textContent='展开全文';hide(button); + d.querySelector('[class*="_contentWrapper_"]').appendChild(button); + expect(jikeExpandButton(d,d.querySelector('[class*="_postCard_"]'))).toBeNull(); + const r=readJike(d,jikeWebUrl);expect(r.ok).toBe(true); + expect(r.clip.author).toBe('泛函');expect(r.clip.publishedAt).toBeUndefined(); + expect(r.clip.markdown).toContain('《斯坦福大学人生设计课》'); + }); +} + +const jikeWebFeedUrl='https://web.okjike.com/following'; +const jikeWebPosts=JSON.parse(readFileSync(new URL('./fixtures/platforms/jike-web-feed.posts.json',import.meta.url),'utf8')); +const webFeed=async(name="jike-web-feed",pageUrl=jikeWebFeedUrl)=>{ + const {readJikeWebStateInPage}=await import('./src/jike-web-state.ts'); + const {runInNewContext}=await import('node:vm');const d=fixture(name),posts=JSON.parse(readFileSync(new URL(`./fixtures/platforms/${name}.posts.json`,import.meta.url),'utf8')); + const cards=[...d.querySelectorAll('[data-clickable-feedback]')].filter(c=>c.querySelector('header')&&c.querySelector('[class*="_contentWrapper_"]')); + cards.forEach((card,i)=>card.__reactProps$fixture={children:{props:{data:structuredClone(posts[i])}}}); + const state=id=>runInNewContext(`(${readJikeWebStateInPage.toString()})(id)`,{document:d,location:{href:pageUrl},URL,id}); + return{d,cards,state}; +}; + +test('Jike Web feed reads the selected current host props without Fiber/alternate or body permalinks',async()=>{ + const {d,cards,state}=await webFeed();cards[0].setAttribute('data-edgeever-jike-selected',''); + cards[0].__reactFiber$fixture={memoizedProps:{data:jikeWebPosts[1]},alternate:{memoizedProps:{data:jikeWebPosts[1]}}}; + const s=state();expect(s.post.id).toBe(jikeWebPosts[0].id);expect(Object.keys(s.post.user)).toEqual(['username']); + expect(Object.keys(s.post)).not.toContain('readTrackInfo');expect(s.post.id).not.toBe('6ad1c239141b85b29238482b'); + const target=platformTarget('https://m.okjike.com/originalPosts/'+s.post.id); + expect(readJike(d,jikeWebFeedUrl,target,s)).toEqual({ok:false,reason:'incomplete'}); + cards[0].querySelector('[class*="_expandButton_"]').remove(); + const r=readJike(d,jikeWebFeedUrl,target,s);expect(r.ok).toBe(true);expect(r.clip.author).toBe('即刻资讯台'); + expect(r.clip.markdown).toContain('感谢以上即友的创作与分享');expect(r.clip.markdown).not.toContain('2026年10月16日'); + expect(r.clip.markdown).not.toContain('收起');expect(r.clip.markdown).not.toContain('查看更多'); + const parsed=markdownToDoc(r.clip.markdown),links=[];let text=''; + const visit=n=>{if(n.type==='text'){text+=n.text;if(n.marks?.some(m=>m.type==='link'))links.push(n.text)}for(const c of n.content||[])visit(c)};visit(parsed); + expect(links).toContain('牛村木木山');expect(links).toContain('一觉醒来发生了什么');expect(text).not.toContain('](https://'); +}); + +for(const failure of ['missing-props','ambiguous-post','unsupported-type','hidden-card','recycled-id','changed-author','changed-body'])test(`Jike Web feed ${failure} refuses stale or unbound content`,async()=>{ + const{d,cards,state}=await webFeed(),id=jikeWebPosts[0].id,target=platformTarget('https://m.okjike.com/originalPosts/'+id);cards[0].setAttribute('data-edgeever-jike-selected','');expect(state().post.id).toBe(id); + cards[0].querySelector('[class*="_expandButton_"]').remove(); + if(failure==='missing-props')delete cards[0].__reactProps$fixture; + if(failure==='ambiguous-post')cards[0].__reactProps$fixture.children=[{props:{data:jikeWebPosts[0]}},{props:{data:jikeWebPosts[1]}}]; + if(failure==='unsupported-type')cards[0].__reactProps$fixture.children.props.data.type='REPOST'; + if(failure==='hidden-card')cards[0].setAttribute('hidden',''); + if(failure==='recycled-id')cards[0].__reactProps$fixture.children.props.data.id='6715e824d9dc81724c2ec74d'; + if(failure==='changed-author')cards[0].querySelector('header a[href]').setAttribute('href','/u/other'); + if(failure==='changed-body')cards[0].querySelector('[class^="_content_93yof_"]').textContent='A different post'; + const s=state(id); + if(['changed-author','changed-body'].includes(failure))expect(readJike(d,jikeWebFeedUrl,target,s).ok).toBe(false); + else expect(s).toBeNull(); +}); + +test('Jike Web feed current props track a recycled card even when its DOM marker and content remain unchanged',async()=>{ + const{cards,state}=await webFeed(),id=jikeWebPosts[0].id;cards[0].setAttribute('data-edgeever-jike-selected','');expect(state().post.id).toBe(id); + cards[0].__reactProps$fixture.children.props.data={...jikeWebPosts[0],id:'6715e824d9dc81724c2ec74d'}; + expect(cards[0].getAttribute('data-edgeever-jike-id')).toBe(id);expect(state(id)).toBeNull(); + expect(state().post.id).toBe('6715e824d9dc81724c2ec74d'); +}); + +test('the Jike Web pointer listener selects one own card without guessing identity from body links',async()=>{ + const{createWindow}=await import('@mixmark-io/domino'),{runInNewContext}=await import('node:vm'),{bundleTestScripts}=await import('./bundle-test-scripts.mjs'); + const w=createWindow(readFileSync(new URL('./fixtures/platforms/jike-web-feed.html',import.meta.url),'utf8'),jikeWebFeedUrl); + runInNewContext(bundleTestScripts(['social-target'])[0],{document:w.document,location:w.location,URL,Element:w.Element,Node:w.Node}); + const cards=[...w.document.querySelectorAll('[data-clickable-feedback]')].filter(c=>c.querySelector('header')&&c.querySelector('[class*="_contentWrapper_"]'));const event=w.document.createEvent('Event');event.initEvent('contextmenu',true,true); + cards[0].querySelector('[class*="_contentWrapper_"]').dispatchEvent(event);expect(cards[0].hasAttribute('data-edgeever-jike-selected')).toBe(true);expect(w.document.querySelectorAll('[data-edgeever-social-target]')).toHaveLength(0); + cards[1].querySelector('[class*="_contentWrapper_"]').dispatchEvent(event);expect(cards[0].hasAttribute('data-edgeever-jike-selected')).toBe(false);expect(cards[1].hasAttribute('data-edgeever-jike-selected')).toBe(true); +}); + +for(const mismatch of [false,true])test(`Jike Web single-image feed verifies gallery identity before using the declared original (mismatch=${mismatch})`,async()=>{ + const url='https://web.okjike.com/topic/63579abb6724cc583b9bba9a/square',{d,cards,state}=await webFeed('jike-web-feed-image',url);cards[0].setAttribute('data-edgeever-jike-selected','');const s=state(),target=platformTarget('https://m.okjike.com/originalPosts/'+s.post.id); + if(mismatch)cards[0].querySelector('img[data-anonymous-gallery="true"]').setAttribute('src','https://cdnv2.ruguoapp.com/other.png?imageMogr2/thumbnail/400x'); + const r=readJike(d,url,target,s); + if(mismatch)expect(r).toEqual({ok:false,reason:'incomplete'}); + else{expect(r.ok).toBe(true);expect(r.clip.markdown).toContain(s.post.pictures[0].picUrl);expect(r.clip.markdown.match(/!\[/g)).toHaveLength(1);expect(r.clip.markdown).toContain('开启一个新的时代');} +}); + +test('Medium in-story preview keeps its label, destination and visible CSS cover in document order',async()=>{ + const {mediumContent}=await import('./src/medium-content.ts'),{platformHtmlMarkdown}=await import('./src/platform-clip.ts'); + const d=createDocument('

Story

Before preview.

Writer profile

Description of writing

example.test

Hidden card

After preview.

Outside recommendation

'); + const before=d.documentElement.outerHTML,r=mediumContent(d,'https://medium.com/@writer/story-abcdef123456'),md=platformHtmlMarkdown(r.body,'https://medium.com/@writer/story-abcdef123456'),doc=markdownToDoc(md),nodes=[];const walk=n=>{nodes.push(n);(n.content||[]).forEach(walk)};walk(doc); + const text=nodes.filter(n=>n.type==='text').map(n=>n.text).join('');expect(text).toContain('Before preview.Writer profile Description of writing example.testAfter preview.');expect(text).not.toContain('Hidden card');expect(text).not.toContain('Outside recommendation');expect(nodes.some(n=>n.marks?.some(m=>m.type==='link'&&m.attrs.href==='https://example.test/writer'))).toBe(true);expect(nodes.filter(n=>n.type==='image').map(n=>n.attrs.src)).toEqual(['https://example.test/cover.png']);expect(d.documentElement.outerHTML).toBe(before); +}); + +for (const wrapping of ['image-role','container-role','container-button']) test(`Jike Web keeps gallery media with ${wrapping} semantics`,()=>{ + const d=fixture('jike-web-grid'),url='https://web.okjike.com/u/af47ed7e-38d4-4798-acd6-04e3cc8d131c/post/6715e824d9dc81724c2ec74d'; + const images=[...d.querySelectorAll('img[data-mode="grid-item"]')],expected=images.map(i=>i.getAttribute('src').split('?')[0]); + if(wrapping==='image-role')images[0].setAttribute('role','button'); + else {const wrapper=d.createElement(wrapping==='container-button'?'button':'div');wrapper.setAttribute('role','button');images[0].replaceWith(wrapper);wrapper.appendChild(images[0]);} + const before=d.body.innerHTML,r=readJike(d,url);expect(r.ok).toBe(true);expect([...r.clip.markdown.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)].map(m=>m[1])).toEqual(expected);expect(d.body.innerHTML).toBe(before); +}); + +test('Medium keeps explicitly visible descendants of a hidden story block and additional body h1',async()=>{ + const {mediumContent}=await import('./src/medium-content.ts'); + const d=createDocument('

Story title

Hidden prefixVisible body

Body heading

Hidden whole block

'); + const before=d.body.innerHTML,r=mediumContent(d,'https://medium.com/@writer/story-abcdef123456');expect(r?.body.textContent).toContain('Visible body');expect(r.body.textContent).toContain('Body heading');expect(r.body.textContent).not.toContain('Hidden prefix');expect(r.body.textContent).not.toContain('Hidden whole block');expect(r.body.textContent).not.toContain('Story title');expect(d.body.innerHTML).toBe(before); +}); + +test('Jike Web retains visible body text and media restored inside a hidden wrapper',()=>{ + const d=fixture('jike-web-detail'),body=d.querySelector('[class*="_contentWrapper_"]');body.style.visibility='hidden'; + body.querySelector('[class^="_content_"]').style.visibility='visible';for(const img of Array.from(body.querySelectorAll('img')))img.style.visibility='visible'; + const before=d.body.innerHTML,r=readJike(d,jikeWebUrl);expect(r.ok).toBe(true);expect(r.clip.markdown).toContain('《斯坦福大学人生设计课》');expect(r.clip.markdown.match(/!\[/g)).toHaveLength(1);expect(d.body.innerHTML).toBe(before); +}); + +test('Medium live-style code keeps br newlines and matching declared syntax language without reading hidden text',async()=>{ + const {mediumContent}=await import('./src/medium-content.ts'),{platformHtmlMarkdown}=await import('./src/platform-clip.ts'); + const state={a:{__typename:'Paragraph',type:'PRE',name:'code1',text:'const x = 1;\n\nconsole.log(x);',codeBlockMetadata:{lang:'javascript'}},b:{__typename:'Paragraph',type:'PRE',name:'code2',text:'Hidden replacement',codeBlockMetadata:{lang:'python'}}}; + const d=createDocument('

Story

const x = 1;

console.log(x);
Visible code
Hidden code
'); + const script=d.createElement('script');script.textContent='window.__APOLLO_STATE__ = '+JSON.stringify(state)+';';d.body.appendChild(script); + const before=d.documentElement.outerHTML,md=platformHtmlMarkdown(mediumContent(d,'https://medium.com/@writer/story-abcdef123456').body,'https://medium.com/@writer/story-abcdef123456'); + expect(md).toContain('```javascript\nconst x = 1;\n\nconsole.log(x);\n```');expect(md).toContain('```\nVisible code\n```');expect(md).not.toContain('python');expect(md).not.toContain('Hidden');expect(d.documentElement.outerHTML).toBe(before); +}); + +test('Medium nested quote code and pre paragraph identity preserve language and line breaks',async()=>{ + const {mediumContent}=await import('./src/medium-content.ts'),{platformHtmlMarkdown}=await import('./src/platform-clip.ts'); + const code='const x = 1;\n\nconsole.log(x);',state={a:{__typename:'Paragraph',type:'PRE',name:'nested',text:code,codeBlockMetadata:{lang:'javascript'}},b:{__typename:'Paragraph',type:'PRE',name:'root',text:'print(1)\nprint(2)',codeBlockMetadata:{lang:'python'}}}; + const d=createDocument('

Story

Example:

const x = 1;

console.log(x);
print(1)
print(2)
'); + const script=d.createElement('script');script.textContent='window.__APOLLO_STATE__ = '+JSON.stringify(state)+';';d.body.appendChild(script); + const before=d.documentElement.outerHTML,r=mediumContent(d,'https://medium.com/@writer/story-abcdef123456'),blocks=Array.from(r.body.querySelectorAll('pre')); + 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)'}]); +}); diff --git a/apps/extension/fixtures/platforms/bluesky-feed-preview.html b/apps/extension/fixtures/platforms/bluesky-feed-preview.html new file mode 100644 index 00000000..cf7996a7 --- /dev/null +++ b/apps/extension/fixtures/platforms/bluesky-feed-preview.html @@ -0,0 +1,3 @@ +
“Pro wrestling conversations, like sports on Bluesky across the board, rarely engagement-farm—posts are celebratory and much more real. The lesson is to invest in product features that serve people who care more about the community than they care about going viral.” —
NPW (The News of Pro Wrestling)
 @newsofprowrestling.bsky.social
· 1 天前
"The experience of a wrestling fan is very similar to other sports fans. You want a place to go when the big moments are happening, and an evergreen community to shoot the shit with during the quieter days.” + +@stholeary.bsky.social interviewed Bluesky's Harry Packer about pro wrestling.
diff --git a/apps/extension/fixtures/platforms/jike-web-detail.html b/apps/extension/fixtures/platforms/jike-web-detail.html new file mode 100644 index 00000000..fcb8cb73 --- /dev/null +++ b/apps/extension/fixtures/platforms/jike-web-detail.html @@ -0,0 +1,73 @@ +
2023/08/02
我在即刻写东西的核心原则是,怕什么真理无穷,进一步有一步的欢喜。 + +当我每次取得一点点微小的进步,我都会快速地将自己的方法、启发和心得分享出来。 + +这才是即刻涨粉的正道,既有利于你的成长,又有利于平台的发展,互利共赢。 + +希望能激励到更多人分享自己的所思所想,我整理了一下自己曾写过的所有关于职业规划和个人成长的帖子,给伙计们做个示范😉 + +探索出了一个超级高效的小红书内容生产方法:m.okjike.com + +热爱的增长飞轮:m.okjike.com + +最简单的找到你的优势的方法:m.okjike.com + +寻找自身优势的两条路径:探险家模式和科学家模式:m.okjike.com + +和
凌空小鱼儿
一起录的一期播客--《用少年心气抵御职业迷茫》:m.okjike.com + +“所有教你赚钱的人,都想赚你的钱。”我发现大伙儿对这句话有极大的误解 :m.okjike.com + +那些学历背景并不是很出色的人,是怎么取得职业成功的? +m.okjike.com + +我是如何在即刻找了四份工作的?用社交媒体,构建你的职场品牌:m.okjike.com + +社交媒体与职业机会蓝海,类比搜索电商和兴趣电商:m.okjike.com + +既然大学与职场脱节严重,那为什么不把大学当成一个为期四年的兴趣班呢:m.okjike.com + +在每份工作开始之前,先想明白自己的核心课题:m.okjike.com + +如何找到「非共识职业机会」:m.okjike.com + +适合于任何聊天的四个要素:人生节点、风口浪尖、江湖故人、心路历程:m.okjike.com + +系统的知识管理方法---PARA 体系:m.okjike.com + +反向构建你的信息茧房:m.okjike.com + +用了多年的极简时间管理方法:m.okjike.com + +在学习新技能时,先想明白你最后想做成什么事情:m.okjike.com + +我离毕业还有一年,实习时长已超过了一年:m.okjike.com + +成功化解了一次职场危机,复盘一下:m.okjike.com + +对齐目标,与强者互赖:m.okjike.com + +怎么区分老板是在 PUA 你,还是在严格管理:m.okjike.com + +我目前最高赞的一条即刻动态:《职业生涯冷启动 SOP》:m.okjike.com + +浅谈产品经理和产品营销的区别:m.okjike.com + +我在大二那年,删掉了所有的课程,去字节实习:m.okjike.com + +我是团队中学历最低的那个,所以我真的好开心:m.okjike.com + +在大厂是不是会沦为螺丝钉,只能干一些简单且无聊的杂活? +m.okjike.com + +分享一个提升工作幸福感的小技巧!建一个自己的夸夸群:m.okjike.com + +大学四年,疫情三年,该如何破局? +m.okjike.com + +打造你的简历中台:通过写简历的方式,来复盘工作。 +m.okjike.com + +适用于各种活动运营的沟通原则:m.okjike.com + +《世界尽头的咖啡馆》和《斯坦福大学人生设计课》:m.okjike.com
泛函 - Yellow Page
职场那些事儿
Do not capture comments
\ No newline at end of file diff --git a/apps/extension/fixtures/platforms/jike-web-feed-image.html b/apps/extension/fixtures/platforms/jike-web-feed-image.html new file mode 100644 index 00000000..f093b7d8 --- /dev/null +++ b/apps/extension/fixtures/platforms/jike-web-feed-image.html @@ -0,0 +1,2 @@ +
4分钟前
入坑了 +开启一个新的时代
图片
\ No newline at end of file diff --git a/apps/extension/fixtures/platforms/jike-web-feed-image.posts.json b/apps/extension/fixtures/platforms/jike-web-feed-image.posts.json new file mode 100644 index 00000000..87496042 --- /dev/null +++ b/apps/extension/fixtures/platforms/jike-web-feed-image.posts.json @@ -0,0 +1,18 @@ +[ + { + "id": "6ad399f0141b85b29268b593", + "type": "ORIGINAL_POST", + "content": "入坑了\n开启一个新的时代", + "createdAt": "2026-10-17T15:53:20.576Z", + "user": { + "username": "E3CBA58B-D94D-4358-8196-4E3C3D282177" + }, + "pictures": [ + { + "picUrl": "https://cdnv2.ruguoapp.com/FsLDJJ-q_6F7OlCbyikCDfLwOfWxv3.png?imageMogr2/meta-keep-list/ZXhpZixVc2VyQ29tbWVudA==/auto-orient", + "middlePicUrl": "https://cdnv2.ruguoapp.com/FsLDJJ-q_6F7OlCbyikCDfLwOfWxv3.png?imageMogr2/auto-orient/thumbnail/1500x2000%3E", + "thumbnailUrl": "https://cdnv2.ruguoapp.com/FsLDJJ-q_6F7OlCbyikCDfLwOfWxv3.png?imageMogr2/auto-orient/thumbnail/300x2000%3E" + } + ] + } +] diff --git a/apps/extension/fixtures/platforms/jike-web-feed.html b/apps/extension/fixtures/platforms/jike-web-feed.html new file mode 100644 index 00000000..688e1824 --- /dev/null +++ b/apps/extension/fixtures/platforms/jike-web-feed.html @@ -0,0 +1,45 @@ +
16小时前
2026年10月17日 +🌍资讯快读 +1、甘肃天水幼儿园铅中毒案一审宣判 +www.jiemian.com +2、四川雷波县教师被安排“耻辱”合影事件调查结果公布 +www.jiemian.com +3、特朗普称美国可能会对伊朗镐山核设施发动打击 +www.jiemian.com +4、泽连斯基要求欧盟明年额外提供700亿欧元援助 +www.jiemian.com + +👬即刻镇小报 +1、墨西哥职场入门必知血泪版(持续更新版) +m.okjike.com +2、我建议每个人都应该从现在开始搭建自己的【私人知识库】 +m.okjike.com +3、在北京一人探店捡到宝了 +m.okjike.com +4、我真切感受到人是在流动中变化的,很多想法时隔几个月回看,就大不相同。 +m.okjike.com + +今日即刻镇小报内容来自
牛村木木山
超级峰
栗噔噔
生姜iris
,感谢以上即友的创作与分享。
查看更多
一觉醒来发生了什么
2026年10月16日 +🌍资讯快读 +1、四部门公布婴幼儿纸尿裤甲酰胺问题调查结果 +www.jiemian.com +2、蒙古国草原火灾已被成功堵截于中蒙边境一线 +www.thepaper.cn +3、搜出来的“400”官方客服电话,十个号码六个假 +www.thepaper.cn +4、乌克兰指责美国帮俄罗斯:星链突然被限速、没收到导弹预警 +www.thepaper.cn + +👬即刻镇小报 +1、坚持本心,相信美好,克制欲望,感受生活 +m.okjike.com +2、我觉得大家都过于关注「时间」了。 +m.okjike.com +3、家庭传递的不只有钱,还有语言习惯、判断标准和处理问题的方式。 +m.okjike.com +4、研究课题本身的乐趣就是最大的回报 +m.okjike.com +5、投资,是一种可以只靠脑子、不靠关系、不靠出身,就能生存下去的方式。 +m.okjike.com + +今日即刻镇小报内容来自
TK观察磊哥
携隐Melody
老黄牛
阑夕ོ
王紫君Zima
,感谢以上即友的创作与分享。
查看更多
一觉醒来发生了什么
\ No newline at end of file diff --git a/apps/extension/fixtures/platforms/jike-web-feed.posts.json b/apps/extension/fixtures/platforms/jike-web-feed.posts.json new file mode 100644 index 00000000..06245a6c --- /dev/null +++ b/apps/extension/fixtures/platforms/jike-web-feed.posts.json @@ -0,0 +1,22 @@ +[ + { + "id": "6ad2adab709f50754cbd2921", + "type": "ORIGINAL_POST", + "content": "2026年10月17日\n🌍资讯快读\n1、甘肃天水幼儿园铅中毒案一审宣判\nhttps://www.jiemian.com/article/15201079.html\n2、四川雷波县教师被安排“耻辱”合影事件调查结果公布\nhttps://www.jiemian.com/article/15197196.html\n3、特朗普称美国可能会对伊朗镐山核设施发动打击\nhttps://www.jiemian.com/article/15201742.html\n4、泽连斯基要求欧盟明年额外提供700亿欧元援助\nhttps://www.jiemian.com/article/15201712.html\n\n👬即刻镇小报\n1、墨西哥职场入门必知血泪版(持续更新版)\nhttps://m.okjike.com/originalPosts/6ad1c239141b85b29238482b\n2、我建议每个人都应该从现在开始搭建自己的【私人知识库】\nhttps://m.okjike.com/originalPosts/6ad0e49cbd0563695b9ac0d9\n3、在北京一人探店捡到宝了\nhttps://m.okjike.com/originalPosts/6ad0d4b2756bbb6658591d0f\n4、我真切感受到人是在流动中变化的,很多想法时隔几个月回看,就大不相同。\nhttps://m.okjike.com/originalPosts/6ad0bcb52d26e50fb461b182\n\n今日即刻镇小报内容来自 @牛村木木山 @超级峰 @栗噔噔 @生姜iris ,感谢以上即友的创作与分享。", + "createdAt": "2026-10-16T23:05:15.836Z", + "user": { + "username": "wenhao1996" + }, + "pictures": [] + }, + { + "id": "6ad15c2977477d8a9f97733b", + "type": "ORIGINAL_POST", + "content": "2026年10月16日\n🌍资讯快读\n1、四部门公布婴幼儿纸尿裤甲酰胺问题调查结果\nhttps://www.jiemian.com/article/15194480.html\n2、蒙古国草原火灾已被成功堵截于中蒙边境一线\nhttps://www.thepaper.cn/newsDetail_forward_34255567\n3、搜出来的“400”官方客服电话,十个号码六个假\nhttps://www.thepaper.cn/newsDetail_forward_34241263\n4、乌克兰指责美国帮俄罗斯:星链突然被限速、没收到导弹预警\nhttps://www.thepaper.cn/newsDetail_forward_34254228\n\n👬即刻镇小报\n1、坚持本心,相信美好,克制欲望,感受生活\nhttps://m.okjike.com/originalPosts/6acf2a8c141b85b292f1b385\n2、我觉得大家都过于关注「时间」了。\nhttps://m.okjike.com/originalPosts/6acf04ae8c58ada77d66b1dc\n3、家庭传递的不只有钱,还有语言习惯、判断标准和处理问题的方式。\nhttps://m.okjike.com/originalPosts/6ad03d11141b85b2920e4917\n4、研究课题本身的乐趣就是最大的回报\nhttps://m.okjike.com/originalPosts/6acdfd29cfb5d08b3e9d263b\n5、投资,是一种可以只靠脑子、不靠关系、不靠出身,就能生存下去的方式。\nhttps://m.okjike.com/originalPosts/6acbfe14bd0563695b153ddd\n\n今日即刻镇小报内容来自 @TK观察磊哥 @携隐Melody @老黄牛 @阑夕ོ @王紫君Zima ,感谢以上即友的创作与分享。", + "createdAt": "2026-10-15T23:05:13.490Z", + "user": { + "username": "wenhao1996" + }, + "pictures": [] + } +] diff --git a/apps/extension/fixtures/platforms/jike-web-grid.html b/apps/extension/fixtures/platforms/jike-web-grid.html new file mode 100644 index 00000000..df9849c7 --- /dev/null +++ b/apps/extension/fixtures/platforms/jike-web-grid.html @@ -0,0 +1 @@ +
2024/10/21
又凑了九张赤坎游客照📷
图片
图片
图片
图片
图片
图片
图片
图片
图片
一起拍写真
\ No newline at end of file diff --git a/apps/extension/fixtures/platforms/sources.json b/apps/extension/fixtures/platforms/sources.json index e7729437..24e6fed2 100644 --- a/apps/extension/fixtures/platforms/sources.json +++ b/apps/extension/fixtures/platforms/sources.json @@ -256,10 +256,43 @@ "capturedOn": "2026-10-15", "state": "Same-origin public detail HTML for the second actual Ego feed card, sanitized as above.", "expected": "Keep full text through 把自己的真金白银押在别人的嘴上,早晚是要付出代价的。, one body image and explicitly linked 09992/NTES. Remove the source-app footer; never save the feed preview as complete." + }, + { + "file": "jike-web-detail.html", + "url": "https://web.okjike.com/u/5e6497e7-a69f-422b-af6e-baf0fc07b512/post/64c9dd9a2edae6abdc610605", + "state": "Captured 2026-10-17 after QR sign-in. Current Web detail layout; only the public post card retained, plus a synthetic comment exclusion marker. No account sidebar or login data. Detail already displays the full long post.", + "expected": "Keep the complete long post through the final book links, author 泛函, date precision 2023-08-02, inline mention text and one preview cover. Exclude mention/avatar images and comments. Bind the sole visible detail card to the route author; reject ambiguous or mismatched cards." + }, + { + "file": "jike-web-grid.html", + "url": "https://web.okjike.com/u/af47ed7e-38d4-4798-acd6-04e3cc8d131c/post/6715e824d9dc81724c2ec74d", + "state": "Captured 2026-10-17 signed in. Only public post card retained. Gallery viewer independently confirmed original CDN paths without thumbnail transforms. Synthetic neighbor exclusion marker.", + "expected": "Keep the caption, author 彗星咻咻咻, date 2024-10-21 and all nine gallery originals in order. No avatar, comments, or adjacent post." + }, + { + "file": "jike-web-feed.html", + "url": "https://web.okjike.com/following", + "capturedOn": "2026-10-17", + "state": "Signed-in current Jike Web feed; two public original posts by 即刻资讯台. Cards have no own DOM permalink. Sanitized companion jike-web-feed.posts.json contains only public id/type/content/date/creator username/pictures from each own host DOM current __reactProps$ children, not Fiber/alternate or global account state.", + "expected": "Bind 6ad2adab709f50754cbd2921 independently of referenced post links and neighbor 6ad15c2977477d8a9f97733b. Click its actual 查看更多 div and retain the complete October 17 post, body link destinations, mentions, author and source. Reject changed current identity, ambiguous or hidden cards, mismatched author/body/images, and non-original posts; do not fetch authenticated APIs." + }, + { + "file": "jike-web-feed-image.html", + "url": "https://web.okjike.com/topic/63579abb6724cc583b9bba9a/square", + "capturedOn": "2026-10-17", + "state": "Signed-in modern Web topic feed; public original post 6ad399f0141b85b29268b593. Sanitized own current host props in jike-web-feed-image.posts.json. Single gallery image has no data-mode attribute.", + "expected": "Keep 成思杨, 入坑了 / 开启一个新的时代 and one original PNG. Match the displayed gallery file to own picture metadata before using picUrl including its imageMogr2 meta-keep transform; reject another file or changed source identity. Preserve the topic/feed page while clipping." + }, + { + "file": "bluesky-feed-preview.html", + "url": "https://bsky.app/profile/bsky.app", + "capturedOn": "2026-10-18", + "state": "Public signed-out profile feed in Ego Chromium. Only the current public 3mxzez2m7wk2r card HTML, including its quoted post and link preview; no account state.", + "expected": "Bind the own timestamp to bsky.app/3mxzez2m7wk2r; retain the full main and quoted text, preview title/description/destination and one original preview image. Parse the preview as a real link, without literal Markdown brackets." } ], "redaction": "Only relevant DOM containers and one comment excerpt remain. Controls, unrelated sections, scripts, event attributes and auth-bearing links were removed. Xiaoyuzhou JSON retains only query.id and episode eid/title/shownotes, used to verify identity and completeness. Tests derive synthetic boundary states from these real DOM fixtures; those variants are not live-site acceptance evidence. Phase 1 fixtures retain the actual target DOM and only post-relevant JSON records. Authentication, session, tracking and unrelated records are excluded. Reader and pointer tests use these structures; boundary mutations are synthetic. 2026-10-14 article/Jike/Douban samples retain only relevant rendered roots and public article metadata; computed hidden nodes are marked before styles are removed. Attribute allowlists exclude handlers, tracking fields and account/session state. Jike Next data retains only the selected public post identity/content/time and picUrl/middlePicUrl/thumbnailUrl. Douban list retains multiple real comments to verify exact pointer targeting and no neighbor capture.", - "updatedOn": "2026-10-15", + "updatedOn": "2026-10-17", "liveChecks": [ { "url": "https://book.douban.com/review/16093028/", diff --git a/apps/extension/platform-capture.test.mjs b/apps/extension/platform-capture.test.mjs index 66cfeed8..35a8f582 100644 --- a/apps/extension/platform-capture.test.mjs +++ b/apps/extension/platform-capture.test.mjs @@ -191,3 +191,22 @@ for(const navigate of [false,true])test(`Jike expansion ${navigate?'navigation': }}); expect(result).toEqual({ok:false,reason:navigate?'changed':'incomplete'});expect(calls.clicks).toBe(1);expect(calls.disconnected).toBe(true); }); + +for (const leaving of [false, true]) test(`Facebook expansion ${leaving ? "cancels before a cross-document URL commits" : "still captures without navigation"}`, async () => { + const url = "https://www.facebook.com/NASA/posts/pfbid0dPToN66CzcAHGMoaNe3jszH4FfBKvqkE5gogCNJkTxXDPsSGYrGvcVwsLgByyMQQl"; + const { result, calls } = await capture({ fixture: "facebook", url, prepare: ({ doc, window, changed }) => { + const body = doc.querySelector('[data-ad-preview="message"]'); + const full = body.innerHTML; + body.innerHTML = '

Preview

'; + body.querySelector('button').addEventListener('click', () => { + // During a real navigation the old URL remains until the response commits. + if (leaving) window.dispatchEvent(new window.Event('beforeunload')); + body.innerHTML = full; + changed(); + }); + } }); + if (leaving) expect(result).toEqual({ ok: false, reason: "changed" }); + else expect(result.ok).toBe(true); + expect(calls.messages).toHaveLength(1); + expect(calls.disconnected).toBe(true); +}); diff --git a/apps/extension/platform-clip.test.mjs b/apps/extension/platform-clip.test.mjs index 0e95d890..55b2f657 100644 --- a/apps/extension/platform-clip.test.mjs +++ b/apps/extension/platform-clip.test.mjs @@ -8,6 +8,17 @@ import { readXiaoyuzhou } from "./src/xiaoyuzhou-clip.ts"; import { markdownToDoc } from "../../packages/shared/src/content.ts"; import { pageImageRefs, replacePageImageUrls } from "./src/page-images.ts"; +test("clipped literal alphabetic list prefixes survive the stored document without losing words",()=>{ + const doc=createDocument('

Dr. Nancy Grace Roman:
Mr. Someone

II. Literal heading

  1. Actual numbered item
Dr. exact code\nII. unchanged
'); + const markdown=platformHtmlMarkdown(doc.body,'https://example.test'); + const parsed=markdownToDoc(markdown),nodes=[];const walk=n=>{nodes.push(n);(n.content||[]).forEach(walk)};walk(parsed); + const text=nodes.filter(n=>n.type==='text').map(n=>n.text).join(''); + expect(text).toContain('Dr. Nancy Grace Roman:');expect(text).toContain('Mr. Someone');expect(text).toContain('II. Literal heading'); + expect(nodes.filter(n=>n.type==='orderedList')).toHaveLength(1); + expect(nodes.find(n=>n.type==='listItem').content[0].content[0].text).toBe('Actual numbered item'); + expect(nodes.find(n=>n.type==='codeBlock').content[0].text).toBe('Dr. exact code\nII. unchanged'); +}); + test("rich table cells retain their grid, line breaks, inline code and archived images",()=>{ const doc=createDocument('
内容代码

首段
次行

  • one
  • two
引用
  a || b;\n\n\nconst tag = `<br>`;
'); const markdown=platformHtmlMarkdown(doc.body,'https://example.test'); @@ -299,3 +310,14 @@ test("HTML cleanup preserves semantic content and code language on a copy", () = for (const unwanted of ["javascript:", "evil()", "bad()", "分享", "onerror", "{ + const source='

一定要尊重市场规律,某队没法改变规律!只是起到稳定市场的功能。

Literal **words** stay literal.

**exact!**code
'; + const doc=createDocument(source),before=doc.body.innerHTML; + const parsed=markdownToDoc(platformHtmlMarkdown(doc.body,'https://example.test')); + const nodes=[];const walk=n=>{nodes.push(n);(n.content||[]).forEach(walk)};walk(parsed); + const paragraph=parsed.content[0];expect(paragraph.content.map(n=>n.text||'').join('')).toBe('一定要尊重市场规律,某队没法改变规律!只是起到稳定市场的功能。'); + expect(paragraph.content.some(n=>n.text==='一定要尊重市场规律,某队没法改变规律'&&n.marks?.some(m=>m.type==='bold'))).toBe(true); + expect(parsed.content[1].content.map(n=>n.text||'').join('')).toBe('Literal **words** stay literal.'); + expect(nodes.find(n=>n.type==='codeBlock').content[0].text).toBe('**exact!**code');expect(doc.body.innerHTML).toBe(before); +}); diff --git a/apps/extension/platform-save.test.mjs b/apps/extension/platform-save.test.mjs index 67adc544..703c737a 100644 --- a/apps/extension/platform-save.test.mjs +++ b/apps/extension/platform-save.test.mjs @@ -81,13 +81,13 @@ const harness = (options = {}) => { } return options.failDownload || (options.requireDoubanImageRule && !doubanImageRule) || (options.requireSspaiImageRule && !sspaiImageRule) ? new Response("Forbidden", { status: 403 }) : new Response(png, { headers: { "content-type": "image/png" } }); }; - let pageContext, selectionContext; + let pageContext, selectionContext, onInstalled; const chrome = { i18n: { getMessage: (key, substitution) => key === "saveFailedWithReason" ? `${key}: ${substitution}` : key }, runtime: { id: "extension-under-test", getURL: path=>`chrome-extension://extension-under-test/${path.replace(/^\/+/, '')}`, - onInstalled: { addListener() {} }, onMessage: { addListener(listener) { onMessage = listener; } }, + onInstalled: { addListener(listener) { onInstalled = listener; } }, onMessage: { addListener(listener) { onMessage = listener; } }, openOptionsPage: async () => {}, sendMessage: async (message) => { if (options.spoofMessages && message.type === "pagePlatformRead") { @@ -98,7 +98,7 @@ const harness = (options = {}) => { } onMessage({ ...message, requestId: "unrelated", result: { ok: false } }, { id: chrome.runtime.id, tab: { id: 7 }, frameId: 0, url: currentUrl }, () => {}); } - onMessage(message, { id: chrome.runtime.id, tab: { id: 7 }, frameId: 0, url: currentUrl }, () => {}); + onMessage(message, { id: chrome.runtime.id, tab: { id: 7 }, frameId: 0, url: options.senderUrl || currentUrl }, () => {}); }, }, storage: { local: { get: async () => ({ instanceUrl: "https://edgeever.test", token: "test-token", notebookId: "" }) } }, @@ -113,6 +113,7 @@ const harness = (options = {}) => { windows: { onRemoved: { addListener() {} } }, contextMenus: { onClicked: { addListener(listener) { onClick = listener; } } }, scripting: { executeScript: async ({ func, args = [], files, target }) => { + if (args.some(value => value === undefined)) throw new Error("scripting.executeScript args must be JSON serializable"); calls.injections.push({ files, target }); const scriptContext = target.tabId === 8 ? selectionContext : pageContext; if (options.denyScripting) throw new Error("Cannot access contents of the page. Missing host permission."); @@ -133,7 +134,7 @@ const harness = (options = {}) => { } }, action: { setBadgeBackgroundColor: async () => {}, setBadgeText: async () => {} }, }; - pageContext = createContext({ window, document: window.document, location: { get href() { return currentUrl; } }, URL, chrome, setTimeout, clearTimeout, setInterval, clearInterval, fetch: fetchMock, AbortSignal, Blob, Uint8Array }); + pageContext = createContext({ window, document: window.document, location: { get href() { return currentUrl; } }, URL, chrome, MutationObserver: window.MutationObserver, setTimeout, clearTimeout, setInterval, clearInterval, fetch: fetchMock, AbortSignal, Blob, Uint8Array }); if (options.selectionWindow) { const selectedWindow = options.selectionWindow; selectedWindow.DOMParser = class { parseFromString(value) { return createDocument(value); } }; @@ -142,8 +143,13 @@ const harness = (options = {}) => { } } }; selectionContext = createContext({ window: selectedWindow, document: selectedWindow.document, location: selectedWindow.location, URL, chrome: selectedChrome, setTimeout, clearTimeout, setInterval, clearInterval, fetch: fetchMock, AbortSignal, Blob, Uint8Array }); } + if (options.withoutDesktopApis) { + delete chrome.contextMenus; + delete chrome.windows; + } runInContext(background, createContext({ chrome, fetch: fetchMock, URL, crypto, setTimeout:options.backgroundSetTimeout || setTimeout, clearTimeout, setInterval, clearInterval, AbortSignal, AbortController, Blob, File, FormData, Uint8Array, ArrayBuffer, Response })); return { + installed: () => onInstalled({ reason: "install" }), document: window.document, calls, toolbar: () => new Promise((resolve) => onMessage({ type: "captureCurrentPage" }, { id: chrome.runtime.id }, resolve)), @@ -158,6 +164,15 @@ const harness = (options = {}) => { }; }; +test("Android without desktop menu/window APIs can initialize and save from the popup", async () => { + const h = harness({ withoutDesktopApis: true }); + expect(() => h.installed()).not.toThrow(); + expect(await h.mode()).toEqual({ kind: "platform", platform: "Hacker News" }); + expect(await h.toolbar()).toMatchObject({ ok: true }); + expect(h.calls.creates).toHaveLength(1); + expect(h.calls.creates[0].contentMarkdown).toContain("news.ycombinator.com/item?id=40380738"); +}); + test("BOSS popup detects login preview through the serialized DOM probe without creating or expanding anything", async () => { const h = harness({ platform: "boss", prepare: (doc) => { doc.body.innerHTML = createWindow(readFileSync(new URL('./fixtures/platforms/boss-login-required.html', import.meta.url), 'utf8')).document.body.innerHTML; } }); expect(await h.mode()).toMatchObject({ kind: "platform", platform: "platformBoss", access: "login-required" }); @@ -387,6 +402,15 @@ for (const [option, message] of [["denyInstance", "instancePermissionRequired"], } +for (const navigate of [false, true]) test(`a stale sender URL after SPA navigation ${navigate ? "still cancels real navigation" : "does not reject the current post"}`, async () => { + const h = harness({ platform: "linkedin", senderUrl: "https://www.linkedin.com/posts/previous-activity-123-test", navigateDuringNotebookLookup: navigate }); + const result = await h.toolbar(); + expect(result.ok).toBe(!navigate); + expect(h.calls.creates).toHaveLength(navigate ? 0 : 1); + if (navigate) expect(result.message).toBe("platformPageChanged"); + else expect(h.calls.creates[0].contentMarkdown).toContain("https://www.linkedin.com/feed/update/urn:li:activity:7497764718095323136/"); +}); + test("a pending saving toast cannot replace the already clicked post with a later right-click",async()=>{ let release,started;const gate=new Promise(resolve=>release=resolve),shown=new Promise(resolve=>started=resolve); const h=harness({platform:'bluesky',fixture:'bluesky-feed',pageUrl:'https://bsky.app/profile/bsky.app',feedbackGate:()=>{started();return gate;}}); @@ -760,3 +784,37 @@ test('stable rich selection preserves code blank lines and archives its selected expect(doc.content.find(node=>node.type==='codeBlock').content[0].text).toBe(code); expect(h.calls.uploads).toHaveLength(1);expect(JSON.stringify(doc)).toContain('/api/v1/resources/res_1/blob'); }); + +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}); + 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);} +}); + +const webJikePosts=JSON.parse(readFileSync(new URL('./fixtures/platforms/jike-web-feed.posts.json',import.meta.url),'utf8')); +const prepareWebJike=(doc,window)=>{ + const cards=[...doc.querySelectorAll('[data-clickable-feedback]')].filter(c=>c.querySelector('header')&&c.querySelector('[class*="_contentWrapper_"]')); + cards.forEach((card,i)=>card.__reactProps$fixture={children:{props:{data:structuredClone(webJikePosts[i])}}}); + cards[0].setAttribute('data-edgeever-jike-selected','');let notify=()=>{}; + window.MutationObserver=class{constructor(fn){notify=fn;}observe(){}disconnect(){}}; + cards[0].querySelector('[class*="_expandButton_"]').addEventListener('click',e=>{e.target.textContent='收起';notify();}); + return cards; +}; + +test('native Jike Web feed menu resolves current props and expands only the selected original post',async()=>{ + const h=harness({platform:'jike',fixture:'jike-web-feed',pageUrl:'https://web.okjike.com/following',prepare:prepareWebJike}); + expect((await h.menu()).kind).toBe('success');expect(h.calls.creates).toHaveLength(1); + const md=h.calls.creates[0].contentMarkdown;expect(md).toContain('https://m.okjike.com/originalPosts/'+webJikePosts[0].id); + expect(md).toContain('感谢以上即友的创作与分享');expect(md).not.toContain('2026年10月16日');expect(md).not.toContain('收起'); + expect(md).toContain('https://web.okjike.com/u/71b51e1e-ece6-4855-b1d9-98703b7c1507'); + expect(h.calls.requests.every(url=>url.startsWith('https://edgeever.test/'))).toBe(true); +}); + +test('Jike Web card identity recycling during notebook lookup cancels before creating a memo',async()=>{ + let cards,release,entered;const started=new Promise(r=>entered=r),gate=new Promise(r=>release=r); + const h=harness({platform:'jike',fixture:'jike-web-feed',pageUrl:'https://web.okjike.com/following',prepare:(d,w)=>{cards=prepareWebJike(d,w);cards[0].querySelector('[class*="_expandButton_"]').remove();},notebookRequested:entered,notebookGate:gate}); + const pending=h.menu();await started; + cards[0].__reactProps$fixture.children.props.data.id='6715e824d9dc81724c2ec74d';release(); + expect(await pending).toMatchObject({kind:'error',message:'platformPageChanged'});expect(h.calls.creates).toHaveLength(0); +}); diff --git a/apps/extension/popup.test.mjs b/apps/extension/popup.test.mjs index 47876aea..050c01fd 100644 --- a/apps/extension/popup.test.mjs +++ b/apps/extension/popup.test.mjs @@ -1,5 +1,6 @@ import { expect, test } from "bun:test"; import { captureCurrentPage } from "./src/popup"; +import { testConnection } from "./src/extension"; const settings = { instanceUrl: "https://example.com", token: "token", notebookId: "" }; async function withChrome(overrides, run) { @@ -41,6 +42,31 @@ test("denied permission prevents popup capture", async () => { }); }); +test("instance permission omits nonstandard ports while connection keeps the configured endpoint", async () => { + const originalFetch = globalThis.fetch; + try { + for (const instanceUrl of ["http://127.0.0.1:8787", "https://example.com:8443", "http://[::1]:8787"]) { + let permissionGranted = false; + await withChrome({ permissions: { request: async ({ origins }) => { + const url = new URL(instanceUrl); + expect(origins).toEqual([`${url.protocol}//${url.hostname}/*`]); + permissionGranted = true; + return true; + } } }, async () => { + globalThis.fetch = async (url, options) => { + expect(permissionGranted).toBe(true); + expect(url).toBe(`${instanceUrl}/api/v1/notebooks`); + expect(options.headers.Authorization).toBe("Bearer token"); + return Response.json({ notebooks: [{ id: "inbox", name: "Inbox" }] }); + }; + expect(await testConnection({ ...settings, instanceUrl })).toEqual([{ id: "inbox", name: "Inbox" }]); + }); + } + } finally { + globalThis.fetch = originalFetch; + } +}); + test("missing configuration never requests permissions or capture", async () => { await withChrome({}, async () => { await expect(captureCurrentPage({ ...settings, token: "" })).rejects.toThrow("completePluginConfiguration"); diff --git a/apps/extension/social-clip.test.mjs b/apps/extension/social-clip.test.mjs index 13711f78..b2ab4a0d 100644 --- a/apps/extension/social-clip.test.mjs +++ b/apps/extension/social-clip.test.mjs @@ -4,6 +4,7 @@ import {createDocument} from '@mixmark-io/domino'; import {readFileSync} from 'node:fs'; import {platformTarget, socialPlatform} from './src/platform-clip.ts'; import {readSocial,socialPostCard} from './src/social-clip.ts'; +import {docToText,markdownToDoc} from '../../packages/shared/src/content.ts'; export const socialSamples={ bluesky:{url:'https://bsky.app/profile/bsky.app/post/3mhgg5ja5gc2w',title:'v1.119 is live!',author:'Bluesky',images:1,text:"what's a bot and what's not."}, facebook:{url:'https://www.facebook.com/NASA/posts/pfbid0dPToN66CzcAHGMoaNe3jszH4FfBKvqkE5gogCNJkTxXDPsSGYrGvcVwsLgByyMQQl',author:'NASA - National Aeronautics and Space Administration',images:1,text:'in the red cloud'}, @@ -53,6 +54,30 @@ test('Threads shortened visible links bind to the loaded caption without reading const a=d.querySelector('a[href="http://go.nasa.gov/Artemis2LunarScience"]');a.setAttribute('href','https://example.test/unrelated'); expect(readSocial(d,target)).toEqual({ok:false,reason:'unreadable'}); }); +test('Threads wrapped short links bind only the visible card to the full caption',()=>{ + const target=platformTarget('https://www.threads.com/@nasa/post/DeMqtuYgkaL'); + const read=href=>{ + const d=fixture('threads-short-link'); + d.querySelector('a[href="http://go.nasa.gov/Artemis2LunarScience"]').setAttribute('href',href); + return readSocial(d,target); + }; + const destination='http://go.nasa.gov/Artemis2LunarScience'; + const wrapped='https://l.threads.com/?u='+encodeURIComponent(destination)+'&h=site-tracking'; + const r=read(wrapped);expect(r.ok).toBe(true); + expect(r.clip.author).toBe('nasa');expect(r.clip.markdown).toContain('go.nasa.gov/Artemis2LunarScience'); + expect(r.clip.markdown).not.toContain('site-tracking');expect(r.clip.markdown.match(/!\[/g)).toHaveLength(1); + for(const href of [ + 'https://l.threads.com/?u='+encodeURIComponent('https://example.test/unrelated'), + 'https://l.threads.com/?u='+encodeURIComponent('javascript:alert(1)'), + 'https://l.threads.com/', + 'https://l.threads.com.example.test/?u='+encodeURIComponent(destination), + 'https://l.threads.com/other?u='+encodeURIComponent(destination), + ]) expect(read(href)).toEqual({ok:false,reason:'unreadable'}); + const truncated=fixture('threads-short-link'); + const a=truncated.querySelector('a[href="http://go.nasa.gov/Artemis2LunarScience"]'); + a.setAttribute('href',wrapped);a.parentElement.firstElementChild.textContent='Truncated body…'; + expect(readSocial(truncated,target)).toEqual({ok:false,reason:'unreadable'}); +}); test('LinkedIn signed-in and guest cards share identical body formatting and link destinations',()=>{ const target=platformTarget(linkedinUgcUrl); const signed=readSocial(fixture('linkedin-ugc'),target),guest=readSocial(fixture('linkedin-ugc-guest'),target); @@ -190,13 +215,24 @@ test('LinkedIn expanded text remains readable when the toggle becomes See less', test('Bluesky feed uses the visible exact post and keeps the sampled quote and its image without SEO JSON',()=>{ const d=fixture('bluesky-feed');const r=readSocial(d,platformTarget('https://bsky.app/profile/bsky.app/post/3mxhdhodv222n')); - expect(r.ok).toBe(true);expect(r.clip.title).toBe('a masterclass in alt text');expect(r.clip.markdown).toContain('Mr. Joshua');expect(r.clip.markdown).toContain('Lil bone people');expect(r.clip.markdown).not.toContain('minesweeper');expect(r.clip.publishedAt.precision).toBe('relative'); + expect(r.ok).toBe(true);expect(r.clip.title).toBe('a masterclass in alt text');expect(docToText(markdownToDoc(r.clip.markdown))).toContain('Mr. Joshua');expect(r.clip.markdown).toContain('Lil bone people');expect(r.clip.markdown).not.toContain('minesweeper');expect(r.clip.publishedAt.precision).toBe('relative'); }); test('Bluesky feed retains the sampled Minesweeper link destination',()=>{ const r=readSocial(fixture('bluesky-feed'),platformTarget('https://bsky.app/profile/mikelacher.bsky.social/post/3mxepywf4ec2j')); expect(r.ok).toBe(true); expect(r.clip.markdown).toContain('[minesweeper.mikelacher.com](https://minesweeper.mikelacher.com/)'); }); +test('Bluesky feed quote link preview remains a parsed image and link in the stored document',()=>{ + const d=fixture('bluesky-feed-preview'),before=d.body.innerHTML; + const r=readSocial(d,platformTarget('https://bsky.app/profile/bsky.app/post/3mxzez2m7wk2r'));expect(r.ok).toBe(true); + const parsed=markdownToDoc(r.clip.markdown),images=[],links=[]; + const visit=n=>{if(n.type==='image')images.push(n.attrs.src);for(const m of n.marks||[])if(m.type==='link')links.push(m.attrs.href);for(const c of n.content||[])visit(c)};visit(parsed); + expect(images).toHaveLength(1);expect(images[0]).toContain('/img/feed_fullsize/'); + expect(links).toContain('https://thenpw.ghost.io/bluesky-wants-more-pro-wrestling-fans/'); + expect(docToText(parsed)).toContain('interviewed Bluesky\'s Harry Packer about pro wrestling.'); + expect(docToText(parsed)).toContain('Fans "You want a place'); + expect(docToText(parsed)).not.toContain('![');expect(docToText(parsed)).not.toContain('](');expect(d.body.innerHTML).toBe(before); +}); test('Bluesky rendered links keep full destinations and line breaks with or without SEO text',()=>{ for(const seo of [true,false]) { const d=fixture('bluesky');if(!seo)Array.from(d.querySelectorAll('script')).forEach(n=>n.remove()); @@ -550,3 +586,46 @@ for(const placement of ['card','header']) test(`LinkedIn personal metadata canno Array.from(d.querySelectorAll('p')).find(n=>n.textContent.trim()==='5 个月前').remove(); expect(readSocial(d,platformTarget(linkedinPersonUrl)).clip.publishedAt).toBeUndefined(); }); + +for (const invalid of [null, "different-post", "different-route", "invalid-alias"]) test(`Facebook permalink dialog alias ${invalid || "preserves the current post and metadata"}`, () => { + const d=fixture('facebook'),target=platformTarget(socialSamples.facebook.url); + const alias='pfbid02hCQ2Dyn2D7bjf8FnAL1moEL4JZpzzhvrt25T4UvzYjpryGmgGcMNRSnajxUhBnaNl'; + for(const a of d.querySelectorAll('a[href]'))a.setAttribute('href',a.getAttribute('href').replaceAll(target.id,alias)); + const script=d.querySelector('script');const story=JSON.parse(script.textContent.replaceAll(target.id,alias)); + const route={canonicalRouteName:invalid==='different-route'?'anotherRoute':'comet.fbweb.CometSinglePostDialogRoute',url:invalid==='different-post'?'/NASA/posts/123':new URL(target.url).pathname,params:{page_token:'NASA',story_token:invalid==='invalid-alias'?'not-a-post':alias}}; + script.textContent=JSON.stringify(story); + const routeScript=d.createElement("script");routeScript.type="application/json";routeScript.textContent=JSON.stringify(route);d.head.appendChild(routeScript); + const result=readSocial(d,target); + if(invalid){expect(result).toEqual({ok:false,reason:'unreadable'});return;} + expect(result.ok).toBe(true);expect(result.clip.url).toBe(target.url); + expect(result.clip.author).toBe(socialSamples.facebook.author); + expect(result.clip.markdown).toContain(socialSamples.facebook.text); + expect(result.clip.markdown.match(/!\[/g)).toHaveLength(1); + expect(result.clip.publishedAt).toEqual({value:new Date(story.creation_time*1000).toISOString(),precision:'datetime'}); +}); + +for(const label of ['收起','See less'])test(`Facebook expanded body excludes its ${label} control but keeps literal content`,()=>{ + const d=fixture('facebook'),body=d.querySelector('[data-ad-preview="message"]'); + body.appendChild(d.createTextNode(`Literal ${label}`));const button=d.createElement('span');button.setAttribute('role','button');button.textContent=label;body.appendChild(button); + const r=readSocial(d,platformTarget(socialSamples.facebook.url));expect(r.ok).toBe(true); + expect(r.clip.markdown).toContain(`Literal ${label}`);expect(r.clip.markdown.split(label)).toHaveLength(2); +}); + +test('Bluesky quote image alt badges are UI while alt descriptions and prose survive',()=>{ + const d=fixture('bluesky-feed'),target=platformTarget('https://bsky.app/profile/bsky.app/post/3mxhdhodv222n'); + const card=socialPostCard(d,target),quote=card.querySelector('[role="link"][aria-label^="Post by "]'),button=quote.querySelector('button:has(img)'); + const holder=d.createElement('div');button.replaceWith(holder);holder.appendChild(button); + const badge=d.createElement('div');badge.style.position='absolute';badge.innerHTML='
替代文本
';holder.appendChild(badge); + const prose=d.createElement('p');prose.textContent='正文中的替代文本与 ALT 需要保留。';quote.appendChild(prose); + const before=d.body.innerHTML,r=readSocial(d,target);expect(r.ok).toBe(true); + const parsed=markdownToDoc(r.clip.markdown),text=docToText(parsed);expect(text).toContain('正文中的替代文本与 ALT 需要保留。');expect(text.match(/替代文本/g)).toHaveLength(1); + expect(r.clip.markdown).toContain('Lil bone people');expect(r.clip.markdown).toContain('/img/feed_fullsize/');expect(d.body.innerHTML).toBe(before); +}); + +test('Threads native pressable quote stays inside the parent boundary and cannot substitute its caption',()=>{ + const d=fixture('threads'),target=platformTarget(socialSamples.threads.url),script=d.querySelector('script'),data=JSON.parse(script.textContent),caption=data.caption.text; + data.text_post_app_info={share_info:{quoted_post:{code:'Quote123',user:{username:'esa'},caption:{text:'Quoted science.'},media_type:19}}};script.textContent=JSON.stringify(data); + const state=script.cloneNode(true);d.body.innerHTML=`
NASA

Quoted science.

Neighbor sentinel

`;d.body.appendChild(state); + d.querySelector('p').textContent=caption;const r=readSocial(d,target);expect(r.ok).toBe(true);expect(r.clip.markdown).toContain('Quoted science.');expect(r.clip.markdown).not.toContain('Neighbor sentinel'); + d.querySelector('p').setAttribute('hidden','');d.querySelector('[data-pressable-container] [data-pressable-container] p').textContent=caption;expect(readSocial(d,target)).toEqual({ok:false,reason:'unreadable'}); +}); diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index 693d2141..83831af7 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -4,7 +4,7 @@ import { clipNotebookId, edgeEverFormRequest, edgeEverRequest, - getInstanceOrigin, + getInstancePermissionPattern, getSettings, listNotebooks, uploadMemoImage, @@ -94,6 +94,7 @@ import { youtubeCaptureFromRead, youtubeTargetFromUrl } from "./video/youtube"; import { t } from "./i18n"; import { readXiaoyuzhouStateInPage } from "./xiaoyuzhou-clip"; import { jikeLoginPost } from "./jike-clip"; +import { readJikeWebStateInPage } from "./jike-web-state"; import { bossLoginRequired } from "./boss-clip"; import { isPlatformClip, @@ -376,7 +377,7 @@ const ensureClipperReady = async () => { if (!settings.instanceUrl || !settings.token) { throw new Error(t("completePluginConfiguration")); } - const granted = await chrome.permissions.contains({ origins: [`${getInstanceOrigin(settings.instanceUrl)}/*`] }); + const granted = await chrome.permissions.contains({ origins: [getInstancePermissionPattern(settings.instanceUrl)] }); if (!granted) throw new Error(t("instancePermissionRequired")); return settings; }; @@ -587,7 +588,6 @@ const pendingTweetReads = new Map void>(); const pendingGithubReads = new Map void>(); const pendingPlatformReads = new Map void; }>(); const pendingXhsReads = new Map void>(); @@ -1322,6 +1322,11 @@ const describePlatformError = (error: unknown) => { return describeGithubError(error); }; +const isJikeWebFeed = (url: string) => url.startsWith("https://web.okjike.com/") && !platformTarget(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; + const readPlatformFromPage = async (tabId: number, target: PlatformTarget, selectionFirst: boolean, sourceUrl: string, selectionText?: string) => { const requestId = crypto.randomUUID(); return new Promise((resolve, reject) => { @@ -1329,7 +1334,7 @@ const readPlatformFromPage = async (tabId: number, target: PlatformTarget, selec pendingPlatformReads.delete(requestId); reject(new Error(t("captureTimeout"))); }, 10_000); - pendingPlatformReads.set(requestId, { tabId, sourceUrl, resolve: (result) => { + pendingPlatformReads.set(requestId, { tabId, resolve: (result) => { clearTimeout(timeout); pendingPlatformReads.delete(requestId); resolve(result); @@ -1338,12 +1343,13 @@ 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; await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, func: (payload: unknown) => { (globalThis as { __edgeeverPlatformClipPayload?: unknown }).__edgeeverPlatformClipPayload = payload; }, - args: [{ requestId, url: target.url, sourceUrl, selectionFirst, selectionText, xiaoyuzhouState }], + args: [{ requestId, url: target.url, sourceUrl, selectionFirst, selectionText, xiaoyuzhouState, jikeState }], }); await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, files: ["assets/capture-platform.js"] }); }; @@ -1381,6 +1387,10 @@ 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) { + const live = await readJikeWebState(tabId,target.id); + if (live?.pageUrl !== pageUrl || live.post.id !== target.id) throw new Error(t("platformPageChanged")); + } // Notebook lookup and capture can both outlive a navigation. Bind the save // to the detail page the user clicked, not the next active tab or episode. const current = await chrome.tabs.get(tabId); @@ -1401,10 +1411,15 @@ const savePlatformFromMenu = async (info: { pageUrl?: string; frameId?: number } const pageUrl = info.pageUrl || tab?.url || ""; let selectedUrl = pageUrl; if (socialPlatform(pageUrl)) { - const located = (await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, func: () => - document.querySelector("[data-edgeever-social-target]")?.getAttribute("data-edgeever-social-target") || "" - }))[0]?.result; - if (typeof located === "string" && platformTarget(located)?.platform === socialPlatform(pageUrl)) selectedUrl = located; + if (isJikeWebFeed(pageUrl)) { + const state = await readJikeWebState(tabId); + if (state?.pageUrl === pageUrl) selectedUrl = `https://m.okjike.com/originalPosts/${state.post.id}`; + } else { + const located = (await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, func: () => + document.querySelector("[data-edgeever-social-target]")?.getAttribute("data-edgeever-social-target") || "" + }))[0]?.result; + if (typeof located === "string" && platformTarget(located)?.platform === socialPlatform(pageUrl)) selectedUrl = located; + } if (!platformTarget(selectedUrl)) { await chrome.scripting.executeScript({target:{tabId,frameIds:[0]},files:["assets/social-target.js"]}); throw new Error(t(socialPlatform(pageUrl) === "douban" ? "doubanSelectComment" : "socialSelectPost")); @@ -1674,6 +1689,7 @@ const saveVideoFromMenu = async ( }; const registerClipMenus = () => { + if (!chrome.contextMenus) return; // Chrome and Firefox fold an extension into a submenu when more than one of // its items is visible. These contexts stay disjoint so each command remains // on the top-level menu: a photo saves the image, selected words save the @@ -1779,7 +1795,7 @@ const createClipMenus = () => { chrome.runtime.onInstalled.addListener(registerClipMenus); -chrome.contextMenus.onClicked.addListener((info: { menuItemId?: string | number; srcUrl?: string; pageUrl?: string; frameUrl?: string; linkUrl?: string; frameId?: number; selectionText?: string }, tab?: { id?: number; url?: string; title?: string }) => { +chrome.contextMenus?.onClicked.addListener((info: { menuItemId?: string | number; srcUrl?: string; pageUrl?: string; frameUrl?: string; linkUrl?: string; frameId?: number; selectionText?: string }, tab?: { id?: number; url?: string; title?: string }) => { if (PLATFORM_MENUS.some((menu) => menu.id === info.menuItemId)) { void savePlatformFromMenu(info, tab); return; @@ -1817,7 +1833,7 @@ chrome.contextMenus.onClicked.addListener((info: { menuItemId?: string | number; } }); -chrome.windows.onRemoved.addListener((windowId: number) => { +chrome.windows?.onRemoved.addListener((windowId: number) => { void chrome.storage.session.get([IMAGE_SAVE_WINDOW_KEY, TWEET_WINDOW_KEY, XHS_WINDOW_KEY, ZHIHU_WINDOW_KEY]).then((stored: Record) => { if (stored[IMAGE_SAVE_WINDOW_KEY] === windowId) { void chrome.storage.session.remove(IMAGE_SAVE_WINDOW_KEY); @@ -1883,7 +1899,9 @@ chrome.runtime.onMessage.addListener((message: { type?: string; page?: unknown; const pending = pendingPlatformReads.get(message.requestId); const sender = _sender as { id?: string; tab?: { id?: number }; frameId?: number; url?: string } | undefined; if (pending && sender && sender.id === chrome.runtime.id && sender.tab?.id === pending.tabId && sender.frameId === 0) { - pending.resolve(sender.url === pending.sourceUrl ? message.result : { ok: false, reason: "changed" }); + // sender.url can retain the original document URL after SPA navigation. + // The page reader checks its live URL; performPlatformSave checks the tab again before creation. + pending.resolve(message.result); } return false; } diff --git a/apps/extension/src/capture-platform.ts b/apps/extension/src/capture-platform.ts index 45167b7c..7e5c1d97 100644 --- a/apps/extension/src/capture-platform.ts +++ b/apps/extension/src/capture-platform.ts @@ -14,7 +14,7 @@ import { platformHtmlMarkdown, platformTarget, type PlatformLabels, type Platfor // Built as a standalone IIFE and injected only after a user action. (() => { const root = globalThis as typeof globalThis & { - __edgeeverPlatformClipPayload?: { requestId?: string; url?: string; selectionFirst?: boolean; selectionText?: string; sourceUrl?: string; xiaoyuzhouState?: unknown }; + __edgeeverPlatformClipPayload?: { requestId?: string; url?: string; selectionFirst?: boolean; selectionText?: string; sourceUrl?: string; xiaoyuzhouState?: unknown; jikeState?: unknown }; }; const payload = root.__edgeeverPlatformClipPayload; delete root.__edgeeverPlatformClipPayload; @@ -29,7 +29,11 @@ import { platformHtmlMarkdown, platformTarget, type PlatformLabels, type Platfor noShowNotes: t("platformNoShowNotes"), relativeTime: t("platformRelativeTimeLabel"), }; const sourceUrl = payload.sourceUrl; - const samePage = () => location.href === sourceUrl; + // A cross-document navigation can start before location.href changes. + let leavingPage = false; + const onBeforeUnload = () => { leavingPage = true; }; + window.addEventListener("beforeunload", onBeforeUnload); + const samePage = () => !leavingPage && location.href === sourceUrl; const read = (): PlatformRead => { if (!samePage()) return { ok: false, reason: "changed" }; switch (expected.platform) { @@ -39,7 +43,7 @@ import { platformHtmlMarkdown, platformTarget, type PlatformLabels, type Platfor case "boss": return readBoss(document, location.href); case "xueqiu": return readXueqiu(document, expected, location.href); case "wechat": case "sspai": case "juejin": return readArticle(document, location.href, t("platformOfficialAccountLabel")); - case "jike": return readJike(document, location.href, expected); + case "jike": return readJike(document, location.href, expected, payload.jikeState); case "douban": return readDouban(document, expected, location.href, t("platformListSourceLabel")); default: return readSocial(document, expected); } @@ -107,5 +111,6 @@ import { platformHtmlMarkdown, platformTarget, type PlatformLabels, type Platfor }); }; void capture().then((result) => finish(samePage() ? result : { ok: false, reason: "changed" })) - .catch(() => finish({ ok: false, reason: "unreadable" })); + .catch(() => finish({ ok: false, reason: "unreadable" })) + .finally(() => window.removeEventListener("beforeunload", onBeforeUnload)); })(); diff --git a/apps/extension/src/diagnostics.ts b/apps/extension/src/diagnostics.ts index d4d71165..4fb6cddc 100644 --- a/apps/extension/src/diagnostics.ts +++ b/apps/extension/src/diagnostics.ts @@ -1,4 +1,4 @@ -import type { ExtensionSettings } from "./extension"; +import { getInstancePermissionPattern, type ExtensionSettings } from "./extension"; import { t } from "./i18n"; export type DiagnosticInfo = { @@ -37,7 +37,7 @@ export const getDiagnosticInfo = async (settings: ExtensionSettings): Promise { return withProtocol.replace(/\/+$/, ""); }; -export const getInstanceOrigin = (instanceUrl: string) => { +export const getInstancePermissionPattern = (instanceUrl: string) => { const url = new URL(normalizeInstanceUrl(instanceUrl)); - return url.origin; + // Firefox match patterns do not support ports; requests retain the configured URL. + return `${url.protocol}//${url.hostname}/*`; }; export const requestInstancePermission = async (instanceUrl: string) => { - const origin = getInstanceOrigin(instanceUrl); - const granted = await chrome.permissions.request({ origins: [`${origin}/*`] }); + const pattern = getInstancePermissionPattern(instanceUrl); + const granted = await chrome.permissions.request({ origins: [pattern] }); if (!granted) { throw new Error(t("instancePermissionRequired")); } diff --git a/apps/extension/src/jike-clip.ts b/apps/extension/src/jike-clip.ts index fc9829d7..b37573b0 100644 --- a/apps/extension/src/jike-clip.ts +++ b/apps/extension/src/jike-clip.ts @@ -1,8 +1,18 @@ -import { platformHtmlMarkdown, platformReadableCopy, platformTarget, platformText, safePlatformUrl, socialPlatform, type PlatformRead, type PlatformTarget } from "./platform-clip"; +import { platformHtmlMarkdown, platformReadableCopy, platformElementHidden, platformTarget, platformText, safePlatformUrl, socialPlatform, type PlatformRead, type PlatformTarget } from "./platform-clip"; + +import { socialBodyMarkdown } from "./social-clip"; type JikePicture = { picUrl?: unknown; middlePicUrl?: unknown; thumbnailUrl?: unknown }; type JikePost = { id?: unknown; content?: unknown; createdAt?: unknown; pictures?: JikePicture[]; user?: { username?: unknown } }; +const WEB_DETAIL_CARD = '[data-clickable-feedback][class*="_postCard_"]'; +const WEB_BODY = '[class*="_contentWrapper_"]'; + export const JIKE_CARD_SELECTOR = ".post-list .post-item-li"; +export const JIKE_WEB_CARD_SELECTOR = '[data-clickable-feedback]'; +export const isJikeWebCard = (card: Element): boolean => { + const header = card.querySelector("header"), body = card.querySelector(WEB_BODY); + return card.matches(JIKE_WEB_CARD_SELECTOR) && !!header && !!body && !body.contains(header) && header.closest(JIKE_WEB_CARD_SELECTOR) === card && body.closest(JIKE_WEB_CARD_SELECTOR) === card; +}; /** The Web site redirects signed-out post/feed requests to this login URL. */ export const jikeLoginPost = (pageUrl: string): boolean => { @@ -24,6 +34,7 @@ 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)}`; // 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); @@ -38,6 +49,18 @@ const normalizedText = (value: string, base: string) => value.replace(/https?:\/ }).replace(/\s+/g,""); const pictureUrls = (picture: JikePicture | undefined, base: string) => picture ? [picture.picUrl,picture.middlePicUrl,picture.thumbnailUrl] .filter((value): value is string=>typeof value === "string").map(value=>safePlatformUrl(value,base)).filter(Boolean) : []; +const webImageIdentity = (src: string) => { + const url=new URL(src); + if (url.origin === "https://cdnv2.ruguoapp.com" && url.search.startsWith("?imageMogr2/") && !url.search.includes("&")) url.search=""; + return url.href; +}; +const removeJikeControls = (body: Element, images: Element[]) => { + body.querySelectorAll('button,[role="button"],audio').forEach(node=>{ + if (images.includes(node)) node.removeAttribute("role"); + else if (node.tagName.toLowerCase() !== "audio" && images.some(image=>node.contains(image))) node.replaceWith(...Array.from(node.childNodes)); + else node.remove(); + }); +}; /** H5 topic cards have no post link. Bind public data by own author and text, * never by array position, a mentioned post, or another card's permalink. */ @@ -64,14 +87,91 @@ export const jikeCardTarget = (card: Element, pageUrl: string): PlatformTarget | }; export const jikePostCard = (doc: Document, target: PlatformTarget, pageUrl: string): Element | null => { const detail=platformTarget(pageUrl); - if (detail?.platform === "jike" && detail.id === target.id) return doc.querySelector(".post-page .post-wrap"); + if (detail?.platform === "jike" && detail.id === target.id) { + const share = doc.querySelector(".post-page .post-wrap"); + if (share) return share; + const url = new URL(pageUrl); + const profilePath = /^(\/u\/[^/]+)\/post\/[a-f0-9]{24}\/?$/.exec(url.pathname)?.[1]; + if (url.hostname !== "web.okjike.com" || !profilePath) return null; + const cards = Array.from(doc.querySelectorAll(WEB_DETAIL_CARD)).filter(card => !platformElementHidden(card) + && Array.from(card.querySelector("header")?.querySelectorAll("a[href]") || []).some(link => + !platformElementHidden(link) && safePlatformUrl(link.getAttribute("href") || "", pageUrl) === `${url.origin}${profilePath}`)); + return cards.length === 1 ? cards[0] : null; + } + const web = Array.from(doc.querySelectorAll(`[data-edgeever-jike-id="${target.id}"]`)).filter(card => isJikeWebCard(card) && !platformElementHidden(card)); + if (web.length) return web.length === 1 ? web[0] : null; 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. */ +const readWebDetail = (doc: Document, card: Element, target: PlatformTarget, post?: JikePost): PlatformRead => { + const body = card.querySelector(WEB_BODY); + if (!body) return {ok:false,reason:"unreadable"}; + if (jikeExpandButton(doc, card)) return {ok:false,reason:"incomplete"}; + const clean = platformReadableCopy(body); + const ownText = (root: Element) => Array.from(root.querySelectorAll('[class^="_content_"]')) + .find(node => !node.closest("a")); + if (platformText(ownText(body) || null) && !platformText(ownText(clean) || null)) return {ok:false,reason:"unreadable"}; + // Inline mentions contain avatars; they are not post attachments. + clean.querySelectorAll('[class*="_mentionUser_"] img, [class*="_expandButton_"]').forEach(node => node.remove()); + removeJikeControls(clean,Array.from(clean.querySelectorAll("img"))); + // Web's gallery viewer uses the same CDN path without its thumbnail transform. + for (const image of Array.from(clean.querySelectorAll('img[data-anonymous-gallery="true"]'))) { + const src = safePlatformUrl(image.getAttribute("src") || "", target.url); + if (!src) continue; + const original=webImageIdentity(src); + if (original !== src) image.setAttribute("data-original",original); + } + for (const link of Array.from(clean.querySelectorAll("a[href]"))) { + const href=safePlatformUrl(link.getAttribute("href") || "", "https://web.okjike.com/"); + const own = platformTarget(href); + if (href) link.setAttribute("href", own?.platform === "jike" ? own.url : href); + } + const text = platformText(ownText(clean) || null); + 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"}; + 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()) { + const shown=safePlatformUrl(image.getAttribute("data-original") || image.getAttribute("src") || "",target.url),picture=post.pictures[index]; + if (!shown || !pictureUrls(picture,target.url).some(url=>webImageIdentity(url)===webImageIdentity(shown))) return {ok:false,reason:"incomplete"}; + const original=typeof picture.picUrl === "string" ? safePlatformUrl(picture.picUrl,target.url) : ""; + if (original) image.setAttribute("data-original",original); + } + } + const liveHeader = card.querySelector("header"); + const header = liveHeader ? platformReadableCopy(liveHeader) : null; + const author = Array.from(header?.querySelectorAll("a[href]") || []).map(link => platformText(link)).find(Boolean); + const time = platformText(header?.querySelector('[class*="_descContainer_"]') || null); + if (post && !Array.from(header?.querySelectorAll('a[href]') || []).some(link => { + const url=safePlatformUrl(link.getAttribute("href") || "","https://web.okjike.com/"); + return url && new URL(url).origin === "https://web.okjike.com" && new URL(url).pathname.replace(/\/$/,"").toLowerCase() === `/u/${String(post.user?.username).toLowerCase()}`; + })) return {ok:false,reason:"changed"}; + const date = /^(\d{4})\/(\d{2})\/(\d{2})$/.exec(time); + // Keep mention markers until the completeness comparison above. Their + // layout divs must then become inline labels to form valid Markdown links. + for (const link of Array.from(clean.querySelectorAll("a[href]"))) { + if (link.querySelector("div") && !link.querySelector("img")) link.textContent = platformText(link); + } + const markdown = socialBodyMarkdown(doc, clean, target.url); + if (!markdown) return {ok:false,reason:"unreadable"}; + return {ok:true,clip:{...target,title:text.slice(0,120) || author || "Jike",author, + publishedAt:date ? {value:`${date[1]}-${date[2]}-${date[3]}`,precision:"date"} : time ? {value:time,precision:"relative"} : undefined, + markdown,tags:["web-clip","jike"],completeness:"complete"}}; +}; + /** Public share-page/card reader. It never fetches authenticated Jike APIs. */ -export const readJike = (doc: Document, pageUrl: string, target: PlatformTarget | null = platformTarget(pageUrl)): PlatformRead => { +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)) { + 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"; @@ -92,11 +192,7 @@ export const readJike = (doc: Document, pageUrl: string, target: PlatformTarget if (original) image.setAttribute("data-original",original); } // Preserve the audio/link preview text and cover, excluding player controls. - clean.querySelectorAll('button,[role="button"],audio').forEach(node=>{ - if (images.includes(node)) node.removeAttribute("role"); - else if (node.tagName.toLowerCase() !== "audio" && images.some(image=>node.contains(image))) node.replaceWith(...Array.from(node.childNodes)); - else node.remove(); - }); + removeJikeControls(clean,images); for (const link of Array.from(clean.querySelectorAll("a[href]"))) { const own=platformTarget(safePlatformUrl(link.getAttribute("href") || "",target.url)); if (own?.platform === "jike") link.setAttribute("href",own.url); @@ -109,5 +205,5 @@ export const readJike = (doc: Document, pageUrl: string, target: PlatformTarget const created=typeof post?.createdAt === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(post.createdAt) ? post.createdAt : ""; return {ok:true,clip:{...target,title,author:author || undefined,publishedAt:created ? {value:created,precision:"datetime"} : time ? {value:time,precision:"relative"} : undefined,markdown,tags:["web-clip","jike"],completeness:"complete"}}; }; -export const jikeExpandButton = (doc: Document, card: Element | null = doc.querySelector(".post-page .post-wrap")): HTMLElement | null => Array.from(card?.querySelectorAll('.text button,.text [role="button"]') || []) - .find(n=>/^(展开|展开全文|展开全部|显示全文)$/.test(platformText(platformReadableCopy(n)))) || null; +export const jikeExpandButton = (doc: Document, card: Element | null = doc.querySelector(".post-page .post-wrap")): HTMLElement | null => Array.from(card?.querySelectorAll('.text button,.text [role="button"], [class*="_contentWrapper_"] button, [class*="_contentWrapper_"] [role="button"], [class*="_contentWrapper_"] [class*="_expandButton_"]') || []) + .find(n=>/^(展开|展开全文|展开全部|显示全文|查看更多)$/.test(platformText(platformReadableCopy(n)))) || null; diff --git a/apps/extension/src/jike-web-state.ts b/apps/extension/src/jike-web-state.ts new file mode 100644 index 00000000..7a355759 --- /dev/null +++ b/apps/extension/src/jike-web-state.ts @@ -0,0 +1,53 @@ +/** Read only the selected card's current host props, never Fiber/alternate trees. */ +export function readJikeWebStateInPage(expectedId?: string) { + // Closure-free: scripting.executeScript serializes this function into MAIN. + if (new URL(location.href).origin !== "https://web.okjike.com") return null; + if (expectedId && !/^[a-f0-9]{24}$/.test(expectedId)) return null; + const object = (v: unknown): Record | null => v && typeof v === "object" ? v as Record : null; + const visible = (node: Element) => { + for (let n: Element | null = node; n; n = n.parentElement) { + if (n.matches('[hidden], [aria-hidden="true"]') || (n.ownerDocument.defaultView?.getComputedStyle?.(n) || (n as HTMLElement).style)?.display === "none") return false; + } + const visibility = node.ownerDocument.defaultView?.getComputedStyle?.(node).visibility || (node as HTMLElement).style?.visibility; + return visibility !== "hidden" && visibility !== "collapse"; + }; + const read = (card: Element) => { + const keys = Object.getOwnPropertyNames(card).filter(k => k.startsWith("__reactProps$")); + if (keys.length !== 1) return null; + const stack: unknown[] = [(card as unknown as Record)[keys[0]]], seen = new WeakSet(); + const posts = new Map(); + while (stack.length) { + const value = object(stack.pop()); + if (!value || seen.has(value)) continue; + seen.add(value); + if (Array.isArray(value)) { stack.push(...value); continue; } + const props = object(value.props) || value, data = object(props.data), user = object(data?.user); + if (data && typeof data.id === "string" && /^[a-f0-9]{24}$/.test(data.id) && typeof data.type === "string" && user && typeof user.username === "string") { + if (typeof data.content !== "string" || !Array.isArray(data.pictures)) return null; + const pictures: { picUrl?: string; middlePicUrl?: string; thumbnailUrl?: string }[] = []; + for (const picture of data.pictures) { + const p = object(picture); + if (!p) return null; + pictures.push({ picUrl: typeof p.picUrl === "string" ? p.picUrl : undefined, middlePicUrl: typeof p.middlePicUrl === "string" ? p.middlePicUrl : undefined, thumbnailUrl: typeof p.thumbnailUrl === "string" ? p.thumbnailUrl : undefined }); + } + const post = { id: data.id, type: data.type, content: data.content, createdAt: typeof data.createdAt === "string" ? data.createdAt : undefined, user: { username: user.username }, pictures }; + if (posts.has(post.id) && JSON.stringify(posts.get(post.id)) !== JSON.stringify(post)) return null; + posts.set(post.id, post); + } + if (props.children) stack.push(props.children); + } + const postsList = [...posts.values()]; + 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]'; + 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; + const post = read(card); + if (post && (!expectedId || post.id === expectedId)) matches.push({ card, post }); + } + if (matches.length !== 1) return null; + matches[0].card.setAttribute("data-edgeever-jike-id", matches[0].post.id); + return { pageUrl: location.href, post: matches[0].post }; +} diff --git a/apps/extension/src/medium-content.ts b/apps/extension/src/medium-content.ts index 02a7efa2..44bc6d88 100644 --- a/apps/extension/src/medium-content.ts +++ b/apps/extension/src/medium-content.ts @@ -1,27 +1,84 @@ import { articlePlatform } from "./article-platform"; import { articleMetadata, type ArticleContent } from "./article-metadata"; -import { platformReadableCopy, platformText } from "./platform-clip"; +import { platformElementHidden, platformReadableCopy, platformText, safePlatformUrl } from "./platform-clip"; + +// Medium renders highlighted code without a language class. Read only the +// language of a paragraph whose identity and visible code both match; never +// use the state to recover article text that the reader cannot see. +const codeLanguages = (doc: Document) => { + const languages = new Map(); + for (const script of Array.from(doc.querySelectorAll('script'))) { + const source = script.textContent || ''; + if (source.length > 10_000_000 || !source.includes('codeBlockMetadata')) continue; + const json = /^\s*window\.__APOLLO_STATE__\s*=\s*({[\s\S]*})\s*;?\s*$/.exec(source)?.[1]; + if (!json) continue; + try { + for (const value of Object.values(JSON.parse(json))) { + if (!value || typeof value !== 'object') continue; + const paragraph = value as Record; + const metadata = paragraph.codeBlockMetadata as { lang?: unknown } | null; + if (paragraph.__typename === 'Paragraph' && paragraph.type === 'PRE' + && typeof paragraph.name === 'string' && typeof paragraph.text === 'string' + && typeof metadata?.lang === 'string' && /^[a-zA-Z0-9_+-]+$/.test(metadata.lang)) { + languages.set(paragraph.name, { text: paragraph.text, language: metadata.lang }); + } + } + } catch { /* Page scripts are data only; malformed state is ignored. */ } + } + return languages; +}; /** Extract only rendered Medium story blocks; never fetch paywalled content. */ export const mediumContent = (doc: Document, pageUrl: string): ArticleContent | null => { if (articlePlatform(doc,pageUrl) !== "Medium") return null; const liveArticle = doc.querySelector('article'); const article = liveArticle ? platformReadableCopy(liveArticle) : null; const title = article?.querySelector('h1[data-selectable-paragraph]'); - if (!article || !title?.textContent?.trim()) return null; + if (!liveArticle || !article || !title?.textContent?.trim()) return null; + const liveTitle = Array.from(liveArticle.querySelectorAll('h1[data-selectable-paragraph]')) + .find(node=>platformText(platformReadableCopy(node))); const body = doc.createElement('div'); + const languages = codeLanguages(doc); const chosen = new Set(); - for (const block of Array.from(article.querySelectorAll('[data-selectable-paragraph], figure, pre, table'))) { - if (block === title || block.closest('[hidden], [aria-hidden="true"]')) continue; + for (const block of Array.from(liveArticle.querySelectorAll('[data-selectable-paragraph], figure, pre, table, a[href]'))) { + if (block === liveTitle) continue; + const preview = block.tagName === "A" && !!block.querySelector("h2"); + if (block.tagName === "A" && !preview) continue; const container = block.closest('figure, pre, ul, ol, blockquote, table'); - const source = container && article.contains(container) ? container : block; + const source = container && liveArticle.contains(container) ? container : block; if (chosen.has(source) || Array.from(chosen).some((parent)=>parent.contains(source))) continue; chosen.add(source); - const copy = source.cloneNode(true) as Element; + const copy = platformReadableCopy(source); + for (const pre of copy.tagName === 'PRE' ? [copy] : Array.from(copy.querySelectorAll('pre'))) { + Array.from(pre.querySelectorAll('br')).forEach(node => node.parentNode?.replaceChild(doc.createTextNode('\n'),node)); + const paragraph = pre.matches('[data-selectable-paragraph][id]') ? pre : pre.querySelector('[data-selectable-paragraph][id]'); + const declared = paragraph && languages.get(paragraph.id); + if (declared && declared.text === pre.textContent + && !/(?:^|\s)(?:language|lang)-/.test(pre.className) + && !pre.querySelector('code[class*="language-"], code[class*="lang-"]')) { + pre.classList.add(`language-${declared.language}`); + } + } copy.querySelectorAll('script, style, button, svg, source, [hidden], [aria-hidden="true"], .speechify-ignore').forEach((node)=>node.remove()); // Keep the loaded image and caption, excluding Medium's image-viewer prompt. copy.querySelectorAll('figure [role="button"] > span, [role="button"] > span').forEach((node)=>node.remove()); - body.appendChild(copy); + if (preview && source === block) { + // Medium link cards are not selectable paragraphs. Flatten their label + // into a real Markdown link and retain the rendered CSS thumbnail. + const card = doc.createElement("p"); + for (const image of Array.from(copy.querySelectorAll("img"))) card.appendChild(image); + for (const element of Array.from(source.querySelectorAll("*"))) { + if (platformElementHidden(element)) continue; + const background = doc.defaultView?.getComputedStyle?.(element).backgroundImage + || (element as HTMLElement).style.backgroundImage; + const raw = /^url\(["']?(.*?)["']?\)$/.exec(background || "")?.[1]; + const url = raw && safePlatformUrl(raw,pageUrl); + if (url) { const image=doc.createElement("img");image.setAttribute("src",url);card.appendChild(image); } + } + for (const part of Array.from(copy.querySelectorAll("h2,h3,p"))) part.appendChild(doc.createTextNode(" ")); + const link=doc.createElement("a");link.setAttribute("href",copy.getAttribute("href") || "");link.textContent=platformText(copy);card.appendChild(link); + body.appendChild(card); + } else body.appendChild(copy); } const name=platformText(title); - return body.textContent?.trim() || body.querySelector('img') ? {title:name,body,...articleMetadata(doc,pageUrl,name,article!)} : null; + return body.textContent?.trim() || body.querySelector('img') ? {title:name,body,...articleMetadata(doc,pageUrl,name,article)} : null; }; diff --git a/apps/extension/src/platform-clip.ts b/apps/extension/src/platform-clip.ts index 7e7487b1..ced2e346 100644 --- a/apps/extension/src/platform-clip.ts +++ b/apps/extension/src/platform-clip.ts @@ -88,7 +88,8 @@ export const platformTarget = (pageUrl: string): PlatformTarget | null => { } } if (url.hostname === "m.okjike.com" || url.hostname === "web.okjike.com") { - const id = (url.hostname === "m.okjike.com" ? /^\/originalPosts\/([a-f0-9]{24})\/?$/ : /^\/originalPost\/([a-f0-9]{24})\/?$/).exec(url.pathname)?.[1]; + const id = (url.hostname === "m.okjike.com" ? /^\/originalPosts\/([a-f0-9]{24})\/?$/ : /^\/originalPost\/([a-f0-9]{24})\/?$/).exec(url.pathname)?.[1] + || (url.hostname === "web.okjike.com" ? /^\/u\/[^/]+\/post\/([a-f0-9]{24})\/?$/.exec(url.pathname)?.[1] : undefined); if (id) return { platform: "jike", id, url: `https://m.okjike.com/originalPosts/${id}` }; } if (url.hostname === "sspai.com" || url.hostname === "www.sspai.com" || url.hostname === "juejin.cn") { @@ -300,10 +301,23 @@ export const platformHtmlMarkdown = (body: Element | DocumentFragment, baseUrl: const options: TurndownService.Options & {preserveTableHtml: boolean} = { bulletListMarker: "-", codeBlockStyle: "fenced", headingStyle: "atx", linkStyle: "inlined", preserveTableHtml:false }; const turndown = new TurndownService(options); turndown.use(tables); + turndown.addRule("platformCjkStrong", { + filter: ["strong", "b"], + replacement: (content, node) => { + // CommonMark cannot close emphasis after punctuation before unspaced + // prose. Keep every character, moving only that punctuation outside bold. + const suffix = /^[\p{L}\p{N}]/u.test(node.nextSibling?.textContent || "") + ? content.match(/[,。!?;:、]+$/u)?.[0] || "" : ""; + const text = suffix ? content.slice(0, -suffix.length) : content; + return text.trim() ? `**${text}**${suffix}` : content; + }, + }); // Escape literal text at Turndown's text boundary. Code blocks keep their // exact bytes, and a pasted HTML tag cannot become a note element. const escape=turndown.escape.bind(turndown); - turndown.escape=(value)=>escape(value.replace(/&/g,"&").replace(//g,">")); + turndown.escape=(value)=>escape(value.replace(/&/g,"&").replace(//g,">")) + // TipTap also recognizes alphabetic/Roman list markers, including "Dr.". + .replace(/^([ \t]*[A-Za-z]+)([.)])(?=\s)/gm,"$1\\$2"); const oddPipeBackslashes = (value: string) => Array.from(value.matchAll(/(\\+)\|/g)).some(match => match[1].length % 2 === 1); // GFM code spans cannot encode an odd backslash run before a pipe without // changing bytes or columns. Keep that fragment as escaped text instead. diff --git a/apps/extension/src/social-clip.ts b/apps/extension/src/social-clip.ts index 4e8a0eb2..bb73899e 100644 --- a/apps/extension/src/social-clip.ts +++ b/apps/extension/src/social-clip.ts @@ -44,7 +44,7 @@ const paragraphs = (doc: Document, value: string) => { const plainSocialMarkdown = (doc: Document, value: string, base: string) => platformHtmlMarkdown(paragraphs(doc,value),base); /** Normalize text newlines in a detached body before shared Markdown conversion. */ -const socialBodyMarkdown = (doc: Document, body: Element, base: string) => { +export const socialBodyMarkdown = (doc: Document, body: Element, base: string) => { const visit = (node: Node) => { for (const child of Array.from(node.childNodes)) { if (child.nodeType !== 3) { visit(child); continue; } @@ -116,15 +116,31 @@ export const linkedinCardTarget = (card: Element): PlatformTarget | null => { return null; }; +const facebookTargetIds = (doc: Document, target: PlatformTarget) => { + const ids = new Set([target.id]); + for (const item of pageRecords(doc, target.id)) { + if (text(item.post_id) === target.id) { + const alias = platformTarget(text(item.permalink_url || item.url)); + if (alias?.platform === "facebook") ids.add(alias.id); + } + // The signed-in permalink dialog can declare a different pfbid for the same post. + if (item.canonicalRouteName !== "comet.fbweb.CometSinglePostDialogRoute" + || !sameTarget(safePlatformUrl(text(item.url), target.url), target)) continue; + const params = record(item.params); + const alias = platformTarget(`https://www.facebook.com/${text(params?.page_token)}/posts/${text(params?.story_token)}`); + if (alias?.platform === "facebook") ids.add(alias.id); + } + return ids; +}; + export const socialPostCard = (doc: Document, target: PlatformTarget): Element | null => { if (target.platform === "facebook") { - const aliases=pageRecords(doc,target.id).filter((item)=>text(item.post_id) === target.id) - .map((item)=>platformTarget(text(item.permalink_url || item.url))?.id).filter(Boolean); + const ids = facebookTargetIds(doc, target); return Array.from(doc.querySelectorAll('[role="article"], article')).find((card) => ownPostLinks(card,target.platform).some((a) => { const url=postLinkUrl(a,target.url); return a.closest('[role="article"], article') === card && !/[?&]comment_id=/.test(url) - && (sameTarget(url,target) || aliases.includes(platformTarget(url)?.id)); + && ids.has(platformTarget(url)?.id || ""); })) || null; } if (target.platform === "linkedin") { @@ -172,8 +188,14 @@ export const readBluesky = (doc: Document, target: PlatformTarget): PlatformRead const content = bodyCopy?.textContent || ""; if (!content.trim() && text(entry?.text).trim()) return {ok:false,reason:"unreadable"}; const copy = post.cloneNode(true) as Element; + for (const button of Array.from(copy.querySelectorAll('button[aria-label]'))) { + if (!button.querySelector('img[src*="/img/feed_"][alt]')) continue; + const badge = button.nextElementSibling as HTMLElement | null; + if (badge?.style.position === "absolute" && /^(?:ALT|替代文本)$/.test(platformText(badge)) + && !badge.querySelector('img,a,button')) badge.remove(); + } copy.querySelectorAll('button, [data-testid="userAvatarImage"], svg').forEach((node)=> { - if(node.tagName.toLowerCase() === "button" && node.querySelector('img[src*="/img/feed_"]')) node.replaceWith(...Array.from(node.childNodes)); + if(node.tagName.toLowerCase() === "button" && node.querySelector('img[src*="/img/feed_"]')) node.replaceWith(...Array.from(node.querySelectorAll('img[src*="/img/feed_"]'))); else node.remove(); }); const parts = [bodyCopy ? socialBodyMarkdown(doc,bodyCopy,target.url) @@ -189,6 +211,17 @@ export const readBluesky = (doc: Document, target: PlatformTarget): PlatformRead parts.push(...visibleImages); for (const quote of Array.from(copy.querySelectorAll(BLUESKY_QUOTE_SELECTOR))) { for (const img of Array.from(quote.querySelectorAll("img[src]"))) img.setAttribute("src",blueskyFullsizeUrl(img.getAttribute("src") || "")); + // Link previews wrap a block of image/title/description. Keep the image + // separately and the complete label inline so Markdown forms a real link. + for (const link of Array.from(quote.querySelectorAll("a[href]"))) { + if (!link.querySelector("div")) continue; + const labelCopy = link.cloneNode(true) as Element; + for (const block of Array.from(labelCopy.querySelectorAll("div,p"))) block.appendChild(doc.createTextNode(" ")); + const label = platformText(labelCopy); + if (!label) continue; + for (const image of Array.from(link.querySelectorAll("img"))) link.parentNode!.insertBefore(image, link); + link.textContent = label; + } parts.push(platformHtmlMarkdown(quote,target.url)); } const author = text(record(entry?.author)?.name) || Array.from(card.querySelectorAll('a[href]')) @@ -207,10 +240,12 @@ export const metaPostCard = (link: Element, platform: string, base: string): Ele if (!link.querySelector("time[datetime]") || link.closest("[hidden], [aria-hidden='true'], script, style, template")) return null; const target = platformTarget(postLinkUrl(link,base)); if (!target || target.platform !== platform) return null; + const pressable = platform === 'threads' ? link.closest('[data-pressable-container]') : null; let card = link.parentElement; for (let depth=0; card && depth<12; depth++,card=card.parentElement) { if (card.matches('main, [role="main"], [role="region"], [role="dialog"], body')) break; const timestamps = Array.from(card.querySelectorAll('a[href]')).filter((a)=>a.querySelector('time[datetime]') + && (!pressable || a.closest('[data-pressable-container]') === pressable) && (!a.closest('article, [role="article"]') || a.closest('article, [role="article"]') === card)); if (timestamps.some((a)=> { const other=platformTarget(postLinkUrl(a,base)); @@ -235,9 +270,16 @@ const readableMetaCard = (doc: Document, target: PlatformTarget, captions: strin if (!card) continue; const copy=platformReadableCopy(card); copy.querySelectorAll("script, style, template, article, [role='article'], [hidden], [aria-hidden='true']").forEach((node)=>node.remove()); + if (target.platform === 'threads') Array.from(copy.querySelectorAll('[data-pressable-container]')).forEach(node=>node.remove()); // Threads shortens visible URL labels; its card's href retains the destination. if (target.platform === 'threads') for (const a of Array.from(copy.querySelectorAll('a[href]'))) { - const href=safePlatformUrl(a.getAttribute('href') || '',target.url); + let href=safePlatformUrl(a.getAttribute('href') || '',target.url); + if (href) { + const url=new URL(href); + if (url.origin === 'https://l.threads.com' && url.pathname === '/') { + href=safePlatformUrl(url.searchParams.get('u') || '',target.url); + } + } const label=[href,href.replace(/^https?:\/\//,'')].find((value)=>value && expected.some((caption)=>caption.includes(value))); if (label) a.textContent=label; } @@ -328,11 +370,16 @@ export const readFacebook = (doc: Document, target: PlatformTarget): PlatformRea const liveBody=ownBody(liveCard); if (platformText(liveBody) && !platformText(body)) return {ok:false,reason:"unreadable"}; if (body?.closest('[hidden], [aria-hidden="true"]')) return {ok:false,reason:"unreadable"}; - const records = pageRecords(doc, target.id); - const story = records.find((item) => item.__typename === "Story" && (sameTarget(item.permalink_url || item.url, target) || text(item.post_id) === target.id)); + const ids = facebookTargetIds(doc, target); + const records = [...ids].flatMap(id => pageRecords(doc, id)); + const story = records.find((item) => item.__typename === "Story" + && (ids.has(platformTarget(text(item.permalink_url || item.url))?.id || "") || ids.has(text(item.post_id)))); const contentStory=record(record(record(story?.comet_sections)?.content)?.story); if (!platformText(body) && text(record(contentStory?.message)?.text).trim()) return {ok:false,reason:"unreadable"}; const cleanBody = body?.cloneNode(true) as Element | undefined; + for (const button of Array.from(cleanBody?.querySelectorAll('button, [role="button"]') || [])) { + if (/^(?:收起|See less)$/i.test(platformText(button))) button.remove(); + } for (const emoji of Array.from(cleanBody?.querySelectorAll('img[src*="/images/emoji.php"]') || [])) emoji.replaceWith(doc.createTextNode(emoji.getAttribute("alt") || "")); const parts = [cleanBody ? socialBodyMarkdown(doc,cleanBody,target.url) : ""]; const copy = card.cloneNode(true) as Element; @@ -365,7 +412,7 @@ export const readFacebook = (doc: Document, target: PlatformTarget): PlatformRea const author = text(actor?.name) || platformText(card.querySelector('h2 a, h3 a, strong a')); const saved = result(target,parts.filter(Boolean).join("\n\n"),author,epochTime(story?.creation_time),platformText(cleanBody).slice(0,100) || author); if (saved.ok && !saved.clip.publishedAt) { - const link = ownPostLinks(card,target.platform).find((a)=>sameTarget(postLinkUrl(a,target.url),target)); + const link = ownPostLinks(card,target.platform).find((a)=>ids.has(platformTarget(postLinkUrl(a,target.url))?.id || "")); saved.clip.publishedAt = publishedTime(link?.querySelector("time, abbr") || link || null); } return saved; diff --git a/apps/extension/src/social-target.ts b/apps/extension/src/social-target.ts index d6ae0508..ea8e07bb 100644 --- a/apps/extension/src/social-target.ts +++ b/apps/extension/src/social-target.ts @@ -2,7 +2,7 @@ import { platformTarget, socialPlatform } from "./platform-clip"; import { ownPostLinks, metaPostCard, linkedinCardTarget, LINKEDIN_CARD_SELECTOR } from "./social-clip"; import { xueqiuCardTarget, XUEQIU_CARD_SELECTOR } from "./xueqiu-clip"; import { doubanCommentTarget } from "./douban-clip"; -import { jikeCardTarget, JIKE_CARD_SELECTOR } from "./jike-clip"; +import { jikeCardTarget, JIKE_CARD_SELECTOR, JIKE_WEB_CARD_SELECTOR, isJikeWebCard } from "./jike-clip"; // Only remembers the card under the pointer. No network or automatic capture. (() => { @@ -11,6 +11,7 @@ import { jikeCardTarget, JIKE_CARD_SELECTOR } from "./jike-clip"; root.__edgeeverSocialTarget = true; document.addEventListener("contextmenu", (event) => { for (const node of document.querySelectorAll("[data-edgeever-social-target]")) node.removeAttribute("data-edgeever-social-target"); + for (const node of document.querySelectorAll("[data-edgeever-jike-selected]")) node.removeAttribute("data-edgeever-jike-selected"); const element = event.target instanceof Element ? event.target : event.target instanceof Node ? event.target.parentElement : null; if (!element) return; const platform = socialPlatform(location.href); @@ -19,13 +20,17 @@ import { jikeCardTarget, JIKE_CARD_SELECTOR } from "./jike-clip"; : platform === "linkedin" ? LINKEDIN_CARD_SELECTOR : platform === "xueqiu" ? XUEQIU_CARD_SELECTOR : platform === "douban" ? ".comment-item[data-cid]" - : platform === "jike" ? JIKE_CARD_SELECTOR + : platform === "jike" ? `${JIKE_CARD_SELECTOR},${JIKE_WEB_CARD_SELECTOR}` : platform === "instagram" ? "article" : ""; - const card = selector ? element.closest(selector) : null; + let card = selector ? element.closest(selector) : null; + if (platform === "jike" && new URL(location.href).hostname === "web.okjike.com") { + while (card && !card.matches(JIKE_CARD_SELECTOR) && !isJikeWebCard(card)) card=card.parentElement?.closest(selector) || null; + } const mark = (node: Element) => { if (platform === "jike") { const target=jikeCardTarget(node,location.href); if (target) {node.setAttribute("data-edgeever-social-target",target.url);return true;} + if (new URL(location.href).origin === "https://web.okjike.com" && isJikeWebCard(node)) {node.setAttribute("data-edgeever-jike-selected","");return true;} return false; } if (platform === "douban") { diff --git a/apps/extension/src/xueqiu-clip.ts b/apps/extension/src/xueqiu-clip.ts index 3964c3c3..50560009 100644 --- a/apps/extension/src/xueqiu-clip.ts +++ b/apps/extension/src/xueqiu-clip.ts @@ -1,7 +1,7 @@ import { platformHtmlCopy, platformHtmlMarkdown, platformReadableCopy, platformTarget, platformText, publishedTime, safePlatformUrl, type PlatformRead, type PlatformTarget } from "./platform-clip"; -export const XUEQIU_CARD_SELECTOR = '.article__container, [class*="timeline__item__main_"]'; -const headerSelector = '.article__author, [class*="timeline__item__info_"]'; +export const XUEQIU_CARD_SELECTOR = '.article__container, .timeline__item__main, [class*="timeline__item__main_"]'; +const headerSelector = '.article__author, .timeline__item__info, [class*="timeline__item__info_"]'; const ownHeader = (card: Element) => { const header = card.querySelector(headerSelector); return header?.closest(XUEQIU_CARD_SELECTOR) === card ? header : null; @@ -10,7 +10,7 @@ const headerTargets = (card: Element): PlatformTarget[] => { const header = ownHeader(card); if (!header) return []; const targets = new Map(); - for (const link of Array.from(header.querySelectorAll('a.time[href], a[class*="date-and-source_"][href]'))) { + for (const link of Array.from(header.querySelectorAll('a.time[href], a.date-and-source[href], a[class*="date-and-source_"][href]'))) { if (!platformText(platformReadableCopy(link))) continue; const target = platformTarget(safePlatformUrl(link.getAttribute("href") || "", "https://xueqiu.com/")); if (target?.platform === "xueqiu") targets.set(target.url, target); @@ -18,7 +18,7 @@ const headerTargets = (card: Element): PlatformTarget[] => { return [...targets.values()]; }; const profileOwner = (header: Element | null): string | undefined => { - const author = header?.querySelector('a.name[href], a[class*="user-name_"][href]'); + const author = header?.querySelector('a.name[href], a.user-name[href], a[class*="user-name_"][href]'); if (!author || !platformText(platformReadableCopy(author))) return; const href = safePlatformUrl(author.getAttribute("href") || "", "https://xueqiu.com/"); if (!href) return; diff --git a/apps/extension/vertical-clip.test.mjs b/apps/extension/vertical-clip.test.mjs index 7c25db5b..61099472 100644 --- a/apps/extension/vertical-clip.test.mjs +++ b/apps/extension/vertical-clip.test.mjs @@ -177,11 +177,12 @@ for (const selector of [".article__container", ".article__bd__detail"]) test(`Xu expect(readXueqiu(doc, platformTarget(xueqiuUrl), xueqiuUrl).ok).toBe(false); }); -test("Xueqiu pointer targets the own header in a real truncated feed; preview requires opening its detail", () => { - const window = createWindow(fixtureText("xueqiu-feed"), "https://www.xueqiu.com/"); +for (const classic of [false, true]) test(`Xueqiu ${classic ? 'classic signed-in' : 'module'} feed pointer targets only its own header; preview requires its detail`, () => { + const html = classic ? fixtureText("xueqiu-feed").replace(/(?:style_)?(timeline__item__(?:main|info)|date-and-source|user-name)_[\w-]+/g, '$1') : fixtureText("xueqiu-feed"); + const window = createWindow(html, "https://www.xueqiu.com/"); const [script] = bundleTestScripts(["social-target"]); runInContext(script, createContext({ document: window.document, location: window.location, Element: window.Element, Node: window.Node, URL })); - const cards = [...window.document.querySelectorAll('[class*="timeline__item__main_"]')]; + const cards = [...window.document.querySelectorAll('.timeline__item__main, [class*="timeline__item__main_"]')]; expect(cards).toHaveLength(2); const target = xueqiuCardTarget(cards[0]); expect(target.url).toBe("https://xueqiu.com/6114376649/411862401"); diff --git a/docs/browser-clipper-acceptance-588.md b/docs/browser-clipper-acceptance-588.md new file mode 100644 index 00000000..15d62c08 --- /dev/null +++ b/docs/browser-clipper-acceptance-588.md @@ -0,0 +1,379 @@ +# Browser clipper live acceptance: Issue #588 + +[简体中文](browser-clipper-acceptance-588.zh-CN.md) + +Updated: 2026-10-18. Tracks [#588](https://github.com/tianma-if/edgeever/issues/588). This is an in-progress record, not a claim of complete cross-browser acceptance. + +## Environment and baseline + +- Code: upstream `67f627cc3c702c9cf2fa3fab9fabc67c70206888`; local `main` sync commit `7bd005ec`, with the instance-permission, SPA-message, Facebook alias/UI-text, current Jike Web detail, Android initialization and cross-document cancellation fixes applied on top. +- OS: macOS 27.0.1 (26A434), Apple Silicon; no Windows, Linux or physical Android coverage in the initial run; see the Android section for the USB follow-up status. +- Extension: `0.1.17`, Chromium/Firefox builds from this repository; older development builds with the same version are distinct. +- Chrome: 155.0.8059.80 installed; current repository build awaits loading. +- ego lite: 0.6.0.12, source page reports Chrome/154.0.0.0; now loads current repository `apps/extension/dist`, ID `edmkendapkebphaoiiogflldcjllbmca`, with no runtime/manifest errors. The older development extension was replaced. +- Edge: 155.0.4283.62; native extension UI shows `0.1.17` and the same development-path ID. HN/Xiaoyuzhou toolbar saves completed. Firefox 158.0 loads `dist-firefox` in an isolated temporary test profile. +- Local instance: `http://127.0.0.1:8787`, Web `http://127.0.0.1:5173`; startup/health is recorded separately from successful clipping. +- Before clipping: 40 memos, 74 resources, one API token, 13 notebooks; clean only IDs created by this run. + +Live records span build iterations. Round twelve defines the latest targeted runtime checks and build boundaries; Firefox retains each historical build boundary. Building a new package does not retroactively extend runtime acceptance. + +## Platform matrix + +Historical sample states and expectations come from [sources.json](../apps/extension/fixtures/platforms/sources.json) and [prior records](browser-clipper-platforms.md). Historical evidence does not become a current-run pass. + +`—` = current login condition is not applicable, without claiming restricted-state acceptance; `P` = the specified sample/entry completed save/readback; `P*` = partial, see limitations; `F` = native save failed, diagnosis pending; `B` = external access/browser environment block; `T` = incomplete. First-browser results are from ego lite, not acceptance of Google Chrome. Historical samples, entries and login states are not exhaustive. Chrome/Chromium does not add a separate native Chrome installation gate. + +| Platform / source | Login state and coverage | Expected | ego lite / Chromium | Edge | Firefox | +| --- | --- | --- | --- | --- | --- | +| Facebook
[facebook](https://www.facebook.com/NASA/posts/pfbid0dPToN66CzcAHGMoaNe3jszH4FfBKvqkE5gogCNJkTxXDPsSGYrGvcVwsLgByyMQQl) | Signed-in detail/feed expansion in ego lite; Edge public detail (one image) and signed-in feed expansion (three images); Firefox public Float detail (three images) and controlled navigation | Body, author, time, one image; exclude comments | P | P* | P* | +| Instagram
[instagram](https://www.instagram.com/p/DeAYvsnn4la/) / [instagram-slide-captions](https://www.instagram.com/p/DeKe6teEpya/) | ego lite/Firefox: NASA 3/3 and Google 20-media; Edge: public Google 20-media | Three-image sample / one video cover and 19 photos, full combined caption | P | P* | P | +| LinkedIn
[linkedin](https://www.linkedin.com/posts/friends-of-nasa_nasa-space-astronomy-activity-7497764718095323136-NRvr) / [linkedin-ugc](https://www.linkedin.com/posts/nasa-space-astronomy-ugcPost-7497764716677758977-qmKO/) / [linkedin-ugc-guest](https://www.linkedin.com/posts/nasa-space-astronomy-ugcPost-7497764716677758977-qmKO/) / [linkedin-person](https://www.linkedin.com/posts/maddyahmad_softwareengineering-artificialintelligence-share-7456582751698190336-6NVK/) | ego lite signed-in UGC and two neighboring company-feed cards; Firefox guest personal share/UGC; Edge signed-in SDUI UGC detail with eight images passed | Distinct activity/ugcPost/share identity, author/job title, complete body and image order | P* | P* | P* | +| Threads
[threads](https://www.threads.com/@nasa/post/DZceA72Drjf) / [threads-short-link](https://www.threads.com/@nasa/post/DeMqtuYgkaL) | ego lite signed-in four-image/short-link samples passed; Firefox signed-in details and exact NASA profile-feed card passed; Edge guest overlay correctly refused; signed-in four-image/short-link details and round eleven text/photo quotes passed | Complete caption, four images / one video cover, actual shortened-link destination | P* | P* | P | +| Bluesky
[bluesky](https://bsky.app/profile/bsky.app/post/3mhgg5ja5gc2w) / [bluesky-feed](https://bsky.app/profile/bsky.app) | Public details and exact quoted feed card passed in ego lite/Firefox; Edge signed-in quoted feed with two images passed | Exact card, body, quote and original image; exclude neighboring cards | P | P* | P | +| Hacker News
[hacker-news](https://news.ycombinator.com/item?id=40380738) | Public signed-out detail; toolbar in three browsers, context menu in ego lite | jnnnthnn, 92 points, final main-post paragraph and external link; exclude comments | P | P* | P | +| V2EX
[v2ex](https://www.v2ex.com/t/1000000) | Public detail in ego lite via toolbar | Main post, both supplements and their times; exclude replies | P | P | P | +| 小宇宙 / Xiaoyuzhou
[xiaoyuzhou](https://www.xiaoyuzhoufm.com/episode/62ad4ae5f9b62781ea058a02) / [xiaoyuzhou-hosts](https://www.xiaoyuzhoufm.com/episode/612375e57f89405077cbeb3b) | Public episode; toolbar saves and note reopen in three browsers | Complete Show Notes, timeline, explicit hosts and cover; no stale episode after navigation | P | P* | P | +| 雪球 / Xueqiu
[xueqiu](https://xueqiu.com/5636485254/253443400) / [xueqiu-feed](https://xueqiu.com/) / [xueqiu-feed-detail](https://www.xueqiu.com/6114376649/411862401) / [xueqiu-feed-detail-second](https://www.xueqiu.com/2754211668/411841572) | Public details in three browsers; ego lite truncated public feed fetched complete body; Edge signed-in recommended home card with complete body/eight images passed | Exact card, complete article, author, explicit stock tags and images | P* | P* | P* | +| 微信公众号 / WeChat
[wechat](https://mp.weixin.qq.com/s/TC-ONpL3PGklsD0yhtcuBQ) | Public article in ego lite via toolbar | Body hierarchy, author/publication, 21 images; exclude footer widgets | P | P | P | +| 即刻 / Jike
[jike](https://m.okjike.com/originalPosts/66ac89b0314f5cced41012d3) / [jike-grid](https://m.okjike.com/originalPosts/6715e824d9dc81724c2ec74d) / [jike-long](https://m.okjike.com/originalPosts/64c9dd9a2edae6abdc610605) / [jike-feed](https://m.okjike.com/topics/63579abb6724cc583b9bba9a) | Public topic card and signed-in Web detail/gallery; ego lite current feed passed two expanded long posts and one image post; Edge signed-in following card passed; round twelve keeps nine image positions in all browsers, with honest 8/9 archival in Edge | Post identity, long-text expansion, grid/original images; never capture whole feed | P | P* | P* | +| Substack
[substack-note](https://substack.com/@tedgioia/note/c-357811021) / [substack-article](https://www.oneusefulthing.org/p/the-dot-and-the-swarm) / [substack-paywall](https://www.lennysnewsletter.com/p/how-to-build-ai-product-sense) | Notes in three browsers; Full article and 3/3 images in all three browsers; user-triggered ego lite toolbar; paid-preview refusal/selection passed | Five Note paragraphs / article sections and captions; reject restricted full capture, permit explicit selection | P* | P* | P* | +| Medium
[medium-article](https://medium.com/@scott.batson_76410/what-should-programmers-do-while-the-ai-writes-code-a2ce2e6de88e) / [medium-public](https://scottbatson.dev/come-to-my-medium-day-talk-17d75b342aeb) | Edge current member preview; public full story passed in Edge/Firefox; ego lite public full story/preview card and two images passed | Original: 11 visible text blocks/one image; earlier Edge/Firefox public story: 16 text blocks, five list items/one image; final ego lite: subtitle, 17 body/preview blocks and two images; rounds eleven/twelve verify 40 code blocks/highlighting in all three browsers; original member full story remains unclaimed | P* | P* | P* | +| 少数派 / Sspai
[sspai](https://sspai.com/post/92653) | Public article: ego lite/Firefox archive nine images; Edge archives 8/9 with correct feedback and reopening | Own authors, update blockquotes, original time and nine images; CDN archival | P | P* | P | +| 掘金 / Juejin
[juejin](https://juejin.cn/post/6972051526416072717) | Public article: ego lite/Firefox toolbar saves, two archived images and reopening | Exact code, three-column/four-data-row table, two images and time | P | P | P | +| 豆瓣 / Douban
[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/) / [douban-comments](https://book.douban.com/subject/36864764/comments/) | ego lite/Firefox book/movie/music reviews, diary and exact short comment passed; Edge movie/music/diary/comment now added | Book/movie/music reviews, diary, exact short-comment card; book review seven paragraphs/six images; Edge specified other four types now covered | P | P | P | +| BOSS 直聘 / BOSS Zhipin
[boss-login-required](https://www.zhipin.com/job_detail/0d7e760b0a0e7c3a0nF939S0E1NU.html) / [boss](https://www.zhipin.com/job_detail/0d7e760b0a0e7c3a0nF939S0E1NU.html) | This issue tracks restriction feedback; round twelve Edge refuses the actual preview with zero writes; ego lite is signed in/full-readable | Firefox verifies refusal/visible selection; full jobs remain #589 | — | P* | P* | + +## Procedure per browser and source + +1. Record full browser version, extension version/path, code SHA, actual source URL, login state, visible final paragraph and image count. Establish expectations before saving. +2. Save a real detail page through the native toolbar, then its native context menu. For supported feeds, right-click one exact card. Record entries separately; direct function calls or synthetic DOM events do not replace native actions. +3. Check feedback, title, beginning/end, author/job title, publication precision, source and tags; exclude neighboring cards, comments and login widgets. +4. Compare source image inventory with instance attachment URLs, counts, hashes and browser decoding. Block source/CDN domains, close and reopen the memo, and verify body/attachments. This does not establish cold-start offline support. +5. Record timestamp, entry, memo ID, attachment counts, feedback, result and evidence path. Results may pass, fail or be externally blocked; page readability is not successful clipping. +6. Delete this run's memos/resources/tokens; compare initial ID sets to preserve existing data. Clean only this run's objects. + +## Signed-in and failure cases + +| ID | Scenario and expected result | Current result | +| --- | --- | --- | +| B01 | Signed-in social feeds / Jike Web: expand complete text, retain one identity and all images, exclude neighbors. | Facebook/Threads signed-in feeds and this round’s Jike Web long/image cards plus neighboring LinkedIn company-feed cards passed; browser/layout limits remain in the matrix | +| B02 | Navigation/card reuse during expansion: cancel stale task without a wrong-identity or partial-text memo. | Controlled SPA cancellation passed; after the new fix, real Facebook expansion with controlled cross-document navigation created no memo in ego lite and Firefox. Natural card recycling remains pending | +| B03 | Login/paywall restriction: refuse full capture and hidden content; permit explicit visible selection. | Substack full paid-preview refusal passed; explicit 221-character visible selection saved and reopened in ego lite. Firefox BOSS login-preview refusal and explicit 117-character selection/save/reopening passed | +| B04 | Some real image downloads/uploads fail: retain body, report remaining external images, never claim complete archival. | Initial Threads 3/4 and follow-up Edge Sspai 8/9 matched partial-image feedback. Threads later passed 4/4; failure causes remain unconfirmed | +| B05 | Reopen after blocking source/CDN: fully archived attachments load from the instance and decode. | Specified earlier cases passed; second follow-up decoded 34 Firefox, 33 Edge and 3 ego lite instance images under local-only image/connection policies | +| B06 | Cleanup: preserve initial memo/resource/token ID sets and remove this run's objects. | Earlier rounds passed. Second follow-up removed 19 notes, 80 resources and one token, preserving original ID sets and all 40 note content hashes | + +## Firefox Android + +The manifest declares `gecko_android.strict_min_version = 142.0`, and the extension README also declares Android 142+. On 2026-10-17, the [Mozilla store API](https://addons.mozilla.org/api/v5/addons/addon/edgeever-web-clipper/) listed public version 0.1.17 for both Firefox Desktop 140+ and Android 142+. Android compatibility is therefore currently declared for the same Firefox extension; this is separate from the EdgeEver Android app. Neither the declaration nor the store listing proves runtime acceptance. The user subsequently connected a physical RMX5200 over USB (Android 17, arm64). Mozilla Firefox 158.0 was installed after APK signature verification. `web-ext` temporarily loaded extension 0.1.17 from the current workspace. A dedicated token and USB mapping connected to the local instance; after the user granted host access, the extension retrieved 13 notebooks. Temporary loading does not prove store installation or extension upgrades. + +The device first reproduced a background initialization failure: Android lacks `chrome.contextMenus`/`chrome.windows`, and unconditional desktop listener registration threw `TypeError`. The popup showed “Unknown,” and saving failed with `Receiving end does not exist`. Menu/window listeners now register only when the APIs exist, and install-time menu registration checks availability. Extraction, permissions, and saving remain unchanged. A regression failed before the fix and now verifies initialization, capture-mode detection, and saving without these APIs; the existing desktop menu installation regression also passes. These three guards can be reverted independently. The main risk is disabling desktop entry points accidentally, covered by the desktop menu test. + +| Physical-device case | Result and evidence | +| --- | --- | +| [Sspai: time-management tools](https://sspai.com/post/92898) | Saved from the native extension menu; author 小雨姑娘, date, and source match. The complete body recorded before capture is preserved, with 5/5 images archived. | +| [Jike H5: nine-image post](https://m.okjike.com/originalPosts/6715e824d9dc81724c2ec74d) | Saved from the native entry; target author 彗星咻咻咻 and body preserved, 9/9 images archived, comments excluded. | +| Reopen both notes on the phone | The actual Web editor displayed matching bodies under a CSP allowing only local images/connections. All 14 instance attachments decoded; external requests produced real CSP violations. File sizes and SHA-256 hashes matched all 14 stored resource records. | +| Hacker News | Source connection timed out on the phone; no note created. This is neither a clipping pass nor a demonstrated clipping failure. | + +Coverage is limited to “Clip current page” in the Android Firefox extension menu, for an article and a public social detail page. Android has no desktop context-menu/separate-window entry for this extension. Single-card feeds, native selections, signed-in sites, and remaining matrix cases are unverified on the device. After the fix, **453 extension tests passed, 0 failed**; Web/mobile type checks, Web build, and both extension builds passed. The 2 test notes, 14 resources, and dedicated token were removed by ID. The phone's test token and 7 copied local-session cookies were removed. All original 40 note content hashes and the exact 40/74/1/13 note/resource/token/notebook ID sets were preserved. + +## 2026-10-18 overall bugfix-review-loop + +Scope is all current uncommitted #588 extension code, fixtures, regressions and acceptance records. The unrelated `apps/mobile/.gitignore` change is preserved; work remains on `main`. Correctness, simplification and requirement boundaries are checked, with independent read-only reviews of identity/navigation and content conversion. + +Three classes of omission are repaired. Jike Web removed button-semantic nodes together with their images, reporting a complete nine-image post with only eight images. It now reuses H5's existing media-preservation rule: confirmed images remain, image buttons are unwrapped and actual controls are removed, through one small shared helper. Jike and Medium rejected whole `visibility:hidden` body wrappers, losing descendants explicitly restored with `visibility:visible`; existing `platformReadableCopy` now filters those descendants individually. Hidden-card and identity protections remain. Medium skipped every `h1`, losing later body headings; it now skips only its own first readable title. + +Five evidenced regressions cover three image/container button variants, Medium restored visibility/body headings, and Jike restored body/media. They reproduce failures before repair and pass afterward. Final review finds no further substantive issue. No permission, API, dependency, retry or required metadata is added. Repairs are confined to the two readers and shared control cleanup and can be reverted independently. Visibility/media filtering remains the main risk, constrained by hidden-content, image-order, completeness, unchanged-source-DOM and existing identity regressions. + +The latest complete non-E2E run passes **3,743 tests with zero failures**, including **481 extension tests**. One real Windows cwd-lock case is skipped on macOS under its existing platform condition. Root/mobile typechecking, Web/Chromium/Firefox builds and diff checks pass. Firefox lint has zero errors, zero notices and seven existing dependency warnings. This is code/automated verification, without repeating native live-site/Android actions or retroactively claiming latest-build hardware acceptance. No business test clip or token is created; login settings remain. No commit, push, deploy, issue update or closure. Evidence: `/private/tmp/edgeever-issue-588-review-20261018`. + +## 2026-10-18 tenth round: Douban, Medium and formatting repairs + +The retained Edge window and local instance are reused. The user authorizes new ego lite space 21 and completes one Medium human verification. Existing signed-in profiles are reused without copying or clearing cookies. All saves use actual toolbars/native context menus; helpers only observe sources and add QA markers. + +| Case | Actual save and source comparison | +| --- | --- | +| Edge Douban movie review `17798662` | Toolbar save preserves title, author, complete body and 2/2 images. | +| Edge Douban music review `17855287` | Toolbar save preserves title, author, complete body and 3/3 images. | +| Edge Douban diary `814698224` | The live source adds `_i`; actions use that actual tab URL, while the stored source normalizes correctly. Complete body and 6/6 images are preserved. | +| Edge Douban book comment `4237804504` | Native context-menu save targets the selected card, preserving body, author, rating and own permalink. No image; avatars are excluded. Text-selection misclicks are not counted as card acceptance. | +| Edge same eight-image Xueqiu article | All 96 visible nonempty body blocks are rechecked. The affected span now stores a real `bold` mark without literal stars. 8/8 images are archived. | +| Edge same Bluesky quote `3my43eyfzs225` | Own/quoted body and 2/2 full-size images remain, including image alt descriptions; the localized alt badges no longer enter prose. | +| ego lite public Medium story `17d75b342aeb` | Actual toolbar save finally preserves subtitle, 17 body/preview text blocks, five list items, explicit ending and 2/2 images, including the complete preview destination. | + +Two formatting repairs are made. CommonMark cannot close bold after Chinese punctuation immediately before unspaced prose; shared HTML conversion moves only that boundary punctuation outside bold, preserving all characters and literal code. The punctuation itself is no longer bold. Bluesky image-viewer buttons retain images and alt descriptions; the observed immediately adjacent, absolutely positioned `ALT`/localized badge is removed. Ordinary prose containing the same words remains. The first repair affects shared HTML conversion; the second is Bluesky-only. Each can be reverted independently. The worst risk is formatting/caption loss; regressions check stored nodes, literal prose/code, alt descriptions and unchanged source DOM. + +Paragraph comparison also exposes a real Medium omission: the in-story preview lacks `[data-selectable-paragraph]`, so existing block selection loses its title, description, destination and CSS cover. A failing regression demonstrates this before adding article-scoped titled link cards. Labels become valid links in source order, with visible CSS background images and existing images retained. Existing visibility/URL sanitation remains, excluding hidden cards and outside recommendations. No paid content or API is added. Upstream structure changes are the main risk; the Medium reader change can be reverted independently. Instance APIs, authentication, storage and migrations are unchanged. The repaired native toolbar save uses the same public article without refreshing it into a verification page. + +All six Edge notes reopen in Edge's own editor with source/CDN access blocked, matching stored text and decoding all 21 local images. Main-world probes record 27 blocked connections and 27 blocked image loads for six source pages and 21 CDN addresses. The final Medium note also reopens in ego lite's own editor with its source/two CDN URLs blocked and both local images decoded. The earlier one-image Medium note remains evidence of the omission and is excluded from final completeness acceptance. + +This round creates eight notes and 24 resources, including the pre-repair Medium note. All 24 API downloads match resource sizes/SHA-256 and independently downloaded originals byte for byte. The last diary source initially returns 418 to the independent fetch; the correct site Referer produces matching bytes. Product downloading/retry behavior is unchanged. All 40 original content hashes remain intact. + +All **476 extension tests pass**, with root/mobile typechecking, Web/Chromium/Firefox builds and diff checks passing. Three-view review finds no outstanding substantive issue. The six Edge cases use this round's preceding build containing both formatting repairs. The final build additionally changes only the Medium reader and is tested in ego lite. Firefox's new package only builds; no runtime claim is extended retroactively. + +All eight owned notes and 24 resources are deleted, the dedicated token is revoked and returns 401. Only this round's token is removed from both browsers; other extension settings and site logins remain. Exact original 40/74/1/13 note/resource/token/notebook ID sets and all content hashes match. Temporary helpers/receivers are removed/stopped, product hashes match the final build, and the test space is cleaned up. Evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/tenth`. No commit, push, release or issue closure. + +## 2026-10-18 Edge signed-in feed ninth-round follow-up + +The user signed in to Xueqiu, Facebook, Jike and Bluesky in the retained Edge window. This round uses the rebuilt and reloaded Chromium extension. The Firefox package only builds; earlier Firefox runtime results do not become acceptance of this new code. + +The signed-in Xueqiu home uses classic `.timeline__item__main`, `.timeline__item__info`, `a.date-and-source` and `a.user-name` classes without module suffixes. Existing selectors only handled suffixed classes, so exact feed-card targeting failed. The repair adds these four classic selectors; pointer tracking and pre-save checks still share the locator, own-author/permalink checks and same-origin detail reading. Scope is Xueqiu targeting; the worst risk is selecting another card. Reverting the four selector additions rolls it back; regression and live checks verify identity and exclude neighbors. The new classic-layout regression fails before the repair and passes afterward. All **473 extension tests pass**, with root/mobile typechecking, Web and both extension builds passing. Correctness, simplification and requirement-boundary review finds no new substantive issue. No API, permission, retry or required metadata is added. + +| Native single-card context-menu case | Source and observed result | +| --- | --- | +| Signed-in Xueqiu recommended home feed | `1843761023/412754286`: the truncated card resolves to its full public detail, with correct title, author and source; text from all 96 visible nonempty body blocks and 8/8 images is preserved. Hidden attribution is excluded from expected body text. This is the recommended feed, not a personal following feed. | +| Signed-in Facebook NASA feed | `pfbid0kAjTCFHeSmCB9TePMvGo3PhbNbh5jFVu3iUpC5HodB45dgpke1fCdRZQHaJr7hXYl`: initially only the first sentence and expand control are visible; the native action expands and preserves the complete caption and 3/3 images, excluding the neighboring aircraft post and comments. | +| Signed-in Jike following feed | `6ad3ff33b6e171bc9b673a51`: current Web card body, eight full external links and four author mentions are retained. Its own picture declaration is empty; link previews/avatars are not counted as post photos. The preceding day's post by the same author is excluded. Body is already presented, so this adds no automatic-expansion claim. | +| Signed-in Bluesky profile feed | `bsky.app/post/3my43eyfzs225`: own body, Josh Sternberg's quoted body and 2/2 full-size images are retained, excluding neighbors. Edge now has an actual save result rather than a loading block; other detail layouts retain their matrix limits. | + +All four notes reopen in Edge's own Web editor with text matching stored nodes; all thirteen instance images decode. Each attachment matches an independently downloaded original byte for byte in order; API downloads match resource sizes/SHA-256. A page policy permits only local instance images/connections. Main-world probes record 17 blocked connections and 17 blocked image loads for the four source pages and thirteen CDN URLs while local attachments remain readable. Helpers only observe sources, read back the instance and add QA markers. Every save uses the native context menu; selected-text misclicks and actions that never dispatch are excluded. + +Two minor formatting observations remain: one Xueqiu bold span shows literal `**`, and each Bluesky quoted image includes a localized alt-text badge. Text and images are intact, including image alt descriptions. This is not pixel-perfect or exhaustive formatting acceptance; no translation- or style-dependent guessing rule is introduced to strip those badges. + +The four owned notes, thirteen resources and dedicated token are removed; the revoked token returns 401. All original 40 content hashes and exact 40/74/1/13 note/resource/token/notebook ID sets match. Only this round's extension token is removed; other extension settings and site logins remain. Browser cookies were neither replaced nor cleared. Temporary extension helpers and receivers are removed/stopped, and product hashes match this round's final build. Evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/ninth`. No commit, push or release. + +## 2026-10-18 Medium eighth-round follow-up + +The user first completed Edge's human verification on `scottbatson.dev`. The original [What Should Programmers Do While the AI Writes Code?](https://scottbatson.dev/what-should-programmers-do-while-the-ai-writes-code-a2ce2e6de88e) now displays “Create an account to read the full story” and a members-only notice, unlike its historical full-body state. Human verification does not establish access to paid content. + +- Edge's native toolbar saved the original sample's **currently visible preview**: title, subtitle, author, source, all 11 visible text blocks including the caption, and 1/1 image. Terminal feedback confirms all images archived. The hidden original ending is absent and image-viewer prompts are excluded. This verifies visible-content boundaries, not the original full two-image article, code or sections. +- The same author's profile links to [Come to my Medium Day talk](https://scottbatson.dev/come-to-my-medium-day-talk-17d75b342aeb), which is fully readable without a membership gate. Edge's native-toolbar save preserves the title, subtitle, author, all 16 distinct text blocks, five list items and explicit ending “Hecklers are permitted but not encouraged”, with 1/1 image archived and complete feedback. Another technical story redirects to `medium.engineering` and encounters that domain's verification; it is not a save pass. +- The user separately completed human verification for the same public article in the original Firefox QA profile. Firefox's native-toolbar save preserves the complete source text, five list items, ending and 1/1 image, with all-images-archived feedback. No Cloudflare verification cookie was copied and no site verification was bypassed. + +Both Edge notes and the Firefox note reopened in their own Web editors with matching stored bodies and all three instance images decoded. Each attachment matches a fresh independent source download by size/SHA-256; all API downloads also match metadata. Only the QA Web pages restrict external images/connections. Edge's main-world probes record four `connect-src` and two `img-src` rejections for both articles and real CDN images. Firefox separately records two `connect-src` and one `img-src` rejection for its article and real CDN image. Edge's reopening helper needs the local Web tab foregrounded for UI rendering; earlier operations without returned evidence are excluded and the completed rerun was checked individually. + +All three notes and three resources were deleted by ID. The dedicated token was revoked and verified to return 401; only matching extension tokens were removed in Edge/Firefox. Firefox initially had no local session cookie. Its one copied `127.0.0.1` session changed value during Web readback; cleanup removed only that QA host/path/name session and verified none remained, preserving other site logins and verification cookies. Edge's original cookies were not replaced. All ten temporary extension helper files, two helper services and plaintext credentials were removed. Product artifact hashes are exactly unchanged. Original 40 memo content hashes and exact 40/74/1/13 memo/resource/token/notebook ID sets are preserved. + +No product code changed; previous 472-test/build results still apply. Nothing was committed, pushed or released. Medium in ego lite was not rechecked this round. The original members-only full story and other Medium layouts such as code do not inherit a pass from the public short article. Evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/eighth`. + +## 2026-10-18 Edge signed-in seventh-round follow-up + +The user signed into Threads and LinkedIn in the retained Edge QA window `1529310870`. The same profile and host grant were reused. The former ego task space 12 no longer exists, and no replacement space was created. Edge's original local session returned 200 through the actual API; a dedicated token was created through the normal owner API without replacing or copying browser cookies. + +- Threads four-image detail `DZceA72Drjf`: the login overlay is gone and the native-toolbar save reports all images archived. The complete three-paragraph caption, NASA author, publication time and 4/4 images are preserved. +- Threads shortened-link detail `DeMqtuYgkaL`: native-toolbar saving preserves all four caption paragraphs, the `go.nasa.gov/Artemis2LunarScience` destination, author/publication time and one video cover, with all-images-archived feedback. A cover is not a video-file archive. The source helper's raw `innerText.includes` prefix boolean is not an integrity criterion; final acceptance compares the complete normalized public source caption with saved text, rather than only checking that potentially incorrect storage reopens consistently. +- The same LinkedIn UGC detail `7497764716677758977` uses the signed-in SDUI layout. Its own card binds to its activity identity, is not hidden and has no login overlay. Native-toolbar saving preserves the entire source body, Friends of NASA author and 8/8 images, with all-images-archived feedback. `Dr. Nancy Grace Roman:` remains literal text. This detail is already expanded, so automatic expansion or feed-card coverage is not claimed. + +All three notes reopened in Edge's own Web editor with matching stored body nodes and all 13 instance images decoded. Every attachment matches a fresh independent source download in order; all 13 API downloads match metadata sizes/SHA-256. The page allows only instance images/connections. Synchronous main-world probes record four actual `connect-src` rejections for Threads, LinkedIn and both real CDN images, plus two `img-src` rejections, while local images still decode. These are the specified detail samples; other feeds, adjacent cards and natural recycling do not inherit a pass. + +Edge's Medium sample redirects to `scottbatson.dev` but remains on Cloudflare security verification. The page is retained for the user to complete verification; no challenge page is clipped as an article. A BOSS guest page was observed only: this visit directly displays its job description, without reproducing the older login-to-view state. No save was triggered and no BOSS acceptance pass is added. + +All three test notes and 13 resources were deleted by ID. The dedicated token was revoked and verified to return 401. Only the matching extension token was removed, with other settings unchanged. Eight temporary helper files, two local helper services and plaintext cookie/token files were removed; product artifact hashes are exactly unchanged. Original 40 memo content hashes and exact 40/74/1/13 memo/resource/token/notebook ID sets are preserved. Original browser cookies were unchanged; Threads/LinkedIn logins and host grants remain. The QA window retains only both sites and Medium for follow-up. + +No product code changed in this round; the previous 472-test/build results still apply. Nothing was committed, pushed or released. Evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/seventh`. + +## 2026-10-18 Firefox and Edge sixth-round follow-up + +- Firefox passed the native-toolbar Google Instagram 20-media sample `DeKe6teEpya`, with terminal feedback confirming all images archived. The optional public guest-page login prompt was dismissed normally without changing visibility checks. The complete source caption, author, one video cover and 19 photos are preserved. This source places its complete caption in the parent post field, with no separate slide-caption fields; another caption layout is not claimed. All 20 archived files match fresh independent source downloads in order. Reopening in Firefox's own Web editor with external images/connections restricted preserves the body and decodes all 20 local images; an actual Instagram CSP rejection was recorded. +- Firefox added native-toolbar Douban movie review `17798662`, music review `17855287` and diary `814698224`, preserving full source bodies, titles and authors with 2/3/6 archived images. Native context-menu capture of short comment `4237804504` was preceded by target-marker verification. It preserves the author, complete 130-character body, list URL and `#c-4237804504` fragment, excluding neighboring comment `4259416906`. All four notes passed source-blocked reopening in Firefox, with matching bodies and all 11 images decoded. Window activation/right-click fixes affect QA tooling only; pointer actions that missed the card are excluded from product failures. +- Edge added native-toolbar saves of the same Google Instagram 20-media sample and the NASA public Facebook detail `pfbid0dPToN66CzcAHGMoaNe3jszH4FfBKvqkE5gogCNJkTxXDPsSGYrGvcVwsLgByyMQQl`, both reporting all images archived. Instagram preserves the complete caption and order, with all 20 files matching fresh independent downloads. Facebook preserves the complete post, author, exact time and one image, excluding comments. Its star emoji is rendered as an image and absent from source `innerText`; headline and paragraph were checked separately to avoid a false body-loss report. +- Both Edge notes reopened in Edge's own Web editor with matching bodies and all 21 local images decoded under local-only `img-src`/`connect-src`. Isolated-extension probes alone do not establish main-page request blocking. Final synchronous main-world injection starts the probes and a subsequent synchronous read records actual `connect-src` rejections for Instagram, Facebook and the real source CDN image, plus an `img-src` rejection for that image. Earlier asynchronous main-world probes did not return usable evidence and are excluded. Global browser security settings were unchanged. +- Edge Threads has a guest login overlay: native saving refuses with login guidance and creates no note. LinkedIn initially renders its full guest body, then displays a login overlay and marks `main` as `aria-hidden="true"`; the actual native save safely refuses with no new note. Background `innerText` does not establish readability, and established visibility checks were preserved. Both login pages remain in the dedicated window for user sign-in; Edge signed-in acceptance is pending. One earlier mouse action did not dispatch saving; the real native AX refusal is recorded separately. +- Firefox's specified Medium sample remains on Cloudflare verification without an article body. This is an access block, not a clipping failure or save pass. + +This round has seven qualified test notes and 52 archived resources: Firefox 31, Edge 21. The 40 Instagram files additionally match independent source downloads in order. All 52 API downloads match metadata sizes/SHA-256. Seven notes and 52 resources were removed by ID; the dedicated token was revoked and verified to return 401. Only matching extension tokens were removed. Firefox's seven copied local cookies were removed while website logins and host grants remain. Edge's original local session cookie was restored and verified through browser readback. All 18 temporary extension helper files, two helper services and plaintext credential files were removed/stopped; product artifact hashes are exactly unchanged. Original 40 memo content hashes and exact 40/74/1/13 memo/resource/token/notebook ID sets are preserved. + +This round adds acceptance evidence and documentation only, retaining the previous 472-test/build results. Nothing was committed, pushed or released. Evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/sixth`. + +## 2026-10-18 ego lite native-toolbar follow-up + +In the same task space, the user manually opened the extension and supplied a supported-page screenshot, then clicked Save current page. The instance received two test notes for [The Dot and the Swarm](https://www.oneusefulthing.org/p/the-dot-and-the-swarm). Both retain the title, author, publication time and all 26 current source body/section fragments, with 3/3 images archived each. Source collection excludes two explicit book-order/share buttons and uses the article title rather than the site-navigation heading. The final notification text was not supplied, so this case does not claim its wording was verified. + +Both notes were reopened in ego lite's own Web editor and matched stored body nodes. A policy limited external images/connections only on that Web page; actual source-site and Substack CDN probes were blocked by CSP while all six instance images decoded. API downloads of all six attachments matched metadata sizes and SHA-256 digests. The earlier Unavailable status occurs when automation targets the native toolbar; it remains an automation window-targeting limitation. Recognition, saving and source-blocked reopening through the manually triggered native entry now pass, without parser or permission changes. + +Both test notes and six resources were removed by ID. The dedicated token was revoked and verified to return 401; only the matching extension token was removed, and the plaintext credential file was deleted. All original 40 memo content hashes and exact 40/74/1/13 memo/resource/token/notebook ID sets are preserved. This follow-up changes acceptance documentation only and retains the previous 472-test/build results. Nothing has been committed, pushed or released. + +## 2026-10-18 fourth follow-up: feeds and full articles + +- Three signed-in Jike Web feed cards passed native context-menu saves: same-author originals `6ad2adab709f50754cbd2921` and neighboring `6ad15c2977477d8a9f97733b`, plus topic image post `6ad399f0141b85b29268b593`. The extension clicked the real expand control for both long posts, preserving complete bodies, mention/topic links and their own identities while excluding neighbors and collapse controls. The image post preserved its author/caption and archived 1/1 original image. Independent source bytes matched the instance hash; the 1179×2556 instance image decoded after source/CDN blocking. Its first save retained an external image; a separate recheck succeeded. The first failure remains unexplained; no speculative retry was added. +- Current Jike feed cards have no own DOM permalink. Earlier checks correctly refused body-reference or Fiber/alternate guesses. New evidence identifies each selected card's committed DOM-host `__reactProps$…`, following element `props.children` to its own public original post. The reader returns only whitelisted ID/type/body/author identifier/image URLs, without account state or authenticated APIs. Complete visible body, author link and images must match, and the same marked node's current ID is checked again before writing. Only verified `ORIGINAL_POST` cards are supported; reposts, conflicts and missing state are refused. React internals remain an upstream-layout risk: changed structures fail closed rather than capturing the whole page. +- A controlled identity change during the real expansion click, leaving the original DOM marker/body in place, produced zero notes from the native context-menu action. This is controlled identity-change coverage, not natural scrolling/recycling acceptance. Live Chromium also exposed rejection of `executeScript.args = [undefined]`; initial calls now use an empty array, and the integration harness enforces this JSON argument constraint. +- Jike mention/topic layout divs produced invalid Markdown links containing blank paragraphs. After completeness validation, plain text labels now become inline content. Fresh native saves were compared with complete source bodies and parsed links, instead of merely reopening incorrect stored content. +- Public Bluesky profile feed post `bsky.app/3mxzez2m7wk2r` passed native context-menu saves in ego lite and Firefox, retaining the complete main/quoted text, preview title/description/destination and 1/1 original image without neighbors. Live checks exposed literal Markdown syntax around a block preview link. The repair keeps its complete label inline, separates its image and preserves spaces between text blocks. The real fixture failed before the repair and passed afterward. Final saves reopened in each browser under source blocking, with image hashes matching independent source downloads. An earlier zero-image read occurred before asynchronous archival finished; final states were subsequently checked. +- A public truncated Xueqiu home-feed card saved through its native context menu as `8378622539/412716009`, obtaining the complete public same-origin detail and explicit `SZ300308`, `SZ002384`, `SZ300476` tags. Source/author matched and neighbors were excluded. Source-blocked reopening passed. Existing stock `inlineMath` attributes and KaTeX annotations were checked separately before removing duplicated render text for body comparison. Signed-in home feeds were not tested in this round. +- Two same-author neighboring cards in NASA's signed-in LinkedIn company feed passed native context-menu saves: `activity:7516859007463047169` and `activity:7516590973359931392`. The extension expanded their real controls, which then disappeared. Complete bodies, distinct sources, author and 1/1 image each were retained without mixing posts. Both images matched independent downloads, and both notes reopened under source blocking. Only an informational website modal was dismissed; page-view visibility settings were unchanged. +- The complete public Substack article [The Dot and the Swarm](https://www.oneusefulthing.org/p/the-dot-and-the-swarm) passed native toolbar saves in Firefox and Edge: title, author, date, all 24 independently recorded body/section fragments and 3/3 images were retained. Both browsers reported complete archival; each reopened its own note under source/CDN blocking with matching body and three decoded instance images. Audio/video files were not archived. +- During fourth-round automation the same article showed Unavailable in ego lite's native toolbar, producing no note. Diagnostics show the target article window has the correct active tab while the browser API reports another normal window as last focused, with no URL on its active tab; automated native-toolbar interaction returns to that window even after explicit target-window focus. The user then manually opened the extension in the same task space and supplied a screenshot showing the supported-page status. Manual recognition passed; actual saving and source-blocked reopening were still pending at that point and subsequently passed as recorded above. Edge/Firefox passes do not extend ego lite acceptance or justify changing the established article parser or broadening permissions. This browser-window environment limitation remains under investigation. +- The final ten qualified notes and eleven archived attachments reopened in their own browsers with sources blocked: four images in ego lite, four in Firefox and three in Edge. Six reproduction/intermediate-build notes and three attachments are excluded from qualified results. All sixteen test notes and fourteen resources were cleaned by ID; all fourteen API downloads matched stored size/SHA-256. The original forty note hashes and exact 40/74/1/13 memo/resource/token/notebook ID sets were preserved. +- The dedicated token was revoked and returned 401; each browser removed only its matching QA token. Firefox's seven copied local cookies were removed while Threads login and host grants were retained. Edge's original local session value was restored and read back, and its owned window closed. Four temporary extension QA files and two local helper servers were removed/stopped, with product build hashes unchanged. Plaintext credential files were deleted. Ego lite retains the diagnostic page in task space 12 for continuation in the same space. +- The local Web session proxy temporarily timed out, and Wrangler's asset map remained stale after the Web rebuild. Restarting this task's `bun run dev` restored service. Production API/authentication/storage were unchanged. The final **472 extension tests passed with zero failures**; Web/mobile typechecks, Web build and both extension builds passed. New code is confined to the extension; Web/mobile checks do not imply new browser acceptance. Changes remain uncommitted, unpushed and unpublished. + +The final review covered correctness, removable complexity and requirement boundaries, with no further evidenced substantive code findings. Jike feed reading and both link-normalization repairs can be reverted independently. The main risks are identity mistakes after upstream layout changes and altered link formatting; identity/body/image checks and actual document-parsing regressions constrain them. Browser-window environment limits and untested matrix entries remain explicit. + +## 2026-10-17 third follow-up live acceptance + +- ego lite added Douban movie review, music review, diary, exact short-comment card, and the Threads shortened-link detail. Complete review/diary bodies and 2/3/6 images were preserved. Comment `4237804504` retained its author, complete body, working list URL and `#c-4237804504` fragment, excluding neighbor `4259416906`. Two native context-menu actions each saved one correct comment. Threads retained all four caption paragraphs, `go.nasa.gov/Artemis2LunarScience` and one video cover, excluding replies. +- Firefox added public signed-out Bluesky detail, the Instagram NASA three-image detail, and LinkedIn guest UGC. Bluesky preserved its body, author, publication time and original image, excluding the author's subsequent reply. Instagram preserved the full caption and three images without comments. Its terminal popup feedback was not captured, so feedback acceptance is not claimed; the saved result, archival and reopening were checked. After closing the visible optional sign-in modal, LinkedIn preserved the complete body, author, publication time and eight images. Its normalized source retained the same UGC ID, checked against the card's own `data-attributed-urn` and activity alias. An old QA helper mistakenly rejected the absence of the full slug after valid URL normalization; this was not a product defect. +- These eight qualified scenarios produced nine notes (including the two comment saves) and 24 instance attachments. All notes reopened in their own browsers under local-only source policies; bodies matched stored nodes, actual CSP violations proved external image/connection blocking, and all 24 images decoded. LinkedIn was additionally compared with the source paragraph by paragraph, preserving `Dr.` after the repair. An earlier LinkedIn note that lost the honorific is a defect reproduction, excluded from passes. All 10 notes and 32 resources from this round were cleaned; all 32 downloads matched size/SHA-256. Original content hashes for 40 notes and memo/resource/notebook ID sets were preserved. The dedicated token and local test session were temporarily retained pending Threads login; see the completed follow-up cleanup below. +- Firefox Threads has a sign-in modal that could not be dismissed normally; the background post is `aria-hidden`. The extension refused and created no memo. The user was asked to sign in in the isolated test window; visibility checks were not relaxed. Signed-in follow-up results appear below. +- Build chronology: the three Douban details preceded this round's new fixes; Threads/comments and Firefox Bluesky/Instagram used the shortened-link repair build; final LinkedIn was saved again on the literal-prefix repair build. Builds do not retroactively expand other acceptance claims. The final **457 extension tests passed with zero failures**; Web/mobile typechecks, Web build and both extension builds passed. Changes remain uncommitted and unpushed. + +### Firefox Threads signed-in follow-up + +After Firefox was restarted, the original debugger connection and temporary extension exited, while the daily window used a different profile. The same QA profile and build were restored with its local host grant intact; the user completed login in the restored QA window. Daily Firefox site sessions were neither cleared nor copied. + +- Native toolbar, shortened-link detail `DeMqtuYgkaL`: the live href actually used `https://l.threads.com/?u=…`. All four caption paragraphs, destination link, author/publication time and one video cover were preserved, with complete-archival feedback. +- Native toolbar, four-image detail `DZceA72Drjf`: complete caption and four archived images, excluding replies. +- Native context menu, exact card `Decc755lYhC` in the signed-in NASA profile feed: its timestamp link and context target marker were checked before saving. The full “space jellyfish” caption, author/time, four images and target URL matched, excluding the pinned podcast and neighboring second thread `Decc8jiFYaY`. This is not acceptance of natural card recycling. +- All three notes reopened in Firefox's own Web editor with actual CSP blocking of external source/CDN images and connections. Bodies matched the source and stored nodes; all nine instance attachments decoded and matched stored sizes/SHA-256 hashes. + +The entire third round covers 11 qualified scenarios, 12 qualified notes (including two comment saves), and 33 source-blocked decoded attachments. One earlier note with the missing honorific is reproduction evidence only. All 13 test notes and 41 resources were cleaned by ID; all 41 files matched size/hash metadata. The original 40 note content hashes and memo/resource/token/notebook ID sets (40/74/1/13) were fully preserved. The dedicated token was revoked and verified to return 401; its Firefox setting and seven copied local cookies were removed. The new Threads login and host grant remain. After ego lite's original QA space 9 disappeared, the user explicitly authorized a new cleanup space. Only the local test instance's token was checked: it returned 401 and was removed, with every other extension setting verified unchanged. The cleanup space was closed. The temporary plaintext credential file was deleted. + +### Fixed: Threads wrapped destination links + +The Firefox guest DOM uses `https://l.threads.com/?u=`, while the existing shortened-link fixture uses a direct destination. Replaying the isolated live target card reproduced rejection; restoring only that link's destination allowed the full caption to match. The reader now unwraps `u` only for the observed HTTPS host/root path, validates it through the existing safe-URL rules, and compares it with the full caption. It never visits the redirect. The regression failed before the fix and passed afterward, with unrelated destinations, invalid schemes, missing destinations, lookalike hosts, other paths and truncated bodies still refused. Signed-in Chromium passed the live shortened-link sample with its direct href. The wrapped form first had live-DOM replay and regression evidence, then passed native saving and source-blocked reopening with the actual wrapped href after Firefox login. + +### Fixed: literal text becoming alphabetic list markers + +The live LinkedIn Markdown retained `Dr. Nancy Grace Roman:`, but the instance's `contentJson` interpreted it as an ordered list and lost `Dr.`. Reopening matched those incorrect stored nodes, showing that storage/readback equality alone cannot establish complete source preservation. TipTap recognizes alphabetic/Roman markers beyond Turndown's default text escaping. The extension's HTML-to-Markdown text boundary now escapes line-leading alphabetic marker punctuation. Server parsing, storage schema and existing notes are unchanged; already-saved content is not automatically repaired. Real HTML lists remain converter-generated, and code blocks retain their bytes. + +A regression first reproduced loss of `Dr.`, `Mr.` and `II.`, then verified literal text, a real ordered list and code. The existing Bluesky quoted author `Mr. Joshua` is checked through actual document parsing. This patch can be reverted independently; list/code formatting is the main risk, covered by these checks. A new native Firefox save of the same guest post on the final build preserved the entire source body, including `Dr.`, with eight archived images and source-blocked reopening. + +## 2026-10-17 second follow-up live acceptance + +- Firefox added native saves and reopening for V2EX, WeChat, Xueqiu, Douban book review, Substack Note, Facebook public Float detail and a LinkedIn personal share guest page. The first five used the Android-initialization repair build; Facebook, LinkedIn and cross-document rechecks used this round's final build. All 34 instance images decoded under Firefox's local-only source policy. Checks covered both V2EX supplements, WeChat body, Xueqiu stock markers, seven Douban paragraphs and five Note paragraphs. LinkedIn preserved the complete long text, author and publication time without the website's AI-generated summary heading or comments. Its source normalized to `urn:li:share` with the same ID; valid normalization was not treated as an identity failure. +- Edge's final build added V2EX, Juejin, WeChat, Douban book review, Xueqiu and Substack Note through the native toolbar. All six bodies matched after reopening in Edge's Web editor with external images/connections blocked. All 33 instance images decoded: Juejin/WeChat 2/21 and Douban/Xueqiu/Note 6/3/1. A refusal before Note finished loading was not classified as a product failure; saving and readback passed after loading. +- In ego lite, the native selection context menu saved exactly the explicitly selected 221-character paid-preview paragraph plus source/capture time, without the full article. Reopening matched with external sources blocked. Normal Facebook feed expansion still saved the complete body and three images on the final build; source-blocked reopening also passed. +- The current Jike Web feed was retested through its native context menu: safe refusal, no new memo. The target card lacks its own stable DOM post permalink; Jike links inside its body reference other posts. Private React objects are not a stable DOM contract. Detail-page passes do not cover this feed layout. +- This round's 15 qualified save cases decoded 70 instance attachments in their own browsers (Firefox 34, Edge 33, ego lite 3). Four other notes from race reproductions or non-target probes are excluded from acceptance passes. All 19 notes and 80 resources were cleaned by ID; all 80 files matched actual sizes and SHA-256 hashes. The original 40 memo content hashes and memo/resource/token/notebook ID sets (40/74/1/13) were preserved. +- Edge's original local Web session returned `http_500`; only the `127.0.0.1` `edgeever_session` cookie was temporarily replaced for QA. Its backed-up original value was restored and verified through the browser's request. Authentication code and other sites' sessions were unchanged. Temporary browser readback helpers did not invoke capture functions; actual saves used native entries. Helper/collector cleanup, dedicated-token revocation and credential cleanup have separate receipts. The isolated Firefox profile and host grant remain available. + +### Fixed: cross-document departure during expansion + +Two live reproductions before the fix assigned `location.assign` inside the actual Facebook expansion button's click callback. The old page still saved the original post about 52 ms later. Identity and body were correct, but expected cancellation was missed. The tab URL updated only about 350 ms later, so a final URL comparison could not detect an already-started cross-document departure. An isolated-world `beforeunload` signal fired about 1 ms after the navigation request. + +The platform capture script now listens to `beforeunload` only during the current read, treats departure as a changed page, and removes the listener when finished. Existing URL, sender and post-identity checks remain; no permissions were added. A regression first reproduced the old implementation returning complete content, then verified departure rejection and successful normal expansion. The same real expansion/navigation sequence in ego lite now creates no memo; normal expansion saves successfully. Firefox's public NASA feed also used the native context menu to trigger actual expansion and controlled cross-document navigation, creating no memo. These are controlled navigation checks, not proof of natural card recycling. A Xiaoyuzhou probe never triggered expansion and is excluded from cancellation passes. + +The patch affects only cancellation during platform reads and can be reverted independently. Its main risk is false cancellation, checked through normal expansion and real saves. The final **455 extension tests passed with zero failures**; Web/mobile typechecks, Web build and both extension builds passed. Fixes remain uncommitted and unpushed. + +## 2026-10-17 follow-up live acceptance + +- Latest reviewed build in ego lite: native toolbar saves and source-blocked reopening passed for NASA Threads (4/4 images), NASA Instagram (3/3), signed-in Jike Web long text (1/1) and gallery (9/9). The Google Instagram 20-media case also passed: one video cover and 19 photos, preserving the author, full combined caption and image order. All 37 files matched independently downloaded originals. Earlier natural partial failures did not recur; their records remain and their cause is unknown. Two early operations retained a text selection and saved only that selection; they do not count as full-post passes. +- Latest build in Firefox 158.0: native toolbar saves passed for Sspai (9/9 images), Juejin (2/2) and the public Jike H5 gallery (9/9). All three notes reopened in Firefox with matching bodies and 20 decoded instance images. Every cell of Juejin's three-column, five-row table matched. All five code blocks retained internal text and blank lines, omitting only one terminal formatting newline each. +- Latest build in Edge 155.0.4283.62: the Xiaoyuzhou cover (1/1) and public Jike H5 gallery (9/9) were fully archived. Sspai archived eight of nine image positions and correctly reported the remaining external image. The three notes reopened in Edge with matching bodies and 18 decoded instance files; policy blocked the remaining external image. That image downloaded independently with a Referer, while a request without one returned 403. The extension already configures this site's Referer; this does not establish the cause of the browser failure. +- Edge/Firefox readback used `img-src`/`connect-src` policies limited to the acceptance Web page, with actual CSP violations proving external requests were blocked. Global browser security settings were unchanged. With Edge's Apple Events page-script interface disabled, two temporary extension test pages read only the local instance tab in the same acceptance window; all saves still used native entry points. The pages and local receiver were removed/stopped, and product background/capture bundle hashes remain unchanged. Edge Bluesky stayed on the site's loading screen; clipping refused with no new note, so this is neither a captured-post pass nor an established clipping defect. +- On the real NASA Facebook feed, native context-menu saving triggered the actual expansion control. Controlled `history.pushState` navigation ran inside its click callback. Evidence records collapsed text, the programmatic expansion click, before/after URLs and timestamps. Feedback said the page changed during clipping and nothing was saved; no memo was created. This covers SPA cancellation at a known point, not cross-document navigation or natural scroll-driven card recycling. +- The actual signed-out BOSS page in Firefox showed a login requirement. The toolbar reported it and refused full capture with no new note. Explicit selection of a complete visible 117-character paragraph then saved only that paragraph, excluding other requirements and restricted-content UI; Firefox reopening matched. Signed-in full-job acceptance was not performed. +- All 14 follow-up test notes, 75 files and one dedicated token were removed by ID. All 75 files passed API-download size/SHA-256 checks and decoded in their source-blocked browser readback cases: ego lite 37, Firefox 20, Edge 18. Original ID sets (40 notes, 74 resources, one token, 13 notebooks) and original note content hashes are unchanged. +- Only settings matching this round's dedicated token were removed in each browser. The server token was revoked and confirmed to return 401. The plaintext credential file, temporary extension pages and receiver were removed; the ego lite acceptance space and dedicated Edge window were closed. The isolated Firefox test profile and host grant remain for continued acceptance, with its token and copied local session cookies removed and its page blank. Ordinary Firefox was untouched. + +## Observations in this run + +- ego lite native toolbar: HN, Xiaoyuzhou, Sspai, Juejin, WeChat, V2EX, Douban book review, Substack Note, Xueqiu and Bluesky detail saved and reopened with source domains blocked. HN also passed the real detail-page context menu. These results apply only to the named samples, not other Douban types, full Substack articles or feeds. +- Jike: a real single-card context menu saved current public topic-feed post `6ad22db3a1af2007be120932`. Author, body, one original image and identity matched, excluding the next post; source-blocked reopen passed. After the user completed QR login, native toolbar saves passed for the current Web long post and nine-image gallery: author/date precision, complete ending and one/nine originals were preserved, excluding mention avatars/comments. All ten files matched independent source downloads and passed source-blocked reopen. Current details show full text without an expand control; current Web feed targeting/expansion remains pending and is not replaced by detail/mobile Web results. +- Edge: initial HN/Xiaoyuzhou toolbar saves passed; Xiaoyuzhou reopened in Edge with the full body ending and a decoded 300×300 instance cover. After loading the repair build at that stage, HN toolbar and native context-menu saves passed. Another Xiaoyuzhou save retained its one cover externally with accurate feedback, so final image acceptance is partial. HN retained visible text generated by the user's existing Immersive Translate. +- Firefox: after the port fix, connection returned 13 notebooks and HN/Xiaoyuzhou toolbar saves passed, with all Xiaoyuzhou images archived. Both notes reopened in Firefox itself, showing the HN/Show Notes endings and a decoded 300×300 instance cover. HN native context-menu capture and a toolbar save on the repair build at that stage also passed. Signed-in social/feed cases and blocking inside Firefox itself remain pending. +- Facebook: initial capture refused an unrecognized page-declared pfbid alias. After the fix, native toolbar save preserved the NASA author, precise publication time `2026-10-10 04:54:33`, full body and one instance image. Text/image readback passed with the source blocked. A real single-card context menu on the signed-in NASA feed also expanded and saved the correct post, full text and three images, passing source-blocked reopen. Initial capture included its See less control; after the fix, native readback excludes the control while regression tests retain matching literal body words. +- LinkedIn: the activity URL redirected to ugcPost, initially causing a false navigation error. Captured evidence shows matching current tab/DOM URLs while extension `sender.url` retained the initial address. After the fix, native capture preserved author/headline, full text and all eight images; source-blocked reopen passed. Other activity/share/guest cases remain pending. +- Threads: signed-in NASA detail saved text/four image positions, archiving three and retaining one externally with accurate feedback. Instagram: NASA detail preserved the complete caption/three image positions, archiving two and retaining one externally with accurate feedback. Archived images decoded with sources blocked; full-image acceptance failed for both. The missing Threads image later downloaded independently and inside the extension; its initial cause remains unknown and no speculative retry was added. +- Medium redirected to a custom domain behind a Cloudflare challenge; no bypass/save attempted. Native capture of a Substack paid preview refused the full article without creating a memo; explicit visible selection remains pending. BOSS was signed in with full content and saved, so it does not prove login-overlay rejection. Full-job acceptance is tracked separately. +- Independent attachment audit: 30 test memos and 77 archived resources; every file's actual bytes/SHA-256 matched instance metadata. All 77 decoded from instance URLs in ego lite with source/CDN domains blocked. Initial Xiaoyuzhou covers from all three browsers were 70,133 bytes and matched an independent source-CDN download. This excludes external images and does not establish cold offline support in every client. +- Body comparison accounts for stored node/UI types: Sspai's three PDF link cards were checked separately. Xueqiu's three stock labels use existing inlineMath nodes; stored attributes matched KaTeX annotations, and text matched after excluding duplicate MathML/HTML representations. Editor controls/non-text attributes were not misreported as body loss. + +### Fixed: Firefox instance permission with nondefault ports + +Firefox does not support ports in WebExtension match patterns. The old `http://127.0.0.1:8787/*` pattern appeared granted while requests failed with `NetworkError`. Permission requests, background checks and diagnostics now share a port-free host pattern; HTTP requests retain the complete configured endpoint. Permission covers all ports on the same protocol/host, while the instance setting still determines where the token is sent. Users may need to grant host permission again after upgrading: both Firefox and ego lite prompted in this run; denied permission prevents saving. API CORS, authentication and storage are unchanged. See [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns). + +Regression coverage checks nondefault HTTP/HTTPS ports and IPv6: connection retains the configured port after permission. Existing synchronous user-gesture and denied-capture cases still pass. Real Firefox connection, save and image readback passed after the fix. Rollback is limited to the shared helper and its three permission consumers; ordinary HTTPS instances still need continued cross-browser coverage. + +### Fixed: SPA message URL and Facebook post aliases + +- SPA: no longer treats the message's initial sender.url as the live address. Extension ID, target tab, top frame and request ID checks remain; the page reader checks live URL before/after capture and the save path checks the tab URL before creation. Regression coverage includes stale sender URL acceptance, rejection of real navigation, and existing spoofed-sender/expansion/queued-cancellation cases. +- Facebook: accepts a story_token alias only from a CometSinglePostDialogRoute whose declared URL matches the requested post. Existing numeric-ID mapping remains. Author/time/image-completeness checks use the same confirmed identities. Regression coverage includes valid aliases and unrelated posts, wrong routes and invalid aliases; no whole-page/neighbor-post fallback. +- Both changes are limited to extension capture, with no API/schema/authentication change. The worst case would be accepting an old/adjacent post, so the identity/navigation boundaries above are retained and rejection paths tested. Each patch can be reverted independently. Live cross-platform limits remain as recorded in the matrix. + +### Fixed: current Jike Web detail and Facebook collapse controls + +- Signed-in Jike Web uses `/u//post/`. The reader recognizes this detail route and binds a single visible detail card to its top-level author link. It keeps body line breaks, mention text, links/preview cover, excluding avatars/comments. Dates retain only the source's day precision. +- For the verified cdnv2.ruguoapp.com gallery-viewer paths only, pure imageMogr2 thumbnail queries are removed to retain originals in order; queries with other signature parameters are untouched. All nine originals were downloaded independently and compared byte-for-byte with instance files. +- Two redacted public-card fixtures record provenance. Coverage includes full long text/originals, wrong author, ambiguous/hidden/missing cards, wrong post, preview-header impersonation and navigation before saving. No authenticated API is read. Current Web feeds lack a reliable DOM post ID, so there is no whole-page/positional fallback. +- Facebook removes only semantic See less/收起 buttons from body content, retaining matching literal words. Real expansion/recapture and two regressions passed. Each patch can be reverted independently; site layout changes require renewed acceptance. + +### Follow-up review: Jike detail visibility + +- Regression fixtures reproduced two omissions: hidden author links could bind the route identity, and hidden author/date values could enter metadata. Identity links now use the shared visibility check; metadata comes from a readable header copy so hidden information is omitted. +- Six regressions cover `hidden`, `aria-hidden` and `display:none`; all failed before the fix and pass afterward. Existing logic already ignores hidden expansion controls, so no extra mechanism was added. This patch only affects the Jike detail reader and can be reverted independently. It has not been rechecked live and adds no browser acceptance claims. +- Correctness, simplification and requirement-boundary review covered permissions, SPA messages, Facebook aliases and Jike reads, with no other evidenced substantive findings left untreated. Exact navigation timing and same-author card recycling remain live-verification gaps below. + +## Local checks and findings + +- `bun run typecheck`, `bun run typecheck:mobile` and `bun run build:web` pass. Initial typechecks mixed old ProseMirror links; `bun install --frozen-lockfile --force` resolved this without changing the lockfile or dependency versions. +- Chromium/Firefox extension builds pass; builds do not count as live acceptance. +- After the follow-up review fix, the full extension suite reports **452 passed, zero failures**. All three checks above and both Chromium/Firefox extension builds passed again. +- `bun run test:extension` twice reports 424 pass / 3 fail: the first React popup test exceeds five seconds, followed by overlapping `act()` calls and incorrect DOM state. The original isolated popup run passed seven tests; with the port regression, `bun test apps/extension/popup.test.mjs` passed eight. With the new port regression, the latest full run passed **446 tests, zero failures** without timeout changes; the earlier intermittent failure remains unattributed. +- After restarting `bun run dev`, both Web/API session endpoints return HTTP 200. Initial startup overlapped dependency reinstallation and failed dependency resolution; separate logs retained. +- Created a dedicated least-scope API token and configured local instance access in all three browsers. Instance permission and Jike Web login completed. All 30 test memos, 77 resources and the dedicated token were removed by explicit IDs. The temporary Firefox profile/credential file were deleted and the acceptance task space closed. Original IDs (40 memos, 74 resources, one token, 13 notebooks) and original memo content hashes were preserved. + +## Delivery state + +This macOS acceptance round and fixes are documented, with cleanup completed. Fixes are not committed/pushed; no GitHub comment or issue closure was performed. Runtime evidence stays local; public records must omit tokens, sessions and private memo content. + +The ninth round additionally verifies the specified Edge signed-in Xueqiu, Facebook, Jike and Bluesky feed cards. The tenth round resolves both formatting observations and the Medium preview omission, adding Edge Douban four-type coverage and ego lite public Medium. Rounds eleven/twelve close the targeted gaps; unsampled layouts and natural card recycling remain explicit limits. The specified public Medium full story passes in Edge/Firefox; Edge also covers only the original sample’s current visible preview. The specified Edge Threads details and signed-in LinkedIn UGC detail now pass. The specified current Jike Web feed, signed-in LinkedIn company feed, Bluesky quoted feed and complete Substack article samples in all three browsers are now covered. Chrome/Chromium does not add a separate native Chrome installation gate. Android USB’s two cases and support declarations are verified without claiming every phone layout. Historical intermittent external images remain observations: partial-failure feedback and successful attachment readback are verified, without inventing an additional root-cause repair gate. Signed-in full BOSS jobs are tracked separately. Complete acceptance across all browsers/layouts is not claimed. + +## 2026-10-18 round eleven: #493 Medium code and Threads quotes + +Reuse signed-in Edge window `1529310870` (155.0.4283.62), the local instance and existing host grant. Reload the current Chromium build and save through the actual toolbar. Read-only helper pages never invoke capture/save APIs. The seven original signed-in tabs remain. + +Live testing exposed two defects. Medium uses `pre` with highlighted `hljs-*` descendants but no language class; its already-loaded Apollo `Paragraph.codeBlockMetadata.lang` declares the language, while `br` represents newlines. The reader now converts readable-copy breaks to newlines and supplements language only when paragraph identity and exact visible code match, preserving existing declarations. It only parses bounded JSON assignments, never executes scripts or recovers hidden/paid text. Threads uses nested `data-pressable-container` quote cards; a second quoted timestamp caused parent rejection. Timestamps now belong to their own container, and quote containers cannot establish readability of a hidden parent caption. + +This restores requested highlighting and quote capture. Scope is the Medium reader and Threads card identity; worst cases are an incorrect language match or card boundary, independently reversible. No instance authentication, storage, migration, API, host permission or dependency changes. Two real-layout regressions cover code/newlines/language, state text not replacing visible content, unchanged source DOM, and quote/neighbor/hidden-parent isolation. + +| Native source | Save/source comparison | Source-blocked reopen | +| --- | --- | --- | +| [Medium JavaScript Fundamentals-Part 1](https://medium.com/@emailforbarinder/javascript-fundamentals-part-1-1a538c9389fe) | All characters/newlines in 40 code blocks match; 38 JavaScript/two XML languages; one body image archived | Same Edge editor body/code/language match, 551 actual syntax spans with distinct computed colors, local 512×512 image decodes | +| [Threads text quote](https://www.threads.com/@mosseri/post/DeAlGo0I71T) | Main caption, techmeme quote caption/actor/permalink preserved | Editor body matches storage; this sample has no quoted photo | +| [Threads photo quote](https://www.threads.com/@mkbhd/post/DehHSaCnEmU) | Main and quoted captions, quoted identity/link and original photo preserved | Body matches and local 1206×1692 image decodes | + +The editor only allows local connections/images. MAIN-world probes prove all five fetches and five image loads to the three sources/two CDNs are blocked by CSP. Three resources (two final images and the pre-fix Medium comparison) match API byte counts/SHA-256 and independent source downloads byte-for-byte. The pre-fix sample is excluded from acceptance. + +**483 extension tests pass, zero fail**. Root/mobile typechecks, Web/Chromium/Firefox builds and diff check pass; Firefox lint has zero errors/notices and seven existing dependency warnings. The full 3,743 non-E2E results belong to the prior review; this round does not claim a fresh full-suite run. Firefox is built, not newly verified at runtime. + +Both targeted gaps are closed; other historical #588 limits and #587/#589 scope remain explicit. Four test notes, three resources and the dedicated token are removed; the revoked token returns 401. Original ID sets of 40 notes/74 resources/one token/13 notebooks and 40 content hashes match exactly. Only this round's browser token is removed; other settings and site logins remain. Six helpers, two credential files and two receivers are removed; all 52 product files match the final build. No commit, push, deployment or GitHub update/closure. Evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/eleventh`. + +## 2026-10-18 round twelve: final-build cross-browser targeted acceptance + +Reuse Firefox 158.0’s original test profile and Edge’s seven-tab window. The user authorizes the sole new ego lite space 23 and completes separate Medium human checks in Firefox/ego lite; the space is finished afterward. Firefox reloads the current `dist-firefox/manifest.json`; ego lite reloads the current Chromium extension. Version numbers do not replace artifact hashes. No product code, permissions, dependencies or runtime configuration changes. The prior 483 extension tests, typechecks and builds are retained without claiming a fresh automated run. + +| Browser/source | Native save, independent source comparison and own-editor reopen | +| --- | --- | +| Firefox/ego lite: Medium `1a538c9389fe` | Native toolbar save; all 40 code blocks match source characters/newlines with language preserved. Source/CDN-blocked reopening produces 551 actual syntax spans with different computed colors; body and one local image remain readable. | +| Firefox/ego lite: Threads `@mkbhd/DehHSaCnEmU` | Native save preserves main/quoted captions, quoted identity/link and original photo; source-blocked reopen matches and decodes the local image. Edge passes this same build in round eleven. | +| Firefox/ego lite/Edge: Jike Web grid `6715e824d9dc81724c2ec74d` | All keep actor/date, both caption lines and nine original-image positions, excluding comments. Firefox/ego lite archive/decode all nine in their own restricted editor. Edge archives eight and retains one external image with accurate partial feedback; only the eight local files pass decoding, while the external image is blocked. | +| Edge: BOSS login preview | The actual page says login is required for full content. Native toolbar explains login/visible selection; clicking full save refuses with zero new notes. Site selection styling prevents a completed native selection save this round; Firefox’s earlier 117-character selection evidence remains. | +| ego lite: current BOSS state | The user is still signed in and the full text is readable. No logout/visibility manipulation to manufacture a refusal, and no restricted-save pass is claimed. Full jobs remain #589. | + +Firefox/ego lite each prove all 14 connection/image probes (three sources and eleven image URLs) blocked by CSP, while their eleven local images decode. Edge proves ten source/image URLs blocked and its eight archived images decode. All 30 successful resources from seven notes match API byte counts/SHA-256 and independently downloaded source bytes. Both Medium code sources, both Threads main/quoted captions and three Jike captions/image orders are independently compared; storage/reopen equality alone is not source completeness. + +Seven notes, 30 resources and the dedicated token are removed; revoked access returns 401. Original ID sets (40 notes/74 resources/one token/13 notebooks) and 40 content hashes match exactly. Only this round’s token is removed from each browser; other settings/site logins remain. Firefox’s test tab is restored blank and Edge’s original seven tabs remain. Eight helpers, two credential files and two receivers are removed. All 52 Chromium and 52 Firefox product files match the final build. No commit, push, deployment or GitHub write. Evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/twelfth`. + +## Delivery check against #588 completion conditions + +| Issue condition | Evidence and allowed boundary | +| --- | --- | +| All-platform matrix with environment/login/expectations/results/unknowns | Matrix and round records exist; not exhaustive coverage of every layout. | +| Chrome/Chromium real toolbar, detail context menus and supported feed cards | ego lite native entry/card evidence exists; no extra native Chrome installation gate. | +| Edge/Firefox loaded extension, entry points, content/metadata, real images and reopening | Historical platform evidence plus final targeted Medium/Threads/Jike checks; other historical samples are not retroactively assigned the latest build. | +| Signed-in feeds/Jike Web, expansion, identity, images, navigation cancellation and restricted refusal | Named Facebook/Threads/LinkedIn/Bluesky/Xueqiu/Jike cases and controlled real navigation are evidenced. Natural virtual-list recycling remains untested. Identical BOSS preview states in every browser are not a separate original gate; actual Edge/Firefox BOSS and Substack restrictions are recorded. | +| Firefox Android scope and USB hardware | Android 142+ declaration verified; USB Firefox 158.0 passes two cases/14 images/reopening. Scope is extension-menu articles/public social details, excluding every mobile layout or store install/update claims. | +| Partial-image feedback, source-blocked archival readback, cleanup and linked repairs | Current/historical evidence covers these; honest Edge 8/9 feedback and successful-file readback satisfy the condition without adding a CDN root-cause gate. | + +All six conditions now have corresponding evidence within the original issue scope, ready for maintainer closure review. Natural card recycling, unsampled layouts and per-round builds remain explicit. #587 store delivery/update and #589 full BOSS jobs stay separate. This is not a claim that every browser/layout or all clients’ cold-start offline behavior passes. GitHub has not been updated or closed. + +## 2026-10-18 round thirteen: final change review-loop + +Review all uncommitted #588 extension changes and relevant consumers for correctness, simplification and requirement boundaries. Identity/permissions/cancellation and content reading receive independent read-only reviews. Work stays on `main`, preserving unrelated `apps/mobile/.gitignore`. + +Medium normalized code only when the chosen node itself was `pre`. If a quote/container was selected first, existing deduplication skipped its nested `pre`, losing newlines/language. A paragraph identity on `pre[data-selectable-paragraph]` itself was also missed. A regression first reproduces nested newline loss; the repair applies existing normalization to code within the chosen readable copy and recognizes the root paragraph identity. HTML→Markdown→stored-node checks verify both paths’ language, characters and blank lines, with unchanged source DOM. Exact visible-code/state matching and existing language priority remain; no new body source is introduced. The unsupported 40-character language-label limit is removed while retaining the character grammar already used for Markdown code languages. + +Scope is the Medium reader and regression, independently reversible. The worst risk is incorrect code language/newline handling; existing visibility/state-not-replacing-body/exact-code/language regressions and the new consumer checks constrain it. Both independent reviewers and the primary reviewer finish all three views without untreated substantive findings. + +Latest full non-E2E: **3,746 pass, zero fail** (3705 + 17 + 24), including **484 extension tests**. One real Windows cwd-lock case skips under its existing macOS platform condition. Root/mobile typechecks, Web/Chromium/Firefox builds, Firefox lint and diff check pass; lint reports zero errors/notices and seven existing dependency warnings. This is code/automated review, with no new native browser/Android operation, memo or token. Round twelve runtime evidence remains tied to its recorded build, not retroactively to these new packages. No commit, push, deployment or GitHub write. Evidence: `/private/tmp/edgeever-issue-588-review-final-20261018`. + +## Contribution-branch pre-submission verification + +Following the [contribution guide](../CONTRIBUTING.md), create `fix/issue-588-browser-acceptance` from official `upstream/main` at `df22f55d` and submit through the matching Fork contribution branch, without pushing deployment `main`. The patch and ten new files match their pre-migration versions exactly, without conflicts; the unrelated mobile `.gitignore` change is excluded. + +After lockfile installation on the new baseline, full non-E2E: **3,760 pass, zero fail** (3719 + 17 + 24, including 484 extension tests). One Windows-specific case skips under its existing macOS condition. Root/mobile typechecks, Web/Chromium/Firefox builds, Firefox lint and diff check pass; lint retains seven existing dependency warnings. No new native operations or expansion of historical build-specific runtime coverage. + +The first PR CI root `bun run lint` flags the Jike CDN query-prefix regex under the startsWith rule; it is replaced with an equivalent string-prefix check. Root lint, 484 extension tests, root/mobile typechecks and Web build pass locally. Firefox add-on lint and root lint are separate checks; remote CI status remains the PR checks result. diff --git a/docs/browser-clipper-acceptance-588.zh-CN.md b/docs/browser-clipper-acceptance-588.zh-CN.md new file mode 100644 index 00000000..fee58e5f --- /dev/null +++ b/docs/browser-clipper-acceptance-588.zh-CN.md @@ -0,0 +1,379 @@ +# 浏览器剪藏实站验收:Issue #588 + +[English](browser-clipper-acceptance-588.md) + +更新:2026-10-18。关联 [#588](https://github.com/tianma-if/edgeever/issues/588)。本文件是进行中的验收记录,不能用于宣称跨浏览器全部通过。 + +## 环境与基线 + +- 代码:上游 `67f627cc3c702c9cf2fa3fab9fabc67c70206888`;本地 `main` 同步提交 `7bd005ec`,在此基线上新增实例权限、SPA 消息判断、Facebook 别名/界面文字、新版即刻 Web 详情、Android 初始化和展开中跨文档取消修复。 +- 系统:macOS 27.0.1(26A434),Apple Silicon;首轮无 Windows、Linux 或 Android 真机;Android USB 续验状态见下文。 +- 扩展:`0.1.17`,当前仓库 Chromium/Firefox 构建;同版本号的旧开发构建不能混为本轮构建。 +- Chrome:155.0.8059.80 已安装,当前仓库扩展尚待加载。 +- ego lite:0.6.0.12,来源页报告 Chrome/154.0.0.0;现已加载当前仓库 `apps/extension/dist`,ID `edmkendapkebphaoiiogflldcjllbmca`,运行/manifest 错误均为空。旧开发扩展已替换。 +- Edge:155.0.4283.62,原生扩展页显示 `0.1.17` 和同一开发目录对应 ID;已完成 HN/小宇宙工具栏保存;Firefox 158.0 使用独立临时测试配置,加载 `dist-firefox`。 +- 本地实例:`http://127.0.0.1:8787`,Web `http://127.0.0.1:5173`;服务启动和健康状态单独记录,不能作为保存验收。 +- 测试前本地数据:40 条笔记、74 个资源、1 个 API Token、13 个笔记本;临时测试内容须按本轮创建 ID 清理。 + +实站记录跨构建迭代,最新定向复验与构建边界见第十二轮;Firefox 保留各历史轮次的构建边界,不将新包构建通过追溯为运行时通过。 + +## 平台矩阵 + +历史样本状态和预期来自 [sources.json](../apps/extension/fixtures/platforms/sources.json) 与[已有验证记录](browser-clipper-platforms.zh-CN.md)。历史证据不自动转换为本轮通过。 + +`—` = 当前登录状态不适用,未声称该受限场景通过;`P` = 下述指定样本和入口完成保存/回读;`P*` = 部分完成,见限制;`F` = 原生保存失败,待诊断;`B` = 外部访问或浏览器环境阻塞;`T` = 未完成。第一列浏览器结果均来自 ego lite,并不代表 Google Chrome 已验收。每个平台的全部历史样本、入口和登录态并未穷尽。Chrome/Chromium 不额外增加原生 Chrome 安装门槛。 + +| 平台/来源页 | 登录状态与覆盖边界 | 预期 | ego lite / Chromium | Edge | Firefox | +| --- | --- | --- | --- | --- | --- | +| Facebook
[facebook](https://www.facebook.com/NASA/posts/pfbid0dPToN66CzcAHGMoaNe3jszH4FfBKvqkE5gogCNJkTxXDPsSGYrGvcVwsLgByyMQQl) | ego lite 已登录详情/信息流;Edge 公开详情 1 图及登录列表展开 3 图;Firefox 公开 Float 详情 3 图及信息流跨文档取消 | 正文、作者、时间、目标图片;排除评论 | P | P* | P* | +| Instagram
[instagram](https://www.instagram.com/p/DeAYvsnn4la/) / [instagram-slide-captions](https://www.instagram.com/p/DeKe6teEpya/) | ego lite/Firefox:NASA 3/3 及 Google 20 项;Edge:公开 Google 20 项 | 3 图/1 视频封面及 19 图、完整合并配文 | P | P* | P | +| LinkedIn
[linkedin](https://www.linkedin.com/posts/friends-of-nasa_nasa-space-astronomy-activity-7497764718095323136-NRvr) / [linkedin-ugc](https://www.linkedin.com/posts/nasa-space-astronomy-ugcPost-7497764716677758977-qmKO/) / [linkedin-ugc-guest](https://www.linkedin.com/posts/nasa-space-astronomy-ugcPost-7497764716677758977-qmKO/) / [linkedin-person](https://www.linkedin.com/posts/maddyahmad_softwareengineering-artificialintelligence-share-7456582751698190336-6NVK/) | ego lite 登录 UGC/公司信息流相邻两卡;Firefox 访客个人 share 及 UGC;Edge 登录后 SDUI UGC 详情八图通过 | activity/ugcPost/share 身份、作者与职务、完整正文和图序;Firefox 访客个人 share 完整长文通过 | P* | P* | P* | +| Threads
[threads](https://www.threads.com/@nasa/post/DZceA72Drjf) / [threads-short-link](https://www.threads.com/@nasa/post/DeMqtuYgkaL) | ego lite 登录态四图/短链通过;Firefox 登录态两详情及 NASA 主页信息流单卡通过;Edge 访客遮罩下正确拒绝,登录后四图/短链详情及第十一轮文字/含图引用通过 | 完整配文、4 图/1 视频封面、短链目的地址;第十一轮含图引用原图 | P* | P* | P | +| Bluesky
[bluesky](https://bsky.app/profile/bsky.app/post/3mhgg5ja5gc2w) / [bluesky-feed](https://bsky.app/profile/bsky.app) | ego lite/Firefox 公开详情及引用信息流单卡通过;Edge 登录引用列表两图通过 | 准确单卡、正文、引用内容、原图,拒绝相邻卡片 | P | P* | P | +| Hacker News
[hacker-news](https://news.ycombinator.com/item?id=40380738) | 本轮公开未登录详情;三浏览器工具栏,ego lite 另测右键 | 作者 jnnnthnn、92 points、主帖末段和外链;排除评论 | P | P* | P | +| V2EX
[v2ex](https://www.v2ex.com/t/1000000) | 本轮公开详情;ego lite 工具栏 | 主帖、两条附言及各自时间;排除回复 | P | P | P | +| 小宇宙 / Xiaoyuzhou
[xiaoyuzhou](https://www.xiaoyuzhoufm.com/episode/62ad4ae5f9b62781ea058a02) / [xiaoyuzhou-hosts](https://www.xiaoyuzhoufm.com/episode/612375e57f89405077cbeb3b) | 本轮公开单集;三浏览器工具栏及笔记重开 | 完整 Show Notes、时间轴、明确主播、封面;切换单集不串内容 | P | P* | P | +| 雪球 / Xueqiu
[xueqiu](https://xueqiu.com/5636485254/253443400) / [xueqiu-feed](https://xueqiu.com/) / [xueqiu-feed-detail](https://www.xueqiu.com/6114376649/411862401) / [xueqiu-feed-detail-second](https://www.xueqiu.com/2754211668/411841572) | 三浏览器公开详情;ego lite 公开截断信息流取回完整正文,Edge 登录首页推荐卡完整正文/八图通过 | 准确单卡、完整长文、作者、显式股票标签和配图;三浏览器详情及 ego lite 公开信息流通过,Edge 指定登录推荐卡已通过,其他登录版式未穷尽 | P* | P* | P* | +| 微信公众号 / WeChat
[wechat](https://mp.weixin.qq.com/s/TC-ONpL3PGklsD0yhtcuBQ) | 本轮公开文章;ego lite 工具栏 | 正文层级、作者、公众号、21 张图;排除底部组件 | P | P | P | +| 即刻 / Jike
[jike](https://m.okjike.com/originalPosts/66ac89b0314f5cced41012d3) / [jike-grid](https://m.okjike.com/originalPosts/6715e824d9dc81724c2ec74d) / [jike-long](https://m.okjike.com/originalPosts/64c9dd9a2edae6abdc610605) / [jike-feed](https://m.okjike.com/topics/63579abb6724cc583b9bba9a) | 公开主题单卡、登录 Web 详情/九宫格;ego lite 新版信息流两长文展开及单图通过;Edge 登录 following 单卡通过;第十二轮三浏览器九宫格均保留九个图片位置,Edge 8/9 归档且如实提示 | 单帖身份、长文展开、九宫格和原图;信息流不保存整页 | P | P* | P* | +| Substack
[substack-note](https://substack.com/@tedgioia/note/c-357811021) / [substack-article](https://www.oneusefulthing.org/p/the-dot-and-the-swarm) / [substack-paywall](https://www.lennysnewsletter.com/p/how-to-build-ai-product-sense) | 三浏览器 Note 及完整文章各 3/3 图通过;ego lite 由用户手动触发工具栏,付费预览拒绝及选区通过 | Note 五段/文章章节与图注;拒绝受限整篇,允许明确选区 | P* | P* | P* | +| Medium
[medium-article](https://medium.com/@scott.batson_76410/what-should-programmers-do-while-the-ai-writes-code-a2ce2e6de88e) / [medium-public](https://scottbatson.dev/come-to-my-medium-day-talk-17d75b342aeb) | Edge 当前会员预览;Edge/Firefox 公开全文通过;ego lite 公开全文及正文预览卡/两图通过 | 原样本仅 11 个可见文本块/1 图;此前 Edge/Firefox 公开全文 16 文本块、五项列表/1 图;最终 ego lite 含副标题、17 个正文/预览块及两图;第十一/十二轮三浏览器另验 40 个代码块及实际高亮;原会员全文不扩报 | P* | P* | P* | +| 少数派 / Sspai
[sspai](https://sspai.com/post/92653) | 公开文章;ego lite/Firefox 9 图归档;Edge 8/9 归档,反馈正确并重开 | 同篇作者、更新说明引用块、原时间和9图;CDN转存 | P | P* | P | +| 掘金 / Juejin
[juejin](https://juejin.cn/post/6972051526416072717) | 公开文章;ego lite/Firefox 工具栏、2 图归档及重开 | 代码原文、三列表格四行数据、2图和时间 | P | P | P | +| 豆瓣 / Douban
[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/) / [douban-comments](https://book.douban.com/subject/36864764/comments/) | ego lite/Firefox 书/影/乐评、日记和短评单卡通过;Edge 影/乐/日记及短评已补齐 | 书影音长评、日记、准确短评卡片;三浏览器书评7段/6图通过,Edge 指定其他四类已补齐 | P | P | P | +| BOSS 直聘 / BOSS Zhipin
[boss-login-required](https://www.zhipin.com/job_detail/0d7e760b0a0e7c3a0nF939S0E1NU.html) / [boss](https://www.zhipin.com/job_detail/0d7e760b0a0e7c3a0nF939S0E1NU.html) | 本 Issue 仅跟踪受限反馈;第十二轮 Edge 登录预览拒绝整篇、零新增;ego lite 当前已登录全文,未退出账号制造受限状态 | Firefox 已验证拒绝整篇及可见选区;完整岗位另跟踪 #589 | — | P* | P* | + +## 每个浏览器与来源的执行步骤 + +1. 记录浏览器完整版本、扩展版本/加载路径、代码 SHA、实际来源 URL、登录状态及可见正文末段、图片数。先确定预期,再点击保存。 +2. 在真实详情页点击原生工具栏并保存;再次通过原生右键菜单保存。对支持的信息流,选择单卡执行右键保存。三种入口分别记录,不以直接调用函数或 DOM 合成事件替代。 +3. 核对保存提示、笔记标题、正文首尾、作者/职务、发布时间精度、来源及标签,确认没有相邻卡片、评论或登录组件。 +4. 对照原页图片清单,核对实例附件地址、数量、摘要和浏览器解码;阻断来源域名及 CDN 后关闭并重开笔记,确认正文与附件可读。此步骤不证明客户端冷启动离线。 +5. 记录每次操作的时间、入口、笔记 ID、附件数量、提示、结果和证据路径;实际结果可为通过、失败或外部阻塞,不能用“页面可读”替代“保存通过”。 +6. 删除本轮笔记、资源和 Token,按初始 ID 集合核对原有数据保留;只清理本轮创建内容。 + +## 登录态与异常矩阵 + +| ID | 场景与预期 | 本轮结果 | +| --- | --- | --- | +| B01 | 登录后的社交信息流/即刻 Web:展开完整长文,目标身份不变,图片齐全,不混入相邻帖子。 | Facebook、Threads 登录信息流,以及本轮即刻 Web 两长文/单图和 LinkedIn 公司信息流相邻两卡通过;各浏览器与版式边界见矩阵 | +| B02 | 展开过程中导航/卡片复用:取消旧任务,不创建错误身份或部分正文笔记。 | 回归测试通过;续验在真实 Facebook 展开点击回调内受控 SPA 切页,正确提示取消且零新增;第二轮已在 ego lite/Firefox 的真实展开回调中触发跨文档导航并零新增;自然卡片复用仍未完成 | +| B03 | 登录/付费受限:拒绝整篇,不读取隐藏内容;明确可见选区仍可保存。 | Substack 付费预览拒绝整篇通过;Firefox BOSS 登录预览拒绝整篇、明确可见的 117 字选区保存及重开通过;ego lite 的 Substack 付费预览明确可见 221 字选区保存与重开通过 | +| B04 | 部分真实图片下载或上传失败:正文保留,显示部分图片仍为外链,不虚报全部归档。 | 初轮 Threads 3/4、续验 Edge 少数派 8/9 归档,提示均与实例状态一致;Threads 后续 4/4 通过,失败根因未确认 | +| B05 | 阻断原站与 CDN 后重开:全部成功转存的附件从实例读取并正常解码。 | ego lite 指定样本通过;前次续验 Firefox 20 张、Edge 18 张;第二轮 Firefox 34 张、Edge 33 张、ego lite 3 张在各自限制外部来源的页面中解码通过 | +| B06 | 测试数据清理:原有笔记/资源/Token ID 集合保留,本轮新增对象全部清理。 | 通过:前轮及续验分别按 ID 清理;前次续验删除 14 条/75 个资源,第二轮删除 19 条/80 个资源及各轮专用 Token;原有 ID 集合与笔记内容摘要均保留 | + +## Firefox Android + +仓库 manifest 声明 `gecko_android.strict_min_version = 142.0`,扩展 README 也声明 Android 142+。2026-10-17 查询 [Mozilla 商店 API](https://addons.mozilla.org/api/v5/addons/addon/edgeever-web-clipper/),公开版 0.1.17 同时列出 Firefox Desktop 140+ 与 Android 142+,因此当前存在 Android 兼容声明,使用同一套 Firefox 插件,并非 EdgeEver Android App。声明和上架信息不是运行时验收证据。随后用户接入 RMX5200 USB 真机(Android 17、arm64),已安装并验证 Mozilla 官方 Firefox 158.0 的 APK 签名。通过 `web-ext` 临时加载当前工作区扩展 0.1.17,使用专用 Token 和 USB 本地实例映射,用户授权主机权限后读取到 13 个笔记本。临时加载不等于商店安装或扩展升级验收。 + +真机首先复现后台初始化失败:Android 不提供 `chrome.contextMenus`/`chrome.windows`,直接注册桌面监听导致 `TypeError`,弹窗显示“未识别”,保存时报 `Receiving end does not exist`。现仅在 API 存在时注册菜单/窗口监听,并在安装菜单注册入口检查支持情况;平台读取、权限与保存逻辑不变。新增回归先复现失败,再验证无这两项 API 时仍可初始化、判断页面及保存;既有桌面菜单安装回归继续通过。可独立撤回这三处判断;最坏风险是误停用桌面入口,因此同时保留桌面菜单测试。 + +| 真机用例 | 结果与证据 | +| --- | --- | +| [少数派:我的时间管理工具进化史](https://sspai.com/post/92898) | 手机原生菜单打开扩展并保存;作者小雨姑娘、日期及来源正确,预先读取的完整正文保留,5/5 图片归档。 | +| [即刻 H5:九宫格](https://m.okjike.com/originalPosts/6715e824d9dc81724c2ec74d) | 原生入口保存;彗星咻咻咻的目标正文及 9/9 图片归档,未包含评论。 | +| 手机重新打开上述笔记 | 使用实际 Web 编辑器,在仅允许本地图片与连接的 CSP 下,两篇正文与保存结果一致,14 张实例附件全部成功解码;外站请求实际触发 CSP 阻断。14 个文件的字节数及 SHA-256 与实例记录一致。 | +| Hacker News | 手机访问连接超时,未创建笔记,不计为剪藏失败或通过。 | + +本轮验证范围为 Android Firefox 扩展菜单中的“剪藏当前网页”,覆盖文章与公开社交详情。Android 没有该扩展的桌面右键菜单/独立窗口入口;社交信息流单卡片、原生选区、登录态站点和其他矩阵项仍未真机验收,不作全平台可用保证。修复后完整扩展测试 **453 通过、0 失败**,Web/移动端类型检查、Web 构建及两种扩展构建通过。已按 ID 清理本轮 2 条笔记、14 个资源和专用 Token,移除手机测试 Token 及复制的 7 个本地会话 Cookie;原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合保持一致。 + +## 2026-10-18 整体复核(bugfix-review-loop) + +范围为 #588 当前全部未提交扩展代码、夹具、回归及验收记录;保留无关 `apps/mobile/.gitignore`,全部工作继续在 `main`。覆盖正确性、简化、需求边界,并对身份/导航与内容转换分别独立只读复核。 + +修复三类遗漏:即刻 Web 直接删除按钮语义节点会把正文图片一并删掉,九图变八图却仍报完整;现复用 H5 既有媒体保留规则,已确认图片保留、图片按钮解包,真实控件仍移除,两条路径共用小函数。即刻和 Medium 的正文入口会整块拒绝 `visibility:hidden` 容器,误丢其显式恢复 `visibility:visible` 的后代;现交给既有 `platformReadableCopy` 逐节点过滤,隐藏卡片和身份保护未放宽。Medium 原循环跳过全部 `h1`,会遗漏后续正文一级标题;现只跳过自身首个可读标题。 + +新增 5 个有证据的回归:三种图片/容器按钮语义,Medium 可见后代及后续正文标题,即刻可见正文/媒体恢复;修复前复现失败、修复后通过。复审没有新的未处理实质发现;未增加权限、接口、依赖、重试或元数据要求。补丁限于两个读取器及共用控件清理,可独立回退;主要风险是可见性/媒体过滤误差,由隐藏内容、图序、完整性、源 DOM 不变及已有身份回归约束。 + +最新代码完整非 E2E **3,743 项通过、0 失败**,包含 **481 项扩展测试**;1 个真实 Windows cwd 锁用例在 macOS 按既有条件跳过。根/移动端类型检查、Web/Chromium/Firefox 构建及 diff 检查通过;Firefox lint 为 0 错误、0 提示、7 条既有依赖警告。本轮是代码与自动化复核,没有重复原生实站/Android 操作,不把这些修复追溯算作最新构建的真机验收。未创建业务测试剪藏或 Token,原登录配置保留。仍未提交、推送、部署、回写或关闭 Issue。证据:`/private/tmp/edgeever-issue-588-review-20261018`。 + +## 2026-10-18 第十轮:补齐豆瓣、Medium 与排版修复 + +沿用 Edge 窗口和本地实例。用户授权新建 ego lite 空间 21,并完成一次 Medium 人机验证;复用已登录浏览器配置,没有复制或清除 Cookie。所有保存均通过真实工具栏或原生右键,来源辅助页只做只读观察和标记。 + +| 用例 | 实际保存与来源核对 | +| --- | --- | +| Edge 豆瓣影评 `17798662` | 工具栏保存标题、作者及完整正文,2/2 图片归档。 | +| Edge 豆瓣乐评 `17855287` | 工具栏保存标题、作者及完整正文,3/3 图片归档。 | +| Edge 豆瓣日记 `814698224` | 来源实际带 `_i` 参数;按真实标签页地址操作,笔记来源规范化正确,完整正文及 6/6 图片归档。 | +| Edge 豆瓣书籍短评 `4237804504` | 在列表原生右键保存所选卡片,正文、作者、评分“还行”及自身永久链接正确;零图片,不把头像计为配图。右击文字产生选区菜单的误触不计入单卡通过。 | +| Edge 雪球同篇八图文章 | 复验第九轮的 96 个可见非空正文块;加粗片段存为 `bold` 标记,星号不再进入正文。8/8 图片归档。 | +| Edge Bluesky 同一引用动态 `3my43eyfzs225` | 自身/引用正文与 2/2 原尺寸图片保留,图片替代描述仍在,“替代文本”角标不再混入正文。 | +| ego lite Medium 公开文章 `17d75b342aeb` | 实际工具栏保存;最终保留副标题、17 个正文/预览文本块、五项列表、明确末段和 2/2 图片。完整外链预览目的地址保留。 | + +排版修复有两处:共享 HTML 转 Markdown 遇到中文标点结尾的加粗直接接普通文字时,Markdown 无法闭合加粗;现仅将该边界标点移到加粗之外,所有字符与字面代码保留,标点自身不再加粗。Bluesky 图片查看按钮只保留图像及其 alt 描述,并移除已采样的、紧邻该按钮且绝对定位的 `ALT`/“替代文本”角标;普通正文中的同名字词保留。前者影响共用 HTML 转换,后者限于 Bluesky;分别可独立撤回。最坏风险是格式或图注误删,存储节点、字面文字/代码、替代描述和原 DOM 不变的回归覆盖了相应边界。 + +本轮 Medium 逐段对照还发现正文内预览卡被遗漏:它没有 `[data-selectable-paragraph]`,因此既有块选择器漏掉标题、描述、外链及 CSS 背景封面。新增回归先复现丢卡,再补齐文章内含标题的链接卡;按原文顺序把完整标签转换成可解析链接,并保留可见 CSS 背景图片和已有图片。继续使用原有可见性与 URL 净化,排除隐藏卡和文章外推荐,不读取付费内容或增加接口。风险为上游卡片结构变化,可独立撤回 Medium 读取器改动;没有改动实例 API、认证、存储或迁移机制。修复版在同一未刷新的公开文章中通过原生工具栏重新保存,避免将验证页当作文章。 + +Edge 六条笔记在自身 Web 编辑器屏蔽原站/CDN后重开,正文与存储节点一致,21 张本地图片全部解码;主环境探测确认六个来源页及二十一个 CDN 地址的 27 次连接、27 次图片加载被页面策略拦截。ego lite 的最终 Medium 笔记也在自身编辑器屏蔽原站及两图 CDN 后重开,正文与两个本地附件正常。修复前 Medium 一图样本另行保留为问题证据,不计入最终完整性通过。 + +本轮共创建 8 条笔记、24 个资源(包含修复前 Medium 样本)。24 个 API 下载均匹配资源大小/SHA-256,且全部与独立源下载逐字节一致。豆瓣日记最后一图首次独立下载返回 418;使用正确站点 Referer 后匹配,并未改产品下载逻辑或增加重试。原有 40 条笔记内容摘要未变。 + +最终 **476 项扩展测试通过、0 失败**,根/移动端类型检查、Web 构建、Chromium/Firefox 扩展构建及 diff 检查通过。三视角复审未发现未处理的实质问题。Edge 六例使用本轮前一构建(已包含加粗/角标修复),最终 Medium 构建仅追加 Medium 读取器修复,并在 ego lite 实测;Firefox 新包只构建,不追溯扩大其运行时覆盖。 + +按 ID 删除全部 8 条测试笔记和 24 个资源,撤销专用 Token并确认 401;两个浏览器仅移除本轮 Token,其他扩展设置与站点登录保留。原有 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合与全部内容摘要一致。临时辅助页及服务删除/停止,产品文件与最终构建摘要一致,测试空间已清理。证据:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/tenth`。仍未提交、推送、发布或关闭 Issue。 + +## 2026-10-18 Edge 登录信息流第九轮补验 + +用户在保留的 Edge 窗口完成雪球、Facebook、即刻及 Bluesky 登录。本轮使用重新构建并重新加载的 Chromium 扩展;Firefox 包只完成构建,没有把此前 Firefox 实测追加为本次新代码运行时验证。 + +登录后的雪球首页使用无 CSS 模块后缀的 `.timeline__item__main`、`.timeline__item__info`、`a.date-and-source` 和 `a.user-name`,既有选择器只覆盖带后缀的类名,导致单卡无法定位。现仅补齐这四处经典类名,指针监听和保存前检查继续共用同一定位器;自身作者/永久链接校验及同源详情读取不变。影响限于雪球卡片识别,最坏风险为误选其他卡片,可独立撤回这四处选择器;回归和实站均检查目标身份与相邻内容。本次新增经典版式回归先失败、修复后通过;完整扩展测试 **473 通过、0 失败**,根/移动端类型检查、Web 构建和两种扩展构建通过。正确性、简化及需求边界复审未发现新的实质问题;未增加 API、权限、重试或元数据必填。 + +| 原生右键单卡用例 | 来源与实际结果 | +| --- | --- | +| 雪球登录首页推荐列表 | `1843761023/412754286`:截断卡片读取完整公开详情,标题/作者/来源准确;96 个可见非空正文块文字保留,8/8 图片归档。隐藏来源说明不纳入正文预期。没有把推荐列表当作个人关注列表。 | +| Facebook 登录 NASA 列表 | `pfbid0kAjTCFHeSmCB9TePMvGo3PhbNbh5jFVu3iUpC5HodB45dgpke1fCdRZQHaJr7hXYl`:展开前只有首句及“展开”,原生右键后完整配文与 3/3 图片保留,未混入相邻飞机帖或评论。 | +| 即刻登录 following 列表 | `6ad3ff33b6e171bc9b673a51`:当前 Web 单卡正文、八个完整外链及四个作者提及保留;自身图片声明为零,不把链接预览/头像计为原帖配图,不混入同作者前一天动态。当前正文已呈现,不增加自动展开通过声明。 | +| Bluesky 登录主页列表 | `bsky.app/post/3my43eyfzs225`:自身正文、Josh Sternberg 引用正文和 2/2 原尺寸图片保留,排除相邻动态。该项补齐 Edge 实际保存,不再记为加载阻塞;其他详情版式仍按矩阵限定。 | + +四条笔记在 Edge 自身 Web 编辑器重新打开,正文与存储节点一致,13 张实例附件全部解码。每个附件与本次独立源下载按图序逐字节一致,API 下载大小/SHA-256 与资源元数据一致。页面仅允许本地实例连接和图片;主环境探测实际记录四个来源页及十三个 CDN 地址的 17 次连接拦截、17 次图片拦截,本地附件仍正常显示。工具只做来源观察、实例回读及测试标记;保存全部来自原生右键,误触选中文字/未触发保存的尝试不计入通过。 + +两处轻微排版观察尚保留:雪球一处加粗片段显示字面 `**`,Bluesky 引用图各带一个“替代文本”标签。文字与图片没有缺失,图片替代描述保留;因此本轮不宣称像素或全部格式完全一致,也未为去掉界面标签增加依赖翻译文字或样式的推断规则。 + +本轮按 ID 删除 4 条笔记、13 个资源并撤销专用 Token,旧 Token 回读 401;原有 40 条笔记内容摘要与 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合完全一致。仅移除扩展内本轮 Token,其他扩展设置及站点登录保留;未替换或清理浏览器 Cookie。临时扩展辅助页及接收服务已删除/停止,产品构建摘要与本轮最终构建一致。证据目录:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/ninth`。改动仍未提交、推送或发布。 + +## 2026-10-18 Medium 第八轮补验 + +用户先在 Edge 完成 `scottbatson.dev` 人机验证。原样本 [What Should Programmers Do While the AI Writes Code?](https://scottbatson.dev/what-should-programmers-do-while-the-ai-writes-code-a2ce2e6de88e) 当前显示“Create an account to read the full story”及会员专享提示,与历史可全文阅读的状态不同;不把通过人机验证当作取得付费全文。 + +- Edge 原生工具栏保存原样本的**当前可见预览**:保留标题、副标题、作者、来源、11 个可见文本块(包括图注)及 1/1 配图,终态提示图片全部归档。隐藏的原文末段没有进入笔记,图片查看提示排除。本项只证明已显示内容的读取边界,不声称原文两图全文、代码或章节已通过。 +- 在作者同域名主页发现 [Come to my Medium Day talk](https://scottbatson.dev/come-to-my-medium-day-talk-17d75b342aeb),实站没有会员截断提示,全文可读。Edge 原生工具栏保存保留标题、副标题、作者、16 个独立文本块、五项列表和明确末段“Hecklers are permitted but not encouraged”,归档 1/1 配图,终态提示全部归档。另一篇技术文实际跳转 `medium.engineering` 后遇到该域名的验证,未计为保存通过。 +- 用户随后在原 Firefox 测试配置中单独完成同篇公开文章的人机验证。Firefox 原生工具栏保存也保留上述全部来源文本、五项列表、末段及 1/1 配图,终态提示全部归档。没有复制 Cloudflare 验证 Cookie 或绕过网站验证。 + +Edge 两条和 Firefox 一条笔记分别在自身 Web 编辑器重开,正文与存储节点一致,三张本地实例图片全部解码。三个附件分别与本次独立源下载的大小/SHA-256 匹配,API 下载也逐个匹配元数据。仅测试 Web 页限制外部图片及连接;Edge 的主环境探测记录两篇文章和两张真实 CDN 图片的四次 `connect-src` 与两次 `img-src` 拦截;Firefox 另实际记录该篇文章及真实 CDN 的两次 `connect-src` 和一次 `img-src` 拦截。Edge 的重开工具需让本地 Web 标签保持前台以完成界面渲染;早期未返回结果的操作不计为通过,重新执行后逐项核对。 + +本轮三条笔记及三个资源已按 ID 删除,专用 Token 撤销并验证 401;Edge/Firefox 仅移除匹配的扩展 Token。Firefox 测试前没有本地会话 Cookie,本轮复制的一个 `127.0.0.1` 会话值在 Web 重开后变化,最终只清理该主机/路径/名称下的测试会话,确认剩余为零;其他站点登录和验证 Cookie 保留。Edge 原有 Cookie 未覆盖。十个临时扩展工具文件、两个辅助服务和明文凭据均已清理,产品构建摘要完全不变。原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合完全保留。 + +本轮未修改产品代码,沿用之前 472 项扩展测试和构建结果;未提交、推送或发布。ego lite 本轮未补验 Medium;原会员文章全文及 Medium 代码等其他版式也不因公开短文通过而扩报。证据目录:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/eighth`。 + +## 2026-10-18 Edge 登录态第七轮补验 + +用户在保留的 Edge 测试窗口完成 Threads 和 LinkedIn 登录。沿用窗口 `1529310870`,不重建配置或重新请求主机权限。原 ego 测试空间 12 已不存在,没有新建替代空间;Edge 原有本地会话通过真实 API 验证 200,并经正常 Token API 创建本轮专用令牌,不覆盖或复制浏览器 Cookie。 + +- Threads 四图详情 `DZceA72Drjf`:登录遮罩消失,原生工具栏保存成功,终态提示全部归档。完整三段配文、NASA 作者、发布时间及 4/4 图片保留。 +- Threads 短链详情 `DeMqtuYgkaL`:原生工具栏保存成功,保留完整四段配文、`go.nasa.gov/Artemis2LunarScience` 的目的地址、作者/发布时间及 1 个视频封面,终态提示全部归档。不会把视频封面计成视频文件归档。来源辅助工具的原始 `innerText.includes` 前缀布尔值不作为完整性判据;最终以来源公开配文与保存正文的完整规范化比较为准,不能只验证错误存储内容重开一致。 +- LinkedIn 同一 UGC 详情 `7497764716677758977`:登录后为 SDUI 布局,目标卡片独立绑定活动身份,未标为隐藏,没有登录遮罩。原生工具栏保存保留来源全部正文、Friends of NASA 作者和 8/8 图片,终态提示全部归档;`Dr. Nancy Grace Roman:` 按普通文字保留。当前详情已经展开,不把它记作自动展开或信息流单卡验收。 + +三条笔记均在 Edge 自身 Web 编辑器重开,正文与存储节点一致,13 张实例图片全部解码。每个附件按图序与本次独立源下载逐一匹配,13 个 API 下载的大小/SHA-256 均匹配元数据。页面只允许实例图片和连接;主环境同步启动探测并读取结果,实际记录 Threads、LinkedIn 及两站真实 CDN 图片的四次 `connect-src` 拦截和两次 `img-src` 拦截,本地实例图片仍可解码。此处补齐的是指定详情样本,不追溯扩展为所有信息流、相邻卡片或自然卡片复用通过。 + +Edge 的 Medium 样本跳转至 `scottbatson.dev` 后仍停在 Cloudflare 安全验证页,已保留页面请用户完成验证,不运行剪藏或将验证页当作文章保存。另只观察了 BOSS 访客页:本次页面直接展示职位描述,没有复现旧样本的“登录查看完整内容”状态,没有触发保存,也不增加 BOSS 验收通过。 + +三条测试笔记和 13 个资源已按 ID 删除,专用 Token 撤销并验证 401;扩展只移除匹配 Token,其余设置一致。八个临时工具文件、两个本地辅助服务、明文 Cookie/Token 文件已清理,产品构建摘要完全不变。原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合完全保留;浏览器原 Cookie 未改动,Threads/LinkedIn 登录和主机授权保留。测试窗口仅留下两站及 Medium 页面用于后续验收。 + +本轮未修改产品代码,沿用上一轮 472 项扩展测试及构建结果;未提交、推送或发布。证据目录:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/seventh`。 + +## 2026-10-18 Firefox 与 Edge 第六轮补验 + +- Firefox 的 Instagram Google 20 项媒体样本 `DeKe6teEpya` 通过原生工具栏保存,终态明确提示图片全部归档。先正常关闭公开访客页的可选登录提示,未改页面可见性检查。完整来源配文、作者和 1 个视频封面/19 张图片保留;本次来源将完整配文放在主帖字段,未另带逐张配文字段,不据此声称覆盖另一种配文布局。20 个归档文件按顺序与本次独立源下载逐一匹配。在 Firefox 自身 Web 编辑器限制外部图片/连接后重开,正文一致、20 张实例图片全部解码,实际记录 Instagram 请求的 CSP 拦截。 +- Firefox 新增豆瓣影评 `17798662`、乐评 `17855287`、日记 `814698224` 的原生工具栏保存,完整来源正文、标题及作者保留,分别归档 2/3/6 张图片。短评列表通过原生右键保存 `4237804504`:事先核对目标标记,作者、130 字完整正文、列表 URL 及 `#c-4237804504` 定位片段保留,排除相邻短评 `4259416906`。四条笔记均在 Firefox 自身阻断来源后重开,正文一致,11 张实例图全部解码。修正的是测试工具的窗口激活/右键定位;早期未落到目标的操作不计为产品失败。 +- Edge 新增同一 Instagram Google 20 项媒体和 Facebook NASA 公开详情 `pfbid0dPToN66CzcAHGMoaNe3jszH4FfBKvqkE5gogCNJkTxXDPsSGYrGvcVwsLgByyMQQl` 的原生工具栏保存,均提示全部图片归档。Instagram 完整配文和 20 项顺序保留,20 个文件逐一匹配本次独立源下载;Facebook 完整主帖、作者、精确时间和 1 张图片保留,排除评论。Facebook 页面以图片显示星形表情,`innerText` 不含该表情,因此将标题与正文分段独立核对,没有据此误报缺字。 +- 两条 Edge 笔记在自身 Web 编辑器重开,正文一致,21 张本地实例图全部解码,页面限定 `img-src`/`connect-src` 为本地实例。初始扩展隔离环境的探测不能单独证明页面主环境的来源请求受阻;最终以主环境同步注入启动请求、后续同步读取结果,实际记录 Instagram、Facebook 及本次真实 CDN 原图的 `connect-src` 拦截和原图的 `img-src` 拦截。主环境异步返回的早期探测未得到有效结果,不计为通过;最终证据另存。未修改浏览器全局安全设置。 +- Edge Threads 访客页有登录遮罩,原生保存提示无法读取、建议登录,零新增。LinkedIn 访客正文最初加载可见,但随后出现登录弹层,将 `main` 标成 `aria-hidden="true"`;实际原生操作安全拒绝且零新增。不能把背景 `innerText` 中的全文当作可读正文,也不修改成熟读取规则。窗口保留这两个登录页等待用户登录,不扩报 Edge 登录态通过。早期一次鼠标动作没有触发保存,原生 AX 操作的实际拒绝另行记录。 +- Firefox 的既定 Medium 样本停在 Cloudflare 验证页,无文章正文,记录访问阻塞,不算剪藏失败或保存通过。 + +本轮共 7 条合格测试笔记、52 个归档资源:Firefox 31 个,Edge 21 个;其中两浏览器 Instagram 共 40 个文件另与独立源下载逐一匹配。52 个 API 下载的大小/SHA-256 全部匹配元数据。七条测试笔记及 52 个资源已按 ID 删除,专用 Token 撤销后验证 401;Firefox 和 Edge 均只移除匹配 Token,Firefox 七个复制本地 Cookie 已移除,原网站登录及主机授权保留。Edge 原本地会话 Cookie 已恢复并由浏览器回送确认。18 个临时扩展工具文件、两个辅助服务和明文凭据均已清理,产品构建文件摘要完全不变。原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合完全保留。 + +本轮仅补验和更新记录,没有产品代码变化,沿用上一轮 472 项扩展测试及构建结果;未提交、推送或发布。证据目录:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/sixth`。 + +## 2026-10-18 ego lite 原生工具栏补验 + +用户在同一测试空间手动打开插件,截图确认“✓ 已适配”,随后手动点击“剪藏当前网页”。实例实际新增两条同篇 [The Dot and the Swarm](https://www.oneusefulthing.org/p/the-dot-and-the-swarm) 的测试笔记;两条均保留标题、作者、发布时间及当前来源的 26 个正文/章节片段,各归档 3/3 图片。来源采集排除两个明确的订书/分享按钮,不将站点导航标题当作文章标题。用户未提供终态提示内容,本项不补报其反馈文案。 + +两条笔记分别在 ego lite 自身 Web 编辑器重开,正文与存储节点一致。仅该 Web 页限制外部图片和连接,实际探测原站及 Substack CDN 均受到 CSP 阻止,六张实例图片全部解码;六个附件经 API 下载核对大小及 SHA-256 一致。此前“不可剪藏”仅在自动化定位原生工具栏时出现,保留为自动化窗口定位限制;手动原生入口的识别、保存及来源阻断重开现已通过,无需修改文章解析器或权限。 + +两条测试笔记和六个资源已按 ID 清理,专用 Token 已撤销并验证 401,扩展仅移除匹配的本轮 Token,明文凭据文件已删除。原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合完全保留。本次仅补充验收记录,没有产品代码变化,沿用上一轮 472 项扩展测试及构建结果;尚未提交、推送或发布。 + +## 2026-10-18 第四轮:信息流与完整文章 + +- 即刻登录 Web 信息流通过三个原生单卡右键场景:同作者的 `6ad2adab709f50754cbd2921`、相邻 `6ad15c2977477d8a9f97733b`,以及圈子中的单图 `6ad399f0141b85b29268b593`。前两条均由扩展点击真实“查看更多”展开,完整正文、提及用户链接与圈子链接保留,排除相邻帖子与“收起”。第三条保留作者和完整配文,1/1 原图归档。图片独立源下载与实例摘要一致,1179×2556 实例图片在阻断原站/CDN 后重开解码。最初一次单图保存保留外链,随后单独复验成功;首次失败根因未确认,不加入无依据重试。 +- 即刻新版信息流没有自己的 DOM 永久链接。此前拒绝从正文引用或 React Fiber/alternate 猜测身份;新证据是在所选卡片 DOM 自身当前提交的 `__reactProps$…` 中,经元素的 `props.children` 找到该卡片公开原帖。新增读取器只返回 ID、类型、正文、作者标识和图片 URL 白名单,不读取账户或认证 API。所选 DOM 卡片与完整可见正文、作者链接、图片逐一匹配,写入前再次确认同一节点的当前 ID。仅支持已验证的 `ORIGINAL_POST`;转发、冲突或缺失状态明确拒绝。React 内部结构仍是站点升级风险,结构变化时失败关闭,不回退整页。 +- 受控地在真实“查看更多”点击中改变该卡片的当前帖子 ID,保持原 DOM 标记和正文,原生右键操作零新增笔记。这证明受控身份变化取消,不能冒充自然滚动卡片复用验收。首次实站另发现 Chromium 不接受 `executeScript.args = [undefined]`,现无 ID 时传空数组;集成环境增加真实 JSON 参数约束。 +- 即刻提及/圈子链接的布局 `div` 会生成带空白段的无效 Markdown 链接。现在完整性校验后将这类纯文字标签转为行内内容;实际重存并对照完整来源正文及解析后的链接,避免只证明错误存储内容能够重开。 +- Bluesky 公开个人资料信息流在 ego lite 和 Firefox 均通过原生右键保存 `bsky.app/3mxzez2m7wk2r`:完整主帖、引用帖、预览标题/说明/目的地址和 1/1 原图保留,排除相邻卡片。实站暴露引用预览的外层块链接显示 Markdown 语法原文;现保留完整行内预览标签,将图片单独保留,并保留块之间的空格。真实夹具回归先失败、后通过;两浏览器的最终笔记重新保存并完成自身来源阻断回读,图片均与独立源下载一致。先前“图片未归档”的即时读数发生在异步转存完成前,后续已按最终状态核对。 +- 雪球公开首页截断卡片通过原生右键保存 `8378622539/412716009`,从同源公开详情取回全部正文和三个明确股票标签:`SZ300308`、`SZ002384`、`SZ300476`。来源和作者正确,排除相邻卡片;来源阻断后重开通过。股票按既有 `inlineMath` 节点存储,分别核对节点属性及 KaTeX annotation,再排除重复渲染文本比较正文;本轮未验证登录首页。 +- LinkedIn 登录后的 NASA 公司信息流通过两个同作者相邻卡片的原生右键:`activity:7516859007463047169`、`activity:7516590973359931392`。扩展自动展开真实按钮,两个按钮随后消失;完整正文、各自来源、作者和各 1/1 图片保留,没有互相混入。两张图均与独立源下载一致,笔记在来源阻断后重开通过。关闭的是普通网站访问说明弹层,未更改主页访问可见范围设置。 +- Substack 完整公开文章 [The Dot and the Swarm](https://www.oneusefulthing.org/p/the-dot-and-the-swarm) 在 Firefox 和 Edge 均通过原生工具栏保存:标题、作者、日期、预先独立读取的全部 24 个正文/章节片段及 3/3 图片保留。两浏览器提示全部归档,分别在阻断原站/CDN 的自身 Web 页重开,正文一致,各三张实例图片解码。此处未归档音视频文件。 +- 第四轮自动化操作 ego lite 同篇文章的原生工具栏显示“不可剪藏”,没有创建笔记。诊断同时记录:目标文章窗口的活动标签正确,但浏览器 API 的最后聚焦窗口是另一个普通窗口,其活动标签没有 URL;自动化触发原生工具栏时会重新落回该窗口。显式聚焦目标窗口后仍复现;用户随后在同一测试空间手动打开插件,截图确认显示“✓ 已适配”,识别正常;当时实际保存和来源阻断重开仍待完成,后续通过见上节。Edge/Firefox 的同篇保存已通过,不能因此给 ego lite 追加通过,也不能据此修改成熟的文章解析器或扩大权限。此项作为浏览器窗口环境限制保留,仍需继续定位。 +- 本轮最终 10 条合格笔记、11 张成功归档附件均完成自身浏览器的来源阻断回读:ego lite 4 张、Firefox 4 张、Edge 3 张。另有六条复现/中间构建笔记和三个附件不计入合格结果。全部 16 条测试笔记、14 个资源已按 ID 清理;14 个 API 下载的大小/SHA-256 均与元数据一致。原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合完全保留。 +- 专用 Token 已撤销并验证 401,三个浏览器均只清理匹配的本轮 Token;Firefox 的七个复制本地 Cookie 已移除,保留 Threads 登录及主机授权。Edge 原本地会话值恢复并回读确认,专用窗口关闭;四个临时扩展验收文件和两个本地辅助服务已删除/停止,产品构建摘要未变。明文凭据文件已删除。ego lite 保留本轮空间 12 的诊断页,后续继续使用同一空间。 +- 本地 Web 代理会话检查一度超时,重建后的 Wrangler 资产映射也仍指向旧文件;重启本任务的 `bun run dev` 后恢复。未改动生产 API/认证/存储。最终 **472 项扩展测试、0 失败**,Web/移动端类型检查、Web 构建和两种扩展构建通过;本轮新代码涉及扩展,Web/移动端结果不当作新浏览器验收。修复未提交、推送或发布。 + +本轮代码复审覆盖正确性、可删复杂度和需求边界,未再发现有证据的待处理实质问题。即刻信息流读取和两处链接规范化均可独立撤回;最坏风险为站点结构变化后误认身份或改变链接排版,已用身份/正文/图片校验及实际解析回归约束。浏览器窗口环境限制与未测矩阵继续保留。 + +## 2026-10-17 第三轮补充验收 + +- ego lite 新增豆瓣电影影评、音乐乐评、日记、短评单卡和 Threads 短链接详情。影评/乐评/日记完整正文及 2/3/6 张图片保留;短评 `4237804504` 的作者、完整正文、可用列表 URL 和 `#c-4237804504` 定位片段保留,排除邻帖 `4259416906`。两次原生右键操作各保存一条正确短评。Threads 保留完整四段配文、`go.nasa.gov/Artemis2LunarScience` 和一个视频封面,排除回复。 +- Firefox 新增公开未登录 Bluesky 详情、Instagram NASA 三图详情和 LinkedIn UGC 访客版。Bluesky 正文、作者、发布时间及原图保留,排除同作者后续回复;Instagram 完整配文和三张图片保留,排除评论。Instagram 的终态弹窗反馈未捕获,不据此补报反馈验收;保存结果与归档/重开均实际检查。LinkedIn 关闭可见的可选登录弹层后,保留完整正文、作者和发布时间及八张图片;来源规范化为相同 UGC ID,页面自身 `data-attributed-urn` 及 activity 别名交叉核对一致。一次旧 QA 助手误把完整 slug 不在规范化 URL 中当作失败,未计为产品缺陷。 +- 上述 8 种合格场景共 9 条笔记(含两次短评保存)、24 张实例附件,均在各自浏览器的本地来源策略下重开;正文与存储节点一致,外站图片/连接被实际 CSP 阻止,24 张图全部解码。其中 LinkedIn 另与来源逐段核对,修复后 `Dr.` 保留。早期一条丢失称谓的 LinkedIn 笔记是缺陷复现,不算通过。本轮全部 10 条笔记、32 个资源已清理,32 个文件均下载核对大小/SHA-256;原有 40 条笔记内容摘要及笔记/资源/笔记本 ID 集合保持不变。此阶段曾保留专用 Token 和本地测试会话等待 Threads 登录,后续清理结果见下节。 +- Firefox Threads 访客页存在无法正常关闭的登录弹层,背景帖标为 `aria-hidden`,扩展拒绝且零新增;请求用户在独立测试窗口登录,未放宽可见性检查;后续登录态结果见下节。 +- 构建顺序:豆瓣三种详情在本轮新增修复前执行;Threads/短评和 Firefox Bluesky/Instagram 在短链修复构建上执行;最终 LinkedIn 在文本序号修复构建上重新保存。构建结果不追溯扩大其他已测场景的范围。最终 **457 项扩展测试、0 失败**,Web/移动端类型检查、Web 构建及 Chromium/Firefox 扩展构建通过;修改尚未提交或推送。 + +### Firefox Threads 登录后续验 + +用户重启 Firefox 后,原调试连接及临时扩展退出,日常窗口使用另一个配置。恢复原测试配置并重新加载相同构建,本地主机授权仍有效;用户在恢复的测试窗口完成登录。未清理或复制日常 Firefox 的网站会话。 + +- 原生工具栏保存短链详情 `DeMqtuYgkaL`:实站 href 确为 `https://l.threads.com/?u=…`,完整四段配文、目标链接、作者/发布时间及一个视频封面保留,提示全部归档。 +- 原生工具栏保存四图详情 `DZceA72Drjf`:完整配文及四图归档,排除回复。 +- 在已登录的 NASA 主页信息流原生右键保存单卡 `Decc755lYhC`:保存前核对时间链接和上下文目标标记。完整“space jellyfish”正文、作者/时间、四张图及目标 URL 正确,排除置顶播客帖和紧邻的第二条串文 `Decc8jiFYaY`。本项不等同于自然卡片复用验收。 +- 三条笔记在 Firefox 自身 Web 编辑器重开,来源/CDN 图片及连接受到实际 CSP 阻断,正文与来源及存储节点均核对一致,九张实例附件全部解码并匹配文件大小/SHA-256。 + +第三轮合计 11 种合格场景、12 条合格笔记(含两次短评保存)、33 张来源阻断后解码的附件;一条修复前称谓丢失笔记仅用于复现。全部 13 条测试笔记、41 个资源已按 ID 清理,41 个文件均核对大小/摘要。原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合完全保留。专用 Token 已撤销并验证 401,Firefox 中的专用 Token 及七个复制的本地 Cookie 已移除;保留刚完成的 Threads 登录及主机授权。ego lite 原验收空间 9 消失后,经用户明确允许新建清理空间,仅检查本地测试实例的 Token;实际返回 401,已移除该失效值并核对其余扩展设置完全不变。清理空间已关闭。临时明文凭据文件已删除。 + +### 已修复:Threads 跳转包装链接 + +Firefox 访客 DOM 使用 `https://l.threads.com/?u=<编码后的目标 URL>`,既有短链夹具使用直接目标 URL。将实站目标卡片单独序列化后,原实现拒绝;仅还原该链接便能匹配完整配文。现在只对这一已观察到的 HTTPS 域名和根路径读取 `u`,用现有安全 URL 规则校验,再与完整配文比较;不访问跳转地址。回归先失败后通过,覆盖错误目标、无效协议、缺失目标、伪装域名、错误路径及截断正文仍被拒绝。Chromium 登录态短链实站通过,但其 href 本身是直接目标;包装形式先有实站 DOM 重放及回归证据,后续登录 Firefox 后,又以真实包装 href 通过原生保存和来源阻断后重开。 + +### 已修复:普通文字被转成字母序号 + +LinkedIn 实站 Markdown 的 `Dr. Nancy Grace Roman:` 在实例 `contentJson` 中变成有序列表,`Dr.` 丢失;旧笔记重开与其错误存储节点一致,说明仅比较“重开等于存储”不足以证明正文完整。原因是 TipTap 还识别字母/罗马序号,而 Turndown 的默认文本转义未覆盖。扩展 HTML 转 Markdown 的文本节点现在转义行首字母序号标点;不改服务端解析、存储结构或既有笔记,不会自动修复已保存的数据。真实 HTML 列表仍由转换器生成,代码块保留原字节。 + +新增回归先复现 `Dr.`、`Mr.`、`II.` 丢失,再验证文字、真实有序列表及代码块均保留;已有 Bluesky 引用作者 `Mr. Joshua` 用实际文档解析后的文字验证。补丁可独立回退,主要风险是列表/代码格式变化,已覆盖对应回归。Firefox 最终构建重新剪藏同一访客帖后,完整来源正文(含 `Dr.`)、八张归档图及来源阻断后重开均通过。 + +## 2026-10-17 第二轮补充验收 + +- Firefox 新增 V2EX、微信公众号、雪球、豆瓣书评、Substack Note、Facebook 公开 Float 详情和 LinkedIn 个人 share 访客版保存与重开。前五例在 Android 初始化修复构建上执行;Facebook/LinkedIn 和跨文档复验使用本轮最终构建。34 张实例图片在 Firefox 的本地来源策略下全部解码;V2EX 两条附言、微信公众号正文、雪球股票标记、豆瓣 7 段和 Substack Note 5 段均核对,LinkedIn 保留完整长文及作者/发布时间,未混入网页的 AI 摘要标题或评论。LinkedIn 来源规范化为相同 ID 的 `urn:li:share`,未将 URL 规范化误报为串帖。 +- Edge 最终构建新增 V2EX、掘金、微信公众号、豆瓣书评、雪球、Substack Note 六例。原生工具栏保存后,六条笔记在 Edge 的 Web 编辑器中重开;正文一致,33 张实例图片全部在阻断外站图片/连接后解码。掘金和微信公众号分别为 2/21 张,豆瓣/雪球/Substack Note 分别为 6/3/1 张。Substack Note 在未完成页面加载时的拒绝不计为产品故障;完成加载后保存与回读通过。 +- ego lite 通过原生右键保存付费预览中明确选中的 221 字段落,仅保存选区及来源/抓取时间,未带入整篇内容;来源阻断后重开一致。最终构建的 Facebook 信息流正常自动展开仍可保存完整正文和 3 张图片,并在来源阻断后重开。 +- 新版即刻 Web 信息流再次实际右键保存,安全拒绝且零新增。目标卡片没有自身稳定 DOM 帖子链接,正文中的即刻链接属于被引用的其他帖子,不能用于身份绑定;React 私有对象不作为稳定 DOM 契约。详情通过不能替代这一信息流布局,仍保留缺口。 +- 本轮 15 条合格保存场景共 70 张实例附件完成各自浏览器重开解码(Firefox 34、Edge 33、ego lite 3)。另有 4 条用于竞态复现或非目标探针,不计为新增验收通过。本轮全部 19 条笔记、80 个资源均按 ID 清理,80 个文件均核对实际大小与 SHA-256。原有 40 条笔记内容摘要及 40/74/1/13 的笔记、资源、Token、笔记本 ID 集合保持一致。 +- Edge 本地 Web 原会话报 `http_500`,验收时仅临时替换 `127.0.0.1` 的 `edgeever_session`;已按备份恢复原 Cookie 值并由浏览器回送核对。未修改认证代码或其他网站会话。两类浏览器临时读回工具不调用捕获函数;实际保存均使用原生入口。临时工具和接收器清理、专用 Token 撤销及凭据清理另有收据;Firefox 独立配置及主机授权保留。 + +### 已修复:展开期间跨文档离页 + +修复前两次实站复现:真实 Facebook“展开”点击回调内调用 `location.assign`,旧页面仍在约 52 毫秒后写入原帖。帖子身份和正文正确,但没有按预期取消。诊断记录显示标签页 URL 到约 350 毫秒后才更新,单靠最终 URL 比较不能识别已经开始的跨文档离页;同页隔离世界中的 `beforeunload` 在导航请求后约 1 毫秒触发。 + +现在平台捕获脚本只在本次读取期间监听 `beforeunload`,收到离页信号后将结果判定为页面已切换,并在结束时移除监听;原有 URL、发送者和帖子身份检查保留,不增加权限。回归先复现旧实现错误返回完整内容,再验证离页拒绝及正常展开成功。ego lite 的同一展开/跨文档跳转复验零新增,正常展开另存通过;Firefox 公开 NASA 信息流也通过原生右键触发真实展开和跨文档导航,零新增。该测试是受控导航,不是自然卡片复用证明。小宇宙的一次探针没有实际触发展开,未计入取消通过。 + +补丁仅涉及平台页面读取的取消判断,可独立回退;主要风险是误取消正常读取,已用正常展开和实际保存检查。本轮最终 **455 项扩展测试、0 失败**,Web/移动端类型检查、Web 构建及两种扩展构建通过。修复尚未提交或推送。 + +## 2026-10-17 后续实站验收 + +- ego lite 最新审查构建:Threads NASA 4/4 图、Instagram NASA 3/3 图、即刻登录 Web 长文 1/1 图及九宫格 9/9 图均通过原生工具栏保存和来源阻断后重开。另完成 Instagram Google 的 20 项媒体样本:1 个视频封面、19 张图片全部归档,作者、完整合并配文和图序保持一致。37 个附件与独立下载的原图逐一匹配。此前自然部分失败未复现,保留原失败记录,不推定其根因。早期两次遗留文字选区的操作只保存了选区,未计为整帖通过。 +- Firefox 158.0 最新构建:少数派 9/9 图、掘金 2/2 图、即刻公开 H5 九宫格 9/9 图均通过原生工具栏保存。三条笔记在 Firefox 自身重开,正文一致,20 张图从实例解码。掘金三列、五行表格的所有单元格一致,5 段代码内部文字和空行一致;每段仅省略一个末尾排版换行。 +- Edge 155.0.4283.62 最新构建:小宇宙封面 1/1、即刻公开 H5 九宫格 9/9 图全部归档;少数派 9 个图片位置归档 8 张,正文保留,明确提示部分图片仍为外链。在 Edge 自身重开三条笔记,正文一致,18 张实例附件解码,剩余外链图片被策略阻止。该图片带 Referer 的独立下载成功,无 Referer 请求返回 403;扩展已有该站 Referer 规则,仍不足以说明浏览器内那次失败的根因。 +- Edge/Firefox 自身回读使用局限于测试 Web 页的 `img-src`/`connect-src` 策略,明确阻止非实例来源,记录实际 CSP 拦截事件;未修改浏览器全局安全设置。Edge 因 Apple Events 页面脚本接口关闭,使用两份临时扩展测试页,只读取同一测试窗口中的本地实例页,保存仍全部来自原生入口。测试页和本地接收器已删除/停止,产品 background/capture 构建摘要未变。Edge Bluesky 停在网站加载画面,扩展拒绝并且零新增,不能算帖子捕获缺陷或通过。 +- 在真实 Facebook NASA 信息流,通过原生右键保存触发真实“展开”控件,在该点击回调内实施受控 `history.pushState` 导航。记录包含折叠正文、扩展程序点击、导航前后 URL 和时刻;提示为“剪藏时页面内容已切换,尚未保存。请在目标详情页重试。”,数据库无新增笔记。这覆盖明确时点的 SPA 导航取消,尚不覆盖跨文档导航或自然滚动导致的卡片复用。 +- Firefox 未登录 BOSS 页面实际显示“登录查看完整内容”,工具栏提示需要登录,整篇保存被拒绝且零新增。随后明确选择可见的 117 字完整段落,原生工具栏只保存该段落,没有其他岗位要求或受限提示;Firefox 内重开一致。未进行登录后完整岗位验收。 +- 14 条续验测试笔记、75 个附件及 1 个专用 Token 已按 ID 清理。75 个附件均经 API 下载核对大小/SHA-256,并在各自来源阻断的浏览器回读场景正常解码(ego lite 37、Firefox 20、Edge 18)。原有 40 条笔记、74 个资源、1 个 Token、13 个笔记本的 ID 集合及原笔记内容摘要不变。 +- 三个浏览器只移除了匹配本轮专用 Token 的设置值,服务端 Token 已撤销并验证返回 401;明文凭据文件、临时扩展测试页、本地接收器均已清理。ego lite 验收空间与专用 Edge 窗口已关闭。保留独立 Firefox 测试配置及主机授权以便继续验收,已移除其本轮 Token 和复制的本地会话 Cookie,停在空白页;日常 Firefox 未改动。 + +## 本轮已观察结果 + +- ego lite 原生工具栏:HN、小宇宙、少数派、掘金、微信公众号、V2EX、豆瓣书评、Substack Note、雪球、Bluesky 详情完成保存与来源阻断后的重开。HN 另完成真实详情页右键保存。所有结论仅限对应样本;豆瓣其他类型、Substack 完整文章及信息流未因此通过。 +- 即刻:公开主题信息流当前首帖 `6ad22db3a1af2007be120932` 完成真实单卡右键保存,作者、正文、1 张原图、目标身份匹配,排除下一帖,来源阻断后重开通过。用户完成扫码后,新版 Web 长文和九宫格通过真实工具栏保存:作者、日期精度、完整末段及 1/9 张原图均保留,提及头像与评论排除。10 张图片与独立源下载字节一致,来源阻断后重开通过。当前详情直接展示全文,没有展开按钮;新版 Web 信息流单卡及其展开仍未完成,不能以详情或移动 Web 替代。 +- Edge:初轮 HN/小宇宙工具栏保存通过,小宇宙在 Edge 自身重开,完整末段和 300×300 实例封面可读。加载当阶段修复构建后,HN 工具栏与原生右键保存通过;再次保存小宇宙则 1 张封面保留外链,提示准确,故最终图片验收仍是部分完成。HN 保留用户现有沉浸式翻译产生的可见译文。 +- Firefox:端口修复后连接返回 13 个笔记本,HN/小宇宙原生工具栏保存通过,小宇宙所有图片归档。两条笔记在 Firefox 自身重开;HN 与 Show Notes 末段可见,300×300 封面来自实例资源地址。HN 原生右键及当阶段构建的工具栏保存也通过。其社交登录态、信息流和 Firefox 自身来源阻断仍待补。 +- Facebook:初轮因未识别页面声明的 `pfbid` 别名而拒绝;修复后实际工具栏保存通过,NASA 作者、精确发布时间 `2026-10-10 04:54:33`、完整正文和 1 张实例图片均保留。来源阻断后正文与图片通过。另在已登录 NASA 信息流通过真实单卡右键自动展开并保存,目标身份不变、完整正文及 3 张图片正确,来源阻断后重开通过。初轮“收起”控件混入正文,修复后复验已排除,正文中的同名字词仍保留。 +- LinkedIn:初轮 activity 地址跳转 ugcPost 后误报切页;记录证实当前标签页/DOM URL 已一致,但扩展 `sender.url` 仍是初始地址。修复后原生保存、作者和职务、正文、8 张图片归档及来源阻断后重开通过;其余 activity/share/访客样本仍待补。 +- Threads:登录态 NASA 详情保存正文及 4 个图片位置,实际归档 3 张、保留外链 1 张,提示准确。Instagram:NASA 详情保存完整配文及 3 个位置,归档 2 张、保留外链 1 张,提示准确。所有已归档图片在阻断来源后可读;两平台完整图片验收未通过。Threads 缺图随后独立下载及扩展内下载成功,首次失败根因未确认,未加入无依据重试。 +- Medium 样本跳转自定义域名后遭遇 Cloudflare 验证,未绕过、未保存。Substack 付费预览原生保存拒绝整篇且未新增笔记,显式可见选区仍待补。BOSS 当前已登录且完整可读,保存成功,不能充当登录遮罩拒绝的证据;完整岗位另行跟踪。 +- 附件独立审计:本轮 30 条测试笔记、77 个成功归档资源;77 个文件的实际字节数及 SHA-256 全部匹配实例元数据。77 个资源均已在 ego lite 阻断来源/CDN 后从实例地址解码。小宇宙初轮三浏览器归档封面均为 70,133 字节,SHA-256 与独立源 CDN 下载一致。此结论不涵盖仍为外链的图片,也不代表所有客户端冷启动离线可用。 +- 正文比较按存储节点及 UI 类型分别核对:少数派的 3 个 PDF 链接卡片单独检查;雪球的 3 个股票标记已存为现有 `inlineMath` 节点,其属性与 KaTeX annotation 一致,除去重复的 MathML/HTML 表示后正文匹配。未将编辑器控件或非文本节点属性误报为正文丢失。 + +### 已修复:Firefox 非默认端口实例权限 + +Firefox 不支持在 WebExtension match pattern 中写端口。旧模式 `http://127.0.0.1:8787/*` 看似获准,但实际请求报 `NetworkError`。现在权限申请、后台检查和诊断共用不含端口的主机模式;HTTP 请求继续使用配置的完整实例地址。权限覆盖同协议同主机所有端口,Token 实际发送目的地仍由实例配置决定。升级后可能需要重新允许主机权限:本轮 Firefox 及 ego lite 均出现新提示;拒绝授权时不执行保存。未修改 API CORS、认证或数据存储。详见 [Mozilla 文档](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns)。 + +回归覆盖非默认 HTTP/HTTPS 端口及 IPv6:请求权限后连接到原端口;原有同步用户手势和拒绝后停止捕获用例仍通过。Firefox 实站复验消除了连接错误并完成保存与图片回读。回滚仅需撤回这三处权限消费者及共享函数的修改;常规 HTTPS 实例仍需持续跨浏览器覆盖。 + +### 已修复:SPA 消息 URL 与 Facebook 帖子别名 + +- SPA:不再将扩展消息的初始 `sender.url` 当作实时地址。保留扩展 ID、目标标签页、顶层 frame 与请求 ID 校验,页面内读取前后仍检查实时 URL,写入笔记前仍检查当前标签页 URL。回归覆盖旧 sender URL 可保存当前帖、真实切页拒绝保存,以及原有伪造发送者、展开与排队取消路径。 +- Facebook:仅接受 `CometSinglePostDialogRoute` 中与请求帖子 URL 匹配的路由声明,将 `params.story_token` 作为同帖别名;原有数字 ID 映射继续支持。作者、时间和图片完整性检查使用同一组已确认别名。回归覆盖有效别名及无关帖子、错误路由、无效别名;不放宽到整页或邻帖兜底。 +- 两项修复均只修改扩展捕获链路,未调整 API、数据结构或认证。最坏风险是错误接受旧页面/邻帖,因此保留上述身份和导航边界并验证拒绝路径;可分别撤回对应补丁。跨平台实站边界以矩阵为准。 + +### 已修复:新版即刻 Web 详情与 Facebook 收起控件 + +- 即刻 Web 登录后实际使用 `/u//post/`,新增这一公开详情路由识别,并以单个可见详情卡片及其顶层作者链接匹配路由。读取正文容器,保留长文换行、提及文字、链接及预览封面,排除头像和评论;日期仅保留网页给出的日精度,不猜测秒数。 +- 九宫格仅对实站确认的 `cdnv2.ruguoapp.com` 图片查看器路径去掉纯 `imageMogr2/…` 缩略图参数,保留原图顺序;不处理含其他签名参数的查询。9 张原图均独立下载并与实例文件逐字节核对。 +- 新增两份只含公开目标卡片的脱敏夹具及来源说明。覆盖正常长文/原图、作者不符、候选不唯一、隐藏/缺失正文、错误帖子、预览头部冒充作者及保存前真实 URL 切换的拒绝路径。未读取认证 API;新版 Web 信息流没有可靠 DOM 帖子 ID,未放宽为整页或按位置保存。 +- Facebook 仅移除正文中语义为按钮的“收起/See less”,保留普通正文中的同名字词;实站自动展开后重存及 2 个回归用例通过。两项补丁可独立撤回;风险主要是网页布局变化,应按本记录重新验收。 + +### 后续审查:即刻详情可见性 + +- 回归夹具复现两处遗漏:隐藏的作者链接仍可匹配路由身份;隐藏的作者/日期仍会进入元数据。身份链接现在沿用共享可见性判断,元数据从可读头部副本提取,避免保存网页未展示的信息。 +- 新增 6 个回归用例,覆盖 `hidden`、`aria-hidden` 和 `display:none`,修复前均失败、修复后均通过。隐藏“展开”控件已被既有逻辑忽略,未增加额外机制。补丁只影响即刻详情读取,可单独回退;这次修复未重新做实站验收,不能追加到浏览器通过项。 +- 对权限、SPA 消息、Facebook 别名和即刻读取路径完成正确性、简化及需求边界复审,未发现其他有证据的待处理实质问题;精确切页时序和同作者卡片复用仍属于下文的实站验证缺口。 + +## 本地检查与发现 + +- `bun run typecheck`、`bun run typecheck:mobile`、`bun run build:web` 通过。初次类型检查混入旧 ProseMirror 链接;`bun install --frozen-lockfile --force` 后恢复,未修改锁文件或依赖版本。 +- Chromium/Firefox 扩展构建通过。构建结果不计为实站验收。 +- 后续审查修复后,完整扩展测试为 **452 通过、0 失败**;上述三项检查及 Chromium/Firefox 扩展构建再次通过。 +- `bun run test:extension` 两次均为 424 通过、3 失败:首个 React 弹窗用例在 5 秒后超时,后两项出现重叠 `act()` 和 DOM 状态错误;原先单独执行弹窗测试为 7 通过,新增端口回归后 `bun test apps/extension/popup.test.mjs` 为 8 通过。新增端口回归用例后,本次完整执行为 **446 通过、0 失败**;未修改超时设置,先前间歇性执行问题未归因,不据此认定产品故障。 +- `bun run dev` 重启后 Web/API 均返回会话接口 HTTP 200。首次启动与依赖重装重叠,构建遇到依赖解析失败;日志单独保留。 +- 已创建本轮专用最小范围 API Token,三个浏览器均已配置并获准访问本地实例;即刻 Web 扫码登录已完成。本轮 30 条笔记、77 个资源及专用 Token 已按 ID 清理;临时 Firefox 配置和凭据文件删除,验收任务空间关闭。原有 40 条笔记、74 个资源、1 个 Token、13 个笔记本的 ID 集合与原笔记内容摘要均保留。 + +## 交付状态 + +本轮 macOS 验收记录及修复已整理,清理完成;修复尚未提交、推送,未发布 GitHub 评论或关闭 Issue。运行证据保留在本机,公开记录不得包含 Token、会话或私人笔记正文。 + +第九轮另已通过 Edge 指定登录雪球、Facebook、即刻及 Bluesky 信息流单卡;第十轮已修复两处排版残留及 Medium 预览卡遗漏,并补齐 Edge 豆瓣四类和 ego lite Medium 公开全文。后续定向缺口已在第十一/十二轮补齐,保留未采样版式与自然卡片复用边界;Edge/Firefox 已通过指定 Medium 公开全文,Edge 原样本仅当前可见预览;Edge 指定 Threads 两详情与 LinkedIn 登录 UGC 已通过。即刻新版 Web 信息流、LinkedIn 登录公司信息流、Bluesky 引用信息流,以及三浏览器的 Substack 完整文章已补齐上述指定样本。Chrome/Chromium 条件不额外要求再安装原生 Chrome;Android USB 两例及兼容声明已核实,未扩展为全部手机版式通过。历史偶发图片外链原因仍是观察项,部分失败反馈与成功附件回读已验证,不自动增设根因修复门槛。BOSS 登录后完整岗位另行跟踪。不得据此宣称所有浏览器和版式全部通过。 + +## 2026-10-18 第十一轮:补齐 #493 Medium 代码与 Threads 引用 + +沿用已登录 Edge 窗口 `1529310870`(155.0.4283.62)、本地实例和现有访问授权;加载当前 Chromium 构建,使用真实工具栏“剪藏当前网页”,只读辅助页不调用保存接口。原七个登录标签保留。 + +实站发现并修复两类问题。Medium 实际代码为带 `hljs-*` 子节点的 `pre`,没有语言 class;语言存在于页面已加载的 Apollo `Paragraph.codeBlockMetadata.lang`,多行使用 `br`。现将可读副本中的 `br` 转为换行;仅当段落 ID 和可见代码原文均精确匹配时补充语言,保留已有语言声明。只解析限定大小的 JSON 赋值,不执行脚本,不从状态补取正文或付费内容。Threads 真实引用使用嵌套 `data-pressable-container`,引用的第二个时间戳曾导致主帖误拒绝;现按所属容器识别主帖时间戳,并从主帖可读性证明中排除引用容器,继续拒绝用引用/邻帖文字替代隐藏主帖。 + +价值是恢复原要求中的代码高亮及引用保存。影响限于 Medium 读取器和 Threads 卡片识别;最坏风险为语言误匹配或引用边界误判,各可独立撤回。未修改实例认证、存储、迁移、接口、主机权限或增加依赖。新增两项真实结构回归覆盖代码换行/语言、状态文本不替代可见正文、源 DOM 不变,以及引用/邻卡与隐藏主帖隔离。 + +| 原生来源 | 保存与来源对照 | 屏蔽来源后重开 | +| --- | --- | --- | +| [Medium JavaScript Fundamentals-Part 1](https://medium.com/@emailforbarinder/javascript-fundamentals-part-1-1a538c9389fe) | 全文 40 个代码块逐字符/换行一致;38 个 JavaScript、2 个 XML;1/1 正文图归档 | 同一 Edge Web 编辑器正文一致,40 块语言和代码一致,551 个实际语法片段及不同计算颜色;本地图片 512×512 解码 | +| [Threads 文字引用](https://www.threads.com/@mosseri/post/DeAlGo0I71T) | 主帖、techmeme 引用正文、作者与引用永久链接保留 | 正文与存储节点一致;此例没有引用照片 | +| [Threads 含图引用](https://www.threads.com/@mkbhd/post/DehHSaCnEmU) | 主帖、被引用者正文/身份/链接和 1/1 引用原图保留 | 正文一致,本地图片 1206×1692 解码 | + +重开页面只允许本地连接/图片;MAIN 环境探针确认三来源及两 CDN 的 5 次 fetch 和 5 次图片加载均被实际 CSP 阻断。三个最终样本的两张图和修复前 Medium 对照图,共三资源均通过 API 字节数/SHA-256 和独立来源下载逐字节对照。修复前 Medium 样本不计入通过。 + +本轮 **483 项扩展测试通过、0 失败**;根/移动端类型检查、Web 构建、Chromium/Firefox 扩展构建及 diff 检查通过。Firefox lint 0 错误、0 提示、7 条既有依赖警告。完整 3,743 项非 E2E 属于前轮结果,本轮没有重复宣称最新完整套件;Firefox 本轮只构建,未将 Edge 结果扩为其运行时通过。 + +两个定向缺口已补齐,#588 其他历史边界及 #587/#589 范围保持。清理 4 条测试笔记、3 个资源及专用 Token,撤销后 401;原有 40 笔记/74 资源/1 Token/13 笔记本 ID 集合和 40 条内容摘要完全一致。浏览器仅移除本轮 Token,其他设置和站点登录保留;六个辅助文件、两份凭据文件与两个接收服务清理,52 个产品文件摘要与最终构建一致。未提交、推送、部署、回写或关闭 Issue。证据:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/eleventh`。 + +## 2026-10-18 第十二轮:最终构建跨浏览器定向复验 + +沿用 Firefox 158.0 原独立测试配置和 Edge 原七标签窗口。用户授权新建唯一 ego lite 空间 23,并分别完成 Firefox/ego lite 的 Medium 人机验证;复验后空间结束。Firefox 重载的来源是当前 `dist-firefox/manifest.json`,ego lite 重载当前 Chromium 扩展;同一版本号不替代构建摘要。没有修改产品代码、权限、依赖或运行配置,沿用第十一轮的 483 项扩展测试及类型检查/构建结果,不重复算作新一轮自动测试。 + +| 浏览器/来源 | 原生保存、来源对照与自身编辑器重开 | +| --- | --- | +| Firefox、ego lite:Medium `1a538c9389fe` | 各自真实工具栏保存。40 个代码块逐字符及换行匹配来源,语言保留;屏蔽来源/CDN 后,各自编辑器生成 551 个实际高亮片段并保留不同计算颜色,正文和 1/1 本地图片正常。 | +| Firefox、ego lite:Threads `@mkbhd/DehHSaCnEmU` | 各自原生保存,主帖、被引用者正文/身份/链接及 1/1 引用原图匹配来源;来源阻断重开正文一致、图片解码。Edge 同构建已于第十一轮通过。 | +| Firefox、ego lite、Edge:即刻 Web 九宫格 `6715e824d9dc81724c2ec74d` | 三者均保留目标作者、日期、两行配文及九个原图位置,不带评论。Firefox/ego lite 各 9/9 归档并在各自阻断来源的编辑器重开;Edge 8/9 归档,余下一张保留外链,明确显示“部分图片仍为外链”,不得计作九张本地附件通过。Edge 重开八张本地图解码,外链被策略阻断。 | +| Edge:BOSS 登录预览 | 页面实际出现“登录查看完整内容”;工具栏显示“需登录”和可保存选区说明,真实点击整篇保存后明确拒绝、零新增。正文受站点禁选样式影响,未完成本轮原生选区保存;Firefox 历史 117 字可见选区证据保留。 | +| ego lite:BOSS 当前条件 | 当前仍登录且可见完整正文,不退出用户账号或改变可见性来制造拒绝场景,未将此观察计为受限页保存通过。完整岗位另属 #589。 | + +Firefox/ego lite 各自策略探测覆盖三来源及十一图片地址:14 次连接和14 次图片加载均实际被 CSP 阻断;各自 11 张本地附件正常。Edge 的十个来源/图片地址同样实际阻断,八张归档图正常。七条最终笔记的 30 个成功归档资源全部经 API 大小/SHA-256 核验,并与本轮独立来源下载逐字节匹配。两个 Medium 来源代码、两条 Threads 主帖/引用正文和三条即刻配文/图序另与原页面对照,未仅以“重开等于存储”代替来源完整性。 + +本轮清理七条笔记、30 个资源和专用 Token,撤销后 401。原有 40 笔记/74 资源/1 Token/13 笔记本 ID 集合及 40 条内容摘要完全一致。三个浏览器只移除本轮 Token,其他设置和站点登录均保留;Firefox 原测试标签恢复空白、Edge 原七标签保留。八个辅助文件、两份凭据文件及两个接收服务清理,Chromium/Firefox 各52个产品文件与复验前最终构建摘要完全一致。无提交、推送、部署或 GitHub 写入。证据:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/twelfth`。 + +## 对照 #588 完成条件的交付核对 + +| Issue 完成条件 | 已有证据/允许边界 | +| --- | --- | +| 全部目标平台验收矩阵及环境、登录、预期、结果和未覆盖项 | 本文矩阵及逐轮记录已建立;不是穷尽每个平台的每种版式。 | +| Chrome/Chromium 真实工具栏、右键详情和支持的信息流单卡 | ego lite 的真实入口/单卡记录已具备;不附加原生 Chrome 安装门槛。 | +| Edge/Firefox 加载扩展、保存入口、正文/元数据、真实图片和重开 | 历史各平台记录与最终 Medium/Threads/即刻定向复验具备;不追溯扩大其他历史样本构建覆盖。 | +| 登录信息流/即刻 Web、展开、身份、图完整性、导航取消及受限拒绝 | 指定 Facebook/Threads/LinkedIn/Bluesky/雪球/即刻样本和受控真实导航已有证据;自然虚拟列表卡片复用未实测。BOSS 每浏览器同一预览状态不是原文独立门槛;Edge/Firefox BOSS 与 Substack 受限记录按实际条件保留。 | +| Firefox Android 支持范围与 USB 真机 | Android 142+ 声明已核实;USB Firefox 158.0 两例、14 图与重开已通过,范围明确为扩展菜单文章/公开社交详情;不宣称全部手机版式或商店安装/升级。 | +| 部分图片失败反馈、来源阻断附件回读、清理及缺陷修复关联 | 本轮及历史均有实际证据;Edge 8/9 诚实反馈与成功资源回读符合这项,不额外把偶发 CDN 根因当作新关闭门槛。 | + +按原 Issue 的上述范围,六项均已有对应证据,可提交给维护者结案审阅。保留自然卡片复用、未采样版式和各轮构建边界;#587 商店交付/更新与 #589 完整 BOSS 岗位分别跟踪。该结论不等于所有浏览器、全部版式和所有客户端冷启动离线均已通过,GitHub Issue 尚未回写或关闭。 + +## 2026-10-18 第十三轮:最终变更 review-loop + +审查当前 #588 全部未提交扩展改动及相关消费者,从正确性、简化和需求边界复核;身份/权限/取消与内容读取分别独立只读审查。继续在 `main`,保留无关 `apps/mobile/.gitignore`。 + +发现 Medium 代码规范仅处理所选节点本身为 `pre` 的情况:引用等容器先被整体选中后,内部 `pre` 被既有去重规则跳过,导致换行及语言遗漏;段落 ID 直接位于 `pre[data-selectable-paragraph]` 自身时也漏读语言。新增回归先复现引用内换行丢失,再将既有规范应用到所选可读副本内的代码块,并识别 `pre` 自身的段落 ID。通过 HTML→Markdown→存储节点验证两条路径的语言、字符和空行,源 DOM 不变。保持可见代码与状态原文精确匹配、已有语言声明优先,不增加正文来源。另撤去语言标签没有契约依据的 40 字符上限,保留与既有 Markdown 代码语言相同的字符格式校验。 + +改动仅限 Medium 读取器与回归,可独立撤回。最坏风险为代码语言或换行误处理;现有可见性/状态不替代正文/原文与已有语言回归及新增消费方验证约束了这些边界。两位独立审查者及主审最终三个视角均未发现未处理的实质问题。 + +最新完整非 E2E **3,746 项通过、0 失败**(3705 + 17 + 24),包含 **484 项扩展测试**;一个真实 Windows cwd 锁用例在 macOS 按既有平台条件跳过。根/移动端类型检查、Web/Chromium/Firefox 构建、Firefox lint 和 diff 检查通过;Firefox lint 0 错误、0 提示、7 条既有依赖警告。本轮为自动化和代码复审,没有新增原生实站、Android 操作、笔记或 Token;第十二轮实站结果仍绑定其当时构建,不追溯到本轮新包。无提交、推送、部署或 GitHub 写入。证据:`/private/tmp/edgeever-issue-588-review-final-20261018`。 + +## 贡献分支提交前复验 + +按[贡献指南](../CONTRIBUTING.zh-CN.md),从官方 `upstream/main` 的 `df22f55d` 建立 `fix/issue-588-browser-acceptance`,向 Fork 同名贡献分支提交;不推送部署 `main`。原补丁及十个新增文件迁移前后完全一致,无冲突;无关移动端 `.gitignore` 不纳入提交。 + +以新基线执行锁文件安装后,完整非 E2E **3,760 项通过、0 失败**(3719 + 17 + 24,含484项扩展测试);一个 Windows 专属用例按 macOS 条件跳过。根/移动端类型检查、Web/Chromium/Firefox 构建、Firefox lint 和 diff 检查通过,lint 保留7条既有依赖警告。本次未新增实站操作,不扩大各历史构建的运行时覆盖。 + +PR 首轮 CI 在项目根 `bun run lint` 发现即刻 CDN 查询前缀正则不符合 `startsWith` 规则;已改为等价字符串前缀判断。补跑根 lint、484 项扩展测试、根/移动端类型检查和 Web 构建通过。Firefox 插件 lint 与根 lint 是不同检查,不能互相替代;远端 CI 状态以 PR 检查结果为准。 diff --git a/docs/browser-clipper-platforms.md b/docs/browser-clipper-platforms.md index bd7ac114..0d345f13 100644 --- a/docs/browser-clipper-platforms.md +++ b/docs/browser-clipper-platforms.md @@ -2,7 +2,45 @@ [简体中文](browser-clipper-platforms.zh-CN.md) -Related to [Issue #493](https://github.com/tianma-if/edgeever/issues/493) and the [implementation plan](browser-clipper-platform-expansion-plan.md). Updated: 2026-10-15. Phase 1 is merged; Phase 2 and the remaining requested adapters are implemented locally; extension-store publication and full cross-browser live acceptance remain pending. +Related to [Issue #493](https://github.com/tianma-if/edgeever/issues/493) and the [implementation plan](browser-clipper-platform-expansion-plan.md). Platform adapters were merged through #505, #521 and #578; cross-browser and signed-in live acceptance continues in the [Issue #588 acceptance matrix](browser-clipper-acceptance-588.md). The records below retain their original code, environment and acceptance boundaries. + +The 2026-10-18 follow-up covers the specified current Jike Web feed, signed-in LinkedIn company feed, Bluesky quoted feed and complete Substack article in Edge/Firefox. See the matrix above for repairs and remaining browser-environment limits. + +The ninth round on 2026-10-18 adds Edge signed-in Xueqiu recommended, Facebook expanded, Jike following and Bluesky quoted feed cards, repairing classic Xueqiu targeting. Four notes and thirteen images reopen with sources blocked. See the acceptance record for minor formatting observations, build boundaries and cleanup evidence. + +The tenth round on 2026-10-18 adds Edge Douban movie/music/diary/comment and ego lite public Medium acceptance, repairing CJK bold boundaries, Bluesky badges and missing Medium in-story link previews. All 476 extension tests pass; see the acceptance record for native saves, source-blocked reopen and cleanup. + +The 2026-10-18 overall review fixes Jike button-media loss, Jike/Medium restored-visible descendants being rejected and missing Medium body h1 headings. All 3,743 non-E2E tests pass, including 481 extension tests; one Windows-only case is conditionally skipped. See the acceptance record for independent review and build boundaries. + +## 2026-10-18 audit against the original #493 requirements + +The [original issue and maintainer closure](https://github.com/tianma-if/edgeever/issues/493#issuecomment-6208560808) are fetched again and compared with current code, fixtures, regressions and live records. GitHub confirms #505, #521 and #578 are MERGED. All 15 groups/17 platforms have implementations. The issue closes development delivery; store publication, every browser/layout and full signed-in BOSS acceptance remain separate. “Live” below only means the recorded samples. + +| Original requirement | Implementation/evidence | Result/boundary | +| --- | --- | --- | +| Facebook: complete own post, actor, time, photos, exclude controls/comments | social-clip; expansion capture; signed-in Edge three-photo feed | Implemented; sampled live coverage | +| Instagram: post/carousel, merged captions, handle, time | social-clip; three-image and 20-media regression/live cases | Implemented; sampled live coverage | +| LinkedIn: full post, see more, name/job title, time/photos | social-clip/capture-platform; personal/job-title, company feed, SDUI UGC | Implemented; sampled live coverage | +| Substack: article, author/time, formatting, remove Subscribe/paywall UI | substack-content/article-metadata; full article, paid refusal/selection | Implemented; sampled live coverage | +| Medium: body/subtitle/author, code highlighting, remove recommendations | medium-content; capture-bundle code preservation; controlled language/highlight probe | Body/subtitle/two images live; rounds eleven/twelve verify 40 real code blocks/highlighting in all three browsers | +| Hacker News: Ask body/Show link, author, score/time | hacker-news-clip; body/link/UTC tests and native actions | Implemented; sampled live coverage | +| Threads/Bluesky: post, actor/time, quotes/original images | social-clip quote regressions; native Bluesky quotes | Edge text/photo quotes and Firefox/ego lite photo quotes pass; other recorded samples retained | +| BOSS: job, salary, city, duties/requirements/company | boss-clip; full-field and restricted fixtures | Implemented; full signed-in persistence/reopen belongs to #589 | +| Xueqiu: article/research, author/images, investment/stock tags | xueqiu-clip; stock-tag fixtures, signed-in eight-image feed | Implemented; sampled live coverage | +| Xiaoyuzhou: episode, complete Show Notes, timestamps, hosts/show | xiaoyuzhou-clip; explicit hosts, six nodes, over-one-hour timestamps | Implemented; no inference of hosts from show authors | +| WeChat: rich body, account/author, cover/inline images, cleanup | article-clip; code/lazy-image/cover/footer regressions, 21 live images | Implemented; sampled live coverage | +| Jike: complete/expanded post, actor/time, original single/grid images | jike-clip/jike-web-state; order/identity/expansion and live cases | Implemented; round twelve rechecks latest filtering in all three browsers | +| V2EX: body and every supplement as Markdown | v2ex-clip; two supplements/times, replies excluded | Implemented; sampled live coverage | +| Sspai/Juejin: body, code, structured large images | article-clip/platform-clip; language/indent/table and real archival | Implemented; Edge Sspai 8/9 correctly reports partial archival | +| Douban: book/movie/music long/short reviews and diaries | douban-clip; all types/three short-comment hosts, Edge follow-up | Implemented; sampled live coverage | + +Shared requirements are traced through actual persistence: `performPlatformSave` stores title, actor/job title, original time, source and platform tags. Medium/Substack use `createMemo` with `web-clip` and platform tags. Images enter existing `page-images` download/upload/rewrite with honest partial-failure feedback; this is not every client's cold-start offline proof. All 16 new menu messages are nonempty in zh_CN/en/ja. Platform extraction/identity/save regressions exist. The just-completed 3,743 non-E2E results are retained rather than rerunning everything for read-only inspection. + +The audit identified two targeted live gaps: a Medium code story and a Threads quote. Existing controlled code/quote regressions did not represent their current real DOM. Round eleven subsequently exposed and fixed Medium language/newline loss and rejection of nested Threads quote timestamps; the earlier controlled results were not live proof. + +Round eleven closes both checks using the signed-in native Edge extension: public Medium story `1a538c9389fe` preserves all characters/newlines in 40 code blocks and their 38 JavaScript/two XML languages. Reopening with sources/CDNs blocked produces 551 actual syntax spans. Threads text quote `@mosseri/DeAlGo0I71T` and photo quote `@mkbhd/DehHSaCnEmU` save/reopen with main and quoted captions, quoted actor and original photo intact. Independent source downloads match archived image bytes. These targeted gaps are closed without extending latest-build coverage to every layout or browser. Risk, checks and cleanup are recorded in [round eleven of #588](browser-clipper-acceptance-588.md). + +#493 retains its development-delivery closure boundary; #587 store delivery/update, #588 browsers/signed-in flows and #589 full BOSS jobs remain separate. Initial read-only audit evidence: `/private/tmp/edgeever-issue-493-audit-20261018`; repair/live evidence: `/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/eleventh`. No commit, push, deployment or GitHub issue update. ## 2026-10-15 PR submission verification @@ -311,3 +349,15 @@ The complete live-site save workflow with the extension loaded in each of Chrome The review corrected a truncated Instagram parent caption by saving the complete loaded per-slide captions, retaining independent parent text. It also bound Meta readability and context-menu targeting to the same post card, excluded hidden Facebook/Bluesky/LinkedIn bodies, removed an unsupported Bluesky image-count guess based on ordinary fractions, and stopped recognized unreadable Medium/Substack articles from falling back to whole-page extraction. Article identity and Markdown conversion now reuse shared helpers. The previous Instagram checks did not assert the final slide text; the new regression and Chromium editor check do. The end-to-end fixture suite covers all 15 samples through the real extension, local API, image archiving and reopening with source hosts blocked, plus unreadable/hidden refusal and cleanup. It uses the same workerd directly for extension, API assertions and editor API requests because the local development proxy intermittently times out. This verifies the local business flow, not the proxy or live CDN behavior across every site/browser. The HTML safety browser check also passes. Firefox is build/lint checked only; there is no new Firefox runtime acceptance in this review. + +2026-10-18 follow-up: the user manually triggered ego lite’s native toolbar twice for the full Substack article. Both saves archived 3/3 images and passed source/CDN-blocked reopening, with matching bodies and six decoded images. Test data and the dedicated token were removed, preserving original data. See the [#588 acceptance record](browser-clipper-acceptance-588.md). + +2026-10-18 sixth-round follow-up: Firefox added Instagram 20-media and Douban movie/music reviews, diary and a single short comment; Edge added Instagram 20-media and a public Facebook detail. Seven notes and 52 attachments passed actual saving and source-blocked reopening. Edge Threads/LinkedIn safely refuse under login overlays, with signed-in acceptance pending user sign-in; Firefox Medium remains blocked by site verification. Test data, tokens, copied sessions and helpers were cleaned, preserving original data and product artifact hashes. See the [#588 acceptance record](browser-clipper-acceptance-588.md). + +2026-10-18 seventh-round follow-up: after user sign-in in the original Edge window, Threads four-image/short-link details and LinkedIn SDUI UGC eight-image detail passed native-toolbar saving, complete source-body comparison and source-blocked reopening. All 13 attachments match independent source downloads in order. Three notes, 13 resources and the dedicated token were cleaned, preserving original data and website logins. Edge Medium still awaits user handling of Cloudflare verification. See the [#588 acceptance record](browser-clipper-acceptance-588.md). + +2026-10-18 Medium eighth-round follow-up: after separate user verification in Edge/Firefox, the public story “Come to my Medium Day talk” passed native saving, all 16 body blocks/five list items/ending, 1/1 image archival and source-blocked reopening in both browsers. The original story now has a member preview; Edge verifies only its 11 visible text blocks and one image, with no hidden paid body. Three notes/resources and the dedicated token were cleaned, preserving original data, site logins and verification state. See the [#588 acceptance record](browser-clipper-acceptance-588.md). + +2026-10-18 round twelve verifies the final build’s Medium 40 code blocks/highlighting and Threads photo quote in Firefox/ego lite, plus nine Jike image positions in all three browsers. Firefox/ego lite each reopen eleven local images with sources blocked; Edge Jike honestly reports 8/9 archival and reopens eight local files. Seven notes/30 resources/token cleanup, original-data checks and evidence for all six #588 completion conditions are in the [acceptance record](browser-clipper-acceptance-588.md). Ready for maintainer closure review, without a GitHub write; natural card recycling, unsampled layouts and #587/#589 limits remain. + +2026-10-18 round thirteen fixes Medium nested-container code and root `pre` paragraph language/newline omissions, removing an unsupported language-length limit. Full non-E2E 3,746 and extension 484 tests pass; independent reviews converge. This round is automated verification; prior runtime evidence is not retroactively assigned the new build. See the [acceptance record](browser-clipper-acceptance-588.md). diff --git a/docs/browser-clipper-platforms.zh-CN.md b/docs/browser-clipper-platforms.zh-CN.md index 9c8541f2..383f3b4a 100644 --- a/docs/browser-clipper-platforms.zh-CN.md +++ b/docs/browser-clipper-platforms.zh-CN.md @@ -2,7 +2,45 @@ [English](browser-clipper-platforms.md) -关联 [Issue #493](https://github.com/tianma-if/edgeever/issues/493) 和[实施方案](browser-clipper-platform-expansion-plan.zh-CN.md)。更新于 2026-10-15:Phase 1 已合并,Phase 2 与其余本次要求的平台适配已在本地实现;扩展商店发布和完整跨浏览器实站验收仍待完成。 +关联 [Issue #493](https://github.com/tianma-if/edgeever/issues/493) 和[实施方案](browser-clipper-platform-expansion-plan.zh-CN.md)。平台适配已通过 #505、#521、#578 合并;跨浏览器与登录态实站验收由 [Issue #588 验收矩阵](browser-clipper-acceptance-588.zh-CN.md)继续跟踪。以下各轮记录保留当时的代码、环境与验收边界。 + +2026-10-18 续验已补齐指定的即刻 Web 信息流、LinkedIn 登录公司信息流、Bluesky 引用信息流和 Edge/Firefox 完整 Substack 文章;新增修复及剩余浏览器环境限制见上述矩阵。 + +2026-10-18 第九轮补齐 Edge 登录雪球推荐列表、Facebook 展开列表、即刻 following 和 Bluesky 引用单卡,修复经典雪球类名定位;四条笔记及十三图来源阻断重开通过。轻微排版残留、构建边界和清理证据见验收记录。 + +2026-10-18 第十轮补齐 Edge 豆瓣影评/乐评/日记/短评及 ego lite Medium 公开全文,修复中文加粗边界、Bluesky 角标及 Medium 正文外链预览卡遗漏;476 项扩展测试通过,真实保存、来源阻断重开与清理结果见验收记录。 + +2026-10-18 整体复核修复即刻按钮媒体误删、即刻/Medium 可见后代被拒及 Medium 正文一级标题遗漏;完整非 E2E 3,743 项、扩展 481 项通过,1 个 Windows 专属用例按条件跳过。独立复审与构建边界见验收记录。 + +## 2026-10-18 对照 #493 原始需求复核 + +重新读取 [#493 正文及维护者结案说明](https://github.com/tianma-if/edgeever/issues/493#issuecomment-6208560808),逐项对照当前代码、夹具、自动回归和实站记录。#505、#521、#578 的 GitHub 状态均为 MERGED。原文 15 组/17 个平台均有对应实现;原 Issue 以开发交付结案,不把商店/全浏览器/完整 BOSS 实站混为同一完成条件。以下“实站”只指已记录的指定样本,不表示全部版式。 + +| 原要求 | 实现与证据 | 结论/边界 | +| --- | --- | --- | +| Facebook:单主帖全文、作者/组织、时间、图,排除交互/评论 | social-clip.ts;social-clip.test.mjs;Edge 登录列表展开三图 | 实现/对应样本实站已覆盖 | +| Instagram:单帖/轮播、合并配文、Handle、时间 | social-clip.ts;三图及20媒体样本回归/实站 | 实现/对应样本实站已覆盖 | +| LinkedIn:完整动态、see more、姓名/职务、时间、图 | social-clip.ts、capture-platform.ts;个人职务、公司列表、SDUI UGC 回归/实站 | 实现/对应样本实站已覆盖 | +| Substack:正文、作者、时间、排版、Subscribe/付费界面过滤 | substack-content.ts、article-metadata.ts;文章三图、付费拒绝及可见选区 | 实现/对应样本实站已覆盖 | +| Medium:正文、副标题、作者、代码块高亮、清理推荐 | medium-content.ts;capture-bundle.test.mjs 代码原文/结构;语言与高亮消费方受控探测 | 正文/副标题/两图实站通过;第十一/十二轮三浏览器 40 个真实代码块原文及高亮通过 | +| Hacker News:Ask正文/Show外链、发布者、积分、提交时间 | hacker-news-clip.ts;platform-clip.test.mjs 正文/链接/UTC | 实现/对应样本实站已覆盖 | +| Threads / Bluesky:单帖、作者、时间、引用、原图 | social-clip.ts;两站引用分支回归;Bluesky引用实站 | Edge 文字/含图引用、Firefox/ego lite 含图引用通过;其余指定样本有证据 | +| BOSS直聘:岗位、薪资、城市、职责/任职要求、公司简介 | boss-clip.ts;vertical-clip.test.mjs 全字段、受限拒绝夹具 | 字段已实现;登录完整岗位持久化/重开按 #589 跟踪 | +| 雪球:长帖/研报、作者、配图、投资/股票标签 | xueqiu-clip.ts;详情三图三股票、登录推荐卡八图 | 实现/对应样本实站已覆盖 | +| 小宇宙:标题、Show Notes全文、时间轴、主播、节目名 | xiaoyuzhou-clip.ts;明确阿丁/毛毛、六节点及超过一小时的时间轴 | 实现;仅保留明确主播,不猜节目作者 | +| 微信公众号:正文富文本、公众号/作者、头图/正文图、净化底栏 | article-clip.ts;代码组件/懒图/头图去重/底栏回归,21图实站 | 实现/对应样本实站已覆盖 | +| 即刻:全文、展开、作者、时间、单图/九图高清 | jike-clip.ts、jike-web-state.ts;原图顺序/身份/展开及实站 | 实现;第十二轮已在三浏览器复验九图位置,Edge 8/9 归档且如实反馈 | +| V2EX:主题正文及各条附言 Markdown | v2ex-clip.ts;两附言、各自时间、排除回复 | 实现/对应样本实站已覆盖 | +| 少数派 / 掘金:正文、代码块、结构化大图 | article-clip.ts、platform-clip.ts;语言/缩进/表格,真实图片转存 | 实现;Edge 少数派8/9图时正确报部分外链 | +| 豆瓣:书影音长评/短评、日记 | douban-clip.ts;长评/三主机短评定位/日记,Edge 四类补验 | 实现/对应样本实站已覆盖 | + +共同规范已核对保存消费方:专用平台通过 `background.ts` 的 `performPlatformSave` 写入统一标题、作者/职务、原发布时间、来源及平台 tags;Medium/Substack 通过 `createMemo` 写入 `web-clip` 和对应平台标签。图片都进入既有 `page-images` 下载/上传/地址改写,部分失败明确反馈,不等同于所有客户端冷启动离线验收。16 个新增平台菜单在 `zh_CN`、`en`、`ja` 的 message 均存在且非空。各平台均有对应提取/身份/保存回归;沿用刚完成的 3,743 项非 E2E 结果,不为纯只读核对重复跑整套测试。 + +复核时确认两项定向实站缺口:Medium 含代码文章及 Threads 引用帖。既有受控代码/引用回归未覆盖它们的当前真实 DOM;后续第十一轮因此发现并修复 Medium 语言/换行丢失和 Threads 嵌套引用时间戳误拒绝,不能把此前受控通过视为实站通过。 + +第十一轮已在保留登录的 Edge 原生扩展中补齐:公开 Medium 文章 `1a538c9389fe` 的 40 个代码块逐字符及换行一致,保留 38 个 JavaScript/2 个 XML 语言,屏蔽原站和 CDN 后重开仍有 551 个实际高亮片段;Threads `@mosseri/DeAlGo0I71T` 文字引用和 `@mkbhd/DehHSaCnEmU` 含图引用均保存/重开通过,主帖、引用正文、引用作者及原图保留。来源图片独立下载与实例附件逐字节一致。上述两个定向缺口已补齐;不扩报所有版式或其他浏览器的最新构建。详细风险、测试与清理见 [#588 第十一轮记录](browser-clipper-acceptance-588.zh-CN.md)。 + +#493 的开发交付结案边界保留;#587 商店交付/更新、#588 跨浏览器及登录态、#589 BOSS 完整岗位仍分别跟踪。初次只读核查证据:`/private/tmp/edgeever-issue-493-audit-20261018`;本轮修复及实站证据:`/private/tmp/edgeever-issue-588-20261017-uz6mqhpj/evidence/eleventh`。未提交、推送、部署或回写 GitHub Issue。 ## 2026-10-15 PR 提交验证 @@ -311,3 +349,15 @@ bunx playwright test tests/e2e/web-clipper-html-safety.spec.ts tests/e2e/web-cli 本轮修复 Instagram 父级配文截断后遗漏后半正文的问题,保存已加载的全部逐张配文并保留独立父级文案;Meta 可读性检查和右键定位共用同一帖卡边界;拒绝隐藏的 Facebook/Bluesky/LinkedIn 正文;删除依据普通分数正文推测 Bluesky 图片数量的逻辑;已识别但不可读的 Medium/Substack 文章明确失败,不再退回整页采集。文章识别与 Markdown 转换复用共享能力。先前 Instagram 验证没有断言最后一张图片的文案,本轮回归及 Chromium 编辑器验证已补齐。 端到端夹具测试覆盖全部 15 个样例,通过真实扩展、本地 API、图片转存及屏蔽来源后的编辑器重开,同时验证不可读/隐藏内容拒绝和清理。因本地开发代理间歇超时,扩展、API 断言及编辑器 API 请求均直连同一 workerd;这证明本地业务链路,不能视为代理故障已修复,也不代表所有实站 CDN 或浏览器均已验收。浏览器 HTML 安全检查通过。Firefox 仅完成构建与 lint,本轮未新增 Firefox 运行时验收。 + +2026-10-18 补验:ego lite 用户手动触发原生工具栏,同篇 Substack 完整文章的两次保存及各 3/3 图片归档通过;两条笔记均在来源/CDN 被阻断后重开,正文一致、六图解码。测试数据及专用令牌已清理,原数据完整保留。详见 [#588 验收记录](browser-clipper-acceptance-588.zh-CN.md)。 + +2026-10-18 第六轮补验:Firefox 新增 Instagram 20 项媒体及豆瓣影评、乐评、日记、短评单卡;Edge 新增 Instagram 20 项媒体及 Facebook 公开详情。七条笔记、52 个附件完成实际保存与来源阻断后重开。Edge Threads/LinkedIn 登录遮罩下正确拒绝,登录态待用户完成;Firefox Medium 访问验证受阻。测试数据、令牌、临时会话与工具已清理,原数据与产品构建摘要保留。详见 [#588 验收记录](browser-clipper-acceptance-588.zh-CN.md)。 + +2026-10-18 第七轮补验:用户在原 Edge 窗口登录后,Threads 四图/短链详情及 LinkedIn SDUI UGC 八图详情通过原生工具栏保存、完整来源正文核对及来源阻断后重开。13 个附件按顺序匹配独立源下载;三条笔记、13 个资源及测试令牌已清理,原数据和网站登录保留。Edge Medium 仍在 Cloudflare 验证页等待用户处理。详见 [#588 验收记录](browser-clipper-acceptance-588.zh-CN.md)。 + +2026-10-18 Medium 第八轮补验:用户分别通过 Edge/Firefox 的人机验证后,公开文章《Come to my Medium Day talk》完成两浏览器原生保存、16 个正文块/五项列表/末段核对、1/1 配图归档及来源阻断重开。原文章当前为会员预览,Edge 仅验证 11 个可见文本块及一张图,没有隐藏付费正文。三条笔记及三个资源和专用令牌已清理,原数据、网站登录及验证状态保留。详见 [#588 验收记录](browser-clipper-acceptance-588.zh-CN.md)。 + +2026-10-18 第十二轮补齐最终构建在 Firefox/ego lite 的 Medium 40 块代码高亮和含图 Threads 引用,以及三浏览器即刻九图位置保留。Firefox/ego lite 各11图来源阻断重开通过;Edge 即刻8/9归档且如实提示外链,八图重开通过。七条笔记/30资源及令牌清理、原数据核对和 #588 六项完成条件逐项证据见[验收记录](browser-clipper-acceptance-588.zh-CN.md)。可提交维护者结案审阅,未回写 GitHub;保留自然卡片复用、未采样版式及 #587/#589 边界。 + +2026-10-18 第十三轮整体复审修复 Medium 容器内代码与根 `pre` 段落身份的语言/换行遗漏,撤去无依据语言长度上限。完整非 E2E 3,746 项、扩展 484 项通过,独立复审收敛;本轮仅自动化验证,实站记录不追溯到新构建。详见[验收记录](browser-clipper-acceptance-588.zh-CN.md)。