- turnId 由编排器生成、loop Run 复用(硬前提:否则 turn_begin 与流帧各铸一个,客户端两级分桶立即失配) - StateSink 增带 turnId 的变体(未接线时退化为现签名,不丢状态帧) - 上行 chat.parallel(WS chat 帧 + REST 三入口同形;省略 = 逐字不变) - 单会话额度默认 2 / 硬上限 3 / <2 关闭;额度满或未声明 ⇒ 落回既有排队语义 - I1′:checkpoint 归属、历史配对、忙态计数三处从会话级单例升为路级 - session_idle 收紧为「所有在途路收尾且 pending 空」(仅并行时生效;单路逐字不变) - 预留接口(形状定死、无实现无接线):tool/legfile.go 的 FileClaimer / ErrFileBusy / WithLegID - 顺手修一个真 bug:并行路 ctx 派生自提交请求作用域,REST 同步入口一写响应就把它连坐杀死(改 context.WithoutCancel 派生) - 23 个新用例(含 -race -count=3);变异 6 条(turnId 复用 / idle 时点 / 并行升级 / checkpoint 键 / 编排器交 id / pending_ack reason) - 契约变更点 10 条待监工升 v0.65(本人未改 Docs/**)
236 lines
9.1 KiB
Go
236 lines
9.1 KiB
Go
// Package gateway 实现 Cyrene v2 的 HTTP 网关:单二进制唯一打开的套接字。
|
||
// 它在同一个端口(:8080)上提供版本化 REST API(/api/v1)、实时
|
||
// WebSocket hub(/ws/chat)以及受 RBAC 保护的管理 API(/api/v1/admin/*)。
|
||
//
|
||
// 它是刻意精简的。所有后端能力都经由注入的依赖(构造期依赖注入)和
|
||
// 进程内 bus 流动,绝不跨进程 IPC,因此本包除网关端口外永不自行
|
||
// 打开监听器。
|
||
//
|
||
// 客户端契约(DevDocs/02-development-constraints.md §九、§十一):
|
||
// - REST 成功响应为 {data},错误响应为 {error:{code,message}}。
|
||
// - 认证是 JWT bearer(internal/auth);授权是 owner>admin>viewer 的
|
||
// RBAC 层级(internal/auth)。
|
||
// - 管理写操作有审计(谁/做了什么/何时/结果)。
|
||
// - 实时事件遵循 DevDocs/design/07 §3(WS 协议)。
|
||
//
|
||
// 文件布局:contracts.go 声明注入的依赖接口,deps.go 声明 Deps 组合,
|
||
// server.go 是 Server 生命周期(构造 / 服务 / 关闭),routes.go 是路由表
|
||
// 加外层中间件,broadcast.go 是下行实时推送。
|
||
package gateway
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"io"
|
||
"io/fs"
|
||
"log/slog"
|
||
"net"
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||
pluginmgr "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/plugin"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
)
|
||
|
||
// source 是本包每条日志记录的来源标签。
|
||
const source = "gateway"
|
||
|
||
// Server 是网关本体。它持有聚合 REST + WS + admin 的 ServeMux 与一个
|
||
// WebSocket hub。构造完成后可安全并发使用。
|
||
type Server struct {
|
||
cfg *config.Config
|
||
auth Authenticator
|
||
bus bus.Bus
|
||
caps CapabilityStore
|
||
runner Chatter
|
||
sessions SessionStore
|
||
personas PersonaStore
|
||
memories MemoryStore
|
||
history HistoryStore
|
||
iot IoTStore
|
||
auditor SubagentAuditor
|
||
taskOutputs TaskOutputStore
|
||
automations AutomationStore
|
||
users UserStore
|
||
refresh RefreshGrantStore
|
||
backthink BackthinkStore
|
||
rag RagStore
|
||
obs *observability
|
||
voice VoiceService
|
||
streaming bool
|
||
frontend fs.FS
|
||
dbPath string
|
||
// dataDir 是支撑上传存储的已解析数据目录(<dataDir>/uploads)。
|
||
// 由组合根注入;cfg.DataDir 是回退值。见 Deps.DataDir。
|
||
dataDir string
|
||
// llm 是可热切换的提供方栈(models.json)。为 nil 时 admin 的 /llm
|
||
// 端点回退到环境变量配置。它是可选的,以便未接它的测试仍能工作。
|
||
llm *model.RuntimeLLM
|
||
// envPath 是启动时解析的 `.env` 路径(见 Deps.EnvPath):只供
|
||
// 「重置为 env 默认」动作按既有解析入口重读 env 默认层,网关不 fopen `.env`。
|
||
envPath string
|
||
channels ChannelConfigStore
|
||
// plugins 是可在运行时安装的外部工具提供方管理器。见
|
||
// Deps.Plugins。
|
||
plugins *pluginmgr.Manager
|
||
// ownerRegistrar 把扫码登录绑定的 owner id 写入 owner 白名单
|
||
// (尽力而为)。见 Deps.OwnerRegistrar。
|
||
ownerRegistrar OwnerRegistrar
|
||
webhooks map[string]http.Handler
|
||
// usage 是 LLM 用量汇总的只读出口(切片 U1,见 Deps.Usage)。为 nil 时
|
||
// GET /api/v1/admin/llm/usage 回 500 usage_unavailable。
|
||
usage LlmUsageStore
|
||
// db 是组合根注入的活 SQLite 连接池(见 Deps.Database)。管理侧
|
||
// 「存储状态」只读端点用它读页级统计与 documents 表;为 nil 时该
|
||
// 端点回 500 storage_unavailable(不另开池、不假装有数据)。
|
||
db *sql.DB
|
||
// vector 是组合根报告的 pgvector 增强运行时状态(见
|
||
// Deps.Vector / VectorState)。零值 = 未配置。
|
||
vector VectorState
|
||
// maxParallelTurns 是单会话在途并行路数的上限(见 Deps.MaxParallelTurns)。
|
||
// 零值 = 未接线 ⇒ 编排器用默认 2。
|
||
maxParallelTurns int
|
||
// envResolver 解析会话每轮的 loop 环境(其角色)。见
|
||
// persona_env.go / design/22 §4.5。
|
||
envResolver turnEnvResolver
|
||
log *slog.Logger
|
||
hub *hub
|
||
}
|
||
|
||
// New 校验 cfg 与 deps 并返回就绪的 Server。Auth 必填;WebSocket hub
|
||
// 会立即订阅 deps.Bus,因此 ListenAndServe 之前发布的事件仍会转发给
|
||
// 客户端。
|
||
func New(cfg *config.Config, deps Deps) (*Server, error) {
|
||
if cfg == nil {
|
||
return nil, fmt.Errorf("gateway: nil config")
|
||
}
|
||
if deps.Auth == nil {
|
||
return nil, fmt.Errorf("gateway: auth dependency required")
|
||
}
|
||
s := &Server{
|
||
cfg: cfg,
|
||
auth: deps.Auth,
|
||
bus: deps.Bus,
|
||
caps: deps.Registry,
|
||
runner: deps.Loop,
|
||
sessions: deps.Session,
|
||
personas: deps.Personas,
|
||
memories: deps.Memories,
|
||
history: deps.History,
|
||
iot: deps.IoT,
|
||
auditor: deps.Scheduler,
|
||
taskOutputs: deps.Tasks,
|
||
automations: deps.Automations,
|
||
users: deps.Users,
|
||
refresh: deps.Refresh,
|
||
backthink: deps.Backthink,
|
||
rag: deps.RAG,
|
||
obs: newObservability(deps),
|
||
streaming: deps.Streaming,
|
||
frontend: deps.Frontend,
|
||
llm: deps.LLM,
|
||
envPath: deps.EnvPath,
|
||
channels: deps.Channels,
|
||
plugins: deps.Plugins,
|
||
ownerRegistrar: deps.OwnerRegistrar,
|
||
webhooks: deps.Webhooks,
|
||
usage: deps.Usage,
|
||
dbPath: deps.DatabasePath,
|
||
dataDir: deps.DataDir,
|
||
db: deps.Database,
|
||
vector: deps.Vector,
|
||
log: sourcedLog(deps.Log),
|
||
maxParallelTurns: deps.MaxParallelTurns,
|
||
}
|
||
// 一个角色解析器由 Server 各处理器与 hub 共用,使每个面向 owner
|
||
// 的入口(REST、WS、语音)以同样方式解析会话角色(design/22 §4.5),
|
||
// 而不是各自硬编码 loop.OwnerEnv()。
|
||
s.envResolver = turnEnvResolver{history: deps.History, personas: deps.Personas, log: s.log}
|
||
s.hub = newHub(s)
|
||
s.hub.orchestrator = newOrchestrator(s)
|
||
s.hub.start()
|
||
return s, nil
|
||
}
|
||
|
||
// Start 在 goroutine 中启动网关,返回携带 ListenAndServe 结果的
|
||
// channel。服务器运行至 ctx 被取消。
|
||
func (s *Server) Start(ctx context.Context) (<-chan error, error) {
|
||
errCh := make(chan error, 1)
|
||
go func() { errCh <- s.ListenAndServe(ctx) }()
|
||
return errCh, nil
|
||
}
|
||
|
||
// ListenAndServe 绑定配置的 host:port(默认 :8080)并阻塞至 ctx 被
|
||
// 取消,随后优雅地排空在途请求。
|
||
func (s *Server) ListenAndServe(ctx context.Context) error {
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
addr := net.JoinHostPort(s.cfg.Server.Host, strconv.Itoa(s.cfg.Server.Port))
|
||
httpSrv := &http.Server{Addr: addr, Handler: s.Handler()}
|
||
|
||
done := make(chan struct{})
|
||
go func() {
|
||
<-ctx.Done()
|
||
s.log.Info("gateway shutting down", "addr", addr)
|
||
s.hub.close()
|
||
shCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
defer cancel()
|
||
_ = httpSrv.Shutdown(shCtx)
|
||
close(done)
|
||
}()
|
||
|
||
s.log.Info("gateway listening", "addr", addr)
|
||
err := httpSrv.ListenAndServe()
|
||
if err != nil && err != http.ErrServerClosed {
|
||
return fmt.Errorf("gateway: listen: %w", err)
|
||
}
|
||
<-done
|
||
return nil
|
||
}
|
||
|
||
// Close 释放 hub 的 bus 订阅并关闭存活连接。
|
||
func (s *Server) Close() { s.hub.close() }
|
||
|
||
// newOrchestrator 构建绑定到本 server 的 hub 的会话消息 orchestrator
|
||
// (design/15)。TurnRunner 复用 hub 的 runTurn(因此一轮总是发出相同的
|
||
// 流契约),StateSink 则把每会话状态迁移经由 hub 广播扇出给前端 ——
|
||
// 这些迁移属于 design/11 §7.1 的「状态机信号」,因此每个已认证连接都
|
||
// 可以跟随它们。它被包在函数里,以便组合根之后能覆盖注入(一个显式
|
||
// 的接线点,而不是硬编码的构造)。
|
||
func newOrchestrator(s *Server) *Orchestrator {
|
||
return NewOrchestrator(OrchestratorOptions{
|
||
Run: func(ctx context.Context, sessionID string, messages []model.Message, env loop.SessionEnv) (*loop.Result, error) {
|
||
return s.hub.runTurn(ctx, sessionID, messages, env)
|
||
},
|
||
Emit: func(sessionID, typ string, data map[string]any) {
|
||
s.hub.broadcast(wsEvent{Type: typ, SessionID: sessionID, Data: data})
|
||
},
|
||
// EmitTurn 是带 turnId 的变体(design/15 §2.5.9):帧壳上的 turnId 让客户端
|
||
// 把「这条状态帧属于哪一路」与流帧对齐 —— 真并行下同一会话有两条交错的路,
|
||
// 只按 sessionId 分桶会把它们糊在一起。
|
||
EmitTurn: func(sessionID, turnID, typ string, data map[string]any) {
|
||
s.hub.broadcast(wsEvent{Type: typ, SessionID: sessionID, TurnID: turnID, Data: data})
|
||
},
|
||
OnPersisted: s.BroadcastPersisted,
|
||
Log: s.log,
|
||
History: s.history,
|
||
MaxParallelTurns: s.maxParallelTurns,
|
||
})
|
||
}
|
||
|
||
// sourcedLog 返回带 source "gateway" 标签的 logger,nil 时替换为丢弃型
|
||
// logger,使 Server 总能安全地打日志。
|
||
func sourcedLog(l *slog.Logger) *slog.Logger {
|
||
if l == nil {
|
||
l = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
}
|
||
return logger.WithSource(l, source)
|
||
}
|