@blueking/chat-x 0.0.49-beta.1 → 0.0.49-beta.11

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 (61) hide show
  1. package/dist/ag-ui/types/constants.d.ts +1 -0
  2. package/dist/ag-ui/types/file.d.ts +1 -11
  3. package/dist/ag-ui/types/messages.d.ts +2 -0
  4. package/dist/components/chat-message/assistant-message/assistant-message.vue.d.ts +12 -1
  5. package/dist/components/chat-message/assistant-message/message-artifacts/artifact-file-card.vue.d.ts +0 -2
  6. package/dist/components/chat-message/assistant-message/message-artifacts/artifact-preview/artifact-preview-host.vue.d.ts +6 -3
  7. package/dist/components/chat-message/assistant-message/message-artifacts/artifact-preview/preview-strategy.d.ts +4 -5
  8. package/dist/components/chat-message/assistant-message/message-artifacts/artifact-preview/renderers/code-preview.vue.d.ts +8 -0
  9. package/dist/components/chat-message/assistant-message/message-artifacts/artifact-preview/renderers/image-preview.vue.d.ts +7 -0
  10. package/dist/components/chat-message/assistant-message/message-artifacts/artifact-preview/use-artifact-preview-loader.d.ts +1 -0
  11. package/dist/components/chat-message/assistant-message/message-artifacts/file-artifact-panel.vue.d.ts +1 -0
  12. package/dist/components/chat-message/assistant-message/message-artifacts/message-artifacts.vue.d.ts +0 -1
  13. package/dist/components/chat-message/interrupt-message/user-question/use-user-question.d.ts +7 -2
  14. package/dist/components/chat-message/user-message/user-message.vue.d.ts +15 -2
  15. package/dist/components/file-icon/file-icon.vue.d.ts +7 -0
  16. package/dist/components/index.d.ts +3 -1
  17. package/dist/components/message-tools/message-time/format-message-time.d.ts +8 -0
  18. package/dist/components/message-tools/message-time/message-time.vue.d.ts +8 -0
  19. package/dist/components/message-tools/message-tools.vue.d.ts +11 -1
  20. package/dist/composables/use-artifact-preview.d.ts +8 -18
  21. package/dist/composables/use-container-scroll.d.ts +9 -2
  22. package/dist/composables/use-custom-tab.d.ts +7 -3
  23. package/dist/composables/use-global-config.d.ts +3 -0
  24. package/dist/composables/use-message-group.d.ts +145 -74
  25. package/dist/icons/file-icons.d.ts +4 -0
  26. package/dist/icons/file.d.ts +0 -18
  27. package/dist/icons/index.d.ts +1 -0
  28. package/dist/icons/tools.d.ts +3 -0
  29. package/dist/index.css +1 -1
  30. package/dist/index.js +2881 -2525
  31. package/dist/index.js.map +1 -1
  32. package/dist/lang/lang.d.ts +4 -1
  33. package/dist/mcp/generated/docs/assistant-message.md +28 -20
  34. package/dist/mcp/generated/docs/chat-container.md +36 -31
  35. package/dist/mcp/generated/docs/chat-input.md +2 -10
  36. package/dist/mcp/generated/docs/constants.md +3 -1
  37. package/dist/mcp/generated/docs/content-render.md +2 -10
  38. package/dist/mcp/generated/docs/execution-summary.md +3 -3
  39. package/dist/mcp/generated/docs/file-artifact-panel.md +60 -46
  40. package/dist/mcp/generated/docs/file-icon.md +111 -0
  41. package/dist/mcp/generated/docs/info-message.md +6 -5
  42. package/dist/mcp/generated/docs/interrupt.md +1 -0
  43. package/dist/mcp/generated/docs/message-container.md +27 -23
  44. package/dist/mcp/generated/docs/message-render.md +4 -27
  45. package/dist/mcp/generated/docs/message-time.md +180 -0
  46. package/dist/mcp/generated/docs/message-tools.md +47 -12
  47. package/dist/mcp/generated/docs/messages.md +9 -0
  48. package/dist/mcp/generated/docs/reasoning-message.md +2 -9
  49. package/dist/mcp/generated/docs/toolcall-render.md +18 -10
  50. package/dist/mcp/generated/docs/use-artifact-preview.md +52 -55
  51. package/dist/mcp/generated/docs/use-container-scroll.md +6 -2
  52. package/dist/mcp/generated/docs/use-custom-tab.md +25 -8
  53. package/dist/mcp/generated/docs/use-global-config.md +15 -5
  54. package/dist/mcp/generated/docs/use-message-group.md +21 -13
  55. package/dist/mcp/generated/docs/user-message.md +6 -0
  56. package/dist/mcp/generated/docs/user-question-card.md +6 -3
  57. package/dist/mcp/generated/index.json +68 -12
  58. package/dist/utils/file-type.d.ts +14 -0
  59. package/dist/utils/index.d.ts +1 -0
  60. package/package.json +2 -2
  61. package/dist/components/chat-message/assistant-message/message-artifacts/file-icon.d.ts +0 -29
