ac520d8ae5
工作区直链 /MindSpace/<userId>/public/*.html 此前对所有人(含匿名访客、 转发链接收到的其他登录用户)展示完全相同的悬浮操作按钮,导致非作者也能 看到"发布 Plaza"入口——而 Plaza 发布会把内容归属到操作者自己的账号下, 必须只对作者开放。 - server.mjs: serveUserPublishFile 用现有 session/cookie 鉴权判断访问者是 否等于 URL 中的 ownerKey,结果透传给 sendPublishFile;该响应内容因人 而异,显式加 Cache-Control: private, no-store(原来未设置任何缓存头) - mindspace-public-share-widget.mjs: injectPublicFileShareButton 新增 isOwner 参数(默认 true 保持兼容),非作者时隐藏"发布 Plaza"按钮与确认 弹窗,"保存长图"/"公开分享"对所有访客保留 - mindspace-public-delivery.mjs: 透传 isOwner,并注入图片重试脚本 - mindspace-public-image-retry.mjs(新增): 页面级 onerror 之前用捕获阶段 监听拦截 mindspace 资产图片的加载失败,带退避自动重试 3 次,避免刚生成 页面时的资产物化竞态被"永久占位符"放大成显示故障 本地起服务用 owner / 非 owner 已登录用户 / 匿名访客三种身份实测验证。 Co-authored-by: Cursor <cursoragent@cursor.com>
68 lines
2.4 KiB
JavaScript
68 lines
2.4 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
/**
|
|
* Public MindSpace pages (workspace `/MindSpace/<userId>/public/*.html`) reference images via
|
|
* `/api/mindspace/v1/assets/:id/download`, which is authorized per-request (owner cookie, signed
|
|
* `public_token`, or `/public/` referrer). Right after an agent generates a page, the very first
|
|
* image request can occasionally race asset materialization and return a transient 401/404/5xx.
|
|
*
|
|
* Generated pages usually ship a page-specific `onerror` fallback that permanently swaps the
|
|
* `<img>` for a static placeholder — so a single transient failure becomes a permanent "broken
|
|
* image" until the visitor manually reloads. This script intercepts image load failures for
|
|
* MindSpace asset URLs (any page, any author) and retries a few times with backoff BEFORE the
|
|
* page's own `onerror` gets a chance to run; only once retries are exhausted does it let the
|
|
* original fallback take over.
|
|
*/
|
|
const IMAGE_RETRY_SCRIPT = `(function(){
|
|
var MAX_RETRIES=3;
|
|
var BASE_DELAY_MS=900;
|
|
var ASSET_PATTERN=/\\/api\\/mindspace\\/v1\\/assets\\/[^/]+\\/download/;
|
|
var retries=new WeakMap();
|
|
document.addEventListener('error',function(event){
|
|
var img=event.target;
|
|
if(!img||img.tagName!=='IMG')return;
|
|
var src=img.getAttribute('src')||'';
|
|
if(!ASSET_PATTERN.test(src))return;
|
|
var count=retries.get(img)||0;
|
|
if(count>=MAX_RETRIES)return;
|
|
event.stopImmediatePropagation();
|
|
retries.set(img,count+1);
|
|
setTimeout(function(){
|
|
try{
|
|
var url=new URL(src,location.href);
|
|
url.searchParams.set('_retry',String(count+1));
|
|
img.src=url.toString();
|
|
}catch(e){
|
|
img.src=src;
|
|
}
|
|
},BASE_DELAY_MS*(count+1));
|
|
},true);
|
|
})();`;
|
|
|
|
const IMAGE_RETRY_SCRIPT_HASH = crypto.createHash('sha256').update(IMAGE_RETRY_SCRIPT).digest('base64');
|
|
|
|
const MARKER_ATTR = 'data-mindspace-image-retry';
|
|
|
|
export function injectPublicImageRetryScript(html) {
|
|
const source = String(html ?? '');
|
|
if (!source || source.includes(MARKER_ATTR)) {
|
|
return { html: source, scriptHashes: [] };
|
|
}
|
|
const markup = `<script ${MARKER_ATTR}="1">${IMAGE_RETRY_SCRIPT}</script>`;
|
|
if (/<\/body>/i.test(source)) {
|
|
return {
|
|
html: source.replace(/<\/body>/i, `${markup}</body>`),
|
|
scriptHashes: [IMAGE_RETRY_SCRIPT_HASH],
|
|
};
|
|
}
|
|
return {
|
|
html: `${source}${markup}`,
|
|
scriptHashes: [IMAGE_RETRY_SCRIPT_HASH],
|
|
};
|
|
}
|
|
|
|
export const publicImageRetryInternals = {
|
|
IMAGE_RETRY_SCRIPT_HASH,
|
|
MARKER_ATTR,
|
|
};
|