diff --git a/backend/crates/gateway-admin/src/model/accounts.rs b/backend/crates/gateway-admin/src/model/accounts.rs
index fe3e7601d..0e287609a 100644
--- a/backend/crates/gateway-admin/src/model/accounts.rs
+++ b/backend/crates/gateway-admin/src/model/accounts.rs
@@ -18,6 +18,15 @@ pub use gateway_core::account::{
QuotaState, resolve_account_status,
};
+/// 导入时统一应用的账号调度与分组设置;缺省时保留原有导入语义。
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AccountImportSettings {
+ pub enabled: bool,
+ pub concurrency_limit: Option,
+ pub weight: AccountWeight,
+ pub group_ids: Vec,
+}
+
/// 账号列表排序字段。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountSortField {
diff --git a/backend/crates/gateway-admin/src/model/provider_credentials.rs b/backend/crates/gateway-admin/src/model/provider_credentials.rs
index bbf1d7da6..255a95fd6 100644
--- a/backend/crates/gateway-admin/src/model/provider_credentials.rs
+++ b/backend/crates/gateway-admin/src/model/provider_credentials.rs
@@ -16,7 +16,9 @@ use uuid::Uuid;
use super::{
AdminError, MutationActor, MutationContext, PageSize, Revision,
- accounts::{AccountRecord, AccountSummary, AccountUsage, CredentialState},
+ accounts::{
+ AccountImportSettings, AccountRecord, AccountSummary, AccountUsage, CredentialState,
+ },
};
/// Provider-owned JSON;公共层只搬运且 Debug 不输出值。
@@ -106,6 +108,7 @@ pub struct CredentialDetails {
/// Provider 正式文档批量导入命令。
pub struct ImportCredentials {
+ pub settings: Option,
pub context: MutationContext,
pub document: ProviderDocument,
}
@@ -172,6 +175,7 @@ pub struct PreparedCredentialImport {
/// Admin 交给 Store 的导入事务命令。
#[derive(Debug, Clone, PartialEq)]
pub struct CredentialImportCommit {
+ pub settings: Option,
pub prepared: PreparedCredentialImport,
}
@@ -480,6 +484,7 @@ pub enum AuthorizationCredentialCommit {
/// Admin 交给 Store 的 OAuth 原子事务命令。
#[derive(Debug, Clone, PartialEq)]
pub struct AuthorizationCommit {
+ pub settings: Option,
pub pending: PendingAuthorizationMutation,
pub credential: AuthorizationCredentialCommit,
}
@@ -514,7 +519,10 @@ impl PreparedAuthorizationCommit {
self
}
- pub(crate) fn into_commit(self) -> AuthorizationCommitSettlement {
+ pub(crate) fn into_commit(
+ self,
+ settings: Option,
+ ) -> AuthorizationCommitSettlement {
let (credential, guard) = match self.credential {
PreparedAuthorizationCredential::Create(credential) => {
(AuthorizationCredentialCommit::Create(credential), None)
@@ -529,6 +537,7 @@ impl PreparedAuthorizationCommit {
};
AuthorizationCommitSettlement {
command: AuthorizationCommit {
+ settings,
pending: self.pending,
credential,
},
@@ -542,7 +551,7 @@ impl PreparedAuthorizationCommit {
credential_guard,
authorization_guard,
..
- } = self.into_commit();
+ } = self.into_commit(None);
drop(credential_guard);
if let Some(guard) = authorization_guard {
guard.abort().await?;
@@ -554,6 +563,7 @@ impl PreparedAuthorizationCommit {
/// 完成 Provider OAuth 流程。
#[derive(Clone, PartialEq, Eq)]
pub struct CompleteAuthorization {
+ pub settings: Option,
pub context: MutationContext,
pub flow_id: String,
pub callback_url: String,
diff --git a/backend/crates/gateway-admin/src/use_case/mod.rs b/backend/crates/gateway-admin/src/use_case/mod.rs
index c2b11bbba..f2395b5f1 100644
--- a/backend/crates/gateway-admin/src/use_case/mod.rs
+++ b/backend/crates/gateway-admin/src/use_case/mod.rs
@@ -222,14 +222,24 @@ async fn validate_authorization_commit(
async fn commit_authorization(
accounts: &dyn AccountStore,
prepared: PreparedAuthorizationCommit,
+ settings: Option,
context: &MutationContext,
resource: &'static str,
) -> Result {
+ if settings.is_some()
+ && matches!(
+ &prepared.credential,
+ PreparedAuthorizationCredential::Reauthorize(_)
+ )
+ {
+ prepared.abort().await?;
+ return Err(AdminError::invalid("重新授权不能修改账号设置"));
+ }
let crate::model::provider_credentials::AuthorizationCommitSettlement {
command,
credential_guard,
authorization_guard,
- } = prepared.into_commit();
+ } = prepared.into_commit(settings);
match accounts.commit_authorization(command, context).await {
Ok(result) => {
if let Some(guard) = credential_guard {
diff --git a/backend/crates/gateway-admin/src/use_case/openai.rs b/backend/crates/gateway-admin/src/use_case/openai.rs
index 85f47638f..09a602780 100644
--- a/backend/crates/gateway-admin/src/use_case/openai.rs
+++ b/backend/crates/gateway-admin/src/use_case/openai.rs
@@ -142,7 +142,13 @@ impl OpenAiService for DefaultOpenAiService {
)?;
let result = self
.accounts
- .commit_credential_import(CredentialImportCommit { prepared }, &context)
+ .commit_credential_import(
+ CredentialImportCommit {
+ prepared,
+ settings: command.settings,
+ },
+ &context,
+ )
.await
.map_err(|error| map_store_error(error, "OpenAI credential import"))?;
self.provider
@@ -173,9 +179,10 @@ impl OpenAiService for DefaultOpenAiService {
async fn complete_authorization(
&self,
- command: CompleteAuthorization,
+ mut command: CompleteAuthorization,
) -> Result {
let context = command.context.clone();
+ let settings = command.settings.take();
let prepared = self
.provider
.complete_authorization(command)
@@ -191,6 +198,7 @@ impl OpenAiService for DefaultOpenAiService {
let result = commit_authorization(
self.accounts.as_ref(),
prepared,
+ settings,
&context,
"OpenAI authorization",
)
diff --git a/backend/crates/gateway-admin/src/use_case/xai.rs b/backend/crates/gateway-admin/src/use_case/xai.rs
index 82e452977..f7faf523f 100644
--- a/backend/crates/gateway-admin/src/use_case/xai.rs
+++ b/backend/crates/gateway-admin/src/use_case/xai.rs
@@ -95,7 +95,13 @@ impl XaiService for DefaultXaiService {
)?;
let result = self
.accounts
- .commit_credential_import(CredentialImportCommit { prepared }, &context)
+ .commit_credential_import(
+ CredentialImportCommit {
+ prepared,
+ settings: command.settings,
+ },
+ &context,
+ )
.await
.map_err(|error| map_store_error(error, "xAI credential import"))?;
publish_committed(self.snapshot.as_ref(), result.config_revision).await?;
@@ -132,9 +138,10 @@ impl XaiService for DefaultXaiService {
async fn complete_authorization(
&self,
- command: CompleteAuthorization,
+ mut command: CompleteAuthorization,
) -> Result {
let context = command.context.clone();
+ let settings = command.settings.take();
let prepared = self
.provider
.complete_authorization(command)
@@ -150,6 +157,7 @@ impl XaiService for DefaultXaiService {
let result = commit_authorization(
self.accounts.as_ref(),
prepared,
+ settings,
&context,
"xAI authorization",
)
diff --git a/backend/crates/gateway-admin/tests/use_case/accounts.rs b/backend/crates/gateway-admin/tests/use_case/accounts.rs
index a6a823fd3..d9bdabdcd 100644
--- a/backend/crates/gateway-admin/tests/use_case/accounts.rs
+++ b/backend/crates/gateway-admin/tests/use_case/accounts.rs
@@ -441,6 +441,7 @@ pub(super) struct FakeAccountStore {
account_after_probe: Mutex
-
+
-
+
返回
diff --git a/frontend/src/views/accounts/components/AccountSettingsFields.vue b/frontend/src/views/accounts/components/AccountSettingsFields.vue
new file mode 100644
index 000000000..7f0f34921
--- /dev/null
+++ b/frontend/src/views/accounts/components/AccountSettingsFields.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+ 调度
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/accounts/composables/useAccountOnboarding.ts b/frontend/src/views/accounts/composables/useAccountOnboarding.ts
index 49cde9f20..f65ce22f5 100644
--- a/frontend/src/views/accounts/composables/useAccountOnboarding.ts
+++ b/frontend/src/views/accounts/composables/useAccountOnboarding.ts
@@ -11,7 +11,7 @@ import { useAsyncAction } from '@/composables/useAsyncAction'
import { errorMessage } from '@/utils/async'
import { isRecord } from '@/utils/object'
import { formatProviderLabel, isSupportedProvider } from '@/utils/providers'
-import { emptyAccountCreateForm } from '../components/AccountCreateModal/model'
+import { accountImportSettings, accountProxyError, emptyAccountCreateForm } from '../components/AccountCreateModal/model'
type AccountRow = Awaited>['items'][number]
type ImportProvider = 'openai' | 'xai'
@@ -57,6 +57,9 @@ export function useAccountOnboarding(options: {
await creatingAccountAction.run(
async () => {
+ const proxyError = accountProxyError(createForm.value)
+ if (proxyError)
+ throw new Error(proxyError)
const message = createForm.value.provider === 'batch'
? await importMixedAccountDocument()
: await importAccountDocument()
@@ -72,10 +75,11 @@ export function useAccountOnboarding(options: {
await authorizingOAuthAction.run(
async () => {
- const input = await newAccountInput()
+ const input = newAccountInput()
const account = reauthorizingAccount.value
- if (!account && createForm.value.proxyMode === 'proxy' && !createForm.value.proxyUrl.trim())
- throw new Error('请输入代理 URL')
+ const proxyError = accountProxyError(createForm.value)
+ if (!account && proxyError)
+ throw new Error(proxyError)
const result = await startAccountOAuth({
...input,
outboundProxyUrl: !account && createForm.value.proxyMode === 'proxy' ? createForm.value.proxyUrl.trim() : undefined,
@@ -117,6 +121,7 @@ export function useAccountOnboarding(options: {
provider: createForm.value.provider,
flowId: createForm.value.oauthFlowId,
callbackUrl,
+ settings: reauthorizingAccount.value ? undefined : accountImportSettings(createForm.value),
})
await finishCreate(
reauthorizingAccount.value
@@ -145,14 +150,14 @@ export function useAccountOnboarding(options: {
createForm.value = {
...emptyAccountCreateForm(),
provider: account.provider,
- name: account.name,
+ step: 'import',
mode: 'oauth',
}
showCreateModal.value = true
void handleAuthorizeOAuth()
}
- async function newAccountInput() {
+ function newAccountInput() {
const account = reauthorizingAccount.value
return {
provider: createForm.value.provider,
@@ -162,16 +167,22 @@ export function useAccountOnboarding(options: {
async function importAccountDocument() {
const provider = requireImportProvider(createForm.value.provider)
+ const mode = createForm.value.mode
+ if (mode === 'oauth')
+ throw new Error('请选择凭据导入方式')
const documents = accountImportDocuments(
provider,
- createForm.value.mode,
- createForm.value.importText,
+ mode,
+ createForm.value.importTexts[mode],
)
let importedCount = 0
for (const entry of documents) {
const result = await importAccounts({
provider,
- data: entry.document,
+ settings: accountImportSettings(createForm.value),
+ data: createForm.value.proxyMode === 'proxy'
+ ? withDefaultImportProxy(entry.document, createForm.value.proxyUrl.trim())
+ : entry.document,
})
importedCount += result.importedCount
}
@@ -179,7 +190,7 @@ export function useAccountOnboarding(options: {
}
async function importMixedAccountDocument() {
- const documents = parseMixedImportDocuments(parseImportJson(createForm.value.importText))
+ const documents = parseMixedImportDocuments(parseImportJson(createForm.value.importTexts.json))
let importedCount = 0
const failures: string[] = []
@@ -187,7 +198,10 @@ export function useAccountOnboarding(options: {
try {
const result = await importAccounts({
provider: entry.provider,
- data: entry.document,
+ settings: accountImportSettings(createForm.value),
+ data: createForm.value.proxyMode === 'proxy'
+ ? withDefaultImportProxy(entry.document, createForm.value.proxyUrl.trim())
+ : entry.document,
})
importedCount += result.importedCount
}
@@ -219,11 +233,26 @@ export function useAccountOnboarding(options: {
createForm.value = {
...createForm.value,
mode: createForm.value.provider === 'batch' ? 'json' : 'oauth',
+ importTexts: { access_token: '', refresh_token: '', json: '' },
oauthFlowId: '',
oauthAuthUrl: '',
oauthCallback: '',
}
},
+ { flush: 'sync' },
+ )
+
+ watch(
+ [
+ () => createForm.value.proxyMode,
+ () => createForm.value.proxyMode === 'proxy' ? createForm.value.proxyUrl.trim() : '',
+ ],
+ () => {
+ createForm.value.oauthFlowId = ''
+ createForm.value.oauthAuthUrl = ''
+ createForm.value.oauthCallback = ''
+ },
+ { flush: 'sync' },
)
return {
@@ -239,6 +268,20 @@ export function useAccountOnboarding(options: {
}
}
+function withDefaultImportProxy(document: Record, proxyUrl: string): Record {
+ if (isRecord(document.data) && Array.isArray(document.data.accounts))
+ return { ...document, data: withDefaultImportProxy(document.data, proxyUrl) }
+ if (Array.isArray(document.accounts)) {
+ return {
+ ...document,
+ accounts: document.accounts.map(account => isRecord(account) ? withDefaultImportProxy(account, proxyUrl) : account),
+ }
+ }
+ if (['outboundProxyUrl', 'outbound_proxy_url', 'proxy_key'].some(key => Object.hasOwn(document, key)))
+ return document
+ return { ...document, outboundProxyUrl: proxyUrl }
+}
+
function parseImportJson(value: string) {
try {
return JSON.parse(value)
diff --git a/frontend/src/views/accounts/index.vue b/frontend/src/views/accounts/index.vue
index 0a356e28b..7f4e5bea0 100644
--- a/frontend/src/views/accounts/index.vue
+++ b/frontend/src/views/accounts/index.vue
@@ -87,7 +87,7 @@ const {
} = useAccountMutations({
accounts,
selectedIds,
- reload: loadAccounts,
+ reload: () => Promise.all([loadAccounts(), loadGroups()]),
replaceAccount,
})
@@ -335,6 +335,8 @@ const {
v-model="showCreateModal"
v-model:form="createForm"
:account="reauthorizingAccount"
+ :groups="groups"
+ :groups-loading="groupsLoading"
:oauth-loading="authorizingOAuth"
:reauthorizing="Boolean(reauthorizingAccount)"
:saving="creatingAccount"
diff --git a/frontend/src/views/api-keys/components/ApiKeyCreateModal.vue b/frontend/src/views/api-keys/components/ApiKeyCreateModal.vue
index 3928d615c..b496ee9e1 100644
--- a/frontend/src/views/api-keys/components/ApiKeyCreateModal.vue
+++ b/frontend/src/views/api-keys/components/ApiKeyCreateModal.vue
@@ -130,7 +130,7 @@ const title = computed(() => props.editing ? '编辑 API Key' : '创建 API Key'
-
+
取消
{
暂无待核账请求
-
+
取消