Skip to content

feat(chat): EnhancedInput 支持 Claude / 斜杠命令补全(commands/skills + 自动学习)#333

Merged
J3n5en merged 1 commit intoJ3n5en:mainfrom
hmhm2022:feat/claude-completions
Mar 5, 2026
Merged

feat(chat): EnhancedInput 支持 Claude / 斜杠命令补全(commands/skills + 自动学习)#333
J3n5en merged 1 commit intoJ3n5en:mainfrom
hmhm2022:feat/claude-completions

Conversation

@hmhm2022
Copy link
Contributor

@hmhm2022 hmhm2022 commented Mar 2, 2026

变更内容

  • 新增 ClaudeCompletionsManager 服务,索引内置命令、用户自定义命令和技能
  • 监听 ~/.claude/commands 和 ~/.claude/skills 目录变化,实时刷新补全列表
  • 支持自动学习用户常用命令,按使用频次排序
  • EnhancedInput 中新增斜杠弹出菜单,支持模糊搜索与键盘导航

交互与行为
输入 /(且 / 前是空格或行首)触发补全弹窗
↑/↓ 选择条目,Enter 执行并发送,Esc 关闭弹窗
与 @ 文件提及互斥:当 @ 提及弹窗激活时不触发 / 补全
防止学习缓存污染:路径样式(如 /usr/local/bin)不会被写入学习缓存

变更文件
src/main/services/claude/ClaudeCompletionsManager.ts
src/main/ipc/claudeCompletions.ts
src/main/ipc/index.ts
src/preload/index.ts
src/renderer/components/chat/EnhancedInput.tsx
src/shared/types/claude.ts
src/shared/types/ipc.ts

如何验证
打开增强输入框,在行首输入 /,应出现补全列表
↑/↓ 切换高亮,按 Enter 应直接发送替换后的 /xxx 消息
运行期间新建 ~/.claude/commands 或 ~/.claude/skills 并添加 .md/SKILL.md,应在短时间内自动出现在补全中(无需重启)
发送以路径开头的文本(如 /usr/local/bin)不应污染后续补全候选

- 新增 ClaudeCompletionsManager 服务,索引内置命令、用户自定义命令和技能
- 监听 ~/.claude/commands 和 ~/.claude/skills 目录变化,实时刷新补全列表
- 支持自动学习用户常用命令,按使用频次排序
- EnhancedInput 中新增斜杠弹出菜单,支持模糊搜索与键盘导航
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Claude finished @hmhm2022's task in 3m 17s —— View job


🔍 代码审查报告

👋 欢迎查看审查结果! 本次审查已完成,以下是详细分析和建议。
💡 使用提示:点击 📁 文件链接可直接跳转到代码位置,建议按优先级处理问题。

📊 审查概览 · 一目了然

📋 审查项目 🔢 发现数量 📈 占比 🎯 处理状态
📁 审查文件 7 100% 已完成
🚨 严重问题 0 0%
⚠️ 中等问题 4 50% 🟡 本版本修复
轻微问题 4 50% 🔵 后续优化

🏆 代码质量评分

📊 综合评分: 78/100  🌟🌟🌟🌟⭐
📋 本次等级: 良好

📋 问题分布

🚨 严重: ░░░░░░░░░░  0%
⚠️ 中等: ████░░░░░░ 50%
⚡ 轻微: ████░░░░░░ 50%
总体评分: ⭐⭐⭐⭐☆ (4/5)

🎯 关键发现

🏷️ 类别 🔍 发现数量 🎯 优先级 📈 趋势
🔒 安全性 1 个 🟡 中 📊 平稳
⚡ 性能 2 个 🟡 中 📊 平稳
🎨 代码质量 4 个 🔵 低 📉 改善
🛠️ 健壮性 1 个 🟡 中 📊 平稳

🎯 问题详细分析 · 📊 点击展开查看

📊 **👆 点击此处展开详细统计和图表分析**

📈 问题趋势分析

本次审查: 🟡🟡🟡🟡🔵🔵🔵🔵 (发现 8 个问题)
代码质量: ████████░░ 78%
安全评分: █████████░ 90%  ✅
性能评分: ███████░░░ 70%  ⚠️
🏆 质量指标 📊 当前分数 🎯 目标分数 📈 改进方向
代码质量 78/100 90+ ⬆️ 需改进
安全等级 90/100 100 ✅ 良好
性能优化 70/100 85+ ⚠️ 可优化

