diff --git a/extensions/msteams/src/store-fs.ts b/extensions/msteams/src/store-fs.ts index fdeb4c663c..75ce75235b 100644 --- a/extensions/msteams/src/store-fs.ts +++ b/extensions/msteams/src/store-fs.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { safeParseJson } from "openclaw/plugin-sdk"; import lockfile from "proper-lockfile"; const STORE_LOCK_OPTIONS = { @@ -14,14 +15,6 @@ const STORE_LOCK_OPTIONS = { stale: 30_000, } as const; -function safeParseJson(raw: string): T | null { - try { - return JSON.parse(raw) as T; - } catch { - return null; - } -} - export async function readJsonFile( filePath: string, fallback: T, diff --git a/scripts/analyze_code_files.py b/scripts/analyze_code_files.py index 66e48a2971..027f0aefbb 100644 --- a/scripts/analyze_code_files.py +++ b/scripts/analyze_code_files.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Lists the longest and shortest code files in the project. +Lists the longest and shortest code files in the project, and counts duplicated function names across files. Useful for identifying potential refactoring targets and enforcing code size guidelines. Threshold can be set to warn about files longer or shorter than a certain number of lines. """ diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index a3ad3460ed..6c69c59392 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -25,7 +25,6 @@ export function isContextOverflowError(errorMessage?: string): boolean { lower.includes("exceeds model context window") || (hasRequestSizeExceeds && hasContextWindow) || lower.includes("context overflow:") || - lower.includes("context overflow") || (lower.includes("413") && lower.includes("too large")) ); } diff --git a/src/gateway/live-image-probe.ts b/src/gateway/live-image-probe.ts index 883d0ac41e..eefeecdaf0 100644 --- a/src/gateway/live-image-probe.ts +++ b/src/gateway/live-image-probe.ts @@ -1,88 +1,4 @@ -import { deflateSync } from "node:zlib"; - -const CRC_TABLE = (() => { - const table = new Uint32Array(256); - for (let i = 0; i < 256; i += 1) { - let c = i; - for (let k = 0; k < 8; k += 1) { - c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; - } - table[i] = c >>> 0; - } - return table; -})(); - -function crc32(buf: Buffer) { - let crc = 0xffffffff; - for (let i = 0; i < buf.length; i += 1) { - crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); - } - return (crc ^ 0xffffffff) >>> 0; -} - -function pngChunk(type: string, data: Buffer) { - const typeBuf = Buffer.from(type, "ascii"); - const len = Buffer.alloc(4); - len.writeUInt32BE(data.length, 0); - const crc = crc32(Buffer.concat([typeBuf, data])); - const crcBuf = Buffer.alloc(4); - crcBuf.writeUInt32BE(crc, 0); - return Buffer.concat([len, typeBuf, data, crcBuf]); -} - -function encodePngRgba(buffer: Buffer, width: number, height: number) { - const stride = width * 4; - const raw = Buffer.alloc((stride + 1) * height); - for (let row = 0; row < height; row += 1) { - const rawOffset = row * (stride + 1); - raw[rawOffset] = 0; // filter: none - buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride); - } - const compressed = deflateSync(raw); - - const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 8; // bit depth - ihdr[9] = 6; // color type RGBA - ihdr[10] = 0; // compression - ihdr[11] = 0; // filter - ihdr[12] = 0; // interlace - - return Buffer.concat([ - signature, - pngChunk("IHDR", ihdr), - pngChunk("IDAT", compressed), - pngChunk("IEND", Buffer.alloc(0)), - ]); -} - -function fillPixel( - buf: Buffer, - x: number, - y: number, - width: number, - r: number, - g: number, - b: number, - a = 255, -) { - if (x < 0 || y < 0) { - return; - } - if (x >= width) { - return; - } - const idx = (y * width + x) * 4; - if (idx < 0 || idx + 3 >= buf.length) { - return; - } - buf[idx] = r; - buf[idx + 1] = g; - buf[idx + 2] = b; - buf[idx + 3] = a; -} +import { encodePngRgba, fillPixel } from "../media/png-encode.js"; const GLYPH_ROWS_5X7: Record = { "0": [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110], diff --git a/src/media/png-encode.ts b/src/media/png-encode.ts new file mode 100644 index 0000000000..a456ac30a2 --- /dev/null +++ b/src/media/png-encode.ts @@ -0,0 +1,90 @@ +/** + * Minimal PNG encoder for generating simple RGBA images without native dependencies. + * Used for QR codes, live probes, and other programmatic image generation. + */ +import { deflateSync } from "node:zlib"; + +const CRC_TABLE = (() => { + const table = new Uint32Array(256); + for (let i = 0; i < 256; i += 1) { + let c = i; + for (let k = 0; k < 8; k += 1) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[i] = c >>> 0; + } + return table; +})(); + +/** Compute CRC32 checksum for a buffer (used in PNG chunk encoding). */ +export function crc32(buf: Buffer): number { + let crc = 0xffffffff; + for (let i = 0; i < buf.length; i += 1) { + crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +/** Create a PNG chunk with type, data, and CRC. */ +export function pngChunk(type: string, data: Buffer): Buffer { + const typeBuf = Buffer.from(type, "ascii"); + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + const crc = crc32(Buffer.concat([typeBuf, data])); + const crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(crc, 0); + return Buffer.concat([len, typeBuf, data, crcBuf]); +} + +/** Write a pixel to an RGBA buffer. Ignores out-of-bounds writes. */ +export function fillPixel( + buf: Buffer, + x: number, + y: number, + width: number, + r: number, + g: number, + b: number, + a = 255, +): void { + if (x < 0 || y < 0 || x >= width) { + return; + } + const idx = (y * width + x) * 4; + if (idx < 0 || idx + 3 >= buf.length) { + return; + } + buf[idx] = r; + buf[idx + 1] = g; + buf[idx + 2] = b; + buf[idx + 3] = a; +} + +/** Encode an RGBA buffer as a PNG image. */ +export function encodePngRgba(buffer: Buffer, width: number, height: number): Buffer { + const stride = width * 4; + const raw = Buffer.alloc((stride + 1) * height); + for (let row = 0; row < height; row += 1) { + const rawOffset = row * (stride + 1); + raw[rawOffset] = 0; // filter: none + buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride); + } + const compressed = deflateSync(raw); + + const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(width, 0); + ihdr.writeUInt32BE(height, 4); + ihdr[8] = 8; // bit depth + ihdr[9] = 6; // color type RGBA + ihdr[10] = 0; // compression + ihdr[11] = 0; // filter + ihdr[12] = 0; // interlace + + return Buffer.concat([ + signature, + pngChunk("IHDR", ihdr), + pngChunk("IDAT", compressed), + pngChunk("IEND", Buffer.alloc(0)), + ]); +} diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index c529df2454..b3f629d11d 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -7,6 +7,7 @@ import type { ChannelId, ChannelPairingAdapter } from "../channels/plugins/types import { getPairingAdapter } from "../channels/plugins/pairing.js"; import { resolveOAuthDir, resolveStateDir } from "../config/paths.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; +import { safeParseJson } from "../utils.js"; const PAIRING_CODE_LENGTH = 8; const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; @@ -72,14 +73,6 @@ function resolveAllowFromPath( return path.join(resolveCredentialsDir(env), `${safeChannelKey(channel)}-allowFrom.json`); } -function safeParseJson(raw: string): T | null { - try { - return JSON.parse(raw) as T; - } catch { - return null; - } -} - async function readJsonFile( filePath: string, fallback: T, diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 67c95c13c8..7fd2a04b4d 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -229,7 +229,7 @@ export { } from "../agents/tools/common.js"; export { formatDocsLink } from "../terminal/links.js"; export type { HookEntry } from "../hooks/types.js"; -export { clamp, escapeRegExp, normalizeE164, sleep } from "../utils.js"; +export { clamp, escapeRegExp, normalizeE164, safeParseJson, sleep } from "../utils.js"; export { stripAnsi } from "../terminal/ansi.js"; export { missingTargetError } from "../infra/outbound/target-errors.js"; export { registerLogTransport } from "../logging/logger.js"; diff --git a/src/utils.ts b/src/utils.ts index 17fa7a3d32..dbbdb40269 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,6 +31,17 @@ export function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +/** + * Safely parse JSON, returning null on error instead of throwing. + */ +export function safeParseJson(raw: string): T | null { + try { + return JSON.parse(raw) as T; + } catch { + return null; + } +} + export type WebChannel = "web"; export function assertWebChannel(input: string): asserts input is WebChannel { diff --git a/src/web/qr-image.ts b/src/web/qr-image.ts index e60b0be67d..0def0d5ac7 100644 --- a/src/web/qr-image.ts +++ b/src/web/qr-image.ts @@ -1,6 +1,6 @@ -import { deflateSync } from "node:zlib"; import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js"; import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js"; +import { encodePngRgba, fillPixel } from "../media/png-encode.js"; type QRCodeConstructor = new ( typeNumber: number, @@ -22,83 +22,6 @@ function createQrMatrix(input: string) { return qr; } -function fillPixel( - buf: Buffer, - x: number, - y: number, - width: number, - r: number, - g: number, - b: number, - a = 255, -) { - const idx = (y * width + x) * 4; - buf[idx] = r; - buf[idx + 1] = g; - buf[idx + 2] = b; - buf[idx + 3] = a; -} - -function crcTable() { - const table = new Uint32Array(256); - for (let i = 0; i < 256; i += 1) { - let c = i; - for (let k = 0; k < 8; k += 1) { - c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; - } - table[i] = c >>> 0; - } - return table; -} - -const CRC_TABLE = crcTable(); - -function crc32(buf: Buffer) { - let crc = 0xffffffff; - for (let i = 0; i < buf.length; i += 1) { - crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); - } - return (crc ^ 0xffffffff) >>> 0; -} - -function pngChunk(type: string, data: Buffer) { - const typeBuf = Buffer.from(type, "ascii"); - const len = Buffer.alloc(4); - len.writeUInt32BE(data.length, 0); - const crc = crc32(Buffer.concat([typeBuf, data])); - const crcBuf = Buffer.alloc(4); - crcBuf.writeUInt32BE(crc, 0); - return Buffer.concat([len, typeBuf, data, crcBuf]); -} - -function encodePngRgba(buffer: Buffer, width: number, height: number) { - const stride = width * 4; - const raw = Buffer.alloc((stride + 1) * height); - for (let row = 0; row < height; row += 1) { - const rawOffset = row * (stride + 1); - raw[rawOffset] = 0; // filter: none - buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride); - } - const compressed = deflateSync(raw); - - const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 8; // bit depth - ihdr[9] = 6; // color type RGBA - ihdr[10] = 0; // compression - ihdr[11] = 0; // filter - ihdr[12] = 0; // interlace - - return Buffer.concat([ - signature, - pngChunk("IHDR", ihdr), - pngChunk("IDAT", compressed), - pngChunk("IEND", Buffer.alloc(0)), - ]); -} - export async function renderQrPngBase64( input: string, opts: { scale?: number; marginModules?: number } = {},