From 67126a981e57656d86fba4921aa3e9cfada98aac Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 16 Sep 2026 00:21:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E6=97=B6=E5=9B=9E=E5=90=88=E6=8C=89?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E4=BA=A4=E6=9B=BF=E6=B8=B2=E6=9F=93=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E4=B8=8E=E5=B7=A5=E5=85=B7=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx:记录"每个工具首次出现时已生成正文的长度"(切分点)并在收尾/清空对话时重置 - ProjectSupervisorView:新增 renderLiveStream,按切分点把流式正文切成若干段,工具块就地插在对应位置;同一切分点的工具合并成一个块 - 有正文时不再重复渲染独立实时工具组(避免同一批工具出现两次) - 历史回合没有切分点:退化为"正文 → 工具块 → 轮次统计",老项目与旧数据不受影响 - tsc 通过;未跑测试 --- apps/ai-game-creator-shell/src/App.tsx | 46 ++++++++++++ .../ProjectSupervisorView.tsx | 74 ++++++++++++++++++- 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 8e51a380f..f4498546d 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -883,6 +883,14 @@ export function App({ // 直连回合的思考过程(流式):整段替换;回合结束/开始新回合/清空对话时一并清掉。 const [directCodexTransientReasoning, setDirectCodexTransientReasoning] = useState(''); + /** 实时回合里"某个工具首次出现时,已生成正文的长度"——用它把正文与工具交替排列。 */ + const [directToolTextOffsets, setDirectToolTextOffsets] = useState< + Record> + >({}); + const directToolTextOffsetsRef = useRef>>( + {}, + ); + const directAccumulatedTextLengthRef = useRef(0); const [ directCodexTransientReplyUpdatedAt, setDirectCodexTransientReplyUpdatedAt, @@ -984,6 +992,9 @@ export function App({ setDirectCodexProgressUpdatedAt(null); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); } @@ -1083,6 +1094,9 @@ export function App({ setDirectCodexProgressUpdatedAt(Date.now()); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); setProjectSupervisorRuntimeError(''); @@ -1117,6 +1131,9 @@ export function App({ activeDirectCodexTurnRef.current = null; setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); return true; @@ -2188,7 +2205,29 @@ export function App({ ); } // 工具调用增量:字段可选,老事件(undefined)走原路径,行为不变。 + if (typeof payload.accumulatedText === 'string') { + directAccumulatedTextLengthRef.current = payload.accumulatedText.length; + } if (payload.toolCalls?.length) { + // 每个工具只记第一次出现时的正文长度:这就是它在对话流里的位置。 + const turnOffsets = + directToolTextOffsetsRef.current[payload.turnId] ?? {}; + let offsetsChanged = false; + for (const call of payload.toolCalls) { + const callId = (call as { id?: string }).id; + if (!callId || turnOffsets[callId] !== undefined) { + continue; + } + turnOffsets[callId] = directAccumulatedTextLengthRef.current; + offsetsChanged = true; + } + if (offsetsChanged) { + directToolTextOffsetsRef.current = { + ...directToolTextOffsetsRef.current, + [payload.turnId]: { ...turnOffsets }, + }; + setDirectToolTextOffsets(directToolTextOffsetsRef.current); + } applyDirectToolCalls( payload.toolCalls.map((call) => ({ ...call, @@ -2212,6 +2251,9 @@ export function App({ setDirectCodexProgressUpdatedAt(updatedAt); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; setDirectCodexTransientReplyUpdatedAt(null); return; } @@ -6860,6 +6902,9 @@ export function App({ setDirectCodexProgress('正在等待陶泥儿开始'); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; setDirectCodexProgressUpdatedAt(Date.now()); directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); @@ -12594,6 +12639,7 @@ export function App({ if (projectSupervisorOnly) { return ( >; showDesignReasoning?: boolean; designReasoning?: string; designReasoningEntries?: DesignReasoningEntry[]; @@ -227,6 +229,7 @@ export function ProjectSupervisorView({ showProfessionalCollaboration = true, transientReply, transientReasoning = '', + directToolTextOffsets = {}, showDesignReasoning = false, designReasoning = '', designReasoningEntries = [], @@ -371,6 +374,69 @@ export function ProjectSupervisorView({ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }; + /** + * 把实时正文与工具块按"工具出现时正文长度"交替排列: + * 文本段 → 该位置的工具块 → 下一段文本……同一切分点的工具合并成一个块。 + * 没有切分点(历史回合)时退化为"正文在前、工具块在后",与老项目兼容。 + */ + const renderLiveStream = (liveText: string, calls: typeof liveToolCalls) => { + const offsets = directToolTextOffsets[liveToolCallTurnId] ?? {}; + const groups = new Map(); + for (const call of calls) { + const offset = offsets[call.id]; + const key = Number.isFinite(offset) ? Number(offset) : liveText.length; + const bucket = groups.get(key) ?? []; + bucket.push(call); + groups.set(key, bucket); + } + if (groups.size === 0) { + return null; + } + const positions = [...groups.keys()].sort((left, right) => left - right); + const parts: ReactNode[] = []; + let cursor = 0; + for (const position of positions) { + const segment = liveText.slice(cursor, Math.max(cursor, position)); + if (segment.length > 0) { + parts.push( +
+ +
, + ); + } + const bucket = groups.get(position) ?? []; + parts.push( + , + ); + cursor = Math.max(cursor, position); + } + const tail = liveText.slice(cursor); + if (tail.length > 0) { + parts.push( +
+ +
, + ); + } + return parts; + }; + /** 整轮会话的结束时间与耗时(进行中时用 tick 驱的 now,所以秒数会实时跳动)。 */ const renderTurnUsage = (turnId: string) => { if (!turnId) { @@ -573,7 +639,10 @@ export function ProjectSupervisorView({ ) : null} {/* 直连回合的流式正文:恢复为对话区里的普通 assistant 消息(不再放进状态卡片), 这样"边生成边显示"和"状态卡片只放状态"两件事同时成立。 */} - {directCodex && transientReply ? ( + {directCodex && transientReply && liveToolCalls.length > 0 + ? renderLiveStream(transientReply, liveToolCalls) + : null} + {directCodex && transientReply && liveToolCalls.length === 0 ? (
) : null} - {liveToolCalls.length > 0 ? ( + {/* 有正文时,工具块的顺序由 renderLiveStream 决定,这里不再重复渲染 */} + {liveToolCalls.length > 0 && !(directCodex && transientReply) ? (