From 02576615cb4c1382abf1d0aee10ed10f1f676e78 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 30 Jan 2026 04:01:31 +0100 Subject: [PATCH] fix: migrate legacy gateway services --- docs/providers/xiaomi.md | 10 +-- package.json | 2 +- src/cli/banner.ts | 9 +-- src/commands/doctor-gateway-services.ts | 89 +++++++++++++++++++++++- src/commands/doctor.ts | 2 +- src/commands/onboard-helpers.ts | 8 ++- src/daemon/inspect.ts | 46 +++++++++--- ui/index.html | 4 +- ui/public/apple-touch-icon.png | Bin 0 -> 5920 bytes ui/public/favicon-32.png | Bin 0 -> 1015 bytes ui/public/favicon.svg | 22 ++++++ 11 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 ui/public/apple-touch-icon.png create mode 100644 ui/public/favicon-32.png create mode 100644 ui/public/favicon.svg diff --git a/docs/providers/xiaomi.md b/docs/providers/xiaomi.md index 008c421058b..1e270c14728 100644 --- a/docs/providers/xiaomi.md +++ b/docs/providers/xiaomi.md @@ -1,14 +1,14 @@ --- -summary: "Use Xiaomi MiMo (mimo-v2-flash) with Moltbot" +summary: "Use Xiaomi MiMo (mimo-v2-flash) with OpenClaw" read_when: - - You want Xiaomi MiMo models in Moltbot + - You want Xiaomi MiMo models in OpenClaw - You need XIAOMI_API_KEY setup --- # Xiaomi MiMo Xiaomi MiMo is the API platform for **MiMo** models. It provides REST APIs compatible with OpenAI and Anthropic formats and uses API keys for authentication. Create your API key in -the [Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys). Moltbot uses +the [Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys). OpenClaw uses the `xiaomi` provider with a Xiaomi MiMo API key. ## Model overview @@ -20,9 +20,9 @@ the `xiaomi` provider with a Xiaomi MiMo API key. ## CLI setup ```bash -moltbot onboard --auth-choice xiaomi-api-key +openclaw onboard --auth-choice xiaomi-api-key # or non-interactive -moltbot onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY" +openclaw onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY" ``` ## Config snippet diff --git a/package.json b/package.json index 1186b041365..9f21078dfd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openclaw", - "version": "2026.1.29-beta.1", + "version": "2026.1.29-beta.2", "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent", "type": "module", "main": "dist/index.js", diff --git a/src/cli/banner.ts b/src/cli/banner.ts index 1174de6740f..79ed3ee9017 100644 --- a/src/cli/banner.ts +++ b/src/cli/banner.ts @@ -64,10 +64,11 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {} const LOBSTER_ASCII = [ "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄", - "█████░█████░█████░█░░░█░█████░█░░░░░░███░░█░░░█", - "█░░░█░█░░░█░███░░░██░░█░█░░░░░█░░░░░█░░░█░█░█░█", - "█████░████░░█████░█░░██░█████░█████░█████░██░██", - "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀", + "█████░█████░█████░█░░░█░█████░█░░░░░█████░█░░░█", + "█░░░█░█░░░█░█░░░░░██░░█░█░░░░░█░░░░░█░░░█░█░░░█", + "█░░░█░█████░████░░█░█░█░█░░░░░█░░░░░█████░█░█░█", + "█░░░█░█░░░░░█░░░░░█░░██░█░░░░░█░░░░░█░░░█░██░██", + "█████░█░░░░░█████░█░░░█░█████░█████░█░░░█░█░░░█", " 🦞 OPENCLAW 🦞 ", " ", ]; diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index 4311c740028..7ca23a13022 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -1,4 +1,8 @@ +import { execFile } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; +import { promisify } from "node:util"; import type { OpenClawConfig } from "../config/config.js"; import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js"; @@ -16,6 +20,8 @@ import { buildGatewayInstallPlan } from "./daemon-install-helpers.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, type GatewayDaemonRuntime } from "./daemon-runtime.js"; import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js"; +const execFileAsync = promisify(execFile); + function detectGatewayRuntime(programArguments: string[] | undefined): GatewayDaemonRuntime { const first = programArguments?.[0]; if (first) { @@ -37,6 +43,42 @@ function normalizeExecutablePath(value: string): string { return path.resolve(value); } +function extractDetailPath(detail: string, prefix: string): string | null { + if (!detail.startsWith(prefix)) return null; + const value = detail.slice(prefix.length).trim(); + return value.length > 0 ? value : null; +} + +async function cleanupLegacyLaunchdService(params: { + label: string; + plistPath: string; +}): Promise { + const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501"; + await execFileAsync("launchctl", ["bootout", domain, params.plistPath]).catch(() => undefined); + await execFileAsync("launchctl", ["unload", params.plistPath]).catch(() => undefined); + + const trashDir = path.join(os.homedir(), ".Trash"); + try { + await fs.mkdir(trashDir, { recursive: true }); + } catch { + // ignore + } + + try { + await fs.access(params.plistPath); + } catch { + return null; + } + + const dest = path.join(trashDir, `${params.label}-${Date.now()}.plist`); + try { + await fs.rename(params.plistPath, dest); + return dest; + } catch { + return null; + } +} + export async function maybeRepairGatewayServiceConfig( cfg: OpenClawConfig, mode: "local" | "remote", @@ -150,7 +192,11 @@ export async function maybeRepairGatewayServiceConfig( } } -export async function maybeScanExtraGatewayServices(options: DoctorOptions) { +export async function maybeScanExtraGatewayServices( + options: DoctorOptions, + runtime: RuntimeEnv, + prompter: DoctorPrompter, +) { const extraServices = await findExtraGatewayServices(process.env, { deep: options.deep, }); @@ -161,6 +207,47 @@ export async function maybeScanExtraGatewayServices(options: DoctorOptions) { "Other gateway-like services detected", ); + const legacyServices = extraServices.filter((svc) => svc.legacy === true); + if (legacyServices.length > 0) { + const shouldRemove = await prompter.confirmSkipInNonInteractive({ + message: "Remove legacy gateway services (clawdbot/moltbot) now?", + initialValue: true, + }); + if (shouldRemove) { + const removed: string[] = []; + const failed: string[] = []; + for (const svc of legacyServices) { + if (svc.platform !== "darwin") { + failed.push(`${svc.label} (${svc.platform})`); + continue; + } + if (svc.scope !== "user") { + failed.push(`${svc.label} (${svc.scope})`); + continue; + } + const plistPath = extractDetailPath(svc.detail, "plist:"); + if (!plistPath) { + failed.push(`${svc.label} (missing plist path)`); + continue; + } + const dest = await cleanupLegacyLaunchdService({ + label: svc.label, + plistPath, + }); + removed.push(dest ? `${svc.label} -> ${dest}` : svc.label); + } + if (removed.length > 0) { + note(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed"); + } + if (failed.length > 0) { + note(failed.map((line) => `- ${line}`).join("\n"), "Legacy gateway cleanup skipped"); + } + if (removed.length > 0) { + runtime.log("Legacy gateway services removed. Installing OpenClaw gateway next."); + } + } + } + const cleanupHints = renderGatewayServiceCleanupHints(); if (cleanupHints.length > 0) { note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints"); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 40d86bc7fb4..f89e84c4177 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -185,7 +185,7 @@ export async function doctorCommand( cfg = await maybeRepairSandboxImages(cfg, runtime, prompter); noteSandboxScopeWarnings(cfg); - await maybeScanExtraGatewayServices(options); + await maybeScanExtraGatewayServices(options, runtime, prompter); await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter); await noteMacLaunchAgentOverrides(); await noteMacLaunchctlGatewayEnvOverrides(cfg); diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 9c23073d17e..0efa9909028 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -65,9 +65,11 @@ export function randomToken(): string { export function printWizardHeader(runtime: RuntimeEnv) { const header = [ "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄", - "█████░█████░█████░█░░░█░█████░█░░░░░░███░░█░░░█", - "█░░░█░█░░░█░███░░░██░░█░█░░░░░█░░░░░█░░░█░█░█░█", - "█████░████░░█████░█░░██░█████░█████░█████░██░██", + "█████░█████░█████░█░░░█░█████░█░░░░░█████░█░░░█", + "█░░░█░█░░░█░█░░░░░██░░█░█░░░░░█░░░░░█░░░█░█░░░█", + "█░░░█░█████░████░░█░█░█░█░░░░░█░░░░░█████░█░█░█", + "█░░░█░█░░░░░█░░░░░█░░██░█░░░░░█░░░░░█░░░█░██░██", + "█████░█░░░░░█████░█░░░█░█████░█████░█░░░█░█░░░█", "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀", " 🦞 FRESH DAILY 🦞 ", " ", diff --git a/src/daemon/inspect.ts b/src/daemon/inspect.ts index 84f22bacf23..1018635ee85 100644 --- a/src/daemon/inspect.ts +++ b/src/daemon/inspect.ts @@ -16,13 +16,15 @@ export type ExtraGatewayService = { label: string; detail: string; scope: "user" | "system"; + marker?: "openclaw" | "clawdbot" | "moltbot"; + legacy?: boolean; }; export type FindExtraGatewayServicesOptions = { deep?: boolean; }; -const EXTRA_MARKERS = ["openclaw"]; +const EXTRA_MARKERS = ["openclaw", "clawdbot", "moltbot"] as const; const execFileAsync = promisify(execFile); export function renderGatewayServiceCleanupHints( @@ -56,9 +58,14 @@ function resolveHomeDir(env: Record): string { return home; } -function containsMarker(content: string): boolean { +type Marker = (typeof EXTRA_MARKERS)[number]; + +function detectMarker(content: string): Marker | null { const lower = content.toLowerCase(); - return EXTRA_MARKERS.some((marker) => lower.includes(marker)); + for (const marker of EXTRA_MARKERS) { + if (lower.includes(marker)) return marker; + } + return null; } function hasGatewayServiceMarker(content: string): boolean { @@ -111,6 +118,11 @@ function isIgnoredSystemdName(name: string): boolean { return name === resolveGatewaySystemdServiceName(); } +function isLegacyLabel(label: string): boolean { + const lower = label.toLowerCase(); + return lower.includes("clawdbot") || lower.includes("moltbot"); +} + async function scanLaunchdDir(params: { dir: string; scope: "user" | "system"; @@ -134,15 +146,18 @@ async function scanLaunchdDir(params: { } catch { continue; } - if (!containsMarker(contents)) continue; + const marker = detectMarker(contents); + if (!marker) continue; const label = tryExtractPlistLabel(contents) ?? labelFromName; if (isIgnoredLaunchdLabel(label)) continue; - if (isOpenClawGatewayLaunchdService(label, contents)) continue; + if (marker === "openclaw" && isOpenClawGatewayLaunchdService(label, contents)) continue; results.push({ platform: "darwin", label, detail: `plist: ${fullPath}`, scope: params.scope, + marker, + legacy: marker !== "openclaw" || isLegacyLabel(label), }); } @@ -172,13 +187,16 @@ async function scanSystemdDir(params: { } catch { continue; } - if (!containsMarker(contents)) continue; - if (isOpenClawGatewaySystemdService(name, contents)) continue; + const marker = detectMarker(contents); + if (!marker) continue; + if (marker === "openclaw" && isOpenClawGatewaySystemdService(name, contents)) continue; results.push({ platform: "linux", label: entry, detail: `unit: ${fullPath}`, scope: params.scope, + marker, + legacy: marker !== "openclaw", }); } @@ -336,15 +354,21 @@ export async function findExtraGatewayServices( if (isOpenClawGatewayTaskName(name)) continue; const lowerName = name.toLowerCase(); const lowerCommand = task.taskToRun?.toLowerCase() ?? ""; - const matches = EXTRA_MARKERS.some( - (marker) => lowerName.includes(marker) || lowerCommand.includes(marker), - ); - if (!matches) continue; + let marker: Marker | null = null; + for (const candidate of EXTRA_MARKERS) { + if (lowerName.includes(candidate) || lowerCommand.includes(candidate)) { + marker = candidate; + break; + } + } + if (!marker) continue; push({ platform: "win32", label: name, detail: task.taskToRun ? `task: ${name}, run: ${task.taskToRun}` : name, scope: "system", + marker, + legacy: marker !== "openclaw", }); } return results; diff --git a/ui/index.html b/ui/index.html index d42804f47b2..dc03f49115c 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,7 +5,9 @@ OpenClaw Control - + + + diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..71781843f857e274449a0e7a2b151c06d92b5e4f GIT binary patch literal 5920 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 literal 0 HcmV?d00001 diff --git a/ui/public/favicon-32.png b/ui/public/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..563c79b0e6bfa77798f7b508ab6ed290c84b7363 GIT binary patch literal 1015 zcmVj|3GLC)Vz$bm0<)8omzPHZ9 z8x6UBeY5QB{F&G$0Twt9U@AxDsv5I5uwla|ROBz&(Q$~_Pxo;Up0>Bgo~>Hd`n0XB z>oM^`YisMh<;!Cr(02o#39@a4f7rT$o>B(kYD?F9t-88M#Rejr z?~qciC@nZ>a@;4<`$vj1W5NNprZjC$U;~u=FD0^41J{#jh=zKIcnmjE8Tg0gR@x}W zZ)HqRyin`^KnCg=`&{I^-qVVoVl7zuXcV9AIfftKZH7Po-8=KJ_`wL4>^_Q(%}s9H z&d!|}$(KGGR_|Jt>i03=K1 zm*D~KI9p810=I2siL~ipUN}4}6MiELqftbuma5B%X8yVlz@HieV)WuS5-$b~;tRu2 z$yLx@DUEe?#oDH8;`dA{HD5~tVR~S`AQ)qwVzOKySR6otbB_UPtRhR3wifT}dqlyz zrqAL0NF;zru=r>?4I`HW&kIjz40H?(JkbD13Tz_^6;xOHp&%RzspOiFI}XN7Q;Ca0 zBZcWW+myD3JWd-zd_{rHAGEZbh`N)nX1$<&j;CxEpoqCmj3~(8wy|#fLss*(4x_ulyuiu zq#6^0H2yu6{Eo9Wu$dCANlgHim{2|X3jki}k?e{CbkDHM1!1?UHQO`cULnLEQc5wU z31F&Tki>sfjj9f%g#CiCZ>763%2{q=)B!pH^i+2$cF~{BLN^6TRUG z(QWJchX5{Ado1@OP25$|>Z<@26PKv_i3zE`DSV3>8gL;?cE|!eyQL4cH$Y;WNNAHS lL~(o_$LW!U_jrp6@fsk=FmPoZ + + + + + + + + + + + + + + + + + + + + +