chatccc 0.2.174 → 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 +220 -219
- 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-
|
|
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,22 +466,23 @@ 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 {
|
|
469
|
+
function buildCodexUsageBatterySvg(remainingPercent: number): Buffer {
|
|
470
470
|
const remaining = clampPercent(remainingPercent);
|
|
471
|
-
const label =
|
|
471
|
+
const label = String(remaining);
|
|
472
472
|
const palette = codexUsagePalette(remaining);
|
|
473
473
|
|
|
474
474
|
const bodyX = 28;
|
|
475
475
|
const bodyY = 38;
|
|
476
|
-
const bodyW =
|
|
477
|
-
const bodyH =
|
|
478
|
-
const capW =
|
|
479
|
-
const capH =
|
|
476
|
+
const bodyW = 109;
|
|
477
|
+
const bodyH = 56;
|
|
478
|
+
const capW = 12;
|
|
479
|
+
const capH = 22;
|
|
480
480
|
const capX = bodyX + bodyW;
|
|
481
481
|
const capY = bodyY + Math.round((bodyH - capH) / 2);
|
|
482
|
-
const pad =
|
|
482
|
+
const pad = 6;
|
|
483
483
|
const fillMaxW = bodyW - pad * 2;
|
|
484
484
|
const fillWidth = Math.round((fillMaxW * remaining) / 100);
|
|
485
|
+
const labelFontSize = label.length >= 3 ? 49 : 51;
|
|
485
486
|
|
|
486
487
|
return Buffer.from(`
|
|
487
488
|
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -502,98 +503,98 @@ function buildCodexUsageBatterySvg(remainingPercent: number): Buffer {
|
|
|
502
503
|
</clipPath>
|
|
503
504
|
</defs>
|
|
504
505
|
<g filter="url(#shadow)">
|
|
505
|
-
<rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="
|
|
506
|
-
<rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillMaxW}" height="${bodyH - pad * 2}" rx="
|
|
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)"/>
|
|
507
508
|
<rect x="${bodyX + pad}" y="${bodyY + pad}" width="${fillWidth}" height="${bodyH - pad * 2}" fill="url(#fill)" clip-path="url(#batteryInnerClip)"/>
|
|
508
|
-
<rect x="${bodyX + pad +
|
|
509
|
-
<rect x="${capX}" y="${capY}" width="${capW}" height="${capH}" rx="
|
|
510
|
-
<rect x="${bodyX}" y="${bodyY}" width="${bodyW}" height="${bodyH}" rx="
|
|
511
|
-
<text x="${bodyX + bodyW / 2}" y="${bodyY +
|
|
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>
|
|
512
513
|
</g>
|
|
513
|
-
</svg>`);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
|
|
517
|
-
const remaining = clampPercent(remainingPercent);
|
|
518
|
-
const palette = codexUsagePalette(remaining);
|
|
519
|
-
const cx = 128;
|
|
520
|
-
const cy = 128;
|
|
521
|
-
const r = 118;
|
|
522
|
-
const strokeWidth = 13;
|
|
523
|
-
const used = clampPercent(100 - remaining);
|
|
524
|
-
const polar = (angleDegrees: number) => {
|
|
525
|
-
const angle = (angleDegrees * Math.PI) / 180;
|
|
526
|
-
return {
|
|
527
|
-
x: cx + r * Math.cos(angle),
|
|
528
|
-
y: cy + r * Math.sin(angle),
|
|
529
|
-
};
|
|
530
|
-
};
|
|
531
|
-
const startAngle = -90 + (used / 100) * 360;
|
|
532
|
-
const sweepAngle = (remaining / 100) * 360;
|
|
533
|
-
const start = polar(startAngle);
|
|
534
|
-
const end = polar(startAngle + Math.min(sweepAngle, 359.99));
|
|
535
|
-
const largeArcFlag = sweepAngle > 180 ? 1 : 0;
|
|
536
|
-
const progressPath = remaining >= 100
|
|
537
|
-
? `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="url(#ring)" stroke-width="${strokeWidth}" stroke-linecap="round" filter="url(#ringShadow)"/>`
|
|
538
|
-
: remaining <= 0
|
|
539
|
-
? ""
|
|
540
|
-
: `<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)"/>`;
|
|
541
|
-
|
|
542
|
-
return Buffer.from(`
|
|
543
|
-
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
544
|
-
<defs>
|
|
545
|
-
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
|
|
546
|
-
<stop offset="0" stop-color="${palette.start}"/>
|
|
547
|
-
<stop offset="1" stop-color="${palette.end}"/>
|
|
548
|
-
</linearGradient>
|
|
549
|
-
<filter id="ringShadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
550
|
-
<feDropShadow dx="0" dy="2" stdDeviation="2.4" flood-color="#0f172a" flood-opacity="0.25"/>
|
|
551
|
-
</filter>
|
|
552
|
-
</defs>
|
|
553
|
-
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#cbd5e1" stroke-width="${strokeWidth}"/>
|
|
554
|
-
${progressPath}
|
|
555
|
-
</svg>`);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function buildAgentBadgeOverlay(tool: string): Promise<sharp.OverlayOptions> {
|
|
559
|
-
const badge = await sharp(AVATAR_BADGES[tool])
|
|
560
|
-
.resize(AVATAR_BADGE_SIZE, AVATAR_BADGE_SIZE, {
|
|
561
|
-
fit: "contain",
|
|
562
|
-
kernel: sharp.kernel.lanczos3,
|
|
563
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
564
|
-
})
|
|
565
|
-
.png()
|
|
566
|
-
.toBuffer();
|
|
567
|
-
return {
|
|
568
|
-
input: badge,
|
|
569
|
-
left: AVATAR_SIZE - AVATAR_BADGE_SIZE - AVATAR_BADGE_MARGIN,
|
|
570
|
-
top: AVATAR_SIZE - AVATAR_BADGE_SIZE - AVATAR_BADGE_MARGIN,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
async function renderAvatar(tool: string, status: string, codexUsage: CodexUsageSummary | null = null): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
|
575
|
-
const normalizedTool = normalizeAvatarTool(tool);
|
|
576
|
-
const normalizedStatus = normalizeAvatarStatus(status);
|
|
577
|
-
const composites: sharp.OverlayOptions[] = [];
|
|
578
|
-
|
|
579
|
-
const useDynamicCodexAvatar = normalizedTool === "codex" && codexUsage?.weekly;
|
|
580
|
-
const basePath = useDynamicCodexAvatar
|
|
581
|
-
? AVATAR_SOURCES[normalizedStatus]
|
|
582
|
-
: avatarCombinationPath(normalizedTool, normalizedStatus);
|
|
583
|
-
|
|
584
|
-
if (useDynamicCodexAvatar) {
|
|
585
|
-
composites.push(
|
|
586
|
-
{ input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 },
|
|
587
|
-
{ input: buildCodexUsageBatterySvg(codexUsage.weekly.remainingPercent), left: 0, top: 0 },
|
|
588
|
-
await buildAgentBadgeOverlay(normalizedTool),
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
let pipeline = sharp(await readFile(basePath))
|
|
593
|
-
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: "cover", position: "center" });
|
|
594
|
-
if (composites.length > 0) {
|
|
595
|
-
pipeline = pipeline.composite(composites);
|
|
596
|
-
}
|
|
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
|
+
}
|
|
597
598
|
|
|
598
599
|
const jpeg = await pipeline
|
|
599
600
|
.flatten({ background: "#ffffff" })
|
|
@@ -601,16 +602,16 @@ async function renderAvatar(tool: string, status: string, codexUsage: CodexUsage
|
|
|
601
602
|
.jpeg({ quality: 95, progressive: false })
|
|
602
603
|
.toBuffer();
|
|
603
604
|
|
|
604
|
-
return {
|
|
605
|
-
buffer: jpeg,
|
|
606
|
-
contentType: "image/jpeg",
|
|
607
|
-
filename: codexUsage?.weekly
|
|
608
|
-
? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
|
|
609
|
-
: `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
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> {
|
|
614
615
|
const image = await renderAvatar(tool, status, codexUsage);
|
|
615
616
|
const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
|
|
616
617
|
const form = new FormData();
|
|
@@ -633,10 +634,10 @@ async function uploadImage(token: string, tool: string, status: string, codexUsa
|
|
|
633
634
|
}
|
|
634
635
|
|
|
635
636
|
async function getOrUploadAvatarKey(token: string, tool: string, status: string): Promise<string> {
|
|
636
|
-
await loadAvatarKeyCache();
|
|
637
|
-
const normalizedTool = normalizeAvatarTool(tool);
|
|
638
|
-
const normalizedStatus = normalizeAvatarStatus(status);
|
|
639
|
-
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;
|
|
640
641
|
const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage);
|
|
641
642
|
const cached = avatarKeyCache.get(keyName);
|
|
642
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
|
+
}
|