fix(mindspace): refresh published HTML after workspace sync
Keep online publication snapshots aligned when public/*.html changes, default static page binds to public access with MindSpace delivery URLs, and split browser-safe workspace helpers out of the Vite import chain. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
export function buildSelectedAssetReadInstructions(context) {
|
||||
const selected = context.selectedAssets ?? [];
|
||||
if (selected.length !== 1) return [];
|
||||
|
||||
const asset = selected[0];
|
||||
const h5ApiBase = String(context.h5ApiBase ?? '').replace(/\/$/, '');
|
||||
const sessionId = String(context.agentSessionId ?? '').trim();
|
||||
if (!h5ApiBase) return [];
|
||||
|
||||
const sessionToken = sessionId || '<CURRENT_AGENT_SESSION>';
|
||||
const sessionHint = sessionId
|
||||
? ''
|
||||
: '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 `<CURRENT_AGENT_SESSION>`。\n';
|
||||
|
||||
return [
|
||||
'【勾选资料读取(硬性)】',
|
||||
`- 用户已勾选「${asset.displayName}」(id: ${asset.id})`,
|
||||
'- **禁止** curl / fetch 浏览器登录接口 `GET /api/mindspace/v1/assets/:id/download`(goosed 无 cookie,会 401)',
|
||||
'- **优先**用 `list_dir oa` / `read_file` / `cat oa/文件名` 读取工作区已落盘副本',
|
||||
sessionHint,
|
||||
'- 若工作区尚无该文件,**必须**用 shell 调用 Agent 代理下载并落盘到 `oa/`:',
|
||||
'```bash',
|
||||
`curl -sS -X POST '${h5ApiBase}/api/agent/mindspace_asset_download' \\`,
|
||||
" -H 'Content-Type: application/json' \\",
|
||||
` -d '{"session_id":"${sessionToken}","asset_id":"${asset.id}"}'`,
|
||||
'```',
|
||||
'- 成功后响应 JSON 含 `relativePath`(如 `oa/report.xlsx`),后续分析/引用只用该相对路径',
|
||||
'- **禁止**访问其它用户目录、MindSpace 根目录或 `./MindSpace/<username>/` 等猜测路径',
|
||||
'',
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
export function buildSelectedAssetDeleteInstructions(context) {
|
||||
const selected = context.selectedAssets ?? [];
|
||||
if (selected.length !== 1) return [];
|
||||
|
||||
const asset = selected[0];
|
||||
const h5ApiBase = String(context.h5ApiBase ?? '').replace(/\/$/, '');
|
||||
const sessionId = String(context.agentSessionId ?? '').trim();
|
||||
if (!h5ApiBase) return [];
|
||||
|
||||
const sessionToken = sessionId || '<CURRENT_AGENT_SESSION>';
|
||||
const sessionHint = sessionId
|
||||
? ''
|
||||
: '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 `<CURRENT_AGENT_SESSION>`。\n';
|
||||
|
||||
return [
|
||||
'【勾选资料删除(硬性)】',
|
||||
`- 你对用户 MindSpace 拥有完整读写删改权限;用户已勾选「${asset.displayName}」(id: ${asset.id})。`,
|
||||
'- 用户要求删除这份资料时:先说明将删除哪一份并等待用户明确确认(如「确定」「删吧」「确认删除」);**禁止**未确认就删。',
|
||||
sessionHint,
|
||||
'- 用户确认后,**必须立即**用 shell 执行(不要只口头说已删,也不要让用户自己去界面点删除):',
|
||||
'```bash',
|
||||
`curl -sS -X POST '${h5ApiBase}/api/agent/mindspace_asset_delete' \\`,
|
||||
" -H 'Content-Type: application/json' \\",
|
||||
` -d '{"session_id":"${sessionToken}","asset_id":"${asset.id}","confirmed":true}'`,
|
||||
'```',
|
||||
'- curl 成功后再回复「已删除」;若返回 asset_in_use,说明被页面引用,告知用户先删关联页面。',
|
||||
'- 禁止删除未勾选的其它文件。',
|
||||
'',
|
||||
].filter(Boolean);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { resolveAssetWorkspaceRelativePath } from './mindspace-workspace-path.mjs';
|
||||
import {
|
||||
buildSelectedAssetDeleteInstructions,
|
||||
buildSelectedAssetReadInstructions,
|
||||
} from './mindspace-asset-agent-instructions.mjs';
|
||||
import { resolveAssetWorkspaceRelativePath } from './mindspace-workspace-relative-path.mjs';
|
||||
import { ensureUserZoneDirs } from './user-space.mjs';
|
||||
|
||||
function sanitizeBasename(value) {
|
||||
@@ -37,68 +41,10 @@ async function materializeAssetToWorkspace({
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSelectedAssetReadInstructions(context) {
|
||||
const selected = context.selectedAssets ?? [];
|
||||
if (selected.length !== 1) return [];
|
||||
|
||||
const asset = selected[0];
|
||||
const h5ApiBase = String(context.h5ApiBase ?? '').replace(/\/$/, '');
|
||||
const sessionId = String(context.agentSessionId ?? '').trim();
|
||||
if (!h5ApiBase) return [];
|
||||
|
||||
const sessionToken = sessionId || '<CURRENT_AGENT_SESSION>';
|
||||
const sessionHint = sessionId
|
||||
? ''
|
||||
: '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 `<CURRENT_AGENT_SESSION>`。\n';
|
||||
|
||||
return [
|
||||
'【勾选资料读取(硬性)】',
|
||||
`- 用户已勾选「${asset.displayName}」(id: ${asset.id})`,
|
||||
'- **禁止** curl / fetch 浏览器登录接口 `GET /api/mindspace/v1/assets/:id/download`(goosed 无 cookie,会 401)',
|
||||
'- **优先**用 `list_dir oa` / `read_file` / `cat oa/文件名` 读取工作区已落盘副本',
|
||||
sessionHint,
|
||||
'- 若工作区尚无该文件,**必须**用 shell 调用 Agent 代理下载并落盘到 `oa/`:',
|
||||
'```bash',
|
||||
`curl -sS -X POST '${h5ApiBase}/api/agent/mindspace_asset_download' \\`,
|
||||
" -H 'Content-Type: application/json' \\",
|
||||
` -d '{"session_id":"${sessionToken}","asset_id":"${asset.id}"}'`,
|
||||
'```',
|
||||
'- 成功后响应 JSON 含 `relativePath`(如 `oa/report.xlsx`),后续分析/引用只用该相对路径',
|
||||
'- **禁止**访问其它用户目录、MindSpace 根目录或 `./MindSpace/<username>/` 等猜测路径',
|
||||
'',
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
export function buildSelectedAssetDeleteInstructions(context) {
|
||||
const selected = context.selectedAssets ?? [];
|
||||
if (selected.length !== 1) return [];
|
||||
|
||||
const asset = selected[0];
|
||||
const h5ApiBase = String(context.h5ApiBase ?? '').replace(/\/$/, '');
|
||||
const sessionId = String(context.agentSessionId ?? '').trim();
|
||||
if (!h5ApiBase) return [];
|
||||
|
||||
const sessionToken = sessionId || '<CURRENT_AGENT_SESSION>';
|
||||
const sessionHint = sessionId
|
||||
? ''
|
||||
: '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 `<CURRENT_AGENT_SESSION>`。\n';
|
||||
|
||||
return [
|
||||
'【勾选资料删除(硬性)】',
|
||||
`- 你对用户 MindSpace 拥有完整读写删改权限;用户已勾选「${asset.displayName}」(id: ${asset.id})。`,
|
||||
'- 用户要求删除这份资料时:先说明将删除哪一份并等待用户明确确认(如「确定」「删吧」「确认删除」);**禁止**未确认就删。',
|
||||
sessionHint,
|
||||
'- 用户确认后,**必须立即**用 shell 执行(不要只口头说已删,也不要让用户自己去界面点删除):',
|
||||
'```bash',
|
||||
`curl -sS -X POST '${h5ApiBase}/api/agent/mindspace_asset_delete' \\`,
|
||||
" -H 'Content-Type: application/json' \\",
|
||||
` -d '{"session_id":"${sessionToken}","asset_id":"${asset.id}","confirmed":true}'`,
|
||||
'```',
|
||||
'- curl 成功后再回复「已删除」;若返回 asset_in_use,说明被页面引用,告知用户先删关联页面。',
|
||||
'- 禁止删除未勾选的其它文件。',
|
||||
'',
|
||||
].filter(Boolean);
|
||||
}
|
||||
export {
|
||||
buildSelectedAssetDeleteInstructions,
|
||||
buildSelectedAssetReadInstructions,
|
||||
} from './mindspace-asset-agent-instructions.mjs';
|
||||
|
||||
export function createAssetAgentService({
|
||||
assetService,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { buildMindSpacePageSaveInstructions } from './mindspace-page-patch.mjs';
|
||||
import {
|
||||
buildSelectedAssetDeleteInstructions,
|
||||
buildSelectedAssetReadInstructions,
|
||||
} from './mindspace-asset-agent.mjs';
|
||||
} from './mindspace-asset-agent-instructions.mjs';
|
||||
|
||||
const VIEW_LABELS = {
|
||||
home: '空间首页',
|
||||
|
||||
+2
-11
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { redactContent, scanContent } from './mindspace-content-scan.mjs';
|
||||
import { normalizeWorkspaceRelativePath } from './mindspace-workspace-relative-path.mjs';
|
||||
import {
|
||||
ensurePageThumbnail,
|
||||
extractCoverSignals,
|
||||
@@ -44,17 +45,7 @@ function pageError(message, code, details) {
|
||||
return Object.assign(new Error(message), { code, details });
|
||||
}
|
||||
|
||||
/** Normalize workspace HTML paths so `demo.html` and `public/demo.html` match. */
|
||||
export function normalizeWorkspaceRelativePath(relativePath) {
|
||||
const parts = String(relativePath ?? '')
|
||||
.replace(/^\/+/, '')
|
||||
.split('/')
|
||||
.filter((part) => part && part !== '.' && part !== '..');
|
||||
if (parts.length === 0) return '';
|
||||
if (parts[0].toLowerCase() === 'public') return parts.join('/');
|
||||
if (parts.length === 1 && parts[0].toLowerCase().endsWith('.html')) return `public/${parts[0]}`;
|
||||
return parts.join('/');
|
||||
}
|
||||
export { normalizeWorkspaceRelativePath } from './mindspace-workspace-relative-path.mjs';
|
||||
|
||||
function normalizeText(value, maxLength, fieldName) {
|
||||
const text = String(value ?? '').normalize('NFKC').trim();
|
||||
|
||||
@@ -138,6 +138,29 @@ function publicationResponse(row) {
|
||||
};
|
||||
}
|
||||
|
||||
function pageRecordStateFromAccessMode(accessMode) {
|
||||
const isPublic = accessMode === 'public';
|
||||
return {
|
||||
status: isPublic ? 'published' : 'protected',
|
||||
visibility: isPublic ? 'public' : 'private',
|
||||
};
|
||||
}
|
||||
|
||||
async function syncPageRecordFromAccessMode(executor, userId, pageId, accessMode, now = Date.now()) {
|
||||
const { status, visibility } = pageRecordStateFromAccessMode(accessMode);
|
||||
await executor.query(
|
||||
`UPDATE h5_page_records
|
||||
SET status = ?, visibility = ?, updated_at = ?
|
||||
WHERE id = ? AND user_id = ? AND status <> 'deleted'`,
|
||||
[status, visibility, now, pageId, userId],
|
||||
);
|
||||
}
|
||||
|
||||
function buildOnlinePublicationPublicUrl(ownerSlug, urlSlug) {
|
||||
const publicBaseUrl = resolvePublicBaseUrl();
|
||||
return `${publicBaseUrl}/u/${encodeURIComponent(ownerSlug)}/pages/${urlSlug}`;
|
||||
}
|
||||
|
||||
function publicHomepageResponse(owner, pages) {
|
||||
const totalViews = pages.reduce(
|
||||
(sum, page) => sum + Number(page.viewCount ?? page.view_count ?? 0),
|
||||
@@ -980,8 +1003,18 @@ export function createPublicationService(pool, options = {}) {
|
||||
WHERE id = ?`,
|
||||
[htmlBytes, checksum, publication.asset_version_id],
|
||||
);
|
||||
let publicUrl = publication.public_url;
|
||||
if (publication.access_mode === 'public') {
|
||||
publicUrl = buildOnlinePublicationPublicUrl(ownerSlug, publication.url_slug);
|
||||
await pool.query(
|
||||
`UPDATE h5_publish_records SET public_url = ?, updated_at = ? WHERE id = ? AND user_id = ?`,
|
||||
[publicUrl, now, publication.id, userId],
|
||||
);
|
||||
}
|
||||
await syncPageRecordFromAccessMode(pool, userId, pageId, publication.access_mode, now);
|
||||
return publicationResponse({
|
||||
...publication,
|
||||
public_url: publicUrl,
|
||||
updated_at: now,
|
||||
});
|
||||
};
|
||||
@@ -1023,6 +1056,13 @@ export function createPublicationService(pool, options = {}) {
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
[normalizedMode, normalizedExpiresAt, passwordHash, now, now, publicationId, userId],
|
||||
);
|
||||
await syncPageRecordFromAccessMode(
|
||||
conn,
|
||||
userId,
|
||||
publication.page_id,
|
||||
normalizedMode,
|
||||
now,
|
||||
);
|
||||
await conn.commit();
|
||||
|
||||
const updated = {
|
||||
|
||||
@@ -336,7 +336,8 @@ const ALL_TOOLS = [
|
||||
title: { type: 'string', description: '页面标题,可选,默认从 HTML <title> 读取' },
|
||||
accessMode: {
|
||||
type: 'string',
|
||||
description: '发布访问模式:public、password、login_required,默认 password',
|
||||
description:
|
||||
'发布访问模式:public、password、login_required;纯静态页默认 public,含 Page Data datasets 时默认 password',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
@@ -650,7 +651,13 @@ async function callTool(name, args) {
|
||||
if (!ownerUserId) throw new Error('缺少用户上下文,无法绑定页面');
|
||||
const pool = getQuotaPool();
|
||||
if (!pool) throw new Error('当前环境未配置数据库,无法绑定页面');
|
||||
const accessMode = String(args.accessMode ?? 'password').trim() || 'password';
|
||||
const hasDatasets =
|
||||
args.datasets != null &&
|
||||
typeof args.datasets === 'object' &&
|
||||
Object.keys(args.datasets).length > 0;
|
||||
const accessMode =
|
||||
String(args.accessMode ?? (hasDatasets ? 'password' : 'public')).trim() ||
|
||||
(hasDatasets ? 'password' : 'public');
|
||||
const h5Root = resolveH5RootFromSandbox();
|
||||
const storageRoot = resolveMindSpaceStorageRoot(h5Root);
|
||||
const result = await bindWorkspaceHtmlForPageData({
|
||||
@@ -664,7 +671,7 @@ async function callTool(name, args) {
|
||||
accessMode,
|
||||
password: args.password,
|
||||
urlSlug: args.urlSlug,
|
||||
pageDataPolicy: args.datasets
|
||||
pageDataPolicy: hasDatasets
|
||||
? {
|
||||
ownerUserId,
|
||||
accessMode,
|
||||
@@ -672,7 +679,20 @@ async function callTool(name, args) {
|
||||
}
|
||||
: null,
|
||||
});
|
||||
return [{ type: 'text', text: JSON.stringify(result, null, 2) }];
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
...result,
|
||||
userDeliveryNote:
|
||||
'向用户交付链接时优先使用 deliveryUrl / workspaceUrl(MindSpace 路径),不要用 publicationUrl',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'schedule_create_item': {
|
||||
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
|
||||
|
||||
@@ -45,6 +45,57 @@ export function createWorkspacePageDeliverService({
|
||||
});
|
||||
}
|
||||
|
||||
async function listOnlineAutoSyncedWorkspacePages(userId) {
|
||||
if (!pool || !userId) return [];
|
||||
const [rows] = await pool.query(
|
||||
`SELECT p.id AS page_id, p.title, p.current_version_id, p.workspace_relative_path,
|
||||
pv.source_snapshot_json
|
||||
FROM h5_page_records p
|
||||
JOIN h5_page_versions pv ON pv.id = p.current_version_id
|
||||
JOIN h5_publish_records pr
|
||||
ON pr.page_id = p.id AND pr.user_id = p.user_id AND pr.status = 'online'
|
||||
WHERE p.user_id = ?
|
||||
AND p.status <> 'deleted'`,
|
||||
[userId],
|
||||
);
|
||||
return (rows ?? []).filter((row) => {
|
||||
const snapshot = parseJsonColumn(row.source_snapshot_json);
|
||||
const relativePath = row.workspace_relative_path ?? snapshot.relative_path ?? null;
|
||||
if (!isPublicWorkspaceHtmlPath(relativePath)) return false;
|
||||
return snapshot.auto_synced === true || snapshot.content_mode === 'static_html';
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshOnlineWorkspacePublications(userId) {
|
||||
if (!publicationService?.refreshOnlinePublicationHtml || !userId) {
|
||||
return { refreshed: 0, skipped: 0, errors: [] };
|
||||
}
|
||||
const candidates = await listOnlineAutoSyncedWorkspacePages(userId);
|
||||
let refreshed = 0;
|
||||
let skipped = 0;
|
||||
const errors = [];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const result = await publicationService.refreshOnlinePublicationHtml(
|
||||
userId,
|
||||
candidate.page_id,
|
||||
);
|
||||
if (result) refreshed += 1;
|
||||
else skipped += 1;
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
pageId: candidate.page_id,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
logger?.warn?.(
|
||||
`[MindSpace] refresh publication failed for page ${candidate.page_id}:`,
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { refreshed, skipped, errors };
|
||||
}
|
||||
|
||||
async function ensureWorkspaceHtmlPublications(userId) {
|
||||
if (!publicationService?.publish || !pageService?.getPage || !userId) {
|
||||
return { published: 0, skipped: 0, errors: [] };
|
||||
@@ -88,11 +139,13 @@ export function createWorkspacePageDeliverService({
|
||||
syncResult = await pageSyncService.syncUserGeneratedPages(userId);
|
||||
}
|
||||
const publishResult = await ensureWorkspaceHtmlPublications(userId);
|
||||
return { sync: syncResult, publish: publishResult };
|
||||
const refreshResult = await refreshOnlineWorkspacePublications(userId);
|
||||
return { sync: syncResult, publish: publishResult, refresh: refreshResult };
|
||||
}
|
||||
|
||||
return {
|
||||
syncAndDeliver,
|
||||
ensureWorkspaceHtmlPublications,
|
||||
refreshOnlineWorkspacePublications,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,37 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createWorkspacePageDeliverService } from './mindspace-workspace-page-deliver.mjs';
|
||||
|
||||
test('refreshOnlineWorkspacePublications refreshes online auto-synced workspace html pages', async () => {
|
||||
const refreshed = [];
|
||||
const service = createWorkspacePageDeliverService({
|
||||
pool: {
|
||||
async query() {
|
||||
return [[{
|
||||
page_id: 'page-1',
|
||||
title: '隔离验证页',
|
||||
current_version_id: 'ver-1',
|
||||
workspace_relative_path: 'public/demo.html',
|
||||
source_snapshot_json: JSON.stringify({
|
||||
auto_synced: true,
|
||||
relative_path: 'public/demo.html',
|
||||
content_mode: 'static_html',
|
||||
}),
|
||||
}]];
|
||||
},
|
||||
},
|
||||
publicationService: {
|
||||
async refreshOnlinePublicationHtml(userId, pageId) {
|
||||
refreshed.push({ userId, pageId });
|
||||
return { id: 'pub-1', publicUrl: 'http://127.0.0.1:8081/u/john/pages/page-demo' };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.refreshOnlineWorkspacePublications('user-1');
|
||||
assert.equal(result.refreshed, 1);
|
||||
assert.deepEqual(refreshed, [{ userId: 'user-1', pageId: 'page-1' }]);
|
||||
});
|
||||
|
||||
test('ensureWorkspaceHtmlPublications publishes auto-synced workspace html pages', async () => {
|
||||
const published = [];
|
||||
const service = createWorkspacePageDeliverService({
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
||||
import {
|
||||
normalizeWorkspaceRelativePath,
|
||||
resolveAssetWorkspaceRelativePath,
|
||||
resolvePageWorkspaceRelativePath,
|
||||
} from './mindspace-workspace-relative-path.mjs';
|
||||
import { resolvePublishDir } from './user-publish.mjs';
|
||||
|
||||
export { normalizeWorkspaceRelativePath };
|
||||
export {
|
||||
normalizeWorkspaceRelativePath,
|
||||
resolveAssetWorkspaceRelativePath,
|
||||
resolvePageWorkspaceRelativePath,
|
||||
};
|
||||
|
||||
export async function readWorkspacePublishHtml(h5Root, userId, relativePath) {
|
||||
const normalized = normalizeWorkspaceRelativePath(relativePath);
|
||||
@@ -15,29 +23,3 @@ export async function readWorkspacePublishHtml(h5Root, userId, relativePath) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePageWorkspaceRelativePath(snapshot) {
|
||||
if (!snapshot || typeof snapshot !== 'object') return null;
|
||||
return normalizeWorkspaceRelativePath(snapshot.relative_path) || null;
|
||||
}
|
||||
|
||||
export function resolveAssetWorkspaceRelativePath({
|
||||
categoryCode = null,
|
||||
originalFilename = null,
|
||||
explicitPath = null,
|
||||
} = {}) {
|
||||
const explicit = normalizeWorkspaceRelativePath(explicitPath);
|
||||
if (explicit) return explicit;
|
||||
const filename = String(originalFilename ?? '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/+/, '');
|
||||
if (!filename) return null;
|
||||
if (/^(?:public|oa)\//i.test(filename)) {
|
||||
return normalizeWorkspaceRelativePath(filename);
|
||||
}
|
||||
const category = String(categoryCode ?? '').trim();
|
||||
if (category === 'public' || category === 'oa') {
|
||||
return normalizeWorkspaceRelativePath(`${category}/${filename}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/** Browser-safe workspace path normalization shared by Portal and Vite client imports. */
|
||||
|
||||
/** Normalize workspace HTML paths so `demo.html` and `public/demo.html` match. */
|
||||
export function normalizeWorkspaceRelativePath(relativePath) {
|
||||
const parts = String(relativePath ?? '')
|
||||
.replace(/^\/+/, '')
|
||||
.split('/')
|
||||
.filter((part) => part && part !== '.' && part !== '..');
|
||||
if (parts.length === 0) return '';
|
||||
if (parts[0].toLowerCase() === 'public') return parts.join('/');
|
||||
if (parts.length === 1 && parts[0].toLowerCase().endsWith('.html')) return `public/${parts[0]}`;
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
export function resolvePageWorkspaceRelativePath(snapshot) {
|
||||
if (!snapshot || typeof snapshot !== 'object') return null;
|
||||
return normalizeWorkspaceRelativePath(snapshot.relative_path) || null;
|
||||
}
|
||||
|
||||
export function resolveAssetWorkspaceRelativePath({
|
||||
categoryCode = null,
|
||||
originalFilename = null,
|
||||
explicitPath = null,
|
||||
} = {}) {
|
||||
const explicit = normalizeWorkspaceRelativePath(explicitPath);
|
||||
if (explicit) return explicit;
|
||||
const filename = String(originalFilename ?? '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/+/, '');
|
||||
if (!filename) return null;
|
||||
if (/^(?:public|oa)\//i.test(filename)) {
|
||||
return normalizeWorkspaceRelativePath(filename);
|
||||
}
|
||||
const category = String(categoryCode ?? '').trim();
|
||||
if (category === 'public' || category === 'oa') {
|
||||
return normalizeWorkspaceRelativePath(`${category}/${filename}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export async function bindWorkspaceHtmlForPageData({
|
||||
workspaceRoot,
|
||||
relativePath,
|
||||
title = null,
|
||||
accessMode = 'password',
|
||||
accessMode = 'public',
|
||||
password = null,
|
||||
pageDataPolicy = null,
|
||||
urlSlug = null,
|
||||
@@ -225,6 +225,7 @@ export async function bindWorkspaceHtmlForPageData({
|
||||
policy;
|
||||
}
|
||||
|
||||
const workspaceUrl = buildWorkspacePublicUrl(userId, normalized);
|
||||
return {
|
||||
pageId: page.id,
|
||||
pageTitle: page.title,
|
||||
@@ -233,7 +234,10 @@ export async function bindWorkspaceHtmlForPageData({
|
||||
publicationAccessMode: publication.accessMode,
|
||||
publicationPasswordApplied: normalizedAccessMode === 'password',
|
||||
publicationUrl: publication.publicUrl,
|
||||
workspaceUrl: buildWorkspacePublicUrl(userId, normalized),
|
||||
workspaceUrl,
|
||||
deliveryUrl: workspaceUrl,
|
||||
deliveryHint:
|
||||
'向用户交付静态页时优先使用 deliveryUrl(MindSpace /MindSpace/<用户ID>/public/... 路径),publicationUrl 仅作补充',
|
||||
policy,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
|
||||
8. 只有用户明确要求 **Word/PDF 等二进制下载** 时:文件单独落盘(如 `public/方案.docx`),链接用相对路径;**禁止**在 HTML 内用 `data:...;base64,...` 嵌入 docx(易截断损坏)
|
||||
9. 只有用户明确要求**长图下载**时:必须先 `load_skill` → `long-image-download`,调用 `generate_long_image` 生成同目录 `public/<页面名>.long.png`,再返回长图预览与下载链接;禁止把 `.thumbnail.svg` 当成长图
|
||||
10. **禁止**在 HTML 中用 CDN `https://...` 引用 `<script src>`;MindSpace 发布页 CSP 仅允许同源脚本。常用库优先用平台路径(如 Chart.js → `/assets/chart.umd.min.js`),或下载到 `public/assets/` 后相对引用
|
||||
11. **纯静态页(无 Page Data 问卷/后台)禁止调用 `private_data_bind_workspace_page`**;`write_file` / `edit_file` 落盘 `public/*.html` 后,直接按下方「回复格式」交付 **MindSpace 路径**(`/MindSpace/<用户ID>/public/...`),无需 bind、无需口令
|
||||
|
||||
详细约束以工作区内的 `.goosehints` 与 `.agents/skills/static-page-publish/SKILL.md` 为准。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user