@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 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.64",
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.64"
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 Error(t('core.auth.session_expired'))
96
+ throw new AuthPromptError()
96
97
  }
97
98
  if (!response.ok) {
98
99
  throw new Error(await resolveStreamErrorMessage(response))
@@ -140,7 +141,7 @@ export function StreamAiMessageByChunkedRequest(
140
141
  }
141
142
  if (res.statusCode === 401 || res.statusCode === 403) {
142
143
  handleAuthExpired('required')
143
- reject(new Error(t('core.auth.session_expired')))
144
+ reject(new AuthPromptError())
144
145
  return
145
146
  }
146
147
  if (res.statusCode < 200 || res.statusCode >= 300) {
@@ -113,5 +113,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
  }
@@ -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
  }
@@ -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
  }
@@ -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
+ }
@@ -53,6 +53,8 @@ export interface SendAiMessageRequest {
53
53
  provider_id: number;
54
54
  /** 所选模型名称 */
55
55
  model_name: string;
56
+ /** 所选知识库ID列表 */
57
+ knowledge_base_ids: number[];
56
58
  }
57
59
 
58
60
  /** AI 助手失败消息重试请求 */
@@ -10,20 +10,28 @@
10
10
  export interface ListAiProviderModelOptionsRequest {
11
11
  }
12
12
 
13
+ /** AI Provider模型选项查询响应。 */
14
+ export interface ListAiProviderModelOptionsResponse {
15
+ /** Provider及其模型列表 */
16
+ providers: AiProviderModelOption[];
17
+ }
18
+
13
19
  /** 已启用的AI Provider及其模型选项。 */
14
20
  export interface AiProviderModelOption {
15
21
  /** Provider ID */
16
22
  provider_id: number;
17
23
  /** Provider名称 */
18
24
  name: string;
19
- /** 模型名称列表 */
20
- models: string[];
25
+ /** 模型选项列表 */
26
+ models: AiProviderModel[];
21
27
  }
22
28
 
23
- /** AI Provider模型选项查询响应。 */
24
- export interface ListAiProviderModelOptionsResponse {
25
- /** Provider及其模型列表 */
26
- providers: AiProviderModelOption[];
29
+ /** AI Provider下的单个模型选项。 */
30
+ export interface AiProviderModel {
31
+ /** 模型名称 */
32
+ model_name: string;
33
+ /** 展示名称 */
34
+ display_name: string;
27
35
  }
28
36
 
29
37
  /** AI Provider 可选模型查询服务。 */
@@ -1,7 +1,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
- providerPlaceholder: string
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 providerNames = computed(() => props.modelProviders.map((provider) => provider.name))
39
- const selectedProvider = computed(() =>
40
- props.modelProviders.find((provider) => provider.provider_id === props.providerId),
41
- )
42
- const modelNames = computed(() => selectedProvider.value?.models ?? [])
43
- const providerIndex = computed(() =>
44
- Math.max(
45
- props.modelProviders.findIndex((provider) => provider.provider_id === props.providerId),
46
- 0,
47
- ),
48
- )
49
- const modelIndex = computed(() => Math.max(modelNames.value.indexOf(props.modelName), 0))
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 handleProviderChange(event: { detail: { value: number | string } }) {
56
- const provider = props.modelProviders[Number(event.detail.value)]
57
- emit('model-change', provider?.provider_id ?? 0, provider?.models[0] ?? '')
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 handleModelChange(event: { detail: { value: number | string } }) {
61
- emit('model-change', props.providerId, modelNames.value[Number(event.detail.value)] ?? '')
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 class="composer-model-selectors">
68
- <picker
69
- mode="selector"
70
- :range="providerNames"
71
- :value="providerIndex"
72
- :disabled="sending || !modelProviders.length"
73
- @change="handleProviderChange"
74
- >
75
- <view class="composer-model-select">{{
76
- selectedProvider?.name || providerPlaceholder
77
- }}</view>
78
- </picker>
79
- <picker
80
- mode="selector"
81
- :range="modelNames"
82
- :value="modelIndex"
83
- :disabled="sending || !modelNames.length"
84
- @change="handleModelChange"
85
- >
86
- <view class="composer-model-select">{{ modelName || modelPlaceholder }}</view>
87
- </picker>
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-selectors {
172
- display: flex;
173
- justify-content: flex-end;
174
- gap: 12rpx;
175
- min-width: 0;
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-select {
180
- max-width: 300rpx;
181
- min-width: 120rpx;
182
- padding: 8rpx 16rpx;
245
+ .composer-model-chip__label {
246
+ max-width: 320rpx;
183
247
  overflow: hidden;
184
- color: #46515b;
185
- font-size: 22rpx;
186
- line-height: 32rpx;
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: 20;
143
+ /* 高于 KratosTabBar 的 z-index:999,避免抽屉被底部导航遮挡 */
144
+ z-index: 1000;
144
145
  background-color: rgba(0, 0, 0, 0.18);
145
146
  }
146
147
 
@@ -151,7 +152,7 @@ function formatSessionTime(session: AiSession) {
151
152
  top: 0;
152
153
  left: 0;
153
154
  bottom: 0;
154
- z-index: 21;
155
+ z-index: 1001;
155
156
  width: 560rpx;
156
157
  padding: 0 24rpx 36rpx;
157
158
  background-color: #fff;
@@ -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
- if (
478
- !modelProviders.value.some(
479
- (item) =>
480
- item.provider_id === selectedProviderId.value &&
481
- item.models.includes(selectedModelName.value),
482
- )
483
- ) {
484
- const firstProvider = modelProviders.value[0]
485
- selectedProviderId.value = firstProvider?.provider_id ?? 0
486
- selectedModelName.value = firstProvider?.models[0] ?? ''
487
- }
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
- :provider-placeholder="t('system.ai.model.provider_placeholder')"
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
+ }