@@ -0,0 +1,180 @@
1
+ <!-- AI SUMMARY -->
2
+ ## 快速了解
3
+
4
+ 按 createdAt 展示消息时间,四档格式:今天 `12:00`、昨天 `昨天 12:00`、今年内更早 `3-12 12:00`、非今年 `2025-3-12 12:00`; 时区取 props.timezone,未传时回退 injectGlobalConfig().timezone(由 ChatContainer 的 timezone prop 注入),都没有则用浏览器时区; 无值或非法时间不渲染任何 DOM。通常通过 MessageTools 的 prepend / append 插槽使用。 源码位置:src/components/message-tools/message-time/message-time.vue。
5
+
6
+ ### 关联组件
7
+ - **message-tools** — 通过 prepend / append 插槽嵌入工具栏
8
+ - **user-message** — 用户消息在工具栏左侧展示时间
9
+ - **message-container** — AI 消息组在工具栏右侧展示本轮回答时间
10
+
11
+ ---
12
+ <!-- FULL DOC -->
13
+
14
+ # MessageTime 消息时间
15
+
16
+ ## 源码事实
17
+
18
+ - **源码位置**:`src/components/message-tools/message-time/message-time.vue`
19
+ - **格式化工具**:`src/components/message-tools/message-time/format-message-time.ts`
20
+ - **能力域**:工具与反馈
21
+ - **能力说明**:按「今天 / 昨天 / 今年内 / 跨年」四档格式展示消息创建时间。
22
+
23
+ > **能力域**:工具与反馈
24
+
25
+ 展示单条消息(或一组 AI 回答)的创建时间。组件本身只负责格式化与渲染一段文本,**位置由使用方决定**——项目内通过 `MessageTools` 的 `prepend` / `append` 插槽嵌入工具栏。
26
+
27
+ ## 格式规则
28
+
29
+ 时间按与「今天」的日历日差值分四档,同一档内时分固定 `HH:mm`(24 小时制,补零),月日**不补零**:
30
+
31
+ | 档位 | 判定条件 | 输出示例 |
32
+ | ------------ | ---------------------- | --------------- |
33
+ | 今天 | 与今天同一日历日 | `12:00` |
34
+ | 昨天 | 与今天相差 1 个日历日 | `昨天 12:00` |
35
+ | 今年内更早 | 相差 ≥ 2 天且年份相同 | `3-12 12:00` |
36
+ | 非今年 | 年份不同 | `2025-3-12 12:00` |
37
+
38
+ - 分档与展示取**同一时区**的日历日:既避免「昨天 23:59」与「今天 00:01」因毫秒差不足一天被判成同一天,也避免按浏览器时区分档、按配置时区显示时分导致的错位
39
+ - `昨天` 走 `t('昨天')` 国际化,英文环境输出 `Yesterday 12:00`
40
+
41
+ ## 基础用法
42
+
43
+ `createdAt` 接受 ISO 字符串或毫秒时间戳:
44
+
45
+ ```vue
46
+ <template>
47
+ <MessageTime :created-at="message.createdAt" />
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { MessageTime } from '@blueking/chat-x';
52
+
53
+ const message = {
54
+ id: '1',
55
+ messageId: '1',
56
+ role: 'user',
57
+ content: '你好',
58
+ status: 'completed',
59
+ createdAt: '2026-08-17T04:00:00.000Z',
60
+ };
61
+ </script>
62
+ ```
63
+
64
+ > **无值不渲染**:`createdAt` 为空、为空字符串或无法解析成合法时间时,组件不渲染任何 DOM(`v-if`),使用方无需额外判空。
65
+
66
+ ## 时区配置
67
+
68
+ 时区按以下优先级取值,均未配置时使用**浏览器时区**:
69
+
70
+ ```
71
+ props.timezone
72
+ └─ 未传 → injectGlobalConfig()?.timezone(由 ChatContainer 的 timezone prop 注入)
73
+ └─ 未配置 → 浏览器时区
74
+ ```
75
+
76
+ ```vue
77
+ <template>
78
+ <!-- 整个会话统一按北京时间展示 -->
79
+ <ChatContainer
80
+ :messages="messages"
81
+ timezone="Asia/Shanghai"
82
+ />
83
+ </template>
84
+ ```
85
+
86
+ ```vue
87
+ <template>
88
+ <!-- 单个实例覆盖全局配置 -->
89
+ <MessageTime
90
+ :created-at="message.createdAt"
91
+ timezone="UTC"
92
+ />
93
+ </template>
94
+ ```
95
+
96
+ - 取值为 [IANA 时区名](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)(如 `Asia/Shanghai`、`UTC`、`America/New_York`)
97
+ - 传入非法时区名时回退到浏览器时区,不会导致渲染失败
98
+ - 同一时区的 `Intl.DateTimeFormat` 实例内部有缓存,长会话中不会为每条消息重复构造
99
+
100
+ ## 在消息工具栏中的使用
101
+
102
+ `MessageTools` 提供 `prepend`(工具图标左侧)与 `append`(工具图标右侧)两个插槽,项目内的时间位置即由此决定:
103
+
104
+ | 场景 | 插槽 | 时间取值 |
105
+ | ------------ | --------- | -------------------------------------------- |
106
+ | 用户消息 | `prepend` | 该条消息的 `createdAt` |
107
+ | AI 消息组 | `append` | 组内**最后一条**带 `createdAt` 的消息,即本轮回答完成时间 |
108
+
109
+ ```vue
110
+ <template>
111
+ <MessageTools :on-action="handleAction">
112
+ <template #append>
113
+ <MessageTime :created-at="createdAt" />
114
+ </template>
115
+ </MessageTools>
116
+ </template>
117
+
118
+ <script setup lang="ts">
119
+ import { MessageTime, MessageTools } from '@blueking/chat-x';
120
+ </script>
121
+ ```
122
+
123
+ > 组内 `reasoning` / `activity` 等子消息不单独展示时间,一个 AI 回答组只显示一次。
124
+
125
+ ## API
126
+
127
+ ### Props
128
+
129
+ | 属性名 | 类型 | 必填 | 默认值 | 说明 |
130
+ | --------- | ------------------ | ---- | ------ | -------------------------------------------------------------------- |
131
+ | createdAt | `number \| string` | 否 | — | 消息创建时间,ISO 字符串或毫秒时间戳;无值或非法时不渲染 |
132
+ | timezone | `string` | 否 | — | IANA 时区名;优先于全局配置,两者都未配置时按浏览器时区展示 |
133
+
134
+ ### Events / Slots / Expose
135
+
136
+ 无。
137
+
138
+ ### 全局配置依赖
139
+
140
+ 通过 `injectGlobalConfig()` 读取 `timezone`。祖先需已调用 `useGlobalConfig()`(通常由 `ChatContainer` 的 `timezone` prop 注册);无 Provider 时按浏览器时区展示。
141
+
142
+ ## 类型定义
143
+
144
+ ```typescript
145
+ export type MessageTimeProps = {
146
+ createdAt?: number | string;
147
+ timezone?: string;
148
+ };
149
+
150
+ // 独立可用的格式化函数(组件内部使用,未从包入口导出)
151
+ declare const formatMessageTime: (createdAt?: number | string, timezone?: string) => string;
152
+ ```
153
+
154
+ ## 样式说明
155
+
156
+ ```scss
157
+ .ai-message-time {
158
+ flex: none;
159
+ font-size: var(--ai-font-size, 12px);
160
+ line-height: 16px;
161
+ color: $color-text-secondary;
162
+ white-space: nowrap;
163
+ }
164
+ ```
165
+
166
+ - 字号跟随 `--ai-font-size`(`ChatContainer` 的 `size` 档位),颜色使用次要文本语义色
167
+ - `flex: none` + `nowrap` 保证在工具栏 flex 布局中不被压缩换行
168
+
169
+ ## 注意事项
170
+
171
+ 1. **时间来源**:`createdAt` 由消息层(`chat-helper`)写入 `BaseMessage`,组件不参与取数
172
+ 2. **不做相对时间**:不提供「几分钟前」这类相对描述,四档格式固定
173
+ 3. **空值语义**:不渲染而非渲染占位,工具栏侧的 `prepend` / `append` 包裹容器会因 `:empty` 收起,不留多余间距
174
+
175
+ ## 关联组件
176
+
177
+ - [MessageTools](/components/feedback/message-tools) — 通过 `prepend` / `append` 插槽嵌入
178
+ - [UserMessage](/components/message/user-message) — 用户消息时间位置
179
+ - [MessageContainer](/components/setup/message-container) — AI 消息组时间位置
180
+ - [useGlobalConfig](/composables/use-global-config) — `timezone` 全局配置
@@ -1,12 +1,13 @@
1
1
  <!-- AI SUMMARY -->
