一键 Commit(DSH-EZ Commit)DSH 插件设计

August 22, 2026 · View on GitHub

状态:已实现为静态双面 bundle(v0.0.3 起)。所有 API 契约均通过 Inspect Provider 与源码检出实际验证(2026-08-21);双面挂载机制对照 dsh-skin-market / maid-atelier 实测。

0. 结论:需求足以独立开发为一个 DSH 静态双面 Plugin

六条需求全部能被当前 DSH 环境已挂载的能力覆盖,无需改 DSH 本体:

#需求承载能力(已验证)
1非 git 仓库 → 按钮置灰Host shell 服务执行 git rev-parse --is-inside-work-tree;Client 按钮禁用态
2按钮左侧展示分支名git symbolic-ref --short HEAD(detached 时退化为短 hash),渲染在按钮左侧
3点击弹二次确认框Client shell.overlay 插槽(已验证:root 域、空占用、点击穿透层)自绘 modal
4模型按业务颗粒度拆分 commitHost llm 服务 stream()(已验证 GenerateOptions 契约 + dsh-compaction-basic 的一次性调用配方)
5无改动 → 置灰git status --porcelain 计数,为 0 时禁用按钮
6模型判定"环境噪音"→ 弹窗提示不 commit同一模型调用输出 verdict: "noise",Client 弹反馈框,不执行任何 git 写操作

架构归属:一个包,双平面(Host + Client),DSH 静态双面 bundle(dsh.bundle.patch + dsh.client + exports "./client",与第三方皮肤包同机制)。

  • Host 半(main):git 状态采集 / diff 分析 / 模型调用 / 分批执行 commit(全部有服务可依:shellllmworkspaceRegistryagentsagentDefaultModel);向 webServer 注册 /ezcommit/api 前缀路由。
  • Client 半(exports "./client"):按钮 + 弹窗 UI(插槽 conversation.session.header.actions + shell.overlay,均已查到完整注册协议),浏览器模块工厂(window.__ModuleLoader__)加载。
  • 通信:Client 经同源 HTTPfetch POST JSON)调用 Host 路由;同源校验拒绝跨站调用。安装并重启 profile 后即生效,无需 cordis preset / cordis_define

1. 总体架构

Client(浏览器)                        Host(Node 进程)
┌─────────────────────────────┐        ┌──────────────────────────────────────┐
│ conversation.session.header │        │ RPC: git.state(sessionId)            │
│  .actions 插槽               │        │   ├─ workspaceRegistry.list()  →路径  │
│  └─ CommitBar 组件           │        │   └─ shell: git rev-parse/status     │
│      ├─ [分支名] [一键Commit]│        │        → {inRepo, branch, hasChanges}│
│      └─ 状态机: enabled/     │        │ RPC: commit.analyze(sessionId)       │
│         disabled            │        │   ├─ git diff/status(截断上限)      │
│ shell.overlay 插槽           │        │   ├─ llm.stream(分析提示词, 当前会话 │
│  └─ CommitDialog 组件        │        │   │   模型) → JSON 计划              │
│      确认 → 分析中 → 计划    │  host. │   └─ 校验 JSON → {verdict, commits}  │
│      审查 → 执行 → 结果      │ fetch │ RPC: commit.execute(sessionId, plan) │
│      (噪音 → 提示弹窗)     │        │   ├─ 逐批: git add --pathspec-from-  │
│  timer: 每 5s 轮询 git.state │        │   │   file=- ; git commit -m         │
└─────────────────────────────┘        │   └─ 返回每批 commit hash            │
                                       └──────────────────────────────────────┘

数据流(一次完整操作)

  1. 挂载时 + 每 5s:Client POST /ezcommit/api/git.state {sessionId} → 更新按钮(分支名、灰/亮)。
  2. 点击按钮(仅在有改动时可点)→ 弹确认框(分支 + 改动计数)【需求 3】。
  3. 用户确认 → commit.analyze:Host 取 diff → 调当前会话模型分析 → 返回裁决【需求 4/6】。
  4. 裁决为 noise → Client 弹"环境噪音,无需 commit"反馈框,不执行任何 git 写操作【需求 6】。
  5. 裁决为计划 → 弹计划审查框:展示拆分后的批次(顺序、每批 message、文件清单),用户确认后执行。
  6. commit.execute:逐批 git add + git commit,返回每批短 hash;Client 弹成功框并立即刷新按钮状态(置灰)【需求 5 闭环】。

2. 需求 → 设计映射(细节)

2.1 仓库判定与按钮置灰(需求 1、5)

  • Host git.state 返回:
    • inRepogit -C <path> rev-parse --is-inside-work-tree(支持工作区在仓库子目录)。
    • branchgit symbolic-ref --short HEAD,失败(detached HEAD)→ git rev-parse --short HEAD
    • hasChangesgit status --porcelain 非空(含 staged / unstaged / untracked)。
    • changedCount / untrackedCount--porcelain 行分类计数,供确认框展示。
  • 按钮禁用条件:!inRepo || !hasChanges || inFlight || analyzing
  • 轮询间隔 5s(Client 原生 setInterval,随组件卸载清理)+ 点击/执行完成时立即刷新。不监听文件系统(避免复杂 watcher;轮询成本 ≈ 一次 porcelain,可接受)。

