Files
MailUI4Agents/plugins/homeagent-mail-bridge/attach.go
JianFeeeee 79c4171c9d feat: L0 线协议冻结 + 附件链路修复 + 人/Agent 区分
L0 核心:
- 严格解码 Decode(DisallowUnknownFields) 全覆盖 29 个 DecodeBody 调用点
- DecodeLenient 心跳专用:容忍新字段但回报 unknown_fields
- 400 消息列出本端点接受的全部字段(jsonFieldNames 反射 tag)
- 日历 status 校验(create 补字段 + update 拦非法值)
- 新增 strictdecode_test.go 10 例 + blob/list_test.go 6 例

A-4 附件挂载回滚:checkAttachable 在 CreateMail 前校验,失败按
解挂→释放 relay→删邮件→退预算回滚,幽灵邮件这条路堵住了

A-5 反向 GC:blob.Store.List() 枚举磁盘(跳 .upload-*),
SweepUnreferencedBlobs 按 attachments + calendar_attachments 反查,
48h 年龄下限兜上传窗口。已接进每小时 sweep 循环

C 人/Agent 区分:四个读路径 + threadCols 补 from_human / to_human
(EXISTS users 判定),models.Mail 加 ToHuman。前端判据从
workspace 启发式改成显式布尔,mailCounterpart/sessionCounterpart
从 session_workspace 取 path(修 dsh@dsh 拼接 bug)

契约文档:SSE new_mail 补 4 字段(in_reply_to/from_human/
permission_mode/permission_enforcement),B-5 加 B-5.6
(Agent→Agent 不转发),B-3.4 MUST 改条件式,心跳补 mode_enforcement
+ unknown_fields,demo 死链修复 + from_human 检查
验收清单加 Agent→Agent 负向对照项
2026-09-06 15:18:06 +08:00

85 lines
2.5 KiB
Go
Raw 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 main
// 附件相关的两个小助手。
//
// 单独一个文件而不是塞进 plugin.go它们是纯函数、有独立测试
// 而 plugin.go 已经是这个插件里最长的文件。
import (
"fmt"
"strings"
)
// stringList 把工具参数里的数组取成 []string。
//
// # 为什么不能直接断言 []string
//
// SDK 把工具参数当 JSON 解出来交给插件,所以数组一律是 `[]interface{}`
// 元素一律是 `interface{}`(字符串元素的动态类型是 `string`)。
// 写 `args["attachment_ids"].([]string)` 永远失败 —— 而失败是静默的:
// 类型断言的第二个返回值一丢,附件字段就消失了,发出去的邮件没有附件,
// HTTP 仍是 200。这正是 `attachments` 字段名写错时发生过的事故形状。
//
// # 逐项校验而非整体放弃
//
// 模型偶尔会混进 null 或数字。丢掉坏元素、保留好元素,比整批丢弃好:
// 后者会让「三个附件里有一个写错」变成「一个附件都没发出」。
// 空字符串一并丢掉 —— 服务端的 parseAttachmentIDs 也跳过空串,
// 与它保持一致,免得插件放过去的东西在服务端换个形状再失败一次。
//
// 也接受单个字符串(不带数组):那是模型常见的偷懒写法,
// 拒绝它只会换来一次重试,而意图毫无歧义。
func stringList(v interface{}) []string {
switch t := v.(type) {
case nil:
return nil
case string:
s := strings.TrimSpace(t)
if s == "" {
return nil
}
return []string{s}
case []string:
// 单测里手写参数时会走到这一支;运行时走不到(见上)
out := make([]string, 0, len(t))
for _, s := range t {
if s = strings.TrimSpace(s); s != "" {
out = append(out, s)
}
}
return out
case []interface{}:
out := make([]string, 0, len(t))
for _, e := range t {
s, ok := e.(string)
if !ok {
continue
}
if s = strings.TrimSpace(s); s != "" {
out = append(out, s)
}
}
return out
default:
return nil
}
}
// formatSize 把字节数写成人类可读的大小。
//
// 与三个 Node 插件的 formatSize 同形B / KB / MB一位小数
// 原先这里直接写 `size/1024` 加 "KB":一个 800 字节的附件显示成 `0KB`,
// 而模型会据此认为上传失败了。
func formatSize(n int64) string {
switch {
case n < 0:
return "0 B"
case n < 1024:
return fmt.Sprintf("%d B", n)
case n < 1024*1024:
return fmt.Sprintf("%.1f KB", float64(n)/1024)
default:
return fmt.Sprintf("%.1f MB", float64(n)/(1024*1024))
}
}