refactor: remove unreachable legacy call logging package
This commit is contained in:
@@ -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 冒充外部签收。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user