fix: 分页问题

This commit is contained in:
07akioni
2026-08-12 20:26:47 +08:00
parent 3784607f3d
commit dabe6207f5
46 changed files with 1594 additions and 66 deletions

View File

@@ -9,6 +9,7 @@ import {
createAssistantMessage,
createToolResultMessage,
createUserMessage,
isTokenDelta,
} from '@deepseek-ai/dsh-llm/message'
import { CallId } from '@deepseek-ai/dsh-llm/brand'
import type {
@@ -861,6 +862,76 @@ function tokenUsageOf(log: readonly SessionEvent[]): FixtureTokenUsageProjection
return totals
}
/** Fixture parallel of session-stats' whole-log counting and wall-time fold. */
function sessionStatsOf(log: readonly SessionEvent[]): {
turns: number
steps: number
llmMs: number
toolMs: number
ttftMs: number
ttftSteps: number
decodeMs: number
decodeTokens: number
} {
const value = { turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0 }
let lastTurn: number | null = null
let openStep: { turn: number; step: number; startTime: number; firstTokenTime: number | null } | null = null
const pendingCalls = new Map<string, number>()
for (const event of log) {
switch (event.type) {
case 'step/start':
openStep = { turn: event.data.turn, step: event.data.step, startTime: event.time, firstTokenTime: null }
break
case 'assistant/chunk':
if (openStep !== null && openStep.turn === event.data.turn && openStep.step === event.data.step
&& openStep.firstTokenTime === null && isTokenDelta(event.data.chunk)) {
openStep.firstTokenTime = event.time
}
break
case 'assistant/message': {
if (openStep === null || openStep.turn !== event.data.turn || openStep.step !== event.data.step) break
value.llmMs += Math.max(0, event.time - openStep.startTime)
if (openStep.firstTokenTime !== null) {
value.ttftMs += Math.max(0, openStep.firstTokenTime - openStep.startTime)
value.ttftSteps += 1
const outputTokens = event.data.usage?.outputTokens
if (typeof outputTokens === 'number' && Number.isFinite(outputTokens) && outputTokens >= 0) {
value.decodeMs += Math.max(0, event.time - openStep.firstTokenTime)
value.decodeTokens += outputTokens
}
}
openStep = null
break
}
case 'tool/call':
pendingCalls.set(event.data.callId, event.time)
break
case 'tool/result': {
const callId = event.data.message.source.callId
const dispatched = pendingCalls.get(callId)
if (dispatched === undefined) break
pendingCalls.delete(callId)
value.toolMs += Math.max(0, event.time - dispatched)
break
}
case 'step/end':
if (event.data.turn !== lastTurn) {
value.turns += 1
lastTurn = event.data.turn
}
value.steps += 1
openStep = null
break
case 'turn/end':
pendingCalls.clear()
break
default:
break
}
}
return value
}
interface FixtureRequestContext {
provider: string
model: string
@@ -979,6 +1050,8 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
values['contextPressure'] = contextPressureOf(log)
// Always present (token-meter composed): heuristic request composition.
values['contextBreakdown'] = contextBreakdownOf(log)
// Always present (session-stats unit composed): whole-log turn/step counts.
values['sessionStats'] = sessionStatsOf(log)
return values
}
@@ -1014,6 +1087,17 @@ function projectionFramesOf(id: SessionId, log: readonly SessionEvent[], event:
seq: event.seq,
})
}
// The stats fold's view advances on message assembly and tool settlement
// (wall times) and on step close (counts).
if (type === 'assistant/message' || type === 'tool/result' || type === 'step/end') {
frames.push({
type: 'session/projection',
sessionId: id,
key: 'sessionStats',
value: sessionStatsOf(log),
seq: event.seq,
})
}
if (frames.length > 0) return frames
if (type === 'session/title') {
const values = projectionValuesOf(log)

View File

@@ -164,6 +164,10 @@ describe('createFixtureApi', () => {
toolsTokens: 0,
messageTokens: 0,
},
// Session-stats unit composed: no figure accrues on the empty log.
sessionStats: {
turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0,
},
} },
})
})
@@ -353,7 +357,7 @@ describe('createFixtureApi', () => {
const envelopes: RpcRequest<MuxFrame>[] = []
for await (const envelope of api.events.mux(req({}), abort.signal)) {
envelopes.push(envelope)
if (envelopes.length >= 11) abort.abort()
if (envelopes.length >= 12) abort.abort()
}
return envelopes
}
@@ -374,10 +378,12 @@ describe('createFixtureApi', () => {
value: { systemTokens: 0, toolsTokens: 0 },
})
expect((first[8]?.payload as { value: { messageTokens: number } }).value.messageTokens).toBeGreaterThan(0)
expect(first[9]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[9]?.rpcId).toBe(first[9]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[10]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[10]?.rpcId).toBe(first[10]?.rpcId)
expect(first[9]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'sessionStats' })
expect((first[9]?.payload as { value: { turns: number; steps: number } }).value.steps).toBeGreaterThan(0)
expect(first[10]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[10]?.rpcId).toBe(first[10]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[11]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[11]?.rpcId).toBe(first[11]?.rpcId)
})
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {