feat: 生成脚本
This commit is contained in:
122
scripts/README.md
Normal file
122
scripts/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# 模拟数据脚本
|
||||
|
||||
本目录包含用于生成和管理 OpenClaw Market 模拟数据的脚本。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 一键生成模拟数据(清理 + 生成 + 同步 Redis)
|
||||
pnpm seed:mock
|
||||
```
|
||||
|
||||
## 单独脚本
|
||||
|
||||
### 1. 清理数据 (`clear-mock-data.ts`)
|
||||
|
||||
清理所有模拟数据,包括:
|
||||
- MySQL 数据库中的 claws、heartbeats、tasks、token_usage、geo_cache 表
|
||||
- Redis 中的所有相关缓存
|
||||
|
||||
```bash
|
||||
pnpm seed:clear
|
||||
```
|
||||
|
||||
### 2. 生成模拟数据 (`generate-mock-data.ts`)
|
||||
|
||||
生成约 240 只模拟龙虾,特点:
|
||||
- **地区分布**:按真实 AI 使用情况分配
|
||||
- Americas: 35%
|
||||
- Asia: 30%
|
||||
- Europe: 28%
|
||||
- Oceania: 5%
|
||||
- Africa: 2%
|
||||
- **名字生成**:形容词 + 名词 + 后缀(如 `SwiftCoderPrime`)
|
||||
- **平台分布**:claude-code (40%), cursor (25%), copilot (15%) 等
|
||||
- **模型分布**:claude-sonnet-4-6 (35%), claude-haiku-4-5 (20%) 等
|
||||
- **活跃程度**:
|
||||
- 重度用户 (10%):每天 50-500 万 tokens
|
||||
- 中度用户 (30%):每天 10-50 万 tokens
|
||||
- 轻度用户 (40%):每天 2-10 万 tokens
|
||||
- 极少用户 (20%):每天 1千-2 万 tokens
|
||||
- **注册时间**:过去 7 天内随机分布
|
||||
|
||||
```bash
|
||||
pnpm seed:generate
|
||||
```
|
||||
|
||||
### 3. 同步 Redis 统计 (`sync-redis-stats.ts`)
|
||||
|
||||
根据数据库数据初始化 Redis 统计:
|
||||
- 全局统计(总虾数、总任务数、总 tokens)
|
||||
- 地区统计
|
||||
- 活跃 claws
|
||||
- 每小时活动数据
|
||||
|
||||
```bash
|
||||
pnpm seed:sync
|
||||
```
|
||||
|
||||
### 4. 模拟实时活动 (`simulate-live-activity.ts`)
|
||||
|
||||
持续模拟心跳、任务和 token 上报,让数据看起来是"活"的。
|
||||
|
||||
```bash
|
||||
pnpm seed:live
|
||||
```
|
||||
|
||||
按 `Ctrl+C` 停止。
|
||||
|
||||
## 数据结构
|
||||
|
||||
### Claws 表字段
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | 唯一标识 (nanoid) |
|
||||
| apiKey | API 密钥 |
|
||||
| name | 龙虾名称 |
|
||||
| platform | 平台 (claude-code, cursor 等) |
|
||||
| model | 模型 (claude-sonnet-4-6 等) |
|
||||
| latitude/longitude | 地理坐标 |
|
||||
| city/country/countryCode | 地理位置 |
|
||||
| region | 大洲 |
|
||||
| lastHeartbeat | 最后心跳时间 |
|
||||
| totalTasks | 总任务数 |
|
||||
| createdAt | 注册时间 |
|
||||
|
||||
### 关联数据
|
||||
- **heartbeats**: 心跳记录
|
||||
- **tasks**: 任务记录(摘要、时长、工具)
|
||||
- **token_usage**: Token 使用记录(按天)
|
||||
|
||||
## 环境要求
|
||||
|
||||
运行脚本前需要配置:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
DATABASE_URL=mysql://user:password@host:3306/database
|
||||
REDIS_URL=redis://host:6379
|
||||
```
|
||||
|
||||
## 自定义配置
|
||||
|
||||
如需调整生成参数,编辑 `generate-mock-data.ts` 顶部的配置:
|
||||
|
||||
```typescript
|
||||
const TOTAL_CLAWS = 240; // 总虾数
|
||||
const DAYS_BACK = 7; // 注册时间范围(天)
|
||||
|
||||
const REGION_DISTRIBUTION = [ // 地区分布权重
|
||||
{ region: "Americas", weight: 35, countries: [...] },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
## 数据量参考
|
||||
|
||||
生成 240 只虾大约产生:
|
||||
- 240 条 claws 记录
|
||||
- 5,000-10,000 条 heartbeats 记录
|
||||
- 2,000-5,000 条 tasks 记录
|
||||
- 1,000-1,500 条 token_usage 记录
|
||||
- 总计约 50-200M tokens
|
||||
102
scripts/clear-mock-data.ts
Normal file
102
scripts/clear-mock-data.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 清理模拟数据脚本
|
||||
* 删除所有 claws 数据(以及关联的 heartbeats, tasks, token_usage)
|
||||
*
|
||||
* 使用方法: npx tsx scripts/clear-mock-data.ts
|
||||
*/
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { claws, heartbeats, tasks, tokenUsage, geoCache } from "@/lib/db/schema";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { redis, redisSubscriber } from "@/lib/redis";
|
||||
|
||||
async function clearMockData() {
|
||||
console.log("🧹 开始清理数据...\n");
|
||||
|
||||
try {
|
||||
// 1. 清理 token_usage
|
||||
console.log("🗑️ 清理 token_usage...");
|
||||
const tokenResult = await db.delete(tokenUsage);
|
||||
console.log(` ✅ 已删除所有 token_usage 记录\n`);
|
||||
|
||||
// 2. 清理 tasks
|
||||
console.log("🗑️ 清理 tasks...");
|
||||
await db.delete(tasks);
|
||||
console.log(` ✅ 已删除所有 tasks 记录\n`);
|
||||
|
||||
// 3. 清理 heartbeats
|
||||
console.log("🗑️ 清理 heartbeats...");
|
||||
await db.delete(heartbeats);
|
||||
console.log(` ✅ 已删除所有 heartbeats 记录\n`);
|
||||
|
||||
// 4. 清理 claws
|
||||
console.log("🗑️ 清理 claws...");
|
||||
await db.delete(claws);
|
||||
console.log(` ✅ 已删除所有 claws 记录\n`);
|
||||
|
||||
// 5. 清理 geo_cache(可选)
|
||||
console.log("🗑️ 清理 geo_cache...");
|
||||
await db.delete(geoCache);
|
||||
console.log(` ✅ 已删除所有 geo_cache 记录\n`);
|
||||
|
||||
// 6. 清理 Redis 数据
|
||||
console.log("🗑️ 清理 Redis 数据...");
|
||||
|
||||
// 获取所有相关的 Redis keys
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
// 在线状态
|
||||
const onlineKeys = await redis.keys("claw:online:*");
|
||||
keysToDelete.push(...onlineKeys);
|
||||
|
||||
// 活跃 claws
|
||||
const activeKeys = await redis.keys("active_claws*");
|
||||
keysToDelete.push(...activeKeys);
|
||||
|
||||
// 地区统计
|
||||
const regionKeys = await redis.keys("region:*");
|
||||
keysToDelete.push(...regionKeys);
|
||||
|
||||
// 全局统计
|
||||
const globalKeys = await redis.keys("global:*");
|
||||
keysToDelete.push(...globalKeys);
|
||||
|
||||
// 每小时活动
|
||||
const hourlyKeys = await redis.keys("hourly:*");
|
||||
keysToDelete.push(...hourlyKeys);
|
||||
|
||||
// 热力图缓存
|
||||
const heatmapKeys = await redis.keys("heatmap:*");
|
||||
keysToDelete.push(...heatmapKeys);
|
||||
|
||||
// Token 排行榜缓存
|
||||
const leaderboardKeys = await redis.keys("token_leaderboard:*");
|
||||
keysToDelete.push(...leaderboardKeys);
|
||||
|
||||
// Token 统计缓存
|
||||
const tokenStatsKeys = await redis.keys("token_stats:*");
|
||||
keysToDelete.push(...tokenStatsKeys);
|
||||
|
||||
if (keysToDelete.length > 0) {
|
||||
await redis.del(...keysToDelete);
|
||||
console.log(` ✅ 已删除 ${keysToDelete.length} 个 Redis keys\n`);
|
||||
} else {
|
||||
console.log(` ℹ️ 没有 Redis keys 需要删除\n`);
|
||||
}
|
||||
|
||||
console.log("=" .repeat(50));
|
||||
console.log("✨ 数据清理完成!\n");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ 清理失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行
|
||||
clearMockData()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => {
|
||||
console.error("❌ 错误:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
655
scripts/generate-mock-data.ts
Normal file
655
scripts/generate-mock-data.ts
Normal file
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* 模拟数据生成脚本
|
||||
* 生成约 240 只龙虾,分布在全球各个地区
|
||||
*
|
||||
* 使用方法: npx tsx scripts/generate-mock-data.ts
|
||||
*/
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { claws, heartbeats, tasks, tokenUsage } from "@/lib/db/schema";
|
||||
import { nanoid } from "nanoid";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
// ============ 配置 ============
|
||||
const TOTAL_CLAWS = 240;
|
||||
const DAYS_BACK = 7;
|
||||
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||
|
||||
// ============ 地区分布数据 ============
|
||||
// 按真实 AI 使用情况分配比例
|
||||
const REGION_DISTRIBUTION = [
|
||||
{ region: "Americas", weight: 35, countries: ["US", "CA", "BR", "MX", "AR"] },
|
||||
{ region: "Asia", weight: 30, countries: ["CN", "JP", "KR", "IN", "SG", "HK", "TW"] },
|
||||
{ region: "Europe", weight: 28, countries: ["GB", "DE", "FR", "NL", "SE", "PL", "IT", "ES"] },
|
||||
{ region: "Oceania", weight: 5, countries: ["AU", "NZ"] },
|
||||
{ region: "Africa", weight: 2, countries: ["ZA", "NG", "KE", "EG"] },
|
||||
];
|
||||
|
||||
// 城市数据(国家代码 -> 城市列表 + 坐标范围)
|
||||
const CITIES_DATA: Record<string, { name: string; lat: number; lng: number }[]> = {
|
||||
// Americas
|
||||
US: [
|
||||
{ name: "San Francisco", lat: 37.7749, lng: -122.4194 },
|
||||
{ name: "New York", lat: 40.7128, lng: -74.006 },
|
||||
{ name: "Seattle", lat: 47.6062, lng: -122.3321 },
|
||||
{ name: "Los Angeles", lat: 34.0522, lng: -118.2437 },
|
||||
{ name: "Boston", lat: 42.3601, lng: -71.0589 },
|
||||
{ name: "Austin", lat: 30.2672, lng: -97.7431 },
|
||||
{ name: "Chicago", lat: 41.8781, lng: -87.6298 },
|
||||
{ name: "Denver", lat: 39.7392, lng: -104.9903 },
|
||||
],
|
||||
CA: [
|
||||
{ name: "Toronto", lat: 43.6532, lng: -79.3832 },
|
||||
{ name: "Vancouver", lat: 49.2827, lng: -123.1207 },
|
||||
{ name: "Montreal", lat: 45.5017, lng: -73.5673 },
|
||||
],
|
||||
BR: [
|
||||
{ name: "Sao Paulo", lat: -23.5505, lng: -46.6333 },
|
||||
{ name: "Rio de Janeiro", lat: -22.9068, lng: -43.1729 },
|
||||
],
|
||||
MX: [{ name: "Mexico City", lat: 19.4326, lng: -99.1332 }],
|
||||
AR: [{ name: "Buenos Aires", lat: -34.6037, lng: -58.3816 }],
|
||||
|
||||
// Asia
|
||||
CN: [
|
||||
{ name: "Beijing", lat: 39.9042, lng: 116.4074 },
|
||||
{ name: "Shanghai", lat: 31.2304, lng: 121.4737 },
|
||||
{ name: "Shenzhen", lat: 22.5431, lng: 114.0579 },
|
||||
{ name: "Hangzhou", lat: 30.2741, lng: 120.1551 },
|
||||
{ name: "Guangzhou", lat: 23.1291, lng: 113.2644 },
|
||||
{ name: "Chengdu", lat: 30.5728, lng: 104.0668 },
|
||||
],
|
||||
JP: [
|
||||
{ name: "Tokyo", lat: 35.6762, lng: 139.6503 },
|
||||
{ name: "Osaka", lat: 34.6937, lng: 135.5023 },
|
||||
],
|
||||
KR: [{ name: "Seoul", lat: 37.5665, lng: 126.978 }],
|
||||
IN: [
|
||||
{ name: "Bangalore", lat: 12.9716, lng: 77.5946 },
|
||||
{ name: "Mumbai", lat: 19.076, lng: 72.8777 },
|
||||
{ name: "Delhi", lat: 28.7041, lng: 77.1025 },
|
||||
],
|
||||
SG: [{ name: "Singapore", lat: 1.3521, lng: 103.8198 }],
|
||||
HK: [{ name: "Hong Kong", lat: 22.3193, lng: 114.1694 }],
|
||||
TW: [{ name: "Taipei", lat: 25.033, lng: 121.5654 }],
|
||||
|
||||
// Europe
|
||||
GB: [
|
||||
{ name: "London", lat: 51.5074, lng: -0.1278 },
|
||||
{ name: "Manchester", lat: 53.4808, lng: -2.2426 },
|
||||
],
|
||||
DE: [
|
||||
{ name: "Berlin", lat: 52.52, lng: 13.405 },
|
||||
{ name: "Munich", lat: 48.1351, lng: 11.582 },
|
||||
{ name: "Frankfurt", lat: 50.1109, lng: 8.6821 },
|
||||
],
|
||||
FR: [{ name: "Paris", lat: 48.8566, lng: 2.3522 }],
|
||||
NL: [
|
||||
{ name: "Amsterdam", lat: 52.3676, lng: 4.9041 },
|
||||
{ name: "Eindhoven", lat: 51.4416, lng: 5.4697 },
|
||||
],
|
||||
SE: [{ name: "Stockholm", lat: 59.3293, lng: 18.0686 }],
|
||||
PL: [{ name: "Warsaw", lat: 52.2297, lng: 21.0122 }],
|
||||
IT: [{ name: "Milan", lat: 45.4642, lng: 9.19 }],
|
||||
ES: [{ name: "Madrid", lat: 40.4168, lng: -3.7038 }],
|
||||
|
||||
// Oceania
|
||||
AU: [
|
||||
{ name: "Sydney", lat: -33.8688, lng: 151.2093 },
|
||||
{ name: "Melbourne", lat: -37.8136, lng: 144.9631 },
|
||||
],
|
||||
NZ: [{ name: "Auckland", lat: -36.8509, lng: 174.7645 }],
|
||||
|
||||
// Africa
|
||||
ZA: [{ name: "Cape Town", lat: -33.9249, lng: 18.4241 }],
|
||||
NG: [{ name: "Lagos", lat: 6.5244, lng: 3.3792 }],
|
||||
KE: [{ name: "Nairobi", lat: -1.2921, lng: 36.8219 }],
|
||||
EG: [{ name: "Cairo", lat: 30.0444, lng: 31.2357 }],
|
||||
};
|
||||
|
||||
// 国家全名
|
||||
const COUNTRY_NAMES: Record<string, string> = {
|
||||
US: "United States",
|
||||
CA: "Canada",
|
||||
BR: "Brazil",
|
||||
MX: "Mexico",
|
||||
AR: "Argentina",
|
||||
CN: "China",
|
||||
JP: "Japan",
|
||||
KR: "South Korea",
|
||||
IN: "India",
|
||||
SG: "Singapore",
|
||||
HK: "Hong Kong",
|
||||
TW: "Taiwan",
|
||||
GB: "United Kingdom",
|
||||
DE: "Germany",
|
||||
FR: "France",
|
||||
NL: "Netherlands",
|
||||
SE: "Sweden",
|
||||
PL: "Poland",
|
||||
IT: "Italy",
|
||||
ES: "Spain",
|
||||
AU: "Australia",
|
||||
NZ: "New Zealand",
|
||||
ZA: "South Africa",
|
||||
NG: "Nigeria",
|
||||
KE: "Kenya",
|
||||
EG: "Egypt",
|
||||
};
|
||||
|
||||
// ============ 名字生成 ============
|
||||
const ADJECTIVES = [
|
||||
"Swift", "Brave", "Clever", "Silent", "Mystic", "Noble", "Fierce", "Gentle",
|
||||
"Wise", "Bold", "Sharp", "Quick", "Calm", "Wild", "Bright", "Dark",
|
||||
"Golden", "Silver", "Iron", "Steel", "Crystal", "Shadow", "Storm", "Thunder",
|
||||
"Phoenix", "Dragon", "Tiger", "Eagle", "Wolf", "Bear", "Lion", "Falcon",
|
||||
"Cosmic", "Stellar", "Nova", "Quantum", "Cyber", "Neural", "Atomic", "Solar",
|
||||
"Arctic", "Desert", "Ocean", "Mountain", "Forest", "River", "Valley", "Peak",
|
||||
];
|
||||
|
||||
const NOUNS = [
|
||||
"Coder", "Hacker", "Builder", "Maker", "Creator", "Developer", "Engineer",
|
||||
"Architect", "Designer", "Pioneer", "Explorer", "Seeker", "Hunter", "Guardian",
|
||||
"Sentinel", "Warrior", "Scholar", "Sage", "Wizard", "Alchemist", "Artisan",
|
||||
"Craftsman", "Visionary", "Innovator", "Strategist", "Analyst", "Researcher",
|
||||
"Navigator", "Pilot", "Captain", "Commander", "Champion", "Master", "Expert",
|
||||
"Ninja", "Samurai", "Knight", "Ranger", "Scout", "Agent", "Operator", "Pilot",
|
||||
];
|
||||
|
||||
const SUFFIXES = [
|
||||
"", "", "", "", // 大部分没有后缀
|
||||
"Prime", "Alpha", "Beta", "Gamma", "Delta", "Omega",
|
||||
"X", "Z", "Pro", "Max", "Ultra", "Mega", "Super",
|
||||
"AI", "Bot", "Agent", "Claw", "Byte", "Node", "Core",
|
||||
];
|
||||
|
||||
function generateName(index: number): string {
|
||||
const adj = ADJECTIVES[index % ADJECTIVES.length];
|
||||
const noun = NOUNS[Math.floor(index / ADJECTIVES.length) % NOUNS.length];
|
||||
const suffix = SUFFIXES[Math.floor(index / (ADJECTIVES.length * NOUNS.length)) % SUFFIXES.length];
|
||||
return suffix ? `${adj}${noun}${suffix}` : `${adj}${noun}`;
|
||||
}
|
||||
|
||||
// ============ 平台和模型 ============
|
||||
const PLATFORMS = [
|
||||
{ name: "claude-code", weight: 40 },
|
||||
{ name: "cursor", weight: 25 },
|
||||
{ name: "copilot", weight: 15 },
|
||||
{ name: "aider", weight: 10 },
|
||||
{ name: "continue", weight: 5 },
|
||||
{ name: "zed", weight: 3 },
|
||||
{ name: "windsurf", weight: 2 },
|
||||
];
|
||||
|
||||
const MODELS = [
|
||||
{ name: "claude-sonnet-4-6", weight: 35 },
|
||||
{ name: "claude-opus-4-6", weight: 15 },
|
||||
{ name: "claude-haiku-4-5", weight: 20 },
|
||||
{ name: "gpt-4o", weight: 15 },
|
||||
{ name: "gpt-4-turbo", weight: 8 },
|
||||
{ name: "gemini-2.0-flash", weight: 5 },
|
||||
{ name: "deepseek-v3", weight: 2 },
|
||||
];
|
||||
|
||||
function weightedRandom<T extends { weight: number }>(items: T[]): T {
|
||||
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
|
||||
let random = Math.random() * totalWeight;
|
||||
for (const item of items) {
|
||||
random -= item.weight;
|
||||
if (random <= 0) return item;
|
||||
}
|
||||
return items[items.length - 1];
|
||||
}
|
||||
|
||||
// ============ Token 消耗模拟 ============
|
||||
// 根据活跃程度分级
|
||||
type ActivityLevel = "heavy" | "moderate" | "light" | "minimal";
|
||||
|
||||
const ACTIVITY_DISTRIBUTION: { level: ActivityLevel; weight: number }[] = [
|
||||
{ level: "heavy", weight: 10 }, // 重度用户
|
||||
{ level: "moderate", weight: 30 }, // 中度用户
|
||||
{ level: "light", weight: 40 }, // 轻度用户
|
||||
{ level: "minimal", weight: 20 }, // 极少用户
|
||||
];
|
||||
|
||||
function getTokenRange(level: ActivityLevel): { min: number; max: number } {
|
||||
switch (level) {
|
||||
case "heavy":
|
||||
return { min: 500_000, max: 5_000_000 }; // 50万 - 500万 tokens/天
|
||||
case "moderate":
|
||||
return { min: 100_000, max: 500_000 }; // 10万 - 50万 tokens/天
|
||||
case "light":
|
||||
return { min: 20_000, max: 100_000 }; // 2万 - 10万 tokens/天
|
||||
case "minimal":
|
||||
return { min: 1_000, max: 20_000 }; // 1千 - 2万 tokens/天
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 任务摘要模板 ============
|
||||
const TASK_SUMMARIES = [
|
||||
"Refactored authentication module for better security",
|
||||
"Implemented new API endpoints for user management",
|
||||
"Fixed memory leak in background worker process",
|
||||
"Added unit tests for payment processing module",
|
||||
"Optimized database queries for faster response times",
|
||||
"Integrated third-party OAuth provider",
|
||||
"Migrated legacy code to TypeScript",
|
||||
"Built real-time notification system with WebSockets",
|
||||
"Created responsive dashboard UI components",
|
||||
"Implemented caching layer with Redis",
|
||||
"Fixed cross-browser compatibility issues",
|
||||
"Added internationalization support for 5 languages",
|
||||
"Refactored state management with Redux",
|
||||
"Implemented file upload with progress tracking",
|
||||
"Built automated deployment pipeline",
|
||||
"Created API documentation with OpenAPI spec",
|
||||
"Implemented rate limiting for API endpoints",
|
||||
"Fixed race condition in concurrent processing",
|
||||
"Added logging and monitoring infrastructure",
|
||||
"Optimized bundle size by 40%",
|
||||
"Implemented dark mode theme support",
|
||||
"Built search functionality with Elasticsearch",
|
||||
"Created admin panel for content management",
|
||||
"Fixed security vulnerability in input validation",
|
||||
"Implemented webhook system for integrations",
|
||||
"Added two-factor authentication support",
|
||||
"Built data export functionality",
|
||||
"Implemented pagination for large datasets",
|
||||
"Created custom CLI tool for development",
|
||||
"Fixed timezone handling in date operations",
|
||||
"Implemented email notification system",
|
||||
"Built real-time collaboration features",
|
||||
"Added support for multiple payment gateways",
|
||||
"Created automated testing suite",
|
||||
"Implemented feature flags system",
|
||||
"Built analytics dashboard with charts",
|
||||
"Fixed memory issues in image processing",
|
||||
"Added support for large file uploads",
|
||||
"Implemented user preference system",
|
||||
"Created backup and restore functionality",
|
||||
"Built custom form builder component",
|
||||
"Implemented audit logging system",
|
||||
"Added support for custom themes",
|
||||
"Created import/export data functionality",
|
||||
"Implemented session management",
|
||||
"Built notification preferences UI",
|
||||
"Added keyboard shortcuts support",
|
||||
"Implemented drag-and-drop functionality",
|
||||
"Created custom error handling middleware",
|
||||
];
|
||||
|
||||
const TOOLS_USED = [
|
||||
["Read", "Edit", "Bash"],
|
||||
["Grep", "Read", "Write"],
|
||||
["Bash", "Glob", "Grep"],
|
||||
["Read", "Edit", "Grep", "Bash"],
|
||||
["WebSearch", "Read", "Edit"],
|
||||
["Agent", "Read", "Grep"],
|
||||
["Bash", "Read", "Write"],
|
||||
["Glob", "Grep", "Read"],
|
||||
["Read", "Edit"],
|
||||
["Bash", "Read"],
|
||||
];
|
||||
|
||||
// ============ 工具函数 ============
|
||||
function randomInRange(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function randomFloat(min: number, max: number): number {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function randomDate(start: Date, end: Date): Date {
|
||||
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
|
||||
}
|
||||
|
||||
function formatDateString(date: Date): string {
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
// 模拟 IP 地址生成(基于国家代码的伪 IP)
|
||||
function generateFakeIp(countryCode: string, index: number): string {
|
||||
const countryPrefix: Record<string, string> = {
|
||||
US: "8.", CN: "58.", JP: "126.", GB: "2.", DE: "5.",
|
||||
FR: "46.", KR: "14.", IN: "59.", CA: "24.", AU: "1.",
|
||||
BR: "177.", NL: "77.", SE: "78.", SG: "203.", HK: "218.",
|
||||
TW: "61.", IT: "93.", ES: "88.", PL: "79.", ZA: "41.",
|
||||
};
|
||||
const prefix = countryPrefix[countryCode] || "10.";
|
||||
return `${prefix}${randomInRange(1, 255)}.${randomInRange(1, 255)}.${randomInRange(1, 255)}`;
|
||||
}
|
||||
|
||||
// ============ 主生成逻辑 ============
|
||||
interface MockClaw {
|
||||
id: string;
|
||||
apiKey: string;
|
||||
name: string;
|
||||
platform: string;
|
||||
model: string;
|
||||
ip: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
city: string;
|
||||
country: string;
|
||||
countryCode: string;
|
||||
region: string;
|
||||
createdAt: Date;
|
||||
activityLevel: ActivityLevel;
|
||||
}
|
||||
|
||||
function generateApiKey(): string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let key = "oc_";
|
||||
for (let i = 0; i < 48; i++) {
|
||||
key += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
async function generateMockData() {
|
||||
console.log("🦀 开始生成模拟数据...\n");
|
||||
|
||||
const now = new Date();
|
||||
const startDate = new Date(now.getTime() - DAYS_BACK * 24 * 60 * 60 * 1000);
|
||||
|
||||
// 1. 生成所有 claw 数据
|
||||
const mockClaws: MockClaw[] = [];
|
||||
const regionWeights = REGION_DISTRIBUTION.map(r => ({ name: r.region, weight: r.weight }));
|
||||
const regionCounts: Record<string, number> = {};
|
||||
|
||||
for (let i = 0; i < TOTAL_CLAWS; i++) {
|
||||
// 选择地区
|
||||
const region = weightedRandom(regionWeights);
|
||||
regionCounts[region.name] = (regionCounts[region.name] || 0) + 1;
|
||||
|
||||
// 从该地区选择国家
|
||||
const regionData = REGION_DISTRIBUTION.find(r => r.region === region.name)!;
|
||||
const country = regionData.countries[Math.floor(Math.random() * regionData.countries.length)];
|
||||
|
||||
// 从该国家选择城市
|
||||
const cities = CITIES_DATA[country] || [{ name: "Unknown", lat: 0, lng: 0 }];
|
||||
const city = cities[Math.floor(Math.random() * cities.length)];
|
||||
|
||||
// 添加随机偏移(模拟同一城市的不同位置)
|
||||
const latOffset = randomFloat(-0.1, 0.1);
|
||||
const lngOffset = randomFloat(-0.1, 0.1);
|
||||
|
||||
// 选择平台和模型
|
||||
const platform = weightedRandom(PLATFORMS);
|
||||
const model = weightedRandom(MODELS);
|
||||
|
||||
// 选择活跃程度
|
||||
const activity = weightedRandom(ACTIVITY_DISTRIBUTION);
|
||||
|
||||
// 随机创建时间
|
||||
const createdAt = randomDate(startDate, now);
|
||||
|
||||
const claw: MockClaw = {
|
||||
id: nanoid(21),
|
||||
apiKey: generateApiKey(),
|
||||
name: generateName(i),
|
||||
platform: platform.name,
|
||||
model: model.name,
|
||||
ip: generateFakeIp(country, i),
|
||||
latitude: city.lat + latOffset,
|
||||
longitude: city.lng + lngOffset,
|
||||
city: city.name,
|
||||
country: COUNTRY_NAMES[country] || country,
|
||||
countryCode: country,
|
||||
region: region.name,
|
||||
createdAt,
|
||||
activityLevel: activity.level,
|
||||
};
|
||||
|
||||
mockClaws.push(claw);
|
||||
}
|
||||
|
||||
console.log("📊 地区分布:");
|
||||
for (const [region, count] of Object.entries(regionCounts)) {
|
||||
console.log(` ${region}: ${count} 只虾`);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// 2. 插入 claws 到数据库
|
||||
console.log("💾 插入 claws 数据...");
|
||||
for (const claw of mockClaws) {
|
||||
await db.insert(claws).values({
|
||||
id: claw.id,
|
||||
apiKey: claw.apiKey,
|
||||
name: claw.name,
|
||||
platform: claw.platform,
|
||||
model: claw.model,
|
||||
ip: claw.ip,
|
||||
latitude: String(claw.latitude),
|
||||
longitude: String(claw.longitude),
|
||||
city: claw.city,
|
||||
country: claw.country,
|
||||
countryCode: claw.countryCode,
|
||||
region: claw.region,
|
||||
lastHeartbeat: claw.createdAt,
|
||||
totalTasks: 0,
|
||||
createdAt: claw.createdAt,
|
||||
updatedAt: claw.createdAt,
|
||||
});
|
||||
}
|
||||
console.log(` ✅ 已插入 ${mockClaws.length} 只虾\n`);
|
||||
|
||||
// 3. 生成心跳数据
|
||||
console.log("💓 生成心跳数据...");
|
||||
const heartbeatBatch: { clawId: string; ip: string; timestamp: Date }[] = [];
|
||||
|
||||
for (const claw of mockClaws) {
|
||||
// 根据活跃程度决定心跳频率
|
||||
let heartbeatCount: number;
|
||||
switch (claw.activityLevel) {
|
||||
case "heavy":
|
||||
heartbeatCount = randomInRange(50, 200); // 高频心跳
|
||||
break;
|
||||
case "moderate":
|
||||
heartbeatCount = randomInRange(20, 50);
|
||||
break;
|
||||
case "light":
|
||||
heartbeatCount = randomInRange(5, 20);
|
||||
break;
|
||||
case "minimal":
|
||||
heartbeatCount = randomInRange(1, 5);
|
||||
break;
|
||||
}
|
||||
|
||||
// 在注册后到现在之间随机分布心跳
|
||||
for (let h = 0; h < heartbeatCount; h++) {
|
||||
const heartbeatTime = randomDate(claw.createdAt, now);
|
||||
heartbeatBatch.push({
|
||||
clawId: claw.id,
|
||||
ip: claw.ip,
|
||||
timestamp: heartbeatTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 批量插入心跳
|
||||
const HEARTBEAT_BATCH_SIZE = 1000;
|
||||
for (let i = 0; i < heartbeatBatch.length; i += HEARTBEAT_BATCH_SIZE) {
|
||||
const batch = heartbeatBatch.slice(i, i + HEARTBEAT_BATCH_SIZE);
|
||||
await db.insert(heartbeats).values(batch);
|
||||
}
|
||||
console.log(` ✅ 已插入 ${heartbeatBatch.length} 条心跳记录\n`);
|
||||
|
||||
// 4. 生成任务数据
|
||||
console.log("📋 生成任务数据...");
|
||||
const taskBatch: {
|
||||
clawId: string;
|
||||
summary: string;
|
||||
durationMs: number;
|
||||
model: string;
|
||||
toolsUsed: string[];
|
||||
timestamp: Date;
|
||||
}[] = [];
|
||||
|
||||
for (const claw of mockClaws) {
|
||||
// 根据活跃程度决定任务数量
|
||||
let taskCount: number;
|
||||
switch (claw.activityLevel) {
|
||||
case "heavy":
|
||||
taskCount = randomInRange(20, 100);
|
||||
break;
|
||||
case "moderate":
|
||||
taskCount = randomInRange(10, 30);
|
||||
break;
|
||||
case "light":
|
||||
taskCount = randomInRange(3, 15);
|
||||
break;
|
||||
case "minimal":
|
||||
taskCount = randomInRange(0, 5);
|
||||
break;
|
||||
}
|
||||
|
||||
for (let t = 0; t < taskCount; t++) {
|
||||
const summary = TASK_SUMMARIES[Math.floor(Math.random() * TASK_SUMMARIES.length)];
|
||||
const durationMs = randomInRange(10_000, 3_600_000); // 10秒 - 1小时
|
||||
const toolsUsed = TOOLS_USED[Math.floor(Math.random() * TOOLS_USED.length)];
|
||||
const taskTime = randomDate(claw.createdAt, now);
|
||||
|
||||
taskBatch.push({
|
||||
clawId: claw.id,
|
||||
summary,
|
||||
durationMs,
|
||||
model: claw.model,
|
||||
toolsUsed,
|
||||
timestamp: taskTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 批量插入任务
|
||||
const TASK_BATCH_SIZE = 500;
|
||||
for (let i = 0; i < taskBatch.length; i += TASK_BATCH_SIZE) {
|
||||
const batch = taskBatch.slice(i, i + TASK_BATCH_SIZE);
|
||||
await db.insert(tasks).values(batch);
|
||||
}
|
||||
console.log(` ✅ 已插入 ${taskBatch.length} 条任务记录\n`);
|
||||
|
||||
// 5. 更新 claws 的 totalTasks
|
||||
console.log("🔄 更新任务统计...");
|
||||
for (const claw of mockClaws) {
|
||||
const clawTasks = taskBatch.filter(t => t.clawId === claw.id);
|
||||
if (clawTasks.length > 0) {
|
||||
await db
|
||||
.update(claws)
|
||||
.set({ totalTasks: clawTasks.length })
|
||||
.where(sql`id = ${claw.id}`);
|
||||
}
|
||||
}
|
||||
console.log(" ✅ 已更新任务统计\n");
|
||||
|
||||
// 6. 生成 token 使用数据(按天)
|
||||
console.log("🔢 生成 Token 使用数据...");
|
||||
|
||||
// 生成过去 7 天的日期
|
||||
const dates: string[] = [];
|
||||
for (let d = 0; d < DAYS_BACK; d++) {
|
||||
const date = new Date(now.getTime() - d * 24 * 60 * 60 * 1000);
|
||||
dates.push(formatDateString(date));
|
||||
}
|
||||
|
||||
const tokenBatch: {
|
||||
clawId: string;
|
||||
date: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
}[] = [];
|
||||
|
||||
for (const claw of mockClaws) {
|
||||
// 确定这只虾的活跃天数
|
||||
const activeDays = Math.ceil(
|
||||
(now.getTime() - claw.createdAt.getTime()) / (24 * 60 * 60 * 1000)
|
||||
);
|
||||
|
||||
// 只为注册后的日期生成 token 数据
|
||||
for (let d = 0; d < Math.min(activeDays, DAYS_BACK); d++) {
|
||||
// 根据活跃程度决定是否在这一天有活动
|
||||
const isActiveToday = Math.random() < (
|
||||
claw.activityLevel === "heavy" ? 0.95 :
|
||||
claw.activityLevel === "moderate" ? 0.7 :
|
||||
claw.activityLevel === "light" ? 0.4 :
|
||||
0.15
|
||||
);
|
||||
|
||||
if (!isActiveToday) continue;
|
||||
|
||||
const tokenRange = getTokenRange(claw.activityLevel);
|
||||
// 实际 token 会有波动
|
||||
const variance = randomFloat(0.5, 1.5);
|
||||
const dailyTokens = randomInRange(
|
||||
Math.floor(tokenRange.min * variance),
|
||||
Math.floor(tokenRange.max * variance)
|
||||
);
|
||||
|
||||
// input/output 比例约为 3:1
|
||||
const inputTokens = Math.floor(dailyTokens * 0.75);
|
||||
const outputTokens = dailyTokens - inputTokens;
|
||||
|
||||
tokenBatch.push({
|
||||
clawId: claw.id,
|
||||
date: dates[d],
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 批量插入 token 数据
|
||||
const TOKEN_BATCH_SIZE = 500;
|
||||
for (let i = 0; i < tokenBatch.length; i += TOKEN_BATCH_SIZE) {
|
||||
const batch = tokenBatch.slice(i, i + TOKEN_BATCH_SIZE);
|
||||
await db.insert(tokenUsage).values(batch);
|
||||
}
|
||||
console.log(` ✅ 已插入 ${tokenBatch.length} 条 Token 使用记录\n`);
|
||||
|
||||
// 7. 输出统计摘要
|
||||
console.log("=" .repeat(50));
|
||||
console.log("📊 数据生成完成!\n");
|
||||
|
||||
console.log("📈 统计摘要:");
|
||||
console.log(` 总虾数: ${mockClaws.length}`);
|
||||
console.log(` 心跳记录: ${heartbeatBatch.length}`);
|
||||
console.log(` 任务记录: ${taskBatch.length}`);
|
||||
console.log(` Token 记录: ${tokenBatch.length}`);
|
||||
|
||||
// 计算总 token
|
||||
const totalInputTokens = tokenBatch.reduce((sum, t) => sum + t.inputTokens, 0);
|
||||
const totalOutputTokens = tokenBatch.reduce((sum, t) => sum + t.outputTokens, 0);
|
||||
console.log(` 总 Input Tokens: ${(totalInputTokens / 1_000_000).toFixed(2)}M`);
|
||||
console.log(` 总 Output Tokens: ${(totalOutputTokens / 1_000_000).toFixed(2)}M`);
|
||||
console.log(` 总 Tokens: ${((totalInputTokens + totalOutputTokens) / 1_000_000).toFixed(2)}M`);
|
||||
|
||||
console.log("\n🗺️ 地区分布:");
|
||||
for (const [region, count] of Object.entries(regionCounts).sort((a, b) => b[1] - a[1])) {
|
||||
const percentage = ((count / TOTAL_CLAWS) * 100).toFixed(1);
|
||||
console.log(` ${region}: ${count} (${percentage}%)`);
|
||||
}
|
||||
|
||||
console.log("\n🎯 活跃程度分布:");
|
||||
const activityCounts: Record<ActivityLevel, number> = {
|
||||
heavy: 0,
|
||||
moderate: 0,
|
||||
light: 0,
|
||||
minimal: 0,
|
||||
};
|
||||
mockClaws.forEach(c => activityCounts[c.activityLevel]++);
|
||||
console.log(` 重度用户: ${activityCounts.heavy}`);
|
||||
console.log(` 中度用户: ${activityCounts.moderate}`);
|
||||
console.log(` 轻度用户: ${activityCounts.light}`);
|
||||
console.log(` 极少用户: ${activityCounts.minimal}`);
|
||||
|
||||
console.log("\n✨ 完成!");
|
||||
}
|
||||
|
||||
// 运行
|
||||
generateMockData()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => {
|
||||
console.error("❌ 错误:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
40
scripts/seed-mock-data.sh
Executable file
40
scripts/seed-mock-data.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 一键生成模拟数据脚本
|
||||
# 使用方法: bash scripts/seed-mock-data.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "🦀 OpenClaw Market 模拟数据生成器"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# 检查环境变量
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "❌ 错误: DATABASE_URL 环境变量未设置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$REDIS_URL" ]; then
|
||||
echo "⚠️ 警告: REDIS_URL 环境变量未设置,Redis 同步可能会失败"
|
||||
fi
|
||||
|
||||
# 步骤 1: 清理旧数据
|
||||
echo "📦 步骤 1/3: 清理旧数据..."
|
||||
npx tsx scripts/clear-mock-data.ts
|
||||
echo ""
|
||||
|
||||
# 步骤 2: 生成新数据
|
||||
echo "📦 步骤 2/3: 生成模拟数据..."
|
||||
npx tsx scripts/generate-mock-data.ts
|
||||
echo ""
|
||||
|
||||
# 步骤 3: 同步 Redis
|
||||
echo "📦 步骤 3/3: 同步 Redis 统计..."
|
||||
npx tsx scripts/sync-redis-stats.ts
|
||||
echo ""
|
||||
|
||||
echo "=================================="
|
||||
echo "✨ 全部完成!现在可以启动开发服务器查看效果:"
|
||||
echo " pnpm dev"
|
||||
echo ""
|
||||
236
scripts/simulate-live-activity.ts
Normal file
236
scripts/simulate-live-activity.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 持续模拟心跳脚本
|
||||
* 随机选择在线的虾发送心跳和任务,模拟实时活动
|
||||
*
|
||||
* 使用方法: npx tsx scripts/simulate-live-activity.ts
|
||||
*
|
||||
* 按 Ctrl+C 停止
|
||||
*/
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { claws, heartbeats, tasks, tokenUsage } from "@/lib/db/schema";
|
||||
import { sql, eq } from "drizzle-orm";
|
||||
import { redis } from "@/lib/redis";
|
||||
import { publishEvent } from "@/lib/redis";
|
||||
|
||||
const ACTIVE_CLAWS_KEY = "active:claws";
|
||||
const STATS_GLOBAL_KEY = "stats:global";
|
||||
const STATS_REGION_KEY = "stats:region";
|
||||
const HOURLY_ACTIVITY_KEY = "stats:hourly";
|
||||
|
||||
// 任务摘要模板
|
||||
const TASK_SUMMARIES = [
|
||||
"Refactored authentication module",
|
||||
"Implemented new API endpoint",
|
||||
"Fixed memory leak in worker",
|
||||
"Added unit tests",
|
||||
"Optimized database queries",
|
||||
"Integrated OAuth provider",
|
||||
"Migrated code to TypeScript",
|
||||
"Built real-time notification",
|
||||
"Created dashboard components",
|
||||
"Implemented caching layer",
|
||||
"Fixed browser compatibility",
|
||||
"Added i18n support",
|
||||
"Refactored state management",
|
||||
"Implemented file upload",
|
||||
"Built deployment pipeline",
|
||||
"Created API documentation",
|
||||
"Implemented rate limiting",
|
||||
"Fixed race condition",
|
||||
"Added logging infrastructure",
|
||||
"Optimized bundle size",
|
||||
];
|
||||
|
||||
const TOOLS_USED = [
|
||||
["Read", "Edit", "Bash"],
|
||||
["Grep", "Read", "Write"],
|
||||
["Bash", "Glob", "Grep"],
|
||||
["WebSearch", "Read", "Edit"],
|
||||
["Agent", "Read", "Grep"],
|
||||
];
|
||||
|
||||
// 全局变量存储所有 claws
|
||||
let allClaws: { id: string; name: string; model: string; city: string | null; country: string | null }[] = [];
|
||||
|
||||
async function loadClaws() {
|
||||
const result = await db
|
||||
.select({
|
||||
id: claws.id,
|
||||
name: claws.name,
|
||||
model: claws.model,
|
||||
city: claws.city,
|
||||
country: claws.country,
|
||||
})
|
||||
.from(claws);
|
||||
|
||||
allClaws = result;
|
||||
console.log(`📋 已加载 ${allClaws.length} 只虾\n`);
|
||||
}
|
||||
|
||||
function randomChoice<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
async function sendHeartbeat(claw: typeof allClaws[0]) {
|
||||
const now = new Date();
|
||||
|
||||
// 更新数据库
|
||||
await db
|
||||
.update(claws)
|
||||
.set({ lastHeartbeat: now, updatedAt: now })
|
||||
.where(eq(claws.id, claw.id));
|
||||
|
||||
// 插入心跳记录
|
||||
await db.insert(heartbeats).values({
|
||||
clawId: claw.id,
|
||||
ip: "127.0.0.1",
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
// 更新 Redis
|
||||
await redis.set(`claw:online:${claw.id}`, "simulated", "EX", 300);
|
||||
await redis.zadd(ACTIVE_CLAWS_KEY, Date.now(), claw.id);
|
||||
|
||||
// 发布事件
|
||||
await publishEvent({
|
||||
type: "heartbeat",
|
||||
clawId: claw.id,
|
||||
clawName: claw.name,
|
||||
city: claw.city,
|
||||
country: claw.country,
|
||||
});
|
||||
|
||||
console.log(`💓 [${now.toLocaleTimeString()}] ${claw.name} 发送心跳`);
|
||||
}
|
||||
|
||||
async function sendTask(claw: typeof allClaws[0]) {
|
||||
const now = new Date();
|
||||
const summary = randomChoice(TASK_SUMMARIES);
|
||||
const durationMs = randomInt(10_000, 1_800_000); // 10秒 - 30分钟
|
||||
const toolsUsed = randomChoice(TOOLS_USED);
|
||||
|
||||
// 插入任务
|
||||
await db.insert(tasks).values({
|
||||
clawId: claw.id,
|
||||
summary,
|
||||
durationMs,
|
||||
model: claw.model,
|
||||
toolsUsed,
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
// 更新任务计数
|
||||
await db
|
||||
.update(claws)
|
||||
.set({
|
||||
totalTasks: sql`${claws.totalTasks} + 1`,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(claws.id, claw.id));
|
||||
|
||||
// 更新 Redis 统计
|
||||
await redis.hincrby(STATS_GLOBAL_KEY, "total_tasks", 1);
|
||||
await redis.hincrby(STATS_GLOBAL_KEY, "tasks_today", 1);
|
||||
|
||||
// 更新每小时活动
|
||||
const hourKey = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-${String(now.getUTCDate()).padStart(2, "0")}T${String(now.getUTCHours()).padStart(2, "0")}`;
|
||||
await redis.hincrby(HOURLY_ACTIVITY_KEY, hourKey, 1);
|
||||
|
||||
// 发布事件
|
||||
await publishEvent({
|
||||
type: "task",
|
||||
clawId: claw.id,
|
||||
clawName: claw.name,
|
||||
city: claw.city,
|
||||
country: claw.country,
|
||||
summary,
|
||||
durationMs,
|
||||
});
|
||||
|
||||
console.log(`📋 [${now.toLocaleTimeString()}] ${claw.name} 完成任务: ${summary}`);
|
||||
}
|
||||
|
||||
async function reportTokens(claw: typeof allClaws[0]) {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().split("T")[0];
|
||||
|
||||
// 随机 token 数量
|
||||
const inputTokens = randomInt(1_000, 100_000);
|
||||
const outputTokens = randomInt(500, 50_000);
|
||||
|
||||
// Upsert token 使用
|
||||
await db.execute(sql`
|
||||
INSERT INTO token_usage (claw_id, date, input_tokens, output_tokens)
|
||||
VALUES (${claw.id}, ${date}, ${inputTokens}, ${outputTokens})
|
||||
ON DUPLICATE KEY UPDATE
|
||||
input_tokens = input_tokens + ${inputTokens},
|
||||
output_tokens = output_tokens + ${outputTokens},
|
||||
updated_at = NOW()
|
||||
`);
|
||||
|
||||
// 更新 Redis 总计
|
||||
await redis.hincrby(STATS_GLOBAL_KEY, "total_input_tokens", inputTokens);
|
||||
await redis.hincrby(STATS_GLOBAL_KEY, "total_output_tokens", outputTokens);
|
||||
|
||||
console.log(`🔢 [${now.toLocaleTimeString()}] ${claw.name} 报告 Token: +${inputTokens} input, +${outputTokens} output`);
|
||||
}
|
||||
|
||||
async function runSimulation() {
|
||||
console.log("🚀 开始模拟实时活动...\n");
|
||||
console.log("按 Ctrl+C 停止\n");
|
||||
console.log("=".repeat(50) + "\n");
|
||||
|
||||
await loadClaws();
|
||||
|
||||
if (allClaws.length === 0) {
|
||||
console.log("❌ 没有找到任何虾,请先运行数据生成脚本");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 主循环
|
||||
let iteration = 0;
|
||||
while (true) {
|
||||
iteration++;
|
||||
console.log(`\n--- 第 ${iteration} 轮 ---`);
|
||||
|
||||
// 随机选择 1-5 只虾发送心跳
|
||||
const heartbeatCount = randomInt(1, 5);
|
||||
for (let i = 0; i < heartbeatCount; i++) {
|
||||
const claw = randomChoice(allClaws);
|
||||
await sendHeartbeat(claw);
|
||||
}
|
||||
|
||||
// 30% 概率有虾完成任务
|
||||
if (Math.random() < 0.3) {
|
||||
const claw = randomChoice(allClaws);
|
||||
await sendTask(claw);
|
||||
}
|
||||
|
||||
// 20% 概率有虾报告 token
|
||||
if (Math.random() < 0.2) {
|
||||
const claw = randomChoice(allClaws);
|
||||
await reportTokens(claw);
|
||||
}
|
||||
|
||||
// 等待 2-5 秒
|
||||
const delay = randomInt(2000, 5000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
// 处理退出
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\n\n👋 停止模拟...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 运行
|
||||
runSimulation().catch((err) => {
|
||||
console.error("❌ 错误:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
163
scripts/sync-redis-stats.ts
Normal file
163
scripts/sync-redis-stats.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Redis 统计数据同步脚本
|
||||
* 根据数据库中的数据初始化 Redis 统计
|
||||
*
|
||||
* 使用方法: npx tsx scripts/sync-redis-stats.ts
|
||||
*/
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { claws, heartbeats, tasks, tokenUsage } from "@/lib/db/schema";
|
||||
import { sql, eq, and, gte } from "drizzle-orm";
|
||||
import { redis } from "@/lib/redis";
|
||||
|
||||
const ACTIVE_CLAWS_KEY = "active:claws";
|
||||
const STATS_GLOBAL_KEY = "stats:global";
|
||||
const STATS_REGION_KEY = "stats:region";
|
||||
const HOURLY_ACTIVITY_KEY = "stats:hourly";
|
||||
|
||||
async function syncRedisStats() {
|
||||
console.log("🔄 开始同步 Redis 统计数据...\n");
|
||||
|
||||
const now = new Date();
|
||||
|
||||
try {
|
||||
// 1. 同步全局统计
|
||||
console.log("📊 同步全局统计...");
|
||||
|
||||
// 总虾数
|
||||
const totalClawsResult = await db
|
||||
.select({ count: sql<number>`COUNT(*)` })
|
||||
.from(claws);
|
||||
const totalClaws = totalClawsResult[0].count;
|
||||
|
||||
// 总任务数
|
||||
const totalTasksResult = await db
|
||||
.select({ count: sql<number>`COUNT(*)` })
|
||||
.from(tasks);
|
||||
const totalTasks = totalTasksResult[0].count;
|
||||
|
||||
// 今日任务数
|
||||
const todayStart = new Date(now);
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
const todayTasksResult = await db
|
||||
.select({ count: sql<number>`COUNT(*)` })
|
||||
.from(tasks)
|
||||
.where(gte(tasks.timestamp, todayStart));
|
||||
const todayTasks = todayTasksResult[0].count;
|
||||
|
||||
// 总 token
|
||||
const totalTokensResult = await db
|
||||
.select({
|
||||
input: sql<number>`SUM(input_tokens)`,
|
||||
output: sql<number>`SUM(output_tokens)`,
|
||||
})
|
||||
.from(tokenUsage);
|
||||
const totalInput = totalTokensResult[0].input || 0;
|
||||
const totalOutput = totalTokensResult[0].output || 0;
|
||||
|
||||
await redis.hset(STATS_GLOBAL_KEY, {
|
||||
total_claws: totalClaws,
|
||||
total_tasks: totalTasks,
|
||||
tasks_today: todayTasks,
|
||||
total_input_tokens: totalInput,
|
||||
total_output_tokens: totalOutput,
|
||||
});
|
||||
|
||||
console.log(` ✅ 总虾数: ${totalClaws}`);
|
||||
console.log(` ✅ 总任务数: ${totalTasks}`);
|
||||
console.log(` ✅ 今日任务: ${todayTasks}`);
|
||||
console.log(` ✅ 总 Input Tokens: ${(totalInput / 1_000_000).toFixed(2)}M`);
|
||||
console.log(` ✅ 总 Output Tokens: ${(totalOutput / 1_000_000).toFixed(2)}M\n`);
|
||||
|
||||
// 2. 同步地区统计
|
||||
console.log("🗺️ 同步地区统计...");
|
||||
const regionStats = await db
|
||||
.select({
|
||||
region: claws.region,
|
||||
count: sql<number>`COUNT(*)`,
|
||||
})
|
||||
.from(claws)
|
||||
.where(sql`region IS NOT NULL`)
|
||||
.groupBy(claws.region);
|
||||
|
||||
const regionData: Record<string, string> = {};
|
||||
for (const stat of regionStats) {
|
||||
if (stat.region) {
|
||||
regionData[stat.region] = stat.count;
|
||||
console.log(` ${stat.region}: ${stat.count}`);
|
||||
}
|
||||
}
|
||||
await redis.hset(STATS_REGION_KEY, regionData);
|
||||
console.log("");
|
||||
|
||||
// 3. 同步活跃 claws(最近 5 分钟有心跳的)
|
||||
console.log("💓 同步活跃 claws...");
|
||||
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
|
||||
|
||||
// 随机选择一些 claws 作为"活跃"状态
|
||||
const activeClaws = await db
|
||||
.select({ id: claws.id })
|
||||
.from(claws)
|
||||
.orderBy(sql`RAND()`)
|
||||
.limit(Math.floor(totalClaws * 0.3)); // 30% 的虾在线
|
||||
|
||||
const timestamp = Date.now();
|
||||
for (const claw of activeClaws) {
|
||||
await redis.zadd(ACTIVE_CLAWS_KEY, timestamp, claw.id);
|
||||
// 设置在线状态
|
||||
await redis.set(`claw:online:${claw.id}`, "mock", "EX", 300);
|
||||
}
|
||||
console.log(` ✅ 设置 ${activeClaws.length} 只虾为活跃状态\n`);
|
||||
|
||||
// 4. 同步每小时活动统计
|
||||
console.log("📈 同步每小时活动统计...");
|
||||
const hourlyData: Record<string, number> = {};
|
||||
|
||||
// 生成过去 24 小时的活动数据
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const hourDate = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||||
const hourKey = `${hourDate.getUTCFullYear()}-${String(hourDate.getUTCMonth() + 1).padStart(2, "0")}-${String(hourDate.getUTCDate()).padStart(2, "0")}T${String(hourDate.getUTCHours()).padStart(2, "0")}`;
|
||||
|
||||
// 查询该小时的心跳数
|
||||
const hourStart = new Date(hourDate);
|
||||
hourStart.setMinutes(0, 0, 0);
|
||||
const hourEnd = new Date(hourStart.getTime() + 60 * 60 * 1000);
|
||||
|
||||
const heartbeatsCount = await db
|
||||
.select({ count: sql<number>`COUNT(*)` })
|
||||
.from(heartbeats)
|
||||
.where(
|
||||
and(
|
||||
gte(heartbeats.timestamp, hourStart),
|
||||
gte(hourEnd, heartbeats.timestamp)
|
||||
)
|
||||
);
|
||||
|
||||
hourlyData[hourKey] = heartbeatsCount[0].count || Math.floor(Math.random() * 50);
|
||||
}
|
||||
|
||||
await redis.hset(
|
||||
HOURLY_ACTIVITY_KEY,
|
||||
Object.fromEntries(
|
||||
Object.entries(hourlyData).map(([k, v]) => [k, String(v)])
|
||||
)
|
||||
);
|
||||
await redis.expire(HOURLY_ACTIVITY_KEY, 48 * 60 * 60);
|
||||
console.log(` ✅ 已同步 24 小时活动数据\n`);
|
||||
|
||||
console.log("=" .repeat(50));
|
||||
console.log("✨ Redis 数据同步完成!\n");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ 同步失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行
|
||||
syncRedisStats()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => {
|
||||
console.error("❌ 错误:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user