mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-26 12:03:34 +00:00
feat: QR code scanning for gateway onboarding
iOS:
- QR scanner view using DataScannerViewController
- Photo library QR detection via CIDetector for saved QR images
- Deep link parser for openclaw://gateway URLs and base64url setup codes
- Onboarding wizard: full-screen welcome with "Scan QR Code" button,
auto-connect on scan, back navigation, step indicators for manual flow
Backend:
- Add /pair qr action to device-pair extension for QR code generation
- TUI/WebUI differentiation: ASCII QR for TUI, markdown image for WebUI
- Telegram: send QR as media attachment via sendMessageTelegram
- Add data URI support to loadWebMedia for generic base64 media handling
- Export renderQrPngBase64 from plugin SDK for extension use
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit d79ed65be0)
This commit is contained in:
committed by
Mariano Belinky
parent
df6d0ee92b
commit
06adadc759
@@ -1,6 +1,15 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import os from "node:os";
|
||||
import { approveDevicePairing, listDevicePairing } from "openclaw/plugin-sdk";
|
||||
import { approveDevicePairing, listDevicePairing, renderQrPngBase64 } from "openclaw/plugin-sdk";
|
||||
import qrcode from "qrcode-terminal";
|
||||
|
||||
function renderQrAscii(data: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
qrcode.generate(data, { small: true }, (output: string) => {
|
||||
resolve(output);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const DEFAULT_GATEWAY_PORT = 18789;
|
||||
|
||||
@@ -434,6 +443,84 @@ export default function register(api: OpenClawPluginApi) {
|
||||
password: auth.password,
|
||||
};
|
||||
|
||||
if (action === "qr") {
|
||||
const setupCode = encodeSetupCode(payload);
|
||||
const [qrBase64, qrAscii] = await Promise.all([
|
||||
renderQrPngBase64(setupCode),
|
||||
renderQrAscii(setupCode),
|
||||
]);
|
||||
const authLabel = auth.label ?? "auth";
|
||||
const dataUrl = `data:image/png;base64,${qrBase64}`;
|
||||
|
||||
const channel = ctx.channel;
|
||||
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
|
||||
|
||||
if (channel === "telegram" && target) {
|
||||
try {
|
||||
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
|
||||
if (send) {
|
||||
await send(target, "Scan this QR code with the OpenClaw iOS app:", {
|
||||
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
|
||||
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
|
||||
mediaUrl: dataUrl,
|
||||
});
|
||||
return {
|
||||
text: [
|
||||
`Gateway: ${payload.url}`,
|
||||
`Auth: ${authLabel}`,
|
||||
"",
|
||||
"After scanning, come back here and run `/pair approve` to complete pairing.",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
api.logger.warn?.(
|
||||
`device-pair: telegram QR send failed, falling back (${String(
|
||||
(err as Error)?.message ?? err,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Render based on channel capability
|
||||
api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
|
||||
const infoLines = [
|
||||
`Gateway: ${payload.url}`,
|
||||
`Auth: ${authLabel}`,
|
||||
"",
|
||||
"After scanning, run `/pair approve` to complete pairing.",
|
||||
];
|
||||
|
||||
// TUI (gateway-client) needs ASCII, WebUI can render markdown images
|
||||
const isTui = target === "gateway-client" || channel !== "webchat";
|
||||
|
||||
if (!isTui) {
|
||||
// WebUI: markdown image only
|
||||
return {
|
||||
text: [
|
||||
"Scan this QR code with the OpenClaw iOS app:",
|
||||
"",
|
||||
``,
|
||||
"",
|
||||
...infoLines,
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
// CLI/TUI: ASCII QR only
|
||||
return {
|
||||
text: [
|
||||
"Scan this QR code with the OpenClaw iOS app:",
|
||||
"",
|
||||
"```",
|
||||
qrAscii,
|
||||
"```",
|
||||
"",
|
||||
...infoLines,
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
const channel = ctx.channel;
|
||||
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
|
||||
const authLabel = auth.label ?? "auth";
|
||||
|
||||
Reference in New Issue
Block a user