chatccc 0.2.187 → 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 +14 -9
- 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 +4 -3
- package/src/__tests__/config-sample.test.ts +17 -6
- 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/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 +86 -48
- 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/config.ts
CHANGED
|
@@ -88,9 +88,9 @@ export interface CursorConfig {
|
|
|
88
88
|
onDemandMonthlyBudget: number;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export interface CodexConfig {
|
|
92
|
-
/** 是否启用 Codex Agent;缺省时按"有任意字段非空"自动判定(向后兼容) */
|
|
93
|
-
enabled: boolean;
|
|
91
|
+
export interface CodexConfig {
|
|
92
|
+
/** 是否启用 Codex Agent;缺省时按"有任意字段非空"自动判定(向后兼容) */
|
|
93
|
+
enabled: boolean;
|
|
94
94
|
/** 是否作为 /new 未指定工具时使用的默认 Agent */
|
|
95
95
|
defaultAgent: boolean;
|
|
96
96
|
/** Codex CLI 可执行文件绝对路径;留空时退回到 PATH 中的 `codex` */
|
|
@@ -98,13 +98,22 @@ export interface CodexConfig {
|
|
|
98
98
|
model: string;
|
|
99
99
|
/** /model 可切换的单个备选模型;留空则不加入候选列表 */
|
|
100
100
|
alternativeModel: string;
|
|
101
|
-
effort: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
effort: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface CccConfig {
|
|
105
|
+
/** DeepSeek API Key for the ChatCCC self-developed agent. */
|
|
106
|
+
DEEPSEEK_API_KEY: string;
|
|
107
|
+
/** DeepSeek-compatible API Base URL for the ChatCCC self-developed agent. */
|
|
108
|
+
DEEPSEEK_BASE_URL: string;
|
|
109
|
+
/** Model used by the ChatCCC self-developed agent. */
|
|
110
|
+
model: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface FeishuConfig {
|
|
114
|
+
appId: string;
|
|
115
|
+
appSecret: string;
|
|
116
|
+
}
|
|
108
117
|
|
|
109
118
|
export interface PlatformConfig {
|
|
110
119
|
enabled: boolean;
|
|
@@ -149,21 +158,22 @@ export interface AppConfig {
|
|
|
149
158
|
/** 若为 false,AI 生成过程中用户发送消息不会打断,须先点「停止」再发送新消息 */
|
|
150
159
|
allowInterrupt: boolean;
|
|
151
160
|
rawStreamLogs: RawStreamLogsConfig;
|
|
152
|
-
claude: ClaudeConfig;
|
|
153
|
-
cursor: CursorConfig;
|
|
154
|
-
codex: CodexConfig;
|
|
155
|
-
|
|
161
|
+
claude: ClaudeConfig;
|
|
162
|
+
cursor: CursorConfig;
|
|
163
|
+
codex: CodexConfig;
|
|
164
|
+
ccc: CccConfig;
|
|
165
|
+
}
|
|
156
166
|
|
|
157
167
|
export type AgentTool = "claude" | "cursor" | "codex";
|
|
158
168
|
export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
|
|
159
169
|
export type CursorAvatarBatteryMode = "apiPercent" | "onDemandUse";
|
|
160
170
|
|
|
161
171
|
/** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
|
|
162
|
-
export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): string[] {
|
|
163
|
-
const seen = new Set<string>();
|
|
164
|
-
const collect = (v: unknown) => {
|
|
165
|
-
if (typeof v === "string" && v.trim()) seen.add(v.trim());
|
|
166
|
-
};
|
|
172
|
+
export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): string[] {
|
|
173
|
+
const seen = new Set<string>();
|
|
174
|
+
const collect = (v: unknown) => {
|
|
175
|
+
if (typeof v === "string" && v.trim()) seen.add(v.trim());
|
|
176
|
+
};
|
|
167
177
|
|
|
168
178
|
if (tool === "claude") {
|
|
169
179
|
collect(cfg.claude.model);
|
|
@@ -175,12 +185,29 @@ export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): s
|
|
|
175
185
|
collect(cfg.codex.model);
|
|
176
186
|
collect(cfg.codex.alternativeModel);
|
|
177
187
|
}
|
|
178
|
-
|
|
179
|
-
return Array.from(seen).slice(0, 100);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
const
|
|
188
|
+
|
|
189
|
+
return Array.from(seen).slice(0, 100);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
193
|
+
export const CODEX_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"] as const;
|
|
194
|
+
|
|
195
|
+
export function getAllEffortsForTool(tool: AgentTool): string[] {
|
|
196
|
+
if (tool === "claude") return [...CLAUDE_EFFORT_LEVELS];
|
|
197
|
+
if (tool === "codex") return [...CODEX_EFFORT_LEVELS];
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getDefaultEffortForTool(tool: AgentTool, cfg: AppConfig = config): string {
|
|
202
|
+
if (tool === "claude") return cfg.claude.effort;
|
|
203
|
+
if (tool === "codex") return cfg.codex.effort;
|
|
204
|
+
return "";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
|
|
208
|
+
const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
|
|
209
|
+
export const DEFAULT_CCC_DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1";
|
|
210
|
+
export const DEFAULT_CCC_MODEL = "deepseek-v4-pro[1m]";
|
|
184
211
|
|
|
185
212
|
/**
|
|
186
213
|
* 将旧位置(PROJECT_ROOT)的持久化数据一次性迁移到 USER_DATA_DIR。
|
|
@@ -398,8 +425,8 @@ function loadConfig(): AppConfig {
|
|
|
398
425
|
claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
399
426
|
cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
400
427
|
codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
401
|
-
},
|
|
402
|
-
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
|
|
428
|
+
},
|
|
429
|
+
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
|
|
403
430
|
cursor: {
|
|
404
431
|
enabled: false,
|
|
405
432
|
defaultAgent: false,
|
|
@@ -408,9 +435,10 @@ function loadConfig(): AppConfig {
|
|
|
408
435
|
alternativeModel: "",
|
|
409
436
|
avatarBatteryMode: "apiPercent",
|
|
410
437
|
onDemandMonthlyBudget: 1000,
|
|
411
|
-
},
|
|
412
|
-
codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "" },
|
|
413
|
-
|
|
438
|
+
},
|
|
439
|
+
codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "" },
|
|
440
|
+
ccc: { DEEPSEEK_API_KEY: "", DEEPSEEK_BASE_URL: DEFAULT_CCC_DEEPSEEK_BASE_URL, model: DEFAULT_CCC_MODEL },
|
|
441
|
+
};
|
|
414
442
|
|
|
415
443
|
if (!IS_TEST_ENV) {
|
|
416
444
|
migrateLegacyData();
|
|
@@ -460,11 +488,12 @@ function loadConfig(): AppConfig {
|
|
|
460
488
|
alternativeModel?: unknown;
|
|
461
489
|
avatarBatteryMode?: unknown;
|
|
462
490
|
onDemandMonthlyBudget?: unknown;
|
|
463
|
-
};
|
|
464
|
-
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown };
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
491
|
+
};
|
|
492
|
+
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown };
|
|
493
|
+
ccc?: { DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
|
|
494
|
+
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
495
|
+
rawStreamLogs?: unknown;
|
|
496
|
+
};
|
|
468
497
|
try {
|
|
469
498
|
parsed = JSON.parse(raw);
|
|
470
499
|
} catch (err) {
|
|
@@ -474,9 +503,10 @@ function loadConfig(): AppConfig {
|
|
|
474
503
|
|
|
475
504
|
const feishu = parsed.feishu ?? { appId: "", appSecret: "" };
|
|
476
505
|
const claude = parsed.claude ?? {} as Partial<ClaudeConfig>;
|
|
477
|
-
const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
|
|
478
|
-
const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
|
|
479
|
-
const
|
|
506
|
+
const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
|
|
507
|
+
const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
|
|
508
|
+
const cccRaw = (parsed.ccc ?? {}) as NonNullable<typeof parsed.ccc>;
|
|
509
|
+
const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {}) as NonNullable<typeof parsed.chromeDevtools>;
|
|
480
510
|
const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
|
|
481
511
|
? parsed.rawStreamLogs as unknown as Record<string, unknown>
|
|
482
512
|
: {};
|
|
@@ -599,16 +629,24 @@ function loadConfig(): AppConfig {
|
|
|
599
629
|
avatarBatteryMode: normalizeCursorAvatarBatteryMode(cursorRaw.avatarBatteryMode),
|
|
600
630
|
onDemandMonthlyBudget: normalizeCursorOnDemandMonthlyBudget(cursorRaw.onDemandMonthlyBudget),
|
|
601
631
|
},
|
|
602
|
-
codex: {
|
|
603
|
-
enabled: codexEnabled,
|
|
604
|
-
defaultAgent: defaultTool === "codex",
|
|
605
|
-
path: readToolCliPath(codexRaw, { label: "codex", onLegacyField }),
|
|
606
|
-
model: normalizeOptionalConfigField(codexRaw.model, { label: "codex.model" }),
|
|
607
|
-
alternativeModel: normalizeOptionalConfigField(codexRaw.alternativeModel, { label: "codex.alternativeModel" }),
|
|
608
|
-
effort: normalizeOptionalConfigField(codexRaw.effort, { label: "codex.effort" }),
|
|
609
|
-
},
|
|
610
|
-
|
|
611
|
-
}
|
|
632
|
+
codex: {
|
|
633
|
+
enabled: codexEnabled,
|
|
634
|
+
defaultAgent: defaultTool === "codex",
|
|
635
|
+
path: readToolCliPath(codexRaw, { label: "codex", onLegacyField }),
|
|
636
|
+
model: normalizeOptionalConfigField(codexRaw.model, { label: "codex.model" }),
|
|
637
|
+
alternativeModel: normalizeOptionalConfigField(codexRaw.alternativeModel, { label: "codex.alternativeModel" }),
|
|
638
|
+
effort: normalizeOptionalConfigField(codexRaw.effort, { label: "codex.effort" }),
|
|
639
|
+
},
|
|
640
|
+
ccc: {
|
|
641
|
+
DEEPSEEK_API_KEY: normalizeOptionalConfigField(cccRaw.DEEPSEEK_API_KEY, { label: "ccc.DEEPSEEK_API_KEY" }),
|
|
642
|
+
DEEPSEEK_BASE_URL: normalizeOptionalConfigField(cccRaw.DEEPSEEK_BASE_URL, {
|
|
643
|
+
label: "ccc.DEEPSEEK_BASE_URL",
|
|
644
|
+
fallback: DEFAULT_CCC_DEEPSEEK_BASE_URL,
|
|
645
|
+
}),
|
|
646
|
+
model: normalizeOptionalConfigField(cccRaw.model, { label: "ccc.model", fallback: DEFAULT_CCC_MODEL }),
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
612
650
|
|
|
613
651
|
/**
|
|
614
652
|
* 全局可变 config 对象。
|
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: {
|