@liujitcn/kratos-admin-system 0.0.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 (208) hide show
  1. package/README.md +137 -0
  2. package/dist/package/declarations/index.d.ts +1 -0
  3. package/dist/package/declarations/src/ai.d.ts +10 -0
  4. package/dist/package/declarations/src/api/base/ai_message.d.ts +27 -0
  5. package/dist/package/declarations/src/api/base/ai_session.d.ts +17 -0
  6. package/dist/package/declarations/src/api/base/ai_tool.d.ts +7 -0
  7. package/dist/package/declarations/src/api/base/oauth.d.ts +13 -0
  8. package/dist/package/declarations/src/api/base/sse.d.ts +31 -0
  9. package/dist/package/declarations/src/api/system/auth.d.ts +17 -0
  10. package/dist/package/declarations/src/api/system/base_api.d.ts +22 -0
  11. package/dist/package/declarations/src/api/system/base_area.d.ts +19 -0
  12. package/dist/package/declarations/src/api/system/base_config.d.ts +20 -0
  13. package/dist/package/declarations/src/api/system/base_dept.d.ts +21 -0
  14. package/dist/package/declarations/src/api/system/base_dict.d.ts +32 -0
  15. package/dist/package/declarations/src/api/system/base_job.d.ts +28 -0
  16. package/dist/package/declarations/src/api/system/base_log.d.ts +9 -0
  17. package/dist/package/declarations/src/api/system/base_menu.d.ts +21 -0
  18. package/dist/package/declarations/src/api/system/base_migration.d.ts +10 -0
  19. package/dist/package/declarations/src/api/system/base_post.d.ts +21 -0
  20. package/dist/package/declarations/src/api/system/base_role.d.ts +23 -0
  21. package/dist/package/declarations/src/api/system/base_tenant.d.ts +21 -0
  22. package/dist/package/declarations/src/api/system/base_user.d.ts +23 -0
  23. package/dist/package/declarations/src/api/system/code_gen.d.ts +14 -0
  24. package/dist/package/declarations/src/api/system/code_gen_column.d.ts +12 -0
  25. package/dist/package/declarations/src/api/system/code_gen_proto.d.ts +10 -0
  26. package/dist/package/declarations/src/api/system/code_gen_sse.d.ts +8 -0
  27. package/dist/package/declarations/src/api/system/code_gen_table.d.ts +18 -0
  28. package/dist/package/declarations/src/api/system/project_document.d.ts +9 -0
  29. package/dist/package/declarations/src/components/Ai.vue.d.ts +3 -0
  30. package/dist/package/declarations/src/index.d.ts +3 -0
  31. package/dist/package/declarations/src/module.d.ts +2 -0
  32. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +82 -0
  33. package/dist/package/declarations/src/rpc/base/v1/ai_session.d.ts +176 -0
  34. package/dist/package/declarations/src/rpc/base/v1/ai_tool.d.ts +59 -0
  35. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +167 -0
  36. package/dist/package/declarations/src/rpc/base/v1/sse.d.ts +13 -0
  37. package/dist/package/declarations/src/rpc/common/v1/common.d.ts +57 -0
  38. package/dist/package/declarations/src/rpc/common/v1/enum.d.ts +58 -0
  39. package/dist/package/declarations/src/rpc/common/v1/types.d.ts +60 -0
  40. package/dist/package/declarations/src/rpc/google/protobuf/empty.d.ts +11 -0
  41. package/dist/package/declarations/src/rpc/google/protobuf/timestamp.d.ts +107 -0
  42. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +156 -0
  43. package/dist/package/declarations/src/rpc/system/admin/v1/base_api.d.ts +193 -0
  44. package/dist/package/declarations/src/rpc/system/admin/v1/base_area.d.ts +82 -0
  45. package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +112 -0
  46. package/dist/package/declarations/src/rpc/system/admin/v1/base_dept.d.ts +112 -0
  47. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +215 -0
  48. package/dist/package/declarations/src/rpc/system/admin/v1/base_job.d.ts +177 -0
  49. package/dist/package/declarations/src/rpc/system/admin/v1/base_log.d.ts +87 -0
  50. package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +151 -0
  51. package/dist/package/declarations/src/rpc/system/admin/v1/base_migration.d.ts +64 -0
  52. package/dist/package/declarations/src/rpc/system/admin/v1/base_post.d.ts +112 -0
  53. package/dist/package/declarations/src/rpc/system/admin/v1/base_role.d.ts +128 -0
  54. package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant.d.ts +112 -0
  55. package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +161 -0
  56. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen.d.ts +155 -0
  57. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_column.d.ts +143 -0
  58. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_proto.d.ts +86 -0
  59. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_table.d.ts +149 -0
  60. package/dist/package/declarations/src/rpc/system/admin/v1/project_document.d.ts +64 -0
  61. package/dist/package/declarations/src/rpc/system/common/v1/enum.d.ts +67 -0
  62. package/dist/package/declarations/src/views/ai/chat/attachment.d.ts +14 -0
  63. package/dist/package/declarations/src/views/ai/chat/components/AiMarkdown.vue.d.ts +13 -0
  64. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +45 -0
  65. package/dist/package/declarations/src/views/ai/chat/components/SessionPanel.vue.d.ts +34 -0
  66. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +12 -0
  67. package/dist/package/declarations/src/views/ai/chat/index.vue.d.ts +4 -0
  68. package/dist/package/declarations/src/views/ai/chat/message.d.ts +59 -0
  69. package/dist/package/declarations/src/views/ai/chat/stream.d.ts +10 -0
  70. package/dist/package/declarations/src/views/ai/chat/types.d.ts +115 -0
  71. package/dist/package/declarations/src/views/base/api/index.vue.d.ts +3 -0
  72. package/dist/package/declarations/src/views/base/area/index.vue.d.ts +3 -0
  73. package/dist/package/declarations/src/views/base/config/index.vue.d.ts +3 -0
  74. package/dist/package/declarations/src/views/base/dept/index.vue.d.ts +3 -0
  75. package/dist/package/declarations/src/views/base/dict/index.vue.d.ts +3 -0
  76. package/dist/package/declarations/src/views/base/dict/item.vue.d.ts +3 -0
  77. package/dist/package/declarations/src/views/base/job/index.vue.d.ts +3 -0
  78. package/dist/package/declarations/src/views/base/job/log.vue.d.ts +3 -0
  79. package/dist/package/declarations/src/views/base/log/index.vue.d.ts +3 -0
  80. package/dist/package/declarations/src/views/base/menu/index.vue.d.ts +3 -0
  81. package/dist/package/declarations/src/views/base/migration/index.vue.d.ts +4 -0
  82. package/dist/package/declarations/src/views/base/post/index.vue.d.ts +3 -0
  83. package/dist/package/declarations/src/views/base/role/index.vue.d.ts +3 -0
  84. package/dist/package/declarations/src/views/base/tenant/index.vue.d.ts +3 -0
  85. package/dist/package/declarations/src/views/base/user/components/dept-tree.vue.d.ts +19 -0
  86. package/dist/package/declarations/src/views/base/user/index.vue.d.ts +3 -0
  87. package/dist/package/declarations/src/views/profile/components/base.vue.d.ts +13 -0
  88. package/dist/package/declarations/src/views/profile/components/password.vue.d.ts +3 -0
  89. package/dist/package/declarations/src/views/profile/components/security.vue.d.ts +15 -0
  90. package/dist/package/declarations/src/views/profile/index.vue.d.ts +3 -0
  91. package/dist/package/declarations/src/views/tool/api-doc/index.vue.d.ts +4 -0
  92. package/dist/package/declarations/src/views/tool/code-gen/code-preview/index.vue.d.ts +3 -0
  93. package/dist/package/declarations/src/views/tool/code-gen/columns/index.vue.d.ts +3 -0
  94. package/dist/package/declarations/src/views/tool/code-gen/columns/option-copy.d.ts +25 -0
  95. package/dist/package/declarations/src/views/tool/code-gen/components/CodeGenProgressDialog.vue.d.ts +16 -0
  96. package/dist/package/declarations/src/views/tool/code-gen/components/CodePreviewPane.vue.d.ts +9 -0
  97. package/dist/package/declarations/src/views/tool/code-gen/config.d.ts +32 -0
  98. package/dist/package/declarations/src/views/tool/code-gen/preview/capabilities.d.ts +20 -0
  99. package/dist/package/declarations/src/views/tool/code-gen/preview/data.d.ts +42 -0
  100. package/dist/package/declarations/src/views/tool/code-gen/preview/index.vue.d.ts +3 -0
  101. package/dist/package/declarations/src/views/tool/code-gen/proto/index.vue.d.ts +3 -0
  102. package/dist/package/declarations/src/views/tool/code-gen/table/index.vue.d.ts +3 -0
  103. package/dist/package/declarations/src/views/tool/project-doc/index.vue.d.ts +4 -0
  104. package/dist/package/src/ai.ts +16 -0
  105. package/dist/package/src/api/base/ai_message.ts +139 -0
  106. package/dist/package/src/api/base/ai_session.ts +77 -0
  107. package/dist/package/src/api/base/ai_tool.ts +22 -0
  108. package/dist/package/src/api/base/oauth.ts +47 -0
  109. package/dist/package/src/api/base/sse.ts +192 -0
  110. package/dist/package/src/api/system/auth.ts +65 -0
  111. package/dist/package/src/api/system/base_api.ts +95 -0
  112. package/dist/package/src/api/system/base_area.ts +73 -0
  113. package/dist/package/src/api/system/base_config.ts +82 -0
  114. package/dist/package/src/api/system/base_dept.ts +83 -0
  115. package/dist/package/src/api/system/base_dict.ts +144 -0
  116. package/dist/package/src/api/system/base_job.ts +125 -0
  117. package/dist/package/src/api/system/base_log.ts +32 -0
  118. package/dist/package/src/api/system/base_menu.ts +83 -0
  119. package/dist/package/src/api/system/base_migration.ts +35 -0
  120. package/dist/package/src/api/system/base_post.ts +83 -0
  121. package/dist/package/src/api/system/base_role.ts +93 -0
  122. package/dist/package/src/api/system/base_tenant.ts +83 -0
  123. package/dist/package/src/api/system/base_user.ts +93 -0
  124. package/dist/package/src/api/system/code_gen.ts +56 -0
  125. package/dist/package/src/api/system/code_gen_column.ts +43 -0
  126. package/dist/package/src/api/system/code_gen_proto.ts +32 -0
  127. package/dist/package/src/api/system/code_gen_sse.ts +33 -0
  128. package/dist/package/src/api/system/code_gen_table.ts +74 -0
  129. package/dist/package/src/api/system/project_document.ts +33 -0
  130. package/dist/package/src/components/Ai.vue +57 -0
  131. package/dist/package/src/index.ts +3 -0
  132. package/dist/package/src/module.ts +17 -0
  133. package/dist/package/src/rpc/base/v1/ai_message.ts +98 -0
  134. package/dist/package/src/rpc/base/v1/ai_session.ts +214 -0
  135. package/dist/package/src/rpc/base/v1/ai_tool.ts +74 -0
  136. package/dist/package/src/rpc/base/v1/oauth.ts +199 -0
  137. package/dist/package/src/rpc/base/v1/sse.ts +22 -0
  138. package/dist/package/src/rpc/common/v1/common.ts +71 -0
  139. package/dist/package/src/rpc/common/v1/enum.ts +71 -0
  140. package/dist/package/src/rpc/common/v1/types.ts +77 -0
  141. package/dist/package/src/rpc/google/protobuf/empty.ts +19 -0
  142. package/dist/package/src/rpc/google/protobuf/timestamp.ts +115 -0
  143. package/dist/package/src/rpc/system/admin/v1/auth.ts +208 -0
  144. package/dist/package/src/rpc/system/admin/v1/base_api.ts +241 -0
  145. package/dist/package/src/rpc/system/admin/v1/base_area.ts +105 -0
  146. package/dist/package/src/rpc/system/admin/v1/base_config.ts +140 -0
  147. package/dist/package/src/rpc/system/admin/v1/base_dept.ts +140 -0
  148. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +249 -0
  149. package/dist/package/src/rpc/system/admin/v1/base_job.ts +206 -0
  150. package/dist/package/src/rpc/system/admin/v1/base_log.ts +101 -0
  151. package/dist/package/src/rpc/system/admin/v1/base_menu.ts +195 -0
  152. package/dist/package/src/rpc/system/admin/v1/base_migration.ts +81 -0
  153. package/dist/package/src/rpc/system/admin/v1/base_post.ts +134 -0
  154. package/dist/package/src/rpc/system/admin/v1/base_role.ts +151 -0
  155. package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +134 -0
  156. package/dist/package/src/rpc/system/admin/v1/base_user.ts +190 -0
  157. package/dist/package/src/rpc/system/admin/v1/code_gen.ts +176 -0
  158. package/dist/package/src/rpc/system/admin/v1/code_gen_column.ts +170 -0
  159. package/dist/package/src/rpc/system/admin/v1/code_gen_proto.ts +104 -0
  160. package/dist/package/src/rpc/system/admin/v1/code_gen_table.ts +179 -0
  161. package/dist/package/src/rpc/system/admin/v1/project_document.ts +79 -0
  162. package/dist/package/src/rpc/system/common/v1/enum.ts +80 -0
  163. package/dist/package/src/typings/swagger-ui-dist.d.ts +28 -0
  164. package/dist/package/src/typings/x-markdown-vue.d.ts +1 -0
  165. package/dist/package/src/views/ai/chat/attachment.ts +65 -0
  166. package/dist/package/src/views/ai/chat/components/AiMarkdown.vue +162 -0
  167. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +1861 -0
  168. package/dist/package/src/views/ai/chat/components/SessionPanel.vue +316 -0
  169. package/dist/package/src/views/ai/chat/components/XSender.vue +554 -0
  170. package/dist/package/src/views/ai/chat/index.vue +848 -0
  171. package/dist/package/src/views/ai/chat/message.ts +416 -0
  172. package/dist/package/src/views/ai/chat/stream.ts +115 -0
  173. package/dist/package/src/views/ai/chat/types.ts +126 -0
  174. package/dist/package/src/views/base/api/index.vue +455 -0
  175. package/dist/package/src/views/base/area/index.vue +269 -0
  176. package/dist/package/src/views/base/config/index.vue +530 -0
  177. package/dist/package/src/views/base/dept/index.vue +461 -0
  178. package/dist/package/src/views/base/dict/index.vue +304 -0
  179. package/dist/package/src/views/base/dict/item.vue +373 -0
  180. package/dist/package/src/views/base/job/index.vue +535 -0
  181. package/dist/package/src/views/base/job/log.vue +202 -0
  182. package/dist/package/src/views/base/log/index.vue +317 -0
  183. package/dist/package/src/views/base/menu/index.vue +944 -0
  184. package/dist/package/src/views/base/migration/index.vue +791 -0
  185. package/dist/package/src/views/base/post/index.vue +324 -0
  186. package/dist/package/src/views/base/role/index.vue +684 -0
  187. package/dist/package/src/views/base/tenant/index.vue +328 -0
  188. package/dist/package/src/views/base/user/components/dept-tree.vue +75 -0
  189. package/dist/package/src/views/base/user/index.vue +783 -0
  190. package/dist/package/src/views/profile/components/base.vue +320 -0
  191. package/dist/package/src/views/profile/components/password.vue +269 -0
  192. package/dist/package/src/views/profile/components/security.vue +443 -0
  193. package/dist/package/src/views/profile/index.vue +199 -0
  194. package/dist/package/src/views/tool/api-doc/index.vue +275 -0
  195. package/dist/package/src/views/tool/code-gen/code-preview/index.vue +233 -0
  196. package/dist/package/src/views/tool/code-gen/columns/index.vue +1456 -0
  197. package/dist/package/src/views/tool/code-gen/columns/option-copy.ts +125 -0
  198. package/dist/package/src/views/tool/code-gen/components/CodeGenProgressDialog.vue +328 -0
  199. package/dist/package/src/views/tool/code-gen/components/CodePreviewPane.vue +42 -0
  200. package/dist/package/src/views/tool/code-gen/config.ts +175 -0
  201. package/dist/package/src/views/tool/code-gen/preview/capabilities.ts +34 -0
  202. package/dist/package/src/views/tool/code-gen/preview/data.ts +331 -0
  203. package/dist/package/src/views/tool/code-gen/preview/index.vue +831 -0
  204. package/dist/package/src/views/tool/code-gen/proto/index.vue +703 -0
  205. package/dist/package/src/views/tool/code-gen/table/index.vue +864 -0
  206. package/dist/package/src/views/tool/project-doc/index.vue +561 -0
  207. package/package.json +60 -0
  208. package/tsconfig.json +13 -0
