@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
@@ -53,22 +53,145 @@
53
53
 
54
54
  <template #action-list>
55
55
  <div class="agent-sender-actions">
56
- <div class="agent-model-selector" :class="{ 'is-disabled': sending || !modelOptions.length }">
57
- <el-cascader
58
- :model-value="selectedModelPath"
59
- :options="modelOptions"
60
- :props="{ expandTrigger: 'click' }"
61
- :placeholder="t('system.ai.model.model_placeholder')"
62
- :disabled="sending || !modelOptions.length"
63
- :show-all-levels="true"
64
- separator="/"
65
- :clearable="true"
66
- :teleported="true"
67
- popper-class="agent-model-cascader-popper"
68
- size="small"
69
- @change="handleModelPathChange"
70
- />
71
- </div>
56
+ <el-popover
57
+ ref="modelPopoverRef"
58
+ placement="top-start"
59
+ :width="324"
60
+ trigger="click"
61
+ :show-arrow="false"
62
+ :persistent="false"
63
+ popper-class="agent-model-popper"
64
+ >
65
+ <template #reference>
66
+ <button
67
+ class="agent-model-selector"
68
+ :class="{ 'is-disabled': sending || !modelOptions.length }"
69
+ type="button"
70
+ :disabled="sending || !modelOptions.length"
71
+ :aria-label="t('system.ai.model.title')"
72
+ >
73
+ <el-icon class="agent-model-selector__spark"><MagicStick /></el-icon>
74
+ <span class="agent-model-selector__label">{{
75
+ selectedModelDisplayName || t("system.ai.model.model_placeholder")
76
+ }}</span>
77
+ <el-icon class="agent-model-selector__arrow"><ArrowDown /></el-icon>
78
+ </button>
79
+ </template>
80
+ <div v-if="modelOptions.length" class="agent-model-panel">
81
+ <div class="agent-model-panel__head">
82
+ <span class="agent-model-panel__title">{{ t("system.ai.model.title") }}</span>
83
+ <span class="agent-model-panel__meta">{{
84
+ t("system.ai.model.provider_model_count", {
85
+ providers: modelOptions.length,
86
+ models: modelOptionCount
87
+ })
88
+ }}</span>
89
+ </div>
90
+ <div class="agent-model-panel__list">
91
+ <template v-for="provider in modelOptions" :key="provider.providerId">
92
+ <div class="agent-model-panel__group">{{ provider.label }}</div>
93
+ <button
94
+ v-for="model in provider.models"
95
+ :key="model.name"
96
+ class="agent-model-row"
97
+ :class="{ 'is-selected': isModelSelected(provider.providerId, model.name) }"
98
+ type="button"
99
+ @click="handleModelSelect(provider.providerId, model.name)"
100
+ >
101
+ <span class="agent-model-row__name">{{ model.displayName }}</span>
102
+ <span v-if="model.displayName !== model.name" class="agent-model-row__raw">{{
103
+ model.name
104
+ }}</span>
105
+ <el-icon
106
+ v-if="isModelSelected(provider.providerId, model.name)"
107
+ class="agent-model-row__check"
108
+ >
109
+ <Check />
110
+ </el-icon>
111
+ </button>
112
+ </template>
113
+ </div>
114
+ <button
115
+ v-if="aiProviderRoute"
116
+ class="agent-model-panel__manage"
117
+ type="button"
118
+ @click="openModelManage"
119
+ >
120
+ <el-icon><Setting /></el-icon>
121
+ <span>{{ t("system.ai.model.manage") }}</span>
122
+ <el-icon class="agent-model-panel__manage-arrow"><ArrowRight /></el-icon>
123
+ </button>
124
+ </div>
125
+ <div v-else class="agent-model-panel agent-model-panel--empty">
126
+ {{ t("system.ai.model.empty") }}
127
+ </div>
128
+ </el-popover>
129
+ <el-popover
130
+ ref="knowledgePopoverRef"
131
+ placement="top-start"
132
+ :width="324"
133
+ trigger="click"
134
+ :show-arrow="false"
135
+ :persistent="false"
136
+ popper-class="agent-model-popper"
137
+ >
138
+ <template #reference>
139
+ <button
140
+ class="agent-model-selector"
141
+ :class="{ 'is-disabled': sending || !knowledgeOptions.length, 'is-selected': selectedKnowledgeCount > 0 }"
142
+ type="button"
143
+ :disabled="sending || !knowledgeOptions.length"
144
+ :aria-label="t('system.ai.knowledge.title')"
145
+ >
146
+ <el-icon class="agent-model-selector__spark"><Collection /></el-icon>
147
+ <span class="agent-model-selector__label">{{
148
+ selectedKnowledgeCount > 0
149
+ ? t("system.ai.knowledge.selected_count", { count: selectedKnowledgeCount })
150
+ : t("system.ai.knowledge.title")
151
+ }}</span>
152
+ <el-icon class="agent-model-selector__arrow"><ArrowDown /></el-icon>
153
+ </button>
154
+ </template>
155
+ <div v-if="knowledgeOptions.length" class="agent-model-panel">
156
+ <div class="agent-model-panel__head">
157
+ <span class="agent-model-panel__title">{{ t("system.ai.knowledge.title") }}</span>
158
+ <span class="agent-model-panel__meta">{{
159
+ t("system.ai.knowledge.select_hint")
160
+ }}</span>
161
+ </div>
162
+ <div class="agent-model-panel__list">
163
+ <button
164
+ v-for="option in knowledgeOptions"
165
+ :key="option.id"
166
+ class="agent-model-row"
167
+ :class="{ 'is-selected': isKnowledgeSelected(option.id) }"
168
+ type="button"
169
+ @click="handleKnowledgeToggle(option.id)"
170
+ >
171
+ <span class="agent-model-row__name">{{ option.name }}</span>
172
+ <span v-if="option.doc_count" class="agent-model-row__raw">{{
173
+ t("system.ai.knowledge.doc_count", { count: option.doc_count })
174
+ }}</span>
175
+ <el-icon v-if="isKnowledgeSelected(option.id)" class="agent-model-row__check">
176
+ <Check />
177
+ </el-icon>
178
+ </button>
179
+ </div>
180
+ <button
181
+ v-if="aiKnowledgeRoute"
182
+ class="agent-model-panel__manage"
183
+ type="button"
184
+ @click="openKnowledgeManage"
185
+ >
186
+ <el-icon><Setting /></el-icon>
187
+ <span>{{ t("system.ai.knowledge.manage") }}</span>
188
+ <el-icon class="agent-model-panel__manage-arrow"><ArrowRight /></el-icon>
189
+ </button>
190
+ </div>
191
+ <div v-else class="agent-model-panel agent-model-panel--empty">
192
+ {{ t("system.ai.knowledge.empty") }}
193
+ </div>
194
+ </el-popover>
72
195
  <el-tooltip
