refactor: dedupe device pair readers

This commit is contained in:
Peter Steinberger
2026-04-07 06:36:06 +01:00
parent 1dea64ab99
commit 775fa78b1e
3 changed files with 48 additions and 36 deletions

View File

@@ -1,6 +1,7 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import {
clearDeviceBootstrapTokens,
definePluginEntry,
@@ -139,7 +140,7 @@ const QR_CHANNEL_SENDERS: Record<string, QrChannelSender> = {
};
function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null {
const candidate = raw.trim();
const candidate = normalizeOptionalString(raw);
if (!candidate) {
return null;
}
@@ -147,7 +148,7 @@ function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null
if (parsedUrl) {
return parsedUrl;
}
const hostPort = candidate.split("/", 1)[0]?.trim() ?? "";
const hostPort = normalizeOptionalString(candidate.split("/", 1)[0]) ?? "";
return hostPort ? `${schemeFallback}://${hostPort}` : null;
}
@@ -230,7 +231,7 @@ function pickMatchingIPv4(predicate: (address: string) => boolean): string | nul
if (!entry || entry.internal || !isIpv4) {
continue;
}
const address = entry.address?.trim() ?? "";
const address = normalizeOptionalString(entry.address) ?? "";
if (!address) {
continue;
}
@@ -281,10 +282,7 @@ function resolveAuthLabel(cfg: OpenClawPluginApi["config"]): ResolveAuthLabelRes
function pickFirstDefined(candidates: Array<unknown>): string | null {
for (const value of candidates) {
if (typeof value !== "string") {
continue;
}
const trimmed = value.trim();
const trimmed = normalizeOptionalString(value);
if (trimmed) {
return trimmed;
}
@@ -312,8 +310,9 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
const scheme = resolveScheme(cfg);
const port = resolveGatewayPort(cfg);
if (typeof pluginCfg.publicUrl === "string" && pluginCfg.publicUrl.trim()) {
const url = normalizeUrl(pluginCfg.publicUrl, scheme);
const configuredPublicUrl = normalizeOptionalString(pluginCfg.publicUrl);
if (configuredPublicUrl) {
const url = normalizeUrl(configuredPublicUrl, scheme);
if (url) {
return { url, source: "plugins.entries.device-pair.config.publicUrl" };
}
@@ -329,8 +328,8 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
return { url: `wss://${host}`, source: `gateway.tailscale.mode=${tailscaleMode}` };
}
const remoteUrl = cfg.gateway?.remote?.url;
if (typeof remoteUrl === "string" && remoteUrl.trim()) {
const remoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url);
if (remoteUrl) {
const url = normalizeUrl(remoteUrl, scheme);
if (url) {
return { url, source: "gateway.remote.url" };
@@ -478,14 +477,19 @@ function canSendQrPngToChannel(channel: string): boolean {
function resolveQrReplyTarget(ctx: QrCommandContext): string {
if (ctx.channel === "discord") {
const senderId = ctx.senderId?.trim() ?? "";
const senderId = normalizeOptionalString(ctx.senderId) ?? "";
if (senderId) {
return senderId.startsWith("user:") || senderId.startsWith("channel:")
? senderId
: `user:${senderId}`;
}
}
return ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
return (
normalizeOptionalString(ctx.senderId) ||
normalizeOptionalString(ctx.from) ||
normalizeOptionalString(ctx.to) ||
""
);
}
const PAIR_SETUP_NON_ISSUING_ACTIONS = new Set([
@@ -521,7 +525,7 @@ async function sendQrPngToSupportedChannel(params: {
qrFilePath: string;
}): Promise<boolean> {
const mediaLocalRoots = [path.dirname(params.qrFilePath)];
const accountId = params.ctx.accountId?.trim() || undefined;
const accountId = normalizeOptionalString(params.ctx.accountId) || undefined;
const sender = QR_CHANNEL_SENDERS[params.ctx.channel];
if (!sender) {
return false;
@@ -557,7 +561,7 @@ export default definePluginEntry({
description: "Generate setup codes and approve device pairing requests.",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const args = normalizeOptionalString(ctx.args) ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
const gatewayClientScopes = Array.isArray(ctx.gatewayClientScopes)
@@ -579,7 +583,7 @@ export default definePluginEntry({
}
if (action === "notify") {
const notifyAction = tokens[1]?.trim().toLowerCase() ?? "status";
const notifyAction = normalizeOptionalString(tokens[1])?.toLowerCase() ?? "status";
return await handleNotifyCommand({
api,
ctx,
@@ -594,7 +598,7 @@ export default definePluginEntry({
const list = await listDevicePairing();
const selected = selectPendingApprovalRequest({
pending: list.pending,
requested: tokens[1]?.trim(),
requested: normalizeOptionalString(tokens[1]),
});
if (selected.reply) {
return selected.reply;
@@ -744,7 +748,11 @@ export default definePluginEntry({
};
}
const channel = ctx.channel;
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
const target =
normalizeOptionalString(ctx.senderId) ||
normalizeOptionalString(ctx.from) ||
normalizeOptionalString(ctx.to) ||
"";
const payload = await issueSetupPayload(urlResult.url);
if (channel === "telegram" && target) {

View File

@@ -1,5 +1,6 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { OpenClawPluginApi } from "./api.js";
import { listDevicePairing } from "./api.js";
@@ -41,7 +42,7 @@ function formatStringList(values?: readonly string[]): string {
}
function formatRoleList(request: PendingPairingRequest): string {
const role = request.role?.trim();
const role = normalizeOptionalString(request.role);
if (role) {
return role;
}
@@ -58,9 +59,9 @@ export function formatPendingRequests(pending: PendingPairingRequest[]): string
}
const lines: string[] = ["Pending device pairing requests:"];
for (const req of pending) {
const label = req.displayName?.trim() || req.deviceId;
const platform = req.platform?.trim();
const ip = req.remoteIp?.trim();
const label = normalizeOptionalString(req.displayName) || req.deviceId;
const platform = normalizeOptionalString(req.platform);
const ip = normalizeOptionalString(req.remoteIp);
const parts = [
`- ${req.requestId}`,
label ? `name=${label}` : null,
@@ -92,17 +93,14 @@ function normalizeNotifyState(raw: unknown): NotifyStateFile {
continue;
}
const record = item as Record<string, unknown>;
const to = typeof record.to === "string" ? record.to.trim() : "";
const to = normalizeOptionalString(record.to) ?? "";
if (!to) {
continue;
}
const accountId =
typeof record.accountId === "string" && record.accountId.trim()
? record.accountId.trim()
: undefined;
const accountId = normalizeOptionalString(record.accountId) ?? undefined;
const messageThreadId =
typeof record.messageThreadId === "string"
? record.messageThreadId.trim() || undefined
? normalizeOptionalString(record.messageThreadId) || undefined
: typeof record.messageThreadId === "number" && Number.isFinite(record.messageThreadId)
? Math.trunc(record.messageThreadId)
: undefined;
@@ -122,13 +120,14 @@ function normalizeNotifyState(raw: unknown): NotifyStateFile {
const notifiedRequestIds: Record<string, number> = {};
for (const [requestId, ts] of Object.entries(notifiedRaw)) {
if (!requestId.trim()) {
const normalizedRequestId = normalizeOptionalString(requestId);
if (!normalizedRequestId) {
continue;
}
if (typeof ts !== "number" || !Number.isFinite(ts) || ts <= 0) {
continue;
}
notifiedRequestIds[requestId] = Math.trunc(ts);
notifiedRequestIds[normalizedRequestId] = Math.trunc(ts);
}
return { subscribers, notifiedRequestIds };
@@ -170,7 +169,11 @@ function resolveNotifyTarget(ctx: {
accountId?: string;
messageThreadId?: string | number;
}): NotifyTarget | null {
const to = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
const to =
normalizeOptionalString(ctx.senderId) ||
normalizeOptionalString(ctx.from) ||
normalizeOptionalString(ctx.to) ||
"";
if (!to) {
return null;
}
@@ -206,9 +209,9 @@ function upsertNotifySubscriber(
}
function buildPairingRequestNotificationText(request: PendingPairingRequest): string {
const label = request.displayName?.trim() || request.deviceId;
const platform = request.platform?.trim();
const ip = request.remoteIp?.trim();
const label = normalizeOptionalString(request.displayName) || request.deviceId;
const platform = normalizeOptionalString(request.platform);
const ip = normalizeOptionalString(request.remoteIp);
const role = formatRoleList(request);
const scopes = formatScopeList(request);
const lines = [

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { approveDevicePairing, listDevicePairing } from "./api.js";
import { formatPendingRequests } from "./notify.js";
@@ -43,8 +44,8 @@ export function selectPendingApprovalRequest(params: {
}
function formatApprovedPairingReply(approved: ApprovedPairingEntry): { text: string } {
const label = approved.device.displayName?.trim() || approved.device.deviceId;
const platform = approved.device.platform?.trim();
const label = normalizeOptionalString(approved.device.displayName) || approved.device.deviceId;
const platform = normalizeOptionalString(approved.device.platform);
const platformLabel = platform ? ` (${platform})` : "";
return { text: `✅ Paired ${label}${platformLabel}.` };
}