Files
memind/mindspace-chat-context.mjs
T
john 71e536eceb feat(mindspace): 悬浮聊天勾选资料联动、UI 与交互优化
勾选单个文件时 Agent 获知上下文并主动问候;打开悬浮窗使用新会话。
统一弹窗浅色主题、固定高度与输入区布局,支持点击外部收起。

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

421 lines
14 KiB
JavaScript
Raw 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 { buildMindSpacePageSaveInstructions } from './mindspace-page-patch.mjs';
const VIEW_LABELS = {
home: '空间首页',
category: '分类列表',
page: '页面详情',
};
const STATUS_LABELS = {
draft: '草稿',
reviewing: '待检查',
risk_found: '发现风险',
ready: '可发布',
published: '已公开',
protected: '受保护',
expired: '已过期',
offline: '已下线',
};
const TEMPLATE_LABELS = {
editorial: '编辑长文',
report: '分析报告',
'knowledge-card': '知识卡片',
profile: '个人介绍',
'static-html': '静态 HTML',
};
const CONTENT_EXCERPT_LIMIT = 1800;
const ASSET_PREVIEW_LIMIT = 10;
function stripHtml(value) {
return String(value ?? '')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function excerptContent(content, contentFormat, max = CONTENT_EXCERPT_LIMIT) {
const raw = String(content ?? '').trim();
if (!raw) return '';
const text = contentFormat === 'html' ? stripHtml(raw) : raw;
if (text.length <= max) return text;
return `${text.slice(0, max - 1)}`;
}
function resolveCategory(space, selectedCategory, page) {
if (selectedCategory) return selectedCategory;
if (!page?.categoryCode) return null;
return space.categories?.find((item) => item.code === page.categoryCode) ?? null;
}
function resolvePageRecord(selectedPageId, pages, pageLive) {
const listed = selectedPageId ? pages.find((item) => item.id === selectedPageId) : undefined;
if (!pageLive) return listed;
return {
...(listed ?? {}),
...pageLive.page,
title: pageLive.title,
summary: pageLive.summary,
content: pageLive.content,
};
}
function normalizeSelectedAsset(asset) {
if (!asset?.id) return null;
return {
id: asset.id,
displayName: asset.displayName ?? asset.filename ?? asset.id,
mimeType: asset.mimeType ?? 'application/octet-stream',
...(asset.filename ? { filename: asset.filename } : {}),
...(typeof asset.sizeBytes === 'number' ? { sizeBytes: asset.sizeBytes } : {}),
...(asset.categoryCode ? { categoryCode: asset.categoryCode } : {}),
...(asset.assetType ? { assetType: asset.assetType } : {}),
};
}
function inferAssetKindLabel(asset) {
const mime = String(asset?.mimeType ?? '').toLowerCase();
const name = String(asset?.displayName ?? asset?.filename ?? '').toLowerCase();
if (mime.startsWith('image/')) return '图片';
if (
mime.includes('spreadsheet') ||
mime.includes('csv') ||
name.endsWith('.xlsx') ||
name.endsWith('.xls') ||
name.endsWith('.csv')
) {
return '数据表';
}
if (mime.includes('pdf') || name.endsWith('.pdf')) return '报告';
if (mime.includes('word') || name.endsWith('.doc') || name.endsWith('.docx')) return '文档';
if (mime.startsWith('text/')) return '文本资料';
return '文件';
}
function buildSelectedAssetInstructions(context) {
const selected = context.selectedAssets ?? [];
if (selected.length === 0) return [];
const lines = ['【界面勾选资料(硬性)】'];
if (selected.length === 1) {
const asset = selected[0];
const kind = inferAssetKindLabel(asset);
lines.push(
`- 用户在界面勾选了 1 份${kind}:「${asset.displayName}」(id: ${asset.id}${asset.mimeType}`,
'- 用户说「这个 / 这份 / 这篇 / 这份报告 / 这个数据 / 帮我分析 / 帮我删除」等指代词时,**必须**指这份勾选资料,不得猜测其它文件。',
`- 读取/下载:GET /api/mindspace/v1/assets/${asset.id}/download`,
`- 删除:DELETE /api/mindspace/v1/assets/${asset.id}(执行删除前必须向用户确认)`,
);
if (context.h5ApiBase) {
lines.push(`- H5 API 基址:${context.h5ApiBase}`);
}
} else {
lines.push(`- 用户在界面同时勾选了 ${selected.length} 份资料:`);
for (const asset of selected) {
lines.push(` · 「${asset.displayName}」(id: ${asset.id}${asset.mimeType}`);
}
lines.push(
'- 用户说「这个 / 这份」等指代词时,**必须先追问**要处理哪一份,不得擅自选一份执行。',
'- 不要主动对多选资料发起默认操作;等用户明确指定后再执行。',
);
}
lines.push('');
return lines;
}
export function buildSelectedAssetGreeting(asset) {
const kind = inferAssetKindLabel(asset);
const name = asset.displayName ?? asset.filename ?? '这份资料';
return `我看到你在界面里勾选了「${name}」。你是想查看或处理这份${kind}吗?可以直接说「帮我分析」「总结一下」或「帮我删除」——我会针对这份资料来操作。`;
}
export function buildMindSpaceChatContext(input) {
const {
space,
ownerUsername,
selectedCategory,
selectedPageId,
pages = [],
pageLive = null,
assets = [],
selectedAssets = [],
focusedAsset = null,
route,
agentSessionId = null,
h5ApiBase = null,
pageEditMode = false,
parentAgentSessionId = null,
} = input;
const pageRecord = resolvePageRecord(selectedPageId, pages, pageLive);
const category = resolveCategory(space, selectedCategory, pageRecord);
let view = 'home';
if (pageRecord && selectedPageId) view = 'page';
else if (selectedCategory) view = 'category';
const normalizedSelected = selectedAssets
.map((asset) => normalizeSelectedAsset(asset))
.filter(Boolean);
const resolvedFocus =
normalizedSelected.length === 1
? normalizedSelected[0]
: normalizeSelectedAsset(focusedAsset);
const context = {
spaceId: space.id,
spaceName: space.name,
view,
route,
...(ownerUsername ? { ownerUsername } : {}),
...(agentSessionId ? { agentSessionId } : {}),
...(h5ApiBase ? { h5ApiBase } : {}),
...(pageEditMode ? { pageEditMode: true } : {}),
...(parentAgentSessionId ? { parentAgentSessionId } : {}),
...(category
? {
category: {
code: category.code,
name: category.name,
itemCount: category.itemCount,
...(view === 'category' && assets.length
? {
assets: assets.slice(0, ASSET_PREVIEW_LIMIT).map((asset) => ({
id: asset.id,
displayName: asset.displayName,
mimeType: asset.mimeType,
})),
}
: {}),
},
}
: {}),
...(pageRecord && selectedPageId
? {
page: {
id: pageRecord.id,
title: pageLive?.title ?? pageRecord.title,
summary: pageLive?.summary ?? pageRecord.summary ?? '',
status: pageRecord.status,
versionNo: pageRecord.versionNo,
categoryCode: pageRecord.categoryCode,
templateId: pageRecord.templateId,
contentFormat: pageRecord.contentFormat ?? 'markdown',
pageType: pageRecord.pageType,
contentExcerpt: excerptContent(
pageLive?.content ?? pageRecord.content ?? '',
pageRecord.contentFormat ?? 'markdown',
),
publicationUrl: pageRecord.publication?.publicUrl ?? null,
},
}
: {}),
...(resolvedFocus
? {
focusedAsset: resolvedFocus,
}
: {}),
...(normalizedSelected.length
? {
selectedAssets: normalizedSelected,
}
: {}),
};
if (view === 'home' && space.categories?.length) {
context.homeCategories = space.categories.map((item) => ({
code: item.code,
name: item.name,
itemCount: item.itemCount,
}));
}
if (view === 'home' && pages.length) {
const recentPages = [...pages].sort((left, right) => {
const createdDiff = (right.createdAt ?? 0) - (left.createdAt ?? 0);
if (createdDiff !== 0) return createdDiff;
return (right.updatedAt ?? 0) - (left.updatedAt ?? 0);
});
context.recentPages = recentPages.slice(0, 8).map((item) => ({
id: item.id,
title: item.title,
status: item.status,
categoryCode: item.categoryCode,
}));
}
return context;
}
function buildLocationHint(context) {
if (context.page) {
return `用户正在查看/编辑页面「${context.page.title}」(id: ${context.page.id})。指代「这个页面」「改这里」时默认指该页面。`;
}
if (context.selectedAssets?.length === 1) {
const asset = context.selectedAssets[0];
const kind = inferAssetKindLabel(asset);
return `用户在界面勾选了${kind}${asset.displayName}」(id: ${asset.id})。指代「这个文件」「这份报告」「这个数据」时**必须**指该勾选资料。`;
}
if (context.selectedAssets?.length > 1) {
return `用户在界面同时勾选了 ${context.selectedAssets.length} 份资料。指代「这个 / 这份」时必须先让用户说明要处理哪一份。`;
}
if (context.focusedAsset) {
return `用户正在处理资料「${context.focusedAsset.displayName}」(id: ${context.focusedAsset.id})。指代「这个文件」「这份资料」时默认指该资产。`;
}
if (context.category) {
return `用户正在「${context.category.name}」分类中浏览。指代「这里的文件」时默认指该分类下的资料。`;
}
return '用户在 MindSpace 空间首页。指代「我的空间」时指整个个人空间。';
}
export function buildPageEditSubAgentInstructions(context) {
const pageTitle = context.page?.title ?? '当前页面';
const lines = [
'【页面编辑子 Agent(硬性)】',
`- 你是专注修改页面「${pageTitle}」的子 Agent,用户正在全屏预览中编辑。`,
'- 只做页面内容/标题/样式修改,不要跑题,不要 delegate / load_skill / 改其它文件。',
'- 每次完成一处修改后必须让预览立刻更新(见下方页面实时修改说明)。',
];
if (context.parentAgentSessionId) {
lines.push(`- 父对话 session${context.parentAgentSessionId}(退出预览后摘要会合并回父对话记忆)。`);
}
lines.push('');
return lines.join('\n');
}
export function buildContextPrefix(context) {
const lines = [];
if (context.pageEditMode) {
lines.push(...buildPageEditSubAgentInstructions(context).split('\n'));
}
lines.push(
'[MindSpace 上下文]',
`- 空间:${context.spaceName}id: ${context.spaceId}`,
);
if (context.ownerUsername) {
lines.push(`- 账号:${context.ownerUsername}`);
}
lines.push(`- 当前视图:${VIEW_LABELS[context.view]}`);
if (context.category) {
const count =
typeof context.category.itemCount === 'number'
? `,共 ${context.category.itemCount}`
: '';
lines.push(`- 分类:${context.category.name}${context.category.code}${count}`);
if (context.category.assets?.length) {
lines.push('- 当前分类资料(节选):');
for (const asset of context.category.assets) {
lines.push(` · ${asset.displayName}id: ${asset.id}${asset.mimeType}`);
}
}
}
if (context.page) {
lines.push(`- 页面:${context.page.title}id: ${context.page.id}`);
if (context.page.categoryCode) {
lines.push(`- 页面所属分类:${context.page.categoryCode}`);
}
if (context.page.status) {
lines.push(
`- 页面状态:${STATUS_LABELS[context.page.status] ?? context.page.status}`,
);
}
if (typeof context.page.versionNo === 'number') {
lines.push(`- 页面版本:v${context.page.versionNo}`);
}
if (context.page.templateId) {
lines.push(
`- 页面模板:${TEMPLATE_LABELS[context.page.templateId] ?? context.page.templateId}`,
);
}
if (context.page.contentFormat) {
lines.push(`- 内容格式:${context.page.contentFormat}`);
}
if (context.page.summary?.trim()) {
lines.push(`- 页面摘要:${context.page.summary.trim()}`);
}
if (context.page.publicationUrl) {
lines.push(`- 公开地址:${context.page.publicationUrl}`);
}
if (context.page.contentExcerpt) {
lines.push('- 当前正文摘录(供直接修改参考):');
lines.push('"""');
lines.push(context.page.contentExcerpt);
lines.push('"""');
}
lines.push(...buildMindSpacePageSaveInstructions(context.page, {
sessionId: context.agentSessionId,
h5ApiBase: context.h5ApiBase,
}));
}
if (context.focusedAsset) {
lines.push(
`- 当前聚焦资料:${context.focusedAsset.displayName}${context.focusedAsset.mimeType}id: ${context.focusedAsset.id}`,
);
}
lines.push(...buildSelectedAssetInstructions(context));
if (context.homeCategories?.length) {
lines.push('- 空间分类概览:');
for (const item of context.homeCategories) {
lines.push(` · ${item.name}${item.code}${item.itemCount ?? 0} 项)`);
}
}
if (context.recentPages?.length) {
lines.push('- 最近页面(节选):');
for (const item of context.recentPages) {
lines.push(` · ${item.title}id: ${item.id}${item.categoryCode}${item.status}`);
}
}
lines.push(`- 路径:${context.route}`);
lines.push('');
lines.push('【定位说明】');
lines.push(buildLocationHint(context));
lines.push('');
lines.push('---');
lines.push('');
return `${lines.join('\n')}\n`;
}
export function formatContextChip(context) {
if (context.pageEditMode && context.page) {
const version =
typeof context.page.versionNo === 'number' ? ` · v${context.page.versionNo}` : '';
return `编辑 · ${context.page.title}${version}`;
}
if (context.selectedAssets?.length === 1) {
return `已选 · ${context.selectedAssets[0].displayName}`;
}
if (context.selectedAssets?.length > 1) {
return `已选 ${context.selectedAssets.length} 项资料`;
}
if (context.page) {
const version =
typeof context.page.versionNo === 'number' ? ` · v${context.page.versionNo}` : '';
return `${context.page.title}${version}`;
}
if (context.category) {
const count =
typeof context.category.itemCount === 'number' ? ` · ${context.category.itemCount}` : '';
return `${context.category.name}${count}`;
}
return context.spaceName;
}
export const mindspaceChatContextInternals = {
stripHtml,
excerptContent,
inferAssetKindLabel,
normalizeSelectedAsset,
};