@blueking/chat-x 0.0.50 → 0.0.51-beta.1

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 (54) hide show
  1. package/dist/ag-ui/types/contents.d.ts +2 -0
  2. package/dist/ag-ui/types/messages.d.ts +5 -0
  3. package/dist/common/constants.d.ts +1 -1
  4. package/dist/components/ai-buttons/file-upload-btn/file-upload-btn.vue.d.ts +0 -2
  5. package/dist/components/chat-content/file-content/file-content.vue.d.ts +5 -2
  6. package/dist/components/chat-content/file-content/upload-file-item.vue.d.ts +12 -0
  7. package/dist/components/chat-content/file-content/upload-image-item.vue.d.ts +21 -0
  8. package/dist/components/chat-input/ai-slash-editor/ai-slash-editor.vue.d.ts +1 -1
  9. package/dist/components/chat-input/build-default-placeholder.d.ts +7 -0
  10. package/dist/components/chat-input/chat-input.vue.d.ts +1 -1
  11. package/dist/components/chat-message/message-container/message-container.vue.d.ts +1 -0
  12. package/dist/components/chat-message/message-render/message-render.vue.d.ts +2 -0
  13. package/dist/components/chat-message/user-message/user-message.vue.d.ts +3 -1
  14. package/dist/components/index.d.ts +2 -1
  15. package/dist/components/message-tools/message-time/format-message-time.d.ts +8 -0
  16. package/dist/components/message-tools/message-time/message-time.vue.d.ts +8 -0
  17. package/dist/components/message-tools/message-tools.vue.d.ts +11 -1
  18. package/dist/composables/use-custom-tab.d.ts +5 -3
  19. package/dist/composables/use-global-config.d.ts +3 -0
  20. package/dist/composables/use-message-group.d.ts +150 -72
  21. package/dist/icons/execution.d.ts +6 -0
  22. package/dist/icons/tools.d.ts +3 -0
  23. package/dist/index.css +1 -1
  24. package/dist/index.js +3124 -2837
  25. package/dist/index.js.map +1 -1
  26. package/dist/lang/lang.d.ts +8 -7
  27. package/dist/mcp/generated/docs/ai-slash-input.md +2 -0
  28. package/dist/mcp/generated/docs/assistant-message.md +9 -7
  29. package/dist/mcp/generated/docs/chat-container.md +38 -32
  30. package/dist/mcp/generated/docs/chat-input.md +18 -12
  31. package/dist/mcp/generated/docs/cite-content.md +3 -3
  32. package/dist/mcp/generated/docs/desc-panel.md +32 -10
  33. package/dist/mcp/generated/docs/execution-summary.md +3 -3
  34. package/dist/mcp/generated/docs/file-artifact-panel.md +6 -4
  35. package/dist/mcp/generated/docs/file-content.md +89 -73
  36. package/dist/mcp/generated/docs/file-upload-btn.md +16 -18
  37. package/dist/mcp/generated/docs/message-container.md +3 -0
  38. package/dist/mcp/generated/docs/message-render.md +2 -1
  39. package/dist/mcp/generated/docs/message-time.md +180 -0
  40. package/dist/mcp/generated/docs/message-tools.md +47 -12
  41. package/dist/mcp/generated/docs/messages.md +9 -0
  42. package/dist/mcp/generated/docs/toolcall-render.md +82 -43
  43. package/dist/mcp/generated/docs/use-artifact-preview.md +19 -17
  44. package/dist/mcp/generated/docs/use-custom-tab.md +12 -8
  45. package/dist/mcp/generated/docs/use-global-config.md +15 -5
  46. package/dist/mcp/generated/docs/user-message.md +9 -0
  47. package/dist/mcp/generated/docs/user-question-card.md +2 -0
  48. package/dist/mcp/generated/index.json +46 -6
  49. package/dist/types/input.d.ts +6 -0
  50. package/dist/utils/file.d.ts +7 -1
  51. package/dist/utils/index.d.ts +2 -0
  52. package/dist/utils/merge-tools-by-id.d.ts +6 -0
  53. package/dist/utils/upload-file.d.ts +35 -0
  54. package/package.json +1 -1
@@ -19,26 +19,34 @@
19
19
 
20
20
  > **能力域**:媒体文件
21
21
 
