fix(mindspace): edit_file 落盘、Finish 聊天 merge 与回归守卫

- finish-sync 支持 edit_file 覆盖 public HTML
- Finish 同步 merge 本地流式消息,剥离 agent 内部前缀
- 新增 verify:mindspace-publish-guards 与 AGENTS.md 跨工具说明
- 发版脚本接入回归门禁;103 runtime 发布含备份回退

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 00:27:15 +08:00
parent 742aee7148
commit 98721371a4
31 changed files with 1440 additions and 141 deletions
+46 -58
View File
@@ -14,6 +14,11 @@ import {
} from './auth.mjs';
import { createDbPool, initSchema, isDatabaseConfigured } from './db.mjs';
import { createAgentRunGateway } from './agent-run-gateway.mjs';
import {
createAgentRunEventsHandler,
createGetAgentRunHandler,
createPostAgentRunsHandler,
} from './agent-run-routes.mjs';
import { createTkmindProxy, sanitizeSessionConversationPublicHtmlLinks } from './tkmind-proxy.mjs';
import {
clearUserSessionCookie,
@@ -78,7 +83,10 @@ import {
resolveChatSaveAnalysis,
resolveStaticHtmlContent,
} from './mindspace-chat-save.mjs';
import { syncPublicHtmlAfterFinish } from './mindspace-public-finish-sync.mjs';
import {
materializePublicHtmlWritesFromSessionEvent,
syncPublicHtmlAfterFinish,
} from './mindspace-public-finish-sync.mjs';
import { syncGeneratedPagesFromPublicAssets } from './mindspace-page-sync.mjs';
import { generateHtmlThumbnail } from './mindspace-thumbnails.mjs';
import { injectOgTags, injectWechatShareBridge } from './mindspace-og-tags.mjs';
@@ -3903,38 +3911,7 @@ api.post('/agent/runs', async (req, res, next) => {
[
tkmindProxy.requireUser,
tkmindProxy.ensureChatAllowed,
async (request, response) => {
try {
const sessionId = String(request.body?.session_id ?? '').trim() || null;
const requestId = String(request.body?.request_id ?? '').trim();
const userMessage = request.body?.user_message ?? null;
if (!requestId) {
response.status(400).json({ message: '缺少 request_id' });
return;
}
if (!userMessage) {
response.status(400).json({ message: '缺少 user_message' });
return;
}
if (sessionId) {
const owns = await userAuth.ownsSession(request.currentUser.id, sessionId);
if (!owns) {
response.status(403).json({ message: '无权访问该会话' });
return;
}
}
const run = await agentRunGateway.createRun(request.currentUser.id, {
sessionId,
requestId,
userMessage,
});
response.status(202).json({ run });
} catch (err) {
response.status(500).json({
message: err instanceof Error ? err.message : '创建任务失败',
});
}
},
createPostAgentRunsHandler({ userAuth, agentRunGateway }),
],
req,
res,
@@ -3948,26 +3925,21 @@ api.get('/agent/runs/:runId', async (req, res, next) => {
return runHandlerChain(
[
tkmindProxy.requireUser,
async (request, response) => {
try {
const run = await agentRunGateway.getRunForUser(
request.currentUser.id,
request.params.runId,
);
if (!run) {
response.status(404).json({ message: '任务不存在' });
return;
}
if (run.status !== 'succeeded' && run.status !== 'failed') {
agentRunGateway.dispatchRun(run.id);
}
response.json({ run });
} catch (err) {
response.status(500).json({
message: err instanceof Error ? err.message : '读取任务失败',
});
}
},
createGetAgentRunHandler({ agentRunGateway }),
],
req,
res,
next,
);
});
api.get('/agent/runs/:runId/events', async (req, res, next) => {
await userAuthReady;
if (!userAuth || !tkmindProxy || !agentRunGateway) return next();
return runHandlerChain(
[
tkmindProxy.requireUser,
createAgentRunEventsHandler({ agentRunGateway }),
],
req,
res,
@@ -4004,9 +3976,11 @@ api.get('/sessions/:sessionId', async (req, res, next) => {
if (sessionSnapshotService?.isEnabled()) {
const snapshot = await sessionSnapshotService.get(sessionId);
if (snapshot) {
const mcMatch = hintMc == null || snapshot.meta.synced_msg_count === hintMc;
const uaMatch = hintUa == null || snapshot.meta.source_updated_at === hintUa;
if (mcMatch && uaMatch) {
// REGRESSION GUARD: without both hints, stale snapshot can wipe mid-turn chat.
const canUseSnapshotCache = hintMc != null && hintUa != null;
const mcMatch = snapshot.meta.synced_msg_count === hintMc;
const uaMatch = snapshot.meta.source_updated_at === hintUa;
if (canUseSnapshotCache && mcMatch && uaMatch) {
const sanitizedMessages = sanitizeSessionConversationPublicHtmlLinks(
snapshot.messages,
req.currentUser,
@@ -4094,6 +4068,10 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
if (!owns) {
return res.status(403).json({ message: '无权访问该会话' });
}
const publishDir = resolvePublishDir(__dirname, { id: req.currentUser.id });
const syncPublicHtmlDuringStream = (event) => {
materializePublicHtmlWritesFromSessionEvent(event, { publishDir });
};
// After Finish, refresh the snapshot and persist any newly generated public
// workspace HTML into the asset store before a later restart rebuilds the
// workspace from DB-backed assets only.
@@ -4122,7 +4100,7 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
await syncPublicHtmlAfterFinish({
messages,
currentUser: req.currentUser,
publishDir: resolvePublishDir(__dirname, { id: uid }),
publishDir,
syncWorkspaceAssets:
WORKSPACE_MAINTENANCE_ENABLED && mindSpaceAssets
? (userId, options) => mindSpaceAssets.syncWorkspaceAssets(userId, options)
@@ -4130,7 +4108,10 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
});
await syncUserGeneratedPages(uid);
};
return tkmindProxy.proxySessionEvents(req, res, sessionId, { onAfterFinish });
return tkmindProxy.proxySessionEvents(req, res, sessionId, {
onAfterFinish,
onEvent: syncPublicHtmlDuringStream,
});
});
api.use(async (req, res, next) => {
@@ -4141,6 +4122,13 @@ api.use(async (req, res, next) => {
return sendError(res, req, 404, 'not_found', `接口不存在:${req.method} ${req.path}`);
}
if (req.method === 'GET' && /^\/agent\/runs\/[^/]+\/events$/.test(req.path)) {
return res.status(503).json({
message: 'Agent Run SSE 接口需在 Portal 本地处理,请确认 server.mjs 已更新并重启后端',
code: 'AGENT_RUNS_NATIVE_REQUIRED',
});
}
const sessionMatch = req.path.match(/^\/sessions\/([^/]+)/);
if (sessionMatch) {
const sessionId = sessionMatch[1];