Skip to content

Commit dfa3acb

Browse files
committed
Better QR display
1 parent 68df19d commit dfa3acb

File tree

3 files changed

+96
-37
lines changed

3 files changed

+96
-37
lines changed

entrypoint.sh

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,11 @@ try {
5959
fi
6060

6161
# ---------------------------------------------------------------------------
62-
# Register the bundled WhatsApp plugin against the runtime state dir so
63-
# `openclaw channels login --channel whatsapp` works without an interactive prompt.
64-
# printf '\r' selects the pre-highlighted "Use local plugin path" option.
62+
# Ensure WhatsApp plugin is installed (from npm, no interactive prompts)
6563
# ---------------------------------------------------------------------------
66-
WHATSAPP_PLUGIN="/usr/local/lib/node_modules/openclaw/dist/extensions/whatsapp/index.js"
67-
if [ -f "$WHATSAPP_PLUGIN" ]; then
68-
echo "Registering WhatsApp plugin..."
69-
printf '\r' | OPENCLAW_STATE_DIR="$OPENCLAW_DIR" openclaw plugins install \
70-
--path "$WHATSAPP_PLUGIN" 2>/dev/null || true
64+
echo "Ensuring WhatsApp plugin is installed..."
65+
if ! OPENCLAW_STATE_DIR="$OPENCLAW_DIR" openclaw plugins list 2>/dev/null | grep -q "@openclaw/whatsapp"; then
66+
OPENCLAW_STATE_DIR="$OPENCLAW_DIR" openclaw plugins install @openclaw/whatsapp || true
7167
fi
7268

7369
# ---------------------------------------------------------------------------

setup-wizard/index.html

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,8 @@ <h2>Integrations & Security</h2>
702702
<span id="whatsapp-btn-icon">&#128241;</span> <span id="whatsapp-btn-label">Connect WhatsApp</span>
703703
</button>
704704
<div id="whatsapp-qr-area" style="display:none;margin-top:0.75rem">
705-
<pre id="whatsapp-qr-output" style="background:var(--card);border:1px solid var(--border);padding:1rem;border-radius:8px;font-size:0.65rem;line-height:1.15;overflow:auto;max-height:360px;white-space:pre;color:var(--text)">Starting…</pre>
705+
<canvas id="whatsapp-qr-canvas" style="display:none;border-radius:8px;max-width:100%;background:#fff;padding:8px"></canvas>
706+
<pre id="whatsapp-qr-log" style="background:var(--card);border:1px solid var(--border);padding:0.75rem;border-radius:8px;font-size:0.75rem;line-height:1.4;overflow:auto;max-height:180px;white-space:pre-wrap;color:var(--text-muted);margin-top:0.5rem">Waiting…</pre>
706707
</div>
707708
<div style="margin-top:0.75rem">
708709
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-weight:400">
@@ -932,32 +933,94 @@ <h2 style="text-align:center">Configuration Saved!</h2>
932933
} catch {}
933934
}
934935

