@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 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.62",
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.62",
42
- "@liujitcn/kratos-taro-app-ui": "^0.0.62"
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 Error(t('core.auth.session_expired'))
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 Error(t('core.auth.session_expired')))
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()
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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: AiAction | undefined;
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
@@ -278,6 +278,8 @@ export interface DeleteNotificationRequest {
278
278
  export interface Notification {
279
279
  /** 投递ID */
280
280
  id: number;
281
+ /** 租户ID */
282
+ tenant_id: number;
281
283
  /** 消息ID */
282
284
  message_id: number;
283
285
  /** 分类ID */
@@ -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 defAiToolService.ListAiShortcut({ terminal: AI_TERMINAL })
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 = { session_id: sessionID, content: payload.text, attachments: payload.attachments, action: undefined }
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
- id: streamKey || `ai-thinking-${now}`,
873
- input_content: undefined,
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({