mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 06:37:28 +00:00
refactor: consolidate PNG encoder and safeParseJson utilities (#12457)
- Create shared PNG encoder module (src/media/png-encode.ts) - Refactor qr-image.ts and live-image-probe.ts to use shared encoder - Add safeParseJson to utils.ts and plugin-sdk exports - Update msteams and pairing-store to use centralized safeParseJson
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { safeParseJson } from "openclaw/plugin-sdk";
|
||||||
import lockfile from "proper-lockfile";
|
import lockfile from "proper-lockfile";
|
||||||
|
|
||||||
const STORE_LOCK_OPTIONS = {
|
const STORE_LOCK_OPTIONS = {
|
||||||
@@ -14,14 +15,6 @@ const STORE_LOCK_OPTIONS = {
|
|||||||
stale: 30_000,
|
stale: 30_000,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function safeParseJson<T>(raw: string): T | null {
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw) as T;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readJsonFile<T>(
|
export async function readJsonFile<T>(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
fallback: T,
|
fallback: T,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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.
|
Threshold can be set to warn about files longer or shorter than a certain number of lines.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export function isContextOverflowError(errorMessage?: string): boolean {
|
|||||||
lower.includes("exceeds model context window") ||
|
lower.includes("exceeds model context window") ||
|
||||||
(hasRequestSizeExceeds && hasContextWindow) ||
|
(hasRequestSizeExceeds && hasContextWindow) ||
|
||||||
lower.includes("context overflow:") ||
|
lower.includes("context overflow:") ||
|
||||||
lower.includes("context overflow") ||
|
|
||||||
(lower.includes("413") && lower.includes("too large"))
|
(lower.includes("413") && lower.includes("too large"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +1,4 @@
|
|||||||
import { deflateSync } from "node:zlib";
|
import { encodePngRgba, fillPixel } from "../media/png-encode.js";
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLYPH_ROWS_5X7: Record<string, number[]> = {
|
const GLYPH_ROWS_5X7: Record<string, number[]> = {
|
||||||
"0": [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
|
"0": [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
|
||||||
|
|||||||
90
src/media/png-encode.ts
Normal file
90
src/media/png-encode.ts
Normal file
@@ -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)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import type { ChannelId, ChannelPairingAdapter } from "../channels/plugins/types
|
|||||||
import { getPairingAdapter } from "../channels/plugins/pairing.js";
|
import { getPairingAdapter } from "../channels/plugins/pairing.js";
|
||||||
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
||||||
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
||||||
|
import { safeParseJson } from "../utils.js";
|
||||||
|
|
||||||
const PAIRING_CODE_LENGTH = 8;
|
const PAIRING_CODE_LENGTH = 8;
|
||||||
const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
@@ -72,14 +73,6 @@ function resolveAllowFromPath(
|
|||||||
return path.join(resolveCredentialsDir(env), `${safeChannelKey(channel)}-allowFrom.json`);
|
return path.join(resolveCredentialsDir(env), `${safeChannelKey(channel)}-allowFrom.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeParseJson<T>(raw: string): T | null {
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw) as T;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readJsonFile<T>(
|
async function readJsonFile<T>(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
fallback: T,
|
fallback: T,
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export {
|
|||||||
} from "../agents/tools/common.js";
|
} from "../agents/tools/common.js";
|
||||||
export { formatDocsLink } from "../terminal/links.js";
|
export { formatDocsLink } from "../terminal/links.js";
|
||||||
export type { HookEntry } from "../hooks/types.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 { stripAnsi } from "../terminal/ansi.js";
|
||||||
export { missingTargetError } from "../infra/outbound/target-errors.js";
|
export { missingTargetError } from "../infra/outbound/target-errors.js";
|
||||||
export { registerLogTransport } from "../logging/logger.js";
|
export { registerLogTransport } from "../logging/logger.js";
|
||||||
|
|||||||
11
src/utils.ts
11
src/utils.ts
@@ -31,6 +31,17 @@ export function escapeRegExp(value: string): string {
|
|||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely parse JSON, returning null on error instead of throwing.
|
||||||
|
*/
|
||||||
|
export function safeParseJson<T>(raw: string): T | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type WebChannel = "web";
|
export type WebChannel = "web";
|
||||||
|
|
||||||
export function assertWebChannel(input: string): asserts input is WebChannel {
|
export function assertWebChannel(input: string): asserts input is WebChannel {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { deflateSync } from "node:zlib";
|
|
||||||
import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js";
|
import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js";
|
||||||
import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js";
|
import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js";
|
||||||
|
import { encodePngRgba, fillPixel } from "../media/png-encode.js";
|
||||||
|
|
||||||
type QRCodeConstructor = new (
|
type QRCodeConstructor = new (
|
||||||
typeNumber: number,
|
typeNumber: number,
|
||||||
@@ -22,83 +22,6 @@ function createQrMatrix(input: string) {
|
|||||||
return qr;
|
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(
|
export async function renderQrPngBase64(
|
||||||
input: string,
|
input: string,
|
||||||
opts: { scale?: number; marginModules?: number } = {},
|
opts: { scale?: number; marginModules?: number } = {},
|
||||||
|
|||||||
Reference in New Issue
Block a user