docs(cyrene): cmd/cyrene 存量英文注释中文化(22 个非测试文件,仅注释)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 四组并行完成:main/wire*.go、wire_capacity|channels|runtime、channels|persona_*|emotion|observability、
  voice|plugins|stream|skills|vision|subagent_tool|extractor|mcp|sessionread_tool|terminal_tool。
- 机械校验(git diff -U0 去 // 行与空行)仅剩 wire_frontend.go:84 一处行尾注释:代码前缀
  「\ta.gw = gw 」经 cat -A 与 HEAD 逐字节比对一致(allow 清单已声明)。
- 验收:隔离 worktree(HEAD + 本批 patch)go build/vet 绿、全树 58 包 ok、
  linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净。
- 备注:整树两次红均为负载敏感 flake(backthink / orchestration 的 timeout 类用例单独 -count=20 全过)。
This commit is contained in:
AskaEth committed 2026-10-04 16:58:06 +08:00
1 parent 5c2189efb0
commit 52f08dc343
22 files changed
+455 -586

No files matched your search

+55 -72
View File
@@ -1,18 +1,15 @@
package main
// This file builds the four built-in IM channel adapters from the runtime-editable
// configuration. The effective source is channels.json when it is present and
// valid (design/04 §3.1); otherwise it falls back to the env channel block
// (CHANNELS_JSON multi-account + the single-account CHANNEL_<NAME>_* vars), which
// stays the final fallback so a bare install boots without any config file.
// 本文件从运行时可变配置构建四个内置 IM 渠道适配器。有效来源是 channels.json
// (存在且合法时优先,design/04 §3.1);否则回退到 env 渠道块(CHANNELS_JSON
// 多账号 + 单账号 CHANNEL_<NAME>_* 变量),后者是最终兜底,使裸装环境在没有任何
// 配置文件时也能启动。
//
// Each account becomes its own adapter instance. A single-account env adapter is
// named after the bare platform (e.g. "kook"); a multi-account entry is named
// "kook:confID" and wrapped in a namedAdapter that surfaces its allow/deny lists
// through Config() for the channel bridge's black-or-white gate (design/16 §4.6
// §5-6). The channelFactory lets the bridge hot-replace a platform's adapters when
// the panel saves a new config (Bridge.Reload), so a token/address/rate change
// takes effect without a restart.
// 每个账号各自成为一个适配器实例。单账号 env 适配器以裸平台名命名(如 "kook");
// 多账号条目命名为 "kook:confID",并包在 namedAdapter 里,通过 Config() 暴露其
// allow/deny 名单,供渠道桥的黑/白名单闸门使用(design/16 §4.6 §5-6)。
// channelFactory 让桥在面板保存新配置时热替换某平台的适配器(Bridge.Reload),
// 因此 token/地址/速率变更无需重启即可生效。
import (
"fmt"
@@ -32,11 +29,9 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/weixin_ilink"
)
// buildChannelAdapters constructs every channel adapter from the effective
// account config (channels.json → env fallback) and returns the successfully
// built subset. Adapter construction never dials the network; actual connection
// happens in the channel bridge. f holds the path/fallback/logger so boot and the
// reload factory read the same source.
// buildChannelAdapters 从有效账号配置(channels.json → env 兜底)构建全部渠道
// 适配器,并返回构建成功的子集。适配器构建从不发起网络连接,真正的连接发生在渠道桥
// 里。f 持有路径/兜底/logger,使启动路径与热重载工厂读取同一来源。
func buildChannelAdapters(f *channelFactory) []adapter.PlatformAdapter {
accounts, warn := runtimeconfig.LoadChannels(f.path, f.fallback)
if warn != nil {
@@ -49,17 +44,16 @@ func buildChannelAdapters(f *channelFactory) []adapter.PlatformAdapter {
return out
}
// channelFactory is the channelbridge.PlatformFactory wired into the bridge. It
// rebuilds a platform's adapters from the CURRENT effective config (file → env),
// so a panel save followed by Bridge.Reload(platform) picks the new config up
// without a restart.
// channelFactory 是接入渠道桥的 channelbridge.PlatformFactory。它从**当前**有效
// 配置(文件 → env)重建某平台的适配器,因此面板保存后调用 Bridge.Reload(platform)
// 即可让新配置生效,无需重启。
type channelFactory struct {
path string
fallback map[string][]config.ChannelAccount
log *slog.Logger
}
// Build returns the adapters for a platform from its effective account config.
// Build 返回某平台按其有效账号配置构建出的适配器。
func (f *channelFactory) Build(platform string) ([]adapter.PlatformAdapter, error) {
accounts, warn := runtimeconfig.LoadChannels(f.path, f.fallback)
if warn != nil {
@@ -68,12 +62,11 @@ func (f *channelFactory) Build(platform string) ([]adapter.PlatformAdapter, erro
return buildPlatformAdapters(platform, accounts[platform], f.log), nil
}
// Persona returns the role bound to one channel account of a platform (design/22
// §2 方案 b). It reads the same effective config Build does — channels.json wins,
// env is the fallback — so the panel's save plus the bridge's hot reload stay the
// single update path, and it is called once per adapter connect rather than per
// message. An unknown platform/account, or an account without a personaId, returns
// "" (no channel-level binding: the session binding or the global default applies).
// Persona 返回某平台某个渠道账号所绑定的角色(design/22 §2 方案 b)。它读取与
// Build 相同的有效配置 —— channels.json 优先、env 兜底 —— 因此「面板保存 + 桥
// 热重载」始终是唯一的更新路径;它每次适配器连接调用一次,而不是每条消息调用一次。
// 平台/账号未知,或账号未配 personaId 时返回 ""(无渠道级绑定:改用会话绑定或全局
// 默认)。
func (f *channelFactory) Persona(platform, accountID string) string {
accounts, warn := runtimeconfig.LoadChannels(f.path, f.fallback)
if warn != nil {
@@ -87,9 +80,8 @@ func (f *channelFactory) Persona(platform, accountID string) string {
return ""
}
// buildPlatformAdapters builds every account of one platform into an adapter
// instance. A failed account build is skipped with a warn (never fatal), matching
// the "no credentials → warn and continue" contract of the bridge.
// buildPlatformAdapters 把某平台的全部账号各构建成一个适配器实例。某个账号构建
// 失败只记一条 WARN 并跳过(绝不致命),与桥的「没有凭据 → 告警并继续」约定一致。
func buildPlatformAdapters(platform string, accts []config.ChannelAccount, log *slog.Logger) []adapter.PlatformAdapter {
out := make([]adapter.PlatformAdapter, 0, len(accts))
for _, acct := range accts {
@@ -103,33 +95,30 @@ func buildPlatformAdapters(platform string, accts []config.ChannelAccount, log *
return out
}
// channelAccountsFromEnv synthesizes the env fallback: the single-account
// CHANNEL_<NAME>_* adapters plus any CHANNELS_JSON multi-account entries. It is
// the .env fallback passed to runtimeconfig.LoadChannels when channels.json is
// absent or invalid. .env only carries the necessary connection fallback
// (token/base_url/...); business and advanced per-account fields (receive_mode,
// webhook path/verify_token/encrypt_key, allow/deny lists, rate limits) are
// configured in the panel's channel management and live in channels.json, never
// in .env.
// channelAccountsFromEnv 合成 env 兜底:单账号 CHANNEL_<NAME>_* 适配器,加上
// CHANNELS_JSON 里的多账号条目。它是 channels.json 缺失或非法时传给
// runtimeconfig.LoadChannels 的 .env 兜底。.env 只承载必要的连接兜底
// (token/base_url/...);业务与高级的按账号字段(receive_mode、webhook
// path/verify_token/encrypt_key、allow/deny 名单、速率限制)在面板的渠道管理里
// 配置,落在 channels.json,绝不出现在 .env。
//
// A token-requiring platform (kook/telegram/weixin-ilink) is synthesized only
// when its token env var is set; with an unset token there is no account at all,
// so an empty-token placeholder never claims the bare platform name. qq-obv11 and
// webhook are always present (they do not need a token; the bridge's configured()
// gate decides whether to dial) so the channel list stays stable.
// 需要 token 的平台(kook/telegram/weixin-ilink)只有在其 token 环境变量已设置时
// 才合成;token 未设置就完全不存在该账号,因此空 token 的占位符绝不会占用裸平台名。
// qq-obv11 与 webhook 始终存在(它们不需要 token;由桥的 configured() 闸门决定是否
// 拨号),所以渠道列表保持稳定。
func channelAccountsFromEnv() map[string][]config.ChannelAccount {
out := map[string][]config.ChannelAccount{}
// KOOK: only the necessary connection fields. receive_mode / webhook_path /
// webhook_verify_token / webhook_encrypt_key are business·advanced fields that
// go to channels.json via the panel — .env does not fall back on them.
// KOOK:只放必要的连接字段。receive_mode / webhook_path /
// webhook_verify_token / webhook_encrypt_key 属于业务·高级字段,经由面板写入
// channels.json —— .env 不再对它们做兜底。
if tok := os.Getenv("CHANNEL_KOOK_TOKEN"); tok != "" {
out["kook"] = append(out["kook"], config.ChannelAccount{Config: map[string]any{
"bot_token": tok,
"base_url": os.Getenv("CHANNEL_KOOK_BASE_URL"),
}})
}
// Telegram: token (necessary) + optional webhook_url (connection). Business
// tuning goes to channels.json.
// Telegram:token(必要)+ 可选的 webhook_url(连接)。业务调优写入
// channels.json。
if tok := os.Getenv("CHANNEL_TELEGRAM_BOT_TOKEN"); tok != "" {
out["telegram"] = append(out["telegram"], config.ChannelAccount{Config: map[string]any{
"bot_token": tok,
@@ -175,10 +164,9 @@ func channelAccountsFromEnv() map[string][]config.ChannelAccount {
return out
}
// namedAdapter wraps a platform adapter so a multi-account instance gets a unique
// name (platform:confID) and surfaces its allow/deny lists through Config(), which
// the channel bridge reads to enforce the platform's black-or-white lists per
// account (design/16 §4.6 §5-6). Everything else delegates to the inner adapter.
// namedAdapter 包装平台适配器,使多账号实例获得唯一名字(platform:confID),并
// 通过 Config() 暴露其 allow/deny 名单 —— 渠道桥读取它来按账号执行平台的黑/白名单
// (design/16 §4.6 §5-6)。其余方法全部委派给内层适配器。
type namedAdapter struct {
adapter.PlatformAdapter
name string
@@ -186,9 +174,8 @@ type namedAdapter struct {
denyGroups []string
allowUsers []string
denyUsers []string
// flow limits (design/12 §6 策略三/四). They are always surfaced through
// Config() so the bridge can build a per-account ingress gate; the bridge maps
// the zero default to the protective values and a negative value to "off".
// 流控限制(design/12 §6 策略三/四)。它们始终经 Config() 暴露,使桥能构建按账号
// 的入口闸门;桥把零值默认映射为保护性取值,把负值映射为「关闭」。
groupMsgPerMin int
msgPerMin int
maxMessages int
@@ -212,9 +199,8 @@ func (n *namedAdapter) Config() map[string]any {
return out
}
// InboundHandler forwards to the inner adapter when it exposes an inbound HTTP
// callback; the bridge type-asserts the wrapper and mounts the handler at
// WebhookPath. It returns nil for a non-webhook inner adapter.
// InboundHandler 在内层适配器暴露入站 HTTP 回调时转发给它;桥会对包装器做类型
// 断言,并把该 handler 挂到 WebhookPath。内层适配器不是 webhook 时返回 nil。
func (n *namedAdapter) InboundHandler() http.Handler {
if inner, ok := n.PlatformAdapter.(interface{ InboundHandler() http.Handler }); ok {
return inner.InboundHandler()
@@ -222,8 +208,8 @@ func (n *namedAdapter) InboundHandler() http.Handler {
return nil
}
// WebhookPath forwards the inner adapter's webhook route, returning "" when the
// inner adapter does not expose one (so the bridge skips it).
// WebhookPath 转发内层适配器的 webhook 路由;内层不暴露该路由时返回 ""(桥据此
// 跳过它)。
func (n *namedAdapter) WebhookPath() string {
if inner, ok := n.PlatformAdapter.(interface{ WebhookPath() string }); ok {
return inner.WebhookPath()
@@ -231,9 +217,8 @@ func (n *namedAdapter) WebhookPath() string {
return ""
}
// buildAccountAdapter builds one account's adapter for a platform, decoding the
// account's generic Config map into the platform's typed config and wrapping it so
// the bridge sees a distinct account name and its allow/deny lists.
// buildAccountAdapter 为某平台构建单个账号的适配器:把账号的通用 Config map 解码
// 成该平台的强类型配置,并包装起来,使桥看到唯一的账号名及其 allow/deny 名单。
func buildAccountAdapter(platform string, acct config.ChannelAccount, log *slog.Logger) (adapter.PlatformAdapter, error) {
name := platform
if acct.ID != "" {
@@ -256,11 +241,10 @@ func buildAccountAdapter(platform string, acct config.ChannelAccount, log *slog.
if err := adapter.DecodeConfig(acct.Config, &cfg); err != nil {
return nil, fmt.Errorf("decode weixin-ilink config: %w", err)
}
// A weixin-ilink account may carry an empty bot_token: it is a pending
// scan-login account the panel drives via the login-qr/login-status admin
// endpoints (design/08 §4). The bridge's configured() gate skips it until
// the scan confirms and the token is persisted, so an empty token here is
// legitimate — build the adapter (disconnected) rather than dropping it.
// weixin-ilink 账号允许 bot_token 为空:这是一条待扫码登录的账号,面板通过
// login-qr/login-status 管理端点驱动它(design/08 §4)。桥的 configured() 闸门
// 会跳过它,直到扫码确认且 token 落库,因此这里空 token 是合法的 —— 应当构建
// 该适配器(未连接),而不是丢弃它。
w, err := weixin_ilink.New(cfg, nil, logger.WithSource(log, "plugins/channels/weixin_ilink"))
if err != nil {
return nil, fmt.Errorf("build weixin-ilink: %w", err)
@@ -303,7 +287,7 @@ func buildAccountAdapter(platform string, acct config.ChannelAccount, log *slog.
}, nil
}
// barePlatform returns the platform name for a possibly-suffixed "platform:confID".
// barePlatform 返回可能带后缀的 "platform:confID" 的平台名。
func barePlatform(name string) string {
for i := 0; i < len(name); i++ {
if name[i] == ':' {
@@ -313,8 +297,7 @@ func barePlatform(name string) string {
return name
}
// envString returns the value of key, or fallback when the variable is unset or
// blank.
// envString 返回 key 的值;环境变量未设置或为空白时返回 fallback。
func envString(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v