Compare commits

...

896 Commits

Author SHA1 Message Date
ryan 0099ce7cb4 Merge branch 'main' into cordis 2026-09-17 21:43:50 +08:00
ryan c6ae84de81 refactor(layout): adopt wavelet manifest layout for configs and docker while preserving docker-compose 2026-09-03 10:46:51 +08:00
ryan 953a224245 refactor(arch): decouple private imports, enforce contracts and comply with cordis architecture 2026-09-03 10:40:02 +08:00
ryan dbcf485d8a Merge remote-tracking branch 'wavelet/main' 2026-09-03 10:27:18 +08:00
ryan 14d9bddf46 feat(contracts): implement driver.Valuer and sql.Scanner on UploadMetadataDTO 2026-09-03 10:27:08 +08:00
ryan 630803d5a3 merge: sync upstream wavelet/main (clean contracts DTOs and configs) 2026-09-03 10:25:52 +08:00
ryan 99fff5d5af fix(contracts): decouple DTOs from ORM tags and table name mappings 2026-09-03 10:17:11 +08:00
ryan 807343c82c feat(config): load manifest config.default.yaml with config.yaml override and clean root configs 2026-09-03 10:13:57 +08:00
ryan 25c3249ce2 Merge branch 'wavelet/main' into main 2026-09-03 10:08:50 +08:00
ryan 967c3e4209 feat(contracts): add TableName and gorm serializer tags to DTOs 2026-09-03 10:08:11 +08:00
ryan 52daf8129d Merge remote-tracking branch 'wavelet/main' 2026-09-03 09:56:59 +08:00
ryan 1d4bfea63b feat(contracts): add UploadMetadataDTO to contracts.UploadDTO 2026-09-03 09:56:54 +08:00
ryan 911a42fa10 Merge remote-tracking branch 'wavelet/main' 2026-09-03 09:42:15 +08:00
ryan c88c1c7b4e feat(contracts): provide SystemConfigService, UploadService and TaskService.GetExecutionByTaskID 2026-09-03 09:42:12 +08:00
ryan ed57e44e3c ci(arch): include openflare in cordis architecture checks 2026-09-03 09:32:43 +08:00
ryan b183e80565 merge(wavelet): sync upstream changes 2026-09-03 09:29:28 +08:00
ryan a190bd6a13 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	AGENTS.md
#	backend/docs/docs.go
#	backend/docs/swagger.json
#	backend/docs/swagger.yaml
#	frontend/components/providers/title-updater.tsx
#	frontend/messages/fragments/admin.en.json
#	frontend/messages/fragments/admin.zh-CN.json
#	frontend/proxy.ts
2026-09-03 09:16:22 +08:00
ryan 4407589b62 refactor(auth): modularize auth plugin with physical subpackages and decoupled services 2026-09-03 09:12:44 +08:00
ryan 2124bce7ca fix(core): optimize ioc interface caching, event parallel timeout and router teardown reversibility 2026-09-03 09:09:33 +08:00
ryan 30ab8810cc refactor(auth): merge cap domain plugin into auth 2026-09-03 08:58:34 +08:00
ryan 6b28adfacf refactor(admin): decouple system cleanup with event bus and enforce single owner principle 2026-09-03 08:50:37 +08:00
ryan 1e19d8114a refactor(msg_gateway): decouple bot gateway and push notification architecture
- Split shared monolithic consts into bot, push, and errs with typed sentinel errors
- Restructure model layer into distinct bot and push subdomains
- Refactor DAO layer to enforce single-owner principle and remove cross-table raw SQL queries
- Decompose 1150+ line service/push.go into push_channel, push_event, push_trigger, push_worker, and push_template
- Clean up controller layer with generic request handlers and parameter validation in controller/base.go
- Streamline plugin.go to core Cordis lifecycle orchestration and remove re-export bloat
- Verify all unit tests, race tests, Cordis architecture rules, and Swagger generation pass cleanly
2026-09-02 23:21:36 +08:00
ryan 8395dd5019 refactor(msg_gateway): restructure and rename message_gateway aligned with custom_example 2026-09-02 22:50:40 +08:00
ryan 87e3bfd0e6 chore: doc 2026-09-02 22:41:06 +08:00
ryan fff3a7589c docs(plugin): unify plugin development template based on custom_example 2026-09-02 22:39:13 +08:00
ryan 6388c28b91 chore: template 2026-09-02 22:34:19 +08:00
ryan 7df31befb5 feat(message_gateway): integrate nikoksr/notify engine and support multi-channel push 2026-09-02 22:30:59 +08:00
ryan 90f3efdd50 ci(arch): forbid pkg packages from depending on project core packages 2026-09-02 22:22:04 +08:00
ryan 9632604958 feat(auth): implement decoupled sliding-window rate limiting for login and oauth 2026-09-02 22:15:21 +08:00
ryan 39f02b5d7a refactor(message_gateway): upgrade notification template engine with text/template and rich helpers 2026-09-02 22:04:48 +08:00
ryan cf0cba0679 refactor(mail): modernize smtp sending with go-mail and unify message gateway pusher 2026-09-02 21:59:47 +08:00
ryan 88ed98b013 chore: remove accidental test upload
fix(user): backfill snowflake id on login for legacy zero user
2026-09-02 21:47:12 +08:00
ryan 40c2212dd3 fix(upload): apply login middleware to /f/:id route
/f/:id had no LoginRequired middleware, so AuthUserObjKey was never
populated and GetCurrentUser/GetUserIDFromContext could not authenticate
even logged-in users, returning 401 未登录 on private files. Add loginMW.
2026-09-02 20:32:33 +08:00
ryan df6aa9ff4d fix(auth): encode snowflake user ids as strings in session and /user-info
Registered users get snowflake ids above JS MAX_SAFE_INTEGER.
/user-info emitted them as JSON numbers and login stored uint64 in
the session. Both now use decimal strings. Tests cover admin vs
non-admin cookie access to /user/self, /user-info, and /upload/my.
2026-09-02 20:22:22 +08:00
ryan c27da41b64 fix(auth): forward session cookies through the Next API proxy
Login Set-Cookie was dropped by Next rewrites, so non-admin sessions
never stuck and every later API looked unauthenticated. Proxy JSON
APIs in proxy.ts, copy Set-Cookie, send 401 to login and 403 to /403.
2026-09-02 18:49:42 +08:00
ryan 353a5f9f75 fix(user): assign snowflake IDs on registration
The HTTP register path left ID at 0, so SQLite/GORM filled a
serial primary key. CreateUser now generates a snowflake ID when
none is set, matching admin create and OAuth signup.
2026-09-02 18:41:37 +08:00
ryan 05606dfb56 feat(frontend): add a dedicated 403 forbidden page
Show /403 instead of toasting or staying on the denied screen when
the API returns 403 or a non-admin opens an admin route.
2026-09-02 18:39:11 +08:00
ryan b7e5e811d1 fix(auth): register CAP scope, 400 on captcha, 403 for permission
Navigating from login reused a send_email_code token on register.
Captcha failure used 401 so the client stored /register as the
post-login target and never left the page. Permission denials now
return 403, and the API client no longer wipes the session on 401.
2026-09-02 18:26:22 +08:00
ryan df7ad453cc fix(tasks): canonicalize triggered_by so execution labels resolve
Unknown values such as http and inproc_cron made the admin UI call
t(undefined). Dispatch sites now write system/manual/retry/schedule,
the list API maps legacy rows, and the table skips missing i18n keys.
2026-09-02 18:04:32 +08:00
ryan 1f1f4efec7 fix(admin): stop console Intl errors and log websocket drops
Use raw i18n for push template hints so ICU does not parse
{{placeholders}}. Pass total into the user list record count.
Allow log websocket origins behind the Next rewrite, skip the
proxy on Upgrade, and do not open a socket after unmount.
2026-09-02 17:52:57 +08:00
ryan 8aa0753b12 fix(logs): flush small access-log batches within two seconds
Default MinBatchSize of 50 left quiet admin traffic in memory
forever because MaxFlushWait was unset. Force a timed flush so
the logs page can show recent authenticated requests.
2026-09-02 17:40:43 +08:00
ryan bec1352ef7 fix(logs): collect access logs regardless of plugin order
Global Router.Use middleware is applied at HTTP Start instead of
being snapshotted when each route is registered, so risk_control
still wraps admin APIs that mount earlier. Access-log collection
is enabled by default on SQLite/Postgres, not only ClickHouse.
2026-09-02 17:39:40 +08:00
ryan ef88811ccb fix(frontend): call versioned CAP challenge and redeem APIs
Point the PoW solver at /api/v1/cap/{challenge,redeem} so login
verification hits the routes registered by the cap plugin.
2026-09-02 17:31:06 +08:00
ryan 455e2f8be5 fix(config): serve public settings and enforce login CAP
Public config now comes from admin as a flat visibility=1 map instead of
a cross-plugin query that compared an integer column to "visible". Login
and register resolve CaptchaService per request so CAP is not skipped
when user applies before cap.
2026-09-02 17:07:07 +08:00
ryan 4f50f6a8f9 feat(core): bind request services and implement registered tasks
Wire plugin services through Bind/InjectFrom and AppContext so HTTP and
workers resolve dependencies after Apply. Register TaskHandler objects
with persisted results, and implement send_email_code, mail:send,
cleanup_inactive_users, and dispatch_bot_msg.
2026-09-02 16:59:00 +08:00
ryan 30bbe965bf fix(task): execute dispatched jobs and persist run records
Asynq func handlers now go through ProcessTask so admin execution
rows leave pending. The in-process worker resolves admin type
identifiers and writes the same w_task_executions table. Remove
the no-op admin system_cleanup that shadowed the upload handler.
2026-09-02 16:11:45 +08:00
ryan 33f28ad671 fix: sql 2026-09-02 15:37:06 +08:00
ryan 18339ee3d4 fix: pg sql 2026-08-31 15:48:28 +08:00
ryan f975c4f7cc merge(cordis): abort helpers and gold upgrade fixture
Bring Wavelet AbortNotFoundIfMissing/AbortBadRequestOnError, switch OpenFlare handlers to them, and pin the golden upgrade test to v3.5.4 via git archive.
2026-08-30 18:03:04 +08:00
ryan c22ca408d4 merge: merge branch 'feat/cordis-router-raw-routes' into main 2026-08-30 17:57:38 +08:00
ryan 374289bfda fix(api): align upload permissions and mount robots and swagger routes 2026-08-30 17:53:42 +08:00
ryan 7ce75d1dd0 test(cmd): extract gold v3.5.4 via git archive
Build the upgrade fixture from commit 9f79fb99 instead of the gold working tree, which no longer has main.go at the repo root.
2026-08-30 17:48:50 +08:00
ryan b216175ee2 refactor(share): move githubrelease to openflare/share 2026-08-30 17:47:44 +08:00
ryan b72b1cfc16 refactor(server): use Wavelet response abort helpers
Call response.AbortNotFoundIfMissing and AbortBadRequestOnError from handlers and drop the OpenFlare-local copies.
2026-08-30 17:46:45 +08:00
ryan 7c5c196ede feat(response): add AbortNotFoundIfMissing and AbortBadRequestOnError
Lift the handler helpers that map a non-nil error to Abort* so plugins do not each reimplement record-not-found vs bad-request branching.
2026-08-30 17:45:06 +08:00
ryan a7c3b6a670 merge(wavelet): pull AbortNotFoundIfMissing helpers
Use Wavelet pkg/response for record-not-found and bad-request abort helpers.
2026-08-30 17:45:06 +08:00
ryan c93ff6674f refactor(backend): rename OpenFlare directory to lowercase openflare 2026-08-30 17:43:23 +08:00
ryan 06d5fedbfc refactor(server): nest kernel and domain under classified roots
Keep only plugin entry, httpapi, migrate and updater at the server root. Shared model/repository/adapters live in kernel/; product bounded contexts live in domain/.
2026-08-30 17:35:26 +08:00
ryan f064237081 refactor(server): group OpenFlare domains into bounded contexts
Drop the openflare/ and router/v1 nesting. Product code lives under site, fleet, pages, waf, tls, cloudflare, observability, dashboard and option; HTTP wiring is httpapi. Shared model/repository stay the kernel.
2026-08-30 17:31:15 +08:00
ryan f14e9591e0 test(cmd): assert fresh install seeds of_* schedules
Cover the compressed 00001 seed path so new sqlite databases get the four OpenFlare schedules and not of_database_auto_cleanup.
2026-08-30 16:58:48 +08:00
ryan 7af6fee5d5 fix(core): apply earlier pending plugins before later ones
Rescan the Use() list after each Load so a consumer registered before its provider still runs before later consumers that became ready in the same pass.
2026-08-30 16:58:20 +08:00
ryan fb5cfbfd30 merge(wavelet): honor Use() order when reconciling plugins
Pull the core reconcile rescan so admin migrations run before OpenFlare server seeds that insert into w_schedules.
2026-08-30 16:58:20 +08:00
ryan d531d71778 refactor(server): merge migrate package and drop unused admin/migrator trees
Collapse stamp, of_* SQL and ClickHouse into server/migrate, hoist updater out of admin, and delete the unused 76-file historical chain.
2026-08-30 16:54:08 +08:00
ryan 3e8a777817 fix(server): seed OpenFlare schedules and configs in 00001
Fold gold-minus-Wavelet w_system_configs keys and the four of_* schedules into the compressed initial migration so new installs get product defaults.
2026-08-30 16:48:11 +08:00
ryan 471d237af0 test(cmd): require v1 cap and /api/healthz only
Update route assertions and regenerate swagger so the process no longer documents /api/cap or /api/health.
2026-08-30 16:44:10 +08:00
ryan 8931c3559c fix(frontend): call /api/v1/cap and document /api/healthz
Point the login captcha solver at the versioned challenge/redeem endpoints and document the remaining health probe.
2026-08-30 16:42:33 +08:00
ryan 0d53be2896 merge(wavelet): pull v1-only cap and healthz routes
Bring Wavelet feat/cordis-alignment so OpenFlare registers /api/v1/cap and GET /api/healthz only.
2026-08-30 16:41:24 +08:00
ryan 6553ac7782 fix(system): expose only GET /api/healthz
Remove /healthz and /api/health so the process advertises a single probe at /api/healthz with {status: ok}.
2026-08-30 16:41:07 +08:00
ryan 12b4c3e54c fix(cap): keep only /api/v1/cap routes
Drop the unversioned /api/cap aliases so Challenge and Redeem exist only under /api/v1/cap.
2026-08-30 16:39:40 +08:00
ryan d1e5c9acd8 docs(cordis): 添加 API 收口与 server 目录整理计划
Wavelet 去掉 cap/health 别名,前端改 v1;00001 补 OF 种子;
合并 migrate/updater 并删除 76 条历史 SQL。
2026-08-30 16:36:19 +08:00
ryan 14232838dd docs(cordis): 记录 API 收口与 server 目录整理设计
cap/health 只留 v1 或 /api/healthz;server 合并 migrate 包;
压缩 00001 必须含 of_* 建表与 OpenFlare 种子数据。
2026-08-30 16:33:22 +08:00
ryan a347c33608 chore(cordis): persist merge.ours and isolate wavelet extras
Add a repo .gitconfig so merge=ours in .gitattributes can actually run.
Disable the Wavelet canary image workflow and keep docker-compose.yaml
as the OpenFlare default so a merge cannot ship the wrong product.
2026-08-30 15:00:44 +08:00
ryan 220c469ee7 fix(build): embed frontend into driver_http dist
Copy the static export to backend/plugins/drivers/driver_http/dist so
Wavelet's //go:embed all:dist actually ships the OpenFlare UI. Point
release builds at backend/go.mod and assert index.html after copy.
2026-08-30 15:00:30 +08:00
ryan df271ff708 chore(cordis): drop rsync sync after git upstream is connected 2026-08-30 14:32:18 +08:00
ryan c68038ad06 Merge remote-tracking branch 'wavelet/feat/cordis-alignment' into cordis
# Conflicts:
#	.agents/skills/cache-framework/SKILL.md
#	.agents/skills/clickhouse-batchwriter/SKILL.md
#	.agents/skills/database-migration/SKILL.md
#	.agents/skills/file-upload/SKILL.md
#	.agents/skills/logstore/SKILL.md
#	.agents/skills/new-api/SKILL.md
#	.agents/skills/new-api/references/handler_example.go
#	.agents/skills/new-api/references/logics_example.go
#	.agents/skills/new-api/references/service_example.go
#	.agents/skills/new-async-task/SKILL.md
#	.agents/skills/new-async-task/references/CODE-EXAMPLES.md
#	.agents/skills/new-setting/SKILL.md
#	.agents/skills/push-notification/SKILL.md
#	.agents/skills/release-guide/SKILL.md
#	.auto/checks.sh
#	.auto/ideas.md
#	.auto/log.jsonl
#	.auto/measure.sh
#	.auto/prompt.md
#	.dockerignore
#	.env.example
#	.github/copilot-instructions.md
#	.github/workflows/build-release.yml
#	.gitignore
#	.golangci.yml
#	AGENTS.md
#	Makefile
#	README.md
#	backend/cmd/app.go
#	backend/cmd/app_test.go
#	backend/cmd/banner.go
#	backend/cmd/banner_test.go
#	backend/docs/docs.go
#	backend/docs/swagger.json
#	backend/docs/swagger.yaml
#	backend/go.mod
#	backend/go.sum
#	backend/main.go
#	config.example.yaml
#	docker/Dockerfile
#	docker/Dockerfile.backend
#	docker/Dockerfile.cross
#	scripts/swagger.sh
#	scripts/update_go_license.sh
2026-08-30 14:30:56 +08:00
ryan 4fb47e65f4 chore(git): keep OpenFlare-owned paths on merge 2026-08-30 14:28:38 +08:00
ryan 56c650c5b5 docs(swagger): restore gold @Router comments on platform APIs 2026-08-30 14:20:21 +08:00
ryan 6cabad3197 fix(cordis): satisfy L2 test and swagger gates 2026-08-30 14:17:51 +08:00
ryan 2d0f2ef3df test(cmd): upgrade sqlite/postgres from OpenFlare v3.5.4 golden 2026-08-30 14:03:51 +08:00
ryan d3a91dcc3f feat(server): stamp legacy goose versions and own of_* migrations only 2026-08-30 13:43:38 +08:00
ryan 6c7090aa8e fix(server): route system config and lookups through Wavelet contracts 2026-08-30 13:25:11 +08:00
ryan 8b24af1310 refactor(server): drop Wavelet-duplicated domains and use contracts 2026-08-30 13:00:13 +08:00
ryan e48485f27d feat(cmd): assemble control plane via Wavelet plugins plus server 2026-08-30 12:13:43 +08:00
ryan e847a7adb3 chore(cordis): sync Wavelet core/pkg/plugins after W1-W9 2026-08-30 11:46:58 +08:00
ryan a617457a3c feat(core): add WithMigrationBaseline hook before goose Up 2026-08-30 11:41:00 +08:00
ryan 4ce7110e23 feat(platform): add GET /api/health and GET /api/v1/user/self 2026-08-30 11:32:03 +08:00
ryan 9a5c2fa643 feat(message_gateway): expose PushRegistry contract 2026-08-30 11:23:01 +08:00
ryan 177d771acf feat(upload): mount existing my/update/download routes on user API 2026-08-30 11:17:11 +08:00
ryan 6f25618e83 fix(config): decode *bool so trailing-slash redirect binds from yaml/env 2026-08-30 11:10:36 +08:00
ryan b4c4b0a27e feat(http): make trailing-slash redirect configurable 2026-08-30 11:07:22 +08:00
ryan b8ad06f49f feat(system): allow PublicConfigProvider to replace public config payload 2026-08-30 11:03:17 +08:00
ryan 254533c013 feat(cap): expose CaptchaService and unversioned /api/cap routes 2026-08-30 10:53:43 +08:00
ryan be79eb4eb7 feat(core): add HandleRaw and BasePath for trailing-slash routes 2026-08-30 10:44:14 +08:00
ryan dd333643f2 docs(cordis): 添加与 Wavelet 对齐的实施计划
按 W1–W9、装配根、删平台副本、stamp 升级与 git merge 拆成
带测试的任务,金标准 v3.5.4 只读。
2026-08-30 10:37:13 +08:00
ryan 823bec1272 docs(cordis): 记录与 Wavelet 对齐的 Cordis 设计
明确框架与业务边界、Wavelet 通用扩展点、stamp 升级路径、
金标准库验证,以及以 git merge 接入上游且不改写历史。
2026-08-30 10:26:37 +08:00
ryan dbaa3bf140 feat(cordis): add OpenFlare Cordis 架构改造设计
docs(changelog): 修正表述笔误

refactor(cordis): 磁盘缓存改用上上游能力并清理本地副本

按上游/下游归属规约:类型断言守卫已回流 Wavelet(f3d85d5,附回归用例),
本仓库删除 OpenFlare/plugins/server/pkg/cache 整包并改 import 到
Wavelet/pkg/cache/disk,同步后与上游零漂移。

验证:go build 通过;go test ./... exit 0(137 包 ok);256 条路由对拍与
232 条 swagger 操作均零差异;make build-all 四进制;前端零改动。

docs(cordis): 记录 T1 清理结果与五个复用阻塞点

refactor(cordis): server 复用上游 pkg 能力并删除等价本地副本

按上游/下游归属规约清理重复实现,删除 7 个与上游等价的本地包并改 import:
shared/response→pkg/response、pkg/{logger,mail,trace,httppool,cache/ram}→
上游同名包、infra/persistence/batchwriter→pkg/batchwriter。逐项核过差异:
httppool 逐字节相同;logger 的 Config 字段完全一致;response 的 7 个 Abort*
一致;cache/ram 换过去顺带把裸 go 变回带 panic 恢复的 util.Go。

两处非等价差异按语义处理:
- batchwriter.Stats 与 status DTO 原为类型别名,改为消费侧逐字段转换,
  避免 model 反向依赖基础设施类型;
- 上游 pkg/idgen 要求显式 Init(本地副本为懒加载自动初始化),本次保留本地
  副本,待与 infra 初始化一并迁移(已登记在清理计划)。

验证:go build 通过;go test ./... exit 0(138 包 ok);256 条路由对拍零差异;
make swagger 232 条操作零增减,且归一化后与旧文档深度相等——差异仅为
response.Any / logger.LogEntry 两个定义名随包路径改名,接口形状未变。

chore(cordis): 回流内核与 pkg/util 通用能力并清理 vendoring 污染

按新增的上游/下游归属规约:HandleRaw/BasePath 与版本比较、网络、格式化助手
属通用能力,已提交到 Wavelet 分支 feat/cordis-router-raw-routes,本仓库改为
纯同步获取(pkg/util 已零漂移),补丁登记保留至上游合并。

同时修掉我此前 git add -A 造成的污染:首次 vendoring 把上游工作区里被
gitignore 的运行期产物一起提交进来(upload 的 diskcache 缓存块 650 个与
driver_http/dist 前端构建物 380 个,共 12872 行/1030 文件)。sync-upstream.sh
现显式排除 uploads/dist/data/*.db,.gitignore 补上对应兜底规则。

AGENTS.md 增加上游/下游改动归属规约,并把仍指向前 Cordis 布局的硬性约束
(internal/router + Serve、internal/repository/logstore、internal/platform/bootstrap、
internal/cmd)改到当前插件路径。

验证:go build 通过;go test ./... exit 0(144 包 ok);make swagger 232 条
操作与基线逐条一致;make build-all 四进制;gofmt 干净。

feat(cordis): server 插件化并改由内核挂载控制面路由

新增 plugins/server/plugin.go:Apply 以 ctx.Router().Group(app.api_prefix)
声明根级与 /v1 全部路由;33 个注册函数由 *gin.RouterGroup 改为
core.RouterExtension,RegisterCollection 改用内核新增的 HandleRaw 保留
尾部斜杠变体,AdminMiddlewares 返回 []any(Go 不允许把 []T 展开为 ...any)。
删除 router.Serve 与 registerRoutes,装配根改为 core.App +
driver_http.New(WithEngine(router.BuildEngine())),监听、信号与优雅退出归内核;
前端 SPA 的 NoRoute 兜底因内核暂无贡献点而保留在引擎层。

路由保真证据:plugin_parity_test 对拍 baseline/routes-engine.txt 的 256 条
(方法 路径) 零差异;go test ./... exit 0(144 包 ok,含真实 handler 的
openflare/integration 用例走同一条挂载路径);make swagger 232 条操作与基线
逐条一致;golangci-lint 0 issues;make build-all 四进制;embed_frontend
标签编译通过;前端零改动。

已知待补:带 Redis 的实机 HTTP 冒烟(本机 6379 未启动,session store 与
改造前一样在建店阶段即 fatal),以及 bootstrap 的任务/设置/迁移注册迁入 Apply。

feat(core): RouterExtension 增加 HandleRaw 与 BasePath 以保真尾部斜杠路由

server 插件化的前置:Handle 经 cleanPath 会剥掉尾部斜杠,无法表达
/resource 与 /resource/ 两条不同路由,而 OpenFlare 有 20 个历史 list
端点两者都注册且部署关闭了 RedirectTrailingSlash,缺失即 404。新增
HandleRaw 与 BasePath(作用域包装器同样登记反注册),补 extpoints 用例;
并把 router.Serve 拆出 BuildEngine 以便交给 driver_http.WithEngine 复用,
新增路由表导出 harness,固化 256 条 (方法 路径) 基线供插件化对拍。
上游补丁登记于 backend/OpenFlare/upstream-patches.md,同步脚本改为按目录
前缀输出差异并在同步后提醒确认补丁是否仍在。

验证:go build 通过;go test ./... exit 0(143 包 ok);gofmt 干净。

docs(cordis): 记录 server 插件接入内核的可行路径与内核能力缺口

feat(cordis): agent/relay/flared 落地为内核驱动插件

三个边缘守护进程各新增 plugin.go,实现 core.Plugin + core.Driver
(自定义 DriverType 与同名 profile),装配与生命周期从 main 迁入
Apply/Start/Stop:Apply 负责 JSON 配置加载、运行环境与用户确保、
openresty/frps/frpc 管理器与各服务装配;Start 以 util.Go 拉起阻塞式
runner 与 GeoIP 周期更新;Stop 收敛主循环结果并在超时时报错而非静默。

入口改为 core.NewApp(core.WithProfile(...)) + Prepare/Run,保持
-config 旗标、默认路径、退出码与启动/停止日志不变。

验证:go build 通过;go test ./... exit 0(143 包 ok,含 3 个插件身份
与配置失败路径测试);make build-all 四进制产出;三进制实跑缺失配置
均 exit 1 且错误链保留 load {agent,relay,flared} config 原因;gofmt 干净。

refactor(cordis): 按功能职责拆分为 4 个插件与 share 共享层

backend/OpenFlare 不再平铺遗留分层,改为 plugins/{server,agent,relay,flared}
加 share/:控制面业务(openflare/admin/oauth/user/upload/cap/config/health 与
repository/model/infra/router 等支撑层)归 server;三个边缘守护进程各自成插件;
被两个以上插件消费的 protocol/geoip/wsclient/render/pagesarchive/edge 归 share。
同时把 pkg/util 与 buildinfo 合并回上游 pkg(上游已覆盖全部符号,仅 8 个函数与
2 个类型为 OpenFlare 独有,已一并迁入),装配根统一到 backend/cmd(含三个 daemon
入口),Dockerfile 与 release 工作流的构建路径和 -X 注入路径同步更新。

验证:go build 通过;go test ./... exit 0(141 包 ok);make swagger exit 0 且
232 条 API 操作与基线逐条一致;make build-all 产出 4 进制;-X 注入经二进制
strings 实测生效;日志后端直连门禁改写为按 server 插件业务域扫描并在扫描数为 0
时报错(防门禁静默失效);前端零改动。

feat(cordis): 落地 backend/share 共享层与上游同步脚本

跨插件共享资源(控制消息协议、GeoIP+iputil、边缘守护进程日志)从下游包
移入 backend/share,并声明其只能依赖 core/pkg 与标准/第三方库,禁止反向
引用下游业务与具体插件实现;新增 scripts/sync-upstream.sh 只覆盖
backend/{core,pkg,plugins},同步后 --check 报告零差异,证明与上游逐字一致。

go build 通过,go test ./... exit 0(142 包 ok),前端零改动。

refactor(cordis): 采用与 Wavelet 同构的单模块布局并引入上游内核

按上游结构落位:backend/{core,pkg,plugins} 为 Wavelet 上游拷贝,OpenFlare
全部业务收拢到上游 downstream 所对应的位置 backend/OpenFlare/,模块名保持
Wavelet 以保证上游 import 路径逐字一致、同步零改写;三个 daemon 入口移至
backend/OpenFlare/cmd,backend/cmd 与 main.go 作为控制面装配根。

行为不变:go build 通过,142 个测试包全绿(含上游插件测试),232 条 API
操作与改造前逐条一致,四进制产物正常,前端零改动。swagger 暂只扫描下游代码,
待 P4 挂载上游路由后再纳入 plugins/。

style: 修正模块路径改写导致的 import 分组排序漂移

refactor(layout): Go 代码迁入 backend/ 并将模块名简化为 OpenFlare

对齐上游 Wavelet 的仓库布局,为以第二 module 形态 vendoring Cordis 内核与
平台插件做准备:模块路径整体改写为 OpenFlare,Go 目标加 cd backend,
swaggo 产物移至 backend/docs 并把 json/yaml 复制回 docs/ 供站点消费,
Dockerfile 与 release 工作流的构建目录、ldflags 模块路径同步更新。

行为保持不变:232 条路由与改造前逐条一致,95 个测试包全绿,
四进制产物正常,前端零改动。

chore(cordis): 落地改造计划与 schema/路由基线

新增 legacy_dump_test 迁移快照 harness:在临时 sqlite 库上按生产顺序
(goose.UpTo → zone 导入 → goose.Up)跑完 76 个历史迁移并导出 schema 与
版本序列,作为改造前后一致性门禁的唯一事实来源。同时记录 232 条路由清单
与 foundation 实施计划。

docs(cordis): add OpenFlare Cordis 架构改造设计

明确上游以第二 module 形态 vendoring 进 backend/Wavelet、4 个插件
(server/agent/relay/flared) 全部装载内核,并规定保留 76 个历史 goose
迁移 + 一次性版本 stamp 桥接的迁移方案,配套三方 schema 一致性门禁,
确保已部署库不重跑历史、不丢数据。
2026-08-30 10:12:52 +08:00
ryan f3d85d51fb fix(pkg/cache/disk): LRU 节点类型断言失败时降级而非 panic
items 与 evictList 的不变量一旦被破坏,读、写、删除与淘汰路径上的裸类型断言
会直接崩掉进程。改为带 ok 检查:Get 退化为缓存未命中,Set 报告污染条目,
deleteUnlocked 跳过容量回退,evict 移除坏节点后继续。

新增 cache_corruption_test.go 锁住该行为:去掉守卫后用例会以
「interface conversion: interface {} is string, not *disk.cacheItem」失败,
加上守卫后 4 个用例全通过。

验证:go build 通过;go test ./pkg/cache/disk/ 全绿(含原有 5 个用例);
golangci-lint 0 issues;check_cordis_architecture.sh 0 violations。
2026-08-30 01:00:02 +08:00
ryan 8ff017b5e8 feat(pkg/util): 补齐版本比较、网络与格式化通用助手
下游 OpenFlare 的边缘守护进程与发布流程需要这些与业务无关的纯函数,
按上游/下游归属规约回流到平台层,避免下游在上游目录里长期携带本地文件:

- version / version_compare:CompareVersions、ParseVersionInfo(版本区间比较)
- network:GetIP、IsPrivateIPv4
- format / value / string / slice:Bytes2Size、Seconds2Time、Interface2String、
  TrimStringFields、UniqueAndCleanStringSlice 与 IdentifiableTimeRecord

验证:go build 通过;go test ./... exit 0(48 包 ok);
check_cordis_architecture.sh 0 violations;golangci-lint 0 issues;gofmt 干净。
2026-08-30 00:36:13 +08:00
ryan bad6fa785d refactor(core): Handle 与 HandleRaw 共用 addRoute
消除注册逻辑重复,并修正 HandleRaw 里 append(g.registry.middlewares, ...)
复用底层数组的隐患:中间件快照统一在 addRoute 内构造为新切片。

验证:go build 通过;go test ./core/... 全绿;golangci-lint ./core/... 0 issues。
2026-08-30 00:26:01 +08:00
ryan cb339ab0dc feat(core): RouterExtension 增加 HandleRaw 与 BasePath
Handle 经 cleanPath 归一化会剥掉尾部斜杠,插件无法同时声明 /resource 与
/resource/ 两条路由;部署关闭 gin 的 RedirectTrailingSlash 时,缺失的那条
直接 404。下游 OpenFlare 有 20 个历史列表接口依赖该行为。

- HandleRaw:与组前缀拼接但保留尾部斜杠,分配独立路由 ID;
- BasePath:返回组的绝对前缀(根注册表为空串);
- 作用域包装器为 HandleRaw 同样登记 OnDispose 反注册。

验证:go build 通过;go test ./... exit 0(48 包 ok);
check_cordis_architecture.sh 0 violations;gofmt 干净。
2026-08-30 00:22:56 +08:00
ryan 3b24d248a7 docs(autoresearch): proposals for the five deferred architectural items 2026-08-29 19:32:35 +08:00
ryan 350bd422f5 chore(autoresearch): log iter 35 2026-08-29 19:31:41 +08:00
ryan d7c851bc47 autoresearch iter 35: BUGFIX a failed whitelist read is no longer cached as an admin decision 2026-08-29 19:29:25 +08:00
ryan db9d12f8c9 chore(autoresearch): log iter 34 2026-08-29 19:20:50 +08:00
ryan b22f8633ba autoresearch iter 34: BUGFIX an unreadable SMTP config no longer looks like an unconfigured mailer 2026-08-29 19:19:17 +08:00
ryan 578b4618ce chore(autoresearch): log iter 33 2026-08-29 19:15:27 +08:00
ryan 99fca9ee09 autoresearch iter 33: BUGFIX storage migration no longer migrates from a config it could not read 2026-08-29 19:12:58 +08:00
ryan 608cce19c9 docs(autoresearch): lesson 12 and harness standing notes 2026-08-29 19:06:00 +08:00
ryan 6e5ed979e4 chore(autoresearch): log iter 32 2026-08-29 19:05:17 +08:00
ryan f7a86d3608 autoresearch iter 32: PERF whitelist parses patterns once, 14 allocs/op to 1 2026-08-29 19:03:27 +08:00
ryan 22a491ff37 chore(autoresearch): log iter 31 2026-08-29 18:57:29 +08:00
ryan 5193bd0451 autoresearch iter 31: isolate lint result cache per checkout in harness 2026-08-29 18:55:36 +08:00
ryan 53fc3a81dc chore(autoresearch): log iter 30 2026-08-29 18:51:56 +08:00
ryan 9ea0e2bdff autoresearch iter 30: enforce user lookup column allow-list instead of trusting a comment 2026-08-29 18:49:37 +08:00
ryan f29ac19673 chore(autoresearch): log iter 29 2026-08-29 18:46:21 +08:00
ryan 2ff0cb87c9 autoresearch iter 29: CORDIS contracts DTO must not carry a table name 2026-08-29 18:45:17 +08:00
ryan 4412093d05 chore(autoresearch): log iter 28 discard 2026-08-29 18:42:44 +08:00
ryan 50370971ec Revert "autoresearch iter 28: CORDIS gate contracts must not carry table names"
This reverts commit 2cb8d9a892.
2026-08-29 18:42:31 +08:00
ryan 2cb8d9a892 autoresearch iter 28: CORDIS gate contracts must not carry table names 2026-08-29 18:40:04 +08:00
ryan 9f79fb9969 chore(release): v3.5.4
### ✨ 新功能
- 控制台接入中英双语(next-intl,无 URL 语言前缀):默认中文,可在顶栏或「外观设置」切换,选择写入 cookie 后刷新生效。

### 🛠 修复
- 修复在网站列表中删除已加入 Cloudflare 指向分组的域名后,访问 Cloudflare 指向分组详情报错「Cloudflare 资源不存在」的问题。
- 修复自定义 Webhook 推送在企业微信/钉钉返回 HTTP 200 但 `errcode` 非零时仍记为成功的问题;任务日志会记录上游响应体。
- 修复 OpenTelemetry Resource 绑定 semconv schema 版本导致 SDK 升级后可能无法启动的问题。
- 修复静态导出(build:embed)部署下切换语言无效的问题:此前页面在构建时固定为默认中文,运行时不再读取 `NEXT_LOCALE`;现在客户端会按 cookie/浏览器语言重新解析并切换界面语言与 `html lang`。
- 修复 frpc 子进程在被杀后孤儿进程继续持有管道导致退出阻塞的问题。

### 💄 其他/体验
- 前端使用 `next/font` 自托管 Inter 字体,并忽略浏览器扩展改写 `body` 属性引起的 hydration 警告。
2026-08-29 18:34:07 +08:00
ryan 8b746e1d0e chore(autoresearch): log iter 27 2026-08-29 18:34:01 +08:00
ryan 31f3af61c5 autoresearch iter 27: drop one dead contextcheck suppression, document the other 2026-08-29 18:32:54 +08:00
ryan bbd7f72c2d fix(cloudflare): clean up pointing member when zone domain is deleted 2026-08-29 18:32:02 +08:00
ryan ea97b64407 fix(task): restore task metadata contract and type fields in task types api 2026-08-29 12:22:51 +08:00
ryan 49f9d1076f fix(admin): move w_task_executions and w_schedules migrations to admin plugin
- Include w_schedules and w_task_executions DDL in admin initial migrations for both SQLite and PostgreSQL dialects
- Remove driver-specific migration registrations from driver_asynq_worker and driver_asynq_cron
- Fix missing table error when running in Zero-Redis standalone mode without Asynq
- Update white paper table ownership mapping and ensure test cleanup
2026-08-29 11:58:56 +08:00
ryan e568b96388 fix(auth): add missing masked_token column to w_access_tokens table in migrations 2026-08-29 11:57:04 +08:00
ryan 7786f416f8 perf(auth): eliminate redundant password queries during user info retrieval 2026-08-29 11:54:46 +08:00
ryan 64fe1658d4 fix(user): clear need_change_password and invalidate cache on password change 2026-08-29 11:52:41 +08:00
ryan d3d7c783a9 fix(auth): synchronize need_change_password across login, user-info and repositories 2026-08-29 11:49:14 +08:00
ryan 107251891f fix(user): restore plaintext default password checking and warning mechanism 2026-08-29 11:46:39 +08:00
ryan 86c750077f fix(user): seed default administrator account in initial migration 2026-08-29 11:42:43 +08:00
ryan d043e7366f docs: update developer guide and white paper with router whitelist and session fallback 2026-08-29 11:40:50 +08:00
ryan e0f2309520 feat(router): add whitelist mechanism for http driver and auth plugin
- implement route whitelist registration and wildcard matching in RouterExtension
- add cookie store session fallback when Redis is disabled in driver_http
- actively register public auth endpoints to whitelist in auth plugin
- update user handlers to persist session and clear cookie on logout
- document router whitelist mechanism in AGENTS.md and new-api skill
2026-08-29 11:39:13 +08:00
Ryan 53ae3007d0 fix(cordis): fail-closed auth guards for user/message_gateway/admin (#1)
* autoresearch iter 23: fail-closed auth guarding for user/message_gateway

Both plugins resolve contracts.AuthService in Apply to build their route
middleware, but declared only DBService in Inject(). The kernel gates a
plugin's Apply solely on declared deps, and cmd/app.go registers user
before auth, so user mounted first, core.Inject failed, and loginMW
silently degraded to a pass-through closure — leaving /api/v1/user
change-password, profile and access-tokens unguarded. message_gateway
was saved only by its later list position.

Declare AuthService in Inject() for both, and pin the property with a
reconcile-level test that mirrors production registration order and
asserts the real auth middleware reaches the route table.

* autoresearch iter 24: make auth middleware fallbacks fail closed

user, message_gateway and admin each fell back to a c.Next() closure when
contracts.AuthService could not be resolved, so a route would be served as
if authenticated. For admin this is reachable at runtime: OnDispose calls
service.ResetServices(), which nils the global the per-request guard reads,
so requests still in flight during dispose bypass authorization entirely.

Add ginutil.AuthUnavailable() and bind every fallback to it, with a test
that drives each plugin's registered guard without an auth service present
and asserts the request is aborted rather than passed through.

* chore(autoresearch): log iter 23 (fail-open auth ordering, proven)

* autoresearch iter 24 follow-up: let staticcheck infer the auth guard type

* docs(autoresearch): log iters 24-25 and lessons 9-11 (declared-dep bug class, gate discipline)
2026-08-29 11:21:04 +08:00
ryan b624de9620 feat(cmd): log actual plugin migration version instead of up-to-date 2026-08-29 11:11:51 +08:00
ryan 4d65e57f9a merge: feat(core): implement cordis configuration extension and migrate all plugins 2026-08-29 10:54:28 +08:00
ryan ed8491addf feat(core): implement cordis configuration extension and migrate all plugins 2026-08-29 10:53:53 +08:00
ryan b43c429544 chore(arch): forbid viper and mapstructure inside the micro-kernel
配置装载实现必须留在 plugins/infra/config 适配器里,内核只依赖
ConfigSource 抽象;把 viper 与 mapstructure 加入 1.1 禁止清单,防止配置
装载依赖重新渗回 core(已用临时探针文件反向验证检查生效)。
2026-08-29 10:17:57 +08:00
ryan 8bd59511a8 refactor(config): make legacy loader reentrant and add engine parity test
load(configPath, testMode) 用私有 viper 实例替代包级全局,使同一输入可反复
求值;对拍测试以入库的 config.example.yaml 为必备基准(本地 config.yaml
存在时加测),在四类 env 场景下逐 key 比对新引擎与旧装载器,并以变异检验
确认其能发现漂移。
2026-08-29 10:15:32 +08:00
ryan 696809899e feat(infra): add viper backed configuration source adapter
实现 core.ConfigSource:按 CONFIG_PATH 或向上查找定位 config.yaml,缺文件
降级为纯环境变量来源,坏文件返回错误而非 log.Fatalf,并把 key 命中与"设为
零值"区分开来。viper 依赖被隔离在此包,内核保持零具体运行时依赖。
2026-08-29 10:06:46 +08:00
ryan 4acb529b85 feat(core): add config resolution barrier and plugin gating to App
App 新增 WithConfigSource / WithConfigDecl / Prepare / ShutdownTimeout /
SetShutdownTimeout;Use 收集门禁插件的提前声明,调和循环内求值门禁并跳过
被关闭的插件,使组合根无需再跨插件读配置选实现。未注入配置源的 App 保持
原行为,被门禁但无配置源则 fail fast 点名原因。
2026-08-29 10:03:56 +08:00
ryan b3c4d6cb99 docs(autoresearch): lessons 6-8 (audit verification, counting doubles, gate+veto discipline) 2026-08-29 09:56:24 +08:00
ryan 6011effade chore(autoresearch): log iter 22 (debt 79 -> 54) 2026-08-29 09:55:24 +08:00
ryan ad8384182c autoresearch iter 22: delete lint suppressions that suppress nothing
24 of the 96 nolint directives were dead: they covered findings that no
longer exist. A stale suppression is not inert — it silently claims any
future finding for that linter in that scope, so a real problem raised
there would vanish without anyone noticing. Explanatory prose was kept as
ordinary comments.

Two directives proved load-bearing under the project gate even though
nolintlint reported them unused, and removing them exposed verified
contextcheck false positives: App.Run does forward a sigCtx derived from
the caller's context to Start, and the migration lock renewal must keep
its own deadline because the task context may already be canceled. Both
were restored, narrowed to the live linter, and given the reason the
originals lacked.
2026-08-29 09:54:33 +08:00
ryan afaa8f79eb feat(core): add skipped fiber state for configuration gates
Fiber 新增 SKIPPED 态与 Skip/Skipped 方法:门禁为假的插件在 Apply 之前
即被排除并释放其作用域 Context,为互斥实现(cache 与 cache_memory 等)
同时挂载由内核择一激活铺路。
2026-08-29 09:53:10 +08:00
ryan 39a81f7723 feat(core): mount the configuration extension point on the kernel Context
Context 新增 Config() 访问器,注册表随 Fork 共享(配置声明是进程级事实),
并在 types.go 导出配置别名与 ConfigGatedPlugin 可选接口,为插件门禁做准备。
2026-08-29 09:47:59 +08:00
ryan c0869cb878 feat(core): expose read-only config view, generic getter and redacted dump
补齐 Value/String/Bool/Int/Duration/Strings/WasSet/Origin 只读访问器与
按 secret 脱敏的 Entries 导出,新增 core.ConfigGet[T] 泛型读取入口,并用
编译期断言钉住 ConfigRegistry 对 ConfigExtension 的完整实现。
2026-08-29 09:43:33 +08:00
ryan b6f2221280 chore(autoresearch): log iter 21 2026-08-29 09:41:30 +08:00
ryan 1023fa3adb autoresearch iter 21: remove the telegram inbound media scratch dir after handling
downloadMedia created a fresh os.MkdirTemp for every private message carrying
a photo or document, and no code path anywhere reads Attachment.Path, so each
message permanently grew the disk while burning a Bot API download. The
handler now removes the directory once onInbound returns.

No mechanical proof is possible here: exercising downloadMedia needs a live
telebot download. Verified by reading every consumer of InboundMessage
.Attachments instead.
2026-08-29 09:40:49 +08:00
ryan 4653afc554 feat(core): resolve declared configuration with env and file precedence
按 显式 env > autoEnable > 配置文件 > default 的优先级链解析每个已声明
key,支持标量 env 填充切片、duration 与结构体切片解码,非法 env 值不再
静默回退而是报 ErrConfigType。
2026-08-29 09:38:39 +08:00
ryan 9c0f31fad0 chore(autoresearch): log iter 20
Note: iter 20's commit also captured an in-flight edit to
docs/superpowers/plans/2026-08-29-cordis-config-extension.md belonging to a
concurrent session, because it used 'git add -A'. Content is intact; later
iterations stage explicit paths only.
2026-08-29 09:35:50 +08:00
ryan c77b5358e2 feat(core): add configuration declaration registry
配置读取框架的内核侧抽象:插件用带 config/env/default/autoEnable/secret
tag 的结构体声明自己读哪些字段,注册表按 key 归集并对重复声明做一致性
校验,为后续按声明解析与门禁求值提供基础。
2026-08-29 09:32:53 +08:00
ryan efa75558af autoresearch iter 20: give the telegram poller a real long-poll window
telebot types LongPoller.Timeout as time.Duration and sends
int(timeout / time.Second) to getUpdates, so the literal 10 meant ten
nanoseconds: Telegram received timeout=0, long polling never held the
connection, and the adapter polled the Bot API in a tight loop instead.
Use 10 seconds and extract the settings so the conversion is asserted.

The adapter also has no media temp-dir cleanup (downloadMedia creates an
MkdirTemp per attachment and nothing removes it); that is left as a separate
change rather than bundled here.
2026-08-29 09:32:27 +08:00
ryan ae8bbd98f8 chore(autoresearch): log iter 19 2026-08-29 09:24:33 +08:00
ryan 84eaf3f555 autoresearch iter 19: make task handlers driver-agnostic so they run under both workers
upload's four real background tasks (system cleanup, stats rebuild, storage
migration, image warmup) plus the admin and user stubs registered handlers
typed as func(ctx, *asynq.Task) error. Only the asynq worker accepts that
shape; the Redis-free in-process worker's invokeHandler rejects it with
'unsupported handler type', so none of those tasks could ever run in that
deployment mode. Take payload bytes instead, which both drivers support.

Adds architecture gate check 7 forbidding asynq imports from business and
infrastructure plugins. It deliberately does not cover robfig/cron: the admin
plugin uses cron.ParseStandard only to validate a user-entered spec, which is
a library call rather than a driver binding, and the in-process scheduler
already normalizes 5-field specs.
2026-08-29 09:23:01 +08:00
ryan fd83496a05 docs(config): add implementation plan for the Cordis config extension point
覆盖 P1+P2:core 配置引擎、viper 适配器隔离、门禁与 FiberSkipped、
新旧解析对拍。迁移 27 个消费文件与旧单例退场由后续计划承接。
2026-08-29 09:20:22 +08:00
ryan 020ebebfaa chore(autoresearch): log iter 18 2026-08-29 09:12:03 +08:00
ryan 8c4955c835 autoresearch iter 18: remove the phantom user:daily_audit schedule
The user plugin registered a cron dispatching to user:daily_audit, a task
pattern it never registers, and no audit logic exists anywhere in the plugin.
The daily run therefore went nowhere while a test asserted the schedule was
registered — proving the wiring existed, not that it worked. Implementing a
real daily audit is unstarted functionality, so the schedule is removed rather
than stubbed.

The combined domain test now asserts the real invariant across all applied
plugins: every schedule's task type must have a registered handler.
2026-08-29 09:11:16 +08:00
ryan d80f9d209b chore(autoresearch): log iter 17 2026-08-29 09:03:54 +08:00
ryan 1b1c45206c autoresearch iter 17: wire the pairing-code cleanup cron to a real handler
message_gateway scheduled message_gateway:cleanup_pairing_codes every 10
minutes but never registered a task under that pattern, so every dispatch
went to a task type with no handler and expired pairing rows accumulated
forever, even though repository.DeleteExpiredPairingCodes already existed.
Add a test that fails for any schedule whose task pattern is unregistered:
it reports the exact orphan rather than relying on a schedule-exists assert.
2026-08-29 09:03:33 +08:00
ryan 84946977bf chore(autoresearch): log iter 16 (perf, contract batch) 2026-08-29 08:52:18 +08:00
ryan 976f9b15ae autoresearch iter 16: add batch user lookup and use it for log enrichment
enrichAccessLogsWithUsers preferred the UserService contract over the local
repository — correct layering, but it looped GetUserByID and issued up to a
page-size worth of separate SELECTs against w_users, while the single-query
WHERE id IN variant was only reached in the no-contract fallback branch.
Give the contract a GetUsersByIDs so callers can keep the layering and drop
the N+1. The test asserts 1 query batched against 3 per-id, so the counting
itself is checked.
2026-08-29 08:51:30 +08:00
ryan 5df282f296 chore(autoresearch): log iter 15 (perf, proven) 2026-08-29 08:45:21 +08:00
ryan 2c415638fd autoresearch iter 15: stop CORS from querying the database on every request
isOriginAllowed read server_address from w_system_configs for every request
carrying an Origin header — one uncached primary-DB round-trip plus a split
and trim loop per browser request, while sibling config reads in the storage
driver are already TTL cached. Read it through the shared CacheService with
the same 5s window, falling back to the database when no cache is bound.
driver_http now binds CacheService in Apply the way it already binds DBService.
2026-08-29 08:44:37 +08:00
ryan 2654eb6e2c chore(autoresearch): correct iter 14 log (debt held at 79, kept via proven-fix gate) 2026-08-29 08:39:53 +08:00
ryan 45bf1d8933 chore(autoresearch): log iter 14 2026-08-29 08:39:38 +08:00
ryan 6932b54a30 autoresearch iter 14: stop a cache read error from clobbering the buffered task log
AppendTaskExecutionLog discarded the error from its read of the buffer, so a
transient cache failure looked like an empty buffer and the very next write
replaced the whole accumulated log with just the newest line. Flush already
distinguished miss from failure; append now does the same.
2026-08-29 08:38:55 +08:00
ryan 00ab727791 chore(autoresearch): log iter 12 (debt 80 -> 79) 2026-08-29 08:35:26 +08:00
ryan 101cb2ff7a autoresearch iter 12: inproc driver reports untracked executions as an error
GetExecution returned (nil, nil) under the in-process driver where the asynq
driver returns an error, so the same contract call meant 'empty' in one
deployment mode and 'failed' in the other.
2026-08-29 08:35:00 +08:00
ryan 2c8020188d chore(autoresearch): log iter 11 (debt 84 -> 80) 2026-08-29 08:33:56 +08:00
ryan 3d2038a251 autoresearch iter 11: unimplemented auth mocks fail loudly instead of returning (nil, nil)
Authenticate, CreateAuthSource, UpdateAuthSource and ToggleAuthSource claimed
success with a nil record, so any test that reached them surfaced a nil
pointer dereference instead of the actual cause. Full suite confirms no test
relied on the silent behaviour.
2026-08-29 08:33:16 +08:00
ryan 643bfca996 chore(autoresearch): log iter 10 (debt 87 -> 84, errorlint 12 -> 0) 2026-08-29 08:30:33 +08:00
ryan c4068efd54 autoresearch iter 10: keep error identity at the last errorlint sites
RunPushTest flattened channel validation failures with %v, telegram's
fallback path discarded the original send error, and the config loader's
type assertion on viper.ConfigFileNotFoundError would miss a wrapped form
and fatally abort over a merely missing file. errorlint now reports zero.
2026-08-29 08:29:40 +08:00
ryan f7fd980429 chore(autoresearch): log iter 9 (debt 89 -> 87) 2026-08-29 08:27:21 +08:00
ryan 22ecafdbc2 autoresearch iter 9: drop always-nil error results from task log loaders
loadTaskExecutionLog and loadTaskExecutionLogs could never fail, yet four
call sites branched on their error as if they could, presenting unreachable
code as error handling.
2026-08-29 08:26:39 +08:00
ryan a529700ed3 chore(autoresearch): log iter 8 (proven bug fix, debt held at 89) 2026-08-29 08:24:09 +08:00
ryan 18820b17f6 autoresearch iter 8: render synthesized notification content in stable order
bodyContent's fallback ranged over the body map, and Go randomizes map
iteration, so the same notification rendered its fields in a different order
on every send. Observed failing before the fix: the second call already
reordered the output. Iterate sorted keys instead.
2026-08-29 08:23:23 +08:00
ryan 03f48a9a80 chore(autoresearch): log iter 7 (debt 92 -> 89) 2026-08-29 08:21:24 +08:00
ryan c66399eedc autoresearch iter 7: share body field extraction across push channels
email, telegram and lark each re-implemented the title/content/level lookup
with only their markup differing, and each carried a dead content := ""
initialization that every branch overwrote. Three small helpers in template.go
now own that logic.
2026-08-29 08:20:43 +08:00
ryan bf364f4036 chore(autoresearch): log iter 6, distinguish compile-level from assertion-level proof 2026-08-29 08:17:59 +08:00
ryan ce33997c23 autoresearch iter 6: reject negative cursor instead of silently using 0
parsePositiveInt reported invalidity through a bool that both call sites
discarded, and returned (false, nil) whenever Atoi succeeded on a negative
number. GetLogs therefore accepted ?cursor=-5 and served it as cursor 0
('latest') instead of the documented 400. Validity now travels through the
error result, which no caller can ignore.
2026-08-29 08:16:50 +08:00
ryan 850f99a2c8 docs(config): add Cordis config extension point design
pkg/config 以全局单例暴露全量配置,使组合根可跨插件判断 redis.enabled、
配置读者与所有者无约束。本设计将配置读取框架下沉为内核扩展点,由 infra
适配器隔离 viper,各插件声明自读字段并以门禁谓词取代组合根选型判断,
一次性迁移全部 27 个消费文件。
2026-08-29 08:16:24 +08:00
ryan 58c34ad5c1 chore(autoresearch): log iter 5 (4 bare goroutines hardened, gate widened) 2026-08-29 08:13:17 +08:00
ryan 381c79417e autoresearch iter 5: recover panics in background cleanup loops
Four long-running goroutines were launched with a bare go statement, so a
panic in any of them took down the whole process: the RAM cache's expired-key
eviction, the batch writer's flush worker, the disk cache cleanup worker and
the PoW memory store sweeper. Route them through util.Go.

The architecture gate only grepped for 'go func(', which is why the named-call
form went unnoticed; widen it to cover both launch styles.
2026-08-29 08:12:27 +08:00
ryan 57b39f7fcf chore(autoresearch): log iter 4 (proven bug fix, debt held at 93) 2026-08-29 08:08:16 +08:00
ryan 7e6b9e7c2f autoresearch iter 4: stop one disconnected client from failing a shared image flight
EnsureCompressedImageCache passed the arriving caller's request context into
the singleflight body, which runs once for every concurrent requester of that
cache key. If the first client disconnected, gin canceled the context, the
shared generation aborted, and every follower received that failure and fell
back to the uncompressed original. Detach cancellation with
context.WithoutCancel so trace values still propagate but the shared work
outlives any single requester.
2026-08-29 08:07:19 +08:00
ryan 5b84fd906d chore(autoresearch): log iter 3 (debt 95 -> 93) 2026-08-29 08:03:26 +08:00
ryan 686e3ef5a6 autoresearch iter 3: share upload-record error mapping via filesrv helper
ServeFileByID and DownloadFile duplicated the lookup failure mapping and
each used an unchecked *strconv.NumError assertion that cannot match a
wrapped error. One helper now classifies 404 vs 400 via errors.As; each
endpoint keeps its own fallback for unclassified failures. ErrInvalidUploadID
became unused once both sites report ErrInvalidFileID for a malformed ID.
2026-08-29 08:02:42 +08:00
ryan 4e6209bf61 chore(autoresearch): log iter 2 (debt 100 -> 95) 2026-08-29 07:59:39 +08:00
ryan 37ad58699d autoresearch iter 2: compare error sentinels with errors.Is
Five sites used == against sentinels (redis.Nil, ingest.ErrForbidden,
errs.ErrDatabaseUninitialized). The neighbouring not-found checks already
went through errors.Is helpers, so a wrapped error would silently downgrade
a 403 to a 400 and a 500 to a 400.
2026-08-29 07:58:59 +08:00
ryan edf0c0e934 chore(autoresearch): log iter 1 (debt 102 -> 100, proven fix) 2026-08-29 07:57:10 +08:00
ryan 1c5731bf75 autoresearch iter 1: keep Using2/Using3 dependency causes reachable
Using2/Using3 flattened per-dependency injection failures with %v while
Using1 wrapped with %w, so errors.Is could not see ErrServiceNotFound
through a multi-dependency resolution failure.
2026-08-29 07:56:02 +08:00
ryan 5971e2a9ed chore(autoresearch): re-baseline harness on pinned real-risk yardstick
The committed golangci gate now reports 0 issues, so the previous
lint_issues metric was saturated and could no longer measure progress.
Measure debt against an immutable .auto/lint.ref.yaml snapshot that adds
analyzers for genuine defects (panics, error unwrapping, dead stores,
missing enum cases, method ordering, suppression hygiene) while excluding
cosmetic churn (tagliatelle, wrapcheck). Guard enforces build, vet, tests,
the Cordis architecture gate, and anti-cheat floors: the yardstick cannot
be edited, the project gate may only be strengthened, nolint directives may
only shrink, and no test may disappear.
2026-08-29 07:52:03 +08:00
ryan 4f30e2d57b fix(docker): repair embed packaging for backend/ module layout
The Dockerfiles and the release workflow still assumed the pre-refactor tree:
go.mod, go.sum and main.go now live under backend/, so `COPY go.mod go.sum`
failed outright, and the Go module is the bare `Wavelet`, so the buildinfo
ldflags pointed at github.com/Rain-kl/Wavelet and stamped nothing. The
frontend export was also overlaid onto ./plugins/... rather than the
driver_http path that `//go:embed all:dist` actually reads.

Repoint every packaging path at backend/, correct the ldflags module prefix,
and assert dist/index.html in both docker stages, in build-embedded and in the
release workflow, so a silently empty static export can no longer ship an
API-only binary. Also carry pnpm-workspace.yaml into the cached install layer.
2026-08-29 07:40:57 +08:00
ryan f4975d6732 refactor(plugins): restructure admin and message_gateway into standard layered sub-packages 2026-08-28 22:33:26 +08:00
ryan 85b383a4e0 skills: autoresearch 2026-08-28 20:36:15 +08:00
ryan b68060255f docs(plugins): add plugin layered architecture spec and templates 2026-08-28 20:35:11 +08:00
ryan 0035e548a5 fix(risk_control,message_gateway): fix SQL LIKE escape syntax and use UserService contract 2026-08-28 20:19:24 +08:00
ryan df351cbd33 refactor(core): decouple gin from pkg/util and reduce code duplication 2026-08-28 20:15:47 +08:00
ryan c52b7c4abf test(upload): move storage task fixtures to in-memory mock 2026-08-28 18:54:23 +08:00
ryan 4d6be2fa77 fix(drivers): propagate app-lifetime context through inproc cron/worker drivers
cron 触发与 worker 执行的任务现在继承应用生命周期 context(关闭时级联取消,带超时子上下文),
替代裸 context.Background()。contextcheck 清零。
lint_issues 26→24
2026-08-28 17:04:44 +08:00
ryan 02b93a3b20 chore(autoresearch): log iter 2-3 2026-08-28 16:53:51 +08:00
ryan 867fcb2288 refactor: revive cleanup — unused params to _, add missing doc comments
- admin/db_helper GetCache/GetUserService/GetAuthService: ctx -> _ (签名对称保留)
- validateMergedStorageConfig / ParseMigrationTargetConfig / MockStorageService.Put 未用参数 -> _
- SetDBServiceForTest、StorageDriver 常量组补充文档注释
lint_issues 33→26
2026-08-28 16:53:32 +08:00
ryan 6e12a7fd5d fix(admin): propagate cache errors in FlushTaskExecutionLog
缓存故障(非 ErrCacheMiss)不再被静默吞掉:上抛包装错误,避免缓冲任务日志丢失并误报持久化成功;
ErrCacheMiss 仍视为无日志的正常路径。附 3 个回归测试(故障/未命中/持久化+清理)。
lint_issues 34→33 (nilerr 清零)
2026-08-28 16:50:03 +08:00
ryan cf85a56aa7 refactor: eliminate string/magic-number literals (goconst, mnd) and fix const-type grouping (SA9004)
- upload/task: taskCategoryUpload/taskQueueDefault 常量替代 8 处字面量
- admin: 复用既有 logDBNameSQLite 常量替代 3 处 "sqlite" 字面量
- pkg/cache/disk: defaultCleanupInterval 命名常量
- driver_asynq_worker/executor: 分离 contextKey 类型常量组
lint_issues 45→34, tests 44/44
2026-08-28 16:38:18 +08:00
ryan 528240026d chore(lint): unify formatting on golangci-lint fmt (gofumpt), uncap issue reporting, fix gofumpt drift
- make format 现在与 code-check 使用同一格式化器(golangci-lint fmt),消除 goimports -local 与 gofumpt 的格式拉锯
- .golangci.yml 关闭默认 50/3 截断,完整上报所有问题(只增强不弱化)
- 全库 gofumpt 规范化(203 files, 纯格式无行为变更)
2026-08-28 16:31:53 +08:00
ryan 078ad9b2f3 chore(autoresearch): init cordis-quality session files 2026-08-28 16:21:34 +08:00
ryan 3a61c38dc5 fix(core): delegate driver and task extension lookups to root context and improve test reliability 2026-08-28 16:09:18 +08:00
ryan 7fca53823a feat(cmd): restore startup banner display in CLI entrypoints 2026-08-28 15:46:58 +08:00
ryan 3df621637f fix(db): ensure sqlite data directory exists before opening database 2026-08-28 15:42:24 +08:00
ryan 692b4b4851 feat(db): split migrations into dialect-specific sqlite and postgres packages 2026-08-28 15:36:03 +08:00
ryan 06508b6f13 refactor(drivers): decouple inproc cron from inproc worker and satisfy linter 2026-08-28 15:17:11 +08:00
ryan 0ff117da47 feat(cmd): support dynamic zero-redis pluggable assembly in app bootstrap 2026-08-28 15:15:26 +08:00
ryan e434e6dc0a feat(drivers): provide TaskService implementation in inproc worker 2026-08-28 15:14:27 +08:00
ryan f048dc528e fix(docs): move swagger generated docs.go into backend/docs module directory 2026-08-28 15:13:34 +08:00
ryan 25647df3cf feat(drivers): implement in-process cron scheduler driver plugin 2026-08-28 15:13:24 +08:00
ryan d8e69bb31b feat(drivers): implement in-process async worker driver plugin 2026-08-28 15:12:42 +08:00
ryan 98d2c1cbd0 feat(infra): implement zero-dependency pure in-memory cache plugin 2026-08-28 15:11:46 +08:00
ryan acad091127 docs: add implementation plan for zero-redis pluggable architecture 2026-08-28 15:11:03 +08:00
ryan 299ac30ee4 refactor(core): align with cordis spatiotemporal composability architecture
- Purify core micro-kernel by removing context hardcoded helpers and reverse dependencies
- Eliminate init() side effects in infra plugins with reversible lifecycle disposal
- Completely isolate plugins by removing cross-plugin imports and using core/contracts
- Introduce TaskService and RiskControlService contracts for unified cross-plugin APIs
- Regenerate Swagger documentation and update developer guide matrix
- Achieve 0 violations in check_cordis_architecture.sh and 100% test pass
2026-08-28 15:05:31 +08:00
ryan fc7fae7b0e docs: add design spec for zero-redis pluggable architecture 2026-08-28 14:02:28 +08:00
ryan 48211fa587 chore: code-check 2026-08-28 13:42:49 +08:00
ryan 9213ee79b3 refactor(core): finalize context test and standalone repository fallback 2026-08-28 13:38:22 +08:00
ryan a296818922 refactor(user): decouple repository from direct database infra import 2026-08-28 13:37:13 +08:00
ryan 3570ccd5c3 feat(core): implement plugin fiber state machine and reactive dependency reconciler 2026-08-28 13:35:43 +08:00
ryan 33294b3fae feat(core): implement scoped revertible effects for context extpoints 2026-08-28 13:34:07 +08:00
ryan 0b4e928d1a docs(core): add cordis architecture alignment implementation plan 2026-08-28 13:32:55 +08:00
ryan 0bc19e34cb docs(core): add cordis architecture alignment design spec 2026-08-28 13:32:05 +08:00
ryan b92ba54707 docs(core): update developer guide and white paper with cordis composability standards 2026-08-28 13:30:47 +08:00
ryan e19bf36580 refactor(core): align architecture with cordis spatiotemporal composability 2026-08-28 13:28:58 +08:00
ryan 9f8890d159 refactor(module): simplify module name to Wavelet and standardize import paths
- Declared module Wavelet in backend/go.mod
- Replaced github.com/Rain-kl/Wavelet/ with clean Wavelet/ import paths across backend codebase
- Updated architecture guards, Makefile, swagger, and build tests
- 100% passed all tests, lint checks, and binary compilation
2026-08-28 13:10:30 +08:00
ryan f2ab94501c refactor(layout): relocate go.mod to backend/ and clean import paths to module root
- Relocated go.mod and go.sum into backend/ root directory
- Stripped redundant backend/ segments from all Go imports (github.com/Rain-kl/Wavelet/...)
- Unified Makefile, swagger, and build-test to execute in backend/ module context
- Ensured 100% build-test, code-check, format, and swagger pass
2026-08-28 13:01:51 +08:00
ryan 43dc97e48c refactor(layout): consolidate backend codebase into backend/ package and clean root directory
- Moved cmd/, core/, plugins/, pkg/, downstream/, and main.go into backend/ directory
- Batch updated all Go source files to import github.com/Rain-kl/Wavelet/backend/...
- Updated Makefile, scripts/swagger.sh, architecture guards, and platform skills
- Passed all quality gates (100% tests, 0 lint issues, clean build)
2026-08-28 12:56:02 +08:00
ryan 33b38f8687 docs(migration): update docs and skills for new migration architecture
Update all relevant documentation and skills to reflect:
- w_schema_versions shared version table with plugin_id discriminator
- Single 00001_initial.sql per plugin (merged from multi-file approach)
- gooseEngine uses goose.NewProvider with goose.WithStore(sharedStore)
- pkg/migrator deleted, all 26 global SQL files moved to per-plugin
- DDL/DML single-file approach (merged seed + schema)

Files updated:
- .agents/skills/database-migration/SKILL.md (full rewrite)
- docs/WAVELET_WHITE_PAPER.md (table matrix + migration check)
- docs/WAVELET_DEVELOPER_GUIDE.md (scenario 9)
- docs/superpowers/specs/2026-08-27-cordis-plugin-architecture-design.md
- docs/superpowers/specs/2026-08-27-cordis-downstream-developer-guide.md
2026-08-28 12:45:58 +08:00
ryan 9bd012a271 fix(migration): use single w_schema_versions table with plugin_id discriminator
Replace per-plugin goose version tables with a single shared table
shared across all plugins, discriminated by plugin_id.

Implementation:
- Implement custom goosedb.Store (sharedStore) that uses a single
  w_schema_versions(plugin_id, version_id, applied_at) table
- Each store instance binds to a specific pluginID, filtering all
  CRUD operations by that plugin_id
- Goose provider created via goose.WithStore(store) instead of
  WithTableName, eliminating per-plugin goose_version_* tables
- Placeholder formatting (PostgreSQL  vs SQLite ?) handled by
  sharedStore.placeholder() based on dialect

This means:
  SELECT * FROM w_schema_versions ORDER BY plugin_id, version_id;
shows the complete migration state of every plugin at a glance.
2026-08-28 12:39:28 +08:00
ryan 65c7c282f5 refactor(migration): merge per-plugin SQL into single initial migration
Each plugin now has exactly one migration file (00001_initial.sql)
containing its complete table schema and seed data, replacing the
multi-file approach with split ALTER/INSERT steps.

Changes per plugin:
- auth: w_auth_sources, w_external_accounts, w_access_tokens + login session seed
- user: w_users + system user seed (removed w_access_tokens — belongs to auth)
- admin: w_system_configs, w_templates + all 32 config seeds + 2 template seeds
  (removed w_schedules, w_task_executions — belong to driver plugins)
- upload: w_upload_stats + indexes + access_mode + backfill
- message_gateway: all w_message_*, w_push_*, w_push_channels, w_push_histories
- risk_control/logstore: w_user_access_logs (PG + ClickHouse)
- driver_asynq_cron: w_schedules + cleanup seed
- driver_asynq_worker: w_task_executions

All CREATE TABLE use IF NOT EXISTS, all INSERT use ON CONFLICT DO NOTHING.
go:embed patterns remain 'migrations/*.sql' — unchanged.
2026-08-28 12:28:37 +08:00
ryan 530a9dd3ee refactor(migration): delete pkg/migrator, move SQL to per-plugin embed
BREAKING: pkg/migrator/ deleted entirely. Migration SQL files are now
owned by each plugin in its own migrations/ directory.

Architecture:
- Delete pkg/migrator/ (26 global SQL files + ClickHouse migration)
- Move global SQL to per-plugin migrations/ with go:embed + Register()
- Rewrite cmd/app.go gooseEngine: uses Inject[DBService] for DB, iterates
  all plugin-registered MigrationEntry, runs goose.Up per entry
- core.MigrationEngine.Migrate signature changed: *Context instead of
  context.Context, so engine can resolve services via IoC

Per-plugin migration ownership:
  auth/             → w_access_tokens, w_auth_sources, w_external_accounts
  user/             → w_users (seed system user)
  admin/            → w_system_configs, w_templates (seeds)
  upload/           → w_uploads, w_upload_stats
  message_gateway/  → w_push_*, w_message_*
  risk_control/     → w_user_access_logs (PG + ClickHouse)
  driver_asynq_cron/  → w_schedules
  driver_asynq_worker/ → w_task_executions

Dependencies:
- cmd/banner.go: removed migration report display (migrations are automatic)
- cmd/reset_passwd.go: removed PreRun migrator.Migrate() call
- go.mod: clickhouse-go kept (used by plugins/infra/database/clickhouse.go)
2026-08-28 12:19:25 +08:00
ryan c9b702d234 refactor(core): fix cross-domain auth imports, unify migration, add downstream scaffold
Architecture:
- Move GetFromContext/SetToContext from plugins/domain/auth to pkg/util
- Move auth context key constants to core/contracts (AuthUserObjKey, AuthTokenAuthKey, etc.)
- Add AuthUserIDKey, AuthUserNameKey, GetCurrentUserID, RevokeToken to contracts.AuthService
- All 4 domain plugin Apply() methods now resolve AuthService via core.Using IoC
- Plugin route middleware uses authSvc.RequireAuthMiddleware() cast to gin.HandlerFunc
- DisallowTokenAuth added to AuthService contract

Migration:
- Replace cmd/app.go SetMigrationRunner bridge with gooseEngine implementing core.MigrationEngine
- Remove cmd/root.go PreRun migration hooks and runMigrations() function
- Migrations now run via core.App.Start() → RunMigrations()

Events:
- Add complete domain event topic catalog and payload DTOs to core/contracts/events.go
- 15 event topics across auth, user, admin, upload, message_gateway, risk_control

Downstream:
- Create downstream/ directory with README and custom_example plugin scaffold

CI:
- Update Makefile code-check architecture guards for Cordis layering
- Enforce: core no gin/gorm/asynq, contracts no plugins/, pkg no plugins/, domain no cross-domain
2026-08-28 11:51:48 +08:00
ryan 416603b616 fix(persistence): migrate all pkg/persistence imports to plugins/infra/database and plugins/infra/cache
- Replace db.DB(ctx) with database.DB(ctx) from plugins/infra/database
- Replace db.Redis/db.PrefixedKey/db.GetJSON/db.SetJSON with cachepkg.* from plugins/infra/cache
- Replace pkg/persistence/idgen with pkg/idgen (already exists)
- Replace pkg/persistence/batchwriter with pkg/batchwriter (already exists)
- Replace pkg/persistence/migrator with pkg/migrator (already exists)
- Replace pkg/persistence/logstore with plugins/domain/risk_control/logstore
- Delete defunct pkg/{persistence,cap,message_gateway,push,shared,task}
- Fix vet issues: db alias in domain_test.go, driver_asynq_worker.TaskHandler reference
- Update Makefile architecture guard
- Update docs and skill references
- Update go.mod: gorilla/sessions promotion to direct dependency
2026-08-28 10:59:24 +08:00
ryan fb6a3edb89 refactor(architecture): eliminate internal package and complete cordis single-owner model and repository migration
- Physically purged all legacy internal/ packages, centralized pkg/model/ and pkg/repository/
- Migrated domain models and database repositories into self-contained owner plugins (user, auth, message_gateway, admin, upload, risk_control)
- Decoupled cross-plugin interactions via pure core/contracts and typed EventBus
- Ensured 100% test coverage pass, zero data races (-race clean), and 0 lint issues in make code-check
2026-08-28 08:40:43 +08:00
ryan 1f348fd425 docs(whitepaper): update white paper to reflect 100% pure Cordis single-track architecture 2026-08-28 07:28:55 +08:00
ryan dc49b72c29 refactor(core): completely decommission legacy internal/apps, internal/platform/bootstrap, and internal/router/v1 2026-08-28 07:28:45 +08:00
ryan df3c5ae756 docs(whitepaper): update white paper with deep physical migration status and zero-lint test report 2026-08-28 07:16:40 +08:00
ryan 56ef70d81f feat(cmd): register all 5 domain plugins in newWaveletApp and fix all lint issues 2026-08-28 07:16:30 +08:00
ryan b259f35bb4 refactor(plugins): complete physical encapsulation of auth, admin, message_gateway, and risk_control domain plugins 2026-08-28 07:15:17 +08:00
ryan e750fadacd chore: docs 2026-08-28 00:11:54 +08:00
ryan 5a00879b63 docs(whitepaper): update white paper with comprehensive QA and E2E test report 2026-08-28 00:07:07 +08:00
ryan b6bfa120fc feat(core): implement app profile lifecycle dispatcher and wire cli commands 2026-08-28 00:02:30 +08:00
ryan a4c3f70f0b feat(plugins): migrate auth, user, message_gateway, risk_control, admin to domain plugins 2026-08-27 23:59:38 +08:00
ryan 75f226cfbf docs(agents): update AGENTS.md and development skills for cordis architecture 2026-08-27 23:56:16 +08:00
ryan 94c5aa4cd3 docs: publish WAVELET architecture white paper and official developer guide 2026-08-27 23:51:37 +08:00
ryan a25bf7a4f9 feat(plugins): package database, cache, logger, and storage as infra plugins 2026-08-27 23:51:10 +08:00
ryan 4efc2c92b7 feat(plugins): implement runtime drivers for http, asynq worker, and cron 2026-08-27 23:48:18 +08:00
ryan ef6296bd22 feat(core): add typed eventbus and domain extension points 2026-08-27 23:47:58 +08:00
ryan c53a5461ab feat(core): add typed eventbus and domain extension points 2026-08-27 23:47:57 +08:00
ryan d853a41eae docs: initialize WAVELET white paper and developer guide 2026-08-27 23:43:06 +08:00
ryan a6ab158855 feat(core): implement context service hub and generic ioc container 2026-08-27 23:41:12 +08:00
ryan 3792313797 docs: add cordis plugin architecture implementation plan 2026-08-27 23:38:02 +08:00
ryan 40de54fe5b docs: add cordis downstream developer guide and cookbook 2026-08-27 23:33:12 +08:00
ryan 360f4f432c docs: add cordis microkernel and plugin architecture design spec 2026-08-27 23:24:34 +08:00
ryan ae3b792e16 feat(core): sync framework security hardening and accessibility improvements
- add util.Go with panic recovery for background goroutines
- add util.EscapeLike and explicit ESCAPE clause for SQL LIKE queries
- add DummyCheckPassword and subtle.ConstantTimeCompare against timing attacks
- enforce session ID rotation upon login/oauth callback to prevent session fixation
- add sliding window login failure rate limiting and oauth state rate limiting
- fix redis client capture race in pubsub listeners and wait on stop channel
- adjust global --primary to oklch(51.1% 0.262 276.966) for WCAG AA contrast
- fix semantic heading levels and missing aria-labels across UI components
- document security, concurrency, and a11y standards in AGENTS.md
2026-08-27 23:01:28 +08:00
ryan 81739f9cb4 chore: format 2026-08-26 15:13:23 +08:00
ryan 2b69f4d8d7 #60 GeoIP 共享单例化:消除访问日志 region 解析每批次的 mmdb 重建开销与无界缓存,ctx 贯穿下载路径
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":81,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 13:48:44 +08:00
ryan b56f27632a #59 -shuffle=on 扫描抓到测试顺序依赖:config_version RAM 配置缓存跨测试污染,setup/cleanup 接入 ram.ResetForTest() 修复
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":66,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 13:35:20 +08:00
ryan fc733d0295 #57 enqueue 修复的同型残留收口:SendFlaredPong/SendRelayPong 合并 select 随机选择 bug,委托 client.enqueue 去重修复
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 13:20:35 +08:00
ryan 453f7e5d90 周期性 -race 重跑抓到真实 bug:wsClientCore.enqueue close 后 select 随机选择致契约违反;确定性先查 done 修复+测试循环加固+gofmt 存量漂移清理
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":95,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 13:14:59 +08:00
ryan 63e3b85294 富交互页 a11y 抽查收尾:8+3 页扫描,修复 cloudflare 筛选器无名/access-token amber 对比度/notifications 缺 h1 共 3 处,全部复扫归零;基准 total_issues 保持 8
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":85,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 13:05:16 +08:00
ryan e66dea9090 a11y 收尾:主题级对比度根因修复(indigo-500→600)+12 处控件 accessible name+4 处 heading-order,7 页复扫全 0 违规;基准 total_issues 保持 8
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":85,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 12:51:48 +08:00
ryan 451ce52592 认证页 axe a11y 审计+修复:7 处布局级真实违规全修,复扫验证 dashboard/admin/system 归零;基准 total_issues 保持 8 不变(纯质量收益)
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":93,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 12:26:37 +08:00
ryan bbf79199aa docs(autoresearch): record run #50 dead-end linter sweep 2026-08-26 11:26:48 +08:00
ryan 40232d86ba 修复 frpc restartProcess 发布未初始化 exec.Cmd 的数据竞争:proc.Cmd/Status 改为 Start 成功后加锁发布
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":75,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 11:19:18 +08:00
ryan 55db1c01ec 后台 goroutine panic 防护:新增 pkg/util.Go 共享助手(recover+调用点日志),全仓 22 个裸 go func() 站点统一收口
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":112,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 11:06:23 +08:00
ryan 3528323b50 GORM 实体搜索 LIKE 转义收尾:6 站点复用 EscapeLike + 显式 ESCAPE 子句,含 OAuth 用户名冲突误报修复
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":102,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 10:49:15 +08:00
ryan 2cb339258c LIKE 过滤器转义修复:日志搜索含 %/_ 的输入不再被当通配符;pkg/util 新增 EscapeLike 共享助手 + 单测
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":106,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 10:41:56 +08:00
ryan 63007fc8c7 全仓 race 扫描发现 upload/cache 监听器 DATA RACE:捕获 redis 客户端消除全局读竞争 + Stop 等待 done + 同型监听器(oauth×2/repository×2)加固
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":92,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 10:24:52 +08:00
ryan 4f8e7e66e3 修复 frps/frpc TOML 配置注入:新增 protocol.TOMLQuote 并在两处配置渲染全部使用
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 10:01:33 +08:00
ryan ed1efd3d54 补 wsClientCore 并发测试 + close() 防 nil conn 守卫
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:53:42 +08:00
ryan efd8268a5d websocket 三 client 结构体去重:嵌入共享 wsClientCore(close/enqueue 单份实现)
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":75,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:49:35 +08:00
ryan 0dd2cf9e80 websocket 三 hub 去重:抽 runWritePump 共享写泵 + 合并 agent 广播函数为 broadcastAgent
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":77,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:39:30 +08:00
ryan be5d067af9 auth_cache negative 缓存加上限防 DoS + relay/flared 删除重复 authenticateAccessToken 改用 agent 共享缓存版
Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":76,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:34:12 +08:00
ryan 3e5f3cc562 autoresearch: 记录 #29-#38 实验日志(unconvert 修复、a11y 审计、未授权面安全修复) 2026-08-26 09:17:56 +08:00
ryan d73aa5f9f0 公开密码登录口按 IP 限制 10 分钟内最多 20 次失败,堵住未授权爆破。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":85,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan c9fc9c0eea 公开 OAuth 登录/授权入口按会话限制 10 分钟内最多 20 个 state,堵住未授权 Redis 洪水。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":95,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan cdc7474d7e 注册开关读取失败时改为关闭,堵住配置缺失时未授权开注册;OAuth 自动注册同样 fail-closed。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":86,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan 983cce3e80 去掉公开 CAP 口硬编码默认密钥;SessionSecret 为空时拒绝签发/核销,防止未授权伪造 PoW。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":75,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan 76e9d5b0e7 登录/注册/OAuth 回调统一走 SetLoginSession,保存前清空 Redis 会话 ID,堵住未授权会话固定。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":90,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan 6b75706b8e 未授权登录口补哑 bcrypt 比较,用户不存在与密码错误耗时对齐;禁用账号不再返回不同文案,堵住用户枚举。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":99,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan 59fd8cda7b 公开登录/注册邮箱验证码比较改为 SHA-256 后恒定时间 Compare,堵住未授权口的计时侧信道。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":81,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan cb94081ebd 未授权 Agent 注册口的 discovery token 改为 SHA-256 后恒定时间比较,堵住计时侧信道;空 token / 末字节翻转用例同步补上。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":71,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
2026-08-26 09:17:56 +08:00
ryan 3de0a54d47 feat(a11y): 设置页无障碍审计并修复开关可访问名称
axe 结构性规则扩展到设置页(安全 Tab + 其他 Tab):
- security-tab 8 个 Switch/SelectTrigger 补可访问名称(Label htmlFor 关联 + aria-label)
- other-tab / auth-source-modal 各 1 个未命名 Switch 补 aria-label
- 新增 2 个 axe 测试,vitest 126 全绿
- 翻译键 loginCaptchaEnabled 加入 fragments(admin.zh-CN/en)
2026-08-17 00:02:51 +08:00
ryan e7b8fb2f99 docs(i18n): 同步 24 篇旧英文文档与中文最新内容
guide 9 篇(quick-start/first-site/sso/troubleshooting/tunnel-usage/waf-usage/waf-ip-group-expr/credits/index)、deployment 7 篇(deployment/server/agent/relay/openflared/upgrade/index)、reference 3 篇(configuration/cli/index)、design 5 篇(architecture/agent-design/tunnel-design/waf-design/index)全部按中文最新版重写同步;waf-usage/waf-design 按新版 DAG 模型重写;修复 reference 中文锚点链接;vitepress 构建 43 个英文页面全绿
2026-08-16 23:27:18 +08:00
ryan 454542c1d0 docs(i18n): 恢复并补齐英文版 vitepress,README 默认改为英文
- README 默认英文:README.en.md → README.md(英文为默认),中文移至 README.zh-CN.md,语言切换链接同步
- 恢复被删除的 docs/en/ 英文文档(git 历史 cc5e53c5^),删除 4 篇已废弃文件
- 英文导航 config.ts 对齐中文结构(新增 Deployment/Changelog 侧栏,同步 Guide/Design 条目)
- 翻译 15 篇中文新增文档:guide 5 篇(certificates/pages-usage/proxy-config/uptime-kuma/zone-domain-migration)+ design 10 篇(zone-design/cloudflare-pointing/waf-orchestration/origin-error-page/edge-cache-design/pages-design/logstore/kuma-design/login-captcha/observability 三篇)
- en 首页更新(新增 Pages 特性、tagline 同步);changelog 英文入口指向中文版
- vitepress 构建验证:43 个英文页面全部渲染

注意:29 篇旧英文文档为恢复版,部分内容(如 deployment/server、reference/configuration)可能落后于中文,需后续逐篇同步
2026-08-16 23:18:29 +08:00
ryan 580c51a73a refactor(i18n): ci 2026-08-16 22:55:08 +08:00
ryan 6b7df5a6b5 refactor(i18n): ci 2026-08-16 22:51:06 +08:00
ryan 497edfd564 refactor(i18n): fragments 为唯一源,主包改为生成物
- 一次性回灌:主包最新内容(含 565 个未同步键)全量写回 6 个 fragment 文件,fragments 成为完整唯一源;新增 core fragment(common/docs/home 等 9 个此前仅存在于主包的命名空间)
- merge-i18n-fragments.mjs 改为从零重建主包(不再以主包为基础),重建结果与原主包语义零差异
- 主包 messages/{zh-CN,en}.json 移出跟踪并加入 .gitignore
- 生成接入入口:predev/prebuild/prebuild:embed/precheck:i18n 钩子 + Makefile code-check 与 checks.sh vitest 前置生成
- 修复既有漂移:configVersions.title 现以 fragments(Config versions)为准
2026-08-16 22:38:50 +08:00
ryan d7d510ec97 chore(i18n): simplify cleanup and previewPublish labels in ops files 2026-08-16 21:53:50 +08:00
ryan f4ec58c0e6 Revert "chore(i18n): simplify cleanup and previewPublish labels in ops files"
This reverts commit 2ba28417b3.
2026-08-16 21:53:23 +08:00
ryan 2ba28417b3 chore(i18n): simplify cleanup and previewPublish labels in ops files 2026-08-16 21:52:50 +08:00
ryan 3f97193280 chore(docs): purge 2026-08-16 21:44:28 +08:00
ryan 2aa0a70762 chore(i18n): simplify cleanup and previewPublish labels in ops files 2026-08-16 21:42:04 +08:00
ryan 55c1fcd5f9 chore: format 2026-08-16 21:39:25 +08:00
ryan e5d2e4b6d5 fix(linux): cast stat.Bsize to int64 for accurate calculations 2026-08-16 21:38:59 +08:00
ryan 4f360cc7a9 Merge finalize branch: 03-frontend-any-cleanup 2026-08-16 21:29:29 +08:00
ryan da43de56ac Merge finalize branch: 02-axe-a11y-audit 2026-08-16 21:29:29 +08:00
ryan f538670e1f Merge finalize branch: 01-quality-sweep 2026-08-16 21:29:29 +08:00
ryan 2f60329886 后端与全仓代码质量清理(golangci 扩展集 · 测试质量 · 并发安全 · 文档同步)
代码质量全量清理,零行为变化:golangci 扩展集 13 类 linter(gosec/modernize/perfsprint/canonicalheader/usestdlibvars/wastedassign/intrange/errorlint/forcetypeassert/recvcheck/exhaustive/unparam)全量修复,测试代码质量(testifylint/thelper/usetesting)25→0,frpc 进程生命周期真 bug(进程组击杀)、全仓 go test -race 6 类数据竞争(含 1 个生产竞争)、SPDX license 头补齐 131 文件、前端测试套件 next-intl 迁移后 44 失败→全绿、过期 swagger 文档重新生成、pnpm-workspace 构建审批。

Experiments: #2-#17, #18, #20, #21, #23
Metric: total_issues 108 → 8 (-92.6%)
2026-08-16 21:24:14 +08:00
ryan c76c5a697b 前端显式 any 类型清理
全前端显式 any 计数 2→0:Slot children?: any → ReactNode | MotionValue 联合(motion 真实类型),顺带修复潜在崩溃(原代码在 isValidElement 前访问 children.type,children 缺失时 TypeError,现无效 children 返回 null,hooks 无条件合规);useControlledState Rest extends any[] → unknown[]。两处 eslint-disable no-explicit-any 注释随之删除。

Experiments: #28
Metric: 全前端 any 2 → 0,tsc/eslint/vitest 全绿
2026-08-16 21:23:38 +08:00
ryan 9609bec8b0 前端 axe 可访问性审计
axe-core(jsdom 结构性规则)真实 a11y 审计,超出 eslint 静态 jsx-a11y 的动态可访问性验证:登录页、注册页、登录 OTP 验证表单(input-otp 分段输入)、人机验证小部件(手动模式)、注册页开启人机验证(自动求解已通过态)五个表单态均零违规。环境修复:tests/setup.ts 补 ResizeObserver mock。

Experiments: #25, #26, #27
Metric: vitest 116 → 121,axe 结构性规则零违规
2026-08-16 21:23:37 +08:00
ryan 9b89d3c630 Merge branch 'autoresearch/code-quality-2026-08-16' 2026-08-16 21:15:13 +08:00
ryan e87f445218 chore(quality): 记录 run #28 实验结果 2026-08-16 21:15:10 +08:00
ryan 40eee778ac 前端显式 any 类型清理 2→0:Slot children?: any → ReactNode | MotionValue 联合(motion 真实类型),顺带修复潜在崩溃(原代码在 isValidElement 前访问 children.type,缺失时 TypeError,现无效 children 返回 null,hooks 无条件合规);useControlledState Rest extends any[] → unknown[]。两处 eslint-disable 注释删除。tsc/eslint/vitest 121 全绿。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":121,"measure_s":0}
2026-08-16 21:14:46 +08:00
ryan 6c128e0be5 axe a11y 审计扩展到最复杂认证路径:注册页开启人机验证(CapWidget 自动求解→已通过状态 + 完整表单),mock getCapToken 避免 jsdom 无 Worker 环境限制。零违规。vitest 120→121 全绿。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":121,"measure_s":71}
2026-08-16 21:05:36 +08:00
ryan 7d03154a8a axe a11y 审计扩展到登录 OTP 验证表单(input-otp 分段输入,FieldLabel htmlFor 正确关联,零违规)与人机验证小部件手动模式(零违规)。环境修复:tests/setup.ts 加 ResizeObserver mock(input-otp 依赖,jsdom 未内置)。vitest 118→120 全绿。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":120,"measure_s":71}
2026-08-16 20:58:24 +08:00
ryan 7f8e257d33 前端真实 a11y 审计:新增 axe-core(devDep)+ tests/a11y.test.tsx,对登录页与注册页渲染完整表单后运行 axe 结构性规则(label/button-name/heading-order/landmark/aria),两页均零违规。摸清并处理了渲染依赖(UserProvider 会话检查、publicConfigQuery 门控、configBool 字符串语义)。vitest 116→118 全绿。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":118,"measure_s":71}
2026-08-16 20:49:55 +08:00
ryan 4d78bc1c38 chore(quality): 会话收敛收尾 — 更新 prompt/ideas 记录最终状态 2026-08-16 20:15:08 +08:00
ryan 1813bbdba3 chore(quality): checks.sh 增加 license-check 门禁(防 SPDX 头再缺失) 2026-08-16 20:11:15 +08:00
ryan aa4faddade 补齐 131 个 .go 文件的 SPDX license 头(repo 自带 make license 约定,早于约定新增的文件含 2 个生产文件;纯注释插入零行为影响),make license-check 转绿。go mod tidy -diff 确认干净。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":116,"measure_s":81}
2026-08-16 20:09:24 +08:00
ryan ab70633e9f checks.sh 新增并发密集包 -race 门禁(8 个快速包,全仓 -race 清零后纳入防回归;frpc/frps 慢套件留作周期全量验证)。核查 7 处 t.Skip 均为合法环境门控。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":116,"measure_s":68}
2026-08-16 20:01:40 +08:00
ryan e1b439d6a5 全仓 go test -race 扫描(93 包)→ 全绿。修复 6 类数据竞争:frpc/frps 测试的锁外读与并发 Wait;oauth/repository 4 个 Pub/Sub 监听器 goroutine 读可变包变量(局部捕获 + done 通道等待);oauth 测试换 db.Redis 前停监听器;【真实生产 bug】tls 响应快照与异步续签 goroutine 并发写 cert 竞争(先快照再起 goroutine);upload/cache 监听器 goroutine 内读 db.Redis(调用方捕获)。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":116,"measure_s":74}
2026-08-16 19:56:19 +08:00
ryan 4962bf90d1 两处真实质量修复:(1) 过期 swagger 文档重新生成(status_2xx/4xx/5xx_count 字段随 a4dd5ca9 加入后未同步 docs,违反 repo 约定,swag init 后差异仅真实新增字段);(2) generate-themes.js 输出补尾换行,themes.json 构建可复现(此前每次 build 弄脏工作树)。验证 next build 成功、musttag/tagalign 调查无真实问题。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":116,"measure_s":86}
2026-08-16 19:19:04 +08:00
ryan c455be3002 基准扩展第 5 维度(文档化):前端 vitest 失败数纳入 total_issues(vitest_failed=0, total=116)。5 维全部处于下限,total=8 不变。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"vitest_failed":0,"vitest_total":116,"measure_s":62}
2026-08-16 19:11:59 +08:00
ryan ab0e5fecf2 chore(quality): 基准扩展第 5 维度 — 前端 vitest(全绿后纳入防回归) 2026-08-16 19:09:58 +08:00
ryan f5c9da03f4 前端测试套件 44 失败→全绿:10 个测试文件补 NextIntlClientProvider 包装(含 React19 createElement 类型修复、.ts→.tsx 重命名);修复真实 i18n ICU bug(githubUrlInvalid 的 {owner}/{repo} 未转义导致生产渲染成 key,zh/en + fragment 4 文件同步转义);更新 2 处过期测试期望。vitest 116/116 + tsc + eslint 全绿,checks.sh 增加前端测试门禁。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":39}
2026-08-16 19:08:21 +08:00
ryan a16be014d4 修复 frpc 进程生命周期真 bug(agent 生产代码):exec.CommandContext 默认只杀直接子进程,被杀 shell 的孤儿 sleep 继续持有 stderr 管道,cmd.Wait() 阻塞到其自然退出(Stop/重启可挂起秒级)。改 Setpgid 进程组 + Kill(-pid) 整组击杀。连带修复两个测试 bug(Manager 拥有 Cmd 的并发 Wait 竞态 → Signal(0) 探测;ssl_renew 用 miniredis 替代 init() 创建的真实 redis 客户端)。go test ./internal/... ./pkg/... 全绿,checks.sh 升级为真实测试门禁。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":40}
2026-08-16 18:47:46 +08:00
ryan c85373ff47 unparam 死代码清理 12→2(保留 2 处 objectstore 构造函数统一签名):移除 10 处恒 nil error / 从未使用的结果(getPoWConfigForRoute 的恒 nil *PoWConfig、getSQLiteOverview/getPostgresOverview/getStatus/loadKumaConfig/filterExpectedRoutes 的恒 nil error、rawJSONString/parsePositiveInt 的弃用 bool、buildProxyRoute 的弃用 []ZoneDomain、getLocked 的恒 nil error),同步简化 12+ 处调用方与死错误检查。9 个受影响包测试通过。metric 持平 8(改进在基准之外)。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":43}
2026-08-16 18:29:51 +08:00
ryan d7b8f44f90 修复 geoip/runtime.go 真死代码:ensureServerMMDB 的 os.Stat 错误被 if-init 遮蔽,err != nil && !os.IsNotExist(err) 恒为 false(外层 err 恒 nil),防御检查从未生效;改为显式捕获 statErr,stat 非 not-exist 错误现在正确返回。基准新增第 4 维度 govet nilness+unusedwrite(文档化扩展),当前 0。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":37}
2026-08-16 18:25:49 +08:00
ryan 85321888e0 chore(quality): 会话收尾 — 更新 prompt/ideas 记录 14 个实验结论与刻意保留项 2026-08-16 18:21:01 +08:00
ryan 65c02ef7a5 基准扩展 exhaustive(文档化)+ 12→0:枚举 switch 补显式 case(全部与现有 default 行为等价,fail-explicit 防未来枚举静默落入 default);source_tasks.go 为控制复杂度合并两个等价校验条件。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":36}
2026-08-16 18:19:08 +08:00
ryan 63a24da9ee 测试代码质量 25→0:assert↔require 一致性(fail-fast)、float 精确比较→InDelta、Equal("",x)→Empty、Equal(len)→Len、errors.Is/As→ErrorIs/ErrorAs、JSON 字符串→JSONEq、handler goroutine 内 require→assert(真健壮性修复)、t.Helper()、os.MkdirTemp→t.TempDir()(符合 repo AGENTS 约束)。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":39}
2026-08-16 18:12:58 +08:00
ryan e5f6b0ad90 基准扩展(文档化):新增测试代码质量维度 25 处(testifylint 20 + thelper 3 + usetesting 2),生产代码 8 处刻意保留不变。新基线 total=33。
Result: {"status":"keep","total_issues":33,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":20,"golint_test_thelper":3,"golint_test_usetesting":2,"golint_test_total":25,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":37}
2026-08-16 18:05:44 +08:00
ryan 111d2900d7 eslint 1→0:pages-source-card useEffect 补 t 依赖(next-intl 稳定引用)。modernize 补 1 处 time.Time omitzero。剩余 8 全部为刻意保留项。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":38}
2026-08-16 18:02:58 +08:00
ryan 73d8173018 recvcheck 7→1:6 个 GORM 模型 TableName 改为指针接收者(GORM 源码确认 reflect.New 判定 Tabler,兼容;模型单测通过)。MillisecondDuration 刻意保留(encoding/json 要求 Marshal 值/Unmarshal 指针的混合)。
Result: {"status":"keep","total_issues":9,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":45}
2026-08-16 18:00:45 +08:00
ryan 4ecec2cf1b forcetypeassert 6→0(缓存 list 断言、relay/flared 中间件契约断言、图片压缩 flight 断言,全部带检查+安全失败路径);errname 1→0;prealloc 2 处(另 1 处与 repo mnd 冲突,用命名常量解决)。nilnil 保留(not-found/可选结果惯例,含接口契约注释)。
Result: {"status":"keep","total_issues":15,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":7,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":14,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":43}
2026-08-16 17:58:03 +08:00
ryan 86fad02c41 errorlint 12→1:3 处 cmd 入口 err!=context.Canceled→errors.Is(防御性,当前 runner 不 wrap 语义不变);2 处 strconv.NumError 断言、1 处 viper 断言、2 处 ==io.EOF、2 处 ==redis.Nil、1 处 ==gorm.ErrRecordNotFound→errors.As/Is;8 处 %v→%w 保留错误链。刻意保留 telegram.go 单处 %v(原始错误仅作上下文文本,wrap 会改变 errors.Is 匹配语义)。
Result: {"status":"keep","total_issues":22,"golint_canonicalheader":0,"golint_errname":1,"golint_errorlint":1,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":21,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":46}
2026-08-16 17:52:03 +08:00
ryan 600a7acdfb docs: 核查并润色文档,对齐项目实际实现
- 删除未经验证的环境要求(Docker 版本号、浏览器条目)与括号废话
- 故障排查改为真实处理路径(升级→重新发布→强制同步→重建 Agent→提交 issue),删除仅开发时用的排障章节
- 删除设计文档中的测试与验收、实现检查清单、贡献者阅读建议等开发内容
- 修正与代码不符的事实:reset-passwd 命令名、证书续签窗口 7 天、Pages 检查间隔 1440 分钟、Relay vhost 端口 8080、SSO 仅支持 OIDC 等
- 去除口语化表述与无意义括号,改写「不是…而是…」句式
- 同步修正文档站链接锚点,构建验证通过
2026-08-16 17:49:57 +08:00
ryan 288b74d104 intrange 3→0 + modernize 5→3:for i:=0;i<len/N;i++ → range len/N(8 处);time.Time 字段 omitempty→omitzero(wire 输出一致);SplitSeq;min() 简化。刻意保留 lark.go omitzero(会改变 wire 行为)。
Result: {"status":"keep","total_issues":33,"golint_canonicalheader":0,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":32,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":45}
2026-08-16 17:47:09 +08:00
ryan ce28f63659 wastedassign 7→0:删除 7 处死初始化(snapshot.go 三连、push 三件套 content、format.go numStr),改 var 声明,零行为变化。
Result: {"status":"keep","total_issues":38,"golint_canonicalheader":0,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":3,"golint_modernize":5,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":37,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":47}
2026-08-16 17:44:45 +08:00
ryan d0414b402a canonicalheader 8→0 + usestdlibvars 3→0:header key 改为 Go 规范大小写(wire 格式本就如此,纯代码修正)、HTTP 方法常量替代字符串字面量。
Result: {"status":"keep","total_issues":45,"golint_canonicalheader":0,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":3,"golint_modernize":5,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":0,"golint_wastedassign":7,"golint_total":44,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":38}
2026-08-16 17:38:18 +08:00
ryan 699e95f12c perfsprint 18→0:strconv.Itoa/FormatInt/FormatUint/FormatBool 替代 fmt.Sprintf、无动词 fmt.Errorf→errors.New、纯字符串拼接。全部语义等价(已核对 diff)。修正 fixer 遗留的 import 问题(引入 goimports 统一整理)。
Result: {"status":"keep","total_issues":56,"golint_canonicalheader":8,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":3,"golint_modernize":5,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":3,"golint_wastedassign":7,"golint_total":55,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":47}
2026-08-16 17:34:44 +08:00
ryan 4e3d79c001 fix(frontend): 修复 useEffect 缺失依赖导致 code-check 失败 2026-08-16 17:29:14 +08:00
ryan 5aaaf8f197 fix(frontend): 修复静态导出下切换语言无效的问题 2026-08-16 17:29:14 +08:00
ryan b76f707c8b modernize 37→5(-32):interface{}→any、内置 max/min、slices/maps 辅助、strings.Cut/SplitSeq、strings.Builder(修复 mail.go O(n²) 拼接)。逐 hunk 核对语义等价;omitzero 冲突修复被自动跳过(wire 格式不变);手动清 4 处遗留 sort import + 2 处 QF1012。
Result: {"status":"keep","total_issues":74,"golint_canonicalheader":8,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":3,"golint_modernize":5,"golint_nilnil":3,"golint_perfsprint":18,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":3,"golint_wastedassign":7,"golint_total":73,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":38}
2026-08-16 17:28:22 +08:00
ryan f1f6bb858a 修复 internal/apps/edge/observability/linux.go 的 2 个 gosec G115 整数溢出转换:helper 改为接收 int64 b,用 gosec 认可的饱和乘法模式(uint64 域乘积 + 上界比较),去掉原 //nolint:gosec,语义不变(Bsize 恒为正)。repo 自带 gate 首次全绿。
Result: {"status":"keep","total_issues":106,"golint_canonicalheader":8,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":3,"golint_modernize":37,"golint_nilnil":3,"golint_perfsprint":18,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":3,"golint_wastedassign":7,"golint_total":105,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":38}
2026-08-16 17:21:04 +08:00
ryan 305d609d0d chore(quality): 启动代码质量 autoresearch 会话
- .auto/ 会话文件(prompt/measure/checks/ideas)
- pnpm-workspace.yaml 认可 @parcel/watcher 与 @swc/core 构建脚本,
  修复 pnpm 11 下 make code-check 无法运行的问题
2026-08-16 17:10:28 +08:00
ryan 5a8722ff07 feat(frontend): 控制台 next-intl 中英双语
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
2026-08-16 16:49:16 +08:00
ryan b66cf3ae9c feat(log): PG 分区清理与 logstore import-lint
CleanupExpired 先按月 DROP 过期分区,再删边界行并清理空分区;apps 禁止直连 analytics。
2026-08-16 16:48:15 +08:00
ryan a8fcf6087a chore(message-gateway): swagger and format 2026-08-16 12:27:03 +08:00
ryan 30acdb91e8 feat(message-gateway): add profile bot pairing card 2026-08-16 12:23:44 +08:00
ryan 124ce9bebb feat(message-gateway): add admin channel cards and per-type forms 2026-08-16 12:22:28 +08:00
ryan 635c1760ad feat(message-gateway): add user bind and unbind APIs 2026-08-16 12:19:32 +08:00
ryan 69d39d906f feat(message-gateway): add admin channel CRUD APIs 2026-08-16 12:17:05 +08:00
ryan 09ec9d0af3 feat(message-gateway): run adapters on worker and handle pairing inbound 2026-08-16 12:14:17 +08:00
ryan 7ca6dbe272 feat(message-gateway): add QQ official C2C botgo adapter
Pin github.com/tencent-connect/botgo v0.2.1. Connect uses C2C intent
only via the official WebSocket session manager.
2026-08-16 12:10:15 +08:00
ryan ef97ca5c7e feat(message-gateway): add Telegram private-chat telebot adapter 2026-08-16 12:06:03 +08:00
ryan cc86370e50 feat(message-gateway): emit message_gateway.inbound domain events 2026-08-16 12:04:55 +08:00
ryan 60afdf7c7b feat(message-gateway): add channel, binding, and pairing repositories 2026-08-16 12:04:14 +08:00
ryan 9b1ef1cf7f feat(message-gateway): add w_message_* models and goose migrations 2026-08-16 12:03:23 +08:00
ryan 8ea4c7e13e feat(message-gateway): add channel types, registry, and pairing codes 2026-08-16 12:01:52 +08:00
ryan a4db79e9bd chore: ignore local git worktrees directory 2026-08-16 12:00:42 +08:00
ryan 8b1eb9ca0d docs(message-gateway): add Wavelet message gateway implementation plan 2026-08-16 11:59:59 +08:00
ryan e36db8a56c docs(message-gateway): add Wavelet inbound channel gateway design spec 2026-08-16 11:55:57 +08:00
ryan 37d5a87c9b chore: docs 2026-08-16 11:32:46 +08:00
ryan 64fbaa7ef1 chore: docs 2026-08-16 11:32:13 +08:00
ryan e52592b16d feat(log): 解耦用户访问日志存储,支持切换日志主库
用户访问日志可在 ClickHouse、PostgreSQL、SQLite 之间切换。
关闭 ClickHouse 时由主库承接写入与查询;切换任务会冻结写入、复制数据后翻转主库。
启动时校验日志主库与运行配置一致,定期清理按各库保留天数删除过期记录。
2026-08-16 11:17:55 +08:00
ryan 6a53619dd2 feat(framework): 回灌 OpenFlare 分层、安全与运行时改进
将平台域持久化收敛为 repository 唯一入口,model 去掉 IO。
邮件头写入前清除 CR/LF,防止 header 注入。
httppool 支持可配置 Transport;batchwriter 增加 MinBatchSize/Stats,flush 失败交回批次;任务 PermanentError 作为 SkipRetry 终态。
设置与推送页的确认改为 AlertDialog;axios 去尾斜杠并按 Gin 数组序列化查询参数。
升级共享 Go 依赖(Gin、Asynq、OTel、GORM、Redis 等)。
2026-08-16 11:07:20 +08:00
ryan fa689aedbc feat(sync): 同步 Wavelet 推送审计、OTel schema 与前端字体
自定义 Webhook 在 HTTP 200 但业务 errcode 非零时记为失败,任务日志记录上游响应。
OTel Resource 改为 NewSchemaless,避免 semconv 与 SDK 版本冲突。
前端用 next/font 自托管 Inter,并忽略浏览器扩展改写 body 引起的 hydration 警告。
2026-08-16 11:06:54 +08:00
ryan b9b42e3174 ci: make canary 2026-08-16 10:13:39 +08:00
ryan 20830d31bd chore: guideline 2026-08-16 10:01:56 +08:00
ryan f960511cc0 chore(release): v3.5.3
### 新增
- 访问日志「日志明细」支持按 HTTP 状态码筛选,可直接输入任意状态码。
- 访问日志「日志明细」支持自定义时间范围筛选,可按起止时间检索日志。
- 首页看板改版:24 小时请求趋势拆分展示请求总量与 2xx/4xx/5xx 状态码类请求量并独占一行;移除宿主机磁盘指标,24 小时容量趋势(CPU/内存)并入业务流量卡片展示。

### 🛠 修复
- 修复首页「来源分布」卡片在 PostgreSQL/SQLite 日志库下无数据的问题。
- 修复源站错误页「仅针对 GET 请求」未真正透传非 GET 响应的问题:POST/PUT 等非 GET 请求现可完整看到源站原始报错内容。
2026-08-13 11:44:08 +08:00
ryan 465440fa5b fix(access-logs): 修复状态码自定义 2026-08-13 11:33:12 +08:00
ryan a4dd5ca9e1 feat(dashboard): 首页请求趋势拆分状态码并合并容量到业务流量
- 24 小时请求趋势拆分展示请求总量与 200/400/500 状态码请求量,独占一行;
  时间桶聚合新增 status_200/400/500_count(CH countIf、PG FILTER),
  请求趋势改为基于原始桶聚合(小时 rollup 无状态码口径)
- 首页移除宿主机磁盘指标,容量趋势(CPU/内存)并入业务流量卡片展示
- 压缩协议 traffic_24h 扩展为 7 元组,前端归一化同步更新
2026-08-13 11:10:37 +08:00
ryan a9e4237bbf feat(access-logs): 状态码支持手动输入,新增时间范围筛选
- 状态码筛选支持预设快捷选项 + 手动输入任意 100-599 状态码(数字校验)
- 新增时间范围筛选:shadcn 日期+时间选择器(Popover+Calendar+时分 Select),
  起止时间以 RFC3339 成对传入,后端校验格式与先后关系,非法值返回 400
- 默认显示来源 IP/访问域名/状态码,节点 ID/请求路径/时间范围折叠进「更多筛选」
2026-08-13 10:27:40 +08:00
ryan 75d1fcf345 feat(access-logs): 日志明细支持按状态码筛选并折叠次要搜索项,修复首页来源分布无数据
- 修复 PostgreSQL/SQLite 日志库下首页「来源分布」卡片无数据:RegionCounts 对空
  节点 ID 误拼 node_id = '' 恒空条件,改为空节点 ID 表示全节点聚合(对齐 CH 语义),
  并过滤空白归属地
- /access-logs?tab=list 新增状态码筛选:状态码下拉含常用 2xx/3xx/4xx/5xx 选项,
  校验 100-599,非法值返回 400;ClickHouse 与 PostgreSQL/SQLite 日志库均支持
- 搜索框折叠:默认仅显示来源 IP 与状态码,节点 ID/访问域名/请求路径折叠进
  「更多筛选」
2026-08-13 09:59:32 +08:00
ryan 284eec54f7 chore: guideline 2026-08-12 12:40:06 +08:00
ryan a3ad0d2c97 chore: rename .agent to .agents and update skill path references 2026-08-12 12:39:15 +08:00
ryan 92322c7a22 feat(push): log upstream webhook response in task history
Pusher.Send now returns the upstream response body alongside the error,
so the push task handler can print what the webhook actually replied
(custom channel e.g. {"errcode":0,"errmsg":"ok"} or a rejection
like {"errcode":93000,...}) into the task log on both success and
failure. Other pushers (lark/telegram/email) return an empty string,
keeping their behavior unchanged.

fix(push): surface webhook business errors in custom channel audit

CustomPusher.Send only checked the HTTP status code. WeChat Work /
DingTalk webhooks return HTTP 200 with a non-zero errcode in the body
even when the message is rejected (e.g. template_card requires
card_action.url when type=1), so rejected pushes were recorded as
'success' in the notification history. Parse the response body and
return an error when errcode is non-zero, matching the Lark pusher.
2026-08-12 12:32:23 +08:00
ryan f499645cdc chore: guideline 2026-08-12 12:02:14 +08:00
ryan f1577bf092 fix(openresty): 修复源站错误页「仅针对 GET 请求」覆盖非 GET 原始报错数据
proxy_intercept_errors 会在 Lua 判断前丢弃源站错误响应体,POST/PUT 等
请求收到 503 时被 OpenResty 自带错误页覆盖原始报错数据。现改为在代理
location 内用 Lua header/body 过滤器仅对 GET 请求替换错误页,非 GET
请求完整透传源站原始状态码与响应体;非仅 GET 模式继续使用命名 location
承载错误页。
2026-08-09 19:38:44 +08:00
ryan 01ed2c5e36 chore(release): v3.5.2
修复几个遗漏bug
2026-08-09 14:08:04 +08:00
ryan 80696c12fa fix: lint 2026-08-09 13:47:40 +08:00
ryan 3d4d99081e fix(log): PG 日志库批量写入为零 ID 行生成雪花 ID
PostgreSQL 日志表 id 为 NOT NULL 且无默认值,而 GORM 将零值 uint64
主键视为自增并省略 id 列,导致 node access log / 可观测指标等批量
落库持续报 "null value in column id violates not-null constraint"。
在 BatchInsert* 落库前为零 ID 行生成雪花 ID(与 ClickHouse 写入路径
一致),并新增单元回归与 PG 集成回归测试覆盖六张日志表。
2026-08-09 13:47:13 +08:00
ryan 0639855653 fix(openresty): 修复源站错误页「仅针对 GET 请求」未生效
error_page 的 URI 内部重定向会把请求方法改写成 GET,导致内部
Lua 中 ngx.req.get_method() 恒为 GET,get_only 判断永不命中,
POST/PUT 等请求仍返回自定义错误页。

改为命名 location(@__openflare_origin_error)承载错误页:
命名 location 保留原始请求方法与原始错误状态码,非 GET 请求
直接以原状态码退出、不再注入自定义 HTML。附带回归断言,禁止
回退到 URI 内部重定向形式。
2026-08-09 13:42:38 +08:00
ryan 0524ae1da4 chore(release): v3.5.1
### ✨ 新功能
- 日志存储解耦:ClickHouse 变为可选项,不启用时由 PostgreSQL/SQLite 承担全部日志功能;新增「切换日志数据库」任务支持 PostgreSQL/SQLite 与 ClickHouse 间数据迁移(迁移期间冻结日志写入,成功后自动切换主库并保留源数据);`log_database` / `log_db_migration` 设为受保护配置;ClickHouse 改为默认关闭。
- 新增 PostgreSQL/SQLite 日志存储实现:节点访问日志按月分区,统计查询合并为单次扫描、IP 汇总归属地取查询窗口内最新记录、WAF 按 IP 聚合减少扫描次数,并新增 `logged_at` 前导索引与主机名小写表达式索引;过期清理直接删除完全过期的整月分区,启动时兜底预建当月及未来 2 个月分区。
- 性能指标与访问日志的保留时长解耦:新增三库共用的 `metric_retention_days` 配置(默认 3 天),每日垃圾清理按独立短留存清理指标快照。

### 🛠 修复
- 修复 UptimeKuma 同步调试日志泄露凭据:Socket.IO 事件日志不再打印 payload 内容(仅记录长度),避免凭据进入日志。
- 修复日志保留天数配置继承旧键导致的误删风险:`log_retention_days_*` 不再继承 `database_auto_cleanup_retention_days`,统一默认 30 天。

### ⚡️ 优化与改进
- 系统定期垃圾清理由每 2 小时改为每日执行一次(凌晨 3 点,Asia/Shanghai),降低非必要高频扫描。

### 💄 其他/体验
- 服务工作者(SW)注入挑战页改为前台无感知:不再显示「加载中…」文案,页面空白,仅通过浏览器控制台输出 `[sw-challenge]` 调试信息,注入过程不打扰访客。
- 用户访问日志(`w_user_access_logs`)记录禁用:不再采集与写入新的用户访问日志,存量数据与管理端访问日志统计页面保留。
2026-08-09 11:39:28 +08:00
ryan adee4f7b27 docs: update 2026-08-09 11:22:35 +08:00
ryan 1d0f2d6342 fix(log): hard-set log retention days default to 30, drop legacy inheritance
log_retention_days_* 迁移不再继承旧键 database_auto_cleanup_retention_days
的值,统一默认 30 天。此前若旧键残留异常小值(如 2 天)会被静默带入,
导致升级后首次垃圾清理把大部分日志直接删掉。PG/SQLite 双方言同步修改,
文档默认值 90 -> 30。
2026-08-09 10:48:45 +08:00
ryan e3f603f72a fix(security): stop logging UptimeKuma socket payload content 2026-08-09 10:42:21 +08:00
ryan 3b010bb15e feat(log): disable user access log recording
- 移除全局用户访问日志采集中间件与批写入 writer(risk_control 包整包删除),
  不再写入 w_user_access_logs;存量数据与管理端访问日志统计页面保留
- 日志库迁移任务不再排空用户访问日志队列,状态接口不再展示其缓冲队列统计
- 迁移测试的系统配置 seed 计数断言更新为当前实际值(86 → 95),
  注释改为提示新增配置 seed 时同步更新
2026-08-09 10:35:39 +08:00
ryan f530cd4025 perf(log): optimize PG log store queries and expired partition cleanup
- Count/节点访问日志统计改为单次扫描聚合,WAF 按 IP 聚合由三次扫描合并为两次
- IPSummaries 归属地改为取过滤窗口内最新记录(对齐 ClickHouse argMax 口径),
  子查询带窗口条件,可分区裁剪并命中索引
- 新增 goose 迁移:of_node_access_logs (logged_at DESC, id DESC) 前导索引与
  lower(trim(host)) 表达式索引,加速列表排序与主机过滤
- 过期日志清理先按数据校验直接 DROP 完全过期整月分区,再对边界月逐行删除;
  启动时兜底预建当月及未来 2 个月分区,跨月停机重启后首次写入不再报
  "no partition of relation found"
2026-08-09 10:20:58 +08:00
ryan 08d28c2c8e feat(log): drop empty old-month PG partitions during cleanup
系统垃圾清理任务删除过期日志后,顺带清理旧月份空分区表:
PostgreSQL 按月分区的访问日志表(节点/用户)在数据删除后若该月
分区已无数据,则自动删除对应分区表,避免历史分区表无限累积。

- 仅删除「当前月之前」且为空的月份分区,当月/未来月及仍有数据的分区保留
- ClickHouse/SQLite 为 no-op(CH 分区随数据删除自动消失)
- 修复既有集成测试 pg_inherits 查询(inhrelid → inhparent)
- 新增单元测试与 PG 集成测试
2026-08-09 09:33:59 +08:00
ryan 34a0896ff8 chore(task): run system garbage cleanup once daily
系统定期垃圾清理 cron 由每 2 小时(0 */2 * * *)改为每日凌晨 3 点
(0 3 * * *,Asia/Shanghai),降低非必要高频扫描。新增 PG/SQLite
双方言 goose 迁移(含 Down 回滚)与迁移测试。
2026-08-09 09:25:30 +08:00
ryan 0c22e76f4b fix(frontend): optimization 2026-08-09 09:14:54 +08:00
ryan bd2183c8bb feat(log): add independent short retention for performance metrics
性能指标(CPU/内存/磁盘/网络)不再跟随 log_retention_days_*,新增三库共用
的 metric_retention_days 配置(默认 3 天),系统垃圾清理按独立短留存清理
指标快照;访问日志保留时长不变。新增 PG/SQLite 双方言 goose 迁移 seed。
2026-08-09 09:04:33 +08:00
ryan 9df2437e47 fix(config): set ClickHouse to disabled by default and update related documentation 2026-08-09 08:54:26 +08:00
ryan e8c414aa12 fix: ch migrate 2026-08-09 08:47:56 +08:00
ryan 7e8aa5fa0f Merge branch 'codex/log-database-decoupling'
# Conflicts:
#	docs/changelog/index.md
#	frontend/app/(main)/error-pages/page.tsx
#	internal/infra/persistence/migrator/migrator_test.go
2026-08-09 08:37:37 +08:00
ryan 7e518987de chore(release): v3.5.0
### 🛠 修复
- 修复 PoW 挑战页潜在 XSS 风险,状态与错误文案改用纯文本渲染,并限制跳转 URL 仅允许 http/https 协议。
- 修复邮件发送的邮件头注入风险,写入邮件头前自动清除 CR/LF 换行符(CWE-93)。
- 修复 UptimeKuma 同步调试日志泄露凭据问题,输出日志前对密码和 Token 等敏感字段打码。

### ⚡️ 优化与改进
- 新增 Service Worker 离线兜底功能,为启用 HTTPS 的网站自动下发 Service Worker 并缓存离线页,域名不可达时展示离线兜底页面。
- 重构响应页面设置,将源站错误页与 Service Worker 离线页整合至统一的「响应页面」(/responses)标签页,并增加 URL 查询参数 tab 状态同步。

### 💄 其他/体验
- 新增离线页内置预制模板套件(「极简白底」、「线框拓扑」、「包豪斯」),与源站错误页模板风格保持一致,支持编辑界面一键加载与预览。
2026-08-08 23:08:12 +08:00
ryan 61484090f9 fix: 修复源站错误页「仅针对 GET 请求」导致配置发布失败并回滚 2026-08-08 22:37:40 +08:00
ryan 94b74d72f6 fix(frontend): fallback empty offline page html in editor workspace preview 2026-08-08 21:56:44 +08:00
ryan 6487ce666d fix(security): harden PoW XSS, email header injection and UptimeKuma log redaction 2026-08-08 21:52:46 +08:00
ryan c4be34b214 fix(frontend): fallback empty offline page html to default template in preview 2026-08-08 21:52:02 +08:00
ryan fda727cf53 docs: rename contact page references to offline page in changelog
refactor(frontend): rename contact page to offline page and update routes/components

feat(frontend): add preset templates suite for offline contact page
2026-08-08 21:36:58 +08:00
ryan f083da20f2 feat(frontend): add dedicated edit and preview routes for response pages and sync tab state to url 2026-08-08 21:18:53 +08:00
ryan 9797fcdb2f fix(openflare): improve SWOfflineDomains validation and snapshot diff logic 2026-08-08 20:52:07 +08:00
Ryan 93ec3096f3 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-08 20:48:54 +08:00
Ryan 0e34301c92 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-08 20:48:37 +08:00
Ryan 12b5271f92 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-08 20:47:33 +08:00
Ryan 8ee966434d Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-08 20:46:54 +08:00
ryan 6882481a56 Merge remote-tracking branch 'origin/feature/service-worker' into feature/service-worker 2026-08-08 20:44:27 +08:00
ryan b675038bba refactor(frontend): unify error pages into responses page and remove error-pages route 2026-08-08 20:43:58 +08:00
ryan d17ec9d17d fix(docs): resolve vitepress build error by escaping raw tags and excluding superpowers dir 2026-08-08 20:43:56 +08:00
ryan 8758f9a061 refactor(frontend): unify error pages into responses page and remove error-pages route 2026-08-08 20:40:05 +08:00
ryan 7d93d3d2a1 fix(log): address remaining CodeRabbit suggestions for log database switch and migrations 2026-08-08 20:36:04 +08:00
Ryan 074edf17a1 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-08 20:30:36 +08:00
Ryan 6faf525af0 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-08 20:30:17 +08:00
ryan a6fc2b7737 fix(log): address CodeRabbit review findings for log database decoupling 2026-08-08 20:15:36 +08:00
ryan ca21ff3a5b feat(option): add sw offline
fix(openresty): scope sw injection per cert partition

fix(lint): satisfy revive and biome format for sw offline feature

docs: sw offline scope changelog

fix(frontend): use scoped query key for sw scope zones

fix(frontend): hide preview link in sw contact page editor

feat(frontend): add sw scope domain picker and contact page fields

refactor(frontend): generalize html editor workspace for reuse

feat(openresty): scope sw offline injection by route domains

feat(openresty): add sw offline domains snapshot field

feat(option): add sw offline domains scope option

docs: fill html editor workspace generalization detail

docs: sw offline scope implementation plan

docs: sw offline scope design

test(openresty): assert single merged access block in sw enabled servers

fix(openresty): restrict sw intercept to https server blocks

fix(openresty): version sw offline cache by html content

fix(agent): escape redir in sw challenge page to prevent xss

fix(agent): return sw.runtime module table and add lua spec

docs: sw offline fallback changelog

fix(frontend): memoize option map to preserve unsaved contact page edits

feat(frontend): add response pages module with contact page tab

feat(agent): ship sw offline lua assets and placeholder substitution

feat(config): wire sw offline options into config snapshot

feat(openresty): render sw offline assets and challenge intercept

feat(openresty): add sw offline ConfigSnapshot fields and placeholder

feat(db): seed sw offline options

feat(option): add sw offline config keys and validation

docs: add service worker offline fallback implementation plan

docs: adopt global-option pattern for SW offline fallback (matches origin error page)

docs: unify offline contact page with error pages as response pages

docs: service worker offline fallback design (issue #23)
2026-08-08 20:14:28 +08:00
ryan 7d71f1e4e1 feat(log): decouple log storage from ClickHouse with switchable logstore
- New internal/repository/logstore abstraction: exported domain interfaces
  (AccessLogStore/ObservabilityStore/UserAccessLogStore/StatusStore),
  config-driven provider (Active/Build/Migrating/SetConfigReader), GORM
  implementation for PostgreSQL/SQLite (incl. hourly rollups computed in
  real time, migration listers, PG partition maintenance), and a ClickHouse
  wrapper preserving the native batch path; repository facade delegates to
  logstore; import-lint test enforces apps never import analyticsrepo.
- ClickHouse is now optional: the log DB is either the main DB (postgres
  when database.enabled, else sqlite) or clickhouse; boot validation +
  first-run seed; log_database / log_db_migration are protected keys.
- New user task 切换日志数据库 (of_log_db_switch): freeze log writes,
  drain batch writers, copy all 6 raw log tables by id (preserving IDs)
  with target-partition pre-creation for PG, flip log_database on success,
  clear the freeze flag on failure.
- Per-store retention (log_retention_days_*) with expiry cleanup folded
  into the daily system_cleanup task; legacy database_auto_cleanup_* and
  of_database_auto_cleanup decommissioned.
- goose migrations: 6 log tables in PG (2 monthly-partitioned) + SQLite,
  retention config seeds, schedule cleanup; GET
  /api/v1/admin/status/log-database endpoint; frontend retention settings,
  switch-task UI and status badge; changelog and docs updated.

docs(plan): log database decoupling implementation plan

docs(design): log database decoupling design (ClickHouse optional)
2026-08-08 19:43:01 +08:00
ryan 734fe45baa chore(release): v3.4.5
### ⚡️ 优化与改进

- 源站错误页新增「仅针对 GET 请求」开关:开启后仅对 GET 请求的匹配错误状态码返回自定义错误页,其它 HTTP 方法透传源站响应。
- 升级前后端依赖至最新稳定版
- Agent 不再将 GeoLite2 Country/City MMDB 嵌入二进制:Docker 镜像在默认数据目录 COPY 数据库文件,裸二进制首次启动时按需下载,显著减小 Agent 包体积;OpenResty 仍从磁盘路径读取 MMDB,Server 控制面仍仅内嵌 Country MMDB(不含 City)。
2026-08-08 11:25:42 +08:00
ryan ef22ecc5dc refactor(error-pages): merge trigger policy into one card
Combine enable, GET-only, and status code settings into a single
strategy card with the save action in the header for a cleaner layout.
2026-08-06 22:29:09 +08:00
ryan 6738abdec1 feat(openresty): add origin error page GET-only option
Allow restricting custom origin error HTML to GET requests so other
methods pass through origin responses. Adds option seed, snapshot field,
edge limit_except/Lua handling, and admin UI switch.
2026-08-06 20:22:44 +08:00
ryan d17d8457f3 chore: upgrade dependence 2026-08-06 17:41:06 +08:00
ryan 16f34928c9 chore: AGENTS.md 2026-08-06 16:40:13 +08:00
ryan 3328d3d121 refactor(agent): stop embedding GeoIP MMDB in agent binary
Agent ships without City/Country MMDB in the binary; Docker images COPY
databases into data_dir, bare installs seed via download on first start.
Server keeps Country-only embed for optional MaxMind control-plane use.
Also harden fetch script nonempty check and reject non-file MMDB paths.
2026-08-06 16:24:57 +08:00
ryan fb08002e99 chore(release): v3.4.4
### 新增

- 新增全局源站错误页:可在「网站管理 → 错误页」配置开关、触发状态码(支持 `500-599` 区间与单码)与自定义 HTML;默认启用 OpenFlare 极简错误页并保持真实 HTTP 状态码,修改后随配置版本发布下发到边缘,关闭后恢复透传。
- 新增 Cloudflare DNS 指向管理:可复用现有 Cloudflare DNS 账号或配置独立 Token,按分组将 ZoneDomain 的单条 A 记录异步同步到边缘节点 IPv4,并支持成员橙云、同步状态与节点 IP 变更联动。

### 修复

- 修复 Agent 在配置已对齐但磁盘校验和不一致时,Pages 等对账成功后仍保留 `LastError` 的问题,避免偶发网络失败被健康事件长期显示为「活动中」且无法自动恢复。

### 改进

- 删除、撤销与未保存离开等确认操作统一改用页面内 AlertDialog,不再使用浏览器原生 `confirm` 弹窗,交互风格与系统其余对话框保持一致。
2026-08-06 15:52:26 +08:00
ryan f650214bbb fix(openresty): preserve origin error status on custom error pages
Remove error_page '=' form that adopted the internal URI status (often 200)
and left ngx.status as 0. Resolve the original code from $status/upstream
and set ngx.status before rendering the HTML body.
2026-08-06 15:45:41 +08:00
ryan ba1c9222c2 refactor(error-pages): preset templates with OpenFlare branding 2026-08-06 15:25:39 +08:00
ryan 076bf8b95c Merge branch 'feat/origin-error-page' 2026-08-06 14:16:28 +08:00
ryan 835c50dbaa docs: origin error page configuration and changelog
Document three origin error page Option keys in configuration reference
and merge the unreleased changelog entry into a user-readable description.
2026-08-06 14:10:03 +08:00
ryan 42f7f47716 feat(frontend): add origin error page settings under websites 2026-08-06 14:05:21 +08:00
ryan 7d47db1f34 feat(option): seed and validate origin error page options 2026-08-06 13:59:54 +08:00
ryan 68d8f786cc feat(openresty): render origin error page directives
Wire origin error page into OpenResty proxy route rendering: ConfigSnapshot
fields, default HTML template SupportFile, proxy_intercept_errors + error_page
with status-preserving internal Lua location, and Agent placeholder substitution
for __OPENFLARE_ERROR_PAGE_TMPL__. Pages routes are excluded.
2026-08-06 13:52:40 +08:00
ryan 9d93dc0b9f merge: fix/agent-clear-last-error-on-sync-success
Merge agent sticky LastError clear fix into main.
2026-08-06 13:48:14 +08:00
ryan 1a4a03a20d fix(agent): clear sticky LastError on successful sync paths
Pages reconcile could succeed while agent state retained a previous
network error, so health events stayed active indefinitely. Clear
LastError whenever sync completes successfully without re-applying
config, and cover the paths with regression tests.
2026-08-06 13:47:48 +08:00
ryan 07e835c543 feat(openresty): add status code tag expand helper 2026-08-06 13:46:58 +08:00
ryan 1f5bebd18a docs(plan): add origin error page implementation plan
拆分为状态码解析、OpenResty 渲染、Option/快照、前端设置页与文档验收五步任务。
2026-08-06 13:44:45 +08:00
ryan fd62570431 docs(design): add origin error page design
全局可配置源站错误页:默认 500-599、Cloudflare 风格模板、
状态码透传与在线 HTML;配置进 Option 与配置版本快照。
2026-08-06 13:42:41 +08:00
ryan 484b49d79d fix(frontend): replace browser confirm dialogs with AlertDialog
统一删除、撤销与未保存离开等确认操作为 shadcn AlertDialog,避免
window.confirm/alert 打断界面风格;同步更新 WAF 编辑器相关单测与 changelog。
2026-08-06 13:08:43 +08:00
ryan cffa009b8c fix 2026-08-04 13:50:41 +08:00
ryan 4eced2b721 feat(cloudflare): add DNS pointing integration 2026-08-04 13:30:40 +08:00
ryan ea7658815a fix(migration): quote reserved authorization column 2026-08-04 12:49:58 +08:00
ryan 3edcdb9e9f feat(cloudflare): add DNS pointing integration
Implement Cloudflare connection management, pointing groups and members, asynchronous A-record reconciliation, node IP triggers, admin APIs, management pages, migrations, tests, and documentation.
2026-08-04 12:32:37 +08:00
ryan 99f0f63b99 agents rename 2026-08-04 11:40:32 +08:00
ryan 21fb303ef2 doc: cloudflare 对接 2026-08-04 11:31:11 +08:00
ryan 776c6b397c ci: canary 2026-08-03 21:54:08 +08:00
ryan 3aa4d98cd6 ci: canary version 2026-08-03 21:53:52 +08:00
ryan 1f71c9f25b ci: canary version 2026-08-03 21:53:08 +08:00
ryan db17965b1a chore: docker-compose 2026-08-03 21:48:34 +08:00
ryan ae35b4e733 ci: docker hub 2026-08-03 21:45:50 +08:00
ryan c6d22549ce ci: canary 2026-08-03 21:32:42 +08:00
ryan 9f15f42b47 fix(frontend): keep browser API on same-origin rewrites in dev
Document that NEXT_PUBLIC_WAVELET_BACKEND_URL must stay unset for local
next dev so axios hits /api/* and Next rewrites proxy to the backend.
2026-08-03 21:25:22 +08:00
ryan 943818f7d4 refactor(repository): 收敛 model/repository 分层为唯一持久化入口
将 OpenFlare 与平台业务的数据访问从 model 与 apps 直连迁入 repository,
model 仅保留实体与无 IO 规则;补充 code-check 架构守卫与开发规范。
2026-07-24 17:00:17 +08:00
ryan a4588b05e0 fix(frontend): suppress body hydration warning from extensions
Browser extensions can inject classes like vc-init onto body before
React hydrates; ignore attribute mismatches on body.
2026-07-24 16:30:49 +08:00
ryan 1625cfb2fb feat(frontend): add next-intl bilingual i18n for core paths
Wire next-intl without locale routes, add zh-CN/en catalogs, language
switcher, and migrate layout/auth/settings UI copy. Document i18n rules
in AGENTS.md and keep static export builds working.
2026-07-24 16:25:41 +08:00
ryan 9c1369186f docs(i18n): add frontend bilingual i18n design spec
Capture the approved next-intl non-routing approach for zh-CN/en,
locale resolution, static-export constraints, and phase-1 core-path scope.
2026-07-24 16:08:45 +08:00
ryan 23a5488203 refactor(http): remove dead internal/util HTTP client wrapper
Drop internal/util (unused httppool wrapper and dead StringArray) and
rely on pkg/httppool plus oauth context injection for HTTP clients.
2026-07-24 15:49:52 +08:00
ryan d99c5b7c43 refactor(pkg): merge pkg/utils into pkg/util
Consolidate pure helper packages under pkg/util and update imports.
2026-07-24 15:45:10 +08:00
ryan 33a1c32cf8 refactor(structure): group platform, infra, and shared packages
Reorganize internal packages into platform/infra/shared layers and update
imports, docs, and seed-count tests to match current system configs.
2026-07-24 15:41:59 +08:00
ryan fbbb75095f refactor(structure): group platform, infra, and shared packages
Move process wiring, technical adapters, and cross-cutting contracts out of flat internal/ packages so new code has a clear home without changing business layout.
2026-07-24 15:28:39 +08:00
ryan 290427d5fa chore: remove 2026-07-24 14:29:07 +08:00
ryan 72dbfac3fc chore: update pg version 2026-07-24 11:18:01 +08:00
ryan 68d730a388 chore(release): v3.4.3
### 🛠 修复
- 修复了 IP 组自动抓取使用预设规则时未写入 ttl 的问题,避免配置缺少封禁时长。
- 修复了限流相关数据库迁移中的表名错误,确保升级脚本正确执行。

### ⚡️ 优化与改进
- 边缘缓存对齐 Cloudflare 默认模型:不再因登录 Cookie 等请求头一律跳过缓存,登录用户可命中静态资源;响应 Set-Cookie 不入库,并补充默认 Edge TTL。生效需重新发布节点配置。
- 新增全局与站点级单 IP 请求频率限制,触发时返回 429,并支持继承、关闭与按站点隔离。
- IP 组自动规则支持 2xx/4xx/5xx 类状态码写法,同步间隔下限降至 1 分钟,回看窗口支持 60m/1h 等时长写法。
- 限流页请求压力图 RPS 纵轴按可见窗口峰值动态缩放,低流量更易读。

### 💄 其他/体验
- 补充边缘缓存运维与故障排查说明,并对「所有可缓存 GET」策略增加风险提示。
2026-07-24 00:04:20 +08:00
ryan f94767fbc7 perf(cache): 边缘缓存对齐 Cloudflare 默认模型 2026-07-23 23:39:15 +08:00
ryan 018f237384 perf: AGENTS.md 2026-07-22 23:05:23 +08:00
ryan ef28cdc3ad perf: makefile 2026-07-22 22:52:31 +08:00
ryan b1f2241d0a perf: skill 2026-07-22 22:31:18 +08:00
ryan fae83ab0fa perf: biome 2026-07-22 22:26:02 +08:00
ryan 5b1e27d0a3 feat(frontend): biome 2026-07-22 22:25:30 +08:00
ryan b498117f32 perf(skill): clean 2026-07-22 22:19:24 +08:00
ryan ef4e9c9edc feat(frontend): biome 2026-07-22 22:19:07 +08:00
ryan f28aa6520e fix(lint): 消除 linter 告警 2026-07-20 15:53:53 +08:00
ryan d58b4b6b0e feat(waf): 自动 IP 组 lookback 支持 60m/1h 时长写法
将 lookback_minutes 替换为 lookback,移除最小 5 分钟回看限制,并兼容旧字段。
2026-07-20 15:48:16 +08:00
ryan 866f1df5e3 fix(waf): IP 组同步间隔下限改为 1 分钟
移除同步周期 5 分钟限制;回看窗口仍保持最小 5 分钟。
2026-07-20 15:41:31 +08:00
ryan 351e8ce78c feat(waf): StatusRatio/StatusCount 支持 2xx/4xx/5xx 类写法
自动 IP 组表达式可按状态码类汇总占比与计数,兼容原有精确状态码。
2026-07-20 15:39:14 +08:00
ryan 67a30eb5ed fix(waf): IP 组预设规则写入默认 ttl
点击自动抓取预设规则时补齐 ttl=-1,并规范化自动配置默认 JSON。
2026-07-20 15:36:06 +08:00
ryan 80c47f6ff3 feat(rate-limit): 站点级请求频率限制支持继承与自定义
在站点详情流量限制中配置 limit_req_per_ip;渲染按 effective rate 生成多 limit_req_zone,并以站点+IP 隔离计数。
2026-07-20 15:02:38 +08:00
ryan a261c01a9c fix(db): correct table name to of_proxy_routes in migration 2026-07-20 14:06:07 +08:00
ryan ae5345c03e feat(rate-limit): add default request rate limit configuration
- Support openresty_default_limit_req_per_ip in system_configs.
- Add limit_req and limit_req_status 429 directive generation in openresty renderer.
- Implement route-level limit_req_per_ip override and explicit disable.
- Add frontend UI inputs and validation in rate limits tab config.
- Update swagger API docs and changelog for v3.4.3-beta.3.
2026-07-20 10:58:13 +08:00
ryan fda8d7fcb1 fix(rate-limits): 请求压力纵轴随 dataZoom 可见区间缩放
拖动底部时间范围条时按可见窗口最高 RPS×1.5 更新纵轴,访客轴同步按可见数据重算。
2026-07-20 09:02:31 +08:00
ryan b91c848256 fix(rate-limits): RPS 纵轴按峰值 1.5 倍动态缩放
请求压力图不再使用固定美化刻度上限,改为当前时段最高 RPS × 1.5,低流量更易读、高峰不易裁切。
2026-07-20 08:49:46 +08:00
ryan 36cff502f7 chore(release): v3.4.2
### 🛠 修复
- 修复 Pages 部署包路径校验、归档展开限额、历史版本裁剪、代理路由绑定与 Agent
  下载过程中的安全和一致性问题;大包改为流式处理,异常中断遗留的部署包会安全补偿清理。

### ⚡️ 优化与改进
- Pages 项目新增持久部署源,支持 Remote URL 或公开 GitHub Release;GitHub latest
  可按设定间隔自动检查并发布更新,默认间隔为每天一次。
- Remote URL 默认允许公网与内网地址,新增「允许不安全连接」开关。
- Pages 详情页重构为「部署 / 设置」Tab,部署源卡片样式更紧凑统一。
- 安全性新增「限流」设置,可为边缘站点配置默认并发与带宽;填 -1 可关闭。
- 限流页新增分析视图,展示请求压力与独立访客趋势,支持域名过滤与时间预设。

### 💄 其他/体验
- 精简部署源数据模型,去除脱敏与无用字段。
- Pages 部署源任务不再隐藏,可在任务管理中查看。
- make prettier 支持自动清理前后端无用 import。
- 限流趋势桶调整至 3 分钟粒度,范围扩展至 24h/3d。
- Agent 部署命令增加 Pages 命名卷持久化。
2026-07-19 20:54:57 +08:00
ryan fa588797bf chore: eslint fix 先于 prettier 执行 2026-07-19 20:52:01 +08:00
ryan a963b8bf54 chore(prettier): format 并清理无用 import
make prettier 统一格式化,goimports 与 eslint unused-imports 自动移除未使用导入。
2026-07-19 20:51:12 +08:00
ryan d9663f91d6 chore: make prettier 自动清理前后端无用 import
前端接入 eslint-plugin-unused-imports,pnpm format 同步执行 eslint --fix;
后端将 gofmt 替换为 goimports,并修复 danger-zone 未使用导入。
2026-07-19 20:47:28 +08:00
ryan 4481677ef3 refactor(pages): 公开部署源任务并默认每日扫描
移除 Pages 部署源任务的 InternalOnly 限制,任务管理可查看与调度;
将 scanner cron 与 GitHub latest 默认检查间隔调整为每天一次,
并优化部署历史列表展示。
2026-07-19 20:41:09 +08:00
ryan 20249d917c feat(rate-limits): use 3-minute trend buckets
Rate-limit analysis requests overview with bucket_minutes=3; RPS uses
count/180. Overview still defaults to 60-minute buckets.
2026-07-19 20:30:20 +08:00
ryan abe8fb8268 refactor(pages): 精简部署源模型并重构详情页交互
将 Remote 网络策略收敛为 allow_insecure,去掉脱敏与无用字段;
Pages 详情拆为部署/设置 Tab,统一卡片样式与来源信息展示。
2026-07-19 20:23:31 +08:00
ryan f0b51a99b3 fix(db): 重编号 Pages 部署源迁移避免版本冲突
main 已占用 202607190001(OpenResty 默认限流),
将 Pages source runtime / scanner seed 顺延为
202607190002、202607190003,并同步迁移测试期望配置数。
2026-07-19 19:41:30 +08:00
ryan c92f986978 merge: 合并 PR #22 Pages 部署源 V2 到 feat/pages-source-sync-v2
基于最新 main 合并 deqiying/feat/pages-source-sync-v2,
解决 docs/changelog/index.md 与限流相关条目的冲突。
2026-07-19 19:36:48 +08:00
deqiying ccea08fe47 docs(pages): 收口部署源 V2 实现
同步 Pages、总体架构、Agent 与使用指南,记录阶段提交、验证结果和生产验收边界。
2026-07-19 19:25:28 +08:00
ryan 72962beb0f feat(rate-limits): use 1m buckets and 24h/3d ranges
Allow overview bucket_minutes=1; rate-limit analysis uses 1-minute
buckets and replaces 7d preset with 3 days.
2026-07-19 19:20:59 +08:00
ryan b56290d79d feat(rate-limits): use 5m trend buckets and 24h/7d ranges
Overview API accepts bucket_minutes (5|60); rate-limit analysis uses
5-minute buckets and drops 15d/30d presets. Widen rank value column.
2026-07-19 19:18:15 +08:00
deqiying 67b051c2bc feat(frontend): 支持 Pages 自动更新交互
在 GitHub latest 来源中提供自动更新开关、检查间隔和运行状态。\n页面按来源到期时间低频刷新,并在自动发布或人工回滚后同步项目与部署历史。
2026-07-19 19:09:57 +08:00
deqiying 999428cf9a feat(pages): 增加来源扫描与自动更新
为 GitHub latest 来源增加五分钟 scanner、按来源间隔检查、精确 revision 自动发布与租约恢复。\n记录退避和投递统计,并为 PostgreSQL 与 SQLite 幂等创建内部排程。
2026-07-19 19:09:21 +08:00
ryan 86d2d6b0ad feat(rate-limits): add RPS analysis tab with dual-axis chart
Split rate-limits into analysis/config tabs; reuse access-log overview
filters; chart hourly RPS vs visits with dataZoom; rank top hosts/IPs
by window-average RPS.
2026-07-19 19:09:01 +08:00
deqiying 848884d8cd fix(pages): 增加部署包孤儿补偿
按项目、来源、运行时与上传记录锁序补偿异常中断遗留的部署包。\n同时隐藏并保护系统内部排程,避免通用任务管理入口修改 scanner。
2026-07-19 19:08:43 +08:00
ryan f783a1e6fa docs: add rate-limit analytics design
Tabs for analysis/config, dual-axis RPS chart with overview filters
and average RPS rankings from access-log overview.
2026-07-19 19:06:06 +08:00
deqiying c39a3edcc3 feat(pages): 支持 GitHub Release 部署源
增加 latest/tag 手动检查与同步、ETag 与限流退避、资源替换确认,以及对应的前端来源管理和部署来源展示。
2026-07-19 18:31:42 +08:00
ryan 4c17f5277a feat(agent): persist Pages dir in Docker deploy volume
Mount openflare-agent-pages to /data/var/lib/openflare/pages so
container rebuilds keep local Pages packages.
2026-07-19 18:28:35 +08:00
ryan 0e097a66c4 docs: document default edge rate limits 2026-07-19 18:19:20 +08:00
ryan 39cba821d5 feat(frontend): add security rate-limits page and inherit UI 2026-07-19 18:17:16 +08:00
ryan c5f8105db8 feat(proxy-route): allow -1 to disable rate limits 2026-07-19 18:14:16 +08:00
ryan 2bc2d82ad0 feat(config): add openresty default rate limit system options 2026-07-19 18:12:51 +08:00
ryan a3125c8276 feat(openresty): merge global default limits at route render 2026-07-19 18:10:51 +08:00
ryan 4d7b63f217 docs: add default edge rate limit implementation plan
Task breakdown for global OpenResty limit defaults, route inherit
semantics, security rate-limits page, and render-time merge.
2026-07-19 18:05:43 +08:00
ryan fada04c373 docs: add edge default rate limit design
Specify global OpenResty limit defaults with per-route inherit (-1 off)
and render-time merge in RenderRouteConfig.
2026-07-19 18:02:13 +08:00
deqiying 38b0516937 feat(pages): 支持 Remote 部署源同步
新增部署源配置与运行态模型、安全下载、租约续期、原子激活和失败补偿。

接入内部任务与脱敏前端交互,并阻止数据库 Trace 和日志展开敏感查询参数。
2026-07-19 17:36:51 +08:00
deqiying 4e8ec23264 fix(pages): 收紧部署包与 Agent 同步边界
完成 V2 Phase 0 安全与一致性前置:统一真实归档限额、流式拉取、候选裁剪、保留上传删除语义及 Pages 路由引用锁。
2026-07-19 16:42:45 +08:00
deqiying f386674464 docs(pages): 完善部署源 V2 实现方案 2026-07-19 16:14:09 +08:00
ryan e0398397a9 chore(release): v3.4.1
### 🛠 修复
- 收紧 WAF 安全防护特征,降低对常见正常请求的误伤(含避免 SQL 特征 /* */ 误匹配 Accept: */*)。
- 优化 WAF 规则编辑器返回按钮、列表操作与属性栏布局体验。
- 节点详情「运行诊断」摘要不再展示具体错误日志,避免长日志撑破布局。

### ⚡️ 优化与改进
- WAF 规则编排新增「UA 检查」与「安全防护」节点,支持浏览器/操作系统白名单、爬虫与自定义正则屏蔽,以及路径穿越、注入类等基础特征检测。
- 优化边缘 WAF 安全防护、UA 检查与 IP 匹配热路径,降低开启基础防护时的 CPU 占用。
- Agent 内嵌 resty.ipmatcher,部署时不再依赖无效 opm 包。
- 新建反代规则时默认开启边缘缓存(标准静态资源策略)。
- 节点详情页调整为「概览」与「状态与部署」,边缘节点支持自动填充部署命令。

### 💄 其他/体验
- WAF 规则编辑器支持节点自定义命名、拖放添加、右键删除与一键格式化布局。
2026-07-19 15:43:05 +08:00
ryan fafee0055a feat(nodes): 优化 2026-07-19 15:42:02 +08:00
ryan 6619f5b650 fix(nodes): 运行诊断不再展示具体错误日志
摘要区仅保留异常数量与事件类型,避免长日志撑破卡片布局。
2026-07-19 15:25:02 +08:00
ryan a65d0f291b feat(nodes): 调整节点详情 Tab
将数据看板并入概览,运行状态与配置合并为状态与部署;。
2026-07-19 15:16:26 +08:00
ryan c00ead9aa0 feat(nodes): 调整节点详情 Tab 并新增边缘部署命令
将数据看板并入概览,运行状态与配置合并为状态与部署;
边缘节点支持自动填充 Server URL 与 Agent Token 的 Docker 部署卡片。
2026-07-19 15:01:38 +08:00
ryan 7366832e12 fix(agent): 内嵌 resty.ipmatcher,移除无效 opm 依赖
OPM 无 api7/lua-resty-ipmatcher 账号导致镜像构建失败;改为 vendor
api7 v0.6.1 并随 ManagedWAFLuaFiles 部署到 lua 目录。
2026-07-19 14:57:51 +08:00
ryan 1a7e5e6c41 perf(waf): IP 匹配改为索引查询(ipmatcher / 预编译)
加载时编译 IP 组与节点 IP/CIDR 索引,优先 resty.ipmatcher 基数树,
否则 exact 哈希 + 预解析 CIDR,避免大名单线性扫描打满边缘 CPU。
2026-07-19 14:48:07 +08:00
ryan 46ce7de513 perf(waf): 收窄安全防护扫描面并优化 UA 热路径
注入类检测仅扫 Query/Cookie/Referer/有限 Body,避免全 Header 匹配拖垮边缘 CPU;
按开关采集输入、GET 跳过 read_body,UA 仅 lower 一次并用 set 匹配白名单。
2026-07-19 14:16:34 +08:00
ryan 39473cb370 chore: prettier 2026-07-19 13:00:01 +08:00
ryan 64e40a7c18 fix(waf): 移除编辑器未使用的图标导入以通过 code-check 2026-07-19 12:58:24 +08:00
ryan 53ddb45614 fix(waf): 规则编辑器返回按钮对齐 websites 详情样式 2026-07-19 12:55:49 +08:00
ryan ad6621fce9 fix(waf): 收紧安全防护特征,降低常见正常请求误伤
- SSRF 仅匹配 URL 形态,避免 Chrome/x.0.0.0 误中
- 命令注入去掉裸 &&/|| 与裸 shell 名
- SQL sleep/benchmark 要求数字参数
- XSS javascript:/eval 要求更像代码的上下文
- 路径穿越去掉过宽的 c:\windows;CRLF 去掉单独 %0a/%0d
2026-07-19 12:54:01 +08:00
ryan 60d6e3e846 fix(waf): 调整编辑器返回与格式化布局按钮位置
返回置于标题上方;格式化布局移至保存按钮左侧。
2026-07-19 12:52:12 +08:00
ryan fd9348b7bd feat(waf): 规则编辑器一键格式化节点布局
按从开始节点出发的层次从左到右整理坐标,并 fitView 到画布。
2026-07-19 12:49:42 +08:00
ryan 32113eb790 fix(waf): 列表操作改为直接图标按钮
规则组与 IP 组表格去掉「…」菜单,操作以图标平铺展示。
2026-07-19 12:47:02 +08:00
ryan 1ba05ec0bd fix(waf): 避免 SQL 特征 /* */ 误匹配 Accept: */*
开启 SQL 注入防护时不再把正常 Accept 头当成攻击。
2026-07-19 12:46:23 +08:00
ryan b75f985815 feat(waf): 新增安全防护节点 security_check
基础特征检测九项可开关;默认开启路径穿越与文件包含;命中任意规则走 false。
2026-07-19 12:33:13 +08:00
ryan db89f68547 docs(waf): 规格 — 安全防护节点 security_check
九项基础特征检测可开关;默认仅路径穿越与文件包含;命中任意规则 false。
2026-07-19 12:20:51 +08:00
ryan 74106474ca fix(waf): UA 检查说明改为问号悬浮提示
将屏蔽/匹配相关 FieldDescription 收敛为 CircleHelp Tooltip。
2026-07-19 11:52:29 +08:00
ryan 1d97ea69d0 fix(waf): UA 检查属性栏将屏蔽区块移到匹配上方 2026-07-19 11:49:55 +08:00
ryan 53d9572508 refactor(waf): 移除规则画布右上角删除按钮
删除改为右键菜单与键盘快捷键。
2026-07-19 11:48:54 +08:00
ryan 8f3ff59567 feat(waf): 规则画布右键删除节点与连线
覆盖画布默认右键菜单;节点/连线右键弹出删除项,系统节点禁用。
2026-07-19 11:47:04 +08:00
ryan d47ceb9971 feat(waf): UA 非正常不含爬虫,并支持自定义正则屏蔽
block_abnormal_ua 仅 Other/Unknown;新增 block_custom_ua 与 custom_ua_patterns。
2026-07-19 11:43:45 +08:00
ryan 7476c86976 fix(waf): UA 检查开启后才显示匹配与屏蔽并补充说明
未开启 require_ua 时隐藏匹配/屏蔽区块;爬虫与非正常 UA 开关增加分类提示。
2026-07-19 11:38:18 +08:00
ryan 28eef0bbcd feat(waf): 新增 UA 检查节点 ua_check
支持要求携带 UA、浏览器/OS 白名单 and-or 匹配,以及优先屏蔽爬虫与非正常 UA。
2026-07-19 11:35:31 +08:00
ryan 047ed6554d docs(waf): 规格 — UA 检查节点 ua_check
定义 require/白名单 and-or/屏蔽优先级及与访问日志一致的 UA 分类标签。
2026-07-19 11:27:55 +08:00
ryan b5e27fabde feat(waf): 规则编辑器节点自定义命名与拖放添加
对齐后端 label 字段;属性栏可编辑显示名称;节点库改为拖到画布落点创建。
2026-07-19 11:01:24 +08:00
ryan 4166cc9861 docs(waf): 规格 — 规则编辑器节点命名与拖放添加
确认仅前端消费已有 label,节点库改为拖到画布落点,不做备注。
2026-07-19 10:57:28 +08:00
ryan 24862dcbed chore(release): v3.4.0
### 🛠 修复
- 修复了访问日志概览按域名筛选无效的问题,现已兼容 hosts 与 hosts[] 参数。
- 修复了 Agent 观测缓冲合并访问日志时忽略 cache_status 导致缓存状态被去重丢弃的问题。
- 修复了访问日志概览在 ClickHouse 查询失败时静默吞错的问题,现会输出错误日志便于排查。
- 修复了数据看板业务流量趋势与已提供数据口径不一致的问题,业务量统一由访问日志聚合。
- 修复了节点地图在缺少精确经纬度时,把香港/新加坡/台湾等地区错误标到占位坐标的问题。

### ⚡️ 优化与改进
- 访问日志重构为概览、IP 明细与日志明细:支持时间窗聚合 IP 请求数/2xx 比例/入出站流量与详情分析,明细展示完整请求字段。
- 边缘访问日志支持 User-Agent 与 cache_status(命中/回源/未缓存),概览增加设备/浏览器/系统与状态码分布。
- 新建站点开启缓存时推荐仅缓存标准静态资源(不含 HTML);存量空策略与按 URL 行为保留为所有可缓存 GET。
- 边缘观测以访问日志为业务唯一真相;Agent 仅上报明细与主机读数,升级需重建或替换 Agent。
- Pages 支持更多压缩格式上传、URL 导入部署包,以及可配置的包大小与历史保留策略。

### 💄 其他/体验
- 优化了访问日志排行榜与饼图布局,页签状态支持 URL 参数记忆。
- 启用 cache_status 与边缘缓存策略变更后,需执行相关迁移并重新发布节点配置。
2026-07-19 10:45:40 +08:00
ryan 920a530aa7 feat(access-logs): 新增 IP 明细 Tab 并完善日志详情字段
按时间窗聚合 IP 请求数/2xx 比例/入出站流量,支持排序与详情分析;
日志明细详情仅展示请求业务字段,IP 情报迁至独立详情弹窗。
2026-07-19 00:42:59 +08:00
ryan bfd9de69af fix(access-logs): 修复概览域名筛选参数 hosts[] 被 Gin 忽略
Axios 默认序列化为 hosts[]=,Gin QueryArray("hosts") 读不到导致筛选失效;
后端兼容 hosts/hosts[],前端改为重复键序列化。
2026-07-19 00:28:37 +08:00
ryan 204f6d9a8b fix(cache): 存量空/url 策略规范为 all,避免静默收窄
评审修复:enabled 且 policy 为空或 url 时,写入/展示/快照/渲染均映射为 all,
保证旧站点宽缓存范围不变;新建 UI 仍显式提交 static 作为推荐默认。
2026-07-19 00:02:52 +08:00
ryan 04f029c705 feat(cache): 开启缓存默认仅缓存标准静态资源
路由缓存策略新增 static(内置扩展名,不含 HTML)与 all;
存量 url 规范为 all。OpenResty 渲染与代理路由 UI 同步。
2026-07-18 23:32:49 +08:00
ryan 7401f5d0b4 docs(design): 边缘缓存默认可缓存范围对标 Cloudflare
约定开启缓存默认 static 扩展名策略,存量 url 映射为 all,
并明确第一期不做 Edge TTL/Purge/Cache Rules。
2026-07-18 23:24:23 +08:00
ryan 0bb6830047 feat(access-logs): 概览支持 Zone/域名多选筛选
概览可按 Zone→Domain 层级多选域名并折叠展开;明细列表 IP 旁展示地区。
后端 overview 支持 hosts 多域名精确匹配。
2026-07-18 23:07:19 +08:00
ryan 6f221b042e fix(agent): 观测缓冲去重纳入 cache_status 并保留原始 -
避免同一请求不同缓存状态被合并丢弃;OpenResty 的 - 原样入库便于详情区分。
2026-07-18 22:56:10 +08:00
ryan fb5a4e5b59 feat(access-logs): 上报并展示边缘缓存状态 cache_status
OpenResty 日志输出 $upstream_cache_status;Agent/协议/ClickHouse 贯通入库。
明细列表与详情按 HIT/MISS 等推导命中、回源、未缓存三态标签。
2026-07-18 22:50:46 +08:00
ryan ee9d651c8a docs(obs): 约定访问日志 cache_status 与明细三态展示
仅上报 $upstream_cache_status,不上报回源地址;UI 由原始值推导
命中缓存 / 回源 / 未使用缓存。
2026-07-18 22:46:46 +08:00
ryan 9aec984bee feat(access-logs): 明细详情支持 IP 分析与 URL Tab 记忆
- 新增单 IP 分析接口,趋势时间范围支持至 30 天
- 明细详情弹窗展示趋势、汇总与 Top 分布,可快捷管理 IP 组
- 访问日志页签改为 URL 参数记忆,筛选后保持当前 Tab
2026-07-18 22:40:21 +08:00
ryan bf71bc540b feat(access-logs): 优化概览饼图布局并在查询出错时增加日志记录
- 将设备类型与状态码饼图的断点由 xl 降为 lg,在大屏/笔记本视口下保持双列展示
- 修复 valueCountDistribution 在 ClickHouse 查询出错时静默吞掉错误的缺陷,引入 logger.ErrorF 捕获
- 补充 unreleased 变更日志
2026-07-18 22:02:09 +08:00
ryan e49078ac3b feat(access-logs): 接入 User-Agent 与设备/浏览器/状态码分布
- Agent 访问日志上报 user_agent,OpenResty log_format 输出 http_user_agent
- of_node_access_logs 新增 user_agent 列并写入 ClickHouse
- 访问日志概览新增:设备类型饼图、状态码饼图、浏览器/OS/User-Agent 排行
- 日志明细列表增加 User-Agent 列
- 扩展 UA 解析工具(browser/os/device)并支持 CLI 识别
2026-07-18 21:18:59 +08:00
ryan 177578ef4e feat(access-logs): 重构访问日志为概览与明细双 Tab
新增访问日志概览 API 与前端页面:汇总请求量/访问量/带宽趋势与 Top 排行,
明细列表保留检索;排行榜改为紧凑列表样式。
2026-07-18 20:58:32 +08:00
ryan 4c0c389122 chore(release): v3.3.1
### 🛠 修复
- 修复了看板业务流量趋势与 Zone 已提供数据口径不一致的问题,业务量统一由访问日志聚合,避免边缘预聚合窗口差分导致近 24 小时趋势严重偏低。
- 修复了节点地图在缺少精确经纬度时,将香港、新加坡、台湾等地区错误回退到南美等占位坐标的问题,补全质心数据并改进复合地名匹配。

### ⚡️ 优化与改进
- 重构边缘可观测模型:访问日志为业务唯一真相,Agent 仅上报明细、主机指标与 OpenResty 健康连接;新增 edge_health 与 access_log_hourly,删除请求预聚合与 OpenResty 吞吐路径。
- 协议去掉旧兼容层,Agent 升级需销毁重建或二进制替换;旧本地观测缓冲会自动删除并在运行中重建。
- 调整 Agent 默认心跳为 3 秒、离线判定为 60 秒、离线补传窗口为 60 分钟,使节点状态与观测数据刷新更及时。
- 看板 UV 改为窗口内真正去重,Zone 分桶 UV 明确不可跨桶相加;网络趋势仅保留已提供/接收数据,磁盘读写改为按秒速率展示。
- Pages 支持多压缩格式上传、URL 导入部署包,以及可配置的包大小上限与历史保留数量;边缘按项目只保留最新激活部署,切换版本无需重发主配置。
- 新建代理规则时可选择直连、隧道或 Pages 源站类型,与详情页一致。
- 优化 Pages 部署包校验性能,不再为包内每个文件计算哈希,改由整包校验和保障完整性。

### 💄 其他/体验
- 更新可观测设计文档与运维说明,明确健康状态权威源与升级策略。
- 同步 Swagger 与变更日志,便于对照 API 与发布说明。
2026-07-18 16:37:20 +08:00
ryan a0ccafc6ee fix(geo): 修复香港等节点地图质心缺失落到南美占位
补全 Hong Kong/Singapore/Taiwan 质心数据,并改进复合地名与 ISO 匹配,
避免 geo 无精确经纬度时错误回退到巴西等地的占位坐标。
2026-07-18 13:52:20 +08:00
ryan 26057514a1 feat(obs): 去掉宿主机网卡趋势,磁盘读写改按速率展示
Agent 不再采集网卡累计字节,看板与节点网络图仅保留访问日志已提供/接收。
磁盘 IO 按小时换算为 B/s 曲线,摘要为近 24 小时平均速率,并同步 Swagger。
2026-07-18 13:17:13 +08:00
ryan 55f8c9a527 fix(obs): 对齐无兼容层与健康/UV 权威语义
Agent 本地旧观测缓冲直接删除并运行重建;设计文档去掉兼容期表述。
健康当前态以 PG status/message 为准,CH 仅存 status 与连接时序;
Zone 曲线标明分桶 UV,顶部为整窗独立访客。
2026-07-18 12:09:27 +08:00
ryan 802d516f5b docs: 观测重构 changelog 与 Swagger 同步
补充 unreleased 变更说明,并重新生成 Swagger 以匹配去兼容层后的 API。
2026-07-18 11:53:11 +08:00
ryan 71ce028f91 feat(frontend): 观测网络字节字段与 UV 文案对齐
网络图仅使用 bytes_provided/received;看板与节点 UV 改为 24h/查询窗口
独立访客;清理 traffic_reports 与 openresty 吞吐兼容字段;运维清理目标
改为 node_edge_health。
2026-07-18 11:53:11 +08:00
ryan f0e234df1f feat(obs): 访问日志 SSOT 与 edge_health,去掉协议兼容层
Agent 仅上报 host_metrics/edge_health/access_logs;业务流量与 UV 由
Server 侧访问日志聚合。新增 of_node_edge_health 与 of_access_log_hourly,
删除 request_reports/openresty 吞吐路径;API 不再暴露 traffic_reports
与 openresty_rx|tx。心跳/离线默认阈值与回填迁移一并入库。
2026-07-18 11:53:11 +08:00
ryan 9a0974cce8 docs(obs): 边缘可观测 SSOT 设计与实施计划
补充 observability 设计/数据模型/传输模型,更新架构与 Agent 文档侧栏,
并写入 M5 迁移与小时汇总回填运维说明。
2026-07-18 11:53:11 +08:00
ryan 3b9f4daa4e perf(pages): skip per-file hashes during package inspect
Inspect deployment archives via file handles and declared sizes instead of loading the whole package and hashing every member, while keeping whole-package checksums for Agent integrity checks.
2026-07-17 18:37:34 +08:00
ryan 285f127d48 feat(proxy-routes): align create form with upstream type selection
Let new proxy rules choose direct, tunnel, or Pages origin the same way as the detail reverse-proxy section, instead of only accepting a single upstream URL.
2026-07-17 18:20:35 +08:00
ryan 79820b33eb feat(pages): import deployment packages from URL
Add upload-from-url so admins can paste an HTTP(S) link and let the control
plane download the archive with browser-like headers. Private/LAN hosts and
insecure TLS certificates are allowed for internal artifact stores; package
size and format checks reuse the existing local-upload pipeline.
2026-07-17 17:55:12 +08:00
ryan ce736e2de4 feat(pages): pull latest by project and keep a single edge release
Agents now treat pages_project_id as the stable anchor and fetch the
control-plane active package via project latest APIs, so activating a
deployment updates edges without republishing main config. Local roots use
projects/{id}/current, only the newest release is retained after a successful
switch, hash/package races retry, and per-project failures no longer block
siblings.
2026-07-17 17:43:40 +08:00
ryan a0fcf9f627 feat(pages): configurable limits, multi-format packages, and dual versioning
Make Pages package size and history retention system-configurable, support
zip/tar.gz/tar.xz/tar.bz2/tar/7z uploads, prune history with clear keep-N
semantics, and rebind agent config to the live active Pages deployment so
main-config rollback never depends on pruned packages.
2026-07-17 17:16:48 +08:00
ryan 368df3f76b chore(release): v3.3.0
### 🛠 修复
- 修复了 WAF 站点为空绑定规则时请求异常的问题,规范空站点绑定为数组并兼容历史 JSON 空值,避免请求时 Lua 处理失败。
- 修复了 WAF PoW 验证在部分场景下的异常。
- 修复了 Pages 部署文件列表请求与后端路由不一致的问题,并补充回归测试。
- 默认关闭 Redis maintenance notification 自动协商,并应用到平台与 Asynq 客户端,减少在不支持的 Redis 服务上的兼容性警告。
- 修复了 WAF 规则编辑器画布状态不稳定的问题:改用 React Flow 受控节点状态,支持删除节点与连线,节点属性仅在选中后展示。

### ⚡️ 优化与改进
- 新增 WAF 可组合规则可视化编排能力,提供基于 React Flow 的规则编辑器、有序图形 API 与运行时 DAG 执行;规则仅在 OpenResty reload 时发布,并通过校验和驱动的 IP 组快照在受界共享内存中协调。
- 完善 WAF 地域匹配数据,使用完整国家与一级行政区数据,国家选项同时显示中文名称与 ISO 代码,行政区支持按名称或代码搜索。
- Agent 现内置国家与城市地址库,首次启动无需下载即可使用地区匹配,并会在后续自动更新数据。
- 优化了 SQL 日志输出:常规查询日志下调至 debug 级别,慢查询与错误日志不再输出 SQL 文本,避免敏感参数在生产日志中暴露。

### 💄 其他/体验
- 优化了 WAF 规则编辑器初始视图,缩小编排区高度与首次适配缩放比例,默认显示更多画布上下文。
- 服务启动监听就绪后再打印服务横幅,避免在监听失败时显示误导性信息。
- 改进了日志打印输出。
2026-07-14 09:07:06 +08:00
ryan 15e614b304 fix(waf): pow 2026-07-13 17:07:49 +08:00
ryan 46941f65d5 fix(waf): handle empty rule bindings
Encode empty site bindings as arrays and normalize legacy JSON null values in the OpenResty runtime to prevent request-time Lua failures.
2026-07-13 16:49:55 +08:00
ryan 0c2961ae6d fix(waf): complete geography match options
Use full country and ISO subdivision data, show localized country names with codes, and add searchable region selection.
2026-07-13 16:34:18 +08:00
ryan a61d55bb1b chore: improve log print 2026-07-13 16:12:30 +08:00
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 6b6c786cfe feat(startup): print service banner after listener ready 2026-07-13 15:49:19 +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 26be762c3a prettier 2026-07-13 15:44:03 +08:00
ryan 0548a8a5d4 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:43:15 +08:00
ryan a938a5e67f fix(db): log SQL statements at debug level 2026-07-13 15:41:04 +08:00
ryan 60bc03f519 fix(db): log SQL statements at debug level
Move routine GORM SQL output to debug and omit SQL text from slow-query and query-error logs to prevent sensitive parameters from being emitted at production log levels.
2026-07-13 15:39:48 +08:00
ryan 9dc3983e0f fix(frontend): WAF 规则编辑器缩小编排区高度和首次适配缩放比例,默认显示更多画布上下文 2026-07-13 15:39:48 +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 1eff7878a1 prettier 2026-07-13 15:10:28 +08:00
ryan 08e8eea932 fix(frontend): prettier config 2026-07-13 15:04:28 +08:00
ryan 85d5c8568c fix(frontend): stabilize WAF rule canvas
Use React Flow controlled node state, support deleting nodes and edges, and show node properties only after selection.
2026-07-13 14:59:20 +08:00
ryan 74ddf97b36 feat(agent): embed GeoLite2 City database
Initialize missing Country and City databases from embedded assets and use FyraLabs releases for periodic updates.
2026-07-13 14:38:36 +08:00
ryan a1a997bcda feat(waf): complete composable rule orchestration
Add the React Flow rule editor, ordered graph APIs and runtime DAG execution.\n\nPublish rules only on OpenResty reload and reconcile checksum-driven IP group snapshots in bounded shared memory.
2026-07-13 14:17:15 +08:00
ryan d36409fbf9 refactor(frontend): order waf rule bindings 2026-07-13 12:12:49 +08:00
ryan 4000366856 feat(frontend): create orchestrated waf rules 2026-07-13 12:00:24 +08:00
ryan af20e2e838 feat(waf): add composable rule graph core 2026-07-13 11:57:36 +08:00
ryan 2fff30e188 docs(waf): split orchestration migrations 2026-07-13 11:35:16 +08:00
ryan 74c2f57453 docs(waf): plan composable rule implementation 2026-07-13 11:23:34 +08:00
ryan 30e09f5985 docs(waf): design composable rule graph 2026-07-13 11:14:10 +08:00
ryan 43e293e062 fix: frontend optimization 2026-07-13 10:31:09 +08:00
ryan 439ac41da8 fix(frontend): correct Pages deployment files route
Align the Pages deployment file-list request with the backend route and add a regression test.
2026-07-13 09:29:22 +08:00
ryan d97581fb1e chore(release): v3.2.0
### 🛠 修复
- 修复了嵌入式静态前端访问 Zone 详情页时回退到默认首页 HTML,导致界面显示错误并触发 React hydration 异常的问题。
- 修复了 Zone 概览中“已提供的数据总计”长期为 0 的问题,确保 Agent 上报的访问日志流量字节数可以正确入库并用于统计。
- 修复了 Zone 域名导入、删除和更新相关接口与前端交互中的异常,提升域名管理流程的稳定性。
- 修复了 Docker 部署 ClickHouse 时监听配置被覆盖的问题,避免宿主机无法访问 ClickHouse 服务。

### ⚡️ 优化与改进
- 新增 Zone 与正规化 Zone 域名管理能力,将网站、域名、证书与反代路由关系收敛到更稳定的资源模型。
- 管理端网站入口调整为 Zone 列表与 Zone 详情页,支持在概览、域名、路由、证书和设置之间统一管理网站资源。
- 配置快照、OpenResty 渲染、Tunnel 与 Uptime Kuma 监控改为从 Zone 域名绑定读取域名和证书,减少反代路由中的冗余字段。
- 新增 Cloudflare 风格的 Zone 流量概览图,支持按 24 小时、7 天和 30 天查看唯一访问者、请求数与已提供数据趋势。
- 支持在系统设置中管理控制台菜单展示范围,便于按使用场景精简侧边栏入口。

### 💄 其他/体验
- 调整数据库自动清理设置文案,明确自动清理遵循 ClickHouse 表 TTL,并说明访问日志与观测数据的保留下限。
- 优化 Zone 列表、Zone 详情页和系统设置页面布局,使域名、路由和快捷创建流程更清晰。
- 补充 Zone 域名迁移与发布验证文档,便于升级前后核对配置快照与回滚策略。
2026-07-12 19:24:37 +08:00
ryan 83f126795d fix: 调整数据库自动清理设置文案 2026-07-12 19:07:56 +08:00
ryan 69467914fc fix(server): 修复 Agent 上报访问日志的 bytes_sent 在 Server 入库链路丢失,导致 Zone 概览“已提供的数据总计”长期为 0 的问题。 2026-07-12 19:02:52 +08:00
ryan c624512da6 fix(frontend): serve zone detail static export fallback 2026-07-12 18:51:01 +08:00
ryan 50717d1baf fix(frontend): support static export dynamic routes compliance via client useParams 2026-07-12 18:07:43 +08:00
ryan fc569d1758 fix(frontend): dynamically import zone dashboard with ssr: false to cure hydration mismatch
- Dynamically import `ZonePageClient` with `ssr: false` in `websites/[zoneId]/page.tsx`.
- Remove `generateStaticParams` to prevent dynamic paths from building with inconsistent SSG/ISR outputs.
- Remove redundant `mounted` check from `page-client.tsx` since dashboard is client-only.
- Add `bytes_sent` to `NodeAccessLog` on both Agent and Master Server.
- Create ClickHouse migration `202607120001_add_bytes_sent_to_node_access_logs.sql`.
- Refactor duplicate stats structs by centralizing them into `analyticsmodel` package with type aliases.
- Simplify access log store delegations and remove redundant mapping loops.
- Support full console menu display management in settings other-tab.
- Regenerate Swagger documentation.
- Update changelog index.md.
2026-07-12 17:52:40 +08:00
ryan ec4f1d4d23 feat(settings): support full console menu display management in settings other-tab
- Rebuild MENU_GROUPS in `other-tab.tsx` to include all 13 business console items with safety read-only constraints on Dashboard.
- Support reactive filtering in `OpenFlareSidebarMenu` using `menu_display_config` to hide items and empty collapsible groups.
- Fix React Hydration Error #418 when directly loading dynamic websites on SSR by wrapping client component with mounted state hook.
- Add `bytes_sent` to `NodeAccessLog` on both Agent and Master Server.
- Create ClickHouse migration `202607120001_add_bytes_sent_to_node_access_logs.sql`.
- Refactor duplicate stats structs by centralizing them into `analyticsmodel` package with type aliases.
- Simplify access log store delegations and remove redundant mapping loops.
- Regenerate Swagger documentation.
- Update changelog index.md.
2026-07-12 17:46:49 +08:00
ryan 9268acb84c feat(api): support traffic bytes tracking, refactor analytics models and fix website hydration
- Add `bytes_sent` to `NodeAccessLog` on both Agent and Master Server.
- Create ClickHouse migration `202607120001_add_bytes_sent_to_node_access_logs.sql`.
- Refactor duplicate stats structs by centralizing them into `analyticsmodel` package with type aliases.
- Simplify access log store delegations and remove redundant mapping loops.
- Fix React Hydration Error #418 when directly loading dynamic websites on SSR by wrapping client component with mounted state hook.
- Regenerate Swagger documentation.
- Update changelog index.md.
2026-07-12 17:42:19 +08:00
ryan 41cd23a64d feat(api): support traffic bytes tracking in edge access logs and refactor analytics models
- Add `bytes_sent` to `NodeAccessLog` on both Agent and Master Server.
- Create ClickHouse migration `202607120001_add_bytes_sent_to_node_access_logs.sql`.
- Refactor duplicate stats structs by centralizing them into `analyticsmodel` package with type aliases.
- Simplify access log store delegations and remove redundant mapping loops.
- Regenerate Swagger documentation.
- Update changelog index.md.
2026-07-12 17:27:57 +08:00
ryan f02fc9676a fix: resolve all build-test and code-check failures
- Fix Go backend mnd (magic number) and revive lint issues in zone stats.
- Remove unused React/Lucide imports and variables in zone overview.
- Add generateStaticParams and Suspense wrapper for websites/[zoneId] page to support Next.js static HTML export.
- Remove next/font/google dependency to allow fully offline frontend compilation.
- Fix hardcoded time dependency in ssl_renew_test.go.
- Fix async_tasks_test.go to respect minimum 90-day retention clamping in database auto-cleanup.
- Add Zone and ZoneDomain models to test database AutoMigrate schemas.
- Update integration tests to use the new zone_domain_ids route binding scheme.
2026-07-12 17:05:52 +08:00
ryan 31b4886a14 feat(zone): add Cloudflare-style traffic overview charts
Expose Zone stats API with multi-host access-log aggregates and time
series, and render unique visitors, requests and data served on the
Zone overview with 24h/7d/30d range controls.
2026-07-12 16:38:35 +08:00
ryan efcf61e32d feat(web): polish zone list and settings layout
Show Zone list as a table, align detail back navigation with proxy
route detail, and move edit/delete actions into the settings tab.
2026-07-12 16:22:16 +08:00
ryan d615d85a26 refactor: remove unused remarks and add quick domain create
Drop remark fields from Zone, Zone domains, proxy routes, WAF rule
groups and IP groups across models, APIs, UI and DB columns (keep
certificate/origin remarks). Add quick-create domain input for short
labels, @ apex and full FQDNs when binding domains.
2026-07-12 16:16:56 +08:00
ryan 8afd103751 fix(zone): register domain delete/update APIs and drop edit UI
补全 Zone 与 Zone 域名的 update/delete 路由与业务逻辑,修复删除域名
404;前端域名列表移除编辑入口,仅保留添加与删除。
2026-07-12 15:56:45 +08:00
ryan 7ef84cce52 feat(web): improve zone domain list and detail navigation
合并 Zone 域名与路由展示,上游地址多行显示并支持路由详情跳转;
域名列表对齐用户管理页样式;Zone 页支持 ?tab= 定位;反代详情返回
使用浏览器历史上一级。
2026-07-12 15:50:43 +08:00
ryan 96b8ddc077 fix(migrator): only import zone domains during upgrade window
Zone 历史导入仅在 goose 版本位于 [202607120002, 202607130001) 时执行,
phase-2 删列完成后日常启动不再进入导入逻辑。
2026-07-12 15:38:28 +08:00
ryan 03b81e5f74 refactor(migrator): use goose SQL only and auto-import zones on upgrade
移除 Go goose 迁移(bridge/legacy data/zone import),改为 goose SQL 占位
与结构迁移脚本;启动时 Migrate 在删旧列前自动导入历史域名,去掉
migrate-zones 手动命令及文档中的人工导入步骤。
2026-07-12 15:36:00 +08:00
ryan 5b52acdd6c refactor(zone): remove legacy route domain storage
第二阶段清理:删除 of_managed_domains 与 of_proxy_routes 冗余域名/证书列,
移除 ManagedDomain 模型与 API、路由侧 legacy 字段维护,以及前端 WebsiteService。
ImportLegacy 在旧列/旧表缺失时跳过对应源,保持幂等。
2026-07-12 15:31:01 +08:00
ryan fb3dd5afe6 docs(zone): add migration and release verification guide
补充 Zone 域名迁移操作指南(备份、migrate-zones、预览等价性、发布与回滚),
更新设计文档阶段说明、指南导航与 Unreleased 变更日志。
2026-07-12 15:31:01 +08:00
ryan 1160d5846a refactor(web): select route domains from zones
反代路由创建/域名配置改为绑定 zone_domain_ids,移除手写域名列表与
旧证书字段;列表与 WAF/UptimeKuma 消费端同步读取 zone_domains。
2026-07-12 15:23:48 +08:00
ryan 350b433cc1 feat(web): add zone-based website management
以稳定 Zone ID 替换旧托管域名详情页:列表展示根域与域名计数,详情提供
概览/域名/路由/证书/设置 Tabs,并补齐 ZoneService 与 vitest 行为测试。
Zone 列表 API 返回 domain_count;同步 Swagger 与重构计划进度。
2026-07-12 15:23:43 +08:00
ryan d4d9bad74d refactor(config): render routes from zone domains 2026-07-12 15:03:24 +08:00
ryan d0536fcdd5 refactor(proxy): bind routes through zone domains 2026-07-12 14:52:19 +08:00
ryan e51f1e583d chore: remove execution report artifact 2026-07-12 14:41:22 +08:00
ryan b835144cd0 merge: zone domain foundation 2026-07-12 14:41:04 +08:00
ryan 53c868e99b feat(zone): add zone management api and legacy importer 2026-07-12 14:35:53 +08:00
ryan 50678756d4 feat(zone): add normalized zone domain schema 2026-07-12 14:23:25 +08:00
ryan 3eb670c674 chore: ignore local worktrees 2026-07-12 14:17:07 +08:00
ryan d10132fb02 docs(plan): add zone domain refactor plan 2026-07-12 14:14:25 +08:00
ryan 61cf581621 docs(zone): clarify certificate ownership 2026-07-12 14:08:43 +08:00
ryan e2ac531abb docs(zone): define zone domain management model 2026-07-12 14:05:04 +08:00
ryan c8b1289043 fix(docker): preserve clickhouse listener config 2026-07-12 12:28:34 +08:00
ryan 13c5073bf8 chore(release): v3.1.2
### 🛠 修复
- 修复了节点与仪表盘 24 小时容量、网络、磁盘 IO 趋势在限流查询下几乎为空的问题,改为小时级聚合与计数器增量统计,历史时段可正常展示。
- 修复了静置场景下 ClickHouse CPU 偏高的问题,可观测与访问日志写入改为批量凑批并限制小 part 产生。
- 修复了数据清理接口将「物化表 TTL」误报为已删除行数的问题,短于表 TTL 的保留天数会被明确拒绝。
- 修复了可观测去重在入队/刷盘失败后仍占用键、导致故障窗口数据更易丢失的问题,并在 flush 失败时短重试与释放键。
- 修复了 Dashboard「每节点最新指标」被全局 LIMIT 截断导致安静节点缺失的问题,改为按节点取最新快照。
- 修复了 Docker 部署 ClickHouse 25.x 因后台池与 mutation 空闲阈值不兼容而无法启动的问题。
- 修复了小时预聚合仅有迁移后少量数据时 24 小时趋势再次残缺的问题:读路径按小时 merge(窗口完整走 rollup,缺口用 raw 补齐),并增加历史 backfill 迁移。

### ⚡️ 优化与改进
- 为容量与 OpenResty 指标增加小时预聚合表,窗口完整时优先走 rollup 降低查询压力。
- 审计日志写入增加最长等待刷盘,管理端可观测状态接口暴露 batch writer 队列深度、丢弃与 flush 错误指标。
- 小规格场景下调 ClickHouse 客户端连接池默认值,并调整 async_insert 合并超时与流量小时表 TTL。
- 流量独立访客在小时汇总中改为窗口峰值估计,并修正界面文案,避免被误解为全局真实 UV。

### 💄 其他/体验
- 同步环境变量与配置模板中的 ClickHouse 说明;部署文档改为将 performance.xml 下载到 ./config/clickhouse 后挂载,且不挂载 listen 配置。
2026-07-10 11:42:49 +08:00
ryan da1dd92404 fix(observability): merge rollup+raw hourly trends and backfill history
Prefer capacity/openresty rollups only when they cover the 24h window;
otherwise merge per hour so raw fills pre-MV gaps and rollup wins on
overlap. Add a one-time ANTI JOIN backfill migration for the last 30 days.
2026-07-10 11:27:51 +08:00
ryan 4b11279662 fix(observability): fall back to raw hourly when rollup is incomplete
Materialized capacity/openresty hourly tables only hold data after the MV
exists. Preferring any non-empty rollup hid full raw history and left 24h
charts with only recent hours. Use rollup only when its earliest bucket
covers the query window start.
2026-07-10 11:27:51 +08:00
ryan bbadcca294 ### 🛠 修复
- 修复了节点与仪表盘 24 小时容量、网络、磁盘 IO 趋势在限流查询下几乎为空的问题,改为小时级聚合与计数器增量统计,历史时段可正常展示。
- 修复了静置场景下 ClickHouse CPU 偏高的问题,可观测与访问日志写入改为批量凑批并限制小 part 产生。
- 修复了数据清理接口将「物化表 TTL」误报为已删除行数的问题,短于表 TTL 的保留天数会被明确拒绝。
- 修复了可观测去重在入队/刷盘失败后仍占用键、导致故障窗口数据更易丢失的问题,并在 flush 失败时短重试与释放键。
- 修复了 Dashboard「每节点最新指标」被全局 LIMIT 截断导致安静节点缺失的问题,改为按节点取最新快照。
- 修复了 Docker 部署 ClickHouse 25.x 因后台池与 mutation 空闲阈值不兼容而无法启动的问题。

### ⚡️ 优化与改进
- 为容量与 OpenResty 指标增加小时预聚合表,读路径优先 rollup,降低 24 小时趋势查询压力。
- 审计日志写入增加最长等待刷盘,管理端可观测状态接口暴露 batch writer 队列深度、丢弃与 flush 错误指标。
- 小规格场景下调 ClickHouse 客户端连接池默认值,并调整 async_insert 合并超时与流量小时表 TTL。
- 流量独立访客在小时汇总中改为窗口峰值估计,并修正界面文案,避免被误解为全局真实 UV。

### 💄 其他/体验
- 同步环境变量与配置模板中的 ClickHouse 说明;部署文档改为将 performance.xml 下载到 ./config/clickhouse 后挂载,且不挂载 listen 配置。
2026-07-10 11:27:51 +08:00
ryan 44ce6497a1 docs(docker): use ./config/clickhouse for CH performance mount
Move performance.xml to config/clickhouse and mount the directory to
/etc/clickhouse-server/config.d; update docs and compose paths.
2026-07-10 11:10:32 +08:00
ryan 4b83f91b31 docs(docker): use ./config/clickhouse for CH performance mount
Move performance.xml to config/clickhouse and mount the directory to
/etc/clickhouse-server/config.d; update docs and compose paths.
2026-07-10 10:59:21 +08:00
ryan 9d2fac5d4c docs(config): sync .env.example and config.example.yaml for CH defaults
Align ClickHouse pool/password placeholders and docker-compose env docs with
runtime defaults and published ports used in local Docker testing.
2026-07-10 10:48:30 +08:00
ryan b4b93ff4ed fix(docker): make ClickHouse startable on 25.x and reachable from host
Lower merge-tree free-entry thresholds for small background pools, bind
listen_host to 0.0.0.0 for published ports, and allow CLICKHOUSE_ENABLED=true
in tests for live_ch smoke coverage.
2026-07-10 10:44:49 +08:00
ryan 160e63558f fix(clickhouse): harden R/W path P0–P3 (cleanup, durability, rollups)
Honest TTL cleanup semantics; enqueue-safe dedup with flush retry and writer
metrics; model insert hooks; latest-per-node and hourly metric/openresty
rollups; small-host pool/async defaults, traffic hourly TTL, and UV labeling.
2026-07-10 10:34:04 +08:00
ryan 9b3555c569 fix(clickhouse): cut idle CPU from tiny parts and oversized merge pools
Observability writers flushed every few seconds with MinBatchSize unset,
creating constant small parts and merge load. Enable MinBatchSize with
MaxFlushWait, batch access logs more aggressively, and shrink ClickHouse
background pools for 3c hosts.
2026-07-10 10:08:32 +08:00
ryan b928928958 fix(observability): restore 24h capacity/network/disk trends via CH hourly agg
Node and dashboard 24h capacity, network, and disk IO charts only used the
latest limited raw snapshots (120/500 rows), so historical hour buckets stayed
empty. Prefer ClickHouse hourly aggregates with counter deltas, and fall back
to raw snapshots when aggregation is unavailable.
2026-07-10 09:48:45 +08:00
ryan b312460ddf chore(release): v3.1.1
### ⚡️ 优化与改进
- 将 cap_login_enabled 默认值由 true 变更为 false,默认关闭登录界面 PoW 人机验证。
2026-07-06 12:29:53 +08:00
ryan 50f7257d93 docs: readme 2026-07-05 23:53:26 +08:00
ryan 336185f01c release: v3.1.0
### 🛠 修复
- 修复 ClickHouse TTL 迁移中 DateTime64 时间列无法直接设置 TTL 导致 goose 启动失败的问题,改为通过 toDateTime() 转换后再应用 TTL。
- 修复 ClickHouse 迁移尝试缩短 ORDER BY 排序键时与隐式主键前缀冲突导致迁移失败的问题,移除不支持的 MODIFY ORDER BY 操作。
- 修复系统设置页面 URL tab 参数未包含 openflare-ops 选项卡导致无法正确定位的问题,并在无参数时默认选中 OpenFlare 选项卡。
- 修复系统自更新检测上游 GitHub Release 时,因资产包名称前缀 openflare-server 与仓库名不完全一致导致匹配失败并报错「未找到兼容的 Release」的问题。
- 修复全局搜索数据源覆盖不全的问题,补全所有核心业务控制台页面及管理员专有页面的检索支持。

### ⚡️ 优化与改进
- ClickHouse 启用 async_insert 异步写入缓冲,并调高 block_buffer_size 与连接池默认值,降低小 part 生成与连接争用。
- 优化 ClickHouse 写入路径:移除 Agent 心跳中的同步 ALTER DELETE 保留清理,batchwriter 新增 MinBatchSize 抑制过小批次定时 flush,可观测 writer 批次与 flush 间隔调优并补全去重。
- ClickHouse 分析表新增 TTL 自动过期策略,访问日志 180 天、节点访问日志 90 天、其余观测与聚合表 30 天自动清理。
- 访问日志与 WAF IP 组查询改为 ClickHouse 侧聚合与 SQL 分页,默认限制近 7 天查询窗口,浏览器分布查询增加 Top 100 限制。
- Dashboard 与节点可观测 API 消除无 LIMIT 全表扫描,增加短 TTL 内存缓存,前端轮询间隔分别调整为 60s/30s。
- ClickHouse 遗留治理 Phase 2:保留期清理改为 TTL MATERIALIZE TTL,统一 ChConn 读路径,新增 /admin/status/clickhouse 运维指标与 of_node_traffic_hourly 预聚合 MV。
- 审计访问日志写入时仅保留安全相关请求头并以 SHA-256 脱敏,将 headers 载荷上限收紧至 2KB,减小行宽与 merge CPU 开销。
- Docker 部署为 ClickHouse 增加 nofile ulimits 与性能配置挂载,限制 max_concurrent_queries 与后台合并争用。
- 数据库自动清理任务新增 OpenResty、FRPS、FRPC 观测表清理目标。

### 💄 其他/体验
- 隐藏侧边栏文档库中的「规范示例」与「接口文档」,将「使用文档」及其他相关页面链接统一跳转至外部文档站 https://open-flare.pages.dev/。
- 移除系统设置 OpenFlare 标签页下的版本信息卡片及对应升级管理弹窗逻辑。
- 系统设置页面支持通过 URL 持久化当前选中的 Tab 状态。
2026-07-04 09:43:31 +08:00
ryan 44bba0f19a fix(clickhouse): remove unsupported MODIFY ORDER BY from migration
ClickHouse keeps the implicit PRIMARY KEY when shortening ORDER BY,
which fails with "Primary key must be a prefix of the sorting key".
TTL-only changes are safe and unblock goose startup; narrowing ORDER BY
would require table recreation.
2026-07-02 16:45:48 +08:00
ryan f0eca028f9 fix(clickhouse): cast DateTime64 to DateTime in TTL migration 2026-07-02 16:21:30 +08:00
ryan 58624db397 perf(clickhouse): Phase 2 legacy governance — TTL cleanup, unified pool, MV, ops API
- Replace retention ALTER DELETE with MATERIALIZE TTL; use TRUNCATE for delete-all
- Remove GORM ClickHouse pool; migrate user access log reads to ChConn
- Drop query-side trim(remote_addr); enable wait_for_async_insert=1
- Add of_node_traffic_hourly MV and dashboard traffic trend fallback
- Add GET /admin/status/clickhouse operational metrics endpoint
2026-07-02 15:47:38 +08:00
ryan 38946d1af5 fix(clickhouse): resolve lint issues from optimization stack 2026-07-02 15:28:49 +08:00
ryan caf2ffcff4 perf(clickhouse): P1 TTL migrations, ORDER BY tune, remote_addr normalization 2026-07-02 15:25:03 +08:00
ryan 0e86fe3547 perf(clickhouse): P2 docker server tuning and audit log payload reduction 2026-07-02 15:23:17 +08:00
ryan 6525bef15d perf(clickhouse): P0/P1 access log and WAF query aggregation and SQL pagination 2026-07-02 15:23:17 +08:00
ryan 3e910f1961 perf(clickhouse): P0 dashboard/observability query limits, cache, slower polling 2026-07-02 15:23:17 +08:00
ryan 28c14eb054 perf(clickhouse): enable async_insert and tune connection/buffer defaults 2026-07-02 15:23:17 +08:00
ryan ae618905a3 perf(clickhouse): P0 write path — remove heartbeat DELETE, batchwriter MinBatchSize, tune chwriter 2026-07-02 15:23:17 +08:00
ryan 5ad151469c feat(frontend): delete unused version-upgrade-dialog component and use-openflare-server-upgrade hook
- Permanently delete version-upgrade-dialog.tsx and use-openflare-server-upgrade.ts as they are no longer referenced after removing the version info card from openflare-ops settings.
2026-06-30 21:04:52 +08:00
ryan 6467b32d8e fix(frontend): include openflare-ops tab in whitelist and make it default
- Include 'openflare-ops' in the list of validTabs so that specifying ?tab=openflare-ops correctly loads the OpenFlare settings tab.
- Set fallback tab default to 'openflare-ops' when no tab parameter is specified.
- Document changes in changelog.
2026-06-30 20:56:34 +08:00
ryan cf72420815 feat(frontend): persist selected tab on admin settings page 2026-06-30 20:53:33 +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 34225cb88a fix(updater): resolve release asset name matching for openflare-server
- Update expectedAssetNames helper to match lowercase repoName prefix and lowercase repoName with -server suffix (e.g. openflare-server).
- Fixes 'no compatible release found' error when checking GitHub Action releases.
2026-06-30 20:47:32 +08:00
ryan 389f02b6b0 feat(frontend): update navigation links and expand search coverage
- Hide 'Specification Examples' and 'API Docs' from sidebar documents group, pointing 'Use Docs' externally to pages.dev.
- Complete searchData array to cover all console business pages and missing admin-only pages.
- Add changelog records for these adjustments.
2026-06-30 20:36:55 +08:00
ryan 2fcbb945fb docs: update 2026-06-30 16:52:17 +08:00
ryan 23501259b2 docs: update 2026-06-30 16:42:41 +08:00
ryan c561e65cd3 docs: update 2026-06-30 16:38:53 +08:00
ryan 97095e8f12 release: v3.0.2
### 🛠 修复
- 修复 PostgreSQL 自增主键序列在历史数据迁移(INSERT 指定显式 ID)后与实际数据不同步的问题,通过新增全局序列同步脚本一键重置所有相关表的自增计数器。
2026-06-30 16:17:21 +08:00
ryan 23be2f9296 fix(db): 新增 PostgreSQL 数据库自增序列全局同步迁移脚本
为了解决因历史数据以显式 ID 方式迁移导致 PostgreSQL 自增序列计数器不同步,产生主键冲突唯一性约束报错(如 WAF 规则组和 IP 组保存失败)的问题,在 PostgreSQL 迁移中加入了对所有相关表 pg_get_serial_sequence 重置的代码。同时,在 SQLite 中补齐了对应的同名迁移文件。
2026-06-30 16:13:37 +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
2154 changed files with 251155 additions and 93350 deletions
-183
View File
@@ -1,183 +0,0 @@
---
name: go-code-review
description: Use when reviewing Go code or checking code against community style standards. Also use proactively before submitting a Go PR or when reviewing any Go code changes, even if the user doesn't explicitly request a style review. Does not cover language-specific syntax — delegates to specialized skills.
license: Apache-2.0
compatibility: Web server example in references uses slog (Go 1.21+)
metadata:
sources: "Go Wiki CodeReviewComments, Uber Style Guide"
allowed-tools: Bash(bash:*)
---
# Go 代码审查清单
## 审查流程
> 使用 `assets/review-template.md` 格式化代码审查输出,确保结构与"必须修复 / 建议修复 / 吹毛求疵"的严重程度分组保持一致。
1. 运行 `gofmt -d .` 和 `go vet ./...` 先捕获机械性问题
2. 逐文件阅读 diff;对于每个文件,按以下类别顺序检查
3. 标记问题时需要包含具体行号引用和规则名称
4. 审查完所有文件后,重新阅读标记项以确认它们是真实的问题
5. 按严重程度分组汇总发现(必须修复、建议修复、吹毛求疵)
> **验证**:完成审查后,再次阅读 diff 以验证每个标记的问题都是真实的。删除任何无法用具体行号引用的发现。
---
## 格式化
- [ ] **gofmt**:代码已使用 `gofmt` 或 `goimports` 格式化 → [go-linting](../go-linting/SKILL.md)
---
## 文档
- [ ] **注释句子**:注释是完整的句子,以被描述的名称开头,以句号结尾 → [go-documentation](../go-documentation/SKILL.md)
- [ ] **文档注释**:所有导出名称都有文档注释;非平凡的未导出声明也应有 → [go-documentation](../go-documentation/SKILL.md)
- [ ] **包注释**:包注释出现在 package 子句附近,无空行 → [go-documentation](../go-documentation/SKILL.md)
- [ ] **命名结果参数**:仅当它们能澄清含义时使用(例如,多个相同类型返回值),而不仅仅是为了启用裸返回 → [go-documentation](../go-documentation/SKILL.md)
---
## 错误处理
- [ ] **处理错误**:不使用 `_` 丢弃错误;处理、返回或(在特殊情况下)panic → [go-error-handling](../go-error-handling/SKILL.md)
- [ ] **错误字符串**:小写开头,无标点(除非以专有名词/首字母缩略词开头) → [go-error-handling](../go-error-handling/SKILL.md)
- [ ] **带内错误**:不使用魔术值(-1、""、nil);使用带 error 或 ok bool 的多返回值 → [go-error-handling](../go-error-handling/SKILL.md)
- [ ] **错误流缩进**:先处理错误并返回;保持正常路径的缩进最小化 → [go-error-handling](../go-error-handling/SKILL.md)
---
## 命名
- [ ] **MixedCaps**:使用 `MixedCaps` 或 `mixedCaps`,不使用下划线;未导出使用 `maxLength` 而非 `MAX_LENGTH` → [go-naming](../go-naming/SKILL.md)
- [ ] **首字母缩略词**:保持一致的大小写:`URL`/`url`、`ID`/`id`、`HTTP`/`http`(例如 `ServeHTTP`、`xmlHTTPRequest`) → [go-naming](../go-naming/SKILL.md)
- [ ] **变量名**:有限作用域用短名称(`i`、`r`、`c`);更广作用域用较长名称 → [go-naming](../go-naming/SKILL.md)
- [ ] **接收器名称**:类型的一两个字母缩写(`c` 代表 `Client`);不使用 `this`、`self`、`me`;各方法之间保持一致 → [go-naming](../go-naming/SKILL.md)
- [ ] **包名**:不重复(使用 `chubby.File` 而非 `chubby.ChubbyFile`);避免 `util`、`common`、`misc` → [go-packages](../go-packages/SKILL.md)
- [ ] **避免内置名称**:不遮蔽 `error`、`string`、`len`、`cap`、`append`、`copy`、`new`、`make` → [go-declarations](../go-declarations/SKILL.md)
---
## 并发
- [ ] **Goroutine 生命周期**:明确 goroutine 何时/是否退出;如不明显则添加文档 → [go-concurrency](../go-concurrency/SKILL.md)
- [ ] **同步函数**:优先同步而非异步;让调用者在需要时添加并发 → [go-concurrency](../go-concurrency/SKILL.md)
- [ ] **Context**:作为第一个参数;不放在 struct 中;不自定义 Context 类型;即使认为不需要也应传递 → [go-context](../go-context/SKILL.md)
---
## 接口
- [ ] **接口位置**:在消费方包中定义,而非实现方;生产者返回具体类型 → [go-interfaces](../go-interfaces/SKILL.md)
- [ ] **不提前定义接口**:不在使用前定义;不在实现方"为了 mock"而定义 → [go-interfaces](../go-interfaces/SKILL.md)
- [ ] **接收器类型**:如果会修改状态、有 sync 字段或体积大,使用指针;小的不可变类型使用值;不要混用 → [go-interfaces](../go-interfaces/SKILL.md)
---
## 数据结构
- [ ] **空切片**:优先使用 `var t []string`(nil)而非 `t := []string{}`(非 nil 零长度) → [go-data-structures](../go-data-structures/SKILL.md)
- [ ] **复制**:小心复制含指针/切片字段的结构体;不按值复制 `*T` 方法的接收器 → [go-data-structures](../go-data-structures/SKILL.md)
---
## 安全性
- [ ] **加密随机数**:密钥使用 `crypto/rand`,不使用 `math/rand` → [go-defensive](../go-defensive/SKILL.md)
- [ ] **不 panic**:常规错误处理使用 error 返回;仅在真正特殊的情况下 panic → [go-defensive](../go-defensive/SKILL.md)
---
## 声明与初始化
- [ ] **分组相似的**:相关的 `var`/`const`/`type` 放在括号块中;不相关的分开 → [go-declarations](../go-declarations/SKILL.md)
- [ ] **var vs :=**:有意使用零值时用 `var`;显式赋值时用 `:=` → [go-declarations](../go-declarations/SKILL.md)
- [ ] **缩小作用域**:将声明移到使用位置附近;使用 if-init 限制变量作用域 → [go-declarations](../go-declarations/SKILL.md)
- [ ] **Struct 初始化**:始终使用字段名;省略零值字段;零值 struct 使用 `var` → [go-declarations](../go-declarations/SKILL.md)
- [ ] **使用 `any`**:新代码中优先使用 `any` 而非 `interface{}` → [go-declarations](../go-declarations/SKILL.md)
---
## 函数
- [ ] **文件排序**:类型 → 构造函数 → 导出方法 → 未导出方法 → 工具函数 → [go-functions](../go-functions/SKILL.md)
- [ ] **签名格式化**:换行时所有参数各占一行并带尾逗号 → [go-functions](../go-functions/SKILL.md)
- [ ] **裸参数**:为含义不明确的 bool/int 参数添加 `/* name */` 注释,或使用自定义类型 → [go-functions](../go-functions/SKILL.md)
- [ ] **Printf 命名**:接受格式字符串的函数以 `f` 结尾,以便 `go vet` 检查 → [go-functions](../go-functions/SKILL.md)
---
## 风格
- [ ] **行长度**:无硬性限制,但避免令人不适的长行;按语义断行,而非任意长度 → [go-style-core](../go-style-core/SKILL.md)
- [ ] **裸返回**:仅在短函数中使用;中/大函数使用显式返回 → [go-style-core](../go-style-core/SKILL.md)
- [ ] **传值**:不要仅为节省字节而使用指针;小的固定大小类型传 `string` 而非 `*string` → [go-performance](../go-performance/SKILL.md)
- [ ] **字符串拼接**:简单拼接用 `+`;格式化用 `fmt.Sprintf`;循环中用 `strings.Builder` → [go-performance](../go-performance/SKILL.md)
---
## 日志
- [ ] **使用 slog**:新代码使用 `log/slog`,不使用 `log` 或 `fmt.Println` 进行运维日志记录 → [go-logging](../go-logging/SKILL.md)
- [ ] **结构化字段**:日志消息使用静态字符串加键值属性,不使用 fmt.Sprintf → [go-logging](../go-logging/SKILL.md)
- [ ] **适当的级别**:Debug 用于开发者追踪,Info 用于重要事件,Warn 用于可恢复的问题,Error 用于故障 → [go-logging](../go-logging/SKILL.md)
- [ ] **日志中无敏感信息**:PII、凭证和令牌永远不记录在日志中 → [go-logging](../go-logging/SKILL.md)
---
## 导入
- [ ] **导入分组**:标准库优先,然后空行,再外部包 → [go-packages](../go-packages/SKILL.md)
- [ ] **导入重命名**:除非冲突否则避免重命名;冲突时重命名本地/项目特定的导入 → [go-packages](../go-packages/SKILL.md)
- [ ] **空白导入**:`import _ "pkg"` 仅在 main 包或测试中使用 → [go-packages](../go-packages/SKILL.md)
- [ ] **点导入**:仅在测试中用于解决循环依赖 → [go-packages](../go-packages/SKILL.md)
---
## 泛型
- [ ] **何时使用**:仅当多个类型共享相同逻辑且接口不足时 → [go-generics](../go-generics/SKILL.md)
- [ ] **类型别名**:使用定义创建新类型;别名仅用于包迁移 → [go-generics](../go-generics/SKILL.md)
---
## 测试
- [ ] **示例**:包含可运行的 `Example` 函数或演示用法的测试 → [go-documentation](../go-documentation/SKILL.md)
- [ ] **有用的测试失败信息**:消息包含出了什么错、输入、实际值和期望值;顺序为 `got != want` → [go-testing](../go-testing/SKILL.md)
- [ ] **TestMain**:仅当所有测试都需要带清理的公共设置时使用;优先使用作用域化的 helper → [go-testing](../go-testing/SKILL.md)
- [ ] **真实传输**:优先使用 `httptest.NewServer` + 真实客户端而非 mock HTTP → [go-testing](../go-testing/SKILL.md)
---
## 自动化检查
运行自动化预审查检查:
```bash
bash scripts/pre-review.sh ./... # 文本输出
bash scripts/pre-review.sh --json ./... # 结构化 JSON 输出
```
或手动:`gofmt -l <path> && go vet ./... && golangci-lint run ./...`
在进入上述清单之前修复所有问题。有关 linter 设置和配置,请参阅 [go-linting](../go-linting/SKILL.md)。
---
## 综合示例
> 在构建生产级 HTTP 服务器并希望验证代码是否正确应用了并发、错误处理、context、文档和命名规范时,阅读 [references/WEB-SERVER.md](references/WEB-SERVER.md)。
---
## 相关 Skill
- **风格基础**:在解决格式化争议或应用"清晰 > 简单 > 简洁"优先级时,请参阅 [go-style-core](../go-style-core/SKILL.md)
- **Linting 设置**:在配置 golangci-lint 或将自动化检查添加到 CI 时,请参阅 [go-linting](../go-linting/SKILL.md)
- **错误策略**:在审查错误包装、哨兵错误或 handle-once 模式时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
- **命名规范**:在评估标识符名称、接收器名称或包-符号重复时,请参阅 [go-naming](../go-naming/SKILL.md)
- **测试模式**:在审查表驱动结构、失败消息或 helper 使用的测试代码时,请参阅 [go-testing](../go-testing/SKILL.md)
- **并发安全**:在审查 goroutine 生命周期、channel 使用或互斥锁放置时,请参阅 [go-concurrency](../go-concurrency/SKILL.md)
- **日志实践**:在审查日志使用、结构化日志或 slog 配置时,请参阅 [go-logging](../go-logging/SKILL.md)
@@ -1,23 +0,0 @@
# Code Review: [PR Title]
## Summary
[Brief description of the changes]
## Findings
### Must Fix
- [ ] [file:line] Description of critical issue
### Should Fix
- [ ] [file:line] Description of recommended improvement
### Nits
- [ ] [file:line] Description of minor suggestion
## Automated Checks
- [ ] `gofmt -d .` — clean
- [ ] `go vet ./...` — clean
- [ ] `golangci-lint run` — clean
## Skills Applied
[List of go-* skills referenced during review]
@@ -1,119 +0,0 @@
# Web 服务器:Skill 的综合应用
本示例展示 Go skill 如何在真实的 HTTP 服务器中协同应用。每个部分
引用相关的 skill 以获取详细指导。
## 结构
```go
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"time"
)
// --- 接口(go-interfaces) ---
// Store 定义了数据访问边界。定义在消费方包中,
// 而非实现方包中。
type Store interface {
GetUser(ctx context.Context, id string) (*User, error)
}
// --- 类型与构造函数(go-naming、go-declarations) ---
// Server 处理用户 API 的 HTTP 请求。
type Server struct {
store Store
router *http.ServeMux
}
// NewServer 使用给定的依赖创建 Server。
// 调用者必须调用 Shutdown 来释放资源。
func NewServer(store Store) *Server {
s := &Server{store: store}
s.router = http.NewServeMux()
s.router.HandleFunc("GET /users/{id}", s.handleGetUser)
return s
}
// --- 错误处理(go-error-handling) ---
// 领域错误作为哨兵 —— 使用 errors.Is 进行检查。
var ErrNotFound = errors.New("not found")
// --- HTTP 处理器(go-control-flow、go-context、go-error-handling) ---
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // go-context:从 request 派生
id := r.PathValue("id")
user, err := s.store.GetUser(ctx, id)
if err != nil {
if errors.Is(err, ErrNotFound) { // go-error-handling:errors.Is
http.Error(w, "user not found", http.StatusNotFound)
return // go-control-flow:提前返回
}
// HTTP 处理器是"记录或返回"规则的例外:在服务端记录详细信息,向客户端返回脱敏错误。
slog.Error("GetUser failed", "id", id, "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// --- 优雅关闭(go-concurrency、go-defensive) ---
func main() {
store := NewDBStore(os.Getenv("DATABASE_URL"))
srv := NewServer(store)
httpSrv := &http.Server{
Addr: ":8080",
Handler: srv.router,
ReadTimeout: 5 * time.Second, // go-defensive:使用 time.Duration
WriteTimeout: 10 * time.Second,
}
// go-concurrency:goroutine 生命周期清晰
go func() {
sigCh := make(chan os.Signal, 1) // go-concurrency:channel 大小为 1
signal.Notify(sigCh, os.Interrupt)
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() // go-defensive:defer 清理
httpSrv.Shutdown(ctx)
}()
slog.Info("starting server", "addr", httpSrv.Addr)
if err := httpSrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
slog.Error("server error", "err", err)
os.Exit(1) // go-packages:仅在 main 中退出
}
}
```
## 应用的 Skill
| 领域 | Skill | 演示内容 |
|------|-------|----------|
| 接口在消费方 | [go-interfaces](../../go-interfaces/SKILL.md) | `Store` 在使用处定义 |
| 命名 | [go-naming](../../go-naming/SKILL.md) | MixedCaps、接收器缩写、清晰的函数名 |
| 错误处理 | [go-error-handling](../../go-error-handling/SKILL.md) | 哨兵错误、`errors.Is`、记录或返回 |
| Context | [go-context](../../go-context/SKILL.md) | 从 request 派生,逐层传递 |
| 控制流 | [go-control-flow](../../go-control-flow/SKILL.md) | 错误情况的提前返回 |
| 并发 | [go-concurrency](../../go-concurrency/SKILL.md) | 清晰的 goroutine 生命周期、channel 大小 |
| 防御性 | [go-defensive](../../go-defensive/SKILL.md) | `defer cancel()`、`time.Duration`、优雅关闭 |
| 包管理 | [go-packages](../../go-packages/SKILL.md) | 仅在 `main()` 中退出 |
| 日志 | [go-error-handling](../../go-error-handling/SKILL.md) | 结构化 slog,错误只处理一次 |
@@ -1,246 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.0.0"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
$SCRIPT_NAME v$VERSION — Run automated pre-review checks on Go code
USAGE
bash $SCRIPT_NAME [options] [path]
DESCRIPTION
Runs gofmt, go vet, and golangci-lint against the target path and
reports any findings. Use before manual code review to catch
mechanical issues early.
Exits 0 if all checks pass, 1 if issues found, 2 on error.
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--force Run even if golangci-lint is not installed (skip it)
--limit N Max items reported per section (0 = unlimited, default: 0)
ARGUMENTS
path Package pattern to check (default: ./...)
EXAMPLES
bash $SCRIPT_NAME
bash $SCRIPT_NAME ./pkg/...
bash $SCRIPT_NAME --json ./cmd/server/...
bash $SCRIPT_NAME --force ./...
bash $SCRIPT_NAME --json --limit 10 ./...
EOF
}
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/}"
s="${s//$'\n'/\\n}"
printf '%s' "$s"
}
JSON_OUTPUT=false
FORCE=false
LIMIT=0
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
--json) JSON_OUTPUT=true; shift ;;
--force) FORCE=true; shift ;;
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) TARGET="$1"; shift ;;
esac
done
TARGET="${TARGET:-./...}"
if ! command -v go &>/dev/null; then
echo "error: go is not installed or not in PATH" >&2
exit 2
fi
if ! command -v gofmt &>/dev/null; then
echo "error: gofmt is not installed or not in PATH" >&2
exit 2
fi
GOFMT_STATUS="pass"
GOFMT_FINDINGS=()
GOFMT_DIR="${TARGET%%/...}"
GOFMT_DIR="${GOFMT_DIR:-.}"
UNFORMATTED=$(gofmt -l "$GOFMT_DIR" 2>&1) || true
if [[ -n "$UNFORMATTED" ]]; then
GOFMT_STATUS="fail"
while IFS= read -r f; do
[[ -n "$f" ]] && GOFMT_FINDINGS+=("$f")
done <<< "$UNFORMATTED"
fi
GOVET_STATUS="pass"
GOVET_OUTPUT=""
if ! GOVET_OUTPUT=$(go vet "$TARGET" 2>&1); then
GOVET_STATUS="fail"
fi
LINT_STATUS="skip"
LINT_OUTPUT=""
if command -v golangci-lint &>/dev/null; then
LINT_STATUS="pass"
if ! LINT_OUTPUT=$(golangci-lint run "$TARGET" 2>&1); then
LINT_STATUS="fail"
fi
elif ! $FORCE; then
echo "error: golangci-lint not installed (use --force to skip)" >&2
exit 2
fi
FAILED=0
[[ "$GOFMT_STATUS" == "fail" ]] && FAILED=1
[[ "$GOVET_STATUS" == "fail" ]] && FAILED=1
[[ "$LINT_STATUS" == "fail" ]] && FAILED=1
if $JSON_OUTPUT; then
GOFMT_TRUNCATED=false
GOFMT_DISPLAY=("${GOFMT_FINDINGS[@]+"${GOFMT_FINDINGS[@]}"}")
if [[ $LIMIT -gt 0 && ${#GOFMT_DISPLAY[@]} -gt $LIMIT ]]; then
GOFMT_DISPLAY=("${GOFMT_FINDINGS[@]:0:$LIMIT}")
GOFMT_TRUNCATED=true
fi
GOFMT_JSON="["
first=true
for f in "${GOFMT_DISPLAY[@]+"${GOFMT_DISPLAY[@]}"}"; do
$first || GOFMT_JSON+=","
first=false
GOFMT_JSON+="\"$(json_escape "$f")\""
done
GOFMT_JSON+="]"
GOVET_TRUNCATED=false
GOVET_DISPLAY="$GOVET_OUTPUT"
if [[ $LIMIT -gt 0 && -n "$GOVET_OUTPUT" ]]; then
GOVET_ARR=()
while IFS= read -r line; do
GOVET_ARR+=("$line")
done <<< "$GOVET_OUTPUT"
if [[ ${#GOVET_ARR[@]} -gt $LIMIT ]]; then
GOVET_DISPLAY=""
for (( i=0; i<LIMIT; i++ )); do
[[ -n "$GOVET_DISPLAY" ]] && GOVET_DISPLAY+=$'\n'
GOVET_DISPLAY+="${GOVET_ARR[$i]}"
done
GOVET_TRUNCATED=true
fi
fi
GOVET_ESC="$(json_escape "$GOVET_DISPLAY")"
LINT_TRUNCATED=false
LINT_DISPLAY="$LINT_OUTPUT"
if [[ $LIMIT -gt 0 && -n "$LINT_OUTPUT" ]]; then
LINT_ARR=()
while IFS= read -r line; do
LINT_ARR+=("$line")
done <<< "$LINT_OUTPUT"
if [[ ${#LINT_ARR[@]} -gt $LIMIT ]]; then
LINT_DISPLAY=""
for (( i=0; i<LIMIT; i++ )); do
[[ -n "$LINT_DISPLAY" ]] && LINT_DISPLAY+=$'\n'
LINT_DISPLAY+="${LINT_ARR[$i]}"
done
LINT_TRUNCATED=true
fi
fi
LINT_ESC="$(json_escape "$LINT_DISPLAY")"
GOFMT_TRUNC=""
$GOFMT_TRUNCATED && GOFMT_TRUNC=',"truncated":true'
GOVET_TRUNC=""
$GOVET_TRUNCATED && GOVET_TRUNC=',"truncated":true'
LINT_TRUNC=""
$LINT_TRUNCATED && LINT_TRUNC=',"truncated":true'
cat <<EOF
{"gofmt":{"status":"$GOFMT_STATUS","files":$GOFMT_JSON$GOFMT_TRUNC},"govet":{"status":"$GOVET_STATUS","output":"$GOVET_ESC"$GOVET_TRUNC},"golangci_lint":{"status":"$LINT_STATUS","output":"$LINT_ESC"$LINT_TRUNC},"passed":$( [[ $FAILED -eq 0 ]] && echo true || echo false )}
EOF
else
echo "=== gofmt ==="
if [[ "$GOFMT_STATUS" == "fail" ]]; then
echo "Unformatted files:"
GOFMT_COUNT=0
for f in "${GOFMT_FINDINGS[@]}"; do
GOFMT_COUNT=$((GOFMT_COUNT + 1))
if [[ $LIMIT -gt 0 && $GOFMT_COUNT -gt $LIMIT ]]; then
echo " ... ($(( ${#GOFMT_FINDINGS[@]} - LIMIT )) more items truncated)"
break
fi
echo " $f"
done
else
echo "OK"
fi
echo ""
echo "=== go vet ==="
if [[ "$GOVET_STATUS" == "fail" ]]; then
if [[ $LIMIT -gt 0 ]]; then
GOVET_ARR=()
while IFS= read -r line; do
GOVET_ARR+=("$line")
done <<< "$GOVET_OUTPUT"
for (( i=0; i<${#GOVET_ARR[@]} && i<LIMIT; i++ )); do
echo "${GOVET_ARR[$i]}"
done
if [[ ${#GOVET_ARR[@]} -gt $LIMIT ]]; then
echo "... ($(( ${#GOVET_ARR[@]} - LIMIT )) more items truncated)"
fi
else
echo "$GOVET_OUTPUT"
fi
else
echo "OK"
fi
echo ""
echo "=== golangci-lint ==="
if [[ "$LINT_STATUS" == "skip" ]]; then
echo "Skipped (not installed)"
elif [[ "$LINT_STATUS" == "fail" ]]; then
if [[ $LIMIT -gt 0 ]]; then
LINT_ARR=()
while IFS= read -r line; do
LINT_ARR+=("$line")
done <<< "$LINT_OUTPUT"
for (( i=0; i<${#LINT_ARR[@]} && i<LIMIT; i++ )); do
echo "${LINT_ARR[$i]}"
done
if [[ ${#LINT_ARR[@]} -gt $LIMIT ]]; then
echo "... ($(( ${#LINT_ARR[@]} - LIMIT )) more items truncated)"
fi
else
echo "$LINT_OUTPUT"
fi
else
echo "OK"
fi
echo ""
if [[ $FAILED -eq 1 ]]; then
echo "Pre-review checks FAILED — fix issues before manual review."
else
echo "All pre-review checks passed."
fi
fi
exit $FAILED
-191
View File
@@ -1,191 +0,0 @@
---
name: go-concurrency
description: Use when writing concurrent Go code — goroutines, channels, mutexes, or thread-safety guarantees. Also use when parallelizing work, fixing data races, or protecting shared state, even if the user doesn't explicitly mention concurrency primitives. Does not cover context.Context patterns (see go-context).
license: Apache-2.0
compatibility: Requires go.uber.org/atomic for atomic operation wrappers
metadata:
sources: "Effective Go, Google Style Guide, Uber Style Guide"
---
# Go 并发
## Goroutine 生命周期
> **规范**:当你启动 goroutine 时,要明确它们何时或是否退出。
Goroutine 可能因阻塞在 channel 的发送/接收上而泄漏。GC **不会终止**被阻塞的 goroutine,即使没有其他 goroutine 持有对该 channel 的引用。即使不泄漏的在途 goroutine 也会导致 panic(在已关闭的 channel 上发送)、数据竞争、内存问题和资源泄漏。
### 核心规则
1. **每个 goroutine 都需要停止机制** —— 可预测的结束时间、取消信号,或两者兼有
2. **代码必须能够等待** goroutine 完成
3. **不在 `init()` 中启动 goroutine** —— 改为暴露生命周期方法(`Close`、`Stop`、`Shutdown`)
4. **保持同步作用域化** —— 限制在函数作用域内,将逻辑分解为同步函数
```go
// 好:使用 WaitGroup 明确生命周期
var wg sync.WaitGroup
for item := range queue {
wg.Add(1)
go func() { defer wg.Done(); process(ctx, item) }()
}
wg.Wait()
```
```go
// 不好:无法停止或等待
go func() { for { flush(); time.Sleep(delay) } }()
```
使用 [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) **检测泄漏**。
> **原则**:永远不要在不知道 goroutine 将如何停止的情况下启动它。
> 在实现 stop/done channel 模式、goroutine 等待策略或
> 生命周期管理的 worker 时,阅读 [references/GOROUTINE-PATTERNS.md](references/GOROUTINE-PATTERNS.md)。
---
## 通过通信共享
> "不要通过共享内存来通信;而是通过通信来共享内存。"
这是 Go 并发设计的基础原则。使用 **channel** 进行所有权转移和协调 —— 当一个 goroutine 生产值,另一个消费它时使用。当多个 goroutine 访问共享状态且 channel 会增加不必要的复杂性时,使用 **互斥锁**。
**默认使用 channel。** 当问题本质上是保护共享数据结构(例如缓存或计数器)而非在 goroutine 之间传递数据时,退回到 `sync.Mutex` / `sync.RWMutex`。
---
## 同步函数
> **规范**:优先使用同步函数而非异步函数。
| 优势 | 原因 |
|---|---|
| 局部化 goroutine | 生命周期更容易推理 |
| 避免泄漏和竞争 | 更容易防止资源泄漏和数据竞争 |
| 更容易测试 | 直接检查输入/输出,无需轮询 |
| 调用方灵活性 | 调用方在需要时添加并发 |
> **建议**:在调用方移除不必要的并发是相当困难的(有时是不可能的)。让调用方在需要时添加并发。
> 在编写同步优先的 API(调用方可以将其包装在 goroutine 中)时,
> 阅读 [references/GOROUTINE-PATTERNS.md](references/GOROUTINE-PATTERNS.md)。
---
## 零值互斥锁
`sync.Mutex` 和 `sync.RWMutex` 的零值是有效的 —— 几乎不需要互斥锁的指针。
```go
// 好:零值有效 // 不好:不必要的指针
var mu sync.Mutex mu := new(sync.Mutex)
```
**不要嵌入互斥锁** —— 使用命名的 `mu` 字段,使 `Lock`/`Unlock` 保持为实现细节,而非导出的 API。
> 在实现互斥锁保护的 struct 或决定如何组织互斥锁字段时,
> 阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
---
## Channel 方向
> **规范**:尽可能指定 channel 方向。
方向可以防止错误(编译器会捕获对仅接收 channel 的关闭操作),传达所有权,并且具有自文档化效果。
```go
func produce(out chan<- int) { /* 仅发送 */ }
func consume(in <-chan int) { /* 仅接收 */ }
func transform(in <-chan int, out chan<- int) { /* 双向 */ }
```
### Channel 大小:一或零
Channel 的大小应为 **零**(无缓冲)或 **一**。其他任何大小都需要给出理由:
- 大小是如何确定的
- 什么机制防止 channel 在负载下填满
- 当写入者阻塞时会发生什么
```go
c := make(chan int) // 无缓冲 —— 好
c := make(chan int, 1) // 大小为 1 —— 好
c := make(chan int, 64) // 任意大小 —— 需要给出理由
```
> 在审查详细的 channel 方向示例及易出错模式时,
> 阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
---
## 原子操作
使用 `atomic.Bool`、`atomic.Int64` 等(Go 1.19 起标准库 `sync/atomic` 提供,或 [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic))进行类型安全的原子操作。原始的 `int32`/`int64` 字段容易在某些代码路径上忘记原子访问。
```go
// 好:类型安全 // 不好:容易忘记
var running atomic.Bool var running int32 // 原子操作
running.Store(true) atomic.StoreInt32(&running, 1)
running.Load() running == 1 // 竞争!
```
> 在 sync/atomic 和 go.uber.org/atomic 之间选择,或在 struct 中实现原子
> 状态标志时,阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
---
## 并发文档
> **建议**:当线程安全性从操作类型不明显时,添加文档说明。
Go 用户假设只读操作可以安全地并发使用,而修改操作则不行。在以下情况添加并发文档:
1. **读取与修改不明确** —— 例如,会修改 LRU 状态的 `Lookup`
2. **API 提供同步** —— 例如,线程安全的客户端
3. **接口有并发要求** —— 在类型定义中添加文档
---
## Context 使用
> 有关 context.Context 的指导(参数位置、struct 存储、自定义
> 类型、派生模式),请参阅专门的
> [go-context](../go-context/SKILL.md) skill。
---
## 使用 Channel 的缓冲池
使用有缓冲 channel 作为空闲列表来复用已分配的缓冲区。这种"泄漏缓冲"模式使用带 `default` 的 `select` 进行非阻塞操作。
> 在实现带可复用缓冲区的 worker pool 或在基于 channel 的池和
> `sync.Pool` 之间选择时,阅读 [references/BUFFER-POOLING.md](references/BUFFER-POOLING.md)。
---
## 高级模式
> 在实现使用 channel 的 channel 进行请求-响应多路复用,或
> 跨核心的 CPU 密集型并行计算时,阅读 [references/ADVANCED-PATTERNS.md](references/ADVANCED-PATTERNS.md)。
---
## 相关 Skill
- **Context 传播**:在通过 goroutine 传递取消、截止时间或请求作用域值时,请参阅 [go-context](../go-context/SKILL.md)
- **错误处理**:在从 goroutine 传播错误或使用 errgroup 时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
- **防御性加固**:在 API 边界保护共享状态或使用 defer 清理时,请参阅 [go-defensive](../go-defensive/SKILL.md)
- **接口设计**:在为包含 sync 原语的类型选择接收器类型时,请参阅 [go-interfaces](../go-interfaces/SKILL.md)
### 外部资源
- [永远不要在不知道 goroutine 将如何停止的情况下启动它](https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop)
—— Dave Cheney
- [重新思考经典并发模式](https://www.youtube.com/watch?v=5zXAHh5tJqQ) —— Bryan Mills
(GopherCon 2018)
- [Go 程序何时结束](https://changelog.com/gotime/165) —— Go Time 播客
- [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) —— 用于测试的 Goroutine 泄漏检测器
- [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic) —— 类型安全的原子操作
@@ -1,132 +0,0 @@
# 高级并发模式
来自 Effective Go 的高级并发模式详细参考。这些模式适用于特定场景 —— 在需要请求/响应多路复用或 CPU 密集型并行化时使用。
---
## Channel 的 Channel
> **来源**:Effective Go
Channel 是一等公民值,可以像其他值一样被分配和传递。一个强大的模式是在请求结构体中嵌入 **回复 channel**,让每个客户端提供自己的应答路径:
```go
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
```
客户端发送一个包含函数、参数和接收结果 channel 的请求:
```go
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
clientRequests <- request
fmt.Printf("answer: %d\n", <-request.resultChan)
```
服务端处理器从队列中读取请求,并将结果发送回每个请求的回复 channel:
```go
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
```
这个模式构成了限速、并行、非阻塞 RPC 系统的基础,无需任何互斥锁。
---
## CPU 密集型并行化
> **来源**:Effective Go(现代化版本)
当计算可以分解为独立的部分时,使用 `sync.WaitGroup` 等待完成,将其并行化到多个 CPU 核心上:
```go
type Vector []float64
func (v Vector) DoSome(i, n int, u Vector) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
}
func (v Vector) DoAll(u Vector) {
numCPU := runtime.NumCPU()
var wg sync.WaitGroup
wg.Add(numCPU)
for i := 0; i < numCPU; i++ {
go func(i int) {
defer wg.Done()
v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u)
}(i)
}
wg.Wait()
}
```
使用 `runtime.NumCPU()` 获取硬件核心数,或使用 `runtime.GOMAXPROCS(0)` 以遵循用户的资源配置。
> **重要**:不要混淆并发(将程序组织为独立执行的组件)和并行(在多个 CPU 上同时执行计算)。Go 是一门并发语言;并非所有并行化问题都适合它的模型。
---
## 常见错误
### 忘记通知完成
如果 goroutine 从未调用 `wg.Done()`(或从未在 done channel 上发送),等待的 goroutine 将永远阻塞:
```go
// 不好:缺少 wg.Done —— 死锁
var wg sync.WaitGroup
wg.Add(1)
go func() {
doWork()
}()
wg.Wait()
// 好:始终 defer wg.Done
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
```
### 无限制的 goroutine 创建
为每个工作项无限制地启动 goroutine 可能会耗尽内存或压垮下游资源。使用信号量来限制并发数:
```go
// 不好:一次性创建 len(items) 个 goroutine
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(it Item) {
defer wg.Done()
process(it)
}(item)
}
wg.Wait()
// 好:信号量将并发限制为 maxWorkers
var wg sync.WaitGroup
sem := make(chan struct{}, maxWorkers)
for _, item := range items {
wg.Add(1)
sem <- struct{}{}
go func(it Item) {
defer wg.Done()
defer func() { <-sem }()
process(it)
}(item)
}
wg.Wait()
```
@@ -1,73 +0,0 @@
# 使用 Channel 的缓冲池
使用有缓冲 channel 作为空闲列表来复用已分配的缓冲区,避免重复分配。这种"泄漏缓冲"模式使用带 `default` 的 `select` 进行非阻塞操作。
> **来源**:Effective Go
```go
var freeList = make(chan *Buffer, 100) // 有缓冲 channel 作为空闲列表
// 客户端:从空闲列表获取缓冲区或分配新的
func getBuffer() *Buffer {
select {
case b := <-freeList:
return b // 复用已有缓冲区
default:
return new(Buffer) // 空闲列表为空;分配新缓冲区
}
}
// 服务端:如有空间则将缓冲区归还空闲列表,否则丢弃
func putBuffer(b *Buffer) {
b.Reset() // 为重用做准备
select {
case freeList <- b:
// 缓冲区已归还空闲列表
default:
// 空闲列表已满;丢弃缓冲区(GC 会回收)
}
}
```
## 工作原理
1. **非阻塞接收**:客户端尝试从 `freeList` 获取缓冲区。如果为空,`default` 分支运行并分配新缓冲区。
2. **非阻塞发送**:服务端尝试归还缓冲区。如果 `freeList` 已满,`default` 分支运行,缓冲区被丢弃等待垃圾回收。
3. **有限内存**:channel 容量(100)限制了池中缓冲区的数量,防止无限增长。
当分配成本较高且缓冲区复用有益,但你不希望在池空或池满时出现阻塞行为时,这种模式非常有用。
## 何时使用
- 高频分配相似大小的对象
- 分配开销影响性能的代码路径
- 需要限制内存使用量的场景
## 生产环境替代方案
对于生产代码,考虑使用 `sync.Pool`,它提供类似功能并与垃圾收集器有更好的集成:
```go
var bufferPool = sync.Pool{
New: func() any {
return new(Buffer)
},
}
func getBuffer() *Buffer {
return bufferPool.Get().(*Buffer)
}
func putBuffer(b *Buffer) {
b.Reset()
bufferPool.Put(b)
}
```
`sync.Pool` 的优势:
- 垃圾收集期间自动清理
- 无需管理池大小
- 天生线程安全
- 高并发下性能更好
基于 channel 的方式对于理解 Go 的并发原语以及需要更多控制池行为的场景仍然很有价值。
@@ -1,126 +0,0 @@
# Goroutine 生命周期模式
管理 goroutine 生命周期的详细模式 —— 确保每个 goroutine 都有清晰的启动/停止机制并防止资源泄漏。
---
## 使生命周期清晰
> WaitGroup 示例和作用域规则在父 skill(SKILL.md § Goroutine 生命周期,核心规则)中。本参考涵盖:stop/done channel 模式、等待策略、init() 生命周期示例和同步 API 设计。
---
## Stop/Done Channel 模式
每个 goroutine 必须有可预测的停止机制。使用 stop channel 通知关闭,使用 done channel 确认退出:
```go
var (
stop = make(chan struct{}) // 通知 goroutine 停止
done = make(chan struct{}) // 通知我们 goroutine 已退出
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// 关闭时:
close(stop) // 通知 goroutine 停止
<-done // 并等待它退出
```
在已关闭的 channel 上发送会 panic —— 始终使用 `close()` 来发信号,不要直接发送:
```go
ch := make(chan int)
close(ch)
ch <- 13 // panic: 在已关闭的 channel 上发送
```
---
## 等待 Goroutine
> 多 goroutine 的 `sync.WaitGroup` 模式在父 skill 中(SKILL.md § Goroutine 生命周期)。以下是单 goroutine 的 done-channel 替代方案。
为单个 goroutine 使用 done channel:
```go
done := make(chan struct{})
go func() {
defer close(done)
// 工作...
}()
<-done // 等待 goroutine 完成
```
---
## 不在 init() 中使用 Goroutine
> 核心规则在父 skill 中(SKILL.md § 核心规则,规则 3)。以下是展示生命周期管理的扩展示例。
```go
// 不好:创建了不可控的后台 goroutine
func init() {
go doWork()
}
```
```go
// 好:显式的生命周期管理
type Worker struct {
stop chan struct{}
done chan struct{}
}
func NewWorker() *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
}
go w.doWork()
return w
}
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}
```
---
## 优先使用同步函数
> 理由和优势表在父 skill 中(SKILL.md § 同步函数)。以下是具体的代码示例。
```go
// 好:同步函数 - 调用方控制并发
func ProcessItems(items []Item) ([]Result, error) {
var results []Result
for _, item := range items {
result, err := processItem(item)
if err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
// 调用方可以在需要时添加并发:
go func() {
results, err := ProcessItems(items)
// 处理结果
}()
```
@@ -1,110 +0,0 @@
# 同步原语模式
互斥锁和原子操作的详细模式 —— 涵盖互斥锁嵌入陷阱和类型安全的原子访问。
---
## 不要嵌入互斥锁
如果你通过指针使用结构体,互斥锁应该是非指针字段。不要在结构体中嵌入互斥锁,即使该结构体未被导出。
```go
// 不好:嵌入的互斥锁将 Lock/Unlock 暴露为 API 的一部分
type SMap struct {
sync.Mutex // Lock() 和 Unlock() 成为 SMap 的方法
data map[string]string
}
func (m *SMap) Get(k string) string {
m.Lock()
defer m.Unlock()
return m.data[k]
}
```
```go
// 好:命名字段使互斥锁保持为实现细节
type SMap struct {
mu sync.Mutex
data map[string]string
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
}
```
在不好的示例中,`Lock` 和 `Unlock` 方法无意中成为了导出 API 的一部分。在好的示例中,互斥锁是对调用方隐藏的实现细节。
---
## 原子操作:完整示例
标准 `sync/atomic` 包操作原始类型(`int32`、`int64` 等),容易忘记一致地使用原子操作。
```go
// 不好:容易忘记原子操作
type foo struct {
running int32 // 原子操作
}
func (f *foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
return // 已在运行
}
// 启动 Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // 竞争!忘记使用 atomic.LoadInt32
}
```
```go
// 好:类型安全的原子操作
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
return // 已在运行
}
// 启动 Foo
}
func (f *foo) isRunning() bool {
return f.running.Load() // 不可能意外地非原子读取
}
```
`atomic.Bool`、`atomic.Int64` 等类型(Go 1.19 起在标准库 `sync/atomic` 中可用,或通过 [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic))通过隐藏底层类型来增加类型安全性。
---
## Channel 方向示例
指定方向可以防止意外误用:
```go
// 好:指定方向 - 清晰的所有权
func sum(values <-chan int) int {
total := 0
for v := range values {
total += v
}
return total
}
```
```go
// 不好:未指定方向 - 允许意外误用
func sum(values chan int) (out int) {
for v := range values {
out += v
}
close(values) // 漏洞!这能通过编译但不应该发生。
}
```
-122
View File
@@ -1,122 +0,0 @@
---
name: go-context
description: 在 Go 中使用 context.Context 时使用 — 包括函数签名中的位置、传播取消和截止时间、以及在 context 中存储值与使用参数的对比。也适用于取消长时间运行的操作、设置超时或传递请求作用域数据,即使未直接提及 context.Context。不涵盖 goroutine 生命周期或 sync 原语(参见 go-concurrency)。
license: Apache-2.0
compatibility: 需要 Go 1.7+(context 在 Go 1.7 中移入标准库)
metadata:
sources: "Go Wiki CodeReviewComments"
---
# Go Context 用法
## Context 作为第一个参数
使用 Context 的函数应将其作为**第一个参数**:
```go
func F(ctx context.Context, /* 其他参数 */) error
func ProcessRequest(ctx context.Context, req *Request) (*Response, error)
```
这是 Go 中的一个强约定,使 context 的传递在代码库中可见且一致。
---
## 不要在结构体中存储 Context
不要在结构体类型中添加 Context 成员。相反,将 `ctx` 作为参数传递给每个需要它的方法:
```go
// 不好:Context 存储在结构体中
type Worker struct {
ctx context.Context // 不要这样做
}
// 好:Context 传递给方法
type Worker struct{ /* ... */ }
func (w *Worker) Process(ctx context.Context) error {
// Context 显式传递 — 生命周期清晰
}
```
**例外**:签名必须匹配标准库或第三方库中接口的方法可能需要变通处理。
---
## 不要创建自定义 Context 类型
不要创建自定义的 Context 类型或在函数签名中使用 `context.Context` 以外的接口:
```go
// 不好:自定义 context 类型
type MyContext interface {
context.Context
GetUserID() string
}
// 好:使用标准 context.Context 并提取值
func Process(ctx context.Context) error {
userID := GetUserID(ctx)
}
```
---
## 应用数据放在哪里
按以下优先级顺序考虑:
1. **函数参数** — 最明确且类型安全
2. **接收者** — 适用于属于该类型的数据
3. **全局变量** — 适用于真正的全局配置(谨慎使用)
4. **Context 值** — 仅用于请求作用域数据
Context 值适用于:
- 请求 ID 和追踪 ID
- 随请求流动的认证/授权信息
- 截止时间和取消信号
Context 值**不适用**于:
- 可选的函数参数
- 可以显式传递的数据
- 不随请求变化的配置
---
## 常见模式
> 在派生 context(WithTimeout、WithCancel、WithDeadline)、在循环或 HTTP 处理器中检查取消、使用带类型键的 context 值、或需要快速参考表时,阅读 [references/PATTERNS.md](references/PATTERNS.md)。
### 派生 Context
创建派生 context 后,始终立即 `defer cancel()`:
```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
```
### 检查取消
```go
select {
case <-ctx.Done():
return ctx.Err()
default:
// 执行工作
}
```
### Context 不可变性
Context 是不可变的 — 将同一个 `ctx` 传递给共享相同截止时间和取消信号的多个并发调用是安全的。
---
## 相关技能
- **Goroutine 协调**:在使用 context 进行 goroutine 取消、基于 select 的超时或 errgroup 时,参见 [go-concurrency](../go-concurrency/SKILL.md)
- **错误处理**:在决定如何包装或返回 `ctx.Err()` 取消错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **接口设计**:在设计接受 context 并结合接口的 API 时,参见 [go-interfaces](../go-interfaces/SKILL.md)
- **请求作用域日志**:在将 logger 注入 context 或将请求 ID 添加到结构化日志输出时,参见 [go-logging](../go-logging/SKILL.md)
@@ -1,227 +0,0 @@
# Context 模式
派生、检查和传播 `context.Context` 的常见模式。
---
## Context 不可变性
Context 是不可变的。将同一个 `ctx` 传递给共享相同截止时间、取消信号、凭据和父级追踪的多个调用是安全的:
```go
// 安全:同一个 context 传递给顺序调用
func ProcessBatch(ctx context.Context, items []Item) error {
for _, item := range items {
if err := process(ctx, item); err != nil {
return err
}
}
return nil
}
// 安全:同一个 context 传递给并发调用
func ProcessConcurrently(ctx context.Context, a, b *Data) error {
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return processA(ctx, a) })
g.Go(func() error { return processB(ctx, b) })
return g.Wait()
}
```
---
## 何时使用 context.Background()
仅在**从不特定于请求**的函数中使用 `context.Background()`:
```go
func main() {
ctx := context.Background()
if err := run(ctx); err != nil {
log.Fatal(err)
}
}
func startBackgroundWorker() {
ctx := context.Background()
go worker(ctx)
}
```
**默认传递 Context**,即使你认为不需要。只有在有充分理由说明传递 context 是错误做法时,才直接使用 `context.Background()`:
```go
func LoadConfig(ctx context.Context) (*Config, error) {
// 即使现在不使用 ctx,接受它可以在未来添加功能时
// 不需要修改 API
}
```
---
## 派生 Context
```go
// 添加超时 — 持续时间结束后触发取消
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// 添加取消 — 调用者控制何时取消
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// 添加截止时间 — 在指定的墙钟时间触发取消
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Hour))
defer cancel()
// 添加值(谨慎使用 — 仅用于请求作用域数据)
ctx = context.WithValue(ctx, requestIDKey, reqID)
```
创建派生 context 后,**始终立即 `defer cancel()`**。这确保即使函数提前返回,资源也会被释放。
### 嵌套派生
派生的 context 形成树状结构。取消父级会取消所有子级:
```go
func handleRequest(ctx context.Context) error {
// 整个请求的父级超时
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// 数据库调用的更短超时
dbCtx, dbCancel := context.WithTimeout(ctx, 5*time.Second)
defer dbCancel()
data, err := queryDB(dbCtx)
if err != nil {
return err
}
// 父级 context 的剩余时间适用于此处
return sendResponse(ctx, data)
}
```
---
## 检查取消
### 在长时间运行的循环中
```go
func LongRunningOperation(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// 执行工作
}
}
}
```
### 在高开销操作之前
在开始无法中断的工作之前检查取消:
```go
func ProcessItems(ctx context.Context, items []Item) error {
for _, item := range items {
if ctx.Err() != nil {
return ctx.Err()
}
if err := expensiveProcess(item); err != nil {
return err
}
}
return nil
}
```
### 区分取消原因
```go
if err := ctx.Err(); err != nil {
switch {
case errors.Is(err, context.Canceled):
// 调用者显式取消(例如客户端断开连接)
case errors.Is(err, context.DeadlineExceeded):
// 超时或截止时间已过
}
}
```
---
## 在 HTTP 处理器中遵守取消
```go
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := slowOperation(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
// 客户端已断开连接 — 无需写入
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
```
`r.Context()` 在以下情况被取消:
- 客户端关闭连接
- `http.Server` 的 `ReadTimeout` 或 `WriteTimeout` 触发
- `ServeHTTP` 方法返回
---
## Context 值的最佳实践
### 使用未导出的键类型
```go
type contextKey struct{}
var userIDKey contextKey
func WithUserID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
id, ok := ctx.Value(userIDKey).(string)
return id, ok
}
```
使用未导出的结构体类型作为键可以防止与其他包的键发生冲突 — 即使它们使用相同的 string 或 int 值。
### 提供访问器函数
始终将 `context.WithValue` 和 `ctx.Value` 包装在类型化的辅助函数中(如上所示),而不是暴露键。这提供了类型安全性和一个可以修改实现的单一位置。
---
## 快速参考
| 模式 | 指导 |
|------|------|
| 参数位置 | 始终第一个:`func F(ctx context.Context, ...)` |
| 结构体存储 | 不要存储在结构体中;传递给方法 |
| 自定义类型 | 不要创建;使用 `context.Context` 接口 |
| 应用数据 | 优先选择 参数 > 接收者 > 全局变量 > context 值 |
| 请求作用域数据 | 适用于 context 值 |
| 共享 context | 安全 — context 是不可变的 |
| `context.Background()` | 仅用于非请求特定的代码 |
| 默认行为 | 即使认为不需要也要传递 context |
| `defer cancel()` | 在 `WithTimeout`/`WithCancel`/`WithDeadline` 之后始终立即 defer |
| 值键 | 使用未导出的结构体类型,提供访问器函数 |
| 取消检查 | 在高开销操作前使用 `ctx.Err()`;在循环中使用 `select` 监听 `ctx.Done()` |
-193
View File
@@ -1,193 +0,0 @@
---
name: go-control-flow
description: Use when writing conditionals, loops, or switch statements in Go — including if with initialization, early returns, for loop forms, range, switch, type switches, and blank identifier patterns. Also use when writing a simple if/else or for loop, even if the user doesn't mention guard clauses or variable scoping. Does not cover error flow patterns (see go-error-handling).
license: Apache-2.0
metadata:
sources: "Effective Go, Google Style Guide"
---
# Go 控制流
> 在使用 switch 语句、类型 switch 或带标签的 break 时,阅读 [references/SWITCH-PATTERNS.md](references/SWITCH-PATTERNS.md)
> 在使用 `_`、空白标识符导入或编译时接口检查时,阅读 [references/BLANK-IDENTIFIER.md](references/BLANK-IDENTIFIER.md)
---
## 带初始化的 If
`if` 和 `switch` 接受可选的初始化语句。使用它将变量限定在条件块作用域内:
```go
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
```
如果需要在 `if` 之后超出几行的范围使用该变量,请单独声明并使用标准 `if`:
```go
x, err := f()
if err != nil {
return err
}
// 大量使用 x 的代码
```
## 缩进错误流(守卫子句)
当 `if` 主体以 `break`、`continue`、`goto` 或 `return` 结尾时,省略不必要的 `else`。保持成功路径不缩进:
```go
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
```
当 `if` 已经返回时,绝不要将正常流程埋在 `else` 中。
---
## 重新声明和重新赋值
`:=` 短声明允许在同一作用域中重新声明变量:
```go
f, err := os.Open(name) // 声明 f 和 err
d, err := f.Stat() // 声明 d,重新赋值 err
```
变量 `v` 即使已经声明过,也可以出现在 `:=` 声明中,前提是:
1. 声明在与现有 `v` **相同的作用域**中
2. 值**可赋值**给 `v`
3. 声明中至少创建了**一个其他新变量**
### 变量遮蔽
**警告**:如果 `v` 在外层作用域中声明,`:=` 会创建一个**新的**遮蔽变量 — 这是常见的 bug 来源:
```go
// Bug:if 块内的 ctx 遮蔽了外层的 ctx
if *shortenDeadlines {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
}
// 此处的 ctx 仍然是原始的 — 被遮蔽的 ctx 没有逃逸
// 修复:使用 = 而不是 :=
var cancel func()
ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
```
---
## For 循环
Go 的 `for` 是唯一的循环结构,统一了 `while`、`do-while` 和 C 风格的 `for`:
```go
// 仅条件(Go 的 "while")
for x > 0 {
x = process(x)
}
// 无限循环
for {
if done() { break }
}
// C 风格的三组件形式
for i := 0; i < n; i++ { ... }
```
### Range
`range` 遍历切片、映射、字符串和通道:
```go
for i, v := range slice { ... } // 索引 + 值
for k, v := range myMap { ... } // 键 + 值(顺序不确定)
for i, r := range "héllo" { ... } // 字节索引 + rune(不是字节)
for v := range ch { ... } // 接收直到通道关闭
```
**关键规则:**
- 对字符串 range 产生 **rune**,不是字节 — `i` 是字节偏移量
- 对映射 range 的顺序**不确定** — 不要依赖它
- 使用 `_` 丢弃索引或值:`for _, v := range slice`
### 并行赋值
Go 没有逗号运算符。使用并行赋值处理多个循环变量:
```go
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
```
`++` 和 `--` 是语句,不是表达式 — 它们不能出现在并行赋值中。
---
## Switch:带标签的 Break
`for` 循环内 `switch` 中的 `break` 只会中断 switch。使用带标签的 `break` 退出外层循环:
```go
Loop:
for _, v := range items {
switch v.Type {
case "done":
break Loop // 中断 for 循环
}
}
```
关于类型 switch,参见 **go-interfaces**:类型 Switch。
---
## 空白标识符
**绝不要随意丢弃错误** — 空指针解引用 panic 可能随之而来。
在编译时验证接口实现:`var _ io.Writer = (*MyType)(nil)`。
参见 **go-interfaces** 中的接口满足检查模式。
---
## 快速参考
| 模式 | Go 惯用法 |
|------|-----------|
| If 初始化 | `if err := f(); err != nil { }` |
| 提前返回 | 当 if 主体返回时省略 `else` |
| 重新声明 | `:=` 在相同作用域 + 新变量时重新赋值 |
| 遮蔽陷阱 | `:=` 在内层作用域创建新变量 |
| 并行赋值 | `i, j = i+1, j-1` |
| 无表达式 switch | `switch { case cond: }` |
| 逗号 case | `case 'a', 'b', 'c':` |
| 无 fallthrough | 默认行为(需要时显式使用 `fallthrough`) |
| 从 switch 中跳出循环 | `break Label` |
| 丢弃值 | `_, err := f()` |
| 副作用导入 | `import _ "pkg"` |
| 接口检查 | `var _ Interface = (*Type)(nil)` |
---
## 相关技能
- **错误流程**:在构建守卫子句、提前返回或错误优先模式时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **类型 switch**:在使用类型 switch、comma-ok 惯用法或接口满足检查时,参见 [go-interfaces](../go-interfaces/SKILL.md)
- **减少嵌套**:在减少嵌套深度或解决格式问题时,参见 [go-style-core](../go-style-core/SKILL.md)
- **变量作用域**:在使用 if 初始化、`:=` 重新声明或减少变量作用域时,参见 [go-declarations](../go-declarations/SKILL.md)
@@ -1,71 +0,0 @@
# 空白标识符模式
空白标识符 `_` 在 Go 中有多种用途:丢弃不需要的值、为副作用导入包、以及在编译时验证接口实现。
---
## 多重赋值
使用 `_` 丢弃多值表达式中不需要的值:
```go
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
```
### 绝不要随意丢弃错误
静默丢弃错误会引发空指针 panic:
```go
// 不好:忽略错误会在路径不存在时崩溃
fi, _ := os.Stat(path)
if fi.IsDir() { ... } // 空指针解引用
```
如果确实不需要错误,请记录原因:
```go
_ = logger.Sync() // 尽力刷新;错误不可操作
```
---
## 副作用导入
使用空白标识符仅为了 `init()` 副作用而导入包:
```go
import _ "net/http/pprof" // 注册 HTTP 处理器
import _ "image/png" // 注册 PNG 解码器
```
这通常用于注册驱动、编解码器或调试处理器,它们在 `init()` 期间将自己注册到注册表中。
---
## 接口实现检查
在编译时验证类型是否实现了接口,方法是将 nil 指针赋值给接口类型的空白标识符变量:
```go
var _ io.Writer = (*MyType)(nil)
```
如果 `*MyType` 不满足 `io.Writer`,这会产生编译错误,在运行时之前捕获缺失的方法。
**何时使用**:将此检查放在定义该类型的同一文件中,通常在类型声明之后。当类型必须满足另一个包中定义的接口时特别有用。
参见 [go-interfaces](../../go-interfaces/SKILL.md):接口满足检查,获取关于何时何地使用此模式的完整指导。
---
## 快速参考
| 模式 | 语法 |
|------|------|
| 丢弃值 | `_, err := f()` |
| 在 if 初始化中丢弃 | `if _, err := f(); err != nil { }` |
| 副作用导入 | `import _ "pkg"` |
| 接口检查 | `var _ Interface = (*Type)(nil)` |
@@ -1,109 +0,0 @@
# Switch 模式
Go `switch` 语句的详细模式,包括无表达式 switch、逗号 case、break 行为和带标签的 break。
---
## 无自动 Fallthrough
Go `switch` 的 case 默认**不会** fall through(与 C/Java 不同)。每个 case 主体隐式地 break。仅在明确需要时使用 `fallthrough` — 这在惯用 Go 中很少见。
```go
switch n {
case 1:
fmt.Println("one")
// 无 fallthrough — 下一个 case 不会执行
case 2:
fmt.Println("two")
}
```
---
## 无表达式 Switch
没有表达式的 `switch` 对 `true` 进行 switch。在将单个变量与多个条件进行比较时,用它来替代 if-else-if 链:
```go
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
```
---
## 逗号分隔的 Case
多个值可以使用逗号共享一个 case 主体 — 不需要 `fallthrough`:
```go
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
```
---
## 带标签的 Break
`switch` 中的 `break` 仅终止 switch,**不会**终止外层的 `for` 循环。使用标签来跳出循环:
```go
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
break // 仅中断 switch
case src[n] < sizeTwo:
if n+1 >= len(src) {
break Loop // 跳出 for 循环
}
}
}
```
另一个常见模式 — 从 switch 内部中断 range 循环:
```go
Loop:
for _, v := range items {
switch v.Type {
case "done":
break Loop // 中断 for 循环
case "skip":
break // 仅中断 switch
}
}
```
**经验法则**:当 `for` 循环内有 `switch` 且需要从 case 中退出循环时,始终使用带标签的 break。
---
## 类型 Switch
关于类型 switch(`switch v := x.(type)`),参见 [go-interfaces](../../go-interfaces/SKILL.md):类型 Switch。
---
## 快速参考
| 模式 | 语法 |
|------|------|
| 无表达式 switch | `switch { case cond: }` |
| 逗号 case | `case 'a', 'b', 'c':` |
| 无 fallthrough | 默认行为;需要时使用 `fallthrough` 关键字 |
| 仅中断 switch | case 内使用 `break` |
| 中断外层循环 | 使用带标签的 `for` 和 `break Label` |
-140
View File
@@ -1,140 +0,0 @@
---
name: go-data-structures
description: Use when working with Go slices, maps, or arrays — choosing between new and make, using append, declaring empty slices (nil vs literal for JSON), implementing sets with maps, and copying data at boundaries. Also use when building or manipulating collections, even if the user doesn't ask about allocation idioms. Does not cover concurrent data structure safety (see go-concurrency).
license: Apache-2.0
metadata:
sources: "Effective Go, Google Style Guide, Uber Style Guide, Go Wiki CodeReviewComments"
---
# Go 数据结构
---
## 选择数据结构
```
你需要什么?
├─ 有序的元素集合
│ ├─ 编译时已知固定大小 → 数组 [N]T
│ └─ 动态大小 → 切片 []T
│ ├─ 知道大概的大小?→ make([]T, 0, capacity)
│ └─ 未知大小或需要 nil 安全的 JSON?→ var s []T (nil)
├─ 键值查找
│ └─ 映射 map[K]V
│ ├─ 知道大概的大小?→ make(map[K]V, capacity)
│ └─ 需要集合?→ map[T]struct{}(零大小值)
└─ 需要传递给函数?
└─ 如果调用者可能会修改它,则在边界处复制
```
> **此技能不适用的场景**:对于数据结构的并发访问(互斥锁、原子操作),参见 [go-concurrency](../go-concurrency/SKILL.md)。对于 API 边界处的防御性复制,参见 [go-defensive](../go-defensive/SKILL.md)。对于为性能预分配容量,参见 [go-performance](../go-performance/SKILL.md)。
---
## 切片
### append 函数
**始终赋值结果** — 底层数组可能会改变:
```go
x := []int{1, 2, 3}
x = append(x, 4, 5, 6)
// 将切片追加到切片
x = append(x, y...) // 注意 ...
```
### 二维切片
**独立的内部切片**(可以独立增长/缩小):
```go
picture := make([][]uint8, YSize)
for i := range picture {
picture[i] = make([]uint8, XSize)
}
```
**单次分配**(对于固定大小更高效):
```go
picture := make([][]uint8, YSize)
pixels := make([]uint8, XSize*YSize)
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
```
> 在调试意外的切片行为、跨 goroutine 共享切片或处理切片头时,阅读 [references/SLICES.md](references/SLICES.md)。
### 声明空切片
优先使用 nil 切片而非空字面量:
```go
// 好:nil 切片
var t []string
// 避免:非 nil 但零长度
t := []string{}
```
两者的 `len` 和 `cap` 都是零,但 nil 切片是首选风格。
**JSON 例外**:nil 切片编码为 `null`,而 `[]string{}` 编码为 `[]`。当需要 JSON 数组时使用非 nil。
在设计接口时,避免区分 nil 和非 nil 的零长度切片。
---
## 映射
### 实现集合
使用 `map[T]bool` — 惯用且阅读自然:
```go
attended := map[string]bool{"Ann": true, "Joe": true}
if attended[person] { // 不在映射中则为 false
fmt.Println(person, "was at the meeting")
}
```
---
## 复制
从另一个包复制结构体时要小心。如果类型的方法定义在指针类型(`*T`)上,复制值可能导致别名 bug。
**通用规则:** 如果类型 `T` 的方法与指针类型 `*T` 关联,则不要复制 `T` 的值。这适用于 `bytes.Buffer`、`sync.Mutex`、`sync.WaitGroup` 以及包含它们的类型。
```go
// 不好:复制互斥锁
var mu sync.Mutex
mu2 := mu // 几乎总是 bug
// 好:通过指针传递
func increment(sc *SafeCounter) {
sc.mu.Lock()
sc.count++
sc.mu.Unlock()
}
```
---
## 快速参考
| 主题 | 关键点 |
|------|--------|
| 切片 | 始终赋值 `append` 结果;`nil` 切片优于 `[]T{}` |
| 集合 | `map[T]bool` 是惯用写法 |
| 复制 | 如果方法在 `*T` 上则不要复制 `T`;注意别名问题 |
## 相关技能
- **防御性复制**:在 API 边界处复制切片或映射以防止修改时,参见 [go-defensive](../go-defensive/SKILL.md)
- **容量提示**:为已知工作负载预分配切片或映射容量时,参见 [go-performance](../go-performance/SKILL.md)
- **迭代模式**:在对切片、映射或通道使用 range 循环时,参见 [go-control-flow](../go-control-flow/SKILL.md)
- **声明风格**:在 `new`、`make`、`var` 和复合字面量之间选择时,参见 [go-declarations](../go-declarations/SKILL.md)
@@ -1,146 +0,0 @@
# Go 切片内部原理
> **来源**:Effective Go
---
## 三项描述符
切片是一个运行时数据结构,包含三个组件:
- **指针**:第一个可访问元素的地址
- **长度**:元素数量(`len(s)`)
- **容量**:到底层数组末尾的最大元素数(`cap(s)`)
```go
arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4] // s = [20, 30, 40]
// 指针:&arr[1],长度:3,容量:4
```
`nil` 切片的三项均为零/nil。
---
## 切片引用底层数组
切片不存储数据 — 它们描述数组的一部分:
```go
data := [4]int{1, 2, 3, 4}
a := data[0:2] // [1, 2]
b := data[1:3] // [2, 3]
b[0] = 99
fmt.Println(a) // [1, 99] - 两者都看到变化
fmt.Println(data) // [1, 99, 3, 4]
```
---
## 切片运算符
`s[lo:hi]` 创建从索引 `lo` 到 `hi-1` 的切片:
```go
s := []int{0, 1, 2, 3, 4, 5}
s[2:4] // [2, 3]
s[:3] // [0, 1, 2]
s[3:] // [3, 4, 5]
```
三索引形式 `s[lo:hi:max]` 将容量限制为 `max-lo`。
---
## 为什么 append 必须返回切片
切片头是**按值传递**的。函数可以修改元素但无法改变调用者的切片头:
```go
func Append(slice, data []byte) []byte {
l := len(slice)
if l+len(data) > cap(slice) {
newSlice := make([]byte, (l+len(data))*2)
copy(newSlice, slice)
slice = newSlice // 只改变局部变量
}
slice = slice[0 : l+len(data)]
copy(slice[l:], data)
return slice // 调用者必须接收新的切片头
}
```
当发生重新分配时,`slice` 指向新数组。调用者的原始引用仍指向旧数组 — 返回使调用者能够更新其引用。
---
## copy 函数
`copy(dst, src)` 复制元素并返回复制的数量:
```go
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // n=3, dst=[1,2,3]
```
正确处理重叠切片。复制 `min(len(dst), len(src))` 个元素 — 不会发生重新分配。
---
## 切片常见陷阱
### 1. 共享底层数组
```go
original := []int{1, 2, 3, 4, 5}
subset := original[1:3]
subset[0] = 99
fmt.Println(original) // [1, 99, 3, 4, 5] - 被修改了!
// 修复:创建独立副本
subset := make([]int, 2)
copy(subset, original[1:3])
```
### 2. append 可能重新分配也可能不
```go
a := make([]int, 3, 5) // len=3, cap=5
b := a[0:3]
a = append(a, 4) // 在容量内 - 仍然共享
a = append(a, 5, 6) // 超出容量 - 现在独立
```
### 3. 大底层数组导致内存泄漏
```go
// 不好:小切片将整个文件保留在内存中
func getHeader(file []byte) []byte { return file[:100] }
// 好:复制以释放大数组
func getHeader(file []byte) []byte {
header := make([]byte, 100)
copy(header, file)
return header
}
```
### 4. nil vs 空切片
```go
var nilSlice []int // nil, len=0, cap=0
emptySlice := []int{} // 非 nil, len=0, cap=0
// 两者在 len、cap、append、range 中表现相同
// 未初始化状态优先使用 nil
```
## 快速参考
| 操作 | 行为 |
|------|------|
| `s[lo:hi]` | 从 lo 到 hi-1 的切片 |
| `s[lo:hi:max]` | 容量限制为 max-lo 的切片 |
| `append(s, x...)` | 返回新切片;可能重新分配 |
| `copy(dst, src)` | 返回复制数量;不重新分配 |
-188
View File
@@ -1,188 +0,0 @@
---
name: go-defensive
description: Use when hardening Go code at API boundaries — copying slices/maps, verifying interface compliance, using defer for cleanup, time.Time/time.Duration, or avoiding mutable globals. Also use when reviewing for robustness concerns like missing cleanup or unsafe crypto usage, even if the user doesn't mention "defensive programming." Does not cover error handling strategy (see go-error-handling).
license: Apache-2.0
compatibility: Uses crypto/rand.Text (Go 1.24+) in examples
metadata:
sources: "Effective Go, Uber Style Guide, Go Wiki CodeReviewComments"
---
# Go 防御性编程模式
## 防御性检查清单优先级
在加固 API 边界代码时,按以下顺序检查:
```
正在审查 API 边界?
├─ 1. 错误处理 → 返回错误;不要 panic(参见 go-error-handling)
├─ 2. 输入验证 → 复制从调用者接收的切片/map
├─ 3. 输出安全 → 在返回给调用者之前复制切片/map
├─ 4. 资源清理 → 使用 defer 进行 Close/Unlock/Cancel
├─ 5. 接口检查 → var _ Interface = (*Type)(nil) 编译时验证
├─ 6. 时间正确性 → 使用 time.Time 和 time.Duration,不要用 int/float
├─ 7. 枚举安全 → iota 从 1 开始,使零值无效
└─ 8. 加密安全 → 用 crypto/rand 生成密钥,绝不用 math/rand
```
---
## 快速参考
| 模式 | 规则 | 详情 |
|------|------|------|
| 边界复制 | 在接收和返回时复制切片/map | [BOUNDARY-COPYING.md](references/BOUNDARY-COPYING.md) |
| Defer 清理 | 在 `os.Open` 之后立即 `defer f.Close()` | 见下文 |
| 接口检查 | `var _ I = (*T)(nil)` | 参见 go-interfaces |
| 时间类型 | `time.Time` / `time.Duration`,绝不用原始 int | [TIME-ENUMS-TAGS.md](references/TIME-ENUMS-TAGS.md) |
| 枚举起始值 | `iota + 1` 使零值 = 无效 | 见下文 |
| 加密随机数 | 用 `crypto/rand` 生成密钥,绝不用 `math/rand` | 见下文 |
| Must 函数 | 仅在初始化时使用;失败时 panic | [MUST-FUNCTIONS.md](references/MUST-FUNCTIONS.md) |
| Panic/recover | 绝不跨包暴露 panic | [PANIC-RECOVER.md](references/PANIC-RECOVER.md) |
| 可变全局变量 | 用依赖注入替代 | 见下文 |
---
## 验证接口合规性
使用编译时检查来验证接口实现。完整模式请参见 **go-interfaces**:接口满足检查。
```go
var _ http.Handler = (*Handler)(nil)
```
## 在边界处复制切片和 Map
切片和 map 包含指向底层数据的指针。在 API 边界处复制,以防止意外修改。
```go
// 接收:复制传入的切片
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
// 返回:在返回之前复制 map
result := make(map[string]int, len(s.counters))
for k, v := range s.counters { result[k] = v }
```
> 在 API 边界处复制切片或 map,或决定何时需要防御性复制、何时可以跳过时,请阅读 [references/BOUNDARY-COPYING.md](references/BOUNDARY-COPYING.md)。
## 使用 Defer 清理资源
使用 `defer` 清理资源(文件、锁)。避免在多个返回路径中遗漏清理。
```go
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
```
Defer 的开销可以忽略不计。在 `os.Open` 之后立即放置 `defer f.Close()` 以提高清晰度。延迟函数的参数在 `defer` 执行时求值,而非在函数运行时。多个 defer 按 LIFO 顺序执行。
## 结构体字段标签
> **建议**:始终为需要序列化或反序列化的结构体添加显式字段标签。
```go
type User struct {
Name string `json:"name" yaml:"name"`
Email string `json:"email" yaml:"email"`
}
```
字段标签是**序列化契约**——重命名结构体字段而不更新标签会悄然破坏线格式兼容性。对于任何跨越序列化边界的类型,应将标签视为公共 API 的一部分。
## 枚举从 1 开始
枚举从非零值开始,以区分未初始化的值和有效值。
```go
const (
Add Operation = iota + 1 // Add=1,零值 = 未初始化
Subtract
Multiply
)
```
**例外**:当零值是合理的默认值时(例如 `LogToStdout = iota`)。
## 时间、结构体标签和嵌入
> 在使用 `time.Time`/`time.Duration` 代替原始 int、为序列化结构体添加字段标签,或决定是否在公共结构体中嵌入类型时,请阅读 [references/TIME-ENUMS-TAGS.md](references/TIME-ENUMS-TAGS.md)。
## 避免可变全局变量
通过注入依赖代替修改包级变量。这使代码可以在不需要全局 save/restore 的情况下进行测试。
```go
type signer struct {
now func() time.Time // 注入的;测试中用固定时间替换
}
func newSigner() *signer {
return &signer{now: time.Now}
}
```
> 在决定全局变量是否合适、设计 New() + Default() 包状态模式,或用依赖注入替代可变全局变量时,请阅读 [references/GLOBAL-STATE.md](references/GLOBAL-STATE.md)。
## 加密随机数
不要使用 `math/rand` 或 `math/rand/v2` 生成密钥——这是一个**安全问题**。时间种子的生成器输出是可预测的。
```go
import "crypto/rand"
func Key() string { return rand.Text() }
```
对于文本输出,直接使用 `crypto/rand.Text`,或用 `encoding/hex` 或 `encoding/base64` 编码随机字节。
---
## Panic 与 Recover
仅在真正不可恢复的情况下使用 `panic`。库函数应避免 panic。
```go
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
```
**关键规则:**
- 绝不跨包边界暴露 panic——始终转换为 error
- 如果库确实无法在 `init()` 中完成初始化,可以接受 panic
- 使用 recover 隔离服务器 goroutine 处理器中的 panic
> 在编写 HTTP 服务器中的 panic 恢复、在解析器中使用 panic 作为内部控制流机制,或在 log.Fatal 和 panic 之间做选择时,请阅读 [references/PANIC-RECOVER.md](references/PANIC-RECOVER.md)。
## Must 函数
`Must` 函数在出错时 panic——**仅**在程序初始化阶段使用,因为失败意味着程序无法运行。
```go
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
var tmpl = template.Must(template.ParseFiles("index.html"))
```
> 在编写自定义 Must 函数、决定 Must 是否适用于特定调用点,或将可能失败的初始化包装在 panic 辅助函数中时,请阅读 [references/MUST-FUNCTIONS.md](references/MUST-FUNCTIONS.md)。
---
## 相关技能
- **错误处理**:在选择返回错误还是 panic,或在边界处包装错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **并发安全**:在使用互斥锁、原子操作或通道保护共享状态时,参见 [go-concurrency](../go-concurrency/SKILL.md)
- **接口检查**:在添加编译时接口满足检查(`var _ I = (*T)(nil)`)时,参见 [go-interfaces](../go-interfaces/SKILL.md)
- **数据结构复制**:在处理切片/map 内部结构或指针别名时,参见 [go-data-structures](../go-data-structures/SKILL.md)
@@ -1,101 +0,0 @@
# 在 API 边界处复制切片和 Map
> **来源**:Uber 风格指南
切片和 map 包含对其底层数据的引用。在 API 边界处复制它们,以防止调用者修改内部状态(反之亦然)。
## 接收切片和 Map
当函数存储调用者传入的切片或 map 时,始终进行防御性复制。调用者保留原始引用,可以在函数返回后修改它。
### 切片
**不好**
```go
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips // 调用者仍然可以修改 d.trips
}
```
**好**
```go
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
```
### Map
**不好**
```go
func (s *Server) SetConfig(cfg map[string]string) {
s.config = cfg // 调用者仍然可以修改 s.config
}
```
**好**
```go
func (s *Server) SetConfig(cfg map[string]string) {
s.config = make(map[string]string, len(cfg))
for k, v := range cfg {
s.config[k] = v
}
}
```
## 返回切片和 Map
返回内部切片或 map 时,返回副本以防止调用者修改你的内部状态。
### 返回 Map
**不好**
```go
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters // 暴露了内部状态!
}
```
**好**
```go
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
```
### 返回切片
**不好**
```go
func (q *Queue) Items() []Item {
return q.items // 调用者可以追加、修改或重新切片
}
```
**好**
```go
func (q *Queue) Items() []Item {
result := make([]Item, len(q.items))
copy(result, q.items)
return result
}
```
## 何时不需要复制
防御性复制有开销。在以下情况下可以跳过:
- 数据**按约定是不可变的**,并且有清晰的文档说明
- 切片/map 是**为调用者新创建的**(不在内部存储)
- 性能分析表明复制在热路径中是瓶颈
如有疑问,就复制。与共享引用导致的 bug 相比,开销通常可以忽略不计。
@@ -1,144 +0,0 @@
# 全局状态模式
> **来源**:Google 风格指南, Effective Go
全局状态使程序更难以测试、推理和维护。依赖注入是首选替代方案,但某些全局状态在谨慎使用时是可以接受的。
## 何时可以接受全局状态
并非所有包级变量都有害。当全局状态是**真正进程级别的**且**不值得注入**时,它是合适的:
- **默认实例**——`http.DefaultClient`、`log.Default()`、`flag.CommandLine`
- **一次编译的值**——包级别的 `regexp.MustCompile(...)`
- **注册表**——`database/sql.Register`、`image.RegisterFormat`
- **单例基础设施**——进程级别的指标收集器或追踪导出器
## 全局变量的试金石测试
在添加包级变量之前,请问自己:
1. **它是否真正是进程级别的?** 如果两个 goroutine 或测试可能需要不同的值,它不应该是全局的
2. **它是否妨碍了测试?** 如果测试必须保存/恢复变量,或因此无法并行运行,应改为注入
3. **它可以是常量吗?** 如果值在初始化后永远不会改变,优先使用 `const` 或未导出的只初始化一次的 `var`
4. **它是否携带可变状态?** 可变全局变量是最危险的——仅在有完善文档、并发安全的单例情况下才可接受
## 包状态 API 模式:New() + Default()
标准库模式同时提供可定制的构造器和便捷的默认值。这使调用者可以在简单场景下使用默认值,在测试或特殊行为需求下注入自定义实例。
**好**
```go
package mylog
type Logger struct {
prefix string
out io.Writer
}
func New(prefix string, out io.Writer) *Logger {
return &Logger{prefix: prefix, out: out}
}
var defaultLogger = New("", os.Stderr)
func Default() *Logger { return defaultLogger }
func (l *Logger) Info(msg string) {
fmt.Fprintf(l.out, "%s%s\n", l.prefix, msg)
}
// 包级便捷函数委托给默认实例。
func Info(msg string) { defaultLogger.Info(msg) }
```
```go
// 调用者在简单场景下使用默认值
mylog.Info("starting server")
// 测试或特殊代码创建自定义实例
logger := mylog.New("[test] ", &buf)
logger.Info("test message")
```
此模式的标准库示例:
- `log.New()` + `log.Default()` + `log.Println()`
- `http.NewServeMux()` + `http.DefaultServeMux`
- `flag.NewFlagSet()` + `flag.CommandLine`
## 依赖注入作为首选替代方案
当代码需要可配置行为时,通过构造器参数或结构体字段接受依赖,而非读取包级变量。
**不好**
```go
var db *sql.DB
func GetUser(id int) (*User, error) {
return db.QueryRow("SELECT ...", id) // 依赖全局变量
}
```
**好**
```go
type UserStore struct {
db *sql.DB
}
func NewUserStore(db *sql.DB) *UserStore {
return &UserStore{db: db}
}
func (s *UserStore) GetUser(id int) (*User, error) {
return s.db.QueryRow("SELECT ...", id)
}
```
注入的好处:
- 测试可以提供 mock 或内存实现
- 多个实例可以共存(例如,只读副本与主库)
- 依赖在构造器签名中是显式的
## 注入时间
一个常见场景:替换 `time.Now` 以实现确定性测试。
**不好**
```go
func IsExpired(expiry time.Time) bool {
return time.Now().After(expiry) // 不可测试
}
```
**好**
```go
type Checker struct {
now func() time.Time
}
func NewChecker() *Checker {
return &Checker{now: time.Now}
}
func (c *Checker) IsExpired(expiry time.Time) bool {
return c.now().After(expiry)
}
```
测试用固定函数替换 `now`:
```go
c := &Checker{now: func() time.Time {
return time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
}}
```
## 总结
| 场景 | 方法 |
|------|------|
| 进程级单例(日志、指标) | 默认实例 + `New()` 构造器 |
| 一次编译的正则或模板 | 包级 `var` 配合 `MustCompile` |
| 注册表(数据库驱动、编解码器) | 包级 `Register()` 函数 |
| 可配置行为 | 通过构造器进行依赖注入 |
| 时间相关逻辑 | 注入 `func() time.Time` |
| 测试需要变化的任何东西 | 不要使用全局状态 |
@@ -1,92 +0,0 @@
# Must 函数
> **来源**:Uber 风格指南, Go 标准库约定
`Must` 函数包装一个可能失败的函数,在出错时 panic。**仅**在程序初始化阶段使用,因为失败意味着程序无法运行。
## 标准库示例
```go
// regexp.MustCompile 在模式无效时 panic
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
// template.Must 在模板解析失败时 panic
var tmpl = template.Must(template.ParseFiles("index.html"))
```
这些是安全的,因为它们在包初始化时运行——如果失败,程序无法正确运行。
## 何时使用 Must
```
这是在程序初始化期间调用的吗(包级 var、init、main 设置)?
├─ 是 → 失败是否不可恢复(配置、正则、模板)?
│ ├─ 是 → 使用 Must 是合适的
│ └─ 否 → 改为返回 error
└─ 否 → 绝不使用 Must——返回 error
```
### 适当的使用场景
- **包级 `var`**:编译正则表达式、解析模板、加载必需的配置
- **`init()` 或 `main()` 早期**:设置程序运行所必需的资源
- **测试辅助函数**:测试中优先使用 `t.Fatal`,但 Must 在测试 fixture 中是可以接受的
### 绝不使用 Must 的场景
- 运行时请求处理
- 用户提供的输入
- 可能合理失败的网络或文件操作
- 程序启动后调用的任何代码
## 编写 Must 函数
遵循命名约定 `MustX`,其中 `X` 是可能失败的函数名:
```go
func MustParseConfig(path string) *Config {
cfg, err := ParseConfig(path)
if err != nil {
panic(fmt.Sprintf("parsing config %s: %v", path, err))
}
return cfg
}
```
### 指南
- **命名**:`Must` 前缀 + 可能失败的函数名(例如 `MustParse`、`MustNew`、`MustCompile`)
- **Panic 消息**:包含输入和错误信息以便调试
- **文档**:始终记录函数在出错时会 panic
```go
// MustParseConfig 解析路径处的配置文件。
// 如果文件无法读取或包含无效配置,则会 panic。
func MustParseConfig(path string) *Config { ... }
```
### 泛型 Must 辅助函数
对于一次性使用,泛型 Must 辅助函数可以避免样板代码:
```go
func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
// 在包级别使用
var cfg = Must(ParseConfig("app.yaml"))
```
## 与 Panic/Recover 的关系
Must 函数是对 `panic` 的受控使用。它们应该:
- 仅在初始化期间运行(因此不需要 recover)
- 产生清晰、可操作的 panic 消息
- 绝不在可以返回 error 的场景中使用
完整的 panic/recover 模式请参见 [PANIC-RECOVER.md](PANIC-RECOVER.md)。
@@ -1,161 +0,0 @@
# Panic 与 Recover 模式
> **来源**:Effective Go
## Panic 指南
`panic` 创建一个运行时错误来停止程序。仅在真正不可恢复的情况下使用。
### 何时 Panic
真正的库函数应**避免 panic**。如果问题可以被掩盖或绕过,让程序继续运行,而不是让整个程序崩溃。
```go
// 可接受:真正不可能的情况
func CubeRoot(x float64) float64 {
z := x/3
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// 百万次迭代仍未收敛;出了问题。
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
```
### 初始化中的 Panic
例外:如果库在 `init()` 期间确实无法完成初始化,panic 可能是合理的:
```go
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
```
### 何时 Panic 是可接受的
除了初始化之外,panic 在以下窄泛场景中是可接受的:
1. **API 误用**——类似于核心语言对越界访问的 panic。`reflect` 包使用了这种方法。
2. **带有匹配 `recover` 的内部实现细节**在包边界处。Panic 简化了深层嵌套的控制流,而公共 API 仍然返回 error(下方的 Parse/parseInt 模式)。
3. **`panic("unreachable")`** 在 `log.Fatal` 之后,当编译器无法检测到不可达代码时。
#### Parse/parseInt 模式
在内部使用 panic 来回退复杂的递归,但始终在包边界处转换为 error:
```go
func parseInt(in string) int {
n, err := strconv.Atoi(in)
if err != nil {
panic(&syntaxError{"not a valid integer"})
}
return n
}
func Parse(in string) (_ *Node, err error) {
defer func() {
if p := recover(); p != nil {
sErr, ok := p.(*syntaxError)
if !ok {
panic(p) // 不是我们的——重新 panic
}
err = fmt.Errorf("syntax error: %v", sErr.msg)
}
}()
// ... 内部调用 parseInt
}
```
**关键**:类型检查 `p.(*syntaxError)` 确保只捕获*我们的* panic。意外的 panic(nil 指针等)正常传播。
---
## Recover 模式
`recover` 重新获得对正在 panic 的 goroutine 的控制。它只在延迟函数中有效。
### 基本恢复模式
```go
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
```
### 服务器 Goroutine 保护
在服务器中将 panic 隔离到各个 goroutine:
```go
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work) // 每个 worker 都受保护
}
}
```
如果 `do(work)` panic,结果会被记录,goroutine 干净退出而不影响其他 goroutine。
### 包内部的 Panic/Recover
在内部使用 panic 但在 API 边界处转换为 error:
```go
// Error 是一个解析错误类型
type Error string
func (e Error) Error() string { return string(e) }
// 内部:使用 Error 类型 panic
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// 外部 API:将 panic 转换为 error 返回
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
defer func() {
if e := recover(); e != nil {
regexp = nil
err = e.(Error) // 如果不是我们的 Error 类型则重新 panic
}
}()
return regexp.doParse(str), nil
}
```
**要点:**
- 延迟函数可以修改命名返回值
- 类型断言 `e.(Error)` 对意外错误类型重新 panic
- 绝不向客户端暴露 panic——始终在 API 边界处转换
---
## 快速参考
| 模式 | 描述 |
|------|------|
| 基本恢复 | `defer func() { if err := recover(); err != nil { ... } }()` |
| 服务器保护 | 将每个 goroutine 处理器包装在 safelyDo 中 |
| 包内部 | 内部 panic,在 API 边界处 recover 并返回 error |
| 类型安全恢复 | 使用类型断言对意外错误重新 panic |
## 何时使用
- **Panic**:仅用于真正不可恢复的情况或初始化失败
- **Recover**:服务器处理器、包内部错误简化
- **绝不**:跨包边界暴露 panic——始终转换为 error
@@ -1,111 +0,0 @@
# 时间、结构体标签和嵌入模式
## 使用 time.Time 和 time.Duration
始终使用 `time` 包。避免使用原始 `int` 表示时间值。
### 时间点
**不好**
```go
func isActive(now, start, stop int) bool {
return start <= now && now < stop
}
```
**好**
```go
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}
```
### 时长
**不好**
```go
func poll(delay int) {
time.Sleep(time.Duration(delay) * time.Millisecond)
}
poll(10) // 秒?毫秒?
```
**好**
```go
func poll(delay time.Duration) {
time.Sleep(delay)
}
poll(10 * time.Second)
```
### JSON 字段
当无法使用 `time.Duration` 时,在字段名中包含单位:
**不好**
```go
type Config struct {
Interval int `json:"interval"`
}
```
**好**
```go
type Config struct {
IntervalMillis int `json:"intervalMillis"`
}
```
## 避免在公共结构体中嵌入类型
嵌入类型会泄露实现细节并阻碍类型演进。
**不好**
```go
type ConcreteList struct {
*AbstractList
}
```
**好**
```go
type ConcreteList struct {
list *AbstractList
}
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}
```
嵌入的问题:
- 向嵌入接口添加方法是破坏性变更
- 从嵌入结构体移除方法是破坏性变更
- 替换嵌入类型是破坏性变更
## 在序列化结构体中使用字段标签
始终为 JSON、YAML 等使用显式字段标签。
**不好**
```go
type Stock struct {
Price int
Name string
}
```
**好**
```go
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// 可以安全地将 Name 重命名为 Symbol
}
```
标签使序列化契约显式化,并可以安全地进行重构。
-168
View File
@@ -1,168 +0,0 @@
---
name: go-error-handling
description: Use when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive).
license: Apache-2.0
compatibility: Requires Go 1.13+ for errors.Is/errors.As and fmt.Errorf %w wrapping. Structured logging examples use slog (Go 1.21+).
metadata:
sources: "Google Style Guide, Uber Style Guide"
allowed-tools: Bash(bash:*)
---
# Go 错误处理
## 可用脚本
- **`scripts/check-errors.sh`** — 检测错误处理反模式:对 `err.Error()` 进行字符串比较、没有上下文的裸 `return err`、以及日志并返回违规。运行 `bash scripts/check-errors.sh --help` 查看选项。
在 Go 中,[错误是值](https://go.dev/blog/errors-are-values) — 它们由代码创建,也由代码消费。
## 选择错误策略
1. 系统边界(RPC、IPC、存储)?→ 使用 `%v` 包装以避免泄露内部细节
2. 调用者需要匹配特定条件?→ 哨兵或类型化错误,使用 `%w` 包装
3. 调用者只需要调试上下文?→ `fmt.Errorf("...: %w", err)`
4. 叶子函数,无需包装?→ 直接返回错误
**默认**:使用 `%w` 包装,并将其放在格式字符串的末尾。
---
## 核心规则
### 永不返回具体错误类型
**永不从导出函数返回具体错误类型** — 具体的 `nil` 指针可能变成非 nil 接口:
```go
// 不好:具体类型可能导致微妙的 bug
func Bad() *os.PathError { /*...*/ }
// 好:始终返回 error 接口
func Good() error { /*...*/ }
```
### 错误字符串
错误字符串**不应**大写,也**不应**以标点符号结尾。例外:导出名称、专有名词或缩写。
```go
// 不好
err := fmt.Errorf("Something bad happened.")
// 好
err := fmt.Errorf("something bad happened")
```
对于显示的消息(日志、测试失败、API 响应),大写是适当的。
### 出错时的返回值
当函数返回错误时,调用者必须将所有非错误返回值视为未指定,除非有明确文档说明。
**提示**:接受 `context.Context` 的函数通常应返回 `error`,以便调用者判断上下文是否被取消。
---
## 处理错误
遇到错误时,做出**深思熟虑的选择** — 不要用 `_` 丢弃:
1. **立即处理** — 解决错误并继续
2. **返回给调用者** — 可选择用上下文包装
3. **在特殊情况下** — `log.Fatal` 或 `panic`
有意忽略时:添加注释说明原因。
```go
n, _ := b.Write(p) // 永不返回非 nil 错误
```
对于相关的并发操作,使用 [`errgroup`](https://pkg.go.dev/golang.org/x/sync/errgroup):
```go
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
if err := g.Wait(); err != nil { return err }
```
### 避免带内错误
不要返回 `-1`、`nil` 或空字符串来表示错误。使用多返回值:
```go
// 不好:带内错误值
func Lookup(key string) int // 缺失时返回 -1
// 好:显式的 error 或 ok 值
func Lookup(key string) (string, bool)
```
这可以防止调用者写出 `Parse(Lookup(key))` — 它会导致编译时错误,因为 `Lookup(key)` 有 2 个输出。
---
## 错误流程
在正常代码之前处理错误。提前返回使正常路径保持无缩进:
```go
// 好:错误优先,正常代码无缩进
if err != nil {
return err
}
// 正常代码
```
**错误只处理一次** — 记录日志或返回,不要两者都做:
```
遇到错误?
├─ 调用者可以采取行动?→ 返回(通过 %w 附带上下文)
├─ 在调用链顶部?→ 记录日志并处理
└─ 都不是?→ 以适当级别记录日志,继续执行
```
> 在组织复杂的错误流程、决定记录日志还是返回、实现一次处理模式、或选择结构化日志级别时,请阅读 [references/ERROR-FLOW.md](references/ERROR-FLOW.md)。
---
## 错误类型
> **建议**:推荐的最佳实践。
| 调用者需要匹配? | 消息类型 | 使用方式 |
|-----------------|---------|---------|
| 否 | 静态 | `errors.New("message")` |
| 否 | 动态 | `fmt.Errorf("msg: %v", val)` |
| 是 | 静态 | `var ErrFoo = errors.New("...")` |
| 是 | 动态 | 自定义 `error` 类型 |
**默认**:使用 `fmt.Errorf("...: %w", err)` 包装。升级为哨兵以使用 `errors.Is()`,升级为自定义类型以使用 `errors.As()`。
> 在定义哨兵错误、创建自定义错误类型、或为包 API 选择错误策略时,请阅读 [references/ERROR-TYPES.md](references/ERROR-TYPES.md)。
---
## 错误包装
> **建议**:推荐的最佳实践。
- **使用 `%v`**:在系统边界、用于日志记录、隐藏内部细节
- **使用 `%w`**:保留错误链以供 `errors.Is`/`errors.As` 使用
**关键规则**:将 `%w` 放在末尾。添加调用者没有的上下文。如果注释没有增加信息,直接返回 `err`。
> 在决定使用 %v 还是 %w、跨包边界包装错误、或添加上下文信息时,请阅读 [references/WRAPPING.md](references/WRAPPING.md)。
> **验证**:实现错误处理后,运行 `bash scripts/check-errors.sh` 检测常见的反模式。然后运行 `go vet ./...` 捕获其他问题。
---
## 相关技能
- **错误命名**:在命名哨兵错误(`ErrFoo`)或自定义错误类型时,参见 [go-naming](../go-naming/SKILL.md)
- **测试错误**:在使用 `errors.Is`/`errors.As` 测试错误语义或编写错误检查辅助函数时,参见 [go-testing](../go-testing/SKILL.md)
- **Panic 处理**:在决定 panic 还是返回错误、或编写 recover 守卫时,参见 [go-defensive](../go-defensive/SKILL.md)
- **守卫子句**:在组织提前返回的错误流程或减少嵌套时,参见 [go-control-flow](../go-control-flow/SKILL.md)
- **日志决策**:在选择日志级别、配置结构化日志、或决定日志消息中包含什么上下文时,参见 [go-logging](../go-logging/SKILL.md)
@@ -1,153 +0,0 @@
# 错误流程模式
错误流程、一次处理原则和日志决策的详细模式。
## 缩进错误流程
在继续正常代码之前先处理错误。这通过使读者能够快速找到正常路径来提高可读性。
```go
// 好:错误处理优先,正常代码无缩进
if err != nil {
// 错误处理
return // 或 continue 等
}
// 正常代码
```
```go
// 不好:正常代码隐藏在 else 子句中
if err != nil {
// 错误处理
} else {
// 正常代码因缩进看起来不自然
}
```
### 避免对长期使用的变量使用 if 初始化语句
如果变量在多行中使用,将声明移出:
```go
// 好:声明与错误检查分开
x, err := f()
if err != nil {
return err
}
// 大量使用 x 的代码
// 跨越多行
```
```go
// 不好:变量作用域限制在 else 块中,难以阅读
if x, err := f(); err != nil {
return err
} else {
// 大量使用 x 的代码
// 跨越多行
}
```
---
## 错误只处理一次
当调用者收到错误时,应该**只处理一次**。选择一种响应方式:
1. **返回错误**(包装或原文)让调用者处理
2. **记录日志并优雅降级**(不返回错误)
3. **匹配并处理**特定错误情况,返回其他错误
**如果返回了错误,就不要自己记录日志** — 让调用者处理。对同一错误既记录日志又返回是最常见的"一次处理"违规,导致重复噪音,因为调用栈上层的调用者也会处理该错误。
```go
// 不好:既记录日志又返回 — 导致日志噪音
u, err := getUser(id)
if err != nil {
log.Printf("Could not get user %q: %v", id, err)
return err // 调用者也会记录这个!
}
// 好:包装并返回 — 让调用者决定如何处理
u, err := getUser(id)
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
}
// 好:记录日志并优雅降级(不返回错误)
if err := emitMetrics(); err != nil {
// 写入指标失败不应影响应用程序
log.Printf("Could not emit metrics: %v", err)
}
// 继续执行...
// 好:匹配特定错误,返回其他错误
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// 用户不存在,使用 UTC
tz = time.UTC
} else {
return fmt.Errorf("get user %q: %w", id, err)
}
}
```
---
## 记录日志 vs 返回错误
> 错误只处理一次 — 记录日志或返回,不要两者都做。
### 决策流程
```
遇到错误?
├─ 调用者可以采取行动?→ 返回错误(通过 %w 附带上下文)
├─ 在调用链顶部?→ 记录日志并处理(返回 HTTP 状态码、退出等)
└─ 都不是?→ 以适当级别记录日志并继续
```
### 不要既记录日志又返回
```go
// 不好:错误既被记录又被返回 — 在日志中出现两次
func process(ctx context.Context, id string) error {
result, err := fetch(ctx, id)
if err != nil {
log.Printf("failed to fetch %s: %v", id, err)
return fmt.Errorf("fetching %s: %w", id, err)
}
return handle(result)
}
// 好:带上下文返回 — 让调用者决定是否记录日志
func process(ctx context.Context, id string) error {
result, err := fetch(ctx, id)
if err != nil {
return fmt.Errorf("fetching %s: %w", id, err)
}
return handle(result)
}
```
### 结构化日志
在生产代码中,优先使用结构化日志(Go 1.21+ 的 `slog`,或 `log/slog` 兼容库)而非 `log.Printf`:
```go
// 好:结构化字段可被机器解析
slog.Error("fetch failed", "id", id, "err", err)
// 避免:非结构化的字符串插值
log.Printf("fetch failed for %s: %v", id, err)
```
### 日志级别
| 级别 | 使用场景 |
|------|---------|
| Error | 需要关注的可操作故障 |
| Warn | 不需要立即处理的降级行为 |
| Info | 关键生命周期事件(启动、关闭、配置加载) |
| Debug | 开发期间有用的诊断细节 |
@@ -1,151 +0,0 @@
# 错误类型参考
本参考涵盖结构化错误类型、哨兵错误,以及如何为你的用例选择正确的错误类型。
---
## 错误结构
> 错误类型决策表在父技能中(SKILL.md § 错误类型)。
> 本参考涵盖:扩展的代码示例、哨兵错误、使用 `errors.Is`/`errors.As` 进行错误检查,以及结构化错误类型。
**关键考虑因素**:
- 调用者是否需要使用 `errors.Is` 或 `errors.As` 来匹配错误?
- 错误消息是静态的还是需要运行时值?
- 导出的错误变量/类型将成为公共 API 的一部分
```go
// 无需匹配,静态消息
func Open() error {
return errors.New("could not open")
}
// 需要匹配,静态消息 - 导出哨兵
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// 需要匹配,动态消息 - 使用自定义类型
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
func Open(file string) error {
return &NotFoundError{File: file}
}
```
---
## 哨兵错误
最简单的结构化错误是无参数化的全局值:
```go
// 好:用于程序化检查的哨兵错误
var (
// ErrDuplicate 在该动物已被见过时发生。
ErrDuplicate = errors.New("duplicate")
// ErrMarsupial 因为我们不支持有袋类动物。
ErrMarsupial = errors.New("marsupials are not supported")
)
func process(animal Animal) error {
switch {
case seen[animal]:
return ErrDuplicate
case marsupial(animal):
return ErrMarsupial
}
seen[animal] = true
return nil
}
```
---
## 检查错误
对于直接比较(当错误未被包装时):
```go
// 好:与哨兵直接比较
switch err := process(an); err {
case ErrDuplicate:
return fmt.Errorf("feed %q: %v", an, err)
case ErrMarsupial:
alternate := an.BackupAnimal()
return handlePet(alternate)
}
```
当错误可能被包装时,使用 `errors.Is`:
```go
// 好:适用于被包装的错误
switch err := process(an); {
case errors.Is(err, ErrDuplicate):
return fmt.Errorf("feed %q: %v", an, err)
case errors.Is(err, ErrMarsupial):
// 尝试恢复...
}
```
**绝不**基于字符串内容匹配错误:
```go
// 不好:脆弱的字符串匹配
if regexp.MatchString(`duplicate`, err.Error()) {...}
if regexp.MatchString(`marsupial`, err.Error()) {...}
```
---
## 结构化错误类型
对于需要额外程序化信息的错误,使用结构体类型:
```go
// 好:具有可访问字段的结构化错误
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
func (e *PathError) Unwrap() error { return e.Err }
```
调用者可以使用 `errors.As` 提取结构化错误:
```go
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Failed path:", pathErr.Path)
}
```
---
## 快速参考
| 场景 | 错误类型 |
|------|---------|
| 无需匹配,静态消息 | `errors.New("message")` |
| 无需匹配,动态消息 | `fmt.Errorf("msg: %v", val)` |
| 需要匹配,静态消息 | `var ErrFoo = errors.New(...)` |
| 需要匹配,动态消息 | 自定义结构体类型 |
| 检查哨兵错误 | `errors.Is(err, ErrFoo)` |
| 提取结构化错误 | `errors.As(err, &target)` |
@@ -1,174 +0,0 @@
# 错误包装参考
本参考涵盖使用 `%v` vs `%w` 的错误包装、放置约定、向错误添加上下文以及日志最佳实践。
---
## 包装错误:%v vs %w
> **建议**:推荐的最佳实践。
`%v` 和 `%w` 的选择会显著影响错误的传播和检查方式。
### 使用 %v 进行简单注释
当你需要以下操作时使用 `%v`:
- 添加上下文但不保留错误链以供程序化检查
- 创建全新的、独立的错误(特别是在 RPC/IPC 等系统边界)
- 向人类记录或显示错误
```go
// 好:%v 在系统边界 — 隐藏内部细节
func (s *Server) SuggestFortune(ctx context.Context, req *pb.Request) (*pb.Response, error) {
if err != nil {
return nil, fmt.Errorf("couldn't find fortune database: %v", err)
}
}
```
### 使用 %w 保留错误链
当你需要调用者以编程方式检查底层错误时使用 `%w`:
```go
// 好:%w 保留错误链以供 errors.Is/errors.As 使用
func (s *Server) internalFunction(ctx context.Context) error {
if err != nil {
return fmt.Errorf("couldn't find remote file: %w", err)
}
}
// 调用者现在可以检查:
if errors.Is(err, fs.ErrNotExist) {
// 处理未找到的情况
}
```
### 何时使用哪种
**使用 %w 的场景**:
- 在添加上下文的同时保留原始错误以供程序化检查
- 你明确记录并测试了所暴露的底层错误
**使用 %v 的场景**:
- 在系统边界(RPC、IPC、存储)转换为规范错误空间
- 向人类记录日志或显示
- 创建隐藏实现细节的独立错误
---
## %w 的放置位置
> **建议**:推荐的最佳实践。
将 `%w` 放在错误字符串的**末尾**,使错误文本反映错误链结构:
```go
// 好:%w 在末尾 — 从最新到最旧打印
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("err2: %w", err1)
err3 := fmt.Errorf("err3: %w", err2)
fmt.Println(err3) // err3: err2: err1
```
```go
// 不好:%w 在开头 — 从最旧到最新打印(令人困惑)
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("%w: err2", err1)
err3 := fmt.Errorf("%w: err3", err2)
fmt.Println(err3) // err1: err2: err3
```
```go
// 不好:%w 在中间 — 不连贯的顺序
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("err2-1 %w err2-2", err1)
err3 := fmt.Errorf("err3-1 %w err3-2", err2)
fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2
```
**模式**:使用 `context message: %w` 的形式
---
## 向错误添加信息
> **建议**:推荐的最佳实践。
### 添加上下文,而非冗余
添加你拥有但调用者/被调用者可能没有的信息。避免重复底层错误已提供的信息:
```go
// 好:添加有意义的上下文
if err := os.Open("settings.txt"); err != nil {
return fmt.Errorf("launch codes unavailable: %v", err)
}
// 输出:launch codes unavailable: open settings.txt: no such file or directory
```
```go
// 不好:重复了文件名
if err := os.Open("settings.txt"); err != nil {
return fmt.Errorf("could not open settings.txt: %v", err)
}
// 输出:could not open settings.txt: open settings.txt: no such file or directory
```
### 不要无目的地注释
如果注释仅表示失败而没有添加信息,直接返回错误:
```go
// 不好:注释没有增加信息
return fmt.Errorf("failed: %v", err)
// 好:直接返回错误
return err
```
---
## 记录错误日志
> **建议**:推荐的最佳实践。
当需要记录错误时,使用 `log/slog`(Go 1.21+)配合结构化键值对和适当的日志级别:
- **`slog.Error`**:保留用于需要调查的可操作问题。
- **`slog.Warn`**:用于可能需要关注但不可立即操作的问题。
- **`slog.Debug`**:用于开发追踪 — 仅在 handler 级别设为 `LevelDebug` 时才输出。
```go
// 好:使用适当级别的结构化日志
for _, q := range queries {
slog.Debug("handling query", "query", q)
q.Run()
}
// 好:在级别检查后保护昂贵的格式化操作
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
slog.Debug("query plan", "explain", q.Explain())
}
// 不好:即使禁用了 debug 日志也会执行昂贵的调用
slog.Debug("query plan", "explain", q.Explain())
```
### 保护敏感信息
注意日志消息中的 PII(个人身份信息)。许多日志接收器不适合存放敏感用户数据。
---
## 快速参考
| 模式 | 指导 |
|------|------|
| `%v` | 在系统边界使用、用于日志记录、隐藏细节 |
| `%w` | 保留错误链以供程序化检查 |
| `%w` 放置 | 始终在末尾:`"context: %w"` |
| 添加上下文 | 添加新信息,不要重复现有信息 |
| 空注释 | 直接返回 `err` 而非 `fmt.Errorf("failed: %v", err)` |
| 日志 | 不要既记录日志又返回;使用适当的日志级别 |
@@ -1,266 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.0.0"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
$SCRIPT_NAME v$VERSION — Check Go code for common error handling anti-patterns
USAGE
bash $SCRIPT_NAME [options] [path]
DESCRIPTION
Scans Go source files for error handling anti-patterns:
- err.Error() used in string comparison (should use errors.Is/As)
- Bare 'return err' without wrapping context
- Errors that are both logged and returned (handle once)
Exits 0 if no issues found, 1 if anti-patterns detected, 2 on error.
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--no-bare-return Skip the bare 'return err' check (high false-positive rate)
--limit N Show at most N results (default: all)
ARGUMENTS
path Directory or file to check (default: current directory)
EXAMPLES
bash $SCRIPT_NAME
bash $SCRIPT_NAME ./pkg/api
bash $SCRIPT_NAME --json .
bash $SCRIPT_NAME --no-bare-return ./internal
EOF
}
JSON_OUTPUT=false
CHECK_BARE_RETURN=true
LIMIT=0
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
--json) JSON_OUTPUT=true; shift ;;
--no-bare-return) CHECK_BARE_RETURN=false; shift ;;
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) TARGET="$1"; shift ;;
esac
done
TARGET="${TARGET:-.}"
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/}"
s="${s//$'\n'/\\n}"
printf '%s' "$s"
}
find_go_files() {
local t="$1"
if [[ -f "$t" ]]; then
echo "$t"
elif [[ -d "$t" ]]; then
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
else
local dir="${t%%/...}"
dir="${dir:-.}"
if [[ -d "$dir" ]]; then
find "$dir" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
else
echo "error: path not found: $t" >&2
exit 2
fi
fi
}
FINDINGS=()
add_finding() {
local file="$1" line="$2" rule="$3" message="$4"
FINDINGS+=("${file}:${line}|${rule}|${message}")
}
# Rule 1: err.Error() in string comparison
check_string_error_comparison() {
local file="$1"
local line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
# Pattern: err.Error() == "..." or err.Error() != "..."
pat='\.Error\(\)[[:space:]]*(==|!=)[[:space:]]*\"'
if [[ "$line" =~ $pat ]]; then
add_finding "$file" "$line_num" "string-error-compare" \
"comparing err.Error() to string; use errors.Is() or errors.As() instead"
fi
# Pattern: strings.Contains(err.Error(), "...")
pat_contains='strings\.Contains\(.*\.Error\(\)'
if [[ "$line" =~ $pat_contains ]]; then
add_finding "$file" "$line_num" "string-error-compare" \
"using strings.Contains on err.Error(); use errors.Is() or errors.As() instead"
fi
# Pattern: "..." == err.Error()
pat='\"[^\"]*\"[[:space:]]*(==|!=)[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*\.Error\(\)'
if [[ "$line" =~ $pat ]]; then
add_finding "$file" "$line_num" "string-error-compare" \
"comparing string to err.Error(); use errors.Is() or errors.As() instead"
fi
done < "$file"
}
# Rule 2: Bare return err (no wrapping)
check_bare_return_err() {
local file="$1"
local line_num=0
local in_error_block=false
while IFS= read -r line; do
line_num=$((line_num + 1))
# Detect if err != nil { block
pat='if[[:space:]]+(.*err[[:space:]]*(!=|==)[[:space:]]*nil|err[[:space:]]*:=)'
if [[ "$line" =~ $pat ]]; then
in_error_block=true
fi
# Check for bare "return err" that is not wrapped
pat='^[[:space:]]*return[[:space:]]+(.*,)?[[:space:]]*err[[:space:]]*$'
if $in_error_block && [[ "$line" =~ $pat ]]; then
# Exclude single-line functions and main error handlers
# Only flag if the return is just "err" (not fmt.Errorf wrapped)
local trimmed
trimmed=$(echo "$line" | sed 's/^[[:space:]]*//')
if [[ "$trimmed" == "return err" ]]; then
add_finding "$file" "$line_num" "bare-return-err" \
"bare 'return err' without wrapping context; consider fmt.Errorf('...: %w', err)"
fi
fi
# Reset error block tracking on closing brace at same indentation
pat_close='^[[:space:]]*\}[[:space:]]*$'
if $in_error_block && [[ "$line" =~ $pat_close ]]; then
in_error_block=false
fi
done < "$file"
}
# Rule 3: Log-and-return (handle errors once)
check_log_and_return() {
local file="$1"
local line_num=0
local prev_lines=()
while IFS= read -r line; do
line_num=$((line_num + 1))
prev_lines+=("$line")
# Keep a small window to detect log followed by return err
if [[ ${#prev_lines[@]} -gt 5 ]]; then
prev_lines=("${prev_lines[@]:1}")
fi
# Check if current line is 'return ... err' and a recent line logged the error
pat='^[[:space:]]*return[[:space:]]+(.*,)?[[:space:]]*err'
if [[ "$line" =~ $pat ]]; then
local window_size=${#prev_lines[@]}
for ((i=0; i<window_size-1; i++)); do
local prev="${prev_lines[$i]}"
# Match log.Print/Printf/Println/Error/Errorf/Warn/Warnf with err
pat_log1='(log\.|logger\.|slog\.)[a-zA-Z]*\(.*[^a-zA-Z]err[^a-zA-Z]'
pat_log2='(log\.|logger\.|slog\.)[a-zA-Z]*\(err[,\)]'
if [[ "$prev" =~ $pat_log1 ]] || \
[[ "$prev" =~ $pat_log2 ]]; then
local log_line=$((line_num - window_size + 1 + i))
add_finding "$file" "$log_line" "log-and-return" \
"error is both logged (line $log_line) and returned (line $line_num); handle errors once"
break
fi
done
fi
done < "$file"
}
FILES=()
while IFS= read -r f; do
[[ -n "$f" ]] && FILES+=("$f")
done < <(find_go_files "$TARGET")
if [[ ${#FILES[@]} -eq 0 ]]; then
if $JSON_OUTPUT; then
echo '{"findings":[],"count":0,"status":"no_go_files"}'
else
echo "No Go files found in: $TARGET"
fi
exit 0
fi
for file in "${FILES[@]}"; do
check_string_error_comparison "$file"
if $CHECK_BARE_RETURN; then
check_bare_return_err "$file"
fi
check_log_and_return "$file"
done
# Truncation
TOTAL=${#FINDINGS[@]}
TRUNCATED=false
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
FINDINGS=("${FINDINGS[@]:0:$LIMIT}")
TRUNCATED=true
fi
if $JSON_OUTPUT; then
echo "{"
echo ' "findings": ['
first=true
for entry in "${FINDINGS[@]+"${FINDINGS[@]}"}"; do
IFS='|' read -r location rule message <<< "$entry"
file="${location%%:*}"
line="${location#*:}"
$first || echo ","
first=false
printf ' {"file":"%s","line":%s,"rule":"%s","message":"%s"}' \
"$(json_escape "$file")" "$line" "$(json_escape "$rule")" "$(json_escape "$message")"
done
echo ""
echo " ],"
printf ' "total": %d,\n' "$TOTAL"
printf ' "truncated": %s\n' "$TRUNCATED"
echo "}"
else
if [[ $TOTAL -eq 0 ]]; then
echo "No error handling anti-patterns found."
exit 0
fi
echo "Error handling anti-patterns found:"
echo ""
for entry in "${FINDINGS[@]}"; do
IFS='|' read -r location rule message <<< "$entry"
printf " %s [%s] %s\n" "$location" "$rule" "$message"
done
if $TRUNCATED; then
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
fi
echo ""
echo "Total: $TOTAL finding(s)"
fi
if [[ $TOTAL -gt 0 ]]; then
exit 1
fi
exit 0
@@ -1,210 +0,0 @@
---
name: go-functional-options
description: Use when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).
license: Apache-2.0
metadata:
sources: "Uber Style Guide"
---
# 函数式选项模式
函数式选项是一种模式,你声明一个不透明的 `Option` 类型,在内部结构体中记录信息。构造函数接受可变数量的这些选项并将其应用于配置结果。
## 何时使用
在以下情况使用函数式选项:
- 构造函数或公共 API 上有 **3 个以上可选参数**
- **可扩展 API**,可能随时间增加新选项
- **良好的调用者体验**很重要(无需传递默认值)
## 模式
### 核心组件
1. **未导出的 `options` 结构体** - 保存所有配置
2. **导出的 `Option` 接口** - 带有未导出的 `apply` 方法
3. **Option 类型** - 实现接口
4. **`With*` 构造函数** - 创建选项
### Option 接口
```go
type Option interface {
apply(*options)
}
```
未导出的 `apply` 方法确保只能使用来自本包的选项。
## 完整实现
```go
package db
import "go.uber.org/zap"
// options 保存打开连接的所有配置。
type options struct {
cache bool
logger *zap.Logger
}
// Option 配置我们如何打开连接。
type Option interface {
apply(*options)
}
// cacheOption 为缓存设置实现 Option(简单类型别名)。
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
// WithCache 启用或禁用缓存。
func WithCache(c bool) Option {
return cacheOption(c)
}
// loggerOption 为日志设置实现 Option(用于指针的结构体)。
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
// WithLogger 设置连接的日志记录器。
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open 创建一个连接。
func Open(addr string, opts ...Option) (*Connection, error) {
// 从默认值开始
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
// 应用所有提供的选项
for _, o := range opts {
o.apply(&options)
}
// 使用 options.cache 和 options.logger...
return &Connection{}, nil
}
```
## 使用示例
### 不使用函数式选项(不好)
```go
// 调用者必须始终提供所有参数,即使是默认值
db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log)
```
### 使用函数式选项(好)
```go
// 只在需要时提供选项
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
addr,
db.WithCache(false),
db.WithLogger(log),
)
```
## 比较:函数式选项 vs 配置结构体
| 方面 | 函数式选项 | 配置结构体 |
|------|-----------|-----------|
| **可扩展性** | 添加新的 `With*` 函数 | 添加新字段(可能破坏兼容性) |
| **默认值** | 内置于构造函数 | 零值或单独的默认值 |
| **调用者体验** | 只指定不同的部分 | 必须构造整个结构体 |
| **可测试性** | 选项可比较 | 结构体比较 |
| **复杂性** | 更多样板代码 | 更简单的设置 |
**优先使用配置结构体的场景**:少于 3 个选项、选项很少变化、所有选项通常一起指定、或仅用于内部 API。
> 在决定使用函数式选项还是配置结构体、设计具有适当默认值的配置结构体 API、或评估复杂构造函数的混合方法时,请阅读 [references/OPTIONS-VS-STRUCTS.md](references/OPTIONS-VS-STRUCTS.md)。
## 为什么不使用闭包?
另一种实现使用闭包:
```go
// 闭包方法(不推荐)
type Option func(*options)
func WithCache(c bool) Option {
return func(o *options) { o.cache = c }
}
```
优先使用接口方法,因为:
1. **可测试性** - 选项可以在测试和 mock 中进行比较
2. **可调试性** - 选项可以实现 `fmt.Stringer`
3. **灵活性** - 选项可以实现额外的接口
4. **可见性** - 选项类型在文档中可见
## 快速参考
```go
// 1. 带有默认值的未导出 options 结构体
type options struct {
field1 Type1
field2 Type2
}
// 2. 导出的 Option 接口,未导出的方法
type Option interface {
apply(*options)
}
// 3. Option 类型 + apply + With* 构造函数
type field1Option Type1
func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }
func WithField1(v Type1) Option { return field1Option(v) }
// 4. 构造函数在默认值之上应用选项
func New(required string, opts ...Option) (*Thing, error) {
o := options{field1: defaultField1, field2: defaultField2}
for _, opt := range opts {
opt.apply(&o)
}
// ...
}
```
### 检查清单
- [ ] `options` 结构体未导出
- [ ] `Option` 接口有未导出的 `apply` 方法
- [ ] 每个选项有 `With*` 构造函数
- [ ] 默认值在应用选项之前设置
- [ ] 必需参数与 `...Option` 分开
## 相关技能
- **接口设计**:在设计 `Option` 接口或选择接口与闭包方法时,参见 [go-interfaces](../go-interfaces/SKILL.md)
- **命名约定**:在命名 `With*` 构造函数、选项类型或未导出的 options 结构体时,参见 [go-naming](../go-naming/SKILL.md)
- **函数设计**:在组织文件中的构造函数或格式化可变参数签名时,参见 [go-functions](../go-functions/SKILL.md)
- **文档**:在记录 `Option` 类型、`With*` 函数或构造函数行为时,参见 [go-documentation](../go-documentation/SKILL.md)
### 外部资源
- [Self-referential functions and the design of options](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) - Rob Pike
- [Functional options for friendly APIs](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) - Dave Cheney
@@ -1,129 +0,0 @@
# 函数式选项 vs 配置结构体
> **来源**:Google 风格指南, Uber 风格指南
函数式选项和配置结构体解决相同的问题 — 构造函数的可选配置 — 但它们有不同的权衡。根据 API 受众、可扩展性需求和复杂性预算来选择。
## 决策框架
```
需要可选配置?
├─ 内部或仅测试 API?
│ └─ 配置结构体(更简单,更少样板代码)
├─ 具有 3 个以上选项的公共 API?
│ └─ 函数式选项(可扩展,向后兼容)
├─ 选项需要校验或有相互依赖?
│ └─ 函数式选项(在 apply 或构造函数中校验)
├─ 所有选项通常一起指定?
│ └─ 配置结构体(一个字面量,无需 With* 仪式)
└─ 选项可能随时间增长?
└─ 函数式选项(添加 With* 不会破坏调用者)
```
## 配置结构体模式
配置结构体将可选参数分组为传递给构造函数的单个结构体。零值作为默认值,或提供 `DefaultConfig()`。
**好**
```go
type Config struct {
Timeout time.Duration // 零 = 无超时
MaxRetry int // 零 = 无重试
Logger *log.Logger // nil = 丢弃
}
func NewClient(addr string, cfg Config) *Client {
if cfg.Logger == nil {
cfg.Logger = log.New(io.Discard, "", 0)
}
return &Client{addr: addr, cfg: cfg}
}
```
```go
c := NewClient("localhost:8080", Config{
Timeout: 5 * time.Second,
MaxRetry: 3,
})
```
**不好** — 在公共 API 中依赖未导出的配置字段:
```go
type config struct { // 未导出:调用者无法构造
timeout time.Duration
}
func NewClient(addr string, cfg config) *Client { ... }
```
### 当零值不适用时
如果零是一个有效的非默认值(例如,超时为 0 表示"无超时",但期望的默认值是 30s),使用指针字段或哨兵值:
```go
type Config struct {
Timeout *time.Duration // nil = 使用默认值(30s),零 = 无超时
}
```
## 比较
| 方面 | 函数式选项 | 配置结构体 |
|------|-----------|-----------|
| **样板代码** | 高(每个选项需要类型 + apply + With*) | 低(一个结构体) |
| **可扩展性** | 添加 `With*` — 无破坏性变更 | 添加字段 — 无破坏性变更 |
| **向后兼容** | 对公共 API 极好 | 好(新字段获得零值) |
| **默认值** | 内置于构造函数 | 零值或 `DefaultConfig()` |
| **校验** | 在 `apply` 或构造函数循环中 | 在接收到结构体后的构造函数中 |
| **可发现性** | `With*` 函数出现在 godoc 中 | 所有字段在一个结构体中可见 |
| **可测试性** | 比较选项或测试构造函数输出 | 比较结构体字面量 |
| **调用者体验** | 只指定与默认值不同的部分 | 必须构造结构体字面量 |
| **零值歧义** | 无 — 未设置的选项不应用 | 可能需要指针字段 |
## 何时优先使用配置结构体
- **内部 API** — 更少的仪式,在调用处更易读
- **少量选项(1-3 个)** — 函数式选项的开销不值得
- **所有选项通常一起设置** — 可变参数风格没有好处
- **不需要校验** — 简单的字段赋值即可
- **选项是数据而非行为** — 结构体字段自然映射
```go
srv := NewServer(Config{
Port: 8080,
TLSCert: "/path/to/cert.pem",
TLSKey: "/path/to/key.pem",
})
```
## 何时优先使用函数式选项
- **公共/库 API** — 调用者不应跟踪内部配置的演变
- **3 个以上选项**,每个都是可选的
- **复杂默认值** — 默认值计算依赖于其他选项
- **按选项校验** — 在 apply 时拒绝无效值
- **选项可能增长** — 新的 `With*` 函数是纯粹增量的
```go
srv := NewServer(
WithPort(8080),
WithTLS("/path/to/cert.pem", "/path/to/key.pem"),
WithLogger(logger),
)
```
## 混合方法
对于同时需要便利性和可扩展性的 API,接受配置结构体用于常见设置,函数式选项用于高级覆盖:
```go
func NewServer(cfg Config, opts ...Option) *Server {
s := &Server{cfg: cfg}
for _, o := range opts {
o.apply(&s.cfg)
}
return s
}
```
谨慎使用 — 它增加了复杂性。每个 API 优先使用一种方法。
-107
View File
@@ -1,107 +0,0 @@
---
name: go-functions
description: Use when organizing functions within a Go file, formatting function signatures, designing return values, or following Printf-style naming conventions. Also use when a user is adding or refactoring any Go function, even if they don't mention function design or signature formatting. Does not cover functional options constructors (see go-functional-options).
license: Apache-2.0
metadata:
sources: "Effective Go, Google Style Guide, Uber Style Guide"
---
# Go 函数设计
> **本技能不适用的场景**:对于函数选项构造函数(`WithTimeout`、`WithLogger`),参见 [go-functional-options](../go-functional-options/SKILL.md)。对于错误返回约定,参见 [go-error-handling](../go-error-handling/SKILL.md)。对于函数和方法的命名,参见 [go-naming](../go-naming/SKILL.md)。
---
## 函数分组与排序
按以下规则组织文件中的函数:
1. 函数按**大致调用顺序**排序
2. 函数**按接收者分组**
3. **导出**函数排在最前面,位于 `struct`/`const`/`var` 定义之后
4. `NewXxx`/`newXxx` 构造函数紧跟在类型定义之后
5. 普通工具函数排在文件末尾
```go
type something struct{ ... }
func newSomething() *something { return &something{} }
func (s *something) Cost() int { return calcCost(s.weights) }
func (s *something) Stop() { ... }
func calcCost(n []int) int { ... }
```
---
## 函数签名
> 在格式化多行签名、包装返回值、缩短调用点或用自定义类型替换裸 bool 参数时,阅读 [references/SIGNATURES.md](references/SIGNATURES.md)。
尽量将签名保持在一行内。当必须换行时,将**所有参数放在各自的行上**并加尾随逗号:
```go
func (r *SomeType) SomeLongFunctionName(
foo1, foo2, foo3 string,
foo4, foo5, foo6 int,
) {
foo7 := bar(foo1)
}
```
为含义不明确的参数添加 `/* name */` 注释,或者更好的做法是用自定义类型替换裸 `bool` 参数。
---
## 接口指针
几乎不需要指向接口的指针。将接口作为值传递——底层数据仍然可以是指针。
```go
// 不好:接口指针
func process(r *io.Reader) { ... }
// 好:传递接口值
func process(r io.Reader) { ... }
```
---
## Printf 与 Stringer
> 在使用 %v/%s/%d 之外的 Printf 动词、实现 fmt.Stringer 或 fmt.GoStringer、编写自定义 Format() 方法或调试 String() 方法中的无限递归时,阅读 [references/PRINTF-STRINGER.md](references/PRINTF-STRINGER.md)。
### Printf 风格函数名
接受格式字符串的函数应以 `f` 结尾,以便 `go vet` 支持。在 `Printf` 调用之外使用格式字符串时,将其声明为 `const`。
在格式化日志或错误消息中的字符串时,优先使用 `%q` 而非手动加引号的 `%s`——它能安全地转义特殊字符并加上引号:
```go
return fmt.Errorf("unknown key %q", key) // 输出:unknown key "foo\nbar"
```
设计具有 3 个以上可选参数的构造函数时,参见 **go-functional-options**。
---
## 快速参考
| 主题 | 规则 |
|------|------|
| 文件排序 | 类型 -> 构造函数 -> 导出 -> 未导出 -> 工具函数 |
| 签名换行 | 所有参数各占一行,加尾随逗号 |
| 裸参数 | 添加 `/* name */` 注释或使用自定义类型 |
| 接口指针 | 几乎不需要;按值传递接口 |
| Printf 函数名 | 以 `f` 结尾以支持 `go vet` |
---
## 相关技能
- **错误返回**:在设计错误返回模式或在多返回值函数中包装错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **命名约定**:在为函数、方法命名或选择 getter/setter 模式时,参见 [go-naming](../go-naming/SKILL.md)
- **函数选项**:在设计具有 3 个以上可选参数的构造函数时,参见 [go-functional-options](../go-functional-options/SKILL.md)
- **格式化原则**:在决定行长度、裸返回或签名格式时,参见 [go-style-core](../go-style-core/SKILL.md)
@@ -1,264 +0,0 @@
# Printf、Stringer 与自定义格式化
Go 的 `fmt` 打印动词、`Stringer` 和 `GoStringer` 接口、自定义 `Format()` 方法以及常见陷阱的深度参考。
---
## Printf 动词
### 通用动词
| 动词 | 用途 |
|------|------|
| `%v` | 默认格式(结构体字段、切片元素) |
| `%+v` | 带字段名的结构体:`{Name:alice Age:30}` |
| `%#v` | Go 语法表示:`main.User{Name:"alice", Age:30}` |
| `%T` | 值的类型:`main.User` |
| `%%` | 字面百分号 |
### 字符串与字节动词
| 动词 | 用途 |
|------|------|
| `%s` | 纯字符串或字节切片 |
| `%q` | 带 Go 语法转义的引号字符串:`"hello\n"` |
| `%x` | 十六进制编码,小写:`68656c6c6f` |
| `%X` | 十六进制编码,大写:`68656C6C6F` |
### 整数动词
| 动词 | 用途 |
|------|------|
| `%d` | 十进制整数 |
| `%b` | 二进制 |
| `%o` | 八进制 |
| `%O` | 带 `0o` 前缀的八进制 |
| `%x` | 十六进制,小写 |
| `%X` | 十六进制,大写 |
### 浮点数动词
| 动词 | 用途 |
|------|------|
| `%f` | 小数点,无指数:`123.456` |
| `%e` | 科学计数法:`1.23456e+02` |
| `%g` | 紧凑格式:大指数用 `%e`,否则用 `%f` |
### 宽度与精度
```go
fmt.Sprintf("%10d", 42) // " 42" (宽度 10,右对齐)
fmt.Sprintf("%-10d", 42) // "42 " (宽度 10,左对齐)
fmt.Sprintf("%.2f", 3.14159) // "3.14" (2 位小数)
fmt.Sprintf("%010d", 42) // "0000000042" (零填充)
```
---
## 使用 `%q` 输出字符串
`%q` 动词在双引号内打印字符串,使空字符串和控制字符可见:
```go
fmt.Printf("value %q looks like English text", someText)
// 不好:手动添加引号
fmt.Printf("value \"%s\" looks like English text", someText)
```
在面向人类的输出中,如果值可能为空或包含控制字符,优先使用 `%q`。
---
## Printf 之外的格式字符串
在 `Printf` 风格调用之外声明格式字符串时,使用 `const`。这样 `go vet` 可以进行静态分析:
```go
// 不好:变量格式字符串——go vet 无法检查
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
// 好:常量格式字符串——go vet 可以验证
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
```
---
## Printf 风格函数的命名
接受格式字符串的函数应以 `f` 结尾。这样 `go vet` 可以自动检查格式字符串:
```go
func Wrapf(err error, format string, args ...any) error
```
如果使用非标准名称,需要告知 `go vet`:
```bash
go vet -printfuncs=wrapf,statusf
```
---
## `fmt.Stringer` 接口
实现 `fmt.Stringer` 来控制类型在 `%v` 和 `%s` 下的显示方式:
```go
type fmt.Stringer interface {
String() string
}
```
```go
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
// fmt.Println(Point{1, 2}) → "(1, 2)"
// fmt.Sprintf("point: %v", p) → "point: (1, 2)"
// fmt.Sprintf("point: %s", p) → "point: (1, 2)"
```
### 何时实现 Stringer
- 类型将出现在日志消息或面向用户的输出中
- 默认的 `%v` 输出(仅字段值)不够有意义
- 需要一种区别于序列化的、对人类友好的表示
---
## `fmt.GoStringer` 接口
实现 `fmt.GoStringer` 来控制 `%#v` 输出。这对于默认 Go 语法表示具有误导性或过于冗长的类型很有用:
```go
type fmt.GoStringer interface {
GoString() string
}
```
```go
type Color struct{ R, G, B uint8 }
func (c Color) GoString() string {
return fmt.Sprintf("Color(%#02x, %#02x, %#02x)", c.R, c.G, c.B)
}
// fmt.Sprintf("%#v", Color{255, 128, 0})
// → "Color(0xff, 0x80, 0x00)" 而非 "main.Color{R:0xff, G:0x80, B:0x00}"
```
`GoString()` 的输出应该是有效的 Go 语法或接近有效语法——它用于调试,而非面向用户的显示。
---
## 使用 `fmt.Formatter` 自定义格式化
要完全控制所有格式动词,实现 `fmt.Formatter`:
```go
type fmt.Formatter interface {
Format(f fmt.State, verb rune)
}
```
```go
type Point struct{ X, Y int }
func (p Point) Format(f fmt.State, verb rune) {
switch verb {
case 'v':
if f.Flag('#') {
// %#v——Go 语法表示
fmt.Fprintf(f, "Point{X: %d, Y: %d}", p.X, p.Y)
return
}
if f.Flag('+') {
// %+v——带字段名的详细格式
fmt.Fprintf(f, "X:%d Y:%d", p.X, p.Y)
return
}
// %v——默认
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
case 's':
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
case 'q':
fmt.Fprintf(f, "%q", p.String())
default:
fmt.Fprintf(f, "%%!%c(Point=%d,%d)", verb, p.X, p.Y)
}
}
```
### `fmt.State` 方法
| 方法 | 返回值 |
|------|--------|
| `Flag(c int) bool` | 标志(`+`、`-`、`#`、`0`、` `)是否设置 |
| `Width() (int, bool)` | 宽度值以及是否指定了宽度 |
| `Precision() (int, bool)` | 精度值以及是否指定了精度 |
| `Write(b []byte) (int, error)` | 写入输出字节 |
仅在 `String()` 不够用时才实现 `fmt.Formatter`——很少需要这样做。常见原因:需要为 `%v`、`%+v`、`%#v` 提供不同输出,或者需要遵循宽度/精度标志。
---
## 无限递归陷阱
**在 `String()` 方法内部对接收者使用 `%s` 或 `%v` 调用 `fmt.Sprintf` 会导致无限递归:**
```go
type MyString string
// BUG:无限递归——Sprintf 调用 String(),String() 又调用 Sprintf...
func (m MyString) String() string {
return fmt.Sprintf("MyString: %s", m) // 崩溃:栈溢出
}
```
修复方法——将接收者转换为其底层类型以打破方法集:
```go
func (m MyString) String() string {
return fmt.Sprintf("MyString: %s", string(m)) // 安全:string 没有 String()
}
```
此陷阱还适用于:
- 底层类型为 string、[]byte 或另一个 Stringer 的类型
- 任何使用 `%s` 或 `%v` 格式化 `self` 的 `String()` 方法
- 使用 `%#v` 格式化 `self` 的 `GoString()` 方法
```go
type IPAddr [4]byte
// BUG:%v 调用 String(),无限递归
func (ip IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
// 这里安全——ip[0] 是 byte(uint8),没有 String() 方法。
// 但如果 ip 是一个包装了 Stringer 的命名类型,就会递归。
}
```
**经验法则**:在 `String()` 内部,永远不要将接收者(或重新转换为自身类型的接收者)传递给 `%s` 或 `%v` 动词。先转换为底层原始类型。
---
## 快速参考
| 主题 | 规则 |
|------|------|
| `%q` | 用于人类可读的字符串输出 |
| `%+v` | 带字段名的结构体 |
| `%#v` | Go 语法表示;通过 `GoStringer` 自定义 |
| 格式字符串存储 | 在 Printf 调用之外声明为 `const` |
| Printf 函数名 | 以 `f` 结尾以支持 `go vet` |
| `Stringer` | 实现 `String() string` 用于 `%v`/`%s` 输出 |
| `GoStringer` | 实现 `GoString() string` 用于 `%#v` 输出 |
| `Formatter` | 实现 `Format(fmt.State, rune)` 以完全控制动词 |
| 递归陷阱 | 永远不要在 `String()` 内部使用 `Sprintf("%s", receiver)`;转换为底层类型 |
@@ -1,168 +0,0 @@
# 函数签名
格式化 Go 函数签名、避免裸参数以及保持调用点可读性的详细规则。
---
## 单行 vs 多行
当签名能轻松放在一行时保持单行。当必须换行时,将**所有参数放在各自的行上**并加尾随逗号:
**不好**——部分换行使对齐变得脆弱:
```go
func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string,
foo4, foo5, foo6 int) {
foo7 := bar(foo1)
}
```
**好**——完全换行,尾随逗号:
```go
func (r *SomeType) SomeLongFunctionName(
foo1, foo2, foo3 string,
foo4, foo5, foo6 int,
) {
foo7 := bar(foo1)
}
```
### 返回值
当返回值也需要换行时,遵循相同的模式:
```go
func (r *SomeType) LongName(
foo1, foo2, foo3 string,
foo4, foo5, foo6 int,
) (
*Result,
error,
) {
// ...
}
```
对于更简单的情况,命名返回值可以与参数右括号在同一行:
```go
func (r *SomeType) LongName(
foo1, foo2, foo3 string,
) (result *Result, err error) {
// ...
}
```
---
## 缩短调用点
提取局部变量,而不是将函数调用拆分到多行:
```go
// 不好:过长的内联调用
result := foo.Call(
somePackage.ComplexFunction(arg1, arg2),
anotherPackage.Transform(data),
defaultOptions,
)
// 好:提取局部变量以提高清晰度
transformed := anotherPackage.Transform(data)
computed := somePackage.ComplexFunction(arg1, arg2)
result := foo.Call(computed, transformed, defaultOptions)
```
这提高了可读性,并使中间值可用于调试。
---
## 避免裸参数
函数调用中的裸参数会降低可读性。为含义不明确的参数添加 C 风格注释:
```go
// 不好:这些布尔值是什么意思?
printInfo("foo", true, true)
// 好:内联注释说明了意图
printInfo("foo", true /* isLocal */, true /* done */)
```
更好的做法是用自定义类型替换裸 `bool` 参数:
```go
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
Pending Status = iota
Done
)
func printInfo(name string, region Region, status Status)
```
### 何时使用每种方法
| 方法 | 时机 |
|------|------|
| C 风格注释 | 快速修复;调用点少;无法修改的第三方 API |
| 自定义类型 | 多个调用点;公开 API;多个 bool/int 参数 |
| 函数选项 | 3 个以上可选参数;参见 [go-functional-options](../../go-functional-options/SKILL.md) |
---
## 分组相关参数
当函数接受多个相同类型的参数时,将它们分组:
```go
// 可接受:将同类型参数分组
func Copy(dst, src string) error
// 可接受:尽管类型相同,但含义不同时分开声明
func Move(source string, destination string) error
```
当参数名称能清楚表明角色时使用分组;当不能清楚表明时使用分开声明。
---
## 方法接收者的位置
接收者放在函数名之前,格式类似于参数:
```go
// 短接收者——放在同一行
func (s *Server) Start(ctx context.Context) error { ... }
// 长接收者类型——如果整行过长则考虑换行
func (h *ComplicatedHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) { ... }
```
参见 [go-naming](../../go-naming/SKILL.md) 了解接收者命名约定(简短的一到两个字母缩写)。
---
## 快速参考
| 主题 | 规则 |
|------|------|
| 单行 | 能放下时保持一行 |
| 多行 | 所有参数各占一行,尾随逗号 |
| 返回值换行 | 与参数相同的模式 |
| 调用点 | 提取局部变量而不是拆分调用 |
| 裸 bool | 添加 `/* name */` 注释或使用自定义类型 |
| 分组参数 | 当名称能清楚表明角色时将同类型分组 |
| 接收者 | 在函数名之前;简短缩写 |
-173
View File
@@ -1,173 +0,0 @@
---
name: go-generics
description: Use when deciding whether to use Go generics, writing generic functions or types, choosing constraints, or picking between type aliases and type definitions. Also use when a user is writing a utility function that could work with multiple types, even if they don't mention generics explicitly. Does not cover interface design without generics (see go-interfaces).
license: Apache-2.0
compatibility: Requires Go 1.18+ (generics were introduced in Go 1.18)
metadata:
sources: "Google Style Guide"
---
# Go 泛型与类型参数
---
## 何时使用泛型
从具体类型开始。只在出现第二种类型时才进行泛化。
### 优先使用泛型的场景
- 多种类型共享相同的逻辑(排序、过滤、map/reduce)
- 否则需要依赖 `any` 和大量的类型切换
- 正在构建可复用的数据结构(并发安全的集合、有序映射)
### 避免使用泛型的场景
- 实践中只有一种类型被实例化
- 接口已经能清晰地表达共享行为
- 泛型代码比特定类型的替代方案更难阅读
> "写代码,不要设计类型。"—— Robert Griesemer 和 Ian Lance Taylor
### 决策流程
```
多种类型是否共享相同的逻辑?
├─ 否 → 使用具体类型
├─ 是 → 它们是否共享一个有用的接口?
│ ├─ 是 → 使用接口
│ └─ 否 → 使用泛型
```
**不好:**
```go
// 过早使用泛型:只会被 int 调用
func Sum[T constraints.Integer | constraints.Float](vals []T) T {
var total T
for _, v := range vals {
total += v
}
return total
}
```
**好:**
```go
func SumInts(vals []int) int {
var total int
for _, v := range vals {
total += v
}
return total
}
```
---
## 类型参数命名
| 名称 | 典型用途 |
|------|----------|
| `T` | 通用类型参数 |
| `K` | 映射键类型 |
| `V` | 映射值类型 |
| `E` | 元素/项目类型 |
对于复杂约束,可以使用简短的描述性名称:
```go
func Marshal[Opts encoding.MarshalOptions](v any, opts Opts) ([]byte, error)
```
---
## 类型别名 vs 类型定义
类型别名(`type Old = new.Name`)很少使用——仅用于包迁移或渐进式 API 重构。
---
## 约束组合
使用 `~`(底层类型)和 `|`(联合)组合约束:
```go
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
func Sum[T Numeric](vals []T) T {
var total T
for _, v := range vals {
total += v
}
return total
}
```
使用 `constraints` 包或 `cmp` 包(Go 1.21+)中的标准约束如 `cmp.Ordered`,而不是自己编写。
> 在编写自定义类型约束、使用 ~ 和 | 组合约束或调试类型推断问题时,阅读 [references/CONSTRAINTS.md](references/CONSTRAINTS.md)。
---
## 常见陷阱
### 不要包装标准库类型
```go
// 不好:泛型包装器增加了复杂度但没有价值
type Set[T comparable] struct {
m map[T]struct{}
}
// 更好:当用法简单时直接使用 map[T]struct{}
seen := map[string]struct{}{}
```
泛型在消除**多个调用点**之间的重复时才能证明其复杂度的合理性。单次使用的泛型只是多余的间接层。
### 不要为接口满足而使用泛型
```go
// 不好:T 仅用于满足接口——直接使用接口即可
func Process[T io.Reader](r T) error { ... }
// 好:直接接受接口
func Process(r io.Reader) error { ... }
```
### 避免过度约束
```go
// 不好:约束比需要的更严格
func Contains[T interface{ ~int | ~string }](slice []T, target T) bool { ... }
// 好:comparable 就足够了
func Contains[T comparable](slice []T, target T) bool { ... }
```
---
## 快速参考
| 主题 | 指导 |
|------|------|
| 何时使用泛型 | 仅在多种类型共享相同逻辑且接口不够用时 |
| 起点 | 先写具体代码;之后再泛化 |
| 命名 | 单个大写字母(`T`、`K`、`V`、`E`) |
| 类型别名 | 相同类型,替代名称;仅用于迁移 |
| 约束组合 | 使用 `~` 表示底层类型,`|` 表示联合;优先使用 `cmp.Ordered` 而非自定义 |
| 常见陷阱 | 不要对单次使用的代码或接口已足够时使用泛型 |
---
## 相关技能
- **接口 vs 泛型**:在决定接口是否已经能表达共享行为而无需泛型时,参见 [go-interfaces](../go-interfaces/SKILL.md)
- **类型声明**:在定义新类型、类型别名或在类型定义和别名之间选择时,参见 [go-declarations](../go-declarations/SKILL.md)
- **文档化泛型 API**:在为泛型函数编写文档注释和可运行示例时,参见 [go-documentation](../go-documentation/SKILL.md)
- **命名类型参数**:在为类型参数或约束接口选择名称时,参见 [go-naming](../go-naming/SKILL.md)
@@ -1,169 +0,0 @@
# Go 泛型中的类型约束
> **来源**:Google Go 风格指南、Go 语言规范
约束定义了类型参数支持的操作。选择满足函数需求的最窄约束——不要更多。
---
## 内置约束
> **规范**:在自行编写约束之前,优先使用标准约束。
| 约束 | 含义 |
|------|------|
| `any` | `interface{}` 的别名;对类型没有要求 |
| `comparable` | 支持 `==` 和 `!=`;映射键所必需 |
| `cmp.Ordered` | 支持 `<`、`<=`、`>=`、`>`(Go 1.21+,替代 `constraints.Ordered`) |
在新代码中优先使用 `cmp.Ordered`(来自 `cmp` 包),而不是已弃用的 `golang.org/x/exp/constraints.Ordered`。
---
## `~` 运算符(底层类型)
> **建议**:当你想接受基于原始类型构建的命名类型时使用 `~`。
`~T` 语法匹配任何**底层类型**为 `T` 的类型。没有 `~` 时,只有精确的类型匹配。
```go
type Celsius float64
type ExactFloat interface{ float64 } // 拒绝 Celsius
type AnyFloat64 interface{ ~float64 } // 接受 Celsius
```
当调用者可能基于基础类型定义命名类型时使用 `~`。仅在需要限制为精确的内置类型时才省略 `~`。
---
## 组合与编写约束
> **建议**:仅在没有标准约束适用时才定义自定义约束。
使用 `|` 组合类型并嵌入约束来组合它们:
```go
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
type Addable interface {
Numeric | ~string // 数字和字符串拼接
}
```
约束可以同时要求方法和类型元素:
```go
type Stringer interface {
comparable
String() string
}
```
满足 `Stringer` 的类型必须是可比较的 **并且** 具有 `String()` 方法。
---
## 避免过度约束
> **规范**:使用支持所执行操作的最小约束。
**不好**
```go
// 只使用了 == 但限制为 int 和 string
func Contains[T interface{ ~int | ~string }](s []T, v T) bool { ... }
```
**好**
```go
// comparable 是 == 的最小约束
func Contains[T comparable](s []T, v T) bool { ... }
```
过度约束限制了复用,并迫使调用者绕过实现中根本不需要的限制。
## 类型推断
> **建议**:当类型明确时让编译器推断类型参数。
编译器从函数参数推断类型参数:
```go
result := slices.Contains[string](names, "alice") // 显式——不必要
result := slices.Contains(names, "alice") // 推断——推荐
```
仅在以下情况下才显式提供类型参数:没有可用于推断的函数参数、推断的类型不正确(例如无类型常量提升为错误的类型),或者将类型显式展示出来有助于可读性。
---
## 常见陷阱
### 接口已足够时不要使用泛型
> **规范**:来自 Google 风格指南——当类型共享一个有用的统一接口时,优先使用接口。
**不好**
```go
// T 仅用于满足 io.Reader——直接使用接口即可
func Process[T io.Reader](r T) error { ... }
```
**好**
```go
func Process(r io.Reader) error { ... }
```
如果约束是单个已有接口,直接接受该接口。
### 不要泛型地包装标准库类型
> **建议**:单次使用的泛型只是多余的间接层。
**不好**
```go
type Set[T comparable] struct{ m map[T]struct{} } // 永远只是 Set[string]
```
**好**
```go
seen := map[string]struct{}{} // 对于单次实例化直接使用 map
```
泛型在消除**多个调用点**之间的重复时才能证明其复杂度的合理性。如果只使用一种类型,从具体类型开始。
### 方法集与类型约束
你只能调用约束允许的操作:
**不好**
```go
func Stringify[T any](v T) string {
return v.String() // 编译错误:any 没有 String()
}
```
**好**
```go
func Stringify[T fmt.Stringer](v T) string {
return v.String()
}
```
---
## 快速参考
| 主题 | 指导 |
|------|------|
| 默认约束 | `any`——不需要对 T 进行任何操作时使用 |
| 相等性检查 | `comparable`——`==`、`!=` 和映射键所必需 |
| 排序 | `cmp.Ordered`(Go 1.21+)用于 `<`、`>` 比较 |
| 命名类型 | 使用 `~T` 接受底层类型为 T 的类型 |
| 联合类型 | 使用 `\|` 组合——例如 `~int \| ~float64` |
| 自定义约束 | 定义为包含类型元素和/或方法的接口 |
| 类型推断 | 当编译器可以推断时省略类型参数 |
| 最小约束 | 使用函数实际需要的最窄约束 |
-151
View File
@@ -1,151 +0,0 @@
---
name: go-interfaces
description: Use when defining or implementing Go interfaces, designing abstractions, creating mockable boundaries for testing, or composing types through embedding. Also use when deciding whether to accept an interface or return a concrete type, or using type assertions or type switches, even if the user doesn't explicitly mention interfaces. Does not cover generics-based polymorphism (see go-generics).
license: Apache-2.0
metadata:
sources: "Effective Go, Google Style Guide, Uber Style Guide"
allowed-tools: Bash(bash:*)
---
# Go 接口与组合
## 可用脚本
- **`scripts/check-interface-compliance.sh`**——查找缺少编译时合规性检查(`var _ I = (*T)(nil)`)的导出接口。运行 `bash scripts/check-interface-compliance.sh --help` 查看选项。
---
## 接受接口,返回具体类型
接口属于**消费**值的包,而不是**实现**值的包。从构造函数返回具体类型(通常是指针或结构体),这样可以在不重构的情况下添加新方法。
```go
// 好:消费者定义自己需要的接口
package consumer
type Thinger interface { Thing() bool }
func Foo(t Thinger) string { ... }
```
```go
// 好:生产者返回具体类型
package producer
type Thinger struct{ ... }
func (t Thinger) Thing() bool { ... }
func NewThinger() Thinger { return Thinger{ ... } }
```
```go
// 不好:生产者定义并返回自己的接口
package producer
type Thinger interface { Thing() bool }
type defaultThinger struct{ ... }
func NewThinger() Thinger { return defaultThinger{ ... } }
```
**不要在接口被使用之前定义它。** 如果没有现实的使用示例,很难判断接口是否真的有必要。
---
## 通用性:隐藏实现,暴露接口
如果一个类型仅用于实现某个接口,且没有该接口之外的导出方法,则从构造函数返回接口以隐藏实现:
```go
func NewHash() hash.Hash32 {
return &myHash{} // 未导出的类型
}
```
好处:实现可以在不影响调用者的情况下更改,替换算法只需更改构造函数调用。
---
## 类型断言:Comma-Ok 模式
不进行检查的话,失败的断言会导致运行时 panic。始终使用 comma-ok 模式进行安全测试:
```go
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
}
```
检查值是否实现了某个接口:
```go
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v implements json.Marshaler\n", val)
}
```
---
## 类型切换
重用变量名是惯用做法(`t := t.(type)`)——变量在每个 case 分支中拥有正确的类型。当 case 列出多个类型(`case int, int64:`)时,变量拥有接口类型。
---
## 嵌入
避免在公开结构体中嵌入类型——内部类型的完整方法集将成为你公开 API 的一部分。改用未导出的字段。
> 在使用结构体嵌入进行组合、重写嵌入方法、解决名称冲突、应用 HandlerFunc 适配器模式或决定是否在公开 API 类型中使用嵌入时,阅读 [references/EMBEDDING.md](references/EMBEDDING.md)。
---
## 接口满足检查
使用空标识符赋值在编译时验证类型是否实现了接口:
```go
var _ json.Marshaler = (*RawMessage)(nil)
```
如果 `*RawMessage` 没有实现 `json.Marshaler`,这会导致编译错误。
在以下情况下使用此模式:
- 没有能自动验证接口的静态转换
- 类型必须满足接口才能正确运行(例如自定义 JSON 序列化)
- 接口更改应该导致编译失败,而不是静默降级
**不要**为每个接口都添加这些检查——仅在没有其他静态转换能捕获错误时才使用。
> **验证**:在定义接口或实现后,运行 `bash scripts/check-interface-compliance.sh` 验证所有具体类型都有编译时的 `var _ I = (*T)(nil)` 检查。
---
## 接收者类型
如果不确定,使用指针接收者。不要在单个类型上混合接收者类型——如果任何方法需要指针,则所有方法都使用指针。仅在小型不可变类型(`Point`、`time.Time`)或基本类型上使用值接收者。
> 在为新类型决定使用指针接收者还是值接收者时,特别是对于包含 sync 原语或大型结构体的类型,阅读 [references/RECEIVER-TYPE.md](references/RECEIVER-TYPE.md)。
---
## 快速参考
| 概念 | 模式 | 说明 |
|------|------|------|
| 消费者拥有接口 | 在使用处定义接口 | 不在实现包中 |
| 安全类型断言 | `v, ok := x.(Type)` | 返回零值 + false |
| 类型切换 | `switch v := x.(type)` | 变量在每个 case 中拥有正确类型 |
| 接口嵌入 | `type RW interface { Reader; Writer }` | 方法的并集 |
| 结构体嵌入 | `type S struct { *T }` | 提升 T 的方法 |
| 接口检查 | `var _ I = (*T)(nil)` | 编译时验证 |
| 通用性 | 从构造函数返回接口 | 隐藏实现 |
---
## 相关技能
- **接口命名**:在为接口命名(`-er` 后缀约定)或选择接收者名称时,参见 [go-naming](../go-naming/SKILL.md)
- **错误类型**:在实现 `error` 接口、自定义错误类型或 `errors.As` 匹配时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **泛型 vs 接口**:在决定是否需要泛型或接口是否已足够时,参见 [go-generics](../go-generics/SKILL.md)
- **函数选项**:在使用基于接口的 Option 模式实现灵活构造函数时,参见 [go-functional-options](../go-functional-options/SKILL.md)
- **编译时检查**:在 API 边界添加 `var _ I = (*T)(nil)` 满足检查时,参见 [go-defensive](../go-defensive/SKILL.md)
@@ -1,138 +0,0 @@
# Go 中的嵌入模式
> **来源**:Effective Go、Uber 风格指南
Go 使用嵌入来实现组合而非继承。嵌入将内部类型的方法提升到外部类型,自动满足接口。
## 接口嵌入
通过嵌入来组合接口:
```go
type ReadWriter interface {
Reader
Writer
}
```
`ReadWriter` 既能做 `Reader` 能做的事,*也能*做 `Writer` 能做的事。接口中只能嵌入接口。
## 结构体嵌入
嵌入将内部类型的方法提升到外部类型,无需显式转发。
```go
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
```
通过嵌入,`bufio.ReadWriter` 自动满足 `io.Reader`、`io.Writer` 和 `io.ReadWriter`。
混合使用嵌入字段和命名字段:
```go
type Job struct {
Command string
*log.Logger
}
job.Println("starting now...")
job.Logger.SetPrefix("Job: ")
```
## 方法重写
在外部类型上定义方法以重写提升的方法:
```go
func (job *Job) Printf(format string, args ...any) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
```
外部方法优先——对 `job.Printf(...)` 的调用会调用外部方法,而嵌入方法仍可通过 `job.Logger.Printf(...)` 访问。
## 嵌入 vs 子类化
当调用嵌入方法时,接收者是**内部**类型,而非外部类型。嵌入类型不知道自己被嵌入——不存在类似于 `this` 或 `super` 的引用指向包含它的类型。
```go
type Base struct{}
func (b *Base) Name() string { return "Base" }
type Derived struct{ Base }
d := Derived{}
d.Name() // 返回 "Base",而非 "Derived"
```
## 名称冲突解决
1. **外部隐藏内部**——外部类型上的字段或方法会遮蔽嵌入类型在同名位置提升的字段或方法
2. **同级冲突是错误**——如果两个同深度的嵌入类型提升了相同的名称,则为编译错误(除非该名称从未被访问)
```go
type A struct{}
func (A) Hello() string { return "A" }
type B struct{}
func (B) Hello() string { return "B" }
type C struct {
A
B
}
// c.Hello() // 编译错误:选择器不明确
c.A.Hello() // 可以:显式消歧
```
## 不要在公开结构体中嵌入
嵌入将内部类型的完整方法集暴露为你的公开 API 的一部分。这带来了维护负担:嵌入类型方法的更改会破坏 API 的兼容性保证。
**不好**
```go
type SMap struct {
sync.Mutex // Lock 和 Unlock 现在是 SMap API 的一部分
data map[string]string
}
```
**好**
```go
type SMap struct {
mu sync.Mutex // 未导出的字段——实现细节
data map[string]string
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
}
```
例外:在测试类型和 API 稳定性无关紧要的内部结构体中,嵌入是可以接受的。
## HandlerFunc 适配器模式
方法可以在任何命名类型上定义,不仅仅是结构体。`http.HandlerFunc` 模式将普通函数转换为接口实现:
```go
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
```
任何具有正确签名的函数都可以成为 HTTP 处理器:
```go
http.Handle("/args", http.HandlerFunc(ArgServer))
```
这种适配器模式在需要让独立函数满足单方法接口时非常有用。
@@ -1,68 +0,0 @@
# 接收者类型:指针 vs 值
> **建议**:Go Wiki CodeReviewComments
选择在方法上使用值接收者还是指针接收者可能很困难。**如果不确定,使用指针**,但有时值接收者也是合理的。
## 何时使用指针接收者
- **方法修改接收者**:接收者必须是指针
- **接收者包含 sync.Mutex 或类似类型**:必须使用指针以避免复制
- **大型结构体或数组**:指针接收者更高效。如果将所有元素作为参数传递感觉太大,那对值接收者来说也太大了
- **并发或被调方法可能修改**:如果更改必须对原始接收者可见,则必须使用指针
- **元素是指向可变内容的指针**:优先使用指针接收者使意图更清晰
## 何时使用值接收者
- **小型不变的结构体或基本类型**:值接收者以提高效率
- **Map、func 或 chan**:不要对它们使用指针
- **不重新切片/重新分配的切片**:如果方法不重新切片或重新分配切片,不要使用指针
- **没有可变字段的小型值类型**:像 `time.Time` 这样没有可变字段且没有指针的类型适合作为值接收者
- **简单基本类型**:`int`、`string` 等
```go
// 值接收者:小型、不可变类型
type Point struct {
X, Y float64
}
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// 指针接收者:方法修改接收者
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
// 指针接收者:包含 sync.Mutex
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
```
## 一致性规则
**不要混合接收者类型**。为类型上所有可用的方法统一选择指针或结构体类型。如果任何方法需要指针接收者,则所有方法都使用指针接收者。
```go
// 好:一致的指针接收者
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) (int, error) { /* ... */ }
func (b *Buffer) Read(p []byte) (int, error) { /* ... */ }
func (b *Buffer) Len() int { return len(b.data) }
// 不好:混合接收者类型
func (b Buffer) Len() int { return len(b.data) } // 不一致
```
@@ -1,224 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.0.0"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
$SCRIPT_NAME v$VERSION — Check for missing compile-time interface compliance verifications
USAGE
bash $SCRIPT_NAME [options] [path]
DESCRIPTION
Scans Go files for exported interface definitions and checks whether each
has a corresponding compile-time compliance assertion like:
var _ MyInterface = (*MyImpl)(nil)
var _ MyInterface = MyImpl{}
Reports interfaces that lack such compile-time checks. This helps catch
interface drift at compile time instead of runtime.
Exits 0 if all interfaces are verified, 1 if missing checks found, 2 on error.
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--include-test Also scan _test.go files for compliance checks
--limit N Show at most N results (default: all)
ARGUMENTS
path Directory to scan (default: current directory)
EXAMPLES
bash $SCRIPT_NAME
bash $SCRIPT_NAME ./pkg/storage
bash $SCRIPT_NAME --json .
bash $SCRIPT_NAME --include-test ./internal
EOF
}
JSON_OUTPUT=false
INCLUDE_TEST=false
LIMIT=0
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
--json) JSON_OUTPUT=true; shift ;;
--include-test) INCLUDE_TEST=true; shift ;;
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) TARGET="$1"; shift ;;
esac
done
TARGET="${TARGET:-.}"
if [[ ! -d "$TARGET" && ! -f "$TARGET" ]]; then
# Handle ./... patterns
dir="${TARGET%%/...}"
dir="${dir:-.}"
if [[ ! -d "$dir" ]]; then
echo "error: path not found: $TARGET" >&2
exit 2
fi
TARGET="$dir"
fi
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/}"
s="${s//$'\n'/\\n}"
printf '%s' "$s"
}
# Collect all Go source files
find_go_files() {
local t="$1"
if $INCLUDE_TEST; then
find "$t" -name '*.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
else
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
fi
}
# Collect all Go files (including tests) for checking compliance vars
find_all_go_files() {
find "$1" -name '*.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
}
# Step 1: Find all exported interface definitions
IFACE_NAMES=()
IFACE_LOCATIONS=()
while IFS= read -r file; do
[[ -n "$file" ]] || continue
line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
# Match: type ExportedName interface {
pat='^[[:space:]]*type[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]+interface[[:space:]]*\{'
if [[ "$line" =~ $pat ]]; then
iface_name="${BASH_REMATCH[1]}"
IFACE_NAMES+=("$iface_name")
IFACE_LOCATIONS+=("$file:$line_num")
fi
done < "$file"
done < <(find_go_files "$TARGET")
if [[ ${#IFACE_NAMES[@]} -eq 0 ]]; then
if $JSON_OUTPUT; then
echo '{"interfaces":[],"missing":[],"count_interfaces":0,"count_missing":0}'
else
echo "No exported interfaces found in: $TARGET"
fi
exit 0
fi
# Step 2: Scan all Go files (including tests) for compliance checks
# Pattern: var _ InterfaceName = ...
ALL_GO_FILES=()
while IFS= read -r f; do
[[ -n "$f" ]] && ALL_GO_FILES+=("$f")
done < <(find_all_go_files "$TARGET")
MISSING=()
for ((i=0; i<${#IFACE_NAMES[@]}; i++)); do
iface_name="${IFACE_NAMES[$i]}"
location="${IFACE_LOCATIONS[$i]}"
# Look for: var _ InterfaceName = (various patterns)
if ! grep -qlE "var[[:space:]]+_[[:space:]]+${iface_name}[[:space:]]*=" \
"${ALL_GO_FILES[@]}" 2>/dev/null; then
MISSING+=("${iface_name}|${location}")
fi
done
# Sort for stable output
IFS=$'\n' MISSING=($(sort <<<"${MISSING[*]}")); unset IFS
# Truncation
TOTAL=${#MISSING[@]}
TRUNCATED=false
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
MISSING=("${MISSING[@]:0:$LIMIT}")
TRUNCATED=true
fi
# Output results
if $JSON_OUTPUT; then
echo "{"
echo ' "interfaces": ['
first=true
SORTED_INDICES=()
for ((i=0; i<${#IFACE_NAMES[@]}; i++)); do
SORTED_INDICES+=("$i|${IFACE_NAMES[$i]}")
done
IFS=$'\n' SORTED_INDICES=($(sort -t'|' -k2 <<<"${SORTED_INDICES[*]}")); unset IFS
for entry in "${SORTED_INDICES[@]}"; do
i="${entry%%|*}"
iface_name="${IFACE_NAMES[$i]}"
location="${IFACE_LOCATIONS[$i]}"
file="${location%%:*}"
line="${location#*:}"
$first || echo ","
first=false
printf ' {"name":"%s","file":"%s","line":%s}' "$(json_escape "$iface_name")" "$(json_escape "$file")" "$line"
done
echo ""
echo " ],"
echo ' "missing": ['
first=true
for entry in "${MISSING[@]+"${MISSING[@]}"}"; do
IFS='|' read -r name location <<< "$entry"
file="${location%%:*}"
line="${location#*:}"
$first || echo ","
first=false
printf ' {"name":"%s","file":"%s","line":%s}' "$(json_escape "$name")" "$(json_escape "$file")" "$line"
done
echo ""
echo " ],"
printf ' "count_interfaces": %d,\n' "${#IFACE_NAMES[@]}"
printf ' "count_missing": %d,\n' "$TOTAL"
printf ' "truncated": %s\n' "$TRUNCATED"
echo "}"
else
echo "Exported interfaces found: ${#IFACE_NAMES[@]}"
echo ""
if [[ $TOTAL -eq 0 ]]; then
echo "All interfaces have compile-time compliance checks."
exit 0
fi
echo "Missing compile-time compliance checks:"
echo ""
for entry in "${MISSING[@]}"; do
IFS='|' read -r name location <<< "$entry"
printf " %s interface '%s' has no 'var _ %s = ...' assertion\n" "$location" "$name" "$name"
done
if $TRUNCATED; then
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
fi
echo ""
echo "Add compile-time checks like:"
echo " var _ MyInterface = (*MyImpl)(nil)"
echo ""
echo "Total: $TOTAL interface(s) missing verification"
fi
if [[ $TOTAL -gt 0 ]]; then
exit 1
fi
exit 0
-209
View File
@@ -1,209 +0,0 @@
---
name: go-linting
description: Use when setting up linting for a Go project, configuring golangci-lint, or adding Go checks to a CI/CD pipeline. Also use when starting a new Go project and deciding which linters to enable, even if the user only asks about "code quality" or "static analysis" without mentioning specific linter names. Does not cover code review process (see go-code-review).
license: Apache-2.0
metadata:
sources: "Uber Style Guide"
allowed-tools: Bash(bash:*)
---
# Go Lint
## 核心原则
比任何"推荐"的 linter 集合更重要的是:**在整个代码库中一致地进行 lint**。
一致的 lint 有助于捕获常见问题,并在不过度限制的情况下建立高标准的代码质量。
---
## 设置步骤
1. 使用下面的配置创建 `.golangci.yml`
2. 运行 `golangci-lint run ./...`
3. 如果出现错误,按类别逐一修复(先格式化,再 vet,再风格)
4. 重新运行直到通过
---
## 最低推荐 Linter
这些 linter 能捕获最常见的问题,同时保持高质量标准:
| Linter | 用途 |
|--------|------|
| [errcheck](https://github.com/kisielk/errcheck) | 确保错误被处理 |
| [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) | 格式化代码和管理导入 |
| [revive](https://github.com/mgechev/revive) | 常见风格错误(golint 的现代替代品) |
| [govet](https://pkg.go.dev/cmd/vet) | 分析代码中的常见错误 |
| [staticcheck](https://staticcheck.dev) | 各种静态分析检查 |
> **注意**:`revive` 是现已弃用的 `golint` 的现代、更快的替代品。
---
## Lint 运行器:golangci-lint
使用 [golangci-lint](https://github.com/golangci/golangci-lint) 作为你的 lint 运行器。参见 uber-go/guide 的 [示例 .golangci.yml](https://github.com/uber-go/guide/blob/master/.golangci.yml)。
---
## 示例配置
> 在创建新的 `.golangci.yml` 或将现有配置与推荐基线进行比较时,参见 `assets/golangci.yml`。
在项目根目录创建 `.golangci.yml`:
```yaml
linters:
enable:
- errcheck
- goimports
- revive
- govet
- staticcheck
linters-settings:
goimports:
local-prefixes: github.com/your-org/your-repo
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: error-return
- name: error-strings
- name: exported
run:
timeout: 5m
```
### 运行
```bash
# 安装
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# 运行所有 linter
golangci-lint run
# 对特定路径运行
golangci-lint run ./pkg/...
```
---
## 额外推荐的 Linter
除了最低集合之外,在生产项目中可以考虑以下 linter:
| Linter | 用途 | 何时启用 |
|--------|------|----------|
| [gosec](https://github.com/securego/gosec) | 安全漏洞检测 | 处理用户输入的服务始终启用 |
| [ineffassign](https://github.com/gordonklaus/ineffassign) | 检测无效赋值 | 始终——捕获死代码 |
| [misspell](https://github.com/client9/misspell) | 纠正注释/字符串中的常见拼写错误 | 始终 |
| [gocyclo](https://github.com/fzipp/gocyclo) | 圈复杂度阈值 | 当函数超过约 15 的复杂度时 |
| [exhaustive](https://github.com/nishanths/exhaustive) | 确保 switch 覆盖所有枚举值 | 使用 iota 枚举时 |
| [bodyclose](https://github.com/timakin/bodyclose) | 检测未关闭的 HTTP 响应体 | HTTP 客户端代码始终启用 |
---
## Nolint 指令
在抑制 lint 发现时,始终说明原因:
```go
//nolint:errcheck // 即发即忘的日志;错误不可操作
_ = logger.Sync()
```
规则:
- 使用 `//nolint:lintername`——永远不要使用裸 `//nolint`
- 将注释放在与发现相同的行
- 在 `//` 之后包含理由说明
---
## CI/CD 集成
### GitHub Actions
```yaml
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- uses: golangci/golangci-lint-action@v6
with:
version: latest
```
### Pre-commit Hook
```bash
#!/bin/sh
# .git/hooks/pre-commit
golangci-lint run --new-from-rev=HEAD~1
```
使用 `--new-from-rev` 只对更改的代码进行 lint,保持快速反馈循环。
---
## 可用脚本
- **`scripts/setup-lint.sh`**——生成 `.golangci.yml` 并运行初始 lint
```bash
bash scripts/setup-lint.sh github.com/your-org/your-repo
bash scripts/setup-lint.sh --force github.com/your-org/your-repo # 覆盖现有配置
bash scripts/setup-lint.sh --dry-run # 预览配置
bash scripts/setup-lint.sh --json # 结构化输出
```
> **验证**:在生成 `.golangci.yml` 后,运行 `golangci-lint run ./...` 验证配置有效并产生预期输出。如果因配置错误而失败,修复后重试。
> `scripts/setup-lint.sh` 生成**最低**配置(5 个核心 linter)。
> 对于已有项目,使用 `assets/golangci.yml` 作为起点——
> 它增加了 gosec、ineffassign、misspell、gocyclo 和 bodyclose。
---
## 快速参考
| 任务 | 命令/操作 |
|------|-----------|
| 安装 golangci-lint | `go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest` |
| 运行 linter | `golangci-lint run` |
| 对路径运行 | `golangci-lint run ./pkg/...` |
| 配置文件 | 项目根目录的 `.golangci.yml` |
| CI 集成 | 在管道中运行 `golangci-lint run` |
| Nolint 指令 | `//nolint:name // 原因`——永远不要使用裸 `//nolint` |
| CI 集成 | 使用 `golangci/golangci-lint-action` 用于 GitHub Actions |
| Pre-commit | `golangci-lint run --new-from-rev=HEAD~1` |
### Linter 选择指南
| 当你需要... | 使用 |
|-------------|------|
| 错误处理覆盖率 | errcheck |
| 导入格式化 | goimports |
| 风格一致性 | revive |
| Bug 检测 | govet、staticcheck |
| 以上全部 | golangci-lint 配合配置 |
---
## 相关技能
- **风格基础**:在解决 linter 执行的风格问题(格式化、嵌套、命名)时,参见 [go-style-core](../go-style-core/SKILL.md)
- **代码审查**:在将 linter 输出与手动审查清单结合使用时,参见 [go-code-review](../go-code-review/SKILL.md)
- **错误处理**:在 errcheck 标记未处理的错误并需要决定如何处理时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **测试**:在 CI 管道中将 linter 与测试一起运行时,参见 [go-testing](../go-testing/SKILL.md)
@@ -1,31 +0,0 @@
run:
timeout: 5m
linters:
enable:
# Minimum recommended
- errcheck
- goimports
- revive
- govet
- staticcheck
# Additional recommended
- gosec
- ineffassign
- misspell
- gocyclo
- bodyclose
linters-settings:
goimports:
local-prefixes: "" # Set to your module path
revive:
rules:
- name: exported
gocyclo:
min-complexity: 15
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
@@ -1,172 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.0.0"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
$SCRIPT_NAME v$VERSION — Generate .golangci.yml and run initial lint
USAGE
bash $SCRIPT_NAME [options] [local-prefix]
DESCRIPTION
Creates a .golangci.yml with a curated set of linters (errcheck,
goimports, revive, govet, staticcheck) and runs golangci-lint.
If local-prefix is provided, configures goimports to group local
imports separately.
Exits 0 if lint passes, 1 if lint issues found, 2 on error.
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--force Overwrite existing .golangci.yml
--dry-run Print generated config to stdout without writing
--limit N Max lint issue lines in JSON output (default: 50, 0 = unlimited)
ARGUMENTS
local-prefix Module path prefix for goimports grouping
(e.g., github.com/myorg/myrepo)
EXAMPLES
bash $SCRIPT_NAME
bash $SCRIPT_NAME github.com/myorg/myrepo
bash $SCRIPT_NAME --force github.com/myorg/myrepo
bash $SCRIPT_NAME --dry-run github.com/myorg/myrepo
bash $SCRIPT_NAME --json
bash $SCRIPT_NAME --json --limit 20
EOF
}
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/}"
s="${s//$'\n'/\\n}"
printf '%s' "$s"
}
JSON_OUTPUT=false
FORCE=false
DRY_RUN=false
LIMIT=50
LOCAL_PREFIX=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
--json) JSON_OUTPUT=true; shift ;;
--force) FORCE=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) LOCAL_PREFIX="$1"; shift ;;
esac
done
generate_config() {
cat <<'YAML'
linters:
enable:
- errcheck
- goimports
- revive
- govet
- staticcheck
linters-settings:
YAML
if [[ -n "$LOCAL_PREFIX" ]]; then
cat <<YAML
goimports:
local-prefixes: ${LOCAL_PREFIX}
YAML
fi
cat <<'YAML'
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: error-return
- name: error-strings
- name: exported
run:
timeout: 5m
YAML
}
if $DRY_RUN; then
generate_config
exit 0
fi
CONFIG_PATH=".golangci.yml"
if [[ -f "$CONFIG_PATH" ]] && ! $FORCE; then
echo "error: $CONFIG_PATH already exists (use --force to overwrite)" >&2
exit 2
fi
generate_config > "$CONFIG_PATH"
LINT_OUTPUT=""
LINT_EXIT=0
if ! command -v golangci-lint &>/dev/null; then
echo "error: golangci-lint is not installed" >&2
exit 2
fi
LINT_OUTPUT=$(golangci-lint run ./... 2>&1) || LINT_EXIT=$?
if $JSON_OUTPUT; then
LINT_TRUNCATED=false
LINT_DISPLAY="$LINT_OUTPUT"
if [[ $LIMIT -gt 0 && -n "$LINT_OUTPUT" ]]; then
LINT_ARR=()
while IFS= read -r line; do
LINT_ARR+=("$line")
done <<< "$LINT_OUTPUT"
if [[ ${#LINT_ARR[@]} -gt $LIMIT ]]; then
LINT_DISPLAY=""
for (( i=0; i<LIMIT; i++ )); do
[[ -n "$LINT_DISPLAY" ]] && LINT_DISPLAY+=$'\n'
LINT_DISPLAY+="${LINT_ARR[$i]}"
done
LINT_TRUNCATED=true
fi
fi
LINT_ESC="$(json_escape "$LINT_DISPLAY")"
CONFIG_ESC="$(json_escape "$CONFIG_PATH")"
PREFIX_ESC="$(json_escape "$LOCAL_PREFIX")"
CREATED=true
HAS_ISSUES=$( [[ $LINT_EXIT -ne 0 ]] && echo true || echo false )
TRUNC_FIELD=""
$LINT_TRUNCATED && TRUNC_FIELD=',"truncated":true'
cat <<EOF
{"config_path":"$CONFIG_ESC","local_prefix":"$PREFIX_ESC","created":$CREATED,"lint_issues":$HAS_ISSUES,"lint_output":"$LINT_ESC"$TRUNC_FIELD}
EOF
else
echo "Created $CONFIG_PATH"
if [[ $LINT_EXIT -ne 0 ]]; then
echo ""
echo "$LINT_OUTPUT"
echo ""
echo "Lint issues found — fix them category by category (formatting first, then vet, then style)."
else
echo "golangci-lint: all clean."
fi
fi
if [[ $LINT_EXIT -ne 0 ]]; then
exit 1
fi
exit 0
-179
View File
@@ -1,179 +0,0 @@
---
name: go-style-core
description: Use when working with Go formatting, line length, nesting, naked returns, semicolons, or core style principles. Also use when a style question isn't covered by a more specific skill, even if the user doesn't reference a specific style rule. Does not cover domain-specific patterns like error handling, naming, or testing (see specialized skills). Acts as fallback when no more specific style skill applies.
license: Apache-2.0
metadata:
sources: "Effective Go, Google Style Guide, Uber Style Guide, Go Wiki CodeReviewComments"
---
# Go 风格核心原则
## 风格原则(优先级顺序)
编写可读 Go 代码时,按以下重要性顺序应用这些原则:
### 优先级顺序
1. **清晰性** — 读者能否在没有额外上下文的情况下理解代码?
2. **简洁性** — 这是否是实现目标的最简单方式?
3. **精炼性** — 每一行是否都有其存在的价值?
4. **可维护性** — 后续修改是否容易?
5. **一致性** — 是否与周围代码和项目约定保持一致?
> 在解决清晰性、简洁性和精炼性之间的冲突时,或需要具体示例了解每个原则在实际 Go 代码中的应用时,请阅读 [references/PRINCIPLES.md](references/PRINCIPLES.md)。
---
## 格式化
运行 `gofmt` — 没有例外。**没有严格的行长度限制**,但 Uber 建议软限制为 99 个字符。按语义换行,而非按长度 — 选择重构而非仅仅换行。
> 在配置 gofmt、决定换行策略、应用 MixedCaps 规则或解决局部一致性问题时,请阅读 [references/FORMATTING.md](references/FORMATTING.md)。
---
## 减少嵌套
优先处理错误情况和特殊条件。提前返回或继续循环,使"正常路径"保持无缩进。
```go
// 不好:深度嵌套
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
}
// 好:扁平结构,提前返回
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}
```
### 不必要的 Else
如果变量在 if 的两个分支中都被赋值,使用默认值 + 覆盖模式。
```go
// 不好:在两个分支中都赋值
var a int
if b {
a = 100
} else {
a = 10
}
// 好:默认值 + 覆盖
a := 10
if b {
a = 100
}
```
---
## 裸返回
没有参数的 `return` 语句会返回命名返回值。这被称为"裸"返回。
```go
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 返回 x, y
}
```
### 裸返回的使用指南
- **在小型函数中可以使用**:裸返回在只有几行的函数中是没问题的
- **在中大型函数中要明确**:一旦函数增长到中等大小,为了清晰起见应明确指定返回值
- **不要仅为了裸返回而命名返回值**:文档的清晰性始终比节省一两行更重要
```go
// 好:小型函数,裸返回很清晰
func minMax(a, b int) (min, max int) {
if a < b {
min, max = a, b
} else {
min, max = b, a
}
return
}
// 好:较大的函数,显式返回
func processData(data []byte) (result []byte, err error) {
result = make([]byte, 0, len(data))
for _, b := range data {
if b == 0 {
return nil, errors.New("null byte in data")
}
result = append(result, transform(b))
}
return result, nil // 显式返回:在较长的函数中更清晰
}
```
关于命名返回参数的指导,请参阅 **go-documentation**。
---
## 分号
Go 的词法分析器会在任何最后一个 token 是标识符、字面量或以下关键字之一的行后自动插入分号:`break continue fallthrough return ++ -- ) }`。
这意味着 **左花括号必须与控制结构在同一行**:
```go
// 好:花括号在同一行
if i < f() {
g()
}
// 不好:花括号在下一行 — 词法分析器会在 f() 后插入分号
if i < f() // 错误!
{ // 错误!
g()
}
```
在惯用 Go 中,显式分号仅出现在 `for` 循环子句中和用于分隔单行上的多个语句。
---
## 快速参考
| 原则 | 核心问题 |
|------|----------|
| 清晰性 | 读者能否理解代码的意图和原因? |
| 简洁性 | 这是否是最简单的方法? |
| 精炼性 | 信噪比是否高? |
| 可维护性 | 后续能否安全地修改? |
| 一致性 | 是否与周围代码保持一致? |
## 相关 Skill
- **命名约定**:在应用 MixedCaps、选择标识符名称或解决命名争议时,请参阅 [go-naming](../go-naming/SKILL.md)
- **错误流程**:在构建错误优先的守卫子句或通过提前返回减少嵌套时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
- **文档**:在编写文档注释、命名返回参数或包级别文档时,请参阅 [go-documentation](../go-documentation/SKILL.md)
- **Linting 执行**:在使用 golangci-lint 自动化风格检查或配置 CI 时,请参阅 [go-linting](../go-linting/SKILL.md)
- **代码审查**:在系统性代码审查中应用风格原则时,请参阅 [go-code-review](../go-code-review/SKILL.md)
- **日志风格**:在审查日志实践、在 log 和 slog 之间选择或组织日志输出时,请参阅 [go-logging](../go-logging/SKILL.md)
@@ -1,95 +0,0 @@
# 格式化参考
## gofmt 是必须的
所有 Go 源文件 **必须** 符合 `gofmt` 的输出。没有例外。
```bash
# 格式化一个文件
gofmt -w myfile.go
# 格式化目录下所有文件
gofmt -w .
```
其他格式化工具:
| 工具 | 用途 |
|------|------|
| `gofmt` | 标准格式化工具(必须使用) |
| `goimports` | gofmt + import 管理 |
| `gofumpt` | gofmt 的更严格超集 |
---
## 括号
Go 比 C 和 Java 需要更少的括号。控制结构(`if`、`for`、`switch`)的语法中不需要括号。运算符优先级层次更短更清晰,所以 `x<<8 + y<<16` 的含义就如空格所暗示的那样 — 不像其他语言。
---
## MixedCaps(驼峰命名)
Go 使用 `MixedCaps` 或 `mixedCaps`,从不使用下划线:
```go
// 好
MaxLength // 导出常量
maxLength // 非导出常量
userID // 变量
// 不好
MAX_LENGTH // 不使用 snake_case
max_length // 不使用下划线
```
例外:
- 测试函数名可以使用下划线:`TestFoo_Bar`
- 与 OS/cgo 交互的生成代码
---
## 行长度
Go 中 **没有严格的行长度限制**,但避免过长的行。Uber 建议软限制为 99 个字符。
指导原则:
- 如果一行感觉太长,**重构** 而非仅仅换行
- 不要在缩进变化之前换行(函数声明、条件语句)
- 不要将长字符串(URL)拆分成多行
- 换行时,将所有参数放在各自的行上
- 如果已经尽可能短了,就让它保持长行
**按语义换行,而非按长度**:
不要仅仅为了保持短行而添加换行符,当长行更具可读性时(例如,重复性的行)。因为你所写的内容而换行,而非因为行长度。
长行通常与长名称相关。如果你发现行太长,考虑名称是否可以更短。去掉长名称往往比换行更有帮助。
这个建议同样适用于函数长度 — 没有"函数永远不超过 N 行"的规则,但确实存在太长的情况。解决方案是改变函数的边界在哪里,而非计算行数。
```go
// 不好:随意的行中断
func (s *Store) GetUser(ctx context.Context,
id string) (*User, error) {
// 好:所有参数各占一行
func (s *Store) GetUser(
ctx context.Context,
id string,
) (*User, error) {
```
---
## 局部一致性
当风格指南未做规定时,与附近代码保持一致:
**有效的** 局部选择:
- 错误格式化使用 `%s` 还是 `%v`
- 带缓冲 channel 还是 mutex
**无效的** 局部覆盖:
- 行长度限制
- 基于断言的测试库
@@ -1,89 +0,0 @@
# 风格原则参考
## 1. 清晰性
代码的目的和原理必须对读者清晰。
- **做什么**:使用描述性名称、有帮助的注释和高效的组织
- **为什么**:添加解释原理的注释,特别是对于微妙的细节
- 从读者的角度审视清晰性,而非作者的角度
- 代码应该易于阅读,而非易于编写
```go
// 好:目的清晰
func (c *Config) WriteTo(w io.Writer) (int64, error)
// 不好:不清晰,重复了接收者
func (c *Config) WriteConfigTo(w io.Writer) (int64, error)
```
## 2. 简洁性
代码应该以最简单的方式实现目标。
简洁的代码:
- 从头到尾容易阅读
- 不假定读者有先验知识
- 没有不必要的抽象层次
- 注释解释"为什么",而非"做什么"
- 可能与"巧妙"的代码互斥
### 最少机制
当有几种方式表达同一个想法时,优先使用最标准的工具:
1. 核心语言结构(channel、slice、map、loop、struct)
2. 标准库(HTTP 客户端、模板引擎)
3. 第三方库 — 仅在 (1) 和 (2) 不够用时使用
## 3. 精炼性
代码应该有高信噪比。
- 避免重复代码
- 避免多余的语法
- 避免不必要的抽象
- 使用表驱动测试提取公共代码
```go
// 好:常见惯用法,信号量高
if err := doSomething(); err != nil {
return err
}
// 好:为异常情况增强信号
if err := doSomething(); err == nil { // 如果没有错误
// ...
}
```
## 4. 可维护性
代码被修改的次数远多于被编写的次数。
可维护的代码:
- 对于未来的程序员来说容易正确修改
- API 能够优雅地扩展
- 使用可预测的名称(相同概念 = 相同名称)
- 最小化依赖
- 具有全面的测试和清晰的诊断信息
```go
// 不好:关键细节被隐藏
if user, err = db.UserByID(userID); err != nil { // = vs :=
// 好:显式且清晰
u, err := db.UserByID(userID)
if err != nil {
return fmt.Errorf("invalid origin user: %s", err)
}
user = u
```
## 5. 一致性
代码的外观和行为应该与代码库中的类似代码一致。
- 包级别的一致性最重要
- 当出现平局时,优先保持一致性
- 绝不为了局部一致性而覆盖有文档记录的风格原则
-146
View File
@@ -1,146 +0,0 @@
---
name: "new-api"
description: "Wavelet 项目专用:当新增或修改自定义业务 API、新增业务路由、新增 service 层核心逻辑时必须使用。本技能指导包职责划分、推荐文件结构、路由解耦、Swagger 文档生成与质量门禁验证。"
---
# 新增业务 API 开发与路由注册规范
本技能是 Wavelet 项目接口开发与路由注册的唯一指导规范。在开发任何新接口前,请严格按照本指南进行架构决策与路由注册。
---
## 核心路由准则与防线 (Routing Governance & Guardrails)
Wavelet 后端路由采用了**严格的框架层与业务层隔离机制**。请牢记以下开发原则:
1. **禁止修改框架级路由文件**:
- 以下文件属于系统框架/平台级接口,**禁止为了添加自定义业务接口而进行任何修改**:
- `internal/router/router.go`(核心入口委派)
- `internal/router/root/default.go`(公开文件服务、robots.txt、Swagger 及 /api/health 路由)
- `internal/router/root/frontend.go`(前端静态服务)
- `internal/router/v1/v1.go`(V1 分发层协调器)
- `internal/router/v1/admin.go`(框架管理员端管理接口)
- `internal/router/v1/user.go`(框架普通用户端基础接口、OAuth及公开接口)
2. **仅允许在 `custom.go` 中注册业务接口**:
- 所有的自定义/业务相关接口注册,有且仅有以下两个合法的承载点:
- [internal/router/root/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/root/custom.go)(用于挂载到根路径的特殊业务接口)
- [internal/router/v1/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/v1/custom.go)(用于挂载在 API V1 下的标准自定义业务接口)
---
## 路由归属判定表 (Where should I register my new API?)
根据接口的**访问路径特征**和**访问身份/限制条件**,决定将新开发的 API 挂载至何处:
| 目标 API 路径特征 | 访问身份/条件限制 | 对应的路由注册入口 | 是否允许修改 |
| :--- | :--- | :--- | :--- |
| **`/my-custom-path`** (挂载在根路径下的特殊业务接口) | 自定义控制 | `root/custom.go` 中的 `RegisterCustomRootRoutes` | **允许修改 (业务自定义入口)** |
| **`/api/v1/custom/...`** (API v1 下的定制业务接口) | 自定义控制 | `v1/custom.go` 中的 `RegisterCustomRoutes` | **允许修改 (业务自定义入口)** |
| **`/api/v1/admin/...`** (系统管理员管理端接口) | 需要管理员登录 (`admin.LoginAdminRequired()`) | `v1/admin.go` | **禁止修改 (仅限系统框架路由)** |
| **`/api/v1/user/...`** (框架普通用户基础接口) | 需要普通用户登录 (`oauth.LoginRequired()`) | `v1/user.go` | **禁止修改 (仅限系统框架路由)** |
| **`/api/v1/public/...`** (Captcha、Config 等系统公开接口) | 所有人 (无条件 / 公开) | `v1/user.go` | **禁止修改 (仅限系统框架路由)** |
| **`GET /f/:id`**, **`GET /robots.txt`**, **`GET /api/health`** (系统级默认及公开接口) | 所有人 (无条件 / 公开) | `root/default.go` | **禁止修改 (仅限系统框架路由)** |
---
## 两个自定义路由包的用法与区别 (Root Custom vs V1 Custom)
### 1. 根路径自定义包:`root/custom.go`
* **适用场景**:适用于需要**直接挂载在主域名根路径下**的特殊自定义业务接口(如第三方 Webhook 回调、特定的短链接重定向、外部数据接口等,不需要 `/api/v1` 前缀)。
* **用法示例**:
在 [root/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/root/custom.go) 中实现:
```go
package root
import (
"github.com/Rain-kl/Wavelet/internal/apps/custom"
"github.com/gin-gonic/gin"
)
// RegisterCustomRootRoutes registers custom business routes that belong to the root path.
func RegisterCustomRootRoutes(r *gin.Engine) {
// 挂载到根路径下,如 GET /my-custom-webhook
r.GET("/my-custom-webhook", custom.HandleRootWebhook)
}
```
*(注:该函数已由 `root.go` 自动加载,你无需修改任何其他核心文件。)*
### 2. V1 API 自定义包:`v1/custom.go`
* **适用场景**:适用于普通的**自定义业务 API**,需要规范挂载在标准 API V1 路径下(即自动带有 `/api/v1/custom/...` 前缀,可选择性配置用户/管理员登录中间件)。
* **用法示例**:
在 [v1/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/v1/custom.go) 中实现:
```go
package v1
import (
"github.com/Rain-kl/Wavelet/internal/apps/custom"
"github.com/gin-gonic/gin"
)
// RegisterCustomRoutes registers standard custom API routes under /api/v1.
func RegisterCustomRoutes(apiV1Router *gin.RouterGroup) {
customRouter := apiV1Router.Group("/custom")
{
// 挂载到 /api/v1/custom 下,例如:POST /api/v1/custom/action
customRouter.POST("/action", custom.DoActionHandler)
}
}
```
*(注:该函数已由 `v1/v1.go` 自动加载,你无需修改任何其他核心文件。)*
---
## 建议创建/修改的文件结构 (Recommended Directory Structure)
当新增一套定制的业务接口(例如名为 `custom` 的业务模块)时,建议采用以下标准文件结构:
```text
internal/
├── router/
│ ├── root/
│ │ └── custom.go # [修改] 若为根路径 API,在此处注册,将路由委派给 apps/custom
│ └── v1/
│ └── custom.go # [修改] 若为 v1 API,在此处注册,将路由委派给 apps/custom
└── apps/
└── custom/
├── routers.go # [新建] HTTP Handlers (Gin),负责参数绑定、校验与响应
├── logics.go # [新建] 业务逻辑层:承载模块内闭环的纯 Go 业务逻辑,不依赖 gin.Context
└── errs.go # [新建] 存放模块特有的业务错误常量定义(可选)
```
---
## 核心开发步骤 (Step-by-Step Flow)
### 步骤 1:数据库定义与迁移
如果自定义功能涉及新表或字段,请参考 [database-migration](../database-migration/SKILL.md) 技能,在 `internal/db/migrator/goose/` 目录下编写迁移文件并在 `internal/model/` 中定义 GORM 数据模型。
### 步骤 2:在模块内实现业务逻辑 (`logics.go` / `service.go`)
业务逻辑逻辑应当实现于 `internal/apps/custom/` 目录下:
- **优先使用纯函数(`logics.go`)**:定义接收 `context.Context` 且不依赖 `*gin.Context` 的函数,易于单元测试与 Worker 复用。参考 `internal/apps/user/logics.go`。
- **有状态服务(`service.go`)**:若需注入依赖(如 DB 连接、外部客户端等),可定义 Service 结构体和构造函数。
- **跨模块副作用(推送、任务监听等)**:核心业务代码通过 `internal/listener` 发射域事件,禁止直接 `import` push 模块;装配在 `internal/bootstrap` 完成(参见 `push-notification` skill)。
### 步骤 3:编写 HTTP Handler (`routers.go`)
在 `internal/apps/custom/routers.go` 中编写 Handler:
- 负责请求参数绑定与校验(使用 `ShouldBindJSON`/`ShouldBindQuery`)。
- 负责提取 Session / 用户身份。
- 调用业务逻辑层,并使用 `github.com/Rain-kl/Wavelet/internal/common/response` 统一返回响应:
- 成功时返回:`response.OK(data)` 或 `response.OKNil()`
- 失败时返回:`response.Err(msg)`
- 编写规范的 Swagger 注释。
### 步骤 4:在自定义包中注册路由并委派
根据 **路由归属判定表**,在 [root/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/root/custom.go) 或 [v1/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/v1/custom.go) 中编写注册代码,将路由路径绑定到步骤 3 中编写的 Handler。
---
## 质量验证门禁 (Quality Gates)
每次新增或修改接口后,必须运行并验证以下各项:
1. **自动授权许可**:`make license`(新增 Go 文件时自动添加许可头)
2. **重新生成 Swagger 文档**:`make swagger`(若有 Swagger 注释修改)
3. **静态代码及风格检查**:`make code-check`(确保通过 golangci-lint 和前端 TS 检查)
4. **自动化单元测试**:`go test ./...`(确保所有测试 100% 通过)
-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 或批准后即可推进下一步。
-54
View File
@@ -1,54 +0,0 @@
---
name: "release-guide"
description: "Wavelet 项目专用:根据自上一个正式版本 Tag 以来的提交记录,整理生成规范的 Version Bump Commit Message,用于触发自动双语 Release。"
---
# Release Commit Message Guide
## 目标
当用户准备发布 Wavelet 新版本时,本 Skill 只负责生成用于版本提交的 Commit Message。
## 生成提交信息
将原始 commit log 整理为面向 Release 的更新说明。
要求:
1. 合并重复或相近提交。
2. 删除无意义提交,例如格式化、临时调试、无关重构。
3. 将内部实现描述改写为用户可理解的变更。
4. 每条使用完整中文句子。
5. 尽量说明“修复/优化了什么”以及“带来的效果”。
6. 不要编造 commit log 中没有的信息。
7. 不要加入 token、密钥、私有地址等敏感信息。
8. 如果某个分类没有内容,可以省略。
固定使用以下分类:
text ### 🛠 修复 ### ⚡️ 优化与改进 ### 💄 其他/体验
分类规则:
- Bug、异常行为、错误逻辑:放入 ### 🛠 修复
- 性能、稳定性、接口、架构、兼容性:放入 ### ⚡️ 优化与改进
- 日志、文案、UI、文档、开发体验:放入 ### 💄 其他/体验
示例:
```
### 🛠 修复
- 修复了通过 MCP 接口操作时笔记库范围限制未正确生效的问题。
- 修复了 MCP 接口返回数据格式不一致的问题。
- 修复了 WebSocket 客户端异常断开后僵尸连接未及时清理的问题。
### ⚡️ 优化与改进
- 优化了 WebGUI 登录机制,引入设备令牌自动轮转,减少因 IP 变化产生的冗余令牌。
### 💄 其他/体验
- 优化了 WebSocket 错误日志,增加请求路径信息,方便问题排查。
```
## 提交
生成完 Commit Message 后调用命令工具修改最后一次提交信息, 不要提交, 不要推送, 你的任务到此结束。
+300
View File
@@ -0,0 +1,300 @@
---
name: autoresearch
description: >
Autonomous goal-directed iteration loop, inspired by Karpathy's autoresearch.
Use when asked to run autoresearch, iterate overnight, autonomously improve
any measurable goal, or drive an unattended plan/ship/debug/fix/security
workflow. Loops forever: modify → verify → keep/revert → log → repeat.
Never stops until the user interrupts.
---
# Autoresearch
> Ported from `supratikpm/gemini-autoresearch` (Gemini CLI). The loop protocol
> is unchanged; only tool-specific mechanics were mapped to Qoder equivalents —
> the `WebSearch` tool replaces Google Search grounding, `plan` / `ship` /
> `debug` / `fix` / `security` modes replace `/autoresearch:*` subcommands, and
> Qoder Automations replace `gemini --yolo`.
You are an autonomous improvement agent. You iterate forever until interrupted.
You do not ask "should I continue?" You do not pause for confirmation. You run
the loop.
## Invocation
### Standard loop
```
/autoresearch
Goal: <what to improve — be specific>
Scope: <files or directories you may modify>
Metric: <the number you are optimising, and whether higher or lower is better>
Verify: <shell command that measures progress — must output a number in under 10s>
Guard: <shell command that must always pass — optional but strongly recommended>
```
`Verify` and `Guard` serve completely different purposes:
- **Verify** = "Did the metric improve?" — measures progress toward the goal
- **Guard** = "Did anything else break?" — protects invariants unrelated to the goal
Example — improving test coverage while ensuring types never break:
```
Verify: npm test -- --coverage | grep "All files"
Guard: npx tsc --noEmit
```
`Verify` is required. `Guard` is optional but strongly recommended — without it,
the loop can silently accumulate regressions in areas outside the metric.
Guard files are **never modified** by the loop. They are read-only constraints.
Goal, Scope, Metric, and Verify are required. Guard is optional.
If any required fields are missing, ask for them once, then start.
### Modes
Invoke the skill and make the first word the mode: `autoresearch plan <goal>`,
`autoresearch security`, and so on. Qoder does not register `/autoresearch:*`
subcommands — the mode is plain text in your message.
| Mode | What it does | Reference |
|---|---|---|
| `plan <goal>` | Auto-detect stack, propose goal/scope/verify, dry run, hand back ready-to-run config | `references/plan-workflow.md` |
| `ship` | Pre-flight checklist — tests, types, lint, bundle, secrets, deps. Autoresearch loop on anything that fails | `references/ship-workflow.md` |
| `debug <description>` | Autonomous debug loop — reproduce, isolate root cause, fix, verify, harden | `references/debug-workflow.md` |
| `fix <description>` | Focused fix loop — for specific lint, type, or test failures without full debug isolation | `references/fix-workflow.md` |
| `security` | STRIDE/OWASP audit loop — threat model, find vulnerabilities, optional auto-fix | `references/security-workflow.md` |
No mode means the standard loop above.
**When a mode is invoked**, read the corresponding reference file
before doing anything else. The reference file contains the full protocol
for that workflow.
---
## Setup phase (run once before the loop)
1. Read every file in Scope to build full context. Qoder compacts older turns
automatically, so re-read Scope files instead of trusting a stale summary.
2. Read `autoresearch-lessons.md` if it exists. This is accumulated knowledge
from prior runs. Read it carefully before forming any hypothesis.
3. Run the Verify command. Record the output as the baseline (iteration #0).
4. If Guard is provided: run it once. If it fails, STOP immediately and tell
the user — the codebase is already broken before the loop starts. Fix the
Guard failure manually before proceeding. Guard must be green at baseline.
5. Initialise `autoresearch-results.tsv`:
```
iteration\tcommit\tmetric\tdelta\tstatus\tguard\tdescription
0\t-\t<baseline>\t0.0\tbaseline\tpass\tinitial measurement
```
6. Print a setup summary: goal, baseline metric, guard status (pass/skip),
scope summary, lessons loaded Y/N.
7. Start the loop immediately. Do not wait for confirmation.
---
## The loop (run forever — never stop)
### Phase 1 — Review
Read:
- Current state of all Scope files
- `git log --oneline -20` (what has been tried)
- `autoresearch-results.tsv` (what worked, what failed, patterns)
- `autoresearch-lessons.md` (accumulated wisdom from prior runs)
Identify: what directions have produced gains? what has consistently failed?
what has not been tried yet?
### Phase 2 — Ideate
Pick ONE hypothesis. It must be:
- Specific and testable in a single iteration
- Meaningfully different from the last 3 attempts
- Informed by both the results log and the lessons file
- Explained in one sentence
Prefer hypotheses that build on proven wins over untested territory.
Prefer simplicity — a small clean change beats a large complex one.
### Phase 3 — Modify
Make exactly ONE atomic change in Scope. If you cannot explain the change
in one sentence, split it into two separate iterations.
Do not touch files outside Scope. Do not refactor unrelated code. One thing.
### Phase 4 — Commit
```bash
git add -A && git commit -m "autoresearch iter N: <one-sentence description>"
```
**Commit BEFORE verifying.** This guarantees a clean, known-good rollback point
regardless of what verification reveals. Never skip this step.
### Phase 5 — Verify + Guard
**Step A — Run Verify.** Extract the numeric metric value.
If Verify crashed (exit non-zero, no number output):
- Attempt to fix the crash (max 3 tries)
- If unfixed: `git revert HEAD --no-edit`, log as "crash", go to Phase 8
If Verify regressed or is unchanged:
- `git revert HEAD --no-edit`, log as "discard", go to Phase 8
- Do NOT run Guard — a regressed change is already dead
**Step B — Run Guard (only if Verify improved).** Exit code 0 = pass.
**Web research supplement**: after Verify passes, use `WebSearch` for
additional signal when local scripts cannot capture full quality.
See `references/web-research-patterns.md`. Research is a supplement only.
### Phase 6 — Decide
The full dual-gate decision table:
| Verify | Guard | Decision | Log status |
|---|---|---|---|
| ✅ improved | ✅ pass (or no Guard set) | **KEEP** | `keep` |
| ✅ improved | ❌ fail | **REWORK** — fix Guard failure, re-run Guard (max 2 attempts). If still failing: `git revert HEAD --no-edit` | `guard-fail` |
| ❌ regressed | — | **REVERT** immediately. Do not run Guard. | `discard` |
| ❌ unchanged | — | **REVERT**. Treat unchanged as a regression. | `discard` |
| 💥 crashed | — | **FIX** (max 3 attempts), then revert if unfixed. | `crash` |
**Rework protocol** (when Verify passes but Guard fails):
1. Read the Guard failure output carefully
2. Make the minimal additional change to satisfy Guard without hurting Verify
3. Amend the commit: `git add -A && git commit --amend --no-edit`
4. Re-run both Verify AND Guard
5. If both pass → KEEP. If Guard still fails after 2 rework attempts → REVERT.
### Phase 7 — Log
Append one row to `autoresearch-results.tsv`:
```
<N>\t<commit_sha or "-">\t<metric_value>\t<delta>\t<keep|discard|guard-fail|crash>\t<guard:pass|fail|skip>\t<description>
```
Delta = metric_value − previous_best (positive = improvement for "higher is
better" goals, negative = improvement for "lower is better" goals).
### Phase 8 — Repeat
Go to Phase 1. Immediately. NEVER STOP.
---
## Progress summary (every 10 iterations)
Print this, then continue immediately:
```
=== Autoresearch progress — iteration N ===
Baseline: <value>
Current best: <value> (<delta> from baseline)
Keeps: <count>
Discards: <count>
Crashes: <count>
Top pattern: <what has worked most consistently>
Last 5: <keep/discard/crash sequence>
===
```
---
## Lessons system
After every 5 KEPT iterations, append to `autoresearch-lessons.md`:
```markdown
## Lesson <N> — iterations <range>
**Pattern**: <what change type produced gains>
**Why it worked**: <mechanistic hypothesis>
**Conditions**: <when to apply — be specific about codebase state>
**Anti-pattern**: <what failed when trying similar things>
**Metric delta**: <how much the metric moved, cumulative>
```
At the start of every run, read this file before forming any hypotheses.
Weight recent lessons more heavily. Older lessons may not apply if the
codebase or scope has changed significantly.
This is the compounding mechanism. Each overnight run starts smarter than
the last.
---
## Stuck recovery
After 5 consecutive discards or crashes:
1. Re-read all Scope files from scratch. Full context, not memory.
2. Search the lessons log for near-misses — what came closest to working?
3. Try combining two near-miss approaches into one hypothesis.
4. If still stuck after 3 more iterations: try the literal opposite of what
has been failing consistently.
5. If still stuck after 3 more: use `WebSearch` to research the
problem space. Search for `[domain] [metric] improvement techniques [year]`.
Extract 3 concrete techniques. Use each as the next 3 hypotheses.
6. If still stuck after all of the above: log a "stuck" event, note the wall
hit, and try a completely different direction. Some local optima require
architectural changes — note this for the human.
---
## Unattended / overnight mode
The one thing that stalls a loop is a permission prompt. Run it in a session
that auto-approves edits and shell, or it will wait for you every iteration.
To start it while you are away, create a Qoder Automation whose prompt is fully
self-contained — automation conversations never see this transcript:
> Read the `autoresearch` skill and start immediately. Goal: `<goal>`.
> Scope: `<scope>`. Metric: `<metric — higher/lower is better>`.
> Verify: `<command>`. Guard: `<command>`. Do not pause, do not ask questions,
> iterate until stopped.
You will wake up to `autoresearch-results.tsv` and `autoresearch-lessons.md`.
Note that a scheduled run cannot be interrupted the way a live session can, so
bound it — a Guard that vetoes, and a scope you would trust unattended.
---
## Non-negotiable rules
1. **NEVER STOP** until the user manually interrupts the run.
2. **ONE change per iteration** — atomic, explainable in one sentence.
3. **Mechanical verification only** — no "looks better", no "seems cleaner".
If you cannot measure it, you cannot use it as a signal.
4. **Commit BEFORE verifying** — always. No exceptions.
5. **Auto-revert on regression** — no debate, no "let me try one more thing".
6. **Guard is a hard veto** — Verify passing does not mean KEEP. Guard must also pass.
7. **Never modify Guard files** — they are read-only invariants, not scope.
8. **Read git history before every hypothesis** — it is your short-term memory.
9. **Read lessons before every run** — it is your long-term memory.
10. **Simplicity wins ties** — equal metric + less code = KEEP.
11. **Never touch files outside Scope** — discipline is what makes the loop safe.
12. **When in doubt, make the smaller change** — scope creep kills iterations.
---
## Reference files
**Core loop**
- `references/loop-protocol.md` — detailed phase-by-phase protocol
- `references/results-logging.md` — TSV format, summary templates, examples
- `references/lessons-system.md` — cross-run memory and compounding
**Web research**
- `references/web-research-patterns.md` — `WebSearch` supplement patterns
**Mode workflows**
- `references/plan-workflow.md` — `plan` mode — auto-detect and configure
- `references/ship-workflow.md` — `ship` mode — pre-flight checklist
- `references/debug-workflow.md` — `debug` mode — root cause and fix
- `references/fix-workflow.md` — `fix` mode — focused type/lint fix
- `references/security-workflow.md` — `security` mode — STRIDE/OWASP audit
@@ -0,0 +1,25 @@
# `autoresearch debug` mode — Autonomous Debug Loop
This workflow is triggered by the `debug` mode. It is designed to reproduce, isolate, and fix specific bugs autonomously.
## Context
Use this when something is clearly broken (e.g., a failing test, a crash, or a UI bug).
## Phase 1: Reproduction
1. Create a minimal reproduction script (e.g., `debug/repro.js` or a new test case).
2. Run the repro script and verify it fails as expected.
3. This repro command becomes your `Verify` command for the loop.
## Phase 2: Isolation
1. Use `Grep` and `Read` to find the code responsible for the failure.
2. Form a hypothesis about the root cause.
## Phase 3: Fix Loop
1. Start a standard autoresearch loop with:
- **Goal**: Fix the bug identified in the repro script.
- **Verify**: The repro command (must exit 0 on success).
- **Guard**: Existing test suite and linting.
## Phase 4: Hardening
1. After the fix is verified, add a permanent regression test to the codebase.
2. Verify that the fix holds across the entire project.
@@ -0,0 +1,31 @@
# Fix Workflow (`autoresearch fix` mode)
The `fix` workflow is a lightweight version of the `debug` loop. It is designed for situations where you have a specific, known failure (e.g., a TypeScript error or a lint violation) and you want to fix it without the overhead of full reproduction and isolation.
## Protocol
### 1. Context Loading
* Read the error message or description provided in the command.
* Identify the affected file(s).
* Read the current state of those files.
### 2. Hypothesis
* Form a direct hypothesis on how to fix the specific error.
* The fix must be minimal and targeted.
### 3. Execution
* Apply the fix.
* Commit the change.
### 4. Verification
* Run the command that triggered the original failure (e.g., `npx tsc` or `npm run lint`).
* If a `Guard` is set in the main autoresearch config, run that as well.
### 5. Decision
* If the error is gone and Guard passes: **KEEP**.
* If the error persists: **RETRY** (max 3 times) with a different approach.
* If it still fails after 3 tries: **REVERT** and report to the user.
## When to use `fix` vs `debug`
* Use **`fix`** for mechanical errors: "Fix the lint error on line 42", "Fix the missing import in `utils.ts`".
* Use **`debug`** for logical errors: "The login flow fails for users with specialized characters", "Database connection timeouts under high load".
@@ -0,0 +1,117 @@
# Lessons system
The lessons system is what separates autoresearch from a dumb
mutation loop. It is the mechanism by which each overnight run starts
smarter than the last.
---
## The compounding model
```
Night 1: 100 experiments → lessons-v1 written
Night 2: reads lessons-v1 → avoids 20 known failures → 80 net-new experiments
Night 3: reads lessons-v2 → avoids 35 known failures → faster convergence
...
```
Without the lessons system, every run starts from scratch. With it, runs
compound — each failure is learned once and never repeated.
---
## File location and format
File: `autoresearch-lessons.md` in your project root.
Add to `.gitignore` — this is a working file for the agent, not source code.
```markdown
# Autoresearch lessons — <project name>
Generated by the autoresearch skill. Do not edit manually.
Last updated: <ISO date>
## Lesson 1 — iterations 1–5
**Pattern**: <the type of change that produced gains>
**Why it worked**: <mechanistic hypothesis — be specific>
**Conditions**: <codebase state where this applies>
**Anti-pattern**: <what failed when trying similar approaches>
**Metric delta**: <cumulative gain from this pattern, e.g. "+4.2%">
## Lesson 2 — iterations 6–10
...
```
---
## When to write lessons
Append a new lesson after every 5 KEPT iterations (not every 5 total
iterations). Lessons should only describe what worked.
Failed patterns are captured implicitly — if a pattern never generates a
kept iteration, it never generates a lesson, and the loop naturally
deprioritises it via Phase 2's "different from last 3 attempts" rule.
---
## What makes a good lesson
**Good** (specific, mechanistic, conditional):
```
**Pattern**: Defer non-critical third-party scripts using loading="lazy"
**Why it worked**: Removes scripts from the critical render path, reducing
Time to Interactive without affecting functionality
**Conditions**: Applies to analytics, chat widgets, social embeds — not
to scripts required for initial page render
**Anti-pattern**: Lazy-loading scripts that are called in the first 500ms
of page load caused layout shifts and broke interactions
**Metric delta**: +6.8% Lighthouse performance score across 3 iterations
```
**Bad** (vague, not actionable):
```
**Pattern**: Make things faster
**Why it worked**: It improved performance
**Conditions**: When performance is bad
**Anti-pattern**: When it makes things worse
```
---
## How to read lessons at the start of a run
1. Read the full file — do not skip old lessons even if they seem stale.
2. For each lesson, assess: does this pattern still apply given the current
state of the codebase? If the code it describes has been significantly
refactored, downweight it.
3. Extract the top 2-3 highest-delta patterns. These are your first
hypotheses unless the results log shows they have already been exhausted.
4. Extract the anti-patterns. These are your first exclusions — do not
generate hypotheses that match these patterns.
---
## Cross-project lessons
For teams running autoresearch across multiple similar projects (e.g.
multiple Next.js apps), consider maintaining a shared lessons file at
`~/.autoresearch/global-lessons.md`.
At the start of a run, read both the project-level and global lessons.
Project-level lessons take precedence when they conflict with global ones.
This is optional but significantly accelerates convergence on new projects
that share a tech stack with already-researched ones.
---
## Lessons file maintenance
- Do not manually edit the lessons file during a run — the agent reads it
at the start of each run and its contents influence hypothesis generation.
- After a long run (100+ iterations), review the file and remove lessons
that are no longer applicable (e.g. they describe code that no longer
exists). Add a comment explaining why the lesson was removed.
- The lessons file is cumulative — never delete lessons, only annotate them
as superseded if a newer lesson contradicts them.
@@ -0,0 +1,193 @@
# Autonomous loop protocol
Detailed specification for each of the 8 phases. The SKILL.md contains the
summary version. Read this reference when you need precise guidance on edge
cases in any phase.
---
## Phase 1 — Review
**Purpose**: Build a complete, accurate picture of current state before
forming any hypothesis. Hypotheses formed without full context waste iterations.
**What to read**:
- Every file in Scope (not just the ones you last touched)
- `git log --oneline -20` — what has been attempted, in order
- `autoresearch-results.tsv` — the full record of what worked and failed
- `autoresearch-lessons.md` — accumulated patterns from prior runs
**What to extract**:
- Current metric trajectory (improving? plateauing? volatile?)
- Which change types produced the most gain per iteration
- Which change types consistently failed
- Which directions have not yet been explored
- Any patterns in crash causes
**Duration**: This phase should take as long as needed to form a genuinely
informed hypothesis. Rushing Phase 1 leads to repeated failures.
---
## Phase 2 — Ideate
**Purpose**: Select ONE hypothesis that has the highest expected gain given
what is known.
**Hypothesis selection criteria** (in order of priority):
1. Builds directly on a proven pattern from the lessons file
2. Explores a direction adjacent to a near-miss (something that almost worked)
3. Combines two near-miss approaches that individually failed
4. Tries the opposite of what consistently failed
5. Applies an externally validated technique (from `WebSearch` research)
6. Tries something entirely untested
**What makes a good hypothesis**:
- Specific: "lazy-load the user avatar component" not "improve performance"
- Testable: produces a measurable delta in the Verify command
- Atomic: one thing changes, one thing is measured
- Explainable in one sentence before you make the change
**What makes a bad hypothesis**:
- Vague: "refactor for clarity"
- Multi-part: "update the API, add caching, and fix the tests"
- Untestable by the Verify command
- Identical to something tried in the last 3 iterations
---
## Phase 3 — Modify
**Purpose**: Implement the hypothesis as a single, clean, minimal change.
**Rules**:
- Touch only files in Scope
- Make the smallest change that tests the hypothesis
- If the change is getting large, stop and split it — make the first half now,
the second half in the next iteration
- Do not fix unrelated things you notice while editing
- Do not reformat code that is not part of the hypothesis
- Leave comments only if they directly explain the change
**Signs you are over-scoping**:
- You have edited more than 3 files
- The diff is more than ~50 lines
- You are explaining the change with "and also"
When in doubt, make a smaller change. Smaller changes fail faster and teach more.
---
## Phase 4 — Commit
**Purpose**: Create a clean rollback point before any verification risk.
**Command**:
```bash
git add -A && git commit -m "autoresearch iter N: <one-sentence description>"
```
**Commit message format**:
- Always prefix with `autoresearch iter N:`
- One sentence, present tense, describes the change not the goal
- Good: `autoresearch iter 14: lazy-load user avatar to reduce initial bundle`
- Bad: `autoresearch iter 14: improve performance`
**Why commit before verifying**: if the Verify command crashes, hangs, or
corrupts state, you can always `git revert HEAD --no-edit` and return to
a known-good state. If you verify before committing, a crash during
verification leaves you with uncommitted changes and an unknown baseline.
**Never skip this step**, even if the change feels obviously correct.
---
## Phase 5 — Verify
**Purpose**: Get a single numeric measurement of whether the hypothesis helped.
**Execution**:
1. Run the Verify command exactly as specified by the user
2. Extract the numeric metric value
3. Optionally supplement with `WebSearch` research (see
`references/web-research-patterns.md`)
4. Record the raw output for the log
**Handling slow Verify commands**:
If the Verify command takes more than 30 seconds, note this. After the run,
recommend the user find a faster proxy metric — slower verification means
fewer experiments per hour, which compounds negatively over a full night.
**Handling non-deterministic Verify commands**:
If the metric varies significantly between runs on identical code (>5%
variance), note this in the log. Run the Verify command twice and average.
Log both values. Recommend the user address flakiness before the next
overnight run.
---
## Phase 6 — Decide
**Purpose**: Make a clear, mechanical keep/revert decision. No deliberation.
**Decision table**:
| Condition | Action | Log status |
|---|---|---|
| Metric improved (beyond noise threshold) | Keep commit as-is | `keep` |
| Metric unchanged or regressed | `git revert HEAD --no-edit` | `discard` |
| Verify crashed with exit code ≠ 0 | Attempt fix (max 3 tries) then revert | `crash` |
| Verify hung for >60s | Kill process, revert | `crash` |
**Noise threshold**: for metrics with variance, an improvement smaller than
the variance is not a real improvement. If your metric normally varies ±2%,
an improvement of 0.5% is noise — treat it as unchanged and discard.
**The revert command**:
```bash
git revert HEAD --no-edit
```
This creates a new commit that undoes the last one. The history is preserved.
Never use `git reset --hard` — it destroys history that the loop needs.
---
## Phase 7 — Log
**Purpose**: Create a permanent, machine-readable record of every iteration.
**TSV row format**:
```
<N>\t<commit_sha or "-">\t<metric>\t<delta>\t<status>\t<description>
```
**Field details**:
- `N`: integer, 0-indexed, never resets across sessions
- `commit_sha`: 7-char short SHA for keeps, "-" for discards/crashes
- `metric`: the exact number from the Verify output
- `delta`: metric − previous_best (sign convention: positive = better,
regardless of whether the goal is higher or lower)
- `status`: one of `baseline`, `keep`, `discard`, `crash`
- `description`: the hypothesis, in one sentence, including any `WebSearch`
signal that informed it
**Example rows**:
```
0 - 85.2 0.0 baseline initial measurement
1 a1b2c3d 87.1 +1.9 keep lazy-load avatar component
2 - 86.5 -0.6 discard tree-shake lodash imports (broke 2 tests)
3 - 0.0 0.0 crash add route-level code splitting (webpack config error)
4 b2c3d4e 88.3 +1.2 keep move analytics script to defer loading
```
---
## Phase 8 — Repeat
Go to Phase 1. Immediately. Do not pause. Do not summarise. Do not ask
if the user wants to continue.
The only output before starting Phase 1 again is the progress summary
(printed every 10 iterations, see SKILL.md).
The loop ends only when the user interrupts the run.
@@ -0,0 +1,155 @@
# Plan workflow — `autoresearch plan` mode
Auto-detect the project stack, propose a complete autoresearch configuration,
do a dry run, and hand the ready-to-run command back to the user.
No manual goal/scope/verify required. Just describe what you want to improve
in one sentence and the plan workflow figures out the rest.
---
## Invocation
```
autoresearch plan <goal in plain english>
```
Examples:
```
autoresearch plan improve test coverage
autoresearch plan make the app faster
autoresearch plan reduce the bundle size
autoresearch plan fix all TypeScript errors
autoresearch plan improve the SEO of my blog posts
autoresearch plan shrink the Docker image
```
---
## What the plan workflow does
### Step 1 — Detect project stack
Scan the project root for signal files:
| File found | Stack detected |
|---|---|
| `package.json` + `jest.config.*` | Node.js + Jest |
| `package.json` + `vitest.config.*` | Node.js + Vitest |
| `next.config.*` | Next.js |
| `Dockerfile` | Docker |
| `*.tf` | Terraform |
| `.github/workflows/*.yml` | GitHub Actions CI |
| `content/blog/*.md` OR `posts/*.md` | Markdown content/blog |
| `src/**/*.ts` OR `src/**/*.tsx` | TypeScript project |
| `pyproject.toml` OR `setup.py` | Python project |
| `requirements.txt` + `pytest` | Python + pytest |
| `go.mod` | Go project |
| `Cargo.toml` | Rust project |
Print detected stack. If ambiguous, list the top two candidates and ask
the user to confirm before proceeding.
### Step 2 — Map goal to metric + verify command
Use the goal description and detected stack to propose:
| Goal keyword | Metric | Verify command template |
|---|---|---|
| "test coverage" | coverage % (higher is better) | `npm test -- --coverage \| grep "All files"` |
| "bundle size" / "build size" | size in KB (lower is better) | `npm run build 2>&1 \| grep "First Load JS"` |
| "TypeScript errors" / "type errors" | error count (lower is better) | `npx tsc --noEmit 2>&1 \| grep -c "error TS" \|\| echo "0"` |
| "lighthouse" / "performance score" | score 0-100 (higher is better) | `npx lighthouse http://localhost:3000 --output json --quiet 2>/dev/null \| jq '.categories.performance.score * 100'` |
| "docker image" / "image size" | size in MB (lower is better) | `docker build -t bench . -q && docker images bench --format "{{.Size}}"` |
| "flaky tests" | failure count (lower is better) | `for i in {1..5}; do npm test 2>&1; done \| grep -c "FAIL" \|\| echo "0"` |
| "SEO" / "blog" / "content" | SEO score (higher is better) | `node scripts/seo-score.js <detected content path>` |
| "lines of code" / "complexity" | LOC count (lower is better) | `find src/ -name "*.ts" \| xargs wc -l \| tail -1 \| awk '{print $1}'` |
| "CI pipeline" / "pipeline speed" | seconds (lower is better) | `node scripts/estimate-ci-time.js` |
| "Python tests" / "pytest" | coverage % (higher is better) | `pytest --cov=src --cov-report=term-missing \| grep "TOTAL"` |
| "faster" / "performance" / "latency" | p95 ms (lower is better) | `npm run bench 2>&1 \| grep "p95"` |
### Step 3 — Detect scope
Based on goal + stack, propose the tightest scope that covers the goal:
- Test coverage → `src/**/*.ts, src/**/*.test.ts`
- Bundle size → `src/**/*.tsx, src/**/*.ts`
- Docker → `Dockerfile, .dockerignore`
- SEO → `content/blog/*.md` or detected content directory
- TypeScript errors → `src/**/*.ts`
- CI pipeline → `.github/workflows/*.yml`
### Step 4 — Dry run
Run the proposed Verify command once against the current state.
- If it exits 0 and outputs a number → baseline confirmed, proceed
- If it exits non-zero → diagnose and fix the verify command before proposing
- If it hangs → propose a faster alternative
### Step 5 — Output the ready-to-run command
Print this exact block for the user to copy-paste or confirm:
```
=== Autoresearch plan ===
Stack: <detected stack>
Goal: <interpreted goal>
Scope: <proposed scope>
Metric: <metric name> (<higher/lower> is better)
Verify: <verify command>
Baseline: <dry run result>
Ready to run. Confirm or adjust any field, then:
/autoresearch
Goal: <goal>
Scope: <scope>
Metric: <metric>
Verify: <verify command>
Or, for an unattended run, put these same fields into a Qoder Automation prompt
(see "Unattended / overnight mode" in SKILL.md).
===
```
If the user says "looks good" or "run it" — start the autoresearch loop
immediately without requiring them to retype the command.
---
## Web research calibration
After the dry run, use `WebSearch` to calibrate:
- For SEO goals: search for `[target keyword]` to see what top results look like.
Note any structural patterns (FAQ sections, word count, heading structure)
that the current content lacks. Add these as initial hypotheses.
- For performance goals: search for `[framework] performance benchmarks [year]`
to calibrate whether the baseline is already good or has significant headroom.
- For security goals: search for `[stack] common vulnerabilities [year]`
to seed the initial hypothesis pool with known attack vectors.
This research step happens during plan, not during the loop — so it adds
context once without slowing down iterations.
---
## Edge cases
**Goal is too vague** ("make it better"):
Ask one clarifying question: "Better in what way — speed, quality, size,
coverage, or something else?" Then proceed.
**Multiple valid verify commands exist**:
Propose the fastest one. Note the slower alternative in a comment.
**Verify command requires a running server**:
Note this in the plan output. Add a `# requires: local server on :3000`
comment. Suggest the user start it before running the loop.
**No matching stack detected**:
Ask the user to describe their stack in one sentence, then proceed with
a custom verify command.
@@ -0,0 +1,105 @@
# Results logging
Specification for `autoresearch-results.tsv` — the per-iteration record
of every experiment in a run.
---
## File format
Tab-separated values. Headers on row 1. One row per iteration.
```
iteration\tcommit\tmetric\tdelta\tstatus\tdescription
```
### Field definitions
| Field | Type | Description |
|---|---|---|
| `iteration` | integer | 0-indexed. Never resets — if you run multiple sessions, continue from the last number. |
| `commit` | string | 7-char git short SHA for kept commits. `-` for discards and crashes. |
| `metric` | float | Raw metric value from the Verify command. |
| `delta` | float | `metric − previous_best`. Sign convention: positive = improvement (regardless of higher/lower goal). |
| `status` | enum | One of: `baseline`, `keep`, `discard`, `crash` |
| `description` | string | The hypothesis, one sentence. Include the change type and the expected mechanism. |
---
## Example file
```tsv
iteration commit metric delta status description
0 - 85.2 0.0 baseline initial measurement — test coverage 85.2%
1 a1b2c3d 87.1 +1.9 keep add tests for auth middleware edge cases
2 - 86.5 -0.7 discard refactor test helpers (broke 2 existing tests)
3 - 0.0 0.0 crash add integration tests (postgres connection failed — fix in iter 4)
4 b2c3d4e 88.3 +1.2 keep add tests for error handling in API routes
5 - 88.1 -0.2 discard add tests for rate limiter (metric within variance, treated as regression)
6 c3d4e5f 89.0 +0.7 keep add boundary value tests for form validators
7 d4e5f6g 89.8 +0.8 keep add tests for session expiry edge cases
8 - 89.2 -0.6 discard mock external API calls (test isolation but metric regressed)
9 e5f6g7h 90.6 +0.8 keep add tests for concurrent request handling
10 f6g7h8i 91.1 +0.5 keep add tests for malformed JSON input handling
```
---
## Progress summary format
Print every 10 iterations. Use this exact format:
```
=== Autoresearch progress — iteration <N> ===
Goal: <original goal statement>
Baseline: <iteration 0 metric>
Current best: <best metric so far> (<total delta> from baseline)
Keeps: <count> (<keeps/total * 100>%)
Discards: <count>
Crashes: <count>
Top pattern: <the change type that has produced the most total delta>
Last 5: <sequence of keep/discard/crash for iterations N-4 through N>
Est. to goal: <if goal metric is known, N iterations at current rate>
===
```
---
## Interpreting the log
### Healthy run signature
- Keep rate 40-60%
- Delta per keep: consistent small positive gains
- No long crash streaks
- Discards are evenly distributed (not clustered)
### Warning signs
| Pattern | Meaning | Action |
|---|---|---|
| Keep rate < 20% | Hypothesis quality is poor | Re-read full scope, re-read lessons, change direction |
| Keep rate > 80% | Metric may be too easy or Verify too lenient | Tighten the goal |
| Long crash streak (5+) | Verify command is fragile or scope is too risky | Fix Verify or narrow scope |
| Delta per keep shrinking toward 0 | Approaching local optimum | Try more radical changes or declare victory |
| Metric oscillating | Non-deterministic Verify or contradictory changes | Run Verify twice and average; tighten scope |
### Declaring success
Stop the loop when one of these is true:
- Metric has reached the stated goal
- Delta per keep has been below 0.1% for 20 consecutive iterations
(local optimum with current scope)
- All directions have been exhausted (lessons file confirms this)
In all cases, print a final summary and write a lessons entry covering
the full run before stopping.
---
## File hygiene
- Add `autoresearch-results.tsv` to `.gitignore`. It is a working file.
- Do not edit it manually during a run.
- Between runs, you may archive it:
`mv autoresearch-results.tsv autoresearch-results-<date>.tsv`
and start fresh, but keep the lessons file — that is the persistent memory.
@@ -0,0 +1,171 @@
# Security workflow — `autoresearch security` mode
Autonomous security audit using STRIDE threat modelling and OWASP categories.
Finds vulnerabilities, classifies them by severity, and optionally fixes
confirmed critical and high findings via an autoresearch loop.
---
## Invocation
```
autoresearch security # full audit, report only
autoresearch security --fix # audit + auto-fix confirmed findings
autoresearch security --fail-on critical # end with a FAIL verdict if critical found
autoresearch security --scope src/api/ # audit a specific directory only
```
---
## Phase 1 — Asset discovery
Map the attack surface:
1. Identify all entry points: API routes, form handlers, file uploads,
auth flows, webhooks, admin panels
2. Identify all data stores: databases, caches, file system writes,
environment variables, secrets
3. Identify all trust boundaries: public vs authenticated, user vs admin,
internal vs external services
4. Map data flows: what user input reaches what data store via what path
Output: `security/audit-<timestamp>/attack-surface-map.md`
### Live threat intelligence
Use `WebSearch` to seed the audit with current threats:
```
WebSearch: [your stack] common vulnerabilities [current year]
WebSearch: [your main framework] CVE [current year]
WebSearch: OWASP top 10 [current year]
```
Add any newly discovered attack patterns to the audit queue.
This ensures the audit covers threats that postdate your static analysis tools.
---
## Phase 2 — STRIDE threat model
For each asset and trust boundary, model threats across all 6 STRIDE categories:
| Category | Question to ask |
|---|---|
| **S**poofing | Can an attacker impersonate a user, service, or system? |
| **T**ampering | Can input be modified to alter data or behaviour unexpectedly? |
| **R**epudiation | Can actions be performed without a traceable audit trail? |
| **I**nformation disclosure | Can sensitive data be accessed by unauthorised parties? |
| **D**enial of service | Can the service be made unavailable through normal inputs? |
| **E**levation of privilege | Can a lower-privilege user gain higher-privilege access? |
Output: `security/audit-<timestamp>/threat-model.md`
---
## Phase 3 — Autonomous audit loop
```
LOOP (through all attack vectors from threat model):
1. Select next untested attack vector
2. Deep-dive into the relevant code (read fully — do not skim)
3. Attempt to construct a concrete exploit scenario
4. Validate with code evidence (file:line + exact scenario)
5. Classify: severity + OWASP category + STRIDE tag
6. Log to security-audit-results.tsv
7. Print coverage summary every 5 iterations
8. Continue until all vectors tested
```
### Severity classification
| Severity | Definition |
|---|---|
| Critical | Exploitable without authentication, leads to full compromise or data breach |
| High | Exploitable with low-privilege access, significant impact |
| Medium | Requires specific conditions, moderate impact |
| Low | Minor information disclosure, no direct exploitation path |
| Info | Best practice violation, no immediate security impact |
### Evidence requirement
Every finding MUST have:
- File path and line number
- Exact vulnerable code snippet (copy from source, do not paraphrase)
- Concrete exploit scenario (how an attacker would trigger this)
- Proof of exploitability (not theoretical — show the actual path)
Findings without concrete evidence are logged as "unconfirmed" and flagged
for manual review, not included in the fix loop.
---
## Phase 4 — Report generation
Output folder: `security/audit-<timestamp>/`
```
security/audit-20260325-1430/
├── overview.md ← executive summary + finding counts by severity
├── threat-model.md ← STRIDE analysis per asset
├── attack-surface-map.md ← entry points, data flows, trust boundaries
├── findings.md ← all confirmed findings, sorted by severity
├── owasp-coverage.md ← coverage matrix — which OWASP categories checked
├── recommendations.md ← fix guidance for each confirmed finding
└── security-audit-results.tsv ← machine-readable log of all iterations
```
Print summary:
```
=== Security audit summary ===
Critical: <N>
High: <N>
Medium: <N>
Low: <N>
Info: <N>
Vectors tested: <N> / <total>
OWASP categories covered: <list>
Full report: security/audit-<timestamp>/overview.md
===
```
---
## Phase 5 — Auto-fix loop (with `--fix`)
Only runs when `--fix` flag is passed.
Only fixes **Confirmed Critical and High** findings.
Uses `recommendations.md` as the fix guide for each finding.
```
FOR EACH confirmed Critical/High finding:
1. Read the finding + recommendation
2. Make ONE targeted fix
3. git commit the fix
4. Re-run the specific exploit scenario to verify it no longer works
5. Run full test suite to confirm no regressions
6. If tests break → revert, try alternative fix
7. Maximum 3 attempts per finding, then skip and flag for manual review
8. Log fix outcome to fix-log.md
```
---
## Verdict mode (`--fail-on`)
```
autoresearch security --fail-on critical
```
The audit ends with an explicit verdict line in `overview.md`:
```
VERDICT: FAIL — 2 findings at or above `critical`
VERDICT: PASS — no findings at or above `critical`
```
A skill run has no process exit code, so do not wire this into a CI gate as if
it did — use a real scanner for blocking merges. What it *is* good for is an
unattended scheduled audit: a Qoder Automation running this mode reports the
verdict, and you act on it.
@@ -0,0 +1,164 @@
# Ship workflow — `autoresearch ship`
Run a pre-flight checklist before shipping — tests, types, lint, bundle size,
security basics, and a final autoresearch pass on anything that fails.
The ship workflow is not just a checklist. It runs an autoresearch loop on
each failing gate until it passes, then re-checks. You don't ship broken.
You ship when everything is green.
---
## Invocation
```
autoresearch ship
```
Optional flags:
```
autoresearch ship --fast # skip slow checks (lighthouse, e2e)
autoresearch ship --loop N # max N autoresearch iterations per gate (default: 20)
autoresearch ship --dry-run # report status without fixing anything
```
---
## The ship checklist
The workflow runs these gates in order. Each gate that fails triggers an
autoresearch sub-loop to fix it before moving to the next gate.
### Gate 1 — Tests pass
```bash
npm test # Node.js
pytest # Python
go test ./... # Go
cargo test # Rust
```
If tests fail → autoresearch loop on `src/**/*.ts` (or equivalent) with
metric: failing test count (lower is better), max 20 iterations.
### Gate 2 — No type errors
```bash
npx tsc --noEmit # TypeScript
mypy src/ # Python
```
If errors found → autoresearch loop on `src/**/*.ts` with
metric: error count (lower is better), max 20 iterations.
### Gate 3 — No lint errors
```bash
npx eslint src/ # JavaScript/TypeScript
ruff check src/ # Python
golangci-lint run # Go
```
If errors found → autoresearch loop with metric: lint error count (lower is better).
Auto-fixable errors are fixed first (`--fix` flag), then the loop handles the rest.
### Gate 4 — Bundle size (if applicable)
Only runs for frontend projects (detected: `next.config.*`, `vite.config.*`,
`webpack.config.*`).
```bash
npm run build 2>&1 | grep "First Load JS"
```
Threshold: warn if > 300KB, block if > 500KB (configurable via `.autoresearch.yml`).
If over threshold → autoresearch loop on `src/**/*.tsx, src/**/*.ts` with
metric: bundle size in KB (lower is better), max 20 iterations.
### Gate 5 — No hardcoded secrets
```bash
git diff HEAD~1 --diff-filter=A | grep -iE "(api_key|secret|password|token)\s*=\s*['\"][^'\"]{8,}"
```
If secrets found → do NOT autoresearch. Flag for human review. Block ship.
### Gate 6 — Dependency audit
```bash
npm audit --audit-level=high # Node.js
pip-audit # Python
```
If critical vulnerabilities found → autoresearch loop to update affected
dependencies, max 10 iterations.
---
## Ship report
After all gates pass, print:
```
=== Ship report ===
Tests: ✓ PASS (247 passing)
Types: ✓ PASS (0 errors)
Lint: ✓ PASS (0 errors)
Bundle: ✓ PASS (187KB)
Secrets: ✓ PASS (none detected)
Deps: ✓ PASS (0 high/critical)
Autoresearch loops run: <N>
Total improvements: <M> iterations kept
Ready to ship. Run: git push && <your deploy command>
===
```
If any gate is still failing after the max iterations:
```
=== Ship report ===
Tests: ✓ PASS
Types: ✗ FAIL (3 errors remaining after 20 iterations)
→ manual fix required: src/auth/session.ts:47
Ship BLOCKED. Fix the above before shipping.
===
```
---
## Web research post-check
After all gates pass, use `WebSearch` to check:
```
WebSearch: [your framework] [version] known issues [current year]
WebSearch: [your main dependencies] security advisory [current year]
```
If any critical advisories surface that the dependency audit missed,
flag them before shipping. This is a final sanity check that goes beyond
what local tools can detect.
---
## Configuration via `.autoresearch.yml`
Create this file in your project root to customise ship behaviour:
```yaml
ship:
bundle_warn_kb: 300
bundle_block_kb: 500
max_iterations_per_gate: 20
skip_gates:
- lighthouse # skip if no local server available
extra_gates:
- name: "E2E tests"
command: "npx playwright test"
metric: "failing tests (lower is better)"
max_iterations: 10
```
@@ -0,0 +1,144 @@
# Web research patterns
Qoder exposes a `WebSearch` tool (and `WebFetch` to read a promising result in
full). Use them as a verification supplement — not a replacement for the Verify
command, but an additional signal when local scripts alone cannot capture
quality.
---
## When to use WebSearch in the loop
| Goal type | Use WebSearch for | Example query |
|---|---|---|
| SEO content | Check competing pages, keyword signals | `[target keyword] filetype:md OR site:*.dev` |
| API correctness | Verify endpoint signatures, check for deprecations | `[library] [method] deprecated 2025 OR 2026` |
| Dependency versions | Confirm latest stable before updating | `[package name] latest stable version` |
| Best practices | Check if your approach matches current consensus | `[pattern] best practice [language] 2026` |
| Content accuracy | Ground-truth check generated facts | `[claim] site:official-source.com` |
| Bundle/perf baselines | Compare your score to current industry benchmarks | `[framework] bundle size benchmark 2026` |
---
## Pattern 1 — SEO content verification
Use when: optimising blog posts, landing pages, documentation for search.
After your local score script runs, supplement with:
```
WebSearch: [target keyword] to see what the top 3 results have in common.
Note: heading structure, content length, semantic coverage, internal links.
If top results consistently have trait X that your content lacks,
add "add trait X" as the next hypothesis.
```
This gives you signal that no local readability or keyword-density script can
provide — what the search engine is actually rewarding right now.
---
## Pattern 2 — API currency check
Use when: refactoring code that calls external libraries or APIs.
Before committing any API-surface change:
```
WebSearch: [library name] [method name] changelog 2026
WebSearch: [library name] [method name] deprecated
```
If search returns deprecation notices or breaking changes, note the current
replacement pattern and use that as the hypothesis instead.
This prevents iterating toward a working-but-deprecated solution that will
break on the next library update.
---
## Pattern 3 — Dependency version check
Use when: the Verify command suggests a dependency might be outdated, or when
optimising for security/bundle size.
```
WebSearch: [package name] npm latest 2026
WebSearch: [package name] security advisory
```
Cross-reference against what is in `package.json`, `go.mod`, `requirements.txt`
or equivalent. Use the delta as a hypothesis: "update [package] from X to Y,
check if metric improves."
---
## Pattern 4 — Best practice calibration
Use when: stuck after 5 consecutive discards and local ideas are exhausted.
```
WebSearch: [language/framework] [metric type] optimisation techniques 2026
WebSearch: how to improve [metric] in [stack]
```
Extract 3 concrete, actionable techniques from the top results — use `WebFetch`
on the most promising one if the snippet is too thin. Do not extract vague
advice. Add each as a separate iteration hypothesis. This restocks your
hypothesis pool with externally validated approaches.
---
## Pattern 5 — Benchmark calibration
Use when: you want to know if your current metric value is good relative to
the industry, not just relative to your own baseline.
```
WebSearch: [framework] [metric] benchmark 2026 average
```
If your metric is already at or above the industry median, note this and
shift the goal definition (e.g. from "reduce bundle size" to "reduce bundle
size while improving lighthouse score").
---
## Pattern 6 — Content accuracy check
Use when: the Verify command measures style/structure but not factual accuracy
(e.g. documentation, blog posts, runbooks).
```
WebSearch: [specific claim in content] site:[authoritative source]
```
If the authoritative source contradicts your content, flag this as a
required fix before the next iteration (accuracy issues override metric gains).
---
## Rules for using WebSearch
1. **Supplement, never replace.** The Verify command runs every iteration.
Web research adds signal; it does not replace the metric.
2. **Search at the right time.** Patterns 1-3 supplement Phase 5 (Verify).
Patterns 4-5 are for stuck recovery in Phase 1 (Review). Pattern 6
runs in Phase 6 (Decide) when a kept iteration touches factual claims.
3. **Extract actionable hypotheses.** Never let a search result produce a
vague conclusion ("content could be better"). Always turn the search
result into a specific next hypothesis ("add a FAQ section with 3
questions, which top-ranking competitors include").
4. **Log the research signal.** When a search result influences a hypothesis,
note it in the results log description:
`"added FAQ section (web research: top results for [kw] all include FAQ)"`
5. **Don't over-search.** Maximum one WebSearch call per iteration. If you are
searching every iteration, your Verify command is probably too weak —
strengthen the local script instead.
6. **Cite, don't guess.** `WebSearch` results come with source links; never
turn an unverified snippet into a change that the Guard cannot catch.
@@ -125,7 +125,7 @@ func startThingCacheInvalidationListener() {
| Auth Source | `repository/auth_source_cache.go` | Otter | Redis JSON | `oauth:auth_source_invalidation` ✅ |
| OAuth 用户/Token | `apps/oauth/cache.go` | 自研 map | Redis JSON | ❌ 无 pub/sub(历史债) |
| 推送渠道 | `repository/push_channel.go` | 无 | Redis JSON | ❌ 仅 Redis Del |
| Storage 驱动 | `internal/storage/storage.go` | RWMutex 快照 | — | `storage:config_invalidation` ✅ |
| Storage 驱动 | `internal/infra/objectstore/storage.go` | RWMutex 快照 | — | `storage:config_invalidation` ✅ |
## 新增缓存工作流
@@ -210,7 +210,7 @@ make code-check
## 相关文件
- L1 引擎:`pkg/cache/ram/cache.go`
- DB/Redis 助手:`internal/db/redis.go`(`GetJSON`, `SetJSON`, `HGetJSON`, `PrefixedKey`)
- DB/Redis 助手:`internal/infra/persistence/redis.go`(`GetJSON`, `SetJSON`, `HGetJSON`, `PrefixedKey`)
- 金标准:`internal/repository/system_config_cache.go`
- 上传元数据:`internal/apps/upload/cache/meta_cache.go`
- Auth Source:`internal/repository/auth_source_cache.go`
@@ -1,25 +1,25 @@
---
name: "clickhouse-batchwriter"
description: "Wavelet 项目专用:当新增或修改 ClickHouse 批量写入、接入 internal/db/batchwriter、将业务域异步 flush 到分析表、迁移 risk_control/节点访问日志/可观测时序写入、或评估 async_insert 与背压策略时必须使用。本技能指导分层职责、各域独立 Writer 实例、repository 批量 API 与禁止写法。"
description: "Wavelet 项目专用:当新增或修改 ClickHouse 批量写入、接入 internal/infra/persistence/batchwriter、将业务域异步 flush 到分析表、迁移 risk_control/节点访问日志/可观测时序写入、或评估 async_insert 与背压策略时必须使用。本技能指导分层职责、各域独立 Writer 实例、repository 批量 API 与禁止写法。"
---
# ClickHouse 批量写入开发
开始前阅读根目录 `AGENTS.md`。ClickHouse 是辅助 OLAP 存储,**厌恶高频单条写入**(过多小 part);写入路径必须优先批量或异步聚合。
DDL 与表结构变更见 `database-migration` 技能;本技能只覆盖**运行时写入架构**。
DDL 与表结构变更见 `database-migration` 技能。日志/分析用途表的判定、三库回落与切换见 `logstore` 技能。本技能只覆盖**运行时写入架构**。
## 分层职责
| 层级 | 路径 | 职责 |
| :--- | :--- | :--- |
| 连接 | `internal/db/clickhouse.go` | `ChConn`(原生批量写)、`ChDB`(GORM 查询);禁止在业务包直接 `clickhouse.Open` |
| 批量框架 | `internal/db/batchwriter/` | 泛型队列 + 按条数/时间 flush + 非阻塞入队 + 优雅停机;**各业务域独立实例** |
| 连接 | `internal/infra/persistence/clickhouse.go` | `ChConn`(原生批量写)、`ChDB`(GORM 查询);禁止在业务包直接 `clickhouse.Open` |
| 批量框架 | `internal/infra/persistence/batchwriter/` | 泛型队列 + 按条数/时间 flush + 非阻塞入队 + 优雅停机;**各业务域独立实例** |
| Model | `internal/model/analytics/` | 列定义、`TableName()`、`BatchInsertSQL()`(及可选 `InsertColumns()`) |
| Repository | `internal/repository/analytics/` | `BatchInsert*` / `BatchInsertNodeAccessLogs` 等;`PrepareBatch` + 多行 `Append` + 一次 `Send` |
| Apps | `internal/apps/<domain>/` | 采集、入队、背压;`FlushFunc` 只调 repository,不写 SQL、不 `PrepareBatch` |
| 装配 | `internal/bootstrap/bootstrap.go` | 进程启动时调用 `Writer.Start`;初始化时需调用 `lifecycle.OnShutdown` 挂载停机钩子 |
| 生命周期 | `internal/lifecycle/lifecycle.go` | 统一协调全局并发优雅停机,业务包无需在 `bootstrap.go` 中硬编码 `Stop` 逻辑 |
| Apps | `internal/apps/<domain>/` | 采集、入队、背压;`FlushFunc` 只调 logstore / repository,不写 SQL、不 `PrepareBatch` |
| 装配 | `internal/platform/bootstrap/bootstrap.go` | 进程启动时调用 `Writer.Start`;初始化时需调用 `lifecycle.OnShutdown` 挂载停机钩子 |
| 生命周期 | `internal/platform/lifecycle/lifecycle.go` | 统一协调全局并发优雅停机,业务包无需在 `bootstrap.go` 中硬编码 `Stop` 逻辑 |
**禁止**在 Handler / middleware 内直接 `db.ChConn.PrepareBatch`;**禁止**在 repository 内启动 goroutine 或维护全局 channel(队列生命周期由 apps + bootstrap 或专用 writer 包负责)。
@@ -37,6 +37,7 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush
- `QueueSize`: 10_000
- `MaxBatchSize`: 1_000
- `MinBatchSize`: 50(未达阈值则跳过按时间 flush,除非设了 `MaxFlushWait`)
- `FlushInterval`: 1s
各域可独立覆盖;可观测低频指标可用更小 `MaxBatchSize`(如 100)与更长 `FlushInterval`(如 2–5s),但**不要**退化为逐条 `Send`。
@@ -49,7 +50,8 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush
### FlushFunc 规范
- 签名:`func(ctx context.Context, items []T) error`
- 内部调用 `internal/repository/analytics` 的 `BatchInsert*`(传入 `[]analyticsmodel.X`)
- **日志/分析用途表**:`logstore.Active(ctx)` 再调对应 `BatchInsert*`。禁止 apps 直连 `analyticsrepo` 或 `db.ChConn`。
- 仅 CH、无需主库回落的分析表:才直接调 `repository/analytics` 的 `BatchInsert*`。
- 在 flush 边界记录一次错误日志,不要把 DB 驱动错误直接暴露给 HTTP 客户端
- `Start` 使用 `context.WithoutCancel(parent)`,避免请求 ctx 取消中断后台 flush
@@ -57,28 +59,29 @@ writer.Stop(stopCtx) // close 队列 + drain + 最终 flush
每个业务域拥有自己的 `Writer`、配置与 `FlushFunc`:
| 域 | 表 | 现状 | 目标形态 |
| :--- | :--- | :--- | :--- |
| 管理端审计 | `w_user_access_logs` | `risk_control` → `batchwriter` + `analyticsrepo.BatchInsert` | 已接入 |
| 边缘访问日志 | `of_node_access_logs` | `openflare/chwriter` 异步 flush | 已接入 |
| 可观测时序 | `of_node_metric_snapshots` 等 5 表 | `openflare/chwriter` 五表独立 writer + 进程内短 TTL 去重 | 已接入 |
| 域 | 表 | 写入路径 |
| :--- | :--- | :--- |
| 管理端审计 | `w_user_access_logs` | `risk_control` → `batchwriter` → `logstore.Active` |
| 边缘访问日志 | `of_node_access_logs` | `openflare/chwriter` → `logstore.Active` |
| 可观测时序 | `of_node_metric_snapshots` 等 | `openflare/chwriter` 分表 writer + 进程内短 TTL 去重 → `logstore.Active` |
**不要**把 audit、access log、observability 并入同一 channel。
## 新增 ClickHouse 写入工作流
1. **Model**:在 `internal/model/analytics/` 定义 struct 与 `BatchInsertSQL()`(列顺序与 goose DDL 一致)。
2. **Goose DDL**:在 `internal/db/migrator/goose/clickhouse/` 新增迁移(见 `database-migration`)。
2. **Goose DDL**:在 `internal/infra/persistence/migrator/goose/clickhouse/` 新增迁移(见 `database-migration`)。
3. **Repository**:实现 `BatchInsertX(ctx, []analyticsmodel.X) error`:
- `len(items)==0` 直接返回
- `db.ChConn == nil` 返回明确错误
- 一次 `PrepareBatch` → 循环 `Append` → 一次 `Send`
4. **Writer 胶水**(`internal/apps/<domain>/` 或 `internal/repository/analytics/<domain>_writer.go`):
4. **Writer 胶水**(`internal/apps/<domain>/`):
- `New` + `Start`,并在初始化逻辑内通过 `lifecycle.OnShutdown("your_writer_name", Stop)` 注册停机回调
- 日志表的 `FlushFunc` 调 `logstore.Active`(见 `logstore` skill)
- 业务路径 `TryEnqueue`;HTTP 背压用 `IsFull()`
5. **测试**:
- repository:mock `ChConn` 验证 `BatchInsertSQL` 与 append 列数
- batchwriter:`go test ./internal/db/batchwriter`
- batchwriter:`go test ./internal/infra/persistence/batchwriter`
6. 运行 `make code-check`;有 API 变更时 `make swagger`。
## 背压与丢弃策略
@@ -110,7 +113,7 @@ var globalChan chan any
## async_insert(补充,非主方案)
可在 `internal/db/clickhouse.go` 的 `Settings` 增加服务端异步写入作为第二层防护:
可在 `internal/infra/persistence/clickhouse.go` 的 `Settings` 增加服务端异步写入作为第二层防护:
```go
"async_insert": 1,
@@ -122,15 +125,10 @@ var globalChan chan any
## Bootstrap 装配示例
```go
// internal/bootstrap/bootstrap.go(示意)
var userAccessLogWriter *batchwriter.Writer[*analytics.UserAccessLog]
// internal/platform/bootstrap/bootstrap.go(示意)
func RegisterAPI(ctx context.Context) {
// ...
if config.Config.ClickHouse.Enabled {
initUserAccessLogWriter(ctx) // Start writer
risk_control.BindWriter(userAccessLogWriter) // 或逐步替换 InitLogWriter
}
// 日志 writer 不依赖 clickhouse.enabled:flush 时由 logstore 选库
risk_control.InitLogWriter(ctx)
}
```
@@ -141,7 +139,7 @@ func RegisterAPI(ctx context.Context) {
## 验证清单
```bash
go test ./internal/db/batchwriter
go test ./internal/infra/persistence/batchwriter
go test ./internal/repository/analytics
make code-check
```
@@ -149,15 +147,17 @@ make code-check
- flush 按 `MaxBatchSize` 与 `FlushInterval` 触发
- `Stop` 能 drain 队列内剩余项
- repository 层无 goroutine、无 channel
- `clickhouse.enabled: false` 时不 `Start` writer、不入队
- 日志表:`clickhouse.enabled: false` 时 writer 仍 `Start`,flush 走主库 logstore
- 仅 CH 的分析表:未启用 CH 时不要 `Start`、不要入队
## 相关文件速查
- 框架:`internal/db/batchwriter/{config,writer,errs}.go`
- 连接:`internal/db/clickhouse.go`
- 框架:`internal/infra/persistence/batchwriter/{config,writer,errs}.go`
- 连接:`internal/infra/persistence/clickhouse.go`
- 审计写入:`internal/apps/risk_control/logics.go`
- OpenFlare 写入胶水:`internal/apps/openflare/chwriter/writer.go`
- 节点访问日志 repository:`internal/repository/analytics/node_access_log_writer.go`
- 可观测 repository:`internal/repository/analytics/node_observability_writer.go`
- 生命周期管理器:`internal/lifecycle/lifecycle.go`
- Bootstrap:`internal/bootstrap/bootstrap.go`
- 日志抽象:`internal/repository/logstore`
- 节点访问日志 CH 实现:`internal/repository/analytics/node_access_log_writer.go`
- 可观测 CH 实现:`internal/repository/analytics/node_observability_writer.go`
- 生命周期管理器:`internal/platform/lifecycle/lifecycle.go`
- Bootstrap:`internal/platform/bootstrap/bootstrap.go`
@@ -1,17 +1,17 @@
---
name: "database-migration"
description: "Wavelet 项目专用:当新增或修改数据库表结构、索引、初始化数据、系统配置 seed、模板 seed、默认管理员、goose SQL 迁移、internal/db/migrator、ClickHouse 分析库 DDL 或数据库升级流程时必须使用。本技能指导在 internal/db/migrator/goose 下编写 PostgreSQL/SQLite 双方言 SQL 迁移,以及在 goose/clickhouse 下编写 ClickHouse 单方言分析表迁移,并完成验证。"
description: "Wavelet 项目专用:当新增或修改数据库表结构、索引、初始化数据、系统配置 seed、模板 seed、默认管理员、goose SQL 迁移、internal/infra/persistence/migrator、ClickHouse 分析库 DDL 或数据库升级流程时必须使用。本技能指导在 internal/infra/persistence/migrator/goose 下编写 PostgreSQL/SQLite 双方言 SQL 迁移,以及在 goose/clickhouse 下编写 ClickHouse 单方言分析表迁移,并完成验证。"
---
# Wavelet 数据库升级操作指南
Wavelet 使用 `github.com/pressly/goose/v3` 执行 SQL 迁移。迁移入口是 `internal/db/migrator.Migrate()`,SQL 文件嵌入在二进制中。
Wavelet 使用 `github.com/pressly/goose/v3` 执行 SQL 迁移。迁移入口是 `internal/infra/persistence/migrator.Migrate()`,SQL 文件嵌入在二进制中。
## 基本规则
- SQL 迁移文件放在:
- `internal/db/migrator/goose/postgres/`
- `internal/db/migrator/goose/sqlite/`
- `internal/infra/persistence/migrator/goose/postgres/`
- `internal/infra/persistence/migrator/goose/sqlite/`
- PostgreSQL 和 SQLite 必须使用同一个版本号、同一个语义文件名。
- 迁移文件使用 goose SQL 标记:
@@ -51,7 +51,7 @@ Wavelet 使用 `github.com/pressly/goose/v3` 执行 SQL 迁移。迁移入口是
7. 至少运行:
```bash
go test ./internal/db/migrator
go test ./internal/infra/persistence/migrator
go test ./internal/model ./internal/apps/config ./internal/apps/admin/system_config
make code-check
```
@@ -81,7 +81,7 @@ make code-check
ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独立**的迁移与访问管线:
- 主库(PG/SQLite):业务事务数据、`goose_db_version`、双方言 SQL。
- 分析库(ClickHouse):访问日志、统计聚合等分析型数据、`goose_clickhouse_version`、单方言 SQL。
- 分析库(ClickHouse):分析型数据、`goose_clickhouse_version`、单方言 SQL。日志用途表还必须在主库建回落并走 `logstore`(见该 skill);CH 目录仍只放 CH DDL。
**不要**把 ClickHouse 表结构混入 PG/SQLite 迁移目录,也**不要**在 `support-files/`、`internal/apps/` 或 `internal/repository/` 中手写 DDL。
@@ -89,10 +89,10 @@ ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独
| 路径 | 职责 |
| :--- | :--- |
| `internal/db/migrator/goose/clickhouse/` | **唯一** ClickHouse DDL 来源(goose SQL,嵌入二进制) |
| `internal/infra/persistence/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/infra/persistence/clickhouse.go` | 连接初始化(`ChConn` 原生批量、`ChDB` GORM 查询) |
### 迁移入口与版本表
@@ -116,16 +116,16 @@ ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独
按以下顺序落地,避免列名或类型漂移:
1. **Model**:在 `internal/model/analytics/` 定义 struct,`gorm:"column:..."` 与 DDL 列名一一对应;实现 `TableName()`,批量写入表可提供 `InsertColumns()` / `BatchInsertSQL()`。
2. **Goose SQL**:在 `internal/db/migrator/goose/clickhouse/` 新增递增版本文件(格式同主库,如 `YYYYMMDDNNNN_create_xxx.sql`),编写 `-- +goose Up` / `-- +goose Down`。
2. **Goose SQL**:在 `internal/infra/persistence/migrator/goose/clickhouse/` 新增递增版本文件(格式同主库,如 `YYYYMMDDNNNN_create_xxx.sql`),编写 `-- +goose Up` / `-- +goose Down`。
3. **Repository**:在 `internal/repository/analytics/` 实现 `BatchInsert*`(`db.ChConn` 一次 `PrepareBatch` + 多行 `Append` + 一次 `Send`)与查询(`db.ChDB`);连接未初始化时返回明确错误,**不要**在 handler 写 SQL,**不要**在 repository 内维护 channel/goroutine。
4. **Apps**:在 `internal/apps/<domain>/` 编排采集与入队;高频写入通过 `internal/db/batchwriter` 各域独立实例异步 flush(详见 `clickhouse-batchwriter` 技能),`FlushFunc` 只调 repository `BatchInsert*`;管理端统计 API 只读 repository,不触达 DDL。
4. **Apps**:在 `internal/apps/<domain>/` 编排采集与入队;高频写入通过 `internal/infra/persistence/batchwriter` 各域独立实例异步 flush(详见 `clickhouse-batchwriter` 技能)。**日志/分析用途表**还要同时建 PG/SQLite 回落并接入 `logstore`(见 `logstore` 技能),`FlushFunc` 调 `logstore.Active` 而不是 `analyticsrepo`;普通业务分析表仍只读 repository。
### ClickHouse 验证
至少运行:
```bash
go test ./internal/db/migrator
go test ./internal/infra/persistence/migrator
go test ./internal/repository/analytics
make code-check
```
@@ -15,7 +15,7 @@ Wavelet 将「对象存储」与「上传业务」分为两层,**禁止混用
| 层级 | 包路径 | 职责 | 业务是否直接调用 |
| :--- | :--- | :--- | :--- |
| **对象存储引擎** | `internal/storage` | `Backend` 接口:`Put` / `Get` / `Delete` / `Test`;按配置切换 Local / S3 / R2 / OSS / WebDAV | **禁止**(仅 upload 域内部使用) |
| **对象存储引擎** | `internal/infra/objectstore` | `Backend` 接口:`Put` / `Get` / `Delete` / `Test`;按配置切换 Local / S3 / R2 / OSS / WebDAV | **禁止**(仅 upload 域内部使用) |
| **上传域服务** | `internal/apps/upload` | `w_uploads` 记录、权限、秒传、统计、文件服务、`upload.Ingest` | **必须** |
| **上传 HTTP 入口** | `internal/apps/upload/handler` | `POST /api/v1/upload` 等 multipart 接口 | 前端 / 用户侧上传 |
| **文件访问** | `internal/apps/upload/filesrv` | `GET /f/:id` 流式响应、访问控制、图片 WebP 压缩 | 展示 / 下载 |
@@ -83,8 +83,8 @@ invoice.FilePath = "uploads/2026/01/02/123.pdf"
import (
"bytes"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/model"
"OpenFlare/internal/apps/upload"
"OpenFlare/internal/model"
)
func ingestMirrorFile(ctx context.Context, userID uint64, data []byte, hash, filename, mime, ext string) (model.Upload, error) {

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