package config import ( "errors" "fmt" "io" "net/url" "os" "path/filepath" "runtime" "strings" "golang.org/x/mod/modfile" ) const ( DefaultAPIBaseURL = "https://api.zhecent.com" APIBaseURLEnv = "LIGHTCORE_API_BASE_URL" TokenEnv = "LIGHTCORE_SHOP_CRM_AGENT_TOKEN" EnvFileName = ".env" maxEnvFileSize = 16 << 10 modulePath = "code.zhecent.com/open/shop-crm-agent" ) type Config struct { APIBaseURL string Token string } func Load() (Config, error) { envFile, err := DefaultEnvFile() if err != nil { return Config{}, err } return load(envFile, os.LookupEnv) } func Initialize(token string) (string, error) { envFile, err := DefaultEnvFile() if err != nil { return "", err } if err := writeEnvFile(envFile, token); err != nil { return "", err } return envFile, nil } func DefaultEnvFile() (string, error) { executable, executableErr := os.Executable() if executableErr == nil { if resolved, err := filepath.EvalSymlinks(executable); err == nil { executable = resolved } } workingDir, err := os.Getwd() if err != nil { return "", fmt.Errorf("定位 shop-crm-agent 仓库失败: %w", err) } return resolveEnvFile(executable, workingDir) } func resolveEnvFile(executable, workingDir string) (string, error) { binaryDir := filepath.Dir(executable) if filepath.Base(binaryDir) == "bin" { repoRoot := filepath.Dir(binaryDir) if isRepositoryRoot(repoRoot) { return filepath.Join(repoRoot, EnvFileName), nil } } if !isRepositoryRoot(workingDir) { return "", errors.New("无法定位 shop-crm-agent 仓库根目录,请从仓库根运行 CLI") } return filepath.Join(workingDir, EnvFileName), nil } func load(envFile string, lookupEnv func(string) (string, bool)) (Config, error) { apiBaseURL := DefaultAPIBaseURL if rawAPIBaseURL, exists := lookupEnv(APIBaseURLEnv); exists && strings.TrimSpace(rawAPIBaseURL) != "" { apiBaseURL = normalizeBaseURL(rawAPIBaseURL) if err := validateAPIBaseURL(apiBaseURL); err != nil { return Config{}, err } } if rawToken, exists := lookupEnv(TokenEnv); exists && strings.TrimSpace(rawToken) != "" { token, err := normalizeToken(rawToken) if err != nil { return Config{}, err } return Config{APIBaseURL: apiBaseURL, Token: token}, nil } if apiBaseURL != DefaultAPIBaseURL { return Config{}, fmt.Errorf("覆盖 %s 时必须同时设置 %s", APIBaseURLEnv, TokenEnv) } token, err := readEnvFile(envFile) if err != nil { return Config{}, err } return Config{APIBaseURL: apiBaseURL, Token: token}, nil } func readEnvFile(path string) (string, error) { pathInfo, err := os.Lstat(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return "", errors.New("CRM Agent 尚未初始化,请运行 shop-crm-agent init") } return "", fmt.Errorf("读取 CRM Agent .env 信息失败: %w", err) } if !pathInfo.Mode().IsRegular() { return "", errors.New("CRM Agent .env 必须是普通文件") } file, err := os.Open(path) if err != nil { return "", fmt.Errorf("打开 CRM Agent .env 失败: %w", err) } defer file.Close() info, err := file.Stat() if err != nil { return "", fmt.Errorf("读取 CRM Agent .env 信息失败: %w", err) } if !info.Mode().IsRegular() || !os.SameFile(pathInfo, info) { return "", errors.New("CRM Agent .env 必须是普通文件") } if runtime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 { return "", errors.New("CRM Agent .env 不得允许组或其他用户访问,请设置为 0600 权限") } data, err := io.ReadAll(io.LimitReader(file, maxEnvFileSize+1)) if err != nil { return "", fmt.Errorf("读取 CRM Agent .env 失败: %w", err) } if len(data) > maxEnvFileSize { return "", errors.New("CRM Agent .env 超过 16 KiB 限制") } contents := strings.TrimSuffix(string(data), "\n") contents = strings.TrimSuffix(contents, "\r") if strings.ContainsAny(contents, "\r\n\x00") { return "", errors.New("CRM Agent .env 格式无效,请重新运行 shop-crm-agent init") } prefix := TokenEnv + "=" if !strings.HasPrefix(contents, prefix) { return "", errors.New("CRM Agent .env 缺少凭证,请重新运行 shop-crm-agent init") } return normalizeToken(strings.TrimPrefix(contents, prefix)) } func writeEnvFile(path, token string) error { return writeEnvFileWithReplace(path, token, replaceFile) } func writeEnvFileWithReplace(path, token string, replace func(string, string) error) error { token, err := normalizeToken(token) if err != nil { return err } contents := TokenEnv + "=" + token + "\n" if len(contents) > maxEnvFileSize { return errors.New("CRM Agent Token 超过 .env 大小限制") } if info, err := os.Lstat(path); err == nil { if !info.Mode().IsRegular() { return errors.New("CRM Agent .env 已存在且不是普通文件,拒绝覆盖") } } else if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("检查 CRM Agent .env 失败: %w", err) } tempFile, err := os.CreateTemp(filepath.Dir(path), EnvFileName+".tmp-*") if err != nil { return fmt.Errorf("创建 CRM Agent .env 临时文件失败: %w", err) } tempPath := tempFile.Name() defer func() { _ = tempFile.Close() _ = os.Remove(tempPath) }() if runtime.GOOS != "windows" { if err := tempFile.Chmod(0o600); err != nil { return fmt.Errorf("设置 CRM Agent .env 权限失败: %w", err) } } if _, err := io.WriteString(tempFile, contents); err != nil { return fmt.Errorf("写入 CRM Agent .env 失败: %w", err) } if err := tempFile.Sync(); err != nil { return fmt.Errorf("同步 CRM Agent .env 失败: %w", err) } if err := tempFile.Close(); err != nil { return fmt.Errorf("关闭 CRM Agent .env 失败: %w", err) } if err := replace(tempPath, path); err != nil { return fmt.Errorf("替换 CRM Agent .env 失败: %w", err) } return nil } func normalizeToken(value string) (string, error) { token := strings.TrimSpace(value) if token == "" { return "", errors.New("CRM Agent Token 不能为空") } if len(token) > maxEnvFileSize || strings.ContainsAny(token, "\r\n\x00") { return "", errors.New("CRM Agent Token 格式无效") } return token, nil } func normalizeBaseURL(value string) string { return strings.TrimRight(strings.TrimSpace(value), "/") } func validateAPIBaseURL(value string) error { parsed, err := url.Parse(value) if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { return errors.New("CRM Agent API Base URL 必须是有效的 http/https URL") } return nil } func isRepositoryRoot(path string) bool { data, err := os.ReadFile(filepath.Join(path, "go.mod")) if err != nil { return false } return modfile.ModulePath(data) == modulePath }