dsh-harness-push 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [1.0.1] - 2026-10-22
6
+
7
+ ### 修复
8
+
9
+ - **智能体不再能自行执行审查**:此前 agent 在「推送 / 提交 / 发布」任务里会反复自动调用审查,既不弹窗也不让用户选择审查内容——一个用户看不见、也管不了的安全步骤等于没有。现在工具 `review` **只登记待确认请求**,面板轮询到后打开与人工点击完全相同的审查弹窗,用户勾选审查项后才执行;已有待确认请求时返回 `already_pending`,从机制上阻止反复请求。弹窗会标注「Agent 请求」,关闭或开始后自动清理请求。
10
+ - **语言构成条改用 GitHub 官方配色**:此前按段序取色(第 1 段品牌色、第 2 段成功色…),所以第一段总显示为黑色,其余颜色也没有含义。现在按语言名查 GitHub linguist 官方色:JavaScript `#F1E05A`、TypeScript `#3178C6`、Python `#3572A5`、HTML `#E34C26`、CSS `#563D7C`、Go `#00ADD8`、Rust `#DEA584`、Vue `#41B883` 等;未知语言按名称散列出色相,稳定且互不同色。
11
+
12
+ ### 测试
13
+
14
+ - 新增 `verify-review-gate.mjs`(22 项),锁死「审查必须经用户确认」与「按语言名配色」两处行为
15
+
16
+ ## [1.0.0] - 2026-10-22
17
+
18
+ 首个公开发布版本。
19
+
20
+ > 发布包名为 **`dsh-harness-push`**;GitHub 仓库名为 `dsh-push`。
21
+ > npm 上的 `dsh-push` 已被另一个用途无关的插件占用,因此发布包改了名,
22
+ > 插件内部标识(工具名、路由、界面入口)保持不变。
23
+
24
+ ### 新增
25
+
26
+ - **设置页 Push**:多账号卡片(头像 / 昵称 / 权限范围)、下拉切换与移除、跳转账号主页
27
+ - **账号录入**:Personal Access Token 与 Device Flow 设备码两种方式
28
+ - **账号概览**:最近一年贡献热力图、未读通知(可标记已读并跳转)
29
+ - **仓库管理弹窗**:概览指标、分支、设置、发布(含附件上传与删除)、协作者、Webhook、危险操作区
30
+ - **左侧栏仓库面板**:搜索、私有/公开分段、分页、新建与删除仓库
31
+ - **列表 / 卡片双视图**:卡片分大小两档且默认卡片;底部语言占比条形图;star / fork / issue 横排指标;简介独立滚动区
32
+ - **工作区侧边栏**:状态、提交、推送、拉取、抓取、撤回(soft/mixed/hard)、分支、标签、stash、改动分组、提交图
33
+ - **推送目标选择**:搜索 GitHub 仓库或使用本地远端,并按分支记忆上次目标
34
+ - **推送前 Agent 审查**:内置 8 项审查规则(4 类)、自定义提示词与预设存档、模型与思考强度可选、规则扫描 + 模型审查双层
35
+ - **审查可与待推送提交对齐**:工作区干净时自动改为审查即将推送的提交,并在结果中标注 `scope`
36
+ - **模型工具 `github_link`**:`status` / `log` / `push` / `pull` / `commit` / `repos` / `accounts` / `create_repo` / `release` / `review`
37
+ - **诊断端点** `GET /api/push/diagnostics`:报告模型工具注册状态,便于排查「智能体看不到工具」
38
+ - **契约守卫测试**:客户端调用的每条路由都必须出现在 `CONTRACT.md` 中,且文档中的每条路由都必须真实存在
39
+
40
+ ### 修复
41
+
42
+ - **git 推送认证方案错误**:git 智能 HTTP 使用 HTTP Basic 认证,此前发送 `Bearer` 导致端点返回 401、git 转而索要密码,推送始终失败。已改为 `Authorization: Basic base64(x-access-token:<token>)`
43
+ - **凭据暴露在进程命令行**:认证信息曾作为 `-c` 参数进入 git 子进程 argv,本机其他账号可读取进程命令行。现改为通过 `GIT_CONFIG_*` **环境变量**注入,argv 中不再出现任何凭据
44
+ - **模型工具从未注册成功**:输出 schema 使用了 `type: [...]` 与 `anyOf`,超出宿主支持的 schema 子集,注册抛错且被静默吞掉。已收敛到子集内表达,并新增失败记录与诊断端点
45
+ - **审查结果无法回传给模型**:findings 中的 `undefined` 字段使结果不是无损 JSON,工具调用被拒。现改为逐字段投影,缺失值转 `null`
46
+ - **侧边栏打开即崩**:审查弹窗与 `useState` 曾被插入条件分支内部,正常仓库路径下 `reviewResult` 未定义。已移至组件顶部
47
+ - **删除仓库永远失败**:注释里一个转义的 `\n` 把 `body:` 属性整行吞进注释,删除请求因此没有请求体,两个入口都返回参数错误。已修复
48
+ - **设置页点击仓库卡片抛异常**:卡片调用了 `SettingsSection` 从未传入的 `onOpenManage`,而卡片正是默认视图,首次点击即报错。已改为使用组件自身的 `setManageRepo`
49
+ - **暂存按钮文案误导**:按钮显示「暂存 / 取消暂存」,实际只切换「是否包含在本次提交」。已改为「包含 / 排除」
50
+ - **附件操作反馈不可见**:上传/删除成功后设置了提示状态却从未渲染。已显示
51
+ - **设置页语言条恒为空**:只有完整仓库页预取了语言数据,设置页卡片从未预取。已补上
52
+ - **卡片语言条显示为纯黑**:内联 `var()` 背景无法解析时回退为首色(黑),改为由样式表按 `data-tone` 上色
53
+ - **卡片指标样式错乱**:`.dsh-push-stat` 与仓库概览页的方框指标类名冲突,继承了边框与内边距。已拆分为独立的 `dsh-push-metric`
54
+ - **卡片简介挤压指标**:简介改为固定高度滚动区,指标行固定在滚动区之外,卡片高度固定以保证网格对齐
55
+ - **错误信息脱敏遗漏**:脱敏正则只匹配 `Bearer`,切换到 Basic 后凭据可能泄漏;PEM 私钥块也未覆盖。现对任意认证方案与私钥块生效
56
+ - **`mutate()` 丢失审查预设**:状态写入用显式字段清单构造草稿,漏掉 `reviewPresets` 导致保存后读不到。已补齐并有回归测试
57
+ - **发布路径泄漏**:README 安装示例含开发者本机绝对路径与用户名,已替换为占位符
58
+ - **`state.json` BOM 导致账号丢失**:读取前剥离 UTF-8 BOM
59
+ - **删除仓库误报成功**:404 曾被视为删除成功,现先探测仓库是否仍可读,并强制显式确认
60
+ - **契约漂移**:`/repos/languages`、`/releases/asset`、`/releases/asset/delete`、`/local/push/target/forget`、`RemoteRepo.forks` 等实现早已存在但未写入契约,已补齐。
61
+
62
+ ### 安全
63
+
64
+ - Token 仅存于 `%DSH_HOME%\push\state.json`:POSIX 以 `0600` 写入,Windows 追加 `icacls` 限制为当前用户;不进入 HTTP 响应、日志、git 回显或工具结果
65
+ - 推送凭据通过子进程环境注入,**既不写入 `.git/config`,也不出现在命令行**
66
+ - 规则扫描命中的密钥原文永不外泄;PEM 私钥块与占位符分别处理(后者不误报)
67
+
68
+ ### 测试
69
+
70
+ - 11 组测试套件 + 路由可达性审计,覆盖客户端源断言、仓库卡片、工具契约、错误映射、推送记忆、删除语义、审查规则与范围、审查预设持久化、契约一致性、可达性
package/CONTRACT.md ADDED
@@ -0,0 +1,506 @@
1
+ # dsh-push 内部契约(Host ⇄ Client)
2
+
3
+ 本文件是 Host 与 Client 两半共享的唯一契约。任何一方改动都必须先改这里。
4
+
5
+ ## 0. 通用约定
6
+
7
+ - HTTP 前缀:`/api/push`
8
+ - 所有响应为 JSON,统一信封:
9
+ - 成功:`{ "ok": true, "data": <payload> }`
10
+ - 失败:`{ "ok": false, "error": { "code": "<CODE>", "message": "<人类可读中文>" , "status"?: <github http status> } }`
11
+ - 非 GET 请求必须带请求头 `x-dsh-plugin: push`(CSRF 约定),缺失返回 403 `FORBIDDEN`。
12
+ - 客户端只通过 `fetch` 调这些路由,`credentials: 'same-origin'`,不要另起 RPC。
13
+ - 所有时间戳为 ISO 8601 字符串;所有 id 为字符串。
14
+ - 错误码常量(Host 与 Client 都要用):`FORBIDDEN`、`BAD_REQUEST`、`NOT_FOUND`、`NO_ACCOUNT`、`UNAUTHORIZED`(token 失效)、`RATE_LIMITED`、`NETWORK`、`GIT_MISSING`、`GIT_FAILED`、`NOT_A_REPO`、`CONFLICT`、`INTERNAL`。
15
+
16
+ ## 1. 账号(多账号登录/切换)
17
+
18
+ 账号对象 `Account`:
19
+ ```ts
20
+ {
21
+ id: string, // 本地稳定 id(login 的小写)
22
+ login: string,
23
+ name: string | null,
24
+ avatarUrl: string | null,
25
+ htmlUrl: string | null,
26
+ tokenKind: 'pat' | 'oauth', // 录入方式
27
+ scopes: string[], // 来自 x-oauth-scopes 响应头(无则 [])
28
+ addedAt: string,
29
+ lastUsedAt: string | null
30
+ }
31
+ ```
32
+
33
+ `GET /state` →
34
+ ```ts
35
+ {
36
+ accounts: Account[],
37
+ activeAccountId: string | null,
38
+ settings: {
39
+ oauthClientId: string, // 空串表示未配置设备码登录
40
+ defaultRemote: string, // 默认 'origin'
41
+ showPrivate: boolean,
42
+ gitPath: string // 空串表示用 PATH 里的 git
43
+ },
44
+ gitAvailable: boolean,
45
+ gitVersion: string | null
46
+ }
47
+ ```
48
+
49
+ - `POST /accounts/token` body `{ token: string, label?: string }` → `{ accounts, activeAccountId }`
50
+ - Host 用该 token 调 `GET /user` 校验;失败返回 `UNAUTHORIZED`。
51
+ - 同一 login 再次提交 = 覆盖 token(不新建)。
52
+ - `POST /accounts/device/start` body `{}` →
53
+ `{ verificationUri: string, userCode: string, deviceCode: string, expiresIn: number, interval: number }`
54
+ - 未配置 `oauthClientId` 时返回 `BAD_REQUEST`。
55
+ - `POST /accounts/device/poll` body `{ deviceCode: string }` →
56
+ `{ status: 'pending' | 'slow_down' | 'done', accounts?, activeAccountId? }`
57
+ - `POST /accounts/activate` body `{ accountId: string }` → `{ activeAccountId }`
58
+ - `POST /accounts/remove` body `{ accountId: string }` → `{ accounts, activeAccountId }`
59
+ - `POST /settings` body `Partial<settings>` → `{ settings }`
60
+
61
+ ## 2. 仓库(远端)
62
+
63
+ 远端仓库对象 `RemoteRepo`:
64
+ ```ts
65
+ {
66
+ id: string, owner: string, name: string, fullName: string,
67
+ private: boolean, fork: boolean, archived: boolean,
68
+ defaultBranch: string, description: string | null,
69
+ updatedAt: string, pushedAt: string | null,
70
+ htmlUrl: string, cloneUrl: string, sshUrl: string,
71
+ permissions: { admin: boolean, push: boolean, pull: boolean } | null,
72
+ language: string | null, stars: number, openIssues: number,
73
+ ownerAvatar: string | null
74
+ }
75
+ ```
76
+
77
+ - `GET /repos?page=1&q=&affiliation=owner,collaborator,organization_member&sort=updated` →
78
+ `{ items: RemoteRepo[], page: number, hasMore: boolean, total?: number }`
79
+ - `q` 非空时走 `GET /search/repositories?q=<q>`(`user:<login>` 前缀 + 关键词),结果同样映射为 `RemoteRepo`。
80
+ - `GET /repos/detail?owner=&name=` → `{ repo: RemoteRepo, branches: Branch[] }`
81
+ 其中 `Branch = { name: string, sha: string, protected: boolean }`
82
+ - `POST /repos/create` body `{ name, private=true, description='', autoInit=false }` → `{ repo: RemoteRepo }`
83
+ - `POST /repos/delete` body `{ owner, name }` → `{ deleted: true }`(需 `delete_repo` scope,否则 `UNAUTHORIZED` 并给中文提示)
84
+ - `GET /releases?owner=&repo=` → `{ items: Release[] }`
85
+ - `POST /releases` body `{ owner, repo, tagName, name?, body?, target?, draft=false, prerelease=false }` → `{ release: Release }`
86
+
87
+ `Release = { id: string, tagName: string, name: string | null, draft: boolean, prerelease: boolean, createdAt: string, htmlUrl: string }`
88
+
89
+ 只读的账号活动(`login` 省略时用当前账号):
90
+
91
+ - `GET /contributions?login=&days=365` → 贡献日历(走 GraphQL `contributionsCollection`)
92
+ ```ts
93
+ {
94
+ total: number, days: { date: 'YYYY-MM-DD', count: number, weekday: 0-6 }[], // 按 date 升序,Client 直接画格子
95
+ weeks: number, restricted: number, login: string, from: string, to: string
96
+ }
97
+ ```
98
+ - 令牌缺 `read:user` 时**不报错**,返回 `{ unavailable: true, reason: 'SCOPE', message: '贡献图需要令牌具备 read:user 权限;当前令牌没有该权限。' }` 且 `ok:true`,面板渲染行内提示即可。真正的网络失败仍是 `NETWORK`。
99
+ - `GET /notifications?all=false&limit=30` → `{ items: Notification[], unreadCount: number }`(`unreadCount` 为当前页未读数)
100
+ ```ts
101
+ Notification = {
102
+ id: string, title: string, type: string, // 'Issue' | 'PullRequest' | 'Release' | 'Discussion' | 'Commit' | ...
103
+ repo: string, // 'owner/name'
104
+ reason: string, // 'review_requested' | 'mention' | 'subscribed' | ...
105
+ unread: boolean, updatedAt: string,
106
+ url: string // 网页地址,由 subject.url 的 API 地址换算
107
+ }
108
+ ```
109
+ - `url` 换算:`pulls/{n}`→`/pull/{n}`、`issues/{n}`→`/issues/{n}`、`commits/{sha}`→`/commit/{sha}`、`discussions/{n}`→`/discussions/{n}`、`releases/{n}`→`/releases`,其它一律 `https://github.com/{repo}`。
110
+ - `POST /notifications/read` body `{ id }` → `{ read: true }`(`PATCH /notifications/threads/{id}`;205 无响应体也算成功)
111
+
112
+ ## 3. 本地仓库(Git)
113
+
114
+ - `GET /local/status?path=<绝对路径>` →
115
+ ```ts
116
+ {
117
+ isRepo: boolean,
118
+ root: string | null, // rev-parse --show-toplevel
119
+ gitDir: string | null,
120
+ branch: string | null, // 分离 HEAD 时为 null
121
+ detachedHead: boolean,
122
+ upstream: string | null, // 如 'origin/main'
123
+ ahead: number, behind: number,
124
+ remotes: { name: string, url: string }[],
125
+ githubRemote: { name: string, owner: string, repo: string, url: string } | null,
126
+ head: { sha: string, shortSha: string, subject: string, author: string, date: string } | null,
127
+ dirty: { changed: number, staged: number, untracked: number, conflicted: number },
128
+ files: { path: string, index: string, worktree: string, staged: boolean, untracked: boolean, conflicted: boolean }[],
129
+ tags: string[],
130
+ branches: string[],
131
+ stashes: { index: number, message: string }[]
132
+ }
133
+ ```
134
+ - `GET /local/log?path=&limit=60&ref=` →
135
+ `{ commits: Commit[], hasMore: boolean }`
136
+ ```ts
137
+ Commit = {
138
+ sha: string, shortSha: string, parents: string[], author: string, authorEmail: string,
139
+ date: string, subject: string, refs: string[], // 装饰引用,如 ['HEAD -> main','origin/main','tag: v1']
140
+ lane: number // Host 计算好的图列(0..N),Client 直接画
141
+ }
142
+ ```
143
+ - `GET /local/diff?path=&file=&staged=true|false` → `{ file: string, patch: string }`(`git diff` 原文,可能很长;Host 截断到 200_000 字符并给 `truncated: true`)
144
+ - `GET /local/commit?path=&sha=` → `{ sha, subject, body, author, date, files: { path, additions, deletions }[] , patch: string }`
145
+ - `GET /local/remotes?path=` → `{ remotes }`
146
+
147
+ 写操作(全部 POST,body 都带 `path`):
148
+
149
+ | 路由 | body | 说明 |
150
+ |---|---|---|
151
+ | `/local/init` | `{ path, name?, owner?, private?, remoteName='origin', createRemote=false }` | `git init` + 可选在 GitHub 建仓 + `remote add` + 首次 `git add -A`(不自动提交) |
152
+ | `/local/remote` | `{ path, name='origin', url }` | 增改远端;url 为 GitHub 时顺便解析 owner/repo |
153
+ | `/local/fetch` | `{ path, remote='origin', prune=false }` | |
154
+ | `/local/pull` | `{ path, remote='origin', branch?, rebase=false }` | |
155
+ | `/local/push` | `{ path, remote='origin', branch?, force=false, setUpstream=true, tags=false }` | 一键推送 |
156
+ | `/local/commit` | `{ path, message, all=true, files?: string[] }` | `all` 时 `git add -A` 后提交 |
157
+ | `/local/checkout` | `{ path, ref, create=false }` | ref 可为分支/sha/tag |
158
+ | `/local/branch` | `{ path, name, from?, checkout=true }` | 新建分支 |
159
+ | `/local/branch/delete` | `{ path, name, force=false }` | |
160
+ | `/local/merge` | `{ path, ref }` | |
161
+ | `/local/reset` | `{ path, ref='HEAD', mode='mixed' }` | 撤回:soft/mixed/hard |
162
+ | `/local/revert` | `{ path, sha }` | |
163
+ | `/local/revert-commit` | `{ path, sha }` | `git revert --no-edit <sha>` |
164
+ | `/local/tag` | `{ path, name, ref='HEAD', push=false, remote='origin' }` | |
165
+ | `/local/stash` | `{ path, message?, includeUntracked=true }` | |
166
+ | `/local/stash/pop` | `{ path, index=0 }` | |
167
+ | `/local/restore` | `{ path, files: string[], staged=false }` | 丢弃工作区改动 |
168
+ | `/local/discard-all` | `{ path }` | 危险操作,`git reset --hard` + 清 untracked |
169
+
170
+ 所有写操作返回 `{ ok:true, data: { output: string } }`,`output` 为 git 的 stdout+stderr 合并(截断 20_000 字符)。失败返回 `GIT_FAILED` 并带原始输出。
171
+
172
+ ## 3b. 推送目标(远端仓库选择 + 按分支记忆)
173
+
174
+ 推送目标不再依赖全局 `defaultRemote`。分三层:
175
+
176
+ 1. **远端(git remote)**:本地已有的 remote name,例如 `origin`、`fork`。
177
+ 2. **目标 GitHub 仓库**:一个远端可以指向任意仓库;用户想推到「另一个 GitHub 仓库」时,本地可能还没有对应 remote。
178
+ 3. **记忆**:`(仓库根, 分支)` → 上次成功推送的目标,下次默认选中。
179
+
180
+ - `GET /push/targets?path=` → 选择器用的候选集合
181
+ ```ts
182
+ {
183
+ remotes: { name: string, url: string, owner: string | null, repo: string | null }[],
184
+ memory: { remote: string, owner: string, repo: string, ref: string, at: string } | null, // 当前分支的记忆
185
+ branch: string | null,
186
+ repoRoot: string | null
187
+ }
188
+ ```
189
+ - `memory` 是**当前分支**上次成功推送的目标;没有则 `null`。
190
+
191
+ - `GET /push/remotes?q=&page=1` → 在已登录账号的 GitHub 仓库中检索(供「下拉选择」与「键入搜索」两种 UI)
192
+ ```ts
193
+ {
194
+ items: { fullName: string, owner: string, name: string, private: boolean,
195
+ defaultBranch: string, cloneUrl: string, sshUrl: string, htmlUrl: string }[],
196
+ page: number, hasMore: boolean
197
+ }
198
+ ```
199
+ - `q` 为空时按最近更新列出;非空时匹配 `owner/name` 与描述。
200
+
201
+ - `POST /local/push` **新增可选字段**:
202
+ ```ts
203
+ {
204
+ path, branch?, force?, setUpstream?, tags?,
205
+ remote?: string, // 已有 remote 名(二选一)
206
+ target?: { owner: string, repo: string }, // 直接指定 GitHub 仓库(二选一)
207
+ remember?: boolean // 默认 true;成功后写入记忆
208
+ }
209
+ ```
210
+ - 传 `target` 且本地没有对应 remote 时,**不得污染用户 .git/config**:若已有 remote 指向同一仓库则复用;否则以远端 URL 形式推送(`git push <url> <ref>`),令牌沿用现有 `http.extraheader` 机制,绝不落盘。
211
+ - 成功后按 `(repoRoot, branch)` 记录 `{ remote, owner, repo, ref, at }`;`branch` 省略时取当前分支。
212
+
213
+ - `GET /push/memory?path=` → `{ entries: { branch, remote, owner, repo, ref, at }[] }`(全部分支,调试用)
214
+
215
+ 记忆持久化:状态文档新增 `pushMemory` 表,键 `<repoRoot>|<branch>`。路径需规范化(统一分隔符、盘符小写),避免同一仓库因写法不同丢记忆。
216
+
217
+ ### `/local/push` 的版本来源
218
+
219
+ `POST /local/push` 的 body 除 `branch` / `force` / `remote` / `target` 外,还接受:
220
+
221
+ ```ts
222
+ {
223
+ source?: string // 要推送的 ref:分支名、标签名或 40 位 sha
224
+ }
225
+ ```
226
+
227
+ - 省略时等于 `branch`(推当前分支)
228
+ - 指定 sha 或标签时,Host 构造 `<source>:refs/heads/<branch>` 形式的 refspec,
229
+ 因此「推送某个指定版本」不会悄悄退化成推送当前分支
230
+ - 目标分支缺失时先返回 `BAD_REQUEST`,不会发起任何网络请求
231
+
232
+
233
+ ## 3c. 仓库管理(弹窗所需的聚合能力)
234
+
235
+ `GET /repos/manage?owner=&name=` →
236
+ ```ts
237
+ {
238
+ repo: RemoteRepo,
239
+ branches: { name: string, sha: string, protected: boolean }[],
240
+ topics: string[],
241
+ stats: { stars: number, forks: number, watchers: number, openIssues: number, openPRs: number,
242
+ sizeKb: number, language: string | null, license: string | null,
243
+ createdAt: string, updatedAt: string, pushedAt: string | null },
244
+ releases: Release[],
245
+ collaborators: { login: string, avatarUrl: string | null, role: string }[] | null,
246
+ webhooks: { id: string, url: string, events: string[], active: boolean }[] | null,
247
+ warnings: string[],
248
+ can: { admin: boolean, push: boolean, delete: boolean }
249
+ }
250
+ ```
251
+ - 任一子项失败**不得**让整体失败:该字段返回 `null`,并在 `warnings` 写中文原因(如缺 `admin:repo_hook` 读不到 webhook)。
252
+
253
+ 写操作(全部 POST,失败给中文原因):
254
+
255
+ | 路由 | body | 说明 |
256
+ |---|---|---|
257
+ | `/repos/update` | `{ owner, name, description?, homepage?, private?, archived?, topics?, defaultBranch? }` | 改元信息 |
258
+ | `/repos/branch/rename` | `{ owner, name, from, to }` | 重命名分支 |
259
+ | `/repos/branch/delete` | `{ owner, name, branch }` | 删除远端分支 |
260
+ | `/repos/branch/protect` | `{ owner, name, branch, protected }` | 保护/取消保护(需 admin) |
261
+ | `/repos/release/delete` | `{ owner, name, id }` | 删除发布 |
262
+ | `/repos/release/update` | `{ owner, name, id, name?, body?, draft?, prerelease? }` | 编辑发布 |
263
+ | `/repos/collaborator/add` | `{ owner, name, user, permission }` | 添加协作者(需 admin) |
264
+ | `/repos/collaborator/remove` | `{ owner, name, user }` | 移除协作者 |
265
+ | `/repos/rename` | `{ owner, name, newName }` | 重命名仓库(危险,需 `confirm: true`) |
266
+ | `/repos/transfer` | `{ owner, name, newOwner }` | 转移仓库(危险,需 `confirm: true`) |
267
+
268
+ `/repos/rename`、`/repos/transfer` 与既有 `/repos/delete` 一律要求 `confirm: true`,否则返回 `CONFLICT` 并附中文说明。
269
+
270
+ ## 3d. 推送前审查(Agent Review)
271
+
272
+ 在推送前让模型检查「这次要推的内容」,把风险在离开本机之前拦下来。审查复用宿主已注册的
273
+ `llm` 服务,不自行发起 HTTP,因此走同一个 provider、同一套凭据与重试策略。
274
+
275
+ ### 预设与自定义提示词
276
+
277
+ - `GET /review/presets` →
278
+ ```ts
279
+ {
280
+ builtin: ReviewPreset[], // Host 内置,只读
281
+ custom: ReviewPreset[], // 用户存档
282
+ categories: { id: string, label: string }[]
283
+ }
284
+ ```
285
+ ```ts
286
+ ReviewPreset = {
287
+ id: string,
288
+ category: string, // 'secrets' | 'privacy' | 'integrity' | 'quality' | 'custom'
289
+ title: string,
290
+ description: string, // 一句话说明它查什么
291
+ prompt: string, // 发给模型的指令
292
+ builtin: boolean,
293
+ createdAt?: string,
294
+ updatedAt?: string
295
+ }
296
+ ```
297
+ 内置分类至少包含:**敏感信息**(令牌、私钥、密码、身份证/手机号/邮箱、内网地址、
298
+ 设备与用户名痕迹)、**隐私**(个人姓名、绝对路径中的用户名、机器名)、
299
+ **完整性**(空实现、调试残留、被删除的测试、冲突标记、未跟踪依赖)、
300
+ **质量**(明显 bug、错误处理缺失、硬编码配置)。
301
+
302
+ - `POST /review/presets` body `{ title, description?, prompt, category? }` → `{ preset: ReviewPreset }`
303
+ 存档一条自定义提示词;`title` 与 `prompt` 必填且非空。
304
+ - `POST /review/presets/delete` body `{ id }` → `{ deleted: true }`
305
+ 只能删自定义预设;删内置返回 `CONFLICT`。
306
+
307
+ ### 审查请求闸门(agent 不得自行审查)
308
+
309
+ 智能体**不能**直接执行审查。它只能登记一个待确认请求,由界面弹出与人工点击
310
+ 相同的审查弹窗,用户选择审查项后才真正开跑。原因:审查是安全步骤,用户必须
311
+ 看得见、能决定审查什么;早期允许直接执行时,agent 会在「推送」任务里反复自动
312
+ 审查而不提示用户。
313
+
314
+ - `GET /review/pending?path=` → `{ pending: { id, path, presetIds, customPrompt, source, createdAt } | null }`
315
+ - 客户端轮询它;非空即打开审查弹窗,并用 `presetIds` 预选、`customPrompt` 预填
316
+ - 同一时刻只保留**一个**待确认请求
317
+
318
+ - `POST /review/pending/clear` body:`{ id?: string }` → `{ cleared: boolean }`
319
+ - 用户开始审查或关闭弹窗时调用;`id` 不匹配则不清理(避免误清掉新请求)
320
+
321
+ 工具 `review` 的行为:
322
+
323
+ ```ts
324
+ { reviewRequest: {
325
+ status: 'awaiting_user' | 'already_pending',
326
+ message: string, // 给模型看的说明:等用户确认,不要重复调用
327
+ pending: object | null
328
+ } }
329
+ ```
330
+
331
+ - **不再返回 findings**:结果由界面在用户确认后产生
332
+ - 已有待确认请求时返回 `already_pending`,这是阻止 agent 反复请求的机制
333
+
334
+
335
+ ### 审查执行
336
+
337
+ - `POST /review/run` body:
338
+ ```ts
339
+ {
340
+ path: string, // 仓库绝对路径
341
+ presetIds?: string[], // 选中的内置/自定义预设
342
+ customPrompt?: string, // 本次临时输入的额外指令(可空)
343
+ provider?: string, model?: string, // 省略时用宿主默认模型
344
+ reasoningEffort?: string, // 省略时用该模型默认档
345
+ staged?: boolean, // true 只审已暂存;false 审全部未推送改动
346
+ includeFiles?: string[], // 限定文件;省略=按下面规则自动选
347
+ maxFiles?: number // 默认 60
348
+ }
349
+ ```
350
+ → `{ reviewId, model, effort, files, findings, summary, usage }`
351
+
352
+ ```ts
353
+ {
354
+ reviewId: string,
355
+ model: { provider: string, model: string, label: string } | null,
356
+ effort: string | null,
357
+ scope: 'working' | 'outgoing', // 审的是工作区改动还是待推送提交
358
+ outgoingCommits: number, // scope=outgoing 时的提交数,否则 0
359
+ truncated: boolean, // 改动过多、有内容未送审
360
+ degraded: boolean, // 模型未参与、只跑了规则扫描
361
+ files: { path: string, additions: number, deletions: number, skipped?: string }[],
362
+ findings: {
363
+ severity: 'blocker' | 'high' | 'medium' | 'low' | 'info',
364
+ category: string,
365
+ title: string,
366
+ detail: string,
367
+ file?: string,
368
+ line?: number,
369
+ suggestion?: string
370
+ }[],
371
+ summary: string,
372
+ usage?: { inputTokens: number, outputTokens: number }
373
+ }
374
+ ```
375
+
376
+ 规则:
377
+
378
+ 1. **只审将要推送的差异**。默认取 `HEAD` 与工作区的差异(含未跟踪文件,逐个读取内容);
379
+ `staged: true` 时只取 `--cached`。单文件内容超过 200 KB 或二进制时标记 `skipped`。
380
+ 2. **工作区没有改动时,改审待推送提交**。推送前的常态是工作区干净、提交已就绪,
381
+ 此时若只读工作区就会返回「无改动」并零发现——对安全功能而言这是最糟的静默漏报。
382
+ 回退规则:有 upstream 时以 `merge-base(upstream, HEAD)` 为基准;无 upstream 时以
383
+ `refs/remotes/origin/<branch>` 为基准;两者都不存在(分支首次推送)时以空树
384
+ `4b825dc642cb6eb9a060e54bf8d69288fbee4904` 为基准。结果中的 `scope` 字段标明
385
+ 本次审查的是哪一类(`working` / `outgoing`),`outgoingCommits` 给出待推送提交数。
386
+ 3. **模式规则先行**(不花钱):Host 先用正则扫一遍已知密钥形态
387
+ (`ghp_[A-Za-z0-9]{36}`、`github_pat_`、`sk-`、`AKIA[0-9A-Z]{16}`、私钥块、
388
+ `xox[baprs]-`、Bearer 长串、以及 `password` / `token` / `api_key` 等键名后跟长字面量的硬编码密码),命中即产出 `blocker` 级 finding。
389
+ 这样即使模型不可用,最危险的一类问题也有答案。
390
+ 4. **模型审查**:把选中预设的 prompt + 自定义提示词拼成一次 `llm` 调用,
391
+ 要求模型只输出 JSON(`{findings:[],summary:""}`)。解析失败时降级:
392
+ 把模型原文放进 `summary` 且 `findings` 为模式规则的结果,不报错。
393
+ 5. 令牌等秘密**永不写进 findings**:命中时只保留位置与类型(如 `ghp_…(已隐藏)`)。
394
+ 6. **模型不可用**(未注册 provider、鉴权失败、网络错误):返回
395
+ `{ ok:true, data:{ ..., model: null, findings: <模式规则结果>, summary: '<中文说明>' } }`,
396
+ 并在 `degraded: true` 标注;绝不因审查失败阻塞推送。
397
+
398
+ ### 模型与强度候选
399
+
400
+ - `GET /review/models` → `{ providers: { id, name, models: { id, name, efforts: { id, name }[], defaultEffort }[] }[], default: { provider, model } | null }`
401
+ 由 Host 侧 `llm` 服务枚举;宿主没有 `llm` 时返回空数组与 `default: null`,
402
+ 界面据此显示「宿主未提供模型配置」。
403
+
404
+ ## 3e. 仓库信息与发布附件
405
+
406
+ 客户端用到的、与仓库展示和发布相关的能力。
407
+
408
+ ### 语言构成
409
+
410
+ - `GET /repos/languages?owner=&name=`
411
+ → `{ items: { name: string, bytes: number, percent: number }[], warning?: string }`
412
+ - 按字节降序;`percent` 保留一位小数,由 Host 计算,客户端只渲染
413
+ - 仓库不可见或无语言数据时返回 `{ items: [] }`,**不报错**(卡片只是不画条形图)
414
+
415
+ ### 发布附件
416
+
417
+ - `POST /releases/asset` body:
418
+ ```ts
419
+ {
420
+ owner: string, name: string, releaseId: string,
421
+ fileName: string,
422
+ contentType?: string,
423
+ base64: string // 文件内容;上限 200 MB,超出返回 BAD_REQUEST
424
+ }
425
+ ```
426
+ → `{ asset: { id, name, size, downloadUrl } }`
427
+ - 附件上传走 `uploads.github.com`(不是 `api.github.com`),URL 由 `API_BASE` 推导
428
+
429
+ - `POST /releases/asset/delete` body:`{ owner, name, assetId }` → `{ deleted: true }`
430
+ - 404 也视为已删除(幂等)
431
+
432
+ ### 修改发布
433
+
434
+ - `POST /repos/release/update` body:
435
+ ```ts
436
+ {
437
+ owner: string,
438
+ name: string, // 仓库名
439
+ id: string, // 发布 id
440
+ releaseName?: string, // 发布标题(不要用 name,那是仓库名)
441
+ body?: string,
442
+ draft?: boolean,
443
+ prerelease?: boolean
444
+ }
445
+ ```
446
+
447
+ > `name` 专指仓库名。发布标题必须用 `releaseName`——早期两者共用 `name`,
448
+ > 对象字面量里重复的键让仓库名被标题覆盖,保存已有发布必然 404。
449
+
450
+
451
+ ### 发布对象
452
+
453
+ `Release` 额外携带附件列表,供界面展示与删除:
454
+ ```ts
455
+ assets: { id: string, name: string, size: number, downloadUrl: string }[]
456
+ ```
457
+
458
+ ### 仓库字段补充
459
+
460
+ `RemoteRepo` 除 `fork: boolean` 外还带计数 `forks: number`(列表与卡片展示用)。
461
+
462
+ ### 推送记忆
463
+
464
+ - `POST /local/push/target/forget` body:`{ path, branch? }` → `{ forgotten: true }`
465
+ - 清除该分支(或全部)的推送目标记忆
466
+
467
+ ### 诊断
468
+
469
+ - `GET /diagnostics` → `{ toolsService, tool: { name, attempted, registered, visible, inSchemas, error } }`
470
+ - 回答「智能体为什么看不到 `github_link`」。注册失败会被记录下来而不是只写日志。
471
+
472
+
473
+ ## 4. Agent 工具(可选但要对齐)
474
+
475
+ Host 另注册一个工具 `github_link`,`action` 参数取值与上表一致(`status`、`log`、`push`、`pull`、`commit`、`repos`、`accounts`)。它调用同一份 Host 服务方法,不另写逻辑。
476
+
477
+ ## 5. 客户端职责
478
+
479
+ - `settings.section`:`{ name:'settings.section', id:'push', order:22, label:()=>'GitHub' }`
480
+ 页面内容:
481
+ 1. 账号列表(头像、login、当前标记、切换、移除)+ 添加账号(PAT 输入 / 设备码流程)
482
+ 2. 连接设置(oauthClientId、gitPath、defaultRemote、showPrivate)
483
+ 3. 仓库列表(搜索、私有过滤、分页"加载更多"、点进详情看分支)
484
+ - `sidebar.panellist`:`{ name:'sidebar.panellist', id:'push', order:2, label:()=>'GitHub' }` + `main` key `push` 的整页:仓库浏览 + 新建仓库 + 删除。
485
+ - `ctx.betterSidebar?.registerTab(...)`:id `push`,标题「GitHub 链接」,单实例,组件按当前会话 `scope.cwd` 显示该工作区的本地版本状态、分支、ahead/behind、文件改动、Git 图(最近 60 条,带 lane 连线)、以及提交/推送/拉取/撤回/切分支/打标签/暂存等操作。**这是"工作区侧边栏入口"。**
486
+ - 所有可见文案中文;颜色只用 `--dsw-alias-*` 主题 token;不要渐变、不要 emoji 图标,用内联 SVG。
487
+
488
+ ## 6. 文件归属(避免写冲突)
489
+
490
+ - `dsh-push/index.js`、`dsh-push/src/host/**` → host-dev
491
+ - `dsh-push/client.js`、`dsh-push/client.css` → client-dev
492
+ - `dsh-push/package.json`、`cordis.patch.yml`、`locale/**`、`icon.svg`、`README.md` → lead
493
+
494
+ ### 动作清单
495
+
496
+ ```text
497
+ status · log · push · pull · commit · repos · accounts
498
+ create_repo · release · review
499
+ ```
500
+
501
+ - `push` / `release` 都接受 `owner` + `repo` 指定目标仓库,因此智能体无需预先配置远端
502
+ - `review` **不执行审查**,只登记待确认请求并返回 `reviewRequest`;用户确认后由界面调用 `reviewRun`
503
+ - 工具结果必须是**无损 JSON**:findings 的缺失字段一律投影为 `null`,不能留 `undefined`
504
+ - schema 只能用宿主支持的子集(`type` / `oneOf` / `properties` / `required` /
505
+ `additionalProperties` / `items` / `enum` / `const`);`type: [...]` 与 `anyOf`
506
+ 会让整个注册失败,工具从此对模型不可见
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QingJing5906
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.