Files
memind/mindspace-public-share-widget.mjs
T
john e2ad3bf62b feat(mindspace): 公开页分享组件、workspace 路径与 Plaza 发布增强
- 新增 public share widget 与 workspace relative path 解析
- 增强 publications/chat-plaza 发布链路与缩略图 demo
- 补充 schema、cover 检查脚本与回归测试

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 23:14:41 +08:00

206 lines
13 KiB
JavaScript

import crypto from 'node:crypto';
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 dialog=root.querySelector('[data-mindspace-public-share-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;}
});
})();`;
const PUBLIC_FILE_SHARE_SCRIPT_HASH = crypto
.createHash('sha256')
.update(PUBLIC_FILE_SHARE_SCRIPT)
.digest('base64');
export function injectPublicFileShareButton(html) {
const source = String(html ?? '');
if (!source || source.includes('data-mindspace-public-share')) {
return { html: source, scriptHashes: [] };
}
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: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]{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]{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]{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"]{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"]{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>
<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>
<small aria-live="polite"></small>
<div data-mindspace-public-share-actions>
<button type="button" data-action="plaza">发布 Plaza</button>
<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: source.replace(/<\/body>/i, `${markup}</body>`),
scriptHashes: [PUBLIC_FILE_SHARE_SCRIPT_HASH],
};
}
return {
html: `${source}${markup}`,
scriptHashes: [PUBLIC_FILE_SHARE_SCRIPT_HASH],
};
}
export const publicShareWidgetInternals = {
PUBLIC_FILE_SHARE_SCRIPT_HASH,
};