@liujitcn/kratos-uni-app-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/dist/index.mjs CHANGED
@@ -369,7 +369,10 @@ var zh_CN_default = {
369
369
  "system.notification.action.delete": "\u5220\u9664",
370
370
  "system.ai.model.provider_placeholder": "\u9009\u62E9\u4F9B\u5E94\u5546",
371
371
  "system.ai.model.model_placeholder": "\u9009\u62E9\u6A21\u578B",
372
- "system.ai.model.load_failed": "\u52A0\u8F7D\u4F9B\u5E94\u5546\u548C\u6A21\u578B\u5931\u8D25"
372
+ "system.ai.model.load_failed": "\u52A0\u8F7D\u4F9B\u5E94\u5546\u548C\u6A21\u578B\u5931\u8D25",
373
+ "system.ai.model.title": "\u9009\u62E9\u6A21\u578B",
374
+ "system.ai.model.provider_model_count": "{providers} \u4E2A\u4F9B\u5E94\u5546 \xB7 {models} \u4E2A\u6A21\u578B",
375
+ "system.ai.model.empty": "\u6682\u65E0\u53EF\u9009\u6A21\u578B"
373
376
  };
374
377
 
375
378
  // src/locales/en-US.json
@@ -488,7 +491,10 @@ var en_US_default = {
488
491
  "system.notification.action.delete": "Delete",
489
492
  "system.ai.model.provider_placeholder": "Select a provider",
490
493
  "system.ai.model.model_placeholder": "Select a model",
491
- "system.ai.model.load_failed": "Failed to load providers and models"
494
+ "system.ai.model.load_failed": "Failed to load providers and models",
495
+ "system.ai.model.title": "Select Model",
496
+ "system.ai.model.provider_model_count": "{providers} providers \xB7 {models} models",
497
+ "system.ai.model.empty": "No models available"
492
498
  };
493
499
 
494
500
  // src/locales/ja-JP.json
@@ -607,7 +613,10 @@ var ja_JP_default = {
607
613
  "system.notification.action.delete": "\u524A\u9664",
608
614
  "system.ai.model.provider_placeholder": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u9078\u629E",
609
615
  "system.ai.model.model_placeholder": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
610
- "system.ai.model.load_failed": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3068\u30E2\u30C7\u30EB\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F"
616
+ "system.ai.model.load_failed": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3068\u30E2\u30C7\u30EB\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F",
617
+ "system.ai.model.title": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
618
+ "system.ai.model.provider_model_count": "{providers} \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC \xB7 {models} \u30E2\u30C7\u30EB",
619
+ "system.ai.model.empty": "\u9078\u629E\u53EF\u80FD\u306A\u30E2\u30C7\u30EB\u304C\u3042\u308A\u307E\u305B\u3093"
611
620
  };
612
621
 
613
622
  // src/locales/zh-TW.json
@@ -726,7 +735,10 @@ var zh_TW_default = {
726
735
  "system.notification.action.delete": "\u522A\u9664",
727
736
  "system.ai.model.provider_placeholder": "\u9078\u64C7\u4F9B\u61C9\u5546",
728
737
  "system.ai.model.model_placeholder": "\u9078\u64C7\u6A21\u578B",
729
- "system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557"
738
+ "system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557",
739
+ "system.ai.model.title": "\u9078\u64C7\u6A21\u578B",
740
+ "system.ai.model.provider_model_count": "{providers} \u500B\u4F9B\u61C9\u5546 \xB7 {models} \u500B\u6A21\u578B",
741
+ "system.ai.model.empty": "\u66AB\u7121\u53EF\u9078\u6A21\u578B"
730
742
  };
731
743
 
732
744
  // src/locales/generated.ts
package/dist/module.mjs CHANGED
@@ -149,7 +149,10 @@ var zh_CN_default = {
149
149
  "system.notification.action.delete": "\u5220\u9664",
150
150
  "system.ai.model.provider_placeholder": "\u9009\u62E9\u4F9B\u5E94\u5546",
151
151
  "system.ai.model.model_placeholder": "\u9009\u62E9\u6A21\u578B",
152
- "system.ai.model.load_failed": "\u52A0\u8F7D\u4F9B\u5E94\u5546\u548C\u6A21\u578B\u5931\u8D25"
152
+ "system.ai.model.load_failed": "\u52A0\u8F7D\u4F9B\u5E94\u5546\u548C\u6A21\u578B\u5931\u8D25",
153
+ "system.ai.model.title": "\u9009\u62E9\u6A21\u578B",
154
+ "system.ai.model.provider_model_count": "{providers} \u4E2A\u4F9B\u5E94\u5546 \xB7 {models} \u4E2A\u6A21\u578B",
155
+ "system.ai.model.empty": "\u6682\u65E0\u53EF\u9009\u6A21\u578B"
153
156
  };
