
























@@ -5,6 +5,7 @@ import { isRecord } from "@openclaw/normalization-core/record-coerce";
55import { note } from "../../packages/terminal-core/src/note.js";
66import { formatCliCommand } from "../cli/command-format.js";
77import type { OpenClawConfig } from "../config/types.openclaw.js";
8+import type { HealthFinding, HealthRepairEffect } from "../flows/health-checks.js";
89import { saveJsonFile } from "../infra/json-file.js";
910import { tryReadJsonSync } from "../infra/json-files.js";
1011import type { BundledPluginSource } from "../plugins/bundled-sources.js";
@@ -18,6 +19,7 @@ import { hasRetainedManagedNpmInstallMarker } from "../plugins/managed-npm-reten
1819import { listManagedPluginNpmRootsSync } from "../plugins/npm-project-roots.js";
1920import {
2021auditOpenClawPeerDependenciesInManagedNpmRoot,
22+type OpenClawPeerLinkAuditIssue,
2123relinkOpenClawPeerDependenciesInManagedNpmRoot,
2224} from "../plugins/plugin-peer-link.js";
2325import { refreshPluginRegistry } from "../plugins/plugin-registry.js";
@@ -34,6 +36,8 @@ import {
3436type PluginRegistryInstallMigrationParams,
3537} from "./doctor/shared/plugin-registry-migration.js";
363839+const PLUGIN_REGISTRY_CHECK_ID = "core/doctor/plugin-registry";
40+3741type PluginRegistryDoctorRepairParams = Omit<PluginRegistryInstallMigrationParams, "config"> &
3842InstalledPluginIndexRecordStoreOptions & {
3943config: OpenClawConfig;
@@ -53,6 +57,31 @@ type PluginRegistryDoctorNoteLogger = {
5357warn: (message: string) => void;
5458};
555960+export type PluginRegistryHealthIssue =
61+| {
62+kind: "registry-missing-or-stale";
63+path: string;
64+}
65+| {
66+kind: "stale-managed-npm-bundled-plugin";
67+pluginId: string;
68+packageName: string;
69+packageDir: string;
70+npmRoot: string;
71+version?: string;
72+}
73+| {
74+kind: "stale-local-bundled-plugin-install-record";
75+pluginId: string;
76+stalePath: string;
77+}
78+| {
79+kind: "managed-npm-openclaw-peer-link";
80+packageName: string;
81+packageDir: string;
82+reason: string;
83+};
84+5685function readJsonObject(filePath: string): Record<string, unknown> | null {
5786const parsed = tryReadJsonSync(filePath);
5887return isRecord(parsed) ? parsed : null;
@@ -390,6 +419,145 @@ async function loadInstallRecordsWithoutPluginIds(
390419return records;
391420}
392421422+async function listManagedNpmOpenClawPeerLinkIssues(
423+params: PluginRegistryDoctorRepairParams,
424+): Promise<OpenClawPeerLinkAuditIssue[]> {
425+const audits = await Promise.all(
426+listManagedPluginNpmRoots(params).map((npmRoot) =>
427+auditOpenClawPeerDependenciesInManagedNpmRoot({ npmRoot }),
428+),
429+);
430+return audits.flatMap((audit) => audit.issues);
431+}
432+433+export async function detectPluginRegistryHealthIssues(
434+params: PluginRegistryDoctorRepairParams,
435+): Promise<PluginRegistryHealthIssue[]> {
436+const preflight = preflightPluginRegistryInstallMigration(params);
437+const issues: PluginRegistryHealthIssue[] = [];
438+if (preflight.action === "migrate") {
439+issues.push({
440+kind: "registry-missing-or-stale",
441+path: preflight.filePath,
442+});
443+}
444+for (const plugin of listStaleManagedNpmBundledPlugins(params)) {
445+issues.push({
446+kind: "stale-managed-npm-bundled-plugin",
447+pluginId: plugin.pluginId,
448+packageName: plugin.packageName,
449+packageDir: plugin.packageDir,
450+npmRoot: plugin.npmRoot,
451+ ...(plugin.version ? { version: plugin.version } : {}),
452+});
453+}
454+for (const record of await listStaleLocalBundledPluginInstallRecordShadows(params)) {
455+issues.push({
456+kind: "stale-local-bundled-plugin-install-record",
457+pluginId: record.pluginId,
458+stalePath: record.stalePath,
459+});
460+}
461+for (const issue of await listManagedNpmOpenClawPeerLinkIssues(params)) {
462+issues.push({
463+kind: "managed-npm-openclaw-peer-link",
464+packageName: issue.packageName,
465+packageDir: issue.packageDir,
466+reason: issue.reason,
467+});
468+}
469+return issues;
470+}
471+472+export function pluginRegistryIssueToHealthFinding(
473+issue: PluginRegistryHealthIssue,
474+): HealthFinding {
475+switch (issue.kind) {
476+case "registry-missing-or-stale":
477+return {
478+checkId: PLUGIN_REGISTRY_CHECK_ID,
479+severity: "warning",
480+message: "Persisted plugin registry is missing or stale.",
481+path: issue.path,
482+fixHint: "Run `openclaw doctor --fix` to rebuild the plugin registry from enabled plugins.",
483+};
484+case "stale-managed-npm-bundled-plugin":
485+return {
486+checkId: PLUGIN_REGISTRY_CHECK_ID,
487+severity: "warning",
488+message: `Managed npm package ${issue.packageName}${
489+ issue.version ? `@${issue.version}` : ""
490+ } shadows bundled plugin ${issue.pluginId}.`,
491+path: issue.packageDir,
492+target: issue.pluginId,
493+fixHint:
494+"Run `openclaw doctor --fix` to remove stale managed npm packages and rebuild the plugin registry.",
495+};
496+case "stale-local-bundled-plugin-install-record":
497+return {
498+checkId: PLUGIN_REGISTRY_CHECK_ID,
499+severity: "warning",
500+message: `Local install record for bundled plugin ${issue.pluginId} points at a stale path.`,
501+path: issue.stalePath,
502+target: issue.pluginId,
503+fixHint:
504+"Run `openclaw doctor --fix` to remove stale local install records and rebuild the plugin registry.",
505+};
506+case "managed-npm-openclaw-peer-link":
507+return {
508+checkId: PLUGIN_REGISTRY_CHECK_ID,
509+severity: "warning",
510+message: `Managed npm package ${issue.packageName} has a broken OpenClaw peer link: ${issue.reason}.`,
511+path: issue.packageDir,
512+target: issue.packageName,
513+fixHint: "Run `openclaw doctor --fix` to relink managed npm plugin packages.",
514+};
515+}
516+return assertNeverPluginRegistryIssue(issue);
517+}
518+519+export function pluginRegistryIssueToRepairEffect(
520+issue: PluginRegistryHealthIssue,
521+): HealthRepairEffect {
522+switch (issue.kind) {
523+case "registry-missing-or-stale":
524+return {
525+kind: "state",
526+action: "would-rebuild-plugin-registry",
527+target: issue.path,
528+dryRunSafe: false,
529+};
530+case "stale-managed-npm-bundled-plugin":
531+return {
532+kind: "package",
533+action: "would-remove-stale-managed-npm-bundled-plugin",
534+target: issue.packageDir,
535+dryRunSafe: false,
536+};
537+case "stale-local-bundled-plugin-install-record":
538+return {
539+kind: "state",
540+action: "would-remove-stale-local-bundled-plugin-install-record",
541+target: issue.pluginId,
542+dryRunSafe: false,
543+};
544+case "managed-npm-openclaw-peer-link":
545+return {
546+kind: "package",
547+action: "would-relink-managed-npm-openclaw-peer",
548+target: issue.packageDir,
549+dryRunSafe: false,
550+};
551+}
552+return assertNeverPluginRegistryIssue(issue);
553+}
554+555+function assertNeverPluginRegistryIssue(issue: never): never {
556+throw new Error(
557+`Unhandled plugin registry issue kind: ${String((issue as { kind?: unknown }).kind)}`,
558+);
559+}
560+393561/**
394562 * Runs plugin registry doctor repairs and refreshes the persisted plugin index when needed.
395563 *
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。