chatccc 0.2.186 → 0.2.188
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.sample.json +39 -32
- package/package.json +1 -1
- package/src/__tests__/builtin-config.test.ts +33 -0
- package/src/__tests__/chrome-devtools-guard.test.ts +40 -0
- package/src/__tests__/config-reload.test.ts +30 -16
- package/src/__tests__/config-sample.test.ts +41 -19
- package/src/__tests__/feishu-avatar.test.ts +180 -115
- package/src/__tests__/orchestrator.test.ts +165 -17
- package/src/__tests__/session.test.ts +17 -7
- package/src/__tests__/web-ui.test.ts +22 -0
- package/src/adapters/codex-adapter.ts +26 -21
- package/src/builtin/cli.ts +8 -16
- package/src/builtin/index.ts +12 -11
- package/src/cards.ts +48 -7
- package/src/chrome-devtools-guard.ts +78 -2
- package/src/config.ts +158 -108
- package/src/feishu-api.ts +219 -190
- package/src/index.ts +2 -2
- package/src/orchestrator.ts +211 -19
- package/src/platform-adapter.ts +9 -1
- package/src/session.ts +92 -64
- package/src/web-ui.ts +32 -7
package/src/feishu-api.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
config,
|
|
21
21
|
} from "./config.ts";
|
|
22
22
|
import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
|
|
23
|
+
import type { ChatAvatarUsageHints } from "./platform-adapter.ts";
|
|
23
24
|
import { applyPrivacy } from "./privacy.ts";
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
@@ -302,10 +303,10 @@ const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
|
|
|
302
303
|
const AVATAR_BADGE_DIR = resolvePath(AVATAR_DIR, "badges");
|
|
303
304
|
const AVATAR_COMBINATIONS_DIR = resolvePath(AVATAR_DIR, "combinations");
|
|
304
305
|
const AVATAR_KEY_CACHE_FILE = resolvePath(USER_DATA_DIR, "state", "avatar-image-keys.json");
|
|
305
|
-
const CODEX_AUTH_FILE = resolvePath(homedir(), ".codex", "auth.json");
|
|
306
|
-
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
307
|
-
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
308
|
-
const CODEX_RESET_CONSUME_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
|
|
306
|
+
const CODEX_AUTH_FILE = resolvePath(homedir(), ".codex", "auth.json");
|
|
307
|
+
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
308
|
+
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
309
|
+
const CODEX_RESET_CONSUME_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
|
|
309
310
|
const AVATAR_SOURCES: Record<string, string> = {
|
|
310
311
|
new: resolvePath(AVATAR_DIR, "status_new.png"),
|
|
311
312
|
busy: resolvePath(AVATAR_DIR, "status_busy.png"),
|
|
@@ -322,31 +323,31 @@ const AVATAR_BADGE_MARGIN = 10;
|
|
|
322
323
|
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v13";
|
|
323
324
|
const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
|
|
324
325
|
|
|
325
|
-
export interface CodexUsageBalance {
|
|
326
|
-
usedPercent: number;
|
|
327
|
-
remainingPercent: number;
|
|
328
|
-
resetAtEpochSeconds: number | null;
|
|
329
|
-
resetAfterSeconds: number | null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export interface CodexRateLimitResetCredit {
|
|
333
|
-
grantedAt: string | null;
|
|
334
|
-
expiresAt: string;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export interface CodexUsageSummary {
|
|
338
|
-
fiveHour: CodexUsageBalance;
|
|
339
|
-
weekly: CodexUsageBalance | null;
|
|
340
|
-
rateLimitResetCreditsAvailable: number | null;
|
|
341
|
-
rateLimitResetCredits: CodexRateLimitResetCredit[] | null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export type CodexResetConsumeCode = "reset" | "nothing_to_reset" | "no_credit" | "already_redeemed";
|
|
345
|
-
|
|
346
|
-
export interface CodexResetConsumeResult {
|
|
347
|
-
code: CodexResetConsumeCode;
|
|
348
|
-
windowsReset: number;
|
|
349
|
-
}
|
|
326
|
+
export interface CodexUsageBalance {
|
|
327
|
+
usedPercent: number;
|
|
328
|
+
remainingPercent: number;
|
|
329
|
+
resetAtEpochSeconds: number | null;
|
|
330
|
+
resetAfterSeconds: number | null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface CodexRateLimitResetCredit {
|
|
334
|
+
grantedAt: string | null;
|
|
335
|
+
expiresAt: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface CodexUsageSummary {
|
|
339
|
+
fiveHour: CodexUsageBalance;
|
|
340
|
+
weekly: CodexUsageBalance | null;
|
|
341
|
+
rateLimitResetCreditsAvailable: number | null;
|
|
342
|
+
rateLimitResetCredits: CodexRateLimitResetCredit[] | null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export type CodexResetConsumeCode = "reset" | "nothing_to_reset" | "no_credit" | "already_redeemed";
|
|
346
|
+
|
|
347
|
+
export interface CodexResetConsumeResult {
|
|
348
|
+
code: CodexResetConsumeCode;
|
|
349
|
+
windowsReset: number;
|
|
350
|
+
}
|
|
350
351
|
|
|
351
352
|
const avatarKeyCache = new Map<string, string>();
|
|
352
353
|
let avatarKeyCacheLoaded = false;
|
|
@@ -427,167 +428,172 @@ function usageBalanceFromWindow(raw: Record<string, unknown>, fieldName: string)
|
|
|
427
428
|
};
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: string[]): CodexUsageBalance | null {
|
|
431
|
+
function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: string[]): CodexUsageBalance | null {
|
|
431
432
|
for (const key of keys) {
|
|
432
433
|
const raw = rateLimit[key];
|
|
433
434
|
if (!raw || typeof raw !== "object") continue;
|
|
434
435
|
if ((raw as { used_percent?: unknown }).used_percent === undefined) continue;
|
|
435
436
|
return usageBalanceFromWindow(raw as Record<string, unknown>, `rate_limit.${key}`);
|
|
436
437
|
}
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function parseRateLimitResetCredits(data: Record<string, unknown>): number | null {
|
|
441
|
-
const raw = data.rate_limit_reset_credits;
|
|
442
|
-
if (!raw || typeof raw !== "object") return null;
|
|
443
|
-
const availableCount = Number((raw as { available_count?: unknown }).available_count);
|
|
444
|
-
if (!Number.isFinite(availableCount)) return null;
|
|
445
|
-
return Math.max(0, Math.trunc(availableCount));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
interface CodexAuth {
|
|
449
|
-
accessToken: string;
|
|
450
|
-
accountId?: string;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function getCodexAuth(): Promise<CodexAuth | null> {
|
|
454
|
-
try {
|
|
455
|
-
const raw = await readFile(CODEX_AUTH_FILE, "utf-8");
|
|
456
|
-
const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown; account_id?: unknown } };
|
|
457
|
-
const token = parsed.tokens?.access_token;
|
|
458
|
-
if (typeof token !== "string" || !token.trim()) return null;
|
|
459
|
-
const accountId = parsed.tokens?.account_id;
|
|
460
|
-
return {
|
|
461
|
-
accessToken: token,
|
|
462
|
-
accountId: typeof accountId === "string" && accountId.trim() ? accountId : undefined,
|
|
463
|
-
};
|
|
464
|
-
} catch {
|
|
465
|
-
return null;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
async function getCodexAccessToken(): Promise<string | null> {
|
|
470
|
-
return (await getCodexAuth())?.accessToken ?? null;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function codexAuthHeaders(auth: CodexAuth): Record<string, string> {
|
|
474
|
-
const headers: Record<string, string> = {
|
|
475
|
-
Authorization: `Bearer ${auth.accessToken}`,
|
|
476
|
-
"OpenAI-Beta": "codex-1",
|
|
477
|
-
originator: "Codex Desktop",
|
|
478
|
-
};
|
|
479
|
-
if (auth.accountId) headers["ChatGPT-Account-ID"] = auth.accountId;
|
|
480
|
-
return headers;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function parseCodexResetCreditDetails(data: Record<string, unknown>): {
|
|
484
|
-
availableCount: number | null;
|
|
485
|
-
availableCredits: CodexRateLimitResetCredit[];
|
|
486
|
-
} {
|
|
487
|
-
const availableCount = Number(data.available_count);
|
|
488
|
-
const credits = Array.isArray(data.credits) ? data.credits : [];
|
|
489
|
-
return {
|
|
490
|
-
availableCount: Number.isFinite(availableCount) ? Math.max(0, Math.trunc(availableCount)) : null,
|
|
491
|
-
availableCredits: credits.flatMap((raw) => {
|
|
492
|
-
if (!raw || typeof raw !== "object") return [];
|
|
493
|
-
const credit = raw as { status?: unknown; granted_at?: unknown; expires_at?: unknown };
|
|
494
|
-
if (credit.status !== "available") return [];
|
|
495
|
-
if (typeof credit.expires_at !== "string" || !credit.expires_at.trim()) return [];
|
|
496
|
-
return [{
|
|
497
|
-
grantedAt: typeof credit.granted_at === "string" && credit.granted_at.trim() ? credit.granted_at : null,
|
|
498
|
-
expiresAt: credit.expires_at,
|
|
499
|
-
}];
|
|
500
|
-
}),
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
async function fetchCodexRateLimitResetCredits(auth: CodexAuth): Promise<{
|
|
505
|
-
availableCount: number | null;
|
|
506
|
-
availableCredits: CodexRateLimitResetCredit[];
|
|
507
|
-
} | null> {
|
|
508
|
-
try {
|
|
509
|
-
const resp = await fetch(CODEX_RESET_CREDITS_URL, {
|
|
510
|
-
headers: codexAuthHeaders(auth),
|
|
511
|
-
});
|
|
512
|
-
const text = await resp.text();
|
|
513
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
514
|
-
return parseCodexResetCreditDetails(JSON.parse(text) as Record<string, unknown>);
|
|
515
|
-
} catch (err) {
|
|
516
|
-
console.warn(`[Codex] reset credits lookup failed: ${(err as Error).message}`);
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
|
|
522
|
-
const auth = await getCodexAuth();
|
|
523
|
-
if (!auth) throw new Error("missing ~/.codex/auth.json access token");
|
|
524
|
-
|
|
525
|
-
const resetCreditsPromise = fetchCodexRateLimitResetCredits(auth);
|
|
526
|
-
|
|
527
|
-
const resp = await fetch(CODEX_USAGE_URL, {
|
|
528
|
-
headers: { Authorization: `Bearer ${auth.accessToken}` },
|
|
529
|
-
});
|
|
530
|
-
const text = await resp.text();
|
|
531
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
532
|
-
|
|
533
|
-
const data = JSON.parse(text) as {
|
|
534
|
-
rate_limit?: Record<string, unknown>;
|
|
535
|
-
rate_limit_reset_credits?: unknown;
|
|
536
|
-
};
|
|
537
|
-
const rateLimit = data.rate_limit;
|
|
538
|
-
if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
|
|
539
|
-
|
|
540
|
-
const fiveHour = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
|
|
541
|
-
if (!fiveHour) throw new Error("missing rate_limit.primary_window.used_percent");
|
|
542
|
-
const resetCredits = await resetCreditsPromise;
|
|
543
|
-
|
|
544
|
-
return {
|
|
545
|
-
fiveHour,
|
|
546
|
-
weekly: parseOptionalUsageWindow(rateLimit, [
|
|
547
|
-
"secondary_window",
|
|
548
|
-
"weekly_window",
|
|
549
|
-
"week_window",
|
|
550
|
-
"long_window",
|
|
551
|
-
]),
|
|
552
|
-
rateLimitResetCreditsAvailable: resetCredits?.availableCount ?? parseRateLimitResetCredits(data),
|
|
553
|
-
rateLimitResetCredits: resetCredits?.availableCredits ?? null,
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
export async function consumeCodexRateLimitResetCredit(redeemRequestId: string): Promise<CodexResetConsumeResult> {
|
|
558
|
-
if (!redeemRequestId.trim()) throw new Error("missing redeem_request_id");
|
|
559
|
-
const token = await getCodexAccessToken();
|
|
560
|
-
if (!token) throw new Error("missing ~/.codex/auth.json access token");
|
|
561
|
-
|
|
562
|
-
const resp = await fetch(CODEX_RESET_CONSUME_URL, {
|
|
563
|
-
method: "POST",
|
|
564
|
-
headers: {
|
|
565
|
-
Authorization: `Bearer ${token}`,
|
|
566
|
-
"Content-Type": "application/json",
|
|
567
|
-
},
|
|
568
|
-
body: JSON.stringify({ redeem_request_id: redeemRequestId }),
|
|
569
|
-
});
|
|
570
|
-
const text = await resp.text();
|
|
571
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
572
|
-
|
|
573
|
-
const data = JSON.parse(text) as { code?: unknown; windows_reset?: unknown };
|
|
574
|
-
const code = data.code;
|
|
575
|
-
if (
|
|
576
|
-
code !== "reset"
|
|
577
|
-
&& code !== "nothing_to_reset"
|
|
578
|
-
&& code !== "no_credit"
|
|
579
|
-
&& code !== "already_redeemed"
|
|
580
|
-
) {
|
|
581
|
-
throw new Error("missing or unknown reset result code");
|
|
582
|
-
}
|
|
583
|
-
const windowsReset = Number(data.windows_reset);
|
|
584
|
-
return {
|
|
585
|
-
code,
|
|
586
|
-
windowsReset: Number.isFinite(windowsReset) ? Math.max(0, Math.trunc(windowsReset)) : 0,
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async function
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function parseRateLimitResetCredits(data: Record<string, unknown>): number | null {
|
|
442
|
+
const raw = data.rate_limit_reset_credits;
|
|
443
|
+
if (!raw || typeof raw !== "object") return null;
|
|
444
|
+
const availableCount = Number((raw as { available_count?: unknown }).available_count);
|
|
445
|
+
if (!Number.isFinite(availableCount)) return null;
|
|
446
|
+
return Math.max(0, Math.trunc(availableCount));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
interface CodexAuth {
|
|
450
|
+
accessToken: string;
|
|
451
|
+
accountId?: string;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function getCodexAuth(): Promise<CodexAuth | null> {
|
|
455
|
+
try {
|
|
456
|
+
const raw = await readFile(CODEX_AUTH_FILE, "utf-8");
|
|
457
|
+
const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown; account_id?: unknown } };
|
|
458
|
+
const token = parsed.tokens?.access_token;
|
|
459
|
+
if (typeof token !== "string" || !token.trim()) return null;
|
|
460
|
+
const accountId = parsed.tokens?.account_id;
|
|
461
|
+
return {
|
|
462
|
+
accessToken: token,
|
|
463
|
+
accountId: typeof accountId === "string" && accountId.trim() ? accountId : undefined,
|
|
464
|
+
};
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function getCodexAccessToken(): Promise<string | null> {
|
|
471
|
+
return (await getCodexAuth())?.accessToken ?? null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function codexAuthHeaders(auth: CodexAuth): Record<string, string> {
|
|
475
|
+
const headers: Record<string, string> = {
|
|
476
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
477
|
+
"OpenAI-Beta": "codex-1",
|
|
478
|
+
originator: "Codex Desktop",
|
|
479
|
+
};
|
|
480
|
+
if (auth.accountId) headers["ChatGPT-Account-ID"] = auth.accountId;
|
|
481
|
+
return headers;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function parseCodexResetCreditDetails(data: Record<string, unknown>): {
|
|
485
|
+
availableCount: number | null;
|
|
486
|
+
availableCredits: CodexRateLimitResetCredit[];
|
|
487
|
+
} {
|
|
488
|
+
const availableCount = Number(data.available_count);
|
|
489
|
+
const credits = Array.isArray(data.credits) ? data.credits : [];
|
|
490
|
+
return {
|
|
491
|
+
availableCount: Number.isFinite(availableCount) ? Math.max(0, Math.trunc(availableCount)) : null,
|
|
492
|
+
availableCredits: credits.flatMap((raw) => {
|
|
493
|
+
if (!raw || typeof raw !== "object") return [];
|
|
494
|
+
const credit = raw as { status?: unknown; granted_at?: unknown; expires_at?: unknown };
|
|
495
|
+
if (credit.status !== "available") return [];
|
|
496
|
+
if (typeof credit.expires_at !== "string" || !credit.expires_at.trim()) return [];
|
|
497
|
+
return [{
|
|
498
|
+
grantedAt: typeof credit.granted_at === "string" && credit.granted_at.trim() ? credit.granted_at : null,
|
|
499
|
+
expiresAt: credit.expires_at,
|
|
500
|
+
}];
|
|
501
|
+
}),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function fetchCodexRateLimitResetCredits(auth: CodexAuth): Promise<{
|
|
506
|
+
availableCount: number | null;
|
|
507
|
+
availableCredits: CodexRateLimitResetCredit[];
|
|
508
|
+
} | null> {
|
|
509
|
+
try {
|
|
510
|
+
const resp = await fetch(CODEX_RESET_CREDITS_URL, {
|
|
511
|
+
headers: codexAuthHeaders(auth),
|
|
512
|
+
});
|
|
513
|
+
const text = await resp.text();
|
|
514
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
515
|
+
return parseCodexResetCreditDetails(JSON.parse(text) as Record<string, unknown>);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
console.warn(`[Codex] reset credits lookup failed: ${(err as Error).message}`);
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
|
|
523
|
+
const auth = await getCodexAuth();
|
|
524
|
+
if (!auth) throw new Error("missing ~/.codex/auth.json access token");
|
|
525
|
+
|
|
526
|
+
const resetCreditsPromise = fetchCodexRateLimitResetCredits(auth);
|
|
527
|
+
|
|
528
|
+
const resp = await fetch(CODEX_USAGE_URL, {
|
|
529
|
+
headers: { Authorization: `Bearer ${auth.accessToken}` },
|
|
530
|
+
});
|
|
531
|
+
const text = await resp.text();
|
|
532
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
533
|
+
|
|
534
|
+
const data = JSON.parse(text) as {
|
|
535
|
+
rate_limit?: Record<string, unknown>;
|
|
536
|
+
rate_limit_reset_credits?: unknown;
|
|
537
|
+
};
|
|
538
|
+
const rateLimit = data.rate_limit;
|
|
539
|
+
if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
|
|
540
|
+
|
|
541
|
+
const fiveHour = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
|
|
542
|
+
if (!fiveHour) throw new Error("missing rate_limit.primary_window.used_percent");
|
|
543
|
+
const resetCredits = await resetCreditsPromise;
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
fiveHour,
|
|
547
|
+
weekly: parseOptionalUsageWindow(rateLimit, [
|
|
548
|
+
"secondary_window",
|
|
549
|
+
"weekly_window",
|
|
550
|
+
"week_window",
|
|
551
|
+
"long_window",
|
|
552
|
+
]),
|
|
553
|
+
rateLimitResetCreditsAvailable: resetCredits?.availableCount ?? parseRateLimitResetCredits(data),
|
|
554
|
+
rateLimitResetCredits: resetCredits?.availableCredits ?? null,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function consumeCodexRateLimitResetCredit(redeemRequestId: string): Promise<CodexResetConsumeResult> {
|
|
559
|
+
if (!redeemRequestId.trim()) throw new Error("missing redeem_request_id");
|
|
560
|
+
const token = await getCodexAccessToken();
|
|
561
|
+
if (!token) throw new Error("missing ~/.codex/auth.json access token");
|
|
562
|
+
|
|
563
|
+
const resp = await fetch(CODEX_RESET_CONSUME_URL, {
|
|
564
|
+
method: "POST",
|
|
565
|
+
headers: {
|
|
566
|
+
Authorization: `Bearer ${token}`,
|
|
567
|
+
"Content-Type": "application/json",
|
|
568
|
+
},
|
|
569
|
+
body: JSON.stringify({ redeem_request_id: redeemRequestId }),
|
|
570
|
+
});
|
|
571
|
+
const text = await resp.text();
|
|
572
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
573
|
+
|
|
574
|
+
const data = JSON.parse(text) as { code?: unknown; windows_reset?: unknown };
|
|
575
|
+
const code = data.code;
|
|
576
|
+
if (
|
|
577
|
+
code !== "reset"
|
|
578
|
+
&& code !== "nothing_to_reset"
|
|
579
|
+
&& code !== "no_credit"
|
|
580
|
+
&& code !== "already_redeemed"
|
|
581
|
+
) {
|
|
582
|
+
throw new Error("missing or unknown reset result code");
|
|
583
|
+
}
|
|
584
|
+
const windowsReset = Number(data.windows_reset);
|
|
585
|
+
return {
|
|
586
|
+
code,
|
|
587
|
+
windowsReset: Number.isFinite(windowsReset) ? Math.max(0, Math.trunc(windowsReset)) : 0,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function resolveCodexAvatarUsage(usageHint?: CodexUsageSummary | null): Promise<CodexUsageSummary | null> {
|
|
592
|
+
if (usageHint !== undefined) {
|
|
593
|
+
if (!usageHint?.weekly) return null;
|
|
594
|
+
return usageHint;
|
|
595
|
+
}
|
|
596
|
+
|
|
591
597
|
try {
|
|
592
598
|
const summary = await getCodexUsageSummary();
|
|
593
599
|
if (!summary.weekly) throw new Error("missing weekly usage window");
|
|
@@ -611,7 +617,17 @@ function cursorAvatarBatteryPercentFromUsage(summary: CursorUsageSummary): numbe
|
|
|
611
617
|
return clampPercent(100 - apiPercentUsed);
|
|
612
618
|
}
|
|
613
619
|
|
|
614
|
-
async function
|
|
620
|
+
async function resolveCursorAvatarBatteryPercent(usageHint?: CursorUsageSummary | null): Promise<number | null> {
|
|
621
|
+
if (usageHint !== undefined) {
|
|
622
|
+
if (!usageHint) return null;
|
|
623
|
+
try {
|
|
624
|
+
return cursorAvatarBatteryPercentFromUsage(usageHint);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
console.warn(`[${ts()}] [AVATAR] Cursor usage unavailable, using plain avatar: ${(err as Error).message}`);
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
615
631
|
try {
|
|
616
632
|
return cursorAvatarBatteryPercentFromUsage(await getCursorUsageSummary());
|
|
617
633
|
} catch (err) {
|
|
@@ -814,12 +830,19 @@ async function uploadImage(
|
|
|
814
830
|
return data.data!.image_key!;
|
|
815
831
|
}
|
|
816
832
|
|
|
817
|
-
async function getOrUploadAvatarKey(
|
|
833
|
+
async function getOrUploadAvatarKey(
|
|
834
|
+
token: string,
|
|
835
|
+
tool: string,
|
|
836
|
+
status: string,
|
|
837
|
+
usageHints: ChatAvatarUsageHints = {},
|
|
838
|
+
): Promise<string> {
|
|
818
839
|
await loadAvatarKeyCache();
|
|
819
840
|
const normalizedTool = normalizeAvatarTool(tool);
|
|
820
841
|
const normalizedStatus = normalizeAvatarStatus(status);
|
|
821
|
-
const codexUsage = normalizedTool === "codex" ? await
|
|
822
|
-
const cursorBatteryPercent = normalizedTool === "cursor"
|
|
842
|
+
const codexUsage = normalizedTool === "codex" ? await resolveCodexAvatarUsage(usageHints.codexUsage) : null;
|
|
843
|
+
const cursorBatteryPercent = normalizedTool === "cursor"
|
|
844
|
+
? await resolveCursorAvatarBatteryPercent(usageHints.cursorUsage)
|
|
845
|
+
: null;
|
|
823
846
|
const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent);
|
|
824
847
|
const cached = avatarKeyCache.get(keyName);
|
|
825
848
|
if (cached) return cached;
|
|
@@ -832,9 +855,15 @@ async function getOrUploadAvatarKey(token: string, tool: string, status: string)
|
|
|
832
855
|
return key;
|
|
833
856
|
}
|
|
834
857
|
|
|
835
|
-
export async function setChatAvatar(
|
|
858
|
+
export async function setChatAvatar(
|
|
859
|
+
token: string,
|
|
860
|
+
chatId: string,
|
|
861
|
+
tool: string,
|
|
862
|
+
status: string,
|
|
863
|
+
usageHints: ChatAvatarUsageHints = {},
|
|
864
|
+
): Promise<void> {
|
|
836
865
|
try {
|
|
837
|
-
const avatarKey = await getOrUploadAvatarKey(token, tool, status);
|
|
866
|
+
const avatarKey = await getOrUploadAvatarKey(token, tool, status, usageHints);
|
|
838
867
|
const resp = await fetch(`${BASE_URL}/im/v1/chats/${chatId}`, {
|
|
839
868
|
method: "PUT",
|
|
840
869
|
headers: {
|
package/src/index.ts
CHANGED
|
@@ -142,8 +142,8 @@ function createFeishuAdapter(): PlatformAdapter {
|
|
|
142
142
|
async disbandChat(chatId) {
|
|
143
143
|
return disbandChat(await auth(), chatId);
|
|
144
144
|
},
|
|
145
|
-
async setChatAvatar(chatId, tool, status) {
|
|
146
|
-
return setChatAvatar(await auth(), chatId, tool, status);
|
|
145
|
+
async setChatAvatar(chatId, tool, status, usageHints) {
|
|
146
|
+
return setChatAvatar(await auth(), chatId, tool, status, usageHints);
|
|
147
147
|
},
|
|
148
148
|
|
|
149
149
|
extractSessionInfo(description) {
|