@liujitcn/kratos-admin-system 0.0.61 → 0.0.64

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.
Files changed (97) hide show
  1. package/README.md +16 -1
  2. package/dist/package/declarations/src/api/base/v1/ai_provider.d.ts +7 -0
  3. package/dist/package/declarations/src/api/system/admin/v1/ai_provider.d.ts +18 -0
  4. package/dist/package/declarations/src/api/system/admin/v1/base_api_rate_limit_policy.d.ts +19 -0
  5. package/dist/package/declarations/src/api/system/admin/v1/base_message_provider.d.ts +21 -0
  6. package/dist/package/declarations/src/api/system/admin/v1/base_message_template.d.ts +16 -0
  7. package/dist/package/declarations/src/api/system/admin/v1/base_oauth_provider.d.ts +18 -0
  8. package/dist/package/declarations/src/api/system/admin/v1/base_rate_limit_rule.d.ts +21 -0
  9. package/dist/package/declarations/src/components/api/ApiTransfer.vue.d.ts +35 -0
  10. package/dist/package/declarations/src/components/api/apiTransfer.d.ts +13 -0
  11. package/dist/package/declarations/src/components/i18n/DynamicI18nCell.vue.d.ts +2 -2
  12. package/dist/package/declarations/src/components/i18n/dynamicI18n.d.ts +3 -4
  13. package/dist/package/declarations/src/locales/generated.d.ts +900 -0
  14. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +4 -0
  15. package/dist/package/declarations/src/rpc/base/v1/ai_provider.d.ts +22 -0
  16. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +20 -0
  17. package/dist/package/declarations/src/rpc/google/protobuf/struct.d.ts +81 -0
  18. package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +110 -0
  19. package/dist/package/declarations/src/rpc/system/admin/v1/base_api_rate_limit_policy.d.ts +137 -0
  20. package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +4 -23
  21. package/dist/package/declarations/src/rpc/system/admin/v1/base_message.d.ts +4 -4
  22. package/dist/package/declarations/src/rpc/system/admin/v1/base_message_category.d.ts +19 -0
  23. package/dist/package/declarations/src/rpc/system/admin/v1/base_message_provider.d.ts +126 -0
  24. package/dist/package/declarations/src/rpc/system/admin/v1/base_message_template.d.ts +99 -0
  25. package/dist/package/declarations/src/rpc/system/admin/v1/base_oauth_provider.d.ts +133 -0
  26. package/dist/package/declarations/src/rpc/system/admin/v1/base_rate_limit_rule.d.ts +144 -0
  27. package/dist/package/declarations/src/utils/persisted-message.d.ts +4 -0
  28. package/dist/package/declarations/src/utils/rateLimit.d.ts +24 -0
  29. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +11 -2
  30. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +9 -0
  31. package/dist/package/declarations/src/views/base/ai-provider/index.vue.d.ts +3 -0
  32. package/dist/package/declarations/src/views/base/api-rate-limit-policy/index.vue.d.ts +3 -0
  33. package/dist/package/declarations/src/views/base/message-provider/index.vue.d.ts +3 -0
  34. package/dist/package/declarations/src/views/base/message-template/index.vue.d.ts +3 -0
  35. package/dist/package/declarations/src/views/base/oauth-provider/index.vue.d.ts +3 -0
  36. package/dist/package/declarations/src/views/base/rate-limit-rule/index.vue.d.ts +3 -0
  37. package/dist/package/src/api/base/v1/ai_provider.ts +22 -0
  38. package/dist/package/src/api/system/admin/v1/ai_provider.ts +50 -0
  39. package/dist/package/src/api/system/admin/v1/base_api_rate_limit_policy.ts +51 -0
  40. package/dist/package/src/api/system/admin/v1/base_message_provider.ts +57 -0
  41. package/dist/package/src/api/system/admin/v1/base_message_template.ts +44 -0
  42. package/dist/package/src/api/system/admin/v1/base_oauth_provider.ts +50 -0
  43. package/dist/package/src/api/system/admin/v1/base_rate_limit_rule.ts +58 -0
  44. package/dist/package/src/components/ai/Ai.vue +1 -1
  45. package/dist/package/src/components/api/ApiTransfer.vue +117 -0
  46. package/dist/package/src/components/api/apiTransfer.ts +27 -0
  47. package/dist/package/src/components/i18n/DynamicI18nCell.vue +8 -8
  48. package/dist/package/src/components/i18n/DynamicI18nEditor.vue +6 -3
  49. package/dist/package/src/components/i18n/dynamicI18n.ts +4 -5
  50. package/dist/package/src/components/tenant-project/TenantProjectManager.vue +2 -1
  51. package/dist/package/src/locales/en-US.json +229 -4
  52. package/dist/package/src/locales/ja-JP.json +229 -4
  53. package/dist/package/src/locales/zh-CN.json +229 -4
  54. package/dist/package/src/locales/zh-TW.json +229 -4
  55. package/dist/package/src/rpc/base/v1/ai_message.ts +7 -1
  56. package/dist/package/src/rpc/base/v1/ai_provider.ts +33 -0
  57. package/dist/package/src/rpc/base/v1/oauth.ts +16 -0
  58. package/dist/package/src/rpc/google/protobuf/struct.ts +80 -0
  59. package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +129 -0
  60. package/dist/package/src/rpc/system/admin/v1/base_api_rate_limit_policy.ts +160 -0
  61. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +4 -24
  62. package/dist/package/src/rpc/system/admin/v1/base_message.ts +4 -4
  63. package/dist/package/src/rpc/system/admin/v1/base_message_category.ts +20 -0
  64. package/dist/package/src/rpc/system/admin/v1/base_message_provider.ts +150 -0
  65. package/dist/package/src/rpc/system/admin/v1/base_message_template.ts +117 -0
  66. package/dist/package/src/rpc/system/admin/v1/base_oauth_provider.ts +156 -0
  67. package/dist/package/src/rpc/system/admin/v1/base_rate_limit_rule.ts +167 -0
  68. package/dist/package/src/utils/persisted-message.ts +21 -0
  69. package/dist/package/src/utils/rateLimit.ts +140 -0
  70. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +36 -4
  71. package/dist/package/src/views/ai/chat/components/XSender.vue +112 -1
  72. package/dist/package/src/views/ai/chat/index.vue +30 -1
  73. package/dist/package/src/views/base/ai-provider/index.vue +512 -0
  74. package/dist/package/src/views/base/api-rate-limit-policy/index.vue +428 -0
  75. package/dist/package/src/views/base/backup-management/archive-record/index.vue +2 -0
  76. package/dist/package/src/views/base/backup-management/archive-restore/index.vue +2 -1
  77. package/dist/package/src/views/base/backup-management/backup-record/index.vue +2 -0
  78. package/dist/package/src/views/base/backup-management/backup-restore/index.vue +2 -1
  79. package/dist/package/src/views/base/config/index.vue +12 -13
  80. package/dist/package/src/views/base/dict/index.vue +2 -3
  81. package/dist/package/src/views/base/dict/item.vue +2 -3
  82. package/dist/package/src/views/base/job/index.vue +2 -3
  83. package/dist/package/src/views/base/job/log.vue +3 -2
  84. package/dist/package/src/views/base/menu/index.vue +27 -83
  85. package/dist/package/src/views/base/message/index.vue +9 -3
  86. package/dist/package/src/views/base/message-category/index.vue +34 -3
  87. package/dist/package/src/views/base/message-provider/index.vue +164 -0
  88. package/dist/package/src/views/base/message-template/index.vue +105 -0
  89. package/dist/package/src/views/base/migration/index.vue +1 -1
  90. package/dist/package/src/views/base/oauth-client/index.vue +6 -6
  91. package/dist/package/src/views/base/oauth-provider/index.vue +274 -0
  92. package/dist/package/src/views/base/rate-limit-rule/index.vue +322 -0
  93. package/dist/package/src/views/base/redact-output-policy/index.vue +1 -1
  94. package/dist/package/src/views/base/redact-rule/index.vue +12 -2
  95. package/dist/package/src/views/base/redact-storage-policy/index.vue +4 -0
  96. package/dist/package/src/views/base/tenant/index.vue +3 -3
  97. package/package.json +2 -2
