@liujitcn/kratos-admin-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.
Files changed (101) 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 +20 -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 +968 -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 +29 -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 +133 -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/ai/chat/modelDisplay.d.ts +5 -0
  32. package/dist/package/declarations/src/views/ai/chat/modelSelection.d.ts +9 -0
  33. package/dist/package/declarations/src/views/base/ai-provider/index.vue.d.ts +3 -0
  34. package/dist/package/declarations/src/views/base/api-rate-limit-policy/index.vue.d.ts +3 -0
  35. package/dist/package/declarations/src/views/base/message-provider/index.vue.d.ts +3 -0
  36. package/dist/package/declarations/src/views/base/message-template/index.vue.d.ts +3 -0
  37. package/dist/package/declarations/src/views/base/oauth-provider/index.vue.d.ts +3 -0
  38. package/dist/package/declarations/src/views/base/rate-limit-rule/index.vue.d.ts +3 -0
  39. package/dist/package/src/api/base/v1/ai_provider.ts +22 -0
  40. package/dist/package/src/api/system/admin/v1/ai_provider.ts +57 -0
  41. package/dist/package/src/api/system/admin/v1/base_api_rate_limit_policy.ts +51 -0
  42. package/dist/package/src/api/system/admin/v1/base_message_provider.ts +57 -0
  43. package/dist/package/src/api/system/admin/v1/base_message_template.ts +44 -0
  44. package/dist/package/src/api/system/admin/v1/base_oauth_provider.ts +50 -0
  45. package/dist/package/src/api/system/admin/v1/base_rate_limit_rule.ts +58 -0
  46. package/dist/package/src/components/ai/Ai.vue +1 -1
  47. package/dist/package/src/components/api/ApiTransfer.vue +117 -0
  48. package/dist/package/src/components/api/apiTransfer.ts +27 -0
  49. package/dist/package/src/components/i18n/DynamicI18nCell.vue +8 -8
  50. package/dist/package/src/components/i18n/DynamicI18nEditor.vue +6 -3
  51. package/dist/package/src/components/i18n/dynamicI18n.ts +4 -5
  52. package/dist/package/src/components/tenant-project/TenantProjectManager.vue +2 -1
  53. package/dist/package/src/locales/en-US.json +246 -4
  54. package/dist/package/src/locales/ja-JP.json +246 -4
  55. package/dist/package/src/locales/zh-CN.json +246 -4
  56. package/dist/package/src/locales/zh-TW.json +246 -4
  57. package/dist/package/src/rpc/base/v1/ai_message.ts +7 -1
  58. package/dist/package/src/rpc/base/v1/ai_provider.ts +41 -0
  59. package/dist/package/src/rpc/base/v1/oauth.ts +16 -0
  60. package/dist/package/src/rpc/google/protobuf/struct.ts +80 -0
  61. package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +155 -0
  62. package/dist/package/src/rpc/system/admin/v1/base_api_rate_limit_policy.ts +160 -0
  63. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +4 -24
  64. package/dist/package/src/rpc/system/admin/v1/base_message.ts +4 -4
  65. package/dist/package/src/rpc/system/admin/v1/base_message_category.ts +20 -0
  66. package/dist/package/src/rpc/system/admin/v1/base_message_provider.ts +150 -0
  67. package/dist/package/src/rpc/system/admin/v1/base_message_template.ts +117 -0
  68. package/dist/package/src/rpc/system/admin/v1/base_oauth_provider.ts +156 -0
  69. package/dist/package/src/rpc/system/admin/v1/base_rate_limit_rule.ts +167 -0
  70. package/dist/package/src/utils/persisted-message.ts +21 -0
  71. package/dist/package/src/utils/rateLimit.ts +140 -0
  72. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +40 -5
  73. package/dist/package/src/views/ai/chat/components/XSender.vue +309 -3
  74. package/dist/package/src/views/ai/chat/index.vue +48 -1
  75. package/dist/package/src/views/ai/chat/modelDisplay.ts +67 -0
  76. package/dist/package/src/views/ai/chat/modelSelection.ts +31 -0
  77. package/dist/package/src/views/base/ai-provider/index.vue +613 -0
  78. package/dist/package/src/views/base/api-rate-limit-policy/index.vue +429 -0
  79. package/dist/package/src/views/base/backup-management/archive-record/index.vue +2 -0
  80. package/dist/package/src/views/base/backup-management/archive-restore/index.vue +2 -1
  81. package/dist/package/src/views/base/backup-management/backup-record/index.vue +2 -0
  82. package/dist/package/src/views/base/backup-management/backup-restore/index.vue +2 -1
  83. package/dist/package/src/views/base/config/index.vue +12 -13
  84. package/dist/package/src/views/base/dict/index.vue +2 -3
  85. package/dist/package/src/views/base/dict/item.vue +2 -3
  86. package/dist/package/src/views/base/job/index.vue +2 -3
  87. package/dist/package/src/views/base/job/log.vue +3 -2
  88. package/dist/package/src/views/base/menu/index.vue +27 -83
  89. package/dist/package/src/views/base/message/index.vue +9 -3
  90. package/dist/package/src/views/base/message-category/index.vue +34 -3
  91. package/dist/package/src/views/base/message-provider/index.vue +168 -0
  92. package/dist/package/src/views/base/message-template/index.vue +105 -0
  93. package/dist/package/src/views/base/migration/index.vue +1 -1
  94. package/dist/package/src/views/base/oauth-client/index.vue +6 -6
  95. package/dist/package/src/views/base/oauth-provider/index.vue +274 -0
  96. package/dist/package/src/views/base/rate-limit-rule/index.vue +327 -0
  97. package/dist/package/src/views/base/redact-output-policy/index.vue +1 -1
  98. package/dist/package/src/views/base/redact-rule/index.vue +12 -2
  99. package/dist/package/src/views/base/redact-storage-policy/index.vue +4 -0
  100. package/dist/package/src/views/base/tenant/index.vue +3 -3
  101. 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>