🐛 问题清单与修复指南 · 📋 点击展开查看

📋 **👆 点击此处展开详细问题列表 (共 8 个问题) · 一键复制追踪 📌**

🚀 快速开始

⚠️ 中等级别 (Major) 建议修复

🟡 #1 src/main/services/claude/ClaudeCompletionsManager.ts:418-428

🏷️ 类型: 健壮性缺陷 | ⏰ 优先级: P1 - 本版本修复 | 🎯 影响范围: 模块级

  • 🔍 问题描述ensureStarted() 中存在并发竞争条件。当 starting Promise 被复用后(第二个调用者 await starting),如果首次启动过程中抛出异常,starting 会被置为 null,但 isStarted 仍为 false。后续调用会再次创建新的 starting Promise,这本身还好。然而更关键的是:如果 starting Promise 正常 resolve,startingawait starting 之后被置为 null——但第二个 await starting 在异步上也会 resolve,此时 starting 已经是 null 了。这里的逻辑虽然能工作,但并不优雅,且缺少错误处理——启动失败时没有任何 retry 机制或状态恢复。
// src/main/services/claude/ClaudeCompletionsManager.ts:419-429
async function ensureStarted(): Promise<void> {
  if (isStarted) return;
  if (starting) return starting;
  starting = (async () => {
    await refreshSnapshot();
    await startWatchers();
    isStarted = true;
  })();
  await starting;
  starting = null;  // If the IIFE throws, isStarted stays false but starting is nulled
}
  • 🛠️ 解决方案:在 finally 块中清理 starting,并为启动失败添加日志或 retry 逻辑:
async function ensureStarted(): Promise<void> {
  if (isStarted) return;
  if (starting) return starting;
  starting = (async () => {
    try {
      await refreshSnapshot();
      await startWatchers();
      isStarted = true;
    } finally {
      starting = null;
    }
  })();
  return starting;
}
  • ⚠️ 潜在影响:启动失败后可能导致状态不一致

Fix this →


🟡 #2 src/renderer/components/chat/EnhancedInput.tsx:241-271 & EnhancedInput.tsx:374-402

🏷️ 类型: 代码重复 | ⏰ 优先级: P1 - 本版本修复 | 🎯 影响范围: 组件级

  • 🔍 问题描述:自动学习 (learn) 逻辑在 executeSlashhandleSend 中完全重复。两处都执行了相同的路径过滤和 learn() 调用,违反 DRY 原则。而且当通过 executeSlash 路径发送时,消息也以 / 开头,导致 handleSend 中的学习逻辑实际不会触发(因为 executeSlash 直接调用 onSend),但如果未来重构可能产生双重学习。
// src/renderer/components/chat/EnhancedInput.tsx:257-263 (executeSlash)
const token = newContent.match(/^\/\S+/)?.[0];
if (token && !token.slice(1).includes('/') && !token.includes('\\')) {
  window.electronAPI?.claudeCompletions?.learn(token).catch(() => {});
}

// src/renderer/components/chat/EnhancedInput.tsx:379-386 (handleSend)
if (trimmed.startsWith('/')) {
  const token = trimmed.match(/^\/\S+/)?.[0];
  if (token && !token.slice(1).includes('/') && !token.includes('\\')) {
    window.electronAPI?.claudeCompletions?.learn(token).catch(() => {});
  }
}
  • 🛠️ 解决方案:将学习逻辑抽取为独立的 learnSlashToken(text: string) 辅助函数,两处共用:
const learnSlashToken = useCallback((text: string) => {
  if (!text.startsWith('/')) return;
  const token = text.match(/^\/\S+/)?.[0];
  if (token && !token.slice(1).includes('/') && !token.includes('\\')) {
    window.electronAPI?.claudeCompletions?.learn(token).catch(() => {});
  }
}, []);
  • ⚠️ 潜在影响:未来重构可能导致重复学习或遗漏学习

Fix this →


🟡 #3 src/main/services/claude/ClaudeCompletionsManager.ts:234-265

🏷️ 类型: 性能问题 | ⏰ 优先级: P1 - 本版本修复 | 🎯 影响范围: 模块级

  • 🔍 问题描述loadUserCommands() 对 commands 目录下每个 .md 文件执行串行 readTextFileSafe。当用户拥有大量自定义命令时(例如团队共享数十个命令文件),逐一串行读取会产生明显延迟。同样的问题存在于 loadUserSkills()walkDirForSkillFiles 之后的逐文件读取。
