Merge pull request #11: feat: add Kiro quota display support

新增 Kiro (AWS CodeWhisperer) 额度查看功能:
- 配额页面新增 Kiro 额度区块
- 显示基础额度、赠送额度、合计额度
- 显示订阅类型和重置时间
- 支持单个刷新和批量获取

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
kongkongyo
2026-02-02 21:43:04 +08:00
11 changed files with 460 additions and 11 deletions

View File

@@ -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';

View File

@@ -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,
@@ -47,6 +51,7 @@ import {
isCodexFile, isCodexFile,
isDisabledAuthFile, isDisabledAuthFile,
isGeminiCliFile, isGeminiCliFile,
isKiroFile,
isRuntimeOnlyAuthFile isRuntimeOnlyAuthFile
} from '@/utils/quota'; } from '@/utils/quota';
import type { QuotaRenderHelpers } from './QuotaCard'; import type { QuotaRenderHelpers } from './QuotaCard';
@@ -54,7 +59,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';
@@ -62,9 +67,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;
} }
@@ -590,3 +597,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
};

View File

@@ -384,6 +384,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",
@@ -395,6 +396,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",
@@ -469,6 +471,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.",

View File

@@ -384,6 +384,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": "空文件",
@@ -395,6 +396,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": "空文件",
@@ -469,6 +471,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。",

View File

@@ -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;

View File

@@ -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}

View File

@@ -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: {}
}) })
})); }));

View File

@@ -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;
}

View File

@@ -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',
};

View File

@@ -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;
}

View File

@@ -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;