diff --git a/cmd/saas-server/config.go b/cmd/saas-server/config.go new file mode 100644 index 0000000..53d88e4 --- /dev/null +++ b/cmd/saas-server/config.go @@ -0,0 +1,172 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + + "git.ipao.vip/rogee/go-sip/internal/configread" + "git.ipao.vip/rogee/go-sip/internal/contract" +) + +type snapshots struct { + dispatcherID string + tenantID int64 + sip, providers, quota []byte + tasks map[string][]byte +} + +func validateResource(raw []byte, resource, dispatcherID string) error { + if err := contract.ValidateCurrent("config-read", raw); err != nil { + return fmt.Errorf("%s violates configuration contract", resource) + } + var identity struct { + Resource string `json:"resource"` + DispatcherID string `json:"dispatcher_id"` + } + if err := json.Unmarshal(raw, &identity); err != nil { + return errors.New("invalid resource JSON") + } + if identity.Resource != resource || identity.DispatcherID != dispatcherID { + return fmt.Errorf("%s has wrong owner or resource", resource) + } + return nil +} + +func loadSnapshots(dir, id string) (snapshots, error) { + d := snapshots{dispatcherID: id, tasks: make(map[string][]byte)} + read := func(name, resource string) ([]byte, error) { + path := filepath.Join(dir, name) + info, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("read %s: %w", name, err) + } + if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 { + return nil, fmt.Errorf("%s must be a private regular file", name) + } + raw, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read %s: %w", name, err) + } + if err = validateResource(raw, resource, id); err != nil { + return nil, err + } + return raw, nil + } + var err error + if d.sip, err = read("sip.json", "sip_config"); err != nil { + return d, err + } + if d.providers, err = read("providers.json", "ai_providers"); err != nil { + return d, err + } + if d.quota, err = read("quota.json", "tenant_quota"); err != nil { + return d, err + } + var q configread.Quota + if err = json.Unmarshal(d.quota, &q); err != nil { + return d, errors.New("invalid quota") + } + d.tenantID = q.TenantID + files, err := filepath.Glob(filepath.Join(dir, "tasks", "*.json")) + if err != nil { + return d, err + } + if len(files) == 0 { + return d, errors.New("at least one task is required") + } + for _, path := range files { + raw, err := read(filepath.Join("tasks", filepath.Base(path)), "task_config") + if err != nil { + return d, err + } + var task configread.Task + if err = json.Unmarshal(raw, &task); err != nil { + return d, errors.New("invalid task snapshot") + } + if filepath.Base(path) != task.TaskID+".json" || task.TenantID != d.tenantID { + return d, errors.New("task filename or tenant mismatch") + } + d.tasks[task.TaskID] = raw + } + if err = d.validateReferences(); err != nil { + return d, err + } + return d, nil +} + +func (d snapshots) validateReferences() error { + var sip struct { + Trunks []struct { + ID string `json:"trunk_id"` + } `json:"trunks"` + } + var providers struct { + Providers []configread.Provider `json:"providers"` + } + if err := json.Unmarshal(d.sip, &sip); err != nil { + return errors.New("invalid SIP snapshot") + } + if err := json.Unmarshal(d.providers, &providers); err != nil { + return errors.New("invalid providers snapshot") + } + trunks := make(map[string]bool) + for _, t := range sip.Trunks { + if trunks[t.ID] { + return errors.New("duplicate trunk") + } + trunks[t.ID] = true + } + codes := make(map[string]bool) + for _, p := range providers.Providers { + if codes[p.Code] { + return errors.New("duplicate provider code") + } + codes[p.Code] = true + } + for _, raw := range d.tasks { + var task configread.Task + if err := json.Unmarshal(raw, &task); err != nil { + return errors.New("invalid task snapshot") + } + for _, t := range task.AllowedTrunks { + if !trunks[t.TrunkID] { + return errors.New("task references missing trunk") + } + } + for _, ref := range []string{task.Agent.ASR.ProviderRef, task.Agent.LLM.ProviderRef, task.Agent.TTS.ProviderRef} { + if ref != "" && !codes[ref] { + return errors.New("task references missing provider") + } + } + } + return nil +} + +func (d snapshots) page(after string) ([]byte, error) { + if after != "" && after != "complete" { + return nil, errors.New("unknown discovery cursor") + } + tasks := []configread.DiscoveredTask{} + if after == "" { + for _, raw := range d.tasks { + var task configread.Task + if err := json.Unmarshal(raw, &task); err != nil { + return nil, errors.New("invalid task snapshot") + } + tasks = append(tasks, configread.DiscoveredTask{TaskID: task.TaskID, TenantID: task.TenantID, Status: task.Status, TaskRevision: task.TaskRevision}) + } + } + sort.Slice(tasks, func(i, j int) bool { return tasks[i].TaskID < tasks[j].TaskID }) + raw, err := json.Marshal(configread.TaskPage{DispatcherID: d.dispatcherID, Cursor: "complete", Tasks: tasks}) + if err != nil { + return nil, err + } + if err = contract.ValidateCurrent("task-discovery", raw); err != nil { + return nil, errors.New("invalid discovery response") + } + return raw, nil +} diff --git a/cmd/saas-server/config_test.go b/cmd/saas-server/config_test.go new file mode 100644 index 0000000..d7dca46 --- /dev/null +++ b/cmd/saas-server/config_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "testing" + + "git.ipao.vip/rogee/go-sip/internal/configread" +) + +func fixture(t *testing.T) (string, string) { + t.Helper() + dir := t.TempDir() + if err := os.Mkdir(filepath.Join(dir, "tasks"), 0700); err != nil { + t.Fatal(err) + } + names := map[string]string{"sip.json": "config-read-sip.json", "providers.json": "config-read-providers.json", "quota.json": "config-read-quota.json", "tasks/1.json": "config-read-task-full.json"} + var id string + for target, example := range names { + raw, err := os.ReadFile(filepath.Join("../../contracts/schema/examples", example)) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err = json.Unmarshal(raw, &fields); err != nil { + t.Fatal(err) + } + if err = json.Unmarshal(fields["dispatcher_id"], &id); err != nil { + t.Fatal(err) + } + if target == "tasks/1.json" { + fields["task_id"] = json.RawMessage(`"1"`) + raw, _ = json.Marshal(fields) + } + if err = os.WriteFile(filepath.Join(dir, target), raw, 0600); err != nil { + t.Fatal(err) + } + } + return dir, id +} + +type appTransport struct{ app *service } + +func (tr appTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return tr.app.app.Test(req) +} + +func TestConfigClientReadsAllResources(t *testing.T) { + dir, id := fixture(t) + data, err := loadSnapshots(dir, id) + if err != nil { + t.Fatal(err) + } + svc := newService(data, "test-secret") + client, err := configread.NewClient("http://saas.test", id, "test-secret", &http.Client{Transport: appTransport{svc}}) + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + sip, err := client.ReadSIP(ctx) + if err != nil { + t.Fatal(err) + } + providers, err := client.ReadProviders(ctx) + if err != nil { + t.Fatal(err) + } + tasks, _, err := client.ReadAllTasks(ctx) + if err != nil || len(tasks) != 1 { + t.Fatalf("tasks=%v err=%v", tasks, err) + } + if _, err = client.ReadTask(ctx, "1", tasks[0].TenantID, sip, providers); err != nil { + t.Fatal(err) + } + for _, path := range []string{"/internal/v1/dispatcher/task/missing", "/internal/v1/dispatcher/tenant/0/quota"} { + req, _ := http.NewRequest("GET", "http://saas.test"+path, nil) + req.Header.Set("X-DISPATCHER-id", id) + req.Header.Set("X-DISPATCHER-SECRET-KEY", "test-secret") + resp, err := svc.app.Test(req) + if err != nil { + t.Fatal(err) + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + if resp.StatusCode != 404 { + t.Fatalf("%s: %d", path, resp.StatusCode) + } + } + req, _ := http.NewRequest("GET", "http://saas.test/internal/v1/dispatcher/sip", nil) + resp, err := svc.app.Test(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + if resp.StatusCode != 403 { + t.Fatal(resp.StatusCode) + } +} + +func TestSnapshotErrorsAndReadOnly(t *testing.T) { + dir, id := fixture(t) + before := make(map[string][]byte) + filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if !entry.IsDir() { + before[path], err = os.ReadFile(path) + } + return err + }) + if _, err := loadSnapshots(dir, id); err != nil { + t.Fatal(err) + } + for path, want := range before { + got, err := os.ReadFile(path) + if err != nil || string(got) != string(want) { + t.Fatalf("input changed: %s", path) + } + } + if _, err := loadSnapshots(dir, "wrong-owner"); err == nil { + t.Fatal("accepted wrong owner") + } + if err := os.WriteFile(filepath.Join(dir, "sip.json"), []byte(`{"api_key":"must-not-leak"}`), 0600); err != nil { + t.Fatal(err) + } + if _, err := loadSnapshots(dir, id); err == nil { + t.Fatal("accepted invalid snapshot") + } +} diff --git a/cmd/saas-server/http.go b/cmd/saas-server/http.go new file mode 100644 index 0000000..94abdc8 --- /dev/null +++ b/cmd/saas-server/http.go @@ -0,0 +1,65 @@ +package main + +import ( + "crypto/subtle" + "strconv" + "sync" + + "github.com/gofiber/fiber/v3" +) + +type service struct { + app *fiber.App + mu sync.RWMutex + data snapshots +} + +func newService(data snapshots, secret string) *service { + s := &service{data: data, app: fiber.New(fiber.Config{Immutable: true})} + const prefix = "/internal/v1/dispatcher" + s.app.Use(prefix, func(c fiber.Ctx) error { + if subtle.ConstantTimeCompare([]byte(c.Get("X-DISPATCHER-id")), []byte(data.dispatcherID)) != 1 || subtle.ConstantTimeCompare([]byte(c.Get("X-DISPATCHER-SECRET-KEY")), []byte(secret)) != 1 { + return c.Status(403).JSON(fiber.Map{"error": "unknown dispatcher"}) + } + return c.Next() + }) + serve := func(resource string) fiber.Handler { + return func(c fiber.Ctx) error { + s.mu.RLock() + defer s.mu.RUnlock() + var raw []byte + switch resource { + case "sip": + raw = s.data.sip + case "providers": + raw = s.data.providers + case "quota": + id, err := strconv.ParseInt(c.Params("tenant_id"), 10, 64) + if err != nil || id != s.data.tenantID { + return c.Status(404).JSON(fiber.Map{"error": "unknown tenant"}) + } + raw = s.data.quota + case "task": + var ok bool + raw, ok = s.data.tasks[c.Params("task_id")] + if !ok { + return c.Status(404).JSON(fiber.Map{"error": "unknown task"}) + } + case "tasks": + var err error + raw, err = s.data.page(c.Query("after")) + if err != nil { + return c.Status(400).JSON(fiber.Map{"error": err.Error()}) + } + } + c.Set("Content-Type", "application/json") + return c.Send(raw) + } + } + s.app.Get(prefix+"/sip", serve("sip")) + s.app.Get(prefix+"/ai-providers", serve("providers")) + s.app.Get(prefix+"/tenant/:tenant_id/quota", serve("quota")) + s.app.Get(prefix+"/task/:task_id", serve("task")) + s.app.Get(prefix+"/tasks", serve("tasks")) + return s +} diff --git a/cmd/saas-server/startup.go b/cmd/saas-server/startup.go new file mode 100644 index 0000000..4860958 --- /dev/null +++ b/cmd/saas-server/startup.go @@ -0,0 +1,118 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "strings" +) + +type startupConfig struct { + Listen string `json:"listen"` + DispatcherID string `json:"dispatcher_id"` + DispatcherSecret string `json:"dispatcher_secret"` + BrokerURL string `json:"broker_url"` + Snapshots string `json:"snapshots"` + Feedback string `json:"feedback_dir"` + TLSCert string `json:"tls_cert,omitempty"` + TLSKey string `json:"tls_key,omitempty"` +} + +func loadStartup(path string) (startupConfig, error) { + var c startupConfig + info, err := os.Stat(path) + if err != nil { + return c, fmt.Errorf("read startup config: %w", err) + } + if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 { + return c, errors.New("startup config must be a private regular file") + } + raw, err := os.ReadFile(path) + if err != nil { + return c, fmt.Errorf("read startup config: %w", err) + } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err = dec.Decode(&c); err != nil || !json.Valid(raw) { + return c, errors.New("invalid startup configuration") + } + if c.Listen == "" { + c.Listen = "0.0.0.0:8089" + } + if _, _, err = net.SplitHostPort(c.Listen); err != nil { + return c, errors.New("listen must specify host and port") + } + if c.DispatcherID == "" || c.DispatcherSecret == "" || strings.ContainsAny(c.DispatcherID+c.DispatcherSecret, "\r\n") || c.Snapshots == "" || c.Feedback == "" { + return c, errors.New("dispatcher identity, secret, snapshots and feedback_dir are required") + } + address, err := url.Parse(c.BrokerURL) + if err != nil || (address.Scheme != "amqp" && address.Scheme != "amqps") || address.Host == "" || !strings.HasPrefix(strings.TrimPrefix(address.Path, "/"), "saas-mock-") { + return c, errors.New("broker_url must use a dedicated saas-mock-* vhost") + } + base, err := filepath.Abs(filepath.Dir(path)) + if err != nil { + return c, err + } + resolve := func(p string) string { + if p == "" { + return "" + } + if !filepath.IsAbs(p) { + p = filepath.Join(base, p) + } + return filepath.Clean(p) + } + c.Snapshots = resolve(c.Snapshots) + c.Feedback = resolve(c.Feedback) + c.TLSCert = resolve(c.TLSCert) + c.TLSKey = resolve(c.TLSKey) + // Resolve existing path components so aliases cannot place output in an input directory. + realPath := func(p string) (string, error) { + var suffix []string + for { + real, e := filepath.EvalSymlinks(p) + if e == nil { + for i := len(suffix) - 1; i >= 0; i-- { + real = filepath.Join(real, suffix[i]) + } + return real, nil + } + if !os.IsNotExist(e) { + return "", e + } + parent := filepath.Dir(p) + if parent == p { + return "", e + } + suffix = append(suffix, filepath.Base(p)) + p = parent + } + } + c.Snapshots, err = realPath(c.Snapshots) + if err != nil { + return c, fmt.Errorf("snapshot path: %w", err) + } + c.Feedback, err = realPath(c.Feedback) + if err != nil { + return c, fmt.Errorf("feedback path: %w", err) + } + overlaps := func(a, b string) bool { + rel, e := filepath.Rel(a, b) + return e == nil && (rel == "." || !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != "..") + } + if overlaps(c.Snapshots, c.Feedback) || overlaps(c.Feedback, c.Snapshots) || overlaps(c.Feedback, resolve(filepath.Base(path))) { + return c, errors.New("feedback directory must be separate from configuration inputs") + } + if (c.TLSCert == "") != (c.TLSKey == "") { + return c, errors.New("tls_cert and tls_key must be provided together") + } + if c.TLSCert != "" && (overlaps(c.Feedback, c.TLSCert) || overlaps(c.Feedback, c.TLSKey)) { + return c, errors.New("feedback directory overlaps TLS inputs") + } + return c, nil +} diff --git a/cmd/saas-server/startup_test.go b/cmd/saas-server/startup_test.go new file mode 100644 index 0000000..177642c --- /dev/null +++ b/cmd/saas-server/startup_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestStartupPathsAndIsolation(t *testing.T) { + dir, id := fixture(t) + config := startupConfig{Listen: "0.0.0.0:8089", DispatcherID: id, DispatcherSecret: "secret", BrokerURL: "amqp://guest:guest@localhost/saas-mock-unit", Snapshots: ".", Feedback: "../feedback"} + path := filepath.Join(dir, "server.json") + raw, _ := json.Marshal(config) + if err := os.WriteFile(path, raw, 0600); err != nil { + t.Fatal(err) + } + got, err := loadStartup(path) + if err != nil { + t.Fatal(err) + } + if got.Snapshots != dir || got.Feedback != filepath.Join(filepath.Dir(dir), "feedback") { + t.Fatalf("paths not relative to config: %+v", got) + } + config.Feedback = "tasks" + raw, _ = json.Marshal(config) + os.WriteFile(path, raw, 0600) + if _, err := loadStartup(path); err == nil { + t.Fatal("accepted feedback inside config dataset") + } + config.Feedback = "../feedback" + config.BrokerURL = "amqp://guest:guest@localhost/production" + raw, _ = json.Marshal(config) + os.WriteFile(path, raw, 0600) + if _, err := loadStartup(path); err == nil { + t.Fatal("accepted non-test vhost") + } +} diff --git a/docs/plan-saas-server-v0.1.md b/docs/plan-saas-server-v0.1.md new file mode 100644 index 0000000..4ab1daa --- /dev/null +++ b/docs/plan-saas-server-v0.1.md @@ -0,0 +1,112 @@ +# SaaS Server v0.1 最小开发计划 + +## 1. 目标与边界 + +需求及本次精简范围已确认;当前仅修改计划,尚未开始实现。 + +使用 `Fiber/v3` 在 `cmd/saas-server` 实现独立测试服务,通过 HTTP 接口和 `curl` 替代 SaaS 浏览器/CDP 操作: + +**指定文件加载配置 → Dispatcher 按现行接口读取 → 手动推送一条外呼 → 查看并校验反馈。** + +硬性边界: + +- 不修改 Dispatcher/Agent、共享业务代码、业务合同、数据库和现有部署配置;不为测试增加旁路、放宽校验或改变执行规则。 +- 只调用可直接复用的公共类型/函数,不提取或重构旧 Mock、Dispatcher/Agent 的代码。不能直接复用时,在新命令内补最小 SaaS 侧逻辑,不另造业务规则。 +- 不直接调用 Agent/Asterisk,不自行选线、判断业务额度、释放占用或编造结果。外呼及配置生效由现行 Dispatcher/Agent 决定。 +- 只使用明确隔离的测试 MQ vhost,不与现有 SaaS 争抢反馈、不改动现有服务。开发和本地验收不授权部署、真实拨号或 AI/OSS 调用。 +- 实现时新增源码、测试及说明限于 `cmd/saas-server/`;只允许为 Fiber 更新 `go.mod/go.sum`,并同步必要的项目文档。遇到需要改变现有业务才能接通的问题,停止并单独说明,不能顺手修改。 + +唯一业务依据为 [`../contracts/schema/`](../contracts/schema/README.md),不复制 Schema 或拓扑规范。调研/修改合同前遵循 [`contracts.md`](contracts.md);本功能不计划变更合同。 + +## 2. 只保留这些功能 + +### 2.1 从文件读取配置 + +建议启动形式(待实现): + +```sh +go run ./cmd/saas-server --config /absolute/path/server.json +``` + +启动文件指定监听地址、Dispatcher 身份、MQ 连接参数、业务快照位置和独立反馈目录。监听 `0.0.0.0`,端口可配置;相对路径以启动文件目录为基准。 + +沿用现有快照组织:`sip.json`、`providers.json`、`quota.json`、`tasks/*.json`。任务列表从已加载任务生成,不再维护第二份列表。所有文件一次性读取并校验,缺失或无效即明确报错,不使用默认业务参数或旧配置兜底。 + +- 配置文件只读,不改写、补字段、格式化、监听变化或退出保存;不扫描真实私密配置。 +- 配置接口提供现行五类资源:SIP、任务详情、任务列表、租户额度和 AI providers;路径、响应及调用标识遵循现行合同。 +- 使用现有 `internal/contract`、`internal/configread` 可用的类型和校验;AMQP 使用已有依赖。参考 `deploys/test/saas-mock/` 的文件加载与拓扑行为,但不修改该工具。 + +### 2.2 仅在内存修改配置 + +按资源提交完整快照,校验成功后一次性替换内存;失败保留原值,并保证并发读取不会读到半份配置。任务列表与任务详情保持一致。 + +只修改已加载资源,不增删任务,不变更 Dispatcher/租户身份。重启重新读取原文件,临时修改消失。 + +修改配置不等于 Dispatcher 已采用: + +- 任务及额度:通过显式 start/resume/edit 指令,按现行规则重新读取;不覆盖 Dispatcher 已有的暂停/停止状态。 +- SIP:通过显式 `sip.config` 通知触发现行加载流程。 +- AI providers:Dispatcher 只在启动读取,修改后需要由操作者另行安排重启;本工具不重启任何服务。 + +这些接口只发送标准指令,不在工具内另建任务状态机,不改变已发出通话的快照。 + +### 2.3 手动发送一条外呼 + +输入源 `event_id`、已加载的 `task_id` 和原始 `callee`;其余归属来自配置。按合同构造并校验消息,再向对应测试队列发布,不开放任意路由/原始消息投递。 + +- 源 `event_id` 原值保留,不哈希、清洗或在重试时另造 ID。 +- 本次进程内,相同消息身份重复请求不重复发布;内容不同则拒绝。只在内存记录,不保存发送日志或实现重启恢复。 +- 使用 persistent、mandatory、publisher confirm;未 return 且确认成功才报告队列已接收。断线或确认超时明确报告结果未知,不自动重发。 +- 重启后不承诺跨进程请求去重,操作者不能把重启当重试或换 ID 补拨;Dispatcher 原有重复消息处理规则不变。 +- 启动和退出不自动发指令。不做批量、循环、定时、重拨或换线。 + +测试拓扑由本工具在专用 vhost 中按现行规范预建;不 purge、删除队列或重置数据。MQ 故障明确报错并停止相关操作,不实现自动重连重发系统。 + +### 2.4 接收、查询和校验反馈 + +消费专用测试反馈队列,提供本次进程收到的反馈查询。仅校验合同、源身份、请求归属及反馈实际具备的关联字段,不复制 Dispatcher 的业务判断。 + +- 使用 `(dispatcher_id, event_type, event_id)` 区分派发回执、控制回执和最终结果;重复/冲突消息不覆盖原记录,校验失败明确显示原因。 +- 原消息写入独立反馈目录并确认保存成功后才 ACK;写入失败报错并停止消费,不 ACK、不丢弃。无效消息也保存并标明错误,避免无限重投。 +- 反馈文件仅用于人工检查,不扫描恢复历史、不建立持久索引。重启后在线查询从空集合开始,原文件保留且不覆盖;历史请求关联不在本次查询范围。 +- 队列已接收、已派发、被拒绝、最终结果分别展示。等待超时只是尚未收到,不能推断通话失败、接通或 AI 成功;未发起的拒绝不强求通话最终结果。 +- 反馈目录与配置来源分离,不能覆盖配置文件。日志与查询返回必要摘要,不输出凭据、完整对话、音频、签名 URL 或原始 SIP;原消息仅保存在受控文件。 + +## 3. 最小接口 + +以下为测试操作接口,不修改 SaaS↔Dispatcher 合同: + +| 接口 | 用途 | +| --- | --- | +| `PUT /test/config/sip` | 替换内存 SIP 完整快照 | +| `PUT /test/config/providers` | 替换内存 AI providers 完整快照 | +| `PUT /test/config/quota` | 替换内存额度完整快照 | +| `PUT /test/config/tasks/:task_id` | 替换已加载任务完整快照 | +| `POST /test/calls` | 手动发送一条外呼 | +| `POST /test/task-controls` | 仅发送 start/resume/edit,满足测试启动及配置读取需要 | +| `POST /test/sip-refresh` | 显式发送 SIP 配置更新通知 | +| `GET /test/events?event_id=...` | 查询本次运行收到的同源反馈和校验结论 | + +源 ID 通过查询参数传递并正确 URL 编码,不改变原值。只提供 `curl` 示例,不开发独立 CLI、页面、实时推送或自动等待脚本。 + +## 4. 开发顺序与验证 + +单个命令包内按启动、配置、HTTP、MQ/反馈分文件即可,不新增服务层、仓储接口、插件或数据库。新增依赖只需 Fiber/v3;异步处理和内存状态不能引用已被 Fiber 回收的请求缓冲。 + +按 TDD 分三步实现: + +1. **配置服务**:只读加载和五类配置接口;用现有配置客户端验证读取,覆盖错误文件、归属和任务列表一致性。 +2. **外呼与反馈**:单条发布、简单反馈文件、当前运行查询;覆盖 confirm/return、结果未知、重复请求、身份关联、非法反馈及保存失败不 ACK。 +3. **内存修改与使用说明**:完整快照替换、必要通知及 `curl` 示例;覆盖更新失败不改变当前值、重启恢复原配置、反馈查询不恢复历史。 + +完成条件: + +- 用未修改的 Dispatcher/Agent 在隔离 Mock 模式和真实 RabbitMQ 中完成「读取配置 → 推送 → 收到回执/结果」测试,不仅手工伪造反馈。 +- 测试前后比对所有输入配置文件,启动、修改、失败和退出均不改变文件;并发操作通过 race 检查。 +- 格式、合同/Proto、`go vet ./...`、`go test -race ./...`、构建及项目既有检查通过,业务单元覆盖率 ≥65%;现有集成检查不因精简而跳过。 +- 检查最终变更范围:Dispatcher/Agent、共享业务代码、合同和现有部署配置无改动。 +- 本轮只改计划,检查文档与变更范围,不声称业务功能或真实通话已验证。 + +## 5. 明确不做 + +发送记录持久化、重启恢复、历史在线查询、持久索引、完整任务管理(pause/stop 等)、任务增删、独立 CLI、管理页面、批量/压测、自动重试、配置保存/导出、服务重启、业务代码重构、数据库、多租户/多 Dispatcher、生产部署。 diff --git a/go.mod b/go.mod index 5f400f8..b215284 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/emiago/sipgo v1.6.0 github.com/go-audio/audio v1.0.0 github.com/go-audio/wav v1.1.0 + github.com/gofiber/fiber/v3 v3.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/hujm2023/go-mp3/v2 v2.0.1 @@ -23,8 +24,8 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.1 github.com/zaf/g711 v1.4.0 - golang.org/x/net v0.58.0 - golang.org/x/sys v0.47.0 + golang.org/x/net v0.60.0 + golang.org/x/sys v0.49.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa google.golang.org/grpc v1.83.2 google.golang.org/protobuf v1.36.12 @@ -40,14 +41,19 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.2 // indirect + github.com/gofiber/schema v1.8.8 // indirect + github.com/gofiber/utils/v2 v2.6.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.20.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-colorable v0.1.16 // indirect github.com/mattn/go-isatty v0.0.24 // indirect + github.com/molecule-man/go-brrr v1.2.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/oklog/ulid v1.3.1 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -57,11 +63,15 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/tinylib/msgp v1.6.5 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.75.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/text v0.41.0 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/text v0.42.0 // indirect golang.org/x/time v0.4.0 // indirect modernc.org/libc v1.75.7 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index fc3c2e5..3ac5ee2 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= @@ -23,6 +22,8 @@ github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7 github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emiago/sipgo v1.6.0 h1:6EuOP7c6f0VRatKYTPEYNezt4hslBEsaCzZZOhT2n3s= github.com/emiago/sipgo v1.6.0/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY= +github.com/fxamacker/cbor/v2 v2.9.6 h1:NHZPxuUjJXQjX9itA785y/5NKSlI4HQ+S7noZsmI7hs= +github.com/fxamacker/cbor/v2 v2.9.6/go.mod h1:4EaZp6ClVunETjQ4GHmGNzysDK91Lr+r6Yz6tJqzVCw= github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= @@ -43,6 +44,12 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gofiber/fiber/v3 v3.6.0 h1:dmu6zB1SSXINf1flhgMRVaIOQ08vaZaN8JYVSBJI3fI= +github.com/gofiber/fiber/v3 v3.6.0/go.mod h1:ce/ZWPbiMom+nAEkPv4+NPBEx8b9+MH3g2VV6nvEpgo= +github.com/gofiber/schema v1.8.8 h1:md5t65+Z9LttOuj7FdMcQ63D2+6GniT2ganLXwvTJM4= +github.com/gofiber/schema v1.8.8/go.mod h1:Hzf0L/fTPP1IwOAwKipKflm9C2722aebnSKJCMOgb1I= +github.com/gofiber/utils/v2 v2.6.1 h1:x3zsS/oUQz0ia2Zx8XXtG/7naR5a+/95FRO8mwQ5QQ4= +github.com/gofiber/utils/v2 v2.6.1/go.mod h1:tc9KqdRAQZRD/u0mN2d1ZHROGheSZv+0FcC9cg5OEdw= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -66,16 +73,21 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.16 h1:+2YjeAe7xP1OQbZW1jERPWWCDLlNqFNCvjiqGiAt84A= +github.com/mattn/go-colorable v0.1.16/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw= +github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -83,13 +95,14 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/openai/openai-go/v3 v3.62.0 h1:P3G1Ip9eOwCJxg2q1lPBFg7iuyDEJDuoqQinzZDaxJM= github.com/openai/openai-go/v3 v3.62.0/go.mod h1:dE39tezpSvL+SHpkZQm7XhSc8BOLDzu8CEh1mHc9MzU= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtp v1.10.5 h1:ip0HhO/wYZqQ4bKS+R99KnZh/GRCmIT0jDXikub7vlE= github.com/pion/rtp v1.10.5/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk= github.com/pion/rtp/v2 v2.0.0 h1:8s4xPETm04IugKZaykpJnJ8LAGDLOQpsIpRXMzgM6Ow= github.com/pion/rtp/v2 v2.0.0/go.mod h1:Vj+rrFbJCT3yxqE/VSwaOo9DQ2pMKGPxuE7hplGOlOs= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= @@ -102,6 +115,8 @@ github.com/rotisserie/eris v0.4.1/go.mod h1:lODN/gtqebxPHRbCcWeCYOE350FC2M3V/oAP github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/shamaton/msgpack/v3 v3.2.3 h1:0r0aZtrXDFOPpwWJTEOpFq+Ih/JmMLm0pqLN5mSilrw= +github.com/shamaton/msgpack/v3 v3.2.3/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo= github.com/shirou/gopsutil/v4 v4.26.8 h1:YQMTF/1J50B5+Y0vlo1eDRf5DoR7Gk69hY+8wjYkQeo= github.com/shirou/gopsutil/v4 v4.26.8/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= @@ -114,8 +129,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= @@ -126,10 +141,18 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tinylib/msgp v1.6.5 h1:iAH6XTP7BpzErjBZlujJQXxA+GmRi2YWs6M4KdLtNbE= +github.com/tinylib/msgp v1.6.5/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.75.0 h1:W3xrvNJHG60j2ZCjxpP8kkB+oPG8kh9/g1n4+3xUwNA= +github.com/valyala/fasthttp v1.75.0/go.mod h1:JiVkPRZU124PuPsreV/s9dZRgG2lRK7wNEikdQ0Rk4s= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -151,29 +174,33 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -186,16 +213,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -203,8 +230,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -221,7 +248,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8= modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=