Files

232 lines
7.6 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package minutesdata
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
)
// RequireWriteAcknowledgement accepts only an explicit successful envelope or
// a non-empty result object. A bare {} cannot prove a remote write succeeded.
func RequireWriteAcknowledgement(operation string, data map[string]any) error {
if err := validateEnvelope(data); err != nil {
return err
}
if success, exists := data["success"]; exists {
value, ok := success.(bool)
if !ok || !value {
return fmt.Errorf("minutes %s response has invalid success acknowledgement", operation)
}
return nil
}
if result, ok := data["result"].(map[string]any); ok && len(result) > 0 {
return nil
}
return fmt.Errorf("minutes %s response has no explicit successful acknowledgement", operation)
}
// RequirePermissionMutationAcknowledgement validates the target-level payload
// returned by add/remove_member_permission. The backend's top-level
// success=true is not sufficient: historically it also accompanied a
// resultMap that merely echoed nonexistent task UUIDs, and it does not prove
// that every requested task/member pair was changed.
func RequirePermissionMutationAcknowledgement(operation string, taskUUIDs, memberUIDs []string, data map[string]any) error {
if err := validateEnvelope(data); err != nil {
return err
}
if success, ok := data["success"].(bool); !ok || !success {
return fmt.Errorf("minutes %s response has no explicit success=true", operation)
}
result, ok := data["result"].(map[string]any)
if !ok {
return fmt.Errorf("minutes %s response has no result object", operation)
}
resultMap, ok := result["resultMap"].(map[string]any)
if !ok {
return fmt.Errorf("minutes %s response has no result.resultMap object", operation)
}
if len(resultMap) != len(taskUUIDs) {
return fmt.Errorf("minutes %s resultMap covers %d task UUIDs, want %d", operation, len(resultMap), len(taskUUIDs))
}
wantMembers := make(map[string]bool, len(memberUIDs))
for _, member := range memberUIDs {
wantMembers[strings.TrimSpace(member)] = true
}
for _, taskUUID := range taskUUIDs {
rawMembers, exists := resultMap[taskUUID]
if !exists {
return fmt.Errorf("minutes %s resultMap is missing task UUID %s", operation, taskUUID)
}
members, ok := rawMembers.([]any)
if !ok {
return fmt.Errorf("minutes %s resultMap[%s] has type %T, want array", operation, taskUUID, rawMembers)
}
gotMembers := make(map[string]bool, len(members))
for index, member := range members {
value := stringField(map[string]any{"member": member}, "member")
if value == "" {
return fmt.Errorf("minutes %s resultMap[%s][%d] is not a member UID", operation, taskUUID, index)
}
gotMembers[value] = true
}
if len(gotMembers) != len(wantMembers) {
return fmt.Errorf("minutes %s resultMap[%s] member coverage mismatch", operation, taskUUID)
}
for member := range wantMembers {
if !gotMembers[member] {
return fmt.Errorf("minutes %s resultMap[%s] is missing member UID %s", operation, taskUUID, member)
}
}
}
return nil
}
// RecordResult validates the gateway's observed listening-note command result.
func RecordResult(expectedCmd, taskUUID string, data map[string]any) (map[string]any, error) {
if err := RequireWriteAcknowledgement("record "+expectedCmd, data); err != nil {
return nil, err
}
result, ok := data["result"].(map[string]any)
if !ok {
return nil, fmt.Errorf("minutes record %s response has no result object", expectedCmd)
}
cmd := stringField(result, "cmd")
if cmd != expectedCmd {
return nil, fmt.Errorf("minutes record response cmd %q does not match %q", cmd, expectedCmd)
}
if expectedCmd != "create" {
actual := TaskUUID(result)
if actual == "" || actual != taskUUID {
return nil, fmt.Errorf("minutes record %s response task UUID mismatch", expectedCmd)
}
}
return result, nil
}
// MindGraphStatus extracts the documented 0=processing, 1=success, 2=failed
// state and rejects empty status responses.
func MindGraphStatus(data map[string]any) (int, map[string]any, error) {
if err := validateEnvelope(data); err != nil {
return 0, nil, err
}
result, ok := data["result"].(map[string]any)
if !ok {
return 0, nil, fmt.Errorf("minutes mind graph response has no result object")
}
value, exists := result["taskStatus"]
if !exists {
return 0, nil, fmt.Errorf("minutes mind graph response has no taskStatus")
}
status, err := intValue(value)
if err != nil || status < 0 || status > 2 {
return 0, nil, fmt.Errorf("minutes mind graph taskStatus is invalid: %v", value)
}
return status, result, nil
}
// SpeakerSummaryTask extracts the recovery handle returned by task creation.
func SpeakerSummaryTask(data map[string]any) (taskID, status string, err error) {
if err := RequireWriteAcknowledgement("speaker summary create", data); err != nil {
return "", "", err
}
result, ok := data["result"].(map[string]any)
if !ok {
return "", "", fmt.Errorf("minutes speaker summary create response has no result object")
}
taskID = stringField(result, "taskId", "taskID")
status = strings.ToLower(stringField(result, "status"))
if taskID == "" {
return "", "", fmt.Errorf("minutes speaker summary create response has no taskId")
}
return taskID, status, nil
}
// SpeakerSummaryResult requires a concrete result payload. The API returns a
// business error while processing, so an empty object is never ready.
func SpeakerSummaryResult(data map[string]any) (any, error) {
if err := validateEnvelope(data); err != nil {
return nil, err
}
result, exists := data["result"]
if !exists || result == nil {
return nil, fmt.Errorf("minutes speaker summary response has no result")
}
switch value := result.(type) {
case map[string]any:
if len(value) == 0 {
return nil, fmt.Errorf("minutes speaker summary result is empty")
}
case []any:
if value == nil {
return nil, fmt.Errorf("minutes speaker summary result is null")
}
default:
return nil, fmt.Errorf("minutes speaker summary result has type %T", result)
}
return result, nil
}
// HotWords extracts the explicit current personal hot-word set. [] is valid;
// a missing/null/wrongly typed list is not.
func HotWords(data map[string]any) ([]string, error) {
if err := validateEnvelope(data); err != nil {
return nil, err
}
result, ok := data["result"].(map[string]any)
if !ok {
return nil, fmt.Errorf("minutes hot-word response has no result object")
}
raw, exists := result["hotWordList"]
if !exists || raw == nil {
return nil, fmt.Errorf("minutes hot-word response has no hotWordList array")
}
items, ok := raw.([]any)
if !ok {
return nil, fmt.Errorf("minutes hotWordList has type %T, want array", raw)
}
out := make([]string, 0, len(items))
seen := map[string]bool{}
for index, item := range items {
word := ""
switch value := item.(type) {
case string:
word = value
case map[string]any:
word = stringField(value, "hotWord", "word", "name", "content")
}
word = strings.TrimSpace(word)
if word == "" {
return nil, fmt.Errorf("minutes hotWordList item %d has no word", index)
}
if !seen[word] {
seen[word] = true
out = append(out, word)
}
}
return out, nil
}
func intValue(value any) (int, error) {
switch typed := value.(type) {
case float64:
if math.IsNaN(typed) || math.IsInf(typed, 0) || math.Trunc(typed) != typed || typed < 0 || typed > 2 {
return 0, fmt.Errorf("invalid status number %v", typed)
}
return int(typed), nil
case int:
return typed, nil
case json.Number:
parsed, err := strconv.Atoi(string(typed))
return parsed, err
case string:
return strconv.Atoi(strings.TrimSpace(typed))
default:
return 0, fmt.Errorf("unsupported number type %T", value)
}
}