dsh-github-flow 0.1.8 → 0.2.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.
Files changed (69) hide show
  1. package/README.en.md +162 -58
  2. package/README.md +167 -60
  3. package/docs/UPGRADE-NOTES.md +93 -0
  4. package/docs/architecture.md +47 -17
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2.png +0 -0
  7. package/docs/images/3.png +0 -0
  8. package/docs/images/4.png +0 -0
  9. package/docs/npm-publish-guide.md +26 -3
  10. package/lib/api/route-helpers.d.ts +47 -0
  11. package/lib/api/route-helpers.js +134 -0
  12. package/lib/api/routes-actions.d.ts +23 -0
  13. package/lib/api/routes-actions.js +239 -0
  14. package/lib/api/routes-lifecycle.d.ts +9 -0
  15. package/lib/api/routes-lifecycle.js +126 -0
  16. package/lib/api/routes-overview.d.ts +11 -0
  17. package/lib/api/routes-overview.js +227 -0
  18. package/lib/api/routes-proxy.d.ts +18 -0
  19. package/lib/api/routes-proxy.js +63 -0
  20. package/lib/api/routes.d.ts +16 -6
  21. package/lib/api/routes.js +14 -504
  22. package/lib/client.js +785 -90
  23. package/lib/commands/gh.js +11 -2
  24. package/lib/executor.d.ts +17 -0
  25. package/lib/executor.js +146 -8
  26. package/lib/main.d.ts +1 -0
  27. package/lib/main.js +49 -9
  28. package/lib/proxy.d.ts +87 -0
  29. package/lib/proxy.js +275 -0
  30. package/lib/renderers/markdown.d.ts +29 -0
  31. package/lib/renderers/markdown.js +520 -0
  32. package/lib/tools/api.d.ts +4 -0
  33. package/lib/tools/api.js +14 -4
  34. package/lib/tools/help.d.ts +31 -0
  35. package/lib/tools/help.js +224 -0
  36. package/lib/tools/image-utils.d.ts +79 -0
  37. package/lib/tools/image-utils.js +247 -0
  38. package/lib/tools/issue.d.ts +40 -4
  39. package/lib/tools/issue.js +167 -19
  40. package/lib/tools/pr.d.ts +29 -4
  41. package/lib/tools/pr.js +144 -18
  42. package/lib/tools/release.d.ts +89 -0
  43. package/lib/tools/release.js +219 -0
  44. package/lib/tools/repo.d.ts +24 -4
  45. package/lib/tools/repo.js +114 -14
  46. package/lib/tools/run.d.ts +22 -1
  47. package/lib/tools/run.js +74 -14
  48. package/lib/tools/secret.d.ts +60 -0
  49. package/lib/tools/secret.js +175 -0
  50. package/lib/tools/skill.d.ts +63 -0
  51. package/lib/tools/skill.js +146 -0
  52. package/lib/types.d.ts +24 -1
  53. package/lib/update-settings.d.ts +18 -0
  54. package/lib/update-settings.js +60 -0
  55. package/lib/updater.d.ts +40 -1
  56. package/lib/updater.js +126 -9
  57. package/lib/upgrade-manager.d.ts +74 -1
  58. package/lib/upgrade-manager.js +356 -46
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/package.json +3 -2
  62. package/lib/client/icons.d.ts +0 -13
  63. package/lib/client/icons.js +0 -41
  64. package/lib/client/index.d.ts +0 -3
  65. package/lib/client/index.js +0 -36
  66. package/lib/client/panel.d.ts +0 -1
  67. package/lib/client/panel.js +0 -102
  68. package/lib/client/style.d.ts +0 -1
  69. package/lib/client/style.js +0 -555
