|
| 1 | +# 忽略目录 @ 引用 |
| 2 | + |
| 3 | +**Date:** 2026-01-21 |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +在当前的 `src/at.ts` 实现中,当用户使用 `@目录` 语法时,系统会递归遍历该目录下的所有文件,并将所有文件的内容注入到 prompt 中。这种行为会导致: |
| 8 | + |
| 9 | +1. **性能问题**: 大目录会导致大量文件被读取,消耗时间和资源 |
| 10 | +2. **token 浪费**: 用户可能并不需要目录下的所有文件内容 |
| 11 | +3. **用户体验差**: 用户无法预期会有多少内容被注入 |
| 12 | + |
| 13 | +用户希望当使用 `@目录` 时,系统完全不处理该路径,保持静默(不添加任何内容,也不显示警告)。 |
| 14 | + |
| 15 | +## Discussion |
| 16 | + |
| 17 | +### 关键问题与决策 |
| 18 | + |
| 19 | +**Q1: 当用户使用 `@目录` 时,希望如何处理?** |
| 20 | +- ✅ **选择**: 保持现状不做任何处理(完全忽略) |
| 21 | +- ❌ 只显示目录结构树,不包含任何文件内容 |
| 22 | +- ❌ 显示目录结构树,但允许用户手动选择要读取的文件 |
| 23 | +- ❌ 只读取目录下的第一层文件,不递归子目录 |
| 24 | +- ❌ 根据文件类型或规则智能过滤 |
| 25 | + |
| 26 | +**Q2: 当识别到 @ 后面是目录时,具体应该怎么做?** |
| 27 | +- ✅ **选择**: 完全忽略 `@目录`,不添加任何内容 |
| 28 | +- ❌ 显示警告提示,告知用户目录不支持 |
| 29 | +- ❌ 显示目录结构树,但不读取文件内容 |
| 30 | + |
| 31 | +### 探索的方案 |
| 32 | + |
| 33 | +#### 方案 1: 早期过滤 (✅ 最终选择) |
| 34 | +在 `extractAtPaths()` 阶段就检测并过滤掉目录路径,只保留文件路径。 |
| 35 | + |
| 36 | +**优点:** |
| 37 | +- 性能最优,避免后续不必要的目录遍历和文件系统操作 |
| 38 | +- 代码改动最小,逻辑最清晰 |
| 39 | +- 对用户完全透明,不会有任何输出 |
| 40 | + |
| 41 | +**缺点:** |
| 42 | +- 用户可能不知道为什么 `@目录` 没有效果(静默失败) |
| 43 | + |
| 44 | +**复杂度:** 低 |
| 45 | + |
| 46 | +#### 方案 2: 中期过滤 |
| 47 | +在 `getContent()` 方法中分类文件和目录时,直接丢弃目录分支,不调用 `getAllFilesInDirectory()`。 |
| 48 | + |
| 49 | +**优点:** |
| 50 | +- 逻辑清晰,在分类阶段就明确处理策略 |
| 51 | +- 仍然保持较好的性能 |
| 52 | + |
| 53 | +**缺点:** |
| 54 | +- 相比方案1有额外的文件系统检查(`statSync`) |
| 55 | +- 代码改动稍多一些 |
| 56 | + |
| 57 | +#### 方案 3: 添加验证提示 |
| 58 | +在识别到目录时,返回一个友好的提示信息,告知用户目录不被支持。 |
| 59 | + |
| 60 | +**优点:** |
| 61 | +- 用户体验更好,明确知道为什么没有内容 |
| 62 | +- 可以提供使用建议 |
| 63 | + |
| 64 | +**缺点:** |
| 65 | +- 会在 prompt 中添加内容(虽然是提示信息) |
| 66 | +- 与"完全不处理"的需求不完全一致 |
| 67 | + |
| 68 | +## Approach |
| 69 | + |
| 70 | +采用**方案 1: 早期过滤**策略。 |
| 71 | + |
| 72 | +**核心思路:** |
| 73 | +在 `extractAtPaths()` 方法中,当解析完 `@路径` 后,立即进行文件系统检查。如果检测到路径指向目录,则直接跳过该路径,不将其加入返回的 `AtPath[]` 数组。 |
| 74 | + |
| 75 | +**数据流变化:** |
| 76 | + |
| 77 | +``` |
| 78 | +当前流程: |
| 79 | +用户输入 @目录 |
| 80 | +→ extractAtPaths 提取所有路径 |
| 81 | +→ getContent 分类文件/目录 |
| 82 | +→ 目录被遍历,所有文件被注入 |
| 83 | +
|
| 84 | +新流程: |
| 85 | +用户输入 @目录 |
| 86 | +→ extractAtPaths 提取路径时检测到是目录 |
| 87 | +→ 直接跳过,不加入返回数组 |
| 88 | +→ getContent 收到空数组或只有文件的数组 |
| 89 | +→ 目录完全不被处理 |
| 90 | +``` |
| 91 | + |
| 92 | +**预期效果:** |
| 93 | +- `@文件` 继续正常工作 |
| 94 | +- `@目录` 被完全忽略,不产生任何输出 |
| 95 | +- 混合使用时(如 `@file1 @dir1 @file2`),只处理文件部分 |
| 96 | + |
| 97 | +## Architecture |
| 98 | + |
| 99 | +### 修改点 |
| 100 | + |
| 101 | +**核心修改位置:** `src/at.ts` 中的 `extractAtPaths()` 方法 |
| 102 | + |
| 103 | +### 实现细节 |
| 104 | + |
| 105 | +1. **在解析路径后立即添加目录检查:** |
| 106 | + - 将相对路径转换为绝对路径: `path.resolve(this.cwd, filePath)` |
| 107 | + - 检查路径是否存在: `fs.existsSync(absolutePath)` |
| 108 | + - 如果存在,检查是否为目录: `fs.statSync(absolutePath).isDirectory()` |
| 109 | + - 如果是目录,执行 `continue` 跳过本次循环 |
| 110 | + |
| 111 | +2. **代码插入位置:** |
| 112 | + - 在 `extractAtPaths()` 方法的 while 循环体内 |
| 113 | + - 在解析完 `filePath` 和 `lineRange` 之后 |
| 114 | + - 在将路径添加到 `pathsMap` 之前 |
| 115 | + |
| 116 | +3. **伪代码示例:** |
| 117 | +```typescript |
| 118 | +// 在 extractAtPaths() 方法中 |
| 119 | +while (match !== null) { |
| 120 | + // ... 解析 filePath 和 lineRange ... |
| 121 | + |
| 122 | + // 🆕 添加目录检查 |
| 123 | + try { |
| 124 | + const absolutePath = path.resolve(this.cwd, filePath); |
| 125 | + if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) { |
| 126 | + match = regex.exec(prompt); |
| 127 | + continue; // 跳过目录 |
| 128 | + } |
| 129 | + } catch { |
| 130 | + // 权限问题等异常,保持原逻辑,让后续处理 |
| 131 | + } |
| 132 | + |
| 133 | + // 创建 key 并添加到 pathsMap |
| 134 | + const key = `${filePath}:${lineRange ? ... : 'all'}`; |
| 135 | + pathsMap.set(key, { path: filePath, lineRange }); |
| 136 | + |
| 137 | + match = regex.exec(prompt); |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +### 错误处理 |
| 142 | + |
| 143 | +1. **文件系统访问异常:** |
| 144 | + - 当 `fs.existsSync()` 或 `fs.statSync()` 因权限问题抛出异常时 |
| 145 | + - 采用保守策略: 捕获异常后,仍然将路径加入 `pathsMap` |
| 146 | + - 理由: 让后续的 `getContent()` 方法处理,保持错误处理的一致性 |
| 147 | + |
| 148 | +2. **符号链接处理:** |
| 149 | + - `fs.statSync()` 会跟随符号链接,返回目标的状态 |
| 150 | + - 如果符号链接指向目录,会被正确过滤 |
| 151 | + - 如果符号链接损坏,会抛出异常,按上述策略处理 |
| 152 | + |
| 153 | +### 边界场景 |
| 154 | + |
| 155 | +| 场景 | 当前行为 | 新行为 | 说明 | |
| 156 | +|------|---------|--------|------| |
| 157 | +| `@src/` | 遍历所有文件并注入 | 完全忽略 | ✅ 符合需求 | |
| 158 | +| `@src/index.ts` | 读取文件内容 | 读取文件内容 | ✅ 保持不变 | |
| 159 | +| `@不存在的路径` | 后续报错 | 后续报错 | ✅ 保持不变 | |
| 160 | +| `@src/:10-20` | 目录+行号(无意义) | 完全忽略 | ✅ 合理忽略 | |
| 161 | +| `@file1 @dir1 @file2` | 3个都处理 | 只处理 file1 和 file2 | ✅ 符合需求 | |
| 162 | + |
| 163 | +### 性能影响 |
| 164 | + |
| 165 | +- **增加的开销**: 每个 `@路径` 会增加 1-2 次文件系统调用 |
| 166 | +- **减少的开销**: 避免了目录遍历,尤其是大目录时性能提升显著 |
| 167 | +- **净影响**: 对于纯文件场景,性能影响可忽略不计;对于包含目录的场景,性能显著提升 |
| 168 | + |
| 169 | +### 测试策略 |
| 170 | + |
| 171 | +1. **基础功能测试:** |
| 172 | + - ✅ `@文件路径` 仍然正常工作 |
| 173 | + - ✅ `@目录路径` 被完全忽略 |
| 174 | + - ✅ `@文件:10-20` 带行号的文件正常工作 |
| 175 | + - ✅ `@目录:10` 带行号的目录被忽略 |
| 176 | + |
| 177 | +2. **混合场景测试:** |
| 178 | + ``` |
| 179 | + 输入: "@src/index.ts @src/ @README.md" |
| 180 | + 预期: 只注入 index.ts 和 README.md 的内容,src/ 被忽略 |
| 181 | + ``` |
| 182 | + |
| 183 | +3. **边界测试:** |
| 184 | + - 空目录的处理 |
| 185 | + - 符号链接指向目录的处理 |
| 186 | + - 没有权限访问的路径处理 |
| 187 | + - 不存在的路径(保持原有错误提示) |
| 188 | + |
| 189 | +4. **回归测试:** |
| 190 | + - 确保原有的文件处理逻辑完全不受影响 |
| 191 | + - 确保 `renderFilesToXml()` 方法仍然正常工作 |
| 192 | + - 确保行号范围功能不受影响 |
| 193 | + |
| 194 | +### 影响范围 |
| 195 | + |
| 196 | +- **修改文件**: 仅 `src/at.ts` |
| 197 | +- **修改方法**: 仅 `extractAtPaths()` 方法 |
| 198 | +- **代码行数**: 约 5-10 行 |
| 199 | +- **向后兼容**: 完全兼容,所有现有文件处理功能不受影响 |
| 200 | +- **不需要修改的部分**: |
| 201 | + - `getContent()` 方法 |
| 202 | + - `getAllFilesInDirectory()` 方法(将不会被调用) |
| 203 | + - `renderDirectoriesToTree()` 方法(将不会被调用) |
| 204 | + - `renderFilesToXml()` 方法 |
0 commit comments