fix: preserve chat order and published page scripts
This commit is contained in:
+35
-2
@@ -30,8 +30,41 @@ export function mergeConversationSnapshot(current, incoming) {
|
||||
}
|
||||
const base = Array.isArray(current) ? current : [];
|
||||
const incomingIds = new Set(incoming.map((message) => message?.id).filter(Boolean));
|
||||
const localOnly = base.filter((message) => message?.id && !incomingIds.has(message.id));
|
||||
return localOnly.length ? [...incoming, ...localOnly] : incoming;
|
||||
const localOnlyByNextAnchor = new Map();
|
||||
|
||||
// Keep the regression-guard behaviour of retaining local streamed messages,
|
||||
// but put them back between the same server messages instead of appending the
|
||||
// whole local tail. Finish/UpdateConversation snapshots can temporarily omit
|
||||
// a middle message while Goose is still persisting it.
|
||||
for (let index = 0; index < base.length; index += 1) {
|
||||
const message = base[index];
|
||||
if (!message?.id || incomingIds.has(message.id)) continue;
|
||||
|
||||
let nextAnchor = null;
|
||||
for (let cursor = index + 1; cursor < base.length; cursor += 1) {
|
||||
const candidateId = base[cursor]?.id;
|
||||
if (candidateId && incomingIds.has(candidateId)) {
|
||||
nextAnchor = candidateId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bucket = localOnlyByNextAnchor.get(nextAnchor) ?? [];
|
||||
bucket.push(message);
|
||||
localOnlyByNextAnchor.set(nextAnchor, bucket);
|
||||
}
|
||||
|
||||
if (localOnlyByNextAnchor.size === 0) return incoming;
|
||||
|
||||
const merged = [];
|
||||
for (const message of incoming) {
|
||||
const before = localOnlyByNextAnchor.get(message?.id);
|
||||
if (before?.length) merged.push(...before);
|
||||
merged.push(message);
|
||||
}
|
||||
const trailing = localOnlyByNextAnchor.get(null);
|
||||
if (trailing?.length) merged.push(...trailing);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,3 +52,31 @@ test('mergeSessionMessagesAfterFinish matches Finish sync merge semantics', () =
|
||||
['u1', 'a1'],
|
||||
);
|
||||
});
|
||||
|
||||
test('mergeConversationSnapshot keeps an omitted middle message between its anchors', () => {
|
||||
const local = [
|
||||
msg('u1', 'user', '第一轮'),
|
||||
msg('a1', 'assistant', '第一轮回复'),
|
||||
msg('u2', 'user', '第二轮'),
|
||||
msg('a2', 'assistant', '第二轮回复'),
|
||||
];
|
||||
const server = [local[0], local[1], local[3]];
|
||||
assert.deepEqual(
|
||||
mergeConversationSnapshot(local, server).map((message) => message.id),
|
||||
['u1', 'a1', 'u2', 'a2'],
|
||||
);
|
||||
});
|
||||
|
||||
test('mergeConversationSnapshot keeps local messages before the first and after the last server anchor', () => {
|
||||
const local = [
|
||||
msg('u0', 'user', '本地前置'),
|
||||
msg('u1', 'user', '服务端消息'),
|
||||
msg('a1', 'assistant', '服务端回复'),
|
||||
msg('a2', 'assistant', '本地尾部'),
|
||||
];
|
||||
const server = [local[1], local[2]];
|
||||
assert.deepEqual(
|
||||
mergeConversationSnapshot(local, server).map((message) => message.id),
|
||||
['u0', 'u1', 'a1', 'a2'],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { injectMindSpacePageDataContext } from './mindspace-public-page-context.
|
||||
import { preparePublishedPlatformBrand } from './mindspace-page-tag.mjs';
|
||||
import { injectPublicImageRetryScript } from './mindspace-public-image-retry.mjs';
|
||||
import { applyWechatSurveyCompat } from './mindspace-page-data-wechat-survey-compat.mjs';
|
||||
import { stripPublicationHtmlCspMeta } from './plaza-embed.mjs';
|
||||
|
||||
const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
|
||||
|
||||
@@ -64,7 +65,12 @@ export function decorateMindSpacePublishedHtml({
|
||||
publishedPageCsp,
|
||||
isWechatUserAgent,
|
||||
} = {}) {
|
||||
let nextHtml = html;
|
||||
// Preview HTML carries a restrictive inline CSP (often script-src 'none').
|
||||
// Published delivery sets the authoritative CSP response header below; keep
|
||||
// the preview meta out of the delivered document so Page Data and other
|
||||
// same-origin scripts are governed by that header instead of being blocked
|
||||
// by a stale preview policy.
|
||||
let nextHtml = stripPublicationHtmlCspMeta(html);
|
||||
let allowEmbedFrame = false;
|
||||
|
||||
if (embed) {
|
||||
|
||||
@@ -137,6 +137,22 @@ test('decorateMindSpacePublishedHtml returns decorated html and csp', () => {
|
||||
assert.equal(options.scriptHashes.length, 4);
|
||||
});
|
||||
|
||||
test('decorateMindSpacePublishedHtml removes preview CSP before published delivery', () => {
|
||||
const result = decorateMindSpacePublishedHtml({
|
||||
html: '<html><head><meta http-equiv="Content-Security-Policy" content="script-src \'none\'"></head><body><script>window.ready=1</script></body></html>',
|
||||
context: { origin: '', pageUrl: '', pageDirUrl: '', fallbackImageUrl: '' },
|
||||
preparePublicationHtmlForEmbed: (value) => value,
|
||||
injectOgTags: (value) => value,
|
||||
injectWechatShareBridge: (value) => value,
|
||||
injectPublicFileShareButton: (value) => ({ html: value, scriptHashes: [] }),
|
||||
publishedPageCsp: (value) => value,
|
||||
isWechatUserAgent: () => false,
|
||||
});
|
||||
|
||||
assert.doesNotMatch(result.html, /Content-Security-Policy/i);
|
||||
assert.match(result.html, /window\.ready=1/);
|
||||
});
|
||||
|
||||
test('decorateMindSpacePublishedHtml forwards isOwner=false to the share button injector', () => {
|
||||
let sharedIsOwner;
|
||||
decorateMindSpacePublishedHtml({
|
||||
|
||||
+11
-5
@@ -234,14 +234,20 @@ export function shouldShowChatMessage(message: Message): boolean {
|
||||
}
|
||||
|
||||
export function pushMessage(messages: Message[], incoming: Message): Message[] {
|
||||
const last = messages[messages.length - 1];
|
||||
if (last?.id && incoming.id && last.id === incoming.id) {
|
||||
const existingIndex = incoming.id
|
||||
? messages.findIndex((message) => message?.id === incoming.id)
|
||||
: -1;
|
||||
if (existingIndex >= 0) {
|
||||
const existing = messages[existingIndex];
|
||||
return [
|
||||
...messages.slice(0, -1),
|
||||
...messages.slice(0, existingIndex),
|
||||
{
|
||||
...last,
|
||||
content: mergeMessageContent(last.content, incoming.content) as MessageContent[],
|
||||
...existing,
|
||||
...incoming,
|
||||
metadata: { ...existing.metadata, ...incoming.metadata },
|
||||
content: mergeMessageContent(existing.content, incoming.content) as MessageContent[],
|
||||
},
|
||||
...messages.slice(existingIndex + 1),
|
||||
];
|
||||
}
|
||||
return [...messages, incoming];
|
||||
|
||||
Reference in New Issue
Block a user