269 lines
14 KiB
Go
269 lines
14 KiB
Go
package rtcconformance
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
CaptureSchema = "real-device-rtc-capture-v1"
|
|
ReportSchema = "real-device-rtc-conformance-report-v1"
|
|
MinimumDevices = 3
|
|
MinimumSessions = 6
|
|
)
|
|
|
|
var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
|
var RequiredEngineModes = []string{"s2s_pure", "s2s_hybrid", "cascade"}
|
|
var RequiredScenarios = []string{"NORMAL_COMPLETION", "TOKEN_RENEWAL", "NETWORK_SWITCH_RECOVERY", "CONTROLLED_STOP"}
|
|
var RequiredLifecycle = []string{"ROOM_ISSUED", "STARTING_AGENT", "ACTIVE", "STOPPING", "COMPLETED"}
|
|
|
|
type Capture struct {
|
|
SchemaVersion string `json:"schema_version"`
|
|
EvidenceSource string `json:"evidence_source"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
Environment string `json:"environment"`
|
|
RTCProviderRef string `json:"rtc_provider_ref"`
|
|
VoiceProviderRef string `json:"voice_provider_ref"`
|
|
RTCApplicationRef string `json:"rtc_application_ref"`
|
|
RegionRef string `json:"region_ref"`
|
|
ChangeTicketRef string `json:"change_ticket_ref"`
|
|
ExecutedByRef string `json:"executed_by_ref"`
|
|
AttestedByRef string `json:"attested_by_ref"`
|
|
AttestedAt time.Time `json:"attested_at"`
|
|
CollectedAt time.Time `json:"collected_at"`
|
|
Devices []DeviceEvidence `json:"devices"`
|
|
Sessions []SessionEvidence `json:"sessions"`
|
|
}
|
|
|
|
type DeviceEvidence struct {
|
|
DeviceID string `json:"device_id"`
|
|
HardwareModel string `json:"hardware_model"`
|
|
HardwareRevision string `json:"hardware_revision"`
|
|
FirmwareVersion string `json:"firmware_version"`
|
|
IdentityRef string `json:"identity_ref"`
|
|
AudioProfileRef string `json:"audio_profile_ref"`
|
|
}
|
|
|
|
type SessionEvidence struct {
|
|
ConversationID string `json:"conversation_id"`
|
|
DeviceID string `json:"device_id"`
|
|
EngineMode string `json:"engine_mode"`
|
|
Scenario string `json:"scenario"`
|
|
RoomRef string `json:"room_ref"`
|
|
DeviceRTCUserRef string `json:"device_rtc_user_ref"`
|
|
AgentRTCUserRef string `json:"agent_rtc_user_ref"`
|
|
ProviderTaskRef string `json:"provider_task_ref"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
EndedAt time.Time `json:"ended_at"`
|
|
Lifecycle []string `json:"lifecycle"`
|
|
RTCJoinSucceeded bool `json:"rtc_join_succeeded"`
|
|
UplinkSucceeded bool `json:"uplink_succeeded"`
|
|
DownlinkSucceeded bool `json:"downlink_succeeded"`
|
|
NearAudioPublished bool `json:"near_audio_published"`
|
|
FarAudioPlayed bool `json:"far_audio_played"`
|
|
CredentialRenewed bool `json:"credential_renewed"`
|
|
NetworkRecovered bool `json:"network_recovered"`
|
|
ProviderStartAccepted bool `json:"provider_start_accepted"`
|
|
ProviderStopConfirmed bool `json:"provider_stop_confirmed"`
|
|
FinalTranscriptReceived bool `json:"final_transcript_received"`
|
|
UsageRecorded bool `json:"usage_recorded"`
|
|
QualityResult string `json:"quality_result"`
|
|
NoDuplicateProviderTask bool `json:"no_duplicate_provider_task"`
|
|
ResourcesReleased bool `json:"resources_released"`
|
|
CloudEvidenceRef string `json:"cloud_evidence_ref"`
|
|
DeviceEvidenceRef string `json:"device_evidence_ref"`
|
|
ProviderEvidenceRef string `json:"provider_evidence_ref"`
|
|
QualityEvidenceRef string `json:"quality_evidence_ref"`
|
|
UsageEvidenceRef string `json:"usage_evidence_ref"`
|
|
}
|
|
|
|
type Check struct {
|
|
Code string `json:"code"`
|
|
Status string `json:"status"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
|
|
type Report struct {
|
|
SchemaVersion string `json:"schema_version"`
|
|
Result string `json:"result"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
EvidenceScope string `json:"evidence_scope"`
|
|
ExternalValidationRequired bool `json:"external_validation_required"`
|
|
Environment string `json:"environment"`
|
|
RTCProviderRef string `json:"rtc_provider_ref"`
|
|
VoiceProviderRef string `json:"voice_provider_ref"`
|
|
DeviceCount int `json:"device_count"`
|
|
SessionCount int `json:"session_count"`
|
|
CompletedEngineModes []string `json:"completed_engine_modes"`
|
|
CompletedScenarios []string `json:"completed_scenarios"`
|
|
ModeSessionCounts map[string]int `json:"mode_session_counts"`
|
|
ScenarioSessionCounts map[string]int `json:"scenario_session_counts"`
|
|
InputSHA256 map[string]string `json:"input_sha256"`
|
|
Checks []Check `json:"checks"`
|
|
Disclaimer string `json:"disclaimer"`
|
|
}
|
|
|
|
func ParseCapture(content []byte) (Capture, error) {
|
|
var capture Capture
|
|
if err := decodeStrict(content, &capture); err != nil {
|
|
return Capture{}, fmt.Errorf("decode real device RTC capture: %w", err)
|
|
}
|
|
return capture, nil
|
|
}
|
|
|
|
func ParseReport(content []byte) (Report, error) {
|
|
var report Report
|
|
if err := decodeStrict(content, &report); err != nil {
|
|
return Report{}, fmt.Errorf("decode real device RTC report: %w", err)
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
func Evaluate(capture Capture, candidateVersion, buildDigest string, generatedAt time.Time) Report {
|
|
candidateVersion = strings.TrimSpace(candidateVersion)
|
|
buildDigest = strings.ToLower(strings.TrimSpace(buildDigest))
|
|
report := Report{
|
|
SchemaVersion: ReportSchema, Result: "PASS", GeneratedAt: generatedAt.UTC(), CandidateVersion: candidateVersion, BuildDigest: buildDigest,
|
|
EvidenceScope: "PROTOCOL_SIMULATION", ExternalValidationRequired: true, Environment: capture.Environment, RTCProviderRef: capture.RTCProviderRef, VoiceProviderRef: capture.VoiceProviderRef,
|
|
DeviceCount: len(capture.Devices), SessionCount: len(capture.Sessions), CompletedEngineModes: []string{}, CompletedScenarios: []string{}, ModeSessionCounts: map[string]int{}, ScenarioSessionCounts: map[string]int{}, InputSHA256: map[string]string{}, Checks: []Check{},
|
|
Disclaimer: "该报告只核对同一候选的终端、RTC、语音供应商、状态、媒体、字幕、用量、质量和回收证据;协议模拟不能替代量产终端与真实供应商验收,也不等于业务发布批准。",
|
|
}
|
|
if capture.EvidenceSource == "HARDWARE_PROVIDER_CAPTURE" {
|
|
report.EvidenceScope = "REAL_DEVICE_RTC_PROVIDER"
|
|
}
|
|
add := func(code string, ok bool, detail string) {
|
|
status := "PASS"
|
|
if !ok {
|
|
status, report.Result = "FAIL", "FAIL"
|
|
}
|
|
report.Checks = append(report.Checks, Check{Code: code, Status: status, Detail: detail})
|
|
}
|
|
add("CAPTURE_SCHEMA", capture.SchemaVersion == CaptureSchema, "RTC证据必须使用"+CaptureSchema)
|
|
add("EVIDENCE_SOURCE", capture.EvidenceSource == "HARDWARE_PROVIDER_CAPTURE" || capture.EvidenceSource == "PROTOCOL_SIMULATION", "证据来源必须明确为HARDWARE_PROVIDER_CAPTURE或PROTOCOL_SIMULATION")
|
|
add("CANDIDATE_IDENTITY", candidateVersion != "" && digestPattern.MatchString(buildDigest), "候选版本和sha256构建摘要必须完整")
|
|
add("CAPTURE_CANDIDATE_BINDING", capture.CandidateVersion == candidateVersion && strings.ToLower(capture.BuildDigest) == buildDigest, "RTC采集必须绑定同一候选版本和构建摘要")
|
|
|
|
real := capture.EvidenceSource == "HARDWARE_PROVIDER_CAPTURE"
|
|
providerOK := capture.RTCProviderRef != "" && capture.VoiceProviderRef != "" && capture.RTCApplicationRef != "" && capture.RegionRef != ""
|
|
environmentOK := capture.ExecutedByRef != "" && capture.AttestedByRef != "" && !capture.AttestedAt.IsZero() && !capture.CollectedAt.IsZero() && !capture.AttestedAt.After(capture.CollectedAt)
|
|
if real {
|
|
providerOK = providerOK && capture.RTCProviderRef != "mock" && capture.VoiceProviderRef != "mock"
|
|
environmentOK = environmentOK && capture.ExecutedByRef != capture.AttestedByRef && (capture.Environment == "STAGING" || capture.Environment == "PRODUCTION") && capture.ChangeTicketRef != ""
|
|
} else {
|
|
environmentOK = environmentOK && capture.Environment == "LOCAL"
|
|
}
|
|
add("PROVIDER_IDENTITY", providerOK, "真实证据必须绑定非模拟RTC、语音供应商、应用和区域引用")
|
|
add("ENVIRONMENT_IDENTITY", environmentOK, "真实证据必须绑定目标环境、变更工单、执行人和独立复核人")
|
|
|
|
deviceOK := len(capture.Devices) > 0
|
|
devices := map[string]bool{}
|
|
for _, device := range capture.Devices {
|
|
if device.DeviceID == "" || devices[device.DeviceID] || device.HardwareModel == "" || device.HardwareRevision == "" || device.FirmwareVersion == "" || device.IdentityRef == "" || device.AudioProfileRef == "" {
|
|
deviceOK = false
|
|
}
|
|
devices[device.DeviceID] = true
|
|
}
|
|
if real && len(devices) < MinimumDevices {
|
|
deviceOK = false
|
|
}
|
|
add("DEVICE_COVERAGE", deviceOK, "真实矩阵至少覆盖3台身份、硬件、固件和音频配置完整的候选终端")
|
|
|
|
sessionOK, modeOK, scenarioOK, mediaOK, lifecycleOK, recoveryOK, providerLifecycleOK, dataOK, refsOK, timelineOK := len(capture.Sessions) > 0, true, true, true, true, true, true, true, true, true
|
|
seenSessions, usedDevices := map[string]bool{}, map[string]bool{}
|
|
normalModes := map[string]bool{}
|
|
for _, session := range capture.Sessions {
|
|
if session.ConversationID == "" || seenSessions[session.ConversationID] || !devices[session.DeviceID] || session.RoomRef == "" || session.DeviceRTCUserRef == "" || session.AgentRTCUserRef == "" || session.ProviderTaskRef == "" {
|
|
sessionOK = false
|
|
}
|
|
seenSessions[session.ConversationID] = true
|
|
usedDevices[session.DeviceID] = true
|
|
if !slices.Contains(RequiredEngineModes, session.EngineMode) {
|
|
modeOK = false
|
|
} else {
|
|
report.ModeSessionCounts[session.EngineMode]++
|
|
}
|
|
if !slices.Contains(RequiredScenarios, session.Scenario) {
|
|
scenarioOK = false
|
|
} else {
|
|
report.ScenarioSessionCounts[session.Scenario]++
|
|
}
|
|
if session.Scenario == "NORMAL_COMPLETION" {
|
|
normalModes[session.EngineMode] = true
|
|
}
|
|
mediaOK = mediaOK && session.RTCJoinSucceeded && session.UplinkSucceeded && session.DownlinkSucceeded && session.NearAudioPublished && session.FarAudioPlayed
|
|
lifecycleOK = lifecycleOK && slices.Equal(session.Lifecycle, RequiredLifecycle)
|
|
recoveryOK = recoveryOK && (session.Scenario != "TOKEN_RENEWAL" || session.CredentialRenewed) && (session.Scenario != "NETWORK_SWITCH_RECOVERY" || session.NetworkRecovered)
|
|
providerLifecycleOK = providerLifecycleOK && session.ProviderStartAccepted && session.ProviderStopConfirmed && session.NoDuplicateProviderTask && session.ResourcesReleased
|
|
dataOK = dataOK && session.FinalTranscriptReceived && session.UsageRecorded && session.QualityResult == "PASS"
|
|
refsOK = refsOK && session.CloudEvidenceRef != "" && session.DeviceEvidenceRef != "" && session.ProviderEvidenceRef != "" && session.QualityEvidenceRef != "" && session.UsageEvidenceRef != ""
|
|
timelineOK = timelineOK && !session.StartedAt.IsZero() && !session.EndedAt.IsZero() && !session.EndedAt.Before(session.StartedAt) && !session.EndedAt.After(capture.CollectedAt)
|
|
}
|
|
if real {
|
|
sessionOK = sessionOK && len(capture.Sessions) >= MinimumSessions && len(usedDevices) >= MinimumDevices
|
|
for _, mode := range RequiredEngineModes {
|
|
if report.ModeSessionCounts[mode] == 0 || !normalModes[mode] {
|
|
modeOK = false
|
|
}
|
|
}
|
|
for _, scenario := range RequiredScenarios {
|
|
if report.ScenarioSessionCounts[scenario] == 0 {
|
|
scenarioOK = false
|
|
}
|
|
}
|
|
}
|
|
for _, mode := range RequiredEngineModes {
|
|
if report.ModeSessionCounts[mode] > 0 {
|
|
report.CompletedEngineModes = append(report.CompletedEngineModes, mode)
|
|
}
|
|
}
|
|
for _, scenario := range RequiredScenarios {
|
|
if report.ScenarioSessionCounts[scenario] > 0 {
|
|
report.CompletedScenarios = append(report.CompletedScenarios, scenario)
|
|
}
|
|
}
|
|
add("SESSION_IDENTITY", sessionOK, "真实矩阵至少6通且会话、设备、房间、用户和供应商任务引用必须唯一完整")
|
|
add("ENGINE_MODE_COVERAGE", modeOK, "三种语音方式均须完成正常闭环")
|
|
add("SCENARIO_COVERAGE", scenarioOK, "矩阵须覆盖正常完成、凭证续签、网络切换恢复和受控停止")
|
|
add("RTC_MEDIA_PATH", mediaOK, "每通必须入房成功并证明上行、下行、近端发布和远端播放")
|
|
add("STATE_LIFECYCLE", lifecycleOK, "每通必须按ROOM_ISSUED到COMPLETED的受控状态序列收敛")
|
|
add("TOKEN_NETWORK_RECOVERY", recoveryOK, "对应场景必须证明短期凭证续签或网络切换恢复")
|
|
add("PROVIDER_LIFECYCLE", providerLifecycleOK, "供应商启停、任务去重和RTC资源回收必须完成")
|
|
add("TRANSCRIPT_USAGE_QUALITY", dataOK, "每通必须形成最终字幕、用量记录和业务方质量通过结论")
|
|
add("EVIDENCE_REFERENCES", refsOK, "每通必须保存云端、设备、供应商、质量和用量脱敏引用")
|
|
add("EXECUTION_TIMELINE", timelineOK, "每通开始结束时间必须有效且不晚于采集时间")
|
|
if real && providerOK && environmentOK && deviceOK && sessionOK && modeOK && scenarioOK && mediaOK && lifecycleOK && recoveryOK && providerLifecycleOK && dataOK && refsOK && timelineOK && report.Result == "PASS" {
|
|
report.ExternalValidationRequired = false
|
|
}
|
|
return report
|
|
}
|
|
|
|
func SHA256(content []byte) string {
|
|
digest := sha256.Sum256(content)
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
func decodeStrict(content []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(content))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
|
return errors.New("trailing JSON data")
|
|
}
|
|
return nil
|
|
}
|