@liujitcn/kratos-taro-app-system 0.0.62 → 0.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/module.mjs +40 -4
- package/package.json +3 -3
- package/src/api/base/v1/ai_message.ts +3 -2
- package/src/api/base/v1/ai_provider.ts +25 -0
- package/src/locales/en-US.json +10 -1
- package/src/locales/ja-JP.json +10 -1
- package/src/locales/zh-CN.json +10 -1
- package/src/locales/zh-TW.json +10 -1
- package/src/rpc/base/v1/ai_message.ts +7 -1
- package/src/rpc/base/v1/ai_provider.ts +41 -0
- package/src/rpc/base/v1/ai_session.ts +4 -0
- package/src/rpc/base/v1/notification.ts +2 -0
- package/src/views/pagesMember/ai/components/Composer.tsx +53 -0
- package/src/views/pagesMember/ai/components/ModelPicker.tsx +95 -0
- package/src/views/pagesMember/ai/components/composer.scss +45 -0
- package/src/views/pagesMember/ai/components/model-picker.scss +147 -0
- package/src/views/pagesMember/ai/index.tsx +66 -6
- package/src/views/pagesMember/ai/modelDisplay.ts +77 -0
- package/src/views/pagesMember/ai/modelSelection.ts +40 -0
- package/src/views/pagesMember/message/detail.tsx +8 -1
- package/src/views/pagesMember/settings/settings.tsx +1 -9
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
## AI 助手
|
|
17
17
|
|
|
18
|
-
AI 页面支持会话创建与切换、历史会话搜索、消息加载、附件上传、复制、删除、再生成、工具提示和快捷入口。H5 使用 Fetch SSE,微信小程序使用 chunked 请求,两端共用增量 SSE 解析器和相同事件契约。
|
|
18
|
+
AI 页面支持会话创建与切换、历史会话搜索、消息加载、附件上传、复制、删除、再生成、工具提示和快捷入口。Provider 与模型通过独立的 `AiModelService` 选项接口加载。H5 使用 Fetch SSE,微信小程序使用 chunked 请求,两端共用增量 SSE 解析器和相同事件契约。
|
|
19
19
|
|
|
20
20
|
请求统一封装在 `src/api`,RPC 类型由 backend Buf 模板生成。页面不能直接写 `Taro.request`,也不能手工修改 `src/rpc`。
|
|
21
21
|
|
package/dist/module.mjs
CHANGED
|
@@ -51,6 +51,7 @@ var zh_CN_default = {
|
|
|
51
51
|
"system.ai.delete_session_failed": "\u5220\u9664\u4F1A\u8BDD\u5931\u8D25",
|
|
52
52
|
"system.ai.efficiency_assistant": "\u6548\u7387\u52A9\u624B",
|
|
53
53
|
"system.ai.failed_response": "\u8FD9\u6B21\u56DE\u590D\u6CA1\u6709\u6210\u529F\u8FD4\u56DE\uFF0C\u4F60\u53EF\u4EE5\u76F4\u63A5\u91CD\u8BD5\u521A\u624D\u7684\u95EE\u9898\u3002",
|
|
54
|
+
"system.ai.chat.error.details_unavailable": "\u804A\u5929\u754C\u9762\u4E0D\u5C55\u793A\u5E95\u5C42\u670D\u52A1\u9519\u8BEF\u8BE6\u60C5\uFF0C\u8BF7\u67E5\u770B\u670D\u52A1\u65E5\u5FD7\u3002",
|
|
54
55
|
"system.ai.general_assistant": "\u901A\u7528\u52A9\u624B",
|
|
55
56
|
"system.ai.greeting": "\u60A8\u597D\uFF0C{period}\u597D\uFF01\u4ECA\u5929\u6709\u4EC0\u4E48\u9700\u8981\u6211\u534F\u52A9\u7684\u5417\uFF1F",
|
|
56
57
|
"system.ai.greeting_hello": "\u60A8\u597D\uFF0CAI\u52A9\u624B\u4E3A\u60A8\u670D\u52A1\uFF01",
|
|
@@ -98,6 +99,7 @@ var zh_CN_default = {
|
|
|
98
99
|
"system.ai.text_assistant": "\u6587\u672C\u52A9\u624B",
|
|
99
100
|
"system.ai.thinking": "\u6B63\u5728\u56DE\u590D",
|
|
100
101
|
"system.ai.tool_called": "\u5DF2\u8C03\u7528\uFF1A{tools}",
|
|
102
|
+
"system.ai.web_search": "\u8054\u7F51\u641C\u7D22",
|
|
101
103
|
"system.ai.upload_failed": "\u9644\u4EF6\u4E0A\u4F20\u5931\u8D25",
|
|
102
104
|
"system.profile.account": "\u8D26\u53F7",
|
|
103
105
|
"system.profile.avatar_change": "\u70B9\u51FB\u4FEE\u6539\u5934\u50CF",
|
|
@@ -151,6 +153,7 @@ var zh_CN_default = {
|
|
|
151
153
|
"system.settings.mfa_copy_uri": "\u590D\u5236\u7ED1\u5B9A\u5730\u5740",
|
|
152
154
|
"system.settings.mfa_copy_success": "\u590D\u5236\u6210\u529F",
|
|
153
155
|
"system.notification.title": "\u6D88\u606F\u4E2D\u5FC3",
|
|
156
|
+
"system.notification.sender.system": "\u7CFB\u7EDF",
|
|
154
157
|
"system.notification.description": "\u67E5\u770B\u7CFB\u7EDF\u901A\u77E5\u548C\u4E1A\u52A1\u6D88\u606F",
|
|
155
158
|
"system.notification.empty": "\u6682\u65E0\u6D88\u606F",
|
|
156
159
|
"system.notification.action.view_all": "\u67E5\u770B\u5168\u90E8",
|
|
@@ -161,7 +164,13 @@ var zh_CN_default = {
|
|
|
161
164
|
"system.notification.mark_all_read": "\u5168\u90E8\u5DF2\u8BFB",
|
|
162
165
|
"system.notification.action.archive": "\u5F52\u6863",
|
|
163
166
|
"system.notification.action.restore": "\u6062\u590D",
|
|
164
|
-
"system.notification.action.delete": "\u5220\u9664"
|
|
167
|
+
"system.notification.action.delete": "\u5220\u9664",
|
|
168
|
+
"system.ai.model.provider_placeholder": "\u9009\u62E9\u4F9B\u5E94\u5546",
|
|
169
|
+
"system.ai.model.model_placeholder": "\u9009\u62E9\u6A21\u578B",
|
|
170
|
+
"system.ai.model.load_failed": "\u52A0\u8F7D\u4F9B\u5E94\u5546\u548C\u6A21\u578B\u5931\u8D25",
|
|
171
|
+
"system.ai.model.title": "\u9009\u62E9\u6A21\u578B",
|
|
172
|
+
"system.ai.model.provider_model_count": "{providers} \u4E2A\u4F9B\u5E94\u5546 \xB7 {models} \u4E2A\u6A21\u578B",
|
|
173
|
+
"system.ai.model.empty": "\u6682\u65E0\u53EF\u9009\u6A21\u578B"
|
|
165
174
|
};
|
|
166
175
|
|
|
167
176
|
// src/locales/en-US.json
|
|
@@ -182,6 +191,7 @@ var en_US_default = {
|
|
|
182
191
|
"system.ai.delete_session_failed": "Failed to delete session",
|
|
183
192
|
"system.ai.efficiency_assistant": "Productivity assistant",
|
|
184
193
|
"system.ai.failed_response": "The response did not complete. You can retry your previous question.",
|
|
194
|
+
"system.ai.chat.error.details_unavailable": "Provider error details are not exposed in chat. See the service logs.",
|
|
185
195
|
"system.ai.general_assistant": "General assistant",
|
|
186
196
|
"system.ai.greeting": "Good {period}, how can I help you today?",
|
|
187
197
|
"system.ai.greeting_hello": "Hello, your AI assistant is ready to help.",
|
|
@@ -229,6 +239,7 @@ var en_US_default = {
|
|
|
229
239
|
"system.ai.text_assistant": "Writing assistant",
|
|
230
240
|
"system.ai.thinking": "Responding",
|
|
231
241
|
"system.ai.tool_called": "Tools used: {tools}",
|
|
242
|
+
"system.ai.web_search": "Web search",
|
|
232
243
|
"system.ai.upload_failed": "Attachment upload failed",
|
|
233
244
|
"system.profile.account": "Account",
|
|
234
245
|
"system.profile.avatar_change": "Tap to change avatar",
|
|
@@ -282,6 +293,7 @@ var en_US_default = {
|
|
|
282
293
|
"system.settings.mfa_copy_uri": "Copy binding address",
|
|
283
294
|
"system.settings.mfa_copy_success": "Copied",
|
|
284
295
|
"system.notification.title": "Message Center",
|
|
296
|
+
"system.notification.sender.system": "System",
|
|
285
297
|
"system.notification.description": "View system notifications and business messages",
|
|
286
298
|
"system.notification.empty": "No messages",
|
|
287
299
|
"system.notification.action.view_all": "View all",
|
|
@@ -292,7 +304,13 @@ var en_US_default = {
|
|
|
292
304
|
"system.notification.mark_all_read": "Mark all read",
|
|
293
305
|
"system.notification.action.archive": "Archive",
|
|
294
306
|
"system.notification.action.restore": "Restore",
|
|
295
|
-
"system.notification.action.delete": "Delete"
|
|
307
|
+
"system.notification.action.delete": "Delete",
|
|
308
|
+
"system.ai.model.provider_placeholder": "Select a provider",
|
|
309
|
+
"system.ai.model.model_placeholder": "Select a model",
|
|
310
|
+
"system.ai.model.load_failed": "Failed to load providers and models",
|
|
311
|
+
"system.ai.model.title": "Select Model",
|
|
312
|
+
"system.ai.model.provider_model_count": "{providers} providers \xB7 {models} models",
|
|
313
|
+
"system.ai.model.empty": "No models available"
|
|
296
314
|
};
|
|
297
315
|
|
|
298
316
|
// src/locales/ja-JP.json
|
|
@@ -313,6 +331,7 @@ var ja_JP_default = {
|
|
|
313
331
|
"system.ai.delete_session_failed": "\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
314
332
|
"system.ai.efficiency_assistant": "\u52B9\u7387\u30A2\u30B7\u30B9\u30BF\u30F3\u30C8",
|
|
315
333
|
"system.ai.failed_response": "\u4ECA\u56DE\u306F\u5FDC\u7B54\u304C\u6B63\u5E38\u306B\u8FD4\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u8CEA\u554F\u3092\u76F4\u63A5\u518D\u8A66\u884C\u3067\u304D\u307E\u3059\u3002",
|
|
334
|
+
"system.ai.chat.error.details_unavailable": "\u30C1\u30E3\u30C3\u30C8\u3067\u306F\u5185\u90E8\u30A8\u30E9\u30FC\u306E\u8A73\u7D30\u306F\u8868\u793A\u3055\u308C\u307E\u305B\u3093\u3002\u30B5\u30FC\u30D3\u30B9\u30ED\u30B0\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
316
335
|
"system.ai.general_assistant": "\u30E6\u30CB\u30D0\u30FC\u30B5\u30EB \u30A2\u30B7\u30B9\u30BF\u30F3\u30C8",
|
|
317
336
|
"system.ai.greeting": "\u3053\u3093\u306B\u3061\u306F\u3001{period} \u3053\u3093\u306B\u3061\u306F\uFF01\u4ECA\u65E5\u306F\u4F55\u304B\u304A\u624B\u4F1D\u3044\u304C\u5FC5\u8981\u306A\u3053\u3068\u306F\u3042\u308A\u307E\u3059\u304B?",
|
|
318
337
|
"system.ai.greeting_hello": "\u3053\u3093\u306B\u3061\u306F\u3001AI \u30A2\u30B7\u30B9\u30BF\u30F3\u30C8\u304C\u304A\u624B\u4F1D\u3044\u3057\u307E\u3059\u3002",
|
|
@@ -360,6 +379,7 @@ var ja_JP_default = {
|
|
|
360
379
|
"system.ai.text_assistant": "\u30C6\u30AD\u30B9\u30C8 \u30A2\u30B7\u30B9\u30BF\u30F3\u30C8",
|
|
361
380
|
"system.ai.thinking": "\u304C\u8FD4\u4FE1\u3057\u3066\u3044\u307E\u3059",
|
|
362
381
|
"system.ai.tool_called": "\u547C\u3073\u51FA\u3057\u5148: {tools}",
|
|
382
|
+
"system.ai.web_search": "\u30A6\u30A7\u30D6\u691C\u7D22",
|
|
363
383
|
"system.ai.upload_failed": "\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
364
384
|
"system.profile.account": "\u30A2\u30AB\u30A6\u30F3\u30C8",
|
|
365
385
|
"system.profile.avatar_change": "\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u30A2\u30D0\u30BF\u30FC\u3092\u5909\u66F4\u3057\u307E\u3059",
|
|
@@ -413,6 +433,7 @@ var ja_JP_default = {
|
|
|
413
433
|
"system.settings.mfa_copy_uri": "\u767B\u9332\u30A2\u30C9\u30EC\u30B9\u3092\u30B3\u30D4\u30FC",
|
|
414
434
|
"system.settings.mfa_copy_success": "\u30B3\u30D4\u30FC\u3057\u307E\u3057\u305F",
|
|
415
435
|
"system.notification.title": "\u30E1\u30C3\u30BB\u30FC\u30B8\u30BB\u30F3\u30BF\u30FC",
|
|
436
|
+
"system.notification.sender.system": "\u30B7\u30B9\u30C6\u30E0",
|
|
416
437
|
"system.notification.description": "\u30B7\u30B9\u30C6\u30E0\u901A\u77E5\u3068\u696D\u52D9\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u78BA\u8A8D",
|
|
417
438
|
"system.notification.empty": "\u30E1\u30C3\u30BB\u30FC\u30B8\u306F\u3042\u308A\u307E\u305B\u3093",
|
|
418
439
|
"system.notification.action.view_all": "\u3059\u3079\u3066\u8868\u793A",
|
|
@@ -423,7 +444,13 @@ var ja_JP_default = {
|
|
|
423
444
|
"system.notification.mark_all_read": "\u3059\u3079\u3066\u65E2\u8AAD",
|
|
424
445
|
"system.notification.action.archive": "\u30A2\u30FC\u30AB\u30A4\u30D6",
|
|
425
446
|
"system.notification.action.restore": "\u5FA9\u5143",
|
|
426
|
-
"system.notification.action.delete": "\u524A\u9664"
|
|
447
|
+
"system.notification.action.delete": "\u524A\u9664",
|
|
448
|
+
"system.ai.model.provider_placeholder": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u9078\u629E",
|
|
449
|
+
"system.ai.model.model_placeholder": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
|
|
450
|
+
"system.ai.model.load_failed": "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3068\u30E2\u30C7\u30EB\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F",
|
|
451
|
+
"system.ai.model.title": "\u30E2\u30C7\u30EB\u3092\u9078\u629E",
|
|
452
|
+
"system.ai.model.provider_model_count": "{providers} \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC \xB7 {models} \u30E2\u30C7\u30EB",
|
|
453
|
+
"system.ai.model.empty": "\u9078\u629E\u53EF\u80FD\u306A\u30E2\u30C7\u30EB\u304C\u3042\u308A\u307E\u305B\u3093"
|
|
427
454
|
};
|
|
428
455
|
|
|
429
456
|
// src/locales/zh-TW.json
|
|
@@ -444,6 +471,7 @@ var zh_TW_default = {
|
|
|
444
471
|
"system.ai.delete_session_failed": "\u522A\u9664\u6703\u8A71\u5931\u6557",
|
|
445
472
|
"system.ai.efficiency_assistant": "\u6548\u7387\u52A9\u624B",
|
|
446
473
|
"system.ai.failed_response": "\u9019\u6B21\u56DE\u8986\u6C92\u6709\u6210\u529F\u8FD4\u56DE\uFF0C\u4F60\u53EF\u4EE5\u76F4\u63A5\u91CD\u8A66\u525B\u624D\u7684\u554F\u984C\u3002",
|
|
474
|
+
"system.ai.chat.error.details_unavailable": "\u804A\u5929\u4ECB\u9762\u4E0D\u6703\u986F\u793A\u5E95\u5C64\u670D\u52D9\u932F\u8AA4\u8A73\u60C5\uFF0C\u8ACB\u67E5\u770B\u670D\u52D9\u65E5\u8A8C\u3002",
|
|
447
475
|
"system.ai.general_assistant": "\u901A\u7528\u52A9\u624B",
|
|
448
476
|
"system.ai.greeting": "\u60A8\u597D\uFF0C{period}\u597D\uFF01\u4ECA\u5929\u6709\u4EC0\u9EBC\u9700\u8981\u6211\u5354\u52A9\u7684\u55CE\uFF1F",
|
|
449
477
|
"system.ai.greeting_hello": "\u60A8\u597D\uFF0CAI\u52A9\u624B\u70BA\u60A8\u670D\u52D9\uFF01",
|
|
@@ -491,6 +519,7 @@ var zh_TW_default = {
|
|
|
491
519
|
"system.ai.text_assistant": "\u6587\u5B57\u52A9\u624B",
|
|
492
520
|
"system.ai.thinking": "\u6B63\u5728\u56DE\u8986",
|
|
493
521
|
"system.ai.tool_called": "\u5DF2\u547C\u53EB\uFF1A{tools}",
|
|
522
|
+
"system.ai.web_search": "\u7DB2\u8DEF\u641C\u5C0B",
|
|
494
523
|
"system.ai.upload_failed": "\u9644\u4EF6\u4E0A\u50B3\u5931\u6557",
|
|
495
524
|
"system.profile.account": "\u8CEC\u865F",
|
|
496
525
|
"system.profile.avatar_change": "\u9EDE\u9078\u4FEE\u6539\u982D\u50CF",
|
|
@@ -544,6 +573,7 @@ var zh_TW_default = {
|
|
|
544
573
|
"system.settings.mfa_copy_uri": "\u8907\u88FD\u7D81\u5B9A\u5730\u5740",
|
|
545
574
|
"system.settings.mfa_copy_success": "\u8907\u88FD\u6210\u529F",
|
|
546
575
|
"system.notification.title": "\u6D88\u606F\u4E2D\u5FC3",
|
|
576
|
+
"system.notification.sender.system": "\u7CFB\u7D71",
|
|
547
577
|
"system.notification.description": "\u67E5\u770B\u7CFB\u7D71\u901A\u77E5\u548C\u696D\u52D9\u6D88\u606F",
|
|
548
578
|
"system.notification.empty": "\u66AB\u7121\u6D88\u606F",
|
|
549
579
|
"system.notification.action.view_all": "\u67E5\u770B\u5168\u90E8",
|
|
@@ -554,7 +584,13 @@ var zh_TW_default = {
|
|
|
554
584
|
"system.notification.mark_all_read": "\u5168\u90E8\u5DF2\u8B80",
|
|
555
585
|
"system.notification.action.archive": "\u6B78\u6A94",
|
|
556
586
|
"system.notification.action.restore": "\u6062\u5FA9",
|
|
557
|
-
"system.notification.action.delete": "\u522A\u9664"
|
|
587
|
+
"system.notification.action.delete": "\u522A\u9664",
|
|
588
|
+
"system.ai.model.provider_placeholder": "\u9078\u64C7\u4F9B\u61C9\u5546",
|
|
589
|
+
"system.ai.model.model_placeholder": "\u9078\u64C7\u6A21\u578B",
|
|
590
|
+
"system.ai.model.load_failed": "\u8F09\u5165\u4F9B\u61C9\u5546\u8207\u6A21\u578B\u5931\u6557",
|
|
591
|
+
"system.ai.model.title": "\u9078\u64C7\u6A21\u578B",
|
|
592
|
+
"system.ai.model.provider_model_count": "{providers} \u500B\u4F9B\u61C9\u5546 \xB7 {models} \u500B\u6A21\u578B",
|
|
593
|
+
"system.ai.model.empty": "\u66AB\u7121\u53EF\u9078\u6A21\u578B"
|
|
558
594
|
};
|
|
559
595
|
|
|
560
596
|
// src/locales/generated.ts
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git@github.com:liujitcn/kratos-admin.git"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.0.
|
|
10
|
+
"version": "0.0.65",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"@tarojs/components": "4.2.1",
|
|
39
39
|
"@tarojs/taro": "4.2.1",
|
|
40
40
|
"react": "18.3.1",
|
|
41
|
-
"@liujitcn/kratos-taro-app-core": "^0.0.
|
|
42
|
-
"@liujitcn/kratos-taro-app-ui": "^0.0.
|
|
41
|
+
"@liujitcn/kratos-taro-app-core": "^0.0.65",
|
|
42
|
+
"@liujitcn/kratos-taro-app-ui": "^0.0.65"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "pnpm run build:entries && pnpm pack --pack-destination ../../../dist/npm",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AuthPromptError,
|
|
2
3
|
getRequestAccessToken,
|
|
3
4
|
handleAuthExpired,
|
|
4
5
|
http,
|
|
@@ -94,7 +95,7 @@ export async function SendAiMessageStream(
|
|
|
94
95
|
// direct stream 不经过 uni.request 拦截器,需要在这里补齐登录失效处理。
|
|
95
96
|
if (response.status === 401 || response.status === 403) {
|
|
96
97
|
handleAuthExpired('required')
|
|
97
|
-
throw new
|
|
98
|
+
throw new AuthPromptError()
|
|
98
99
|
}
|
|
99
100
|
if (!response.ok) {
|
|
100
101
|
throw new Error(await resolveStreamErrorMessage(response))
|
|
@@ -142,7 +143,7 @@ export function StreamAiMessageByChunkedRequest(
|
|
|
142
143
|
}
|
|
143
144
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
144
145
|
handleAuthExpired('required')
|
|
145
|
-
reject(new
|
|
146
|
+
reject(new AuthPromptError())
|
|
146
147
|
return
|
|
147
148
|
}
|
|
148
149
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { http } from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
2
|
+
import type {
|
|
3
|
+
AiModelService,
|
|
4
|
+
ListAiProviderModelOptionsRequest,
|
|
5
|
+
ListAiProviderModelOptionsResponse,
|
|
6
|
+
} from '../../../rpc/base/v1/ai_provider'
|
|
7
|
+
|
|
8
|
+
const AI_PROVIDER_MODELS_URL = '/v1/base/ai/provider-models'
|
|
9
|
+
|
|
10
|
+
/** AI Provider模型选项服务。 */
|
|
11
|
+
export class AiModelServiceImpl implements AiModelService {
|
|
12
|
+
/** 查询已启用Provider及其可用模型。 */
|
|
13
|
+
ListAiProviderModelOptions(
|
|
14
|
+
request: ListAiProviderModelOptionsRequest,
|
|
15
|
+
): Promise<ListAiProviderModelOptionsResponse> {
|
|
16
|
+
return http<ListAiProviderModelOptionsResponse>({
|
|
17
|
+
url: AI_PROVIDER_MODELS_URL,
|
|
18
|
+
method: 'GET',
|
|
19
|
+
authMode: 'required',
|
|
20
|
+
data: request,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const defAiModelService = new AiModelServiceImpl()
|
package/src/locales/en-US.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"system.ai.delete_session_failed": "Failed to delete session",
|
|
16
16
|
"system.ai.efficiency_assistant": "Productivity assistant",
|
|
17
17
|
"system.ai.failed_response": "The response did not complete. You can retry your previous question.",
|
|
18
|
+
"system.ai.chat.error.details_unavailable": "Provider error details are not exposed in chat. See the service logs.",
|
|
18
19
|
"system.ai.general_assistant": "General assistant",
|
|
19
20
|
"system.ai.greeting": "Good {period}, how can I help you today?",
|
|
20
21
|
"system.ai.greeting_hello": "Hello, your AI assistant is ready to help.",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"system.ai.text_assistant": "Writing assistant",
|
|
63
64
|
"system.ai.thinking": "Responding",
|
|
64
65
|
"system.ai.tool_called": "Tools used: {tools}",
|
|
66
|
+
"system.ai.web_search": "Web search",
|
|
65
67
|
"system.ai.upload_failed": "Attachment upload failed",
|
|
66
68
|
"system.profile.account": "Account",
|
|
67
69
|
"system.profile.avatar_change": "Tap to change avatar",
|
|
@@ -115,6 +117,7 @@
|
|
|
115
117
|
"system.settings.mfa_copy_uri": "Copy binding address",
|
|
116
118
|
"system.settings.mfa_copy_success": "Copied",
|
|
117
119
|
"system.notification.title": "Message Center",
|
|
120
|
+
"system.notification.sender.system": "System",
|
|
118
121
|
"system.notification.description": "View system notifications and business messages",
|
|
119
122
|
"system.notification.empty": "No messages",
|
|
120
123
|
"system.notification.action.view_all": "View all",
|
|
@@ -125,5 +128,11 @@
|
|
|
125
128
|
"system.notification.mark_all_read": "Mark all read",
|
|
126
129
|
"system.notification.action.archive": "Archive",
|
|
127
130
|
"system.notification.action.restore": "Restore",
|
|
128
|
-
"system.notification.action.delete": "Delete"
|
|
131
|
+
"system.notification.action.delete": "Delete",
|
|
132
|
+
"system.ai.model.provider_placeholder": "Select a provider",
|
|
133
|
+
"system.ai.model.model_placeholder": "Select a model",
|
|
134
|
+
"system.ai.model.load_failed": "Failed to load providers and models",
|
|
135
|
+
"system.ai.model.title": "Select Model",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} providers · {models} models",
|
|
137
|
+
"system.ai.model.empty": "No models available"
|
|
129
138
|
}
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"system.ai.delete_session_failed": "セッションの削除に失敗しました",
|
|
16
16
|
"system.ai.efficiency_assistant": "効率アシスタント",
|
|
17
17
|
"system.ai.failed_response": "今回は応答が正常に返されませんでした。質問を直接再試行できます。",
|
|
18
|
+
"system.ai.chat.error.details_unavailable": "チャットでは内部エラーの詳細は表示されません。サービスログを確認してください。",
|
|
18
19
|
"system.ai.general_assistant": "ユニバーサル アシスタント",
|
|
19
20
|
"system.ai.greeting": "こんにちは、{period} こんにちは!今日は何かお手伝いが必要なことはありますか?",
|
|
20
21
|
"system.ai.greeting_hello": "こんにちは、AI アシスタントがお手伝いします。",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"system.ai.text_assistant": "テキスト アシスタント",
|
|
63
64
|
"system.ai.thinking": "が返信しています",
|
|
64
65
|
"system.ai.tool_called": "呼び出し先: {tools}",
|
|
66
|
+
"system.ai.web_search": "ウェブ検索",
|
|
65
67
|
"system.ai.upload_failed": "添付ファイルのアップロードに失敗しました",
|
|
66
68
|
"system.profile.account": "アカウント",
|
|
67
69
|
"system.profile.avatar_change": "クリックしてアバターを変更します",
|
|
@@ -115,6 +117,7 @@
|
|
|
115
117
|
"system.settings.mfa_copy_uri": "登録アドレスをコピー",
|
|
116
118
|
"system.settings.mfa_copy_success": "コピーしました",
|
|
117
119
|
"system.notification.title": "メッセージセンター",
|
|
120
|
+
"system.notification.sender.system": "システム",
|
|
118
121
|
"system.notification.description": "システム通知と業務メッセージを確認",
|
|
119
122
|
"system.notification.empty": "メッセージはありません",
|
|
120
123
|
"system.notification.action.view_all": "すべて表示",
|
|
@@ -125,5 +128,11 @@
|
|
|
125
128
|
"system.notification.mark_all_read": "すべて既読",
|
|
126
129
|
"system.notification.action.archive": "アーカイブ",
|
|
127
130
|
"system.notification.action.restore": "復元",
|
|
128
|
-
"system.notification.action.delete": "削除"
|
|
131
|
+
"system.notification.action.delete": "削除",
|
|
132
|
+
"system.ai.model.provider_placeholder": "プロバイダーを選択",
|
|
133
|
+
"system.ai.model.model_placeholder": "モデルを選択",
|
|
134
|
+
"system.ai.model.load_failed": "プロバイダーとモデルを読み込めませんでした",
|
|
135
|
+
"system.ai.model.title": "モデルを選択",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} プロバイダー · {models} モデル",
|
|
137
|
+
"system.ai.model.empty": "選択可能なモデルがありません"
|
|
129
138
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"system.ai.delete_session_failed": "删除会话失败",
|
|
16
16
|
"system.ai.efficiency_assistant": "效率助手",
|
|
17
17
|
"system.ai.failed_response": "这次回复没有成功返回,你可以直接重试刚才的问题。",
|
|
18
|
+
"system.ai.chat.error.details_unavailable": "聊天界面不展示底层服务错误详情,请查看服务日志。",
|
|
18
19
|
"system.ai.general_assistant": "通用助手",
|
|
19
20
|
"system.ai.greeting": "您好,{period}好!今天有什么需要我协助的吗?",
|
|
20
21
|
"system.ai.greeting_hello": "您好,AI助手为您服务!",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"system.ai.text_assistant": "文本助手",
|
|
63
64
|
"system.ai.thinking": "正在回复",
|
|
64
65
|
"system.ai.tool_called": "已调用:{tools}",
|
|
66
|
+
"system.ai.web_search": "联网搜索",
|
|
65
67
|
"system.ai.upload_failed": "附件上传失败",
|
|
66
68
|
"system.profile.account": "账号",
|
|
67
69
|
"system.profile.avatar_change": "点击修改头像",
|
|
@@ -115,6 +117,7 @@
|
|
|
115
117
|
"system.settings.mfa_copy_uri": "复制绑定地址",
|
|
116
118
|
"system.settings.mfa_copy_success": "复制成功",
|
|
117
119
|
"system.notification.title": "消息中心",
|
|
120
|
+
"system.notification.sender.system": "系统",
|
|
118
121
|
"system.notification.description": "查看系统通知和业务消息",
|
|
119
122
|
"system.notification.empty": "暂无消息",
|
|
120
123
|
"system.notification.action.view_all": "查看全部",
|
|
@@ -125,5 +128,11 @@
|
|
|
125
128
|
"system.notification.mark_all_read": "全部已读",
|
|
126
129
|
"system.notification.action.archive": "归档",
|
|
127
130
|
"system.notification.action.restore": "恢复",
|
|
128
|
-
"system.notification.action.delete": "删除"
|
|
131
|
+
"system.notification.action.delete": "删除",
|
|
132
|
+
"system.ai.model.provider_placeholder": "选择供应商",
|
|
133
|
+
"system.ai.model.model_placeholder": "选择模型",
|
|
134
|
+
"system.ai.model.load_failed": "加载供应商和模型失败",
|
|
135
|
+
"system.ai.model.title": "选择模型",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} 个供应商 · {models} 个模型",
|
|
137
|
+
"system.ai.model.empty": "暂无可选模型"
|
|
129
138
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"system.ai.delete_session_failed": "刪除會話失敗",
|
|
16
16
|
"system.ai.efficiency_assistant": "效率助手",
|
|
17
17
|
"system.ai.failed_response": "這次回覆沒有成功返回,你可以直接重試剛才的問題。",
|
|
18
|
+
"system.ai.chat.error.details_unavailable": "聊天介面不會顯示底層服務錯誤詳情,請查看服務日誌。",
|
|
18
19
|
"system.ai.general_assistant": "通用助手",
|
|
19
20
|
"system.ai.greeting": "您好,{period}好!今天有什麼需要我協助的嗎?",
|
|
20
21
|
"system.ai.greeting_hello": "您好,AI助手為您服務!",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"system.ai.text_assistant": "文字助手",
|
|
63
64
|
"system.ai.thinking": "正在回覆",
|
|
64
65
|
"system.ai.tool_called": "已呼叫:{tools}",
|
|
66
|
+
"system.ai.web_search": "網路搜尋",
|
|
65
67
|
"system.ai.upload_failed": "附件上傳失敗",
|
|
66
68
|
"system.profile.account": "賬號",
|
|
67
69
|
"system.profile.avatar_change": "點選修改頭像",
|
|
@@ -115,6 +117,7 @@
|
|
|
115
117
|
"system.settings.mfa_copy_uri": "複製綁定地址",
|
|
116
118
|
"system.settings.mfa_copy_success": "複製成功",
|
|
117
119
|
"system.notification.title": "消息中心",
|
|
120
|
+
"system.notification.sender.system": "系統",
|
|
118
121
|
"system.notification.description": "查看系統通知和業務消息",
|
|
119
122
|
"system.notification.empty": "暫無消息",
|
|
120
123
|
"system.notification.action.view_all": "查看全部",
|
|
@@ -125,5 +128,11 @@
|
|
|
125
128
|
"system.notification.mark_all_read": "全部已讀",
|
|
126
129
|
"system.notification.action.archive": "歸檔",
|
|
127
130
|
"system.notification.action.restore": "恢復",
|
|
128
|
-
"system.notification.action.delete": "刪除"
|
|
131
|
+
"system.notification.action.delete": "刪除",
|
|
132
|
+
"system.ai.model.provider_placeholder": "選擇供應商",
|
|
133
|
+
"system.ai.model.model_placeholder": "選擇模型",
|
|
134
|
+
"system.ai.model.load_failed": "載入供應商與模型失敗",
|
|
135
|
+
"system.ai.model.title": "選擇模型",
|
|
136
|
+
"system.ai.model.provider_model_count": "{providers} 個供應商 · {models} 個模型",
|
|
137
|
+
"system.ai.model.empty": "暫無可選模型"
|
|
129
138
|
}
|
|
@@ -46,7 +46,13 @@ export interface SendAiMessageRequest {
|
|
|
46
46
|
/** 附件列表 */
|
|
47
47
|
attachments: AiAttachment[];
|
|
48
48
|
/** 助手动作 */
|
|
49
|
-
action:
|
|
49
|
+
action:
|
|
50
|
+
| AiAction
|
|
51
|
+
| undefined;
|
|
52
|
+
/** 所选供应商ID;留空时使用首个可用模型 */
|
|
53
|
+
provider_id: number;
|
|
54
|
+
/** 所选模型名称 */
|
|
55
|
+
model_name: string;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
/** AI 助手失败消息重试请求 */
|
|
@@ -0,0 +1,41 @@
|
|
|
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_provider.proto
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
|
|
9
|
+
/** AI Provider模型选项查询请求。 */
|
|
10
|
+
export interface ListAiProviderModelOptionsRequest {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** AI Provider模型选项查询响应。 */
|
|
14
|
+
export interface ListAiProviderModelOptionsResponse {
|
|
15
|
+
/** Provider及其模型列表 */
|
|
16
|
+
providers: AiProviderModelOption[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 已启用的AI Provider及其模型选项。 */
|
|
20
|
+
export interface AiProviderModelOption {
|
|
21
|
+
/** Provider ID */
|
|
22
|
+
provider_id: number;
|
|
23
|
+
/** Provider名称 */
|
|
24
|
+
name: string;
|
|
25
|
+
/** 模型选项列表 */
|
|
26
|
+
models: AiProviderModel[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** AI Provider下的单个模型选项。 */
|
|
30
|
+
export interface AiProviderModel {
|
|
31
|
+
/** 模型名称 */
|
|
32
|
+
model_name: string;
|
|
33
|
+
/** 展示名称 */
|
|
34
|
+
display_name: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** AI Provider 可选模型查询服务。 */
|
|
38
|
+
export interface AiModelService {
|
|
39
|
+
/** 查询已启用的AI Provider及其可用模型。 */
|
|
40
|
+
ListAiProviderModelOptions(request: ListAiProviderModelOptionsRequest): Promise<ListAiProviderModelOptionsResponse>;
|
|
41
|
+
}
|
|
@@ -108,6 +108,8 @@ export interface DeleteAiSessionResponse {
|
|
|
108
108
|
export interface AiSession {
|
|
109
109
|
/** 会话ID */
|
|
110
110
|
id: string;
|
|
111
|
+
/** 租户ID */
|
|
112
|
+
tenant_id: number;
|
|
111
113
|
/** 会话标题 */
|
|
112
114
|
title: string;
|
|
113
115
|
/** 会话摘要 */
|
|
@@ -124,6 +126,8 @@ export interface AiSession {
|
|
|
124
126
|
export interface AiMessage {
|
|
125
127
|
/** 消息ID */
|
|
126
128
|
id: string;
|
|
129
|
+
/** 租户ID */
|
|
130
|
+
tenant_id: number;
|
|
127
131
|
/** 输入内容 */
|
|
128
132
|
input_content:
|
|
129
133
|
| AiInputContent
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { Button, Text, Textarea, View } from '@tarojs/components'
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
2
3
|
import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
|
|
3
4
|
import type { AiAttachment } from '../../../../rpc/base/v1/ai_session'
|
|
5
|
+
import type { AiProviderModelOption } from '../../../../rpc/base/v1/ai_provider'
|
|
6
|
+
import { fallbackModelDisplayName } from '../modelDisplay'
|
|
7
|
+
import ModelPicker from './ModelPicker'
|
|
4
8
|
import './composer.scss'
|
|
5
9
|
|
|
10
|
+
/** AI 助手输入框与模型选择器参数。 */
|
|
6
11
|
type ComposerProps = {
|
|
7
12
|
value: string
|
|
8
13
|
attachments: AiAttachment[]
|
|
14
|
+
/** 当前可供选择的供应商与模型。 */
|
|
15
|
+
modelProviders: AiProviderModelOption[]
|
|
16
|
+
/** 当前选中的供应商编号和模型名称。 */
|
|
17
|
+
providerId: number
|
|
18
|
+
modelName: string
|
|
19
|
+
/** 选择器未配置时显示的本地化占位文字。 */
|
|
20
|
+
modelPlaceholder: string
|
|
9
21
|
placeholder: string
|
|
10
22
|
bottom: string
|
|
11
23
|
recording: boolean
|
|
@@ -16,12 +28,43 @@ type ComposerProps = {
|
|
|
16
28
|
onRecord: () => void
|
|
17
29
|
onSend: () => void
|
|
18
30
|
onRemoveAttachment: (attachment: AiAttachment) => void
|
|
31
|
+
onModelChange: (providerId: number, modelName: string) => void
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
/** AI 助手消息输入器。 */
|
|
22
35
|
export default function Composer(props: ComposerProps) {
|
|
36
|
+
const [showModelPicker, setShowModelPicker] = useState(false)
|
|
37
|
+
|
|
38
|
+
const selectedModelDisplayName = useMemo(() => {
|
|
39
|
+
const provider = props.modelProviders.find((item) => item.provider_id === props.providerId)
|
|
40
|
+
const matched = provider?.models.find((model) => model.model_name === props.modelName)
|
|
41
|
+
return matched?.display_name || fallbackModelDisplayName(props.modelName)
|
|
42
|
+
}, [props.modelProviders, props.providerId, props.modelName])
|
|
43
|
+
|
|
44
|
+
const openModelPicker = () => {
|
|
45
|
+
if (props.sending || !props.modelProviders.length) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
setShowModelPicker(true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleModelSelect = (providerId: number, modelName: string) => {
|
|
52
|
+
setShowModelPicker(false)
|
|
53
|
+
props.onModelChange(providerId, modelName)
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
return (
|
|
24
57
|
<View className='composer' style={{ paddingBottom: props.bottom }}>
|
|
58
|
+
<View
|
|
59
|
+
className={`composer-model-chip${props.sending || !props.modelProviders.length ? ' is-disabled' : ''}`}
|
|
60
|
+
onClick={openModelPicker}
|
|
61
|
+
>
|
|
62
|
+
<Text className='composer-model-chip__spark'>✦</Text>
|
|
63
|
+
<Text className='composer-model-chip__label'>
|
|
64
|
+
{selectedModelDisplayName || props.modelPlaceholder}
|
|
65
|
+
</Text>
|
|
66
|
+
<Text className='composer-model-chip__chevron'>▾</Text>
|
|
67
|
+
</View>
|
|
25
68
|
<View className='composer-main'>
|
|
26
69
|
<Button className='attach-button' hoverClass='none' onClick={props.onAttach}>
|
|
27
70
|
<UniIcon type='plusempty' size={30} color='#111' />
|
|
@@ -66,6 +109,16 @@ export default function Composer(props: ComposerProps) {
|
|
|
66
109
|
<UniIcon type='paperplane' size={28} color={props.disabled ? '#111' : '#00a96b'} />
|
|
67
110
|
</Button>
|
|
68
111
|
</View>
|
|
112
|
+
|
|
113
|
+
{showModelPicker ? (
|
|
114
|
+
<ModelPicker
|
|
115
|
+
providers={props.modelProviders}
|
|
116
|
+
providerId={props.providerId}
|
|
117
|
+
modelName={props.modelName}
|
|
118
|
+
onSelect={handleModelSelect}
|
|
119
|
+
onClose={() => setShowModelPicker(false)}
|
|
120
|
+
/>
|
|
121
|
+
) : null}
|
|
69
122
|
</View>
|
|
70
123
|
)
|
|
71
124
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ScrollView, Text, View } from '@tarojs/components'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { t } from '@liujitcn/kratos-taro-app-core'
|
|
4
|
+
import type { AiProviderModelOption } from '../../../../rpc/base/v1/ai_provider'
|
|
5
|
+
import { fallbackModelDisplayName } from '../modelDisplay'
|
|
6
|
+
import './model-picker.scss'
|
|
7
|
+
|
|
8
|
+
type PickerModel = {
|
|
9
|
+
name: string
|
|
10
|
+
displayName: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PickerProvider = {
|
|
14
|
+
providerId: number
|
|
15
|
+
label: string
|
|
16
|
+
models: PickerModel[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ModelPickerProps = {
|
|
20
|
+
providers: AiProviderModelOption[]
|
|
21
|
+
providerId: number
|
|
22
|
+
modelName: string
|
|
23
|
+
onSelect: (providerId: number, modelName: string) => void
|
|
24
|
+
onClose: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** AI 模型选择底部弹层。 */
|
|
28
|
+
export default function ModelPicker(props: ModelPickerProps) {
|
|
29
|
+
const pickerProviders = useMemo<PickerProvider[]>(
|
|
30
|
+
() =>
|
|
31
|
+
props.providers.map((provider) => ({
|
|
32
|
+
providerId: provider.provider_id,
|
|
33
|
+
label: provider.name,
|
|
34
|
+
models: provider.models.map((model) => ({
|
|
35
|
+
name: model.model_name,
|
|
36
|
+
displayName: model.display_name || fallbackModelDisplayName(model.model_name),
|
|
37
|
+
})),
|
|
38
|
+
})),
|
|
39
|
+
[props.providers],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const modelCount = pickerProviders.reduce((total, provider) => total + provider.models.length, 0)
|
|
43
|
+
|
|
44
|
+
const isSelected = (providerId: number, modelName: string) =>
|
|
45
|
+
props.providerId === providerId && props.modelName === modelName
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View className='model-picker' catchMove>
|
|
49
|
+
<View className='model-picker__mask' onClick={props.onClose} />
|
|
50
|
+
<View className='model-picker__sheet'>
|
|
51
|
+
<View className='model-picker__handle' />
|
|
52
|
+
<View className='model-picker__head'>
|
|
53
|
+
<Text className='model-picker__title'>{t('system.ai.model.title')}</Text>
|
|
54
|
+
<Text className='model-picker__count'>
|
|
55
|
+
{t('system.ai.model.provider_model_count', {
|
|
56
|
+
providers: pickerProviders.length,
|
|
57
|
+
models: modelCount,
|
|
58
|
+
})}
|
|
59
|
+
</Text>
|
|
60
|
+
<View className='model-picker__close' onClick={props.onClose}>
|
|
61
|
+
✕
|
|
62
|
+
</View>
|
|
63
|
+
</View>
|
|
64
|
+
<ScrollView className='model-picker__body' scrollY>
|
|
65
|
+
{pickerProviders.map((provider) => (
|
|
66
|
+
<View key={provider.providerId}>
|
|
67
|
+
<View className='model-picker__group'>{provider.label}</View>
|
|
68
|
+
{provider.models.map((model) => (
|
|
69
|
+
<View
|
|
70
|
+
key={model.name}
|
|
71
|
+
className={`model-picker__row${isSelected(provider.providerId, model.name) ? ' is-selected' : ''}`}
|
|
72
|
+
onClick={() => props.onSelect(provider.providerId, model.name)}
|
|
73
|
+
>
|
|
74
|
+
<View className='model-picker__row-main'>
|
|
75
|
+
<Text className='model-picker__row-name'>{model.displayName}</Text>
|
|
76
|
+
{model.displayName !== model.name ? (
|
|
77
|
+
<Text className='model-picker__row-raw'>{model.name}</Text>
|
|
78
|
+
) : null}
|
|
79
|
+
</View>
|
|
80
|
+
{isSelected(provider.providerId, model.name) ? (
|
|
81
|
+
<Text className='model-picker__row-check'>✓</Text>
|
|
82
|
+
) : null}
|
|
83
|
+
</View>
|
|
84
|
+
))}
|
|
85
|
+
</View>
|
|
86
|
+
))}
|
|
87
|
+
{!pickerProviders.length ? (
|
|
88
|
+
<View className='model-picker__empty'>{t('system.ai.model.empty')}</View>
|
|
89
|
+
) : null}
|
|
90
|
+
</ScrollView>
|
|
91
|
+
<View className='model-picker__safe' />
|
|
92
|
+
</View>
|
|
93
|
+
</View>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -19,6 +19,51 @@
|
|
|
19
19
|
box-sizing: border-box;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
.composer-model-chip {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 10px;
|
|
26
|
+
max-width: 100%;
|
|
27
|
+
height: 56px;
|
|
28
|
+
padding: 0 24px;
|
|
29
|
+
margin: 0 8px 12px;
|
|
30
|
+
color: #1f2937;
|
|
31
|
+
background: #fff;
|
|
32
|
+
border: 2px solid #d6dae2;
|
|
33
|
+
border-radius: 28px;
|
|
34
|
+
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.composer-model-chip.is-disabled {
|
|
39
|
+
opacity: 0.55;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.composer-model-chip__spark {
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
color: #00a96b;
|
|
45
|
+
font-size: 26px;
|
|
46
|
+
line-height: 26px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.composer-model-chip__label {
|
|
50
|
+
max-width: 320px;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
color: inherit;
|
|
53
|
+
font-size: 24px;
|
|
54
|
+
font-weight: 500;
|
|
55
|
+
line-height: 34px;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.composer-model-chip__chevron {
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
color: #8a8f99;
|
|
63
|
+
font-size: 22px;
|
|
64
|
+
line-height: 22px;
|
|
65
|
+
}
|
|
66
|
+
|
|
22
67
|
.composer-main {
|
|
23
68
|
display: flex;
|
|
24
69
|
align-items: center;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
.model-picker {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
right: 0;
|
|
5
|
+
bottom: 0;
|
|
6
|
+
left: 0;
|
|
7
|
+
z-index: 100;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.model-picker__mask {
|
|
11
|
+
position: absolute;
|
|
12
|
+
top: 0;
|
|
13
|
+
right: 0;
|
|
14
|
+
bottom: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
background-color: rgba(15, 23, 42, 0.45);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.model-picker__sheet {
|
|
20
|
+
position: absolute;
|
|
21
|
+
right: 0;
|
|
22
|
+
bottom: 0;
|
|
23
|
+
left: 0;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
max-height: 68vh;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
background-color: #fff;
|
|
29
|
+
border-radius: 32px 32px 0 0;
|
|
30
|
+
box-shadow: 0 -24px 80px rgba(15, 23, 42, 0.16);
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.model-picker__handle {
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
width: 72px;
|
|
37
|
+
height: 8px;
|
|
38
|
+
margin: 16px auto 8px;
|
|
39
|
+
background-color: #e2e5ea;
|
|
40
|
+
border-radius: 4px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.model-picker__head {
|
|
44
|
+
position: relative;
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: baseline;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
gap: 12px;
|
|
50
|
+
padding: 0 96px 16px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.model-picker__title {
|
|
54
|
+
color: #111;
|
|
55
|
+
font-size: 32px;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.model-picker__count {
|
|
60
|
+
color: #8a8f99;
|
|
61
|
+
font-size: 20px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.model-picker__close {
|
|
65
|
+
position: absolute;
|
|
66
|
+
top: -6px;
|
|
67
|
+
right: 24px;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
width: 56px;
|
|
72
|
+
height: 56px;
|
|
73
|
+
color: #8a8f99;
|
|
74
|
+
font-size: 28px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.model-picker__body {
|
|
78
|
+
flex: 1;
|
|
79
|
+
min-height: 0;
|
|
80
|
+
max-height: calc(68vh - 130px - env(safe-area-inset-bottom));
|
|
81
|
+
padding: 0 32px;
|
|
82
|
+
box-sizing: border-box;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.model-picker__group {
|
|
86
|
+
padding: 24px 8px 12px;
|
|
87
|
+
color: #8a8f99;
|
|
88
|
+
font-size: 22px;
|
|
89
|
+
line-height: 32px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.model-picker__row {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
min-height: 88px;
|
|
96
|
+
padding: 16px 28px;
|
|
97
|
+
margin-bottom: 12px;
|
|
98
|
+
border: 2px solid #eef0f4;
|
|
99
|
+
border-radius: 24px;
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.model-picker__row.is-selected {
|
|
104
|
+
color: #0f7a62;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
background-color: #e8f8f4;
|
|
107
|
+
border-color: #d5efe7;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.model-picker__row-main {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
gap: 4px;
|
|
114
|
+
min-width: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.model-picker__row-name {
|
|
118
|
+
color: inherit;
|
|
119
|
+
font-size: 28px;
|
|
120
|
+
line-height: 40px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.model-picker__row-raw {
|
|
124
|
+
color: #a7acb5;
|
|
125
|
+
font-size: 22px;
|
|
126
|
+
font-weight: 400;
|
|
127
|
+
line-height: 30px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.model-picker__row-check {
|
|
131
|
+
margin-left: auto;
|
|
132
|
+
color: #00a96b;
|
|
133
|
+
font-size: 32px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.model-picker__empty {
|
|
137
|
+
padding: 64px 0;
|
|
138
|
+
color: #8a8f99;
|
|
139
|
+
font-size: 26px;
|
|
140
|
+
text-align: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.model-picker__safe {
|
|
144
|
+
flex-shrink: 0;
|
|
145
|
+
height: calc(18px + constant(safe-area-inset-bottom));
|
|
146
|
+
height: calc(18px + env(safe-area-inset-bottom));
|
|
147
|
+
}
|
|
@@ -10,16 +10,21 @@ import {
|
|
|
10
10
|
useI18n,
|
|
11
11
|
} from '@liujitcn/kratos-taro-app-core'
|
|
12
12
|
import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
|
|
13
|
+
import { isAuthExpiredError } from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
13
14
|
import { defAiMessageService, StreamAiMessageByChunkedRequest } from '../../../api/base/v1/ai_message'
|
|
14
15
|
import { defAiSessionService } from '../../../api/base/v1/ai_session'
|
|
15
16
|
import { defAiToolService } from '../../../api/base/v1/ai_tool'
|
|
17
|
+
import { defAiModelService } from '../../../api/base/v1/ai_provider'
|
|
16
18
|
import type { AiAttachment, AiMessage, AiSession } from '../../../rpc/base/v1/ai_session'
|
|
19
|
+
import type { AiProviderModelOption } from '../../../rpc/base/v1/ai_provider'
|
|
17
20
|
import type { AiShortcut, AiToolCall } from '../../../rpc/base/v1/ai_tool'
|
|
18
21
|
import { AiMessageStatus } from '../../../rpc/base/v1/ai_session'
|
|
19
22
|
import { Terminal } from '../../../rpc/base/v1/ai_tool'
|
|
20
23
|
import Composer from './components/Composer'
|
|
21
24
|
import SessionDrawer from './components/SessionDrawer'
|
|
22
25
|
import WelcomePanel from './components/WelcomePanel'
|
|
26
|
+
import { resolveModelDisplayName } from './modelDisplay'
|
|
27
|
+
import { loadStoredModelSelection, saveModelSelection } from './modelSelection'
|
|
23
28
|
import {
|
|
24
29
|
type AiStreamEvent,
|
|
25
30
|
type AiStreamPayload,
|
|
@@ -92,6 +97,9 @@ export default function AiPage() {
|
|
|
92
97
|
const [sessions, setSessionsState] = useState<AiSession[]>([])
|
|
93
98
|
const [messages, setMessagesState] = useState<MessageMap>({})
|
|
94
99
|
const [selectedAttachments, setSelectedAttachments] = useState<AiAttachment[]>([])
|
|
100
|
+
const [modelProviders, setModelProviders] = useState<AiProviderModelOption[]>([])
|
|
101
|
+
const [selectedProviderId, setSelectedProviderId] = useState(0)
|
|
102
|
+
const [selectedModelName, setSelectedModelName] = useState('')
|
|
95
103
|
const [starterShortcuts, setStarterShortcuts] = useState<AiShortcut[]>(createDefaultShortcuts)
|
|
96
104
|
|
|
97
105
|
const activeSessionIDRef = useRef('')
|
|
@@ -351,25 +359,67 @@ export default function AiPage() {
|
|
|
351
359
|
if (loadingShortcutsRef.current) return
|
|
352
360
|
setLoadingShortcuts(true)
|
|
353
361
|
try {
|
|
354
|
-
const response = await
|
|
362
|
+
const [response, providerResponse] = await Promise.all([
|
|
363
|
+
defAiToolService.ListAiShortcut({ terminal: AI_TERMINAL }),
|
|
364
|
+
defAiModelService.ListAiProviderModelOptions({}),
|
|
365
|
+
])
|
|
355
366
|
const shortcuts = normalizeStarterShortcuts(response.shortcuts).filter((item) => !item.action)
|
|
367
|
+
const providers = providerResponse.providers ?? []
|
|
368
|
+
setModelProviders(providers)
|
|
369
|
+
applyModelSelection(providers)
|
|
356
370
|
if (shortcuts.length) {
|
|
357
371
|
setStarterShortcuts(shortcuts)
|
|
358
372
|
setStarterPromptGroupIndex(0)
|
|
359
373
|
}
|
|
360
374
|
} catch (error) {
|
|
375
|
+
setModelProviders([])
|
|
376
|
+
setSelectedProviderId(0)
|
|
377
|
+
setSelectedModelName('')
|
|
361
378
|
showError(error, t('system.ai.load_shortcuts_failed'))
|
|
362
379
|
} finally {
|
|
363
380
|
setLoadingShortcuts(false)
|
|
364
381
|
}
|
|
365
382
|
}
|
|
366
383
|
|
|
384
|
+
const handleModelChange = (providerId: number, modelName: string) => {
|
|
385
|
+
setSelectedProviderId(providerId)
|
|
386
|
+
setSelectedModelName(modelName)
|
|
387
|
+
saveModelSelection({ provider_id: providerId, model_name: modelName })
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 优先恢复本地缓存的模型选择,缓存失效时回退到首个可用模型。 */
|
|
391
|
+
const applyModelSelection = (providers: AiProviderModelOption[]) => {
|
|
392
|
+
const stored = loadStoredModelSelection()
|
|
393
|
+
const provider = stored
|
|
394
|
+
? providers.find(
|
|
395
|
+
(item) =>
|
|
396
|
+
item.provider_id === stored.provider_id &&
|
|
397
|
+
item.models.some((model) => model.model_name === stored.model_name),
|
|
398
|
+
)
|
|
399
|
+
: undefined
|
|
400
|
+
if (provider && stored) {
|
|
401
|
+
setSelectedProviderId(provider.provider_id)
|
|
402
|
+
setSelectedModelName(stored.model_name)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
const firstProvider = providers[0]
|
|
406
|
+
setSelectedProviderId(firstProvider?.provider_id ?? 0)
|
|
407
|
+
setSelectedModelName(firstProvider?.models[0]?.model_name ?? '')
|
|
408
|
+
}
|
|
409
|
+
|
|
367
410
|
const runAiTask = async (
|
|
368
411
|
sessionID: string,
|
|
369
412
|
payload: { text: string; attachments: AiAttachment[] },
|
|
370
413
|
) => {
|
|
371
414
|
let task: StreamTask | undefined
|
|
372
|
-
const request = {
|
|
415
|
+
const request = {
|
|
416
|
+
session_id: sessionID,
|
|
417
|
+
content: payload.text,
|
|
418
|
+
provider_id: selectedProviderId,
|
|
419
|
+
model_name: selectedModelName,
|
|
420
|
+
attachments: payload.attachments,
|
|
421
|
+
action: undefined,
|
|
422
|
+
}
|
|
373
423
|
try {
|
|
374
424
|
let handledByStream = false
|
|
375
425
|
if (process.env.TARO_ENV === 'weapp') {
|
|
@@ -687,7 +737,7 @@ export default function AiPage() {
|
|
|
687
737
|
onLongPress={() => void handleMessageAction(item)}
|
|
688
738
|
>
|
|
689
739
|
{item.role === 'ai' && item.model ? (
|
|
690
|
-
<View className='reply-meta'><Text className='reply-tag'>{t('system.ai.model_reply')}</Text><Text className='reply-model'>{item.model}</Text></View>
|
|
740
|
+
<View className='reply-meta'><Text className='reply-tag'>{t('system.ai.model_reply')}</Text><Text className='reply-model'>{resolveModelDisplayName(modelProviders, item.model)}</Text></View>
|
|
691
741
|
) : null}
|
|
692
742
|
<View className='bubble-content'>{item.content}</View>
|
|
693
743
|
{item.attachments.length ? (
|
|
@@ -719,6 +769,10 @@ export default function AiPage() {
|
|
|
719
769
|
<Composer
|
|
720
770
|
value={inputText}
|
|
721
771
|
attachments={selectedAttachments}
|
|
772
|
+
modelProviders={modelProviders}
|
|
773
|
+
providerId={selectedProviderId}
|
|
774
|
+
modelName={selectedModelName}
|
|
775
|
+
modelPlaceholder={t('system.ai.model.model_placeholder')}
|
|
722
776
|
placeholder={composerPlaceholder}
|
|
723
777
|
bottom={composerBottom}
|
|
724
778
|
recording={isRecording}
|
|
@@ -736,6 +790,7 @@ export default function AiPage() {
|
|
|
736
790
|
})
|
|
737
791
|
}}
|
|
738
792
|
onSend={() => void handleSend()}
|
|
793
|
+
onModelChange={handleModelChange}
|
|
739
794
|
onRemoveAttachment={(attachment) => setSelectedAttachments((current) => current.filter((item) => item !== attachment))}
|
|
740
795
|
/>
|
|
741
796
|
<SessionDrawer
|
|
@@ -758,6 +813,7 @@ export default function AiPage() {
|
|
|
758
813
|
function normalizeSession(session?: Partial<AiSession> | null): AiSession {
|
|
759
814
|
return {
|
|
760
815
|
id: String(session?.id ?? ''),
|
|
816
|
+
tenant_id: Number(session?.tenant_id ?? 0),
|
|
761
817
|
title: String(session?.title ?? t('system.ai.new_session')),
|
|
762
818
|
summary: String(session?.summary ?? ''),
|
|
763
819
|
updated_at: session?.updated_at,
|
|
@@ -848,6 +904,7 @@ function createLocalUserMessage(payload: { text: string; attachments: AiAttachme
|
|
|
848
904
|
const now = Date.now()
|
|
849
905
|
const message = mapMessageItem({
|
|
850
906
|
id: `${LOCAL_USER_MESSAGE_PREFIX}-${now}`,
|
|
907
|
+
tenant_id: 0,
|
|
851
908
|
input_content: { kind: 'text', content: payload.text },
|
|
852
909
|
output_content: undefined,
|
|
853
910
|
attachments: payload.attachments,
|
|
@@ -869,8 +926,9 @@ function createThinkingMessage(options?: { sessionID?: string; messageID?: strin
|
|
|
869
926
|
? buildStreamMessageKey(options.sessionID, options.messageID || PENDING_MESSAGE_ID)
|
|
870
927
|
: undefined
|
|
871
928
|
const message = mapMessageItem({
|
|
872
|
-
|
|
873
|
-
|
|
929
|
+
id: streamKey || `ai-thinking-${now}`,
|
|
930
|
+
tenant_id: 0,
|
|
931
|
+
input_content: undefined,
|
|
874
932
|
output_content: {
|
|
875
933
|
kind: 'text', content: t('system.ai.thinking'), reply_source: '', model: '', fallback: false,
|
|
876
934
|
fallback_reason: '', flow: '', step: '', blocks_json: '',
|
|
@@ -997,10 +1055,12 @@ function formatAttachmentMeta(attachment: AiAttachment) {
|
|
|
997
1055
|
}
|
|
998
1056
|
|
|
999
1057
|
function formatTools(tools: AiToolCall[]) {
|
|
1000
|
-
return tools.map((item) => item.title || item.name).filter(Boolean).join(' · ')
|
|
1058
|
+
return tools.map((item) => (item.name === 'web_search' ? t('system.ai.web_search') : item.title || item.name)).filter(Boolean).join(' · ')
|
|
1001
1059
|
}
|
|
1002
1060
|
|
|
1003
1061
|
function showError(error: unknown, fallback: string) {
|
|
1062
|
+
// 登录态失效时请求层已弹窗引导重新登录,不再叠加失败提示。
|
|
1063
|
+
if (isAuthExpiredError(error)) return
|
|
1004
1064
|
const message = error instanceof Error ? error.message : fallback
|
|
1005
1065
|
void Taro.showToast({ icon: 'none', title: message || fallback })
|
|
1006
1066
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AiProviderModelOption } from '../../../rpc/base/v1/ai_provider'
|
|
2
|
+
|
|
3
|
+
/** 常见模型的展示名映射;未命中时按通用规则美化原始名。 */
|
|
4
|
+
const MODEL_DISPLAY_NAMES: Record<string, string> = {
|
|
5
|
+
'deepseek-chat': 'DeepSeek Chat',
|
|
6
|
+
'deepseek-reasoner': 'DeepSeek Reasoner',
|
|
7
|
+
'deepseek-v3': 'DeepSeek V3',
|
|
8
|
+
'deepseek-r1': 'DeepSeek R1',
|
|
9
|
+
'gpt-4o': 'GPT-4o',
|
|
10
|
+
'gpt-4o-mini': 'GPT-4o mini',
|
|
11
|
+
'gpt-4.1': 'GPT-4.1',
|
|
12
|
+
'gpt-4.1-mini': 'GPT-4.1 mini',
|
|
13
|
+
'gpt-4-turbo': 'GPT-4 Turbo',
|
|
14
|
+
'o3-mini': 'O3-mini',
|
|
15
|
+
'claude-3-5-sonnet': 'Claude 3.5 Sonnet',
|
|
16
|
+
'claude-3-7-sonnet': 'Claude 3.7 Sonnet',
|
|
17
|
+
'qwen-max': 'Qwen Max',
|
|
18
|
+
'qwen-plus': 'Qwen Plus',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 需要整体大写的模型名片段。 */
|
|
22
|
+
const UPPERCASE_TOKENS = new Set([
|
|
23
|
+
'gpt',
|
|
24
|
+
'llama',
|
|
25
|
+
'qwen',
|
|
26
|
+
'glm',
|
|
27
|
+
'kimi',
|
|
28
|
+
'gemini',
|
|
29
|
+
'claude',
|
|
30
|
+
'doubao',
|
|
31
|
+
'deepseek',
|
|
32
|
+
'moonshot',
|
|
33
|
+
'hunyuan',
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
/** 对未配置展示名的模型名做通用美化。 */
|
|
37
|
+
export function fallbackModelDisplayName(modelName: string): string {
|
|
38
|
+
if (!modelName) {
|
|
39
|
+
return ''
|
|
40
|
+
}
|
|
41
|
+
const mapped = MODEL_DISPLAY_NAMES[modelName.toLowerCase()]
|
|
42
|
+
if (mapped) {
|
|
43
|
+
return mapped
|
|
44
|
+
}
|
|
45
|
+
return modelName
|
|
46
|
+
.split(/[-_]/)
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((token) => {
|
|
49
|
+
const lower = token.toLowerCase()
|
|
50
|
+
if (UPPERCASE_TOKENS.has(lower)) {
|
|
51
|
+
return lower.toUpperCase()
|
|
52
|
+
}
|
|
53
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1)
|
|
54
|
+
})
|
|
55
|
+
.join(' ')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 从供应商模型选项中解析模型展示名称,兼容历史消息中「供应商 / 模型」格式的原始值。 */
|
|
59
|
+
export function resolveModelDisplayName(
|
|
60
|
+
providers: AiProviderModelOption[] | undefined,
|
|
61
|
+
modelName: string,
|
|
62
|
+
): string {
|
|
63
|
+
if (!modelName) {
|
|
64
|
+
return ''
|
|
65
|
+
}
|
|
66
|
+
const separator = ' / '
|
|
67
|
+
const modelPart = modelName.includes(separator)
|
|
68
|
+
? modelName.slice(modelName.lastIndexOf(separator) + separator.length)
|
|
69
|
+
: modelName
|
|
70
|
+
for (const provider of providers ?? []) {
|
|
71
|
+
const matched = provider.models.find((item) => item.model_name === modelPart)
|
|
72
|
+
if (matched?.display_name) {
|
|
73
|
+
return matched.display_name
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return fallbackModelDisplayName(modelPart)
|
|
77
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Taro from '@tarojs/taro'
|
|
2
|
+
|
|
3
|
+
/** 用户最近一次选择的供应商与模型,仅保存在设备本地。 */
|
|
4
|
+
export interface AiModelSelection {
|
|
5
|
+
provider_id: number
|
|
6
|
+
model_name: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const MODEL_SELECTION_STORAGE_KEY = 'kratos-taro-app:ai:model-selection'
|
|
10
|
+
|
|
11
|
+
/** 读取本地缓存的模型选择,缓存缺失或损坏时返回 undefined。 */
|
|
12
|
+
export function loadStoredModelSelection(): AiModelSelection | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const raw = Taro.getStorageSync<unknown>(MODEL_SELECTION_STORAGE_KEY)
|
|
15
|
+
if (!raw) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
19
|
+
const value = parsed as Partial<AiModelSelection>
|
|
20
|
+
if (
|
|
21
|
+
typeof value.provider_id === 'number' &&
|
|
22
|
+
typeof value.model_name === 'string' &&
|
|
23
|
+
value.model_name
|
|
24
|
+
) {
|
|
25
|
+
return { provider_id: value.provider_id, model_name: value.model_name }
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// 缓存损坏或存储不可用时忽略,回退到默认选择。
|
|
29
|
+
}
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 持久化模型选择,存储不可用时静默失败,选择仍对当前会话生效。 */
|
|
34
|
+
export function saveModelSelection(selection: AiModelSelection): void {
|
|
35
|
+
try {
|
|
36
|
+
Taro.setStorageSync(MODEL_SELECTION_STORAGE_KEY, JSON.stringify(selection))
|
|
37
|
+
} catch {
|
|
38
|
+
// 忽略存储失败,不影响当前会话内的选择。
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -25,6 +25,13 @@ export default function MessageDetailPage() {
|
|
|
25
25
|
if (!value.read_at) await defNotificationService.MarkNotificationRead({ ids: [id] })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function resolveSenderName(value?: string) {
|
|
29
|
+
if (value === '系统' || value === '__I18N__:system.notification.sender.system') {
|
|
30
|
+
return t('system.notification.sender.system')
|
|
31
|
+
}
|
|
32
|
+
return value || ''
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
/** 执行通知携带的稳定 viewKey 动作。 */
|
|
29
36
|
function openAction() {
|
|
30
37
|
if (!detail || detail.action_type !== MessageActionType.MESSAGE_ACTION_TYPE_VIEW_KEY) return
|
|
@@ -60,7 +67,7 @@ export default function MessageDetailPage() {
|
|
|
60
67
|
/>
|
|
61
68
|
<Text>{detail.category_name}</Text>
|
|
62
69
|
</View>
|
|
63
|
-
<Text>{detail.sender_name}</Text>
|
|
70
|
+
<Text>{resolveSenderName(detail.sender_name)}</Text>
|
|
64
71
|
<Text>{detail.received_at}</Text>
|
|
65
72
|
</View>
|
|
66
73
|
{detail.content_format === MessageContentFormat.MESSAGE_CONTENT_FORMAT_RICH_TEXT ? (
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useUserStore,
|
|
10
10
|
} from '@liujitcn/kratos-taro-app-core'
|
|
11
11
|
import { defMfaService } from '@liujitcn/kratos-taro-app-core/api/base/v1/mfa'
|
|
12
|
+
import { isAuthExpiredError } from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
12
13
|
import PasswordVerifyDialog from './components/PasswordVerifyDialog'
|
|
13
14
|
import './settings.scss'
|
|
14
15
|
|
|
@@ -109,15 +110,6 @@ export default function SettingsPage() {
|
|
|
109
110
|
void loadMfaStatus()
|
|
110
111
|
})
|
|
111
112
|
|
|
112
|
-
/** 登录态失效时请求层已弹窗引导重新登录,页面侧不再叠加失败提示。 */
|
|
113
|
-
const isAuthExpiredError = (error: unknown) => {
|
|
114
|
-
if (error && typeof error === 'object') {
|
|
115
|
-
const statusCode = (error as { statusCode?: number }).statusCode
|
|
116
|
-
if (statusCode === 401 || statusCode === 403) return true
|
|
117
|
-
}
|
|
118
|
-
return error instanceof Error && /auth (required|expired)/.test(error.message)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
113
|
const onLogout = async () => {
|
|
122
114
|
if (logoutLoading) return
|
|
123
115
|
const result = await Taro.showModal({
|