Files
Cyrene_v2/cmd/cyrene/main.go
T
AskaEth a6cb44b8a8
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(flow): 链路图本地落盘 + 多轮历史回放(design/19 §P3,契约 v0.73)
主人要求「链路流程应该有本地数据,不然每次重载之后页面就没数据了」:
- 落盘:迁移 0022_flow_graphs(session_id/turn_id/graph_json/时间戳 + 唯一键 upsert)+ internal/runtime/flowgraph(RecordRound/Graph/Latest/Turns/Turn + 保留期 GC)
- 写入:loop.EndRound **每轮一条**(锁外深拷贝后写;失败 WARN 且不打印图内容);只落发布会话白名单(复用 flowSessionAllowed)
- 读回:端点加 turnId + source(live/persisted/none) + turns[](最新在前,空为 []);**内存优先、miss 才落盘**(最新一轮永远最新)
- 前端:flowGraphs 纳入 persistOptions.partialize(**刷新即刻有图**,version 3→4 + migrate)+ 历史轮选择器 + 诚实文案;顺带收 L1 转交的 flow/NodeDetail.tsx 节点日志倒序
- 新 env 键:FLOW_GRAPH_RETENTION=168h / FLOW_GRAPH_GC_INTERVAL=1h
- 变异实测:每 span 一行 / 保留期守卫失效 / partialize 去掉 flowGraphs / 去掉内存优先 / 白名单失效 ⇒ 全部打红
- 3 个禁动面文件的纯追加改动(Flow.tsx 挂选择器、FlowSnapshotHint 纠已成假话的文案、其渲染断言随之更新)由主会话裁定认可
- 登记存疑:图桶持久化无体积上限;useFlowTurns 的 effect 无自动化证据(项目无 jsdom)
2026-10-05 16:19:32 +08:00

139 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// cyrene 命令是 Cyrene v2 的唯一二进制入口:它把全部子系统(配置、日志、数据库、
// bus、人格、模型适配器、工具分发器、记忆栈、agent 循环、编排、执行、IoT、渠道
// 会话与适配器)组装进同一个进程,在单端口(:8080)上同时提供网关与内嵌前端,
// 并在收到终止信号时优雅退出。
//
// 构建:`go build -ldflags="-s -w" -trimpath -o cyrene ./cmd/cyrene/`。
// 前端必须先构建,SPA 才能被内嵌:`cd frontend/web && npm run build`。
package main
import (
"context"
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"time"
// 内嵌 IANA 时区数据库,让配置的 TZ(config.TZ,默认 "Asia/Shanghai")
// 在任何平台都能解析:Windows 不自带系统 tzdata,Go 也刻意不捆绑,缺了它
// 循环的时间注入在 Windows 上会静默失效(internal/runtime/loop/time.go 会记
// "timezone load failed")。代价约 450 KB,换来二进制自包含。
_ "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 是启动时合并的 .env 文件(仅开发用;生产走 systemd/env_file)。
// .env 不存在不算错误。
const envPath = ".env"
// shutdownTimeout 是优雅退场的时长上界。
const shutdownTimeout = 15 * time.Second
// externalDistCandidates 是前端覆盖目录的候选清单:按序检查,都不存在才回落到
// 内嵌 dist;命中的第一个生效。路径相对进程工作目录。
func externalDistCandidates() []string {
return []string{
filepath.Join("frontend", "web", "dist"),
filepath.Join("frontend", "dist"),
}
}
// resolveFrontendFS 返回用于伺服 SPA 的文件系统:存在外部 dist 目录时用它
// (部署期热覆盖),否则用内嵌 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 启动应用、拉起全部服务与网关,然后阻塞等待终止信号或网关失败。
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
}
// 可观测性日志追尾:logger 的环形缓冲在上面(网关之前)就建好了,
// 现在网关已存在,把它交给网关。
wireGatewayLogBuffer(a, logRing)
if err := a.automation.Start(ctx); err != nil {
return fmt.Errorf("start automations: %w", err)
}
a.backthink.Start(ctx)
// 记忆整理(design/23 §4):低峰定期后台任务。它不做启动即跑 —— 只在
// 安静时段起点开跑,因此进程重启不会触发一次全库扫描;配置关闭时只记
// 一条日志并立即返回。
if a.memoryTidy != nil {
a.memoryTidy.Start(ctx)
}
// 文档回收(切片 S7):启动期清扫一次 + 此后按 DOCUMENT_GC_INTERVAL 定时清扫。
// 它不等这一次跑完就去拉网关(见 wire_document_gc.go 的取舍 1)。
a.startDocumentGC(ctx)
// 渠道事件留存维护(切片 E3+E4):保留期清扫一次 + 此后定时清扫,内存计数
// 按 CHANNEL_EVENT_COUNTER_FLUSH 定时落盘(退出前最后一次落盘在循环里)。
a.startChannelEvents(ctx)
// 链路图留存维护(切片 F2):保留期清扫一次 + 此后按 FLOW_GRAPH_GC_INTERVAL 定时清扫。
// 没有它,落盘的轮次图会**无限增长**(design/19 §4 P3 的 GC 口径)。
a.startFlowGraphs(ctx)
// LLM 用量落库与保留期清理(切片 U1):单写者 + 有界队列的异步写入器,
// 加上按 LLM_USAGE_RETENTION 的过期样本清理。
a.startUsage(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
}