Files
memind/mindspace-chat-context.mjs
T

452 lines
16 KiB
JavaScript
Raw Normal View History

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) {
2026-06-29 22:20:04 +08:00
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,
};