@foundbyte/uni-agent 1.0.0-alpha.0

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.
Files changed (32) hide show
  1. package/components/bubble-list/index.scss +254 -0
  2. package/components/bubble-list/index.vue +505 -0
  3. package/components/bubble-list/types.ts +110 -0
  4. package/components/bubble-list/use-typewriter.ts +193 -0
  5. package/components/bubble-wrapper/index.scss +34 -0
  6. package/components/bubble-wrapper/index.vue +97 -0
  7. package/components/history/index.scss +87 -0
  8. package/components/history/index.vue +118 -0
  9. package/components/history/types.ts +16 -0
  10. package/components/index.ts +4 -0
  11. package/components/prompts/index.scss +30 -0
  12. package/components/prompts/index.vue +46 -0
  13. package/components/prompts/types.ts +11 -0
  14. package/components/sender/image-picker.ts +100 -0
  15. package/components/sender/index.scss +288 -0
  16. package/components/sender/index.vue +374 -0
  17. package/components/sender/types.ts +56 -0
  18. package/composables/index.ts +6 -0
  19. package/composables/use-inner-audio/audio-manager.ts +22 -0
  20. package/composables/use-inner-audio/enums.ts +15 -0
  21. package/composables/use-inner-audio/types.ts +8 -0
  22. package/composables/use-inner-audio/use-inner-audio.ts +149 -0
  23. package/composables/use-prefix.ts +54 -0
  24. package/composables/use-recorder/h5-recorder-manager.ts +492 -0
  25. package/composables/use-recorder/native-recorder-manager.ts +181 -0
  26. package/composables/use-recorder/recorder-manager.ts +40 -0
  27. package/composables/use-recorder/types.ts +73 -0
  28. package/composables/use-recorder/use-recorder.ts +205 -0
  29. package/configs/index.ts +5 -0
  30. package/index.ts +3 -0
  31. package/package.json +90 -0
  32. package/styles/_base.scss +8 -0
