① light.go 的 2 处 "light judge: " + d.Reason 与 engage.go 同形(model reason → 返回值 → coalesce_prepare 的 Debug)。把上一轮的 engageReason 提成包内共用的 judgeReason(judge_reason.go,RedactTextLimited + strings.Fields,60 rune), engage.go 7 处 + light.go 2 处 + notice.go 的 "notice review failed: "+err.Error() 统一走它。控制流零改动:只换「进去的那份文本」,判定条件与返回值拼接结构未动。 ② MCP 的 Logger 面先探针后修:go-sdk v1.8.0 的 keepalive 会把对端 JSON-RPC error 原样打进日志(实测泄漏,见 sdklog_probe_test.go 的输出),tools/call 路径不打日志。 新增 sdklog.go:redactingLogger 交给 SDK,wire 错误(链上有 *jsonrpc.Error)只留 code + message_bytes(复用 upstreamError),本地错误保留原因,消息/属性压平+打码+300 rune。 ③ errortoken.go 的口径注释纠正:从「包外不得调用」改为按字段类别判定(协议枚举可守、 自由文本默认不带),并把 cdp.go 这条 M51 用法作为例子写进去。
125 lines
5.3 KiB
Go
125 lines
5.3 KiB
Go
package mcp
|
||
|
||
// 本文件是「交给官方 SDK 的 Logger」的唯一收敛点。
|
||
//
|
||
// 为什么需要它(探针证据见 sdklog_probe_test.go):官方 SDK v1.8.0 的客户端侧会自己
|
||
// 打日志,其中 keepalive 那条把**对端错误原样**交给 logger:
|
||
//
|
||
// logger.Error("keepalive ping failed; closing session", "error", err, …)
|
||
//
|
||
// 而 err 是 SDK 自己的包装(`calling "ping": <对端 error.message>`),**不是**本包
|
||
// upstreamError 的产物 —— 于是第三方 server 的自由文本可以借 SDK 的手进我们的日志,
|
||
// 绕开 client.go 里那条「默认不带自由文本」的出口。探针实测:未加本文件时日志里出现
|
||
// 了对端回显的整句话。
|
||
//
|
||
// 收敛口径(与 client.go 同一刻画,不做第二套):
|
||
// - **线上错误**(链上有 *jsonrpc.Error):自由文本一律不带,只留
|
||
// `upstreamError` 渲染的 code + message_bytes;
|
||
// - **本地错误**(context deadline / canceled、传输错误):文本是代码自己产生的,
|
||
// 保留并以 maxSDKLogRunes 截断 —— 运维靠它判断「为什么 session 被关掉」;
|
||
// - 消息本身与所有字符串属性同样过 trace.RedactTextLimited(抹凭据形状 + 截断)。
|
||
//
|
||
// 为什么不是「一律 RedactText」:RedactText 只抹**凭据形状**,不删「她的话」——
|
||
// 对端把用户正文回显进 error.message 时,凭据清洗一个字符都不会动它。要挡住这一类,
|
||
// 判据必须是「这条文本来自线上」而不是「它像不像凭据」。
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log/slog"
|
||
"strings"
|
||
|
||
sdkjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||
)
|
||
|
||
// maxSDKLogRunes 是 SDK 日志行(消息与字符串属性)允许出镜的 rune 上限。
|
||
//
|
||
// 取 300 的理由:这些行是「方法名 + 原因」的合成串(`calling "ping": …`),
|
||
// 300 rune 容得下任何真实诊断,同时挡住 MiB 级对象把日志行撑爆;与网关日志页
|
||
// 的 maxLogMsgRunes(4096,整页一行要留全文)刻意不同 —— 这里的行是我们自己
|
||
// 产出的诊断,不需要全文,只需要「哪一步失败、为什么」。
|
||
const maxSDKLogRunes = 300
|
||
|
||
// redactingLogger 返回交给 SDK 的日志器:同一底层 handler,包一层日志面收敛。
|
||
// 它不改变级别与输出格式(Enabled 原样委托),因此宿主注入的 Logger 行为不变。
|
||
func redactingLogger(l *slog.Logger) *slog.Logger {
|
||
return slog.New(&redactingHandler{inner: l.Handler()})
|
||
}
|
||
|
||
// redactingHandler 是 redactingLogger 的 handler 形态。
|
||
type redactingHandler struct {
|
||
inner slog.Handler
|
||
}
|
||
|
||
// Enabled 原样委托:收敛发生在写出时,不影响任何级别判定。
|
||
func (h *redactingHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||
return h.inner.Enabled(ctx, level)
|
||
}
|
||
|
||
// Handle 先收敛一条记录(消息 + 全部属性),再交给底层 handler。
|
||
func (h *redactingHandler) Handle(ctx context.Context, r slog.Record) error {
|
||
out := slog.NewRecord(r.Time, r.Level, scrubSDKText(r.Message), r.PC)
|
||
r.Attrs(func(a slog.Attr) bool {
|
||
out.AddAttrs(scrubSDKAttr(a))
|
||
return true
|
||
})
|
||
return h.inner.Handle(ctx, out)
|
||
}
|
||
|
||
// WithAttrs 让预置属性(logger.With(...))同样过收敛 —— 否则「先 With 再打」的
|
||
// 记录会绕过本文件。
|
||
func (h *redactingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||
scrubbed := make([]slog.Attr, len(attrs))
|
||
for i, a := range attrs {
|
||
scrubbed[i] = scrubSDKAttr(a)
|
||
}
|
||
return &redactingHandler{inner: h.inner.WithAttrs(scrubbed)}
|
||
}
|
||
|
||
// WithGroup 透传分组,组内属性仍由 Handle 逐条收敛。
|
||
func (h *redactingHandler) WithGroup(name string) slog.Handler {
|
||
return &redactingHandler{inner: h.inner.WithGroup(name)}
|
||
}
|
||
|
||
// scrubSDKText 是一条自由文本进入日志前唯一处理:压平空白(一个换行就能在按行聚合
|
||
// 的日志里伪造出「另一条记录」)+ 凭据形状清洗 + 截断。
|
||
func scrubSDKText(s string) string {
|
||
return trace.RedactTextLimited(strings.Join(strings.Fields(s), " "), maxSDKLogRunes)
|
||
}
|
||
|
||
// scrubSDKAttr 收敛一条属性:字符串与 error 是自由文本面,分组递归,其余(数字、
|
||
// 时间、布尔)原样保留 —— 它们是体积与状态,没有内容。
|
||
func scrubSDKAttr(a slog.Attr) slog.Attr {
|
||
switch a.Value.Kind() {
|
||
case slog.KindString:
|
||
a.Value = slog.StringValue(scrubSDKText(a.Value.String()))
|
||
case slog.KindAny:
|
||
if err, ok := a.Value.Any().(error); ok && err != nil {
|
||
a.Value = slog.StringValue(scrubSDKError(err))
|
||
}
|
||
case slog.KindGroup:
|
||
group := a.Value.Group()
|
||
scrubbed := make([]slog.Attr, len(group))
|
||
for i, item := range group {
|
||
scrubbed[i] = scrubSDKAttr(item)
|
||
}
|
||
a.Value = slog.GroupValue(scrubbed...)
|
||
}
|
||
return a
|
||
}
|
||
|
||
// scrubSDKError 区分两类错误来源(口径见文件头):
|
||
// - 链上有 *jsonrpc.Error ⇒ 文本来自对端,走 upstreamError 的骨架(code +
|
||
// message_bytes),一个字节的自由文本都不留;
|
||
// - 否则是本地产生的错误(context deadline / canceled、传输层、SDK 自身),
|
||
// 保留原因(运维要靠它判断失败性质),只做凭据清洗与截断。
|
||
func scrubSDKError(err error) string {
|
||
var wire *sdkjsonrpc.Error
|
||
if errors.As(err, &wire) {
|
||
return upstreamError(err).Error()
|
||
}
|
||
return scrubSDKText(err.Error())
|
||
}
|