22
- 文件列表展示组件,支持图片缩略图预览、点击图片全屏预览(`ImagePreview`)、文档卡片展示(文件名/扩展名/文件大小)、图片加载失败占位和删除操作。
22
+ 文件列表展示组件,支持图片缩略图预览、点击图片全屏预览(`ImagePreview`)、文件卡片展示(类型图标 / 文件名 / 文件大小)、图片加载失败占位和删除操作。
23
+
24
+ 内部由两个子组件承载单项渲染:
25
+
26
+ | 子组件 | 源码位置 | 职责 |
27
+ | ------------------ | ------------------------------------------------------------------- | --------------------------------------------- |
28
+ | `UploadImageItem` | `src/components/chat-content/file-content/upload-image-item.vue` | 图片缩略图、加载失败占位、hover 删除徽标 |
29
+ | `UploadFileItem` | `src/components/chat-content/file-content/upload-file-item.vue` | 180px 文件卡片(`FileIcon` + 文件名 + 大小) |
23
30
 
24
31
  ## 渲染决策逻辑
25
32
 
26
- 每个文件按以下优先级决定渲染方式:
33
+ 先分组、再渲染。设计稿要求**图片始终排在文件前方**,两类各自成行:
27
34
 
28
35
  ```
29
- file.url 存在?
30
- ├── 是 → 图片模式(用 file.url 作为 <img src>,点击可全屏预览)
31
- 图片加载失败 错误占位(粉色背景 + 红色边框 + 灰色图标)
32
- └── 检查 mimeType file.file?.type
33
- ├── 'image/' 开头 图片模式(用 getFilePreviewUrl(file.file) 作为 src,点击可全屏预览)
34
- └── 其他 → 文档卡片模式(图标 + 文件名 + 扩展名 + 大小)
36
+ splitUploadFiles(files) // 单次遍历
37
+ ├── 图片组(.ai-files-content-row.is-images)
38
+ 判定依据:mimeType file.file?.type 'image/' 开头
39
+ │ src:file.url 优先,否则用本地 File blob URL(按 key 缓存,移除 / 卸载时 revoke)
40
+ │ 加载失败 错误占位(粉色背景 + 红色边框 + 灰色图标),且不进入预览列表
41
+ └── 文件组(.ai-files-content-row.is-files)
42
+ 文件卡片:类型图标(FileIcon,按文件名解析扩展名)+ 文件名 + 文件大小
35
43
  ```
36
44
 
37
- > **注意**:`file.url` 存在时**无论文件 MIME 类型是什么**都会走图片模式。若要将 PDF 等非图片文件显示为文档卡片,确保不设置 `url` 字段(或设为 `undefined`)。
45
+ > **是否为图片只看 MIME,不看 `url`。** 解除上传类型限制后,任意文件上传成功都会拿到 `url`,若按 `url` 判断会把 PDF / DOC 渲染成破图。因此 `url` 只决定 `<img src>` 从哪里来,不参与图片判定。
38
46
 
39
- ## 基础用法(文档文件)
47
+ ## 基础用法(文件卡片)
40
48
 
41
- 无 `url` 字段、MIME 类型非 `image/*` 的文件,渲染为文档卡片(文档图标 + 文件名 + 扩展名 + 大小):
49
+ MIME 类型非 `image/*` 的文件,渲染为固定宽 180px 的文件卡片(类型图标 + 文件名 + 大小)。类型图标与文件产物侧栏共用 `FileIcon` 的扩展名映射,`pdf` / `py` / `docx` 等各有专属图标,未登记的扩展名回退兜底图标:
42
50
 
43
51
  ```vue
44
52
  <template>
@@ -64,11 +72,11 @@ file.url 存在?
64
72
  </script>
65
73
  ```
66
74
 
67
- **渲染效果**(悬停文件卡片,右上角出现删除按钮)
75
+ **渲染效果**(悬停文件卡片,底色加深并在右上角出现删除徽标)
68
76
 
69
77
  ## 图片文件预览
70
78
 
71
- 设置了 `url` 字段时,渲染为 48×48 的图片缩略图(`cursor: zoom-in`)。点击图片可打开全屏预览(内部集成 `ImagePreview` 组件),支持缩放、旋转、下载等操作:
79
+ MIME `image/*` 时渲染为图片缩略图(`cursor: zoom-in`)。点击图片可打开全屏预览(内部集成 `ImagePreview` 组件),支持缩放、旋转、下载等操作:
72
80
 
