Files
memind/mindspace-chat-context.mjs
T
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

452 lines
16 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';
import {
buildSelectedAssetDeleteInstructions,
buildSelectedAssetReadInstructions,
} from './mindspace-asset-agent-instructions.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 normalizeWorkspaceRelativePath(value) {
const trimmed = String(value ?? '').trim().replace(/^\.?\//, '');
if (!trimmed || trimmed.includes('..')) return null;
return trimmed;
}
function normalizeSelectedAsset(asset) {
if (!asset?.id) return null;
const workspaceRelativePath = normalizeWorkspaceRelativePath(
asset.workspaceRelativePath ?? asset.workspace_relative_path,
);
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 } : {}),
...(workspaceRelativePath ? { workspaceRelativePath } : {}),
};
}
function resolveCategoryItemCount(category, view, assets) {
const listed = typeof category?.itemCount === 'number' ? category.itemCount : 0;
if (view !== 'category' || !Array.isArray(assets) || assets.length === 0) {
return category?.itemCount;
}
return Math.max(listed, assets.length);
}
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);
const workspacePath = normalizeWorkspaceRelativePath(
asset.workspaceRelativePath ?? asset.workspace_relative_path,
);
lines.push(
`- 用户在界面勾选了 1 份${kind}:「${asset.displayName}」(id: ${asset.id}${asset.mimeType}`,
'- 用户说「这个 / 这份 / 这篇 / 这份报告 / 这个数据 / 帮我分析 / 帮我删除」等指代词时,**必须**指这份勾选资料,不得猜测其它文件。',
workspacePath
? `- 读取/分析:文件已落盘 \`${workspacePath}\`,直接 read/脚本读取;见下方「勾选资料读取」。`
: '- 读取/分析:优先 `list_dir oa` / `read_file oa/文件名`;若工作区尚无副本,见下方「勾选资料读取」用 Agent 代理下载落盘。',
'- 删除:须先与用户确认,确认后由你直接执行删除(见下方「勾选资料删除」);你对该用户空间有完整读写删改权限,**禁止**推给用户自行去界面删除。',
);
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}吗?可以直接说「帮我分析」「总结一下」或「帮我删除」——确认后我可以直接在这份${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: resolveCategoryItemCount(category, view, assets),
...(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.workspacePublicUrl) {
lines.push(`- 公开地址(MindSpace):${context.page.workspacePublicUrl}`);
} else 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));
lines.push(...buildSelectedAssetReadInstructions(context));
lines.push(...buildSelectedAssetDeleteInstructions(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,
};