feat(deploy): M11 单二进制装配(cmd/cyrene 全链路 wire + go:embed 前端 + 外置 dist 覆盖 + 优雅关闭 + SPA 静态服务);前端 build 拷到 internal/webui/dist
This commit is contained in:
1 parent
f43eb17030
commit
a7feaeb2cb
7 files changed
+745
-1
No files matched your search
@@ -0,0 +1,115 @@
|
||||
// Command cyrene is the single Cyrene v2 binary. It assembles every subsystem
|
||||
// (config, logging, database, bus, personas, model adapter, tool dispatcher,
|
||||
// memory stack, agent loop, orchestration, execution, IoT, channel sessions and
|
||||
// adapters) into one process and serves the gateway + embedded frontend on a
|
||||
// single port (:8080), shutting down gracefully on a termination signal.
|
||||
//
|
||||
// Build with `go build -ldflags="-s -w" -trimpath -o cyrene ./cmd/cyrene/`.
|
||||
// The frontend must be built first so the SPA can be embedded:
|
||||
// `cd frontend/web && npm run build`.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/platform"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/webui"
|
||||
)
|
||||
|
||||
// envPath is the .env file merged at boot (development only; production uses
|
||||
// systemd/env_file). A missing .env is not an error.
|
||||
const envPath = ".env"
|
||||
|
||||
// shutdownTimeout bounds how long the graceful teardown may take.
|
||||
const shutdownTimeout = 15 * time.Second
|
||||
|
||||
// externalDistCandidates are the on-disk directories checked (in order) for a
|
||||
// frontend override before falling back to the embedded dist. The first that
|
||||
// exists wins. They are relative to the process working directory.
|
||||
func externalDistCandidates() []string {
|
||||
return []string{
|
||||
filepath.Join("frontend", "web", "dist"),
|
||||
filepath.Join("frontend", "dist"),
|
||||
}
|
||||
}
|
||||
|
||||
// resolveFrontendFS returns the filesystem serving the SPA: an external dist
|
||||
// directory when present (deploy-time hot override), otherwise the embedded dist.
|
||||
func resolveFrontendFS(log *slog.Logger) fs.FS {
|
||||
for _, dir := range externalDistCandidates() {
|
||||
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
||||
log.Info("serving external frontend dist", "dir", dir)
|
||||
return os.DirFS(dir)
|
||||
}
|
||||
}
|
||||
log.Info("serving embedded frontend dist")
|
||||
return webui.Assets()
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "cyrene:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// run boots the application, starts every service and the gateway, then blocks
|
||||
// until a termination signal arrives or the gateway fails.
|
||||
func run() error {
|
||||
cfg := config.MustLoad(envPath)
|
||||
format := logger.FormatText
|
||||
if cfg.Env == "production" {
|
||||
format = logger.FormatJSON
|
||||
}
|
||||
log := logger.New(logger.Options{Level: cfg.LogLevel, Format: string(format)})
|
||||
log = logger.WithSource(log, "main")
|
||||
|
||||
ctx, stop := platform.SignalContext()
|
||||
defer stop()
|
||||
|
||||
a, err := newApp(ctx, cfg, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.automation.Start(ctx); err != nil {
|
||||
return fmt.Errorf("start automations: %w", err)
|
||||
}
|
||||
a.backthink.Start(ctx)
|
||||
a.log.Info("cyrene subsystems started",
|
||||
"personas", a.personas.CountPersonas(),
|
||||
"tools", len(a.caps.List(capreg.KindTool)),
|
||||
"channels", len(a.caps.List(capreg.KindChannel)))
|
||||
a.log.Info("cyrene ready", "data_dir", resolveDataDir(cfg))
|
||||
|
||||
errCh, err := a.gw.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start gateway: %w", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
a.log.Info("cyrene signal received, shutting down")
|
||||
case gwErr := <-errCh:
|
||||
if gwErr != nil {
|
||||
shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer cancel()
|
||||
a.close(shutCtx)
|
||||
return fmt.Errorf("gateway failed: %w", gwErr)
|
||||
}
|
||||
}
|
||||
|
||||
shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer cancel()
|
||||
a.close(shutCtx)
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user