154
157
 
155
158
  // src/locales/en-US.json
@@ -268,7 +271,10 @@ var en_US_default = {
268
271
  "system.notification.action.delete": "Delete",
269
272
  "system.ai.model.provider_placeholder": "Select a provider",
270
273
  "system.ai.model.model_placeholder": "Select a model",
271
- "system.ai.model.load_failed": "Failed to load providers and models"
274
+ "system.ai.model.load_failed": "Failed to load providers and models",
275
+ "system.ai.model.title": "Select Model",
276
+ "system.ai.model.provider_model_count": "{providers} providers \xB7 {models} models",
277
+ "system.ai.model.empty": "No models available"
272
278
  };
273
279
 
274
280
  // src/locales/ja-JP.json
@@ -387,7 +393,10 @@ var ja_JP_default = {
387
393
  "system.notification.action.delete": "\u524A\u9664",
388
394
  "system.ai.model.provider_placeholder": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u9078\u629E",
389
395
  "system.ai.model.model_placeholder": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
390
- "system.ai.model.load_failed": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3068\u30E2\u30C7\u30EB\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F"
396
+ "system.ai.model.load_failed": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3068\u30E2\u30C7\u30EB\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F",
397
+ "system.ai.model.title": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
398
+ "system.ai.model.provider_model_count": "{providers} \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC \xB7 {models} \u30E2\u30C7\u30EB",
399
+ "system.ai.model.empty": "\u9078\u629E\u53EF\u80FD\u306A\u30E2\u30C7\u30EB\u304C\u3042\u308A\u307E\u305B\u3093"
391
400
  };
392
401
 
393
402
  // src/locales/zh-TW.json
@@ -506,7 +515,10 @@ var zh_TW_default = {
506
515
  "system.notification.action.delete": "\u522A\u9664",
507
516
  "system.ai.model.provider_placeholder": "\u9078\u64C7\u4F9B\u61C9\u5546",
508
517
  "system.ai.model.model_placeholder": "\u9078\u64C7\u6A21\u578B",
509
- "system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557"
518
+ "system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557",
519
+ "system.ai.model.title": "\u9078\u64C7\u6A21\u578B",
520
+ "system.ai.model.provider_model_count": "{providers} \u500B\u4F9B\u61C9\u5546 \xB7 {models} \u500B\u6A21\u578B",
521
+ "system.ai.model.empty": "\u66AB\u7121\u53EF\u9078\u6A21\u578B"
510
522
  };
511
523
 
512
524
  // src/locales/generated.ts
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "type": "git",
8
8
  "url": "git@github.com:liujitcn/kratos-admin.git"
9
9
  },
10
- "version": "0.0.64",
10
+ "version": "0.0.65",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -35,7 +35,7 @@
35
35
  "@dcloudio/uni-app": "3.0.0-5010520260709002",
36
36
  "@dcloudio/uni-ui": "^1.4.28",
37
37
  "vue": "^3.4.21",
38
- "@liujitcn/kratos-uni-app-core": "^0.0.64"
38
+ "@liujitcn/kratos-uni-app-core": "^0.0.65"
39
39
  },
