Files
memind/mindspace-page-edit-session.mjs
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

270 lines
8.2 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 { Agent, fetch as undiciFetch } from 'undici';
import { buildPageEditAgentPolicy } from './capabilities.mjs';
import { buildPageEditSubAgentInstructions } from './mindspace-chat-context.mjs';
import { buildMindSpacePageSaveInstructions } from './mindspace-page-patch.mjs';
import { reconcileAgentSession } from './session-reconcile.mjs';
const insecureDispatcher = new Agent({
connect: { rejectUnauthorized: false },
});
function isHttpsTarget(target) {
return target.startsWith('https://');
}
function createApiFetch(apiTarget, apiSecret) {
return async (pathname, init = {}) => {
const url = new URL(pathname, apiTarget);
const headers = {
...(init.headers ?? {}),
'X-Secret-Key': apiSecret,
};
if (init.body && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json';
}
return undiciFetch(url, {
...init,
headers,
dispatcher: isHttpsTarget(apiTarget) ? insecureDispatcher : undefined,
});
};
}
async function readJson(upstream) {
const text = await upstream.text();
if (!upstream.ok) {
throw Object.assign(new Error(text || `upstream ${upstream.status}`), {
code: 'worker_unavailable',
});
}
return text ? JSON.parse(text) : null;
}
function buildPageEditSeedContent({ page, parentSessionId, h5ApiBase, sessionId }) {
const context = {
spaceId: 'mindspace',
spaceName: 'MindSpace',
view: 'page',
route: `/mindspace/page/${page.id}/preview`,
pageEditMode: true,
parentAgentSessionId: parentSessionId,
agentSessionId: sessionId,
h5ApiBase,
page: {
id: page.id,
title: page.title,
summary: page.summary ?? '',
status: page.status,
versionNo: page.versionNo,
templateId: page.templateId,
contentFormat: page.contentFormat ?? 'html',
},
};
const lines = [
...buildPageEditSubAgentInstructions(context).split('\n'),
'[MindSpace 页面编辑子会话]',
`- 页面:${page.title}id: ${page.id}`,
`- 内容格式:${page.contentFormat ?? 'html'}`,
`- 当前版本:v${page.versionNo ?? 1}`,
...buildMindSpacePageSaveInstructions(
{
id: page.id,
contentFormat: page.contentFormat ?? 'html',
},
{ sessionId, h5ApiBase },
),
'请用一句话确认你已准备好修改此页面,并等待用户指令。',
];
return lines.join('\n');
}
export function buildPageEditSessionConstraints(pageTitle) {
return [
'【页面编辑子 Agent 沙箱】',
`- 当前任务:仅修改 MindSpace 页面「${pageTitle}`,
'- 禁止读写工作区其它文件;禁止 delegate / load_skill / 发布新页面',
'- 改页面优先 curl 调用 mindspace_page_patch;若无 shell 权限则在回复末尾输出 ```mindspace-page-update``` 块',
'- 每次改动后立即触发预览更新,不要只口头说「已修改」',
].join('\n');
}
export function createPageEditSessionService({
apiTarget,
apiSecret,
userAuth,
pageService,
pageLiveEdit,
llmProviderService,
}) {
const apiFetch = createApiFetch(apiTarget, apiSecret);
const applySessionLlmProvider = async (sessionId) => {
if (!llmProviderService || !sessionId) return null;
try {
return await llmProviderService.applyBestProviderForSession(sessionId);
} catch {
return null;
}
};
return {
async forkSession({ userId, pageId, parentSessionId, h5ApiBase = null }) {
if (!userId || !pageId || !parentSessionId) {
throw Object.assign(new Error('缺少 fork 参数'), { code: 'invalid_request' });
}
const ownsParent = await userAuth.ownsSession(userId, parentSessionId);
if (!ownsParent) {
throw Object.assign(new Error('无权使用父会话'), { code: 'forbidden' });
}
const page = await pageService.getPage(userId, pageId);
if (page.contentFormat !== 'html') {
throw Object.assign(new Error('仅 HTML 页面支持编辑子会话'), { code: 'invalid_request' });
}
const gate = await userAuth.canUseChat(userId);
if (!gate.ok) {
throw Object.assign(new Error(gate.message || '当前用户无法使用 Agent'), { code: 'forbidden' });
}
const workingDir = await userAuth.resolveWorkingDir(userId);
const basePolicy = await userAuth.getAgentSessionPolicy(userId);
const sessionPolicy = buildPageEditAgentPolicy(basePolicy);
const publishLayout = await userAuth.getUserPublishLayout(userId);
const startSession = await readJson(
await apiFetch('/agent/start', {
method: 'POST',
body: JSON.stringify({
working_dir: workingDir,
enable_context_memory: sessionPolicy.enableContextMemory,
...(sessionPolicy.extensionOverrides
? { extension_overrides: sessionPolicy.extensionOverrides }
: {}),
}),
}),
);
const sessionId = startSession?.id;
if (!sessionId) {
throw Object.assign(new Error('Agent 会话启动失败'), { code: 'worker_unavailable' });
}
await userAuth.registerAgentSession(userId, sessionId);
if (sessionPolicy.gooseMode) {
await readJson(
await apiFetch('/agent/update_session', {
method: 'POST',
body: JSON.stringify({
session_id: sessionId,
goose_mode: sessionPolicy.gooseMode,
}),
}),
);
}
const sandboxConstraints = [
publishLayout?.constraints ?? '',
buildPageEditSessionConstraints(page.title),
]
.filter(Boolean)
.join('\n\n');
await reconcileAgentSession(apiFetch, sessionId, {
workingDir,
sessionPolicy,
sandboxConstraints,
userContext: publishLayout
? {
userId,
displayName: publishLayout.displayName,
username: publishLayout.username,
slug: publishLayout.slug,
}
: null,
});
await applySessionLlmProvider(sessionId);
await pageLiveEdit.bindSession({
userId,
sessionId,
pageId,
parentSessionId,
});
const seed = buildPageEditSeedContent({
page,
parentSessionId,
h5ApiBase,
sessionId,
});
await apiFetch('/agent/harness_remember', {
method: 'POST',
body: JSON.stringify({
sessionId,
title: `页面编辑 · ${page.title}`,
content: seed,
}),
});
await apiFetch('/agent/harness_bootstrap', {
method: 'POST',
body: JSON.stringify({ sessionId, force: true }),
});
return {
sessionId,
pageId,
parentSessionId,
};
},
async closeSession({ userId, sessionId, pageId, parentSessionId, summary = '' }) {
if (!userId || !sessionId || !pageId) {
throw Object.assign(new Error('缺少 close 参数'), { code: 'invalid_request' });
}
const owns = await userAuth.ownsSession(userId, sessionId);
if (!owns) {
throw Object.assign(new Error('无权关闭该子会话'), { code: 'forbidden' });
}
const binding = pageLiveEdit.getBinding(sessionId);
if (binding && binding.pageId !== String(pageId)) {
throw Object.assign(new Error('子会话未绑定该页面'), { code: 'page_binding_mismatch' });
}
pageLiveEdit.unbindSession(sessionId);
const trimmedSummary = String(summary ?? '').trim();
const parentId = String(parentSessionId ?? binding?.parentSessionId ?? '').trim();
if (trimmedSummary && parentId) {
const ownsParent = await userAuth.ownsSession(userId, parentId);
if (ownsParent) {
const page = await pageService.getPage(userId, pageId).catch(() => null);
await apiFetch('/agent/harness_remember', {
method: 'POST',
body: JSON.stringify({
sessionId: parentId,
title: `页面编辑摘要 · ${page?.title ?? pageId}`,
content: trimmedSummary,
}),
});
}
}
return { sessionId, pageId, merged: Boolean(trimmedSummary && parentId) };
},
};
}
export const pageEditSessionInternals = {
buildPageEditSeedContent,
buildPageEditSessionConstraints,
};