好的,下面是可以直接复制到 CSDN 的 Markdown 版本:


一、项目背景

IntelliGit 是一个面向 Git 工作流增强的桌面应用,技术栈主要包括 Electron、React、TypeScript、Ant Design、Zustand 和 Electron IPC。

本次开发沿三条主线推进:

  1. 补齐智能提交的完整执行链路——从 AI 生成到用户确认,再到成功 / 失败反馈
  2. 接入 OpenAI-compatible 协议,默认推荐 DeepSeek,支持 AI 分析变更与生成 commit message
  3. 定位并修复设置面板打开时的黑屏问题,顺带优化提交面板布局

项目的核心目标是把"智能提交"从可用推进到好用——让 AI 不只是生成一行 commit message,而是真正理解变更意图、帮助用户组织提交。


二、智能提交工作流

2.1 补齐完整的提交执行链路

之前的项目中,智能提交的"分析"和"生成"部分已经可以运行,但缺少完整的执行闭环:用户生成了 commit message 之后,没有清晰的路径走到"提交成功"这一步,失败了也不知道发生了什么。

本次把整个链路打通,完整流程如下:

查看变更 diff
    ↓
AI 分析变更分组
    ↓
选择分组,暂存所选文件
    ↓
AI 生成 Commit Message
    ↓
用户编辑 Commit Message(可选)
    ↓
用户点击"确认创建 Commit"
    ↓
提交前三重校验
    ↓
提交成功 → 显示短 hash,清空状态
提交失败 → 保留输入,展示错误信息

2.2 提交前三重校验

为了防止用户误触或在错误状态下提交,在执行 git commit 前增加了三层检查,任意一层不通过都会阻止提交并给出提示:

  • 是否存在非空的 commit message
  • 是否存在至少一个已暂存(staged)的文件
  • 当前是否没有其他 Git 操作正在执行(防止并发冲突)

2.3 成功与失败的反馈设计

提交结果直接显示在面板内,不需要用户打开终端或查看 Git 日志:

  • 提交成功:清空 commit message 输入框,清空智能分组状态,在面板内显示短 hash,如 ea39eb2
  • 提交失败:保留用户已输入的 commit message,展示具体错误信息,方便用户修改后直接重试

2.4 提交面板布局重构

随着智能分组结果、commit message 输入框、确认按钮、反馈区域等内容逐渐增多,原有面板高度(156px)已严重不够用,内容互相挤压导致按钮看不到。

本次将父容器高度调整为 340px,并重新划分了面板内的布局层次:

智能工具栏
    ↓
分组分析结果(可折叠滚动区)
    ↓
Commit Message 输入框
    ↓
确认提交按钮
    ↓
提交反馈区域

分组结果区使用折叠面板展示,折叠时只显示"分析结果:N 个分组",展开后可以看到 AI 分析出的分组类型、摘要和涉及文件,不影响下方的提交操作区域。

涉及文件:

  • src/renderer/src/views/ChangesView/CommitPanel.tsx
  • src/renderer/src/views/ChangesView/CommitPanel.module.css
  • src/renderer/src/services/gitWorkflowService.ts
  • src/renderer/src/views/ChangesView/ChangesView.module.css

三、设置面板黑屏问题修复

3.1 问题现象

接入 AI 配置面板后,点击左侧"设置"按钮,界面直接变黑,主界面其他功能完全正常。控制台报错:

Maximum update depth exceeded

3.2 排查过程

第一步怀疑是 useEffect 内部的表单同步逻辑——设置面板中有这样一段常见模式:

useEffect(() => {
  form.setFieldsValue(config)  // config 变化 → 同步表单
}, [config])                   // 表单变化 → state 更新 → 再次触发

移除这部分逻辑后,问题依然存在。继续往上追溯,最终定位到 Zustand store 的 selector 写法。

3.3 根本原因

原来的 selector 每次调用都返回一个新的对象字面量:

// 有问题的写法
const { config, status, error } = useStore(
  state => ({ config: state.config, status: state.status, error: state.error })
)
// 每次调用都是新对象 → 引用永远不等 → 无限更新

在 React 19 / Zustand 的 useSyncExternalStore 机制下,React 通过引用比较判断 store 快照是否变化。每次调用都返回新对象,引用永远不相等,React 认为 store 持续变化,无限触发 re-render,最终栈溢出,表现为黑屏。

3.4 修复方案

将 selector 拆分为分别订阅各字段的独立调用:

// 修复后
const config = useLlmConfigStore(s => s.config)
const status = useLlmConfigStore(s => s.status)
const error  = useLlmConfigStore(s => s.error)
// 每个 selector 返回基础值 → 值未变时引用稳定

每个 selector 返回稳定的基础值,只有对应字段真正发生变化时才会触发重新渲染。

3.5 增加 Fatal Error Fallback

