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

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+125
View File
@@ -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)
}
+29
View File
@@ -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
}
+143
View File
@@ -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)
}
}
+10
View File
@@ -0,0 +1,10 @@
//go:build !windows
package main
import (
"os"
"syscall"
)
var shutdownSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP}
+10
View File
@@ -0,0 +1,10 @@
//go:build windows
package main
import (
"os"
"syscall"
)
var shutdownSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}