40
40
  "devDependencies": {
41
41
  "esbuild": "^0.20.2",
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AuthPromptError,
2
3
  getRequestAccessToken,
3
4
  handleAuthExpired,
4
5
  http,
@@ -92,7 +93,7 @@ export async function SendAiMessageStream(
92
93
  // direct stream 不经过 uni.request 拦截器,需要在这里补齐登录失效处理。
93
94
  if (response.status === 401 || response.status === 403) {
94
95
  handleAuthExpired('required')
95
- throw new Error(t('core.auth.session_expired'))
96
+ throw new AuthPromptError()
96
97
  }
97
98
  if (!response.ok) {
98
99
  throw new Error(await resolveStreamErrorMessage(response))
@@ -140,7 +141,7 @@ export function StreamAiMessageByChunkedRequest(
140
141
  }
141
142
  if (res.statusCode === 401 || res.statusCode === 403) {
142
143
  handleAuthExpired('required')
143
- reject(new Error(t('core.auth.session_expired')))
144
+ reject(new AuthPromptError())
144
145
  return
145
146
  }
146
147
  if (res.statusCode < 200 || res.statusCode >= 300) {
@@ -113,5 +113,8 @@
113
113
  "system.notification.action.delete": "Delete",
114
114
  "system.ai.model.provider_placeholder": "Select a provider",
115
115
  "system.ai.model.model_placeholder": "Select a model",
116
- "system.ai.model.load_failed": "Failed to load providers and models"
116
+ "system.ai.model.load_failed": "Failed to load providers and models",
117
+ "system.ai.model.title": "Select Model",
118
+ "system.ai.model.provider_model_count": "{providers} providers · {models} models",
119
+ "system.ai.model.empty": "No models available"
117
120
  }
@@ -113,5 +113,8 @@
113
113
  "system.notification.action.delete": "削除",
114
114
  "system.ai.model.provider_placeholder": "プロバイダーを選択",
115
115
  "system.ai.model.model_placeholder": "モデルを選択",
116
- "system.ai.model.load_failed": "プロバイダーとモデルを読み込めませんでした"
116
+ "system.ai.model.load_failed": "プロバイダーとモデルを読み込めませんでした",
117
+ "system.ai.model.title": "モデルを選択",
118
+ "system.ai.model.provider_model_count": "{providers} プロバイダー · {models} モデル",
119
+ "system.ai.model.empty": "選択可能なモデルがありません"
117
120
  }
@@ -113,5 +113,8 @@
113
113
  "system.notification.action.delete": "删除",
114
114
  "system.ai.model.provider_placeholder": "选择供应商",
115
115
  "system.ai.model.model_placeholder": "选择模型",
116
- "system.ai.model.load_failed": "加载供应商和模型失败"
116
+ "system.ai.model.load_failed": "加载供应商和模型失败",
117
+ "system.ai.model.title": "选择模型",
118
+ "system.ai.model.provider_model_count": "{providers} 个供应商 · {models} 个模型",
119
+ "system.ai.model.empty": "暂无可选模型"
117
120
  }
@@ -113,5 +113,8 @@
113
113
  "system.notification.action.delete": "刪除",
114
114
  "system.ai.model.provider_placeholder": "選擇供應商",
115
115
  "system.ai.model.model_placeholder": "選擇模型",
116
- "system.ai.model.load_failed": "載入供應商與模型失敗"
116
+ "system.ai.model.load_failed": "載入供應商與模型失敗",
117
+ "system.ai.model.title": "選擇模型",
118
+ "system.ai.model.provider_model_count": "{providers} 個供應商 · {models} 個模型",
119
+ "system.ai.model.empty": "暫無可選模型"
117
120
  }
@@ -10,20 +10,28 @@
10
10
  export interface ListAiProviderModelOptionsRequest {
11
11
  }
12
12
 
13
+ /** AI Provider模型选项查询响应。 */
14
+ export interface ListAiProviderModelOptionsResponse {
15
+ /** Provider及其模型列表 */
16
+ providers: AiProviderModelOption[];
17
+ }
18
+
13
19
  /** 已启用的AI Provider及其模型选项。 */
14
20
  export interface AiProviderModelOption {
15
21
  /** Provider ID */
16
22
  provider_id: number;
17
23
  /** Provider名称 */
18
24
  name: string;
19
- /** 模型名称列表 */
20
- models: string[];
25
+ /** 模型选项列表 */
26
+ models: AiProviderModel[];
21
27
  }
22
28
 
