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
+20 -9
View File
@@ -1,5 +1,7 @@
import type { Message, MessageContent } from '../types';
import { mergeMessageContent } from '../../message-stream.mjs';
import { mergeConversationSnapshot as mergeConversationSnapshotCore } from '../../chat-finish-sync.mjs';
import { deriveUserFacingText } from '../../conversation-display.mjs';
import { stripUserAddressPrefix } from './userAddress';
const IMAGE_URL_LINE_RE = /^\[图片\d+]: (.+)$/;
@@ -153,30 +155,39 @@ export function normalizeConversationMessages(messages: Message[]): Message[] {
* Merge an authoritative server conversation snapshot into the currently
* displayed messages without ever erasing what the user can already see.
*
* Used by UpdateConversation SSE and Finish sync. Implementation lives in
* chat-finish-sync.mjs (shared with regression tests — do not inline elsewhere).
*
* `UpdateConversation` snapshots arrive on every SSE (re)connect — which on
* mobile happens constantly via the page visibility cycle. A mid-turn snapshot
* can be shorter than the live view (e.g. assistant messages not yet flagged
* userVisible), so a blind replace would blank an active conversation even
* though the backend session is alive and persisted. We therefore treat the
* snapshot as authoritative for content/order but keep any locally-displayed
* messages the snapshot hasn't caught up to yet (optimistic / in-flight tail).
* The authoritative full reload on `Finish` corrects any drift afterwards.
* though the backend session is alive and persisted. Finish sync also merges
* because Goose may persist assistant turns after the Finish event.
*/
export function mergeConversationSnapshot(current: Message[], incoming: Message[]): Message[] {
if (incoming.length === 0) return current;
const incomingIds = new Set(incoming.map((message) => message.id).filter(Boolean));
const localOnly = current.filter((message) => message.id && !incomingIds.has(message.id));
return localOnly.length ? [...incoming, ...localOnly] : incoming;
return mergeConversationSnapshotCore(current, incoming) as Message[];
}
export function getDisplayText(message: Message): string {
// REGRESSION GUARD: never show agent-only routing/skill prefixes in the chat UI.
if (message.role === 'user') {
if ('displayText' in message.metadata && message.metadata.displayText != null) {
return stripImageUrlLines(message.metadata.displayText);
}
const raw = message.content
.filter((c): c is Extract<MessageContent, { type: 'text' }> => c.type === 'text')
.map((c) => c.text)
.join('\n');
return stripImageUrlLines(deriveUserFacingText(raw));
}
if ('displayText' in message.metadata) {
return stripImageUrlLines(message.metadata.displayText ?? '');
}
const systemText = getSystemNotificationText(message);
if (systemText) return systemText;
const visible = getVisibleText(message);
return message.role === 'user' ? stripImageUrlLines(stripUserAddressPrefix(visible)) : visible;
return visible;
}
export function pushMessage(messages: Message[], incoming: Message): Message[] {