WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
// Bootstrap-time hooks that run after the DI container is built but
|
||||
// before the HTTP server starts listening. These are deliberately
|
||||
// best-effort: any failure here only warns and does NOT abort startup.
|
||||
// The reasoning is that an operator running with a misconfigured env
|
||||
// var should still be able to bring the server up (and fix the issue
|
||||
// from the running instance) rather than have a typo brick the deploy.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
// bootstrapEnvVar is the env var that names the email of the user who
|
||||
// may be promoted to system administrator when the deployment has no
|
||||
// existing system administrators.
|
||||
//
|
||||
// Why an env var (vs a CLI subcommand)?
|
||||
// - Zero-friction in docker-compose / k8s deploys: set it once in the
|
||||
// manifest and the very first user account that signs up with that
|
||||
// email is auto-promoted, with no extra ops step.
|
||||
// - Idempotent: if the user is already a system admin, bootstrapping is
|
||||
// a no-op.
|
||||
// - Safe to leave set: once at least one system admin exists, the env
|
||||
// var stops granting privileges. That prevents a UI revoke from being
|
||||
// silently undone on the next restart.
|
||||
const bootstrapEnvVar = "WEKNORA_BOOTSTRAP_SYSTEM_ADMIN_EMAIL"
|
||||
|
||||
// runStartupBootstrap consults the env and applies any one-shot
|
||||
// bootstrap actions. Currently it only handles system-admin promotion;
|
||||
// future bootstrap steps (default model seeding, etc.) can be added
|
||||
// here as additional dig.Invoke calls.
|
||||
func runStartupBootstrap(c *dig.Container) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Legacy hash repair for migration 000065 placeholder rows. Invoked each
|
||||
// startup but short-circuits with a cheap EXISTS once every row is
|
||||
// backfilled (no api_key decryption on the steady-state path).
|
||||
if err := c.Invoke(func(apiKeySvc interfaces.TenantAPIKeyService) {
|
||||
if n, err := apiKeySvc.BackfillMissingKeyHashes(ctx); err != nil {
|
||||
logger.Warnf(ctx, "[bootstrap] tenant api key hash backfill failed: %v", err)
|
||||
} else if n > 0 {
|
||||
logger.Infof(ctx, "[bootstrap] backfilled %d legacy tenant api key hash(es)", n)
|
||||
}
|
||||
}); err != nil {
|
||||
logger.Warnf(ctx, "[bootstrap] failed to resolve TenantAPIKeyService: %v", err)
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(os.Getenv(bootstrapEnvVar))
|
||||
if email == "" {
|
||||
return
|
||||
}
|
||||
// dig.Invoke resolves UserService from the container; if user
|
||||
// service registration is broken we want to know loudly, but still
|
||||
// not abort startup — bootstrap is best-effort.
|
||||
if err := c.Invoke(func(userSvc interfaces.UserService) {
|
||||
bootstrapSystemAdmin(ctx, userSvc, email)
|
||||
}); err != nil {
|
||||
logger.Warnf(ctx, "[bootstrap] failed to resolve UserService: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// bootstrapSystemAdmin promotes the user identified by `email` to system
|
||||
// administrator only when the deployment currently has no system admins.
|
||||
// The function is idempotent and non-fatal — it warns and returns on
|
||||
// every error path.
|
||||
//
|
||||
// The bootstrap intentionally does NOT create a user when the email is
|
||||
// not yet registered: account creation is a workflow with side effects
|
||||
// (password hashing, tenant assignment, audit) that we don't want to
|
||||
// short-circuit. Operators should sign up normally first, then set the
|
||||
// env var on the next restart.
|
||||
func bootstrapSystemAdmin(ctx context.Context, userSvc interfaces.UserService, email string) {
|
||||
user, err := userSvc.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
// "not found" surfaces as an error in this codebase; treat it
|
||||
// gently — operators commonly set the var before the user has
|
||||
// signed up. The next restart after registration will succeed.
|
||||
logger.Warnf(ctx,
|
||||
"[bootstrap] %s=%s: user lookup failed (have they signed up yet?): %v",
|
||||
bootstrapEnvVar, email, err)
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
logger.Warnf(ctx,
|
||||
"[bootstrap] %s=%s: no matching user (will retry on next restart)",
|
||||
bootstrapEnvVar, email)
|
||||
return
|
||||
}
|
||||
if user.IsSystemAdmin {
|
||||
logger.Infof(ctx,
|
||||
"[bootstrap] %s=%s: user %s is already a system admin (no-op)",
|
||||
bootstrapEnvVar, email, user.ID)
|
||||
return
|
||||
}
|
||||
_, total, err := userSvc.ListSystemAdmins(ctx, 0, 1)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx,
|
||||
"[bootstrap] %s=%s: cannot verify existing system admins, skipping promotion: %v",
|
||||
bootstrapEnvVar, email, err)
|
||||
return
|
||||
}
|
||||
if total > 0 {
|
||||
logger.Infof(ctx,
|
||||
"[bootstrap] %s=%s: %d system admin(s) already exist; not promoting user %s",
|
||||
bootstrapEnvVar, email, total, user.ID)
|
||||
return
|
||||
}
|
||||
user.IsSystemAdmin = true
|
||||
if err := userSvc.UpdateUser(ctx, user); err != nil {
|
||||
logger.Warnf(ctx,
|
||||
"[bootstrap] %s=%s: failed to promote user %s: %v",
|
||||
bootstrapEnvVar, email, user.ID, err)
|
||||
return
|
||||
}
|
||||
logger.Infof(ctx,
|
||||
"[bootstrap] promoted user %s (%s) to system admin via %s",
|
||||
user.ID, email, bootstrapEnvVar)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
)
|
||||
|
||||
func listenWithRetry(addr string, maxRetries int, baseDelay time.Duration) (net.Listener, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err == nil {
|
||||
return listener, nil
|
||||
}
|
||||
lastErr = err
|
||||
if i < maxRetries-1 {
|
||||
delay := baseDelay * time.Duration(1<<uint(i))
|
||||
if delay > 3*time.Second {
|
||||
delay = 3 * time.Second
|
||||
}
|
||||
logger.Warnf(context.Background(), "Port %s in use, retrying in %v... (%d/%d)", addr, delay, i+1, maxRetries)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Package main is the main package for the WeKnora server
|
||||
// It contains the main function and the entry point for the server
|
||||
//
|
||||
// @title WeKnora API
|
||||
// @version 1.0
|
||||
// @description WeKnora 知识库管理系统 API 文档
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
//
|
||||
// @contact.name WeKnora Github
|
||||
// @contact.url https://github.com/Tencent/WeKnora
|
||||
//
|
||||
// @BasePath /api/v1
|
||||
//
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description 用户登录认证:输入 Bearer {token} 格式的 JWT 令牌
|
||||
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name X-API-Key
|
||||
// @description API Key 认证:空间 Key 固定访问所属空间;平台 Key 调用空间接口时需同时传 X-Tenant-ID
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/container"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/runtime"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set Gin mode
|
||||
if os.Getenv("GIN_MODE") == "release" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
} else {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
// Mute Gin's per-route registration spam (one line per route × ~150
|
||||
// routes) — replaced by a single summary printed after router build.
|
||||
runtime.SilenceGinRouteSpam()
|
||||
// Print the env banner before container build so operators see what
|
||||
// config landed even when DB / storage init fails.
|
||||
runtime.LogStartupEnv(context.Background())
|
||||
runtime.MarkServerStarted()
|
||||
|
||||
// Build dependency injection container
|
||||
c := container.BuildContainer(runtime.GetContainer())
|
||||
|
||||
// One-shot bootstrap hooks (e.g. promote env-named user to system
|
||||
// admin). Best-effort: never aborts startup — see bootstrap.go.
|
||||
runStartupBootstrap(c)
|
||||
|
||||
// Run application
|
||||
err := c.Invoke(func(
|
||||
cfg *config.Config,
|
||||
router *gin.Engine,
|
||||
resourceCleaner interfaces.ResourceCleaner,
|
||||
systemSettingSvc interfaces.SystemSettingService,
|
||||
) error {
|
||||
// Create HTTP server
|
||||
server := &http.Server{
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
listener, err := listenWithRetry(addr, 10, 300*time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start server: %v", err)
|
||||
}
|
||||
|
||||
ctx, done := context.WithCancel(context.Background())
|
||||
|
||||
// Start the system_settings pubsub subscriber. Runs in its own
|
||||
// goroutine and exits when ctx is cancelled at shutdown. Best-
|
||||
// effort: an error here only warns (Redis may legitimately be
|
||||
// disabled in lite-mode deployments — the service no-ops in
|
||||
// that case anyway).
|
||||
if err := systemSettingSvc.SubscribeRedis(ctx); err != nil {
|
||||
logger.Warnf(ctx, "[system_settings] subscribe failed: %v", err)
|
||||
}
|
||||
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, shutdownSignals...)
|
||||
go func() {
|
||||
sig := <-signals
|
||||
logger.Infof(context.Background(), "Received signal: %v, starting server shutdown...", sig)
|
||||
|
||||
// Close listener first to release port immediately,
|
||||
// so the next process can bind during our graceful drain.
|
||||
listener.Close()
|
||||
|
||||
shutdownTimeout := cfg.Server.ShutdownTimeout
|
||||
if shutdownTimeout == 0 {
|
||||
shutdownTimeout = 30 * time.Second
|
||||
}
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer shutdownCancel()
|
||||
|
||||
// Second signal → force close all connections immediately
|
||||
go func() {
|
||||
sig := <-signals
|
||||
logger.Warnf(context.Background(), "Received second signal: %v, forcing shutdown...", sig)
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Errorf(context.Background(), "Server forced to shutdown: %v", err)
|
||||
server.Close()
|
||||
}
|
||||
|
||||
logger.Info(context.Background(), "Cleaning up resources...")
|
||||
errs := resourceCleaner.Cleanup(shutdownCtx)
|
||||
if len(errs) > 0 {
|
||||
logger.Errorf(context.Background(), "Errors occurred during resource cleanup: %v", errs)
|
||||
}
|
||||
logger.Info(context.Background(), "Server has exited")
|
||||
done()
|
||||
}()
|
||||
|
||||
runtime.LogGinRouteCount(context.Background())
|
||||
logger.Infof(context.Background(), "Server is running at %s", addr)
|
||||
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
return fmt.Errorf("server error: %v", err)
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatalf(context.Background(), "Failed to run application: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var shutdownSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var shutdownSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
|
||||
Reference in New Issue
Block a user