mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge pull request #419 from wendaochangsheng/aether-rust-pioneer
修复 Vertex AI 服务账号 JSON 导入入口
This commit is contained in:
@@ -32,7 +32,24 @@
|
|||||||
data-1p-ignore="true"
|
data-1p-ignore="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="showAuthTypeSelector">
|
||||||
|
<Label :for="authTypeSelectId">认证类型</Label>
|
||||||
|
<Select v-model="form.auth_type">
|
||||||
|
<SelectTrigger :id="authTypeSelectId">
|
||||||
|
<SelectValue placeholder="选择认证类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="option in authTypeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div :class="showAuthTypeSelector ? 'col-span-2' : undefined">
|
||||||
<Label :for="apiKeyInputId">
|
<Label :for="apiKeyInputId">
|
||||||
{{ authSecretLabel }}
|
{{ authSecretLabel }}
|
||||||
{{ authSecretRequiredMark }}
|
{{ authSecretRequiredMark }}
|
||||||
@@ -360,7 +377,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
|
import {
|
||||||
|
Dialog,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Switch,
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
} from '@/components/ui'
|
||||||
import { Key, SquarePen, CircleHelp } from 'lucide-vue-next'
|
import { Key, SquarePen, CircleHelp } from 'lucide-vue-next'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useFormDialog } from '@/composables/useFormDialog'
|
import { useFormDialog } from '@/composables/useFormDialog'
|
||||||
@@ -527,6 +555,7 @@ function getDefaultApiFormats(): string[] {
|
|||||||
const visibleApiFormats = computed(() => getSelectableApiFormats())
|
const visibleApiFormats = computed(() => getSelectableApiFormats())
|
||||||
|
|
||||||
const authTypeOptions = computed(() => getAuthTypeOptions(props.providerType))
|
const authTypeOptions = computed(() => getAuthTypeOptions(props.providerType))
|
||||||
|
const showAuthTypeSelector = computed(() => props.providerType === 'vertex_ai')
|
||||||
|
|
||||||
const apiFormatHelpOpen = ref(false)
|
const apiFormatHelpOpen = ref(false)
|
||||||
const apiFormatHelpHovered = ref(false)
|
const apiFormatHelpHovered = ref(false)
|
||||||
@@ -677,6 +706,7 @@ const saving = ref(false)
|
|||||||
const formNonce = ref(createFieldNonce())
|
const formNonce = ref(createFieldNonce())
|
||||||
const keyNameInputId = computed(() => `key-name-${formNonce.value}`)
|
const keyNameInputId = computed(() => `key-name-${formNonce.value}`)
|
||||||
const apiKeyInputId = computed(() => `api-key-${formNonce.value}`)
|
const apiKeyInputId = computed(() => `api-key-${formNonce.value}`)
|
||||||
|
const authTypeSelectId = computed(() => `auth-type-${formNonce.value}`)
|
||||||
const keyNameFieldName = computed(() => `key-name-field-${formNonce.value}`)
|
const keyNameFieldName = computed(() => `key-name-field-${formNonce.value}`)
|
||||||
const apiKeyFieldName = computed(() => `api-key-field-${formNonce.value}`)
|
const apiKeyFieldName = computed(() => `api-key-field-${formNonce.value}`)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ vi.mock('@/api/endpoints', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/components/ui', async () => {
|
vi.mock('@/components/ui', async () => {
|
||||||
const { defineComponent, h } = await import('vue')
|
const { defineComponent, h, inject, provide } = await import('vue')
|
||||||
|
const SelectContextKey = Symbol('SelectContext')
|
||||||
|
|
||||||
const passthrough = (name: string, tag = 'div') => defineComponent({
|
const passthrough = (name: string, tag = 'div') => defineComponent({
|
||||||
name,
|
name,
|
||||||
@@ -104,17 +105,56 @@ vi.mock('@/components/ui', async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const Select = defineComponent({
|
||||||
|
name: 'SelectStub',
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit, slots }) {
|
||||||
|
provide(SelectContextKey, {
|
||||||
|
select: (value: string) => emit('update:modelValue', value),
|
||||||
|
modelValue: props.modelValue,
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => h('div', {
|
||||||
|
'data-select': 'true',
|
||||||
|
'data-value': props.modelValue,
|
||||||
|
}, slots.default?.())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const SelectItem = defineComponent({
|
||||||
|
name: 'SelectItemStub',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { attrs, slots }) {
|
||||||
|
const context = inject<{ select: (value: string) => void } | null>(SelectContextKey, null)
|
||||||
|
return () => h('button', {
|
||||||
|
...attrs,
|
||||||
|
type: 'button',
|
||||||
|
'data-select-item': props.value,
|
||||||
|
onClick: () => context?.select(props.value),
|
||||||
|
}, slots.default?.())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Dialog,
|
Dialog,
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Switch,
|
Switch,
|
||||||
Select: passthrough('SelectStub'),
|
Select,
|
||||||
SelectTrigger: passthrough('SelectTriggerStub'),
|
SelectTrigger: passthrough('SelectTriggerStub'),
|
||||||
SelectValue: passthrough('SelectValueStub', 'span'),
|
SelectValue: passthrough('SelectValueStub', 'span'),
|
||||||
SelectContent: passthrough('SelectContentStub'),
|
SelectContent: passthrough('SelectContentStub'),
|
||||||
SelectItem: passthrough('SelectItemStub'),
|
SelectItem,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -124,8 +164,18 @@ vi.mock('@/components/common/JsonImportInput.vue', async () => {
|
|||||||
return {
|
return {
|
||||||
default: defineComponent({
|
default: defineComponent({
|
||||||
name: 'JsonImportInputStub',
|
name: 'JsonImportInputStub',
|
||||||
setup() {
|
props: {
|
||||||
return () => h('textarea')
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
return () => h('textarea', {
|
||||||
|
value: props.modelValue,
|
||||||
|
onInput: (event: Event) => emit('update:modelValue', (event.target as HTMLTextAreaElement).value),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -222,6 +272,11 @@ function updateInput(input: HTMLInputElement, value: string) {
|
|||||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTextarea(textarea: HTMLTextAreaElement, value: string) {
|
||||||
|
textarea.value = value
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
|
||||||
async function submit(root: HTMLElement) {
|
async function submit(root: HTMLElement) {
|
||||||
const form = root.querySelector('form')
|
const form = root.querySelector('form')
|
||||||
expect(form).not.toBeNull()
|
expect(form).not.toBeNull()
|
||||||
@@ -254,6 +309,47 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('provider key concurrent_limit form behavior', () => {
|
describe('provider key concurrent_limit form behavior', () => {
|
||||||
|
it('lets Vertex AI keys switch to Service Account JSON and submit auth_config', async () => {
|
||||||
|
const root = mountDialog(KeyFormDialog, {
|
||||||
|
open: true,
|
||||||
|
endpoint: null,
|
||||||
|
editingKey: null,
|
||||||
|
providerId: 'provider-vertex',
|
||||||
|
providerType: 'vertex_ai',
|
||||||
|
availableApiFormats: ['gemini:generate_content', 'claude:messages'],
|
||||||
|
})
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
const serviceAccountOption = root.querySelector<HTMLButtonElement>('[data-select-item="service_account"]')
|
||||||
|
expect(serviceAccountOption).not.toBeNull()
|
||||||
|
serviceAccountOption?.click()
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
const nameInput = root.querySelector<HTMLInputElement>('input[placeholder="例如:主 Key、备用 Key 1"]')
|
||||||
|
expect(nameInput).not.toBeNull()
|
||||||
|
updateInput(nameInput as HTMLInputElement, 'Vertex service account')
|
||||||
|
|
||||||
|
const textarea = root.querySelector<HTMLTextAreaElement>('textarea')
|
||||||
|
expect(textarea).not.toBeNull()
|
||||||
|
updateTextarea(textarea as HTMLTextAreaElement, JSON.stringify({
|
||||||
|
client_email: 'svc@example.iam.gserviceaccount.com',
|
||||||
|
private_key: '-----BEGIN PRIVATE KEY-----\\nTEST\\n-----END PRIVATE KEY-----\\n',
|
||||||
|
project_id: 'demo-project',
|
||||||
|
}))
|
||||||
|
|
||||||
|
await submit(root)
|
||||||
|
|
||||||
|
expect(endpointMocks.addProviderKey).toHaveBeenCalledWith('provider-vertex', expect.objectContaining({
|
||||||
|
auth_type: 'service_account',
|
||||||
|
auth_config: expect.objectContaining({
|
||||||
|
client_email: 'svc@example.iam.gserviceaccount.com',
|
||||||
|
private_key: '-----BEGIN PRIVATE KEY-----\\nTEST\\n-----END PRIVATE KEY-----\\n',
|
||||||
|
project_id: 'demo-project',
|
||||||
|
}),
|
||||||
|
api_formats: ['gemini:generate_content'],
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
it('hydrates and serializes a positive concurrent_limit number from the normal key form', async () => {
|
it('hydrates and serializes a positive concurrent_limit number from the normal key form', async () => {
|
||||||
const root = mountDialog(KeyFormDialog, {
|
const root = mountDialog(KeyFormDialog, {
|
||||||
open: true,
|
open: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user