版本: 0.2.0
日期: 2026-06-19
状态: 进行中
基于: api-design patterns + frontend-patterns
生产前端主线是 frontend/:React + Vite + nginx Dockerfile。
experiments/next-web/ 是 Next.js 实验壳,不参与默认 Docker、CI 或导航。
默认 docker-compose.yml 会从 ./frontend 构建前端镜像。
本文后续关于 Next.js/Hono/Turborepo 的章节属于历史目标架构或迁移设想,不能覆盖当前实现事实。
opencli-admin 是一个现代化的多渠道数据采集管理系统,支持 AI 智能体处理、分布式节点调度和实时通知推送。
多渠道数据采集 (opencli / RSS / API / Web 爬虫 / CLI)
定时计划调度 (Cron)
AI 智能体处理 (Claude / OpenAI / DeepSeek 等)
分布式边缘节点管理
通知推送 (Webhook / 飞书 / 钉钉 / 企微 / Email)
原则
说明
模块化
每个功能独立部署,按需启停
类型安全
前后端共享类型定义
可观测性
结构化日志、链路追踪、指标监控
容错性
重试、降级、超时处理
可扩展性
边缘节点按需扩缩
┌─────────────────────────────────────────────────────────────────────────┐
│ 用户层 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Web 管理界面 │ │ Tauri 桌面端 │ │ CLI 工具 │ │
│ │ (Next.js) │ │ (Rust+Web) │ │ (Rust) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ │ │
└───────────────────────────────────┼─────────────────────────────────────┘
│ HTTPS/WSS
┌───────────────────────────────────┼─────────────────────────────────────┐
│ 边缘计算层 │
├───────────────────────────────────┼─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ API 网关层 (Hono) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Auth │ │ CRUD │ │ 实时 │ │ Webhook │ │ │
│ │ │ 中间件 │ │ 操作 │ │ SSE/WS │ │ 回调 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────┼────────────────────────────────┐ │
│ │ 控制面 (Python) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ III │ │ Scheduler │ │ AI │ │ Notifier │ │ │
│ │ │ 调度 │ │ 定时器 │ │ Processors│ │ 推送 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────┼────────────────────────────────┐ │
│ │ 数据面 (Rust) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ odp- │ │ Dedup │ │ odp- │ │ Redis │ │ │
│ │ │ ingest │ │ 去重 │ │ store │ │ Streams│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────────────┼────────────────────────────────────┘
│
┌────────────────────────────────────┼────────────────────────────────────┐
│ 存储层 │
├────────────────────────────────────┼────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ DuckDB │ │
│ │ (关系数据) │ │ (缓存/队列) │ │ (冷存储) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 文件存储 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Chrome │ │ 采集 │ │ AI │ │ 备份 │ │ │
│ │ │ Profiles │ │ 缓存 │ │ 模型 │ │ 文件 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
│
┌────────────────────────────────────┼────────────────────────────────────┐
│ 采集节点层 │
├────────────────────────────────────┼────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ NAS 节点 │ │ PC 边缘 │ │ 云服务器 │ │
│ │ (主控节点) │ │ (Discord) │ │ (高并发) │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │ opencli │ │ │ │ Discord CLI│ │ │ │ opencli │ │ │
│ │ │ + Chrome │ │ │ │ + opencli │ │ │ │ + Chrome │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 采集流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 触发源 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 定时 │ │ 手动 │ │ Webhook │ │ API │ │
│ │ Cron │ │ 触发 │ │ 回调 │ │ 调用 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └──────────────┴──────────────┴──────────────┘ │
│ │ │
│ ▼ │
│ 2. 任务调度 (III) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ - 解析 cron 表达式 │ │
│ │ - 节点路由 (按站点/优先级/空闲度) │ │
│ │ - 任务分发 (WS / HTTP) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. 节点执行 (odp-rs) │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ opencli │───▶│ Bridge/ │───▶│ 目标平台 │ │
│ │ CLI │ │ CDP │ │ (抓取) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │
│ ▼ │
│ 4. 数据归一化 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ - 字段映射 (title/url/content/author/published_at) │ │
│ │ - 数据清洗 (HTML 解析/敏感词过滤) │ │
│ │ - SHA-256 内容哈希 (去重) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 5. 存储写入 │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Redis │───▶│ PostgreSQL │───▶│ DuckDB │ │
│ │ (实时) │ │ (热数据) │ │ (冷数据) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
apps/api/
├── src/
│ ├── index.ts # 入口
│ ├── routes/
│ │ ├── auth.ts # 认证
│ │ ├── sources.ts # 数据源
│ │ ├── schedules.ts # 定时计划
│ │ ├── records.ts # 采集记录
│ │ ├── nodes.ts # 节点管理
│ │ ├── notifications.ts # 通知
│ │ └── webhooks.ts # Webhook
│ ├── middleware/
│ │ ├── auth.ts # JWT 验证
│ │ ├── cors.ts # CORS
│ │ ├── rate-limit.ts # 限流
│ │ └── logger.ts # 请求日志
│ ├── services/ # 业务逻辑
│ └── types/ # 类型定义
职责 :
HTTP 请求处理
请求验证 (Zod)
认证授权
限流熔断
请求日志
iii/
├── config.yaml # III 引擎配置
├── lib/ # 共享库
│ ├── odp_record.py # ODP 数据格式
│ ├── discord_cli.py # Discord CLI 封装
│ ├── opencli_cli.py # opencli CLI 封装
│ └── schedules.py # 计划管理
├── schedules/ # 定时配置
│ ├── discord.yaml # Discord 采集计划
│ └── opencli.yaml # opencli 采集计划
├── workers/ # III Worker
│ ├── odp-ingest-bridge/ # ODP 摄入桥接
│ ├── collector-discord/ # Discord 采集器
│ ├── collector-opencli/ # opencli 采集器
│ └── schedule-bootstrap/ # 计划引导
└── scripts/ # 运维脚本
├── start-local.ps1 # 本地启动
├── deploy-nas.sh # NAS 部署
└── restart-workers.ps1 # 重启 Worker
职责 :
定时任务调度
任务分发路由
节点健康检查
失败重试
odp-rs/
├── crates/
│ ├── odp-bus/ # Redis Streams 总线
│ │ ├── lib.rs
│ │ └── redis_streams.rs
│ ├── odp-contracts/ # 数据契约
│ │ └── src/lib.rs # ODP Record v2
│ ├── odp-ingest/ # 摄入服务
│ │ ├── src/
│ │ │ ├── main.rs # 入口
│ │ │ ├── handlers.rs # HTTP 处理
│ │ │ ├── dedup.rs # 去重
│ │ │ └── state.rs # 状态管理
│ │ └── Cargo.toml
│ ├── odp-store/ # 存储服务
│ │ ├── src/
│ │ │ ├── main.rs
│ │ │ └── writer.rs # PostgreSQL 写入
│ │ └── Cargo.toml
│ └── odp-egress/ # 导出服务 (待实现)
│ └── src/lib.rs
├── Dockerfile.ingest
├── Dockerfile.store
└── Cargo.toml
职责 :
高并发数据摄入
幂等去重
PostgreSQL 写入
Redis Streams 缓冲
3.4 前端实验壳 (Next.js, 非生产主线)
experiments/next-web/
├── src/
│ ├── app/ # App Router
│ │ ├── (auth)/ # 认证路由组
│ │ │ ├── login/
│ │ │ └── register/
│ │ ├── (dashboard)/ # Dashboard 路由组
│ │ │ ├── layout.tsx # 侧边栏布局
│ │ │ ├── page.tsx # 首页
│ │ │ ├── sources/ # 数据源
│ │ │ ├── schedules/ # 定时计划
│ │ │ ├── records/ # 采集记录
│ │ │ ├── nodes/ # 节点管理
│ │ │ ├── settings/ # 设置
│ │ │ └── providers/ # AI 提供商
│ │ └── api/ # API Routes (可选)
│ ├── components/
│ │ ├── ui/ # shadcn/ui 组件
│ │ └── features/ # 业务组件
│ │ ├── sources/
│ │ ├── schedules/
│ │ ├── records/
│ │ └── nodes/
│ ├── hooks/ # 自定义 Hooks
│ ├── lib/ # 工具库
│ │ ├── api.ts # API 客户端
│ │ └── utils.ts # 工具函数
│ └── types/ # 前端类型
└── public/ # 静态资源
// contracts/record_v2.schema.json
interface ODPRecord {
schema_version : 1 ;
provider : string ; // e.g., "opencli/bilibili"
source_id : string ; // UUID5 from channel_id
event_id : string ; // Stable id for idempotency
ingest_mode : "snapshot" | "stream" ;
source_ts : string ; // ISO 8601
cursor ?: string ; // Pagination cursor
payload : {
title ?: string ;
url ?: string ;
content ?: string ;
author ?: string ;
published_at ?: string ;
[ key : string ] : unknown ; // Provider-specific fields
} ;
raw_data ?: object ; // Original data
trace_id ?: string ; // For distributed tracing
task_id ?: string ; // Collection task reference
}
// packages/db/src/schema.ts
import { pgTable , uuid , text , timestamp , jsonb , boolean } from 'drizzle-orm/pg-core' ;
export const sources = pgTable ( 'sources' , {
id : uuid ( 'id' ) . primaryKey ( ) . defaultRandom ( ) ,
name : text ( 'name' ) . notNull ( ) ,
type : text ( 'type' ) . notNull ( ) , // opencli, rss, api, web, cli
config : jsonb ( 'config' ) . notNull ( ) ,
enabled : boolean ( 'enabled' ) . default ( true ) ,
createdAt : timestamp ( 'created_at' ) . defaultNow ( ) ,
updatedAt : timestamp ( 'updated_at' ) . defaultNow ( ) ,
} ) ;
export const schedules = pgTable ( 'schedules' , {
id : uuid ( 'id' ) . primaryKey ( ) . defaultRandom ( ) ,
sourceId : uuid ( 'source_id' ) . references ( ( ) => sources . id ) ,
cron : text ( 'cron' ) . notNull ( ) ,
timezone : text ( 'timezone' ) . default ( 'Asia/Shanghai' ) ,
enabled : boolean ( 'enabled' ) . default ( true ) ,
nodeId : uuid ( 'node_id' ) . references ( ( ) => nodes . id ) ,
createdAt : timestamp ( 'created_at' ) . defaultNow ( ) ,
} ) ;
export const records = pgTable ( 'records' , {
id : uuid ( 'id' ) . primaryKey ( ) . defaultRandom ( ) ,
sourceId : uuid ( 'source_id' ) . references ( ( ) => sources . id ) ,
taskId : uuid ( 'task_id' ) . references ( ( ) => tasks . id ) ,
provider : text ( 'provider' ) . notNull ( ) ,
eventId : text ( 'event_id' ) . notNull ( ) , // For dedup
title : text ( 'title' ) ,
url : text ( 'url' ) ,
content : text ( 'content' ) ,
author : text ( 'author' ) ,
rawData : jsonb ( 'raw_data' ) ,
aiSummary : text ( 'ai_summary' ) ,
aiTags : text ( 'ai_tags' ) . array ( ) ,
createdAt : timestamp ( 'created_at' ) . defaultNow ( ) ,
} ) ;
export const nodes = pgTable ( 'nodes' , {
id : uuid ( 'id' ) . primaryKey ( ) . defaultRandom ( ) ,
name : text ( 'name' ) . notNull ( ) ,
host : text ( 'host' ) . notNull ( ) ,
port : text ( 'port' ) . notNull ( ) ,
status : text ( 'status' ) . default ( 'offline' ) , // online, offline, busy
capabilities : text ( 'capabilities' ) . array ( ) ,
lastSeen : timestamp ( 'last_seen' ) ,
createdAt : timestamp ( 'created_at' ) . defaultNow ( ) ,
} ) ;
export const tasks = pgTable ( 'tasks' , {
id : uuid ( 'id' ) . primaryKey ( ) . defaultRandom ( ) ,
sourceId : uuid ( 'source_id' ) . references ( ( ) => sources . id ) ,
scheduleId : uuid ( 'schedule_id' ) . references ( ( ) => schedules . id ) ,
nodeId : uuid ( 'node_id' ) . references ( ( ) => nodes . id ) ,
status : text ( 'status' ) . notNull ( ) , // pending, running, success, failed
startedAt : timestamp ( 'started_at' ) ,
completedAt : timestamp ( 'completed_at' ) ,
error : text ( 'error' ) ,
createdAt : timestamp ( 'created_at' ) . defaultNow ( ) ,
} ) ;
认证:
POST /api/v1/auth/login # 登录
POST /api/v1/auth/register # 注册
POST /api/v1/auth/logout # 登出
POST /api/v1/auth/refresh # 刷新 Token
数据源:
GET /api/v1/sources # 列表
POST /api/v1/sources # 创建
GET /api/v1/sources/:id # 详情
PUT /api/v1/sources/:id # 更新
DELETE /api/v1/sources/:id # 删除
POST /api/v1/sources/:id/test # 测试连接
定时计划:
GET /api/v1/schedules # 列表
POST /api/v1/schedules # 创建
GET /api/v1/schedules/:id # 详情
PUT /api/v1/schedules/:id # 更新
DELETE /api/v1/schedules/:id # 删除
采集记录:
GET /api/v1/records # 列表 (分页/搜索)
GET /api/v1/records/:id # 详情
DELETE /api/v1/records/:id # 删除
POST /api/v1/records/batch-delete # 批量删除
节点管理:
GET /api/v1/nodes # 列表
POST /api/v1/nodes # 注册
GET /api/v1/nodes/:id # 详情
DELETE /api/v1/nodes/:id # 删除
POST /api/v1/nodes/:id/heartbeat # 心跳
任务:
GET /api/v1/tasks # 列表
GET /api/v1/tasks/:id # 详情
POST /api/v1/tasks/:id/cancel # 取消
通知:
GET /api/v1/notifications # 列表
POST /api/v1/notifications # 创建
PUT /api/v1/notifications/:id # 更新
DELETE /api/v1/notifications/:id # 删除
Webhook:
POST /api/v1/webhooks/:id/trigger # 手动触发
// 成功响应
interface ApiResponse < T > {
data : T ;
meta ?: {
page ?: number ;
pageSize ?: number ;
total ?: number ;
hasMore ?: boolean ;
} ;
}
// 错误响应
interface ApiError {
error : {
code : string ; // e.g., "VALIDATION_ERROR"
message : string ;
details ?: Array < {
field : string ;
issue : string ;
} > ;
} ;
}
┌─────────────────────────────────────────────────────────────┐
│ 开发机器 (本地) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Next.js │ │ Hono │ │ III │ │
│ │ Dev Server │ │ API │ │ Engine │ │
│ │ :3000 │ │ :8000 │ │ :49134 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ odp-rs │ │
│ │ :5432 │ │ :6379 │ │ :8040 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ NAS (192.168.50.130) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Docker Compose │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Nginx │ │ Hono │ │ │
│ │ │ 端口 80 │ │ API │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ III 调度 + Workers │ │ │
│ │ │ (schedule-bootstrap, collector-*) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ odp-ingest │ │ odp-store │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ PostgreSQL │ │ Redis │ │ │
│ │ │ + TimescaleDB │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ opencli-agent │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Chrome │ │ opencli │ │ │
│ │ │ Profile │ │ Daemon │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 控制中心 (NAS) │
├─────────────────────────────────────────────────────────────────────┤
│ Hono API + III + PostgreSQL + Redis + odp-rs │
└─────────────────────────────────────────────────────────────────────┘
│ │ │
WS/HTTP │ │ │ WS/HTTP
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ PC 边缘 │ │ 云服务器 │ │ 移动设备 │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ Discord CLI │ │ opencli + Chrome│ │ (监控/查看) │
│ opencli + Chrome│ │ (高并发采集) │ │ │
│ (实时任务) │ │ (公开数据) │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 认证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 登录请求 │
│ ┌──────────┐ │
│ │ 用户 │──▶ /api/v1/auth/login │
│ └──────────┘ │ │
│ ▼ │
│ 2. 验证凭证 │
│ ┌────────────────────────────────────────────────┐ │
│ │ - 邮箱/密码 → bcrypt 验证 │ │
│ │ - OAuth → 第三方验证 │ │
│ │ - API Key → 签名验证 │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. 生成 Token │
│ ┌────────────────────────────────────────────────┐ │
│ │ - Access Token (15min, JWT) │ │
│ │ - Refresh Token (7d, HttpOnly Cookie) │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. 返回响应 │
│ ┌────────────────────────────────────────────────┐ │
│ │ { accessToken, expiresIn, user } │ │
│ └────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
角色
权限
Admin
全部权限
Operator
源/计划/记录 CRUD,节点查看
Viewer
只读访问
// 使用 Pino 结构化日志
logger . info ( {
level : "info" ,
service : "opencli-api" ,
requestId : "req-123" ,
userId : "user-456" ,
action : "source.create" ,
duration : 150 ,
} , "Source created" ) ;
// 级别:
enum LogLevel {
TRACE = 10 , // 调试详情
DEBUG = 20 , // 开发调试
INFO = 30 , // 业务事件
WARN = 40 , // 可恢复问题
ERROR = 50 , // 失败
}
指标
类型
说明
http_requests_total
Counter
HTTP 请求总数
http_request_duration_seconds
Histogram
请求延迟
task_queue_size
Gauge
任务队列大小
task_success_total
Counter
成功任务数
task_failed_total
Counter
失败任务数
node_online_count
Gauge
在线节点数
collection_rate
Gauge
采集速率 (records/s)
Trace: task-abc123
├── Span: iii.schedule.tick (100ms)
│ └── Span: iii.route.select_node (10ms)
│ └── Span: node.dispatch (50ms)
│ └── Span: odp.collect (500ms)
│ ├── Span: opencli.execute (400ms)
│ └── Span: odp.ingest (100ms)
│ └── Span: postgres.insert (50ms)
└── Span: notifier.dispatch (20ms)
版本
日期
变更
0.2.0
2026-06-19
补充 api-design + frontend-patterns 最佳实践
0.1.0
2026-06-19
初始架构文档
10. API 设计增强 (api-design patterns)
// 成功响应 (符合 api-design skill)
interface ApiResponse < T > {
data : T ;
meta ?: {
page ?: number ;
pageSize ?: number ;
total ?: number ;
hasMore ?: boolean ;
} ;
links ?: {
self : string ;
next ?: string ;
prev ?: string ;
first ?: string ;
last ?: string ;
} ;
}
// 错误响应 (符合 api-design skill)
interface ApiError {
error : {
code : string ; // validation_error / not_found / forbidden / conflict
message : string ;
details ?: Array < {
field : string ;
message : string ;
code : string ; // invalid_format / out_of_range / required
} > ;
} ;
}
// HTTP 状态码规范 (api-design skill)
enum StatusCode {
// 2xx 成功
OK = 200 ,
CREATED = 201 ,
NO_CONTENT = 204 ,
// 4xx 客户端错误
BAD_REQUEST = 400 ,
UNAUTHORIZED = 401 ,
FORBIDDEN = 403 ,
NOT_FOUND = 404 ,
CONFLICT = 409 ,
UNPROCESSABLE_ENTITY = 422 , // 语义错误
TOO_MANY_REQUESTS = 429 ,
// 5xx 服务端错误
INTERNAL_ERROR = 500 ,
BAD_GATEWAY = 502 ,
SERVICE_UNAVAILABLE = 503 ,
}
10.2 查询参数规范 (api-design skill)
// 过滤语法
GET / api / v1 / records
?status = success // 等于
?created_at [ gte ] = 2026 - 01 - 01 // 大于等于
?created_at [ lte ] = 2026 - 06 - 19 // 小于等于
?provider = opencli / bilibili // 精确匹配
?tags [ contains ] = ai // 数组包含
// 排序语法
GET / api / v1 / records ?sort = - created_at , provider
// - 前缀表示降序
// 稀疏字段集 (减少 payload)
GET / api / v1 / records ?fields = id , title , url , created_at
GET / api / v1 / sources ?fields = id , name , type , enabled
// 全量删除 (带条件)
DELETE / api / v1 / records ?before = 2026 - 01 - 01 & provider = opencli / test
10.3 分页规范 (api-design skill)
// 光标分页 (推荐用于大数据量)
GET / api / v1 / records ?cursor = eyJpZCI6MTIzfQ & limit = 20
// 响应
{
"data" : [ ...] ,
"meta" : {
"has_next" : true ,
"next_cursor" : "eyJpZCI6MTQzfQ"
}
}
// 偏移分页 (适用于小数据集)
GET / api / v1 / records ?page = 2 & per_page = 20
// 响应
{
"data" : [ ...] ,
"meta" : {
"total" : 142 ,
"page" : 2 ,
"pageSize" : 20 ,
"totalPages" : 8
} ,
"links" : {
"self" : "/api/v1/records?page=2&per_page=20" ,
"next" : "/api/v1/records?page=3&per_page=20" ,
"prev" : "/api/v1/records?page=1&per_page=20" ,
"first" : "/api/v1/records?page=1&per_page=20" ,
"last" : "/api/v1/records?page=8&per_page=20"
}
}
// 批量创建
POST / api / v1 / sources / batch
Request: { "sources" : [ { "name" : "..." , "type" : "..." } , ...] }
Response: {
"data" : {
"created" : 5 ,
"failed" : 1 ,
"errors" : [ { "index" : 2 , "error" : "Name already exists" } ]
}
}
// 批量更新
PATCH / api / v1 / sources / batch
Request: { "ids" : [ "uuid1" , "uuid2" ] , "updates" : { "enabled" : false } }
// 批量删除
DELETE / api / v1 / records / batch
Request: { "ids" : [ "uuid1" , "uuid2" ] }
Response: { "data" : { "deleted" : 2 } }
10.5 限流规范 (api-design skill)
// 限流响应头
Response Headers :
X - RateLimit - Limit : 100
X - RateLimit - Remaining : 95
X - RateLimit - Reset : 1640000000
// 超限响应
HTTP / 1.1 429 Too Many Requests
Retry - After : 60
{
"error" : {
"code" : "rate_limit_exceeded" ,
"message" : "Rate limit exceeded. Try again in 60 seconds."
}
}
// 限流分层
| Tier | Limit | Window | Use Case |
| -- -- -- | -- -- -- - | -- -- -- -- | -- -- -- -- -- |
| Anonymous | 30 / min | Per IP | 公开端点 |
| Authenticated | 100 / min | Per user | 标准 API |
| Premium | 1000 / min | Per API key | 付费用户 |
| Internal | 10000 / min | Per service | 服务间调用 |
11. 前端模式增强 (frontend-patterns)
11.1 组件设计模式 (frontend-patterns)
// Compound Components 模式 (frontend-patterns)
interface CardProps {
children : React . ReactNode
variant ?: 'default' | 'outlined' | 'elevated'
}
export function Card ( { children, variant = 'default' } : CardProps ) {
return < div className = { cn ( 'card' , `card-${ variant } ` ) } > { children} < / d i v >
}
export function CardHeader ( { children } : { children : React . ReactNode } ) {
return < div className = "card-header" > { children} < / d i v >
}
export function CardBody ( { children } : { children : React . ReactNode } ) {
return < div className = "card-body" > { children} < / d i v >
}
export function CardFooter ( { children } : { children : React . ReactNode } ) {
return < div className = "card-footer" > { children} < / d i v >
}
// 使用
< Card variant = "elevated" >
< CardHeader > 数据源配置 < / C a r d H e a d e r >
< CardBody >
< SourceForm / >
< / CardBody >
< CardFooter >
< Button > 保存 < / B u t t o n >
< / C a r d F o o t e r >
< / C a r d >
11.2 自定义 Hooks (frontend-patterns)
// packages/web/src/hooks/useDebounce.ts
export function useDebounce < T > ( value : T , delay : number ) : T {
const [ debouncedValue , setDebouncedValue ] = useState < T > ( value )
useEffect ( ( ) => {
const handler = setTimeout ( ( ) => setDebouncedValue ( value ) , delay )
return ( ) => clearTimeout ( handler )
} , [ value , delay ] )
return debouncedValue
}
// packages/web/src/hooks/useToggle.ts
export function useToggle ( initialValue = false ) : [ boolean , ( ) => void ] {
const [ value , setValue ] = useState ( initialValue )
const toggle = useCallback ( ( ) => setValue ( v => ! v ) , [ ] )
return [ value , toggle ]
}
// packages/web/src/hooks/useClickOutside.ts
export function useClickOutside (
ref : RefObject < HTMLElement > ,
handler : ( ) => void
) {
useEffect ( ( ) => {
const listener = ( event : MouseEvent | TouchEvent ) => {
if ( ! ref . current || ref . current . contains ( event . target as Node ) ) return
handler ( )
}
document . addEventListener ( 'mousedown' , listener )
document . addEventListener ( 'touchstart' , listener )
return ( ) => {
document . removeEventListener ( 'mousedown' , listener )
document . removeEventListener ( 'touchstart' , listener )
}
} , [ ref , handler ] )
}
// packages/web/src/hooks/useLocalStorage.ts
export function useLocalStorage < T > (
key : string ,
initialValue : T
) : [ T , ( value : T ) => void ] {
const [ storedValue , setStoredValue ] = useState < T > ( ( ) => {
if ( typeof window === 'undefined' ) return initialValue
try {
const item = window . localStorage . getItem ( key )
return item ? JSON . parse ( item ) : initialValue
} catch { return initialValue }
} )
const setValue = ( value : T ) => {
setStoredValue ( value )
window . localStorage . setItem ( key , JSON . stringify ( value ) )
}
return [ storedValue , setValue ]
}
11.3 错误边界 (frontend-patterns)
// packages/web/src/components/ui/error-boundary.tsx
interface ErrorBoundaryState {
hasError : boolean
error : Error | null
}
export class ErrorBoundary extends React . Component <
{ children : React . ReactNode ; fallback ?: React . ReactNode } ,
ErrorBoundaryState
> {
state : ErrorBoundaryState = { hasError : false , error : null }
static getDerivedStateFromError ( error : Error ) : ErrorBoundaryState {
return { hasError : true , error }
}
componentDidCatch ( error : Error , errorInfo : React . ErrorInfo ) {
console . error ( 'ErrorBoundary caught:' , error , errorInfo )
// 可发送到 Sentry
}
render ( ) {
if ( this . state . hasError ) {
return this . props . fallback ?? (
< div className = "error-fallback" >
< h2 > 出错了 < / h 2 >
< p > { this. state . error ?. message } < / p >
< Button onClick = { ( ) => this . setState ( { hasError : false } ) } >
重试
< / B u t t o n >
< / div >
)
}
return this . props . children
}
}
// 使用
< ErrorBoundary fallback = { < GlobalErrorPage / > } >
< Dashboard / >
< / ErrorBoundary >
11.4 虚拟列表 (frontend-patterns)
// packages/web/src/components/ui/virtual-list.tsx
import { useVirtualizer } from '@tanstack/react-virtual'
interface VirtualListProps < T > {
items : T [ ]
estimateSize : number
renderItem : ( item : T , index : number ) => React . ReactNode
}
export function VirtualList < T > ( {
items,
estimateSize,
renderItem
} : VirtualListProps < T > ) {
const parentRef = useRef < HTMLDivElement > ( null )
const virtualizer = useVirtualizer ( {
count : items . length ,
getScrollElement : ( ) => parentRef . current ,
estimateSize : ( ) => estimateSize ,
overscan : 5 ,
} )
return (
< div ref = { parentRef } className = "h-[600px] overflow-auto" >
< div style = { { height : virtualizer . getTotalSize ( ) , position : 'relative' } } >
{ virtualizer . getVirtualItems ( ) . map ( virtualRow => (
< div
key = { virtualRow . index }
style = { {
position : 'absolute' ,
top : 0 ,
left : 0 ,
width : '100%' ,
height : virtualRow . size ,
transform : `translateY(${virtualRow . start } px)`,
} }
>
{ renderItem ( items [ virtualRow . index ] , virtualRow . index ) }
< / div >
) ) }
< / div >
< / div >
)
}
// 使用 (采集记录列表)
< VirtualList
items = { records}
estimateSize = { 80 }
renderItem = { ( record ) => < RecordCard record = { record} / > }
/ >
11.5 动画模式 (frontend-patterns)
// packages/web/src/components/ui/animate-list.tsx
import { motion , AnimatePresence } from 'framer-motion'
interface AnimateListProps < T > {
items : T [ ]
renderItem : ( item : T , index : number ) => React . ReactNode
getKey : ( item : T ) => string
}
export function AnimateList < T > ( {
items,
renderItem,
getKey
} : AnimateListProps < T > ) {
return (
< AnimatePresence mode = "popLayout" >
{ items. map ( ( item , index ) => (
< motion . div
key = { getKey ( item ) }
initial = { { opacity : 0 , y : 20 } }
animate = { { opacity : 1 , y : 0 } }
exit = { { opacity : 0 , y : - 20 } }
transition = { { duration : 0.3 , delay : index * 0.05 } }
layout
>
{ renderItem ( item , index ) }
< / motion . div >
) ) }
< / AnimatePresence >
)
}
// Modal 动画 (frontend-patterns)
export function AnimatedModal ( {
isOpen,
onClose,
children
} : ModalProps ) {
return (
< AnimatePresence >
{ isOpen && (
< >
< motion . div
className = "modal - overlay "
initial = { { opacity : 0 } }
animate = { { opacity : 1 } }
exit = { { opacity : 0 } }
onClick = { onClose }
/ >
< motion . div
className = "modal - content "
initial = { { opacity : 0 , scale : 0.9 , y : 20 } }
animate = { { opacity : 1 , scale : 1 , y : 0 } }
exit = { { opacity : 0 , scale : 0.9 , y : 20 } }
transition = { { type : 'spring ', damping : 25 } }
>
{ children }
< / motion . div >
< / >
) }
< / AnimatePresence >
)
}
11.6 表单验证 (frontend-patterns)
// packages/web/src/lib/validations.ts
import { z } from 'zod'
export const createSourceSchema = z . object ( {
name : z . string ( ) . min ( 1 , '名称不能为空' ) . max ( 100 , '名称最长 100 字符' ) ,
type : z . enum ( [ 'opencli' , 'rss' , 'api' , 'web' , 'cli' ] ) ,
config : z . object ( {
command : z . string ( ) . optional ( ) ,
url : z . string ( ) . url ( '请输入有效的 URL' ) . optional ( ) ,
interval : z . number ( ) . min ( 60 ) . optional ( ) ,
} ) ,
enabled : z . boolean ( ) . default ( true ) ,
} )
export type CreateSourceInput = z . infer < typeof createSourceSchema >
// packages/web/src/components/features/sources/source-form.tsx
export function SourceForm ( ) {
const [ errors , setErrors ] = useState < Record < string , string > > ( { } )
const handleSubmit = async ( data : unknown ) => {
const result = createSourceSchema . safeParse ( data )
if ( ! result . success ) {
const fieldErrors : Record < string , string > = { }
result . error . issues . forEach ( issue => {
const path = issue . path . join ( '.' )
fieldErrors [ path ] = issue . message
} )
setErrors ( fieldErrors )
return
}
// 提交
}
return (
< form onSubmit = { handleSubmit} >
{ /* 表单字段 */ }
< Input
{ ...register ( 'name' ) }
error = { errors . name}
/ >
< Select { ...register ( 'type' ) } error = { errors . type} / >
{ /* ... */ }
< / f o r m >
)
}
11.7 性能优化 (frontend-patterns)
// 代码分割 (frontend-patterns)
const SourceConfigForm = lazy ( ( ) => import ( './SourceConfigForm' ) )
const NodeDetail = lazy ( ( ) => import ( './NodeDetail' ) )
export function SourcesPage ( ) {
return (
< Suspense fallback = { < Skeleton / > } >
< SourceConfigForm / >
< / Suspense >
)
}
// React.memo 优化纯组件 (frontend-patterns)
export const SourceCard = React . memo < SourceCardProps > ( ( { source } ) => {
return (
< Card >
< CardHeader > { source . name} < / C a r d H e a d e r >
< CardBody > { source . type} < / C a r d B o d y >
< / C a r d >
)
} , ( prev , next ) => prev . source . id === next . source . id )
// useMemo 缓存计算 (frontend-patterns)
const sortedRecords = useMemo ( ( ) => {
return [ ...records ] . sort ( ( a , b ) =>
new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( )
)
} , [ records ] )
// useCallback 稳定回调 (frontend-patterns)
const handleSelect = useCallback ( ( id : string ) => {
setSelectedIds ( prev =>
prev . includes ( id )
? prev . filter ( i => i !== id )
: [ ...prev , id ]
)
} , [ ] )
11.8 可访问性 (frontend-patterns)
// 键盘导航 (frontend-patterns)
export function CommandMenu ( { items } : CommandMenuProps ) {
const [ isOpen , setIsOpen ] = useState ( false )
const [ activeIndex , setActiveIndex ] = useState ( 0 )
const handleKeyDown = ( e : React . KeyboardEvent ) => {
switch ( e . key ) {
case 'ArrowDown' :
e . preventDefault ( )
setActiveIndex ( i => Math . min ( i + 1 , items . length - 1 ) )
break
case 'ArrowUp' :
e . preventDefault ( )
setActiveIndex ( i => Math . max ( i - 1 , 0 ) )
break
case 'Enter' :
e . preventDefault ( )
selectItem ( items [ activeIndex ] )
break
case 'Escape' :
setIsOpen ( false )
break
}
}
return (
< div
role = "combobox"
aria - expanded = { isOpen }
aria - haspopup = "listbox"
onKeyDown = { handleKeyDown }
>
{ /* ... */ }
< / d i v >
)
}
// Focus 管理 (frontend-patterns)
export function Modal ( { isOpen, onClose } : ModalProps ) {
const modalRef = useRef < HTMLDivElement > ( null )
const previousFocusRef = useRef < HTMLElement | null > ( null )
useEffect ( ( ) => {
if ( isOpen ) {
previousFocusRef . current = document . activeElement as HTMLElement
modalRef . current ?. focus ( )
} else {
previousFocusRef . current ?. focus ( )
}
} , [ isOpen ] )
return isOpen ? (
< div
ref = { modalRef }
role = "dialog"
aria - modal = "true"
tabIndex = { - 1 }
onKeyDown = { e => e . key = == 'Escape' & & onClose ( ) }
>
{ children }
< / div >
) : null
}
组件
用途
Button
按钮
Card
卡片容器
Dialog
对话框
Input
输入框
Select
选择器
Badge
标签
Tooltip
提示
Separator
分隔线
Skeleton
加载占位
AlertDialog
确认对话框
组件
用途
DataTable
数据表格 (虚拟滚动)
AnimateList
动画列表
ErrorBoundary
错误边界
VirtualList
虚拟列表
AnimatedModal
动画模态框
SourceForm
数据源表单
ScheduleForm
定时计划表单
RecordCard
记录卡片
NodeCard
节点卡片
StatsChart
统计图表
Hook
用途
useDebounce
防抖
useToggle
开关状态
useClickOutside
点击外部
useLocalStorage
本地存储
useMediaQuery
媒体查询
useAsync
异步状态
usePagination
分页
// API 路由缓存 (Next.js)
export const dynamic = 'force-dynamic'
// GET 端点缓存
GET / api / v1 / sources // Cache: 60s (stale-while-revalidate)
GET / api / v1 / sources / :id // Cache: 300s
GET / api / v1 / nodes // Cache: 30s (节点状态经常变化)
GET / api / v1 / stats // No cache (实时数据)
// POST/PUT/DELETE 端点
// 自动失效相关 GET 缓存
POST / api / v1 / sources // Invalidate: /api/v1/sources
PUT / api / v1 / sources / :id // Invalidate: /api/v1/sources/:id
DELETE / api / v1 / sources / :id // Invalidate: /api/v1/sources
// TanStack Query 配置
const queryClient = new QueryClient ( {
defaultOptions : {
queries : {
staleTime : 60 * 1000 , // 1 分钟内认为新鲜
gcTime : 5 * 60 * 1000 , // 5 分钟后垃圾回收
retry : 3 , // 重试 3 次
refetchOnWindowFocus : true , // 窗口聚焦时重新获取
} ,
} ,
} )
// 特定查询配置
useQuery ( {
queryKey : [ 'sources' ] ,
queryFn : fetchSources ,
staleTime : 30 * 1000 , // 数据源 30s 过期
} )
useQuery ( {
queryKey : [ 'records' , cursor ] ,
queryFn : ( ) => fetchRecords ( cursor ) ,
staleTime : 0 , // 记录列表始终获取最新
} )