@liujitcn/kratos-admin-system 0.0.65 → 0.0.66

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