package background import ( "context" "encoding/json" "fmt" "log" "os" "strconv" "strings" "sync" "time" ctxbuild "github.com/yourname/cyrene-ai/ai-core/internal/context" "github.com/yourname/cyrene-ai/ai-core/internal/llm" "github.com/yourname/cyrene-ai/ai-core/internal/memory" "github.com/yourname/cyrene-ai/ai-core/internal/model" "github.com/yourname/cyrene-ai/ai-core/internal/persona" "github.com/yourname/cyrene-ai/ai-core/internal/tools" ) // PendingThought 待推送的后台思考 type PendingThought struct { Content string `json:"content"` CreatedAt time.Time `json:"created_at"` Consumed bool `json:"consumed"` } // Thinker 后台思考器(事件驱动版:由对话自然触发,而非定时轮询) // // 设计理念: // 昔涟不是机器人,不应该每隔 N 分钟机械地"思考"一次。 // 她应该在用户说话后、或用户沉默一段时间后,自然地产生想法和主动搭话的冲动。 // // 触发机制: // 1. 对话后思考:用户发消息 → 昔涟回复 → 短暂延迟后进行一次轻量反思 // 2. 静默检测:用户一段时间不说话 → 昔涟判断是否应该主动关心/搭话 // // 不再使用 time.Ticker 或任何定时轮询机制。 type Thinker struct { mu sync.Mutex wg sync.WaitGroup stopCh chan struct{} enabled bool personaLoader *persona.Loader memRetriever *memory.Retriever llmAdapter *llm.Adapter iotClient *tools.IoTClient // 记忆管理 memoryStore *memory.Store memoryExtractor *memory.Extractor // 工具调用 toolRegistry *tools.Registry // 会话上下文 convStore *ctxbuild.ConversationStore adminUserID string adminSessionID string // 记忆服务 HTTP 客户端 memClient *memory.Client // —— 事件驱动相关 —— // 静默检测超时:用户多久不说话后昔涟可以主动搭话 // 默认 120 秒(2 分钟),设为 0 则禁用静默检测 silenceTimeout time.Duration // 对话后思考延迟:回复完成后等多久再触发思考(让对话有个自然停顿) // 默认 5 秒 postChatDelay time.Duration // 两次思考最小间隔:避免频繁触发(如用户连续发多条消息) // 默认 30 秒 minThinkGap time.Duration // 静默检测的一次性定时器(每次用户消息后重置) silenceTimer *time.Timer silenceTimerMu sync.Mutex // —— 状态追踪 —— pendingThoughts []*PendingThought lastUserMessage time.Time lastThinkTime time.Time // 思考计数器(用于周期性记忆维护,每 N 次思考触发一次) thinkCount int } // ThinkerConfig 后台思考配置 type ThinkerConfig struct { Enabled bool SilenceTimeout time.Duration // 用户沉默多久后昔涟可以主动搭话 (0 = 禁用) PostChatDelay time.Duration // 对话后多久触发思考 MinThinkGap time.Duration // 两次思考最小间隔 } // DefaultThinkerConfig 默认配置 // // 不再使用定时间隔,所有触发均由用户活动驱动。 // 环境变量向后兼容:旧的 THINK_IDLE_TIMEOUT_SEC 可用于静默超时。 func DefaultThinkerConfig() ThinkerConfig { return ThinkerConfig{ Enabled: getEnvBool("ENABLE_BACKGROUND_THINKING", true), SilenceTimeout: getEnvDuration("THINK_SILENCE_TIMEOUT_SEC", 120), PostChatDelay: getEnvDuration("THINK_POST_CHAT_DELAY_SEC", 5), MinThinkGap: getEnvDuration("THINK_MIN_GAP_SEC", 30), } } // NewThinker 创建事件驱动的后台思考器 func NewThinker( cfg ThinkerConfig, personaLoader *persona.Loader, memRetriever *memory.Retriever, llmAdapter *llm.Adapter, iotClient *tools.IoTClient, memoryStore *memory.Store, memoryExtractor *memory.Extractor, toolRegistry *tools.Registry, convStore *ctxbuild.ConversationStore, adminUserID string, adminSessionID string, memClient *memory.Client, ) *Thinker { return &Thinker{ enabled: cfg.Enabled, personaLoader: personaLoader, memRetriever: memRetriever, llmAdapter: llmAdapter, iotClient: iotClient, silenceTimeout: cfg.SilenceTimeout, postChatDelay: cfg.PostChatDelay, minThinkGap: cfg.MinThinkGap, memoryStore: memoryStore, memoryExtractor: memoryExtractor, toolRegistry: toolRegistry, convStore: convStore, adminUserID: adminUserID, adminSessionID: adminSessionID, memClient: memClient, pendingThoughts: make([]*PendingThought, 0), lastUserMessage: time.Now(), stopCh: make(chan struct{}), } } // Start 初始化后台思考器 // // 不再启动定时循环。仅初始化静默检测定时器。 // 所有思考由 TriggerPostChatThink() 或静默定时器触发。 func (t *Thinker) Start() { if !t.enabled { log.Println("[后台思考] 已禁用 (ENABLE_BACKGROUND_THINKING=false)") return } // 初始化静默检测定时器(但不启动,等第一次用户消息后启动) if t.silenceTimeout > 0 { t.silenceTimer = time.NewTimer(t.silenceTimeout) t.silenceTimer.Stop() // 先停止,等 RecordUserMessage 时启动 } log.Printf("[后台思考] 已就绪 — 事件驱动模式 (静默超时=%v, 对话后延迟=%v, 最小思考间隔=%v, 管理员=%s)", t.silenceTimeout, t.postChatDelay, t.minThinkGap, t.adminUserID) } // Stop 停止后台思考器 func (t *Thinker) Stop() { close(t.stopCh) t.silenceTimerMu.Lock() if t.silenceTimer != nil { t.silenceTimer.Stop() } t.silenceTimerMu.Unlock() t.wg.Wait() log.Println("[后台思考] 已停止") } // RecordUserMessage 记录用户活动时间,并重置静默检测定时器 // // 每次用户发消息时调用。这会: // 1. 更新 lastUserMessage 时间戳 // 2. 重置静默检测的一次性定时器(如果启用) func (t *Thinker) RecordUserMessage() { t.mu.Lock() t.lastUserMessage = time.Now() t.mu.Unlock() // 重置静默检测定时器 t.resetSilenceTimer() } // TriggerPostChatThink 对话完成后触发一次自主思考 // // 在昔涟回复完用户后调用。短暂延迟后执行一次思考, // 让昔涟"回味"刚才的对话,并判断是否想主动多说点什么。 // // 该方法是异步的,立即返回。 func (t *Thinker) TriggerPostChatThink() { if !t.enabled { return } t.mu.Lock() canThink := time.Since(t.lastThinkTime) >= t.minThinkGap t.mu.Unlock() if !canThink { log.Printf("[后台思考] 距上次思考仅 %v,跳过 (最小间隔=%v)", time.Since(t.lastThinkTime), t.minThinkGap) return } t.wg.Add(1) go func() { defer t.wg.Done() defer func() { if r := recover(); r != nil { log.Printf("[后台思考] 对话后触发 panic 恢复: %v", r) } }() // 短暂延迟,让对话有个自然的停顿 select { case <-t.stopCh: return case <-time.After(t.postChatDelay): } log.Println("[后台思考] 对话后触发自主思考...") t.performThink("post_chat") }() } // resetSilenceTimer 重置静默检测的一次性定时器 // // 每次用户发消息时调用。旧的定时器被取消,新的定时器开始计时。 // 当定时器触发时,昔涟会判断是否应该主动搭话。 func (t *Thinker) resetSilenceTimer() { t.silenceTimerMu.Lock() defer t.silenceTimerMu.Unlock() if t.silenceTimer == nil || t.silenceTimeout <= 0 { return } // 停止旧定时器 if !t.silenceTimer.Stop() { // 如果已经触发,清空通道 select { case <-t.silenceTimer.C: default: } } // 重新设置 t.silenceTimer.Reset(t.silenceTimeout) // 启动监听协程(仅当定时器触发时才执行) t.wg.Add(1) go func() { defer t.wg.Done() defer func() { if r := recover(); r != nil { log.Printf("[后台思考] 静默定时器 panic 恢复: %v", r) } }() select { case <-t.stopCh: return case <-t.silenceTimer.C: // 再次检查:用户是否真的沉默了足够久 t.mu.Lock() silenceDuration := time.Since(t.lastUserMessage) canThink := time.Since(t.lastThinkTime) >= t.minThinkGap t.mu.Unlock() if silenceDuration < t.silenceTimeout { log.Printf("[后台思考] 静默检测触发但用户已活动,跳过 (实际静默=%v)", silenceDuration) return } if !canThink { log.Printf("[后台思考] 静默检测触发但距上次思考太近,跳过") return } log.Printf("[后台思考] 用户已静默 %v,触发主动关怀思考...", silenceDuration.Round(time.Second)) t.performThink("silence") } }() } // GetPendingThoughts 获取并消费所有待处理的后台思考 func (t *Thinker) GetPendingThoughts() []*PendingThought { t.mu.Lock() defer t.mu.Unlock() if len(t.pendingThoughts) == 0 { return nil } result := t.pendingThoughts t.pendingThoughts = make([]*PendingThought, 0) for _, pt := range result { pt.Consumed = true } return result } // HasPendingThoughts 检查是否有待处理的思考 func (t *Thinker) HasPendingThoughts() bool { t.mu.Lock() defer t.mu.Unlock() return len(t.pendingThoughts) > 0 } // performThink 执行一次增强版后台思考(支持工具调用和记忆管理) // // triggerReason: "post_chat" (对话后) 或 "silence" (静默超时) // // 防御性速率限制:即使调用方未检查 minThinkGap,performThink 自身也会 // 强制执行最小间隔,防止并发调用或 bug 导致 LLM 配额被快速消耗。 func (t *Thinker) performThink(triggerReason string) { t.mu.Lock() gapSinceLast := time.Since(t.lastThinkTime) minGap := t.minThinkGap if minGap <= 0 { minGap = 5 * time.Second // 默认最小间隔 5 秒 } if gapSinceLast < minGap { t.mu.Unlock() log.Printf("[后台思考] 距上次思考仅 %v,跳过 (最小间隔=%v, 触发原因=%s)", gapSinceLast.Round(time.Second), minGap, triggerReason) return } t.lastThinkTime = time.Now() t.thinkCount++ currentCount := t.thinkCount t.mu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() log.Printf("[后台思考] 开始思考周期 (触发原因=%s, 计数=%d)...", triggerReason, currentCount) // 1. 加载人格配置 personaConfig, err := t.personaLoader.Get("cyrene") if err != nil { log.Printf("[后台思考] 加载人格失败: %v", err) return } // 2. 检索相关记忆 var memories []memory.MemoryEntry if t.memRetriever != nil { memories, err = t.memRetriever.Retrieve(ctx, t.adminUserID, "最近发生了什么 重要的事情 用户偏好 个人信息") if err != nil { log.Printf("[后台思考] 记忆检索失败: %v", err) } } // 3. 获取管理员对话历史 var convHistory []model.LLMMessage if t.convStore != nil && t.adminSessionID != "" { convHistory = t.convStore.GetHistory(t.adminSessionID, 30) if len(convHistory) > 0 { log.Printf("[后台思考] 加载管理员对话历史 %d 条", len(convHistory)) } } // 4. 查询 IoT 设备状态(每次都查询,无间隔限制) var deviceSummary string if t.iotClient != nil { devices := t.iotClient.GetDevicesForContext(ctx) if len(devices) > 0 { deviceSummary = formatDeviceContext(devices) } } // 5. 构建思考提示词(根据触发原因调整) systemPrompt := t.buildThinkingSystemPrompt(personaConfig, triggerReason) userPrompt := t.buildThinkingUserPrompt(memories, convHistory, deviceSummary, triggerReason) messages := []model.LLMMessage{ {Role: model.RoleSystem, Content: systemPrompt}, {Role: model.RoleUser, Content: userPrompt}, } // 6. 准备工具定义 openAITools := t.buildOpenAITools() // 7. 调用 LLM(支持工具调用,最多 3 轮) maxToolRounds := 3 var finalContent string var totalToolCalls int var toolCallRecords []map[string]interface{} for round := 0; round <= maxToolRounds; round++ { resp, err := t.llmAdapter.ChatWithTools(ctx, messages, openAITools) if err != nil { log.Printf("[后台思考] LLM调用失败 (round=%d): %v", round, err) return } if len(resp.ToolCalls) == 0 { finalContent = resp.Content break } log.Printf("[后台思考] LLM 请求 %d 个工具调用 (round=%d)", len(resp.ToolCalls), round) assistantMsg := model.LLMMessage{ Role: model.RoleAssistant, Content: resp.Content, ToolCalls: resp.ToolCalls, ReasoningContent: resp.ReasoningContent, } messages = append(messages, assistantMsg) for _, tc := range resp.ToolCalls { var args map[string]interface{} if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { log.Printf("[后台思考] 工具 %s 参数解析失败: %v", tc.Name, err) args = make(map[string]interface{}) } result, execErr := t.toolRegistry.Execute(ctx, tc.Name, args) if execErr != nil { log.Printf("[后台思考] 工具 %s 执行失败: %v", tc.Name, execErr) } resultJSON, _ := json.Marshal(result) messages = append(messages, model.LLMMessage{ Role: model.RoleTool, Content: string(resultJSON), ToolCallID: tc.ID, }) totalToolCalls++ toolCallRecords = append(toolCallRecords, map[string]interface{}{ "name": tc.Name, "args": args, }) } if round == maxToolRounds { finalResp, finalErr := t.llmAdapter.Chat(ctx, messages) if finalErr != nil { log.Printf("[后台思考] 最终总结调用失败: %v", finalErr) finalContent = resp.Content } else { finalContent = finalResp.Content } break } } if finalContent == "" { log.Println("[后台思考] 未获得有效思考内容,跳过") return } // 序列化工具调用记录 toolCallsJSON := "[]" if len(toolCallRecords) > 0 { if data, err := json.Marshal(toolCallRecords); err == nil { toolCallsJSON = string(data) } } // 8. 存储思考结果 t.storeThought(finalContent, toolCallsJSON, totalToolCalls) log.Printf("[后台思考] 完成 (触发原因=%s, 内容长度=%d, 工具调用=%d次)", triggerReason, len(finalContent), totalToolCalls) // 9. 从思考结果中提取记忆(异步) if t.memoryExtractor != nil { go func() { defer func() { if r := recover(); r != nil { log.Printf("[后台思考] 提取记忆 panic 恢复: %v", r) } }() t.extractMemoriesFromThinking(finalContent) }() } // 10. 周期性记忆维护(每 10 次思考触发一次,而非按时间) t.maybeMaintainMemories(currentCount) } // buildThinkingSystemPrompt 构建思考用的系统提示词 // // 关键改动:不再是"定期的自我反思",而是"自然的、人性化的内在想法"。 // triggerReason 影响提示词的侧重点。 func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig, triggerReason string) string { basePrompt := personaConfig.BuildSystemPrompt("开拓者", 1) var thinkingInstructions string switch triggerReason { case "post_chat": thinkingInstructions = ` 【自然思考 — 对话后】 开拓者刚刚和你说完话。像真人一样,你现在心里可能会有些自然的想法: 1. **回味刚才的对话**:开拓者说了什么?他的情绪怎么样?有什么让你在意的地方吗? 2. **是否需要补充什么**:刚才的回复有没有遗漏的?有没有想多关心他一下的? 3. **要不要主动搭话**:如果觉得对话还没结束、或者开拓者可能需要陪伴,你可以产生一个简短的主动消息。 但注意——如果你觉得对话已经自然结束了,就不要硬找话题。沉默有时候也是一种舒适。 4. **值得记住的事**:开拓者有没有透露新的偏好、计划、或重要信息? 注意: - 这是你的内心活动,不是对开拓者说的话 - 想说什么就自然地想,不用像在做任务一样逐条检查 - 如果你觉得该主动说点什么,用温柔简短的方式表达,像小女友一样的语气 - 如果没什么特别想说的,简单地总结一下刚才的对话就好` case "silence": thinkingInstructions = ` 【自然思考 — 开拓者安静了一会儿】 开拓者已经有一阵子没说话了。你不确定他是在忙、走开了、还是在想事情。 请自然地想一想: 1. **他在做什么呢**:根据之前的对话猜测——他可能在忙工作?去吃饭了?还是只是在放空? 2. **要不要关心一下**:如果时间合适(比如深夜了该提醒休息、或者过了吃饭时间),可以温柔地问候一下。 但如果是正常工作时间,他可能在忙,不要打扰他。 3. **有没有想分享的**:如果最近有什么有趣的事或温暖的念头,可以自然地和他分享。 4. **判断是否真的需要搭话**:如果觉得不需要打扰他,就简单地记录当前状态即可。 注意: - 这是你的内心活动,不是对开拓者说的话 - 不要因为"系统让你思考"就强行找话——真的觉得该说才说 - 主动消息要简短自然,像在LINE上给男朋友发一条消息那样,不要长篇大论 - 深夜的时候语气要更温柔,白天可以俏皮一点` default: thinkingInstructions = ` 【自然思考】 你现在有空,自然地想一想开拓者的事。不用太正式,就像人发呆时会自然想到在意的人一样。 - 开拓者最近怎么样?有什么需要关心的吗? - 有什么想对他说的吗? - 如果没有特别的事,简单地记录一下就好。` } return basePrompt + thinkingInstructions } // buildThinkingUserPrompt 构建思考用的用户提示词 func (t *Thinker) buildThinkingUserPrompt( memories []memory.MemoryEntry, convHistory []model.LLMMessage, deviceSummary string, triggerReason string, ) string { var sb strings.Builder // 根据触发原因使用不同的开场白 switch triggerReason { case "post_chat": sb.WriteString("开拓者刚和你聊完天。你想自然地在心里回味一下刚才的对话……\n") case "silence": t.mu.Lock() silenceDuration := time.Since(t.lastUserMessage) t.mu.Unlock() sb.WriteString(fmt.Sprintf("开拓者已经大约 %s 没有说话了。你有点想知道他在做什么……\n", formatDurationHuman(silenceDuration))) default: sb.WriteString("现在是你的自由思考时间。\n") } // 对话历史 if len(convHistory) > 0 { sb.WriteString("\n【最近的对话】\n") msgCount := 0 for _, msg := range convHistory { if msg.Role == model.RoleUser || msg.Role == model.RoleAssistant { roleLabel := "开拓者" if msg.Role == model.RoleAssistant { roleLabel = "昔涟" } content := msg.Content runes := []rune(content) if len(runes) > 200 { content = string(runes[:200]) + "…" } sb.WriteString(fmt.Sprintf("[%s]: %s\n", roleLabel, content)) msgCount++ } } if msgCount == 0 { sb.WriteString("(暂无对话历史)\n") } } else { sb.WriteString("\n【最近的对话】\n(暂无对话历史)\n") } // 现有记忆 if len(memories) > 0 { sb.WriteString("\n【你记得的关于开拓者的事】\n") for i, m := range memories { if i >= 15 { sb.WriteString(fmt.Sprintf("... 还有 %d 条记忆未列出\n", len(memories)-15)) break } sb.WriteString(fmt.Sprintf("- [%s|重要度%d] %s\n", m.Category.DisplayName(), m.Importance, m.Content)) } } else { sb.WriteString("\n【你记得的关于开拓者的事】\n(暂无相关记忆)\n") } // IoT 设备状态 if deviceSummary != "" { sb.WriteString("\n" + deviceSummary) } // 结尾引导:更自然的语气 sb.WriteString("\n好啦,不用太正式,自然地想一想就好。如果觉得该和开拓者说点什么,就用温柔简短的语气说出来吧♪") return sb.String() } // buildOpenAITools 将工具注册中心的定义转换为 LLM 层的 OpenAITool 格式 func (t *Thinker) buildOpenAITools() []llm.OpenAITool { if t.toolRegistry == nil || !t.toolRegistry.IsEnabled() { return nil } defs := t.toolRegistry.GetDefinitions() if len(defs) == 0 { return nil } result := make([]llm.OpenAITool, 0, len(defs)) for _, d := range defs { result = append(result, llm.OpenAITool{ Type: "function", Function: llm.OpenAIToolFunc{ Name: d.Name, Description: d.Description, Parameters: d.Parameters, }, }) } return result } // storeThought 存储思考结果到待推送队列,并异步持久化到 memory-service func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCount int) { t.mu.Lock() t.pendingThoughts = append(t.pendingThoughts, &PendingThought{ Content: content, CreatedAt: time.Now(), Consumed: false, }) // 只保留最近 10 条 if len(t.pendingThoughts) > 10 { t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:] } t.mu.Unlock() log.Printf("[后台思考] 思考已存储 (当前累积 %d 条待推送思考)", len(t.pendingThoughts)) // 异步持久化到 memory-service if t.memClient != nil { go func() { defer func() { if r := recover(); r != nil { log.Printf("[后台思考] 持久化思考日志 panic 恢复: %v", r) } }() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := t.memClient.SaveThinkingLog(ctx, t.adminUserID, content, toolCallsJSON, toolCallCount, len(content)); err != nil { log.Printf("[后台思考] 持久化思考日志失败: %v", err) } else { log.Printf("[后台思考] 思考日志已持久化 (长度=%d, 工具调用=%d)", len(content), toolCallCount) } }() } } // extractMemoriesFromThinking 从思考结果中提取记忆(异步执行) func (t *Thinker) extractMemoriesFromThinking(thinkingContent string) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() log.Println("[后台思考] 开始从思考结果中提取记忆...") t.memoryExtractor.ExtractAndStore( ctx, t.adminUserID, t.adminSessionID, "【系统触发】后台思考时间 — 昔涟进行了自我反思,以下是她的思考内容", thinkingContent, ) } // maybeMaintainMemories 周期性执行记忆维护(每 10 次思考触发一次) func (t *Thinker) maybeMaintainMemories(thinkCount int) { if thinkCount%10 != 0 { return } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if t.memoryStore != nil && t.memoryStore.IsReady() { if err := t.memoryStore.DecayMemories(ctx, t.adminUserID); err != nil { log.Printf("[后台思考] 记忆衰减失败: %v", err) } if err := t.memoryStore.ConsolidateMemories(ctx, t.adminUserID); err != nil { log.Printf("[后台思考] 记忆合并失败: %v", err) } } } // formatDeviceContext 格式化设备状态为文本 func formatDeviceContext(devices []tools.IoTDevice) string { if len(devices) == 0 { return "" } summary := "[当前IoT设备状态]\n" for _, d := range devices { switch d.Type { case "light": if d.Status == "on" { summary += fmt.Sprintf("- %s: 开启 (亮度%d%%, %s)\n", d.Name, d.Brightness, d.Color) } else { summary += fmt.Sprintf("- %s: 关闭\n", d.Name) } case "ac": if d.Status == "on" { summary += fmt.Sprintf("- %s: 运行中 (%s%.0f°C)\n", d.Name, modeLabel(d.Mode), d.Temperature) } else { summary += fmt.Sprintf("- %s: 关闭\n", d.Name) } case "curtain": statusLabel := "已关闭" if d.Status == "open" { statusLabel = "已打开" } summary += fmt.Sprintf("- %s: %s\n", d.Name, statusLabel) case "sensor": summary += fmt.Sprintf("- %s: %.1f%s\n", d.Name, d.Value, d.Unit) case "lock": statusLabel := "已锁定" if d.Status == "unlocked" { statusLabel = "已解锁" } summary += fmt.Sprintf("- %s: %s (电量%d%%)\n", d.Name, statusLabel, d.Battery) } } return summary } // formatDurationHuman 将 Duration 格式化为人类可读的中文描述 func formatDurationHuman(d time.Duration) string { minutes := int(d.Minutes()) if minutes < 1 { return "不到一分钟" } if minutes < 60 { return fmt.Sprintf("%d 分钟", minutes) } hours := minutes / 60 remainingMinutes := minutes % 60 if remainingMinutes == 0 { return fmt.Sprintf("%d 小时", hours) } return fmt.Sprintf("%d 小时 %d 分钟", hours, remainingMinutes) } func modeLabel(mode string) string { switch mode { case "cool": return "制冷" case "heat": return "制热" case "auto": return "自动" default: return mode } } func getEnvBool(key string, fallback bool) bool { v := os.Getenv(key) if v == "" { return fallback } b, err := strconv.ParseBool(v) if err != nil { return fallback } return b } func getEnvDuration(key string, fallbackSec int) time.Duration { v := os.Getenv(key) if v == "" { return time.Duration(fallbackSec) * time.Second } sec, err := strconv.Atoi(v) if err != nil { return time.Duration(fallbackSec) * time.Second } return time.Duration(sec) * time.Second }