@liujitcn/kratos-admin-system 0.0.65 → 0.0.67
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 +1805 -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/base/v1/login.d.ts +2 -39
- package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +2 -2
- package/dist/package/declarations/src/rpc/common/v1/types.d.ts +4 -4
- package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +66 -7
- 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/rpc/system/admin/v1/auth.d.ts +3 -3
- package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +4 -3
- package/dist/package/declarations/src/rpc/system/admin/v1/base_login_policy.d.ts +2 -2
- package/dist/package/declarations/src/rpc/system/admin/v1/base_message_provider.d.ts +3 -2
- package/dist/package/declarations/src/rpc/system/admin/v1/base_oauth_provider.d.ts +3 -2
- package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +3 -3
- 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 +7 -5
- package/dist/package/src/components/tenant-project/TenantProjectSelect.vue +21 -4
- package/dist/package/src/locales/README.md +1 -1
- package/dist/package/src/locales/en-US.json +355 -292
- package/dist/package/src/locales/ja-JP.json +352 -289
- package/dist/package/src/locales/zh-CN.json +352 -289
- package/dist/package/src/locales/zh-TW.json +352 -289
- package/dist/package/src/module.ts +8 -0
- 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/base/v1/login.ts +2 -42
- package/dist/package/src/rpc/base/v1/oauth.ts +2 -2
- package/dist/package/src/rpc/common/v1/types.ts +4 -4
- package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +73 -7
- 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/rpc/system/admin/v1/auth.ts +3 -3
- package/dist/package/src/rpc/system/admin/v1/base_config.ts +6 -3
- package/dist/package/src/rpc/system/admin/v1/base_login_policy.ts +2 -2
- package/dist/package/src/rpc/system/admin/v1/base_message_provider.ts +5 -2
- package/dist/package/src/rpc/system/admin/v1/base_oauth_provider.ts +5 -2
- package/dist/package/src/rpc/system/admin/v1/base_user.ts +3 -3
- package/dist/package/src/views/ai/chat/components/ChatPanel.vue +18 -0
- package/dist/package/src/views/ai/chat/components/XSender.vue +102 -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 +216 -169
- package/dist/package/src/views/base/config/index.vue +9 -9
- package/dist/package/src/views/base/login-policy/index.vue +2 -2
- package/dist/package/src/views/base/message-provider/index.vue +7 -5
- package/dist/package/src/views/base/oauth-provider/index.vue +10 -7
- package/dist/package/src/views/base/tenant-project-grant/index.vue +9 -4
- package/dist/package/src/views/base/user/index.vue +3 -3
- package/dist/package/src/views/profile/components/security.vue +4 -5
- package/dist/package/src/views/tool/code-gen/config.ts +2 -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"
|
|
@@ -111,12 +119,15 @@ import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
|
|
|
111
119
|
import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
|
|
112
120
|
import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
|
|
113
121
|
import { t } from "@liujitcn/kratos-admin-core";
|
|
122
|
+
import { encodeSecretFields } from "@liujitcn/kratos-admin-core/security";
|
|
114
123
|
import { defAiProviderService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/ai_provider";
|
|
115
|
-
import
|
|
116
|
-
AiProvider,
|
|
117
|
-
AiProviderForm,
|
|
124
|
+
import {
|
|
125
|
+
type AiProvider,
|
|
126
|
+
type AiProviderForm,
|
|
127
|
+
AiModelCategory,
|
|
128
|
+
type AiProviderModelForm,
|
|
118
129
|
AiProviderModelTestResult,
|
|
119
|
-
PageAiProviderRequest
|
|
130
|
+
type PageAiProviderRequest
|
|
120
131
|
} from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/ai_provider";
|
|
121
132
|
import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
|
|
122
133
|
|
|
@@ -128,30 +139,26 @@ interface ConfigItem {
|
|
|
128
139
|
value: string;
|
|
129
140
|
}
|
|
130
141
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/** 单个AI模型的可编辑配置。 */
|
|
135
|
-
interface AiModelConfig {
|
|
136
|
-
/** 服务商侧的模型名称。 */
|
|
137
|
-
model_name: string;
|
|
138
|
-
/** 模型在聊天前端的展示名称,留空时由前端按模型名自动映射。 */
|
|
139
|
-
display_name: string;
|
|
140
|
-
/** 模型请求接口类型。 */
|
|
141
|
-
api_type: ModelApiType;
|
|
142
|
-
/** 文本生成温度。 */
|
|
142
|
+
/** 单个AI模型的页面表单状态;分类个性化配置先拍平成兄弟字段,提交时再收敛进 config。 */
|
|
143
|
+
interface ModelFormState extends Omit<AiProviderModelForm, "config"> {
|
|
144
|
+
api_type: string;
|
|
143
145
|
temperature: number;
|
|
144
|
-
/** 单次请求最大输出Token数。 */
|
|
145
146
|
max_tokens: number;
|
|
146
|
-
|
|
147
|
+
dimensions: number;
|
|
148
|
+
duration: number;
|
|
149
|
+
size: string;
|
|
150
|
+
quality: string;
|
|
151
|
+
resolution: string;
|
|
152
|
+
task: string;
|
|
153
|
+
voice: string;
|
|
154
|
+
speed: number;
|
|
147
155
|
timeout_seconds: number;
|
|
148
|
-
/** 模型请求失败后的最大重试次数。 */
|
|
149
156
|
max_retries: number;
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
/** AI Provider页面表单状态。 */
|
|
153
|
-
interface FormState extends Omit<AiProviderForm, "
|
|
154
|
-
models:
|
|
160
|
+
interface FormState extends Omit<AiProviderForm, "models"> {
|
|
161
|
+
models: ModelFormState[];
|
|
155
162
|
config_items: ConfigItem[];
|
|
156
163
|
}
|
|
157
164
|
|
|
@@ -163,7 +170,7 @@ const modelDialogRef = ref<InstanceType<typeof FormDialog>>();
|
|
|
163
170
|
const dialog = reactive({ visible: false, editing: false });
|
|
164
171
|
const modelDialog = reactive({ visible: false, editing: false, index: -1 });
|
|
165
172
|
const form = reactive<FormState>(defaultForm());
|
|
166
|
-
const modelForm = reactive<
|
|
173
|
+
const modelForm = reactive<ModelFormState>(defaultModelForm());
|
|
167
174
|
const testingModels = ref(false);
|
|
168
175
|
const modelTestResults = ref<Record<string, AiProviderModelTestResult>>({});
|
|
169
176
|
|
|
@@ -184,44 +191,88 @@ const providerOptions = computed<ProFormOption[]>(() => [
|
|
|
184
191
|
{ label: t("system.base.ai_provider.provider.ollama"), value: "ollama" }
|
|
185
192
|
]);
|
|
186
193
|
|
|
187
|
-
const
|
|
188
|
-
{ label: t("system.base.
|
|
189
|
-
{ label: t("system.base.
|
|
194
|
+
const categoryOptions = computed<ProFormOption[]>(() => [
|
|
195
|
+
{ label: t("system.base.ai.model.category.chat"), value: AiModelCategory.AI_MODEL_CATEGORY_CHAT },
|
|
196
|
+
{ label: t("system.base.ai.model.category.embedding"), value: AiModelCategory.AI_MODEL_CATEGORY_EMBEDDING },
|
|
197
|
+
{ label: t("system.base.ai.model.category.rerank"), value: AiModelCategory.AI_MODEL_CATEGORY_RERANK },
|
|
198
|
+
{ label: t("system.base.ai.model.category.image"), value: AiModelCategory.AI_MODEL_CATEGORY_IMAGE },
|
|
199
|
+
{ label: t("system.base.ai.model.category.video"), value: AiModelCategory.AI_MODEL_CATEGORY_VIDEO },
|
|
200
|
+
{ label: t("system.base.ai.model.category.audio"), value: AiModelCategory.AI_MODEL_CATEGORY_AUDIO }
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
const apiTypeOptions = computed<ProFormOption[]>(() => [
|
|
204
|
+
{ label: t("system.base.ai.model.api_type.chat_completions"), value: "CHAT_COMPLETIONS" },
|
|
205
|
+
{ label: t("system.base.ai.model.api_type.responses"), value: "RESPONSES" }
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const taskOptions = computed<ProFormOption[]>(() => [
|
|
209
|
+
{ label: t("system.base.ai.model.task.tts"), value: "tts" },
|
|
210
|
+
{ label: t("system.base.ai.model.task.asr"), value: "asr" }
|
|
190
211
|
]);
|
|
191
212
|
|
|
213
|
+
/** 分类个性化配置的表单字段,按分类过滤展示。 */
|
|
214
|
+
const categoryConfigFields: Partial<Record<AiModelCategory, string[]>> = {
|
|
215
|
+
[AiModelCategory.AI_MODEL_CATEGORY_CHAT]: ["api_type", "temperature", "max_tokens", "timeout_seconds", "max_retries"],
|
|
216
|
+
[AiModelCategory.AI_MODEL_CATEGORY_EMBEDDING]: ["dimensions", "timeout_seconds", "max_retries"],
|
|
217
|
+
[AiModelCategory.AI_MODEL_CATEGORY_RERANK]: ["timeout_seconds", "max_retries"],
|
|
218
|
+
[AiModelCategory.AI_MODEL_CATEGORY_IMAGE]: ["size", "quality", "timeout_seconds", "max_retries"],
|
|
219
|
+
[AiModelCategory.AI_MODEL_CATEGORY_VIDEO]: ["duration", "resolution", "timeout_seconds", "max_retries"],
|
|
220
|
+
[AiModelCategory.AI_MODEL_CATEGORY_AUDIO]: ["task", "voice", "speed", "timeout_seconds", "max_retries"]
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** 全部配置字段的表单项定义。 */
|
|
224
|
+
const configFieldMap: Record<string, ProFormField> = {
|
|
225
|
+
api_type: { prop: "api_type", label: t("system.base.ai.model.field.api_type"), component: "select", colSpan: 12, options: apiTypeOptions.value },
|
|
226
|
+
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" } },
|
|
227
|
+
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" } },
|
|
228
|
+
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") } },
|
|
229
|
+
duration: { prop: "duration", label: t("system.base.ai.model.field.duration"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0, controlsPosition: "right" } },
|
|
230
|
+
size: { prop: "size", label: t("system.base.ai.model.field.size"), component: "input", colSpan: 12, props: { maxlength: 50, placeholder: "1024x1024" } },
|
|
231
|
+
quality: { prop: "quality", label: t("system.base.ai.model.field.quality"), component: "input", colSpan: 12, props: { maxlength: 50 } },
|
|
232
|
+
resolution: { prop: "resolution", label: t("system.base.ai.model.field.resolution"), component: "input", colSpan: 12, props: { maxlength: 50, placeholder: "1920x1080" } },
|
|
233
|
+
task: { prop: "task", label: t("system.base.ai.model.field.task"), component: "select", colSpan: 12, options: taskOptions.value },
|
|
234
|
+
voice: { prop: "voice", label: t("system.base.ai.model.field.voice"), component: "input", colSpan: 12, props: { maxlength: 100 } },
|
|
235
|
+
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" } },
|
|
236
|
+
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" } },
|
|
237
|
+
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" } }
|
|
238
|
+
};
|
|
239
|
+
|
|
192
240
|
const fields = computed<ProFormField[]>(() => [
|
|
193
241
|
{ 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") } },
|
|
242
|
+
{ prop: "name", label: t("system.base.ai_provider.field.name"), component: "input", colSpan: 12, props: { maxlength: 100, autocomplete: "off" } },
|
|
243
|
+
{ 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") } },
|
|
244
|
+
{ prop: "api_key", label: t("system.base.ai_provider.field.api_key"), component: "input", colSpan: 12, secret: { resource: "ai_provider", field: "api_key" }, 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
245
|
{ prop: "models", label: t("system.base.ai_provider.field.models"), component: "slot", slotName: "models", colSpan: 24 },
|
|
198
246
|
{ prop: "config_items", label: t("system.base.ai_provider.field.config"), component: "kv-list", colSpan: 24, props: { keyInputProps: { maxlength: 128 } } },
|
|
199
247
|
{ prop: "sort", label: t("common.field.sort"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0 } },
|
|
200
248
|
{ prop: "status", label: t("common.field.status"), component: "radio-group", colSpan: 12, options: statusOptions.value }
|
|
201
249
|
]);
|
|
202
250
|
|
|
203
|
-
const modelFields = computed<ProFormField[]>(() =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
251
|
+
const modelFields = computed<ProFormField[]>(() => {
|
|
252
|
+
const category = modelForm.category;
|
|
253
|
+
const configFields = (categoryConfigFields[category] ?? []).map(key => configFieldMap[key]);
|
|
254
|
+
return [
|
|
255
|
+
{ prop: "model_name", label: t("system.base.ai.model.field.model_name"), component: "input", colSpan: 12, props: { maxlength: 200, autocomplete: "off" } },
|
|
256
|
+
{ 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") } },
|
|
257
|
+
{ prop: "category", label: t("system.base.ai.model.field.category"), component: "select", colSpan: 12, options: categoryOptions.value },
|
|
258
|
+
{ prop: "sort", label: t("common.field.sort"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0 } },
|
|
259
|
+
{ prop: "status", label: t("common.field.status"), component: "radio-group", colSpan: 12, options: statusOptions.value },
|
|
260
|
+
...configFields
|
|
261
|
+
];
|
|
262
|
+
});
|
|
212
263
|
|
|
213
264
|
const rules = computed<FormRules>(() => ({
|
|
214
265
|
name: [{ required: true, message: t("system.base.ai_provider.validation.name"), trigger: "blur" }],
|
|
215
266
|
provider: [{ required: true, message: t("system.base.ai_provider.validation.provider"), trigger: "change" }],
|
|
216
267
|
models: [{
|
|
217
|
-
validator: (_rule, value:
|
|
268
|
+
validator: (_rule, value: ModelFormState[], callback) => {
|
|
218
269
|
if (!Array.isArray(value) || value.length === 0) {
|
|
219
270
|
callback(new Error(t("system.base.ai_provider.model.validation.required")));
|
|
220
271
|
return;
|
|
221
272
|
}
|
|
222
273
|
const names = value.map(model => model.model_name);
|
|
223
274
|
callback(names.some(name => !name)
|
|
224
|
-
? new Error(t("system.base.
|
|
275
|
+
? new Error(t("system.base.ai.model.validation.model_name"))
|
|
225
276
|
: new Set(names).size !== names.length
|
|
226
277
|
? new Error(t("system.base.ai_provider.model.validation.duplicate"))
|
|
227
278
|
: undefined);
|
|
@@ -232,15 +283,10 @@ const rules = computed<FormRules>(() => ({
|
|
|
232
283
|
|
|
233
284
|
const modelRules = computed<FormRules>(() => ({
|
|
234
285
|
model_name: [
|
|
235
|
-
{ required: true, message: t("system.base.
|
|
236
|
-
{ max:
|
|
286
|
+
{ required: true, message: t("system.base.ai.model.validation.model_name"), trigger: "blur" },
|
|
287
|
+
{ max: 200, message: t("system.base.ai.model.validation.model_name_length"), trigger: "blur" }
|
|
237
288
|
],
|
|
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" }]
|
|
289
|
+
category: [{ required: true, message: t("system.base.ai.model.validation.category"), trigger: "change" }]
|
|
244
290
|
}));
|
|
245
291
|
|
|
246
292
|
const columns = computed<ColumnProps[]>(() => [
|
|
@@ -289,7 +335,7 @@ function defaultForm(): FormState {
|
|
|
289
335
|
provider: "openai_compatible",
|
|
290
336
|
name: "",
|
|
291
337
|
base_url: "",
|
|
292
|
-
api_key:
|
|
338
|
+
api_key: undefined,
|
|
293
339
|
api_key_configured: false,
|
|
294
340
|
models: [],
|
|
295
341
|
config: {},
|
|
@@ -299,6 +345,43 @@ function defaultForm(): FormState {
|
|
|
299
345
|
};
|
|
300
346
|
}
|
|
301
347
|
|
|
348
|
+
/** 创建默认模型表单;各分类配置字段都先给默认值,切换分类时无需重置。 */
|
|
349
|
+
function defaultModelForm(): ModelFormState {
|
|
350
|
+
return {
|
|
351
|
+
id: 0,
|
|
352
|
+
model_name: "",
|
|
353
|
+
display_name: "",
|
|
354
|
+
category: AiModelCategory.AI_MODEL_CATEGORY_CHAT,
|
|
355
|
+
sort: 0,
|
|
356
|
+
status: Status.STATUS_ENABLE,
|
|
357
|
+
api_type: "CHAT_COMPLETIONS",
|
|
358
|
+
temperature: 0.2,
|
|
359
|
+
max_tokens: 1024,
|
|
360
|
+
dimensions: 0,
|
|
361
|
+
duration: 0,
|
|
362
|
+
size: "",
|
|
363
|
+
quality: "",
|
|
364
|
+
resolution: "",
|
|
365
|
+
task: "tts",
|
|
366
|
+
voice: "",
|
|
367
|
+
speed: 0,
|
|
368
|
+
timeout_seconds: 60,
|
|
369
|
+
max_retries: 2
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** 模型分类显示名。 */
|
|
374
|
+
function categoryLabel(category: AiModelCategory) {
|
|
375
|
+
const option = categoryOptions.value.find(item => item.value === category);
|
|
376
|
+
return option?.label ?? String(category);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** 接口类型显示名。 */
|
|
380
|
+
function apiTypeLabel(apiType: string) {
|
|
381
|
+
const option = apiTypeOptions.value.find(item => item.value === apiType);
|
|
382
|
+
return option?.label ?? apiType;
|
|
383
|
+
}
|
|
384
|
+
|
|
302
385
|
/** 请求AI供应商表格数据。 */
|
|
303
386
|
async function requestTable(params: Record<string, unknown>) {
|
|
304
387
|
const data = await defAiProviderService.PageAiProvider(buildPageRequest<PageAiProviderRequest>(params as unknown as PageAiProviderRequest));
|
|
@@ -311,9 +394,8 @@ async function openDialog(id?: number) {
|
|
|
311
394
|
load: () => (id ? defAiProviderService.GetAiProvider({ id }) : undefined),
|
|
312
395
|
commit: value => {
|
|
313
396
|
if (value) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
models: parseModelConfigs(models_json),
|
|
397
|
+
Object.assign(form, defaultForm(), value, {
|
|
398
|
+
models: (value.models ?? []).map(model => flattenModel(model)),
|
|
317
399
|
config_items: configToItems(value.config)
|
|
318
400
|
});
|
|
319
401
|
} else {
|
|
@@ -324,11 +406,11 @@ async function openDialog(id?: number) {
|
|
|
324
406
|
});
|
|
325
407
|
}
|
|
326
408
|
|
|
327
|
-
/** 提交AI
|
|
409
|
+
/** 提交AI供应商表单(供应商与模型两张表随同一请求保存)。 */
|
|
328
410
|
async function submit() {
|
|
329
411
|
const valid = await dialogRef.value?.validate();
|
|
330
412
|
if (!valid) return;
|
|
331
|
-
const payload = buildAiProviderPayload();
|
|
413
|
+
const payload = buildAiProviderPayload(await encodeSecretFields(fields.value, form));
|
|
332
414
|
if (!payload) return;
|
|
333
415
|
if (dialog.editing) await defAiProviderService.UpdateAiProvider({ ai_provider: payload });
|
|
334
416
|
else await defAiProviderService.CreateAiProvider({ ai_provider: payload });
|
|
@@ -342,7 +424,7 @@ async function submit() {
|
|
|
342
424
|
async function testProviderModels() {
|
|
343
425
|
const valid = await dialogRef.value?.validate();
|
|
344
426
|
if (!valid) return;
|
|
345
|
-
const payload = buildAiProviderPayload();
|
|
427
|
+
const payload = buildAiProviderPayload(await encodeSecretFields(fields.value, form));
|
|
346
428
|
if (!payload) return;
|
|
347
429
|
|
|
348
430
|
testingModels.value = true;
|
|
@@ -356,37 +438,66 @@ async function testProviderModels() {
|
|
|
356
438
|
modelTestResults.value = Object.fromEntries(response.results.map(result => [result.model_name, result]));
|
|
357
439
|
}
|
|
358
440
|
|
|
359
|
-
/** 构造AI Provider
|
|
360
|
-
function buildAiProviderPayload(): AiProviderForm | undefined {
|
|
361
|
-
const config = configItemsToMap(
|
|
441
|
+
/** 构造AI Provider保存或测试请求参数,source 为加密转换后的提交模型。 */
|
|
442
|
+
function buildAiProviderPayload(source: Record<string, any>): AiProviderForm | undefined {
|
|
443
|
+
const config = configItemsToMap(source.config_items);
|
|
362
444
|
if (!config) return undefined;
|
|
363
445
|
return {
|
|
364
|
-
id:
|
|
365
|
-
provider:
|
|
366
|
-
name:
|
|
367
|
-
base_url:
|
|
368
|
-
api_key:
|
|
369
|
-
api_key_configured:
|
|
370
|
-
|
|
446
|
+
id: source.id,
|
|
447
|
+
provider: source.provider,
|
|
448
|
+
name: source.name,
|
|
449
|
+
base_url: source.base_url,
|
|
450
|
+
api_key: source.api_key,
|
|
451
|
+
api_key_configured: source.api_key_configured,
|
|
452
|
+
models: source.models.map(model => buildModelPayload(model)),
|
|
453
|
+
config,
|
|
454
|
+
sort: source.sort,
|
|
455
|
+
status: source.status
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** 将页面模型表单收敛为保存或测试请求参数。 */
|
|
460
|
+
function buildModelPayload(model: ModelFormState): AiProviderModelForm {
|
|
461
|
+
const category = model.category;
|
|
462
|
+
const config: Record<string, unknown> = {};
|
|
463
|
+
for (const key of categoryConfigFields[category] ?? []) {
|
|
464
|
+
const value = model[key as keyof ModelFormState];
|
|
465
|
+
if (value !== undefined && value !== "") config[key] = value;
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
id: model.id,
|
|
469
|
+
model_name: model.model_name,
|
|
470
|
+
display_name: model.display_name,
|
|
471
|
+
category: model.category,
|
|
371
472
|
config,
|
|
372
|
-
sort:
|
|
373
|
-
status:
|
|
473
|
+
sort: model.sort,
|
|
474
|
+
status: model.status
|
|
374
475
|
};
|
|
375
476
|
}
|
|
376
477
|
|
|
478
|
+
/** 将接口返回的模型表单拍平为页面状态。 */
|
|
479
|
+
function flattenModel(model: AiProviderModelForm): ModelFormState {
|
|
480
|
+
const state = { ...defaultModelForm(), ...model, config: undefined } as unknown as ModelFormState;
|
|
481
|
+
for (const [key, value] of Object.entries(model.config ?? {})) {
|
|
482
|
+
if (key in state) (state as unknown as Record<string, unknown>)[key] = value;
|
|
483
|
+
}
|
|
484
|
+
return state;
|
|
485
|
+
}
|
|
486
|
+
|
|
377
487
|
/** 重置供应商表单。 */
|
|
378
488
|
function resetForm() {
|
|
379
489
|
Object.assign(form, defaultForm());
|
|
380
490
|
dialog.editing = false;
|
|
381
491
|
modelDialog.visible = false;
|
|
382
492
|
resetModelDialog();
|
|
493
|
+
modelTestResults.value = {};
|
|
383
494
|
}
|
|
384
495
|
|
|
385
496
|
/** 打开模型新增或编辑表单。 */
|
|
386
497
|
function openModelDialog(index?: number) {
|
|
387
498
|
modelDialog.index = index ?? -1;
|
|
388
499
|
modelDialog.editing = index !== undefined;
|
|
389
|
-
Object.assign(modelForm, index === undefined ?
|
|
500
|
+
Object.assign(modelForm, index === undefined ? defaultModelForm() : { ...form.models[index] });
|
|
390
501
|
modelDialog.visible = true;
|
|
391
502
|
}
|
|
392
503
|
|
|
@@ -394,7 +505,7 @@ function openModelDialog(index?: number) {
|
|
|
394
505
|
async function saveModel() {
|
|
395
506
|
const valid = await modelDialogRef.value?.validate();
|
|
396
507
|
if (!valid) return;
|
|
397
|
-
const duplicated = form.models.some((model, index) => index !== modelDialog.index && model.model_name === modelForm.model_name);
|
|
508
|
+
const duplicated = form.models.some((model, index) => index !== modelDialog.index && model.model_name === modelForm.model_name.trim());
|
|
398
509
|
if (duplicated) {
|
|
399
510
|
ElMessage.warning(t("system.base.ai_provider.model.validation.duplicate"));
|
|
400
511
|
return;
|
|
@@ -418,39 +529,14 @@ async function removeModel(index: number) {
|
|
|
418
529
|
}
|
|
419
530
|
}
|
|
420
531
|
|
|
421
|
-
/**
|
|
532
|
+
/** 重置模型表单。 */
|
|
422
533
|
function resetModelDialog() {
|
|
534
|
+
Object.assign(modelForm, defaultModelForm());
|
|
423
535
|
modelDialog.editing = false;
|
|
424
536
|
modelDialog.index = -1;
|
|
425
537
|
}
|
|
426
538
|
|
|
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配置对象转换为键值表单项。 */
|
|
539
|
+
/** 将供应商返回的配置对象转换为键值表单项。 */
|
|
454
540
|
function configToItems(config: Record<string, unknown> | undefined): ConfigItem[] {
|
|
455
541
|
return Object.entries(config ?? {}).map(([key, value]) => ({ key, value: typeof value === "string" ? value : JSON.stringify(value) }));
|
|
456
542
|
}
|
|
@@ -478,7 +564,7 @@ function configItemsToMap(items: ConfigItem[]): Record<string, unknown> | undefi
|
|
|
478
564
|
return result;
|
|
479
565
|
}
|
|
480
566
|
|
|
481
|
-
/** 批量删除AI
|
|
567
|
+
/** 批量删除AI供应商(级联删除其下模型)。 */
|
|
482
568
|
async function handleDelete(target: AiProvider | number | Array<number>) {
|
|
483
569
|
const row = typeof target === "object" && !Array.isArray(target) ? target : undefined;
|
|
484
570
|
const ids = (row ? [row.id] : normalizeSelectedIds(target as number | number[])).map(Number);
|
|
@@ -517,97 +603,58 @@ async function handleSetStatus(row: AiProvider) {
|
|
|
517
603
|
}
|
|
518
604
|
</script>
|
|
519
605
|
|
|
520
|
-
<style scoped>
|
|
606
|
+
<style scoped lang="scss">
|
|
521
607
|
.provider-models {
|
|
522
608
|
display: flex;
|
|
523
609
|
flex-direction: column;
|
|
524
|
-
align-items: flex-start;
|
|
525
610
|
gap: 12px;
|
|
526
611
|
width: 100%;
|
|
527
612
|
}
|
|
528
|
-
|
|
529
613
|
.provider-models__empty {
|
|
530
|
-
|
|
531
|
-
padding: 16px;
|
|
532
|
-
border: 1px dashed var(--el-border-color);
|
|
533
|
-
border-radius: 6px;
|
|
614
|
+
padding: 16px 0;
|
|
534
615
|
color: var(--el-text-color-secondary);
|
|
535
616
|
text-align: center;
|
|
617
|
+
background: var(--el-fill-color-light);
|
|
618
|
+
border-radius: 6px;
|
|
536
619
|
}
|
|
537
|
-
|
|
538
620
|
.provider-models__list {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
border-radius: 6px;
|
|
621
|
+
display: flex;
|
|
622
|
+
flex-direction: column;
|
|
623
|
+
gap: 8px;
|
|
543
624
|
}
|
|
544
|
-
|
|
545
625
|
.provider-models__item {
|
|
546
626
|
display: flex;
|
|
627
|
+
gap: 12px;
|
|
547
628
|
align-items: center;
|
|
548
629
|
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);
|
|
630
|
+
padding: 8px 12px;
|
|
631
|
+
background: var(--el-fill-color-light);
|
|
632
|
+
border-radius: 6px;
|
|
556
633
|
}
|
|
557
|
-
|
|
558
634
|
.provider-models__content {
|
|
559
635
|
display: flex;
|
|
560
636
|
flex-direction: column;
|
|
561
|
-
gap:
|
|
637
|
+
gap: 4px;
|
|
562
638
|
min-width: 0;
|
|
563
639
|
}
|
|
564
|
-
|
|
565
|
-
.provider-models__heading,
|
|
566
|
-
.provider-models__details,
|
|
567
|
-
.provider-models__actions {
|
|
640
|
+
.provider-models__heading {
|
|
568
641
|
display: flex;
|
|
642
|
+
gap: 8px;
|
|
569
643
|
align-items: center;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
.provider-models__heading {
|
|
573
644
|
flex-wrap: wrap;
|
|
574
|
-
gap: 8px;
|
|
575
|
-
min-width: 0;
|
|
576
645
|
}
|
|
577
|
-
|
|
578
|
-
.provider-models__heading strong {
|
|
579
|
-
overflow-wrap: anywhere;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
646
|
.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;
|
|
647
|
+
color: var(--el-text-color-secondary);
|
|
593
648
|
}
|
|
594
|
-
|
|
595
649
|
.provider-models__details {
|
|
650
|
+
display: flex;
|
|
651
|
+
gap: 12px;
|
|
596
652
|
flex-wrap: wrap;
|
|
597
|
-
gap: 4px 14px;
|
|
598
|
-
color: var(--el-text-color-secondary);
|
|
599
653
|
font-size: 12px;
|
|
654
|
+
color: var(--el-text-color-secondary);
|
|
600
655
|
}
|
|
601
|
-
|
|
602
656
|
.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
|
-
}
|
|
657
|
+
display: flex;
|
|
658
|
+
flex-shrink: 0;
|
|
612
659
|
}
|
|
613
660
|
</style>
|