@liujitcn/kratos-admin-system 0.0.65 → 0.0.66
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 +5 -3
- package/dist/package/declarations/src/api/base/v1/ai_knowledge.d.ts +7 -0
- package/dist/package/declarations/src/api/system/admin/v1/ai_provider.d.ts +3 -1
- package/dist/package/declarations/src/api/system/admin/v1/ai_query.d.ts +10 -0
- package/dist/package/declarations/src/api/system/admin/v1/ai_session.d.ts +9 -0
- package/dist/package/declarations/src/locales/generated.d.ts +1801 -1553
- package/dist/package/declarations/src/rpc/base/v1/ai_knowledge.d.ts +22 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +2 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +63 -5
- package/dist/package/declarations/src/rpc/system/admin/v1/ai_query.d.ts +73 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/ai_session.d.ts +114 -0
- package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +9 -2
- package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +7 -0
- package/dist/package/declarations/src/views/ai/query/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/ai/session/index.vue.d.ts +3 -0
- package/dist/package/src/api/base/v1/ai_knowledge.ts +22 -0
- package/dist/package/src/api/system/admin/v1/ai_provider.ts +7 -0
- package/dist/package/src/api/system/admin/v1/ai_query.ts +25 -0
- package/dist/package/src/api/system/admin/v1/ai_session.ts +25 -0
- package/dist/package/src/components/account/ChangePasswordForm.vue +1 -1
- package/dist/package/src/components/tenant-project/TenantProjectSelect.vue +21 -4
- package/dist/package/src/locales/en-US.json +354 -292
- package/dist/package/src/locales/ja-JP.json +351 -289
- package/dist/package/src/locales/zh-CN.json +351 -289
- package/dist/package/src/locales/zh-TW.json +351 -289
- package/dist/package/src/rpc/base/v1/ai_knowledge.ts +33 -0
- package/dist/package/src/rpc/base/v1/ai_message.ts +2 -0
- package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +68 -5
- package/dist/package/src/rpc/system/admin/v1/ai_query.ts +89 -0
- package/dist/package/src/rpc/system/admin/v1/ai_session.ts +131 -0
- package/dist/package/src/views/ai/chat/components/ChatPanel.vue +18 -0
- package/dist/package/src/views/ai/chat/components/XSender.vue +101 -0
- package/dist/package/src/views/ai/chat/index.vue +22 -2
- package/dist/package/src/views/ai/query/index.vue +222 -0
- package/dist/package/src/views/ai/session/index.vue +422 -0
- package/dist/package/src/views/base/ai-provider/index.vue +201 -155
- package/dist/package/src/views/base/message-provider/index.vue +2 -2
- package/dist/package/src/views/base/oauth-provider/index.vue +2 -2
- package/dist/package/src/views/base/tenant-project-grant/index.vue +9 -4
- package/dist/package/src/views/tool/code-gen/config.ts +1 -0
- package/package.json +2 -2
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
<div class="provider-models__heading">
|
|
28
28
|
<strong>{{ model.model_name }}</strong>
|
|
29
29
|
<span v-if="model.display_name" class="provider-models__alias">✦ {{ model.display_name }}</span>
|
|
30
|
-
<el-tag size="small" effect="plain">{{
|
|
30
|
+
<el-tag size="small" effect="plain">{{ categoryLabel(model.category) }}</el-tag>
|
|
31
|
+
<el-tag size="small" effect="plain" :type="model.status === Status.STATUS_ENABLE ? 'success' : 'info'">
|
|
32
|
+
{{ t(model.status === Status.STATUS_ENABLE ? "common.status.enabled" : "common.status.disabled") }}
|
|
33
|
+
</el-tag>
|
|
31
34
|
<el-tooltip
|
|
32
35
|
v-if="modelTestResults[model.model_name]"
|
|
33
36
|
:content="modelTestResults[model.model_name]?.message"
|
|
@@ -35,21 +38,27 @@
|
|
|
35
38
|
placement="top"
|
|
36
39
|
>
|
|
37
40
|
<el-tag
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
size="small"
|
|
42
|
+
effect="plain"
|
|
43
|
+
:type="modelTestResults[model.model_name]?.success ? 'success' : 'danger'"
|
|
41
44
|
>
|
|
42
45
|
{{ t(modelTestResults[model.model_name]?.success
|
|
43
|
-
? "system.base.
|
|
44
|
-
: "system.base.
|
|
46
|
+
? "system.base.ai.model.action.test_success"
|
|
47
|
+
: "system.base.ai.model.action.test_failed") }} · {{ modelTestResults[model.model_name]?.duration_ms }} ms
|
|
45
48
|
</el-tag>
|
|
46
49
|
</el-tooltip>
|
|
47
50
|
</div>
|
|
48
51
|
<div class="provider-models__details">
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
<template v-if="model.category === AiModelCategory.AI_MODEL_CATEGORY_CHAT">
|
|
53
|
+
<span>{{ t("system.base.ai.model.field.api_type") }}: {{ apiTypeLabel(model.api_type) }}</span>
|
|
54
|
+
<span>{{ t("system.base.ai.model.field.temperature") }}: {{ model.temperature }}</span>
|
|
55
|
+
<span>{{ t("system.base.ai.model.field.max_tokens") }}: {{ model.max_tokens }}</span>
|
|
56
|
+
</template>
|
|
57
|
+
<span v-if="model.category === AiModelCategory.AI_MODEL_CATEGORY_EMBEDDING && model.dimensions > 0">
|
|
58
|
+
{{ t("system.base.ai.model.field.dimensions") }}: {{ model.dimensions }}
|
|
59
|
+
</span>
|
|
60
|
+
<span>{{ t("system.base.ai.model.field.timeout_seconds") }}: {{ model.timeout_seconds }}</span>
|
|
61
|
+
<span>{{ t("system.base.ai.model.field.max_retries") }}: {{ model.max_retries }}</span>
|
|
53
62
|
</div>
|
|
54
63
|
</div>
|
|
55
64
|
<div class="provider-models__actions">
|
|
@@ -86,9 +95,8 @@
|
|
|
86
95
|
v-model="modelDialog.visible"
|
|
87
96
|
ref="modelDialogRef"
|
|
88
97
|
:title="t(modelDialog.editing ? 'system.base.ai_provider.model.edit' : 'system.base.ai_provider.model.add')"
|
|
89
|
-
width="min(
|
|
90
|
-
label-width="
|
|
91
|
-
label-position="left"
|
|
98
|
+
width="min(720px, calc(100vw - 32px))"
|
|
99
|
+
label-width="10em"
|
|
92
100
|
:model="modelForm"
|
|
93
101
|
:fields="modelFields"
|
|
94
102
|
:rules="modelRules"
|
|
@@ -112,11 +120,13 @@ import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-c
|
|
|
112
120
|
import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
|
|
113
121
|
import { t } from "@liujitcn/kratos-admin-core";
|
|
114
122
|
import { defAiProviderService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/ai_provider";
|
|
115
|
-
import
|
|
116
|
-
AiProvider,
|
|
117
|
-
AiProviderForm,
|
|
123
|
+
import {
|
|
124
|
+
type AiProvider,
|
|
125
|
+
type AiProviderForm,
|
|
126
|
+
AiModelCategory,
|
|
127
|
+
type AiProviderModelForm,
|
|
118
128
|
AiProviderModelTestResult,
|
|
119
|
-
PageAiProviderRequest
|
|
129
|
+
type PageAiProviderRequest
|
|
120
130
|
} from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/ai_provider";
|
|
121
131
|
import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
|
|
122
132
|
|
|
@@ -128,30 +138,26 @@ interface ConfigItem {
|
|
|
128
138
|
value: string;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/** 单个AI模型的可编辑配置。 */
|
|
135
|
-
interface AiModelConfig {
|
|
136
|
-
/** 服务商侧的模型名称。 */
|
|
137
|
-
model_name: string;
|
|
138
|
-
/** 模型在聊天前端的展示名称,留空时由前端按模型名自动映射。 */
|
|
139
|
-
display_name: string;
|
|
140
|
-
/** 模型请求接口类型。 */
|
|
141
|
-
api_type: ModelApiType;
|
|
142
|
-
/** 文本生成温度。 */
|
|
141
|
+
/** 单个AI模型的页面表单状态;分类个性化配置先拍平成兄弟字段,提交时再收敛进 config。 */
|
|
142
|
+
interface ModelFormState extends Omit<AiProviderModelForm, "config"> {
|
|
143
|
+
api_type: string;
|
|
143
144
|
temperature: number;
|
|
144
|
-
/** 单次请求最大输出Token数。 */
|
|
145
145
|
max_tokens: number;
|
|
146
|
-
|
|
146
|
+
dimensions: number;
|
|
147
|
+
duration: number;
|
|
148
|
+
size: string;
|
|
149
|
+
quality: string;
|
|
150
|
+
resolution: string;
|
|
151
|
+
task: string;
|
|
152
|
+
voice: string;
|
|
153
|
+
speed: number;
|
|
147
154
|
timeout_seconds: number;
|
|
148
|
-
/** 模型请求失败后的最大重试次数。 */
|
|
149
155
|
max_retries: number;
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
/** AI Provider页面表单状态。 */
|
|
153
|
-
interface FormState extends Omit<AiProviderForm, "
|
|
154
|
-
models:
|
|
159
|
+
interface FormState extends Omit<AiProviderForm, "models"> {
|
|
160
|
+
models: ModelFormState[];
|
|
155
161
|
config_items: ConfigItem[];
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -163,7 +169,7 @@ const modelDialogRef = ref<InstanceType<typeof FormDialog>>();
|
|
|
163
169
|
const dialog = reactive({ visible: false, editing: false });
|
|
164
170
|
const modelDialog = reactive({ visible: false, editing: false, index: -1 });
|
|
165
171
|
const form = reactive<FormState>(defaultForm());
|
|
166
|
-
const modelForm = reactive<
|
|
172
|
+
const modelForm = reactive<ModelFormState>(defaultModelForm());
|
|
167
173
|
const testingModels = ref(false);
|
|
168
174
|
const modelTestResults = ref<Record<string, AiProviderModelTestResult>>({});
|
|
169
175
|
|
|
@@ -184,44 +190,88 @@ const providerOptions = computed<ProFormOption[]>(() => [
|
|
|
184
190
|
{ label: t("system.base.ai_provider.provider.ollama"), value: "ollama" }
|
|
185
191
|
]);
|
|
186
192
|
|
|
187
|
-
const
|
|
188
|
-
{ label: t("system.base.
|
|
189
|
-
{ label: t("system.base.
|
|
193
|
+
const categoryOptions = computed<ProFormOption[]>(() => [
|
|
194
|
+
{ label: t("system.base.ai.model.category.chat"), value: AiModelCategory.AI_MODEL_CATEGORY_CHAT },
|
|
195
|
+
{ label: t("system.base.ai.model.category.embedding"), value: AiModelCategory.AI_MODEL_CATEGORY_EMBEDDING },
|
|
196
|
+
{ label: t("system.base.ai.model.category.rerank"), value: AiModelCategory.AI_MODEL_CATEGORY_RERANK },
|
|
197
|
+
{ label: t("system.base.ai.model.category.image"), value: AiModelCategory.AI_MODEL_CATEGORY_IMAGE },
|
|
198
|
+
{ label: t("system.base.ai.model.category.video"), value: AiModelCategory.AI_MODEL_CATEGORY_VIDEO },
|
|
199
|
+
{ label: t("system.base.ai.model.category.audio"), value: AiModelCategory.AI_MODEL_CATEGORY_AUDIO }
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
const apiTypeOptions = computed<ProFormOption[]>(() => [
|
|
203
|
+
{ label: t("system.base.ai.model.api_type.chat_completions"), value: "CHAT_COMPLETIONS" },
|
|
204
|
+
{ label: t("system.base.ai.model.api_type.responses"), value: "RESPONSES" }
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
const taskOptions = computed<ProFormOption[]>(() => [
|
|
208
|
+
{ label: t("system.base.ai.model.task.tts"), value: "tts" },
|
|
209
|
+
{ label: t("system.base.ai.model.task.asr"), value: "asr" }
|
|
190
210
|
]);
|
|
191
211
|
|
|
212
|
+
/** 分类个性化配置的表单字段,按分类过滤展示。 */
|
|
213
|
+
const categoryConfigFields: Partial<Record<AiModelCategory, string[]>> = {
|
|
214
|
+
[AiModelCategory.AI_MODEL_CATEGORY_CHAT]: ["api_type", "temperature", "max_tokens", "timeout_seconds", "max_retries"],
|
|
215
|
+
[AiModelCategory.AI_MODEL_CATEGORY_EMBEDDING]: ["dimensions", "timeout_seconds", "max_retries"],
|
|
216
|
+
[AiModelCategory.AI_MODEL_CATEGORY_RERANK]: ["timeout_seconds", "max_retries"],
|
|
217
|
+
[AiModelCategory.AI_MODEL_CATEGORY_IMAGE]: ["size", "quality", "timeout_seconds", "max_retries"],
|
|
218
|
+
[AiModelCategory.AI_MODEL_CATEGORY_VIDEO]: ["duration", "resolution", "timeout_seconds", "max_retries"],
|
|
219
|
+
[AiModelCategory.AI_MODEL_CATEGORY_AUDIO]: ["task", "voice", "speed", "timeout_seconds", "max_retries"]
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** 全部配置字段的表单项定义。 */
|
|
223
|
+
const configFieldMap: Record<string, ProFormField> = {
|
|
224
|
+
api_type: { prop: "api_type", label: t("system.base.ai.model.field.api_type"), component: "select", colSpan: 12, options: apiTypeOptions.value },
|
|
225
|
+
temperature: { prop: "temperature", label: t("system.base.ai.model.field.temperature"), component: "input-number", colSpan: 12, props: { min: 0, max: 2, precision: 2, step: 0.1, controlsPosition: "right" } },
|
|
226
|
+
max_tokens: { prop: "max_tokens", label: t("system.base.ai.model.field.max_tokens"), component: "input-number", colSpan: 12, props: { min: 0, max: 200000, precision: 0, controlsPosition: "right" } },
|
|
227
|
+
dimensions: { prop: "dimensions", label: t("system.base.ai.model.field.dimensions"), component: "input-number", colSpan: 12, props: { min: 0, max: 8192, precision: 0, controlsPosition: "right", placeholder: t("system.base.ai.model.placeholder.dimensions") } },
|
|
228
|
+
duration: { prop: "duration", label: t("system.base.ai.model.field.duration"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0, controlsPosition: "right" } },
|
|
229
|
+
size: { prop: "size", label: t("system.base.ai.model.field.size"), component: "input", colSpan: 12, props: { maxlength: 50, placeholder: "1024x1024" } },
|
|
230
|
+
quality: { prop: "quality", label: t("system.base.ai.model.field.quality"), component: "input", colSpan: 12, props: { maxlength: 50 } },
|
|
231
|
+
resolution: { prop: "resolution", label: t("system.base.ai.model.field.resolution"), component: "input", colSpan: 12, props: { maxlength: 50, placeholder: "1920x1080" } },
|
|
232
|
+
task: { prop: "task", label: t("system.base.ai.model.field.task"), component: "select", colSpan: 12, options: taskOptions.value },
|
|
233
|
+
voice: { prop: "voice", label: t("system.base.ai.model.field.voice"), component: "input", colSpan: 12, props: { maxlength: 100 } },
|
|
234
|
+
speed: { prop: "speed", label: t("system.base.ai.model.field.speed"), component: "input-number", colSpan: 12, props: { min: 0, max: 4, precision: 2, step: 0.1, controlsPosition: "right" } },
|
|
235
|
+
timeout_seconds: { prop: "timeout_seconds", label: t("system.base.ai.model.field.timeout_seconds"), component: "input-number", colSpan: 12, props: { min: 0, max: 600, precision: 0, controlsPosition: "right" } },
|
|
236
|
+
max_retries: { prop: "max_retries", label: t("system.base.ai.model.field.max_retries"), component: "input-number", colSpan: 12, props: { min: 0, max: 10, precision: 0, controlsPosition: "right" } }
|
|
237
|
+
};
|
|
238
|
+
|
|
192
239
|
const fields = computed<ProFormField[]>(() => [
|
|
193
240
|
{ prop: "provider", label: t("system.base.ai_provider.field.provider"), component: "select", colSpan: 12, options: providerOptions.value, props: { disabled: dialog.editing } },
|
|
194
|
-
{ prop: "name", label: t("system.base.ai_provider.field.name"), component: "input", colSpan: 12, props: { maxlength: 100 } },
|
|
195
|
-
{ prop: "base_url", label: t("system.base.ai_provider.field.base_url"), component: "input", colSpan: 12, props: { maxlength: 512, placeholder: t("system.base.ai_provider.placeholder.base_url") } },
|
|
196
|
-
{ prop: "api_key", label: t("system.base.ai_provider.field.api_key"), component: "input", colSpan: 12, props: { type: "password", showPassword: true, maxlength: 1024, placeholder: form.api_key_configured ? t("system.base.ai_provider.placeholder.keep_api_key") : t("system.base.ai_provider.placeholder.api_key") } },
|
|
241
|
+
{ prop: "name", label: t("system.base.ai_provider.field.name"), component: "input", colSpan: 12, props: { maxlength: 100, autocomplete: "off" } },
|
|
242
|
+
{ prop: "base_url", label: t("system.base.ai_provider.field.base_url"), component: "input", colSpan: 12, props: { maxlength: 512, autocomplete: "off", placeholder: t("system.base.ai_provider.placeholder.base_url") } },
|
|
243
|
+
{ prop: "api_key", label: t("system.base.ai_provider.field.api_key"), component: "input", colSpan: 12, props: { type: "password", showPassword: true, maxlength: 1024, autocomplete: "new-password", placeholder: form.api_key_configured ? t("system.base.ai_provider.placeholder.keep_api_key") : t("system.base.ai_provider.placeholder.api_key") } },
|
|
197
244
|
{ prop: "models", label: t("system.base.ai_provider.field.models"), component: "slot", slotName: "models", colSpan: 24 },
|
|
198
245
|
{ prop: "config_items", label: t("system.base.ai_provider.field.config"), component: "kv-list", colSpan: 24, props: { keyInputProps: { maxlength: 128 } } },
|
|
199
246
|
{ prop: "sort", label: t("common.field.sort"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0 } },
|
|
200
247
|
{ prop: "status", label: t("common.field.status"), component: "radio-group", colSpan: 12, options: statusOptions.value }
|
|
201
248
|
]);
|
|
202
249
|
|
|
203
|
-
const modelFields = computed<ProFormField[]>(() =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
250
|
+
const modelFields = computed<ProFormField[]>(() => {
|
|
251
|
+
const category = modelForm.category;
|
|
252
|
+
const configFields = (categoryConfigFields[category] ?? []).map(key => configFieldMap[key]);
|
|
253
|
+
return [
|
|
254
|
+
{ prop: "model_name", label: t("system.base.ai.model.field.model_name"), component: "input", colSpan: 12, props: { maxlength: 200, autocomplete: "off" } },
|
|
255
|
+
{ prop: "display_name", label: t("system.base.ai.model.field.display_name"), component: "input", colSpan: 12, props: { maxlength: 100, autocomplete: "off", placeholder: t("system.base.ai.model.placeholder.display_name") } },
|
|
256
|
+
{ prop: "category", label: t("system.base.ai.model.field.category"), component: "select", colSpan: 12, options: categoryOptions.value },
|
|
257
|
+
{ prop: "sort", label: t("common.field.sort"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0 } },
|
|
258
|
+
{ prop: "status", label: t("common.field.status"), component: "radio-group", colSpan: 12, options: statusOptions.value },
|
|
259
|
+
...configFields
|
|
260
|
+
];
|
|
261
|
+
});
|
|
212
262
|
|
|
213
263
|
const rules = computed<FormRules>(() => ({
|
|
214
264
|
name: [{ required: true, message: t("system.base.ai_provider.validation.name"), trigger: "blur" }],
|
|
215
265
|
provider: [{ required: true, message: t("system.base.ai_provider.validation.provider"), trigger: "change" }],
|
|
216
266
|
models: [{
|
|
217
|
-
validator: (_rule, value:
|
|
267
|
+
validator: (_rule, value: ModelFormState[], callback) => {
|
|
218
268
|
if (!Array.isArray(value) || value.length === 0) {
|
|
219
269
|
callback(new Error(t("system.base.ai_provider.model.validation.required")));
|
|
220
270
|
return;
|
|
221
271
|
}
|
|
222
272
|
const names = value.map(model => model.model_name);
|
|
223
273
|
callback(names.some(name => !name)
|
|
224
|
-
? new Error(t("system.base.
|
|
274
|
+
? new Error(t("system.base.ai.model.validation.model_name"))
|
|
225
275
|
: new Set(names).size !== names.length
|
|
226
276
|
? new Error(t("system.base.ai_provider.model.validation.duplicate"))
|
|
227
277
|
: undefined);
|
|
@@ -232,15 +282,10 @@ const rules = computed<FormRules>(() => ({
|
|
|
232
282
|
|
|
233
283
|
const modelRules = computed<FormRules>(() => ({
|
|
234
284
|
model_name: [
|
|
235
|
-
{ required: true, message: t("system.base.
|
|
236
|
-
{ max:
|
|
285
|
+
{ required: true, message: t("system.base.ai.model.validation.model_name"), trigger: "blur" },
|
|
286
|
+
{ max: 200, message: t("system.base.ai.model.validation.model_name_length"), trigger: "blur" }
|
|
237
287
|
],
|
|
238
|
-
|
|
239
|
-
api_type: [{ required: true, message: t("system.base.ai_provider.validation.api_type"), trigger: "change" }],
|
|
240
|
-
temperature: [{ type: "number", required: true, min: 0, max: 2, message: t("system.base.ai_provider.model.validation.range"), trigger: "change" }],
|
|
241
|
-
max_tokens: [{ type: "number", required: true, min: 0, max: 200000, message: t("system.base.ai_provider.model.validation.range"), trigger: "change" }],
|
|
242
|
-
timeout_seconds: [{ type: "number", required: true, min: 0, max: 600, message: t("system.base.ai_provider.model.validation.range"), trigger: "change" }],
|
|
243
|
-
max_retries: [{ type: "number", required: true, min: 0, max: 10, message: t("system.base.ai_provider.model.validation.range"), trigger: "change" }]
|
|
288
|
+
category: [{ required: true, message: t("system.base.ai.model.validation.category"), trigger: "change" }]
|
|
244
289
|
}));
|
|
245
290
|
|
|
246
291
|
const columns = computed<ColumnProps[]>(() => [
|
|
@@ -299,6 +344,43 @@ function defaultForm(): FormState {
|
|
|
299
344
|
};
|
|
300
345
|
}
|
|
301
346
|
|
|
347
|
+
/** 创建默认模型表单;各分类配置字段都先给默认值,切换分类时无需重置。 */
|
|
348
|
+
function defaultModelForm(): ModelFormState {
|
|
349
|
+
return {
|
|
350
|
+
id: 0,
|
|
351
|
+
model_name: "",
|
|
352
|
+
display_name: "",
|
|
353
|
+
category: AiModelCategory.AI_MODEL_CATEGORY_CHAT,
|
|
354
|
+
sort: 0,
|
|
355
|
+
status: Status.STATUS_ENABLE,
|
|
356
|
+
api_type: "CHAT_COMPLETIONS",
|
|
357
|
+
temperature: 0.2,
|
|
358
|
+
max_tokens: 1024,
|
|
359
|
+
dimensions: 0,
|
|
360
|
+
duration: 0,
|
|
361
|
+
size: "",
|
|
362
|
+
quality: "",
|
|
363
|
+
resolution: "",
|
|
364
|
+
task: "tts",
|
|
365
|
+
voice: "",
|
|
366
|
+
speed: 0,
|
|
367
|
+
timeout_seconds: 60,
|
|
368
|
+
max_retries: 2
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** 模型分类显示名。 */
|
|
373
|
+
function categoryLabel(category: AiModelCategory) {
|
|
374
|
+
const option = categoryOptions.value.find(item => item.value === category);
|
|
375
|
+
return option?.label ?? String(category);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 接口类型显示名。 */
|
|
379
|
+
function apiTypeLabel(apiType: string) {
|
|
380
|
+
const option = apiTypeOptions.value.find(item => item.value === apiType);
|
|
381
|
+
return option?.label ?? apiType;
|
|
382
|
+
}
|
|
383
|
+
|
|
302
384
|
/** 请求AI供应商表格数据。 */
|
|
303
385
|
async function requestTable(params: Record<string, unknown>) {
|
|
304
386
|
const data = await defAiProviderService.PageAiProvider(buildPageRequest<PageAiProviderRequest>(params as unknown as PageAiProviderRequest));
|
|
@@ -311,9 +393,8 @@ async function openDialog(id?: number) {
|
|
|
311
393
|
load: () => (id ? defAiProviderService.GetAiProvider({ id }) : undefined),
|
|
312
394
|
commit: value => {
|
|
313
395
|
if (value) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
models: parseModelConfigs(models_json),
|
|
396
|
+
Object.assign(form, defaultForm(), value, {
|
|
397
|
+
models: (value.models ?? []).map(model => flattenModel(model)),
|
|
317
398
|
config_items: configToItems(value.config)
|
|
318
399
|
});
|
|
319
400
|
} else {
|
|
@@ -324,7 +405,7 @@ async function openDialog(id?: number) {
|
|
|
324
405
|
});
|
|
325
406
|
}
|
|
326
407
|
|
|
327
|
-
/** 提交AI
|
|
408
|
+
/** 提交AI供应商表单(供应商与模型两张表随同一请求保存)。 */
|
|
328
409
|
async function submit() {
|
|
329
410
|
const valid = await dialogRef.value?.validate();
|
|
330
411
|
if (!valid) return;
|
|
@@ -367,26 +448,55 @@ function buildAiProviderPayload(): AiProviderForm | undefined {
|
|
|
367
448
|
base_url: form.base_url,
|
|
368
449
|
api_key: form.api_key,
|
|
369
450
|
api_key_configured: form.api_key_configured,
|
|
370
|
-
|
|
451
|
+
models: form.models.map(model => buildModelPayload(model)),
|
|
371
452
|
config,
|
|
372
453
|
sort: form.sort,
|
|
373
454
|
status: form.status
|
|
374
455
|
};
|
|
375
456
|
}
|
|
376
457
|
|
|
458
|
+
/** 将页面模型表单收敛为保存或测试请求参数。 */
|
|
459
|
+
function buildModelPayload(model: ModelFormState): AiProviderModelForm {
|
|
460
|
+
const category = model.category;
|
|
461
|
+
const config: Record<string, unknown> = {};
|
|
462
|
+
for (const key of categoryConfigFields[category] ?? []) {
|
|
463
|
+
const value = model[key as keyof ModelFormState];
|
|
464
|
+
if (value !== undefined && value !== "") config[key] = value;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
id: model.id,
|
|
468
|
+
model_name: model.model_name,
|
|
469
|
+
display_name: model.display_name,
|
|
470
|
+
category: model.category,
|
|
471
|
+
config,
|
|
472
|
+
sort: model.sort,
|
|
473
|
+
status: model.status
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** 将接口返回的模型表单拍平为页面状态。 */
|
|
478
|
+
function flattenModel(model: AiProviderModelForm): ModelFormState {
|
|
479
|
+
const state = { ...defaultModelForm(), ...model, config: undefined } as unknown as ModelFormState;
|
|
480
|
+
for (const [key, value] of Object.entries(model.config ?? {})) {
|
|
481
|
+
if (key in state) (state as unknown as Record<string, unknown>)[key] = value;
|
|
482
|
+
}
|
|
483
|
+
return state;
|
|
484
|
+
}
|
|
485
|
+
|
|
377
486
|
/** 重置供应商表单。 */
|
|
378
487
|
function resetForm() {
|
|
379
488
|
Object.assign(form, defaultForm());
|
|
380
489
|
dialog.editing = false;
|
|
381
490
|
modelDialog.visible = false;
|
|
382
491
|
resetModelDialog();
|
|
492
|
+
modelTestResults.value = {};
|
|
383
493
|
}
|
|
384
494
|
|
|
385
495
|
/** 打开模型新增或编辑表单。 */
|
|
386
496
|
function openModelDialog(index?: number) {
|
|
387
497
|
modelDialog.index = index ?? -1;
|
|
388
498
|
modelDialog.editing = index !== undefined;
|
|
389
|
-
Object.assign(modelForm, index === undefined ?
|
|
499
|
+
Object.assign(modelForm, index === undefined ? defaultModelForm() : { ...form.models[index] });
|
|
390
500
|
modelDialog.visible = true;
|
|
391
501
|
}
|
|
392
502
|
|
|
@@ -394,7 +504,7 @@ function openModelDialog(index?: number) {
|
|
|
394
504
|
async function saveModel() {
|
|
395
505
|
const valid = await modelDialogRef.value?.validate();
|
|
396
506
|
if (!valid) return;
|
|
397
|
-
const duplicated = form.models.some((model, index) => index !== modelDialog.index && model.model_name === modelForm.model_name);
|
|
507
|
+
const duplicated = form.models.some((model, index) => index !== modelDialog.index && model.model_name === modelForm.model_name.trim());
|
|
398
508
|
if (duplicated) {
|
|
399
509
|
ElMessage.warning(t("system.base.ai_provider.model.validation.duplicate"));
|
|
400
510
|
return;
|
|
@@ -418,39 +528,14 @@ async function removeModel(index: number) {
|
|
|
418
528
|
}
|
|
419
529
|
}
|
|
420
530
|
|
|
421
|
-
/**
|
|
531
|
+
/** 重置模型表单。 */
|
|
422
532
|
function resetModelDialog() {
|
|
533
|
+
Object.assign(modelForm, defaultModelForm());
|
|
423
534
|
modelDialog.editing = false;
|
|
424
535
|
modelDialog.index = -1;
|
|
425
536
|
}
|
|
426
537
|
|
|
427
|
-
/**
|
|
428
|
-
function apiTypeLabel(apiType: ModelApiType) {
|
|
429
|
-
return t(apiType === "RESPONSES"
|
|
430
|
-
? "system.base.ai_provider.model.api_type.responses"
|
|
431
|
-
: "system.base.ai_provider.model.api_type.chat_completions");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/** 将供应商返回的模型配置数组转换为表单数据。 */
|
|
435
|
-
function parseModelConfigs(raw: string): AiModelConfig[] {
|
|
436
|
-
const values = JSON.parse(raw) as Partial<AiModelConfig>[];
|
|
437
|
-
return values.map(value => ({
|
|
438
|
-
model_name: value.model_name ?? "",
|
|
439
|
-
display_name: value.display_name ?? "",
|
|
440
|
-
api_type: value.api_type === "RESPONSES" ? "RESPONSES" : "CHAT_COMPLETIONS",
|
|
441
|
-
temperature: value.temperature ?? 0,
|
|
442
|
-
max_tokens: value.max_tokens ?? 0,
|
|
443
|
-
timeout_seconds: value.timeout_seconds ?? 0,
|
|
444
|
-
max_retries: value.max_retries ?? 0
|
|
445
|
-
}));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** 创建单个模型的默认配置。 */
|
|
449
|
-
function defaultModelConfig(): AiModelConfig {
|
|
450
|
-
return { model_name: "", display_name: "", api_type: "CHAT_COMPLETIONS", temperature: 0.2, max_tokens: 1024, timeout_seconds: 60, max_retries: 2 };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/** 将Provider配置对象转换为键值表单项。 */
|
|
538
|
+
/** 将供应商返回的配置对象转换为键值表单项。 */
|
|
454
539
|
function configToItems(config: Record<string, unknown> | undefined): ConfigItem[] {
|
|
455
540
|
return Object.entries(config ?? {}).map(([key, value]) => ({ key, value: typeof value === "string" ? value : JSON.stringify(value) }));
|
|
456
541
|
}
|
|
@@ -478,7 +563,7 @@ function configItemsToMap(items: ConfigItem[]): Record<string, unknown> | undefi
|
|
|
478
563
|
return result;
|
|
479
564
|
}
|
|
480
565
|
|
|
481
|
-
/** 批量删除AI
|
|
566
|
+
/** 批量删除AI供应商(级联删除其下模型)。 */
|
|
482
567
|
async function handleDelete(target: AiProvider | number | Array<number>) {
|
|
483
568
|
const row = typeof target === "object" && !Array.isArray(target) ? target : undefined;
|
|
484
569
|
const ids = (row ? [row.id] : normalizeSelectedIds(target as number | number[])).map(Number);
|
|
@@ -517,97 +602,58 @@ async function handleSetStatus(row: AiProvider) {
|
|
|
517
602
|
}
|
|
518
603
|
</script>
|
|
519
604
|
|
|
520
|
-
<style scoped>
|
|
605
|
+
<style scoped lang="scss">
|
|
521
606
|
.provider-models {
|
|
522
607
|
display: flex;
|
|
523
608
|
flex-direction: column;
|
|
524
|
-
align-items: flex-start;
|
|
525
609
|
gap: 12px;
|
|
526
610
|
width: 100%;
|
|
527
611
|
}
|
|
528
|
-
|
|
529
612
|
.provider-models__empty {
|
|
530
|
-
|
|
531
|
-
padding: 16px;
|
|
532
|
-
border: 1px dashed var(--el-border-color);
|
|
533
|
-
border-radius: 6px;
|
|
613
|
+
padding: 16px 0;
|
|
534
614
|
color: var(--el-text-color-secondary);
|
|
535
615
|
text-align: center;
|
|
616
|
+
background: var(--el-fill-color-light);
|
|
617
|
+
border-radius: 6px;
|
|
536
618
|
}
|
|
537
|
-
|
|
538
619
|
.provider-models__list {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
border-radius: 6px;
|
|
620
|
+
display: flex;
|
|
621
|
+
flex-direction: column;
|
|
622
|
+
gap: 8px;
|
|
543
623
|
}
|
|
544
|
-
|
|
545
624
|
.provider-models__item {
|
|
546
625
|
display: flex;
|
|
626
|
+
gap: 12px;
|
|
547
627
|
align-items: center;
|
|
548
628
|
justify-content: space-between;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
.provider-models__item + .provider-models__item {
|
|
555
|
-
border-top: 1px solid var(--el-border-color-lighter);
|
|
629
|
+
padding: 8px 12px;
|
|
630
|
+
background: var(--el-fill-color-light);
|
|
631
|
+
border-radius: 6px;
|
|
556
632
|
}
|
|
557
|
-
|
|
558
633
|
.provider-models__content {
|
|
559
634
|
display: flex;
|
|
560
635
|
flex-direction: column;
|
|
561
|
-
gap:
|
|
636
|
+
gap: 4px;
|
|
562
637
|
min-width: 0;
|
|
563
638
|
}
|
|
564
|
-
|
|
565
|
-
.provider-models__heading,
|
|
566
|
-
.provider-models__details,
|
|
567
|
-
.provider-models__actions {
|
|
639
|
+
.provider-models__heading {
|
|
568
640
|
display: flex;
|
|
641
|
+
gap: 8px;
|
|
569
642
|
align-items: center;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
.provider-models__heading {
|
|
573
643
|
flex-wrap: wrap;
|
|
574
|
-
gap: 8px;
|
|
575
|
-
min-width: 0;
|
|
576
644
|
}
|
|
577
|
-
|
|
578
|
-
.provider-models__heading strong {
|
|
579
|
-
overflow-wrap: anywhere;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
645
|
.provider-models__alias {
|
|
583
|
-
|
|
584
|
-
align-items: center;
|
|
585
|
-
gap: 4px;
|
|
586
|
-
padding: 1px 8px;
|
|
587
|
-
font-size: 12px;
|
|
588
|
-
font-weight: 600;
|
|
589
|
-
color: var(--el-color-primary);
|
|
590
|
-
background: var(--el-color-primary-light-9);
|
|
591
|
-
border-radius: 999px;
|
|
592
|
-
white-space: nowrap;
|
|
646
|
+
color: var(--el-text-color-secondary);
|
|
593
647
|
}
|
|
594
|
-
|
|
595
648
|
.provider-models__details {
|
|
649
|
+
display: flex;
|
|
650
|
+
gap: 12px;
|
|
596
651
|
flex-wrap: wrap;
|
|
597
|
-
gap: 4px 14px;
|
|
598
|
-
color: var(--el-text-color-secondary);
|
|
599
652
|
font-size: 12px;
|
|
653
|
+
color: var(--el-text-color-secondary);
|
|
600
654
|
}
|
|
601
|
-
|
|
602
655
|
.provider-models__actions {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
@media (max-width: 640px) {
|
|
608
|
-
.provider-models__item {
|
|
609
|
-
align-items: flex-start;
|
|
610
|
-
padding: 10px;
|
|
611
|
-
}
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-shrink: 0;
|
|
612
658
|
}
|
|
613
659
|
</style>
|
|
@@ -68,8 +68,8 @@ const formFields = computed<ProFormField[]>(() => [
|
|
|
68
68
|
{ prop: "name", label: t("system.base.message_provider.field.name"), component: "input", props: { maxlength: 50 } },
|
|
69
69
|
{ prop: "description", label: t("system.base.message_provider.field.description"), component: "input", colSpan: 24, props: { maxlength: 255 } },
|
|
70
70
|
{ prop: "icon", label: t("system.base.message_provider.field.icon"), component: "input", props: { maxlength: 100 } },
|
|
71
|
-
{ prop: "client_id", label: t("system.base.message_provider.field.client_id"), component: "input", props: { maxlength: 255 } },
|
|
72
|
-
{ prop: "client_secret", label: t("system.base.message_provider.field.client_secret"), component: "input", props: { type: "password", showPassword: true, maxlength: 1024, placeholder: dialog.editing ? t("system.base.message_provider.placeholder.keep_secret") : "" } },
|
|
71
|
+
{ prop: "client_id", label: t("system.base.message_provider.field.client_id"), component: "input", props: { maxlength: 255, autocomplete: "off" } },
|
|
72
|
+
{ prop: "client_secret", label: t("system.base.message_provider.field.client_secret"), component: "input", props: { type: "password", showPassword: true, maxlength: 1024, autocomplete: "new-password", placeholder: dialog.editing ? t("system.base.message_provider.placeholder.keep_secret") : "" } },
|
|
73
73
|
{ prop: "config_items", label: t("system.base.message_provider.field.config"), component: "kv-list", props: { keyInputProps: { maxlength: 128 } } },
|
|
74
74
|
{ prop: "sort", label: t("common.field.sort"), component: "input-number", props: { min: 0, precision: 0 } },
|
|
75
75
|
{ prop: "status", label: t("common.field.status"), component: "radio-group", options: statusOptions.value }
|
|
@@ -102,8 +102,8 @@ const formFields = computed<ProFormField[]>(() => [
|
|
|
102
102
|
{ prop: "name", suffixSlotName: "nameI18ns", label: t("system.base.oauth_provider.field.name"), component: "input", colSpan: 12, props: { maxlength: 50, placeholder: t("system.base.oauth_provider.placeholder.name") } },
|
|
103
103
|
{ prop: "description", suffixSlotName: "descriptionI18ns", label: t("system.base.oauth_provider.field.description"), component: "input", colSpan: 24, props: { maxlength: 255, placeholder: t("system.base.oauth_provider.placeholder.description") } },
|
|
104
104
|
{ prop: "icon", label: t("system.base.oauth_provider.field.icon"), component: "input", colSpan: 12, props: { maxlength: 255, placeholder: t("system.base.oauth_provider.placeholder.icon") } },
|
|
105
|
-
{ prop: "client_id", label: t("system.base.oauth_provider.field.client_id"), component: "input", colSpan: 12, props: { maxlength: 255, placeholder: t("system.base.oauth_provider.placeholder.client_id") } },
|
|
106
|
-
{ prop: "client_secret", label: t("system.base.oauth_provider.field.client_secret"), component: "input", colSpan: 12, props: { type: "password", showPassword: true, maxlength: 512, placeholder: dialog.editing ? t("system.base.oauth_provider.placeholder.keep_secret") : t("system.base.oauth_provider.placeholder.client_secret") } },
|
|
105
|
+
{ prop: "client_id", label: t("system.base.oauth_provider.field.client_id"), component: "input", colSpan: 12, props: { maxlength: 255, autocomplete: "off", placeholder: t("system.base.oauth_provider.placeholder.client_id") } },
|
|
106
|
+
{ prop: "client_secret", label: t("system.base.oauth_provider.field.client_secret"), component: "input", colSpan: 12, props: { type: "password", showPassword: true, maxlength: 512, autocomplete: "new-password", placeholder: dialog.editing ? t("system.base.oauth_provider.placeholder.keep_secret") : t("system.base.oauth_provider.placeholder.client_secret") } },
|
|
107
107
|
{ prop: "redirect_uri", label: t("system.base.oauth_provider.field.redirect_uri"), component: "input", colSpan: 12, props: { maxlength: 512, placeholder: t("system.base.oauth_provider.placeholder.redirect_uri") } },
|
|
108
108
|
{ prop: "scopes", label: t("system.base.oauth_provider.field.scopes"), component: "dynamic-list", colSpan: 12, props: { inputProps: { maxlength: 128, placeholder: t("system.base.oauth_provider.placeholder.scopes") } } },
|
|
109
109
|
{ prop: "config_items", label: t("system.base.oauth_provider.field.config"), component: "kv-list", colSpan: 12, props: { keyInputProps: { maxlength: 128 } } },
|
|
@@ -150,9 +150,12 @@ const columns = computed<ColumnProps[]>(() => [
|
|
|
150
150
|
showOverflowTooltip: true,
|
|
151
151
|
render: scope => {
|
|
152
152
|
const row = scope.row as BaseTenantProjectGrant;
|
|
153
|
-
|
|
153
|
+
// proto3 JSON 会省略空数组字段,project_id / project_names 可能为 undefined。
|
|
154
|
+
const projectIds = row.project_id ?? [];
|
|
155
|
+
const projectNames = row.project_names ?? [];
|
|
156
|
+
return projectIds.length === 1 && projectIds[0] === 0
|
|
154
157
|
? t("system.base.tenant_project_grant.all_projects")
|
|
155
|
-
:
|
|
158
|
+
: projectNames.join(", ") || projectIds.join(", ") || t("common.value.none");
|
|
156
159
|
}
|
|
157
160
|
},
|
|
158
161
|
{
|
|
@@ -213,12 +216,14 @@ async function handleOpenDialog(row?: BaseTenantProjectGrant) {
|
|
|
213
216
|
await formDialogRef.value?.open({
|
|
214
217
|
load: async () => {
|
|
215
218
|
await loadTenantOptions(true);
|
|
219
|
+
// proto3 JSON 会省略空数组字段,行数据里的 project_id 可能为 undefined。
|
|
220
|
+
const rowProjectIds = row?.project_id ?? [];
|
|
216
221
|
const nextForm = {
|
|
217
222
|
tenant_id: row?.tenant_id ?? (isDefaultTenant.value ? undefined : Number(tenantOptions.value[0]?.value) || undefined),
|
|
218
223
|
subject_type: row?.subject_type ?? DEFAULT_SUBJECT_TYPE,
|
|
219
224
|
subject_id: row?.subject_id,
|
|
220
|
-
project_id: row && !(
|
|
221
|
-
all: Boolean(row &&
|
|
225
|
+
project_id: row && !(rowProjectIds.length === 1 && rowProjectIds[0] === 0) ? [...rowProjectIds] : [],
|
|
226
|
+
all: Boolean(row && rowProjectIds.length === 1 && rowProjectIds[0] === 0)
|
|
222
227
|
} satisfies ProjectGrantFormState;
|
|
223
228
|
const [projects, subjects] = await Promise.all([
|
|
224
229
|
requestProjectOptions(nextForm.tenant_id),
|
|
@@ -74,6 +74,7 @@ const codeGenFormComponentLabelKeys: Record<ProFormComponentType, string> = {
|
|
|
74
74
|
input: "system.code.gen.component.input",
|
|
75
75
|
password: "system.code.gen.component.password",
|
|
76
76
|
textarea: "system.code.gen.component.textarea",
|
|
77
|
+
"yaml-editor": "system.code.gen.component.yaml_editor",
|
|
77
78
|
"input-number": "system.code.gen.component.input_number",
|
|
78
79
|
"color-picker": "system.code.gen.component.color_picker",
|
|
79
80
|
segmented: "system.code.gen.component.segmented",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liujitcn/kratos-admin-system",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.66",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "kratos-admin system management module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"sortablejs": "^1.15.2",
|
|
17
17
|
"swagger-ui-dist": "^5.32.15",
|
|
18
18
|
"vue-element-plus-x": "^2.0.3",
|
|
19
|
-
"@liujitcn/kratos-admin-core": "0.0.
|
|
19
|
+
"@liujitcn/kratos-admin-core": "0.0.66"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@element-plus/icons-vue": "^2.3.1",
|