@perrylink/dsh-github 0.6.0 → 0.6.2

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.
@@ -0,0 +1,259 @@
1
+ // Local simulator for the composite action — explicitly isolated.
2
+ //
3
+ // The real action resolves every path from the runner environment:
4
+ // DSH_HOME = ${{ runner.temp }}/dsh-home
5
+ // DSH_PROFILE_DIR = ${{ runner.temp }}/dsh-home/profiles/headless
6
+ // output dir = ${{ inputs.output-dir }} (default ${{ runner.temp }}/dsh-github)
7
+ // A naive local copy inherits the developer's real environment instead: a
8
+ // process-level (or machine-scope) DSH_HOME wins over everything, so the
9
+ // headless profile, sessions, storages, and reports would be written straight
10
+ // into the real dsh home — which is exactly what shuts local content down.
11
+ //
12
+ // This script therefore never reads DSH_HOME / DSH_PROFILE_DIR / RUNNER_TEMP
13
+ // from the inherited environment. Every spawned step receives hardcoded
14
+ // process-level values rooted in a fresh system-temp sandbox:
15
+ // DSH_HOME = <tmp>/dsh-github-local-<n>/dsh-home
16
+ // DSH_PROFILE_DIR = <tmp>/dsh-github-local-<n>/dsh-home/profiles/headless
17
+ // RUNNER_TEMP = <tmp>/dsh-github-local-<n>
18
+ // GITHUB_WORKSPACE = --workspace (default: this repository root)
19
+ // INPUT_OUTPUT_DIR = <tmp>/dsh-github-local-<n>/output
20
+ //
21
+ // Steps replayed, mirroring action.yml: install → prepare (action-patch.mjs)
22
+ // → headless run → post (action-post.mjs). Nothing outside the sandbox is
23
+ // written. Run `node scripts/local-test.mjs --help` for the option list.
24
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
25
+ import { spawnSync } from 'node:child_process'
26
+ import { tmpdir } from 'node:os'
27
+ import { dirname, delimiter, isAbsolute, join, resolve } from 'node:path'
28
+ import { fileURLToPath } from 'node:url'
29
+
30
+ const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)))
31
+
32
+ const USAGE = `dsh-github local action simulator
33
+
34
+ Usage: node scripts/local-test.mjs [options]
35
+
36
+ Options:
37
+ --task <review|fix-ci|report> CI task to simulate (default: review)
38
+ --pr <number> Pull request number (required for review/fix-ci)
39
+ --owner-repo <owner/repo> Repository to review (required for review/fix-ci)
40
+ --task-prompt <text> Complete replacement for the default task text
41
+ --model <id> Model for the headless session (default: deepseek-v4-flash)
42
+ --engine <static|model> Review engine (default: static)
43
+ --check-name <name> Status-check name (default: dsh-github-review)
44
+ --blocking <true|false> Fail on needs-changes verdict (default: true)
45
+ --fail-on <error|warning> Lowest severity that flips the verdict (default: error)
46
+ --label-filters <a,b> Comma-separated label filters
47
+ --path-filters <glob,glob> Comma-separated path filters
48
+ --max-diff-chars <n> Diff character cap (default: 8000)
49
+ --post-comments <true|false> Post inline review comments (default: true)
50
+ --post-check <true|false> Publish the status check (default: true)
51
+ --request-timeout-ms <n> Per GitHub request timeout (default: 30000)
52
+ --plugin-version <version|path> @perrylink/dsh-github version or local folder
53
+ (default: latest)
54
+ --workspace <dir> GITHUB_WORKSPACE for the sandbox (default: repo root)
55
+ --skip-install Skip the npm install of dsh-github into the profile
56
+ --skip-run Only install + generate the overlay/task (no headless run)
57
+ --clean Delete the temp sandbox when finished
58
+
59
+ Environment (passed through from your shell, never logged):
60
+ DEEPSEEK_API_KEY Required (the action's deepseek-api-key input)
61
+ GITHUB_TOKEN | DSH_GITHUB_TOKEN Optional (the action's github-token input)
62
+
63
+ Isolation: DSH_HOME, DSH_PROFILE_DIR, RUNNER_TEMP, and the output directory are
64
+ hardcoded under the system temp directory for every spawned step, overriding any
65
+ inherited DSH_HOME (including machine-scope values). Your real dsh home is never
66
+ read or written.`
67
+
68
+ const args = process.argv.slice(2)
69
+ const options = {
70
+ task: 'review',
71
+ pr: '',
72
+ ownerRepo: '',
73
+ taskPrompt: '',
74
+ model: 'deepseek-v4-flash',
75
+ engine: 'static',
76
+ checkName: 'dsh-github-review',
77
+ blocking: 'true',
78
+ failOn: 'error',
79
+ labelFilters: '',
80
+ pathFilters: '',
81
+ maxDiffChars: '8000',
82
+ postComments: 'true',
83
+ postCheck: 'true',
84
+ requestTimeoutMs: '30000',
85
+ pluginVersion: 'latest',
86
+ workspace: repoRoot,
87
+ skipInstall: false,
88
+ skipRun: false,
89
+ clean: false,
90
+ }
91
+ const valueOf = { task: 1, pr: 1, 'owner-repo': 1, 'task-prompt': 1, model: 1, engine: 1, 'check-name': 1, blocking: 1, 'fail-on': 1, 'label-filters': 1, 'path-filters': 1, 'max-diff-chars': 1, 'post-comments': 1, 'post-check': 1, 'request-timeout-ms': 1, 'plugin-version': 1, workspace: 1 }
92
+ const keyOf = { task: 'task', pr: 'pr', 'owner-repo': 'ownerRepo', 'task-prompt': 'taskPrompt', model: 'model', engine: 'engine', 'check-name': 'checkName', blocking: 'blocking', 'fail-on': 'failOn', 'label-filters': 'labelFilters', 'path-filters': 'pathFilters', 'max-diff-chars': 'maxDiffChars', 'post-comments': 'postComments', 'post-check': 'postCheck', 'request-timeout-ms': 'requestTimeoutMs', 'plugin-version': 'pluginVersion', workspace: 'workspace' }
93
+ for (let i = 0; i < args.length; i++) {
94
+ const arg = args[i]
95
+ if (arg === '--help' || arg === '-h') { console.log(USAGE); process.exit(0) }
96
+ if (arg === '--skip-install') { options.skipInstall = true; continue }
97
+ if (arg === '--skip-run') { options.skipRun = true; continue }
98
+ if (arg === '--clean') { options.clean = true; continue }
99
+ const eq = arg.indexOf('=')
100
+ const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq)
101
+ if (name in valueOf) {
102
+ const inline = eq === -1 ? '' : arg.slice(eq + 1)
103
+ const value = inline !== '' ? inline : args[++i] ?? ''
104
+ options[keyOf[name]] = value
105
+ continue
106
+ }
107
+ if (arg.startsWith('-')) {
108
+ console.error(`dsh-github local-test: unknown option ${arg}`)
109
+ console.error('Run `node scripts/local-test.mjs --help` for the option list.')
110
+ process.exit(2)
111
+ }
112
+ }
113
+
114
+ // Sandbox — hardcoded under the system temp directory, derived from nothing
115
+ // that the developer's environment could point at their real home.
116
+ const sandboxRoot = mkdtempSync(join(tmpdir(), 'dsh-github-local-'))
117
+ const dshHome = join(sandboxRoot, 'dsh-home')
118
+ const profileDir = join(dshHome, 'profiles', 'headless')
119
+ const outputDir = join(sandboxRoot, 'output')
120
+ const workspace = isAbsolute(options.workspace) ? options.workspace : resolve(options.workspace)
121
+
122
+ console.log('dsh-github local action simulator — isolated sandbox')
123
+ console.log(` sandbox: ${sandboxRoot}`)
124
+ console.log(` DSH_HOME: ${dshHome}`)
125
+ console.log(` DSH_PROFILE_DIR:${profileDir}`)
126
+ console.log(` output dir: ${outputDir}`)
127
+ console.log(` workspace: ${workspace}`)
128
+ console.log('The real dsh home and any inherited DSH_HOME are never read or written.')
129
+
130
+ /** Run one shell command line, inheriting the caller's environment plus explicit overrides. */
131
+ function run(commandLine, { env = {}, allowFail = false, capture = false } = {}) {
132
+ const result = spawnSync(commandLine, {
133
+ shell: true,
134
+ cwd: repoRoot,
135
+ env: { ...process.env, ...env },
136
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
137
+ encoding: 'utf8',
138
+ maxBuffer: 64 * 1024 * 1024,
139
+ })
140
+ const status = typeof result.status === 'number' ? result.status : 1
141
+ if (!allowFail && status !== 0) {
142
+ console.error(`dsh-github local-test: command failed (exit ${status}): ${commandLine}`)
143
+ process.exit(status)
144
+ }
145
+ return { status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
146
+ }
147
+
148
+ /** Locate an executable (with a .cmd/.exe/.bat shim on Windows) on PATH. */
149
+ function findOnPath(name) {
150
+ const entries = (process.env.PATH ?? '').split(delimiter)
151
+ const extensions = process.platform === 'win32' ? ['cmd', 'exe', 'bat', ''] : ['']
152
+ for (const entry of entries) {
153
+ for (const extension of extensions) {
154
+ const candidate = join(entry, extension === '' ? name : `${name}.${extension}`)
155
+ if (candidate !== name && existsSync(candidate)) return candidate
156
+ }
157
+ }
158
+ return ''
159
+ }
160
+ const q = (value) => JSON.stringify(value)
161
+
162
+ // ---- Step 1: install the profile (action.yml "Install dsh and dsh-github") ----
163
+ mkdirSync(profileDir, { recursive: true })
164
+ writeFileSync(join(profileDir, 'package.json'), '{"private": true}\n')
165
+ const installEnv = { DSH_HOME: dshHome, DSH_PROFILE_DIR: profileDir }
166
+ if (options.skipInstall) {
167
+ console.log('skipping npm install (--skip-install): the overlay row @perrylink/dsh-github must resolve from the profile node_modules')
168
+ } else {
169
+ console.log(`installing @perrylink/dsh-github@${options.pluginVersion} into the sandboxed profile …`)
170
+ const target = existsSync(options.pluginVersion) || options.pluginVersion.includes('/') || options.pluginVersion.includes('\\')
171
+ ? resolve(options.pluginVersion)
172
+ : `@perrylink/dsh-github@${options.pluginVersion}`
173
+ run(`npm install --prefix ${q(profileDir)} --legacy-peer-deps --package-lock=false --no-save --no-audit --no-fund ${q(target)}`, { env: installEnv })
174
+ }
175
+
176
+ // ---- Step 2: prepare (action.yml "Generate the dsh profile overlay and task") ----
177
+ mkdirSync(outputDir, { recursive: true })
178
+ const inputs = {
179
+ INPUT_TASK: options.task,
180
+ INPUT_PR: options.pr,
181
+ INPUT_OWNER_REPO: options.ownerRepo,
182
+ INPUT_MODEL: options.model,
183
+ INPUT_ENGINE: options.engine,
184
+ INPUT_CHECK_NAME: options.checkName,
185
+ INPUT_BLOCKING: options.blocking,
186
+ INPUT_FAIL_ON: options.failOn,
187
+ INPUT_LABEL_FILTERS: options.labelFilters,
188
+ INPUT_PATH_FILTERS: options.pathFilters,
189
+ INPUT_MAX_DIFF_CHARS: options.maxDiffChars,
190
+ INPUT_POST_COMMENTS: options.postComments,
191
+ INPUT_POST_CHECK: options.postCheck,
192
+ INPUT_REQUEST_TIMEOUT_MS: options.requestTimeoutMs,
193
+ INPUT_TASK_PROMPT: options.taskPrompt,
194
+ INPUT_OUTPUT_DIR: outputDir,
195
+ }
196
+ run(`node ${q(join(repoRoot, 'scripts', 'action-patch.mjs'))}`, {
197
+ env: { RUNNER_TEMP: sandboxRoot, GITHUB_WORKSPACE: workspace, ...inputs },
198
+ })
199
+ console.log(`overlay and task written: ${join(outputDir, 'dsh-github-ci.cordis.yml')}`)
200
+
201
+ if (options.skipRun) {
202
+ console.log('stopping after prepare (--skip-run); the sandbox is kept for inspection.')
203
+ console.log(`sandbox: ${sandboxRoot}`)
204
+ process.exit(0)
205
+ }
206
+
207
+ // ---- Step 3: headless run (action.yml "Run dsh headless") ----
208
+ if ((process.env.DEEPSEEK_API_KEY ?? '').trim() === '') {
209
+ console.error('dsh-github local-test: DEEPSEEK_API_KEY is required (the action\'s deepseek-api-key input). Set it in your shell, not in the profile.')
210
+ console.error(`sandbox kept for inspection: ${sandboxRoot}`)
211
+ process.exit(2)
212
+ }
213
+ if (findOnPath('dsh') === '') {
214
+ console.error('dsh-github local-test: the dsh CLI was not found on PATH. Install it first: npm install --global @deepseek-ai/dsh')
215
+ console.error(`sandbox kept for inspection: ${sandboxRoot}`)
216
+ process.exit(2)
217
+ }
218
+ const githubToken = process.env.DSH_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? ''
219
+ if (githubToken === '' && options.postComments === 'true') {
220
+ console.log('note: no DSH_GITHUB_TOKEN / GITHUB_TOKEN in the environment — posting comments and the status check will fail per operation (static dry inspections still work).')
221
+ }
222
+ const taskText = readFileSync(join(outputDir, 'task.txt'), 'utf8')
223
+ const overlayPath = join(outputDir, 'dsh-github-ci.cordis.yml')
224
+ const runEnv = {
225
+ DSH_HOME: dshHome,
226
+ DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
227
+ DSH_GITHUB_CI_DRIVER: '1',
228
+ DSH_GITHUB_CI_OUTPUT_DIR: outputDir,
229
+ DSH_TELEMETRY_DISABLED: '1',
230
+ }
231
+ if (githubToken !== '') runEnv.DSH_GITHUB_TOKEN = githubToken
232
+ console.log('running dsh headless in the sandboxed home …')
233
+ const headless = run(`dsh --profile headless --patch ${q(overlayPath)} ${q(taskText)}`, { env: runEnv, allowFail: true, capture: true })
234
+ writeFileSync(join(outputDir, 'dsh-github-stdout.log'), headless.stdout)
235
+ writeFileSync(join(outputDir, 'dsh-github-stderr.log'), headless.stderr)
236
+ writeFileSync(join(outputDir, 'dsh-github-exit.txt'), `${headless.status}\n`)
237
+ console.log(headless.stdout)
238
+ if (headless.stderr.trim() !== '') console.log(headless.stderr)
239
+
240
+ // ---- Step 4: post (action.yml "Publish outputs and enforce the gate") ----
241
+ const post = run(`node ${q(join(repoRoot, 'scripts', 'action-post.mjs'))}`, {
242
+ env: {
243
+ RUNNER_TEMP: sandboxRoot,
244
+ GITHUB_WORKSPACE: workspace,
245
+ INPUT_OUTPUT_DIR: outputDir,
246
+ INPUT_BLOCKING: options.blocking,
247
+ GITHUB_OUTPUT: join(outputDir, 'github-output.env'),
248
+ },
249
+ allowFail: true,
250
+ })
251
+ console.log(`\nsandbox kept at: ${sandboxRoot}`)
252
+ console.log(` result: ${join(outputDir, 'dsh-github-ci-result.json')}`)
253
+ console.log(` summary: ${join(outputDir, 'dsh-github-ci-summary.md')}`)
254
+ console.log(` logs: ${join(outputDir, 'dsh-github-stdout.log')} / dsh-github-stderr.log`)
255
+ if (options.clean) {
256
+ rmSync(sandboxRoot, { recursive: true, force: true })
257
+ console.log('sandbox removed (--clean).')
258
+ }
259
+ process.exit(post.status)
package/README.zh-CN.md DELETED
@@ -1,292 +0,0 @@
1
- <h1 align="center">dsh-github</h1>
2
-
3
- <p align="center">
4
- <b>把 GitHub 接入 DeepSeek Harness。</b><br/>
5
- 创建 PR · 行级或汇总评论审查 PR · 管理 issue · 搜索 —— 每个写操作都经人类审批,token 永不落日志。
6
- </p>
7
-
8
- <p align="center">
9
- <a href="README.md">English</a> ·
10
- <a href="README.es.md">Español</a> ·
11
- <a href="README.pt.md">Português</a> ·
12
- <a href="README.hi.md">हिन्दी</a>
13
- </p>
14
-
15
- <p align="center">
16
- <img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License: Apache 2.0">
17
- <img src="https://img.shields.io/badge/dsh-0.1.0--rc.6-4D6BFE" alt="dsh: 0.1.0-rc.6">
18
- <img src="https://img.shields.io/badge/dsh-dsh--plugin-4D6BFE" alt="dsh-plugin">
19
- <img src="https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-brightgreen" alt="Node: ^22.19 || >=24">
20
- <img src="https://github.com/PerryLink/dsh-github/actions/workflows/ci.yml/badge.svg" alt="CI">
21
- <img src="https://img.shields.io/badge/documents-EN%2FZH%2FES%2FPT%2FHI-8257D0" alt="Documents: EN/ZH/ES/PT/HI">
22
- </p>
23
-
24
- ---
25
-
26
- **dsh-github** 是 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`,"一切皆插件"的 agent 框架)的 bundle 插件。它填补了 dsh 相对 [Claude Code](https://github.com/anthropics/claude-code)(`gh claude` / [claude-code-action](https://github.com/anthropics/claude-code-action))与 [Codex](https://github.com/openai/codex)(`@codex review` / Autofix CI)的 GitHub 集成空白:agent 能**看 PR、审 PR、开 PR、合并与更新 PR、读仓库元数据与文件、评论与关闭 issue、搜索**——写操作由人类审批,token 全程保密。
27
-
28
- - 🛠 **12 个工具** —— `pr_create` · `pr_merge` · `pr_update` · `gh_review` · `review_post` · `gh_issue` · `issue_open` · `issue_comment` · `issue_close` · `gh_search` · `gh_repo` · `gh_file`,全部经 `defineTool` 返回规范 JSON
29
- - ⌨️ **3 族命令** —— `/pr create` · `/review`(启动/停止/发布)· `/issue open`
30
- - 🔀 **完整 PR 生命周期** —— 创建 → 审查 → 更新(标题/正文/状态/目标分支)→ 合并(merge/squash/rebase,可选合并后删源分支)
31
- - 📝 **行级审查评论** —— `review_post` 既可发布单条汇总评论,也可按行锚定到 PR head commit 发布行级 review 评论
32
- - 🔒 **写操作审批** —— 每个 GitHub 写操作都经 `ctx.approval`(默认 `ask`,fail-closed);审批理由预览标题、正文长度与评论覆盖内容
33
- - 🗝 **token 保密** —— credentials seam → 环境变量 → `gh` CLI 三级解析,逐操作执行,绝不进日志/事件/渲染/错误
34
- - ⏱ **后台审查 job** —— `/review` 跑在 `ctx.jobs` 上,复用宿主自带 `job_list` / `job_output` / `job_kill` 工具面;输出除发现外还带 CI 状态与既有评论数
35
- - 🤖 **可选模型评审** —— `reviewMode: "model"` 把截断后的 diff 交给宿主 `subagents` 接缝的一次性 subagent 评审;默认 `static` 模式保持确定性、零 token
36
- - 🚦 **429 退避 + 配额可见** —— 每个结果(含失败)都向模型暴露剩余配额;各分节抓取失败显式上报,不再静默吞掉
37
- - 🌐 **5 语文档** —— English · 中文 · Español · Português · हिन्दी
38
-
39
- ---
40
-
41
- ## 📚 目录
42
-
43
- - [快速上手](#🚀-快速上手)
44
- - [特性](#✨-特性)
45
- - [安装](#📦-安装)
46
- - [配置](#⚙️-配置)
47
- - [工具](#🛠-工具)
48
- - [命令](#⌨️-命令)
49
- - [架构](#🏗-架构)
50
- - [安全边界](#🔒-安全边界)
51
- - [已知局限](#⚠️-已知局限)
52
- - [开发](#🧪-开发)
53
- - [目录结构](#🗂-目录结构)
54
- - [Topics](#🏷-topics)
55
- - [许可证](#许可证)
56
- - [PerryLink DSH 插件家族](#perrylink-dsh-插件家族)
57
-
58
- ## 🚀 快速上手
59
-
60
- ```sh
61
- # 1. 安装(npm registry —— 最简单;也可用下方 tarball 通道)
62
- dsh plugin --profile <name> add @perrylink/dsh-github
63
- # tarball 通道(不需要 registry):
64
- # pnpm pack → dsh-github-<version>.tgz
65
- # dsh plugin --profile <name> add ./dsh-github-<version>.tgz
66
-
67
- # 2. 配置 GitHub token(推荐:credentials seam)
68
- # $DSH_HOME/.credentials.yaml
69
- # GITHUB_TOKEN: <你的 token>
70
-
71
- # 3. 使用 —— dsh Web UI 或 headless 均可
72
- # /pr create "add dark mode" → agent 起草并创建 PR(需审批)
73
- # /review 42 → 后台审查 job,用 job_output 读结论
74
- # /review post github-review-1 → 发布审查评论(需审批)
75
- # /issue open "crash on startup" → agent 创建 issue(需审批)
76
- ```
77
-
78
- 验证:`dsh --profile <name> --dump-config` 应显示 `# == dsh-github` 段且**无 FAILED 行**。
79
-
80
- ## ✨ 特性
81
-
82
- | 领域 | 你能得到什么 |
83
- |---|---|
84
- | **创建 PR** | `/pr create [标题]` 读取 git 状态(分支、变更文件、未推送提交),把草稿交给 agent;`pr_create` 创建 PR 并返回 URL |
85
- | **更新 PR** | `pr_update` 修改标题、正文、状态或目标分支——与其它写操作一样经审批门 |
86
- | **合并 PR** | `pr_merge` 以 `merge`/`squash`/`rebase` 合并,可选提交标题/消息与合并后删除源分支 |
87
- | **审查 PR** | `gh_review` 汇总元数据、截断 diff(canonical 值含完整 diff 文本、渲染面为有界摘要)、评论、CI 状态与静态发现;各分节抓取失败以 `diff.error` / `comments.error` / `ci.error` 显式上报 |
88
- | **发布审查** | `review_post` 发布单条 issue 级汇总评论(`mode: "summary"`,默认)或按行锚定 PR head commit 的行级评论(`mode: "inline"`);`body` 覆盖参数让模型先润色评论——发布前必须审批 |
89
- | **后台审查** | `/review <pr>` 在 `ctx.jobs` job 内抓取元数据、截断 diff、CI 检查与既有评论;完成输出带发现汇总、CI 状态与评论数。`reviewMode: "model"` 改为把 diff 交给一次性 subagent 评审 |
90
- | **读仓库** | `gh_repo` 读取仓库元数据:描述、默认分支、可见性、star、fork、开放 issue、语言、license、topics |
91
- | **读文件** | `gh_file` 按分支/tag/commit 读取单个文件,base64 解码、上限可配;目录返回结构化错误 |
92
- | **读取 issue** | `gh_issue` 支持 list / get / comments;列表中的 PR 以 `kind: "pr"` 标记 |
93
- | **管理 issue** | `issue_open` 创建、`issue_comment` 评论(对 PR 同样可用)、`issue_close` 关闭并可选记录关闭原因——全部审批门控 |
94
- | **搜索** | `gh_search` 以 GitHub 搜索语法查询 issue 与 PR,暴露独立的搜索配额 |
95
- | **审批** | `tools/pre-execute` 对每个写操作向 `ctx.approval` 发起 `ask`;`allowedActions` 白名单在询问前拒绝 |
96
- | **密钥安全** | token 逐操作读取、只写入 Authorization 头;专项测试断言它不出现在任何可见输出中 |
97
- | **韧性** | 按 `Retry-After`/`x-ratelimit-reset` 退避重试 429;读工具并发安全;所有调用尊重取消信号 |
98
- | **可观测** | 模型可见 ⟺ 已记录:模型看到的一切都经宿主自有会话事件(`tool/result`、`user/message`、`command/run`、`approval/asked`…) |
99
-
100
- ## 📦 安装
101
-
102
- 四条通道,全部有文档——任选其一。
103
-
104
- | 通道 | 命令 | 说明 |
105
- |---|---|---|
106
- | **npm registry** | `dsh plugin --profile <name> add @perrylink/dsh-github` | 已发布到 npm —— 最简单的通道 |
107
- | **npm tarball** | `dsh plugin --profile <name> add ./dsh-github-<version>.tgz` | 自带构建好的 `lib/`——无需构建许可 |
108
- | **git 源** | `dsh plugin --profile <name> add "github:PerryLink/dsh-github#<sha>"` | 需 `prepare` + `allowBuilds`(见下);请钉住 commit |
109
- | **本地 link** | `pnpm link --dir .` 后 `dsh plugin add @perrylink/dsh-github` | 开发用 |
110
-
111
- > npm 包发布在 `@perrylink` 作用域下,因为裸名 `dsh-github` 已被 registry 上另一个无关项目占用。插件模块名仍是 `dsh-github`。
112
-
113
- git 安装:pnpm ≥10 默认拒绝运行 git 依赖的 `prepare`,直到放行——`dsh` 会打印确切包键,复制进 profile 的 `pnpm-workspace.yaml`:
114
-
115
- ```yaml
116
- allowBuilds:
117
- '@perrylink/dsh-github': true
118
- ```
119
-
120
- `prepare` 脚本(`scripts/prepare.mjs`)自包含:能找到 TypeScript 编译器就构建,否则回退到**仓库内已提交的 `lib/` 产物**,两者都没有才响亮失败。
121
-
122
- **卸载:** `dsh plugin --profile <name> remove @perrylink/dsh-github`。
123
-
124
- ## ⚙️ 配置
125
-
126
- 加载期由 Schemastery 校验(非法即响亮失败)。可在 profile 的 `cordis.patch.yml` 覆盖任意键(整行 config 被替换,不深合并)。
127
-
128
- | 键 | 默认值 | 含义 |
129
- |---|---|---|
130
- | `tokenSource` | `auto` | `auto`(credentials → env → gh)或指定 `credentials` / `env` / `gh` |
131
- | `tokenRef` | `GITHUB_TOKEN` | credentials seam 引用名 / 环境变量名 |
132
- | `defaultOwnerRepo` | — | 调用未指定且 git 无 origin 时的兜底 `owner/repo` |
133
- | `autoCommit` | `false` | `/pr create` 是否允许指示模型先 commit+push |
134
- | `maxDiffChars` | `8000` | 审查读取 PR diff 的字符数上限 |
135
- | `renderExcerptChars` | `2000` | 渲染进工具输出的 diff 摘要字符数上限 |
136
- | `maxComments` | `20` | `gh_review` 列出 PR 评论的上限 |
137
- | `reviewJobTimeoutMs` | `600000` | 单个后台审查 job 的截止时间(超时以 `timeout` 失败) |
138
- | `maxReviewRecords` | `50` | 内存审查 job 记录上限;最旧的已终态记录先淘汰 |
139
- | `maxFileChars` | `12000` | `gh_file` 读取文件内容的字符数上限 |
140
- | `maxFindings` | `50` | 每次审查分析器发现数上限 |
141
- | `maxLineLength` | `300` | 行长度超过该值时分析器报超长行发现 |
142
- | `reviewMode` | `static` | 评审引擎:`static`(确定性分析器)或 `model`(经宿主 `subagents` 接缝的一次性 subagent;接缝缺失时响亮失败) |
143
- | `modelReviewProvider` | — | `reviewMode: "model"` 使用的 subagent provider 名;缺省用第一个注册的 provider |
144
- | `maxRetries` | `3` | 单请求的 429 重试次数 |
145
- | `retryBaseMs` | `500` | 重试退避基数(逐次翻倍) |
146
- | `retryMaxWaitMs` | `60000` | 重试退避上限 |
147
- | `requestTimeoutMs` | `30000` | 单次请求硬超时;超时即中止 fetch |
148
- | `apiBaseUrl` | `https://api.github.com` | GitHub REST 基地址(GitHub Enterprise) |
149
- | `allowedActions` | `['pr.create','pr.merge','pr.update','review.post','issue.create','issue.comment','issue.close','ci.run']` | 写动作白名单;名单外直接拒绝 |
150
- | `workspaceDir` | 进程 cwd | 只读 git 检查的工作目录 |
151
- | `ci` | `{ enabled: false, … }` | CI 集成段:轮询式审查机器人、状态检查门禁与一次性 `ci_run` 工具(其下为全部 `ci.*` 子键) |
152
-
153
- ## 🛠 工具
154
-
155
- | 工具 | 类型 | 参数 | 返回 |
156
- |---|---|---|---|
157
- | `pr_create` | 写 | `title*`、`body?`、`base?`、`head?`、`draft?`、`ownerRepo?` | `{status:'created', url, number, title, state, draft, base, head, rateLimit}` 或结构化错误 |
158
- | `pr_merge` | 写 | `pr*`(数字 / `#n` / `o/r#n` / URL)、`mergeMethod?`、`commitTitle?`、`commitMessage?`、`deleteBranch?` | `{status:'merged', merged, sha?, message, url, branchDeleted, branchDeleteNote?, rateLimit}` 或结构化错误 |
159
- | `pr_update` | 写 | `pr*`(数字 / `#n` / `o/r#n` / URL)、`title?`、`body?`、`state?`(`open`/`closed`)、`base?` | `{status:'updated', url, number, title, state, base, rateLimit}` 或结构化错误 |
160
- | `gh_review` | 读 | `pr*`(数字 / `#n` / `o/r#n` / URL)、`fields?`、`maxDiffChars?` | 元数据、截断 diff(完整 `diff.text` + 有界 `diff.excerpt` + 逐文件统计)、评论、CI、静态发现、各分节 `error` 字段、配额 |
161
- | `gh_repo` | 读 | `ownerRepo?` | `{repo, description, defaultBranch, visibility, stars, forks, openIssues, language, license, topics, url, updatedAt, rateLimit}` 或结构化错误 |
162
- | `gh_file` | 读 | `ownerRepo?`、`path*`、`ref?`、`maxChars?` | `{repo, path, ref, size, truncated, content, sha, url, rateLimit}` 或结构化错误 |
163
- | `gh_issue` | 读 | `action*`(`list`/`get`/`comments`)、`ownerRepo?`、`issueNumber?`、`state?`、`limit?` | 归一化条目(每条带 `kind: issue/pr/comment`)+ 配额 |
164
- | `review_post` | 写 | `jobId*`、`mode?`(`summary`/`inline`)、`body?` | `{status:'posted', mode, url, commentId?, reviewId?, findings, rateLimit}` 或结构化错误 |
165
- | `issue_open` | 写 | `title*`、`body?`、`labels?`、`ownerRepo?` | `{status:'created', url, number, title, rateLimit}` 或结构化错误 |
166
- | `issue_comment` | 写 | `issueNumber*`、`body*`、`ownerRepo?` | `{status:'commented', url, commentId, issueNumber, rateLimit}` 或结构化错误 |
167
- | `issue_close` | 写 | `issueNumber*`、`ownerRepo?`、`stateReason?`(`completed`/`not_planned`) | `{status:'closed', url, number, title, rateLimit}` 或结构化错误 |
168
- | `gh_search` | 读 | `q*`、`sort?`、`order?`、`perPage?` | `{query, total, items[{number,title,state,kind,author,url,repo,comments,createdAt}], rateLimit}` 或结构化错误 |
169
-
170
- `execute` 只返回 `output.schema` 声明的规范 JSON。缺 token 与 GitHub API 失败是携带配额事实的结构化错误分支,基础设施故障抛出(→ `isError`)。全程尊重 `exec.signal`。
171
-
172
- ## ⌨️ 命令
173
-
174
- | 命令 | 效果 |
175
- |---|---|
176
- | `/pr create [标题]` | 读取 git 状态并为模型排队 `pr_create` 指令(描述草稿、默认值;除非 `autoCommit`,否则不 commit/push)。创建 PR 需审批。 |
177
- | `/review <pr>` | 启动后台审查 job 并打印 job id;完成后宿主会通知,用 `job_output` 读取。 |
178
- | `/review <pr> --max-diff <n> --no-ci --no-comments` | 单 job 覆盖:diff 上限与是否抓取补充分节。 |
179
- | `/review stop <jobId>` | 取消 job(本地控制,非 GitHub 写操作)。 |
180
- | `/review post <jobId>` | 为模型排队 `review_post` 指令(汇总或行级);发布需审批。 |
181
- | `/issue open <标题>` | 为模型排队 `issue_open` 指令;创建需审批。 |
182
-
183
- ## 🏗 架构
184
-
185
- ```
186
- ┌───────────────────────────────────────────────┐
187
- │ dsh-github │
188
- │ │
189
- 人类 ─── /pr ──────┼──► git 读取(只读)──► agent.followup │
190
- /review ───┼──► ctx.jobs.start("github-review") ──► job │
191
- /issue ────┼──► agent.followup │
192
- │ │
193
- 模型 ─── pr_create / pr_merge / pr_update / gh_review / │
194
- review_post / gh_issue / issue_open / issue_comment / │
195
- issue_close / gh_search / gh_repo / gh_file │
196
- (defineTool,只返回规范 JSON) │
197
- │ │
198
- └───────┬───────────────┬───────────────┬───────┘
199
- │ │ │
200
- tools/pre-execute 凭证解析 GitHub REST
201
- 审批门(ask|deny) (seam→env→ 客户端(fetch、
202
- gh CLI,逐次解析) 429 重试、配额)
203
- ```
204
-
205
- - **凭证接缝。** `tokenSource: auto` 每次操作按「credentials seam 引用(`GITHUB_TOKEN`)→ 环境变量 → `gh` CLI 登录态」顺序解析。token 值只是交给 REST 客户端的局部变量,绝不进入规范值、渲染文本、UI 卡片、命令输出、注入通知、job 输出、审批理由或错误消息。
206
- - **审批。** 所有写操作都经模型工具。`tools/pre-execute` waterfall 监听器对七个写工具返回 `ask`,注册表即通过 `ctx.approval` 询问人类(宿主自动落 `approval/asked` + `approval/decided` 审计对),无应答者时 fail-closed。审批理由预览将要发布的内容(标题、正文长度、合并方式、覆盖评论的首行)。命令本身从不直接写:命令 handler 运行时没有开启的 turn,审批 seam 对命令在结构上不可用——写命令先收集只读上下文,再唤醒 agent(空闲 `followup`、忙碌 `inject`),让模型在 turn 内调用受审批门保护的工具。
207
- - **后台审查。** `/review <pr>` 在 `ctx.jobs` 上启动 `github-review` job(label、owner、超时、可取消)。job 逐操作解析 token,抓取 PR 元数据(记录 head-commit SHA,供行级发布使用)、截断后的 diff,以及(除非关闭)CI 检查与既有评论,然后运行确定性的多文件静态分析器(`src/review.ts`:硬编码密钥、Google API key、凭证赋值、调试语句、eval、TODO 标记、超长行、超大改动)——零 token、完全可测。`reviewMode: "model"` 时,job 改为把截断后的 diff 交给宿主 `subagents` 接缝的一次性 subagent(parent 为发起 job 的 agent),把子 agent 的 Markdown 输出存为可发布的报告;接缝或 provider 缺失时响亮失败。补充分节抓取失败只在输出中注明,不使 job 失败。完成通知由宿主的 `dsh-tool-jobs` 消费者送回发起会话;模型用自带 `job_output` 工具读取结论,用 `review_post` 发布——发布前必须审批。
208
- - **模型可见 ⟺ 已记录。** 本插件**不新增任何自定义会话事件类型**。仓库外插件的事件类型不在宿主 `KNOWN_SESSION_EVENT_TYPES` 中,未知的 required 事件会让宿主拒绝读取会话日志(宿主明确把外部插件事件注册面推迟到未来)。因此所有模型可见内容都走宿主已记录的表面:`tool/result` 规范值、经 `agent.inject`/`agent.followup` 的 `user/message` 通知、`command/run` + `command/done` 生命周期对、`approval/asked` + `approval/decided` 审计对。
209
- - **纯 presenter。** `presentCall`/`presentResult` 是 `args`(+ 持久化的 `result.meta`)的纯函数,实时流与日志回放行为一致。PR 创建结果以 generic 卡片展示 PR 链接。
210
-
211
- ## 🔒 安全边界
212
-
213
- - token 逐操作从配置的源(credentials seam、环境变量或 `gh` CLI)读取,只写入 REST 客户端的 Authorization 头;从不落日志、不渲染、不注入、不进会话日志、不进错误消息。
214
- - 每次 GitHub 写操作都需要 `ctx.approval` 的 `allowed-once`(默认策略 `ask`);`rejected`、`cancelled`、`unavailable` 一律 fail-closed。
215
- - `/pr create` 自己从不 commit/push;`autoCommit: true` 时模型通过 bash 工具(其自身审批门)执行这些写操作。dsh-github **不**管理 git 提交身份(dsh-git-identity 的职责)、**不**做 worktree(dsh-worktree 的职责)。
216
- - 审查 job 零写操作:只读 diff、把报告存进进程内存;只有 `review_post` 在审批后发布。
217
- - 发布的评论会插入 diff 中的文件名——这是不可信的仓库内容:`formatPostBody` 对文件名做反引号转义与 HTML 转义,恶意 PR 无法向审查评论注入 Markdown。
218
- - `gh_file` 读到的文件内容以及从 GitHub 读到的 issue/PR 正文、评论与搜索结果都是进入模型上下文的外部不可信内容——与网页抓取同属固有权衡;插件在渲染中把它们标注为外部内容。
219
- - 配额:429 带退避重试,剩余配额在包括失败在内的每个结果上对模型可见。
220
-
221
- ## ⚠️ 已知局限
222
-
223
- - **无自定义会话事件** —— 刻意为之(见架构);审计依赖宿主自有事件词汇。
224
- - **默认静态分析器** —— 确定性规则集(`src/review.ts`),零 token、可复现。`reviewMode: "model"` 会把截断后的 diff 交给宿主 `subagents` 接缝的一次性 subagent 做 LLM 评审(消耗 token;需要接缝与已注册的 provider)。
225
- - **job 与记录是进程内状态** —— 审查报告按 job id 存于插件内存,与宿主 job 注册表同为进程级生命周期;记录表受 `maxReviewRecords` 上限约束(最旧已终态记录先淘汰)。
226
- - **npm `latest` 标签过期** —— 本插件用 `^0.1.0-rc.5` peer 范围对齐 `dsh-base` 提供的 profile 闭包,开发时钉 `0.1.0-rc.6`。不要裸跑 `npm i @deepseek-ai/dsh-tools`。
227
- - **CI / GitHub Action** — 随本仓库发布(v0.6.0):复合动作(`action.yml`)负责审查 PR、修复 CI 并产出报告;轮询式审查机器人带幂等行内评论;外加状态检查门禁。所有写动作仍需审批。
228
-
229
- ## 🧪 开发
230
-
231
- ```sh
232
- pnpm install
233
- pnpm test # vitest:配置、凭证、429 重试、工具、命令、job、审批门、token 不泄露
234
- pnpm typecheck
235
- pnpm build # tsc → lib/(noEmitOnError)
236
- pnpm pack # 可安装 tarball
237
- pnpm run check:readmes # 交叉检查 5 个 README 的目录锚点、工具与配置键
238
- ```
239
-
240
- 测试通过注入的 runner mock 掉 GitHub API、`gh` CLI 与 git——不联网、不用真实凭证。`test/security.test.ts` 断言 token 字符串不出现在任何模型或人类可见输出中。`test/e2e.test.ts` 是可选真实 API 冒烟测试:未设置 `DSH_GITHUB_E2E_TOKEN` 时自动跳过(只打只读端点;独立变量保证单测套件与环境隔离)。
241
-
242
- ## 🗂 目录结构
243
-
244
- ```
245
- src/index.ts 插件入口(name/inject/apply,applyWithDeps 供测试注入)
246
- src/config.ts Schemastery 配置
247
- src/types.ts 宿主服务的最小结构视图 + Context 声明合并
248
- src/credential.ts token 解析(seam → env → gh),逐操作解析
249
- src/github.ts REST 客户端:429 重试、配额、diff 媒体类型
250
- src/git.ts 只读 git 检查 + 任意 API 主机的 origin 解析
251
- src/review.ts 确定性 diff 分析器 + 转义后的评论草稿
252
- src/jobs.ts github-review 后台 job 生产者(元数据 + diff + CI + 评论)
253
- src/approval-gate.ts tools/pre-execute ask/deny 门(带写操作预览)
254
- src/tools.ts 十二个模型可调工具
255
- src/commands.ts /pr、/review、/issue
256
- src/present.ts 纯 UI 卡片 presenter
257
- test/ vitest 套件 + mock 宿主脚手架 + 可选 e2e 冒烟
258
- cordis.patch.yml bundle patch(单行 insert)
259
- scripts/prepare.mjs git 安装用的自包含构建
260
- ```
261
-
262
- ## 🏷 Topics
263
-
264
- 推荐的 GitHub 仓库 Topics(在仓库设置里添加——它们驱动 [`dsh-plugin` 话题页](https://github.com/topics/dsh-plugin) 与各 DSH 插件市场):
265
-
266
- `dsh` · `dsh-plugin` · `deepseek-harness` · `github` · `pull-request` · `code-review` · `issue-tracker`
267
-
268
- ## 许可证
269
-
270
- [Apache License 2.0](LICENSE)
271
-
272
- ## PerryLink DSH 插件家族
273
-
274
- 本项目是 [PerryLink](https://github.com/PerryLink) 维护的 [15 个 DeepSeek Harness 插件](https://github.com/PerryLink)之一。如果你觉得这个插件有用,其余的很可能同样有用:
275
-
276
- | 插件 | 一句话说明 |
277
- |---|---|
278
- | [dsh-mcp-panel](https://github.com/PerryLink/dsh-mcp-panel) | 只读 MCP 运行时面板:/mcp 命令 + 设置页,状态/工具/错误一览 |
279
- | [dsh-doublecheck](https://github.com/PerryLink/dsh-doublecheck) | 工程纪律守门:需求审讯、测试证据门、对抗评审 |
280
- | [dsh-background-agents](https://github.com/PerryLink/dsh-background-agents) | 持久化后台子代理:Web 侧边栏进度、随时留言与打断 |
281
- | [dsh-lsp-actions](https://github.com/PerryLink/dsh-lsp-actions) | 基于语言服务器的诊断/格式化/补全/代码动作/重命名 |
282
- | [dsh-output-styles](https://github.com/PerryLink/dsh-output-styles) | 对标 Claude Code outputStyles 的运行时风格切换 |
283
- | [dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind) | 对标 Claude Code /rewind:快照、会话 fork、一键回退 |
284
- | [dsh-permission-rules](https://github.com/PerryLink/dsh-permission-rules) | Claude Code 风格声明式 allow/deny/ask 权限规则,带审计 |
285
- | [dsh-auto-review](https://github.com/PerryLink/dsh-auto-review) | 审批链上的第二模型自动审查,默认 fail-closed |
286
- | [dsh-memento](https://github.com/PerryLink/dsh-memento) | 带审批门的跨会话记忆:ctx.memory + SQLite + memory 工具 |
287
- | [dsh-skill-pack-security](https://github.com/PerryLink/dsh-skill-pack-security) | 安全审计技能包:密钥扫描、依赖与供应链审查 |
288
- | [dsh-session-pin](https://github.com/PerryLink/dsh-session-pin) | 在 Web 侧边栏置顶会话,持久排序 |
289
- | [dsh-composer-history](https://github.com/PerryLink/dsh-composer-history) | Web 作曲器终端式输入历史:方向键、Ctrl+R 搜索 |
290
- | **[dsh-github](https://github.com/PerryLink/dsh-github)** | DSH 的 GitHub PR/issue 集成,所有写操作经审批门 |
291
- | [dsh-plugin-guide](https://github.com/PerryLink/dsh-plugin-guide) | 插件开发知识库,随 bundle 安装的按需 agent 技能 |
292
- | [dsh-claude-move](https://github.com/PerryLink/dsh-claude-move) | 把 Claude Code 会话、记忆、技能和 CLAUDE.md 迁入 DSH |