mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 05:12:03 +08:00
feat(desktop): add plugin resource actions
This commit is contained in:
@@ -2,14 +2,14 @@ import type { JsonValue } from "cursor-byok:plugin";
|
||||
import type { ModelDefinition, ModelSnapshot, ModelSupport } from "cursor-byok:model";
|
||||
import { accountData } from "./resources.ts";
|
||||
|
||||
export const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
||||
export const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
||||
export const ANTIGRAVITY_PROD_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
||||
export const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
||||
export const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
||||
|
||||
export const ANTIGRAVITY_ENDPOINTS = [
|
||||
ANTIGRAVITY_SANDBOX_ENDPOINT,
|
||||
ANTIGRAVITY_DAILY_ENDPOINT,
|
||||
ANTIGRAVITY_PROD_ENDPOINT,
|
||||
ANTIGRAVITY_DAILY_ENDPOINT,
|
||||
ANTIGRAVITY_SANDBOX_ENDPOINT,
|
||||
];
|
||||
|
||||
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"apiVersion": 1,
|
||||
"id": "dev.cursorbyok.plugins.antigravity-auth",
|
||||
"name": "Antigravity",
|
||||
"version": "0.1.7",
|
||||
"name": "Google",
|
||||
"version": "0.1.8",
|
||||
"author": "@masudranaxpert",
|
||||
"minAppVersion": "0.1.0",
|
||||
"icon": "assets/antigravity.svg",
|
||||
|
||||
@@ -357,6 +357,7 @@ async function streamCloudCode(
|
||||
const headers: Record<string, string> = {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
"content-type": "application/json",
|
||||
accept: "text/event-stream",
|
||||
"user-agent": ANTIGRAVITY_USER_AGENT,
|
||||
...ANTIGRAVITY_CLIENT_HEADERS,
|
||||
};
|
||||
@@ -403,6 +404,7 @@ async function streamCloudCode(
|
||||
inputTokens: number | null;
|
||||
outputTokens: number | null;
|
||||
totalTokens: number | null;
|
||||
cacheReadTokens: number | null;
|
||||
} | null = null;
|
||||
|
||||
for await (const line of response.lines) {
|
||||
@@ -428,6 +430,9 @@ async function streamCloudCode(
|
||||
? usage.candidatesTokenCount
|
||||
: null,
|
||||
totalTokens: typeof usage.totalTokenCount === "number" ? usage.totalTokenCount : null,
|
||||
cacheReadTokens: typeof usage.cachedContentTokenCount === "number"
|
||||
? usage.cachedContentTokenCount
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -535,7 +540,7 @@ async function streamCloudCode(
|
||||
inputTokens: finalUsage.inputTokens,
|
||||
outputTokens: finalUsage.outputTokens,
|
||||
totalTokens: finalUsage.totalTokens,
|
||||
cacheReadTokens: null,
|
||||
cacheReadTokens: finalUsage.cacheReadTokens,
|
||||
cacheWriteTokens: null,
|
||||
reasoningTokens: null,
|
||||
},
|
||||
@@ -573,7 +578,7 @@ async function streamCloudCode(
|
||||
inputTokens: finalUsage.inputTokens,
|
||||
outputTokens: finalUsage.outputTokens,
|
||||
totalTokens: finalUsage.totalTokens,
|
||||
cacheReadTokens: null,
|
||||
cacheReadTokens: finalUsage.cacheReadTokens,
|
||||
cacheWriteTokens: null,
|
||||
reasoningTokens: null,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type {
|
||||
JsonValue,
|
||||
NetworkEventStream,
|
||||
NetworkResponse,
|
||||
PluginContext,
|
||||
} from "cursor-byok:plugin";
|
||||
import type { LlmRequest, ModelEvent } from "cursor-byok:provider";
|
||||
import type { ResourceSnapshot } from "cursor-byok:resource";
|
||||
import { antigravityProvider, isQuotaError } from "./provider.ts";
|
||||
import { RESOURCE_TYPE } from "./resources.ts";
|
||||
|
||||
function assert(condition: unknown, message = "assertion failed"): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
const left = JSON.stringify(actual);
|
||||
const right = JSON.stringify(expected);
|
||||
if (left !== right) throw new Error(`expected ${right}, received ${left}`);
|
||||
}
|
||||
|
||||
type RequestInit = { body?: string; headers?: Record<string, string> };
|
||||
type FetchHandler = (url: string, init?: RequestInit) => NetworkResponse;
|
||||
type StreamHandler = (url: string, init?: RequestInit) => NetworkEventStream;
|
||||
|
||||
function context(handlers: { fetch?: FetchHandler; stream?: StreamHandler }): PluginContext {
|
||||
return {
|
||||
network: {
|
||||
fetch: (url, init) => {
|
||||
if (!handlers.fetch) throw new Error("fetch was not expected");
|
||||
return Promise.resolve(handlers.fetch(url, init));
|
||||
},
|
||||
stream: (url, init) => {
|
||||
if (!handlers.stream) throw new Error("stream was not expected");
|
||||
return Promise.resolve(handlers.stream(url, init));
|
||||
},
|
||||
},
|
||||
signal: new AbortController().signal,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(privateData: JsonValue): ResourceSnapshot {
|
||||
return {
|
||||
id: "resource-1",
|
||||
type: RESOURCE_TYPE,
|
||||
key: "antigravity:user-1",
|
||||
privateData,
|
||||
state: { status: "ready" },
|
||||
};
|
||||
}
|
||||
|
||||
async function* sse(lines: string[]): AsyncGenerator<string> {
|
||||
for (const line of lines) yield line;
|
||||
}
|
||||
|
||||
function request(): LlmRequest {
|
||||
return {
|
||||
instructions: "You are an AI coding assistant.",
|
||||
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
|
||||
tools: [],
|
||||
reasoning: { enabled: true, effort: "medium" },
|
||||
latency: "standard",
|
||||
maxOutputTokens: 65536,
|
||||
cacheKey: "conv-1",
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("provider parses usage metadata including cachedContentTokenCount as cacheReadTokens", async () => {
|
||||
let requestBody = "";
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
const events: ModelEvent[] = [];
|
||||
|
||||
const result = await antigravityProvider.invoke(
|
||||
{
|
||||
model: {
|
||||
id: "gemini-3.7-flash-medium",
|
||||
displayName: "Gemini 3.7 Flash Medium",
|
||||
privateData: {},
|
||||
},
|
||||
resource: snapshot({
|
||||
accessToken: "mock-access-token",
|
||||
refreshToken: "mock-refresh-token",
|
||||
expiresAt: Date.now() + 3600_000,
|
||||
projectId: "test-project-123",
|
||||
}),
|
||||
request: request(),
|
||||
},
|
||||
{ emit: (event) => events.push(event) },
|
||||
context({
|
||||
stream: (url, init) => {
|
||||
assert(url.includes("/v1internal:streamGenerateContent?alt=sse"));
|
||||
requestBody = init?.body ?? "";
|
||||
requestHeaders = init?.headers ?? {};
|
||||
return {
|
||||
status: 200,
|
||||
headers: {},
|
||||
lines: sse([
|
||||
'data: {"response":{"candidates":[{"content":{"parts":[{"thought":true,"text":"Thinking..."}]}}]}}',
|
||||
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"Hello world"}]}}]}}',
|
||||
'data: {"response":{"candidates":[{"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":120,"candidatesTokenCount":15,"totalTokenCount":135,"cachedContentTokenCount":80}}}',
|
||||
]),
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
assertEquals(result, { status: "completed" });
|
||||
assertEquals(requestHeaders["authorization"], "Bearer mock-access-token");
|
||||
assertEquals(requestHeaders.accept, "text/event-stream");
|
||||
assert(!("session-id" in requestHeaders), "Codex-only session-id header was sent");
|
||||
assert(!("thread-id" in requestHeaders), "Codex-only thread-id header was sent");
|
||||
assert(!("x-client-request-id" in requestHeaders), "request-id header was reused as cache key");
|
||||
const body = JSON.parse(requestBody) as Record<string, unknown>;
|
||||
assertEquals(body.project, "test-project-123");
|
||||
assertEquals(body.model, "gemini-3.7-flash-medium");
|
||||
assert(!("sessionId" in body), "unsupported sessionId field was sent");
|
||||
assert(!("conversationId" in body), "unsupported conversationId field was sent");
|
||||
|
||||
assertEquals(events, [
|
||||
{ type: "thinking-start" },
|
||||
{ type: "thinking-delta", text: "Thinking..." },
|
||||
{ type: "thinking-end" },
|
||||
{ type: "text-start" },
|
||||
{ type: "text-delta", text: "Hello world" },
|
||||
{ type: "text-end" },
|
||||
{
|
||||
type: "usage",
|
||||
usage: {
|
||||
inputTokens: 120,
|
||||
outputTokens: 15,
|
||||
totalTokens: 135,
|
||||
cacheReadTokens: 80,
|
||||
cacheWriteTokens: null,
|
||||
reasoningTokens: null,
|
||||
},
|
||||
},
|
||||
{ type: "done", reason: "stop" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("isQuotaError identifies rate limits and quota exhaustion", () => {
|
||||
assert(isQuotaError("RESOURCE_EXHAUSTED: quota exceeded"));
|
||||
assert(isQuotaError("Rate limit exceeded for model"));
|
||||
assert(isQuotaError("HTTP 429 Too Many Requests"));
|
||||
assert(!isQuotaError("Invalid authorization header"));
|
||||
});
|
||||
@@ -13,6 +13,8 @@ import { codexProvider, isQuotaError } from "./provider.ts";
|
||||
import {
|
||||
accountIdentity,
|
||||
credentialDraft,
|
||||
consumeResetCardAction,
|
||||
listResetCardsAction,
|
||||
parseCodexUsage,
|
||||
parseCredentialFiles,
|
||||
presentAccount,
|
||||
@@ -134,6 +136,7 @@ Deno.test("usage maps secondary to weekly and primary to five-hour quota", () =>
|
||||
primary_window: { used_percent: 80, reset_at: 1_800_000_000 },
|
||||
secondary_window: { used_percent: 25, reset_at: 1_900_000_000 },
|
||||
},
|
||||
rate_limit_reset_credits: { available_count: 2 },
|
||||
}, 1_700_000_000_000);
|
||||
assertEquals(quota.planLabel, "ChatGPT Plus");
|
||||
assertEquals(quota.weekly?.remainingPercent, 75);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { defineProviderPlugin } from "cursor-byok:plugin";
|
||||
import { codexDeviceOAuth } from "./oauth.ts";
|
||||
import { codexProvider } from "./provider.ts";
|
||||
import { credentialImport, presentAccount, refreshAccount, RESOURCE_TYPE } from "./resources.ts";
|
||||
import {
|
||||
credentialImport,
|
||||
consumeResetCardAction,
|
||||
listResetCardsAction,
|
||||
presentAccount,
|
||||
refreshAccount,
|
||||
RESOURCE_TYPE,
|
||||
} from "./resources.ts";
|
||||
|
||||
export default defineProviderPlugin({
|
||||
providers: [codexProvider],
|
||||
@@ -11,6 +18,7 @@ export default defineProviderPlugin({
|
||||
add: [codexDeviceOAuth],
|
||||
import: credentialImport,
|
||||
present: presentAccount,
|
||||
actions: [listResetCardsAction, consumeResetCardAction],
|
||||
refresh: refreshAccount,
|
||||
}],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { JsonValue, PluginContext } from "cursor-byok:plugin";
|
||||
import type {
|
||||
ResourceAction,
|
||||
ResourceActionCard,
|
||||
ResourceActionResult,
|
||||
ResourceDraft,
|
||||
ResourceImportFile,
|
||||
ResourceImportResult,
|
||||
@@ -14,8 +17,13 @@ import type {
|
||||
export const RESOURCE_TYPE = "chatgpt-account";
|
||||
|
||||
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
||||
const RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
||||
const RESET_CREDITS_CONSUME_URL = `${RESET_CREDITS_URL}/consume`;
|
||||
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
||||
|
||||
export const LIST_RESET_CARDS_ACTION_ID = "list-reset-cards";
|
||||
export const CONSUME_RESET_CARD_ACTION_ID = "consume-reset-card";
|
||||
|
||||
export type QuotaWindow = {
|
||||
usedPercent: number | null;
|
||||
remainingPercent: number | null;
|
||||
@@ -26,6 +34,7 @@ export type AccountQuota = {
|
||||
planLabel: string | null;
|
||||
weekly: QuotaWindow | null;
|
||||
fiveHour: QuotaWindow | null;
|
||||
resetCreditsAvailable: number | null;
|
||||
limitReached: boolean;
|
||||
updatedAtMs: number;
|
||||
};
|
||||
@@ -34,6 +43,7 @@ export type AccountQuota = {
|
||||
export type AccountData = {
|
||||
accessToken: string;
|
||||
refreshToken: string | null;
|
||||
accountId: string | null;
|
||||
displayName: string;
|
||||
quota: AccountQuota | null;
|
||||
};
|
||||
@@ -41,6 +51,7 @@ export type AccountData = {
|
||||
export type CredentialCandidate = {
|
||||
accessToken: string;
|
||||
refreshToken: string | null;
|
||||
accountId?: string | null;
|
||||
displayName: string | null;
|
||||
};
|
||||
|
||||
@@ -102,9 +113,10 @@ function profileEmail(payload: Record<string, unknown> | null): string | null {
|
||||
|
||||
export async function accountIdentity(
|
||||
accessToken: string,
|
||||
accountId?: string | null,
|
||||
): Promise<{ key: string; displayName: string }> {
|
||||
const payload = decodeJwtPayload(accessToken);
|
||||
const identity = chatGptAccountId(accessToken) ??
|
||||
const identity = accountId ?? chatGptAccountId(accessToken) ??
|
||||
claim(payload, "sub") ??
|
||||
claim(payload, "email") ??
|
||||
await tokenFingerprint(accessToken);
|
||||
@@ -117,10 +129,11 @@ export async function accountIdentity(
|
||||
}
|
||||
|
||||
export async function credentialDraft(credential: CredentialCandidate): Promise<ResourceDraft> {
|
||||
const identity = await accountIdentity(credential.accessToken);
|
||||
const identity = await accountIdentity(credential.accessToken, credential.accountId);
|
||||
const data: AccountData = {
|
||||
accessToken: credential.accessToken,
|
||||
refreshToken: credential.refreshToken,
|
||||
accountId: credential.accountId ?? chatGptAccountId(credential.accessToken),
|
||||
displayName: credential.displayName ?? identity.displayName,
|
||||
quota: null,
|
||||
};
|
||||
@@ -134,6 +147,7 @@ export function accountData(resource: ResourceSnapshot): AccountData {
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken: text(data?.refreshToken),
|
||||
accountId: text(data?.accountId) ?? chatGptAccountId(accessToken),
|
||||
displayName: text(data?.displayName) ?? "ChatGPT account",
|
||||
quota: (data?.quota ?? null) as AccountQuota | null,
|
||||
};
|
||||
@@ -145,7 +159,7 @@ export function accountHeaders(data: AccountData): Record<string, string> {
|
||||
originator: "codex_cli_rs",
|
||||
authorization: `Bearer ${data.accessToken}`,
|
||||
};
|
||||
const accountId = chatGptAccountId(data.accessToken);
|
||||
const accountId = data.accountId ?? chatGptAccountId(data.accessToken);
|
||||
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
|
||||
return headers;
|
||||
}
|
||||
@@ -207,10 +221,17 @@ export function parseCodexUsage(body: unknown, nowMs = Date.now()): AccountQuota
|
||||
? null
|
||||
: quotaWindow(primary, nowMs);
|
||||
const explicitLimit = rateLimit.limit_reached ?? rateLimit.limitReached;
|
||||
const resetCredits = object(root.rate_limit_reset_credits ?? root.rateLimitResetCredits);
|
||||
const resetCreditsAvailable = number(
|
||||
resetCredits?.available_count ?? resetCredits?.availableCount,
|
||||
);
|
||||
return {
|
||||
planLabel: planLabel(root.plan_type ?? root.planType),
|
||||
weekly,
|
||||
fiveHour,
|
||||
resetCreditsAvailable: resetCreditsAvailable === null
|
||||
? null
|
||||
: Math.max(0, Math.floor(resetCreditsAvailable)),
|
||||
limitReached: typeof explicitLimit === "boolean" ? explicitLimit : [weekly, fiveHour].some(
|
||||
(window) => window?.remainingPercent !== null && window?.remainingPercent === 0,
|
||||
),
|
||||
@@ -271,6 +292,7 @@ export function quotaExhaustedPatch(
|
||||
remainingPercent: 0,
|
||||
resetAtMs: resetFromError(error, nowMs),
|
||||
},
|
||||
resetCreditsAvailable: data.quota?.resetCreditsAvailable ?? null,
|
||||
limitReached: true,
|
||||
updatedAtMs: nowMs,
|
||||
};
|
||||
@@ -280,6 +302,144 @@ export function quotaExhaustedPatch(
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchResetCredits(
|
||||
data: AccountData,
|
||||
context: PluginContext,
|
||||
): Promise<{ cards: ResourceActionCard[]; availableCount: number }> {
|
||||
const accountId = data.accountId ?? chatGptAccountId(data.accessToken);
|
||||
if (!accountId) throw new Error("ChatGPT account is missing its account ID");
|
||||
const response = await context.network.fetch(RESET_CREDITS_URL, {
|
||||
method: "GET",
|
||||
headers: accountHeaders(data),
|
||||
});
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(
|
||||
`Codex reset card lookup failed (HTTP ${response.status}): ${response.body}`,
|
||||
);
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
body = JSON.parse(response.body);
|
||||
} catch {
|
||||
throw new Error("Codex reset card lookup returned invalid JSON");
|
||||
}
|
||||
const root = object(body) ?? {};
|
||||
const rawCredits = Array.isArray(root.credits) ? root.credits : [];
|
||||
const cards = rawCredits.flatMap((value, index): ResourceActionCard[] => {
|
||||
const credit = object(value);
|
||||
const id = text(credit?.id);
|
||||
if (!id) return [];
|
||||
const resetType = text(credit?.reset_type ?? credit?.resetType);
|
||||
const grantedAt = timestampMs(credit?.granted_at ?? credit?.grantedAt);
|
||||
const expiresAt = timestampMs(credit?.expires_at ?? credit?.expiresAt);
|
||||
return [{
|
||||
id,
|
||||
title: text(credit?.title) ?? resetType ?? `Codex reset card ${index + 1}`,
|
||||
...(text(credit?.status) ? { status: text(credit?.status)! } : {}),
|
||||
...(grantedAt !== null ? { grantedAtMs: grantedAt } : {}),
|
||||
...(expiresAt !== null ? { expiresAtMs: expiresAt } : {}),
|
||||
fields: resetType
|
||||
? [{
|
||||
id: "reset-type",
|
||||
label: { "en-US": "Reset type", "zh-CN": "重置类型" },
|
||||
value: resetType,
|
||||
}]
|
||||
: [],
|
||||
}];
|
||||
});
|
||||
const availableCount = number(root.available_count ?? root.availableCount);
|
||||
return {
|
||||
cards,
|
||||
availableCount: availableCount === null ? cards.filter((card) => card.status === "available").length : Math.max(0, Math.floor(availableCount)),
|
||||
};
|
||||
}
|
||||
|
||||
function timestampMs(value: unknown): number | null {
|
||||
const numeric = number(value);
|
||||
if (numeric !== null) return numeric > 10_000_000_000 ? numeric : numeric * 1000;
|
||||
if (typeof value === "string") {
|
||||
const parsed = Date.parse(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function actionDescription(availableCount: number): ResourceActionResult["description"] {
|
||||
return {
|
||||
"en-US": `${availableCount} reset card${availableCount === 1 ? "" : "s"} available`,
|
||||
"zh-CN": `可用重置卡 ${availableCount} 张`,
|
||||
};
|
||||
}
|
||||
|
||||
async function listResetCards(
|
||||
resource: ResourceSnapshot,
|
||||
_input: JsonValue,
|
||||
context: PluginContext,
|
||||
): Promise<ResourceActionResult> {
|
||||
const result = await fetchResetCredits(accountData(resource), context);
|
||||
return {
|
||||
title: { "en-US": "Codex reset cards", "zh-CN": "Codex 重置卡" },
|
||||
description: actionDescription(result.availableCount),
|
||||
cards: result.cards,
|
||||
};
|
||||
}
|
||||
|
||||
async function consumeResetCard(
|
||||
resource: ResourceSnapshot,
|
||||
input: JsonValue,
|
||||
context: PluginContext,
|
||||
): Promise<ResourceActionResult> {
|
||||
const inputObject = object(input);
|
||||
const creditId = text(inputObject?.creditId ?? inputObject?.cardId);
|
||||
if (!creditId) throw new Error("A reset card ID is required");
|
||||
|
||||
const data = accountData(resource);
|
||||
const available = await fetchResetCredits(data, context);
|
||||
const card = available.cards.find((item) => item.id === creditId && item.status === "available");
|
||||
if (!card) throw new Error("The selected reset card is not available");
|
||||
|
||||
const response = await context.network.fetch(RESET_CREDITS_CONSUME_URL, {
|
||||
method: "POST",
|
||||
headers: { ...accountHeaders(data), "content-type": "application/json" },
|
||||
body: JSON.stringify({ credit_id: creditId, redeem_request_id: crypto.randomUUID() }),
|
||||
});
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(
|
||||
`Codex reset card consumption failed (HTTP ${response.status}): ${response.body}`,
|
||||
);
|
||||
}
|
||||
|
||||
const patch = await refreshAccount(resource, context);
|
||||
const refreshedResource: ResourceSnapshot = {
|
||||
...resource,
|
||||
...(patch.privateData ? { privateData: patch.privateData } : {}),
|
||||
};
|
||||
const refreshed = await fetchResetCredits(accountData(refreshedResource), context);
|
||||
return {
|
||||
title: { "en-US": "Codex reset card used", "zh-CN": "Codex 重置卡已使用" },
|
||||
description: actionDescription(refreshed.availableCount),
|
||||
cards: refreshed.cards,
|
||||
patch,
|
||||
};
|
||||
}
|
||||
|
||||
export const listResetCardsAction: ResourceAction = {
|
||||
id: LIST_RESET_CARDS_ACTION_ID,
|
||||
displayName: { "en-US": "View reset cards", "zh-CN": "查看重置卡" },
|
||||
description: { "en-US": "List available Codex reset cards.", "zh-CN": "查看当前账号的 Codex 重置卡。" },
|
||||
target: "resource",
|
||||
run: listResetCards,
|
||||
};
|
||||
|
||||
export const consumeResetCardAction: ResourceAction = {
|
||||
id: CONSUME_RESET_CARD_ACTION_ID,
|
||||
displayName: { "en-US": "Use reset card", "zh-CN": "使用重置卡" },
|
||||
description: { "en-US": "Redeem one available Codex reset card.", "zh-CN": "消耗一张可用的 Codex 重置卡。" },
|
||||
target: "card",
|
||||
destructive: true,
|
||||
run: consumeResetCard,
|
||||
};
|
||||
|
||||
export function presentAccount(resource: ResourceSnapshot): ResourceView {
|
||||
const data = accountData(resource);
|
||||
const metrics: ResourceMetric[] = [];
|
||||
@@ -303,6 +463,15 @@ export function presentAccount(resource: ResourceSnapshot): ResourceView {
|
||||
...(fiveHour.resetAtMs !== null ? { resetAtMs: fiveHour.resetAtMs } : {}),
|
||||
});
|
||||
}
|
||||
const resetCreditsAvailable = data.quota?.resetCreditsAvailable;
|
||||
if (resetCreditsAvailable !== null && resetCreditsAvailable !== undefined) {
|
||||
metrics.push({
|
||||
id: "reset-credits",
|
||||
label: { "en-US": "Reset cards", "zh-CN": "重置卡" },
|
||||
unit: "count",
|
||||
value: resetCreditsAvailable,
|
||||
});
|
||||
}
|
||||
return {
|
||||
// 旧记录可能存的是账号 ID;展示时优先从 token 现算邮箱。
|
||||
displayName: jwtDisplayName(data.accessToken) ?? data.displayName,
|
||||
@@ -375,12 +544,14 @@ function collectCredentials(value: unknown, output: CredentialCandidate[]): void
|
||||
if (!accessToken) return;
|
||||
const refreshToken = firstText(tokens, ["refresh_token", "refreshToken"]) ??
|
||||
firstText(item, ["refresh_token", "refreshToken"]);
|
||||
const accountId = firstText(tokens, ["account_id", "accountId", "chatgpt_account_id"]) ??
|
||||
firstText(item, ["account_id", "accountId", "chatgpt_account_id"]);
|
||||
const idToken = firstText(tokens, ["id_token", "idToken"]) ??
|
||||
firstText(item, ["id_token", "idToken"]);
|
||||
const displayName = firstText(item, ["email", "display_name", "displayName", "name"]) ??
|
||||
firstText(tokens, ["email", "display_name", "displayName", "name"]) ??
|
||||
jwtDisplayName(idToken);
|
||||
output.push({ accessToken, refreshToken, displayName });
|
||||
output.push({ accessToken, refreshToken, ...(accountId ? { accountId } : {}), displayName });
|
||||
}
|
||||
|
||||
export function parseCredentialFiles(files: ResourceImportFile[]): {
|
||||
|
||||
Reference in New Issue
Block a user