修复消息渲染并更新发布包

This commit is contained in:
shiyue
2026-09-28 08:13:09 +08:00
parent 6fb46385e2
commit d70bbf08d9
15 changed files with 739 additions and 20 deletions

View File

@@ -1613,6 +1613,91 @@ function assertFrontendMarkdownLinkContract() {
assert(styleSource.includes('.msg-bubble a.local-file-link'), 'Local file links should have a distinct message style');
}
function assertMarkdownRenderingContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8');
const normalizeMarkdownText = require(path.join(PUBLIC_DIR, 'markdown-normalizer.js'));
const { createCodexAppRuntime } = require(path.join(REPO_DIR, 'lib', 'codex-app-runtime'));
const assistantRendererStart = source.indexOf('function renderAssistantContent');
const assistantRendererEnd = source.indexOf('function createAssistantImageElement', assistantRendererStart);
const assistantRenderer = assistantRendererStart >= 0 && assistantRendererEnd > assistantRendererStart
? source.slice(assistantRendererStart, assistantRendererEnd)
: '';
const indentedChineseText = ' 目前代码里 `PlanList` 的运行时和前端渲染逻辑仍然存在,问题更像是事件没有被识别。';
assert(
normalizeMarkdownText(indentedChineseText).startsWith('目前代码里'),
'Clearly indented Chinese prose should be normalized before Markdown parsing'
);
assert(
normalizeMarkdownText(' 这是一段普通说明。\n\n 这是下一段普通说明。') === '这是一段普通说明。\n\n这是下一段普通说明。',
'Multi-paragraph prose indentation should be removed independently'
);
assert(normalizeMarkdownText('\t这是一段普通说明。') === '这是一段普通说明。', 'Tab-indented prose should also be normalized');
const fencedCode = '说明文字。\n\n```js\n const value = 1;\n```';
assert(normalizeMarkdownText(fencedCode) === fencedCode, 'Explicit fenced code blocks should remain byte-for-byte unchanged');
const indentedCode = ' const value = 1;\n console.log(value);';
assert(normalizeMarkdownText(indentedCode) === indentedCode, 'Indented source code should retain its indentation');
const whitespaceOnly = '结论一。\n \n结论二。';
assert(normalizeMarkdownText(whitespaceOnly) === whitespaceOnly, 'Whitespace-only lines should never be treated as prose indentation');
assert(
assistantRenderer.includes("block.type === 'todo_list'") && assistantRenderer.includes('createTodoListElement(block)'),
'Structured todo_list blocks should continue to render through the PlanList card path'
);
assert(
indexSource.includes('markdown-normalizer.js')
&& source.includes('typeof window.ccNormalizeMarkdownText === \'function\'')
&& source.includes('return marked.parse(normalizedText)'),
'Assistant Markdown rendering should load and apply the shared text normalizer'
);
const streamed = [];
const runtime = createCodexAppRuntime({
wsSend: (_ws, payload) => streamed.push(payload),
loadSession: () => null,
saveSession: () => {},
});
const entry = { ws: {}, toolCalls: [], fullText: '' };
runtime.processCodexAppNotification(entry, {
method: 'item/agentMessage/delta',
params: { itemId: 'msg-first', delta: '先给出的结论。' },
}, 'markdown-session');
runtime.processCodexAppNotification(entry, {
method: 'item/completed',
params: { item: { id: 'msg-first', type: 'agentMessage', text: '先给出的结论。' } },
}, 'markdown-session');
['rs-noise-1', 'rs-noise-2', 'rs-noise-3'].forEach((itemId) => {
runtime.processCodexAppNotification(entry, {
method: 'item/agentMessage/delta',
params: { itemId, delta: ' ' },
}, 'markdown-session');
runtime.processCodexAppNotification(entry, {
method: 'item/completed',
params: { item: { id: itemId, type: 'agentMessage', text: ' ' } },
}, 'markdown-session');
});
runtime.processCodexAppNotification(entry, {
method: 'item/agentMessage/delta',
params: { itemId: 'msg-second', delta: '普通正文不应带缩进。' },
}, 'markdown-session');
const lastLine = entry.fullText.split('\n').filter((line) => line.trim()).pop() || '';
assert(lastLine === '普通正文不应带缩进。', 'Whitespace-only agent message chunks must not indent the following message text');
assert(entry.fullText.startsWith('先给出的结论。'), 'Whitespace-only chunks must not create leading whitespace on the first message');
assert(
!streamed.some((payload) => payload.type === 'text_delta' && !payload.text.trim()),
'Whitespace-only chunks must not be streamed to the transcript'
);
runtime.processCodexAppNotification(entry, {
method: 'item/agentMessage/delta',
params: { itemId: 'msg-code', delta: ' ' },
}, 'markdown-session');
runtime.processCodexAppNotification(entry, {
method: 'item/agentMessage/delta',
params: { itemId: 'msg-code', delta: 'const value = 1;' },
}, 'markdown-session');
assert(entry.fullText.includes(' const value = 1;'), 'Leading indentation should be retained when later chunks complete the same code message');
}
function assertFrontendMcpReloadContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
assert(source.includes('function mcpStartupStatusToastText(status)'), 'Frontend should format MCP startup status toast text');
@@ -7162,6 +7247,12 @@ async function main() {
console.log('Plan List progress regression checks passed.');
return;
}
if (regressionTarget === 'markdown-rendering') {
assertMarkdownRenderingContract();
assertPlanListProgressContract();
console.log('Markdown rendering regression checks passed.');
return;
}
if (regressionTarget === 'runtime-image-send') {
await runRuntimeImageSendTarget();
console.log('Runtime image send regression checks passed.');
@@ -7300,6 +7391,7 @@ async function main() {
assertComposerSlashRoutingContract();
assertFrontendCcwebPromptContract();
assertFrontendMarkdownLinkContract();
assertMarkdownRenderingContract();
assertMockCodexAppPromptUserNotTextTriggered();
assertFrontendMcpReloadContract();
assertCcwebMcpRecoveryContract();