2
2
  ## 快速了解
3
3
 
4
- 消息悬浮工具栏,组合复制、删除、反馈等工具按钮。 源码位置:src/components/message-tools/message-tools.vue。
4
+ 消息悬浮工具栏,组合复制、删除、反馈等工具按钮;提供 prepend / append 两端插槽承载消息时间等附加内容。 源码位置:src/components/message-tools/message-tools.vue。
5
5
 
6
6
  ### 关联组件
7
7
  - **tool-btn** — 普通工具项由 ToolBtn 渲染
8
8
  - **user-feedback** — like/unlike 时弹出反馈表单
9
9
  - **delete-tool** — id 为 delete 时替换为带确认的删除按钮
10
+ - **message-time** — 通过 prepend / append 插槽嵌入消息时间
10
11
 
11
12
  ---
12
13
  <!-- FULL DOC -->
@@ -20,22 +21,22 @@
20
21
 
21
22
  > **能力域**:工具与反馈
22
23
 
23
- AI 消息的操作工具栏组件,由**左侧消息工具区**和**右侧更新工具区**两部分组成,中间以分隔线分隔。仅 `like` / `unlike` 按钮会弹出反馈表单(`UserFeedback`),其余按钮直接触发 `onAction`。
24
+ AI 消息的操作工具栏组件,由**左侧消息工具区**和**右侧更新工具区**两部分组成,中间以分隔线分隔;两端另有 `prepend` / `append` 插槽用于挂载消息时间等附加内容。仅 `like` / `unlike` 按钮会弹出反馈表单(`UserFeedback`),其余按钮直接触发 `onAction`。
24
25
 
25
26
  ## 组件结构
26
27
 
27
28
  ```
28
- ┌─────────────────────────────────────────────────────┐
29
- │ .ai-message-tools-container │
30
- │ ┌─────────────────────┐ │ ┌───────────────────┐ │
31
- │ │ messageTools │ │ │ updateTools │ │
32
- │ │ copy cite rebuild… │ │ │ like unlike del │ │
33
- │ └─────────────────────┘ │ └───────────────────┘ │
34
- │ 左侧区域 分隔线 右侧区域 │
35
- └─────────────────────────────────────────────────────┘
29
+ ┌───────────────────────────────────────────────────────────────────────┐
30
+ │ .ai-message-tools-container(display: flex,gap: 4px) │
31
+ │ ┌────────┐ ┌─────────────────────┐ │ ┌───────────────┐ ┌───────┐ │
32
+ │ │#prepend│ │ messageTools │ │ │ updateTools │ │#append│ │
33
+ │ │ │ │ copy cite rebuild… │ │ │ like unlike… │ │ │ │
34
+ │ └────────┘ └─────────────────────┘ │ └───────────────┘ └───────┘ │
35
+ │ 插槽区 左侧区域 分隔线 右侧区域 插槽区 │
36
+ └───────────────────────────────────────────────────────────────────────┘
36
37
  ```
