feat: and cf deploy button

This commit is contained in:
zaunist committed 2025-06-25 20:23:40 +08:00
1 parent 4e354cd277
commit 0877ad013f
14 files changed
+1602 -22

No files matched your search

+3
View File
@@ -4,6 +4,9 @@
.pnp.js
node_modules/
.wrangler
xugou.wiki
# --- Project Specific: Agent ---
# Build artifacts for the agent project
/agent/xugou-agent
+137
View File
@@ -0,0 +1,137 @@
# XUGOU 项目开发规则
## 项目概述
XUGOU 是一个基于 CloudFlare 的轻量化系统监控平台,采用现代化的全栈架构:
- **Frontend**: React + TypeScript + Vite + TailwindCSS + Radix UI
- **Backend**: Hono + TypeScript + CloudFlare Workers + D1 Database
- **Agent**: Go + Cobra CLI + gopsutil
- **部署**: CloudFlare Workers + D1 + Pages
## 代码规范
### 通用规范
1. **代码是写给人看的,只是机器恰好可以运行而已!**
2. **每次修改代码以后,需要确保代码能够编译通过!**
3. 使用 TypeScript 进行类型安全开发
4. 遵循 ESLint 和 Prettier 配置
5. 使用有意义的变量和函数命名
6. 添加必要的注释,特别是复杂业务逻辑
7. 保持代码简洁,避免过度工程化
### Frontend 规范
#### 文件组织
```
src/
├── api/ # API 调用封装
├── components/ # 可复用组件
│ └── ui/ # 基础 UI 组件 (shadcn/ui)
├── config/ # 配置文件
├── hooks/ # 自定义 React Hooks
├── i18n/ # 国际化配置
├── lib/ # 工具库
├── pages/ # 页面组件
├── providers/ # Context Providers
├── router/ # 路由配置
├── styles/ # 样式文件
├── types/ # TypeScript 类型定义
└── utils/ # 工具函数
```
#### 组件规范
1. 使用函数式组件和 React Hooks
2. 组件文件使用 PascalCase 命名 (如 `MonitorCard.tsx`)
3. 导出组件使用 default export
4. Props 接口命名为 `ComponentNameProps`
5. 使用 Radix UI 作为基础组件库
6. 样式使用 TailwindCSS,避免内联样式
7. 状态管理优先使用 React Context,复杂状态考虑 Zustand
#### 示例组件结构
```typescript
interface MonitorCardProps {
monitor: Monitor;
onEdit?: (monitor: Monitor) => void;
}
export default function MonitorCard({ monitor, onEdit }: MonitorCardProps) {
// 组件逻辑
return (
<Card className="p-4">
{/* 组件内容 */}
</Card>
);
}
```
### Backend 规范
#### 文件组织
```
src/
├── api/ # API 路由处理
├── config/ # 配置文件
├── db/ # 数据库相关
│ └── schema.ts # Drizzle ORM 模式定义
├── jobs/ # 定时任务
├── middlewares/ # 中间件
├── models/ # 数据模型和类型
├── repositories/ # 数据访问层
├── services/ # 业务逻辑层
├── static/ # 静态资源
└── utils/ # 工具函数
```
#### API 规范
1. 使用 Hono 框架构建 RESTful API
2. 路由文件按功能模块组织 (如 `auth.ts`, `monitors.ts`)
3. 统一的错误处理和响应格式
4. 使用中间件处理 CORS、JWT 认证等
5. API 路径统一使用 `/api/` 前缀
#### 数据库规范
1. 使用 Drizzle ORM 进行数据库操作
2. 所有表结构定义在 `schema.ts` 中
3. 使用 SQLite (CloudFlare D1) 作为数据库
4. 表名使用复数形式,字段使用 snake_case
5. 必须包含 `created_at` 和 `updated_at` 字段
6. 使用 Repository 模式封装数据访问
#### 示例 API 结构
```typescript
const monitors = new Hono<{ Bindings: Bindings }>();
monitors.get('/', async (c) => {
try {
const result = await MonitorService.getAllMonitors(c.env);
return c.json({ success: true, data: result });
} catch (error) {
return c.json({ success: false, message: error.message }, 500);
}
});
```
### Agent 规范
#### 文件组织
```
cmd/
└── agent/ # CLI 命令定义
pkg/
├── collector/ # 数据收集器
├── config/ # 配置管理
├── model/ # 数据模型
├── reporter/ # 数据上报
└── utils/ # 工具函数
```
#### Go 代码规范
1. 遵循 Go 官方代码规范
2. 使用 Cobra 构建 CLI 应用
3. 使用 Viper 进行配置管理
4. 错误处理要完整,不忽略任何错误
5. 包名使用小写,避免下划线
6. 导出的函数和类型使用 PascalCase
7. 私有函数和变量使用 camelCase
+3
View File
@@ -0,0 +1,3 @@
CREATE INDEX `agent_metrics_24h_agent_timestamp_idx` ON `agent_metrics_24h` (`agent_id`,`timestamp`);--> statement-breakpoint
CREATE INDEX `monitor_daily_stats_monitor_id_date_idx` ON `monitor_daily_stats` (`monitor_id`,`date`);--> statement-breakpoint
CREATE INDEX `monitor_status_history_24h_monitor_timestamp_idx` ON `monitor_status_history_24h` (`monitor_id`,`timestamp`);
File diff suppressed because it is too large. Load diff
+7
View File
@@ -15,6 +15,13 @@
"when": 1749736889133,
"tag": "0001_fluffy_pestilence",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1750852918387,
"tag": "0002_public_domino",
"breakpoints": true
}
]
}
+24
View File
@@ -198,5 +198,29 @@ CREATE TABLE IF NOT EXISTS \`users\` (
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS \`users_username_unique\` ON \`users\` (\`username\`);`
},
{
name: "0001_fluffy_pestilence.sql",
sql: `ALTER TABLE \`agents\` DROP COLUMN \`cpu_usage\`;
--> statement-breakpoint
ALTER TABLE \`agents\` DROP COLUMN \`memory_total\`;
--> statement-breakpoint
ALTER TABLE \`agents\` DROP COLUMN \`memory_used\`;
--> statement-breakpoint
ALTER TABLE \`agents\` DROP COLUMN \`disk_total\`;
--> statement-breakpoint
ALTER TABLE \`agents\` DROP COLUMN \`disk_used\`;
--> statement-breakpoint
ALTER TABLE \`agents\` DROP COLUMN \`network_rx\`;
--> statement-breakpoint
ALTER TABLE \`agents\` DROP COLUMN \`network_tx\`;`
},
{
name: "0002_public_domino.sql",
sql: `CREATE INDEX IF NOT EXISTS \`agent_metrics_24h_agent_timestamp_idx\` ON \`agent_metrics_24h\` (\`agent_id\`,\`timestamp\`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS \`monitor_daily_stats_monitor_id_date_idx\` ON \`monitor_daily_stats\` (\`monitor_id\`,\`date\`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS \`monitor_status_history_24h_monitor_timestamp_idx\` ON \`monitor_status_history_24h\` (\`monitor_id\`,\`timestamp\`);`
}
];
+12 -4
View File
@@ -1,4 +1,4 @@
import { int, sqliteTable, text, real, primaryKey } from "drizzle-orm/sqlite-core";
import { int, sqliteTable, text, real, primaryKey, index } from "drizzle-orm/sqlite-core";
// 用户表
export const users = sqliteTable("users", {
@@ -40,7 +40,10 @@ export const monitorStatusHistory24h = sqliteTable("monitor_status_history_24h",
response_time: int("response_time"),
status_code: int("status_code"),
error: text("error")
});
}, (table) => ({
// monitor_id 和 timestamp 的联合索引,用于优化按监控项和时间查询的性能
monitorTimestampIdx: index("monitor_status_history_24h_monitor_timestamp_idx").on(table.monitor_id, table.timestamp)
}));
// 监控每日统计表
export const monitorDailyStats = sqliteTable("monitor_daily_stats", {
@@ -55,7 +58,9 @@ export const monitorDailyStats = sqliteTable("monitor_daily_stats", {
max_response_time: int("max_response_time").default(0),
availability: real("availability").default(0),
created_at: text("created_at").notNull()
});
}, (table) => ({
monitorDateIdx: index("monitor_daily_stats_monitor_id_date_idx").on(table.monitor_id, table.date)
}));
// 客户端表
export const agents = sqliteTable("agents", {
@@ -90,7 +95,10 @@ export const agentMetrics24h = sqliteTable("agent_metrics_24h", {
load_15: real("load_15"),
disk_metrics: text("disk_metrics"),
network_metrics: text("network_metrics")
});
}, (table) => ({
// agent_id 和 timestamp 的联合索引,用于优化按代理和时间查询的性能
agentTimestampIdx: index("agent_metrics_24h_agent_timestamp_idx").on(table.agent_id, table.timestamp)
}));
// 状态页配置表
export const statusPageConfig = sqliteTable("status_page_config", {
+41
View File
@@ -29,6 +29,47 @@ app.route("/api/users", api.users);
app.route("/api/status", api.status);
app.route("/api/notifications", api.notifications);
app.route("/api/dashboard", api.dashboard);
// 静态文件路由 - 处理所有非 API 请求,返回前端应用
app.get("*", async (c) => {
const url = new URL(c.req.url);
// 如果是 API 路由,跳过静态文件处理
if (url.pathname.startsWith("/api/")) {
return c.notFound();
}
try {
// 尝试获取请求的静态文件
const asset = await c.env.ASSETS.fetch(c.req.url);
// 如果文件存在,直接返回
if (asset.status === 200) {
return asset;
}
// 如果文件不存在,返回 index.html 以支持 React Router
const indexUrl = new URL("/index.html", c.req.url);
const indexAsset = await c.env.ASSETS.fetch(indexUrl.toString());
if (indexAsset.status === 200) {
// 设置正确的 Content-Type
const response = new Response(indexAsset.body, {
status: 200,
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache"
}
});
return response;
}
return c.notFound();
} catch (error) {
console.error("静态文件服务错误:", error);
return c.text("Internal Server Error", 500);
}
});
// 数据库状态标志,用于记录数据库初始化状态
let dbInitialized = false;
+21 -15
View File
@@ -4,7 +4,11 @@ import { Monitor } from "../models/monitor";
import { getMonitorsToCheck, checkMonitor } from "../services";
import { shouldSendNotification, sendNotification } from "../services";
import { db } from "../config";
import { monitorDailyStats, monitorStatusHistory24h,monitors } from "../db/schema";
import {
monitorDailyStats,
monitorStatusHistory24h,
monitors,
} from "../db/schema";
import { and, gte, lte } from "drizzle-orm";
const monitorTask = new Hono<{ Bindings: Bindings }>();
@@ -25,7 +29,7 @@ async function checkMonitors(c: any) {
// 检查每个监控
const results = await Promise.all(
monitors.map(async (monitor: Monitor) => {
monitors.map(async (monitor: Monitor) => {
console.log(`开始检查监控: ${monitor.name} (ID: ${monitor.id})`);
const checkResult = await checkMonitor(monitor);
// 处理通知
@@ -166,12 +170,12 @@ async function generateDailyStats(c: any) {
// 一次性获取所有监控
const monitorsResult = await db.select().from(monitors);
if (!monitorsResult.results || monitorsResult.results.length === 0) {
if (!monitorsResult || monitorsResult.length === 0) {
console.log("没有找到监控");
return { success: true, message: "没有监控", processed: 0 };
}
const allMonitors = monitorsResult.results as Monitor[];
const allMonitors = monitorsResult as Monitor[];
console.log(`找到 ${allMonitors.length} 个监控`);
// 创建监控ID列表
@@ -191,14 +195,13 @@ async function generateDailyStats(c: any) {
lte(monitorStatusHistory24h.timestamp, endTime)
)
);
if (!historyResult.results || historyResult.results.length === 0) {
if (!historyResult || historyResult.length === 0) {
console.log(`在 ${dateStr} 没有找到任何监控历史记录`);
return { success: true, message: "没有历史记录", processed: 0 };
}
console.log(`找到 ${historyResult.results.length} 条历史记录`);
console.log(`找到 ${historyResult.length} 条历史记录`);
// 按监控ID分组处理数据
const statsMap = new Map();
@@ -219,7 +222,7 @@ async function generateDailyStats(c: any) {
}
// 处理所有历史记录
for (const record of historyResult.results) {
for (const record of historyResult) {
const monitorId = record.monitor_id;
if (!statsMap.has(monitorId)) continue;
@@ -306,12 +309,14 @@ async function generateDailyStats(c: any) {
// 从 24h 表中删除已处理的数据
console.log(`开始从24小时热表删除已处理的数据`);
await db.delete(monitorStatusHistory24h).where(
and(
gte(monitorStatusHistory24h.timestamp, startTime),
lte(monitorStatusHistory24h.timestamp, endTime)
)
);
await db
.delete(monitorStatusHistory24h)
.where(
and(
gte(monitorStatusHistory24h.timestamp, startTime),
lte(monitorStatusHistory24h.timestamp, endTime)
)
);
console.log(`从24小时热表删除已处理的数据完成`);
return {
@@ -341,9 +346,10 @@ export default {
const hour = now.getUTCHours();
const minute = now.getUTCMinutes();
if (hour === 0 && minute === 5) {
if (hour == 0 && minute == 5) {
// 生成每日监控统计数据
const statsResult = await generateDailyStats(c);
console.log("生成每日监控统计测试");
if (statsResult.error) {
console.error("生成每日监控统计数据时出错:", statsResult.error);
}
+6
View File
@@ -7,6 +7,12 @@ import { getJwtSecret } from "../utils/jwt";
* 验证请求中的JWT令牌并将解码的payload存入上下文
*/
export const jwtMiddleware = async (c: Context, next: Next) => {
// 跳过所有非 API 路径的认证检查(用于静态文件服务)
if (!c.req.path.startsWith("/api/")) {
return next();
}
// 跳过特定的 API 端点
if (
(c.req.path.endsWith("/status") ||
c.req.path.endsWith("/register") ||
+6
View File
@@ -14,6 +14,11 @@ export interface D1PreparedStatement {
raw<T = unknown>(): Promise<T[]>;
}
// Cloudflare Workers Fetcher 类型定义
export interface Fetcher {
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
}
// 扩展D1Result meta属性的类型
export interface D1Meta {
last_row_id?: number;
@@ -39,4 +44,5 @@ export interface VersionMetadata {
export type Bindings = {
DB: D1Database;
CF_VERSION_METADATA?: VersionMetadata;
ASSETS: Fetcher;
};
+2 -2
View File
@@ -3,8 +3,8 @@ import { ENV_API_BASE_URL, ENV_API_TIMEOUT } from "../config";
// 创建 axios 实例
const api = axios.create({
baseURL: ENV_API_BASE_URL, // 从配置中获取API基础URL
timeout: ENV_API_TIMEOUT, // 从配置中获取超时设置
baseURL: ENV_API_BASE_URL || "", // 从配置中获取API基础URL
timeout: ENV_API_TIMEOUT || 100000, // 从配置中获取超时设置
headers: {
"Content-Type": "application/json",
},
+20
View File
@@ -0,0 +1,20 @@
{
"name": "xugou",
"version": "1.0.0",
"description": "Xugou monitoring system",
"private": true,
"workspaces": [
"frontend",
"backend"
],
"scripts": {
"dev:frontend": "npm run dev --workspace=frontend",
"dev:backend": "npm run dev --workspace=backend",
"build:frontend": "npm run build --workspace=frontend",
"build:backend": "npm run build --workspace=backend",
"db:generate": "npm run db:generate --workspace=backend",
"build": "npm run build:frontend && npm run build:backend",
"deploy": "wrangler deploy",
"preview": "npm run dev --workspace=backend"
}
}
+6 -1
View File
@@ -1,5 +1,5 @@
name = "xugou-backend"
main = "src/index.ts"
main = "backend/src/index.ts"
compatibility_date = "2025-03-10"
compatibility_flags = ["nodejs_compat"]
@@ -12,5 +12,10 @@ binding = "DB"
database_name = "xugou_db"
database_id = "32a2ca5f-3a77-4b08-965e-804612557880"
[assets]
binding = "ASSETS"
directory = "frontend/dist"
not_found_handling = "single-page-application"
[version_metadata]
binding = "CF_VERSION_METADATA"