chatccc 0.2.177 → 0.2.179
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/README.md +4 -4
- package/config.sample.json +3 -1
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +1 -1
- package/src/__tests__/config-reload.test.ts +29 -22
- package/src/__tests__/feishu-avatar.test.ts +219 -129
- package/src/__tests__/orchestrator.test.ts +163 -93
- package/src/cards.ts +6 -6
- package/src/config.ts +31 -2
- package/src/cursor-usage.ts +128 -0
- package/src/feishu-api.ts +71 -14
- package/src/index.ts +1 -1
- package/src/orchestrator.ts +217 -131
- package/src/web-ui.ts +95 -11
package/src/feishu-api.ts
CHANGED
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
ts,
|
|
18
18
|
resolveDefaultAgentTool,
|
|
19
19
|
toolDisplayName,
|
|
20
|
+
config,
|
|
20
21
|
} from "./config.ts";
|
|
22
|
+
import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
|
|
21
23
|
import { applyPrivacy } from "./privacy.ts";
|
|
22
24
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
@@ -315,7 +317,8 @@ const AVATAR_BADGES: Record<string, string> = {
|
|
|
315
317
|
const AVATAR_SIZE = 256;
|
|
316
318
|
const AVATAR_BADGE_SIZE = 92;
|
|
317
319
|
const AVATAR_BADGE_MARGIN = 10;
|
|
318
|
-
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-
|
|
320
|
+
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v13";
|
|
321
|
+
const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
|
|
319
322
|
|
|
320
323
|
export interface CodexUsageBalance {
|
|
321
324
|
usedPercent: number;
|
|
@@ -344,7 +347,12 @@ function avatarCombinationPath(tool: string, status: string): string {
|
|
|
344
347
|
return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
function avatarCacheKey(
|
|
350
|
+
function avatarCacheKey(
|
|
351
|
+
tool: string,
|
|
352
|
+
status: string,
|
|
353
|
+
codexUsage: CodexUsageSummary | null = null,
|
|
354
|
+
cursorBatteryPercent: number | null = null,
|
|
355
|
+
): string {
|
|
348
356
|
const normalizedTool = normalizeAvatarTool(tool);
|
|
349
357
|
const normalizedStatus = normalizeAvatarStatus(status);
|
|
350
358
|
if (normalizedTool === "codex") {
|
|
@@ -352,6 +360,11 @@ function avatarCacheKey(tool: string, status: string, codexUsage: CodexUsageSumm
|
|
|
352
360
|
? `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:week-battery:${codexUsage.weekly?.remainingPercent}:5h-ring:${codexUsage.fiveHour.remainingPercent}`
|
|
353
361
|
: `${normalizedTool}:${normalizedStatus}:plain`;
|
|
354
362
|
}
|
|
363
|
+
if (normalizedTool === "cursor") {
|
|
364
|
+
return cursorBatteryPercent !== null
|
|
365
|
+
? `${normalizedTool}:${normalizedStatus}:${CURSOR_AVATAR_USAGE_STYLE_VERSION}:battery:${cursorBatteryPercent}`
|
|
366
|
+
: `${normalizedTool}:${normalizedStatus}:plain`;
|
|
367
|
+
}
|
|
355
368
|
return `${normalizedTool}:${normalizedStatus}`;
|
|
356
369
|
}
|
|
357
370
|
|
|
@@ -460,6 +473,28 @@ async function fetchCodexAvatarUsage(): Promise<CodexUsageSummary | null> {
|
|
|
460
473
|
}
|
|
461
474
|
}
|
|
462
475
|
|
|
476
|
+
function cursorAvatarBatteryPercentFromUsage(summary: CursorUsageSummary): number {
|
|
477
|
+
if (config.cursor.avatarBatteryMode === "onDemandUse") {
|
|
478
|
+
const usedCents = Number(summary.spendLimitUsage?.individualUsed);
|
|
479
|
+
if (!Number.isFinite(usedCents)) throw new Error("missing spendLimitUsage.individualUsed");
|
|
480
|
+
const budget = config.cursor.onDemandMonthlyBudget > 0 ? config.cursor.onDemandMonthlyBudget : 1000;
|
|
481
|
+
return clampPercent(100 - ((usedCents / 100) / budget) * 100);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const apiPercentUsed = Number(summary.planUsage?.apiPercentUsed);
|
|
485
|
+
if (!Number.isFinite(apiPercentUsed)) throw new Error("missing planUsage.apiPercentUsed");
|
|
486
|
+
return clampPercent(100 - apiPercentUsed);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function fetchCursorAvatarBatteryPercent(): Promise<number | null> {
|
|
490
|
+
try {
|
|
491
|
+
return cursorAvatarBatteryPercentFromUsage(await getCursorUsageSummary());
|
|
492
|
+
} catch (err) {
|
|
493
|
+
console.warn(`[${ts()}] [AVATAR] Cursor usage unavailable, using plain avatar: ${(err as Error).message}`);
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
463
498
|
function codexUsagePalette(remainingPercent: number): { start: string; end: string; glow: string } {
|
|
464
499
|
if (remainingPercent <= 25) return { start: "#ef4444", end: "#fb923c", glow: "#fed7aa" };
|
|
465
500
|
if (remainingPercent <= 60) return { start: "#eab308", end: "#facc15", glow: "#fef3c7" };
|
|
@@ -520,7 +555,7 @@ function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
|
|
|
520
555
|
const cx = 128;
|
|
521
556
|
const cy = 128;
|
|
522
557
|
const r = 118;
|
|
523
|
-
const strokeWidth =
|
|
558
|
+
const strokeWidth = 16;
|
|
524
559
|
const used = clampPercent(100 - remaining);
|
|
525
560
|
const polar = (angleDegrees: number) => {
|
|
526
561
|
const angle = (angleDegrees * Math.PI) / 180;
|
|
@@ -547,11 +582,12 @@ function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
|
|
|
547
582
|
<stop offset="0" stop-color="${palette.start}"/>
|
|
548
583
|
<stop offset="1" stop-color="${palette.end}"/>
|
|
549
584
|
</linearGradient>
|
|
550
|
-
<filter id="ringShadow" x="-
|
|
551
|
-
<feDropShadow dx="0" dy="
|
|
585
|
+
<filter id="ringShadow" x="-22%" y="-22%" width="144%" height="144%">
|
|
586
|
+
<feDropShadow dx="0" dy="0" stdDeviation="5.2" flood-color="#0f172a" flood-opacity="0.78"/>
|
|
587
|
+
<feDropShadow dx="0" dy="3" stdDeviation="2.8" flood-color="#0f172a" flood-opacity="0.45"/>
|
|
552
588
|
</filter>
|
|
553
589
|
</defs>
|
|
554
|
-
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#
|
|
590
|
+
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#94a3b8" stroke-width="${strokeWidth}"/>
|
|
555
591
|
${progressPath}
|
|
556
592
|
</svg>`);
|
|
557
593
|
}
|
|
@@ -572,20 +608,32 @@ async function buildAgentBadgeOverlay(tool: string): Promise<sharp.OverlayOption
|
|
|
572
608
|
};
|
|
573
609
|
}
|
|
574
610
|
|
|
575
|
-
async function renderAvatar(
|
|
611
|
+
async function renderAvatar(
|
|
612
|
+
tool: string,
|
|
613
|
+
status: string,
|
|
614
|
+
codexUsage: CodexUsageSummary | null = null,
|
|
615
|
+
cursorBatteryPercent: number | null = null,
|
|
616
|
+
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
|
576
617
|
const normalizedTool = normalizeAvatarTool(tool);
|
|
577
618
|
const normalizedStatus = normalizeAvatarStatus(status);
|
|
578
619
|
const composites: sharp.OverlayOptions[] = [];
|
|
579
620
|
|
|
580
|
-
const
|
|
581
|
-
const
|
|
621
|
+
const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly : null;
|
|
622
|
+
const useDynamicCodexAvatar = codexUsage && codexWeeklyUsage;
|
|
623
|
+
const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
|
|
624
|
+
const basePath = useDynamicCodexAvatar || useDynamicCursorAvatar
|
|
582
625
|
? AVATAR_SOURCES[normalizedStatus]
|
|
583
626
|
: avatarCombinationPath(normalizedTool, normalizedStatus);
|
|
584
627
|
|
|
585
628
|
if (useDynamicCodexAvatar) {
|
|
586
629
|
composites.push(
|
|
587
630
|
{ input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 },
|
|
588
|
-
{ input: buildCodexUsageBatterySvg(
|
|
631
|
+
{ input: buildCodexUsageBatterySvg(codexWeeklyUsage.remainingPercent), left: 0, top: 0 },
|
|
632
|
+
await buildAgentBadgeOverlay(normalizedTool),
|
|
633
|
+
);
|
|
634
|
+
} else if (useDynamicCursorAvatar) {
|
|
635
|
+
composites.push(
|
|
636
|
+
{ input: buildCodexUsageBatterySvg(cursorBatteryPercent), left: 0, top: 0 },
|
|
589
637
|
await buildAgentBadgeOverlay(normalizedTool),
|
|
590
638
|
);
|
|
591
639
|
}
|
|
@@ -607,12 +655,20 @@ async function renderAvatar(tool: string, status: string, codexUsage: CodexUsage
|
|
|
607
655
|
contentType: "image/jpeg",
|
|
608
656
|
filename: codexUsage?.weekly
|
|
609
657
|
? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
|
|
658
|
+
: cursorBatteryPercent !== null
|
|
659
|
+
? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
|
|
610
660
|
: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
|
|
611
661
|
};
|
|
612
662
|
}
|
|
613
663
|
|
|
614
|
-
async function uploadImage(
|
|
615
|
-
|
|
664
|
+
async function uploadImage(
|
|
665
|
+
token: string,
|
|
666
|
+
tool: string,
|
|
667
|
+
status: string,
|
|
668
|
+
codexUsage: CodexUsageSummary | null = null,
|
|
669
|
+
cursorBatteryPercent: number | null = null,
|
|
670
|
+
): Promise<string> {
|
|
671
|
+
const image = await renderAvatar(tool, status, codexUsage, cursorBatteryPercent);
|
|
616
672
|
const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
|
|
617
673
|
const form = new FormData();
|
|
618
674
|
form.append("image_type", "avatar");
|
|
@@ -638,10 +694,11 @@ async function getOrUploadAvatarKey(token: string, tool: string, status: string)
|
|
|
638
694
|
const normalizedTool = normalizeAvatarTool(tool);
|
|
639
695
|
const normalizedStatus = normalizeAvatarStatus(status);
|
|
640
696
|
const codexUsage = normalizedTool === "codex" ? await fetchCodexAvatarUsage() : null;
|
|
641
|
-
const
|
|
697
|
+
const cursorBatteryPercent = normalizedTool === "cursor" ? await fetchCursorAvatarBatteryPercent() : null;
|
|
698
|
+
const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent);
|
|
642
699
|
const cached = avatarKeyCache.get(keyName);
|
|
643
700
|
if (cached) return cached;
|
|
644
|
-
const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage);
|
|
701
|
+
const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent);
|
|
645
702
|
avatarKeyCache.set(keyName, key);
|
|
646
703
|
await persistAvatarKeyCache().catch((err) => {
|
|
647
704
|
console.error(`[${ts()}] [AVATAR] persist cache FAIL: ${(err as Error).message}`);
|
package/src/index.ts
CHANGED
|
@@ -213,7 +213,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
213
213
|
}
|
|
214
214
|
if (!cmd) return null;
|
|
215
215
|
|
|
216
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart",
|
|
216
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", update: "/update", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
217
217
|
let text = CMD_MAP[cmd] ?? "";
|
|
218
218
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
219
219
|
const path = (action.value as Record<string, string>).path;
|
package/src/orchestrator.ts
CHANGED
|
@@ -78,10 +78,11 @@ import {
|
|
|
78
78
|
enqueueMessage,
|
|
79
79
|
cancelQueuedMessage,
|
|
80
80
|
} from "./session-chat-binding.ts";
|
|
81
|
-
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
import type {
|
|
81
|
+
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
82
|
+
import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
|
|
83
|
+
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
84
|
+
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
85
|
+
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
85
86
|
|
|
86
87
|
// ---------------------------------------------------------------------------
|
|
87
88
|
// 辅助函数
|
|
@@ -97,7 +98,7 @@ export function sessionChatName(left: string, cwd: string): string {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
|
|
100
|
-
function findModelMatch(input: string, models: string[]): string | null {
|
|
101
|
+
function findModelMatch(input: string, models: string[]): string | null {
|
|
101
102
|
if (models.length === 0) return null;
|
|
102
103
|
const inputLower = input.toLowerCase();
|
|
103
104
|
// 1) 精确匹配(忽略大小写)
|
|
@@ -109,72 +110,166 @@ function findModelMatch(input: string, models: string[]): string | null {
|
|
|
109
110
|
.filter(m => m.toLowerCase().includes(inputLower))
|
|
110
111
|
.sort((a, b) => a.length - b.length);
|
|
111
112
|
return candidates[0] ?? null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function formatCodexUsageSummary(usage: CodexUsageSummary): string {
|
|
115
|
-
const progressBar = (usedPercent: number) => {
|
|
116
|
-
const width = 20;
|
|
117
|
-
const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
118
|
-
return `[${"█".repeat(usedBlocks)}${"░".repeat(width - usedBlocks)}]`;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const formatDuration = (seconds: number | null) => {
|
|
122
|
-
if (seconds === null) return "";
|
|
123
|
-
if (seconds <= 0) return "(已到重置时间)";
|
|
124
|
-
const totalMinutes = Math.max(1, Math.floor(seconds / 60));
|
|
125
|
-
const days = Math.floor(totalMinutes / 1440);
|
|
126
|
-
const hours = Math.floor((totalMinutes % 1440) / 60);
|
|
127
|
-
const minutes = totalMinutes % 60;
|
|
128
|
-
const parts: string[] = [];
|
|
129
|
-
if (days > 0) parts.push(`${days}天`);
|
|
130
|
-
if (hours > 0) parts.push(`${hours}小时`);
|
|
131
|
-
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`);
|
|
132
|
-
return `(约 ${parts.join("")}后)`;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const formatResetTime = (balance: CodexUsageSummary["fiveHour"]) => {
|
|
136
|
-
if (balance.resetAtEpochSeconds === null) return "暂无数据";
|
|
137
|
-
const date = new Date(balance.resetAtEpochSeconds * 1000);
|
|
138
|
-
const pad = (value: number) => String(value).padStart(2, "0");
|
|
139
|
-
const absolute = [
|
|
140
|
-
date.getFullYear(),
|
|
141
|
-
"-",
|
|
142
|
-
pad(date.getMonth() + 1),
|
|
143
|
-
"-",
|
|
144
|
-
pad(date.getDate()),
|
|
145
|
-
" ",
|
|
146
|
-
pad(date.getHours()),
|
|
147
|
-
":",
|
|
148
|
-
pad(date.getMinutes()),
|
|
149
|
-
].join("");
|
|
150
|
-
return `${absolute}${formatDuration(balance.resetAfterSeconds)}`;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
154
|
-
if (!balance) return `**${label}:** 暂无数据`;
|
|
155
|
-
return [
|
|
156
|
-
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
157
|
-
progressBar(balance.usedPercent),
|
|
158
|
-
].join("\n");
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
return [
|
|
162
|
-
"Codex 用量:",
|
|
163
|
-
"",
|
|
164
|
-
formatWindow("5h", usage.fiveHour),
|
|
165
|
-
formatWindow("周", usage.weekly),
|
|
166
|
-
].join("\n");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatCodexUsageSummary(usage: CodexUsageSummary): string {
|
|
116
|
+
const progressBar = (usedPercent: number) => {
|
|
117
|
+
const width = 20;
|
|
118
|
+
const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
119
|
+
return `[${"█".repeat(usedBlocks)}${"░".repeat(width - usedBlocks)}]`;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const formatDuration = (seconds: number | null) => {
|
|
123
|
+
if (seconds === null) return "";
|
|
124
|
+
if (seconds <= 0) return "(已到重置时间)";
|
|
125
|
+
const totalMinutes = Math.max(1, Math.floor(seconds / 60));
|
|
126
|
+
const days = Math.floor(totalMinutes / 1440);
|
|
127
|
+
const hours = Math.floor((totalMinutes % 1440) / 60);
|
|
128
|
+
const minutes = totalMinutes % 60;
|
|
129
|
+
const parts: string[] = [];
|
|
130
|
+
if (days > 0) parts.push(`${days}天`);
|
|
131
|
+
if (hours > 0) parts.push(`${hours}小时`);
|
|
132
|
+
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`);
|
|
133
|
+
return `(约 ${parts.join("")}后)`;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const formatResetTime = (balance: CodexUsageSummary["fiveHour"]) => {
|
|
137
|
+
if (balance.resetAtEpochSeconds === null) return "暂无数据";
|
|
138
|
+
const date = new Date(balance.resetAtEpochSeconds * 1000);
|
|
139
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
140
|
+
const absolute = [
|
|
141
|
+
date.getFullYear(),
|
|
142
|
+
"-",
|
|
143
|
+
pad(date.getMonth() + 1),
|
|
144
|
+
"-",
|
|
145
|
+
pad(date.getDate()),
|
|
146
|
+
" ",
|
|
147
|
+
pad(date.getHours()),
|
|
148
|
+
":",
|
|
149
|
+
pad(date.getMinutes()),
|
|
150
|
+
].join("");
|
|
151
|
+
return `${absolute}${formatDuration(balance.resetAfterSeconds)}`;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
155
|
+
if (!balance) return `**${label}:** 暂无数据`;
|
|
156
|
+
return [
|
|
157
|
+
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
158
|
+
progressBar(balance.usedPercent),
|
|
159
|
+
].join("\n");
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
"Codex 用量:",
|
|
164
|
+
"",
|
|
165
|
+
formatWindow("5h", usage.fiveHour),
|
|
166
|
+
formatWindow("周", usage.weekly),
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatCursorUsageSummary(usage: CursorUsageSummary): string {
|
|
171
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
172
|
+
const dateFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
173
|
+
timeZone,
|
|
174
|
+
year: "numeric",
|
|
175
|
+
month: "2-digit",
|
|
176
|
+
day: "2-digit",
|
|
177
|
+
hour: "2-digit",
|
|
178
|
+
minute: "2-digit",
|
|
179
|
+
second: "2-digit",
|
|
180
|
+
hour12: false,
|
|
181
|
+
timeZoneName: "shortOffset",
|
|
182
|
+
});
|
|
183
|
+
const formatDate = (value: string | undefined) => {
|
|
184
|
+
const timestamp = Number(value);
|
|
185
|
+
if (!Number.isFinite(timestamp)) return "暂无数据";
|
|
186
|
+
return dateFormatter.format(new Date(timestamp));
|
|
187
|
+
};
|
|
188
|
+
const formatMoney = (value: number | undefined) => {
|
|
189
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "暂无数据";
|
|
190
|
+
return `$${(value / 100).toFixed(2)}`;
|
|
191
|
+
};
|
|
192
|
+
const formatPercent = (value: number | undefined) => {
|
|
193
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "暂无数据";
|
|
194
|
+
return `${value}%`;
|
|
195
|
+
};
|
|
196
|
+
const plan = usage.planUsage;
|
|
197
|
+
const spendLimit = usage.spendLimitUsage;
|
|
198
|
+
|
|
199
|
+
return [
|
|
200
|
+
"Cursor 用量:",
|
|
201
|
+
"",
|
|
202
|
+
`**计费周期:** ${formatDate(usage.billingCycleStart)} - ${formatDate(usage.billingCycleEnd)}`,
|
|
203
|
+
"",
|
|
204
|
+
"**Included usage:**",
|
|
205
|
+
`- Total: ${formatMoney(plan?.totalSpend)} / ${formatMoney(plan?.limit)} (${formatPercent(plan?.totalPercentUsed)})`,
|
|
206
|
+
`- Included: ${formatMoney(plan?.includedSpend)}`,
|
|
207
|
+
`- Bonus: ${formatMoney(plan?.bonusSpend)}`,
|
|
208
|
+
`- Auto: ${formatPercent(plan?.autoPercentUsed)}`,
|
|
209
|
+
`- API: ${formatPercent(plan?.apiPercentUsed)}`,
|
|
210
|
+
"",
|
|
211
|
+
"**On-Demand / Spend limit:**",
|
|
212
|
+
`- Individual used: ${formatMoney(spendLimit?.individualUsed)}`,
|
|
213
|
+
`- Pool used: ${formatMoney(spendLimit?.pooledUsed)} / ${formatMoney(spendLimit?.pooledLimit)}`,
|
|
214
|
+
`- Pool remaining: ${formatMoney(spendLimit?.pooledRemaining)}`,
|
|
215
|
+
`- Limit type: ${spendLimit?.limitType ?? "暂无数据"}`,
|
|
216
|
+
`- Display threshold: ${formatMoney(usage.displayThreshold)}`,
|
|
217
|
+
"",
|
|
218
|
+
`**Enabled:** ${usage.enabled === undefined ? "暂无数据" : String(usage.enabled)}`,
|
|
219
|
+
usage.displayMessage ? `**Message:** ${usage.displayMessage}` : "",
|
|
220
|
+
usage.autoModelSelectedDisplayMessage ? `**Auto model:** ${usage.autoModelSelectedDisplayMessage}` : "",
|
|
221
|
+
usage.namedModelSelectedDisplayMessage ? `**Named model:** ${usage.namedModelSelectedDisplayMessage}` : "",
|
|
222
|
+
usage.autoBucketModels?.length ? `**Auto bucket models:** ${usage.autoBucketModels.join(", ")}` : "",
|
|
223
|
+
].filter(Boolean).join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function usageHelpLine(tool: string): string {
|
|
227
|
+
if (tool === "codex") return "\n发送 **/usage** 查看 Codex 5h 和周用量。";
|
|
228
|
+
if (tool === "cursor") return "\n发送 **/usage** 查看 Cursor 用量。";
|
|
229
|
+
return "";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function resolveUsageTool(chatId: string): Promise<"codex" | "cursor"> {
|
|
233
|
+
try {
|
|
234
|
+
const registry = await loadSessionRegistryForBinding();
|
|
235
|
+
return registry[chatId]?.tool === "cursor" ? "cursor" : "codex";
|
|
236
|
+
} catch {
|
|
237
|
+
return "codex";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor"): Promise<void> {
|
|
242
|
+
if (tool === "cursor") {
|
|
243
|
+
const content = formatCursorUsageSummary(await getCursorUsageSummary());
|
|
244
|
+
if (platform.kind === "wechat") {
|
|
245
|
+
await platform.sendText(chatId, content).catch(() => {});
|
|
246
|
+
} else {
|
|
247
|
+
await platform.sendCard(chatId, "Cursor Usage", content, "blue");
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const content = formatCodexUsageSummary(await getCodexUsageSummary());
|
|
253
|
+
if (platform.kind === "wechat") {
|
|
254
|
+
await platform.sendText(chatId, content).catch(() => {});
|
|
255
|
+
} else {
|
|
256
|
+
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function sendUsageError(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor", err: unknown): Promise<void> {
|
|
261
|
+
const toolLabel = tool === "cursor" ? "Cursor" : "Codex";
|
|
262
|
+
const message = `${toolLabel} 用量获取失败:${(err as Error).message}`;
|
|
263
|
+
if (platform.kind === "wechat") {
|
|
264
|
+
await platform.sendText(chatId, message).catch(() => {});
|
|
265
|
+
} else {
|
|
266
|
+
await platform.sendCard(chatId, `${toolLabel} Usage`, message, "red");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function isUntitledSessionChatName(name: string): boolean {
|
|
271
|
+
return name === "新会话" || name.startsWith("新会话-");
|
|
272
|
+
}
|
|
178
273
|
|
|
179
274
|
function shouldSendWechatProcessingAck(
|
|
180
275
|
platform: PlatformAdapter,
|
|
@@ -232,49 +327,49 @@ function isRunningFromGlobalNpm(): boolean {
|
|
|
232
327
|
}
|
|
233
328
|
}
|
|
234
329
|
|
|
235
|
-
const
|
|
330
|
+
const UPDATE_LOG = join(homedir(), ".chatccc", "logs", "update-watcher.log");
|
|
236
331
|
|
|
237
332
|
function updLog(msg: string): void {
|
|
238
333
|
const ts = new Date().toISOString();
|
|
239
|
-
try { appendFileSync(
|
|
334
|
+
try { appendFileSync(UPDATE_LOG, `${ts} [UPDATE-SYNC] ${msg}\n`, "utf-8"); } catch {}
|
|
240
335
|
}
|
|
241
336
|
|
|
242
337
|
/** 同步更新 npm 全局包并 spawn 新进程重启。不依赖 systemd 或任何服务管理器。 */
|
|
243
338
|
function syncUpdateAndRestart(): void {
|
|
244
339
|
updLog(`sync update start, pid=${process.pid}`);
|
|
245
|
-
appendStartupTrace("
|
|
340
|
+
appendStartupTrace("update: sync update start", { pid: process.pid });
|
|
246
341
|
|
|
247
342
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
248
343
|
|
|
249
344
|
// 1. npm update
|
|
250
345
|
updLog(`running: ${npmCmd} update -g chatccc`);
|
|
251
|
-
appendStartupTrace("
|
|
346
|
+
appendStartupTrace("update: npm update begin", { npmCmd });
|
|
252
347
|
const t0 = Date.now();
|
|
253
348
|
try {
|
|
254
349
|
const out = execSync(`${npmCmd} update -g chatccc 2>&1`, { encoding: "utf8", timeout: 120000, windowsHide: true });
|
|
255
350
|
const elapsed = Date.now() - t0;
|
|
256
351
|
updLog(`npm update OK (${elapsed}ms): ${out.slice(0, 500)}`);
|
|
257
|
-
appendStartupTrace("
|
|
352
|
+
appendStartupTrace("update: npm update OK", { elapsedMs: elapsed, outputLen: out.length });
|
|
258
353
|
} catch (e) {
|
|
259
354
|
const elapsed = Date.now() - t0;
|
|
260
355
|
const err = e as Error & { stderr?: string; stdout?: string; status?: number };
|
|
261
356
|
updLog(`npm update failed (${elapsed}ms): message=${err.message}, stderr=${(err.stderr || "").slice(0, 500)}, stdout=${(err.stdout || "").slice(0, 200)}`);
|
|
262
|
-
appendStartupTrace("
|
|
357
|
+
appendStartupTrace("update: npm update failed", { elapsedMs: elapsed, message: err.message, stderrLen: (err.stderr || "").length });
|
|
263
358
|
|
|
264
359
|
// fallback
|
|
265
360
|
updLog(`fallback: ${npmCmd} install -g chatccc@latest`);
|
|
266
|
-
appendStartupTrace("
|
|
361
|
+
appendStartupTrace("update: npm install fallback begin", { npmCmd });
|
|
267
362
|
const t1 = Date.now();
|
|
268
363
|
try {
|
|
269
364
|
const out2 = execSync(`${npmCmd} install -g chatccc@latest 2>&1`, { encoding: "utf8", timeout: 120000, windowsHide: true });
|
|
270
365
|
const elapsed2 = Date.now() - t1;
|
|
271
366
|
updLog(`npm install fallback OK (${elapsed2}ms): ${out2.slice(0, 500)}`);
|
|
272
|
-
appendStartupTrace("
|
|
367
|
+
appendStartupTrace("update: npm install fallback OK", { elapsedMs: elapsed2, outputLen: out2.length });
|
|
273
368
|
} catch (e2) {
|
|
274
369
|
const elapsed2 = Date.now() - t1;
|
|
275
370
|
const err2 = e2 as Error & { stderr?: string; stdout?: string };
|
|
276
371
|
updLog(`npm install fallback also failed (${elapsed2}ms): message=${err2.message}, stderr=${(err2.stderr || "").slice(0, 500)}`);
|
|
277
|
-
appendStartupTrace("
|
|
372
|
+
appendStartupTrace("update: npm install fallback failed", { elapsedMs: elapsed2, message: err2.message });
|
|
278
373
|
}
|
|
279
374
|
}
|
|
280
375
|
|
|
@@ -283,22 +378,22 @@ function syncUpdateAndRestart(): void {
|
|
|
283
378
|
const binName = process.platform === "win32" ? "chatccc.cmd" : "chatccc";
|
|
284
379
|
const binPath = npmPrefix ? join(npmPrefix, binName) : "chatccc";
|
|
285
380
|
updLog(`bin path: npmPrefix=${npmPrefix || "(empty)"}, binPath=${binPath}`);
|
|
286
|
-
appendStartupTrace("
|
|
381
|
+
appendStartupTrace("update: spawn begin", { npmPrefix: npmPrefix || "(empty)", binPath });
|
|
287
382
|
|
|
288
383
|
// 3. spawn new chatccc
|
|
289
384
|
try {
|
|
290
385
|
const child = spawn(binPath, [], { detached: true, stdio: "ignore", shell: true });
|
|
291
386
|
child.unref();
|
|
292
387
|
updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${binPath}`);
|
|
293
|
-
appendStartupTrace("
|
|
388
|
+
appendStartupTrace("update: spawn OK", { childPid: child.pid, binPath });
|
|
294
389
|
} catch (e) {
|
|
295
390
|
const errMsg = (e as Error).message;
|
|
296
391
|
updLog(`spawn new chatccc failed: ${errMsg}`);
|
|
297
|
-
appendStartupTrace("
|
|
392
|
+
appendStartupTrace("update: spawn failed", { error: errMsg });
|
|
298
393
|
}
|
|
299
394
|
|
|
300
395
|
updLog("sync update done, parent exiting in 2s");
|
|
301
|
-
appendStartupTrace("
|
|
396
|
+
appendStartupTrace("update: sync update done, exiting", { pid: process.pid });
|
|
302
397
|
}
|
|
303
398
|
|
|
304
399
|
// ---------------------------------------------------------------------------
|
|
@@ -354,46 +449,37 @@ export async function handleCommand(
|
|
|
354
449
|
return;
|
|
355
450
|
}
|
|
356
451
|
|
|
357
|
-
if (textLower === "/
|
|
358
|
-
logTrace(tid, "BRANCH", { cmd: "/
|
|
452
|
+
if (textLower === "/update") {
|
|
453
|
+
logTrace(tid, "BRANCH", { cmd: "/update" });
|
|
359
454
|
const isGlobal = isRunningFromGlobalNpm();
|
|
360
|
-
appendStartupTrace("
|
|
455
|
+
appendStartupTrace("update: command received", { isGlobal, chatId });
|
|
361
456
|
if (!isGlobal) {
|
|
362
|
-
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /
|
|
363
|
-
logTrace(tid, "DONE", { outcome: "
|
|
457
|
+
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update 更新。请通过 npm install -g chatccc 安装后使用。").catch(() => {});
|
|
458
|
+
logTrace(tid, "DONE", { outcome: "update_not_global" });
|
|
364
459
|
return;
|
|
365
460
|
}
|
|
366
461
|
await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => {});
|
|
367
|
-
logTrace(tid, "DONE", { outcome: "
|
|
368
|
-
appendStartupTrace("
|
|
462
|
+
logTrace(tid, "DONE", { outcome: "update" });
|
|
463
|
+
appendStartupTrace("update: sync update begin", { fromPid: process.pid });
|
|
369
464
|
syncUpdateAndRestart();
|
|
370
465
|
setTimeout(() => process.exit(0), 2000);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (textLower === "/usage") {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
} else {
|
|
389
|
-
await platform.sendCard(chatId, "Codex Usage", message, "red");
|
|
390
|
-
}
|
|
391
|
-
logTrace(tid, "DONE", { outcome: "usage_fail", error: (err as Error).message });
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (textLower === "/cd" || textLower.startsWith("/cd ")) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (textLower === "/usage") {
|
|
470
|
+
const usageTool = await resolveUsageTool(chatId);
|
|
471
|
+
logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
|
|
472
|
+
try {
|
|
473
|
+
await sendUsageSummary(platform, chatId, usageTool);
|
|
474
|
+
logTrace(tid, "DONE", { outcome: "usage", tool: usageTool });
|
|
475
|
+
} catch (err) {
|
|
476
|
+
await sendUsageError(platform, chatId, usageTool, err);
|
|
477
|
+
logTrace(tid, "DONE", { outcome: "usage_fail", tool: usageTool, error: (err as Error).message });
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (textLower === "/cd" || textLower.startsWith("/cd ")) {
|
|
397
483
|
logTrace(tid, "BRANCH", {
|
|
398
484
|
cmd: "/cd",
|
|
399
485
|
arg: text.slice(3).trim() || "(none)",
|
|
@@ -612,13 +698,13 @@ export async function handleCommand(
|
|
|
612
698
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
613
699
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
614
700
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
615
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
616
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
617
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
618
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
619
|
-
|
|
620
|
-
"green",
|
|
621
|
-
);
|
|
701
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
702
|
+
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
703
|
+
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
704
|
+
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
705
|
+
usageHelpLine(tool),
|
|
706
|
+
"green",
|
|
707
|
+
);
|
|
622
708
|
console.log(
|
|
623
709
|
`[${ts()}] [NEW] P2P session created: ${sessionId} (${toolLabel})`,
|
|
624
710
|
);
|
|
@@ -699,13 +785,13 @@ export async function handleCommand(
|
|
|
699
785
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
700
786
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
701
787
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
702
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
703
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
704
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
705
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
706
|
-
|
|
707
|
-
"green",
|
|
708
|
-
);
|
|
788
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
789
|
+
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
790
|
+
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
791
|
+
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
792
|
+
usageHelpLine(tool),
|
|
793
|
+
"green",
|
|
794
|
+
);
|
|
709
795
|
|
|
710
796
|
console.log(`[${ts()}] [STEP 4/4] Replied to new group → OK`);
|
|
711
797
|
logTrace(tid, "DONE", {
|