// src/main/services/claude/ClaudeCompletionsManager.ts:248-263
for (const entry of entries) {
  if (!entry.isFile()) continue;
  if (!entry.name.toLowerCase().endsWith('.md')) continue;
  const commandName = entry.name.slice(0, -3);
  const filePath = path.join(dir, entry.name);
  const content = await readTextFileSafe(filePath);  // Sequential await in loop
  const heading = content ? parseMarkdownHeading(content) : null;
  // ...
}
  • 🛠️ 解决方案:使用 Promise.all 并行读取文件内容:
const readPromises = mdEntries.map(async (entry) => {
  const filePath = path.join(dir, entry.name);
  const content = await readTextFileSafe(filePath);
  const heading = content ? parseMarkdownHeading(content) : null;
  const commandName = entry.name.slice(0, -3);
  return { kind: 'command' as const, label: `/${commandName}`, ... };
});
items.push(...await Promise.all(readPromises));
  • ⚠️ 潜在影响:文件数量较多时刷新延迟较高

Fix this →


🟡 #4 src/main/services/claude/ClaudeCompletionsManager.ts:179-201

🏷️ 类型: 安全/性能问题 | ⏰ 优先级: P1 - 本版本修复 | 🎯 影响范围: 模块级

  • 🔍 问题描述:学习缓存 (ensoai-slash-learned.json) 没有容量上限。每次用户输入新的 /xxx 命令都会写入缓存,理论上缓存文件可以无限增长。虽然 normalizeSlashLabel 做了路径过滤,但长时间使用后仍可能积累大量条目,导致 JSON 解析变慢和磁盘占用增加。
// src/main/services/claude/ClaudeCompletionsManager.ts:471-475
const current = cache.items[normalized] ?? { count: 0, lastUsedAt: 0 };
cache.items[normalized] = { count: Math.max(0, current.count) + 1, lastUsedAt: now };
await writeLearnedCacheSafe(cache);
// No eviction / size limit
  • 🛠️ 解决方案:在写入缓存时添加容量上限(如最多 200 条),当超出时淘汰 lastUsedAt 最早的条目:
const MAX_LEARNED = 200;
if (Object.keys(cache.items).length > MAX_LEARNED) {
  const sorted = Object.entries(cache.items).sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt);
  const toRemove = sorted.slice(0, sorted.length - MAX_LEARNED);
  for (const [key] of toRemove) delete cache.items[key];
}
  • ⚠️ 潜在影响:长期使用后缓存文件膨胀,影响解析速度

Fix this →


⚡ 轻微级别 (Minor) 优化建议

🔵 #5 src/main/services/claude/ClaudeCompletionsManager.ts:110 及多处

🏷️ 类型: 代码规范 | ⏰ 优先级: P2 - 后续版本 | 🎯 影响范围: 全局

  • 🔍 问题描述console.warn 日志消息使用了中文,与 CLAUDE.md 中的注释规范不一致:

📄 参考规范: CLAUDE.md

## 代码注释规范
- **语言**:所有代码注释必须使用英文
- **风格**:简洁明了,避免冗余描述

日志消息虽然不严格等同于代码注释,但保持一致的英文风格有助于国际化和日志分析。

// src/main/services/claude/ClaudeCompletionsManager.ts:110
console.warn('[ClaudeCompletions] 刷新失败:', err);
// src/main/services/claude/ClaudeCompletionsManager.ts:174
console.warn('[ClaudeCompletions] 读取失败:', filePath, err);
// src/main/services/claude/ClaudeCompletionsManager.ts:199
console.warn('[ClaudeCompletions] 写入学习缓存失败:', filePath, err);
  • 🛠️ 解决方案:将日志消息改为英文,例如 '[ClaudeCompletions] refresh failed:''[ClaudeCompletions] read failed:' 等。

🔵 #6 src/main/services/claude/ClaudeCompletionsManager.ts:322-365

🏷️ 类型: 可维护性 | ⏰ 优先级: P2 - 后续版本 | 🎯 影响范围: 局部

  • 🔍 问题描述loadBuiltinCommands() 中硬编码了 21 个内置命令和中文描述。这些命令随 Claude CLI 版本更新可能频繁变化,硬编码不利于维护。同时中文描述在不同语言环境下不通用。
