mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-08-29 00:41:25 +08:00
feat(auth-files): add support for using official API and related functionality
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "Необязательно. Используется для описания назначения или владельца учётных данных; оставьте пустым, чтобы не записывать.",
|
||||
|
||||
@@ -282,6 +282,8 @@
|
||||
"priority_hint": "仅支持整数;非法值会被忽略。数值越大优先级越高。",
|
||||
"websockets_label": "WebSockets(websockets)",
|
||||
"websockets_hint": "为该凭证开启 Responses API 的 websocket 传输。",
|
||||
"using_api_label": "使用官方 API(using_api)",
|
||||
"using_api_hint": "开启后使用 xAI 官方 API;关闭时使用 Grok CLI chat-proxy。",
|
||||
"note_label": "备注(note)",
|
||||
"note_placeholder": "输入备注信息,例如:张三的账号",
|
||||
"note_hint": "可选,用于标记凭证用途或归属;留空则不写入。",
|
||||
|
||||
@@ -282,6 +282,8 @@
|
||||
"priority_hint": "僅支援整數;無效值會被忽略。數值越大優先順序越高。",
|
||||
"websockets_label": "WebSockets(websockets)",
|
||||
"websockets_hint": "為該憑證開啟 Responses API 的 websocket 傳輸。",
|
||||
"using_api_label": "使用官方 API(using_api)",
|
||||
"using_api_hint": "開啟後使用 xAI 官方 API;關閉時使用 Grok CLI chat-proxy。",
|
||||
"note_label": "備註(note)",
|
||||
"note_placeholder": "輸入備註資訊,例如:張三的帳號",
|
||||
"note_hint": "選填,用於標記憑證用途或歸屬;留空則不寫入。",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user