Files
AskaEth eb1828ee7a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留
- 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
2026-10-04 13:30:04 +08:00

115 lines
3.6 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 audit
import (
"context"
"errors"
"fmt"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// ErrQueryEmpty 在 ContextAuditor.Keyword 的 query 为空或全为空白时返回。用 errors.Is
// 判断它。
var ErrQueryEmpty = errors.New("audit: query is empty")
// contextRadius 是关键词命中处两侧各纳入多少条消息作为上下行窗口(上下各一条)。
const contextRadius = 1
// Located 描述子代理上下文中的单次关键词命中。
type Located struct {
// Line 是命中消息在完整上下文中的 1 基下标。
Line int
// Surrounding 是以命中消息为中心的窗口:紧邻其前的那条消息、命中消息本身,
// 以及紧随其后的一条消息,在上下文边界处截断。它始终包含命中消息。
Surrounding []model.Message
}
// ContextAuditor 提供对某个子代理上下文的只读、可追溯访问。它在构造时快照消息列表,
// 因此调用方此后的修改不会改变审计器所报告的内容。它是值类型,不持有存储或策略;由
// 编排层决定要检视哪段上下文。
type ContextAuditor struct {
messages []model.Message
}
// NewContextAuditor 快照 messages,使调用方后续的修改不影响审计器。允许传入 nil,
// 此时得到一个空上下文。
func NewContextAuditor(messages []model.Message) ContextAuditor {
cp := make([]model.Message, len(messages))
copy(cp, messages)
return ContextAuditor{messages: cp}
}
// Full 返回整段上下文的防御性拷贝。
func (a ContextAuditor) Full() []model.Message {
cp := make([]model.Message, len(a.messages))
copy(cp, a.messages)
return cp
}
// Tail 按原顺序返回上下文最后 n 条消息。
//
// n <= 0 得到空但非 nil 的切片;n >= len 返回整段上下文的拷贝。结果是防御性拷贝。
func (a ContextAuditor) Tail(n int) []model.Message {
if n <= 0 {
return []model.Message{}
}
if n >= len(a.messages) {
return a.Full()
}
start := len(a.messages) - n
out := make([]model.Message, n)
copy(out, a.messages[start:])
return out
}
// Keyword 定位首个文本包含 query 的消息,对消息的文本分段做大小写不敏感的子串匹配
// (图片载荷被忽略)。它返回该消息的 1 基下标与一个上下行窗口;无命中时返回
// nil, nil。
//
// query 为空时返回 ErrQueryEmpty,ctx 被取消时包装 ctx.Err()。只报告最早的命中。
func (a ContextAuditor) Keyword(ctx context.Context, query string) (*Located, error) {
if strings.TrimSpace(query) == "" {
return nil, fmt.Errorf("%w: query is blank", ErrQueryEmpty)
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("audit: keyword: %w", err)
}
q := strings.ToLower(query)
for i, m := range a.messages {
if strings.Contains(strings.ToLower(messageText(m)), q) {
return &Located{Line: i + 1, Surrounding: a.window(i)}, nil
}
}
return nil, nil
}
// window 返回以 index i 为中心、至多 2*contextRadius+1 条消息,并在上下文边界处截断。
func (a ContextAuditor) window(i int) []model.Message {
lo := i - contextRadius
hi := i + contextRadius
if lo < 0 {
lo = 0
}
if hi >= len(a.messages) {
hi = len(a.messages) - 1
}
if lo > hi {
return nil
}
out := make([]model.Message, 0, hi-lo+1)
out = append(out, a.messages[lo:hi+1]...)
return out
}
// messageText 返回一条消息各文本分段拼接后的文本。
func messageText(m model.Message) string {
var b strings.Builder
for _, c := range m.Content {
if c.Type == model.ContentTypeText && c.Text != "" {
b.WriteString(c.Text)
}
}
return b.String()
}