Files
Cyrene_v2/cmd/cyrene/main.go
T
AskaEth 2092094d0a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(platform): 内嵌 IANA tzdata,Windows 也能解析 TZ;新增本机 embedding 服务脚本
- cmd/cyrene/main.go: import _ "time/tzdata" —— Windows 无系统 tzdata,导致 config.TZ(默认 Asia/Shanghai) 解析失败、loop 时间注入被静默禁用;内嵌后单二进制自洽(约 +450KB)
- 实测:迁移到 Windows 后启动日志不再出现 "timezone load failed; time injection disabled"
- scripts/embed.bat(新):本机 llama.cpp embedding 服务启停(start/stop/status),OpenAI 兼容 /v1/embeddings,跑 Qwen3-Embedding-0.6B-Q8_0(dim 1024)
- 实测:start→Ready→status running→POST /v1/embeddings 200 dim=1024;配合 docker compose 的 cyrene-pg(pgvector) 后端日志变为 pgvector index ready dim=1024(不再回落内存语义)
2026-10-03 17:58:45 +08:00

126 lines
4.1 KiB
Go

// 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"
// Embed the IANA timezone database so the configured TZ (config.TZ, default
// "Asia/Shanghai") resolves everywhere: Windows ships no system tzdata and Go
// deliberately does not bundle one, so without this the loop's time injection
// silently disables itself there (internal/runtime/loop/time.go logs
// "timezone load failed"). Costs ~450 KB and keeps the binary self-contained.
_ "time/tzdata"
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
}
logRing := logger.NewRingBuffer(logger.DefaultRingCapacity)
log := logger.New(logger.Options{Level: cfg.LogLevel, Format: string(format), Ring: logRing})
log = logger.WithSource(log, "main")
ctx, stop := platform.SignalContext()
defer stop()
a, err := newApp(ctx, cfg, log)
if err != nil {
return err
}
// Observability log tail: the logger's ring buffer is built above (before the
// gateway), so hand it to the gateway now that the gateway exists.
wireGatewayLogBuffer(a, logRing)
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
}