Files
memind/page-data-workspace-bind.mjs
john 32fb2cdeaf feat: chat uploads, vision turn isolation, and MindSpace agent improvements
Add chat file/image upload UX, attachment proxying, vision thumbnails, and per-turn image scoping so agents only use the current upload. Extend MindSpace asset context, billing token state, OA/scenario verify scripts, and related runtime config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 00:23:01 +08:00

244 lines
8.2 KiB
JavaScript
Raw Permalink 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 fs from 'node:fs/promises';
import path from 'node:path';
import { createPageService, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { createPublicationService, publicationInternals } from './mindspace-publications.mjs';
import { slugFromPageTitle } from './mindspace-chat-plaza.mjs';
import { writePageAccessPolicy } from './page-data-policy-store.mjs';
import { normalizePageAccessPolicy } from './page-access-policy.mjs';
import { upsertPageDataPolicyIndex } from './page-data-policy-index.mjs';
import { syncPageDataPolicyAccessMode } from './page-data-publish-sync.mjs';
import { createMindSpacePublicUrl } from './mindspace-canonical-url.mjs';
import { resolvePublicBaseUrl } from './user-publish.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
import {
assertPolicyMatchesHtmlDatasets,
buildPageDataPolicyDatasetsFromRegistry,
detectPageDataDatasetUsageFromHtml,
} from './page-data-html-detect.mjs';
export const DEFAULT_PAGE_DATA_ADMIN_PASSWORD = '88888888';
export function resolvePageDataBindAccess(accessMode = 'public') {
return publicationInternals.normalizeAccessMode(accessMode);
}
/** password 模式:用户未传口令时使用 DEFAULT_PAGE_DATA_ADMIN_PASSWORD<8 位抛错 */
export function resolvePageDataBindPassword(accessMode, password = null) {
const mode = resolvePageDataBindAccess(accessMode);
if (mode !== 'password') return null;
const explicit = String(password ?? '').trim();
if (explicit) {
return publicationInternals.normalizePassword(explicit, true);
}
return DEFAULT_PAGE_DATA_ADMIN_PASSWORD;
}
function titleFromPublicHtml(content, relativePath) {
const match = String(content ?? '').match(/<title[^>]*>([^<]+)<\/title>/i);
const fromTitle = match?.[1]?.replace(/\s+/g, ' ').trim();
if (fromTitle) return fromTitle;
const basename = path.basename(String(relativePath ?? ''), '.html');
return basename || 'MindSpace 页面';
}
function normalizePublicHtmlRelativePath(relativePath) {
const normalized = normalizeWorkspaceRelativePath(relativePath);
if (!normalized || !normalized.startsWith('public/') || !normalized.toLowerCase().endsWith('.html')) {
throw Object.assign(new Error('仅支持 workspace 公开 HTML 页面(public/*.html'), {
code: 'invalid_public_html_path',
});
}
return normalized;
}
async function ensureWorkspaceHtmlPage({ userId, relativePath, content, title, mindSpacePages }) {
const normalized = normalizePublicHtmlRelativePath(relativePath);
let page = await mindSpacePages.findPageByRelativePath(userId, normalized);
const pageInput = {
title: title ?? page?.title ?? titleFromPublicHtml(content, normalized),
content,
contentFormat: 'html',
pageType: 'html',
categoryCode: 'draft',
};
if (page) {
return mindSpacePages.updatePage(
userId,
page.id,
{
...pageInput,
expectedVersion: page.versionNo,
},
{
snapshot: {
content_mode: 'static_html',
relative_path: normalized,
},
},
);
}
return mindSpacePages.createFromChat(userId, pageInput, {
snapshot: {
content_mode: 'static_html',
relative_path: normalized,
},
});
}
async function ensureWorkspaceHtmlPublished({
userId,
page,
mindSpacePublications,
accessMode,
password,
urlSlug,
}) {
const normalizedAccessMode = resolvePageDataBindAccess(accessMode);
const resolvedPassword = resolvePageDataBindPassword(normalizedAccessMode, password);
const existing = await mindSpacePublications.getCurrent(userId, page.id);
if (existing?.id) {
const accessChanged = existing.accessMode !== normalizedAccessMode;
const needsPassword =
normalizedAccessMode === 'password' && Boolean(resolvedPassword);
if (accessChanged || needsPassword) {
const updated = await mindSpacePublications.updatePublicationAccess(userId, existing.id, {
accessMode: normalizedAccessMode,
password: resolvedPassword,
});
existing.accessMode = updated.accessMode;
existing.id = updated.id;
existing.publicUrl = updated.publicUrl;
}
if (page.pageType === 'html' && mindSpacePublications.refreshOnlinePublicationHtml) {
return (
(await mindSpacePublications.refreshOnlinePublicationHtml(userId, page.id)) ?? existing
);
}
return existing;
}
const preferredSlug = urlSlug || slugFromPageTitle(page.title, page.id);
return mindSpacePublications.publish(userId, page.id, {
pageVersionId: page.currentVersionId,
accessMode: normalizedAccessMode,
password: resolvedPassword,
urlSlug: preferredSlug,
autoAcknowledgeFindings: true,
});
}
function buildWorkspacePublicUrl(userId, relativePath) {
const publicBaseUrl = resolvePublicBaseUrl();
return createMindSpacePublicUrl({
publicBaseUrl,
ownerKey: userId,
relativePath,
});
}
export async function bindWorkspaceHtmlForPageData({
pool,
h5Root,
storageRoot,
userId,
workspaceRoot,
relativePath,
title = null,
accessMode = 'public',
password = null,
pageDataPolicy = null,
urlSlug = null,
publicPageLimit,
}) {
if (!pool) throw new Error('数据库未配置,无法绑定页面');
if (!userId) throw new Error('缺少 userId');
if (!workspaceRoot) throw new Error('缺少 workspaceRoot');
const normalized = normalizePublicHtmlRelativePath(relativePath);
const absoluteHtmlPath = path.join(workspaceRoot, ...normalized.split('/'));
const content = await fs.readFile(absoluteHtmlPath, 'utf8');
const normalizedAccessMode = resolvePageDataBindAccess(accessMode);
const resolvedPassword = resolvePageDataBindPassword(normalizedAccessMode, password);
const mindSpacePages = createPageService(pool, { h5Root, storageRoot });
const mindSpacePublications = createPublicationService(pool, {
h5Root,
storageRoot,
publicPageLimit,
});
const page = await ensureWorkspaceHtmlPage({
userId,
relativePath: normalized,
content,
title,
mindSpacePages,
});
const publication = await ensureWorkspaceHtmlPublished({
userId,
page,
mindSpacePublications,
accessMode: normalizedAccessMode,
password: resolvedPassword,
urlSlug,
});
let policy = null;
const htmlDatasetUsage = detectPageDataDatasetUsageFromHtml(content);
let resolvedPageDataPolicy = pageDataPolicy;
if (htmlDatasetUsage.size) {
if (pageDataPolicy?.datasets) {
assertPolicyMatchesHtmlDatasets(content, pageDataPolicy.datasets);
} else {
const userDataSpace = createUserDataSpaceService({ workspaceRoot });
const autoDatasets = buildPageDataPolicyDatasetsFromRegistry({
html: content,
registryDatasets: userDataSpace.listDatasets(),
usage: htmlDatasetUsage,
});
resolvedPageDataPolicy = {
ownerUserId: userId,
accessMode: normalizedAccessMode,
datasets: autoDatasets,
};
}
}
if (resolvedPageDataPolicy) {
const ownerUserId = String(resolvedPageDataPolicy.ownerUserId ?? userId).trim();
policy = normalizePageAccessPolicy(
{
pageId: page.id,
ownerUserId,
accessMode: resolvedPageDataPolicy.accessMode ?? normalizedAccessMode,
datasets: resolvedPageDataPolicy.datasets,
defaultVisitorRole: resolvedPageDataPolicy.defaultVisitorRole,
visitors: resolvedPageDataPolicy.visitors,
roles: resolvedPageDataPolicy.roles,
},
{ fallbackPageId: page.id, fallbackOwnerUserId: ownerUserId },
);
policy = writePageAccessPolicy(workspaceRoot, policy);
await upsertPageDataPolicyIndex(pool, policy).catch(() => null);
policy =
syncPageDataPolicyAccessMode(workspaceRoot, page.id, publication.accessMode, ownerUserId) ??
policy;
}
const workspaceUrl = buildWorkspacePublicUrl(userId, normalized);
return {
pageId: page.id,
pageTitle: page.title,
relativePath: normalized,
publicationId: publication.id,
publicationAccessMode: publication.accessMode,
publicationPasswordApplied: normalizedAccessMode === 'password',
publicationUrl: workspaceUrl,
workspaceUrl,
deliveryUrl: workspaceUrl,
deliveryHint:
'向用户交付静态页时优先使用 deliveryUrlMindSpace /MindSpace/<用户ID>/public/... 路径),publicationUrl 仅作补充',
policy,
};
}