@liujitcn/kratos-taro-app-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.
- package/dist/module.mjs +16 -4
- package/package.json +3 -3
- 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.tsx +42 -28
- package/src/views/pagesMember/ai/components/ModelPicker.tsx +95 -0
- package/src/views/pagesMember/ai/components/composer.scss +37 -15
- package/src/views/pagesMember/ai/components/model-picker.scss +147 -0
- package/src/views/pagesMember/ai/index.tsx +28 -7
- package/src/views/pagesMember/ai/modelDisplay.ts +77 -0
- package/src/views/pagesMember/ai/modelSelection.ts +40 -0
- package/src/views/pagesMember/settings/settings.tsx +1 -9
package/dist/module.mjs
CHANGED
|
@@ -167,7 +167,10 @@ var zh_CN_default = {
|
|
|
167
167
|
"system.notification.action.delete": "\u5220\u9664",
|
|
168
168
|
"system.ai.model.provider_placeholder": "\u9009\u62E9\u4F9B\u5E94\u5546",
|
|
169
169
|
"system.ai.model.model_placeholder": "\u9009\u62E9\u6A21\u578B",
|
|
170
|
-
"system.ai.model.load_failed": "\u52A0\u8F7D\u4F9B\u5E94\u5546\u548C\u6A21\u578B\u5931\u8D25"
|
|
170
|
+
"system.ai.model.load_failed": "\u52A0\u8F7D\u4F9B\u5E94\u5546\u548C\u6A21\u578B\u5931\u8D25",
|
|
171
|
+
"system.ai.model.title": "\u9009\u62E9\u6A21\u578B",
|
|
172
|
+
"system.ai.model.provider_model_count": "{providers} \u4E2A\u4F9B\u5E94\u5546 \xB7 {models} \u4E2A\u6A21\u578B",
|
|
173
|
+
"system.ai.model.empty": "\u6682\u65E0\u53EF\u9009\u6A21\u578B"
|
|
171
174
|
};
|
|
172
175
|
|
|
173
176
|
// src/locales/en-US.json
|
|
@@ -304,7 +307,10 @@ var en_US_default = {
|
|
|
304
307
|
"system.notification.action.delete": "Delete",
|
|
305
308
|
"system.ai.model.provider_placeholder": "Select a provider",
|
|
306
309
|
"system.ai.model.model_placeholder": "Select a model",
|
|
307
|
-
"system.ai.model.load_failed": "Failed to load providers and models"
|
|
310
|
+
"system.ai.model.load_failed": "Failed to load providers and models",
|
|
311
|
+
"system.ai.model.title": "Select Model",
|
|
312
|
+
"system.ai.model.provider_model_count": "{providers} providers \xB7 {models} models",
|
|
313
|
+
"system.ai.model.empty": "No models available"
|
|
308
314
|
};
|
|
309
315
|
|
|
310
316
|
// src/locales/ja-JP.json
|
|
@@ -441,7 +447,10 @@ var ja_JP_default = {
|
|
|
441
447
|
"system.notification.action.delete": "\u524A\u9664",
|
|
442
448
|
"system.ai.model.provider_placeholder": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u9078\u629E",
|
|
443
449
|
"system.ai.model.model_placeholder": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
|
|
444
|
-
"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"
|
|
450
|
+
"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",
|
|
451
|
+
"system.ai.model.title": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
|
|
452
|
+
"system.ai.model.provider_model_count": "{providers} \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC \xB7 {models} \u30E2\u30C7\u30EB",
|
|
453
|
+
"system.ai.model.empty": "\u9078\u629E\u53EF\u80FD\u306A\u30E2\u30C7\u30EB\u304C\u3042\u308A\u307E\u305B\u3093"
|
|
445
454
|
};
|
|
446
455
|
|
|
447
456
|
// src/locales/zh-TW.json
|
|
@@ -578,7 +587,10 @@ var zh_TW_default = {
|
|
|
578
587
|
"system.notification.action.delete": "\u522A\u9664",
|
|
579
588
|
"system.ai.model.provider_placeholder": "\u9078\u64C7\u4F9B\u61C9\u5546",
|
|
580
589
|
"system.ai.model.model_placeholder": "\u9078\u64C7\u6A21\u578B",
|
|
581
|
-
"system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557"
|
|
590
|
+
"system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557",
|
|
591
|
+
"system.ai.model.title": "\u9078\u64C7\u6A21\u578B",
|
|
592
|
+
"system.ai.model.provider_model_count": "{providers} \u500B\u4F9B\u61C9\u5546 \xB7 {models} \u500B\u6A21\u578B",
|
|
593
|
+
"system.ai.model.empty": "\u66AB\u7121\u53EF\u9078\u6A21\u578B"
|
|
582
594
|
};
|
|
583
595
|
|
|
584
596
|
// 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.66",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"@tarojs/components": "4.2.1",
|
|
39
39
|
"@tarojs/taro": "4.2.1",
|
|
40
40
|
"react": "18.3.1",
|
|
41
|
-
"@liujitcn/kratos-taro-app-
|
|
42
|
-
"@liujitcn/kratos-taro-app-
|
|
41
|
+
"@liujitcn/kratos-taro-app-core": "^0.0.66",
|
|
42
|
+
"@liujitcn/kratos-taro-app-ui": "^0.0.66"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "pnpm run build:entries && pnpm pack --pack-destination ../../../dist/npm",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AuthPromptError,
|
|
2
3
|
getRequestAccessToken,
|
|
3
4
|
handleAuthExpired,
|
|
4
5
|
http,
|
|
@@ -94,7 +95,7 @@ export async function SendAiMessageStream(
|
|
|
94
95
|
// direct stream 不经过 uni.request 拦截器,需要在这里补齐登录失效处理。
|
|
95
96
|
if (response.status === 401 || response.status === 403) {
|
|
96
97
|
handleAuthExpired('required')
|
|
97
|
-
throw new
|
|
98
|
+
throw new AuthPromptError()
|
|
98
99
|
}
|
|
99
100
|
if (!response.ok) {
|
|
100
101
|
throw new Error(await resolveStreamErrorMessage(response))
|
|
@@ -142,7 +143,7 @@ export function StreamAiMessageByChunkedRequest(
|
|
|
142
143
|
}
|
|
143
144
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
144
145
|
handleAuthExpired('required')
|
|
145
|
-
reject(new
|
|
146
|
+
reject(new AuthPromptError())
|
|
146
147
|
return
|
|
147
148
|
}
|
|
148
149
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
package/src/locales/en-US.json
CHANGED
|
@@ -131,5 +131,8 @@
|
|
|
131
131
|
"system.notification.action.delete": "Delete",
|
|
132
132
|
"system.ai.model.provider_placeholder": "Select a provider",
|
|
133
133
|
"system.ai.model.model_placeholder": "Select a model",
|
|
134
|
-
"system.ai.model.load_failed": "Failed to load providers and models"
|
|
134
|
+
"system.ai.model.load_failed": "Failed to load providers and models",
|
|
135
|
+
"system.ai.model.title": "Select Model",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} providers · {models} models",
|
|
137
|
+
"system.ai.model.empty": "No models available"
|
|
135
138
|
}
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -131,5 +131,8 @@
|
|
|
131
131
|
"system.notification.action.delete": "削除",
|
|
132
132
|
"system.ai.model.provider_placeholder": "プロバイダーを選択",
|
|
133
133
|
"system.ai.model.model_placeholder": "モデルを選択",
|
|
134
|
-
"system.ai.model.load_failed": "プロバイダーとモデルを読み込めませんでした"
|
|
134
|
+
"system.ai.model.load_failed": "プロバイダーとモデルを読み込めませんでした",
|
|
135
|
+
"system.ai.model.title": "モデルを選択",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} プロバイダー · {models} モデル",
|
|
137
|
+
"system.ai.model.empty": "選択可能なモデルがありません"
|
|
135
138
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -131,5 +131,8 @@
|
|
|
131
131
|
"system.notification.action.delete": "删除",
|
|
132
132
|
"system.ai.model.provider_placeholder": "选择供应商",
|
|
133
133
|
"system.ai.model.model_placeholder": "选择模型",
|
|
134
|
-
"system.ai.model.load_failed": "加载供应商和模型失败"
|
|
134
|
+
"system.ai.model.load_failed": "加载供应商和模型失败",
|
|
135
|
+
"system.ai.model.title": "选择模型",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} 个供应商 · {models} 个模型",
|
|
137
|
+
"system.ai.model.empty": "暂无可选模型"
|
|
135
138
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -131,5 +131,8 @@
|
|
|
131
131
|
"system.notification.action.delete": "刪除",
|
|
132
132
|
"system.ai.model.provider_placeholder": "選擇供應商",
|
|
133
133
|
"system.ai.model.model_placeholder": "選擇模型",
|
|
134
|
-
"system.ai.model.load_failed": "載入供應商與模型失敗"
|
|
134
|
+
"system.ai.model.load_failed": "載入供應商與模型失敗",
|
|
135
|
+
"system.ai.model.title": "選擇模型",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} 個供應商 · {models} 個模型",
|
|
137
|
+
"system.ai.model.empty": "暫無可選模型"
|
|
135
138
|
}
|
|
@@ -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,10 @@
|
|
|
1
|
-
import { Button,
|
|
1
|
+
import { Button, Text, Textarea, View } from '@tarojs/components'
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
2
3
|
import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
|
|
3
4
|
import type { AiAttachment } from '../../../../rpc/base/v1/ai_session'
|
|
4
5
|
import type { AiProviderModelOption } from '../../../../rpc/base/v1/ai_provider'
|
|
6
|
+
import { fallbackModelDisplayName } from '../modelDisplay'
|
|
7
|
+
import ModelPicker from './ModelPicker'
|
|
5
8
|
import './composer.scss'
|
|
6
9
|
|
|
7
10
|
/** AI 助手输入框与模型选择器参数。 */
|
|
@@ -14,7 +17,6 @@ type ComposerProps = {
|
|
|
14
17
|
providerId: number
|
|
15
18
|
modelName: string
|
|
16
19
|
/** 选择器未配置时显示的本地化占位文字。 */
|
|
17
|
-
providerPlaceholder: string
|
|
18
20
|
modelPlaceholder: string
|
|
19
21
|
placeholder: string
|
|
20
22
|
bottom: string
|
|
@@ -31,35 +33,37 @@ type ComposerProps = {
|
|
|
31
33
|
|
|
32
34
|
/** AI 助手消息输入器。 */
|
|
33
35
|
export default function Composer(props: ComposerProps) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
const [showModelPicker, setShowModelPicker] = useState(false)
|
|
37
|
+
|
|
38
|
+
const selectedModelDisplayName = useMemo(() => {
|
|
39
|
+
const provider = props.modelProviders.find((item) => item.provider_id === props.providerId)
|
|
40
|
+
const matched = provider?.models.find((model) => model.model_name === props.modelName)
|
|
41
|
+
return matched?.display_name || fallbackModelDisplayName(props.modelName)
|
|
42
|
+
}, [props.modelProviders, props.providerId, props.modelName])
|
|
43
|
+
|
|
44
|
+
const openModelPicker = () => {
|
|
45
|
+
if (props.sending || !props.modelProviders.length) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
setShowModelPicker(true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleModelSelect = (providerId: number, modelName: string) => {
|
|
52
|
+
setShowModelPicker(false)
|
|
53
|
+
props.onModelChange(providerId, modelName)
|
|
54
|
+
}
|
|
38
55
|
|
|
39
56
|
return (
|
|
40
57
|
<View className='composer' style={{ paddingBottom: props.bottom }}>
|
|
41
|
-
<View
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}}
|
|
51
|
-
>
|
|
52
|
-
<View className='composer-model-select'><Text>{selectedProvider?.name || props.providerPlaceholder}</Text></View>
|
|
53
|
-
</Picker>
|
|
54
|
-
<Picker
|
|
55
|
-
mode='selector'
|
|
56
|
-
range={modelNames}
|
|
57
|
-
value={modelIndex}
|
|
58
|
-
disabled={props.sending || !modelNames.length}
|
|
59
|
-
onChange={(event) => props.onModelChange(props.providerId, modelNames[Number(event.detail.value)] ?? '')}
|
|
60
|
-
>
|
|
61
|
-
<View className='composer-model-select'><Text>{props.modelName || props.modelPlaceholder}</Text></View>
|
|
62
|
-
</Picker>
|
|
58
|
+
<View
|
|
59
|
+
className={`composer-model-chip${props.sending || !props.modelProviders.length ? ' is-disabled' : ''}`}
|
|
60
|
+
onClick={openModelPicker}
|
|
61
|
+
>
|
|
62
|
+
<Text className='composer-model-chip__spark'>✦</Text>
|
|
63
|
+
<Text className='composer-model-chip__label'>
|
|
64
|
+
{selectedModelDisplayName || props.modelPlaceholder}
|
|
65
|
+
</Text>
|
|
66
|
+
<Text className='composer-model-chip__chevron'>▾</Text>
|
|
63
67
|
</View>
|
|
64
68
|
<View className='composer-main'>
|
|
65
69
|
<Button className='attach-button' hoverClass='none' onClick={props.onAttach}>
|
|
@@ -105,6 +109,16 @@ export default function Composer(props: ComposerProps) {
|
|
|
105
109
|
<UniIcon type='paperplane' size={28} color={props.disabled ? '#111' : '#00a96b'} />
|
|
106
110
|
</Button>
|
|
107
111
|
</View>
|
|
112
|
+
|
|
113
|
+
{showModelPicker ? (
|
|
114
|
+
<ModelPicker
|
|
115
|
+
providers={props.modelProviders}
|
|
116
|
+
providerId={props.providerId}
|
|
117
|
+
modelName={props.modelName}
|
|
118
|
+
onSelect={handleModelSelect}
|
|
119
|
+
onClose={() => setShowModelPicker(false)}
|
|
120
|
+
/>
|
|
121
|
+
) : null}
|
|
108
122
|
</View>
|
|
109
123
|
)
|
|
110
124
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ScrollView, Text, View } from '@tarojs/components'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { t } from '@liujitcn/kratos-taro-app-core'
|
|
4
|
+
import type { AiProviderModelOption } from '../../../../rpc/base/v1/ai_provider'
|
|
5
|
+
import { fallbackModelDisplayName } from '../modelDisplay'
|
|
6
|
+
import './model-picker.scss'
|
|
7
|
+
|
|
8
|
+
type PickerModel = {
|
|
9
|
+
name: string
|
|
10
|
+
displayName: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PickerProvider = {
|
|
14
|
+
providerId: number
|
|
15
|
+
label: string
|
|
16
|
+
models: PickerModel[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ModelPickerProps = {
|
|
20
|
+
providers: AiProviderModelOption[]
|
|
21
|
+
providerId: number
|
|
22
|
+
modelName: string
|
|
23
|
+
onSelect: (providerId: number, modelName: string) => void
|
|
24
|
+
onClose: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** AI 模型选择底部弹层。 */
|
|
28
|
+
export default function ModelPicker(props: ModelPickerProps) {
|
|
29
|
+
const pickerProviders = useMemo<PickerProvider[]>(
|
|
30
|
+
() =>
|
|
31
|
+
props.providers.map((provider) => ({
|
|
32
|
+
providerId: provider.provider_id,
|
|
33
|
+
label: provider.name,
|
|
34
|
+
models: provider.models.map((model) => ({
|
|
35
|
+
name: model.model_name,
|
|
36
|
+
displayName: model.display_name || fallbackModelDisplayName(model.model_name),
|
|
37
|
+
})),
|
|
38
|
+
})),
|
|
39
|
+
[props.providers],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const modelCount = pickerProviders.reduce((total, provider) => total + provider.models.length, 0)
|
|
43
|
+
|
|
44
|
+
const isSelected = (providerId: number, modelName: string) =>
|
|
45
|
+
props.providerId === providerId && props.modelName === modelName
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View className='model-picker' catchMove>
|
|
49
|
+
<View className='model-picker__mask' onClick={props.onClose} />
|
|
50
|
+
<View className='model-picker__sheet'>
|
|
51
|
+
<View className='model-picker__handle' />
|
|
52
|
+
<View className='model-picker__head'>
|
|
53
|
+
<Text className='model-picker__title'>{t('system.ai.model.title')}</Text>
|
|
54
|
+
<Text className='model-picker__count'>
|
|
55
|
+
{t('system.ai.model.provider_model_count', {
|
|
56
|
+
providers: pickerProviders.length,
|
|
57
|
+
models: modelCount,
|
|
58
|
+
})}
|
|
59
|
+
</Text>
|
|
60
|
+
<View className='model-picker__close' onClick={props.onClose}>
|
|
61
|
+
✕
|
|
62
|
+
</View>
|
|
63
|
+
</View>
|
|
64
|
+
<ScrollView className='model-picker__body' scrollY>
|
|
65
|
+
{pickerProviders.map((provider) => (
|
|
66
|
+
<View key={provider.providerId}>
|
|
67
|
+
<View className='model-picker__group'>{provider.label}</View>
|
|
68
|
+
{provider.models.map((model) => (
|
|
69
|
+
<View
|
|
70
|
+
key={model.name}
|
|
71
|
+
className={`model-picker__row${isSelected(provider.providerId, model.name) ? ' is-selected' : ''}`}
|
|
72
|
+
onClick={() => props.onSelect(provider.providerId, model.name)}
|
|
73
|
+
>
|
|
74
|
+
<View className='model-picker__row-main'>
|
|
75
|
+
<Text className='model-picker__row-name'>{model.displayName}</Text>
|
|
76
|
+
{model.displayName !== model.name ? (
|
|
77
|
+
<Text className='model-picker__row-raw'>{model.name}</Text>
|
|
78
|
+
) : null}
|
|
79
|
+
</View>
|
|
80
|
+
{isSelected(provider.providerId, model.name) ? (
|
|
81
|
+
<Text className='model-picker__row-check'>✓</Text>
|
|
82
|
+
) : null}
|
|
83
|
+
</View>
|
|
84
|
+
))}
|
|
85
|
+
</View>
|
|
86
|
+
))}
|
|
87
|
+
{!pickerProviders.length ? (
|
|
88
|
+
<View className='model-picker__empty'>{t('system.ai.model.empty')}</View>
|
|
89
|
+
) : null}
|
|
90
|
+
</ScrollView>
|
|
91
|
+
<View className='model-picker__safe' />
|
|
92
|
+
</View>
|
|
93
|
+
</View>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -19,27 +19,49 @@
|
|
|
19
19
|
box-sizing: border-box;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
.composer-model-
|
|
23
|
-
display: flex;
|
|
24
|
-
|
|
25
|
-
gap:
|
|
26
|
-
|
|
22
|
+
.composer-model-chip {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 10px;
|
|
26
|
+
max-width: 100%;
|
|
27
|
+
height: 56px;
|
|
28
|
+
padding: 0 24px;
|
|
27
29
|
margin: 0 8px 12px;
|
|
30
|
+
color: #1f2937;
|
|
31
|
+
background: #fff;
|
|
32
|
+
border: 2px solid #d6dae2;
|
|
33
|
+
border-radius: 28px;
|
|
34
|
+
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.composer-model-chip.is-disabled {
|
|
39
|
+
opacity: 0.55;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.composer-model-chip__spark {
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
color: #00a96b;
|
|
45
|
+
font-size: 26px;
|
|
46
|
+
line-height: 26px;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
.composer-model-
|
|
31
|
-
max-width:
|
|
32
|
-
min-width: 120px;
|
|
33
|
-
padding: 8px 16px;
|
|
49
|
+
.composer-model-chip__label {
|
|
50
|
+
max-width: 320px;
|
|
34
51
|
overflow: hidden;
|
|
35
|
-
color:
|
|
36
|
-
font-size:
|
|
37
|
-
|
|
52
|
+
color: inherit;
|
|
53
|
+
font-size: 24px;
|
|
54
|
+
font-weight: 500;
|
|
55
|
+
line-height: 34px;
|
|
38
56
|
text-overflow: ellipsis;
|
|
39
57
|
white-space: nowrap;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.composer-model-chip__chevron {
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
color: #8a8f99;
|
|
63
|
+
font-size: 22px;
|
|
64
|
+
line-height: 22px;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
.composer-main {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
.model-picker {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
right: 0;
|
|
5
|
+
bottom: 0;
|
|
6
|
+
left: 0;
|
|
7
|
+
z-index: 100;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.model-picker__mask {
|
|
11
|
+
position: absolute;
|
|
12
|
+
top: 0;
|
|
13
|
+
right: 0;
|
|
14
|
+
bottom: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
background-color: rgba(15, 23, 42, 0.45);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.model-picker__sheet {
|
|
20
|
+
position: absolute;
|
|
21
|
+
right: 0;
|
|
22
|
+
bottom: 0;
|
|
23
|
+
left: 0;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
max-height: 68vh;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
background-color: #fff;
|
|
29
|
+
border-radius: 32px 32px 0 0;
|
|
30
|
+
box-shadow: 0 -24px 80px rgba(15, 23, 42, 0.16);
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.model-picker__handle {
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
width: 72px;
|
|
37
|
+
height: 8px;
|
|
38
|
+
margin: 16px auto 8px;
|
|
39
|
+
background-color: #e2e5ea;
|
|
40
|
+
border-radius: 4px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.model-picker__head {
|
|
44
|
+
position: relative;
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: baseline;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
gap: 12px;
|
|
50
|
+
padding: 0 96px 16px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.model-picker__title {
|
|
54
|
+
color: #111;
|
|
55
|
+
font-size: 32px;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.model-picker__count {
|
|
60
|
+
color: #8a8f99;
|
|
61
|
+
font-size: 20px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.model-picker__close {
|
|
65
|
+
position: absolute;
|
|
66
|
+
top: -6px;
|
|
67
|
+
right: 24px;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
width: 56px;
|
|
72
|
+
height: 56px;
|
|
73
|
+
color: #8a8f99;
|
|
74
|
+
font-size: 28px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.model-picker__body {
|
|
78
|
+
flex: 1;
|
|
79
|
+
min-height: 0;
|
|
80
|
+
max-height: calc(68vh - 130px - env(safe-area-inset-bottom));
|
|
81
|
+
padding: 0 32px;
|
|
82
|
+
box-sizing: border-box;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.model-picker__group {
|
|
86
|
+
padding: 24px 8px 12px;
|
|
87
|
+
color: #8a8f99;
|
|
88
|
+
font-size: 22px;
|
|
89
|
+
line-height: 32px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.model-picker__row {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
min-height: 88px;
|
|
96
|
+
padding: 16px 28px;
|
|
97
|
+
margin-bottom: 12px;
|
|
98
|
+
border: 2px solid #eef0f4;
|
|
99
|
+
border-radius: 24px;
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.model-picker__row.is-selected {
|
|
104
|
+
color: #0f7a62;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
background-color: #e8f8f4;
|
|
107
|
+
border-color: #d5efe7;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.model-picker__row-main {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
gap: 4px;
|
|
114
|
+
min-width: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.model-picker__row-name {
|
|
118
|
+
color: inherit;
|
|
119
|
+
font-size: 28px;
|
|
120
|
+
line-height: 40px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.model-picker__row-raw {
|
|
124
|
+
color: #a7acb5;
|
|
125
|
+
font-size: 22px;
|
|
126
|
+
font-weight: 400;
|
|
127
|
+
line-height: 30px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.model-picker__row-check {
|
|
131
|
+
margin-left: auto;
|
|
132
|
+
color: #00a96b;
|
|
133
|
+
font-size: 32px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.model-picker__empty {
|
|
137
|
+
padding: 64px 0;
|
|
138
|
+
color: #8a8f99;
|
|
139
|
+
font-size: 26px;
|
|
140
|
+
text-align: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.model-picker__safe {
|
|
144
|
+
flex-shrink: 0;
|
|
145
|
+
height: calc(18px + constant(safe-area-inset-bottom));
|
|
146
|
+
height: calc(18px + env(safe-area-inset-bottom));
|
|
147
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useI18n,
|
|
11
11
|
} from '@liujitcn/kratos-taro-app-core'
|
|
12
12
|
import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
|
|
13
|
+
import { isAuthExpiredError } from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
13
14
|
import { defAiMessageService, StreamAiMessageByChunkedRequest } from '../../../api/base/v1/ai_message'
|
|
14
15
|
import { defAiSessionService } from '../../../api/base/v1/ai_session'
|
|
15
16
|
import { defAiToolService } from '../../../api/base/v1/ai_tool'
|
|
@@ -22,6 +23,8 @@ import { Terminal } from '../../../rpc/base/v1/ai_tool'
|
|
|
22
23
|
import Composer from './components/Composer'
|
|
23
24
|
import SessionDrawer from './components/SessionDrawer'
|
|
24
25
|
import WelcomePanel from './components/WelcomePanel'
|
|
26
|
+
import { resolveModelDisplayName } from './modelDisplay'
|
|
27
|
+
import { loadStoredModelSelection, saveModelSelection } from './modelSelection'
|
|
25
28
|
import {
|
|
26
29
|
type AiStreamEvent,
|
|
27
30
|
type AiStreamPayload,
|
|
@@ -363,11 +366,7 @@ export default function AiPage() {
|
|
|
363
366
|
const shortcuts = normalizeStarterShortcuts(response.shortcuts).filter((item) => !item.action)
|
|
364
367
|
const providers = providerResponse.providers ?? []
|
|
365
368
|
setModelProviders(providers)
|
|
366
|
-
|
|
367
|
-
const firstProvider = providers[0]
|
|
368
|
-
setSelectedProviderId(firstProvider?.provider_id ?? 0)
|
|
369
|
-
setSelectedModelName(firstProvider?.models[0] ?? '')
|
|
370
|
-
}
|
|
369
|
+
applyModelSelection(providers)
|
|
371
370
|
if (shortcuts.length) {
|
|
372
371
|
setStarterShortcuts(shortcuts)
|
|
373
372
|
setStarterPromptGroupIndex(0)
|
|
@@ -385,6 +384,27 @@ export default function AiPage() {
|
|
|
385
384
|
const handleModelChange = (providerId: number, modelName: string) => {
|
|
386
385
|
setSelectedProviderId(providerId)
|
|
387
386
|
setSelectedModelName(modelName)
|
|
387
|
+
saveModelSelection({ provider_id: providerId, model_name: modelName })
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 优先恢复本地缓存的模型选择,缓存失效时回退到首个可用模型。 */
|
|
391
|
+
const applyModelSelection = (providers: AiProviderModelOption[]) => {
|
|
392
|
+
const stored = loadStoredModelSelection()
|
|
393
|
+
const provider = stored
|
|
394
|
+
? providers.find(
|
|
395
|
+
(item) =>
|
|
396
|
+
item.provider_id === stored.provider_id &&
|
|
397
|
+
item.models.some((model) => model.model_name === stored.model_name),
|
|
398
|
+
)
|
|
399
|
+
: undefined
|
|
400
|
+
if (provider && stored) {
|
|
401
|
+
setSelectedProviderId(provider.provider_id)
|
|
402
|
+
setSelectedModelName(stored.model_name)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
const firstProvider = providers[0]
|
|
406
|
+
setSelectedProviderId(firstProvider?.provider_id ?? 0)
|
|
407
|
+
setSelectedModelName(firstProvider?.models[0]?.model_name ?? '')
|
|
388
408
|
}
|
|
389
409
|
|
|
390
410
|
const runAiTask = async (
|
|
@@ -717,7 +737,7 @@ export default function AiPage() {
|
|
|
717
737
|
onLongPress={() => void handleMessageAction(item)}
|
|
718
738
|
>
|
|
719
739
|
{item.role === 'ai' && item.model ? (
|
|
720
|
-
<View className='reply-meta'><Text className='reply-tag'>{t('system.ai.model_reply')}</Text><Text className='reply-model'>{item.model}</Text></View>
|
|
740
|
+
<View className='reply-meta'><Text className='reply-tag'>{t('system.ai.model_reply')}</Text><Text className='reply-model'>{resolveModelDisplayName(modelProviders, item.model)}</Text></View>
|
|
721
741
|
) : null}
|
|
722
742
|
<View className='bubble-content'>{item.content}</View>
|
|
723
743
|
{item.attachments.length ? (
|
|
@@ -752,7 +772,6 @@ export default function AiPage() {
|
|
|
752
772
|
modelProviders={modelProviders}
|
|
753
773
|
providerId={selectedProviderId}
|
|
754
774
|
modelName={selectedModelName}
|
|
755
|
-
providerPlaceholder={t('system.ai.model.provider_placeholder')}
|
|
756
775
|
modelPlaceholder={t('system.ai.model.model_placeholder')}
|
|
757
776
|
placeholder={composerPlaceholder}
|
|
758
777
|
bottom={composerBottom}
|
|
@@ -1040,6 +1059,8 @@ function formatTools(tools: AiToolCall[]) {
|
|
|
1040
1059
|
}
|
|
1041
1060
|
|
|
1042
1061
|
function showError(error: unknown, fallback: string) {
|
|
1062
|
+
// 登录态失效时请求层已弹窗引导重新登录,不再叠加失败提示。
|
|
1063
|
+
if (isAuthExpiredError(error)) return
|
|
1043
1064
|
const message = error instanceof Error ? error.message : fallback
|
|
1044
1065
|
void Taro.showToast({ icon: 'none', title: message || fallback })
|
|
1045
1066
|
}
|
|
@@ -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,40 @@
|
|
|
1
|
+
import Taro from '@tarojs/taro'
|
|
2
|
+
|
|
3
|
+
/** 用户最近一次选择的供应商与模型,仅保存在设备本地。 */
|
|
4
|
+
export interface AiModelSelection {
|
|
5
|
+
provider_id: number
|
|
6
|
+
model_name: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const MODEL_SELECTION_STORAGE_KEY = 'kratos-taro-app:ai:model-selection'
|
|
10
|
+
|
|
11
|
+
/** 读取本地缓存的模型选择,缓存缺失或损坏时返回 undefined。 */
|
|
12
|
+
export function loadStoredModelSelection(): AiModelSelection | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const raw = Taro.getStorageSync<unknown>(MODEL_SELECTION_STORAGE_KEY)
|
|
15
|
+
if (!raw) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
19
|
+
const value = parsed as Partial<AiModelSelection>
|
|
20
|
+
if (
|
|
21
|
+
typeof value.provider_id === 'number' &&
|
|
22
|
+
typeof value.model_name === 'string' &&
|
|
23
|
+
value.model_name
|
|
24
|
+
) {
|
|
25
|
+
return { provider_id: value.provider_id, model_name: value.model_name }
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// 缓存损坏或存储不可用时忽略,回退到默认选择。
|
|
29
|
+
}
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 持久化模型选择,存储不可用时静默失败,选择仍对当前会话生效。 */
|
|
34
|
+
export function saveModelSelection(selection: AiModelSelection): void {
|
|
35
|
+
try {
|
|
36
|
+
Taro.setStorageSync(MODEL_SELECTION_STORAGE_KEY, JSON.stringify(selection))
|
|
37
|
+
} catch {
|
|
38
|
+
// 忽略存储失败,不影响当前会话内的选择。
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useUserStore,
|
|
10
10
|
} from '@liujitcn/kratos-taro-app-core'
|
|
11
11
|
import { defMfaService } from '@liujitcn/kratos-taro-app-core/api/base/v1/mfa'
|
|
12
|
+
import { isAuthExpiredError } from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
12
13
|
import PasswordVerifyDialog from './components/PasswordVerifyDialog'
|
|
13
14
|
import './settings.scss'
|
|
14
15
|
|
|
@@ -109,15 +110,6 @@ export default function SettingsPage() {
|
|
|
109
110
|
void loadMfaStatus()
|
|
110
111
|
})
|
|
111
112
|
|
|
112
|
-
/** 登录态失效时请求层已弹窗引导重新登录,页面侧不再叠加失败提示。 */
|
|
113
|
-
const isAuthExpiredError = (error: unknown) => {
|
|
114
|
-
if (error && typeof error === 'object') {
|
|
115
|
-
const statusCode = (error as { statusCode?: number }).statusCode
|
|
116
|
-
if (statusCode === 401 || statusCode === 403) return true
|
|
117
|
-
}
|
|
118
|
-
return error instanceof Error && /auth (required|expired)/.test(error.message)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
113
|
const onLogout = async () => {
|
|
122
114
|
if (logoutLoading) return
|
|
123
115
|
const result = await Taro.showModal({
|