@liujitcn/kratos-admin-system 0.0.64 → 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 +5 -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 +1405 -1089
- 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/ai_provider.d.ts +13 -6
- package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +85 -4
- 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/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/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 +14 -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 +351 -272
- package/dist/package/src/locales/ja-JP.json +351 -272
- package/dist/package/src/locales/zh-CN.json +361 -282
- package/dist/package/src/locales/zh-TW.json +351 -272
- 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/ai_provider.ts +14 -6
- package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +93 -4
- 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 +22 -1
- package/dist/package/src/views/ai/chat/components/XSender.vue +367 -71
- package/dist/package/src/views/ai/chat/index.vue +46 -8
- 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/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 +301 -154
- package/dist/package/src/views/base/api-rate-limit-policy/index.vue +4 -3
- package/dist/package/src/views/base/message-provider/index.vue +9 -5
- package/dist/package/src/views/base/message-template/index.vue +2 -2
- package/dist/package/src/views/base/oauth-provider/index.vue +2 -2
- package/dist/package/src/views/base/rate-limit-rule/index.vue +8 -3
- 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
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
:model="form"
|
|
11
11
|
:fields="fields"
|
|
12
12
|
:rules="rules"
|
|
13
|
+
:form-props="{ disabled: testingModels }"
|
|
14
|
+
:close-on-click-modal="!testingModels"
|
|
15
|
+
:close-on-press-escape="!testingModels"
|
|
13
16
|
@confirm="submit"
|
|
14
17
|
@close="resetForm"
|
|
15
18
|
>
|
|
@@ -23,39 +26,77 @@
|
|
|
23
26
|
<div class="provider-models__content">
|
|
24
27
|
<div class="provider-models__heading">
|
|
25
28
|
<strong>{{ model.model_name }}</strong>
|
|
26
|
-
<
|
|
29
|
+
<span v-if="model.display_name" class="provider-models__alias">✦ {{ model.display_name }}</span>
|
|
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>
|
|
34
|
+
<el-tooltip
|
|
35
|
+
v-if="modelTestResults[model.model_name]"
|
|
36
|
+
:content="modelTestResults[model.model_name]?.message"
|
|
37
|
+
:disabled="!modelTestResults[model.model_name]?.message"
|
|
38
|
+
placement="top"
|
|
39
|
+
>
|
|
40
|
+
<el-tag
|
|
41
|
+
size="small"
|
|
42
|
+
effect="plain"
|
|
43
|
+
:type="modelTestResults[model.model_name]?.success ? 'success' : 'danger'"
|
|
44
|
+
>
|
|
45
|
+
{{ t(modelTestResults[model.model_name]?.success
|
|
46
|
+
? "system.base.ai.model.action.test_success"
|
|
47
|
+
: "system.base.ai.model.action.test_failed") }} · {{ modelTestResults[model.model_name]?.duration_ms }} ms
|
|
48
|
+
</el-tag>
|
|
49
|
+
</el-tooltip>
|
|
27
50
|
</div>
|
|
28
51
|
<div class="provider-models__details">
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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>
|
|
33
62
|
</div>
|
|
34
63
|
</div>
|
|
35
64
|
<div class="provider-models__actions">
|
|
36
65
|
<el-tooltip :content="t('common.action.edit')" placement="top">
|
|
37
|
-
<el-button text circle :icon="EditPen" :aria-label="t('common.action.edit')" @click="openModelDialog(index)" />
|
|
66
|
+
<el-button text circle :disabled="testingModels" :icon="EditPen" :aria-label="t('common.action.edit')" @click="openModelDialog(index)" />
|
|
38
67
|
</el-tooltip>
|
|
39
68
|
<el-tooltip :content="t('common.action.delete')" placement="top">
|
|
40
|
-
<el-button text circle :icon="Delete" :aria-label="t('common.action.delete')" @click="removeModel(index)" />
|
|
69
|
+
<el-button text circle :disabled="testingModels" :icon="Delete" :aria-label="t('common.action.delete')" @click="removeModel(index)" />
|
|
41
70
|
</el-tooltip>
|
|
42
71
|
</div>
|
|
43
72
|
</div>
|
|
44
73
|
</div>
|
|
45
|
-
<el-button type="primary" plain :icon="CirclePlus" @click="openModelDialog()">
|
|
74
|
+
<el-button type="primary" plain :icon="CirclePlus" :disabled="testingModels" @click="openModelDialog()">
|
|
46
75
|
{{ t("system.base.ai_provider.model.add") }}
|
|
47
76
|
</el-button>
|
|
48
77
|
</div>
|
|
49
78
|
</template>
|
|
79
|
+
<template #footer>
|
|
80
|
+
<div class="dialog-footer">
|
|
81
|
+
<el-button :disabled="testingModels" @click="dialog.visible = false">
|
|
82
|
+
{{ t("common.action.cancel") }}
|
|
83
|
+
</el-button>
|
|
84
|
+
<el-button type="primary" plain :icon="Connection" :loading="testingModels" :disabled="!form.models.length" @click="testProviderModels">
|
|
85
|
+
{{ t("system.base.ai_provider.model.test") }}
|
|
86
|
+
</el-button>
|
|
87
|
+
<el-button type="primary" :disabled="testingModels" @click="submit">
|
|
88
|
+
{{ t("common.action.confirm") }}
|
|
89
|
+
</el-button>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
50
92
|
</FormDialog>
|
|
51
93
|
|
|
52
94
|
<FormDialog
|
|
53
95
|
v-model="modelDialog.visible"
|
|
54
96
|
ref="modelDialogRef"
|
|
55
97
|
:title="t(modelDialog.editing ? 'system.base.ai_provider.model.edit' : 'system.base.ai_provider.model.add')"
|
|
56
|
-
width="min(
|
|
57
|
-
label-width="
|
|
58
|
-
label-position="left"
|
|
98
|
+
width="min(720px, calc(100vw - 32px))"
|
|
99
|
+
label-width="10em"
|
|
59
100
|
:model="modelForm"
|
|
60
101
|
:fields="modelFields"
|
|
61
102
|
:rules="modelRules"
|
|
@@ -66,10 +107,10 @@
|
|
|
66
107
|
</template>
|
|
67
108
|
|
|
68
109
|
<script setup lang="ts">
|
|
69
|
-
import { computed, reactive, ref } from "vue";
|
|
110
|
+
import { computed, reactive, ref, watch } from "vue";
|
|
70
111
|
import { ElMessage, ElMessageBox } from "element-plus";
|
|
71
112
|
import type { FormRules } from "element-plus";
|
|
72
|
-
import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
|
|
113
|
+
import { CirclePlus, Connection, Delete, EditPen } from "@element-plus/icons-vue";
|
|
73
114
|
import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
|
|
74
115
|
import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
|
|
75
116
|
import type { ColumnProps, HeaderActionProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
|
|
@@ -79,10 +120,13 @@ import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-c
|
|
|
79
120
|
import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
|
|
80
121
|
import { t } from "@liujitcn/kratos-admin-core";
|
|
81
122
|
import { defAiProviderService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/ai_provider";
|
|
82
|
-
import
|
|
83
|
-
AiProvider,
|
|
84
|
-
AiProviderForm,
|
|
85
|
-
|
|
123
|
+
import {
|
|
124
|
+
type AiProvider,
|
|
125
|
+
type AiProviderForm,
|
|
126
|
+
AiModelCategory,
|
|
127
|
+
type AiProviderModelForm,
|
|
128
|
+
AiProviderModelTestResult,
|
|
129
|
+
type PageAiProviderRequest
|
|
86
130
|
} from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/ai_provider";
|
|
87
131
|
import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
|
|
88
132
|
|
|
@@ -94,28 +138,26 @@ interface ConfigItem {
|
|
|
94
138
|
value: string;
|
|
95
139
|
}
|
|
96
140
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/** 单个AI模型的可编辑配置。 */
|
|
101
|
-
interface AiModelConfig {
|
|
102
|
-
/** 服务商侧的模型名称。 */
|
|
103
|
-
model_name: string;
|
|
104
|
-
/** 模型请求接口类型。 */
|
|
105
|
-
api_type: ModelApiType;
|
|
106
|
-
/** 文本生成温度。 */
|
|
141
|
+
/** 单个AI模型的页面表单状态;分类个性化配置先拍平成兄弟字段,提交时再收敛进 config。 */
|
|
142
|
+
interface ModelFormState extends Omit<AiProviderModelForm, "config"> {
|
|
143
|
+
api_type: string;
|
|
107
144
|
temperature: number;
|
|
108
|
-
/** 单次请求最大输出Token数。 */
|
|
109
145
|
max_tokens: number;
|
|
110
|
-
|
|
146
|
+
dimensions: number;
|
|
147
|
+
duration: number;
|
|
148
|
+
size: string;
|
|
149
|
+
quality: string;
|
|
150
|
+
resolution: string;
|
|
151
|
+
task: string;
|
|
152
|
+
voice: string;
|
|
153
|
+
speed: number;
|
|
111
154
|
timeout_seconds: number;
|
|
112
|
-
/** 模型请求失败后的最大重试次数。 */
|
|
113
155
|
max_retries: number;
|
|
114
156
|
}
|
|
115
157
|
|
|
116
158
|
/** AI Provider页面表单状态。 */
|
|
117
|
-
interface FormState extends Omit<AiProviderForm, "
|
|
118
|
-
models:
|
|
159
|
+
interface FormState extends Omit<AiProviderForm, "models"> {
|
|
160
|
+
models: ModelFormState[];
|
|
119
161
|
config_items: ConfigItem[];
|
|
120
162
|
}
|
|
121
163
|
|
|
@@ -127,7 +169,16 @@ const modelDialogRef = ref<InstanceType<typeof FormDialog>>();
|
|
|
127
169
|
const dialog = reactive({ visible: false, editing: false });
|
|
128
170
|
const modelDialog = reactive({ visible: false, editing: false, index: -1 });
|
|
129
171
|
const form = reactive<FormState>(defaultForm());
|
|
130
|
-
const modelForm = reactive<
|
|
172
|
+
const modelForm = reactive<ModelFormState>(defaultModelForm());
|
|
173
|
+
const testingModels = ref(false);
|
|
174
|
+
const modelTestResults = ref<Record<string, AiProviderModelTestResult>>({});
|
|
175
|
+
|
|
176
|
+
watch(
|
|
177
|
+
() => [form.provider, form.base_url, form.api_key, JSON.stringify(form.config_items), JSON.stringify(form.models)],
|
|
178
|
+
() => {
|
|
179
|
+
modelTestResults.value = {};
|
|
180
|
+
}
|
|
181
|
+
);
|
|
131
182
|
|
|
132
183
|
const statusOptions = computed<ProFormOption[]>(() => [
|
|
133
184
|
{ label: t("common.status.enabled"), value: Status.STATUS_ENABLE },
|
|
@@ -139,43 +190,88 @@ const providerOptions = computed<ProFormOption[]>(() => [
|
|
|
139
190
|
{ label: t("system.base.ai_provider.provider.ollama"), value: "ollama" }
|
|
140
191
|
]);
|
|
141
192
|
|
|
142
|
-
const
|
|
143
|
-
{ label: t("system.base.
|
|
144
|
-
{ 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 }
|
|
145
200
|
]);
|
|
146
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" }
|
|
210
|
+
]);
|
|
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
|
+
|
|
147
239
|
const fields = computed<ProFormField[]>(() => [
|
|
148
240
|
{ prop: "provider", label: t("system.base.ai_provider.field.provider"), component: "select", colSpan: 12, options: providerOptions.value, props: { disabled: dialog.editing } },
|
|
149
|
-
{ prop: "name", label: t("system.base.ai_provider.field.name"), component: "input", colSpan: 12, props: { maxlength: 100 } },
|
|
150
|
-
{ 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") } },
|
|
151
|
-
{ 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") } },
|
|
152
244
|
{ prop: "models", label: t("system.base.ai_provider.field.models"), component: "slot", slotName: "models", colSpan: 24 },
|
|
153
245
|
{ prop: "config_items", label: t("system.base.ai_provider.field.config"), component: "kv-list", colSpan: 24, props: { keyInputProps: { maxlength: 128 } } },
|
|
154
246
|
{ prop: "sort", label: t("common.field.sort"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0 } },
|
|
155
247
|
{ prop: "status", label: t("common.field.status"), component: "radio-group", colSpan: 12, options: statusOptions.value }
|
|
156
248
|
]);
|
|
157
249
|
|
|
158
|
-
const modelFields = computed<ProFormField[]>(() =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
});
|
|
166
262
|
|
|
167
263
|
const rules = computed<FormRules>(() => ({
|
|
168
264
|
name: [{ required: true, message: t("system.base.ai_provider.validation.name"), trigger: "blur" }],
|
|
169
265
|
provider: [{ required: true, message: t("system.base.ai_provider.validation.provider"), trigger: "change" }],
|
|
170
266
|
models: [{
|
|
171
|
-
validator: (_rule, value:
|
|
267
|
+
validator: (_rule, value: ModelFormState[], callback) => {
|
|
172
268
|
if (!Array.isArray(value) || value.length === 0) {
|
|
173
269
|
callback(new Error(t("system.base.ai_provider.model.validation.required")));
|
|
174
270
|
return;
|
|
175
271
|
}
|
|
176
272
|
const names = value.map(model => model.model_name);
|
|
177
273
|
callback(names.some(name => !name)
|
|
178
|
-
? new Error(t("system.base.
|
|
274
|
+
? new Error(t("system.base.ai.model.validation.model_name"))
|
|
179
275
|
: new Set(names).size !== names.length
|
|
180
276
|
? new Error(t("system.base.ai_provider.model.validation.duplicate"))
|
|
181
277
|
: undefined);
|
|
@@ -186,14 +282,10 @@ const rules = computed<FormRules>(() => ({
|
|
|
186
282
|
|
|
187
283
|
const modelRules = computed<FormRules>(() => ({
|
|
188
284
|
model_name: [
|
|
189
|
-
{ required: true, message: t("system.base.
|
|
190
|
-
{ 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" }
|
|
191
287
|
],
|
|
192
|
-
|
|
193
|
-
temperature: [{ type: "number", required: true, min: 0, max: 2, message: t("system.base.ai_provider.model.validation.range"), trigger: "change" }],
|
|
194
|
-
max_tokens: [{ type: "number", required: true, min: 0, max: 200000, message: t("system.base.ai_provider.model.validation.range"), trigger: "change" }],
|
|
195
|
-
timeout_seconds: [{ type: "number", required: true, min: 0, max: 600, message: t("system.base.ai_provider.model.validation.range"), trigger: "change" }],
|
|
196
|
-
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" }]
|
|
197
289
|
}));
|
|
198
290
|
|
|
199
291
|
const columns = computed<ColumnProps[]>(() => [
|
|
@@ -252,6 +344,43 @@ function defaultForm(): FormState {
|
|
|
252
344
|
};
|
|
253
345
|
}
|
|
254
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
|
+
|
|
255
384
|
/** 请求AI供应商表格数据。 */
|
|
256
385
|
async function requestTable(params: Record<string, unknown>) {
|
|
257
386
|
const data = await defAiProviderService.PageAiProvider(buildPageRequest<PageAiProviderRequest>(params as unknown as PageAiProviderRequest));
|
|
@@ -264,9 +393,8 @@ async function openDialog(id?: number) {
|
|
|
264
393
|
load: () => (id ? defAiProviderService.GetAiProvider({ id }) : undefined),
|
|
265
394
|
commit: value => {
|
|
266
395
|
if (value) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
models: parseModelConfigs(models_json),
|
|
396
|
+
Object.assign(form, defaultForm(), value, {
|
|
397
|
+
models: (value.models ?? []).map(model => flattenModel(model)),
|
|
270
398
|
config_items: configToItems(value.config)
|
|
271
399
|
});
|
|
272
400
|
} else {
|
|
@@ -277,30 +405,82 @@ async function openDialog(id?: number) {
|
|
|
277
405
|
});
|
|
278
406
|
}
|
|
279
407
|
|
|
280
|
-
/** 提交AI
|
|
408
|
+
/** 提交AI供应商表单(供应商与模型两张表随同一请求保存)。 */
|
|
281
409
|
async function submit() {
|
|
282
410
|
const valid = await dialogRef.value?.validate();
|
|
283
411
|
if (!valid) return;
|
|
412
|
+
const payload = buildAiProviderPayload();
|
|
413
|
+
if (!payload) return;
|
|
414
|
+
if (dialog.editing) await defAiProviderService.UpdateAiProvider({ ai_provider: payload });
|
|
415
|
+
else await defAiProviderService.CreateAiProvider({ ai_provider: payload });
|
|
416
|
+
ElMessage.success(t(dialog.editing ? "system.base.ai_provider.message.update_success" : "system.base.ai_provider.message.create_success"));
|
|
417
|
+
dialog.visible = false;
|
|
418
|
+
resetForm();
|
|
419
|
+
table.value?.getTableList();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** 测试当前草稿中的全部模型并展示逐项结果。 */
|
|
423
|
+
async function testProviderModels() {
|
|
424
|
+
const valid = await dialogRef.value?.validate();
|
|
425
|
+
if (!valid) return;
|
|
426
|
+
const payload = buildAiProviderPayload();
|
|
427
|
+
if (!payload) return;
|
|
428
|
+
|
|
429
|
+
testingModels.value = true;
|
|
430
|
+
modelTestResults.value = {};
|
|
431
|
+
let response: Awaited<ReturnType<typeof defAiProviderService.TestAiProviderModels>>;
|
|
432
|
+
try {
|
|
433
|
+
response = await defAiProviderService.TestAiProviderModels({ ai_provider: payload });
|
|
434
|
+
} finally {
|
|
435
|
+
testingModels.value = false;
|
|
436
|
+
}
|
|
437
|
+
modelTestResults.value = Object.fromEntries(response.results.map(result => [result.model_name, result]));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** 构造AI Provider保存或测试请求参数。 */
|
|
441
|
+
function buildAiProviderPayload(): AiProviderForm | undefined {
|
|
284
442
|
const config = configItemsToMap(form.config_items);
|
|
285
|
-
if (!config) return;
|
|
286
|
-
|
|
443
|
+
if (!config) return undefined;
|
|
444
|
+
return {
|
|
287
445
|
id: form.id,
|
|
288
446
|
provider: form.provider,
|
|
289
447
|
name: form.name,
|
|
290
448
|
base_url: form.base_url,
|
|
291
449
|
api_key: form.api_key,
|
|
292
450
|
api_key_configured: form.api_key_configured,
|
|
293
|
-
|
|
451
|
+
models: form.models.map(model => buildModelPayload(model)),
|
|
294
452
|
config,
|
|
295
453
|
sort: form.sort,
|
|
296
454
|
status: form.status
|
|
297
455
|
};
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
456
|
+
}
|
|
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;
|
|
304
484
|
}
|
|
305
485
|
|
|
306
486
|
/** 重置供应商表单。 */
|
|
@@ -309,13 +489,14 @@ function resetForm() {
|
|
|
309
489
|
dialog.editing = false;
|
|
310
490
|
modelDialog.visible = false;
|
|
311
491
|
resetModelDialog();
|
|
492
|
+
modelTestResults.value = {};
|
|
312
493
|
}
|
|
313
494
|
|
|
314
495
|
/** 打开模型新增或编辑表单。 */
|
|
315
496
|
function openModelDialog(index?: number) {
|
|
316
497
|
modelDialog.index = index ?? -1;
|
|
317
498
|
modelDialog.editing = index !== undefined;
|
|
318
|
-
Object.assign(modelForm, index === undefined ?
|
|
499
|
+
Object.assign(modelForm, index === undefined ? defaultModelForm() : { ...form.models[index] });
|
|
319
500
|
modelDialog.visible = true;
|
|
320
501
|
}
|
|
321
502
|
|
|
@@ -323,7 +504,7 @@ function openModelDialog(index?: number) {
|
|
|
323
504
|
async function saveModel() {
|
|
324
505
|
const valid = await modelDialogRef.value?.validate();
|
|
325
506
|
if (!valid) return;
|
|
326
|
-
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());
|
|
327
508
|
if (duplicated) {
|
|
328
509
|
ElMessage.warning(t("system.base.ai_provider.model.validation.duplicate"));
|
|
329
510
|
return;
|
|
@@ -347,38 +528,14 @@ async function removeModel(index: number) {
|
|
|
347
528
|
}
|
|
348
529
|
}
|
|
349
530
|
|
|
350
|
-
/**
|
|
531
|
+
/** 重置模型表单。 */
|
|
351
532
|
function resetModelDialog() {
|
|
533
|
+
Object.assign(modelForm, defaultModelForm());
|
|
352
534
|
modelDialog.editing = false;
|
|
353
535
|
modelDialog.index = -1;
|
|
354
536
|
}
|
|
355
537
|
|
|
356
|
-
/**
|
|
357
|
-
function apiTypeLabel(apiType: ModelApiType) {
|
|
358
|
-
return t(apiType === "RESPONSES"
|
|
359
|
-
? "system.base.ai_provider.model.api_type.responses"
|
|
360
|
-
: "system.base.ai_provider.model.api_type.chat_completions");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** 将供应商返回的模型配置数组转换为表单数据。 */
|
|
364
|
-
function parseModelConfigs(raw: string): AiModelConfig[] {
|
|
365
|
-
const values = JSON.parse(raw) as Partial<AiModelConfig>[];
|
|
366
|
-
return values.map(value => ({
|
|
367
|
-
model_name: value.model_name ?? "",
|
|
368
|
-
api_type: value.api_type === "RESPONSES" ? "RESPONSES" : "CHAT_COMPLETIONS",
|
|
369
|
-
temperature: value.temperature ?? 0,
|
|
370
|
-
max_tokens: value.max_tokens ?? 0,
|
|
371
|
-
timeout_seconds: value.timeout_seconds ?? 0,
|
|
372
|
-
max_retries: value.max_retries ?? 0
|
|
373
|
-
}));
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** 创建单个模型的默认配置。 */
|
|
377
|
-
function defaultModelConfig(): AiModelConfig {
|
|
378
|
-
return { model_name: "", api_type: "CHAT_COMPLETIONS", temperature: 0.2, max_tokens: 1024, timeout_seconds: 60, max_retries: 2 };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** 将Provider配置对象转换为键值表单项。 */
|
|
538
|
+
/** 将供应商返回的配置对象转换为键值表单项。 */
|
|
382
539
|
function configToItems(config: Record<string, unknown> | undefined): ConfigItem[] {
|
|
383
540
|
return Object.entries(config ?? {}).map(([key, value]) => ({ key, value: typeof value === "string" ? value : JSON.stringify(value) }));
|
|
384
541
|
}
|
|
@@ -406,12 +563,16 @@ function configItemsToMap(items: ConfigItem[]): Record<string, unknown> | undefi
|
|
|
406
563
|
return result;
|
|
407
564
|
}
|
|
408
565
|
|
|
409
|
-
/** 批量删除AI
|
|
566
|
+
/** 批量删除AI供应商(级联删除其下模型)。 */
|
|
410
567
|
async function handleDelete(target: AiProvider | number | Array<number>) {
|
|
411
568
|
const row = typeof target === "object" && !Array.isArray(target) ? target : undefined;
|
|
412
569
|
const ids = (row ? [row.id] : normalizeSelectedIds(target as number | number[])).map(Number);
|
|
413
570
|
if (!ids.length) return;
|
|
414
|
-
|
|
571
|
+
try {
|
|
572
|
+
await ElMessageBox.confirm(t("system.base.ai_provider.message.confirm_delete", { name: row?.name ?? ids.join(", ") }), t("common.action.delete"), { type: "warning" });
|
|
573
|
+
} catch {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
415
576
|
for (const id of ids) await defAiProviderService.DeleteAiProvider({ id });
|
|
416
577
|
ElMessage.success(t("system.base.ai_provider.message.delete_success"));
|
|
417
578
|
table.value?.getTableList();
|
|
@@ -419,94 +580,80 @@ async function handleDelete(target: AiProvider | number | Array<number>) {
|
|
|
419
580
|
|
|
420
581
|
/** 更新AI供应商状态。 */
|
|
421
582
|
async function handleSetStatus(row: AiProvider) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
583
|
+
const status = row.status === Status.STATUS_ENABLE ? Status.STATUS_DISABLE : Status.STATUS_ENABLE;
|
|
584
|
+
try {
|
|
585
|
+
await ElMessageBox.confirm(
|
|
586
|
+
t("common.dialog.status_change", {
|
|
587
|
+
action: t(status === Status.STATUS_ENABLE ? "common.status.enabled" : "common.status.disabled"),
|
|
588
|
+
resource: t("system.base.ai_provider.resource"),
|
|
589
|
+
field: t("system.base.ai_provider.field.name"),
|
|
590
|
+
value: row.name
|
|
591
|
+
}),
|
|
592
|
+
t("common.title.notice"),
|
|
593
|
+
{ type: "warning" }
|
|
594
|
+
);
|
|
595
|
+
await defAiProviderService.SetAiProviderStatus({ id: row.id, status });
|
|
596
|
+
ElMessage.success(t("system.base.ai_provider.message.status_success"));
|
|
597
|
+
table.value?.getTableList();
|
|
598
|
+
return true;
|
|
599
|
+
} catch {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
429
602
|
}
|
|
430
603
|
</script>
|
|
431
604
|
|
|
432
|
-
<style scoped>
|
|
605
|
+
<style scoped lang="scss">
|
|
433
606
|
.provider-models {
|
|
434
607
|
display: flex;
|
|
435
608
|
flex-direction: column;
|
|
436
|
-
align-items: flex-start;
|
|
437
609
|
gap: 12px;
|
|
438
610
|
width: 100%;
|
|
439
611
|
}
|
|
440
|
-
|
|
441
612
|
.provider-models__empty {
|
|
442
|
-
|
|
443
|
-
padding: 16px;
|
|
444
|
-
border: 1px dashed var(--el-border-color);
|
|
445
|
-
border-radius: 6px;
|
|
613
|
+
padding: 16px 0;
|
|
446
614
|
color: var(--el-text-color-secondary);
|
|
447
615
|
text-align: center;
|
|
616
|
+
background: var(--el-fill-color-light);
|
|
617
|
+
border-radius: 6px;
|
|
448
618
|
}
|
|
449
|
-
|
|
450
619
|
.provider-models__list {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
border-radius: 6px;
|
|
620
|
+
display: flex;
|
|
621
|
+
flex-direction: column;
|
|
622
|
+
gap: 8px;
|
|
455
623
|
}
|
|
456
|
-
|
|
457
624
|
.provider-models__item {
|
|
458
625
|
display: flex;
|
|
626
|
+
gap: 12px;
|
|
459
627
|
align-items: center;
|
|
460
628
|
justify-content: space-between;
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
.provider-models__item + .provider-models__item {
|
|
467
|
-
border-top: 1px solid var(--el-border-color-lighter);
|
|
629
|
+
padding: 8px 12px;
|
|
630
|
+
background: var(--el-fill-color-light);
|
|
631
|
+
border-radius: 6px;
|
|
468
632
|
}
|
|
469
|
-
|
|
470
633
|
.provider-models__content {
|
|
471
634
|
display: flex;
|
|
472
635
|
flex-direction: column;
|
|
473
|
-
gap:
|
|
636
|
+
gap: 4px;
|
|
474
637
|
min-width: 0;
|
|
475
638
|
}
|
|
476
|
-
|
|
477
|
-
.provider-models__heading,
|
|
478
|
-
.provider-models__details,
|
|
479
|
-
.provider-models__actions {
|
|
639
|
+
.provider-models__heading {
|
|
480
640
|
display: flex;
|
|
641
|
+
gap: 8px;
|
|
481
642
|
align-items: center;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.provider-models__heading {
|
|
485
643
|
flex-wrap: wrap;
|
|
486
|
-
gap: 8px;
|
|
487
|
-
min-width: 0;
|
|
488
644
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
overflow-wrap: anywhere;
|
|
645
|
+
.provider-models__alias {
|
|
646
|
+
color: var(--el-text-color-secondary);
|
|
492
647
|
}
|
|
493
|
-
|
|
494
648
|
.provider-models__details {
|
|
649
|
+
display: flex;
|
|
650
|
+
gap: 12px;
|
|
495
651
|
flex-wrap: wrap;
|
|
496
|
-
gap: 4px 14px;
|
|
497
|
-
color: var(--el-text-color-secondary);
|
|
498
652
|
font-size: 12px;
|
|
653
|
+
color: var(--el-text-color-secondary);
|
|
499
654
|
}
|
|
500
|
-
|
|
501
655
|
.provider-models__actions {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
@media (max-width: 640px) {
|
|
507
|
-
.provider-models__item {
|
|
508
|
-
align-items: flex-start;
|
|
509
|
-
padding: 10px;
|
|
510
|
-
}
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-shrink: 0;
|
|
511
658
|
}
|
|
512
659
|
</style>
|