返回 DeepSeek-Reasonix
dotenv.go
根目录 / internal / config / dotenv.go
1 package config
2
3 import (
4 "os"
5 "path/filepath"
6 "sort"
7 "strings"
8
9 "github.com/joho/godotenv"
10
11 fileencoding "reasonix/internal/fileutil/encoding"
12 )
13
14 type dotEnvFile struct {
15 Path string
16 Values map[string]string
17 Duplicates []string
18 }
19
20 // loadDotEnv loads Reasonix's global .env for provider credentials. The
21 // workspace .env values returned by loadDotEnvForRoot are ignored here because
22 // loadDotEnv has no Config to carry a workspace-scoped expansion environment.
23 func loadDotEnv() {
24 loadDotEnvForRoot(".")
25 }
26
27 // loadDotEnvForRoot returns workspace .env values for scoped plugin/MCP/proxy
28 // expansion, then loads Reasonix's global .env for provider credentials.
29 // Workspace .env values are deliberately not written into the process
30 // environment, so multiple desktop/ACP workspaces cannot leak tokens into each
31 // other and project files cannot redirect Reasonix's own config/credential
32 // paths.
33 func loadDotEnvForRoot(root string) map[string]string {
34 projectEnv := loadProjectDotEnvForExpansion(root)
35 loadCredentialStoreForRoot(root)
36 return projectEnv
37 }
38
39 func loadProjectDotEnvForExpansion(root string) map[string]string {
40 root = resolveRoot(root)
41 path := ".env"
42 if root != "." {
43 path = filepath.Join(root, ".env")
44 }
45 if current := UserCredentialsPath(); current != "" && samePath(path, current) {
46 return nil
47 }
48 file, ok := readDotEnvFile(path)
49 if !ok {
50 return nil
51 }
52 return file.filtered(func(key string) bool {
53 return !isProjectDotEnvControlKey(key)
54 })
55 }
56
57 func isProjectDotEnvControlKey(key string) bool {
58 key = strings.TrimSpace(key)
59 if key == "" {
60 return true
61 }
62 upper := strings.ToUpper(key)
63 if strings.HasPrefix(upper, "REASONIX_") {
64 return true
65 }
66 switch upper {
67 case "HOME", "USERPROFILE", "APPDATA", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME":
68 return true
69 default:
70 return false
71 }
72 }
73
74 func legacyCredentialsPaths() []string {
75 current := UserCredentialsPath()
76 seen := map[string]bool{}
77 var paths []string
78 add := func(path string) {
79 if path == "" {
80 return
81 }
82 path = filepath.Clean(path)
83 if current != "" && samePath(path, current) {
84 return
85 }
86 if seen[path] {
87 return
88 }
89 seen[path] = true
90 paths = append(paths, path)
91 }
92 if dir := legacyOSSupportDir(); dir != "" {
93 add(filepath.Join(dir, "credentials"))
94 }
95 if dir := userSupportDir(); dir != "" {
96 add(filepath.Join(dir, "credentials"))
97 add(filepath.Join(dir, ".env"))
98 }
99 for _, cfg := range legacyXDGConfigPaths() {
100 add(filepath.Join(filepath.Dir(cfg), "credentials"))
101 }
102 return paths
103 }
104
105 func loadDotEnvFileAs(path string, source CredentialSource) {
106 file, ok := readDotEnvFile(path)
107 if !ok {
108 return
109 }
110 for key, val := range file.Values {
111 key = strings.TrimSpace(key)
112 if key == "" {
113 continue
114 }
115 if _, exists := os.LookupEnv(key); exists && source.Kind != CredentialSourceCredentials {
116 recordExistingCredentialSource(key)
117 continue
118 }
119 if err := os.Setenv(key, val); err == nil && source.Kind != "" {
120 source.Path = path
121 recordCredentialSource(key, val, source)
122 }
123 }
124 }
125
126 func readDotEnvFile(path string) (dotEnvFile, bool) {
127 raw, err := fileencoding.ReadFileUTF8(path)
128 if err != nil {
129 return dotEnvFile{}, false
130 }
131 values, err := godotenv.Unmarshal(string(raw))
132 if err != nil {
133 return dotEnvFile{}, false
134 }
135 return dotEnvFile{
136 Path: path,
137 Values: values,
138 Duplicates: detectDotEnvDuplicateKeys(path),
139 }, true
140 }
141
142 func (f dotEnvFile) filtered(allow func(string) bool) map[string]string {
143 out := map[string]string{}
144 for key, val := range f.Values {
145 key = strings.TrimSpace(key)
146 if key == "" || allow != nil && !allow(key) {
147 continue
148 }
149 out[key] = val
150 }
151 if len(out) == 0 {
152 return nil
153 }
154 return out
155 }
156
157 func (f dotEnvFile) warnings() []string {
158 if len(f.Duplicates) == 0 {
159 return nil
160 }
161 warnings := make([]string, 0, len(f.Duplicates))
162 for _, key := range f.Duplicates {
163 warnings = append(warnings, "duplicate .env key "+key+" in "+f.Path+"; last parsed value wins")
164 }
165 return warnings
166 }
167
168 func detectDotEnvDuplicateKeys(path string) []string {
169 raw, err := fileencoding.ReadFileUTF8(path)
170 if err != nil {
171 return nil
172 }
173 seen := map[string]bool{}
174 dups := map[string]bool{}
175 for _, line := range strings.Split(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n") {
176 values, err := godotenv.Unmarshal(line)
177 if err != nil {
178 continue
179 }
180 for key := range values {
181 key = strings.TrimSpace(key)
182 if key == "" {
183 continue
184 }
185 if seen[key] {
186 dups[key] = true
187 }
188 seen[key] = true
189 }
190 }
191 out := make([]string, 0, len(dups))
192 for key := range dups {
193 out = append(out, key)
194 }
195 sort.Strings(out)
196 return out
197 }
198
199 func envFileValue(path, wantKey string) (string, bool) {
200 file, ok := readDotEnvFile(path)
201 if !ok {
202 return "", false
203 }
204 val, ok := file.Values[wantKey]
205 return val, ok
206 }
207
207 lines GO