Files
memind/mindspace-public-share-widget.mjs
T
john ee8677cc6d fix(learn): skip plaza share widget on learning assistant pages
Wire delivery path detection through portal static routes and strip baked share chrome from learn parent/child HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 18:50:01 +08:00

365 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import crypto from 'node:crypto';
import { injectBeforeDocumentClosingBody } from './html-document-injection.mjs';
const PUBLIC_FILE_SHARE_SCRIPT = `(function(){
var root=document.querySelector('[data-mindspace-public-share]');
if(!root)return;
var shareButton=root.querySelector('[data-action="share"]');
var captureButton=root.querySelector('[data-action="capture"]');
var plazaButton=root.querySelector('[data-action="plaza"]');
var wechatButton=root.querySelector('[data-action="wechat-draft"]');
var dialog=root.querySelector('[data-mindspace-public-share-dialog]');
var wechatDialog=root.querySelector('[data-mindspace-wechat-dialog]');
var plazaCancel=root.querySelector('[data-action="plaza-cancel"]');
var plazaConfirm=root.querySelector('[data-action="plaza-confirm"]');
var plazaAlreadyClose=root.querySelector('[data-action="plaza-already-close"]');
var plazaSuccessClose=root.querySelector('[data-action="plaza-success-close"]');
var plazaErrorClose=root.querySelector('[data-action="plaza-error-close"]');
var plazaAlreadyLink=root.querySelector('[data-plaza-already-link]');
var plazaSuccessLink=root.querySelector('[data-plaza-success-link]');
var plazaViews={
confirm:root.querySelector('[data-plaza-view="confirm"]'),
loading:root.querySelector('[data-plaza-view="loading"]'),
already:root.querySelector('[data-plaza-view="already"]'),
success:root.querySelector('[data-plaza-view="success"]'),
error:root.querySelector('[data-plaza-view="error"]')
};
var status=root.querySelector('small');
var timer=null;
var publishing=false;
var checking=false;
function setStatus(text,err,persist){
if(!status)return;
status.textContent=text||'';
status.classList.toggle('is-error',!!err);
if(timer)clearTimeout(timer);
if(text&&!persist)timer=setTimeout(function(){status.textContent='';status.classList.remove('is-error');},3200);
}
function cleanUrl(){var url=new URL(location.href);url.hash='';url.searchParams.delete('download');url.searchParams.delete('export');return url.toString();}
function withParam(url,key,value){var next=new URL(url);next.searchParams.set(key,value);return next.toString();}
function fallbackCopy(text){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();var ok=document.execCommand('copy');ta.remove();if(!ok)throw new Error('复制失败');}
async function copyText(text){if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(text);return;}catch(e){}}fallbackCopy(text);}
function workspaceRelativePath(){var parts=location.pathname.split('/').filter(Boolean);if(parts[0]==='MindSpace'&&parts.length>=3)return parts.slice(2).join('/');return'';}
function showPlazaView(name){Object.keys(plazaViews).forEach(function(key){var view=plazaViews[key];if(view)view.hidden=key!==name;});}
function openPlazaDialog(){if(!dialog)return;dialog.hidden=false;showPlazaView('loading');void refreshPlazaDialog();}
function closePlazaDialog(){if(dialog)dialog.hidden=true;showPlazaView('confirm');}
function apiErrorBody(payload){return payload&&(payload.error||payload)||{};}
function resolvePlazaUrl(payload){var body=apiErrorBody(payload);if(body.plaza_url)return body.plaza_url;var details=body.details;if(details&&details.plaza_url)return details.plaza_url;var postId=details&&(details.post_id||details.postId);if(!postId)return'';return location.origin.replace(/:\\d+$/,'')+':3001/plaza/p/'+encodeURIComponent(postId);}
function showAlreadyPublished(plazaUrl){showPlazaView('already');if(plazaAlreadyLink){if(plazaUrl){plazaAlreadyLink.href=plazaUrl;plazaAlreadyLink.hidden=false;}else{plazaAlreadyLink.hidden=true;}}}
async function refreshPlazaDialog(){
if(checking)return;
var relativePath=workspaceRelativePath();
if(!relativePath){showPlazaView('error');return;}
checking=true;
try{
var res=await fetch('/api/mindspace/v1/pages/quick-plaza-from-public-html/status?relative_path='+encodeURIComponent(relativePath),{credentials:'same-origin'});
var payload=await res.json().catch(function(){return{};});
if(!res.ok){
if(res.status===401)throw new Error('请先登录 MindSpace');
var errBody=apiErrorBody(payload);
throw new Error(errBody.message||'无法检查发布状态');
}
var data=payload.data||{};
if(data.published){showAlreadyPublished(data.plaza_url||resolvePlazaUrl({details:{post_id:data.post&&data.post.id}}));return;}
showPlazaView('confirm');
}catch(e){
showPlazaView('error');
setStatus(e&&e.message?e.message:'无法检查发布状态',true);
}
finally{checking=false;}
}
shareButton&&shareButton.addEventListener('click',async function(){var url=cleanUrl();var title=document.title||'MindSpace';try{if(navigator.share){await navigator.share({title:title,url:url});setStatus('已打开分享');return;}await copyText(url);setStatus('链接已复制');}catch(e){setStatus(e&&e.message?e.message:'分享失败',true);}});
captureButton&&captureButton.addEventListener('click',function(){setStatus('正在生成长图,请稍候');var link=document.createElement('a');link.href=withParam(cleanUrl(),'download','long-image');link.download='';document.body.appendChild(link);link.click();link.remove();});
plazaButton&&plazaButton.addEventListener('click',openPlazaDialog);
plazaCancel&&plazaCancel.addEventListener('click',closePlazaDialog);
plazaAlreadyClose&&plazaAlreadyClose.addEventListener('click',closePlazaDialog);
plazaSuccessClose&&plazaSuccessClose.addEventListener('click',closePlazaDialog);
plazaErrorClose&&plazaErrorClose.addEventListener('click',closePlazaDialog);
dialog&&dialog.addEventListener('click',function(event){if(event.target===dialog)closePlazaDialog();});
plazaConfirm&&plazaConfirm.addEventListener('click',async function(){
if(publishing)return;
var relativePath=workspaceRelativePath();
if(!relativePath){setStatus('无法识别页面路径',true);closePlazaDialog();return;}
publishing=true;
plazaConfirm.disabled=true;
showPlazaView('loading');
try{
var res=await fetch('/api/mindspace/v1/pages/quick-plaza-from-public-html',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:JSON.stringify({relative_path:relativePath})});
var payload=await res.json().catch(function(){return{};});
if(!res.ok){
if(res.status===401)throw new Error('请先登录 MindSpace');
var errBody=apiErrorBody(payload);
var plazaUrl=resolvePlazaUrl(payload);
if(errBody.code==='ALREADY_PUBLISHED'){showAlreadyPublished(plazaUrl);setStatus('该内容已在广场发布',false,true);return;}
throw new Error(errBody.message||'发布失败');
}
var data=payload.data||{};
var successUrl=data.plaza_url||resolvePlazaUrl({details:{post_id:data.post&&data.post.id}});
showPlazaView('success');
if(plazaSuccessLink&&successUrl){plazaSuccessLink.href=successUrl;plazaSuccessLink.hidden=false;}
else if(plazaSuccessLink){plazaSuccessLink.hidden=true;}
setStatus('发布成功',false,true);
if(successUrl)window.open(successUrl,'_blank','noopener,noreferrer');
}catch(e){
showPlazaView('error');
setStatus(e&&e.message?e.message:'发布失败',true);
}finally{publishing=false;plazaConfirm.disabled=false;}
});
if(wechatButton&&wechatDialog){
var wechatCancel=wechatDialog.querySelector('[data-action="wechat-cancel"]');
var wechatConfirm=wechatDialog.querySelector('[data-action="wechat-confirm"]');
var wechatSuccessClose=wechatDialog.querySelector('[data-action="wechat-success-close"]');
var wechatErrorClose=wechatDialog.querySelector('[data-action="wechat-error-close"]');
var wechatBlockedClose=wechatDialog.querySelector('[data-action="wechat-blocked-close"]');
var wechatForbiddenClose=wechatDialog.querySelector('[data-action="wechat-forbidden-close"]');
var wechatDraftId=wechatDialog.querySelector('[data-wechat-draft-id]');
var wechatHint=wechatDialog.querySelector('[data-wechat-view="confirm"] [data-wechat-hint]');
var wechatViews={
confirm:wechatDialog.querySelector('[data-wechat-view="confirm"]'),
loading:wechatDialog.querySelector('[data-wechat-view="loading"]'),
success:wechatDialog.querySelector('[data-wechat-view="success"]'),
error:wechatDialog.querySelector('[data-wechat-view="error"]'),
blocked:wechatDialog.querySelector('[data-wechat-view="blocked"]'),
forbidden:wechatDialog.querySelector('[data-wechat-view="forbidden"]')
};
var pushing=false;
var wechatChecking=false;
function isPageOwner(){return root.getAttribute('data-is-owner')==='true';}
function showWechatView(name){Object.keys(wechatViews).forEach(function(key){var view=wechatViews[key];if(view)view.hidden=key!==name;});}
function openWechatDialog(){
wechatDialog.hidden=false;
if(!isPageOwner()){showWechatView('forbidden');return;}
showWechatView('loading');
void refreshWechatDialog();
}
function closeWechatDialog(){wechatDialog.hidden=true;showWechatView('confirm');}
function setWechatHint(text){if(wechatHint)wechatHint.textContent=text||'';}
async function refreshWechatDialog(){
if(wechatChecking)return;
var relativePath=workspaceRelativePath();
if(!relativePath){showWechatView('error');setWechatHint('无法识别页面路径');return;}
wechatChecking=true;
try{
var res=await fetch('/api/mindspace/v1/pages/quick-wechat-draft-from-public-html/status?relative_path='+encodeURIComponent(relativePath),{credentials:'same-origin'});
var payload=await res.json().catch(function(){return{};});
if(!res.ok){
if(res.status===401)throw new Error('请先登录 MindSpace');
var errBody=apiErrorBody(payload);
throw new Error(errBody.message||'无法检查公众号草稿状态');
}
var data=payload.data||{};
if(!data.configured){showWechatView('blocked');setWechatHint('请先在 M 配置中绑定公众号 AppID 和 AppSecret。');return;}
var blockers=(data.validation&&data.validation.blockers)||[];
if(!data.canPush){showWechatView('error');setWechatHint(blockers[0]||'当前页面暂不符合推送要求');return;}
setWechatHint(data.pageTitle||'当前页面');
showWechatView('confirm');
}catch(e){
showWechatView('error');
setWechatHint(e&&e.message?e.message:'无法检查公众号草稿状态');
}finally{wechatChecking=false;}
}
wechatButton.addEventListener('click',openWechatDialog);
wechatCancel&&wechatCancel.addEventListener('click',closeWechatDialog);
wechatSuccessClose&&wechatSuccessClose.addEventListener('click',closeWechatDialog);
wechatErrorClose&&wechatErrorClose.addEventListener('click',closeWechatDialog);
wechatBlockedClose&&wechatBlockedClose.addEventListener('click',closeWechatDialog);
wechatForbiddenClose&&wechatForbiddenClose.addEventListener('click',closeWechatDialog);
wechatDialog.addEventListener('click',function(event){if(event.target===wechatDialog)closeWechatDialog();});
wechatConfirm&&wechatConfirm.addEventListener('click',async function(){
if(pushing)return;
var relativePath=workspaceRelativePath();
if(!relativePath){setStatus('无法识别页面路径',true);closeWechatDialog();return;}
pushing=true;
wechatConfirm.disabled=true;
showWechatView('loading');
setWechatHint('正在推送到公众号草稿箱…');
try{
var res=await fetch('/api/mindspace/v1/pages/quick-wechat-draft-from-public-html',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:JSON.stringify({relative_path:relativePath})});
var payload=await res.json().catch(function(){return{};});
if(!res.ok){
if(res.status===401)throw new Error('请先登录 MindSpace');
var errBody=apiErrorBody(payload);
throw new Error(errBody.message||'推送失败');
}
var data=payload.data||{};
showWechatView('success');
if(wechatDraftId){wechatDraftId.textContent=data.draftMediaId?'草稿 media_id'+data.draftMediaId:'';}
setStatus('已推送到公众号草稿箱',false,true);
}catch(e){
showWechatView('error');
setWechatHint(e&&e.message?e.message:'推送失败');
}finally{pushing=false;wechatConfirm.disabled=false;}
});
}
})();`;
const PUBLIC_FILE_SHARE_SCRIPT_HASH = crypto
.createHash('sha256')
.update(PUBLIC_FILE_SHARE_SCRIPT)
.digest('base64');
const PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN =
/<style id="mindspace-public-share-style">[\s\S]*?<\/script>\s*/i;
export function stripPublicFileShareWidget(html) {
let next = String(html ?? '').replace(PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN, '');
next = next.replace(/<div[^>]*data-mindspace-wechat-dialog[\s\S]*?<\/div>\s*/gi, '');
next = next.replace(/<div[^>]*data-mindspace-public-share[\s\S]*?<\/div>\s*/gi, '');
return next;
}
export function injectPublicFileShareButton(html, { isOwner = true } = {}) {
let source = stripPublicFileShareWidget(html);
if (!source) {
return { html: source, scriptHashes: [] };
}
// REGRESSION GUARD: mindspace-public-owner-vs-visitor — the "发布 Plaza" entry mutates content
// ownership (publishes the author's page under the visitor's own Plaza account), so it must only
// render for the page owner. Anyone else opening this URL (share link, forwarded chat link) is a
// visitor and must NOT see it. Everything else (长图 / 分享) stays available to all viewers.
const plazaDialogMarkup = isOwner
? ` <div data-mindspace-public-share-dialog hidden>
<div data-mindspace-public-share-dialog-panel role="dialog" aria-modal="true" aria-labelledby="mindspace-public-plaza-title">
<div data-plaza-view="confirm">
<h3 id="mindspace-public-plaza-title">发布到 Plaza</h3>
<p>将把当前页面发布并推送到发现广场,确认继续?</p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="plaza-cancel">取消</button>
<button type="button" data-action="plaza-confirm">确认发布</button>
</div>
</div>
<div data-plaza-view="loading" hidden>
<h3>请稍候</h3>
<p>正在检查发布状态…</p>
</div>
<div data-plaza-view="already" hidden>
<h3>已在广场发布</h3>
<p>该内容已发布到 Plaza,无法重复发布。如需重新发布,请先在广场删除原帖。</p>
<a data-plaza-already-link data-mindspace-public-share-dialog-link href="#" target="_blank" rel="noopener noreferrer">查看已有帖子</a>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="plaza-already-close">知道了</button>
</div>
</div>
<div data-plaza-view="success" hidden>
<h3>发布成功</h3>
<p>内容已推送到 Plaza 广场。</p>
<a data-plaza-success-link data-mindspace-public-share-dialog-link href="#" target="_blank" rel="noopener noreferrer">查看 Plaza 帖子</a>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="plaza-success-close">关闭</button>
</div>
</div>
<div data-plaza-view="error" hidden>
<h3>操作失败</h3>
<p>请稍后重试,或先登录 MindSpace。</p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="plaza-error-close">关闭</button>
</div>
</div>
</div>
</div>
`
: '';
const plazaButtonMarkup = isOwner
? ' <button type="button" data-action="plaza">发布 Plaza</button>\n'
: '';
// REGRESSION GUARD: mindspace-public-wechat-visibility — the "公众号" entry is visible to all
// viewers so visitors discover the feature, but only the page owner may push to WeChat draft.
const wechatButtonMarkup = ' <button type="button" data-action="wechat-draft">公众号</button>\n';
const wechatDialogMarkup = ` <div data-mindspace-wechat-dialog hidden>
<div data-mindspace-public-share-dialog-panel role="dialog" aria-modal="true" aria-labelledby="mindspace-wechat-draft-title">
<div data-wechat-view="confirm">
<h3 id="mindspace-wechat-draft-title">推送到公众号草稿箱</h3>
<p data-wechat-hint>将把当前页面按标准格式推送到你的公众号草稿箱,确认继续?</p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="wechat-cancel">取消</button>
<button type="button" data-action="wechat-confirm">确认推送</button>
</div>
</div>
<div data-wechat-view="loading" hidden>
<h3>请稍候</h3>
<p data-wechat-hint>正在检查页面与公众号配置…</p>
</div>
<div data-wechat-view="blocked" hidden>
<h3>尚未配置公众号</h3>
<p>请先在 M 配置中绑定公众号 AppID 和 AppSecret。</p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="wechat-blocked-close">知道了</button>
</div>
</div>
<div data-wechat-view="forbidden" hidden>
<h3>暂时无权限</h3>
<p>仅页面作者可推送到公众号草稿箱。</p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="wechat-forbidden-close">知道了</button>
</div>
</div>
<div data-wechat-view="success" hidden>
<h3>推送成功</h3>
<p>内容已写入公众号草稿箱,请登录微信公众平台审阅后发表。</p>
<p data-wechat-draft-id style="font-size:12px;opacity:.82;"></p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="wechat-success-close">关闭</button>
</div>
</div>
<div data-wechat-view="error" hidden>
<h3>操作失败</h3>
<p data-wechat-hint>请稍后重试,或先登录 MindSpace。</p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="wechat-error-close">关闭</button>
</div>
</div>
</div>
</div>
`;
const markup = `
<style id="mindspace-public-share-style">
[data-mindspace-public-share]{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;display:flex;flex-direction:column;align-items:flex-end;gap:6px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
[data-mindspace-public-share] div[data-mindspace-public-share-actions]{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
[data-mindspace-public-share] button{border:0;border-radius:999px;padding:10px 15px;background:#2f6f57;color:#fff;font-size:14px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.18);cursor:pointer}
[data-mindspace-public-share] button[data-action="capture"]{background:#8f6b2f}
[data-mindspace-public-share] button[data-action="plaza"]{background:#5a4a7b}
[data-mindspace-public-share] button[data-action="wechat-draft"]{background:#07c160}
[data-mindspace-public-share] button:active{transform:translateY(1px)}
[data-mindspace-public-share] button:disabled{opacity:.65;cursor:wait}
[data-mindspace-public-share] small{min-height:18px;max-width:220px;border-radius:999px;padding:4px 9px;background:rgba(255,255,255,.92);color:#245845;font-size:12px;text-align:right;box-shadow:0 4px 14px rgba(0,0,0,.1)}
[data-mindspace-public-share] small:empty{display:none}
[data-mindspace-public-share] small.is-error{color:#8b2d20}
[data-mindspace-public-share-dialog],[data-mindspace-wechat-dialog]{position:fixed;inset:0;z-index:2147483646;display:grid;place-items:center;padding:20px;background:rgba(7,12,10,.72);backdrop-filter:blur(8px)}
[data-mindspace-public-share-dialog][hidden],[data-mindspace-wechat-dialog][hidden]{display:none!important}
[data-mindspace-public-share-dialog-panel]{width:min(360px,100%);padding:24px;border-radius:22px;background:rgba(18,24,32,.96) !important;border:1px solid rgba(255,255,255,.12);box-shadow:0 24px 80px rgba(0,0,0,.38);text-align:center;color:#fff !important;color-scheme:dark;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
[data-mindspace-public-share-dialog-panel] h3{margin:0 0 10px;font-size:18px;font-weight:600;color:#fff !important}
[data-mindspace-public-share-dialog-panel] p{margin:0 0 16px;font-size:14px;line-height:1.5;color:rgba(255,255,255,.86) !important}
[data-mindspace-public-share-dialog-panel] [data-plaza-view][hidden],[data-mindspace-public-share-dialog-panel] [data-wechat-view][hidden]{display:none!important}
[data-mindspace-public-share-dialog-actions]{display:flex;justify-content:center;gap:10px;flex-wrap:wrap}
[data-mindspace-public-share-dialog-actions] button{border-radius:999px;padding:8px 18px;font:inherit;font-size:14px;cursor:pointer;box-shadow:none}
[data-mindspace-public-share-dialog-actions] [data-action="plaza-cancel"],[data-mindspace-public-share-dialog-actions] [data-action="plaza-already-close"],[data-mindspace-public-share-dialog-actions] [data-action="plaza-success-close"],[data-mindspace-public-share-dialog-actions] [data-action="plaza-error-close"],[data-mindspace-public-share-dialog-actions] [data-action="wechat-cancel"],[data-mindspace-public-share-dialog-actions] [data-action="wechat-success-close"],[data-mindspace-public-share-dialog-actions] [data-action="wechat-error-close"],[data-mindspace-public-share-dialog-actions] [data-action="wechat-blocked-close"],[data-mindspace-public-share-dialog-actions] [data-action="wechat-forbidden-close"]{border:1px solid rgba(255,255,255,.18) !important;background:rgba(255,255,255,.08) !important;color:#fff !important}
[data-mindspace-public-share-dialog-actions] [data-action="plaza-confirm"],[data-mindspace-public-share-dialog-actions] [data-action="wechat-confirm"]{border:1px solid rgba(47,111,87,.35) !important;background:#2f6f57 !important;color:#fff !important;font-weight:600}
[data-mindspace-public-share-dialog-link]{display:inline-block;margin:0 0 14px;font-size:14px;color:#8fd4b4 !important;text-decoration:underline}
[data-mindspace-public-share-dialog-link][hidden]{display:none!important}
@media(max-width:640px){[data-mindspace-public-share]{right:12px;bottom:calc(12px + env(safe-area-inset-bottom,0px))}[data-mindspace-public-share] button{padding:9px 13px;font-size:13px}}
</style>
<div data-mindspace-public-share data-is-owner="${isOwner ? 'true' : 'false'}">
${plazaDialogMarkup}${wechatDialogMarkup} <small aria-live="polite"></small>
<div data-mindspace-public-share-actions>
${wechatButtonMarkup}${plazaButtonMarkup} <button type="button" data-action="capture">保存长图</button>
<button type="button" data-action="share">公开分享</button>
</div>
</div>
<script>${PUBLIC_FILE_SHARE_SCRIPT}</script>`;
if (/<\/body>/i.test(source)) {
return {
html: injectBeforeDocumentClosingBody(source, markup),
scriptHashes: [PUBLIC_FILE_SHARE_SCRIPT_HASH],
};
}
return {
html: `${source}${markup}`,
scriptHashes: [PUBLIC_FILE_SHARE_SCRIPT_HASH],
};
}
export const publicShareWidgetInternals = {
PUBLIC_FILE_SHARE_SCRIPT_HASH,
stripPublicFileShareWidget,
};