Compare commits

..

244 Commits

Author SHA1 Message Date
ryan 5fd056f99f chore(release): bump version to v1.4.1
### 🛠 修复
- 修复了 Redis 维护通知(maintenance notifications)在启动阶段默认开启协商可能影响启动流程的问题,新增 `maint_notifications` 启动开关并将其默认值调整为禁用,同时统一应用到平台 Redis 与 Asynq 任务客户端。
- 修复了 PostgreSQL SQL 日志级别配置未按预期生效的问题,将常规 SQL 语句输出统一收敛至 debug 级别,避免在生产日志中产生冗余输出。
- 修复了 CI 镜像构建流水线中缺少 IMAGE 环境变量、导致后续步骤无法正确引用镜像地址的问题。
- 修复了前端 prettier 配置缺失的问题。

### ⚡️ 优化与改进
- 在 API、Worker、Scheduler 等进程启动并完成监听器绑定后,统一打印服务 Banner 信息,便于快速识别各进程的运行状态、监听端口与数据库迁移进度。
- 管理员设置页面在浏览器刷新后自动保持当前选中的标签页,通过 URL query 参数持久化并校验非法取值,提升后台操作的连续性。

### 💄 其他/体验
- 新增前端 prettier 配置文件与忽略规则,统一代码格式化规范。
2026-07-13 16:05:55 +08:00
ryan 08fac67f2a feat(startup): print service banner after listener ready 2026-07-13 15:59:29 +08:00
ryan 9cac25696a fix(redis): add maintenance notification startup switch
Default Redis maintenance notification negotiation to disabled and apply the startup-only setting to both platform and Asynq clients.
2026-07-13 15:48:31 +08:00
ryan a938a5e67f fix(db): log SQL statements at debug level 2026-07-13 15:41:04 +08:00
ryan e0eb4e5975 prettier 2026-07-13 15:17:45 +08:00
ryan 08e61ce833 fix(frontend): prettier config 2026-07-13 15:14:47 +08:00
ryan c293a255d0 feat(frontend): persist selected tab on admin settings page refresh
Store the selected tab of the admin settings page in the URL query string under the tab parameter. Defend against invalid values by validating it against the list of known tabs.
2026-06-30 20:52:24 +08:00
ryan 1855b48028 fix ci 2026-06-28 17:31:57 +08:00
ryan 2310e1d12a chore(release): bump version to v1.4.0
### 🛠 修复
- 修复了前端人机验证(Captcha)中请求和响应信封格式不匹配的问题。
- 修复了由于自定义 CSS 变量命名冲突导致用户删除确认按钮在特定主题模式下变黑的问题。
- 修复了由于注册 `reset-passwd` 命令行后触发 Cobra 严格子命令校验、导致原有以参数形式启动的 `all`、`api`、`worker`、`scheduler` 等模式命令失效的 Bug。

### ⚡️ 优化与改进
- 重构了 OAuth 缓存,将其替换为系统标准 RAM 内存缓存并引入了多节点之间的 Pub/Sub 订阅发布缓存同步机制,增强高频鉴权的稳定性。
- 后端新增 `reset-passwd` 命令行工具,支持管理员在后台快速重置指定用户的密码并使该用户的 Token 缓存立即失效。
- 后台用户管理支持在详情中直接编辑个人资料和重置密码,并在列表页新增“邮箱”字段与支持邮箱前缀 LIKE 模糊条件搜索。
- 前端引入本地自托管的 Inter 字体,搭配系统原生“苹方”(PingFang SC)与微软雅黑,极大提升了西文、数字及中文字符在不同操作系统下的显示与排版效果。
- 优化了 OpenTelemetry 链路追踪配置,使用 Schemaless 规范避免 semconv 产生 Schema 版本冲突。

### 💄 其他/体验
- 清理了系统设置及个人中心等页面中所有硬编码的靛蓝(indigo)样式色彩,统一改用标准的 Primary 主题配色,使其完美支持多主题与亮暗色模式切换。
- 移除了用户编辑弹窗和徽章(Badge)上不合规的硬编码前背景色,确保所有基础组件的配色由主题系统统一驱动。
- 优化了前端 UI,微调了侧边栏激活项的字体颜色,并修复了危险动作(Destructive)按钮在特定模式下的色彩对比度。
- 优化了 CI 流程,提高构建流水线的执行效率。
2026-06-28 11:40:59 +08:00
ryan f5ee19405f fix(cmd): register all app modes as subcommands in cobra
Resolve unknown command error when launching all/api/worker/scheduler modes due to Cobra strict subcommand validation triggered by reset-passwd. Subcommands now run database migrations via dynamic PreRun hooks.
2026-06-28 11:37:26 +08:00
ryan de605834b7 style(frontend): remove hardcoded indigo colors in settings components
Replace all manual bg-indigo and text-indigo overrides with standard CSS variables such as bg-primary/10 and text-primary across common settings modules to support theme integration.
2026-06-28 11:30:52 +08:00
ryan 5a6ffd600a feat(frontend): integrate self-hosted Inter Google font
Use next/font/google to download and host Inter locally, injecting it into html root via Tailwind CSS variables and fallback to PingFang SC for Chinese characters.
2026-06-28 11:20:02 +08:00
ryan bdbc42c22d style(frontend): remove hardcoded text and bg color overrides on components
Remove redundant text/bg style overrides on Button and Badge components in UserDetailSheet and UserFilterBar to allow proper default and destructive theme variants.
2026-06-28 11:08:11 +08:00
ryan 84626f9613 fix(frontend): remove custom classes to fix black delete confirmation button
Remove custom className from AlertDialogAction to prevent CSS variables overlap, restoring the default red background with white text styling.
2026-06-28 11:06:51 +08:00
ryan 932f0c65b9 feat(admin): support user profile editing, password resetting, and email column with search
- Add UpdateUser API and logics supporting nickname, email, admin flag modification, and password reset.
- Relocate user delete button and confirmation Alert into the EditUserModal.
- Optimize admin Switch change to trigger instant API request with rollback support.
- Fix missing email field in edit form initialization by fetching full profile metadata.
- Render email column in users list and support email-based filtering in UserFilterBar.
- Remove hardcoded styles and sizes from Switch components to follow global theme.
2026-06-28 11:05:57 +08:00
ryan 206f8b59c9 feat(cmd): add reset-passwd command to reset user password
- Added ./wavelet reset-passwd subcommand to reset user passwords via CLI
- Supported --user flag; if not specified, prompts for username interactively
- Supported --password flag; if not specified, generates a secure random password
- Handled access token deletion and cache invalidation
- Added comprehensive unit tests
2026-06-28 10:50:15 +08:00
ryan a25a570973 fix(frontend): adjust sidebar active text color and fix destructive button contrast
- Update sidebar active menu button text color to use theme dynamic `sidebar-primary` variable instead of hardcoded hex value.

- Add missing `destructive-foreground` variables to default theme config and styles, resolving the black-on-black text contrast issue on confirmation dialog delete buttons.

- Update changelog to track these fixes.
2026-06-28 10:50:15 +08:00
ryan 681de3b8cc refactor(oauth): replace legacy oauth cache with standard ram cache and add pubsub synchronization
- Replaced custom map-based cache in apps/oauth/cache.go with standard pkg/cache/ram framework.
- Implemented Redis Pub/Sub invalidation channels for distributed token and user cache synchronization.
- Created apps/oauth/cache_test.go to verify local cache operations and pub/sub broadcasts.

refactor(cache): generic RAM cache with CoW and unified preheating

Replaced L2 Redis cache and old cache package with process-local generic pkg/cache/ram. Implemented Copy-on-Write for reads, fine-grained locks per type for writes, and unified preheating in bootstrap. Changed cache invalidation to lazy-loading to resolve SQLite deadlocks during transactions.
2026-06-27 14:17:26 +08:00
ryan a4f6c2ae34 fix(frontend): cap envelope mismatch 2026-06-21 11:20:36 +08:00
ryan 6e6bce6a03 Optimize CI 2026-06-20 13:53:54 +08:00
ryan ef5d5b46af chore(release): bump version to v1.3.1
### 🛠 修复
- 修复了风控中间件测试在 ClickHouse 批写架构迁移后无法正确初始化的问题。
- 修复了 OpenTelemetry trace provider 初始化时 semconv Schema URL 版本冲突导致进程无法启动的问题。

### ⚡️ 优化与改进
- 新增 ClickHouse 独立 OLAP 管线,以 goose 迁移作为唯一 schema 来源,并抽取 analytics repository 统一访问日志读写。
- 新增通用 ClickHouse batchwriter 批量写入框架,支持各业务域独立缓冲 flush 管道与默认批处理调优。
- 风控访问日志与管理端日志查询改为经 repository 层批写与查询,移除内联 SQL 与手动 DDL 维护路径。
- OAuth Access Token 与会话校验引入 RAM+Redis 缓存,降低高频鉴权路径的数据库读取压力。
- 上传元数据、Auth Source 与系统配置批量读取接入三层缓存(RAM→Redis→DB),并通过 pub/sub 支持多节点失效同步。
- 上传统计增量更新收敛为单事务写入,减少 ingest/remove 路径的锁竞争与统计偏差风险。
- ClickHouse 迁移与连接初始化增加进程隔离,避免与主库迁移互相阻塞。
- 引入 lifecycle 优雅停机钩子,确保进程退出前 flush 批写缓冲与释放资源。

### 💄 其他/体验
- 新增 cache-framework 与 clickhouse-batchwriter 开发技能,并更新 AGENTS.md Skill 关联索引。
- 登录与 Token 校验路径增加缓存预热,缩短冷启动后首次鉴权延迟。
2026-06-20 11:20:11 +08:00
ryan a1f6459c09 fix(trace): 使用 NewSchemaless 避免 semconv schema 版本冲突
业务 Resource 改为 NewSchemaless 合并,继承 resource.Default() 的 SDK 内置
schema URL,不再硬编码 semconv 版本路径。
2026-06-20 11:20:01 +08:00
ryan 280bb63cbd chore(release): bump version to v1.3.1
### 🛠 修复
- 修复了风控中间件测试在 ClickHouse 批写架构迁移后无法正确初始化的问题。

### ⚡️ 优化与改进
- 新增 ClickHouse 独立 OLAP 管线,以 goose 迁移作为唯一 schema 来源,并抽取 analytics repository 统一访问日志读写。
- 新增通用 ClickHouse batchwriter 批量写入框架,支持各业务域独立缓冲 flush 管道与默认批处理调优。
- 风控访问日志与管理端日志查询改为经 repository 层批写与查询,移除内联 SQL 与手动 DDL 维护路径。
- OAuth Access Token 与会话校验引入 RAM+Redis 缓存,降低高频鉴权路径的数据库读取压力。
- 上传元数据、Auth Source 与系统配置批量读取接入三层缓存(RAM→Redis→DB),并通过 pub/sub 支持多节点失效同步。
- 上传统计增量更新收敛为单事务写入,减少 ingest/remove 路径的锁竞争与统计偏差风险。
- ClickHouse 迁移与连接初始化增加进程隔离,避免与主库迁移互相阻塞。
- 引入 lifecycle 优雅停机钩子,确保进程退出前 flush 批写缓冲与释放资源。

### 💄 其他/体验
- 新增 cache-framework 与 clickhouse-batchwriter 开发技能,并更新 AGENTS.md Skill 关联索引。
- 登录与 Token 校验路径增加缓存预热,缩短冷启动后首次鉴权延迟。
2026-06-20 10:51:27 +08:00
ryan f52c8db21a perf(cache): 三层缓存框架补强
- 新增 cache-framework skill,规范 RAM→Redis→DB 读路径、失效与 pub/sub
- 上传元数据 Otter+Redis 缓存与多节点失效;Auth Source 缓存与 pub/sub
- ListSystemConfigsByKeys 补 Redis 层;上传统计单事务;登录/Token 缓存预热
- cleanup 任务补 upload meta 失效钩子
2026-06-20 10:20:35 +08:00
ryan d490030b75 fix: test 2026-06-20 09:58:37 +08:00
ryan 200525a1ab perf: refactor 2026-06-20 09:54:48 +08:00
ryan cac6e88bc0 perf: refactor 2026-06-20 09:46:29 +08:00
ryan 080be1e03a perf: access token cache 2026-06-20 09:46:29 +08:00
ryan d0a9958711 perf: clickhouse isolation 2026-06-19 21:31:03 +08:00
ryan a98d266278 feat(db): add ClickHouse batchwriter framework and skill
Introduce internal/db/batchwriter as a reusable generic buffered writer
for per-domain ClickHouse flush pipelines, with unit tests and default
batch tuning aligned with audit log ingestion.

Add clickhouse-batchwriter agent skill and cross-references in AGENTS.md
and database-migration. Business layers are not wired yet.
2026-06-19 20:59:07 +08:00
ryan 00593742c3 feat(clickhouse): integrate goose migrations and analytics repository
Add a separate ClickHouse OLAP pipeline with goose/clickhouse DDL as the
sole schema source, model/analytics for ORM mapping, and repository/analytics
for reads (ChDB/GORM) and batch writes (ChConn). Refactor risk_control and
admin/logs to use the repository layer instead of inline SQL. Remove the
manual support-files DDL and document the workflow in database-migration skill.
2026-06-19 12:04:22 +08:00
ryan 13e9fead35 chore(release): bump version to v1.3.0
### 🛠 修复
- 修复了存储驱动切换保存后上传记录未同步指向新后端的问题。
- 修复了 S3 不可达时无法暂存本地存储配置的问题。
- 修复了文件软删除后增量统计未正确扣减的问题。
- 修复了 Snowflake ID 生成异常或出现负值时的不可靠行为,增加重试与集中错误处理。
- 修复了自更新流程中解压文件名不匹配导致更新失败的问题。
- 修复了用户列表按 ID 排序方向不正确的问题。
- 修复了前端服务层循环依赖导致构建失败的问题。

### ⚡️ 优化与改进
- 新增 upload.Ingest 程序化上传域服务,统一对象写入、上传记录与增量统计,支持秒传、Worker 摄取与镜像去重策略。
- 新增文件上传统计全量重建异步任务,可在管理端触发以修复历史统计偏差。
- 存储配置收敛为单一 storage_config 来源,移除逐条上传记录的 storage_driver 冗余字段。
- 抽取 repository 层并瘦身 HTTP Handler,统一 Abort 系列错误响应与链路追踪集成。
- 将进程级初始化从 router 迁至 bootstrap/cmd,任务与推送改为显式装配,消除 init 副作用。
- 认证与用户模块通过 listener 域事件解耦推送,避免核心业务直接依赖通知模块。
- 分离 user 模块 Handler 与 Logic 边界,便于 Worker 与单元测试复用业务逻辑。
- 引入系统配置内存缓存与验证码运行时配置快照,降低高频配置读取的数据库压力。
- 优化上传热路径与管理端增量统计表,文件统计查询由全表扫描降为常数级读取。
- 前端拆分管理端 bundle、并行化鉴权与公共配置加载,并虚拟化日志列表以提升首屏与滚动性能。

### 💄 其他/体验
- 新增 file-upload 开发技能,并将 AGENTS.md Skill 索引整理为分类表格。
- 优化文件管理统计页在 Tab 切换与再次进入时的自动刷新,避免展示过期缓存数据。
- 优化异步任务日志的展示与清理体验。
- 前端管理组件就近归位并拆分服务层目录,提升代码可维护性。
2026-06-18 15:14:38 +08:00
ryan 83b5475d69 fix(frontend): refresh file storage stats on tab and page revisit
Set stats query staleTime to 0 with refetchOnMount always, and unmount
inactive file admin tabs so returning to the stats view triggers a new fetch
instead of serving the 30s React Query cache.
2026-06-18 15:13:09 +08:00
ryan 990e3a6f51 feat(upload): add rebuild stats async task 2026-06-18 15:05:52 +08:00
ryan 1cad0c5c55 feat(upload): add programmatic Ingest service and file-upload skill
Introduce upload/ingest as the single domain entry for storing files,
writing w_uploads records, and maintaining incremental stats. Refactor
HTTP UploadFile and delete handlers to delegate to ingest, fix stats
decrement ordering on Remove, and document usage in the file-upload skill.
2026-06-18 14:57:47 +08:00
ryan 9c6c697d95 fix(user): user id asc 2026-06-18 14:00:21 +08:00
ryan 410ff14795 refactor(storage): drop per-upload storage_driver, use storage_config as single source
Remove w_uploads.storage_driver and route all read/write/delete paths through
storage.Active() backed by storage_config.driver. Block direct driver switches
when uploads exist; require migration task instead. Simplify migration to
cursor-based file_path iteration without per-row driver updates.
2026-06-18 13:56:31 +08:00
ryan 1f391e9ec2 fix(storage): apply driver switch on save and repoint upload records
Saving storage settings now activates the selected driver immediately
instead of staging it until migration. When the driver changes, existing
upload rows are repointed to the new storage_driver so /f/{id} reads use
the correct backend. ForDriver can also open non-active drivers from the
saved multi-backend config.
2026-06-18 13:40:29 +08:00
ryan 6fa7034172 update AGENTS.md 2026-06-18 13:32:44 +08:00
ryan e766066a75 fix(storage): allow staging local config when S3 is unreachable
When switching storage drivers in admin settings, save now validates
connectivity against the selected target backend instead of retesting
the still-active driver. The active driver remains unchanged until
migration completes, so unreachable MinIO no longer blocks saving local
storage settings.
2026-06-18 13:28:56 +08:00
ryan 2648f3f3b7 merge main into repository-context-enabled-background 2026-06-18 12:13:45 +08:00
ryan 1b2e083aec refactor(api): extract repository layer and thin HTTP handlers
Introduce internal/repository for data access and cache-backed system
config reads. Move business logic into logics.go across admin push,
user, template, cache, system_config, and upload/handler packages.

Remove Gin from internal/util by relocating request-scoped helpers to
oauth/gin_context.go. Propagate request context for config lookups in
user flows. Slim model entities and delete model-level DB/cache helpers.

Wire handlers to logics/repository so targeted packages no longer call
db.DB directly. Update admin router tests to use ErrorHandlerMiddleware.
2026-06-18 12:12:49 +08:00
ryan dd991909af test(response): fix AbortWithError router tests and oauth/bootstrap reliability
- Add middleware_test.go covering ErrorHandlerMiddleware and Abort helpers
- Switch router test setups to testhelper.NewTestGinEngine for error JSON
- Fix OAuth provider cache to use mock HTTP client and normalize issuer URLs
- Add ResetInitRuntimeOnceForTest to make bootstrap tests hermetic under -count
- Update admin/task test imports for upload/task package move
2026-06-18 12:05:56 +08:00
ryan e5b3a60f73 merge main into handler-model-logics-user 2026-06-18 10:55:18 +08:00
ryan beae8d2dd9 refactor(frontend): colocate admin components and split service layer
Move route-specific admin components from components/common/admin into
app/(main)/admin/<feature>/components/. Split AdminService god object into
domain services, fix UploadService to use BaseService, add AdminUploadService,
consolidate DB export into DbManageService, and migrate consumers to the
unified services entry.
2026-06-18 10:55:14 +08:00
ryan 3d25a377cb merge: integrate http-handler-abort-error architecture refactor
Resolve user package conflicts by keeping main's service-layer logics and
applying Abort* error handling in routers. Align oauth callback with
listener.EmitAdminLoggedIn from main.
2026-06-18 10:53:03 +08:00
ryan 9af84c8ed6 refactor(api): unify error handling and split upload/oauth god modules
Replace c.JSON(200, response.Err) and middleware gin.H bypasses with
response.Abort* helpers so errors flow through Gin Error chain and
ErrorHandlerMiddleware for OTel trace correlation.

Split oauth/sources.go into domain-focused files and decompose upload
into handler/filesrv/stats/task/cache/storage/util subpackages with a
root facade preserving existing import paths.
2026-06-18 10:51:25 +08:00
ryan 13f823e5e3 test(bootstrap): fix review findings and sync architecture docs
- Register tasks in admin/task test setup after init() removal
- Strengthen bootstrap test: RegisterPushDomainEvents before Init
- Add admin_login auth→push listener integration test
- Update AGENTS.md and push/new-async-task/new-api skills for
  bootstrap composition root, listener domain events, and explicit
  test wiring conventions introduced since 50c45db5
2026-06-18 10:50:45 +08:00
ryan dac1c979d1 chore: remove accidental .grok hook files from merge 2026-06-18 10:39:37 +08:00
ryan de8a21a49f merge main: resolve bootstrap and router init conflicts 2026-06-18 10:38:55 +08:00
ryan 03fa5d948d refactor(bootstrap): move runtime init from router to cmd layer
Extract SyncEvents and InitLogWriter from router.Serve into bootstrap.Init
called from cmd entry points with trace-aware context. Preserve existing
Register* wiring for task and push domain integrations.
2026-06-18 10:38:08 +08:00
ryan fcf17db2e7 merge: replace init registration with bootstrap wiring 2026-06-18 10:34:57 +08:00
ryan 22c2ad5c73 refactor(task): replace init registration with bootstrap wiring
Introduce internal/bootstrap as the composition root with sync.Once
guards for task handler registration and push listener wiring. Replace
the single OnTaskCompleted global hook with multi-subscriber handlers
and remove init()-driven side effects from worker, admin task, and push.
2026-06-18 10:34:49 +08:00
ryan 1135347a96 merge: decouple auth from push via domain events 2026-06-18 10:31:14 +08:00
ryan b659f47b62 merge: refactor(user) separate Handler and Logic layer boundaries 2026-06-18 10:31:12 +08:00
ryan 6066eb114b refactor(auth): decouple auth from push via domain events
Introduce internal/listener as a domain event bus so oauth and user
modules emit AdminLoggedIn without depending on admin/push. Register
push handlers explicitly at the router composition root, replacing
init() side-effect registration and blank imports.
2026-06-18 10:31:11 +08:00
ryan 50c45db561 refactor(user): separate Handler and Logic layer boundaries
Move HTTP handlers out of logics.go and replace gin.Context-coupled
login email verification with context-only processLoginEmailVerification.
Add logics_test.go for pure business logic unit tests.
2026-06-18 10:31:09 +08:00
ryan eb99628cd6 dmux 2026-06-18 10:29:48 +08:00
ryan f826807cf1 feat(task): clean task log 2026-06-17 14:29:38 +08:00
ryan 333e45572a fix(frontend): optimize 2026-06-17 13:58:32 +08:00
ryan 3a05e7f09c refactor(idgen): centralize negative ID handling via panic
Restore NextUint64ID() to uint64-only API so callers need no error
checks. Retry logic stays in idgen; after 3 negative values it panics
instead of returning 0, preventing silent NULL primary keys.
2026-06-17 13:14:17 +08:00
ryan 92dc6a59c0 fix(idgen): retry snowflake generation and error on negative ID
NextUint64ID now retries up to 3 times when Int64() is negative, then
returns an error instead of 0 or Fatalf. All call sites propagate the
error to avoid GORM omitting zero-value primary keys.
2026-06-17 13:11:16 +08:00
ryan a27113feb2 fix(frontend): break service layer circular imports for build
Point service implementations at @/lib/services/core for BaseService
instead of the barrel index, fixing static export prerender failure on
ConfigService initialization.
2026-06-17 12:17:05 +08:00
ryan a44043262e perf(frontend): split admin bundles and tighten data fetching
Add next/dynamic lazy loading for database, logs, and settings heavy
modules; migrate access-logs and task-executions to React Query; parallelize
login auth-sources with public config; consolidate users table tooltips;
enable lazy image decoding; and replace barrel @/lib/services imports with
direct module paths.
2026-06-17 12:12:47 +08:00
ryan 9550fa6ff3 perf(cap): add runtime settings snapshot for dynamic config
Replace per-request GetByKey calls with a singleflight-backed
RuntimeSettings snapshot loaded via ListSystemConfigsByKeys.
Invalidate snapshot on admin cap_* writes and via Redis pub/sub.
Simplify VerifyMiddleware and update PERFORMANCE.md status.
2026-06-17 12:07:02 +08:00
ryan d3ba767087 perf(config): add Otter RAM cache layer for system configs
Introduce pkg/cache/ram on top of existing Redis/DB config reads.
GetByKey now checks local RAM before Redis. Unified invalidation clears
RAM and Redis hash fields on admin writes, with pub/sub for multi-node
RAM eviction. Add tests and wire create/update/migrator write paths.
2026-06-17 11:57:04 +08:00
ryan ca21e0eca7 perf(frontend,config): parallelize auth, virtualize logs, cache public configs
Frontend layout renders immediately; pages gate via RequireAuth and
useAuthRedirect. Login/register skip getUserInfo. Admin log panel uses
useVirtualizer to avoid 2000-row DOM. ListVisibleSystemConfigs caches
visibility=1 configs in Redis with invalidation on create/update.
PERFORMANCE.md marks completed optimizations.
2026-06-17 11:41:24 +08:00
ryan 6a8f9a7aea perf(upload): optimize file hot paths and incremental admin stats
- Use RWMutex for disk cache reads and singleflight for WebP cache misses
- Cache migration read-only state and file access whitelist with pub/sub invalidation
- Add w_upload_stats incremental counters updated on upload/delete
- Add w_uploads composite indexes and goose backfill migrations
- Document performance analysis in docs/PERFORMANCE.md
- Fix RegisterCustomRoutes to accept apiV1Router parameter
2026-06-17 11:33:48 +08:00
ryan e057cd5de5 fix(update): extracted filename mismatch 2026-06-16 10:51:58 +08:00
ryan 82155b8599 chore(release): bump version to v1.2.0
### 🛠 修复
- 修复了异步任务列表在成功状态下允许重试、失败状态下无重试按钮的问题。
- 修复了启用推送事件时,在无可用推送渠道配置下仍尝试触发并阻塞的问题。
- 修复了 WebDAV 存储驱动在 Put/Get/Delete 等操作中由于丢弃 context.Context 导致 HTTP 链路追踪断裂(生成无源 Root Span)的问题。
- 修复了结构化日志在没有 active span 时仍强制打印全零 traceID/spanID 产生的日志冗余噪音。

### ⚡️ 优化与改进
- 实现了全新的系统通知推送机制,支持 Telegram Bot、Lark 机器人及自定义 Webhook 等多种推送渠道。
- 优化了路由结构设计,按照 V1 分类与业务模块实现扁平化的路由解耦。
- 引入了全局 OpenTelemetry 链路追踪(Tracer)框架,集成 Gin, GORM 与 Redis 自动化耗时度量,并补充了统一的全局错误处理中间件。
- 优化了采样器命名,将 ParentBasedErrorAwareSampler 重命名为更契合其真实机制的 ParentBasedRatioSampler。
- 重构并统一了项目架构为基于 Feature 的功能模块化结构,将 internal/util/ 拆分得更加纯净,优化了验证码等公共库提取(pkg/cap)。
- 实现了推送事件与自定义通道的 Redis 缓存机制,极大降低了推送触发时的高频 DB 查询压力。

### 💄 其他/体验
- 优化了前端界面布局,同步系统菜单与侧边栏配置显示。
- 优化了前端自定义通道表格的布局与样式,使其与事件管理 Tab 页面保持一致。
- 更新了项目开发技能手册(Skills),包括新增接口路由规范(new-api)和异步任务开发指南(new-async-task)。
2026-06-16 10:39:22 +08:00
ryan f129a9cfca feat(api): implement global error handler middleware and trace integration
- Introduce APIError type and AbortWithError helper in response package
- Implement errorHandlerMiddleware to record Go errors to Otel Spans and format JSON response
- Register errorHandlerMiddleware globally in router
- Refactor logs analytics handler to use the new unified error pattern
2026-06-16 10:24:14 +08:00
ryan 9fa38dcfdc feat(push): implement Redis caching for push events and custom channels
- Implement cached queries in GetActivePushEventByKey and GetActivePushChannelByName.
- Set TTL for cached items to 24 hours via activePushEventCacheTTL and activePushChannelCacheTTL constants.
- Implement GORM hooks (AfterSave and AfterDelete) on PushEvent and PushChannel to auto-evict Redis caches, guaranteeing cache consistency.
- Evict Redis caches manually inside API handlers for Create/Update/Delete/Toggle event/channel endpoints.
- Update events.go to query models through the new caching methods.
2026-06-16 10:19:06 +08:00
ryan 9515f0811b chore: fix release ci 2026-06-16 10:08:21 +08:00
ryan c6df73e522 tracer 2026-06-16 09:58:15 +08:00
ryan bfa00ae6ac feat(push): validate push channels presence before enabling push event
- Reject toggling an event to enabled in ToggleEvent handler if channels list is empty.
- Add model-level validation in PushEvent.Validate() to prevent enabling events without channels during creation/updates.
- Update TestPushRouters/toggle_event_status unit test to cover this verification.
2026-06-16 08:59:03 +08:00
ryan 6a913701b9 refactor(push): remove legacy push_global_token setting
- Clean up redundant push_global_token INSERT and DELETE statements from 202606140004 migration.
- Add DELETE for push_global_token in 202606160001 migration to wipe out legacy settings on update.
- Adjust expected w_system_configs count in migrator_test.go to 30.
2026-06-16 08:57:35 +08:00
ryan ff60f1c699 refactor(push): clean up legacy push_config and w_push_events hardcoded insertions
- Remove ConfigKeyPushConfig and delete legacy push_config query logic from EventTrigger.Trigger.
- Simplify the push event notification dispatching engine to rely purely on database custom channels.
- Remove w_push_events hardcoded INSERT statement from migration files, letting SyncEvents handle default event registration.
- Rewrite unit tests to use model.PushChannel instead of push_config.
2026-06-16 08:54:32 +08:00
ryan e50e600bc8 docs(skill): update router packaging and categorization guidelines in documentation
- Updated `new-api` skill `SKILL.md` to document the centralized v1.go route registration and domain-driven sub-routing patterns
- Updated `AGENTS.md` instructions with the new router subpackages (`root`, `v1`) and route file layout guidelines
2026-06-16 00:00:53 +08:00
ryan 0adc5e8189 refactor(router): organize root routes and centralize v1 route registration
- Created `internal/router/root` package to register root-level paths
- Moved files serving, robots.txt, and Swagger docs to `root/default.go`
- Registered custom root routes under `root/custom.go`
- Centralized all v1 routes registration in `internal/router/v1/v1.go`
- Simplified `internal/router/router.go` by delegating route registration
- Updated `new-api` skill `SKILL.md` to document the new router structure
2026-06-15 23:42:48 +08:00
ryan dfcbdfe47c docs(skill): update new-api guidelines to reflect router v1 structure
- Updated package structure diagram and routing instructions to point to the new `internal/router/v1` package
- Changed custom routes registration guide to `internal/router/v1/custom.go`
2026-06-15 23:30:45 +08:00
ryan 5016103774 refactor(router): split route registration into v1 package categorizations
- Created `internal/router/v1` subpackage
- Split routes into `admin.go`, `user.go`, `public.go`, and `custom.go`
- Cleaned up imports and helper registration functions from `router.go`
- Removed obsolete `internal/router/custom.go`
2026-06-15 23:30:16 +08:00
ryan 363d2cb4cc refactor(router): merge frontend.go and frontend_embedded.go into one file
- Declared package-level `registerFrontend` variable in `router.go` as a no-op fallback
- Configured `frontend.go` under `embed_frontend` build tag to override `registerFrontend` on package initialization
- Removed separate `frontend_embedded.go`
- Resolved related revive linter warnings
2026-06-15 23:24:09 +08:00
ryan 34de1639f5 feat(frontend): sync menu display config items with sidebar layout
- Added missing routes `/files` and `/admin/push` to menu display management
- Standardized icons, labels and descriptions to match the sidebar layout
- Declared MenuItem/MenuGroup interfaces for type safety
2026-06-15 23:18:04 +08:00
ryan 7f00ed6a65 fix(task): fix manual retry button visibility and validation
- Only show the manual retry button for failed tasks (status === "failed") on the frontend.
- Remove the maximum retry limit validation for manual retries in the task executor.
- Remove obsolete test cases verifying maximum retry limit for manual retries.
2026-06-15 23:10:36 +08:00
ryan 3e8e879661 docs(architecture): update architectural guidelines to reflect feature-based service design
- Updated AGENTS.md, new-api SKILL.md, and new-async-task SKILL.md to remove references to the deleted global internal/service/ package.
- Documented feature-based local logics/services design within internal/apps/<module>/.
- Updated package comments in internal/diskcache to point to pkg/cache/disk.
2026-06-15 17:24:44 +08:00
ryan 239711cea7 refactor(service): adopt feature-based architecture and rename pkg/diskcache
- Moved GORM/Redis CAPTCHA manager from internal/service/cap directly into the cohesive CAPTCHA app folder at internal/apps/cap/.
- Moved background system cleanup handler from internal/service/cleanup.go into internal/apps/upload/cleanup.go.
- Completely removed the global internal/service directory to keep module logic self-contained.
- Renamed the core utility engine pkg/diskcache to pkg/cache/disk to separate underlying utility code from db/config integrations.
- Renamed DiskCache struct in pkg/cache/disk to Cache to resolve revive package-name stuttering warning.
- Regenerated Swagger API documentation and confirmed all tests compile and pass with 0 linter issues.
2026-06-15 16:45:26 +08:00
ryan 953af7d8db refactor(util): move response helper to common/response and session logic to oauth
- Relocated generic HTTP response helpers (Response, OK, Err, etc.) from internal/util/ to a dedicated internal/common/response/ package.
- Renamed ResponseAny to Any to resolve revive stuttering warnings.
- Moved session building options and cookie headers logic from internal/util/ to internal/apps/oauth/.
- Removed all direct imports of Gin/Sessions/HTTP frameworks from internal/util/ to keep general utilities 100% pure.
- Regenerated Swagger API documentation via make swagger.
- All tests and make code-check compile and pass with 0 issues.
2026-06-15 16:39:55 +08:00
ryan b3ed94342c refactor(backend): extract to pkg/cap 2026-06-15 16:39:55 +08:00
ryan 84ae4ec27e refactor(frontend): optimization 2026-06-15 16:39:55 +08:00
ryan 1425fd1dbb feat(frontend): align custom channels table layout and styling with events tab 2026-06-15 16:39:55 +08:00
ryan 2a3b9f8a5f feat(push-task): connect notification module with task module
- Add task_type to w_push_events table and GORM models.
- Implement OnTaskCompleted callback hook in task executor to avoid circular dependencies.
- Implement task listener in push package to trigger notifications on task completion.
- Automatically resolve User objects from payload and results.
- Enhance UI to select task completed events and preview default templates.
- Update Swagger documentation.
2026-06-15 16:39:55 +08:00
ryan de58b118b4 feat(push): add telegram bot push notification channel
Implement TelegramPusher in pkg/push, register config schema in channels_definition.go, add validation in model/push_channel.go, update task routing, and update settings-tab.tsx UI validation.
2026-06-15 15:00:49 +08:00
ryan cb018b3b60 feat(push): implement system notification and push framework 2026-06-14 23:07:08 +08:00
ryan aee457093d chore(release): v1.1.0
### 🛠 修复
- 修复了 CAPTCHA 验证的逻辑和安全性。

### ⚡️ 优化与改进
- 新增了动态存储配置与多后端迁移机制。
- 新增了手动触发存储迁移的 Web GUI 操作界面。
- 实现了存储迁移的并发处理(使用 Group)与上传后 SHA-256 完整性自动校验机制。
- 实现了基于 Redis 的分布式锁与集群多节点缓存失效广播机制,防止迁移任务冲突。
- 实现了 Redis 锁续期守护协程 (watchdog) 防止超长迁移任务锁过期。
- 优化了更新存储配置时的连通性自动校验机制,防止配置错误。
- 优化了存储配置的内存缓存机制,并引入了全局共享连接池以提高 TCP 复用率。
- 优化了上传前已有同名文件的比对逻辑,跳过下载阶段,仅比对 Content-Length 以实现零网络流量跳过。
- 新增了普通用户的独立文件管理 Dashboard 与控制接口。
- 优化了文件管理接口命名空间,将全局管理迁移至管理员级 API Namespace 隔离控制。
- 优化了管理员新建用户接口,增加了邮箱(Email)字段的必填要求。

### 💄 其他/体验
- 重构并优化了文件管理页面,引入了多标签页多维度统计数据大屏。
- 基于 shadcn 原生 UI 组件重构并优化了文件详情和仪表盘组件。
- 抽取后端核心路由注册流程,降低主路由文件圈复杂度,使路由配置更易维护。
2026-06-13 17:13:36 +08:00
ryan 5698169f20 refactor(router): extract route registration into helper methods 2026-06-13 16:29:27 +08:00
ryan 922f764241 feat(user): require email when admin creates a user
- Add `email` as a required field in `createUserRequest`
- Enforce email format verification and database uniqueness checks in the admin user creation handler
- Update the admin user creation frontend modal with validation and form field
- Update the corresponding backend unit tests and regenerate Swagger docs
2026-06-13 16:23:07 +08:00
ryan 5b13d5b464 feat(storage): implement user-group level file management dashboard and APIs
- Expose user-scoped CRUD APIs under `/api/v1/upload` (my files query, stats, rename, delete)
- Update backend handlers and routers with ownership validation checks
- Create a dedicated frontend personal file manager card-list and upload button under `/files`
- Add comprehensive backend test coverage and update API docs
2026-06-13 16:16:43 +08:00
ryan 8b19ffed90 refactor(storage): move file management routes from user to admin namespace
- Remove file list, stats, download, and deletion routes from '/api/v1/upload'
- Move these endpoints under '/api/v1/admin/uploads'
- Remove user-specific filtering from files query and statistics to aggregate system-wide uploads by default
- Allow admins to bypass ownership check when downloading private files
- Update backend unit tests, Swagger documentation, and frontend service client and components
2026-06-13 16:04:21 +08:00
ryan dbabe8b8d7 feat(system_config): validate storage configuration connectivity on update
- Add a live test connectivity check in UpdateSystemConfig before saving the storage configuration.
- Merge masked placeholder secrets from current configuration prior to testing and database storage.
- Extract validation and test logic to validateAndMergeStorageConfig helper to satisfy cyclomatic complexity.
- Add TestUpdateStorageConfigValidation covering successful updates and failed checks.
2026-06-13 15:57:41 +08:00
ryan 1d37242a8b refactor(storage): update Backend.Put to return PutResult and encapsulate bucket mapping
- Update Backend.Put method signature in storage.go to return (PutResult, error).
- Adjust all backend implementations (local, oss, s3, webdav) to return a PutResult enclosing Key and Bucket.
- Refactor storeUploadFile in upload routers.go to extract key/bucket from PutResult, eliminating manual config bucket lookups.
- Remove the unused cfgBucket helper from storage_ops.go.
- Adjust storage_migration_task.go and tests to accommodate the updated method signature.
2026-06-13 15:48:41 +08:00
ryan ce9423f877 feat(upload): implement Redis lock renewal watchdog for storage migration
- Add a watchdog goroutine that periodically extends the Redis lock TTL every 10 minutes to prevent premature lease expiration for long-running migrations.
- Use context.Background with timeout contexts for lock renewal and final deletion to prevent parent context cancellation from aborting lock cleanup.
- Add nolint directives for gosec and contextcheck and define constants for durations to satisfy strict code quality gates.
2026-06-13 15:31:39 +08:00
ryan 66e0a69666 feat(frontend): support manual storage migration triggering
- Modify storage-config-tab.tsx to separate storage configuration saving from task dispatching.
- Save operations now always preserve the currently active storage driver to prevent premature storage engine switching.
- Add a manual '开始迁移' button to trigger storage migration explicitly.
- Display a warning notice to the operator when the selected storage type differs from the active one.
2026-06-13 15:31:39 +08:00
ryan 26a1e71a27 feat(storage): implement Redis distributed lock and cache invalidation broadcasting
- Add Redis distributed lock in Execute of MigrationHandler to prevent concurrent storage migrations.
- Define storage:config_invalidation Redis pub/sub channel to broadcast cache invalidation events.
- Implement background pub/sub listener on all nodes to evict config memory cache concurrently.
- Add integration tests for distributed locking and invalidation propagation using miniredis.
2026-06-13 15:31:39 +08:00
ryan acd430836f feat(upload): implement parallel storage migration and integrity check
- Parallelize storage migration using `errgroup` with a concurrency limit of 10.
- Perform post-copy SHA-256 data integrity validation to prevent silent data corruption.
- Add test case verifying migration with both incorrect and correct hashes.
2026-06-13 15:31:39 +08:00
ryan bc18800b58 perf(storage): add config caching and shared HTTP connection pool
- Implement thread-safe local config caching with a 5s TTL check.
- Reuse backend client singletons in storage.Active and storage.ForDriver.
- Reset in-memory config cache on configuration saves and updates.
- Create internal/httppool package to manage shared HTTP transports.
- Configure WebDAV client and CDN retrieval to reuse the shared pool.
- Add unit tests for httppool and storage caching behaviors.
2026-06-13 15:31:39 +08:00
ryan 9a18bea324 feat(storage): add dynamic storage config and migration
Move storage backend configuration from startup YAML to system_config-backed runtime configuration. Add local, S3-compatible, R2, MinIO, OSS, and WebDAV backend support.

Add a storage migration async task using the existing task dispatch framework. Migration target config is carried in task payload, and maintenance mode is derived from task execution state.

Split upload file management and storage operations, add the admin storage configuration tab, and update migrations and Swagger docs.
2026-06-13 15:31:38 +08:00
ryan 4bf8a4806e refactor(frontend): split files coordinator into separate tab components 2026-06-13 12:53:15 +08:00
ryan 9453af2aa0 refactor(frontend): style files stats tab with native shadcn
refactor(frontend): style files stats tab with native shadcn charts
2026-06-13 12:49:29 +08:00
ryan b262880189 refactor(auth): improve session security, CAPTCHA validation and code hygiene
- Integrate CapWidget with dual-scope capability on the frontend and protect registration/send-email-code endpoints on the backend.
- Set session cookie SameSite mode to Lax.
- Propagate request context through auth source database operations and optimize username uniqueness validation.
- Standardize local error naming to camelCase and resolve references.
- Fix linter rules, missing SheetContent closing tag, and unit tests.
2026-06-13 12:33:01 +08:00
ryan 04280a7b11 feat(upload): refactor file management with statistics and multi-tab layout 2026-06-13 12:32:42 +08:00
ryan 40e83a832f add(skill): code review 2026-06-13 11:33:47 +08:00
ryan e5c661f957 add(skill): code review 2026-06-13 11:28:17 +08:00
ryan 49bd850c8b fix(frontend): repair login session flow
Resolve login-page 401 hangs and redirect races by relying on the shared user state. Keep protected-route redirects intact, clean pending requests without unhandled rejections, and allow the dynamic icon route through the page proxy.
2026-06-13 11:27:30 +08:00
ryan 9a6bb04bf5 fix(frontend): distinguish initial authentication state on login page mount
- Add wasUserPresentRef to detect if the user was already authenticated on initial page load.
- Guard the useEffect redirect block so that it only redirects automatically if the user was already authenticated when mounting.
- Prevent duplicate concurrent router.replace calls from canceling each other when logging in via the form.
2026-06-13 11:09:55 +08:00
ryan 0df4712831 fix(frontend): resolve login redirect loop and clean up info tab
- Prevent infinite session probe requests on the login page by guarding the check with the authenticated user state and using the Latest Ref pattern.
- Decouple useEffect from resolveRedirectTarget by using resolveRedirectTargetRef to avoid searchParams dependency loops.
- Remove the unused '服务连接' Card from the settings info tab and clean up unused imports, queries, and properties.
2026-06-13 11:07:27 +08:00
ryan 30aa89f686 chore(release): v1.0.2
### 🛠 修复
- 修复了修改密码时不会吊销现有会话和访问 Token 的问题,确保密码更改后,所有其它客户端会话和 Access Token 立即失效,防范被盗凭据的持续利用。
- 修复了 WebSocket 连接未校验 Origin 的问题,引入严格的 Origin 允许列表校验,防范跨源 WebSocket 劫持攻击 (CSWSH)。
- 修复了未配置服务地址时 CORS 中间件原样反射 Origin 的问题,严格限制允许跨域访问的源为精确配置的 Origin 列表,增强跨域请求安全性。
- 修复了已认证用户可以通过上传记录 ID 直接越权读取其他用户私有文件的问题,引入了基于文件所有权及权限模式的严格访问控制。
- 修复了 OIDC 策略强制执行不严以及未登录用户能够触发自动绑定导致账户接管的问题,增强了 OAuth 绑定过程中的策略校验。
- 修复了 OAuth 流程中 State 校验不严的问题,在 Session 中强制绑定 State 并在回调时进行一致性验证,防止 CSRF 和账号接管攻击。
- 修复了登录或认证重定向时未对 URL 目标域进行限制的问题,实现了重定向目标 URL 的安全净化与源校验,防范开放重定向与反射型 XSS 漏洞。
2026-06-13 10:31:54 +08:00
ryan 26e12594a2 fix(user): revoke all sessions and access tokens on password change
- Store user password hash in session during login

- Validate password hash compatibility on requests to prevent session reuse

- Revoke all user access tokens and clear session on ChangePassword
2026-06-13 10:27:28 +08:00
ryan eb999eba09 fix(logs): restrict websocket origin to prevent cswsh (LOG-2)
- Restrict WebSocket upgrade to same-origin or configured server_address allowed origins.
- Add comprehensive test suite in utils_test.go to verify origin matching rules.
2026-06-13 10:25:25 +08:00
ryan f6f8c25930 fix(router): restrict CORS origin reflection to allowed hosts (AUTH-ROUTE-5)
- Extract isOriginAllowed helper to match Origin against server_address configurations
- Ensure arbitrary origins are not reflected and credentials are not allowed when server_address is unconfigured or mismatched
- Trim trailing slashes from allowed origins configuration for robust matching
- Add TestCORSMiddleware to cover all CORS matching and rejection scenarios
2026-06-13 10:23:20 +08:00
ryan 7b379863b4 fix(upload): restrict cross-user private file access (UPLOAD-1)
- Add access_mode column to w_uploads table (0 = private, 1 = public) and initialize data in a single migration script
- Enforce strict ownership check for private files during download
- Allow public files to follow whitelisted public-access rules
- Default access_mode to public for avatars and private for generic uploads
- Update frontend service to support optional accessMode parameter
2026-06-13 10:13:41 +08:00
ryan 5412c385dc fix(oauth): remove pending oauth auto-binding and enforce oidc policies
- Complete removal of completePendingOAuthBinding logic to prevent unintended account takeovers (AUTH-ROUTE-1).
- Add strict OIDC policy checks (global switch and source active states) across authorization and callback paths (AUTH-POLICY-1).
- Fix OIDC test cases to properly clear the Redis-backed system config cache using composite keys.
2026-06-13 10:06:49 +08:00
ryan 895788974c fix(oauth): secure OAuth state session binding to prevent account takeover
Bind OAuth state payloads to the initiating session token and user ID.
Verifies session token hash continuity during callback, and validates that
the user ID completing the binding flow matches the user ID that initiated it.
2026-06-13 09:55:07 +08:00
ryan f48426dbf8 fix(frontend): sanitize redirect targets to prevent XSS/open redirect
Sanitize and validate redirect targets from callbackUrl parameter and sessionStorage in login, registration, and OAuth callback flows.
Introduced safeRedirectTarget helper which rejects protocol-relative URLs, non-relative schemes, control characters, backslashes, and encoding bypasses.
2026-06-13 09:51:30 +08:00
ryan 50d21b431b feat(valkey): migrate redis to valkey and fix updater custom prefix selection
Replace redis:7-alpine with valkey:8.0-alpine and configure MaintNotificationsConfig ModeDisabled to suppress handshake warnings on Valkey. Resolve updater bug by dynamically matching custom repository asset name prefixes like PixezSync.
2026-06-12 15:44:49 +08:00
ryan 3228574a8c 兼容包名 2026-06-12 15:32:47 +08:00
ryan 752101612e 打印日志 2026-06-12 15:17:45 +08:00
ryan c9642fb5e2 修复退出异常问题 2026-06-12 14:32:27 +08:00
ryan 179a23f1a0 MAKE 调整 2026-06-12 14:29:20 +08:00
ryan 55831efd44 界面优化 2026-06-12 14:17:53 +08:00
ryan c916f566d9 系统控制台完成更新 2026-06-12 14:12:03 +08:00
ryan 407c1edf74 更新指导 2026-06-12 13:42:17 +08:00
ryan 654d7fd646 修复导出编译问题 2026-06-12 13:10:39 +08:00
ryan 16604e5f86 设置增加站名配置 2026-06-12 11:57:30 +08:00
ryan 6a1f89936e 登录界面优化 2026-06-12 11:35:48 +08:00
ryan d9df78d2c7 邮箱注册要求 2026-06-12 10:55:24 +08:00
ryan 63e3ade7f4 登录注册分开 2026-06-12 10:27:03 +08:00
ryan 1fe5118029 登录状态记录 2026-06-12 10:15:29 +08:00
ryan 62fcd245f2 fix: 映射后台任务查询接口的 task_type 参数为 Asynq 任务名以解决类型过滤无数据问题 2026-06-11 23:28:50 +08:00
ryan 01b80ac376 优化CI 2026-06-11 20:32:42 +08:00
ryan 50533e1837 build: 优化 Docker/Workflow 构建,使用 Next.js 环境变量注入版本和构建时间,并移除对 package.json 的硬编码替换 2026-06-11 20:18:49 +08:00
ryan 7c125ccef2 fix: 修复任务执行记录列表按任务类型和状态过滤失效的 Bug 并更新 Swagger 2026-06-11 20:14:02 +08:00
ryan 46760d4286 缓存处理修补 2026-06-11 19:07:22 +08:00
ryan 5915b31519 图片预热任务 2026-06-11 17:49:08 +08:00
ryan 7da4b72d24 任务日志优化 2026-06-11 16:56:53 +08:00
ryan f14875a8de 图片缓存不过期 2026-06-11 15:57:31 +08:00
ryan 1eef5336e3 修复文件管理页面分页问题 2026-06-11 15:52:35 +08:00
ryan 533f783268 质量优化 2026-06-11 15:36:46 +08:00
ryan 6b93320404 接口参数调整 2026-06-11 15:32:44 +08:00
ryan ad97ca7df1 图片压缩调用缓存 2026-06-11 15:23:15 +08:00
ryan 0221d4de14 缓存框架 2026-06-11 15:12:09 +08:00
ryan e8e0326879 图片压缩 2026-06-11 14:58:17 +08:00
ryan 2a3a17b6fe 优化 2026-06-11 14:28:07 +08:00
ryan eb9f6023f0 文件管理权限控制 2026-06-11 14:17:36 +08:00
ryan 5e0de01c4b 文件管理权限控制 2026-06-11 14:09:32 +08:00
ryan 312bc7d4d5 框架表改名 w_{name} 2026-06-11 09:27:15 +08:00
ryan 786fe71778 优化定时任务 2026-06-11 09:20:28 +08:00
ryan b5a1707898 优化 2026-06-11 09:10:06 +08:00
ryan 616242fad8 去除 access_token 访问写库逻辑 2026-06-11 09:09:17 +08:00
ryan 6cbc368dc1 修复任务日志显示问题 2026-06-11 09:03:40 +08:00
ryan c1a1904a67 修复重复注册问题 2026-06-11 09:03:24 +08:00
ryan 3023d47eec 解耦任务框架与业务任务 2026-06-11 08:52:44 +08:00
ryan 5dedff1324 修复任务参数类型转换 2026-06-11 08:40:08 +08:00
ryan 8964054fd8 修复任务参数类型转换 2026-06-11 08:26:30 +08:00
ryan 5241055030 根据名称获取认证源(名称比较不区分大小写) 2026-06-11 08:06:46 +08:00
ryan 983228227e AccessToken 默认非管理员权限 2026-06-10 22:50:44 +08:00
ryan bff9e8811d 界面优化 2026-06-10 20:25:21 +08:00
ryan 3ebead0d15 界面优化 2026-06-10 20:22:10 +08:00
ryan e6a6182a0c 优化任务管理 2026-06-10 20:15:12 +08:00
ryan db163034c0 优化任务管理 2026-06-10 19:36:35 +08:00
ryan 69edb1252b 升级 nextjs 2026-06-10 15:18:01 +08:00
ryan 15b3c83625 更新示例 2026-06-10 15:11:42 +08:00
ryan 57944398e6 质量优化 2026-06-10 13:50:58 +08:00
ryan 3ed4dec4a3 db manager 2026-06-10 13:42:42 +08:00
ryan d05acd804f 精简 2026-06-10 11:33:33 +08:00
ryan df1961c42d 压缩 2026-06-09 22:00:01 +08:00
ryan 1b5d37a8e8 修复CI 2026-06-09 21:54:34 +08:00
ryan ebf644adb7 修复单位问题 2026-06-09 21:54:03 +08:00
ryan 7aeaa7b0d2 调整CI 2026-06-09 21:50:13 +08:00
ryan 32e4a32462 make build 2026-06-09 21:45:57 +08:00
ryan 05ef5ed7bd 数据导出 2026-06-09 21:18:58 +08:00
ryan 0ead6348f6 make bin 2026-06-09 21:01:13 +08:00
ryan 949c021d45 质量优化 2026-06-09 20:39:22 +08:00
ryan 1c76c7158a 质量优化 2026-06-09 20:38:14 +08:00
ryan 673061265c 质量优化 2026-06-09 20:28:05 +08:00
ryan 50f39a6983 修复创建账号逻辑 2026-06-09 20:07:25 +08:00
ryan 055005688a 升级数据库后更新缓存 2026-06-09 16:47:49 +08:00
ryan b1c161b255 skill 2026-06-09 16:47:49 +08:00
ryan 6b3c0217f0 goose 迁移 2026-06-09 16:47:49 +08:00
ryan 40e8a7cfa3 重构获取公共参数 2026-06-09 16:47:49 +08:00
ryan bff09241d3 skill 2026-06-09 16:47:49 +08:00
ryan 73b220de3c 用户详情 2026-06-09 16:47:49 +08:00
ryan 92664273ed async task skill 2026-06-09 16:47:49 +08:00
ryan d705f2ff64 skill 2026-06-09 16:47:49 +08:00
ryan 8801b3976c go dev skill
go dev skill

go dev skill

shadcn skill
2026-06-09 16:47:49 +08:00
ryan c6eea8111d fix(revive): rename unused parameters to _ for lint compliance 2026-06-09 15:03:13 +08:00
ryan e06f76436e refactor: extract magic numbers to named constants for mnd lint compliance 2026-06-09 13:44:29 +08:00
ryan b05d26c9c6 docs: add package and exported symbol comments for revive lint compliance 2026-06-09 13:42:06 +08:00
ryan 4ac9857fe8 代码质量优化 2026-06-09 12:28:12 +08:00
ryan f428839602 前端优化 2026-06-09 11:35:31 +08:00
ryan 31f0fb4ceb 规约 2026-06-09 11:26:57 +08:00
ryan e100441e8a 规约 2026-06-09 11:16:02 +08:00
ryan d7521dc49a 界面优化 2026-06-09 10:40:43 +08:00
ryan d4d214d074 界面优化 2026-06-09 10:39:59 +08:00
ryan 61bc569bd8 clickhouse 日志采集 2026-06-09 10:39:59 +08:00
ryan b41457553d 优化 2026-06-09 09:19:56 +08:00
ryan 7a62a78fad smtp 发件前验证配置 2026-06-09 09:01:14 +08:00
ryan 2ae55da52e seo 检索开关 2026-06-09 08:56:25 +08:00
ryan b0023787c5 fix 2026-06-09 08:51:53 +08:00
ryan 7579d3865f eslint 2026-06-09 08:19:38 +08:00
ryan d14d222ede env load 2026-06-08 23:14:57 +08:00
ryan ddda7c44ef docker-compose.yml 2026-06-08 23:14:57 +08:00
ryan 957116d983 修改ci 2026-06-08 21:13:18 +08:00
ryan 356b3df81e 融合模式启动 2026-06-08 21:12:55 +08:00
ryan db0d503bb4 ci 2026-06-08 20:59:00 +08:00
ryan 2cce8a3175 ci 2026-06-08 20:55:18 +08:00
ryan a998f02f2b ci 2026-06-08 20:47:48 +08:00
ryan 31253eb23d remove idea 2026-06-08 20:41:35 +08:00
ryan cd3d0c9f82 重构 2026-06-08 20:38:17 +08:00
ryan 02f458856d 用户优化 2026-06-08 20:38:17 +08:00
ryan d99bd5231a 安全加固 2026-06-08 20:38:17 +08:00
ryan 0f65202659 个人信息页面增强 2026-06-08 20:38:17 +08:00
ryan b96624a251 模板系统 2026-06-08 20:38:17 +08:00
ryan 3aff95d256 优化 2026-06-08 20:38:17 +08:00
ryan 85f91b1ed7 更新 license .github 2026-06-08 20:38:17 +08:00
ryan e3ef6c9d27 修改路径 2026-06-08 20:38:17 +08:00
ryan c1fbf73f7e 更新项目 2026-06-08 20:38:01 +08:00
ryan c6cc8c3305 logs 2026-06-08 20:38:01 +08:00
ryan 5c7c995a95 框架化 2026-06-08 20:38:01 +08:00
ryan b03d2d7ea6 改名 2026-06-08 20:38:01 +08:00
ryan f136c9abdb 邮箱 2026-06-08 20:38:01 +08:00
ryan 72c74803be 优化 2026-06-08 20:38:01 +08:00
ryan db68a130ce 嵌入与邮箱 2026-06-08 20:37:57 +08:00
ryan 62bd5d09d4 移除 risk 2026-06-08 20:37:57 +08:00
ryan 47c9bc53fa skill 2026-06-08 20:37:52 +08:00
ryan 474e3da3b7 代码优化
界面优化
2026-06-08 20:37:52 +08:00
ryan 72d72810b2 cap 与系统信息 2026-06-08 20:37:47 +08:00
ryan 9d0f9f0576 user 2026-06-08 20:37:40 +08:00
ryan 4b72419a96 去除默认OIDC 2026-06-08 20:37:40 +08:00
ryan 53a4c92176 设置界面优化 2026-06-08 20:37:40 +08:00
ryan 589ae08318 async task framework 2026-06-08 20:37:40 +08:00
ryan 70a13dc107 upload+accessKey 2026-06-08 20:37:40 +08:00
ryan 360a26f109 oauth 2026-06-08 20:34:28 +08:00
ryan 48d414e197 裁剪
swagger

移除 merchant

swagger

改造首页内容为通用后台管理系统定位

- 修改首页标题从 'LINUX DO Credit' 改为 'Modern Platform'
- 更新副标题为 '为二次开发而生'
- 更新首页描述为通用平台的特点
- 更新首页特性标签为 '开箱即用、高度可扩展、工业级基建'
- 修改展示卡片为技术栈和二次开发相关
- 更新开发者示例代码为通用的注册和 API Key 获取示例
- 更新页脚品牌名为 'Modern Platform'
- 调整页脚导航链接为通用平台相关内容

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

去除遗留

裁剪
移除 /api/v1/user/pay-key 相关代码

- 删除后端 UpdatePayKey 处理器函数和 UpdatePayKeyRequest 结构体
- 删除 User 模型中的 PayKey 字段
- 删除 User.VerifyPayKey 方法
- 删除 EncryptPayKeyFailed 错误常量
- 删除 /api/v1/user/pay-key PUT 路由
- 删除 OAuth 返回中的 IsPayKey 字段
- 删除前端 UserService.updatePayKey 方法
- 删除前端所有支付密钥 UI 和逻辑
- 更新相关的导出和注释

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

去除遗留

api 修正

系统配置

前端裁剪

后端裁剪

init
2026-06-08 20:34:28 +08:00
ryan 8a782525de 压缩历史至 95081aff 2026-06-08 20:34:27 +08:00
965 changed files with 4055 additions and 195674 deletions
@@ -70,15 +70,15 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush
1. **Model**:在 `internal/model/analytics/` 定义 struct 与 `BatchInsertSQL()`(列顺序与 goose DDL 一致)。
2. **Goose DDL**:在 `internal/db/migrator/goose/clickhouse/` 新增迁移(见 `database-migration`)。
3. **Repository**:实现 `BatchInsertX(ctx, []analyticsmodel.X) error`:
- `len(items)==0` 直接返回
- `db.ChConn == nil` 返回明确错误
- 一次 `PrepareBatch` → 循环 `Append` → 一次 `Send`
- `len(items)==0` 直接返回
- `db.ChConn == nil` 返回明确错误
- 一次 `PrepareBatch` → 循环 `Append` → 一次 `Send`
4. **Writer 胶水**(`internal/apps/<domain>/` 或 `internal/repository/analytics/<domain>_writer.go`):
- `New` + `Start`,并在初始化逻辑内通过 `lifecycle.OnShutdown("your_writer_name", Stop)` 注册停机回调
- 业务路径 `TryEnqueue`;HTTP 背压用 `IsFull()`
5. **测试**:
- repository:mock `ChConn` 验证 `BatchInsertSQL` 与 append 列数
- batchwriter:`go test ./internal/db/batchwriter`
- repository:mock `ChConn` 验证 `BatchInsertSQL` 与 append 列数
- batchwriter:`go test ./internal/db/batchwriter`
6. 运行 `make code-check`;有 API 变更时 `make swagger`。
## 背压与丢弃策略
+1 -1
View File
@@ -92,7 +92,7 @@ ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独
| `internal/db/migrator/goose/clickhouse/` | **唯一** ClickHouse DDL 来源(goose SQL,嵌入二进制) |
| `internal/model/analytics/` | 分析表 Go model,列名须与 goose DDL 一致 |
| `internal/repository/analytics/` | 所有 ClickHouse 读写(批量写入、查询、聚合) |
| `internal/db/clickhouse.go` | 连接初始化(`ChConn` 原生批量、`ChDB` GORM 查询) |
| `internal/db/clickhouse.go` | 连接初始化(`ChConn` 原生批量、`chDB` GORM 查询) |
### 迁移入口与版本表
-37
View File
@@ -1,37 +0,0 @@
---
name: plan
description: 项目级技能:规定在开启新方案、新计划或进行任务交接时,必须将计划落库到 docs/plan 文件夹中并使用对应模板。
---
# Plan & Handover Skill
当你在当前项目中被要求“开启一个新的方案”、“制定开发计划”或者准备“任务交接(Handover)”时,你**必须**遵循本技能的工作流,将计划或方案落库到 `docs/plan/` 目录下。
> [!IMPORTANT]
> **什么时候应当创建实现计划?**
> * **必须创建的场景**:新功能开发、涉及多组件的重大架构重构、引入新基础设施依赖,以及存在显著设计决策冲突的**中大型、复杂**需求。
> * **绝对不要创建的场景**:改个包名、挪个文件、重命名函数、小修小改修复 Bug 等**轻量级、简单的局部重构**。对于此类改动,应当直接完成并运行单元测试通过后交付,禁止制造冗余的计划文档。
## 执行工作流 (Workflow)
### 1. 确定计划类型
* **新特性/技术实现计划**:如果你要开发新功能或进行重大重构,你需要创建**实现计划**。
* **AI 任务交接计划**:如果当前任务尚未完成但需要记录进度留作以后或其他 AI 代理接手,你需要创建**交接计划**。
### 2. 读取对应模板
在创建计划文档前,必须读取对应的模板内容,并严格按照模板的骨架进行填充:
* **实现计划模板**:`docs/plan/implementation-plan-template.md`
* **接手计划模板**:`docs/plan/handover-plan-template.md`
### 3. 落库与命名规范
在 `docs/plan/` 目录下创建新的 Markdown 文件进行保存:
* **实现计划**命名格式:`docs/plan/YYYYMMDD-[feature-name].md` (例如:`20260605-uptime-kuma-sync.md`)
* **接手计划**命名格式:`docs/plan/handover-[task-name].md` (例如:`handover-waf-ip-group.md`)
### 4. 隔离约束 (极其重要)
`docs/plan/` 目录下的文档**仅限内部开发和 AI 代理同步使用**。
* **绝对禁止**将新创建的 plan 文档加入到项目的官方导航配置(如 `docs/config.ts` 的 `nav` 或 `sidebar` 导航条中)。
* **绝对禁止**通过任何方式将其暴露给文档渲染框架(如 VitePress)对外渲染。
## 后续动作
落库完成后,向用户报告计划已生成在 `docs/plan/` 目录下,并列出文档的核心要点或待决策项(如有),等待用户 Review 或批准后即可推进下一步。
+4 -54
View File
@@ -7,18 +7,7 @@ description: "Wavelet 项目专用:根据自上一个正式版本 Tag 以来
## 目标
当用户准备发布 Wavelet 新版本时,本 Skill 负责:
1. 根据上一正式版本 Tag 以来的提交,整理面向用户的发版说明;
2. 新建 **独立的** `chore(release): vX.Y.Z` 提交(可附带将 `docs/changelog` 从 `[unreleased]` 落版)。
## 硬性约束(禁止改写历史)
- **禁止** `git commit --amend` 修改任何**已经 push 到远端**的提交。
- **禁止** 为了发版去改写已有功能/修复提交的 message 或内容。
- **禁止** 发版流程中的 force-push(除非用户明确要求且知晓后果)。
- 发版提交必须是 **新增 commit**:在当前 `HEAD` 之上 `git commit` 一次。
- 默认 **不要 push、不要打 tag**;生成并完成本地 release commit 后,把后续 `push` / `git tag` 命令交给用户确认执行。
当用户准备发布 Wavelet 新版本时,本 Skill 只负责生成用于版本提交的 Commit Message。
## 生成提交信息
@@ -37,11 +26,7 @@ description: "Wavelet 项目专用:根据自上一个正式版本 Tag 以来
固定使用以下分类:
```text
### 🛠 修复
### ⚡️ 优化与改进
### 💄 其他/体验
```
text ### 🛠 修复 ### ⚡️ 优化与改进 ### 💄 其他/体验
分类规则:
@@ -52,8 +37,6 @@ description: "Wavelet 项目专用:根据自上一个正式版本 Tag 以来
示例:
```
chore(release): v3.3.0
### 🛠 修复
- 修复了通过 MCP 接口操作时笔记库范围限制未正确生效的问题。
- 修复了 MCP 接口返回数据格式不一致的问题。
@@ -66,39 +49,6 @@ chore(release): v3.3.0
- 优化了 WebSocket 错误日志,增加请求路径信息,方便问题排查。
```
## 提交步骤
## 提交
1. 确认工作区干净,且 `HEAD` 与将要发布的代码一致(通常已与 `origin/main` 对齐或仅含未 push 的合法新提交)。
2. 将 `docs/changelog/index.md` 中 `[unreleased]` 落版为 `[vX.Y.Z] - YYYY-MM-DD`(按需整理条目)。
3. **新建** release 提交(不要 amend):
```bash
git add docs/changelog/index.md # 及其他发版所需文件
git commit -m "$(cat <<'EOF'
chore(release): vX.Y.Z
### 🛠 修复
- ...
### ⚡️ 优化与改进
- ...
### 💄 其他/体验
- ...
EOF
)"
```
4. 向用户展示完整 commit message,并说明后续可由用户执行:
```bash
git push origin main
git tag vX.Y.Z
git push origin vX.Y.Z
```
(打 tag 后由 CI 创建双语 Release。)
## 任务结束条件
本地已存在 **新的** `chore(release): vX.Y.Z` 提交,且**未**改写任何已 push 提交、**未**擅自 push/tag。
生成完 Commit Message 后调用命令工具修改最后一次提交信息, 不要提交, 不要推送, 你的任务到此结束。
Symlink
+1
View File
@@ -0,0 +1 @@
.agent
+445
View File
@@ -0,0 +1,445 @@
# dmux Hooks System - Agent Reference
**Auto-generated documentation for AI agents**
This document contains everything an AI agent needs to create, modify, and understand dmux hooks. It is automatically generated from the dmux source code and embedded in the binary.
## What You're Working On
You are editing hooks for **dmux**, a tmux pane manager that creates AI-powered development workflows. Each pane runs in its own git worktree with an AI agent.
## Your Goal
Create executable bash scripts in `.dmux-hooks/` that run automatically at key lifecycle events.
## Quick Start
1. **Create a hook file**: `touch .dmux-hooks/worktree_created`
2. **Make it executable**: `chmod +x .dmux-hooks/worktree_created`
3. **Add shebang**: Start with `#!/bin/bash`
4. **Use environment variables**: Access `$DMUX_ROOT`, `$DMUX_WORKTREE_PATH`, etc.
5. **Test it**: Set env vars manually and run the script
## Hook Execution Model
- **Mostly non-blocking**: Most hooks run in background (detached processes)
- **Bootstrap-gated**: `worktree_created` blocks agent launch so setup can finish first, with no fixed timeout
- **Live bootstrap output**: During `worktree_created`, stdout/stderr is streamed into the new pane's setup UI
- **Failure behavior**: Background hook errors are logged; gated hooks can abort the operation
- **Environment-based**: All context passed via environment variables
- **Version controlled**: Hooks in `.dmux-hooks/` are shared with team
- **Priority resolution**: `.dmux-hooks/` → `.dmux/hooks/` → `~/.dmux/hooks/`
## Available Hooks
### Pane Lifecycle Hooks
| Hook | When | Common Use Cases |
|------|------|------------------|
| `before_pane_create` | Before pane creation | Validation, notifications, pre-flight checks |
| `pane_created` | After pane, before worktree | Configure tmux settings, prepare environment |
| `worktree_created` | After worktree creation, before agent launch | Install deps, copy configs, setup git |
| `before_pane_close` | Before closing | Save state, backup uncommitted work |
| `pane_closed` | After closed | Cleanup resources, analytics, notifications |
### Worktree Lifecycle Hooks
| Hook | When | Common Use Cases |
|------|------|------------------|
| `before_worktree_remove` | Before worktree removal | Archive worktree, save artifacts |
| `worktree_removed` | After worktree removed | Cleanup external references |
### Merge Lifecycle Hooks
| Hook | When | Common Use Cases |
|------|------|------------------|
| `pre_merge` | Before merge operation | Run final tests, create backups |
| `post_merge` | After successful merge | Deploy, close issues, notify team |
### Interactive Hooks (with HTTP callbacks)
| Hook | When | Common Use Cases |
|------|------|------------------|
| `run_test` | When tests triggered | Run test suite, report status via HTTP |
| `run_dev` | When dev server triggered | Start dev server, create tunnel, report URL |
## Environment Variables
### Always Available
```bash
DMUX_ROOT="/path/to/project" # Project root directory
DMUX_SERVER_PORT="3142" # HTTP server port
```
### Pane Context (most hooks)
```bash
DMUX_PANE_ID="dmux-1234567890" # dmux pane identifier
DMUX_SLUG="fix-auth-bug" # Branch/worktree name
DMUX_PROMPT="Fix authentication bug" # User's prompt
DMUX_AGENT="claude" # Agent type (registry id, e.g. claude, codex, opencode)
DMUX_TMUX_PANE_ID="%38" # tmux pane ID
```
### Worktree Context
```bash
DMUX_WORKTREE_PATH="/path/.dmux/worktrees/fix-auth-bug"
DMUX_BRANCH="fix-auth-bug" # Same as slug
```
### Bootstrap Progress Context
```bash
DMUX_PROGRESS="1" # Set when output is shown in the new pane setup UI
DMUX_STATUS_PREFIX="DMUX_STATUS:" # Prefix for clean status messages
```
`worktree_created` can emit progress while it runs. Any stdout/stderr line is shown in the setup UI; lines prefixed with `$DMUX_STATUS_PREFIX` are displayed without the prefix.
```bash
status() {
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
else
echo "[Hook] $*"
fi
}
```
### Merge Context
```bash
DMUX_TARGET_BRANCH="main" # Branch being merged into
```
## HTTP Callback API
Interactive hooks (`run_test` and `run_dev`) can update dmux UI via HTTP.
### Update Test Status
```bash
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test" -H "Content-Type: application/json" -d '{"status": "running", "output": "optional test output"}'
# Status values: "running" | "passed" | "failed"
```
### Update Dev Server
```bash
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev" -H "Content-Type: application/json" -d '{"status": "running", "url": "http://localhost:3000"}'
# Status values: "running" | "stopped"
# url: Can be localhost or tunnel URL (ngrok, cloudflared, etc.)
```
## Common Patterns
### Pattern 1: Install Dependencies
```bash
#!/bin/bash
# .dmux-hooks/worktree_created
cd "$DMUX_WORKTREE_PATH"
status() {
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
else
echo "[Hook] $*"
fi
}
if [ -f "pnpm-lock.yaml" ]; then
status "Installing dependencies with pnpm"
pnpm install --prefer-offline
elif [ -f "package-lock.json" ]; then
status "Installing dependencies with npm"
npm install
elif [ -f "yarn.lock" ]; then
status "Installing dependencies with yarn"
yarn install
elif [ -f "Gemfile" ]; then
status "Installing gems"
bundle install
elif [ -f "requirements.txt" ]; then
status "Installing Python dependencies"
pip install -r requirements.txt
elif [ -f "Cargo.toml" ]; then
status "Building Rust project"
cargo build
fi
```
### Pattern 2: Copy Configuration
```bash
#!/bin/bash
# .dmux-hooks/worktree_created
# Copy environment file
if [ -f "$DMUX_ROOT/.env.local" ]; then
cp "$DMUX_ROOT/.env.local" "$DMUX_WORKTREE_PATH/.env.local"
fi
# Copy other config files
for file in .env.development .npmrc .yarnrc; do
if [ -f "$DMUX_ROOT/$file" ]; then
cp "$DMUX_ROOT/$file" "$DMUX_WORKTREE_PATH/$file"
fi
done
```
### Pattern 3: Run Tests with Status Updates
```bash
#!/bin/bash
# .dmux-hooks/run_test
set -e
cd "$DMUX_WORKTREE_PATH"
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test"
# Update: starting
curl -s -X PUT "$API" -H "Content-Type: application/json" -d '{"status": "running"}' > /dev/null
# Run tests and capture output
OUTPUT_FILE="/tmp/dmux-test-$DMUX_PANE_ID.txt"
if pnpm test > "$OUTPUT_FILE" 2>&1; then
STATUS="passed"
else
STATUS="failed"
fi
# Get output (truncate if too long)
OUTPUT=$(head -c 5000 "$OUTPUT_FILE")
# Update: complete
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "$(jq -n --arg status "$STATUS" --arg output "$OUTPUT" '{status: $status, output: $output}')" > /dev/null
rm -f "$OUTPUT_FILE"
```
### Pattern 4: Dev Server with Tunnel
```bash
#!/bin/bash
# .dmux-hooks/run_dev
set -e
cd "$DMUX_WORKTREE_PATH"
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev"
# Start dev server in background
LOG_FILE="/tmp/dmux-dev-$DMUX_PANE_ID.log"
pnpm dev > "$LOG_FILE" 2>&1 &
DEV_PID=$!
# Wait for server to start
sleep 5
# Detect port from logs
PORT=$(grep -oP 'localhost:Kd+' "$LOG_FILE" | head -1)
[ -z "$PORT" ] && PORT=3000
# Optional: Create tunnel with cloudflared
if command -v cloudflared &> /dev/null; then
TUNNEL=$(cloudflared tunnel --url "http://localhost:$PORT" 2>&1 | grep -oP 'https://[a-z0-9-]+.trycloudflare.com' | head -1)
URL="${TUNNEL:-http://localhost:$PORT}"
else
URL="http://localhost:$PORT"
fi
# Report status
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "{"status": "running", "url": "$URL"}" > /dev/null
echo "[Hook] Dev server running at $URL (PID: $DEV_PID)"
```
### Pattern 5: Post-Merge Deployment
```bash
#!/bin/bash
# .dmux-hooks/post_merge
set -e
cd "$DMUX_ROOT"
# Only deploy from main/master
if [ "$DMUX_TARGET_BRANCH" != "main" ] && [ "$DMUX_TARGET_BRANCH" != "master" ]; then
exit 0
fi
# Push to remote
git push origin "$DMUX_TARGET_BRANCH"
# Trigger deployment (example: Vercel)
if [ -n "$VERCEL_TOKEN" ]; then
curl -s -X POST "https://api.vercel.com/v1/deployments" -H "Authorization: Bearer $VERCEL_TOKEN" -H "Content-Type: application/json" -d '{"name": "my-project"}' > /dev/null
fi
# Close GitHub issue if prompt contains #123
ISSUE=$(echo "$DMUX_PROMPT" | grep -oP '#Kd+' | head -1)
if [ -n "$ISSUE" ] && command -v gh &> /dev/null; then
gh issue close "$ISSUE" -c "Resolved in $DMUX_SLUG, merged to $DMUX_TARGET_BRANCH" 2>/dev/null || true
fi
```
## Best Practices
1. **Always start with shebang**: `#!/bin/bash`
2. **Set error handling**: `set -e` (exit on error)
3. **Make executable**: `chmod +x .dmux-hooks/hook_name`
4. **Background long operations**: Append `&` to avoid blocking
5. **Check for required tools**: `command -v tool &> /dev/null`
6. **Log for debugging**: `echo "[Hook] message" >> "$DMUX_ROOT/.dmux/hooks.log"`
7. **Handle missing vars gracefully**: `[ -z "$VAR" ] && exit 0`
8. **Use silent curl**: `curl -s` to avoid noise in logs
9. **Clean up temp files**: Remove files in `/tmp/`
10. **Test before committing**: Run hooks manually with mock env vars
## Testing Hooks
### Manual Testing
```bash
# 1. Set environment variables
export DMUX_ROOT="$(pwd)"
export DMUX_PANE_ID="test-pane"
export DMUX_SLUG="test-branch"
export DMUX_WORKTREE_PATH="$(pwd)"
export DMUX_SERVER_PORT="3142"
export DMUX_AGENT="claude"
export DMUX_PROMPT="Test prompt"
# 2. Run hook directly
./.dmux-hooks/worktree_created
# 3. Check exit code
echo $? # Should be 0 for success
```
### Syntax Check
```bash
# Check for syntax errors without running
bash -n ./.dmux-hooks/worktree_created
```
### Shellcheck (if available)
```bash
shellcheck ./.dmux-hooks/worktree_created
```
## Project Context Analysis
Before creating hooks, analyze these files in the project:
### Package Manager Detection
```bash
# Check which package manager is used
if [ -f "pnpm-lock.yaml" ]; then
# Use: pnpm install, pnpm test, pnpm dev
elif [ -f "package-lock.json" ]; then
# Use: npm install, npm test, npm run dev
elif [ -f "yarn.lock" ]; then
# Use: yarn install, yarn test, yarn dev
fi
```
### Test Command Discovery
```bash
# Read package.json to find test command
cat package.json | grep '"test"'
# Or with jq:
jq -r '.scripts.test' package.json
```
### Dev Command Discovery
```bash
# Read package.json to find dev command
cat package.json | grep '"dev"'
# Or with jq:
jq -r '.scripts.dev' package.json
```
### Environment Variables
```bash
# Check for .env files to copy
ls -la | grep '.env'
```
### Build System
```bash
# Detect build system
if [ -f "vite.config.ts" ]; then
# Vite project
elif [ -f "next.config.js" ]; then
# Next.js project
elif [ -f "nuxt.config.ts" ]; then
# Nuxt project
fi
```
## Common Mistakes to Avoid
❌ **Blocking operations**: `sleep 60` (blocks dmux)
✅ **Background long tasks**: `slow_operation &`
❌ **Hardcoded paths**: `/Users/me/project`
✅ **Use variables**: `"$DMUX_ROOT"`
❌ **Assuming tools exist**: `pnpm install`
✅ **Check first**: `command -v pnpm && pnpm install`
❌ **No error handling**: Script fails silently
✅ **Set error mode**: `set -e` or check exit codes
❌ **Forgetting executable bit**: Hook won't run
✅ **Make executable**: `chmod +x`
❌ **Noisy output**: Clutters dmux logs
✅ **Silent operations**: `curl -s`, `> /dev/null 2>&1`
❌ **Not testing**: Deploy and hope
✅ **Test manually**: Run with mock env vars first
## Debugging
If a hook isn't working:
1. **Check if file exists**: `ls -la .dmux-hooks/`
2. **Check permissions**: Should show `x` in `rwxr-xr-x`
3. **Check syntax**: `bash -n .dmux-hooks/hook_name`
4. **Test manually**: Set env vars and run
5. **Check logs**: dmux logs to stderr with `[Hooks]` prefix
6. **Simplify**: Remove complex parts, test basic version
7. **Check tool availability**: `command -v required_tool`
### Debug Mode
```bash
#!/bin/bash
# Add to top of hook for debugging
set -x # Print each command before executing
set -e # Exit on error
# Your hook logic here
```
## Summary Checklist
When creating a new hook:
- [ ] Create file in `.dmux-hooks/`
- [ ] Add shebang: `#!/bin/bash`
- [ ] Make executable: `chmod +x`
- [ ] Add `set -e` for error handling
- [ ] Use environment variables (never hardcode paths)
- [ ] Keep blocking hooks chatty with status output
- [ ] Background long operations with `&` only when the hook does not gate the operation
- [ ] Check for required tools before using
- [ ] Test manually with mock env vars
- [ ] Add comments explaining what it does
- [ ] Commit to version control
## Getting Help
- **Full documentation**: See `HOOKS.md` in project root
- **Claude-specific tips**: See `CLAUDE.md` in `.dmux-hooks/`
- **Examples**: Check `.dmux-hooks/examples/` directory
- **dmux API**: See `API.md` for REST endpoints
---
*This documentation was auto-generated from dmux source code.*
*Version: 2026-05-25*
+445
View File
@@ -0,0 +1,445 @@
# dmux Hooks System - Agent Reference
**Auto-generated documentation for AI agents**
This document contains everything an AI agent needs to create, modify, and understand dmux hooks. It is automatically generated from the dmux source code and embedded in the binary.
## What You're Working On
You are editing hooks for **dmux**, a tmux pane manager that creates AI-powered development workflows. Each pane runs in its own git worktree with an AI agent.
## Your Goal
Create executable bash scripts in `.dmux-hooks/` that run automatically at key lifecycle events.
## Quick Start
1. **Create a hook file**: `touch .dmux-hooks/worktree_created`
2. **Make it executable**: `chmod +x .dmux-hooks/worktree_created`
3. **Add shebang**: Start with `#!/bin/bash`
4. **Use environment variables**: Access `$DMUX_ROOT`, `$DMUX_WORKTREE_PATH`, etc.
5. **Test it**: Set env vars manually and run the script
## Hook Execution Model
- **Mostly non-blocking**: Most hooks run in background (detached processes)
- **Bootstrap-gated**: `worktree_created` blocks agent launch so setup can finish first, with no fixed timeout
- **Live bootstrap output**: During `worktree_created`, stdout/stderr is streamed into the new pane's setup UI
- **Failure behavior**: Background hook errors are logged; gated hooks can abort the operation
- **Environment-based**: All context passed via environment variables
- **Version controlled**: Hooks in `.dmux-hooks/` are shared with team
- **Priority resolution**: `.dmux-hooks/` → `.dmux/hooks/` → `~/.dmux/hooks/`
## Available Hooks
### Pane Lifecycle Hooks
| Hook | When | Common Use Cases |
|------|------|------------------|
| `before_pane_create` | Before pane creation | Validation, notifications, pre-flight checks |
| `pane_created` | After pane, before worktree | Configure tmux settings, prepare environment |
| `worktree_created` | After worktree creation, before agent launch | Install deps, copy configs, setup git |
| `before_pane_close` | Before closing | Save state, backup uncommitted work |
| `pane_closed` | After closed | Cleanup resources, analytics, notifications |
### Worktree Lifecycle Hooks
| Hook | When | Common Use Cases |
|------|------|------------------|
| `before_worktree_remove` | Before worktree removal | Archive worktree, save artifacts |
| `worktree_removed` | After worktree removed | Cleanup external references |
### Merge Lifecycle Hooks
| Hook | When | Common Use Cases |
|------|------|------------------|
| `pre_merge` | Before merge operation | Run final tests, create backups |
| `post_merge` | After successful merge | Deploy, close issues, notify team |
### Interactive Hooks (with HTTP callbacks)
| Hook | When | Common Use Cases |
|------|------|------------------|
| `run_test` | When tests triggered | Run test suite, report status via HTTP |
| `run_dev` | When dev server triggered | Start dev server, create tunnel, report URL |
## Environment Variables
### Always Available
```bash
DMUX_ROOT="/path/to/project" # Project root directory
DMUX_SERVER_PORT="3142" # HTTP server port
```
### Pane Context (most hooks)
```bash
DMUX_PANE_ID="dmux-1234567890" # dmux pane identifier
DMUX_SLUG="fix-auth-bug" # Branch/worktree name
DMUX_PROMPT="Fix authentication bug" # User's prompt
DMUX_AGENT="claude" # Agent type (registry id, e.g. claude, codex, opencode)
DMUX_TMUX_PANE_ID="%38" # tmux pane ID
```
### Worktree Context
```bash
DMUX_WORKTREE_PATH="/path/.dmux/worktrees/fix-auth-bug"
DMUX_BRANCH="fix-auth-bug" # Same as slug
```
### Bootstrap Progress Context
```bash
DMUX_PROGRESS="1" # Set when output is shown in the new pane setup UI
DMUX_STATUS_PREFIX="DMUX_STATUS:" # Prefix for clean status messages
```
`worktree_created` can emit progress while it runs. Any stdout/stderr line is shown in the setup UI; lines prefixed with `$DMUX_STATUS_PREFIX` are displayed without the prefix.
```bash
status() {
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
else
echo "[Hook] $*"
fi
}
```
### Merge Context
```bash
DMUX_TARGET_BRANCH="main" # Branch being merged into
```
## HTTP Callback API
Interactive hooks (`run_test` and `run_dev`) can update dmux UI via HTTP.
### Update Test Status
```bash
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test" -H "Content-Type: application/json" -d '{"status": "running", "output": "optional test output"}'
# Status values: "running" | "passed" | "failed"
```
### Update Dev Server
```bash
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev" -H "Content-Type: application/json" -d '{"status": "running", "url": "http://localhost:3000"}'
# Status values: "running" | "stopped"
# url: Can be localhost or tunnel URL (ngrok, cloudflared, etc.)
```
## Common Patterns
### Pattern 1: Install Dependencies
```bash
#!/bin/bash
# .dmux-hooks/worktree_created
cd "$DMUX_WORKTREE_PATH"
status() {
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
else
echo "[Hook] $*"
fi
}
if [ -f "pnpm-lock.yaml" ]; then
status "Installing dependencies with pnpm"
pnpm install --prefer-offline
elif [ -f "package-lock.json" ]; then
status "Installing dependencies with npm"
npm install
elif [ -f "yarn.lock" ]; then
status "Installing dependencies with yarn"
yarn install
elif [ -f "Gemfile" ]; then
status "Installing gems"
bundle install
elif [ -f "requirements.txt" ]; then
status "Installing Python dependencies"
pip install -r requirements.txt
elif [ -f "Cargo.toml" ]; then
status "Building Rust project"
cargo build
fi
```
### Pattern 2: Copy Configuration
```bash
#!/bin/bash
# .dmux-hooks/worktree_created
# Copy environment file
if [ -f "$DMUX_ROOT/.env.local" ]; then
cp "$DMUX_ROOT/.env.local" "$DMUX_WORKTREE_PATH/.env.local"
fi
# Copy other config files
for file in .env.development .npmrc .yarnrc; do
if [ -f "$DMUX_ROOT/$file" ]; then
cp "$DMUX_ROOT/$file" "$DMUX_WORKTREE_PATH/$file"
fi
done
```
### Pattern 3: Run Tests with Status Updates
```bash
#!/bin/bash
# .dmux-hooks/run_test
set -e
cd "$DMUX_WORKTREE_PATH"
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test"
# Update: starting
curl -s -X PUT "$API" -H "Content-Type: application/json" -d '{"status": "running"}' > /dev/null
# Run tests and capture output
OUTPUT_FILE="/tmp/dmux-test-$DMUX_PANE_ID.txt"
if pnpm test > "$OUTPUT_FILE" 2>&1; then
STATUS="passed"
else
STATUS="failed"
fi
# Get output (truncate if too long)
OUTPUT=$(head -c 5000 "$OUTPUT_FILE")
# Update: complete
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "$(jq -n --arg status "$STATUS" --arg output "$OUTPUT" '{status: $status, output: $output}')" > /dev/null
rm -f "$OUTPUT_FILE"
```
### Pattern 4: Dev Server with Tunnel
```bash
#!/bin/bash
# .dmux-hooks/run_dev
set -e
cd "$DMUX_WORKTREE_PATH"
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev"
# Start dev server in background
LOG_FILE="/tmp/dmux-dev-$DMUX_PANE_ID.log"
pnpm dev > "$LOG_FILE" 2>&1 &
DEV_PID=$!
# Wait for server to start
sleep 5
# Detect port from logs
PORT=$(grep -oP 'localhost:Kd+' "$LOG_FILE" | head -1)
[ -z "$PORT" ] && PORT=3000
# Optional: Create tunnel with cloudflared
if command -v cloudflared &> /dev/null; then
TUNNEL=$(cloudflared tunnel --url "http://localhost:$PORT" 2>&1 | grep -oP 'https://[a-z0-9-]+.trycloudflare.com' | head -1)
URL="${TUNNEL:-http://localhost:$PORT}"
else
URL="http://localhost:$PORT"
fi
# Report status
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "{"status": "running", "url": "$URL"}" > /dev/null
echo "[Hook] Dev server running at $URL (PID: $DEV_PID)"
```
### Pattern 5: Post-Merge Deployment
```bash
#!/bin/bash
# .dmux-hooks/post_merge
set -e
cd "$DMUX_ROOT"
# Only deploy from main/master
if [ "$DMUX_TARGET_BRANCH" != "main" ] && [ "$DMUX_TARGET_BRANCH" != "master" ]; then
exit 0
fi
# Push to remote
git push origin "$DMUX_TARGET_BRANCH"
# Trigger deployment (example: Vercel)
if [ -n "$VERCEL_TOKEN" ]; then
curl -s -X POST "https://api.vercel.com/v1/deployments" -H "Authorization: Bearer $VERCEL_TOKEN" -H "Content-Type: application/json" -d '{"name": "my-project"}' > /dev/null
fi
# Close GitHub issue if prompt contains #123
ISSUE=$(echo "$DMUX_PROMPT" | grep -oP '#Kd+' | head -1)
if [ -n "$ISSUE" ] && command -v gh &> /dev/null; then
gh issue close "$ISSUE" -c "Resolved in $DMUX_SLUG, merged to $DMUX_TARGET_BRANCH" 2>/dev/null || true
fi
```
## Best Practices
1. **Always start with shebang**: `#!/bin/bash`
2. **Set error handling**: `set -e` (exit on error)
3. **Make executable**: `chmod +x .dmux-hooks/hook_name`
4. **Background long operations**: Append `&` to avoid blocking
5. **Check for required tools**: `command -v tool &> /dev/null`
6. **Log for debugging**: `echo "[Hook] message" >> "$DMUX_ROOT/.dmux/hooks.log"`
7. **Handle missing vars gracefully**: `[ -z "$VAR" ] && exit 0`
8. **Use silent curl**: `curl -s` to avoid noise in logs
9. **Clean up temp files**: Remove files in `/tmp/`
10. **Test before committing**: Run hooks manually with mock env vars
## Testing Hooks
### Manual Testing
```bash
# 1. Set environment variables
export DMUX_ROOT="$(pwd)"
export DMUX_PANE_ID="test-pane"
export DMUX_SLUG="test-branch"
export DMUX_WORKTREE_PATH="$(pwd)"
export DMUX_SERVER_PORT="3142"
export DMUX_AGENT="claude"
export DMUX_PROMPT="Test prompt"
# 2. Run hook directly
./.dmux-hooks/worktree_created
# 3. Check exit code
echo $? # Should be 0 for success
```
### Syntax Check
```bash
# Check for syntax errors without running
bash -n ./.dmux-hooks/worktree_created
```
### Shellcheck (if available)
```bash
shellcheck ./.dmux-hooks/worktree_created
```
## Project Context Analysis
Before creating hooks, analyze these files in the project:
### Package Manager Detection
```bash
# Check which package manager is used
if [ -f "pnpm-lock.yaml" ]; then
# Use: pnpm install, pnpm test, pnpm dev
elif [ -f "package-lock.json" ]; then
# Use: npm install, npm test, npm run dev
elif [ -f "yarn.lock" ]; then
# Use: yarn install, yarn test, yarn dev
fi
```
### Test Command Discovery
```bash
# Read package.json to find test command
cat package.json | grep '"test"'
# Or with jq:
jq -r '.scripts.test' package.json
```
### Dev Command Discovery
```bash
# Read package.json to find dev command
cat package.json | grep '"dev"'
# Or with jq:
jq -r '.scripts.dev' package.json
```
### Environment Variables
```bash
# Check for .env files to copy
ls -la | grep '.env'
```
### Build System
```bash
# Detect build system
if [ -f "vite.config.ts" ]; then
# Vite project
elif [ -f "next.config.js" ]; then
# Next.js project
elif [ -f "nuxt.config.ts" ]; then
# Nuxt project
fi
```
## Common Mistakes to Avoid
❌ **Blocking operations**: `sleep 60` (blocks dmux)
✅ **Background long tasks**: `slow_operation &`
❌ **Hardcoded paths**: `/Users/me/project`
✅ **Use variables**: `"$DMUX_ROOT"`
❌ **Assuming tools exist**: `pnpm install`
✅ **Check first**: `command -v pnpm && pnpm install`
❌ **No error handling**: Script fails silently
✅ **Set error mode**: `set -e` or check exit codes
❌ **Forgetting executable bit**: Hook won't run
✅ **Make executable**: `chmod +x`
❌ **Noisy output**: Clutters dmux logs
✅ **Silent operations**: `curl -s`, `> /dev/null 2>&1`
❌ **Not testing**: Deploy and hope
✅ **Test manually**: Run with mock env vars first
## Debugging
If a hook isn't working:
1. **Check if file exists**: `ls -la .dmux-hooks/`
2. **Check permissions**: Should show `x` in `rwxr-xr-x`
3. **Check syntax**: `bash -n .dmux-hooks/hook_name`
4. **Test manually**: Set env vars and run
5. **Check logs**: dmux logs to stderr with `[Hooks]` prefix
6. **Simplify**: Remove complex parts, test basic version
7. **Check tool availability**: `command -v required_tool`
### Debug Mode
```bash
#!/bin/bash
# Add to top of hook for debugging
set -x # Print each command before executing
set -e # Exit on error
# Your hook logic here
```
## Summary Checklist
When creating a new hook:
- [ ] Create file in `.dmux-hooks/`
- [ ] Add shebang: `#!/bin/bash`
- [ ] Make executable: `chmod +x`
- [ ] Add `set -e` for error handling
- [ ] Use environment variables (never hardcode paths)
- [ ] Keep blocking hooks chatty with status output
- [ ] Background long operations with `&` only when the hook does not gate the operation
- [ ] Check for required tools before using
- [ ] Test manually with mock env vars
- [ ] Add comments explaining what it does
- [ ] Commit to version control
## Getting Help
- **Full documentation**: See `HOOKS.md` in project root
- **Claude-specific tips**: See `CLAUDE.md` in `.dmux-hooks/`
- **Examples**: Check `.dmux-hooks/examples/` directory
- **dmux API**: See `API.md` for REST endpoints
---
*This documentation was auto-generated from dmux source code.*
*Version: 2026-05-25*
+53
View File
@@ -0,0 +1,53 @@
# dmux Hooks
This directory contains hooks that run automatically at key lifecycle events in dmux.
## Quick Start
1. **Read the documentation**:
- `AGENTS.md` - Complete reference (for any AI agent)
- `CLAUDE.md` - Same content (Claude Code looks for this filename)
2. **Check examples**:
- `examples/` directory contains starter templates
3. **Create a hook**:
```bash
touch worktree_created
chmod +x worktree_created
nano worktree_created
```
4. **Test it**:
```bash
export DMUX_ROOT="$(pwd)"
export DMUX_WORKTREE_PATH="$(pwd)"
./worktree_created
```
## Available Hooks
- `before_pane_create` - Before pane creation
- `pane_created` - After pane created
- `worktree_created` - After worktree setup
- `before_pane_close` - Before closing
- `pane_closed` - After closed
- `before_worktree_remove` - Before worktree removal
- `worktree_removed` - After worktree removed
- `pre_merge` - Before merge
- `post_merge` - After merge
- `run_test` - When running tests
- `run_dev` - When starting dev server
## Documentation
See `AGENTS.md` or `CLAUDE.md` for complete documentation including:
- Environment variables
- HTTP callback API
- Common patterns
- Best practices
- Testing strategies
## Note
This directory is **version controlled**. Hooks you create here will be shared with your team.
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Example: post_merge hook
#
# This hook runs after a successful merge into the target branch.
# Use it to trigger deployments, close issues, notify teams, etc.
set -e
echo "[Hook] Post-merge processing for $DMUX_SLUG → $DMUX_TARGET_BRANCH"
cd "$DMUX_ROOT"
# Push to remote if merging to main/master
if [ "$DMUX_TARGET_BRANCH" = "main" ] || [ "$DMUX_TARGET_BRANCH" = "master" ]; then
echo "[Hook] Pushing to origin/$DMUX_TARGET_BRANCH"
git push origin "$DMUX_TARGET_BRANCH"
# Optional: Trigger deployment
# if [ -n "$VERCEL_TOKEN" ]; then
# echo "[Hook] Triggering Vercel deployment..."
# curl -X POST "https://api.vercel.com/v1/deployments" \
# -H "Authorization: Bearer $VERCEL_TOKEN" \
# -H "Content-Type: application/json" \
# -d '{
# "name": "my-project",
# "gitSource": {
# "type": "github",
# "ref": "main"
# }
# }'
# fi
fi
# Close related GitHub issue (if prompt contains #123 format)
ISSUE_NUM=$(echo "$DMUX_PROMPT" | grep -oP '#\K\d+' | head -1)
if [ -n "$ISSUE_NUM" ]; then
echo "[Hook] Closing GitHub issue #$ISSUE_NUM"
if command -v gh &> /dev/null; then
gh issue close "$ISSUE_NUM" \
-c "Resolved in branch $DMUX_SLUG, merged to $DMUX_TARGET_BRANCH" \
2>/dev/null || echo "[Hook] Warning: Failed to close issue (maybe already closed?)"
else
echo "[Hook] GitHub CLI (gh) not found, skipping issue close"
fi
fi
# Send notification to Slack
# if [ -n "$SLACK_WEBHOOK" ]; then
# echo "[Hook] Sending Slack notification"
# curl -s -X POST "$SLACK_WEBHOOK" \
# -H "Content-Type: application/json" \
# -d "{
# \"text\": \"Merged: $DMUX_SLUG → $DMUX_TARGET_BRANCH\",
# \"blocks\": [
# {
# \"type\": \"section\",
# \"text\": {
# \"type\": \"mrkdwn\",
# \"text\": \"*Branch Merged* :rocket:\n\n*From:* \`$DMUX_SLUG\`\n*To:* \`$DMUX_TARGET_BRANCH\`\n*Task:* $DMUX_PROMPT\"
# }
# }
# ]
# }" > /dev/null
# fi
echo "[Hook] Post-merge processing complete"
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# Example: run_dev hook
#
# This hook starts a dev server and optionally creates a tunnel for sharing.
# It reports the server URL back to dmux via the HTTP API.
set -e
echo "[Hook] Starting dev server for $DMUX_SLUG"
cd "$DMUX_WORKTREE_PATH"
API_URL="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev"
# Update status: starting
curl -s -X PUT "$API_URL" \
-H "Content-Type: application/json" \
-d '{"status": "running"}' > /dev/null
# Start dev server in background
# Adjust the command for your project (pnpm dev, npm run dev, vite, etc.)
LOG_FILE="/tmp/dmux-dev-$DMUX_PANE_ID.log"
pnpm dev > "$LOG_FILE" 2>&1 &
DEV_PID=$!
# Wait for server to be ready
echo "[Hook] Waiting for dev server to start..."
sleep 5
# Detect port from log output
# Adjust the grep pattern for your dev server's output format
PORT=$(grep -oP '(?<=localhost:)\d+' "$LOG_FILE" | head -1)
if [ -z "$PORT" ]; then
echo "[Hook] Warning: Could not detect port from logs, using default 3000"
PORT=3000
fi
LOCAL_URL="http://localhost:$PORT"
echo "[Hook] Dev server running at $LOCAL_URL"
# Optional: Create a public tunnel (uncomment to enable)
# Requires ngrok, cloudflared, or another tunneling tool
# Example with cloudflared:
# TUNNEL_URL=$(cloudflared tunnel --url "$LOCAL_URL" 2>&1 | \
# grep -oP 'https://[a-z0-9-]+\.trycloudflare\.com' | head -1)
# Example with ngrok:
# TUNNEL_URL=$(ngrok http $PORT --log=stdout 2>&1 | \
# grep -oP 'url=https://[^"]+' | head -1 | cut -d= -f2)
# For now, just use local URL (uncomment tunnel code above to enable)
FINAL_URL="$LOCAL_URL"
# Report status back to dmux
curl -s -X PUT "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"status\": \"running\", \"url\": \"$FINAL_URL\"}" > /dev/null
echo "[Hook] Dev server ready at: $FINAL_URL"
echo "[Hook] Dev server PID: $DEV_PID"
echo "[Hook] Log file: $LOG_FILE"
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# Example: run_test hook
#
# This hook runs tests and reports the status back to dmux via the HTTP API.
# Status updates appear in real-time in the dmux UI.
set -e
echo "[Hook] Running tests for $DMUX_SLUG"
cd "$DMUX_WORKTREE_PATH"
API_URL="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test"
# Update status: running
curl -s -X PUT "$API_URL" \
-H "Content-Type: application/json" \
-d '{"status": "running"}' > /dev/null
echo "[Hook] Running test suite..."
# Capture test output
OUTPUT_FILE="/tmp/dmux-test-$DMUX_PANE_ID.txt"
# Run tests (adjust command for your project)
# Examples:
# - pnpm test
# - npm test
# - vitest run
# - jest
# - pytest
# - cargo test
if pnpm test > "$OUTPUT_FILE" 2>&1; then
STATUS="passed"
echo "[Hook] Tests passed ✓"
else
STATUS="failed"
echo "[Hook] Tests failed ✗"
fi
# Get output (truncate if too long)
OUTPUT=$(head -c 5000 "$OUTPUT_FILE")
# Report results back to dmux
curl -s -X PUT "$API_URL" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg status "$STATUS" \
--arg output "$OUTPUT" \
'{status: $status, output: $output}')" > /dev/null
# Cleanup
rm -f "$OUTPUT_FILE"
echo "[Hook] Test results reported to dmux"
# Exit with test status
if [ "$STATUS" = "passed" ]; then
exit 0
else
exit 1
fi
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Example: worktree_created hook
#
# This hook runs after a new worktree is created and before the agent launches.
# Use it to set up the worktree environment (install deps, copy configs, etc.)
# Stdout/stderr is streamed into the new pane's setup UI while this hook runs.
# dmux waits for this hook without a fixed timeout.
set -e # Exit on error
status() {
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
else
echo "[Hook] $*"
fi
}
status "Setting up worktree: $DMUX_SLUG"
cd "$DMUX_WORKTREE_PATH"
# Install dependencies before the agent launches.
if [ -f "pnpm-lock.yaml" ]; then
status "Installing dependencies with pnpm"
pnpm install --prefer-offline
elif [ -f "package-lock.json" ]; then
status "Installing dependencies with npm"
npm install
elif [ -f "yarn.lock" ]; then
status "Installing dependencies with yarn"
yarn install
fi
# Copy environment file if it exists
if [ -f "$DMUX_ROOT/.env.local" ]; then
status "Copying .env.local"
cp "$DMUX_ROOT/.env.local" "$DMUX_WORKTREE_PATH/.env.local"
fi
# Keep existing git author identity.
# Do not set git user.name/user.email in this hook.
# Create a log entry
echo "[$(date)] Created worktree: $DMUX_SLUG | Agent: $DMUX_AGENT | Prompt: $DMUX_PROMPT" \
>> "$DMUX_ROOT/.dmux/worktree_history.log"
status "Worktree setup complete"
+9 -14
View File
@@ -1,27 +1,21 @@
.git
.idea
.vscode
.github
anubis-source
**/node_modules
**/.next
**/build
**/dist
**/.cache
**/coverage
**/*.db
**/*.log
tmp
logs
.DS_Store
Thumbs.db
config.yaml
.env
.env.*
docker-compose*.yml
config.yaml
bin/
build/
dist/
data/
logs/
uploads/
s3_cache/
frontend/node_modules/
frontend/.next/
frontend/out/
@@ -31,5 +25,6 @@ frontend/.env
frontend/next-env.d.ts
frontend/*.tsbuildinfo
frontend/package-lock.json
internal/router/dist/
internal/router/root/dist/
+18 -22
View File
@@ -1,5 +1,5 @@
# ──────────────────────────────────────────────────────────────────────────────
# openflare — 环境变量配置模板
# wavelet — 环境变量配置模板
# 复制此文件为 .env 并填入实际值: cp .env.example .env
# 环境变量优先级高于 config.yaml
# docker compose 会读取本文件(env_file: .env)并替换 compose 中的 ${VAR}
@@ -9,13 +9,13 @@
TZ=Asia/Shanghai
# ─── 应用配置 ──────────────────────────────────────────────────────────────────
APP_NAME=openflare
APP_NAME=wavelet
APP_ENV=production
APP_ADDR=:3000
APP_ADDR=:8000
APP_NODE_ID=1
APP_API_PREFIX=/api
# APP_GRACEFUL_SHUTDOWN_TIMEOUT=30
APP_SESSION_COOKIE_NAME=openflare_session_id
APP_SESSION_COOKIE_NAME=wavelet_session_id
APP_SESSION_SECRET=change-me-to-a-random-string-in-production
# APP_SESSION_DOMAIN=
APP_SESSION_AGE=86400
@@ -27,13 +27,12 @@ APP_SESSION_SECURE=true
# 设置 DB_HOST 后自动启用 PostgreSQL,也可通过 DB_ENABLED 显式控制
# DB_ENABLED=false 时使用 SQLite 作为后备数据库
DB_ENABLED=true
# SQLITE_PATH=./data/openflare.db
# compose 内应用连服务名;本机直连 Docker 映射端口时用 127.0.0.1
# SQLITE_PATH=./data/wavelet.db
DB_HOST=postgres
DB_PORT=5432
DB_USERNAME=openflare
DB_PASSWORD=replace-with-strong-password
DB_NAME=openflare
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_NAME=wavelet
DB_SSL_MODE=disable
DB_TIMEZONE=Asia/Shanghai
# DB_LOG_LEVEL=info
@@ -47,23 +46,20 @@ REDIS_ADDR=redis:6379
# REDIS_USERNAME=
# REDIS_PASSWORD=
# REDIS_DB=0
REDIS_KEY_PREFIX=openflare:
REDIS_KEY_PREFIX=wavelet:
# REDIS_POOL_SIZE=100
# 启动时开关;修改后需重启服务
REDIS_MAINT_NOTIFICATIONS=false
# compose 宿主机映射端口(仅 docker-compose 使用)
# REDIS_PORT=6379
# ─── ClickHouse(必需)────────────────────────────────────────────────────────
# CLICKHOUSE_HOST 设置后会自动启用;测试环境可显式 CLICKHOUSE_ENABLED=true 做 live 联调
CLICKHOUSE_ENABLED=true
# compose 内:clickhouse:9000;本机连映射端口:127.0.0.1:9000
CLICKHOUSE_HOST=clickhouse:9000
CLICKHOUSE_USERNAME=default
# 须与 compose clickhouse 服务密码一致(首次初始化后改密码需清 data/clickhouse_data)
CLICKHOUSE_PASSWORD=replace-with-clickhouse-password
CLICKHOUSE_NAME=openflare
# ─── ClickHouse(可选,默认关闭)──────────────────────────────────────────
# 设置 CLICKHOUSE_HOST 后自动启用,也可显式控制
# CLICKHOUSE_ENABLED=false
# CLICKHOUSE_HOST=clickhouse:9000
# CLICKHOUSE_USERNAME=default
# CLICKHOUSE_PASSWORD=
# CLICKHOUSE_NAME=wavelet
# ─── 日志 ──────────────────────────────────────────────────────────────────────
LOG_LEVEL=info
@@ -76,8 +72,8 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
OTEL_EXPORTER_OTLP_INSECURE=true
# 设为 0 关闭 tracing;本地 Jaeger 调试建议设为 1.0
OTEL_SAMPLING_RATE=0.0
# 全局 Tracer 命名空间,默认为 github.com/Rain-kl/OpenFlare
# OTEL_TRACER_NAME=github.com/Rain-kl/OpenFlare
# 全局 Tracer 命名空间,默认为 github.com/Rain-kl/Wavelet
# OTEL_TRACER_NAME=github.com/Rain-kl/Wavelet
# compose 可选端口覆盖
# JAEGER_VERSION=2.19.0
# JAEGER_UI_PORT=16686
-1
View File
@@ -1 +0,0 @@
* -text
@@ -1,197 +0,0 @@
name: Build Image (openflare-agent)
on:
workflow_dispatch:
inputs:
version:
description: "Image version/tag to publish, for example v1.0.0-beta"
required: false
type: string
push:
tags: ["v*"]
permissions:
contents: read
packages: write
attestations: write
id-token: write
env:
IMAGE_NAME: openflare-agent
DOCKERFILE: docker/Dockerfile.agent
jobs:
build:
name: Build (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-agent" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v7
with:
context: .
file: ${{ env.DOCKERFILE }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests"
touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }}
path: /tmp/${{ env.IMAGE_NAME }}-digests/*
if-no-files-found: error
retention-days: 1
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
merge:
name: Merge multi-arch manifest
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-agent" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/${{ env.IMAGE_NAME }}-digests
pattern: ${{ env.IMAGE_NAME }}-digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/${{ env.IMAGE_NAME }}-digests
shell: bash
run: |
shopt -s nullglob
references=()
for digest in *; do
references+=("${IMAGE}@sha256:${digest}")
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2
exit 1
fi
if [[ "${VERSION}" =~ (alpha|beta|rc) ]]; then
FLOATING_TAG="beta"
else
FLOATING_TAG="latest"
fi
docker buildx imagetools create \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:${FLOATING_TAG}" \
"${references[@]}"
env:
IMAGE: ${{ env.IMAGE }}
- name: Inspect image
run: docker buildx imagetools inspect "${{ env.IMAGE }}:${{ env.VERSION }}"
@@ -1,197 +0,0 @@
name: Build Image (openflare-relay)
on:
workflow_dispatch:
inputs:
version:
description: "Image version/tag to publish, for example v1.0.0-beta"
required: false
type: string
push:
tags: ["v*"]
permissions:
contents: read
packages: write
attestations: write
id-token: write
env:
IMAGE_NAME: openflare-relay
DOCKERFILE: docker/Dockerfile.relay
jobs:
build:
name: Build (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-relay" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v7
with:
context: .
file: ${{ env.DOCKERFILE }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests"
touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }}
path: /tmp/${{ env.IMAGE_NAME }}-digests/*
if-no-files-found: error
retention-days: 1
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
merge:
name: Merge multi-arch manifest
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare-relay" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/${{ env.IMAGE_NAME }}-digests
pattern: ${{ env.IMAGE_NAME }}-digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/${{ env.IMAGE_NAME }}-digests
shell: bash
run: |
shopt -s nullglob
references=()
for digest in *; do
references+=("${IMAGE}@sha256:${digest}")
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2
exit 1
fi
if [[ "${VERSION}" =~ (alpha|beta|rc) ]]; then
FLOATING_TAG="beta"
else
FLOATING_TAG="latest"
fi
docker buildx imagetools create \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:${FLOATING_TAG}" \
"${references[@]}"
env:
IMAGE: ${{ env.IMAGE }}
- name: Inspect image
run: docker buildx imagetools inspect "${{ env.IMAGE }}:${{ env.VERSION }}"
@@ -1,197 +0,0 @@
name: Build Image (openflared)
on:
workflow_dispatch:
inputs:
version:
description: "Image version/tag to publish, for example v1.0.0-beta"
required: false
type: string
push:
tags: ["v*"]
permissions:
contents: read
packages: write
attestations: write
id-token: write
env:
IMAGE_NAME: openflared
DOCKERFILE: docker/Dockerfile.flared
jobs:
build:
name: Build (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflared" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v7
with:
context: .
file: ${{ env.DOCKERFILE }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests"
touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }}
path: /tmp/${{ env.IMAGE_NAME }}-digests/*
if-no-files-found: error
retention-days: 1
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
merge:
name: Merge multi-arch manifest
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflared" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/${{ env.IMAGE_NAME }}-digests
pattern: ${{ env.IMAGE_NAME }}-digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/${{ env.IMAGE_NAME }}-digests
shell: bash
run: |
shopt -s nullglob
references=()
for digest in *; do
references+=("${IMAGE}@sha256:${digest}")
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2
exit 1
fi
if [[ "${VERSION}" =~ (alpha|beta|rc) ]]; then
FLOATING_TAG="beta"
else
FLOATING_TAG="latest"
fi
docker buildx imagetools create \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:${FLOATING_TAG}" \
"${references[@]}"
env:
IMAGE: ${{ env.IMAGE }}
- name: Inspect image
run: docker buildx imagetools inspect "${{ env.IMAGE }}:${{ env.VERSION }}"
@@ -1,4 +1,4 @@
name: Build Image (openflare)
name: Build Image
on:
workflow_dispatch:
@@ -17,7 +17,7 @@ permissions:
id-token: write
env:
IMAGE_NAME: openflare
IMAGE_NAME: wavelet
DOCKERFILE: docker/Dockerfile
jobs:
@@ -63,7 +63,7 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare" >> "$GITHUB_ENV"
echo "IMAGE=ghcr.io/${OWNER}/${IMAGE_NAME}" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV"
@@ -87,7 +87,6 @@ jobs:
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
BUILD_DATE=${{ env.BUILD_DATE }}
cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }}
@@ -147,7 +146,7 @@ jobs:
exit 1
fi
echo "IMAGE=ghcr.io/${OWNER}/openflare" >> "$GITHUB_ENV"
echo "IMAGE=ghcr.io/${OWNER}/${IMAGE_NAME}" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
@@ -206,4 +205,4 @@ jobs:
curl -fsSL "$WEBHOOK_URL"
else
echo "Webhook URL is not set, skipping."
fi
fi
+1 -145
View File
@@ -11,7 +11,7 @@ on:
type: string
env:
APP_NAME: openflare-server
APP_NAME: wavelet
GO_MAIN: ./main.go
GO_BUILD_TAGS: embed_frontend
GO_LDFLAGS: -s -w
@@ -268,147 +268,3 @@ jobs:
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: ${{ steps.package.outputs.artifact }}
build-agent-binaries:
name: Build agent ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-agent-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-agent-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-agent-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-agent-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Fetch embedded GeoIP database
run: bash scripts/fetch-agent-geoip-mmdb.sh
- name: Build Agent
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/agent/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/agent/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: dist/${{ matrix.asset_name }}
build-relay-binaries:
name: Build relay ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflare-relay-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflare-relay-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflare-relay-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflare-relay-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Relay
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/relay/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/relay/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: dist/${{ matrix.asset_name }}
build-flared-binaries:
name: Build flared ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
asset_name: openflared-linux-amd64
- goos: linux
goarch: arm64
asset_name: openflared-linux-arm64
- goos: darwin
goarch: amd64
asset_name: openflared-darwin-amd64
- goos: darwin
goarch: arm64
asset_name: openflared-darwin-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build Flared
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/flared/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/flared/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.create-release.outputs.version }}
files: dist/${{ matrix.asset_name }}
+9 -31
View File
@@ -12,8 +12,6 @@
# config
config.yaml
.env
.env.*
!.env.example
# sqlite
*.db
@@ -29,6 +27,8 @@ frontend/.next/*
frontend/next-env.d.ts
frontend/package-lock.json
frontend/.env
.env.*
!.env.example
*.tsbuildinfo
# os
@@ -46,39 +46,17 @@ go.work.sum
main
# upload
/uploads/
s3_cache
uploads/*
s3_cache
/frontend/.next/
/data/
/internal/router/dist/
/frontend/out/
/.idea/
/uploads/
/*-source/
/*-source.zip
/.cache/
/internal/router/root/dist/
.dmux/
# test coverage
*.out
coverage.*
*.coverprofile
profile.cov
# generic ignores
.cache
.gocache*
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*-source
*-source.zip
.codex*
.grok
/.gomodcache/
*.mmdb
!internal/apps/agent/geoipdata/GeoLite2-Country.mmdb
!internal/apps/agent/geoipdata/GeoLite2-City.mmdb
/.superpowers/
/.worktrees/
+6 -36
View File
@@ -1,19 +1,6 @@
# AGENTS.md
# AGENTS.md — 项目AI助手工作操作手册
本文件是 OpenFlare 的 AI 接手入口,不承载详细设计、规范和计划。接手项目时,请根据以下分层文档指引进行阅读与开发:
### 1. 开发指导规范 (AI & Developer Guidelines)
* **必须阅读**:
* **[docs/plan/index.md](./docs/plan/index.md)**:查看正在进行的开发实现计划(Implementation Plan)与 AI 代理交接文档(Handover),接手项目时优先检查。
### 2. 系统设计与架构 (Design Docs)
* **[docs/design/index.md](./docs/design/index.md)**:理解产品范围、系统边界、核心对象及长期约束,以及[仓库结构](./docs/design/index.md#仓库结构)。
* **[docs/design/architecture.md](./docs/design/architecture.md)**:理解 Server、Agent、OpenResty 与前端的职责边界与网络拓扑。
* **[docs/design/agent-design.md](./docs/design/agent-design.md)**:理解 Agent 设计原则、与 Server 交互时序、OpenResty 管控与配置发布回滚模型。
---
本文件面向 AI 开发助手,定义其职责与操作规范。
## Git 提交规范指南
@@ -56,7 +43,7 @@
- 所有 HTTP 路由仅在 `internal/router/router.go` 中注册。
- 当 API Handler 发生变化时,更新 Swagger 文档(运行 `make swagger`)。
- 在完成代码开发后必须运行 `make code-check`, 并修复报错。
- 在完成代码开发后或者 git 提交前必须运行 `make prettier` 格式化代码。
- 在完成代码开发后/git 提交前必须运行 `make prettier`进行格式化。
- 需要缓存或文件管理能力时,必须复用现有平台实现,禁止在业务包中自行创建缓存目录、直接管理缓存文件或重复封装存储后端。
- 文件摄取必须通过 `upload.Ingest`(`upload.PolicyCreate` / `PolicyDedupNewRecord` / `PolicyResolveExisting`);删除必须通过 `upload.Remove` 或 `upload.RemoveOwned`。禁止业务模块直接调用 `repository.CreateUpload` / `repository.SoftDeleteUpload`,禁止 `db.Create(&model.Upload{})` 旁路写 `w_uploads`。
- 禁止在 `init()` 中注册跨模块集成(任务 Handler、推送内置事件、域事件监听器、任务完成钩子)。统一通过 `internal/bootstrap` 在 `internal/cmd` 入口显式装配。
@@ -64,23 +51,6 @@
- 核心业务模块(如 `oauth`、`user`)禁止直接 `import` `internal/apps/admin/push` 或 `custom_events` 触发通知;应通过 `internal/listener` 发射域事件,由 push 模块在 bootstrap 阶段订阅。
- 编写依赖任务注册或推送事件同步的测试时,必须在测试 setup 中显式调用 `bootstrap.RegisterTasks()`、`bootstrap.RegisterPushDomainEvents()` 等,不得依赖 `init()` 副作用。
- API 错误响应必须通过 `response.Abort*` 中断请求,由 `ErrorHandlerMiddleware` 统一写出 JSON;禁止 `c.JSON(http.StatusOK, response.Err(...))` 及 Handler 直接 `c.JSON(status, response.Err(...))`。
1. **设计先行**:
* 开发新功能或重要特性时,必须在 `docs/design/` 下创建/更新对应的设计文档,理清架构与核心决策。
* 新增的设计文档应同步更新至 `docs/design/architecture.md` 及在 `docs/config.ts` 中注册侧边栏路由。
* 若实现内容超出产品边界,必须先修改设计文档,再编码实现。
3. **开发计划与交接**:
* 正在进行的开发计划或 AI 接手交接发生变化时,在 `docs/plan/` 下更新对应的开发计划或接手文档,并使用相应模板初始化。
4. **文档与变更日志**:
* 当相关内容发生变化时,同步更新对应的**中文文档**(不要同步英文文档)。
* 代码或配置变更完成后,必须在 [`docs/changelog/index.md`](./docs/changelog/index.md) 的 `[Unreleased]` 区块补充对应变更条目。
* **纯文档变更(如 `docs/` 下的 Markdown 文档、README 等)不需要写入 changelog。**
* 更新 changelog 时遵循以下书写规则:
1. 合并重复或相近的变更,不按提交逐条罗列。
2. 不记录格式化、临时调试、无关重构等对用户无意义的变更。
3. 使用用户可理解的表述,不描述内部实现细节。
4. 每条均使用完整中文句子,并尽量说明修复或优化的内容及其带来的效果。
5. 仅基于实际变更撰写,不编造提交或代码中不存在的信息。
6. 不记录 Token、密钥、私有地址等敏感信息;没有内容的分类可以省略。
## 项目介绍
@@ -283,7 +253,7 @@ func doSomething(c *gin.Context) { response.AbortBadRequest(c, "...") }
路由与模块:
- 仅在 `internal/router/router.go` 中作为统一高层入口进行路由分发委派,不允许在 `router.go` 中直接挂载业务 Handler。
- 关于所有的路由归属划分、接口开发隔离防线以及详细的注册和开发步骤,请直接阅读并严格遵循 [new-api](.agent/skills/new-api/SKILL.md) 技能。
- 关于所有的路由归属划分、接口开发隔离防线以及详细的注册和开发步骤,请直接阅读并严格遵循 [new-api](file:///Users/ryan/DEV/Go/Wavelet/.claude/skills/new-api/SKILL.md) 技能。
应用装配与跨模块集成:
@@ -315,7 +285,7 @@ func doSomething(c *gin.Context) { response.AbortBadRequest(c, "...") }
在进行任何 Next.js 工作之前,请在 `node_modules/next/dist/docs/` 中找到并阅读相关文档。您的训练数据已过时 —— 这些文档是唯一的真理来源。
请直接查看并参考项目提供的示例和 Demo 代码:[frontend/app/(main)/admin/demo](frontend/app/(main)/admin/demo)。
请直接查看并参考项目提供的示例和 Demo 代码:[frontend/app/(main)/admin/demo](file:///Users/ryan/DEV/Go/Wavelet/frontend/app/(main)/admin/demo)。
样式规范:
@@ -360,5 +330,5 @@ frontend/lib/services/<service-name>/
```
- 服务类继承 `BaseService`,定义 `basePath`,并暴露有类型的静态方法。
- **防止回调 `this` 上下文丢失(核心规范)**:在传递服务类的静态方法作为组件事件回调(如 `onClick`)或 React Query 的 `mutationFn`/`queryFn` 时,**禁止直接传递静态方法引用**(如 `mutationFn: DnsAccountService.create`),必须使用箭头函数包裹以防止 `this` 上下文丢失导致运行时崩溃(如 `mutationFn: (payload) => DnsAccountService.create(payload)`)。
- 在 `frontend/lib/services/index.ts` 中注册新服务。
+1 -1
View File
@@ -1 +1 @@
AGENTS.md
Agents.md
+6 -33
View File
@@ -1,4 +1,4 @@
.PHONY: swagger license license-check prettier build-embedded build-test cross-build code-check build-backend build-frontend build-agent build-relay build-flared build-all
.PHONY: swagger license license-check build-embedded build-test cross-build code-check prettier
VERSION ?= dev
BUILD_DATE ?= $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
@@ -14,13 +14,9 @@ license-check:
scripts/update_go_license.sh --check
prettier:
@echo "==> Formatting backend Go source and removing unused imports..."
@command -v goimports >/dev/null 2>&1 || { \
echo "goimports not found, installing..."; \
go install golang.org/x/tools/cmd/goimports@latest; \
}
goimports -w $$(find . -type f -name '*.go' -not -path './.git/*' -not -path './frontend/*')
@echo "==> Formatting frontend source and removing unused imports..."
@echo "==> Formatting backend Go source..."
gofmt -w $$(find . -type f -name '*.go' -not -path './.git/*' -not -path './frontend/*')
@echo "==> Formatting frontend source..."
cd frontend && pnpm format
build-embedded:
@@ -34,7 +30,7 @@ build-embedded:
go build \
-tags embed_frontend \
-ldflags "-s -w -X '$(MODULE)/internal/buildinfo.Version=$(VERSION)' -X '$(MODULE)/internal/buildinfo.BuildTime=$(BUILD_DATE)'" \
-o bin/openflare-server \
-o bin/wavelet \
main.go
code-check:
@@ -45,32 +41,9 @@ build-backend:
@echo "==> Building backend version=$(VERSION) build_date=$(BUILD_DATE)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/buildinfo.Version=$(VERSION)' -X '$(MODULE)/internal/buildinfo.BuildTime=$(BUILD_DATE)'" \
-o bin/openflare-server \
-o bin/wavelet \
main.go
build-agent:
@echo "==> Building agent version=$(VERSION)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/apps/agent/config.Version=$(VERSION)'" \
-o bin/openflare-agent \
cmd/agent/main.go
build-relay:
@echo "==> Building relay version=$(VERSION)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/apps/relay/config.Version=$(VERSION)'" \
-o bin/openflare-relay \
cmd/relay/main.go
build-flared:
@echo "==> Building flared version=$(VERSION)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/apps/flared/config.Version=$(VERSION)'" \
-o bin/flared \
cmd/flared/main.go
build-all: build-backend build-agent build-relay build-flared
build-frontend:
@echo "==> Building frontend version=$(VERSION) build_date=$(BUILD_DATE)..."
cd frontend && \
+4 -4
View File
@@ -1,9 +1,9 @@
OpenFlare
Wavelet
This product includes software derived from Wavelet.
This product includes software derived from LinuxDO Credit.
Wavelet:
Copyright 2025 Arctel.net
LinuxDO Credit:
Copyright 2025 linux.do
Licensed under the Apache License, Version 2.0.
This distribution includes modifications by Arctel.net.
-225
View File
@@ -1,225 +0,0 @@
<div align="center">
# OpenFlare
**[English](./README.en.md) | [📖 中文](./README.md)**
OpenFlare is an open-source CDN orchestration and edge security platform. It supports reverse proxies, centralized configuration synchronization, secure intranet penetration (Tunnels), dynamic WAF protection, and anti-CC challenges.
</div>
<p align="center">
<a href="https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/LICENSE">
<img src="https://img.shields.io/github/license/Rain-kl/OpenFlare?color=brightgreen" alt="license">
</a>
<a href="https://github.com/Rain-kl/OpenFlare/releases/latest">
<img src="https://img.shields.io/github/v/release/Rain-kl/OpenFlare?color=brightgreen&include_prereleases" alt="release">
</a>
<a href="https://github.com/Rain-kl/OpenFlare/pkgs/container/openflare">
<img src="https://img.shields.io/badge/GHCR-ghcr.io%2Frain--kl%2Fopenflare-brightgreen" alt="ghcr">
</a>
</p>
> [!WARNING]
> After logging in for the first time with the `root` user, make sure to change the default password `123456`.
>
> The BETA version is a temporary product for the development and testing phase. It may contain unknown issues and should not be used in production environments.
## Documentation
**https://open-flare.pages.dev**
Quick links:
* [Quick Start](https://open-flare.pages.dev/en/guide/quick-start)
* [Deployment Guide](https://open-flare.pages.dev/en/deployment/deployment)
* [Configuration Reference](https://open-flare.pages.dev/reference/configuration)
* [System Design](https://open-flare.pages.dev/design/)
## Core Features
* **Reverse Proxy Management**: Website rules as the aggregation boundary, supporting multi-domain binding and multi-upstream load balancing with unified management of all OpenResty node configurations.
* **Immutable Config Version Control**: Full-snapshot publish model based on version numbers (`YYYYMMDD-NNN`), with pre-publish diff preview, a single globally active version, and one-click sub-second rollback.
* **Secure Intranet Penetration (Tunnels)**: An open-source alternative to Cloudflare Tunnels. Securely expose local intranet Web services to the public network via Relay and OpenFlared clients — no public IP or open inbound ports required.
* **Edge WAF Safety Protection**: Provides global and custom rule groups, supporting manual/automatic/subscription IP groups, MaxMind GeoIP country-level access control, Checksum-based differential IP group sync (no Nginx reload), and custom block responses.
* **Anti-CC & Human-Machine Challenge (PoW)**: Built-in high-performance client-side cryptographic Proof of Work challenges (similar to Turnstile) to block and intercept botnets and scrapers at the gateway edge in seconds.
* **Pages Static Hosting**: Upload pre-built ZIP packages directly; edge Agents pull and serve them via local OpenResty, with SPA Fallback and built-in API reverse proxy configuration.
* **Automated TLS Certificate Management**: Supports dynamic certificate upload, automatic multi-domain certificate matching and binding, and ACME-based automatic issuance and renewal via Let's Encrypt.
* **Uptime Kuma Monitoring Sync**: Integrates with Uptime Kuma to automatically sync the monitoring site list using differential updates, providing real-time awareness of node availability and service health.
* **SSO Single Sign-On**: Supports GitHub OAuth and standard OIDC protocol for seamless integration with enterprise identity providers.
* **Unified Observability**: Aggregates node request metrics, real-time access log details, host/Nginx resource snapshots, health events, and a re-upload buffer for network fluctuations.
## Quick Start
### 1. Launch Server
```yaml
services:
openflare:
image: ghcr.io/rain-kl/openflare:latest
restart: unless-stopped
env_file: .env
environment:
TZ: ${TZ:-Asia/Shanghai}
ports:
- "3000:3000"
volumes:
- openflare_uploads:/app/uploads
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-openflare}
POSTGRES_USER: ${DB_USERNAME:-openflare}
POSTGRES_PASSWORD: ${DB_PASSWORD:-replace-with-strong-password}
volumes:
- openflare_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-openflare} -d ${DB_NAME:-openflare}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- openflare_redis_data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
clickhouse:
image: clickhouse/clickhouse-server:25.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: ${CLICKHOUSE_NAME:-openflare}
CLICKHOUSE_USER: ${CLICKHOUSE_USERNAME:-default}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
TZ: ${TZ:-Asia/Shanghai}
volumes:
- openflare_clickhouse_data:/var/lib/clickhouse
healthcheck:
test: ["CMD", "clickhouse-client", "--user", "${CLICKHOUSE_USERNAME:-default}", "--password", "${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
openflare_uploads:
openflare_postgres_data:
openflare_redis_data:
openflare_clickhouse_data:
```
```bash
docker compose up -d
```
Access at: `http://localhost:3000`
Default credentials:
* Username: `root`
* Password: `123456`
### 2. Install Agent
Before installing an Agent, please install OpenResty on the target node first, or use the Agent Docker image with OpenResty built-in.
You can copy the installation command from **Node Management -> Details -> Node Info -> Node Token & Deployment** in the control panel, or directly use the scripts below:
#### Docker Deployment
For Docker deployment, you can directly run the Agent image:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
#### Local Installation
Using `discovery_token` to register:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
Using node-specific `agent_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
The installation script defaults to `/opt/openflare-agent`, creates a `openflare-agent.service`, automatically searches for `openresty`, and can be executed repeatedly to reinstall or upgrade the Agent.
### 3. Uninstall Agent
To completely uninstall the Agent and clear local data, run:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash
```
The uninstallation script will stop and remove the `openflare-agent.service`, and delete the entire `/opt/openflare-agent` directory. It will not delete the local OpenResty installation.
### 4. Publish Your First Configuration
1. Log in to the management panel and add a reverse proxy rule.
2. View the preview or change summary before publishing.
3. Activate the new version.
4. Agents will receive the configuration and apply it via WebSocket notification or subsequent heartbeats.
The version number format is fixed as `YYYYMMDD-NNN`. Historical versions are immutable, and rollback is achieved by reactivating an older version.
## UI Preview
### Dashboard Overview
![OpenFlare dashboard overview](./docs/assets/readme/dashboard-overview.png)
### Node Details
![OpenFlare node detail](./docs/assets/readme/node-detail.png)
### Proxy Configuration
![OpenFlare version release](./docs/assets/readme/proxy-route-detail.png)
## License
This project is licensed under [Apache License 2.0](./LICENSE).
## Star History
<a href="https://www.star-history.com/?repos=Rain-kl%2FOpenFlare&type=date&legend=bottom-right">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&legend=top-left" />
</picture>
</a>
+324 -174
View File
@@ -1,202 +1,352 @@
<div align="center">
# wavelet
# OpenFlare
🚀 A modern, production-ready full-stack boilerplate for building scalable web applications
**[📖 中文](./README.md) | [English](./README.en.md)**
[中文](./README_zh.md)
OpenFlare 是开源 CDN 编排与边缘安全平台。它支持反向代理、集中式配置同步、内网穿透(Tunnels)、动态 WAF 防护以及防 CC 挑战。
[![License: Apache2.0](https://img.shields.io/badge/License-Apache2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Go Version](https://img.shields.io/badge/Go-1.25+-blue.svg)](https://golang.org/)
[![Next.js](https://img.shields.io/badge/Next.js-16-black.svg)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19-blue.svg)](https://reactjs.org/)
</div>
## 📖 Introduction
<p align="center">
<a href="https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/LICENSE">
<img src="https://img.shields.io/github/license/Rain-kl/OpenFlare?color=brightgreen" alt="license">
</a>
<a href="https://github.com/Rain-kl/OpenFlare/releases/latest">
<img src="https://img.shields.io/github/v/release/Rain-kl/OpenFlare?color=brightgreen&include_prereleases" alt="release">
</a>
<a href="https://github.com/Rain-kl/OpenFlare/pkgs/container/openflare">
<img src="https://img.shields.io/badge/GHCR-ghcr.io%2Frain--kl%2Fopenflare-brightgreen" alt="ghcr">
</a>
</p>
**wavelet** is a generic, production-ready full-stack boilerplate built with **Go (Gin + GORM)** on the backend and **Next.js (App Router + Shadcn UI)** on the frontend. It ships with everything you need to bootstrap a modern SaaS, internal tool, or developer platform — without the boilerplate headaches.
> [!WARNING]
> 使用 `admin` 用户初次登录系统后,务必修改默认密码 `12345678`。
>
> BETA 版本为开发测试阶段的临时产物,可能存在未知问题,请勿在生产环境使用。
The project was designed from the ground up to be **framework-first and business-agnostic**: plug in your own domain logic while reusing the battle-tested infrastructure that comes out of the box.
## 文档
### ✨ Key Features
**https://open-flare.pages.dev**
- 🔐 **Multi-auth System** — Local password login/registration + pluggable OIDC/OAuth2 providers (supports multiple auth sources simultaneously)
- 🗝️ **Personal Access Tokens** — API key management for programmatic access; supports `Authorization: Bearer` and `X-Access-Token` headers
- 👤 **User Management** — Admin panel for listing, searching, filtering, enabling/disabling user accounts
- ⚙️ **Dynamic System Config** — Key-value system configuration management with live reload, controllable from the admin UI
- 📋 **Async Task Queue** — Background job processing with [Asynq](https://github.com/hibiken/asynq) (Redis-backed), including a scheduling dashboard
- 📁 **S3 File Storage** — Unified file upload/download via S3-compatible APIs with local disk cache
- 📊 **Observability** — Structured logging (Zap) + distributed tracing (OpenTelemetry)
- 🎨 **Modern UI** — Responsive, dark-mode-ready design system built with Tailwind CSS 4 and Shadcn UI
- 📖 **Built-in Documentation** — Integrated docs portal with usage guides, API reference, privacy policy, and terms of service
常用入口:
## 🏗️ Architecture Overview
* [快速开始](https://open-flare.pages.dev/guide/quick-start)
* [部署说明](https://open-flare.pages.dev/deployment/deployment)
* [配置项参考](https://open-flare.pages.dev/reference/configuration)
* [系统设计](https://open-flare.pages.dev/design/)
```
┌─────────────────┐ ┌─────────────────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ Database │
│ (Next.js) │◄──►│ (Go) │◄──►│ (PostgreSQL) │
│ │ │ │ │ │
│ • React 19 │ │ • Gin HTTP Framework │ │ • PostgreSQL │
│ • TypeScript │ │ • GORM ORM │ │ • Redis Cache │
│ • Tailwind 4 │ │ • Multi-provider Auth │ │ │
│ • Shadcn UI │ │ • AccessToken Middleware │ │ │
│ │ │ • Asynq Task Queue │ │ │
│ │ │ • OpenTelemetry Tracing │ │ │
│ │ │ • Swagger API Docs │ │ │
└─────────────────┘ └─────────────────────────────┘ └─────────────────┘
│
┌──────────┴──────────┐
│ Multi-Process CLI │
│ (Cobra + Viper) │
│ • api (HTTP) │
│ • worker (Queue) │
│ • scheduler(Cron) │
└─────────────────────┘
```
## 核心能力
## 🛠️ Tech Stack
* **反代配置管理**:以网站规则为聚合边界,支持多域名绑定与多上游负载均衡,统一管理所有 OpenResty 节点的反代配置。
* **安全内网穿透(Tunnels)**:开源版的 Cloudflare Tunnels。无须公网 IP 或暴露入向端口,通过 Relay 中继节点与 OpenFlared 客户端安全反向穿透内网 Web 服务至公网。
* **边缘 WAF 安全防护**:提供全局与自定义规则组,支持手动/自动/订阅型 IP 组、MaxMind GeoIP 国家级地域准入、IP 组成员 Checksum 差分同步(无需 Nginx 重载)以及自定义拦截响应。
* **防 CC 与人机挑战(PoW)**:内置高性能客户端密码学 Proof of Work 挑战(类似 Turnstile),在网关边缘秒级拦截并阻断僵尸网络与爬虫。
* **Pages 静态托管**:支持上传或从受限 Remote URL、公开 GitHub Release asset 同步预构建产物;GitHub latest 可定时检查并可选自动发布。所有来源统一生成不可变部署,由边缘 Agent 拉取并通过 OpenResty 本地提供服务,支持回滚、SPA Fallback 与 API 反向代理。
* **TLS 证书自动化**:支持证书动态上传、多域名证书自动匹配绑定,以及通过 ACME 协议向 Let's Encrypt 自动申请与续期证书。
* **Uptime Kuma 监控同步**:与 Uptime Kuma 集成,自动差分同步监控站点列表,实时感知节点存活与服务可用状态。
* **SSO 单点登录**:支持 GitHub OAuth 与标准 OIDC 协议,无缝接入企业身份提供商实现统一登录。
* **统一观测**:聚合节点请求指标、实时访问日志明细、宿主机与 Nginx 资源快照、健康事件以及网络波动补传缓冲。
### Backend
- **[Go 1.25+](https://go.dev/doc)** — Primary language
- **[Gin](https://github.com/gin-gonic/gin)** — HTTP web framework
- **[GORM](https://github.com/go-gorm/gorm)** — ORM with PostgreSQL & ClickHouse support
- **[Redis](https://github.com/redis/redis)** — Cache, session store, and task queue backend
- **[Asynq](https://github.com/hibiken/asynq)** — Distributed task queue (Redis-backed)
- **[Cobra + Viper](https://github.com/spf13/cobra)** — CLI entrypoint and configuration management
- **[OpenTelemetry](https://opentelemetry.io)** — Distributed tracing and observability
- **[Zap](https://github.com/uber-go/zap)** — Structured, high-performance logging
- **[Swagger (Swaggo)](https://github.com/swaggo/swag)** — Auto-generated API documentation
- **[AWS SDK v2](https://github.com/aws/aws-sdk-go-v2)** — S3-compatible file storage
- **[Snowflake](https://github.com/bwmarrin/snowflake)** — Distributed ID generation
## 界面预览
### Frontend
- **[Next.js 16](https://github.com/vercel/next.js)** — React framework with App Router
- **[React 19](https://github.com/facebook/react)** — UI library
- **[TypeScript](https://github.com/microsoft/TypeScript)** — Type safety
- **[Tailwind CSS 4](https://github.com/tailwindlabs/tailwindcss)** — Utility-first styling
- **[Shadcn UI](https://github.com/shadcn-ui/ui)** — Accessible, composable component library
- **[Lucide Icons](https://github.com/lucide-icons/lucide)** — Icon library
### 仪表盘总览
## 📋 Requirements
![OpenFlare dashboard overview](./docs/assets/readme/dashboard-overview.png)
- **Go** >= 1.25
- **Node.js** >= 18.0
- **PostgreSQL** >= 14
- **Redis** >= 6.0
- **pnpm** >= 8.0 (recommended)
### 节点详情
## 🚀 Quick Start
![OpenFlare node detail](./docs/assets/readme/node-detail.png)
### 配置新增
![OpenFlare version release](./docs/assets/readme/proxy-route-detail.png)
## 快速开始
### 1. 启动 Server
使用 docker-compose
### 1. Clone the Repository
```bash
# 下载环境变量模板并创建 .env 文件
curl -o .env.example https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/.env.example
cp .env.example .env
# ClickHouse 服务端:curl performance.xml 到 ./config/clickhouse,并以单文件方式挂载到 config.d
mkdir -p ./config/clickhouse
curl -fsSL -o ./config/clickhouse/performance.xml \
https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/config/clickhouse/performance.xml
git clone https://github.com/Rain-kl/Wavelet.git refreshing
cd refreshing
```
```yaml
services:
openflare:
image: ghcr.io/rain-kl/openflare:latest
restart: unless-stopped
env_file: .env
environment:
TZ: ${TZ:-Asia/Shanghai}
ports:
- "3000:3000"
volumes:
- openflare_uploads:/app/uploads
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-openflare}
POSTGRES_USER: ${DB_USERNAME:-openflare}
POSTGRES_PASSWORD: ${DB_PASSWORD:-replace-with-strong-password}
volumes:
- openflare_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-openflare} -d ${DB_NAME:-openflare}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- openflare_redis_data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
clickhouse:
image: clickhouse/clickhouse-server:25.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: ${CLICKHOUSE_NAME:-openflare}
CLICKHOUSE_USER: ${CLICKHOUSE_USERNAME:-default}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
TZ: ${TZ:-Asia/Shanghai}
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- openflare_clickhouse_data:/var/lib/clickhouse
- ./config/clickhouse/performance.xml:/etc/clickhouse-server/config.d/performance.xml:ro
healthcheck:
test: ["CMD", "clickhouse-client", "--user", "${CLICKHOUSE_USERNAME:-default}", "--password", "${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
openflare_uploads:
openflare_postgres_data:
openflare_redis_data:
openflare_clickhouse_data:
```
详细部署说明见 [部署文档](https://open-flare.pages.dev/deployment/deployment)。
访问地址:`http://localhost:3000`
默认账号:
* 用户名:`admin`
* 密码:`12345678`
### 2. 安装 Agent
安装 Agent 前请先在节点上安装 OpenResty,或改用内置 OpenResty 的 Agent Docker 镜像。
你可以在控制面板的节点管理->详情->节点信息->节点标识与部署复制安装命令,或直接使用下面的脚本:
#### Docker 部署
Docker 部署可直接运行 Agent 镜像:
### 2. Configure Environment
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-v openflare-agent-pages:/data/var/lib/openflare/pages \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
cp config.example.yaml config.yaml
```
## 开源协议
Edit `config.yaml` to configure your database and Redis. OIDC auth sources are configured at runtime in the admin settings page.
本项目采用 [Apache License 2.0](./LICENSE) 开源。
### 3. Initialize Database
## Star History
```bash
# Start local dependencies (PostgreSQL + Redis)
docker compose up -d
<a href="https://www.star-history.com/?repos=Rain-kl%2FOpenFlare&type=date&legend=bottom-right">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=Rain-kl/OpenFlare&type=date&legend=top-left" />
</picture>
</a>
# Optional: also start ClickHouse
docker compose --profile clickhouse up -d
# If you use an external PostgreSQL instance instead of Docker, create the database manually
createdb -h <host> -p 5432 -U postgres refreshing
# Database schema is auto-migrated on first startup
```
### 4. Start the Backend
```bash
# Install Go dependencies
go mod tidy
# Generate Swagger API documentation
make swagger
# Start the HTTP API server
go run main.go api
```
> The backend also supports separate `scheduler` and `worker` processes for async task processing:
> ```bash
> go run main.go scheduler # Cron job scheduler
> go run main.go worker # Asynq task worker
> ```
### 5. Start the Frontend
```bash
cd frontend
# Install dependencies
pnpm install
# Start dev server (Turbopack)
pnpm dev
```
### 6. Access the Application
| Service | URL |
|---------|-----|
| Frontend | http://localhost:3000 |
| Swagger API Docs | http://localhost:8000/swagger/index.html |
| Health Check | http://localhost:8000/api/health |
## ⚙️ Configuration
Key configuration options (see `config.example.yaml` for the full reference):
| Option | Description | Example |
|--------|-------------|---------|
| `app.addr` | Backend listen address | `:8000` |
| `database.host` | PostgreSQL host | `127.0.0.1` |
| `database.database` | Database name | `refreshing` |
| `redis.host` | Redis host | `127.0.0.1` |
| `storage.endpoint` | S3-compatible endpoint | `s3.amazonaws.com` |
## 🔧 Development Guide
### Backend
```bash
# Run API server
go run main.go api
# Run task scheduler
go run main.go scheduler
# Run async worker
go run main.go worker
# Regenerate Swagger docs (required after controller changes)
make swagger
# Format & vet code
make tidy
```
### Frontend
```bash
cd frontend
# Development mode (Turbopack)
pnpm dev
# Production build
pnpm build
# Start production server
pnpm start
# Lint & format
pnpm lint
pnpm format
```
## 📁 Project Structure
```
wavelet/
├── main.go # Entry point (delegates to internal/cmd)
├── config.example.yaml # Configuration template
├── Makefile # Common commands (swagger, tidy, license, cross-build)
├── docker/ # Docker image build files (integrated/frontend/backend)
├── docs/ # Swagger auto-generated docs
├── frontend/ # Next.js frontend application
│ ├── app/ # App Router pages
│ ├── components/ # React components (ui, common, layout)
│ ├── lib/services/ # API service layer
│ └── types/ # TypeScript type definitions
└── internal/ # Go backend (private)
├── cmd/ # CLI commands (api, scheduler, worker)
├── apps/ # Business modules (oauth, user, admin, upload)
├── model/ # GORM entities and business methods
├── router/ # HTTP route registration
├── task/ # Async task definitions and workers
├── db/ # Database and Redis initialization
├── storage/ # S3 file storage abstraction
└── common/ # Shared utilities and response helpers
```
## 📚 API Documentation
Swagger API documentation is auto-generated and available once the backend is running:
```
http://localhost:8000/swagger/index.html
```
The built-in frontend docs portal at `/docs` includes:
- **Usage Guide** — Step-by-step walkthrough for getting started
- **API Reference** — Detailed interface documentation
- **Privacy Policy** — Template privacy policy (customize as needed)
- **Terms of Service** — Template terms of service
## 🧪 Testing
```bash
# Backend tests
go test ./...
# Frontend lint
cd frontend && pnpm lint
```
## 🚀 Deployment
### Cross-platform Binary
Build static binaries for all 6 targets (Linux / macOS / Windows × amd64 / arm64) with a single command.
The compiled frontend is embedded in every binary — no separate deployment needed.
**Prerequisites:** Docker with BuildKit enabled (Docker 23+ defaults to on).
```bash
# Build all 6 binaries → ./bin/
make cross-build
# Stamp a release version
make cross-build VERSION=v1.2.3
# Build only a specific OS (both architectures)
make cross-build GOOS=linux
make cross-build GOOS=darwin
make cross-build GOOS=windows
# Build only a specific architecture (all OSes)
make cross-build GOARCH=amd64
make cross-build GOARCH=arm64
# Combine filters — single binary
make cross-build GOOS=linux GOARCH=arm64
make cross-build GOOS=darwin GOARCH=amd64 VERSION=v1.2.3
```
Output files in `./bin/`:
| File | Platform |
|------|----------|
| `wavelet_linux_amd64` | Linux x86-64 |
| `wavelet_linux_arm64` | Linux ARM64 |
| `wavelet_darwin_amd64` | macOS Intel |
| `wavelet_darwin_arm64` | macOS Apple Silicon |
| `wavelet_windows_amd64.exe` | Windows x86-64 |
| `wavelet_windows_arm64.exe` | Windows ARM64 |
> The version string is accessible at runtime via `wavelet --version`.
### Docker
```bash
# Build image
docker build -t refreshing .
# Run (pass your config as a volume mount)
docker run -d -p 8000:8000 \
-v $(pwd)/config.yaml:/app/config.yaml \
refreshing api
```
### Production
1. Build the frontend:
```bash
cd frontend && pnpm build
```
2. Compile the backend:
```bash
go build -o refreshing main.go
```
3. Configure `config.yaml` for production.
4. Start services:
```bash
./refreshing api # HTTP API
./refreshing scheduler # Cron scheduler (optional)
./refreshing worker # Task worker (optional)
```
## 🤝 Contributing
We welcome contributions! Please read the following before submitting code:
- [Contributing Guidelines](CONTRIBUTING.md)
- [Code of Conduct](CODE_OF_CONDUCT.md)
- [Contributor License Agreement](CLA.md)
### Workflow
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/your-feature`)
3. Commit your changes (`git commit -am 'Add your feature'`)
4. Push to the branch (`git push origin feature/your-feature`)
5. Open a Pull Request
## 📄 License
This project is licensed under the [Apache 2.0 License](LICENSE).
+352
View File
@@ -0,0 +1,352 @@
# wavelet
🚀 现代化、生产就绪的全栈应用脚手架
[English](./README.md)
[![License: Apache2.0](https://img.shields.io/badge/License-Apache2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Go Version](https://img.shields.io/badge/Go-1.25+-blue.svg)](https://golang.org/)
[![Next.js](https://img.shields.io/badge/Next.js-16-black.svg)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19-blue.svg)](https://reactjs.org/)
## 📖 项目简介
**wavelet** 是一个通用型、生产就绪的现代全栈脚手架,后端采用 **Go(Gin + GORM)**,前端采用 **Next.js(App Router + Shadcn UI)**。项目开箱即用,内置构建现代 SaaS、内部工具或开发者平台所需的核心基础设施。
项目设计理念是 **框架优先、业务中立**:您可以在沿用经过实战检验的底层基础设施的同时,自由接入自己的业务逻辑。
### ✨ 主要特性
- 🔐 **多认证方式** — 本地账号密码登录/注册 + 可插拔 OIDC/OAuth2 认证源(支持同时配置多个认证源)
- 🗝️ **个人访问令牌** — API Key 管理,支持程序化接口访问;兼容 `Authorization: Bearer` 和 `X-Access-Token` 请求头
- 👤 **用户管理** — 管理后台提供用户列表、搜索筛选、启用/禁用账号等功能
- ⚙️ **动态系统配置** — KV 系统配置管理,支持实时变更,可通过管理后台界面直接操作
- 📋 **异步任务队列** — 基于 [Asynq](https://github.com/hibiken/asynq)(Redis 驱动)的后台任务处理系统,含任务调度面板
- 📁 **S3 文件存储** — 通过 S3 兼容 API 统一处理文件上传/下载,支持本地磁盘缓存
- 📊 **可观测性** — 结构化日志(Zap)+ 分布式链路追踪(OpenTelemetry)
- 🎨 **现代化 UI** — 基于 Tailwind CSS 4 和 Shadcn UI 构建的响应式、支持深色模式的设计系统
- 📖 **内置文档中心** — 集成文档门户,包含使用指南、接口文档、隐私政策和服务条款
## 🏗️ 架构概览
```
┌─────────────────┐ ┌─────────────────────────────┐ ┌─────────────────┐
│ 前端 │ │ 后端 │ │ 数据库 │
│ (Next.js) │◄──►│ (Go) │◄──►│ (PostgreSQL) │
│ │ │ │ │ │
│ • React 19 │ │ • Gin HTTP 框架 │ │ • PostgreSQL │
│ • TypeScript │ │ • GORM ORM │ │ • Redis 缓存 │
│ • Tailwind 4 │ │ • 多认证源适配 │ │ │
│ • Shadcn UI │ │ • AccessToken 中间件 │ │ │
│ │ │ • Asynq 任务队列 │ │ │
│ │ │ • OpenTelemetry 链路追踪 │ │ │
│ │ │ • Swagger 接口文档 │ │ │
└─────────────────┘ └─────────────────────────────┘ └─────────────────┘
│
┌──────────┴──────────┐
│ 多进程 CLI 入口 │
│ (Cobra + Viper) │
│ • api (HTTP) │
│ • worker (队列) │
│ • scheduler(定时) │
└─────────────────────┘
```
## 🛠️ 技术栈
### 后端
- **[Go 1.25+](https://go.dev/doc)** — 主语言
- **[Gin](https://github.com/gin-gonic/gin)** — HTTP Web 框架
- **[GORM](https://github.com/go-gorm/gorm)** — ORM,支持 PostgreSQL 和 ClickHouse
- **[Redis](https://github.com/redis/redis)** — 缓存、Session 存储、任务队列后端
- **[Asynq](https://github.com/hibiken/asynq)** — 分布式任务队列(Redis 驱动)
- **[Cobra + Viper](https://github.com/spf13/cobra)** — CLI 入口 + 配置管理
- **[OpenTelemetry](https://opentelemetry.io)** — 分布式链路追踪与可观测性
- **[Zap](https://github.com/uber-go/zap)** — 结构化高性能日志
- **[Swagger (Swaggo)](https://github.com/swaggo/swag)** — 自动生成 API 文档
- **[AWS SDK v2](https://github.com/aws/aws-sdk-go-v2)** — S3 兼容文件存储
- **[Snowflake](https://github.com/bwmarrin/snowflake)** — 分布式 ID 生成
### 前端
- **[Next.js 16](https://github.com/vercel/next.js)** — React 框架(App Router)
- **[React 19](https://github.com/facebook/react)** — UI 库
- **[TypeScript](https://github.com/microsoft/TypeScript)** — 类型安全
- **[Tailwind CSS 4](https://github.com/tailwindlabs/tailwindcss)** — 原子化 CSS 框架
- **[Shadcn UI](https://github.com/shadcn-ui/ui)** — 可访问、可组合的组件库
- **[Lucide Icons](https://github.com/lucide-icons/lucide)** — 图标库
## 📋 环境要求
- **Go** >= 1.25
- **Node.js** >= 18.0
- **PostgreSQL** >= 14
- **Redis** >= 6.0
- **pnpm** >= 8.0(推荐)
## 🚀 快速开始
### 1. 克隆仓库
```bash
git clone https://github.com/Rain-kl/Wavelet.git refreshing
cd refreshing
```
### 2. 配置环境
```bash
cp config.example.yaml config.yaml
```
编辑 `config.yaml`,配置数据库和 Redis。OIDC 认证源统一在管理后台的系统设置页面运行时配置。
### 3. 初始化数据库
```bash
# 启动本地依赖服务(PostgreSQL + Redis)
docker compose up -d
# 可选:同时启动 ClickHouse
docker compose --profile clickhouse up -d
# 如果使用外部 PostgreSQL,而不是 Docker 内置服务,则手动创建数据库
createdb -h <主机> -p 5432 -U postgres refreshing
# 数据库表结构在首次启动时自动迁移,无需手动执行
```
### 4. 启动后端
```bash
# 安装 Go 依赖
go mod tidy
# 生成 Swagger 接口文档
make swagger
# 启动 HTTP API 服务器
go run main.go api
```
> 后端也支持独立运行 `scheduler` 和 `worker` 进程来处理异步任务:
> ```bash
> go run main.go scheduler # 定时任务调度器
> go run main.go worker # Asynq 任务处理工作进程
> ```
### 5. 启动前端
```bash
cd frontend
# 安装依赖
pnpm install
# 启动开发服务器(Turbopack)
pnpm dev
```
### 6. 访问应用
| 服务 | 地址 |
|------|------|
| 前端界面 | http://localhost:3000 |
| Swagger 接口文档 | http://localhost:8000/swagger/index.html |
| 健康检查 | http://localhost:8000/api/health |
## ⚙️ 配置说明
主要配置项(完整说明请参考 `config.example.yaml`):
| 配置项 | 说明 | 示例 |
|--------|------|------|
| `app.addr` | 后端监听地址 | `:8000` |
| `database.host` | PostgreSQL 主机 | `127.0.0.1` |
| `database.database` | 数据库名称 | `refreshing` |
| `redis.host` | Redis 主机 | `127.0.0.1` |
| `storage.endpoint` | S3 兼容存储端点 | `s3.amazonaws.com` |
## 🔧 开发指南
### 后端
```bash
# 运行 API 服务器
go run main.go api
# 运行定时任务调度器
go run main.go scheduler
# 运行异步任务工作进程
go run main.go worker
# 修改 Controller 后重新生成 Swagger 文档(必须执行)
make swagger
# 代码格式化与检查
make tidy
```
### 前端
```bash
cd frontend
# 开发模式(Turbopack)
pnpm dev
# 构建生产版本
pnpm build
# 启动生产服务器
pnpm start
# 代码 Lint 和格式化
pnpm lint
pnpm format
```
## 📁 项目结构
```
wavelet/
├── main.go # 程序入口(委托给 internal/cmd)
├── config.example.yaml # 配置模板
├── Makefile # 常用命令(swagger、tidy、license、cross-build)
├── docker/ # Docker 镜像构建文件(集成/前端/后端)
├── docs/ # Swagger 自动生成文档
├── frontend/ # Next.js 前端应用
│ ├── app/ # App Router 页面
│ ├── components/ # React 组件(ui、common、layout)
│ ├── lib/services/ # API 服务层
│ └── types/ # TypeScript 类型定义
└── internal/ # Go 后端(private)
├── cmd/ # CLI 命令(api、scheduler、worker)
├── apps/ # 业务模块(oauth、user、admin、upload)
├── model/ # GORM 实体与业务方法
├── router/ # HTTP 路由注册
├── task/ # 异步任务定义与工作进程
├── db/ # 数据库与 Redis 初始化
├── storage/ # S3 文件存储抽象层
└── common/ # 公共工具与响应封装
```
## 📚 接口文档
Swagger 接口文档在后端启动后自动可用:
```
http://localhost:8000/swagger/index.html
```
前端文档中心(路径 `/docs`)内置以下内容:
- **使用指南** — 分步入门教程
- **接口文档** — 详细接口说明
- **隐私政策** — 隐私政策模板(请按需自定义)
- **服务条款** — 服务条款模板
## 🧪 测试
```bash
# 后端测试
go test ./...
# 前端 Lint
cd frontend && pnpm lint
```
## 🚀 部署
### 跨平台二进制编译
一条命令构建全部 6 个平台的静态二进制文件(Linux / macOS / Windows × amd64 / arm64)。
前端已内嵌到每个二进制文件中,无需单独部署。
**前提条件:** 已安装 Docker 且启用 BuildKit(Docker 23+ 默认开启)。
```bash
# 构建全部 6 个二进制文件 → ./bin/
make cross-build
# 指定版本号
make cross-build VERSION=v1.2.3
# 只构建指定系统(两种架构均会构建)
make cross-build GOOS=linux
make cross-build GOOS=darwin
make cross-build GOOS=windows
# 只构建指定架构(所有系统均会构建)
make cross-build GOARCH=amd64
make cross-build GOARCH=arm64
# 同时指定系统和架构 — 只生成单个文件
make cross-build GOOS=linux GOARCH=arm64
make cross-build GOOS=darwin GOARCH=amd64 VERSION=v1.2.3
```
输出到 `./bin/` 目录:
| 文件名 | 平台 |
|--------|------|
| `wavelet_linux_amd64` | Linux x86-64 |
| `wavelet_linux_arm64` | Linux ARM64 |
| `wavelet_darwin_amd64` | macOS Intel |
| `wavelet_darwin_arm64` | macOS Apple Silicon |
| `wavelet_windows_amd64.exe` | Windows x86-64 |
| `wavelet_windows_arm64.exe` | Windows ARM64 |
> 版本号可通过 `wavelet --version` 在运行时查看。
### Docker
```bash
# 构建镜像
docker build -t refreshing .
# 运行(通过卷挂载传入配置文件)
docker run -d -p 8000:8000 \
-v $(pwd)/config.yaml:/app/config.yaml \
refreshing api
```
### 生产环境
1. 构建前端资源:
```bash
cd frontend && pnpm build
```
2. 编译后端程序:
```bash
go build -o refreshing main.go
```
3. 配置生产环境的 `config.yaml`。
4. 启动服务:
```bash
./refreshing api # HTTP API
./refreshing scheduler # 定时调度器(可选)
./refreshing worker # 任务工作进程(可选)
```
## 🤝 贡献指南
我们欢迎社区贡献!请在提交代码前阅读以下文档:
- [贡献指南](CONTRIBUTING.md)
- [行为准则](CODE_OF_CONDUCT.md)
- [贡献者许可协议](CLA.md)
### 贡献流程
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/your-feature`)
3. 提交更改 (`git commit -am 'Add your feature'`)
4. 推送到分支 (`git push origin feature/your-feature`)
5. 创建 Pull Request
## 📄 许可证
本项目基于 [Apache 2.0 许可证](LICENSE) 开源。
-152
View File
@@ -1,152 +0,0 @@
// Command agent runs the OpenFlare edge agent daemon.
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/Rain-kl/Wavelet/internal/apps/agent/agent"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
"github.com/Rain-kl/Wavelet/internal/apps/agent/geoipupdate"
"github.com/Rain-kl/Wavelet/internal/apps/agent/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/agent/httpclient"
"github.com/Rain-kl/Wavelet/internal/apps/agent/logging"
"github.com/Rain-kl/Wavelet/internal/apps/agent/nginx"
"github.com/Rain-kl/Wavelet/internal/apps/agent/runtimeuser"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
syncservice "github.com/Rain-kl/Wavelet/internal/apps/agent/sync"
"github.com/Rain-kl/Wavelet/internal/apps/agent/updater"
"github.com/Rain-kl/Wavelet/internal/apps/agent/wsclient"
)
func main() {
logging.Setup()
configPath := flag.String("config", "./agent.json", "agent config path")
flag.Parse()
cfg, err := config.Load(*configPath)
if err != nil {
slog.Error("load agent config failed", "error", err)
os.Exit(1)
}
if err = runtimeuser.EnsureProcessUser(); err != nil {
slog.Error("ensure runtime user failed", "error", err)
os.Exit(1)
}
if err = runtimeuser.EnsurePathOwnership(cfg.DataDir, runtimeuser.DefaultDirPerm, runtimeuser.DefaultFilePerm); err != nil {
slog.Error("ensure data dir ownership failed", "error", err, "data_dir", cfg.DataDir)
os.Exit(1)
}
cfg.ExtVersion = nginx.DetectVersion(
context.Background(),
nginx.ExecutorOptions{
NginxPath: cfg.OpenrestyPath,
MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
},
)
slog.Info("agent config loaded",
"server", cfg.ServerURL,
"node", cfg.NodeName,
"ip", cfg.NodeIP,
"heartbeat_interval", cfg.HeartbeatInterval,
"route_config", cfg.RouteConfigPath,
"access_log", cfg.AccessLogPath,
"cert_dir", cfg.CertDir,
"lua_dir", cfg.LuaDir,
"runtime_config_dir", cfg.RuntimeConfigDir,
"mmdb_path", cfg.MMDBPath,
"city_mmdb_path", cfg.CityMMDBPath,
)
client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
wsClient := wsclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
stateStore := state.NewStore(cfg.StatePath)
observabilityBuffer := state.NewObservabilityBufferStore(cfg.ObservabilityBufferPath)
runtimeManager := &nginx.Manager{
MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath,
AccessLogPath: cfg.AccessLogPath,
CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
RuntimeConfigDir: cfg.RuntimeConfigDir,
MMDBPath: cfg.MMDBPath,
CityMMDBPath: cfg.CityMMDBPath,
PagesDir: cfg.PagesDir,
OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyObservabilityPort),
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
OpenrestyResolverDirective: "",
Executor: nginx.NewExecutor(nginx.ExecutorOptions{
NginxPath: cfg.OpenrestyPath,
MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
}),
}
if err = runtimeManager.EnsureLuaAssets(); err != nil {
slog.Error("ensure managed lua assets failed", "error", err)
os.Exit(1)
}
syncService := syncservice.New(client, runtimeManager, stateStore)
syncService.SetPagesDir(cfg.PagesDir)
heartbeatService := heartbeat.New(client)
updateService := updater.New()
runner := &agent.Runner{
Config: cfg,
StateStore: stateStore,
HeartbeatCycle: &heartbeat.Cycle{
Config: cfg,
StateStore: stateStore,
ObservabilityBuffer: observabilityBuffer,
Heartbeat: heartbeatService,
Sync: syncService,
Updater: updateService,
},
HeartbeatService: heartbeatService,
SyncService: syncService,
RuntimeManager: runtimeManager,
WebSocketService: wsClient,
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
geoIPUpdater := newGeoIPUpdater(cfg)
if err = geoIPUpdater.EnsureInitialDatabases(ctx); err != nil {
slog.Warn("failed to prepare GeoIP databases before agent startup", "error", err)
}
go geoIPUpdater.Run(ctx)
slog.Info("agent process started")
if err = runner.Run(ctx); err != nil && err != context.Canceled {
slog.Error("agent process exited with error", "error", err)
stop()
os.Exit(1)
}
stop()
slog.Info("agent process stopped")
}
func newGeoIPUpdater(cfg *config.Config) *geoipupdate.Updater {
return &geoipupdate.Updater{
MMDBPath: cfg.MMDBPath,
DownloadURL: cfg.MMDBDownloadURL,
CityMMDBPath: cfg.CityMMDBPath,
CityDownloadURL: cfg.CityMMDBDownloadURL,
UpdateInterval: cfg.MMDBUpdateInterval.Duration(),
}
}
-24
View File
@@ -1,24 +0,0 @@
package main
import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
)
func TestNewGeoIPUpdaterWiresCountryAndCity(t *testing.T) {
cfg := &config.Config{
MMDBPath: "/data/GeoLite2-Country.mmdb",
MMDBDownloadURL: "https://geo.example/GeoLite2-Country.mmdb",
CityMMDBPath: "/data/GeoLite2-City.mmdb",
CityMMDBDownloadURL: "https://geo.example/GeoLite2-City.mmdb",
MMDBUpdateInterval: config.MillisecondDuration(time.Hour),
}
updater := newGeoIPUpdater(cfg)
if updater.MMDBPath != cfg.MMDBPath || updater.DownloadURL != cfg.MMDBDownloadURL ||
updater.CityMMDBPath != cfg.CityMMDBPath || updater.CityDownloadURL != cfg.CityMMDBDownloadURL ||
updater.UpdateInterval != time.Hour {
t.Fatalf("GeoIP updater wiring incomplete: %#v", updater)
}
}
-73
View File
@@ -1,73 +0,0 @@
// Command flared runs the OpenFlare tunnel client daemon.
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"syscall"
edgelogging "github.com/Rain-kl/Wavelet/internal/apps/edge/logging"
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
"github.com/Rain-kl/Wavelet/internal/apps/flared/flared"
"github.com/Rain-kl/Wavelet/internal/apps/flared/frpc"
"github.com/Rain-kl/Wavelet/internal/apps/flared/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/flared/httpclient"
"github.com/Rain-kl/Wavelet/internal/apps/flared/sync"
"github.com/Rain-kl/Wavelet/internal/apps/flared/wsclient"
)
func main() {
edgelogging.Setup(edgelogging.Options{})
configPath := flag.String("config", "./flared.json", "flared config path")
flag.Parse()
cfg, err := config.Load(*configPath)
if err != nil {
slog.Error("load flared config failed", "error", err)
os.Exit(1)
}
slog.Info("flared config loaded",
"server", cfg.ServerURL,
"frpc_path", cfg.FrpcPath,
"data_dir", cfg.DataDir,
"heartbeat_interval", cfg.HeartbeatInterval,
"sync_interval", cfg.SyncInterval,
)
frpcManager := frpc.NewManager(cfg)
_ = frpcManager.LoadState()
slog.Info("detected frpc version", "version", frpcManager.GetVersion(context.Background()))
httpClient := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
wsClient := wsclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
syncService := sync.New(httpClient, frpcManager, cfg)
heartbeatService := heartbeat.New(httpClient, frpcManager, cfg)
runner := &flared.Runner{
Config: cfg,
FrpcManager: frpcManager,
HTTPClient: httpClient,
WebSocketService: wsClient,
HeartbeatService: heartbeatService,
SyncService: syncService,
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
slog.Info("flared process started")
if err := runner.Run(ctx); err != nil && err != context.Canceled {
slog.Error("flared process exited with error", "error", err)
stop()
os.Exit(1)
}
stop()
slog.Info("flared process stopped")
}
-73
View File
@@ -1,73 +0,0 @@
// Command relay runs the OpenFlare relay node daemon.
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"syscall"
edgelogging "github.com/Rain-kl/Wavelet/internal/apps/edge/logging"
"github.com/Rain-kl/Wavelet/internal/apps/relay/config"
"github.com/Rain-kl/Wavelet/internal/apps/relay/frps"
"github.com/Rain-kl/Wavelet/internal/apps/relay/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/relay/httpclient"
"github.com/Rain-kl/Wavelet/internal/apps/relay/relay"
"github.com/Rain-kl/Wavelet/internal/apps/relay/state"
"github.com/Rain-kl/Wavelet/internal/apps/relay/wsclient"
)
func main() {
edgelogging.Setup(edgelogging.Options{})
configPath := flag.String("config", "./relay.json", "relay config path")
flag.Parse()
cfg, err := config.Load(*configPath)
if err != nil {
slog.Error("load relay config failed", "error", err)
os.Exit(1)
}
slog.Info("relay config loaded",
"server", cfg.ServerURL,
"node", cfg.NodeName,
"ip", cfg.NodeIP,
"frps_path", cfg.FrpsPath,
"data_dir", cfg.DataDir,
"heartbeat_interval", cfg.HeartbeatInterval,
)
stateStore := state.NewStore(cfg.StatePath)
_ = stateStore // In the future we may use stateStore for auth caching
frpsManager := frps.NewManager(cfg.FrpsPath, cfg.DataDir, cfg.InitialAuthToken())
slog.Info("detected frps version", "version", frpsManager.GetVersion(context.Background()))
httpClient := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
wsClient := wsclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
runner := &relay.Runner{
Config: cfg,
StateStore: stateStore,
FrpsManager: frpsManager,
HTTPClient: httpClient,
WebSocketService: wsClient,
HeartbeatService: heartbeat.New(httpClient, frpsManager, cfg, stateStore),
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
slog.Info("relay process started")
if err := runner.Run(ctx); err != nil && err != context.Canceled {
slog.Error("relay process exited with error", "error", err)
stop()
os.Exit(1)
}
stop()
slog.Info("relay process stopped")
}
+20 -23
View File
@@ -1,15 +1,15 @@
# openflare — Platform Config
# wavelet — Full-Stack Boilerplate Config
# Copy this file to config.yaml and fill in your values.
# Fields marked with <...> are required; others have sensible defaults.
# ─── Application ────────────────────────────────────────────────────────────────
app:
app_name: "openflare"
env: "production" # development | testing | production
addr: ":3000"
app_name: "wavelet"
env: "development" # development | testing | production
addr: ":8000"
node_id: 1 # Snowflake node ID (0-1023). Must be unique per instance.
graceful_shutdown_timeout: 30
session_cookie_name: "openflare_session_id" # Change to something unique before deploy
session_cookie_name: "wavelet_session_id" # Change to something unique before deploy
session_secret: "<uniq-random-string>" # Cannot be changed after first start
session_domain: "" # e.g. ".yourdomain.com"
session_age: 86400 # Session lifetime in seconds (default: 24h)
@@ -21,12 +21,12 @@ app:
# Supports Standalone and Primary-Replica (read/write split) modes.
database:
enabled: true
sqlite_path: "openflare.db" # PostgreSQL 禁用时使用此 SQLite 文件路径
sqlite_path: "wavelet.db" # PostgreSQL 禁用时使用此 SQLite 文件路径
host: "127.0.0.1"
port: 5432
username: "openflare"
password: "replace-with-strong-password"
database: "openflare"
username: "postgres"
password: "postgres"
database: "wavelet"
max_idle_conn: 16
max_open_conn: 128
conn_max_lifetime: 1800
@@ -34,7 +34,7 @@ database:
log_level: "info" # error | warn | info | debug | silent;SQL 语句仅在 log.level=debug 时输出
ssl_mode: "disable"
time_zone: "UTC"
application_name: "openflare-server"
application_name: "wavelet-server"
prefer_simple_protocol: false
search_path: "public"
statement_cache_capacity: 256
@@ -58,7 +58,7 @@ redis:
db: 0 # Ignored in Cluster mode
cluster_mode: false # Set true to enable Cluster mode
master_name: "" # Set non-empty to enable Sentinel mode
key_prefix: "openflare:"
key_prefix: "wavelet:"
pool_size: 100
min_idle_conn: 10
dial_timeout: 5
@@ -96,22 +96,19 @@ worker:
# ─── OpenTelemetry Tracing ──────────────────────────────────────────────────────
otel:
sampling_rate: 0.0 # Trace sampling rate (0.0 – 1.0)
tracer_name: "github.com/Rain-kl/OpenFlare" # Global tracer instrumentation name
tracer_name: "github.com/Rain-kl/Wavelet" # Global tracer instrumentation name
# ─── ClickHouse (required) ──────────────────────────────────────────────────────
# Analytics / observability OLAP store. Telemetry writes are best-effort (async batch).
# ─── ClickHouse (optional) ──────────────────────────────────────────────────────
clickhouse:
enabled: true
enabled: false
hosts:
- "127.0.0.1:9000" # compose 内应用可用 clickhouse:9000(经 CLICKHOUSE_HOST)
- "127.0.0.1:9000"
username: "default"
password: "replace-with-clickhouse-password" # 与 .env / compose CLICKHOUSE_PASSWORD 一致
database: "openflare"
max_idle_conn: 8 # keep warm sockets low to save client + server RAM
max_open_conn: 16 # cap concurrent native sessions on modest CH boxes
password: ""
database: "wavelet"
max_idle_conn: 10
max_open_conn: 100
conn_max_lifetime: 3600
dial_timeout: 5
block_buffer_size: 32 # rows buffered per block; 32 is enough for our batch sizes
# Runtime client also enables async_insert (wait_for_async_insert=1, busy_timeout≈2s)
# in internal/db/clickhouse.go — not configured via YAML.
block_buffer_size: 10
-25
View File
@@ -1,25 +0,0 @@
<?xml version="1.0"?>
<!--
Tuned for small control-plane hosts (e.g. 3c6g).
background_pool_size * background_merges_mutations_concurrency_ratio must stay
greater than merge_tree number_of_free_entries_in_pool_to_execute_mutation
(ClickHouse 25.x refuses to start otherwise). Keep the merge free-entry
thresholds low so a small pool remains valid.
-->
<clickhouse>
<max_concurrent_queries>20</max_concurrent_queries>
<background_pool_size>4</background_pool_size>
<background_merges_mutations_concurrency_ratio>2</background_merges_mutations_concurrency_ratio>
<background_schedule_pool_size>4</background_schedule_pool_size>
<background_common_pool_size>2</background_common_pool_size>
<background_fetches_pool_size>2</background_fetches_pool_size>
<background_move_pool_size>1</background_move_pool_size>
<mark_cache_size>268435456</mark_cache_size>
<uncompressed_cache_size>0</uncompressed_cache_size>
<merge_tree>
<number_of_free_entries_in_pool_to_execute_mutation>2</number_of_free_entries_in_pool_to_execute_mutation>
<number_of_free_entries_in_pool_to_lower_max_size_of_merge>2</number_of_free_entries_in_pool_to_lower_max_size_of_merge>
<number_of_free_entries_in_pool_to_execute_optimize_entire_partition>2</number_of_free_entries_in_pool_to_execute_optimize_entire_partition>
</merge_tree>
</clickhouse>
-145
View File
@@ -1,145 +0,0 @@
services:
openflare:
build:
context: .
dockerfile: docker/Dockerfile
args:
VERSION: v0.9.9
# image: ghcr.io/rain-kl/openflare:latest
restart: unless-stopped
env_file: .env
environment:
TZ: ${TZ:-Asia/Shanghai}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://jaeger:4317}
OTEL_EXPORTER_OTLP_INSECURE: ${OTEL_EXPORTER_OTLP_INSECURE:-true}
OTEL_SAMPLING_RATE: ${OTEL_SAMPLING_RATE:-1.0}
ports:
- "3000:3000"
volumes:
- ./uploads:/app/uploads
- ./data/sqlite:/app/data
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
jaeger:
condition: service_started
postgres:
image: postgres:17-alpine
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_DB: ${DB_NAME:-openflare}
POSTGRES_USER: ${DB_USERNAME:-openflare}
POSTGRES_PASSWORD: ${DB_PASSWORD:-replace-with-strong-password}
volumes:
- ./data/postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-openflare} -d ${DB_NAME:-openflare}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- ./data/valkey:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
jaeger:
image: jaegertracing/jaeger:${JAEGER_VERSION:-2.19.0}
restart: unless-stopped
environment:
TZ: ${TZ:-Asia/Shanghai}
ports:
- "${JAEGER_UI_PORT:-16686}:16686"
- "${JAEGER_OTLP_GRPC_PORT:-4317}:4317"
- "${JAEGER_OTLP_HTTP_PORT:-4318}:4318"
clickhouse:
image: clickhouse/clickhouse-server:25.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: ${CLICKHOUSE_NAME:-openflare}
CLICKHOUSE_USER: ${CLICKHOUSE_USERNAME:-default}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
TZ: ${TZ:-Asia/Shanghai}
ulimits:
nofile:
soft: 262144
hard: 262144
ports:
- "8123:8123"
- "9000:9000"
volumes:
- ./data/clickhouse_data:/var/lib/clickhouse
- ./config/clickhouse/performance.xml:/etc/clickhouse-server/config.d/performance.xml:ro
healthcheck:
test: ["CMD", "clickhouse-client", "--user", "${CLICKHOUSE_USERNAME:-default}", "--password", "${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
agent:
build:
context: .
dockerfile: docker/Dockerfile.agent
container_name: openflare-agent
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "127.0.0.1:18081:18081"
volumes:
- ./data/agent/:/data
environment:
OPENFLARE_SERVER_URL: "http://host.docker.internal:3000"
OPENFLARE_AGENT_TOKEN: "af2fb112f36a0055ec25dd164c908fea"
LOG_LEVEL: "debug"
extra_hosts:
- "host.docker.internal:host-gateway"
relay:
build:
context: .
dockerfile: docker/Dockerfile.relay
container_name: openflare-relay
network_mode: host
restart: unless-stopped
volumes:
- ./data/relay/:/app/data
environment:
OPENFLARE_SERVER_URL: http://host.docker.internal:3000
OPENFLARE_DISCOVERY_TOKEN: 85464eeb72c49abc430569d6b9c77f78
LOG_LEVEL: "debug"
extra_hosts:
- "host.docker.internal:host-gateway"
flared:
build:
context: .
dockerfile: docker/Dockerfile.flared
container_name: openflare-flared
network_mode: "host"
restart: unless-stopped
volumes:
- ./data/flared/:/app/data
environment:
OPENFLARE_SERVER_URL: "http://host.docker.internal:3000"
OPENFLARE_TUNNEL_TOKEN: deb0783ac1e264a9d86440169aca0f09
+94
View File
@@ -0,0 +1,94 @@
services:
wavelet:
build:
context: .
dockerfile: docker/Dockerfile
args:
VERSION: v0.9.9
# image: ghcr.io/rain-kl/wavelet:latest
restart: unless-stopped
env_file: .env
environment:
TZ: ${TZ:-Asia/Shanghai}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://jaeger:4317}
OTEL_EXPORTER_OTLP_INSECURE: ${OTEL_EXPORTER_OTLP_INSECURE:-true}
OTEL_SAMPLING_RATE: ${OTEL_SAMPLING_RATE:-1.0}
ports:
- "${APP_PORT:-8000}:8000"
volumes:
- ./uploads:/app/uploads
- ./data/sqlite:/app/data
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
jaeger:
condition: service_started
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-wavelet}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
TZ: ${TZ:-Asia/Shanghai}
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- ./data/postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-wavelet}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- ./data/valkey:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
jaeger:
image: jaegertracing/jaeger:${JAEGER_VERSION:-2.19.0}
restart: unless-stopped
environment:
TZ: ${TZ:-Asia/Shanghai}
ports:
- "${JAEGER_UI_PORT:-16686}:16686"
- "${JAEGER_OTLP_GRPC_PORT:-4317}:4317"
- "${JAEGER_OTLP_HTTP_PORT:-4318}:4318"
#
# clickhouse:
# image: clickhouse/clickhouse-server:25.3-alpine
# restart: unless-stopped
# profiles:
# - clickhouse
# environment:
# CLICKHOUSE_DB: ${CLICKHOUSE_DB:-wavelet}
# CLICKHOUSE_USER: ${CLICKHOUSE_USER:-default}
# CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-123456}
# CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
# TZ: ${TZ:-Asia/Shanghai}
# ports:
# - "${CLICKHOUSE_HTTP_PORT:-8123}:8123"
# - "${CLICKHOUSE_NATIVE_PORT:-9000}:9000"
# volumes:
# - ./data/clickhouse_data:/var/lib/clickhouse
# healthcheck:
# test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
# interval: 10s
# timeout: 5s
# retries: 5
# start_period: 15s
+4 -4
View File
@@ -46,7 +46,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
-tags embed_frontend \
-trimpath \
-ldflags="-s -w -X github.com/Rain-kl/Wavelet/internal/buildinfo.Version=${VERSION} -X github.com/Rain-kl/Wavelet/internal/buildinfo.BuildTime=${BUILD_DATE}" \
-o /out/openflare-server \
-o /out/wavelet \
./main.go
FROM alpine:${ALPINE_VERSION}
@@ -59,10 +59,10 @@ RUN apk add --no-cache ca-certificates tzdata postgresql-client && \
WORKDIR /app
COPY --from=backend-builder /out/openflare-server ./openflare-server
COPY --from=backend-builder /out/wavelet ./wavelet
COPY docs ./docs
EXPOSE 3000
EXPOSE 8000
ENTRYPOINT ["./openflare-server"]
ENTRYPOINT ["./wavelet"]
CMD ["all"]
-46
View File
@@ -1,46 +0,0 @@
# syntax=docker/dockerfile:1.7
ARG VERSION=dev
FROM golang:1.25-alpine AS builder
ARG VERSION
ARG TARGETOS=linux
ARG TARGETARCH
ENV CGO_ENABLED=0 \
GOOS=${TARGETOS} \
GOARCH=${TARGETARCH}
WORKDIR /build
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN apk add --no-cache bash curl \
&& bash scripts/fetch-agent-geoip-mmdb.sh
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/agent/config.Version=$VERSION'" -o /build/bin/openflare-agent ./cmd/agent/main.go
FROM openresty/openresty:alpine
RUN apk add --no-cache ca-certificates tzdata perl libmaxminddb su-exec libcap \
&& ln -sf /usr/lib/libmaxminddb.so.0 /usr/lib/libmaxminddb.so \
&& opm get anjia0532/lua-resty-maxminddb \
&& addgroup -S openflare \
&& adduser -S -G openflare -H -h /data -s /sbin/nologin openflare \
&& mkdir -p /etc/openflare /data \
&& chown -R openflare:openflare /etc/openflare /data \
&& setcap 'cap_net_bind_service=+ep' /usr/local/openresty/nginx/sbin/nginx
ENV OPENFLARE_OPENRESTY_PATH=openresty \
OPENFLARE_DATA_DIR=/data
COPY --from=builder /build/bin/openflare-agent /usr/local/bin/openflare-agent
COPY scripts/agent-entrypoint.sh /usr/local/bin/openflare-agent-entrypoint.sh
RUN chmod +x /usr/local/bin/openflare-agent-entrypoint.sh
EXPOSE 80 443 18081
ENTRYPOINT ["/usr/local/bin/openflare-agent-entrypoint.sh"]
CMD ["-config", "/etc/openflare/agent.json"]
+4 -4
View File
@@ -20,7 +20,7 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w -X github.com/Rain-kl/Wavelet/internal/buildinfo.Version=${VERSION} -X github.com/Rain-kl/Wavelet/internal/buildinfo.BuildTime=${BUILD_DATE}" \
-o /out/openflare-server \
-o /out/wavelet \
./main.go
FROM alpine:${ALPINE_VERSION}
@@ -33,10 +33,10 @@ RUN apk add --no-cache ca-certificates tzdata postgresql-client && \
WORKDIR /app
COPY --from=builder /out/openflare-server ./openflare-server
COPY --from=builder /out/wavelet ./wavelet
COPY docs ./docs
EXPOSE 3000
EXPOSE 8000
ENTRYPOINT ["./openflare-server"]
ENTRYPOINT ["./wavelet"]
CMD ["api"]
+1 -1
View File
@@ -90,7 +90,7 @@ RUN set -e; \
[ -n "$FILTER_ARCH" ] && [ "$GOARCH" != "$FILTER_ARCH" ] && continue; \
EXT=""; \
[ "$GOOS" = "windows" ] && EXT=".exe"; \
OUTPUT="/out/openflare-server_${GOOS}_${GOARCH}${EXT}"; \
OUTPUT="/out/wavelet_${GOOS}_${GOARCH}${EXT}"; \
echo "==> Building ${OUTPUT} (version=${VERSION})..."; \
CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \
go build \
-30
View File
@@ -1,30 +0,0 @@
# syntax=docker/dockerfile:1.7
ARG VERSION=dev
FROM golang:1.25-alpine AS builder
ARG VERSION
WORKDIR /build
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/flared/config.Version=$VERSION'" -o flared ./cmd/flared/main.go
# Final runtime image
FROM fatedier/frpc:v0.69.0
WORKDIR /app
# Copy openflared binary
COPY --from=builder /build/flared .
ENV OPENFLARE_DATA_DIR=/app/data
ENV OPENFLARE_FRPC_PATH=/usr/bin/frpc
ENTRYPOINT ["/app/flared"]
CMD []
-31
View File
@@ -1,31 +0,0 @@
# syntax=docker/dockerfile:1.7
ARG VERSION=dev
FROM golang:1.25-alpine AS builder
ARG VERSION
WORKDIR /build
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/relay/config.Version=$VERSION'" -o /build/bin/openflare-relay ./cmd/relay/main.go
# Final runtime image
FROM fatedier/frps:v0.69.0
WORKDIR /app
# Copy openflare-relay binary
COPY --from=builder /build/bin/openflare-relay ./openflare-relay
VOLUME ["/app/data"]
ENV OPENFLARE_FRPS_PATH=/usr/bin/frps
ENV OPENFLARE_DATA_DIR=/app/data
ENTRYPOINT ["/app/openflare-relay"]
-18
View File
@@ -1,18 +0,0 @@
/coverage
/src/client/shared.ts
/src/node/shared.ts
*.log
*.tgz
.DS_Store
.idea
.temp
.vite_opt_cache
.vscode
dist
cache
temp
examples-temp
node_modules
pnpm-global
TODOs.md
*.timestamp-*.mjs
-8
View File
@@ -1,8 +0,0 @@
{
"plugins": {
"postcss-rtlcss": {
"ltrPrefix": ":where([dir=\"ltr\"])",
"rtlPrefix": ":where([dir=\"rtl\"])"
}
}
}
-86
View File
@@ -1,86 +0,0 @@
import { defineConfig, type HeadConfig, resolveSiteDataByRoute } from 'vitepress'
import llmstxt from 'vitepress-plugin-llms'
const prod = !!process.env.NETLIFY
export default defineConfig({
title: 'OpenFlare',
lastUpdated: true,
cleanUrls: true,
ignoreDeadLinks: true,
metaChunk: true,
srcExclude: [
'zh/**',
'components/**',
'snippets/**',
'plan/**',
'guideline/**'
],
markdown: {
math: true
},
sitemap: {
hostname: 'https://openflare.io'
},
head: [
['meta', { name: 'theme-color', content: '#10b981' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'OpenFlare' }],
['meta', { property: 'og:url', content: 'https://openflare.io/' }],
['script', { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-TBZPQFMLFH' }],
[
'script',
{},
`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-TBZPQFMLFH');`
]
],
themeConfig: {
socialLinks: [
{ icon: 'github', link: 'https://github.com/Rain-kl/OpenFlare' }
],
search: {
provider: 'local'
}
},
locales: {
root: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr' },
en: { label: 'English', lang: 'en-US', dir: 'ltr' }
},
vite: {
plugins: [
prod &&
llmstxt({
workDir: '.',
ignoreFiles: ['index.md']
})
],
experimental: {
enableNativePlugin: true
}
},
transformPageData: prod
? (pageData, ctx) => {
const site = resolveSiteDataByRoute(
ctx.siteConfig.site,
pageData.relativePath
)
const title = `${pageData.title || site.title} | ${
pageData.description || site.description
}`
;((pageData.frontmatter.head ??= []) as HeadConfig[]).push(
['meta', { property: 'og:locale', content: site.lang }],
['meta', { property: 'og:title', content: title }]
)
}
: undefined
})
-4
View File
@@ -1,4 +0,0 @@
import Theme from 'vitepress/theme'
import './styles.css'
export default Theme
-20
View File
@@ -1,20 +0,0 @@
:root {
--vp-c-brand-1: #059669;
--vp-c-brand-2: #10b981;
--vp-c-brand-3: #34d399;
--vp-c-brand-soft: rgba(16, 185, 129, 0.16);
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(120deg, #059669, #2563eb);
--vp-font-family-base:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
}
.VPHomeHero .text,
.VPHomeHero .tagline {
max-width: 760px;
}
.VPFeature {
border-radius: 8px;
}
+335
View File
@@ -0,0 +1,335 @@
# wavelet 部署指南
本文档详细介绍了 **wavelet** 脚手架系统在不同业务阶段的部署方案,涵盖从**最小化单机部署**到**最大化高可用分布式部署**的全生命周期架构。
---
## 一、 系统组件概览
在部署系统前,请了解各运行组件及其角色:
| 组件名称 | 运行命令/形式 | 职责说明 | 必选/可选 |
| :--- | :--- | :--- | :--- |
| **HTTP API 服务** | `bin/wavelet api` | 接收并处理前端及第三方的 RESTful API 请求 | **必选** |
| **异步任务工作进程** | `bin/wavelet worker` | 消费并处理异步队列任务(如邮件发送、清理上传文件等) | **必选** |
| **定时任务调度器** | `bin/wavelet scheduler` | 定时向 Redis 队列下发 Cron 任务(仅负责触发,不负责执行) | **必选** |
| **前端服务 (Node.js)** | `pnpm start` | 提供 React/Next.js 页面服务(在分离部署时使用) | 分离模式必选 |
| **PostgreSQL** | 关系型主数据库 | 存储用户、系统配置、认证源、任务执行记录等核心数据 | **必选** |
| **Redis** | 缓存与消息队列中间件 | 存储 Session 会话、临时缓存以及 Asynq 异步任务队列数据 | **必选** |
| **ClickHouse** | 分析型数据库 | 存储历史数据同步或进行高性能分析 | 可选 |
| **对象存储 (S3)** | 兼容 S3 的云存储/私有云 | 存放用户上传的静态文件、图片等 | 可选 |
---
## 二、 部署配置准备
系统在启动前会从当前目录加载 `config.yaml` 配置文件。
生产环境部署前,请复制 `config.example.yaml` 为 `config.yaml`,并至少确认以下关键参数的配置:
```yaml
app:
env: "production" # 生产环境标识
addr: ":8000" # API 服务监听端口
session_secret: "prod-random-secret" # 极其重要的加密密钥,首发启动后不可更改
session_domain: ".yourdomain.com" # 跨域共享 Session 时需配置
database:
host: "db.yourdomain.com"
port: 5432
username: "postgres"
password: "YOUR_DB_PASSWORD"
database: "refreshing"
redis:
addrs:
- "redis.yourdomain.com:6379"
password: "YOUR_REDIS_PASSWORD"
```
---
## 三、 方案一:最小部署 — 单机嵌入式极简版 (推荐)
此部署方案将**前端静态网页全部直接打入 Go 后端二进制文件中**,极大地简化了部署运维,是中小型应用、内部系统、SaaS 早期阶段的首选。
### 📊 架构设计
- **服务载体**:单台云服务器 (1核2G 即可)。
- **依赖服务**:在一台机器上启动轻量级 PostgreSQL 与 Redis(可采用 Docker 部署)。
- **进程管理**:在一台机器上直接拉起打包好的 Go 单文件,并分别运行 `api`、`worker`、`scheduler` 进程。
- **前端托管**:Go 服务直接在 8000 端口承载前端的所有页面,不需要额外配置 Node.js 生产服务器。
### 🛠️ 步骤说明
#### 1. 单机依赖服务初始化 (使用 Docker Compose)
在机器上准备以下 `docker-compose.yml` 快速启动 PostgreSQL 和 Redis:
```yaml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: refreshing-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: YOUR_DB_PASSWORD
POSTGRES_DB: refreshing
ports:
- "5432:5432"
volumes:
- ./data/pg:/var/lib/postgresql/data
restart: always
redis:
image: valkey/valkey:8.0-alpine
container_name: refreshing-redis
command: valkey-server --requirepass YOUR_REDIS_PASSWORD
ports:
- "6379:6379"
volumes:
- ./data/redis:/data
restart: always
```
执行命令启动:
```bash
docker compose up -d
```
#### 2. 前后端一键嵌入式打包
在开发或编译机上,运行编译指令:
```bash
make build-embedded
```
该命令会自动完成前端的静态编译导出 (`frontend/out`)、复制到 Go 后端目录,最后使用 `-tags embed_frontend` 生成后端单文件:
- 产物路径:`bin/wavelet`
#### 3. 进程管理 (使用 Systemd)
将 `bin/wavelet` 拷贝到生产服务器 `/usr/local/bin/wavelet`,并为 `api`、`worker` 和 `scheduler` 配置 Systemd 管理服务。
新建 API 进程服务文件 `/etc/systemd/system/wavelet-api.service`:
```ini
[Unit]
Description=Refreshing API Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/app
ExecStart=/usr/local/bin/wavelet api
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
同理,新建 Worker 服务 `/etc/systemd/system/wavelet-worker.service`(将命令改为 `wavelet worker`),以及 Scheduler 服务 `/etc/systemd/system/wavelet-scheduler.service`(将命令改为 `wavelet scheduler`)。
启动并启用所有服务:
```bash
systemctl daemon-reload
systemctl enable --now refreshing-api refreshing-worker refreshing-scheduler
```
#### 4. 配置 Nginx 证书
配置 Nginx 作为反向代理并启用 HTTPS 证书:
```nginx
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /path/to/cert.crt;
ssl_certificate_key /path/to/cert.key;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## 四、 方案二:标准部署 — 前后端物理分离架构
此方案中前端与后端彻底解耦。前端采用 SSR/ISR (Next.js Node 服务) 运行,后端采用独立的 API 服务运行。
### 📊 架构设计
- **前端部署**:单独部署到 Node.js 托管环境(如多台前端机器或 Vercel/Cloudflare Pages)。
- **后端部署**:多台后端云服务器,统一指向云数据库 RDS 与云缓存 Redis。
- **通信方式**:前后端通过 Nginx 规则路由或独立域名(如 `app.yourdomain.com` 访问前端,`api.yourdomain.com` 访问后端)进行跨域通信。
### 🛠️ 步骤说明
#### 1. 部署后端 Go 服务
1. 编译后端:
```bash
go build -o bin/wavelet main.go
```
2. 在后端服务器上,同样使用 Systemd 或 Docker 守护启动 `wavelet api`、`wavelet worker` 和 `wavelet scheduler`。
3. 配置后端 Nginx 将客户端 API 请求(如 `/api/...`)反向代理至后端绑定的端口(如 `:8000`)。
#### 2. 部署前端 Next.js 服务
1. 前端服务器环境确保已安装 Node.js 和 pnpm。
2. 安装依赖并编译生产版本:
```bash
cd frontend
pnpm install
pnpm build
```
3. 使用 PM2 守护前端 Node.js 服务运行。新建 `ecosystem.config.js`:
```javascript
module.exports = {
apps: [
{
name: 'refreshing-frontend',
script: 'node_modules/next/dist/bin/next',
args: 'start -p 3000',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
WAVELET_BACKEND_URL: 'https://api.yourdomain.com'
}
}
]
};
```
启动前端服务:
```bash
pm2 start ecosystem.config.js
```
#### 3. 跨域与 Cookie 说明
- 若前后端使用**不同子域名**部署(例如 `app.yourdomain.com` 和 `api.yourdomain.com`),必须在 `config.yaml` 中将 `app.session_domain` 显式设置为顶级域名(`.yourdomain.com`),以确保 Session Cookie 可以在子域间顺利透传。
- 在跨域状态下,前端请求必须配置 `withCredentials: true`,API 端的跨域中间件(`corsMiddleware`)会自动将该域添加至允许源中。
---
## 五、 方案三:最大部署 — 企业级高可用分布式架构 (Max)
当系统面临高并发流量、海量后台任务或极高的可用性要求时,需要将所有组件拆分为无状态水平扩容,并引入高可用的云基础设施。
### 📊 架构设计图
```
┌────────────────────────┐
│ 域名 / 负载均衡器 │
│ (SLB / Cloudflare) │
└──────────┬─────────────┘
│
┌──────────────────┴──────────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ 前端集群 │ │ 后端 API 集群 │
│ (Next.js Node) │ │ (Go 无状态实例) │
│ [弹性扩容 / 8台+] │ │ [弹性扩容 / 8台+] │
└─────────────────────┘ └──────────┬──────────┘
│
┌────────────────────────────────────────┼────────────────────────────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ 异步 Worker 集群 │ │ 定时 Scheduler │ │ S3 对象存储集群 │
│ (多节点并发处理) │ │ (主备模式,限单节点)│ │(R2/MinIO/AWS S3) │
└─────────┬─────────┘ └─────────┬─────────┘ └───────────────────┘
│ │
└───────────────────┬────────────────────┘
│
┌───────────────────┴────────────────────┐
▼ ▼
┌───────────────────────────────────┐ ┌───────────────────────────────────┐
│ Redis 哨兵/集群 │ │ PG 主从读写分离集群 │
│ (高可用缓存/Asynq 队列) │ │ (RDS Primary-Replica) │
└───────────────────────────────────┘ └───────────────────────────────────┘
```
### ⚙️ 最大部署配置要点
#### 1. 数据库高可用 (主从读写分离)
在 `config.yaml` 中配置 `database` 的主库写与从库读:
```yaml
database:
enabled: true
host: "pg-primary.yourdomain.com" # 主库地址(写)
port: 5432
username: "postgres"
password: "YOUR_DB_PASSWORD"
database: "refreshing"
# 配置读写分离只读副本(GORM 自动轮询读,支持配置多个从库)
replicas:
- host: "pg-replica-1.yourdomain.com"
port: 5432
username: "postgres"
password: "YOUR_DB_PASSWORD"
- host: "pg-replica-2.yourdomain.com"
port: 5432
username: "postgres"
password: "YOUR_DB_PASSWORD"
```
#### 2. Redis 高可用 (哨兵/Sentinel 或集群)
- **Sentinel 哨兵模式**:通过配置 `redis.master_name` 启用,SDK 会自动监视 Master 的主备切换。
- **Cluster 集群模式**:将 `redis.cluster_mode` 设为 `true`,并提供所有集群节点的 `addrs`。
```yaml
redis:
addrs:
- "redis-node-1.yourdomain.com:6379"
- "redis-node-2.yourdomain.com:6379"
- "redis-node-3.yourdomain.com:6379"
cluster_mode: true
```
#### 3. 对象存储与缓存分离 (S3 + Local Cache)
高可用集群下,本地文件系统不再可共享。文件存储必须启用 S3 兼容服务,并在多节点间开启本地高速磁盘缓存加速读取:
```yaml
s3:
enabled: true
endpoint: "https://your-r2-or-s3-id.r2.cloudflarestorage.com"
region: "auto"
bucket: "refreshing-assets"
access_key_id: "YOUR_S3_KEY"
secret_access_key: "YOUR_S3_SECRET"
local_cache:
enabled: true # 开启本地磁盘缓存
cache_dir: "/data/s3_cache" # 本地高性能 SSD 挂载点
```
#### 4. 后端进程横向拆分部署
- **API 集群**:启动数十个甚至上百个 `wavelet api` 无状态容器。它们可以通过负载均衡器直接挂载,支持随时弹性缩容扩容。
- **Worker 集群**:启动多个 `wavelet worker` 容器。因为 `Asynq` 基于 Redis 分布式处理,多个 Worker 进程可以安全地同时运行并竞抢同一队列的异步任务,自动保障任务的并发吞吐能力。
- **Scheduler 独占**:**【注意】** 为避免重复触发定时 Cron 任务,`wavelet scheduler` 定时调度器进程**同一时间应仅运行单个活跃实例**(主备高可用可以通过容器平台的单实例保障或 K8s Job 机制来限制实例数为 1)。
#### 5. ClickHouse 高并发同步
在大数据量、高频支付结算场景下,开启 ClickHouse 以接收系统的历史数据同步,通过定时器把 PostgreSQL 的压力转移到 ClickHouse 列式存储中。
```yaml
clickhouse:
enabled: true
hosts:
- "ch-node-1.yourdomain.com:9000"
- "ch-node-2.yourdomain.com:9000"
```
#### 6. OpenTelemetry 分布式链路追踪
最大部署架构必须引入链路追踪(Jaeger 或 OTel Collector)以便排查节点间请求延迟或网络问题。
在生产环境,通过配置 OTel 将 Span 发送至公共日志分析平台。
```yaml
otel:
sampling_rate: 0.05 # 开启 5% 的流量追踪采样率以减少开销
```
---
## 六、 部署方案对比与选择建议
| 指标维度 | 方案一:最小单机嵌入版 | 方案二:标准前后端分离版 | 方案三:最大高可用分布式版 |
| :--- | :--- | :--- | :--- |
| **支持流量/并发** | 1,000 ~ 5,000 QPS (视机器性能) | 5,000 ~ 20,000 QPS | 20,000 ~ 100,000+ QPS (无限扩展) |
| **服务器数量** | 1 台 | 3 ~ 5 台 | 10 台以上集群 |
| **运维复杂度** | 极简 (只需部署一个程序) | 中等 (需维护 Node 和 Go 两套环境) | 较高 (K8s/多组件集群维护) |
| **适合场景** | 个人项目、内部系统、SaaS 早期起步 | 正常线上运营项目、有中等规模团队 | 大型企业级应用、高并发核心交易系统 |
+501
View File
@@ -0,0 +1,501 @@
# Wavelet 系统性能分析与优化建议
> 分析日期:2026-06-17
> 范围:Go 后端 + Next.js 前端
> 目标:识别可能在生产环境真实出现的性能问题,并给出高 ROI 优化路线
**状态图例**:`✅ 已完成` · `🔶 部分完成` · `⬜ 待做`
| 修复批次 | 范围 | 状态 |
|----------|------|------|
| P0 后端 #1–#4 | WebP 锁、文件路径缓存、增量统计、复合索引 | ✅ |
| P0 前端 #6–#7 | 认证并行化、日志虚拟化 | ✅ |
| P1 #9 | 公共配置 Redis 列表缓存 | ✅ |
| P1 参数中心 | 系统配置 Otter RAM 缓存 + 统一失效 + 多节点 pub/sub | ✅ |
| P1 CAPTCHA | 运行时配置快照 + 批量加载 + pub/sub 失效 | ✅ |
| P0 前端 #12–#19 | dynamic 分割、React Query、登录并行、Tooltip、lazy、barrel 收窄 | ✅ |
---
## 目录
- [架构概览与核心瓶颈](#架构概览与核心瓶颈)
- [Critical — 高概率生产问题](#critical--高概率生产问题)
- [Medium — 中等风险](#medium--中等风险)
- [高价值优化路线图](#高价值优化路线图)
- [已做得好的设计](#已做得好的设计)
- [场景风险矩阵](#场景风险矩阵)
- [优先行动清单](#优先行动清单)
---
## 架构概览与核心瓶颈
```mermaid
flowchart LR
subgraph frontend["前端 (Static Export)"]
A[HTML 静态壳] --> B[Hydrate]
B --> C["UserProvider.getUserInfo()"]
C --> D[页面数据请求]
D --> E[渲染]
end
subgraph backend["后端热点路径"]
F["/f/{id}?quality=..."] --> G[DB 查 upload]
G --> H[迁移状态 DB 查询]
H --> I[白名单 Redis/DB]
I --> J{WebP 缓存命中?}
J -->|否| K["全量读文件 + 编码 + 磁盘缓存(全局锁)"]
J -->|是| L[返回]
end
C -.->|已解除阻塞| D
```
**参数中心读路径**(`SystemConfig.GetByKey`):
```mermaid
flowchart LR
R[业务调用 GetByKey] --> A{RAM 命中?}
A -->|是| Z[返回]
A -->|否| B{Redis HGET 命中?}
B -->|是| C[写入 RAM]
C --> Z
B -->|否| D[查 PostgreSQL]
D --> E[回写 Redis + RAM]
E --> Z
W[管理员 Create/Update] --> F[写 DB]
F --> G["InvalidateSystemConfigCache(key)"]
G --> H[清本机 RAM + Redis field]
G --> I[pub/sub 通知其他节点清 RAM]
```
当前最大的结构性问题(2026-06-17 更新):
1. **前端**:~~全局认证瀑布流~~ ✅ 已改为 layout 即时渲染 + 子页面 `RequireAuth` 自行处理未登录态;~~Admin 重模块无 `dynamic()` 分割~~ ✅ database/logs/settings 已懒加载子模块。其余路由 `page.tsx` 仍为 `"use client"`(静态导出下 RSC 收益有限,待逐步薄壳化)。
2. **后端**:文件服务路径(`/f/{id}`)仍是最高频热点;~~磁盘缓存全局互斥锁~~ ✅ 已改为 `RWMutex` + `singleflight`,但 WebP miss 仍在请求线程内同步编码,部署预热与异步回退原图尚未落地。
3. **参数中心**:~~`GetByKey` 每次直打 Redis~~ ✅ 已统一使用底层的进程内缓存库(`pkg/cache/store`),读路径直接为 RAM → DB(无 Redis 数据缓存);管理员写配置后通过 Redis pub/sub 进行广播(`system:config_broadcast`),多节点本地触发全量预热/刷新,实现最终一致性。
---
## Critical — 高概率生产问题
### 1. 图片 WebP 服务:请求路径阻塞 + 全局锁串行化 `🔶 部分完成`
**涉及文件**:
- `internal/apps/upload/file_server.go`
- `pkg/cache/disk/cache.go`
**问题描述**:
缓存未命中时,在 HTTP 请求 goroutine 内执行:
1. `io.ReadAll` 将原始文件全量读入内存
2. 进程内 WebP 解码 + 编码
3. 写入磁盘缓存
同时,磁盘缓存 `Get`/`Set` 使用**全局 `sync.Mutex`**,所有并发图片请求在缓存层完全串行。
```go
// file_server.go — 缓存 miss 时的重操作
origBytes, err := getOriginalFileBytes(ctx, upload) // io.ReadAll
webpBytes, err = CompressImageToWebP(bytes.NewReader(origBytes), quality)
cache.Set(cacheKey, webpBytes, diskcache.NoExpiration)
// pkg/cache/disk/cache.go — 全局互斥锁
func (c *Cache) Get(key string) ([]byte, error) {
c.mu.Lock()
defer c.mu.Unlock()
// ...
}
```
**生产表现**:
- 首次访问或缓存淘汰后,P99 延迟从几十毫秒飙升到数秒
- 并发图片请求形成「隐形队列」
- 大文件全量读入带来内存尖峰,可能触发 OOM 或 GC 停顿
**优化价值**:⭐⭐⭐⭐⭐
**建议**:
- [x] ✅ 磁盘缓存改用 `RWMutex`,读路径不互斥 — `pkg/cache/disk/cache.go`
- [x] ✅ 对同一 cache key 使用 `singleflight` 合并并发 miss — `internal/apps/upload/file_server.go`
- [ ] 部署后强制执行 `upload:warm_image_cache` 异步预热任务
- [ ] 考虑 miss 时先返回原图,后台异步生成 WebP
---
### 2. 文件访问路径:每次请求多次 DB/Redis 查询 `✅ 已完成`
**涉及文件**:
- `internal/apps/upload/storage_ops.go`
- `internal/apps/upload/file_server.go`
**问题描述**:
存储迁移状态**无进程内缓存**,每次文件操作都查询 `w_task_executions`:
```go
// storage_ops.go
func StorageReadOnly(ctx context.Context) bool {
execution, ok, err := latestStorageMigrationExecution(ctx)
// ...
}
func backendForStoredDriver(ctx context.Context, driver storage.Driver) (storage.Backend, error) {
// 可能再次调用 currentMigrationTargetConfig → 又一次相同 DB 查询
}
```
公开文件白名单每次走 Redis/DB:
```go
// file_server.go
func isFilePublic(ctx context.Context, uploadType string) bool {
sc.GetByKey(ctx, model.ConfigKeyFileAccessWhitelist)
// JSON 解析 + 遍历
}
```
对比:`storage.Active()` 已有 5 秒内存缓存 + Redis pub/sub 失效机制,迁移状态却未复用该模式。
**生产表现**:
- 每个 `/f/{id}` 请求额外 2–4 次 DB/Redis 往返
- 图片站/CDN 场景下 QPS 放大后 PostgreSQL 连接池压力明显
**优化价值**:⭐⭐⭐⭐⭐
**建议**:
- [x] ✅ 为 `StorageReadOnly` / `latestStorageMigrationExecution` 增加 5s TTL 进程内缓存 — `internal/apps/upload/access_cache.go`
- [x] ✅ 配置变更或迁移状态变化时通过 Redis pub/sub 失效 — `access_cache.go` + `system_config/routers.go`
- [x] ✅ `file_access_whitelist` 增加进程内缓存,复用 `GetByKey` 的失效机制 — `access_cache.go`
---
### 3. Admin 文件统计:无界全表扫描 `✅ 已完成`
**涉及文件**:`internal/apps/upload/stats.go`
**问题描述**:
```go
err = db.DB(ctx).Model(&model.Upload{}).
Select("extension, mime_type, file_size").
Where("status != ?", model.UploadStatusDeleted).
Scan(&fileRaws).Error
// 然后在 Go 中遍历全量结果做分类统计
```
**生产表现**:
- 10 万+ 文件时,管理端「文件统计」接口耗时数秒
- 占用数百 MB 内存,可能拖垮 admin API
**优化价值**:⭐⭐⭐⭐
**建议**:
- [ ] 改为 SQL `GROUP BY` + `CASE WHEN` 聚合(未采用)
- [x] ✅ 维护增量统计表,上传/删除时更新计数 — `w_upload_stats` + `stats_counter.go` + `GetFileStats` 读统计表
---
### 4. `w_uploads` 索引缺口 `✅ 已完成`
**涉及文件**:`internal/db/migrator/goose/postgres/202606090001_initial_schema.sql`
**当前索引**:`user_id`, `file_path`, `hash`, `type`
**缺失的高频查询索引**:
| 查询场景 | 建议索引 |
|----------|----------|
| 清理任务 `status + created_at` | `(status, created_at)` |
| 存储迁移 `storage_driver + status` | `(storage_driver, status)` |
| 秒传去重 `hash + file_size + status` | `(hash, file_size, status)` |
**生产表现**:
- 数据量增长后,清理 worker、迁移任务、上传去重退化为顺序扫描
- 后台任务积压,admin 操作变慢
**优化价值**:⭐⭐⭐⭐
**建议**:
- [x] ✅ 通过 goose migration 新增上述复合索引(PostgreSQL + SQLite 双方言)— `202606170001_add_upload_composite_indexes.sql`
---
### 5. 批量 ZIP 下载:无上限 + 同步阻塞
**涉及文件**:`internal/apps/upload/routers.go` — `BatchDownloadFiles`
**问题描述**:
- `req.IDs` 无数量上限
- 在请求 goroutine 内串行打开每个文件并 `io.Copy` 到 ZIP
- 远端 S3 场景下单个文件就可能耗时数秒
**生产表现**:
- 网关超时、连接耗尽
- Admin 批量下载操作卡死
**优化价值**:⭐⭐⭐⭐
**建议**:
- [ ] 限制单次批量数量(如 max 50)
- [ ] 或改为 Asynq 后台任务生成 ZIP,前端轮询下载链接
---
### 6. 前端全局认证瀑布流 `✅ 已完成`
**涉及文件**:
- `frontend/contexts/user-context.tsx`
- `frontend/app/(main)/layout.tsx`
**问题描述**:
```tsx
// user-context.tsx — 挂载时获取用户
useEffect(() => {
fetchUser()
}, [fetchUser])
// layout.tsx — 阻塞所有子页面渲染
if (loading || !user) {
return <LoadingPage text="登录状态" badgeText="Auth" />
}
```
**生产表现**:
- 每次进入 `/home`、`/files`、`/admin/*` 都先等 `getUserInfo`(约 200–800ms)
- 页面级数据请求无法并行启动,TTI 被硬性拉长
**优化价值**:⭐⭐⭐⭐⭐
**建议**:
- [x] ✅ Layout 不阻塞渲染,子页面自行处理未登录状态 — `layout.tsx` + `RequireAuth` / `RequireAdminAuth`
- [ ] 或 Server Component 通过 cookie 预取 session,消除客户端首屏等待
- [x] ✅ `/login`、`/register` 跳过 `getUserInfo` — `user-context.tsx`
---
### 7. 实时日志面板:2000 行 DOM 无虚拟化 `✅ 已完成`
**涉及文件**:`frontend/components/common/admin/app-logs.tsx`
**问题描述**:
- 日志上限 2000 行(内存有界,但 DOM 无界)
- 每行渲染完整 `<div>`,无虚拟滚动
- `@tanstack/react-virtual` 已在 `package.json` 但未使用
**生产表现**:
- 管理员开着日志 Tab 时 CPU/内存持续升高
- 滚动卡顿,长时间运行拖慢整台机器
**优化价值**:⭐⭐⭐⭐
**建议**:
- [x] ✅ 使用 `useVirtualizer` 只渲染可视区域行 — `app-logs.tsx`
- [x] ✅ 行组件 `React.memo` 避免无效重渲染 — `LogLine`
---
## Medium — 中等风险
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 1 | ~~公共配置接口无 Redis 缓存~~ ✅ | `internal/model/system_configs.go` — `ListVisibleSystemConfigs` | ~~每次前端启动/登录直查 PostgreSQL~~ → Redis 列表缓存 + Create/Update 时失效 |
| 2 | ~~CAPTCHA 每次 5 次独立 `GetByKey`~~ ✅ | `internal/apps/cap/runtime_settings.go` | ~~登录高峰 5× 配置读取~~ → `CurrentSettings` 快照一次加载 6 个 key,`Generate`/`Redeem`/中间件零 `GetByKey` |
| 3 | ~~系统配置单 key 无进程内缓存~~ ✅ | `system_config_cache.go`, `pkg/cache/ram` | ~~热路径重复 Redis HGET~~ → Otter RAM + 写后 `InvalidateSystemConfigCache` + pub/sub |
| 4 | OIDC 每次 `oidc.NewProvider` 无缓存 | `internal/apps/oauth/sources.go:164` | 登录发起/回调多一次外部 HTTP |
| 5 | CORS 每次跨域查 `server_address` 配置 `🔶` | `internal/router/middlewares.go:75` | 预检请求仍每次调用 `GetByKey`,但 `server_address` 已受益于 RAM 缓存 |
| 6 | 推送通知无界 goroutine + 逐 target DB 查询 | `internal/apps/admin/push/events.go:102` | 通知风暴时 goroutine/DB 双压 |
| 7 | 上传清理:每文件一个事务 | `internal/apps/upload/cleanup.go` | 大量 pending 文件时 commit 风暴 |
| 8 | ClickHouse 风控:每请求 `json.Marshal` 全部 headers | `internal/apps/risk_control/middleware.go:58` | 高 QPS 时 CPU 开销(写入本身已异步批处理) |
| 9 | 存储迁移日志大量写 Redis | `internal/apps/upload/storage_migration_task.go` | 迁移期间 Redis CPU/内存压力 |
| 10 | 存储迁移后二次 SHA 全量读取验证 | `storage_migration_task.go` | 迁移期间对象 I/O 翻倍 |
| 11 | Admin 状态页 5s 轮询 | `frontend/components/common/admin/status.tsx` | Tab 常驻时持续打后端 |
| 12 | 路由切换 500ms fade 动画 | `frontend/app/(main)/layout.tsx:53-60` | 即使数据已缓存,感知仍慢 |
| 13 | ~~无 `next/dynamic` 代码分割~~ ✅ | `database/`, `logs/`, `settings/` page-client | Admin 重模块拆分为独立 chunk |
| 14 | 19/24 个 `page.tsx` 为 `"use client"` `🔶` | 各路由 | database/logs/settings 已薄壳化;其余待迁移 |
| 15 | ~~Admin 部分页面用 `useEffect` 而非 React Query~~ ✅ | `access-logs.tsx`, `task-executions.tsx` | 列表/详情走 React Query 缓存去重 |
| 16 | ~~登录页 OIDC sources 等待 public config~~ ✅ | `login-form.tsx` | public config 与 auth sources 并行请求 |
| 17 | ~~Users 表每行嵌套 3 个 `TooltipProvider`~~ ✅ | `admin/users/page.tsx` | 表格外层单一 Provider |
| 18 | ~~缩略图用原生 `<img>` 无 lazy loading~~ ✅ | `file-list.tsx`, `file-manager.tsx` | `loading="lazy"` + `decoding="async"` |
| 19 | ~~`@/lib/services` barrel 导入~~ ✅ | 全前端消费侧 | 改为 `@/lib/services/<module>` 直接导入 |
| 20 | SQLite 模式无连接池调优 | `internal/db/postgres.go` | 默认 SQLite 写锁瓶颈 |
| 21 | Session Redis 仅用第一个地址 | `internal/router/router.go` | Sentinel/Cluster 场景不一致 |
---
## 高价值优化路线图
### P0 — 立即做(1–2 周,收益最大)
| # | 优化项 | 涉及模块 | 预期收益 | 复杂度 | 状态 |
|---|--------|----------|----------|--------|------|
| 1 | WebP:`singleflight` + `RWMutex` + 强制预热 | `file_server.go`, `pkg/cache/disk/` | 图片 P99 ↓ 80%+,并发吞吐 ↑ 5–10x | 中 | 🔶 锁与去重已完成,预热待做 |
| 2 | 缓存 `StorageReadOnly` / 迁移状态 | `access_cache.go` | 每文件请求减少 1–3 次 DB | 低 | ✅ |
| 3 | 内存缓存 `file_access_whitelist` | `access_cache.go` | 每公开文件请求减少 1 次 Redis | 低 | ✅ |
| 4 | `GetFileStats` 增量统计表 | `stats.go`, `w_upload_stats` | Admin 统计从 O(n) → O(1) | 低 | ✅ |
| 5 | 新增 `w_uploads` 复合索引 | goose migration | 清理/迁移/秒传全面加速 | 低 | ✅ |
| 6 | 前端日志虚拟化 | `app-logs.tsx` | Admin 日志 Tab 流畅度质变 | 低 | ✅ |
| 7 | Admin 重模块 `dynamic()` 懒加载 | `database/page-client.tsx`, `logs/page-client.tsx`, `settings/page-client.tsx` | 首包 JS ↓ 150–300KB | 低 | ✅ |
### P1 — 短期(2–4 周)
| # | 优化项 | 预期收益 | 状态 |
|---|--------|----------|------|
| 8 | 认证并行化:layout 不阻塞 / Server 预取 session | TTI ↓ 200–800ms | 🔶 客户端并行化已完成,RSC 预取待做 |
| 9 | `ListVisibleSystemConfigs` 加 Redis 缓存 | 前端冷启动加速 | ✅ |
| 10 | 系统配置 Otter RAM 缓存 + 统一失效 | 热路径 `GetByKey` 零 Redis RTT(命中后) | ✅ |
| 11 | CAPTCHA 运行时配置快照 | 验证码路径配置读取 → O(1) 快照 | ✅ |
| 12 | OIDC Provider/JWKS 进程内缓存(TTL 1h) | 登录延迟 ↓ 100–500ms | ⬜ |
| 13 | 批量下载限制(max 50)或异步任务 | 消除网关超时风险 | ⬜ |
| 14 | Admin `useEffect` 数据获取迁移到 React Query | 去重、缓存、后台刷新 | 🔶 access-logs / task-executions 已完成 |
| 15 | 登录页并行请求 public config + auth sources | 登录页 ↓ 100–300ms | ✅ |
| 16 | 状态轮询在 `document.hidden` 时暂停 | 降低后台 + 客户端负载 | ⬜ |
### P2 — 中期架构演进
| # | 优化项 | 预期收益 |
|---|--------|----------|
| 16 | 批量 ZIP 改为 Asynq 后台任务 | 彻底解耦长耗时操作 |
| 17 | 存储迁移日志降噪 + 跳过已验证文件二次 SHA | 迁移期间 Redis/I/O ↓ 50% |
| 18 | 推送通知 target 批量解析(`WHERE id IN ?`) | 通知风暴 DB 查询 ↓ N 倍 |
| 19 | 上传清理改为批量 UPDATE + 异步存储删除 | 减少 DB commit 频率 |
| 20 | 路由动画 0.5s → 0.15s 或纯 CSS | 导航感知速度 ↑ |
| 21 | ~~服务导入收窄(直接 import 具体 Service)~~ ✅ | 每路由 bundle ↓ 10–30KB |
| 22 | Admin 路由级 `loading.tsx` + Suspense | 渐进式渲染体验 |
| 23 | ~~缩略图 `loading="lazy"` + 固定尺寸~~ ✅ | 文件管理页初始 paint 加速 |
---
## 已做得好的设计
以下设计说明团队已有性能意识,优化应在此基础上增量改进,**不必重复造轮子**:
| # | 设计 | 位置 |
|---|------|------|
| 1 | 系统配置两层缓存 RAM → DB | `pkg/cache/store`, `system_config_cache.go`, `GetByKey` |
| 2 | 系统配置统一刷新 + 多节点 pub/sub 预热广播 | `InvalidateSystemConfigCache`, `InvalidateAllSystemConfigCaches` |
| 3 | Storage Backend 单例 + 5s TTL + pub/sub 失效 | `internal/storage/storage.go` — `Active()` |
| 4 | 推送事件/渠道 24h Redis 缓存 + GORM hook 失效 | `internal/model/push_event.go`, `push_channel.go` |
| 5 | 风控日志异步批写 ClickHouse(1 万缓冲 + 1000 条/1s + 429 背压) | `internal/apps/risk_control/` |
| 6 | HTTP 连接池统一(`httppool` + OTel) | `pkg/httppool/` |
| 7 | DB/Redis 连接池显式配置 | `config.yaml`, `internal/db/` |
| 8 | 游标分批处理(`id > ? LIMIT n`) | `cleanup.go`, image warmup |
| 9 | 存储迁移并发上限 `errgroup.SetLimit(10)` | `storage_migration_task.go` |
| 10 | 邮件/推送走 Asynq,不在 HTTP 路径同步发送 | `user/logics.go`, `push/events.go` |
| 11 | 文件服务 ETag/304 + 原图 `DataFromReader` 流式返回 | `file_server.go` |
| 12 | 无 GORM `Preload` 滥用 | 全项目 |
| 13 | 前端 API 请求去重(`pendingRequests` Map) | `frontend/lib/services/core/api-client.ts` |
| 14 | React Query 全局 30s `staleTime` | `frontend/components/providers/query-provider.tsx` |
| 15 | React Compiler 已启用 | `frontend/next.config.ts` |
| 16 | 读副本支持(`dbresolver`) | `internal/db/postgres.go` |
| 17 | 任务执行日志 Redis 缓冲 + 批量回写 | `internal/model/task_execution.go` |
| 18 | 公共配置列表 Redis 缓存 + 写后失效 | `ListVisibleSystemConfigs`, `InvalidateVisibleSystemConfigsCache` |
| 19 | 上传文件统计增量表 `w_upload_stats` | `stats_counter.go`, 上传/删除 hook |
| 20 | 文件访问路径进程内缓存 + pub/sub | `internal/apps/upload/access_cache.go` |
| 21 | 磁盘缓存读路径 `RWMutex` + WebP `singleflight` | `pkg/cache/disk/cache.go`, `file_server.go` |
| 22 | 前端认证非阻塞 + 页面级鉴权 | `use-auth-redirect.ts`, `require-auth.tsx` |
| 23 | Admin 实时日志虚拟滚动 | `frontend/components/common/admin/app-logs.tsx` |
| 24 | CAPTCHA 运行时配置快照 + 批量加载 | `runtime_settings.go`, `ListSystemConfigsByKeys` |
---
## 场景风险矩阵
| 场景 | 最可能爆的点 | 对应优先级 |
|------|-------------|-----------|
| 图片站 / 公开相册 | WebP miss(锁/白名单已优化) | P0 #1 预热待做 |
| 文件量 10 万+ | 清理慢(统计/索引已优化) | P2 #19 清理批量化 |
| 管理端日常使用 | ~~大 bundle~~(dynamic 分割 + barrel 收窄已落地) | P2 #22 路由 loading.tsx |
| 存储迁移进行中 | Redis 日志风暴 | P2 #17 |
| 登录高峰 | OIDC discovery 无缓存 | P1 #12 OIDC |
| 多租户 / 跨域前端 | CORS 仍每次调 `GetByKey`(`server_address` 已 RAM 缓存) | 可选 CORS 快照 |
| 参数热更新 | 多节点 RAM 一致性 | ✅ `system:config_invalidation` pub/sub |
| 批量文件操作 | ZIP 同步打包无上限 | P0 #5, P1 #12 |
---
## 优先行动清单
如果只选 **3 件事** 先做(预计用户感知延迟降低 50–70%):
1. ~~**WebP 路径解耦**~~ ✅ `singleflight` + `RWMutex` 已落地;**下一步**:部署后预热 + miss 异步回退原图
2. ~~**文件路径查询缓存**~~ ✅ 迁移状态 + 白名单进程内缓存已落地
3. ~~**前端认证与首屏并行化**~~ ✅ 全局 auth gate 已移除;~~Admin `dynamic()` 代码分割~~ ✅ 已落地;**下一步**:其余 Admin 路由薄壳化 + `loading.tsx`
### 实施检查清单
```
P0 后端
[x] disk cache RWMutex + singleflight ✅ 2026-06-17
[x] StorageReadOnly 5s 缓存 + pub/sub 失效 ✅ 2026-06-17
[x] file_access_whitelist 进程内缓存 ✅ 2026-06-17
[x] GetFileStats 增量统计表 (w_upload_stats) ✅ 2026-06-17
[x] w_uploads 复合索引 migration ✅ 2026-06-17
[ ] 批量下载数量上限
[ ] WebP 部署预热 + miss 异步回退原图
P0 前端
[x] app-logs.tsx 虚拟滚动 ✅ 2026-06-17
[x] SQLConsole / Settings Tabs / Logs Tabs dynamic import ✅ 2026-06-17
[x] 认证 gate 并行化 ✅ 2026-06-17
[x] 登录页 public config + auth sources 并行 ✅ 2026-06-17
[x] access-logs / task-executions → React Query ✅ 2026-06-17
[x] Users TooltipProvider 合并 ✅ 2026-06-17
[x] 缩略图 loading="lazy" ✅ 2026-06-17
[x] @/lib/services barrel 导入收窄 ✅ 2026-06-17
P1
[x] ListVisibleSystemConfigs Redis 缓存 ✅ 2026-06-17
[x] 系统配置 Otter RAM 缓存 + 统一失效 + pub/sub ✅ 2026-06-17
[x] CAPTCHA 运行时配置快照 ✅ 2026-06-17
[ ] OIDC Provider 缓存
[ ] Admin useEffect → React Query 统一(database overview 等待)
[ ] 状态轮询 visibility 感知
[ ] Server Component session 预取
```
---
## 附录:关键代码路径索引
| 路径 | 文件 | 说明 |
|------|------|------|
| 图片服务 | `internal/apps/upload/file_server.go` | `/f/{id}` 热点 |
| 磁盘缓存 | `pkg/cache/disk/cache.go` | ✅ RWMutex 读路径 |
| 迁移/白名单缓存 | `internal/apps/upload/access_cache.go` | ✅ 5s TTL + pub/sub |
| 文件统计 | `internal/apps/upload/stats.go` | ✅ 读 `w_upload_stats` |
| 公共配置列表 | `internal/model/system_configs.go` | ✅ Redis 列表缓存 |
| RAM 缓存封装 | `pkg/cache/ram/cache.go` | ✅ Otter v2 薄封装 |
| 系统配置缓存 | `internal/model/system_config_cache.go` | ✅ RAM + 失效 + pub/sub |
| 参数失效 API | `InvalidateSystemConfigCache` | ✅ 清 RAM + Redis field |
| CAPTCHA 快照 | `internal/apps/cap/runtime_settings.go` | ✅ `CurrentSettings` + pub/sub |
| 批量下载 | `internal/apps/upload/routers.go` | 同步 ZIP |
| 上传索引 | `internal/db/migrator/goose/*202606170001*.sql` | ✅ 复合索引已加 |
| 认证 gate | `frontend/app/(main)/layout.tsx` | ✅ 即时渲染 + `useAuthRedirect` |
| 页面鉴权 | `frontend/components/auth/require-auth.tsx` | ✅ 子页面按需拦截 |
| 用户上下文 | `frontend/contexts/user-context.tsx` | ✅ 登录/注册页跳过 fetch |
| 实时日志 | `frontend/components/common/admin/app-logs.tsx` | ✅ `useVirtualizer` |
| API 去重 | `frontend/lib/services/core/api-client.ts` | 已有,可复用模式 |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

-630
View File
@@ -1,630 +0,0 @@
---
sidebar: false
---
# 更新日志
本文件记录 OpenFlare 每个版本的重要变更。
格式基于 [Keep a Changelog](http://keepachangelog.com/),版本号遵循 [语义化版本](http://semver.org/)。
## 重大变更
> [!IMPORTANT]
>
> 3.1.2 版本更新了 CLickHouse 部署配置。
>
> 3.0.0 版本为 Wavelet 平台迁移与架构重构版本,涉及数据库表结构、环境变量以及前后端底层架构的重大变更。请务必在升级前备份数据库,并且更新到 V2.3.4。
> 目前已知的兼容性问题:
>
> - Pages 无法迁移, 升级前请先手动下载并备份 Pages 静态站点的 ZIP 包,升级后重新创建。
> - 性能调优参数重置, 升级后请重新配置
## [unreleased]
## [v3.4.2] - 2026-07-19
### 新增
- 安全性新增「限流」设置:可为边缘站点配置默认并发与带宽;站点未设置时继承,填 `-1` 可显式关闭。
- Pages 项目新增持久部署源,可配置 Remote URL 或公开 GitHub Release,并支持手动检查、同步发布、来源状态查看与同一 Release 资源替换确认;GitHub latest 来源可按设定间隔自动检查并发布更新,部署历史会保留安全的来源快照。
- Pages 部署源默认扫描间隔调整为每天一次,部署源任务可在任务管理中查看与调度。
### 改进
- 站点流量限制语义调整为空或 `0` 继承全局默认、`-1` 关闭、大于 `0` 自定义;修改全局默认后需发布配置版本生效。
- Agent Docker 部署命令默认挂载命名卷 `openflare-agent-pages` 持久化 Pages 目录,重建容器时无需重新拉取静态站点包。
- 限流页新增「分析」视图:默认展示近 24 小时请求压力(RPS)与独立访客双轴趋势(3 分钟桶),支持域名过滤与 24 小时/3 天预设,并按窗口平均 RPS 排行域名与 IP;原全局默认配置迁入「配置」页签。
- Pages 详情页重构为「部署 / 设置」Tab,部署源卡片样式更紧凑统一,Remote URL 改为明文编辑。
### 修复
- 修复 Pages 部署包路径校验、归档展开限额、历史版本裁剪、代理路由绑定与 Agent 下载过程中的安全和一致性问题;大包改为流式处理,部署入口、旧版目录切换、保留版本及上传记录在并发场景下更加可靠,异常中断遗留的部署包也会被安全补偿清理。
## [v3.4.1] - 2026-07-19
### 新增
- WAF 规则编排新增「UA 检查」节点:可要求携带 User-Agent、按浏览器/操作系统白名单(且/或)匹配,并优先屏蔽常见爬虫、非正常 UA(不含爬虫)与自定义正则 UA。
- WAF 规则编排新增「安全防护」节点:可开关路径穿越、文件包含、SQL 注入、XSS、命令注入、SSRF、恶意上传、XXE 与 CRLF 等基础特征检测;默认仅开启路径穿越与文件包含。
### 改进
- 新建反代规则时默认开启边缘缓存,策略为仅缓存标准静态资源。
- 节点详情页 Tab 调整为「概览」与「状态与部署」:原数据看板并入概览;运行状态与配置信息并入状态与部署;边缘节点新增可自动填充 Server URL 与 Agent Token 的 Docker 部署命令卡片。
- 节点详情「运行诊断」摘要不再展示具体错误日志,避免长日志撑破布局。
- WAF 规则编辑器支持为节点自定义显示名称,并从节点库拖放到画布指定位置添加节点。
- WAF 规则画布支持右键删除节点或连线,并屏蔽浏览器默认右键菜单。
- WAF 规则编辑器支持一键格式化布局,按流程层次自动整理节点位置。
- 优化边缘 WAF「安全防护」与「UA 检查」热路径:SQL/命令/XSS 等仅扫描 Query、Cookie、Referer 与有限 Body,避免对全部请求头做特征匹配;路径检测不再重复扫描完整 `request_uri`;无请求体时跳过 Body 读取;UA 分类仅小写一次并加速白名单匹配,显著降低开启基础防护时的 CPU 占用。
- 优化边缘 WAF「IP 匹配」:IP 组与节点 IP/CIDR 在加载时编译为索引(优先随 Agent 下发的 `resty.ipmatcher` 基数树,否则 exact 哈希 + 预解析 CIDR),查询与名单规模解耦,避免大名单线性扫描打满 CPU。
- Agent 内嵌 `resty.ipmatcher`,部署时不再依赖无效 opm 包。
### 修复
- 收紧 WAF 安全防护特征,降低对常见正常请求的误伤(含避免 SQL 特征 `/* */` 误匹配 `Accept: */*`)。
- 优化 WAF 规则编辑器返回按钮、列表操作与属性栏布局体验。
## [v3.4.0] - 2026-07-19
### 新增
- 访问日志重构为「概览」「IP 明细」与「日志明细」:概览含请求量/访问量/带宽趋势与 Top 排行;IP 明细可按时间窗查看请求数、2xx 比例、入出站流量并支持详情分析;日志明细展示完整请求字段。
- 边缘访问日志支持 User-Agent 与 `cache_status`(命中/回源/未缓存);概览新增设备类型、浏览器、操作系统与状态码分布。
- 访问日志概览支持按 Zone/域名多选筛选;明细列表在 IP 旁展示地区信息。
- 新建站点开启缓存时推荐「标准静态资源」(不含 HTML);原按 URL/空策略存量行为保留为「所有可缓存 GET」。
- Pages 现支持上传 zip、tar.gz、tar.xz、tar.bz2、tar、7z 等常用压缩格式的部署包。
- 管理员可在运维设置中配置 Pages 部署包大小上限与每个项目的历史部署保留数量。
- Pages 支持从 URL 导入部署包:填写下载链接后由控制面代为拉取并创建部署。
- 观测存储新增 `of_node_edge_health` 与 `of_access_log_hourly`,业务趋势优先读访问日志小时汇总。
- 访问日志增加 `request_length` / `request_time_ms`,用于接收数据与耗时统计。
### 修复
- 修复访问日志概览按域名筛选无效的问题,现已兼容 `hosts` / `hosts[]` 参数。
- 修复 Agent 观测缓冲合并访问日志时忽略 `cache_status` 导致缓存状态被去重丢弃的问题。
- 修复访问日志概览在 ClickHouse 查询失败时静默吞错的问题,现会输出错误日志。
- 修复数据看板业务流量趋势与已提供数据口径不一致的问题:业务量统一由访问日志聚合。
- 修复节点地图在缺少精确经纬度时,把香港/新加坡/台湾等地区错误标到占位坐标的问题。
### 变更
- 边缘观测改为「访问日志为业务唯一真相」:Agent 仅上报明细、主机指标与 OpenResty 健康/连接;协议去掉旧兼容字段,**升级需重建或替换 Agent**。
- Agent 默认心跳改为 3 秒、离线判定 60 秒,离线补传窗口默认 60 分钟。
- 看板 UV 使用窗口内真正去重;Zone 曲线标明分桶 UV;磁盘读写改为按小时速率(B/s)展示。
- 不再采集或展示宿主机网卡入/出站;网络趋势仅保留访问日志已提供/接收数据。
- Pages 包大小与历史保留可配置,边缘按项目只保留最新激活部署;创建规则表单与详情一致支持直连/隧道/Pages 源站类型。
- 优化 Pages 部署包校验性能:不再为包内每个文件计算哈希,整包校验和保障完整性。
- 优化访问日志排行榜与饼图布局;页签状态支持 URL 参数记忆。
- 启用 `cache_status` 与边缘缓存策略变更需执行相关迁移并重新发布节点配置。
### 移除
- 移除请求预聚合表与 OpenResty 吞吐观测相关路径;管理端不再返回 `traffic_reports` 与 `openresty_rx|tx`。
- 访问日志已移除时间折叠视图;IP 情报从日志明细详情迁出至 IP 明细。
## [v3.3.0] - 2026-07-14
### 新增
- WAF 规则现支持可视化编排、版本冲突保护和按顺序绑定路由,便于创建和维护复杂的防护策略。
- WAF IP 组现支持按城市匹配来源地址,帮助更精细地控制访问范围。
### 变更
- 优化了 WAF 规则编辑器的初始视图和操作方式,编辑规则时可看到更多上下文并可直接管理节点、连线和启用状态。
- WAF 地域匹配编辑器改用完整国家与一级行政区数据,国家选项同时显示中文名称和 ISO 代码,行政区支持按名称或代码搜索。
- Agent 现内置国家和城市地址库,首次启动无需下载即可使用地区匹配功能,并会在后续自动更新数据。
- 默认关闭 Redis maintenance notifications 自动协商,减少不支持该功能的 Redis 服务产生兼容性警告。
### 移除
- 移除了 WAF 旧版固定名单与人机验证配置;升级后请在发布前使用新的可视化规则重新编排防护策略。
## [v3.2.0] - 2026-07-12
### 新增
- 新增网站和域名管理能力,并提供 24 小时、7 天和 30 天的流量概览,便于集中查看访问趋势和已提供的数据量。
### 变更
- 网站管理入口调整为网站详情中的概览、域名、路由、证书和设置页面,域名与证书的关联方式更加统一。
- 配置发布、边缘代理和监控现统一从网站域名读取域名与证书,减少配置不一致导致的运行问题。
- 自动清理说明明确了分析数据的最短保留期限,便于管理员预期数据保存时间。
### 移除
- 移除了旧版托管域名管理入口,请改用网站及网站域名管理功能。
- 移除了网站、域名、路由和 WAF 相关对象的备注字段;证书和源站备注仍可继续使用。
### 修复
- 修复了网站概览中已提供的数据量无法统计的问题,使流量数据更加准确。
- 修复了嵌入式前端打开网站详情时可能错误跳回首页的问题。
- 修复了 Docker 部署中 ClickHouse 可能无法从宿主机访问的问题。
## [v3.1.2] - 2026-07-10
### 修复
- 修复了节点和仪表盘在 24 小时范围内容量、网络与磁盘趋势数据不完整的问题。
- 优化了 ClickHouse 的写入、查询和后台处理方式,降低节点空闲时的资源占用并提升高负载下的稳定性。
- 修复了数据保留清理和写入失败重试的统计问题,使清理结果和运行状态更可信。
- 改进了小规格环境下的 ClickHouse 部署配置,减少启动和连接争用问题。
## [v3.1.1] - 2026-07-06
### 修改
- 默认关闭登录页面的人机验证,减少普通登录流程的额外操作;管理员仍可按需启用。
## [v3.1.0] - 2026-07-04
### 变更
- 优化了分析数据的写入、查询、缓存和自动过期策略,降低高频心跳和访问日志对系统资源的影响。
- 调整了 ClickHouse 的连接、批处理和 Docker 部署配置,提升小规格环境下的运行稳定性。
- 收紧了审计访问日志的请求头记录范围并进行脱敏,减少敏感数据暴露风险。
- 更新了管理后台的文档入口和全局搜索范围,使常用功能更容易查找。
### 修复
- 修复了数据库迁移、系统自更新和设置页跳转可能失败的问题。
## [v3.0.2] - 2026-06-30
### 修复
- 修复了历史数据迁移后 PostgreSQL 自增编号可能与现有数据冲突的问题,避免后续创建记录失败。
## [v3.0.1] - 2026-06-30
### 新增
- 新增用户资料编辑、密码重置和按邮箱搜索功能,便于管理员维护用户账号。
- 新增命令行密码重置工具,方便无法登录管理后台时恢复账号访问。
### 修复
- 修复了创建 DNS 账号可能失败的问题。
- 修复了主题切换后侧边栏和危险操作按钮颜色异常的问题,提升界面可读性。
- 修复了部分服务运行模式无法正确启动的问题。
## [v3.0.0] - 2026-06-27
### 升级与迁移注意事项
> [!WARNING]
> 本次重构涉及数据库表结构以及环境变量的重大变更,老版本务必从 v2.3.4 最新版本升级迁移,否则可能导致数据库结构不兼容或管理端 API 无法访问。
> 升级前务必备份数据库
### 重大重构说明
本版本完成了控制面的重大升级:
- 管理后台重构为统一的用户、登录验证和系统设置体验,配置管理更加集中。
- 网站管理拆分为域名、路由、静态托管、WAF 和缓存等独立能力,更适合维护复杂站点配置。
- Tunnel 节点统一纳入节点管理,配置发布和运行状态查看更加一致。
## [v2.3.4] - 2026-06-17
### 变更
- 访问日志列表查询将分页与计数下推到数据库执行,避免百万级数据全量加载到内存。
- 访问日志 `total_ip` 统计改为 SQL `UNION` + `COUNT(*)` 下推执行,分片计数与分页查询并行化。
- 访问日志折叠视图、IP 汇总与趋势改为 SQL `GROUP BY` 聚合;过滤条件改为 `node_id` 精确匹配及其他字段前缀匹配以利用索引。
- 标准化 Server Go 目录结构,引入 `cmd/server`、`openflare-server/internal` 与根级 `pkg` 分层,并拆分原 `utils` 公共能力包。
## [v2.3.3] - 2026-06-06
### 新增
- 新增密码登录人机验证(基于 Proof-of-Work 和无感浏览器检测的 Cap 验证码防护)
- 新增后端 PoW 校验服务,实现 FNV-1a/XORShift PRNG 难题生成、验证及 JWT 难题校验算法,支持基于路由路径参数 `scope` 进行验证流的强校验与安全隔离
- 新增线程安全的内存 TTL 核销缓存,支持高并发与 Single-use 难题令牌防重放
- 新增 Gin 拦截中间件与参数化路由 `/api/cap/:scope/challenge` 和 `/api/cap/:scope/redeem`,登录接口 `POST /api/user/login` 自动从 HTTP 请求头校验 `X-Cap-Token` 并放行
- 前端登录页集成 cap-widget 组件,配置 `/api/cap/login/` 隔离端点按需加载 CDN 脚本,实现静默 PoW 求解与令牌提交
- 管理后台系统设置页“登录与注册开关”中新增“启用登录人机验证”开关,支持热更新全局防护状态
- 新增 Agent 交互式安装向导,支持选择本地安装和 Docker 运行模式;未传参数时自动进入交互菜单
- 新增 Docker 运行模式的智能环境检查,检测到未安装 Docker 时支持一键在线安装,中国大陆环境支持多镜像源自动测速优选与加速器配置
- 新增 Agent 交互式卸载向导,支持选择本地卸载和 Docker 容器卸载模式;未传参数时自动进入交互菜单
### 变更
- 重构 `install-agent.sh` 安装脚本与 `uninstall-agent.sh` 卸载脚本以兼容交互式导引、非交互式命令行参数及 Docker 部署/卸载参数(`--docker`/`--method docker`)
- 重构 Go 包依赖结构为统一模块(Monorepo),模块命名为 `github.com/rain-kl/openflare`
- 移除各子目录下独立的 `go.mod`/`go.sum` 文件,统一由根目录 `go.mod` 进行全局依赖管理与依赖版本锁定
- 替换全仓库 Go源文件中的内部引用路径,由本地相对路径迁移为标准 GitHub 绝对导入路径
- 适配 Docker 镜像构建,所有组件镜像的 Dockerfile 调整为基于根目录的上下文编译
- 更新 GitHub release 自动化发布流水线,适配全新 monorepo 包结构与符号信息注入路径
- 简化并重构数据库历史迁移校验逻辑,将版本 2 至 6 的中间校验函数合并到基线校验函数 `validateDatabaseSchemaV7` 中,消除冗余代码
- 重构数据库历史迁移校验架构,引入基于 GORM 反射解析(`schema.Parse`)的通用自动表结构校验,彻底废弃老版本中大量手动编写的 `HasTable`/`HasColumn` 结构字段存在性检测代码
---
## [v2.3.2] - 2026-06-04
### 说明
> [!IMPORTANT]
> 2.3.2 开始使用 JWT_SECRET 环境变量替代 SESSION_SECRET 进行管理端 API 的 JWT 签名密钥管理。SESSION_SECRET 将会在之后的版本中逐步废弃,请务必尽快迁移到 JWT_SECRET。
### 新增
- 新增 `JWT_SECRET` 环境变量,专用于管理端 API JWT 签名密钥;生产环境必须显式配置
- 新增 VitePress 更新日志页面(`docs/changelog/index.md`),记录所有版本变更历史
### 变更
- 管理端 API 鉴权框架迁移至 `gin-jwt`
- 认证方式变更为 Headers 认证.
- `JWT_SECRET` 优先于 `SESSION_SECRET` 用于 JWT 签名;未配置时回退到 `SESSION_SECRET`,向下兼容
- 屏蔽手动升级入口(`/api/update/manual-upload`、`/api/update/manual-upgrade`),前端隐藏对应 UI 组件
---
## [v2.3.1] - 2026-06-03
### 变更
- 屏蔽手动升级入口,前端隐藏对应 UI 组件
- POW 与 WAF 规则合并, 统一逻辑处理
---
## [v2.3.0] - 2026-06-03
### 新增
- WAF IP 组支持订阅模式,可从远程文本或 JSON 源定时同步
- 新增 Pages 静态站点托管,支持 SPA fallback 路由配置
- Agent 实现 WebSocket 实时推送,Server 发布配置后立即通知在线 Agent
### 变更
- Agent 数据面与 OpenResty 合并为集成镜像部署方式
- 访问日志与观测数据支持数据库分片,按 ID 分片替代原有逻辑
---
## [v2.2.8] - 2026-06-03
### 修复
- 修复多域名部署场景下跨域认证绕过安全漏洞
---
## [v2.2.6] - 2026-06-02
### 新增
- 新增 Uptime Kuma 集成,支持自动同步监控任务
- WAF 新增 PoW(工作量证明)防护能力,可配置有效期
### 变更
- 内网穿透支持 TunnelRelay 中继节点(frps),新增 OpenFlared 客户端(frpc)
---
## [v2.2.5] - 2026-06-02
### 新增
- 新增 WAF 自动 IP 组,支持基于 Expr 规则定时聚合请求日志更新名单
- WAF IP 组黑白名单支持直接引用 IP 组对象
### 变更
- WAF 规则组与网站解耦,支持全局规则组和自定义规则组独立管理
---
## [v2.2.4] - 2026-06-02
### 新增
- WAF 规则组新增拦截返回配置 Tab
### 修复
- 修复 WAF 配置发布后部分规则不生效的问题
---
## [v2.2.3] - 2026-06-02
### 新增
- 新增 WAF 安全防护模块,支持 IP 黑白名单和地域拦截规则
---
## [v2.2.2] - 2026-06-01
### 变更
- 观测数据支持按时间窗口自动清理,新增数据库自动清理调度器
---
## [v2.2.1] - 2026-06-01
### 修复
- 修复仪表板概览数据压缩与规范化问题
---
## [v2.2.0] - 2026-06-01
### 新增
- 新增 TLS 证书转换为 ACME 托管证书的接口(`/convert-acme`)
- 新增 ACME 账号与 DNS 账号管理页面
- 支持 Let's Encrypt 自动申请与续期
---
## [v2.1.1] - 2026-06-01
### 变更
- Agent 架构调整,采用集成镜像方式内置 OpenResty
---
## [v2.0.3] - 2026-05-31
### 修复
- 修复版本号生成逻辑,确保使用当日最大序列号
---
## [v2.0.1] - 2026-05-30
### 修复
- 修复 GitHub 登录逻辑异常
---
## [v2.0.0] - 2026-05-30
### 新增
- 全面重构发布模型,引入配置版本不可变快照机制
- 支持配置版本回滚(重新激活旧版本)
- 新增 `source_config_json` 与 `support_files` 供 Agent 获取完整配置包
- 新增节点专属 Agent Token 与 Discovery Token 双轨鉴权
### 变更
- 数据库迁移框架切换至 goose,统一管理版本升级步骤
- Agent API 与管理端 API 鉴权完全分离
---
## [v1.9.3] - 2026-05-30
### 修复
- 修复节点 IP 自动探测逻辑,优先使用公网地址
---
## [v1.9.2] - 2026-05-29
### 变更
- Agent 心跳超时后自动退回 HTTP 轮询模式
---
## [v1.9.1] - 2026-05-29
### 修复
- 修复 Agent WebSocket 升级失败时的重连逻辑
---
## [v1.9.0] - 2026-05-29
### 新增
- Agent 支持 WebSocket 长连接,Server 发布后实时推送配置变更
---
## [v1.8.0] - 2026-05-26
### 新增
- 支持自定义 DNS 解析器(`OpenRestyResolvers`)
- 新增历史配置快照清理功能
### 变更
- CORS 配置支持动态源与凭证
- 上游统一渲染为命名 `upstream` 并启用 keepalive
---
## [v1.7.0] - 2026-05-25
### 新增
- 新增 ACME 和 DNS 账号管理功能,支持证书申请与续期
### 变更
- 移除新用户注册功能
- 更新 Go 版本要求至 1.25+
---
## [v1.6.1] - 2026-05-13
### 修复
- 修复个人设置页无法查看第三方认证源及解绑功能
---
## [v1.6.0] - 2026-05-13
### 新增
- 支持 OIDC 单点登录(SSO)
---
## [v1.5.0] - 2026-04-25
### 新增
- 集成 PoW(Anubis)防护,支持有效期配置
---
## [v1.4.0] - 2026-04-01
### 新增
- 支持域名级别独立绑定 TLS 证书,每个域名可单独选择证书
- 新增批量更新配置项接口
- 新增 Agent 卸载脚本
### 变更
- 禁用新用户自助注册
- 默认服务器块新增 HTTPS 握手拒绝支持
---
## [v1.3.2] - 2026-03-30
### 新增
- 网站配置支持多域名绑定与共享设置
- 新增抽屉式规则创建组件
---
## [v1.3.1] - 2026-03-20
### 新增
- 新增源站管理功能,支持源站创建、更新与删除
### 变更
- 重构代理路由页面,优化输入组件与样式
---
## [v1.3.0] - 2026-03-19
### 新增
- 新增数据库观测数据手动和自动清理策略
- 节点访问日志支持数据库分片,按 ID 分片
### 变更
- 数据库版本管理与迁移逻辑重构
---
## [v1.2.0] - 2026-03-19
### 新增
- 支持多上游地址负载均衡
- 新增缓存策略配置(路径前缀、精确路径)
- 节点健康事件清理功能
### 变更
- 上游渲染改为命名 upstream 并启用 keepalive
- 更新 HTTPS 配置,启用 reuseport 与 epoll 事件模型
---
## [v1.1.2] - 2026-03-18
### 变更
- HTTPS 启用 HTTP/2 支持
---
## [v1.1.1] - 2026-03-18
### 新增
- 新增获取配置版本详情 API
### 变更
- 仪表板概览数据结构优化,添加压缩与规范化
---
## [v1.1.0] - 2026-03-18
### 新增
- 新增应用日志分页查询与清理功能
- 新增访问日志 IP 汇总与趋势查询
- 新增 OpenResty DNS 解析器指令支持
- Docker 部署支持在运行中容器内执行 reload
### 修复
- 修复应用结果警告逻辑
- Lua 和证书文件管理重构,优化文件同步与清理机制
---
## [v1.0.2] - 2026-03-17
### 新增
- 支持 PostgreSQL 数据库,添加数据库迁移逻辑
- 新增 Docker Compose 配置,支持 PostgreSQL 联动部署
### 变更
- 多个管理端 API 请求方法从 PUT/DELETE 统一改为 POST
---
## [v1.0.1] - 2026-03-16
### 新增
- 新增 `origin_host` 字段,支持覆盖回源请求的 Host 头
### 修复
- 修复代理配置中 SSL 服务器名称和主机头覆盖逻辑
---
## [v1.0.0] - 2026-03-15
OpenFlare 首个正式版本发布。
### 新增
- 管理端 UI、管理 API、Agent API 基础功能
- 反向代理配置管理与 OpenResty 配置渲染
- 配置版本发布与 Agent 同步
- TLS 证书导入与管理
- 节点注册、心跳与状态观测
- SQLite 数据库支持
-144
View File
@@ -1,144 +0,0 @@
import {type DefaultTheme, defineAdditionalConfig} from 'vitepress'
export default defineAdditionalConfig({
description:
'OpenFlare 是轻量、自托管的 OpenResty 控制面,用于管理反向代理、配置发布、节点同步、TLS 证书与基础观测。',
themeConfig: {
nav: nav(),
sidebar: {
'/guide/': { base: '/guide/', items: sidebarGuide() },
'/reference/': { base: '/reference/', items: sidebarReference() },
'/deployment/': { base: '/deployment/', items: sidebarDeployment() },
'/design/': { base: '/design/', items: sidebarDesign() },
'/changelog/': { base: '/changelog/', items: [] }
},
editLink: {
pattern: 'https://github.com/Rain-kl/OpenFlare/edit/main/docs/:path',
text: '在 GitHub 上编辑此页面'
},
footer: {
message: '基于 Apache License 2.0 发布',
copyright: 'Copyright © OpenFlare contributors'
},
docFooter: {
prev: '上一页',
next: '下一页'
},
outline: {
label: '页面导航'
},
lastUpdated: {
text: '最后更新于'
},
notFound: {
title: '页面未找到',
quote: '这份文档还没有对应页面。',
linkLabel: '前往首页',
linkText: '回到 OpenFlare 文档'
},
langMenuLabel: '语言',
returnToTopLabel: '回到顶部',
sidebarMenuLabel: '菜单',
darkModeSwitchLabel: '主题',
lightModeSwitchTitle: '切换到浅色模式',
darkModeSwitchTitle: '切换到深色模式',
skipToContentLabel: '跳转到内容'
}
})
function nav(): DefaultTheme.NavItem[] {
return [
{ text: '指南', link: '/guide/', activeMatch: '/guide/' },
{ text: '部署', link: '/deployment/', activeMatch: '/deployment/' },
{ text: '参考', link: '/reference/', activeMatch: '/reference/' },
{ text: '设计', link: '/design/', activeMatch: '/design/' },
{ text: '更新日志', link: '/changelog/', activeMatch: '/changelog/' }
]
}
function sidebarGuide(): DefaultTheme.SidebarItem[] {
return [
{
text: '指南',
items: [
{ text: '概览', link: '' },
{ text: '快速开始', link: 'quick-start' },
{ text: 'TLS 证书与自动续期', link: 'certificates' },
{ text: 'Zone 域名迁移', link: 'zone-domain-migration' },
{ text: '新建反代配置', link: 'proxy-config' },
{ text: 'Pages 静态托管使用', link: 'pages-usage' },
{ text: '内网穿透与隧道使用', link: 'tunnel-usage' },
{ text: 'WAF 安全防护使用', link: 'waf-usage' },
{ text: 'WAF 自动 IP 组语法', link: 'waf-ip-group-expr' },
{ text: 'Uptime Kuma 监控同步', link: 'uptime-kuma' },
{ text: 'SSO 登录配置', link: 'sso' },
{ text: '发布第一份配置', link: 'first-site' },
{ text: '故障排查', link: 'troubleshooting' },
{ text: '引用与致谢', link: 'credits' }
]
}
]
}
function sidebarReference(): DefaultTheme.SidebarItem[] {
return [
{
text: '参考',
items: [
{ text: '概览', link: '' },
{ text: '配置项', link: 'configuration' },
{ text: '命令与脚本', link: 'cli' }
]
}
]
}
function sidebarDeployment(): DefaultTheme.SidebarItem[] {
return [
{
text: '部署',
items: [
{ text: '概览', link: '' },
{ text: '部署说明', link: 'deployment' },
{ text: '启动 Server', link: 'server' },
{ text: '接入 Agent', link: 'agent' },
{ text: '部署 Relay (Tunnel)', link: 'relay' },
{ text: '部署 OpenFlared', link: 'openflared' },
{ text: '升级与维护', link: 'upgrade' }
]
}
]
}
function sidebarDesign(): DefaultTheme.SidebarItem[] {
return [
{
text: '设计',
items: [
{ text: '产品边界', link: '' },
{ text: '系统架构', link: 'architecture' },
{ text: 'Zone 与域名资源设计', link: 'zone-design' },
{ text: 'Agent 与发布模型', link: 'agent-design' },
{ text: '内网穿透隧道设计', link: 'tunnel-design' },
{ text: 'WAF 设计', link: 'waf-design' },
{ text: 'WAF 可编排规则设计', link: 'waf-orchestration-design' },
{ text: 'Pages 静态托管设计', link: 'pages-design' },
{ text: '边缘缓存策略设计', link: 'edge-cache-design' },
{ text: '边缘可观测与业务流量统计', link: 'observability-design' },
{ text: '观测数据传输模型', link: 'observability-transport-model' },
{ text: '观测上报协议与表结构', link: 'observability-data-model' },
{ text: 'Uptime Kuma 监控同步设计', link: 'kuma-design' },
{ text: '登录验证码设计', link: 'login-captcha' }
]
}
]
}
-232
View File
@@ -1,232 +0,0 @@
# 接入 Agent
你会学到:Agent 的职责、两种接入 Token 的区别、安装脚本参数、`agent.json` 配置方式,以及如何确认节点已经上线。
OpenFlare Agent 运行在代理节点侧。它不会接收远程 shell 指令,而是通过 Agent API 拉取控制面发布的配置版本,在本地写入 OpenResty 文件、执行配置校验、reload,并在失败时尝试回滚到可运行配置。
## 接入方式
| 方式 | 适用场景 |
| --- | --- |
| `discovery_token` | 首次自动注册节点,由 Server 置换为节点专属凭证 |
| `agent_token` | 已在管理端创建或分配节点,直接使用节点专属凭证接入 |
`agent_token` 与 `discovery_token` 至少填写一个。
### 凭证获取路径
- **`discovery_token`(自动注册凭证)**:登录管理端后台,导航至「系统设置」->「自动注册」,在页面中可直接生成、查看和复制全局的自动注册凭证。
- **`agent_token`(节点专属凭证)**:登录管理端后台,导航至「节点管理」->「新增节点」,填写节点基本信息保存后,在节点详情页面即可直接复制该节点专属的接入 Token。
## 一键安装
### 交互式安装 (推荐)
如果在不传递任何参数的情况下运行安装脚本,脚本将进入交互模式。您将可以通过向导选择安装方式(本地运行 / Docker 容器运行),并配置 Server 地址与认证 Token(若选择 Docker 方式且本地没有 Docker,脚本还会询问并智能安装 Docker):
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash
```
### 自动化 (非交互式) 安装
如果在执行脚本时附加了任何参数,脚本将进入自动化安装模式,不需要任何交互。
使用 `discovery_token` 进行本地安装:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
使用节点专属 `agent_token` 进行本地安装:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
使用 Docker 容器自动化安装:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN \
--docker
```
安装脚本在本地安装模式下会下载最新 Agent,默认写入 `/opt/openflare-agent`,生成 `agent.json`,自动检测并创建低权限系统账号 `openflare`(将整个安装目录赋权给该用户),并在 Linux + systemd 环境创建 `openflare-agent.service` 服务。该服务将以 `openflare` 普通用户运行,并通过 Linux Capabilities(`CAP_NET_BIND_SERVICE`)保障其监听特权端口(如 80、443)的能力。
支持参数:
| 参数 | 说明 |
| --- | --- |
| `--server-url` | Server 地址 |
| `--discovery-token` | 首次自动注册 Token |
| `--agent-token` | 节点专属 Token |
| `--install-dir` | 安装目录,默认 `/opt/openflare-agent`(仅本地安装生效) |
| `--openresty-path` | OpenResty 二进制路径,未传时自动查找 `openresty`(仅本地安装生效) |
| `--repo` | 下载 Agent 的 GitHub 仓库,默认 `Rain-kl/OpenFlare` |
| `--no-service` | 不创建 systemd 服务(仅本地安装生效) |
| `--docker` | 使用 Docker 容器方式安装 |
| `--method` | 安装方式,可选 `local` 或 `docker`(默认 `local`) |
## 配置文件
默认配置文件路径:
```text
/opt/openflare-agent/agent.json
```
本地配置示例:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"openresty_path": "openresty",
"openresty_observability_port": 18081,
"observability_replay_minutes": 60,
"heartbeat_interval": 3000,
"request_timeout": 10000
}
```
自定义 OpenResty 路径示例:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "/var/lib/openflare-agent",
"openresty_path": "/usr/local/openresty/nginx/sbin/openresty",
"main_config_path": "/var/lib/openflare-agent/etc/nginx/nginx.conf",
"route_config_path": "/var/lib/openflare-agent/etc/nginx/conf.d/openflare_routes.conf",
"access_log_path": "/var/lib/openflare-agent/var/log/openflare/access.log",
"cert_dir": "/var/lib/openflare-agent/etc/nginx/certs",
"lua_dir": "/var/lib/openflare-agent/etc/nginx/lua",
"runtime_config_dir": "/var/lib/openflare-agent/etc/openflare",
"heartbeat_interval": 3000,
"request_timeout": 10000
}
```
如果不配置 `openresty_path`,Agent 默认调用 `openresty`。完整字段见 [配置项参考](../reference/configuration.md#agent-配置字段)。
## Docker 运行
Docker 部署时直接运行内置 OpenResty 的 Agent 镜像:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-v openflare-agent-pages:/data/var/lib/openflare/pages \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
> [!NOTE]
> **Pages 持久化**
> 默认将 Pages 部署目录挂载到 Docker 命名卷 `openflare-agent-pages`(容器内路径 `/data/var/lib/openflare/pages`)。重建或升级 Agent 容器时无需重新拉取静态站点包。
> [!NOTE]
> **非 Root 安全加固运行**
> Agent 容器内部已完成安全加固,在启动后会统一以低权限非 root 用户 `openflare` 运行。
> 容器已内置了 `cap_net_bind_service` 内核能力,使得低权限进程依然能够正常监听宿主机的 `80` 和 `443` 特权端口。
> 同时,OpenResty 运行时所需的各种临时路径(包括 PID 路径、各类临时缓存目录如 `client_body_temp_path`、`proxy_temp_path` 等)都由 Agent 控制器动态渲染并自动重定向至容器内的 `/data` 目录,彻底避免在非 root 权限运行时写入默认系统路径而导致的权限拒绝错误(Permission Denied)。
> 具体物理缓存写入路径为:
> * 临时缓存目录:`/data/var/cache/nginx`
> * 代理缓存目录:`/data/var/cache/openflare_proxy`
## 启动与验证
systemd 环境:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -f
```
手动启动:
```bash
/opt/openflare-agent/openflare-agent -config /opt/openflare-agent/agent.json
```
源码运行:
```bash
export LOG_LEVEL='info'
go run ./cmd/agent -config /path/to/agent.json
```
编译后二进制运行:
```bash
go build -o openflare-agent ./cmd/agent
export LOG_LEVEL='info'
./openflare-agent -config /path/to/agent.json
```
在管理端确认:
| 位置 | 期望结果 |
| --- | --- |
| 节点列表 | 节点在线 |
| 节点详情 | 能看到心跳时间、当前版本和基础资源信息 |
| 应用记录 | 发布配置后出现应用结果 |
## 卸载
### 交互式卸载 (推荐)
如果在不传递任何参数的情况下运行卸载脚本,脚本将进入交互模式。您可以通过提示菜单选择卸载方式(本地卸载 / Docker 容器卸载):
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash
```
### 自动化 (非交互式) 卸载
使用命令行传参进行无人值守卸载。
本地卸载(默认):
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash -s -- --install-dir /opt/openflare-agent
```
Docker 容器卸载:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/uninstall-agent.sh | bash -s -- --docker
```
支持参数:
| 参数 | 说明 |
| --- | --- |
| `--install-dir` | 安装目录,默认 `/opt/openflare-agent`(仅本地卸载生效) |
| `--service-name` | systemd 服务名,默认 `openflare-agent`(仅本地卸载生效) |
| `--docker` | 使用 Docker 容器方式卸载 |
| `--method` | 卸载方式,可选 `local` 或 `docker`(默认 `local`) |
本地卸载只会移除 Agent 服务、进程和安装目录,不会删除本机 OpenResty。Docker 卸载会停止并删除 `openflare-agent` 容器,交互模式下还可以选择是否清理对应的 Docker 镜像。
## 常见问题
| 现象 | 处理步骤 |
| --- | --- |
| `agent_token 和 discovery_token 不能同时为空` | 检查 `agent.json` 至少配置了一个 Token |
| 节点一直离线 | 在 Agent 节点执行 `curl -I http://your-server:3000`,确认 Server 地址可达 |
| OpenResty 没有启动 | 查看 `journalctl -u openflare-agent`,确认 `openresty_path` 可执行,80/443 端口未被占用,且运行用户(如 `openflare`)对数据目录具有读写权限 |
| 发布后重复失败 | Agent 会阻断同一 `version + checksum` 的重复应用;需要修正配置后重新发布,或激活旧版本回滚 |
-209
View File
@@ -1,209 +0,0 @@
# 部署说明
你会学到:OpenFlare 的推荐部署方式、Server 与 Agent 的运行要求、源码启动方式、联调步骤、升级与卸载入口。
生产环境建议使用 PostgreSQL 作为 Server 数据库,并通过 `config.yaml` 或环境变量配置 `APP_SESSION_SECRET` 等参数。完整 Docker Compose 部署还需 Redis 与 ClickHouse(见仓库根目录 `docker-compose.yaml`)。Agent 部署方式推荐为 Docker 部署(即直接使用内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本或手动本地运行。
## 部署拓扑
### 标准反代流量路径
```text
Browser
|
v
OpenFlare Server :3000
|
| Agent API / heartbeat / config pull
v
OpenFlare Agent
|
v
OpenResty binary
|
v
Origin service
```
### 内网穿透流量路径
```text
Browser
|
v
OpenResty (Agent, WAF/HTTPS 终结) <-- TunnelRelay 节点
|
| proxy_pass (127.0.0.1:{vhost_port})
v
OpenFlareRelay (frps 进程) <-- TunnelRelay 节点
|
| frp 隧道协议
v
OpenFlared (frpc 客户端) <-- 内网服务器
|
v
Internal Service (192.168.x.x)
```
## 前置条件
Server:
| 项目 | 要求 |
| --- | --- |
| Go | `1.25+`,仅源码运行需要 |
| Node.js | `18+`,仅源码构建管理端需要 |
| 数据库 | 可写 SQLite 文件目录,或可访问的 PostgreSQL 实例 |
| 端口 | 默认监听 `3000` |
Agent:
| 项目 | 要求 |
| --- | --- |
| 系统 | 安装脚本支持 Linux 和 macOS;systemd 服务仅在 Linux + systemd 环境创建 |
| 架构 | `amd64` 或 `arm64` |
| OpenResty | 本地部署需要可执行 `openresty`,或通过 `--openresty-path` 指定路径 |
| Docker | 仅 Docker 部署 Agent 镜像时需要 |
| 网络 | Agent 节点必须能访问 Server 地址 |
| GeoIP | WAF 地域规则使用 Agent 本地 MaxMind mmdb;Agent 内置初始库并会定期更新 |
### 硬件配置推荐
| 组件 | 最低硬件配额 | 推荐硬件配额 | 说明 |
| --- | --- | --- | --- |
| **Server 控制面** | 1 核 CPU / 1 GB 内存 / 10 GB 磁盘 | 2 核 CPU / 4 GB 内存 / 50 GB+ 磁盘 | 磁盘用量需根据访问日志留存时长与并发流量合理扩容 |
| **Agent 数据面** | 1 核 CPU / 512 MB 内存 / 2 GB 磁盘 | 2 核 CPU / 2 GB 内存 / 10 GB+ 磁盘 | 根据 OpenResty 的并发代理连接量与 WAF 拦截处理扩容 |
| **Relay 中继节点**| 1 核 CPU / 1 GB 内存 / 5 GB 磁盘 | 2 核 CPU / 2 GB 内存 / 20 GB 磁盘 | frps 传输中继吞吐量主要受带宽与 CPU 吞吐能力限制 |
| **OpenFlared 客户端**| 1 核 CPU / 256 MB 内存 / 1 GB 磁盘 | 1 核 CPU / 512 MB 内存 / 5 GB 磁盘 | 独立运行于内网,自身资源占用极小,保障网络吞吐即可 |
## Docker Compose 部署 Server
仓库根目录已提供完整 `docker-compose.yaml`(含 PostgreSQL、Redis、ClickHouse、Jaeger)。
```bash
curl -o .env.example https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/.env.example
cp .env.example .env
# 编辑 .env,至少修改 APP_SESSION_SECRET 与数据库密码
docker compose up -d
docker compose ps
docker compose logs -f openflare
```
首次访问 `http://localhost:3000`,默认账号为 `admin` / `12345678`。登录后请立即修改默认密码。
## 源码启动 Server
先构建管理端前端:
```bash
cd frontend
corepack enable
pnpm install
pnpm build:embed
```
再启动 Server(仓库根目录):
```bash
cp config.example.yaml config.yaml
export APP_SESSION_SECRET='replace-with-a-long-random-string'
# 可选:使用 PostgreSQL
# export DB_HOST=127.0.0.1 DB_USERNAME=postgres DB_PASSWORD=postgres DB_NAME=openflare
go run main.go all
```
默认监听 `:3000`(由 `config.yaml` 的 `app.addr` 或 `APP_ADDR` 控制)。
## Docker 运行 Agent(推荐)
Docker 部署是 Agent 推荐的部署方式。Docker 部署时直接运行 Agent 镜像,该镜像基于 OpenResty 镜像制作,内置 Agent 控制器与 OpenResty 二进制。未显式配置 `node_ip` 时,Agent 会优先通过第三方 API 获取真实出口 IP,避免把 Docker 网桥地址登记为节点 IP。
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-v openflare-agent-pages:/data/var/lib/openflare/pages \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
命名卷 `openflare-agent-pages` 持久化 Pages 部署目录,重建容器时无需重新拉取静态站点包。
## Agent 接入(脚本安装)
除了 Docker 部署外,也支持通过安装脚本将 Agent 部署在本地宿主机上。安装脚本会自动在本地 Linux 系统中注册低权限的 `openflare` 服务账号,并将 systemd 服务配置为以该用户身份运行,利用 Linux Capabilities 安全地监听 80/443 特权端口。
使用 `discovery_token` 自动注册:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
使用节点专属 `agent_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
安装脚本支持参数:
| 参数 | 说明 |
| --- | --- |
| `--server-url` | Server 地址,必填 |
| `--discovery-token` | 首次自动注册 Token,与 `--agent-token` 二选一 |
| `--agent-token` | 节点专属 Token,与 `--discovery-token` 二选一 |
| `--install-dir` | 安装目录,默认 `/opt/openflare-agent` |
| `--openresty-path` | OpenResty 二进制路径,未传时自动查找 `openresty` |
| `--repo` | 下载 Agent 的 GitHub 仓库,默认 `Rain-kl/OpenFlare` |
| `--no-service` | 不创建 systemd 服务 |
确认状态:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -f
```
## 手动运行 Agent
源码运行:
```bash
export LOG_LEVEL='info'
go run ./cmd/agent -config /path/to/agent.json
```
编译后二进制运行:
```bash
go build -o openflare-agent ./cmd/agent
export LOG_LEVEL='info'
./openflare-agent -config /path/to/agent.json
```
最小 `agent.json` 示例:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token",
"data_dir": "./data",
"openresty_path": "openresty",
"heartbeat_interval": 3000,
"request_timeout": 10000
}
```
未配置 `openresty_path` 时,Agent 默认调用 `openresty`。
默认情况下,Agent 在 HTTP 心跳成功后会尝试升级为 WebSocket。升级成功时,Server 发布或激活配置会立即通知 Agent;如果 WebSocket 无法建立或意外断开,Agent 会自动退回 HTTP 心跳同步。
WAF 地域规则依赖 Agent 本地 `GeoLite2-Country.mmdb`。Agent 启动时会在 `data_dir/etc/openflare/GeoLite2-Country.mmdb` 初始化内置数据库,并按配置周期尝试更新;更新失败只记录警告,不影响配置同步与 OpenResty reload。
-24
View File
@@ -1,24 +0,0 @@
# 部署与升级
本分区提供 OpenFlare Server、Agent、Relay 中继以及 OpenFlared 内网穿透客户端的详细部署指南、配置说明和升级维护步骤。
## 内容导航
### 快速开始
* **[快速开始](../guide/quick-start.md)**:5 分钟内使用 Docker Compose 启动 Server 和首个 Agent(推荐新用户)
### Server 部署
* **[启动 Server](./server.md)**:从源码构建前端、启动 Server、选择 SQLite 或 PostgreSQL
### Agent 部署
* **[部署 Agent](./agent.md)**:Agent 接入方式、Docker 部署、脚本安装、配置文件及故障排查
### Tunnel 内网穿透部署
* **[部署 Relay](./relay.md)**:TunnelRelay 节点的配置说明、Docker 部署与宿主机运行指南
* **[部署 OpenFlared](./openflared.md)**:内网穿透客户端配置说明、Docker 运行与自同步机制
### 升级与维护
* **[升级与维护](./upgrade.md)**:Server 与 Agent 升级步骤、数据清理策略、验证命令
### 参考资料
* **[部署说明](./deployment.md)**:部署拓扑、前置条件、Docker Compose 配置示例、多种部署方式综览
-118
View File
@@ -1,118 +0,0 @@
# 部署 OpenFlared 客户端
你会学到:OpenFlared 客户端的职责、配置参数与环境变量、基于 Docker 运行客户端的方法,以及如何在内网服务器上通过二进制方式独立部署。
**OpenFlared** 是部署在用户内网(局域网、私有云等无法被公网直接访问的环境)的隧道客户端。它的核心职责是通过 `X-Tunnel-Token` 与控制面(OpenFlare Server)建立通信,并在本地自动拉起并管理一个或多个 **frpc (快速反向代理客户端)** 进程,从而将内网的 HTTP 流量安全、稳定地穿透至外网的中继节点。
---
## 前置条件
1. **获取 Tunnel Token**:在 OpenFlare 管理端的「内网穿透」或「隧道管理」页面中,创建一个新的隧道实例,系统会自动生成唯一的 `tunnel_id` 与 `tunnel_token`(形如 `tun-<32hex>`)。
2. **网络出方向权限**:内网服务器无需任何公网入方向 IP 或端口映射,但必须能够通过网络访问公网上的 **OpenFlare Server 地址** 以及对应的 **TunnelRelay 节点中继端口 (默认 7000)**。
3. **软件依赖**(仅限宿主机直接部署):
- 本地需有可执行的 `frpc` 二进制文件(建议版本为 `v0.61.0+` 或最新稳定版 `v0.69.0`),或通过参数显式指定路径。
---
## 配置文件与环境变量
`openflared` 启动时默认会读取当前目录下的 `flared.json`。同时也完全支持通过环境变量进行覆盖。
### 配置字段详情
| JSON 字段 | 环境变量 | 说明 | 默认值 |
| --- | --- | --- | --- |
| `server_url` | `OPENFLARE_SERVER_URL` | OpenFlare Server 接口服务地址 | **无(必填)** |
| `tunnel_token` | `OPENFLARE_TUNNEL_TOKEN` | 隧道客户端专属认证 Token | **无(必填)** |
| `frpc_path` | `OPENFLARE_FRPC_PATH` | frpc 可执行二进制文件路径 | `"frpc"` |
| `data_dir` | `OPENFLARE_DATA_DIR` | 本地数据与生成的 `frpc_{relayNodeID}.toml` 存放目录 | `"./data"` |
| `state_path` | - | 本地状态记录文件路径(保存最后应用的配置版本)| `"{data_dir}/flared-state.json"` |
| `heartbeat_interval`| - | 状态心跳上报周期(支持毫秒数或 Go Duration 字符串) | `10000` (10s) |
| `sync_interval` | - | 隧道配置拉取同步周期(支持毫秒数或 Go Duration 字符串) | `30000` (30s) |
| `request_timeout` | - | 接口网络请求超时时长 | `10000` (10s) |
---
## Docker 运行(推荐)
Docker 部署是内网运行最简单也最安全的方式。官方的 `openflared` 镜像已经内置了客户端控制器以及 `frpc v0.69.0` 二进制运行时,无需额外搭建环境。
```bash
docker pull ghcr.io/rain-kl/openflared:latest
docker rm -f openflared 2>/dev/null || true
docker run -d --name openflared --restart unless-stopped \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_TUNNEL_TOKEN=YOUR_TUNNEL_TOKEN \
-v openflared-data:/app/data \
ghcr.io/rain-kl/openflared:latest
```
---
## 宿主机手动运行
如果您需要直接在内网的 Linux/macOS/Windows 宿主机上独立运行:
### 1. 编译二进制
```bash
go build -o bin/flared ./cmd/flared
```
### 2. 准备 `flared.json`
在程序同级目录下创建 `flared.json` 配置文件:
```json
{
"server_url": "http://your-server-ip:3000",
"tunnel_token": "your-tunnel-auth-token",
"frpc_path": "/usr/local/bin/frpc",
"data_dir": "./data",
"heartbeat_interval": "10s",
"sync_interval": "30s"
}
```
### 3. 运行服务
```bash
export LOG_LEVEL='info'
./flared -config ./flared.json
```
---
## 启动与验证
### 1. 自动同步逻辑
启动成功后,OpenFlared 将执行以下工作流:
- **心跳与配置获取**:周期性向 Server 的 `/api/v1/tunnel/heartbeat` 和 `/api/v1/tunnel/config/active` 接口发起同步,验证 Token 并检测配置版本。
- **文件渲染**:当检测到配置版本(或校验和 Checksum)变化时,会自动拉取该隧道的完整路由规则。如果绑定了多个中继 Relay,将为每个 Relay 分别在 `data_dir` 下渲染出 `frpc_{relayNodeID}.toml`。
- **热重载或重启**:拉起对应的 `frpc` 子进程,或在配置文件发生改变时执行 `frpc reload` / 重启动作,以确保流量映射保持最新。
- **异常自恢复**:如果本地 `frpc` 隧道进程异常退出,主控程序会在 5 秒的退避惩罚后自动尝试重新启动。
### 2. 查看日志与连接状态
```bash
# Docker 容器日志
docker logs -f openflared
```
若进程运行无误,您会在日志中看到类似如下输出:
```text
flared config loaded ...
detected frpc version v0.69.0
flared process started
applying new tunnel config {"version": "...", "checksum": "..."}
frpc process missing, starting {"relay_id": "..."}
```
### 3. 管理端确认
打开管理后台的 **「内网穿透」** 页面:
- 查看对应隧道的在线状态,此时应当绿灯显示 **「在线」**。
- 您可以清晰地看到该隧道目前连接了哪些中继节点,以及各内网服务的穿透路由详情。
-131
View File
@@ -1,131 +0,0 @@
# 部署 Relay (Tunnel 中继)
你会学到:TunnelRelay 节点的职责、`openflare-relay` 的配置项与环境变量、使用 Docker 运行 Relay 的方法,以及如何通过源码手动构建并部署 Relay。
在 OpenFlare 的内网穿透体系中,**TunnelRelay 节点** 扮演着关键的角色。它与普通的边缘节点(Edge Node)不同,除了运行传统的 Agent(托管 OpenResty 进行 HTTPS/WAF 处理)外,还同机运行了 **Relay (frps 隧道管理器)** 服务,负责监听内网客户端(OpenFlared)的隧道连接并进行流量中继。
---
## 前置条件
在部署 TunnelRelay 节点之前,请确保:
1. **已注册为 TunnelRelay 类型节点**:在 OpenFlare 管理端「节点管理」中,添加一个类型为 `tunnel_relay` 的节点,并获取其专属的 `agent_token` 或使用全局 `discovery_token`。
2. **网络端口**:
- 必须确保 `bindPort`(frpc 连接端口,默认 `7000`)可被公网/内网客户端访问。
- 必须确保 `vhostHTTPPort`(HTTP Vhost 端口,默认 `8080`)处于空闲状态,Agent 将在此端口上与 frps 进行流量传递。
3. **软件依赖**(仅限宿主机直接部署):
- 本地需有可执行的 `frps` 二进制文件(建议版本为 `v0.61.0+` 或最新稳定版 `v0.69.0`),或通过参数显式指定路径。
---
## 配置文件与环境变量
`openflare-relay` 启动时默认会读取当前目录下的 `relay.json`。同时也完全支持通过环境变量进行覆盖。
### 配置字段详情
| JSON 字段 | 环境变量 | 说明 | 默认值 |
| --- | --- | --- | --- |
| `server_url` | `OPENFLARE_SERVER_URL` | OpenFlare Server 接口服务地址 | **无(必填)** |
| `agent_token` | `OPENFLARE_AGENT_TOKEN` | 节点专属 Token | 与下者二选一 |
| `discovery_token` | `OPENFLARE_DISCOVERY_TOKEN` | 自动注册 Token | 与上者二选一 |
| `node_name` | `OPENFLARE_NODE_NAME` | 节点标识名称 | 默认获取本机主机名 |
| `node_ip` | `OPENFLARE_NODE_IP` | 节点出口/监听 IP | 自动检测真实出口 IP |
| `frps_path` | `OPENFLARE_FRPS_PATH` | frps 可执行二进制文件路径 | `"frps"` |
| `data_dir` | `OPENFLARE_DATA_DIR` | 本地数据与生成的 `frps.toml` 存放目录 | `"./data"` |
| `state_path` | - | 本地状态 JSON 记录文件路径 | `"{data_dir}/relay-state.json"` |
| `heartbeat_interval`| - | 心跳周期(支持毫秒数或 Go Duration 字符串) | `10000` (10s) |
| `request_timeout` | - | 接口请求超时时长 | `10000` (10s) |
---
## Docker 运行(推荐)
Docker 运行是 TunnelRelay 节点最便捷的部署方案。官方镜像内置了 `openflare-relay` 控制器与 `frps v0.69.0` 运行时,开箱即用。
```bash
docker pull ghcr.io/rain-kl/openflare-relay:latest
docker rm -f openflare-relay 2>/dev/null || true
docker run -d --name openflare-relay --restart unless-stopped \
-p 7000:7000 \
-p 17500:17500 \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
-v openflare-relay-data:/app/data \
ghcr.io/rain-kl/openflare-relay:latest
```
> [!TIP]
> 这里的 `-p 7000:7000` 映射的是 `frpc` 客户端连接中继的端口。如果管理端配置了自定义的 `relay_bind_port`,请对应修改宿主机端口映射。
> [!NOTE]
> **开启内嵌 frps Web UI**:
> 如果在 Server 控制端开启了中继流量监控面板(即数据库/系统设置中的 `relay_frps_web_ui_enabled` 设为 `true`),你需要将 Web 端口(默认是 `17500`,由系统设置中的 `relay_frps_web_ui_port` 控制)也通过 `-p 17500:17500` 映射到宿主机。
> 登录 Web UI 时的用户名固定为 `admin`,密码为当前中继节点的 `agent_token`。
---
## 宿主机手动运行
如果您倾向于在物理机或虚拟机上直接运行:
### 1. 编译二进制
```bash
go build -o bin/openflare-relay ./cmd/relay
```
### 2. 准备 `relay.json`
在程序同级目录下创建 `relay.json` 配置文件:
```json
{
"server_url": "http://127.0.0.1:3000",
"agent_token": "your-relay-node-agent-token",
"frps_path": "/usr/local/bin/frps",
"data_dir": "./data",
"heartbeat_interval": "10s",
"request_timeout": "10s"
}
```
### 3. 运行服务
```bash
export LOG_LEVEL='info'
./openflare-relay -config ./relay.json
```
---
## 启动与验证
### 1. 查看进程日志
```bash
# Docker 容器日志
docker logs -f openflare-relay
```
如果是在 Linux 上通过 Systemd 托管的,可执行:
```bash
journalctl -u openflare-relay -f
```
### 2. 验证运行状态
启动成功后,Relay 将进行以下工作:
- 向控制面发送 HTTP 心跳以注册/上线。
- 从控制面获取最新的 frps 基础配置(包括 `bindPort`、`vhostHTTPPort` 与自动生成的隧道认证凭证 `auth_token`)。
- 在本地自动渲染出 `data/frps.toml` 配置文件。
- 自动拉起子进程 `frps -c data/frps.toml`。
- 如果进程意外崩溃,Relay 将在 2 秒后自动拉起它。
### 3. 管理端确认
登录管理后台,导航至 **「节点管理」**,确认:
- 该 TunnelRelay 节点状态标记为 **「在线」**。
- 节点类型正确标记为 **中继节点** 且 frps 运行状态为 **正常 (Healthy)**。
-440
View File
@@ -1,440 +0,0 @@
# 启动 Server
你会学到:如何使用 Docker(分为快速启动、生产推荐、进阶版)部署,以及如何从源码本地部署 OpenFlare Server。
OpenFlare Server 是 Gin + GORM 单体控制面,负责管理端 UI、管理 API、Agent API、配置渲染、版本发布、数据存储与聚合查询。
> [!IMPORTANT]
> **关于外部依赖**:
> OpenFlare 系统内建了对后台异步任务(Asynq 框架)及海量节点日志分析与度量指标(观测面板)的支持。因此,**无论采用何种部署模式,系统都必须依赖 Redis(或 Valkey)与 ClickHouse 的运行**。各个部署方案的主要差异在于主关系型数据库的选择(SQLite vs PostgreSQL)以及是否启用链路追踪服务(Jaeger)。
> [!TIP]
> **ClickHouse 服务端性能配置(推荐挂载)**
> 控制面常见为小规格主机(如 3c6g)。仓库提供的 `performance.xml` 会收紧后台 merge/mutation 线程池,避免默认配置在小机器上静置 CPU 偏高或 ClickHouse 25.x 启动校验失败。
> 将本地 `./config/clickhouse/performance.xml` 以单文件方式挂载到容器 `/etc/clickhouse-server/config.d/performance.xml`,以保留官方镜像内置的 Docker 网络监听配置。
部署前将配置拉到本地:
```bash
mkdir -p ./config/clickhouse
curl -fsSL -o ./config/clickhouse/performance.xml \
https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/config/clickhouse/performance.xml
```
在 ClickHouse 服务的 `volumes` 中增加(与数据卷并列):
```yaml
volumes:
- ./data/clickhouse_data:/var/lib/clickhouse # 或 named volume
- ./config/clickhouse/performance.xml:/etc/clickhouse-server/config.d/performance.xml:ro
```
修改 `performance.xml` 后需 `docker compose restart clickhouse` 才生效。
---
## 方式一:Docker 部署 (推荐)
使用 Docker 部署可以免去本地配置 Go 与 Node.js 前端构建环境的麻烦。根据你的服务器硬件配置及业务需求,你可以选择以下三种方案之一:
### 1. 快速启动 (SQLite + Redis + ClickHouse)
> **适用场景**:测试体验、轻量化单机部署。
>
> **特点**:主关系型数据库使用内建的 SQLite 文件
创建 `docker-compose.yaml` 文件:
```yaml
version: '3.8'
services:
openflare:
image: ghcr.io/rain-kl/openflare:latest
container_name: openflare-server
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./openflare-data:/data
- ./uploads:/app/uploads
environment:
TZ: Asia/Shanghai
APP_SESSION_SECRET: 'replace-with-a-long-random-string' # 生产环境请替换为长随机字符串
DB_ENABLED: "false" # 禁用 PostgreSQL,自动启用内置 SQLite 后备
SQLITE_PATH: "/data/openflare.db"
REDIS_ENABLED: "true"
REDIS_ADDR: "redis:6379"
CLICKHOUSE_ENABLED: "true"
CLICKHOUSE_HOST: "clickhouse:9000"
depends_on:
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- ./data/valkey:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
clickhouse:
image: clickhouse/clickhouse-server:25.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: openflare
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: 123456
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
TZ: Asia/Shanghai
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- ./data/clickhouse_data:/var/lib/clickhouse
- ./config/clickhouse/performance.xml:/etc/clickhouse-server/config.d/performance.xml:ro
healthcheck:
test: ["CMD", "clickhouse-client", "--user", "default", "--password", "123456", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
```
运行启动命令:
```bash
mkdir -p ./config/clickhouse
curl -fsSL -o ./config/clickhouse/performance.xml \
https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/config/clickhouse/performance.xml
docker compose up -d
```
---
### 2. 生产推荐 (PostgreSQL + Redis + ClickHouse)
> **适用场景**:生产环境、多节点集群管理、高并发高可用要求。
>
> **特点**:完全分层架构。启用专用的 PostgreSQL 服务作为主关系数据库,Redis 负责高并发分布式锁、会话缓存与异步队列,ClickHouse 承载海量日志异步 Flush 与观测指标。
创建 `docker-compose.yaml` 文件:
```yaml
services:
openflare:
image: ghcr.io/rain-kl/openflare:latest
restart: unless-stopped
env_file: .env
environment:
TZ: ${TZ:-Asia/Shanghai}
ports:
- "3000:3000"
volumes:
- openflare_uploads:/app/uploads
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-openflare}
POSTGRES_USER: ${DB_USERNAME:-openflare}
POSTGRES_PASSWORD: ${DB_PASSWORD:-replace-with-strong-password}
volumes:
- openflare_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-openflare} -d ${DB_NAME:-openflare}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- openflare_redis_data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
clickhouse:
image: clickhouse/clickhouse-server:25.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: ${CLICKHOUSE_NAME:-openflare}
CLICKHOUSE_USER: ${CLICKHOUSE_USERNAME:-default}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
TZ: ${TZ:-Asia/Shanghai}
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- openflare_clickhouse_data:/var/lib/clickhouse
- ./config/clickhouse/performance.xml:/etc/clickhouse-server/config.d/performance.xml:ro
healthcheck:
test: ["CMD", "clickhouse-client", "--user", "${CLICKHOUSE_USERNAME:-default}", "--password", "${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
openflare_uploads:
openflare_postgres_data:
openflare_redis_data:
openflare_clickhouse_data:
```
创建对应的 `.env` 文件来配置系统环境变量(可复制并修改根目录下的 `.env.example`):
```bash
mkdir -p ./config/clickhouse
curl -fsSL -o ./config/clickhouse/performance.xml \
https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/config/clickhouse/performance.xml
curl -o .env.example https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/.env.example
cp .env.example .env
# 编辑 .env 文件,填入对应的数据库、Redis、ClickHouse 连接地址、密码与 APP_SESSION_SECRET
docker compose up -d
```
---
### 3. 进阶版 (含 Jaeger 链路追踪的完整编排)
> **适用场景**:开发者调试、系统深度性能诊断、高级可观测性追溯。
>
> **特点**:在“生产推荐”全家桶的基础上,联动拉起 Jaeger 作为 OpenTelemetry (OTel) 链路追踪的后端,收集 Server 运行时各个 API 请求的 Span Trace 信息。
创建 `docker-compose.yaml` 文件:
```yaml
version: '3.8'
services:
openflare:
image: ghcr.io/rain-kl/openflare:latest
restart: unless-stopped
env_file: .env
environment:
TZ: ${TZ:-Asia/Shanghai}
OTEL_EXPORTER_OTLP_ENDPOINT: "http://jaeger:4317"
OTEL_EXPORTER_OTLP_INSECURE: "true"
OTEL_SAMPLING_RATE: "1.0" # 本地调试建议设为 1.0 以采样所有 Trace
ports:
- "3000:3000"
volumes:
- openflare_uploads:/app/uploads
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
jaeger:
condition: service_started
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-openflare}
POSTGRES_USER: ${DB_USERNAME:-openflare}
POSTGRES_PASSWORD: ${DB_PASSWORD:-replace-with-strong-password}
volumes:
- openflare_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-openflare} -d ${DB_NAME:-openflare}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- openflare_redis_data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
jaeger:
image: jaegertracing/jaeger:2.19.0
restart: unless-stopped
environment:
TZ: ${TZ:-Asia/Shanghai}
ports:
- "16686:16686" # Web UI 端口
- "4317:4317" # OTLP gRPC 接收端口
- "4318:4318" # OTLP HTTP 接收端口
clickhouse:
image: clickhouse/clickhouse-server:25.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: ${CLICKHOUSE_NAME:-openflare}
CLICKHOUSE_USER: ${CLICKHOUSE_USERNAME:-default}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
TZ: ${TZ:-Asia/Shanghai}
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- openflare_clickhouse_data:/var/lib/clickhouse
- ./config/clickhouse/performance.xml:/etc/clickhouse-server/config.d/performance.xml:ro
healthcheck:
test: ["CMD", "clickhouse-client", "--user", "${CLICKHOUSE_USERNAME:-default}", "--password", "${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
openflare_uploads:
openflare_postgres_data:
openflare_redis_data:
openflare_clickhouse_data:
```
启动并验证:
```bash
mkdir -p ./config/clickhouse
curl -fsSL -o ./config/clickhouse/performance.xml \
https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/config/clickhouse/performance.xml
curl -o .env.example https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/.env.example
cp .env.example .env
# 编辑 .env 文件并确保设置好 APP_SESSION_SECRET 密码
docker compose up -d
```
启动后可以通过访问 `http://localhost:16686` 打开 Jaeger 监控端查看系统 Span 链路。
---
## 方式二:本地部署 (源码/二进制启动)
如果你不希望使用 Docker,也可以直接在本地或虚拟机上从源码构建和运行 Server。由于后台异步任务和可观测指标分析为系统核心防线,**本地部署时依然需要连接外部 Redis 与 ClickHouse 实例**。
### 前置条件
| 项目 | 要求 |
| --- | --- |
| Go | `1.25+` |
| Node.js | `18+` |
| pnpm | 推荐通过 `corepack enable` 使用项目声明的 pnpm |
| 外部服务 | 必须在本地或远端运行 Redis (Valkey) 和 ClickHouse 实例;ClickHouse 建议挂载仓库提供的 `performance.xml`(见上文「ClickHouse 服务端性能配置」) |
### 1. 构建管理端前端
Go Server 运行时需要嵌入前端静态资源。编译 Go 二进制前需要先构建前端静态产物并输出到 Go 服务目录:
```bash
cd frontend
corepack enable
pnpm install
pnpm build:embed
cd ..
```
> **常用前端代码检查命令**:
> * `pnpm lint`
> * `pnpm typecheck`
### 2. 使用 SQLite 启动
关系数据库存储在本地 SQLite 文件,但依然需要提供 Redis 和 ClickHouse 连接配置:
```bash
cp config.example.yaml config.yaml
# 编辑 config.yaml:
# 1. 设置 app.session_secret 为一个随机的长字符串
# 2. 将 database.enabled 设为 false 以启用内置 SQLite
# 3. 将 redis.addrs 与 clickhouse.hosts 修改为你的本地/局域网服务连接信息
# 启动 Server(默认融合模式)
go run main.go all
```
### 3. 使用 PostgreSQL 启动
```bash
cp config.example.yaml config.yaml
# 编辑 config.yaml:
# 1. 设置 app.session_secret
# 2. 将 database.enabled 设为 true,并完整设置 database.*、redis.*、clickhouse.* 字段连接参数
# 启动 Server(默认融合模式)
go run main.go all
```
---
## 首次登录
Server 默认监听 `3000` 端口,启动成功后可以使用浏览器访问:`http://localhost:3000`。
默认管理员账户信息如下:
| 用户名 | 密码 |
| --- | --- |
| `admin` | `12345678` |
> [!WARNING]
> 为了你的系统安全,首次登录后请立即前往个人设置页面修改默认密码。
---
## 常用运维指南
### 1. 命令行子服务分进程启动
在大型生产部署中,你可以选择将 Server 按职责拆分为多个进程运行:
```bash
go run main.go api # 仅启动管理端与节点通信的 API 服务
go run main.go worker # 仅启动后台任务的 Worker 服务
go run main.go scheduler # 仅启动定时任务的 Scheduler 服务
go run main.go all # 融合模式(在一进程内运行上述所有服务,默认)
```
### 2. 状态验证
```bash
# 验证编译是否通过
go build ./...
# 运行内部单元测试
go test ./internal/apps/openflare/... -count=1
# 检查服务健康状态
curl http://127.0.0.1:3000/api/v1/d/status
```
-20
View File
@@ -1,20 +0,0 @@
# 升级与维护
你会学到:如何升级 Server 与 Agent、如何清理观测数据,以及维护前后应该执行哪些验证命令。
升级前建议先确认当前激活版本、最近一次 Agent 应用结果和数据库备份策略。生产环境不要在发布配置、Agent 大规模重连或数据库迁移进行中同时升级。
## Server 升级
拉取最新镜像升级
```bash
docker compose pull
docker compose up
```
如果是源码部署,重新启动 Server 后确认日志中没有数据库迁移或启动错误。
## Agent 升级
Agent 是完全无状态的,升级时直接拉取最新镜像重建容器即可。具体部署命令与安装方式请参考 **[接入 Agent](./agent.md)**。
-177
View File
@@ -1,177 +0,0 @@
# Agent 设计文档
你会学到:Agent 的设计原则、核心功能模块、与 Server 的交互链路,以及如何通过不可变版本模型与三阶段容灾机制来保证配置应用的安全性和可靠性。
---
## 需求分析
在分布式反向代理与边缘安全网关场景中,Agent 扮演着打通控制面(Server)与数据面(OpenResty)的核心角色。由于 Agent 运行在用户实际的节点服务器上,其设计必须遵循以下核心安全与高可用需求:
1. **主动拉取(Pull 模型)而非被动接收**:Server 不直接持有节点的 SSH 秘钥,也不主动发起向节点的入向连接。所有控制指令与配置更新均由 Agent 主动通过心跳(Heartbeat)或长连接(WebSocket)向上拉取。这消除了节点侧的入向防火墙安全隐患,防止了控制通道被劫持。
2. **极低侵入性**:Agent 作为一个独立的 Go 二进制进程运行,只与本地 OpenResty 进程进行基于文件的配置重写与信号通知交互,不干涉节点上的其他系统服务。
3. **极强容灾与自愈能力**:由于网络抖动、磁盘写满或异常配置等因素极易导致配置同步失败,Agent 必须具备零依赖的本地回滚自愈能力,严防因单次配置失误导致整机服务彻底瘫痪。
4. **纯粹的数据与状态落地**:Agent 仅负责承载 Server 渲染好的文件与控制意图落地,不包含复杂的业务逻辑校验、多端租户鉴权等控制面职责,确保了节点侧的高效与轻量。
---
## 核心功能
Agent 主要由以下核心子模块组成,共同配合完成其完整的生命周期管理:
| 模块名称 | 对应目录 | 功能职责 |
| :--- | :--- | :--- |
| **配置同步** | `sync/` | 负责拉取完整配置包,写入文件,触发重载,记录并回报同步状态。 |
| **心跳管理** | `heartbeat/` | 定期向 Server 上报节点健康状态、资源指标,并获取最新激活版本摘要。 |
| **WebSocket** | `wsclient/` | 保持与 Server 的长连接,提供秒级实时的配置推送与控制面指令响应。 |
| **OpenResty 管控** | `nginx/` | 执行 Nginx 配置校验 (`openresty -t`)、重写、平滑重载 (`reload`) 及进程自启动。 |
| **本地状态库** | `state/` | 持久化记录本地应用版本、错误日志及未成功上报的可观测性指标缓冲。 |
| **自更新服务** | `updater/` | 监听 Server 自更新指令,安全拉取新版本二进制并完成原地热升级。 |
| **可观测性** | `observability/` | 采集宿主机资源读数、OpenResty 健康/连接,并 tail 访问日志明细上报;**不做** UV/TopN/吞吐等业务预聚合。详见 [边缘可观测与业务流量统计](./observability-design.md)。 |
| **GeoIP 维护** | `geoipdata/` `geoipupdate/` | 维护并定期更新本地 GeoIP 数据库,为 WAF 地域过滤提供支撑。 |
---
## 与 Server 的交互链路
Agent 在生命周期中主要通过 **基于 Token 的自动注册** 和 **心跳/WebSocket 双通道** 与控制面通信。
### 1. 自动注册流程
若 Agent 启动时本地 `agent.json` 的 `access_token` 为空,但配置了 `discovery_token`,将触发自动注册流程:
1. Agent 向控制面 `/api/v1/agent/nodes/register` 发送注册请求,携带本地硬件摘要、IP 及主机名。
2. Server 校验 `discovery_token` 有效后,在数据库生成唯一的 `NodeID` 与专属 `AccessToken`(即 `agent_token`)并返回。
3. Agent 将获取的专用 Token 写入本地配置文件,擦除一次性 `discovery_token`,后续所有的通信均基于专属 `AccessToken` 进行鉴权认证。
### 2. 双通道心跳与同步机制
* **HTTP 轮询通道(兜底与探测)**:Agent 默认按设定的 `heartbeat_interval` 间隔发送 POST 心跳包。上报指标的同时获取当前激活版本的摘要信息(Version & Checksum)。
* **WebSocket 通道(实时通信)**:在 HTTP 心跳成功后,Agent 自动尝试将连接升级为 WebSocket (`/api/v1/agent/ws`)。
* WS 连接建立后,心跳与指标上报全面转移到 WS 管道,降低网络开销。
* Server 发布或激活新版本时,通过 WS 广播通知 Agent。Agent 收到变更事件后,**立即触发同步流程**,实现秒级配置生效。
* 若 WS 链路因网络问题断开,Agent 自动降级为 HTTP 轮询,并采用指数退避机制尝试重建 WS。
### 3. 交互时序图
```mermaid
sequenceDiagram
autonumber
participant Agent as OpenFlare Agent
participant OR as 本地 OpenResty
participant Server as OpenFlare Server
Note over Agent: 首次启动 (无 AccessToken)
Agent->>Server: 1. 自动注册请求 (携带 discovery_token)
Server-->>Agent: 2. 颁发 NodeID 与专属 AccessToken (agent_token)
Note over Agent: 存储 Token 至本地配置文件
rect rgb(240, 248, 255)
Note over Agent, Server: HTTP 兜底与 WebSocket 升级
Agent->>Server: 3. 发送 HTTP Heartbeat (上报系统状态与健康度)
Server-->>Agent: 4. 返回 ActiveConfig 摘要及 AgentSettings
Agent->>Server: 5. 发起 WebSocket 升级请求 (/api/v1/agent/ws)
Server-->>Agent: 6. 升级成功 (建立双向持久实时通道)
end
rect rgb(245, 245, 245)
Note over Agent, Server: 实时配置发布应用链路
Note over Server: 管理员在 UI 点击发布配置
Server->>Agent: 7. 通过 WS 广播新配置摘要 (WSMessageTypeActiveConfig)
Agent->>Server: 8. 请求拉取完整配置详情 (携带目标 Version/Checksum)
Server-->>Agent: 9. 返回完整配置快照 (Nginx配置、证书、WAF规则等)
Note over Agent: 备份旧文件,写入新配置至本地临时路径
Agent->>OR: 10. 执行配置语法校验 (openresty -t)
OR-->>Agent: 11. 返回语法校验结果 (OK)
Agent->>OR: 12. 平滑重载信号 (openresty -s reload)
Agent->>Server: 13. 上报应用成功状态 (Apply Log & ActiveVersion)
end
```
---
## OpenResty 的管控
Agent 对数据面 OpenResty 的管控实现了端到端的闭环,包含配置落地、语法验证、平滑重载和异常状态捕获:
### 1. 配置文件的落地组织
同步成功后,Agent 会将配置按照特定的物理结构写入到本地 `/etc/nginx/openflare-lua/` 目录下(或配置指定的 `LuaDir`):
* `nginx.conf`:主配置文件(替换相关占位符,配置性能参数、Shared Dictionaries 及全局 Server)。
* `routes.conf`:路由配置文件(由 Agent 生成,包含所有代理网站的 Server 块、证书路径、缓存及速率限制指令)。
* `certs/`:证书存放目录(文件命名为 `{cert_id}.crt` 和 `{cert_id}.key`)。
* `waf/` 与 `pow/`:WAF 及防 CC 挑战所需的专用 Lua 运行时脚本。
* `waf_config.json` 与 `waf_ip_groups.json`:WAF 过滤引擎所需的结构化规则配置文件。
* `pages_dir`:Pages 静态站点部署目录,默认位于 `data_dir/var/lib/openflare/pages`。当激活配置引用 Pages **项目**时,Agent 按 `project_id` 请求控制面「最新激活包」(hash + package),以流式方式写入临时文件并执行实际响应上限与 SHA-256 校验,再安全解压到 `projects/{project_id}/releases/{hash}`。解压后会复核文件数与总字节,绝对防御上限为 2 GiB 包、1,000 个文件、单文件及总量 8 GiB;随后原子切换 `current` 并**立即删除同项目其它历史 release**(仅保留最新)。项目内切换激活无需重发主配置;多项目对账时单项目失败不阻塞其它项目。
### 2. 精细化的重载动作
1. **备份当前配置**:在写入新文件之前,Agent 会将现有的配置文件复制到 `.backup` 临时目录下,保留完整的现场快照。
2. **写入并替换占位符**:将最新拉取的模板写入,自动将模板中的绝对路径占位符(如 `__OPENFLARE_LUA_DIR__`、`__OPENFLARE_PAGES_DIR__`)替换为本地实际运行路径。
3. **语法校验**:调用 `openresty -t -c <temp_nginx.conf>` 进行严格的语法测试。
4. **平滑重载**:若校验通过,将新配置移至正式路径,执行 `openresty -s reload`。若 OpenResty 处于未启动状态,则使用当前配置拉起进程。
5. **捕获异常**:校验或重载失败时,Agent 会截获标准错误输出(stderr),提取前 2000 个字符的详细报错信息。
---
## 发布与配置应用模型
OpenFlare 摒弃了动态 Patch 节点配置的落后方式,采用 **不可变配置版本发布模型**。
```text
修改规则 -> 预览 / 查看 diff -> 发布 -> 生成完整配置版本 -> 激活版本 -> Agent 拉取 -> 本地应用 -> 上报结果
```
### 1. 核心设计原则
* **完整发布**:每次发布均是对当前控制面所有启用路由、证书、Pages 部署引用、全局与局部 WAF 规则进行一次性全量编译,生成带唯一 `checksum` 的完整版本。
* **版本格式**:采用 `YYYYMMDD-NNN` 递增格式,确保版本历史直观、具备单调递增性。
* **全局单激活版本**:系统同时只有一个处于 `active` 状态的全局配置版本。回滚时无需逆向打补丁,只需将历史某个健康版本的状态改为 `active`,Agent 重新拉取应用即可。
### 2. 三阶段容灾回滚机制
当 Agent 发现配置应用(或平滑重载)失败时,将自动激活以下三阶段容灾防瘫痪链路:
```mermaid
graph TD
A[配置应用失败] --> B[第一阶段: 尝试本地备份恢复]
B -- 备份文件存在 --> C[写入本地备份文件]
C --> D[执行 openresty -t 校验]
D -- 校验成功 --> E[reload 恢复旧版本运行]
D -- 校验失败 --> F[进入第二阶段]
B -- 无备份 --> F[第二阶段: 写入内置安全兜底配置]
F --> G[写入兜底 nginx.conf: 仅监听 80 端口]
G --> H[启用 stub_status 健康检查]
G --> I[其他路由统一返回 503 且拦截异常配置]
G --> J[尝试拉起 OpenResty 维持基础存活]
J --> K[进入第三阶段]
E --> L[上报 Apply Warning]
K --> M[本地阻断该异常版本重复应用]
M --> N[上报 Apply Error 并保留详细报错]
```
1. **第一阶段:本地备份回退**
* Agent 尝试从前一步保存的 `.backup` 目录恢复主配置、路由及证书。
* 写入备份文件后,重新执行 `openresty -t` 校验。若成功,重载回退并向 Server 上报 `Warning`(警告:应用新版本失败,已自动退回历史健康版本)。
2. **第二阶段:内置安全兜底运行**
* 若本地不存在备份配置(如首次部署即配置错误),或者回退备份配置依然校验失败,Agent 将激活最终自愈机制——写入**内置安全兜底配置**。
* **安全兜底配置规范**:
* 仅监听 `80` 端口,不包含任何用户的真实反代路由。
* 除 `/openflare/stub_status` 健康监测路由返回正常外,其他一切访问请求统一返回状态码 `503 Service Unavailable`,响应体固定为 `OpenFlare: No Valid Configuration`。
* 尝试以此极简配置拉起 OpenResty。这能够确保 Nginx 进程自身不瘫痪,保留了底层的健康检查与探针通道,防止容器/Pod 因健康检查失败而被调度系统不断销毁重启,同时保护了敏感路由的安全性。
3. **第三阶段:本地配置阻断**
* Agent 会将当前导致崩溃的配置 `version + checksum` 记录在本地状态库的阻断名单中。
* 在控制面未激活新的配置(`checksum` 发生变化)之前,Agent 心跳将阻断对此异常版本的重复同步拉取,防止节点陷入“心跳 -> 拉取崩溃配置 -> 崩溃回滚”的死循环。
### 3. WAF IP 组运行时异步同步
为了避免高频变动的恶意 IP 黑名单频繁触发主配置的全量发布与 reload(平滑重载对 Nginx 依然有微小的 CPU 与连接开销),IP 组成员采用了与发布版解耦的**异步差分同步设计**:
* **静态发布快照**:发布生成的 `waf_config.json` 中仅包含规则组对 IP 组的引用关系(即 `ip_whitelist_group_ids` / `ip_blacklist_group_ids`),不包含具体的 IP 成员列表。
* **心跳差分对比**:Agent 在心跳包中上报本地已缓存 IP 组的 MD5 Checksum 映射表。
* **差分下发**:Server 比对当前激活版本引用的 IP 组哈希,仅向 Agent 下发缺失或发生变更的 IP 组成员,写入本地 `waf_ip_groups.json`,实现极速差分同步。
* **WebSocket 实时通知**:当 Server 手动更新 IP 组、订阅源自动同步成功、或安全规则自动触发临时封禁时,Server 会立即通过 WebSocket 广播受影响的 IP 组更新包,Agent 接收落地并即时生效,全程**无须 reload Nginx**。
---
## 设计约束
为保证数据与控制链路的安全边界,Agent 代码编写与二次开发必须严格遵守以下工程约束:
1. **零特权指令通道**:Server 绝对禁止向 Agent 传递任何任意 shell 命令或远程执行脚本(如 exec/eval 等)。所有系统控制原语(如启动、停止、重载、更新)必须硬编码在 Agent 二进制内部。
2. **严格的 Token 过滤与前缀验证**:Agent 侧向 Server 请求资源时,接口端点固定以 `/api/v1/agent/` 为前缀,并强制携带 `X-Agent-Token` 进行签名或令牌核验。
3. **节点自治原则**:Agent 须具备完备的离线工作能力。在与 Server 失去连接期间,本地 OpenResty 必须依靠本地已落地的配置保持反向代理服务的绝对正常运行。
4. **观测只上报事实**:访问日志以明细形式上送;主机指标上报计数器/瞬时读数。禁止在 Agent 内计算业务 UV、Top 域名、24h 已提供数据等结论性指标(由 Server 聚合)。详见 [边缘可观测与业务流量统计](./observability-design.md)。
5. **Pages 只消费控制面产物**:Remote URL、GitHub Release、自动 scanner,以及未来仓库 checkout/build executor 均属于 Server 职责。Agent 不接收外部 URL、访问令牌、仓库凭据或任意 clone/install/build 命令,只拉取已经激活且带完整性元数据的部署包。
-200
View File
@@ -1,200 +0,0 @@
# 系统架构
你会学到:OpenFlare 的整体架构、各核心组件(Server, Agent, OpenResty, Relay, Client)的职责分工,以及主要数据与请求流的宏观流向。
OpenFlare 是一套自托管的 OpenResty 控制面。它在物理上由 Server(控制面)、Agent(配置落地端)、节点本地 OpenResty(数据面)、内网穿透组件(Relay 与 OpenFlared,数据面扩展)以及管理端前端组成。
---
## 流量路径概览
根据不同的网站上游类型,OpenFlare 支持三种不同的数据面流量路径:
### 1. 标准反代流量路径
```text
Browser
|
| HTTPS/HTTP request
v
OpenResty (WAF, TLS, Rate Limit)
|
| reverse proxy (proxy_pass)
v
Origin Server (直连公网/局域网上游)
```
### 2. 内网穿透流量路径
适用于内网受限服务器上的源站服务接入:
```text
Browser
|
| HTTPS/HTTP request
v
OpenResty (Agent 宿主机, TLS/WAF)
|
| proxy_pass http://localhost:vhost_port (Host header preserved)
v
OpenFlareRelay (frps) <-- 与 Agent 同机部署,提供中继
|
| frp tunnel protocol (Host header routing)
v
OpenFlared (frpc) <-- 内网受限服务器
|
| HTTP/HTTPS forward
v
Internal Service (192.168.x.x)
```
### 3. Pages 静态托管流量路径
适用于预构建的单页应用(SPA)或静态网站托管:
```text
Browser
|
| HTTPS/HTTP request
v
OpenResty (Agent, TLS/WAF)
|
+---> [静态服务] root/try_files ---> Agent 本地 Pages 部署目录
|
+---> [API 反代] proxy_pass ---> 后端 API 服务 (如果启用了 API 代理)
```
---
## 组件职责
| 组件 | 职责 | 详细设计参考 |
| --------------- | ---------------------------------------------------------------------- | ------------ |
| **Server** | 管理端 UI/API、控制面状态持久化、配置编译渲染、发布版本控制、Pages 部署包存储、访问日志入库与业务流量聚合、Uptime Kuma 监控同步与登录验证码防护 | [Agent 与发布模型](./agent-design.md) / [边缘可观测与业务流量统计](./observability-design.md) / [Uptime Kuma 监控同步设计](./kuma-design.md) / [登录验证码设计](./login-captcha.md) |
| **Agent** | 周期心跳与 WS 同步、静态资源包拉取与解压、OpenResty 配置写入/校验/重载与自愈;观测仅上报访问明细与主机/健康读数,不做业务预聚合 | [Agent 与发布模型](./agent-design.md) / [边缘可观测与业务流量统计](./observability-design.md) |
| **OpenResty** | 接收真实流量,执行 WAF 过滤、PoW 防护、Basic Auth 认证与静态/反代服务 | [WAF 设计](./waf-design.md) / [Pages 设计](./pages-design.md) |
| **Relay** | 部署于边缘节点,管理 `frps` 守护进程生命周期,接受心跳派发的穿透中继配置 | [内网穿透设计](./tunnel-design.md) |
| **OpenFlared** | 部署于内网,管理 `frpc` 进程组,向多个 Relay 建立反向隧道,上报连接状态 | [内网穿透设计](./tunnel-design.md) |
---
## 组件架构与分工
### 1. Server (控制面)
仓库根目录的 Go 后端(模块 `github.com/Rain-kl/Wavelet`)是 OpenFlare 控制面,基于 Wavelet 全栈脚手架构建:
* 提供管理端 REST API(`/api/v1/d/*`),通过 **Session Cookie** 鉴权,可选 `X-Access-Token` 访问令牌。
* 边缘节点协议走 `/api/v1/agent|relay|tunnel/*`,分别使用 `X-Agent-Token` / `X-Tunnel-Token` 鉴权。
* 包含配置编译器(Compiler),将数据库中的规则、证书与全局参数统一编译为不可变的配置快照及 OpenResty 物理配置文件文本。
* 统一接收 Pages 本地上传、Remote URL 与公开 GitHub Release 预构建产物,完成来源检查、受限下载、归档校验和不可变 deployment;manual 上传生成待显式激活的 candidate,持久来源 sync 才 create-or-load 并原子激活。Server 向 Agent 提供受控的 latest 下载接口;内部 scanner 负责 GitHub latest 的限量检查、租约恢复、可选自动发布与孤儿上传记录补偿,通用任务管理入口不能修改该排程。未来仓库源码构建由独立 Server build executor 扩展,Agent 不执行第三方拉取或构建命令。
* 后台集成 Uptime Kuma 监控同步服务,自动为可用站点维护 HTTP 探测任务。
* 启动入口为根目录 `main.go` + `internal/cmd/`(`api` / `worker` / `scheduler` / `all`);OpenFlare 业务在 `internal/apps/openflare/`,边缘协议处理在 `internal/apps/openflare/{agent,relay,flared}/`。
* *详细设计请参阅:[Agent 与发布模型设计](./agent-design.md) 以及 [Uptime Kuma 监控同步设计](./kuma-design.md)*
### 2. Agent (配置落地端)
`openflare-agent` 是运行在节点本地的守护进程:
* 启动后维持与控制面的周期性心跳,并通过可选的 WebSocket 接收实时的配置发布广播。
* 负责拉取最新激活版本的配置文件及证书,写入本地目录,并通过 `openresty -t` 执行安全校验后平滑重载 (`reload`)。
* 在本地处理 Pages 部署包的下载、SHA-256 校验与解压缩切换。
* *详细设计请参阅:[Agent 与发布模型设计](./agent-design.md)*
### 3. OpenResty (数据面)
接收访客流量并执行最终的业务落地:
* 流量入口,支持 HTTP/2、HTTP/3(QUIC)和 TLS 证书动态绑定。
* 嵌入 Lua 逻辑,在 `access_by_lua` 阶段高效过滤 WAF 规则、验证工作量证明 (PoW) 挑战,并在此之后执行连接数/速率限制及基础缓存(策略见 [边缘缓存策略设计](./edge-cache-design.md))。
* *详细设计请参阅:[WAF 设计文档](./waf-design.md) 与 [Pages 静态托管设计文档](./pages-design.md)*
### 4. Relay 与 OpenFlared (穿透组件)
扩展数据面反穿透能力:
* `openflare-relay` 守护本地 `frps`,接受 Server 的配置派发,自动更新中继端口。
* `openflared` 在内网守护一组 `frpc` 客户端进程,实现多中继就近建连与高可用容灾。
* *详细设计请参阅:[内网穿透隧道设计文档](./tunnel-design.md)*
---
## 数据与请求流概览
### 1. 配置发布与同步流
```text
管理端修改配置 -> 发布新版本 -> 生成全局唯一 Checksum 激活版本
|
+------------------+------------------+
| (WebSocket 广播或周期 Heartbeat) |
v v
[边缘节点 Agent] [内网 OpenFlared]
拉取最新 OpenResty 配置/证书 拉取最新 Tunnel 映射配置
增量拉取/解压 Pages 静态部署包 生成/重写 frpc.toml
Nginx 校验配置并平滑重载 (reload) 平滑重载或拉起 frpc 进程
上报应用状态 (Success / Error) 上报隧道连接状态与活跃指标
```
* *同步与自愈的精细时序及回滚模型详见:[Agent 与发布模型设计](./agent-design.md)*
### 2. 静态托管与 API 代理流
* 静态资源解压落地于 Agent 节点的 `projects/{project_id}/current` 下(按项目 latest 拉取,仅保留最新包),OpenResty 通过 `root`/`index`/`try_files` 在边缘直接提供静态资源服务。
* 当启用 API 代理时,OpenResty 自动根据站点配置的 `api_proxy_path`(如 `/api`)将 API 请求重写并转发(`proxy_pass`)给后端动态接口。
* 管理员操作和内部 scanner 都只生成受约束的 artifact candidate,并复用统一 inspect、`upload.Ingest` 与 deployment pipeline。manual 上传创建新的未激活 candidate;持久来源 sync/scanner 才 create-or-load 并原子激活。未来 repository build executor 也只能向同一 artifact pipeline 输出产物;Agent 始终只是 active deployment 消费者。
* *部署包校验、解压逃逸防御及 Nginx 规则渲染详见:[Pages 静态托管设计文档](./pages-design.md)*
### 3. WAF 安全过滤流
* WAF 引擎嵌入在 OpenResty 请求生命周期中。
* WAF 规则由控制面以可视化 DAG 编排,发布时编译为运行态图;OpenResty reload 后由每个 Worker 加载一次,后续请求只遍历内存对象。
* 全局规则固定前置,路由绑定规则按显式顺序执行;当前规则抵达“通过”后继续下一条,抵达“阻止”则立即返回该节点配置的拦截响应。
* IP 组成员独立热更新:协调 Worker 每 5 秒检查一次 checksum,仅在变化时加载完整快照,各 Worker 的请求路径始终读取本地内存对象。
* *IP 组来源与同步机制详见:[WAF 设计文档](./waf-design.md);图模型、执行语义与发布约束详见:[WAF 可编排规则设计](./waf-orchestration-design.md)。*
### 4. 边缘可观测与业务流量统计流
```text
OpenResty access.log(业务事实)
|
| Agent tail 增量明细(不 sum/count/uniq)
v
Server 入库 ClickHouse
|
+---> 全局聚合 --> 看板「已提供数据 / 请求 / UV」
+---> host∈Zone --> Zone「已提供数据」等(同一套语义)
+---> node_id 过滤 --> 节点业务量
主机 /proc 网卡与 CPU 等 --> Agent 读数快照 --> 宿主机资源趋势(与业务交付分开展示)
OpenResty 健康与连接数 --> 边缘健康(瞬时,不作 24h 业务总量)
```
* **原则**:Agent 只上报事实,Server 解释事实;业务流量唯一真相为访问日志。`openresty_tx` 与「已提供数据」不得双轨并存。
* *传输模型、示例与采集频率详见:[观测数据传输模型](./observability-transport-model.md);字段收敛与迁移详见:[边缘可观测与业务流量统计](./observability-design.md)*
---
## 核心对象
当前系统核心实体包括:
* **反代与配置**:`zones` (根域管理边界), `zone_domains` (明确域名与证书/路由关联), `proxy_routes` (路由策略), `origins` (源站), `config_versions` (配置版本), `tls_certificates` (证书). 详见 [Zone 与域名资源设计](./zone-design.md)。
* **Pages 静态托管**:`of_pages_projects` (Pages项目), `of_pages_project_sources` / `of_pages_project_source_runtime` (可变来源配置与运行态), `of_pages_deployments` (不可变部署), `of_pages_deployment_files` (部署文件清单).
* **节点与穿透**:`nodes` (节点), `tunnels` (隧道客户端), `node_system_profiles` (系统概况), `apply_logs` (应用日志).
* **WAF 与安全**:`waf_rule_groups` (WAF规则组), `waf_ip_groups` (WAF IP组), `waf_rule_group_bindings` (网站WAF绑定).
* **系统与账号**:`acme_accounts` (ACME账户), `dns_accounts` (DNS账户), `geoip_update_configs` (GeoIP更新配置).
---
## 关键设计决策
| 决策 | 原因 |
| ------------------------------ | --------------------------------------------------------------------------- |
| 完整配置版本,而不是在线 patch | 让预览、激活、历史和回滚有稳定边界,保证节点状态一致 |
| Agent 主动拉取 | Server 不需要 SSH 权限,降低安全风险;支持 HTTP 与 WebSocket 双协议灵活切换 |
| 全局单激活版本 | 降低控制面复杂度,保证所有节点默认一致;提供一键秒级回滚的稳定机制 |
| Zone 域名与路由策略分离 | Zone 提供根域入口与域名边界;路由仍可复用同一套站点级策略并按域名绑定证书 |
| 内网穿透基于 frp 整合 | 复用成熟隧道协议,避免自研隧道引起稳定性风险;其 Vhost 机制天然适配反代路由 |
| 运行时配置与控制库解耦 | WAF 规则发布时编译并随 OpenResty reload 加载;动态 IP 组通过 checksum 驱动的内存快照独立刷新 |
| 业务流量以访问日志为唯一真相 | Agent 禁止业务预聚合;看板与 Zone 共用 Server 侧聚合,避免 openresty_tx 与 bytes_sent 双轨 |
| 业务交付 / 边缘健康 / 主机资源分层 | 已提供数据≠宿主机网卡出站≠OpenResty 连接数,UI 与 API 分名分区 |
| Pages artifact 与仓库构建分离 | 现有来源只导入预构建产物;未来 checkout/build 由 Server 隔离 executor 完成并复用 artifact pipeline,Agent 不执行第三方构建 |
---
## 贡献者阅读建议
修改系统架构或开发新功能前,请按以下顺序阅读:
1. **[产品边界](./index.md)**:了解 OpenFlare 核心定位与不允许逾越的设计边界。
3. **[Agent 与发布模型](./agent-design.md)**:理解版本快照同步及失败回滚的安全兜底逻辑。
4. **细分领域设计**:
* Zone 与域名相关开发:阅读 [Zone 与域名资源设计](./zone-design.md)。
* 穿透相关开发:阅读 [内网穿透隧道设计](./tunnel-design.md)。
* WAF 相关开发:阅读 [WAF 设计](./waf-design.md) 与 [WAF 可编排规则设计](./waf-orchestration-design.md)。
* Pages 托管开发:阅读 [Pages 静态托管设计](./pages-design.md)。
* 监控同步开发:阅读 [Uptime Kuma 监控同步设计](./kuma-design.md)。
* 看板/访问日志/节点指标开发:阅读 [观测数据传输模型](./observability-transport-model.md) 与 [边缘可观测与业务流量统计](./observability-design.md)。
5. **[仓库结构](./index.md#仓库结构)**:明确各个物理目录分层职责,避免堆砌和重复开发。
-190
View File
@@ -1,190 +0,0 @@
# 边缘缓存策略设计(对标 Cloudflare 默认可缓存范围)
你会学到:OpenFlare 边缘 `proxy_cache` 的产品边界、默认可缓存范围如何对齐 Cloudflare「静态资源默认可缓存」、策略枚举与渲染规则、兼容迁移,以及本阶段明确不做的能力。
本设计是 [系统架构](./architecture.md) 中「基础缓存」的产品化专章;访问日志中的缓存结果见 [观测数据模型 §3.5.1](./observability-data-model.md)。
---
## 1. 目标与非目标
### 1.1 目标(第一期)
* **开箱接近 CF 默认**:路由开启缓存后,**默认只缓存静态扩展名**,不默认缓存 HTML/无扩展名动态路径。
* **行为可解释**:与现有安全旁路(非 GET、Authorization、会话 Cookie、请求 `Cache-Control`)叠加,不削弱安全。
* **可观测一致**:继续依赖 `$upstream_cache_status` → `cache_status` 明细三态。
* **兼容存量**:旧路由 `cache_policy=url`(近似「过旁路即可缓存」)迁移为显式策略 `all`,行为不变。
### 1.2 非目标(后续迭代)
* Cache Rules 表达式引擎
* Edge TTL / `proxy_cache_valid` / 忽略源站 `Cache-Control`
* 可配置 Cookie 旁路列表、Query 忽略列表
* Purge(按 URL/前缀/全站)
* 浏览器 TTL 改写、客户端 `CF-Cache-Status` 响应头
* 命中率看板
---
## 2. 现状摘要
| 层 | 现状 |
| --- | --- |
| 全局 | `proxy_cache_path` / key / lock / stale(Performance 部分字段) |
| 路由 | `cache_enabled` + `cache_policy`:`url` \| `suffix` \| `path_prefix` \| `path_exact` |
| 旁路 | 渲染器硬编码:非 GET、Authorization、会话 Cookie、请求 Cache-Control |
| TTL | **无** `proxy_cache_valid`;存多久主要看源站头 + `inactive` |
| 观测 | 已上报 `cache_status`,UI 三态:命中 / 回源 / 未缓存 |
问题:默认策略 `url` 对「过旁路的 GET」范围过宽,与 CF「默认主要缓存静态扩展名、默认不缓存 HTML」不一致。
---
## 3. 产品语义
### 3.1 双层开关(不变)
* **全局** `openresty_cache_enabled`:生成 `proxy_cache_path` 等;关闭则路由级缓存指令不生效。
* **路由** `cache_enabled`:是否在该站点 `location` 启用 `proxy_cache`。
两者均开启时才进入缓存逻辑。
### 3.2 策略枚举(第一期)
| `cache_policy` | 含义 | 新建默认 | 旧值兼容 |
| --- | --- | --- | --- |
| **`static`** | 仅 URI 匹配**标准静态扩展名**(内置表)才允许缓存 | **是** | — |
| **`all`** | 过安全旁路后,不限制路径/扩展名(等同今日 `url`) | 否 | 存量 `url` → `all` |
| **`suffix`** | 自定义扩展名列表(`cache_rules`) | 否 | 保持 |
| **`path_prefix`** | 自定义路径前缀 | 否 | 保持 |
| **`path_exact`** | 自定义精确路径 | 否 | 保持 |
> 渲染层:读到历史值 `url` 时按 `all` 处理,避免未迁移数据行为突变;API 校验与 UI 只暴露上表枚举(写入时可将 `url` 规范为 `all`)。
### 3.3 标准静态扩展名(内置,V1 硬编码)
对齐 Cloudflare 常见「默认可缓存静态」集合,**默认不包含** `html` / `htm`:
```text
css js mjs map json
ico cur gif jpg jpeg png webp avif svg svgz
ttf otf woff woff2 eot
mp3 mp4 webm ogg flac
wasm pdf
zip 7z gz tar
```
* 匹配对象:`$uri` 的扩展名(大小写不敏感),实现上与现有 `suffix` 策略相同:
`if ($uri !~* \.(?:css|js|…)$) { set $openflare_skip_cache 1; }`
* **V1.1(可选)**:全局配置项覆盖该列表;第一期不强制。
### 3.4 安全旁路(保持硬编码)
在策略匹配之前/之外,仍设置 `$openflare_skip_cache=1`:
1. `$request_method != GET`(含 HEAD,与现网一致)
2. `$http_authorization != ""`
3. 会话类 Cookie 正则(现网列表)
4. 请求 `$http_cache_control` 匹配 `no-cache|no-store|private`
`proxy_cache_bypass` / `proxy_no_cache` 均绑定 `$openflare_skip_cache`。
### 3.5 与源站头的关系(本阶段不改)
* 仍不输出 `proxy_cache_valid`。
* 对象**是否进入缓存流程**由策略 + 旁路决定;**存多久**继续依赖源站 `Cache-Control` / `Expires` 等及全局 `inactive`。
* Edge TTL / 强制忽略源站头 → 后续专项。
---
## 4. 渲染与数据流
```text
全局 cache_enabled?
│ no → 不生成 proxy_cache_*
▼ yes
路由 cache_enabled?
│ no → location 无 proxy_cache
▼ yes
set $openflare_skip_cache 0
→ 安全旁路 if → 置 1
→ 策略 if(static/all/suffix/…)→ 可置 1
proxy_cache openflare_cache
proxy_cache_methods GET
proxy_cache_bypass / proxy_no_cache $openflare_skip_cache
→
access.log cache_status=$upstream_cache_status
```
### 4.1 策略 → Nginx 条件
| 策略 | 额外条件 |
| --- | --- |
| `static` | `$uri` 不匹配内置扩展名表 → skip |
| `all` | 无额外路径条件 |
| `suffix` | 不匹配 `cache_rules` 扩展名 → skip |
| `path_prefix` / `path_exact` | 同现实现 |
### 4.2 涉及代码面(实现时)
| 区域 | 路径 |
| --- | --- |
| 渲染 | `pkg/render/openresty/render.go`(策略分支 + 内置扩展名常量) |
| 校验 | `internal/apps/openflare/proxy_route/helpers.go` |
| 模型/默认 | 创建路由默认 `cache_policy=static`;读写时 `url`→`all` |
| 快照 | `config_version/snapshot.go` |
| UI | `proxy-routes/detail/components/cache-section.tsx` |
| 测试 | `pkg/render/openresty/render_test.go`、proxy_route helpers 测试 |
---
## 5. 兼容与迁移
| 数据 | 处理 |
| --- | --- |
| DB 中 `cache_policy=''` 或 `url`(且已启用缓存) | 读取 / 快照 / 渲染均规范为 **`all`**,保证存量「宽缓存」不变 |
| API 写入时 `enabled` 且 policy 为空 | 规范为 **`all`**(兼容旧客户端);UI 新建开启时**显式提交** `static` |
| 新建路由 | 默认 `cache_enabled=false`;表单开启缓存时默认策略 **`static`** |
| 已开启且 `url` 的站点 | 显示与发布为 `all`,**缓存范围不变** |
| 期望「只缓存静态」的旧站点 | 用户在 UI 改为 `static` 或自定义 `suffix` |
**发布说明建议:** 说明默认策略变更仅影响**新配置**;存量 `url` 视为 `all`。
---
## 6. UI 文案要点(缓存 Tab)
* 开启缓存后默认:**标准静态资源**(列出扩展名摘要,并写明不含 HTML)。
* 选项:**标准静态资源** / **所有可缓存 GET(高级)** / 自定义后缀 / 路径前缀 / 精确路径。
* 固定说明:非 GET、带 Authorization、常见登录 Cookie、请求禁止缓存头时跳过缓存。
* 提示:全局 Performance 中缓存总开关须开启,否则站点开关无效。
---
## 7. 验证要点
* 渲染:`static` 生成扩展名 `if`;`all`/`url` 无路径限制;旁路四条仍在。
* 单测:内置表含 `css`/`js`/`woff2`,不含 `html`。
* 手动:开启 `static` 后请求 `/a.css` 可出现 HIT/MISS;`/index.html` 或 `/api` 多为未缓存/BYPASS。
* 观测:access log `cache_status` 与列表三态一致。
---
## 8. 后续路线图(非本设计交付)
1. **Edge TTL / 尊重源站开关**(`proxy_cache_valid`、`proxy_ignore_headers`)
2. **可配置旁路**(Cookie/Query)
3. **Purge API**
4. **Cache Rules**(有序规则 + 动作)
5. **全局默认可缓存扩展名配置**
---
## 9. 决策记录
| 决策 | 选择 | 原因 |
| --- | --- | --- |
| 默认可缓存范围 | 开启缓存默认 `static` 扩展名表 | 对标 CF 开箱行为,降低 HTML/API 被误缓存 |
| 旧 `url` | 映射为 `all` | 避免存量站点行为变化 |
| HTML | 默认不在白名单 | 对齐 CF 默认不缓存 HTML |
| 第一期不做 Edge TTL/Purge | 明确 Out of Scope | 先收敛「谁可以进缓存」再优化「存多久/怎么清」 |
-195
View File
@@ -1,195 +0,0 @@
# 产品边界
你会学到:OpenFlare 是什么、当前稳定能力,以及开发时应遵守的核心产品边界与仓库结构目录分工。
OpenFlare 是一套自托管的 OpenResty 控制面,面向单团队或单组织内部运维场景。
---
## 项目定位
OpenFlare 适合需要统一管理多台 OpenResty 代理节点的团队,具备以下定位:
* **控制与落地分离**:Server 控制面不直接 SSH 到代理节点,而是通过 Agent 主动拉取版本并应用。
* **不可变配置发布**:采用完整的配置版本进行预览、发布、激活和一键回滚。
* **一体化网关托管**:在同一个控制面内集成网站反代、TLS 证书自动续期申请、WAF 防护拦截、内网穿透(Tunnel)以及 Pages 静态网站托管。
**非本产品定位**:多租户云平台、Kubernetes Ingress Controller、服务网格或通用日志平台。
---
## 当前能力
| 能力 | 说明 | 详细设计/使用指南 |
| --- | --- | --- |
| **反代配置管理** | 以网站规则(Proxy Route)为聚合边界,支持多域名与多上游负载均衡 | [新建反代配置](../guide/proxy-config.md) |
| **边缘缓存** | 单节点 OpenResty `proxy_cache`;开启后默认仅缓存标准静态扩展名(对标 CF 默认可缓存范围) | [边缘缓存策略设计](./edge-cache-design.md) |
| **Zone 与域名管理** | 以可注册根域为管理入口,聚合明确域名、域名证书与反代路由 | [Zone 与域名资源设计](./zone-design.md) |
| **配置版本控制** | 支持全局单一激活版本的预览、发布、不可变快照历史与秒级一键回滚 | [Agent 与发布模型](./agent-design.md) |
| **WAF 安全防护** | 支持可视化 DAG 编排规则、手动/自动/订阅型 IP 组、GeoIP 匹配与 PoW CC 防护 | [WAF 设计](./waf-design.md) / [WAF 可编排规则设计](./waf-orchestration-design.md) / [WAF 使用指南](../guide/waf-usage.md) |
| **内网穿透** | 通过中继节点(Relay)与内网客户端(OpenFlared),反向穿透暴露内网 Web 服务 | [内网穿透设计](./tunnel-design.md) / [穿透使用指南](../guide/tunnel-usage.md) |
| **Pages 静态托管** | 支持上传或从 Remote URL、公开 GitHub Release 同步预构建产物;GitHub latest 可定时检查并可选自动发布。不可变部署由边缘节点拉取并由 OpenResty 本地服务,支持回滚、API 反代与 SPA Fallback | [Pages 静态托管设计](./pages-design.md) / [Pages 使用指南](../guide/pages-usage.md) |
| **TLS 证书自动续期** | 将证书显式绑定到 Zone 域名,并通过 ACME 协议向 Let's Encrypt 申请/续期证书 | [Zone 与域名资源设计](./zone-design.md) |
| **多节点监控与观测** | 访问日志为业务流量唯一真相;Agent 只上报明细与主机读数,Server 统一聚合;与 Zone/看板对账 | [观测数据传输模型](./observability-transport-model.md) / [边缘可观测与业务流量统计](./observability-design.md) / [上报协议与表结构](./observability-data-model.md) / [系统架构](./architecture.md) |
---
## 核心产品边界与约束
在开发与贡献代码时,**必须严格遵守**以下业务边界与技术约束,禁止为了临时需求而绕过限制:
### 1. 网站配置与上游约束
* **单站点域名共享策略**:一条路由规则对应一个网站,该站点下的多域名共享限流、缓存与反代上游等配置,不支持在同一规则内为不同域名做差异化服务配置。
* **上游类型互斥**:上游必须是直连地址(`direct`)、内网穿透(`tunnel`)或 Pages 静态托管(`pages`)三者之一,不允许在同一规则中混用。
* **直连类型限制**:直连上游可以是纯 `http://` 或 `https://` 的单个或多个地址(多地址仅支持纯 `scheme://host[:port]`),不支持非 HTTP 协议(如 TCP/UDP)上游。
### 2. WAF 安全边界
* **白名单优先原则**:白名单拥有绝对匹配权。若未命中白名单规则,才依次触发全局和自定义黑名单过滤。
* **GeoIP 弱依赖性**:地域准入解析完全依赖节点本地 MaxMind 库。当 GeoIP 异常或解析失败时,系统必须自动忽略地域规则,**绝对不能**破坏 IP 组过滤和反代主链路的可用性。
* **运行时数据解耦**:OpenResty 拦截时仅读取 Agent 同步至本地的 JSON,不与 Server 数据库通信。IP 组成员同步与版本发布解耦,通过 Checksum 差分拉取以实现零重载平滑生效。
### 3. 内网穿透边界
* **仅限 HTTP 流量**:穿透组件仅支持 HTTP/HTTPS 协议(底层依靠 frp 虚拟主机 Vhost 机制实现单端口域名路由复用),暂不支持单独的 TCP/UDP 端口分配。
* **中继配置动态化控制**:中继节点(Relay)在连接至 Server 后,可通过心跳周期性动态拉取并同步全局系统配置(例如是否开启内嵌 FRPS Web UI 及其监听端口),但不直接纳入控制面的不可变配置版本发布体系。
* **Tunnel 与 Node 体系隔离**:Tunnel 客户端在内网发起出向建连,与控制面托管的边缘 Node(公网节点)是独立的实体,使用专属的 `tunnel_token` 进行鉴权。
### 4. Pages 静态托管边界
* **预构建产物来源**:项目可保持手动上传,或配置一个 Remote URL / 公开 GitHub Release asset 来源。Remote 与固定 tag 只支持手动操作;只有 GitHub latest 进入定时检查并可选择自动更新。来源可切换,但不可变 deployment 与当前生产版本不会随 source 编辑或删除而丢失。
* **归档与资源上限**:支持 `zip`、`tar.gz` / `tgz`、`tar.xz` / `txz`、`tar.bz2` / `tbz2`、`tar`、`7z`。压缩包上限由 `pages_max_package_size_mb` 控制(默认 100 MiB,范围 1~2048);展开后的单文件和总量上限为包上限的 4 倍且最低 100 MiB,最多 1,000 个常规文件。Server 与 Agent 都校验实际字节,并拒绝路径逃逸、软/硬链接与特殊文件。
* **构建与运行时边界**:当前不从外部 Git 仓库拉取源码或执行构建,也不提供边缘 Serverless、动态 SSR 或二级预览域名。未来仓库集成必须使用独立 `git_repository` Provider 与 Server 侧隔离 build executor,只向统一 artifact 管线输出受限产物;Agent 不接收仓库凭据、外部 URL 或 clone/install/build 命令。
### 5. 系统与版本边界
* **全局单一激活版本**:所有节点拉取并消费同一份全局激活配置。不进行按节点分组的差异化配置发布。
* **单租户架构**:OpenFlare 仅供单团队在受信任的内部网络部署使用。采用单租户设计,不支持细粒度的多用户角色或多租户资源隔离。
* **外部基础设施依赖性**:Server 虽支持 SQLite 作为本地轻量关系数据库,但**系统必须强制依赖外部 Redis(或 Valkey)及 ClickHouse 实例**。Redis 用于处理分布式协调、后台异步队列(Asynq 框架)及系统级全局缓存;ClickHouse 用于接收海量节点访问日志与基础观测的异步 Flush。系统不支持完全脱离这两个组件运行。
---
## 仓库结构
OpenFlare 已收敛为**单 monorepo**(Go 模块 `github.com/Rain-kl/Wavelet`)。控制面 Server 与边缘组件(Agent、Relay、OpenFlared)共享同一仓库,业务代码按 Wavelet `internal/apps/` 领域模块组织。
在贡献代码时,请严格遵守以下物理分层与目录分工:
| 路径 | 职责 |
| --- | --- |
| `main.go` | Server 唯一入口,委派给 `internal/cmd/` |
| `cmd/agent`、`cmd/relay`、`cmd/flared` | 边缘组件 CLI 入口(**不含** Server) |
| `internal/` | 控制面与边缘运行时实现 |
| `frontend/` | Next.js 管理端,构建产物嵌入 Go Server |
| `pkg/` | 跨组件共享库(协议、渲染、GeoIP 等) |
| `scripts/` | Swagger 生成、安装脚本等 |
| `docs/` | VitePress 文档站与设计基线 |
| `docker/` | 各组件 Dockerfile |
| `uploads/`、`data/` | 运行时上传目录与静态数据(`.gitignore` 忽略) |
### 1. Server 分层(`main.go` + `internal/`)
| 目录 | 职责 |
| --- | --- |
| `main.go` | Server 启动入口 |
| `internal/cmd/` | Cobra 子命令:`api`、`worker`、`scheduler`、`all`(默认融合模式) |
| `internal/bootstrap/` | 跨模块装配:任务 Handler、推送域事件、进程级初始化 |
| `internal/router/` | HTTP 路由注册与全局中间件 |
| `internal/router/v1/openflare/` | OpenFlare 路由注册器(`register_*.go`) |
| `internal/apps/openflare/` | OpenFlare 控制面业务域(`routers.go` + `logics.go`) |
| `internal/apps/{admin,user,oauth,upload,cap,...}/` | Wavelet 平台能力(用户、认证、任务、推送等) |
| `internal/apps/openflare/{agent,relay,flared}/` | **Server 侧**边缘协议处理器(鉴权、心跳、WS) |
| `internal/model/` | GORM 实体(`openflare_*.go` + 平台模型) |
| `internal/db/migrator/goose/` | goose SQL 迁移(PostgreSQL / SQLite / ClickHouse) |
| `internal/repository/` | 平台域数据访问层 |
| `internal/task/` | Asynq 异步任务(Worker + Scheduler) |
| `internal/config/` | Viper 配置加载 |
| `internal/common/` | 统一 API 响应封装(`response/`) |
| `pkg/protocol/` | Relay / Tunnel 共享 HTTP/WS 协议结构 |
| `pkg/render/`、`pkg/geoip/`、`pkg/wsclient/` | OpenResty 配置渲染、GeoIP、WebSocket 客户端 |
**API 路由前缀:**
| 前缀 | 用途 | 鉴权 |
| --- | --- | --- |
| `/api/v1/d/*` | OpenFlare 管理控制台 API | Session Cookie + 可选 `X-Access-Token` |
| `/api/v1/agent/*` | Agent 节点协议 | `X-Agent-Token` |
| `/api/v1/relay/*` | Relay 中继协议 | `X-Agent-Token` |
| `/api/v1/tunnel/*` | Tunnel 客户端协议 | `X-Tunnel-Token` |
| `/api/v1/admin/*` | Wavelet 平台管理 API | 管理员 Session |
### 2. Agent 模块 (`internal/apps/agent/` / `cmd/agent/`)
| 目录/模块 | 职责 |
| ----------------------------- | -------------------------------------------- |
| `cmd/agent/` | Agent 命令行启动入口及主函数 |
| `internal/apps/agent/config/` | 配置读取与默认值 |
| `internal/apps/agent/heartbeat/` | 心跳与版本摘要判断 |
| `internal/apps/agent/sync/` | 配置拉取与应用编排 |
| `internal/apps/agent/nginx/` | OpenResty 文件写入、校验、reload、启动与回滚 |
| `internal/apps/agent/state/` | 本地状态与观测补报缓冲 |
| `internal/apps/agent/httpclient/` | Server 通信 |
| `internal/apps/agent/wsclient/` | WebSocket 客户端通信 |
| `internal/apps/agent/protocol/` | Agent API 协议类型 |
| `internal/apps/agent/updater/` | Agent 自更新逻辑 |
| `internal/apps/agent/logging/` | 日志处理 |
| `internal/apps/agent/observability/`| 可观测性(指标、链路等) |
| `internal/apps/agent/geoipdata/` | GeoIP 数据处理 |
| `internal/apps/agent/geoipupdate/` | GeoIP 数据更新 |
| `internal/apps/agent/agent/` | 核心 Agent 逻辑与生命周期 |
### 3. Frontend 分层 (`frontend/`)
基于 Wavelet Next.js 脚手架,OpenFlare 业务 UI 以路由共置方式组织在 `app/(main)/` 下。
| 目录 | 职责 |
| --- | --- |
| `app/` | Next.js App Router;`(main)` 控制台、`(auth)` 认证、`(docs)` 文档页 |
| `app/(main)/<domain>/` | 业务页面与域内组件(路由共置) |
| `components/` | 跨域复用 UI(`ui/`、`layout/`、`common/` 等) |
| `lib/services/` | API 服务层:`core/` 基类 + `openflare/` 业务 API |
| `lib/navigation/` | OpenFlare 侧栏导航配置(`openflare-nav.ts`) |
| `lib/theme/` | 主题解析与切换 |
| `contexts/` | 跨页面 UI 状态(用户、通知等) |
| `hooks/`、`lib/hooks/` | 可复用 React Hooks |
| `public/` | 静态资源与主题 CSS |
| `scripts/` | 构建辅助脚本 |
| `proxy.ts` | 开发/生产代理:API 限流与页面鉴权 |
**API 约定**:OpenFlare 业务接口统一前缀 `/api/v1/d/*`,通过 `OpenFlareBaseService` 封装;页面数据获取使用 `@tanstack/react-query`。
### 4. Relay 模块 (`internal/apps/relay/` / `cmd/relay/`)
| 模块 | 职责 |
| ---------------- | ------------------------------------------------ |
| `cmd/relay/` | Relay 命令行启动入口及初始化主函数 |
| `internal/apps/relay/config/`| 本地配置文件解析与默认参数初始化 |
| `internal/apps/relay/frps/` | 管理 frps 进程生命周期、端口与 Token 并监控运行 |
| `internal/apps/relay/heartbeat/`| 周期性 HTTP 心跳通信、上报状态并获取更新请求 |
| `internal/apps/relay/httpclient/`| Server 的通用 API 客户端调用工具类 |
| `internal/apps/relay/observability/`| 采集本地宿主机、frps 的基础运行指标并进行预聚合 |
| `internal/apps/relay/relay/` | 协调中继的核心生命周期、初始化与清理 |
| `internal/apps/relay/state/` | 本地运行时状态、错误记录与持久化缓存 |
| `internal/apps/relay/updater/`| Relay 升级检查、下载安装与重启机制 |
| `internal/apps/relay/wsclient/`| 与 Server 保持的长连接 WebSocket 双向通信管道 |
### 5. OpenFlared (Client) 模块 (`internal/apps/flared/` / `cmd/flared/`)
| 模块 | 职责 |
| ---------------- | ------------------------------------------------ |
| `cmd/flared/` | Client 命令行启动入口及初始化主函数 |
| `internal/apps/flared/config/`| 本地客户端配置加载与解析 |
| `internal/apps/flared/flared/`| 内网穿透客户端的核心调度与状态管理机制 |
| `internal/apps/flared/frpc/` | 热重载/动态生成多 Relay 的 `frpc_{relayNodeID}.toml` 并监控 frpc |
| `internal/apps/flared/heartbeat/`| 与控制面进行的心跳通信,包含 Token 校验机制 |
| `internal/apps/flared/httpclient/`| 客户端通用 API 通信(`/api/v1/tunnel/*`) |
| `internal/apps/flared/sync/` | 增量拉取最新 Tunnel 路由绑定关系、生成快照并应用 |
| `internal/apps/flared/updater/`| 客户端自更新、新版检查与更新落地逻辑 |
| `internal/apps/flared/wsclient/`| 用于实时监听 Server 端隧道配置变更推送的 WS 信道 |
> **说明**:OpenFlared 无独立 `state/` 包;版本与 checksum 由 `frpc/manager.go` 持久化到 `flared-state.json`。
---
## 文档维护原则
* 产品范围或系统边界变化:更新本文档([产品边界](./index.md))。
* 系统结构、组件分工变化:更新 [系统架构](./architecture.md)。
* 发布、同步、回滚与 Agent 模型变化:更新 [Agent 与发布模型](./agent-design.md)。
* 部署方式变化:更新 [部署说明](../deployment/deployment.md) 与 README。
* 配置项变化:更新 [配置项参考](../reference/configuration.md)。
-109
View File
@@ -1,109 +0,0 @@
# Uptime Kuma 监控同步设计
你会学到:OpenFlare 与 Uptime Kuma 监控服务集成的设计背景、基于 Socket.IO 协议的控制流设计、以标签隔离为核心的防污染模型,以及差分增量同步的状态机比对逻辑。
---
## 需求分析
在多节点的网关架构中,监控系统的状态与反向代理路由的状态通常是相互脱节的:
1. **录入开销大**:每当网关控制面新增或下线一个站点,管理员都必须在监控系统(如 Uptime Kuma)中重复配置对应的探测地址与告警策略。
2. **数据不一致**:当代理路由域名发生变更或切换 HTTPS 时,容易遗漏修改监控参数,导致监控系统误报或漏报。
3. **环境污染隐患**:如果简单的在监控中执行全量“删除-重建”同步,不仅会清空监控系统中的历史统计指标和 SLA 曲线,还会影响到用户在此监控实例上自行配置的、与网关无关的其他监控任务。
为了解决这些痛点,OpenFlare 引入了基于客户端/服务器模式的 **Uptime Kuma 自动监控同步机制**,实现网关站点路由定义与可用性监测系统的强一致、低开销以及零污染同步。
---
## 核心架构设计
Uptime Kuma 同步子系统完全运行在 **Server 控制面** 的后台调度器中。
```text
[ OpenFlare 控制面 / 数据库 ] [ Uptime Kuma 实例 ]
│ │
1. 定时 Cron 触发 (Job) │
│ │
2. 读取代理路由与选项配置 │
│ │
3. 连接 Socket.IO 接口 <──── 4. Socket.IO 握手 & 登录 ───┤
│ │
├────── 5. 校验 / 创建 "OpenFlare" 标签 ────────►│
├────── 6. 比对监测站点属性与 Kuma 监控清单 ──────►│
│ │
└────── 7. 执行差分指令 (add / edit / delete) ─►│
```
同步子系统不经过数据面的 Agent 节点,而是由 Server 通过 Uptime Kuma 暴露的 Socket.IO 端点直接交互。这种设计可以降低边缘节点的网络开销,并将鉴权凭证(Kuma 用户名与密码)安全收拢在控制面中。
---
## 标签隔离与防污染设计
为了在一个共享的 Uptime Kuma 实例中安全运行,而不干扰用户手动创建的其他监控项,设计上采用了 **专属标签隔离机制**:
1. **`OpenFlare` 专属标签**:
* 同步程序首次连接时,会调用 `getTags` 接口拉取实例中的所有标签。
* 检查是否存在名为 `OpenFlare` 的标签(默认颜色为靛蓝色 `#4f46e5`)。如果不存在,则通过 `addTag` 接口在 Kuma 中自动创建它。
2. **过滤范围收拢**:
* 同步任务在拉取 Uptime Kuma 的监控列表(`monitorList`)后,仅会保留**打有 `OpenFlare` 标签**的监控项。
* 所有的修改比对(`editMonitor`)和下线清理(`deleteMonitor`)**仅在此过滤子集内进行**。任何未绑定 `OpenFlare` 标签的监控项对同步程序均是“隐形”的,实现了完美的防污染隔离。
---
## 差分同步状态机逻辑
同步程序每次执行时,会对 OpenFlare 本地配置与 Uptime Kuma 数据进行差分计算,根据比对结果执行不同的 Socket.IO 事件:
```mermaid
stateDiagram-v2
[*] --> 检查站点状态与监控范围
state "检查监控范围" as Scope {
[*] --> 校验站点是否启用并且在 Scope 内
校验站点是否启用并且在 Scope 内 --> 在Scope内 : 是
校验站点是否启用并且在 Scope 内 --> 不在Scope内 : 否
}
不在Scope内 --> 检查Kuma中是否存在同名且带标签的监控
检查Kuma中是否存在同名且带标签的监控 --> 执行清理 : 存在
检查Kuma中是否存在同名且带标签的监控 --> 忽略 : 不存在
在Scope内 --> 检查Kuma中是否存在同名监控
state "比对属性" as Compare {
[*] --> 检查是否存在
检查是否存在 --> 新建监控项 : 否
检查是否存在 --> 比对元数据 : 是
比对元数据 --> 属性一致 : 匹配
比对元数据 --> 属性不一致 : 不匹配
}
新建监控项 --> 发送add指令并绑定Tag
属性不一致 --> 发送editMonitor指令
属性一致 --> 忽略
执行清理 --> 发送deleteMonitor指令
忽略 --> [*]
```
### 1. 监测 URL 规范化
站点路由在 OpenFlare 中可配置多个域名,同步程序自动提取其主域名(Primary Domain)并根据是否启用 HTTPS 组装为标准的 `http://` 或 `https://` 前缀。
### 2. 比对属性清单
如果同名且带标签的监控已存在,同步程序会细致比对以下 5 个关键字段是否与当前网关全局 Option 一致。只要有一个字段不匹配,便会触发更新:
* **URL 地址**:`Url`
* **探测频率**:`Interval`(默认 60s)
* **重试次数**:`MaxRetries`
* **重试间隔**:`RetryInterval`(默认 60s)
* **请求超时**:`Timeout`(默认 48s)
---
## 调度器与高并发保护
1. **基于 Cron 的单线程执行**:
* Server 周期性(每 1 分钟)通过后台的 Cron Job 探测是否达到配置的同步间隔(`UptimeKumaSyncInterval`)。
* 任务内部设计了互斥锁(Mutex Locking)。如果前一次同步请求因为网络延迟等原因尚未结束,下一次调度将自动跳过,防止并发多个 Socket.IO 连接对 Uptime Kuma 实例造成 DDOS 冲击。
2. **WebSocket 状态监听**:
* 同步程序利用 Socket.IO 的事件监听机制,在连接建立后,必须等到监听到 `monitorList` 事件的完整列表推送后,才允许向下执行差分算法,以规避因为数据加载不完整导致误删监控项的边界情况。
-127
View File
@@ -1,127 +0,0 @@
# 登录验证码设计 (Login CAPTCHA Integration)
本文档阐述在 OpenFlare 控制面中引入基于 Proof-of-Work (PoW) 与无感浏览器指纹特征的开源 CAPTCHA 方案 —— Cap,以防止对登录 API 进行暴力破解与爬虫撞库攻击的设计。
---
## 1. 业务背景与产品范围
### 背景与痛点
根据我们的系统安全分析,OpenFlare 的登录端点 `/api/user/login` 虽然配置了基于 IP 的限流限制,但由于缺少用户维度的防护机制,攻击者可使用代理池绕过 IP 限制对高权限账户(如 `root`)实施撞库和暴力破解。同时,对于系统登录页面,标准的视觉验证码对用户体验和无障碍不够友好。
### 产品范围与技术选型
* **技术选型**:Cap (Proof-of-Work 驱动的无感无图像验证码解决方案)。
- **核心原理**:客户端(Widget/网页)从服务器获取工作量证明 (PoW) 的难题,使用浏览器后台计算求解并将答案回传。服务器验证答案的正确性,完成人机识别。
- **优势**:无感、无图像验证、不依赖任何外部第三方 API 节点(私密)、包极小。
* **接入范围**:控制面 Server 登录 API(`/api/user/login`)以及前端登录页面。
* **配置粒度**:支持管理员通过控制台 Option 表随时开启/关闭验证码(`CapLoginEnabled`)。
---
## 2. 系统架构与交互时序
### 2.1 模块分工
1. **Frontend (前端)**:
* 在登录页面引入 `cap-widget`(React 19 自定义元素)。
* 提交表单时,伴随提交由 Widget 求解出并得到的 `cap-token`。
2. **Server (控制面后端)**:
* 暴露 `POST /api/cap/challenge` 接口,为客户端分发 PoW 难题和签名的 JWT Token。
* 暴露 `POST /api/cap/redeem` 接口,校验客户端提交的 PoW 解答并核发带有失效时间的登录凭证(Redeem Token)。
* 将 Redeem Token 与对应过期时间保存在内存缓存/Redis 缓存中。
* 在 `POST /api/user/login` 接口中,若启用了验证码保护,先校验并消耗(单次失效)对应的 `cap-token`。
### 2.2 验证流时序图
```mermaid
sequenceDiagram
autonumber
actor User as 用户
participant Browser as 浏览器 (前端 Web)
participant Server as OpenFlare Server (后端)
participant Cache as 内存/Redis 缓存
User->>Browser: 打开登录页面
Browser->>Server: POST /api/cap/challenge (获取难题)
Server->>Browser: 返回 {challenge, token, expires} (JWT 格式)
Note over Browser: Widget 在后台(WASM/Worker)执行 PoW 难题计算
Browser->>Server: POST /api/cap/redeem (提交 solutions + token)
alt 校验 PoW 解答通过
Server->>Cache: 存储 Redeem Token (tokenKey:expires)
Server->>Browser: 返回 {success: true, token} (即 cap-token)
else 校验失败
Server->>Browser: 返回 {success: false, reason}
end
User->>Browser: 输入账号密码,点击登录
Browser->>Server: POST /api/user/login (在 HTTP 请求头中携带 X-Cap-Token)
alt CapLoginEnabled = true
Server->>Server: Middleware (CapAuth) 校验并消费 X-Cap-Token
alt token 合法且未过期且未被消费
Server->>Server: c.Next() -> 执行常规登录逻辑 (密码 Bcrypt 校验)
Server->>Browser: 返回登录成功 (Session Cookie)
else token 无效或已被消费
Server->>Browser: 拦截并返回验证码错误 (401 Unauthorized)
end
else CapLoginEnabled = false
Server->>Server: c.Next() -> 执行常规登录逻辑
end
```
---
## 3. 核心接口与数据模型
### 3.1 接口定义
#### 1. 获取难题 (GET/POST /api/cap/challenge)
* **请求方式**:`POST`
* **接口权限**:公开
* **响应负载**(统一 API 信封,`data` 为业务载荷):
```json
{
"error_msg": "",
"data": {
"challenge": {
"c": 50,
"s": 32,
"d": 4
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires": 1717660800000
}
}
```
#### 2. 核销难题 (POST /api/cap/redeem)
* **请求方式**:`POST`
* **请求负载**:
```json
{
"token": "challenge_jwt_token_here",
"solutions": [12345, 67890, 54321]
}
```
* **响应负载 (成功)**:
```json
{
"success": true,
"token": "random_id:ver_token",
"expires": 1717661000000
}
```
#### 3. 登录接口 (POST /api/user/login)
* **请求负载保持不变**:
```json
{
"username": "root",
"password": "your_password"
}
```
* **验证码载体**:放置于 HTTP Request Header `X-Cap-Token` 中。
---
## 4. 重放攻击防护与安全性权衡
1. **JWT 临时状态绑定**:难题在生成时就被签入 JWT payload,包含过期时间限制(10 分钟)。
2. **Replay 拦截(Nonce 消耗)**:当客户端调用 `/redeem` 提交解答时,后端在缓存中标记该 JWT Signature 已使用。重复提交相同的解密包将返回 `already_redeemed`。
3. **Redeem 一次性核销(单次失效)**:当客户端登录并提交 `cap-token` 时,后端在检验到合法性后立即从缓存中删除该 Key,防止黑客提取历史正确的 `cap-token` 进行重放登录。
4. **验证机制无感化**:通过调整 `c (难题数)=50`,`d (难度)=4`,普通用户在桌面端和移动端只需 0.5 秒至 1.5 秒即可静默解出,极大地兼顾了用户体验和反爬效果。
-779
View File
@@ -1,779 +0,0 @@
# Agent 上报协议与观测落库数据模型
你会学到:重构后 Agent 心跳/WS 上报的 **数据结构**、Server **如何解析与写入**、ClickHouse / 关系库 **目标表结构**。
**无协议兼容层**:Agent 以销毁重建或二进制替换升级;旧字段不解析、旧缓冲整文件丢弃。
本设计是 [边缘可观测与业务流量统计重构](./observability-design.md) 的 **协议与存储专章**,实现时以本文字段与 DDL 为准。
**先读传输全景与示例:** [观测数据传输模型](./observability-transport-model.md)。
---
## 1. 设计目标
| 目标 | 说明 |
| --- | --- |
| Agent 只报事实 | 明细 + 主机读数 + 边缘健康瞬时态;无业务预聚合 |
| 一张业务明细表 | 访问日志是 L1 唯一写入路径 |
| 聚合在库内/控制面 | 小时汇总由 ClickHouse MV 或查询生成,Agent 不写汇总表 |
| 字段不重叠 | `bytes_sent` = 已提供数据;网卡 `network_*` = 宿主机;不再有业务 `openresty_tx` |
| 可演进 | 新字段可选;缺省数值填 0,不解析已删除的旧协议字段 |
---
## 2. 分层与写入总览
```text
Agent NodePayload (v2)
│
┌───────────────┼───────────────┐
▼ ▼ ▼
access_logs host_metrics edge_health
(L1 明细) (L3 读数) (L2 瞬时)
│ │ │
▼ ▼ ▼
of_node_access_logs of_node_metric_ of_node_edge_health
│ snapshots │
│ │ │
▼ ▼ │
of_access_log_hourly of_node_metric_ │
(MV, Server 侧) capacity_hourly (MV) │
│ │ │
└─────── 管理端聚合 API ───────────┘
关系库 (PostgreSQL/SQLite):节点最新状态、Profile、健康事件(非明细湖)
```
| 层 | 含义 | Agent 上报块 | ClickHouse 事实表 |
| --- | --- | --- | --- |
| L1 | 业务交付 | `access_logs` | `of_node_access_logs` |
| L2 | 边缘健康 | `edge_health` | `of_node_edge_health` |
| L3 | 宿主机资源 | `host_metrics` | `of_node_metric_snapshots` |
---
## 3. Agent 上报数据结构(协议 v2)
### 3.1 顶层 `NodePayload`
传输:HTTP 心跳 body 与 WebSocket `status` 消息共用同一结构。
```json
{
"schema_version": 2,
"node_id": "n_xxx",
"name": "edge-1",
"ip": "1.2.3.4",
"version": "3.3.0",
"ext_version": "",
"current_version": "cfg-checksum-or-version",
"last_error": "",
"profile": { },
"host_metrics": { },
"edge_health": { },
"access_logs": [ ],
"buffered": [ ],
"health_events": [ ],
"waf_ip_group_checksums": { "1": "md5..." }
}
```
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `schema_version` | int | 建议 | 固定为 `2`(本设计) |
| `node_id` | string | ✅ | 节点 ID |
| `name` | string | ✅ | 显示名 |
| `ip` | string | ✅ | 上报 IP |
| `version` / `ext_version` | string | ✅ | Agent 版本 |
| `current_version` | string | | 本地激活配置版本摘要 |
| `last_error` | string | | 最近同步/运行错误,可空 |
| `openresty_status` | string | ✅(有 OpenResty 时) | **最新健康态权威字段** → 写 PG 节点表 |
| `openresty_message` | string | | **最新健康说明权威字段** → 写 PG 节点表(**不进 CH**) |
| `profile` | object | | 主机概况,变化时上报(可节流) |
| `host_metrics` | object | 建议每拍 | L3 资源快照 |
| `edge_health` | object | 建议每拍 | L2 连接时序 + 与顶层一致的 status |
| `access_logs` | array | | 本拍增量访问明细 |
| `buffered` | array | | 离线补传的事实批次(见 §3.6) |
| `health_events` | array | | 边缘健康事件 |
| `waf_ip_group_checksums` | map | | 差分同步用,非观测湖 |
**已删除、Server 不再解析的字段(无兼容层):**
| 旧字段 | 处置 |
| --- | --- |
| `traffic_report` | 不存在于协议;不落库 |
| `openresty_observation` | 不存在;连接与状态走 `edge_health` |
| `snapshot` | 不存在;仅用 `host_metrics` |
| `buffered_observability` | 不存在;仅用 `buffered` |
### 3.2 `profile` — 主机概况(低频)
对应关系库 `of_node_system_profiles`(或现有等价表),**不进 ClickHouse 明细湖**。
```json
{
"hostname": "edge-1",
"os_name": "linux",
"os_version": "...",
"kernel_version": "...",
"architecture": "amd64",
"cpu_model": "...",
"cpu_cores": 8,
"total_memory_bytes": 16106127360,
"total_disk_bytes": 107374182400,
"uptime_seconds": 864000,
"reported_at_unix": 1720000000
}
```
| 字段 | 语义 |
| --- | --- |
| 硬件/OS 描述字段 | 事实读数 |
| `reported_at_unix` | Agent 采集时刻(UTC 秒) |
### 3.3 `host_metrics` — 宿主机资源(L3)
**全部为读数,不做 24h 业务总量。**
网卡/磁盘字节为 **内核累计计数器原值**(单调递增,重启可归零);CPU 为瞬时百分比;内存/磁盘占用为当前用量。
```json
{
"captured_at_unix": 1720000000,
"cpu_usage_percent": 12.5,
"memory_used_bytes": 4294967296,
"memory_total_bytes": 16106127360,
"storage_used_bytes": 50000000000,
"storage_total_bytes": 107374182400,
"disk_read_bytes": 9000000000,
"disk_write_bytes": 12000000000,
"network_rx_bytes": 500000000000,
"network_tx_bytes": 800000000000
}
```
| 字段 | 类型 | 语义 | Server 如何用 |
| --- | --- | --- | --- |
| `captured_at_unix` | int64 | 采样时刻 | `captured_at` |
| `cpu_usage_percent` | float | 瞬时 CPU% | 直接存;趋势取平均 |
| `memory_*` / `storage_*` | int64 | 当前用量/总量 | 直接存;算占用率 |
| `disk_read_bytes` / `disk_write_bytes` | int64 | **累计** IO 字节 | 存原值;查询时相邻差分 |
| `network_rx_bytes` / `network_tx_bytes` | int64 | **累计** 网卡字节 | 存原值;查询时相邻差分 →「宿主机网卡入/出站」 |
> Agent **禁止** 在上报前对网卡/磁盘做「本周期增量」替换累计值(否则 Server 差分会错)。
### 3.4 `edge_health` — OpenResty 边缘健康(L2)
**仅瞬时态,不包含业务吞吐。**
```json
{
"captured_at_unix": 1720000000,
"status": "healthy",
"message": "",
"connections": 42
}
```
| 字段 | 类型 | 语义 |
| --- | --- | --- |
| `status` | string | `healthy` / `unhealthy` / `unknown`(须与顶层 `openresty_status` 一致) |
| `message` | string | 状态说明(上报可带;**仅用于回填 PG 最新态,不进 CH**) |
| `connections` | int64 | stub_status Active connections |
#### 健康状态权威源(收敛)
| 数据 | 权威存储 | 说明 |
| --- | --- | --- |
| **当前** OpenResty 是否健康 + 说明文案 | **PG 节点表** `openresty_status` / `openresty_message` | UI 徽章、列表、告警以这里为准 |
| **时序** 健康 status + 连接数 | **CH** `of_node_edge_health`(`status`, `connections`) | 连接曲线 / 健康状态历史;**无 message 列** |
| Agent 上报 | 顶层 status/message + `edge_health` | Server 归一化后二者 status 对齐;message **只写 PG** |
因此:查「现在是否 unhealthy」→ 读 PG;查「过去 24h 连接数」→ 读 CH。
### 3.5 `access_logs[]` — 访问明细(L1,业务唯一事实)
Agent:tail access.log → 解析 JSON 行 → 原样字段上报(可截断 path)。
```json
{
"logged_at_unix": 1720000001,
"remote_addr": "203.0.113.10",
"host": "www.example.com",
"path": "/api/v1/ping",
"status_code": 200,
"bytes_sent": 1024,
"request_length": 128,
"request_time_ms": 15,
"user_agent": "Mozilla/5.0 ...",
"cache_status": "HIT"
}
```
| 字段 | 类型 | 必填 | 来源(OpenResty) | 业务含义 |
| --- | --- | --- | --- | --- |
| `logged_at_unix` | int64 | ✅ | `$time_iso8601` 解析 | 请求完成时间 |
| `remote_addr` | string | ✅ | `$remote_addr` | 客户端 IP → UV |
| `host` | string | ✅ | `$host` | 域名 → Zone 归属 |
| `path` | string | ✅ | `$request_uri`,Agent 可截断 | 路径 |
| `status_code` | int | ✅ | `$status` | 状态码 |
| `bytes_sent` | int64 | ✅ | **`$body_bytes_sent`** | **已提供数据**(响应体) |
| `request_length` | int64 | 建议 | `$request_length` | **接收数据** |
| `request_time_ms` | int64 | 可选 | `$request_time * 1000` | 耗时;缺省 0 |
| `user_agent` | string | 建议 | `$http_user_agent` | UA;可截断入库 |
| `cache_status` | string | 建议 | **`$upstream_cache_status`** | 边缘缓存结果(见 §3.5.1) |
**明确不由 Agent 上报(由 Server 写入):**
* `region` / 国家:入库时 GeoIP 解析
* `id` / `created_at`:Server 生成
* `node_id`:取自 payload / 鉴权上下文
**明确不上报:**
* `upstream_addr` / 回源地址 / `origin_fetched`:不做回源端点追踪;「是否回源」仅由 `cache_status` 在控制面推导(§3.5.1)
### 3.5.1 `cache_status` — 缓存命中与回源(明细优先)
**目标(第一期):** 访问日志明细/详情能展示「是否命中缓存 / 是否回源 / 未使用缓存」。
**口径:** 只存 OpenResty `$upstream_cache_status` 原始值;**不上报** upstream 地址。
#### 原始值(入库)
| 值 | 含义(OpenResty) |
| --- | --- |
| `HIT` | 命中缓存 |
| `MISS` | 未命中,向 upstream 取内容 |
| `BYPASS` | 跳过缓存(如 method/cookie/策略导致 `$openflare_skip_cache`) |
| `EXPIRED` | 缓存过期后回源 |
| `STALE` | 提供陈旧缓存(stale) |
| `UPDATING` | 后台更新中,可能返回旧缓存 |
| `REVALIDATED` | 协商验证后仍用缓存 |
| `-` 或空 | 未经过 `proxy_cache`(如 Pages 本地静态、非代理 location) |
#### UI 三态推导(不落库)
控制面展示用派生枚举 `cache_outcome`,**不写 CH**:
| 三态 | 条件(`cache_status`) | 列表标签建议 |
| --- | --- | --- |
| **命中缓存** | `HIT` / `STALE` / `REVALIDATED` / `UPDATING` | 命中 |
| **回源** | `MISS` / `EXPIRED` | 回源 |
| **未使用缓存** | `BYPASS` / `-` / `""` | 未缓存 |
详情可同时显示三态 + 原始 `cache_status`。
#### 边界
* Pages 静态 / 无 `proxy_cache` 的 location:多为空或 `-` → **未使用缓存**,不得标成「命中」。
* 第一期只做明细可见;命中率看板、hourly 维度可后续用同一列聚合。
**单次心跳条数建议:**
* 软上限例如 2000 条/拍;超出进入 `buffered` 下一批,**禁止** 在 Agent 压成 TrafficReport。
### 3.6 `buffered[]` — 离线补传(只装事实)
```json
{
"captured_at_unix": 1719999900,
"host_metrics": { },
"edge_health": { },
"access_logs": [ ]
}
```
| 字段 | 说明 |
| --- | --- |
| `captured_at_unix` | 该批次采集/缓冲时刻,用于 ack 与去重窗口 |
| `host_metrics` / `edge_health` / `access_logs` | 与主 payload 同结构;可省略空块 |
**禁止** 在 buffered 中携带 `traffic_report` 或 rx/tx 吞吐。
### 3.7 `health_events[]`
```json
{
"event_type": "openresty_unhealthy",
"severity": "critical",
"message": "...",
"triggered_at_unix": 1720000000,
"metadata": { }
}
```
写入关系库健康事件表(现有模型即可),不进访问日志湖。
### 3.8 Go 协议草图(目标)
```go
// pkg/protocol/agent.go(目标形态,实现时替换旧类型)
type NodePayload struct {
SchemaVersion int `json:"schema_version,omitempty"`
NodeID string `json:"node_id"`
Name string `json:"name"`
IP string `json:"ip"`
Version string `json:"version"`
ExtVersion string `json:"ext_version"`
CurrentVersion string `json:"current_version"`
LastError string `json:"last_error"`
OpenrestyStatus string `json:"openresty_status"` // PG 最新态权威
OpenrestyMessage string `json:"openresty_message"` // PG 最新态权威;不进 CH
Profile *NodeSystemProfile `json:"profile,omitempty"`
HostMetrics *NodeHostMetrics `json:"host_metrics,omitempty"`
EdgeHealth *NodeEdgeHealth `json:"edge_health,omitempty"`
AccessLogs []NodeAccessLog `json:"access_logs,omitempty"`
Buffered []BufferedFacts `json:"buffered,omitempty"`
HealthEvents []NodeHealthEvent `json:"health_events"`
WAFIPGroupChecksums map[string]string `json:"waf_ip_group_checksums,omitempty"`
}
type NodeHostMetrics struct {
CapturedAtUnix int64 `json:"captured_at_unix"`
CPUUsagePercent float64 `json:"cpu_usage_percent"`
MemoryUsedBytes int64 `json:"memory_used_bytes"`
MemoryTotalBytes int64 `json:"memory_total_bytes"`
StorageUsedBytes int64 `json:"storage_used_bytes"`
StorageTotalBytes int64 `json:"storage_total_bytes"`
DiskReadBytes int64 `json:"disk_read_bytes"`
DiskWriteBytes int64 `json:"disk_write_bytes"`
NetworkRxBytes int64 `json:"network_rx_bytes"`
NetworkTxBytes int64 `json:"network_tx_bytes"`
}
type NodeEdgeHealth struct {
CapturedAtUnix int64 `json:"captured_at_unix"`
Status string `json:"status"`
Message string `json:"message"`
Connections int64 `json:"connections"`
}
type NodeAccessLog struct {
LoggedAtUnix int64 `json:"logged_at_unix"`
RemoteAddr string `json:"remote_addr"`
Host string `json:"host"`
Path string `json:"path"`
UserAgent string `json:"user_agent,omitempty"`
CacheStatus string `json:"cache_status,omitempty"` // $upstream_cache_status
StatusCode int `json:"status_code"`
BytesSent int64 `json:"bytes_sent"` // body_bytes_sent,已提供数据
RequestLength int64 `json:"request_length"` // 接收数据
RequestTimeMs int64 `json:"request_time_ms"` // 可选
}
type BufferedFacts struct {
CapturedAtUnix int64 `json:"captured_at_unix"`
HostMetrics *NodeHostMetrics `json:"host_metrics,omitempty"`
EdgeHealth *NodeEdgeHealth `json:"edge_health,omitempty"`
AccessLogs []NodeAccessLog `json:"access_logs,omitempty"`
}
```
---
## 4. Server 解析与落库流程
### 4.1 入口
* HTTP:`POST /api/v1/agent/...` 心跳(现有路径)
* WebSocket:`type=status` payload = `NodePayload`
* 鉴权:`X-Agent-Token` → 绑定 `node_id`(payload.node_id 必须与 token 节点一致)
### 4.2 处理流水线(单次 payload)
```text
1. 反序列化 NodePayload
2. 归一化(normalize)
- schema_version < 2:
host_metrics ← snapshot
edge_health.status ← openresty_status
edge_health.connections ← openresty_observation.connections(若有)
traffic_report → drop
openresty_observation.rx/tx → drop
buffered ← buffered_observability
- path 再截断、status 范围钳制、负数字节 → 0
3. 关系库事务(节点最新态)
- 更新 node 在线时间、IP、版本、edge_health.status/message
- upsert profile(若有)
- insert health_events(若有)
4. ClickHouse 异步 batch(失败记日志,不阻断心跳响应的配置下发)
a. access_logs + buffered[].access_logs
→ 补 region(GeoIP)
→ 分配 snowflake id
→ BatchInsert of_node_access_logs
b. host_metrics + buffered[].host_metrics
→ of_node_metric_snapshots
c. edge_health + buffered[].edge_health
→ of_node_edge_health(仅 connections + status 快照可选)
5. 返回心跳响应(settings / active_config / waf 差分)
6. 若使用 buffer ack:按 buffered.captured_at_unix 列表确认
```
### 4.3 归一化规则(硬约束)
| 规则 | 行为 |
| --- | --- |
| `logged_at` 超前 now+5m | 钳制为 now 或丢弃该条(实现选定一种并单测) |
| `logged_at` 早于 now−TTL | 仍可写入,依赖表 TTL 清理 |
| 空 `host` | 允许,聚合进「未归属」 |
| `bytes_sent` / `request_length` < 0 | 置 0 |
| 单批 access_logs > N | 截断并打点监控(或只入 buffer 队列),不改为预聚合 |
| 重复补传 | CH 允许少量重复行;查询用 sum 近似(不强制精确去重) |
### 4.4 字段映射表(上报 → 表)
| 上报路径 | 目标存储 | 列 |
| --- | --- | --- |
| `access_logs[]` | CH `of_node_access_logs` | 见 §5.1 |
| `host_metrics` | CH `of_node_metric_snapshots` | 见 §5.2 |
| `edge_health` | CH `of_node_edge_health` + PG node 最新状态 | 见 §5.3 / §5.6 |
| `profile` | PG `of_node_system_profiles` | 现有列 |
| `health_events` | PG 健康事件表 | 现有模型 |
| `waf_ip_group_checksums` | 不落观测表 | 同步逻辑 |
| `traffic_report`(旧) | **不写** | — |
| `openresty_rx/tx`(旧) | **不写** | — |
### 4.5 查询侧(不落新「业务出站」列)
| 产品指标 | SQL 语义(示意) |
| --- | --- |
| 已提供数据 | `sum(bytes_sent)` |
| 接收数据 | `sum(request_length)` |
| 请求数 | `count()` |
| UV | `uniqExact(remote_addr)` |
| 5xx | `countIf(status_code >= 500)` |
| 按域名/状态码/地区 | `GROUP BY host / status_code / region` |
| 宿主机网卡出站 | 对 `network_tx_bytes` 按 node 时间序非负差分后 sum |
| OpenResty 连接 | `of_node_edge_health.connections` 最新或平均 |
---
## 5. 表结构(目标 DDL)
> 引擎与 TTL 与现网一致倾向:访问日志 90 天,指标 30 天。
> `id` 使用控制面 Snowflake/唯一 UInt64。
### 5.1 L1 事实表:`of_node_access_logs`
```sql
CREATE TABLE IF NOT EXISTS of_node_access_logs
(
id UInt64,
node_id String,
logged_at DateTime64(3, 'UTC'),
remote_addr String,
region String, -- Server GeoIP 写入,Agent 不传
host String,
path String,
user_agent String DEFAULT '', -- $http_user_agent
cache_status String DEFAULT '', -- $upstream_cache_status
status_code Int32,
bytes_sent UInt64, -- 已提供数据(body)
request_length UInt64 DEFAULT 0, -- 接收数据
request_time_ms UInt32 DEFAULT 0, -- 可选
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(logged_at)
ORDER BY (node_id, logged_at, host, status_code, remote_addr)
TTL toDateTime(logged_at) + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;
```
| 列 | 类型 | 来源 |
| --- | --- | --- |
| `id` | UInt64 | Server |
| `node_id` | String | 鉴权/payload |
| `logged_at` | DateTime64(3) | `logged_at_unix` |
| `remote_addr` | String | 上报 |
| `region` | String | Server GeoIP |
| `host` | String | 上报 |
| `path` | String | 上报 |
| `user_agent` | String | 上报(可空) |
| `cache_status` | String | 上报(可空)→ **缓存状态** |
| `status_code` | Int32 | 上报 |
| `bytes_sent` | UInt64 | 上报 → **已提供数据** |
| `request_length` | UInt64 | 上报 → **接收数据** |
| `request_time_ms` | UInt32 | 上报可选 |
| `created_at` | DateTime64(3) | Server now |
**迁移:** 现表已有 `bytes_sent` / `request_length` / `request_time_ms` / `user_agent`;缓存状态新增:
```sql
ALTER TABLE of_node_access_logs
ADD COLUMN IF NOT EXISTS cache_status String DEFAULT '';
```
### 5.2 L1 小时汇总(Server 侧 MV)
**禁止 Agent 写入。** 供看板/节点 24h 快速查询请求数、错误数、字节量。
**已实现选型:`SummingMergeTree` + 不含 UV 列。**
```sql
CREATE TABLE IF NOT EXISTS of_access_log_hourly
(
node_id String,
hour DateTime('UTC'),
host String,
request_count UInt64,
error_count UInt64,
bytes_sent UInt64,
request_length UInt64
)
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (node_id, hour, host)
TTL hour + INTERVAL 90 DAY;
CREATE MATERIALIZED VIEW IF NOT EXISTS of_access_log_hourly_mv
TO of_access_log_hourly
AS
SELECT
node_id,
toStartOfHour(logged_at) AS hour,
host,
toUInt64(count()) AS request_count,
toUInt64(countIf(status_code >= 500)) AS error_count,
sum(bytes_sent) AS bytes_sent,
sum(request_length) AS request_length
FROM of_node_access_logs
GROUP BY node_id, hour, host;
```
历史小时(MV 创建前已入库的明细)需一次性回填,见迁移 `202607180003_backfill_access_log_hourly.sql`(ANTI JOIN 防重)。
#### UV 策略(必须遵守)
| 场景 | 数据源 | 算法 | 说明 |
| --- | --- | --- | --- |
| **窗口总 UV**(看板汇总、节点卡片、Zone 汇总) | `of_node_access_logs` 明细 | `uniqExact(remote_addr)`(`TrafficSummary` / 节点聚合) | **唯一权威**;不可用小时 UV 相加 |
| **24h 趋势折线请求/错误/字节** | `of_access_log_hourly` 优先,缺数据回落明细桶 | `sum(request_count)` 等 | 小时路径 **不填** `unique_visitor_count`(恒为 0) |
| **24h 趋势折线分时 UV** | 仅明细桶路径 | 桶内 `uniqExact` | 走 hourly 时 UI 应展示空/0 或隐藏 UV 序列,**禁止**对小时行做 `sum(UV)` |
**为何 hourly 不存 UV:**
1. `SummingMergeTree` 只能安全合并可加和计数;`uniqExact` 跨 part 合并需要 `AggregatingMergeTree` + state,实现与查询更重。
2. 即便存每小时 UV,对多小时窗口 **相加会严重高估**(同一 IP 跨小时重复计)。
3. 产品「24h 独立访客」只认整窗 `uniqExact`;趋势图主序列是请求量/错误/字节,分时 UV 非主指标。
可选未来:若需要分时 UV 曲线,再单独加 `AggregatingMergeTree` 状态表或查询时对明细做 `uniqExact` 按小时 group(成本更高,不阻塞当前看板)。
### 5.3 L3 事实表:`of_node_metric_snapshots`(保留,语义明确)
```sql
CREATE TABLE IF NOT EXISTS of_node_metric_snapshots
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
cpu_usage_percent Float64,
memory_used_bytes Int64,
memory_total_bytes Int64,
storage_used_bytes Int64,
storage_total_bytes Int64,
disk_read_bytes Int64, -- 累计原值
disk_write_bytes Int64,
network_rx_bytes Int64, -- 累计原值 → 宿主机网卡入站
network_tx_bytes Int64, -- 累计原值 → 宿主机网卡出站
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
TTL toDateTime(captured_at) + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
```
列与现网一致;**文档与 API 必须标注 network_* 为宿主机网卡累计值**。
### 5.4 L3 小时汇总:`of_node_metric_capacity_hourly`(保留)
现有 min/max 用于累计计数器小时增量近似 + CPU/内存平均。逻辑不变:
* `network_tx_max - network_tx_min` ≈ 该小时宿主机出站
* **不得** 用于「已提供数据」
### 5.5 L2 事实表:`of_node_edge_health`(新建,替换吞吐型 openresty 表)
```sql
CREATE TABLE IF NOT EXISTS of_node_edge_health
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
status LowCardinality(String), -- healthy / unhealthy / unknown
connections Int64,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
TTL toDateTime(captured_at) + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
```
| 列 | 说明 |
| --- | --- |
| `status` | 瞬时健康(与 PG 当前态同源;用于时序,非唯一 UI 权威) |
| `connections` | 当前连接数 |
**无** `message` 列(说明文案仅 PG 最新态)。
**无** `openresty_rx_bytes` / `openresty_tx_bytes`。
### 5.6 关系库(节点最新态,非分析湖)
与观测湖分离,保持「最新一份」:
| 表(逻辑名) | 用途 | 关键列 |
| --- | --- | --- |
| `of_nodes`(或现节点表) | 在线、版本、IP | `last_seen_at`, `openresty_status`, `openresty_message`, `agent_version` |
| `of_node_system_profiles` | profile upsert | hostname, cpu_cores, total_memory_bytes, ... |
| 健康事件表 | `health_events` | event_type, severity, message, triggered_at |
> 具体物理表名以仓库现有 GORM 模型为准;本设计不强制改名,只强制 **不再把业务吞吐写进节点表**。
### 5.7 废弃表(停止写入 → TTL 后删除)
| 表 | 原因 | 替代 |
| --- | --- | --- |
| `of_node_request_reports` | Agent 预聚合 | `of_node_access_logs` + hourly |
| `of_node_traffic_hourly` + MV | 依赖 request_reports | `of_access_log_hourly` |
| `of_node_obs_openresty` | 含业务 rx/tx | `of_node_edge_health` |
| `of_node_openresty_hourly` + MV | 业务吞吐差分 | `of_access_log_hourly` 的 bytes_* |
Relay 专用 `of_node_obs_frps` / `of_node_obs_frpc` **保留**(非本 Agent 主路径,但同属 CH 观测)。
---
## 6. 表与协议对照总表
| 产品概念 | 协议字段 | 表.列 | 聚合 |
| --- | --- | --- | --- |
| 已提供数据 | `access_logs[].bytes_sent` | `of_node_access_logs.bytes_sent` | `sum` |
| 接收数据 | `access_logs[].request_length` | `...request_length` | `sum` |
| 请求数 | 行数 | — | `count` |
| UV(窗口总) | `remote_addr` | 同左明细 | `uniqExact`(**禁止** sum 小时 UV) |
| Top 域名 | `host` | 同左 | `group by` |
| 状态码分布 | `status_code` | 同左 | `group by` |
| 来源地区 | — | `region`(Server) | `group by` |
| 宿主机网卡出站 | `host_metrics.network_tx_bytes` | `of_node_metric_snapshots.network_tx_bytes` | 时间序差分 |
| 宿主机网卡入站 | `network_rx_bytes` | 同左 | 差分 |
| 磁盘读/写 | `disk_*_bytes` | 同左 | 差分 |
| CPU/内存 | 瞬时字段 | 同左 | avg |
| OpenResty 连接 | `edge_health.connections` | `of_node_edge_health.connections` | 最新/avg |
| OpenResty 健康 | `edge_health.status` | 节点表 + 可选 CH | 最新 |
**不再存在的映射:**
| 旧概念 | 旧字段 | 处置 |
| --- | --- | --- |
| OpenResty 出站 | `openresty_tx_bytes` | 删除;用已提供数据 |
| OpenResty 入站 | `openresty_rx_bytes` | 删除;用接收数据 |
| 窗口请求报告 | `traffic_report` | 删除 |
---
## 7. OpenResty 日志格式(与明细对齐)
目标 `log_format`(保证 `bytes_sent` 键 = body;含 UA 与缓存状态):
```nginx
log_format openflare_json escape=json
'{"ts":"$time_iso8601","host":"$host","path":"$request_uri",'
'"remote_addr":"$remote_addr","status":$status,'
'"request_time":$request_time,'
'"bytes_sent":$body_bytes_sent,"request_length":$request_length,'
'"user_agent":"$http_user_agent",'
'"cache_status":"$upstream_cache_status"}';
```
Agent 解析:
* `ts` → `logged_at_unix`
* `bytes_sent` → 协议 `bytes_sent`(已提供)
* `request_length` → 协议 `request_length`
* `request_time` → 可选 `request_time_ms = round(sec * 1000)`
* `user_agent` → 协议 `user_agent`
* `cache_status` → 协议 `cache_status`(原样透传,不做三态压缩)
---
## 8. 升级策略(无兼容层)
| 项 | 策略 |
| --- | --- |
| Agent 升级 | **销毁重建**优先;允许**二进制替换** |
| 协议 | 仅 schema v2 字段;旧 JSON 字段不解析 |
| 本地观测缓冲 | 若仍是旧格式(含 `snapshot` / `openresty_observation` / `traffic_report`)或损坏 → **整文件删除**,运行中重建 |
| 读路径 | 业务 API **只读** access_logs(及 hourly);健康当前态读 PG;连接时序读 CH edge_health |
| 旧 Agent | 必须升级;控制面不提供 v1 双读路径 |
---
## 9. 示例:一次心跳的落库结果
**Agent 上报(节选):**
```json
{
"schema_version": 2,
"node_id": "n1",
"host_metrics": {
"captured_at_unix": 1720000000,
"cpu_usage_percent": 10,
"memory_used_bytes": 1,
"memory_total_bytes": 2,
"storage_used_bytes": 3,
"storage_total_bytes": 4,
"disk_read_bytes": 100,
"disk_write_bytes": 200,
"network_rx_bytes": 1000,
"network_tx_bytes": 2000
},
"edge_health": {
"captured_at_unix": 1720000000,
"status": "healthy",
"message": "",
"connections": 5
},
"access_logs": [
{
"logged_at_unix": 1720000001,
"remote_addr": "1.1.1.1",
"host": "a.example.com",
"path": "/",
"status_code": 200,
"bytes_sent": 500,
"request_length": 80
}
]
}
```
**写入:**
1. PG 节点最新态:`openresty_status` / `openresty_message`(若上报)
2. `of_node_metric_snapshots` 1 行(network_tx=2000 累计)
3. `of_node_edge_health` 1 行(status + connections=5;**无 message**)
4. `of_node_access_logs` 1 行(bytes_sent=500, request_length=80, region=Server 填充)
5. MV 异步计入 `of_access_log_hourly`
**查询 24h 已提供数据:** `sum(bytes_sent)` → 至少 500(加历史)
**查询宿主机出站:** 对 snapshots 差分,与 500 **无强制相等关系**。
---
## 10. 实现检查清单
- [x] `pkg/protocol`:仅 v2 字段,无兼容别名
- [x] Agent:只组 `host_metrics` / `edge_health` / `access_logs` / `buffered`
- [x] Server:无 request_reports / openresty 吞吐;健康当前态 PG、时序 CH
- [x] CH migration:`request_length`、`request_time_ms`、`of_node_edge_health`、`of_access_log_hourly`、hourly 回填
- [x] 看板/Zone API 统一读 access log 聚合
- [x] UV:整窗 uniqExact;Zone 曲线标明分桶 UV;小时趋势不绘 UV
---
## 11. 修订记录
| 日期 | 说明 |
| --- | --- |
| 2026-07-17 | 初稿:协议 v2、Server 落库流水线、CH/关系库目标表结构与废弃表清单 |
-585
View File
@@ -1,585 +0,0 @@
# 边缘可观测与业务流量统计重构设计
你会学到:当前观测链路为何出现「看板 OpenResty 出站」与「Zone 已提供数据」不一致、字段与聚合为何冗余,以及目标架构如何让 **Agent 只上报事实、Server 只解释事实**,业务流量以访问日志为唯一真相源。
---
## 1. 目标
### 1.1 要解决的问题
1. **双真相源**:业务吞吐同时来自访问日志聚合与 OpenResty 观测差分,数值长期对不上。
2. **Agent 越权计算**:边缘预聚合 `TrafficReport`、吞吐累计,控制面再聚合一遍,语义难演进、难对账。
3. **字段语义重叠**:「OpenResty 出站」与「已提供数据」对用户是同一业务问题,系统却用两套字段、两条管道。
4. **瞬时与累计混用**:60 秒窗口计数被当成进程累计做 24h 差分,造成严重偏低。
5. **UI 诱导错误对比**:看板与 Zone 页使用相近「流量/数据」文案,却未声明范围与口径差异。
### 1.2 重构目标
| 目标 | 说明 |
| --- | --- |
| **单一业务真相** | 请求数、已提供数据、UV、状态码分布、Top 域名等 **只** 从访问日志(及其 Server 侧派生汇总)得出 |
| **Agent 只上报事实** | 明细日志 + 机器读数 + 健康瞬时态;**禁止** 业务 UV/TopN/24h 总量等预聚合 |
| **字段收敛** | 一个业务概念对应一个权威字段;机器网卡与业务交付严格分名 |
| **可对账** | 全局「已提供数据」≈ 各 Zone「已提供数据」之和(差仅为未绑定/未知 Host) |
| **可演进** | 改时间窗、TopN、归属规则只改 Server,不升 Agent |
### 1.3 非目标(本设计不覆盖)
* 建成通用日志平台、全量日志长期归档或检索产品。
* 替换 ClickHouse / 取消分析库依赖。
* 改造 Relay / OpenFlared 的主机指标采集(可对齐原则,但不在本轮协议主路径)。
* 实时流式告警引擎、APM 链路追踪(OpenTelemetry 服务端已有,与本业务流量模型正交)。
---
## 2. 范围与约束
### 2.1 产品约束(继承)
* 单租户、全局单激活配置;观测不引入多租户计费隔离。
* ClickHouse 为访问日志与时序观测的强制分析存储。
* Agent 无入向控制、Pull 模型;离线期间本地 OpenResty 继续服务,观测可本地缓冲后补传。
### 2.2 工程约束
* Agent 保持轻量:解析日志行、读 `/proc`、健康检查;不做业务分析。
* 控制面 API 错误仍走统一信封与 `response.Abort*`。
* 访问日志字段变更须同时更新 OpenResty `log_format` 与 Agent 解析器;Agent 与控制面同版本发布,不保留旧协议解析。
---
## 3. 设计原则
### 原则 P1:Agent 上报事实,Server 解释事实
```text
Agent = 采集 + 可靠投递(原始/近原始)
Server = 入库 + 聚合 + 归属 + 趋势 + 对账
```
**允许的边缘处理(采集)**
* 将 JSON access.log 行解析为结构化字段
* path 长度上限、丢弃非法行、跳过观测端口自身请求
* 读取网卡/CPU/内存等计数器 **原值**
* 批量、压缩、离线缓冲与重试
**禁止的边缘处理(业务计算)**
* UV / Top 域名 / 状态码直方图 / 窗口 request_count 作为权威指标
* 为看板单独维护「业务入出站累计」
* Zone / 域名归属统计、国家分布(国家可在 Server 入库时解析)
### 原则 P2:业务流量唯一真相 = 访问日志
| 业务问题 | 唯一答案 |
| --- | --- |
| 提供了多少数据 | `sum(bytes_sent)` |
| 多少请求 | `count()` |
| 多少独立访客 | `uniqExact(remote_addr)`(或产品约定哈希) |
| 状态码 / Top 域名 | 对日志 `group by` |
### 原则 P3:三层指标互不混用
| 层 | 名称 | 用途 | 典型字段 |
| --- | --- | --- | --- |
| L1 业务交付 | Business Traffic | 用户与 Zone 对账、看板业务趋势 | access log |
| L2 边缘健康 | Edge Health | OpenResty 是否活着、当前连接 | status、connections |
| L3 宿主机资源 | Host Capacity | 容量规划、机器是否打满 | CPU、内存、磁盘、**网卡** |
禁止将 L3 网卡或 L2 瞬时计数命名为「已提供数据」;禁止将 L1 与 L3 画在同一摘要卡片上却不标注语义。
### 原则 P4:一个业务概念一个字段
* **已提供数据** ≡ 响应体交付量 ≡ 历史文案中的「OpenResty 出站(业务含义)」→ **只保留 `bytes_sent` 聚合**
* **接收数据**(可选)≡ 请求侧体量 → 日志 `request_length` 聚合
* **宿主机出站** ≡ `network_tx` 差分,文案必须含「宿主机/网卡」
---
## 4. 现状问题(基线)
### 4.1 当前数据流(冗余)
```text
一次 HTTP 请求
│
├─ access.log 一行
│ → Agent tail → AccessLogs[]
│ → CH of_node_access_logs
│ → Zone「已提供数据」✅
│
├─ Lua shared dict 窗口/累计计数
│ → /openflare/observability
│ → TrafficReport + OpenrestyObservation(rx/tx)
│ → CH request_reports / obs_openresty
│ → 看板「OpenResty 入/出站」❌ 易与 Zone 不一致
│
├─ access.log 二次汇总(观测 endpoint 失败时回退)
│ → 又一份 TrafficReport / 吞吐
│
└─ 宿主机 network_rx/tx
→ Snapshot → 网络趋势中的「主机」曲线
```
### 4.2 字段重叠
| 用户感知 | 系统字段 A | 系统字段 B | 问题 |
| --- | --- | --- | --- |
| 出站 / 已提供 | `openresty_tx_bytes` | `bytes_sent` | 业务语义重复 |
| 入站 | `openresty_rx_bytes` | `request_length`(日志) | 业务语义重复 |
| 请求数 | `TrafficReport.request_count` | `count(access_logs)` | 聚合重复且窗口易重计 |
| 出站(机器) | `network_tx_bytes` | (无业务对应) | 应单独命名,勿与业务对账 |
### 4.3 典型故障模式
1. 窗口计数被当累计差分 → 24h 业务吞吐严重偏低。
2. 小时 rollup `max−min` 对重置型计数失效。
3. Zone 用日志、看板用观测 → 用户认为系统算错。
4. 改口径需同步改 Lua、Agent 状态累计、Server 差分、前端文案。
---
## 5. 目标架构
### 5.1 目标数据流
```mermaid
flowchart TB
subgraph edge [边缘节点]
OR[OpenResty]
LOG[access.log]
PROC[主机 /proc 与磁盘]
STUB[stub_status 连接数]
AG[Agent]
OR -->|log_format 写行| LOG
LOG -->|仅 tail 增量明细| AG
PROC -->|读数快照| AG
STUB -->|瞬时连接| AG
OR -->|健康探测| AG
end
subgraph server [控制面 Server]
HB[心跳 / WS 接收]
CH[(ClickHouse)]
AGG[聚合查询层]
API[管理端 API]
HB --> CH
CH --> AGG
AGG --> API
end
subgraph ui [管理端]
DASH[看板:全局业务趋势]
ZONE[Zone:按域名过滤]
NODE[节点:主机资源 + 健康]
end
AG -->|AccessLogs + HostSnapshot + Health| HB
API --> DASH
API --> ZONE
API --> NODE
```
### 5.2 职责矩阵
| 能力 | Agent | Server | 前端 |
| --- | --- | --- | --- |
| 写 access.log | OpenResty | — | — |
| 读并上报明细 | ✅ | 入库 | — |
| sum/count/uniq/TopN | ❌ | ✅ | 展示 |
| Zone 域名过滤 | ❌ | ✅ | 选择 Zone |
| 主机 CPU/内存/网卡 | 读原值上报 | 差分/平均 | 节点/看板资源区 |
| OpenResty 连接数 | 读瞬时上报 | 最近值 | 节点健康 |
| 业务 24h 入出站 | ❌ | 日志聚合 | 统一称「已提供/接收数据」 |
---
## 6. 指标与字段模型
### 6.1 权威字段表(目标)
#### L1 业务交付(来自访问日志)
| 概念 | 存储字段 | 聚合 | 展示名 |
| --- | --- | --- | --- |
| 请求时间 | `logged_at` | 时间窗过滤 | — |
| 节点 | `node_id` | group | — |
| 客户端 IP | `remote_addr` | `uniq` → UV | 唯一访问者 |
| Host | `host` | group / Zone 映射 | 域名 |
| 路径 | `path` | 可选 | — |
| 状态码 | `status_code` | group | 状态码分布 |
| **已提供数据** | **`bytes_sent`** | **`sum`** | **已提供数据** |
| **接收数据** | **`request_length`** | **`sum`** | **接收数据**(可选展示) |
| 地区 | `region`(Server 解析写入) | group | 来源地区 |
> 说明:OpenResty `log_format` 中 JSON 键名可继续叫 `bytes_sent`,值必须来自 **`$body_bytes_sent`**(与现网一致),表示响应体交付量,即「已提供数据」。
#### L2 边缘健康(瞬时,不做 24h 业务总量)
| 概念 | 字段 | 说明 |
| --- | --- | --- |
| OpenResty 健康 | `openresty_status` / message | 已有 |
| 当前连接 | `openresty_connections` | stub_status |
| (可选)近窗 QPS 粗估 | 仅节点详情「此刻」,**不得**作为 24h 总量权威 | 若实现须标明「瞬时」 |
#### L3 宿主机资源
| 概念 | 字段 | 展示名 |
| --- | --- | --- |
| CPU / 内存 / 磁盘占用 | `host_metrics` | 保持 |
| 网卡累计字节 | `network_rx_bytes` / `network_tx_bytes` | **宿主机网卡入/出站** |
| 磁盘 IO 累计 | `disk_read_bytes` / `disk_write_bytes` | 磁盘读/写 |
### 6.2 已删除字段(无兼容层)
| 原字段 | 处置 | 原因 |
| --- | --- | --- |
| `openresty_tx_bytes` / `openresty_rx_bytes` | **删除** | 业务字节以 access log 为准 |
| `TrafficReport` 及 TopN/窗内 UV | **删除** | 边缘预聚合 |
| Agent state 内业务 lifetime 累计 | 删除 | 违背 P1 |
| Lua shared dict 业务吞吐/窗口请求计数 | 删除 | 非投递主路径 |
### 6.3 命名对照(前端文案强制)
| 禁止混用文案 | 正确文案 | 数据来源 |
| --- | --- | --- |
| OpenResty 出站(指业务量) | **已提供数据** | `sum(bytes_sent)` |
| OpenResty 入站(指业务量) | **接收数据** | `sum(request_length)` |
| 网络出站(未说明) | **宿主机网卡出站** | `network_tx` 差分 |
| 已提供数据 vs 出站 两套卡片 | **只保留一套业务卡片** | 日志 |
---
## 7. Agent 设计
### 7.1 心跳载荷(目标协议)
保留并强化:
```text
NodePayload
identity / version / openresty_status / openresty_message # 最新态 → PG
profile # 主机概况(低频)
host_metrics # L3 资源读数(含网卡累计原值)
edge_health # L2:status + connections(CH 时序;message 不进 CH)
access_logs[] # L1 明细(主路径)
health_events[]
buffered[] # 缓冲的是上述事实,不是报表
waf_ip_group_checksums
```
协议中已删除(无兼容层):
```text
traffic_report
openresty_observation
snapshot / buffered_observability 别名
```
### 7.2 Access log 上报要求
每条明细至少包含:
| 字段 | 必填 | 备注 |
| --- | --- | --- |
| `logged_at_unix` | ✅ | 请求完成时间 |
| `remote_addr` | ✅ | UV |
| `host` | ✅ | Zone 映射 |
| `path` | ✅ | 可截断 |
| `status_code` | ✅ | |
| `bytes_sent` | ✅ | body 字节,已提供数据 |
| `request_length` | ✅ | 接收数据 |
Agent 职责:
1. 按 offset tail `access.log`(截断/轮转时重置 offset,**只上报文件中仍存在的新行**)。
2. 结构化解析后批量放入心跳 / WS。
3. 离线写入本地 buffer,连通后按窗口补传。
4. **不对明细做 sum/count/uniq。**
### 7.3 主机 Snapshot
* 继续上报网卡/磁盘 **累计计数器原值**(非业务预聚合)。
* Server 侧对累计值做相邻采样非负差分 → 宿主机趋势。
* 这与「已提供数据」无关,UI 必须分区展示。
### 7.4 OpenResty 本地观测
**收敛后建议:**
* 保留:健康检查、`stub_status` 当前连接。
* 删除主路径依赖:`log.lua` 中对 request/status/domain/rx/tx 的 shared dict 业务计数,以及 `/openflare/observability` 作为 TrafficReport 来源。
* 若短期内保留 endpoint 供调试,不得再写入 Server 权威分析表。
### 7.5 与 Agent 设计文档的关系
本设计强化 [Agent 与发布模型](./agent-design.md) 中的「纯粹数据落地」:
* 配置与证书:落地与上报应用状态。
* 观测:只搬运事实,不搬运业务结论。
---
## 8. Server 设计
### 8.1 入库
| 输入 | 表 | 说明 |
| --- | --- | --- |
| `access_logs[]` | `of_node_access_logs` | 权威业务明细 |
| `host_metrics` | `of_node_metric_snapshots` | L3;网卡/磁盘累计 |
| `openresty_status` / `openresty_message` | **PG 节点表** | L2 **最新态权威**(message 仅此) |
| `edge_health` | `of_node_edge_health` | L2 时序:status + connections(**无 message**) |
GeoIP:继续在 Server 入库路径解析 `remote_addr` → `region`,不在 Agent 做。
### 8.2 聚合层(统一)
所有业务趋势与 Zone 统计共用同一查询语义:
```text
过滤:logged_at ∈ [since, until]
可选:node_id / host IN (...)
指标:
request_count = count()
unique_visitors = uniqExact(remote_addr)
bytes_provided = sum(bytes_sent) -- 已提供数据
bytes_received = sum(request_length) -- 接收数据
按 hour/bucket 折叠 series
按 status_code / host / region 分布
```
实现位置:
* Zone:`GET .../zones/:id/stats`(已有,对齐字段命名)
* 看板:overview 的 traffic / 业务网络趋势 **改为调用同一聚合**(全局、无 host 过滤或 Top 过滤)
* 节点详情:业务量 = 该 `node_id` 过滤的同一聚合;主机网卡仍走 metric 差分
### 8.3 派生汇总(可选性能路径)
当明细查询在 24h 全量节点上过重时,允许 **Server 侧** 物化视图:
```text
of_access_log_hourly
(hour, node_id, host, request_count, bytes_sent, bytes_received, ...)
```
约束:
* 仅由 CH 从 `of_node_access_logs` 派生,**禁止** Agent 直接写该表。
* Zone / 看板优先读 rollup,缺口回退明细(与现有 metric hourly 策略类似)。
### 8.4 停用的分析路径
| 路径 | 迁移后 |
| --- | --- |
| `BuildNetworkTrendPoints` 对 openresty_rx/tx 差分 | 删除或仅保留 network_* 主机曲线 |
| `of_node_obs_openresty` 吞吐字段 | 停止写入;TTL 过期后删表或缩列 |
| `of_node_request_reports` + traffic hourly | 业务趋势不再依赖;可整表废弃 |
| Dashboard compact 中 openresty_tx 序列 | 改为 bytes_provided 序列 |
---
## 9. API 与前端
### 9.1 语义统一的响应字段
建议在业务统计 API 中统一使用:
```json
{
"request_count": 0,
"unique_visitors": 0,
"bytes_provided": 0,
"bytes_received": 0,
"series": [
{
"bucket_started_at": "...",
"request_count": 0,
"unique_visitors": 0,
"bytes_provided": 0,
"bytes_received": 0
}
]
}
```
API 业务字节字段使用 `bytes_provided` / `bytes_received`(访问日志聚合);不再返回 openresty 吞吐别名。
### 9.2 看板
* **业务区**:请求趋势、已提供数据、接收数据(可选)、状态码、Top 域名、来源地区 —— 全部 L1。
* **资源区**:CPU/内存、**宿主机网卡**、磁盘 IO —— 全部 L3。
* **禁止**:在业务区展示「OpenResty 入/出站」作为与 Zone 对账的指标。
「24 小时网络与磁盘趋势」建议拆分或改标题:
* 「24 小时业务流量」→ `bytes_provided` / `bytes_received` / 请求
* 「24 小时宿主机网络与磁盘」→ `network_*` / `disk_*`
### 9.3 Zone `/websites/:id`
* 保持「已提供的数据总计」等卡片。
* 数据与看板业务区 **同一聚合函数**,仅 `hosts = zone 域名列表`。
* 文档与 UI 可注明:全局看板含全部 Host;本页仅本 Zone。
### 9.4 节点详情
* 业务吞吐:该节点 `sum(bytes_sent)` 等。
* OpenResty:健康 + 当前连接。
* 网卡:明确「宿主机」。
---
## 10. OpenResty 与日志格式
### 10.1 保持
现有 JSON `log_format` 核心字段:
```text
ts, host, path, remote_addr, status, request_time,
bytes_sent (= $body_bytes_sent), request_length
```
### 10.2 变更
* 不再依赖 log phase 写入业务 shared dict 计数作为控制面输入。
* 观测端口请求继续不写业务统计(或 access_log off)。
### 10.3 Agent 解析
* 协议 `NodeAccessLog` 增加 `request_length`。
* 旧日志行缺字段时按 0,不阻断整批。
---
## 11. 升级与迁移(无兼容层)
### 11.1 阶段回顾(已落地)
| 阶段 | 内容 |
| --- | --- |
| **M1–M5** | 读路径切 access log;协议 v2;停预聚合;edge_health + access_log_hourly;删旧表与 API 兼容字段 |
### 11.2 升级策略
* **Agent:销毁重建优先**;允许二进制替换。
* 二进制替换时:本地旧观测缓冲(含 `snapshot` / `openresty_observation` / `traffic_report`)**整文件删除**,运行后重建。
* Server **不**解析 v1 字段,**不**双读 request_reports / openresty 吞吐。
* 明细缺失时段:业务图为空或仅部分;**不得**用网卡或已删除的 openresty 吞吐冒充已提供数据。
### 11.3 数据回填
* 历史「已提供数据」以 access log 为准。
* `of_access_log_hourly` 创建前历史用 goose 回填 SQL(ANTI JOIN 防重)。
### 11.4 健康状态权威
* **当前态**:PG `openresty_status` / `openresty_message`。
* **时序**:CH `of_node_edge_health`(status + connections;无 message)。
### 11.5 UV
* **整窗独立访客**:`uniqExact(remote_addr)`(看板合计、Zone 合计)。
* **分桶 UV**(Zone 曲线):桶内 uniq,**不可跨桶相加**;UI 须标明。
* **小时趋势路径**:不绘 / 不填分时 UV(hourly 表不含 UV)。
---
## 12. 存储与容量
* 业务趋势依赖明细或 hourly rollup,需关注 `of_node_access_logs` TTL 与采样。
* 若明细量过大:优先 **Server 侧 rollup**,而不是恢复 Agent 预聚合。
* 可对 path 高基数场景限制明细 path 长度(已有),聚合默认不按完整 path 做全局 Top。
---
## 13. 验证标准
### 13.1 对账
在仅有单一 Zone 产生流量的环境:
```text
看板「已提供数据」(24h) ≈ Zone「已提供的数据总计」(24h)
误差仅来自时间窗对齐(整点截断)与未计入 Host
```
多 Zone 时:
```text
sum(各 Zone 已提供) + sum(未归属 Host) = 全局已提供
```
### 13.2 回归
* Agent 单测:只解析与 offset,不出现业务 sum 断言为「上报契约」。
* Server:Zone stats 与 dashboard business traffic 共用聚合测例。
* 前端:文案快照/测试中不再出现业务含义的「OpenResty 出站」与「已提供数据」双卡片。
### 13.3 性能
* 24h 看板聚合 P95 可接受(必要时 hourly MV)。
* 心跳 payload 体积:明细批量有上限;超限拆缓冲,不在 Agent 做摘要替代。
---
## 14. 风险与权衡
| 风险 | 缓解 |
| --- | --- |
| 明细量大导致 CH 与心跳变重 | 批量、压缩、采样策略评估;Server rollup;限制单次条数 |
| 短暂丢失日志导致业务量偏低 | 本地 buffer 与轮转处理;监控 access log 采集滞后 |
| 用户仍对比「网卡出站」与「已提供」 | UI 分区与文案强制「宿主机」前缀 |
| 旧 Agent 长期在线 | **无兼容层**;必须升级/重建 Agent |
**为何不保留 Agent 预聚合作为优化?**
* 省带宽的代价是再次分裂真相、口径漂移、本次问题重演。
* 优化应落在 Server 派生表与查询,而不是边缘业务计算。
---
## 15. 关键决策摘要
| 决策 | 选择 | 否决方案 |
| --- | --- | --- |
| 业务流量真相 | 访问日志 | OpenResty dict / TrafficReport |
| Agent 角色 | 只上报事实 | 边缘 UV/TopN/吞吐累计 |
| 「出站」与「已提供」 | 合并为已提供数据 | 双字段双管道长期并存 |
| 网卡流量 | 独立 L3,单独文案 | 与业务出站并列对账 |
| 性能 | CH rollup | Agent 预聚合 |
| 迁移 | 先切读路径再瘦身 Agent | 先删明细依赖预聚合 |
---
## 16. 文档与代码映射(落地时)
| 区域 | 主要路径 |
| --- | --- |
| 协议 | `pkg/protocol/agent.go` |
| Agent 采集 | `internal/apps/agent/observability/`、`heartbeat/` |
| OpenResty 日志与 Lua | `pkg/render/openresty/`、`internal/apps/agent/nginx/observability_assets.go` |
| Server 入库 | `internal/apps/openflare/agent/observability.go` |
| 日志聚合 | `internal/repository/analytics/node_access_log*.go`、`internal/apps/openflare/zone/stats.go` |
| 看板 | `internal/apps/openflare/dashboard/`、`internal/apps/openflare/observability/analytics.go` |
| 前端 | `frontend/app/(main)/page.tsx`、`components/dashboard/*`、`websites/.../zone-overview.tsx` |
实现计划见:`docs/plan/20260717-observability-redesign.md`。
**推荐阅读顺序:**
1. **[观测数据传输模型](./observability-transport-model.md)**(最新:传什么、从哪采、频率、示例 JSON)
2. [Agent 上报协议与观测落库数据模型](./observability-data-model.md)(协议字段与 DDL)
---
## 17. 修订记录
| 日期 | 说明 |
| --- | --- |
| 2026-07-17 | 初稿:针对双真相、Agent 预聚合、字段冗余给出目标架构与迁移阶段 |
| 2026-07-17 | 增补协议/表结构专章链接 `observability-data-model.md` |
@@ -1,503 +0,0 @@
# 边缘观测数据传输模型(现行目标版)
> **本文是「Agent ↔ Server 观测数据怎么传」的最新权威说明。**
> 读完应能回答:传什么、从哪采、多久采一次、Server 怎么存、产品指标从哪查。
> 协议字段与 DDL 细节另见 [观测上报协议与表结构](./observability-data-model.md);问题背景见 [边缘可观测与业务流量统计](./observability-design.md)。
---
## 0. 先记住三层(不要混)
| 层 | 回答的问题 | 唯一数据来源 | 产品例子 |
| --- | --- | --- | --- |
| **L1 业务交付** | 提供了多少数据?多少请求? | **access.log 明细** | 已提供数据、请求数、UV、状态码、Top 域名 |
| **L2 边缘健康** | OpenResty 活着吗?现在多少连接? | **本机 `/openflare/observability`** | 节点健康、当前连接 |
| **L3 宿主机资源** | CPU/内存/磁盘/网卡怎样? | **操作系统读数** | 容量趋势、宿主机网卡 |
**三层互不对账。**
「已提供数据」≠「当前连接」≠「宿主机网卡出站」。
---
## 1. 总览:谁采集、谁上报、谁聚合
```text
┌─────────────────────────────────────────────────────────────┐
│ 边缘节点 │
│ │
│ 访客请求 ──► OpenResty │
│ │ │
│ ├─ access.log(每请求一行) ←── L1 采集点 │
│ │ │
│ └─ 连接状态(进程内维护) │
│ │ │
│ ▼ │
│ GET /openflare/observability ←── L2 读快照 │
│ (不扫日志、不重算业务量) │
│ │
│ 操作系统 /proc 等 ────────────────────── L3 读快照 │
│ │
│ ┌────────── Agent ──────────┐ │
│ │ 默认每 3s 组一包 NodePayload │ │
│ │ · tail access.log 增量 │ │
│ │ · GET 本机 observability │ │
│ │ · 读 host_metrics │ │
│ └────────────┬──────────────┘ │
└─────────────────────────────│──────────────────────────────────┘
│ HTTP 心跳 或 WebSocket status
▼
┌─────────────────────────────────────────────────────────────┐
│ Server(控制面) │
│ · 明细 → ClickHouse of_node_access_logs │
│ · 健康 → 节点最新态 + of_node_edge_health │
│ · 主机 → of_node_metric_snapshots │
│ · 业务趋势 / Zone 统计 = 只对 access_logs 做 sum/count/uniq │
└─────────────────────────────────────────────────────────────┘
```
| 角色 | 做什么 | 不做什么 |
| --- | --- | --- |
| OpenResty | 写 access.log;维护连接数 | 不向控制面直接上报 |
| Agent | **采集事实并上报** | **不算** UV/TopN/24h 已提供数据 |
| Server | 入库 + **聚合解释** | 不信任边缘业务预汇总 |
---
## 2. 采集频率(默认)
| 动作 | 默认频率 | 配置 |
| --- | --- | --- |
| Agent → Server 上报 | **每 3 秒** 一次完整 payload | `heartbeat_interval` / 控制面 `agent_heartbeat_interval`(毫秒,默认 `3000`) |
| 组包时 tail access.log | **随上报**(两次上报之间的新行) | 同上 |
| 组包时 GET `/openflare/observability` | **随上报**(读**当前**连接快照) | 同上 |
| 组包时读主机指标 | **随上报** | 同上 |
| OpenResty 写 access.log | **每个请求结束时** 1 行 | 与心跳无关 |
| 连接数在进程内更新 | **连接变化时**(内核维护) | 与心跳无关 |
| 离线补传窗口 | 默认保留约 **60 分钟** | `observability_replay_minutes` |
| 节点离线判定 | 约 **60 秒** 无成功心跳 | `node_offline_threshold`(默认 `60000` 毫秒) |
**说明:**
- Agent **没有**单独的「采样时钟」;**采样点 = 上报点**(默认 3s)。
- access.log 是「请求级连续写入」;Agent 只是周期性 **搬运增量行**。
- `/openflare/observability` **不是**「被调用才开始统计业务」;对连接而言是 **读 Nginx 已有瞬时值**。
传输通道:
- **HTTP 心跳**:按间隔 POST 整包。
- **WebSocket**:连通后按同一间隔发 `status` 消息(内容同构);此时不再走 HTTP 心跳双发。
---
## 3. Agent → Server 数据包(NodePayload v2)
### 3.1 结构骨架
```json
{
"schema_version": 2,
"node_id": "n_01hxyz",
"name": "edge-shanghai-1",
"ip": "203.0.113.10",
"version": "3.4.0",
"ext_version": "",
"current_version": "20260718-abc",
"last_error": "",
"profile": { },
"host_metrics": { },
"edge_health": { },
"access_logs": [ ],
"buffered": [ ],
"health_events": [ ],
"waf_ip_group_checksums": { }
}
```
| 字段 | 层 | 含义 |
| --- | --- | --- |
| 身份/版本/last_error | 控制 | 节点是谁、跑什么版本 |
| `profile` | 低频概况 | 主机名、核数等(变化才报) |
| `access_logs` | **L1** | 访问明细增量 |
| `edge_health` | **L2** | OpenResty 健康 + 当前连接 |
| `host_metrics` | **L3** | CPU/内存/磁盘/网卡读数 |
| `buffered` | 补传 | 离线期间攒的事实批次 |
| `health_events` | 事件 | 如 openresty_unhealthy |
| `waf_ip_group_checksums` | 同步 | 非观测湖 |
**协议已删除(无兼容层,旧 Agent 必须升级):**
- `traffic_report`
- `openresty_observation`(含 rx/tx)
- `snapshot` / `buffered_observability`
- 业务含义的 openresty 吞吐字段
---
## 4. L1 业务:access_logs
### 4.1 采集从哪里来
| 步骤 | 位置 | 说明 |
| --- | --- | --- |
| 1 | OpenResty `log_format openflare_json` | 每请求写一行 JSON 到 `access_log_path` |
| 2 | Agent 按文件 offset **tail 增量** | 两次心跳之间的新行 |
| 3 | 解析后放入 `access_logs[]` | 可截断过长 path;**不做 sum/count** |
日志格式(OpenResty 变量):
```text
ts ← $time_iso8601
host ← $host
path ← $request_uri
remote_addr ← $remote_addr
status ← $status
request_time ← $request_time
bytes_sent ← $body_bytes_sent 【已提供数据 = 响应体字节】
request_length← $request_length 【接收数据】
user_agent ← $http_user_agent
cache_status ← $upstream_cache_status 【缓存状态;UI 可推导命中/回源/未缓存】
```
观测端口请求 **不写** 业务 access.log(独立 server `access_log off`)。
### 4.2 上报示例
```json
"access_logs": [
{
"logged_at_unix": 1721289601,
"remote_addr": "198.51.100.20",
"host": "www.example.com",
"path": "/api/v1/ping",
"status_code": 200,
"bytes_sent": 1024,
"request_length": 128,
"request_time_ms": 15,
"user_agent": "curl/8.0",
"cache_status": "MISS"
},
{
"logged_at_unix": 1721289602,
"remote_addr": "198.51.100.21",
"host": "www.example.com",
"path": "/index.html",
"status_code": 200,
"bytes_sent": 8192,
"request_length": 300,
"request_time_ms": 8,
"user_agent": "Mozilla/5.0",
"cache_status": "HIT"
}
]
```
| 字段 | 解释 |
| --- | --- |
| `bytes_sent` | **已提供数据**(单请求);全局/Zone 合计 = Server `sum` |
| `request_length` | **接收数据**(单请求) |
| `logged_at_unix` | 请求完成时间(业务时间轴) |
| `host` | 用于 Zone 域名过滤 |
| `cache_status` | `$upstream_cache_status` 原样;详情/列表可推导三态(命中/回源/未缓存);**不上报** upstream 地址 |
| 无 `region` | **Server 入库时** GeoIP 写入 |
### 4.3 Server 如何用(产品指标)
| 产品指标 | 算法(仅 L1) |
| --- | --- |
| 已提供数据 | `sum(bytes_sent)` |
| 接收数据 | `sum(request_length)` |
| 请求数 | `count()` |
| UV | `uniqExact(remote_addr)` |
| 状态码分布 | `group by status_code` |
| Top 域名 | `group by host` |
| Zone 页 | 同上 + `host IN (该 Zone 域名)` |
| 看板业务区 | 同上,全局或 Top 过滤 |
落库表:`of_node_access_logs`(可选 Server 侧 `of_access_log_hourly` 加速,**Agent 不写**)。
### 4.4 频率再强调
```text
请求发生 ──立即──► 写 access.log
Agent 每 3s ──搬运──► 这 3s 内新行(可能 0 行,也可能很多行)
Server ──立即/批量──► CH
```
业务量正确性 **不依赖** 3s 对齐;3s 只影响「明细到达控制面的延迟」和单包条数。
---
## 5. L2 健康:edge_health 与 `/openflare/observability`
### 5.1 本机监测口(合并后目标)
**只保留一个接口:**
```http
GET http://127.0.0.1:{openresty_observability_port}/openflare/observability
```
默认端口:**18081**(`openresty_observability_port`)。
**职责:** 回答「OpenResty 此刻怎样」,**不**回答业务已提供多少数据。
#### 返回示例(目标 JSON)
```json
{
"ok": true,
"captured_at_unix": 1721289600,
"connections": {
"active": 42,
"reading": 0,
"writing": 1,
"waiting": 41
}
}
```
| 字段 | 是否瞬时 | 从哪来 | 说明 |
| --- | --- | --- | --- |
| `ok` | 当次探测 | 能返回 200 即 true | 探活 |
| `captured_at_unix` | 采样时刻 | `ngx.time()` | 与上报对齐 |
| `connections.active` | **瞬时** | Nginx 连接状态(原 stub_status Active) | 当前活跃连接 |
| `reading` / `writing` / `waiting` | **瞬时** | 同上细分 | 可选但建议带 |
**不返回(已从目标模型删除):**
| 旧字段 | 原因 |
| --- | --- |
| `request_count` / `error_count` / UV / status_codes / top_domains | 业务窗汇总,改由 access log |
| `openresty_rx_bytes` / `openresty_tx_bytes` | 与已提供/接收数据重复且易错 |
| `source_countries` | 从未实现;国家走 Server GeoIP |
| `server.accepts/handled/requests` | 进程累计 counter,易与业务请求混淆;主路径不收录 |
**`/openflare/stub_status`:** 合并进上述 JSON 后 **删除**(过渡期可双挂,Agent 只打合并口)。
### 5.2 采集机制(读快照,不是「调用才开始统计业务」)
```text
Nginx 在连接建立/释放时维护 Active connections 等
│
Agent GET /openflare/observability
│
只读取「当前值」拼 JSON 返回
```
- **不是** GET 一次才去扫 access.log。
- **不是** 60 秒业务均值。
- 是 **瞬时 gauge 快照**。
### 5.3 上报示例(装进 NodePayload)
```json
"edge_health": {
"captured_at_unix": 1721289600,
"status": "healthy",
"message": "",
"connections": 42
}
```
| 字段 | 来源 |
| --- | --- |
| `status` / `message` | Agent 健康探测(配置校验/进程等,可与观测口 `ok` 配合);须与顶层 `openresty_status` / `openresty_message` 对齐 |
| `connections` | 观测口 `connections.active` |
**落库拆分(权威源):**
| 内容 | 写入 |
| --- | --- |
| 最新 `status` + `message` | **PG 节点表**(UI / 列表 / 告警) |
| 时序 `status` + `connections` | **CH `of_node_edge_health`**(**无 message**) |
---
## 6. L3 主机:host_metrics
### 6.1 采集从哪里来
Agent 读本机(如 `/proc`、磁盘统计等),**每次组包时读一次**。
| 字段 | 语义 | 说明 |
| --- | --- | --- |
| `cpu_usage_percent` | 瞬时 | 当前 CPU% |
| `memory_*` / `storage_*` | 瞬时用量/总量 | 占用率在 Server 或展示层算 |
| `disk_read_bytes` / `disk_write_bytes` | **累计 counter** | 内核累计 IO |
| `network_rx_bytes` / `network_tx_bytes` | **累计 counter** | **宿主机网卡**,不是已提供数据 |
### 6.2 上报示例
```json
"host_metrics": {
"captured_at_unix": 1721289600,
"cpu_usage_percent": 12.5,
"memory_used_bytes": 4294967296,
"memory_total_bytes": 16106127360,
"storage_used_bytes": 50000000000,
"storage_total_bytes": 107374182400,
"disk_read_bytes": 9000000000,
"disk_write_bytes": 12000000000,
"network_rx_bytes": 500000000000,
"network_tx_bytes": 800000000000
}
```
### 6.3 Server 如何处理累计字段
```text
存原值时间序列
展示「这段时间网卡出站」时:
delta = 本次 - 上次
若 delta < 0 → 视为重启/计数器归零,本段增量记 0,从新基线继续
若 delta >= 0 → 记入该时段增量
```
- Agent **上报原值**,不在边缘算 24h 总量。
- **禁止** 对累计原值做 `sum` 当业务量。
- 文案必须是 **「宿主机网卡」**,禁止叫「已提供数据 / OpenResty 出站」。
落库:`of_node_metric_snapshots`(可选 capacity hourly MV)。
---
## 7. 一次完整上报示例(拼起来)
```json
{
"schema_version": 2,
"node_id": "n_01hxyz",
"name": "edge-shanghai-1",
"ip": "203.0.113.10",
"version": "3.4.0",
"ext_version": "",
"current_version": "20260718-abc",
"last_error": "",
"host_metrics": {
"captured_at_unix": 1721289600,
"cpu_usage_percent": 12.5,
"memory_used_bytes": 4294967296,
"memory_total_bytes": 16106127360,
"storage_used_bytes": 50000000000,
"storage_total_bytes": 107374182400,
"disk_read_bytes": 9000000000,
"disk_write_bytes": 12000000000,
"network_rx_bytes": 500000000000,
"network_tx_bytes": 800000000000
},
"edge_health": {
"captured_at_unix": 1721289600,
"status": "healthy",
"message": "",
"connections": 42
},
"access_logs": [
{
"logged_at_unix": 1721289595,
"remote_addr": "198.51.100.20",
"host": "www.example.com",
"path": "/",
"status_code": 200,
"bytes_sent": 4096,
"request_length": 200,
"request_time_ms": 12
}
],
"buffered": [],
"health_events": [],
"waf_ip_group_checksums": {
"1": "d41d8cd98f00b204e9800998ecf8427e"
}
}
```
**Server 落库示意:**
| payload 块 | 写入 |
| --- | --- |
| `access_logs[0]` | CH 一行,`bytes_sent=4096`,`region` 由 GeoIP 填 |
| `edge_health` | 节点 `openresty_status=healthy`,connections=42 |
| `host_metrics` | CH metric 一行累计/瞬时字段 |
**产品查询示意(24h):**
- 已提供数据 = 该节点(或全局)日志 `sum(bytes_sent)`
- 当前连接 = 最新 `edge_health.connections`
- 宿主机网卡出站 = metric 上 `network_tx` 非负差分之和
三者数字 **不必相等**。
---
## 8. 离线补传 `buffered`
Agent 上报失败时,把 **同一类事实** 按窗口缓存在本地(默认约 60 分钟),恢复后塞进 `buffered[]`:
```json
"buffered": [
{
"captured_at_unix": 1721289500,
"host_metrics": { },
"edge_health": { },
"access_logs": [ ]
}
]
```
- 只装事实,不装旧 TrafficReport。
- Server 处理逻辑与主字段相同。
---
## 9. 端到端时序(默认 3s)
```text
t=0.0s 访客请求完成 → 写 access.log 一行;连接数可能变化
t=0.1s 又一请求 → 又一行 log
…
t=3s Agent 心跳:
· 读走 2 行 access_logs
· GET observability → connections=42
· 读 host_metrics
· 发给 Server
t=3s+ Server 入库;看板/Zone 查询时聚合日志
t=6s 下一轮…
```
---
## 10. 旧模型对照(帮助消歧)
| 旧做法 | 新模型 |
| --- | --- |
| Lua dict 60s 窗 request_count + Agent 10s 拉 + Server sum | **删除**;请求数 = 日志 count |
| openresty_tx 当「出站」 | **删除**;已提供数据 = `sum(bytes_sent)` |
| 两个口 observability + stub_status | **合并为一个** observability,只返回连接/探活 |
| TrafficReport 预聚合 | **删除**;协议与 API 均无此路径 |
| 业务与网卡混称「流量」 | **分文案、分 API、分表** |
| 健康 status/message | **PG 最新态权威**;CH 仅 status+连接时序 |
---
## 11. 配置与实现索引
| 项 | 位置/键 |
| --- | --- |
| 心跳间隔 | Agent `heartbeat_interval`;控制面 `agent_heartbeat_interval`(默认 3000ms) |
| 离线阈值 | 控制面 `node_offline_threshold`(默认 60000ms) |
| 观测端口 | `openresty_observability_port`(默认 18081) |
| access.log 路径 | `access_log_path` |
| 补传分钟数 | `observability_replay_minutes`(默认 60) |
| 协议类型 | `pkg/protocol/agent.go`(落地时按 v2 演进) |
| 表结构 DDL | [observability-data-model.md](./observability-data-model.md) |
---
## 12. 修订记录
| 日期 | 说明 |
| --- | --- |
| 2026-07-18 | 初稿:作为「最新传输模型」单页说明——三层、频率、示例 JSON、采集来源、与旧模型对照 |
| 2026-07-18 | 默认上报间隔 3s;离线阈值 60s;补传窗口 60 分钟 |
| 2026-07-18 | M5:edge_health 表、access_log_hourly、废弃 request_reports/obs_openresty 吞吐表 |
| 2026-07-18 | 无兼容层:删除「兼容期可忽略」表述;健康 message 仅 PG、CH 无 message |
-259
View File
@@ -1,259 +0,0 @@
# Pages 静态托管设计文档
你会学到:OpenFlare Pages 静态站点托管的架构设计、不可变部署与安全解压流程、OpenResty 的静态服务与 API 反向代理配置渲染,以及控制面与 Agent 的协同工作流。
---
## 需求分析
在现代 Web 运维中,除了动态应用的反向代理,静态前端站点(如 React、Vue 等构建的单页应用 SPA,或者 Hugo、VitePress 等静态生成器产物)的部署与托管也是极高频的场景。
传统方案中,静态站点的发布通常面临以下痛点:
1. **发布与反代配置脱节**:前端构建产物上传到 Nginx 宿主机后,还需要手动或通过其他脚本修改 Nginx 虚拟主机配置,容易出错且缺乏版本控制。
2. **多节点分发困难**:当控制面管理多台边缘节点时,将静态文件同步分发到所有节点,并确保文件一致性,需要维护复杂的同步脚本(如 rsync 等)。
3. **回滚缺乏一致性**:一旦新前端包发布失败或存在严重缺陷,不仅要恢复静态文件,还要恢复对应的反代规则,很难做到原子回滚。
为了解决这些问题,OpenFlare 引入了受 Cloudflare Pages 启发的 **Pages 静态托管** 功能。该功能将“预构建产物导入”与“网站代理规则配置”纳入同一控制面,依托 OpenFlare 的 pull-based(拉取式)协同架构,以不可变 deployment、单节点原子切换和周期对账实现多 Agent 最终收敛,并支持快速回滚。
---
## 核心功能
Pages 静态托管子系统包含以下核心能力:
* **预构建产物部署**:支持直接上传静态资源压缩包,也可为项目保存一个 Remote URL 或公开 GitHub Release asset 来源。外部来源只由 Server 访问,成功同步后统一创建或复用不可变 deployment 并原子激活。
* **不可变部署快照**:本地上传每次创建新的候选 deployment;持久来源同步按 source identity/revision 创建或复用 deployment 并激活。所有部署都有唯一 ID 和整包 SHA-256,支持按系统配置保留最近 N 个历史版本并随时回滚。
* **检查与自动更新**:GitHub latest 可按项目间隔定时检查;默认只提示可用更新,管理员显式开启后才按检查到的精确 revision 自动同步并发布。
* **SPA Fallback 支持**:支持对单页应用(SPA)进行 Fallback 路由配置,请求找不到静态文件时自动重定向到入口文件。
* **内置 API 反代服务**:支持在 Pages 规则内一键启用 API 代理,消除跨域问题,将请求转发给指定的后端服务。
* **安全包校验与解压缩**:内置路径逃逸防御、防软链接劫持、文件大小/数量上限与可配置上传包体积控制,保障节点物理安全。
* **可配置限额**:管理员可在运维设置中调整「部署包大小上限」与「历史部署保留数」。
### 部署源与未来构建边界
项目当前支持 manual、Remote URL、GitHub Release 三种来源视图。无 source 记录即 manual;切换或删除 source 不删除历史 deployment,也不改变当前 active deployment。Remote URL 只允许手动“同步并发布”;GitHub Release 支持 latest/tag 手动检查与同步,只有 latest 可选择定时检查和自动更新。
source 是可变配置,deployment 是不可变事实。source 配置与运行态游标、状态、租约分别存储;deployment 只保存创建时的安全 provenance 快照。所有产物都复用“下载或接收产物 → 真实字节与入口校验 → `upload.Ingest` → deployment”的 artifact pipeline:manual 上传停在 candidate,等待管理员显式激活;持久来源 sync 才在同一业务事务中 create-or-load 并原子激活。Agent 只消费 active deployment,不感知来源类型。
后续从 Git 仓库拉取源码并自动构建时,将新增独立 `git_repository` provider 与隔离的 build executor。它输出受限的预构建产物后继续复用上述导入管线;不得把 clone、依赖安装或任意构建命令下发给 Agent,也不得把 branch/build/env 字段塞入现有 `github_release` source。当前 V2 不增加这些未来字段或空任务,只稳定 provider 输出、source discriminated view 与 deployment provenance 三个扩展边界。
管理端信息架构参考 Cloudflare Pages 当前把 [Git integration](https://developers.cloudflare.com/pages/configuration/git-integration/) 与 [Direct Upload](https://developers.cloudflare.com/pages/get-started/direct-upload/) 分离、并统一展示生产状态与历史部署的方式:OpenFlare 项目详情按“当前生产部署 → 部署源 → 部署历史”组织。OpenFlare 仍允许切换来源并保留历史部署,不采用 Cloudflare 项目创建后来源不可切换的限制。
---
## Pages 静态托管架构
Pages 静态托管在逻辑上分为 **控制面 (Control Plane)** 与 **数据面 (Data Plane)**。
```mermaid
graph TD
%% 数据流
Browser[1. 浏览器 / 访客] -->|HTTPS 请求 / 流量| OpenResty[2. OpenResty / WAF]
OpenResty -->|1. 静态服务 try_files| StaticFiles[3. 边缘节点本地静态目录 current]
OpenResty -->|2. 转发 API 代理| BackEnd[4. 后端 API 服务]
%% 控制流与心跳
Admin[管理员 / CI] -->|上传或配置来源| Server[OpenFlare Server 控制面]
Providers[Remote / GitHub Provider] -->|受限 artifact candidate| Server
Scanner[内部 scanner / action task] -->|检查与自动同步| Server
Server <-->|Agent API / Heartbeat| Agent[openflare-agent 进程]
Server -.->|统一 upload.Ingest| UploadStore[(平台 upload backend)]
Agent -->|1. 发现新版本| Server
Agent -->|2. 下载部署包| Server
Agent -->|3. 校验、解压并原子切换| StaticFiles
style Browser fill:#f9f,stroke:#333,stroke-width:2px
style StaticFiles fill:#9f9,stroke:#333,stroke-width:2px
style Server fill:#f96,stroke:#333,stroke-width:2px
```
* **控制面(Control Plane)**:Server 接收本地上传,或通过受限 Provider 获取 Remote/GitHub 预构建产物;action task 与内部 scanner 负责检查、同步和自动更新。所有产物经统一 inspect 与 `upload.Ingest` 写入平台存储后端;manual 上传创建新的 candidate,持久来源 sync 则 create-or-load deployment 并原子激活。配置发布时只编译稳定的项目锚点与静态服务元数据。
* **数据面(Data Plane)**:Agent 在心跳/WS 对账中发现配置引用的 Pages 项目,通过专属 API 拉取该项目当前激活包并执行校验解压缩。OpenResty 在本地提供静态文件服务;Agent 不感知产物来自上传、Remote、GitHub 或未来 build executor。
---
## 数据模型与元数据设计
### 1. 核心数据库实体
* **Pages 项目 (`of_pages_projects`)**:
* 记录项目的业务名称、Slug 标识(URL 友好型)、启用状态、静态服务根目录(RootDir,可为空)、入口文件名(EntryFile,默认 `index.html`)、SPA Fallback 设置,以及 API 反向代理配置(APIProxyPath, APIProxyPass, APIProxyRewrite)。
* **部署源配置 (`of_pages_project_sources`)**:
* 每个项目最多一条可变来源配置,使用 `source_type` 区分 Remote URL 与 GitHub Release。`config_version` 用于 fence 旧任务;Remote 完整 URL 只保存在配置表中,不会进入响应、日志、任务 payload 或 deployment provenance。V2 不承诺数据库列加密。
* **部署源运行态 (`of_pages_project_source_runtime`)**:
* 与 source 1:1 保存 ETag、seen/applied revision、最近检查/同步、下次检查、错误和 lease。状态固定为 `idle | checking | update_available | syncing | failed | attention`,排队/完成状态由 `TaskExecution` 承担。
* **Pages 部署 (`of_pages_deployments`)**:
* 记录不可变部署事实:项目内递增部署号、整包 SHA-256、`upload_id`、文件数/总字节、创建者,以及可空的 source identity/revision、来源安全快照与 trigger。`artifact_path` 仅为旧数据兼容字段,不再是新部署的存储真相。
* **部署文件清单 (`of_pages_deployment_files`)**:
* 存储每次部署的完整常规文件路径与实际字节数,供控制台展示与统计。
* 不再为包内每个文件计算内容哈希;完整性由**整包** SHA-256(`of_pages_deployments.checksum`)保证,Agent 拉取时校验整包 hash。
* 控制面 inspect 通过文件句柄读取归档,流式消费每个常规文件体并核对声明大小与实际字节,避免将整包 `ReadFile` 进内存,也避免逐文件落盘计算 hash。
### 2. 路由关联与快照
`proxy_routes` 路由规则通过 `upstream_type = "pages"` 及 `pages_project_id` 关联 Pages 项目。当路由类型为 `pages` 且该项目存在已激活的部署时,才允许将该路由加入发布流程。
发布时生成的版本快照中包含 `snapshotPagesDeployment`,主要结构为:
```json
{
"project_id": 1,
"project_slug": "my-spa-app",
"deployment_id": 12,
"deployment_number": 3,
"checksum": "a7b3c2...",
"entry_file": "index.html",
"spa_fallback_enabled": true,
"spa_fallback_path": "/index.html",
"api_proxy_enabled": true,
"api_proxy_path": "/api",
"api_proxy_pass": "http://api.internal:8000",
"api_proxy_rewrite": "/api/(.*) /$1",
"local_root": "__OPENFLARE_PAGES_DIR__/projects/1/current"
}
```
### 3. 与主配置版本的双轨关系(项目锚点 + latest 拉取)
* **主配置版本**与 **Pages 部署** 是两套独立的版本体系。
* 主配置中 Pages 路由的稳定锚点是 **`pages_project_id`(项目 ID)**,不是某次部署 ID。
* OpenResty `root` 使用项目级路径:`__OPENFLARE_PAGES_DIR__/projects/{project_id}/current`,激活切换时路径不变,无需为换包而重发主配置。
* Agent 按项目请求「最新激活包」(类似 `github/release/latest`):
* `GET /api/v1/agent/pages/projects/:project_id/latest/hash`
* `GET /api/v1/agent/pages/projects/:project_id/latest/package`
* 控制面根据该项目**当前激活部署**返回 deployment ID、哈希、包大小与展开清单元数据。Agent 用 deployment ID 与其它 latest 元数据识别下载期间的指针竞态,但主配置和本地目录的稳定锚点仍是 project ID。
* 因此:在项目内切换激活部署后,**不必发布主配置**;Agent 在周期性对账时轮询 latest hash,发现变化即下载并切换 `current`。
* 快照中的 `pages_deployment` 字段仍可记录发布时元数据(入口文件、SPA/API 代理等),但不作为 Agent 拉包的版本锁定。
---
## Server 端 (控制面) 职责与生命周期
### 1. 部署包安全校验与分析
为了避免不可信产物攻击服务器,控制面对本地上传和所有外部来源执行同一套严格校验:
* **格式支持**:`zip`、`tar.gz` / `tgz`、`tar.xz` / `txz`、`tar.bz2` / `tbz2`、`tar`、`7z`。
* **大小限制**:压缩包体积由系统配置 `pages_max_package_size_mb` 控制(默认 100 MiB,范围 1~2048);展开后的单文件与总体积上限为「包大小 × 4」且不低于 100 MiB。inspect 始终流式读取常规文件体,核对声明大小与实际字节并按实际值执行上限。
* **数量限制**:压缩包中包含的静态文件总数不得超过 1,000 个。
* **软链接阻断**:遍历归档文件,一旦检测到任何软链接,立即抛出错误并拒绝上传,防御软链接劫持攻击。
* **路径逃逸防御**:对每个压缩文件路径进行 `Clean` 并检查是否包含 `..` 或以 `/` 开头,防御目录跨越漏洞,防止写入系统敏感路径。
* **入口文件校验**:项目指定的入口文件(例如 `index.html`,可在 `project.RootDir` 下)必须在部署包中存在,否则拒绝上传。
* **公共根目录去噪**:许多打包工具会包含一个多余的主文件夹作为公共根前缀。控制面自动探测公共根前缀并将其安全剥离。
* **整包完整性**:上传/导入时对压缩包字节计算一次 SHA-256,写入部署记录;Agent 拉包后按整包 hash 对账。包内单文件不做内容哈希。
* **实际体积复核**:`InspectOptions.VerifySizes` 只保留兼容意义;当前 inspect 无论该值为何都会读取常规文件体、核对声明值并累计实际大小,但仍不为单文件计算内容 hash。
* **历史保留**:系统配置 `pages_max_history_count`(默认 20,0 表示不限制)在部署成功后执行裁剪。通常语义为:**每个项目最多保留 N 条部署**;当前激活部署始终保留,其余名额按部署 ID 从新到旧填充。`history_count=1` 时,manual 上传会临时保留 active 与最新 candidate 两条,下一次上传替换旧 candidate;candidate 激活后恢复严格上限。超出的非激活 deployment 与文件清单会删除,对应 upload record 通过平台原语幂等软删除;Pages 不直接物理删除可能被 dedup 共享的 blob。部署已成功时裁剪失败只记日志、不回滚激活;并发操作下可能短暂超过 N,后续裁剪会收敛回 N。主配置版本回滚不依赖旧 Pages 包(见上节双轨关系)。
### 2. 部署包存储规划
控制面通过统一上传框架(`upload.Ingest`)把本地、Remote 和 GitHub 产物存入配置的本地/S3 后端,并在数据库中记录 `upload_id` 与文件清单。**大体积静态包不写入 config_versions 记录和任何配置推送通道**,以保障控制面数据同步的轻量与高效。
### 3. 来源检查、自动更新与上传补偿
* `openflare:pages_source_action` 执行管理员 check/sync 或 scanner 派发的精确 revision sync;payload 不携带 URL、Token、ETag 或 lease token。手动 sync 只接受真实用户 actor,自动 sync 只接受系统 actor 与 `scheduled_auto_update` trigger。
* `openflare:pages_source_scan` 是固定 `*/5 * * * *` 的 internal-only TaskHandler,只接受 `{}`,不会出现在通用任务类型与排程管理界面。每轮按“恢复过期 lease → 补偿 orphan upload → 扫描到期来源”执行。
* scanner 按 `next_check_at, source_id` 稳定排序,每批最多串行检查 20 个 GitHub latest source;ETag/304 仍推进检查时间,403/429 记录状态码和实际退避截止时间,单来源失败不阻塞后续来源。
* 发现更新总会先保存 seen cursor。只有 `auto_update_enabled=true` 且状态为普通 `update_available` 时,才携带本次检查得到的精确 revision 派发同步;`attention`、Remote 和固定 tag 不会自动发布。人工激活其它 deployment 会 fence 在途任务并关闭 auto。
* orphan 补偿每轮最多检查 100 条至少隔离 2 小时的 upload record,并要求 system owner、Pages 保留 type、V2 marker、无 deployment 引用。候选在 `project → source → runtime → upload` 锁序内复查,只通过上传框架软删除 record 和更新统计,不直接物理删除可能被 dedup 共享的 blob。
---
## Agent 端 (数据落地) 职责与自愈
Agent 运行在各边缘代理节点上:首次应用引用 Pages 项目的配置时,以及后续周期性 latest 对账时,都会把当前激活的静态资源“原子”地拉取到节点本地。
### 1. 按项目拉取 latest
1. Agent 从激活主配置中解析 `UpstreamType == "pages"` 的路由,收集稳定锚点 **`pages_project_id`**。
2. 对每个项目调用 `GET /api/v1/agent/pages/projects/:project_id/latest/hash` 获取控制面当前激活包哈希(类似 latest 指针)。
3. 若本地 `projects/{project_id}/releases/{hash}` 尚未就绪,再把 `.../latest/package` 流式下载到临时文件,执行真实响应上限与 SHA-256;下载后 **再次请求 hash**,避免激活切换造成的竞态,不一致则有限次重试。
4. 请求头携带节点 `X-Agent-Token`。
### 2. 安全解压缩、原子切换与只保留最新
1. 包体绝对上限为 2 GiB;下载内容的 SHA-256 须与「下载后再次查询」的 latest hash 一致,整个包不会进入 `[]byte`。
2. 解压至 `projects/{project_id}/releases/.{hash}-<random>.tmp` 随机 staging 目录(支持 zip / tar.* / 7z),拒绝路径逃逸、链接和特殊文件。Agent 同时服从 Server metadata 上限与本地绝对上限:最多 1,000 个文件,单文件及总量最多 8 GiB。
3. 解压完成后遍历实际文件树,精确复核文件数与总字节是否等于 Server metadata;不一致时拒绝切换。
4. 写入 `.openflare-pages.json` 后 rename 为 `releases/{hash}`。
5. **原子切换** `projects/{project_id}/current` 指向新 release(优先 symlink,失败则拷贝)。
6. **仅当新包已就绪且 current 切换成功后**,删除该项目下其它 `releases/*`(含 `.tmp`),**不保留历史部署包**。边缘节点每个项目永远只保留一份最新内容。
7. 多项目对账时 **隔离失败**:单个项目失败记日志并继续其它项目,最后汇总返回错误。
---
## OpenResty (静态服务与代理) 配置渲染
对于 Pages 托管站点,控制面自动渲染对应的 `server` 块,取代常规代理路由中的 `proxy_pass`。
### 1. 静态服务指令渲染
* **`root` 与 `index`**:
Server 将 `root` 指向项目级占位路径 `__OPENFLARE_PAGES_DIR__/projects/{project_id}/current`(可再追加 `RootDir`)。激活切换只换目录内容,路径不变,无需为换包重发主配置。
```nginx
server {
listen 80;
server_name myapp.example.com;
root "/var/lib/openflare/pages/projects/3/current";
index "index.html";
...
}
```
### 2. try_files 与 SPA Fallback 机制
* **禁用 SPA Fallback (默认)**:
仅匹配物理存在的文件,否则返回 strict 404:
```nginx
location / {
try_files $uri $uri/ =404;
}
```
* **启用 SPA Fallback**:
若请求的文件不存在,重定向到项目配置的入口 Fallback 文件(通常为 `/index.html`):
```nginx
location / {
try_files $uri $uri/ /index.html;
}
```
### 3. API 反向代理与重写 (Rewrite) 渲染
当静态前端项目需要请求后端 API 且不希望面临跨域问题时,可开启 API 反代。OpenResty 渲染器会自动在其对应的静态 `server` 块内嵌套专属的 API `location` 分支:
```nginx
server {
listen 80;
server_name myapp.example.com;
...
# API 代理路径匹配
location /api {
# 如果配置了 Rewrite 规则,应用重写逻辑
rewrite ^/api/(.*)$ /v1/$1 break;
rewrite ^/api$ / break;
proxy_pass http://api.internal:8000;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location / {
try_files $uri $uri/ /index.html;
}
}
```
---
## 交互逻辑与同步流程
一次完整的预构建产物导入与生效生命周期如下。首次绑定项目需要发布主配置;后续 active deployment 变化通过项目 latest 独立收敛:
```text
[管理员 / scanner] [Server 控制面] [Agent] [OpenResty]
| | | |
|-- manual 上传 ------>|-- inspect / Ingest ---->| |
| |-- 创建 candidate | |
|-- 显式激活 candidate ->|-- 切换 active | |
| | | |
|-- source sync ------>|-- inspect / Ingest | |
| |-- create/load + 原子激活 | |
| | | |
|-- 首次绑定项目并发布 ->|-- 广播项目锚点 -------->|-- 写入/重载路由 ---------->|
| | | |
|-- 后续激活/同步/回滚 ->|-- active latest 改变 ---| |
| |<-- latest 元数据对账 ----| |
| |--- 流式返回 package ---->| |
| | |-- 校验、解压、复核 --------|
| | |-- 原子切换 current -------->|
```
-130
View File
@@ -1,130 +0,0 @@
# 内网穿透隧道设计文档
你会学到:OpenFlare 内网穿透隧道的架构设计、双端管控组件(Relay 与 Client)的内部原理、交互逻辑以及数据面与控制面的通信流程。
---
## 需求分析
在典型的 Web 应用托管场景中,许多源站(Origin Server)部署在内网环境(如本地开发机、局域网服务器或受防火墙限制的内网集群)。这些服务器通常:
1. **无公网 IP**:无法直接被公网流量访问。
2. **安全合规限制**:不允许随意在边界路由器上配置端口映射(NAT)。
3. **动态 IP 变动**:传统的 DDNS 方案延迟高且极不稳定。
为了让内网源站能够无缝接入 OpenFlare 全局数据网关并享受 WAF 地域防护、TLS 证书托管等增值服务,OpenFlare 设计了基于 **反向中继穿透隧道** 的整体解决方案。在该架构中,公网边缘节点作为反代入口和流量中继,内网侧仅需发起安全出向连接,即可实现公网流量安全、稳定地反向穿透到内网源站。
---
## 核心功能
内网穿透隧道子系统包含以下核心能力:
* **Relay 节点动态管理**:由控制面动态派发中继服务(frps),动态分发服务端口与认证令牌(Token)。
* **多隧道反向代理映射**:支持在单个内网客户端上映射多个内网 Web 端口,并将多域名路由绑定至对应的中继节点。
* **独立进程生命周期管控**:中继与客户端均为 Go 编写的独立二进制守护进程,内部负责拉起、监控、自愈及热升级底层的 frp 引擎。
* **基于 Token 的独立认证隔离**:中继端使用 `agent_token`,内网客户端使用专属 `tunnel_token`,权限与路由边界隔离。
* **配置校验与增量热重载**:仅在隧道绑定关系、证书或 Relay 拓扑发生实际变化时,才重写配置文件并平滑重载进程,降低运行开销。
---
## 内网穿透与隧道架构
内网穿透子系统基于成熟的 `frp` 高性能隧道协议进行整合,分为 **控制面 (Control Plane)** 与 **数据面 (Data Plane)**。
```mermaid
graph TD
%% 数据流
Browser[1. 浏览器 / 访客] -->|HTTPS 请求| Agent[2. OpenResty / Agent]
Agent -->|本机转发 proxy_pass| RelayFrps[3. OpenFlare Relay / frps]
RelayFrps -->|加密隧道协议| FlaredFrpc[4. OpenFlared / frpc]
FlaredFrpc -->|转发本地请求| LocalOrigin[5. 内网源站 192.168.x.x]
%% 控制流与心跳
Server[OpenFlare Server 控制面] <-->|Relay API / Heartbeat| RelayManager[openflare-relay 进程]
Server <-->|Client API / Heartbeat| ClientManager[openflared 进程]
RelayManager -.->|管控进程及配置| RelayFrps
ClientManager -.->|管控多 Relay 进程| FlaredFrpc
style Browser fill:#f9f,stroke:#333,stroke-width:2px
style LocalOrigin fill:#9f9,stroke:#333,stroke-width:2px
style Server fill:#f96,stroke:#333,stroke-width:2px
```
* **控制面(Control Plane)**:Server 维护数据库状态;中继节点上的 `openflare-relay` 进程与内网服务器上的 `openflared` 进程通过 HTTP 心跳与 WebSocket 长通道同步隧道配置。
* **数据面(Data Plane)**:公网流量首先进入公网边缘的 Agent (OpenResty),在此完成 HTTPS 握手、TLS 终止和 WAF 过滤,接着通过 `proxy_pass` 转发到同机部署的 `openflare-relay (frps)`。`frps` 再将请求封包通过与内网 `openflared (frpc)` 建立的持久隧道传输过去,最后由 `frpc` 拆包并分发给内网实际的源站服务。
---
## Relay (中继端) 设计
`openflare-relay` 是部署在公网边缘的中继管理器,运行在 `tunnel_relay` 类型的节点上。
### 1. 核心架构与逻辑
* **进程守护**:Relay 进程内部持有 `frps` 二进制,通过 `exec.Command` 拉起 `frps -c frps.toml` 子进程,并启动 goroutine 异步监听其退出状态。如果发现 `frps` 异常退出,会结合退避机制自动拉起。
* **动态配置渲染**:通过 HTTP 心跳向控制面同步状态,获取当前的 `RelayConfig`,主要参数包括:
* `bindPort`:frps 用于监听内网 frpc 客户端连接的公网控制端口。
* `vhostHTTPPort`:虚拟主机(Virtual Host)HTTP 流量监听端口,Agent 的 proxy_pass 会指向此端口。
* `authToken`:客户端连接时进行握手校验的安全凭证。
* `webServer`:开启 frps 的仪表盘 API,Relay 基于此接口或管理控制端口收集实时的活跃隧道数和流量指标。
* **状态上报**:Relay 每周期心跳会向控制面上报底层 `frps` 的活跃连接数、注册客户端数、各个代理通道的实时状态以及 Relay 版本。
---
## Openflared (客户端) 设计
`openflared` 是运行在用户内网服务器侧的客户端管理器,使用独立的 `tunnel_token` 进行鉴权。
### 1. 核心设计机制
* **多 Relay 支持(多路复用)**:
为保障高可用或就近接入,控制面可能会将客户端连接调度到多个公网 Relay。`openflared` 会读取 `TunnelConfig` 中下发的 Relays 列表,在本地为每一个 Relay 节点独立生成一个专用的配置文件(命名为 `frpc_<relay_node_id>.toml`),并分别为每个 Relay 进程分配独立的 cancelable context。
* **子进程独立监控**:
`openflared` 内部维护一个 `processes` 映射表,对每个 `frpc` 子进程进行独立的生命周期管控。当控制面增加或移除 Relay 时,客户端会增量拉起新进程或优雅注销老进程,避免影响其他正常工作的隧道。
* **动态 TOML 生成**:
为每个 Relay 渲染 TOML 时,客户端会遍历 Proxies 列表,将每个内网服务的 `LocalAddr`、`LocalPort`、绑定的 `CustomDomains` 写入到 `[[proxies]]` 块中。
---
## 交互逻辑与流量模型
内网穿透子系统实现了一致性版本控制和状态反馈。
### 1. 控制面发布与同步流程
```text
管理员修改隧道/内网端口映射 -> 提交发布 -> 生成新 Tunnel 版本与 Checksum
|
v (推送或心跳拉取)
+-------------------------------------------+-------------------------------------------+
| |
v (中继端) v (内网客户端)
openflare-relay 心跳检测到 frps 端口/Token 变化 openflared 心跳检测到 tunnel_version 发生变更
重新渲染本地 frps.toml 请求拉取最新代理映射包
Kill 并重新拉起 frps 进程 重新渲染 frpc_<relay_id>.toml
上报健康状态为 healthy 对有变更的 Relay 进程执行重启与配置热重载
上报应用结果 (Apply Success/Error)
```
1. **版本化控制**:所有内网隧道的路由和映射关系与主路由系统类似,也经过版本化控制,下发 `version` 与 `checksum`,确保客户端不重复写入和频繁重载进程。
2. **应用结果闭环**:客户端应用新配置后,会在心跳中携带应用结果上报控制面。若因内网端口不可达或证书配置有误导致 frpc 无法建连,客户端会截获进程输出将 `LastError` 上报,管理员在 Server 即可直观查看穿透失败原因。
### 2. 数据面流量模型
1. **公网入口 (Agent)**:
```nginx
server {
listen 443 ssl;
server_name intranet.example.com;
# ... TLS 证书与 WAF 过滤逻辑 ...
location / {
proxy_pass http://127.0.0.1:18080; # 指向本地 frps 的虚拟主机端口
proxy_set_header Host $host; # 必须保留原 Host,因为 frps 依靠 Host 进行内部路由分发
proxy_set_header X-Real-IP $remote_addr;
}
}
```
2. **中继节点 (frps)**:
`frps` 监听到 `18080` 端口有 HTTP 请求进来,读取 HTTP 请求头中的 `Host: intranet.example.com`,在其已注册的活跃隧道表中检索该域名对应的加密 TCP 连接(由内网 frpc 建立)。
3. **加密隧道传输 (TCP)**:
`frps` 将 HTTP 请求封装进内部 TCP 隧道协议,发送给内网的 `frpc` 客户端。
4. **内网客户端分发 (frpc)**:
`openflared` 管理的 `frpc` 收到封包,根据本地配置(`localIP = "127.0.0.1"`, `localPort = 8080`)将请求建立本地 TCP 连接转发给内网 Web 服务,并将 Web 服务的响应原路打包返回,最终呈现给公网用户。
-15
View File
@@ -1,15 +0,0 @@
# WAF 设计
OpenFlare WAF 的现行规则模型是可视化 DAG。节点语义、图约束、多规则顺序、发布编译与迁移边界统一以 [WAF 可编排规则设计](./waf-orchestration-design.md) 为准。
## 系统边界
Server 保存带坐标和修订号的编辑图,发布时再次校验并编译为紧凑运行图;Agent 原子写入快照并 reload OpenResty;请求热路径只遍历 Worker 内存中的不可变图。
IP 组独立于规则拓扑更新。手动、订阅和自动 IP 组由控制面维护,Agent 先原子替换 JSON、最后更新 checksum。协调 Worker 每 5 秒检查 checksum,仅变化时读取完整快照并分发给其它 Worker;失败时保留上一份有效数据。完整运行时快照上限为 20 MiB,Server 发布/同步与 Agent 落盘使用同一序列化校验;OpenResty 使用独立的 64 MiB 共享字典和非淘汰写入,容量不足时拒绝新版本而不破坏已提交快照。
地域节点使用 Country 与 City MMDB。Agent 首次启动时从程序内嵌数据库初始化缺失文件,后续按配置周期下载更新,请求处理始终读取 OpenResty 已加载的数据库。数据库不可用时地域匹配返回 `false` 并限频告警,不允许因数据损坏意外放行其它执行错误。
## 安全顺序
启用的全局规则固定前置;路由规则按绑定 sequence 执行。阻止节点立即终止,通过节点仅结束当前规则,全部规则通过后才进入回源链路。未知节点、缺失出口或步数超限一律阻止请求。
-128
View File
@@ -1,128 +0,0 @@
# WAF 可编排规则设计
本文定义 OpenFlare WAF 从固定判定链重构为可视化有向无环图(DAG)的目标架构、数据模型、执行语义、发布模型与迁移边界。IP 组的来源与成员计算仍遵循 [WAF 设计](./waf-design.md),本文只改变规则如何组合和执行。
## 目标与边界
用户新增 WAF 规则时只输入名称。Server 随即创建一张合法的默认图 `开始 → 通过`,前端进入基于 React Flow 的独立编排页面。用户通过添加处理单元、配置节点并连接分支构建策略,不再填写固定顺序的黑白名单与 PoW 表单。
第一阶段支持以下节点:
| 节点 | 数量约束 | 输入 | 输出 | 配置 |
| --- | --- | --- | --- | --- |
| 开始 | 每张图恰好一个 | 无 | `next` | 无 |
| 通过 | 每张图恰好一个 | 一个或多个 | 无 | 无 |
| 阻止 | 可创建多个 | 一个或多个 | 无 | HTTP 状态码、HTML 响应体 |
| IP 匹配 | 可创建多个 | 一个或多个 | `true`、`false` | IP、CIDR、IP 组 ID |
| 地域匹配 | 可创建多个 | 一个或多个 | `true`、`false` | 国家代码、地区代码 |
| UA 检查 | 可创建多个 | 一个或多个 | `true`、`false` | 要求携带 UA、浏览器/OS 白名单与 and/or、屏蔽爬虫/非正常 UA(不含爬虫)/自定义正则 |
| 安全防护 | 可创建多个 | 一个或多个 | `true`、`false` | 基础特征检测(路径穿越/文件包含默认开;SQL/XSS/命令注入/SSRF/上传/XXE/CRLF 可开关);命中任一已启用规则为 false |
| PoW | 可创建多个 | 一个或多个 | `next` | 算法、难度、会话 TTL、挑战 TTL |
IP 匹配、地域匹配、UA 检查与安全防护不区分黑名单或白名单。`true` 只表示请求通过该节点判定,`false` 只表示未通过;放行或阻止的业务含义完全由连线决定。UA 检查的求值顺序为:要求携带 UA → 屏蔽爬虫/非正常 UA → 白名单匹配。安全防护在请求 Path/Query/Header/Cookie/Body(有限)上做特征匹配。PoW 验证完成后沿 `next` 继续,未完成时由挑战页面接管当前请求,不产生 `false` 分支。
不在第一阶段实现循环、脚本节点、任意表达式节点、子图调用和跨规则跳转。
## 控制面架构
规则图采用控制面编辑态和数据面运行态分离的双模型:
1. React Flow 编辑器提交版本化图 JSON,其中包含节点 ID、节点类型、显示名称、坐标、类型化配置和连线。
2. Server 对整张图执行权威校验,通过后以单个事务保存图并递增修订号。
3. 配置发布时,Server 再次校验所有启用规则,将图编译为不含坐标、标签等 UI 字段的紧凑运行时 DAG,并收集被引用的 IP 组 ID。
4. Agent 原子落盘完整发布快照并 reload OpenResty。新 Worker 启动时只加载和解析一次规则 JSON。
5. 请求热路径只遍历 Worker 内存中的不可变运行时图,不读取文件、不计算 checksum、不解析 JSON。
编辑态 JSON 使用明确的 `schema_version`。节点配置使用按节点类型区分的结构,不允许用无约束键值对象绕过 Server 校验。初始安全上限为每条规则 128 个节点、256 条边和 256 KiB 编辑态 JSON;这些限制由 API 和发布编译器共同执行。
## 图结构约束
规则保存与发布必须满足全部约束:
* 图是有向无环图,禁止自环和任意循环。
* 恰好存在一个开始节点和一个通过节点;阻止节点可以存在多个。
* 开始节点无入边且恰好有一个 `next` 出口;通过和阻止节点无出口。
* IP 匹配、地域匹配、UA 检查与安全防护的 `true`、`false` 出口必须各连接一次;PoW 的 `next` 必须连接一次。
* 除终止节点外不得存在悬空出口;每个非开始节点至少有一条入边。
* 所有节点都必须从开始节点可达,且从每个可执行节点出发都能抵达通过或阻止。
* 边的源端口必须属于源节点类型;同一源端口不得连接多个目标。
* 节点 ID 在图内唯一,边 ID 在图内唯一,所有边引用的节点必须存在。
* 节点配置必须通过对应类型的字段、范围、引用存在性和体积校验。
前端提供即时校验和连线限制以改善体验,但 Server 是唯一权威校验方。删除节点时前端同步删除关联边并将规则标记为未保存;图恢复合法前禁止保存。
## 多规则执行语义
一个路由可以绑定多条自定义规则。绑定关系是有序列表,并遵循以下顺序:
1. 启用的全局规则固定最先执行,不参与路由侧排序。
2. 路由绑定的启用规则按绑定顺序依次执行。
3. 当前规则抵达阻止节点时立即输出该节点配置的响应并终止请求。
4. 当前规则抵达通过节点时,只表示当前规则执行完成;若仍有后续规则则继续执行。
5. 全部规则均抵达通过节点后,请求才真正放行并进入后续 OpenResty/回源链路。
运行时图在发布前已经过完整校验。若 Lua 执行器仍遇到未知节点、未知端口、缺失目标或超过节点步数上限,则记录限频错误并阻止请求,避免损坏的安全配置意外放行。
## IP 组内存刷新
规则拓扑只在发布并 reload OpenResty 时生效;IP 组成员仍可由手动、订阅或自动任务独立更新,不要求发布或 reload。
IP 组采用协调 Worker、共享快照和 Worker 本地对象的两级缓存:
1. 请求始终读取当前 Worker 内存中的 IP 组对象,不访问文件或共享字典中的 JSON。
2. 每 5 秒只有一个取得共享锁的 Worker 读取轻量 checksum 文件。
3. checksum 未变化时立即结束,不读取完整 `waf_ip_groups.json`。
4. checksum 变化时,协调 Worker 读取并验证一次完整 JSON,再把原始快照按 checksum 写入独立的 64 MiB `ngx.shared.openflare_waf_ip_groups`,最后更新提交指针。
5. 其他 Worker 发现共享版本变化后,从共享内存取得快照、解析并原子替换各自的本地对象,不重复读取磁盘。
6. 刷新失败时继续使用上一份有效对象,限频记录错误,并在下一周期重试。
Agent 必须先原子替换 IP 组 JSON,最后原子更新 checksum,使 Worker 永远不会把半写入文件识别为新版本。Server 发布/同步和 Agent 落盘共同执行 20 MiB 聚合快照上限;共享字典使用不会强制淘汰旧键的安全写入,失败时保留当前与上一代不可变快照。
## API 与编辑器
创建接口只接受规则名称,创建成功后返回带默认图的规则详情。规则元数据、图保存和路由绑定使用独立操作,避免修改启用状态或绑定时覆盖画布。
图详情包含 `revision`。保存请求提交 `revision + graph`,Server 仅在修订号匹配时更新并递增修订号;不匹配时返回冲突,前端提示重新加载,禁止静默覆盖其他页面的修改。路由绑定接口接受有序规则 ID 数组。
React Flow 编辑页采用全宽画布和固定右侧属性栏:
* 顶部提供返回、规则名称、启用状态、校验状态和保存操作。
* 画布使用紧凑高度和较小的首次适配缩放,支持缩放、平移、框选、删除、自动布局和 MiniMap/Controls 等必要导航能力;节点拖动由 React Flow 本地受控状态实时处理,拖动结束后才把坐标写回编辑图。
* “添加处理单元”提供 IP 匹配、地域匹配、UA 检查、安全防护、PoW 和阻止;开始与通过由默认图提供且不可删除或重复添加。
* 选中普通节点或连线后可使用画布删除按钮或 Delete/Backspace 删除;删除节点时同步移除关联连线。
* 右侧属性栏默认隐藏,选中节点后才显示并用于编辑配置;点击连线或画布空白处时收起。
* 地域匹配属性使用完整国家与 ISO 3166-2 一级行政区数据;国家选项同时显示本地化名称与代码,行政区支持按国家名、行政区名或代码搜索,避免一次渲染数千个选项。
* 离开存在未保存变更的页面前必须提示;保存冲突和 Server 校验错误应定位到相关节点或边。
WAF 列表展示规则名称、启用状态、节点数量、应用路由数量和更新时间。新建规则的对话框只有名称字段,成功后立即导航到编排页面。
## 持久化与迁移
规则记录增加版本化图 JSON 与修订号;绑定记录增加执行顺序。图作为一个聚合整体保存,不拆成节点表和边表,以保证编辑操作的事务边界,并让新增节点类型不必频繁扩展数据库 Schema。
升级现有安装时:
* 保留规则名称、全局标记、启用状态及路由绑定关系。
* 所有规则图重置为 `开始 → 通过`,不迁移旧 IP/地域名单、PoW 或拦截响应配置。
* 现有绑定按稳定顺序写入顺序字段;全局规则仍固定前置。
* 新图和运行时稳定后移除旧规则字段、固定顺序编译逻辑和旧前端表单,不长期维护双执行器。
该迁移会让旧防护配置停止生效,升级说明必须显著提示管理员在发布下一版本前重新编排规则。
## 发布、失败与回滚
规则图只在配置发布时生效。发布前校验或编译失败时拒绝发布,当前活动版本保持不变。Agent 写入、OpenResty 配置检查或 reload 失败时,应用流程失败并恢复上一份有效发布版本。
新 Worker 只接受完整且可解析的规则运行态配置。旧 Worker 在 OpenResty 优雅 reload 期间继续使用旧内存图,新 Worker 使用新图,因此请求不会观察到半更新状态。
地域数据库不可用时,地域匹配返回 `false` 并限频告警,保持现有行为。IP 组刷新失败时保留旧内存快照。PoW 未完成由挑战模块接管请求,不视为执行错误;PoW 节点配置先以短期键写入 OpenResty 共享内存,再通过 `ngx.exec` 的显式参数传给内部挑战处理器,不能依赖内部重定向保留 `ngx.ctx` 或隐式继承请求参数。发布快照中的空规则绑定必须编码为 JSON 空数组;运行时将旧快照中的 `null` 可选数组按空数组处理,禁止因 `cjson` 的 `ngx.null` userdata 中断请求。
## 测试与验收
* Go 单元测试覆盖图结构、端口、可达性、终止性、节点配置、体积限制、编译结果、修订冲突和绑定顺序。
* 数据库测试覆盖 PostgreSQL/SQLite 迁移、默认图、旧绑定稳定排序和回滚。
* Lua 测试覆盖所有节点出口、多规则顺序、全局规则前置、多个阻止响应、PoW 接管和损坏运行时图保护。
* Agent/OpenResty 测试覆盖发布 reload、加载一次、失败回滚、IP 组五秒 checksum 刷新和旧快照保留。
* 前端测试覆盖创建后导航、特殊节点唯一性、连线限制、属性编辑、即时校验、未保存提示和并发冲突。
* 集成测试从控制面创建并编排规则,发布后用真实请求验证放行、阻止、PoW 和 IP 组热刷新。
* API 变更后运行 `make swagger`;完成实现后运行前端检查与构建以及 `make code-check`。
-100
View File
@@ -1,100 +0,0 @@
# Zone 与域名资源设计
## 目标
将“网站”重构为以可注册根域为入口的 Zone 管理体验。`example.com` 之类的 Zone 是稳定的管理边界;用户通过稳定 ID 路径进入该 Zone,查看并维护其中明确声明的域名、域名所绑定的反代路由和证书,以及路由级 WAF、Pages 等能力。
本设计替代 `managed_domains` 的概念、表与 API。它不引入权威 DNS 解析记录管理。
## 范围与约束
* Zone 根域使用 Public Suffix List 解析,例如 `api.example.co.uk` 归属 `example.co.uk`。
* URL 使用 ID:列表为 `/websites`,详情为 `/websites/:zoneId`;不使用域名作为 URL 参数。
* Zone 域名必须是明确的 FQDN,禁止录入 `*.example.com`。TLS 证书可仍含通配符 SAN,并用于覆盖明确的 Zone 域名。
* 一个 Zone 域名至多关联一条反代路由;一条反代路由可关联多个 Zone 域名,因而可跨 Zone 共享同一套上游、缓存、限流、WAF 与 Pages 配置。
* 不新增 DNS 记录、边缘函数、预览子域或租户隔离能力。
## 核心模型
```mermaid
erDiagram
ZONES ||--o{ ZONE_DOMAINS : contains
PROXY_ROUTES ||--o{ ZONE_DOMAINS : serves
TLS_CERTIFICATES ||--o{ ZONE_DOMAINS : secures
PROXY_ROUTES ||--o{ WAF_RULE_GROUP_BINDINGS : applies
PAGES_PROJECTS ||--o{ PROXY_ROUTES : backs
ZONES {
uint id PK
string domain UK
}
ZONE_DOMAINS {
uint id PK
uint zone_id
uint proxy_route_id
string domain UK
uint cert_id
}
```
### `of_zones`
保存根域、创建时间与更新时间。根域全局唯一且创建后不可原地修改;需要变更时新建 Zone 并迁移域名。删除 Zone 前必须先清空其 Zone 域名。
### `of_zone_domains`
保存 `zone_id`、明确 `domain`、可空的 `proxy_route_id`、可空的 `cert_id` 及时间戳。`domain` 全局唯一;所有关系字段建立索引但不建立物理外键。`proxy_route_id` 允许为空,以承接已准备证书但尚未配置反代的历史域名。
`of_proxy_routes` 逐步移除 `domain`、`domains`、`cert_id`、`cert_ids` 与 `domain_cert_ids` 等域名/证书冗余列。路由不得再指定任何 TLS 证书;路由名称 `site_name` 成为稳定的人类可读标识,编译器从关联的 Zone 域名读取 `server_name` 与其 `cert_id`。这使每个明确域名的证书只有一个来源。
## 业务与 API
管理端新增 Zone 资源:
* `GET/POST /api/v1/d/zones`
* `GET/POST /api/v1/d/zones/:id/update`
* `POST /api/v1/d/zones/:id/delete`
* `GET/POST /api/v1/d/zones/:id/domains`
* `POST /api/v1/d/zones/:id/domains/:domainID/update`
* `POST /api/v1/d/zones/:id/domains/:domainID/delete`
* `GET /api/v1/d/zones/:id/overview`
反代路由的创建、更新请求改用 `zone_domain_ids`,不再提交 `domains`、`cert_id`、`cert_ids` 或 `domain_cert_ids`。服务端在事务中验证域名归属、全局唯一性和证书 SAN 覆盖;失败通过 `response.Abort*` 统一返回。删除已绑定路由的 Zone 域名必须先解除或删除该路由;删除仍有域名的 Zone 必须拒绝。
WAF、Pages、上游与发布版本仍属于 `proxy_routes`。Zone 概览只聚合展示其域名关联的路由状态,不复制或重新定义这些配置。
## 前端体验
`/websites` 只展示 Zone 根域,显示已配置域名数、路由数与状态,并提供搜索、创建和操作菜单。点击进入 `/websites/:zoneId`。
详情页包含:
* 概览:域名、路由和有效证书统计;域名—路由—证书摘要;路由级 WAF 与 Pages 摘要。
* 域名:明确 FQDN 的列表、证书选择和关联路由;不显示或接受通配符域名。
* 路由:筛选到当前 Zone 的路由并链接到既有路由详情。
* 证书:当前 Zone 域名实际引用的证书。
* 设置:Zone 备注和受保护的删除操作。
新增路由时从 Zone 域名中选择;用户也可以先在 Zone 中登记域名,再绑定路由。全局反代路由入口保留,但改用同一套 Zone 域名选择器。
## 数据迁移
本次改造分两个发布阶段,以免 SQL 用错误的“末两段域名”规则处理多级公共后缀。操作细则见 [Zone 域名迁移与发布验收](../guide/zone-domain-migration.md)。
1. **第一阶段 DDL**:PostgreSQL 与 SQLite 同版本 Goose 创建 `of_zones` / `of_zone_domains`;暂时保留 `of_managed_domains` 与路由冗余列。
2. **数据导入(自动)**:Server 启动时 `migrator.Migrate()` 先应用 goose SQL 至 `202607120002`,再自动导入旧路由域名 / `managed_domains`(`publicsuffix` 解析注册根域,写入 `cert_id` 与 `proxy_route_id`),最后继续后续 SQL。冲突时启动失败;修复后重启可幂等重试。无需手动命令。
3. **代码切换**:控制面 API、配置快照、渲染、前端均以 Zone 域名为唯一来源;路由写入仅使用 `zone_domain_ids`。
4. **第二阶段清理**:Goose SQL `202607130001_drop_legacy_route_domain_columns` 删除 `of_managed_domains` 与 `of_proxy_routes` 冗余列。Down 仅恢复开发库空结构,不回填历史数据。
### 运行时模型边界
* 持久化:域名与证书只存在于 `of_zone_domains`;`of_proxy_routes` 仅保存路由策略(上游、缓存、限流、WAF 绑定键等)。
* 渲染:配置快照在内存中组装临时 `Domains` / `DomainCertIDs` 供 OpenResty 渲染,不写回数据库。
* 结构迁移仅使用 `internal/db/migrator/goose/{postgres,sqlite}/*.sql`;启动时自动导入历史域名,第二阶段后旧列不存在则为空操作。
## 验证
* 单元测试:Public Suffix List 分组、FQDN / 通配符拒绝、跨 Zone 路由、证书 SAN 覆盖、删除保护及迁移幂等性;清理后断言旧列/旧表不存在。
* 集成测试:Zone、Zone 域名与路由 API 的成功与失败响应;现有路由迁移后生成相同 OpenResty 域名与证书配置。
* 前端测试:Zone 列表、ID 路由、详情加载 / 错误 / 空状态、域名选择器与 API 负载。
* 手动验证:迁移前后比较激活配置快照中的 `server_name` 和证书路径,发布后使用根域及各子域请求验证路由。
+41 -12635
View File
File diff suppressed because it is too large Load Diff
-68
View File
@@ -1,68 +0,0 @@
# TLS 证书与自动续期
本指南介绍如何在 OpenFlare 中管理 TLS 证书。为了使用 HTTPS 安全加密流量,你需要配置对应的证书。OpenFlare 支持**手动导入已有证书**以及**通过 ACME 自动申请与托管续期**。
---
## 方式一:手动导入已有证书
如果你已经从第三方服务商(如腾讯云、阿里云等)申请了免费或收费的证书,或者在本地生成了自签名证书:
1. 登录管理端控制面板,进入左侧导航 **「网站管理」->「TLS证书」** 页面。
2. 点击右上角的 **「导入证书」**。
3. 填写配置信息:
* **证书名称**:输入一个易于识别的别名(如 `my-domain-cert`)。
* **证书内容 (PEM)**:复制并粘贴 PEM 格式 of 证书公钥内容(通常以 `-----BEGIN CERTIFICATE-----` 开头)。
* **证书私钥 (KEY)**:复制并粘贴证书的私钥内容(通常以 `-----BEGIN PRIVATE KEY-----` 或 `-----BEGIN RSA PRIVATE KEY-----` 开头)。
4. 点击 **「保存」**。导入成功后,该证书即可在配置域名时直接绑定使用。
---
## 方式二:自动申请与到期自动续签 (ACME)
OpenFlare 内置了 ACME 客户端并对接了 **Asynq 异步任务队列**。通过配合云解析服务商的 DNS API,系统能自动完成 DNS-01 挑战(Challenge)校验,并向 CA(默认 Let's Encrypt)申请通配符/单域名证书,并在**到期前 30 天自动触发后台秒级续签**。
### 第一步:在 Cloudflare 申请 DNS API Token
为了使 OpenFlare 能够自动在你的域名下添加 TXT 记录以完成 DNS 校验,你需要准备一个具有特定权限的 Cloudflare API Token。
> [!IMPORTANT]
> 安全起见,**强烈建议使用限定权限的 API Token**,而非全局 API Key (Global API Key)。
1. 登录 [Cloudflare 控制台](https://dash.cloudflare.com/)。
2. 点击右上角的用户头像,选择 **「我的个人资料 (My Profile)」**。
3. 在左侧菜单中选择 **「API 令牌 (API Tokens)」**,然后点击 **「创建令牌 (Create Token)」**。
4. 找到 **「编辑区域 DNS (Edit Zone DNS)」** 模板,点击 **「使用模板 (Use template)」**。
5. 配置令牌权限与范围(保持默认或根据实际情况限定):
* **权限 (Permissions)**:
* `区域 (Zone)` - `DNS` - `编辑 (Edit)` (必须,ACME 写入 TXT 记录用)
* `区域 (Zone)` - `区域 (Zone)` - `读取 (Read)` (必须,用于列出和检索区域 ID)
* **区域资源 (Zone Resources)**:
* 选择 **「包括 (Include)」** -> **「所有区域 (All zones)」**,或者选择 **「特定区域 (Specific zone)」** 并指向你托管的特定域名。
6. 点击 **「继续以转到摘要 (Continue to summary)」**,确认无误后点击 **「创建令牌 (Create Token)」**。
7. 复制生成的 **API 令牌 (Token)** 字符串。*注意:该令牌仅展示一次,请妥善保存*。
### 第二步:在控制端添加 DNS 账号
1. 登录 OpenFlare 管理端,进入左侧导航 **「网站管理」->「DNS账号」**。
2. 点击 **「添加账号」**。
3. 填写配置信息:
* **账号名称**:如 `cloudflare-main`。
* **DNS 服务商**:选择 `Cloudflare`。
* **API Token**:填入刚刚在 Cloudflare 复制的 API 令牌(该值在入库时会自动加密存储,保障安全)。
4. 点击 **「保存」**。
### 第三步:提交证书申请任务
1. 进入左侧导航 **「网站管理」->「TLS证书」**,点击右上角 **「申请证书」**。
2. 在申请表单中填写:
* **证书名称**:自定义名称(如 `wildcard-example-cert`)。
* **主域名**:你申请的主域名(支持通配符,如 `example.com` 或 `*.example.com`)。
* **关联域名**:如有多个,在此处追加(支持通配符,多个域名间用英文逗号分隔)。
* **DNS 账号**:在下拉列表中选择刚才添加的 DNS 账号(如 `cloudflare-main`)。
3. 点击 **「保存并申请」**。
### 第四步:查看申请进度与续期状态
- **查看实时进度**:保存后,系统会向 Asynq 队列投递单证书续期/申请任务(`of_ssl_single_renew`)。你可以进入管理后台的任务或节点日志页面,实时查看每一步(添加 TXT 记录、DNS 记录全球生效探测、ACME 验证、证书颁发落地等)的详细日志。
- **自动续期**:所有通过 ACME 申请的证书都会被系统自动托管。后台的 Scheduler 每日会自动扫描证书有效期,在到期前 30 天自动通过异步任务触发续签,无需任何手动维护。
-29
View File
@@ -1,29 +0,0 @@
# 引用与致谢
OpenFlare 本质上是一个方案整合项目, 在设计与实现过程中借鉴了众多开源项目的优秀理念、架构设计和技术实现。以下是 OpenFlare 在核心底层引擎、安全防护机制以及前后端系统框架等方面所引用的关键开源项目,以及对这些项目及其社区的感谢。
---
### 1. OpenResty
* **项目定位**:基于 Nginx 与 Lua 的高性能 Web 平台。
* **在 OpenFlare 中的作用**:作为全局数据面(Data Plane)的边缘网关。所有的公网 Web 流量均首先由 OpenResty 接收,在此处进行高并发的 HTTPS 握手、WAF 安全规则比对、防 CC 人机验证,并最终执行反向代理转发。
* **项目链接**:[OpenResty 官网](https://openresty.org/)
### 2. FRP (Fast Reverse Proxy)
* **项目定位**:高性能的反向代理应用,专注于内网穿透。
* **在 OpenFlare 中的作用**:作为内网穿透子系统的底层隧道引擎。中继端管理器 `openflare-relay` 负责守护和调度 `frps` 引擎,而内网客户端 `openflared` 则负责在本地自动生成 TOML 配置并守护多路复用 `frpc` 子进程。
* **项目链接**:[fatedier/frp (GitHub)](https://github.com/fatedier/frp)
---
### 3. Anubis (PoW 方案)
* **项目定位**:基于工作量证明(Proof of Work)的轻量级人机验证防护方案。
* **在 OpenFlare 中的作用**:为网关 WAF 提供了核心的**无感防 CC 人机挑战**能力。
---
### 4. gin-template
* **项目定位**:基于 Go Gin 与前端构建的现代化全栈开发脚手架模板。
* **在 OpenFlare 中的作用**:为 OpenFlare 控制面(Server)提供了规范、统一的前后端系统架构雏形。
---
-72
View File
@@ -1,72 +0,0 @@
# 发布第一份配置
你会学到:如何以最简单的方式创建第一条反向代理规则、发布配置版本,并确认 Agent 已经拉取并应用配置。
OpenFlare 的发布链路以“不可变配置版本”为核心。你在管理端修改规则后,需要发布并激活新版本,在线的 Agent 才会自动同步并应用。
---
## 发布前检查
在开始发布前,请确保以下条件已满足:
| 检查项 | 状态要求 |
| --- | --- |
| **Server** | 控制面板已正常启动,且能顺利登录管理端 |
| **Agent** | 至少有一个 Agent 节点处于在线状态(可在「节点管理」中确认) |
| **源站** | 确认你的后端源站服务可从 Agent 宿主机正常访问 |
| **域名/测试** | 域名已完成 DNS 解析,或者准备好在客户端使用本地 hosts / curl 命令行 Host 头进行测试 |
---
## 步骤一:创建首个网站配置
为了快速验证,我们首先部署一个最基础的 HTTP 反代站点:
1. 登录控制面板,进入左侧导航 **「网站管理」->「域名列表」**,点击 **「新增网站」**。
2. 填写域名配置:
* **域名**:输入用于测试的域名(如 `first.example.com`)。
* **绑定证书**:选择不绑定证书(作为 HTTP 快速验证)。
* 点击保存,完成网站登记。
3. 进入左侧导航 **「规则管理」**,点击 **「新增规则」**:
* **规则名称**:输入简易标识(如 `first-app-route`)。
* **域名匹配**:填入你的测试域名(如 `first.example.com`)。
* 在下方 **「反向代理」** 选项卡中,配置 **源站类型** 为「标准反代」 (Direct)。
* **上游地址**:填写后端服务地址(如测试专用的 `http://httpbin.org`)。
* 点击保存创建规则。
> [!TIP]
> **关于 HTTPS 与证书准备**
> 本节仅引导快速部署基础 HTTP 规则。若你需要导入已有的 SSL 证书或通过 ACME 协议向 Let's Encrypt 自动申请证书并开启 443 端口 HTTPS 代理,请前往 [新建反代配置](./proxy-config.md) 查阅详细步骤。
---
## 步骤二:预览并发布配置版本
新增的网站配置仍保存在 Server 的数据库中,处于草稿状态,需要通过发布版本分发到数据面:
1. 点击控制面板右上角的 **「配置预览」** 按钮,系统会展示本次新增路由的物理配置文件 Diff 差异。
2. 确认渲染出的配置内容正确无误后,点击 **「发布并激活」**。
3. 控制面将生成一个唯一的配置版本号(格式为 `YYYYMMDD-NNN`)。
---
## 步骤三:验证 Agent 生效状态
发布成功后,控制面会立即通过 WebSocket 通知在线 Agent(若 WebSocket 离线,则会在 Agent 的心跳中作为差分感知):
1. **管理端验证**:进入「节点管理」-> 点击节点进入详情,检查**当前版本号**是否已成功变为刚刚发布的最新激活版本,且「应用记录」显示为成功。
2. **边缘节点验证**:你可以在 Agent 节点宿主机上通过日志检查应用情况:
```bash
# 如果是 Docker 部署的 Agent
docker logs openflare-agent
# 如果是本地 systemd 部署的 Agent
journalctl -u openflare-agent -n 50 --no-pager
```
3. **连通性测试**:
在客户端电脑上,使用 `curl` 携带测试 Host 请求 Agent 节点的 IP 地址进行最终验证:
```bash
curl -I -H "Host: first.example.com" http://AGENT_NODE_IP
```
若返回的状态码与后端源站响应一致,即代表你的第一条反代规则已成功在边缘节点落地生效!
-49
View File
@@ -1,49 +0,0 @@
# 指南
你会学到:OpenFlare 文档如何组织、首次运行应该读哪些页面,以及部署、使用、排查和开发分别从哪里开始。
OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站配置、配置版本发布、Agent 节点同步、TLS 证书和基础观测放到一个管理端中,适合单团队或单组织管理多台代理节点。
## 推荐阅读路径
如果你第一次接触 OpenFlare,按下面顺序阅读:
1. [快速开始](./quick-start.md):用 Docker Compose 启动 Server,登录管理端,并接入第一个 Agent。
2. [发布第一份配置](./first-site.md):快速新建一条最基础的 HTTP 反代站点规则,并验证节点生效状态。
3. [新建反代配置](./proxy-config.md):一步一步了解如何从证书导入与申请开始,配置 HTTPS 加密与上游源站管理。
4. [Zone 域名迁移](./zone-domain-migration.md):从旧托管域名/路由内嵌域名升级到 Zone 模型(goose 自动导入),含备份、验收与回滚说明。
5. [Pages 静态托管使用](./pages-usage.md):了解静态项目 ZIP 上传限制、SPA Fallback、以及内置 API 反向代理配置。
6. [内网穿透与隧道使用](./tunnel-usage.md):部署 Relay 与 Client,实现安全、无公网 IP 反向穿透。
7. [WAF 安全防护使用](./waf-usage.md):配置 WAF 规则组,掌握 IP 黑白名单、自动/订阅 IP 组、地域限制与 PoW CC 防护。
8. [WAF 自动 IP 组语法](./waf-ip-group-expr.md):编写自动 IP 组 Expr 规则,了解关键字含义和预设规则。
9. [Uptime Kuma 监控同步](./uptime-kuma.md):配置并使用 Uptime Kuma 自动差分同步和监控范围控制。
10. [SSO 登录配置](./sso.md):配置 GitHub 或 OIDC 实现第三方单点登录 (SSO) 接入。
11. [故障排查](./troubleshooting.md):按症状排查登录、数据库、节点同步、OpenResty 应用和前端构建问题。
12. [引用与致谢](./credits.md):查看系统依赖的优秀开源项目与社区致谢清单。
## 按角色查找
| 你想做什么 | 推荐入口 |
| --- | --- |
| 5 分钟内跑起管理端 | [快速开始](./quick-start.md) |
| 发布第一条反向代理配置 | [发布第一份配置](./first-site.md) |
| 配置域名证书与高级反代 | [新建反代配置](./proxy-config.md) |
| 托管单页应用或静态网站 | [Pages 静态托管使用](./pages-usage.md) |
| 配置内网穿透映射 | [内网穿透与隧道使用](./tunnel-usage.md) |
| 配置防 CC 与 IP 组拦截 | [WAF 安全防护使用](./waf-usage.md) |
| 编写自动 IP 组规则 | [WAF 自动 IP 组语法](./waf-ip-group-expr.md) |
| 自动同步监测站点状态 | [Uptime Kuma 监控同步](./uptime-kuma.md) |
| 接入或重装节点 Agent | [接入 Agent](../deployment/agent.md) |
| 从源码启动 Server | [启动 Server](../deployment/server.md) |
| 配置 GitHub 或 OIDC 登录 | [SSO 登录配置](./sso.md) |
| 升级 Server 或 Agent | [升级与维护](../deployment/upgrade.md) |
| 理解架构和发布模型 | [系统架构](../design/architecture.md) 与 [Agent 与发布模型](../design/agent-design.md) |
| 查看开源引用与致谢 | [引用与致谢](./credits.md) |
## 文档分区
`guide/` 面向使用者和部署者,提供从安装到日常操作的可执行步骤。
`reference/` 收敛稳定事实,例如配置字段、命令、API 响应约定和仓库结构。
`design/` 面向维护者和贡献者,描述产品边界、系统架构、Agent 与发布模型和工程约束。新增能力或改变边界前,应先更新对应设计文档。
-116
View File
@@ -1,116 +0,0 @@
# Pages 静态托管使用
你会学到:如何通过本地上传、Remote URL 或公开 GitHub Release asset 部署预构建静态站点,配置 SPA Fallback 与 API 反向代理,并安全地检查更新、自动发布和回滚。
---
## 核心机制与页面结构
OpenFlare Pages 受 Cloudflare Pages 的 Direct Upload 与部署历史交互启发,但当前处理的是**预构建产物**,不是仓库源码构建。项目详情按“当前生产部署 → 部署源 → 部署历史”组织:来源配置可以变化,已经创建的 deployment 保持不可变。
```text
本地上传 ─> 统一校验 / upload.Ingest ─> 新 candidate ─> 管理员显式激活 ─┐
Remote URL ── Server 受限下载 ────────┐ │
GitHub Release asset ─ Server 解析 ───┴─> create/load deployment ─────┤
└─> source sync 原子激活 ────────┘
|
v
Agent 按项目 latest 拉取
|
v
OpenResty 本地静态服务
```
外部 URL、GitHub 元数据和自动检查都只由 Server 处理。Agent 只从控制面拉取当前激活的部署包,不接收外部来源凭据,也不执行 `git clone`、依赖安装或构建命令。
## 第一步:创建项目
1. 登录管理端,进入 **「Pages」**,点击 **「创建项目」**。
2. 填写项目名称与唯一 Slug。
3. 配置内容入口:
* **入口文件名**:默认 `index.html`。
* **静态资源根路径(RootDir)**:产物位于 `dist/` 等子目录时填写该相对路径;产物就在归档根目录时留空。
4. 按需设置 SPA Fallback 与 API 代理。RootDir 和入口文件是项目级配置,会统一应用于所有来源。
## 第二步:选择部署源
### 1. 手动上传
不配置持久来源时,项目保持手动模式。点击 **「上传部署包」** 选择预构建归档;上传成功会创建一条候选 deployment,再从部署历史中显式激活。重复上传不会修改已有 deployment。
支持 `zip`、`tar.gz` / `tgz`、`tar.xz` / `txz`、`tar.bz2` / `tbz2`、`tar` 与 `7z`。
### 2. Remote URL
在部署源卡片中选择 **Remote URL**,填写 HTTP(S) 地址并选择网络策略:
* **public**:默认策略,拒绝 loopback、私网、链路本地地址、DNS rebinding、自签 TLS,以及重定向到非公网目标。
* **trusted_internal**:仅用于明确受信的内网或自签服务;保存前需要再次确认风险。
保存后地址只以脱敏形式展示。编辑其它配置时无需重新填写;只有选择更换地址时才提交新 URL。Remote 来源只提供 **「同步并发布」**:每次由 Server 下载、校验并原子激活,不支持“检查更新”、定时检查或自动更新。
### 3. GitHub Release
GitHub 来源仅支持公开 `github.com` 仓库。填写:
* `https://github.com/{owner}/{repo}` 格式的仓库地址;
* **最新 Release** 或 **固定 Tag**;
* 精确、区分大小写的 Release Asset 文件名,默认 `dist.zip`。
两种选择都可手动 **「检查更新」** 和 **「同步并发布」**。区别如下:
* **latest**:可设置 5~1440 分钟检查间隔,默认 60 分钟;自动更新默认关闭。开启后,scanner 发现新 revision 才会异步同步并发布。
* **tag**:只支持管理员手动检查和同步,不参与定时 scanner。
“检查更新”只解析 Release/asset 并更新版本游标,不下载部署包;“同步并发布”才会下载、校验、创建或复用 deployment 并激活。如果同一个 Release 下的 asset 被替换,来源会进入 **「需要确认」**,必须确认页面显示的精确 revision 后才能发布,避免静默覆盖。
GitHub Release 在这里是预构建产物源,不等同于连接代码仓库自动构建。未来仓库集成会使用独立的 `git_repository` 来源和 Server build executor,再把构建产物送入同一部署管线。
### 4. 切换或删除来源
可以在手动、Remote 和 GitHub Release 之间切换。修改或删除来源不会删除当前生产部署和历史 deployment;切回手动模式后可继续上传并显式激活。
## 部署包安全限制
部署包必须满足以下约束:
* 压缩包大小由系统配置 `pages_max_package_size_mb` 控制,默认 100 MiB,可配置 1~2048 MiB。
* 展开后的单文件和总量上限为“包大小上限 × 4”,且最低为 100 MiB;最多 1,000 个常规文件。
* 控制面会流式读取常规文件体,核对声明大小与实际字节,并校验项目入口文件。
* 归档中的绝对路径、`..` 路径逃逸、软链接、硬链接和特殊文件都会被拒绝。
Agent 下载时还会执行 SHA-256、真实响应字节上限、解压后文件数与总大小复核;失败不会切换现有 `current`。
## 第三步:配置高级路由规则
### 1. SPA Fallback
使用 React Router、Vue Router 等前端路由时,开启 **「SPA Fallback」** 并设置入口路径(通常为 `/index.html`)。访客直接访问不存在的物理路径时,OpenResty 会回退到入口文件交由前端路由处理。
### 2. API 反向代理
Pages 可在同一域名下把指定前缀转发到后端 API:
* **APIProxyPath**:匹配前缀,例如 `/api`。
* **APIProxyPass**:后端地址,例如 `http://10.0.0.5:8080`。
* **APIProxyRewrite**:可选的路径重写规则。
匹配 API 前缀的请求走反向代理,其余请求继续由静态站点处理。
## 第四步:绑定路由并首次发布
1. 创建或编辑一条代理规则。
2. 将源站类型设为 **Pages**,并选择 Pages **项目**。
3. 预览配置后发布并激活。
路由绑定的是稳定的项目 ID,不是某个 deployment。首次发布让 Agent 获得项目锚点;此后本地上传、来源同步、自动更新或人工回滚只会改变项目的 active deployment,Agent 会通过 latest hash 对账收敛,无需重新发布主配置。
## 运维、状态与回滚
* 来源卡片展示最近检查/同步、已发现与已应用 revision、下次检查和安全错误。检查或同步任务运行时,页面会轮询任务状态;latest 空闲时只在接近检查时间时低频刷新。
* 自动更新失败不会替换旧 active deployment;单个来源失败也不会阻塞 scanner 处理其它项目。
* 在部署历史中激活其它 deployment 即完成人工回滚。系统会 fence 在途来源任务,并关闭该来源的自动更新,避免下一轮 latest 又覆盖人工选择;重复激活当前版本是 no-op。
* Agent 下载到临时文件并校验 SHA-256,安全解压后原子切换 `current`。任一步失败都保留旧内容,多项目对账时单项目失败不影响其它项目。
> [!TIP]
> 关于来源状态机、自动 scanner、上传补偿、不可变部署和 Agent 原子切换,请参阅 [Pages 静态托管设计](../design/pages-design.md)。
-86
View File
@@ -1,86 +0,0 @@
# 新建反代配置
你会学到:如何一步一步在 OpenFlare 中从零新建并发布一个反向代理网站配置。本指南将指导你如何完成证书导入与申请、源站定义、路由规则配置、版本发布以及连通性验证。
---
## 推荐操作流程
在网关控制面中,建议遵循以下步骤新增反代规则:
```text
[ 步骤 1. 证书管理 ] ──► [ 步骤 2. 源站定义 (可选) ] ──► [ 步骤 3. 新增网站配置 ]
│
[ 步骤 5. 验证访问 ] ◄── [ 步骤 4. 发布与激活版本 ] ◄───────────────┘
```
---
## 第一步:证书准备
在使用 HTTPS 安全加密流量前,你需要先准备好对应的 TLS 证书(支持手动导入已有证书,或通过 DNS 验证自动向 CA 申请并托管续期)。
为了保持反代配置指南的简洁,证书相关的详细操作(包括如何在 Cloudflare 申请专用 DNS API Token)已独立拆分为专属指南。请先前往 **[TLS 证书与自动续期](./certificates.md)** 完成证书准备,然后回到这里继续下一步。
---
## 第二步:准备上游源站(可选)
源站(Origin)代表被代理的后端真实服务地址。虽然在新建网站时可以直接填写 IP,但推荐先在源站库中进行注册,以便后续复用与维护:
1. 进入左侧导航 **「网站管理」->「源站地址」**,点击 **「创建源站」**。
2. 填写源站名称(如 `production-api`)。
3. 填入合法的上游地址(如 `http://10.0.0.10:8080`),点击保存。
---
## 第三步:新建网站配置
证书和源站就绪后,即可创建核心网站代理路由:
1. 进入左侧导航 **「网站管理」->「域名列表」**,点击 **「新增网站」**:
* **域名**:输入该站点绑定的域名。
* **绑定证书**:选择第一步准备或申请好的证书。
2. 配置请求路由规则:进入 **「规则管理」** 页面,点击 **「新增规则」** 或编辑已有规则:
* **规则名称**:输入规则的唯一简易标识(如 `app-portal-route`)。
* **域名匹配**:填入对应的域名(支持通配符或精确域名,需与上面登记的域名一致)。
* 在下方 **「反向代理」** 选项卡下,选择源站类型为 **「标准反代」**。
* **源站选择**:从下拉框中选择第二步创建的源站;或者选择手动输入并填入 `http://10.0.0.20:9000`。
3. 点击保存创建配置。
---
## 第四步:发布并生效配置
你在管理端新增的网站配置仅保存在 Server 数据库中,**不会立即生效**。必须生成配置版本快照并分发到 Agent 边缘节点:
1. 点击控制面板右上角的 **「配置预览」** 按钮。
2. 检查配置文件的 Diff 差异,确认你刚刚新增的 `server` 块以及证书绑定规则无误。
3. 点击 **「发布并激活」** 按钮。
4. **Agent 落地机制**:
* 数据面的 Agent 节点在心跳中发现激活的版本 Checksum 变更,会自动拉取完整的 OpenResty 配置文件和证书包到本地。
* 自动在本地执行配置校验(类似于 `openresty -t`),确认无语法错误后,执行平滑重载(`reload`)。
* *如果重载或校验失败,Agent 会安全阻断并回滚至上一稳定版本,保证节点高可用。*
---
## 第五步:连通性与回滚验证
### 1. 验证访问
你可以通过以下方式验证新配置是否生效:
* **浏览器访问**:直接在浏览器输入 `https://your-domain.com` 查看是否成功代理后端。
* **命令行验证**(推荐):使用 `curl` 探测:
```bash
curl -I https://your-domain.com
```
* **绕过 DNS 校验**:若你的域名尚未正式解析,可以临时指定 `Host` 请求头请求 Agent 节点物理 IP:
```bash
curl -I -H "Host: your-domain.com" https://AGENT_NODE_IP --insecure
```
### 2. 一键秒级回滚
如果发布的新配置导致了线上业务异常:
1. 导航至左侧 **「配置版本」** 菜单。
2. 在历史列表中找到发布前的上一个稳定版本。
3. 点击 **「激活此版本」**。
4. 所有在线 Agent 节点将在秒级自动重载回历史配置,实现秒级避险。
-272
View File
@@ -1,272 +0,0 @@
# 快速开始
你会学到:如何用 Docker Compose 启动 OpenFlare Server、完成首次登录、接入第一个 Agent,并验证一份配置是否已经发布到节点。
OpenFlare 的最小运行单元包含:
| 组件 | 职责 |
| --- | --- |
| Server | 管理端 UI、管理 API、Agent API、配置渲染、版本发布与状态存储 |
| Agent | 运行在代理节点上,拉取配置、写入 OpenResty、执行校验与 reload |
| OpenResty | 实际接收流量并反向代理到源站 |
Agent 统一通过 OpenResty 二进制控制运行时。本地部署需要节点上已有 `openresty` 可执行文件;Docker 部署可直接运行内置 OpenResty 的 Agent 镜像。
## 环境要求
| 项目 | 要求 |
| --- | --- |
| Docker / Docker Compose | 用于启动 Server 及其依赖的 PostgreSQL、Redis 和 ClickHouse 容器;如采用 Docker Agent,也用于运行 Agent |
| OpenResty | 本地安装 Agent 时需要可执行 `openresty`,或在安装脚本中指定路径 |
| 可访问端口 | Server 默认监听 `3000`,Agent 节点需要能访问 Server 地址 |
| 浏览器 | 用于访问管理端 |
- **Docker**:`20.10.0+`
- **Docker Compose**:`2.0.0+`
---
## 1. 启动 Server
为了保证异步任务队列(Asynq 框架)及可观测流量看板功能完整运行,快速开始推荐采用 **PostgreSQL + Redis + ClickHouse** 经典单机版编排。
先拉取 ClickHouse 服务端性能配置到 `./config/clickhouse`,并以单文件方式挂载:
```bash
mkdir -p ./config/clickhouse
curl -fsSL -o ./config/clickhouse/performance.xml \
https://raw.githubusercontent.com/Rain-kl/OpenFlare/refs/heads/main/config/clickhouse/performance.xml
```
在空目录中创建 `docker-compose.yaml`:
```yaml
version: '3.8'
services:
openflare:
image: ghcr.io/rain-kl/openflare:latest
container_name: openflare-server
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- openflare_uploads:/app/uploads
environment:
TZ: Asia/Shanghai
APP_SESSION_SECRET: 'replace-with-a-long-random-string' # 生产环境请替换为长随机字符串
DB_ENABLED: "true"
DB_HOST: "postgres"
DB_PORT: "5432"
DB_USERNAME: "${DB_USERNAME:-openflare}"
DB_PASSWORD: "${DB_PASSWORD:-replace-with-strong-password}"
DB_NAME: "${DB_NAME:-openflare}"
REDIS_ENABLED: "true"
REDIS_ADDR: "redis:6379"
CLICKHOUSE_ENABLED: "true"
CLICKHOUSE_HOST: "clickhouse:9000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-openflare}
POSTGRES_USER: ${DB_USERNAME:-openflare}
POSTGRES_PASSWORD: ${DB_PASSWORD:-replace-with-strong-password}
volumes:
- openflare_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-openflare} -d ${DB_NAME:-openflare}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: valkey/valkey:8.0-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- openflare_redis_data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
clickhouse:
image: clickhouse/clickhouse-server:25.3-alpine
restart: unless-stopped
environment:
CLICKHOUSE_DB: openflare
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
TZ: Asia/Shanghai
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- openflare_clickhouse_data:/var/lib/clickhouse
- ./config/clickhouse/performance.xml:/etc/clickhouse-server/config.d/performance.xml:ro
healthcheck:
test: ["CMD", "clickhouse-client", "--user", "default", "--password", "${CLICKHOUSE_PASSWORD:-replace-with-clickhouse-password}", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
openflare_uploads:
openflare_postgres_data:
openflare_redis_data:
openflare_clickhouse_data:
```
启动服务:
```bash
docker compose up -d
```
确认容器已经运行:
```bash
docker compose ps
docker compose logs -f openflare
```
看到 `server listening` 且 `openflare-server` 容器状态为 running 后,使用浏览器打开:
```text
http://localhost:3000
```
默认账号:
| 用户名 | 密码 |
| --- | --- |
| `admin` | `12345678` |
> [!WARNING]
> 为了你的系统安全,首次登录后请立即修改默认密码。
---
## 2. 准备 Agent Token
Agent 可以用两类凭证接入:
| 凭证 | 适用场景 |
| --- | --- |
| `discovery_token` | 首次自动注册节点,由 Server 换成节点专属 Token |
| `agent_token` | 已经在管理端创建或分配节点,直接使用节点专属 Token |
在管理端准备其中一种凭证后,进入下一步。
- **`discovery_token`** 获取菜单路径:「系统设置」 (Settings) -> 「OpenFlare」选项卡 -> 「自动注册」凭证
- **`agent_token`** 获取菜单路径:在「节点管理」中创建节点后,点击进入节点详情页即可查看到对应的专属 Token。
---
## 3. 安装/运行 Agent
Agent 部署方式推荐使用 Docker 部署(即直接运行内置 OpenResty 的 Agent 镜像);亦支持通过安装脚本将 Agent 部署在本地宿主机上。
### 方式 A:Docker 运行 Agent(推荐)
在代理节点上直接运行 Agent 镜像:
```bash
docker pull ghcr.io/rain-kl/openflare-agent:latest
docker rm -f openflare-agent 2>/dev/null || true
docker run -d --name openflare-agent --restart unless-stopped \
-p 80:80 -p 443:443/tcp -p 443:443/udp \
-v openflare-agent-pages:/data/var/lib/openflare/pages \
-e OPENFLARE_SERVER_URL=http://your-server:3000 \
-e OPENFLARE_AGENT_TOKEN=YOUR_AGENT_TOKEN \
ghcr.io/rain-kl/openflare-agent:latest
```
### 方式 B:执行安装脚本(本地部署)
在代理节点上执行安装脚本。
使用 `discovery_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--discovery-token YOUR_DISCOVERY_TOKEN
```
使用节点专属 `agent_token`:
```bash
curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh | bash -s -- \
--server-url http://your-server:3000 \
--agent-token YOUR_AGENT_TOKEN
```
脚本默认会:
| 项目 | 默认值 |
| --- | --- |
| 安装目录 | `/opt/openflare-agent` |
| 配置文件 | `/opt/openflare-agent/agent.json` |
| systemd 服务 | `openflare-agent.service` |
| OpenResty 路径 | 未指定时自动查找 `openresty` |
确认 Agent 服务状态:
```bash
systemctl status openflare-agent
journalctl -u openflare-agent -f
```
如果没有 systemd,脚本会输出手动启动命令。
---
## 4. 后续步骤
完成控制面板启动和 Agent 节点接入后,你已经成功搭建好了 OpenFlare 网关的基础运行环境。接下来你可以按顺序继续阅读以下两份指南,开始部署你的第一个反代站点:
1. **发布第一个网站**:
* 请参阅 [发布第一份配置](./first-site.md)。它将引导你以最简单的方式(使用纯 HTTP)发布你的第一条代理规则,并验证节点落地状态。
2. **完整配置反向代理(HTTPS 与源站管理)**:
* 请参阅 [新建反代配置](./proxy-config.md)。它将指导你从证书导入与申请开始,配置域名 HTTPS 证书绑定、源站管理并预览发布。
---
## 常见失败原因
| 现象 | 排查方向 |
| --- | --- |
| 浏览器打不开管理端 | 确认 `docker compose ps` 中 Server 正在运行,宿主机 `3000` 端口没有被占用 |
| 登录后数据无法保存/提示报错 | 检查 PostgreSQL 容器健康状态,以及 `DB_PASSWORD` / 密码等连接参数是否一致 |
| Agent 无法注册 | 确认 Agent 节点能访问 `--server-url`,并检查 Token 是否填错或已失效 |
| Agent 在线但没有应用配置 | 确认网站配置已启用,并且已经发布并激活版本 |
| OpenResty 应用失败 | 查看节点应用记录和 `journalctl -u openflare-agent`,重点检查域名、证书、上游地址和端口占用 |
更多排查路径见 [故障排查](./troubleshooting.md)。
---
## 进阶部署指引
当您完成快速开始并熟悉了 OpenFlare 的基本操作后,可以阅读以下进阶部署文档,将各组件投入到正式生产环境中:
* **Server 生产部署**:阅读 [启动 Server](../deployment/server.md) 了解如何从源码构建前端、配置系统环境变量及使用 Docker Compose 运行。
* **Agent 生产接入**:阅读 [部署 Agent](../deployment/agent.md) 了解基于 systemd 的服务管理、详细本地配置文件字段及故障排查。
* **内网穿透中继端部署**:阅读 [部署 Relay](../deployment/relay.md) 了解如何为穿透隧道配置公网中继节点(frps)。
* **内网穿透客户端部署**:阅读 [部署 OpenFlared](../deployment/openflared.md) 了解如何在内网服务器侧运行穿透守护客户端(frpc)。
* **生产部署拓扑参考**:阅读 [部署说明](../deployment/deployment.md) 了解生产高可用拓扑和整体网络规划。
* **系统升级与日常维护**:阅读 [升级与维护](../deployment/upgrade.md) 了解如何平滑升级 Server 和各代理节点 Agent。
-106
View File
@@ -1,106 +0,0 @@
# SSO 登录配置
你会学到:如何为 OpenFlare 配置 GitHub OAuth 或标准 OIDC 登录入口,如何填写回调地址,以及第三方账号如何绑定本地用户。
OpenFlare 支持通过认证源配置第三方登录入口。当前支持 GitHub OAuth 与标准 OIDC Provider,例如 Logto、authentik、Keycloak、Casdoor 等。
认证源配置完成并启用后,会显示在登录页的第三方账号登录区域。用户可以通过第三方账号登录,也可以在已登录状态下把第三方账号绑定到当前本地账号。
## 使用前准备
你需要先准备:
| 项目 | 说明 |
| --- | --- |
| OpenFlare 访问地址 | 用户浏览器实际访问的地址,例如 `https://openflare.example.com` |
| 认证源名称 | OpenFlare 内部唯一标识,例如 `github`、`company-oidc` |
| Client ID | 第三方平台创建应用后提供 |
| Client Secret | 第三方平台创建应用后提供 |
| OIDC Discovery URL | 仅 OIDC 需要,例如 `https://idp.example.com/.well-known/openid-configuration` |
**确认系统设置->通用设置->服务器地址能正确和域名匹配**
认证源名称只能包含字母、数字、短横线或下划线,并且必须以字母或数字开头。认证源名称会出现在回调地址中,保存后如需修改名称,也必须同步修改第三方平台中的回调地址。
## 回调地址
第三方平台中的 Redirect URI / Callback URL 填写格式为:
```text
<OpenFlare 访问地址>/oauth/<认证源名称>
```
示例:
```text
https://openflare.example.com/oauth/github
https://openflare.example.com/oauth/company-oidc
```
在管理端新增或修改认证源时,表单会根据当前浏览器访问地址和你输入的认证源名称自动显示应填写的回调地址。
## 配置 GitHub 登录
1. 在 GitHub 创建 OAuth App。
2. `Homepage URL` 填写 OpenFlare 访问地址。
3. `Authorization callback URL` 填写 OpenFlare 显示的回调地址,例如 `https://openflare.example.com/oauth/github`。
4. 复制 GitHub 提供的 Client ID 和 Client Secret。
5. 登录 OpenFlare 管理端,进入左侧导航 **「系统设置」** (Settings),选择 **「安全设置」** 选项卡,在 **「认证源管理」** 栏目中进行配置。
6. 新增认证源,类型选择 `GitHub`。
7. 填写认证源名称、展示名称、Client ID、Client Secret。
8. Scope 默认使用 `user:email`,通常无需修改。
9. 保存并启用认证源。
启用后,登录页会显示对应的 GitHub 登录按钮。
## 配置 OIDC 登录
1. 在 OIDC Provider 中创建应用或客户端。
2. 应用类型选择 Web / Confidential Client。
3. Redirect URI / Callback URL 填写 OpenFlare 显示的回调地址,例如 `https://openflare.example.com/oauth/company-oidc`。
4. 复制 Client ID 和 Client Secret。
5. 获取 Provider 的 Discovery URL,通常以 `/.well-known/openid-configuration` 结尾。
6. 登录 OpenFlare 管理端,进入左侧导航 **「系统设置」** (Settings),选择 **「安全设置」** 选项卡,在 **「认证源管理」** 栏目中进行配置。
7. 新增认证源,类型选择 `OIDC`。
8. 填写认证源名称、展示名称、Client ID、Client Secret、OIDC Discovery URL。
9. Scope 默认使用 `openid profile email`。如果 Provider 限制了 scope,请按 Provider 允许的值调整。
10. 保存并启用认证源。
启用后,登录页会显示对应的 OIDC 登录按钮。
## 登录与绑定行为
第三方账号回到 OpenFlare 后按以下规则处理:
| 场景 | 行为 |
| --- | --- |
| 第三方账号已绑定本地用户 | 直接登录 |
| 用户已登录并发起第三方授权 | 绑定到当前本地用户 |
| 第三方账号未绑定,且允许注册 | 自动创建普通用户并绑定 |
| 第三方账号未绑定,且关闭注册 | 要求输入已有本地账号密码完成绑定 |
如果希望只允许已有用户使用 SSO,可以关闭用户注册。未绑定的第三方账号会进入绑定已有账号流程。
## 修改认证源
修改认证源时,Client Secret 输入框留空表示保留已有密钥;填写新值则会覆盖保存。
如果修改了认证源名称,回调地址也会随之变化。你必须到第三方平台同步修改 Redirect URI / Callback URL,否则第三方平台会拒绝回调或返回错误。
## 常见问题
### 返回 `invalid_scope`
说明第三方平台不允许当前配置的 Scope。OIDC 默认 Scope 是 `openid profile email`,GitHub 默认 Scope 是 `user:email`。请到认证源编辑页调整 Scope,或在第三方平台放行对应 Scope。
### 提示回调地址不匹配
检查第三方平台中配置的 Redirect URI / Callback URL 是否与 OpenFlare 表单提示完全一致。协议、域名、端口和路径都必须一致。
### 登录页没有显示第三方登录按钮
检查认证源是否已启用,并确认 Client ID 和 Client Secret 已保存。启用认证源前,OpenFlare 会校验这些字段。
### 已经保存 Client Secret,但列表不显示明文
这是预期行为。OpenFlare 不会通过 API 回显 Client Secret,只显示该密钥是否已配置。
-249
View File
@@ -1,249 +0,0 @@
# 故障排查
你会学到:如何按症状排查 OpenFlare Server、数据库、登录、Agent、OpenResty、配置发布和前端构建问题。
排查时先确认问题发生在哪一层:浏览器、Server、数据库、Agent、OpenResty、源站或 DNS。OpenFlare 的配置不会直接在线写入所有节点,只有激活版本变化后,Agent 才会在 heartbeat 中发现并应用。
## 快速定位
| 现象 | 先看哪里 |
| --- | --- |
| 管理端打不开 | Server 容器或进程日志、端口监听 |
| 登录异常 | 默认账号、OPENFLARE_TOKEN、浏览器请求、Server 日志 |
| 数据无法保存 | 数据库连接、SQLite 文件权限、PostgreSQL 健康状态 |
| Agent 离线 | Agent 日志、Token、Server 地址、网络连通性 |
| 发布后节点未更新 | 激活版本、节点 heartbeat、应用记录 |
| OpenResty 应用失败 | 应用记录、Agent 日志、证书、上游地址、端口占用 |
| 访问分析无数据 | OpenResty 容器状态、观测端口、Agent 补报日志 |
## Server 无法启动
1. 查看日志:
```bash
docker compose logs -n 200 openflare
```
源码运行时查看终端输出。
2. 检查端口占用:
```bash
lsof -i :3000
```
3. 如果使用 PostgreSQL,确认数据库健康:
```bash
docker compose ps postgres
docker compose logs -n 100 postgres
```
4. 如果使用 SQLite,确认数据库文件目录可写:
```bash
ls -ld "$(dirname /path/to/openflare.db)"
```
常见原因:
| 日志或现象 | 处理 |
| --- | --- |
| 数据库连接失败 | 检查 `DSN` 中用户名、密码、主机、端口、库名和 `sslmode` |
| SQLite 无法创建文件 | 检查 `SQLITE_PATH` 所在目录是否存在且可写 |
| 端口被占用 | 修改 `PORT` 或 `--port`,或停止占用端口的进程 |
## 管理端打不开或空白
1. 确认 Server 正在监听:
```bash
curl -I http://127.0.0.1:3000
```
2. 如果是源码运行,确认已经构建前端静态产物:
```bash
cd frontend
pnpm build
```
3. 检查浏览器访问地址是否与反向代理配置一致。
4. 如果通过前端开发服务器访问,确认后端代理地址:
```bash
cd frontend
NEXT_DEV_BACKEND_URL=http://127.0.0.1:3000 pnpm dev
```
## 默认账号无法登录
默认账号是 `admin` / `12345678`。首次登录后如果已经修改密码,应使用修改后的密码。
排查步骤:
1. 确认连接的是预期数据库,避免 `SQLITE_PATH` 或 `DSN` 指向了另一个环境。
2. 查看 Server 日志中使用的是 `sqlite` 还是 `postgres`。
3. 在浏览器开发者工具中确认管理端 API 请求已正确携带 Session Cookie。
4. 清理浏览器缓存及 Cookie 后重新登录。
### 应急重置管理员密码
如果忘记了 `admin` 账户的密码,可以通过直接更新数据库中的密码哈希值将其重置为 `12345678`(登录后请务必立即修改):
#### 1. 若使用 SQLite 数据库
停止 Server 运行,使用 sqlite3 客户端打开数据库文件:
```bash
sqlite3 /path/to/openflare.db
```
执行以下 SQL 语句:
```sql
UPDATE users SET password = '$2a$10$eXpE9i/6S3gPT94/G0mu0.B8ser66ARETFz5NWYSYcrQ4JmtSrMXu' WHERE username = 'admin';
```
输入 `.exit` 退出并重新启动 Server。
#### 2. 若使用 PostgreSQL 数据库
通过您的数据库连接工具(如 psql、pgAdmin 或 DBeaver)连接到 PostgreSQL 实例,选择对应的 `openflare` 数据库,执行以下 SQL 语句:
```sql
UPDATE users SET password = '$2a$10$eXpE9i/6S3gPT94/G0mu0.B8ser66ARETFz5NWYSYcrQ4JmtSrMXu' WHERE username = 'admin';
```
执行成功后即可使用默认密码 `12345678` 重新登录管理后台。
## Agent 无法注册或一直离线
在 Agent 节点执行:
```bash
curl -I http://your-server:3000
```
查看 Agent 日志:
```bash
journalctl -u openflare-agent -n 200 --no-pager
```
检查配置文件:
```bash
sed -n '1,160p' /opt/openflare-agent/agent.json
```
重点确认:
| 配置 | 说明 |
| --- | --- |
| `server_url` | 必须是 Agent 节点能访问的 Server 地址 |
| `agent_token` / `discovery_token` | 至少填写一个 |
| `heartbeat_interval` | 支持毫秒整数或 Go duration 字符串 |
| `request_timeout` | 网络较慢时可适当增大 |
如果日志提示 Token 无效,重新在管理端准备 Token 并更新 `agent.json`,然后重启:
```bash
systemctl restart openflare-agent
```
## 发布后节点没有应用新版本
按顺序检查:
1. 版本页面中是否已经激活目标版本。
2. 节点是否在线,最近心跳时间是否更新。
3. 应用记录中是否有目标版本的成功、警告或失败记录。
4. 网站配置是否启用;未启用的网站不会参与发布渲染。
5. Agent 日志是否出现拉取、校验、reload 或回滚信息。
查看 Agent 日志:
```bash
journalctl -u openflare-agent -f
```
注意:某个目标 `version + checksum` 一旦应用失败并回退,Agent 会在本地状态中阻断该目标重复应用。修正配置后需要重新发布生成新的 checksum,或激活旧版本回滚。
如果这是 Agent 首次应用配置,且本地没有历史 `nginx.conf` 可回滚,失败目标仍会被阻断,但 Agent 会尝试进入安全兜底运行态。此时应用记录和 Agent 日志会包含 `fallback runtime started`,OpenResty 对外只监听 `80` 端口并统一返回 `503` 与 `OpenFlare: No Valid Configuration`,同时保留本地 `stub_status` 健康检查入口。修正配置并重新发布新版本后,Agent 会覆盖兜底配置并恢复正常代理。
## OpenResty 应用失败
常见原因:
| 原因 | 排查 |
| --- | --- |
| 域名或 server 块冲突 | 检查同一域名是否被多个网站配置使用 |
| 上游地址不合法 | 确认所有上游都是 `http://` 或 `https://` |
| 多上游格式不符合约束 | 多上游必须是纯 `scheme://host[:port]` |
| 证书缺失或路径错误 | 检查域名是否绑定证书,以及 Agent 证书目录是否可写 |
| 端口被占用 | 检查本机 `80`、`443` 端口 |
OpenResty 配置校验:
```bash
openresty -t -c /path/to/openflare/data/etc/nginx/nginx.conf
```
OpenResty 运行状态:
```bash
ps aux | grep openresty
```
Agent 周期性健康检查通过本地 `http://127.0.0.1:<openresty_observability_port>/openflare/stub_status` 判断 OpenResty 是否存活,不会反复执行 `openresty -t`。如果节点被标记为 unhealthy,优先确认该本地观测端口是否正在监听;如果只在应用配置时出现 `host not found in upstream`,说明失败来自配置校验或 reload,而不是周期性健康探针。
实际二进制路径和主配置路径以 `agent.json` 中的 `openresty_path` 与 `main_config_path` 为准。
## HTTPS 不生效
1. 确认证书已经上传或托管。
2. 确认网站配置中对应域名已经绑定证书。
3. 确认发布并激活了新版本。
4. 查看应用记录是否成功。
5. 用 `curl` 查看证书和状态码:
```bash
curl -Iv https://your-domain
```
没有绑定证书的域名不会被自动加入 HTTPS 配置,这是预期行为。
## 访问分析没有数据
1. 确认节点已经成功应用包含观测 Lua 资源的配置。
2. 确认 OpenResty 正在运行。
3. 查看 Agent 日志是否有观测采集或补报失败信息。
4. 检查 `openresty_observability_port` 是否被占用,默认是 `18081`。
5. 确认 Server 侧没有因数据库清理策略删除对应时间窗口数据。
## 前端构建失败
执行:
```bash
cd frontend
corepack enable
pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm build
```
常见原因:
| 现象 | 处理 |
| --- | --- |
| pnpm 版本不一致 | 使用 `corepack enable` 后重新安装 |
| 类型错误 | 先运行 `pnpm typecheck` 定位具体文件 |
| API 类型不一致 | 检查 `lib/api/` 和 `types/` 中的响应结构 |
| E2E 失败 | 确认 Server 和前端开发服务器都已启动 |
## 文档站构建失败
```bash
cd docs
pnpm install
pnpm build
```
如果是链接错误,检查新增页面是否已经加入 `docs/config.ts` 侧边栏,或者相对链接是否指向存在的 Markdown 文件。
-170
View File
@@ -1,170 +0,0 @@
# 内网穿透与隧道使用
你会学到:OpenFlare 内网穿透隧道的设计原理、核心概念(中继节点与隧道客户端),以及如何从零开始将内网开发环境或私有云服务一步步安全、稳定地发布到公网域名上。
在许多实际开发和运维场景中,我们的源站服务部署在局域网、本地开发机或防范严密的私有 VPC 内部,没有公网 IP,亦无法在边界防火墙或路由器上配置端口映射。
OpenFlare 提供了**基于反向中继穿透隧道**的整体解决方案。你只需在内网环境发起向公网中继节点的出向安全连接,无需配置任何入方向端口,即可将公网的 Web 访问流量平滑引入内网源站,同时享有网关提供的 TLS 证书自动托管与 WAF 安全防护。
---
## 核心概念
在使用内网穿透功能前,你需要熟悉以下组件与核心概念:
| **中继节点 (Relay)** | 部署在公网边缘的流量中继服务,负责监听内网客户端的长连接,并作为网关 Agent (OpenResty) 与内网流量的中转桥梁。 | 运行 `openflare-relay` 守护的 `tunnel_relay` 节点 |
| **穿透隧道 (Tunnel)** | 逻辑上的穿透客户端实例,拥有全局唯一 ID 与安全认证令牌,用以标识一个具体的内网环境。 | 在「节点管理」中创建的 `tunnel_client` 节点,分配专属 Tunnel Token |
| **隧道客户端 (Client)** | 运行在内网环境下的轻量控制器,根据 Server 下发的配置自动管理底层的 frpc 隧道子进程。 | 内网部署的 `openflared` 容器或独立二进制进程 |
| **隧道上游 (Tunnel Upstream)** | 路由规则中的特殊反代类型。选择此类型后,网关会将公网流量转发至本地中继端的 Vhost 端口,最终送达内网源站。 | 在「规则管理」详情页中配置的反向代理类型,选择源站类型为「内网穿透」并绑定对应 Tunnel 节点 |
---
## 推荐操作顺序
将一个内网服务发布到公网,推荐按这个顺序进行:
1. 注册并部署至少一个公网 **中继节点 (Relay)** 并保持在线。
2. 进入 **「节点管理」**,新建一个类型为 **Tunnel 节点 (tunnel_client)** 的节点,获取专属 Token。
3. 在内网服务器中部署并启动 **隧道客户端 (OpenFlared)**。
4. 确认管理端中该 Tunnel 节点的状态显示为「在线」。
5. 在 **「规则管理」** 页面新增或编辑规则,在「反向代理」选项卡中选择源站类型为 **「内网穿透」**,绑定对应 Tunnel 节点并填写内网服务端口(如 `127.0.0.1:8080`)。
6. 发布并激活新版本。
7. 通过公网域名访问,验证内网穿透链路是否打通。
---
## 详细配置步骤
### 第一步:准备中继节点 (Relay)
内网流量需要通过公网的中继节点进行中转。在开始前,你需要确保公网有一台可用的中继服务器。
1. 登录管理端,进入 **「节点管理」**。
2. 添加一个新节点,并将 **节点类型** 选择为 **中继节点 (tunnel_relay)**。
3. 保存后,复制该节点专属的 `agent_token`。
4. 在你的公网服务器上启动 `openflare-relay`。你可以直接使用 Docker 快速运行:
```bash
docker run -d --name openflare-relay --restart unless-stopped \
-p 7000:7000 \
-e OPENFLARE_SERVER_URL=http://<你的Server公网IP>:3000 \
-e OPENFLARE_AGENT_TOKEN=<刚才复制的AgentToken> \
-v openflare-relay-data:/var/lib/openflare-relay \
ghcr.io/rain-kl/openflare-relay:latest
```
> [!IMPORTANT]
> 请务必在云服务器安全组中放行 `7000` 端口(frpc 客户端连接控制端口)。如果你的 Server 与中继节点部署在同一台机器,这里的 `OPENFLARE_SERVER_URL` 应指向 Server 的公网或内网通信 IP。
### 第二步:在管理端创建 Tunnel 节点
1. 导航至管理侧边栏的 **「节点管理」** 页面。
2. 点击 **「新增节点」** 按钮,在弹窗中选择节点类型为 **「Tunnel 节点 (tunnel_client)」**。
3. 填入节点名称与描述,点击保存。
4. 在节点列表中点击进入刚才创建的 Tunnel 节点详情页,你可以找到专属的 **Tunnel Token** 及相应的客户端一键部署命令。
### 第三步:部署内网客户端 (OpenFlared)
回到你的内网服务器中,根据刚才复制的部署命令运行客户端。
#### 方案 A:使用 Docker 部署(强烈推荐)
官方提供的 `openflared` 镜像已经内置了主控守护进程与 `frpc` 运行时,开箱即用,无需配置额外依赖:
```bash
docker run -d --name openflared --restart unless-stopped \
-e OPENFLARE_SERVER_URL=http://<你的Server公网IP>:3000 \
-e OPENFLARE_TUNNEL_TOKEN=<刚才复制的TunnelToken> \
-v openflared-data:/app/data \
ghcr.io/rain-kl/openflared:latest
```
#### 方案 B:宿主机二进制手动运行
如果你不便使用 Docker,也可以下载或自行编译 `flared` 二进制程序:
1. 在内网机器的程序同级目录下创建 `flared.json` 配置文件:
```json
{
"server_url": "http://<你的Server公网IP>:3000",
"tunnel_token": "<刚才复制的TunnelToken>",
"frpc_path": "/usr/local/bin/frpc",
"data_dir": "./data"
}
```
2. 执行启动命令:
```bash
./flared -config ./flared.json
```
#### 状态确认
启动成功后,内网客户端会通过出向网络向控制面发送心跳同步配置。此时:
1. 刷新管理端的 **「节点管理」** 列表,刚才创建的 Tunnel 节点状态指示灯应当变为绿色的 **「在线」**。
2. 点击节点进入详情页,你可以直观地查看到当前内网客户端连接了公网的哪些中继 Relay 节点。
### 第四步:配置请求路由并绑定隧道上游
现在你可以为你的内网服务配置公网反向代理和域名访问了。
1. 首先进入 **「网站管理」->「域名列表」** 录入你想要公开访问的域名。
2. 进入 **「规则管理」** 页面,点击 **「新增规则」** 或编辑已有规则。
3. 在下方 **「反向代理」** 选项卡下,将 **源站类型** 切换为 **「内网穿透」**。
4. 从下拉列表中选择刚才部署在线的 **Tunnel 节点**。
5. 填写 **内网目标地址**(对于内网客户端来说可访问的本地地址与端口,例如 `127.0.0.1:8080`)与 **内网协议**(通常为 `http`)。
6. 配置其他站点常规项,并点击保存。
### 第五步:发布与生效
为了让网关的 OpenResty 能够正确匹配并路由域名流量,我们需要发布新的配置版本。
1. 点击导航栏右上角的 **「配置预览」**,确认生成的站点配置无误。
2. 在弹出窗口中,点击 **「发布并激活」**。
3. 此时,公网边缘的 Agent 会拉取到最新路由:它会将 `nas.example.com` 的请求转发至同机部署的 `openflare-relay (frps)` 的虚拟主机端口下。
4. 内网客户端 `openflared (frpc)` 会接收到被中继的封包,并安全地透传给内网的 `127.0.0.1:8080` 服务,最后原路返回响应。
5. 在你的公网浏览器中访问 `nas.example.com`,确认内网服务成功展示!
---
## 高级应用场景
### 1. 单隧道多服务复用 (多端口映射)
你并不需要为内网的每一个服务都部署一个 `openflared` 容器。
如果你想在一个内网环境映射多个不同的服务(例如:`127.0.0.1:80` 是博客,`127.0.0.1:8080` 是 API,`192.168.1.120:9000` 是内网网盘):
1. 保持这一个 `openflared` 客户端在线。
2. 在管理端创建三个独立的网站配置(绑定各自对应的公网域名)。
3. 这三个网站配置都将 **上游类型** 选为 **同一个穿透隧道**。
4. 分别在各自的“内网目标地址”中填入对应不同的端口或局域网 IP(例如 `127.0.0.1:80`、`127.0.0.1:8080`、`192.168.1.120:9000`)。
5. 发布并激活新版本,即可实现一隧多用。
### 2. 网关安全功能无缝叠加
因为所有公网流量均首先进入公网的 Agent 节点,在此处完成了 HTTPS/TLS 握手与 WAF 引擎拦截,然后再通过安全隧道送达内网。
因此,你的内网服务**天然且无需做任何改造**即可享受以下高级特性:
* **一键启用 HTTPS**:直接在管理端为域名选择或申请 SSL 证书,数据传输全程加密。
* **全局/自定义 WAF 防护**:开启 SQL 注入拦截、XSS 注入防御与恶意地域 IP 屏蔽。
* **人机挑战 (CC PoW)**:一键抵御针对内网服务的恶意 CC 刷接口攻击。
---
## 常见故障排查
### 1. 隧道在管理端显示为「离线」
* **检查 Token 是否正确**:查看 `flared` 日志或环境变量中配置的 `tunnel_token` 是否与管理端生成的一致。
* **检查网络连通性**:内网服务器需能通过出向网络正常请求 Server 地址。确保控制面没有启用防火墙限制客户端的 HTTP 请求。
* **中继节点防火墙未开**:检查对应中继节点的公网 `7000` 端口(或你自定义的 bindPort)是否已经在安全组中对公网放行。
### 2. 访问公网域名返回 502 Bad Gateway / 504 Gateway Timeout
* **内网服务未运行**:确认内网目标地址对应的服务已在内网服务器上成功启动并处于监听状态。
* **目标地址不可达**:如果内网地址填的是 `127.0.0.1:8080`,确保服务确实在运行着 `openflared` 的同一台主机上;如果填的是局域网 IP `192.168.x.x`,请在 `openflared` 容器内测试该局域网 IP 的连通性。
* **检查客户端应用日志**:在管理端查看「应用记录」或在内网查看 `flared` 运行日志,排查是否有 `LastError` 产生。frpc 在连不上内网端口时,会将连接失败报错原样上报至 Server 方便管理员定位。
### 3. 多中继网络动荡或重试失败
* 当控制面关联了多个 Relay 中继节点时,`openflared` 会为每个 Relay 独立派生 frpc 守护进程,并在 `flared.json` 中配置的 `sync_interval`(默认 30s)内定时向控制面拉取拓扑状态。
* 若发现某一中继节点频繁由于网络抖动离线,系统会自动触发退避重试机制。你可以在宿主机日志中看到 `frpc process missing, starting` 的日志,这属于正常的进程自愈逻辑,通常在网络恢复后 5~10 秒内即可自动恢复建连。
-49
View File
@@ -1,49 +0,0 @@
# Uptime Kuma 监控同步
你会学到:如何启用并配置 Uptime Kuma 自动同步集成,控制监测站点的同步范围与心跳探测参数,以及 OpenFlare 与 Uptime Kuma 差分同步的底层原理。
---
## 功能概述
在边缘多节点运维中,及时了解各个代理站点的可用性至关重要。为了避免手动在监控系统中重复录入站点信息,OpenFlare 提供了与开源监控服务 **Uptime Kuma** 的深度集成。
启用集成后,OpenFlare 会启动一个后台同步调度器,自动将管理端配置的代理站点同步为 Uptime Kuma 中的 HTTP 监控任务。支持检测范围过滤、差分属性更新以及对下线站点的自动清理。
---
## 第一步:在系统设置中配置集成
1. 登录管理端控制面板,进入左侧导航 **「系统设置」** (Settings),选择 **「OpenFlare」** 选项卡,在 **「Uptime Kuma 集成」** 区域进行配置。
2. 配置以下核心连接参数:
* **启用状态 (Enabled)**:开启集成开关。
* **实例地址 (Instance URL)**:你的 Uptime Kuma 服务地址。例如 `http://192.168.1.100:3001` 或 `https://kuma.example.com`(必须包含协议前缀 `http://` 或 `https://`)。
* **用户名 (Username)** 与 **密码 (Password)**:具有管理权限的 Uptime Kuma 账户凭证,用于 API 鉴权。
---
## 第二步:控制监控范围与心跳参数
在集成面板中,你可以对监控范围和具体探测行为进行细粒度控制:
### 1. 监控范围 (Monitor Scope)
* **全部站点 (All)**:默认选项。OpenFlare 将自动同步所有**已启用**的代理路由站点。当新站点被创建且启用,或者旧站点被停用时,监控列表将自动增删。
* **选择站点 (Selected)**:仅监控指定站点。选择此模式后,可以点击 **「选择监控站点」** 弹出框。在弹出框内可以通过搜索过滤站点并进行勾选。被取消勾选或未勾选的站点将不会被同步(若已存在则会被自动清理)。
### 2. 监测频率与心跳设置
你可以为自动生成的监控项指定统一的探测参数:
* **同步间隔 (Sync Interval)**:自动差分同步的频率(分钟),默认为 `5` 分钟。即控制面每 5 分钟与 Uptime Kuma 进行一次状态比对。
* **心跳检测频率 (Interval)**:Uptime Kuma 探测站点的频率(秒),默认为 `60` 秒。
* **最大重试次数 (Retry)**:探测失败后,判定为 Down 之前的最大重试次数,默认为 `0`。
* **重试间隔时间 (Retry Interval)**:重试之间的等待秒数,默认为 `60` 秒。
* **请求超时时间 (Timeout)**:探测请求判定为超时的秒数,默认为 `48` 秒。
---
## 同步与清理机制
* **专属标签隔离**:所有自动创建的监控项均会绑定 `OpenFlare` 专属标签(紫蓝色)。同步和清理程序仅操作带有该标签的监控任务,**绝不干扰或破坏你在 Uptime Kuma 中手动创建的其他监控项**。
* **差分增量同步**:同步程序会周期性对比监控元数据。当检测到域名或心跳配置变更时,仅执行差分更新,避免中断历史统计数据;当站点停用或移出范围时,会自动执行监控下线清理。
> [!TIP]
> 关于 Uptime Kuma 监控同步的 Socket.IO 控制流、防污染标签模型及差分比对算法细节,请参阅 [Uptime Kuma 监控同步设计](../design/kuma-design.md)。
-168
View File
@@ -1,168 +0,0 @@
# WAF 自动 IP 组规则语法
自动 IP 组用于从请求日志中按单个客户端 IP 聚合指标,再用 Expr 表达式判断是否把该 IP 加入组内名单。自动 IP 组可以被 WAF 规则组的 IP 黑名单或白名单引用;发布配置时,Server 只把 IP 组引用 ID 写入 `waf_config.json`,IP 组成员由 Agent 独立同步到本地运行时文件。
## 配置结构
自动 IP 组的配置是一个 JSON 对象:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "单 IP 404 高频扫描",
"expr": "request_count > 100 && StatusRatio(404) >= 0.8"
}
]
}
```
字段说明:
| 字段 | 类型 | 作用 |
| --- | --- | --- |
| `lookback_minutes` | number | 每次执行时回看多少分钟内的请求日志。未填写时默认 60 分钟,最小 5 分钟,最大 43200 分钟。 |
| `rules` | array | 自动规则列表。任意一条规则命中时,该 IP 会进入自动 IP 组名单。 |
| `rules[].name` | string | 规则名称,只用于界面展示和错误提示。 |
| `rules[].expr` | string | Expr 表达式,必须返回布尔值。 |
## 执行口径
自动规则不是逐条请求判断,而是先按单个客户端 IP 聚合:
1. Server 读取最近 `lookback_minutes` 分钟内的请求日志。
2. 按 `remote_addr` 归一化后的 IP 分组。
3. 为每个 IP 计算请求数、404 数、直连 IP Host 次数等指标。
4. 逐个 IP 执行 `rules[].expr`。
5. 只要某个 IP 命中任意规则,就写入该自动 IP 组的 `IP / IP 段` 列表。
Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断:如果 Host 是 IPv4 或 IPv6 字面量,例如 `203.0.113.10`、`[2001:db8::10]`、`203.0.113.10:443`,就计入 `ip_host_count`。
## 可用关键字
表达式中可以直接使用以下字段:
| 关键字 | 类型 | 作用 |
| --- | --- | --- |
| `ip` | string | 当前正在判断的客户端 IP。 |
| `request_count` | number | 当前 IP 在回看窗口内的总请求数。 |
| `status_404_count` | number | 当前 IP 在回看窗口内返回 404 的请求数。 |
| `status_404_ratio` | number | 404 请求占比,计算方式为 `status_404_count / request_count`。 |
| `ip_host_count` | number | 当前 IP 通过 IP 地址作为 Host 访问的请求数。 |
| `ip_host_ratio` | number | 通过 IP 地址访问的占比,计算方式为 `ip_host_count / request_count`。 |
| `client_error_count` | number | 当前 IP 返回 4xx 状态码的请求数。 |
| `server_error_count` | number | 当前 IP 返回 5xx 状态码的请求数。 |
| `last_seen_unix` | number | 当前 IP 在回看窗口内最后一次请求的 Unix 秒级时间戳。 |
比例字段都是 `0` 到 `1` 之间的小数。80% 应写成 `0.8`,50% 应写成 `0.5`。
### 自定义状态码匹配方法
如果内置的 `status_404_count` 和 `status_404_ratio` 不能满足您的需求,您可以使用以下内置方法来匹配任意状态码的请求数与占比:
* **`StatusCount(code)`**: 获取当前 IP 在回看窗口内返回指定状态码的请求数(如 `StatusCount(403) > 10`)
* **`StatusRatio(code)`**: 获取当前 IP 在回看窗口内返回指定状态码的请求数占该 IP 总请求数的比例(如 `StatusRatio(502) >= 0.5`)
## Expr 常用写法
自动 IP 组使用 Expr 语法,当前表达式必须返回布尔值。
常用运算符:
| 写法 | 作用 | 示例 |
| --- | --- | --- |
| `>`、`>=`、`<`、`<=` | 数值比较 | `request_count > 100` |
| `==`、`!=` | 相等或不相等 | `ip != "127.0.0.1"` |
| `&&` | 并且 | `request_count > 100 && StatusRatio(404) >= 0.8` |
| `||` | 或者 | `StatusRatio(404) >= 0.8 || server_error_count > 20` |
| `!` | 取反 | `!(ip == "127.0.0.1")` |
| `in` | 判断值是否在列表中 | `ip in ["203.0.113.10", "198.51.100.20"]` |
| `not in` | 判断值是否不在列表中 | `ip not in ["127.0.0.1"]` |
| `()` | 分组控制优先级 | `(request_count > 100 && StatusRatio(404) >= 0.8) || server_error_count > 50` |
## 内置预设
管理端内置两个预设规则,可以直接添加后再按需调整:
```json
{
"name": "单 IP 404 高频扫描",
"expr": "request_count > 100 && StatusRatio(404) >= 0.8"
}
```
含义:单个 IP 在回看窗口内请求数大于 100,并且 404 状态码占比不低于 80%。
```json
{
"name": "单 IP 直连访问异常",
"expr": "ip_host_count > 50 && ip_host_ratio > 0.5"
}
```
含义:单个 IP 通过 IP 地址作为 Host 访问的次数大于 50,并且这种访问占比大于 50%。
## 示例
高频 404 扫描:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "高频 404 扫描",
"expr": "request_count > 100 && StatusRatio(404) >= 0.8"
}
]
}
```
IP 直连访问异常:
```json
{
"lookback_minutes": 30,
"rules": [
{
"name": "IP 直连访问异常",
"expr": "ip_host_count > 50 && ip_host_ratio > 0.5"
}
]
}
```
同时捕获高 4xx 与高 5xx:
```json
{
"lookback_minutes": 120,
"rules": [
{
"name": "异常错误率",
"expr": "(client_error_count > 80 && request_count > 100) || server_error_count > 30"
}
]
}
```
排除可信 IP:
```json
{
"lookback_minutes": 60,
"rules": [
{
"name": "排除可信 IP 的 404 扫描",
"expr": "ip not in [\"203.0.113.10\", \"198.51.100.20\"] && request_count > 100 && StatusRatio(404) >= 0.8"
}
]
}
```
## 使用建议
先用较短的回看窗口和较高阈值观察命中结果,再逐步调整阈值。管理端 IP 组页面支持在保存前点击 **测试规则**,直接查看当前回看窗口内命中的 IP;自动 IP 组真正执行后会覆盖该组的 IP 列表。如果要长期保留某些地址,建议放入手动 IP 组,并在 WAF 规则组中同时引用手动组和自动组。
自动 IP 组更新后不需要重新发布配置版本。在线 Agent 会通过 WebSocket 收到变更 IP 组并更新本地 `waf_ip_groups.json`;WebSocket 不可用时,Agent 会在下一次心跳中上报本地 IP 组 checksum,Server 只返回 checksum 不一致的 IP 组。
-31
View File
@@ -1,31 +0,0 @@
# WAF 安全防护使用
OpenFlare WAF 使用可视化有向无环图编排规则。新建规则时只填写名称,系统创建默认的“开始 → 通过”图并进入编辑器。
## 节点与连线
- **开始**:每条规则唯一,沿 `next` 进入图。
- **通过**:结束当前规则;若路由仍有后续规则则继续执行。
- **阻止**:立即按配置的状态码和 HTML 响应终止请求。
- **IP 匹配**:配置 IP、CIDR 或 IP 组,分别连接 `true`、`false`。
- **地域匹配**:按国家或 ISO 3166-2 一级行政区代码分支;国家列表同时显示中文名称与代码,行政区可按国家名、行政区名或代码搜索。Country 与 City MMDB 缺失时由 Agent 从程序内嵌数据库初始化,并按配置周期更新。City MMDB 不可用时按未匹配处理。
- **PoW**:未完成挑战时接管请求,验证通过后沿 `next` 继续。
服务端会拒绝循环、悬空出口、不可达节点、重复端口连接和无效配置。保存时携带页面加载得到的 `revision`;发生 409 冲突时应重新加载,避免覆盖他人修改。
选中普通节点或连线后,可点击画布右上角的删除按钮,或按 Delete/Backspace 删除。删除节点会同时删除关联连线;唯一的“开始”和“通过”节点不可删除。拖动节点只在松开时记录最终坐标,不会在移动过程中反复重建画布状态。
右侧节点属性栏默认隐藏,点击节点后显示;点击连线或画布空白区域后自动收起。
编排区默认使用较紧凑的高度和较小的首次缩放比例,仍可通过滚轮或画布 Controls 自由缩放。
## 绑定与生效
启用的全局规则固定最先执行;路由绑定的自定义规则严格按列表顺序执行。调整顺序后需要发布配置版本,规则拓扑才会随 OpenResty reload 生效。
IP 组成员是动态资源。Agent 每 5 秒检查 checksum,变化后在 Worker 间更新内存快照,无需重新发布规则或 reload。手动、订阅与自动 IP 组均可被 IP 匹配节点引用。单次完整 IP 组运行时快照最多 20 MiB;超过上限时发布或同步会返回错误,并继续使用上一份有效快照。
> [!IMPORTANT]
> 从旧固定黑白名单/地域/PoW 表单升级时,规则图会重置为“开始 → 通过”,旧策略字段不会迁移。请在发布新版本前逐条重新编排并验证规则。
架构、图校验和失败回滚细节见 [WAF 可编排规则设计](../design/waf-orchestration-design.md)。
-52
View File
@@ -1,52 +0,0 @@
# Zone 域名迁移与发布验收
从旧版 `managed_domains` / 反代路由内嵌域名列迁移到 Zone + Zone 域名模型时,数据导入与表结构升级均由 **Server 启动时的 goose 自动迁移**完成,无需单独执行导入命令。
## 升级时发生了什么
启动(或滚动升级)包含 Zone 改造的 Server 版本时,**无需手动命令**,`migrator.Migrate()` 自动:
1. 应用 goose SQL:创建 `of_zones` / `of_zone_domains`(若尚未存在)。
2. **自动导入**旧路由域名列(及无路由域名时的 `of_managed_domains`)为 Zone / Zone 域名,并绑定 `proxy_route_id` / `cert_id`(公共后缀列表解析注册根域)。
3. 继续 goose SQL:删除 `of_managed_domains` 与 `of_proxy_routes` 冗余域名/证书列。
导入幂等:已存在的域名会跳过或补绑路由。
**若历史数据无法解析(冲突域名、无效根域、证书不存在等),启动失败。** 修复数据或恢复备份后再次启动即可重试。
## 建议操作
### 1. 升级前备份
```bash
# PostgreSQL 示例
pg_dump "$DATABASE_URL" > openflare-pre-zone-$(date +%Y%m%d).sql
# 或复制备份卷 / 快照;SQLite 则复制 data 目录中的库文件
```
可选:在管理端记下当前**激活配置版本号**与 checksum,便于配置回滚对比。
### 2. 升级并启动 Server
部署新版本并启动即可。观察启动日志中的 goose 成功信息;若出现「迁移 Zone 失败(N 个冲突)」则按日志中的冲突项修复源数据后重启。
### 3. 升级后检查
1. 管理端 **网站** `/websites`:Zone 根域与域名计数是否合理。
2. Zone 详情:域名、证书、关联路由 ID。
3. **反代路由**:域名绑定来自 Zone 域名,而非旧手写字段。
### 4. 配置预览与发布
1. 在管理端查看配置差异 / 预览。
2. **逐路由**核对:`server_name` 集合、证书路径、WAF Route ID、Pages 引用。
3. **允许**旧快照 JSON 中路由上的冗余 `domain` / `domains` / `cert_ids` 消失。
4. **不允许**数据面语义变化。
5. 预览通过后发布;需要时在配置版本中激活升级前版本做配置回滚。数据库回退请使用升级前备份(Down 迁移不回填业务域名数据)。
## 相关文档
* [Zone 与域名资源设计](../design/zone-design.md)
* [新建反代配置](./proxy-config.md)
* [发布第一份配置](./first-site.md)
-38
View File
@@ -1,38 +0,0 @@
---
layout: home
hero:
name: OpenFlare
text: 开源 CDN 编排与边缘安全平台
tagline: 支持反向代理、集中式配置同步、Pages 静态托管、内网穿透(Tunnels)、动态 WAF 防护与人机防 CC 挑战。
actions:
- theme: brand
text: 快速开始
link: /guide/quick-start
- theme: alt
text: 设计边界
link: /design/
- theme: alt
text: GitHub
link: https://github.com/Rain-kl/OpenFlare
features:
- icon: 🛰️
title: 集中式配置同步
details: 通过 WebSocket 与心跳实现全网节点配置秒级同步下发与热生效,状态即时回收。
- icon: 🌐
title: 分布式 CDN 编排
details: 将独立的 OpenResty 编排为高度协同的分布式 CDN 舰队,支持源站多负载均衡。
- icon: 📄
title: Pages 静态托管
details: 直接上传前端打包 zip 资产,由边缘节点拉取解压并提供高性能本地服务与 API 代理。
- icon: 🚇
title: 安全内网穿透 (Tunnels)
details: 对标 Cloudflare Tunnels,无须公网 IP 或暴露入向端口,安全穿透本地服务至公网。
- icon: 🛡️
title: 边缘 WAF 安全防护
details: IP 组成员差分同步写入 Lua 共享内存,实现免 Nginx 重载的 WAF 热更新与 GeoIP 过滤。
- icon: 🧩
title: 防 CC 与人机挑战 (PoW)
details: 内置高性能客户端 Proof of Work 密码学挑战,网关边缘秒级拦截阻断僵尸网络与爬虫。
---
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "./node_modules/@lunariajs/core/config.schema.json",
"repository": {
"name": "Rain-kl/OpenFlare",
"rootDir": "docs"
},
"files": [
{
"location": "**/config.ts",
"pattern": "@lang/@path",
"type": "universal"
},
{
"location": "**/*.md",
"pattern": "@lang/@path",
"type": "universal"
}
],
"defaultLocale": {
"label": "简体中文",
"lang": "zh"
},
"locales": [
{
"label": "English",
"lang": "en"
}
],
"outDir": ".vitepress/dist/_translations",
"ignoreKeywords": ["lunaria-ignore"]
}
-20
View File
@@ -1,20 +0,0 @@
{
"name": "openflare-docs",
"private": true,
"type": "module",
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview",
"lunaria:build": "lunaria build",
"lunaria:open": "open-cli .vitepress/dist/_translations/index.html"
},
"devDependencies": {
"@lunariajs/core": "^0.1.1",
"markdown-it-mathjax3": "^4.3.2",
"open-cli": "^8.0.0",
"postcss-rtlcss": "^5.7.1",
"vitepress": "2.0.0-alpha.17",
"vitepress-plugin-llms": "^1.11.0"
}
}
@@ -1,22 +0,0 @@
# ClickHouse P0–P3 修复计划
> 状态: 已完成(已合并主工作区,`make code-check` 通过)
> 策略: 4 个互不干扰 worktree 并行,最后由主代理合并
## 任务拆分
| ID | Worktree 主题 | 范围 | 禁止改动 |
|----|---------------|------|----------|
| WT1 | P0 清理语义 C1 | cleanup maintenance / delete / tasks | chwriter、dashboard、DDL 新 MV |
| WT2 | 写路径 C2+H1+H2+H3 | chwriter、batchwriter、risk_control、model store 分层、status 指标 | goose 迁移、dashboard 读逻辑 |
| WT3 | 读路径 H4+H5 | 最新快照查询、metric/openresty 小时 MV + 读路径 | chwriter、cleanup |
| WT4 | P3 打磨 | 连接池/async_insert、traffic hourly TTL、UV 语义 | model store 分层、cleanup |
## 合并顺序
1. WT1 → 2. WT2 → 3. WT3 → 4. WT4
(迁移文件时间戳已错开,changelog 由主代理统一写)
## 验收
各 worktree: 相关 `go test` + 可运行部分;合并后 `make code-check`。
-491
View File
@@ -1,491 +0,0 @@
# Zone 与域名资源重构 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 以稳定 ID 的 Zone 管理入口和正规化 Zone 域名替代 `managed_domains` 及反代路由中的域名/证书冗余字段,同时保持配置发布后的 OpenResty 行为不变。
**Architecture:** `of_zones` 管理可注册根域;`of_zone_domains` 是明确 FQDN、证书和反代路由之间的唯一关联来源。反代路由保留路由策略,配置快照在控制面联查 Zone 域名与证书后生成现有 OpenResty 配置格式。第一发布阶段保留旧列供可重复执行的历史数据导入读取;生产快照对比通过后才执行第二阶段清理。
**Tech Stack:** Go 1.25、Gin、GORM、goose(PostgreSQL/SQLite)、`golang.org/x/net/publicsuffix`、Next.js App Router、TypeScript、TanStack Query、shadcn/ui。
## Global Constraints
* Zone URL 必须为 `/websites/:zoneId`,不得使用域名作为路由参数。
* Zone 根域由 `publicsuffix.EffectiveTLDPlusOne` 解析;Zone 域名只接受明确 FQDN,拒绝 `*.`。
* TLS 证书可含通配符 SAN;证书只能由 `of_zone_domains.cert_id` 指定,`of_proxy_routes` 不再保存证书字段。
* `of_zone_domains.domain` 全局唯一;同一 Zone 域名至多绑定一条反代路由,路由可关联多个 Zone 的域名。
* 不建立物理数据库外键;所有关联列必须建立显式索引。
* 所有 HTTP 路由仅通过 `internal/router/v1/openflare/` 的管理端注册器委派;Handler 使用 `response.Abort*` 报错并补全 Swagger。
* 不新增 DNS 记录管理、边缘函数、预览子域或多租户能力。
* 每次 API 变更运行 `make swagger`;每个实现任务结束运行对应测试;完成前必须运行 `make code-check`。
---
## File Structure
| 路径 | 职责 |
| --- | --- |
| `internal/db/migrator/goose/{postgres,sqlite}/202607120001_create_zone_domain_tables.sql` | 第一阶段 Zone/ZoneDomain DDL 与索引。 |
| `internal/model/openflare_zone.go` | Zone、ZoneDomain 模型及数据访问。 |
| `internal/apps/openflare/zone/{logics.go,routers.go,errs.go,legacy_import.go}` | Zone CRUD、概览、输入验证和历史导入。 |
| `internal/cmd/migrate_zones.go` | 显式、可重复运行的历史 Zone 数据导入命令。 |
| `internal/router/v1/openflare/register_zone.go` | `/api/v1/d/zones` 路由注册。 |
| `internal/apps/openflare/proxy_route/*` | 以 `zone_domain_ids` 取代域名与证书输入。 |
| `internal/apps/openflare/config_version/*`、`pkg/render/openresty/*` | 快照与 OpenResty 渲染改为使用 Zone 域名。 |
| `frontend/lib/services/openflare/{zone.service.ts,types.ts,index.ts}` | Zone API 类型和服务。 |
| `frontend/vitest.config.ts`、`frontend/tests/zone/*.test.tsx` | Zone 页面与域名选择器的最小前端测试运行环境。 |
| `frontend/app/(main)/websites/*` | Zone 列表、`[zoneId]` 动态详情页和局部组件。 |
| `frontend/app/(main)/proxy-routes/*` | Zone 域名选择器替换旧域名/证书编辑器。 |
| `internal/db/migrator/goose/{postgres,sqlite}/202607130001_drop_legacy_route_domain_columns.sql` | 第二阶段删除旧表、列与索引。 |
### Task 1: 第一阶段 Schema、模型与迁移测试
**Files:**
- Create: `internal/db/migrator/goose/postgres/202607120001_create_zone_domain_tables.sql`
- Create: `internal/db/migrator/goose/sqlite/202607120001_create_zone_domain_tables.sql`
- Create: `internal/model/openflare_zone.go`
- Create: `internal/model/openflare_zone_test.go`
- Modify: `internal/model/openflare_proxy_route.go`
- Test: `internal/db/migrator/migrator_test.go`
**Interfaces:**
- Produces: `model.Zone`, `model.ZoneDomain`, `ListZoneDomainsByRouteID(ctx, routeID)`, `ReplaceZoneDomainRouteBindings(ctx, routeID, domainIDs)`.
- Consumes: existing `model.ProxyRoute` and `model.TLSCertificate` IDs; no physical FK.
- [x] **Step 1: 写失败的模型与迁移测试**
```go
func TestReplaceZoneDomainRouteBindingsRejectsForeignDomain(t *testing.T) {
// Create zones and domains, then assert a domain cannot be bound twice.
}
```
Run: `go test ./internal/model ./internal/db/migrator -run 'Zone|Migrat' -count=1`
Expected: FAIL,因为 Zone 模型和 goose 文件尚不存在。
- [x] **Step 2: 新建双方言 DDL**
```sql
CREATE TABLE IF NOT EXISTS of_zones (
id BIGSERIAL PRIMARY KEY,
domain VARCHAR(255) NOT NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_zones_domain ON of_zones (domain);
CREATE TABLE IF NOT EXISTS of_zone_domains (
id BIGSERIAL PRIMARY KEY,
zone_id BIGINT NOT NULL,
proxy_route_id BIGINT,
domain VARCHAR(255) NOT NULL,
cert_id BIGINT,
remark VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_zone_domains_domain ON of_zone_domains (domain);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_zone_id ON of_zone_domains (zone_id);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_proxy_route_id ON of_zone_domains (proxy_route_id);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_cert_id ON of_zone_domains (cert_id);
```
SQLite 使用 `INTEGER PRIMARY KEY AUTOINCREMENT`、`DATETIME`,字段/索引语义完全对齐。此任务不得删除旧列或旧表。
- [x] **Step 3: 实现模型和受事务保护的绑定替换**
```go
type Zone struct { ID uint; Domain string; Remark string; CreatedAt time.Time; UpdatedAt time.Time }
type ZoneDomain struct { ID uint; ZoneID uint; ProxyRouteID *uint; Domain string; CertID *uint; Remark string; CreatedAt time.Time; UpdatedAt time.Time }
func ReplaceZoneDomainRouteBindings(ctx context.Context, routeID uint, domainIDs []uint) error
```
实现先锁定/读取请求域名,拒绝已绑定到其他路由的记录,再把当前路由已绑定但不在 `domainIDs` 的记录置空,最后将请求记录写为 `routeID`;所有动作放在同一 `db.DB(ctx).Transaction` 内。
- [x] **Step 4: 运行模型与迁移测试**
Run: `go test ./internal/model ./internal/db/migrator -run 'Zone|Migrat' -count=1`
Expected: PASS,空 SQLite 库可应用迁移,唯一域名和绑定排他性受保护。
- [x] **Step 5: Commit**
```bash
git add internal/db/migrator/goose internal/model
git commit -m "feat(zone): add normalized zone domain schema"
```
### Task 2: Zone 领域逻辑、历史导入命令与管理 API
**Files:**
- Create: `internal/apps/openflare/zone/{logics.go,routers.go,errs.go,legacy_import.go,logics_test.go}`
- Create: `internal/cmd/migrate_zones.go`
- Create: `internal/router/v1/openflare/register_zone.go`
- Modify: `internal/cmd/root.go`
- Modify: `internal/router/v1/openflare/register_tls.go`
- Test: `internal/apps/openflare/integration/security_test.go`
**Interfaces:**
- Produces: `zone.Create`, `zone.Update`, `zone.GetOverview`, `zone.ImportLegacy(ctx) (ImportReport, error)` and Zone REST handlers.
- Consumes: Task 1 models; legacy `managed_domains` and proxy-route columns only inside `ImportLegacy`.
- [x] **Step 1: 写失败的逻辑与 API 测试**
```go
func TestCreateZoneDomainRejectsWildcard(t *testing.T) { _, err := CreateDomain(ctx, zoneID, DomainInput{Domain: "*.example.com"}); require.EqualError(t, err, errDomainWildcardUnsupported) }
func TestLegacyImportUsesEffectiveTLDPlusOne(t *testing.T) {
root, err := zoneRoot("api.example.co.uk")
require.NoError(t, err)
require.Equal(t, "example.co.uk", root)
}
```
集成测试请求 `POST /api/v1/d/zones/`、`POST /api/v1/d/zones/:id/domains`,并断言错误响应使用 400 信封。
- [x] **Step 2: 实现精确域名和 Zone 归属验证**
```go
func zoneRoot(domain string) (string, error) { return publicsuffix.EffectiveTLDPlusOne(strings.ToLower(strings.TrimSpace(domain))) }
func CreateDomain(ctx context.Context, zoneID uint, input DomainInput) (*model.ZoneDomain, error)
```
拒绝空值、协议、路径和 `*`;要求 `zoneRoot(input.Domain) == zone.Domain`;若 `cert_id` 非空,验证 TLS 证书存在。Zone 根域创建也必须经 `EffectiveTLDPlusOne` 验证且输入等于结果。
- [x] **Step 3: 实现显式导入命令**
```go
var migrateZonesCmd = &cobra.Command{Use: "migrate-zones", RunE: func(_ *cobra.Command, _ []string) error {
report, err := zone.ImportLegacy(context.Background())
return report.LogAndReturn(err)
}}
```
导入以事务执行:用 `routeidentity.DecodeDomains(route.Domains, route.Domain)` 读取旧路由;按 `domain_cert_ids` 的同一索引写入 `zone_domains.cert_id`;只在无路由域名时导入旧 `managed_domains`。发现无效根域、通配符记录或全局域名冲突时回滚并输出全部冲突项。重复执行不得生成重复 Zone/ZoneDomain。
- [x] **Step 4: 注册 API 并删除旧 managed-domain 路由**
```go
zoneGroup := apiGroup.Group("/zones")
zoneGroup.Use(apiutil.AdminMiddlewares()...)
zoneGroup.GET("/", zone.ListHandler)
zoneGroup.POST("/", zone.CreateHandler)
zoneGroup.GET("/:id/overview", zone.GetOverviewHandler)
```
把 `managed-domains` 路由块从 `register_tls.go` 移除;每个 Handler 使用 `apiutil.BindJSON` 和 `response.AbortBadRequest/AbortNotFound/AbortConflict`。
- [x] **Step 5: 验证并 Commit**
Run: `go test ./internal/apps/openflare/zone ./internal/apps/openflare/integration -count=1 && make swagger`
Expected: PASS,Swagger 不再含 `/managed-domains` 且包含 `/zones`。
```bash
git add internal/apps/openflare/zone internal/cmd internal/router/v1/openflare docs
git commit -m "feat(zone): add zone management api and legacy importer"
```
### Task 3: 反代路由改用 ZoneDomain 关联
**Files:**
- Modify: `internal/apps/openflare/proxy_route/{logics.go,helpers.go,build_helpers.go,routers.go,errs.go,logics_test.go}`
- Modify: `internal/model/openflare_proxy_route.go`
- Modify: `internal/apps/openflare/tls/logics.go`
- Modify: `internal/apps/openflare/origin/logics.go`
**Interfaces:**
- Consumes: `zone_domain_ids []uint` and Task 1 binding API.
- Produces: `proxy_route.Input{ZoneDomainIDs []uint}`, `proxy_route.View{ZoneDomains []ZoneDomainView}`.
- [x] **Step 1: 写失败的路由逻辑测试**
```go
input := Input{SiteName: "api", ZoneDomainIDs: []uint{domainA.ID, domainB.ID}, EnableHTTPS: true}
view, err := CreateProxyRoute(ctx, input)
require.NoError(t, err)
require.Equal(t, []uint{domainA.ID, domainB.ID}, view.ZoneDomainIDs)
```
同时覆盖:空 `zone_domain_ids`、重复 ID、其他路由已占用域名、HTTPS 域名无证书、证书 SAN 不覆盖。
- [x] **Step 2: 删除路由输入/视图中的旧域名与证书字段**
```go
type ZoneDomainBindingInput struct {
ZoneDomainIDs []uint `json:"zone_domain_ids"`
}
type ZoneDomainView struct { ID uint `json:"id"`; ZoneID uint `json:"zone_id"`; Domain string `json:"domain"`; CertID *uint `json:"cert_id"` }
```
移除 `Input.Domain`、`Input.Domains`、`Input.CertID`、`Input.CertIDs`、`Input.DomainCertIDs` 及对应 View 字段;删除旧证书派生辅助函数与 `WebsiteService.match` 所需后端逻辑。
- [x] **Step 3: 用关联记录验证并构建路由**
在 `buildProxyRoute` 中读取所有 `ZoneDomainIDs`,对每个 HTTPS 域名调用现有 `validateCertificateCoverage`,再调用 `ReplaceZoneDomainRouteBindings`。更新/删除路由也必须在事务内同步解除关联。来源、证书删除检查和 Origin 路由摘要改从 `zone_domains` 查询域名/证书。
- [x] **Step 4: 运行路由和 TLS 回归测试**
Run: `go test ./internal/apps/openflare/proxy_route ./internal/apps/openflare/tls ./internal/apps/openflare/origin -count=1`
Expected: PASS;任一证书已被 Zone 域名引用时,删除证书被拒绝。
- [x] **Step 5: Commit**
```bash
git add internal/apps/openflare/proxy_route internal/apps/openflare/tls internal/apps/openflare/origin internal/model
git commit -m "refactor(proxy): bind routes through zone domains"
```
### Task 4: 配置快照、渲染与关联消费者
**Files:**
- Modify: `internal/apps/openflare/config_version/{snapshot.go,helpers.go,logics.go,logics_test.go,certificate_snapshot_test.go,pages_snapshot.go}`
- Modify: `pkg/render/openresty/{types.go,render.go,render_route.go,render_test.go}`
- Modify: `internal/apps/openflare/{flared/logics.go,uptimekuma/sync.go}`
- Modify: `internal/apps/openflare/routeidentity/identity.go`
**Interfaces:**
- Produces: snapshot/render `Route{SiteName, Domains, DomainCertIDs}` built transiently from ZoneDomain rows; neither DB model nor API stores those fields.
- [x] **Step 1: 写快照等价性失败测试**
```go
func TestBuildSnapshotReadsZoneDomainCertificates(t *testing.T) {
// Two explicit ZoneDomains with different certs must render two TLS server blocks.
}
```
加入 Pages、Tunnel、WAF 绑定测试,断言 Route ID 与 `site_name` 未改变。
- [x] **Step 2: 在快照边界联查并生成临时渲染字段**
```go
domains, err := model.ListZoneDomainsByRouteID(ctx, route.ID)
snapshotRoute.Domains = maps.Values(domainNames)
snapshotRoute.DomainCertIDs = certIDsInDomainOrder(domains)
```
`pkg/render/openresty.Route` 可继续保留 `Domains` 与 `DomainCertIDs`,因为它是不可变配置快照的渲染输入;移除其中持久化主域/证书回退逻辑,所有错误消息改用 `SiteName`。
- [x] **Step 3: 移除旧字段回退路径**
删除 `routeidentity.DecodeDomains` 对持久化 `route.Domain` 的依赖;Flared、Uptime Kuma、配置 diff、WAF 文档和 Pages 错误信息都从 snapshot/ZoneDomain 查询的明确域名获取显示文本。
- [x] **Step 4: 运行数据面测试**
Run: `go test ./internal/apps/openflare/config_version ./pkg/render/openresty ./internal/apps/openflare/flared ./internal/apps/openflare/uptimekuma -count=1`
Expected: PASS;迁移后的路由产生的 `server_name`、证书支持文件和 WAF RouteID 绑定与迁移前一致。
- [x] **Step 5: Commit**
```bash
git add internal/apps/openflare/config_version internal/apps/openflare/flared internal/apps/openflare/uptimekuma internal/apps/openflare/routeidentity pkg/render/openresty
git commit -m "refactor(config): render routes from zone domains"
```
### Task 5: Zone 前端服务与 ID 动态页面
**Files:**
- Create: `frontend/lib/services/openflare/zone.service.ts`
- Create: `frontend/vitest.config.ts`
- Create: `frontend/tests/zone/{websites-page.test.tsx,zone-page.test.tsx}`
- Modify: `frontend/lib/services/openflare/{types.ts,index.ts}`
- Modify: `frontend/lib/services/index.ts`
- Modify: `frontend/app/(main)/websites/page.tsx`
- Create: `frontend/app/(main)/websites/[zoneId]/page.tsx`
- Create: `frontend/app/(main)/websites/[zoneId]/components/{zone-overview.tsx,zone-domains-table.tsx,zone-route-summary.tsx,zone-editor-dialog.tsx,zone-domain-dialog.tsx}`
- Delete: `frontend/app/(main)/websites/detail/page.tsx`
- Delete: `frontend/app/(main)/websites/detail/page-client.tsx`
- Delete: legacy Website/managed-domain-only components after imports are removed.
**Interfaces:**
- Produces: `ZoneService.list/getOverview/create/update/delete`, `ZoneDomainService.create/update/delete` and `ZoneOverview` TypeScript types.
- [x] **Step 1: 写服务与页面行为测试**
先安装仅用于本次页面测试的开发依赖:
```bash
cd frontend && pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom
```
```ts
expect(ZoneService.getOverview).toHaveBeenCalledWith(42)
expect(screen.getByRole('heading', {name: 'example.com'})).toBeVisible()
```
覆盖 `/websites/42` 的加载、404、空域名、搜索列表和从列表点击 ID 链接。
- [x] **Step 2: 实现类型化服务与查询键**
```ts
export interface ZoneDomainItem { id: number; zone_id: number; proxy_route_id: number | null; domain: string; cert_id: number | null; remark: string }
export class ZoneService extends OpenFlareBaseService { protected static override basePath = '/api/v1/d/zones' }
export const zoneQueryKey = ['openflare', 'zones'] as const
```
所有 React Query 回调使用箭头函数,避免静态 service `this` 丢失。
- [x] **Step 3: 用 Next 动态段实现 Zone 详情**
```tsx
export default async function ZonePage({params}: PageProps<'/websites/[zoneId]'>) {
const {zoneId} = await params
return <ZonePageClient zoneId={Number(zoneId)} />
}
```
遵循本地 Next 文档:动态 `params` 是 Promise;无效或非正整数 ID 显示既有 `EmptyStateWithBorder`,不把域名写入 URL。主页面只维护页面骨架和 Tabs,具体 Tab 放入同目录组件。
- [x] **Step 4: 实现列表和详情交互**
列表仅渲染 Zone 根域及计数;详情使用概览、域名、路由、证书、设置 Tabs。域名弹窗拒绝 `*.`,但证书选择器不限制其 SAN。删除 Zone/域名使用确认对话框和服务端错误文案。
- [x] **Step 5: 验证并 Commit**
Run: `cd frontend && pnpm exec vitest run && pnpm lint`
Expected: PASS。
```bash
git add frontend/lib/services frontend/app/'(main)'/websites
git commit -m "feat(web): add zone-based website management"
```
### Task 6: 反代路由前端切换到 Zone 域名选择器
**Files:**
- Create: `frontend/app/(main)/proxy-routes/components/zone-domain-selector.tsx`
- Create: `frontend/tests/zone/zone-domain-selector.test.tsx`
- Modify: `frontend/app/(main)/proxy-routes/{components/proxy-route-create-sheet.tsx,components/helpers.ts,page-client.tsx}`
- Modify: `frontend/app/(main)/proxy-routes/detail/{helpers.ts,page-client.tsx,components/domain-section.tsx}`
- Delete: `frontend/app/(main)/proxy-routes/detail/components/domain-list-input.tsx`
- Modify: `frontend/lib/services/openflare/types.ts`
**Interfaces:**
- Consumes: `ZoneDomainItem[]` and route `zone_domain_ids: number[]`.
- Produces: selector values with explicit domain/Zone/证书信息;不发送任何旧域名或证书字段。
- [x] **Step 1: 写失败的选择器测试**
```tsx
render(<ZoneDomainSelector value={[7]} onChange={onChange} domains={[apiDomain]} />)
expect(screen.getByText('api.example.com')).toBeVisible()
expect(onChange).toHaveBeenCalledWith([7])
```
覆盖搜索、跨 Zone 多选、已被其他路由占用的禁用项和 HTTPS 缺少证书的表单错误。
- [x] **Step 2: 移除旧前端负载与自动匹配**
从 `ProxyRouteItem`/`ProxyRouteMutationPayload` 删除 `domain`、`domains`、`primary_domain`、`cert_id`、`cert_ids`、`domain_cert_ids`;删除 `WebsiteService.match` 及 `DomainListInput` 自动填证书交互。
- [x] **Step 3: 实现 Zone 域名选择和保存负载**
```ts
mutationFn: (payload) => ProxyRouteService.update(route.id, {
...payload,
zone_domain_ids: selectedDomainIDs,
})
```
展示每个选择项的 FQDN、所属 Zone 与证书;路由详情的“域名”区只编辑关联关系,证书链接跳转 Zone 详情而非路由内编辑。
- [x] **Step 4: 运行前端类型和交互测试**
Run: `cd frontend && pnpm exec tsc --noEmit`
Expected: PASS;不存在旧持久化域名/证书字段的 TypeScript 引用。
- [x] **Step 5: Commit**
```bash
git add frontend/app/'(main)'/proxy-routes frontend/lib/services/openflare/types.ts
git commit -m "refactor(web): select route domains from zones"
```
### Task 7: 第一发布阶段验证、文档与发布前数据检查
**Files:**
- Modify: `docs/design/zone-design.md`
- Modify: `docs/changelog/index.md`
- Modify: generated `docs/{docs.go,swagger.json,swagger.yaml}`
- Create: `docs/guide/zone-domain-migration.md`
- [x] **Step 1: 为导入命令写可操作迁移指南**
文档写明备份、执行 `wavelet migrate-zones`、读取导入报告、发布预览、比较 `server_name`/证书支持文件、发布激活和回滚步骤;不允许在报告有冲突时继续。
- [x] **Step 2: 生成 Swagger 和更新未发布变更**
Run: `make swagger`
在 `[Unreleased]` 记录 Zone 管理、反代路由域名正规化和移除 managed-domain API。
- [x] **Step 3: 运行全量质量门禁**
Run: `go test ./... && make code-check`
Expected: PASS。
- [x] **Step 4: 做快照等价性验收**
在升级前导出活动版本,在导入后生成预览;逐个比较所有路由的明确 `server_name` 集合、证书路径、WAF RouteID 绑定与 Pages 部署引用。只允许旧快照的域名/证书冗余 JSON 消失,不允许数据面语义变化。
- [x] **Step 5: Commit**
```bash
git add docs
git commit -m "docs(zone): add migration and release verification guide"
```
### Task 8: 第二发布阶段——删除旧表和冗余列
**Precondition:** 已在生产环境完成 Task 7 的导入、预览对比和至少一次发布/回滚验证;`migrate-zones` 报告无冲突。
**Files:**
- Create: `internal/db/migrator/goose/postgres/202607130001_drop_legacy_route_domain_columns.sql`
- Create: `internal/db/migrator/goose/sqlite/202607130001_drop_legacy_route_domain_columns.sql`
- Delete: `internal/model/openflare_managed_domain.go`
- Delete: `internal/apps/openflare/tls/managed_domain.go`
- Delete: `internal/apps/openflare/tls/helpers.go` 中仅用于旧路由证书数组的函数
- Modify: legacy迁移相关测试、模型测试与 `docs/design/zone-design.md`
- [x] **Step 1: 写空库与升级库清理失败测试**
```go
func TestLegacyRouteColumnsAreAbsentAfterCleanup(t *testing.T) {
require.False(t, db.DB(ctx).Migrator().HasColumn(&model.ProxyRoute{}, "domain"))
}
```
- [x] **Step 2: 编写双方言清理 DDL**
PostgreSQL 删除旧唯一索引和 `domain`、`domains`、`cert_id`、`cert_ids`、`domain_cert_ids`,再删除 `of_managed_domains`;SQLite 使用重建 `of_proxy_routes` 表的迁移方式保留所有非旧字段与索引。Down 仅在开发数据库恢复旧结构,不回填历史数据。
- [x] **Step 3: 删除旧读取代码与测试 fixture**
删除所有 `route.Domain`、`route.Domains`、`route.CertID`、`route.CertIDs`、`route.DomainCertIDs` 的持久化引用;让编译器、Uptime Kuma、Flared、来源摘要及 API 只使用 ZoneDomain 查询结果。
- [x] **Step 4: 验证升级和完整回归**
Run: `go test ./internal/db/migrator ./internal/model ./internal/apps/openflare/... ./pkg/render/openresty -count=1 && make code-check`
Expected: PASS;全仓搜索不再发现旧 `ManagedDomain` 业务代码、`ProxyRoute` 持久化字段或管理端 API;渲染快照中的临时 `DomainCertIDs` 类型允许保留。
- [x] **Step 5: Commit**
```bash
git add internal/db/migrator internal/model internal/apps frontend docs
git commit -m "refactor(zone): remove legacy route domain storage"
```
## Plan Self-Review
* Spec coverage: Tasks 1–4 交付正规化数据模型、API、迁移与数据面;Tasks 5–6 交付 ID 路由和 Zone 交互;Tasks 7–8 覆盖质量门禁与旧表清理。
* Placeholder scan: 无待定标记或未定义的实现步骤;所有删除动作在明确的生产验证前置条件后执行。
* Type consistency: 路由写入统一使用 `zone_domain_ids`,持久化关系统一为 `ZoneDomain.ProxyRouteID`,渲染边界仅使用临时 `Domains`/`DomainCertIDs`。
-639
View File
@@ -1,639 +0,0 @@
# WAF 可编排规则实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将固定顺序的 WAF 规则组重构为可用 React Flow 编辑、发布时编译、OpenResty 内存执行的有序 DAG 规则系统。
**Architecture:** 控制面以带修订号的版本化 JSON 保存整张编辑态图,Server 保存与发布时执行同一套强校验并编译为精简运行态 DAG。规则仅随配置发布和 OpenResty reload 加载一次;动态 IP 组由协调 Worker 每 5 秒检查 checksum,变化时更新共享快照和各 Worker 本地内存对象。
**Tech Stack:** Go 1.25、Gin、GORM、goose、PostgreSQL/SQLite、OpenResty Lua、Next.js 16 App Router、React 19、TypeScript、`@xyflow/react`、TanStack Query、shadcn/ui、Vitest。
## 实现状态(2026-07-13)
Tasks 1–11 已实现,包含三段数据库迁移、图模型与编译器、规则 API、发布快照、OpenResty 内存执行器、IP 组协调刷新、React Flow 编辑器、有序绑定、GeoLite2 City/Country 支持以及中文文档与 Swagger 更新。React Flow 画布使用本地受控节点状态处理拖动,并支持显式或键盘删除普通节点与连线。Country 与 City MMDB 均随 Agent 内嵌,缺失文件在启动时从程序内初始化,网络仅用于后续周期更新。地域属性栏使用完整国家与 ISO 3166-2 一级行政区数据,国家同时展示中文名称与代码,行政区支持按名称或代码搜索。发布器保证空规则绑定编码为 `[]`,Lua 运行时兼容旧快照中的 `null` 数组,避免未启用或空绑定规则导致请求 500。PoW 节点通过共享内存暂存配置,并以 `ngx.exec` 显式参数把配置键传入内部挑战处理器,避免内部重定向丢失请求上下文后误报节点未执行。
当前工作区已完成 `go test ./...`、前端全量 Vitest(56 项)、`make swagger`、`make code-check` 与 `git diff --check` 验证。Next.js 生产构建在本机持续停留于 Turbopack 的 `Creating an optimized production build ...`,未返回编译错误或成功状态,故不计为通过。
## Global Constraints
- 每张图恰好一个 `start` 和一个 `allow`;`block` 可多个;图必须无环、无悬空、无不可达节点,所有路径必须抵达 `allow` 或 `block`。
- `ip_match` 与 `geo_match` 只输出 `true`/`false`,分别表示匹配与未匹配;`pow` 只输出 `next`。
- 全局规则固定前置;路由自定义规则按绑定 `sequence` 升序执行;当前规则 `allow` 后继续下一条,`block` 立即终止。
- 规则运行态 JSON 仅在 OpenResty reload 后由 Worker 加载一次;请求路径禁止文件 I/O、checksum 和 JSON 解码。
- IP 组请求路径始终读取 Worker 本地对象;整个实例每 5 秒最多一个 Worker 检查 checksum。
- 迁移只保留规则名称、全局标记、启用状态和绑定关系,旧策略统一重置为 `开始 → 通过`。
- 所有 HTTP 路由仅在 `internal/router/router.go` 的既有分发体系中通过 `internal/router/v1/openflare/register_waf.go` 注册;API 失败使用 `response.Abort*`。
- API Handler 变化后运行 `make swagger`;代码完成后运行 `make code-check`;代码变更写入 `docs/changelog/index.md` 的 `[Unreleased]`。
- 前端不得删除 `frontend/node_modules`;使用 shadcn variant 与全局 CSS 变量,页面根容器保持 `w-full py-6 px-1`。
- 实现前阅读 `.agent/skills/database-migration/SKILL.md`、`.agent/skills/new-api/SKILL.md`、`.agent/skills/shadcn/SKILL.md` 以及 `frontend/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/page.md` 等匹配的 Next.js 本地文档。
---
## 1. 目标与背景
当前 `OpenFlareWAFRuleGroup` 把 IP/地域黑白名单、PoW 与阻止响应平铺为固定字段,Lua 按硬编码顺序判断,无法表达用户自定义分支。本次交付包含图模型、强校验、API、迁移、发布编译、Lua DAG 执行、有序绑定、IP 组五秒内存刷新和 React Flow 编辑器;不包含循环、脚本节点、表达式节点、子图和跨规则跳转。
## 2. 数据与控制流
```mermaid
flowchart LR
UI[React Flow 编辑态图] -->|revision + graph| API[Server 校验与保存]
API --> DB[(规则 graph JSON)]
DB --> PUB[发布编译器]
PUB --> SNAP[运行态 DAG 快照]
SNAP --> AGENT[Agent 原子落盘]
AGENT -->|reload| LUA[OpenResty Worker 内存]
IPS[IP 组异步更新] -->|JSON 后 checksum| TIMER[5 秒协调定时器]
TIMER --> SHM[ngx.shared 原始快照]
SHM --> LUA
```
## 3. 文件结构与职责
- `internal/apps/openflare/waf/graph_types.go`:编辑态图、节点配置、运行态图和默认图类型。
- `internal/apps/openflare/waf/graph_validate.go`:结构、端口、配置、引用、可达性和终止性校验。
- `internal/apps/openflare/waf/graph_compile.go`:删除 UI 字段、编译索引化 DAG、收集 IP 组引用。
- `internal/apps/openflare/waf/rule_logics.go`:规则元数据、修订保存和有序绑定逻辑;从现有过大的 `logics.go` 中抽离规则职责。
- `internal/apps/openflare/waf/rule_routers.go`:规则 API Handler 与 Swagger;IP 组 Handler 留在现有文件或后续独立拆分。
- `internal/apps/agent/nginx/waf_assets.go`:只负责嵌入 Lua 文件;实际 Lua 拆到 `internal/apps/agent/nginx/waf_runtime.lua` 与 `waf_ip_groups.lua` 并使用 `go:embed`,避免继续膨胀 Go 字符串。
- `frontend/app/(main)/waf/page.tsx`:规则/IP 组列表和仅名称创建流程。
- `frontend/app/(main)/waf/rules/editor/page.tsx`:静态可导出的编辑器路由入口,通过查询参数读取规则 ID,避免 Next 静态导出的动态参数限制。
- `frontend/app/(main)/waf/rules/editor/components/`:画布、节点、节点库、属性栏和校验提示,单文件保持低于 600 行。
---
### Task 1: 数据库迁移与持久化模型
**Files:**
- Create: `internal/db/migrator/goose/postgres/202607150001_orchestrate_waf_rules.sql`
- Create: `internal/db/migrator/goose/sqlite/202607150001_orchestrate_waf_rules.sql`
- Create: `internal/db/migrator/goose/postgres/202607150002_reset_waf_rule_graphs.sql`
- Create: `internal/db/migrator/goose/sqlite/202607150002_reset_waf_rule_graphs.sql`
- Modify: `internal/model/openflare_waf.go`
- Create: `internal/model/openflare_waf_graph_test.go`
**Interfaces:**
- Produces: `Graph string`, `Revision uint64`, `Sequence int`;`UpdateOpenFlareWAFRuleGraph(ctx, id, revision, graph) (uint64, error)`;绑定查询按 `sequence, id` 排序。
- Consumes: 现有 `OpenFlareWAFRuleGroup` 与 `OpenFlareWAFRuleGroupBinding`。
- [ ] **Step 1: 阅读数据库迁移 Skill 并写迁移失败测试**
测试建立旧 Schema、插入两个规则和无序绑定、执行迁移后断言图统一为默认图、`revision = 1`、绑定顺序稳定。测试核心断言:
```go
require.JSONEq(t, `{"schema_version":1,"nodes":[{"id":"start","type":"start","position":{"x":0,"y":0},"config":{}},{"id":"allow","type":"allow","position":{"x":320,"y":0},"config":{}}],"edges":[{"id":"start-allow","source":"start","source_handle":"next","target":"allow"}]}`, group.Graph)
assert.Equal(t, uint64(1), group.Revision)
assert.Equal(t, []int{0, 1}, []int{bindings[0].Sequence, bindings[1].Sequence})
```
- [ ] **Step 2: 运行模型测试确认失败**
Run: `go test ./internal/model -run 'TestOpenFlareWAFGraph|TestReplaceOpenFlareWAFRuleGroupBindings' -count=1`
Expected: FAIL,缺少新字段或迁移列。
- [ ] **Step 3: 编写 PostgreSQL 与 SQLite goose 迁移**
`202607150001` 只执行 DDL:两端都增加 `graph TEXT NOT NULL`、`revision BIGINT/INTEGER NOT NULL DEFAULT 1`、`sequence INTEGER NOT NULL DEFAULT 0`。`202607150002` 只执行 DML:用确定性的 `id` 顺序为每个 `proxy_route_id` 回填 sequence,并将所有 graph 重置为同一默认 JSON。Down 分别恢复数据语义与旧列结构;不要创建物理外键,禁止把 DDL 与 DML 放入同一个迁移文件。
- [ ] **Step 4: 实现乐观锁与有序绑定模型方法**
```go
var ErrWAFRuleRevisionConflict = errors.New("waf rule revision conflict")
func UpdateOpenFlareWAFRuleGraph(ctx context.Context, id uint, revision uint64, graph string) (uint64, error) {
result := db.DB(ctx).Model(&OpenFlareWAFRuleGroup{}).
Where("id = ? AND revision = ?", id, revision).
Updates(map[string]any{"graph": graph, "revision": gorm.Expr("revision + 1")})
if result.Error != nil { return 0, result.Error }
if result.RowsAffected != 1 { return 0, ErrWAFRuleRevisionConflict }
return revision + 1, nil
}
```
绑定替换在事务中按输入下标写 `Sequence: index`;查询显式 `Order("sequence asc").Order("id asc")`。
- [ ] **Step 5: 运行测试并提交**
Run: `go test ./internal/model ./internal/db/migrator/... -count=1`
Expected: PASS。
Commit: `feat(waf): add graph persistence and binding order`
---
### Task 2: 图类型、默认图和强校验器
**Files:**
- Create: `internal/apps/openflare/waf/graph_types.go`
- Create: `internal/apps/openflare/waf/graph_validate.go`
- Create: `internal/apps/openflare/waf/graph_validate_test.go`
- Modify: `internal/apps/openflare/waf/errs.go`
**Interfaces:**
- Produces: `RuleGraph`, `RuleNode`, `RuleEdge`, `DefaultRuleGraph() RuleGraph`, `ValidateRuleGraph(ctx context.Context, graph RuleGraph, ipGroupExists func(context.Context, uint) (bool, error)) error`。
- Consumes: Task 1 的 JSON 持久化字段。
- [ ] **Step 1: 写表驱动失败测试**
覆盖合法默认图、重复 start/allow、环、不可达节点、悬空端口、错误 handle、同 handle 多目标、无终止路径、未知类型、无效 IP/CIDR、缺失 IP 组、非法国家/地区、PoW 范围和超限图。
```go
tests := []struct{name string; mutate func(*RuleGraph); want string}{
{"cycle", addCycle, "规则图不能包含循环"},
{"missing false edge", removeFalseEdge, "节点 match-1 的 false 出口未连接"},
{"unreachable", addUnreachableNode, "节点 orphan 无法从开始节点到达"},
}
```
- [ ] **Step 2: 运行测试确认失败**
Run: `go test ./internal/apps/openflare/waf -run TestValidateRuleGraph -count=1`
Expected: FAIL,类型和校验函数不存在。
- [ ] **Step 3: 定义带判别联合的图类型**
```go
type RuleNodeType string
const (
RuleNodeStart RuleNodeType = "start"
RuleNodeAllow RuleNodeType = "allow"
RuleNodeBlock RuleNodeType = "block"
RuleNodeIPMatch RuleNodeType = "ip_match"
RuleNodeGeoMatch RuleNodeType = "geo_match"
RuleNodePoW RuleNodeType = "pow"
)
type RuleGraph struct { SchemaVersion int `json:"schema_version"`; Nodes []RuleNode `json:"nodes"`; Edges []RuleEdge `json:"edges"` }
type RuleEdge struct { ID, Source, SourceHandle, Target string }
```
`RuleNode.Config` 先用 `json.RawMessage` 解码到明确的 `IPMatchConfig`、`GeoMatchConfig`、`PoWNodeConfig`、`BlockNodeConfig`,禁止透传未知字段。
- [ ] **Step 4: 实现结构和 DFS/Kahn 校验**
先检查大小、ID、类型、端口和配置,再用 Kahn 检测环,用从 start 的 DFS 检测可达性,用反向图从所有终止节点遍历检测终止性。错误文案携带节点/边 ID,供前端定位。
- [ ] **Step 5: 运行测试并提交**
Run: `go test ./internal/apps/openflare/waf -run 'Test(DefaultRuleGraph|ValidateRuleGraph)' -count=1`
Expected: PASS。
Commit: `feat(waf): validate composable rule graphs`
---
### Task 3: 运行态图编译器
**Files:**
- Create: `internal/apps/openflare/waf/graph_compile.go`
- Create: `internal/apps/openflare/waf/graph_compile_test.go`
**Interfaces:**
- Produces: `CompileRuleGraph(graph RuleGraph) (RuntimeRuleGraph, error)`、`ReferencedIPGroupIDs(graph RuleGraph) []uint`。
- Consumes: Task 2 的已校验图类型。
- [ ] **Step 1: 写编译快照测试**
断言位置和显示名不进入 JSON、节点通过 ID map O(1) 查找、出口按 handle 编译、IP 组 ID 去重排序。
```go
compiled, err := CompileRuleGraph(graph)
require.NoError(t, err)
assert.Equal(t, "start", compiled.Entry)
assert.Equal(t, "allow", compiled.Nodes["match"].Next["true"])
assert.Equal(t, []uint{2, 7}, ReferencedIPGroupIDs(graph))
```
- [ ] **Step 2: 运行测试确认失败**
Run: `go test ./internal/apps/openflare/waf -run 'TestCompileRuleGraph|TestReferencedIPGroupIDs' -count=1`
Expected: FAIL,编译接口不存在。
- [ ] **Step 3: 实现确定性编译**
输出结构只保留 `entry`、按节点 ID 索引的类型化运行配置和 handle→target 映射;序列化前对可排序切片排序,确保相同图生成相同快照和 checksum。
- [ ] **Step 4: 运行测试并提交**
Run: `go test ./internal/apps/openflare/waf -run 'TestCompileRuleGraph|TestReferencedIPGroupIDs' -count=1`
Expected: PASS。
Commit: `feat(waf): compile rule graphs for runtime`
---
### Task 4: 规则 API、修订冲突与有序绑定
**Files:**
- Create: `internal/apps/openflare/waf/rule_logics.go`
- Create: `internal/apps/openflare/waf/rule_routers.go`
- Create: `internal/apps/openflare/waf/rule_logics_test.go`
- Modify: `internal/apps/openflare/waf/logics.go`
- Modify: `internal/apps/openflare/waf/routers.go`
- Modify: `internal/router/v1/openflare/register_waf.go`
**Interfaces:**
- Produces: `CreateRuleInput{Name string}`、`SaveRuleGraphInput{Revision uint64; Graph RuleGraph}`、`UpdateRuleMetaInput{Name string; Enabled bool}`;创建、详情、元数据、图保存、删除和有序绑定 API。
- Consumes: Tasks 1–3 的模型、默认图和校验器。
- [ ] **Step 1: 阅读 new-api Skill,写逻辑与 Handler 失败测试**
覆盖只传名称创建默认图、空名 400、图非法 400、revision 冲突 409、绑定顺序往返不变、全局规则不能被路由绑定排序覆盖。
- [ ] **Step 2: 运行测试确认失败**
Run: `go test ./internal/apps/openflare/waf ./internal/router/v1/openflare -run 'Test(CreateRule|SaveRuleGraph|ReplaceSiteRuleGroups)' -count=1`
Expected: FAIL,API 输入与路由尚未实现。
- [ ] **Step 3: 实现 logic 与安全错误映射**
```go
func SaveRuleGraph(ctx context.Context, id uint, input SaveRuleGraphInput) (*RuleView, error) {
if err := ValidateRuleGraph(ctx, input.Graph, ipGroupExists); err != nil { return nil, err }
raw, err := json.Marshal(input.Graph)
if err != nil { return nil, err }
if _, err = model.UpdateOpenFlareWAFRuleGraph(ctx, id, input.Revision, string(raw)); err != nil { return nil, err }
return GetRule(ctx, id)
}
```
数据库/编码错误用 `pkg/logger` 记录后映射 `AbortInternal`;校验错误用 `AbortBadRequest`;revision 冲突用 `AbortConflict`。
- [ ] **Step 4: 注册路由并补全 Swagger**
保留 `/rule-groups` 路径以减少前端与兼容面变化,但将创建 payload 改为仅名称,新增 `POST /rule-groups/:id/graph` 与 `POST /rule-groups/:id/meta`。所有 `@Failure 400/404/409/500` 与统一 response envelope 完整声明。
- [ ] **Step 5: 运行测试并提交**
Run: `go test ./internal/apps/openflare/waf ./internal/router/v1/openflare -count=1`
Expected: PASS。
Commit: `feat(api): expose orchestrated waf rules`
---
### Task 5: 发布快照与规则顺序
**Files:**
- Modify: `internal/apps/openflare/config_version/snapshot.go`
- Modify: `internal/apps/openflare/config_version/logics_test.go`
- Create: `internal/apps/openflare/config_version/waf_graph_snapshot_test.go`
**Interfaces:**
- Produces: 发布快照中的 `rule_groups[].graph` 运行态 DAG、按 sequence 排序的 `bindings[].rule_group_ids`、图引用 IP 组集合。
- Consumes: Task 3 编译器与 Task 1 有序绑定。
- [ ] **Step 1: 写失败测试**
建立全局规则和两个自定义图,绑定顺序 `[customB, customA]`,断言快照保持该顺序、运行图无 position、只包含图引用的 IP 组;非法启用图阻止预览/发布。
- [ ] **Step 2: 运行测试确认失败**
Run: `go test ./internal/apps/openflare/config_version -run 'TestWAFGraphSnapshot|TestBuildSnapshotRejectsInvalidWAFGraph' -count=1`
Expected: FAIL,快照仍输出旧固定字段并按 ID 排序。
- [ ] **Step 3: 替换固定字段快照编译**
删除 `snapshotWAFRuleGroup` 的旧黑白名单/PoW 字段,加入 `Graph waf.RuntimeRuleGraph`;`buildSnapshotWAFIPGroups` 从所有编辑图的 `ReferencedIPGroupIDs` 聚合;绑定不再按 group ID 排序。
- [ ] **Step 4: 运行测试并提交**
Run: `go test ./internal/apps/openflare/config_version ./internal/apps/openflare/integration -count=1`
Expected: PASS。
Commit: `feat(waf): publish ordered runtime graphs`
---
### Task 6: OpenResty 内存 DAG 执行器
**Files:**
- Create: `internal/apps/agent/nginx/waf_runtime.lua`
- Create: `internal/apps/agent/nginx/waf_runtime_spec.lua`
- Modify: `internal/apps/agent/nginx/waf_assets.go`
- Modify: `internal/apps/agent/nginx/waf_assets_test.go`
- Modify: `internal/apps/agent/nginx/manager.go`
- Modify: `internal/apps/agent/nginx/manager_test.go`
- Modify: `internal/apps/agent/nginx/pow_assets.go`
**Interfaces:**
- Produces: `require("waf.runtime").check()`,模块加载时读取一次规则配置,请求时执行内存 DAG。
- Consumes: Task 5 的运行态快照;现有 PoW challenge/session 代码。
- [ ] **Step 1: 写 Lua 执行器失败测试**
用 stub `ngx` 覆盖 IP true/false、地域 true/false、PoW 接管/完成、多个 block 响应、全局前置、自定义顺序、未知节点 fail-closed、请求期间 `io.open` 调用次数为 0。
- [ ] **Step 2: 运行测试确认失败**
Run: `go test ./internal/apps/agent/nginx -run 'TestWAFRuntime' -count=1`
Expected: FAIL,运行时仍为固定链且每次请求读取配置。
- [ ] **Step 3: 将规则加载移到 Lua 模块初始化**
```lua
local rules_config = assert(load_json_once(runtime_dir .. "/waf_config.json"))
function _M.check()
local rules = active_rules_for_site(rules_config, ngx.var.openflare_waf_site or "")
for _, rule in ipairs(rules) do
local decision = execute_graph(rule.graph)
if decision.kind == "block" then return render_block(decision.config) end
end
end
```
在 `manager.go` 生成的 `http` 块中显式加入 `init_worker_by_lua_block { require("waf.runtime").init() }`,使新 Worker 在 reload 启动阶段完成读取与解析,而不是推迟到首个请求。模块缓存使每个 Worker 只解析一次;执行器设置最大步数为节点数,任何损坏图都记录限频错误并返回阻止响应。
- [ ] **Step 4: 将 PoW 变为节点执行接口**
抽取现有 PoW runtime 为 `pow.evaluate(config)`:完成返回 `true`,未完成直接输出/重定向挑战并返回接管标记。移除“按站点选择第一个 pow_enabled 规则”的旧扫描逻辑。
- [ ] **Step 5: 运行测试并提交**
Run: `go test ./internal/apps/agent/nginx ./internal/apps/agent/sync -count=1`
Expected: PASS。
Commit: `feat(agent): execute waf graphs from worker memory`
---
### Task 7: IP 组 checksum 与五秒内存刷新
**Files:**
- Create: `internal/apps/agent/nginx/waf_ip_groups.lua`
- Create: `internal/apps/agent/nginx/waf_ip_groups_spec.lua`
- Modify: `internal/apps/agent/sync/service.go`
- Modify: `internal/apps/agent/sync/service_test.go`
- Modify: `internal/apps/agent/nginx/waf_assets.go`
**Interfaces:**
- Produces: `waf_ip_groups.json.checksum`;Lua `ip_groups.current()` 返回 Worker 本地对象;协调刷新间隔固定 5 秒。
- Consumes: 现有 Agent IP 组同步 payload 与独立的 `ngx.shared.openflare_waf_ip_groups`(64 MiB);完整运行时快照上限为 20 MiB。
- [ ] **Step 1: 写失败测试**
断言 Agent 先原子替换 JSON、最后原子替换 checksum;Lua 稳定状态 15 秒只读取 checksum 3 次且不读 JSON;变化时全实例只读一次 JSON;非法新 JSON 保留旧对象。
- [ ] **Step 2: 运行测试确认失败**
Run: `go test ./internal/apps/agent/sync ./internal/apps/agent/nginx -run 'TestWAFIPGroup(Checksum|Refresh)' -count=1`
Expected: FAIL,checksum sidecar 和定时器不存在。
- [ ] **Step 3: Agent 写 checksum sidecar**
checksum 使用 Agent 已有快照 checksum;写入采用同目录临时文件、fsync/close、rename 的现有原子文件工具。严格顺序为 JSON rename 成功后 checksum rename。
- [ ] **Step 4: 实现协调 Worker 刷新**
```lua
local function tick(premature)
if premature then return end
local ok = shared:add("ip_refresh_lock", true, 4)
if ok then refresh_from_checksum() end
adopt_shared_snapshot_if_changed()
end
ngx.timer.every(5, tick)
```
协调 Worker 变化时把 raw JSON 和 checksum 写共享字典;每个 Worker 只在 shared version 变化时 decode 到模块局部 `current_groups`。请求只调用 `ip_groups.current()`。
- [ ] **Step 5: 运行测试并提交**
Run: `go test ./internal/apps/agent/nginx ./internal/apps/agent/sync -count=1`
Expected: PASS。
Commit: `perf(waf): refresh ip groups by checksum timer`
---
### Task 8: 前端类型、Service 与创建流程
**Files:**
- Modify: `frontend/package.json`
- Modify: `frontend/pnpm-lock.yaml`
- Modify: `frontend/lib/services/openflare/types.ts`
- Modify: `frontend/lib/services/openflare/waf.service.ts`
- Modify: `frontend/app/(main)/waf/page.tsx`
- Create: `frontend/app/(main)/waf/components/create-rule-dialog.tsx`
- Modify: `frontend/app/(main)/waf/components/rule-groups-table.tsx`
- Delete after replacement: `frontend/app/(main)/waf/components/rule-group-sheet.tsx`
- Test: `frontend/tests/unit/waf-rule-service.test.ts`
**Interfaces:**
- Produces: TypeScript 判别联合 `WAFRuleNode`、`WAFRuleGraph`、`WAFRule`;`WafService.createRule({name})`、`saveRuleGraph(id, {revision, graph})`。
- Consumes: Task 4 API。
- [ ] **Step 1: 阅读 shadcn 与 Next 本地文档,安装 React Flow**
Run: `cd frontend && pnpm add @xyflow/react`
Expected: `package.json` 与 lockfile 增加同一版本的 `@xyflow/react`。
- [ ] **Step 2: 写 Service 与创建流程失败测试**
断言创建 payload 只有 `{name}`,保存包含 revision,创建成功导航到 `/waf/rules/editor?id=<id>`,不再打开旧规则大表单。
- [ ] **Step 3: 运行测试确认失败**
Run: `cd frontend && pnpm vitest run tests/unit/waf-rule-service.test.ts`
Expected: FAIL,旧 payload 仍要求固定字段。
- [ ] **Step 4: 实现类型、Service 和名称对话框**
```ts
export type WAFRuleNode =
| {id: string; type: 'start'; position: XYPosition; config: Record<string, never>}
| {id: string; type: 'ip_match'; position: XYPosition; config: IPMatchConfig}
| {id: string; type: 'geo_match'; position: XYPosition; config: GeoMatchConfig}
| {id: string; type: 'pow'; position: XYPosition; config: PoWNodeConfig}
| {id: string; type: 'allow'; position: XYPosition; config: Record<string, never>}
| {id: string; type: 'block'; position: XYPosition; config: BlockNodeConfig};
```
静态方法作为 React Query 回调时继续用箭头函数包裹。列表创建成功后 `router.push('/waf/rules/editor?id=' + rule.id)`。
- [ ] **Step 5: 运行测试并提交**
Run: `cd frontend && pnpm vitest run tests/unit/waf-rule-service.test.ts && pnpm lint`
Expected: PASS。
Commit: `feat(frontend): create orchestrated waf rules`
---
### Task 9: React Flow 编排器与固定属性栏
**Files:**
- Create: `frontend/app/(main)/waf/rules/editor/page.tsx`
- Create: `frontend/app/(main)/waf/rules/editor/components/rule-flow-canvas.tsx`
- Create: `frontend/app/(main)/waf/rules/editor/components/rule-node.tsx`
- Create: `frontend/app/(main)/waf/rules/editor/components/node-library.tsx`
- Create: `frontend/app/(main)/waf/rules/editor/components/node-properties.tsx`
- Create: `frontend/app/(main)/waf/rules/editor/components/graph-validation.ts`
- Create: `frontend/app/(main)/waf/rules/editor/components/graph-validation.test.ts`
- Create: `frontend/app/(main)/waf/rules/editor/components/unsaved-changes.tsx`
**Interfaces:**
- Produces: 全宽 React Flow 编辑器;前端 `validateGraph(graph): GraphIssue[]`;Server 错误节点定位。
- Consumes: Task 8 类型与 Service。
- [ ] **Step 1: 写前端图校验失败测试**
覆盖唯一 start/allow、必需 handle、禁止环、不可达、终止性以及删除节点同步删边。
- [ ] **Step 2: 运行测试确认失败**
Run: `cd frontend && pnpm vitest run 'app/(main)/waf/rules/editor/components/graph-validation.test.ts'`
Expected: FAIL,校验器不存在。
- [ ] **Step 3: 实现页面骨架和数据状态**
`page.tsx` 直接维护 query、mutation、React Flow nodes/edges、dirty、selection 和右侧栏状态,不创建同名中转容器。根节点使用 `w-full py-6 px-1`,标题严格使用既定图标和 `h1` 规范。
- [ ] **Step 4: 实现节点、handle 和连线约束**
`start/pow` 只显示 `next` source handle,`ip_match/geo_match` 显示 `true`、`false`,`allow/block` 只显示 target handle。`isValidConnection` 阻止错误端口、同端口重复连接和形成环;start/allow 禁止删除。
- [ ] **Step 5: 实现固定右侧属性栏**
属性栏按节点判别联合渲染 IP/CIDR 与 IP 组多选、国家/地区多选、PoW 配置、阻止状态码与 HTML。颜色和阴影通过节点组件 variant/CSS 变量集中定义,不在业务调用点硬编码。
- [ ] **Step 6: 实现保存、冲突和未保存提示**
仅图合法时启用保存;409 显示“规则已在其他页面更新,请重新加载”;Server 返回节点/边 ID 时选中并聚焦;浏览器离开和应用内返回均提示未保存变更。
- [ ] **Step 7: 运行测试、构建并提交**
Run: `cd frontend && pnpm vitest run && pnpm lint && pnpm build`
Expected: PASS;静态导出包含 `/waf/rules/editor`。
Commit: `feat(frontend): add visual waf rule composer`
---
### Task 10: 路由绑定排序 UI 与旧界面清理
**Files:**
- Modify: `frontend/app/(main)/waf/components/site-binding-sheet.tsx`
- Modify: `frontend/app/(main)/proxy-routes/detail/components/waf-section.tsx`
- Modify: `frontend/app/(main)/waf/components/helpers.ts`(删除仅旧规则表单使用的导出;若清空则删除文件)
- Delete: `frontend/app/(main)/waf/components/pow-config-panel.tsx`
- Delete: `frontend/app/(main)/waf/components/rule-entry-dialog.tsx`
- Delete: `frontend/app/(main)/waf/components/rule-list-section.tsx`
- Test: `frontend/tests/unit/waf-binding-order.test.tsx`
**Interfaces:**
- Produces: 拖拽或上下移动的有序绑定列表,提交 ID 顺序不被排序。
- Consumes: Task 4 有序绑定 API 和 Task 8 Service。
- [ ] **Step 1: 写绑定顺序失败测试**
选择规则 A/B/C,移动为 C/A/B,断言 API payload 为 `{ids:[C,A,B]}`;全局规则单独展示为固定前置且不可拖动。
- [ ] **Step 2: 运行测试确认失败**
Run: `cd frontend && pnpm vitest run tests/unit/waf-binding-order.test.tsx`
Expected: FAIL,当前 UI 只表达集合。
- [ ] **Step 3: 实现排序并删除旧固定表单组件**
复用项目现有 `@dnd-kit/sortable`;为键盘用户提供上移/下移操作。清理旧字段、旧 PoW 面板和不再引用的 helper,保留 IP 组管理组件。
- [ ] **Step 4: 运行测试并提交**
Run: `cd frontend && pnpm vitest run && pnpm lint && pnpm build`
Expected: PASS。
Commit: `refactor(frontend): order waf bindings and remove legacy editor`
---
### Task 11: 旧后端字段清理、Swagger、中文文档与端到端验证
**Files:**
- Create: `internal/db/migrator/goose/postgres/202607150003_drop_legacy_waf_rule_fields.sql`
- Create: `internal/db/migrator/goose/sqlite/202607150003_drop_legacy_waf_rule_fields.sql`
- Modify: `internal/model/openflare_waf.go`
- Modify: `internal/apps/openflare/waf/logics_test.go`
- Modify: `docs/design/waf-design.md`
- Modify: `docs/guide/waf-usage.md`
- Modify: `docs/changelog/index.md`
- Generated: `docs/docs.go`, `docs/swagger.json`, `docs/swagger.yaml`
**Interfaces:**
- Produces: 无旧固定策略字段的最终 Schema 与中文使用文档。
- Consumes: Tasks 1–10 的完整替代实现。
- [ ] **Step 1: 写迁移与集成失败测试**
断言最终表不再包含 `block_status_code`、`ip_whitelist`、`ip_blacklist`、地域名单、`pow_enabled`、`pow_config`;端到端图分别产生 allow、block、PoW 接管,IP 组变化在 5–10 秒内生效。
- [ ] **Step 2: 运行测试确认失败**
Run: `go test ./internal/apps/openflare/integration ./internal/model -run 'TestOrchestratedWAF|TestLegacyWAFColumnsRemoved' -count=1`
Expected: FAIL,旧列仍存在。
- [ ] **Step 3: 删除旧列和旧代码路径**
PostgreSQL 直接 `DROP COLUMN`;SQLite 使用项目支持版本的 `DROP COLUMN` 或重建表迁移并复制 `id/name/enabled/is_global/graph/revision/timestamps`。删除 Go model/view/input 中的旧字段和固定链 helper,确保仓库中业务代码不再引用它们。
- [ ] **Step 4: 更新中文文档与 changelog**
`waf-design.md` 删除固定链作为现行设计的表述,链接可编排设计;`waf-usage.md` 写创建、节点语义、绑定顺序、发布生效和迁移警告;`[Unreleased]` 增加 WAF 可视编排、发布加载和 IP 组刷新条目。
- [ ] **Step 5: 生成 Swagger 并运行全量验证**
Run: `make swagger`
Expected: PASS,生成文件包含新 graph/meta API 与 409 response。
Run: `go test ./... -count=1`
Expected: PASS。
Run: `cd frontend && pnpm vitest run && pnpm lint && pnpm build`
Expected: PASS。
Run: `make code-check`
Expected: PASS,无格式、lint、测试或生成文件差异。
- [ ] **Step 6: 最终人工数据面验收**
创建规则并编排 `开始 → IP 匹配 → true:通过 / false:地域匹配 → true:阻止A / false:PoW → 通过`,绑定到测试路由并发布。用命中/未命中 IP、不同 GeoIP 和无 PoW cookie 请求验证三个分支;更新引用 IP 组后不发布,确认 5–10 秒内结果变化且 OpenResty 未 reload。
- [ ] **Step 7: 提交**
Commit: `feat(waf): complete composable rule orchestration`
## 4. 最终验收标准
- 用户新增规则时只输入名称并立即进入 React Flow 编排器。
- 默认规则为 `开始 → 通过`;特殊节点与处理节点满足设计约束。
- Server 和前端均拒绝非法图,发布再次校验,revision 冲突返回 409。
- 全局规则固定前置,自定义规则严格按绑定顺序执行。
- OpenResty 请求路径对规则与 IP 组均为纯内存读取。
- 规则只在发布 reload 时加载;IP 组每 5 秒 checksum 检查且仅变化时读取完整 JSON。
- PostgreSQL、SQLite、Go、Lua、前端、Swagger、构建与 `make code-check` 全部通过。
@@ -1,99 +0,0 @@
# 边缘可观测与业务流量统计重构 — 实现计划
说明:本计划对应设计文档 [observability-design.md](../design/observability-design.md)。重大架构重构,按阶段交付,避免一次大爆炸。
---
## 0. 落地进度(2026-07-18)
* [x] M1 看板业务趋势改读 access log;网络图文案改为已提供/接收 + 宿主机网卡
* [x] M2 协议 v2 字段(host_metrics/edge_health/request_length);CH 列 `request_length`/`request_time_ms`
* [x] M3 Agent:观测口仅健康连接;payload 不再发 TrafficReport;access_logs 带 request_length
* [x] M4 Server:停写 TrafficReport;openresty 仅存 connections;明细入库带 request_length
* [x] 分布图 status/top domains + 节点行请求/UV 改 access log;24h UV 用 uniqExact;API bytes_provided/received
* [x] M5:`of_node_edge_health`、`of_access_log_hourly`(+MV);删除 request_reports/traffic_hourly/openresty_hourly/obs_openresty;写入/查询改道
* [x] 收尾:清 openresty hourly / request_report 死路径;edge_health 写全 status;cleanup 命名 `node_edge_health`;hourly 回填 SQL + UV 策略文档
* [x] 协议/API 去兼容层(Agent 销毁重建):删除 TrafficReport / openresty_observation / snapshot 别名 / request_reports API 字段 / openresty_rx|tx
* [x] 前端 UV 文案:24h/查询窗口独立访客;趋势图不绘分时 UV
* [ ] 真实环境 ClickHouse 迁移 + `202607180003` 回填(本机 Docker 未起时需运维执行)
## 1. 目标与背景 (Goal & Context)
* **需求背景**:看板「OpenResty 入/出站」与 Zone「已提供数据」不一致;Agent 预聚合与访问日志双轨;`openresty_tx` 与 `bytes_sent` 业务语义重复。
* **开发范围 (Scope)**:
* **必做**:业务趋势统一为访问日志聚合;UI 字段与文案收敛;协议补齐 `request_length`;停用预聚合作为权威源;Agent 瘦身。
* **后续**:废弃 CH 表清理、hourly rollup 性能优化、Relay 指标对齐。
* **Out of Scope**:通用日志平台、替换 ClickHouse、APM。
---
## 2. 设计与决策 (Design & Decisions)
* **核心对象**:以 `of_node_access_logs` 为 L1 权威;主机 snapshot 为 L3;OpenResty 仅健康/连接为 L2。
* **传输模型(示例与频率)**:见 [observability-transport-model.md](../design/observability-transport-model.md)。
* **协议与表结构**:见 [observability-data-model.md](../design/observability-data-model.md)(NodePayload v2、落库流水线、DDL、废弃表)。
* **API**:看板与 Zone 共用聚合语义;`bytes_provided` / `bytes_received`(兼容 `bytes_sent` 别名)。
* **数据流**:见 [observability-design.md](../design/observability-design.md) §5。
* **权衡**:性能用 Server 侧 rollup,不恢复 Agent 预聚合。
---
## 3. 阶段与修改清单 (Proposed Changes)
### 阶段 M1 — 读路径切换(优先对账)
* #### [MODIFY] `internal/apps/openflare/dashboard/*`、`observability/analytics.go`
* 业务 24h 趋势改为 access log 聚合(全局)。
* 网络趋势中业务曲线与主机网卡分离。
* #### [MODIFY] 前端 dashboard 组件与文案
* 「OpenResty 出站/入站」→「已提供数据/接收数据」或拆卡片。
* #### [MODIFY] Zone stats 字段对齐(如需别名)
* **验收**:单 Zone 流量时看板已提供 ≈ Zone 已提供。
### 阶段 M2 — 协议与入库补齐
* #### [MODIFY] `pkg/protocol/agent.go` — `NodeAccessLog.request_length`
* #### [MODIFY] Agent 解析与 CH 写入列
* #### [MODIFY] goose ClickHouse migration(如缺列)
### 阶段 M3 — 停写预聚合权威路径
* #### [MODIFY] Server persist:TrafficReport / openresty rx/tx 不再驱动看板
* 可选:直接停写以减 CH 压力
### 阶段 M4 — Agent 瘦身
* #### [MODIFY] 移除 TrafficReport 构建主路径、Lua 业务 dict 计数、state 内业务累计
* #### [MODIFY] 心跳仅明细 + snapshot + 连接/健康
### 阶段 M5 — 清理
* 删除废弃 API 字段、前端类型、CH 表/MV、相关测试夹具
* 更新 agent-design / changelog(代码变更时)
---
## 4. 验证计划 (Verification Plan)
### 自动化
* `go test`:zone stats、dashboard 聚合、agent access log 解析
* 前端:zone / dashboard 文案与字段测试
### 手动
* 制造已知大小响应,对比 Zone 与看板 24h 已提供数据
* 确认宿主机网卡曲线与业务已提供数据分区展示、数值可不一致且文案不诱导对账
### 质量门禁
* `make swagger`(若 API 变更)
* `make code-check`
* `make prettier`
---
## 5. 依赖与风险
* 明细量大时 M1 需同步评估 hourly rollup(仍 Server 侧)。
* 旧 Agent 无 `request_length` 时接收数据为空,需 UI 降级。
@@ -1,63 +0,0 @@
# 访问日志 cache_status 明细可见 — 实现计划
说明:对应设计 [observability-data-model.md §3.5.1](../design/observability-data-model.md)。第一期只做明细可见,不上报 upstream 地址。
---
## 1. 目标与背景
* **需求背景**:访问日志无法判断请求是否命中边缘缓存、是否回源。
* **开发范围 (Scope)**:
* **必做**:OpenResty 日志输出 `$upstream_cache_status`;Agent 上报;CH 入库;列表/详情展示三态标签。
* **Out of Scope**:命中率看板、hourly 维度、`upstream_addr`。
---
## 2. 设计与决策
* **唯一字段**:`cache_status` string(原始值)。
* **UI 三态(不落库)**:
* 命中:`HIT` / `STALE` / `REVALIDATED` / `UPDATING`
* 回源:`MISS` / `EXPIRED`
* 未缓存:`BYPASS` / `-` / 空
* **数据流**:log_format → Agent parse → protocol → Server model → CH → API → 前端明细。
---
## 3. 修改清单
### 边缘 / 协议
* `pkg/render/openresty/types.go`、`internal/model/openflare_option.go`:`log_format` 增加 `cache_status`
* `internal/apps/agent/observability/traffic.go`:解析与映射
* `pkg/protocol/agent.go`:`NodeAccessLog.CacheStatus`
### Server / CH
* goose:`202607180005_access_log_cache_status.sql`
* `internal/model/analytics/node_access_log.go`、writer、list/scan、store 映射
* `internal/model/openflare_observability.go`、agent build records
* API `AccessLogView` + list 响应带 `cache_status`
### 前端
* types / 明细列表标签 / 详情字段
* 三态 helper:`resolveCacheOutcome(cache_status)`
---
## 4. 验证
* `go test ./internal/apps/agent/observability/ ./internal/repository/analytics/ ./internal/apps/openflare/agent/`
* `make swagger`(若 Handler 响应结构变更)
* `make code-check` / `make prettier`
---
## 5. 落地进度
* [x] log_format + protocol + agent parse
* [x] CH migration + 写入/读取
* [x] API + 前端明细展示
* [x] 缓冲去重 key 含 cache_status;保留 `-` 原始值
* [x] 测试与提交
@@ -1,38 +0,0 @@
# 边缘缓存默认 static 策略 — 实现计划
对应设计:[edge-cache-design.md](../design/edge-cache-design.md)
## 目标
路由开启缓存后,**新建推荐**仅缓存标准静态扩展名(`static`);存量 `url`/空策略映射为 `all`,不收窄缓存范围。
## 兼容规则(评审后定稿)
| 场景 | 行为 |
| --- | --- |
| 已启用 + `''` / `url` | 读 API / 快照 / 渲染 → **`all`** |
| 写入时 enabled 且 policy 为空 | 规范为 **`all`**(旧客户端兼容) |
| UI 新建/推荐默认 | **显式提交** `static` |
| 关闭缓存 | policy 存 `''`,rules 清空 |
## 修改清单
1. **渲染** `pkg/render/openresty/render.go`:`static` 内置扩展名;空/`url`/`all` 无路径限制
2. **校验/展示** `proxy_route/helpers.go`:`normalizeCachePolicy` + `displayCachePolicy`
3. **快照** `config_version/logics.go`:`normalizeSnapshotCachePolicy`
4. **前端** `cache-section.tsx` + helpers:存量 empty/url→`all`;关闭时提交 `''`;新建默认 `static`
5. **测试** render + proxy_route
6. **设计/changelog** 同步兼容说明
## 验证
```bash
go test ./pkg/render/openresty/ ./internal/apps/openflare/proxy_route/ ./internal/apps/openflare/config_version/
# 已通过(2026-07-18)
```
## 状态
- [x] 功能实现 + 评审修复(empty→all,禁止静默收窄)
- [ ] 提交 `fix(cache): ...`(待用户确认)
- [ ] 合并 / 发布后需重新发布节点配置
@@ -1,71 +0,0 @@
# ClickHouse 观测表迁移与小时汇总回填(运维手册)
适用:M5 观测存储(`of_node_edge_health`、`of_access_log_hourly`、删旧表)及历史小时回填。
## 前提
* 控制面 `config.yaml` / 环境变量中 ClickHouse 已启用,账号可写 `openflare` 库。
* 备份策略已就绪(可选:对 `of_node_access_logs` 做快照)。
* **Agent 升级策略为销毁重建**;勿混跑旧 Agent(旧协议字段已从 Server 删除)。
## 1. 自动迁移(推荐)
进程启动时 `migrator.MigrateClickHouse()` 会按 goose 顺序执行:
| 版本 | 作用 |
| --- | --- |
| `202607180001` | access log 增加 `request_length` / `request_time_ms` |
| `202607180002` | 建 `of_node_edge_health`、`of_access_log_hourly`(+MV);删 request_reports / openresty 吞吐表 |
| `202607180003` | 从明细 ANTI JOIN 回填近 90 天 `of_access_log_hourly` |
启动 API / all 模式一次即可:
```bash
# 示例:本地
./bin/openflare api
# 或
make run # 以项目实际入口为准
```
查看 goose 版本表(ClickHouse)确认三版本均已应用。
## 2. 仅回填(迁移已执行、MV 创建前缺历史)
若只需重跑回填 SQL:
```bash
clickhouse-client --host 127.0.0.1 --port 9000 \
--user default --password "$CLICKHOUSE_PASSWORD" \
--database openflare \
--multiquery < internal/db/migrator/goose/clickhouse/202607180003_backfill_access_log_hourly.sql
```
(goose 文件含 `+goose Up` 注释,若 client 报错可去掉注释行后执行 INSERT 主体。)
回填可重复:`ANTI JOIN` 跳过已有 `(node_id, hour, host)`。
## 3. 验收
```sql
-- 新表存在
SHOW TABLES FROM openflare LIKE 'of_node_edge_health';
SHOW TABLES FROM openflare LIKE 'of_access_log_hourly';
-- 旧表应不存在
SHOW TABLES FROM openflare LIKE 'of_node_request_reports';
SHOW TABLES FROM openflare LIKE 'of_node_obs_openresty';
-- 小时汇总有数据(有历史访问时)
SELECT count() FROM of_access_log_hourly;
SELECT min(hour), max(hour), sum(request_count) FROM of_access_log_hourly;
```
看板 24h 请求趋势应优先走 hourly;UV 卡片为整窗独立访客,**不等于**小时 UV 之和。
## 4. 本机执行记录
| 日期 | 环境 | 结果 |
| --- | --- | --- |
| 2026-07-18 | 开发机 | Docker daemon 未启动,未能 live 迁移;SQL 与 goose 文件已入库 |
运维在目标环境按 §1–§3 执行后更新本表。

Some files were not shown because too many files have changed in this diff Show More