From 6fc55cee0d4b715717583ab83c4daa0ec6fcd9d6 Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 30 Sep 2026 17:19:35 +0800 Subject: [PATCH] refactor: remove unreachable legacy call logging package --- .../saas-dispatcher-implementation.md | 2 + internal/calllog/log.go | 270 ------------------ internal/calllog/log_test.go | 130 --------- 3 files changed, 2 insertions(+), 400 deletions(-) delete mode 100644 internal/calllog/log.go delete mode 100644 internal/calllog/log_test.go diff --git a/docs/evidence/saas-dispatcher-implementation.md b/docs/evidence/saas-dispatcher-implementation.md index cc75705..87b7910 100644 --- a/docs/evidence/saas-dispatcher-implementation.md +++ b/docs/evidence/saas-dispatcher-implementation.md @@ -146,6 +146,8 @@ - 普通时段测试不再读取旧方案示例:`TestEvaluateApprovedTaskAndLineConfigShape` 改用 `contracts.Files` 嵌入的当前 task/SIP 正例;TDD 先验证旧 `trunk_details` 在当前 `trunks` 响应下产生预期失败,再绑定当前字段,周一 `09:30` 的允许终点为 `20:00`。`contracts.ReadCurrent` 只允许顶层 Schema/拓扑而**不**允许例子,这是原有严格边界,不为测试扩张;测试只读嵌入的当前正例。删除无人引用、带旧 `schema_version` 和 `agent_version_id` 字段的 `internal/testfixture/execute.go`,不清理数据、日志或上游历史原件。受影响模块 `go test -race`、全仓 `make check`(含三项实际 PASS 的隔离 MQ 测试)、`make release-check-local` 通过。本批仍需完成全仓残留/例外总账与 P08,外部验收未运行。 +- 未接入运行路径的旧通话日志包:全仓活动 Go 代码中没有 `internal/calllog` 调用,独立 `internal/calllog/log.go`/单元测试仍包含旧 `schema_version`、旧任务条目等数据模型;删除该死代码而不是留下看似可用的第二条事实通道。Agent/Dispatcher 的现行失败关键路径仍有 `cmd/sip-go-agent/main.go` 的 `slog.Error` 与 `internal/rpc/` 的脱敏执行、录音授予、结束和结果提交日志,删包不删除任何已存在的日志/录音/恢复文件。`go test -race ./...`、`make check`(含三项真实隔离 MQ PASS)与 `make release-check-local` 均通过;不将旧日志格式留作兼容层。旧 AI 部署夹具与自有 `Current` 入口名称仍需另行审计,P07 尚未收口。 + ## 验收台账 P01–P06 的项目内隔离证据见上;P07 全仓命名与唯一入口尚在清理,A01–A12 和 K01–K16 的最终对照仍待 P08。不得用本地 Mock 冒充外部签收。 diff --git a/internal/calllog/log.go b/internal/calllog/log.go deleted file mode 100644 index 664d0b3..0000000 --- a/internal/calllog/log.go +++ /dev/null @@ -1,270 +0,0 @@ -// Package calllog writes redacted, durable business facts for outbound calls. -// It never writes the original phone number, credentials, audio, prompts, or -// provider URLs. The phone reference is a keyed digest so one number can be -// correlated across tasks without making the log a contact database. -package calllog - -import ( - "crypto/hmac" - cryptorand "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "sync" - "time" -) - -var phonePattern = regexp.MustCompile(`^[0-9]{3,32}$`) -var eventTypePattern = regexp.MustCompile(`^[a-z][a-z0-9_.-]{0,63}$`) - -// Identity is the only phone identity exposed to log records. -type Identity struct { - Ref string - Mask string -} - -// Event is the allow-listed input to Logger.Append. Phone is consumed to make -// a keyed identity and is never serialized. -type Event struct { - EventType string - Phone string - PhoneRef string - PhoneMask string - EventID string - OccurredAt time.Time - - TenantID string - TraceID string - ExecutionID string - TaskID string - TaskItemID string - TaskRevision int64 - AttemptID string - CallID string - AgentID string - CellID string - RoutePolicyID string - CallerProfileID string - TrunkID string - CallState string - AttemptState string - SIPStage string - SIPStatusCode int - SIPReason string - Status string - Result string - ReasonCode string - RecordingID string - RecordingState string - RecordingSize int64 - RecordingDuration int64 - RecordingSHA256 string - DurationMS int64 -} - -type record struct { - SchemaVersion string `json:"schema_version"` - EventID string `json:"event_id"` - OccurredAt string `json:"occurred_at"` - EventType string `json:"event_type"` - PhoneRef string `json:"phone_ref"` - PhoneMask string `json:"phone_mask"` - - TenantID string `json:"tenant_id,omitempty"` - TraceID string `json:"trace_id,omitempty"` - ExecutionID string `json:"execution_id,omitempty"` - TaskID string `json:"task_id,omitempty"` - TaskItemID string `json:"task_item_id,omitempty"` - TaskRevision int64 `json:"task_revision,omitempty"` - AttemptID string `json:"attempt_id,omitempty"` - CallID string `json:"call_id,omitempty"` - AgentID string `json:"agent_id,omitempty"` - CellID string `json:"cell_id,omitempty"` - RoutePolicyID string `json:"route_policy_id,omitempty"` - CallerProfileID string `json:"caller_profile_id,omitempty"` - TrunkID string `json:"trunk_id,omitempty"` - CallState string `json:"call_state,omitempty"` - AttemptState string `json:"attempt_state,omitempty"` - SIPStage string `json:"sip_stage,omitempty"` - SIPStatusCode int `json:"sip_status_code,omitempty"` - SIPReason string `json:"sip_reason,omitempty"` - Status string `json:"status,omitempty"` - Result string `json:"result,omitempty"` - ReasonCode string `json:"reason_code,omitempty"` - RecordingID string `json:"recording_id,omitempty"` - RecordingState string `json:"recording_state,omitempty"` - RecordingSize int64 `json:"recording_size_bytes,omitempty"` - RecordingDuration int64 `json:"recording_duration_ms,omitempty"` - RecordingSHA256 string `json:"recording_sha256,omitempty"` - DurationMS int64 `json:"duration_ms,omitempty"` -} - -// Logger appends one JSON object per line. A mutex and Sync keep concurrent -// call updates from interleaving and make a successful append durable enough -// for the Agent's file-backed recovery boundary. -type Logger struct { - path string - key []byte - now func() time.Time - mu sync.Mutex -} - -func New(path string, key []byte, now func() time.Time) (*Logger, error) { - if strings.TrimSpace(path) == "" { - return nil, errors.New("call log path is required") - } - if len(key) < 16 { - return nil, errors.New("call log phone key must contain at least 16 bytes") - } - if now == nil { - now = time.Now - } - return &Logger{path: path, key: append([]byte(nil), key...), now: now}, nil -} - -func (l *Logger) Path() string { return l.path } - -// Identity returns the stable, redacted reference for a contract callee. -func (l *Logger) Identity(phone string) (Identity, error) { - if err := validatePhone(phone); err != nil { - return Identity{}, err - } - mac := hmac.New(sha256.New, l.key) - _, _ = mac.Write([]byte(phone)) - return Identity{Ref: "hmac-sha256:" + hex.EncodeToString(mac.Sum(nil)), Mask: maskPhone(phone)}, nil -} - -func (l *Logger) Append(event Event) error { - if l == nil { - return errors.New("call logger is nil") - } - if !eventTypePattern.MatchString(event.EventType) { - return errors.New("event type is invalid") - } - identity := Identity{Ref: event.PhoneRef, Mask: event.PhoneMask} - if event.Phone != "" { - computed, err := l.Identity(event.Phone) - if err != nil { - return err - } - if identity.Ref != "" && identity.Ref != computed.Ref { - return errors.New("phone reference does not match phone") - } - identity = computed - } - if err := validateIdentity(identity); err != nil { - return err - } - if event.SIPStatusCode != 0 && (event.SIPStatusCode < 100 || event.SIPStatusCode > 699) { - return errors.New("SIP status code is invalid") - } - if event.TaskRevision < 0 || event.DurationMS < 0 || event.RecordingSize < 0 || event.RecordingDuration < 0 { - return errors.New("negative business log value") - } - if len(event.RecordingSHA256) > 0 && (len(event.RecordingSHA256) != 64 || !isLowerHex(event.RecordingSHA256)) { - return errors.New("recording SHA-256 is invalid") - } - occurredAt := event.OccurredAt - if occurredAt.IsZero() { - occurredAt = l.now() - } - eventID := event.EventID - if eventID == "" { - var random [16]byte - if _, err := cryptorand.Read(random[:]); err != nil { - return fmt.Errorf("generate call log event ID: %w", err) - } - eventID = hex.EncodeToString(random[:]) - } - value := record{ - SchemaVersion: "1.0", EventID: eventID, OccurredAt: occurredAt.UTC().Format(time.RFC3339Nano), - EventType: event.EventType, PhoneRef: identity.Ref, PhoneMask: identity.Mask, - TenantID: event.TenantID, TraceID: event.TraceID, ExecutionID: event.ExecutionID, - TaskID: event.TaskID, TaskItemID: event.TaskItemID, TaskRevision: event.TaskRevision, - AttemptID: event.AttemptID, CallID: event.CallID, AgentID: event.AgentID, CellID: event.CellID, - RoutePolicyID: event.RoutePolicyID, CallerProfileID: event.CallerProfileID, TrunkID: event.TrunkID, - CallState: event.CallState, AttemptState: event.AttemptState, SIPStage: event.SIPStage, - SIPStatusCode: event.SIPStatusCode, SIPReason: event.SIPReason, Status: event.Status, - Result: event.Result, ReasonCode: event.ReasonCode, RecordingID: event.RecordingID, - RecordingState: event.RecordingState, RecordingSize: event.RecordingSize, - RecordingDuration: event.RecordingDuration, RecordingSHA256: event.RecordingSHA256, - DurationMS: event.DurationMS, - } - data, err := json.Marshal(value) - if err != nil { - return fmt.Errorf("encode call log event: %w", err) - } - - l.mu.Lock() - defer l.mu.Unlock() - if err := os.MkdirAll(filepath.Dir(l.path), 0o700); err != nil { - return fmt.Errorf("create call log directory: %w", err) - } - file, err := os.OpenFile(l.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) - if err != nil { - return fmt.Errorf("open call log: %w", err) - } - if err := file.Chmod(0o600); err != nil { - _ = file.Close() - return fmt.Errorf("protect call log: %w", err) - } - if _, err := file.Write(append(data, '\n')); err != nil { - _ = file.Close() - return fmt.Errorf("append call log: %w", err) - } - if err := file.Sync(); err != nil { - _ = file.Close() - return fmt.Errorf("sync call log: %w", err) - } - if err := file.Close(); err != nil { - return fmt.Errorf("close call log: %w", err) - } - return nil -} - -func validateIdentity(identity Identity) error { - if !strings.HasPrefix(identity.Ref, "hmac-sha256:") || len(identity.Ref) != len("hmac-sha256:")+64 || !isLowerHex(strings.TrimPrefix(identity.Ref, "hmac-sha256:")) { - return errors.New("redacted phone reference is invalid") - } - if len(identity.Mask) < 3 || strings.Contains(identity.Mask, " ") || strings.ContainsAny(identity.Mask, "\r\n") { - return errors.New("redacted phone mask is invalid") - } - for _, char := range identity.Mask { - if char != '*' && (char < '0' || char > '9') { - return errors.New("redacted phone mask is invalid") - } - } - if !strings.Contains(identity.Mask, "*") { - return errors.New("redacted phone mask must hide digits") - } - return nil -} - -func validatePhone(phone string) error { - if !phonePattern.MatchString(phone) { - return errors.New("phone must be the original 3-32 digit callee") - } - return nil -} - -func maskPhone(phone string) string { - if len(phone) <= 4 { - return strings.Repeat("*", len(phone)) - } - return strings.Repeat("*", len(phone)-4) + phone[len(phone)-4:] -} - -func isLowerHex(value string) bool { - for _, char := range value { - if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { - return false - } - } - return true -} diff --git a/internal/calllog/log_test.go b/internal/calllog/log_test.go deleted file mode 100644 index ffc5020..0000000 --- a/internal/calllog/log_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package calllog - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" -) - -func TestAppendCorrelatesPhoneWithoutWritingOriginal(t *testing.T) { - directory := t.TempDir() - logger, err := New(filepath.Join(directory, "logs", "calls.jsonl"), []byte("0123456789abcdef"), func() time.Time { - return time.Date(2026, 9, 19, 1, 2, 3, 4, time.UTC) - }) - if err != nil { - t.Fatal(err) - } - phone := "15003164745" - if err := logger.Append(Event{ - EventID: "event-1", EventType: "sip.status", Phone: phone, - ExecutionID: "exec-1", TaskID: "task-1", AttemptID: "attempt-1", CallID: "call-1", - RoutePolicyID: "route-sip-first", CallerProfileID: "caller-sip-first", TrunkID: "provider-primary", - SIPStage: "invite", SIPStatusCode: 183, Status: "ringing", ReasonCode: "provisional", - RecordingID: "recording-1", RecordingState: "pending", DurationMS: 120, - }); err != nil { - t.Fatal(err) - } - if err := logger.Append(Event{ - EventID: "event-2", EventType: "call.finished", Phone: phone, - ExecutionID: "exec-1", CallID: "call-1", Result: "no_answer", ReasonCode: "provider_480", - RecordingState: "failed", DurationMS: 3000, - }); err != nil { - t.Fatal(err) - } - - data, err := os.ReadFile(logger.Path()) - if err != nil { - t.Fatal(err) - } - text := string(data) - if strings.Contains(text, phone) { - t.Fatalf("call log contains original phone: %s", text) - } - lines := strings.Split(strings.TrimSpace(text), "\n") - if len(lines) != 2 { - t.Fatalf("got %d lines, want 2", len(lines)) - } - var first, second map[string]any - if err := json.Unmarshal([]byte(lines[0]), &first); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(lines[1]), &second); err != nil { - t.Fatal(err) - } - if first["phone_ref"] != second["phone_ref"] { - t.Fatalf("same phone must have one reference: %#v %#v", first["phone_ref"], second["phone_ref"]) - } - if first["phone_mask"] != "*******4745" { - t.Fatalf("unexpected phone mask: %v", first["phone_mask"]) - } - if first["trunk_id"] != "provider-primary" || first["sip_stage"] != "invite" || first["sip_status_code"] != float64(183) { - t.Fatalf("missing SIP correlation fields: %#v", first) - } - if first["recording_id"] != "recording-1" || second["result"] != "no_answer" { - t.Fatalf("missing recording/result fields: %#v %#v", first, second) - } - if mode := fileMode(t, logger.Path()); mode.Perm() != 0o600 { - t.Fatalf("log mode is %o, want 600", mode.Perm()) - } -} - -func TestAppendRejectsUnredactedOrInvalidIdentity(t *testing.T) { - logger, err := New(filepath.Join(t.TempDir(), "calls.jsonl"), []byte("0123456789abcdef"), nil) - if err != nil { - t.Fatal(err) - } - for _, event := range []Event{ - {EventType: "call.status"}, - {EventType: "call.status", PhoneRef: "15003164745", PhoneMask: "15003164745"}, - {EventType: "call.status", PhoneRef: "hmac-sha256:bad", PhoneMask: "*******4745"}, - {EventType: "call.status", Phone: "15003164745", SIPStatusCode: 700}, - } { - if err := logger.Append(event); err == nil { - t.Fatalf("event %#v was accepted", event) - } - } -} - -func TestAppendSerializesConcurrentEvents(t *testing.T) { - logger, err := New(filepath.Join(t.TempDir(), "calls.jsonl"), []byte("0123456789abcdef"), nil) - if err != nil { - t.Fatal(err) - } - var wait sync.WaitGroup - for index := 0; index < 32; index++ { - wait.Add(1) - go func(index int) { - defer wait.Done() - if err := logger.Append(Event{EventType: "call.status", Phone: "15830461047", EventID: "event-" + string(rune('a'+index))}); err != nil { - t.Errorf("append %d: %v", index, err) - } - }(index) - } - wait.Wait() - data, err := os.ReadFile(logger.Path()) - if err != nil { - t.Fatal(err) - } - if lines := strings.Count(strings.TrimSpace(string(data)), "\n") + 1; lines != 32 { - t.Fatalf("got %d serialized events, want 32", lines) - } -} - -func TestNewRequiresPhoneKey(t *testing.T) { - if _, err := New(filepath.Join(t.TempDir(), "calls.jsonl"), []byte("short"), nil); err == nil { - t.Fatal("short key was accepted") - } -} - -func fileMode(t *testing.T, path string) os.FileMode { - t.Helper() - info, err := os.Stat(path) - if err != nil { - t.Fatal(err) - } - return info.Mode() -}