73
81
  ```vue
74
82
  <script setup lang="ts">
@@ -77,9 +85,9 @@ file.url 存在?
77
85
 
78
86
  const imageFiles = ref<Partial<UploadFile>[]>([
79
87
  {
80
- url: 'https://example.com/cat.jpg', // 有 url → 图片模式
88
+ url: 'https://example.com/cat.jpg',
81
89
  filename: 'cat.jpg',
82
- mimeType: 'image/jpeg',
90
+ mimeType: 'image/jpeg', // 图片判定依据
83
91
  file: new File([''], 'cat.jpg', { type: 'image/jpeg' }),
84
92
  },
85
93
  {
@@ -120,9 +128,9 @@ file.url 存在?
120
128
 
121
129
  `<img>` 触发 `onerror` 时,切换为粉色背景 + 红色边框的错误占位:
122
130
 
123
- ## 混合文件(图片 + 文档)
131
+ ## 混合文件(图片 + 文件)
124
132
 
125
- 同一列表中可同时包含图片和文档文件,横向 flex 布局、换行排列:
133
+ 同一列表可同时包含图片和其他文件,组件内部自动把图片排在前一行、文件排在后一行,各行内部横向排列并按需换行:
126
134
 
127
135
  ```vue
128
136
  <script setup lang="ts">
@@ -148,19 +156,20 @@ file.url 存在?
148
156
 
149
157
  ## 仅有 filename(无 File 对象)
150
158
 
151
- 从服务端恢复的历史文件没有 `File` 对象时,仍可渲染文档卡片,但**文件大小不显示**,扩展名从 `filename` `mimeType` 推断:
159
+ 从服务端恢复的历史文件没有 `File` 对象时仍可渲染文件卡片,类型图标从 `filename` 推断。文件大小取 `size` 字段;未下发 `size` 时大小节点不渲染:
152
160
 
153
161
  ```vue
154
162
  <script setup lang="ts">
155
163
  const remoteFiles = [
156
- // 无 file 对象,文件大小显示为空
164
+ // 无 file 对象,未带 size,文件大小不渲染
157
165
  { filename: 'server-report.pdf', mimeType: 'application/pdf' },
158
- { filename: 'config.json', mimeType: 'application/json' },
166
+ // size 时正常显示「1.00M」
167
+ { filename: 'config.json', mimeType: 'application/json', size: 1024 * 1024 },
159
168
  ];
160
169
  </script>
161
170
  ```
162
171
 
163
- **渲染效果**(大小区域为空)
172
+ **渲染效果**(未带 `size` 的项无大小行)
164
173
 
165
174
  ## 在 ChatInput 中使用
166
175
 
@@ -199,10 +208,20 @@ file.url 存在?
199
208
 
200
209
  ### Props
201
210
 
202
- | 属性名 | 类型 | 默认值 | 必填 | 说明 |
203
- | -------- | ----------------------- | ------- | ---- | ------------------------------- |
204
- | files | `Partial<UploadFile>[]` | - | ✅ | 文件列表 |
205
- | readonly | `boolean` | `false` | - | 只读模式,`true` 时隐藏删除按钮 |
211
+ | 属性名 | 类型 | 默认值 | 必填 | 说明 |
212
+ | -------- | ----------------------- | --------- | ---- | ------------------------------------------- |
213
+ | files | `Partial<UploadFile>[]` | - | ✅ | 文件列表 |
214
+ | readonly | `boolean` | `false` | - | 只读模式,`true` 时隐藏删除徽标与 hover 态 |
215
+ | variant | `'input' \| 'message'` | `'input'` | - | 展示形态,见下方「展示形态」 |
216
+
217
+ ### 展示形态(variant)
218
+
219
+ 只影响图片缩略图的圆角描边与整体对齐,文件卡片两种形态一致。图片尺寸规则两种形态相同:**定高 48px,宽度按原图比例,并在 48~120px 之间夹取**(竖图不至于过窄,长图不会撑破容器),超出部分由 `object-fit: cover` 裁切。
220
+
221
+ | variant | 使用场景 | 图片圆角 / 描边 | 对齐 |
222
+ | ----------- | ------------------ | --------------- | ------ |
223
+ | `'input'` | 输入框内待发送态 | 8px / `#f0f1f5` | 左对齐 |
224
+ | `'message'` | 用户消息内已发送态 | 4px / `#eaebf0` | 右对齐 |
206
225
 
207
226
  ### Events
208
227
 
