@liujitcn/kratos-uni-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/index.mjs +36 -4
- package/dist/module.mjs +36 -4
- package/package.json +2 -2
- package/src/api/base/v1/ai_knowledge.ts +25 -0
- package/src/api/base/v1/ai_message.ts +3 -2
- package/src/locales/en-US.json +9 -1
- package/src/locales/ja-JP.json +9 -1
- package/src/locales/zh-CN.json +9 -1
- package/src/locales/zh-TW.json +9 -1
- package/src/rpc/base/v1/ai_knowledge.ts +33 -0
- package/src/rpc/base/v1/ai_message.ts +2 -0
- package/src/rpc/base/v1/ai_provider.ts +14 -6
- package/src/views/pagesMember/ai/components/Composer.vue +117 -55
- package/src/views/pagesMember/ai/components/KnowledgePicker.vue +181 -0
- 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 +49 -15
- 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,15 @@ 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",
|
|
376
|
+
"system.ai.knowledge.title": "\u77E5\u8BC6\u5E93",
|
|
377
|
+
"system.ai.knowledge.selected_count": "\u5DF2\u9009 {count} \u4E2A\u77E5\u8BC6\u5E93",
|
|
378
|
+
"system.ai.knowledge.doc_count": "{count} \u7BC7\u6587\u6863",
|
|
379
|
+
"system.ai.knowledge.select_hint": "\u52FE\u9009\u8981\u6CE8\u5165\u4E0A\u4E0B\u6587\u7684\u77E5\u8BC6\u5E93",
|
|
380
|
+
"system.ai.knowledge.empty": "\u6682\u65E0\u77E5\u8BC6\u5E93"
|
|
373
381
|
};
|
|
374
382
|
|
|
375
383
|
// src/locales/en-US.json
|
|
@@ -488,7 +496,15 @@ var en_US_default = {
|
|
|
488
496
|
"system.notification.action.delete": "Delete",
|
|
489
497
|
"system.ai.model.provider_placeholder": "Select a provider",
|
|
490
498
|
"system.ai.model.model_placeholder": "Select a model",
|
|
491
|
-
"system.ai.model.load_failed": "Failed to load providers and models"
|
|
499
|
+
"system.ai.model.load_failed": "Failed to load providers and models",
|
|
500
|
+
"system.ai.model.title": "Select Model",
|
|
501
|
+
"system.ai.model.provider_model_count": "{providers} providers \xB7 {models} models",
|
|
502
|
+
"system.ai.model.empty": "No models available",
|
|
503
|
+
"system.ai.knowledge.title": "Knowledge Base",
|
|
504
|
+
"system.ai.knowledge.selected_count": "{count} knowledge base(s) selected",
|
|
505
|
+
"system.ai.knowledge.doc_count": "{count} doc(s)",
|
|
506
|
+
"system.ai.knowledge.select_hint": "Select knowledge bases to inject into context",
|
|
507
|
+
"system.ai.knowledge.empty": "No knowledge base available"
|
|
492
508
|
};
|
|
493
509
|
|
|
494
510
|
// src/locales/ja-JP.json
|
|
@@ -607,7 +623,15 @@ var ja_JP_default = {
|
|
|
607
623
|
"system.notification.action.delete": "\u524A\u9664",
|
|
608
624
|
"system.ai.model.provider_placeholder": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u9078\u629E",
|
|
609
625
|
"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"
|
|
626
|
+
"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",
|
|
627
|
+
"system.ai.model.title": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
|
|
628
|
+
"system.ai.model.provider_model_count": "{providers} \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC \xB7 {models} \u30E2\u30C7\u30EB",
|
|
629
|
+
"system.ai.model.empty": "\u9078\u629E\u53EF\u80FD\u306A\u30E2\u30C7\u30EB\u304C\u3042\u308A\u307E\u305B\u3093",
|
|
630
|
+
"system.ai.knowledge.title": "\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9",
|
|
631
|
+
"system.ai.knowledge.selected_count": "{count} \u4EF6\u306E\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9\u3092\u9078\u629E\u4E2D",
|
|
632
|
+
"system.ai.knowledge.doc_count": "{count} \u4EF6\u306E\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8",
|
|
633
|
+
"system.ai.knowledge.select_hint": "\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u306B\u6CE8\u5165\u3059\u308B\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9\u3092\u9078\u629E",
|
|
634
|
+
"system.ai.knowledge.empty": "\u5229\u7528\u53EF\u80FD\u306A\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9\u304C\u3042\u308A\u307E\u305B\u3093"
|
|
611
635
|
};
|
|
612
636
|
|
|
613
637
|
// src/locales/zh-TW.json
|
|
@@ -726,7 +750,15 @@ var zh_TW_default = {
|
|
|
726
750
|
"system.notification.action.delete": "\u522A\u9664",
|
|
727
751
|
"system.ai.model.provider_placeholder": "\u9078\u64C7\u4F9B\u61C9\u5546",
|
|
728
752
|
"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"
|
|
753
|
+
"system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557",
|
|
754
|
+
"system.ai.model.title": "\u9078\u64C7\u6A21\u578B",
|
|
755
|
+
"system.ai.model.provider_model_count": "{providers} \u500B\u4F9B\u61C9\u5546 \xB7 {models} \u500B\u6A21\u578B",
|
|
756
|
+
"system.ai.model.empty": "\u66AB\u7121\u53EF\u9078\u6A21\u578B",
|
|
757
|
+
"system.ai.knowledge.title": "\u77E5\u8B58\u5EAB",
|
|
758
|
+
"system.ai.knowledge.selected_count": "\u5DF2\u9078 {count} \u500B\u77E5\u8B58\u5EAB",
|
|
759
|
+
"system.ai.knowledge.doc_count": "{count} \u7BC7\u6587\u4EF6",
|
|
760
|
+
"system.ai.knowledge.select_hint": "\u52FE\u9078\u8981\u6CE8\u5165\u4E0A\u4E0B\u6587\u7684\u77E5\u8B58\u5EAB",
|
|
761
|
+
"system.ai.knowledge.empty": "\u66AB\u7121\u77E5\u8B58\u5EAB"
|
|
730
762
|
};
|
|
731
763
|
|
|
732
764
|
// src/locales/generated.ts
|
package/dist/module.mjs
CHANGED
|
@@ -149,7 +149,15 @@ 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",
|
|
156
|
+
"system.ai.knowledge.title": "\u77E5\u8BC6\u5E93",
|
|
157
|
+
"system.ai.knowledge.selected_count": "\u5DF2\u9009 {count} \u4E2A\u77E5\u8BC6\u5E93",
|
|
158
|
+
"system.ai.knowledge.doc_count": "{count} \u7BC7\u6587\u6863",
|
|
159
|
+
"system.ai.knowledge.select_hint": "\u52FE\u9009\u8981\u6CE8\u5165\u4E0A\u4E0B\u6587\u7684\u77E5\u8BC6\u5E93",
|
|
160
|
+
"system.ai.knowledge.empty": "\u6682\u65E0\u77E5\u8BC6\u5E93"
|
|
153
161
|
};
|
|
154
162
|
|
|
155
163
|
// src/locales/en-US.json
|
|
@@ -268,7 +276,15 @@ var en_US_default = {
|
|
|
268
276
|
"system.notification.action.delete": "Delete",
|
|
269
277
|
"system.ai.model.provider_placeholder": "Select a provider",
|
|
270
278
|
"system.ai.model.model_placeholder": "Select a model",
|
|
271
|
-
"system.ai.model.load_failed": "Failed to load providers and models"
|
|
279
|
+
"system.ai.model.load_failed": "Failed to load providers and models",
|
|
280
|
+
"system.ai.model.title": "Select Model",
|
|
281
|
+
"system.ai.model.provider_model_count": "{providers} providers \xB7 {models} models",
|
|
282
|
+
"system.ai.model.empty": "No models available",
|
|
283
|
+
"system.ai.knowledge.title": "Knowledge Base",
|
|
284
|
+
"system.ai.knowledge.selected_count": "{count} knowledge base(s) selected",
|
|
285
|
+
"system.ai.knowledge.doc_count": "{count} doc(s)",
|
|
286
|
+
"system.ai.knowledge.select_hint": "Select knowledge bases to inject into context",
|
|
287
|
+
"system.ai.knowledge.empty": "No knowledge base available"
|
|
272
288
|
};
|
|
273
289
|
|
|
274
290
|
// src/locales/ja-JP.json
|
|
@@ -387,7 +403,15 @@ var ja_JP_default = {
|
|
|
387
403
|
"system.notification.action.delete": "\u524A\u9664",
|
|
388
404
|
"system.ai.model.provider_placeholder": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u9078\u629E",
|
|
389
405
|
"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"
|
|
406
|
+
"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",
|
|
407
|
+
"system.ai.model.title": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
|
|
408
|
+
"system.ai.model.provider_model_count": "{providers} \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC \xB7 {models} \u30E2\u30C7\u30EB",
|
|
409
|
+
"system.ai.model.empty": "\u9078\u629E\u53EF\u80FD\u306A\u30E2\u30C7\u30EB\u304C\u3042\u308A\u307E\u305B\u3093",
|
|
410
|
+
"system.ai.knowledge.title": "\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9",
|
|
411
|
+
"system.ai.knowledge.selected_count": "{count} \u4EF6\u306E\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9\u3092\u9078\u629E\u4E2D",
|
|
412
|
+
"system.ai.knowledge.doc_count": "{count} \u4EF6\u306E\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8",
|
|
413
|
+
"system.ai.knowledge.select_hint": "\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u306B\u6CE8\u5165\u3059\u308B\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9\u3092\u9078\u629E",
|
|
414
|
+
"system.ai.knowledge.empty": "\u5229\u7528\u53EF\u80FD\u306A\u30CA\u30EC\u30C3\u30B8\u30D9\u30FC\u30B9\u304C\u3042\u308A\u307E\u305B\u3093"
|
|
391
415
|
};
|
|
392
416
|
|
|
393
417
|
// src/locales/zh-TW.json
|
|
@@ -506,7 +530,15 @@ var zh_TW_default = {
|
|
|
506
530
|
"system.notification.action.delete": "\u522A\u9664",
|
|
507
531
|
"system.ai.model.provider_placeholder": "\u9078\u64C7\u4F9B\u61C9\u5546",
|
|
508
532
|
"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"
|
|
533
|
+
"system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557",
|
|
534
|
+
"system.ai.model.title": "\u9078\u64C7\u6A21\u578B",
|
|
535
|
+
"system.ai.model.provider_model_count": "{providers} \u500B\u4F9B\u61C9\u5546 \xB7 {models} \u500B\u6A21\u578B",
|
|
536
|
+
"system.ai.model.empty": "\u66AB\u7121\u53EF\u9078\u6A21\u578B",
|
|
537
|
+
"system.ai.knowledge.title": "\u77E5\u8B58\u5EAB",
|
|
538
|
+
"system.ai.knowledge.selected_count": "\u5DF2\u9078 {count} \u500B\u77E5\u8B58\u5EAB",
|
|
539
|
+
"system.ai.knowledge.doc_count": "{count} \u7BC7\u6587\u4EF6",
|
|
540
|
+
"system.ai.knowledge.select_hint": "\u52FE\u9078\u8981\u6CE8\u5165\u4E0A\u4E0B\u6587\u7684\u77E5\u8B58\u5EAB",
|
|
541
|
+
"system.ai.knowledge.empty": "\u66AB\u7121\u77E5\u8B58\u5EAB"
|
|
510
542
|
};
|
|
511
543
|
|
|
512
544
|
// 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",
|
|
@@ -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.66"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"esbuild": "^0.20.2",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { http } from '@liujitcn/kratos-uni-app-core/utils/http'
|
|
2
|
+
import type {
|
|
3
|
+
AiKnowledgeService,
|
|
4
|
+
ListAiKnowledgeOptionsRequest,
|
|
5
|
+
ListAiKnowledgeOptionsResponse,
|
|
6
|
+
} from '../../../rpc/base/v1/ai_knowledge'
|
|
7
|
+
|
|
8
|
+
const AI_KNOWLEDGE_OPTIONS_URL = '/v1/base/ai/knowledge/options'
|
|
9
|
+
|
|
10
|
+
/** AI知识库选项服务。 */
|
|
11
|
+
export class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
|
12
|
+
/** 查询可用的AI知识库选项。 */
|
|
13
|
+
ListAiKnowledgeOptions(
|
|
14
|
+
request: ListAiKnowledgeOptionsRequest,
|
|
15
|
+
): Promise<ListAiKnowledgeOptionsResponse> {
|
|
16
|
+
return http<ListAiKnowledgeOptionsResponse>({
|
|
17
|
+
url: AI_KNOWLEDGE_OPTIONS_URL,
|
|
18
|
+
method: 'GET',
|
|
19
|
+
authMode: 'required',
|
|
20
|
+
data: request,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const defAiKnowledgeService = new AiKnowledgeServiceImpl()
|
|
@@ -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,13 @@
|
|
|
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",
|
|
120
|
+
"system.ai.knowledge.title": "Knowledge Base",
|
|
121
|
+
"system.ai.knowledge.selected_count": "{count} knowledge base(s) selected",
|
|
122
|
+
"system.ai.knowledge.doc_count": "{count} doc(s)",
|
|
123
|
+
"system.ai.knowledge.select_hint": "Select knowledge bases to inject into context",
|
|
124
|
+
"system.ai.knowledge.empty": "No knowledge base available"
|
|
117
125
|
}
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -113,5 +113,13 @@
|
|
|
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": "選択可能なモデルがありません",
|
|
120
|
+
"system.ai.knowledge.title": "ナレッジベース",
|
|
121
|
+
"system.ai.knowledge.selected_count": "{count} 件のナレッジベースを選択中",
|
|
122
|
+
"system.ai.knowledge.doc_count": "{count} 件のドキュメント",
|
|
123
|
+
"system.ai.knowledge.select_hint": "コンテキストに注入するナレッジベースを選択",
|
|
124
|
+
"system.ai.knowledge.empty": "利用可能なナレッジベースがありません"
|
|
117
125
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -113,5 +113,13 @@
|
|
|
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": "暂无可选模型",
|
|
120
|
+
"system.ai.knowledge.title": "知识库",
|
|
121
|
+
"system.ai.knowledge.selected_count": "已选 {count} 个知识库",
|
|
122
|
+
"system.ai.knowledge.doc_count": "{count} 篇文档",
|
|
123
|
+
"system.ai.knowledge.select_hint": "勾选要注入上下文的知识库",
|
|
124
|
+
"system.ai.knowledge.empty": "暂无知识库"
|
|
117
125
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -113,5 +113,13 @@
|
|
|
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": "暫無可選模型",
|
|
120
|
+
"system.ai.knowledge.title": "知識庫",
|
|
121
|
+
"system.ai.knowledge.selected_count": "已選 {count} 個知識庫",
|
|
122
|
+
"system.ai.knowledge.doc_count": "{count} 篇文件",
|
|
123
|
+
"system.ai.knowledge.select_hint": "勾選要注入上下文的知識庫",
|
|
124
|
+
"system.ai.knowledge.empty": "暫無知識庫"
|
|
117
125
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
|
2
|
+
// versions:
|
|
3
|
+
// protoc-gen-ts_proto v2.11.8
|
|
4
|
+
// protoc unknown
|
|
5
|
+
// source: base/v1/ai_knowledge.proto
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
|
|
9
|
+
/** AI知识库选项查询请求。 */
|
|
10
|
+
export interface ListAiKnowledgeOptionsRequest {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** AI知识库选项查询响应。 */
|
|
14
|
+
export interface ListAiKnowledgeOptionsResponse {
|
|
15
|
+
/** 知识库选项列表 */
|
|
16
|
+
options: AiKnowledgeOption[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** AI知识库选项。 */
|
|
20
|
+
export interface AiKnowledgeOption {
|
|
21
|
+
/** 知识库ID */
|
|
22
|
+
id: number;
|
|
23
|
+
/** 知识库名称 */
|
|
24
|
+
name: string;
|
|
25
|
+
/** 文档数量 */
|
|
26
|
+
doc_count: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** AI 知识库选项查询服务。 */
|
|
30
|
+
export interface AiKnowledgeService {
|
|
31
|
+
/** 查询可用的AI知识库选项。 */
|
|
32
|
+
ListAiKnowledgeOptions(request: ListAiKnowledgeOptionsRequest): Promise<ListAiKnowledgeOptionsResponse>;
|
|
33
|
+
}
|
|
@@ -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,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { useI18n } from '@liujitcn/kratos-uni-app-core'
|
|
3
4
|
import type { AiAttachment } from '../../../../rpc/base/v1/ai_session'
|
|
4
5
|
import type { AiProviderModelOption } from '../../../../rpc/base/v1/ai_provider'
|
|
6
|
+
import type { AiKnowledgeOption } from '../../../../rpc/base/v1/ai_knowledge'
|
|
7
|
+
import { fallbackModelDisplayName } from '../modelDisplay'
|
|
8
|
+
import ModelPicker from './ModelPicker.vue'
|
|
9
|
+
import KnowledgePicker from './KnowledgePicker.vue'
|
|
10
|
+
|
|
11
|
+
const { t } = useI18n()
|
|
5
12
|
|
|
6
13
|
type InputEventValue = {
|
|
7
14
|
detail: {
|
|
@@ -17,7 +24,10 @@ const props = defineProps<{
|
|
|
17
24
|
/** 当前选中的供应商编号和模型名称。 */
|
|
18
25
|
providerId: number
|
|
19
26
|
modelName: string
|
|
20
|
-
|
|
27
|
+
/** 当前可供选择的知识库。 */
|
|
28
|
+
knowledgeOptions: AiKnowledgeOption[]
|
|
29
|
+
/** 当前选择的知识库编号列表。 */
|
|
30
|
+
knowledgeIds: number[]
|
|
21
31
|
modelPlaceholder: string
|
|
22
32
|
placeholder: string
|
|
23
33
|
bottom: string
|
|
@@ -33,58 +43,78 @@ const emit = defineEmits<{
|
|
|
33
43
|
send: []
|
|
34
44
|
'remove-attachment': [attachment: AiAttachment]
|
|
35
45
|
'model-change': [providerId: number, modelName: string]
|
|
46
|
+
'knowledge-change': [knowledgeIds: number[]]
|
|
36
47
|
}>()
|
|
37
48
|
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
0,
|
|
47
|
-
),
|
|
48
|
-
)
|
|
49
|
-
const modelIndex = computed(() => Math.max(modelNames.value.indexOf(props.modelName), 0))
|
|
49
|
+
const showModelPicker = ref(false)
|
|
50
|
+
const showKnowledgePicker = ref(false)
|
|
51
|
+
|
|
52
|
+
const selectedModelDisplayName = computed(() => {
|
|
53
|
+
const provider = props.modelProviders.find((item) => item.provider_id === props.providerId)
|
|
54
|
+
const matched = provider?.models.find((model) => model.model_name === props.modelName)
|
|
55
|
+
return matched?.display_name || fallbackModelDisplayName(props.modelName)
|
|
56
|
+
})
|
|
50
57
|
|
|
51
58
|
function handleInput(event: Event) {
|
|
52
59
|
emit('update:modelValue', ((event as unknown as InputEventValue).detail?.value || '').toString())
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
function openModelPicker() {
|
|
63
|
+
if (props.sending || !props.modelProviders.length) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
showModelPicker.value = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleModelSelect(providerId: number, modelName: string) {
|
|
70
|
+
showModelPicker.value = false
|
|
71
|
+
emit('model-change', providerId, modelName)
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
function
|
|
61
|
-
|
|
74
|
+
function openKnowledgePicker() {
|
|
75
|
+
if (props.sending || !props.knowledgeOptions.length) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
showKnowledgePicker.value = true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleKnowledgeToggle(knowledgeId: number) {
|
|
82
|
+
const current = new Set(props.knowledgeIds)
|
|
83
|
+
if (current.has(knowledgeId)) {
|
|
84
|
+
current.delete(knowledgeId)
|
|
85
|
+
} else {
|
|
86
|
+
current.add(knowledgeId)
|
|
87
|
+
}
|
|
88
|
+
emit('knowledge-change', [...current])
|
|
62
89
|
}
|
|
63
90
|
</script>
|
|
64
91
|
|
|
65
92
|
<template>
|
|
66
93
|
<view class="composer" :style="{ paddingBottom: bottom }">
|
|
67
|
-
<view
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
>
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
<view
|
|
95
|
+
class="composer-model-chip"
|
|
96
|
+
:class="{ 'is-disabled': sending || !modelProviders.length }"
|
|
97
|
+
@tap="openModelPicker"
|
|
98
|
+
>
|
|
99
|
+
<text class="composer-model-chip__spark">✦</text>
|
|
100
|
+
<text class="composer-model-chip__label">{{ selectedModelDisplayName || modelPlaceholder }}</text>
|
|
101
|
+
<uni-icons type="down" size="12" color="#8a8f99" />
|
|
102
|
+
</view>
|
|
103
|
+
<view
|
|
104
|
+
class="composer-model-chip"
|
|
105
|
+
:class="{
|
|
106
|
+
'is-disabled': sending || !knowledgeOptions.length,
|
|
107
|
+
'is-selected': knowledgeIds.length > 0,
|
|
108
|
+
}"
|
|
109
|
+
@tap="openKnowledgePicker"
|
|
110
|
+
>
|
|
111
|
+
<text class="composer-model-chip__spark">📚</text>
|
|
112
|
+
<text class="composer-model-chip__label">{{
|
|
113
|
+
knowledgeIds.length > 0
|
|
114
|
+
? t('system.ai.knowledge.selected_count', { count: knowledgeIds.length })
|
|
115
|
+
: t('system.ai.knowledge.title')
|
|
116
|
+
}}</text>
|
|
117
|
+
<uni-icons type="down" size="12" color="#8a8f99" />
|
|
88
118
|
</view>
|
|
89
119
|
<view class="composer-main">
|
|
90
120
|
<button class="attach-button" hover-class="none" @tap="emit('attach')">
|
|
@@ -129,6 +159,23 @@ function handleModelChange(event: { detail: { value: number | string } }) {
|
|
|
129
159
|
<uni-icons type="paperplane" size="28" :color="disabled ? '#111' : '#00a96b'" />
|
|
130
160
|
</button>
|
|
131
161
|
</view>
|
|
162
|
+
|
|
163
|
+
<ModelPicker
|
|
164
|
+
v-if="showModelPicker"
|
|
165
|
+
:providers="modelProviders"
|
|
166
|
+
:provider-id="providerId"
|
|
167
|
+
:model-name="modelName"
|
|
168
|
+
@select="handleModelSelect"
|
|
169
|
+
@close="showModelPicker = false"
|
|
170
|
+
/>
|
|
171
|
+
|
|
172
|
+
<KnowledgePicker
|
|
173
|
+
v-if="showKnowledgePicker"
|
|
174
|
+
:options="knowledgeOptions"
|
|
175
|
+
:selected-ids="knowledgeIds"
|
|
176
|
+
@toggle="handleKnowledgeToggle"
|
|
177
|
+
@close="showKnowledgePicker = false"
|
|
178
|
+
/>
|
|
132
179
|
</view>
|
|
133
180
|
</template>
|
|
134
181
|
|
|
@@ -168,27 +215,42 @@ function handleModelChange(event: { detail: { value: number | string } }) {
|
|
|
168
215
|
box-sizing: border-box;
|
|
169
216
|
}
|
|
170
217
|
|
|
171
|
-
.composer-model-
|
|
172
|
-
display: flex;
|
|
173
|
-
|
|
174
|
-
gap:
|
|
175
|
-
|
|
218
|
+
.composer-model-chip {
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: 10rpx;
|
|
222
|
+
max-width: 100%;
|
|
223
|
+
height: 56rpx;
|
|
224
|
+
padding: 0 24rpx;
|
|
176
225
|
margin: 0 8rpx 12rpx;
|
|
226
|
+
color: #1f2937;
|
|
227
|
+
background: #fff;
|
|
228
|
+
border: 2rpx solid #d6dae2;
|
|
229
|
+
border-radius: 28rpx;
|
|
230
|
+
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04);
|
|
231
|
+
box-sizing: border-box;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.composer-model-chip.is-disabled {
|
|
235
|
+
opacity: 0.55;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.composer-model-chip__spark {
|
|
239
|
+
flex-shrink: 0;
|
|
240
|
+
color: #00a96b;
|
|
241
|
+
font-size: 26rpx;
|
|
242
|
+
line-height: 26rpx;
|
|
177
243
|
}
|
|
178
244
|
|
|
179
|
-
.composer-model-
|
|
180
|
-
max-width:
|
|
181
|
-
min-width: 120rpx;
|
|
182
|
-
padding: 8rpx 16rpx;
|
|
245
|
+
.composer-model-chip__label {
|
|
246
|
+
max-width: 320rpx;
|
|
183
247
|
overflow: hidden;
|
|
184
|
-
color:
|
|
185
|
-
font-size:
|
|
186
|
-
|
|
248
|
+
color: inherit;
|
|
249
|
+
font-size: 24rpx;
|
|
250
|
+
font-weight: 500;
|
|
251
|
+
line-height: 34rpx;
|
|
187
252
|
text-overflow: ellipsis;
|
|
188
253
|
white-space: nowrap;
|
|
189
|
-
background: #fff;
|
|
190
|
-
border: 2rpx solid #d6dae2;
|
|
191
|
-
border-radius: 8rpx;
|
|
192
254
|
}
|
|
193
255
|
|
|
194
256
|
.attach-button {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useI18n } from '@liujitcn/kratos-uni-app-core'
|
|
4
|
+
import type { AiKnowledgeOption } from '../../../../rpc/base/v1/ai_knowledge'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
options: AiKnowledgeOption[]
|
|
8
|
+
selectedIds: number[]
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
toggle: [knowledgeId: number]
|
|
13
|
+
close: []
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
|
|
18
|
+
const selectedCount = computed(() => props.selectedIds.length)
|
|
19
|
+
|
|
20
|
+
function isSelected(knowledgeId: number) {
|
|
21
|
+
return props.selectedIds.includes(knowledgeId)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleToggle(knowledgeId: number) {
|
|
25
|
+
emit('toggle', knowledgeId)
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<view class="knowledge-picker">
|
|
31
|
+
<view class="knowledge-picker__mask" @tap="emit('close')" />
|
|
32
|
+
<view class="knowledge-picker__sheet">
|
|
33
|
+
<view class="knowledge-picker__handle" />
|
|
34
|
+
<view class="knowledge-picker__head">
|
|
35
|
+
<text class="knowledge-picker__title">{{ t('system.ai.knowledge.title') }}</text>
|
|
36
|
+
<text class="knowledge-picker__count">{{
|
|
37
|
+
t('system.ai.knowledge.selected_count', { count: selectedCount })
|
|
38
|
+
}}</text>
|
|
39
|
+
<button class="knowledge-picker__close" hover-class="none" @tap="emit('close')">✕</button>
|
|
40
|
+
</view>
|
|
41
|
+
<text class="knowledge-picker__hint">{{ t('system.ai.knowledge.select_hint') }}</text>
|
|
42
|
+
<scroll-view class="knowledge-picker__body" scroll-y :show-scrollbar="true">
|
|
43
|
+
<view
|
|
44
|
+
v-for="option in options"
|
|
45
|
+
:key="option.id"
|
|
46
|
+
class="knowledge-picker__row"
|
|
47
|
+
:class="{ 'is-selected': isSelected(option.id) }"
|
|
48
|
+
@tap="handleToggle(option.id)"
|
|
49
|
+
>
|
|
50
|
+
<view class="knowledge-picker__row-main">
|
|
51
|
+
<text class="knowledge-picker__row-name">{{ option.name }}</text>
|
|
52
|
+
<text v-if="option.doc_count" class="knowledge-picker__row-meta">{{
|
|
53
|
+
t('system.ai.knowledge.doc_count', { count: option.doc_count })
|
|
54
|
+
}}</text>
|
|
55
|
+
</view>
|
|
56
|
+
<view class="knowledge-picker__check" :class="{ 'is-checked': isSelected(option.id) }">
|
|
57
|
+
<text v-if="isSelected(option.id)">✓</text>
|
|
58
|
+
</view>
|
|
59
|
+
</view>
|
|
60
|
+
</scroll-view>
|
|
61
|
+
</view>
|
|
62
|
+
</view>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style lang="scss" scoped>
|
|
66
|
+
.knowledge-picker {
|
|
67
|
+
position: fixed;
|
|
68
|
+
inset: 0;
|
|
69
|
+
z-index: 1000;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: flex-end;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.knowledge-picker__mask {
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset: 0;
|
|
77
|
+
background: rgba(0, 0, 0, 0.45);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.knowledge-picker__sheet {
|
|
81
|
+
position: relative;
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
width: 100%;
|
|
85
|
+
max-height: 62vh;
|
|
86
|
+
padding: 10px 20px calc(20px + env(safe-area-inset-bottom));
|
|
87
|
+
background: #fff;
|
|
88
|
+
border-radius: 20px 20px 0 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.knowledge-picker__handle {
|
|
92
|
+
width: 36px;
|
|
93
|
+
height: 4px;
|
|
94
|
+
margin: 0 auto 10px;
|
|
95
|
+
background: #e5e6eb;
|
|
96
|
+
border-radius: 2px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.knowledge-picker__head {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 10px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.knowledge-picker__title {
|
|
106
|
+
font-size: 17px;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
color: #111;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.knowledge-picker__count {
|
|
112
|
+
flex: 1;
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
color: #8a8f99;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.knowledge-picker__close {
|
|
118
|
+
padding: 0;
|
|
119
|
+
font-size: 16px;
|
|
120
|
+
line-height: 1;
|
|
121
|
+
color: #8a8f99;
|
|
122
|
+
background: transparent;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.knowledge-picker__close::after {
|
|
126
|
+
border: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.knowledge-picker__hint {
|
|
130
|
+
margin-top: 6px;
|
|
131
|
+
font-size: 12px;
|
|
132
|
+
color: #8a8f99;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.knowledge-picker__body {
|
|
136
|
+
margin-top: 10px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.knowledge-picker__row {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: space-between;
|
|
143
|
+
gap: 12px;
|
|
144
|
+
padding: 12px 4px;
|
|
145
|
+
border-bottom: 1px solid #f2f3f5;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.knowledge-picker__row-main {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 2px;
|
|
152
|
+
min-width: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.knowledge-picker__row-name {
|
|
156
|
+
font-size: 15px;
|
|
157
|
+
color: #111;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.knowledge-picker__row-meta {
|
|
161
|
+
font-size: 12px;
|
|
162
|
+
color: #8a8f99;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.knowledge-picker__check {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
width: 22px;
|
|
170
|
+
height: 22px;
|
|
171
|
+
border: 1px solid #d0d2d6;
|
|
172
|
+
border-radius: 50%;
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
color: #fff;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.knowledge-picker__check.is-checked {
|
|
178
|
+
background: #00a96b;
|
|
179
|
+
border-color: #00a96b;
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
@@ -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;
|
|
@@ -8,14 +8,17 @@ import {
|
|
|
8
8
|
import { defAiSessionService } from '../../../api/base/v1/ai_session'
|
|
9
9
|
import { defAiToolService } from '../../../api/base/v1/ai_tool'
|
|
10
10
|
import { defAiModelService } from '../../../api/base/v1/ai_provider'
|
|
11
|
+
import { defAiKnowledgeService } from '../../../api/base/v1/ai_knowledge'
|
|
11
12
|
import type { AiMessage } from '../../../rpc/base/v1/ai_session'
|
|
12
13
|
import type { AiAttachment, AiSession } from '../../../rpc/base/v1/ai_session'
|
|
13
14
|
import type { AiProviderModelOption } from '../../../rpc/base/v1/ai_provider'
|
|
15
|
+
import type { AiKnowledgeOption } from '../../../rpc/base/v1/ai_knowledge'
|
|
14
16
|
import type { AiShortcut, AiToolCall } from '../../../rpc/base/v1/ai_tool'
|
|
15
17
|
import { AiMessageStatus } from '../../../rpc/base/v1/ai_session'
|
|
16
18
|
import { Terminal } from '../../../rpc/base/v1/ai_tool'
|
|
17
19
|
import { uploadFile } from '@liujitcn/kratos-uni-app-core/utils/file'
|
|
18
20
|
import { formatSrc } from '@liujitcn/kratos-uni-app-core/utils/index'
|
|
21
|
+
import { isAuthExpiredError } from '@liujitcn/kratos-uni-app-core/utils/http'
|
|
19
22
|
import Composer from './components/Composer.vue'
|
|
20
23
|
import SessionDrawer from './components/SessionDrawer.vue'
|
|
21
24
|
import WelcomePanel from './components/WelcomePanel.vue'
|
|
@@ -27,6 +30,8 @@ import {
|
|
|
27
30
|
parseAiEventStreamText,
|
|
28
31
|
readAiEventStream,
|
|
29
32
|
} from './stream'
|
|
33
|
+
import { resolveModelDisplayName } from './modelDisplay'
|
|
34
|
+
import { loadStoredModelSelection, saveModelSelection } from './modelSelection'
|
|
30
35
|
|
|
31
36
|
type ChatRole = 'user' | 'ai'
|
|
32
37
|
|
|
@@ -97,6 +102,8 @@ const selectedAttachments = ref<AiAttachment[]>([])
|
|
|
97
102
|
const modelProviders = ref<AiProviderModelOption[]>([])
|
|
98
103
|
const selectedProviderId = ref(0)
|
|
99
104
|
const selectedModelName = ref('')
|
|
105
|
+
const knowledgeOptions = ref<AiKnowledgeOption[]>([])
|
|
106
|
+
const selectedKnowledgeIds = ref<number[]>([])
|
|
100
107
|
const runningStreamTaskMap = new Map<string, StreamTask>()
|
|
101
108
|
const pendingDeltaMap = new Map<string, AiStreamPayload>()
|
|
102
109
|
let pendingDeltaTimer = 0
|
|
@@ -469,22 +476,17 @@ async function loadAiShortcuts() {
|
|
|
469
476
|
}
|
|
470
477
|
loadingShortcuts.value = true
|
|
471
478
|
try {
|
|
472
|
-
const [response, providerResponse] = await Promise.all([
|
|
479
|
+
const [response, providerResponse, knowledgeResponse] = await Promise.all([
|
|
473
480
|
defAiToolService.ListAiShortcut({ terminal: AI_TERMINAL }),
|
|
474
481
|
defAiModelService.ListAiProviderModelOptions({}),
|
|
482
|
+
defAiKnowledgeService.ListAiKnowledgeOptions({}),
|
|
475
483
|
])
|
|
476
484
|
modelProviders.value = providerResponse.providers ?? []
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
)
|
|
483
|
-
) {
|
|
484
|
-
const firstProvider = modelProviders.value[0]
|
|
485
|
-
selectedProviderId.value = firstProvider?.provider_id ?? 0
|
|
486
|
-
selectedModelName.value = firstProvider?.models[0] ?? ''
|
|
487
|
-
}
|
|
485
|
+
knowledgeOptions.value = knowledgeResponse.options ?? []
|
|
486
|
+
selectedKnowledgeIds.value = selectedKnowledgeIds.value.filter((id) =>
|
|
487
|
+
knowledgeOptions.value.some((option) => option.id === id),
|
|
488
|
+
)
|
|
489
|
+
applyModelSelection()
|
|
488
490
|
const shortcuts = normalizeStarterShortcuts(response.shortcuts).filter((item) => !item.action)
|
|
489
491
|
if (shortcuts.length) {
|
|
490
492
|
starterShortcuts.value = shortcuts
|
|
@@ -494,16 +496,43 @@ async function loadAiShortcuts() {
|
|
|
494
496
|
modelProviders.value = []
|
|
495
497
|
selectedProviderId.value = 0
|
|
496
498
|
selectedModelName.value = ''
|
|
499
|
+
knowledgeOptions.value = []
|
|
497
500
|
showError(error, t('system.ai.load_shortcuts_failed'))
|
|
498
501
|
} finally {
|
|
499
502
|
loadingShortcuts.value = false
|
|
500
503
|
}
|
|
501
504
|
}
|
|
502
505
|
|
|
503
|
-
/**
|
|
506
|
+
/** 优先恢复本地缓存的模型选择,缓存失效时回退到首个可用模型。 */
|
|
507
|
+
function applyModelSelection() {
|
|
508
|
+
const stored = loadStoredModelSelection()
|
|
509
|
+
const provider = stored
|
|
510
|
+
? modelProviders.value.find(
|
|
511
|
+
(item) =>
|
|
512
|
+
item.provider_id === stored.provider_id &&
|
|
513
|
+
item.models.some((model) => model.model_name === stored.model_name),
|
|
514
|
+
)
|
|
515
|
+
: undefined
|
|
516
|
+
if (provider && stored) {
|
|
517
|
+
selectedProviderId.value = provider.provider_id
|
|
518
|
+
selectedModelName.value = stored.model_name
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
const firstProvider = modelProviders.value[0]
|
|
522
|
+
selectedProviderId.value = firstProvider?.provider_id ?? 0
|
|
523
|
+
selectedModelName.value = firstProvider?.models[0]?.model_name ?? ''
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** 更新当前聊天使用的供应商和模型并持久化到本地缓存。 */
|
|
504
527
|
function handleModelChange(providerId: number, modelName: string) {
|
|
505
528
|
selectedProviderId.value = providerId
|
|
506
529
|
selectedModelName.value = modelName
|
|
530
|
+
saveModelSelection({ provider_id: providerId, model_name: modelName })
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** 更新当前聊天检索注入的知识库选择。 */
|
|
534
|
+
function handleKnowledgeChange(knowledgeIds: number[]) {
|
|
535
|
+
selectedKnowledgeIds.value = knowledgeIds
|
|
507
536
|
}
|
|
508
537
|
|
|
509
538
|
async function sendAiPayload(payload: { text: string; attachments: AiAttachment[] }) {
|
|
@@ -536,6 +565,7 @@ async function runAiTask(
|
|
|
536
565
|
content: payload.text,
|
|
537
566
|
provider_id: selectedProviderId.value,
|
|
538
567
|
model_name: selectedModelName.value,
|
|
568
|
+
knowledge_base_ids: selectedKnowledgeIds.value,
|
|
539
569
|
attachments: payload.attachments,
|
|
540
570
|
action: undefined,
|
|
541
571
|
}
|
|
@@ -1224,6 +1254,8 @@ function formatTools(tools: AiToolCall[]) {
|
|
|
1224
1254
|
}
|
|
1225
1255
|
|
|
1226
1256
|
function showError(error: unknown, fallback: string) {
|
|
1257
|
+
// 登录态失效时请求层已弹窗引导重新登录,不再叠加失败提示。
|
|
1258
|
+
if (isAuthExpiredError(error)) return
|
|
1227
1259
|
const message = error instanceof Error ? error.message : fallback
|
|
1228
1260
|
uni.showToast({ icon: 'none', title: message || fallback })
|
|
1229
1261
|
}
|
|
@@ -1277,7 +1309,7 @@ function showError(error: unknown, fallback: string) {
|
|
|
1277
1309
|
>
|
|
1278
1310
|
<view v-if="item.role === 'ai' && item.model" class="reply-meta">
|
|
1279
1311
|
<text class="reply-tag">{{ t('system.ai.model_reply') }}</text>
|
|
1280
|
-
<text class="reply-model">{{ item.model }}</text>
|
|
1312
|
+
<text class="reply-model">{{ resolveModelDisplayName(modelProviders, item.model) }}</text>
|
|
1281
1313
|
</view>
|
|
1282
1314
|
<view class="bubble-content">{{ item.content }}</view>
|
|
1283
1315
|
<view v-if="item.attachments.length" class="attachment-list">
|
|
@@ -1316,7 +1348,8 @@ function showError(error: unknown, fallback: string) {
|
|
|
1316
1348
|
:model-providers="modelProviders"
|
|
1317
1349
|
:provider-id="selectedProviderId"
|
|
1318
1350
|
:model-name="selectedModelName"
|
|
1319
|
-
:
|
|
1351
|
+
:knowledge-options="knowledgeOptions"
|
|
1352
|
+
:knowledge-ids="selectedKnowledgeIds"
|
|
1320
1353
|
:model-placeholder="t('system.ai.model.model_placeholder')"
|
|
1321
1354
|
:placeholder="composerPlaceholder"
|
|
1322
1355
|
:bottom="composerBottom"
|
|
@@ -1327,6 +1360,7 @@ function showError(error: unknown, fallback: string) {
|
|
|
1327
1360
|
@record="handleToggleRecord"
|
|
1328
1361
|
@send="handleSend"
|
|
1329
1362
|
@model-change="handleModelChange"
|
|
1363
|
+
@knowledge-change="handleKnowledgeChange"
|
|
1330
1364
|
@remove-attachment="removeSelectedAttachment"
|
|
1331
1365
|
/>
|
|
1332
1366
|
|
|
@@ -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
|
+
}
|