936+
// Decode Unicode block characters into QR pixel data and draw on canvas
937+
function renderBlockQr(lines, canvas) {
938+
const moduleSize = 6;
939+
const cols = Math.max(...lines.map(l => [...l].length));
940+
const rows = lines.length * 2;
941+
canvas.width = cols * moduleSize;
942+
canvas.height = rows * moduleSize;
943+
const ctx = canvas.getContext("2d");
944+
ctx.fillStyle = "#ffffff";
945+
ctx.fillRect(0, 0, canvas.width, canvas.height);
946+
ctx.fillStyle = "#000000";
947+
for (let li = 0; li < lines.length; li++) {
948+
const chars = [...lines[li]];
949+
for (let ci = 0; ci < chars.length; ci++) {
950+
let t = false, b = false;
951+
switch (chars[ci]) {
952+
case "\u2588": t = true; b = true; break; // █
953+
case "\u2580": t = true; break; // ▀
954+
case "\u2584": b = true; break; // ▄
955+
}
956+
if (t) ctx.fillRect(ci * moduleSize, li * 2 * moduleSize, moduleSize, moduleSize);
957+
if (b) ctx.fillRect(ci * moduleSize, (li * 2 + 1) * moduleSize, moduleSize, moduleSize);
958+
}
959+
}
960+
canvas.style.display = "block";
961+
}
962+
963+
function tryRenderAsciiQr(text, canvas) {
964+
const allLines = text.split("\n");
965+
let best = [], cur = [];
966+
for (const line of allLines) {
967+
const t = line.trimEnd();
968+
if (/[\u2588\u2580\u2584]/.test(t) && [...t].length >= 15) {
969+
cur.push(t);
970+
} else {
971+
if (cur.length > best.length) best = cur;
972+
cur = [];
973+
}
974+
}
975+
if (cur.length > best.length) best = cur;
976+
if (best.length < 8) return false;
977+
renderBlockQr(best, canvas);
978+
return true;
979+
}
980+
935981
function startWhatsAppLogin() {
936982
const qrArea = document.getElementById("whatsapp-qr-area");
937-
const qrOutput = document.getElementById("whatsapp-qr-output");
983+
const qrCanvas = document.getElementById("whatsapp-qr-canvas");
984+
const qrLog = document.getElementById("whatsapp-qr-log");
938985
const btn = document.getElementById("whatsapp-connect-btn");
939986

940987
qrArea.style.display = "block";
941-
qrOutput.textContent = "Starting WhatsApp login…";
988+
qrCanvas.style.display = "none";
989+
qrLog.textContent = "Connecting\u2026";
942990
btn.disabled = true;
943991

944992
if (whatsappEventSource) { whatsappEventSource.close(); whatsappEventSource = null; }
945993

946994
whatsappEventSource = new EventSource("/api/whatsapp/login-stream");
947-
let buf = "";
995+
let logBuf = "";
996+
let qrRendered = false;
948997

949-
whatsappEventSource.onmessage = (e) => {
950-
const payload = JSON.parse(e.data);
951-
if (payload && typeof payload === "object" && payload.done) {
998+
whatsappEventSource.onmessage = async (e) => {
999+
const msg = JSON.parse(e.data);
1000+
if (msg.type === "qr") {
1001+
// Raw QR string — render with QRCode.js
1002+
try {
1003+
qrCanvas.style.display = "block";
1004+
await QRCode.toCanvas(qrCanvas, msg.qr, { width: 256, margin: 2, color: { dark: "#000", light: "#fff" } });
1005+
qrLog.textContent = "Scan with WhatsApp \u2192 Linked Devices \u2192 Link a Device";
1006+
qrRendered = true;
1007+
} catch { qrLog.textContent += "\n[raw qr: " + msg.qr + "]"; }
1008+
} else if (msg.type === "log") {
1009+
logBuf += msg.data;
1010+
// Show only non-block-char lines in the status log
1011+
const statusLines = logBuf.split("\n").filter(l => !/[\u2588\u2580\u2584]/.test(l) && l.trim()).join("\n");
1012+
if (statusLines) qrLog.textContent = statusLines;
1013+
// Try to extract and render ASCII art QR to canvas
1014+
if (!qrRendered) {
1015+
qrRendered = tryRenderAsciiQr(logBuf, qrCanvas);
1016+
if (qrRendered) qrLog.textContent = "Scan with WhatsApp \u2192 Linked Devices \u2192 Link a Device";
1017+
}
1018+
} else if (msg.type === "done") {
9521019
whatsappEventSource.close();
9531020
whatsappEventSource = null;
9541021
btn.disabled = false;
9551022
checkWhatsAppLinked();
956-
return;
9571023
}
958-
buf += payload;
959-
qrOutput.textContent = buf;
960-
qrOutput.scrollTop = qrOutput.scrollHeight;
9611024
};
9621025

9631026
whatsappEventSource.onerror = () => {
@@ -2034,6 +2097,7 @@ <h2>Configure ${p.name}</h2>
20342097
}
20352098
}
20362099
</script>
2100+
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js"></script>
20372101
</body>
20382102

20392103
</html>

setup-wizard/server.cjs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -155,35 +155,34 @@ const server = http.createServer(async (req, res) => {
155155

156156
const child = spawn("openclaw", ["channels", "login", "--channel", "whatsapp"], {
157157
env: { ...process.env, OPENCLAW_STATE_DIR: CONFIG_DIR },
158-
stdio: ["pipe", "pipe", "pipe"],
158+
stdio: ["ignore", "pipe", "pipe"],
159159
});
160160

161-
const send = (text) => {
162-
// Strip ANSI escape codes before sending to browser
163-
const clean = text
164-
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
165-
.replace(/\x1b\][^\x07]*\x07/g, "");
166-
res.write(`data: ${JSON.stringify(clean)}\n\n`);
167-
};
161+
const sendMsg = (obj) => res.write(`data: ${JSON.stringify(obj)}\n\n`);
162+
163+
const cleanAnsi = (text) => text
164+
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
165+
.replace(/\x1b\][^\x07]*\x07/g, "")
166+
.replace(/\r\n/g, "\n")
167+
.replace(/\r/g, "\n");
168+
169+
// WhatsApp QR data from baileys: `1@base64,base64,base64` or bare multi-segment base64
170+
const QR_PATTERN = /(?:^|\n)(\d+@[A-Za-z0-9+/=,]{40,}|[A-Za-z0-9+/=]{20,}(?:,[A-Za-z0-9+/=]{20,}){2,})/m;
168171

169-
// Clack uses raw-mode stdin; auto-confirm the "Use local plugin path" prompt with \r
170-
let promptAnswered = false;
171-
let buf = "";
172172
const onData = (chunk) => {
173173
const text = chunk.toString();
174-
buf += text;
175-
if (buf.length > 512) buf = buf.slice(-512);
176-
if (!promptAnswered && buf.includes("Install WhatsApp plugin")) {
177-
promptAnswered = true;
178-
child.stdin.write("\r");
174+
const match = text.match(QR_PATTERN);
175+
if (match) {
176+
sendMsg({ type: "qr", qr: match[1].trim() });
177+
} else {
178+
sendMsg({ type: "log", data: cleanAnsi(text) });
179179
}
180-
send(text);
181180
};
182181

183182
child.stdout.on("data", onData);
184183
child.stderr.on("data", onData);
185184
child.on("close", (code) => {
186-
res.write(`data: ${JSON.stringify({ done: true, code })}\n\n`);
185+
sendMsg({ type: "done", code });
187186
res.end();
188187
});
189188
req.on("close", () => child.kill());

0 commit comments

Comments
 (0)