feat: and cf deploy button
This commit is contained in:
1 parent
4e354cd277
commit
0877ad013f
14 files changed
+1602
-22
No files matched your search
@@ -4,6 +4,9 @@
|
||||
.pnp.js
|
||||
node_modules/
|
||||
|
||||
.wrangler
|
||||
xugou.wiki
|
||||
|
||||
# --- Project Specific: Agent ---
|
||||
# Build artifacts for the agent project
|
||||
/agent/xugou-agent
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -15,6 +15,13 @@
|
||||
"when": 1749736889133,
|
||||
"tag": "0001_fluffy_pestilence",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1750852918387,
|
||||
"tag": "0002_public_domino",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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\`);`
|
||||
}
|
||||
];
|
||||
@@ -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", {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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") ||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user