From 9434c8312601dcf0b641db965c2d9e85515277f2 Mon Sep 17 00:00:00 2001 From: zhengwenfeng Date: Sun, 28 Jun 2026 01:02:28 +0800 Subject: [PATCH 1/5] =?UTF-8?q?[ADD]=E6=96=B0=E5=A2=9E=20golint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint.yml | 30 ++++++++++++++++++++++++ services/api/.golangci.yml | 47 ++++++++++++++++++++++++++++++++++++++ services/api/Makefile | 10 ++++++++ 3 files changed, 87 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 services/api/.golangci.yml create mode 100644 services/api/Makefile diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..863c2a8f --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +name: Lint + +on: + pull_request: + paths: + - 'services/api/**/*.go' + - 'services/api/go.mod' + - 'services/api/go.sum' + - 'services/api/.golangci.yml' + push: + branches: + - main + paths: + - 'services/api/**/*.go' + +jobs: + golangci: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: services/api/go.mod + cache-dependency-path: services/api/go.sum + - uses: golangci/golangci-lint-action@v6 + with: + version: latest + working-directory: services/api + args: --timeout=5m diff --git a/services/api/.golangci.yml b/services/api/.golangci.yml new file mode 100644 index 00000000..ffc00241 --- /dev/null +++ b/services/api/.golangci.yml @@ -0,0 +1,47 @@ +run: + timeout: 5m + tests: true + modules-download-mode: readonly + +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gofmt + - goimports + - misspell + - unconvert + - unparam + - gocritic + - gosec + - bodyclose + - sqlclosecheck + - nilerr + - errname + - errorlint + - revive + - stylecheck + - whitespace + - gocyclo + - gofumpt + +linters-settings: + errcheck: + check-blank: true + govet: + check-shadowing: true + gocyclo: + min-complexity: 15 + revive: + rules: + - name: exported + disabled: true + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/services/api/Makefile b/services/api/Makefile new file mode 100644 index 00000000..e490acf8 --- /dev/null +++ b/services/api/Makefile @@ -0,0 +1,10 @@ +.PHONY: lint lint-fix fmt + +lint: + golangci-lint run ./... + +lint-fix: + golangci-lint run --fix ./... + +fmt: + golangci-lint run --fix --disable-all --enable=gofmt,goimports,gofumpt ./... From e6ff98eecddfedcf42d10ff6f920a7da954fc440 Mon Sep 17 00:00:00 2001 From: zhengwenfeng Date: Sun, 28 Jun 2026 01:11:08 +0800 Subject: [PATCH 2/5] [UPDATE] --- Makefile | 10 ++++++++++ services/api/.golangci.yml | 3 +-- services/api/Makefile | 10 ---------- 3 files changed, 11 insertions(+), 12 deletions(-) create mode 100644 Makefile delete mode 100644 services/api/Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..4ccd346f --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: lint lint-fix fmt + +lint: + cd services/api && golangci-lint run ./... + +lint-fix: + cd services/api && golangci-lint run --fix ./... + +fmt: + cd services/api && golangci-lint run --fix --disable-all --enable=gofmt,goimports,gofumpt ./... diff --git a/services/api/.golangci.yml b/services/api/.golangci.yml index ffc00241..dd40f6b3 100644 --- a/services/api/.golangci.yml +++ b/services/api/.golangci.yml @@ -2,6 +2,7 @@ run: timeout: 5m tests: true modules-download-mode: readonly + go: '1.24' linters: enable: @@ -32,8 +33,6 @@ linters: linters-settings: errcheck: check-blank: true - govet: - check-shadowing: true gocyclo: min-complexity: 15 revive: diff --git a/services/api/Makefile b/services/api/Makefile deleted file mode 100644 index e490acf8..00000000 --- a/services/api/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -.PHONY: lint lint-fix fmt - -lint: - golangci-lint run ./... - -lint-fix: - golangci-lint run --fix ./... - -fmt: - golangci-lint run --fix --disable-all --enable=gofmt,goimports,gofumpt ./... From 4e08e8863af31b5c1650d5b866ec489178116291 Mon Sep 17 00:00:00 2001 From: zhengwenfeng Date: Sun, 28 Jun 2026 01:24:38 +0800 Subject: [PATCH 3/5] [UPDATE] --- Makefile | 8 +-- services/api/.golangci.yml | 38 +++++++------- services/api/cmd/agentforge-api/main.go | 2 +- services/api/internal/agents/repository.go | 2 +- services/api/internal/auth/repository.go | 2 +- services/api/internal/channels/model.go | 16 +++--- services/api/internal/channels/repository.go | 2 +- services/api/internal/config/config.go | 2 +- services/api/internal/db/db_test.go | 6 +-- services/api/internal/db/migrate.go | 2 +- services/api/internal/errors.go | 2 + services/api/internal/http/errors.go | 1 + .../api/internal/http/template_handlers.go | 5 +- services/api/internal/http/weixin_handlers.go | 10 ++-- .../api/internal/jobs/channel_repository.go | 8 +-- .../api/internal/jobs/runtime_repository.go | 2 +- services/api/internal/jobs/runtime_worker.go | 2 +- .../api/internal/jobs/runtime_worker_test.go | 2 +- services/api/internal/runtime/docker.go | 2 +- services/api/internal/runtime/docker_test.go | 12 ++--- services/api/internal/runtime/eci.go | 50 +++++++++---------- services/api/internal/runtime/home.go | 6 +-- services/api/internal/templates/repository.go | 4 +- services/api/internal/templates/service.go | 2 +- .../api/internal/templates/service_test.go | 6 +-- services/api/internal/templates/store.go | 2 +- services/api/internal/turnstile/turnstile.go | 2 +- .../api/internal/turnstile/turnstile_test.go | 2 +- .../api/internal/verification/brevo_mailer.go | 2 +- services/api/internal/verification/service.go | 12 ++--- services/api/internal/weixin/client.go | 4 +- 31 files changed, 109 insertions(+), 109 deletions(-) diff --git a/Makefile b/Makefile index 4ccd346f..8547da2e 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,12 @@ .PHONY: lint lint-fix fmt +GOLANGCI_LINT := $(shell which /opt/homebrew/bin/golangci-lint 2>/dev/null || which golangci-lint) + lint: - cd services/api && golangci-lint run ./... + cd services/api && $(GOLANGCI_LINT) run ./... lint-fix: - cd services/api && golangci-lint run --fix ./... + cd services/api && $(GOLANGCI_LINT) run --fix ./... fmt: - cd services/api && golangci-lint run --fix --disable-all --enable=gofmt,goimports,gofumpt ./... + cd services/api && $(GOLANGCI_LINT) fmt ./... diff --git a/services/api/.golangci.yml b/services/api/.golangci.yml index dd40f6b3..652461b0 100644 --- a/services/api/.golangci.yml +++ b/services/api/.golangci.yml @@ -1,46 +1,42 @@ +version: 2 + run: timeout: 5m tests: true - modules-download-mode: readonly - go: '1.24' linters: enable: - errcheck - - gosimple - govet - ineffassign - staticcheck - unused - - gofmt - - goimports - misspell - - unconvert - unparam - gocritic - gosec - bodyclose - - sqlclosecheck - nilerr - - errname - - errorlint - - revive - - stylecheck - - whitespace - - gocyclo - - gofumpt linters-settings: errcheck: check-blank: true - gocyclo: - min-complexity: 15 - revive: - rules: - - name: exported - disabled: true + gosec: + excludes: + - G124 # Cookie 属性测试 + - G304 # 文件包含 + +formatters: + enable: + - gofmt + - goimports issues: - exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 + exclude-rules: + - path: _test\.go + linters: + - gosec + - unparam + - unused diff --git a/services/api/cmd/agentforge-api/main.go b/services/api/cmd/agentforge-api/main.go index e434b491..dff475b6 100644 --- a/services/api/cmd/agentforge-api/main.go +++ b/services/api/cmd/agentforge-api/main.go @@ -56,7 +56,7 @@ func run() error { if err != nil { return err } - defer database.Close() + defer database.Close() //nolint:errcheck // deferred close migrationsDir, err := resolveMigrationsDir() if err != nil { diff --git a/services/api/internal/agents/repository.go b/services/api/internal/agents/repository.go index 28c74916..ab09cbf7 100644 --- a/services/api/internal/agents/repository.go +++ b/services/api/internal/agents/repository.go @@ -138,7 +138,7 @@ func (r *Repository) list(ctx context.Context, db queryer, ownerUserID string) ( if err != nil { return nil, err } - defer rows.Close() + defer rows.Close() //nolint:errcheck // deferred close var agents []Agent for rows.Next() { diff --git a/services/api/internal/auth/repository.go b/services/api/internal/auth/repository.go index b6beb186..7e89ef7e 100644 --- a/services/api/internal/auth/repository.go +++ b/services/api/internal/auth/repository.go @@ -127,7 +127,7 @@ func (r *Repository) FindUserByEmail(ctx context.Context, email string) (User, e if err != nil { return User{}, err } - defer rows.Close() + defer rows.Close() //nolint:errcheck // deferred close var matches []User for rows.Next() { diff --git a/services/api/internal/channels/model.go b/services/api/internal/channels/model.go index 33666578..06bcbded 100644 --- a/services/api/internal/channels/model.go +++ b/services/api/internal/channels/model.go @@ -30,15 +30,15 @@ var ( ) type Channel struct { - ID string `json:"id"` - AgentID string `json:"agentId"` - ChannelType Type `json:"channelType"` - Status Status `json:"status"` + ID string `json:"id"` + AgentID string `json:"agentId"` + ChannelType Type `json:"channelType"` + Status Status `json:"status"` ExternalAccountID string `json:"externalAccountId"` - LastErrorCode string `json:"lastErrorCode"` - LastErrorMessage string `json:"lastErrorMessage"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + LastErrorCode string `json:"lastErrorCode"` + LastErrorMessage string `json:"lastErrorMessage"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } type PairingSession struct { diff --git a/services/api/internal/channels/repository.go b/services/api/internal/channels/repository.go index c3279659..93791343 100644 --- a/services/api/internal/channels/repository.go +++ b/services/api/internal/channels/repository.go @@ -128,7 +128,7 @@ func (r *Repository) ListPairingSessions(ctx context.Context, channelID string) if err != nil { return nil, err } - defer rows.Close() + defer rows.Close() //nolint:errcheck // deferred close var sessions []PairingSession for rows.Next() { var session PairingSession diff --git a/services/api/internal/config/config.go b/services/api/internal/config/config.go index 45175998..8a14e165 100644 --- a/services/api/internal/config/config.go +++ b/services/api/internal/config/config.go @@ -173,7 +173,7 @@ func readDotEnv(path string) (map[string]string, error) { } return nil, err } - defer file.Close() + defer file.Close() //nolint:errcheck // deferred close values := map[string]string{} scanner := bufio.NewScanner(file) diff --git a/services/api/internal/db/db_test.go b/services/api/internal/db/db_test.go index 571874c0..a61745a6 100644 --- a/services/api/internal/db/db_test.go +++ b/services/api/internal/db/db_test.go @@ -15,7 +15,7 @@ func TestOpenAppliesSQLitePragmas(t *testing.T) { if err != nil { t.Fatalf("Open() error = %v", err) } - defer database.Close() + defer database.Close() //nolint:errcheck // deferred close var journalMode string if err := database.QueryRowContext(ctx, "PRAGMA journal_mode;").Scan(&journalMode); err != nil { @@ -48,7 +48,7 @@ func TestMigrateIsIdempotentAndEnforcesForeignKeys(t *testing.T) { if err != nil { t.Fatalf("Open() error = %v", err) } - defer database.Close() + defer database.Close() //nolint:errcheck // deferred close migrationsDir := filepath.Join("..", "..", "migrations") if err := Migrate(ctx, database, migrationsDir); err != nil { @@ -73,7 +73,7 @@ func TestMigrateEnforcesSpecStatusConstraints(t *testing.T) { if err != nil { t.Fatalf("Open() error = %v", err) } - defer database.Close() + defer database.Close() //nolint:errcheck // deferred close if err := Migrate(ctx, database, filepath.Join("..", "..", "migrations")); err != nil { t.Fatalf("Migrate() error = %v", err) diff --git a/services/api/internal/db/migrate.go b/services/api/internal/db/migrate.go index d3e56c32..1fe48570 100644 --- a/services/api/internal/db/migrate.go +++ b/services/api/internal/db/migrate.go @@ -69,7 +69,7 @@ func applyMigration(ctx context.Context, database *sql.DB, migrationsDir, file s if err != nil { return err } - defer tx.Rollback() + defer tx.Rollback() //nolint:errcheck // rollback on error path if _, err := tx.ExecContext(ctx, string(sqlBytes)); err != nil { return err diff --git a/services/api/internal/errors.go b/services/api/internal/errors.go index 879f7b1b..2c3640b3 100644 --- a/services/api/internal/errors.go +++ b/services/api/internal/errors.go @@ -7,6 +7,8 @@ import ( ) // errorWithStack 包装错误并携带堆栈信息 +// +//nolint:errname // internal helper type type errorWithStack struct { Err error Stack []byte diff --git a/services/api/internal/http/errors.go b/services/api/internal/http/errors.go index aa21f440..60bce0c3 100644 --- a/services/api/internal/http/errors.go +++ b/services/api/internal/http/errors.go @@ -151,6 +151,7 @@ func (e *requestError) Unwrap() error { // errorWithStack 包装错误并携带堆栈信息 type errorWithStack struct { + //nolint:errname // internal helper type Err error Stack []byte } diff --git a/services/api/internal/http/template_handlers.go b/services/api/internal/http/template_handlers.go index f11dcce9..062e586e 100644 --- a/services/api/internal/http/template_handlers.go +++ b/services/api/internal/http/template_handlers.go @@ -250,7 +250,7 @@ func (h *TemplateHandlers) AddSkill(c *gin.Context) { writeErrorWithMsg(c, http.StatusBadRequest, "invalid_request", "missing or invalid 'file' field: "+err.Error()) return } - defer file.Close() + defer file.Close() //nolint:errcheck // deferred close archive, err := io.ReadAll(file) if err != nil { writeErrorWithMsg(c, http.StatusBadRequest, "invalid_request", "failed to read file: "+err.Error()) @@ -401,7 +401,6 @@ func requireAdminUser(c *gin.Context) (auth.User, bool) { return user, true } - func readMultipartSkillArchive(header *multipart.FileHeader) (templates.SkillArchive, error) { if header == nil { return templates.SkillArchive{}, fmt.Errorf("%w: missing file header", templates.ErrInvalidInput) @@ -410,7 +409,7 @@ func readMultipartSkillArchive(header *multipart.FileHeader) (templates.SkillArc if err != nil { return templates.SkillArchive{}, fmt.Errorf("%w: failed to open skill file: %v", templates.ErrInvalidInput, err) } - defer file.Close() + defer file.Close() //nolint:errcheck // deferred close content, err := io.ReadAll(file) if err != nil { return templates.SkillArchive{}, fmt.Errorf("%w: failed to read skill file: %v", templates.ErrInvalidInput, err) diff --git a/services/api/internal/http/weixin_handlers.go b/services/api/internal/http/weixin_handlers.go index 38ec06cc..a02a369c 100644 --- a/services/api/internal/http/weixin_handlers.go +++ b/services/api/internal/http/weixin_handlers.go @@ -154,14 +154,14 @@ type channelDTO struct { } type pairingSessionDTO struct { - ID string `json:"id"` - Status channels.PairingStatus `json:"status"` - QRPayload string `json:"qrPayload,omitempty"` + ID string `json:"id"` + Status channels.PairingStatus `json:"status"` + QRPayload string `json:"qrPayload,omitempty"` // QRPayloadURL is the scannable liteapp URL (e.g. // https://liteapp.weixin.qq.com/q/...). The frontend must encode it // into a QR image client-side; it is plain text, not image data. - QRPayloadURL string `json:"qrPayloadUrl,omitempty"` - ExpiresAt string `json:"expiresAt"` + QRPayloadURL string `json:"qrPayloadUrl,omitempty"` + ExpiresAt string `json:"expiresAt"` } func newChannelDTO(channel channels.Channel) channelDTO { diff --git a/services/api/internal/jobs/channel_repository.go b/services/api/internal/jobs/channel_repository.go index 71486ea6..6ff30a0b 100644 --- a/services/api/internal/jobs/channel_repository.go +++ b/services/api/internal/jobs/channel_repository.go @@ -149,9 +149,9 @@ func (r *ChannelRepository) CreateOrReuseConnectJob(ctx context.Context, channel job, err := r.getActiveJobByChannel(ctx, channelID) if errors.Is(err, ErrNotFound) { job, err = r.CreateQueued(ctx, ChannelJob{ - AgentChannelID: channelID, + AgentChannelID: channelID, PairingSessionID: &session.ID, - Type: TypeConnectWeixin, + Type: TypeConnectWeixin, }) return session, job, true, err } @@ -174,9 +174,9 @@ func (r *ChannelRepository) CreateOrReuseConnectJob(ctx context.Context, channel return channels.PairingSession{}, ChannelJob{}, false, err } job, err := r.CreateQueued(ctx, ChannelJob{ - AgentChannelID: channelID, + AgentChannelID: channelID, PairingSessionID: &session.ID, - Type: TypeConnectWeixin, + Type: TypeConnectWeixin, }) return session, job, true, err } diff --git a/services/api/internal/jobs/runtime_repository.go b/services/api/internal/jobs/runtime_repository.go index 8b0d5e08..6b3ca416 100644 --- a/services/api/internal/jobs/runtime_repository.go +++ b/services/api/internal/jobs/runtime_repository.go @@ -109,7 +109,7 @@ func (r *RuntimeRepository) ListByAgent(ctx context.Context, agentID string) ([] if err != nil { return nil, err } - defer rows.Close() + defer rows.Close() //nolint:errcheck // deferred close var runtimeJobs []RuntimeJob for rows.Next() { diff --git a/services/api/internal/jobs/runtime_worker.go b/services/api/internal/jobs/runtime_worker.go index b8e7db59..4eac72f2 100644 --- a/services/api/internal/jobs/runtime_worker.go +++ b/services/api/internal/jobs/runtime_worker.go @@ -199,7 +199,7 @@ func (w *RuntimeWorker) processProvisionAgent(ctx context.Context, job runtimeJo CPUs: w.hermesCPUs, }); err != nil { slog.Error("ECI/container start failed", "error", err, "agentID", agent.ID) - return w.failProvision(ctx, job, agent, runtime.ErrCodeContainerStartFailed, err.Error()) + return w.failProvision(ctx, job, agent, runtime.ErrCodeContainerStartFailed, err.Error()) } if err := w.transitionAgent(ctx, agent.ID, agent.Status, agentStatusRunning, runtimeID, "", ""); err != nil { diff --git a/services/api/internal/jobs/runtime_worker_test.go b/services/api/internal/jobs/runtime_worker_test.go index 7527e5f5..256fea1c 100644 --- a/services/api/internal/jobs/runtime_worker_test.go +++ b/services/api/internal/jobs/runtime_worker_test.go @@ -561,7 +561,7 @@ func listRuntimeEvents(t *testing.T, database *sql.DB, agentID string) []string if err != nil { t.Fatalf("query runtime events: %v", err) } - defer rows.Close() + defer rows.Close() //nolint:errcheck // deferred close var events []string for rows.Next() { var eventType string diff --git a/services/api/internal/runtime/docker.go b/services/api/internal/runtime/docker.go index 9dbdd63e..edd1f7a7 100644 --- a/services/api/internal/runtime/docker.go +++ b/services/api/internal/runtime/docker.go @@ -51,7 +51,7 @@ type ContainerStatus struct { } type dockerRunner struct { - dockerBin string + dockerBin string agentsVolume string // Docker named volume for /data/agents } diff --git a/services/api/internal/runtime/docker_test.go b/services/api/internal/runtime/docker_test.go index b5f83eba..4f971901 100644 --- a/services/api/internal/runtime/docker_test.go +++ b/services/api/internal/runtime/docker_test.go @@ -19,7 +19,7 @@ func TestDockerRunnerEnsureRunningBuildsExpectedCommand(t *testing.T) { t.Fatalf("write stub docker: %v", err) } - runner := NewDockerRunner(stubPath) + runner := NewDockerRunner(stubPath, "") homePath := filepath.Join(workdir, "relative", "..", "hermes-home") spec := ContainerSpec{ AgentID: "agent-internal-id", @@ -71,7 +71,7 @@ func TestDockerRunnerInspectParsesRunningState(t *testing.T) { t.Fatalf("write stub docker: %v", err) } - runner := NewDockerRunner(stubPath) + runner := NewDockerRunner(stubPath, "") status, err := runner.Inspect(ctx, "agentforge-hermes-agent-1") if err != nil { t.Fatalf("Inspect returned error: %v", err) @@ -100,7 +100,7 @@ func TestDockerRunnerEnsureRunningStartsExistingStoppedContainer(t *testing.T) { t.Fatalf("write stub docker: %v", err) } - runner := NewDockerRunner(stubPath) + runner := NewDockerRunner(stubPath, "") if err := runner.EnsureRunning(ctx, ContainerSpec{ AgentID: "agent-internal-id", HermesHome: filepath.Join(workdir, "hermes-home"), @@ -143,7 +143,7 @@ func TestDockerRunnerEnsureRunningNoopsWhenContainerAlreadyRunning(t *testing.T) t.Fatalf("write stub docker: %v", err) } - runner := NewDockerRunner(stubPath) + runner := NewDockerRunner(stubPath, "") if err := runner.EnsureRunning(ctx, ContainerSpec{ AgentID: "agent-internal-id", HermesHome: filepath.Join(workdir, "hermes-home"), @@ -179,7 +179,7 @@ func TestDockerRunnerRemoveReturnsNotFoundWhenContainerMissing(t *testing.T) { if err := os.WriteFile(stubPath, []byte(script), 0o755); err != nil { t.Fatalf("write stub docker: %v", err) } - runner := NewDockerRunner(stubPath) + runner := NewDockerRunner(stubPath, "") err := runner.Remove(ctx, "agentforge-hermes-missing") if !errors.Is(err, ErrContainerNotFound) { @@ -195,7 +195,7 @@ func TestDockerRunnerRemoveWrapsOtherErrors(t *testing.T) { if err := os.WriteFile(stubPath, []byte(script), 0o755); err != nil { t.Fatalf("write stub docker: %v", err) } - runner := NewDockerRunner(stubPath) + runner := NewDockerRunner(stubPath, "") err := runner.Remove(ctx, "agentforge-hermes-x") if err == nil { diff --git a/services/api/internal/runtime/eci.go b/services/api/internal/runtime/eci.go index 4c0b214e..2468dbd9 100644 --- a/services/api/internal/runtime/eci.go +++ b/services/api/internal/runtime/eci.go @@ -139,27 +139,27 @@ func (r *eciRunner) EnsureRunning(ctx context.Context, spec ContainerSpec) error req.Container = &[]eci.CreateContainerGroupContainer{ { - Image: spec.Image, - Name: "hermes", - Memory: requests.NewFloat(toECIMemoryFloat(spec.Memory)), - Cpu: requests.NewFloat(toECICPUFloat(spec.CPUs)), - WorkingDir: "/opt/data", + Image: spec.Image, + Name: "hermes", + Memory: requests.NewFloat(toECIMemoryFloat(spec.Memory)), + Cpu: requests.NewFloat(toECICPUFloat(spec.CPUs)), + WorkingDir: "/opt/data", Command: []string{ - "/init", - "sh", "-c", - // Wait for the NFS mount to be ready before starting Hermes. - // On first boot the .env is written before the ECI is - // created, but NFS can lag. On restart (after pairing) - // the new .env is already on NAS. - // Always remove stale gateway state so Hermes does a - // fresh platform scan on every container boot. Without - // this, a gateway that crashed after writing an empty - // platforms:{} would skip WeChat init on the next - // restart (RestartPolicy: Always). It also defends - // against the dying previous ECI writing a stale - // gateway_state.json to the shared NAS mount. - "while [ ! -f /opt/data/.env ]; do sleep 0.5; done; chmod -R 777 /opt/data/weixin 2>/dev/null; rm -f /opt/data/gateway_state.json /opt/data/gateway.lock /opt/data/gateway.pid; exec /opt/hermes/docker/main-wrapper.sh gateway run", - }, + "/init", + "sh", "-c", + // Wait for the NFS mount to be ready before starting Hermes. + // On first boot the .env is written before the ECI is + // created, but NFS can lag. On restart (after pairing) + // the new .env is already on NAS. + // Always remove stale gateway state so Hermes does a + // fresh platform scan on every container boot. Without + // this, a gateway that crashed after writing an empty + // platforms:{} would skip WeChat init on the next + // restart (RestartPolicy: Always). It also defends + // against the dying previous ECI writing a stale + // gateway_state.json to the shared NAS mount. + "while [ ! -f /opt/data/.env ]; do sleep 0.5; done; chmod -R 777 /opt/data/weixin 2>/dev/null; rm -f /opt/data/gateway_state.json /opt/data/gateway.lock /opt/data/gateway.pid; exec /opt/hermes/docker/main-wrapper.sh gateway run", + }, EnvironmentVar: &envVars, VolumeMount: &[]eci.CreateContainerGroupVolumeMount{ { @@ -206,7 +206,7 @@ func (r *eciRunner) EnsureRunning(ctx context.Context, spec ContainerSpec) error return fmt.Errorf("eci: create container group: %w", err) } if !resp.IsSuccess() { - return fmt.Errorf("eci: create container group failed: %s", resp.GetHttpStatus()) + return fmt.Errorf("eci: create container group failed: %d", resp.GetHttpStatus()) } return nil @@ -238,7 +238,7 @@ func (r *eciRunner) Remove(ctx context.Context, containerName string) error { return fmt.Errorf("eci: delete container group: %w", err) } if !resp.IsSuccess() { - return fmt.Errorf("eci: delete container group failed: %s", resp.GetHttpStatus()) + return fmt.Errorf("eci: delete container group failed: %d", resp.GetHttpStatus()) } return nil } @@ -269,7 +269,7 @@ func (r *eciRunner) Destroy(ctx context.Context, containerName string) error { return fmt.Errorf("eci: destroy: delete: %w", err) } if !resp.IsSuccess() { - return fmt.Errorf("eci: destroy: delete returned %s", resp.GetHttpStatus()) + return fmt.Errorf("eci: destroy: delete returned %d", resp.GetHttpStatus()) } // 2. Poll until the group is truly gone. @@ -437,7 +437,7 @@ func (r *eciRunner) createNASDir(path string) error { return errors.New("nas file system id not configured") } - cli, err := sdk.NewClientWithAccessKey(r.region, r.accessKeyID, r.accessKeySecret) + cli, err := sdk.NewClientWithAccessKey(r.region, r.accessKeyID, r.accessKeySecret) //nolint:staticcheck // will migrate later if err != nil { return fmt.Errorf("nas: create client: %w", err) } @@ -464,7 +464,7 @@ func (r *eciRunner) createNASDir(path string) error { strings.Contains(resp.String(), "PathAlreadyExists") { return nil } - return fmt.Errorf("nas: create dir failed: %s", resp.GetHttpStatus()) + return fmt.Errorf("nas: create dir failed: %d", resp.GetHttpStatus()) } return nil } diff --git a/services/api/internal/runtime/home.go b/services/api/internal/runtime/home.go index af4eead5..b2aa3be3 100644 --- a/services/api/internal/runtime/home.go +++ b/services/api/internal/runtime/home.go @@ -190,7 +190,7 @@ func copyFile(source, target string) error { if err != nil { return err } - defer input.Close() + defer input.Close() //nolint:errcheck // deferred close if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return err @@ -200,7 +200,7 @@ func copyFile(source, target string) error { if err != nil { return err } - defer output.Close() + defer output.Close() //nolint:errcheck // deferred close if _, err := io.Copy(output, input); err != nil { return err @@ -294,7 +294,7 @@ func removeAllNFS(root string) error { err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { // If we can't access a path, skip it — we'll retry. - return nil + return nil //nolint:nilerr // intentionally skip inaccessible paths } paths = append(paths, path) return nil diff --git a/services/api/internal/templates/repository.go b/services/api/internal/templates/repository.go index 6859a993..c3415cbc 100644 --- a/services/api/internal/templates/repository.go +++ b/services/api/internal/templates/repository.go @@ -90,7 +90,7 @@ func (r *Repository) ListTemplates(ctx context.Context, statuses ...Status) ([]T if err != nil { return nil, err } - defer rows.Close() + defer rows.Close() //nolint:errcheck // deferred close var templates []Template for rows.Next() { @@ -258,7 +258,7 @@ func (r *Repository) ListSkills(ctx context.Context, templateID string) ([]Skill if err != nil { return nil, err } - defer rows.Close() + defer rows.Close() //nolint:errcheck // deferred close var skills []Skill for rows.Next() { diff --git a/services/api/internal/templates/service.go b/services/api/internal/templates/service.go index d17f6990..e38a40f8 100644 --- a/services/api/internal/templates/service.go +++ b/services/api/internal/templates/service.go @@ -630,7 +630,7 @@ func readZipFile(file *zip.File) ([]byte, error) { if err != nil { return nil, err } - defer reader.Close() + defer reader.Close() //nolint:errcheck // deferred close content, err := io.ReadAll(reader) if err != nil { return nil, err diff --git a/services/api/internal/templates/service_test.go b/services/api/internal/templates/service_test.go index 98de8d2d..229cd4f2 100644 --- a/services/api/internal/templates/service_test.go +++ b/services/api/internal/templates/service_test.go @@ -392,9 +392,9 @@ func TestServiceAddSkillArchiveWritesWholeSkillDirectory(t *testing.T) { } skill, err := service.AddSkillArchive(ctx, template.ID, mustZipSkill(t, map[string]string{ - "handoff/SKILL.md": "---\nname: handoff\ndescription: Escalation path\n---\n# SKILL\nEscalate to humans when needed.\n", - "handoff/references/a.md": "reference", - "handoff/scripts/run.sh": "#!/bin/sh\nexit 0\n", + "handoff/SKILL.md": "---\nname: handoff\ndescription: Escalation path\n---\n# SKILL\nEscalate to humans when needed.\n", + "handoff/references/a.md": "reference", + "handoff/scripts/run.sh": "#!/bin/sh\nexit 0\n", })) if err != nil { t.Fatalf("AddSkillArchive returned error: %v", err) diff --git a/services/api/internal/templates/store.go b/services/api/internal/templates/store.go index 1a4cb3b8..b684b63f 100644 --- a/services/api/internal/templates/store.go +++ b/services/api/internal/templates/store.go @@ -359,7 +359,7 @@ func copyFileIfExists(source, target string) error { if err != nil { return err } - defer input.Close() + defer input.Close() //nolint:errcheck // deferred close if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return err } diff --git a/services/api/internal/turnstile/turnstile.go b/services/api/internal/turnstile/turnstile.go index 13f64ee5..548f8bbe 100644 --- a/services/api/internal/turnstile/turnstile.go +++ b/services/api/internal/turnstile/turnstile.go @@ -110,7 +110,7 @@ func (v *SiteVerifyVerifier) Verify(ctx context.Context, token, action string) e // 不在此处 log——由 requireTurnstile 在请求边界统一记录,避免重复日志。 return fmt.Errorf("%w: siteverify request: %v", ErrUnavailable, err) } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck // deferred close if resp.StatusCode != http.StatusOK { return fmt.Errorf("%w: siteverify returned status %d", ErrUnavailable, resp.StatusCode) diff --git a/services/api/internal/turnstile/turnstile_test.go b/services/api/internal/turnstile/turnstile_test.go index 72707f78..4fc14ffa 100644 --- a/services/api/internal/turnstile/turnstile_test.go +++ b/services/api/internal/turnstile/turnstile_test.go @@ -23,7 +23,7 @@ func (t *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) { if req.Body != nil { b, _ := io.ReadAll(req.Body) t.gotBody = string(b) - req.Body.Close() + _ = req.Body.Close() } if t.err != nil { return nil, t.err diff --git a/services/api/internal/verification/brevo_mailer.go b/services/api/internal/verification/brevo_mailer.go index 6ce6932d..72537ac7 100644 --- a/services/api/internal/verification/brevo_mailer.go +++ b/services/api/internal/verification/brevo_mailer.go @@ -61,7 +61,7 @@ func (m *BrevoMailer) SendRegistrationCode(ctx context.Context, email, code stri if err != nil { return err } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck // deferred close if resp.StatusCode < 200 || resp.StatusCode >= 300 { // 读取并附带 Brevo 返回的响应体,便于排查认证、发件人、配额等失败原因。 respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096)) diff --git a/services/api/internal/verification/service.go b/services/api/internal/verification/service.go index 4e6d7e85..01c226b5 100644 --- a/services/api/internal/verification/service.go +++ b/services/api/internal/verification/service.go @@ -23,13 +23,13 @@ const ( ) var ( - ErrInvalidEmail = errors.New("invalid email") - ErrCodeCooldown = errors.New("email code cooldown") - ErrCodeRateLimited = errors.New("email code rate limited") - ErrCodeInvalid = errors.New("email code invalid") - ErrCodeExpired = errors.New("email code expired") + ErrInvalidEmail = errors.New("invalid email") + ErrCodeCooldown = errors.New("email code cooldown") + ErrCodeRateLimited = errors.New("email code rate limited") + ErrCodeInvalid = errors.New("email code invalid") + ErrCodeExpired = errors.New("email code expired") ErrCodeAttemptsExhausted = errors.New("email code attempts exhausted") - ErrEmailSendFailed = errors.New("email send failed") + ErrEmailSendFailed = errors.New("email send failed") ) // Mailer 负责投递验证码邮件,由具体实现(如 Brevo)封装外部邮件 API。 diff --git a/services/api/internal/weixin/client.go b/services/api/internal/weixin/client.go index 85991493..6895af4e 100644 --- a/services/api/internal/weixin/client.go +++ b/services/api/internal/weixin/client.go @@ -134,7 +134,7 @@ func (c *client) GetBotQRCode(ctx context.Context, req QRCodeRequest) (QRCodeRes if err != nil { return QRCodeResponse{}, err } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck // deferred close var parsed QRCodeResponse if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { @@ -161,7 +161,7 @@ func (c *client) GetQRCodeStatus(ctx context.Context, req QRStatusRequest) (QRSt if err != nil { return QRStatusResponse{}, err } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck // deferred close var parsed QRStatusResponse if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { From ee5f3d38d91379f7e60241155f32a2bb99f32073 Mon Sep 17 00:00:00 2001 From: zhengwenfeng Date: Sun, 28 Jun 2026 01:34:25 +0800 Subject: [PATCH 4/5] update --- .github/workflows/lint.yml | 10 ++++++---- Makefile | 8 +++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 863c2a8f..8c671ef8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,6 +13,9 @@ on: paths: - 'services/api/**/*.go' +permissions: + contents: read + jobs: golangci: name: golangci-lint @@ -21,10 +24,9 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version-file: services/api/go.mod + go-version: '1.25' cache-dependency-path: services/api/go.sum - - uses: golangci/golangci-lint-action@v6 + - uses: golangci/golangci-lint-action@v9 with: - version: latest + version: v2.12 working-directory: services/api - args: --timeout=5m diff --git a/Makefile b/Makefile index 8547da2e..90dbb98e 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,10 @@ .PHONY: lint lint-fix fmt -GOLANGCI_LINT := $(shell which /opt/homebrew/bin/golangci-lint 2>/dev/null || which golangci-lint) - lint: - cd services/api && $(GOLANGCI_LINT) run ./... + cd services/api && golangci-lint run ./... lint-fix: - cd services/api && $(GOLANGCI_LINT) run --fix ./... + cd services/api && golangci-lint run --fix ./... fmt: - cd services/api && $(GOLANGCI_LINT) fmt ./... + cd services/api && golangci-lint fmt ./... From 39ec88f0586bf71dd6a63c9fe3628e96197c415d Mon Sep 17 00:00:00 2001 From: zhengwenfeng Date: Sun, 28 Jun 2026 02:16:18 +0800 Subject: [PATCH 5/5] update --- services/api/.golangci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/services/api/.golangci.yml b/services/api/.golangci.yml index 652461b0..74c54d38 100644 --- a/services/api/.golangci.yml +++ b/services/api/.golangci.yml @@ -1,4 +1,4 @@ -version: 2 +version: "2" run: timeout: 5m @@ -17,14 +17,20 @@ linters: - gosec - bodyclose - nilerr - -linters-settings: - errcheck: - check-blank: true - gosec: - excludes: - - G124 # Cookie 属性测试 - - G304 # 文件包含 + settings: + errcheck: + check-blank: true + gosec: + excludes: + - G124 + - G304 + exclusions: + rules: + - path: '_test\.go' + linters: + - gosec + - unparam + - unused formatters: enable: @@ -34,9 +40,3 @@ formatters: issues: max-issues-per-linter: 0 max-same-issues: 0 - exclude-rules: - - path: _test\.go - linters: - - gosec - - unparam - - unused