feat: 生成脚本
This commit is contained in:
@@ -13,7 +13,12 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"seed:mock": "bash scripts/seed-mock-data.sh",
|
||||||
|
"seed:clear": "tsx scripts/clear-mock-data.ts",
|
||||||
|
"seed:generate": "tsx scripts/generate-mock-data.ts",
|
||||||
|
"seed:sync": "tsx scripts/sync-redis-stats.ts",
|
||||||
|
"seed:live": "tsx scripts/simulate-live-activity.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -52,6 +57,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"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -111,6 +111,9 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.21.0
|
||||||
|
version: 4.21.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.0
|
specifier: ^5.8.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -129,7 +132,7 @@ importers:
|
|||||||
version: 22.19.15
|
version: 22.19.15
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.3.0
|
specifier: ^8.3.0
|
||||||
version: 8.5.1(@swc/core@1.15.18)(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)
|
version: 8.5.1(@swc/core@1.15.18)(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.0
|
specifier: ^5.8.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -3756,6 +3759,11 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -6716,12 +6724,13 @@ snapshots:
|
|||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8):
|
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
postcss: 8.5.8
|
postcss: 8.5.8
|
||||||
|
tsx: 4.21.0
|
||||||
|
|
||||||
postcss@8.4.31:
|
postcss@8.4.31:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7282,7 +7291,7 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tsup@8.5.1(@swc/core@1.15.18)(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3):
|
tsup@8.5.1(@swc/core@1.15.18)(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
bundle-require: 5.1.0(esbuild@0.27.4)
|
bundle-require: 5.1.0(esbuild@0.27.4)
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
@@ -7293,7 +7302,7 @@ snapshots:
|
|||||||
fix-dts-default-cjs-exports: 1.0.1
|
fix-dts-default-cjs-exports: 1.0.1
|
||||||
joycon: 3.1.1
|
joycon: 3.1.1
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)
|
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
rollup: 4.59.0
|
rollup: 4.59.0
|
||||||
source-map: 0.7.6
|
source-map: 0.7.6
|
||||||
@@ -7311,6 +7320,13 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.27.4
|
||||||
|
get-tsconfig: 4.13.6
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|||||||
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