@@ -0,0 +1,140 @@
1
+ import { BaseRateLimitAlgorithm } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_rate_limit_rule";
2
+
3
+ /** 限流算法的可编辑参数集合。 */
4
+ export interface RateLimitParams {
5
+ tokens_per_second?: number;
6
+ burst?: number;
7
+ limit?: number;
8
+ window_seconds?: number;
9
+ leak_rate_per_second?: number;
10
+ capacity?: number;
11
+ }
12
+
13
+ /** 返回所选算法的空参数表单。 */
14
+ export function defaultRateLimitParams(algorithm: BaseRateLimitAlgorithm): RateLimitParams {
15
+ switch (algorithm) {
16
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_TOKEN_BUCKET:
17
+ return { tokens_per_second: undefined, burst: undefined };
18
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_FIXED_WINDOW:
19
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_COUNTER:
20
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_LOG:
21
+ return { limit: undefined, window_seconds: undefined };
22
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_LEAKY_BUCKET:
23
+ return { leak_rate_per_second: undefined, capacity: undefined };
24
+ default:
25
+ return {};
26
+ }
27
+ }
28
+
29
+ /** 按算法解析 JSON,只保留该算法支持的参数。 */
30
+ export function parseRateLimitParams(algorithm: BaseRateLimitAlgorithm, raw: string): RateLimitParams {
31
+ let value: Record<string, unknown> = {};
32
+ try {
33
+ const parsed: unknown = JSON.parse(raw);
34
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) value = parsed as Record<string, unknown>;
35
+ } catch {
36
+ return defaultRateLimitParams(algorithm);
37
+ }
38
+ const number = (key: string) => (typeof value[key] === "number" && Number.isFinite(value[key]) ? (value[key] as number) : undefined);
39
+ switch (algorithm) {
40
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_TOKEN_BUCKET:
41
+ return { tokens_per_second: number("tokens_per_second"), burst: number("burst") };
42
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_FIXED_WINDOW:
43
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_COUNTER:
44
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_LOG:
45
+ return { limit: number("limit"), window_seconds: number("window_seconds") };
46
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_LEAKY_BUCKET:
47
+ return { leak_rate_per_second: number("leak_rate_per_second"), capacity: number("capacity") };
48
+ default:
49
+ return defaultRateLimitParams(algorithm);
50
+ }
51
+ }
52
+
53
+ /** 校验参数并生成仅包含当前算法字段的 JSON 对象。 */
54
+ export function serializeRateLimitParams(algorithm: BaseRateLimitAlgorithm, params: RateLimitParams): Record<string, number> | undefined {
55
+ switch (algorithm) {
56
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_TOKEN_BUCKET:
57
+ if (!isRate(params.tokens_per_second) || !isIntegerInRange(params.burst, 1, 1_000_000)) return undefined;
58
+ return { tokens_per_second: params.tokens_per_second, burst: params.burst };
59
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_FIXED_WINDOW:
60
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_COUNTER:
61
+ if (!isIntegerInRange(params.limit, 1, 1_000_000) || !isIntegerInRange(params.window_seconds, 1, 86_400)) return undefined;
62
+ return { limit: params.limit, window_seconds: params.window_seconds };
63
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_LOG:
64
+ if (!isIntegerInRange(params.limit, 1, 10_000) || !isIntegerInRange(params.window_seconds, 1, 86_400)) return undefined;
65
+ return { limit: params.limit, window_seconds: params.window_seconds };
66
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_LEAKY_BUCKET:
67
+ if (!isRate(params.leak_rate_per_second) || !isIntegerInRange(params.capacity, 1, 1_000_000)) return undefined;
68
+ return { leak_rate_per_second: params.leak_rate_per_second, capacity: params.capacity };
69
+ default:
70
+ return undefined;
71
+ }
72
+ }
73
+
74
+ /** 返回可直接交给国际化模板的完整参数。 */
75
+ export function rateLimitSummaryParams(algorithm: BaseRateLimitAlgorithm, raw: string): Record<string, string | number> {
76
+ const params = parseRateLimitParams(algorithm, raw);
77
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, value ?? 0])) as Record<string, string | number>;
78
+ }
79
+
80
+ /** 返回限流算法名称对应的多语言资源键。 */
81
+ export function rateLimitAlgorithmLabelKey(algorithm: BaseRateLimitAlgorithm): string {
82
+ switch (algorithm) {
83
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_TOKEN_BUCKET:
84
+ return "system.base.rate_limit_rule.algorithm.token_bucket";
85
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_FIXED_WINDOW:
86
+ return "system.base.rate_limit_rule.algorithm.fixed_window";
87
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_COUNTER:
88
+ return "system.base.rate_limit_rule.algorithm.sliding_window_counter";
89
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_LOG:
90
+ return "system.base.rate_limit_rule.algorithm.sliding_window_log";
91
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_LEAKY_BUCKET:
92
+ return "system.base.rate_limit_rule.algorithm.leaky_bucket";
93
+ default:
94
+ return "";
95
+ }
96
+ }
97
+
98
+ /** 返回所选限流算法说明对应的多语言资源键。 */
99
+ export function rateLimitAlgorithmTooltipKey(algorithm: BaseRateLimitAlgorithm): string {
100
+ switch (algorithm) {
101
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_TOKEN_BUCKET:
102
+ return "system.base.rate_limit_rule.algorithm_help.token_bucket";
103
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_FIXED_WINDOW:
104
+ return "system.base.rate_limit_rule.algorithm_help.fixed_window";
105
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_COUNTER:
106
+ return "system.base.rate_limit_rule.algorithm_help.sliding_window_counter";
107
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_LOG:
108
+ return "system.base.rate_limit_rule.algorithm_help.sliding_window_log";
109
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_LEAKY_BUCKET:
110
+ return "system.base.rate_limit_rule.algorithm_help.leaky_bucket";
111
+ default:
112
+ return "";
113
+ }
114
+ }
115
+
116
+ /** 返回参数摘要对应的多语言资源键。 */
117
+ export function rateLimitSummaryKey(algorithm: BaseRateLimitAlgorithm): string {
118
+ switch (algorithm) {
119
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_TOKEN_BUCKET:
120
+ return "system.base.rate_limit_rule.summary.token_bucket";
121
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_FIXED_WINDOW:
122
+ return "system.base.rate_limit_rule.summary.fixed_window";
123
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_COUNTER:
124
+ return "system.base.rate_limit_rule.summary.sliding_window_counter";
125
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_SLIDING_WINDOW_LOG:
126
+ return "system.base.rate_limit_rule.summary.sliding_window_log";
127
+ case BaseRateLimitAlgorithm.BASE_RATE_LIMIT_ALGORITHM_LEAKY_BUCKET:
128
+ return "system.base.rate_limit_rule.summary.leaky_bucket";
129
+ default:
130
+ return "";
131
+ }
132
+ }
133
+
134
+ function isRate(value: number | undefined): value is number {
135
+ return typeof value === "number" && Number.isFinite(value) && value >= 0.001 && value <= 1_000_000;
136
+ }
137
+
138
+ function isIntegerInRange(value: number | undefined, min: number, max: number): value is number {
139
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
140
+ }
@@ -47,7 +47,15 @@
47
47
  </div>
