Files
modelstudioai__cli/packages/cli/src/error-handler.ts
T
ls 1fd08fe1c8 Merge pull request #56 from modelstudioai/feat/console-gateway-region-site
feat(console): resolve gateway URL from region + site, add switchAgent
2026-06-17 14:49:28 +08:00

203 lines
6.1 KiB
TypeScript

import {
BailianError,
ExitCode,
detectOutputFormat,
type OutputFormat,
CONSOLE_GATEWAY_NO_TOKEN_MESSAGE,
} from "bailian-cli-core";
import { API_KEY_PAGE } from "./urls.ts";
const LABEL_WIDTH = 13;
/** Short reminder; full resolution order matches `loadConfig` in bailian-cli-core. */
const BASE_URL_HINT =
"If the DashScope host is wrong, check baseUrl (--base-url, bl config show, or DASHSCOPE_BASE_URL).";
function pad(label: string): string {
return label.padEnd(LABEL_WIDTH);
}
function alignContinuation(text: string): string {
return text
.split("\n")
.map((line, i) => (i === 0 ? line : " ".repeat(LABEL_WIDTH) + line))
.join("\n");
}
function enhanceHint(err: BailianError): string | undefined {
if (err.exitCode === ExitCode.AUTH) {
if (
err.message === CONSOLE_GATEWAY_NO_TOKEN_MESSAGE ||
err.hint?.includes("auth login --console")
) {
return err.hint;
}
return [
err.hint,
"",
"bl auth login --api-key <your-key>",
"bl auth status",
`Get API Key: ${API_KEY_PAGE}`,
]
.filter((s): s is string => s !== undefined)
.join("\n");
}
return err.hint;
}
export function detectErrorOutputFormat(
argv: string[] = process.argv.slice(2),
envOutput: string | undefined = process.env.DASHSCOPE_OUTPUT,
): OutputFormat {
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--output") {
return detectOutputFormat(argv[i + 1] || envOutput);
}
if (arg?.startsWith("--output=")) {
return detectOutputFormat(arg.slice("--output=".length));
}
}
return detectOutputFormat(envOutput);
}
function fromFetchFailed(err: TypeError): BailianError {
const cause = err.cause as NodeJS.ErrnoException | undefined;
const code = cause?.code;
const causeMsg = cause?.message;
const detailParts: string[] = [];
if (code) detailParts.push(code);
if (causeMsg && causeMsg !== code) detailParts.push(causeMsg);
const detail = detailParts.length > 0 ? detailParts.join(": ") : "unknown cause";
return new BailianError(
`Network request failed: ${detail}`,
ExitCode.NETWORK,
pickNetworkHint(code),
{ cause: err },
);
}
function pickNetworkHint(code: string | undefined): string {
switch (code) {
case "ENOTFOUND":
case "EAI_AGAIN":
return `DNS resolution failed. Check DNS / network. ${BASE_URL_HINT}`;
case "ECONNREFUSED":
return "Connection refused. Check the target host/port and proxy settings.";
case "ECONNRESET":
return (
"Connection reset by peer. Retry, or check proxy / firewall.\n" +
"If you are behind a VPN or corporate proxy, route bl through it:\n" +
"export HTTPS_PROXY=http://127.0.0.1:<proxy-port>"
);
case "ETIMEDOUT":
return `Connection timed out. Check your network. ${BASE_URL_HINT}`;
case "CERT_HAS_EXPIRED":
case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
case "DEPTH_ZERO_SELF_SIGNED_CERT":
return "TLS certificate error. Check system clock and CA bundle.";
default:
return `Check network and proxy (HTTP_PROXY / HTTPS_PROXY). ${BASE_URL_HINT}`;
}
}
function fromFsError(err: NodeJS.ErrnoException): BailianError {
const ecode = err.code;
let hint = "Check the file path and permissions.";
if (ecode === "ENOENT") hint = "File or directory not found.";
else if (ecode === "EACCES" || ecode === "EPERM")
hint = "Permission denied — check file or directory permissions.";
else if (ecode === "ENOSPC") hint = "Disk full — free up space and try again.";
return new BailianError(`File system error: ${err.message}`, ExitCode.GENERAL, hint, {
cause: err,
});
}
function writeCauseChain(err: Error): void {
let cur: unknown = (err as { cause?: unknown }).cause;
let depth = 0;
while (cur instanceof Error && depth < 5) {
process.stderr.write(`${pad("Caused by:")}${cur.message}\n`);
cur = (cur as { cause?: unknown }).cause;
depth++;
}
}
function writeBailianErrorText(err: BailianError): void {
process.stderr.write(`\n${pad("Error:")}${err.message}\n`);
const hint = enhanceHint(err);
if (hint) {
process.stderr.write(`${pad("Hint:")}${alignContinuation(hint)}\n`);
}
const api = err.api;
if (api) {
if (api.httpStatus !== undefined) {
const codeSuffix = api.apiCode ? ` (${api.apiCode})` : "";
process.stderr.write(`${pad("Status:")}HTTP ${api.httpStatus}${codeSuffix}\n`);
} else if (api.apiCode) {
process.stderr.write(`${pad("Code:")}${api.apiCode}\n`);
}
if (api.requestId) {
process.stderr.write(`${pad("Request ID:")}${api.requestId}\n`);
}
}
writeCauseChain(err);
process.stderr.write(`${pad("Exit code:")}${err.exitCode}\n`);
}
export function handleError(err: unknown): never {
if (err instanceof BailianError) {
const format = detectErrorOutputFormat();
if (format === "json") {
process.stderr.write(JSON.stringify(err.toJSON(), null, 2) + "\n");
} else {
writeBailianErrorText(err);
}
process.exit(err.exitCode);
}
if (err instanceof Error) {
if (
err.name === "AbortError" ||
err.name === "TimeoutError" ||
err.message.includes("timed out")
) {
const timeout = new BailianError(
"Request timed out.",
ExitCode.TIMEOUT,
"Try increasing --timeout (e.g. --timeout 60).\n" +
`${BASE_URL_HINT}\n` +
"Run: bl auth status — to check credentials.",
{ cause: err },
);
return handleError(timeout);
}
if (err instanceof TypeError && err.message === "fetch failed") {
return handleError(fromFetchFailed(err));
}
const ecode = (err as NodeJS.ErrnoException).code;
if (typeof ecode === "string" && ecode.startsWith("E")) {
return handleError(fromFsError(err as NodeJS.ErrnoException));
}
process.stderr.write(`\n${pad("Error:")}${err.message}\n`);
writeCauseChain(err);
if (process.env.DASHSCOPE_VERBOSE === "1") {
process.stderr.write(`${err.stack}\n`);
}
} else {
process.stderr.write(`\n${pad("Error:")}${String(err)}\n`);
}
process.exit(ExitCode.GENERAL);
}