mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
8b7a674d22
* ✨ feat(cli): add `lh doctor` to diagnose the whole chain in one command Diagnosing a broken `lh` today means knowing which layer to suspect: `status` answers only "does the gateway accept this machine", `whoami` only "who am I". Everything else — a node too old for the CLI's own imports, a device gateway that belongs to another deployment, a workspace scope saved under a different account, a provider with no key — surfaces much later as an error that names the symptom rather than the cause. `lh doctor` walks the chain in order and stops reporting derived symptoms once it finds a root cause: runtime → endpoints → credentials → scope → run context → server → device → execution - Profiles: bare `doctor` runs the core layers (fast, cheap); `--profile connect | agent | hetero | selfhost | all` adds the deeper ones. - `--offline` skips everything that touches the network, `--deep` opts into the costly checks, so a diagnostic never spends money by default. - `--json` prints the same report object the terminal renderer consumes, one entry per check with `status`, `detail`, `evidence`, `fix` and — for skips — the upstream check or flag that excluded it. - Exit code contract: 0 unless something failed, `--strict` promotes warnings. - `--fix` applies only the repairs a check knows how to make (stale workspace scope, unparseable settings.json, loose permissions, leftover daemon state) and re-runs the check so the report shows the repaired state. - `--deep --agent <id|slug>` starts a real run and reports tokens and cost, proving the chain end to end instead of inspecting it. Also extracts the credential precedence (`LOBEHUB_JWT` > explicit flags > `LOBEHUB_CLI_API_KEY` > stored login) into `pickAuthSource`, shared by `resolveToken`, the tRPC client and doctor — it was spelled out twice and had already drifted on which env names it accepts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): stop doctor from leaking proxy credentials or repairing healthy state Five findings from an independent review of the doctor series, all real: - `--fix` deleted the pid/status files of a daemon that was merely reconnecting, orphaning a live process `connect stop` could no longer find. Repairs now run only for the finding a check marks `evidence.repairable`, and each repair re-checks which state it was handed. The runner skips repairs entirely for findings without that marker, so a warning with nothing to undo no longer reports a failed repair. - Proxy env vars were copied into the report verbatim. `http://user:pass@host` is a normal way to configure one, and the report is meant to be pasted into an issue — URL credentials are now redacted. - A self-hosted server with the default device gateway failed `endpoints.resolution`, which skipped reachability, credentials, identity and every agent check behind it. An installation that only uses the HTTP API needs no device gateway, so this is a warning; the device profile's own handshake still fails concretely. - `--fix` dropped a valid saved workspace scope whenever `LOBEHUB_WORKSPACE_ID` overrode it for one run, silently moving the user to personal scope once the override went away. - `getAiProviderList` reports a provider's toggle, not whether a key is stored, so an enabled-but-keyless provider was reported as ready and the run then died with `InvalidProviderAPIKey` — the exact failure the check exists to pre-empt. Agent readiness now reads that one provider's key vault and names where the key comes from, saying "not verified" rather than passing when a restricted API key cannot see vaults at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(cli): record why the node check cannot catch a too-old runtime Closing that gap means changing how the CLI is launched, which is not this command's to change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): keep doctor from crashing, over-claiming, or leaking identifiers Ten findings from a second independent review, all confirmed: - A socket failure reaching the gateway arrives as an EventEmitter `error` event, which `connect().catch()` cannot see. With no listener the whole report died with "Unhandled error" instead of reporting a failed handshake — verified by removing the new listener and reproducing the crash against a refused port. - Server, gateway and proxy URLs are redacted at the report boundary, not just in the proxy variable: a basic-auth URL in `LOBEHUB_SERVER` used to print its password in `--json`. Gateway error text also quoted the WebSocket query string, carrying hostname, device id and user id; the account email is now masked too. The raw values still flow to whatever connects with them. - "Enabled on the server" no longer claims a key exists. `deepseek` is enabled unconditionally in the server's own config, and `ENABLED_OPENAI` / `ENABLED_OLLAMA` default to true, so the report now says the server reports it enabled and that server-side keys are invisible from here. - A provider holding only a `baseURL` is a local runtime (Ollama, LM Studio), not a missing key: it warns instead of failing, so it no longer blocks the deep round trip through `dependsOn`. - The round trip only reads "no longer tracked" as success once it has seen the run live. The server also answers null when there is no state at all, which made the check incapable of failing. - A saved workspace scope is only repairable when the account is identified and differs. Under `LOBEHUB_CLI_API_KEY` there is no readable subject at all, and `--fix` was deleting a selection that was still valid for the stored login. - The settings repair no longer overwrites an existing `.bak`. - The update check bounds its own registry request: the runner's timeout would otherwise surface as a failure and a non-zero exit, for something this check only ever reports as a warning. - The command sets `process.exitCode` instead of calling `process.exit`, which does not wait for stdout to drain and can truncate a piped JSON report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): redact the server URL the workspace-scope check reports Sweeping every check's evidence for an injected credential — the way the acceptance round does — turned up one the previous pass missed: the scope check copies the resolved server URL verbatim, so a basic-auth self-hosted URL put its password into `--json`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): make the URL redactor linear instead of backtracking CodeQL flagged js/polynomial-redos: matching the URL lazily and then requiring a `?` backtracks across the remainder of the string at every starting position, so an error message with many `ws://` runs takes quadratic time. Tokenise first and decide per token — same output, no backtracking, with a test that fails if the quadratic shape comes back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): scrub the doctor report centrally and stop it lingering after exit Seven findings from a third independent review, all confirmed: - Redaction had leaked in a third place: Node's fetch error quotes the full request URL, and capability and daemon evidence copied gateway URLs as-is. Per-check redaction had now missed an instance in each of three rounds, so the runner scrubs every result — detail, fix, evidence, repair messages — before it enters the report: URL userinfo, WebSocket query strings and email addresses, by linear token scanning rather than a backtracking pattern. - With `process.exit` gone, the process waited for the event loop to empty, so a request abandoned by a check timeout, or the registry check's uncleared timer, kept it alive — up to the full `--timeout` after the report printed. The command now exits from a stdout write callback: after the output has drained, and no later. - The registry check clears its timer and gets a runner budget that always outlasts its own deadline; a sub-second `--timeout` no longer turns its warning into a failure. - A settings.json that cannot be read is no longer classified as corrupt and offered to `--fix`, which moved a possibly valid file aside unparsed. The repair also re-confirms the parse failure before moving anything. - A run that stops being tracked is inconclusive even after it was seen running: the server returns null for lost state too, so only `isCompleted` proves the round trip. - A live daemon that has not reported a connection state is a warning, not "connected as device unknown". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
99 lines
4.3 KiB
TypeScript
99 lines
4.3 KiB
TypeScript
/**
|
|
* Every string that names the product, in one place.
|
|
*
|
|
* These were previously literals scattered across a dozen modules — the bin
|
|
* names in `utils/completion.ts`, the config directory in five separate files,
|
|
* the systemd unit name defined twice, the product name in a handful of
|
|
* descriptions and one agent-facing prompt. That worked while there was exactly
|
|
* one distribution, but it meant a white-label build had no seam: the strings
|
|
* are reached at runtime from paths no UI gating can intercept, and one of them
|
|
* (`buildNotifyProtocol`) is a prompt the model repeats back to the user.
|
|
*
|
|
* The values below are upstream's, unchanged, so the default build is
|
|
* byte-identical. A distribution replaces this module wholesale.
|
|
*/
|
|
|
|
/** Product name as it appears inside sentences. */
|
|
export const CLI_PRODUCT_NAME = 'LobeHub';
|
|
|
|
/** The CLI's own name, e.g. in `--help` and the connect banner. */
|
|
export const CLI_DISPLAY_NAME = 'LobeHub CLI';
|
|
|
|
/**
|
|
* The command users type. Also the man page's title (uppercased) and the name
|
|
* shown in "run `<bin> login`" hints.
|
|
*/
|
|
export const CLI_PRIMARY_BIN = 'lh';
|
|
|
|
/** Additional command names installed alongside the primary one. */
|
|
export const CLI_BIN_ALIASES: readonly string[] = ['lobe', 'lobehub'];
|
|
|
|
/** Every installed command name — what shell completion has to bind to. */
|
|
export const CLI_BIN_NAMES: readonly string[] = [CLI_PRIMARY_BIN, ...CLI_BIN_ALIASES];
|
|
|
|
/**
|
|
* Shell function name emitted by `completion`.
|
|
*
|
|
* A shell identifier, so it must stay `[A-Za-z_][A-Za-z0-9_]*` — a distribution
|
|
* overriding this cannot use hyphens or spaces.
|
|
*/
|
|
export const CLI_COMPLETION_FUNCTION = '_lobehub_completion';
|
|
|
|
/** Directory under $HOME holding settings, credentials and daemon state. */
|
|
export const CLI_CONFIG_DIR_NAME = '.lobehub';
|
|
|
|
/**
|
|
* Env var the generated completion script uses to hand `__complete` the
|
|
* current word index.
|
|
*
|
|
* Purely internal plumbing: the completion function sets it and the CLI reads
|
|
* it back within the same invocation, so a user never sets or sees it
|
|
* directly — unlike `CLI_API_KEY_ENV_NAMES`, there is no back-compat concern
|
|
* for a rebranded distribution, since both sides always come from the one
|
|
* binary running. Still collected here rather than left as a literal in
|
|
* `utils/completion.ts`/`commands/completion.ts`: it is one of the strings
|
|
* this module's doc comment above promises are all in one place, and it was
|
|
* the one that got missed the first time around.
|
|
*/
|
|
export const CLI_COMPLETION_CWORD_ENV = 'LOBEHUB_COMP_CWORD';
|
|
|
|
/** systemd unit name for `connect service`. */
|
|
export const CLI_CONNECT_SERVICE_NAME = 'lobehub-connect.service';
|
|
|
|
/**
|
|
* Environment variable names the user may set, most preferred first.
|
|
*
|
|
* A list rather than a single name so a rebranded distribution can introduce
|
|
* its own prefix while still honouring anything already exported in a user's
|
|
* shell or CI config. Only variables the USER sets belong here — the ones the
|
|
* desktop app and the server inject (LOBEHUB_SERVER, LOBEHUB_JWT, the agent-run
|
|
* ids) are a cross-process contract and must be renamed on both sides at once.
|
|
*/
|
|
export const CLI_API_KEY_ENV_NAMES: readonly string[] = ['LOBEHUB_CLI_API_KEY'];
|
|
export const CLI_HOME_ENV_NAMES: readonly string[] = ['LOBEHUB_CLI_HOME'];
|
|
|
|
const firstEnvEntry = (names: readonly string[]): { name: string; value: string } | undefined => {
|
|
for (const name of names) {
|
|
const value = process.env[name]?.trim();
|
|
if (value) return { name, value };
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const firstEnvValue = (names: readonly string[]): string | undefined => firstEnvEntry(names)?.value;
|
|
|
|
/** API key from the environment, honouring every accepted variable name. */
|
|
export const readCliApiKeyEnv = (): string | undefined => firstEnvValue(CLI_API_KEY_ENV_NAMES);
|
|
|
|
/**
|
|
* Same lookup as {@link readCliApiKeyEnv}, but keeps the variable name that
|
|
* actually matched. `lh doctor` reports which env var a credential came from,
|
|
* and "LOBEHUB_CLI_API_KEY" is the wrong answer when an older alias won.
|
|
*/
|
|
export const readCliApiKeyEnvSource = (): { name: string; value: string } | undefined =>
|
|
firstEnvEntry(CLI_API_KEY_ENV_NAMES);
|
|
|
|
/** Config directory name, overridable per-install by the home env var. */
|
|
export const resolveCliDirName = (): string =>
|
|
firstEnvValue(CLI_HOME_ENV_NAMES) ?? CLI_CONFIG_DIR_NAME;
|