forked from boos/BoosAPI
fix(routes): dedupe direct credentials and clarify binding labels
This commit is contained in:
@@ -182,6 +182,8 @@ function ensureTokenManagementSchema() {
|
||||
WHERE
|
||||
a.api_token IS NOT NULL
|
||||
AND trim(a.api_token) <> ''
|
||||
AND a.access_token IS NOT NULL
|
||||
AND trim(a.access_token) <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM account_tokens AS t
|
||||
WHERE t.account_id = a.id
|
||||
|
||||
@@ -22,6 +22,7 @@ import { oauthRoutes } from './routes/api/oauth.js';
|
||||
import { siteAnnouncementsRoutes } from './routes/api/siteAnnouncements.js';
|
||||
import { proxyRoutes } from './routes/proxy/router.js';
|
||||
import { startScheduler } from './services/checkinScheduler.js';
|
||||
import { rebuildTokenRoutesFromAvailability } from './services/modelService.js';
|
||||
import { setLegacyProxyLogRetentionFallbackEnabled, stopProxyLogRetentionService } from './services/proxyLogRetentionService.js';
|
||||
import { buildStartupSummaryLines } from './services/startupInfo.js';
|
||||
import { repairStoredCreatedAtValues } from './services/storedTimestampRepairService.js';
|
||||
@@ -329,6 +330,7 @@ try {
|
||||
await repairStoredCreatedAtValues();
|
||||
await migrateSiteApiKeysToAccounts();
|
||||
await ensureDefaultSitesSeeded();
|
||||
await rebuildTokenRoutesFromAvailability();
|
||||
|
||||
console.log('Loaded runtime settings overrides');
|
||||
} catch (error) {
|
||||
|
||||
@@ -248,6 +248,55 @@ describe('refreshModelsForAccount credential discovery', () => {
|
||||
expect(parsed.runtimeHealth?.checkedAt).toMatch(/\d{4}-\d{2}-\d{2}T/);
|
||||
});
|
||||
|
||||
it('does not scan hidden managed tokens for direct apikey connections', async () => {
|
||||
getApiTokenMock.mockResolvedValue(null);
|
||||
getModelsMock.mockImplementation(async (_baseUrl: string, token: string) => (
|
||||
token === 'sk-direct-credential' ? ['gpt-4.1'] : ['legacy-should-not-be-used']
|
||||
));
|
||||
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'apikey-direct-site',
|
||||
url: 'https://apikey-direct.example.com',
|
||||
platform: 'new-api',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'apikey-direct-user',
|
||||
accessToken: '',
|
||||
apiToken: 'sk-direct-credential',
|
||||
status: 'active',
|
||||
extraConfig: JSON.stringify({ credentialMode: 'apikey' }),
|
||||
}).returning().get();
|
||||
|
||||
const hiddenToken = await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'legacy-hidden',
|
||||
token: 'sk-legacy-hidden',
|
||||
source: 'legacy',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
}).returning().get();
|
||||
|
||||
const result = await refreshModelsForAccount(account.id);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
accountId: account.id,
|
||||
refreshed: true,
|
||||
status: 'success',
|
||||
modelCount: 1,
|
||||
modelsPreview: ['gpt-4.1'],
|
||||
tokenScanned: 0,
|
||||
discoveredByCredential: true,
|
||||
});
|
||||
|
||||
const tokenRows = await db.select().from(schema.tokenModelAvailability)
|
||||
.where(eq(schema.tokenModelAvailability.tokenId, hiddenToken.id))
|
||||
.all();
|
||||
expect(tokenRows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns structured result when account missing', async () => {
|
||||
const result = await refreshModelsForAccount(9999);
|
||||
|
||||
|
||||
@@ -85,6 +85,67 @@ describe('rebuildTokenRoutesFromAvailability', () => {
|
||||
expect(channels[0]?.manualOverride).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores hidden account_tokens for direct apikey connections when rebuilding routes', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'apikey-legacy-site',
|
||||
url: 'https://apikey-legacy.example.com',
|
||||
platform: 'new-api',
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'apikey-legacy-user',
|
||||
accessToken: '',
|
||||
apiToken: 'sk-direct-credential',
|
||||
status: 'active',
|
||||
extraConfig: JSON.stringify({ credentialMode: 'apikey' }),
|
||||
}).returning().get();
|
||||
|
||||
const hiddenToken = await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'legacy-hidden',
|
||||
token: 'sk-hidden-legacy-token',
|
||||
source: 'legacy',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
}).returning().get();
|
||||
|
||||
await db.insert(schema.modelAvailability).values({
|
||||
accountId: account.id,
|
||||
modelName: 'gpt-4.1',
|
||||
available: true,
|
||||
latencyMs: 200,
|
||||
checkedAt: '2026-03-20T08:00:00.000Z',
|
||||
}).run();
|
||||
|
||||
await db.insert(schema.tokenModelAvailability).values({
|
||||
tokenId: hiddenToken.id,
|
||||
modelName: 'gpt-4.1',
|
||||
available: true,
|
||||
latencyMs: 180,
|
||||
checkedAt: '2026-03-20T08:00:00.000Z',
|
||||
}).run();
|
||||
|
||||
const rebuild = await rebuildTokenRoutesFromAvailability();
|
||||
|
||||
expect(rebuild.models).toBe(1);
|
||||
|
||||
const route = await db.select().from(schema.tokenRoutes)
|
||||
.where(eq(schema.tokenRoutes.modelPattern, 'gpt-4.1'))
|
||||
.get();
|
||||
expect(route).toBeDefined();
|
||||
|
||||
const channels = await db.select().from(schema.routeChannels)
|
||||
.where(and(
|
||||
eq(schema.routeChannels.routeId, route!.id),
|
||||
eq(schema.routeChannels.accountId, account.id),
|
||||
))
|
||||
.all();
|
||||
|
||||
expect(channels).toHaveLength(1);
|
||||
expect(channels[0]?.tokenId ?? null).toBeNull();
|
||||
});
|
||||
|
||||
it('creates an exact route with an account-direct channel for oauth model availability', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'codex-site',
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
isUsableAccountToken,
|
||||
} from './accountTokenService.js';
|
||||
import {
|
||||
getCredentialModeFromExtraConfig,
|
||||
mergeAccountExtraConfig,
|
||||
requiresManagedAccountTokens,
|
||||
resolvePlatformUserId,
|
||||
supportsDirectAccountRoutingConnection,
|
||||
} from './accountExtraConfig.js';
|
||||
@@ -141,12 +141,6 @@ function isSiteDisabled(status?: string | null): boolean {
|
||||
return (status || 'active') === 'disabled';
|
||||
}
|
||||
|
||||
function isApiKeyConnection(account: typeof schema.accounts.$inferSelect): boolean {
|
||||
const explicit = getCredentialModeFromExtraConfig(account.extraConfig);
|
||||
if (explicit && explicit !== 'auto') return explicit === 'apikey';
|
||||
return !(account.accessToken || '').trim();
|
||||
}
|
||||
|
||||
function normalizeModels(models: string[]): string[] {
|
||||
const normalizedModels: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -821,18 +815,21 @@ export async function refreshModelsForAccount(accountId: number): Promise<ModelR
|
||||
} catch { }
|
||||
}
|
||||
|
||||
let enabledTokens = await db.select()
|
||||
const usesManagedTokens = requiresManagedAccountTokens(account);
|
||||
let enabledTokens = usesManagedTokens
|
||||
? await db.select()
|
||||
.from(schema.accountTokens)
|
||||
.where(and(
|
||||
eq(schema.accountTokens.accountId, account.id),
|
||||
eq(schema.accountTokens.enabled, true),
|
||||
eq(schema.accountTokens.valueStatus, ACCOUNT_TOKEN_VALUE_STATUS_READY),
|
||||
))
|
||||
.all();
|
||||
.all()
|
||||
: [];
|
||||
enabledTokens = enabledTokens.filter(isUsableAccountToken);
|
||||
|
||||
// Last fallback: if still no managed token but account has a legacy apiToken, mirror it into token table.
|
||||
if (!isApiKeyConnection(account) && enabledTokens.length === 0) {
|
||||
if (usesManagedTokens && enabledTokens.length === 0) {
|
||||
const fallback = discoveredApiToken || account.apiToken || null;
|
||||
if (fallback) {
|
||||
ensureDefaultTokenForAccount(account.id, fallback, { name: 'default', source: 'legacy' });
|
||||
@@ -1018,7 +1015,10 @@ export async function rebuildTokenRoutesFromAvailability() {
|
||||
),
|
||||
)
|
||||
.all();
|
||||
const usableTokenRows = tokenRows.filter((row) => isUsableAccountToken(row.account_tokens));
|
||||
const usableTokenRows = tokenRows.filter((row) => (
|
||||
isUsableAccountToken(row.account_tokens)
|
||||
&& requiresManagedAccountTokens(row.accounts)
|
||||
));
|
||||
|
||||
const accountRows = await db.select().from(schema.modelAvailability)
|
||||
.innerJoin(schema.accounts, eq(schema.modelAvailability.accountId, schema.accounts.id))
|
||||
|
||||
@@ -1164,12 +1164,17 @@ export default function Accounts() {
|
||||
</div>
|
||||
{isSub2ApiSelected && (
|
||||
<>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<input
|
||||
placeholder="Sub2API refresh_token(可选,用于托管自动续期)"
|
||||
value={tokenForm.refreshToken}
|
||||
onChange={(e) => setTokenForm((f) => ({ ...f, refreshToken: e.target.value.trim() }))}
|
||||
style={{ ...inputStyle, fontFamily: 'var(--font-mono)' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||||
可在浏览器控制台执行 <code style={{ fontFamily: 'var(--font-mono)' }}>localStorage.getItem('refresh_token')</code> 获取。
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<input
|
||||
placeholder="token_expires_at(可选,毫秒时间戳)"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
buildFixedTokenOptionDescription,
|
||||
buildFixedTokenOptionLabel,
|
||||
describeTokenBinding,
|
||||
resolveTokenBindingConnectionMode,
|
||||
} from './tokenBindingPresentation.js';
|
||||
import { getChannelDecisionState, getPriorityTagStyle, getProbabilityColor } from './utils.js';
|
||||
|
||||
@@ -54,7 +55,15 @@ export function SortableChannelRow({
|
||||
};
|
||||
|
||||
const decisionState = getChannelDecisionState(decisionCandidate, channel, isExactRoute, loadingDecision);
|
||||
const tokenBinding = describeTokenBinding(tokenOptions, activeTokenId, channel.token?.name ?? null);
|
||||
const tokenBinding = describeTokenBinding(
|
||||
tokenOptions,
|
||||
activeTokenId,
|
||||
channel.token?.name ?? null,
|
||||
{
|
||||
connectionMode: resolveTokenBindingConnectionMode(channel.account),
|
||||
accountName: channel.account?.username || `account-${channel.accountId}`,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={rowStyle}>
|
||||
@@ -113,10 +122,10 @@ export function SortableChannelRow({
|
||||
className="badge"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
background: tokenBinding.isFollowingAccountDefault
|
||||
background: tokenBinding.badgeTone === 'info'
|
||||
? 'color-mix(in srgb, var(--color-info) 15%, transparent)'
|
||||
: 'color-mix(in srgb, var(--color-warning) 15%, transparent)',
|
||||
color: tokenBinding.isFollowingAccountDefault ? 'var(--color-info)' : 'var(--color-warning)',
|
||||
color: tokenBinding.badgeTone === 'info' ? 'var(--color-info)' : 'var(--color-warning)',
|
||||
}}
|
||||
>
|
||||
{tokenBinding.bindingModeLabel}
|
||||
@@ -211,7 +220,7 @@ export function SortableChannelRow({
|
||||
options={[
|
||||
{
|
||||
value: '0',
|
||||
label: '跟随账号默认',
|
||||
label: tokenBinding.followOptionLabel,
|
||||
description: tokenBinding.followOptionDescription,
|
||||
},
|
||||
...tokenOptions.map((token) => ({
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
buildFixedTokenOptionDescription,
|
||||
buildFixedTokenOptionLabel,
|
||||
describeTokenBinding,
|
||||
resolveTokenBindingConnectionMode,
|
||||
} from './tokenBindingPresentation.js';
|
||||
|
||||
describe('tokenBindingPresentation', () => {
|
||||
@@ -13,8 +14,10 @@ describe('tokenBindingPresentation', () => {
|
||||
], 0);
|
||||
|
||||
expect(result.bindingModeLabel).toBe('跟随账号默认');
|
||||
expect(result.badgeTone).toBe('info');
|
||||
expect(result.effectiveTokenName).toBe('token-a');
|
||||
expect(result.helperText).toContain('以后账号默认变化时会自动切换');
|
||||
expect(result.followOptionLabel).toBe('跟随账号默认');
|
||||
expect(result.followOptionDescription).toContain('当前生效:token-a');
|
||||
});
|
||||
|
||||
@@ -25,6 +28,7 @@ describe('tokenBindingPresentation', () => {
|
||||
], 1);
|
||||
|
||||
expect(result.bindingModeLabel).toBe('固定令牌');
|
||||
expect(result.badgeTone).toBe('warning');
|
||||
expect(result.effectiveTokenName).toBe('default');
|
||||
expect(result.helperText).toContain('它目前也是账号默认');
|
||||
expect(result.helperText).toContain('不会跟着变');
|
||||
@@ -37,8 +41,10 @@ describe('tokenBindingPresentation', () => {
|
||||
], 2);
|
||||
|
||||
expect(result.bindingModeLabel).toBe('固定令牌');
|
||||
expect(result.badgeTone).toBe('warning');
|
||||
expect(result.effectiveTokenName).toBe('backup');
|
||||
expect(result.helperText).toContain('不会随账号默认变化');
|
||||
expect(result.followOptionLabel).toBe('跟随账号默认');
|
||||
});
|
||||
|
||||
it('describes follow-account-default mode when no default token exists yet', () => {
|
||||
@@ -48,11 +54,76 @@ describe('tokenBindingPresentation', () => {
|
||||
], 0);
|
||||
|
||||
expect(result.bindingModeLabel).toBe('跟随账号默认');
|
||||
expect(result.badgeTone).toBe('info');
|
||||
expect(result.effectiveTokenName).toBe('未设置默认令牌');
|
||||
expect(result.helperText).toContain('当前账号还没有默认令牌');
|
||||
expect(result.followOptionLabel).toBe('跟随账号默认');
|
||||
expect(result.followOptionDescription).toContain('以后账号默认变化时会自动切换');
|
||||
});
|
||||
|
||||
it('describes follow-api-setting mode for direct apikey connections', () => {
|
||||
const result = describeTokenBinding([], 0, null, {
|
||||
connectionMode: 'apikey',
|
||||
accountName: 'elysiver_api',
|
||||
});
|
||||
|
||||
expect(result.bindingModeLabel).toBe('API令牌');
|
||||
expect(result.badgeTone).toBe('warning');
|
||||
expect(result.isFollowingAccountDefault).toBe(false);
|
||||
expect(result.effectiveTokenName).toBe('elysiver_api');
|
||||
expect(result.helperText).toContain('连接「elysiver_api」保存的 API Key');
|
||||
expect(result.followOptionLabel).toBe('固定使用:elysiver_api(跟随 API Key 设置)');
|
||||
expect(result.followOptionDescription).toContain('API Key');
|
||||
});
|
||||
|
||||
it('describes oauth direct binding without default-token wording', () => {
|
||||
const result = describeTokenBinding([], 0, null, {
|
||||
connectionMode: 'oauth',
|
||||
accountName: 'mail@urlk.cn',
|
||||
});
|
||||
|
||||
expect(result.bindingModeLabel).toBe('OAuth授权');
|
||||
expect(result.badgeTone).toBe('warning');
|
||||
expect(result.isFollowingAccountDefault).toBe(false);
|
||||
expect(result.effectiveTokenName).toBe('mail@urlk.cn');
|
||||
expect(result.helperText).toContain('OAuth 授权');
|
||||
expect(result.helperText).not.toContain('默认令牌');
|
||||
expect(result.followOptionLabel).toBe('固定使用:mail@urlk.cn(OAuth 授权)');
|
||||
expect(result.followOptionDescription).toContain('OAuth 授权');
|
||||
});
|
||||
|
||||
it('uses oauth fallback copy when a fixed token route is switched back to oauth direct binding', () => {
|
||||
const result = describeTokenBinding([
|
||||
{ id: 1, name: 'default', isDefault: true },
|
||||
{ id: 2, name: 'backup', isDefault: false },
|
||||
], 2, null, {
|
||||
connectionMode: 'oauth',
|
||||
accountName: 'mail@urlk.cn',
|
||||
});
|
||||
|
||||
expect(result.bindingModeLabel).toBe('固定令牌');
|
||||
expect(result.followOptionLabel).toBe('OAuth授权');
|
||||
expect(result.followOptionDescription).toContain('mail@urlk.cn');
|
||||
expect(result.followOptionDescription).not.toContain('当前生效:default');
|
||||
});
|
||||
|
||||
it('resolves token binding connection mode from stored account fields', () => {
|
||||
expect(resolveTokenBindingConnectionMode({
|
||||
accessToken: '',
|
||||
extraConfig: JSON.stringify({ credentialMode: 'apikey' }),
|
||||
})).toBe('apikey');
|
||||
|
||||
expect(resolveTokenBindingConnectionMode({
|
||||
accessToken: 'session-token',
|
||||
extraConfig: JSON.stringify({ credentialMode: 'session' }),
|
||||
})).toBe('session');
|
||||
|
||||
expect(resolveTokenBindingConnectionMode({
|
||||
accessToken: 'oauth-access-token',
|
||||
extraConfig: JSON.stringify({ credentialMode: 'session', oauth: { provider: 'codex' } }),
|
||||
})).toBe('oauth');
|
||||
});
|
||||
|
||||
it('formats fixed token options with clearer labels and descriptions', () => {
|
||||
const token = {
|
||||
id: 3,
|
||||
|
||||
@@ -8,11 +8,93 @@ export type TokenBindingOption = {
|
||||
export type TokenBindingPresentation = {
|
||||
isFollowingAccountDefault: boolean;
|
||||
bindingModeLabel: string;
|
||||
badgeTone: 'info' | 'warning';
|
||||
effectiveTokenName: string;
|
||||
helperText: string;
|
||||
followOptionLabel: string;
|
||||
followOptionDescription: string;
|
||||
};
|
||||
|
||||
type TokenBindingConnectionMode = 'session' | 'apikey' | 'oauth';
|
||||
|
||||
type TokenBindingContext = {
|
||||
connectionMode?: TokenBindingConnectionMode;
|
||||
accountName?: string | null;
|
||||
};
|
||||
|
||||
function parseExtraConfigHints(extraConfig?: string | null): {
|
||||
credentialMode: Extract<TokenBindingConnectionMode, 'session' | 'apikey'> | null;
|
||||
hasOauthProvider: boolean;
|
||||
} {
|
||||
if (typeof extraConfig !== 'string' || !extraConfig.trim()) {
|
||||
return {
|
||||
credentialMode: null,
|
||||
hasOauthProvider: false,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(extraConfig) as {
|
||||
credentialMode?: unknown;
|
||||
oauth?: { provider?: unknown } | null;
|
||||
};
|
||||
const raw = String(parsed?.credentialMode || '').trim().toLowerCase();
|
||||
return {
|
||||
credentialMode: raw === 'session' || raw === 'apikey' ? raw : null,
|
||||
hasOauthProvider: typeof parsed?.oauth?.provider === 'string' && parsed.oauth.provider.trim().length > 0,
|
||||
};
|
||||
} catch {}
|
||||
return {
|
||||
credentialMode: null,
|
||||
hasOauthProvider: false,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDirectBindingPresentation(
|
||||
connectionMode: Extract<TokenBindingConnectionMode, 'apikey' | 'oauth'>,
|
||||
accountName: string,
|
||||
): TokenBindingPresentation {
|
||||
if (connectionMode === 'oauth') {
|
||||
return {
|
||||
isFollowingAccountDefault: false,
|
||||
bindingModeLabel: 'OAuth授权',
|
||||
badgeTone: 'warning',
|
||||
effectiveTokenName: accountName,
|
||||
helperText: `当前直接使用连接「${accountName}」的 OAuth 授权,不依赖账号令牌。`,
|
||||
followOptionLabel: `固定使用:${accountName}(OAuth 授权)`,
|
||||
followOptionDescription: `直接使用连接「${accountName}」的 OAuth 授权`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isFollowingAccountDefault: false,
|
||||
bindingModeLabel: 'API令牌',
|
||||
badgeTone: 'warning',
|
||||
effectiveTokenName: accountName,
|
||||
helperText: `当前直接使用连接「${accountName}」保存的 API Key,不依赖账号令牌。`,
|
||||
followOptionLabel: `固定使用:${accountName}(跟随 API Key 设置)`,
|
||||
followOptionDescription: `直接使用连接「${accountName}」保存的 API Key`,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTokenBindingConnectionMode(account?: {
|
||||
accessToken?: string | null;
|
||||
extraConfig?: string | null;
|
||||
credentialMode?: string | null;
|
||||
} | null): TokenBindingConnectionMode {
|
||||
const parsedHints = parseExtraConfigHints(account?.extraConfig);
|
||||
if (parsedHints.hasOauthProvider) return 'oauth';
|
||||
|
||||
const rawMode = String(account?.credentialMode || '').trim().toLowerCase();
|
||||
if (rawMode === 'session') return 'session';
|
||||
if (rawMode === 'apikey') return 'apikey';
|
||||
|
||||
if (parsedHints.credentialMode) return parsedHints.credentialMode;
|
||||
|
||||
return typeof account?.accessToken === 'string' && account.accessToken.trim()
|
||||
? 'session'
|
||||
: 'apikey';
|
||||
}
|
||||
|
||||
export function getDefaultTokenOption(options: TokenBindingOption[]): TokenBindingOption | null {
|
||||
return options.find((option) => option.isDefault) || null;
|
||||
}
|
||||
@@ -21,21 +103,30 @@ export function describeTokenBinding(
|
||||
options: TokenBindingOption[],
|
||||
activeTokenId: number,
|
||||
fallbackTokenName?: string | null,
|
||||
context: TokenBindingContext = {},
|
||||
): TokenBindingPresentation {
|
||||
const defaultToken = getDefaultTokenOption(options);
|
||||
const selectedToken = activeTokenId
|
||||
? options.find((option) => option.id === activeTokenId) || null
|
||||
: null;
|
||||
const connectionMode = context.connectionMode || 'session';
|
||||
const accountName = String(context.accountName || '').trim() || '当前连接';
|
||||
|
||||
if (!activeTokenId) {
|
||||
if (connectionMode === 'apikey' || connectionMode === 'oauth') {
|
||||
return buildDirectBindingPresentation(connectionMode, accountName);
|
||||
}
|
||||
|
||||
const effectiveTokenName = defaultToken?.name || fallbackTokenName || '未设置默认令牌';
|
||||
return {
|
||||
isFollowingAccountDefault: true,
|
||||
bindingModeLabel: '跟随账号默认',
|
||||
badgeTone: 'info',
|
||||
effectiveTokenName,
|
||||
helperText: defaultToken
|
||||
? `跟随账号默认。当前生效的是「${defaultToken.name}」,以后账号默认变化时会自动切换。`
|
||||
: '跟随账号默认。当前账号还没有默认令牌。',
|
||||
followOptionLabel: '跟随账号默认',
|
||||
followOptionDescription: defaultToken
|
||||
? `当前生效:${defaultToken.name};以后账号默认变化时会自动切换`
|
||||
: '以后账号默认变化时会自动切换',
|
||||
@@ -46,13 +137,21 @@ export function describeTokenBinding(
|
||||
return {
|
||||
isFollowingAccountDefault: false,
|
||||
bindingModeLabel: '固定令牌',
|
||||
badgeTone: 'warning',
|
||||
effectiveTokenName,
|
||||
helperText: selectedToken?.isDefault
|
||||
? `已固定到「${effectiveTokenName}」。它目前也是账号默认,但以后账号默认变化时,这个通道不会跟着变。`
|
||||
: `已固定到「${effectiveTokenName}」,不会随账号默认变化。`,
|
||||
followOptionDescription: defaultToken
|
||||
followOptionLabel: connectionMode === 'oauth'
|
||||
? 'OAuth授权'
|
||||
: (connectionMode === 'apikey' ? 'API 设置' : '跟随账号默认'),
|
||||
followOptionDescription: connectionMode === 'oauth'
|
||||
? `直接使用连接「${accountName}」的 OAuth 授权`
|
||||
: (connectionMode === 'apikey'
|
||||
? `直接使用连接「${accountName}」保存的 API Key`
|
||||
: (defaultToken
|
||||
? `当前生效:${defaultToken.name};以后账号默认变化时会自动切换`
|
||||
: '以后账号默认变化时会自动切换',
|
||||
: '以后账号默认变化时会自动切换')),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ export type RouteChannel = {
|
||||
cooldownUntil?: string | null;
|
||||
account?: {
|
||||
username: string | null;
|
||||
accessToken?: string | null;
|
||||
extraConfig?: string | null;
|
||||
credentialMode?: string | null;
|
||||
};
|
||||
site?: {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user