73
196
  :content="recording ? t('system.ai.chat.action.stop_voice_input') : t('system.ai.chat.action.voice_input')"
74
197
  placement="top"
@@ -117,17 +240,32 @@
117
240
 
118
241
  <script setup lang="ts" name="XSender">
119
242
  import { computed, ref } from "vue";
243
+ import { useRouter } from "vue-router";
120
244
  import { Attachments, XSender as BaseXSender } from "vue-element-plus-x";
121
245
  import type { FilesCardProps } from "vue-element-plus-x/types/FilesCard";
122
- import { Loading, Microphone, Paperclip, Promotion } from "@element-plus/icons-vue";
123
- import { ElMessage } from "element-plus";
124
- import type { CascaderValue } from "element-plus";
246
+ import {
247
+ ArrowDown,
248
+ ArrowRight,
249
+ Check,
250
+ Collection,
251
+ Loading,
252
+ MagicStick,
253
+ Microphone,
254
+ Paperclip,
255
+ Promotion,
256
+ Setting
257
+ } from "@element-plus/icons-vue";
258
+ import { ElMessage, ElPopover } from "element-plus";
125
259
  import { t } from "@liujitcn/kratos-admin-core";
260
+ import { navigateTo } from "@liujitcn/kratos-admin-core/navigation";
261
+ import { useAuthStore } from "@liujitcn/kratos-admin-core/stores/runtime";
126
262
  import { defFileService } from "@liujitcn/kratos-admin-core/api/base/v1/file";
