Skip to content

Commit 33e7dbd

Browse files
committed
feat: enhance environment variable handling and configuration resolution
1 parent 4fff1ed commit 33e7dbd

13 files changed

Lines changed: 444 additions & 92 deletions

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ MCP(Model Context Protocol)服务器配置。值是键值对,键为服务
111111

112112
> `command``npx` 时,Deep Code 会自动在参数前补充 `-y`
113113
114-
详细 MCP 使用说明请参考 [docs/mcp.md](docs/mcp.md)
114+
详细 MCP 使用说明请参考 [mcp.md](mcp.md)
115115

116116

117117
#### `debugLogEnabled` — 调试日志

src/cli.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ if (args.includes("--help") || args.includes("-h")) {
2323
" deepcode --help Show this help",
2424
"",
2525
"Configuration:",
26-
" ~/.deepcode/settings.json API key, model, base URL",
26+
" ~/.deepcode/settings.json User-level API key, model, base URL",
27+
" ./.deepcode/settings.json Project-level settings",
2728
" ~/.agents/skills/*/SKILL.md User-level skills",
2829
" ./.agents/skills/*/SKILL.md Project-level skills",
2930
" ./.deepcode/skills/*/SKILL.md Legacy project-level skills",

src/common/shell-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@ export function toNativeCwd(shellCwd: string): string {
128128
return posixPathToWindowsPath(shellCwd);
129129
}
130130

131-
export function buildShellEnv(shellPath: string): NodeJS.ProcessEnv {
131+
export function buildShellEnv(shellPath: string, extraEnv: Record<string, string> = {}): NodeJS.ProcessEnv {
132132
const env: NodeJS.ProcessEnv = {
133133
...process.env,
134+
...extraEnv,
134135
SHELL: shellPath,
135136
GIT_EDITOR: "true",
136137
};

src/notify.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export function launchNotifyScript(
2727
notifyPath: string | undefined,
2828
durationMs: number,
2929
workingDirectory?: string,
30-
spawnProcess: NotifySpawn = spawn as unknown as NotifySpawn
30+
spawnProcess: NotifySpawn = spawn as unknown as NotifySpawn,
31+
configuredEnv: Record<string, string> = {}
3132
): void {
3233
const commandPath = notifyPath?.trim();
3334
if (!commandPath) {
@@ -37,7 +38,7 @@ export function launchNotifyScript(
3738
const options = {
3839
cwd: workingDirectory,
3940
detached: process.platform !== "win32",
40-
env: buildNotifyEnv(durationMs),
41+
env: buildNotifyEnv(durationMs, { ...process.env, ...configuredEnv }),
4142
stdio: "ignore" as const,
4243
};
4344

src/session.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,7 @@ ${skillMd}
941941

942942
async activateSession(sessionId: string, controller?: AbortController): Promise<void> {
943943
const startedAt = Date.now();
944-
const { client, model, baseURL, thinkingEnabled, reasoningEffort, debugLogEnabled, notify } =
944+
const { client, model, baseURL, thinkingEnabled, reasoningEffort, debugLogEnabled, notify, env } =
945945
this.createOpenAIClient();
946946
const now = new Date().toISOString();
947947

@@ -955,12 +955,12 @@ ${skillMd}
955955
this.onAssistantMessage(
956956
this.buildAssistantMessage(
957957
sessionId,
958-
"OpenAI API key not found. Please configure ~/.deepcode/settings.json.",
958+
"OpenAI API key not found. Please configure ~/.deepcode/settings.json or ./.deepcode/settings.json.",
959959
null
960960
),
961961
false
962962
);
963-
this.maybeNotifyTaskCompletion(sessionId, notify, startedAt);
963+
this.maybeNotifyTaskCompletion(sessionId, notify, startedAt, env);
964964
return;
965965
}
966966

@@ -972,7 +972,7 @@ ${skillMd}
972972
failReason: "interrupted",
973973
updateTime: now,
974974
}));
975-
this.maybeNotifyTaskCompletion(sessionId, notify, startedAt);
975+
this.maybeNotifyTaskCompletion(sessionId, notify, startedAt, env);
976976
return;
977977
}
978978

@@ -1114,7 +1114,7 @@ ${skillMd}
11141114
if (this.sessionControllers.get(sessionId) === sessionController) {
11151115
this.sessionControllers.delete(sessionId);
11161116
}
1117-
this.maybeNotifyTaskCompletion(sessionId, notify, startedAt);
1117+
this.maybeNotifyTaskCompletion(sessionId, notify, startedAt, env);
11181118
}
11191119
}
11201120

