refactor(config): remove unused mixed-mode environment config

This commit is contained in:
2026-09-30 15:54:47 +08:00
parent 3ad606b3f5
commit 7d69a24b4e
6 changed files with 17 additions and 336 deletions
@@ -130,6 +130,8 @@
- 旧 Dispatcher 本地配置解析:旧 `LoadDispatcherFile` 独占读取上游 v1 `dispatcher-config.schema.json`,没有现行 Dispatcher CLI 调用者;删除实现与旧文件专属测试。现行入口仍从部署环境独立读取 Dispatcher 身份、SaaS 只读配置端点及受控 OSS 配置,并保留当前校验和错误报告。历史上游 Schema 未改,删除旧入口不代表真实配置来源已联调。
- 本地发布与合同脚本:先运行发布检查复现旧校验器因受保护第三方文档的历史 SHA-256 不匹配而中断,未覆盖用户修改、未改写历史 manifest/hash;改由当前合同正反例验证作准入,单独保留上游 v1 文件 SHA-256 的历史来源核对,删除会重新生成/校验过时代次的两个脚本。发布清单只记录当前 local 合同、MQ 拓扑、Proto 及历史来源 manifest 的实际哈希,不再宣称旧 v0.1/v0.3 草案是现行。发布检查先因旧清单格式失败,改完后仍因旧 CLI 错误文案失败;以当前 `dispatcher --mode mixed` 实测确认其在创建 SQLite 前拒绝,再修正检查文本。最终本地发布及打包、防覆盖、mixed/real 资源前拒绝和当前合同/队列检查均通过;仅生成并删除自有临时产物,未部署、未拨号、未处理现存业务数据。
- 旧全局环境配置:旧 `Config`/`FromEnv` 同时配置 Mock、mixed/real、旧 Dispatcher SQLite 和 OSS 环境凭据,已无现行业务调用;删除实现和专属测试,保留当前分角色环境及批准文件/受控引用读取。把「旧 OSS 环境变量不可覆盖配置文件与受控凭据」迁至现行 `LoadCurrentOSSConfig` 隔离测试;原旧结构的 mTLS 专属测试删除,现行 Dispatcher/Agent mTLS 校验测试仍在。全仓 Go 测试通过;不更改实际部署配置或现存数据。
## 验收台账
P01–P06 的项目内隔离证据见上;P07 全仓命名与唯一入口尚在清理,A01–A12 和 K01–K16 的最终对照仍待 P08。不得用本地 Mock 冒充外部签收。
-11
View File
@@ -52,14 +52,3 @@ func TestParseCertificateFingerprints(t *testing.T) {
t.Fatal("expected invalid fingerprint to be rejected")
}
}
func TestConfigRequiresDispatcherMTLSForEndpointInventory(t *testing.T) {
cfg := Config{Mode: "mock", DBPath: ":memory:", AgentEndpointsFile: "agents.json"}
if err := cfg.Validate("dispatcher"); err == nil {
t.Fatal("expected Dispatcher mTLS requirement")
}
cfg.MTLSCAFile, cfg.MTLSCertFile, cfg.MTLSKeyFile = "ca.pem", "dispatcher.pem", "dispatcher.key"
if err := cfg.Validate("dispatcher"); err != nil {
t.Fatal(err)
}
}
-208
View File
@@ -1,208 +0,0 @@
package config
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
Mode string
DBPath string
SpoolRoot string
RabbitURL string
Exchange string
OutboxBatch int
AgentID string
DispatcherID string
Version string
CellID string
GRPCListen string
DispatcherGRPCListen string
DispatcherGRPCEndpoint string
DispatcherGRPCServerName string
DispatcherGRPCAllowedAgentIDs string
MTLSCAFile string
MTLSCertFile string
MTLSKeyFile string
MTLSServerName string
MTLSPeerCertificateFingerprints string
OSSRegion string
OSSEndpoint string
OSSBucket string
OSSAccessKeyID string
OSSAccessKeySecret string
OSSKeyPrefix string
OSSGrantTTL time.Duration
OSSMaxAssetBytes int64
StaticArtifactPath string
AgentEndpointsFile string
CallLogPath string
CallLogPhoneKey string
ARIURL string
ARIWebsocketURL string
ARIApplication string
ARIUsername string
ARIPassword string
CallTarget string
CallTrunkID string
CallCallerID string
CallMediaBind string
CallMediaPort int
CallRecordingDirectory string
CallAISnapshotPath string
CallTenantID string
CallTenantKey string
CallTaskID string
CallTaskItemID string
CallExecutionID string
}
func FromEnv() (Config, error) {
mediaPort, err := envInt("AGENT_CALL_MEDIA_PORT", 12000)
if err != nil {
return Config{}, err
}
return Config{
Mode: envOr("SIP_GO_AGENT_MODE", "mock"),
DBPath: envOr("DISPATCHER_DB", "dispatcher.db"),
SpoolRoot: envOr("AGENT_SPOOL", "./spool"),
RabbitURL: os.Getenv("RABBITMQ_URL"),
Exchange: envOr("RABBITMQ_EXCHANGE", "agent-call.commands.v1"),
OutboxBatch: 50,
AgentID: envOr("AGENT_ID", "agent-local"),
DispatcherID: envOr("DISPATCHER_ID", "dispatcher-local"),
Version: envOr("AGENT_VERSION", "dev"),
CellID: envOr("CELL_ID", "cell-local"),
GRPCListen: os.Getenv("AGENT_GRPC_LISTEN"),
DispatcherGRPCListen: os.Getenv("DISPATCHER_GRPC_LISTEN"),
DispatcherGRPCEndpoint: os.Getenv("DISPATCHER_GRPC_ENDPOINT"),
DispatcherGRPCServerName: envOr("DISPATCHER_GRPC_SERVER_NAME", os.Getenv("MTLS_SERVER_NAME")),
DispatcherGRPCAllowedAgentIDs: os.Getenv("DISPATCHER_ALLOWED_AGENT_IDS"),
MTLSCAFile: os.Getenv("MTLS_CA_FILE"),
MTLSCertFile: os.Getenv("MTLS_CERT_FILE"),
MTLSKeyFile: os.Getenv("MTLS_KEY_FILE"),
MTLSServerName: os.Getenv("MTLS_SERVER_NAME"),
MTLSPeerCertificateFingerprints: os.Getenv("MTLS_PEER_CERT_FINGERPRINTS"),
OSSMaxAssetBytes: 64 << 20,
StaticArtifactPath: os.Getenv("AGENT_STATIC_ARTIFACT"),
AgentEndpointsFile: os.Getenv("DISPATCHER_AGENT_ENDPOINTS_FILE"),
CallLogPath: os.Getenv("AGENT_CALL_BUSINESS_LOG"),
CallLogPhoneKey: os.Getenv("AGENT_CALL_PHONE_LOG_KEY"),
ARIURL: envOr("ARI_URL", "http://127.0.0.1:8088/ari"),
ARIWebsocketURL: envOr("ARI_WS_URL", "ws://127.0.0.1:8088/ari/events"),
ARIApplication: envOr("ARI_APPLICATION", "agent-call"),
ARIUsername: os.Getenv("ARI_USERNAME"),
ARIPassword: os.Getenv("ARI_PASSWORD"),
CallTarget: os.Getenv("AGENT_CALL_TARGET"),
CallTrunkID: os.Getenv("AGENT_CALL_TRUNK_ID"),
CallCallerID: os.Getenv("AGENT_CALL_CALLER_ID"),
CallMediaBind: envOr("AGENT_CALL_MEDIA_BIND", "127.0.0.1"),
CallMediaPort: mediaPort,
CallRecordingDirectory: envOr("AGENT_CALL_RECORDING_DIR", "./recordings"),
CallAISnapshotPath: os.Getenv("AGENT_CALL_AI_SNAPSHOT"),
CallTenantID: os.Getenv("AGENT_CALL_TENANT_ID"),
CallTenantKey: os.Getenv("AGENT_CALL_TENANT_KEY"),
CallTaskID: os.Getenv("AGENT_CALL_TASK_ID"),
CallTaskItemID: os.Getenv("AGENT_CALL_TASK_ITEM_ID"),
CallExecutionID: os.Getenv("AGENT_CALL_EXECUTION_ID"),
}, nil
}
func (c Config) Validate(role string) error {
if c.Mode != "mock" && c.Mode != "mixed" && c.Mode != "real" {
return fmt.Errorf("unsupported mode %q", c.Mode)
}
if role == "dispatcher" && strings.TrimSpace(c.DBPath) == "" {
return errors.New("dispatcher DB path is required")
}
if role == "agent" && strings.TrimSpace(c.SpoolRoot) == "" {
return errors.New("agent spool root is required")
}
if role == "agent" && strings.TrimSpace(c.CallLogPath) != "" && strings.TrimSpace(c.CallLogPhoneKey) == "" {
return errors.New("AGENT_CALL_PHONE_LOG_KEY is required when AGENT_CALL_BUSINESS_LOG is set")
}
if role == "agent" && strings.TrimSpace(c.CallLogPhoneKey) != "" && len([]byte(c.CallLogPhoneKey)) < 16 {
return errors.New("AGENT_CALL_PHONE_LOG_KEY must contain at least 16 bytes")
}
if role == "agent" && strings.TrimSpace(c.GRPCListen) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when AGENT_GRPC_LISTEN is enabled", name)
}
}
}
if role == "agent" && c.Mode == "real" && strings.TrimSpace(c.GRPCListen) == "" {
return errors.New("real agent mode requires AGENT_GRPC_LISTEN")
}
if role == "agent" && c.Mode == "real" && strings.TrimSpace(c.StaticArtifactPath) == "" {
return errors.New("real agent mode requires AGENT_STATIC_ARTIFACT")
}
if role == "agent" && strings.TrimSpace(c.MTLSPeerCertificateFingerprints) != "" {
if _, err := ParseCertificateFingerprints(c.MTLSPeerCertificateFingerprints); err != nil {
return fmt.Errorf("invalid MTLS_PEER_CERT_FINGERPRINTS: %w", err)
}
}
if role == "agent" && strings.TrimSpace(c.DispatcherGRPCEndpoint) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when DISPATCHER_GRPC_ENDPOINT is enabled", name)
}
}
}
if role == "dispatcher" && strings.TrimSpace(c.DispatcherGRPCListen) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when DISPATCHER_GRPC_LISTEN is enabled", name)
}
}
for name, value := range map[string]string{"oss.region": c.OSSRegion, "oss.endpoint": c.OSSEndpoint, "oss.bucket": c.OSSBucket, "oss.access_key_id_env reference": c.OSSAccessKeyID, "oss.access_key_secret_env reference": c.OSSAccessKeySecret} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s must be supplied by the Dispatcher configuration file", name)
}
}
if strings.TrimSpace(c.DispatcherGRPCAllowedAgentIDs) == "" {
return errors.New("DISPATCHER_ALLOWED_AGENT_IDS is required when Dispatcher gRPC is enabled")
}
fingerprints, err := ParseCertificateFingerprints(c.MTLSPeerCertificateFingerprints)
if err != nil {
return fmt.Errorf("invalid MTLS_PEER_CERT_FINGERPRINTS: %w", err)
}
if len(fingerprints) == 0 {
return errors.New("MTLS_PEER_CERT_FINGERPRINTS is required when Dispatcher gRPC is enabled")
}
}
if role == "dispatcher" && strings.TrimSpace(c.AgentEndpointsFile) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when DISPATCHER_AGENT_ENDPOINTS_FILE is enabled", name)
}
}
}
if c.Mode == "real" && strings.TrimSpace(c.RabbitURL) == "" && role == "dispatcher" {
return errors.New("real dispatcher mode requires RABBITMQ_URL")
}
return nil
}
func envOr(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func envInt(key string, fallback int) (int, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
parsed, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("invalid %s: %w", key, err)
}
return parsed, nil
}
-100
View File
@@ -1,100 +0,0 @@
package config
import (
"strings"
"testing"
)
func TestConfigRejectsRealDispatcherWithoutBroker(t *testing.T) {
c, err := FromEnv()
if err != nil {
t.Fatal(err)
}
c.Mode, c.RabbitURL, c.DBPath = "real", "", ":memory:"
if err := c.Validate("dispatcher"); err == nil {
t.Fatal("expected real-mode broker requirement")
}
}
func TestFromEnvRejectsInvalidMediaPort(t *testing.T) {
t.Setenv("AGENT_CALL_MEDIA_PORT", "not-a-port")
if _, err := FromEnv(); err == nil || !strings.Contains(err.Error(), "AGENT_CALL_MEDIA_PORT") {
t.Fatalf("expected explicit media port error, got %v", err)
}
t.Setenv("AGENT_CALL_MEDIA_PORT", "43210")
cfg, err := FromEnv()
if err != nil || cfg.CallMediaPort != 43210 {
t.Fatalf("expected configured port, got %d, %v", cfg.CallMediaPort, err)
}
t.Setenv("AGENT_CALL_MEDIA_PORT", "")
cfg, err = FromEnv()
if err != nil || cfg.CallMediaPort != 12000 {
t.Fatalf("expected default port, got %d, %v", cfg.CallMediaPort, err)
}
}
func TestConfigAcceptsMockAgent(t *testing.T) {
c := Config{Mode: "mock", SpoolRoot: t.TempDir()}
if err := c.Validate("agent"); err != nil {
t.Fatal(err)
}
}
func TestConfigLoadsUnifiedDispatcherGRPCSettings(t *testing.T) {
t.Setenv("DISPATCHER_GRPC_LISTEN", "127.0.0.1:19443")
t.Setenv("DISPATCHER_GRPC_ENDPOINT", "dispatcher.test:19443")
t.Setenv("DISPATCHER_GRPC_SERVER_NAME", "dispatcher.test")
t.Setenv("DISPATCHER_ALLOWED_AGENT_IDS", "agent-cell-a,agent-cell-b")
c, err := FromEnv()
if err != nil {
t.Fatal(err)
}
if c.DispatcherGRPCListen != "127.0.0.1:19443" || c.DispatcherGRPCEndpoint != "dispatcher.test:19443" || c.DispatcherGRPCServerName != "dispatcher.test" || c.DispatcherGRPCAllowedAgentIDs != "agent-cell-a,agent-cell-b" {
t.Fatalf("unified Dispatcher gRPC settings were not loaded: %+v", c)
}
}
func TestConfigRequiresPhoneLogPair(t *testing.T) {
c := Config{Mode: "mock", SpoolRoot: t.TempDir(), CallLogPath: "calls.jsonl"}
if err := c.Validate("agent"); err == nil {
t.Fatal("expected phone log key requirement")
}
c.CallLogPhoneKey = "0123456789abcdef"
if err := c.Validate("agent"); err != nil {
t.Fatal(err)
}
c.CallLogPath = ""
if err := c.Validate("agent"); err != nil {
t.Fatalf("key-only configuration should use spool fallback: %v", err)
}
c.CallLogPhoneKey = "short"
if err := c.Validate("agent"); err == nil {
t.Fatal("expected phone log key length requirement")
}
}
func TestConfigRequiresAgentTLSFilesWhenRPCIsEnabled(t *testing.T) {
c := Config{Mode: "mock", SpoolRoot: t.TempDir(), GRPCListen: "127.0.0.1:19090"}
if err := c.Validate("agent"); err == nil {
t.Fatal("expected mTLS file requirement")
}
c.MTLSCAFile, c.MTLSCertFile, c.MTLSKeyFile = "ca.pem", "agent.pem", "agent.key"
if err := c.Validate("agent"); err != nil {
t.Fatal(err)
}
c.Mode, c.GRPCListen = "real", ""
if err := c.Validate("agent"); err == nil {
t.Fatal("expected real agent listener requirement")
}
}
func TestConfigRequiresStaticArtifactForRealAgent(t *testing.T) {
c := Config{Mode: "real", SpoolRoot: t.TempDir(), GRPCListen: "127.0.0.1:19090", MTLSCAFile: "ca.pem", MTLSCertFile: "agent.pem", MTLSKeyFile: "agent.key"}
if err := c.Validate("agent"); err == nil {
t.Fatal("expected static artifact requirement")
}
c.StaticArtifactPath = "artifact.json"
if err := c.Validate("agent"); err != nil {
t.Fatal(err)
}
}
+15
View File
@@ -33,6 +33,21 @@ func TestLoadCurrentOSSConfigRequiresBoundLocalHTTPSAndExplicitCredentials(t *te
}
}
func TestLoadCurrentOSSConfigIgnoresLegacyOSSEnvironment(t *testing.T) {
for _, key := range []string{"DISPATCHER_OSS_REGION", "DISPATCHER_OSS_ENDPOINT", "DISPATCHER_OSS_BUCKET", "DISPATCHER_OSS_ACCESS_KEY_ID", "DISPATCHER_OSS_ACCESS_KEY_SECRET", "DISPATCHER_OSS_KEY_PREFIX", "DISPATCHER_OSS_GRANT_TTL_SECONDS"} {
t.Setenv(key, "legacy-value")
}
t.Setenv("MOCK_OSS_KEY_ID", "isolated-id")
t.Setenv("MOCK_OSS_KEY_SECRET", "isolated-secret")
configuration, err := LoadCurrentOSSConfig(writeMockOSSDeployment(t, mockOSSDeployment), dispatcherFixtureID)
if err != nil {
t.Fatal(err)
}
if configuration.Endpoint != "https://127.0.0.1:19445" || configuration.Region != "cn-mock" || configuration.Bucket != "mock-bucket" || configuration.KeyPrefix != "approved" || configuration.GrantTTL != 15*time.Minute || configuration.AccessKeyID != "isolated-id" || configuration.AccessKeySecret != "isolated-secret" {
t.Fatal("legacy OSS environment overrode the approved file and credential references")
}
}
func TestLoadCurrentOSSConfigRejectsUnapprovedDeploymentWithoutLeakingCredentials(t *testing.T) {
t.Setenv("MOCK_OSS_KEY_ID", "isolated-id")
t.Setenv("MOCK_OSS_KEY_SECRET", "isolated-secret")
-17
View File
@@ -1,17 +0,0 @@
package config
import "testing"
func TestLegacyOSSEnvironmentIsNotAConfigurationSource(t *testing.T) {
for _, key := range []string{"DISPATCHER_OSS_REGION", "DISPATCHER_OSS_ENDPOINT", "DISPATCHER_OSS_BUCKET", "DISPATCHER_OSS_ACCESS_KEY_ID", "DISPATCHER_OSS_ACCESS_KEY_SECRET", "DISPATCHER_OSS_KEY_PREFIX"} {
t.Setenv(key, "legacy-value")
}
t.Setenv("DISPATCHER_OSS_GRANT_TTL_SECONDS", "1")
cfg, err := FromEnv()
if err != nil {
t.Fatal(err)
}
if cfg.OSSRegion != "" || cfg.OSSEndpoint != "" || cfg.OSSBucket != "" || cfg.OSSAccessKeyID != "" || cfg.OSSAccessKeySecret != "" || cfg.OSSKeyPrefix != "" || cfg.OSSGrantTTL != 0 {
t.Fatal("legacy environment still supplies OSS settings before the required JSON file")
}
}