@@ -0,0 +1,1861 @@
1
+ <template>
2
+ <main class="agent-chat-panel" :class="{ 'is-empty': isEmptyState, 'has-shortcuts': hasShortcutPanel }">
3
+ <template v-if="isEmptyState">
4
+ <div class="agent-chat-empty">
5
+ <div class="agent-chat-empty__title">{{ welcomeTitle }}</div>
6
+ <div class="agent-chat-empty__desc">可直接提问,也可以上传附件一起分析。</div>
7
+ <section v-if="hasShortcutPanel" class="agent-shortcuts">
8
+ <div class="agent-shortcuts__head">
9
+ <div>
10
+ <div class="agent-shortcuts__eyebrow">快捷操作</div>
11
+ <div class="agent-shortcuts__title">从常用工作流开始</div>
12
+ </div>
13
+ <div class="agent-shortcuts__actions">
14
+ <span class="agent-shortcuts__count">{{ shortcutBatchLabel }}</span>
15
+ <button
16
+ v-if="canRefreshShortcutBatch"
17
+ class="agent-shortcuts__refresh"
18
+ type="button"
19
+ :disabled="sending"
20
+ @click="refreshShortcutBatch"
21
+ >
22
+ <span>换一批</span>
23
+ <el-icon><Refresh /></el-icon>
24
+ </button>
25
+ </div>
26
+ </div>
27
+ <div v-if="loadingShortcuts" class="agent-shortcuts__loading">正在加载快捷入口...</div>
28
+ <div v-else class="agent-shortcuts__grid">
29
+ <button
30
+ v-for="shortcut in shortcutVisibleList"
31
+ :key="shortcut.key"
32
+ class="agent-shortcut"
33
+ type="button"
34
+ :disabled="sending"
35
+ @click="handleShortcutClick(shortcut)"
36
+ >
37
+ <span class="agent-shortcut__icon">
38
+ <el-icon><component :is="resolveShortcutIcon(shortcut)" /></el-icon>
39
+ </span>
40
+ <span class="agent-shortcut__content">
41
+ <span class="agent-shortcut__title">{{ shortcut.title }}</span>
42
+ <span class="agent-shortcut__meta">{{ resolveShortcutMeta(shortcut) }}</span>
43
+ </span>
44
+ </button>
45
+ </div>
46
+ </section>
47
+ <div class="agent-chat-empty__sender">
48
+ <XSender :key="senderKey" :sending="sending" @submit="handleSubmit" />
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <template v-else>
54
+ <div class="agent-chat-content">
55
+ <BubbleList class="agent-message-list" :list="bubbleList" max-height="100%" :auto-scroll="true">
56
+ <template #content="{ item }">
57
+ <div class="agent-message-body" :class="{ 'is-user': item.role === 'user' }">
58
+ <div
59
+ v-if="
60
+ item.role !== 'user' && (item.replySourceTag || item.model || item.fallback || resolveMessageReplyTime(item))
61
+ "
62
+ class="agent-message-meta"
63
+ >
64
+ <span
65
+ v-if="item.replySourceTag"
66
+ class="agent-message-meta__tag"
67
+ :class="resolveTagClass(item.replySourceTag.tone)"
68
+ >
69
+ {{ item.replySourceTag.text }}
70
+ </span>
71
+ <span v-if="item.model" class="agent-message-meta__model">{{ item.model }}</span>
72
+ <span v-if="resolveMessageReplyTime(item)" class="agent-message-meta__time">{{
73
+ resolveMessageReplyTime(item)
74
+ }}</span>
75
+ </div>
76
+ <el-collapse
77
+ v-if="item.role !== 'user' && resolveVisibleTools(item).length"
78
+ class="agent-message-tools"
79
+ :model-value="resolveActiveToolKeys(item)"
80
+ @update:model-value="value => handleToolCollapseUpdate(item, value)"
81
+ >
82
+ <el-collapse-item
83
+ v-for="(tool, toolIndex) in resolveVisibleTools(item)"
84
+ :key="resolveToolKey(tool, toolIndex)"
85
+ :name="resolveToolKey(tool, toolIndex)"
86
+ >
87
+ <template #title>
88
+ <span class="agent-message-tool-title">
89
+ <span class="agent-message-tool-title__main">
90
+ <span class="agent-message-tool-title__icon">
91
+ <el-icon><Link /></el-icon>
92
+ </span>
93
+ <span class="agent-message-tool-title__text">
94
+ <span class="agent-message-tool-title__label">{{ resolveToolTitle(tool) }}</span>
95
+ <span v-if="resolveToolName(tool)" class="agent-message-tool-title__name">{{
96
+ resolveToolName(tool)
97
+ }}</span>
98
+ </span>
99
+ </span>
100
+ <span class="agent-message-tool-title__status" :class="resolveToolStatusClass(tool.status)">
101
+ {{ resolveToolStatusText(tool.status) }}
102
+ <el-icon v-if="!isToolFailed(tool.status)"><Check /></el-icon>
103
+ </span>
104
+ <button
105
+ class="agent-message-tool-copy"
106
+ type="button"
107
+ aria-label="复制请求报文"
108
+ @click.stop="handleCopyToolRequest(tool)"
109
+ >
110
+ <el-icon><CopyDocument /></el-icon>
111
+ </button>
112
+ </span>
113
+ </template>
114
+ <div class="agent-message-tool-payload">
115
+ <section class="agent-message-tool-payload__section">
116
+ <div class="agent-message-tool-payload__label">REQUEST</div>
117
+ <pre>{{ formatToolRequest(tool) }}</pre>
118
+ </section>
119
+ <section class="agent-message-tool-payload__section">
120
+ <div class="agent-message-tool-payload__label">RESPONSE</div>
121
+ <pre>{{ formatToolResponse(tool) }}</pre>
122
+ </section>
123
+ </div>
124
+ </el-collapse-item>
125
+ </el-collapse>
126
+ <div
127
+ class="agent-message-content"
128
+ :class="{
129
+ 'is-thinking': item.progressState === 'streaming',
130
+ 'is-failed-ai': isAIFailedMessage(item),
131
+ 'is-user': item.role === 'user'
132
+ }"
133
+ >
134
+ <div v-if="isAIFailedMessage(item)" class="agent-message-error">
135
+ <div class="agent-message-error__title">服务器异常,建议稍后重试</div>
136
+ <div class="agent-message-error__content">{{ resolveAIErrorMessage(item) }}</div>
137
+ <el-collapse v-if="item.fallback_reason" class="agent-message-error__detail" accordion>
138
+ <el-collapse-item title="错误详情" :name="String(item.id)">
139
+ <pre>{{ item.fallback_reason }}</pre>
140
+ </el-collapse-item>
141
+ </el-collapse>
142
+ </div>
143
+ <template v-else-if="item.role !== 'user'">
144
+ <AiMarkdown :content="item.content" :streaming="item.progressState === 'streaming'" />
145
+ <slot name="flow-blocks" :message="item" :active-flow-message-id="activeFlowMessageID" />
146
+ <el-collapse v-if="item.fallback_reason" class="agent-message-error__detail" accordion>
147
+ <el-collapse-item title="错误详情" :name="String(item.id)">
148
+ <pre>{{ item.fallback_reason }}</pre>
149
+ </el-collapse-item>
150
+ </el-collapse>
151
+ </template>
152
+ <span v-else>{{ item.content }}</span>
153
+ <span v-if="item.progressState === 'streaming'" class="agent-thinking-dots"> <i></i><i></i><i></i> </span>
154
+ </div>
155
+ <div v-if="item.attachments?.length" class="agent-message-attachments">
156
+ <Attachments :items="buildMessageAttachmentItems(item.attachments)" overflow="wrap" :hide-upload="true" />
157
+ </div>
158
+ </div>
159
+ </template>
160
+ <template #footer="{ item }">
161
+ <div
162
+ v-message-footer-width
163
+ class="agent-message-footer"
164
+ :class="{ 'is-user': item.role === 'user', 'is-editing': isEditingMessage(item) }"
165
+ >
166
+ <div class="agent-message-actions" :class="{ 'is-user': item.role === 'user' }">
167
+ <template v-for="action in resolveMessageActions(item)" :key="action.key">
168
+ <el-popconfirm
169
+ v-if="shouldConfirmMessageAction(action.key)"
170
+ :title="resolveActionConfirmTitle(action.key, item)"
171
+ confirm-button-text="确定"
172
+ cancel-button-text="取消"
173
+ width="220"
174
+ @confirm="handleMessageAction(action.key, item)"
175
+ >
176
+ <template #reference>
177
+ <button
178
+ class="agent-message-action"
179
+ type="button"
180
+ :disabled="sending || item.progressState === 'streaming'"
181
+ :aria-label="action.label"
182
+ >
183
+ <el-icon><component :is="action.icon" /></el-icon>
184
+ </button>
185
+ </template>
186
+ </el-popconfirm>
187
+ <el-tooltip v-else :content="action.label" placement="top">
188
+ <button
189
+ class="agent-message-action"
190
+ type="button"
191
+ :disabled="sending || item.progressState === 'streaming'"
192
+ :aria-label="action.label"
193
+ @click="handleMessageAction(action.key, item)"
194
+ >
195
+ <el-icon><component :is="action.icon" /></el-icon>
196
+ </button>
197
+ </el-tooltip>
198
+ </template>
199
+ <el-popover
200
+ v-if="item.role !== 'user' && hasAIUsage(item)"
201
+ popper-class="agent-message-runtime-popover"
202
+ placement="top"
203
+ trigger="hover"
204
+ width="260"
205
+ >
206
+ <template #reference>
207
+ <button
208
+ class="agent-message-action agent-message-runtime-trigger"
209
+ type="button"
210
+ aria-label="查看运行明细"
211
+ :disabled="item.progressState === 'streaming'"
212
+ >
213
+ <el-icon><DataAnalysis /></el-icon>
214
+ <span v-if="resolveTokenTotal(item) > 0">{{ formatCompactNumber(resolveTokenTotal(item)) }}</span>
215
+ <span v-if="item.duration_ms > 0">{{ formatDurationMs(item.duration_ms) }}</span>
216
+ </button>
217
+ </template>
218
+ <div class="agent-runtime-detail">
219
+ <div class="agent-runtime-detail__title">运行明细</div>
220
+ <div v-if="resolveTokenTotal(item) > 0" class="agent-runtime-detail__section">
221
+ <div class="agent-runtime-detail__section-title">Token</div>
222
+ <div class="agent-runtime-detail__row">
223
+ <span>输入</span>
224
+ <strong>{{ formatNumber(item.token?.input) }}</strong>
225
+ </div>
226
+ <div class="agent-runtime-detail__row">
227
+ <span>输出</span>
228
+ <strong>{{ formatNumber(item.token?.output) }}</strong>
229
+ </div>
230
+ <div v-if="(item.token?.cache ?? 0) > 0" class="agent-runtime-detail__row">
231
+ <span>缓存读取</span>
232
+ <strong>{{ formatNumber(item.token?.cache) }}</strong>
233
+ </div>
234
+ <div class="agent-runtime-detail__row is-total">
235
+ <span>总计</span>
236
+ <strong>{{ formatNumber(item.token?.total) }}</strong>
237
+ </div>
238
+ </div>
239
+ <div v-if="item.first_token_ms > 0 || item.duration_ms > 0" class="agent-runtime-detail__section">
240
+ <div class="agent-runtime-detail__section-title">耗时</div>
241
+ <div v-if="item.first_token_ms > 0" class="agent-runtime-detail__row">
242
+ <span>首 Token</span>
243
+ <strong>{{ formatDurationMs(item.first_token_ms) }}</strong>
244
+ </div>
245
+ <div v-if="item.duration_ms > 0" class="agent-runtime-detail__row is-total">
246
+ <span>总耗时</span>
247
+ <strong>{{ formatDurationMs(item.duration_ms) }}</strong>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ </el-popover>
252
+ </div>
253
+ <div v-if="isEditingMessage(item)" class="agent-message-edit">
254
+ <el-input
255
+ v-model="editingContent"
256
+ class="agent-message-edit__input"
257
+ type="textarea"
258
+ resize="none"
259
+ :autosize="{ minRows: 3, maxRows: 8 }"
260
+ :disabled="sending"
261
+ @keydown.ctrl.enter.prevent="submitMessageEdit(item)"
262
+ @keydown.meta.enter.prevent="submitMessageEdit(item)"
263
+ />
264
+ <div class="agent-message-edit__footer">
265
+ <el-button size="small" :disabled="sending" @click="cancelMessageEdit">取消</el-button>
266
+ <el-button size="small" type="primary" :loading="sending" @click="submitMessageEdit(item)">发送</el-button>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </template>
271
+ </BubbleList>
272
+ </div>
273
+
274
+ <div class="agent-composer-dock">
275
+ <section v-if="hasShortcutPanel" class="agent-shortcuts agent-shortcuts--compact">
276
+ <div class="agent-shortcuts__head">
277
+ <div>
278
+ <div class="agent-shortcuts__eyebrow">快捷操作</div>
279
+ <div class="agent-shortcuts__title">继续调用常用流程</div>
280
+ </div>
281
+ <div class="agent-shortcuts__actions">
282
+ <span class="agent-shortcuts__count">{{ shortcutBatchLabel }}</span>
283
+ <button
284
+ v-if="canRefreshShortcutBatch"
285
+ class="agent-shortcuts__refresh"
286
+ type="button"
287
+ :disabled="sending"
288
+ @click="refreshShortcutBatch"
289
+ >
290
+ <span>换一批</span>
291
+ <el-icon><Refresh /></el-icon>
292
+ </button>
293
+ </div>
294
+ </div>
295
+ <div v-if="loadingShortcuts" class="agent-shortcuts__loading">正在加载快捷入口...</div>
296
+ <div v-else class="agent-shortcuts__grid">
297
+ <button
298
+ v-for="shortcut in shortcutVisibleList"
299
+ :key="shortcut.key"
300
+ class="agent-shortcut"
301
+ type="button"
302
+ :disabled="sending"
303
+ @click="handleShortcutClick(shortcut)"
304
+ >
305
+ <span class="agent-shortcut__icon">
306
+ <el-icon><component :is="resolveShortcutIcon(shortcut)" /></el-icon>
307
+ </span>
308
+ <span class="agent-shortcut__content">
309
+ <span class="agent-shortcut__title">{{ shortcut.title }}</span>
310
+ <span class="agent-shortcut__meta">{{ resolveShortcutMeta(shortcut) }}</span>
311
+ </span>
312
+ </button>
313
+ </div>
314
+ </section>
315
+ <XSender :key="senderKey" :sending="sending" @submit="handleSubmit" />
316
+ </div>
317
+ </template>
318
+ </main>
319
+ </template>
320
+
321
+ <script setup lang="ts" name="ChatPanel">
322
+ import { computed, defineAsyncComponent, h, nextTick, ref } from "vue";
323
+ import type { Component, ObjectDirective } from "vue";
324
+ import { Attachments, BubbleList } from "vue-element-plus-x";
325
+ import type { FilesCardProps } from "vue-element-plus-x/types/FilesCard";
326
+ import { Check, CopyDocument, DataAnalysis, Delete, EditPen, Link, Memo, PieChart, Refresh, User } from "@element-plus/icons-vue";
327
+ import { ElMessage } from "element-plus";
328
+ import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
329
+ import { type AiAttachment, type AiSession } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
330
+ import type { AiShortcut, AiToolCall } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
331
+ import { AiMessageStatus } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
332
+ import XSender from "./XSender.vue";
333
+
334
+ // AI Markdown 渲染器依赖较重,仅在真正出现助手消息时再加载。
335
+ const AiMarkdown = defineAsyncComponent(() => import("./AiMarkdown.vue"));
336
+ import { buildAIAttachmentFileCard } from "../attachment";
337
+ import type { ChatMessageAction, ChatMessageEditPayload, ChatMessageItem, ReplySourceTag, SubmitPayload } from "../types";
338
+
339
+ /** 消息操作按钮配置。 */
340
+ type MessageActionOption = {
341
+ /** 操作类型。 */
342
+ key: ChatMessageAction;
343
+ /** 悬浮提示和无障碍文案。 */
344
+ label: string;
345
+ /** 操作图标组件。 */
346
+ icon: Component;
347
+ };
348
+
349
+ /** 消息底部操作栏宽度同步状态。 */
350
+ type MessageFooterWidthState = {
351
+ /** 监听上方内容卡片宽度变化。 */
352
+ observer: ResizeObserver;
353
+ /** 当前 footer 对应的内容卡片元素。 */
354
+ contentEl: HTMLElement;
355
+ };
356
+
357
+ /** 工具折叠面板的 Element Plus 受控值类型。 */
358
+ type ToolCollapseValue = string | number | Array<string | number>;
359
+
360
+ const messageFooterWidthStateMap = new WeakMap<HTMLElement, MessageFooterWidthState>();
361
+ const activeToolKeyMap = ref<Record<string, string[]>>({});
362
+ const editingMessageKey = ref("");
363
+ const editingContent = ref("");
364
+ /** 助手消息最小阅读宽度,保证底部操作和运行信息不互相遮挡。 */
365
+ const AI_MESSAGE_MIN_WIDTH = 360;
366
+ /** 快捷入口每批展示数量,和移动端保持一致,避免大量入口挤压输入区。 */
367
+ const SHORTCUT_BATCH_SIZE = 4;
368
+ const shortcutBatchIndex = ref(0);
369
+
370
+ /** 朗读图标,按产品示意图绘制为喇叭声波。 */
371
+ const SpeakActionIcon = defineAsyncComponent(() =>
372
+ Promise.resolve({
373
+ name: "SpeakActionIcon",
374
+ render() {
375
+ return h(
376
+ "svg",
377
+ {
378
+ class: "agent-message-action__custom-icon",
379
+ xmlns: "http://www.w3.org/2000/svg",
380
+ viewBox: "80 160 864 704",
381
+ fill: "none",
382
+ "aria-hidden": "true"
383
+ },
384
+ [
385
+ h("path", {
386
+ d: "M128 400h144L512 224v576L272 624H128z",
387
+ stroke: "currentColor",
388
+ "stroke-width": "64",
389
+ "stroke-linejoin": "round"
390
+ }),
391
+ h("path", {
392
+ d: "M640 352a224 224 0 0 1 0 320",
393
+ stroke: "currentColor",
394
+ "stroke-width": "64",
395
+ "stroke-linecap": "round"
396
+ }),
397
+ h("path", {
398
+ d: "M768 224a384 384 0 0 1 0 576",
399
+ stroke: "currentColor",
400
+ "stroke-width": "64",
401
+ "stroke-linecap": "round"
402
+ })
403
+ ]
404
+ );
405
+ }
406
+ })
407
+ );
408
+
409
+ /** 分支图标,按产品示意图绘制为向上分叉箭头。 */
410
+ const BranchActionIcon = defineAsyncComponent(() =>
411
+ Promise.resolve({
412
+ name: "BranchActionIcon",
413
+ render() {
414
+ return h(
415
+ "svg",
416
+ {
417
+ class: "agent-message-action__custom-icon",
418
+ xmlns: "http://www.w3.org/2000/svg",
419
+ viewBox: "96 160 832 704",
420
+ fill: "none",
421
+ "aria-hidden": "true"
422
+ },
423
+ [
424
+ h("path", {
425
+ d: "M256 832V192",
426
+ stroke: "currentColor",
427
+ "stroke-width": "64",
428
+ "stroke-linecap": "round"
429
+ }),
430
+ h("path", {
431
+ d: "M256 192 128 320M256 192l128 128",
432
+ stroke: "currentColor",
433
+ "stroke-width": "64",
434
+ "stroke-linecap": "round",
435
+ "stroke-linejoin": "round"
436
+ }),
437
+ h("path", {
438
+ d: "M384 640c256 0 384-176 384-448",
439
+ stroke: "currentColor",
440
+ "stroke-width": "64",
441
+ "stroke-linecap": "round"
442
+ }),
443
+ h("path", {
444
+ d: "M768 192 640 320M768 192l128 128",
445
+ stroke: "currentColor",
446
+ "stroke-width": "64",
447
+ "stroke-linecap": "round",
448
+ "stroke-linejoin": "round"
449
+ })
450
+ ]
451
+ );
452
+ }
453
+ })
454
+ );
455
+
456
+ const props = defineProps<{
457
+ /** 当前活动会话,用于输入框按会话重建。 */
458
+ activeSession?: AiSession;
459
+ /** 当前会话消息列表。 */
460
+ messages: ChatMessageItem[];
461
+ /** 消息发送加载状态。 */
462
+ sending: boolean;
463
+ /** 空态快捷入口加载状态。 */
464
+ loadingShortcuts?: boolean;
465
+ /** 当前终端可用快捷入口。 */
466
+ shortcuts?: AiShortcut[];
467
+ }>();
468
+
469
+ const emit = defineEmits<{
470
+ /** 提交输入框内容。 */
471
+ submit: [payload: SubmitPayload];
472
+ /** 触发消息级操作。 */
473
+ messageAction: [payload: { action: ChatMessageAction; item: ChatMessageItem }];
474
+ /** 提交当前用户消息的文本编辑。 */
475
+ messageEdit: [payload: ChatMessageEditPayload];
476
+ }>();
477
+
478
+ const isEmptyState = computed(() => props.messages.length === 0);
479
+
480
+ const senderKey = computed(() => props.activeSession?.id || "empty-session");
481
+
482
+ const bubbleList = computed<ChatMessageItem[]>(() =>
483
+ (props.messages ?? []).map(item => ({
484
+ ...item,
485
+ key: String(item.key ?? item.id ?? `${item.role}-${item.created_at?.seconds ?? 0}`),
486
+ content: String(item.content ?? ""),
487
+ placement: item.placement ?? "start"
488
+ }))
489
+ );
490
+
491
+ const lastEditableUserMessageKey = computed(() => {
492
+ for (let index = props.messages.length - 1; index >= 0; index--) {
493
+ const item = props.messages[index];
494
+ if (item.role === "user" && !item.localOnly) {
495
+ return resolveMessageEditKey(item);
496
+ }
497
+ }
498
+ return "";
499
+ });
500
+
501
+ const activeFlowMessageID = computed(() => {
502
+ for (let index = props.messages.length - 1; index >= 0; index--) {
503
+ const item = props.messages[index];
504
+ if (item.role !== "user" && item.blocks?.length) return String(item.id ?? "");
505
+ }
506
+ return "";
507
+ });
508
+
509
+ const welcomeTitle = computed(() => {
510
+ const hour = new Date().getHours();
511
+ if (hour < 12) return "上午好,我是通用 AI 助手";
512
+ if (hour < 18) return "下午好,我是通用 AI 助手";
513
+ return "晚上好,我是通用 AI 助手";
514
+ });
515
+
516
+ const shortcutAllList = computed(() =>
517
+ [...(props.shortcuts ?? [])]
518
+ .filter(item => Boolean(item?.key && (item.title || item.prompt)))
519
+ .sort((left, right) => Number(left.sort ?? 0) - Number(right.sort ?? 0))
520
+ );
521
+
522
+ const shortcutBatchCount = computed(() => Math.max(1, Math.ceil(shortcutAllList.value.length / SHORTCUT_BATCH_SIZE)));
523
+
524
+ const shortcutVisibleList = computed(() => {
525
+ const pageIndex = shortcutBatchIndex.value % shortcutBatchCount.value;
526
+ const start = pageIndex * SHORTCUT_BATCH_SIZE;
527
+ return shortcutAllList.value.slice(start, start + SHORTCUT_BATCH_SIZE);
528
+ });
529
+
530
+ const hasShortcutPanel = computed(() => props.loadingShortcuts || shortcutVisibleList.value.length > 0);
531
+
532
+ const canRefreshShortcutBatch = computed(() => shortcutAllList.value.length > SHORTCUT_BATCH_SIZE);
533
+
534
+ const shortcutBatchLabel = computed(() => {
535
+ const total = shortcutAllList.value.length;
536
+ if (props.loadingShortcuts && total === 0) return "加载中";
537
+ if (total <= SHORTCUT_BATCH_SIZE) return `${total} 项`;
538
+ const pageIndex = shortcutBatchIndex.value % shortcutBatchCount.value;
539
+ const start = pageIndex * SHORTCUT_BATCH_SIZE + 1;
540
+ const end = Math.min(start + SHORTCUT_BATCH_SIZE - 1, total);
541
+ return `${start}-${end} / ${total}`;
542
+ });
543
+
544
+ /** 查找 footer 所属气泡里的消息内容元素。 */
545
+ function findMessageContentElement(el: HTMLElement) {
546
+ const wrapper = el.closest(".elx-bubble__content-wrapper");
547
+ return (
548
+ wrapper?.querySelector<HTMLElement>(".agent-message-body") ??
549
+ wrapper?.querySelector<HTMLElement>(".elx-bubble__content") ??
550
+ null
551
+ );
552
+ }
553
+
554
+ /** 把消息 footer 宽度同步为上方内容宽度,避免底部按钮越出正文边界。 */
555
+ function syncMessageFooterWidth(el: HTMLElement) {
556
+ const contentEl = findMessageContentElement(el);
557
+ if (!contentEl) return;
558
+
559
+ const isAIMessage = !el.classList.contains("is-user");
560
+ const width = Math.max(contentEl.getBoundingClientRect().width, isAIMessage ? AI_MESSAGE_MIN_WIDTH : 0);
561
+ if (width <= 0) return;
562
+
563
+ el.style.setProperty("--agent-message-footer-width", `${Math.ceil(width)}px`);
564
+ }
565
+
566
+ /** 绑定内容尺寸监听,覆盖流式内容、Markdown 渲染和窗口变化后的重新对齐。 */
567
+ function bindMessageFooterWidth(el: HTMLElement) {
568
+ const contentEl = findMessageContentElement(el);
569
+ const existingState = messageFooterWidthStateMap.get(el);
570
+ if (!contentEl) {
571
+ existingState?.observer.disconnect();
572
+ messageFooterWidthStateMap.delete(el);
573
+ el.style.removeProperty("--agent-message-footer-width");
574
+ return;
575
+ }
576
+
577
+ if (existingState?.contentEl === contentEl) {
578
+ syncMessageFooterWidth(el);
579
+ return;
580
+ }
581
+
582
+ existingState?.observer.disconnect();
583
+ const observer = new ResizeObserver(() => syncMessageFooterWidth(el));
584
+ observer.observe(contentEl);
585
+ messageFooterWidthStateMap.set(el, { observer, contentEl });
586
+ syncMessageFooterWidth(el);
587
+ }
588
+
589
+ const vMessageFooterWidth: ObjectDirective<HTMLElement> = {
590
+ mounted(el) {
591
+ void nextTick(() => bindMessageFooterWidth(el));
592
+ },
593
+ updated(el) {
594
+ void nextTick(() => bindMessageFooterWidth(el));
595
+ },
596
+ beforeUnmount(el) {
597
+ messageFooterWidthStateMap.get(el)?.observer.disconnect();
598
+ messageFooterWidthStateMap.delete(el);
599
+ }
600
+ };
601
+
602
+ /** 读取输入框内容并提交给父组件。 */
603
+ function handleSubmit(payload: SubmitPayload) {
604
+ emit("submit", payload);
605
+ }
606
+
607
+ /** 点击快捷入口时提交对应入口动作。 */
608
+ function handleShortcutClick(shortcut: AiShortcut) {
609
+ emit("submit", {
610
+ text: shortcut.prompt || shortcut.title,
611
+ attachments: [],
612
+ action: buildShortcutAction(shortcut)
613
+ });
614
+ }
615
+
616
+ /** 轮换下一批快捷入口,保持大量入口也能被逐批访问。 */
617
+ function refreshShortcutBatch() {
618
+ if (!canRefreshShortcutBatch.value) return;
619
+ shortcutBatchIndex.value = (shortcutBatchIndex.value + 1) % shortcutBatchCount.value;
620
+ }
621
+
622
+ /** 根据快捷入口的通用类型给出稳定的轻量图标。 */
623
+ function resolveShortcutIcon(shortcut: AiShortcut) {
624
+ const text = `${shortcut.group || ""} ${shortcut.title || ""} ${shortcut.key || ""}`.toLowerCase();
625
+ if (text.includes("user") || text.includes("用户")) return User;
626
+ if (text.includes("report") || text.includes("统计") || text.includes("分析")) return PieChart;
627
+ return Memo;
628
+ }
629
+
630
+ /** 展示快捷入口的分组和工具依赖摘要,帮助运营判断动作上下文。 */
631
+ function resolveShortcutMeta(shortcut: AiShortcut) {
632
+ const group = shortcut.group || "通用助手";
633
+ const toolCount = shortcut.required_tools?.length ?? 0;
634
+ return toolCount > 0 ? `${group} · ${toolCount} 个工具` : group;
635
+ }
636
+
637
+ /** 生成快捷入口对应的流程动作。 */
638
+ function buildShortcutAction(shortcut: AiShortcut) {
639
+ if (!shortcut.action?.type) return undefined;
640
+ return buildFlowAction(shortcut.action, parseActionPayload(shortcut.action.payload_json));
641
+ }
642
+
643
+ /** 构造发送给后端的流程动作,统一补齐 payload JSON 默认值。 */
644
+ function buildFlowAction(action?: Partial<AiAction>, payload?: Record<string, unknown>): AiAction | undefined {
645
+ if (!action?.type) return undefined;
646
+ return {
647
+ flow: action.flow || "",
648
+ step: action.step || "",
649
+ type: action.type,
650
+ payload_json: JSON.stringify(payload || {}),
651
+ source_message_id: action.source_message_id || "",
652
+ action_id: action.action_id || "",
653
+ flow_version: Number(action.flow_version || 0)
654
+ };
655
+ }
656
+
657
+ /** 解析流程动作负载,兼容空字符串和异常 JSON。 */
658
+ function parseActionPayload(payload?: string) {
659
+ try {
660
+ return JSON.parse(payload || "{}") as Record<string, unknown>;
661
+ } catch {
662
+ return {};
663
+ }
664
+ }
665
+
666
+ /** 根据消息角色返回可用操作。 */
667
+ function resolveMessageActions(item: ChatMessageItem) {
668
+ if (item.progressState === "streaming" || item.status === AiMessageStatus.GENERATING_AAMS) return [];
669
+
670
+ const copyAction: MessageActionOption = { key: "copy" as const, label: "复制", icon: CopyDocument };
671
+ const deleteAction: MessageActionOption = { key: "delete" as const, label: "删除", icon: Delete };
672
+ const editAction: MessageActionOption = { key: "edit" as const, label: "编辑", icon: EditPen };
673
+ if (item.role === "user") {
674
+ const actions = isLastEditableUserMessage(item) ? [editAction, copyAction, deleteAction] : [copyAction, deleteAction];
675
+ if (item.status === AiMessageStatus.FAILED_AAMS) {
676
+ return item.localOnly
677
+ ? [{ key: "retry" as const, label: "重新发送", icon: Refresh }, copyAction, deleteAction]
678
+ : [{ key: "retry" as const, label: "重新发送", icon: Refresh }, ...actions];
679
+ }
680
+ return item.localOnly ? [copyAction, deleteAction] : actions;
681
+ }
682
+
683
+ const actions: MessageActionOption[] = [{ key: "retry" as const, label: "重新生成", icon: Refresh }];
684
+ if (item.status === AiMessageStatus.SUCCESS_AAMS) {
685
+ actions.push({ key: "branch" as const, label: "从此处创建分支会话", icon: BranchActionIcon });
686
+ actions.push({ key: "speak" as const, label: item.speaking ? "停止朗读" : "朗读", icon: SpeakActionIcon });
687
+ }
688
+ return [...actions, copyAction, deleteAction];
689
+ }
690
+
691
+ /** 判断是否为需要渲染错误卡片的助手失败消息。 */
692
+ function isAIFailedMessage(item: ChatMessageItem) {
693
+ return item.role !== "user" && item.status === AiMessageStatus.FAILED_AAMS;
694
+ }
695
+
696
+ /** 返回助手错误摘要,优先展示服务端可读错误。 */
697
+ function resolveAIErrorMessage(item: ChatMessageItem) {
698
+ const content = String(item.content ?? "").trim();
699
+ return content || "这次回复没有成功返回,你可以直接重试刚才的问题。";
700
+ }
701
+
702
+ /** 判断助手消息是否存在最终用量信息。 */
703
+ function hasAIUsage(item: ChatMessageItem) {
704
+ return resolveTokenTotal(item) > 0 || item.first_token_ms > 0 || item.duration_ms > 0;
705
+ }
706
+
707
+ /** 返回当前气泡总 token 数。 */
708
+ function resolveTokenTotal(item: ChatMessageItem) {
709
+ return Number(item.token?.total ?? 0);
710
+ }
711
+
712
+ /** 将毫秒耗时格式化为秒。 */
713
+ function formatDurationMs(value?: number) {
714
+ const ms = Number(value ?? 0);
715
+ if (ms <= 0) return "0s";
716
+ return `${(ms / 1000).toFixed(2)}s`;
717
+ }
718
+
719
+ /** 返回助手消息回复时间,优先展示后端消息创建时间。 */
720
+ function resolveMessageReplyTime(item: ChatMessageItem) {
721
+ const timestamp = resolveMessageCreatedAt(item);
722
+ if (!timestamp) return "";
723
+ return new Intl.DateTimeFormat("zh-CN", {
724
+ year: "numeric",
725
+ month: "2-digit",
726
+ day: "2-digit",
727
+ hour: "2-digit",
728
+ minute: "2-digit",
729
+ second: "2-digit",
730
+ hour12: false
731
+ }).format(new Date(timestamp));
732
+ }
733
+
734
+ /** 解析消息创建时间戳,兼容 protobuf Timestamp 结构。 */
735
+ function resolveMessageCreatedAt(item: ChatMessageItem) {
736
+ const seconds = Number(item.created_at?.seconds ?? 0);
737
+ const nanos = Number(item.created_at?.nanos ?? 0);
738
+ if (seconds <= 0 && nanos <= 0) return 0;
739
+ return seconds * 1000 + Math.floor(nanos / 1_000_000);
740
+ }
741
+
742
+ /** 将较大的数字压缩成适合 footer 展示的短格式。 */
743
+ function formatCompactNumber(value?: number) {
744
+ const number = Number(value ?? 0);
745
+ if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(number >= 10_000_000 ? 0 : 1)}M`;
746
+ if (number >= 1_000) return `${(number / 1_000).toFixed(number >= 100_000 ? 0 : 1)}K`;
747
+ return formatNumber(number);
748
+ }
749
+
750
+ /** 格式化明细数字,保持 Token 明细易读。 */
751
+ function formatNumber(value?: number) {
752
+ return new Intl.NumberFormat("zh-CN").format(Number(value ?? 0));
753
+ }
754
+
755
+ /** 生成工具标签稳定键。 */
756
+ function resolveToolKey(tool: AiToolCall, index?: number) {
757
+ return `${tool.type || "tool"}:${tool.name || tool.title}:${index ?? 0}`;
758
+ }
759
+
760
+ /** 生成消息维度的工具展开状态键,避免不同气泡之间互相影响。 */
761
+ function resolveToolCollapseMessageKey(item: ChatMessageItem) {
762
+ return String(item.key ?? item.id ?? "");
763
+ }
764
+
765
+ /** 返回当前消息已展开的工具面板,默认保持全部收起。 */
766
+ function resolveActiveToolKeys(item: ChatMessageItem) {
767
+ return activeToolKeyMap.value[resolveToolCollapseMessageKey(item)] ?? [];
768
+ }
769
+
770
+ /** 更新工具面板展开状态,兼容 Element Plus 单值和数组两类返回值。 */
771
+ function handleToolCollapseUpdate(item: ChatMessageItem, value: ToolCollapseValue) {
772
+ activeToolKeyMap.value = {
773
+ ...activeToolKeyMap.value,
774
+ [resolveToolCollapseMessageKey(item)]: Array.isArray(value) ? value.map(String) : [String(value)].filter(Boolean)
775
+ };
776
+ }
777
+
778
+ /** 返回需要展示在消息内容区的 function 工具记录,过滤联网搜索等 server 工具。 */
779
+ function resolveVisibleTools(item: ChatMessageItem) {
780
+ return (item.tools ?? []).filter(tool => tool.type === "function" && String(tool.name || tool.title).trim());
781
+ }
782
+
783
+ /** 生成工具展示名称。 */
784
+ function resolveToolTitle(tool: AiToolCall) {
785
+ return tool.title || tool.name || "工具";
786
+ }
787
+
788
+ /** 返回工具真实名称,用于排障时识别具体 Agent Tool。 */
789
+ function resolveToolName(tool: AiToolCall) {
790
+ return String(tool.name ?? "").trim();
791
+ }
792
+
793
+ /** 生成工具状态样式名。 */
794
+ function resolveToolStatusClass(status?: string) {
795
+ return isToolFailed(status) ? "is-error" : "is-success";
796
+ }
797
+
798
+ /** 生成工具状态展示文案。 */
799
+ function resolveToolStatusText(status?: string) {
800
+ return isToolFailed(status) ? "失败" : "已完成";
801
+ }
802
+
803
+ /** 判断工具调用是否失败,兼容后端历史状态值。 */
804
+ function isToolFailed(status?: string) {
805
+ return ["error", "failed", "fail"].includes(String(status ?? "").toLowerCase());
806
+ }
807
+
808
+ /** 判断原始报文是否有实际内容,避免空对象撑出无意义区块。 */
809
+ function hasToolPayloadValue(payload?: string) {
810
+ const value = String(payload ?? "").trim();
811
+ if (!value || value === "{}") return false;
812
+ return true;
813
+ }
814
+
815
+ /** 格式化工具原始报文,JSON 内容优先缩进展示。 */
816
+ function formatToolPayload(payload?: string) {
817
+ const value = String(payload ?? "");
818
+ if (!value) return "{}";
819
+ try {
820
+ return JSON.stringify(JSON.parse(value), null, 2);
821
+ } catch {
822
+ return value;
823
+ }
824
+ }
825
+
826
+ /** 格式化工具请求报文,展开态始终保留稳定内容区。 */
827
+ function formatToolRequest(tool: AiToolCall) {
828
+ const input = String(tool.input ?? "");
829
+ if (!hasToolPayloadValue(input)) return "{}";
830
+ if (!isJSONLikeText(input) && !hasToolPayloadValue(tool.output)) return "{}";
831
+ return formatToolArguments(input);
832
+ }
833
+
834
+ /** 格式化工具出参,兼容历史结果落在 input 的消息。 */
835
+ function formatToolResponse(tool: AiToolCall) {
836
+ if (hasToolPayloadValue(tool.output)) return formatToolPayload(tool.output);
837
+ if (hasToolPayloadValue(tool.input) && !isJSONLikeText(tool.input)) return formatToolPayload(tool.input);
838
+ return "{}";
839
+ }
840
+
841
+ /** 判断文本是否像 JSON,工具请求报文正常应由模型参数 JSON 构成。 */
842
+ function isJSONLikeText(payload?: string) {
843
+ const value = String(payload ?? "");
844
+ if (!value) return false;
845
+ const first = value[0];
846
+ return first === "{" || first === "[";
847
+ }
848
+
849
+ /** 格式化工具入参,简单对象按字段行展示,贴近工具调用详情的阅读习惯。 */
850
+ function formatToolArguments(payload?: string) {
851
+ const value = String(payload ?? "");
852
+ if (!value) return "";
853
+ try {
854
+ const parsed = JSON.parse(value) as unknown;
855
+ if (!isPlainRecord(parsed)) return JSON.stringify(parsed, null, 2);
856
+ return Object.entries(parsed)
857
+ .map(([key, item]) => `${key} ${formatToolArgumentValue(item)}`)
858
+ .join("\n");
859
+ } catch {
860
+ return value;
861
+ }
862
+ }
863
+
864
+ /** 判断值是否为普通对象,避免把数组按入参表展示。 */
865
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
866
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
867
+ }
868
+
869
+ /** 格式化单个工具入参值,复杂值保留 JSON 结构。 */
870
+ function formatToolArgumentValue(value: unknown) {
871
+ if (isPlainRecord(value) || Array.isArray(value)) return JSON.stringify(value);
872
+ return String(value ?? "");
873
+ }
874
+
875
+ /** 复制工具请求报文,点击时不触发展开收起。 */
876
+ async function handleCopyToolRequest(tool: AiToolCall) {
877
+ try {
878
+ await navigator.clipboard.writeText(formatToolRequest(tool));
879
+ ElMessage.success("请求报文已复制");
880
+ } catch {
881
+ ElMessage.error("当前浏览器不支持复制");
882
+ }
883
+ }
884
+
885
+ /** 向父组件透传消息操作,由页面层决定是否重发、复制或移除。 */
886
+ function handleMessageAction(action: ChatMessageAction, item: ChatMessageItem) {
887
+ if (action === "edit") {
888
+ startMessageEdit(item);
889
+ return;
890
+ }
891
+ emit("messageAction", { action, item });
892
+ }
893
+
894
+ /** 生成编辑态稳定键,用于只展开当前用户消息下方的编辑框。 */
895
+ function resolveMessageEditKey(item: ChatMessageItem) {
896
+ return String(item.key ?? `${item.id}:${item.role}`);
897
+ }
898
+
899
+ /** 判断当前消息是否正在编辑。 */
900
+ function isEditingMessage(item: ChatMessageItem) {
901
+ return editingMessageKey.value === resolveMessageEditKey(item);
902
+ }
903
+
904
+ /** 判断用户消息是否为当前会话最后一轮可编辑消息。 */
905
+ function isLastEditableUserMessage(item: ChatMessageItem) {
906
+ return item.role === "user" && !item.localOnly && lastEditableUserMessageKey.value === resolveMessageEditKey(item);
907
+ }
908
+
909
+ /** 开始编辑用户消息正文,附件保持原样不进入编辑区。 */
910
+ function startMessageEdit(item: ChatMessageItem) {
911
+ if (!isLastEditableUserMessage(item)) return;
912
+ editingMessageKey.value = resolveMessageEditKey(item);
913
+ editingContent.value = String(item.content ?? "");
914
+ }
915
+
916
+ /** 取消当前消息文本编辑。 */
917
+ function cancelMessageEdit() {
918
+ editingMessageKey.value = "";
919
+ editingContent.value = "";
920
+ }
921
+
922
+ /** 提交当前消息文本编辑,并交给页面层重新生成同一轮输出。 */
923
+ function submitMessageEdit(item: ChatMessageItem) {
924
+ if (props.sending) return;
925
+ const content = editingContent.value;
926
+ if (content === "") {
927
+ ElMessage.warning("消息内容不能为空");
928
+ return;
929
+ }
930
+ if (content === String(item.content ?? "")) {
931
+ ElMessage.info("消息内容没有变化");
932
+ return;
933
+ }
934
+ emit("messageEdit", { item, content });
935
+ cancelMessageEdit();
936
+ }
937
+
938
+ /** 判断消息操作是否需要二次确认。 */
939
+ function shouldConfirmMessageAction(action: ChatMessageAction) {
940
+ return action === "retry" || action === "delete";
941
+ }
942
+
943
+ /** 返回消息操作确认文案,避免误删或误覆盖当前气泡。 */
944
+ function resolveActionConfirmTitle(action: ChatMessageAction, item: ChatMessageItem) {
945
+ if (action === "delete") return "确认删除当前消息?";
946
+ if (item.role === "user") return "确认重新发送当前消息?";
947
+ return "重新生成会覆盖当前消息";
948
+ }
949
+
950
+ /** 统一回复来源标签配色。 */
951
+ function resolveTagClass(tone?: ReplySourceTag["tone"]) {
952
+ return tone ? `is-${tone}` : "";
953
+ }
954
+
955
+ /** 构建消息附件卡片,统一交给 Attachments / FilesCard 处理图片预览。 */
956
+ function buildMessageAttachmentItems(attachments: AiAttachment[]): FilesCardProps[] {
957
+ return attachments.map(attachment =>
958
+ buildAIAttachmentFileCard(attachment, {
959
+ maxWidth: "240px"
960
+ })
961
+ );
962
+ }
963
+ </script>
964
+
965
+ <style scoped lang="scss">
966
+ .agent-chat-panel {
967
+ position: relative;
968
+ box-sizing: border-box;
969
+ display: flex;
970
+ flex-direction: column;
971
+ min-width: 0;
972
+ height: 100%;
973
+ min-height: 0;
974
+ padding: 20px 0 24px;
975
+ overflow: hidden;
976
+ background: var(--admin-page-card-bg);
977
+ }
978
+ .agent-chat-panel.is-empty {
979
+ justify-content: center;
980
+ padding: 0;
981
+ }
982
+ .agent-chat-content {
983
+ display: flex;
984
+ flex: 1;
985
+ width: min(1180px, calc(100% - 72px));
986
+ min-height: 0;
987
+ padding-bottom: 168px;
988
+ margin: 0 auto;
989
+ overflow: hidden;
990
+ }
991
+ .agent-chat-panel.has-shortcuts .agent-chat-content {
992
+ padding-bottom: 262px;
993
+ }
994
+ .agent-chat-empty {
995
+ display: flex;
996
+ flex: 1;
997
+ flex-direction: column;
998
+ align-items: center;
999
+ justify-content: center;
1000
+ width: min(1100px, calc(100% - 96px));
1001
+ min-height: 0;
1002
+ margin: 0 auto;
1003
+ }
1004
+ .agent-chat-empty__title {
1005
+ font-size: clamp(28px, 4vw, 44px);
1006
+ font-weight: 700;
1007
+ line-height: 1.25;
1008
+ color: var(--admin-page-text-primary);
1009
+ text-align: center;
1010
+ }
1011
+ .agent-chat-empty__desc {
1012
+ margin-top: 14px;
1013
+ font-size: 14px;
1014
+ line-height: 28px;
1015
+ color: var(--admin-page-text-secondary);
1016
+ text-align: center;
1017
+ }
1018
+ .agent-chat-empty__sender {
1019
+ width: min(980px, 100%);
1020
+ margin-top: 28px;
1021
+ }
1022
+ .agent-shortcuts {
1023
+ box-sizing: border-box;
1024
+ width: min(760px, 100%);
1025
+ padding: 14px;
1026
+ margin-top: 26px;
1027
+ background: linear-gradient(180deg, rgba(64, 158, 255, 0.06), rgba(255, 255, 255, 0)), var(--admin-page-card-bg);
1028
+ border: 1px solid var(--el-border-color-lighter);
1029
+ border-radius: var(--admin-page-radius);
1030
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
1031
+ }
1032
+ .agent-shortcuts__head {
1033
+ display: flex;
1034
+ gap: 16px;
1035
+ align-items: flex-start;
1036
+ justify-content: space-between;
1037
+ min-width: 0;
1038
+ margin-bottom: 12px;
1039
+ }
1040
+ .agent-shortcuts__actions {
1041
+ display: inline-flex;
1042
+ flex: 0 0 auto;
1043
+ gap: 8px;
1044
+ align-items: center;
1045
+ }
1046
+ .agent-shortcuts__eyebrow {
1047
+ font-size: 12px;
1048
+ font-weight: 600;
1049
+ line-height: 18px;
1050
+ color: var(--el-color-primary);
1051
+ }
1052
+ .agent-shortcuts__title {
1053
+ margin-top: 2px;
1054
+ font-size: 15px;
1055
+ font-weight: 700;
1056
+ line-height: 22px;
1057
+ color: var(--admin-page-text-primary);
1058
+ }
1059
+ .agent-shortcuts__count {
1060
+ flex: 0 0 auto;
1061
+ padding: 2px 8px;
1062
+ font-size: 12px;
1063
+ line-height: 20px;
1064
+ color: var(--admin-page-text-secondary);
1065
+ background: var(--el-fill-color-light);
1066
+ border: 1px solid var(--el-border-color-lighter);
1067
+ border-radius: var(--admin-page-radius);
1068
+ }
1069
+ .agent-shortcuts__refresh {
1070
+ display: inline-flex;
1071
+ gap: 4px;
1072
+ align-items: center;
1073
+ height: 26px;
1074
+ padding: 0 8px;
1075
+ font-size: 12px;
1076
+ line-height: 1;
1077
+ color: var(--el-color-primary);
1078
+ cursor: pointer;
1079
+ background: var(--el-color-primary-light-9);
1080
+ border: 1px solid var(--el-color-primary-light-7);
1081
+ border-radius: var(--admin-page-radius);
1082
+ }
1083
+ .agent-shortcuts__refresh:hover {
1084
+ border-color: var(--el-color-primary-light-5);
1085
+ }
1086
+ .agent-shortcuts__refresh:disabled {
1087
+ cursor: not-allowed;
1088
+ opacity: 0.55;
1089
+ }
1090
+ .agent-shortcuts__refresh .el-icon {
1091
+ font-size: 13px;
1092
+ }
1093
+ .agent-shortcuts__loading {
1094
+ box-sizing: border-box;
1095
+ min-height: 70px;
1096
+ padding: 23px 12px;
1097
+ font-size: 13px;
1098
+ line-height: 22px;
1099
+ color: var(--admin-page-text-secondary);
1100
+ text-align: center;
1101
+ background: var(--el-fill-color-lighter);
1102
+ border-radius: var(--admin-page-radius);
1103
+ }
1104
+ .agent-shortcuts__grid {
1105
+ display: grid;
1106
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1107
+ gap: 10px;
1108
+ min-width: 0;
1109
+ }
1110
+ .agent-shortcut {
1111
+ box-sizing: border-box;
1112
+ display: grid;
1113
+ grid-template-columns: 34px minmax(0, 1fr);
1114
+ gap: 10px;
1115
+ align-items: center;
1116
+ min-width: 0;
1117
+ min-height: 70px;
1118
+ padding: 12px;
1119
+ color: var(--admin-page-text-primary);
1120
+ text-align: left;
1121
+ cursor: pointer;
1122
+ background: var(--admin-page-card-bg);
1123
+ border: 1px solid var(--el-border-color-lighter);
1124
+ border-radius: var(--admin-page-radius);
1125
+ transition:
1126
+ border-color 0.18s ease,
1127
+ box-shadow 0.18s ease,
1128
+ transform 0.18s ease;
1129
+ }
1130
+ .agent-shortcut:hover {
1131
+ border-color: var(--el-color-primary-light-5);
1132
+ box-shadow: 0 8px 18px rgba(64, 158, 255, 0.12);
1133
+ transform: translateY(-1px);
1134
+ }
1135
+ .agent-shortcut:disabled {
1136
+ cursor: not-allowed;
1137
+ opacity: 0.55;
1138
+ transform: none;
1139
+ }
1140
+ .agent-shortcut__icon {
1141
+ display: inline-flex;
1142
+ align-items: center;
1143
+ justify-content: center;
1144
+ width: 34px;
1145
+ height: 34px;
1146
+ font-size: 12px;
1147
+ font-weight: 700;
1148
+ line-height: 1;
1149
+ color: var(--el-color-primary);
1150
+ background: var(--el-color-primary-light-9);
1151
+ border: 1px solid var(--el-color-primary-light-7);
1152
+ border-radius: var(--admin-page-radius);
1153
+ }
1154
+ .agent-shortcut__content {
1155
+ display: flex;
1156
+ flex-direction: column;
1157
+ gap: 2px;
1158
+ min-width: 0;
1159
+ }
1160
+ .agent-shortcut__title {
1161
+ min-width: 0;
1162
+ overflow: hidden;
1163
+ font-size: 14px;
1164
+ font-weight: 600;
1165
+ line-height: 20px;
1166
+ text-overflow: ellipsis;
1167
+ white-space: nowrap;
1168
+ }
1169
+ .agent-shortcut__meta {
1170
+ min-width: 0;
1171
+ overflow: hidden;
1172
+ font-size: 12px;
1173
+ line-height: 18px;
1174
+ color: var(--admin-page-text-secondary);
1175
+ text-overflow: ellipsis;
1176
+ white-space: nowrap;
1177
+ }
1178
+ .agent-message-list {
1179
+ flex: 1;
1180
+ min-height: 0;
1181
+ padding: 8px 0 24px;
1182
+ overflow: auto;
1183
+ :deep(.elx-bubble__content-wrapper),
1184
+ :deep(.elx-bubble__content) {
1185
+ min-width: 0;
1186
+ }
1187
+ :deep(.elx-bubble--start .elx-bubble__content-wrapper .elx-bubble__footer) {
1188
+ box-sizing: border-box;
1189
+ width: fit-content;
1190
+ max-width: 100%;
1191
+ margin-top: 2px;
1192
+ }
1193
+ :deep(.elx-bubble__content-wrapper:hover .elx-bubble__footer),
1194
+ :deep(.elx-bubble__content-wrapper:focus-within .elx-bubble__footer) {
1195
+ pointer-events: auto;
1196
+ opacity: 1;
1197
+ }
1198
+ :deep(.elx-bubble__content-wrapper:hover .agent-message-footer),
1199
+ :deep(.elx-bubble__content-wrapper:focus-within .agent-message-footer) {
1200
+ pointer-events: auto;
1201
+ opacity: 1;
1202
+ visibility: visible;
1203
+ }
1204
+ :deep(.elx-bubble--start .elx-bubble__content-wrapper .elx-bubble__content) {
1205
+ box-sizing: border-box;
1206
+ width: min(720px, 100%);
1207
+ max-width: 100%;
1208
+ min-width: min(520px, 100%);
1209
+ }
1210
+ :deep(.elx-bubble--start .elx-bubble__content-wrapper .elx-bubble__content) {
1211
+ padding: 0;
1212
+ background: transparent;
1213
+ border: 0;
1214
+ box-shadow: none;
1215
+ }
1216
+ :deep(.elx-bubble--end .elx-bubble__content-wrapper .elx-bubble__content) {
1217
+ border-radius: var(--admin-page-radius);
1218
+ }
1219
+ :deep(.elx-bubble--end .elx-bubble__content-wrapper .elx-bubble__content--corner) {
1220
+ border-start-start-radius: var(--admin-page-radius);
1221
+ border-start-end-radius: var(--admin-page-radius);
1222
+ }
1223
+ :deep(.elx-bubble-list__boundary-content),
1224
+ :deep(.elx-bubble-list__embedded-item) {
1225
+ border-radius: var(--admin-page-radius);
1226
+ }
1227
+ }
1228
+ .agent-message-content {
1229
+ box-sizing: border-box;
1230
+ width: 100%;
1231
+ max-width: 100%;
1232
+ min-width: 0;
1233
+ line-height: 24px;
1234
+ margin: 6px 0 0;
1235
+ overflow-wrap: anywhere;
1236
+ }
1237
+ .agent-message-content:first-child {
1238
+ margin-top: 0;
1239
+ }
1240
+ .agent-message-content.is-user {
1241
+ width: fit-content;
1242
+ min-width: 0;
1243
+ white-space: pre-wrap;
1244
+ }
1245
+ .agent-message-content.is-thinking {
1246
+ display: inline-flex;
1247
+ gap: 8px;
1248
+ align-items: center;
1249
+ :deep(.agent-markdown) {
1250
+ flex: 0 1 auto;
1251
+ width: auto;
1252
+ }
1253
+ :deep(.agent-markdown .x-md-core) {
1254
+ width: auto;
1255
+ }
1256
+ :deep(.agent-markdown p) {
1257
+ display: inline;
1258
+ margin: 0;
1259
+ }
1260
+ }
1261
+ .agent-message-content.is-failed-ai {
1262
+ width: min(720px, 100%);
1263
+ max-width: 100%;
1264
+ }
1265
+ .agent-message-body {
1266
+ display: flex;
1267
+ flex-direction: column;
1268
+ align-items: flex-start;
1269
+ gap: 0;
1270
+ width: 100%;
1271
+ min-width: min(360px, 100%);
1272
+ max-width: 100%;
1273
+ }
1274
+ .agent-message-body.is-user {
1275
+ width: fit-content;
1276
+ min-width: 0;
1277
+ }
1278
+ .agent-message-meta {
1279
+ display: inline-flex;
1280
+ gap: 8px;
1281
+ align-items: center;
1282
+ width: 100%;
1283
+ max-width: 100%;
1284
+ font-size: 12px;
1285
+ color: var(--admin-page-text-secondary);
1286
+ }
1287
+ .agent-message-meta__tag {
1288
+ padding: 2px 8px;
1289
+ background: var(--el-fill-color-light);
1290
+ border-radius: var(--admin-page-radius);
1291
+ }
1292
+ .agent-message-meta__tag.is-primary {
1293
+ color: var(--el-color-primary);
1294
+ }
1295
+ .agent-message-meta__tag.is-success {
1296
+ color: var(--el-color-success);
1297
+ }
1298
+ .agent-message-meta__tag.is-warning {
1299
+ color: var(--el-color-warning);
1300
+ }
1301
+ .agent-message-meta__tag.is-info {
1302
+ color: var(--admin-page-text-secondary);
1303
+ }
1304
+ .agent-message-meta__model {
1305
+ opacity: 0.85;
1306
+ }
1307
+ .agent-message-meta__time {
1308
+ opacity: 0.85;
1309
+ }
1310
+ .agent-message-error {
1311
+ box-sizing: border-box;
1312
+ width: 100%;
1313
+ max-width: 100%;
1314
+ padding: 12px 14px;
1315
+ color: var(--admin-page-text-primary);
1316
+ background: var(--el-color-danger-light-9);
1317
+ border: 1px solid var(--el-color-danger-light-7);
1318
+ border-radius: var(--admin-page-radius);
1319
+ }
1320
+ .agent-message-error__title {
1321
+ font-size: 14px;
1322
+ font-weight: 700;
1323
+ line-height: 22px;
1324
+ color: var(--el-color-danger);
1325
+ }
1326
+ .agent-message-error__content {
1327
+ margin-top: 6px;
1328
+ font-size: 13px;
1329
+ line-height: 22px;
1330
+ color: var(--admin-page-text-regular);
1331
+ word-break: break-word;
1332
+ }
1333
+ .agent-message-error__detail {
1334
+ margin-top: 8px;
1335
+ border: 0;
1336
+ :deep(.el-collapse-item__header) {
1337
+ height: 28px;
1338
+ font-size: 12px;
1339
+ color: var(--admin-page-text-secondary);
1340
+ background: transparent;
1341
+ border: 0;
1342
+ }
1343
+ :deep(.el-collapse-item__wrap) {
1344
+ background: transparent;
1345
+ border: 0;
1346
+ }
1347
+ :deep(.el-collapse-item__content) {
1348
+ padding-bottom: 0;
1349
+ }
1350
+ pre {
1351
+ max-height: 180px;
1352
+ padding: 8px;
1353
+ margin: 0;
1354
+ overflow: auto;
1355
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1356
+ font-size: 12px;
1357
+ line-height: 18px;
1358
+ color: var(--admin-page-text-secondary);
1359
+ white-space: pre-wrap;
1360
+ word-break: break-word;
1361
+ background: var(--el-fill-color-blank);
1362
+ border: 1px solid var(--el-border-color-light);
1363
+ border-radius: var(--admin-page-radius);
1364
+ }
1365
+ }
1366
+ .agent-thinking-dots {
1367
+ display: inline-flex;
1368
+ flex: 0 0 auto;
1369
+ gap: 4px;
1370
+ align-items: center;
1371
+ color: var(--admin-page-text-secondary);
1372
+ i {
1373
+ display: inline-block;
1374
+ width: 6px;
1375
+ height: 6px;
1376
+ background: currentcolor;
1377
+ border-radius: 50%;
1378
+ animation: thinking-bounce 1.2s infinite ease-in-out;
1379
+ }
1380
+ i:nth-child(2) {
1381
+ animation-delay: 0.15s;
1382
+ }
1383
+ i:nth-child(3) {
1384
+ animation-delay: 0.3s;
1385
+ }
1386
+ }
1387
+ .agent-message-attachments {
1388
+ :deep(.elx-files-card) {
1389
+ border-radius: var(--admin-page-radius);
1390
+ }
1391
+ :deep(.elx-files-card-img),
1392
+ :deep(.elx-files-card__image-preview),
1393
+ :deep(.elx-files-card-delete-icon) {
1394
+ border-radius: var(--admin-page-radius);
1395
+ }
1396
+ }
1397
+ .agent-message-tools {
1398
+ width: 100%;
1399
+ min-width: 0;
1400
+ max-width: 100%;
1401
+ margin-top: 8px;
1402
+ border: 0;
1403
+ :deep(.el-collapse-item + .el-collapse-item) {
1404
+ margin-top: 8px;
1405
+ }
1406
+ :deep(.el-collapse-item) {
1407
+ position: relative;
1408
+ overflow: hidden;
1409
+ border: 1px solid var(--el-border-color-lighter);
1410
+ border-radius: var(--admin-page-radius);
1411
+ }
1412
+ :deep(.el-collapse-item::after) {
1413
+ position: absolute;
1414
+ right: 0;
1415
+ bottom: 0;
1416
+ left: 0;
1417
+ z-index: 2;
1418
+ height: 1px;
1419
+ pointer-events: none;
1420
+ content: "";
1421
+ background: var(--el-border-color-lighter);
1422
+ }
1423
+ :deep(.el-collapse-item__header) {
1424
+ box-sizing: border-box;
1425
+ display: flex;
1426
+ min-width: 0;
1427
+ min-height: 44px;
1428
+ height: auto;
1429
+ padding: 0 12px;
1430
+ overflow: hidden;
1431
+ font-size: 13px;
1432
+ line-height: normal;
1433
+ color: var(--admin-page-text-regular);
1434
+ background: transparent;
1435
+ border: 0;
1436
+ }
1437
+ :deep(.el-collapse-item__arrow) {
1438
+ flex: 0 0 auto;
1439
+ margin-left: 10px;
1440
+ color: var(--admin-page-text-secondary);
1441
+ }
1442
+ :deep(.el-collapse-item__wrap) {
1443
+ overflow: hidden;
1444
+ background: transparent;
1445
+ border: 0;
1446
+ }
1447
+ :deep(.el-collapse-item__content) {
1448
+ padding: 0;
1449
+ }
1450
+ }
1451
+ .agent-message-tool-title {
1452
+ display: grid;
1453
+ grid-template-columns: minmax(0, 1fr) max-content max-content;
1454
+ gap: 8px;
1455
+ align-items: center;
1456
+ width: 100%;
1457
+ max-width: 100%;
1458
+ min-width: 0;
1459
+ }
1460
+ .agent-message-tool-title__main {
1461
+ display: grid;
1462
+ grid-template-columns: max-content minmax(0, 1fr);
1463
+ gap: 8px;
1464
+ align-items: center;
1465
+ min-width: 0;
1466
+ }
1467
+ .agent-message-tool-title__text {
1468
+ display: flex;
1469
+ flex-direction: column;
1470
+ gap: 1px;
1471
+ min-width: 0;
1472
+ }
1473
+ .agent-message-tool-title__label {
1474
+ min-width: 0;
1475
+ overflow: hidden;
1476
+ line-height: 18px;
1477
+ text-overflow: ellipsis;
1478
+ white-space: nowrap;
1479
+ }
1480
+ .agent-message-tool-title__name {
1481
+ min-width: 0;
1482
+ overflow: hidden;
1483
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1484
+ font-size: 11px;
1485
+ line-height: 14px;
1486
+ color: var(--admin-page-text-secondary);
1487
+ text-overflow: ellipsis;
1488
+ white-space: nowrap;
1489
+ }
1490
+ .agent-message-tool-title__icon {
1491
+ display: inline-flex;
1492
+ flex: 0 0 auto;
1493
+ align-items: center;
1494
+ font-size: 14px;
1495
+ color: var(--admin-page-text-secondary);
1496
+ }
1497
+ .agent-message-tool-title__status {
1498
+ display: inline-flex;
1499
+ flex: 0 0 auto;
1500
+ gap: 4px;
1501
+ align-items: center;
1502
+ font-size: 12px;
1503
+ line-height: 20px;
1504
+ color: var(--el-color-success);
1505
+ }
1506
+ .agent-message-tool-title__status.is-error {
1507
+ color: var(--el-color-danger);
1508
+ }
1509
+ .agent-message-tool-copy {
1510
+ display: inline-flex;
1511
+ align-items: center;
1512
+ justify-content: center;
1513
+ width: 24px;
1514
+ height: 24px;
1515
+ padding: 0;
1516
+ color: var(--admin-page-text-secondary);
1517
+ cursor: pointer;
1518
+ background: transparent;
1519
+ border: 0;
1520
+ border-radius: var(--admin-page-radius);
1521
+ transition:
1522
+ color 0.2s ease,
1523
+ background-color 0.2s ease;
1524
+ &:hover {
1525
+ color: var(--el-color-primary);
1526
+ background: var(--el-fill-color-light);
1527
+ }
1528
+ .el-icon {
1529
+ font-size: 15px;
1530
+ }
1531
+ }
1532
+ .agent-message-tool-payload {
1533
+ box-sizing: border-box;
1534
+ display: flex;
1535
+ flex-direction: column;
1536
+ min-width: 0;
1537
+ max-height: 220px;
1538
+ padding: 0 0 1px;
1539
+ overflow: auto;
1540
+ border-top: 1px solid var(--el-border-color-lighter);
1541
+ }
1542
+ .agent-message-tool-payload__section {
1543
+ box-sizing: border-box;
1544
+ min-width: 0;
1545
+ padding: 10px 12px;
1546
+ }
1547
+ .agent-message-tool-payload__section + .agent-message-tool-payload__section {
1548
+ border-top: 1px solid var(--el-border-color-lighter);
1549
+ }
1550
+ .agent-message-tool-payload__label {
1551
+ margin-bottom: 2px;
1552
+ font-size: 11px;
1553
+ font-weight: 700;
1554
+ line-height: 16px;
1555
+ letter-spacing: 0.02em;
1556
+ color: var(--admin-page-text-secondary);
1557
+ }
1558
+ .agent-message-tool-payload pre {
1559
+ box-sizing: border-box;
1560
+ padding: 0;
1561
+ margin: 0;
1562
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1563
+ font-size: 12px;
1564
+ line-height: 20px;
1565
+ color: var(--admin-page-text-regular);
1566
+ white-space: pre-wrap;
1567
+ word-break: break-word;
1568
+ background: transparent;
1569
+ border: 0;
1570
+ }
1571
+ .agent-message-footer {
1572
+ box-sizing: border-box;
1573
+ display: grid;
1574
+ grid-template-columns: minmax(0, 1fr);
1575
+ gap: 8px;
1576
+ align-items: center;
1577
+ width: var(--agent-message-footer-width, fit-content);
1578
+ max-width: 100%;
1579
+ min-width: min(360px, 100%);
1580
+ padding: 0;
1581
+ margin-top: 0;
1582
+ pointer-events: none;
1583
+ opacity: 0;
1584
+ transition:
1585
+ opacity 0.16s ease,
1586
+ visibility 0.16s ease;
1587
+ visibility: hidden;
1588
+ }
1589
+ .agent-message-footer:hover,
1590
+ .agent-message-footer:focus-within,
1591
+ .agent-message-footer.is-editing {
1592
+ pointer-events: auto;
1593
+ opacity: 1;
1594
+ visibility: visible;
1595
+ }
1596
+ .agent-message-footer.is-editing {
1597
+ grid-template-columns: minmax(0, 1fr);
1598
+ row-gap: 6px;
1599
+ }
1600
+ .agent-message-footer.is-user {
1601
+ grid-template-columns: auto;
1602
+ justify-content: flex-end;
1603
+ min-width: 0;
1604
+ padding: 0;
1605
+ }
1606
+ .agent-message-footer.is-user.is-editing {
1607
+ grid-template-columns: minmax(0, 1fr);
1608
+ justify-content: stretch;
1609
+ justify-self: end;
1610
+ width: min(380px, 100%);
1611
+ min-width: min(320px, 100%);
1612
+ }
1613
+ .agent-message-actions {
1614
+ display: inline-flex;
1615
+ gap: 8px;
1616
+ align-items: center;
1617
+ justify-content: flex-start;
1618
+ min-width: 0;
1619
+ }
1620
+ .agent-message-actions.is-user {
1621
+ justify-content: flex-end;
1622
+ }
1623
+ .agent-message-edit {
1624
+ box-sizing: border-box;
1625
+ width: 100%;
1626
+ min-width: min(320px, 100%);
1627
+ padding: 10px;
1628
+ background: var(--el-fill-color-light);
1629
+ border: 1px solid var(--el-border-color-light);
1630
+ border-radius: var(--admin-page-radius);
1631
+ }
1632
+ .agent-message-edit__input {
1633
+ width: 100%;
1634
+ :deep(.el-textarea__inner) {
1635
+ min-height: 82px !important;
1636
+ padding: 8px 10px;
1637
+ font-size: 14px;
1638
+ line-height: 24px;
1639
+ color: var(--admin-page-text-primary);
1640
+ background: var(--admin-page-card-bg);
1641
+ border-radius: var(--admin-page-radius);
1642
+ box-shadow: 0 0 0 1px var(--el-border-color-light) inset;
1643
+ }
1644
+ }
1645
+ .agent-message-edit__footer {
1646
+ display: flex;
1647
+ gap: 8px;
1648
+ justify-content: flex-end;
1649
+ margin-top: 8px;
1650
+ }
1651
+ .agent-message-action {
1652
+ display: inline-flex;
1653
+ align-items: center;
1654
+ justify-content: center;
1655
+ width: 28px;
1656
+ height: 28px;
1657
+ color: var(--admin-page-text-secondary);
1658
+ cursor: pointer;
1659
+ background: transparent;
1660
+ border: 0;
1661
+ border-radius: var(--admin-page-radius);
1662
+ transition:
1663
+ color 0.2s ease,
1664
+ background-color 0.2s ease,
1665
+ opacity 0.2s ease;
1666
+ &:hover {
1667
+ color: var(--el-color-primary);
1668
+ background: var(--el-fill-color-light);
1669
+ }
1670
+ &:disabled {
1671
+ cursor: not-allowed;
1672
+ opacity: 0.45;
1673
+ }
1674
+ }
1675
+ .agent-message-action :deep(.el-icon) {
1676
+ width: 1em;
1677
+ height: 1em;
1678
+ font-size: 16px;
1679
+ line-height: 1;
1680
+ }
1681
+ .agent-message-action :deep(svg) {
1682
+ display: block;
1683
+ width: 1em;
1684
+ height: 1em;
1685
+ }
1686
+ .agent-message-action :deep(.agent-message-action__custom-icon) {
1687
+ width: 16px;
1688
+ height: 16px;
1689
+ }
1690
+ .agent-message-action.agent-message-runtime-trigger {
1691
+ flex: 0 0 auto;
1692
+ gap: 6px;
1693
+ width: fit-content;
1694
+ min-width: 0;
1695
+ max-width: 150px;
1696
+ padding: 0 6px;
1697
+ overflow: hidden;
1698
+ font-size: 12px;
1699
+ :deep(.el-icon) {
1700
+ flex: 0 0 auto;
1701
+ font-size: 15px;
1702
+ }
1703
+ span {
1704
+ max-width: 70px;
1705
+ overflow: hidden;
1706
+ text-overflow: ellipsis;
1707
+ white-space: nowrap;
1708
+ }
1709
+ }
1710
+ .agent-composer-dock {
1711
+ position: absolute;
1712
+ right: 0;
1713
+ bottom: 24px;
1714
+ left: 0;
1715
+ z-index: 1;
1716
+ width: min(760px, calc(100% - 72px));
1717
+ padding: 0;
1718
+ margin: 0 auto;
1719
+ background: var(--admin-page-card-bg);
1720
+ }
1721
+ .agent-shortcuts--compact {
1722
+ width: 100%;
1723
+ padding: 10px;
1724
+ margin: 0 0 10px;
1725
+ background: linear-gradient(180deg, rgba(64, 158, 255, 0.05), rgba(255, 255, 255, 0)), var(--admin-page-card-bg);
1726
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
1727
+ }
1728
+ .agent-shortcuts--compact .agent-shortcuts__head {
1729
+ align-items: center;
1730
+ margin-bottom: 8px;
1731
+ }
1732
+ .agent-shortcuts--compact .agent-shortcuts__eyebrow {
1733
+ display: none;
1734
+ }
1735
+ .agent-shortcuts--compact .agent-shortcuts__title {
1736
+ margin-top: 0;
1737
+ font-size: 13px;
1738
+ line-height: 20px;
1739
+ }
1740
+ .agent-shortcuts--compact .agent-shortcuts__actions {
1741
+ gap: 6px;
1742
+ }
1743
+ .agent-shortcuts--compact .agent-shortcuts__grid {
1744
+ display: grid;
1745
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1746
+ gap: 8px;
1747
+ min-width: 0;
1748
+ }
1749
+ .agent-shortcuts--compact .agent-shortcut {
1750
+ grid-template-columns: 28px minmax(0, 1fr);
1751
+ min-height: 56px;
1752
+ padding: 8px 10px;
1753
+ }
1754
+ .agent-shortcuts--compact .agent-shortcut__icon {
1755
+ width: 28px;
1756
+ height: 28px;
1757
+ font-size: 11px;
1758
+ }
1759
+ .agent-shortcuts--compact .agent-shortcut__title {
1760
+ font-size: 13px;
1761
+ line-height: 18px;
1762
+ }
1763
+ .agent-shortcuts--compact .agent-shortcut__meta {
1764
+ font-size: 11px;
1765
+ line-height: 16px;
1766
+ }
1767
+
1768
+ @media screen and (width <= 768px) {
1769
+ .agent-chat-panel {
1770
+ padding: 0 0 22px;
1771
+ }
1772
+ .agent-chat-panel.is-empty {
1773
+ justify-content: center;
1774
+ padding: 24px 0;
1775
+ }
1776
+ .agent-chat-content,
1777
+ .agent-composer-dock {
1778
+ width: calc(100% - 36px);
1779
+ }
1780
+ .agent-chat-empty {
1781
+ width: calc(100% - 36px);
1782
+ }
1783
+ .agent-shortcuts__grid {
1784
+ grid-template-columns: minmax(0, 1fr);
1785
+ }
1786
+ .agent-shortcuts__head {
1787
+ align-items: stretch;
1788
+ }
1789
+ .agent-shortcuts__actions {
1790
+ align-items: flex-end;
1791
+ flex-direction: column;
1792
+ gap: 6px;
1793
+ }
1794
+ .agent-shortcuts--compact .agent-shortcuts__grid {
1795
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1796
+ }
1797
+ .agent-chat-empty__title {
1798
+ font-size: 30px;
1799
+ }
1800
+ .agent-chat-empty__sender {
1801
+ width: 100%;
1802
+ }
1803
+ }
1804
+
1805
+ @keyframes thinking-bounce {
1806
+ 0%,
1807
+ 80%,
1808
+ 100% {
1809
+ opacity: 0.35;
1810
+ transform: translateY(0);
1811
+ }
1812
+ 40% {
1813
+ opacity: 1;
1814
+ transform: translateY(-2px);
1815
+ }
1816
+ }
1817
+ </style>
1818
+
1819
+ <style lang="scss">
1820
+ .agent-message-runtime-popover {
1821
+ .agent-runtime-detail {
1822
+ min-width: 0;
1823
+ font-size: 12px;
1824
+ color: var(--admin-page-text-secondary);
1825
+ }
1826
+ .agent-runtime-detail__title {
1827
+ margin-bottom: 10px;
1828
+ font-size: 13px;
1829
+ font-weight: 700;
1830
+ line-height: 20px;
1831
+ color: var(--admin-page-text-primary);
1832
+ }
1833
+ .agent-runtime-detail__section + .agent-runtime-detail__section {
1834
+ padding-top: 10px;
1835
+ margin-top: 10px;
1836
+ border-top: 1px solid var(--el-border-color-lighter);
1837
+ }
1838
+ .agent-runtime-detail__section-title {
1839
+ margin-bottom: 6px;
1840
+ font-weight: 600;
1841
+ line-height: 18px;
1842
+ color: var(--admin-page-text-regular);
1843
+ }
1844
+ .agent-runtime-detail__row {
1845
+ display: flex;
1846
+ gap: 12px;
1847
+ align-items: center;
1848
+ justify-content: space-between;
1849
+ min-height: 22px;
1850
+ line-height: 22px;
1851
+ }
1852
+ .agent-runtime-detail__row strong {
1853
+ font-weight: 600;
1854
+ font-variant-numeric: tabular-nums;
1855
+ color: var(--admin-page-text-primary);
1856
+ }
1857
+ .agent-runtime-detail__row.is-total strong {
1858
+ color: var(--el-color-primary);
1859
+ }
1860
+ }
1861
+ </style>