为了避免下次再出现"纯黑屏但没有任何信息"的情况,在 renderer 入口增加了全局错误兜底页:如果 React 启动或运行时抛出未捕获的错误,页面会直接展示错误信息与完整堆栈,而不是纯黑屏,方便后续定位问题。

涉及文件:

  • src/renderer/src/viewModels/useGlobalSettingsPanelModel.ts
  • src/renderer/src/main.tsx
  • src/renderer/src/layout/GlobalSettingsPanel/index.tsx

四、接入 OpenAI-compatible 协议

4.1 为什么优先接 OpenAI-compatible

没有直接绑定某一家厂商,而是优先支持 OpenAI-compatible 协议——这个协议已经成为事实标准,一次接入可以兼容绝大多数主流模型:

  • OpenAI
  • DeepSeek
  • 通义千问(兼容模式)
  • Moonshot / Kimi
  • 智谱 GLM
  • SiliconFlow
  • 火山方舟
  • 本地模型(Ollama、LM Studio、vLLM)

4.2 默认配置

考虑到国内用户的实际情况,OpenAI-compatible 模式的默认配置直接指向 DeepSeek:

Provider  : OpenAI 兼容
Base URL  : https://api.deepseek.com
Model     : deepseek-chat
Temperature: 0.2
Max Tokens : 4096

用户只需填入自己的 DeepSeek API Key,即可直接使用智能提交能力。

4.3 Base URL 规范化

用户填写 Base URL 时,可能带或不带 /v1 后缀,不处理会拼出 /v1/v1/chat/completions 这样的错误地址。实现了规范化逻辑统一处理:

https://api.deepseek.com     → https://api.deepseek.com/v1/chat/completions  ✓
https://api.deepseek.com/v1  → https://api.deepseek.com/v1/chat/completions  ✓

4.4 其他工程细节

  • AI 请求统一设置 30 秒超时,超时后自动 fallback 到本地模板
  • 错误日志中对 API Key 脱敏:sk-abcdef...sk-***,防止密钥泄露

涉及文件:

  • src/renderer/src/agent/llmClient.ts
  • src/renderer/src/layout/GlobalSettingsPanel/index.tsx

五、LLM 请求迁移到主进程代理

5.1 问题现象

配置好 DeepSeek 之后,点击"分析变更分组",控制台报:

Failed to fetch

这不是 API Key 错误,也不是模型名称问题,而是请求根本没有发出去。

5.2 原因分析

Electron 的 renderer 进程本质上运行在一个受限的浏览器环境中。直接在 renderer 里 fetch 第三方 API 会受到 CORS 策略和 Electron 安全沙箱的限制,导致请求被拦截。

5.3 解决方案

将 LLM 的 HTTP 请求从 renderer 进程迁移到主进程(main process)执行,renderer 通过 IPC 发起调用:

Renderer(构造请求参数)
    ↓ IPC channel: llm:proxy
Main Process(执行 fetch)
    ↓ HTTPS
DeepSeek / OpenAI API
    ↑ 响应结果原路返回

主进程没有浏览器 CORS 的限制,可以直接发起任意 HTTP 请求。这个架构的额外好处:

  • API Key 不再出现在 renderer 的请求逻辑中,也不会出现在 DevTools 的 Network 面板
  • 超时控制、重试、日志审计可以集中在主进程统一处理
  • 后续如需对 AI 请求做频控或审计,只需改一处

涉及文件:

  • src/main/ipc/llmHandlers.ts
  • src/main/ipc/index.ts
  • src/preload/index.ts
  • src/shared/types/sidecar.ts
  • src/renderer/src/agent/llmClient.ts

六、AI 能力设计

6.1 Commit Message 生成

Prompt 设计思路

要求模型只输出 JSON,不带任何 Markdown 或解释文字,遵循 Conventional Commits 规范。type 从固定集合中选取,subject 使用中文短句,scope 根据文件路径自动推断。

支持的 type:feat / fix / refactor / style / docs / test / chore / perf / build / ci

目标输出示例:

{
  "type": "fix",
  "scope": "settings",
  "subject": "修复设置面板打开黑屏",
  "body": "",
  "breaking": false
}

最终转换为:

fix(settings): 修复设置面板打开黑屏

输出解析与清洗

