2026-06-15 22:09:38 -07:00
|
|
|
|
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';
|
2026-07-06 14:19:48 +08:00
|
|
|
|
import { resolveSessionAccess } from './session-broker.mjs';
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-07-06 14:19:48 +08:00
|
|
|
|
sessionAccess = null,
|
2026-06-15 22:09:38 -07:00
|
|
|
|
pageService,
|
|
|
|
|
|
pageLiveEdit,
|
|
|
|
|
|
llmProviderService,
|
|
|
|
|
|
}) {
|
2026-07-06 14:19:48 +08:00
|
|
|
|
const sessionStore = resolveSessionAccess({ userAuth, sessionAccess });
|
2026-06-15 22:09:38 -07:00
|
|
|
|
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' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 14:19:48 +08:00
|
|
|
|
const ownsParent = await sessionStore.validateOwnership(userId, parentSessionId);
|
2026-06-15 22:09:38 -07:00
|
|
|
|
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' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 14:19:48 +08:00
|
|
|
|
await sessionStore.registerAgentSession(userId, sessionId);
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
|
|
|
|
|
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' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 14:19:48 +08:00
|
|
|
|
const owns = await sessionStore.validateOwnership(userId, sessionId);
|
2026-06-15 22:09:38 -07:00
|
|
|
|
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) {
|
2026-07-06 14:19:48 +08:00
|
|
|
|
const ownsParent = await sessionStore.validateOwnership(userId, parentId);
|
2026-06-15 22:09:38 -07:00
|
|
|
|
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,
|
|
|
|
|
|
};
|