chatccc 0.2.175 → 0.2.176
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 +3 -3
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +2 -2
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/package.json +1 -1
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/config-reload.test.ts +18 -18
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +129 -129
- package/src/__tests__/feishu-platform.test.ts +16 -16
- package/src/__tests__/format-message.test.ts +272 -272
- package/src/__tests__/orchestrator.test.ts +93 -93
- package/src/__tests__/privacy.test.ts +67 -12
- package/src/__tests__/web-ui.test.ts +92 -92
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/agent-stop-stuck.ts +129 -129
- package/src/cards.ts +4 -4
- package/src/feishu-api.ts +236 -236
- package/src/feishu-platform.ts +15 -15
- package/src/format-message.ts +213 -213
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +112 -112
- package/src/privacy.ts +89 -39
- package/src/session-chat-binding.ts +6 -6
- package/src/sim-platform.ts +14 -14
- package/src/web-ui.ts +6 -6
package/src/feishu-api.ts
CHANGED
|
@@ -307,30 +307,30 @@ const AVATAR_SOURCES: Record<string, string> = {
|
|
|
307
307
|
busy: resolvePath(AVATAR_DIR, "status_busy.png"),
|
|
308
308
|
idle: resolvePath(AVATAR_DIR, "status_idle.png"),
|
|
309
309
|
};
|
|
310
|
-
const AVATAR_BADGES: Record<string, string> = {
|
|
311
|
-
claude: resolvePath(AVATAR_BADGE_DIR, "badge_claude.png"),
|
|
312
|
-
cursor: resolvePath(AVATAR_BADGE_DIR, "badge_cursor.png"),
|
|
313
|
-
codex: resolvePath(AVATAR_BADGE_DIR, "badge_codex.png"),
|
|
314
|
-
};
|
|
315
|
-
const AVATAR_SIZE = 256;
|
|
316
|
-
const AVATAR_BADGE_SIZE = 92;
|
|
317
|
-
const AVATAR_BADGE_MARGIN = 10;
|
|
318
|
-
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v5";
|
|
319
|
-
|
|
320
|
-
export interface CodexUsageBalance {
|
|
321
|
-
usedPercent: number;
|
|
322
|
-
remainingPercent: number;
|
|
323
|
-
resetAtEpochSeconds: number | null;
|
|
324
|
-
resetAfterSeconds: number | null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export interface CodexUsageSummary {
|
|
328
|
-
fiveHour: CodexUsageBalance;
|
|
329
|
-
weekly: CodexUsageBalance | null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const avatarKeyCache = new Map<string, string>();
|
|
333
|
-
let avatarKeyCacheLoaded = false;
|
|
310
|
+
const AVATAR_BADGES: Record<string, string> = {
|
|
311
|
+
claude: resolvePath(AVATAR_BADGE_DIR, "badge_claude.png"),
|
|
312
|
+
cursor: resolvePath(AVATAR_BADGE_DIR, "badge_cursor.png"),
|
|
313
|
+
codex: resolvePath(AVATAR_BADGE_DIR, "badge_codex.png"),
|
|
314
|
+
};
|
|
315
|
+
const AVATAR_SIZE = 256;
|
|
316
|
+
const AVATAR_BADGE_SIZE = 92;
|
|
317
|
+
const AVATAR_BADGE_MARGIN = 10;
|
|
318
|
+
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v5";
|
|
319
|
+
|
|
320
|
+
export interface CodexUsageBalance {
|
|
321
|
+
usedPercent: number;
|
|
322
|
+
remainingPercent: number;
|
|
323
|
+
resetAtEpochSeconds: number | null;
|
|
324
|
+
resetAfterSeconds: number | null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface CodexUsageSummary {
|
|
328
|
+
fiveHour: CodexUsageBalance;
|
|
329
|
+
weekly: CodexUsageBalance | null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const avatarKeyCache = new Map<string, string>();
|
|
333
|
+
let avatarKeyCacheLoaded = false;
|
|
334
334
|
|
|
335
335
|
function normalizeAvatarTool(tool: string): string {
|
|
336
336
|
return AVATAR_BADGES[tool] ? tool : "claude";
|
|
@@ -344,16 +344,16 @@ function avatarCombinationPath(tool: string, status: string): string {
|
|
|
344
344
|
return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
function avatarCacheKey(tool: string, status: string, codexUsage: CodexUsageSummary | null = null): string {
|
|
348
|
-
const normalizedTool = normalizeAvatarTool(tool);
|
|
349
|
-
const normalizedStatus = normalizeAvatarStatus(status);
|
|
350
|
-
if (normalizedTool === "codex") {
|
|
351
|
-
return codexUsage
|
|
352
|
-
? `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:week-battery:${codexUsage.weekly?.remainingPercent}:5h-ring:${codexUsage.fiveHour.remainingPercent}`
|
|
353
|
-
: `${normalizedTool}:${normalizedStatus}:plain`;
|
|
354
|
-
}
|
|
355
|
-
return `${normalizedTool}:${normalizedStatus}`;
|
|
356
|
-
}
|
|
347
|
+
function avatarCacheKey(tool: string, status: string, codexUsage: CodexUsageSummary | null = null): string {
|
|
348
|
+
const normalizedTool = normalizeAvatarTool(tool);
|
|
349
|
+
const normalizedStatus = normalizeAvatarStatus(status);
|
|
350
|
+
if (normalizedTool === "codex") {
|
|
351
|
+
return codexUsage
|
|
352
|
+
? `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:week-battery:${codexUsage.weekly?.remainingPercent}:5h-ring:${codexUsage.fiveHour.remainingPercent}`
|
|
353
|
+
: `${normalizedTool}:${normalizedStatus}:plain`;
|
|
354
|
+
}
|
|
355
|
+
return `${normalizedTool}:${normalizedStatus}`;
|
|
356
|
+
}
|
|
357
357
|
|
|
358
358
|
async function loadAvatarKeyCache(): Promise<void> {
|
|
359
359
|
if (avatarKeyCacheLoaded) return;
|
|
@@ -378,37 +378,37 @@ async function persistAvatarKeyCache(): Promise<void> {
|
|
|
378
378
|
);
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
function clampPercent(value: number): number {
|
|
382
|
-
if (!Number.isFinite(value)) return 0;
|
|
383
|
-
return Math.max(0, Math.min(100, Math.round(value)));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function usageBalanceFromWindow(raw: Record<string, unknown>, fieldName: string): CodexUsageBalance {
|
|
387
|
-
const value = raw.used_percent;
|
|
388
|
-
const usedPercent = Number(value);
|
|
389
|
-
if (!Number.isFinite(usedPercent)) throw new Error(`missing ${fieldName}.used_percent`);
|
|
390
|
-
const used = clampPercent(usedPercent);
|
|
391
|
-
const resetAt = Number(raw.reset_at);
|
|
392
|
-
const resetAfter = Number(raw.reset_after_seconds);
|
|
393
|
-
return {
|
|
394
|
-
usedPercent: used,
|
|
395
|
-
remainingPercent: clampPercent(100 - used),
|
|
396
|
-
resetAtEpochSeconds: Number.isFinite(resetAt) ? resetAt : null,
|
|
397
|
-
resetAfterSeconds: Number.isFinite(resetAfter) ? resetAfter : null,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: string[]): CodexUsageBalance | null {
|
|
402
|
-
for (const key of keys) {
|
|
403
|
-
const raw = rateLimit[key];
|
|
404
|
-
if (!raw || typeof raw !== "object") continue;
|
|
405
|
-
if ((raw as { used_percent?: unknown }).used_percent === undefined) continue;
|
|
406
|
-
return usageBalanceFromWindow(raw as Record<string, unknown>, `rate_limit.${key}`);
|
|
407
|
-
}
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async function getCodexAccessToken(): Promise<string | null> {
|
|
381
|
+
function clampPercent(value: number): number {
|
|
382
|
+
if (!Number.isFinite(value)) return 0;
|
|
383
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function usageBalanceFromWindow(raw: Record<string, unknown>, fieldName: string): CodexUsageBalance {
|
|
387
|
+
const value = raw.used_percent;
|
|
388
|
+
const usedPercent = Number(value);
|
|
389
|
+
if (!Number.isFinite(usedPercent)) throw new Error(`missing ${fieldName}.used_percent`);
|
|
390
|
+
const used = clampPercent(usedPercent);
|
|
391
|
+
const resetAt = Number(raw.reset_at);
|
|
392
|
+
const resetAfter = Number(raw.reset_after_seconds);
|
|
393
|
+
return {
|
|
394
|
+
usedPercent: used,
|
|
395
|
+
remainingPercent: clampPercent(100 - used),
|
|
396
|
+
resetAtEpochSeconds: Number.isFinite(resetAt) ? resetAt : null,
|
|
397
|
+
resetAfterSeconds: Number.isFinite(resetAfter) ? resetAfter : null,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: string[]): CodexUsageBalance | null {
|
|
402
|
+
for (const key of keys) {
|
|
403
|
+
const raw = rateLimit[key];
|
|
404
|
+
if (!raw || typeof raw !== "object") continue;
|
|
405
|
+
if ((raw as { used_percent?: unknown }).used_percent === undefined) continue;
|
|
406
|
+
return usageBalanceFromWindow(raw as Record<string, unknown>, `rate_limit.${key}`);
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function getCodexAccessToken(): Promise<string | null> {
|
|
412
412
|
try {
|
|
413
413
|
const raw = await readFile(CODEX_AUTH_FILE, "utf-8");
|
|
414
414
|
const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown } };
|
|
@@ -416,49 +416,49 @@ async function getCodexAccessToken(): Promise<string | null> {
|
|
|
416
416
|
return typeof token === "string" && token.trim() ? token : null;
|
|
417
417
|
} catch {
|
|
418
418
|
return null;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
|
|
423
|
-
const token = await getCodexAccessToken();
|
|
424
|
-
if (!token) throw new Error("missing ~/.codex/auth.json access token");
|
|
425
|
-
|
|
426
|
-
const resp = await fetch(CODEX_USAGE_URL, {
|
|
427
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
428
|
-
});
|
|
429
|
-
const text = await resp.text();
|
|
430
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
431
|
-
|
|
432
|
-
const data = JSON.parse(text) as {
|
|
433
|
-
rate_limit?: Record<string, unknown>;
|
|
434
|
-
};
|
|
435
|
-
const rateLimit = data.rate_limit;
|
|
436
|
-
if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
|
|
437
|
-
|
|
438
|
-
const fiveHour = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
|
|
439
|
-
if (!fiveHour) throw new Error("missing rate_limit.primary_window.used_percent");
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
fiveHour,
|
|
443
|
-
weekly: parseOptionalUsageWindow(rateLimit, [
|
|
444
|
-
"secondary_window",
|
|
445
|
-
"weekly_window",
|
|
446
|
-
"week_window",
|
|
447
|
-
"long_window",
|
|
448
|
-
]),
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function fetchCodexAvatarUsage(): Promise<CodexUsageSummary | null> {
|
|
453
|
-
try {
|
|
454
|
-
const summary = await getCodexUsageSummary();
|
|
455
|
-
if (!summary.weekly) throw new Error("missing weekly usage window");
|
|
456
|
-
return summary;
|
|
457
|
-
} catch (err) {
|
|
458
|
-
console.warn(`[${ts()}] [AVATAR] Codex usage unavailable, using plain avatar: ${(err as Error).message}`);
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
|
|
423
|
+
const token = await getCodexAccessToken();
|
|
424
|
+
if (!token) throw new Error("missing ~/.codex/auth.json access token");
|
|
425
|
+
|
|
426
|
+
const resp = await fetch(CODEX_USAGE_URL, {
|
|
427
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
428
|
+
});
|
|
429
|
+
const text = await resp.text();
|
|
430
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
|
|
431
|
+
|
|
432
|
+
const data = JSON.parse(text) as {
|
|
433
|
+
rate_limit?: Record<string, unknown>;
|
|
434
|
+
};
|
|
435
|
+
const rateLimit = data.rate_limit;
|
|
436
|
+
if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
|
|
437
|
+
|
|
438
|
+
const fiveHour = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
|
|
439
|
+
if (!fiveHour) throw new Error("missing rate_limit.primary_window.used_percent");
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
fiveHour,
|
|
443
|
+
weekly: parseOptionalUsageWindow(rateLimit, [
|
|
444
|
+
"secondary_window",
|
|
445
|
+
"weekly_window",
|
|
446
|
+
"week_window",
|
|
447
|
+
"long_window",
|
|
448
|
+
]),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function fetchCodexAvatarUsage(): Promise<CodexUsageSummary | null> {
|
|
453
|
+
try {
|
|
454
|
+
const summary = await getCodexUsageSummary();
|
|
455
|
+
if (!summary.weekly) throw new Error("missing weekly usage window");
|
|
456
|
+
return summary;
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.warn(`[${ts()}] [AVATAR] Codex usage unavailable, using plain avatar: ${(err as Error).message}`);
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
462
|
|
|
463
463
|
function codexUsagePalette(remainingPercent: number): { start: string; end: string; glow: string } {
|
|
464
464
|
if (remainingPercent <= 25) return { start: "#ef4444", end: "#fb923c", glow: "#fed7aa" };
|
|
@@ -466,26 +466,26 @@ function codexUsagePalette(remainingPercent: number): { start: string; end: stri
|
|
|
466
466
|
return { start: "#16a34a", end: "#34d399", glow: "#bbf7d0" };
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
-
function buildCodexUsageBatterySvg(remainingPercent: number): Buffer {
|
|
470
|
-
const remaining = clampPercent(remainingPercent);
|
|
471
|
-
const label = String(remaining);
|
|
472
|
-
const palette = codexUsagePalette(remaining);
|
|
473
|
-
|
|
474
|
-
const bodyX = 28;
|
|
475
|
-
const bodyY = 38;
|
|
476
|
-
const bodyW = 109;
|
|
477
|
-
const bodyH = 56;
|
|
478
|
-
const capW = 12;
|
|
479
|
-
const capH = 22;
|
|
480
|
-
const capX = bodyX + bodyW;
|
|
481
|
-
const capY = bodyY + Math.round((bodyH - capH) / 2);
|
|
482
|
-
const pad = 6;
|
|
483
|
-
const fillMaxW = bodyW - pad * 2;
|
|
484
|
-
const fillWidth = Math.round((fillMaxW * remaining) / 100);
|
|
485
|
-
const labelFontSize = label.length >= 3 ? 49 : 51;
|
|
486
|
-
|
|
487
|
-
return Buffer.from(`
|
|
488
|
-
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
469
|
+
function buildCodexUsageBatterySvg(remainingPercent: number): Buffer {
|
|
470
|
+
const remaining = clampPercent(remainingPercent);
|
|
471
|
+
const label = String(remaining);
|
|
472
|
+
const palette = codexUsagePalette(remaining);
|
|
473
|
+
|
|
474
|
+
const bodyX = 28;
|
|
475
|
+
const bodyY = 38;
|
|
476
|
+
const bodyW = 109;
|
|
477
|
+
const bodyH = 56;
|
|
478
|
+
const capW = 12;
|
|
479
|
+
const capH = 22;
|
|
480
|
+
const capX = bodyX + bodyW;
|
|
481
|
+
const capY = bodyY + Math.round((bodyH - capH) / 2);
|
|
482
|
+
const pad = 6;
|
|
483
|
+
const fillMaxW = bodyW - pad * 2;
|
|
484
|
+
const fillWidth = Math.round((fillMaxW * remaining) / 100);
|
|
485
|
+
const labelFontSize = label.length >= 3 ? 49 : 51;
|
|
486
|
+
|
|
487
|
+
return Buffer.from(`
|
|
488
|
+
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
489
489
|
<defs>
|
|
490
490
|
<filter id="shadow" x="-35%" y="-35%" width="170%" height="170%">
|
|
491
491
|
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#4a2712" flood-opacity="0.30"/>
|
|
@@ -501,100 +501,100 @@ function buildCodexUsageBatterySvg(remainingPercent: number): Buffer {
|
|
|
501
501
|
<clipPath id="batteryInnerClip">
|
|
502
502
|
<rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillMaxW}" height="${bodyH - pad * 2}" rx="10"/>
|
|
503
503
|
</clipPath>
|
|
504
|
-
</defs>
|
|
505
|
-
<g filter="url(#shadow)">
|
|
506
|
-
<rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="18" fill="#0f172a"/>
|
|
507
|
-
<rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillMaxW}" height="${bodyH - pad * 2}" rx="12" fill="url(#well)"/>
|
|
508
|
-
<rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillWidth}" height="${bodyH - pad * 2}" fill="url(#fill)" clip-path="url(#batteryInnerClip)"/>
|
|
509
|
-
<rect x="${bodyX + pad + 4}" y="${bodyY + pad + 5}" width="${Math.max(0, fillWidth - 8)}" height="8" rx="4" fill="${palette.glow}" fill-opacity="0.42" clip-path="url(#batteryInnerClip)"/>
|
|
510
|
-
<rect x="${capX}" y="${capY}" width="${capW}" height="${capH}" rx="6" fill="#0f172a"/>
|
|
511
|
-
<rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="18" fill="none" stroke="#f8fafc" stroke-opacity="0.82" stroke-width="3.8"/>
|
|
512
|
-
<text x="${bodyX + bodyW / 2}" y="${bodyY + bodyH / 2 + 3}" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" font-family="Arial, Helvetica, sans-serif" font-size="${labelFontSize}" font-weight="700" letter-spacing="0" stroke="#0b1220" stroke-width="2.6" paint-order="stroke" stroke-linejoin="round" fill="#ffffff">${label}</text>
|
|
513
|
-
</g>
|
|
514
|
-
</svg>`);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
|
|
518
|
-
const remaining = clampPercent(remainingPercent);
|
|
519
|
-
const palette = codexUsagePalette(remaining);
|
|
520
|
-
const cx = 128;
|
|
521
|
-
const cy = 128;
|
|
522
|
-
const r = 118;
|
|
523
|
-
const strokeWidth = 13;
|
|
524
|
-
const used = clampPercent(100 - remaining);
|
|
525
|
-
const polar = (angleDegrees: number) => {
|
|
526
|
-
const angle = (angleDegrees * Math.PI) / 180;
|
|
527
|
-
return {
|
|
528
|
-
x: cx + r * Math.cos(angle),
|
|
529
|
-
y: cy + r * Math.sin(angle),
|
|
530
|
-
};
|
|
531
|
-
};
|
|
532
|
-
const startAngle = -90 + (used / 100) * 360;
|
|
533
|
-
const sweepAngle = (remaining / 100) * 360;
|
|
534
|
-
const start = polar(startAngle);
|
|
535
|
-
const end = polar(startAngle + Math.min(sweepAngle, 359.99));
|
|
536
|
-
const largeArcFlag = sweepAngle > 180 ? 1 : 0;
|
|
537
|
-
const progressPath = remaining >= 100
|
|
538
|
-
? `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="url(#ring)" stroke-width="${strokeWidth}" stroke-linecap="round" filter="url(#ringShadow)"/>`
|
|
539
|
-
: remaining <= 0
|
|
540
|
-
? ""
|
|
541
|
-
: `<path d="M ${start.x.toFixed(3)} ${start.y.toFixed(3)} A ${r} ${r} 0 ${largeArcFlag} 1 ${end.x.toFixed(3)} ${end.y.toFixed(3)}" fill="none" stroke="url(#ring)" stroke-width="${strokeWidth}" stroke-linecap="round" filter="url(#ringShadow)"/>`;
|
|
542
|
-
|
|
543
|
-
return Buffer.from(`
|
|
544
|
-
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
545
|
-
<defs>
|
|
546
|
-
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
|
|
547
|
-
<stop offset="0" stop-color="${palette.start}"/>
|
|
548
|
-
<stop offset="1" stop-color="${palette.end}"/>
|
|
549
|
-
</linearGradient>
|
|
550
|
-
<filter id="ringShadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
551
|
-
<feDropShadow dx="0" dy="2" stdDeviation="2.4" flood-color="#0f172a" flood-opacity="0.25"/>
|
|
552
|
-
</filter>
|
|
553
|
-
</defs>
|
|
554
|
-
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#cbd5e1" stroke-width="${strokeWidth}"/>
|
|
555
|
-
${progressPath}
|
|
556
|
-
</svg>`);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async function buildAgentBadgeOverlay(tool: string): Promise<sharp.OverlayOptions> {
|
|
560
|
-
const badge = await sharp(AVATAR_BADGES[tool])
|
|
561
|
-
.resize(AVATAR_BADGE_SIZE, AVATAR_BADGE_SIZE, {
|
|
562
|
-
fit: "contain",
|
|
563
|
-
kernel: sharp.kernel.lanczos3,
|
|
564
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
565
|
-
})
|
|
566
|
-
.png()
|
|
567
|
-
.toBuffer();
|
|
568
|
-
return {
|
|
569
|
-
input: badge,
|
|
570
|
-
left: AVATAR_SIZE - AVATAR_BADGE_SIZE - AVATAR_BADGE_MARGIN,
|
|
571
|
-
top: AVATAR_SIZE - AVATAR_BADGE_SIZE - AVATAR_BADGE_MARGIN,
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async function renderAvatar(tool: string, status: string, codexUsage: CodexUsageSummary | null = null): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
|
576
|
-
const normalizedTool = normalizeAvatarTool(tool);
|
|
577
|
-
const normalizedStatus = normalizeAvatarStatus(status);
|
|
578
|
-
const composites: sharp.OverlayOptions[] = [];
|
|
579
|
-
|
|
580
|
-
const useDynamicCodexAvatar = normalizedTool === "codex" && codexUsage?.weekly;
|
|
581
|
-
const basePath = useDynamicCodexAvatar
|
|
582
|
-
? AVATAR_SOURCES[normalizedStatus]
|
|
583
|
-
: avatarCombinationPath(normalizedTool, normalizedStatus);
|
|
584
|
-
|
|
585
|
-
if (useDynamicCodexAvatar) {
|
|
586
|
-
composites.push(
|
|
587
|
-
{ input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 },
|
|
588
|
-
{ input: buildCodexUsageBatterySvg(codexUsage.weekly.remainingPercent), left: 0, top: 0 },
|
|
589
|
-
await buildAgentBadgeOverlay(normalizedTool),
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
let pipeline = sharp(await readFile(basePath))
|
|
594
|
-
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" });
|
|
595
|
-
if (composites.length > 0) {
|
|
596
|
-
pipeline = pipeline.composite(composites);
|
|
597
|
-
}
|
|
504
|
+
</defs>
|
|
505
|
+
<g filter="url(#shadow)">
|
|
506
|
+
<rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="18" fill="#0f172a"/>
|
|
507
|
+
<rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillMaxW}" height="${bodyH - pad * 2}" rx="12" fill="url(#well)"/>
|
|
508
|
+
<rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillWidth}" height="${bodyH - pad * 2}" fill="url(#fill)" clip-path="url(#batteryInnerClip)"/>
|
|
509
|
+
<rect x="${bodyX + pad + 4}" y="${bodyY + pad + 5}" width="${Math.max(0, fillWidth - 8)}" height="8" rx="4" fill="${palette.glow}" fill-opacity="0.42" clip-path="url(#batteryInnerClip)"/>
|
|
510
|
+
<rect x="${capX}" y="${capY}" width="${capW}" height="${capH}" rx="6" fill="#0f172a"/>
|
|
511
|
+
<rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="18" fill="none" stroke="#f8fafc" stroke-opacity="0.82" stroke-width="3.8"/>
|
|
512
|
+
<text x="${bodyX + bodyW / 2}" y="${bodyY + bodyH / 2 + 3}" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" font-family="Arial, Helvetica, sans-serif" font-size="${labelFontSize}" font-weight="700" letter-spacing="0" stroke="#0b1220" stroke-width="2.6" paint-order="stroke" stroke-linejoin="round" fill="#ffffff">${label}</text>
|
|
513
|
+
</g>
|
|
514
|
+
</svg>`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
|
|
518
|
+
const remaining = clampPercent(remainingPercent);
|
|
519
|
+
const palette = codexUsagePalette(remaining);
|
|
520
|
+
const cx = 128;
|
|
521
|
+
const cy = 128;
|
|
522
|
+
const r = 118;
|
|
523
|
+
const strokeWidth = 13;
|
|
524
|
+
const used = clampPercent(100 - remaining);
|
|
525
|
+
const polar = (angleDegrees: number) => {
|
|
526
|
+
const angle = (angleDegrees * Math.PI) / 180;
|
|
527
|
+
return {
|
|
528
|
+
x: cx + r * Math.cos(angle),
|
|
529
|
+
y: cy + r * Math.sin(angle),
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
const startAngle = -90 + (used / 100) * 360;
|
|
533
|
+
const sweepAngle = (remaining / 100) * 360;
|
|
534
|
+
const start = polar(startAngle);
|
|
535
|
+
const end = polar(startAngle + Math.min(sweepAngle, 359.99));
|
|
536
|
+
const largeArcFlag = sweepAngle > 180 ? 1 : 0;
|
|
537
|
+
const progressPath = remaining >= 100
|
|
538
|
+
? `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="url(#ring)" stroke-width="${strokeWidth}" stroke-linecap="round" filter="url(#ringShadow)"/>`
|
|
539
|
+
: remaining <= 0
|
|
540
|
+
? ""
|
|
541
|
+
: `<path d="M ${start.x.toFixed(3)} ${start.y.toFixed(3)} A ${r} ${r} 0 ${largeArcFlag} 1 ${end.x.toFixed(3)} ${end.y.toFixed(3)}" fill="none" stroke="url(#ring)" stroke-width="${strokeWidth}" stroke-linecap="round" filter="url(#ringShadow)"/>`;
|
|
542
|
+
|
|
543
|
+
return Buffer.from(`
|
|
544
|
+
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
545
|
+
<defs>
|
|
546
|
+
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
|
|
547
|
+
<stop offset="0" stop-color="${palette.start}"/>
|
|
548
|
+
<stop offset="1" stop-color="${palette.end}"/>
|
|
549
|
+
</linearGradient>
|
|
550
|
+
<filter id="ringShadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
551
|
+
<feDropShadow dx="0" dy="2" stdDeviation="2.4" flood-color="#0f172a" flood-opacity="0.25"/>
|
|
552
|
+
</filter>
|
|
553
|
+
</defs>
|
|
554
|
+
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#cbd5e1" stroke-width="${strokeWidth}"/>
|
|
555
|
+
${progressPath}
|
|
556
|
+
</svg>`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function buildAgentBadgeOverlay(tool: string): Promise<sharp.OverlayOptions> {
|
|
560
|
+
const badge = await sharp(AVATAR_BADGES[tool])
|
|
561
|
+
.resize(AVATAR_BADGE_SIZE, AVATAR_BADGE_SIZE, {
|
|
562
|
+
fit: "contain",
|
|
563
|
+
kernel: sharp.kernel.lanczos3,
|
|
564
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
565
|
+
})
|
|
566
|
+
.png()
|
|
567
|
+
.toBuffer();
|
|
568
|
+
return {
|
|
569
|
+
input: badge,
|
|
570
|
+
left: AVATAR_SIZE - AVATAR_BADGE_SIZE - AVATAR_BADGE_MARGIN,
|
|
571
|
+
top: AVATAR_SIZE - AVATAR_BADGE_SIZE - AVATAR_BADGE_MARGIN,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function renderAvatar(tool: string, status: string, codexUsage: CodexUsageSummary | null = null): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
|
576
|
+
const normalizedTool = normalizeAvatarTool(tool);
|
|
577
|
+
const normalizedStatus = normalizeAvatarStatus(status);
|
|
578
|
+
const composites: sharp.OverlayOptions[] = [];
|
|
579
|
+
|
|
580
|
+
const useDynamicCodexAvatar = normalizedTool === "codex" && codexUsage?.weekly;
|
|
581
|
+
const basePath = useDynamicCodexAvatar
|
|
582
|
+
? AVATAR_SOURCES[normalizedStatus]
|
|
583
|
+
: avatarCombinationPath(normalizedTool, normalizedStatus);
|
|
584
|
+
|
|
585
|
+
if (useDynamicCodexAvatar) {
|
|
586
|
+
composites.push(
|
|
587
|
+
{ input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 },
|
|
588
|
+
{ input: buildCodexUsageBatterySvg(codexUsage.weekly.remainingPercent), left: 0, top: 0 },
|
|
589
|
+
await buildAgentBadgeOverlay(normalizedTool),
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let pipeline = sharp(await readFile(basePath))
|
|
594
|
+
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" });
|
|
595
|
+
if (composites.length > 0) {
|
|
596
|
+
pipeline = pipeline.composite(composites);
|
|
597
|
+
}
|
|
598
598
|
|
|
599
599
|
const jpeg = await pipeline
|
|
600
600
|
.flatten({ background: "#ffffff" })
|
|
@@ -602,16 +602,16 @@ async function renderAvatar(tool: string, status: string, codexUsage: CodexUsage
|
|
|
602
602
|
.jpeg({ quality: 95, progressive: false })
|
|
603
603
|
.toBuffer();
|
|
604
604
|
|
|
605
|
-
return {
|
|
606
|
-
buffer: jpeg,
|
|
607
|
-
contentType: "image/jpeg",
|
|
608
|
-
filename: codexUsage?.weekly
|
|
609
|
-
? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
|
|
610
|
-
: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
async function uploadImage(token: string, tool: string, status: string, codexUsage: CodexUsageSummary | null = null): Promise<string> {
|
|
605
|
+
return {
|
|
606
|
+
buffer: jpeg,
|
|
607
|
+
contentType: "image/jpeg",
|
|
608
|
+
filename: codexUsage?.weekly
|
|
609
|
+
? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
|
|
610
|
+
: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function uploadImage(token: string, tool: string, status: string, codexUsage: CodexUsageSummary | null = null): Promise<string> {
|
|
615
615
|
const image = await renderAvatar(tool, status, codexUsage);
|
|
616
616
|
const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
|
|
617
617
|
const form = new FormData();
|
|
@@ -634,10 +634,10 @@ async function uploadImage(token: string, tool: string, status: string, codexUsa
|
|
|
634
634
|
}
|
|
635
635
|
|
|
636
636
|
async function getOrUploadAvatarKey(token: string, tool: string, status: string): Promise<string> {
|
|
637
|
-
await loadAvatarKeyCache();
|
|
638
|
-
const normalizedTool = normalizeAvatarTool(tool);
|
|
639
|
-
const normalizedStatus = normalizeAvatarStatus(status);
|
|
640
|
-
const codexUsage = normalizedTool === "codex" ? await fetchCodexAvatarUsage() : null;
|
|
637
|
+
await loadAvatarKeyCache();
|
|
638
|
+
const normalizedTool = normalizeAvatarTool(tool);
|
|
639
|
+
const normalizedStatus = normalizeAvatarStatus(status);
|
|
640
|
+
const codexUsage = normalizedTool === "codex" ? await fetchCodexAvatarUsage() : null;
|
|
641
641
|
const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage);
|
|
642
642
|
const cached = avatarKeyCache.get(keyName);
|
|
643
643
|
if (cached) return cached;
|
package/src/feishu-platform.ts
CHANGED
|
@@ -29,9 +29,9 @@ export interface FeishuPlatform {
|
|
|
29
29
|
updateChatInfo: typeof realApi.updateChatInfo;
|
|
30
30
|
getChatInfo: typeof realApi.getChatInfo;
|
|
31
31
|
disbandChat: typeof realApi.disbandChat;
|
|
32
|
-
setChatAvatar: typeof realApi.setChatAvatar;
|
|
33
|
-
getCodexUsageSummary: typeof realApi.getCodexUsageSummary;
|
|
34
|
-
getOrDownloadImage: typeof realApi.getOrDownloadImage;
|
|
32
|
+
setChatAvatar: typeof realApi.setChatAvatar;
|
|
33
|
+
getCodexUsageSummary: typeof realApi.getCodexUsageSummary;
|
|
34
|
+
getOrDownloadImage: typeof realApi.getOrDownloadImage;
|
|
35
35
|
verifyAllPermissions: typeof realApi.verifyAllPermissions;
|
|
36
36
|
reportPermissionResults: typeof realApi.reportPermissionResults;
|
|
37
37
|
extractSessionInfo: typeof realApi.extractSessionInfo;
|
|
@@ -109,17 +109,17 @@ export function disbandChat(...args: Parameters<typeof realApi.disbandChat>): Re
|
|
|
109
109
|
return _impl.disbandChat(...args);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
export function setChatAvatar(...args: Parameters<typeof realApi.setChatAvatar>): ReturnType<typeof realApi.setChatAvatar> {
|
|
113
|
-
return _impl.setChatAvatar(...args);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function getCodexUsageSummary(...args: Parameters<typeof realApi.getCodexUsageSummary>): ReturnType<typeof realApi.getCodexUsageSummary> {
|
|
117
|
-
return _impl.getCodexUsageSummary(...args);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function getOrDownloadImage(...args: Parameters<typeof realApi.getOrDownloadImage>): ReturnType<typeof realApi.getOrDownloadImage> {
|
|
121
|
-
return _impl.getOrDownloadImage(...args);
|
|
122
|
-
}
|
|
112
|
+
export function setChatAvatar(...args: Parameters<typeof realApi.setChatAvatar>): ReturnType<typeof realApi.setChatAvatar> {
|
|
113
|
+
return _impl.setChatAvatar(...args);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getCodexUsageSummary(...args: Parameters<typeof realApi.getCodexUsageSummary>): ReturnType<typeof realApi.getCodexUsageSummary> {
|
|
117
|
+
return _impl.getCodexUsageSummary(...args);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getOrDownloadImage(...args: Parameters<typeof realApi.getOrDownloadImage>): ReturnType<typeof realApi.getOrDownloadImage> {
|
|
121
|
+
return _impl.getOrDownloadImage(...args);
|
|
122
|
+
}
|
|
123
123
|
|
|
124
124
|
export function verifyAllPermissions(...args: Parameters<typeof realApi.verifyAllPermissions>): ReturnType<typeof realApi.verifyAllPermissions> {
|
|
125
125
|
return _impl.verifyAllPermissions(...args);
|
|
@@ -151,4 +151,4 @@ export function sendRestartCard(...args: Parameters<typeof realApi.sendRestartCa
|
|
|
151
151
|
|
|
152
152
|
export function getMergeForwardMessages(...args: Parameters<typeof realApi.getMergeForwardMessages>): ReturnType<typeof realApi.getMergeForwardMessages> {
|
|
153
153
|
return _impl.getMergeForwardMessages(...args);
|
|
154
|
-
}
|
|
154
|
+
}
|