fix(device-pair): resolve QR senders by own key (#101652)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Alix-007
2026-07-10 05:31:07 +08:00
committed by GitHub
parent a1c16f0a3d
commit a4ae95c1c6
2 changed files with 35 additions and 8 deletions
+27
View File
@@ -601,6 +601,33 @@ describe("device-pair /pair qr", () => {
expect(text).not.toContain("```");
});
it.each(["toString", "constructor", "__proto__"])(
"requires QR channel sender %s to be an own entry",
async (channel) => {
const loadAdapter = vi.fn(async () => undefined);
const command = registerPairCommand({
runtime: {
channel: { outbound: { loadAdapter } },
} as unknown as OpenClawPluginApi["runtime"],
});
const result = await command.handler(
createCommandContext({
channel,
senderId: "prototype-channel",
gatewayClientScopes: INTERNAL_SETUP_SCOPES,
}),
);
const text = requireText(result);
expect(pluginApiMocks.writeQrPngTempFile).not.toHaveBeenCalled();
expect(loadAdapter).not.toHaveBeenCalled();
expect(pluginApiMocks.revokeDeviceBootstrapToken).not.toHaveBeenCalled();
expect(pluginApiMocks.issueDeviceBootstrapToken).toHaveBeenCalledTimes(1);
expect(text).toContain("QR image delivery is not available on this channel");
expect(text).toContain("Setup code:");
},
);
it("supports invalidating unused setup codes", async () => {
const command = registerPairCommand();
const result = await command?.handler(
+8 -8
View File
@@ -590,8 +590,9 @@ function formatQrInfoMarkdown(params: {
].join("\n");
}
function canSendQrPngToChannel(channel: string): boolean {
return channel in QR_CHANNEL_SENDERS;
function resolveQrChannelSender(channel: string): QrChannelSender | undefined {
// Prototype names are not supported channel entries and must take the setup-code fallback.
return Object.hasOwn(QR_CHANNEL_SENDERS, channel) ? QR_CHANNEL_SENDERS[channel] : undefined;
}
function resolveQrReplyTarget(ctx: QrCommandContext): string {
@@ -634,16 +635,13 @@ async function issueSetupPayload(url: string, urls?: string[]): Promise<SetupPay
async function sendQrPngToSupportedChannel(params: {
api: OpenClawPluginApi;
ctx: QrCommandContext;
sender: QrChannelSender;
target: string;
caption: string;
qrFilePath: string;
}): Promise<boolean> {
const mediaLocalRoots = [path.dirname(params.qrFilePath)];
const accountId = normalizeOptionalString(params.ctx.accountId) || undefined;
const sender = QR_CHANNEL_SENDERS[params.ctx.channel];
if (!sender) {
return false;
}
const adapter = await params.api.runtime.channel.outbound.loadAdapter(params.ctx.channel);
const send = adapter?.sendMedia;
if (!send) {
@@ -653,7 +651,7 @@ async function sendQrPngToSupportedChannel(params: {
cfg: params.api.config,
to: params.target,
text: params.caption,
...sender.createOpts({
...params.sender.createOpts({
ctx: params.ctx,
qrFilePath: params.qrFilePath,
mediaLocalRoots,
@@ -783,6 +781,7 @@ export default definePluginEntry({
if (action === "qr") {
const channel = ctx.channel;
const qrChannelSender = resolveQrChannelSender(channel);
const target = resolveQrReplyTarget(ctx);
let autoNotifyArmed = false;
@@ -807,7 +806,7 @@ export default definePluginEntry({
expiresAtMs: payload.expiresAtMs,
});
if (target && canSendQrPngToChannel(channel)) {
if (target && qrChannelSender) {
let qrFilePath: string | undefined;
try {
const { resolvePreferredOpenClawTmpDir, writeQrPngTempFile } =
@@ -822,6 +821,7 @@ export default definePluginEntry({
const sent = await sendQrPngToSupportedChannel({
api,
ctx,
sender: qrChannelSender,
target,
caption: ["Scan this QR code with the OpenClaw iOS app:", "", ...infoLines].join(
"\n",