# 一键 Commit(DSH-EZ Commit)DSH 插件设计 > 状态:已实现为**静态双面 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 | 模型按业务颗粒度拆分 commit | Host `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(全部有服务可依:`shell`、`llm`、`workspaceRegistry`、`agents`、`agentDefaultModel`);向 `webServer` 注册 `/ezcommit/api` 前缀路由。 - Client 半(`exports "./client"`):按钮 + 弹窗 UI(插槽 `conversation.session.header.actions` + `shell.overlay`,均已查到完整注册协议),浏览器模块工厂(`window.__ModuleLoader__`)加载。 - 通信:Client 经**同源 HTTP**(`fetch` 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` 返回: - `inRepo`:`git -C rev-parse --is-inside-work-tree`(支持工作区在仓库子目录)。 - `branch`:`git symbolic-ref --short HEAD`,失败(detached HEAD)→ `git rev-parse --short HEAD`。 - `hasChanges`:`git 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`、`*.key`、`id_rsa*`、`.ssh/`、`.aws/`、`.kube/`、`credentials*.json` 等路径不进入 `fileSet` 与模型提示词;`commit.execute` 二次校验时同样拒绝这些路径,`commit.analyze` 通过 `sensitive` 字段把清单返回给 Client 仅做本地展示。 - **输出 JSON 契约**(system 提示词内定义,响应解析时剥离 ```json 围栏): ```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 add --pathspec-from-file=- --`(文件清单经 **stdin** 传入——已验证 `ShellExecRequest.stdin` 字段,天然规避空格/特殊字符路径转义问题) - `git -C commit -m [-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 `shell` | `resolve(ShellExecRequest{command, workdir?, timeoutMs?, stdoutMaxBytes?, stdin?, signal?})` → `run(spec)` → `ShellRunResult{exitCode, stdout, stderr, timedOut, aborted}` | | Host `llm` | `stream(GenerateOptions{provider, model, reasoningEffort?, messages, system?, maxTokens?, signal?})` → `AsyncIterable<StreamChunk>`(block-start/text-delta/block-end/finish{reason:{kind:'stop'\|'error'\|...}}) | | 一次性调用配方 | `dsh-compaction-basic`:`messages` 手拼 user 消息(`source:{kind:'plugin', plugin}`)、`llm.stream()` 消费 chunks | | 会话模型取法 | `agents.get(sessionId).session.requestHeader()?.config`;兜底 `agentDefaultModel.currentSelection()` → `ModelSelection{provider, model, reasoningEffort?}` | | 工作区路径 | `workspaceRegistry.list()` → `Workspace`(含 `path`、`sessionIds`);按 sessionId 反查 | | Client 插槽 `conversation.session.header.actions` | `kind:list`、`scope:session`、注册项 `{id, order?, label?}`、无 owner props;标准 props:`sessionId`、`useSession`、`useWorkspaces`、`useInput`、`inputActions`;现有占用:agent-preset(-10)、subagent-catalog(10)、job-list(20) → 新 id `one-click-commit`、order 建议 30 | | Client 插槽 `shell.overlay` | `kind:list`、`scope:root`、空占用;层点击穿透,入口需自设 pointer-events | | Client 侧能力 | `React.createElement/useState/useEffect`(`require("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))?.path`(`WorkspaceView{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.client`(`platform: 'web'`、`inject: []`)+ `exports "./client"`。 2. Host 半(`src/index.js`,`main` 入口): - `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.js`,`window.__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)。