模型的输出不总是干净的 JSON,解析流程分三步:

  1. 从响应文本中提取 JSON 部分,去掉 ```json 等 Markdown 标记
  2. JSON.parse 解析,并对 schema(type、subject 等字段)做校验
  3. 任何环节失败则 fallback 本地模板,保证流程不中断

6.2 AI 变更分组

功能目标

一次开发往往同时修改了多个不相关的问题。AI 变更分组的目标是把这些变更按提交意图拆开,让用户可以分批提交。

例如同时改了设置面板黑屏修复、LLM 请求代理、CommitPanel 样式,AI 应拆分为三个分组并各自给出合适的 type 和摘要:

{
  "groups": [
    {
      "type": "fix",
      "scope": "settings",
      "summary": "修复设置面板打开黑屏",
      "files": [
        "src/renderer/src/viewModels/useGlobalSettingsPanelModel.ts",
        "src/renderer/src/layout/GlobalSettingsPanel/index.tsx"
      ]
    },
    {
      "type": "feat",
      "scope": "ai",
      "summary": "通过主进程代理 LLM 请求",
      "files": [
        "src/main/ipc/llmHandlers.ts",
        "src/renderer/src/agent/llmClient.ts"
      ]
    }
  ]
}

Prompt 约束与输出清洗

Prompt 中加入了明确限制:只输出 JSON,不超过 5 个分组,files 必须来自 diff 中真实出现的路径,不允许编造文件。

输出后进行二次清洗:

  • 过滤 diff 中不存在的文件路径
  • 对文件列表去重
  • 过滤空 summary 和空 files 的分组
  • 最多保留 5 组,超出截断
  • 结果不可用时 fallback,避免污染后续 Git 操作

6.3 降级策略

智能提交的核心原则:AI 只是辅助,任何情况下流程都不应该卡死。触发 fallback 的条件:

  • 未配置 API Key
  • 请求超时(超过 30 秒)
  • 请求失败(网络错误等)
  • 模型返回空内容
  • 输出不是合法 JSON
  • schema 校验失败

以上任意一种情况,均自动切换到本地模板生成 commit message,用户无感知,提交流程继续。

涉及文件:

  • src/renderer/src/agent/prompts/commit.ts
  • src/renderer/src/agent/outputParser.ts
  • src/renderer/src/services/smartCommitProvider.ts

七、安全设计

本次 AI 接入的核心原则:AI 只生成建议,人始终在决策回路中。

  • Commit 前必须用户点击确认,不会自动 commit
  • 不会自动 push,push 操作完全由用户发起
  • AI 分组结果中的文件路径必须来自真实 diff,不允许模型编造
  • API Key 错误信息脱敏,不出现在日志中
  • Renderer 不直接请求 LLM,所有 AI 请求通过主进程 IPC 代理
  • 模型输出经过 JSON 解析和 schema 校验后才会影响界面状态

八、当前项目进度

模块 状态
Git 基础操作(status / diff / stage / commit / push / pull) ✅ 已完成
部分暂存(行级 diff patch 选择) ✅ 已完成
智能提交工作流(分组→暂存→生成→确认→反馈) ✅ 基本闭环
AI Commit Message 生成(OpenAI-compatible) ✅ 已完成
AI 变更分组 ✅ 已完成
全局设置面板 + AI 配置项 ✅ 已完成
LLM 请求主进程 IPC 代理 ✅ 已完成
智能冲突管控 🕐 待开始
自然语言 Git 助手 🕐 待推进
高危操作安全体系(force push 阻断、reset 二次确认) 🕐 待推进

九、遇到的典型问题总结

问题一:设置面板黑屏

  • 根因:Zustand selector 每次返回新对象,useSyncExternalStore 引用比较永远失败,无限触发 re-render
  • 修复:拆分 selector,分别订阅 config / status / error 各字段

问题二:AI 请求 Failed to fetch

  • 根因:Renderer 直接请求第三方 LLM API,受到 CORS 和 Electron 安全沙箱限制
  • 修复:通过 Electron 主进程代理 LLM 请求,新增 IPC channel llm:proxy

问题三:CommitPanel 中分组结果和提交按钮互相挤占

  • 根因:提交面板高度太小(156px),承载内容过多
  • 修复:提高父容器高度至 340px,重新设计 CommitPanel 布局,分组结果使用可折叠滚动区域

问题四:AI 输出不可控

  • 根因:模型可能返回 Markdown 标记、解释文本、非法 JSON 或编造文件路径
  • 修复:Prompt 强约束 + JSON 提取 + schema 校验 + 文件路径过滤 + fallback 降级

十、下一步计划

AI 生成质量优化

  • 调整 Commit Message prompt,控制 subject 长度
  • 自动推断更准确的 scope
  • 支持多语言配置

变更分组体验优化

  • 分组结果可编辑,支持合并 / 拆分分组
  • 一键暂存多个分组
  • 支持分组后连续创建多个 commit

智能冲突管控

  • 检测 merge conflict 状态,解析冲突块
  • 展示 ours / theirs 对比
  • AI 生成冲突解决建议,用户确认后写回文件

自然语言 Git 助手

  • 自然语言输入 → 识别 Git 意图 → 生成操作计划 → 用户确认 → 执行 → 反馈结果

Safety 体系完善

  • 高危操作识别,force push 阻断
  • reset / rebase 二次确认
  • AI 工具调用审计日志

目前 IntelliGit 的智能提交已经从"本地模板降级可用"推进到了"真实 AI 可用"的阶段,完整的提交工作流基本闭环。下一阶段的重点是提升 AI 输出质量,以及开始推进智能冲突管控方向。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