mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 19:54:46 +00:00
fix(cli): clarify terminal recovery errors
This commit is contained in:
@@ -41,6 +41,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Auto-reply/TUI: keep fallback timeout recovery deliverable after a primary model lifecycle error by emitting fallback progress and deferring terminal TUI errors until recovery has a chance to finish. Fixes #80000. (#80009) Thanks @TurboTheTurtle.
|
||||
- CLI/agent: let `openclaw agent --model` use the backend/admin Gateway scope without cached device-token scopes silently downscoping the request. (#78837) Thanks @VACInc.
|
||||
- CLI/help: keep help and version invocations configless while improving shared port, channel, plugin, task, session, message, pairing, and auth recovery text.
|
||||
- CLI/config: explain strict JSON parse failures with a valid example and the plain-string escape hatch.
|
||||
- CLI/secrets: turn offline Gateway reload failures into actionable recovery text.
|
||||
- CLI/channels: explain missing or ambiguous channel selections with next commands.
|
||||
- Browser/Docker: detect Playwright-managed Chromium from `PLAYWRIGHT_BROWSERS_PATH` and the default Playwright cache on Linux, so Docker installs that persist `/home/node/.cache/ms-playwright` no longer need `browser.executablePath`.
|
||||
- Ollama: keep DeepSeek V4 cloud models thinking-capable even when Ollama Cloud `/api/show` omits the `thinking` capability, so `/think high` no longer rejects `ollama/deepseek-v4-*:cloud`.
|
||||
- ACPX/Claude ACP: keep foreground prompts waiting for their own result when autonomous task-notification results arrive during the same session, and retarget the patch for Claude Agent ACP `0.33.1`.
|
||||
|
||||
@@ -734,6 +734,12 @@ describe("config cli", () => {
|
||||
|
||||
expect(mockWriteConfigFile).not.toHaveBeenCalled();
|
||||
expect(mockReadConfigFileSnapshot).not.toHaveBeenCalled();
|
||||
expect(mockError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Could not parse "{bad" as JSON for --strict-json.'),
|
||||
);
|
||||
expect(mockError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("For plain strings, omit --strict-json."),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps --json as a strict parsing alias", async () => {
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
type ConfigSetOptions,
|
||||
} from "./config-set-input.js";
|
||||
import { resolveConfigSetMode } from "./config-set-parser.js";
|
||||
import { formatStrictJsonParseFailure } from "./error-format.js";
|
||||
import { setCommandJsonMode } from "./program/json-mode.js";
|
||||
|
||||
type PathSegment = string;
|
||||
@@ -266,7 +267,7 @@ function parseValue(raw: string, opts: ConfigSetParseOpts): unknown {
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse JSON value: ${String(err)}`, { cause: err });
|
||||
throw new Error(formatStrictJsonParseFailure({ value: raw, cause: err }), { cause: err });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,39 @@ export function formatUnsupportedChannelActionMessage(params: {
|
||||
)} to inspect supported actions.`;
|
||||
}
|
||||
|
||||
export function formatStrictJsonParseFailure(params: { value: string; cause: unknown }): string {
|
||||
const rawCause = params.cause instanceof Error ? params.cause.message : String(params.cause);
|
||||
const cause = rawCause.trim().replace(/[.。]+$/u, "");
|
||||
const preview =
|
||||
params.value.length > 48 ? `${params.value.slice(0, 45).trimEnd()}...` : params.value;
|
||||
return [
|
||||
`Could not parse ${JSON.stringify(preview)} as JSON for --strict-json.`,
|
||||
`${cause}.`,
|
||||
`Use valid JSON, for example ${formatInlineCliCommand(
|
||||
"openclaw config set gateway.port 18789 --strict-json",
|
||||
)}.`,
|
||||
"For plain strings, omit --strict-json.",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
export function formatGatewayCommandFailure(params: {
|
||||
action: string;
|
||||
error: unknown;
|
||||
inspectCommand?: string;
|
||||
}): string {
|
||||
const raw = params.error instanceof Error ? params.error.message : String(params.error);
|
||||
const message = raw
|
||||
.replace(/\s*Run [`"]?openclaw doctor[`"]? for diagnostics\.?/gi, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.replace(/[.。]+$/u, "");
|
||||
const inspectCommand = params.inspectCommand ?? "openclaw gateway status --deep";
|
||||
const detail = message ? `: ${message}` : "";
|
||||
return `Could not ${params.action} because the Gateway did not respond${detail}. Run ${formatInlineCliCommand(
|
||||
inspectCommand,
|
||||
)} to inspect the active Gateway.`;
|
||||
}
|
||||
|
||||
export function formatLookupMiss(params: {
|
||||
noun: string;
|
||||
value: string;
|
||||
|
||||
@@ -175,6 +175,22 @@ describe("secrets CLI", () => {
|
||||
expect(runtimeLogs.at(-1)).toContain('"ok": true');
|
||||
});
|
||||
|
||||
it("explains Gateway reload failures without duplicate doctor noise", async () => {
|
||||
callGatewayFromCli.mockRejectedValue(
|
||||
new Error("gateway closed (1006 abnormal closure). Run `openclaw doctor` for diagnostics."),
|
||||
);
|
||||
|
||||
await expect(
|
||||
createProgram().parseAsync(["secrets", "reload"], { from: "user" }),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(runtimeErrors.at(-1)).toContain(
|
||||
"Could not reload secrets because the Gateway did not respond: gateway closed (1006 abnormal closure).",
|
||||
);
|
||||
expect(runtimeErrors.at(-1)).toContain("openclaw gateway status --deep");
|
||||
expect(runtimeErrors.at(-1)).not.toContain("diagnostics..");
|
||||
});
|
||||
|
||||
it("runs secrets audit and exits via check code", async () => {
|
||||
runSecretsAudit.mockResolvedValue({
|
||||
version: 1,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isSecretsApplyPlan, type SecretsApplyPlan } from "../secrets/plan.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { formatGatewayCommandFailure } from "./error-format.js";
|
||||
import { addGatewayClientOptions, callGatewayFromCli, type GatewayRpcOpts } from "./gateway-rpc.js";
|
||||
|
||||
type SecretsReloadOptions = GatewayRpcOpts & { json?: boolean };
|
||||
@@ -82,7 +83,11 @@ export function registerSecretsCli(program: Command) {
|
||||
} catch (err) {
|
||||
defaultRuntime.error(
|
||||
danger(
|
||||
`Secrets reload failed: ${formatErrorMessage(err)}. Run ${formatCliCommand("openclaw gateway status --deep")} to inspect the active gateway.`,
|
||||
formatGatewayCommandFailure({
|
||||
action: "reload secrets",
|
||||
error: err,
|
||||
inspectCommand: "openclaw gateway status --deep",
|
||||
}),
|
||||
),
|
||||
);
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -271,7 +271,8 @@ describe("resolveMessageChannelSelection", () => {
|
||||
},
|
||||
{
|
||||
params: { cfg: {} as never },
|
||||
expectedMessage: "Channel is required (no configured channels detected).",
|
||||
expectedMessage:
|
||||
"Channel is required (no configured channels detected). Run openclaw channels add to configure one",
|
||||
},
|
||||
{
|
||||
setup: () => {
|
||||
@@ -292,7 +293,8 @@ describe("resolveMessageChannelSelection", () => {
|
||||
]);
|
||||
},
|
||||
params: { cfg: { channels: { whatsapp: { enabled: true } } } as never },
|
||||
expectedMessage: "Channel is required (no configured channels detected).",
|
||||
expectedMessage:
|
||||
"Channel is required (no configured channels detected). Run openclaw channels add to configure one",
|
||||
},
|
||||
{
|
||||
setup: () => {
|
||||
@@ -302,7 +304,8 @@ describe("resolveMessageChannelSelection", () => {
|
||||
]);
|
||||
},
|
||||
params: { cfg: {} as never },
|
||||
expectedMessage: "Channel is required when multiple channels are configured: beta, gamma",
|
||||
expectedMessage:
|
||||
"Channel is required when multiple channels are configured: beta, gamma. Pass --channel <channel> to choose one.",
|
||||
},
|
||||
])("rejects invalid channel selection for %j", async ({ setup, params, expectedMessage }) => {
|
||||
setup?.();
|
||||
|
||||
@@ -102,6 +102,21 @@ function formatMissingOfficialExternalChannelsMessage(
|
||||
return `Configured official external channels ${labels} are missing their plugins. Run: openclaw doctor --fix, or install individually: ${installCommands}.`;
|
||||
}
|
||||
|
||||
function formatNoConfiguredChannelsMessage(): string {
|
||||
return [
|
||||
"Channel is required (no configured channels detected).",
|
||||
"Run openclaw channels add to configure one, or pass --channel <channel> after enabling a channel.",
|
||||
"Use openclaw channels list --all to see available channel ids.",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function formatMultipleConfiguredChannelsMessage(configured: readonly string[]): string {
|
||||
return [
|
||||
`Channel is required when multiple channels are configured: ${configured.join(", ")}.`,
|
||||
"Pass --channel <channel> to choose one.",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function isAccountEnabled(account: unknown): boolean {
|
||||
if (!account || typeof account !== "object") {
|
||||
return true;
|
||||
@@ -263,11 +278,9 @@ export async function resolveMessageChannelSelection(params: {
|
||||
`Channel is required (no available channels detected). ${formatMissingOfficialExternalChannelsMessage(repairHints)}`,
|
||||
);
|
||||
}
|
||||
throw new Error("Channel is required (no configured channels detected).");
|
||||
throw new Error(formatNoConfiguredChannelsMessage());
|
||||
}
|
||||
throw new Error(
|
||||
`Channel is required when multiple channels are configured: ${configured.join(", ")}`,
|
||||
);
|
||||
throw new Error(formatMultipleConfiguredChannelsMessage(configured));
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
|
||||
Reference in New Issue
Block a user