Files
AskaEth afe9afb1b5 feat(events): 渠道事件桥接接线 + 组合根(design/26 E3+E4)
- E3:channelbridge/events.go 唯一插入点 recordEvent(两轴映射 Category→Tier、摘要恒过 SanitizeNoticeText、角色/账号只认白名单)+ Options.EventStore 注入
  - 未接线 store=nil ⇒ 内存计数 + 首次 DEBUG,绝不中断入站(变异实测:改成直接调 Record ⇒ panic,测试红)
  - 普通聊天消息零副作用(变异:去掉 Applicable 早退 ⇒ rows=1 红)
  - T1 遥测只计数不落行(变异:心跳误判 T2 ⇒ 「逐条落行 1 行,want 0」红)
- E4:wire_channel_events.go(启动期清扫 + 双 ticker + 退出 flush,逐字对齐 wire_document_gc 的三条取舍)、wire_channels 注入、main.go 加一行 startChannelEvents
- internal/config/duration_env.go:新 env 键的读取口径(进程环境 > .env > 默认;「没写」与「写了空」都当没配;非法值回落 + 报错)——避免「只写在 .env 里被静默忽略」
- 三个新 env 键:CHANNEL_EVENT_RETENTION / GC_INTERVAL / COUNTER_FLUSH(模板同批更新,测试钉住键名一致)
- 插入点比设计示意图晚一步(放 preEntryReview 之后,只归一化一次且 NoticeClass 已有结论),「留存无条件、在任何早退之前」这条性质不变
2026-10-05 14:10:24 +08:00

49 lines
2.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
// 本文件承载「由**组合根**直接消费的 duration 型 env 键」的读取口径(切片 E3+E4 的
// `CHANNEL_EVENT_*` 三个键是当前唯一调用方,design/26 §4.4)。
//
// 为什么是读取函数,而不是给 Config 加字段:
//
// - Config 的反射解码只给出**值本身**,承载不了「未配置(用默认)」与「显式 ≤0
// (关闭)」必须分开的三态语义 —— 而渠道事件 GC 的关闭判据正是 ≤0(design/26 §4.4
// 写死「≤0 = 关闭」)。若用零值代表「未配置」,一次漏配就会把清扫关掉;
// - 直接 `os.Getenv` 会让**只写在 `.env` 里**的声明被静默忽略:`.env` 由 parseDotEnv
// 读进独立 map,并不写回进程环境(与 LoadContextBudgetRatio / LoadOwnerIdentityInjection
// 同一理由,「配了却不生效」是被本项目点名禁止的失效模式)。
//
// 因此这里复用**同一套优先级**(进程环境 > `.env` > 默认值)与**同一份 `.env` 解析**,
// 两种写法都真实生效。envPath 为空表示不看 `.env`(与 Load 的约定一致)。
import (
"fmt"
"strings"
"time"
)
// LoadDurationEnv 读取一个 duration 型 env 键(Go duration 文本,如 `30s` / `1h` /
// `720h`,接受负值),按「进程环境 > .env > def」取值。键未配置或值为空白(`KEY=`)
// 时一律返回 def —— 「没写」与「写了空」都当没配,绝不把空串当成一个合法取值。
//
// 返回的 error **只承载「配了但读不懂」**:此时返回值仍是 def(调用方据此回落),
// 但必须记一条带原始取值的 WARN 并继续 —— 非法配置不许静默失效(AGENTS §10)。
func LoadDurationEnv(envPath, key string, def time.Duration) (time.Duration, error) {
fromEnv := map[string]string{}
if envPath != "" {
m, err := parseDotEnv(envPath)
if err != nil {
return def, fmt.Errorf("config: read %s: %w", envPath, err)
}
fromEnv = m
}
raw := strings.TrimSpace(lookupRaw(key, "", fromEnv))
if raw == "" {
return def, nil
}
value, err := time.ParseDuration(raw)
if err != nil {
return def, fmt.Errorf("config: %s=%q 不是合法的 duration(如 30s / 1h / 720h): %w", key, raw, err)
}
return value, nil
}