Skip to content

Commit e489ba6

Browse files
committed
Merge branch 'main' into feature/dropdown-menu-refactor
# Conflicts: # src/ui/App.tsx
2 parents ef875a0 + a300172 commit e489ba6

25 files changed

Lines changed: 784 additions & 119 deletions

.deepcode/AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ src/
1515
│ ├── MessageView.tsx # Renders assistant/tool messages with markdown
1616
│ ├── SessionList.tsx # Session picker for /resume
1717
│ └── ...
18+
├── mcp/
19+
│ ├── mcp-client.ts # MCP client — JSON-RPC communication with MCP servers
20+
│ └── mcp-manager.ts # MCP manager — lifecycle, tool registration, execution
21+
├── common/
22+
│ ├── file-utils.ts # File read/write with encoding and diff preview
23+
│ ├── shell-utils.ts # Shell path resolution (Git Bash, zsh, bash)
24+
│ ├── state.ts # In-memory file state and snippet tracking
25+
│ └── runtime.ts # Tool validation runtime helpers (executeValidatedTool, semanticBoolean)
1826
├── tools/
1927
│ ├── executor.ts # ToolExecutor — dispatches tool calls to handlers
2028
│ ├── bash-handler.ts # Executes shell commands

docs/configuration.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Deep Code 配置
2+
3+
## 配置层级
4+
5+
配置按以下优先级顺序应用(数字较小的会被数字较大的覆盖):
6+
7+
| 层级 | 配置来源 | 说明 |
8+
| ---- | ------------ | ------------------------------------------- |
9+
| 1 | 默认值 | 应用程序内硬编码的默认值 |
10+
| 2 | 用户设置文件 | 当前用户的全局设置 |
11+
| 3 | 项目设置文件 | 项目特定的设置 |
12+
| 4 | 环境变量 | 系统范围或会话特定的变量 |
13+
14+
## 设置文件
15+
16+
Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两个层级的存放位置:
17+
18+
| 文件类型 | 位置 | 作用范围 |
19+
| ------------ | ---------------------------------- | ---------------------------------------------------- |
20+
| 用户设置文件 | `~/.deepcode/settings.json` | 适用于当前用户的所有 Deep Code 会话。 |
21+
| 项目设置文件 | `项目根目录/.deepcode/settings.json` | 仅在该特定项目中运行 Deep Code 时生效。项目设置会覆盖用户设置。 |
22+
23+
### `settings.json` 中的可用设置
24+
25+
以下是 `settings.json` 支持的全部顶层字段,以及 `env` 内部支持的子字段:
26+
27+
| 字段 | 类型 | 说明 |
28+
| -------------------- | --------- | ------------------------------------------------------------------- |
29+
| `env` | object | 环境变量分组(见下方子字段表) |
30+
| `model` | string | 模型名称。优先级高于 `env.MODEL` |
31+
| `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) |
32+
| `reasoningEffort` | string | 推理强度,可选 `"high"``"max"`(默认 `"max"`|
33+
| `debugLogEnabled` | boolean | 是否启用调试日志输出(默认 `false`|
34+
| `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) |
35+
| `webSearchTool` | string | 自定义联网搜索脚本的完整路径 |
36+
| `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) |
37+
38+
#### `env` 子字段
39+
40+
| 字段 | 类型 | 说明 |
41+
| ---------- | ------ | ------------------------------------------------------------------ |
42+
| `MODEL` | string | 模型名称。例如 `"deepseek-v4-pro"``"deepseek-v4-flash"` |
43+
| `BASE_URL` | string | API 请求的基础 URL。例如 `"https://api.deepseek.com"` |
44+
| `API_KEY` | string | API 密钥 |
45+
| `THINKING_ENABLED` | string | 是否启用思考模式 |
46+
| `REASONING_EFFORT` | string | 推理强度 |
47+
| `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 |
48+
| `<其他任意KEY>` | string | 自定义环境变量 |
49+
50+
#### `thinkingEnabled` — 思考模式
51+
52+
是否启用 DeepSeek 思考模式。设置为 `true` 启用、`false` 禁用。
53+
54+
- 对于 `deepseek-v4-pro``deepseek-v4-flash`,思考模式**默认启用**
55+
- 对于其他模型,思考模式**默认关闭**
56+
57+
#### `reasoningEffort` — 推理强度
58+
59+
当思考模式启用时,控制模型思考的深度:
60+
61+
|| 说明 |
62+
| ------ | --------------------------------- |
63+
| `max` | 最大推理深度(默认值) |
64+
| `high` | 较高推理深度,token消耗相对较小 |
65+
66+
#### `notify` — 任务完成通知
67+
68+
设置一个 Shell 脚本的完整路径。当 AI 助手完成一轮任务后,会自动执行该脚本,可用于发送通知(如 Slack 消息)。
69+
70+
```json
71+
{
72+
"notify": "/path/to/slack-notify.sh"
73+
}
74+
```
75+
76+
#### `webSearchTool` — 自定义联网搜索
77+
78+
Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径:
79+
80+
```json
81+
{
82+
"webSearchTool": "/path/to/my-search-script.sh"
83+
}
84+
```
85+
86+
脚本接收一个搜索查询参数,输出 JSON 格式的结果供 AI 使用。
87+
88+
#### `mcpServers` — MCP 服务器
89+
90+
MCP(Model Context Protocol)服务器配置。值是键值对,键为服务名称,值为服务器配置对象。
91+
92+
```json
93+
{
94+
"mcpServers": {
95+
"<服务名>": {
96+
"command": "npx",
97+
"args": ["-y", "@modelcontextprotocol/server-github"],
98+
"env": {
99+
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
100+
}
101+
}
102+
}
103+
}
104+
```
105+
106+
| McpServerConfig 字段 | 类型 | 必填 | 说明 |
107+
| -------------------- | -------- | ---- | -------------------------------------------------------------------- |
108+
| `command` | string || 可执行文件路径或命令(如 `npx``node``python`|
109+
| `args` | string[] || 传递给命令的参数列表 |
110+
| `env` | object || 传递给 MCP 服务器进程的环境变量 |
111+
112+
> `command``npx` 时,Deep Code 会自动在参数前补充 `-y`
113+
114+
详细 MCP 使用说明请参考 [mcp.md](mcp.md)
115+
116+
117+
#### `debugLogEnabled` — 调试日志
118+
119+
设为 `true` 可让程序输出详细的调试日志(默认 `false`),用于排查 API 调用和工具执行的问题。
120+
121+
## 环境变量优先级
122+
123+
环境变量是配置应用程序的常用方式,尤其适用于敏感信息(如 api-key)或可能在不同环境之间更改的设置。
124+
125+
### 优先级原则
126+
127+
环境变量优先级遵循“越具体、越局部的配置,优先级越高”和“env文件默认保护现有环境,系统变量高于env文件”的覆盖逻辑。(settings.json的env对象可以认为是一种env文件)
128+
129+
优先级层级 (由低到高)
130+
1. settings.json 外层的 env:这是针对整个工具及其所有子进程的通用配置(全局变量)。可被外层环境变量覆盖,但环境变量KEY会移除`DEEPCODE_`前缀。
131+
2. settings.json mcpServers 内定义的 env:这是针对特定 MCP 服务的最具体配置(局部变量)。可被外层环境变量覆盖,但环境变量KEY会移除`MCP_`前缀。
132+
3. Shell 环境系统变量:操作系统层面的环境变量。
133+
134+
### 场景
135+
136+
#### 一、设置模型的api_key, base_url
137+
138+
按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以api_key为例):
139+
140+
1. 硬编码默认值: `""`
141+
2. 用户级settings.json: `{"env": {"API_KEY": "abc123"}}`
142+
3. 项目级settings.json: `{"env": {"API_KEY": "abc123"}}`
143+
4. 系统环境变量: `DEEPCODE_API_KEY=abc123 deepcode`
144+
145+
#### 二、设置模型的model, thinkingEnabled, reasoningEffort
146+
147+
按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以thinkingEnabled为例):
148+
149+
1. 硬编码默认值: `true`
150+
2. 用户级settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
151+
3. 用户级settings.json: `{"thinkingEnabled": true}`
152+
4. 项目级settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
153+
5. 项目级settings.json: `{"thinkingEnabled": true}`
154+
6. 系统环境变量: `DEEPCODE_THINKING_ENABLED=true deepcode`
155+
156+
#### 三、设置启动notify, webSearchTool等外挂脚本的环境变量
157+
158+
按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以notify为例):
159+
160+
1. 硬编码默认值:`os.environ.get('WEBHOOK', '...') # notify脚本代码`
161+
2. 用户级settings.json: `{"env": {"WEBHOOK": "..."}}`
162+
3. 项目级settings.json: `{"env": {"WEBHOOK": "true"}}`
163+
4. 系统环境变量: `DEEPCODE_WEBHOOK=... deepcode`
164+
165+
#### 四、设置MCP Service的环境变量
166+
167+
按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以github MCP server为例):
168+
169+
1. 用户级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
170+
2. 用户级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
171+
3. 项目级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
172+
4. 项目级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
173+
5. 系统环境变量: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`

docs/mcp.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,7 @@ MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,
146146

147147
## 使用 MCP
148148

149-
配置完成后,启动 `deepcode`,使用 `/mcp` 命令管理 MCP 连接:
150-
151-
- `/mcp` — 查看已配置的 MCP 服务器状态
152-
- `/mcp add` — 添加新的 MCP 服务器
153-
- `/mcp remove` — 移除 MCP 服务器
154-
- `/mcp list` — 列出所有已连接的 MCP 服务器及其工具
149+
配置完成后,启动 `deepcode`,在聊天中输入 `/mcp` 即可查看所有已配置的 MCP 服务器状态以及每个服务器提供的工具列表。
155150

156151
在对话中直接使用 MCP 工具名称即可调用,例如:
157152

@@ -172,7 +167,7 @@ MCP 工具名称由三部分组成:`mcp__<服务名>__<工具名>`
172167
| playwright | browser_navigate | `mcp__playwright__browser_navigate` |
173168
| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` |
174169

