docs(comments): 注释中文化 · internal/services/channelbridge(纯替换)
- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避) - 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
This commit is contained in:
1 parent
61cf80d5ee
commit
093f38d986
28 files changed
+1089
-1089
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user