@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.
Files changed (50) hide show
  1. package/README.md +5 -3
  2. package/dist/package/declarations/src/api/base/v1/ai_knowledge.d.ts +7 -0
  3. package/dist/package/declarations/src/api/system/admin/v1/ai_provider.d.ts +5 -1
  4. package/dist/package/declarations/src/api/system/admin/v1/ai_query.d.ts +10 -0
  5. package/dist/package/declarations/src/api/system/admin/v1/ai_session.d.ts +9 -0
  6. package/dist/package/declarations/src/locales/generated.d.ts +1405 -1089
  7. package/dist/package/declarations/src/rpc/base/v1/ai_knowledge.d.ts +22 -0
  8. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +2 -0
  9. package/dist/package/declarations/src/rpc/base/v1/ai_provider.d.ts +13 -6
  10. package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +85 -4
  11. package/dist/package/declarations/src/rpc/system/admin/v1/ai_query.d.ts +73 -0
  12. package/dist/package/declarations/src/rpc/system/admin/v1/ai_session.d.ts +114 -0
  13. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +9 -2
  14. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +7 -0
  15. package/dist/package/declarations/src/views/ai/chat/modelDisplay.d.ts +5 -0
  16. package/dist/package/declarations/src/views/ai/chat/modelSelection.d.ts +9 -0
  17. package/dist/package/declarations/src/views/ai/query/index.vue.d.ts +3 -0
  18. package/dist/package/declarations/src/views/ai/session/index.vue.d.ts +3 -0
  19. package/dist/package/src/api/base/v1/ai_knowledge.ts +22 -0
  20. package/dist/package/src/api/system/admin/v1/ai_provider.ts +14 -0
  21. package/dist/package/src/api/system/admin/v1/ai_query.ts +25 -0
  22. package/dist/package/src/api/system/admin/v1/ai_session.ts +25 -0
  23. package/dist/package/src/components/account/ChangePasswordForm.vue +1 -1
  24. package/dist/package/src/components/tenant-project/TenantProjectSelect.vue +21 -4
  25. package/dist/package/src/locales/en-US.json +351 -272
  26. package/dist/package/src/locales/ja-JP.json +351 -272
  27. package/dist/package/src/locales/zh-CN.json +361 -282
  28. package/dist/package/src/locales/zh-TW.json +351 -272
  29. package/dist/package/src/rpc/base/v1/ai_knowledge.ts +33 -0
  30. package/dist/package/src/rpc/base/v1/ai_message.ts +2 -0
  31. package/dist/package/src/rpc/base/v1/ai_provider.ts +14 -6
  32. package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +93 -4
  33. package/dist/package/src/rpc/system/admin/v1/ai_query.ts +89 -0
  34. package/dist/package/src/rpc/system/admin/v1/ai_session.ts +131 -0
  35. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +22 -1
  36. package/dist/package/src/views/ai/chat/components/XSender.vue +367 -71
  37. package/dist/package/src/views/ai/chat/index.vue +46 -8
  38. package/dist/package/src/views/ai/chat/modelDisplay.ts +67 -0
  39. package/dist/package/src/views/ai/chat/modelSelection.ts +31 -0
  40. package/dist/package/src/views/ai/query/index.vue +222 -0
  41. package/dist/package/src/views/ai/session/index.vue +422 -0
  42. package/dist/package/src/views/base/ai-provider/index.vue +301 -154
  43. package/dist/package/src/views/base/api-rate-limit-policy/index.vue +4 -3
  44. package/dist/package/src/views/base/message-provider/index.vue +9 -5
  45. package/dist/package/src/views/base/message-template/index.vue +2 -2
  46. package/dist/package/src/views/base/oauth-provider/index.vue +2 -2
  47. package/dist/package/src/views/base/rate-limit-rule/index.vue +8 -3
  48. package/dist/package/src/views/base/tenant-project-grant/index.vue +9 -4
  49. package/dist/package/src/views/tool/code-gen/config.ts +1 -0
  50. 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