175-
你可以通过 `/mcp list` 查看每个服务器提供的具体工具列表。
170+
你可以通过 `/mcp` 查看每个服务器提供的具体工具列表。
176171

177172
## 故障排查
178173

src/cli.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { render } from "ink";
33
import { App } from "./ui";
4-
import { setShellIfWindows } from "./tools/shell-utils";
4+
import { setShellIfWindows } from "./common/shell-utils";
55
import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck";
66

77
const args = process.argv.slice(2);
@@ -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",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod";
2-
import type { ToolExecutionContext, ToolExecutionResult } from "./executor";
2+
import type { ToolExecutionContext, ToolExecutionResult } from "../tools/executor";
33

44
export type ValidationResult = { ok: true; input: Record<string, unknown> } | { ok: false; error: string };
55

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ import * as path from "path";
55
import * as pathWin32 from "path/win32";
66

77
const WINDOWS_GIT_LOCATIONS = ["C:\\Program Files\\Git\\cmd\\git.exe", "C:\\Program Files (x86)\\Git\\cmd\\git.exe"];
8+
const WINDOWS_BASH_LOCATIONS = ["C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files (x86)\\Git\\bin\\bash.exe"];
89

910
const NUL_REDIRECT_REGEX = /(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])/g;
1011
let cachedGitBashPath: string | null = null;
1112