@@ -214,19 +233,21 @@ file.url 存在?
214
233
 
215
234
  ### 图片模式
216
235
 
217
- | 条件 | 图片 src | 点击行为 |
218
- | ---------------------------------- | -------------------------------- | -------------- |
219
- | `file.url` 有值(优先) | `file.url` | 打开全屏预览 |
220
- | MIME `image/` 开头(无 url 时) | `URL.createObjectURL(file.file)` | 打开全屏预览 |
221
- | 图片加载失败 | 错误占位 | 不进入预览列表 |
236
+ | 条件 | 图片 src | 点击行为 |
237
+ | ------------------- | ------------------------------------------------- | -------------- |
238
+ | `file.url` 有值 | `file.url` | 打开全屏预览 |
239
+ | url、有 `File` | `URL.createObjectURL(file.file)`(按 key 缓存) | 打开全屏预览 |
240
+ | 图片加载失败 | 错误占位 | 不进入预览列表 |
241
+
242
+ blob URL 按附件 key 缓存,同一文件重复渲染不会重复创建;文件被移除或组件卸载时统一 `revokeObjectURL`。
222
243
 
223
- ### 文档卡片模式
244
+ ### 文件卡片模式
224
245
 
225
- | 字段 | 取值优先级 |
226
- | -------- | ------------------------------------------------------------------------------------------------------------ |
227
- | 文件名 | `file.filename` → `file.file?.name` |
228
- | 扩展名 | `file.file`:取文件名最后一段 `.xxx` 或 MIME 后缀<br>无 `file.file`:取 `filename` 后缀 → `mimeType` 后缀 |
229
- | 文件大小 | 仅当有 `file.file`(`File` 对象)时显示,否则为空 |
246
+ | 字段 | 取值优先级 |
247
+ | -------- | --------------------------------------------- |
248
+ | 文件名 | `file.filename` → `file.file?.name` |
249
+ | 类型图标 | 由文件名解析扩展名,交给 `FileIcon` 映射 |
250
+ | 文件大小 | `file.file?.size` → `file.size`;都没有则不渲染 |
230
251
 
231
252
  ## 类型定义
232
253
 