- <el-tag size="small" effect="plain">{{ apiTypeLabel(model.api_type) }}</el-tag>
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
- <span>{{ t("system.base.ai_provider.field.temperature") }}: {{ model.temperature }}</span>
30
- <span>{{ t("system.base.ai_provider.field.max_tokens") }}: {{ model.max_tokens }}</span>
31
- <span>{{ t("system.base.ai_provider.field.timeout_seconds") }}: {{ model.timeout_seconds }}</span>
32
- <span>{{ t("system.base.ai_provider.field.max_retries") }}: {{ model.max_retries }}</span>
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(640px, calc(100vw - 32px))"
57
- label-width="9em"
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 type {
83
- AiProvider,
84
- AiProviderForm,
85
- PageAiProviderRequest
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
- type ModelApiType = "CHAT_COMPLETIONS" | "RESPONSES";
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, "models_json"> {
118
- models: AiModelConfig[];
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<AiModelConfig>(defaultModelConfig());
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 modelApiTypeOptions = computed<ProFormOption[]>(() => [
143
- { label: t("system.base.ai_provider.model.api_type.chat_completions"), value: "CHAT_COMPLETIONS" },
144
- { label: t("system.base.ai_provider.model.api_type.responses"), value: "RESPONSES" }
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
- { prop: "model_name", label: t("system.base.ai_provider.field.model_name"), component: "input", colSpan: 12, props: { maxlength: 100 } },
160
- { prop: "api_type", label: t("system.base.ai_provider.field.api_type"), component: "select", colSpan: 12, options: modelApiTypeOptions.value },
161
- { prop: "temperature", label: t("system.base.ai_provider.field.temperature"), component: "input-number", colSpan: 12, props: { min: 0, max: 2, precision: 2, step: 0.1, controlsPosition: "right" } },
162
- { prop: "max_tokens", label: t("system.base.ai_provider.field.max_tokens"), component: "input-number", colSpan: 12, props: { min: 0, max: 200000, precision: 0, controlsPosition: "right" } },
163
- { prop: "timeout_seconds", label: t("system.base.ai_provider.field.timeout_seconds"), component: "input-number", colSpan: 12, props: { min: 0, max: 600, precision: 0, controlsPosition: "right" } },
164
- { prop: "max_retries", label: t("system.base.ai_provider.field.max_retries"), component: "input-number", colSpan: 12, props: { min: 0, max: 10, precision: 0, controlsPosition: "right" } }
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: AiModelConfig[], callback) => {
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.ai_provider.validation.model_name"))
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.ai_provider.validation.model_name"), trigger: "blur" },
190
- { max: 100, message: t("system.base.ai_provider.validation.model_name_length"), trigger: "blur" }
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
- api_type: [{ required: true, message: t("system.base.ai_provider.validation.api_type"), trigger: "change" }],
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
- const { models_json, ...provider } = value;
268
- Object.assign(form, defaultForm(), provider, {
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
- const payload: AiProviderForm = {
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
- models_json: JSON.stringify(form.models),
451
+ models: form.models.map(model => buildModelPayload(model)),
294
452
  config,
295
453
  sort: form.sort,
296
454
  status: form.status
297
455
  };
298
- if (dialog.editing) await defAiProviderService.UpdateAiProvider({ ai_provider: payload });
299
- else await defAiProviderService.CreateAiProvider({ ai_provider: payload });
300
- ElMessage.success(t(dialog.editing ? "system.base.ai_provider.message.update_success" : "system.base.ai_provider.message.create_success"));
301
- dialog.visible = false;
302
- resetForm();
303
- table.value?.getTableList();
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 ? defaultModelConfig() : { ...form.models[index] });
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
- await ElMessageBox.confirm(t("system.base.ai_provider.message.confirm_delete", { name: row?.name ?? ids.join(", ") }), t("common.action.delete"), { type: "warning" });
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
- await defAiProviderService.SetAiProviderStatus({
423
- id: row.id,
424
- status: row.status === Status.STATUS_ENABLE ? Status.STATUS_DISABLE : Status.STATUS_ENABLE
425
- });
426
- ElMessage.success(t("system.base.ai_provider.message.status_success"));
427
- table.value?.getTableList();
428
- return true;
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
- width: 100%;
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
- width: 100%;
452
- overflow: hidden;
453
- border: 1px solid var(--el-border-color);
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
- gap: 16px;
462
- min-width: 0;
463
- padding: 12px 14px;
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: 8px;
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
- .provider-models__heading strong {
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
- flex: none;
503
- gap: 4px;
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>