mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: migrate legacy gateway services
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
---
|
---
|
||||||
summary: "Use Xiaomi MiMo (mimo-v2-flash) with Moltbot"
|
summary: "Use Xiaomi MiMo (mimo-v2-flash) with OpenClaw"
|
||||||
read_when:
|
read_when:
|
||||||
- You want Xiaomi MiMo models in Moltbot
|
- You want Xiaomi MiMo models in OpenClaw
|
||||||
- You need XIAOMI_API_KEY setup
|
- You need XIAOMI_API_KEY setup
|
||||||
---
|
---
|
||||||
# Xiaomi MiMo
|
# Xiaomi MiMo
|
||||||
|
|
||||||
Xiaomi MiMo is the API platform for **MiMo** models. It provides REST APIs compatible with
|
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
|
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.
|
the `xiaomi` provider with a Xiaomi MiMo API key.
|
||||||
|
|
||||||
## Model overview
|
## Model overview
|
||||||
@@ -20,9 +20,9 @@ the `xiaomi` provider with a Xiaomi MiMo API key.
|
|||||||
## CLI setup
|
## CLI setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
moltbot onboard --auth-choice xiaomi-api-key
|
openclaw onboard --auth-choice xiaomi-api-key
|
||||||
# or non-interactive
|
# 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
|
## Config snippet
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openclaw",
|
"name": "openclaw",
|
||||||
"version": "2026.1.29-beta.1",
|
"version": "2026.1.29-beta.2",
|
||||||
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -64,10 +64,11 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {}
|
|||||||
|
|
||||||
const LOBSTER_ASCII = [
|
const LOBSTER_ASCII = [
|
||||||
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
|
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
|
||||||
"█████░█████░█████░█░░░█░█████░█░░░░░░███░░█░░░█",
|
"█████░█████░█████░█░░░█░█████░█░░░░░█████░█░░░█",
|
||||||
"█░░░█░█░░░█░███░░░██░░█░█░░░░░█░░░░░█░░░█░█░█░█",
|
"█░░░█░█░░░█░█░░░░░██░░█░█░░░░░█░░░░░█░░░█░█░░░█",
|
||||||
"█████░████░░█████░█░░██░█████░█████░█████░██░██",
|
"█░░░█░█████░████░░█░█░█░█░░░░░█░░░░░█████░█░█░█",
|
||||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
|
"█░░░█░█░░░░░█░░░░░█░░██░█░░░░░█░░░░░█░░░█░██░██",
|
||||||
|
"█████░█░░░░░█████░█░░░█░█████░█████░█░░░█░█░░░█",
|
||||||
" 🦞 OPENCLAW 🦞 ",
|
" 🦞 OPENCLAW 🦞 ",
|
||||||
" ",
|
" ",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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 path from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.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 { DEFAULT_GATEWAY_DAEMON_RUNTIME, type GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||||
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
|
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
function detectGatewayRuntime(programArguments: string[] | undefined): GatewayDaemonRuntime {
|
function detectGatewayRuntime(programArguments: string[] | undefined): GatewayDaemonRuntime {
|
||||||
const first = programArguments?.[0];
|
const first = programArguments?.[0];
|
||||||
if (first) {
|
if (first) {
|
||||||
@@ -37,6 +43,42 @@ function normalizeExecutablePath(value: string): string {
|
|||||||
return path.resolve(value);
|
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<string | null> {
|
||||||
|
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(
|
export async function maybeRepairGatewayServiceConfig(
|
||||||
cfg: OpenClawConfig,
|
cfg: OpenClawConfig,
|
||||||
mode: "local" | "remote",
|
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, {
|
const extraServices = await findExtraGatewayServices(process.env, {
|
||||||
deep: options.deep,
|
deep: options.deep,
|
||||||
});
|
});
|
||||||
@@ -161,6 +207,47 @@ export async function maybeScanExtraGatewayServices(options: DoctorOptions) {
|
|||||||
"Other gateway-like services detected",
|
"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();
|
const cleanupHints = renderGatewayServiceCleanupHints();
|
||||||
if (cleanupHints.length > 0) {
|
if (cleanupHints.length > 0) {
|
||||||
note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints");
|
note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints");
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export async function doctorCommand(
|
|||||||
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
|
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
|
||||||
noteSandboxScopeWarnings(cfg);
|
noteSandboxScopeWarnings(cfg);
|
||||||
|
|
||||||
await maybeScanExtraGatewayServices(options);
|
await maybeScanExtraGatewayServices(options, runtime, prompter);
|
||||||
await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter);
|
await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter);
|
||||||
await noteMacLaunchAgentOverrides();
|
await noteMacLaunchAgentOverrides();
|
||||||
await noteMacLaunchctlGatewayEnvOverrides(cfg);
|
await noteMacLaunchctlGatewayEnvOverrides(cfg);
|
||||||
|
|||||||
@@ -65,9 +65,11 @@ export function randomToken(): string {
|
|||||||
export function printWizardHeader(runtime: RuntimeEnv) {
|
export function printWizardHeader(runtime: RuntimeEnv) {
|
||||||
const header = [
|
const header = [
|
||||||
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
|
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
|
||||||
"█████░█████░█████░█░░░█░█████░█░░░░░░███░░█░░░█",
|
"█████░█████░█████░█░░░█░█████░█░░░░░█████░█░░░█",
|
||||||
"█░░░█░█░░░█░███░░░██░░█░█░░░░░█░░░░░█░░░█░█░█░█",
|
"█░░░█░█░░░█░█░░░░░██░░█░█░░░░░█░░░░░█░░░█░█░░░█",
|
||||||
"█████░████░░█████░█░░██░█████░█████░█████░██░██",
|
"█░░░█░█████░████░░█░█░█░█░░░░░█░░░░░█████░█░█░█",
|
||||||
|
"█░░░█░█░░░░░█░░░░░█░░██░█░░░░░█░░░░░█░░░█░██░██",
|
||||||
|
"█████░█░░░░░█████░█░░░█░█████░█████░█░░░█░█░░░█",
|
||||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
|
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
|
||||||
" 🦞 FRESH DAILY 🦞 ",
|
" 🦞 FRESH DAILY 🦞 ",
|
||||||
" ",
|
" ",
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ export type ExtraGatewayService = {
|
|||||||
label: string;
|
label: string;
|
||||||
detail: string;
|
detail: string;
|
||||||
scope: "user" | "system";
|
scope: "user" | "system";
|
||||||
|
marker?: "openclaw" | "clawdbot" | "moltbot";
|
||||||
|
legacy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FindExtraGatewayServicesOptions = {
|
export type FindExtraGatewayServicesOptions = {
|
||||||
deep?: boolean;
|
deep?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXTRA_MARKERS = ["openclaw"];
|
const EXTRA_MARKERS = ["openclaw", "clawdbot", "moltbot"] as const;
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
export function renderGatewayServiceCleanupHints(
|
export function renderGatewayServiceCleanupHints(
|
||||||
@@ -56,9 +58,14 @@ function resolveHomeDir(env: Record<string, string | undefined>): string {
|
|||||||
return home;
|
return home;
|
||||||
}
|
}
|
||||||
|
|
||||||
function containsMarker(content: string): boolean {
|
type Marker = (typeof EXTRA_MARKERS)[number];
|
||||||
|
|
||||||
|
function detectMarker(content: string): Marker | null {
|
||||||
const lower = content.toLowerCase();
|
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 {
|
function hasGatewayServiceMarker(content: string): boolean {
|
||||||
@@ -111,6 +118,11 @@ function isIgnoredSystemdName(name: string): boolean {
|
|||||||
return name === resolveGatewaySystemdServiceName();
|
return name === resolveGatewaySystemdServiceName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLegacyLabel(label: string): boolean {
|
||||||
|
const lower = label.toLowerCase();
|
||||||
|
return lower.includes("clawdbot") || lower.includes("moltbot");
|
||||||
|
}
|
||||||
|
|
||||||
async function scanLaunchdDir(params: {
|
async function scanLaunchdDir(params: {
|
||||||
dir: string;
|
dir: string;
|
||||||
scope: "user" | "system";
|
scope: "user" | "system";
|
||||||
@@ -134,15 +146,18 @@ async function scanLaunchdDir(params: {
|
|||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!containsMarker(contents)) continue;
|
const marker = detectMarker(contents);
|
||||||
|
if (!marker) continue;
|
||||||
const label = tryExtractPlistLabel(contents) ?? labelFromName;
|
const label = tryExtractPlistLabel(contents) ?? labelFromName;
|
||||||
if (isIgnoredLaunchdLabel(label)) continue;
|
if (isIgnoredLaunchdLabel(label)) continue;
|
||||||
if (isOpenClawGatewayLaunchdService(label, contents)) continue;
|
if (marker === "openclaw" && isOpenClawGatewayLaunchdService(label, contents)) continue;
|
||||||
results.push({
|
results.push({
|
||||||
platform: "darwin",
|
platform: "darwin",
|
||||||
label,
|
label,
|
||||||
detail: `plist: ${fullPath}`,
|
detail: `plist: ${fullPath}`,
|
||||||
scope: params.scope,
|
scope: params.scope,
|
||||||
|
marker,
|
||||||
|
legacy: marker !== "openclaw" || isLegacyLabel(label),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,13 +187,16 @@ async function scanSystemdDir(params: {
|
|||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!containsMarker(contents)) continue;
|
const marker = detectMarker(contents);
|
||||||
if (isOpenClawGatewaySystemdService(name, contents)) continue;
|
if (!marker) continue;
|
||||||
|
if (marker === "openclaw" && isOpenClawGatewaySystemdService(name, contents)) continue;
|
||||||
results.push({
|
results.push({
|
||||||
platform: "linux",
|
platform: "linux",
|
||||||
label: entry,
|
label: entry,
|
||||||
detail: `unit: ${fullPath}`,
|
detail: `unit: ${fullPath}`,
|
||||||
scope: params.scope,
|
scope: params.scope,
|
||||||
|
marker,
|
||||||
|
legacy: marker !== "openclaw",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,15 +354,21 @@ export async function findExtraGatewayServices(
|
|||||||
if (isOpenClawGatewayTaskName(name)) continue;
|
if (isOpenClawGatewayTaskName(name)) continue;
|
||||||
const lowerName = name.toLowerCase();
|
const lowerName = name.toLowerCase();
|
||||||
const lowerCommand = task.taskToRun?.toLowerCase() ?? "";
|
const lowerCommand = task.taskToRun?.toLowerCase() ?? "";
|
||||||
const matches = EXTRA_MARKERS.some(
|
let marker: Marker | null = null;
|
||||||
(marker) => lowerName.includes(marker) || lowerCommand.includes(marker),
|
for (const candidate of EXTRA_MARKERS) {
|
||||||
);
|
if (lowerName.includes(candidate) || lowerCommand.includes(candidate)) {
|
||||||
if (!matches) continue;
|
marker = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!marker) continue;
|
||||||
push({
|
push({
|
||||||
platform: "win32",
|
platform: "win32",
|
||||||
label: name,
|
label: name,
|
||||||
detail: task.taskToRun ? `task: ${name}, run: ${task.taskToRun}` : name,
|
detail: task.taskToRun ? `task: ${name}, run: ${task.taskToRun}` : name,
|
||||||
scope: "system",
|
scope: "system",
|
||||||
|
marker,
|
||||||
|
legacy: marker !== "openclaw",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OpenClaw Control</title>
|
<title>OpenClaw Control</title>
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<openclaw-app></openclaw-app>
|
<openclaw-app></openclaw-app>
|
||||||
|
|||||||
BIN
ui/public/apple-touch-icon.png
Normal file
BIN
ui/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
BIN
ui/public/favicon-32.png
Normal file
BIN
ui/public/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1015 B |
22
ui/public/favicon.svg
Normal file
22
ui/public/favicon.svg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ff4d4d"/>
|
||||||
|
<stop offset="100%" stop-color="#991b1b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<!-- Body -->
|
||||||
|
<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
|
||||||
|
<!-- Left Claw -->
|
||||||
|
<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
|
||||||
|
<!-- Right Claw -->
|
||||||
|
<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
|
||||||
|
<!-- Antenna -->
|
||||||
|
<path d="M45 15 Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M75 15 Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<!-- Eyes -->
|
||||||
|
<circle cx="45" cy="35" r="6" fill="#050810"/>
|
||||||
|
<circle cx="75" cy="35" r="6" fill="#050810"/>
|
||||||
|
<circle cx="46" cy="34" r="2.5" fill="#00e5cc"/>
|
||||||
|
<circle cx="76" cy="34" r="2.5" fill="#00e5cc"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user