package httpapi import ( "context" "time" "knowledge-governance/internal/libraryexecutor" ) // libraryExecutorView 是"封存执行器到底能不能用"的页面口径。 // // 为什么不是 `LIBRARY_AGENT_SOCKET != ""`:正式栈里 API 根本没有挂载那个 socket, // 配置非空并不代表链路通;更危险的是 agent 可能顶着正式单元名却在冻结另一个实例。 // 这里只认 worker 写入的真实观测,并把观测时间与执行器自报的身份一起给出来。 type libraryExecutorView struct { Available bool `json:"available"` Reason string `json:"reason,omitempty"` Instance string `json:"instance,omitempty"` Project string `json:"project,omitempty"` SourceApp string `json:"source_app,omitempty"` ArchiveRoot string `json:"archive_root,omitempty"` Busy bool `json:"busy"` Degraded string `json:"degraded,omitempty"` ObservedAt *time.Time `json:"observed_at,omitempty"` } type libraryExecutorStore interface { LibraryExecutorStatus(ctx context.Context, instance string) (*libraryexecutor.Observation, error) } // libraryExecutorAvailability 把"配置里的期望"与"worker 观测到的真实身份"合起来判定: // 观测不存在、过期、不可达、实例不符、降级、正忙,任何一条都不可用,并给出原因。 func libraryExecutorAvailability(ctx context.Context, config Config) (bool, string, libraryExecutorView) { // 期望身份是**封存源**:agent 冻结的是源库写入者,与同步目标(LIBRARY_TARGET_*) // 本来就是两套名字(本机候选栈:源 kgmaint、目标 kgseal)。 instance := config.LibrarySourceInstance view := libraryExecutorView{Instance: instance} if instance == "" { return false, "未配置封存来源实例身份(LIBRARY_SOURCE_INSTANCE),无法确认执行器归属", view } store, ok := config.Store.(libraryExecutorStore) if !ok { return false, "执行器状态存储不可用", view } observation, err := store.LibraryExecutorStatus(ctx, instance) if err != nil { return false, "读取执行器状态失败:" + err.Error(), view } if observation == nil { return false, "封存执行器还没有上报状态(worker 未运行,或实例身份与 worker 不一致)", view } available, reason := libraryexecutor.Evaluate(*observation, libraryexecutor.Expectation{Instance: instance, Project: config.LibrarySourceProject, ArchiveRoot: config.LibraryArchiveRoot}, executorStatusMaxAge(config), time.Now().UTC()) observedAt := observation.ObservedAt view.Available = available view.Reason = reason view.Project = observation.Identity.Project view.SourceApp = observation.Identity.SourceApp view.ArchiveRoot = observation.Identity.ArchiveRoot view.Busy = observation.Busy view.Degraded = observation.Degraded view.ObservedAt = &observedAt return available, reason, view } // defaultLibraryExecutorStatusMaxAge 与 worker 的探测间隔(30s)配套: // 没配上限绝不等于"永不过期"——那会把一次陈旧的"可用"永久留在页面上。 const defaultLibraryExecutorStatusMaxAge = 2 * time.Minute func executorStatusMaxAge(config Config) time.Duration { if config.LibraryExecutorStatusMaxAge > 0 { return config.LibraryExecutorStatusMaxAge } return defaultLibraryExecutorStatusMaxAge }