Files

273 lines
8.4 KiB
JavaScript
Raw Permalink Normal View History

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';
import { resolveSessionAccess } from './session-broker.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,
sessionAccess = null,
pageService,
pageLiveEdit,
llmProviderService,
}) {
const sessionStore = resolveSessionAccess({ userAuth, sessionAccess });
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 sessionStore.validateOwnership(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 sessionStore.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 sessionStore.validateOwnership(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 sessionStore.validateOwnership(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,
};