- Go gateway (stdlib only): /v1/messages non-stream + SSE, /v1/messages/count_tokens, management API /api/* (providers CRUD, apply/restore, settings, backups, logs) - Protocol conversion: Anthropic Messages <-> Chat Completions with a streaming state machine keyed by content block index (text, tool_use, parallel calls) - ~/.claude/settings.json guarded read/write: timestamped backups, managed env keys only, one-click restore - Tauri 2 shell: spawns the gateway sidecar, parses the random port from stdout, stdin pipe watchdog prevents orphans, kills the child on exit - React frontend: Dashboard/Providers/Settings, zh-CN + en-US, TanStack Query - Tests: Go table-driven + httptest integration (all green), vitest + RTL
173 lines
5.7 KiB
JavaScript
173 lines
5.7 KiB
JavaScript
// Generates the Tauri app icons (PNG + ICO) with a hand-drawn "relay"
|
|
// glyph: two arrows passing each other on a teal tile.
|
|
// Run: node scripts/gen-icons.mjs
|
|
import zlib from "node:zlib";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const outDir = path.join(
|
|
path.dirname(path.dirname(fileURLToPath(import.meta.url))),
|
|
"src-tauri",
|
|
"icons",
|
|
);
|
|
|
|
function crc32(buf) {
|
|
let c = ~0;
|
|
for (let i = 0; i < buf.length; i++) {
|
|
c ^= buf[i];
|
|
for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
|
|
}
|
|
return ~c >>> 0;
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length);
|
|
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(body));
|
|
return Buffer.concat([len, body, crc]);
|
|
}
|
|
|
|
function png(width, height, rgba) {
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(width, 0);
|
|
ihdr.writeUInt32BE(height, 4);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 6; // color type RGBA
|
|
const raw = Buffer.alloc((width * 4 + 1) * height);
|
|
for (let y = 0; y < height; y++) {
|
|
raw[y * (width * 4 + 1)] = 0; // filter: none
|
|
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
|
|
}
|
|
return Buffer.concat([
|
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
chunk("IHDR", ihdr),
|
|
chunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
|
|
chunk("IEND", Buffer.alloc(0)),
|
|
]);
|
|
}
|
|
|
|
const TEAL = [13, 148, 136]; // #0d9488
|
|
const TEAL_DARK = [15, 118, 110]; // #0f766e
|
|
const WHITE = [255, 255, 255];
|
|
|
|
function drawRgba(size) {
|
|
const rgba = Buffer.alloc(size * size * 4);
|
|
const radius = size * 0.22;
|
|
const center = (size - 1) / 2;
|
|
const bar = Math.max(1, Math.round(size * 0.075));
|
|
const headLong = size * 0.17;
|
|
const headShort = size * 0.11;
|
|
const gap = size * 0.1;
|
|
const armLen = size * 0.3;
|
|
|
|
const inTile = (x, y) => {
|
|
const dx = Math.min(x, size - 1 - x);
|
|
const dy = Math.min(y, size - 1 - y);
|
|
if (dx >= radius || dy >= radius) return true;
|
|
const cx = dx - (radius - 1);
|
|
const cy = dy - (radius - 1);
|
|
return cx * cx + cy * cy <= 1;
|
|
};
|
|
|
|
const inArrow = (x, y, dir) => {
|
|
// dir = 1: arrow points right (top), -1: points left (bottom)
|
|
const cy = dir === 1 ? center - gap : center + gap;
|
|
const tipX = center + dir * (armLen + headLong);
|
|
const baseX = center - dir * armLen;
|
|
// shaft
|
|
if (Math.abs(y - cy) <= bar / 2 && x >= Math.min(baseX, tipX) && x <= Math.max(baseX, tipX)) {
|
|
return true;
|
|
}
|
|
// head: triangle growing away from the shaft
|
|
const headStart = dir === 1 ? tipX - headLong : tipX;
|
|
const headEnd = dir === 1 ? tipX : tipX + headLong;
|
|
if (x < Math.min(headStart, headEnd) || x > Math.max(headStart, headEnd)) return false;
|
|
const t = dir === 1 ? (x - headStart) / headLong : (headEnd - x) / headLong;
|
|
return Math.abs(y - cy) <= (bar / 2) * (1 - t) + headShort * t;
|
|
};
|
|
|
|
for (let y = 0; y < size; y++) {
|
|
for (let x = 0; x < size; x++) {
|
|
const i = (y * size + x) * 4;
|
|
if (!inTile(x, y)) {
|
|
rgba[i + 3] = 0;
|
|
continue;
|
|
}
|
|
// subtle vertical gradient on the tile
|
|
const mix = y / size;
|
|
const base = TEAL.map((c, k) => Math.round(c * (1 - mix) + TEAL_DARK[k] * mix));
|
|
let color = base;
|
|
if (inArrow(x, y, 1) || inArrow(x, y, -1)) color = WHITE;
|
|
rgba[i] = color[0];
|
|
rgba[i + 1] = color[1];
|
|
rgba[i + 2] = color[2];
|
|
rgba[i + 3] = 255;
|
|
}
|
|
}
|
|
return rgba;
|
|
}
|
|
|
|
function drawIcon(size) {
|
|
return png(size, size, drawRgba(size));
|
|
}
|
|
|
|
// Converts an RGBA buffer (top-down) into a bottom-up BGRA DIB plus a
|
|
// transparent AND mask, the classic BMP-in-ICO layout RC.exe expects.
|
|
function dibFromRgba(size, rgba) {
|
|
const header = Buffer.alloc(40);
|
|
header.writeUInt32LE(40, 0);
|
|
header.writeInt32LE(size, 4);
|
|
header.writeInt32LE(size * 2, 8); // height includes the AND mask
|
|
header.writeUInt16LE(1, 12);
|
|
header.writeUInt16LE(32, 14);
|
|
const pixels = Buffer.alloc(size * size * 4);
|
|
for (let y = 0; y < size; y++) {
|
|
const srcRow = (size - 1 - y) * size * 4;
|
|
rgba.copy(pixels, y * size * 4, srcRow, srcRow + size * 4);
|
|
for (let x = 0; x < size; x++) {
|
|
const i = y * size * 4 + x * 4;
|
|
const r = pixels[i];
|
|
pixels[i] = pixels[i + 2];
|
|
pixels[i + 2] = r; // RGBA -> BGRA
|
|
}
|
|
}
|
|
const maskRowBytes = Math.ceil(size / 8 / 4) * 4 || 4;
|
|
const mask = Buffer.alloc(maskRowBytes * size); // all zero: fully opaque
|
|
return Buffer.concat([header, pixels, mask]);
|
|
}
|
|
|
|
function ico(entries) {
|
|
// entries: [{ size, dib }]
|
|
const header = Buffer.alloc(6);
|
|
header.writeUInt16LE(0, 0);
|
|
header.writeUInt16LE(1, 2);
|
|
header.writeUInt16LE(entries.length, 4);
|
|
let offset = 6 + entries.length * 16;
|
|
const dirEntries = entries.map(({ size, dib }) => {
|
|
const entry = Buffer.alloc(16);
|
|
entry[0] = size >= 256 ? 0 : size;
|
|
entry[1] = size >= 256 ? 0 : size;
|
|
entry[2] = 0;
|
|
entry[3] = 0;
|
|
entry.writeUInt16LE(1, 4);
|
|
entry.writeUInt16LE(32, 6);
|
|
entry.writeUInt32LE(dib.length, 8);
|
|
entry.writeUInt32LE(offset, 12);
|
|
offset += dib.length;
|
|
return entry;
|
|
});
|
|
return Buffer.concat([header, ...dirEntries, ...entries.map((e) => e.dib)]);
|
|
}
|
|
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
const sizes = [16, 32, 48, 256];
|
|
const dibs = sizes.map((size) => ({ size, dib: dibFromRgba(size, drawRgba(size)) }));
|
|
fs.writeFileSync(path.join(outDir, "32x32.png"), drawIcon(32));
|
|
fs.writeFileSync(path.join(outDir, "128x128.png"), drawIcon(128));
|
|
fs.writeFileSync(path.join(outDir, "128x128@2x.png"), drawIcon(256));
|
|
fs.writeFileSync(path.join(outDir, "icon.ico"), ico(dibs));
|
|
console.log(`icons written to ${outDir}`);
|