237 lines
6.2 KiB
TypeScript
237 lines
6.2 KiB
TypeScript
/**
|
|
* 持续模拟心跳脚本
|
|
* 随机选择在线的虾发送心跳和任务,模拟实时活动
|
|
*
|
|
* 使用方法: 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);
|
|
});
|