function parseIPv4Octets(address: string): [number, number, number, number] | null { const parts = address.split("."); if (parts.length !== 4) { returnnull;
} const octets = parts.map((part) => Number.parseInt(part, 10)); if (octets.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) { returnnull;
} return octets as [number, number, number, number];
}
function isPrivateIPv4(address: string): boolean { const octets = parseIPv4Octets(address); if (!octets) { returnfalse;
} const [a, b] = octets; if (a === 10) { returntrue;
} if (a === 172 && b >= 16 && b <= 31) { returntrue;
} if (a === 192 && b === 168) { returntrue;
} returnfalse;
}
function isTailnetIPv4(address: string): boolean { const octets = parseIPv4Octets(address); if (!octets) { returnfalse;
} const [a, b] = octets; return a === 100 && b >= 64 && b <= 127;
}
function pickMatchingIPv4(predicate: (address: string) => boolean): string | null { const nets = os.networkInterfaces(); for (const entries of Object.values(nets)) { if (!entries) { continue;
} for (const entry of entries) { const family = entry?.family; // Keep the numeric check for older Node runtimes that reported family as 4. const isIpv4 = family === "IPv4" || (family as unknown) === 4; if (!entry || entry.internal || !isIpv4) { continue;
} const address = normalizeOptionalString(entry.address) ?? ""; if (!address) { continue;
} if (predicate(address)) { return address;
}
}
} returnnull;
}
function pickLanIPv4(): string | null { return pickMatchingIPv4(isPrivateIPv4);
}
function pickTailnetIPv4(): string | null { return pickMatchingIPv4(isTailnetIPv4);
}
if (mode === "token" || mode === "password") { return resolveRequiredAuthLabel(mode, { token, password });
} if (token) { return { label: "token" };
} if (password) { return { label: "password" };
} return { error: "Gateway auth is not configured (no token or password)." };
}
function pickFirstDefined(candidates: Array<unknown>): string | null { for (const value of candidates) { const trimmed = normalizeOptionalString(value); if (trimmed) { return trimmed;
}
} returnnull;
}
function resolveRequiredAuthLabel(
mode: "token" | "password",
values: { token?: string; password?: string },
): ResolveAuthLabelResult { if (mode === "token") { return values.token
? { label: "token" }
: { error: "Gateway auth is set to token, but no token is configured." };
} return values.password
? { label: "password" }
: { error: "Gateway auth is set to password, but no password is configured." };
}
async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResult> { const cfg = api.config; const pluginCfg = (api.pluginConfig ?? {}) as DevicePairPluginConfig; const scheme = resolveScheme(cfg); const port = resolveGatewayPort(cfg);
return {
error: "Gateway is only bound to loopback. Set gateway.bind=lan, enable tailscale serve, or configure plugins.entries.device-pair.config.publicUrl.",
};
}
function buildPairingFlowLines(stepTwo: string): string[] { return [ "1) Open the iOS app → Settings → Gateway",
`2) ${stepTwo}`, "3) Back here, run /pair approve", "4) If this code leaks or you are done, run /pair cleanup",
];
}
function buildQrFollowUpLines(autoNotifyArmed: boolean): string[] { return autoNotifyArmed
? [ "After scanning, wait here for the pairing request ping.", "I’ll auto-ping here when the pairing request arrives, then auto-disable.", "If the ping does not arrive, run `/pair approve latest` manually.",
]
: ["After scanning, run `/pair approve` to complete pairing."];
}
function formatSetupInstructions(expiresAtMs: number): string { return [ "Pairing setup code generated.", "",
...buildPairingFlowLines("Paste the setup code from my next message and tap Connect"), "",
...buildSecurityNoticeLines({
kind: "setup code",
expiresAtMs,
}),
].join("\n");
}
function buildQrInfoLines(params: {
payload: SetupPayload;
authLabel: string;
autoNotifyArmed: boolean;
expiresAtMs: number;
}): string[] { return [
`Gateway: ${params.payload.url}`,
`Auth: ${params.authLabel}`,
...buildSecurityNoticeLines({
kind: "QR code",
expiresAtMs: params.expiresAtMs,
}), "",
...buildQrFollowUpLines(params.autoNotifyArmed), "", "If your camera still won’t lock on, run `/pair` for a pasteable setup code.",
];
}
function formatQrInfoMarkdown(params: {
payload: SetupPayload;
authLabel: string;
autoNotifyArmed: boolean;
expiresAtMs: number;
}): string { return [
`- Gateway: ${params.payload.url}`,
`- Auth: ${params.authLabel}`,
...buildSecurityNoticeLines({
kind: "QR code",
expiresAtMs: params.expiresAtMs,
markdown: true,
}), "",
...buildQrFollowUpLines(params.autoNotifyArmed), "", "If your camera still won’t lock on, run `/pair` for a pasteable setup code.",
].join("\n");
}
function canSendQrPngToChannel(channel: string): boolean { return channel in QR_CHANNEL_SENDERS;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.