@liujitcn/kratos-admin-system 0.0.64 → 0.0.65
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.
- package/README.md +2 -2
- package/dist/package/declarations/src/api/system/admin/v1/ai_provider.d.ts +3 -1
- package/dist/package/declarations/src/locales/generated.d.ts +68 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_provider.d.ts +13 -6
- package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +23 -0
- package/dist/package/declarations/src/views/ai/chat/modelDisplay.d.ts +5 -0
- package/dist/package/declarations/src/views/ai/chat/modelSelection.d.ts +9 -0
- package/dist/package/src/api/system/admin/v1/ai_provider.ts +7 -0
- package/dist/package/src/locales/en-US.json +18 -1
- package/dist/package/src/locales/ja-JP.json +18 -1
- package/dist/package/src/locales/zh-CN.json +18 -1
- package/dist/package/src/locales/zh-TW.json +18 -1
- package/dist/package/src/rpc/base/v1/ai_provider.ts +14 -6
- package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +26 -0
- package/dist/package/src/views/ai/chat/components/ChatPanel.vue +4 -1
- package/dist/package/src/views/ai/chat/components/XSender.vue +266 -71
- package/dist/package/src/views/ai/chat/index.vue +24 -6
- package/dist/package/src/views/ai/chat/modelDisplay.ts +67 -0
- package/dist/package/src/views/ai/chat/modelSelection.ts +31 -0
- package/dist/package/src/views/base/ai-provider/index.vue +123 -22
- package/dist/package/src/views/base/api-rate-limit-policy/index.vue +4 -3
- package/dist/package/src/views/base/message-provider/index.vue +7 -3
- package/dist/package/src/views/base/message-template/index.vue +2 -2
- package/dist/package/src/views/base/rate-limit-rule/index.vue +8 -3
- package/package.json +2 -2
|
@@ -53,22 +53,79 @@
|
|
|
53
53
|
|
|
54
54
|
<template #action-list>
|
|
55
55
|
<div class="agent-sender-actions">
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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>
|
|
72
129
|
<el-tooltip
|
|
73
130
|
:content="recording ? t('system.ai.chat.action.stop_voice_input') : t('system.ai.chat.action.voice_input')"
|
|
74
131
|
placement="top"
|
|
@@ -117,17 +174,30 @@
|
|
|
117
174
|
|
|
118
175
|
<script setup lang="ts" name="XSender">
|
|
119
176
|
import { computed, ref } from "vue";
|
|
177
|
+
import { useRouter } from "vue-router";
|
|
120
178
|
import { Attachments, XSender as BaseXSender } from "vue-element-plus-x";
|
|
121
179
|
import type { FilesCardProps } from "vue-element-plus-x/types/FilesCard";
|
|
122
|
-
import {
|
|
123
|
-
|
|
124
|
-
|
|
180
|
+
import {
|
|
181
|
+
ArrowDown,
|
|
182
|
+
ArrowRight,
|
|
183
|
+
Check,
|
|
184
|
+
Loading,
|
|
185
|
+
MagicStick,
|
|
186
|
+
Microphone,
|
|
187
|
+
Paperclip,
|
|
188
|
+
Promotion,
|
|
189
|
+
Setting
|
|
190
|
+
} from "@element-plus/icons-vue";
|
|
191
|
+
import { ElMessage, ElPopover } from "element-plus";
|
|
125
192
|
import { t } from "@liujitcn/kratos-admin-core";
|
|
193
|
+
import { navigateTo } from "@liujitcn/kratos-admin-core/navigation";
|
|
194
|
+
import { useAuthStore } from "@liujitcn/kratos-admin-core/stores/runtime";
|
|
126
195
|
import { defFileService } from "@liujitcn/kratos-admin-core/api/base/v1/file";
|
|
127
196
|
import type { AiAttachment } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
|
|
128
197
|
import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
|
|
129
198
|
import type { SubmitPayload } from "../types";
|
|
130
199
|
import { buildAIAttachmentFileCard } from "../attachment";
|
|
200
|
+
import { fallbackModelDisplayName } from "../modelDisplay";
|
|
131
201
|
import { useSpeechRecognition } from "./speech-recognition";
|
|
132
202
|
import type { SpeechRecognitionError } from "./speech-recognition";
|
|
133
203
|
|
|
@@ -151,17 +221,34 @@ const emit = defineEmits<{
|
|
|
151
221
|
|
|
152
222
|
const senderRef = ref<InstanceType<typeof BaseXSender>>();
|
|
153
223
|
const fileInputRef = ref<HTMLInputElement>();
|
|
224
|
+
const modelPopoverRef = ref<InstanceType<typeof ElPopover>>();
|
|
225
|
+
const router = useRouter();
|
|
226
|
+
const authStore = useAuthStore();
|
|
154
227
|
const inputText = ref("");
|
|
155
228
|
const selectedAttachments = ref<AiAttachment[]>([]);
|
|
156
229
|
const modelOptions = computed(() =>
|
|
157
230
|
props.modelProviders.map(provider => ({
|
|
158
|
-
|
|
231
|
+
providerId: provider.provider_id,
|
|
159
232
|
label: provider.name,
|
|
160
|
-
|
|
161
|
-
|
|
233
|
+
models: provider.models.map(model => ({
|
|
234
|
+
name: model.model_name,
|
|
235
|
+
displayName: model.display_name || fallbackModelDisplayName(model.model_name)
|
|
236
|
+
}))
|
|
162
237
|
}))
|
|
163
238
|
);
|
|
164
|
-
const
|
|
239
|
+
const modelOptionCount = computed(() =>
|
|
240
|
+
modelOptions.value.reduce((total, provider) => total + provider.models.length, 0)
|
|
241
|
+
);
|
|
242
|
+
const selectedModelDisplayName = computed(() => {
|
|
243
|
+
const provider = modelOptions.value.find(item => item.providerId === props.providerId);
|
|
244
|
+
return (
|
|
245
|
+
provider?.models.find(model => model.name === props.modelName)?.displayName ??
|
|
246
|
+
fallbackModelDisplayName(props.modelName)
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
const aiProviderRoute = computed(() =>
|
|
250
|
+
authStore.flatMenuListGet.find(item => item.name === "AiProvider" && item.path)
|
|
251
|
+
);
|
|
165
252
|
const uploading = ref(false);
|
|
166
253
|
const maxAttachmentCount = 6;
|
|
167
254
|
const maxAttachmentSizeMB = 20;
|
|
@@ -218,10 +305,22 @@ const isSubmitDisabled = computed(() => {
|
|
|
218
305
|
return uploading.value || (!inputText.value.trim() && selectedAttachments.value.length === 0);
|
|
219
306
|
});
|
|
220
307
|
|
|
221
|
-
/**
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
308
|
+
/** 判断指定供应商与模型是否为当前选择。 */
|
|
309
|
+
function isModelSelected(providerId: number, modelName: string) {
|
|
310
|
+
return props.providerId === providerId && props.modelName === modelName;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 选中模型并关闭选择面板。 */
|
|
314
|
+
function handleModelSelect(providerId: number, modelName: string) {
|
|
315
|
+
emit("model-change", providerId, modelName);
|
|
316
|
+
modelPopoverRef.value?.hide();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 跳转到 AI 供应商配置页。 */
|
|
320
|
+
async function openModelManage() {
|
|
321
|
+
if (!aiProviderRoute.value?.path) return;
|
|
322
|
+
modelPopoverRef.value?.hide();
|
|
323
|
+
await navigateTo(router, aiProviderRoute.value.path);
|
|
225
324
|
}
|
|
226
325
|
|
|
227
326
|
/** 读取输入内容并发送给父组件。 */
|
|
@@ -464,11 +563,15 @@ function resetFileInput() {
|
|
|
464
563
|
min-height: 36px;
|
|
465
564
|
}
|
|
466
565
|
.agent-model-selector {
|
|
467
|
-
display: flex;
|
|
566
|
+
display: inline-flex;
|
|
468
567
|
flex: 0 1 auto;
|
|
469
|
-
|
|
568
|
+
align-items: center;
|
|
569
|
+
gap: 6px;
|
|
570
|
+
width: clamp(180px, 18vw, 220px);
|
|
470
571
|
min-width: 0;
|
|
471
572
|
height: 34px;
|
|
573
|
+
padding: 0 10px;
|
|
574
|
+
overflow: hidden;
|
|
472
575
|
box-sizing: border-box;
|
|
473
576
|
color: var(--admin-page-text-secondary);
|
|
474
577
|
cursor: pointer;
|
|
@@ -479,48 +582,34 @@ function resetFileInput() {
|
|
|
479
582
|
color 0.2s ease,
|
|
480
583
|
border-color 0.2s ease,
|
|
481
584
|
background-color 0.2s ease;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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;
|
|
585
|
+
&:hover {
|
|
586
|
+
color: var(--el-color-primary);
|
|
587
|
+
background: var(--el-color-primary-light-9);
|
|
588
|
+
border-color: var(--el-color-primary-light-5);
|
|
589
|
+
}
|
|
590
|
+
&:disabled {
|
|
591
|
+
cursor: not-allowed;
|
|
592
|
+
opacity: 0.55;
|
|
593
|
+
}
|
|
594
|
+
&__spark {
|
|
595
|
+
flex-shrink: 0;
|
|
596
|
+
font-size: 15px;
|
|
597
|
+
}
|
|
598
|
+
&__label {
|
|
599
|
+
flex: 1;
|
|
600
|
+
min-width: 0;
|
|
601
|
+
overflow: hidden;
|
|
602
|
+
font-size: 14px;
|
|
603
|
+
font-weight: 500;
|
|
604
|
+
line-height: 20px;
|
|
605
|
+
text-align: left;
|
|
606
|
+
text-overflow: ellipsis;
|
|
607
|
+
white-space: nowrap;
|
|
608
|
+
}
|
|
609
|
+
&__arrow {
|
|
610
|
+
flex-shrink: 0;
|
|
611
|
+
font-size: 14px;
|
|
612
|
+
}
|
|
524
613
|
}
|
|
525
614
|
.agent-sender-actions {
|
|
526
615
|
display: inline-flex;
|
|
@@ -612,6 +701,112 @@ function resetFileInput() {
|
|
|
612
701
|
:global(.agent-sender-popover.el-popover) {
|
|
613
702
|
box-shadow: 0 14px 36px rgb(15 23 42 / 10%);
|
|
614
703
|
}
|
|
704
|
+
:global(.agent-model-popper.el-popover) {
|
|
705
|
+
padding: 0 !important;
|
|
706
|
+
border-radius: var(--admin-page-radius) !important;
|
|
707
|
+
box-shadow: 0 14px 36px rgb(15 23 42 / 16%) !important;
|
|
708
|
+
}
|
|
709
|
+
.agent-model-panel {
|
|
710
|
+
padding: 10px;
|
|
711
|
+
&__list {
|
|
712
|
+
max-height: min(52vh, 420px);
|
|
713
|
+
overflow-y: auto;
|
|
714
|
+
scrollbar-width: thin;
|
|
715
|
+
scrollbar-color: var(--el-border-color-darker) transparent;
|
|
716
|
+
}
|
|
717
|
+
&__head {
|
|
718
|
+
display: flex;
|
|
719
|
+
align-items: baseline;
|
|
720
|
+
justify-content: space-between;
|
|
721
|
+
padding: 4px 6px 10px;
|
|
722
|
+
}
|
|
723
|
+
&__title {
|
|
724
|
+
font-size: 14px;
|
|
725
|
+
font-weight: 700;
|
|
726
|
+
color: var(--admin-page-text-primary);
|
|
727
|
+
}
|
|
728
|
+
&__meta {
|
|
729
|
+
font-size: 11px;
|
|
730
|
+
color: var(--el-text-color-placeholder);
|
|
731
|
+
}
|
|
732
|
+
&__group {
|
|
733
|
+
padding: 8px 6px 6px;
|
|
734
|
+
font-size: 12px;
|
|
735
|
+
color: var(--admin-page-text-secondary);
|
|
736
|
+
}
|
|
737
|
+
&__manage {
|
|
738
|
+
display: inline-flex;
|
|
739
|
+
align-items: center;
|
|
740
|
+
gap: 6px;
|
|
741
|
+
width: 100%;
|
|
742
|
+
height: 36px;
|
|
743
|
+
margin-top: 6px;
|
|
744
|
+
padding: 0 6px;
|
|
745
|
+
font-size: 12.5px;
|
|
746
|
+
font-weight: 600;
|
|
747
|
+
color: var(--el-color-primary);
|
|
748
|
+
cursor: pointer;
|
|
749
|
+
background: transparent;
|
|
750
|
+
border: 0;
|
|
751
|
+
border-top: 1px solid var(--el-border-color-lighter);
|
|
752
|
+
border-radius: 0;
|
|
753
|
+
&:hover {
|
|
754
|
+
color: var(--el-color-primary-light-3);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
&__manage-arrow {
|
|
758
|
+
margin-left: auto;
|
|
759
|
+
}
|
|
760
|
+
&--empty {
|
|
761
|
+
padding: 20px 10px;
|
|
762
|
+
font-size: 13px;
|
|
763
|
+
color: var(--admin-page-text-secondary);
|
|
764
|
+
text-align: center;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
.agent-model-row {
|
|
768
|
+
display: flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
gap: 8px;
|
|
771
|
+
width: 100%;
|
|
772
|
+
height: 42px;
|
|
773
|
+
margin-bottom: 2px;
|
|
774
|
+
padding: 0 10px;
|
|
775
|
+
font-size: 13.5px;
|
|
776
|
+
color: var(--admin-page-text-primary);
|
|
777
|
+
text-align: left;
|
|
778
|
+
cursor: pointer;
|
|
779
|
+
background: transparent;
|
|
780
|
+
border: 0;
|
|
781
|
+
border-radius: var(--admin-page-radius);
|
|
782
|
+
&:hover {
|
|
783
|
+
background: var(--el-fill-color-light);
|
|
784
|
+
}
|
|
785
|
+
&.is-selected {
|
|
786
|
+
font-weight: 600;
|
|
787
|
+
color: var(--el-color-primary);
|
|
788
|
+
background: var(--el-color-primary-light-9);
|
|
789
|
+
}
|
|
790
|
+
&__name {
|
|
791
|
+
overflow: hidden;
|
|
792
|
+
text-overflow: ellipsis;
|
|
793
|
+
white-space: nowrap;
|
|
794
|
+
}
|
|
795
|
+
&__raw {
|
|
796
|
+
flex: 1;
|
|
797
|
+
min-width: 0;
|
|
798
|
+
overflow: hidden;
|
|
799
|
+
font-size: 12px;
|
|
800
|
+
font-weight: 400;
|
|
801
|
+
color: var(--el-text-color-placeholder);
|
|
802
|
+
text-overflow: ellipsis;
|
|
803
|
+
white-space: nowrap;
|
|
804
|
+
}
|
|
805
|
+
&__check {
|
|
806
|
+
flex-shrink: 0;
|
|
807
|
+
font-size: 15px;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
615
810
|
|
|
616
811
|
@media screen and (width <= 768px) {
|
|
617
812
|
.agent-prefix-actions {
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
sortMessages
|
|
86
86
|
} from "./message";
|
|
87
87
|
import { readAiEventStream } from "./stream";
|
|
88
|
+
import { loadStoredModelSelection, saveModelSelection } from "./modelSelection";
|
|
88
89
|
import type {
|
|
89
90
|
AiStreamEvent,
|
|
90
91
|
AiStreamPayload,
|
|
@@ -662,11 +663,7 @@ async function loadAiShortcuts() {
|
|
|
662
663
|
]);
|
|
663
664
|
starterShortcuts.value = normalizeStarterShortcuts(response.shortcuts);
|
|
664
665
|
modelProviders.value = providerResponse.providers ?? [];
|
|
665
|
-
|
|
666
|
-
const firstProvider = modelProviders.value[0];
|
|
667
|
-
selectedProviderId.value = firstProvider?.provider_id ?? 0;
|
|
668
|
-
selectedModelName.value = firstProvider?.models[0] ?? "";
|
|
669
|
-
}
|
|
666
|
+
applyModelSelection();
|
|
670
667
|
} catch {
|
|
671
668
|
starterShortcuts.value = [];
|
|
672
669
|
modelProviders.value = [];
|
|
@@ -677,10 +674,31 @@ async function loadAiShortcuts() {
|
|
|
677
674
|
}
|
|
678
675
|
}
|
|
679
676
|
|
|
680
|
-
/**
|
|
677
|
+
/** 优先恢复本地缓存的模型选择,缓存失效时回退到首个可用模型。 */
|
|
678
|
+
function applyModelSelection() {
|
|
679
|
+
const stored = loadStoredModelSelection();
|
|
680
|
+
const provider = stored
|
|
681
|
+
? modelProviders.value.find(
|
|
682
|
+
item =>
|
|
683
|
+
item.provider_id === stored.provider_id &&
|
|
684
|
+
item.models.some(model => model.model_name === stored.model_name)
|
|
685
|
+
)
|
|
686
|
+
: undefined;
|
|
687
|
+
if (provider && stored) {
|
|
688
|
+
selectedProviderId.value = provider.provider_id;
|
|
689
|
+
selectedModelName.value = stored.model_name;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const firstProvider = modelProviders.value[0];
|
|
693
|
+
selectedProviderId.value = firstProvider?.provider_id ?? 0;
|
|
694
|
+
selectedModelName.value = firstProvider?.models[0]?.model_name ?? "";
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** 更新当前聊天使用的供应商与模型并持久化到本地缓存。 */
|
|
681
698
|
function handleModelChange(providerId: number, modelName: string) {
|
|
682
699
|
selectedProviderId.value = providerId;
|
|
683
700
|
selectedModelName.value = modelName;
|
|
701
|
+
saveModelSelection({ provider_id: providerId, model_name: modelName });
|
|
684
702
|
}
|
|
685
703
|
|
|
686
704
|
/** 使指定会话仍在途的消息列表请求失效。 */
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
|
|
2
|
+
|
|
3
|
+
/** 常见模型的展示名映射;未命中时按通用规则美化原始名。 */
|
|
4
|
+
const MODEL_DISPLAY_NAMES: Record<string, string> = {
|
|
5
|
+
"deepseek-chat": "DeepSeek Chat",
|
|
6
|
+
"deepseek-reasoner": "DeepSeek Reasoner",
|
|
7
|
+
"deepseek-v3": "DeepSeek V3",
|
|
8
|
+
"deepseek-r1": "DeepSeek R1",
|
|
9
|
+
"gpt-4o": "GPT-4o",
|
|
10
|
+
"gpt-4o-mini": "GPT-4o mini",
|
|
11
|
+
"gpt-4.1": "GPT-4.1",
|
|
12
|
+
"gpt-4.1-mini": "GPT-4.1 mini",
|
|
13
|
+
"gpt-4-turbo": "GPT-4 Turbo",
|
|
14
|
+
"o3-mini": "O3-mini",
|
|
15
|
+
"claude-3-5-sonnet": "Claude 3.5 Sonnet",
|
|
16
|
+
"claude-3-7-sonnet": "Claude 3.7 Sonnet",
|
|
17
|
+
"qwen-max": "Qwen Max",
|
|
18
|
+
"qwen-plus": "Qwen Plus"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** 需要整体大写的模型名片段。 */
|
|
22
|
+
const UPPERCASE_TOKENS = new Set([
|
|
23
|
+
"gpt",
|
|
24
|
+
"llama",
|
|
25
|
+
"qwen",
|
|
26
|
+
"glm",
|
|
27
|
+
"kimi",
|
|
28
|
+
"gemini",
|
|
29
|
+
"claude",
|
|
30
|
+
"doubao",
|
|
31
|
+
"deepseek",
|
|
32
|
+
"moonshot",
|
|
33
|
+
"hunyuan"
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/** 对未配置展示名的模型名做通用美化。 */
|
|
37
|
+
export function fallbackModelDisplayName(modelName: string): string {
|
|
38
|
+
if (!modelName) return "";
|
|
39
|
+
const mapped = MODEL_DISPLAY_NAMES[modelName.toLowerCase()];
|
|
40
|
+
if (mapped) return mapped;
|
|
41
|
+
return modelName
|
|
42
|
+
.split(/[-_]/)
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.map(token => {
|
|
45
|
+
const lower = token.toLowerCase();
|
|
46
|
+
if (UPPERCASE_TOKENS.has(lower)) return lower.toUpperCase();
|
|
47
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
48
|
+
})
|
|
49
|
+
.join(" ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 从供应商模型选项中解析模型展示名称,兼容历史消息中「供应商 / 模型」格式的原始值。 */
|
|
53
|
+
export function resolveModelDisplayName(
|
|
54
|
+
providers: AiProviderModelOption[] | undefined,
|
|
55
|
+
modelName: string
|
|
56
|
+
): string {
|
|
57
|
+
if (!modelName) return "";
|
|
58
|
+
const separator = " / ";
|
|
59
|
+
const modelPart = modelName.includes(separator)
|
|
60
|
+
? modelName.slice(modelName.lastIndexOf(separator) + separator.length)
|
|
61
|
+
: modelName;
|
|
62
|
+
for (const provider of providers ?? []) {
|
|
63
|
+
const matched = provider.models.find(item => item.model_name === modelPart);
|
|
64
|
+
if (matched?.display_name) return matched.display_name;
|
|
65
|
+
}
|
|
66
|
+
return fallbackModelDisplayName(modelPart);
|
|
67
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** 用户最近一次选择的供应商与模型,仅保存在浏览器本地。 */
|
|
2
|
+
export interface AiModelSelection {
|
|
3
|
+
provider_id: number;
|
|
4
|
+
model_name: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const MODEL_SELECTION_STORAGE_KEY = "kratos-admin:ai:model-selection";
|
|
8
|
+
|
|
9
|
+
/** 读取本地缓存的模型选择,缓存缺失或损坏时返回 undefined。 */
|
|
10
|
+
export function loadStoredModelSelection(): AiModelSelection | undefined {
|
|
11
|
+
try {
|
|
12
|
+
const raw = localStorage.getItem(MODEL_SELECTION_STORAGE_KEY);
|
|
13
|
+
if (!raw) return undefined;
|
|
14
|
+
const parsed = JSON.parse(raw) as Partial<AiModelSelection>;
|
|
15
|
+
if (typeof parsed.provider_id === "number" && typeof parsed.model_name === "string" && parsed.model_name) {
|
|
16
|
+
return { provider_id: parsed.provider_id, model_name: parsed.model_name };
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// 缓存损坏或存储不可用时忽略,回退到默认选择。
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 持久化模型选择,存储不可用时静默失败,选择仍对当前会话生效。 */
|
|
25
|
+
export function saveModelSelection(selection: AiModelSelection): void {
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem(MODEL_SELECTION_STORAGE_KEY, JSON.stringify(selection));
|
|
28
|
+
} catch {
|
|
29
|
+
// 忽略存储失败,不影响当前会话内的选择。
|
|
30
|
+
}
|
|
31
|
+
}
|