feat: 添加设备注册 API 并重构安装横幅组件
This commit is contained in:
@@ -50,23 +50,18 @@ export default function HomePage() {
|
|||||||
<Hero />
|
<Hero />
|
||||||
|
|
||||||
{/* Section 1: Main Map View + Dashboard */}
|
{/* Section 1: Main Map View + Dashboard */}
|
||||||
<section className="min-h-[calc(100vh-5rem)]">
|
<section id="get-started">
|
||||||
<div className="mb-4">
|
<div className="grid gap-4 lg:grid-cols-[240px_1fr_280px]">
|
||||||
<InstallBanner />
|
{/* Left Panel — Stats */}
|
||||||
</div>
|
<div className="flex flex-col gap-3">
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-[280px_1fr_280px]">
|
|
||||||
{/* Left Panel */}
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<StatsPanel />
|
<StatsPanel />
|
||||||
<RegionRanking />
|
<RegionRanking />
|
||||||
<TokenLeaderboard />
|
<TokenLeaderboard />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center - Map/Globe + Timeline */}
|
{/* Center — Map/Globe (hero of the page) */}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* View Switcher + Map Container */}
|
<div className="relative h-[520px] lg:h-[680px]">
|
||||||
<div className="relative h-[500px] lg:h-[600px]">
|
|
||||||
{/* View Mode Toggle */}
|
{/* View Mode Toggle */}
|
||||||
<div className="absolute left-4 top-4 z-20 flex rounded-lg border border-white/10 bg-[var(--bg-card)]/90 p-1 backdrop-blur-sm">
|
<div className="absolute left-4 top-4 z-20 flex rounded-lg border border-white/10 bg-[var(--bg-card)]/90 p-1 backdrop-blur-sm">
|
||||||
<button
|
<button
|
||||||
@@ -95,12 +90,9 @@ export default function HomePage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Map View */}
|
|
||||||
{viewMode === "2d" && (
|
{viewMode === "2d" && (
|
||||||
<WorldMap className="h-full w-full" />
|
<WorldMap className="h-full w-full" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Globe View */}
|
|
||||||
{viewMode === "3d" && (
|
{viewMode === "3d" && (
|
||||||
<GlobeView />
|
<GlobeView />
|
||||||
)}
|
)}
|
||||||
@@ -109,8 +101,9 @@ export default function HomePage() {
|
|||||||
<ActivityTimeline />
|
<ActivityTimeline />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel */}
|
{/* Right Panel — Register + Feed */}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<InstallBanner />
|
||||||
<ClawFeed />
|
<ClawFeed />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
40
app/api/v1/device/name/route.ts
Normal file
40
app/api/v1/device/name/route.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { claws } from "@/lib/db/schema";
|
||||||
|
import { authenticateRequest } from "@/lib/auth/request";
|
||||||
|
import { updateNameSchema } from "@/lib/validators/schemas";
|
||||||
|
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await authenticateRequest(req);
|
||||||
|
if (auth instanceof NextResponse) {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
const { claw } = auth;
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const parsed = updateNameSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Validation failed", details: parsed.error.flatten() },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name } = parsed.data;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(claws)
|
||||||
|
.set({ name, updatedAt: new Date() })
|
||||||
|
.where(eq(claws.id, claw.id));
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, name });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update name error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
150
app/api/v1/device/register/route.ts
Normal file
150
app/api/v1/device/register/route.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { claws } from "@/lib/db/schema";
|
||||||
|
import { redis } from "@/lib/redis";
|
||||||
|
import { generateApiKey } from "@/lib/auth/api-key";
|
||||||
|
import { getGeoLocation } from "@/lib/geo/ip-location";
|
||||||
|
import { applyDeterministicOffset } from "@/lib/geo/offset";
|
||||||
|
import { deviceRegisterSchema } from "@/lib/validators/schemas";
|
||||||
|
|
||||||
|
function getClientIp(req: NextRequest): string {
|
||||||
|
const forwarded = req.headers.get("x-forwarded-for");
|
||||||
|
if (forwarded) return forwarded.split(",")[0].trim();
|
||||||
|
const realIp = req.headers.get("x-real-ip");
|
||||||
|
if (realIp) return realIp.trim();
|
||||||
|
return "127.0.0.1";
|
||||||
|
}
|
||||||
|
|
||||||
|
const RATE_LIMIT_KEY_PREFIX = "ratelimit:device:";
|
||||||
|
const RATE_LIMIT_MAX = 10;
|
||||||
|
const RATE_LIMIT_WINDOW = 3600; // 1 hour
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const clientIp = getClientIp(req);
|
||||||
|
|
||||||
|
// Rate limit check
|
||||||
|
const rateLimitKey = `${RATE_LIMIT_KEY_PREFIX}${clientIp}`;
|
||||||
|
const currentCount = await redis.incr(rateLimitKey);
|
||||||
|
if (currentCount === 1) {
|
||||||
|
await redis.expire(rateLimitKey, RATE_LIMIT_WINDOW);
|
||||||
|
}
|
||||||
|
if (currentCount > RATE_LIMIT_MAX) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Too many requests. Try again later." },
|
||||||
|
{ status: 429 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const parsed = deviceRegisterSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Validation failed", details: parsed.error.flatten() },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { deviceId, name, platform } = parsed.data;
|
||||||
|
|
||||||
|
// Check if device already registered
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(claws)
|
||||||
|
.where(eq(claws.deviceId, deviceId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
clawId: existing[0].id,
|
||||||
|
apiKey: existing[0].apiKey,
|
||||||
|
name: existing[0].name,
|
||||||
|
isNew: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new claw
|
||||||
|
const clawId = nanoid(21);
|
||||||
|
const apiKey = generateApiKey();
|
||||||
|
const geo = await getGeoLocation(clientIp);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
let finalLat: number | null = null;
|
||||||
|
let finalLng: number | null = null;
|
||||||
|
if (geo) {
|
||||||
|
const offset = applyDeterministicOffset(
|
||||||
|
geo.latitude,
|
||||||
|
geo.longitude,
|
||||||
|
clawId
|
||||||
|
);
|
||||||
|
finalLat = offset.lat;
|
||||||
|
finalLng = offset.lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generate name if not provided
|
||||||
|
const clawName = name || generateClawName();
|
||||||
|
|
||||||
|
await db.insert(claws).values({
|
||||||
|
id: clawId,
|
||||||
|
apiKey,
|
||||||
|
deviceId,
|
||||||
|
name: clawName,
|
||||||
|
platform: platform ? platform.slice(0, 20) : null,
|
||||||
|
ip: clientIp,
|
||||||
|
latitude: finalLat !== null ? String(finalLat) : null,
|
||||||
|
longitude: finalLng !== null ? String(finalLng) : null,
|
||||||
|
city: geo?.city ?? null,
|
||||||
|
country: geo?.country ?? null,
|
||||||
|
countryCode: geo?.countryCode ?? null,
|
||||||
|
region: geo?.region ?? null,
|
||||||
|
lastHeartbeat: now,
|
||||||
|
totalTasks: 0,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
clawId,
|
||||||
|
apiKey,
|
||||||
|
name: clawName,
|
||||||
|
isNew: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Device register error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ADJECTIVES = [
|
||||||
|
"Swift",
|
||||||
|
"Brave",
|
||||||
|
"Cool",
|
||||||
|
"Dark",
|
||||||
|
"Fire",
|
||||||
|
"Ice",
|
||||||
|
"Storm",
|
||||||
|
"Shadow",
|
||||||
|
"Cyber",
|
||||||
|
"Neon",
|
||||||
|
"Pixel",
|
||||||
|
"Turbo",
|
||||||
|
"Mega",
|
||||||
|
"Ultra",
|
||||||
|
"Nova",
|
||||||
|
"Star",
|
||||||
|
"Flash",
|
||||||
|
"Thunder",
|
||||||
|
"Iron",
|
||||||
|
"Cosmic",
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateClawName(): string {
|
||||||
|
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
||||||
|
const num = Math.floor(Math.random() * 100);
|
||||||
|
return `${adj}Claw-${num}`;
|
||||||
|
}
|
||||||
@@ -181,6 +181,7 @@ export function ClawFeed() {
|
|||||||
<Badge variant="secondary">{formatDuration(item.durationMs)}</Badge>
|
<Badge variant="secondary">{formatDuration(item.durationMs)}</Badge>
|
||||||
)}
|
)}
|
||||||
<span className="text-[10px] text-[var(--text-muted)]">
|
<span className="text-[10px] text-[var(--text-muted)]">
|
||||||
|
{new Date(item.timestamp).toLocaleDateString(locale, { month: "2-digit", day: "2-digit" })}{" "}
|
||||||
{new Date(item.timestamp).toLocaleTimeString(locale)}
|
{new Date(item.timestamp).toLocaleTimeString(locale)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,36 +8,36 @@ export function Hero() {
|
|||||||
const t = useTranslations("hero");
|
const t = useTranslations("hero");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-16 md:py-24">
|
<section className="relative py-6 md:py-10">
|
||||||
<div className="mx-auto max-w-4xl px-4 text-center">
|
<div className="mx-auto max-w-4xl px-4 text-center">
|
||||||
{/* Badge */}
|
{/* Badge */}
|
||||||
<Badge className="mb-6 px-4 py-1.5 text-sm animate-fade-in">
|
<Badge className="mb-3 px-3 py-1 text-xs animate-fade-in">
|
||||||
{t("badge")}
|
{t("badge")}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
{/* Main Title */}
|
{/* Main Title */}
|
||||||
<h1 className="mb-6 text-4xl font-bold leading-tight tracking-tight md:text-5xl lg:text-6xl glow-text-pulse animate-fade-in-up">
|
<h1 className="mb-3 text-2xl font-bold leading-tight tracking-tight md:text-3xl lg:text-4xl glow-text-pulse animate-fade-in-up">
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
<p className="mb-8 text-lg md:text-xl text-[var(--text-secondary)] max-w-2xl mx-auto animate-fade-in-up animation-delay-200">
|
<p className="mb-4 text-sm md:text-base text-[var(--text-secondary)] max-w-2xl mx-auto animate-fade-in-up animation-delay-200">
|
||||||
{t("subtitle")}
|
{t("subtitle")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
{/* CTA Buttons */}
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-fade-in-up animation-delay-400">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 animate-fade-in-up animation-delay-400">
|
||||||
<a
|
<a
|
||||||
href="#get-started"
|
href="#get-started"
|
||||||
className="group inline-flex items-center gap-2 rounded-lg bg-[var(--accent-cyan)] px-6 py-3 font-semibold text-[var(--bg-primary)] transition-all hover:shadow-[0_0_20px_rgba(0,240,255,0.4)] hover:scale-105"
|
className="group inline-flex items-center gap-2 rounded-lg bg-[var(--accent-cyan)] px-5 py-2.5 text-sm font-semibold text-[var(--bg-primary)] transition-all hover:shadow-[0_0_20px_rgba(0,240,255,0.4)] hover:scale-105"
|
||||||
>
|
>
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
{t("cta")}
|
{t("cta")}
|
||||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-1" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#learn-more"
|
href="#learn-more"
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-[var(--accent-cyan)]/30 bg-transparent px-6 py-3 font-semibold text-[var(--text-primary)] transition-all hover:border-[var(--accent-cyan)]/60 hover:bg-[var(--accent-cyan)]/5"
|
className="inline-flex items-center gap-2 rounded-lg border border-[var(--accent-cyan)]/30 bg-transparent px-5 py-2.5 text-sm font-semibold text-[var(--text-primary)] transition-all hover:border-[var(--accent-cyan)]/60 hover:bg-[var(--accent-cyan)]/5"
|
||||||
>
|
>
|
||||||
{t("secondary")}
|
{t("secondary")}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,124 +1,329 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { Check, Copy, MessageSquare, Bot } from "lucide-react";
|
import {
|
||||||
|
Check,
|
||||||
|
Copy,
|
||||||
|
MessageSquare,
|
||||||
|
Terminal,
|
||||||
|
Pencil,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Bot,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useDeviceToken } from "@/hooks/use-device-token";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type Tab = "openclaw" | "terminal";
|
||||||
|
|
||||||
export function InstallBanner() {
|
export function InstallBanner() {
|
||||||
const t = useTranslations("installBanner");
|
const t = useTranslations("installBanner");
|
||||||
const tGuide = useTranslations("skillGuide");
|
const tGuide = useTranslations("skillGuide");
|
||||||
|
const { token, name, isLoading, error, updateName } = useDeviceToken();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<Tab>("terminal");
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [isEditingName, setIsEditingName] = useState(false);
|
||||||
|
const [editNameValue, setEditNameValue] = useState("");
|
||||||
|
const [nameStatus, setNameStatus] = useState<
|
||||||
|
"idle" | "saving" | "success" | "error"
|
||||||
|
>("idle");
|
||||||
|
const [showToken, setShowToken] = useState(false);
|
||||||
|
|
||||||
const prompt = t("prompt");
|
const maskedToken = token ? `${token.slice(0, 6)}...` : "";
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const openclawPrompt = token ? t("openclawPrompt", { token }) : "";
|
||||||
|
const terminalCommand = token ? t("terminalCommand", { token }) : "";
|
||||||
|
|
||||||
|
const handleCopy = useCallback(async (text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(prompt);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
} catch {
|
} catch {
|
||||||
// fallback: select the text
|
// fallback
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
const handleEditName = useCallback(() => {
|
||||||
|
setEditNameValue(name || "");
|
||||||
|
setIsEditingName(true);
|
||||||
|
setNameStatus("idle");
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
const handleSaveName = useCallback(async () => {
|
||||||
|
if (!editNameValue.trim()) return;
|
||||||
|
setNameStatus("saving");
|
||||||
|
const ok = await updateName(editNameValue.trim());
|
||||||
|
if (ok) {
|
||||||
|
setNameStatus("success");
|
||||||
|
setIsEditingName(false);
|
||||||
|
setTimeout(() => setNameStatus("idle"), 2000);
|
||||||
|
} else {
|
||||||
|
setNameStatus("error");
|
||||||
|
}
|
||||||
|
}, [editNameValue, updateName]);
|
||||||
|
|
||||||
|
const handleCancelEdit = useCallback(() => {
|
||||||
|
setIsEditingName(false);
|
||||||
|
setNameStatus("idle");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<article className="glow-card rounded-xl px-4 py-3">
|
||||||
|
<div className="flex items-center justify-center gap-2 py-3">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-[var(--accent-cyan)]" />
|
||||||
|
<span className="text-xs text-[var(--text-muted)]">
|
||||||
|
{t("generatingToken")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error || !token) {
|
||||||
|
return (
|
||||||
|
<article className="glow-card rounded-xl px-4 py-3">
|
||||||
|
<div className="flex items-center justify-center py-3">
|
||||||
|
<span className="text-xs text-red-400">{t("tokenError")}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className="glow-card rounded-xl px-5 py-4"
|
className="glow-card rounded-xl px-4 py-3"
|
||||||
data-skill="openclaw-reporter"
|
|
||||||
data-install-method="clawhub"
|
|
||||||
aria-label={tGuide("ariaLabel")}
|
aria-label={tGuide("ariaLabel")}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-2.5">
|
||||||
{/* Header */}
|
{/* Compact header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<div
|
<span className="text-base">🦞</span>
|
||||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
|
<span className="text-xs font-medium text-[var(--text-primary)] truncate">
|
||||||
style={{ backgroundColor: "rgba(0, 240, 255, 0.1)" }}
|
{t("title")}
|
||||||
>
|
|
||||||
<span className="text-lg">🦞</span>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
{t("title")}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-[var(--text-muted)]">
|
|
||||||
{t("subtitle")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="group flex items-center gap-1.5 rounded-lg border border-white/10 bg-[var(--bg-primary)] px-3 py-2 transition-all hover:border-[var(--accent-cyan)]/40 hover:bg-[var(--bg-primary)]/80 active:scale-[0.98] cursor-pointer shrink-0"
|
|
||||||
title={t("copyTooltip")}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-3.5 w-3.5 text-[var(--accent-green)]" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3.5 w-3.5 text-[var(--text-muted)] transition-colors group-hover:text-[var(--accent-cyan)]" />
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)]">
|
|
||||||
{copied ? t("copied") : t("copyButton")}
|
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Claw name row */}
|
||||||
|
<div className="flex items-center justify-between gap-2 text-[11px]">
|
||||||
|
<div className="flex items-center gap-1 min-w-0">
|
||||||
|
<span className="text-[var(--text-muted)] shrink-0">
|
||||||
|
{t("yourClaw")}:
|
||||||
|
</span>
|
||||||
|
{isEditingName ? (
|
||||||
|
<div className="flex items-center gap-1 min-w-0">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editNameValue}
|
||||||
|
onChange={(e) => setEditNameValue(e.target.value)}
|
||||||
|
className="w-20 rounded border border-[var(--accent-cyan)]/30 bg-[var(--bg-primary)] px-1.5 py-0.5 text-[11px] text-[var(--text-primary)] outline-none focus:border-[var(--accent-cyan)]"
|
||||||
|
maxLength={30}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") handleSaveName();
|
||||||
|
if (e.key === "Escape") handleCancelEdit();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveName}
|
||||||
|
disabled={nameStatus === "saving"}
|
||||||
|
className="text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 cursor-pointer"
|
||||||
|
>
|
||||||
|
{nameStatus === "saving" ? (
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
className="text-[var(--text-muted)] hover:text-[var(--text-secondary)] text-[10px] cursor-pointer"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1 min-w-0">
|
||||||
|
<span className="font-mono text-[var(--accent-cyan)] truncate">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
{nameStatus === "success" && (
|
||||||
|
<Check className="h-2.5 w-2.5 text-[var(--accent-green)]" />
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleEditName}
|
||||||
|
className="text-[var(--text-muted)] hover:text-[var(--accent-cyan)] cursor-pointer shrink-0"
|
||||||
|
>
|
||||||
|
<Pencil className="h-2.5 w-2.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Token mini display */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowToken(!showToken)}
|
||||||
|
className="flex items-center gap-1 text-[var(--text-muted)] hover:text-[var(--text-secondary)] cursor-pointer shrink-0"
|
||||||
|
title={showToken ? t("hideToken") : t("showToken")}
|
||||||
|
>
|
||||||
|
<code className="font-mono text-[10px]">
|
||||||
|
{showToken ? token.slice(0, 12) + "..." : maskedToken}
|
||||||
|
</code>
|
||||||
|
{showToken ? (
|
||||||
|
<EyeOff className="h-2.5 w-2.5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-2.5 w-2.5" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Human prompt card */}
|
{/* Compact tabs */}
|
||||||
<button
|
<div className="flex rounded-md border border-white/10 bg-[var(--bg-primary)] p-0.5">
|
||||||
onClick={handleCopy}
|
<button
|
||||||
className="group relative w-full cursor-pointer rounded-lg border border-white/10 bg-[var(--bg-primary)] p-3 text-left transition-all hover:border-[var(--accent-cyan)]/30"
|
onClick={() => setActiveTab("openclaw")}
|
||||||
>
|
className={cn(
|
||||||
<div className="flex gap-2">
|
"flex flex-1 items-center justify-center gap-1 rounded px-2 py-1 text-[10px] font-medium transition-all cursor-pointer",
|
||||||
<MessageSquare className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[var(--accent-cyan)]/60" />
|
activeTab === "openclaw"
|
||||||
<p className="text-xs leading-relaxed text-[var(--text-secondary)] select-all">
|
? "bg-[var(--accent-cyan)]/15 text-[var(--accent-cyan)]"
|
||||||
{prompt}
|
: "text-[var(--text-muted)] hover:text-[var(--text-secondary)]"
|
||||||
</p>
|
)}
|
||||||
</div>
|
>
|
||||||
</button>
|
<MessageSquare className="h-3 w-3" />
|
||||||
|
{t("tabOpenClaw")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("terminal")}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-center gap-1 rounded px-2 py-1 text-[10px] font-medium transition-all cursor-pointer",
|
||||||
|
activeTab === "terminal"
|
||||||
|
? "bg-[var(--accent-cyan)]/15 text-[var(--accent-cyan)]"
|
||||||
|
: "text-[var(--text-muted)] hover:text-[var(--text-secondary)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Terminal className="h-3 w-3" />
|
||||||
|
{t("tabTerminal")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* AI-crawlable installation guide (collapsed for humans, fully in DOM for crawlers) */}
|
{/* Tab content */}
|
||||||
|
{activeTab === "openclaw" && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(openclawPrompt)}
|
||||||
|
className="group relative w-full cursor-pointer rounded-md border border-white/10 bg-[var(--bg-primary)] p-2.5 text-left transition-all hover:border-[var(--accent-cyan)]/30"
|
||||||
|
>
|
||||||
|
<p className="text-[11px] leading-relaxed text-[var(--text-secondary)] select-all break-all line-clamp-4">
|
||||||
|
{openclawPrompt}
|
||||||
|
</p>
|
||||||
|
<div className="absolute right-1.5 top-1.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-3 w-3 text-[var(--accent-green)]" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3 w-3 text-[var(--text-muted)]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(openclawPrompt)}
|
||||||
|
className="group flex w-full items-center justify-center gap-1 rounded-md border border-white/10 bg-[var(--bg-primary)] py-1.5 text-[10px] transition-all hover:border-[var(--accent-cyan)]/40 cursor-pointer"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-2.5 w-2.5 text-[var(--accent-green)]" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-2.5 w-2.5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)]" />
|
||||||
|
)}
|
||||||
|
<span className="text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)]">
|
||||||
|
{copied ? t("copied") : t("copyPrompt")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "terminal" && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="rounded-md border border-white/10 bg-[var(--bg-primary)] p-2.5">
|
||||||
|
<div className="flex items-start gap-1.5">
|
||||||
|
<span className="font-mono text-[11px] text-[var(--accent-green)] select-none">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
<code className="font-mono text-[11px] leading-relaxed text-[var(--accent-cyan)] select-all break-all">
|
||||||
|
{terminalCommand}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(terminalCommand)}
|
||||||
|
className="group flex w-full items-center justify-center gap-1 rounded-md border border-white/10 bg-[var(--bg-primary)] py-1.5 text-[10px] transition-all hover:border-[var(--accent-cyan)]/40 cursor-pointer"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-2.5 w-2.5 text-[var(--accent-green)]" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-2.5 w-2.5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)]" />
|
||||||
|
)}
|
||||||
|
<span className="text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)]">
|
||||||
|
{copied ? t("copied") : t("copyCommand")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI-crawlable guide (collapsed) */}
|
||||||
<details className="group/details">
|
<details className="group/details">
|
||||||
<summary className="flex cursor-pointer items-center gap-1.5 text-[10px] text-[var(--text-muted)] hover:text-[var(--text-secondary)]">
|
<summary className="flex cursor-pointer items-center gap-1 text-[10px] text-[var(--text-muted)] hover:text-[var(--text-secondary)]">
|
||||||
<Bot className="h-3 w-3" />
|
<Bot className="h-2.5 w-2.5" />
|
||||||
<span>{tGuide("summaryLabel")}</span>
|
<span>{tGuide("summaryLabel")}</span>
|
||||||
</summary>
|
</summary>
|
||||||
|
<div className="mt-2 space-y-2 rounded-md border border-white/5 bg-[var(--bg-primary)] p-3 text-[10px] leading-relaxed text-[var(--text-muted)]">
|
||||||
<div className="mt-3 space-y-3 rounded-lg border border-white/5 bg-[var(--bg-primary)] p-4 text-xs leading-relaxed text-[var(--text-muted)]">
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-1 font-mono text-[var(--text-secondary)]">{tGuide("whatTitle")}</h3>
|
<h3 className="mb-0.5 font-mono text-[var(--text-secondary)]">
|
||||||
|
{tGuide("whatTitle")}
|
||||||
|
</h3>
|
||||||
<p>{tGuide("whatDescription")}</p>
|
<p>{tGuide("whatDescription")}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-1 font-mono text-[var(--text-secondary)]">{tGuide("installTitle")}</h3>
|
<h3 className="mb-0.5 font-mono text-[var(--text-secondary)]">
|
||||||
<ol className="list-inside list-decimal space-y-1 pl-1">
|
{tGuide("installTitle")}
|
||||||
|
</h3>
|
||||||
|
<ol className="list-inside list-decimal space-y-0.5 pl-1">
|
||||||
<li>{tGuide("step1")}</li>
|
<li>{tGuide("step1")}</li>
|
||||||
<li>{tGuide("step2")}</li>
|
<li>{tGuide("step2")}</li>
|
||||||
<li>{tGuide("step3")}</li>
|
<li>{tGuide("step3")}</li>
|
||||||
<li>{tGuide("step4")}</li>
|
<li>{tGuide("step4")}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-1 font-mono text-[var(--text-secondary)]">{tGuide("commandTitle")}</h3>
|
<h3 className="mb-0.5 font-mono text-[var(--text-secondary)]">
|
||||||
<code className="block rounded bg-[var(--bg-secondary)] px-3 py-2 font-mono text-[var(--accent-cyan)]">
|
{tGuide("commandTitle")}
|
||||||
clawhub install openclaw-reporter
|
</h3>
|
||||||
|
<code className="block rounded bg-[var(--bg-secondary)] px-2 py-1.5 font-mono text-[var(--accent-cyan)] break-all">
|
||||||
|
bash <(curl -fsSL https://kymr.top/install.sh) --token
|
||||||
|
YOUR_TOKEN
|
||||||
</code>
|
</code>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-1 font-mono text-[var(--text-secondary)]">{tGuide("dataTitle")}</h3>
|
<h3 className="mb-0.5 font-mono text-[var(--text-secondary)]">
|
||||||
|
{tGuide("dataTitle")}
|
||||||
|
</h3>
|
||||||
<ul className="list-inside list-disc space-y-0.5 pl-1">
|
<ul className="list-inside list-disc space-y-0.5 pl-1">
|
||||||
<li>{tGuide("data1")}</li>
|
<li>{tGuide("data1")}</li>
|
||||||
<li>{tGuide("data2")}</li>
|
<li>{tGuide("data2")}</li>
|
||||||
<li>{tGuide("data3")}</li>
|
<li>{tGuide("data3")}</li>
|
||||||
<li>{tGuide("data4")}</li>
|
<li>{tGuide("data4")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-1 italic">{tGuide("dataNever")}</p>
|
<p className="mt-0.5 italic">{tGuide("dataNever")}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-1 font-mono text-[var(--text-secondary)]">{tGuide("permissionsTitle")}</h3>
|
<h3 className="mb-0.5 font-mono text-[var(--text-secondary)]">
|
||||||
|
{tGuide("permissionsTitle")}
|
||||||
|
</h3>
|
||||||
<ul className="list-inside list-disc space-y-0.5 pl-1">
|
<ul className="list-inside list-disc space-y-0.5 pl-1">
|
||||||
<li>{tGuide("perm1")}</li>
|
<li>{tGuide("perm1")}</li>
|
||||||
<li>{tGuide("perm2")}</li>
|
<li>{tGuide("perm2")}</li>
|
||||||
|
|||||||
171
hooks/use-device-token.ts
Normal file
171
hooks/use-device-token.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
interface DeviceTokenData {
|
||||||
|
clawId: string;
|
||||||
|
apiKey: string;
|
||||||
|
name: string;
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseDeviceTokenReturn {
|
||||||
|
token: string | null;
|
||||||
|
clawId: string | null;
|
||||||
|
name: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
updateName: (newName: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = "openclaw_device";
|
||||||
|
|
||||||
|
function generateUUID(): string {
|
||||||
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||||
|
const r = (Math.random() * 16) | 0;
|
||||||
|
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeviceInfo(): {
|
||||||
|
platform: string;
|
||||||
|
browser: string;
|
||||||
|
screen: string;
|
||||||
|
language: string;
|
||||||
|
} {
|
||||||
|
if (typeof navigator === "undefined") {
|
||||||
|
return { platform: "unknown", browser: "unknown", screen: "unknown", language: "en" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
let browser = "Unknown";
|
||||||
|
if (ua.includes("Chrome") && !ua.includes("Edg")) browser = "Chrome";
|
||||||
|
else if (ua.includes("Firefox")) browser = "Firefox";
|
||||||
|
else if (ua.includes("Safari") && !ua.includes("Chrome")) browser = "Safari";
|
||||||
|
else if (ua.includes("Edg")) browser = "Edge";
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform: navigator.platform || "unknown",
|
||||||
|
browser,
|
||||||
|
screen:
|
||||||
|
typeof screen !== "undefined"
|
||||||
|
? `${screen.width}x${screen.height}`
|
||||||
|
: "unknown",
|
||||||
|
language: navigator.language || "en",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedData(): DeviceTokenData | null {
|
||||||
|
if (typeof localStorage === "undefined") return null;
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
return JSON.parse(raw) as DeviceTokenData;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCachedData(data: DeviceTokenData): void {
|
||||||
|
if (typeof localStorage === "undefined") return;
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCachedName(newName: string): void {
|
||||||
|
const data = getCachedData();
|
||||||
|
if (data) {
|
||||||
|
setCachedData({ ...data, name: newName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeviceToken(): UseDeviceTokenReturn {
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
const [clawId, setClawId] = useState<string | null>(null);
|
||||||
|
const [name, setName] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
// Check cache first
|
||||||
|
const cached = getCachedData();
|
||||||
|
if (cached) {
|
||||||
|
setToken(cached.apiKey);
|
||||||
|
setClawId(cached.clawId);
|
||||||
|
setName(cached.name);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register new device
|
||||||
|
try {
|
||||||
|
const deviceId = generateUUID();
|
||||||
|
const deviceInfo = getDeviceInfo();
|
||||||
|
|
||||||
|
const res = await fetch("/api/v1/device/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
deviceId,
|
||||||
|
...deviceInfo,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Registration failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const tokenData: DeviceTokenData = {
|
||||||
|
clawId: data.clawId,
|
||||||
|
apiKey: data.apiKey,
|
||||||
|
name: data.name,
|
||||||
|
deviceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
setCachedData(tokenData);
|
||||||
|
setToken(data.apiKey);
|
||||||
|
setClawId(data.clawId);
|
||||||
|
setName(data.name);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Registration failed");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateName = useCallback(
|
||||||
|
async (newName: string): Promise<boolean> => {
|
||||||
|
if (!token) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/v1/device/name", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: newName }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return false;
|
||||||
|
|
||||||
|
setName(newName);
|
||||||
|
updateCachedName(newName);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[token]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { token, clawId, name, isLoading, error, updateName };
|
||||||
|
}
|
||||||
@@ -11,24 +11,29 @@ import {
|
|||||||
} from "drizzle-orm/mysql-core";
|
} from "drizzle-orm/mysql-core";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
export const claws = mysqlTable("claws", {
|
export const claws = mysqlTable(
|
||||||
id: varchar("id", { length: 21 }).primaryKey(),
|
"claws",
|
||||||
apiKey: varchar("api_key", { length: 64 }).notNull().unique(),
|
{
|
||||||
name: varchar("name", { length: 100 }).notNull(),
|
id: varchar("id", { length: 21 }).primaryKey(),
|
||||||
platform: varchar("platform", { length: 20 }),
|
apiKey: varchar("api_key", { length: 64 }).notNull().unique(),
|
||||||
model: varchar("model", { length: 50 }),
|
deviceId: varchar("device_id", { length: 64 }),
|
||||||
ip: varchar("ip", { length: 45 }),
|
name: varchar("name", { length: 100 }).notNull(),
|
||||||
latitude: decimal("latitude", { precision: 10, scale: 7 }),
|
platform: varchar("platform", { length: 20 }),
|
||||||
longitude: decimal("longitude", { precision: 10, scale: 7 }),
|
model: varchar("model", { length: 50 }),
|
||||||
city: varchar("city", { length: 100 }),
|
ip: varchar("ip", { length: 45 }),
|
||||||
country: varchar("country", { length: 100 }),
|
latitude: decimal("latitude", { precision: 10, scale: 7 }),
|
||||||
countryCode: varchar("country_code", { length: 5 }),
|
longitude: decimal("longitude", { precision: 10, scale: 7 }),
|
||||||
region: varchar("region", { length: 50 }),
|
city: varchar("city", { length: 100 }),
|
||||||
lastHeartbeat: datetime("last_heartbeat"),
|
country: varchar("country", { length: 100 }),
|
||||||
totalTasks: int("total_tasks").default(0),
|
countryCode: varchar("country_code", { length: 5 }),
|
||||||
createdAt: datetime("created_at").default(sql`NOW()`),
|
region: varchar("region", { length: 50 }),
|
||||||
updatedAt: datetime("updated_at").default(sql`NOW()`),
|
lastHeartbeat: datetime("last_heartbeat"),
|
||||||
});
|
totalTasks: int("total_tasks").default(0),
|
||||||
|
createdAt: datetime("created_at").default(sql`NOW()`),
|
||||||
|
updatedAt: datetime("updated_at").default(sql`NOW()`),
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex("claws_device_id_unq").on(table.deviceId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const heartbeats = mysqlTable(
|
export const heartbeats = mysqlTable(
|
||||||
"heartbeats",
|
"heartbeats",
|
||||||
|
|||||||
@@ -27,7 +27,26 @@ export const tokenSchema = z.object({
|
|||||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deviceRegisterSchema = z.object({
|
||||||
|
deviceId: z.string().min(1).max(64),
|
||||||
|
name: z.string().min(1).max(100).optional(),
|
||||||
|
platform: z.string().max(50).optional(),
|
||||||
|
browser: z.string().max(100).optional(),
|
||||||
|
screen: z.string().max(20).optional(),
|
||||||
|
language: z.string().max(10).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateNameSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(30)
|
||||||
|
.regex(/^[A-Za-z0-9_-]+$/, "Only alphanumeric, hyphens, and underscores"),
|
||||||
|
});
|
||||||
|
|
||||||
export type RegisterInput = z.infer<typeof registerSchema>;
|
export type RegisterInput = z.infer<typeof registerSchema>;
|
||||||
export type HeartbeatInput = z.infer<typeof heartbeatSchema>;
|
export type HeartbeatInput = z.infer<typeof heartbeatSchema>;
|
||||||
export type TaskInput = z.infer<typeof taskSchema>;
|
export type TaskInput = z.infer<typeof taskSchema>;
|
||||||
export type TokenInput = z.infer<typeof tokenSchema>;
|
export type TokenInput = z.infer<typeof tokenSchema>;
|
||||||
|
export type DeviceRegisterInput = z.infer<typeof deviceRegisterSchema>;
|
||||||
|
export type UpdateNameInput = z.infer<typeof updateNameSchema>;
|
||||||
|
|||||||
@@ -17,12 +17,29 @@
|
|||||||
"live": "Live"
|
"live": "Live"
|
||||||
},
|
},
|
||||||
"installBanner": {
|
"installBanner": {
|
||||||
"title": "Join the Heatmap",
|
"title": "Register Your Agent",
|
||||||
"subtitle": "Copy and paste this into Claude Code to get started",
|
"subtitle": "Get your agent on the global map in seconds",
|
||||||
"copyTooltip": "Copy prompt to clipboard",
|
"tabOpenClaw": "OpenClaw",
|
||||||
|
"tabTerminal": "Terminal",
|
||||||
|
"openclawPrompt": "Help me join the OpenClaw global heatmap! Run this install command in the terminal: bash <(curl -fsSL https://kymr.top/install.sh) --token {token}",
|
||||||
|
"terminalCommand": "bash <(curl -fsSL https://kymr.top/install.sh) --token {token}",
|
||||||
"copyButton": "Copy",
|
"copyButton": "Copy",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"prompt": "Help me join the OpenClaw global heatmap! First, search for the \"openclaw-reporter\" skill on ClawHub and install it. After installation, help me pick a cool name for my claw (like CoolClaw, NightCrawler, etc.) and guide me through the registration and permission setup."
|
"copyPrompt": "Copy prompt",
|
||||||
|
"copyCommand": "Copy command",
|
||||||
|
"openclawHint": "Paste this prompt into your OpenClaw session",
|
||||||
|
"terminalHint": "Run this command in your terminal",
|
||||||
|
"yourClaw": "Your Claw",
|
||||||
|
"editName": "Edit",
|
||||||
|
"saveName": "Save",
|
||||||
|
"cancelEdit": "Cancel",
|
||||||
|
"nameUpdated": "Name updated!",
|
||||||
|
"nameError": "Failed to update name",
|
||||||
|
"tokenLabel": "Token",
|
||||||
|
"showToken": "Show",
|
||||||
|
"hideToken": "Hide",
|
||||||
|
"generatingToken": "Generating your token...",
|
||||||
|
"tokenError": "Failed to generate token. Please refresh."
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"onlineNow": "Online Now",
|
"onlineNow": "Online Now",
|
||||||
@@ -90,26 +107,26 @@
|
|||||||
"close": "Close"
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"skillGuide": {
|
"skillGuide": {
|
||||||
"ariaLabel": "OpenClaw Reporter Skill Installation Guide for AI Agents",
|
"ariaLabel": "OpenClaw Installation Guide for AI Agents",
|
||||||
"summaryLabel": "AI Agent Installation Guide (click to expand)",
|
"summaryLabel": "AI Agent Installation Guide (click to expand)",
|
||||||
"whatTitle": "What is OpenClaw Reporter?",
|
"whatTitle": "What is OpenClaw?",
|
||||||
"whatDescription": "openclaw-reporter is a Claude Code skill that lets you participate in the OpenClaw global heatmap at https://kymr.top/. It sends anonymous, non-identifying heartbeats (platform + model only) and generic task summaries. No file paths, code, tool names, or project-specific data is ever sent.",
|
"whatDescription": "OpenClaw lets you participate in the global claw activity heatmap at https://kymr.top/. A lightweight install script sets up automated heartbeats and token usage reporting. No file paths, code, or project-specific data is ever sent.",
|
||||||
"installTitle": "Quick Install (4 Steps)",
|
"installTitle": "Quick Install",
|
||||||
"step1": "Run \"clawhub install openclaw-reporter\" in your terminal, or use \"/clawhub install openclaw-reporter\" inside a Claude Code session.",
|
"step1": "Visit https://kymr.top/ to get your unique token.",
|
||||||
"step2": "The skill will ask for your consent before registering — agree to allow anonymous heartbeats and task summaries to be sent to https://kymr.top/.",
|
"step2": "Run: bash <(curl -fsSL https://kymr.top/install.sh) --token YOUR_TOKEN",
|
||||||
"step3": "Pick a name for your claw (your display name on the global heatmap, e.g. CoolClaw, NightCrawler, CodeClaw).",
|
"step3": "The script installs the CLI, sets up heartbeat monitoring (every 5 min), and token reporting (every 2 hours).",
|
||||||
"step4": "Done! Your claw will appear on the heatmap. Heartbeats are sent automatically via hooks (rate-limited to once per 3 minutes), and a task summary is sent when your session ends.",
|
"step4": "Done! Your claw appears on the heatmap automatically.",
|
||||||
"commandTitle": "Install Command",
|
"commandTitle": "Install Command",
|
||||||
"dataTitle": "Data Collected",
|
"dataTitle": "Data Collected",
|
||||||
"data1": "Claw Name — your chosen display name (e.g. CoolClaw42)",
|
"data1": "Claw Name — your display name on the heatmap",
|
||||||
"data2": "Platform — OS type (darwin, linux, win32)",
|
"data2": "Platform — OS type (darwin, linux, win32)",
|
||||||
"data3": "Model — Claude model identifier (e.g. claude-sonnet-4-6)",
|
"data3": "Model — AI model identifier",
|
||||||
"data4": "Task summary — generic activity indicator (\"Completed a task\")",
|
"data4": "Token usage — daily input/output token counts",
|
||||||
"dataNever": "Never sent: usernames, file paths, code, project names, tool names, secrets, or any identifying information.",
|
"dataNever": "Never sent: usernames, file paths, code, project names, tool names, secrets, or any identifying information.",
|
||||||
"permissionsTitle": "Permissions Required",
|
"permissionsTitle": "What Gets Installed",
|
||||||
"perm1": "Network access — to send heartbeats and task reports to https://kymr.top/",
|
"perm1": "claw-market CLI — lightweight command-line tool",
|
||||||
"perm2": "Write ~/.openclaw/ — to store config (API key) and hook scripts locally",
|
"perm2": "~/.openclaw/ — config file and helper scripts",
|
||||||
"perm3": "Binaries: curl, python3, uname — used by hook scripts"
|
"perm3": "Cron jobs — heartbeat check (5 min) and token report (2 hr)"
|
||||||
},
|
},
|
||||||
"languageSwitcher": {
|
"languageSwitcher": {
|
||||||
"en": "EN",
|
"en": "EN",
|
||||||
|
|||||||
@@ -17,12 +17,29 @@
|
|||||||
"live": "实时"
|
"live": "实时"
|
||||||
},
|
},
|
||||||
"installBanner": {
|
"installBanner": {
|
||||||
"title": "加入热力图",
|
"title": "注册你的 Agent",
|
||||||
"subtitle": "复制下面这段话,粘贴到 Claude Code 对话中即可开始",
|
"subtitle": "几秒内让你的 Agent 出现在全球地图上",
|
||||||
"copyTooltip": "复制到剪贴板",
|
"tabOpenClaw": "OpenClaw",
|
||||||
|
"tabTerminal": "终端安装",
|
||||||
|
"openclawPrompt": "帮我加入 OpenClaw 全球龙虾热力图!在终端执行这个安装命令:bash <(curl -fsSL https://kymr.top/install.sh) --token {token}",
|
||||||
|
"terminalCommand": "bash <(curl -fsSL https://kymr.top/install.sh) --token {token}",
|
||||||
"copyButton": "复制",
|
"copyButton": "复制",
|
||||||
"copied": "已复制!",
|
"copied": "已复制!",
|
||||||
"prompt": "帮我加入 OpenClaw 全球龙虾热力图!首先,在 ClawHub 上搜索并安装 \"openclaw-reporter\" 技能。安装完成后,帮我给我的小龙虾起一个酷炫的名字(比如 酷龙虾、暗夜龙虾、代码龙虾 等),然后引导我完成注册和权限授权。"
|
"copyPrompt": "复制提示词",
|
||||||
|
"copyCommand": "复制命令",
|
||||||
|
"openclawHint": "将此提示词粘贴到你的 OpenClaw 会话中",
|
||||||
|
"terminalHint": "在终端中运行此命令",
|
||||||
|
"yourClaw": "你的龙虾",
|
||||||
|
"editName": "编辑",
|
||||||
|
"saveName": "保存",
|
||||||
|
"cancelEdit": "取消",
|
||||||
|
"nameUpdated": "名称已更新!",
|
||||||
|
"nameError": "更新名称失败",
|
||||||
|
"tokenLabel": "令牌",
|
||||||
|
"showToken": "显示",
|
||||||
|
"hideToken": "隐藏",
|
||||||
|
"generatingToken": "正在生成你的令牌...",
|
||||||
|
"tokenError": "生成令牌失败,请刷新页面。"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"onlineNow": "当前在线",
|
"onlineNow": "当前在线",
|
||||||
@@ -90,26 +107,26 @@
|
|||||||
"close": "关闭"
|
"close": "关闭"
|
||||||
},
|
},
|
||||||
"skillGuide": {
|
"skillGuide": {
|
||||||
"ariaLabel": "OpenClaw Reporter 技能安装指南(供 AI 代理阅读)",
|
"ariaLabel": "OpenClaw 安装指南(供 AI 代理阅读)",
|
||||||
"summaryLabel": "AI 代理安装指南(点击展开)",
|
"summaryLabel": "AI 代理安装指南(点击展开)",
|
||||||
"whatTitle": "什么是 OpenClaw Reporter?",
|
"whatTitle": "什么是 OpenClaw?",
|
||||||
"whatDescription": "openclaw-reporter 是一个 Claude Code 技能,让你参与 OpenClaw 全球热力图(https://kymr.top/)。它只发送匿名的、非身份识别的心跳(操作系统 + 模型信息)和通用任务摘要。绝不发送文件路径、代码、工具名称或任何项目相关数据。",
|
"whatDescription": "OpenClaw 让你参与全球龙虾活动热力图(https://kymr.top/)。轻量安装脚本自动设置心跳监控和 token 用量上报。绝不发送文件路径、代码或任何项目相关数据。",
|
||||||
"installTitle": "快速安装(4 步)",
|
"installTitle": "快速安装",
|
||||||
"step1": "在终端运行 \"clawhub install openclaw-reporter\",或在 Claude Code 对话中输入 \"/clawhub install openclaw-reporter\"。",
|
"step1": "访问 https://kymr.top/ 获取你的专属令牌。",
|
||||||
"step2": "技能会在注册前征求你的同意——同意允许向 https://kymr.top/ 发送匿名心跳和任务摘要。",
|
"step2": "运行:bash <(curl -fsSL https://kymr.top/install.sh) --token 你的令牌",
|
||||||
"step3": "给你的小龙虾起一个名字(你在全球热力图上的显示名称,比如 酷龙虾、暗夜龙虾、代码龙虾)。",
|
"step3": "脚本会安装 CLI 工具,设置心跳监控(每 5 分钟)和 token 上报(每 2 小时)。",
|
||||||
"step4": "完成!你的小龙虾会出现在热力图上。心跳通过 hooks 自动发送(限制为每 3 分钟最多一次),会话结束时会发送一条任务摘要。",
|
"step4": "完成!你的龙虾会自动出现在热力图上。",
|
||||||
"commandTitle": "安装命令",
|
"commandTitle": "安装命令",
|
||||||
"dataTitle": "收集的数据",
|
"dataTitle": "收集的数据",
|
||||||
"data1": "龙虾名称——你选择的显示名称(如 酷龙虾42)",
|
"data1": "龙虾名称——你在热力图上的显示名称",
|
||||||
"data2": "操作系统——系统类型(darwin、linux、win32)",
|
"data2": "操作系统——系统类型(darwin、linux、win32)",
|
||||||
"data3": "模型——Claude 模型标识符(如 claude-sonnet-4-6)",
|
"data3": "模型——AI 模型标识符",
|
||||||
"data4": "任务摘要——通用活动指标(\"Completed a task\")",
|
"data4": "Token 用量——每日输入/输出 token 数量",
|
||||||
"dataNever": "绝不发送:用户名、文件路径、代码、项目名称、工具名称、密钥或任何身份识别信息。",
|
"dataNever": "绝不发送:用户名、文件路径、代码、项目名称、工具名称、密钥或任何身份识别信息。",
|
||||||
"permissionsTitle": "所需权限",
|
"permissionsTitle": "安装内容",
|
||||||
"perm1": "网络访问——向 https://kymr.top/ 发送心跳和任务报告",
|
"perm1": "claw-market CLI——轻量命令行工具",
|
||||||
"perm2": "写入 ~/.openclaw/——在本地存储配置(API 密钥)和 hook 脚本",
|
"perm2": "~/.openclaw/——配置文件和辅助脚本",
|
||||||
"perm3": "系统工具:curl、python3、uname——供 hook 脚本使用"
|
"perm3": "定时任务——心跳检测(5 分钟)和 token 上报(2 小时)"
|
||||||
},
|
},
|
||||||
"languageSwitcher": {
|
"languageSwitcher": {
|
||||||
"en": "EN",
|
"en": "EN",
|
||||||
|
|||||||
465
package-lock.json
generated
465
package-lock.json
generated
@@ -47,6 +47,7 @@
|
|||||||
"eslint-config-next": "^15.3.0",
|
"eslint-config-next": "^15.3.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^4.1.0",
|
"tailwindcss": "^4.1.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.8.0"
|
"typescript": "^5.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -8316,6 +8317,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-intl/node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.19",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||||
|
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/nanoid": {
|
"node_modules/next/node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -10335,6 +10347,459 @@
|
|||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/esbuild": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/esbuild/-/esbuild-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.4",
|
||||||
|
"@esbuild/android-arm": "0.27.4",
|
||||||
|
"@esbuild/android-arm64": "0.27.4",
|
||||||
|
"@esbuild/android-x64": "0.27.4",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.4",
|
||||||
|
"@esbuild/darwin-x64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.4",
|
||||||
|
"@esbuild/linux-arm": "0.27.4",
|
||||||
|
"@esbuild/linux-arm64": "0.27.4",
|
||||||
|
"@esbuild/linux-ia32": "0.27.4",
|
||||||
|
"@esbuild/linux-loong64": "0.27.4",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.4",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.4",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.4",
|
||||||
|
"@esbuild/linux-s390x": "0.27.4",
|
||||||
|
"@esbuild/linux-x64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.4",
|
||||||
|
"@esbuild/sunos-x64": "0.27.4",
|
||||||
|
"@esbuild/win32-arm64": "0.27.4",
|
||||||
|
"@esbuild/win32-ia32": "0.27.4",
|
||||||
|
"@esbuild/win32-x64": "0.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|||||||
291
public/install.sh
Normal file
291
public/install.sh
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OpenClaw Market — One-click installer
|
||||||
|
# Usage: bash <(curl -fsSL https://kymr.top/install.sh) --token <TOKEN>
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
VERSION="1.0.0"
|
||||||
|
API_ENDPOINT="https://kymr.top/api/v1"
|
||||||
|
CONFIG_DIR="$HOME/.openclaw"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||||
|
HEARTBEAT_SCRIPT="$CONFIG_DIR/heartbeat-check.sh"
|
||||||
|
TRACK_TOKENS_SCRIPT="$CONFIG_DIR/track-tokens.sh"
|
||||||
|
REPORT_TOKENS_SCRIPT="$CONFIG_DIR/report-tokens.sh"
|
||||||
|
TOKEN_USAGE_FILE="$CONFIG_DIR/token-usage.json"
|
||||||
|
CRON_TAG="# openclaw-market"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[✗]${NC} $1"; }
|
||||||
|
step() { echo -e "${CYAN}[→]${NC} $1"; }
|
||||||
|
|
||||||
|
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||||
|
TOKEN=""
|
||||||
|
UNINSTALL=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--token) TOKEN="$2"; shift 2 ;;
|
||||||
|
--token=*) TOKEN="${1#*=}"; shift ;;
|
||||||
|
--uninstall) UNINSTALL=true; shift ;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: bash install.sh --token <TOKEN>"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --token <TOKEN> Your OpenClaw token (get it from https://kymr.top)"
|
||||||
|
echo " --uninstall Remove OpenClaw and all cron jobs"
|
||||||
|
echo " --help Show this help"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) error "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Uninstall ───────────────────────────────────────────────────────────────
|
||||||
|
if [ "$UNINSTALL" = true ]; then
|
||||||
|
step "Uninstalling OpenClaw..."
|
||||||
|
|
||||||
|
# Remove cron jobs
|
||||||
|
crontab -l 2>/dev/null | grep -v "$CRON_TAG" | crontab - 2>/dev/null || true
|
||||||
|
info "Cron jobs removed"
|
||||||
|
|
||||||
|
# Remove config directory
|
||||||
|
if [ -d "$CONFIG_DIR" ]; then
|
||||||
|
rm -rf "$CONFIG_DIR"
|
||||||
|
info "Config directory removed: $CONFIG_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optionally remove CLI
|
||||||
|
if command -v claw-market &>/dev/null; then
|
||||||
|
warn "CLI 'claw-market' is still installed. Run 'npm uninstall -g @ricardweii/claw-market' to remove it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "OpenClaw uninstalled successfully!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Validate token ──────────────────────────────────────────────────────────
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
error "Token is required. Usage: bash install.sh --token <YOUR_TOKEN>"
|
||||||
|
echo ""
|
||||||
|
echo "Get your token at https://kymr.top"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🦞 OpenClaw Market Installer v${VERSION}${NC}"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Check prerequisites ────────────────────────────────────────────────────
|
||||||
|
step "Checking prerequisites..."
|
||||||
|
|
||||||
|
if ! command -v node &>/dev/null; then
|
||||||
|
error "Node.js is required but not installed."
|
||||||
|
echo " Install it from https://nodejs.org/ or use nvm:"
|
||||||
|
echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
info "Node.js $(node --version) found"
|
||||||
|
|
||||||
|
if ! command -v npm &>/dev/null; then
|
||||||
|
error "npm is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
info "npm $(npm --version) found"
|
||||||
|
|
||||||
|
# ── Install CLI ─────────────────────────────────────────────────────────────
|
||||||
|
step "Installing claw-market CLI..."
|
||||||
|
|
||||||
|
if command -v claw-market &>/dev/null; then
|
||||||
|
info "claw-market CLI already installed"
|
||||||
|
else
|
||||||
|
npm install -g @ricardweii/claw-market 2>/dev/null || {
|
||||||
|
warn "Global install failed, trying with sudo..."
|
||||||
|
sudo npm install -g @ricardweii/claw-market
|
||||||
|
}
|
||||||
|
info "claw-market CLI installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Fetch claw info from server ─────────────────────────────────────────────
|
||||||
|
step "Verifying token with server..."
|
||||||
|
|
||||||
|
CLAW_INFO=$(curl -sf -H "Authorization: Bearer $TOKEN" "${API_ENDPOINT}/heartbeat" \
|
||||||
|
-X POST -H "Content-Type: application/json" -d '{}' 2>/dev/null) || {
|
||||||
|
error "Invalid token or server unreachable."
|
||||||
|
echo " Check your token and try again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
info "Token verified successfully"
|
||||||
|
|
||||||
|
# ── Write config ────────────────────────────────────────────────────────────
|
||||||
|
step "Writing configuration..."
|
||||||
|
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
chmod 700 "$CONFIG_DIR"
|
||||||
|
|
||||||
|
cat > "$CONFIG_FILE" << EOF
|
||||||
|
{
|
||||||
|
"apiKey": "$TOKEN",
|
||||||
|
"endpoint": "$API_ENDPOINT"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
info "Config written to $CONFIG_FILE"
|
||||||
|
|
||||||
|
# ── Create heartbeat check script ──────────────────────────────────────────
|
||||||
|
step "Setting up heartbeat checker..."
|
||||||
|
|
||||||
|
cat > "$HEARTBEAT_SCRIPT" << 'HEARTBEAT_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# OpenClaw heartbeat checker — runs every 5 minutes via cron
|
||||||
|
# Checks if openclaw process is running, sends heartbeat if so
|
||||||
|
|
||||||
|
export PATH="$HOME/.nvm/versions/node/$(ls "$HOME/.nvm/versions/node/" 2>/dev/null | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||||
|
|
||||||
|
if pgrep -f "openclaw" > /dev/null 2>&1; then
|
||||||
|
claw-market heartbeat 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
HEARTBEAT_EOF
|
||||||
|
chmod +x "$HEARTBEAT_SCRIPT"
|
||||||
|
info "Heartbeat checker created"
|
||||||
|
|
||||||
|
# ── Create token tracking script ───────────────────────────────────────────
|
||||||
|
step "Setting up token tracker..."
|
||||||
|
|
||||||
|
cat > "$TRACK_TOKENS_SCRIPT" << 'TRACK_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# OpenClaw token tracker — call with: track-tokens.sh <input> <output>
|
||||||
|
# Accumulates daily token usage in ~/.openclaw/token-usage.json
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.openclaw"
|
||||||
|
TOKEN_FILE="$CONFIG_DIR/token-usage.json"
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
INPUT=${1:-0}
|
||||||
|
OUTPUT=${2:-0}
|
||||||
|
|
||||||
|
# Validate numeric
|
||||||
|
if ! [[ "$INPUT" =~ ^[0-9]+$ ]] || ! [[ "$OUTPUT" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Usage: track-tokens.sh <input_tokens> <output_tokens>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read existing data or init
|
||||||
|
if [ -f "$TOKEN_FILE" ]; then
|
||||||
|
EXISTING_DATE=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('date',''))" 2>/dev/null || echo "")
|
||||||
|
if [ "$EXISTING_DATE" = "$TODAY" ]; then
|
||||||
|
EXISTING_INPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('inputTokens',0))" 2>/dev/null || echo "0")
|
||||||
|
EXISTING_OUTPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('outputTokens',0))" 2>/dev/null || echo "0")
|
||||||
|
INPUT=$((INPUT + EXISTING_INPUT))
|
||||||
|
OUTPUT=$((OUTPUT + EXISTING_OUTPUT))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write updated data
|
||||||
|
cat > "$TOKEN_FILE" << EOF
|
||||||
|
{
|
||||||
|
"date": "$TODAY",
|
||||||
|
"inputTokens": $INPUT,
|
||||||
|
"outputTokens": $OUTPUT,
|
||||||
|
"updatedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Token usage updated: input=$INPUT, output=$OUTPUT (date=$TODAY)"
|
||||||
|
TRACK_EOF
|
||||||
|
chmod +x "$TRACK_TOKENS_SCRIPT"
|
||||||
|
info "Token tracker created"
|
||||||
|
|
||||||
|
# ── Create token reporter script ───────────────────────────────────────────
|
||||||
|
step "Setting up token reporter..."
|
||||||
|
|
||||||
|
cat > "$REPORT_TOKENS_SCRIPT" << 'REPORT_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# OpenClaw token reporter — runs every 2 hours via cron
|
||||||
|
# Reads accumulated token usage and reports to server
|
||||||
|
|
||||||
|
export PATH="$HOME/.nvm/versions/node/$(ls "$HOME/.nvm/versions/node/" 2>/dev/null | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.openclaw"
|
||||||
|
TOKEN_FILE="$CONFIG_DIR/token-usage.json"
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
# Check if token usage file exists
|
||||||
|
if [ ! -f "$TOKEN_FILE" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
DATE=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('date',''))" 2>/dev/null || echo "")
|
||||||
|
INPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('inputTokens',0))" 2>/dev/null || echo "0")
|
||||||
|
OUTPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('outputTokens',0))" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
# Only report if there's data and it's for today
|
||||||
|
if [ "$INPUT" = "0" ] && [ "$OUTPUT" = "0" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report via CLI
|
||||||
|
if [ -n "$DATE" ]; then
|
||||||
|
claw-market token "$INPUT" "$OUTPUT" --date "$DATE" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
claw-market token "$INPUT" "$OUTPUT" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
REPORT_EOF
|
||||||
|
chmod +x "$REPORT_TOKENS_SCRIPT"
|
||||||
|
info "Token reporter created"
|
||||||
|
|
||||||
|
# ── Set up cron jobs ────────────────────────────────────────────────────────
|
||||||
|
step "Setting up scheduled tasks..."
|
||||||
|
|
||||||
|
# Remove existing openclaw cron jobs
|
||||||
|
EXISTING_CRON=$(crontab -l 2>/dev/null | grep -v "$CRON_TAG" || true)
|
||||||
|
|
||||||
|
# Add new cron jobs
|
||||||
|
NEW_CRON="$EXISTING_CRON
|
||||||
|
*/5 * * * * $HEARTBEAT_SCRIPT $CRON_TAG
|
||||||
|
7 */2 * * * $REPORT_TOKENS_SCRIPT $CRON_TAG"
|
||||||
|
|
||||||
|
echo "$NEW_CRON" | crontab -
|
||||||
|
info "Cron jobs installed:"
|
||||||
|
echo " - Heartbeat check: every 5 minutes"
|
||||||
|
echo " - Token report: every 2 hours"
|
||||||
|
|
||||||
|
# ── Send initial heartbeat ──────────────────────────────────────────────────
|
||||||
|
step "Sending initial heartbeat..."
|
||||||
|
|
||||||
|
claw-market heartbeat 2>/dev/null && {
|
||||||
|
info "Initial heartbeat sent"
|
||||||
|
} || {
|
||||||
|
warn "Initial heartbeat failed (non-blocking)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Done ────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${GREEN}🦞 OpenClaw installed successfully!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " Your claw is now active on the heatmap."
|
||||||
|
echo " View it at: https://kymr.top"
|
||||||
|
echo ""
|
||||||
|
echo " Scripts installed:"
|
||||||
|
echo " $HEARTBEAT_SCRIPT — heartbeat (every 5 min)"
|
||||||
|
echo " $TRACK_TOKENS_SCRIPT — track token usage"
|
||||||
|
echo " $REPORT_TOKENS_SCRIPT — report tokens (every 2 hr)"
|
||||||
|
echo ""
|
||||||
|
echo " To track tokens manually:"
|
||||||
|
echo " ~/.openclaw/track-tokens.sh <input> <output>"
|
||||||
|
echo ""
|
||||||
|
echo " To uninstall:"
|
||||||
|
echo " bash <(curl -fsSL https://kymr.top/install.sh) --uninstall"
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user