@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.
- package/README.md +16 -1
- package/dist/package/declarations/src/api/base/v1/ai_provider.d.ts +7 -0
- package/dist/package/declarations/src/api/system/admin/v1/ai_provider.d.ts +20 -0
- package/dist/package/declarations/src/api/system/admin/v1/base_api_rate_limit_policy.d.ts +19 -0
- package/dist/package/declarations/src/api/system/admin/v1/base_message_provider.d.ts +21 -0
- package/dist/package/declarations/src/api/system/admin/v1/base_message_template.d.ts +16 -0
- package/dist/package/declarations/src/api/system/admin/v1/base_oauth_provider.d.ts +18 -0
- package/dist/package/declarations/src/api/system/admin/v1/base_rate_limit_rule.d.ts +21 -0
- package/dist/package/declarations/src/components/api/ApiTransfer.vue.d.ts +35 -0
- package/dist/package/declarations/src/components/api/apiTransfer.d.ts +13 -0
- package/dist/package/declarations/src/components/i18n/DynamicI18nCell.vue.d.ts +2 -2
- package/dist/package/declarations/src/components/i18n/dynamicI18n.d.ts +3 -4
- package/dist/package/declarations/src/locales/generated.d.ts +968 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +4 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_provider.d.ts +29 -0
- package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +20 -0
- package/dist/package/declarations/src/rpc/google/protobuf/struct.d.ts +81 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +133 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_api_rate_limit_policy.d.ts +137 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +4 -23
- package/dist/package/declarations/src/rpc/system/admin/v1/base_message.d.ts +4 -4
- package/dist/package/declarations/src/rpc/system/admin/v1/base_message_category.d.ts +19 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_message_provider.d.ts +126 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_message_template.d.ts +99 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_oauth_provider.d.ts +133 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_rate_limit_rule.d.ts +144 -0
- package/dist/package/declarations/src/utils/persisted-message.d.ts +4 -0
- package/dist/package/declarations/src/utils/rateLimit.d.ts +24 -0
- package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +11 -2
- package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +9 -0
- package/dist/package/declarations/src/views/ai/chat/modelDisplay.d.ts +5 -0
- package/dist/package/declarations/src/views/ai/chat/modelSelection.d.ts +9 -0
- package/dist/package/declarations/src/views/base/ai-provider/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/api-rate-limit-policy/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/message-provider/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/message-template/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/oauth-provider/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/rate-limit-rule/index.vue.d.ts +3 -0
- package/dist/package/src/api/base/v1/ai_provider.ts +22 -0
- package/dist/package/src/api/system/admin/v1/ai_provider.ts +57 -0
- package/dist/package/src/api/system/admin/v1/base_api_rate_limit_policy.ts +51 -0
- package/dist/package/src/api/system/admin/v1/base_message_provider.ts +57 -0
- package/dist/package/src/api/system/admin/v1/base_message_template.ts +44 -0
- package/dist/package/src/api/system/admin/v1/base_oauth_provider.ts +50 -0
- package/dist/package/src/api/system/admin/v1/base_rate_limit_rule.ts +58 -0
- package/dist/package/src/components/ai/Ai.vue +1 -1
- package/dist/package/src/components/api/ApiTransfer.vue +117 -0
- package/dist/package/src/components/api/apiTransfer.ts +27 -0
- package/dist/package/src/components/i18n/DynamicI18nCell.vue +8 -8
- package/dist/package/src/components/i18n/DynamicI18nEditor.vue +6 -3
- package/dist/package/src/components/i18n/dynamicI18n.ts +4 -5
- package/dist/package/src/components/tenant-project/TenantProjectManager.vue +2 -1
- package/dist/package/src/locales/en-US.json +246 -4
- package/dist/package/src/locales/ja-JP.json +246 -4
- package/dist/package/src/locales/zh-CN.json +246 -4
- package/dist/package/src/locales/zh-TW.json +246 -4
- package/dist/package/src/rpc/base/v1/ai_message.ts +7 -1
- package/dist/package/src/rpc/base/v1/ai_provider.ts +41 -0
- package/dist/package/src/rpc/base/v1/oauth.ts +16 -0
- package/dist/package/src/rpc/google/protobuf/struct.ts +80 -0
- package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +155 -0
- package/dist/package/src/rpc/system/admin/v1/base_api_rate_limit_policy.ts +160 -0
- package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +4 -24
- package/dist/package/src/rpc/system/admin/v1/base_message.ts +4 -4
- package/dist/package/src/rpc/system/admin/v1/base_message_category.ts +20 -0
- package/dist/package/src/rpc/system/admin/v1/base_message_provider.ts +150 -0
- package/dist/package/src/rpc/system/admin/v1/base_message_template.ts +117 -0
- package/dist/package/src/rpc/system/admin/v1/base_oauth_provider.ts +156 -0
- package/dist/package/src/rpc/system/admin/v1/base_rate_limit_rule.ts +167 -0
- package/dist/package/src/utils/persisted-message.ts +21 -0
- package/dist/package/src/utils/rateLimit.ts +140 -0
- package/dist/package/src/views/ai/chat/components/ChatPanel.vue +40 -5
- package/dist/package/src/views/ai/chat/components/XSender.vue +309 -3
- package/dist/package/src/views/ai/chat/index.vue +48 -1
- package/dist/package/src/views/ai/chat/modelDisplay.ts +67 -0
- package/dist/package/src/views/ai/chat/modelSelection.ts +31 -0
- package/dist/package/src/views/base/ai-provider/index.vue +613 -0
- package/dist/package/src/views/base/api-rate-limit-policy/index.vue +429 -0
- package/dist/package/src/views/base/backup-management/archive-record/index.vue +2 -0
- package/dist/package/src/views/base/backup-management/archive-restore/index.vue +2 -1
- package/dist/package/src/views/base/backup-management/backup-record/index.vue +2 -0
- package/dist/package/src/views/base/backup-management/backup-restore/index.vue +2 -1
- package/dist/package/src/views/base/config/index.vue +12 -13
- package/dist/package/src/views/base/dict/index.vue +2 -3
- package/dist/package/src/views/base/dict/item.vue +2 -3
- package/dist/package/src/views/base/job/index.vue +2 -3
- package/dist/package/src/views/base/job/log.vue +3 -2
- package/dist/package/src/views/base/menu/index.vue +27 -83
- package/dist/package/src/views/base/message/index.vue +9 -3
- package/dist/package/src/views/base/message-category/index.vue +34 -3
- package/dist/package/src/views/base/message-provider/index.vue +168 -0
- package/dist/package/src/views/base/message-template/index.vue +105 -0
- package/dist/package/src/views/base/migration/index.vue +1 -1
- package/dist/package/src/views/base/oauth-client/index.vue +6 -6
- package/dist/package/src/views/base/oauth-provider/index.vue +274 -0
- package/dist/package/src/views/base/rate-limit-rule/index.vue +327 -0
- package/dist/package/src/views/base/redact-output-policy/index.vue +1 -1
- package/dist/package/src/views/base/redact-rule/index.vue +12 -2
- package/dist/package/src/views/base/redact-storage-policy/index.vue +4 -0
- package/dist/package/src/views/base/tenant/index.vue +3 -3
- 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
|
|
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">{{
|
|
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
|
|
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 {
|
|
108
|
-
|
|
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
|
}
|