@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.
Files changed (65) 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 +3 -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 +1805 -1553
  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/login.d.ts +2 -39
  10. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +2 -2
  11. package/dist/package/declarations/src/rpc/common/v1/types.d.ts +4 -4
  12. package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +66 -7
  13. package/dist/package/declarations/src/rpc/system/admin/v1/ai_query.d.ts +73 -0
  14. package/dist/package/declarations/src/rpc/system/admin/v1/ai_session.d.ts +114 -0
  15. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +3 -3
  16. package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +4 -3
  17. package/dist/package/declarations/src/rpc/system/admin/v1/base_login_policy.d.ts +2 -2
  18. package/dist/package/declarations/src/rpc/system/admin/v1/base_message_provider.d.ts +3 -2
  19. package/dist/package/declarations/src/rpc/system/admin/v1/base_oauth_provider.d.ts +3 -2
  20. package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +3 -3
  21. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +9 -2
  22. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +7 -0
  23. package/dist/package/declarations/src/views/ai/query/index.vue.d.ts +3 -0
  24. package/dist/package/declarations/src/views/ai/session/index.vue.d.ts +3 -0
  25. package/dist/package/src/api/base/v1/ai_knowledge.ts +22 -0
  26. package/dist/package/src/api/system/admin/v1/ai_provider.ts +7 -0
  27. package/dist/package/src/api/system/admin/v1/ai_query.ts +25 -0
  28. package/dist/package/src/api/system/admin/v1/ai_session.ts +25 -0
  29. package/dist/package/src/components/account/ChangePasswordForm.vue +7 -5
  30. package/dist/package/src/components/tenant-project/TenantProjectSelect.vue +21 -4
  31. package/dist/package/src/locales/README.md +1 -1
  32. package/dist/package/src/locales/en-US.json +355 -292
  33. package/dist/package/src/locales/ja-JP.json +352 -289
  34. package/dist/package/src/locales/zh-CN.json +352 -289
  35. package/dist/package/src/locales/zh-TW.json +352 -289
  36. package/dist/package/src/module.ts +8 -0
  37. package/dist/package/src/rpc/base/v1/ai_knowledge.ts +33 -0
  38. package/dist/package/src/rpc/base/v1/ai_message.ts +2 -0
  39. package/dist/package/src/rpc/base/v1/login.ts +2 -42
  40. package/dist/package/src/rpc/base/v1/oauth.ts +2 -2
  41. package/dist/package/src/rpc/common/v1/types.ts +4 -4
  42. package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +73 -7
  43. package/dist/package/src/rpc/system/admin/v1/ai_query.ts +89 -0
  44. package/dist/package/src/rpc/system/admin/v1/ai_session.ts +131 -0
  45. package/dist/package/src/rpc/system/admin/v1/auth.ts +3 -3
  46. package/dist/package/src/rpc/system/admin/v1/base_config.ts +6 -3
  47. package/dist/package/src/rpc/system/admin/v1/base_login_policy.ts +2 -2
  48. package/dist/package/src/rpc/system/admin/v1/base_message_provider.ts +5 -2
  49. package/dist/package/src/rpc/system/admin/v1/base_oauth_provider.ts +5 -2
  50. package/dist/package/src/rpc/system/admin/v1/base_user.ts +3 -3
  51. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +18 -0
  52. package/dist/package/src/views/ai/chat/components/XSender.vue +102 -0
  53. package/dist/package/src/views/ai/chat/index.vue +22 -2
  54. package/dist/package/src/views/ai/query/index.vue +222 -0
  55. package/dist/package/src/views/ai/session/index.vue +422 -0
  56. package/dist/package/src/views/base/ai-provider/index.vue +216 -169
  57. package/dist/package/src/views/base/config/index.vue +9 -9
  58. package/dist/package/src/views/base/login-policy/index.vue +2 -2
  59. package/dist/package/src/views/base/message-provider/index.vue +7 -5
  60. package/dist/package/src/views/base/oauth-provider/index.vue +10 -7
  61. package/dist/package/src/views/base/tenant-project-grant/index.vue +9 -4
  62. package/dist/package/src/views/base/user/index.vue +3 -3
  63. package/dist/package/src/views/profile/components/security.vue +4 -5
  64. package/dist/package/src/views/tool/code-gen/config.ts +2 -0
  65. 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">{{ apiTypeLabel(model.api_type) }}</el-tag>
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
- size="small"
39
- effect="plain"
40
- :type="modelTestResults[model.model_name]?.success ? 'success' : 'danger'"
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.ai_provider.model.test_success"
44
- : "system.base.ai_provider.model.test_failed") }} · {{ modelTestResults[model.model_name]?.duration_ms }} ms
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
- <span>{{ t("system.base.ai_provider.field.temperature") }}: {{ model.temperature }}</span>
50
- <span>{{ t("system.base.ai_provider.field.max_tokens") }}: {{ model.max_tokens }}</span>
51
- <span>{{ t("system.base.ai_provider.field.timeout_seconds") }}: {{ model.timeout_seconds }}</span>
52
- <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>
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(640px, calc(100vw - 32px))"
90
- label-width="9em"
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 type {
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
- type ModelApiType = "CHAT_COMPLETIONS" | "RESPONSES";
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, "models_json"> {
154
- models: AiModelConfig[];
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<AiModelConfig>(defaultModelConfig());
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 modelApiTypeOptions = computed<ProFormOption[]>(() => [
188
- { label: t("system.base.ai_provider.model.api_type.chat_completions"), value: "CHAT_COMPLETIONS" },
189
- { label: t("system.base.ai_provider.model.api_type.responses"), value: "RESPONSES" }
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
- { prop: "model_name", label: t("system.base.ai_provider.field.model_name"), component: "input", colSpan: 12, props: { maxlength: 100 } },
205
- { prop: "display_name", label: t("system.base.ai_provider.field.display_name"), component: "input", colSpan: 12, props: { maxlength: 100, placeholder: t("system.base.ai_provider.model.placeholder.display_name") } },
206
- { prop: "api_type", label: t("system.base.ai_provider.field.api_type"), component: "select", colSpan: 12, options: modelApiTypeOptions.value },
207
- { 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" } },
208
- { 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" } },
209
- { 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" } },
210
- { 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" } }
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: AiModelConfig[], callback) => {
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.ai_provider.validation.model_name"))
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.ai_provider.validation.model_name"), trigger: "blur" },
236
- { max: 100, message: t("system.base.ai_provider.validation.model_name_length"), trigger: "blur" }
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
- display_name: [{ max: 100, message: t("system.base.ai_provider.model.validation.display_name"), trigger: "blur" }],
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
- const { models_json, ...provider } = value;
315
- Object.assign(form, defaultForm(), provider, {
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(form.config_items);
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: form.id,
365
- provider: form.provider,
366
- name: form.name,
367
- base_url: form.base_url,
368
- api_key: form.api_key,
369
- api_key_configured: form.api_key_configured,
370
- models_json: JSON.stringify(form.models),
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: form.sort,
373
- status: form.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 ? defaultModelConfig() : { ...form.models[index] });
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
- width: 100%;
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
- width: 100%;
540
- overflow: hidden;
541
- border: 1px solid var(--el-border-color);
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
- gap: 16px;
550
- min-width: 0;
551
- padding: 12px 14px;
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: 8px;
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
- display: inline-flex;
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
- flex: none;
604
- gap: 4px;
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>