@veltra/ai 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/fold.js +14 -2
- package/dist/chat/fold.js.map +1 -1
- package/dist/chat/session.d.ts +13 -6
- package/dist/chat/session.js.map +1 -1
- package/dist/chat/use-chat-session.js +3 -13
- package/dist/chat/use-chat-session.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/approval-banner.js.map +1 -1
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/job-bar.js.map +1 -1
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +2 -2
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js.map +1 -1
- package/dist/components/ai-chat/queue-list.js.map +1 -1
- package/dist/components/ai-chat/reasoning-picker.js.map +1 -1
- package/dist/components/ai-chat/side-panel.js.map +1 -1
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/tool-json.js.map +1 -1
- package/dist/components/ai-chat/turn-process.js.map +1 -1
- package/dist/components/ai-orb/ai-orb.js.map +1 -1
- package/package.json +3 -3
- package/src/chat/__test__/bedrock-harness-sim.test.ts +467 -0
- package/src/chat/__test__/fixtures/bedrock/harness.ts +735 -0
- package/src/chat/__test__/fixtures/bedrock/http.ts +117 -0
- package/src/chat/__test__/fixtures/bedrock/types.ts +15 -0
- package/src/chat/__test__/fold.test.ts +26 -1
- package/src/chat/__test__/session.test.ts +1 -1
- package/src/chat/__test__/use-chat.test.ts +6 -3
- package/src/chat/fold.ts +16 -1
- package/src/chat/session.ts +13 -6
- package/src/chat/transports/__test__/openai.test.ts +1 -1
- package/src/chat/use-chat-session.ts +4 -5
- package/src/components/ai-chat/__test__/ai-chat.test.ts +38 -1
- package/src/components/ai-chat/__test__/tool-icons.test.ts +1 -1
- package/src/components/ai-chat/message-list.vue +6 -5
- package/src/components/ai-orb/__test__/ai-orb.test.ts +1 -1
- package/src/tools/ask-question/__test__/ask-question.test.ts +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from bedrock web/src/api(自包含快照,避免测试依赖仓库外绝对路径)。
|
|
3
|
+
* 上游更新时同步此处;依赖面:@veltra/ai 类型 + ./http + ./types。
|
|
4
|
+
*/
|
|
5
|
+
import { storage, storageKey } from '@cat-kit/fe'
|
|
6
|
+
import { HTTPClient, TokenPlugin, type HTTPClientPlugin } from '@cat-kit/http'
|
|
7
|
+
|
|
8
|
+
import type { ApiEnvelope } from './types'
|
|
9
|
+
|
|
10
|
+
const ACCESS_KEY = storageKey<string>('access_token')
|
|
11
|
+
/** Legacy keys cleaned on set/clear (refresh is HttpOnly cookie from server). */
|
|
12
|
+
const LEGACY_REFRESH_KEY = storageKey<string>('refresh_token')
|
|
13
|
+
|
|
14
|
+
export function getAccessToken(): string | null {
|
|
15
|
+
return storage.local.get(ACCESS_KEY)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setAccessToken(access: string): void {
|
|
19
|
+
storage.local.set(ACCESS_KEY, access)
|
|
20
|
+
storage.local.remove(LEGACY_REFRESH_KEY)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clearTokens(): void {
|
|
24
|
+
storage.local.remove(ACCESS_KEY)
|
|
25
|
+
storage.local.remove(LEGACY_REFRESH_KEY)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isApiEnvelope(body: unknown): body is ApiEnvelope {
|
|
29
|
+
if (body === null || typeof body !== 'object') return false
|
|
30
|
+
const o = body as Record<string, unknown>
|
|
31
|
+
return typeof o.code === 'number' && typeof o.message === 'string'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function envelopeErrorMessage(envelope: ApiEnvelope | undefined, fallback: string): string {
|
|
35
|
+
const msg = envelope?.message || fallback
|
|
36
|
+
return envelope?.request_id ? `${msg} [${envelope.request_id}]` : msg
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Unwraps Bedrock `{ code, message, data?, request_id? }` envelopes.
|
|
41
|
+
* Successful calls resolve with `response.body === data`.
|
|
42
|
+
* Must run after TokenPlugin so 401 refresh/retry happens first.
|
|
43
|
+
*/
|
|
44
|
+
function BedrockEnvelopePlugin(): HTTPClientPlugin {
|
|
45
|
+
return {
|
|
46
|
+
name: 'bedrock-envelope',
|
|
47
|
+
afterRespond({ response }) {
|
|
48
|
+
const { code, body } = response
|
|
49
|
+
|
|
50
|
+
// Already unwrapped (e.g. TokenPlugin retry returned a processed response).
|
|
51
|
+
if (!isApiEnvelope(body)) {
|
|
52
|
+
if (code < 200 || code >= 300) {
|
|
53
|
+
throw new Error(`HTTP ${code}`)
|
|
54
|
+
}
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (code < 200 || code >= 300) {
|
|
59
|
+
throw new Error(envelopeErrorMessage(body, `HTTP ${code}`))
|
|
60
|
+
}
|
|
61
|
+
if (body.code !== 0) {
|
|
62
|
+
throw new Error(envelopeErrorMessage(body, 'request failed'))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { ...response, body: body.data }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const envelopePlugin = BedrockEnvelopePlugin()
|
|
71
|
+
|
|
72
|
+
/** Bare client for login/refresh (no Bearer injection). credentials so Set-Cookie / Cookie work. */
|
|
73
|
+
export const bareHttp = new HTTPClient('/api/v1', {
|
|
74
|
+
timeout: 30_000,
|
|
75
|
+
credentials: true,
|
|
76
|
+
plugins: [envelopePlugin]
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
let onAuthExpired: (() => void) | null = null
|
|
80
|
+
|
|
81
|
+
export function setOnAuthExpired(cb: (() => void) | null): void {
|
|
82
|
+
onAuthExpired = cb
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function expireSession(): void {
|
|
86
|
+
clearTokens()
|
|
87
|
+
onAuthExpired?.()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 401 → POST /auth/refresh (cookie) → write new access_token; TokenPlugin retries the request. */
|
|
91
|
+
async function refreshAccessToken(): Promise<void> {
|
|
92
|
+
try {
|
|
93
|
+
const { body } = await bareHttp.post<{ access_token: string }>('/auth/refresh', {})
|
|
94
|
+
if (!body?.access_token) {
|
|
95
|
+
throw new Error('refresh failed')
|
|
96
|
+
}
|
|
97
|
+
setAccessToken(body.access_token)
|
|
98
|
+
} catch (err) {
|
|
99
|
+
expireSession()
|
|
100
|
+
throw err
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const http = new HTTPClient('/api/v1', {
|
|
105
|
+
timeout: 30_000,
|
|
106
|
+
credentials: true,
|
|
107
|
+
plugins: [
|
|
108
|
+
TokenPlugin({
|
|
109
|
+
getter: () => getAccessToken(),
|
|
110
|
+
authType: 'Bearer',
|
|
111
|
+
maxRetries: 1,
|
|
112
|
+
onRefresh: refreshAccessToken,
|
|
113
|
+
shouldRefresh: (response) => response.code === 401
|
|
114
|
+
}),
|
|
115
|
+
envelopePlugin
|
|
116
|
+
]
|
|
117
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from bedrock web/src/api(自包含快照,避免测试依赖仓库外绝对路径)。
|
|
3
|
+
* 上游更新时同步此处;依赖面:@veltra/ai 类型 + ./http + ./types。
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* bedrock web 端 API 类型的最小子集(仅本仿真测试消费的 PageResult)。
|
|
7
|
+
* 来源:bedrock 仓 web/src/api/types.ts,同步以字段演化为准。
|
|
8
|
+
*/
|
|
9
|
+
export interface PageResult<T> {
|
|
10
|
+
items: T[]
|
|
11
|
+
total: number
|
|
12
|
+
page: number
|
|
13
|
+
page_size: number
|
|
14
|
+
total_pages: number
|
|
15
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from '
|
|
1
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
2
2
|
|
|
3
3
|
import { createFoldState, foldSessionEvent } from '../fold'
|
|
4
4
|
import type { ChatSessionEvent } from '../session'
|
|
@@ -45,6 +45,31 @@ describe('foldSessionEvent', () => {
|
|
|
45
45
|
})
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
+
it('history 的 assistant/message 不把流式消息降级为 done、不用更短快照覆盖内容', () => {
|
|
49
|
+
// 断线补拉:直播已流式输出,历史快照落后(甚至为空)
|
|
50
|
+
const lagging = foldAll([
|
|
51
|
+
{ type: 'assistant/chunk', messageId: 'a1', seq: 1, delta: '命令输出' },
|
|
52
|
+
{ type: 'assistant/message', messageId: 'a1', content: '', history: true }
|
|
53
|
+
])
|
|
54
|
+
expect(lagging.messages[0]).toMatchObject({ content: '命令输出', status: 'streaming' })
|
|
55
|
+
|
|
56
|
+
// 断线期间服务端推进更多:历史快照比直播长时采用历史(直播漏了增量)
|
|
57
|
+
const ahead = foldAll([
|
|
58
|
+
{ type: 'assistant/chunk', messageId: 'a1', seq: 1, delta: '命令' },
|
|
59
|
+
{ type: 'assistant/message', messageId: 'a1', content: '命令输出为 slow-done', history: true }
|
|
60
|
+
])
|
|
61
|
+
expect(ahead.messages[0]).toMatchObject({
|
|
62
|
+
content: '命令输出为 slow-done',
|
|
63
|
+
status: 'streaming'
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// 已终态的消息不受 history 标记影响,正常被历史回放覆盖(首挂载播种路径)
|
|
67
|
+
const seeded = foldAll([
|
|
68
|
+
{ type: 'assistant/message', messageId: 'a2', content: '历史答复', history: true }
|
|
69
|
+
])
|
|
70
|
+
expect(seeded.messages[0]).toMatchObject({ content: '历史答复', status: 'done' })
|
|
71
|
+
})
|
|
72
|
+
|
|
48
73
|
it('tool/call 追加 pending 的 ChatToolCall 并保留 view', () => {
|
|
49
74
|
const view = { preview: 'ls' }
|
|
50
75
|
const state = foldAll([
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from '
|
|
1
|
+
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
2
2
|
import { effectScope, nextTick, reactive } from 'vue'
|
|
3
3
|
|
|
4
4
|
import * as toolsModule from '../../tools'
|
|
@@ -765,7 +765,7 @@ describe('useChat session', () => {
|
|
|
765
765
|
spy.mockRestore()
|
|
766
766
|
})
|
|
767
767
|
|
|
768
|
-
it('abort 调 session.cancel;running
|
|
768
|
+
it('abort 调 session.cancel;running 由事件驱动,断线不翻转', async () => {
|
|
769
769
|
const { chat, adapter } = await setupSession()
|
|
770
770
|
expect(chat.running.value).toBe(false)
|
|
771
771
|
|
|
@@ -782,11 +782,14 @@ describe('useChat session', () => {
|
|
|
782
782
|
adapter.emit({ type: 'error', code: 'x', message: '失败' })
|
|
783
783
|
expect(chat.running.value).toBe(false)
|
|
784
784
|
|
|
785
|
+
// 断线 ≠ 停止:running 保持乐观,由重连回放的 prompted / idle 收敛。
|
|
786
|
+
// 曾在此翻转 running=false,步骤间隙断线会让过程块在「已完成 / 思考中」间跳。
|
|
785
787
|
adapter.emit({ type: 'running', running: true })
|
|
786
788
|
adapter.disconnect()
|
|
787
789
|
await vi.waitFor(() => {
|
|
788
|
-
expect(
|
|
790
|
+
expect(adapter.fetchHistory).toHaveBeenCalled()
|
|
789
791
|
})
|
|
792
|
+
expect(chat.running.value).toBe(true)
|
|
790
793
|
})
|
|
791
794
|
|
|
792
795
|
it('队列只认 snapshot;enqueue / startQueued / removeQueued / regenerate 为 no-op', async () => {
|
package/src/chat/fold.ts
CHANGED
|
@@ -161,12 +161,27 @@ export function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent):
|
|
|
161
161
|
break
|
|
162
162
|
}
|
|
163
163
|
case 'assistant/message': {
|
|
164
|
+
const current = next.messages.find((m) => m.id === event.messageId)
|
|
165
|
+
if (event.history && current && current.status === 'streaming') {
|
|
166
|
+
// 历史快照命中直播流式中的消息:快照可能落后(甚至为空),不能把消息
|
|
167
|
+
// 降级为 done 或用更短内容覆盖 —— 否则断线补拉会让消息在「完成 / 思考中」
|
|
168
|
+
// 之间来回翻转;终态由直播的 durable 帧(message_text / step 收尾)落定。
|
|
169
|
+
const longer = (a: string | undefined, b: string | undefined) =>
|
|
170
|
+
(b?.length ?? 0) > (a?.length ?? 0) ? b : a
|
|
171
|
+
next.messages = patchMessage(next.messages, event.messageId, {
|
|
172
|
+
role: 'assistant',
|
|
173
|
+
content: longer(current.content, event.content),
|
|
174
|
+
reasoning: longer(current.reasoning, event.reasoning),
|
|
175
|
+
toolCalls: event.toolCalls ?? current.toolCalls
|
|
176
|
+
})
|
|
177
|
+
break
|
|
178
|
+
}
|
|
164
179
|
next.messages = patchMessage(next.messages, event.messageId, {
|
|
165
180
|
role: 'assistant',
|
|
166
181
|
status: 'done',
|
|
167
182
|
content: event.content,
|
|
168
183
|
reasoning: event.reasoning,
|
|
169
|
-
toolCalls: event.toolCalls ??
|
|
184
|
+
toolCalls: event.toolCalls ?? current?.toolCalls
|
|
170
185
|
})
|
|
171
186
|
break
|
|
172
187
|
}
|
package/src/chat/session.ts
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
import type { AskQuestionItem } from '../tools'
|
|
2
2
|
import type { ChatAttachment, ChatJob, ChatQueuedMessage, ChatToolCall } from './types'
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** 归一化后的服务端会话事件(协议无关)。
|
|
5
|
+
* seq 为持久帧序号:瞬态/历史回放事件省略(不参与乱序门),durable 终态事件携带。 */
|
|
5
6
|
export type ChatSessionEvent =
|
|
6
7
|
| {
|
|
7
8
|
type: 'user/message'
|
|
8
9
|
messageId: string
|
|
9
|
-
seq
|
|
10
|
+
seq?: number
|
|
10
11
|
content: string
|
|
11
12
|
attachments?: ChatAttachment[]
|
|
12
13
|
}
|
|
13
14
|
| {
|
|
14
15
|
type: 'assistant/chunk'
|
|
15
16
|
messageId: string
|
|
16
|
-
seq
|
|
17
|
+
seq?: number
|
|
17
18
|
delta: string
|
|
18
19
|
reasoningDelta?: string
|
|
19
20
|
}
|
|
20
21
|
| {
|
|
21
22
|
type: 'assistant/message'
|
|
22
23
|
messageId: string
|
|
23
|
-
seq
|
|
24
|
+
seq?: number
|
|
24
25
|
content: string
|
|
25
26
|
reasoning?: string
|
|
26
27
|
toolCalls?: ChatToolCall[]
|
|
28
|
+
/**
|
|
29
|
+
* 历史回放(REST 拉取/断线补拉)来源标记。
|
|
30
|
+
* 历史快照可能落后于直播流:fold 对流式中的消息不降级为 done、不用更短的
|
|
31
|
+
* 快照覆盖内容;直播(durable 终态帧)始终是状态权威。
|
|
32
|
+
*/
|
|
33
|
+
history?: boolean
|
|
27
34
|
}
|
|
28
35
|
| {
|
|
29
36
|
type: 'tool/call'
|
|
30
37
|
callId: string
|
|
31
38
|
name: string
|
|
32
39
|
arguments: string
|
|
33
|
-
seq
|
|
40
|
+
seq?: number
|
|
34
41
|
view?: unknown
|
|
35
42
|
}
|
|
36
43
|
| {
|
|
@@ -39,7 +46,7 @@ export type ChatSessionEvent =
|
|
|
39
46
|
status: 'success' | 'error' | 'rejected'
|
|
40
47
|
result?: string
|
|
41
48
|
error?: string
|
|
42
|
-
seq
|
|
49
|
+
seq?: number
|
|
43
50
|
view?: unknown
|
|
44
51
|
}
|
|
45
52
|
| {
|
|
@@ -58,14 +58,13 @@ export function createSessionRuntime(surface: SessionChatSurface) {
|
|
|
58
58
|
fold = createFoldState()
|
|
59
59
|
syncFold(fold, surface)
|
|
60
60
|
let disposed = false
|
|
61
|
+
// 断线 ≠ 会话停止:不注册 onDisconnect 把 running 置 false(保持乐观,由
|
|
62
|
+
// 重连回放的 prompted / step_started / idle 帧收敛)。曾在此置 false ——
|
|
63
|
+
// 步骤间隙断线会把过程块折叠成「已完成」,重连随即又展开,消息列表在
|
|
64
|
+
// 「完成 / 思考中」之间来回跳。
|
|
61
65
|
const dispose = transport.open({
|
|
62
66
|
onEvent(event) {
|
|
63
67
|
if (!disposed) apply(event)
|
|
64
|
-
},
|
|
65
|
-
onDisconnect() {
|
|
66
|
-
if (disposed) return
|
|
67
|
-
fold = { ...fold, running: false }
|
|
68
|
-
surface.running.value = false
|
|
69
68
|
}
|
|
70
69
|
})
|
|
71
70
|
opened = true
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from '
|
|
1
|
+
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
2
2
|
import {
|
|
3
3
|
createApp,
|
|
4
4
|
defineComponent,
|
|
@@ -515,6 +515,43 @@ describe('UAiChat', () => {
|
|
|
515
515
|
unmount()
|
|
516
516
|
})
|
|
517
517
|
|
|
518
|
+
it('工具循环最终答案流式中过程保持展开,停跑后才折叠', async () => {
|
|
519
|
+
let round = 0
|
|
520
|
+
let releaseAnswer: (() => void) | undefined
|
|
521
|
+
const transport: ChatTransport = async (_req, handlers) => {
|
|
522
|
+
round++
|
|
523
|
+
if (round === 1) {
|
|
524
|
+
handlers.onToolCall?.({ id: 'c1', name: 'calculate', arguments: '{"expression":"1+1"}' })
|
|
525
|
+
} else {
|
|
526
|
+
handlers.onTextDelta('答案是 2')
|
|
527
|
+
await new Promise<void>((resolve) => {
|
|
528
|
+
releaseAnswer = resolve
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const tools: ChatTool[] = [
|
|
533
|
+
{ name: 'calculate', description: '计算', parameters: {}, execute: () => ({ value: 2 }) }
|
|
534
|
+
]
|
|
535
|
+
const { host, chat, unmount } = mountAiChat({ transport, tools })
|
|
536
|
+
|
|
537
|
+
chat.value?.send('1+1 等于几')
|
|
538
|
+
|
|
539
|
+
// 最终答案流式中(仍在运行):过程不折叠,工具卡片留在展开列表
|
|
540
|
+
await vi.waitFor(() => {
|
|
541
|
+
expect(host.querySelector('.md-stub')?.textContent).toContain('答案是 2')
|
|
542
|
+
})
|
|
543
|
+
expect(host.querySelector('.u-ai-chat__process')).toBeFalsy()
|
|
544
|
+
expect(host.querySelector('.u-ai-chat__tool-call')).toBeTruthy()
|
|
545
|
+
|
|
546
|
+
// 停跑且末条消息落定后:过程收进「已完成」折叠块
|
|
547
|
+
releaseAnswer?.()
|
|
548
|
+
await vi.waitFor(() => {
|
|
549
|
+
expect(host.querySelector('.u-ai-chat__process-title')?.textContent).toBe('已完成')
|
|
550
|
+
})
|
|
551
|
+
expect(host.querySelector('.u-ai-chat__tool-call')).toBeFalsy()
|
|
552
|
+
unmount()
|
|
553
|
+
})
|
|
554
|
+
|
|
518
555
|
it('单轮无工具对话不出现「已完成」过程块', async () => {
|
|
519
556
|
const transport: ChatTransport = (_req, handlers) => {
|
|
520
557
|
handlers.onReasoningDelta?.('想一下')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EditPen, Folder, Internet, QuestionFilled, Terminal, Tools } from '@veltra/icons/normal'
|
|
2
|
-
import { describe, expect, it } from '
|
|
2
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
3
3
|
import { defineComponent, h } from 'vue'
|
|
4
4
|
|
|
5
5
|
import { FALLBACK_TOOL_ICON, resolveToolIcon } from '../tool-icons'
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
@regenerate="emit('regenerate')"
|
|
18
18
|
/>
|
|
19
19
|
|
|
20
|
-
<!--
|
|
20
|
+
<!-- 本轮落定(结束 / 失败 / 中止)后,之前的思考/工具调用过程收进「已完成」折叠块 -->
|
|
21
21
|
<TurnProcess
|
|
22
22
|
v-if="turn.processCollapsed"
|
|
23
23
|
:messages="turn.processMsgs"
|
|
@@ -200,11 +200,12 @@ const turns = computed<ChatTurn[]>(() => {
|
|
|
200
200
|
const processMsgs = turn.assistants.slice(0, -1)
|
|
201
201
|
const answerMsg = turn.assistants[turn.assistants.length - 1]
|
|
202
202
|
const isLastTurn = index === grouped.length - 1
|
|
203
|
-
//
|
|
204
|
-
// content
|
|
203
|
+
// 该轮落定(非最后一轮 / 停跑且末条消息进入终态)才折叠过程,运行中始终展开。
|
|
204
|
+
// 不能以 answerMsg.content 判断:工具循环每步追加一条空 assistant 消息,answerMsg
|
|
205
|
+
// 指随后移使 content 空/非空交替,折叠态会来回翻转;断连时 running 短暂变 false
|
|
206
|
+
// 但消息仍是 streaming,不会误折叠;regenerate 删除本轮 assistant 重跑,条件自然复位
|
|
205
207
|
const processCollapsed =
|
|
206
|
-
processMsgs.length > 0 &&
|
|
207
|
-
(!isLastTurn || !!answerMsg?.content || (!props.running && isFinalStatus(answerMsg)))
|
|
208
|
+
processMsgs.length > 0 && (!isLastTurn || (!props.running && isFinalStatus(answerMsg)))
|
|
208
209
|
return {
|
|
209
210
|
key: turn.key,
|
|
210
211
|
userMsg: turn.userMsg,
|