@@ -1966,7 +1966,12 @@ ${skillMd}
19661966
}
19671967
}
19681968

1969-
private maybeNotifyTaskCompletion(sessionId: string, notifyCommand: string | undefined, startedAt: number): void {
1969+
private maybeNotifyTaskCompletion(
1970+
sessionId: string,
1971+
notifyCommand: string | undefined,
1972+
startedAt: number,
1973+
configuredEnv: Record<string, string> = {}
1974+
): void {
19701975
if (!notifyCommand) {
19711976
return;
19721977
}
@@ -1976,7 +1981,7 @@ ${skillMd}
19761981
return;
19771982
}
19781983

1979-
launchNotifyScript(notifyCommand, Date.now() - startedAt, this.projectRoot);
1984+
launchNotifyScript(notifyCommand, Date.now() - startedAt, this.projectRoot, undefined, configuredEnv);
19801985
}
19811986

19821987
private addSessionProcess(sessionId: string, processId: string | number, command: string): void {

src/settings.ts

Lines changed: 184 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { defaultsToThinkingMode } from "./model-capabilities";
22

3-
export type DeepcodingEnv = {
3+
export type DeepcodingEnv = Record<string, string | undefined> & {
44
MODEL?: string;
55
BASE_URL?: string;
66
API_KEY?: string;
7-
THINKING?: string;
7+
THINKING_ENABLED?: string;
8+
REASONING_EFFORT?: string;
9+
DEBUG_LOG_ENABLED?: string;
810
};
911

1012
export type ReasoningEffort = "high" | "max";
@@ -27,6 +29,7 @@ export type DeepcodingSettings = {
2729
};
2830

2931
export type ResolvedDeepcodingSettings = {
32+
env: Record<string, string>;
3033
apiKey?: string;
3134
baseURL: string;
3235
model: string;
@@ -44,48 +47,203 @@ export type ModelConfigSelection = {
4447
reasoningEffort: ReasoningEffort;
4548
};
4649

47-
function resolveReasoningEffort(value: unknown): ReasoningEffort {
48-
return value === "high" || value === "max" ? value : "max";
50+
export type SettingsProcessEnv = Record<string, string | undefined>;
51+
52+
function resolveReasoningEffort(value: unknown): ReasoningEffort | undefined {
53+
return value === "high" || value === "max" ? value : undefined;
4954
}
5055

51-
function resolveThinkingEnabled(settings: DeepcodingSettings | null | undefined, model: string): boolean {
52-
if (typeof settings?.thinkingEnabled === "boolean") {
53-
return settings.thinkingEnabled;
56+
function parseBoolean(value: unknown): boolean | undefined {
57+
if (typeof value === "boolean") {
58+
return value;
59+
}
60+
if (typeof value !== "string") {
61+
return undefined;
5462
}
5563

56-
const legacyThinking = settings?.env?.THINKING;
57-
if (typeof legacyThinking === "string" && legacyThinking.trim()) {
58-
return legacyThinking.trim().toLowerCase() === "enabled";
64+
const normalized = value.trim().toLowerCase();
65+
if (["1", "true", "enabled", "yes", "on"].includes(normalized)) {
66+
return true;
67+
}
68+
if (["0", "false", "disabled", "no", "off"].includes(normalized)) {
69+
return false;
5970
}
71+
return undefined;
72+
}
6073

61-
return defaultsToThinkingMode(model);
74+
function trimString(value: unknown): string {
75+
return typeof value === "string" ? value.trim() : "";
6276
}
6377

64-
export function resolveSettings(
65-
settings: DeepcodingSettings | null | undefined,
66-
defaults: { model: string; baseURL: string }
78+
function normalizeEnv(env: DeepcodingSettings["env"]): Record<string, string> {
79+
const result: Record<string, string> = {};
80+
if (!env) {
81+
return result;
82+
}
83+
84+
for (const [key, value] of Object.entries(env)) {
85+
if (typeof value === "string") {
86+
result[key] = value;
87+
}
88+
}
89+
return result;
90+
}
91+
92+
export function collectDeepcodeEnv(processEnv: SettingsProcessEnv = process.env): Record<string, string> {
93+
const result: Record<string, string> = {};
94+
for (const [key, value] of Object.entries(processEnv)) {
95+
if (!key.startsWith("DEEPCODE_") || typeof value !== "string") {
96+
continue;
97+
}
98+
const strippedKey = key.slice("DEEPCODE_".length);
99+
if (strippedKey) {
100+
result[strippedKey] = value;
101+
}
102+
}
103+
return result;
104+
}
105+
106+
function extractMcpEnv(env: Record<string, string>): Record<string, string> {
107+
const result: Record<string, string> = {};
108+
for (const [key, value] of Object.entries(env)) {
109+
if (!key.startsWith("MCP_")) {
110+
continue;
111+
}
112+
const strippedKey = key.slice("MCP_".length);
113+
if (strippedKey) {
114+
result[strippedKey] = value;
115+
}
116+
}
117+
return result;
118+
}
119+
120+
function mergeMcpServers(
121+
userSettings: DeepcodingSettings | null | undefined,
122+
projectSettings: DeepcodingSettings | null | undefined,
123+
userEnv: Record<string, string>,
124+
projectEnv: Record<string, string>,
125+
systemEnv: Record<string, string>
126+
): Record<string, McpServerConfig> | undefined {
127+
const userServers = userSettings?.mcpServers ?? {};
128+
const projectServers = projectSettings?.mcpServers ?? {};
129+
const serverNames = new Set([...Object.keys(userServers), ...Object.keys(projectServers)]);
130+
if (serverNames.size === 0) {
131+
return undefined;
132+
}
133+
134+
const userMcpEnv = extractMcpEnv(userEnv);
135+
const projectMcpEnv = extractMcpEnv(projectEnv);
136+
const systemMcpEnv = extractMcpEnv(systemEnv);
137+
const merged: Record<string, McpServerConfig> = {};
138+
139+
for (const name of serverNames) {
140+
const userConfig = userServers[name];
141+
const projectConfig = projectServers[name];
142+
const command = projectConfig?.command ?? userConfig?.command;
143+
if (!command) {
144+
continue;
145+
}
146+
147+
const env = {
148+
...userEnv,
149+
...(userConfig?.env ?? {}),
150+
...userMcpEnv,
151+
...projectEnv,
152+
...(projectConfig?.env ?? {}),
153+
...projectMcpEnv,
154+
...systemEnv,
155+
...systemMcpEnv,
156+
};
157+
const config: McpServerConfig = {
158+
command,
159+
args: projectConfig?.args ?? userConfig?.args,
160+
};
161+
if (Object.keys(env).length > 0) {
162+
config.env = env;
163+
}
164+
merged[name] = config;
165+
}
166+
167+
return Object.keys(merged).length > 0 ? merged : undefined;
168+
}
169+
170+
export function resolveSettingsSources(
171+
userSettings: DeepcodingSettings | null | undefined,
172+
projectSettings: DeepcodingSettings | null | undefined,
173+
defaults: { model: string; baseURL: string },
174+
processEnv: SettingsProcessEnv = process.env
67175
): ResolvedDeepcodingSettings {
68-
const env = settings?.env ?? {};
69-
const topLevelModel = typeof settings?.model === "string" ? settings.model.trim() : "";
70-
const model = topLevelModel || env.MODEL?.trim() || defaults.model;
71-
const notify = typeof settings?.notify === "string" ? settings.notify.trim() : "";
72-
const webSearchTool = typeof settings?.webSearchTool === "string" ? settings.webSearchTool.trim() : "";
176+
const userEnv = normalizeEnv(userSettings?.env);
177+
const projectEnv = normalizeEnv(projectSettings?.env);
178+
const systemEnv = collectDeepcodeEnv(processEnv);
179+
const env = {
180+
...userEnv,
181+
...projectEnv,
182+
...systemEnv,
183+
};
184+
185+
const model =
186+
trimString(systemEnv.MODEL) ||
187+
trimString(projectSettings?.model) ||
188+
trimString(projectEnv.MODEL) ||
189+
trimString(userSettings?.model) ||
190+
trimString(userEnv.MODEL) ||
191+
defaults.model;
73192

74-
const mcpServers = settings?.mcpServers;
193+
const thinkingEnabled =
194+
parseBoolean(systemEnv.THINKING_ENABLED) ??
195+
parseBoolean(projectSettings?.thinkingEnabled) ??
196+
parseBoolean(projectEnv.THINKING_ENABLED) ??
197+
parseBoolean(userSettings?.thinkingEnabled) ??
198+
parseBoolean(userEnv.THINKING_ENABLED) ??
199+
defaultsToThinkingMode(model);
200+
201+
const reasoningEffort =
202+
resolveReasoningEffort(systemEnv.REASONING_EFFORT) ??
203+
resolveReasoningEffort(projectSettings?.reasoningEffort) ??
204+
resolveReasoningEffort(projectEnv.REASONING_EFFORT) ??
205+
resolveReasoningEffort(userSettings?.reasoningEffort) ??
206+
resolveReasoningEffort(userEnv.REASONING_EFFORT) ??
207+
"max";
208+
209+
const debugLogEnabled =
210+
parseBoolean(systemEnv.DEBUG_LOG_ENABLED) ??
211+
parseBoolean(projectSettings?.debugLogEnabled) ??
212+
parseBoolean(projectEnv.DEBUG_LOG_ENABLED) ??
213+
parseBoolean(userSettings?.debugLogEnabled) ??
214+
parseBoolean(userEnv.DEBUG_LOG_ENABLED) ??
215+
false;
216+
217+
const notify =
218+
trimString(systemEnv.NOTIFY) || trimString(projectSettings?.notify) || trimString(userSettings?.notify) || "";
219+
const webSearchTool =
220+
trimString(systemEnv.WEB_SEARCH_TOOL) ||
221+
trimString(projectSettings?.webSearchTool) ||
222+
trimString(userSettings?.webSearchTool) ||
223+
"";
75224

76225
return {
77-
apiKey: env.API_KEY?.trim(),
78-
baseURL: env.BASE_URL?.trim() || defaults.baseURL,
226+
env,
227+
apiKey: trimString(env.API_KEY) || undefined,
228+
baseURL: trimString(env.BASE_URL) || defaults.baseURL,
79229
model,
80-
thinkingEnabled: resolveThinkingEnabled(settings, model),
81-
reasoningEffort: resolveReasoningEffort(settings?.reasoningEffort),
82-
debugLogEnabled: settings?.debugLogEnabled === true,
230+
thinkingEnabled,
231+
reasoningEffort,
232+
debugLogEnabled,
83233
notify: notify || undefined,
84234
webSearchTool: webSearchTool || undefined,
85-
mcpServers,
235+
mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv),
86236
};
87237
}
88238

239+
export function resolveSettings(
240+
settings: DeepcodingSettings | null | undefined,
241+
defaults: { model: string; baseURL: string },
242+
processEnv: SettingsProcessEnv = process.env
243+
): ResolvedDeepcodingSettings {
244+
return resolveSettingsSources(settings, null, defaults, processEnv);
245+
}
246+
89247
export function modelConfigKey(config: Pick<ModelConfigSelection, "thinkingEnabled" | "reasoningEffort">): string {
90248
return config.thinkingEnabled ? `thinking:${config.reasoningEffort}` : "thinking:none";
91249
}

0 commit comments

Comments
 (0)