Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

377 lines
9.9 KiB
Go

package agentsessionstore
import (
"encoding/json"
"fmt"
"strings"
)
func SanitizeTimelineItemsForRelay(items []WorkspaceAgentTimelineItem) []WorkspaceAgentTimelineItem {
return sanitizeTimelineItemsForUpstream(items)
}
// SanitizeStatePatchesForRelay reuses the upstream payload sanitation rules for
// local relay transports so oversized tool outputs do not block activity reporting.
func SanitizeStatePatchesForRelay(patches []WorkspaceAgentStatePatch) []WorkspaceAgentStatePatch {
return sanitizeStatePatchesForUpstream(patches)
}
func marshalReportSessionMessagesRequestsForUpload(req reportSessionMessagesRequest) ([][]byte, error) {
requestBody, err := marshalRequestBody(req)
if err != nil {
return nil, err
}
if len(requestBody) <= maxUpstreamReportRequestBytes {
return [][]byte{requestBody}, nil
}
base := reportSessionMessagesRequest{
WorkspaceID: req.WorkspaceID,
AgentTargetID: req.AgentTargetID,
DeviceID: req.DeviceID,
SessionOrigin: req.SessionOrigin,
Connector: req.Connector,
Source: req.Source,
}
current := base
currentBody, err := marshalRequestBody(current)
if err != nil {
return nil, err
}
requests := make([][]byte, 0, 2)
flush := func() {
if len(current.Updates) == 0 {
return
}
requests = append(requests, currentBody)
current = base
currentBody, _ = marshalRequestBody(current)
}
appendMessageUpdate := func(update WorkspaceAgentSessionMessageUpdate) error {
candidate := current
candidate.Updates = append(append([]WorkspaceAgentSessionMessageUpdate(nil), current.Updates...), update)
candidateBody, err := marshalRequestBody(candidate)
if err != nil {
return err
}
if len(candidateBody) <= maxUpstreamReportRequestBytes {
current = candidate
currentBody = candidateBody
return nil
}
if len(current.Updates) == 0 {
return fmt.Errorf("message update %q exceeds upstream request size limit after sanitization", strings.TrimSpace(update.MessageID))
}
flush()
current = base
current.Updates = []WorkspaceAgentSessionMessageUpdate{update}
currentBody, err = marshalRequestBody(current)
if err != nil {
return err
}
if len(currentBody) > maxUpstreamReportRequestBytes {
return fmt.Errorf("message update %q exceeds upstream request size limit after sanitization", strings.TrimSpace(update.MessageID))
}
return nil
}
for _, update := range req.Updates {
if err := appendMessageUpdate(update); err != nil {
return nil, err
}
}
flush()
return requests, nil
}
func sanitizeSessionStateUpdateForUpstream(update WorkspaceAgentSessionStateUpdate) WorkspaceAgentSessionStateUpdate {
if update.Turn != nil && len(update.Turn.FileChanges) > 0 {
turn := *update.Turn
turn.FileChanges = sanitizeToolPayloadMap(clonePayloadMap(update.Turn.FileChanges))
update.Turn = &turn
}
return update
}
func sanitizeSessionMessageUpdatesForUpstream(updates []WorkspaceAgentSessionMessageUpdate) []WorkspaceAgentSessionMessageUpdate {
if len(updates) == 0 {
return updates
}
out := make([]WorkspaceAgentSessionMessageUpdate, len(updates))
for i, update := range updates {
out[i] = update
if len(update.Payload) == 0 {
continue
}
out[i].Payload = sanitizeSessionMessagePayloadForUpstream(update.Kind, update.Payload)
}
return out
}
func sanitizeSessionMessagePayloadForUpstream(kind string, payload map[string]any) map[string]any {
if isTextualSessionMessageKind(kind) {
return sanitizeTextSessionMessagePayloadForUpstream(payload)
}
sanitized := sanitizeToolPayloadMap(clonePayloadMap(payload))
if len(sanitized) == 0 {
return sanitized
}
body, err := json.Marshal(sanitized)
if err != nil || len(body) <= maxUpstreamSessionMessagePayloadBytes {
return sanitized
}
return compactOversizedSessionMessagePayload(sanitized, len(body))
}
func isTextualSessionMessageKind(kind string) bool {
switch strings.TrimSpace(strings.ToLower(kind)) {
case "text", "reasoning":
return true
default:
return false
}
}
func sanitizeTextSessionMessagePayloadForUpstream(payload map[string]any) map[string]any {
if len(payload) == 0 {
return payload
}
out := make(map[string]any, len(payload))
for key, value := range payload {
if isTextSessionMessageContentKey(key) {
out[key] = value
continue
}
out[key] = sanitizeToolPayloadField(key, value)
}
return out
}
func isTextSessionMessageContentKey(key string) bool {
switch strings.TrimSpace(strings.ToLower(key)) {
case "content", "text", "message", "body":
return true
default:
return false
}
}
func compactOversizedSessionMessagePayload(payload map[string]any, originalBytes int) map[string]any {
out := make(map[string]any)
for _, key := range []string{
"callId",
"callID",
"call_id",
"parentCallId",
"parent_call_id",
"rootCallId",
"root_call_id",
"toolName",
"name",
"kind",
"status",
"title",
"type",
"contentMode",
"text",
} {
if value, ok := compactSessionMessagePayloadValue(payload[key]); ok {
out[key] = value
}
}
out["truncatedPayload"] = fmt.Sprintf("[truncated payload; %d bytes]", originalBytes)
out["truncatedPayloadBytes"] = originalBytes
return out
}
func compactSessionMessagePayloadValue(value any) (any, bool) {
switch typed := value.(type) {
case string:
if strings.TrimSpace(typed) == "" {
return nil, false
}
return sanitizeToolPayloadString(typed), true
case bool:
return typed, true
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return typed, true
default:
return nil, false
}
}
func sanitizeTimelineItemsForUpstream(items []WorkspaceAgentTimelineItem) []WorkspaceAgentTimelineItem {
if len(items) == 0 {
return items
}
out := make([]WorkspaceAgentTimelineItem, len(items))
for i, item := range items {
out[i] = item
if len(item.Payload) == 0 {
continue
}
out[i].Payload = sanitizeToolPayloadMap(clonePayloadMap(item.Payload))
}
return out
}
func sanitizeStatePatchesForUpstream(patches []WorkspaceAgentStatePatch) []WorkspaceAgentStatePatch {
if len(patches) == 0 {
return patches
}
out := make([]WorkspaceAgentStatePatch, len(patches))
for i, patch := range patches {
out[i] = patch
if len(patch.Entities) == 0 {
continue
}
out[i].Entities = make([]WorkspaceAgentEntityPatch, len(patch.Entities))
for j, entity := range patch.Entities {
out[i].Entities[j] = entity
out[i].Entities[j].Input = sanitizeToolPayloadMap(entity.Input)
out[i].Entities[j].Output = sanitizeToolPayloadMap(entity.Output)
out[i].Entities[j].Error = sanitizeToolPayloadMap(entity.Error)
}
}
return out
}
func sanitizeToolPayloadMap(payload map[string]any) map[string]any {
if len(payload) == 0 {
return payload
}
if sanitized, ok := sanitizeStructuredBinaryPayloadMap(payload); ok {
return sanitized
}
out := make(map[string]any, len(payload))
for key, value := range payload {
out[key] = sanitizeToolPayloadField(key, value)
}
return out
}
func sanitizeStructuredBinaryPayloadMap(payload map[string]any) (map[string]any, bool) {
data, ok := payload["data"].(string)
if !ok || data == "" {
return nil, false
}
payloadType, _ := payload["type"].(string)
mimeType, _ := payload["mimeType"].(string)
if strings.TrimSpace(payloadType) != "image" && strings.TrimSpace(mimeType) == "" {
return nil, false
}
out := make(map[string]any, len(payload))
for key, value := range payload {
if key == "data" {
out[key] = summarizeBinaryPayloadString(strings.TrimSpace(payloadType), strings.TrimSpace(mimeType), data)
continue
}
out[key] = sanitizeToolPayloadField(key, value)
}
return out, true
}
func sanitizeToolPayloadField(key string, value any) any {
switch typed := value.(type) {
case map[string]any:
return sanitizeToolPayloadMap(typed)
case []any:
if len(typed) == 0 {
return []any{}
}
out := make([]any, len(typed))
for i, item := range typed {
out[i] = sanitizeToolPayloadValue(item)
}
return out
case string:
return sanitizeToolPayloadStringForKey(key, typed)
default:
return value
}
}
func sanitizeToolPayloadValue(value any) any {
switch typed := value.(type) {
case map[string]any:
return sanitizeToolPayloadMap(typed)
case []any:
if len(typed) == 0 {
return []any{}
}
out := make([]any, len(typed))
for i, item := range typed {
out[i] = sanitizeToolPayloadValue(item)
}
return out
case string:
return sanitizeToolPayloadString(typed)
default:
return value
}
}
func sanitizeToolPayloadStringForKey(key string, value string) string {
if looksLikeRawBinaryField(key, value) {
return summarizeBinaryPayloadString("", "", value)
}
return sanitizeToolPayloadString(value)
}
func sanitizeToolPayloadString(value string) string {
if len(value) == 0 {
return value
}
if mediaType, ok := dataURLMediaType(value); ok {
return summarizeBinaryPayloadString("image", mediaType, value)
}
if len(value) <= maxUpstreamToolPayloadStringBytes {
return value
}
return value[:maxUpstreamToolPayloadStringBytes] +
fmt.Sprintf("...[truncated %d bytes]", len(value)-maxUpstreamToolPayloadStringBytes)
}
func looksLikeRawBinaryField(key string, value string) bool {
if len(value) <= maxUpstreamToolPayloadStringBytes {
return false
}
switch strings.ToLower(strings.TrimSpace(key)) {
case "data", "base64", "bytes", "image", "image_data":
return true
default:
return false
}
}
func summarizeBinaryPayloadString(payloadType string, mimeType string, value string) string {
label := strings.TrimSpace(mimeType)
if label == "" {
label = strings.TrimSpace(payloadType)
}
if label == "" {
label = "binary"
}
return fmt.Sprintf("[omitted %s bytes; %d bytes]", label, len(value))
}
func dataURLMediaType(value string) (string, bool) {
if !strings.HasPrefix(value, "data:") {
return "", false
}
comma := strings.IndexByte(value, ',')
if comma <= len("data:") {
return "", false
}
meta := value[len("data:"):comma]
semi := strings.IndexByte(meta, ';')
if semi >= 0 {
meta = meta[:semi]
}
mediaType := strings.TrimSpace(meta)
if mediaType == "" {
mediaType = "data"
}
return mediaType, true
}