From e3063ea8063bca14181089ab38a6289c1a59449e Mon Sep 17 00:00:00 2001 From: john Date: Fri, 10 Jul 2026 20:44:04 +0800 Subject: [PATCH] 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 --- mindspace-asset-agent-instructions.mjs | 62 +++++++++++++++++++ mindspace-asset-agent.mjs | 72 +++-------------------- mindspace-chat-context.mjs | 2 +- mindspace-pages.mjs | 13 +--- mindspace-publications.mjs | 40 +++++++++++++ mindspace-sandbox-mcp.mjs | 28 +++++++-- mindspace-workspace-page-deliver.mjs | 55 ++++++++++++++++- mindspace-workspace-page-deliver.test.mjs | 31 ++++++++++ mindspace-workspace-path.mjs | 38 ++++-------- mindspace-workspace-relative-path.mjs | 39 ++++++++++++ page-data-workspace-bind.mjs | 8 ++- skills/static-page-publish/SKILL.md | 1 + 12 files changed, 279 insertions(+), 110 deletions(-) create mode 100644 mindspace-asset-agent-instructions.mjs create mode 100644 mindspace-workspace-relative-path.mjs diff --git a/mindspace-asset-agent-instructions.mjs b/mindspace-asset-agent-instructions.mjs new file mode 100644 index 0000000..931a2a9 --- /dev/null +++ b/mindspace-asset-agent-instructions.mjs @@ -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 || ''; + const sessionHint = sessionId + ? '' + : '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 ``。\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//` 等猜测路径', + '', + ].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 || ''; + const sessionHint = sessionId + ? '' + : '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 ``。\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); +} diff --git a/mindspace-asset-agent.mjs b/mindspace-asset-agent.mjs index 35b362c..cf81fbe 100644 --- a/mindspace-asset-agent.mjs +++ b/mindspace-asset-agent.mjs @@ -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 || ''; - const sessionHint = sessionId - ? '' - : '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 ``。\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//` 等猜测路径', - '', - ].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 || ''; - const sessionHint = sessionId - ? '' - : '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 ``。\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, diff --git a/mindspace-chat-context.mjs b/mindspace-chat-context.mjs index 4d6aa69..c98635f 100644 --- a/mindspace-chat-context.mjs +++ b/mindspace-chat-context.mjs @@ -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: '空间首页', diff --git a/mindspace-pages.mjs b/mindspace-pages.mjs index 71c5116..bde15bb 100644 --- a/mindspace-pages.mjs +++ b/mindspace-pages.mjs @@ -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(); diff --git a/mindspace-publications.mjs b/mindspace-publications.mjs index 6ecc79a..4d41d05 100644 --- a/mindspace-publications.mjs +++ b/mindspace-publications.mjs @@ -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 = { diff --git a/mindspace-sandbox-mcp.mjs b/mindspace-sandbox-mcp.mjs index 38d374e..5cc760e 100644 --- a/mindspace-sandbox-mcp.mjs +++ b/mindspace-sandbox-mcp.mjs @@ -336,7 +336,8 @@ const ALL_TOOLS = [ title: { type: 'string', description: '页面标题,可选,默认从 HTML 读取' }, 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'; diff --git a/mindspace-workspace-page-deliver.mjs b/mindspace-workspace-page-deliver.mjs index 995c761..5b47a15 100644 --- a/mindspace-workspace-page-deliver.mjs +++ b/mindspace-workspace-page-deliver.mjs @@ -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, }; } diff --git a/mindspace-workspace-page-deliver.test.mjs b/mindspace-workspace-page-deliver.test.mjs index 36c5c4a..f135694 100644 --- a/mindspace-workspace-page-deliver.test.mjs +++ b/mindspace-workspace-page-deliver.test.mjs @@ -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({ diff --git a/mindspace-workspace-path.mjs b/mindspace-workspace-path.mjs index e86d8f9..6fa22a4 100644 --- a/mindspace-workspace-path.mjs +++ b/mindspace-workspace-path.mjs @@ -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; -} diff --git a/mindspace-workspace-relative-path.mjs b/mindspace-workspace-relative-path.mjs new file mode 100644 index 0000000..c6386e1 --- /dev/null +++ b/mindspace-workspace-relative-path.mjs @@ -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; +} diff --git a/page-data-workspace-bind.mjs b/page-data-workspace-bind.mjs index ebc474c..072d15d 100644 --- a/page-data-workspace-bind.mjs +++ b/page-data-workspace-bind.mjs @@ -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, }; } diff --git a/skills/static-page-publish/SKILL.md b/skills/static-page-publish/SKILL.md index b6300cb..9d6c463 100644 --- a/skills/static-page-publish/SKILL.md +++ b/skills/static-page-publish/SKILL.md @@ -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` 为准。