feat(auth-files): add support for using official API and related functionality

This commit is contained in:
Supra4E8C
2026-07-11 13:17:24 +08:00
parent 4af4cf4d6d
commit 07562b73ae
9 changed files with 95 additions and 1 deletions
@@ -9,7 +9,10 @@ import type {
PrefixProxyEditorFieldValue,
PrefixProxyEditorState,
} from '@/features/authFiles/hooks/useAuthFilesPrefixProxyEditor';
import { supportsAuthFileWebsockets } from '@/features/authFiles/constants';
import {
supportsAuthFileUsingApi,
supportsAuthFileWebsockets,
} from '@/features/authFiles/constants';
import styles from '@/pages/AuthFilesPage.module.scss';
export type AuthFilesPrefixProxyEditorModalProps = {
@@ -155,6 +158,18 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
<div className="hint">{t('auth_files.websockets_hint')}</div>
</div>
)}
{supportsAuthFileUsingApi(editor.providerKey) && (
<div className="form-group">
<label>{t('auth_files.using_api_label')}</label>
<ToggleSwitch
checked={editor.usingApi}
onChange={(value) => onChange('usingApi', value)}
disabled={disableControls || editor.saving || !editor.json}
ariaLabel={t('auth_files.using_api_label')}
/>
<div className="hint">{t('auth_files.using_api_hint')}</div>
</div>
)}
<div className="form-group">
<label>{t('auth_files.headers_label')}</label>
<textarea
+12
View File
@@ -54,6 +54,7 @@ export const INTEGER_STRING_PATTERN = /^[+-]?\d+$/;
export const TRUTHY_TEXT_VALUES = new Set(['true', '1', 'yes', 'y', 'on']);
export const FALSY_TEXT_VALUES = new Set(['false', '0', 'no', 'n', 'off']);
export const AUTH_FILE_WEBSOCKET_PROVIDERS = new Set(['codex', 'xai']);
export const AUTH_FILE_USING_API_PROVIDERS = new Set(['xai']);
// 标签类型颜色配置 — 基于各提供商 Logo 品牌色调配,确保彼此不重复
export const TYPE_COLORS: Record<string, TypeColorSet> = {
@@ -236,6 +237,17 @@ export const applyAuthFileWebsockets = (
return next;
};
export const supportsAuthFileUsingApi = (providerKey: string): boolean =>
AUTH_FILE_USING_API_PROVIDERS.has(normalizeProviderKey(providerKey));
export const readAuthFileUsingApi = (value: Record<string, unknown>): boolean =>
parseDisableCoolingValue(value.using_api) ?? false;
export const applyAuthFileUsingApi = (
value: Record<string, unknown>,
usingApi: boolean
): Record<string, unknown> => ({ ...value, using_api: usingApi });
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
const raw = file['runtime_only'] ?? file.runtimeOnly;
if (typeof raw === 'boolean') return raw;
@@ -5,10 +5,13 @@ import type { AuthFileItem } from '@/types';
import { useNotificationStore } from '@/stores';
import {
applyAuthFileWebsockets,
applyAuthFileUsingApi,
normalizeProviderKey,
parsePriorityValue,
readAuthFileWebsockets,
readAuthFileUsingApi,
supportsAuthFileWebsockets,
supportsAuthFileUsingApi,
} from '@/features/authFiles/constants';
type AuthFileHeaders = Record<string, string>;
@@ -25,6 +28,7 @@ export type PrefixProxyEditorField =
| 'proxyUrl'
| 'priority'
| 'websockets'
| 'usingApi'
| 'note'
| 'headersText';
@@ -46,6 +50,8 @@ export type PrefixProxyEditorState = {
priority: string;
websockets: boolean;
websocketsTouched: boolean;
usingApi: boolean;
usingApiTouched: boolean;
note: string;
noteTouched: boolean;
headersText: string;
@@ -265,6 +271,14 @@ const buildAuthFileFieldsPatch = (
}
}
if (supportsAuthFileUsingApi(editor.providerKey) && editor.usingApiTouched) {
const originalUsingApi = readAuthFileUsingApi(original);
const nextUsingApi = Boolean(editor.usingApi);
if (nextUsingApi !== originalUsingApi) {
patch.using_api = nextUsingApi;
}
}
if (editor.headersTouched) {
const { value: parsedHeaders, errorKey } = parseHeadersText(editor.headersText);
if (errorKey) {
@@ -326,6 +340,10 @@ const buildPrefixProxyUpdatedText = (
next = applyAuthFileWebsockets(next, patch.websockets);
}
if (patch.using_api !== undefined) {
next = applyAuthFileUsingApi(next, patch.using_api);
}
return JSON.stringify(next);
};
@@ -383,6 +401,8 @@ export function useAuthFilesPrefixProxyEditor(
priority: '',
websockets: false,
websocketsTouched: false,
usingApi: false,
usingApiTouched: false,
note: '',
noteTouched: false,
headersText: '',
@@ -430,6 +450,7 @@ export function useAuthFilesPrefixProxyEditor(
const websockets = supportsAuthFileWebsockets(providerKey)
? readAuthFileWebsockets(json)
: false;
const usingApi = supportsAuthFileUsingApi(providerKey) ? readAuthFileUsingApi(json) : false;
const note = typeof json.note === 'string' ? json.note : '';
const headers = json.headers;
let headersText = '';
@@ -455,6 +476,8 @@ export function useAuthFilesPrefixProxyEditor(
priority: priority !== undefined ? String(priority) : '',
websockets,
websocketsTouched: false,
usingApi,
usingApiTouched: false,
note,
noteTouched: false,
headersText,
@@ -485,6 +508,9 @@ export function useAuthFilesPrefixProxyEditor(
if (field === 'websockets') {
return { ...prev, websockets: Boolean(value), websocketsTouched: true };
}
if (field === 'usingApi') {
return { ...prev, usingApi: Boolean(value), usingApiTouched: true };
}
if (field === 'note') return { ...prev, note: String(value), noteTouched: true };
if (field === 'headersText') {
const headersText = String(value);
+2
View File
@@ -282,6 +282,8 @@
"priority_hint": "Integers only. Invalid values are ignored. Larger value means higher priority.",
"websockets_label": "WebSockets (websockets)",
"websockets_hint": "Enable Responses API websocket transport for this credential.",
"using_api_label": "Use official API (using_api)",
"using_api_hint": "Use the official xAI API when enabled; use Grok CLI chat-proxy when disabled.",
"note_label": "Note",
"note_placeholder": "Enter a note, e.g.: John's account",
"note_hint": "Optional. Used to describe the purpose or owner of this credential; leave empty to omit.",
+2
View File
@@ -281,6 +281,8 @@
"priority_hint": "Только целые числа. Некорректные значения игнорируются. Чем больше число, тем выше приоритет.",
"websockets_label": "WebSockets (websockets)",
"websockets_hint": "Включает websocket-транспорт Responses API для этих учётных данных.",
"using_api_label": "Использовать официальный API (using_api)",
"using_api_hint": "Если включено, используется официальный API xAI; если выключено — Grok CLI chat-proxy.",
"note_label": "Заметка (note)",
"note_placeholder": "Введите заметку, например: аккаунт Ивана",
"note_hint": "Необязательно. Используется для описания назначения или владельца учётных данных; оставьте пустым, чтобы не записывать.",
+2
View File
@@ -282,6 +282,8 @@
"priority_hint": "仅支持整数;非法值会被忽略。数值越大优先级越高。",
"websockets_label": "WebSocketswebsockets",
"websockets_hint": "为该凭证开启 Responses API 的 websocket 传输。",
"using_api_label": "使用官方 APIusing_api",
"using_api_hint": "开启后使用 xAI 官方 API;关闭时使用 Grok CLI chat-proxy。",
"note_label": "备注(note",
"note_placeholder": "输入备注信息,例如:张三的账号",
"note_hint": "可选,用于标记凭证用途或归属;留空则不写入。",
+2
View File
@@ -282,6 +282,8 @@
"priority_hint": "僅支援整數;無效值會被忽略。數值越大優先順序越高。",
"websockets_label": "WebSocketswebsockets",
"websockets_hint": "為該憑證開啟 Responses API 的 websocket 傳輸。",
"using_api_label": "使用官方 APIusing_api",
"using_api_hint": "開啟後使用 xAI 官方 API;關閉時使用 Grok CLI chat-proxy。",
"note_label": "備註(note",
"note_placeholder": "輸入備註資訊,例如:張三的帳號",
"note_hint": "選填,用於標記憑證用途或歸屬;留空則不寫入。",
+1
View File
@@ -17,6 +17,7 @@ export type AuthFileFieldsPatch = {
headers?: Record<string, string>;
priority?: number;
websockets?: boolean;
using_api?: boolean;
note?: string;
};
type AuthFileBatchFailure = { name: string; error: string };
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, test } from 'bun:test';
import {
applyAuthFileUsingApi,
readAuthFileUsingApi,
supportsAuthFileUsingApi,
} from '@/features/authFiles/constants';
describe('xAI auth-file using_api', () => {
test('is only exposed for xAI credentials', () => {
expect(supportsAuthFileUsingApi('xai')).toBe(true);
expect(supportsAuthFileUsingApi('grok')).toBe(true);
expect(supportsAuthFileUsingApi('codex')).toBe(false);
});
test('reads boolean-compatible values and defaults to chat-proxy mode', () => {
expect(readAuthFileUsingApi({ using_api: true })).toBe(true);
expect(readAuthFileUsingApi({ using_api: 'true' })).toBe(true);
expect(readAuthFileUsingApi({ using_api: false })).toBe(false);
expect(readAuthFileUsingApi({})).toBe(false);
});
test('writes an explicit using_api boolean', () => {
expect(applyAuthFileUsingApi({ type: 'xai' }, true)).toEqual({
type: 'xai',
using_api: true,
});
expect(applyAuthFileUsingApi({ type: 'xai', using_api: true }, false)).toEqual({
type: 'xai',
using_api: false,
});
});
});