feat(mindspace): 悬浮聊天勾选资料联动、UI 与交互优化

勾选单个文件时 Agent 获知上下文并主动问候;打开悬浮窗使用新会话。
统一弹窗浅色主题、固定高度与输入区布局,支持点击外部收起。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 02:17:36 +08:00
parent 2c91689692
commit 71e536eceb
10 changed files with 609 additions and 47 deletions
+103 -6
View File
@@ -63,6 +63,75 @@ function resolvePageRecord(selectedPageId, pages, pageLive) {
};
}
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,
@@ -72,6 +141,7 @@ export function buildMindSpaceChatContext(input) {
pages = [],
pageLive = null,
assets = [],
selectedAssets = [],
focusedAsset = null,
route,
agentSessionId = null,
@@ -87,6 +157,14 @@ export function buildMindSpaceChatContext(input) {
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,
@@ -135,13 +213,14 @@ export function buildMindSpaceChatContext(input) {
},
}
: {}),
...(focusedAsset
...(resolvedFocus
? {
focusedAsset: {
id: focusedAsset.id,
displayName: focusedAsset.displayName,
mimeType: focusedAsset.mimeType,
},
focusedAsset: resolvedFocus,
}
: {}),
...(normalizedSelected.length
? {
selectedAssets: normalizedSelected,
}
: {}),
};
@@ -175,6 +254,14 @@ 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})。指代「这个文件」「这份资料」时默认指该资产。`;
}
@@ -274,6 +361,8 @@ export function buildContextPrefix(context) {
);
}
lines.push(...buildSelectedAssetInstructions(context));
if (context.homeCategories?.length) {
lines.push('- 空间分类概览:');
for (const item of context.homeCategories) {
@@ -304,6 +393,12 @@ export function formatContextChip(context) {
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}` : '';
@@ -320,4 +415,6 @@ export function formatContextChip(context) {
export const mindspaceChatContextInternals = {
stripHtml,
excerptContent,
inferAssetKindLabel,
normalizeSelectedAsset,
};