diff --git a/cmd/gateway.go b/cmd/gateway.go index 66ac2dc6..e18efc85 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -12,6 +12,7 @@ import ( "time" "github.com/nextlevelbuilder/goclaw/internal/agent" + "github.com/nextlevelbuilder/goclaw/internal/bgalert" "github.com/nextlevelbuilder/goclaw/internal/bootstrap" "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/cache" @@ -201,6 +202,7 @@ func runGateway() { SystemConfigs: pgStores.SystemConfigs, Registry: providerRegistry, Extractor: kgExtractor, + AlertDeps: bgalert.AlertDeps{SystemConfigs: pgStores.SystemConfigs, MsgBus: msgBus}, AgentStore: pgStores.Agents, }) defer cleanupConsolidation() @@ -222,6 +224,7 @@ func runGateway() { EventBus: domainBus, MsgBus: msgBus, TeamStore: pgStores.Teams, + AlertDeps: bgalert.AlertDeps{SystemConfigs: pgStores.SystemConfigs, MsgBus: msgBus}, }) enrichProgress = ep enrichWorker = ew diff --git a/internal/bgalert/report.go b/internal/bgalert/report.go new file mode 100644 index 00000000..e32093dc --- /dev/null +++ b/internal/bgalert/report.go @@ -0,0 +1,120 @@ +// Package bgalert reports non-retryable LLM errors from background workers +// to the web UI via system_configs + WS events. +package bgalert + +import ( + "context" + "encoding/json" + "log/slog" + "regexp" + "time" + + "github.com/nextlevelbuilder/goclaw/internal/bus" + "github.com/nextlevelbuilder/goclaw/internal/providers" + "github.com/nextlevelbuilder/goclaw/internal/store" + "github.com/nextlevelbuilder/goclaw/pkg/protocol" +) + +// AlertKeyProviderError is the system_configs key for background provider errors. +const AlertKeyProviderError = "alert.background.provider_error" + +// AlertDeps bundles dependencies for error reporting. +type AlertDeps struct { + SystemConfigs store.SystemConfigStore + MsgBus bus.EventPublisher +} + +// ProviderErrorPayload is stored in system_configs and sent via WS event. +type ProviderErrorPayload struct { + Reason string `json:"reason"` + Worker string `json:"worker"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` +} + +// alertableReasons are non-retryable errors that should surface to the user. +var alertableReasons = map[providers.FailoverReason]bool{ + providers.FailoverAuth: true, + providers.FailoverAuthPermanent: true, + providers.FailoverBilling: true, + providers.FailoverModelNotFound: true, +} + +// IsAlertableError returns true if the error is non-retryable and should +// be surfaced to the user (auth, billing, model_not_found). +func IsAlertableError(err error) (providers.FailoverReason, bool) { + if err == nil { + return "", false + } + cls := providers.ClassifyHTTPError(providers.NewDefaultClassifier(), err) + if cls.Kind != "reason" { + return "", false + } + return cls.Reason, alertableReasons[cls.Reason] +} + +// ReportProviderError stores an alert in system_configs and broadcasts a +// WS event when the error is non-retryable (auth/billing/model_not_found). +// Safe to call with nil deps or for retryable errors — returns silently. +func ReportProviderError(ctx context.Context, deps AlertDeps, workerName string, err error) { + if deps.SystemConfigs == nil || err == nil { + return + } + reason, alertable := IsAlertableError(err) + if !alertable { + return + } + + payload := ProviderErrorPayload{ + Reason: string(reason), + Worker: workerName, + Message: sanitizeErrorMessage(err.Error()), + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + data, jsonErr := json.Marshal(payload) + if jsonErr != nil { + slog.Warn("bgalert: marshal failed", "err", jsonErr) + return + } + + if setErr := deps.SystemConfigs.Set(ctx, AlertKeyProviderError, string(data)); setErr != nil { + slog.Warn("bgalert: store alert failed", "err", setErr) + return + } + + slog.Warn("bgalert: provider error stored", + "worker", workerName, "reason", reason, "err", err) + + if deps.MsgBus != nil { + tenantID := store.TenantIDFromContext(ctx) + deps.MsgBus.Broadcast(bus.Event{ + Name: protocol.EventBackgroundError, + TenantID: tenantID, + Payload: payload, + }) + } +} + +// ClearProviderError removes the alert key from system_configs. +// Ignores "not found" errors. Safe to call when no alert exists. +func ClearProviderError(ctx context.Context, configs store.SystemConfigStore) { + if configs == nil { + return + } + _ = configs.Delete(ctx, AlertKeyProviderError) +} + +// apiKeyPattern matches common API key formats in error messages. +var apiKeyPattern = regexp.MustCompile(`(?i)(sk-|Bearer\s+|api[_-]?key[=:]\s*)[a-zA-Z0-9_-]{4,}`) + +// sanitizeErrorMessage strips potentially sensitive content from error messages. +// Keeps the message useful for diagnostics while removing API keys and long bodies. +func sanitizeErrorMessage(msg string) string { + msg = apiKeyPattern.ReplaceAllString(msg, "${1}****") + // Rune-safe truncation to avoid corrupting multi-byte UTF-8 characters. + const maxLen = 200 + if runes := []rune(msg); len(runes) > maxLen { + msg = string(runes[:maxLen]) + "..." + } + return msg +} diff --git a/internal/consolidation/dreaming_worker.go b/internal/consolidation/dreaming_worker.go index 36b86bc7..4a97bf76 100644 --- a/internal/consolidation/dreaming_worker.go +++ b/internal/consolidation/dreaming_worker.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/nextlevelbuilder/goclaw/internal/bgalert" "github.com/nextlevelbuilder/goclaw/internal/eventbus" "github.com/nextlevelbuilder/goclaw/internal/providers" "github.com/nextlevelbuilder/goclaw/internal/providerresolve" @@ -29,6 +30,7 @@ type dreamingWorker struct { memoryStore store.MemoryStore systemConfigs store.SystemConfigStore // per-tenant provider config registry *providers.Registry // provider resolution + alertDeps bgalert.AlertDeps // threshold/debounce are the global defaults. Per-agent overrides come // from resolveConfig which reads the agent's MemoryConfig.Dreaming JSONB. @@ -160,6 +162,7 @@ func (w *dreamingWorker) Handle(ctx context.Context, event eventbus.DomainEvent) // Build LLM prompt and call provider. synthesis, err := w.synthesize(ctx, provider, model, entries) if err != nil { + bgalert.ReportProviderError(ctx, w.alertDeps, "dreaming", err) slog.Warn("dreaming: LLM synthesis failed", "err", err, "agent", agentID) return nil } diff --git a/internal/consolidation/episodic_worker.go b/internal/consolidation/episodic_worker.go index dfa519f1..a2f5c184 100644 --- a/internal/consolidation/episodic_worker.go +++ b/internal/consolidation/episodic_worker.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/nextlevelbuilder/goclaw/internal/bgalert" "github.com/nextlevelbuilder/goclaw/internal/eventbus" "github.com/nextlevelbuilder/goclaw/internal/providers" "github.com/nextlevelbuilder/goclaw/internal/providerresolve" @@ -21,6 +22,7 @@ type episodicWorker struct { systemConfigs store.SystemConfigStore // per-tenant provider config registry *providers.Registry // provider resolution eventBus eventbus.DomainEventBus + alertDeps bgalert.AlertDeps } // resolveProvider delegates to shared background provider resolution. @@ -45,6 +47,8 @@ func (w *episodicWorker) Handle(ctx context.Context, event eventbus.DomainEvent) if err != nil { return fmt.Errorf("episodic: invalid tenant_id %q: %w", event.TenantID, err) } + // Inject tenant context so store queries and bgalert scope correctly. + ctx = store.WithTenantID(ctx, tenantUUID) agentUUID, err := uuid.Parse(event.AgentID) if err != nil { return fmt.Errorf("episodic: invalid agent_id %q: %w", event.AgentID, err) @@ -68,6 +72,7 @@ func (w *episodicWorker) Handle(ctx context.Context, event eventbus.DomainEvent) if provider != nil { summary, err = w.summarizeSession(ctx, provider, model, payload) if err != nil { + bgalert.ReportProviderError(ctx, w.alertDeps, "episodic", err) return fmt.Errorf("episodic: summarize: %w", err) } } diff --git a/internal/consolidation/semantic_worker.go b/internal/consolidation/semantic_worker.go index c6b75f01..fe243082 100644 --- a/internal/consolidation/semantic_worker.go +++ b/internal/consolidation/semantic_worker.go @@ -6,6 +6,8 @@ import ( "log/slog" "time" + "github.com/google/uuid" + "github.com/nextlevelbuilder/goclaw/internal/bgalert" "github.com/nextlevelbuilder/goclaw/internal/eventbus" "github.com/nextlevelbuilder/goclaw/internal/store" ) @@ -15,6 +17,7 @@ type semanticWorker struct { kgStore store.KnowledgeGraphStore extractor EntityExtractor eventBus eventbus.DomainEventBus + alertDeps bgalert.AlertDeps } // Handle extracts entities and relations from an episodic summary. @@ -24,6 +27,13 @@ func (w *semanticWorker) Handle(ctx context.Context, event eventbus.DomainEvent) return fmt.Errorf("semantic: unexpected payload type %T", event.Payload) } + // Inject tenant context so bgalert scopes correctly. + if event.TenantID != "" { + if tid, err := uuid.Parse(event.TenantID); err == nil { + ctx = store.WithTenantID(ctx, tid) + } + } + if w.extractor == nil || payload.Summary == "" { return nil } @@ -31,6 +41,7 @@ func (w *semanticWorker) Handle(ctx context.Context, event eventbus.DomainEvent) // Extract entities/relations from summary (much cheaper than full session) result, err := w.extractor.Extract(ctx, payload.Summary) if err != nil { + bgalert.ReportProviderError(ctx, w.alertDeps, "kg_extraction", err) slog.Warn("semantic: extraction failed", "episodic_id", payload.EpisodicID, "err", err) return nil // non-fatal: extraction failure doesn't block pipeline } diff --git a/internal/consolidation/workers.go b/internal/consolidation/workers.go index 1078fa42..1c5fb82a 100644 --- a/internal/consolidation/workers.go +++ b/internal/consolidation/workers.go @@ -9,6 +9,7 @@ import ( "log/slog" "time" + "github.com/nextlevelbuilder/goclaw/internal/bgalert" "github.com/nextlevelbuilder/goclaw/internal/eventbus" "github.com/nextlevelbuilder/goclaw/internal/providers" "github.com/nextlevelbuilder/goclaw/internal/store" @@ -24,6 +25,7 @@ type ConsolidationDeps struct { SystemConfigs store.SystemConfigStore // per-tenant provider config Registry *providers.Registry // provider resolution Extractor EntityExtractor + AlertDeps bgalert.AlertDeps // for reporting non-retryable LLM errors // AgentStore is optional: when present, the dreaming worker reads // per-agent overrides from MemoryConfig.Dreaming. If nil, the worker // uses its built-in defaults for every agent. @@ -39,11 +41,13 @@ func Register(deps ConsolidationDeps) func() { systemConfigs: deps.SystemConfigs, registry: deps.Registry, eventBus: deps.EventBus, + alertDeps: deps.AlertDeps, } semantic := &semanticWorker{ kgStore: deps.KGStore, extractor: deps.Extractor, eventBus: deps.EventBus, + alertDeps: deps.AlertDeps, } dedup := &dedupWorker{ kgStore: deps.KGStore, @@ -54,6 +58,7 @@ func Register(deps ConsolidationDeps) func() { memoryStore: deps.MemoryStore, systemConfigs: deps.SystemConfigs, registry: deps.Registry, + alertDeps: deps.AlertDeps, threshold: dreamingDefaultThreshold, debounce: dreamingDefaultDebounce, resolveConfig: newAgentStoreResolver(deps.AgentStore), diff --git a/internal/gateway/event_filter.go b/internal/gateway/event_filter.go index e7ac9d01..157d0ff5 100644 --- a/internal/gateway/event_filter.go +++ b/internal/gateway/event_filter.go @@ -147,7 +147,8 @@ func isAdminOnlyEvent(name string) bool { case protocol.EventNodePairRequested, protocol.EventNodePairResolved, protocol.EventDevicePairReq, protocol.EventDevicePairRes, protocol.EventAgentLinkCreated, protocol.EventAgentLinkUpdated, protocol.EventAgentLinkDeleted, - protocol.EventWorkspaceFileChanged: + protocol.EventWorkspaceFileChanged, + protocol.EventBackgroundError: return true } return false diff --git a/internal/http/system_configs.go b/internal/http/system_configs.go index da4a0b9d..ebf68150 100644 --- a/internal/http/system_configs.go +++ b/internal/http/system_configs.go @@ -6,6 +6,7 @@ import ( "net/http" "regexp" + "github.com/nextlevelbuilder/goclaw/internal/bgalert" "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/i18n" "github.com/nextlevelbuilder/goclaw/internal/store" @@ -24,6 +25,17 @@ func NewSystemConfigsHandler(s store.SystemConfigStore, msgBus *bus.MessageBus) // validKeyRe allows alphanumeric, dots, underscores, hyphens (1-100 chars). var validKeyRe = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,100}$`) +// providerRelatedKeys lists system config keys whose changes should clear +// background worker error alerts (user is likely fixing the root cause). +var providerRelatedKeys = map[string]bool{ + "background.provider": true, + "background.model": true, + "agent.default_provider": true, + "agent.default_model": true, + "embedding.provider": true, + "embedding.model": true, +} + // RegisterRoutes registers system config endpoints on the given mux. func (h *SystemConfigsHandler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /v1/system-configs", requireAuth("", h.handleList)) @@ -74,6 +86,11 @@ func (h *SystemConfigsHandler) handleSet(w http.ResponseWriter, r *http.Request) return } + // Auto-clear background error alert when provider settings change. + if providerRelatedKeys[key] { + bgalert.ClearProviderError(r.Context(), h.store) + } + // Broadcast change with tenant context so in-memory config refreshes for the right tenant. // Use fresh context with tenant ID — request context may be canceled before subscriber runs. if h.msgBus != nil { diff --git a/internal/vault/enrich_worker.go b/internal/vault/enrich_worker.go index 1fcf12ca..9494352b 100644 --- a/internal/vault/enrich_worker.go +++ b/internal/vault/enrich_worker.go @@ -13,6 +13,7 @@ import ( "time" "github.com/google/uuid" + "github.com/nextlevelbuilder/goclaw/internal/bgalert" "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/eventbus" "github.com/nextlevelbuilder/goclaw/internal/providers" @@ -45,6 +46,7 @@ type EnrichWorkerDeps struct { EventBus eventbus.DomainEventBus MsgBus bus.EventPublisher // for WS event broadcast TeamStore store.TaskCommentStore // for Phase 2.5 task-based auto-linking (nil-safe) + AlertDeps bgalert.AlertDeps // for reporting non-retryable LLM errors } // RegisterEnrichWorker subscribes the enrichment worker to vault doc events. @@ -57,6 +59,7 @@ func RegisterEnrichWorker(deps EnrichWorkerDeps) (func(), *EnrichProgress, *Enri systemConfigs: deps.SystemConfigs, registry: deps.Registry, msgBus: deps.MsgBus, + alertDeps: deps.AlertDeps, dedup: make(map[string]string), sem: semaphore.NewWeighted(enrichMaxConcurrent), progress: progress, @@ -75,6 +78,7 @@ type EnrichWorker struct { systemConfigs store.SystemConfigStore // per-tenant provider config registry *providers.Registry // provider resolution msgBus bus.EventPublisher // for error event broadcast + alertDeps bgalert.AlertDeps // for reporting non-retryable LLM errors queue enrichBatchQueue progress *EnrichProgress @@ -201,6 +205,11 @@ func (w *EnrichWorker) Handle(ctx context.Context, event eventbus.DomainEvent) e return nil // another goroutine already processing this agent's queue } + // Inject tenant context so store queries and bgalert scope correctly. + if tid, parseErr := uuid.Parse(payload.TenantID); parseErr == nil { + ctx = store.WithTenantID(ctx, tid) + } + // Create per-tenant cancel context for stop capability. cancelCtx, cancel := context.WithCancel(ctx) w.cancelFuncs.Store(payload.TenantID, cancel) @@ -517,6 +526,7 @@ func (w *EnrichWorker) chatWithRetry(ctx context.Context, provider providers.Pro } return strings.TrimSpace(resp.Content), nil } + bgalert.ReportProviderError(ctx, w.alertDeps, "vault_enrich", lastErr) return "", fmt.Errorf("%s exhausted %d retries: %w", logPrefix, enrichMaxRetries, lastErr) } diff --git a/pkg/protocol/events.go b/pkg/protocol/events.go index 3333c134..95a01982 100644 --- a/pkg/protocol/events.go +++ b/pkg/protocol/events.go @@ -108,6 +108,9 @@ const ( // Vault enrichment pipeline progress. EventVaultEnrichProgress = "vault.enrich.progress" + + // Background worker alerts (non-retryable LLM errors). + EventBackgroundError = "background.error" ) // Agent event subtypes (in payload.type) diff --git a/ui/web/src/components/layout/app-layout.tsx b/ui/web/src/components/layout/app-layout.tsx index 6095d542..8e0ad6b7 100644 --- a/ui/web/src/components/layout/app-layout.tsx +++ b/ui/web/src/components/layout/app-layout.tsx @@ -1,9 +1,10 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Outlet, useLocation } from "react-router"; import { WifiOff } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Sidebar } from "./sidebar"; import { Topbar } from "./topbar"; +import { BackgroundErrorBanner } from "./background-error-banner"; import { ErrorBoundary } from "@/components/shared/error-boundary"; import { useUiStore } from "@/stores/use-ui-store"; import { useAuthStore } from "@/stores/use-auth-store"; @@ -29,6 +30,7 @@ export function AppLayout() { const setMobileSidebarOpen = useUiStore((s) => s.setMobileSidebarOpen); const connected = useAuthStore((s) => s.connected); const isMobile = useIsTablet(); + const [settingsOpen, setSettingsOpen] = useState(false); // Close mobile sidebar on route change (e.g. programmatic navigation, back button) useEffect(() => { @@ -62,7 +64,8 @@ export function AppLayout() { )}
- + + setSettingsOpen(true)} /> {!connected && (
diff --git a/ui/web/src/components/layout/background-error-banner.tsx b/ui/web/src/components/layout/background-error-banner.tsx new file mode 100644 index 00000000..a706c994 --- /dev/null +++ b/ui/web/src/components/layout/background-error-banner.tsx @@ -0,0 +1,82 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { AlertTriangle, X, Settings2 } from "lucide-react"; +import { useHttp } from "@/hooks/use-ws"; +import { useWsEvent } from "@/hooks/use-ws-event"; +import { useAuthStore } from "@/stores/use-auth-store"; +import { Button } from "@/components/ui/button"; + +interface ProviderErrorPayload { + reason: string; + worker: string; + message: string; + timestamp: string; +} + +interface BackgroundErrorBannerProps { + settingsOpen: boolean; + onOpenSettings: () => void; +} + +export function BackgroundErrorBanner({ settingsOpen, onOpenSettings }: BackgroundErrorBannerProps) { + const { t } = useTranslation("system-settings"); + const http = useHttp(); + const role = useAuthStore((s) => s.role); + const isAdmin = role === "admin" || role === "owner"; + const [error, setError] = useState(null); + const [dismissed, setDismissed] = useState(false); + + // Fetch alert state from the specific system config key. + const fetchAlert = useCallback(() => { + if (!isAdmin) return; + http.get<{ key: string; value: string }>("/v1/system-configs/alert.background.provider_error") + .then((res) => { + if (res.value) { + try { setError(JSON.parse(res.value)); } catch { /* ignore */ } + } else { + setError(null); + } + }) + .catch(() => { setError(null); }); // 404 = no alert + }, [http, isAdmin]); + + // Fetch on mount + useEffect(() => { fetchAlert(); }, [fetchAlert]); + + // Re-fetch when settings modal closes (user may have fixed the issue) + const prevSettingsOpen = useRef(settingsOpen); + useEffect(() => { + if (prevSettingsOpen.current && !settingsOpen) fetchAlert(); + prevSettingsOpen.current = settingsOpen; + }, [settingsOpen, fetchAlert]); + + // Real-time WS event + useWsEvent("background.error", useCallback((payload: unknown) => { + setError(payload as ProviderErrorPayload); + setDismissed(false); + }, [])); + + if (!isAdmin || !error || dismissed) return null; + + const reasonKey = `alert.reason.${error.reason}`; + const reasonText = t(reasonKey, { defaultValue: t("alert.reason.unknown") }); + + return ( +
+ + + {t("alert.message", { reason: reasonText })} + + + +
+ ); +} diff --git a/ui/web/src/components/layout/topbar.tsx b/ui/web/src/components/layout/topbar.tsx index dc451f69..2ca870e7 100644 --- a/ui/web/src/components/layout/topbar.tsx +++ b/ui/web/src/components/layout/topbar.tsx @@ -15,7 +15,12 @@ import { ConfirmDialog } from "@/components/shared/confirm-dialog"; import { AboutDialog } from "./about-dialog"; import { SystemSettingsModal } from "./system-settings-modal"; -export function Topbar() { +interface TopbarProps { + settingsOpen: boolean; + onSettingsOpenChange: (open: boolean) => void; +} + +export function Topbar({ settingsOpen, onSettingsOpenChange }: TopbarProps) { const { t } = useTranslation("topbar"); const theme = useUiStore((s) => s.theme); const setTheme = useUiStore((s) => s.setTheme); @@ -29,7 +34,7 @@ export function Topbar() { const isMobile = useIsMobile(); const isDark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); const { status: embStatus } = useEmbeddingStatus(); - const [settingsOpen, setSettingsOpen] = useState(false); + const setSettingsOpen = onSettingsOpenChange; const role = useAuthStore((s) => s.role); const isAdmin = role === "admin" || role === "owner"; diff --git a/ui/web/src/i18n/locales/en/system-settings.json b/ui/web/src/i18n/locales/en/system-settings.json index 82587c59..fafc76c7 100644 --- a/ui/web/src/i18n/locales/en/system-settings.json +++ b/ui/web/src/i18n/locales/en/system-settings.json @@ -76,5 +76,16 @@ "maxTokensHint": "Token limit for compacted summary.", "info": "When a group chat exceeds the threshold, the LLM automatically summarizes older messages into a single compact entry, keeping the most recent ones intact for context." }, - "moreConfig": "More Config" + "moreConfig": "More Config", + "alert": { + "message": "Background workers failed: {{reason}}. Features like memory consolidation and vault enrichment may not work.", + "openSettings": "Fix in Settings", + "reason": { + "auth": "Authentication failed (invalid or expired API key)", + "auth_permanent": "API key revoked or disabled", + "billing": "Billing issue (credits exhausted or payment required)", + "model_not_found": "Model not found (invalid model name or not available)", + "unknown": "Unknown provider error" + } + } } diff --git a/ui/web/src/i18n/locales/vi/system-settings.json b/ui/web/src/i18n/locales/vi/system-settings.json index bf8dd366..994ffc63 100644 --- a/ui/web/src/i18n/locales/vi/system-settings.json +++ b/ui/web/src/i18n/locales/vi/system-settings.json @@ -76,5 +76,16 @@ "maxTokensHint": "Giới hạn token cho bản tóm tắt.", "info": "Khi nhóm chat vượt ngưỡng, LLM tự động tóm tắt các tin nhắn cũ thành một mục gọn, giữ nguyên các tin gần nhất cho ngữ cảnh." }, - "moreConfig": "Cấu hình thêm" + "moreConfig": "Cấu hình thêm", + "alert": { + "message": "Tác vụ nền gặp lỗi: {{reason}}. Các tính năng như tổng hợp bộ nhớ và làm giàu vault có thể không hoạt động.", + "openSettings": "Sửa trong Cài đặt", + "reason": { + "auth": "Xác thực thất bại (API key không hợp lệ hoặc hết hạn)", + "auth_permanent": "API key đã bị thu hồi hoặc vô hiệu hóa", + "billing": "Lỗi thanh toán (hết credit hoặc cần thanh toán)", + "model_not_found": "Không tìm thấy model (tên model không hợp lệ)", + "unknown": "Lỗi nhà cung cấp không xác định" + } + } } diff --git a/ui/web/src/i18n/locales/zh/system-settings.json b/ui/web/src/i18n/locales/zh/system-settings.json index 17d0a836..b87a3aaa 100644 --- a/ui/web/src/i18n/locales/zh/system-settings.json +++ b/ui/web/src/i18n/locales/zh/system-settings.json @@ -76,5 +76,16 @@ "maxTokensHint": "压缩摘要的Token限制。", "info": "当群聊超过阈值时,LLM自动将旧消息总结为一条紧凑记录,保留最近的消息以保持上下文。" }, - "moreConfig": "更多配置" + "moreConfig": "更多配置", + "alert": { + "message": "后台任务失败:{{reason}}。记忆整合和知识库增强等功能可能无法正常工作。", + "openSettings": "前往设置修复", + "reason": { + "auth": "认证失败(API密钥无效或已过期)", + "auth_permanent": "API密钥已被吊销或禁用", + "billing": "计费问题(余额不足或需要付款)", + "model_not_found": "模型未找到(模型名称无效或不可用)", + "unknown": "未知的提供商错误" + } + } }