build(deps): trim runtime dependency graph

This commit is contained in:
Peter Steinberger
2026-04-29 08:11:34 +01:00
parent 023d3371a5
commit 7e5d6dba80
20 changed files with 252 additions and 106 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Plugin SDK: mark remaining legacy alias exports and diffs tool/config aliases with deprecation metadata, and add a guard so future legacy alias comments require `@deprecated` tags. Thanks @vincentkoc.
- CLI/QR/dependencies: internalize small terminal progress and QR wrapper helpers while keeping the real QR encoder dependency direct, reducing the default runtime dependency graph without changing QR output behavior. Thanks @vincentkoc.
- Channels: add Yuanbao channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.
- Active Memory: add optional per-conversation `allowedChatIds` and `deniedChatIds` filters so operators can enable recall only for selected direct, group, or channel conversations while keeping broad sessions skipped. (#67977) Thanks @quengh.
- Active Memory: return bounded partial recall summaries when the hidden memory sub-agent times out, including the default temporary-transcript path, so useful recovered context is not discarded. (#73219) Thanks @joeykrug.

View File

@@ -10,11 +10,11 @@
"express": "5.2.1",
"playwright-core": "1.59.1",
"typebox": "1.1.33",
"undici": "8.1.0",
"ws": "^8.20.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
"@openclaw/plugin-sdk": "workspace:*",
"undici": "8.1.0"
},
"openclaw": {
"extensions": [

View File

@@ -4,9 +4,6 @@
"private": true,
"description": "OpenClaw Deepgram media-understanding provider",
"type": "module",
"dependencies": {
"ws": "^8.20.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},

View File

@@ -4,9 +4,6 @@
"private": true,
"description": "OpenClaw ElevenLabs speech plugin",
"type": "module",
"dependencies": {
"ws": "^8.20.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},

View File

@@ -4,9 +4,6 @@
"private": true,
"description": "OpenClaw Mistral provider plugin",
"type": "module",
"dependencies": {
"ws": "^8.20.0"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},

View File

@@ -6,11 +6,11 @@
"type": "module",
"dependencies": {
"@mariozechner/pi-ai": "0.70.5",
"typebox": "1.1.33",
"ws": "^8.20.0"
"typebox": "1.1.33"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
"@openclaw/plugin-sdk": "workspace:*",
"ws": "^8.20.0"
},
"openclaw": {
"extensions": [

View File

@@ -1605,7 +1605,6 @@
"@mariozechner/pi-coding-agent": "0.70.5",
"@mariozechner/pi-tui": "0.70.5",
"@modelcontextprotocol/sdk": "1.29.0",
"@vincentkoc/qrcode-tui": "0.2.1",
"ajv": "^8.20.0",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
@@ -1621,8 +1620,8 @@
"jszip": "^3.10.1",
"markdown-it": "14.1.1",
"openai": "^6.34.0",
"osc-progress": "^0.3.0",
"proxy-agent": "^8.0.1",
"qrcode": "1.5.4",
"semver": "7.7.4",
"sqlite-vec": "0.1.9",
"tar": "7.5.13",

48
pnpm-lock.yaml generated
View File

@@ -66,9 +66,6 @@ importers:
'@modelcontextprotocol/sdk':
specifier: 1.29.0
version: 1.29.0(zod@4.3.6)
'@vincentkoc/qrcode-tui':
specifier: 0.2.1
version: 0.2.1
ajv:
specifier: ^8.20.0
version: 8.20.0
@@ -114,12 +111,12 @@ importers:
openai:
specifier: ^6.34.0
version: 6.34.0(ws@8.20.0)(zod@4.3.6)
osc-progress:
specifier: ^0.3.0
version: 0.3.0
proxy-agent:
specifier: ^8.0.1
version: 8.0.1
qrcode:
specifier: 1.5.4
version: 1.5.4
semver:
specifier: 7.7.4
version: 7.7.4
@@ -356,9 +353,6 @@ importers:
typebox:
specifier: 1.1.33
version: 1.1.33
undici:
specifier: 8.1.0
version: 8.1.0
ws:
specifier: ^8.20.0
version: 8.20.0
@@ -366,6 +360,9 @@ importers:
'@openclaw/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk
undici:
specifier: 8.1.0
version: 8.1.0
extensions/byteplus:
devDependencies:
@@ -426,10 +423,6 @@ importers:
version: link:../../packages/plugin-sdk
extensions/deepgram:
dependencies:
ws:
specifier: ^8.20.0
version: 8.20.0
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
@@ -566,10 +559,6 @@ importers:
version: link:../../packages/plugin-sdk
extensions/elevenlabs:
dependencies:
ws:
specifier: ^8.20.0
version: 8.20.0
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
@@ -946,10 +935,6 @@ importers:
version: link:../../packages/plugin-sdk
extensions/mistral:
dependencies:
ws:
specifier: ^8.20.0
version: 8.20.0
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
@@ -1492,13 +1477,13 @@ importers:
typebox:
specifier: 1.1.33
version: 1.1.33
ws:
specifier: ^8.20.0
version: 8.20.0
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk
ws:
specifier: ^8.20.0
version: 8.20.0
extensions/xiaomi:
devDependencies:
@@ -4285,11 +4270,6 @@ packages:
resolution: {integrity: sha512-N8/FHc/lmlMDCumMuTXyRHCxlov5KZY6unmJ9QR2GOw+OpROZMBsXYGwE+ZMtvN21ql9+Xb8KhGNBj08IrG3Wg==}
engines: {node: '>=16', npm: '>=8'}
'@vincentkoc/qrcode-tui@0.2.1':
resolution: {integrity: sha512-F2XVHMfasJ0q8G93gtcyU9Px0wMH6o6nIZLrZYSHc6dm9Pq3oCbHuVYYG/UQvJD0rhrGH3P9B6qgpCAqSDUw5w==}
engines: {node: '>=20'}
hasBin: true
'@vitest/browser-playwright@4.1.5':
resolution: {integrity: sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==}
peerDependencies:
@@ -6401,10 +6381,6 @@ packages:
opusscript@0.1.1:
resolution: {integrity: sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==}
osc-progress@0.3.0:
resolution: {integrity: sha512-4/8JfsetakdeEa4vAYV45FW20aY+B/+K8NEXp5Eiar3wR8726whgHrbSg5Ar/ZY1FLJ/AGtUqV7W2IVF+Gvp9A==}
engines: {node: '>=20'}
oxfmt@0.46.0:
resolution: {integrity: sha512-CopwJOwPAjZ9p76fCvz+mSOJTw9/NY3cSksZK3VO/bUQ8UoEcketNgUuYS0UB3p+R9XnXe7wGGXUmyFxc7QxJA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -11061,10 +11037,6 @@ snapshots:
'@urbit/aura@3.0.0': {}
'@vincentkoc/qrcode-tui@0.2.1':
dependencies:
qrcode: 1.5.4
'@vitest/browser-playwright@4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.5)':
dependencies:
'@vitest/browser': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.5)
@@ -13571,8 +13543,6 @@ snapshots:
opusscript@0.1.1: {}
osc-progress@0.3.0: {}
oxfmt@0.46.0:
dependencies:
tinypool: 2.1.0

View File

@@ -21,5 +21,5 @@ docker_build_run qr-import-build \
-f "$ROOT_DIR/scripts/e2e/Dockerfile.qr-import" \
"$ROOT_DIR"
echo "Running qrcode-tui import smoke..."
run_logged qr-import-run docker run --rm -t "$IMAGE_NAME" node -e "import('@vincentkoc/qrcode-tui').then(async (m)=>{process.stdout.write(await m.renderTerminal('qr-smoke',{small:true}))})"
echo "Running qrcode import smoke..."
run_logged qr-import-run docker run --rm -t "$IMAGE_NAME" node -e "import('qrcode').then(async (m)=>{const q=m.default??m;process.stdout.write(await q.toString('qr-smoke',{small:true,type:'terminal'}))})"

View File

@@ -56,11 +56,6 @@
],
"risk": ["native", "parser", "untrusted-files"]
},
"@vincentkoc/qrcode-tui": {
"owner": "core:qr-setup",
"class": "default-runtime-initially",
"risk": ["terminal-rendering"]
},
"ajv": {
"owner": "core:json-schema-validation",
"class": "core-runtime",
@@ -142,11 +137,6 @@
"class": "default-runtime-initially",
"risk": ["provider-sdk", "network"]
},
"osc-progress": {
"owner": "core:terminal-progress",
"class": "core-runtime",
"risk": ["terminal-rendering"]
},
"pdfjs-dist": {
"owner": "plugin:document-extract",
"class": "plugin-runtime",
@@ -158,6 +148,11 @@
"class": "core-runtime",
"risk": ["network", "proxy"]
},
"qrcode": {
"owner": "core:qr-setup",
"class": "default-runtime-initially",
"risk": ["terminal-rendering", "png-encoding"]
},
"semver": {
"owner": "core:package-versioning",
"class": "core-runtime",

View File

@@ -1,5 +1,5 @@
import { spinner } from "@clack/prompts";
import { createOscProgressController, supportsOscProgress } from "osc-progress";
import { createOscProgressController, supportsOscProgress } from "../terminal/osc-progress.js";
import {
clearActiveProgressLine,
registerActiveProgressLine,

View File

@@ -3,16 +3,20 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { MOCK_PNG_BASE64, renderPngBase64 } = vi.hoisted(() => {
const { MOCK_PNG_BASE64, MOCK_PNG_DATA_URL, toDataURL } = vi.hoisted(() => {
const MOCK_PNG_BASE64 = "ZmFrZXBuZw==";
const MOCK_PNG_DATA_URL = `data:image/png;base64,${MOCK_PNG_BASE64}`;
return {
MOCK_PNG_BASE64,
renderPngBase64: vi.fn(async () => MOCK_PNG_BASE64),
MOCK_PNG_DATA_URL,
toDataURL: vi.fn(async () => MOCK_PNG_DATA_URL),
};
});
vi.mock("@vincentkoc/qrcode-tui", () => ({
renderPngBase64,
vi.mock("qrcode", () => ({
default: {
toDataURL,
},
}));
import {
@@ -26,36 +30,40 @@ describe("renderQrPngBase64", () => {
const tmpRoot = path.join(os.tmpdir(), "openclaw-qr-image-tests");
beforeEach(() => {
renderPngBase64.mockClear();
toDataURL.mockClear();
toDataURL.mockResolvedValue(MOCK_PNG_DATA_URL);
});
afterEach(async () => {
await fs.rm(tmpRoot, { recursive: true, force: true });
});
it("delegates PNG rendering to qrcode-tui", async () => {
it("delegates PNG rendering to qrcode", async () => {
await expect(renderQrPngBase64("openclaw", { scale: 8, marginModules: 2 })).resolves.toBe(
MOCK_PNG_BASE64,
);
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
expect(toDataURL).toHaveBeenCalledWith("openclaw", {
margin: 2,
scale: 8,
type: "image/png",
});
});
it("uses the default PNG rendering options", async () => {
await renderQrPngBase64("openclaw");
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
expect(toDataURL).toHaveBeenCalledWith("openclaw", {
margin: 4,
scale: 6,
type: "image/png",
});
});
it("floors finite PNG rendering options before delegating", async () => {
await renderQrPngBase64("openclaw", { scale: 8.9, marginModules: 2.9 });
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
expect(toDataURL).toHaveBeenCalledWith("openclaw", {
margin: 2,
scale: 8,
type: "image/png",
});
});
@@ -68,7 +76,14 @@ describe("renderQrPngBase64", () => {
["marginModules", 6, Number.POSITIVE_INFINITY, "marginModules must be a finite number."],
])("rejects invalid %s values", async (_name, scale, marginModules, message) => {
await expect(renderQrPngBase64("openclaw", { scale, marginModules })).rejects.toThrow(message);
expect(renderPngBase64).not.toHaveBeenCalled();
expect(toDataURL).not.toHaveBeenCalled();
});
it("rejects non-PNG qrcode data URLs", async () => {
toDataURL.mockResolvedValue("data:image/svg+xml;base64,PHN2Zz4=");
await expect(renderQrPngBase64("openclaw")).rejects.toThrow(
"Expected qrcode to return a PNG data URL.",
);
});
it("formats QR PNG data URLs", async () => {
@@ -104,6 +119,6 @@ describe("renderQrPngBase64", () => {
fileName: opts.fileName,
}),
).rejects.toThrow(`${name} must be a non-empty filename segment.`);
expect(renderPngBase64).not.toHaveBeenCalled();
expect(toDataURL).not.toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,6 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { loadQrCodeTuiRuntime } from "./qr-runtime.ts";
import { loadQrCodeRuntime, normalizeQrText } from "./qr-runtime.ts";
const DEFAULT_QR_PNG_SCALE = 6;
const DEFAULT_QR_PNG_MARGIN_MODULES = 4;
@@ -72,11 +72,16 @@ export async function renderQrPngBase64(
min: MIN_QR_PNG_MARGIN_MODULES,
max: MAX_QR_PNG_MARGIN_MODULES,
});
const { renderPngBase64 } = await loadQrCodeTuiRuntime();
return await renderPngBase64(input, {
const qrCode = await loadQrCodeRuntime();
const dataUrl = await qrCode.toDataURL(normalizeQrText(input), {
margin: marginModules,
scale,
type: "image/png",
});
if (!dataUrl.startsWith(QR_PNG_DATA_URL_PREFIX)) {
throw new Error("Expected qrcode to return a PNG data URL.");
}
return dataUrl.slice(QR_PNG_DATA_URL_PREFIX.length);
}
export function formatQrPngDataUrl(base64: string): string {

View File

@@ -1,8 +1,22 @@
let qrCodeTuiRuntimePromise: Promise<typeof import("@vincentkoc/qrcode-tui")> | null = null;
import type QRCode from "qrcode";
export async function loadQrCodeTuiRuntime() {
if (!qrCodeTuiRuntimePromise) {
qrCodeTuiRuntimePromise = import("@vincentkoc/qrcode-tui");
type QrCodeRuntime = typeof QRCode;
let qrCodeRuntimePromise: Promise<QrCodeRuntime> | null = null;
export async function loadQrCodeRuntime(): Promise<QrCodeRuntime> {
if (!qrCodeRuntimePromise) {
qrCodeRuntimePromise = import("qrcode").then((mod) => mod.default ?? mod);
}
return await qrCodeTuiRuntimePromise;
return await qrCodeRuntimePromise;
}
export function normalizeQrText(text: string): string {
if (typeof text !== "string") {
throw new TypeError("QR text must be a string.");
}
if (text.length === 0) {
throw new Error("QR text must not be empty.");
}
return text;
}

View File

@@ -0,0 +1,28 @@
import { describe, expect, it, vi } from "vitest";
const { toString } = vi.hoisted(() => ({
toString: vi.fn(async () => "ASCII-QR"),
}));
vi.mock("qrcode", () => ({
default: {
toString,
},
}));
import { renderQrTerminal } from "./qr-terminal.ts";
describe("renderQrTerminal", () => {
it("delegates terminal rendering to qrcode", async () => {
await expect(renderQrTerminal("openclaw")).resolves.toBe("ASCII-QR");
expect(toString).toHaveBeenCalledWith("openclaw", {
small: true,
type: "terminal",
});
});
it("rejects empty QR text", async () => {
await expect(renderQrTerminal("")).rejects.toThrow("QR text must not be empty.");
expect(toString).not.toHaveBeenCalledWith("", expect.anything());
});
});

View File

@@ -1,9 +1,12 @@
import { loadQrCodeTuiRuntime } from "./qr-runtime.ts";
import { loadQrCodeRuntime, normalizeQrText } from "./qr-runtime.ts";
export async function renderQrTerminal(
input: string,
opts: { small?: boolean } = {},
): Promise<string> {
const { renderTerminal } = await loadQrCodeTuiRuntime();
return await renderTerminal(input, { small: opts.small ?? true });
const qrCode = await loadQrCodeRuntime();
return await qrCode.toString(normalizeQrText(input), {
small: opts.small ?? true,
type: "terminal",
});
}

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { createOscProgressController, supportsOscProgress } from "./osc-progress.js";
describe("OSC progress", () => {
it("detects supported terminal environments", () => {
expect(supportsOscProgress({ TERM_PROGRAM: "WezTerm" }, true)).toBe(true);
expect(supportsOscProgress({ TERM_PROGRAM: "Apple_Terminal" }, true)).toBe(false);
expect(supportsOscProgress({ WT_SESSION: "1" }, false)).toBe(false);
});
it("writes sanitized OSC 9;4 progress sequences", () => {
const writes: string[] = [];
const controller = createOscProgressController({
env: { TERM_PROGRAM: "ghostty" },
isTty: true,
write: (chunk) => writes.push(chunk),
});
controller.setIndeterminate("Build\u001b]bad\u0007");
controller.setPercent("Build", 42.6);
controller.clear();
expect(writes).toEqual([
"\u001b]9;4;3;;Buildbad\u001b\\",
"\u001b]9;4;1;43;Build\u001b\\",
"\u001b]9;4;0;0;Build\u001b\\",
]);
});
});

View File

@@ -0,0 +1,70 @@
const OSC_PROGRESS_PREFIX = "\u001b]9;4;";
const OSC_PROGRESS_ST = "\u001b\\";
const OSC_PROGRESS_BEL = "\u0007";
const OSC_PROGRESS_C1_ST = "\u009c";
export type OscProgressController = {
setIndeterminate: (label: string) => void;
setPercent: (label: string, percent: number) => void;
clear: () => void;
};
export function supportsOscProgress(env: NodeJS.ProcessEnv, isTty: boolean): boolean {
if (!isTty) {
return false;
}
const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
return (
termProgram.includes("ghostty") || termProgram.includes("wezterm") || Boolean(env.WT_SESSION)
);
}
function sanitizeOscProgressLabel(label: string): string {
return label
.replaceAll(OSC_PROGRESS_ST, "")
.replaceAll(OSC_PROGRESS_BEL, "")
.replaceAll(OSC_PROGRESS_C1_ST, "")
.split("\u001b")
.join("")
.replaceAll("]", "")
.trim();
}
function formatOscProgress(state: number, percent: number | null, label: string): string {
const cleanLabel = sanitizeOscProgressLabel(label);
if (percent === null) {
return `${OSC_PROGRESS_PREFIX}${state};;${cleanLabel}${OSC_PROGRESS_ST}`;
}
const normalizedPercent = Math.max(0, Math.min(100, Math.round(percent)));
return `${OSC_PROGRESS_PREFIX}${state};${normalizedPercent};${cleanLabel}${OSC_PROGRESS_ST}`;
}
export function createOscProgressController(params: {
env: NodeJS.ProcessEnv;
isTty: boolean;
write: (chunk: string) => void;
}): OscProgressController {
if (!supportsOscProgress(params.env, params.isTty)) {
return {
setIndeterminate: () => {},
setPercent: () => {},
clear: () => {},
};
}
let lastLabel = "";
return {
setIndeterminate: (label: string) => {
lastLabel = label;
params.write(formatOscProgress(3, null, label));
},
setPercent: (label: string, percent: number) => {
lastLabel = label;
params.write(formatOscProgress(1, percent, label));
},
clear: () => {
params.write(formatOscProgress(0, 0, lastLabel));
},
};
}

View File

@@ -1,16 +0,0 @@
declare module "osc-progress" {
export type OscProgressController = {
setIndeterminate: (label: string) => void;
setPercent: (label: string, percent: number) => void;
clear: () => void;
done?: () => void;
};
export function createOscProgressController(params: {
env: NodeJS.ProcessEnv;
isTty: boolean;
write: (chunk: string) => void;
}): OscProgressController;
export function supportsOscProgress(env: NodeJS.ProcessEnv, isTty: boolean): boolean;
}

42
src/types/qrcode.d.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
declare module "qrcode" {
export type QrCodeErrorCorrectionLevel =
| "L"
| "M"
| "Q"
| "H"
| "low"
| "medium"
| "quartile"
| "high";
export type QrCodeColorOptions = {
dark?: string;
light?: string;
};
export type QrCodeRenderOptions = {
color?: QrCodeColorOptions;
errorCorrectionLevel?: QrCodeErrorCorrectionLevel;
margin?: number;
scale?: number;
small?: boolean;
type?: "image/png" | "png" | "svg" | "terminal" | "utf8";
width?: number;
};
export function toString(text: string, options?: QrCodeRenderOptions): Promise<string>;
export function toDataURL(text: string, options?: QrCodeRenderOptions): Promise<string>;
export function toFile(
filePath: string,
text: string,
options?: QrCodeRenderOptions,
): Promise<void>;
const qrcode: {
toString: typeof toString;
toDataURL: typeof toDataURL;
toFile: typeof toFile;
};
export default qrcode;
}