@liujitcn/kratos-admin-system 0.0.62 → 0.0.65

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 (101) 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 +20 -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 +968 -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 +29 -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 +133 -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/ai/chat/modelDisplay.d.ts +5 -0
  32. package/dist/package/declarations/src/views/ai/chat/modelSelection.d.ts +9 -0
  33. package/dist/package/declarations/src/views/base/ai-provider/index.vue.d.ts +3 -0
  34. package/dist/package/declarations/src/views/base/api-rate-limit-policy/index.vue.d.ts +3 -0
  35. package/dist/package/declarations/src/views/base/message-provider/index.vue.d.ts +3 -0
  36. package/dist/package/declarations/src/views/base/message-template/index.vue.d.ts +3 -0
  37. package/dist/package/declarations/src/views/base/oauth-provider/index.vue.d.ts +3 -0
  38. package/dist/package/declarations/src/views/base/rate-limit-rule/index.vue.d.ts +3 -0
  39. package/dist/package/src/api/base/v1/ai_provider.ts +22 -0
  40. package/dist/package/src/api/system/admin/v1/ai_provider.ts +57 -0
  41. package/dist/package/src/api/system/admin/v1/base_api_rate_limit_policy.ts +51 -0
  42. package/dist/package/src/api/system/admin/v1/base_message_provider.ts +57 -0
  43. package/dist/package/src/api/system/admin/v1/base_message_template.ts +44 -0
  44. package/dist/package/src/api/system/admin/v1/base_oauth_provider.ts +50 -0
  45. package/dist/package/src/api/system/admin/v1/base_rate_limit_rule.ts +58 -0
  46. package/dist/package/src/components/ai/Ai.vue +1 -1
  47. package/dist/package/src/components/api/ApiTransfer.vue +117 -0
  48. package/dist/package/src/components/api/apiTransfer.ts +27 -0
  49. package/dist/package/src/components/i18n/DynamicI18nCell.vue +8 -8
  50. package/dist/package/src/components/i18n/DynamicI18nEditor.vue +6 -3
  51. package/dist/package/src/components/i18n/dynamicI18n.ts +4 -5
  52. package/dist/package/src/components/tenant-project/TenantProjectManager.vue +2 -1
  53. package/dist/package/src/locales/en-US.json +246 -4
  54. package/dist/package/src/locales/ja-JP.json +246 -4
  55. package/dist/package/src/locales/zh-CN.json +246 -4
  56. package/dist/package/src/locales/zh-TW.json +246 -4
  57. package/dist/package/src/rpc/base/v1/ai_message.ts +7 -1
  58. package/dist/package/src/rpc/base/v1/ai_provider.ts +41 -0
  59. package/dist/package/src/rpc/base/v1/oauth.ts +16 -0
  60. package/dist/package/src/rpc/google/protobuf/struct.ts +80 -0
  61. package/dist/package/src/rpc/system/admin/v1/ai_provider.ts +155 -0
  62. package/dist/package/src/rpc/system/admin/v1/base_api_rate_limit_policy.ts +160 -0
  63. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +4 -24
  64. package/dist/package/src/rpc/system/admin/v1/base_message.ts +4 -4
  65. package/dist/package/src/rpc/system/admin/v1/base_message_category.ts +20 -0
  66. package/dist/package/src/rpc/system/admin/v1/base_message_provider.ts +150 -0
  67. package/dist/package/src/rpc/system/admin/v1/base_message_template.ts +117 -0
  68. package/dist/package/src/rpc/system/admin/v1/base_oauth_provider.ts +156 -0
  69. package/dist/package/src/rpc/system/admin/v1/base_rate_limit_rule.ts +167 -0
  70. package/dist/package/src/utils/persisted-message.ts +21 -0
  71. package/dist/package/src/utils/rateLimit.ts +140 -0
  72. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +40 -5
  73. package/dist/package/src/views/ai/chat/components/XSender.vue +309 -3
  74. package/dist/package/src/views/ai/chat/index.vue +48 -1
  75. package/dist/package/src/views/ai/chat/modelDisplay.ts +67 -0
  76. package/dist/package/src/views/ai/chat/modelSelection.ts +31 -0
  77. package/dist/package/src/views/base/ai-provider/index.vue +613 -0
  78. package/dist/package/src/views/base/api-rate-limit-policy/index.vue +429 -0
  79. package/dist/package/src/views/base/backup-management/archive-record/index.vue +2 -0
  80. package/dist/package/src/views/base/backup-management/archive-restore/index.vue +2 -1
  81. package/dist/package/src/views/base/backup-management/backup-record/index.vue +2 -0
  82. package/dist/package/src/views/base/backup-management/backup-restore/index.vue +2 -1
  83. package/dist/package/src/views/base/config/index.vue +12 -13
  84. package/dist/package/src/views/base/dict/index.vue +2 -3
  85. package/dist/package/src/views/base/dict/item.vue +2 -3
  86. package/dist/package/src/views/base/job/index.vue +2 -3
  87. package/dist/package/src/views/base/job/log.vue +3 -2
  88. package/dist/package/src/views/base/menu/index.vue +27 -83
  89. package/dist/package/src/views/base/message/index.vue +9 -3
  90. package/dist/package/src/views/base/message-category/index.vue +34 -3
  91. package/dist/package/src/views/base/message-provider/index.vue +168 -0
  92. package/dist/package/src/views/base/message-template/index.vue +105 -0
  93. package/dist/package/src/views/base/migration/index.vue +1 -1
  94. package/dist/package/src/views/base/oauth-client/index.vue +6 -6
  95. package/dist/package/src/views/base/oauth-provider/index.vue +274 -0
  96. package/dist/package/src/views/base/rate-limit-rule/index.vue +327 -0
  97. package/dist/package/src/views/base/redact-output-policy/index.vue +1 -1
  98. package/dist/package/src/views/base/redact-rule/index.vue +12 -2
  99. package/dist/package/src/views/base/redact-storage-policy/index.vue +4 -0
  100. package/dist/package/src/views/base/tenant/index.vue +3 -3
  101. package/package.json +2 -2