@@ -70,7 +78,9 @@
70
78
  >
71
79
  {{ item.replySourceTag.text }}
72
80
  </span>
73
- <span v-if="item.model" class="agent-message-meta__model">{{ item.model }}</span>
81
+ <span v-if="item.model" class="agent-message-meta__model">{{
82
+ resolveModelDisplayName(modelProviders, item.model)
83
+ }}</span>
74
84
  <span v-if="resolveMessageReplyTime(item)" class="agent-message-meta__time">{{
75
85
  resolveMessageReplyTime(item)
76
86
  }}</span>
@@ -138,7 +148,7 @@
138
148
  <div class="agent-message-error__content">{{ resolveAIErrorMessage(item) }}</div>
139
149
  <el-collapse v-if="item.fallback_reason" class="agent-message-error__detail" accordion>
140
150
  <el-collapse-item :title="t('system.ai.chat.title.error_detail')" :name="String(item.id)">
141
- <pre>{{ item.fallback_reason }}</pre>
151
+ <pre>{{ resolvePersistedMessage(item.fallback_reason) }}</pre>
142
152
  </el-collapse-item>
143
153
  </el-collapse>
144
154
  </div>
@@ -147,7 +157,7 @@
147
157
  <slot name="flow-blocks" :message="item" :active-flow-message-id="activeFlowMessageID" />
148
158
  <el-collapse v-if="item.fallback_reason" class="agent-message-error__detail" accordion>
149
159
  <el-collapse-item :title="t('system.ai.chat.title.error_detail')" :name="String(item.id)">
150
- <pre>{{ item.fallback_reason }}</pre>
160
+ <pre>{{ resolvePersistedMessage(item.fallback_reason) }}</pre>
151
161
  </el-collapse-item>
152
162
  </el-collapse>
153
163
  </template>
@@ -320,7 +330,15 @@
320
330
  </button>
321
331
  </div>
322
332
  </section>
323
- <XSender :key="senderKey" :sending="sending" @submit="handleSubmit" />
333
+ <XSender
334
+ :key="senderKey"
335
+ :sending="sending"
336
+ :model-providers="modelProviders"
337
+ :provider-id="providerId"
338
+ :model-name="modelName"
339
+ @submit="handleSubmit"
340
+ @model-change="handleModelChange"
341
+ />
324
342
  </div>
325
343
  </template>
326
344
  </main>
@@ -337,8 +355,11 @@ import { getCurrentLocale, t } from "@liujitcn/kratos-admin-core";
337
355
  import { formatDateTime } from "@liujitcn/kratos-admin-core/format";
338
356
  import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
339
357
  import { type AiAttachment, type AiSession } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
358
+ import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
340
359
  import type { AiShortcut, AiToolCall } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
