mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix: honor qr remote precedence and wire clawbot alias
This commit is contained in:
@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
|
||||
- TUI: suppress false `(no output)` placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.
|
||||
- TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.
|
||||
- CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.
|
||||
- CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091)
|
||||
|
||||
## 2026.2.14
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ describe("registerSubCliCommands", () => {
|
||||
const names = program.commands.map((cmd) => cmd.name());
|
||||
expect(names).toContain("acp");
|
||||
expect(names).toContain("gateway");
|
||||
expect(names).toContain("clawbot");
|
||||
expect(registerAcpCli).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -176,6 +176,14 @@ const entries: SubCliEntry[] = [
|
||||
mod.registerQrCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clawbot",
|
||||
description: "Legacy clawbot command aliases",
|
||||
register: async (program) => {
|
||||
const mod = await import("../clawbot-cli.js");
|
||||
mod.registerClawbotCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pairing",
|
||||
description: "Pairing helpers",
|
||||
|
||||
@@ -174,4 +174,40 @@ describe("registerQrCli", () => {
|
||||
expect(payload.auth).toBe("token");
|
||||
expect(payload.urlSource).toBe("gateway.remote.url");
|
||||
});
|
||||
|
||||
it("prefers gateway.remote.url over tailscale when --remote is set", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
tailscale: { mode: "serve" },
|
||||
remote: { url: "wss://remote.example.com:444", token: "remote-tok" },
|
||||
auth: { mode: "token", token: "local-tok" },
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"device-pair": {
|
||||
config: {
|
||||
publicUrl: "wss://wrong.example.com:443",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
runCommandWithTimeout.mockResolvedValue({
|
||||
code: 0,
|
||||
stdout: '{"Self":{"DNSName":"ts-host.tailnet.ts.net."}}',
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const program = new Command();
|
||||
registerQrCli(program);
|
||||
await program.parseAsync(["qr", "--json", "--remote"], { from: "user" });
|
||||
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls.at(-1)?.[0] ?? "{}")) as {
|
||||
gatewayUrl?: string;
|
||||
urlSource?: string;
|
||||
};
|
||||
expect(payload.gatewayUrl).toBe("wss://remote.example.com:444");
|
||||
expect(payload.urlSource).toBe("gateway.remote.url");
|
||||
expect(runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,28 +102,12 @@ export function registerQrCli(program: Command) {
|
||||
: typeof opts.publicUrl === "string" && opts.publicUrl.trim()
|
||||
? opts.publicUrl.trim()
|
||||
: undefined;
|
||||
if (wantsRemote && !explicitUrl) {
|
||||
const existing = cfg.plugins?.entries?.["device-pair"];
|
||||
if (existing && typeof existing === "object") {
|
||||
cfg.plugins = {
|
||||
...cfg.plugins,
|
||||
entries: {
|
||||
...cfg.plugins?.entries,
|
||||
"device-pair": {
|
||||
...existing,
|
||||
config: {
|
||||
...existing.config,
|
||||
publicUrl: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const publicUrl = explicitUrl ?? readDevicePairPublicUrlFromConfig(cfg);
|
||||
const publicUrl =
|
||||
explicitUrl ?? (wantsRemote ? undefined : readDevicePairPublicUrlFromConfig(cfg));
|
||||
|
||||
const resolved = await resolvePairingSetupFromConfig(cfg, {
|
||||
publicUrl,
|
||||
preferRemoteUrl: wantsRemote,
|
||||
runCommandWithTimeout: async (argv, runOpts) =>
|
||||
await runCommandWithTimeout(argv, {
|
||||
timeoutMs: runOpts.timeoutMs,
|
||||
|
||||
@@ -101,4 +101,38 @@ describe("pairing setup code", () => {
|
||||
urlSource: "gateway.tailscale.mode=serve",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers gateway.remote.url over tailscale when requested", async () => {
|
||||
const runCommandWithTimeout = vi.fn(async () => ({
|
||||
code: 0,
|
||||
stdout: '{"Self":{"DNSName":"mb-server.tailnet.ts.net."}}',
|
||||
stderr: "",
|
||||
}));
|
||||
|
||||
const resolved = await resolvePairingSetupFromConfig(
|
||||
{
|
||||
gateway: {
|
||||
tailscale: { mode: "serve" },
|
||||
remote: { url: "wss://remote.example.com:444" },
|
||||
auth: { mode: "token", token: "tok_123" },
|
||||
},
|
||||
},
|
||||
{
|
||||
preferRemoteUrl: true,
|
||||
runCommandWithTimeout,
|
||||
},
|
||||
);
|
||||
|
||||
expect(resolved).toEqual({
|
||||
ok: true,
|
||||
payload: {
|
||||
url: "wss://remote.example.com:444",
|
||||
token: "tok_123",
|
||||
password: undefined,
|
||||
},
|
||||
authLabel: "token",
|
||||
urlSource: "gateway.remote.url",
|
||||
});
|
||||
expect(runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ export type PairingSetupCommandRunner = (
|
||||
export type ResolvePairingSetupOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
publicUrl?: string;
|
||||
preferRemoteUrl?: boolean;
|
||||
forceSecure?: boolean;
|
||||
runCommandWithTimeout?: PairingSetupCommandRunner;
|
||||
networkInterfaces?: () => ReturnType<typeof os.networkInterfaces>;
|
||||
@@ -292,6 +293,7 @@ async function resolveGatewayUrl(
|
||||
opts: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
publicUrl?: string;
|
||||
preferRemoteUrl?: boolean;
|
||||
forceSecure?: boolean;
|
||||
runCommandWithTimeout?: PairingSetupCommandRunner;
|
||||
networkInterfaces: () => ReturnType<typeof os.networkInterfaces>;
|
||||
@@ -308,6 +310,15 @@ async function resolveGatewayUrl(
|
||||
return { error: "Configured publicUrl is invalid." };
|
||||
}
|
||||
|
||||
const remoteUrlRaw = cfg.gateway?.remote?.url;
|
||||
const remoteUrl =
|
||||
typeof remoteUrlRaw === "string" && remoteUrlRaw.trim()
|
||||
? normalizeUrl(remoteUrlRaw, scheme)
|
||||
: null;
|
||||
if (opts.preferRemoteUrl && remoteUrl) {
|
||||
return { url: remoteUrl, source: "gateway.remote.url" };
|
||||
}
|
||||
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
if (tailscaleMode === "serve" || tailscaleMode === "funnel") {
|
||||
const host = await resolveTailnetHost(opts.runCommandWithTimeout);
|
||||
@@ -317,12 +328,8 @@ async function resolveGatewayUrl(
|
||||
return { url: `wss://${host}`, source: `gateway.tailscale.mode=${tailscaleMode}` };
|
||||
}
|
||||
|
||||
const remoteUrl = cfg.gateway?.remote?.url;
|
||||
if (typeof remoteUrl === "string" && remoteUrl.trim()) {
|
||||
const url = normalizeUrl(remoteUrl, scheme);
|
||||
if (url) {
|
||||
return { url, source: "gateway.remote.url" };
|
||||
}
|
||||
if (remoteUrl) {
|
||||
return { url: remoteUrl, source: "gateway.remote.url" };
|
||||
}
|
||||
|
||||
const bind = cfg.gateway?.bind ?? "loopback";
|
||||
@@ -375,6 +382,7 @@ export async function resolvePairingSetupFromConfig(
|
||||
const urlResult = await resolveGatewayUrl(cfg, {
|
||||
env,
|
||||
publicUrl: options.publicUrl,
|
||||
preferRemoteUrl: options.preferRemoteUrl,
|
||||
forceSecure: options.forceSecure,
|
||||
runCommandWithTimeout: options.runCommandWithTimeout,
|
||||
networkInterfaces: options.networkInterfaces ?? os.networkInterfaces,
|
||||
|
||||
Reference in New Issue
Block a user