@liujitcn/kratos-admin-system 0.0.64 → 0.0.66

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 (50) hide show
  1. package/README.md +5 -3
  2. package/dist/package/declarations/src/api/base/v1/ai_knowledge.d.ts +7 -0
  3. package/dist/package/declarations/src/api/system/admin/v1/ai_provider.d.ts +5 -1
  4. package/dist/package/declarations/src/api/system/admin/v1/ai_query.d.ts +10 -0
  5. package/dist/package/declarations/src/api/system/admin/v1/ai_session.d.ts +9 -0
  6. package/dist/package/declarations/src/locales/generated.d.ts +1405 -1089
  7. package/dist/package/declarations/src/rpc/base/v1/ai_knowledge.d.ts +22 -0
  8. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +2 -0
  9. package/dist/package/declarations/src/rpc/base/v1/ai_provider.d.ts +13 -6
  10. package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +85 -4
  11. package/dist/package/declarations/src/rpc/system/admin/v1/ai_query.d.ts +73 -0
  12. package/dist/package/declarations/src/rpc/system/admin/v1/ai_session.d.ts +114 -0
  13. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +9 -2
  14. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +7 -0
  15. package/dist/package/declarations/src/views/ai/chat/modelDisplay.d.ts +5 -0
  16. package/dist/package/declarations/src/views/ai/chat/modelSelection.d.ts +9 -0
  17. package/dist/package/declarations/src/views/ai/query/index.vue.d.ts +3 -0
  18. package/dist/package/declarations/src/views/ai/session/index.vue.d.ts +3 -0
  19. package/dist/package/src/api/base/v1/ai_knowledge.ts +22 -0
  20. package/dist/package/src/api/system/admin/v1/ai_provider.ts +14 -0
  21. package/dist/package/src/api/system/admin/v1/ai_query.ts +25 -0
  22. package/dist/package/src/api/system/admin/v1/ai_session.ts +25 -0
  23. package/dist/package/src/components/account/ChangePasswordForm.vue +1 -1
  24. package/dist/package/src/components/tenant-project/TenantProjectSelect.vue +21 -4
  25. package/dist/package/src/locales/en-US.json +351 -272
  26. package/dist/package/src/locales/ja-JP.json +351 -272
  27. package/dist/package/src/locales/zh-CN.json +361 -282
  28. package/dist/package/src/locales/zh-TW.json +351 -272
  29. package/dist/package/src/rpc/base/v1/ai_knowledge.ts +33 -0
  30. package/dist/package/src/rpc/base/v1/ai_message.ts +2 -0
  31. package/dist/package/src/rpc/base/v1/ai_provider.ts +14 -6
  32. package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +93 -4
  33. package/dist/package/src/rpc/system/admin/v1/ai_query.ts +89 -0
  34. package/dist/package/src/rpc/system/admin/v1/ai_session.ts +131 -0
  35. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +22 -1
  36. package/dist/package/src/views/ai/chat/components/XSender.vue +367 -71
  37. package/dist/package/src/views/ai/chat/index.vue +46 -8
  38. package/dist/package/src/views/ai/chat/modelDisplay.ts +67 -0
  39. package/dist/package/src/views/ai/chat/modelSelection.ts +31 -0
  40. package/dist/package/src/views/ai/query/index.vue +222 -0
  41. package/dist/package/src/views/ai/session/index.vue +422 -0
  42. package/dist/package/src/views/base/ai-provider/index.vue +301 -154
  43. package/dist/package/src/views/base/api-rate-limit-policy/index.vue +4 -3
  44. package/dist/package/src/views/base/message-provider/index.vue +9 -5
  45. package/dist/package/src/views/base/message-template/index.vue +2 -2
  46. package/dist/package/src/views/base/oauth-provider/index.vue +2 -2
  47. package/dist/package/src/views/base/rate-limit-rule/index.vue +8 -3
  48. package/dist/package/src/views/base/tenant-project-grant/index.vue +9 -4
  49. package/dist/package/src/views/tool/code-gen/config.ts +1 -0
  50. package/package.json +2 -2