@@ -249,48 +270,43 @@ type UploadFile = BinaryInputContent & {
249
270
  // 二进制内容基础类型
250
271
  interface BinaryInputContent {
251
272
  type: 'binary';
252
- url?: string; // 文件访问地址,存在时强制走图片模式
253
- filename?: string; // 文件名(用于文档卡片)
254
- mimeType?: string; // MIME 类型(用于图片判断和扩展名推断)
273
+ url?: string; // 文件访问地址,只决定 <img src> 来源,不参与图片判定
274
+ filename?: string; // 文件名(文件卡片展示 + 类型图标解析)
275
+ mimeType?: string; // MIME 类型(图片判定依据)
276
+ size?: number; // 文件字节数,发送时由原始 File 写入
255
277
  }
256
278
  ```
257
279
 
258
- ## 工具函数(内部使用)
259
-
260
- ```typescript
261
- // 判断是否走图片模式
262
- // ⚠️ file.url 存在时直接返回 true,不判断 MIME 类型
263
- const isImage = (file: Partial<UploadFile>): boolean => {
264
- if (file.url) return true;
265
- return isImageFile(file.mimeType || file.file?.type);
266
- };
267
-
268
- // 判断 MIME 类型是否为图片
269
- const isImageFile = (mimeType?: string): boolean => {
270
- if (!mimeType) return false;
271
- return mimeType.startsWith('image/');
272
- };
273
-
274
- // 获取 File 对象的临时预览 URL(无 url 时的图片模式备选)
275
- const getFilePreviewUrl = (file?: File): string => {
276
- if (!file) return '';
277
- return URL.createObjectURL(file);
278
- };
280
+ ## 工具函数(`src/utils/upload-file.ts`)
279
281
 
280
- // 获取文件扩展名
281
- const getFileExtension = (file?: File): string => {
282
- if (!file) return '';
283
- return file.name.split('.').pop() || file.type?.split('/').pop() || '';
284
- };
282
+ 组件内的取值与分组逻辑都收敛在这里,`ChatInput`、`UserMessage` 共用同一套判定:
285
283
 
286
- // 格式化文件大小(需要 File 对象)
287
- const formatFileSize = (file?: File): string => {
288
- if (!file) return '';
289
- const size = file.size;
290
- const units = ['B', 'KB', 'M', 'GB'];
291
- const index = Math.floor(Math.log2(size || 1) / 10);
292
- return `${(size / Math.pow(1024, index)).toFixed(2)} ${units[index]}`;
293
- };
284
+ ```typescript
285
+ import {
286
+ getFileIdentity,
287
+ getUploadFileKey,
288
+ getUploadFileName,
289
+ getUploadFileSize,
290
+ isUploadImageFile,
291
+ splitUploadFiles,
292
+ } from '@blueking/chat-x';
293
+
294
+ // File 身份:文件名 + 大小 + 修改时间,用于去重与列表 key
295
+ getFileIdentity(file); // 'report.pdf_2048_1700000000000'
296
+
297
+ // 附件稳定 key:待发送态用 File 身份(上传成功回填 url 后不变),已发送态退回 url / 文件名
298
+ getUploadFileKey({ file }); // 'report.pdf_2048_1700000000000'
299
+ getUploadFileKey({ url: 'https://x/a.pdf' }); // 'https://x/a.pdf'
300
+
301
+ // 是否按图片渲染:只看 MIME,有 url 也不例外
302
+ isUploadImageFile({ mimeType: 'application/pdf', url: 'https://x/a.pdf' }); // false
303
+
304
+ // 文件名 / 字节数取值优先级
305
+ getUploadFileName({ filename: 'remote.pdf', file }); // 'remote.pdf'
306
+ getUploadFileSize({ size: 2048 }); // 2048
307
+
308
+ // 单次遍历分出图片组与其他文件组(图片在前)
309
+ splitUploadFiles(files); // { imageFiles, otherFiles }
294
310
  ```
295
311
 
296
312
  ## 使用场景
@@ -19,16 +19,18 @@
19
19
 
20
20
  > **能力域**:输入交互
21
21
 
22
- 聊天输入框内置的文件上传触发按钮,点击后弹出系统文件选择框。内部包含隐藏的 `<input type="file">` 与可见的图标按钮;在按钮层对**单文件**做大小与空文件过滤,**已选文件个数上限**由上层(如 `ChatInput`)统一校验并提示,避免按钮与输入区各弹一条错误提示。
22
+ 聊天输入框内置的文件上传触发按钮,点击后弹出系统文件选择框。内部包含隐藏的 `<input type="file">` 与可见的图标按钮;**不限制文件类型**,在按钮层只对**单文件**做大小与空文件过滤,**已选文件个数上限**由上层(如 `ChatInput`)统一校验并提示,避免按钮与输入区各弹一条错误提示。
23
23
 
24
24
  ## 组件结构
25
25
 
26
26
  ```
27
27
  .ai-file-upload-btn(display: flex,align-items: center)
28
28
  ├── input[type="file"](.file-upload-btn-input,display: none,multiple,:accept)
29
+ │ accept 缺省时不下发该属性,系统选择框不过滤任何类型
29
30
  │ 触发后走 handleFileInputChange → 校验 → emit upload → target.value = ''
30
- └── span.ai-shortcut-btn.file-upload-btn-icon(24×24px,color: #979ba5hover: cursor: pointer
31
- v-tippy: "上传图片, 最多支持上传 3 个, 最大支持 2.4MB"(theme: ai-chat-box,offset: [0, 16],可通过 tippyOptions 扩展)
31
+ └── span.ai-shortcut-btn.file-upload-btn-icon(热区 32×32px / 圆角 8px;图标字号跟随 --ai-icon-size-sm:small=16px、normal=18px;color: #979ba5hover: #f0f1f5
32
+ v-tippy: "上传文件,最多支持 {count} 个,单个最大 {size}MB"
33
+ ({count} / {size} 由 MAX_UPLOAD_FILES 与 MAX_UPLOAD_FILE_SIZE 运行时填充,theme: ai-chat-box,offset: [0, 16],可通过 tippyOptions 扩展)
32
34
  @click → fileInputRef.click()
33
35
  └── <slot> 默认:FileUploadIcon
34
36
  ```
@@ -61,7 +63,7 @@
61
63
 
62
64
  > `multiple` prop 声明存在但当前模板中 `input` 的 `multiple` 属性为**硬编码**(非 `:multiple="multiple"` 绑定),始终允许多选,该 prop 暂时无实际效果。
63
65
 
64
- > **`maxFiles` prop**:当前版本在按钮内**不参与截断或计数校验**,仅作类型与文档预留;列表最多几个文件由上层(如 `ChatInput` 的 `MAX_UPLOAD_FILES`)控制。详见 [ChatInput 文件上传](/components/input/chat-input#file-upload)。
66
+ > **文件类型不做限制**:组件不再默认 `accept="image/*"`,任意类型文件都可选择。若业务需要收窄,显式传入 `accept`。个数上限由上层(如 `ChatInput` 的 `MAX_UPLOAD_FILES`)控制,详见 [ChatInput 文件上传](/components/input/chat-input#file-upload)。
65
67
 
66
68
  ## 基础用法
67
69
 
@@ -84,11 +86,14 @@
84
86
 
85
87
  ## 限制文件类型
86
88
 
87
- 通过 `accept` 属性控制系统文件选择框的过滤条件,遵循 `<input type="file">` 的 `accept` 规范:
89
+ 默认**不限制**文件类型。需要收窄时通过 `accept` 属性控制系统文件选择框的过滤条件,遵循 `<input type="file">` 的 `accept` 规范:
88
90
 
89
91
  ```vue
90
92
  <template>
91
- <!-- 仅图片(默认) -->
93
+ <!-- 不限制类型(默认,不下发 accept) -->
94
+ <FileUploadBtn @upload="handleUpload" />
95
+
96
+ <!-- 仅图片 -->
92
97
  <FileUploadBtn
93
98
  accept="image/*"
94
99
  @upload="handleUpload"
@@ -99,12 +104,6 @@
99
104
  accept=".pdf,.doc,.docx,.xlsx,.pptx"
100
105
  @upload="handleUpload"
101
106
  />
102
-
103
- <!-- 不限制类型 -->
104
- <FileUploadBtn
105
- accept="*/*"
106
- @upload="handleUpload"
107
- />
108
107
  </template>
109
108
  ```
110
109
 
@@ -126,12 +125,11 @@
126
125
 
127
126
  ### Props
128
127
 
129
- | 属性名 | 类型 | 默认值 | 说明 |
130
- | ------------ | -------------- | ----------- | -------------------------------------------------------------------- |
131
- | accept | `string` | `'image/*'` | 文件选择框过滤类型,遵循 `<input accept>` 规范 |
132
- | maxFiles | `number` | `3` | 预留字段,**当前不在按钮内生效**;个数上限见 `ChatInput` 与全局常量 |
133
- | multiple | `boolean` | `true` | 声明属性(当前版本未实际绑定到 input,始终多选) |
134
- | tippyOptions | `AITippyProps` | — | 扩展 tooltip 配置,会与内置配置合并 |
128
+ | 属性名 | 类型 | 默认值 | 说明 |
129
+ | ------------ | -------------- | ------ | -------------------------------------------------------------------------- |
130
+ | accept | `string` | | 文件选择框过滤类型,遵循 `<input accept>` 规范;缺省时不下发,不限制类型 |
131
+ | multiple | `boolean` | `true` | 声明属性(当前版本未实际绑定到 input,始终多选) |
132
+ | tippyOptions | `AITippyProps` | | 扩展 tooltip 配置,会与内置配置合并 |
135
133
 
136
134
  ### Events
137
135
 
@@ -124,6 +124,7 @@
124
124
  - `renderMode === RenderMode.Share`(分享预览模式)
125
125
  - 消息组的 `pause` 为 `true`(来源于 `message.property?.extra?.pause`)
126
126
  - 多选模式(`enableSelection`)开启且消息组不是 Loading 类型
127
+ - AI 消息组的时间通过 `MessageTools` 的 `#append` 插槽渲染在工具图标右侧,取值为组内**最后一条**带 `createdAt` 的消息(即本轮回答完成时间);组内 `reasoning` / `activity` 等子消息不单独展示时间,全组都没有 `createdAt` 时不展示
127
128
  - `renderMode === RenderMode.Test` 时,工具栏会过滤掉「分享」按钮,其余正常
128
129
  - `renderMode === RenderMode.Share` 时,`message-group-messages` 自动添加 `message-group-enabled-selection` 类名(与 `enableSelection: true` 一致的多选视觉效果)
129
130
  - Loading 消息组的 `type` 是 `MessageRole.Loading`,不显示工具栏和多选 Checkbox
@@ -567,6 +568,7 @@ AI 回复状态为 `error` 时,消息以错误样式展示:
567
568
  | messageStatus | `MessageStatus` | — | 当前整体消息状态,控制底部「停止生成」按钮显示;`ChatContainer` 会结合末尾 Loading 占位推导 `fetching` 等再传入 |
568
569
  | messageTools | `IToolBtn[]` | — | AI 消息左侧工具(复制/引用等)的自定义配置;按 `id` 与内置 `CONST_MESSAGE_TOOLS` 合并(覆盖同 id、追加新 id、`hidden` 过滤),详见「自定义消息工具栏」 |
569
570
  | updateTools | `IToolBtn[]` | — | AI 消息右侧反馈工具(点赞/踩/删除等)的自定义配置;按 `id` 与内置 `CONST_UPDATE_TOOLS` 合并,规则同上 |
571
+ | userMessageTools | `IToolBtn[]` | — | 自定义用户消息工具组,透传给 `MessageRender` → `UserMessage`;按 id 与 `CONST_USER_MESSAGE_TOOLS` 合并,`{ id, hidden: true }` 可隐藏 |
570
572
  | messageToolsStatus | `MessageToolsStatus` | — | 工具栏状态,透传给 `MessageTools` 和 `MessageRender` |
571
573
  | messageToolsTippyOptions | `AITippyProps` | — | 透传给 `MessageTools` 和 `MessageRender`(进而透传给 `UserMessage` 的工具栏)的 Tippy 配置,用于自定义 tooltip 挂载点、位置等(如 `appendTo`、`placement`、`zIndex`) |
572
574
  | enableSelection | `boolean` | `false` | 是否启用多选模式 |
@@ -641,6 +643,7 @@ enum MessageToolsStatus {
641
643
  ## 关联组件
642
644
 
643
645
  - [MessageRender](/components/message/message-render) — 按组渲染每条消息时委托使用
646
+ - [MessageTime](/components/feedback/message-time) — AI 消息组工具栏右侧的时间
644
647
  - [InterruptMessage 中断消息](/components/agent/interrupt-message) — `role: 'interrupt'` 的渲染与 `onInterruptResume` 透传
645
648
  - [ChatInput](/components/input/chat-input) — 常与输入区组合构成完整对话界面
646
649
  - [LoadingMessage](/components/message/loading-message) — 末尾为用户消息时自动追加加载组
@@ -281,6 +281,7 @@ h(ContentRender, { content: message.content || '', status: message.status }, /*
281
281
  | 属性名 | 类型 | 默认值 | 说明 |
282
282
  | ------------------ | -------------------------------------------------------------------------- | ------ | ------------------------------------------------- |
283
283
  | message | `Partial<Message>` | — | **必填**,消息对象,`role` 字段决定渲染哪个子组件 |
284
+ | messageTools | `IToolBtn[]` | — | 自定义用户消息工具组;**仅转发给 `UserMessage`** |
284
285
  | messageToolsStatus | `MessageToolsStatus` | — | 工具按钮状态;**仅转发给 `UserMessage`** |
285
286
  | onAction | `(tool: IToolBtn) => Promise<string[] \| void>` | — | 工具操作回调;**仅转发给 `UserMessage`** |
286
287
  | onInputConfirm | `(content: UserMessage['content'], docSchema: TagSchema) => Promise<void>` | — | 用户编辑确认回调;**仅转发给 `UserMessage`** |
@@ -300,7 +301,7 @@ h(ContentRender, { content: message.content || '', status: message.status }, /*
300
301
 
301
302
  | `MessageRole` | 渲染组件 | prop 路由 | 说明 |
302
303
  | ------------- | ------------------ | ------------------------------------------------------------------------------------------------------- | --------------------- |
303
- | `user` | `UserMessage` | `message` + `onAction` + `onInputConfirm` + `onShortcutConfirm` + `messageToolsStatus` + `tippyOptions` | 用户发送的消息 |
304
+ | `user` | `UserMessage` | `message` + `onAction` + `onInputConfirm` + `onShortcutConfirm` + `messageTools` + `messageToolsStatus` + `tippyOptions` | 用户发送的消息 |
304
305
  | `assistant` | `AssistantMessage` | `message` + `default slot` | AI 助手回复消息 |
305
306
  | `info` | `InfoMessage` | `message` | 系统信息 / 会话分隔符 |
306
307
  | `reasoning` | `ReasoningMessage` | `message` | AI 思考过程(可折叠) |
@@ -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` 全局配置