37
38
 
38
- - 分隔线(`.ai-divider`)仅在 `updateTools` 非空时显示
39
+ - 分隔线(`.ai-divider`)与**右侧更新工具区**均仅在 `updateTools` 非空时渲染
39
40
  - `updateTools` 中 `id` 为 `like` / `unlike` 的按钮被 `Tippy` 弹窗包裹,点击后展示 `UserFeedback` 反馈表单
40
41
  - `updateTools` 中 `id` 为 `delete` 的按钮使用 `DeleteTool` 组件,点击后展示**确认删除弹窗**(含"删除"/"取消"按钮),确认后触发 `onAction`
41
42
  - `updateTools` 中其他按钮直接触发 `onAction`,不弹表单
@@ -224,6 +225,32 @@ const CONST_UPDATE_TOOLS = [
224
225
 
225
226
  **渲染效果**
226
227
 
228
+ ## 两端插槽(prepend / append)
229
+
230
+ `prepend`(工具图标左侧)与 `append`(工具图标右侧)用于在工具栏两端挂载附加内容,项目内用于放置 [MessageTime 消息时间](/components/feedback/message-time):
231
+
232
+ | 场景 | 使用插槽 | 效果 |
233
+ | --------- | --------- | --------------------------------------- |
234
+ | 用户消息 | `prepend` | 时间显示在工具图标左侧 |
235
+ | AI 消息组 | `append` | 时间显示在工具图标右侧 |
236
+
237
+ ```vue
238
+ <template>
239
+ <MessageTools :on-action="handleAction">
240
+ <template #append>
241
+ <MessageTime :created-at="createdAt" />
242
+ </template>
243
+ </MessageTools>
244
+ </template>
245
+
246
+ <script setup lang="ts">
247
+ import { MessageTime, MessageTools } from '@blueking/chat-x';
248
+ </script>
249
+ ```
250
+
251
+ - 插槽包裹容器(`.ai-message-tools-prepend` / `.ai-message-tools-append`)仅在对应插槽传入时渲染
252
+ - 插槽内容为空时(如消息无 `createdAt`,`MessageTime` 不渲染任何 DOM),包裹容器命中 `:empty` 被收起,不会留下多余间距
253
+
227
254
  ## 工具栏状态控制
228
255
 
229
256
  `messageToolsStatus` 控制工具栏整体状态:
@@ -357,6 +384,13 @@ const CONST_UPDATE_TOOLS = [
357
384
  | -------- | ------------------------------------------------------------- | ------------------------------------------ |
358
385
  | feedback | `(tool: IToolBtn, reasonList: string[], otherReason: string)` | 用户在反馈表单点击"提交"后触发(not 取消) |
359
386
 
387
+ ### Slots
388
+
389
+ | 插槽名 | 作用域参数 | 说明 |
390
+ | ------- | ---------- | ------------------------------------------------ |
391
+ | prepend | — | 工具图标左侧的附加内容,如用户消息的时间 |
392
+ | append | — | 工具图标右侧的附加内容,如 AI 消息组的时间 |
393
+
360
394
  ## 类型定义
361
395
 
362
396
  ```typescript
@@ -382,4 +416,5 @@ enum MessageToolsStatus {
382
416
 
383
417
  - [ToolBtn](/components/feedback/tool-btn) — 单项工具按钮
384
418
  - [UserFeedback](/components/feedback/user-feedback) — 点赞/踩反馈面板
385
- - [DeleteTool](/components/feedback/delete-tool) — 删除二次确认
419
+ - [DeleteTool](/components/feedback/delete-tool) — 删除二次确认
420
+ - [MessageTime](/components/feedback/message-time) — 两端插槽内的消息时间
@@ -54,6 +54,10 @@ interface BaseMessage<T extends MessageType, C = string> {
54
54
  // 消息名称(可选)
55
55
  name?: string;
56
56
 
57
+ // 可选:消息创建时间,ISO 字符串或毫秒时间戳,由消息层(chat-helper)写入
58
+ // 用于 MessageTime 展示;缺省时不展示时间
59
+ createdAt?: number | string;
60
+
57
61
  // 消息属性(可选)
58
62
  property?: {
59
63
  extra?: {
@@ -144,6 +148,9 @@ enum MessageStatus {
144
148
  // 已完成
145
149
  Complete = 'complete',
146
150
 
151
+ // 已完成(与 Complete 同义,兼容协议/后端返回的 completed)
152
+ Completed = 'completed',
153
+
147
154
  // 已禁用
148
155
  Disabled = 'disabled',
149
156
 
@@ -170,6 +177,8 @@ enum MessageStatus {
170
177
  }
171
178
  ```
172
179
 
180
+ 完整取值与说明见 [常量枚举 · MessageStatus](./constants#messagestatus)。
181
+
173
182
  ## 具体消息类型
174
183
 
175
184
  ### UserMessage
@@ -224,17 +224,10 @@ interface ReasoningMessage {
224
224
  duration?: number; // 推理耗时(毫秒)
225
225
  name?: string;
226
226
  }
227
-
228
- enum MessageStatus {
229
- Pending = 'pending',
230
- Streaming = 'streaming',
231
- Complete = 'complete',
232
- Success = 'success',
233
- Error = 'error',
234
- Stop = 'stop',
235
- }
236
227
  ```
237
228
 
229
+ > `MessageStatus` 完整取值见 [常量枚举](../../types/constants)。本组件完成态识别 `complete` / `success`(与标题文案表一致)。
230
+
238
231
  ## 关联组件
239
232
 
240
233
  - [MessageRender](/components/message/message-render) — reasoning 角色由其实例化
@@ -31,7 +31,9 @@
31
31
  │ ├── "调用工具:" / "调用 MCP:"(有 mcpName 时)
32
32
  │ ├── .toolcall-header-title(工具名,overflow-tips 溢出截断)
33
33
  │ └── .toolcall-status-title
34
- │ ├── Loading(仅 pending / streaming 时显示)
34
+ │ ├── Loading(pending / streaming)
35
+ │ ├── BkFlowSuccessIcon(success / complete / completed)
36
+ │ ├── BkFlowFailedIcon(error)
35
37
  │ ├── 状态文案(调用中 / 调用成功 / 调用失败)
36
38
  │ └── .toolcall-duration(耗时,如 "(1.2s)")
37
39
  │
@@ -76,16 +78,16 @@
76
78
 
77
79
  ## 调用状态
78
80
 
79
- `status` prop 同时控制头部的 CSS class(`toolcall-status-{status}`)、背景/边框颜色、状态文案和 Loading 动画:
81
+ `status` prop 同时控制头部的 CSS class(`toolcall-status-{status}`)、背景/边框颜色、状态文案和状态图标:
80
82
 
81
- | `status` | 状态文案 | 背景色 | 边框色 | Loading |
82
- | ----------------------- | -------- | ----------------- | --------- | ------- |
83
- | `pending` / `streaming` | 调用中 | `#fafbfd` | `#dcdee5` | ✓ |
84
- | `complete` / `success` | 调用成功 | `#ebfaf0` | `#a1e3ba` | - |
85
- | `error` | 调用失败 | `#fff0f0` | `#f8b4b4` | - |
86
- | 其他 / `undefined` | 调用中 | —(无匹配 class) | — | - |
83
+ | `status` | 状态文案 | 背景色 | 边框色 | 状态图标 |
84
+ | ------------------------------------- | -------- | ----------------- | --------- | ------------------- |
85
+ | `pending` / `streaming` | 调用中 | `#fafbfd` | `#dcdee5` | `Loading` |
86
+ | `complete` / `completed` / `success` | 调用成功 | `#ebfaf0` | `#a1e3ba` | `BkFlowSuccessIcon` |
87
+ | `error` | 调用失败 | `#fff0f0` | `#f8b4b4` | `BkFlowFailedIcon` |
88
+ | 其他 / `undefined` | 调用中 | —(无匹配 class) | — | — |
87
89
 
88
- > **说明**:`statusTitle` 的 `switch` 语句中 `default` 与 `case Pending` 共享同一返回值,`streaming` 和未知 status 均命中 `default` 分支,显示"调用中"。Loading 动画由 `v-if="status === 'pending' || status === 'streaming'"` 单独控制。
90
+ > **说明**:`statusTitle` 将 `Completed`(`completed`)与 `Complete` / `Success` 一并视为成功;主题 `$toolcallStatusMap` 同步提供 `completed` 色值。`default` 与 `case Pending` 共享「调用中」文案,`streaming` 与未知 status 命中 `default`。状态图标互斥:`pending` / `streaming` 显示 `Loading`;`success` / `complete` / `completed` 显示 `BkFlowSuccessIcon`;`error` 显示 `BkFlowFailedIcon`。
89
91
 
90
92
  **三种状态对比**
91
93
 
@@ -244,7 +246,13 @@ const assistantMessage = {
244
246
  v-for="toolCall in assistantMessage.toolCalls"
245
247
  :key="toolCall.id"
246
248
  :tool-call="toolCall"
247
- :status="toolCall.toolMessage?.status ?? MessageStatus.Pending"
249
+ :status="
250
+ !toolCall.toolMessage
251
+ ? MessageStatus.Pending
252
+ : toolCall.toolMessage.error
253
+ ? MessageStatus.Error
254
+ : (toolCall.toolMessage.status ?? assistantMessage.status)
255
+ "
248
256
  />
249
257
  </template>
250
258
  ```
@@ -1,7 +1,7 @@
1
1
  <!-- AI SUMMARY -->
2
2
  ## 快速了解
3
3
 
4
- useArtifactPreviewProvider 维护 activeArtifactId,openPreview 命中文件并触发 onOpen 打开侧栏 Tab; 并通过 getOnArtifactClick 封装 resolveArtifactUrls(按 outputId 缓存 download_url / preview_url); useArtifactPreviewConsumer 在后代注入同一套 API。buildArtifactId 用 messageUid#index#outputId 生成唯一 id。 正文加载与分类型渲染不在本 composable,由 FileArtifactPanel 内 ArtifactPreviewHost 完成。 FILE_ARTIFACT_TAB_NAME 标识固定「文件产物」Tab。
4
+ useArtifactPreviewProvider 维护 activeArtifactId(值为 outputId),openPreview 命中文件并触发 onOpen 打开侧栏 Tab; 并通过 getOnArtifactClick 封装 resolveArtifactUrls(每次重新取链,并发去重); useArtifactPreviewConsumer 在后代注入同一套 API。SessionArtifact 即 AIFileInfo,会话内以 outputId 为唯一键。 正文加载与分类型渲染不在本 composable,由 FileArtifactPanel 内 ArtifactPreviewHost 完成(重载键 outputId:type)。 FILE_ARTIFACT_TAB_NAME 标识固定「文件产物」Tab。
5
5
 
6
6
  ### 关联组件
7
7
  - **chat-container** — Provider 主场景,聚合 sessionArtifacts 并挂载 FileArtifactPanel
@@ -15,12 +15,12 @@ useArtifactPreviewProvider 维护 activeArtifactId,openPreview 命中文件并
15
15
 
16
16
  > **分类**:composable
17
17
 
18
- Provider/Consumer 模式的文件产物预览状态管理。Provider 在 `ChatContainer` 中创建,负责维护当前命中的文件 id;Consumer 在深层 `ArtifactFileCard` 中注入,用于点击卡片触发预览。
18
+ Provider/Consumer 模式的文件产物预览状态管理。Provider 在 `ChatContainer` 中创建,负责维护当前命中的文件 `outputId`;Consumer 在深层 `ArtifactFileCard` 中注入,用于点击卡片触发预览。
19
19
 
20
20
  **职责边界**:
21
21
 
22
- - **本 composable**:维护「命中文件」与「URL 解析缓存」;打开侧栏 Tab(`addCustomTab`)由容器通过 `onOpen` 注入
23
- - **不在本 composable**:聚合会话文件列表、渲染预览面板、按类型 fetch 正文 / iframe 展示 —— 分别由 `useMessageGroup.sessionArtifacts`、`FileArtifactPanel`、内部 `ArtifactPreviewHost` + `useArtifactPreviewLoader` 承担
22
+ - **本 composable**:维护「命中文件」与「URL 解析」(每次重新取链 + 并发去重);打开侧栏 Tab(`addCustomTab`)由容器通过 `onOpen` 注入
23
+ - **不在本 composable**:聚合会话文件列表、渲染预览面板、按类型 fetch 正文 / iframe 展示 —— 分别由 `useMessageGroup.sessionArtifacts`、`FileArtifactPanel`、内部 `ArtifactPreviewHost` + `useArtifactPreviewLoader` 承担(预览重载键为 `outputId:type`)
24
24
 
25
25
  ## 函数签名
26
26
 
@@ -31,7 +31,7 @@ function useArtifactPreviewProvider(options: {
31
31
  /** 读取业务侧异步取链回调(getter 保持对 props 变更敏感) */
32
32
  getOnArtifactClick?: () => OnArtifactClick | undefined;
33
33
  /** 命中文件后触发:由容器负责 addCustomTab + 展开侧栏 + 选中 Tab */
34
- onOpen: (artifactId: string) => void;
34
+ onOpen: (outputId: string) => void;
35
35
  }): {
36
36
  activeArtifactId: ShallowRef<string>;
37
37
  canResolveArtifactUrl: ComputedRef<boolean>;
@@ -55,13 +55,6 @@ function useArtifactPreviewConsumer():
55
55
  };
56
56
  ```
57
57
 
58
- ### buildArtifactId
59
-
60
- ```typescript
61
- function buildArtifactId(messageUid: string, index: number, outputId: string): string;
62
- // => `${messageUid}#${index}#${outputId}`
63
- ```
64
-
65
58
  ## 使用示例
66
59
 
67
60
  ### Provider(ChatContainer)
@@ -77,30 +70,39 @@ import {
77
70
  } from '@blueking/chat-x';
78
71
  import { t } from '@blueking/chat-x/lang';
79
72
 
80
- const { addCustomTab, removeCustomTab } = useCustomTabProvider({ /* ... */ });
73
+ const { addCustomTab, ensureCustomTab, removeCustomTab } = useCustomTabProvider({ /* ... */ });
81
74
 
82
75
  // 会话级文件产物聚合已内聚在 useMessageGroup,直接消费
83
76
  const { sessionArtifacts } = useMessageGroup({ keyword, messages, selectedUserMessages });
84
77
 
78
+ const FILE_ARTIFACT_TAB = {
79
+ closable: false,
80
+ label: t('文件产物'),
81
+ name: FILE_ARTIFACT_TAB_NAME,
82
+ order: -1,
83
+ };
84
+
85
+ // 常驻挂载:不随产物有无增删,无产物时由面板展示空态
86
+ ensureCustomTab(FILE_ARTIFACT_TAB);
87
+
85
88
  const { activeArtifactId, setActiveArtifactId } = useArtifactPreviewProvider({
86
89
  getOnArtifactClick: () => props.onArtifactClick,
90
+ // 点击文件卡片:展开侧栏并选中「文件产物」Tab
87
91
  onOpen: () => {
88
- addCustomTab({
89
- closable: false,
90
- label: t('文件产物'),
91
- name: FILE_ARTIFACT_TAB_NAME,
92
- order: -1,
93
- });
92
+ addCustomTab(FILE_ARTIFACT_TAB);
94
93
  },
95
94
  });
96
95
 
97
- // 无文件产物时清理 Tab 与命中态
96
+ // 仅维护命中态:无产物清空,命中项失效时回落到第一个
98
97
  watch(sessionArtifacts, list => {
99
98
  if (!list.length) {
100
- removeCustomTab(FILE_ARTIFACT_TAB_NAME);
101
99
  setActiveArtifactId('');
100
+ return;
102
101
  }
103
- });
102
+ if (!list.some(item => item.outputId === activeArtifactId.value)) {
103
+ setActiveArtifactId(list[0].outputId);
104
+ }
105
+ }, { immediate: true });
104
106
  ```
105
107
 
106
108
  ### Consumer(ArtifactFileCard)
@@ -118,18 +120,14 @@ const handleCardClick = () => {
118
120
  props.onPreview(props.file);
119
121
  return;
120
122
  }
121
- artifactPreview?.openPreview({
122
- file: props.file,
123
- index: props.index ?? 0,
124
- messageUid: props.messageUid ?? '',
125
- });
123
+ artifactPreview?.openPreview({ file: props.file });
126
124
  };
127
125
  ```
128
126
 
129
127
  ### 侧栏列表内切换命中文件
130
128
 
131
129
  ```typescript
132
- // FileArtifactPanel 列表点击 → emit select → 容器调用 setActiveArtifactId
130
+ // FileArtifactPanel 列表点击 → emit select(outputId) → 容器调用 setActiveArtifactId
133
131
  // 右侧预览由面板内 ArtifactPreviewHost 消费 activeArtifact,自行取链并按类型渲染
134
132
  <FileArtifactPanel
135
133
  :active-id="activeArtifactId"
@@ -163,32 +161,27 @@ const onArtifactClick = async (file: AIFileInfo) => {
163
161
 
164
162
  | 属性/方法名 | 类型 | 说明 |
165
163
  | ------------------- | ----------------------------------------- | -------------------------------------------------------------------- |
166
- | activeArtifactId | `ShallowRef<string>` | 当前命中的文件 id(`messageUid#index#outputId`) |
164
+ | activeArtifactId | `ShallowRef<string>` | 当前命中的文件 `outputId` |
167
165
  | canResolveArtifactUrl | `ComputedRef<boolean>` | 是否具备异步取链能力(有 `onArtifactClick` 时为 true,下载按钮据此显隐) |
168
- | openPreview | `(payload: OpenArtifactPreviewPayload) => void` | 由文件卡片触发:计算 id、更新命中态、调用 `onOpen` |
169
- | resolveArtifactUrls | `(file: AIFileInfo) => Promise<ArtifactUrlResult>` | 调用 `onArtifactClick` 并按 `outputId` 缓存结果;并发请求去重 |
170
- | setActiveArtifactId | `(id: string) => void` | 直接设置命中文件 id;侧栏列表内切换选中时使用 |
166
+ | openPreview | `(payload: OpenArtifactPreviewPayload) => void` | 由文件卡片触发:以 `file.outputId` 更新命中态、调用 `onOpen` |
167
+ | resolveArtifactUrls | `(file: AIFileInfo) => Promise<ArtifactUrlResult>` | 调用 `onArtifactClick` 取链;每次重新获取,不缓存;同文件并发去重 |
168
+ | setActiveArtifactId | `(id: string) => void` | 直接设置命中文件 `outputId`;侧栏列表内切换选中时使用 |
171
169
 
172
170
  ## 类型定义
173
171
 
174
172
  ```typescript
175
173
  import type { AIFileInfo, ArtifactUrlResult, OnArtifactClick } from '@blueking/chat-x';
176
174
 
177
- /** 打开预览时的入参:文件 + 消息内下标 + 所属消息 uid */
175
+ /** 打开预览时的入参 */
178
176
  type OpenArtifactPreviewPayload = {
179
177
  file: AIFileInfo;
180
- index: number;
181
- messageUid: string;
182
178
  };
183
179
 
184
180
  /**
185
- * 会话级文件产物:在 AIFileInfo 基础上补充命中所需的唯一 id 与所属消息。
186
- * 同一会话可能出现多个 AssistantMessage + 同名文件,文件名不可作唯一键。
181
+ * 会话级文件产物:以 outputId 为会话内唯一键(同 outputId 视为同一文件)。
182
+ * 拍平去重后即为 AIFileInfo,此处用别名标明语义。
187
183
  */
188
- type SessionArtifact = AIFileInfo & {
189
- artifactId: string;
190
- messageUid: string;
191
- };
184
+ type SessionArtifact = AIFileInfo;
192
185
 
193
186
  /** onArtifactClick 返回值(snake_case) */
194
187
  type ArtifactUrlResult = {
@@ -197,28 +190,31 @@ type ArtifactUrlResult = {
197
190
  };
198
191
  ```
199
192
 
200
- ## 唯一 id 规则
201
-
202
- 同一会话可能存在多个 `AssistantMessage`,且不同消息里可能有同名文件,因此**文件名不可作为唯一键**。Provider 侧聚合与 Consumer 侧透传必须使用同一 `buildArtifactId` 规则:
193
+ ## 唯一键规则
203
194
 
204
- ```typescript
205
- buildArtifactId('msg-a', 2, 'output-9'); // => 'msg-a#2#output-9'
206
- ```
195
+ 会话内以 **`outputId`** 作为文件产物唯一键:
207
196
 
208
- - `messageUid`:所属 `AssistantMessage` 的 `uid`(回退 `String(id)`)
209
- - `index`:文件在所属消息 `property.artifacts` 数组中的下标
210
- - `outputId`:文件自身的 `AIFileInfo.outputId`
197
+ - 同一 `outputId` 在多条 `AssistantMessage` 中出现时,`sessionArtifacts` 去重并保留**最后一次**出现的文件信息
198
+ - `activeArtifactId`、列表 `:key`、`select` 事件参数均使用 `outputId`
199
+ - 文件名可能重复,**不可**作为唯一键
211
200
 
212
201
  ## 完整触发链路
213
202
 
214
203
  ```
215
204
  ArtifactFileCard(点击)
216
- └─ useArtifactPreviewConsumer().openPreview({ file, index, messageUid })
205
+ └─ useArtifactPreviewConsumer().openPreview({ file })
217
206
  └─ useArtifactPreviewProvider(ChatContainer)
218
- ├─ activeArtifactId = buildArtifactId(...)
219
- └─ onOpen(artifactId) → addCustomTab(FILE_ARTIFACT_TAB_NAME)
207
+ ├─ activeArtifactId = file.outputId
208
+ └─ onOpen(outputId) → addCustomTab(FILE_ARTIFACT_TAB_NAME) 展开并选中
220
209
  └─ FileArtifactPanel(列表 + 下载头,@select → setActiveArtifactId)
221
210
  └─ ArtifactPreviewHost(loader + 分类型 renderer)
211
+
212
+ 容器初始化
213
+ └─ ensureCustomTab(FILE_ARTIFACT_TAB_NAME) 常驻挂上(不展开侧栏);因 order:-1 排在首位,
214
+ 未主动切换过 Tab 时会成为默认选中面板;无产物时由面板展示整块空态
215
+
216
+ sessionArtifacts 变化
217
+ └─ 仅维护命中态(无产物清空,命中项失效回落第一个);不增删 Tab
222
218
  ```
223
219
 
224
220
  分类型预览策略见 [FileArtifactPanel 预览机制](../components/message/file-artifact-panel#预览机制)。
@@ -228,10 +224,11 @@ ArtifactFileCard(点击)
228
224
  - **职责单一**:composable 不直接调用 `useCustomTab`,侧栏 Tab 打开逻辑由 `onOpen` 注入;也不做正文 fetch / iframe 渲染
229
225
  - **ShallowRef 优先**:`activeArtifactId` 使用 `shallowRef`,避免不必要的深层响应式开销
230
226
  - **Consumer 兜底**:`useArtifactPreviewConsumer` 无 Provider 时返回 `undefined`,文件卡片在无容器上下文时自动不可点击
231
- - **与 useCustomTab 协作**:「文件产物」Tab 通过 `addCustomTab` 按需添加(`order: -1`、`closable: false`),会话无文件产物时由容器 `removeCustomTab` 清理
227
+ - **与 useCustomTab 协作**:点击卡片走 `addCustomTab`(展开 + 选中);容器初始化时走 `ensureCustomTab` 常驻挂载(不展开;因 `order: -1` 在未主动切换前会成为默认选中),无产物也不移除,由面板展示空态
232
228
 
233
229
  ## 关联组件
234
230
 
235
231
  - [ChatContainer](../components/setup/chat-container) — Provider 主场景,内置「文件产物」Tab
236
232
  - [FileArtifactPanel](../components/message/file-artifact-panel) — 侧栏列表与预览 Host 挂载
237
- - [AssistantMessage](../components/message/assistant-message) — 文件产物来源(`property.artifacts`)
233
+ - [AssistantMessage](../components/message/assistant-message) — 文件产物来源(`property.artifacts`)
234
+ - [useCustomTab](./use-custom-tab) — `addCustomTab` / `ensureCustomTab` 分工
@@ -1,7 +1,7 @@
1
1
  <!-- AI SUMMARY -->
2
2
  ## 快速了解
3
3
 
4
- useContainerScrollProvider 在滚动容器与底部锚点上绑定 IntersectionObserver、scroll、wheel,提供 isScrollBottom、scrollBottomHeight、autoScrollEnabled、toScrollBottom/toScrollTop 及防抖「返回底部」按钮状态。 useContainerScrollConsumer 通过 inject 在子组件中获取同一套控制,无需 props 透传。 典型用于流式输出时仅在用户位于底部时自动滚底。MessageContainer 与 ScrollBtn 配合使用。
4
+ useContainerScrollProvider 在滚动容器与底部锚点上绑定 IntersectionObserver、scroll、wheel,提供 isScrollBottom、scrollBottomHeight、autoScrollEnabled、jumpToBottom、toScrollBottom/toScrollTop 及防抖「返回底部」按钮状态。 toScrollBottom 缺省按距底部距离自动选择行为:超过 INSTANT_SCROLL_DISTANCE(600px)时瞬时贴底,否则平滑滚动,避免切换会话时出现长距离平滑滚动动画。 useContainerScrollConsumer 通过 inject 在子组件中获取同一套控制,无需 props 透传。 典型用于流式输出时仅在用户位于底部时自动滚底。MessageContainer 与 ScrollBtn 配合使用。
5
5
 
6
6
  ### 关联组件
7
7
  - **message-container** — Provider 挂载于消息列表滚动区域
@@ -35,7 +35,10 @@ useContainerScrollProvider(containerRef, bottomRef)
35
35
  ├── wheel 事件(passive)→ deltaY < 0 时 autoScrollEnabled=false
36
36
  │ (用户向上滚动时暂停自动滚动)
37
37
  │
38
- ├── toScrollBottom() → autoScrollEnabled=true + bottomRef.scrollIntoView('smooth')
38
+ ├── jumpToBottom() → autoScrollEnabled=true + container.scrollTop = scrollHeight(瞬时)
39
+ ├── toScrollBottom(behavior?) → autoScrollEnabled=true;
40
+ │ behavior 缺省时:距底部 > INSTANT_SCROLL_DISTANCE(600) → jumpToBottom()
41
+ │ 否则 / 显式 'smooth' → bottomRef.scrollIntoView({ behavior:'smooth', block:'end' })
39
42
  ├── toScrollTop() → containerRef.scrollTo({ top:0, behavior:'smooth' })
40
43
  │
41
44
  └── provide(CONTAINER_SCROLL_TOKEN, computed(() => ({
@@ -43,6 +46,7 @@ useContainerScrollProvider(containerRef, bottomRef)
43
46
  isScrollBottom, // ShallowRef<boolean>(保持响应式)
44
47
  scrollBottomHeight, // ShallowRef<number>(保持响应式)
45
48
  debouncedShowScrollBottomBtn, // customRef,防抖显示返回底部按钮
49
+ jumpToBottom,
46
50
  toScrollBottom,
47
51
  toScrollTop,
48
52
  })))