48
48
  </section>
49
49
  <div class="agent-chat-empty__sender">
50
- <XSender :key="senderKey" :sending="sending" @submit="handleSubmit" />
50
+ <XSender
51
+ :key="senderKey"
52
+ :sending="sending"
53
+ :model-providers="modelProviders"
54
+ :provider-id="providerId"
55
+ :model-name="modelName"
56
+ @submit="handleSubmit"
57
+ @model-change="handleModelChange"
58
+ />
51
59
  </div>
52
60
  </div>
53
61
  </template>
@@ -138,7 +146,7 @@
138
146
  <div class="agent-message-error__content">{{ resolveAIErrorMessage(item) }}</div>
139
147
  <el-collapse v-if="item.fallback_reason" class="agent-message-error__detail" accordion>
140
148
  <el-collapse-item :title="t('system.ai.chat.title.error_detail')" :name="String(item.id)">
141
- <pre>{{ item.fallback_reason }}</pre>
149
+ <pre>{{ resolvePersistedMessage(item.fallback_reason) }}</pre>
142
150
  </el-collapse-item>
143
151
  </el-collapse>
144
152
  </div>
@@ -147,7 +155,7 @@
147
155
  <slot name="flow-blocks" :message="item" :active-flow-message-id="activeFlowMessageID" />
