feat: add Kiro (AWS CodeWhisperer) quota display support
- Add Kiro quota types (KiroFreeTrialInfo, KiroUsageBreakdown, KiroQuotaPayload, KiroQuotaState) - Add Kiro API constants and request headers - Add isKiroFile validator and parseKiroQuotaPayload parser - Add KIRO_CONFIG with fetchKiroQuota and renderKiroItems - Add kiroQuota state to useQuotaStore - Add Kiro QuotaSection to QuotaPage - Add Kiro styles (.kiroGrid, .kiroControls, .kiroControl, .kiroCard) - Add i18n translations for kiro_quota (zh-CN and en) Features: - Separate display for base credits and bonus credits (freeTrialInfo) - Total credits summary - Correct parsing of seconds timestamp (scientific notation like 1.769904E9) - Reset time display - Subscription type display
This commit is contained in:
@@ -5,5 +5,5 @@
|
|||||||
export { QuotaSection } from './QuotaSection';
|
export { QuotaSection } from './QuotaSection';
|
||||||
export { QuotaCard } from './QuotaCard';
|
export { QuotaCard } from './QuotaCard';
|
||||||
export { useQuotaLoader } from './useQuotaLoader';
|
export { useQuotaLoader } from './useQuotaLoader';
|
||||||
export { ANTIGRAVITY_CONFIG, CODEX_CONFIG, GEMINI_CLI_CONFIG } from './quotaConfigs';
|
export { ANTIGRAVITY_CONFIG, CODEX_CONFIG, GEMINI_CLI_CONFIG, KIRO_CONFIG } from './quotaConfigs';
|
||||||
export type { QuotaConfig } from './quotaConfigs';
|
export type { QuotaConfig } from './quotaConfigs';
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import type {
|
|||||||
CodexUsagePayload,
|
CodexUsagePayload,
|
||||||
GeminiCliParsedBucket,
|
GeminiCliParsedBucket,
|
||||||
GeminiCliQuotaBucketState,
|
GeminiCliQuotaBucketState,
|
||||||
GeminiCliQuotaState
|
GeminiCliQuotaState,
|
||||||
|
KiroQuotaState
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
|
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +27,8 @@ import {
|
|||||||
CODEX_REQUEST_HEADERS,
|
CODEX_REQUEST_HEADERS,
|
||||||
GEMINI_CLI_QUOTA_URL,
|
GEMINI_CLI_QUOTA_URL,
|
||||||
GEMINI_CLI_REQUEST_HEADERS,
|
GEMINI_CLI_REQUEST_HEADERS,
|
||||||
|
KIRO_QUOTA_URL,
|
||||||
|
KIRO_REQUEST_HEADERS,
|
||||||
normalizeAuthIndexValue,
|
normalizeAuthIndexValue,
|
||||||
normalizeNumberValue,
|
normalizeNumberValue,
|
||||||
normalizePlanType,
|
normalizePlanType,
|
||||||
@@ -34,6 +37,7 @@ import {
|
|||||||
parseAntigravityPayload,
|
parseAntigravityPayload,
|
||||||
parseCodexUsagePayload,
|
parseCodexUsagePayload,
|
||||||
parseGeminiCliQuotaPayload,
|
parseGeminiCliQuotaPayload,
|
||||||
|
parseKiroQuotaPayload,
|
||||||
resolveCodexChatgptAccountId,
|
resolveCodexChatgptAccountId,
|
||||||
resolveCodexPlanType,
|
resolveCodexPlanType,
|
||||||
resolveGeminiCliProjectId,
|
resolveGeminiCliProjectId,
|
||||||
@@ -46,6 +50,7 @@ import {
|
|||||||
isAntigravityFile,
|
isAntigravityFile,
|
||||||
isCodexFile,
|
isCodexFile,
|
||||||
isGeminiCliFile,
|
isGeminiCliFile,
|
||||||
|
isKiroFile,
|
||||||
isRuntimeOnlyAuthFile
|
isRuntimeOnlyAuthFile
|
||||||
} from '@/utils/quota';
|
} from '@/utils/quota';
|
||||||
import type { QuotaRenderHelpers } from './QuotaCard';
|
import type { QuotaRenderHelpers } from './QuotaCard';
|
||||||
@@ -53,7 +58,7 @@ import styles from '@/pages/QuotaPage.module.scss';
|
|||||||
|
|
||||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||||
|
|
||||||
type QuotaType = 'antigravity' | 'codex' | 'gemini-cli';
|
type QuotaType = 'antigravity' | 'codex' | 'gemini-cli' | 'kiro';
|
||||||
|
|
||||||
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
||||||
|
|
||||||
@@ -61,9 +66,11 @@ export interface QuotaStore {
|
|||||||
antigravityQuota: Record<string, AntigravityQuotaState>;
|
antigravityQuota: Record<string, AntigravityQuotaState>;
|
||||||
codexQuota: Record<string, CodexQuotaState>;
|
codexQuota: Record<string, CodexQuotaState>;
|
||||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||||
|
kiroQuota: Record<string, KiroQuotaState>;
|
||||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||||
|
setKiroQuota: (updater: QuotaUpdater<Record<string, KiroQuotaState>>) => void;
|
||||||
clearQuotaCache: () => void;
|
clearQuotaCache: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,3 +609,291 @@ export const GEMINI_CLI_CONFIG: QuotaConfig<GeminiCliQuotaState, GeminiCliQuotaB
|
|||||||
gridClassName: styles.geminiCliGrid,
|
gridClassName: styles.geminiCliGrid,
|
||||||
renderQuotaItems: renderGeminiCliItems
|
renderQuotaItems: renderGeminiCliItems
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kiro quota data structure from API
|
||||||
|
interface KiroQuotaData {
|
||||||
|
// Base quota (原本额度)
|
||||||
|
baseUsage: number | null;
|
||||||
|
baseLimit: number | null;
|
||||||
|
baseRemaining: number | null;
|
||||||
|
// Free trial/bonus quota (赠送额度)
|
||||||
|
bonusUsage: number | null;
|
||||||
|
bonusLimit: number | null;
|
||||||
|
bonusRemaining: number | null;
|
||||||
|
bonusStatus?: string;
|
||||||
|
// Total (合计)
|
||||||
|
currentUsage: number | null;
|
||||||
|
usageLimit: number | null;
|
||||||
|
remainingCredits: number | null;
|
||||||
|
nextReset?: string;
|
||||||
|
subscriptionType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchKiroQuota = async (
|
||||||
|
file: AuthFileItem,
|
||||||
|
t: TFunction
|
||||||
|
): Promise<KiroQuotaData> => {
|
||||||
|
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||||
|
const authIndex = normalizeAuthIndexValue(rawAuthIndex);
|
||||||
|
if (!authIndex) {
|
||||||
|
throw new Error(t('kiro_quota.missing_auth_index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiCallApi.request({
|
||||||
|
authIndex,
|
||||||
|
method: 'GET',
|
||||||
|
url: KIRO_QUOTA_URL,
|
||||||
|
header: { ...KIRO_REQUEST_HEADERS }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.statusCode < 200 || result.statusCode >= 300) {
|
||||||
|
throw createStatusError(getApiCallErrorMessage(result), result.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = parseKiroQuotaPayload(result.body ?? result.bodyText);
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error(t('kiro_quota.empty_data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract usage data from usageBreakdownList (separating base and bonus)
|
||||||
|
const breakdownList = payload.usageBreakdownList ?? [];
|
||||||
|
let baseLimit = 0;
|
||||||
|
let baseUsage = 0;
|
||||||
|
let bonusLimit = 0;
|
||||||
|
let bonusUsage = 0;
|
||||||
|
let bonusStatus: string | undefined;
|
||||||
|
|
||||||
|
for (const breakdown of breakdownList) {
|
||||||
|
// Add base quota
|
||||||
|
const limit = normalizeNumberValue(breakdown.usageLimitWithPrecision ?? breakdown.usageLimit);
|
||||||
|
const usage = normalizeNumberValue(breakdown.currentUsageWithPrecision ?? breakdown.currentUsage);
|
||||||
|
if (limit !== null) baseLimit += limit;
|
||||||
|
if (usage !== null) baseUsage += usage;
|
||||||
|
|
||||||
|
// Add free trial quota if available (e.g., 500 bonus credits)
|
||||||
|
const freeTrialInfo = breakdown.freeTrialInfo;
|
||||||
|
if (freeTrialInfo) {
|
||||||
|
const freeLimit = normalizeNumberValue(freeTrialInfo.usageLimitWithPrecision ?? freeTrialInfo.usageLimit);
|
||||||
|
const freeUsage = normalizeNumberValue(freeTrialInfo.currentUsageWithPrecision ?? freeTrialInfo.currentUsage);
|
||||||
|
if (freeLimit !== null) bonusLimit += freeLimit;
|
||||||
|
if (freeUsage !== null) bonusUsage += freeUsage;
|
||||||
|
if (freeTrialInfo.freeTrialStatus) {
|
||||||
|
bonusStatus = freeTrialInfo.freeTrialStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalLimit = baseLimit + bonusLimit;
|
||||||
|
const totalUsage = baseUsage + bonusUsage;
|
||||||
|
|
||||||
|
// Calculate next reset time
|
||||||
|
// Note: nextDateReset from Kiro API is in SECONDS (e.g., 1.769904E9 = 1769904000)
|
||||||
|
// JavaScript Date() requires milliseconds, so multiply by 1000
|
||||||
|
let nextReset: string | undefined;
|
||||||
|
if (payload.nextDateReset) {
|
||||||
|
// API returns seconds timestamp (scientific notation like 1.769904E9)
|
||||||
|
const timestampSeconds = payload.nextDateReset;
|
||||||
|
const resetDate = new Date(timestampSeconds * 1000);
|
||||||
|
if (!isNaN(resetDate.getTime())) {
|
||||||
|
nextReset = resetDate.toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get subscription type
|
||||||
|
const subscriptionType = payload.subscriptionInfo?.subscriptionTitle ?? payload.subscriptionInfo?.type;
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseUsage,
|
||||||
|
baseLimit,
|
||||||
|
baseRemaining: baseLimit > 0 ? Math.max(0, baseLimit - baseUsage) : null,
|
||||||
|
bonusUsage,
|
||||||
|
bonusLimit,
|
||||||
|
bonusRemaining: bonusLimit > 0 ? Math.max(0, bonusLimit - bonusUsage) : null,
|
||||||
|
bonusStatus,
|
||||||
|
currentUsage: totalUsage,
|
||||||
|
usageLimit: totalLimit,
|
||||||
|
remainingCredits: totalLimit > 0 ? Math.max(0, totalLimit - totalUsage) : null,
|
||||||
|
nextReset,
|
||||||
|
subscriptionType
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderKiroItems = (
|
||||||
|
quota: KiroQuotaState,
|
||||||
|
t: TFunction,
|
||||||
|
helpers: QuotaRenderHelpers
|
||||||
|
): ReactNode => {
|
||||||
|
const { styles: styleMap, QuotaProgressBar } = helpers;
|
||||||
|
const { createElement: h, Fragment } = React;
|
||||||
|
|
||||||
|
const nodes: ReactNode[] = [];
|
||||||
|
|
||||||
|
// Show subscription type if available
|
||||||
|
if (quota.subscriptionType) {
|
||||||
|
nodes.push(
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ key: 'subscription', className: styleMap.codexPlan },
|
||||||
|
h('span', { className: styleMap.codexPlanLabel }, t('kiro_quota.subscription_label')),
|
||||||
|
h('span', { className: styleMap.codexPlanValue }, quota.subscriptionType)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usageLimit = quota.usageLimit;
|
||||||
|
|
||||||
|
if (usageLimit === null || usageLimit === 0) {
|
||||||
|
nodes.push(
|
||||||
|
h('div', { key: 'empty', className: styleMap.quotaMessage }, t('kiro_quota.empty_data'))
|
||||||
|
);
|
||||||
|
return h(Fragment, null, ...nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetLabel = formatQuotaResetTime(quota.nextReset);
|
||||||
|
|
||||||
|
// Base quota display (原本额度)
|
||||||
|
const baseLimit = quota.baseLimit;
|
||||||
|
const baseRemaining = quota.baseRemaining;
|
||||||
|
if (baseLimit !== null && baseLimit > 0) {
|
||||||
|
const baseRemainingPercent = baseRemaining !== null && baseLimit > 0
|
||||||
|
? Math.round((baseRemaining / baseLimit) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ key: 'base-credits', className: styleMap.quotaRow },
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaRowHeader },
|
||||||
|
h('span', { className: styleMap.quotaModel }, t('kiro_quota.base_credits_label')),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaMeta },
|
||||||
|
h('span', { className: styleMap.quotaPercent }, `${baseRemainingPercent}%`),
|
||||||
|
baseRemaining !== null
|
||||||
|
? h('span', { className: styleMap.quotaAmount }, t('kiro_quota.remaining_credits', { count: Math.round(baseRemaining) }))
|
||||||
|
: null,
|
||||||
|
h('span', { className: styleMap.quotaReset }, resetLabel)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
h(QuotaProgressBar, { percent: baseRemainingPercent, highThreshold: 60, mediumThreshold: 20 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus quota display (赠送额度)
|
||||||
|
const bonusLimit = quota.bonusLimit;
|
||||||
|
const bonusRemaining = quota.bonusRemaining;
|
||||||
|
if (bonusLimit !== null && bonusLimit > 0) {
|
||||||
|
const bonusRemainingPercent = bonusRemaining !== null && bonusLimit > 0
|
||||||
|
? Math.round((bonusRemaining / bonusLimit) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ key: 'bonus-credits', className: styleMap.quotaRow },
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaRowHeader },
|
||||||
|
h('span', { className: styleMap.quotaModel }, t('kiro_quota.bonus_credits_label')),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaMeta },
|
||||||
|
h('span', { className: styleMap.quotaPercent }, `${bonusRemainingPercent}%`),
|
||||||
|
bonusRemaining !== null
|
||||||
|
? h('span', { className: styleMap.quotaAmount }, t('kiro_quota.remaining_credits', { count: Math.round(bonusRemaining) }))
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
h(QuotaProgressBar, { percent: bonusRemainingPercent, highThreshold: 60, mediumThreshold: 20 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total credits display (合计)
|
||||||
|
const currentUsage = quota.currentUsage;
|
||||||
|
const remainingCredits = quota.remainingCredits;
|
||||||
|
const totalRemainingPercent = currentUsage !== null && usageLimit > 0
|
||||||
|
? Math.max(0, 100 - Math.round((currentUsage / usageLimit) * 100))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ key: 'total-credits', className: styleMap.quotaRow },
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaRowHeader },
|
||||||
|
h('span', { className: styleMap.quotaModel }, t('kiro_quota.total_credits_label')),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaMeta },
|
||||||
|
h('span', { className: styleMap.quotaPercent }, `${totalRemainingPercent}%`),
|
||||||
|
remainingCredits !== null
|
||||||
|
? h('span', { className: styleMap.quotaAmount }, t('kiro_quota.remaining_credits', { count: Math.round(remainingCredits) }))
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
h(QuotaProgressBar, { percent: totalRemainingPercent, highThreshold: 60, mediumThreshold: 20 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return h(Fragment, null, ...nodes);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KIRO_CONFIG: QuotaConfig<KiroQuotaState, KiroQuotaData> = {
|
||||||
|
type: 'kiro',
|
||||||
|
i18nPrefix: 'kiro_quota',
|
||||||
|
filterFn: (file) => isKiroFile(file),
|
||||||
|
fetchQuota: fetchKiroQuota,
|
||||||
|
storeSelector: (state) => state.kiroQuota,
|
||||||
|
storeSetter: 'setKiroQuota',
|
||||||
|
buildLoadingState: () => ({
|
||||||
|
status: 'loading',
|
||||||
|
baseUsage: null,
|
||||||
|
baseLimit: null,
|
||||||
|
baseRemaining: null,
|
||||||
|
bonusUsage: null,
|
||||||
|
bonusLimit: null,
|
||||||
|
bonusRemaining: null,
|
||||||
|
currentUsage: null,
|
||||||
|
usageLimit: null,
|
||||||
|
remainingCredits: null
|
||||||
|
}),
|
||||||
|
buildSuccessState: (data) => ({
|
||||||
|
status: 'success',
|
||||||
|
baseUsage: data.baseUsage,
|
||||||
|
baseLimit: data.baseLimit,
|
||||||
|
baseRemaining: data.baseRemaining,
|
||||||
|
bonusUsage: data.bonusUsage,
|
||||||
|
bonusLimit: data.bonusLimit,
|
||||||
|
bonusRemaining: data.bonusRemaining,
|
||||||
|
bonusStatus: data.bonusStatus,
|
||||||
|
currentUsage: data.currentUsage,
|
||||||
|
usageLimit: data.usageLimit,
|
||||||
|
remainingCredits: data.remainingCredits,
|
||||||
|
nextReset: data.nextReset,
|
||||||
|
subscriptionType: data.subscriptionType
|
||||||
|
}),
|
||||||
|
buildErrorState: (message, status) => ({
|
||||||
|
status: 'error',
|
||||||
|
baseUsage: null,
|
||||||
|
baseLimit: null,
|
||||||
|
baseRemaining: null,
|
||||||
|
bonusUsage: null,
|
||||||
|
bonusLimit: null,
|
||||||
|
bonusRemaining: null,
|
||||||
|
currentUsage: null,
|
||||||
|
usageLimit: null,
|
||||||
|
remainingCredits: null,
|
||||||
|
error: message,
|
||||||
|
errorStatus: status
|
||||||
|
}),
|
||||||
|
cardClassName: styles.kiroCard,
|
||||||
|
controlsClassName: styles.kiroControls,
|
||||||
|
controlClassName: styles.kiroControl,
|
||||||
|
gridClassName: styles.kiroGrid,
|
||||||
|
renderQuotaItems: renderKiroItems
|
||||||
|
};
|
||||||
|
|||||||
@@ -375,6 +375,7 @@
|
|||||||
"filter_claude": "Claude",
|
"filter_claude": "Claude",
|
||||||
"filter_codex": "Codex",
|
"filter_codex": "Codex",
|
||||||
"filter_antigravity": "Antigravity",
|
"filter_antigravity": "Antigravity",
|
||||||
|
"filter_kiro": "Kiro",
|
||||||
"filter_iflow": "iFlow",
|
"filter_iflow": "iFlow",
|
||||||
"filter_vertex": "Vertex",
|
"filter_vertex": "Vertex",
|
||||||
"filter_empty": "Empty",
|
"filter_empty": "Empty",
|
||||||
@@ -386,6 +387,7 @@
|
|||||||
"type_claude": "Claude",
|
"type_claude": "Claude",
|
||||||
"type_codex": "Codex",
|
"type_codex": "Codex",
|
||||||
"type_antigravity": "Antigravity",
|
"type_antigravity": "Antigravity",
|
||||||
|
"type_kiro": "Kiro",
|
||||||
"type_iflow": "iFlow",
|
"type_iflow": "iFlow",
|
||||||
"type_vertex": "Vertex",
|
"type_vertex": "Vertex",
|
||||||
"type_empty": "Empty",
|
"type_empty": "Empty",
|
||||||
@@ -460,6 +462,23 @@
|
|||||||
"fetch_all": "Fetch All",
|
"fetch_all": "Fetch All",
|
||||||
"remaining_amount": "Remaining {{count}}"
|
"remaining_amount": "Remaining {{count}}"
|
||||||
},
|
},
|
||||||
|
"kiro_quota": {
|
||||||
|
"title": "Kiro Quota",
|
||||||
|
"empty_title": "No Kiro Auth Files",
|
||||||
|
"empty_desc": "Upload a Kiro credential to view remaining quota.",
|
||||||
|
"idle": "Not loaded. Click Refresh Button.",
|
||||||
|
"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",
|
||||||
|
"subscription_label": "Subscription",
|
||||||
|
"base_credits_label": "Base Credits",
|
||||||
|
"bonus_credits_label": "Bonus Credits",
|
||||||
|
"total_credits_label": "Total Credits",
|
||||||
|
"remaining_credits": "Remaining {{count}}"
|
||||||
|
},
|
||||||
"vertex_import": {
|
"vertex_import": {
|
||||||
"title": "Vertex JSON Login",
|
"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.",
|
"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.",
|
||||||
|
|||||||
@@ -375,6 +375,7 @@
|
|||||||
"filter_claude": "Claude",
|
"filter_claude": "Claude",
|
||||||
"filter_codex": "Codex",
|
"filter_codex": "Codex",
|
||||||
"filter_antigravity": "Antigravity",
|
"filter_antigravity": "Antigravity",
|
||||||
|
"filter_kiro": "Kiro",
|
||||||
"filter_iflow": "iFlow",
|
"filter_iflow": "iFlow",
|
||||||
"filter_vertex": "Vertex",
|
"filter_vertex": "Vertex",
|
||||||
"filter_empty": "空文件",
|
"filter_empty": "空文件",
|
||||||
@@ -386,6 +387,7 @@
|
|||||||
"type_claude": "Claude",
|
"type_claude": "Claude",
|
||||||
"type_codex": "Codex",
|
"type_codex": "Codex",
|
||||||
"type_antigravity": "Antigravity",
|
"type_antigravity": "Antigravity",
|
||||||
|
"type_kiro": "Kiro",
|
||||||
"type_iflow": "iFlow",
|
"type_iflow": "iFlow",
|
||||||
"type_vertex": "Vertex",
|
"type_vertex": "Vertex",
|
||||||
"type_empty": "空文件",
|
"type_empty": "空文件",
|
||||||
@@ -460,6 +462,23 @@
|
|||||||
"fetch_all": "获取全部",
|
"fetch_all": "获取全部",
|
||||||
"remaining_amount": "剩余 {{count}}"
|
"remaining_amount": "剩余 {{count}}"
|
||||||
},
|
},
|
||||||
|
"kiro_quota": {
|
||||||
|
"title": "Kiro 额度",
|
||||||
|
"empty_title": "暂无 Kiro 认证",
|
||||||
|
"empty_desc": "上传 Kiro 认证文件后即可查看额度。",
|
||||||
|
"idle": "尚未加载额度,请点击刷新按钮。",
|
||||||
|
"loading": "正在加载额度...",
|
||||||
|
"load_failed": "额度获取失败:{{message}}",
|
||||||
|
"missing_auth_index": "认证文件缺少 auth_index",
|
||||||
|
"empty_data": "暂无额度数据",
|
||||||
|
"refresh_button": "刷新额度",
|
||||||
|
"fetch_all": "获取全部",
|
||||||
|
"subscription_label": "订阅类型",
|
||||||
|
"base_credits_label": "基础额度",
|
||||||
|
"bonus_credits_label": "赠送额度",
|
||||||
|
"total_credits_label": "合计额度",
|
||||||
|
"remaining_credits": "剩余 {{count}}"
|
||||||
|
},
|
||||||
"vertex_import": {
|
"vertex_import": {
|
||||||
"title": "Vertex JSON 登录",
|
"title": "Vertex JSON 登录",
|
||||||
"description": "上传 Google 服务账号 JSON,使用 CLI vertex-import 同步规则写入 auth-dir/vertex-<project>.json。",
|
"description": "上传 Google 服务账号 JSON,使用 CLI vertex-import 同步规则写入 auth-dir/vertex-<project>.json。",
|
||||||
|
|||||||
@@ -104,7 +104,8 @@
|
|||||||
|
|
||||||
.antigravityGrid,
|
.antigravityGrid,
|
||||||
.codexGrid,
|
.codexGrid,
|
||||||
.geminiCliGrid {
|
.geminiCliGrid,
|
||||||
|
.kiroGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $spacing-md;
|
gap: $spacing-md;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||||
@@ -116,7 +117,8 @@
|
|||||||
|
|
||||||
.antigravityControls,
|
.antigravityControls,
|
||||||
.codexControls,
|
.codexControls,
|
||||||
.geminiCliControls {
|
.geminiCliControls,
|
||||||
|
.kiroControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-md;
|
gap: $spacing-md;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -126,7 +128,8 @@
|
|||||||
|
|
||||||
.antigravityControl,
|
.antigravityControl,
|
||||||
.codexControl,
|
.codexControl,
|
||||||
.geminiCliControl {
|
.geminiCliControl,
|
||||||
|
.kiroControl {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -163,6 +166,12 @@
|
|||||||
rgba(231, 239, 255, 0));
|
rgba(231, 239, 255, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kiroCard {
|
||||||
|
background-image: linear-gradient(180deg,
|
||||||
|
rgba(255, 248, 225, 0.18),
|
||||||
|
rgba(255, 248, 225, 0));
|
||||||
|
}
|
||||||
|
|
||||||
.quotaSection {
|
.quotaSection {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Quota management page - coordinates the three quota sections.
|
* Quota management page - coordinates the four quota sections.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
QuotaSection,
|
QuotaSection,
|
||||||
ANTIGRAVITY_CONFIG,
|
ANTIGRAVITY_CONFIG,
|
||||||
CODEX_CONFIG,
|
CODEX_CONFIG,
|
||||||
GEMINI_CLI_CONFIG
|
GEMINI_CLI_CONFIG,
|
||||||
|
KIRO_CONFIG
|
||||||
} from '@/components/quota';
|
} from '@/components/quota';
|
||||||
import type { AuthFileItem } from '@/types';
|
import type { AuthFileItem } from '@/types';
|
||||||
import styles from './QuotaPage.module.scss';
|
import styles from './QuotaPage.module.scss';
|
||||||
@@ -75,6 +76,12 @@ export function QuotaPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disableControls}
|
disabled={disableControls}
|
||||||
/>
|
/>
|
||||||
|
<QuotaSection
|
||||||
|
config={KIRO_CONFIG}
|
||||||
|
files={files}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disableControls}
|
||||||
|
/>
|
||||||
<QuotaSection
|
<QuotaSection
|
||||||
config={CODEX_CONFIG}
|
config={CODEX_CONFIG}
|
||||||
files={files}
|
files={files}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { AntigravityQuotaState, CodexQuotaState, GeminiCliQuotaState } from '@/types';
|
import type { AntigravityQuotaState, CodexQuotaState, GeminiCliQuotaState, KiroQuotaState } from '@/types';
|
||||||
|
|
||||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||||
|
|
||||||
@@ -11,9 +11,11 @@ interface QuotaStoreState {
|
|||||||
antigravityQuota: Record<string, AntigravityQuotaState>;
|
antigravityQuota: Record<string, AntigravityQuotaState>;
|
||||||
codexQuota: Record<string, CodexQuotaState>;
|
codexQuota: Record<string, CodexQuotaState>;
|
||||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||||
|
kiroQuota: Record<string, KiroQuotaState>;
|
||||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||||
|
setKiroQuota: (updater: QuotaUpdater<Record<string, KiroQuotaState>>) => void;
|
||||||
clearQuotaCache: () => void;
|
clearQuotaCache: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
|||||||
antigravityQuota: {},
|
antigravityQuota: {},
|
||||||
codexQuota: {},
|
codexQuota: {},
|
||||||
geminiCliQuota: {},
|
geminiCliQuota: {},
|
||||||
|
kiroQuota: {},
|
||||||
setAntigravityQuota: (updater) =>
|
setAntigravityQuota: (updater) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
antigravityQuota: resolveUpdater(updater, state.antigravityQuota)
|
antigravityQuota: resolveUpdater(updater, state.antigravityQuota)
|
||||||
@@ -40,10 +43,15 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota)
|
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota)
|
||||||
})),
|
})),
|
||||||
|
setKiroQuota: (updater) =>
|
||||||
|
set((state) => ({
|
||||||
|
kiroQuota: resolveUpdater(updater, state.kiroQuota)
|
||||||
|
})),
|
||||||
clearQuotaCache: () =>
|
clearQuotaCache: () =>
|
||||||
set({
|
set({
|
||||||
antigravityQuota: {},
|
antigravityQuota: {},
|
||||||
codexQuota: {},
|
codexQuota: {},
|
||||||
geminiCliQuota: {}
|
geminiCliQuota: {},
|
||||||
|
kiroQuota: {}
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -145,3 +145,58 @@ export interface CodexQuotaState {
|
|||||||
error?: string;
|
error?: string;
|
||||||
errorStatus?: number;
|
errorStatus?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kiro (AWS CodeWhisperer) quota types
|
||||||
|
export interface KiroFreeTrialInfo {
|
||||||
|
freeTrialStatus?: string;
|
||||||
|
usageLimit?: number;
|
||||||
|
currentUsage?: number;
|
||||||
|
usageLimitWithPrecision?: number;
|
||||||
|
currentUsageWithPrecision?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KiroUsageBreakdown {
|
||||||
|
usageLimit?: number;
|
||||||
|
currentUsage?: number;
|
||||||
|
usageLimitWithPrecision?: number;
|
||||||
|
currentUsageWithPrecision?: number;
|
||||||
|
nextDateReset?: number;
|
||||||
|
displayName?: string;
|
||||||
|
resourceType?: string;
|
||||||
|
freeTrialInfo?: KiroFreeTrialInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KiroQuotaPayload {
|
||||||
|
daysUntilReset?: number;
|
||||||
|
nextDateReset?: number;
|
||||||
|
userInfo?: {
|
||||||
|
email?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
subscriptionInfo?: {
|
||||||
|
subscriptionTitle?: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
usageBreakdownList?: KiroUsageBreakdown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KiroQuotaState {
|
||||||
|
status: 'idle' | 'loading' | 'success' | 'error';
|
||||||
|
// Base quota (原本额度)
|
||||||
|
baseUsage: number | null;
|
||||||
|
baseLimit: number | null;
|
||||||
|
baseRemaining: number | null;
|
||||||
|
// Free trial/bonus quota (赠送额度)
|
||||||
|
bonusUsage: number | null;
|
||||||
|
bonusLimit: number | null;
|
||||||
|
bonusRemaining: number | null;
|
||||||
|
bonusStatus?: string;
|
||||||
|
// Total (合计)
|
||||||
|
currentUsage: number | null;
|
||||||
|
usageLimit: number | null;
|
||||||
|
remainingCredits: number | null;
|
||||||
|
nextReset?: string;
|
||||||
|
subscriptionType?: string;
|
||||||
|
error?: string;
|
||||||
|
errorStatus?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export const TYPE_COLORS: Record<string, TypeColorSet> = {
|
|||||||
light: { bg: '#e0f7fa', text: '#006064' },
|
light: { bg: '#e0f7fa', text: '#006064' },
|
||||||
dark: { bg: '#004d40', text: '#80deea' },
|
dark: { bg: '#004d40', text: '#80deea' },
|
||||||
},
|
},
|
||||||
|
kiro: {
|
||||||
|
light: { bg: '#fff8e1', text: '#ff8f00' },
|
||||||
|
dark: { bg: '#ff6f00', text: '#ffe082' },
|
||||||
|
},
|
||||||
iflow: {
|
iflow: {
|
||||||
light: { bg: '#f3e5f5', text: '#7b1fa2' },
|
light: { bg: '#f3e5f5', text: '#7b1fa2' },
|
||||||
dark: { bg: '#4a148c', text: '#ce93d8' },
|
dark: { bg: '#4a148c', text: '#ce93d8' },
|
||||||
@@ -149,3 +153,15 @@ export const CODEX_REQUEST_HEADERS = {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal',
|
'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kiro (AWS CodeWhisperer) API configuration
|
||||||
|
export const KIRO_QUOTA_URL =
|
||||||
|
'https://codewhisperer.us-east-1.amazonaws.com/getUsageLimits?isEmailRequired=true&origin=AI_EDITOR&resourceType=AGENTIC_REQUEST';
|
||||||
|
|
||||||
|
export const KIRO_REQUEST_HEADERS = {
|
||||||
|
Authorization: 'Bearer $TOKEN$',
|
||||||
|
'x-amz-user-agent': 'aws-sdk-js/1.0.0 KiroIDE-0.6.18-cpamc',
|
||||||
|
'User-Agent': 'aws-sdk-js/1.0.0 ua/2.1 os/windows lang/js md/nodejs#20.16.0 api/codewhispererruntime#1.0.0 m/E KiroIDE-0.6.18-cpamc',
|
||||||
|
'amz-sdk-request': 'attempt=1; max=1',
|
||||||
|
Connection: 'close',
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Normalization and parsing functions for quota data.
|
* Normalization and parsing functions for quota data.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CodexUsagePayload, GeminiCliQuotaPayload } from '@/types';
|
import type { CodexUsagePayload, GeminiCliQuotaPayload, KiroQuotaPayload } from '@/types';
|
||||||
|
|
||||||
export function normalizeAuthIndexValue(value: unknown): string | null {
|
export function normalizeAuthIndexValue(value: unknown): string | null {
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
@@ -151,3 +151,20 @@ export function parseGeminiCliQuotaPayload(payload: unknown): GeminiCliQuotaPayl
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseKiroQuotaPayload(payload: unknown): KiroQuotaPayload | 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 KiroQuotaPayload;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof payload === 'object') {
|
||||||
|
return payload as KiroQuotaPayload;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export function isGeminiCliFile(file: AuthFileItem): boolean {
|
|||||||
return resolveAuthProvider(file) === 'gemini-cli';
|
return resolveAuthProvider(file) === 'gemini-cli';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isKiroFile(file: AuthFileItem): boolean {
|
||||||
|
return resolveAuthProvider(file) === 'kiro';
|
||||||
|
}
|
||||||
|
|
||||||
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
||||||
const raw = file['runtime_only'] ?? file.runtimeOnly;
|
const raw = file['runtime_only'] ?? file.runtimeOnly;
|
||||||
if (typeof raw === 'boolean') return raw;
|
if (typeof raw === 'boolean') return raw;
|
||||||
|
|||||||
Reference in New Issue
Block a user