@liujitcn/kratos-admin-system 0.0.61 → 0.0.64

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 (97) hide show
  1. package/README.md +16 -1
  2. package/dist/package/declarations/src/api/base/v1/ai_provider.d.ts +7 -0
  3. package/dist/package/declarations/src/api/system/admin/v1/ai_provider.d.ts +18 -0
  4. package/dist/package/declarations/src/api/system/admin/v1/base_api_rate_limit_policy.d.ts +19 -0
  5. package/dist/package/declarations/src/api/system/admin/v1/base_message_provider.d.ts +21 -0
  6. package/dist/package/declarations/src/api/system/admin/v1/base_message_template.d.ts +16 -0
  7. package/dist/package/declarations/src/api/system/admin/v1/base_oauth_provider.d.ts +18 -0
  8. package/dist/package/declarations/src/api/system/admin/v1/base_rate_limit_rule.d.ts +21 -0
  9. package/dist/package/declarations/src/components/api/ApiTransfer.vue.d.ts +35 -0
  10. package/dist/package/declarations/src/components/api/apiTransfer.d.ts +13 -0
  11. package/dist/package/declarations/src/components/i18n/DynamicI18nCell.vue.d.ts +2 -2
  12. package/dist/package/declarations/src/components/i18n/dynamicI18n.d.ts +3 -4
  13. package/dist/package/declarations/src/locales/generated.d.ts +900 -0
  14. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +4 -0
  15. package/dist/package/declarations/src/rpc/base/v1/ai_provider.d.ts +22 -0
  16. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +20 -0
  17. package/dist/package/declarations/src/rpc/google/protobuf/struct.d.ts +81 -0
  18. package/dist/package/declarations/src/rpc/system/admin/v1/ai_provider.d.ts +110 -0
  19. package/dist/package/declarations/src/rpc/system/admin/v1/base_api_rate_limit_policy.d.ts +137 -0
  20. package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +4 -23
  21. package/dist/package/declarations/src/rpc/system/admin/v1/base_message.d.ts +4 -4
  22. package/dist/package/declarations/src/rpc/system/admin/v1/base_message_category.d.ts +19 -0
  23. package/dist/package/declarations/src/rpc/system/admin/v1/base_message_provider.d.ts +126 -0
  24. package/dist/package/declarations/src/rpc/system/admin/v1/base_message_template.d.ts +99 -0
  25. package/dist/package/declarations/src/rpc/system/admin/v1/base_oauth_provider.d.ts +133 -0
  26. package/dist/package/declarations/src/rpc/system/admin/v1/base_rate_limit_rule.d.ts +144 -0
  27. package/dist/package/declarations/src/utils/persisted-message.d.ts +4 -0
  28. package/dist/package/declarations/src/utils/rateLimit.d.ts +24 -0
  29. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +11 -2
  30. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +9 -0
  31. package/dist/package/declarations/src/views/base/ai-provider/index.vue.d.ts +3 -0
  32. package/dist/package/declarations/src/views/base/api-rate-limit-policy/index.vue.d.ts +3 -0
  33. package/dist/package/declarations/src/views/base/message-provider/index.vue.d.ts +3 -0
  34. package/dist/package/declarations/src/views/base/message-template/index.vue.d.ts +3 -0
  35. package/dist/package/declarations/src/views/base/oauth-provider/index.vue.d.ts +3 -0
  36. package/dist/package/declarations/src/views/base/rate-limit-rule/index.vue.d.ts +3 -0
  37. package/dist/package/src/api/base/v1/ai_provider.ts +22 -0
  38. package/dist/package/src/api/system/admin/v1/ai_provider.ts +50 -0
  39. package/dist/package/src/api/system/admin/v1/base_api_rate_limit_policy.ts +51 -0
  40. package/dist/package/src/api/system/admin/v1/base_message_provider.ts +57 -0
  41. package/dist/package/src/api/system/admin/v1/base_message_template.ts +44 -0
  42. package/dist/package/src/api/system/admin/v1/base_oauth_provider.ts +50 -0
  43. package/dist/package/src/api/system/admin/v1/base_rate_limit_rule.ts +58 -0
  44. package/dist/package/src/components/ai/Ai.vue +1 -1
  45. package/dist/package/src/components/api/ApiTransfer.vue +117 -0
  46. package/dist/package/src/components/api/apiTransfer.ts +27 -0
  47. package/dist/package/src/components/i18n/DynamicI18nCell.vue +8 -8
  48. package/dist/package/src/components/i18n/DynamicI18nEditor.vue +6 -3
  49. package/dist/package/src/components/i18n/dynamicI18n.ts +4 -5
  50. package/dist/package/src/components/tenant-project/TenantProjectManager.vue +2 -1
  51. package/dist/package/src/locales/en-US.json +229 -4
  52. package/dist/package/src/locales/ja-JP.json +229 -4
  53. package/dist/package/src/locales/zh-CN.json +229 -4
  54. package/dist/package/src/locales/zh-TW.json +229 -4
  55. package/dist/package/src/rpc/base/v1/ai_message.ts +7 -1
  56. package/dist/package/src/rpc/base/v1/ai_provider.ts +33 -0
  57. package/dist/package/src/rpc/base/v1/oauth.ts +16 -0
  58. package/dist/package/src/rpc/google/protobuf/struct.ts +80 -0
  59. package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +129 -0
  60. package/dist/package/src/rpc/system/admin/v1/base_api_rate_limit_policy.ts +160 -0
  61. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +4 -24
  62. package/dist/package/src/rpc/system/admin/v1/base_message.ts +4 -4
  63. package/dist/package/src/rpc/system/admin/v1/base_message_category.ts +20 -0
  64. package/dist/package/src/rpc/system/admin/v1/base_message_provider.ts +150 -0
  65. package/dist/package/src/rpc/system/admin/v1/base_message_template.ts +117 -0
  66. package/dist/package/src/rpc/system/admin/v1/base_oauth_provider.ts +156 -0
  67. package/dist/package/src/rpc/system/admin/v1/base_rate_limit_rule.ts +167 -0
  68. package/dist/package/src/utils/persisted-message.ts +21 -0
  69. package/dist/package/src/utils/rateLimit.ts +140 -0
  70. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +36 -4
  71. package/dist/package/src/views/ai/chat/components/XSender.vue +112 -1
  72. package/dist/package/src/views/ai/chat/index.vue +30 -1
  73. package/dist/package/src/views/base/ai-provider/index.vue +512 -0
  74. package/dist/package/src/views/base/api-rate-limit-policy/index.vue +428 -0
  75. package/dist/package/src/views/base/backup-management/archive-record/index.vue +2 -0
  76. package/dist/package/src/views/base/backup-management/archive-restore/index.vue +2 -1
  77. package/dist/package/src/views/base/backup-management/backup-record/index.vue +2 -0
  78. package/dist/package/src/views/base/backup-management/backup-restore/index.vue +2 -1
  79. package/dist/package/src/views/base/config/index.vue +12 -13
  80. package/dist/package/src/views/base/dict/index.vue +2 -3
  81. package/dist/package/src/views/base/dict/item.vue +2 -3
  82. package/dist/package/src/views/base/job/index.vue +2 -3
  83. package/dist/package/src/views/base/job/log.vue +3 -2
  84. package/dist/package/src/views/base/menu/index.vue +27 -83
  85. package/dist/package/src/views/base/message/index.vue +9 -3
  86. package/dist/package/src/views/base/message-category/index.vue +34 -3
  87. package/dist/package/src/views/base/message-provider/index.vue +164 -0
  88. package/dist/package/src/views/base/message-template/index.vue +105 -0
  89. package/dist/package/src/views/base/migration/index.vue +1 -1
  90. package/dist/package/src/views/base/oauth-client/index.vue +6 -6
  91. package/dist/package/src/views/base/oauth-provider/index.vue +274 -0
  92. package/dist/package/src/views/base/rate-limit-rule/index.vue +322 -0
  93. package/dist/package/src/views/base/redact-output-policy/index.vue +1 -1
  94. package/dist/package/src/views/base/redact-rule/index.vue +12 -2
  95. package/dist/package/src/views/base/redact-storage-policy/index.vue +4 -0
  96. package/dist/package/src/views/base/tenant/index.vue +3 -3
  97. package/package.json +2 -2
