feat: add StepFun and Nous Portal provider support (#140)

- Add StepFun provider (API key auth, STEPFUN_API_KEY)
- Add Nous Portal provider with full OAuth device code flow
  (device code request → poll for token → mint agent key → save to auth.json)
- Add NousLoginModal component for OAuth UI (user code display + verification link)
- Update ProviderFormModal to handle Nous OAuth flow (hide API key fields)
- Add nous-auth backend controller and routes
- Update PROVIDER_ENV_MAP with stepfun and nous entries
- Add i18n translations for Nous OAuth in all 8 locales

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-23 08:39:19 +08:00
committed by GitHub
parent 696d19298e
commit df797d09b2
16 changed files with 692 additions and 3 deletions
@@ -4,6 +4,7 @@ import { NModal, NForm, NFormItem, NInput, NButton, NSelect, useMessage } from '
import { useModelsStore } from '@/stores/hermes/models'
import { useI18n } from 'vue-i18n'
import CodexLoginModal from './CodexLoginModal.vue'
import NousLoginModal from './NousLoginModal.vue'
const { t } = useI18n()
@@ -19,6 +20,7 @@ const showModal = ref(true)
const loading = ref(false)
const fetchingModels = ref(false)
const showCodexLogin = ref(false)
const showNousLogin = ref(false)
const providerType = ref<'preset' | 'custom'>('preset')
const selectedPreset = ref<string | null>(null)
@@ -32,8 +34,10 @@ const formData = ref({
const modelOptions = ref<Array<{ label: string; value: string }>>([])
const CODEX_KEY = 'openai-codex'
const NOUS_KEY = 'nous'
const isCodex = computed(() => selectedPreset.value === CODEX_KEY)
const isNous = computed(() => selectedPreset.value === NOUS_KEY)
const presetOptions = computed(() =>
modelsStore.allProviders.map(g => ({ label: g.label, value: g.provider })),
@@ -125,6 +129,12 @@ async function handleSave() {
return
}
// Nous: 弹出 OAuth 设备码弹窗
if (isNous.value) {
showNousLogin.value = true
return
}
if (!formData.value.base_url.trim()) {
message.warning(t('models.baseUrlRequired'))
return
@@ -166,6 +176,12 @@ async function handleCodexSuccess() {
emit('saved')
}
async function handleNousSuccess() {
showNousLogin.value = false
message.success(t('models.providerAdded'))
emit('saved')
}
function handleClose() {
showModal.value = false
setTimeout(() => emit('close'), 200)
@@ -178,7 +194,7 @@ function handleClose() {
preset="card"
:title="t('models.addProvider')"
:style="{ width: 'min(520px, calc(100vw - 32px))' }"
:mask-closable="!loading && !showCodexLogin"
:mask-closable="!loading && !showCodexLogin && !showNousLogin"
@after-leave="emit('close')"
>
<NForm label-placement="top">
@@ -217,7 +233,7 @@ function handleClose() {
/>
</NFormItem>
<NFormItem v-if="!isCodex" :label="t('models.baseUrl')" required>
<NFormItem v-if="!isCodex && !isNous" :label="t('models.baseUrl')" required>
<NInput
v-model:value="formData.base_url"
:placeholder="t('models.baseUrlPlaceholder')"
@@ -225,7 +241,7 @@ function handleClose() {
/>
</NFormItem>
<NFormItem v-if="!isCodex" :label="t('models.apiKey')" required>
<NFormItem v-if="!isCodex && !isNous" :label="t('models.apiKey')" required>
<NInput
v-model:value="formData.api_key"
type="password"
@@ -270,6 +286,12 @@ function handleClose() {
@close="showCodexLogin = false"
@success="handleCodexSuccess"
/>
<NousLoginModal
v-if="showNousLogin"
@close="showNousLogin = false"
@success="handleNousSuccess"
/>
</NModal>
</template>