mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
fix(qr): replace qrcode-terminal with qrcode-tui
Replace legacy qrcode-terminal usage with shared qrcode-tui media helpers, bound QR PNG rendering options, and raise bundled plugin host floors for the new SDK runtime surface.
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/QR: replace legacy `qrcode-terminal` QR rendering with bounded `qrcode-tui` helpers for plugin login/setup flows. (#65969) Thanks @vincentkoc.
|
||||
- Auto-reply/system events: route async exec-event completion replies through the persisted session delivery context, so long-running command results return to the originating channel instead of being dropped when live origin metadata is missing. (#70258) Thanks @wzfukui.
|
||||
- OpenAI/image generation: send reference-image edits as guarded multipart uploads instead of JSON data URLs, restoring complex multi-reference `gpt-image-2` edits. Fixes #70642. Thanks @dashhuang.
|
||||
- QA channel/security: reject non-HTTP(S) inbound attachment URLs before media fetch, and log rejected schemes so suspicious or misconfigured payloads are visible during debugging. (#70708) Thanks @vincentkoc.
|
||||
|
||||
@@ -16,7 +16,7 @@ Feishu/Lark is an all-in-one collaboration platform where teams chat, share docu
|
||||
|
||||
## Quick start
|
||||
|
||||
> **Requires OpenClaw 2026.4.10 or above.** Run `openclaw --version` to check. Upgrade with `openclaw update`.
|
||||
> **Requires OpenClaw 2026.4.23 or above.** Run `openclaw --version` to check. Upgrade with `openclaw update`.
|
||||
|
||||
<Steps>
|
||||
<Step title="Run the channel setup wizard">
|
||||
|
||||
@@ -258,6 +258,11 @@ const kind = api.runtime.media.mediaKindFromMime("image/jpeg"); // "image"
|
||||
const isVoice = api.runtime.media.isVoiceCompatibleAudio(filePath);
|
||||
const metadata = await api.runtime.media.getImageMetadata(filePath);
|
||||
const resized = await api.runtime.media.resizeToJpeg(buffer, { maxWidth: 800 });
|
||||
const terminalQr = await api.runtime.media.renderQrTerminal("https://openclaw.ai");
|
||||
const pngQr = await api.runtime.media.renderQrPngBase64("https://openclaw.ai", {
|
||||
scale: 6, // 1-12
|
||||
marginModules: 4, // 0-16
|
||||
});
|
||||
```
|
||||
|
||||
### `api.runtime.config`
|
||||
|
||||
@@ -124,7 +124,7 @@ This script drives the interactive wizard via a pseudo-tty, verifies config/work
|
||||
|
||||
## QR import smoke (Docker)
|
||||
|
||||
Ensures `qrcode-terminal` loads under the supported Docker Node runtimes (Node 24 default, Node 22 compatible):
|
||||
Ensures the maintained QR runtime helper loads under the supported Docker Node runtimes (Node 24 default, Node 22 compatible):
|
||||
|
||||
```bash
|
||||
pnpm test:docker:qr
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "^1.61.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"typebox": "1.1.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -13,7 +12,7 @@
|
||||
"openclaw": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.4.20"
|
||||
"openclaw": ">=2026.4.23"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -41,10 +40,10 @@
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/feishu",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
"minHostVersion": ">=2026.4.23"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.4.20"
|
||||
"pluginApi": ">=2026.4.23"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.4.20"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* Replaces axios with native fetch, removes inquirer/ora/chalk in favor of
|
||||
* the openclaw WizardPrompter surface.
|
||||
*/
|
||||
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { renderQrTerminal } from "./qr-terminal.js";
|
||||
import type { FeishuDomain } from "./types.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -252,9 +252,8 @@ export async function pollAppRegistration(params: {
|
||||
* otherwise the pattern is corrupted and cannot be scanned.
|
||||
*/
|
||||
export async function printQrCode(url: string): Promise<void> {
|
||||
const mod = await import("qrcode-terminal");
|
||||
const qrcode = mod.default ?? mod;
|
||||
qrcode.generate(url, { small: true });
|
||||
const output = await renderQrTerminal(url, { small: true });
|
||||
process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
1
extensions/feishu/src/qr-terminal.ts
Normal file
1
extensions/feishu/src/qr-terminal.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { renderQrTerminal } from "openclaw/plugin-sdk/media-runtime";
|
||||
12
extensions/feishu/src/qrcode-terminal.d.ts
vendored
12
extensions/feishu/src/qrcode-terminal.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
declare module "qrcode-terminal" {
|
||||
type GenerateOptions = {
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
type QrCodeTerminal = {
|
||||
generate: (input: string, options?: GenerateOptions, cb?: (output: string) => void) => void;
|
||||
};
|
||||
|
||||
const qrcode: QrCodeTerminal;
|
||||
export default qrcode;
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
"dependencies": {
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"jimp": "^1.6.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"typebox": "1.1.28",
|
||||
"undici": "8.1.0"
|
||||
},
|
||||
@@ -15,7 +14,7 @@
|
||||
"openclaw": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.4.20"
|
||||
"openclaw": ">=2026.4.23"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -54,10 +53,10 @@
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/whatsapp",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
"minHostVersion": ">=2026.4.23"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.4.20"
|
||||
"pluginApi": ">=2026.4.23"
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderQrPngBase64 } from "./qr-image.js";
|
||||
@@ -88,13 +86,4 @@ describe("renderQrPngBase64", () => {
|
||||
const buf = Buffer.from(b64, "base64");
|
||||
expect(buf.subarray(0, 8).toString("hex")).toBe("89504e470d0a1a0a");
|
||||
});
|
||||
|
||||
it("avoids dynamic require of qrcode-terminal vendor modules", async () => {
|
||||
const sourcePath = resolve(process.cwd(), "src/media/qr-image.ts");
|
||||
const source = await readFile(sourcePath, "utf-8");
|
||||
expect(source).not.toContain("createRequire(");
|
||||
expect(source).not.toContain('require("qrcode-terminal/vendor/QRCode")');
|
||||
expect(source).toContain("qrcode-terminal/vendor/QRCode/index.js");
|
||||
expect(source).toContain("qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js");
|
||||
});
|
||||
});
|
||||
|
||||
1
extensions/whatsapp/src/qr-terminal.ts
Normal file
1
extensions/whatsapp/src/qr-terminal.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { renderQrTerminal } from "openclaw/plugin-sdk/media-runtime";
|
||||
12
extensions/whatsapp/src/qrcode-terminal.d.ts
vendored
12
extensions/whatsapp/src/qrcode-terminal.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
declare module "qrcode-terminal" {
|
||||
type GenerateOptions = {
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
type QrCodeTerminal = {
|
||||
generate: (input: string, options?: GenerateOptions, cb?: (output: string) => void) => void;
|
||||
};
|
||||
|
||||
const qrcode: QrCodeTerminal;
|
||||
export default qrcode;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
writeCredsJsonAtomically,
|
||||
type CredsQueueWaitResult,
|
||||
} from "./creds-persistence.js";
|
||||
import { renderQrTerminal } from "./qr-terminal.js";
|
||||
import { formatError, getStatusCode } from "./session-errors.js";
|
||||
import {
|
||||
DisconnectReason,
|
||||
@@ -60,11 +61,6 @@ const LOGGED_OUT_STATUS = DisconnectReason?.loggedOut ?? 401;
|
||||
const CREDS_FLUSH_TIMEOUT_MESSAGE =
|
||||
"Queued WhatsApp creds save did not finish before auth bootstrap; skipping repair and continuing with primary creds.";
|
||||
|
||||
async function loadQrTerminal() {
|
||||
const mod = await import("qrcode-terminal");
|
||||
return mod.default ?? mod;
|
||||
}
|
||||
|
||||
function enqueueSaveCreds(
|
||||
authDir: string,
|
||||
saveCreds: () => Promise<void> | void,
|
||||
@@ -113,6 +109,11 @@ async function safeSaveCreds(
|
||||
}
|
||||
}
|
||||
|
||||
async function printTerminalQr(qr: string): Promise<void> {
|
||||
const output = await renderQrTerminal(qr, { small: true });
|
||||
process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Baileys socket backed by the multi-file auth store we keep on disk.
|
||||
* Consumers can opt into QR printing for interactive login flows.
|
||||
@@ -172,8 +173,9 @@ export async function createWaSocket(
|
||||
opts.onQr?.(qr);
|
||||
if (printQr) {
|
||||
console.log("Scan this QR in WhatsApp (Linked Devices):");
|
||||
const qrcode = await loadQrTerminal();
|
||||
qrcode.generate(qr, { small: true });
|
||||
void printTerminalQr(qr).catch((err) => {
|
||||
sessionLogger.warn({ error: String(err) }, "failed rendering WhatsApp QR");
|
||||
});
|
||||
}
|
||||
}
|
||||
if (connection === "close") {
|
||||
|
||||
@@ -610,9 +610,8 @@ vi.mock("./session.runtime.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("qrcode-terminal", () => ({
|
||||
default: { generate: vi.fn() },
|
||||
generate: vi.fn(),
|
||||
vi.mock("./qr-terminal.js", () => ({
|
||||
renderQrTerminal: vi.fn(async () => "ASCII-QR"),
|
||||
}));
|
||||
|
||||
export const baileys = await import("./session.runtime.js");
|
||||
|
||||
@@ -1553,6 +1553,7 @@
|
||||
"@mariozechner/pi-tui": "0.69.0",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@vincentkoc/qrcode-tui": "0.2.1",
|
||||
"ajv": "^8.18.0",
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
@@ -1573,7 +1574,6 @@
|
||||
"osc-progress": "^0.3.0",
|
||||
"pdfjs-dist": "^5.6.205",
|
||||
"proxy-agent": "^8.0.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"semver": "7.7.4",
|
||||
"sharp": "^0.34.5",
|
||||
"sqlite-vec": "0.1.9",
|
||||
@@ -1594,7 +1594,6 @@
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260423.1",
|
||||
"@vitest/coverage-v8": "^4.1.4",
|
||||
|
||||
172
pnpm-lock.yaml
generated
172
pnpm-lock.yaml
generated
@@ -75,6 +75,9 @@ importers:
|
||||
'@napi-rs/canvas':
|
||||
specifier: ^0.1.89
|
||||
version: 0.1.92
|
||||
'@vincentkoc/qrcode-tui':
|
||||
specifier: 0.2.1
|
||||
version: 0.2.1
|
||||
ajv:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
@@ -138,9 +141,6 @@ importers:
|
||||
proxy-agent:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
qrcode-terminal:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0
|
||||
semver:
|
||||
specifier: 7.7.4
|
||||
version: 7.7.4
|
||||
@@ -196,9 +196,6 @@ importers:
|
||||
'@types/node':
|
||||
specifier: 25.6.0
|
||||
version: 25.6.0
|
||||
'@types/qrcode-terminal':
|
||||
specifier: ^0.12.2
|
||||
version: 0.12.2
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
@@ -547,9 +544,6 @@ importers:
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.61.1
|
||||
version: 1.61.1
|
||||
qrcode-terminal:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0
|
||||
typebox:
|
||||
specifier: 1.1.28
|
||||
version: 1.1.28
|
||||
@@ -1343,9 +1337,6 @@ importers:
|
||||
jimp:
|
||||
specifier: ^1.6.1
|
||||
version: 1.6.1
|
||||
qrcode-terminal:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0
|
||||
typebox:
|
||||
specifier: 1.1.28
|
||||
version: 1.1.28
|
||||
@@ -4197,9 +4188,6 @@ packages:
|
||||
'@types/node@25.6.0':
|
||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||
|
||||
'@types/qrcode-terminal@0.12.2':
|
||||
resolution: {integrity: sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==}
|
||||
|
||||
'@types/qs@6.15.0':
|
||||
resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==}
|
||||
|
||||
@@ -4291,6 +4279,11 @@ 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.4':
|
||||
resolution: {integrity: sha512-q3PchVhZINX23Pv+RERgAtDlp6wzVkID/smOPnZ5YGWpeWUe3jMNYppeVh15j4il3G7JIJty1d1Kicpm0HSMig==}
|
||||
peerDependencies:
|
||||
@@ -4699,6 +4692,10 @@ packages:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
@@ -4772,6 +4769,9 @@ packages:
|
||||
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
|
||||
engines: {node: 10.* || >= 12.*}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
cliui@7.0.4:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
|
||||
@@ -4917,6 +4917,10 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
@@ -4982,6 +4986,9 @@ packages:
|
||||
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dijkstrajs@1.0.3:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
|
||||
discord-api-types@0.38.45:
|
||||
resolution: {integrity: sha512-DiI01i00FPv6n+hXcFkFxK8Y/rFRpKs6U6aP32N4T73nTbj37Eua3H/95TBpLktLWB6xnLXhYDGvyLq6zzYY2w==}
|
||||
|
||||
@@ -5274,6 +5281,10 @@ packages:
|
||||
resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
find-up@4.1.0:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
flatbuffers@24.12.23:
|
||||
resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==}
|
||||
|
||||
@@ -5907,6 +5918,10 @@ packages:
|
||||
lit@3.3.2:
|
||||
resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
lodash.camelcase@4.3.0:
|
||||
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||
|
||||
@@ -6456,6 +6471,14 @@ packages:
|
||||
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-locate@4.1.0:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-queue@6.6.2:
|
||||
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6484,6 +6507,10 @@ packages:
|
||||
resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pac-proxy-agent@7.2.0:
|
||||
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -6547,6 +6574,10 @@ packages:
|
||||
partial-json@0.1.7:
|
||||
resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-expression-matcher@1.5.0:
|
||||
resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -6618,6 +6649,10 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
pngjs@6.0.0:
|
||||
resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
@@ -6758,6 +6793,11 @@ packages:
|
||||
resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==}
|
||||
hasBin: true
|
||||
|
||||
qrcode@1.5.4:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
qs@6.14.2:
|
||||
resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -6870,6 +6910,9 @@ packages:
|
||||
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
||||
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
|
||||
@@ -7631,6 +7674,9 @@ packages:
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -7660,6 +7706,10 @@ packages:
|
||||
resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==}
|
||||
engines: {node: '>=12.17'}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7701,6 +7751,9 @@ packages:
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7717,6 +7770,10 @@ packages:
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yargs-parser@20.2.9:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7725,6 +7782,10 @@ packages:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@15.4.1:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
yargs@16.2.0:
|
||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -11095,8 +11156,6 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
|
||||
'@types/qrcode-terminal@0.12.2': {}
|
||||
|
||||
'@types/qs@6.15.0': {}
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
@@ -11172,6 +11231,10 @@ snapshots:
|
||||
|
||||
'@urbit/aura@3.0.0': {}
|
||||
|
||||
'@vincentkoc/qrcode-tui@0.2.1':
|
||||
dependencies:
|
||||
qrcode: 1.5.4
|
||||
|
||||
'@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.1.4(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
||||
@@ -11619,6 +11682,8 @@ snapshots:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
@@ -11681,6 +11746,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@colors/colors': 1.5.0
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
cliui@7.0.4:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -11820,6 +11891,8 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
decode-named-character-reference@1.3.0:
|
||||
@@ -11871,6 +11944,8 @@ snapshots:
|
||||
|
||||
diff@8.0.4: {}
|
||||
|
||||
dijkstrajs@1.0.3: {}
|
||||
|
||||
discord-api-types@0.38.45: {}
|
||||
|
||||
discord-api-types@0.38.47: {}
|
||||
@@ -12220,6 +12295,11 @@ snapshots:
|
||||
dependencies:
|
||||
array-back: 3.1.0
|
||||
|
||||
find-up@4.1.0:
|
||||
dependencies:
|
||||
locate-path: 5.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
flatbuffers@24.12.23: {}
|
||||
|
||||
follow-redirects@1.16.0: {}
|
||||
@@ -13019,6 +13099,10 @@ snapshots:
|
||||
lit-element: 4.2.2
|
||||
lit-html: 3.3.2
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
lodash.clonedeep@4.5.0: {}
|
||||
@@ -13811,6 +13895,14 @@ snapshots:
|
||||
|
||||
p-finally@1.0.0: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
p-locate@4.1.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-queue@6.6.2:
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
@@ -13838,6 +13930,8 @@ snapshots:
|
||||
|
||||
p-timeout@7.0.1: {}
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
pac-proxy-agent@7.2.0:
|
||||
dependencies:
|
||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||
@@ -13918,6 +14012,8 @@ snapshots:
|
||||
|
||||
partial-json@0.1.7: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-expression-matcher@1.5.0: {}
|
||||
|
||||
path-is-absolute@1.0.1:
|
||||
@@ -13983,6 +14079,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
pngjs@6.0.0: {}
|
||||
|
||||
pngjs@7.0.0: {}
|
||||
@@ -14165,6 +14263,12 @@ snapshots:
|
||||
|
||||
qrcode-terminal@0.12.0: {}
|
||||
|
||||
qrcode@1.5.4:
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
|
||||
qs@6.14.2:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
@@ -14311,6 +14415,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
requires-port@1.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -14448,8 +14554,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
set-blocking@2.0.0:
|
||||
optional: true
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
@@ -15089,6 +15194,8 @@ snapshots:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
@@ -15118,6 +15225,12 @@ snapshots:
|
||||
|
||||
wordwrapjs@5.1.1: {}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -15145,6 +15258,8 @@ snapshots:
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@4.0.0: {}
|
||||
@@ -15153,10 +15268,29 @@ snapshots:
|
||||
|
||||
yaml@2.8.3: {}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs-parser@20.2.9: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@15.4.1:
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 4.1.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 2.0.0
|
||||
set-blocking: 2.0.0
|
||||
string-width: 4.2.3
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
|
||||
yargs@16.2.0:
|
||||
dependencies:
|
||||
cliui: 7.0.4
|
||||
|
||||
@@ -17,7 +17,7 @@ COPY --chown=appuser:appuser package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY --chown=appuser:appuser ui/package.json ./ui/package.json
|
||||
COPY --chown=appuser:appuser patches ./patches
|
||||
|
||||
# This image only exercises the root qrcode-terminal dependency path.
|
||||
# This image only exercises the root QR runtime dependency path.
|
||||
# Keep the pre-install copy set limited to the manifests needed for root
|
||||
# workspace resolution so unrelated extension edits do not bust the layer.
|
||||
ARG OPENCLAW_QR_INSTALL_CACHE_BUSTER=stable
|
||||
|
||||
@@ -26,5 +26,5 @@ DOCKER_BUILD_CMD+=(
|
||||
)
|
||||
run_logged qr-import-build "${DOCKER_BUILD_CMD[@]}"
|
||||
|
||||
echo "Running qrcode-terminal import smoke..."
|
||||
run_logged qr-import-run docker run --rm -t "$IMAGE_NAME" node -e "import('qrcode-terminal').then((m)=>m.default.generate('qr-smoke',{small:true}))"
|
||||
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}))})"
|
||||
|
||||
@@ -10,9 +10,7 @@ const mocks = vi.hoisted(() => ({
|
||||
resolvedConfig: config,
|
||||
diagnostics: [] as string[],
|
||||
})),
|
||||
qrGenerate: vi.fn((_input: unknown, _opts: unknown, cb: (output: string) => void) => {
|
||||
cb("ASCII-QR");
|
||||
}),
|
||||
renderTerminal: vi.fn(async () => "ASCII-QR"),
|
||||
}));
|
||||
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
|
||||
const runtimeLog = runtime.log;
|
||||
@@ -27,6 +25,9 @@ vi.mock("../runtime.js", async () => {
|
||||
});
|
||||
vi.mock("../config/config.js", () => ({ loadConfig: mocks.loadConfig }));
|
||||
vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: mocks.runCommandWithTimeout }));
|
||||
vi.mock("../media/qr-terminal.ts", () => ({
|
||||
renderQrTerminal: mocks.renderTerminal,
|
||||
}));
|
||||
vi.mock("./command-secret-gateway.js", () => ({
|
||||
resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway,
|
||||
}));
|
||||
@@ -36,16 +37,10 @@ vi.mock("../infra/device-bootstrap.js", () => ({
|
||||
expiresAtMs: 123,
|
||||
})),
|
||||
}));
|
||||
vi.mock("qrcode-terminal", () => ({
|
||||
default: {
|
||||
generate: mocks.qrGenerate,
|
||||
},
|
||||
}));
|
||||
|
||||
const loadConfig = mocks.loadConfig;
|
||||
const runCommandWithTimeout = mocks.runCommandWithTimeout;
|
||||
const resolveCommandSecretRefsViaGateway = mocks.resolveCommandSecretRefsViaGateway;
|
||||
const qrGenerate = mocks.qrGenerate;
|
||||
const renderTerminal = mocks.renderTerminal;
|
||||
|
||||
const { registerQrCli } = await import("./qr-cli.js");
|
||||
|
||||
@@ -196,7 +191,7 @@ describe("registerQrCli", () => {
|
||||
bootstrapToken: "bootstrap-123",
|
||||
});
|
||||
expect(runtime.log).toHaveBeenCalledWith(expected);
|
||||
expect(qrGenerate).not.toHaveBeenCalled();
|
||||
expect(renderTerminal).not.toHaveBeenCalled();
|
||||
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -211,7 +206,7 @@ describe("registerQrCli", () => {
|
||||
|
||||
await runQr([]);
|
||||
|
||||
expect(qrGenerate).toHaveBeenCalledTimes(1);
|
||||
expect(renderTerminal).toHaveBeenCalledTimes(1);
|
||||
const output = runtimeLog.mock.calls.map((call) => readRuntimeCallText(call)).join("\n");
|
||||
expect(output).toContain("Pairing QR");
|
||||
expect(output).toContain("ASCII-QR");
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
||||
import { trimToUndefined } from "../gateway/credentials.js";
|
||||
import { resolveRequiredConfiguredSecretRefInputString } from "../gateway/resolve-configured-secret-input-string.js";
|
||||
import { renderQrTerminal } from "../media/qr-terminal.ts";
|
||||
import { resolvePairingSetupFromConfig, encodePairingSetupCode } from "../pairing/setup-code.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -23,20 +24,9 @@ type QrCliOptions = {
|
||||
password?: string;
|
||||
};
|
||||
|
||||
async function loadQrTerminal() {
|
||||
const mod = await import("qrcode-terminal");
|
||||
return mod.default ?? mod;
|
||||
function renderQrAscii(data: string): Promise<string> {
|
||||
return renderQrTerminal(data, { small: true });
|
||||
}
|
||||
|
||||
async function renderQrAscii(data: string): Promise<string> {
|
||||
const qrcode = await loadQrTerminal();
|
||||
return new Promise((resolve) => {
|
||||
qrcode.generate(data, { small: true }, (output: string) => {
|
||||
resolve(output);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readDevicePairPublicUrlFromConfig(cfg: OpenClawConfig): string | undefined {
|
||||
const value = cfg.plugins?.entries?.["device-pair"]?.config?.["publicUrl"];
|
||||
if (typeof value !== "string") {
|
||||
|
||||
@@ -138,9 +138,7 @@ describe("docker build cache layout", () => {
|
||||
/^COPY(?:\s+--chown=\S+)?\s+ui\/package\.json \.\/ui\/package\.json$/m,
|
||||
),
|
||||
).toBeLessThan(installIndex);
|
||||
expect(dockerfile).toContain(
|
||||
"This image only exercises the root qrcode-terminal dependency path.",
|
||||
);
|
||||
expect(dockerfile).toContain("This image only exercises the root QR runtime dependency path.");
|
||||
expect(
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
|
||||
53
src/media/qr-image.test.ts
Normal file
53
src/media/qr-image.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const renderPngBase64 = vi.hoisted(() => vi.fn(async () => "mocked-base64"));
|
||||
|
||||
vi.mock("@vincentkoc/qrcode-tui", () => ({
|
||||
renderPngBase64,
|
||||
}));
|
||||
|
||||
import { renderQrPngBase64 } from "./qr-image.ts";
|
||||
|
||||
describe("renderQrPngBase64", () => {
|
||||
beforeEach(() => {
|
||||
renderPngBase64.mockClear();
|
||||
});
|
||||
|
||||
it("delegates PNG rendering to qrcode-tui", async () => {
|
||||
await expect(renderQrPngBase64("openclaw", { scale: 8, marginModules: 2 })).resolves.toBe(
|
||||
"mocked-base64",
|
||||
);
|
||||
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
|
||||
margin: 2,
|
||||
scale: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default PNG rendering options", async () => {
|
||||
await renderQrPngBase64("openclaw");
|
||||
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
|
||||
margin: 4,
|
||||
scale: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it("floors finite PNG rendering options before delegating", async () => {
|
||||
await renderQrPngBase64("openclaw", { scale: 8.9, marginModules: 2.9 });
|
||||
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
|
||||
margin: 2,
|
||||
scale: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["scale", 0, 4, "scale must be between 1 and 12."],
|
||||
["scale", 13, 4, "scale must be between 1 and 12."],
|
||||
["scale", Number.NaN, 4, "scale must be a finite number."],
|
||||
["marginModules", 6, -1, "marginModules must be between 0 and 16."],
|
||||
["marginModules", 6, 17, "marginModules must be between 0 and 16."],
|
||||
["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();
|
||||
});
|
||||
});
|
||||
@@ -1,68 +1,53 @@
|
||||
import { encodePngRgba, fillPixel } from "./png-encode.ts";
|
||||
import { loadQrCodeTuiRuntime } from "./qr-runtime.ts";
|
||||
|
||||
type QRCodeConstructor = new (
|
||||
typeNumber: number,
|
||||
errorCorrectLevel: unknown,
|
||||
) => {
|
||||
addData: (data: string) => void;
|
||||
make: () => void;
|
||||
getModuleCount: () => number;
|
||||
isDark: (row: number, col: number) => boolean;
|
||||
};
|
||||
const DEFAULT_QR_PNG_SCALE = 6;
|
||||
const DEFAULT_QR_PNG_MARGIN_MODULES = 4;
|
||||
const MIN_QR_PNG_SCALE = 1;
|
||||
const MAX_QR_PNG_SCALE = 12;
|
||||
const MIN_QR_PNG_MARGIN_MODULES = 0;
|
||||
const MAX_QR_PNG_MARGIN_MODULES = 16;
|
||||
|
||||
let qrCodeRuntimePromise: Promise<{
|
||||
QRCode: QRCodeConstructor;
|
||||
QRErrorCorrectLevel: Record<string, unknown>;
|
||||
}> | null = null;
|
||||
|
||||
async function loadQrCodeRuntime() {
|
||||
if (!qrCodeRuntimePromise) {
|
||||
qrCodeRuntimePromise = Promise.all([
|
||||
import("qrcode-terminal/vendor/QRCode/index.js"),
|
||||
import("qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js"),
|
||||
]).then(([qrCodeModule, errorCorrectLevelModule]) => ({
|
||||
QRCode: qrCodeModule.default as QRCodeConstructor,
|
||||
QRErrorCorrectLevel: errorCorrectLevelModule.default,
|
||||
}));
|
||||
function resolveQrPngIntegerOption(params: {
|
||||
name: string;
|
||||
value: number | undefined;
|
||||
defaultValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}): number {
|
||||
if (params.value === undefined) {
|
||||
return params.defaultValue;
|
||||
}
|
||||
return await qrCodeRuntimePromise;
|
||||
}
|
||||
|
||||
async function createQrMatrix(input: string) {
|
||||
const { QRCode, QRErrorCorrectLevel } = await loadQrCodeRuntime();
|
||||
const qr = new QRCode(-1, QRErrorCorrectLevel.L);
|
||||
qr.addData(input);
|
||||
qr.make();
|
||||
return qr;
|
||||
if (!Number.isFinite(params.value)) {
|
||||
throw new RangeError(`${params.name} must be a finite number.`);
|
||||
}
|
||||
const value = Math.floor(params.value);
|
||||
if (value < params.min || value > params.max) {
|
||||
throw new RangeError(`${params.name} must be between ${params.min} and ${params.max}.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function renderQrPngBase64(
|
||||
input: string,
|
||||
opts: { scale?: number; marginModules?: number } = {},
|
||||
): Promise<string> {
|
||||
const { scale = 6, marginModules = 4 } = opts;
|
||||
const qr = await createQrMatrix(input);
|
||||
const modules = qr.getModuleCount();
|
||||
const size = (modules + marginModules * 2) * scale;
|
||||
|
||||
const buf = Buffer.alloc(size * size * 4, 255);
|
||||
for (let row = 0; row < modules; row += 1) {
|
||||
for (let col = 0; col < modules; col += 1) {
|
||||
if (!qr.isDark(row, col)) {
|
||||
continue;
|
||||
}
|
||||
const startX = (col + marginModules) * scale;
|
||||
const startY = (row + marginModules) * scale;
|
||||
for (let y = 0; y < scale; y += 1) {
|
||||
const pixelY = startY + y;
|
||||
for (let x = 0; x < scale; x += 1) {
|
||||
const pixelX = startX + x;
|
||||
fillPixel(buf, pixelX, pixelY, size, 0, 0, 0, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const png = encodePngRgba(buf, size, size);
|
||||
return png.toString("base64");
|
||||
const scale = resolveQrPngIntegerOption({
|
||||
name: "scale",
|
||||
value: opts.scale,
|
||||
defaultValue: DEFAULT_QR_PNG_SCALE,
|
||||
min: MIN_QR_PNG_SCALE,
|
||||
max: MAX_QR_PNG_SCALE,
|
||||
});
|
||||
const marginModules = resolveQrPngIntegerOption({
|
||||
name: "marginModules",
|
||||
value: opts.marginModules,
|
||||
defaultValue: DEFAULT_QR_PNG_MARGIN_MODULES,
|
||||
min: MIN_QR_PNG_MARGIN_MODULES,
|
||||
max: MAX_QR_PNG_MARGIN_MODULES,
|
||||
});
|
||||
const { renderPngBase64 } = await loadQrCodeTuiRuntime();
|
||||
return await renderPngBase64(input, {
|
||||
margin: marginModules,
|
||||
scale,
|
||||
});
|
||||
}
|
||||
|
||||
8
src/media/qr-runtime.ts
Normal file
8
src/media/qr-runtime.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
let qrCodeTuiRuntimePromise: Promise<typeof import("@vincentkoc/qrcode-tui")> | null = null;
|
||||
|
||||
export async function loadQrCodeTuiRuntime() {
|
||||
if (!qrCodeTuiRuntimePromise) {
|
||||
qrCodeTuiRuntimePromise = import("@vincentkoc/qrcode-tui");
|
||||
}
|
||||
return await qrCodeTuiRuntimePromise;
|
||||
}
|
||||
9
src/media/qr-terminal.ts
Normal file
9
src/media/qr-terminal.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { loadQrCodeTuiRuntime } 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 });
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export * from "../media/mime.js";
|
||||
export * from "../media/outbound-attachment.js";
|
||||
export * from "../media/png-encode.ts";
|
||||
export * from "../media/qr-image.ts";
|
||||
export * from "../media/qr-terminal.ts";
|
||||
export * from "../media/read-response-with-limit.js";
|
||||
export * from "../media/store.js";
|
||||
export * from "../media/temp-files.js";
|
||||
|
||||
@@ -18,7 +18,7 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
|
||||
{
|
||||
pluginId: "feishu",
|
||||
pluginLocalRuntimeDeps: ["@larksuiteoapi/node-sdk"],
|
||||
mirroredRootRuntimeDeps: ["typebox", "qrcode-terminal"],
|
||||
mirroredRootRuntimeDeps: ["typebox"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "google", pluginLocalRuntimeDeps: ["@google/genai"] },
|
||||
@@ -105,7 +105,6 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
pluginLocalRuntimeDeps: ["@whiskeysockets/baileys", "jimp"],
|
||||
mirroredRootRuntimeDeps: ["qrcode-terminal"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "zalo", minHostVersionBaseline: "2026.3.22" },
|
||||
|
||||
22
src/types/qrcode-terminal.d.ts
vendored
22
src/types/qrcode-terminal.d.ts
vendored
@@ -1,22 +0,0 @@
|
||||
declare module "qrcode-terminal" {
|
||||
type GenerateOptions = {
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
type QrCodeTerminal = {
|
||||
generate: (input: string, options?: GenerateOptions, cb?: (output: string) => void) => void;
|
||||
};
|
||||
|
||||
const qrcode: QrCodeTerminal;
|
||||
export default qrcode;
|
||||
}
|
||||
|
||||
declare module "qrcode-terminal/vendor/QRCode/index.js" {
|
||||
const QRCode: unknown;
|
||||
export default QRCode;
|
||||
}
|
||||
|
||||
declare module "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js" {
|
||||
const QRErrorCorrectLevel: Record<string, unknown>;
|
||||
export default QRErrorCorrectLevel;
|
||||
}
|
||||
@@ -431,6 +431,29 @@ describe("runSetupWizard", () => {
|
||||
expect(runTui).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails fast if the auth choice prompt returns nothing", async () => {
|
||||
promptAuthChoiceGrouped.mockImplementationOnce(async () => undefined as never);
|
||||
const prompter = buildWizardPrompter();
|
||||
const runtime = createRuntime();
|
||||
|
||||
await expect(
|
||||
runSetupWizard(
|
||||
{
|
||||
acceptRisk: true,
|
||||
flow: "quickstart",
|
||||
installDaemon: false,
|
||||
skipProviders: true,
|
||||
skipSkills: true,
|
||||
skipSearch: true,
|
||||
skipHealth: true,
|
||||
skipUi: true,
|
||||
},
|
||||
runtime,
|
||||
prompter,
|
||||
),
|
||||
).rejects.toThrow("auth choice is required");
|
||||
});
|
||||
|
||||
async function runTuiHatchTest(params: {
|
||||
writeBootstrapFile: boolean;
|
||||
expectedMessage: string | undefined;
|
||||
|
||||
Reference in New Issue
Block a user