23
- /** AI Provider模型选项查询响应。 */
24
- export interface ListAiProviderModelOptionsResponse {
25
- /** Provider及其模型列表 */
26
- providers: AiProviderModelOption[];
29
+ /** AI Provider下的单个模型选项。 */
30
+ export interface AiProviderModel {
31
+ /** 模型名称 */
32
+ model_name: string;
33
+ /** 展示名称 */
34
+ display_name: string;
27
35
  }
28
36
 
29
37
  /** AI Provider 可选模型查询服务。 */
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
2
+ import { computed, ref } from 'vue'
3
3
  import type { AiAttachment } from '../../../../rpc/base/v1/ai_session'
4
4
  import type { AiProviderModelOption } from '../../../../rpc/base/v1/ai_provider'
5
+ import { fallbackModelDisplayName } from '../modelDisplay'
6
+ import ModelPicker from './ModelPicker.vue'
5
7
 
6
8
  type InputEventValue = {
7
9
  detail: {
@@ -17,7 +19,6 @@ const props = defineProps<{
17
19
  /** 当前选中的供应商编号和模型名称。 */
18
20
  providerId: number
19
21
  modelName: string
20
- providerPlaceholder: string
21
22
  modelPlaceholder: string
22
23
  placeholder: string
23
24
  bottom: string
@@ -35,56 +36,41 @@ const emit = defineEmits<{
35
36
  'model-change': [providerId: number, modelName: string]
36
37
  }>()
37
38
 
38
- const providerNames = computed(() => props.modelProviders.map((provider) => provider.name))
39
- const selectedProvider = computed(() =>
40
- props.modelProviders.find((provider) => provider.provider_id === props.providerId),
41
- )
42
- const modelNames = computed(() => selectedProvider.value?.models ?? [])
43
- const providerIndex = computed(() =>
44
- Math.max(
45
- props.modelProviders.findIndex((provider) => provider.provider_id === props.providerId),
46
- 0,
47
- ),
48
- )
49
- const modelIndex = computed(() => Math.max(modelNames.value.indexOf(props.modelName), 0))
39
+ const showModelPicker = ref(false)
40
+
41
+ const selectedModelDisplayName = computed(() => {
42
+ const provider = props.modelProviders.find((item) => item.provider_id === props.providerId)
43
+ const matched = provider?.models.find((model) => model.model_name === props.modelName)
44
+ return matched?.display_name || fallbackModelDisplayName(props.modelName)
45
+ })
50
46
 
51
47
  function handleInput(event: Event) {
52
48
  emit('update:modelValue', ((event as unknown as InputEventValue).detail?.value || '').toString())
53
49
  }
54
50
 
55
- function handleProviderChange(event: { detail: { value: number | string } }) {
56
- const provider = props.modelProviders[Number(event.detail.value)]
57
- emit('model-change', provider?.provider_id ?? 0, provider?.models[0] ?? '')
51
+ function openModelPicker() {
52
+ if (props.sending || !props.modelProviders.length) {
53
+ return
54
+ }
55
+ showModelPicker.value = true
58
56
  }
59
57
 
60
- function handleModelChange(event: { detail: { value: number | string } }) {
61
- emit('model-change', props.providerId, modelNames.value[Number(event.detail.value)] ?? '')
58
+ function handleModelSelect(providerId: number, modelName: string) {
59
+ showModelPicker.value = false
60
+ emit('model-change', providerId, modelName)
62
61
  }
63
62
  </script>
64
63
 
65
64
  <template>
66
65
  <view class="composer" :style="{ paddingBottom: bottom }">
67
- <view class="composer-model-selectors">
68
- <picker
69
- mode="selector"
70
- :range="providerNames"
71
- :value="providerIndex"
72
- :disabled="sending || !modelProviders.length"
73
- @change="handleProviderChange"
74
- >
75
- <view class="composer-model-select">{{
76
- selectedProvider?.name || providerPlaceholder
77
- }}</view>
78
- </picker>
79
- <picker
80
- mode="selector"
81
- :range="modelNames"
82
- :value="modelIndex"
83
- :disabled="sending || !modelNames.length"
84
- @change="handleModelChange"
85
- >
86
- <view class="composer-model-select">{{ modelName || modelPlaceholder }}</view>
87
- </picker>
66
+ <view
67
+ class="composer-model-chip"
68
+ :class="{ 'is-disabled': sending || !modelProviders.length }"
69
+ @tap="openModelPicker"
70
+ >
71
+ <text class="composer-model-chip__spark">✦</text>
72
+ <text class="composer-model-chip__label">{{ selectedModelDisplayName || modelPlaceholder }}</text>
73
+ <uni-icons type="down" size="12" color="#8a8f99" />
88
74
  </view>
89
75
  <view class="composer-main">
90
76
  <button class="attach-button" hover-class="none" @tap="emit('attach')">
@@ -129,6 +115,15 @@ function handleModelChange(event: { detail: { value: number | string } }) {
129
115
  <uni-icons type="paperplane" size="28" :color="disabled ? '#111' : '#00a96b'" />
130
116
  </button>
131
117
  </view>
118
+
119
+ <ModelPicker
120
+ v-if="showModelPicker"
121
+ :providers="modelProviders"
122
+ :provider-id="providerId"
123
+ :model-name="modelName"
124
+ @select="handleModelSelect"
125
+ @close="showModelPicker = false"
126
+ />
132
127
  </view>
133
128
  </template>
134
129
 
@@ -168,27 +163,42 @@ function handleModelChange(event: { detail: { value: number | string } }) {
168
163
  box-sizing: border-box;
169
164
  }
170
165
 
171
- .composer-model-selectors {
172
- display: flex;
173
- justify-content: flex-end;
174
- gap: 12rpx;
175
- min-width: 0;
166
+ .composer-model-chip {
167
+ display: inline-flex;
168
+ align-items: center;
169
+ gap: 10rpx;
170
+ max-width: 100%;
171
+ height: 56rpx;
172
+ padding: 0 24rpx;
176
173
  margin: 0 8rpx 12rpx;
174
+ color: #1f2937;
175
+ background: #fff;
176
+ border: 2rpx solid #d6dae2;
177
+ border-radius: 28rpx;
178
+ box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04);
179
+ box-sizing: border-box;
180
+ }
181
+
182
+ .composer-model-chip.is-disabled {
183
+ opacity: 0.55;
184
+ }
185
+
186
+ .composer-model-chip__spark {
187
+ flex-shrink: 0;
188
+ color: #00a96b;
189
+ font-size: 26rpx;
190
+ line-height: 26rpx;
177
191
  }
178
192
 
179
- .composer-model-select {
180
- max-width: 300rpx;
181
- min-width: 120rpx;
182
- padding: 8rpx 16rpx;
193
+ .composer-model-chip__label {
194
+ max-width: 320rpx;
183
195
  overflow: hidden;
184
- color: #46515b;
185
- font-size: 22rpx;
186
- line-height: 32rpx;
196
+ color: inherit;
197
+ font-size: 24rpx;
198
+ font-weight: 500;
199
+ line-height: 34rpx;
187
200
  text-overflow: ellipsis;
188
201
  white-space: nowrap;
189
- background: #fff;
190
- border: 2rpx solid #d6dae2;
191
- border-radius: 8rpx;
192
202
  }
193
203
 
194
204
  .attach-button {
@@ -0,0 +1,268 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useI18n } from '@liujitcn/kratos-uni-app-core'
4
+ import type { AiProviderModelOption } from '../../../../rpc/base/v1/ai_provider'
5
+ import { fallbackModelDisplayName } from '../modelDisplay'
6
+
7
+ type PickerModel = {
8
+ name: string
9
+ displayName: string
10
+ }
11
+
12
+ type PickerProvider = {
13
+ providerId: number
14
+ label: string
15
+ models: PickerModel[]
16
+ }
17
+
18
+ const props = defineProps<{
19
+ providers: AiProviderModelOption[]
20
+ providerId: number
21
+ modelName: string
22
+ }>()
23
+
24
+ const emit = defineEmits<{
25
+ select: [providerId: number, modelName: string]
26
+ close: []
27
+ }>()
28
+
29
+ const { t } = useI18n()
30
+
31
+ const pickerProviders = computed<PickerProvider[]>(() =>
32
+ props.providers.map((provider) => ({
33
+ providerId: provider.provider_id,
34
+ label: provider.name,
35
+ models: provider.models.map((model) => ({
36
+ name: model.model_name,
37
+ displayName: model.display_name || fallbackModelDisplayName(model.model_name),
38
+ })),
39
+ })),
40
+ )
41
+
42
+ const modelCount = computed(() =>
43
+ pickerProviders.value.reduce((total, provider) => total + provider.models.length, 0),
44
+ )
45
+
46
+ function isSelected(providerId: number, modelName: string) {
47
+ return props.providerId === providerId && props.modelName === modelName
48
+ }
49
+
50
+ function handleSelect(providerId: number, modelName: string) {
51
+ emit('select', providerId, modelName)
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <view class="model-picker">
57
+ <view class="model-picker__mask" @tap="emit('close')" />
58
+ <view class="model-picker__sheet">
59
+ <view class="model-picker__handle" />
60
+ <view class="model-picker__head">
61
+ <text class="model-picker__title">{{ t('system.ai.model.title') }}</text>
62
+ <text class="model-picker__count">{{
63
+ t('system.ai.model.provider_model_count', {
64
+ providers: pickerProviders.length,
65
+ models: modelCount,
66
+ })
67
+ }}</text>
68
+ <button class="model-picker__close" hover-class="none" @tap="emit('close')">✕</button>
69
+ </view>
70
+ <scroll-view class="model-picker__body" scroll-y :show-scrollbar="true">
71
+ <block v-for="provider in pickerProviders" :key="provider.providerId">
72
+ <view class="model-picker__group">{{ provider.label }}</view>
73
+ <view
74
+ v-for="model in provider.models"
75
+ :key="model.name"
76
+ class="model-picker__row"
77
+ :class="{ 'is-selected': isSelected(provider.providerId, model.name) }"
78
+ @tap="handleSelect(provider.providerId, model.name)"
79
+ >
80
+ <view class="model-picker__row-main">
81
+ <text class="model-picker__row-name">{{ model.displayName }}</text>
82
+ <text v-if="model.displayName !== model.name" class="model-picker__row-raw">{{
83
+ model.name
84
+ }}</text>
85
+ </view>
86
+ <text v-if="isSelected(provider.providerId, model.name)" class="model-picker__row-check"
87
+ >✓</text
88
+ >
89
+ </view>
90
+ </block>
91
+ <view v-if="!pickerProviders.length" class="model-picker__empty">{{
92
+ t('system.ai.model.empty')
93
+ }}</view>
94
+ </scroll-view>
95
+ <view class="model-picker__safe" />
96
+ </view>
97
+ </view>
98
+ </template>
99
+
100
+ <style lang="scss" scoped>
101
+ .model-picker {
102
+ position: fixed;
103
+ inset: 0;
104
+ /* 高于 KratosTabBar 的 z-index:999,避免弹层被底部导航遮挡 */
105
+ z-index: 1000;
106
+ }
107
+
108
+ .model-picker__mask {
109
+ position: absolute;
110
+ inset: 0;
111
+ background-color: rgba(15, 23, 42, 0.45);
112
+ }
113
+
114
+ .model-picker__sheet {
115
+ position: absolute;
116
+ right: 0;
117
+ bottom: 0;
118
+ left: 0;
119
+ display: flex;
120
+ flex-direction: column;
121
+ max-height: 68vh;
122
+ overflow: hidden;
123
+ background-color: #fff;
124
+ border-radius: 32rpx 32rpx 0 0;
125
+ box-shadow: 0 -24rpx 80rpx rgba(15, 23, 42, 0.16);
126
+ box-sizing: border-box;
127
+ }
128
+
129
+ .model-picker__handle {
130
+ flex-shrink: 0;
131
+ width: 72rpx;
132
+ height: 8rpx;
133
+ margin: 16rpx auto 8rpx;
134
+ background-color: #e2e5ea;
135
+ border-radius: 4rpx;
136
+ }
137
+
138
+ .model-picker__head {
139
+ position: relative;
140
+ flex-shrink: 0;
141
+ display: flex;
142
+ align-items: baseline;
143
+ justify-content: center;
144
+ gap: 12rpx;
145
+ padding: 0 96rpx 16rpx;
146
+ }
147
+
148
+ .model-picker__title {
149
+ color: #111;
150
+ font-size: 32rpx;
151
+ font-weight: 600;
152
+ }
153
+
154
+ .model-picker__count {
155
+ color: #8a8f99;
156
+ font-size: 20rpx;
157
+ }
158
+
159
+ .model-picker__close {
160
+ position: absolute;
161
+ top: -6rpx;
162
+ right: 24rpx;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ width: 56rpx;
167
+ height: 56rpx;
168
+ padding: 0;
169
+ margin: 0;
170
+ color: #8a8f99;
171
+ font-size: 28rpx;
172
+ line-height: normal;
173
+ background: transparent;
174
+ border-radius: 0;
175
+ }
176
+
177
+ .model-picker__close::after {
178
+ border: 0;
179
+ }
180
+
181
+ .model-picker__body {
182
+ flex: 1;
183
+ min-height: 0;
184
+ max-height: calc(68vh - 130rpx - env(safe-area-inset-bottom));
185
+ padding: 0 32rpx;
186
+ box-sizing: border-box;
187
+ }
188
+
189
+ /* #ifdef H5 */
190
+ .model-picker__body :deep(::-webkit-scrollbar) {
191
+ width: 8rpx;
192
+ height: 8rpx;
193
+ }
194
+
195
+ .model-picker__body :deep(::-webkit-scrollbar-thumb) {
196
+ background-color: #c9ced6;
197
+ border-radius: 4rpx;
198
+ }
199
+
200
+ .model-picker__body :deep(::-webkit-scrollbar-track) {
201
+ background-color: transparent;
202
+ }
203
+ /* #endif */
204
+
205
+ .model-picker__group {
206
+ padding: 24rpx 8rpx 12rpx;
207
+ color: #8a8f99;
208
+ font-size: 22rpx;
209
+ line-height: 32rpx;
210
+ }
211
+
212
+ .model-picker__row {
213
+ display: flex;
214
+ align-items: center;
215
+ min-height: 88rpx;
216
+ padding: 16rpx 28rpx;
217
+ margin-bottom: 12rpx;
218
+ border: 2rpx solid #eef0f4;
219
+ border-radius: 24rpx;
220
+ box-sizing: border-box;
221
+ }
222
+
223
+ .model-picker__row.is-selected {
224
+ color: #0f7a62;
225
+ font-weight: 500;
226
+ background-color: #e8f8f4;
227
+ border-color: #d5efe7;
228
+ }
229
+
230
+ .model-picker__row-main {
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 4rpx;
234
+ min-width: 0;
235
+ }
236
+
237
+ .model-picker__row-name {
238
+ color: inherit;
239
+ font-size: 28rpx;
240
+ line-height: 40rpx;
241
+ }
242
+
243
+ .model-picker__row-raw {
244
+ color: #a7acb5;
245
+ font-size: 22rpx;
246
+ font-weight: 400;
247
+ line-height: 30rpx;
248
+ }
249
+
250
+ .model-picker__row-check {
251
+ margin-left: auto;
252
+ color: #00a96b;
253
+ font-size: 32rpx;
254
+ }
255
+
256
+ .model-picker__empty {
257
+ padding: 64rpx 0;
258
+ color: #8a8f99;
259
+ font-size: 26rpx;
260
+ text-align: center;
261
+ }
262
+
263
+ .model-picker__safe {
264
+ flex-shrink: 0;
265
+ height: calc(18rpx + constant(safe-area-inset-bottom));
266
+ height: calc(18rpx + env(safe-area-inset-bottom));
267
+ }
268
+ </style>
@@ -140,7 +140,8 @@ function formatSessionTime(session: AiSession) {
140
140
  right: 0;
141
141
  bottom: 0;
142
142
  left: 560rpx;
143
- z-index: 20;
143
+ /* 高于 KratosTabBar 的 z-index:999,避免抽屉被底部导航遮挡 */
144
+ z-index: 1000;
144
145
  background-color: rgba(0, 0, 0, 0.18);
145
146
  }
146
147
 
@@ -151,7 +152,7 @@ function formatSessionTime(session: AiSession) {
151
152
  top: 0;
152
153
  left: 0;
153
154
  bottom: 0;
154
- z-index: 21;
155
+ z-index: 1001;
155
156
  width: 560rpx;
156
157
  padding: 0 24rpx 36rpx;
157
158
  background-color: #fff;
@@ -16,6 +16,7 @@ import { AiMessageStatus } from '../../../rpc/base/v1/ai_session'
16
16
  import { Terminal } from '../../../rpc/base/v1/ai_tool'
17
17
  import { uploadFile } from '@liujitcn/kratos-uni-app-core/utils/file'
18
18
  import { formatSrc } from '@liujitcn/kratos-uni-app-core/utils/index'
19
+ import { isAuthExpiredError } from '@liujitcn/kratos-uni-app-core/utils/http'
19
20
  import Composer from './components/Composer.vue'
20
21
  import SessionDrawer from './components/SessionDrawer.vue'
21
22
  import WelcomePanel from './components/WelcomePanel.vue'
@@ -27,6 +28,8 @@ import {
27
28
  parseAiEventStreamText,
28
29
  readAiEventStream,
29
30
  } from './stream'
31
+ import { resolveModelDisplayName } from './modelDisplay'
32
+ import { loadStoredModelSelection, saveModelSelection } from './modelSelection'
30
33
 
31
34
  type ChatRole = 'user' | 'ai'
32
35
 
@@ -474,17 +477,7 @@ async function loadAiShortcuts() {
474
477
  defAiModelService.ListAiProviderModelOptions({}),
475
478
  ])
476
479
  modelProviders.value = providerResponse.providers ?? []
477
- if (
478
- !modelProviders.value.some(
479
- (item) =>
480
- item.provider_id === selectedProviderId.value &&
481
- item.models.includes(selectedModelName.value),
482
- )
483
- ) {
484
- const firstProvider = modelProviders.value[0]
485
- selectedProviderId.value = firstProvider?.provider_id ?? 0
486
- selectedModelName.value = firstProvider?.models[0] ?? ''
487
- }
480
+ applyModelSelection()
488
481
  const shortcuts = normalizeStarterShortcuts(response.shortcuts).filter((item) => !item.action)
489
482
  if (shortcuts.length) {
490
483
  starterShortcuts.value = shortcuts
@@ -500,10 +493,31 @@ async function loadAiShortcuts() {
500
493
  }
501
494
  }
502
495
 
503
- /** 更新当前聊天使用的供应商和模型。 */
496
+ /** 优先恢复本地缓存的模型选择,缓存失效时回退到首个可用模型。 */
497
+ function applyModelSelection() {
498
+ const stored = loadStoredModelSelection()
499
+ const provider = stored
500
+ ? modelProviders.value.find(
501
+ (item) =>
502
+ item.provider_id === stored.provider_id &&
503
+ item.models.some((model) => model.model_name === stored.model_name),
504
+ )
505
+ : undefined
506
+ if (provider && stored) {
507
+ selectedProviderId.value = provider.provider_id
508
+ selectedModelName.value = stored.model_name
509
+ return
510
+ }
511
+ const firstProvider = modelProviders.value[0]
512
+ selectedProviderId.value = firstProvider?.provider_id ?? 0
513
+ selectedModelName.value = firstProvider?.models[0]?.model_name ?? ''
514
+ }
515
+
516
+ /** 更新当前聊天使用的供应商和模型并持久化到本地缓存。 */
504
517
  function handleModelChange(providerId: number, modelName: string) {
505
518
  selectedProviderId.value = providerId
506
519
  selectedModelName.value = modelName
520
+ saveModelSelection({ provider_id: providerId, model_name: modelName })
507
521
  }
508
522
 
509
523
  async function sendAiPayload(payload: { text: string; attachments: AiAttachment[] }) {
@@ -1224,6 +1238,8 @@ function formatTools(tools: AiToolCall[]) {
1224
1238
  }
1225
1239
 
1226
1240
  function showError(error: unknown, fallback: string) {
1241
+ // 登录态失效时请求层已弹窗引导重新登录,不再叠加失败提示。
1242
+ if (isAuthExpiredError(error)) return
1227
1243
  const message = error instanceof Error ? error.message : fallback
1228
1244
  uni.showToast({ icon: 'none', title: message || fallback })
1229
1245
  }
@@ -1277,7 +1293,7 @@ function showError(error: unknown, fallback: string) {
1277
1293
  >
1278
1294
  <view v-if="item.role === 'ai' && item.model" class="reply-meta">
1279
1295
  <text class="reply-tag">{{ t('system.ai.model_reply') }}</text>
1280
- <text class="reply-model">{{ item.model }}</text>
1296
+ <text class="reply-model">{{ resolveModelDisplayName(modelProviders, item.model) }}</text>
1281
1297
  </view>
1282
1298
  <view class="bubble-content">{{ item.content }}</view>
1283
1299
  <view v-if="item.attachments.length" class="attachment-list">
@@ -1316,7 +1332,6 @@ function showError(error: unknown, fallback: string) {
1316
1332
  :model-providers="modelProviders"
1317
1333
  :provider-id="selectedProviderId"
1318
1334
  :model-name="selectedModelName"
1319
- :provider-placeholder="t('system.ai.model.provider_placeholder')"
1320
1335
  :model-placeholder="t('system.ai.model.model_placeholder')"
1321
1336
  :placeholder="composerPlaceholder"
1322
1337
  :bottom="composerBottom"
@@ -0,0 +1,77 @@
1
+ import type { AiProviderModelOption } from '../../../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) {
39
+ return ''
40
+ }
41
+ const mapped = MODEL_DISPLAY_NAMES[modelName.toLowerCase()]
42
+ if (mapped) {
43
+ return mapped
44
+ }
45
+ return modelName
46
+ .split(/[-_]/)
47
+ .filter(Boolean)
48
+ .map((token) => {
49
+ const lower = token.toLowerCase()
50
+ if (UPPERCASE_TOKENS.has(lower)) {
51
+ return lower.toUpperCase()
52
+ }
53
+ return lower.charAt(0).toUpperCase() + lower.slice(1)
54
+ })
55
+ .join(' ')
56
+ }
57
+
58
+ /** 从供应商模型选项中解析模型展示名称,兼容历史消息中「供应商 / 模型」格式的原始值。 */
59
+ export function resolveModelDisplayName(
60
+ providers: AiProviderModelOption[] | undefined,
61
+ modelName: string,
62
+ ): string {
63
+ if (!modelName) {
64
+ return ''
65
+ }
66
+ const separator = ' / '
67
+ const modelPart = modelName.includes(separator)
68
+ ? modelName.slice(modelName.lastIndexOf(separator) + separator.length)
69
+ : modelName
70
+ for (const provider of providers ?? []) {
71
+ const matched = provider.models.find((item) => item.model_name === modelPart)
72
+ if (matched?.display_name) {
73
+ return matched.display_name
74
+ }
75
+ }
76
+ return fallbackModelDisplayName(modelPart)
77
+ }
@@ -0,0 +1,38 @@
1
+ /** 用户最近一次选择的供应商与模型,仅保存在设备本地。 */
2
+ export interface AiModelSelection {
3
+ provider_id: number
4
+ model_name: string
5
+ }
6
+
7
+ const MODEL_SELECTION_STORAGE_KEY = 'kratos-uni-app:ai:model-selection'
8
+
9
+ /** 读取本地缓存的模型选择,缓存缺失或损坏时返回 undefined。 */
10
+ export function loadStoredModelSelection(): AiModelSelection | undefined {
11
+ try {
12
+ const raw = uni.getStorageSync(MODEL_SELECTION_STORAGE_KEY) as unknown
13
+ if (!raw) {
14
+ return undefined
15
+ }
16
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
17
+ const value = parsed as Partial<AiModelSelection>
18
+ if (
19
+ typeof value.provider_id === 'number' &&
20
+ typeof value.model_name === 'string' &&
21
+ value.model_name
22
+ ) {
23
+ return { provider_id: value.provider_id, model_name: value.model_name }
24
+ }
25
+ } catch {
26
+ // 缓存损坏或存储不可用时忽略,回退到默认选择。
27
+ }
28
+ return undefined
29
+ }
30
+
31
+ /** 持久化模型选择,存储不可用时静默失败,选择仍对当前会话生效。 */
32
+ export function saveModelSelection(selection: AiModelSelection): void {
33
+ try {
34
+ uni.setStorageSync(MODEL_SELECTION_STORAGE_KEY, JSON.stringify(selection))
35
+ } catch {
36
+ // 忽略存储失败,不影响当前会话内的选择。
37
+ }
38
+ }