127
263
  import type { AiAttachment } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
128
264
  import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
265
+ import type { AiKnowledgeOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_knowledge";
129
266
  import type { SubmitPayload } from "../types";
130
267
  import { buildAIAttachmentFileCard } from "../attachment";
268
+ import { fallbackModelDisplayName } from "../modelDisplay";
131
269
  import { useSpeechRecognition } from "./speech-recognition";
132
270
  import type { SpeechRecognitionError } from "./speech-recognition";
133
271
 
@@ -140,6 +278,10 @@ const props = defineProps<{
140
278
  providerId: number;
141
279
  /** 当前选择的模型名称。 */
142
280
  modelName: string;
281
+ /** 当前可供选择的知识库。 */
282
+ knowledgeOptions: AiKnowledgeOption[];
283
+ /** 当前选择的知识库编号列表。 */
284
+ knowledgeIds: number[];
143
285
  }>();
144
286
 
145
287
  const emit = defineEmits<{
@@ -147,21 +289,43 @@ const emit = defineEmits<{
147
289
  submit: [payload: SubmitPayload];
148
290
  /** 更新当前选择的供应商和模型。 */
149
291
  "model-change": [providerId: number, modelName: string];
292
+ /** 更新当前选择的知识库编号列表。 */
293
+ "knowledge-change": [knowledgeIds: number[]];
150
294
  }>();
151
295
 
152
296
  const senderRef = ref<InstanceType<typeof BaseXSender>>();
153
297
  const fileInputRef = ref<HTMLInputElement>();
298
+ const modelPopoverRef = ref<InstanceType<typeof ElPopover>>();
299
+ const router = useRouter();
300
+ const authStore = useAuthStore();
154
301
  const inputText = ref("");
155
302
  const selectedAttachments = ref<AiAttachment[]>([]);
156
303
  const modelOptions = computed(() =>
157
304
  props.modelProviders.map(provider => ({
158
- value: provider.provider_id,
305
+ providerId: provider.provider_id,
159
306
  label: provider.name,
160
- disabled: provider.models.length === 0,
161
- children: provider.models.map(model => ({ value: model, label: model }))
307
+ models: provider.models.map(model => ({
308
+ name: model.model_name,
309
+ displayName: model.display_name || fallbackModelDisplayName(model.model_name)
310
+ }))
162
311
  }))
163
312
  );
164
- const selectedModelPath = computed(() => (props.providerId && props.modelName ? [props.providerId, props.modelName] : []));
313
+ const modelOptionCount = computed(() =>
314
+ modelOptions.value.reduce((total, provider) => total + provider.models.length, 0)
315
+ );
316
+ const selectedModelDisplayName = computed(() => {
317
+ const provider = modelOptions.value.find(item => item.providerId === props.providerId);
318
+ return (
319
+ provider?.models.find(model => model.name === props.modelName)?.displayName ??
320
+ fallbackModelDisplayName(props.modelName)
321
+ );
322
+ });
323
+ const aiProviderRoute = computed(() =>
324
+ authStore.flatMenuListGet.find(item => item.name === "AiProvider" && item.path)
325
+ );
326
+ const aiKnowledgeRoute = computed(() =>
327
+ authStore.flatMenuListGet.find(item => item.name === "AiKnowledge" && item.path)
328
+ );
165
329
  const uploading = ref(false);
166
330
  const maxAttachmentCount = 6;
167
331
  const maxAttachmentSizeMB = 20;
@@ -218,10 +382,46 @@ const isSubmitDisabled = computed(() => {
218
382
  return uploading.value || (!inputText.value.trim() && selectedAttachments.value.length === 0);
219
383
  });
220
384
 
221
- /** 更新当前选中的供应商和模型路径。 */
222
- function handleModelPathChange(value: CascaderValue | null | undefined) {
223
- const path = Array.isArray(value) ? value : [];
224
- emit("model-change", Number(path[0]) || 0, typeof path[1] === "string" ? path[1] : "");
385
+ /** 判断指定供应商与模型是否为当前选择。 */
386
+ function isModelSelected(providerId: number, modelName: string) {
387
+ return props.providerId === providerId && props.modelName === modelName;
388
+ }
389
+
390
+ /** 选中模型并关闭选择面板。 */
391
+ function handleModelSelect(providerId: number, modelName: string) {
392
+ emit("model-change", providerId, modelName);
393
+ modelPopoverRef.value?.hide();
394
+ }
395
+
396
+ /** 跳转到 AI 供应商配置页。 */
397
+ async function openModelManage() {
398
+ if (!aiProviderRoute.value?.path) return;
399
+ modelPopoverRef.value?.hide();
400
+ await navigateTo(router, aiProviderRoute.value.path);
401
+ }
402
+
403
+ const knowledgePopoverRef = ref<InstanceType<typeof ElPopover>>();
404
+
405
+ const selectedKnowledgeCount = computed(() => props.knowledgeIds.length);
406
+
407
+ /** 判断知识库是否在当前选择中。 */
408
+ function isKnowledgeSelected(knowledgeId: number) {
409
+ return props.knowledgeIds.includes(knowledgeId);
410
+ }
411
+
412
+ /** 切换知识库选择状态并同步父组件。 */
413
+ function handleKnowledgeToggle(knowledgeId: number) {
414
+ const current = new Set(props.knowledgeIds);
415
+ if (current.has(knowledgeId)) current.delete(knowledgeId);
416
+ else current.add(knowledgeId);
417
+ emit("knowledge-change", [...current]);
418
+ }
419
+
420
+ /** 跳转到 AI 知识库管理页。 */
421
+ async function openKnowledgeManage() {
422
+ if (!aiKnowledgeRoute.value?.path) return;
423
+ knowledgePopoverRef.value?.hide();
424
+ await navigateTo(router, aiKnowledgeRoute.value.path);
225
425
  }
226
426
 
227
427
  /** 读取输入内容并发送给父组件。 */
@@ -464,11 +664,15 @@ function resetFileInput() {
464
664
  min-height: 36px;
465
665
  }
466
666
  .agent-model-selector {
467
- display: flex;
667
+ display: inline-flex;
468
668
  flex: 0 1 auto;
469
- width: clamp(180px, 18vw, 200px);
669
+ align-items: center;
670
+ gap: 6px;
671
+ width: clamp(180px, 18vw, 220px);
470
672
  min-width: 0;
471
673
  height: 34px;
674
+ padding: 0 10px;
675
+ overflow: hidden;
472
676
  box-sizing: border-box;
473
677
  color: var(--admin-page-text-secondary);
474
678
  cursor: pointer;
@@ -479,48 +683,34 @@ function resetFileInput() {
479
683
  color 0.2s ease,
480
684
  border-color 0.2s ease,
481
685
  background-color 0.2s ease;
482
- }
483
- .agent-model-selector:hover {
484
- color: var(--el-color-primary);
485
- background: var(--el-color-primary-light-9);
486
- border-color: var(--el-color-primary-light-5);
487
- }
488
- .agent-model-selector:focus-within {
489
- border-color: var(--el-color-primary-light-5);
490
- }
491
- .agent-model-selector.is-disabled {
492
- color: var(--el-disabled-text-color);
493
- cursor: not-allowed;
494
- background: var(--el-disabled-bg-color);
495
- border-color: var(--el-disabled-border-color);
496
- }
497
- .agent-model-selector :deep(.el-cascader) {
498
- width: 100%;
499
- height: 100%;
500
- min-width: 0;
501
- }
502
- .agent-model-selector :deep(.el-input) {
503
- height: 100%;
504
- }
505
- .agent-model-selector :deep(.el-input__wrapper) {
506
- height: 100%;
507
- padding: 0 9px;
508
- background: transparent;
509
- box-shadow: none;
510
- border-radius: inherit;
511
- }
512
- .agent-model-selector :deep(.el-input__wrapper.is-focus) {
513
- box-shadow: none;
514
- }
515
- .agent-model-selector :deep(.el-input__inner) {
516
- font-size: 14px;
517
- font-weight: 500;
518
- line-height: 20px;
519
- color: currentColor;
520
- }
521
- .agent-model-selector :deep(.el-input__suffix .el-icon) {
522
- font-size: 14px;
523
- color: currentColor;
686
+ &:hover {
687
+ color: var(--el-color-primary);
688
+ background: var(--el-color-primary-light-9);
689
+ border-color: var(--el-color-primary-light-5);
690
+ }
691
+ &:disabled {
692
+ cursor: not-allowed;
693
+ opacity: 0.55;
694
+ }
695
+ &__spark {
696
+ flex-shrink: 0;
697
+ font-size: 15px;
698
+ }
699
+ &__label {
700
+ flex: 1;
701
+ min-width: 0;
702
+ overflow: hidden;
703
+ font-size: 14px;
704
+ font-weight: 500;
705
+ line-height: 20px;
706
+ text-align: left;
707
+ text-overflow: ellipsis;
708
+ white-space: nowrap;
709
+ }
710
+ &__arrow {
711
+ flex-shrink: 0;
712
+ font-size: 14px;
713
+ }
524
714
  }
525
715
  .agent-sender-actions {
526
716
  display: inline-flex;
@@ -612,6 +802,112 @@ function resetFileInput() {
612
802
  :global(.agent-sender-popover.el-popover) {
613
803
  box-shadow: 0 14px 36px rgb(15 23 42 / 10%);
614
804
  }
805
+ :global(.agent-model-popper.el-popover) {
806
+ padding: 0 !important;
807
+ border-radius: var(--admin-page-radius) !important;
808
+ box-shadow: 0 14px 36px rgb(15 23 42 / 16%) !important;
809
+ }
810
+ .agent-model-panel {
811
+ padding: 10px;
812
+ &__list {
813
+ max-height: min(52vh, 420px);
814
+ overflow-y: auto;
815
+ scrollbar-width: thin;
816
+ scrollbar-color: var(--el-border-color-darker) transparent;
817
+ }
818
+ &__head {
819
+ display: flex;
820
+ align-items: baseline;
821
+ justify-content: space-between;
822
+ padding: 4px 6px 10px;
823
+ }
824
+ &__title {
825
+ font-size: 14px;
826
+ font-weight: 700;
827
+ color: var(--admin-page-text-primary);
828
+ }
829
+ &__meta {
830
+ font-size: 11px;
831
+ color: var(--el-text-color-placeholder);
832
+ }
833
+ &__group {
834
+ padding: 8px 6px 6px;
835
+ font-size: 12px;
836
+ color: var(--admin-page-text-secondary);
837
+ }
838
+ &__manage {
839
+ display: inline-flex;
840
+ align-items: center;
841
+ gap: 6px;
842
+ width: 100%;
843
+ height: 36px;
844
+ margin-top: 6px;
845
+ padding: 0 6px;
846
+ font-size: 12.5px;
847
+ font-weight: 600;
848
+ color: var(--el-color-primary);
849
+ cursor: pointer;
850
+ background: transparent;
851
+ border: 0;
852
+ border-top: 1px solid var(--el-border-color-lighter);
853
+ border-radius: 0;
854
+ &:hover {
855
+ color: var(--el-color-primary-light-3);
856
+ }
857
+ }
858
+ &__manage-arrow {
859
+ margin-left: auto;
860
+ }
861
+ &--empty {
862
+ padding: 20px 10px;
863
+ font-size: 13px;
864
+ color: var(--admin-page-text-secondary);
865
+ text-align: center;
866
+ }
867
+ }
868
+ .agent-model-row {
869
+ display: flex;
870
+ align-items: center;
871
+ gap: 8px;
872
+ width: 100%;
873
+ height: 42px;
874
+ margin-bottom: 2px;
875
+ padding: 0 10px;
876
+ font-size: 13.5px;
877
+ color: var(--admin-page-text-primary);
878
+ text-align: left;
879
+ cursor: pointer;
880
+ background: transparent;
881
+ border: 0;
882
+ border-radius: var(--admin-page-radius);
883
+ &:hover {
884
+ background: var(--el-fill-color-light);
885
+ }
886
+ &.is-selected {
887
+ font-weight: 600;
888
+ color: var(--el-color-primary);
889
+ background: var(--el-color-primary-light-9);
890
+ }
891
+ &__name {
892
+ overflow: hidden;
893
+ text-overflow: ellipsis;
894
+ white-space: nowrap;
895
+ }
896
+ &__raw {
897
+ flex: 1;
898
+ min-width: 0;
899
+ overflow: hidden;
900
+ font-size: 12px;
901
+ font-weight: 400;
902
+ color: var(--el-text-color-placeholder);
903
+ text-overflow: ellipsis;
904
+ white-space: nowrap;
905
+ }
906
+ &__check {
907
+ flex-shrink: 0;
908
+ font-size: 15px;
909
+ }
910
+ }
615
911
 
616
912
  @media screen and (width <= 768px) {
617
913
  .agent-prefix-actions {
@@ -30,8 +30,11 @@
30
30
  :model-providers="modelProviders"
31
31
  :provider-id="selectedProviderId"
32
32
  :model-name="selectedModelName"
33
+ :knowledge-options="knowledgeOptions"
34
+ :knowledge-ids="selectedKnowledgeIds"
33
35
  @submit="handleSubmit"
34
36
  @model-change="handleModelChange"
37
+ @knowledge-change="handleKnowledgeChange"
35
38
  @message-action="handleMessageAction"
36
39
  @message-edit="handleEditMessage"
37
40
  >
@@ -59,9 +62,11 @@ import { defAiMessageService } from "@liujitcn/kratos-admin-system/api/base/v1/a
59
62
  import { defAiSessionService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_session";
60
63
  import { defAiToolService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_tool";
61
64
  import { defAiModelService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_provider";
65
+ import { defAiKnowledgeService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_knowledge";
62
66
  import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
63
67
  import type { AiSession } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
64
68
  import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
69
+ import type { AiKnowledgeOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_knowledge";
65
70
  import type { AiShortcut } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
66
71
  import { AiMessageStatus } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
67
72
  import { Terminal } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
@@ -85,6 +90,7 @@ import {
85
90
  sortMessages
86
91
  } from "./message";
87
92
  import { readAiEventStream } from "./stream";
93
+ import { loadStoredModelSelection, saveModelSelection } from "./modelSelection";
88
94
  import type {
89
95
  AiStreamEvent,
90
96
  AiStreamPayload,
@@ -111,6 +117,8 @@ const starterShortcuts = ref<AiShortcut[]>([]);
111
117
  const modelProviders = ref<AiProviderModelOption[]>([]);
112
118
  const selectedProviderId = ref(0);
113
119
  const selectedModelName = ref("");
120
+ const knowledgeOptions = ref<AiKnowledgeOption[]>([]);
121
+ const selectedKnowledgeIds = ref<number[]>([]);
114
122
  const messages = ref<Record<string, ChatMessageItem[]>>({});
115
123
  const pendingDeltaMap = new Map<string, AiStreamPayload>();
116
124
  const runningStreamTaskMap = new Map<string, AiStreamTask>();
@@ -228,6 +236,7 @@ async function runAiStreamTask(sessionID: string, payload: SubmitPayload) {
228
236
  content: payload.text,
229
237
  provider_id: selectedProviderId.value,
230
238
  model_name: selectedModelName.value,
239
+ knowledge_base_ids: selectedKnowledgeIds.value,
231
240
  attachments: payload.attachments.map(item => ({
232
241
  id: item.id,
233
242
  name: item.name,
@@ -656,20 +665,23 @@ async function loadAiShortcuts() {
656
665
  if (loadingShortcuts.value) return;
657
666
  loadingShortcuts.value = true;
658
667
  try {
659
- const [response, providerResponse] = await Promise.all([
668
+ const [response, providerResponse, knowledgeResponse] = await Promise.all([
660
669
  defAiToolService.ListAiShortcut({ terminal: Terminal.TERMINAL_ADMIN }),
661
- defAiModelService.ListAiProviderModelOptions({})
670
+ defAiModelService.ListAiProviderModelOptions({}),
671
+ defAiKnowledgeService.ListAiKnowledgeOptions({})
662
672
  ]);
663
673
  starterShortcuts.value = normalizeStarterShortcuts(response.shortcuts);
664
674
  modelProviders.value = providerResponse.providers ?? [];
665
- if (!modelProviders.value.some(item => item.provider_id === selectedProviderId.value && item.models.includes(selectedModelName.value))) {
666
- const firstProvider = modelProviders.value[0];
667
- selectedProviderId.value = firstProvider?.provider_id ?? 0;
668
- selectedModelName.value = firstProvider?.models[0] ?? "";
669
- }
675
+ knowledgeOptions.value = knowledgeResponse.options ?? [];
676
+ // 知识库可能已被删除,剔除无效选择。
677
+ selectedKnowledgeIds.value = selectedKnowledgeIds.value.filter(id =>
678
+ knowledgeOptions.value.some(option => option.id === id)
679
+ );
680
+ applyModelSelection();
670
681
  } catch {
671
682
  starterShortcuts.value = [];
672
683
  modelProviders.value = [];
684
+ knowledgeOptions.value = [];
673
685
  selectedProviderId.value = 0;
674
686
  selectedModelName.value = "";
675
687
  } finally {
@@ -677,10 +689,36 @@ async function loadAiShortcuts() {
677
689
  }
678
690
  }
679
691
 
680
- /** 更新当前聊天使用的供应商与模型。 */
692
+ /** 优先恢复本地缓存的模型选择,缓存失效时回退到首个可用模型。 */
693
+ function applyModelSelection() {
694
+ const stored = loadStoredModelSelection();
695
+ const provider = stored
696
+ ? modelProviders.value.find(
697
+ item =>
698
+ item.provider_id === stored.provider_id &&
699
+ item.models.some(model => model.model_name === stored.model_name)
700
+ )
701
+ : undefined;
702
+ if (provider && stored) {
703
+ selectedProviderId.value = provider.provider_id;
704
+ selectedModelName.value = stored.model_name;
705
+ return;
706
+ }
707
+ const firstProvider = modelProviders.value[0];
708
+ selectedProviderId.value = firstProvider?.provider_id ?? 0;
709
+ selectedModelName.value = firstProvider?.models[0]?.model_name ?? "";
710
+ }
711
+
712
+ /** 更新当前聊天使用的供应商与模型并持久化到本地缓存。 */
681
713
  function handleModelChange(providerId: number, modelName: string) {
682
714
  selectedProviderId.value = providerId;
683
715
  selectedModelName.value = modelName;
716
+ saveModelSelection({ provider_id: providerId, model_name: modelName });
717
+ }
718
+
719
+ /** 更新当前聊天检索注入的知识库选择。 */
720
+ function handleKnowledgeChange(knowledgeIds: number[]) {
721
+ selectedKnowledgeIds.value = knowledgeIds;
684
722
  }
685
723
 
686
724
  /** 使指定会话仍在途的消息列表请求失效。 */