docs(comments): 注释中文化 · internal/services/channelbridge(纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避)
- 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
This commit is contained in:
AskaEth committed 2026-10-04 13:33:06 +08:00
1 parent 61cf80d5ee
commit 093f38d986
28 files changed
+1089 -1089

No files matched your search

+41 -41
View File
@@ -8,42 +8,42 @@ import (
"unicode/utf8"
)
// This file holds the chat-reply sanitizer used before a reply is delivered to an
// IM channel adapter (design/17 §2 + the role-play convention: the Web client
// renders <action>/<narration> as muted italic; IM channels must receive plain chat
// text only, with the action/monologue markup removed).
// 本文件承载聊天回复清洗器,用于在回复投递给
// IM 渠道适配器之前(design/17 §2 + 角色扮演约定:Web 客户端
// 把 <action>/<narration> 渲染为暗色斜体;IM 渠道只能收到纯聊天
// 文本,动作/独白标记必须移除)。
var (
// pairedRoleplayRe matches a complete <action>...</action> / <narration>...</narration>
// block including its inner text, so an action/monologue is dropped wholesale before
// IM delivery. Non-greedy and case-insensitive to tolerate a model that emits <Action>.
// pairedRoleplayRe 匹配完整的 <action>...</action> / <narration>...</narration>
// 块(含内部文本),因此动作/独白会在投递给
// IM 前被整体丢弃。非贪婪且不区分大小写,以容忍模型写出 <Action>。
pairedRoleplayRe = regexp.MustCompile(`(?is)<(action|narration)\b[^>]*?>.*?</(action|narration)\s*>`)
// strayRoleplayRe matches any remaining single opening/closing role-play tag (for a
// malformed or unpaired emission).
// strayRoleplayRe 匹配任何残留的单个角色扮演开/闭标签
// (用于格式错误或未配对的情况)。
strayRoleplayRe = regexp.MustCompile(`(?is)</?(action|narration)\b[^>]*?>`)
// paragraphRe matches <p>...</p> / <p/> paragraph markers, keeping the inner text.
// paragraphRe 匹配 <p>...</p> / <p/> 段落标记,保留内部文本。
paragraphRe = regexp.MustCompile(`(?is)</?p\b[^>]*?>`)
// blankLinesRe collapses runs of blank lines left behind after stripping.
// blankLinesRe 把剥离后残留的连续空行折叠掉。
blankLinesRe = regexp.MustCompile(`\n\s*\n+`)
// starRoleplayRe matches a full-width star-wrapped block *...* — the drift
// form when the model forgets the <action>/<narration> XML tags. The body is
// captured so a non-role-play full-width emphasis can be left alone.
// starRoleplayRe 匹配全角星号包裹块 *...* ——这是模型忘记
// <action>/<narration> XML 标签时的漂移形式。正文被
// 捕获,以便非角色扮演的全角强调可以原样保留。
starRoleplayRe = regexp.MustCompile(`(?s)*([\s\S]*?)*`)
// halfStarRoleplayRe matches a half-width star block that clearly carries a
// role-play keyword (动作/旁白/内心/神态/轻声), so *动作:弯起眼睛* is stripped
// while plain markdown *emphasis* is left untouched.
// halfStarRoleplayRe 匹配明确带有角色扮演
// 关键词(动作/旁白/内心/神态/轻声)的半角星号块,因此 *动作:弯起眼睛* 会被剥离,
// 而普通的 markdown *emphasis* 保持不动。
halfStarRoleplayRe = regexp.MustCompile(`(?s)\*((?:动作|旁白|内心|神态|轻声)\s*[::]?[\s\S]*?)\*`)
)
// shortRoleplayRunes caps how long a keyword-less full-width star block may be
// and still be treated as a role-play remark (design §9.5: conservative — a long
// keyword-less *...* paragraph is left as text rather than dropped wholesale).
// shortRoleplayRunes 限定无关键词的全角星号块最长可以是多少
// 仍算作角色扮演批注(design §9.5:保守起见——长的
// 无关键词 *...* 段落保留为文本,而不整体丢弃)。
const shortRoleplayRunes = 40
// stripNarration returns the plain chat text of a reply with role-play/action/
// narration markup removed, paragraph markers flattened and blank-line runs
// collapsed. It never raises or panics on malformed input; a reply that was only an
// action/monologue becomes an empty string (caller may then skip the send).
// stripNarration 返回去掉角色扮演/动作/
// 旁白标记、段落标记被抹平、连续空行被折叠后的
// 纯聊天文本。它对畸形输入绝不报错或 panic;纯动作/独白的
// 回复会变成空字符串(调用方可据此跳过发送)。
func stripNarration(text string) string {
if strings.TrimSpace(text) == "" {
return ""
@@ -62,10 +62,10 @@ func stripNarration(text string) string {
return strings.TrimSpace(s)
}
// isStarRoleplay reports whether a full-width star block *...* holds a role-play
// remark: it carries a role-play keyword, or it is a short stand-alone fragment
// (no newline, no long body) — the drift shape *弯起眼睛*. A long keyword-less
// paragraph wrapped in stars is kept so a real message is not dropped.
// isStarRoleplay 报告全角星号块 *...* 是否承载角色扮演
// 批注:它带角色扮演关键词,或者是短的独立片段
// (无换行、无长正文)——即漂移形态 *弯起眼睛*。长的无关键词
// 星号包裹段落会保留,以免丢掉真实消息。
func isStarRoleplay(inner string) bool {
t := strings.TrimSpace(inner)
if t == "" {
@@ -77,7 +77,7 @@ func isStarRoleplay(inner string) bool {
return !strings.Contains(t, "\n") && utf8.RuneCountInString(t) <= shortRoleplayRunes
}
// hasRoleplayPrefix reports whether the block starts with a role-play keyword.
// hasRoleplayPrefix 报告该块是否以角色扮演关键词开头。
func hasRoleplayPrefix(t string) bool {
for _, p := range []string{"动作", "旁白", "内心", "神态", "轻声"} {
if strings.HasPrefix(t, p) {
@@ -87,15 +87,15 @@ func hasRoleplayPrefix(t string) bool {
return false
}
// paragraphBlockRe matches an entire <p>...</p> block, capturing its inner text
// (design: each <p> is one chat bubble; the Web client renders each as its own
// bubble and the IM adapters deliver each as its own message).
// paragraphBlockRe 匹配整个 <p>...</p> 块并捕获其内部文本
// (设计:每个 <p> 即一个聊天气泡;Web 客户端各自渲染为一个
// 气泡,IM 适配器各自投递为一条消息)。
var paragraphBlockRe = regexp.MustCompile(`(?is)<p\b[^>]*?>(.*?)</p\s*>`)
// splitBubbles turns a reply into the list of chat bubbles it carries, in order.
// Each <p>...</p> block is one bubble, with role-play/action/narration stripped from
// its text; empty bubbles are dropped. A reply with no <p> markers is treated as a
// single bubble (newlines preserved). A pure action/monologue yields an empty slice.
// splitBubbles 把一条回复按顺序拆成它承载的聊天气泡列表。
// 每个 <p>...</p> 块为一个气泡,其文本已剥离角色扮演/动作/
// 旁白;空气泡被丢弃。没有 <p> 标记的回复按
// 单个气泡处理(保留换行)。纯动作/独白得到空切片。
func splitBubbles(text string) []string {
if !strings.Contains(strings.ToLower(text), "<p") {
if c := stripNarration(text); c != "" {
@@ -110,7 +110,7 @@ func splitBubbles(text string) []string {
}
}
if len(out) == 0 {
// <p> markers present but no well-formed block — fall back to whole text.
// 有 <p> 标记但没有格式良好的块——回退为整段文本。
if c := stripNarration(text); c != "" {
out = append(out, c)
}
@@ -118,10 +118,10 @@ func splitBubbles(text string) []string {
return out
}
// bubbleGap returns a randomized 0.5s..2s delay used between IM outbound bubbles so a
// burst of paragraphs doesn't flood the channel (design/17 §2). The Web frontend is
// not affected — it renders all bubbles from the persisted stream without any delay.
// It is only consulted when a reply carries more than one bubble.
// bubbleGap 返回 IM 出站气泡之间使用的 0.5s..2s 随机延时,
// 使连发段落不会刷屏(design/17 §2)。Web 前端
// 不受影响——它从已持久化的流渲染所有气泡,没有任何延时。
// 仅当一条回复承载多于一个气泡时才会用到它。
func bubbleGap() time.Duration {
return 500*time.Millisecond + time.Duration(rand.Float64()*1500)*time.Millisecond
}