当通过npm install -g openclaw安装 OpenClaw 时,控制界面无法加载,并显示 “

Control UI assets not found. Build them with `pnpm ui:build` (auto-installs UI deps), or run `pnpm ui:dev` during development.

” (缺少控制界面资源。请使用pnpm ui:build构建它们),尽管资源已正确包含在包中。

预期行为

由于dist/control-ui/index.html和所有资源都存在于 npm 包中,控制界面应正常加载。

实际行为

界面资源检测失败,并建议运行pnpm ui:build,但这是不可能的,因为构建脚本并未包含在 npm 包中。

根本原因

位于src/infra/control-ui-assets.ts中的resolveControlUiDistIndexPath()函数假设process.argv[1]指向dist/目录下的一个文件:

const distDir = path.dirname(normalized);
if (path.basename(distDir) !== "dist") return null;

建议的修复方案

resolveControlUiDistIndexPath中添加一个备用方案,当入口点不在dist/目录下时,相对于包的根目录检查dist/control-ui/index.html

// Fallback: check relative to package.json location
const pkgDir = findPackageRoot(normalized); // find nearest package.json
if (pkgDir) {
  const fallbackIndex = path.join(pkgDir, "dist", "control-ui", "index.html");
  if (fs.existsSync(fallbackIndex)) return fallbackIndex;
}

修改后的文件:替代./openclaw-main/src/infra/control-ui-assets.ts文件。

import fs from "node:fs";
import path from "node:path";
import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { resolveOpenClawPackageRoot } from "./openclaw-root.js";

// +++ 新增:添加 findPackageRoot 函数用于查找最近的 package.json 所在目录
function findPackageRoot(startPath: string): string | null {
  let current = path.resolve(startPath);
  // 如果 startPath 是文件,则从它的目录开始
  if (fs.existsSync(current) && !fs.statSync(current).isDirectory()) {
    current = path.dirname(current);
  }
  
  for (let i = 0; i < 10; i++) {
    const pkgPath = path.join(current, "package.json");
    if (fs.existsSync(pkgPath)) {
      return current;
    }
    const parent = path.dirname(current);
    if (parent === current) {
      break;
    }
    current = parent;
  }
  return null;
}
// --- 新增结束

export function resolveControlUiRepoRoot(
  argv1: string | undefined = process.argv[1],
): string | null {
  if (!argv1) {
    return null;
  }
  const normalized = path.resolve(argv1);
  const parts = normalized.split(path.sep);
  const srcIndex = parts.lastIndexOf("src");
  if (srcIndex !== -1) {
    const root = parts.slice(0, srcIndex).join(path.sep);
    if (fs.existsSync(path.join(root, "ui", "vite.config.ts"))) {
      return root;
    }
  }

  let dir = path.dirname(normalized);
  for (let i = 0; i < 8; i++) {
    if (
      fs.existsSync(path.join(dir, "package.json")) &&
      fs.existsSync(path.join(dir, "ui", "vite.config.ts"))
    ) {
      return dir;
    }
    const parent = path.dirname(dir);
    if (parent === dir) {
      break;
    }
    dir = parent;
  }

  return null;
}

export async function resolveControlUiDistIndexPath(
  argv1: string | undefined = process.argv[1],
): Promise<string | null> {
  if (!argv1) {
    return null;
  }
  const normalized = path.resolve(argv1);

  // Case 1: entrypoint is directly inside dist/ (e.g., dist/entry.js)
  const distDir = path.dirname(normalized);
  if (path.basename(distDir) === "dist") {
    return path.join(distDir, "control-ui", "index.html");
  }

  // +++ 新增:回退逻辑 - 检查相对于 package.json 位置(针对全局安装)
  const pkgDir = findPackageRoot(normalized); // 查找最近的 package.json
  if (pkgDir) {
    const fallbackIndex = path.join(pkgDir, "dist", "control-ui", "index.html");
    if (fs.existsSync(fallbackIndex)) return fallbackIndex;
  }
  // --- 新增结束

  const packageRoot = await resolveOpenClawPackageRoot({ argv1: normalized });
  if (!packageRoot) {
    return null;
  }
  return path.join(packageRoot, "dist", "control-ui", "index.html");
}

export type EnsureControlUiAssetsResult = {
  ok: boolean;
  built: boolean;
  message?: string;
};

function summarizeCommandOutput(text: string): string | undefined {
  const lines = text
    .split(/\r?\n/g)
    .map((l) => l.trim())
    .filter(Boolean);
  if (!lines.length) {
    return undefined;
  }
  const last = lines.at(-1);
  if (!last) {
    return undefined;
  }
  return last.length > 240 ? `${last.slice(0, 239)}…` : last;
}

export async function ensureControlUiAssetsBuilt(
  runtime: RuntimeEnv = defaultRuntime,
  opts?: { timeoutMs?: number },
): Promise<EnsureControlUiAssetsResult> {
  const indexFromDist = await resolveControlUiDistIndexPath(process.argv[1]);
  if (indexFromDist && fs.existsSync(indexFromDist)) {
    return { ok: true, built: false };
  }

  const repoRoot = resolveControlUiRepoRoot(process.argv[1]);
  if (!repoRoot) {
    const hint = indexFromDist
      ? `Missing Control UI assets at ${indexFromDist}`
      : "Missing Control UI assets";
    return {
      ok: false,
      built: false,
      message: `${hint}. Build them with \`pnpm ui:build\` (auto-installs UI deps).`,
    };
  }

  const indexPath = path.join(repoRoot, "dist", "control-ui", "index.html");
  if (fs.existsSync(indexPath)) {
    return { ok: true, built: false };
  }

  const uiScript = path.join(repoRoot, "scripts", "ui.js");
  if (!fs.existsSync(uiScript)) {
    return {
      ok: false,
      built: false,
      message: `Control UI assets missing but ${uiScript} is unavailable.`,
    };
  }

  runtime.log("Control UI assets missing; building (ui:build, auto-installs UI deps)…");

  const build = await runCommandWithTimeout([process.execPath, uiScript, "build"], {
    cwd: repoRoot,
    timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
  });
  if (build.code !== 0) {
    return {
      ok: false,
      built: false,
      message: `Control UI build failed: ${summarizeCommandOutput(build.stderr) ?? `exit ${build.code}`}`,
    };
  }

  if (!fs.existsSync(indexPath)) {
    return {
      ok: false,
      built: true,
      message: `Control UI build completed but ${indexPath} is still missing.`,
    };
  }

  return { ok: true, built: true };
}

然后重新编译启动即可解决:
 

# git clone https://github.com/openclaw/openclaw.git
# cd openclaw
# pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm openclaw onboard --install-daemon

注:control-ui-assets.ts代码修改由DeepSeek自动完成。亲测可用。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