148
156
  <el-collapse v-if="item.fallback_reason" class="agent-message-error__detail" accordion>
149
157
  <el-collapse-item :title="t('system.ai.chat.title.error_detail')" :name="String(item.id)">
150
- <pre>{{ item.fallback_reason }}</pre>
158
+ <pre>{{ resolvePersistedMessage(item.fallback_reason) }}</pre>
151
159
  </el-collapse-item>
152
160
  </el-collapse>
153
161
  </template>
@@ -320,7 +328,15 @@
320
328
  </button>
321
329
  </div>
322
330
  </section>
323
- <XSender :key="senderKey" :sending="sending" @submit="handleSubmit" />
331
+ <XSender
332
+ :key="senderKey"
333
+ :sending="sending"
334
+ :model-providers="modelProviders"
335
+ :provider-id="providerId"
336
+ :model-name="modelName"
337
+ @submit="handleSubmit"
338
+ @model-change="handleModelChange"
339
+ />
324
340
  </div>
325
341
  </template>
326
342
  </main>
@@ -337,8 +353,10 @@ import { getCurrentLocale, t } from "@liujitcn/kratos-admin-core";
337
353
  import { formatDateTime } from "@liujitcn/kratos-admin-core/format";
338
354
  import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
339
355
  import { type AiAttachment, type AiSession } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