@@ -1,3 +1,5 @@
1
+ import { renderIssueMarkdown } from '../renderers/markdown.js';
2
+ import { extractImageUrls, downloadImages, buildImageBlocks, filterRequestedUrls } from './image-utils.js';
1
3
  function normalizeOutput(data) {
2
4
  if (data && typeof data === 'object' && !Array.isArray(data)) {
3
5
  return data;
@@ -7,21 +9,23 @@ function normalizeOutput(data) {
7
9
  }
8
10
  return { result: data !== undefined && data !== null ? data : '' };
9
11
  }
12
+ // 共享图片工具链的同名再导出:phase8 测试直接 import 本模块,保持路径稳定
13
+ export { extractImageUrls as extractIssueImageUrls } from './image-utils.js';
10
14
  export function createIssueTool(executor) {
11
15
  return {
12
16
  name: 'github_issue',
13
- description: '管理 GitHub Issues。支持查看 Issue 列表、详情、创建新 Issue、追加评论以及关闭 Issue。',
17
+ description: '管理 GitHub Issues。支持查看 Issue 列表、详情、创建新 Issue、追加评论、关闭 Issue、编辑 Issue 以及关联创建本地开发分支 (develop)。',
14
18
  parameters: {
15
19
  type: 'object',
16
20
  properties: {
17
21
  action: {
18
22
  type: 'string',
19
- enum: ['list', 'view', 'create', 'comment', 'close', 'reopen'],
20
- description: 'Issue 操作类型。',
23
+ enum: ['list', 'view', 'create', 'comment', 'close', 'reopen', 'edit', 'develop', 'view_images'],
24
+ description: 'Issue 操作类型。view_images 下载正文内的图片到本地并返回可渲染的图片内容块(Agent 可直接"看"用户报障截图)。',
21
25
  },
22
26
  issue_number: {
23
27
  type: 'number',
24
- description: 'Issue 编号(view, comment, close, reopen 时使用)。',
28
+ description: 'Issue 编号(view, comment, close, reopen, edit, develop 时使用)。',
25
29
  },
26
30
  repo: {
27
31
  type: 'string',
@@ -29,21 +33,53 @@ export function createIssueTool(executor) {
29
33
  },
30
34
  title: {
31
35
  type: 'string',
32
- description: 'Issue 标题(create 时使用)。',
36
+ description: 'Issue 标题(create 或 edit 时使用)。',
33
37
  },
34
38
  body: {
35
39
  type: 'string',
36
- description: 'Issue 描述正文或追加评论内容。',
40
+ description: 'Issue 描述正文或追加评论内容(create, comment, edit 时使用)。',
37
41
  },
38
42
  labels: {
39
43
  type: 'array',
40
44
  items: { type: 'string' },
41
- description: '标签名称列表。',
45
+ description: '标签名称列表(create 或 list 动作筛选使用)。',
42
46
  },
43
47
  assignees: {
44
48
  type: 'array',
45
49
  items: { type: 'string' },
46
- description: '被指派人的 GitHub 登录名列表。',
50
+ description: '被指派人的 GitHub 登录名列表(create 或 list 动作筛选使用)。',
51
+ },
52
+ state: {
53
+ type: 'string',
54
+ enum: ['open', 'closed', 'all'],
55
+ description: 'Issue 状态过滤(list 动作使用,可选 open, closed, all,默认 open)。',
56
+ },
57
+ limit: {
58
+ type: 'number',
59
+ description: '返回 Issue 数量上限(list 动作使用,默认 20,建议最大不超过 100)。',
60
+ },
61
+ branch_name: {
62
+ type: 'string',
63
+ description: '开发分支名称(develop 动作使用,例如 "fix-auth-bug")。',
64
+ },
65
+ add_labels: {
66
+ type: 'array',
67
+ items: { type: 'string' },
68
+ description: '要追加的标签列表(edit 动作使用)。',
69
+ },
70
+ remove_labels: {
71
+ type: 'array',
72
+ items: { type: 'string' },
73
+ description: '要移除的标签列表(edit 动作使用)。',
74
+ },
75
+ confirm: {
76
+ type: 'boolean',
77
+ description: '高危破坏性操作确认标志(close 动作必需)。若为 false 则仅输出待关闭警告。',
78
+ },
79
+ image_urls: {
80
+ type: 'array',
81
+ items: { type: 'string' },
82
+ description: 'view_images 可选:仅下载指定 URL 的图片(从 view 返回的 images 清单中选取,不在清单内的会被拒绝)。留空下载全部(正文+评论)。',
47
83
  },
48
84
  },
49
85
  required: ['action'],
@@ -51,15 +87,9 @@ export function createIssueTool(executor) {
51
87
  output: {
52
88
  schema: { type: 'object' },
53
89
  render(args, value) {
54
- if (!value)
55
- return [{ type: 'text', text: '无返回内容' }];
56
- if (typeof value.result === 'string') {
57
- return [{ type: 'text', text: value.result }];
58
- }
59
- if (Array.isArray(value.items)) {
60
- return [{ type: 'text', text: JSON.stringify(value.items, null, 2) }];
61
- }
62
- return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
90
+ // view_images 的图片以宿主 image 内容块输出:模型与人类都能直接看到图,
91
+ // base64 会污染上下文(一张截图 ~88KB),引用落盘文件由宿主按需加载
92
+ return [{ type: 'text', text: renderIssueMarkdown(args, value) }, ...buildImageBlocks(value)];
63
93
  },
64
94
  },
65
95
  async execute(args, execContext) {
@@ -79,7 +109,23 @@ export function createIssueTool(executor) {
79
109
  const runOpts = { cwd, sessionId };
80
110
  switch (args.action) {
81
111
  case 'list': {
82
- const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'], runOpts);
112
+ const listCmd = ['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt,url'];
113
+ if (args.state) {
114
+ listCmd.push('--state', args.state);
115
+ }
116
+ if (args.labels && Array.isArray(args.labels) && args.labels.length > 0) {
117
+ for (const l of args.labels) {
118
+ listCmd.push('--label', l);
119
+ }
120
+ }
121
+ if (args.assignees && Array.isArray(args.assignees) && args.assignees.length > 0) {
122
+ // gh issue list 的 --assignee 仅支持单账号或 @me,传逗号连接会导致 GraphQL 报用户未找到
123
+ listCmd.push('--assignee', args.assignees[0]);
124
+ }
125
+ // 显式 limit 优先;缺省回退 executor.defaultListLimit(用户可在插件配置中调整)
126
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(args.limit, 100) : Math.min(executor.defaultListLimit ?? 20, 100);
127
+ listCmd.push('-L', String(limit));
128
+ const res = await executor.run(listCmd, runOpts);
83
129
  if (!res.ok)
84
130
  throw new Error(res.error);
85
131
  return normalizeOutput(res.data || res.rawOutput);
@@ -90,7 +136,68 @@ export function createIssueTool(executor) {
90
136
  const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'], runOpts);
91
137
  if (!res.ok)
92
138
  throw new Error(res.error);
93
- return normalizeOutput(res.data || res.rawOutput);
139
+ const out = normalizeOutput(res.data || res.rawOutput);
140
+ // 附带图片 URL 清单(正文+评论,与 view_images 提取范围同源):
141
+ // Agent 按正文里的原始 URL 决策,需要看图时用 image_urls 精准选取
142
+ const bodies = [typeof out.body === 'string' ? out.body : ''];
143
+ if (Array.isArray(out.comments)) {
144
+ for (const c of out.comments) {
145
+ if (c && typeof c.body === 'string')
146
+ bodies.push(c.body);
147
+ }
148
+ }
149
+ out.images = Array.from(new Set(bodies.flatMap((b) => extractImageUrls(b))));
150
+ return out;
151
+ }
152
+ case 'view_images': {
153
+ if (!args.issue_number)
154
+ throw new Error('view_images 操作必须提供 issue_number');
155
+ // 第一步:取正文+评论提取图片 URL(与 view 清单同源)
156
+ const detail = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'body,comments'], runOpts);
157
+ if (!detail.ok)
158
+ throw new Error(detail.error);
159
+ const d = detail.data && typeof detail.data === 'object' ? detail.data : {};
160
+ const bodies = [typeof d.body === 'string' ? d.body : ''];
161
+ if (Array.isArray(d.comments)) {
162
+ for (const c of d.comments) {
163
+ if (c && typeof c.body === 'string')
164
+ bodies.push(c.body);
165
+ }
166
+ }
167
+ const extracted = Array.from(new Set(bodies.flatMap((b) => extractImageUrls(b))));
168
+ if (extracted.length === 0) {
169
+ return { message: `Issue #${args.issue_number} 正文与评论中未发现图片附件。` };
170
+ }
171
+ // 第二步:目标选择——不传 image_urls 下载全部;传入则与正文清单
172
+ // 严格交集(防幻觉/诱导的任意 URL 下载),空数组视为误传明确报错
173
+ let targets = extracted;
174
+ const rejections = [];
175
+ if (args.image_urls !== undefined) {
176
+ if (!Array.isArray(args.image_urls) || args.image_urls.length === 0) {
177
+ throw new Error('image_urls 为空数组:如需下载全部请省略该参数,如需指定请传入 view 返回清单中的 URL。');
178
+ }
179
+ const filtered = filterRequestedUrls(args.image_urls, extracted);
180
+ targets = filtered.matched;
181
+ for (const bad of filtered.rejected) {
182
+ rejections.push(`${String(bad).slice(-24)}: 不在本 Issue 图片清单中,已拒绝(防任意下载)`);
183
+ }
184
+ }
185
+ // 第三步:共享下载执行体(gh api 二进制通道 + 魔数校验 + 字节护栏 + 内容寻址落盘)
186
+ const { images, failures: dlFailures, remaining } = await downloadImages(targets, (url) => executor.run(['api', url], { ...runOpts, binary: true, timeoutMs: 30_000 }));
187
+ const failures = [...rejections, ...dlFailures];
188
+ if (images.length === 0 && rejections.length === 0 && !remaining) {
189
+ return { message: `Issue #${args.issue_number} 的图片全部下载失败:`, failures };
190
+ }
191
+ if (images.length === 0) {
192
+ return { message: `Issue #${args.issue_number} 未能获取任何图片(请求 ${targets.length} 张,被拒 ${rejections.length} 张)`, failures, remaining };
193
+ }
194
+ return {
195
+ message: `Issue #${args.issue_number} 成功获取 ${images.length} 张图片${failures.length ? `(${failures.length} 条失败/提示)` : ''}:`,
196
+ images,
197
+ failures: failures.length ? failures : undefined,
198
+ // 续传清单:触顶时 Agent 直接 image_urls: remaining 发起下一批
199
+ remaining,
200
+ };
94
201
  }
95
202
  case 'create': {
96
203
  const cmd = ['issue', 'create', ...repoArgs, '--title', args.title || '', '--body', args.body || ''];
@@ -105,6 +212,39 @@ export function createIssueTool(executor) {
105
212
  throw new Error(res.error);
106
213
  return { message: 'Issue 创建成功', url: res.rawOutput };
107
214
  }
215
+ case 'edit': {
216
+ if (!args.issue_number)
217
+ throw new Error('edit 操作必须提供 issue_number');
218
+ const editCmd = ['issue', 'edit', String(args.issue_number), ...repoArgs];
219
+ if (args.title)
220
+ editCmd.push('--title', args.title);
221
+ if (args.body)
222
+ editCmd.push('--body', args.body);
223
+ if (args.add_labels && Array.isArray(args.add_labels) && args.add_labels.length > 0) {
224
+ for (const l of args.add_labels)
225
+ editCmd.push('--add-label', l);
226
+ }
227
+ if (args.remove_labels && Array.isArray(args.remove_labels) && args.remove_labels.length > 0) {
228
+ for (const l of args.remove_labels)
229
+ editCmd.push('--remove-label', l);
230
+ }
231
+ const res = await executor.run(editCmd, runOpts);
232
+ if (!res.ok)
233
+ throw new Error(res.error);
234
+ return { message: `Issue #${args.issue_number} 更新成功`, output: res.rawOutput };
235
+ }
236
+ case 'develop': {
237
+ if (!args.issue_number)
238
+ throw new Error('develop 操作必须提供 issue_number');
239
+ const devCmd = ['issue', 'develop', String(args.issue_number), ...repoArgs, '--checkout'];
240
+ if (args.branch_name) {
241
+ devCmd.push('-n', args.branch_name);
242
+ }
243
+ const res = await executor.run(devCmd, runOpts);
244
+ if (!res.ok)
245
+ throw new Error(res.error);
246
+ return { message: `已成功为 Issue #${args.issue_number} 创建并检出开发分支`, output: res.rawOutput };
247
+ }
108
248
  case 'comment': {
109
249
  if (!args.issue_number || !args.body)
110
250
  throw new Error('comment 必须提供 issue_number 和 body');
@@ -116,6 +256,14 @@ export function createIssueTool(executor) {
116
256
  case 'close': {
117
257
  if (!args.issue_number)
118
258
  throw new Error('close 操作必须提供 issue_number');
259
+ if (args.confirm !== true) {
260
+ return {
261
+ status: 'AWAITING_CONFIRMATION',
262
+ warning: `⚠️ 高危操作拦截:你即将关闭 Issue #${args.issue_number}。`,
263
+ impact: '关闭后该议题状态将变更为 CLOSED,请与用户确认是否确实已解决或需要关闭。',
264
+ requiredAction: '如确认关闭,请在参数中附加 `confirm: true` 重新调用。',
265
+ };
266
+ }
119
267
  const res = await executor.run(['issue', 'close', String(args.issue_number), ...repoArgs], runOpts);
120
268
  if (!res.ok)
121
269
  throw new Error(res.error);
package/lib/tools/pr.d.ts CHANGED
@@ -52,10 +52,38 @@ export declare function createPrTool(executor: GhExecutor): {
52
52
  type: string;
53
53
  description: string;
54
54
  };
55
+ state: {
56
+ type: string;
57
+ enum: string[];
58
+ description: string;
59
+ };
60
+ limit: {
61
+ type: string;
62
+ description: string;
63
+ };
64
+ author: {
65
+ type: string;
66
+ description: string;
67
+ };
68
+ stat_only: {
69
+ type: string;
70
+ description: string;
71
+ };
72
+ confirm: {
73
+ type: string;
74
+ description: string;
75
+ };
55
76
  dry_run: {
56
77
  type: string;
57
78
  description: string;
58
79
  };
80
+ image_urls: {
81
+ type: string;
82
+ items: {
83
+ type: string;
84
+ };
85
+ description: string;
86
+ };
59
87
  };
60
88
  required: string[];
61
89
  };
@@ -63,10 +91,7 @@ export declare function createPrTool(executor: GhExecutor): {
63
91
  schema: {
64
92
  type: string;
65
93
  };
66
- render(args: any, value: any): {
67
- type: string;
68
- text: any;
69
- }[];
94
+ render(args: any, value: any): any[];
70
95
  };
71
96
  execute(args: any, execContext: any): Promise<Record<string, any>>;
72
97
  };
package/lib/tools/pr.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { renderPrMarkdown } from '../renderers/markdown.js';
2
+ import { extractImageUrls, downloadImages, buildImageBlocks, filterRequestedUrls } from './image-utils.js';
1
3
  function normalizeOutput(data) {
2
4
  if (data && typeof data === 'object' && !Array.isArray(data)) {
3
5
  return data;
@@ -10,18 +12,18 @@ function normalizeOutput(data) {
10
12
  export function createPrTool(executor) {
11
13
  return {
12
14
  name: 'github_pr',
13
- description: '管理与审查 GitHub Pull Request (PR)。支持查看列表、详情、差异比对、CI 检查状态、代码审查与合并操作。默认针对当前工作区关联的仓库。',
15
+ description: '管理与审查 GitHub Pull Request (PR)。支持查看列表、详情、差异比对、CI 检查状态、代码审查、合并操作、本地代码检出 (checkout) 与冲突状态检测 (status)。默认针对当前工作区关联的仓库。',
14
16
  parameters: {
15
17
  type: 'object',
16
18
  properties: {
17
19
  action: {
18
20
  type: 'string',
19
- enum: ['list', 'view', 'create', 'diff', 'checks', 'review', 'merge'],
20
- description: 'PR 操作类型。',
21
+ enum: ['list', 'view', 'create', 'diff', 'checks', 'review', 'merge', 'checkout', 'status', 'view_images'],
22
+ description: 'PR 操作类型。view_images 下载正文与评论中的图片(UI 变更对比图、复现截图等)到本地并以图片内容块返回,供 Agent 审查时直接查看。',
21
23
  },
22
24
  pr_number: {
23
25
  type: 'number',
24
- description: 'PR 编号(view, diff, checks, review, merge 时使用;若在 PR 对应分支可不填,自动识别当前分支 PR)。',
26
+ description: 'PR 编号(view, diff, checks, review, merge, checkout 时使用;若在 PR 对应分支可不填,自动识别当前分支 PR)。',
25
27
  },
26
28
  repo: {
27
29
  type: 'string',
@@ -61,25 +63,43 @@ export function createPrTool(executor) {
61
63
  type: 'boolean',
62
64
  description: '合并后是否删除源分支(merge 操作时使用,默认 false)。删除分支不可撤销,只在确认该分支已无用后再开启。',
63
65
  },
66
+ state: {
67
+ type: 'string',
68
+ enum: ['open', 'closed', 'merged', 'all'],
69
+ description: 'PR 状态过滤(list 操作时使用,可选 open, closed, merged, all,默认 open)。',
70
+ },
71
+ limit: {
72
+ type: 'number',
73
+ description: '返回 PR 数量上限(list 操作时使用,默认 20,建议最大不超过 100)。',
74
+ },
75
+ author: {
76
+ type: 'string',
77
+ description: '按作者筛选(list 动作使用,支持 "@me" 或 GitHub 用户名)。',
78
+ },
79
+ stat_only: {
80
+ type: 'boolean',
81
+ description: '是否仅输出文件变动概览(diff 动作使用,映射为 gh pr diff --stat,防止大 PR 爆 Token)。',
82
+ },
83
+ confirm: {
84
+ type: 'boolean',
85
+ description: '高危破坏性操作确认标志(merge 动作或 delete_branch 时必需)。若为 false 则仅输出合并预演警告。',
86
+ },
64
87
  dry_run: {
65
88
  type: 'boolean',
66
89
  description: '是否只预演不真正创建(create 操作时使用,默认 false)。透传 gh --dry-run:打印将要创建的 PR 详情而不落库,用于提交前确认 head/base/title 是否正确。',
67
90
  },
91
+ image_urls: {
92
+ type: 'array',
93
+ items: { type: 'string' },
94
+ description: 'view_images 可选:仅下载指定 URL 的图片(从 view 返回的 images 清单中选取,不在清单内的会被拒绝)。留空下载全部(正文+评论)。',
95
+ },
68
96
  },
69
97
  required: ['action'],
70
98
  },
71
99
  output: {
72
100
  schema: { type: 'object' },
73
101
  render(args, value) {
74
- if (!value)
75
- return [{ type: 'text', text: '无返回内容' }];
76
- if (typeof value.result === 'string') {
77
- return [{ type: 'text', text: value.result }];
78
- }
79
- if (Array.isArray(value.items)) {
80
- return [{ type: 'text', text: JSON.stringify(value.items, null, 2) }];
81
- }
82
- return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
102
+ return [{ type: 'text', text: renderPrMarkdown(args, value) }, ...buildImageBlocks(value)];
83
103
  },
84
104
  },
85
105
  async execute(args, execContext) {
@@ -99,32 +119,130 @@ export function createPrTool(executor) {
99
119
  const runOpts = { cwd, sessionId };
100
120
  switch (args.action) {
101
121
  case 'list': {
102
- const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'], runOpts);
122
+ const listCmd = ['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt,mergedAt,url'];
123
+ if (args.state) {
124
+ listCmd.push('--state', args.state);
125
+ }
126
+ if (args.author) {
127
+ listCmd.push('--author', args.author);
128
+ }
129
+ // 显式 limit 优先;缺省回退 executor.defaultListLimit(用户可在插件配置中调整)
130
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(args.limit, 100) : Math.min(executor.defaultListLimit ?? 20, 100);
131
+ listCmd.push('-L', String(limit));
132
+ const res = await executor.run(listCmd, runOpts);
103
133
  if (!res.ok)
104
134
  throw new Error(res.error);
105
135
  return normalizeOutput(res.data || res.rawOutput);
106
136
  }
107
137
  case 'view': {
108
138
  const target = args.pr_number ? [String(args.pr_number)] : [];
109
- const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'], runOpts);
139
+ const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url,comments'], runOpts);
110
140
  if (!res.ok)
111
141
  throw new Error(res.error);
112
- return normalizeOutput(res.data || res.rawOutput);
142
+ const out = normalizeOutput(res.data || res.rawOutput);
143
+ // 附带图片 URL 清单(正文+评论,与 view_images 提取范围同源):
144
+ // Agent 按正文里的原始 URL 决策,需要看图时用 image_urls 精准选取
145
+ const bodies = [typeof out.body === 'string' ? out.body : ''];
146
+ if (Array.isArray(out.comments)) {
147
+ for (const c of out.comments) {
148
+ if (c && typeof c.body === 'string')
149
+ bodies.push(c.body);
150
+ }
151
+ }
152
+ out.images = Array.from(new Set(bodies.flatMap((b) => extractImageUrls(b))));
153
+ return out;
154
+ }
155
+ case 'view_images': {
156
+ if (!args.pr_number)
157
+ throw new Error('view_images 操作必须提供 pr_number');
158
+ // PR 与 Issue 图片机制同构;评论走同一 issues-comments API(实测 PR 评论
159
+ // 的 <img> 形态与 issue 完全一致),正文 + 评论合并提取一次覆盖全部
160
+ const detail = await executor.run(['pr', 'view', String(args.pr_number), ...repoArgs, '--json', 'body,comments'], runOpts);
161
+ if (!detail.ok)
162
+ throw new Error(detail.error);
163
+ const d = detail.data && typeof detail.data === 'object' ? detail.data : {};
164
+ const bodies = [];
165
+ if (typeof d.body === 'string')
166
+ bodies.push(d.body);
167
+ if (Array.isArray(d.comments)) {
168
+ for (const c of d.comments) {
169
+ if (c && typeof c.body === 'string')
170
+ bodies.push(c.body);
171
+ }
172
+ }
173
+ // 多来源合并去重(Set 语义由 extractImageUrls 内部保证)
174
+ const extracted = Array.from(new Set(bodies.flatMap((b) => extractImageUrls(b))));
175
+ if (extracted.length === 0) {
176
+ return { message: `PR #${args.pr_number} 正文与评论中未发现图片附件。` };
177
+ }
178
+ // 目标选择:不传 image_urls 下载全部;传入则与清单严格交集
179
+ //(防幻觉/诱导的任意 URL 下载),空数组视为误传明确报错
180
+ let targets = extracted;
181
+ const rejections = [];
182
+ if (args.image_urls !== undefined) {
183
+ if (!Array.isArray(args.image_urls) || args.image_urls.length === 0) {
184
+ throw new Error('image_urls 为空数组:如需下载全部请省略该参数,如需指定请传入 view 返回清单中的 URL。');
185
+ }
186
+ const filtered = filterRequestedUrls(args.image_urls, extracted);
187
+ targets = filtered.matched;
188
+ for (const bad of filtered.rejected) {
189
+ rejections.push(`${String(bad).slice(-24)}: 不在本 PR 图片清单中,已拒绝(防任意下载)`);
190
+ }
191
+ }
192
+ const { images, failures: dlFailures, remaining } = await downloadImages(targets, (url) => executor.run(['api', url], { ...runOpts, binary: true, timeoutMs: 30_000 }));
193
+ const failures = [...rejections, ...dlFailures];
194
+ if (images.length === 0 && rejections.length === 0 && !remaining) {
195
+ return { message: `PR #${args.pr_number} 的图片全部下载失败:`, failures };
196
+ }
197
+ if (images.length === 0) {
198
+ return { message: `PR #${args.pr_number} 未能获取任何图片(请求 ${targets.length} 张,被拒 ${rejections.length} 张)`, failures, remaining };
199
+ }
200
+ return {
201
+ message: `PR #${args.pr_number} 成功获取 ${images.length} 张图片(正文+评论)${failures.length ? `(${failures.length} 条失败/提示)` : ''}:`,
202
+ images,
203
+ failures: failures.length ? failures : undefined,
204
+ // 续传清单:触顶时 Agent 直接 image_urls: remaining 发起下一批
205
+ remaining,
206
+ };
113
207
  }
114
208
  case 'checks': {
115
209
  const target = args.pr_number ? [String(args.pr_number)] : [];
116
- const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'], runOpts);
210
+ // 字段集为 gh pr checks 的合法九字段子集(v2.50.0 引入 --json 起从未变更)。
211
+ // 旧字段集的 conclusion/detailsUrl 是 REST API 字段名,pr checks 从未支持——
212
+ // 初版误搬导致该 action 在所有 gh 版本上必然报 Unknown JSON field(Issue #3)。
213
+ // bucket 是 state 的分类视图(pass/fail/pending/skipping/cancel),link 即详情页。
214
+ const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,bucket,link,workflow,startedAt,completedAt'], runOpts);
117
215
  if (!res.ok)
118
216
  throw new Error(res.error);
119
217
  return normalizeOutput(res.data || res.rawOutput);
120
218
  }
121
219
  case 'diff': {
122
220
  const target = args.pr_number ? [String(args.pr_number)] : [];
123
- const res = await executor.run(['pr', 'diff', ...target, ...repoArgs], runOpts);
221
+ const diffCmd = ['pr', 'diff', ...target, ...repoArgs];
222
+ if (args.stat_only === true) {
223
+ diffCmd.push('--stat');
224
+ }
225
+ const res = await executor.run(diffCmd, runOpts);
124
226
  if (!res.ok)
125
227
  throw new Error(res.error);
126
228
  return { result: res.rawOutput || '无差异内容' };
127
229
  }
230
+ case 'checkout': {
231
+ if (!args.pr_number)
232
+ throw new Error('checkout 操作必须提供 pr_number');
233
+ const checkoutCmd = ['pr', 'checkout', String(args.pr_number), ...repoArgs, '-f'];
234
+ const res = await executor.run(checkoutCmd, runOpts);
235
+ if (!res.ok)
236
+ throw new Error(res.error);
237
+ return { message: `已成功将 PR #${args.pr_number} 检出至本地分支`, output: res.rawOutput || 'Checked out' };
238
+ }
239
+ case 'status': {
240
+ const statusCmd = ['pr', 'status', ...repoArgs, '-c', '--json', 'number,title,state,mergeStateStatus,url'];
241
+ const res = await executor.run(statusCmd, runOpts);
242
+ if (!res.ok)
243
+ throw new Error(res.error);
244
+ return normalizeOutput(res.data || res.rawOutput);
245
+ }
128
246
  case 'create': {
129
247
  const createCmd = ['pr', 'create', ...repoArgs, '--title', args.title || '', '--body', args.body || ''];
130
248
  if (args.base)
@@ -166,6 +284,14 @@ export function createPrTool(executor) {
166
284
  case 'merge': {
167
285
  if (!args.pr_number)
168
286
  throw new Error('merge 操作必须提供 pr_number');
287
+ if (args.confirm !== true) {
288
+ return {
289
+ status: 'AWAITING_CONFIRMATION',
290
+ warning: `⚠️ 高危操作拦截:你即将合并 PR #${args.pr_number}(合并策略: ${args.merge_method || 'squash'}${args.delete_branch ? ',且删除源分支' : ''})。`,
291
+ impact: '合并操作将直接向基准分支写入代码并更新远端 PR 状态。',
292
+ requiredAction: '如确认执行合并,请向用户明确确认并附加 `confirm: true` 重新调用。',
293
+ };
294
+ }
169
295
  const method = args.merge_method === 'merge' ? '--merge' : (args.merge_method === 'rebase' ? '--rebase' : '--squash');
170
296
  const mergeCmd = ['pr', 'merge', String(args.pr_number), ...repoArgs, method];
171
297
  // 删分支默认关闭:合并他人仓库的 PR 时删掉源分支是不可撤销的破坏性动作,
@@ -0,0 +1,89 @@
1
+ import type { GhExecutor } from '../executor.js';
2
+ export declare function createReleaseTool(executor: GhExecutor): {
3
+ name: string;
4
+ description: string;
5
+ parameters: {
6
+ type: string;
7
+ properties: {
8
+ action: {
9
+ type: string;
10
+ enum: string[];
11
+ description: string;
12
+ };
13
+ repo: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ tag: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ title: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ notes: {
26
+ type: string;
27
+ description: string;
28
+ };
29
+ generate_notes: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ draft: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ prerelease: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ target: {
42
+ type: string;
43
+ description: string;
44
+ };
45
+ assets: {
46
+ type: string;
47
+ items: {
48
+ type: string;
49
+ };
50
+ description: string;
51
+ };
52
+ clobber: {
53
+ type: string;
54
+ description: string;
55
+ };
56
+ pattern: {
57
+ type: string;
58
+ description: string;
59
+ };
60
+ dir: {
61
+ type: string;
62
+ description: string;
63
+ };
64
+ limit: {
65
+ type: string;
66
+ description: string;
67
+ };
68
+ confirm: {
69
+ type: string;
70
+ description: string;
71
+ };
72
+ cleanup_tag: {
73
+ type: string;
74
+ description: string;
75
+ };
76
+ };
77
+ required: string[];
78
+ };
79
+ output: {
80
+ schema: {
81
+ type: string;
82
+ };
83
+ render(args: any, value: any): {
84
+ type: string;
85
+ text: string;
86
+ }[];
87
+ };
88
+ execute(args: any, execContext: any): Promise<Record<string, any>>;
89
+ };