mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Compare commits
2 Commits
@@ -5,5 +5,5 @@
|
||||
export { QuotaSection } from './QuotaSection';
|
||||
export { QuotaCard } from './QuotaCard';
|
||||
export { useQuotaLoader } from './useQuotaLoader';
|
||||
export { ANTIGRAVITY_CONFIG, CLAUDE_CONFIG, CODEX_CONFIG, GEMINI_CLI_CONFIG } from './quotaConfigs';
|
||||
export { ANTIGRAVITY_CONFIG, CLAUDE_CONFIG, CODEX_CONFIG, GEMINI_CLI_CONFIG, KIMI_CONFIG } from './quotaConfigs';
|
||||
export type { QuotaConfig } from './quotaConfigs';
|
||||
|
||||
@@ -22,6 +22,8 @@ import type {
|
||||
GeminiCliParsedBucket,
|
||||
GeminiCliQuotaBucketState,
|
||||
GeminiCliQuotaState,
|
||||
KimiQuotaRow,
|
||||
KimiQuotaState,
|
||||
} from '@/types';
|
||||
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
|
||||
import {
|
||||
@@ -34,6 +36,8 @@ import {
|
||||
CODEX_REQUEST_HEADERS,
|
||||
GEMINI_CLI_QUOTA_URL,
|
||||
GEMINI_CLI_REQUEST_HEADERS,
|
||||
KIMI_USAGE_URL,
|
||||
KIMI_REQUEST_HEADERS,
|
||||
normalizeGeminiCliModelId,
|
||||
normalizeNumberValue,
|
||||
normalizePlanType,
|
||||
@@ -43,13 +47,16 @@ import {
|
||||
parseClaudeUsagePayload,
|
||||
parseCodexUsagePayload,
|
||||
parseGeminiCliQuotaPayload,
|
||||
parseKimiUsagePayload,
|
||||
resolveCodexChatgptAccountId,
|
||||
resolveCodexPlanType,
|
||||
resolveGeminiCliProjectId,
|
||||
formatCodexResetLabel,
|
||||
formatQuotaResetTime,
|
||||
formatKimiResetHint,
|
||||
buildAntigravityQuotaGroups,
|
||||
buildGeminiCliQuotaBuckets,
|
||||
buildKimiQuotaRows,
|
||||
createStatusError,
|
||||
getStatusFromError,
|
||||
isAntigravityFile,
|
||||
@@ -57,6 +64,7 @@ import {
|
||||
isCodexFile,
|
||||
isDisabledAuthFile,
|
||||
isGeminiCliFile,
|
||||
isKimiFile,
|
||||
isRuntimeOnlyAuthFile,
|
||||
} from '@/utils/quota';
|
||||
import { normalizeAuthIndex } from '@/utils/usage';
|
||||
@@ -65,7 +73,7 @@ import styles from '@/pages/QuotaPage.module.scss';
|
||||
|
||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||
|
||||
type QuotaType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli';
|
||||
type QuotaType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi';
|
||||
|
||||
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
||||
|
||||
@@ -74,10 +82,12 @@ export interface QuotaStore {
|
||||
claudeQuota: Record<string, ClaudeQuotaState>;
|
||||
codexQuota: Record<string, CodexQuotaState>;
|
||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||
kimiQuota: Record<string, KimiQuotaState>;
|
||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||
setClaudeQuota: (updater: QuotaUpdater<Record<string, ClaudeQuotaState>>) => void;
|
||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||
setKimiQuota: (updater: QuotaUpdater<Record<string, KimiQuotaState>>) => void;
|
||||
clearQuotaCache: () => void;
|
||||
}
|
||||
|
||||
@@ -859,3 +869,107 @@ export const GEMINI_CLI_CONFIG: QuotaConfig<GeminiCliQuotaState, GeminiCliQuotaB
|
||||
gridClassName: styles.geminiCliGrid,
|
||||
renderQuotaItems: renderGeminiCliItems,
|
||||
};
|
||||
|
||||
const fetchKimiQuota = async (
|
||||
file: AuthFileItem,
|
||||
t: TFunction
|
||||
): Promise<KimiQuotaRow[]> => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndex = normalizeAuthIndex(rawAuthIndex);
|
||||
if (!authIndex) {
|
||||
throw new Error(t('kimi_quota.missing_auth_index'));
|
||||
}
|
||||
|
||||
const result = await apiCallApi.request({
|
||||
authIndex,
|
||||
method: 'GET',
|
||||
url: KIMI_USAGE_URL,
|
||||
header: { ...KIMI_REQUEST_HEADERS },
|
||||
});
|
||||
|
||||
if (result.statusCode < 200 || result.statusCode >= 300) {
|
||||
throw createStatusError(getApiCallErrorMessage(result), result.statusCode);
|
||||
}
|
||||
|
||||
const payload = parseKimiUsagePayload(result.body ?? result.bodyText);
|
||||
if (!payload) {
|
||||
throw new Error(t('kimi_quota.empty_data'));
|
||||
}
|
||||
|
||||
return buildKimiQuotaRows(payload);
|
||||
};
|
||||
|
||||
const renderKimiItems = (
|
||||
quota: KimiQuotaState,
|
||||
t: TFunction,
|
||||
helpers: QuotaRenderHelpers
|
||||
): ReactNode => {
|
||||
const { styles: styleMap, QuotaProgressBar } = helpers;
|
||||
const { createElement: h } = React;
|
||||
const rows = quota.rows ?? [];
|
||||
|
||||
if (rows.length === 0) {
|
||||
return h('div', { className: styleMap.quotaMessage }, t('kimi_quota.empty_data'));
|
||||
}
|
||||
|
||||
return rows.map((row) => {
|
||||
const limit = row.limit;
|
||||
const used = row.used;
|
||||
const remaining =
|
||||
limit > 0
|
||||
? Math.max(0, Math.min(100, Math.round(((limit - used) / limit) * 100)))
|
||||
: used > 0
|
||||
? 0
|
||||
: null;
|
||||
const percentLabel = remaining === null ? '--' : `${remaining}%`;
|
||||
const rowLabel = row.labelKey
|
||||
? t(row.labelKey, (row.labelParams ?? {}) as Record<string, string | number>)
|
||||
: row.label ?? '';
|
||||
const resetLabel = formatKimiResetHint(t, row.resetHint);
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{ key: row.id, className: styleMap.quotaRow },
|
||||
h(
|
||||
'div',
|
||||
{ className: styleMap.quotaRowHeader },
|
||||
h('span', { className: styleMap.quotaModel }, rowLabel),
|
||||
h(
|
||||
'div',
|
||||
{ className: styleMap.quotaMeta },
|
||||
h('span', { className: styleMap.quotaPercent }, percentLabel),
|
||||
limit > 0
|
||||
? h('span', { className: styleMap.quotaAmount }, `${used} / ${limit}`)
|
||||
: null,
|
||||
resetLabel
|
||||
? h('span', { className: styleMap.quotaReset }, resetLabel)
|
||||
: null
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, { percent: remaining, highThreshold: 60, mediumThreshold: 20 })
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const KIMI_CONFIG: QuotaConfig<KimiQuotaState, KimiQuotaRow[]> = {
|
||||
type: 'kimi',
|
||||
i18nPrefix: 'kimi_quota',
|
||||
cardIdleMessageKey: 'quota_management.card_idle_hint',
|
||||
filterFn: (file) => isKimiFile(file) && !isDisabledAuthFile(file),
|
||||
fetchQuota: fetchKimiQuota,
|
||||
storeSelector: (state) => state.kimiQuota,
|
||||
storeSetter: 'setKimiQuota',
|
||||
buildLoadingState: () => ({ status: 'loading', rows: [] }),
|
||||
buildSuccessState: (rows) => ({ status: 'success', rows }),
|
||||
buildErrorState: (message, status) => ({
|
||||
status: 'error',
|
||||
rows: [],
|
||||
error: message,
|
||||
errorStatus: status,
|
||||
}),
|
||||
cardClassName: styles.kimiCard,
|
||||
controlsClassName: styles.kimiControls,
|
||||
controlClassName: styles.kimiControl,
|
||||
gridClassName: styles.kimiGrid,
|
||||
renderQuotaItems: renderKimiItems,
|
||||
};
|
||||
|
||||
@@ -95,7 +95,9 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
? styles.codexCard
|
||||
: quotaType === 'gemini-cli'
|
||||
? styles.geminiCliCard
|
||||
: '';
|
||||
: quotaType === 'kimi'
|
||||
? styles.kimiCard
|
||||
: '';
|
||||
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndexKey = normalizeAuthIndex(rawAuthIndex);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { ANTIGRAVITY_CONFIG, CODEX_CONFIG, GEMINI_CLI_CONFIG } from '@/components/quota';
|
||||
import { ANTIGRAVITY_CONFIG, CODEX_CONFIG, GEMINI_CLI_CONFIG, KIMI_CONFIG } from '@/components/quota';
|
||||
import { useNotificationStore, useQuotaStore } from '@/stores';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { getStatusFromError } from '@/utils/quota';
|
||||
@@ -18,6 +18,7 @@ type QuotaState = { status?: string; error?: string; errorStatus?: number } | un
|
||||
const getQuotaConfig = (type: QuotaProviderType) => {
|
||||
if (type === 'antigravity') return ANTIGRAVITY_CONFIG;
|
||||
if (type === 'codex') return CODEX_CONFIG;
|
||||
if (type === 'kimi') return KIMI_CONFIG;
|
||||
return GEMINI_CLI_CONFIG;
|
||||
};
|
||||
|
||||
@@ -35,12 +36,14 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
const quota = useQuotaStore((state) => {
|
||||
if (quotaType === 'antigravity') return state.antigravityQuota[file.name] as QuotaState;
|
||||
if (quotaType === 'codex') return state.codexQuota[file.name] as QuotaState;
|
||||
if (quotaType === 'kimi') return state.kimiQuota[file.name] as QuotaState;
|
||||
return state.geminiCliQuota[file.name] as QuotaState;
|
||||
});
|
||||
|
||||
const updateQuotaState = useQuotaStore((state) => {
|
||||
if (quotaType === 'antigravity') return state.setAntigravityQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'codex') return state.setCodexQuota as unknown as (updater: unknown) => void;
|
||||
if (quotaType === 'kimi') return state.setKimiQuota as unknown as (updater: unknown) => void;
|
||||
return state.setGeminiCliQuota as unknown as (updater: unknown) => void;
|
||||
});
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ export type TypeColorSet = { light: ThemeColors; dark?: ThemeColors };
|
||||
export type ResolvedTheme = 'light' | 'dark';
|
||||
export type AuthFileModelItem = { id: string; display_name?: string; type?: string; owned_by?: string };
|
||||
|
||||
export type QuotaProviderType = 'antigravity' | 'codex' | 'gemini-cli';
|
||||
export type QuotaProviderType = 'antigravity' | 'codex' | 'gemini-cli' | 'kimi';
|
||||
|
||||
export const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>(['antigravity', 'codex', 'gemini-cli']);
|
||||
export const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>(['antigravity', 'codex', 'gemini-cli', 'kimi']);
|
||||
|
||||
export const MIN_CARD_PAGE_SIZE = 3;
|
||||
export const MAX_CARD_PAGE_SIZE = 30;
|
||||
|
||||
@@ -619,6 +619,22 @@
|
||||
"fetch_all": "Fetch All",
|
||||
"remaining_amount": "Remaining {{count}}"
|
||||
},
|
||||
"kimi_quota": {
|
||||
"title": "Kimi Quota",
|
||||
"empty_title": "No Kimi Auth Files",
|
||||
"empty_desc": "Upload a Kimi credential to view remaining quota.",
|
||||
"idle": "Click here to refresh quota",
|
||||
"loading": "Loading quota...",
|
||||
"load_failed": "Failed to load quota: {{message}}",
|
||||
"missing_auth_index": "Auth file missing auth_index",
|
||||
"empty_data": "No quota data available",
|
||||
"refresh_button": "Refresh Quota",
|
||||
"fetch_all": "Fetch All",
|
||||
"weekly_limit": "Weekly limit",
|
||||
"limit_window": "{{duration}} limit",
|
||||
"limit_index": "Limit #{{index}}",
|
||||
"reset_hint": "resets in {{hint}}"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Vertex JSON Login",
|
||||
"description": "Upload a Google service account JSON to store it as auth-dir/vertex-<project>.json using the same rules as the CLI vertex-import helper.",
|
||||
|
||||
@@ -622,6 +622,22 @@
|
||||
"fetch_all": "Получить все",
|
||||
"remaining_amount": "Осталось {{count}}"
|
||||
},
|
||||
"kimi_quota": {
|
||||
"title": "Квота Kimi",
|
||||
"empty_title": "Файлы авторизации Kimi отсутствуют",
|
||||
"empty_desc": "Загрузите учётные данные Kimi, чтобы увидеть оставшуюся квоту.",
|
||||
"idle": "Не загружено. Нажмите \"Обновить квоту\".",
|
||||
"loading": "Загрузка квоты...",
|
||||
"load_failed": "Не удалось загрузить квоту: {{message}}",
|
||||
"missing_auth_index": "В файле авторизации отсутствует auth_index",
|
||||
"empty_data": "Данные по квоте отсутствуют",
|
||||
"refresh_button": "Обновить квоту",
|
||||
"fetch_all": "Получить все",
|
||||
"weekly_limit": "Недельный лимит",
|
||||
"limit_window": "Лимит {{duration}}",
|
||||
"limit_index": "Лимит #{{index}}",
|
||||
"reset_hint": "сброс через {{hint}}"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Вход с Vertex JSON",
|
||||
"description": "Загрузите JSON ключа сервисного аккаунта Google, чтобы сохранить его как auth-dir/vertex-<project>.json по тем же правилам, что и помощник CLI vertex-import.",
|
||||
|
||||
@@ -619,6 +619,22 @@
|
||||
"fetch_all": "获取全部",
|
||||
"remaining_amount": "剩余 {{count}}"
|
||||
},
|
||||
"kimi_quota": {
|
||||
"title": "Kimi 额度",
|
||||
"empty_title": "暂无 Kimi 认证",
|
||||
"empty_desc": "上传 Kimi 认证文件后即可查看额度。",
|
||||
"idle": "点击此处刷新额度",
|
||||
"loading": "正在加载额度...",
|
||||
"load_failed": "额度获取失败:{{message}}",
|
||||
"missing_auth_index": "认证文件缺少 auth_index",
|
||||
"empty_data": "暂无额度数据",
|
||||
"refresh_button": "刷新额度",
|
||||
"fetch_all": "获取全部",
|
||||
"weekly_limit": "周限额",
|
||||
"limit_window": "{{duration}} 限额",
|
||||
"limit_index": "限额 #{{index}}",
|
||||
"reset_hint": "{{hint}} 后重置"
|
||||
},
|
||||
"vertex_import": {
|
||||
"title": "Vertex JSON 登录",
|
||||
"description": "上传 Google 服务账号 JSON,使用 CLI vertex-import 同步规则写入 auth-dir/vertex-<project>.json。",
|
||||
|
||||
@@ -322,6 +322,10 @@
|
||||
background-image: linear-gradient(180deg, rgba(231, 239, 255, 0.2), rgba(231, 239, 255, 0));
|
||||
}
|
||||
|
||||
.kimiCard {
|
||||
background-image: linear-gradient(180deg, rgba(255, 244, 229, 0.2), rgba(255, 244, 229, 0));
|
||||
}
|
||||
|
||||
.quotaSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
.antigravityGrid,
|
||||
.claudeGrid,
|
||||
.codexGrid,
|
||||
.geminiCliGrid {
|
||||
.geminiCliGrid,
|
||||
.kimiGrid {
|
||||
display: grid;
|
||||
gap: $spacing-md;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
@@ -118,7 +119,8 @@
|
||||
.antigravityControls,
|
||||
.claudeControls,
|
||||
.codexControls,
|
||||
.geminiCliControls {
|
||||
.geminiCliControls,
|
||||
.kimiControls {
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
flex-wrap: wrap;
|
||||
@@ -129,7 +131,8 @@
|
||||
.antigravityControl,
|
||||
.claudeControl,
|
||||
.codexControl,
|
||||
.geminiCliControl {
|
||||
.geminiCliControl,
|
||||
.kimiControl {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -172,6 +175,12 @@
|
||||
rgba(231, 239, 255, 0));
|
||||
}
|
||||
|
||||
.kimiCard {
|
||||
background-image: linear-gradient(180deg,
|
||||
rgba(255, 244, 229, 0.2),
|
||||
rgba(255, 244, 229, 0));
|
||||
}
|
||||
|
||||
.quotaSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
ANTIGRAVITY_CONFIG,
|
||||
CLAUDE_CONFIG,
|
||||
CODEX_CONFIG,
|
||||
GEMINI_CLI_CONFIG
|
||||
GEMINI_CLI_CONFIG,
|
||||
KIMI_CONFIG
|
||||
} from '@/components/quota';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import styles from './QuotaPage.module.scss';
|
||||
@@ -94,6 +95,12 @@ export function QuotaPage() {
|
||||
loading={loading}
|
||||
disabled={disableControls}
|
||||
/>
|
||||
<QuotaSection
|
||||
config={KIMI_CONFIG}
|
||||
files={files}
|
||||
loading={loading}
|
||||
disabled={disableControls}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { AntigravityQuotaState, ClaudeQuotaState, CodexQuotaState, GeminiCliQuotaState } from '@/types';
|
||||
import type { AntigravityQuotaState, ClaudeQuotaState, CodexQuotaState, GeminiCliQuotaState, KimiQuotaState } from '@/types';
|
||||
|
||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||
|
||||
@@ -12,10 +12,12 @@ interface QuotaStoreState {
|
||||
claudeQuota: Record<string, ClaudeQuotaState>;
|
||||
codexQuota: Record<string, CodexQuotaState>;
|
||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||
kimiQuota: Record<string, KimiQuotaState>;
|
||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||
setClaudeQuota: (updater: QuotaUpdater<Record<string, ClaudeQuotaState>>) => void;
|
||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||
setKimiQuota: (updater: QuotaUpdater<Record<string, KimiQuotaState>>) => void;
|
||||
clearQuotaCache: () => void;
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
||||
claudeQuota: {},
|
||||
codexQuota: {},
|
||||
geminiCliQuota: {},
|
||||
kimiQuota: {},
|
||||
setAntigravityQuota: (updater) =>
|
||||
set((state) => ({
|
||||
antigravityQuota: resolveUpdater(updater, state.antigravityQuota)
|
||||
@@ -47,11 +50,16 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
||||
set((state) => ({
|
||||
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota)
|
||||
})),
|
||||
setKimiQuota: (updater) =>
|
||||
set((state) => ({
|
||||
kimiQuota: resolveUpdater(updater, state.kimiQuota)
|
||||
})),
|
||||
clearQuotaCache: () =>
|
||||
set({
|
||||
antigravityQuota: {},
|
||||
claudeQuota: {},
|
||||
codexQuota: {},
|
||||
geminiCliQuota: {}
|
||||
geminiCliQuota: {},
|
||||
kimiQuota: {}
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -197,3 +197,64 @@ export interface CodexQuotaState {
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
}
|
||||
|
||||
// Kimi API payload types
|
||||
export interface KimiUsageDetail {
|
||||
used?: number;
|
||||
limit?: number;
|
||||
remaining?: number;
|
||||
name?: string;
|
||||
title?: string;
|
||||
resetAt?: string;
|
||||
reset_at?: string;
|
||||
resetTime?: string;
|
||||
reset_time?: string;
|
||||
resetIn?: number;
|
||||
reset_in?: number;
|
||||
ttl?: number;
|
||||
}
|
||||
|
||||
export interface KimiLimitWindow {
|
||||
duration?: number;
|
||||
timeUnit?: string;
|
||||
}
|
||||
|
||||
export interface KimiLimitItem {
|
||||
name?: string;
|
||||
title?: string;
|
||||
scope?: string;
|
||||
detail?: KimiUsageDetail;
|
||||
window?: KimiLimitWindow;
|
||||
used?: number;
|
||||
limit?: number;
|
||||
remaining?: number;
|
||||
duration?: number;
|
||||
timeUnit?: string;
|
||||
resetAt?: string;
|
||||
reset_at?: string;
|
||||
resetIn?: number;
|
||||
reset_in?: number;
|
||||
ttl?: number;
|
||||
}
|
||||
|
||||
export interface KimiUsagePayload {
|
||||
usage?: KimiUsageDetail;
|
||||
limits?: KimiLimitItem[];
|
||||
}
|
||||
|
||||
export interface KimiQuotaRow {
|
||||
id: string;
|
||||
label?: string;
|
||||
labelKey?: string;
|
||||
labelParams?: Record<string, string | number>;
|
||||
used: number;
|
||||
limit: number;
|
||||
resetHint?: string;
|
||||
}
|
||||
|
||||
export interface KimiQuotaState {
|
||||
status: 'idle' | 'loading' | 'success' | 'error';
|
||||
rows: KimiQuotaRow[];
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ import type {
|
||||
AntigravityModelsPayload,
|
||||
GeminiCliParsedBucket,
|
||||
GeminiCliQuotaBucketState,
|
||||
KimiUsagePayload,
|
||||
KimiUsageDetail,
|
||||
KimiLimitItem,
|
||||
KimiLimitWindow,
|
||||
KimiQuotaRow,
|
||||
} from '@/types';
|
||||
import {
|
||||
ANTIGRAVITY_QUOTA_GROUPS,
|
||||
@@ -260,3 +265,156 @@ export function buildAntigravityQuotaGroups(
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function toInt(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value);
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value.trim());
|
||||
return Number.isFinite(parsed) ? Math.floor(parsed) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type KimiRowLabel = Pick<KimiQuotaRow, 'label' | 'labelKey' | 'labelParams'>;
|
||||
|
||||
function kimiResetHint(data: Record<string, unknown>): string | undefined {
|
||||
const absoluteKeys = ['reset_at', 'resetAt', 'reset_time', 'resetTime'];
|
||||
for (const key of absoluteKeys) {
|
||||
const raw = data[key];
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
const truncated = raw.replace(/(\.\d{6})\d+/, '$1');
|
||||
const date = new Date(truncated);
|
||||
if (Number.isNaN(date.getTime())) continue;
|
||||
const now = Date.now();
|
||||
const delta = date.getTime() - now;
|
||||
if (delta <= 0) return undefined;
|
||||
const totalMinutes = Math.floor(delta / 60000);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
|
||||
if (hours > 0) return `${hours}h`;
|
||||
if (minutes > 0) return `${minutes}m`;
|
||||
return '<1m';
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const relativeKeys = ['reset_in', 'resetIn', 'ttl'];
|
||||
for (const key of relativeKeys) {
|
||||
const raw = toInt(data[key]);
|
||||
if (raw !== null && raw > 0) {
|
||||
const hours = Math.floor(raw / 3600);
|
||||
const minutes = Math.floor((raw % 3600) / 60);
|
||||
if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
|
||||
if (hours > 0) return `${hours}h`;
|
||||
if (minutes > 0) return `${minutes}m`;
|
||||
return '<1m';
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function kimiDurationToken(duration: number, rawTimeUnit: unknown): string {
|
||||
const unit = typeof rawTimeUnit === 'string' ? rawTimeUnit.trim().toUpperCase() : '';
|
||||
if (unit === 'MINUTES') {
|
||||
return duration % 60 === 0 ? `${duration / 60}h` : `${duration}m`;
|
||||
}
|
||||
if (unit === 'HOURS') return `${duration}h`;
|
||||
if (unit === 'DAYS') return `${duration}d`;
|
||||
return `${duration}s`;
|
||||
}
|
||||
|
||||
function kimiLimitLabel(
|
||||
item: KimiLimitItem,
|
||||
detail: KimiUsageDetail | KimiLimitItem,
|
||||
window: KimiLimitWindow,
|
||||
index: number
|
||||
): KimiRowLabel {
|
||||
for (const key of ['name', 'title', 'scope'] as const) {
|
||||
const val = (item as Record<string, unknown>)[key] ?? (detail as Record<string, unknown>)[key];
|
||||
if (typeof val === 'string' && val.trim()) return { label: val.trim() };
|
||||
}
|
||||
|
||||
const duration =
|
||||
toInt(window.duration) ??
|
||||
toInt((item as Record<string, unknown>).duration) ??
|
||||
toInt((detail as Record<string, unknown>).duration);
|
||||
const timeUnit =
|
||||
(window as Record<string, unknown>).timeUnit ??
|
||||
(item as Record<string, unknown>).timeUnit ??
|
||||
(detail as Record<string, unknown>).timeUnit;
|
||||
|
||||
if (duration !== null && duration > 0) {
|
||||
return {
|
||||
labelKey: 'kimi_quota.limit_window',
|
||||
labelParams: {
|
||||
duration: kimiDurationToken(duration, timeUnit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
labelKey: 'kimi_quota.limit_index',
|
||||
labelParams: {
|
||||
index: index + 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toKimiUsageRow(
|
||||
data: Record<string, unknown>,
|
||||
fallbackLabel: KimiRowLabel
|
||||
): (KimiRowLabel & { used: number; limit: number; resetHint?: string }) | null {
|
||||
const limit = toInt(data.limit);
|
||||
let used = toInt(data.used);
|
||||
if (used === null) {
|
||||
const remaining = toInt(data.remaining);
|
||||
if (remaining !== null && limit !== null) {
|
||||
used = limit - remaining;
|
||||
}
|
||||
}
|
||||
if (used === null && limit === null) return null;
|
||||
const explicitLabel =
|
||||
(typeof data.name === 'string' && data.name.trim()) ||
|
||||
(typeof data.title === 'string' && data.title.trim());
|
||||
const label = explicitLabel ? { label: explicitLabel } : fallbackLabel;
|
||||
return {
|
||||
...label,
|
||||
used: used ?? 0,
|
||||
limit: limit ?? 0,
|
||||
resetHint: kimiResetHint(data),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildKimiQuotaRows(payload: KimiUsagePayload): KimiQuotaRow[] {
|
||||
const rows: KimiQuotaRow[] = [];
|
||||
|
||||
const usage = payload.usage;
|
||||
if (usage && typeof usage === 'object') {
|
||||
const summary = toKimiUsageRow(usage as Record<string, unknown>, {
|
||||
labelKey: 'kimi_quota.weekly_limit',
|
||||
});
|
||||
if (summary) {
|
||||
rows.push({ id: 'summary', ...summary });
|
||||
}
|
||||
}
|
||||
|
||||
const limits = payload.limits;
|
||||
if (Array.isArray(limits)) {
|
||||
limits.forEach((item, idx) => {
|
||||
const detail = (item.detail && typeof item.detail === 'object' ? item.detail : item) as KimiUsageDetail | KimiLimitItem;
|
||||
const window = (item.window && typeof item.window === 'object' ? item.window : {}) as KimiLimitWindow;
|
||||
const fallbackLabel = kimiLimitLabel(item, detail, window, idx);
|
||||
const row = toKimiUsageRow(detail as Record<string, unknown>, fallbackLabel);
|
||||
if (row) {
|
||||
rows.push({ id: `limit-${idx}`, ...row });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ export const TYPE_COLORS: Record<string, TypeColorSet> = {
|
||||
light: { bg: '#fff3e0', text: '#ef6c00' },
|
||||
dark: { bg: '#e65100', text: '#ffb74d' },
|
||||
},
|
||||
kimi: {
|
||||
light: { bg: '#fff4e5', text: '#ad6800' },
|
||||
dark: { bg: '#7c4a03', text: '#ffd591' },
|
||||
},
|
||||
antigravity: {
|
||||
light: { bg: '#e0f7fa', text: '#006064' },
|
||||
dark: { bg: '#004d40', text: '#80deea' },
|
||||
@@ -178,3 +182,10 @@ export const CODEX_REQUEST_HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal',
|
||||
};
|
||||
|
||||
// Kimi API configuration
|
||||
export const KIMI_USAGE_URL = 'https://api.kimi.com/coding/v1/usages';
|
||||
|
||||
export const KIMI_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Formatting functions for quota display.
|
||||
*/
|
||||
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { CodexUsageWindow } from '@/types';
|
||||
import { normalizeNumberValue } from './parsers';
|
||||
|
||||
@@ -66,3 +67,8 @@ export function getStatusFromError(err: unknown): number | undefined {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatKimiResetHint(t: TFunction, hint?: string): string {
|
||||
if (!hint) return '';
|
||||
return t('kimi_quota.reset_hint', { hint });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Normalization and parsing functions for quota data.
|
||||
*/
|
||||
|
||||
import type { ClaudeUsagePayload, CodexUsagePayload, GeminiCliQuotaPayload } from '@/types';
|
||||
import type { ClaudeUsagePayload, CodexUsagePayload, GeminiCliQuotaPayload, KimiUsagePayload } from '@/types';
|
||||
import { normalizeAuthIndex } from '@/utils/usage';
|
||||
|
||||
const GEMINI_CLI_MODEL_SUFFIX = '_vertex';
|
||||
@@ -170,3 +170,20 @@ export function parseGeminiCliQuotaPayload(payload: unknown): GeminiCliQuotaPayl
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseKimiUsagePayload(payload: unknown): KimiUsagePayload | null {
|
||||
if (payload === undefined || payload === null) return null;
|
||||
if (typeof payload === 'string') {
|
||||
const trimmed = payload.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as KimiUsagePayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof payload === 'object') {
|
||||
return payload as KimiUsagePayload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ export function isGeminiCliFile(file: AuthFileItem): boolean {
|
||||
return resolveAuthProvider(file) === 'gemini-cli';
|
||||
}
|
||||
|
||||
export function isKimiFile(file: AuthFileItem): boolean {
|
||||
return resolveAuthProvider(file) === 'kimi';
|
||||
}
|
||||
|
||||
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
||||
const raw = file['runtime_only'] ?? file.runtimeOnly;
|
||||
if (typeof raw === 'boolean') return raw;
|
||||
|
||||
Reference in New Issue
Block a user