1213
export type ShellKind = "bash" | "zsh" | "unknown";
1314

15+
type WindowsGitBashLookup = {
16+
findExecutableCandidates: (executable: string) => string[];
17+
findGitExecPath: () => string | null;
18+
existsSync: (candidate: string) => boolean;
19+
};
20+
1421
export function setShellIfWindows(): void {
1522
if (process.platform !== "win32") {
1623
return;
@@ -23,16 +30,30 @@ export function findGitBashPath(): string {
2330
return cachedGitBashPath;
2431
}
2532

26-
for (const gitPath of findAllWindowsExecutableCandidates("git")) {
27-
const bashPath = pathWin32.join(gitPath, "..", "..", "bin", "bash.exe");
28-
if (fs.existsSync(bashPath)) {
29-
cachedGitBashPath = bashPath;
30-
return bashPath;
31-
}
33+
const bashPath = resolveWindowsGitBashPath({
34+
findExecutableCandidates: findAllWindowsExecutableCandidates,
35+
findGitExecPath,
36+
existsSync: fs.existsSync,
37+
});
38+
if (bashPath) {
39+
cachedGitBashPath = bashPath;
40+
return bashPath;
3241
}
3342

3443
throw new Error(
35-
"Deep Code on Windows requires Git Bash. Install Git Bash for Windows and ensure bash.exe is available in PATH."
44+
"Deep Code on Windows requires Git Bash. Install Git for Windows, or ensure Git's bash.exe is available in PATH."
45+
);
46+
}
47+
48+
export function resolveWindowsGitBashPath(lookup: WindowsGitBashLookup): string | null {
49+
return firstExistingWindowsPath(
50+
[
51+
...lookup.findExecutableCandidates("bash"),
52+
...WINDOWS_BASH_LOCATIONS,
53+
...gitExecPathToBashCandidates(lookup.findGitExecPath()),
54+
...lookup.findExecutableCandidates("git").flatMap(gitExecutableToBashCandidates),
55+
],
56+
lookup.existsSync
3657
);
3758
}
3859

@@ -128,9 +149,10 @@ export function toNativeCwd(shellCwd: string): string {
128149
return posixPathToWindowsPath(shellCwd);
129150
}
130151

131-
export function buildShellEnv(shellPath: string): NodeJS.ProcessEnv {
152+
export function buildShellEnv(shellPath: string, extraEnv: Record<string, string> = {}): NodeJS.ProcessEnv {
132153
const env: NodeJS.ProcessEnv = {
133154
...process.env,
155+
...extraEnv,
134156
SHELL: shellPath,
135157
GIT_EDITOR: "true",
136158
};
@@ -145,26 +167,76 @@ export function buildShellEnv(shellPath: string): NodeJS.ProcessEnv {
145167
}
146168

147169
function findAllWindowsExecutableCandidates(executable: string): string[] {
148-
const extraCandidates = executable === "git" ? WINDOWS_GIT_LOCATIONS : [];
170+
const extraCandidates =
171+
executable === "git" ? WINDOWS_GIT_LOCATIONS : executable === "bash" ? WINDOWS_BASH_LOCATIONS : [];
149172

150173
try {
151174
const output = execFileSync("where.exe", [executable], {
152175
encoding: "utf8",
153176
stdio: ["ignore", "pipe", "ignore"],
154177
windowsHide: true,
155178
});
156-
return filterWindowsExecutableCandidates([
157-
...output
158-
.split(/\r?\n/)
159-
.map((line) => line.trim())
160-
.filter(Boolean),
161-
...extraCandidates,
162-
]);
179+
let whereResults = output
180+
.split(/\r?\n/)
181+
.map((line) => line.trim())
182+
.filter(Boolean);
183+
if (executable === "bash") {
184+
// Skip WSL's deprecated bash.exe launcher (C:\Windows\System32\bash.exe).
185+
// It would start commands inside the Linux distro instead of the Windows host,
186+
// breaking all path translations and tool invocations.
187+
whereResults = whereResults.filter((candidate) => !/system32[\\/]bash\.exe$/i.test(candidate));
188+
}
189+
return filterWindowsExecutableCandidates([...whereResults, ...extraCandidates]);
163190
} catch {
164191
return filterWindowsExecutableCandidates(extraCandidates);
165192
}
166193
}
167194

195+
function findGitExecPath(): string | null {
196+
try {
197+
const output = execFileSync("git", ["--exec-path"], {
198+
encoding: "utf8",
199+
stdio: ["ignore", "pipe", "ignore"],
200+
windowsHide: true,
201+
}).trim();
202+
return output || null;
203+
} catch {
204+
return null;
205+
}
206+
}
207+
208+
function gitExecPathToBashCandidates(execPath: string | null): string[] {
209+
if (!execPath) {
210+
return [];
211+
}
212+
213+
const normalized = execPath.replace(/\//g, "\\");
214+
return [
215+
pathWin32.join(normalized, "..", "..", "..", "bin", "bash.exe"),
216+
pathWin32.join(normalized, "..", "..", "bin", "bash.exe"),
217+
];
218+
}
219+
220+
function gitExecutableToBashCandidates(gitPath: string): string[] {
221+
return [pathWin32.join(gitPath, "..", "..", "bin", "bash.exe"), pathWin32.join(gitPath, "..", "bin", "bash.exe")];
222+
}
223+
224+
function firstExistingWindowsPath(candidates: string[], existsSync: (candidate: string) => boolean): string | null {
225+
const seen = new Set<string>();
226+
for (const candidate of candidates) {
227+
const normalized = pathWin32.resolve(candidate);
228+
const key = normalized.toLowerCase();
229+
if (seen.has(key)) {
230+
continue;
231+
}
232+
seen.add(key);
233+
if (getShellKind(normalized) === "bash" && existsSync(normalized)) {
234+
return normalized;
235+
}
236+
}
237+
return null;
238+
}
239+
168240
function filterWindowsExecutableCandidates(candidates: string[]): string[] {
169241
const cwd = process.cwd().toLowerCase();
170242
const seen = new Set<string>();
File renamed without changes.

0 commit comments

Comments
 (0)