diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx index bdbcc92df..e50ee4028 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx @@ -381,10 +381,38 @@ export function ProjectSupervisorView({ */ const renderLiveStream = (liveText: string, calls: typeof liveToolCalls) => { const offsets = directToolTextOffsets[liveToolCallTurnId] ?? {}; + /** + * 把切分点吸附到句子边界:工具事件与文本增量是交替到达的,直接按"当时的长度"切 + * 会把句子切碎(历史上出现过"我来做一次轻|量|的"这种)。这里向后找最近的 + * 句末标点或换行,同一句里的多个工具因此落到同一个切点、自动合并成一块。 + */ + const snapToBoundary = (source: string, index: number) => { + const start = Math.max(0, Math.min(index, source.length)); + const limit = Math.min(source.length, start + 200); + for (let cursor = start; cursor < limit; cursor += 1) { + const char = source[cursor]; + if (char === '\n') { + return cursor + 1; + } + if ('。!?;!?;'.includes(char)) { + return cursor + 1; + } + if ( + char === '.' && + (source[cursor + 1] === ' ' || source[cursor + 1] === undefined) + ) { + return cursor + 1; + } + } + return start; + }; const groups = new Map(); for (const call of calls) { const offset = offsets[call.id]; - const key = Number.isFinite(offset) ? Number(offset) : liveText.length; + const key = snapToBoundary( + liveText, + Number.isFinite(offset) ? Number(offset) : liveText.length, + ); const bucket = groups.get(key) ?? []; bucket.push(call); groups.set(key, bucket);