@@ -0,0 +1,505 @@
1
+ <template>
2
+ <scroll-view
3
+ :class="p('list')"
4
+ scroll-y
5
+ :scroll-top="scrollTop"
6
+ :scroll-with-animation="true"
7
+ :scroll-into-view="scrollIntoView"
8
+ enhanced
9
+ :bounces="false"
10
+ :throttle="false"
11
+ :style="rootStyle"
12
+ @scroll="onScroll"
13
+ @scrolltolower="$emit('reach-bottom')"
14
+ >
15
+ <view :class="p('list-content')">
16
+ <!-- 列表顶部插槽 -->
17
+ <slot name="before" />
18
+
19
+ <view
20
+ v-for="(message, index) in messages"
21
+ :id="`bubble-${message.id}`"
22
+ :key="message.id || index"
23
+ :class="[
24
+ p('item'),
25
+ p('item', getPlacement(message)),
26
+ { [p('item', 'last')]: index === messages.length - 1 },
27
+ getAnimationClass(message),
28
+ ]"
29
+ :style="getAnimationStyle(message)"
30
+ >
31
+ <!-- 头像 - 左侧(对方) -->
32
+ <view
33
+ v-if="getPlacement(message) === BubblePlacement.Start && isShowAvatar(message)"
34
+ :class="[p('avatar'), p('avatar', 'left')]"
35
+ >
36
+ <slot name="avatar-left" :message="message" :index="index">
37
+ <image
38
+ v-if="getAvatar(message)"
39
+ :class="p(['avatar', 'image'])"
40
+ :src="getAvatar(message)"
41
+ mode="aspectFill"
42
+ />
43
+ <view v-else :class="[p(['avatar', 'default']), p(['avatar', 'default'], 'ai')]">
44
+ <text :class="p(['avatar', 'icon'])"> 🤖 </text>
45
+ </view>
46
+ </slot>
47
+ </view>
48
+
49
+ <!-- 消息内容区域 -->
50
+ <view :class="[p('content'), p('content', getPlacement(message))]">
51
+ <!-- 角色名称 -->
52
+ <view v-if="getRoleName(message)" :class="p('role-name')">
53
+ {{ getRoleName(message) }}
54
+ </view>
55
+
56
+ <!-- 消息气泡 -->
57
+ <view
58
+ :class="[
59
+ p('message'),
60
+ p('message', getPlacement(message)),
61
+ p('message', message.role),
62
+ { [p('message', 'loading')]: message.loading },
63
+ ]"
64
+ >
65
+ <!-- 加载状态 -->
66
+ <view v-if="message.loading" :class="p('loading')">
67
+ <view :class="p(['loading', 'dot'])" />
68
+ <view :class="p(['loading', 'dot'])" />
69
+ <view :class="p(['loading', 'dot'])" />
70
+ </view>
71
+
72
+ <slot v-if="!message.loading" name="content" :message="message" :index="index"></slot>
73
+
74
+ <!-- 内容底部插槽 -->
75
+ <slot name="content-footer" :message="message" :index="index" />
76
+ </view>
77
+
78
+ <!-- 消息底部(时间等) -->
79
+ <view v-if="message.time || $slots['message-footer']" :class="p('message-footer')">
80
+ <!-- <text v-if="message.time" :class="p('time')">
81
+ {{ message.time }}
82
+ </text> -->
83
+ <slot name="message-footer" :message="message" :index="index" />
84
+ </view>
85
+ </view>
86
+
87
+ <!-- 头像 - 右侧(自己) -->
88
+ <view
89
+ v-if="getPlacement(message) === BubblePlacement.End && isShowAvatar(message)"
90
+ :class="[p('avatar'), p('avatar', 'right')]"
91
+ >
92
+ <slot name="avatar-right" :message="message" :index="index">
93
+ <image
94
+ v-if="getAvatar(message)"
95
+ :class="p(['avatar', 'image'])"
96
+ :src="getAvatar(message)"
97
+ mode="aspectFill"
98
+ />
99
+ <view v-else :class="[p(['avatar', 'default']), p(['avatar', 'default'], 'user')]">
100
+ <text :class="p(['avatar', 'icon'])"> 👤 </text>
101
+ </view>
102
+ </slot>
103
+ </view>
104
+ </view>
105
+
106
+ <!-- 列表底部插槽 -->
107
+ <slot name="after" />
108
+
109
+ <!-- 底部锚点,用于滚动到底部 -->
110
+ <view :id="listBottomAnchorId" :class="p('list-bottom-anchor')" />
111
+ </view>
112
+ </scroll-view>
113
+ </template>
114
+
115
+ <script setup lang="ts">
116
+ // eslint-disable-next-line no-restricted-imports
117
+ import { computed, nextTick, ref, watch } from 'vue'
118
+ import { parse as parseRichText } from 'marked'
119
+ import {
120
+ type BubbleListProps,
121
+ type BubbleMessage,
122
+ BubblePlacement,
123
+ MessageAnimation,
124
+ type MessageAnimationConfig,
125
+ MessageRole,
126
+ } from './types'
127
+ import { useTypewriterManager } from './use-typewriter'
128
+ import { usePrefix } from '../../composables'
129
+
130
+ defineOptions({
131
+ options: {
132
+ styleIsolation: 'shared',
133
+ virtualHost: true,
134
+ },
135
+ })
136
+
137
+ const props = withDefaults(defineProps<BubbleListProps>(), {
138
+ messages: () => [],
139
+ roleConfigs: () => ({}),
140
+ defaultPlacement: 'start',
141
+ autoScroll: true,
142
+ defaultTypingSpeed: 30,
143
+ showRole: true,
144
+ showAvatar: true,
145
+ animation: () => ({ type: MessageAnimation.SlideLeft }),
146
+ })
147
+
148
+ const emit = defineEmits<{
149
+ (e: 'scroll', event: ScrollEvent): void
150
+ (e: 'reach-top'): void
151
+ (e: 'reach-bottom'): void
152
+ }>()
153
+
154
+ const p = usePrefix('bubble')
155
+
156
+ const listBottomAnchorId = `anchor-${Math.random().toString(32).substring(2)}`
157
+
158
+ type ScrollEvent = {
159
+ detail: {
160
+ deltaX: number
161
+ deltaY: number
162
+ scrollHeight: number
163
+ scrollLeft: number
164
+ scrollTop: number
165
+ scrollWidth: number
166
+ }
167
+ }
168
+
169
+ // 滚动位置控制
170
+ const scrollTop = ref(0)
171
+ const scrollIntoView = ref('')
172
+
173
+ // 需要触发动画的消息 ID 集合
174
+ const pendingAnimationIds = ref<Set<string>>(new Set())
175
+ // 已经准备好初始状态、可以触发动画的消息 ID 集合
176
+ const readyToAnimateIds = ref<Set<string>>(new Set())
177
+
178
+ // 打字机效果管理
179
+ const typewriterManager = useTypewriterManager()
180
+
181
+ /**
182
+ * 根节点样式 - 注入默认动画 CSS 变量
183
+ */
184
+ const rootStyle = computed(() => {
185
+ const style: Record<string, string> = {}
186
+ const globalAnimation = props.animation
187
+
188
+ style['--animation-duration'] = `${globalAnimation.duration || 300}ms`
189
+ style['--animation-delay'] = `${globalAnimation.delay || 0}ms`
190
+
191
+ return style
192
+ })
193
+
194
+ /**
195
+ * 获取消息的打字机状态
196
+ */
197
+ function getMessageTypewriter(message: BubbleMessage) {
198
+ // 从 props.messages 中动态查找消息,确保响应式更新
199
+ const messageRef = computed(() => {
200
+ return props.messages.find((m) => m.id === message.id) || message
201
+ })
202
+
203
+ // 只有 AI 消息且开启了 typing 才启用打字机效果
204
+ const enabled = computed(
205
+ () => messageRef.value.role === MessageRole.Ai && !!messageRef.value.typing,
206
+ )
207
+ const content = computed(() => messageRef.value.content || '')
208
+
209
+ // 用于控制滚动频率的节流
210
+ // let scrollThrottleTimer: ReturnType<typeof setTimeout> | null = null
211
+
212
+ return typewriterManager.getOrCreate(message.id, content, enabled, {
213
+ speed: message.typingSpeed || props.defaultTypingSpeed,
214
+ autoStart: true,
215
+ onUpdate: (index, len) => {
216
+ // 打字时自动滚动(如果不是用户主动滚动)
217
+ if (props.autoScroll) {
218
+ const lastMessage = props.messages[props.messages.length - 1]
219
+ if (lastMessage?.id === message.id) {
220
+ scrollToBottom()
221
+ if (index === len) {
222
+ setTimeout(scrollToBottom, 500)
223
+ }
224
+ }
225
+ }
226
+ },
227
+ })
228
+ }
229
+
230
+ function getMessageContent(message: BubbleMessage, isRichText = false): string {
231
+ const { displayText, isComplete } = getMessageTypewriter(message)
232
+ let content = displayText.value
233
+ if (isComplete.value || message.role !== MessageRole.Ai || !message.typing) {
234
+ content = message.content || ''
235
+ }
236
+ return isRichText ? (parseRichText(content) as string) : content
237
+ }
238
+
239
+ // 监听消息变化,自动滚动到底部
240
+ watch(
241
+ () => props.messages.length,
242
+ (newLen, oldLen) => {
243
+ if (newLen > (oldLen || 0) && props.autoScroll) {
244
+ scrollToBottom()
245
+ }
246
+ },
247
+ { flush: 'post' },
248
+ )
249
+
250
+ // 监听最后一条消息的 loading 状态变化
251
+ watch(
252
+ () => props.messages[props.messages.length - 1]?.loading,
253
+ (loading) => {
254
+ if (props.autoScroll && loading) {
255
+ scrollToBottom()
256
+ }
257
+ },
258
+ )
259
+
260
+ // 监听最后一条 AI 消息的内容变化(流式输出时实时滚动)
261
+ let contentWatchStop: (() => void) | null = null
262
+
263
+ watch(
264
+ () => props.messages[props.messages.length - 1],
265
+ (lastMessage) => {
266
+ // 清理旧的监听
267
+ if (contentWatchStop) {
268
+ contentWatchStop()
269
+ contentWatchStop = null
270
+ }
271
+
272
+ // 如果是 AI 消息且有内容变化,监听其 content
273
+ if (lastMessage?.role === MessageRole.Ai && lastMessage?.content !== undefined) {
274
+ contentWatchStop = watch(
275
+ () => lastMessage.content,
276
+ () => {
277
+ if (props.autoScroll) {
278
+ scrollToBottom()
279
+ }
280
+ },
281
+ { flush: 'post' },
282
+ )
283
+ }
284
+ },
285
+ { immediate: true },
286
+ )
287
+
288
+ function getPlacement(message: BubbleMessage) {
289
+ const config = props.roleConfigs[message.role]
290
+ return (
291
+ config?.placement ||
292
+ (message.role === MessageRole.User ? BubblePlacement.End : BubblePlacement.Start)
293
+ )
294
+ }
295
+
296
+ /**
297
+ * 获取消息的动画配置
298
+ * 优先级:消息自身的 animation > 全局 animation 配置
299
+ */
300
+ function getMessageAnimation(message: BubbleMessage): MessageAnimationConfig {
301
+ return (
302
+ message.animation ||
303
+ props.animation || {
304
+ type: MessageAnimation.SlideLeft,
305
+ duration: 300,
306
+ delay: 0,
307
+ }
308
+ )
309
+ }
310
+
311
+ /**
312
+ * 获取动画类名
313
+ * 根据消息位置和动画类型返回对应的动画类
314
+ * 只有在 readyToAnimateIds 中时才返回动画类(确保初始状态已设置)
315
+ */
316
+ function getAnimationClass(message: BubbleMessage): string {
317
+ const animation = getMessageAnimation(message)
318
+
319
+ // 如果指定了自定义类名,直接返回
320
+ if (animation.customClass) {
321
+ return animation.customClass
322
+ }
323
+
324
+ // 根据动画类型返回对应类名
325
+ const type = animation.type || MessageAnimation.SlideLeft
326
+
327
+ // 如果是无动画,返回空字符串
328
+ if (type === MessageAnimation.None) {
329
+ return ''
330
+ }
331
+
332
+ // 只有当消息准备好触发动画时才添加动画类
333
+ if (!readyToAnimateIds.value.has(message.id)) {
334
+ return ''
335
+ }
336
+
337
+ // 根据消息位置和动画类型生成类名
338
+ const placement = getPlacement(message)
339
+ const baseClass = p('animate')
340
+
341
+ // 处理 SlideLeft 和 SlideRight,需要根据位置决定方向
342
+ if (type === MessageAnimation.SlideLeft) {
343
+ // 左侧消息从左滑入,右侧消息从右滑入
344
+ return `${baseClass}--${placement === BubblePlacement.End ? 'slide-right' : 'slide-left'}`
345
+ }
346
+
347
+ if (type === MessageAnimation.SlideRight) {
348
+ // 左侧消息从右滑入,右侧消息从左滑入(反向)
349
+ return `${baseClass}--${placement === BubblePlacement.End ? 'slide-left' : 'slide-right'}`
350
+ }
351
+
352
+ // 其他动画类型直接使用
353
+ return `${baseClass}--${type}`
354
+ }
355
+
356
+ /**
357
+ * 获取动画样式
358
+ * 设置初始状态和 transition 相关的 CSS 变量
359
+ */
360
+ function getAnimationStyle(message: BubbleMessage): Record<string, string> {
361
+ const animation = getMessageAnimation(message)
362
+ const style: Record<string, string> = {}
363
+
364
+ // 获取全局默认值
365
+ const globalDuration = props.animation?.duration ?? 300
366
+ const globalDelay = props.animation?.delay ?? 0
367
+
368
+ // 只在与全局默认值不同时才设置 CSS 变量
369
+ if (animation.duration !== undefined && animation.duration !== globalDuration) {
370
+ style['--animation-duration'] = `${animation.duration}ms`
371
+ }
372
+ if (animation.delay !== undefined && animation.delay !== globalDelay) {
373
+ style['--animation-delay'] = `${animation.delay}ms`
374
+ }
375
+
376
+ const type = animation.type || MessageAnimation.SlideLeft
377
+
378
+ // 无动画类型,直接返回
379
+ if (type === MessageAnimation.None) {
380
+ return style
381
+ }
382
+
383
+ // 只有当消息是新添加的(在 pendingAnimationIds 中但不在 readyToAnimateIds 中)
384
+ // 才设置初始状态(opacity: 0, transform: ...)
385
+ if (!pendingAnimationIds.value.has(message.id) || readyToAnimateIds.value.has(message.id)) {
386
+ return style
387
+ }
388
+
389
+ // 根据动画类型设置初始状态
390
+ switch (type) {
391
+ case MessageAnimation.SlideLeft:
392
+ // 左侧消息从左滑入,右侧消息从右滑入
393
+ if (getPlacement(message) === BubblePlacement.End) {
394
+ style.transform = 'translateX(60rpx)'
395
+ } else {
396
+ style.transform = 'translateX(-60rpx)'
397
+ }
398
+ style.opacity = '0'
399
+ break
400
+ case MessageAnimation.SlideRight:
401
+ // 左侧消息从右滑入,右侧消息从左滑入(反向)
402
+ if (getPlacement(message) === BubblePlacement.End) {
403
+ style.transform = 'translateX(-60rpx)'
404
+ } else {
405
+ style.transform = 'translateX(60rpx)'
406
+ }
407
+ style.opacity = '0'
408
+ break
409
+ case MessageAnimation.Scale:
410
+ case MessageAnimation.Bounce:
411
+ style.transform = 'scale(0.8)'
412
+ style.opacity = '0'
413
+ break
414
+ case MessageAnimation.Fade:
415
+ default:
416
+ style.opacity = '0'
417
+ break
418
+ }
419
+
420
+ return style
421
+ }
422
+
423
+ function isShowRole(message: BubbleMessage): boolean {
424
+ const config = props.roleConfigs[message.role]
425
+ // 优先使用角色配置,如果没有则使用全局配置
426
+ if (config?.showRole !== undefined) return config.showRole
427
+ return props.showRole
428
+ }
429
+
430
+ function isShowAvatar(message: BubbleMessage): boolean {
431
+ const config = props.roleConfigs[message.role]
432
+ // 优先使用角色配置,如果没有则使用全局配置
433
+ if (config?.showAvatar !== undefined) return config.showAvatar
434
+ return props.showAvatar
435
+ }
436
+
437
+ function getRoleName(message: BubbleMessage): string {
438
+ if (!isShowRole(message)) return ''
439
+ return message.roleName || props.roleConfigs[message.role]?.name || ''
440
+ }
441
+
442
+ function getAvatar(message: BubbleMessage): string {
443
+ if (!isShowAvatar(message)) return ''
444
+ return message.avatar || props.roleConfigs[message.role]?.avatar || ''
445
+ }
446
+
447
+ function isRichContent(content?: string): content is string {
448
+ if (!content) return false
449
+ return /<[^>]+>/.test(content) || /[*#`\-_!]/.test(content)
450
+ }
451
+
452
+ const scrollToBottom = () => {
453
+ // 使用底部锚点进行滚动,更可靠
454
+ scrollIntoView.value = ''
455
+ nextTick(() => {
456
+ scrollIntoView.value = listBottomAnchorId
457
+ })
458
+ }
459
+
460
+ function onScroll(event: ScrollEvent) {
461
+ emit('scroll', event)
462
+ if (event.detail.scrollTop <= 10) emit('reach-top')
463
+ }
464
+
465
+ // 监听消息列表变化,为正在打字的AI消息添加滚动监听
466
+ watch(
467
+ () => props.messages.map((m) => m.id),
468
+ (newIds, oldIds) => {
469
+ // 清理已删除消息的打字机状态
470
+ if (oldIds) {
471
+ oldIds.forEach((id) => {
472
+ if (!newIds.includes(id)) {
473
+ typewriterManager.remove(id)
474
+ pendingAnimationIds.value.delete(id)
475
+ readyToAnimateIds.value.delete(id)
476
+ }
477
+ })
478
+ }
479
+
480
+ // 为新添加的消息触发动画
481
+ const newMessageIds = newIds.filter((id) => !oldIds?.includes(id))
482
+ if (newMessageIds.length > 0) {
483
+ // 第一步:立即添加到 pendingAnimationIds,让 getAnimationStyle 设置初始状态(opacity: 0)
484
+ newMessageIds.forEach((id) => pendingAnimationIds.value.add(id))
485
+
486
+ // 第二步:在下一帧添加到 readyToAnimateIds,让 getAnimationClass 添加动画类,触发 transition
487
+ nextTick(() => {
488
+ newMessageIds.forEach((id) => readyToAnimateIds.value.add(id))
489
+ })
490
+ }
491
+ },
492
+ { immediate: true },
493
+ )
494
+
495
+ // 暴露方法
496
+ defineExpose({
497
+ isRichContent,
498
+ getMessageContent,
499
+ scrollToBottom,
500
+ })
501
+ </script>
502
+
503
+ <style lang="scss">
504
+ @import './index.scss';
505
+ </style>
@@ -0,0 +1,110 @@
1
+ /** 消息角色类型 */
2
+ export enum MessageRole {
3
+ Ai = 'ai',
4
+ User = 'user',
5
+ System = 'system',
6
+ }
7
+
8
+ /** 气泡位置 */
9
+ export enum BubblePlacement {
10
+ Start = 'start',
11
+ End = 'end',
12
+ Both = 'both',
13
+ Center = 'center',
14
+ }
15
+
16
+ /** 消息动画类型 */
17
+ export enum MessageAnimation {
18
+ /** 无动画 */
19
+ None = 'none',
20
+ /** 淡入 */
21
+ Fade = 'fade',
22
+ /** 从左滑入 */
23
+ SlideLeft = 'slide-left',
24
+ /** 从右滑入 */
25
+ SlideRight = 'slide-right',
26
+ /** 缩放进入 */
27
+ Scale = 'scale',
28
+ /** 弹跳进入 */
29
+ Bounce = 'bounce',
30
+ }
31
+
32
+ /** 消息动画配置 */
33
+ export interface MessageAnimationConfig {
34
+ /** 动画类型 */
35
+ type?: MessageAnimation
36
+ /** 动画持续时间(毫秒),默认 300ms */
37
+ duration?: number
38
+ /** 动画延迟(毫秒),默认 0ms */
39
+ delay?: number
40
+ /** 自定义动画类名(会覆盖 type) */
41
+ customClass?: string
42
+ }
43
+
44
+ /** 单条消息 */
45
+ export interface BubbleMessage {
46
+ /** 消息唯一标识 */
47
+ id: string
48
+ /** 消息角色 */
49
+ role: string
50
+ /** 消息内容 */
51
+ content?: string
52
+ /** 是否加载中 */
53
+ loading?: boolean
54
+ /** 是否开启打字机效果(仅AI消息有效) */
55
+ typing?: boolean
56
+ /** 打字速度(毫秒/字符),默认 30ms */
57
+ typingSpeed?: number
58
+ /** 发送时间 */
59
+ time?: string
60
+ /** 音频URL */
61
+ audioUrl?: string
62
+ /** 是否正在播放音频 */
63
+ isPlaying?: boolean
64
+ /** 动画配置(优先级高于全局配置) */
65
+ animation?: MessageAnimationConfig
66
+ /** 自定义数据 */
67
+ [key: string]: any
68
+ }
69
+
70
+ /** 角色配置 */
71
+ export interface BubbleRoleConfig {
72
+ /** 角色名称 */
73
+ name?: string
74
+ /** 头像地址 */
75
+ avatar?: string
76
+ /** 气泡位置:start-左侧(对方), end-右侧(自己), both-两侧,center-水平居中 */
77
+ placement?: 'start' | 'end' | 'both' | 'center'
78
+ /** 气泡背景色 */
79
+ bubbleColor?: string
80
+ /** 气泡文字颜色 */
81
+ textColor?: string
82
+ /** 自定义渲染器 */
83
+ contentRenderer?: (content: string) => string
84
+ /** 是否显示角色名称 */
85
+ showRole?: boolean
86
+ /** 是否显示头像 */
87
+ showAvatar?: boolean
88
+ }
89
+
90
+ /** 组件 Props */
91
+ export interface BubbleListProps {
92
+ /** 消息列表 */
93
+ messages?: BubbleMessage[]
94
+ /** 角色配置 */
95
+ roleConfigs?: Record<string, BubbleRoleConfig>
96
+ /** 默认气泡位置 */
97
+ defaultPlacement?: 'start' | 'end'
98
+ /** 是否自动滚动到底部 */
99
+ autoScroll?: boolean
100
+ /** 是否开启虚拟列表(暂未实现) */
101
+ virtualList?: boolean
102
+ /** 默认打字速度(毫秒/字符) */
103
+ defaultTypingSpeed?: number
104
+ /** 是否显示角色名称(全局配置,可被 roleConfigs 中的 showRole 覆盖) */
105
+ showRole?: boolean
106
+ /** 是否显示头像(全局配置,可被 roleConfigs 中的 showAvatar 覆盖) */
107
+ showAvatar?: boolean
108
+ /** 全局动画配置(可被单条消息的 animation 配置覆盖) */
109
+ animation?: MessageAnimationConfig
110
+ }