2.2 分支名展示(需求 2)

按钮条结构:[分支名 chip] [一键Commit button],同一 flex 行。分支名为空/非仓库时显示"非Git仓库";无改动时仍显示分支名但整体置灰。

2.3 二次确认(需求 3)

shell.overlay 注册 id one-click-commit-dialog 的 modal(层本身点击穿透,modal 根节点 pointerEvents: 'auto' + 遮罩)。四态状态机:confirm → analyzing → plan → done/error,噪音分支跳 noise 态。

2.4 模型拆分颗粒度(需求 4)

  • 模型来源(当前会话模型):优先 agents.get(sessionId)agent.session.requestHeader()?.config(会话已路由的 provider/model,源码中 dsh-compaction-basic 同款取法);退化为 agent.options.provider/model;再退化 agentDefaultModel.currentSelection()
  • 调用方式ctx.llm.stream({ provider, model, messages: [{role:'user', content:[{type:'text',text:PROMPT}], source:{kind:'plugin',plugin:'one-click-commit'}}], system: SYSTEM, maxTokens: 4096, signal });手写迷你 assembler 累积 text-delta,读取 finish 块处理 error/aborted。
  • 喂给模型的改动事实
    • git status --porcelain(全部条目,含未跟踪;敏感路径行替换为 [敏感文件已隐藏] 占位)
    • git diff --stat + git diff(统一上限,如 200KB,截断时注入"DIFF TRUNCATED"标记并告知模型;敏感文件的 stat 路径与 diff hunk 整体省略)
    • 未跟踪文本文件内容采样(每个 ≤8KB,超限标记"binary/large";敏感路径不采样,提示词中的路径使用相对路径)
  • 敏感路径过滤.env*.npmrc.pypirc.netrc.git-credentials*.pem*.keyid_rsa*.ssh/.aws/.kube/credentials*.json 等路径不进入 fileSet 与模型提示词;commit.execute 二次校验时同样拒绝这些路径,commit.analyze 通过 sensitive 字段把清单返回给 Client 仅做本地展示。
  • 输出 JSON 契约(system 提示词内定义,响应解析时剥离 ```json 围栏):
    {
      "verdict": "noise" | "commit",
      "reason": "一句话说明(noise 时必填)",
      "commits": [
        { "title": "feat(scope): ...", "body": "可选多行说明", "files": ["相对路径..."] }
      ]
    }
    
  • 拆分规则(提示词要求):按业务意图分组(一个功能/一个修复/一个重构 = 一批);每个文件恰好属于一批;批次顺序按依赖(先基础后上层);title 遵循 Conventional Commits(feat/fix/refactor/chore/docs/test);files 路径必须来自输入的改动清单,模型不得发明路径(解析后校验:不存在或重复的文件 → 归入未计划集合并在 UI 提示)。
  • 噪音判定标准(提示词要求):模型产物、锁文件自动生成、格式化器副产品、.DS_Store、空变更、与需求无关的临时文件等 → noise。是否 commit 完全由模型裁决,插件不做写操作直到裁决为 commit 且用户确认。

2.5 执行(需求 4 后半)

commit.execute 逐批执行:

  • git -C <path> add --pathspec-from-file=- --(文件清单经 stdin 传入——已验证 ShellExecRequest.stdin 字段,天然规避空格/特殊字符路径转义问题)
  • git -C <path> commit -m <title> [-m <body>]
  • 每批后 git rev-parse --short HEAD 记录 hash;任一批失败 → 停止并返回已成功批次 + 错误详情(不回滚,由用户决定)。
  • 完成后 git status --porcelain 复查残留,返回 leftoverCount

2.6 噪音防污染(需求 6)

  • 噪音裁决在 git add/commit 之前完成;noise 路径上 Host 零 git 写操作。
  • 噪音信息 UI:黄色反馈框"环境噪音,无需 commit"+ 模型给出的 reason + "仍然查看详情"折叠。

3. 已验证的关键契约清单(实现时直接引用)

契约事实
Host shellresolve(ShellExecRequest{command, workdir?, timeoutMs?, stdoutMaxBytes?, stdin?, signal?})run(spec)ShellRunResult{exitCode, stdout, stderr, timedOut, aborted}
Host llmstream(GenerateOptions{provider, model, reasoningEffort?, messages, system?, maxTokens?, signal?})AsyncIterable<StreamChunk>(block-start/text-delta/block-end/finish{reason:{kind:'stop'|'error'|...}})
一次性调用配方dsh-compaction-basicmessages 手拼 user 消息(source:{kind:'plugin', plugin})、llm.stream() 消费 chunks
会话模型取法agents.get(sessionId).session.requestHeader()?.config;兜底 agentDefaultModel.currentSelection()ModelSelection{provider, model, reasoningEffort?}
工作区路径workspaceRegistry.list()Workspace(含 pathsessionIds);按 sessionId 反查
Client 插槽 conversation.session.header.actionskind:listscope:session、注册项 {id, order?, label?}、无 owner props;标准 props:sessionIduseSessionuseWorkspacesuseInputinputActions;现有占用:agent-preset(-10)、subagent-catalog(10)、job-list(20) → 新 id one-click-commit、order 建议 30
Client 插槽 shell.overlaykind:listscope:root、空占用;层点击穿透,入口需自设 pointer-events
Client 侧能力React.createElement/useState/useEffectrequire("react") 基线)、fetch(同源 JSON)、<style> 元素注入
双面通信Host webServer.register({kind:'prefix', path:'/ezcommit/api', handler});Client fetch POST JSON;仅同源、参数与返回值仅无损 JSON
Client 服务slots(register/inject,ctx.get('slots')
会话→工作区Client 侧亦可 useWorkspaces().items.find(w => w.sessionIds.includes(sessionId))?.pathWorkspaceView{workspaceId, path, title, sessionIds} 已验证);本设计以 Host 反查为准,Client 只传 sessionId

4. 需求未覆盖、由本设计补足的决策点

决策点默认选择备选
按钮位置会话标题行操作区(conversation.session.header.actions输入框工具行左端 conversation.input.left;dock 条
分析后是否先给用户看拆分计划(计划审查框,用户确认后才执行)——这是"分批 commit"价值的可见部分,也符合"二次确认"精神直接执行(少一次点击,但用户看不到拆分结果)
模型裁决噪音纯模型裁决(含噪音事实清单)加确定性预过滤(.DS_Store 等先剔除再问模型)
commit message 风格Conventional Commits自由文本
diff 上限200KB 截断 + 标记可配置
轮询间隔5s事件驱动(fs watcher,复杂度高)
多批次中某批失败停止、不回滚、报告继续执行后续批次
空仓库(无 HEAD)支持 root commit(rev-parse HEAD 失败时走未出生分支路径)提示用户先手动初始化提交

5. 风险与边界

  1. 模型输出解析失败(非 JSON / 非法 files)→ 重试一次;再失败返回错误弹窗,绝不猜测执行。
  2. 超大 diff / 二进制:截断与采样上限兜底,模型被告知截断事实,宁判噪音勿误 commit。
  3. 并发:同一 sessionId 的 analyze/execute 设置 in-flight 锁(Host 内存态),重复点击直接拒绝。
  4. 提交安全:绝不触碰用户未确认的批次;不 git push;不 --force;不修改历史。插件生命周期内所有副作用(RPC handler、轮询、插槽、样式)均通过 ctx.effect/slots.inject 挂在 Fiber 上,stop/update/undefine 时自动清理。
  5. 数据外发边界commit.analyze 会把 status、diff 与非敏感未跟踪文本采样发送给当前会话模型服务商;敏感路径(.env*、密钥、凭据类)在 Host 侧过滤,不进入提示词、不参与计划、也不允许执行提交。使用者仍应只对允许外发的代码/数据使用本插件。
  6. 生效方式:静态双面包随 profile 启动加载,客户端 bundle 由 dsh-client-modules 组装进 web 启动图;更新/卸载需重启 profile 生效。

6. 实施计划(静态双面 bundle)

  1. 包契约:dsh.bundle.patch(锚点行 ezcommit)+ dsh.clientplatform: 'web'inject: [])+ exports "./client"
  2. Host 半(src/index.jsmain 入口):
    • webServer.register({kind:'prefix', path:'/ezcommit/api', handler}) 挂载三个方法路由(git.state / commit.analyze / commit.execute),同源校验 + JSON 体解析;
    • git.state:路径反查 + 三个只读 git 命令;commit.analyze:diff 采集(含截断)→ 模型调用 → JSON 校验;commit.execute:分批 add/commit + hash 收集;
    • ctx.get('webServer'|'shell'|'llm'|'workspaceRegistry'|'agents'|'agentDefaultModel') 缺失时优雅降级(返回 {ok:false, error}),路由挂载失败仅告警。
  3. Client 半(src/client.jswindow.__ModuleLoader__.load({id:'dsh-ezcommit-plugin', factory})):
    • slots.inject('conversation.session.header.actions', ...) 注册 CommitBar(分支 chip + 按钮 + 禁用态);
    • slots.inject('shell.overlay', ...) 注册 CommitDialog(四态状态机 + 噪音框);
    • <style id="ezcommit-styles"> 注入局部样式(主题 CSS 变量,ctx.effect 持有卸载);setInterval 每 5s 轮询 + 动作后即时刷新;fetch('/ezcommit/api/<method>', …) 调 Host。
  4. dsh plugin --profile web add 安装 → 重启 profile → 用一个测试仓库逐项验证六条需求(见 docs/VERIFICATION.md)。