package monitor import ( "context" "strings" "sync" "time" "monica-go-completion-backend/internal/completion" ) // Config 控制探测间隔与下线阈值。 type Config struct { ProbeInterval time.Duration ProbeTimeout time.Duration FailureThreshold int } // LanguageStatus 表示单语言探测状态。 type LanguageStatus struct { Language string `json:"language"` Online bool `json:"online"` LastCheckedAt time.Time `json:"lastCheckedAt"` LastSuccessAt time.Time `json:"lastSuccessAt,omitempty"` ConsecutiveFailures int `json:"consecutiveFailures"` LastError string `json:"lastError,omitempty"` } // LSPStatusMonitor 周期性探测各语言 LSP 是否可用。 type LSPStatusMonitor struct { specs []completion.LanguageServerSpec workspaceDir string factory completion.ClientFactory cfg Config mu sync.RWMutex statuses map[string]LanguageStatus stopCh chan struct{} stopOnce sync.Once } // NewLSPStatusMonitor 创建并启动探测协程。 func NewLSPStatusMonitor( specs []completion.LanguageServerSpec, workspaceDir string, factory completion.ClientFactory, cfg Config, ) *LSPStatusMonitor { if cfg.ProbeInterval <= 0 { cfg.ProbeInterval = 8 * time.Second } if cfg.ProbeTimeout <= 0 { cfg.ProbeTimeout = 5 * time.Second } if cfg.FailureThreshold <= 0 { cfg.FailureThreshold = 2 } normalized := make([]completion.LanguageServerSpec, 0, len(specs)) statuses := make(map[string]LanguageStatus) for _, spec := range specs { lang := normalizeLanguage(spec.Language) if lang == "" { continue } spec.Language = lang normalized = append(normalized, spec) statuses[lang] = LanguageStatus{ Language: lang, Online: false, } } m := &LSPStatusMonitor{ specs: normalized, workspaceDir: workspaceDir, factory: factory, cfg: cfg, statuses: statuses, stopCh: make(chan struct{}), } go m.loop() return m } // LspServiceStatus 返回按语言聚合的状态快照。 func (m *LSPStatusMonitor) LspServiceStatus() map[string]any { m.mu.RLock() defer m.mu.RUnlock() out := make(map[string]any, len(m.statuses)) for language, status := range m.statuses { out[language] = status } return out } // Close 停止后台探测循环。 func (m *LSPStatusMonitor) Close() { m.stopOnce.Do(func() { close(m.stopCh) }) } func (m *LSPStatusMonitor) loop() { // 启动后先做一轮探测,避免首次读取全是未知状态。 m.probeAll() ticker := time.NewTicker(m.cfg.ProbeInterval) defer ticker.Stop() for { select { case <-ticker.C: m.probeAll() case <-m.stopCh: return } } } func (m *LSPStatusMonitor) probeAll() { for _, spec := range m.specs { m.probeOne(spec) } } func (m *LSPStatusMonitor) probeOne(spec completion.LanguageServerSpec) { language := normalizeLanguage(spec.Language) if language == "" { return } ctx, cancel := context.WithTimeout(context.Background(), m.cfg.ProbeTimeout) defer cancel() client, err := m.factory(ctx, spec, m.workspaceDir) if err != nil { m.markFailure(language, err) return } _ = client.Close() m.markSuccess(language) } func (m *LSPStatusMonitor) markSuccess(language string) { now := time.Now() m.mu.Lock() defer m.mu.Unlock() st := m.statuses[language] st.LastCheckedAt = now st.LastSuccessAt = now st.ConsecutiveFailures = 0 st.LastError = "" st.Online = true m.statuses[language] = st } func (m *LSPStatusMonitor) markFailure(language string, err error) { now := time.Now() m.mu.Lock() defer m.mu.Unlock() st := m.statuses[language] st.LastCheckedAt = now st.ConsecutiveFailures++ if err != nil { st.LastError = err.Error() } if st.ConsecutiveFailures >= m.cfg.FailureThreshold { st.Online = false } m.statuses[language] = st } func normalizeLanguage(language string) string { return strings.ToLower(strings.TrimSpace(language)) }