@@ -0,0 +1,613 @@
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
+ :form-props="{ disabled: testingModels }"
14
+ :close-on-click-modal="!testingModels"
15
+ :close-on-press-escape="!testingModels"
16
+ @confirm="submit"
17
+ @close="resetForm"
18
+ >
19
+ <template #models>
20
+ <div class="provider-models">
21
+ <div v-if="form.models.length === 0" class="provider-models__empty">
22
+ {{ t("system.base.ai_provider.model.empty") }}
23
+ </div>
24
+ <div v-else class="provider-models__list">
25
+ <div v-for="(model, index) in form.models" :key="`${model.model_name}-${index}`" class="provider-models__item">
26
+ <div class="provider-models__content">
27
+ <div class="provider-models__heading">
28
+ <strong>{{ model.model_name }}</strong>
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>
31
+ <el-tooltip
32
+ v-if="modelTestResults[model.model_name]"
33
+ :content="modelTestResults[model.model_name]?.message"
34
+ :disabled="!modelTestResults[model.model_name]?.message"
35
+ placement="top"
36
+ >
37
+ <el-tag
38
+ size="small"
39
+ effect="plain"
40
+ :type="modelTestResults[model.model_name]?.success ? 'success' : 'danger'"
41
+ >
42
+ {{ 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
45
+ </el-tag>
46
+ </el-tooltip>
47
+ </div>
48
+ <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>
53
+ </div>
54
+ </div>
55
+ <div class="provider-models__actions">
56
+ <el-tooltip :content="t('common.action.edit')" placement="top">
57
+ <el-button text circle :disabled="testingModels" :icon="EditPen" :aria-label="t('common.action.edit')" @click="openModelDialog(index)" />
58
+ </el-tooltip>
59
+ <el-tooltip :content="t('common.action.delete')" placement="top">
60
+ <el-button text circle :disabled="testingModels" :icon="Delete" :aria-label="t('common.action.delete')" @click="removeModel(index)" />
61
+ </el-tooltip>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <el-button type="primary" plain :icon="CirclePlus" :disabled="testingModels" @click="openModelDialog()">
66
+ {{ t("system.base.ai_provider.model.add") }}
67
+ </el-button>
68
+ </div>
69
+ </template>
70
+ <template #footer>
71
+ <div class="dialog-footer">
72
+ <el-button :disabled="testingModels" @click="dialog.visible = false">
73
+ {{ t("common.action.cancel") }}
74
+ </el-button>
75
+ <el-button type="primary" plain :icon="Connection" :loading="testingModels" :disabled="!form.models.length" @click="testProviderModels">
76
+ {{ t("system.base.ai_provider.model.test") }}
77
+ </el-button>
78
+ <el-button type="primary" :disabled="testingModels" @click="submit">
79
+ {{ t("common.action.confirm") }}
80
+ </el-button>
81
+ </div>
82
+ </template>
83
+ </FormDialog>
84
+
85
+ <FormDialog
86
+ v-model="modelDialog.visible"
87
+ ref="modelDialogRef"
88
+ :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"
92
+ :model="modelForm"
93
+ :fields="modelFields"
94
+ :rules="modelRules"
95
+ @confirm="saveModel"
96
+ @close="resetModelDialog"
97
+ />
98
+ </div>
99
+ </template>
100
+
101
+ <script setup lang="ts">
102
+ import { computed, reactive, ref, watch } from "vue";
103
+ import { ElMessage, ElMessageBox } from "element-plus";
104
+ import type { FormRules } from "element-plus";
105
+ import { CirclePlus, Connection, Delete, EditPen } from "@element-plus/icons-vue";
106
+ import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
107
+ import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
108
+ import type { ColumnProps, HeaderActionProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
109
+ import type { ProFormField, ProFormOption } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
110
+ import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
111
+ import { buildPageRequest, normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
112
+ import { useTenantScope } from "@liujitcn/kratos-admin-core/tenant";
113
+ import { t } from "@liujitcn/kratos-admin-core";
114
+ import { defAiProviderService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/ai_provider";
115
+ import type {
116
+ AiProvider,
117
+ AiProviderForm,
118
+ AiProviderModelTestResult,
119
+ PageAiProviderRequest
120
+ } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/ai_provider";
121
+ import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
122
+
123
+ defineOptions({ name: "AiProvider", inheritAttrs: false });
124
+
125
+ /** Provider个性化配置编辑项。 */
126
+ interface ConfigItem {
127
+ key: string;
128
+ value: string;
129
+ }
130
+
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
+ /** 文本生成温度。 */
143
+ temperature: number;
144
+ /** 单次请求最大输出Token数。 */
145
+ max_tokens: number;
146
+ /** 模型请求超时秒数。 */
147
+ timeout_seconds: number;
148
+ /** 模型请求失败后的最大重试次数。 */
149
+ max_retries: number;
150
+ }
151
+
152
+ /** AI Provider页面表单状态。 */
153
+ interface FormState extends Omit<AiProviderForm, "models_json"> {
154
+ models: AiModelConfig[];
155
+ config_items: ConfigItem[];
156
+ }
157
+
158
+ const { BUTTONS } = useAuthButtons();
159
+ const { isDefaultTenant } = useTenantScope();
160
+ const table = ref<ProTableInstance>();
161
+ const dialogRef = ref<InstanceType<typeof FormDialog>>();
162
+ const modelDialogRef = ref<InstanceType<typeof FormDialog>>();
163
+ const dialog = reactive({ visible: false, editing: false });
164
+ const modelDialog = reactive({ visible: false, editing: false, index: -1 });
165
+ const form = reactive<FormState>(defaultForm());
166
+ const modelForm = reactive<AiModelConfig>(defaultModelConfig());
167
+ const testingModels = ref(false);
168
+ const modelTestResults = ref<Record<string, AiProviderModelTestResult>>({});
169
+
170
+ watch(
171
+ () => [form.provider, form.base_url, form.api_key, JSON.stringify(form.config_items), JSON.stringify(form.models)],
172
+ () => {
173
+ modelTestResults.value = {};
174
+ }
175
+ );
176
+
177
+ const statusOptions = computed<ProFormOption[]>(() => [
178
+ { label: t("common.status.enabled"), value: Status.STATUS_ENABLE },
179
+ { label: t("common.status.disabled"), value: Status.STATUS_DISABLE }
180
+ ]);
181
+
182
+ const providerOptions = computed<ProFormOption[]>(() => [
183
+ { label: t("system.base.ai_provider.provider.openai_compatible"), value: "openai_compatible" },
184
+ { label: t("system.base.ai_provider.provider.ollama"), value: "ollama" }
185
+ ]);
186
+
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" }
190
+ ]);
191
+
192
+ const fields = computed<ProFormField[]>(() => [
193
+ { 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") } },
197
+ { prop: "models", label: t("system.base.ai_provider.field.models"), component: "slot", slotName: "models", colSpan: 24 },
198
+ { prop: "config_items", label: t("system.base.ai_provider.field.config"), component: "kv-list", colSpan: 24, props: { keyInputProps: { maxlength: 128 } } },
199
+ { prop: "sort", label: t("common.field.sort"), component: "input-number", colSpan: 12, props: { min: 0, precision: 0 } },
200
+ { prop: "status", label: t("common.field.status"), component: "radio-group", colSpan: 12, options: statusOptions.value }
201
+ ]);
202
+
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
+ ]);
212
+
213
+ const rules = computed<FormRules>(() => ({
214
+ name: [{ required: true, message: t("system.base.ai_provider.validation.name"), trigger: "blur" }],
215
+ provider: [{ required: true, message: t("system.base.ai_provider.validation.provider"), trigger: "change" }],
216
+ models: [{
217
+ validator: (_rule, value: AiModelConfig[], callback) => {
218
+ if (!Array.isArray(value) || value.length === 0) {
219
+ callback(new Error(t("system.base.ai_provider.model.validation.required")));
220
+ return;
221
+ }
222
+ const names = value.map(model => model.model_name);
223
+ callback(names.some(name => !name)
224
+ ? new Error(t("system.base.ai_provider.validation.model_name"))
225
+ : new Set(names).size !== names.length
226
+ ? new Error(t("system.base.ai_provider.model.validation.duplicate"))
227
+ : undefined);
228
+ },
229
+ trigger: "change"
230
+ }]
231
+ }));
232
+
233
+ const modelRules = computed<FormRules>(() => ({
234
+ 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" }
237
+ ],
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" }]
244
+ }));
245
+
246
+ const columns = computed<ColumnProps[]>(() => [
247
+ { prop: "name", label: t("system.base.ai_provider.field.name"), minWidth: 150, search: { el: "input" } },
248
+ { prop: "provider", label: t("system.base.ai_provider.field.provider"), width: 190, search: { el: "select", enum: providerOptions.value }, enum: providerOptions.value },
249
+ { prop: "base_url", label: t("system.base.ai_provider.field.base_url"), minWidth: 220 },
250
+ { 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") },
251
+ { prop: "model_names", label: t("system.base.ai_provider.field.models"), minWidth: 220, formatter: row => (row.model_names ?? []).join(", ") },
252
+ { prop: "sort", label: t("common.field.sort"), width: 80, align: "right" },
253
+ {
254
+ prop: "status",
255
+ label: t("common.field.status"),
256
+ width: 110,
257
+ search: { el: "select", enum: statusOptions.value },
258
+ cellType: "status",
259
+ statusProps: {
260
+ activeValue: Status.STATUS_ENABLE,
261
+ inactiveValue: Status.STATUS_DISABLE,
262
+ activeText: t("common.status.enabled"),
263
+ inactiveText: t("common.status.disabled"),
264
+ disabled: () => !isDefaultTenant.value || !BUTTONS.value["base:ai-provider:status"],
265
+ beforeChange: scope => handleSetStatus(scope.row as AiProvider)
266
+ }
267
+ },
268
+ { prop: "updated_at", label: t("common.field.updated_at"), minWidth: 180, align: "center" },
269
+ {
270
+ prop: "operation",
271
+ label: t("common.field.operation"),
272
+ cellType: "actions",
273
+ actions: [
274
+ { 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) },
275
+ { 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) }
276
+ ]
277
+ }
278
+ ]);
279
+
280
+ const headerActions = computed<HeaderActionProps[]>(() => [
281
+ { label: t("common.action.create"), type: "primary", icon: CirclePlus, hidden: !isDefaultTenant.value || !BUTTONS.value["base:ai-provider:create"], onClick: () => openDialog() },
282
+ { 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[]) }
283
+ ]);
284
+
285
+ /** 创建默认供应商表单。 */
286
+ function defaultForm(): FormState {
287
+ return {
288
+ id: 0,
289
+ provider: "openai_compatible",
290
+ name: "",
291
+ base_url: "",
292
+ api_key: "",
293
+ api_key_configured: false,
294
+ models: [],
295
+ config: {},
296
+ config_items: [],
297
+ sort: 0,
298
+ status: Status.STATUS_DISABLE
299
+ };
300
+ }
301
+
302
+ /** 请求AI供应商表格数据。 */
303
+ async function requestTable(params: Record<string, unknown>) {
304
+ const data = await defAiProviderService.PageAiProvider(buildPageRequest<PageAiProviderRequest>(params as unknown as PageAiProviderRequest));
305
+ return { data: { list: data.providers ?? [], total: data.total } };
306
+ }
307
+
308
+ /** 打开供应商编辑表单。 */
309
+ async function openDialog(id?: number) {
310
+ await dialogRef.value?.open({
311
+ load: () => (id ? defAiProviderService.GetAiProvider({ id }) : undefined),
312
+ commit: value => {
313
+ if (value) {
314
+ const { models_json, ...provider } = value;
315
+ Object.assign(form, defaultForm(), provider, {
316
+ models: parseModelConfigs(models_json),
317
+ config_items: configToItems(value.config)
318
+ });
319
+ } else {
320
+ Object.assign(form, defaultForm());
321
+ }
322
+ dialog.editing = Boolean(id);
323
+ }
324
+ });
325
+ }
326
+
327
+ /** 提交AI供应商表单。 */
328
+ async function submit() {
329
+ const valid = await dialogRef.value?.validate();
330
+ if (!valid) return;
331
+ const payload = buildAiProviderPayload();
332
+ if (!payload) return;
333
+ if (dialog.editing) await defAiProviderService.UpdateAiProvider({ ai_provider: payload });
334
+ else await defAiProviderService.CreateAiProvider({ ai_provider: payload });
335
+ ElMessage.success(t(dialog.editing ? "system.base.ai_provider.message.update_success" : "system.base.ai_provider.message.create_success"));
336
+ dialog.visible = false;
337
+ resetForm();
338
+ table.value?.getTableList();
339
+ }
340
+
341
+ /** 测试当前草稿中的全部模型并展示逐项结果。 */
342
+ async function testProviderModels() {
343
+ const valid = await dialogRef.value?.validate();
344
+ if (!valid) return;
345
+ const payload = buildAiProviderPayload();
346
+ if (!payload) return;
347
+
348
+ testingModels.value = true;
349
+ modelTestResults.value = {};
350
+ let response: Awaited<ReturnType<typeof defAiProviderService.TestAiProviderModels>>;
351
+ try {
352
+ response = await defAiProviderService.TestAiProviderModels({ ai_provider: payload });
353
+ } finally {
354
+ testingModels.value = false;
355
+ }
356
+ modelTestResults.value = Object.fromEntries(response.results.map(result => [result.model_name, result]));
357
+ }
358
+
359
+ /** 构造AI Provider保存或测试请求参数。 */
360
+ function buildAiProviderPayload(): AiProviderForm | undefined {
361
+ const config = configItemsToMap(form.config_items);
362
+ if (!config) return undefined;
363
+ 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),
371
+ config,
372
+ sort: form.sort,
373
+ status: form.status
374
+ };
375
+ }
376
+
377
+ /** 重置供应商表单。 */
378
+ function resetForm() {
379
+ Object.assign(form, defaultForm());
380
+ dialog.editing = false;
381
+ modelDialog.visible = false;
382
+ resetModelDialog();
383
+ }
384
+
385
+ /** 打开模型新增或编辑表单。 */
386
+ function openModelDialog(index?: number) {
387
+ modelDialog.index = index ?? -1;
388
+ modelDialog.editing = index !== undefined;
389
+ Object.assign(modelForm, index === undefined ? defaultModelConfig() : { ...form.models[index] });
390
+ modelDialog.visible = true;
391
+ }
392
+
393
+ /** 校验并保存当前模型表单。 */
394
+ async function saveModel() {
395
+ const valid = await modelDialogRef.value?.validate();
396
+ if (!valid) return;
397
+ const duplicated = form.models.some((model, index) => index !== modelDialog.index && model.model_name === modelForm.model_name);
398
+ if (duplicated) {
399
+ ElMessage.warning(t("system.base.ai_provider.model.validation.duplicate"));
400
+ return;
401
+ }
402
+ if (modelDialog.index < 0) form.models.push({ ...modelForm });
403
+ else form.models.splice(modelDialog.index, 1, { ...modelForm });
404
+ modelDialog.visible = false;
405
+ }
406
+
407
+ /** 删除模型表单项。 */
408
+ async function removeModel(index: number) {
409
+ try {
410
+ await ElMessageBox.confirm(
411
+ t("system.base.ai_provider.model.confirm_remove", { name: form.models[index].model_name }),
412
+ t("common.action.delete"),
413
+ { type: "warning" }
414
+ );
415
+ form.models.splice(index, 1);
416
+ } catch {
417
+ return;
418
+ }
419
+ }
420
+
421
+ /** 重置模型编辑弹窗状态。 */
422
+ function resetModelDialog() {
423
+ modelDialog.editing = false;
424
+ modelDialog.index = -1;
425
+ }
426
+
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配置对象转换为键值表单项。 */
454
+ function configToItems(config: Record<string, unknown> | undefined): ConfigItem[] {
455
+ return Object.entries(config ?? {}).map(([key, value]) => ({ key, value: typeof value === "string" ? value : JSON.stringify(value) }));
456
+ }
457
+
458
+ /** 将键值表单项转换为Provider配置JSON对象。 */
459
+ function configItemsToMap(items: ConfigItem[]): Record<string, unknown> | undefined {
460
+ const result: Record<string, unknown> = {};
461
+ for (const item of items) {
462
+ const key = item.key.trim();
463
+ if (!key || Object.prototype.hasOwnProperty.call(result, key)) {
464
+ ElMessage.error(t("system.base.ai_provider.validation.config_invalid"));
465
+ return undefined;
466
+ }
467
+ const value = item.value.trim();
468
+ if (!value) {
469
+ result[key] = "";
470
+ continue;
471
+ }
472
+ try {
473
+ result[key] = JSON.parse(value);
474
+ } catch {
475
+ result[key] = value;
476
+ }
477
+ }
478
+ return result;
479
+ }
480
+
481
+ /** 批量删除AI供应商。 */
482
+ async function handleDelete(target: AiProvider | number | Array<number>) {
483
+ const row = typeof target === "object" && !Array.isArray(target) ? target : undefined;
484
+ const ids = (row ? [row.id] : normalizeSelectedIds(target as number | number[])).map(Number);
485
+ if (!ids.length) return;
486
+ try {
487
+ await ElMessageBox.confirm(t("system.base.ai_provider.message.confirm_delete", { name: row?.name ?? ids.join(", ") }), t("common.action.delete"), { type: "warning" });
488
+ } catch {
489
+ return;
490
+ }
491
+ for (const id of ids) await defAiProviderService.DeleteAiProvider({ id });
492
+ ElMessage.success(t("system.base.ai_provider.message.delete_success"));
493
+ table.value?.getTableList();
494
+ }
495
+
496
+ /** 更新AI供应商状态。 */
497
+ async function handleSetStatus(row: AiProvider) {
498
+ const status = row.status === Status.STATUS_ENABLE ? Status.STATUS_DISABLE : Status.STATUS_ENABLE;
499
+ try {
500
+ await ElMessageBox.confirm(
501
+ t("common.dialog.status_change", {
502
+ action: t(status === Status.STATUS_ENABLE ? "common.status.enabled" : "common.status.disabled"),
503
+ resource: t("system.base.ai_provider.resource"),
504
+ field: t("system.base.ai_provider.field.name"),
505
+ value: row.name
506
+ }),
507
+ t("common.title.notice"),
508
+ { type: "warning" }
509
+ );
510
+ await defAiProviderService.SetAiProviderStatus({ id: row.id, status });
511
+ ElMessage.success(t("system.base.ai_provider.message.status_success"));
512
+ table.value?.getTableList();
513
+ return true;
514
+ } catch {
515
+ return false;
516
+ }
517
+ }
518
+ </script>
519
+
520
+ <style scoped>
521
+ .provider-models {
522
+ display: flex;
523
+ flex-direction: column;
524
+ align-items: flex-start;
525
+ gap: 12px;
526
+ width: 100%;
527
+ }
528
+
529
+ .provider-models__empty {
530
+ width: 100%;
531
+ padding: 16px;
532
+ border: 1px dashed var(--el-border-color);
533
+ border-radius: 6px;
534
+ color: var(--el-text-color-secondary);
535
+ text-align: center;
536
+ }
537
+
538
+ .provider-models__list {
539
+ width: 100%;
540
+ overflow: hidden;
541
+ border: 1px solid var(--el-border-color);
542
+ border-radius: 6px;
543
+ }
544
+
545
+ .provider-models__item {
546
+ display: flex;
547
+ align-items: center;
548
+ 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);
556
+ }
557
+
558
+ .provider-models__content {
559
+ display: flex;
560
+ flex-direction: column;
561
+ gap: 8px;
562
+ min-width: 0;
563
+ }
564
+
565
+ .provider-models__heading,
566
+ .provider-models__details,
567
+ .provider-models__actions {
568
+ display: flex;
569
+ align-items: center;
570
+ }
571
+
572
+ .provider-models__heading {
573
+ flex-wrap: wrap;
574
+ gap: 8px;
575
+ min-width: 0;
576
+ }
577
+
578
+ .provider-models__heading strong {
579
+ overflow-wrap: anywhere;
580
+ }
581
+
582
+ .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;
593
+ }
594
+
595
+ .provider-models__details {
596
+ flex-wrap: wrap;
597
+ gap: 4px 14px;
598
+ color: var(--el-text-color-secondary);
599
+ font-size: 12px;
600
+ }
601
+
602
+ .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
+ }
612
+ }
613
+ </style>