356
+ import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
340
357
  import type { AiShortcut, AiToolCall } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
341
358
  import { AiMessageStatus } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
359
+ import { resolvePersistedMessage } from "../../../../utils/persisted-message";
342
360
  import XSender from "./XSender.vue";
343
361
 
344
362
  // AI Markdown 渲染器依赖较重,仅在真正出现助手消息时再加载。
@@ -474,11 +492,19 @@ const props = defineProps<{
474
492
  loadingShortcuts?: boolean;
475
493
  /** 当前终端可用快捷入口。 */
476
494
  shortcuts?: AiShortcut[];
495
+ /** 当前启用的供应商和模型。 */
496
+ modelProviders: AiProviderModelOption[];
497
+ /** 当前选择的供应商编号。 */
498
+ providerId: number;
499
+ /** 当前选择的模型名称。 */
500
+ modelName: string;
477
501
  }>();
478
502
 
479
503
  const emit = defineEmits<{
480
504
  /** 提交输入框内容。 */
481
505
  submit: [payload: SubmitPayload];
506
+ /** 更新当前选择的供应商和模型。 */
507
+ modelChange: [providerId: number, modelName: string];
482
508
  /** 触发消息级操作。 */
483
509
  messageAction: [payload: { action: ChatMessageAction; item: ChatMessageItem }];
484
510
  /** 提交当前用户消息的文本编辑。 */
@@ -614,6 +640,11 @@ function handleSubmit(payload: SubmitPayload) {
614
640
  emit("submit", payload);
615
641
  }
616
642
 
643
+ /** 转发输入器发出的供应商和模型变更。 */
644
+ function handleModelChange(providerId: number, modelName: string) {
645
+ emit("modelChange", providerId, modelName);
646
+ }
647
+
617
648
  /** 点击快捷入口时提交对应入口动作。 */
618
649
  function handleShortcutClick(shortcut: AiShortcut) {
619
650
  emit("submit", {
@@ -792,6 +823,7 @@ function resolveVisibleTools(item: ChatMessageItem) {
792
823
 
793
824
  /** 生成工具展示名称。 */
794
825
  function resolveToolTitle(tool: AiToolCall) {
826
+ if (tool.name === "web_search") return t("system.ai.chat.value.web_search");
795
827
  return tool.title || tool.name || t("system.ai.chat.value.tool");
796
828
  }
797
829
 
@@ -47,13 +47,28 @@
47
47
  </button>
48
48
  </div>
49
49
  </el-popover>
50
-
51
50
  <span class="agent-action-text">{{ actionHintText }}</span>
52
51
  </div>
53
52
  </template>
54
53
 
55
54
  <template #action-list>
56
55
  <div class="agent-sender-actions">
56
+ <div class="agent-model-selector" :class="{ 'is-disabled': sending || !modelOptions.length }">
57
+ <el-cascader
58
+ :model-value="selectedModelPath"
59
+ :options="modelOptions"
60
+ :props="{ expandTrigger: 'click' }"
61
+ :placeholder="t('system.ai.model.model_placeholder')"
62
+ :disabled="sending || !modelOptions.length"
63
+ :show-all-levels="true"
64
+ separator="/"
65
+ :clearable="true"
66
+ :teleported="true"
67
+ popper-class="agent-model-cascader-popper"
68
+ size="small"
69
+ @change="handleModelPathChange"
70
+ />
71
+ </div>
57
72
  <el-tooltip
58
73
  :content="recording ? t('system.ai.chat.action.stop_voice_input') : t('system.ai.chat.action.voice_input')"
59
74
  placement="top"
@@ -106,9 +121,11 @@ import { Attachments, XSender as BaseXSender } from "vue-element-plus-x";
106
121
  import type { FilesCardProps } from "vue-element-plus-x/types/FilesCard";
107
122
  import { Loading, Microphone, Paperclip, Promotion } from "@element-plus/icons-vue";
108
123
  import { ElMessage } from "element-plus";
124
+ import type { CascaderValue } from "element-plus";
109
125
  import { t } from "@liujitcn/kratos-admin-core";
110
126
  import { defFileService } from "@liujitcn/kratos-admin-core/api/base/v1/file";
111
127
  import type { AiAttachment } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
128
+ import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
112
129
  import type { SubmitPayload } from "../types";
113
130
  import { buildAIAttachmentFileCard } from "../attachment";
114
131
  import { useSpeechRecognition } from "./speech-recognition";
@@ -117,17 +134,34 @@ import type { SpeechRecognitionError } from "./speech-recognition";
117
134
  const props = defineProps<{
118
135
  /** 消息发送加载状态。 */
119
136
  sending: boolean;
137
+ /** 当前可供选择的供应商与模型。 */
138
+ modelProviders: AiProviderModelOption[];
139
+ /** 当前选择的供应商编号。 */
140
+ providerId: number;
141
+ /** 当前选择的模型名称。 */
142
+ modelName: string;
120
143
  }>();
121
144
 
122
145
  const emit = defineEmits<{
123
146
  /** 提交输入器内容。 */
124
147
  submit: [payload: SubmitPayload];
148
+ /** 更新当前选择的供应商和模型。 */
149
+ "model-change": [providerId: number, modelName: string];
125
150
  }>();
126
151
 
127
152
  const senderRef = ref<InstanceType<typeof BaseXSender>>();
128
153
  const fileInputRef = ref<HTMLInputElement>();
129
154
  const inputText = ref("");
130
155
  const selectedAttachments = ref<AiAttachment[]>([]);
156
+ const modelOptions = computed(() =>
157
+ props.modelProviders.map(provider => ({
158
+ value: provider.provider_id,
159
+ label: provider.name,
160
+ disabled: provider.models.length === 0,
161
+ children: provider.models.map(model => ({ value: model, label: model }))
162
+ }))
163
+ );
164
+ const selectedModelPath = computed(() => (props.providerId && props.modelName ? [props.providerId, props.modelName] : []));
131
165
  const uploading = ref(false);
132
166
  const maxAttachmentCount = 6;
133
167
  const maxAttachmentSizeMB = 20;
@@ -184,6 +218,12 @@ const isSubmitDisabled = computed(() => {
184
218
  return uploading.value || (!inputText.value.trim() && selectedAttachments.value.length === 0);
185
219
  });
186
220
 
221
+ /** 更新当前选中的供应商和模型路径。 */
222
+ function handleModelPathChange(value: CascaderValue | null | undefined) {
223
+ const path = Array.isArray(value) ? value : [];
224
+ emit("model-change", Number(path[0]) || 0, typeof path[1] === "string" ? path[1] : "");
225
+ }
226
+
187
227
  /** 读取输入内容并发送给父组件。 */
188
228
  function handleSubmit() {
189
229
  if (uploading.value) return;
@@ -392,6 +432,13 @@ function resetFileInput() {
392
432
  align-items: center;
393
433
  justify-content: space-between;
394
434
  }
435
+ :deep(.elx-x-sender__updown-action-list .elx-x-sender__prefix) {
436
+ flex: 1 1 auto;
437
+ width: 100%;
438
+ min-width: 0;
439
+ height: auto;
440
+ padding-left: 0;
441
+ }
395
442
  :deep(.elx-x-sender__action-list) {
396
443
  height: auto;
397
444
  }
@@ -410,10 +457,71 @@ function resetFileInput() {
410
457
  }
411
458
  .agent-prefix-actions {
412
459
  display: flex;
460
+ flex-wrap: wrap;
413
461
  gap: 10px;
414
462
  align-items: center;
463
+ width: 100%;
415
464
  min-height: 36px;
416
465
  }
466
+ .agent-model-selector {
467
+ display: flex;
468
+ flex: 0 1 auto;
469
+ width: clamp(180px, 18vw, 200px);
470
+ min-width: 0;
471
+ height: 34px;
472
+ box-sizing: border-box;
473
+ color: var(--admin-page-text-secondary);
474
+ cursor: pointer;
475
+ background: var(--admin-page-card-bg);
476
+ border: 1px solid var(--el-border-color);
477
+ border-radius: var(--admin-page-radius);
478
+ transition:
479
+ color 0.2s ease,
480
+ border-color 0.2s ease,
481
+ background-color 0.2s ease;
482
+ }
483
+ .agent-model-selector:hover {
484
+ color: var(--el-color-primary);
485
+ background: var(--el-color-primary-light-9);
486
+ border-color: var(--el-color-primary-light-5);
487
+ }
488
+ .agent-model-selector:focus-within {
489
+ border-color: var(--el-color-primary-light-5);
490
+ }
491
+ .agent-model-selector.is-disabled {
492
+ color: var(--el-disabled-text-color);
493
+ cursor: not-allowed;
494
+ background: var(--el-disabled-bg-color);
495
+ border-color: var(--el-disabled-border-color);
496
+ }
497
+ .agent-model-selector :deep(.el-cascader) {
498
+ width: 100%;
499
+ height: 100%;
500
+ min-width: 0;
501
+ }
502
+ .agent-model-selector :deep(.el-input) {
503
+ height: 100%;
504
+ }
505
+ .agent-model-selector :deep(.el-input__wrapper) {
506
+ height: 100%;
507
+ padding: 0 9px;
508
+ background: transparent;
509
+ box-shadow: none;
510
+ border-radius: inherit;
511
+ }
512
+ .agent-model-selector :deep(.el-input__wrapper.is-focus) {
513
+ box-shadow: none;
514
+ }
515
+ .agent-model-selector :deep(.el-input__inner) {
516
+ font-size: 14px;
517
+ font-weight: 500;
518
+ line-height: 20px;
519
+ color: currentColor;
520
+ }
521
+ .agent-model-selector :deep(.el-input__suffix .el-icon) {
522
+ font-size: 14px;
523
+ color: currentColor;
524
+ }
417
525
  .agent-sender-actions {
418
526
  display: inline-flex;
419
527
  gap: 8px;
@@ -509,6 +617,9 @@ function resetFileInput() {
509
617
  .agent-prefix-actions {
510
618
  gap: 8px;
511
619
  }
620
+ .agent-model-selector {
621
+ width: clamp(128px, calc(100vw - 220px), 180px);
622
+ }
512
623
  .agent-action-text {
513
624
  display: none;
514
625
  }
@@ -27,7 +27,11 @@
27
27
  :sending="currentSessionSending"
28
28
  :shortcuts="starterShortcuts"
29
29
  :loading-shortcuts="loadingShortcuts"
30
+ :model-providers="modelProviders"
31
+ :provider-id="selectedProviderId"
32
+ :model-name="selectedModelName"
30
33
  @submit="handleSubmit"
34
+ @model-change="handleModelChange"
31
35
  @message-action="handleMessageAction"
32
36
  @message-edit="handleEditMessage"
33
37
  >
@@ -54,8 +58,10 @@ import { getAdminAiExtension } from "@liujitcn/kratos-admin-system";
54
58
  import { defAiMessageService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_message";
55
59
  import { defAiSessionService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_session";
56
60
  import { defAiToolService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_tool";
61
+ import { defAiModelService } from "@liujitcn/kratos-admin-system/api/base/v1/ai_provider";
57
62
  import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
58
63
  import type { AiSession } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
64
+ import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
59
65
  import type { AiShortcut } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
60
66
  import { AiMessageStatus } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
61
67
  import { Terminal } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
@@ -102,6 +108,9 @@ const loadingSessionID = ref("");
102
108
  const loadingShortcuts = ref(false);
103
109
  const sessions = ref<AiSession[]>([]);
104
110
  const starterShortcuts = ref<AiShortcut[]>([]);
111
+ const modelProviders = ref<AiProviderModelOption[]>([]);
112
+ const selectedProviderId = ref(0);
113
+ const selectedModelName = ref("");
105
114
  const messages = ref<Record<string, ChatMessageItem[]>>({});
106
115
  const pendingDeltaMap = new Map<string, AiStreamPayload>();
107
116
  const runningStreamTaskMap = new Map<string, AiStreamTask>();
@@ -217,6 +226,8 @@ async function runAiStreamTask(sessionID: string, payload: SubmitPayload) {
217
226
  {
218
227
  session_id: sessionID,
219
228
  content: payload.text,
229
+ provider_id: selectedProviderId.value,
230
+ model_name: selectedModelName.value,
220
231
  attachments: payload.attachments.map(item => ({
221
232
  id: item.id,
222
233
  name: item.name,
@@ -645,15 +656,33 @@ async function loadAiShortcuts() {
645
656
  if (loadingShortcuts.value) return;
646
657
  loadingShortcuts.value = true;
647
658
  try {
648
- const response = await defAiToolService.ListAiShortcut({ terminal: Terminal.TERMINAL_ADMIN });
659
+ const [response, providerResponse] = await Promise.all([
660
+ defAiToolService.ListAiShortcut({ terminal: Terminal.TERMINAL_ADMIN }),
661
+ defAiModelService.ListAiProviderModelOptions({})
662
+ ]);
649
663
  starterShortcuts.value = normalizeStarterShortcuts(response.shortcuts);
664
+ modelProviders.value = providerResponse.providers ?? [];
665
+ if (!modelProviders.value.some(item => item.provider_id === selectedProviderId.value && item.models.includes(selectedModelName.value))) {
666
+ const firstProvider = modelProviders.value[0];
667
+ selectedProviderId.value = firstProvider?.provider_id ?? 0;
668
+ selectedModelName.value = firstProvider?.models[0] ?? "";
669
+ }
650
670
  } catch {
651
671
  starterShortcuts.value = [];
672
+ modelProviders.value = [];
673
+ selectedProviderId.value = 0;
674
+ selectedModelName.value = "";
652
675
  } finally {
653
676
  loadingShortcuts.value = false;
654
677
  }
655
678
  }
656
679
 
680
+ /** 更新当前聊天使用的供应商与模型。 */
681
+ function handleModelChange(providerId: number, modelName: string) {
682
+ selectedProviderId.value = providerId;
683
+ selectedModelName.value = modelName;
684
+ }
685
+
657
686
  /** 使指定会话仍在途的消息列表请求失效。 */
658
687
  function invalidateLoadMessages(sessionID: string) {
659
688
  loadMessagesSeq.set(sessionID, (loadMessagesSeq.get(sessionID) ?? 0) + 1);