This file provides guidance to Claude Code when working with code in this repository.
Octo-server is the Go backend for DMWork (enterprise IM platform). It handles business logic on top of WuKongIM for messaging transport.
- Go Module:
github.com/Mininglamp-OSS/octo-server - Go Version: 1.25
- Shared Library:
github.com/Mininglamp-OSS/octo-lib(config, wkhttp, testutil, register, model) - Default Branch:
main
# Build
docker build -t octo-server .
# Run tests (single module)
go test ./modules/group/...
go test ./modules/message/ -run TestSendMsg
# Run all tests
go test ./...
# Lint
golangci-lint run ./...HTTP (Gin/wkhttp) β Auth Middleware (pkg/auth/) β Space Middleware β API Handler β Service β DB (MySQL/DBR)
β
WuKongIM (gRPC)
27 modules in modules/, each auto-registered via init() + register.AddModule().
Standard module structure:
1module.goβ registration entry (init()+register.AddModule())api*.goβ HTTP handlers implementingregister.APIRouter.Route(r *wkhttp.WKHttp)service.goβ business logic, typically definesIServiceinterfacedb*.goβ database operations usinggocraft/dbrmodel.goβ data models and response structssql/β SQL migrations embedded via//go:embed sql
| Package | Purpose |
|---|---|
pkg/auth/ |
Token parsing, CacheTokenParser, auth middleware |
pkg/errcode/ |
Error code definitions per module (group.go, message.go, user.go, oidc.go) |
pkg/httperr/ |
ResponseErrorL / ResponseErrorLWithStatus error facades |
pkg/i18n/ |
Localization SDK: codes registry, localizer, renderer, language negotiation, locales/ |
internal/ |
Internal wiring, module imports |
modules/base/event/ |
Async event system |
All user-facing error responses go through the i18n error envelope. Never use
c.ResponseError(errors.New(...)), c.ResponseErrorf(...), c.AbortWithStatusJSON(...),
or non-OK c.JSON(...) β these are legacy and bypass the localized envelope.
Two facades (pkg/httperr) β the envelope body is identical, only the wire status differs:
| Facade | Wire status | Use for |
|---|---|---|
ResponseErrorL(c, code, params, details) |
pinned 400 (D14 compat); real status in error.http_status |
default β every legacy-bearing endpoint |
ResponseErrorLWithStatus(c, code, params, details) |
the code's real HTTPStatus |
new endpoints only with no clients depending on fixed-400 (currently just modules/oidc bind); diverging from D14 needs maintainer sign-off |
httperr.ResponseErrorL(c, errcode.ErrGroupQueryFailed, nil, nil)Error codes β register in pkg/errcode/<module>.go:
ErrXxx = register(codes.Code{
ID: "err.server.<module>.<reason>", // or reuse err.shared.* (auth/rate/param/internal/not_found)
HTTPStatus: http.StatusBadRequest,
DefaultMessage: "English source (D4).", // zh-CN runtime translation goes in active.zh-CN.toml
SafeDetailKeys: []string{"field"}, // whitelist for details; all other keys are dropped
Internal: false, // see invariant below
})- 5xx βΊ
Internal=true(renderer hides the message + details; log the cause viazap.Errorbefore responding). 4xx codes must NOT be Internal. - Anti-enumeration: auth / verify failures map to ONE generic code (e.g. a single 401), never a per-reason code β the specific reason goes to logs only.
- Params vs Details (D15):
paramsinterpolate into the message template;detailsare structured fields surfaced to the client, filtered bySafeDetailKeys.
Per-module helpers live in modules/<module>/api_i18n.go (respond<Module>Xxx for detail-carrying shapes; mustLookupSharedCode resolves shared codes at init, panicking loudly if unregistered).
After adding/changing any code, these must pass (also enforced in CI):
make i18n-extract # regenerate en-US markers from codes.Register call sites
make i18n-extract-check # 100% recall: every registered code has a marker
make i18n-lint # D23 guard (no new raw error responses) + unregistered-code checkThen add the zh-CN translation to pkg/i18n/locales/active.zh-CN.toml (one ["id"] + other = "..." block per code).
Guard test: each migrated module has a Test<Module>NoLegacyResponseError source guard forbidding legacy/raw responses β add any new handler files to its list. Protocol endpoints that intentionally keep raw responses (e.g. OAuth2/OIDC browser-redirect flow) are exempted and tracked in tools/lint-direct-error-response/baseline.txt.
Emails: localized templates live in modules/base/common/emailtmpl/templates/{lang}/ (per-language subject/html/text, go:embed). Send functions take a lang arg resolved via i18n.OutboundLanguage(ctx) β never hardcode subject/body strings.
Use the shared middleware in octo-lib pkg/wkhttp/ratelimit.go β do NOT hand-roll Redis INCR/TTL counters for request-frequency limiting. Three layers, each sets X-RateLimit-Limit/Remaining/Scope/Retry-After headers, returns i18n rate.limited, and is fail-open on Redis errors:
| Middleware | Scope header | Dimension | Use for |
|---|---|---|---|
RateLimitMiddleware |
ip |
global per-IP | DDoS floor β already mounted globally in main.go (route.Use), don't re-add |
StrictIPRateLimitMiddleware(tag, rps, burst) |
strict:{tag} |
per-IP, per-endpoint | unauthenticated sensitive endpoints (login/register/sms/search/group_invite/space_invite) |
SharedUIDRateLimiter(r, ctx) (wraps UIDRateLimitMiddleware) |
uid |
per-login-user, shared bucket ratelimit:uid:{uid} |
default for authenticated endpoints |
SharedUIDRateLimiter (pkg/wkhttp/ratelimit_helper.go) is a process-wide singleton β one quota per UID across all mounted routes (default 2 rps / burst 60, tunable via DM_API_UID_RATELIMIT_RPS/_BURST). Mount it AFTER AuthMiddleware on the route group, else it can't read the uid and silently fails open:
auth := r.Group("/v1/foo", ctx.AuthMiddleware(r), appwkhttp.SharedUIDRateLimiter(r, ctx))Exception β per-resource cooldowns keyed by a business identity (phone/email/bind-session), which the IP/UID buckets cannot express, may use a hand-written Redis counter: e.g. sms_rate_limit:{zone}@{phone} (base/common/service_sms.go), email_rate_limit:{email} (base/common/service_email.go), OIDC bind attempt caps. These are intentional; generic HTTP request-frequency limiting is not.
Tests that hit UID-limited routes must reset the bucket in setup (ratelimit:uid:*) β see category test's resetUIDRateLimit; the bucket persists in Redis and is NOT cleared by CleanAllTables.
- ORM:
gocraft/dbrv2 - Migration files:
modules/<name>/sql/<yyyyMMdd>-<seq>_<name>.sql, embedded via//go:embed sql - Field naming: underscore (
util.AttrToUnderscore())
_, ctx := testutil.NewTestServer()
defer testutil.CleanAllTables(ctx)Tests require MySQL + Redis + WuKongIM running (see CI or make env-test in dmworkim).
- Commit messages: English, Conventional Commits (
feat:,fix:,test:,refactor:) - API routes: prefix
/v1/ - New modules: add blank import in
internal/modules.go - Auth: all routes go through
AuthMiddlewareunless explicitly excluded β document why if skipping - i18n: user-facing errors use
httperr.ResponseErrorL+ a registeredpkg/errcodecode; never rawc.ResponseError/c.JSON/AbortWithStatusJSON. Runmake i18n-extract-check+make i18n-lintafter touching codes (see Architecture βΊ Error Handling & i18n) - Rate limiting: mount
SharedUIDRateLimiter(auth routes) orStrictIPRateLimitMiddleware(unauth) β never hand-roll a Redis counter for request-frequency limiting (see Architecture βΊ Rate Limiting) - Space isolation: handlers that access user data must go through Space middleware
- Bot API (
modules/bot_api/): validate bot ownership before operations - Thread (
modules/thread/): verify parent channel access
This repo participates in the octo-spec
engineering standard. Shared rules live in .octospec/.
- Rules:
.octospec/rules/β the source of truth for this repo's conventions (error-handling, rate-limit, space-isolation, testing, commit-style). The global "constitution" rules are pulled into git-ignored.octospec/_global/viaoctospec-sync(pin in.octospec/manifest.yaml). - Before changing load-bearing behavior: read the rules whose
inject_whenmatches the files you are touching (see.octospec/rules/_index.yaml). - Tasks: capture goal / load-bearing list / out-of-scope / acceptance in
.octospec/tasks/<slug>/brief.md(template:tasks/_brief.template.md). - PRs: fill Linked Spec + the COMPREHENSION three questions for load-bearing, architectural, or P0 changes. Trivial changes (typo/docs/lint/config) are exempt.
This region is managed by octospec-sync; edit outside the markers.