// src/main/services/claude/ClaudeCompletionsManager.ts:325-351
const commands: Array<{ command: string; description: string }> = [
  { command: 'help', description: '查看帮助' },
  { command: 'init', description: '初始化项目(生成 CLAUDE.md)' },
  // ... 19 more hardcoded entries
];
  • 🛠️ 解决方案:考虑将内置命令列表抽取为单独的常量文件(如 builtinSlashCommands.ts),并支持 i18n 描述 key。或者添加注释说明维护策略,提醒在 CLI 更新时同步修改。
  • ⚠️ 潜在影响:CLI 升级时补全列表过时

🔵 #7 src/main/ipc/index.ts:147-184

🏷️ 类型: 健壮性 | ⏰ 优先级: P2 - 后续版本 | 🎯 影响范围: 模块级

  • 🔍 问题描述cleanupAllResourcesSync() 函数没有包含 Claude completions watcher 的同步清理。异步清理在 cleanupAllResources() 中已处理(第 111-124 行),但同步清理路径(SIGINT/SIGTERM 信号处理)会跳过 completions watcher 的清理。这意味着在强制退出场景下,watcher 的定时器可能残留。

  • 🛠️ 解决方案:可以考虑在 ClaudeCompletionsManager 中导出一个 stopSync() 方法(仅清理 timers 即可),并在 cleanupAllResourcesSync() 中调用。或者评估是否在此场景下残留定时器影响较小可忽略。

  • ⚠️ 潜在影响:进程退出时可能有少量定时器残留(影响较小)


🔵 #8 src/renderer/components/chat/EnhancedInput.tsx:738-739

🏷️ 类型: 可访问性 | ⏰ 优先级: P2 - 后续版本 | 🎯 影响范围: UI 组件

  • 🔍 问题描述:斜杠补全弹窗中 "命令" / "技能" 标签使用了硬编码中文,没有通过 useI18nt() 函数做国际化处理。其他 UI 文本(如 placeholder、按钮 label)都已做了 i18n 支持,这里不一致。
// src/renderer/components/chat/EnhancedInput.tsx:738-739
<span className="text-muted-foreground text-xs shrink-0">
  {item.kind === 'command' ? '命令' : '技能'}
</span>
  • 🛠️ 解决方案:使用 t('Command')t('Skill') 替代硬编码中文。
  • ⚠️ 潜在影响:多语言用户看到混合语言的 UI

Fix this →


📊 修复优先级矩阵

🎯 优先级 🚨 严重 ⚠️ 中等 ⚡ 轻微 📈 总计
🔴 P0 0 个 - - 0 个
🟡 P1 - 4 个 - 4 个
🔵 P2 - - 4 个 4 个
📊 合计 0 4 4 8 个

💡 架构亮点

值得肯定的设计决策:

  1. 模块化设计优秀ClaudeCompletionsManager 作为独立服务与 IPC 层分离,职责清晰
  2. 防御性编程normalizeSlashLabel 对路径样式(/usr/local/bin)的过滤处理非常周到
  3. 文件监听策略合理 — 使用 @parcel/watcher + debounce/maxWait 双重策略,避免频繁刷新
  4. 重试机制scheduleWatchersRetry 轮询检测目录创建,支持运行时动态添加
  5. dedup 优先级buildSnapshot 中 learned → builtin → user 的覆盖策略直觉正确
  6. @ mention 互斥 — 避免两个弹窗同时出现的交互冲突

🎯 合并决策建议

📋 合并评估结果

📊 评估维度 ⭐ 得分 🎯 状态 💭 说明
🔐 安全性 8/10 ✅ 通过 缓存无上限为唯一隐患,无注入风险
📊 代码质量 8/10 ✅ 良好 结构清晰,有少量重复代码
⚡ 性能影响 7/10 ⚠️ 轻微 串行文件读取可优化
🛠️ 功能完整性 9/10 ✅ 完整 核心功能完备,交互体验好
🧪 测试覆盖 5/10 ⚠️ 基本 无单元测试覆盖

📈 综合评分: 37/50 分 · 等级: 良好


🚦 最终建议

⚠️ 条件合并

📝 详细理由

🎯 具体行动建议

  1. 建议本版本修复

  2. 后续版本改进


@J3n5en J3n5en merged commit 2dee8c0 into J3n5en:main Mar 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants