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 base = Array.isArray(current) ? current : [];
|
||||||
const incomingIds = new Set(incoming.map((message) => message?.id).filter(Boolean));
|
const incomingIds = new Set(incoming.map((message) => message?.id).filter(Boolean));
|
||||||
const localOnly = base.filter((message) => message?.id && !incomingIds.has(message.id));
|
const localOnlyByNextAnchor = new Map();
|
||||||
return localOnly.length ? [...incoming, ...localOnly] : incoming;
|
|
||||||
|
// 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'],
|
['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 { preparePublishedPlatformBrand } from './mindspace-page-tag.mjs';
|
||||||
import { injectPublicImageRetryScript } from './mindspace-public-image-retry.mjs';
|
import { injectPublicImageRetryScript } from './mindspace-public-image-retry.mjs';
|
||||||
import { applyWechatSurveyCompat } from './mindspace-page-data-wechat-survey-compat.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;
|
const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
|
||||||
|
|
||||||
@@ -64,7 +65,12 @@ export function decorateMindSpacePublishedHtml({
|
|||||||
publishedPageCsp,
|
publishedPageCsp,
|
||||||
isWechatUserAgent,
|
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;
|
let allowEmbedFrame = false;
|
||||||
|
|
||||||
if (embed) {
|
if (embed) {
|
||||||
|
|||||||
@@ -137,6 +137,22 @@ test('decorateMindSpacePublishedHtml returns decorated html and csp', () => {
|
|||||||
assert.equal(options.scriptHashes.length, 4);
|
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', () => {
|
test('decorateMindSpacePublishedHtml forwards isOwner=false to the share button injector', () => {
|
||||||
let sharedIsOwner;
|
let sharedIsOwner;
|
||||||
decorateMindSpacePublishedHtml({
|
decorateMindSpacePublishedHtml({
|
||||||
|
|||||||
+11
-5
@@ -234,14 +234,20 @@ export function shouldShowChatMessage(message: Message): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function pushMessage(messages: Message[], incoming: Message): Message[] {
|
export function pushMessage(messages: Message[], incoming: Message): Message[] {
|
||||||
const last = messages[messages.length - 1];
|
const existingIndex = incoming.id
|
||||||
if (last?.id && incoming.id && last.id === incoming.id) {
|
? messages.findIndex((message) => message?.id === incoming.id)
|
||||||
|
: -1;
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
const existing = messages[existingIndex];
|
||||||
return [
|
return [
|
||||||
...messages.slice(0, -1),
|
...messages.slice(0, existingIndex),
|
||||||
{
|
{
|
||||||
...last,
|
...existing,
|
||||||
content: mergeMessageContent(last.content, incoming.content) as MessageContent[],
|
...incoming,
|
||||||
|
metadata: { ...existing.metadata, ...incoming.metadata },
|
||||||
|
content: mergeMessageContent(existing.content, incoming.content) as MessageContent[],
|
||||||
},
|
},
|
||||||
|
...messages.slice(existingIndex + 1),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [...messages, incoming];
|
return [...messages, incoming];
|
||||||
|
|||||||
Reference in New Issue
Block a user