@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 +16 -4
- package/dist/module.mjs +16 -4
- package/package.json +2 -2
- package/src/api/base/v1/ai_message.ts +3 -2
- package/src/locales/en-US.json +4 -1
- package/src/locales/ja-JP.json +4 -1
- package/src/locales/zh-CN.json +4 -1
- package/src/locales/zh-TW.json +4 -1
- package/src/rpc/base/v1/ai_provider.ts +14 -6
- package/src/views/pagesMember/ai/components/Composer.vue +65 -55
- package/src/views/pagesMember/ai/components/ModelPicker.vue +268 -0
- package/src/views/pagesMember/ai/components/SessionDrawer.vue +3 -2
- package/src/views/pagesMember/ai/index.vue +29 -14
- package/src/views/pagesMember/ai/modelDisplay.ts +77 -0
- package/src/views/pagesMember/ai/modelSelection.ts +38 -0
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.
|
|
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.
|
|
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
|
|
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
|
|
144
|
+
reject(new AuthPromptError())
|
|
144
145
|
return
|
|
145
146
|
}
|
|
146
147
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
package/src/locales/en-US.json
CHANGED
|
@@ -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
|
}
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -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
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -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
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -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:
|
|
25
|
+
/** 模型选项列表 */
|
|
26
|
+
models: AiProviderModel[];
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
/** AI Provider
|
|
24
|
-
export interface
|
|
25
|
-
/**
|
|
26
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
function openModelPicker() {
|
|
52
|
+
if (props.sending || !props.modelProviders.length) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
showModelPicker.value = true
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
function
|
|
61
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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-
|
|
172
|
-
display: flex;
|
|
173
|
-
|
|
174
|
-
gap:
|
|
175
|
-
|
|
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-
|
|
180
|
-
max-width:
|
|
181
|
-
min-width: 120rpx;
|
|
182
|
-
padding: 8rpx 16rpx;
|
|
193
|
+
.composer-model-chip__label {
|
|
194
|
+
max-width: 320rpx;
|
|
183
195
|
overflow: hidden;
|
|
184
|
-
color:
|
|
185
|
-
font-size:
|
|
186
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|