341
360
  import { AiMessageStatus } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
361
+ import { resolvePersistedMessage } from "../../../../utils/persisted-message";
362
+ import { resolveModelDisplayName } from "../modelDisplay";
342
363
  import XSender from "./XSender.vue";
343
364
 
344
365
  // AI Markdown 渲染器依赖较重,仅在真正出现助手消息时再加载。
@@ -474,11 +495,19 @@ const props = defineProps<{
474
495
  loadingShortcuts?: boolean;
475
496
  /** 当前终端可用快捷入口。 */
476
497
  shortcuts?: AiShortcut[];
498
+ /** 当前启用的供应商和模型。 */
499
+ modelProviders: AiProviderModelOption[];
500
+ /** 当前选择的供应商编号。 */
501
+ providerId: number;
502
+ /** 当前选择的模型名称。 */
503
+ modelName: string;
477
504
  }>();
478
505
 
479
506
  const emit = defineEmits<{
480
507
  /** 提交输入框内容。 */
481
508
  submit: [payload: SubmitPayload];
509
+ /** 更新当前选择的供应商和模型。 */
510
+ modelChange: [providerId: number, modelName: string];
482
511
  /** 触发消息级操作。 */
483
512
  messageAction: [payload: { action: ChatMessageAction; item: ChatMessageItem }];
484
513
  /** 提交当前用户消息的文本编辑。 */
@@ -614,6 +643,11 @@ function handleSubmit(payload: SubmitPayload) {
614
643
  emit("submit", payload);
615
644
  }
616
645
 
646
+ /** 转发输入器发出的供应商和模型变更。 */
647
+ function handleModelChange(providerId: number, modelName: string) {
648
+ emit("modelChange", providerId, modelName);
649
+ }
650
+
617
651
  /** 点击快捷入口时提交对应入口动作。 */
618
652
  function handleShortcutClick(shortcut: AiShortcut) {
619
653
  emit("submit", {
@@ -792,6 +826,7 @@ function resolveVisibleTools(item: ChatMessageItem) {
792
826
 
793
827
  /** 生成工具展示名称。 */
794
828
  function resolveToolTitle(tool: AiToolCall) {
829
+ if (tool.name === "web_search") return t("system.ai.chat.value.web_search");
795
830
  return tool.title || tool.name || t("system.ai.chat.value.tool");
796
831
  }
797
832
 
@@ -47,13 +47,85 @@
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
+ <el-popover
57
+ ref="modelPopoverRef"
58
+ placement="top-start"
59
+ :width="324"
60
+ trigger="click"
61
+ :show-arrow="false"
62
+ :persistent="false"
63
+ popper-class="agent-model-popper"
64
+ >
65
+ <template #reference>
66
+ <button
67
+ class="agent-model-selector"
68
+ :class="{ 'is-disabled': sending || !modelOptions.length }"
69
+ type="button"
70
+ :disabled="sending || !modelOptions.length"
71
+ :aria-label="t('system.ai.model.title')"
72
+ >
73
+ <el-icon class="agent-model-selector__spark"><MagicStick /></el-icon>
74
+ <span class="agent-model-selector__label">{{
75
+ selectedModelDisplayName || t("system.ai.model.model_placeholder")
76
+ }}</span>
77
+ <el-icon class="agent-model-selector__arrow"><ArrowDown /></el-icon>
78
+ </button>
79
+ </template>
80
+ <div v-if="modelOptions.length" class="agent-model-panel">
81
+ <div class="agent-model-panel__head">
82
+ <span class="agent-model-panel__title">{{ t("system.ai.model.title") }}</span>
83
+ <span class="agent-model-panel__meta">{{
84
+ t("system.ai.model.provider_model_count", {
85
+ providers: modelOptions.length,
86
+ models: modelOptionCount
87
+ })
88
+ }}</span>
89
+ </div>
90
+ <div class="agent-model-panel__list">
91
+ <template v-for="provider in modelOptions" :key="provider.providerId">
92
+ <div class="agent-model-panel__group">{{ provider.label }}</div>
93
+ <button
94
+ v-for="model in provider.models"
95
+ :key="model.name"
96
+ class="agent-model-row"
97
+ :class="{ 'is-selected': isModelSelected(provider.providerId, model.name) }"
98
+ type="button"
99
+ @click="handleModelSelect(provider.providerId, model.name)"
100
+ >
101
+ <span class="agent-model-row__name">{{ model.displayName }}</span>
102
+ <span v-if="model.displayName !== model.name" class="agent-model-row__raw">{{
103
+ model.name
104
+ }}</span>
105
+ <el-icon
106
+ v-if="isModelSelected(provider.providerId, model.name)"
107
+ class="agent-model-row__check"
108
+ >
109
+ <Check />
110
+ </el-icon>
111
+ </button>
112
+ </template>
113
+ </div>
114
+ <button
115
+ v-if="aiProviderRoute"
116
+ class="agent-model-panel__manage"
117
+ type="button"
118
+ @click="openModelManage"
119
+ >
120
+ <el-icon><Setting /></el-icon>
121
+ <span>{{ t("system.ai.model.manage") }}</span>
122
+ <el-icon class="agent-model-panel__manage-arrow"><ArrowRight /></el-icon>
123
+ </button>
124
+ </div>
125
+ <div v-else class="agent-model-panel agent-model-panel--empty">
126
+ {{ t("system.ai.model.empty") }}
127
+ </div>
128
+ </el-popover>
57
129
  <el-tooltip
58
130
  :content="recording ? t('system.ai.chat.action.stop_voice_input') : t('system.ai.chat.action.voice_input')"
59
131
  placement="top"
@@ -102,32 +174,81 @@
102
174
 
103
175
  <script setup lang="ts" name="XSender">
104
176
  import { computed, ref } from "vue";
177
+ import { useRouter } from "vue-router";
105
178
  import { Attachments, XSender as BaseXSender } from "vue-element-plus-x";
106
179
  import type { FilesCardProps } from "vue-element-plus-x/types/FilesCard";
107
- import { Loading, Microphone, Paperclip, Promotion } from "@element-plus/icons-vue";
108
- import { ElMessage } from "element-plus";
180
+ import {
181
+ ArrowDown,
182
+ ArrowRight,
183
+ Check,
184
+ Loading,
185
+ MagicStick,
186
+ Microphone,
187
+ Paperclip,
188
+ Promotion,
189
+ Setting
190
+ } from "@element-plus/icons-vue";
191
+ import { ElMessage, ElPopover } from "element-plus";
109
192
  import { t } from "@liujitcn/kratos-admin-core";
193
+ import { navigateTo } from "@liujitcn/kratos-admin-core/navigation";
194
+ import { useAuthStore } from "@liujitcn/kratos-admin-core/stores/runtime";
110
195
  import { defFileService } from "@liujitcn/kratos-admin-core/api/base/v1/file";
111
196
  import type { AiAttachment } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
197
+ import type { AiProviderModelOption } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_provider";
112
198
  import type { SubmitPayload } from "../types";
113
199
  import { buildAIAttachmentFileCard } from "../attachment";
200
+ import { fallbackModelDisplayName } from "../modelDisplay";
114
201
  import { useSpeechRecognition } from "./speech-recognition";
115
202
  import type { SpeechRecognitionError } from "./speech-recognition";
116
203
 
117
204
  const props = defineProps<{
118
205
  /** 消息发送加载状态。 */
119
206
  sending: boolean;
207
+ /** 当前可供选择的供应商与模型。 */
208
+ modelProviders: AiProviderModelOption[];
209
+ /** 当前选择的供应商编号。 */
210
+ providerId: number;
211
+ /** 当前选择的模型名称。 */
212
+ modelName: string;
120
213
  }>();
121
214
 
122
215
  const emit = defineEmits<{
123
216
  /** 提交输入器内容。 */
124
217
  submit: [payload: SubmitPayload];
218
+ /** 更新当前选择的供应商和模型。 */
219
+ "model-change": [providerId: number, modelName: string];
125
220
  }>();
126
221
 
127
222
  const senderRef = ref<InstanceType<typeof BaseXSender>>();
128
223
  const fileInputRef = ref<HTMLInputElement>();
224
+ const modelPopoverRef = ref<InstanceType<typeof ElPopover>>();
225
+ const router = useRouter();
226
+ const authStore = useAuthStore();
129
227
  const inputText = ref("");
130
228
  const selectedAttachments = ref<AiAttachment[]>([]);
229
+ const modelOptions = computed(() =>
230
+ props.modelProviders.map(provider => ({
231
+ providerId: provider.provider_id,
232
+ label: provider.name,
233
+ models: provider.models.map(model => ({
234
+ name: model.model_name,
235
+ displayName: model.display_name || fallbackModelDisplayName(model.model_name)
236
+ }))
237
+ }))
238
+ );
239
+ const modelOptionCount = computed(() =>
240
+ modelOptions.value.reduce((total, provider) => total + provider.models.length, 0)
241
+ );
242
+ const selectedModelDisplayName = computed(() => {
243
+ const provider = modelOptions.value.find(item => item.providerId === props.providerId);
244
+ return (
245
+ provider?.models.find(model => model.name === props.modelName)?.displayName ??
246
+ fallbackModelDisplayName(props.modelName)
247
+ );
248
+ });
249
+ const aiProviderRoute = computed(() =>
250
+ authStore.flatMenuListGet.find(item => item.name === "AiProvider" && item.path)
251
+ );
131
252
  const uploading = ref(false);
132
253
  const maxAttachmentCount = 6;
133
254
  const maxAttachmentSizeMB = 20;
@@ -184,6 +305,24 @@ const isSubmitDisabled = computed(() => {
184
305
  return uploading.value || (!inputText.value.trim() && selectedAttachments.value.length === 0);
185
306
  });
186
307
 
308
+ /** 判断指定供应商与模型是否为当前选择。 */
309
+ function isModelSelected(providerId: number, modelName: string) {
310
+ return props.providerId === providerId && props.modelName === modelName;
311
+ }
312
+
313
+ /** 选中模型并关闭选择面板。 */
314
+ function handleModelSelect(providerId: number, modelName: string) {
315
+ emit("model-change", providerId, modelName);
316
+ modelPopoverRef.value?.hide();
317
+ }
318
+
319
+ /** 跳转到 AI 供应商配置页。 */
320
+ async function openModelManage() {
321
+ if (!aiProviderRoute.value?.path) return;
322
+ modelPopoverRef.value?.hide();
323
+ await navigateTo(router, aiProviderRoute.value.path);
324
+ }
325
+
187
326
  /** 读取输入内容并发送给父组件。 */
188
327
  function handleSubmit() {
189
328
  if (uploading.value) return;
@@ -392,6 +531,13 @@ function resetFileInput() {
392
531
  align-items: center;
393
532
  justify-content: space-between;
394
533
  }
534
+ :deep(.elx-x-sender__updown-action-list .elx-x-sender__prefix) {
535
+ flex: 1 1 auto;
536
+ width: 100%;
537
+ min-width: 0;
538
+ height: auto;
539
+ padding-left: 0;
540
+ }
395
541
  :deep(.elx-x-sender__action-list) {
396
542
  height: auto;
397
543
  }
@@ -410,10 +556,61 @@ function resetFileInput() {
410
556
  }
411
557
  .agent-prefix-actions {
412
558
  display: flex;
559
+ flex-wrap: wrap;
413
560
  gap: 10px;
414
561
  align-items: center;
562
+ width: 100%;
415
563
  min-height: 36px;
416
564
  }
565
+ .agent-model-selector {
566
+ display: inline-flex;
567
+ flex: 0 1 auto;
568
+ align-items: center;
569
+ gap: 6px;
570
+ width: clamp(180px, 18vw, 220px);
571
+ min-width: 0;
572
+ height: 34px;
573
+ padding: 0 10px;
574
+ overflow: hidden;
575
+ box-sizing: border-box;
576
+ color: var(--admin-page-text-secondary);
577
+ cursor: pointer;
578
+ background: var(--admin-page-card-bg);
579
+ border: 1px solid var(--el-border-color);
580
+ border-radius: var(--admin-page-radius);
581
+ transition:
582
+ color 0.2s ease,
583
+ border-color 0.2s ease,
584
+ background-color 0.2s ease;
585
+ &:hover {
586
+ color: var(--el-color-primary);
587
+ background: var(--el-color-primary-light-9);
588
+ border-color: var(--el-color-primary-light-5);
589
+ }
590
+ &:disabled {
591
+ cursor: not-allowed;
592
+ opacity: 0.55;
593
+ }
594
+ &__spark {
595
+ flex-shrink: 0;
596
+ font-size: 15px;
597
+ }
598
+ &__label {
599
+ flex: 1;
600
+ min-width: 0;
601
+ overflow: hidden;
602
+ font-size: 14px;
603
+ font-weight: 500;
604
+ line-height: 20px;
605
+ text-align: left;
606
+ text-overflow: ellipsis;
607
+ white-space: nowrap;
608
+ }
609
+ &__arrow {
610
+ flex-shrink: 0;
611
+ font-size: 14px;
612
+ }
613
+ }
417
614
  .agent-sender-actions {
418
615
  display: inline-flex;
419
616
  gap: 8px;
@@ -504,11 +701,120 @@ function resetFileInput() {
504
701
  :global(.agent-sender-popover.el-popover) {
505
702
  box-shadow: 0 14px 36px rgb(15 23 42 / 10%);
506
703
  }
704
+ :global(.agent-model-popper.el-popover) {
705
+ padding: 0 !important;
706
+ border-radius: var(--admin-page-radius) !important;
707
+ box-shadow: 0 14px 36px rgb(15 23 42 / 16%) !important;
708
+ }
709
+ .agent-model-panel {
710
+ padding: 10px;
711
+ &__list {
712
+ max-height: min(52vh, 420px);
713
+ overflow-y: auto;
714
+ scrollbar-width: thin;
715
+ scrollbar-color: var(--el-border-color-darker) transparent;
716
+ }
717
+ &__head {
718
+ display: flex;
719
+ align-items: baseline;
720
+ justify-content: space-between;
721
+ padding: 4px 6px 10px;
722
+ }
723
+ &__title {
724
+ font-size: 14px;
725
+ font-weight: 700;
726
+ color: var(--admin-page-text-primary);
727
+ }
728
+ &__meta {
729
+ font-size: 11px;
730
+ color: var(--el-text-color-placeholder);
731
+ }
732
+ &__group {
733
+ padding: 8px 6px 6px;
734
+ font-size: 12px;
735
+ color: var(--admin-page-text-secondary);
736
+ }
737
+ &__manage {
738
+ display: inline-flex;
739
+ align-items: center;
740
+ gap: 6px;
741
+ width: 100%;
742
+ height: 36px;
743
+ margin-top: 6px;
744
+ padding: 0 6px;
745
+ font-size: 12.5px;
746
+ font-weight: 600;
747
+ color: var(--el-color-primary);
748
+ cursor: pointer;
749
+ background: transparent;
750
+ border: 0;
751
+ border-top: 1px solid var(--el-border-color-lighter);
752
+ border-radius: 0;
753
+ &:hover {
754
+ color: var(--el-color-primary-light-3);
755
+ }
756
+ }
757
+ &__manage-arrow {
758
+ margin-left: auto;
759
+ }
760
+ &--empty {
761
+ padding: 20px 10px;
762
+ font-size: 13px;
763
+ color: var(--admin-page-text-secondary);
764
+ text-align: center;
765
+ }
766
+ }
767
+ .agent-model-row {
768
+ display: flex;
769
+ align-items: center;
770
+ gap: 8px;
771
+ width: 100%;
772
+ height: 42px;
773
+ margin-bottom: 2px;
774
+ padding: 0 10px;
775
+ font-size: 13.5px;
776
+ color: var(--admin-page-text-primary);
777
+ text-align: left;
778
+ cursor: pointer;
779
+ background: transparent;
780
+ border: 0;
781
+ border-radius: var(--admin-page-radius);
782
+ &:hover {
783
+ background: var(--el-fill-color-light);
784
+ }
785
+ &.is-selected {
786
+ font-weight: 600;
787
+ color: var(--el-color-primary);
788
+ background: var(--el-color-primary-light-9);
789
+ }
790
+ &__name {
791
+ overflow: hidden;
792
+ text-overflow: ellipsis;
793
+ white-space: nowrap;
794
+ }
795
+ &__raw {
796
+ flex: 1;
797
+ min-width: 0;
798
+ overflow: hidden;
799
+ font-size: 12px;
800
+ font-weight: 400;
801
+ color: var(--el-text-color-placeholder);
802
+ text-overflow: ellipsis;
803
+ white-space: nowrap;
804
+ }
805
+ &__check {
806
+ flex-shrink: 0;
807
+ font-size: 15px;
808
+ }
809
+ }
507
810
 
508
811
  @media screen and (width <= 768px) {
509
812
  .agent-prefix-actions {
510
813
  gap: 8px;
511
814
  }
815
+ .agent-model-selector {
816
+ width: clamp(128px, calc(100vw - 220px), 180px);
817
+ }
512
818
  .agent-action-text {
513
819
  display: none;
514
820
  }