@@ -0,0 +1,422 @@
1
+ <template>
2
+ <div class="table-box">
3
+ <ProTable ref="table" row-key="id" :columns="columns" :request-api="requestTable" />
4
+
5
+ <el-drawer
6
+ v-model="detail.visible"
7
+ :title="t('system.ai.session.detail.title')"
8
+ size="min(920px, calc(100vw - 32px))"
9
+ destroy-on-close
10
+ >
11
+ <div v-if="detail.row" class="ai-session-detail">
12
+ <div class="ai-session-detail__head">
13
+ <div class="ai-session-detail__title">{{ detail.row.title }}</div>
14
+ <div v-if="detail.row.summary" class="ai-session-detail__summary">{{ detail.row.summary }}</div>
15
+ <div class="ai-session-detail__meta">
16
+ <el-tag size="small" effect="plain">{{ terminalLabel(detail.row.terminal) }}</el-tag>
17
+ <span>{{ t("system.ai.session.field.user") }}:{{ displayUser(detail.row) }}</span>
18
+ <span>{{ t("system.ai.session.field.message_count") }}:{{ detail.row.message_count }}</span>
19
+ <span>{{ t("system.ai.session.field.total_tokens") }}:{{ detail.row.total_tokens }}</span>
20
+ <span>{{ t("common.field.updated_at") }}:{{ detail.row.updated_at }}</span>
21
+ </div>
22
+ </div>
23
+
24
+ <div v-loading="detail.loading" class="ai-session-detail__chat">
25
+ <div v-if="!detail.list.length" class="ai-session-detail__empty">
26
+ {{ t("system.ai.session.message.empty") }}
27
+ </div>
28
+ <BubbleList v-else class="ai-session-detail__bubbles" :list="detailBubbles" max-height="52vh" :auto-scroll="false">
29
+ <template #content="{ item }">
30
+ <div class="ai-session-detail__bubble" :class="{ 'is-user': item.role === 'user' }">
31
+ <div
32
+ v-if="item.role !== 'user'"
33
+ class="ai-session-detail__message-meta"
34
+ >
35
+ <el-tag :type="statusTagType(item.status)" size="small" effect="plain">{{ statusLabel(item.status) }}</el-tag>
36
+ <span v-if="item.model">{{ t("system.ai.session.field.model") }}:{{ item.model }}</span>
37
+ <span>{{ t("system.ai.session.field.input_tokens") }}:{{ item.inputTokens }}</span>
38
+ <span>{{ t("system.ai.session.field.output_tokens") }}:{{ item.outputTokens }}</span>
39
+ <span>{{ t("system.ai.session.field.total_tokens") }}:{{ item.totalTokens }}</span>
40
+ <span>{{ t("common.field.created_at") }}:{{ item.createdAtText }}</span>
41
+ </div>
42
+ <div class="ai-session-detail__content" :class="{ 'is-user': item.role === 'user' }">
43
+ <AiMarkdown v-if="item.role !== 'user'" :content="item.content" />
44
+ <span v-else>{{ item.content || t("system.ai.session.detail.no_content") }}</span>
45
+ </div>
46
+ <div v-if="item.attachments?.length" class="ai-session-detail__attachments">
47
+ <Attachments :items="buildMessageAttachmentItems(item.attachments)" overflow="wrap" :hide-upload="true" />
48
+ </div>
49
+ </div>
50
+ </template>
51
+ </BubbleList>
52
+ </div>
53
+
54
+ <el-pagination
55
+ v-model:current-page="detail.pageNum"
56
+ :page-size="detail.pageSize"
57
+ :total="detail.total"
58
+ layout="total, prev, pager, next"
59
+ background
60
+ class="ai-session-detail__pagination"
61
+ @current-change="loadDetailMessages"
62
+ />
63
+ </div>
64
+ </el-drawer>
65
+
66
+ <ProDialog
67
+ v-model="usage.visible"
68
+ :title="usage.row ? t('system.ai.session.usage.title') + ' - ' + usage.row.title : t('system.ai.session.usage.title')"
69
+ width="min(1320px, calc(100vw - 32px))"
70
+ :show-footer="false"
71
+ >
72
+ <div v-loading="usage.loading" class="ai-session-usage">
73
+ <el-table :data="usage.list" row-key="id" border size="default">
74
+ <el-table-column prop="model" :label="t('system.ai.session.field.model')" min-width="160" />
75
+ <el-table-column prop="input_tokens" :label="t('system.ai.session.field.input_tokens')" width="110" align="right" />
76
+ <el-table-column prop="output_tokens" :label="t('system.ai.session.field.output_tokens')" width="110" align="right" />
77
+ <el-table-column prop="cache_tokens" :label="t('system.ai.session.field.cache_tokens')" width="110" align="right" />
78
+ <el-table-column prop="total_tokens" :label="t('system.ai.session.field.total_tokens')" width="110" align="right" />
79
+ <el-table-column prop="first_token_ms" :label="t('system.ai.session.field.first_token_ms')" width="120" align="right" />
80
+ <el-table-column prop="duration_ms" :label="t('system.ai.session.field.duration_ms')" width="100" align="right" />
81
+ <el-table-column :label="t('system.ai.session.field.status')" width="90" align="center">
82
+ <template #default="{ row }">
83
+ <el-tag :type="statusTagType(row.status)" size="small" effect="plain">{{ statusLabel(row.status) }}</el-tag>
84
+ </template>
85
+ </el-table-column>
86
+ <el-table-column prop="created_at" :label="t('common.field.created_at')" width="185" align="center" />
87
+ </el-table>
88
+ </div>
89
+ </ProDialog>
90
+ </div>
91
+ </template>
92
+
93
+ <script setup lang="ts">
94
+ import { computed, reactive, ref } from "vue";
95
+ import { DataAnalysis, View } from "@element-plus/icons-vue";
96
+ import { Attachments, BubbleList } from "vue-element-plus-x";
97
+ import type { FilesCardProps } from "vue-element-plus-x/types/FilesCard";
98
+ import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
99
+ import ProDialog from "@liujitcn/kratos-admin-core/components/Dialog/ProDialog.vue";
100
+ import type { ColumnProps, EnumProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
101
+ import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
102
+ import { buildPageRequest } from "@liujitcn/kratos-admin-core/table";
103
+ import { t } from "@liujitcn/kratos-admin-core";
104
+ import { defAiSessionService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/ai_session";
105
+ import type { AiSessionMessageRecord, AiSessionRecord, PageAiSessionRequest } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/ai_session";
106
+ import type { AiAttachment } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
107
+ import { AiMessageStatus } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
108
+ import { Terminal } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
109
+ import AiMarkdown from "../chat/components/AiMarkdown.vue";
110
+ import { buildAIAttachmentFileCard } from "../chat/attachment";
111
+ import type { ChatMessageItem } from "../chat/types";
112
+
113
+ defineOptions({ name: "AiSession", inheritAttrs: false });
114
+
115
+ const PAGE_SIZE = 20;
116
+ const MAX_USAGE_MESSAGES = 2000;
117
+ const USAGE_PAGE_SIZE = 100;
118
+
119
+ /** 会话详情气泡展示项,在聊天气泡基础上补充管理端展示信息。 */
120
+ interface DetailBubbleItem extends ChatMessageItem {
121
+ /** 创建时间文本。 */
122
+ createdAtText?: string;
123
+ /** 输入 Token 数。 */
124
+ inputTokens?: number;
125
+ /** 输出 Token 数。 */
126
+ outputTokens?: number;
127
+ /** 总 Token 数。 */
128
+ totalTokens?: number;
129
+ }
130
+
131
+ const { BUTTONS } = useAuthButtons();
132
+ const table = ref<ProTableInstance>();
133
+ const detail = reactive({
134
+ visible: false,
135
+ row: null as AiSessionRecord | null,
136
+ list: [] as AiSessionMessageRecord[],
137
+ loading: false,
138
+ pageNum: 1,
139
+ pageSize: PAGE_SIZE,
140
+ total: 0
141
+ });
142
+ const usage = reactive({
143
+ visible: false,
144
+ row: null as AiSessionRecord | null,
145
+ list: [] as AiSessionMessageRecord[],
146
+ loading: false
147
+ });
148
+
149
+ /** 终端枚举选项。 */
150
+ const terminalEnum = computed<EnumProps[]>(() => [
151
+ { label: t("system.ai.session.terminal.admin"), value: Terminal.TERMINAL_ADMIN, tagType: "primary" },
152
+ { label: t("system.ai.session.terminal.app"), value: Terminal.TERMINAL_APP, tagType: "success" }
153
+ ]);
154
+
155
+ const columns = computed<ColumnProps[]>(() => [
156
+ { prop: "id", label: t("system.ai.session.field.id"), width: 90, align: "center" },
157
+ { prop: "title", label: t("system.ai.session.field.title"), minWidth: 200, showOverflowTooltip: true, search: { el: "input" } },
158
+ {
159
+ prop: "user",
160
+ label: t("system.ai.session.field.user"),
161
+ minWidth: 140,
162
+ showOverflowTooltip: true,
163
+ render: scope => displayUser(scope.row as AiSessionRecord)
164
+ },
165
+ {
166
+ prop: "terminal",
167
+ label: t("system.ai.session.field.terminal"),
168
+ width: 110,
169
+ align: "center",
170
+ enum: terminalEnum,
171
+ isFilterEnum: true,
172
+ tag: true,
173
+ search: { el: "select", props: { clearable: true } }
174
+ },
175
+ { prop: "message_count", label: t("system.ai.session.field.message_count"), width: 100, align: "right" },
176
+ { prop: "input_tokens", label: t("system.ai.session.field.input_tokens"), width: 110, align: "right" },
177
+ { prop: "output_tokens", label: t("system.ai.session.field.output_tokens"), width: 110, align: "right" },
178
+ { prop: "total_tokens", label: t("system.ai.session.field.total_tokens"), width: 110, align: "right" },
179
+ { prop: "updated_at", label: t("common.field.updated_at"), minWidth: 170, align: "center" },
180
+ {
181
+ prop: "operation",
182
+ label: t("common.field.operation"),
183
+ cellType: "actions",
184
+ fixed: "right",
185
+ width: 170,
186
+ actions: [
187
+ { label: t("common.action.detail"), link: true, icon: View, hidden: () => !BUTTONS.value["base:ai-session:detail"], onClick: scope => openDetail(scope.row as AiSessionRecord) },
188
+ { label: t("system.ai.session.action.usage"), link: true, icon: DataAnalysis, hidden: () => !BUTTONS.value["base:ai-session:usage"], onClick: scope => openUsage(scope.row as AiSessionRecord) }
189
+ ]
190
+ }
191
+ ]);
192
+
193
+ /** 详情抽屉内按聊天窗口结构拆分的提问与回复气泡。 */
194
+ const detailBubbles = computed<DetailBubbleItem[]>(() =>
195
+ detail.list.flatMap(message => {
196
+ const attachments = (message.attachments ?? []).map(item => ({
197
+ id: "",
198
+ name: item.name,
199
+ size: item.size,
200
+ url: item.url,
201
+ mime_type: item.mime_type
202
+ })) as AiAttachment[];
203
+ const question: DetailBubbleItem = {
204
+ key: `question-${message.id}`,
205
+ role: "user",
206
+ kind: "text",
207
+ content: String(message.input_content ?? ""),
208
+ placement: "end",
209
+ attachments: attachments
210
+ } as DetailBubbleItem;
211
+ const answer: DetailBubbleItem = {
212
+ key: `answer-${message.id}`,
213
+ role: "ai",
214
+ kind: "text",
215
+ content: String(message.output_content ?? ""),
216
+ placement: "start",
217
+ model: message.model,
218
+ status: message.status,
219
+ createdAtText: message.created_at,
220
+ inputTokens: message.input_tokens,
221
+ outputTokens: message.output_tokens,
222
+ totalTokens: message.total_tokens
223
+ } as DetailBubbleItem;
224
+ return [question, answer];
225
+ })
226
+ );
227
+
228
+ /** 请求AI会话表格数据。 */
229
+ async function requestTable(params: Record<string, unknown>) {
230
+ const request = buildPageRequest<PageAiSessionRequest>(params as unknown as PageAiSessionRequest);
231
+ if (!request.terminal) delete request.terminal;
232
+ const data = await defAiSessionService.PageAiSession(request);
233
+ return { data: { list: data.sessions ?? [], total: data.total } };
234
+ }
235
+
236
+ /** 展示会话所属用户,昵称缺失时回退账号,用户已删除时标注未知用户。 */
237
+ function displayUser(row: AiSessionRecord): string {
238
+ return row.nick_name || row.user_name || t("system.ai.session.unknown_user", { id: row.user_id ?? 0 });
239
+ }
240
+
241
+ /** 转换终端枚举为展示文案。 */
242
+ function terminalLabel(terminal?: Terminal): string {
243
+ return terminal === Terminal.TERMINAL_APP ? t("system.ai.session.terminal.app") : t("system.ai.session.terminal.admin");
244
+ }
245
+
246
+ /** 转换消息状态为展示文案。 */
247
+ function statusLabel(status?: AiMessageStatus): string {
248
+ switch (status) {
249
+ case AiMessageStatus.AI_MESSAGE_STATUS_SUCCESS:
250
+ return t("system.ai.session.status.success");
251
+ case AiMessageStatus.AI_MESSAGE_STATUS_FAILED:
252
+ return t("system.ai.session.status.failed");
253
+ case AiMessageStatus.AI_MESSAGE_STATUS_GENERATING:
254
+ return t("system.ai.session.status.generating");
255
+ default:
256
+ return "";
257
+ }
258
+ }
259
+
260
+ /** 转换消息状态为标签类型。 */
261
+ function statusTagType(status?: AiMessageStatus): "success" | "danger" | "warning" | "info" {
262
+ switch (status) {
263
+ case AiMessageStatus.AI_MESSAGE_STATUS_SUCCESS:
264
+ return "success";
265
+ case AiMessageStatus.AI_MESSAGE_STATUS_FAILED:
266
+ return "danger";
267
+ case AiMessageStatus.AI_MESSAGE_STATUS_GENERATING:
268
+ return "warning";
269
+ default:
270
+ return "info";
271
+ }
272
+ }
273
+
274
+ /** 构建消息附件卡片,与聊天窗口一致。 */
275
+ function buildMessageAttachmentItems(attachments: AiAttachment[]): FilesCardProps[] {
276
+ return attachments.map(attachment => buildAIAttachmentFileCard(attachment, { maxWidth: "240px" }));
277
+ }
278
+
279
+ /** 打开会话详情抽屉。 */
280
+ async function openDetail(row: AiSessionRecord) {
281
+ detail.row = row;
282
+ detail.visible = true;
283
+ detail.pageNum = 1;
284
+ detail.total = 0;
285
+ detail.list = [];
286
+ await loadDetailMessages();
287
+ }
288
+
289
+ /** 加载会话详情消息列表。 */
290
+ async function loadDetailMessages() {
291
+ const row = detail.row;
292
+ if (!row) return;
293
+ detail.loading = true;
294
+ try {
295
+ const response = await defAiSessionService.PageAiSessionMessage({ id: row.id, page_num: detail.pageNum, page_size: detail.pageSize });
296
+ detail.list = response.messages ?? [];
297
+ detail.total = response.total;
298
+ } finally {
299
+ detail.loading = false;
300
+ }
301
+ }
302
+
303
+ /** 打开会话用量统计弹窗。 */
304
+ async function openUsage(row: AiSessionRecord) {
305
+ usage.row = row;
306
+ usage.visible = true;
307
+ usage.list = [];
308
+ await loadUsageMessages();
309
+ }
310
+
311
+ /** 加载当前会话全部消息。 */
312
+ async function loadUsageMessages() {
313
+ const row = usage.row;
314
+ if (!row) return;
315
+ usage.loading = true;
316
+ try {
317
+ let pageNum = 1;
318
+ while (pageNum * USAGE_PAGE_SIZE <= MAX_USAGE_MESSAGES) {
319
+ const response = await defAiSessionService.PageAiSessionMessage({ id: row.id, page_num: pageNum, page_size: USAGE_PAGE_SIZE });
320
+ const messages = response.messages ?? [];
321
+ usage.list.push(...messages);
322
+ if (messages.length < USAGE_PAGE_SIZE || usage.list.length >= response.total) break;
323
+ pageNum++;
324
+ }
325
+ } finally {
326
+ usage.loading = false;
327
+ }
328
+ }
329
+ </script>
330
+
331
+ <style scoped>
332
+ .ai-session-detail {
333
+ display: flex;
334
+ flex-direction: column;
335
+ gap: 16px;
336
+ }
337
+
338
+ .ai-session-detail__head {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 8px;
342
+ padding-bottom: 12px;
343
+ border-bottom: 1px solid var(--el-border-color-lighter);
344
+ }
345
+
346
+ .ai-session-detail__title {
347
+ font-size: 16px;
348
+ font-weight: 600;
349
+ }
350
+
351
+ .ai-session-detail__summary {
352
+ color: var(--el-text-color-secondary);
353
+ font-size: 13px;
354
+ }
355
+
356
+ .ai-session-detail__meta {
357
+ display: flex;
358
+ align-items: center;
359
+ flex-wrap: wrap;
360
+ gap: 12px;
361
+ color: var(--el-text-color-secondary);
362
+ font-size: 12px;
363
+ }
364
+
365
+ .ai-session-detail__chat {
366
+ min-height: 120px;
367
+ }
368
+
369
+ .ai-session-detail__empty {
370
+ padding: 32px 0;
371
+ color: var(--el-text-color-secondary);
372
+ text-align: center;
373
+ }
374
+
375
+ .ai-session-detail__bubbles {
376
+ width: 100%;
377
+ }
378
+
379
+ .ai-session-detail__bubble {
380
+ min-width: 0;
381
+ }
382
+
383
+ .ai-session-detail__message-meta {
384
+ display: flex;
385
+ align-items: center;
386
+ flex-wrap: wrap;
387
+ gap: 10px;
388
+ margin-bottom: 4px;
389
+ color: var(--el-text-color-secondary);
390
+ font-size: 12px;
391
+ }
392
+
393
+ .ai-session-detail__content {
394
+ font-size: 13px;
395
+ line-height: 1.7;
396
+ overflow-wrap: anywhere;
397
+ }
398
+
399
+ .ai-session-detail__content.is-user {
400
+ white-space: pre-wrap;
401
+ word-break: break-word;
402
+ }
403
+
404
+ .ai-session-detail__attachments {
405
+ margin-top: 8px;
406
+ }
407
+
408
+ .ai-session-detail__pagination {
409
+ justify-content: flex-end;
410
+ }
411
+
412
+ .ai-session-usage {
413
+ display: flex;
414
+ flex-direction: column;
415
+ gap: 16px;
416
+ }
417
+
418
+ /* 用量表格单元格统一不换行,避免时间等列折行。 */
419
+ .ai-session-usage :deep(.el-table .cell) {
420
+ white-space: nowrap;
421
+ }
422
+ </style>