@@ -0,0 +1,512 @@
1
+ <template>
2
+ <div class="table-box">
3
+ <ProTable ref="table" row-key="id" :columns="columns" :header-actions="headerActions" :request-api="requestTable" />
4
+ <FormDialog
5
+ v-model="dialog.visible"
6
+ ref="dialogRef"
7
+ :title="t(dialog.editing ? 'common.action.edit' : 'common.action.create')"
8
+ width="min(980px, calc(100vw - 32px))"
9
+ label-width="10em"
10
+ :model="form"
11
+ :fields="fields"
12
+ :rules="rules"
13
+ @confirm="submit"
14
+ @close="resetForm"
15
+ >
16
+ <template #models>
17
+ <div class="provider-models">
18
+ <div v-if="form.models.length === 0" class="provider-models__empty">
19
+ {{ t("system.base.ai_provider.model.empty") }}
20
+ </div>
21
+ <div v-else class="provider-models__list">
22
+ <div v-for="(model, index) in form.models" :key="`${model.model_name}-${index}`" class="provider-models__item">
23
+ <div class="provider-models__content">
24
+ <div class="provider-models__heading">
25
+ <strong>{{ model.model_name }}</strong>
26
+ <el-tag size="small" effect="plain">{{ apiTypeLabel(model.api_type) }}</el-tag>
27
+ </div>
28
+ <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>
33
+ </div>
34
+ </div>
35
+ <div class="provider-models__actions">
36
+ <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)" />
38
+ </el-tooltip>
39
+ <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)" />
41
+ </el-tooltip>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <el-button type="primary" plain :icon="CirclePlus" @click="openModelDialog()">
46
+ {{ t("system.base.ai_provider.model.add") }}
47
+ </el-button>
48
+ </div>
49
+ </template>
50
+ </FormDialog>
51
+
52
+ <FormDialog
53
+ v-model="modelDialog.visible"
54
+ ref="modelDialogRef"
55
+ :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"
59
+ :model="modelForm"
60
+ :fields="modelFields"
61
+ :rules="modelRules"
62
+ @confirm="saveModel"
63
+ @close="resetModelDialog"
64
+ />
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { computed, reactive, ref } from "vue";
70
+ import { ElMessage, ElMessageBox } from "element-plus";
71
+ import type { FormRules } from "element-plus";
72
+ import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
73
+ import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
74
+ import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
75
+ import type { ColumnProps, HeaderActionProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
76
+ import type { ProFormField, ProFormOption } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
77
+ import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
78
+ import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
79
+ import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
80
+ import { t } from "@liujitcn/kratos-admin-core";
81
+ import { defAiProviderService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/ai_provider";
82
+ import type {
83
+ AiProvider,
84
+ AiProviderForm,
85
+ PageAiProviderRequest
86
+ } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/ai_provider";
87
+ import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
88
+
89
+ defineOptions({ name: "AiProvider", inheritAttrs: false });
90
+
91
+ /** Provider个性化配置编辑项。 */
92
+ interface ConfigItem {
93
+ key: string;
94
+ value: string;
95
+ }
96
+
97
+ /** 模型请求接口类型。 */
98
+ type ModelApiType = "CHAT_COMPLETIONS" | "RESPONSES";
99
+
100
+ /** 单个AI模型的可编辑配置。 */
101
+ interface AiModelConfig {
102
+ /** 服务商侧的模型名称。 */
103
+ model_name: string;
104
+ /** 模型请求接口类型。 */
105
+ api_type: ModelApiType;
106
+ /** 文本生成温度。 */
107
+ temperature: number;
108
+ /** 单次请求最大输出Token数。 */
109
+ max_tokens: number;
110
+ /** 模型请求超时秒数。 */
111
+ timeout_seconds: number;
112
+ /** 模型请求失败后的最大重试次数。 */
113
+ max_retries: number;
114
+ }
115
+
116
+ /** AI Provider页面表单状态。 */
117
+ interface FormState extends Omit<AiProviderForm, "models_json"> {
118
+ models: AiModelConfig[];
119
+ config_items: ConfigItem[];
120
+ }
121
+
122
+ const { BUTTONS } = useAuthButtons();
123
+ const { isDefaultTenant } = useTenantScope();
124
+ const table = ref<ProTableInstance>();
125
+ const dialogRef = ref<InstanceType<typeof FormDialog>>();
126
+ const modelDialogRef = ref<InstanceType<typeof FormDialog>>();
127
+ const dialog = reactive({ visible: false, editing: false });
128
+ const modelDialog = reactive({ visible: false, editing: false, index: -1 });
129
+ const form = reactive<FormState>(defaultForm());
130
+ const modelForm = reactive<AiModelConfig>(defaultModelConfig());
131
+
132
+ const statusOptions = computed<ProFormOption[]>(() => [
133
+ { label: t("common.status.enabled"), value: Status.STATUS_ENABLE },
134
+ { label: t("common.status.disabled"), value: Status.STATUS_DISABLE }
135
+ ]);
136
+
137
+ const providerOptions = computed<ProFormOption[]>(() => [
138
+ { label: t("system.base.ai_provider.provider.openai_compatible"), value: "openai_compatible" },
139
+ { label: t("system.base.ai_provider.provider.ollama"), value: "ollama" }
140
+ ]);
141
+
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" }
145
+ ]);
146
+
147
+ const fields = computed<ProFormField[]>(() => [
148
+ { 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") } },
152
+ { prop: "models", label: t("system.base.ai_provider.field.models"), component: "slot", slotName: "models", colSpan: 24 },
153
+ { prop: "config_items", label: t("system.base.ai_provider.field.config"), component: "kv-list", colSpan: 24, props: { keyInputProps: { maxlength: 128 } } },
154
+ { prop: "sort", label: t("common.field.sort"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0 } },
155
+ { prop: "status", label: t("common.field.status"), component: "radio-group", colSpan: 12, options: statusOptions.value }
156
+ ]);
157
+
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
+ ]);
166
+
167
+ const rules = computed<FormRules>(() => ({
168
+ name: [{ required: true, message: t("system.base.ai_provider.validation.name"), trigger: "blur" }],
169
+ provider: [{ required: true, message: t("system.base.ai_provider.validation.provider"), trigger: "change" }],
170
+ models: [{
171
+ validator: (_rule, value: AiModelConfig[], callback) => {
172
+ if (!Array.isArray(value) || value.length === 0) {
173
+ callback(new Error(t("system.base.ai_provider.model.validation.required")));
174
+ return;
175
+ }
176
+ const names = value.map(model => model.model_name);
177
+ callback(names.some(name => !name)
178
+ ? new Error(t("system.base.ai_provider.validation.model_name"))
179
+ : new Set(names).size !== names.length
180
+ ? new Error(t("system.base.ai_provider.model.validation.duplicate"))
181
+ : undefined);
182
+ },
183
+ trigger: "change"
184
+ }]
185
+ }));
186
+
187
+ const modelRules = computed<FormRules>(() => ({
188
+ 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" }
191
+ ],
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" }]
197
+ }));
198
+
199
+ const columns = computed<ColumnProps[]>(() => [
200
+ { prop: "name", label: t("system.base.ai_provider.field.name"), minWidth: 150, search: { el: "input" } },
201
+ { prop: "provider", label: t("system.base.ai_provider.field.provider"), width: 190, search: { el: "select", enum: providerOptions.value }, enum: providerOptions.value },
202
+ { prop: "base_url", label: t("system.base.ai_provider.field.base_url"), minWidth: 220 },
203
+ { prop: "api_key_configured", label: t("system.base.ai_provider.field.api_key"), width: 140, formatter: row => row.api_key_configured ? t("system.base.ai_provider.value.configured") : t("system.base.ai_provider.value.not_configured") },
204
+ { prop: "model_names", label: t("system.base.ai_provider.field.models"), minWidth: 220, formatter: row => (row.model_names ?? []).join(", ") },
205
+ { prop: "sort", label: t("common.field.sort"), width: 80, align: "right" },
206
+ {
207
+ prop: "status",
208
+ label: t("common.field.status"),
209
+ width: 110,
210
+ search: { el: "select", enum: statusOptions.value },
211
+ cellType: "status",
212
+ statusProps: {
213
+ activeValue: Status.STATUS_ENABLE,
214
+ inactiveValue: Status.STATUS_DISABLE,
215
+ activeText: t("common.status.enabled"),
216
+ inactiveText: t("common.status.disabled"),
217
+ disabled: () => !isDefaultTenant.value || !BUTTONS.value["base:ai-provider:status"],
218
+ beforeChange: scope => handleSetStatus(scope.row as AiProvider)
219
+ }
220
+ },
221
+ { prop: "updated_at", label: t("common.field.updated_at"), minWidth: 180, align: "center" },
222
+ {
223
+ prop: "operation",
224
+ label: t("common.field.operation"),
225
+ cellType: "actions",
226
+ actions: [
227
+ { label: t("common.action.edit"), link: true, icon: EditPen, hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:ai-provider:update"], onClick: scope => openDialog((scope.row as AiProvider).id) },
228
+ { label: t("common.action.delete"), type: "danger", link: true, icon: Delete, hidden: () => !isDefaultTenant.value || !BUTTONS.value["base:ai-provider:delete"], onClick: scope => handleDelete(scope.row as AiProvider) }
229
+ ]
230
+ }
231
+ ]);
232
+
233
+ const headerActions = computed<HeaderActionProps[]>(() => [
234
+ { label: t("common.action.create"), type: "primary", icon: CirclePlus, hidden: !isDefaultTenant.value || !BUTTONS.value["base:ai-provider:create"], onClick: () => openDialog() },
235
+ { label: t("common.action.delete"), type: "danger", icon: Delete, hidden: !isDefaultTenant.value || !BUTTONS.value["base:ai-provider:delete"], disabled: scope => !scope.isSelected, onClick: scope => handleDelete(scope.selectedListIds as number[]) }
236
+ ]);
237
+
238
+ /** 创建默认供应商表单。 */
239
+ function defaultForm(): FormState {
240
+ return {
241
+ id: 0,
242
+ provider: "openai_compatible",
243
+ name: "",
244
+ base_url: "",
245
+ api_key: "",
246
+ api_key_configured: false,
247
+ models: [],
248
+ config: {},
249
+ config_items: [],
250
+ sort: 0,
251
+ status: Status.STATUS_DISABLE
252
+ };
253
+ }
254
+
255
+ /** 请求AI供应商表格数据。 */
256
+ async function requestTable(params: Record<string, unknown>) {
257
+ const data = await defAiProviderService.PageAiProvider(buildPageRequest<PageAiProviderRequest>(params as unknown as PageAiProviderRequest));
258
+ return { data: { list: data.providers ?? [], total: data.total } };
259
+ }
260
+
261
+ /** 打开供应商编辑表单。 */
262
+ async function openDialog(id?: number) {
263
+ await dialogRef.value?.open({
264
+ load: () => (id ? defAiProviderService.GetAiProvider({ id }) : undefined),
265
+ commit: value => {
266
+ if (value) {
267
+ const { models_json, ...provider } = value;
268
+ Object.assign(form, defaultForm(), provider, {
269
+ models: parseModelConfigs(models_json),
270
+ config_items: configToItems(value.config)
271
+ });
272
+ } else {
273
+ Object.assign(form, defaultForm());
274
+ }
275
+ dialog.editing = Boolean(id);
276
+ }
277
+ });
278
+ }
279
+
280
+ /** 提交AI供应商表单。 */
281
+ async function submit() {
282
+ const valid = await dialogRef.value?.validate();
283
+ if (!valid) return;
284
+ const config = configItemsToMap(form.config_items);
285
+ if (!config) return;
286
+ const payload: AiProviderForm = {
287
+ id: form.id,
288
+ provider: form.provider,
289
+ name: form.name,
290
+ base_url: form.base_url,
291
+ api_key: form.api_key,
292
+ api_key_configured: form.api_key_configured,
293
+ models_json: JSON.stringify(form.models),
294
+ config,
295
+ sort: form.sort,
296
+ status: form.status
297
+ };
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();
304
+ }
305
+
306
+ /** 重置供应商表单。 */
307
+ function resetForm() {
308
+ Object.assign(form, defaultForm());
309
+ dialog.editing = false;
310
+ modelDialog.visible = false;
311
+ resetModelDialog();
312
+ }
313
+
314
+ /** 打开模型新增或编辑表单。 */
315
+ function openModelDialog(index?: number) {
316
+ modelDialog.index = index ?? -1;
317
+ modelDialog.editing = index !== undefined;
318
+ Object.assign(modelForm, index === undefined ? defaultModelConfig() : { ...form.models[index] });
319
+ modelDialog.visible = true;
320
+ }
321
+
322
+ /** 校验并保存当前模型表单。 */
323
+ async function saveModel() {
324
+ const valid = await modelDialogRef.value?.validate();
325
+ if (!valid) return;
326
+ const duplicated = form.models.some((model, index) => index !== modelDialog.index && model.model_name === modelForm.model_name);
327
+ if (duplicated) {
328
+ ElMessage.warning(t("system.base.ai_provider.model.validation.duplicate"));
329
+ return;
330
+ }
331
+ if (modelDialog.index < 0) form.models.push({ ...modelForm });
332
+ else form.models.splice(modelDialog.index, 1, { ...modelForm });
333
+ modelDialog.visible = false;
334
+ }
335
+
336
+ /** 删除模型表单项。 */
337
+ async function removeModel(index: number) {
338
+ try {
339
+ await ElMessageBox.confirm(
340
+ t("system.base.ai_provider.model.confirm_remove", { name: form.models[index].model_name }),
341
+ t("common.action.delete"),
342
+ { type: "warning" }
343
+ );
344
+ form.models.splice(index, 1);
345
+ } catch {
346
+ return;
347
+ }
348
+ }
349
+
350
+ /** 重置模型编辑弹窗状态。 */
351
+ function resetModelDialog() {
352
+ modelDialog.editing = false;
353
+ modelDialog.index = -1;
354
+ }
355
+
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配置对象转换为键值表单项。 */
382
+ function configToItems(config: Record<string, unknown> | undefined): ConfigItem[] {
383
+ return Object.entries(config ?? {}).map(([key, value]) => ({ key, value: typeof value === "string" ? value : JSON.stringify(value) }));
384
+ }
385
+
386
+ /** 将键值表单项转换为Provider配置JSON对象。 */
387
+ function configItemsToMap(items: ConfigItem[]): Record<string, unknown> | undefined {
388
+ const result: Record<string, unknown> = {};
389
+ for (const item of items) {
390
+ const key = item.key.trim();
391
+ if (!key || Object.prototype.hasOwnProperty.call(result, key)) {
392
+ ElMessage.error(t("system.base.ai_provider.validation.config_invalid"));
393
+ return undefined;
394
+ }
395
+ const value = item.value.trim();
396
+ if (!value) {
397
+ result[key] = "";
398
+ continue;
399
+ }
400
+ try {
401
+ result[key] = JSON.parse(value);
402
+ } catch {
403
+ result[key] = value;
404
+ }
405
+ }
406
+ return result;
407
+ }
408
+
409
+ /** 批量删除AI供应商。 */
410
+ async function handleDelete(target: AiProvider | number | Array<number>) {
411
+ const row = typeof target === "object" && !Array.isArray(target) ? target : undefined;
412
+ const ids = (row ? [row.id] : normalizeSelectedIds(target as number | number[])).map(Number);
413
+ 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" });
415
+ for (const id of ids) await defAiProviderService.DeleteAiProvider({ id });
416
+ ElMessage.success(t("system.base.ai_provider.message.delete_success"));
417
+ table.value?.getTableList();
418
+ }
419
+
420
+ /** 更新AI供应商状态。 */
421
+ 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;
429
+ }
430
+ </script>
431
+
432
+ <style scoped>
433
+ .provider-models {
434
+ display: flex;
435
+ flex-direction: column;
436
+ align-items: flex-start;
437
+ gap: 12px;
438
+ width: 100%;
439
+ }
440
+
441
+ .provider-models__empty {
442
+ width: 100%;
443
+ padding: 16px;
444
+ border: 1px dashed var(--el-border-color);
445
+ border-radius: 6px;
446
+ color: var(--el-text-color-secondary);
447
+ text-align: center;
448
+ }
449
+
450
+ .provider-models__list {
451
+ width: 100%;
452
+ overflow: hidden;
453
+ border: 1px solid var(--el-border-color);
454
+ border-radius: 6px;
455
+ }
456
+
457
+ .provider-models__item {
458
+ display: flex;
459
+ align-items: center;
460
+ 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);
468
+ }
469
+
470
+ .provider-models__content {
471
+ display: flex;
472
+ flex-direction: column;
473
+ gap: 8px;
474
+ min-width: 0;
475
+ }
476
+
477
+ .provider-models__heading,
478
+ .provider-models__details,
479
+ .provider-models__actions {
480
+ display: flex;
481
+ align-items: center;
482
+ }
483
+
484
+ .provider-models__heading {
485
+ flex-wrap: wrap;
486
+ gap: 8px;
487
+ min-width: 0;
488
+ }
489
+
490
+ .provider-models__heading strong {
491
+ overflow-wrap: anywhere;
492
+ }
493
+
494
+ .provider-models__details {
495
+ flex-wrap: wrap;
496
+ gap: 4px 14px;
497
+ color: var(--el-text-color-secondary);
498
+ font-size: 12px;
499
+ }
500
+
501
+ .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
+ }
511
+ }
512
+ </style>