fix: 修复对话轮次分组器的类型安全问题

- 移除 grouper.ts 中的 as any 类型断言,使用正确的类型守卫
- 导入具体的 RenderBlock 类型替代 import() 语法
- 合并 ConversationView.vue 中的两个 watch 避免竞争条件
- 使用 lastRenderResultId 追踪变化防止重复初始化
This commit is contained in:
fawney19
2026-01-16 19:53:54 +08:00
parent aa59862a70
commit b807c16395
2 changed files with 72 additions and 44 deletions

View File

@@ -122,12 +122,18 @@ const props = defineProps<{
const systemExpanded = ref(false) const systemExpanded = ref(false)
const historyExpanded = ref(false) const historyExpanded = ref(false)
const expandedTurns = ref<Set<number>>(new Set()) const expandedTurns = ref<Set<number>>(new Set())
const lastRenderResultId = ref<string>('')
// 将渲染结果转换为分组对话 // 将渲染结果转换为分组对话
const groupedConversation = computed(() => { const groupedConversation = computed(() => {
return groupRenderBlocksIntoTurns(props.renderResult.blocks, props.renderResult.isStream) return groupRenderBlocksIntoTurns(props.renderResult.blocks, props.renderResult.isStream)
}) })
// 生成 renderResult 的唯一标识
function getRenderResultId(): string {
return `${props.renderResult.blocks.length}-${props.renderResult.isStream}`
}
// 历史轮次(除最后 1-2 轮外的所有轮次) // 历史轮次(除最后 1-2 轮外的所有轮次)
const historyTurns = computed(() => { const historyTurns = computed(() => {
const turns = groupedConversation.value.turns const turns = groupedConversation.value.turns
@@ -163,18 +169,16 @@ function initExpandedTurns() {
} }
} }
// renderResult 变化重置状态 // 监听 renderResult 变化重置状态并初始化展开轮次
watch(() => props.renderResult, () => { watch(() => props.renderResult, () => {
systemExpanded.value = false const currentId = getRenderResultId()
nextTick(() => { if (currentId !== lastRenderResultId.value) {
initExpandedTurns() lastRenderResultId.value = currentId
}) systemExpanded.value = false
}, { deep: true }) historyExpanded.value = false
nextTick(() => {
// 初始加载时也要初始化 initExpandedTurns()
watch(groupedConversation, () => { })
if (expandedTurns.value.size === 0 && groupedConversation.value.turns.length > 0) {
initExpandedTurns()
} }
}, { immediate: true }) }, { deep: true, immediate: true })
</script> </script>

View File

@@ -11,8 +11,21 @@ import type {
GroupedConversation, GroupedConversation,
TurnStats, TurnStats,
TurnSummary, TurnSummary,
TextContentBlock,
} from './types' } from './types'
import type {
RenderBlock,
MessageRenderBlock,
TextRenderBlock,
CollapsibleRenderBlock,
CodeRenderBlock,
ToolUseRenderBlock,
ToolResultRenderBlock,
ImageRenderBlock,
ErrorRenderBlock,
} from './render'
// ============================================================ // ============================================================
// 常量配置 // 常量配置
// ============================================================ // ============================================================
@@ -32,8 +45,8 @@ const SUMMARY_MIN_RATIO = 0.6
*/ */
function extractTextFromBlocks(blocks: ContentBlock[]): string { function extractTextFromBlocks(blocks: ContentBlock[]): string {
return blocks return blocks
.filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text') .filter((b): b is TextContentBlock => b.type === 'text')
.map(b => (b as any).text || '') .map(b => b.text || '')
.join(' ') .join(' ')
.trim() .trim()
} }
@@ -277,7 +290,7 @@ export function groupConversation(conversation: ParsedConversation): GroupedConv
* 用于已渲染的结果进行轮次分组 * 用于已渲染的结果进行轮次分组
*/ */
export function groupRenderBlocksIntoTurns( export function groupRenderBlocksIntoTurns(
blocks: import('./render').RenderBlock[], blocks: RenderBlock[],
isStream: boolean = false isStream: boolean = false
): GroupedConversation { ): GroupedConversation {
const turns: ConversationTurn[] = [] const turns: ConversationTurn[] = []
@@ -288,7 +301,7 @@ export function groupRenderBlocksIntoTurns(
for (const block of blocks) { for (const block of blocks) {
if (block.type !== 'message') continue if (block.type !== 'message') continue
const messageBlock = block as import('./render').MessageRenderBlock const messageBlock = block as MessageRenderBlock
if (messageBlock.role === 'system') { if (messageBlock.role === 'system') {
// 提取 system prompt // 提取 system prompt
@@ -345,10 +358,10 @@ export function groupRenderBlocksIntoTurns(
/** /**
* 从渲染块中提取文本 * 从渲染块中提取文本
*/ */
function extractTextFromRenderBlocks(blocks: import('./render').RenderBlock[]): string { function extractTextFromRenderBlocks(blocks: RenderBlock[]): string {
return blocks return blocks
.filter(b => b.type === 'text') .filter((b): b is TextRenderBlock => b.type === 'text')
.map(b => (b as any).content || '') .map(b => b.content || '')
.join(' ') .join(' ')
.trim() .trim()
} }
@@ -356,60 +369,71 @@ function extractTextFromRenderBlocks(blocks: import('./render').RenderBlock[]):
/** /**
* 将 MessageRenderBlock 转换为 ParsedMessage * 将 MessageRenderBlock 转换为 ParsedMessage
*/ */
function renderBlockToMessage(block: import('./render').MessageRenderBlock): ParsedMessage { function renderBlockToMessage(block: MessageRenderBlock): ParsedMessage {
const content: ContentBlock[] = [] const content: ContentBlock[] = []
for (const child of block.content) { for (const child of block.content) {
switch (child.type) { switch (child.type) {
case 'text': case 'text': {
content.push({ type: 'text', text: (child as any).content || '' }) const textBlock = child as TextRenderBlock
content.push({ type: 'text', text: textBlock.content || '' })
break break
case 'collapsible': }
// 可能是 thinking从 code 块中提取内容 case 'collapsible': {
if ((child as any).title?.includes('思考')) { const collapsibleBlock = child as CollapsibleRenderBlock
const innerBlocks = (child as any).content || [] if (collapsibleBlock.title?.includes('思考')) {
const codeBlock = innerBlocks.find((b: any) => b.type === 'code') const codeBlock = collapsibleBlock.content.find(
const thinkingText = codeBlock?.code || '' (b): b is CodeRenderBlock => b.type === 'code'
content.push({ type: 'thinking', thinking: thinkingText }) )
content.push({ type: 'thinking', thinking: codeBlock?.code || '' })
} }
break break
case 'tool_use': }
case 'tool_use': {
const toolUseBlock = child as ToolUseRenderBlock
content.push({ content.push({
type: 'tool_use', type: 'tool_use',
toolId: (child as any).toolId || '', toolId: toolUseBlock.toolId || '',
toolName: (child as any).toolName || '', toolName: toolUseBlock.toolName || '',
input: (child as any).input || '', input: toolUseBlock.input || '',
}) })
break break
case 'tool_result': }
case 'tool_result': {
const toolResultBlock = child as ToolResultRenderBlock
content.push({ content.push({
type: 'tool_result', type: 'tool_result',
toolUseId: '', toolUseId: '',
content: (child as any).content || '', content: toolResultBlock.content || '',
isError: (child as any).isError, isError: toolResultBlock.isError,
}) })
break break
case 'image': }
case 'image': {
const imageBlock = child as ImageRenderBlock
content.push({ content.push({
type: 'image', type: 'image',
sourceType: 'url', sourceType: 'url',
url: (child as any).src, url: imageBlock.src,
mimeType: (child as any).mimeType, mimeType: imageBlock.mimeType,
alt: (child as any).alt, alt: imageBlock.alt,
}) })
break break
case 'error': }
case 'error': {
const errorBlock = child as ErrorRenderBlock
content.push({ content.push({
type: 'error', type: 'error',
message: (child as any).message || '', message: errorBlock.message || '',
code: (child as any).code, code: errorBlock.code,
}) })
break break
}
} }
} }
return { return {
role: block.role as any, role: block.role,
content, content,
} }
} }