dsh-github-flow 0.6.0 → 0.7.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/README.en.md +102 -15
- package/README.md +105 -15
- package/docs/UPGRADE-NOTES.md +12 -11
- package/docs/architecture.md +67 -14
- package/lib/api/route-helpers.d.ts +19 -0
- package/lib/api/route-helpers.js +54 -0
- package/lib/api/routes-actions.js +126 -2
- package/lib/api/routes-lifecycle.js +8 -1
- package/lib/api/routes-overview.d.ts +26 -0
- package/lib/api/routes-overview.js +276 -34
- package/lib/api/routes-proxy.js +8 -1
- package/lib/api/routes.d.ts +11 -1
- package/lib/api/routes.js +15 -2
- package/lib/cache.d.ts +0 -21
- package/lib/cache.js +0 -37
- package/lib/client.d.ts +2 -1
- package/lib/client.js +2108 -658
- package/lib/destructive.d.ts +59 -0
- package/lib/destructive.js +285 -0
- package/lib/executor/args-guard.d.ts +17 -0
- package/lib/executor/args-guard.js +23 -0
- package/lib/executor/failure-anchors.d.ts +52 -0
- package/lib/executor/failure-anchors.js +116 -0
- package/lib/executor/pr-review.d.ts +95 -0
- package/lib/executor/pr-review.js +175 -0
- package/lib/executor/run-log.d.ts +36 -0
- package/lib/executor/run-log.js +78 -0
- package/lib/executor.d.ts +204 -3
- package/lib/executor.js +670 -28
- package/lib/github-service.d.ts +1 -1
- package/lib/main.d.ts +92 -18
- package/lib/main.js +186 -95
- package/lib/renderers/common.d.ts +0 -3
- package/lib/renderers/common.js +8 -44
- package/lib/renderers/markdown.d.ts +1 -0
- package/lib/renderers/markdown.js +1 -0
- package/lib/renderers/pr.js +26 -1
- package/lib/renderers/task.d.ts +9 -0
- package/lib/renderers/task.js +120 -0
- package/lib/review-rules.d.ts +60 -0
- package/lib/review-rules.js +204 -0
- package/lib/shared/state-badges.d.ts +42 -0
- package/lib/shared/state-badges.js +102 -0
- package/lib/task-primitives.d.ts +109 -0
- package/lib/task-primitives.js +194 -0
- package/lib/tool-tiers.d.ts +57 -0
- package/lib/tool-tiers.js +168 -0
- package/lib/tools/agent-task.d.ts +4 -0
- package/lib/tools/agent-task.js +22 -9
- package/lib/tools/api.d.ts +4 -0
- package/lib/tools/api.js +48 -12
- package/lib/tools/body-input.d.ts +58 -0
- package/lib/tools/body-input.js +123 -0
- package/lib/tools/cache.js +42 -40
- package/lib/tools/codespace.js +4 -12
- package/lib/tools/discussion.d.ts +4 -0
- package/lib/tools/discussion.js +33 -17
- package/lib/tools/gist.d.ts +1 -39
- package/lib/tools/gist.js +4 -14
- package/lib/tools/help.js +65 -13
- package/lib/tools/issue.d.ts +4 -0
- package/lib/tools/issue.js +37 -38
- package/lib/tools/label.js +4 -15
- package/lib/tools/pr.d.ts +51 -0
- package/lib/tools/pr.js +204 -46
- package/lib/tools/project.js +4 -13
- package/lib/tools/release.d.ts +4 -0
- package/lib/tools/release.js +26 -26
- package/lib/tools/repo.js +4 -4
- package/lib/tools/ruleset.js +9 -6
- package/lib/tools/run.js +76 -24
- package/lib/tools/secret.js +11 -17
- package/lib/tools/skill.js +18 -22
- package/lib/tools/status.js +4 -4
- package/lib/tools/task.d.ts +223 -0
- package/lib/tools/task.js +464 -0
- package/lib/types.d.ts +183 -1
- package/lib/ui/commit-modal.js +108 -4
- package/lib/ui/diff-utils.d.ts +51 -0
- package/lib/ui/diff-utils.js +155 -0
- package/lib/ui/file-diff-modal.d.ts +33 -0
- package/lib/ui/file-diff-modal.js +250 -0
- package/lib/ui/help-modal.d.ts +1 -1
- package/lib/ui/help-modal.js +24 -12
- package/lib/ui/issue-modal.js +7 -3
- package/lib/ui/pr-modal.js +50 -11
- package/lib/ui/run-modal.d.ts +26 -0
- package/lib/ui/run-modal.js +486 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +376 -44
- package/lib/ui/sync-settings-modal.js +1 -1
- package/lib/ui/workspace-panel.js +78 -23
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +9 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub 正文上限(Issue/PR/评论/讨论统一口径 65,536 字符)。
|
|
3
|
+
* 抽成常量而不是散落字面量:这是上游约束,将来调整只改一处。
|
|
4
|
+
*/
|
|
5
|
+
export declare const GITHUB_BODY_MAX_CHARS = 65536;
|
|
6
|
+
/**
|
|
7
|
+
* 单个正文文件的字节硬上限(4MB)。
|
|
8
|
+
* 字符上限只管正文语义,管不住「模型指了一个 200MB 日志文件」——先按字节拦住,
|
|
9
|
+
* 避免 readFileSync 把整个文件拉进内存再判定。
|
|
10
|
+
*/
|
|
11
|
+
export declare const BODY_FILE_MAX_BYTES: number;
|
|
12
|
+
/** 入参归一化的产出:argv 片段 + 需要注入的 stdin + 权威文本 */
|
|
13
|
+
export interface BodySource {
|
|
14
|
+
/** 交给 gh 的 argv 片段:`[]` | `[flag, text]` | `[fileFlag, '-']` */
|
|
15
|
+
argv: string[];
|
|
16
|
+
/** `[fileFlag, '-']` 时需要经 stdin 注入的正文;其余情况为 undefined */
|
|
17
|
+
stdin?: string;
|
|
18
|
+
/** 权威正文(REST/GraphQL 通道直接用它;调用方做必填判定也看它) */
|
|
19
|
+
text: string;
|
|
20
|
+
/** `text` 的字符数(按 UTF-16 单元近似 GitHub 的字符口径) */
|
|
21
|
+
chars: number;
|
|
22
|
+
fromFile: boolean;
|
|
23
|
+
/** 文件来源时解析后的绝对路径(仅用于回执与错误诊断) */
|
|
24
|
+
file?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BodySourceOptions {
|
|
27
|
+
/** 相对路径的解析基准(工具层传 `executor.resolveExecScope` 得到的工作区) */
|
|
28
|
+
cwd: string;
|
|
29
|
+
/** 内联 flag,缺省 `--body`(discussion 用 `-b`) */
|
|
30
|
+
flag?: string;
|
|
31
|
+
/** 文件 flag,缺省 `--body-file`(release 用 `--notes-file`,api/agent-task 用各自 flag) */
|
|
32
|
+
fileFlag?: string;
|
|
33
|
+
/** 内联字段名,仅用于错误文案(缺省 `body`) */
|
|
34
|
+
inlineField?: string;
|
|
35
|
+
/** 文件字段名,仅用于错误文案(缺省 `body_file`) */
|
|
36
|
+
fileField?: string;
|
|
37
|
+
/** 无正文时仍下发 `[flag, '']`:create 需要它保持非交互(否则 gh 会打开编辑器) */
|
|
38
|
+
keepEmptyInline?: boolean;
|
|
39
|
+
/** 字符上限覆盖;传 0 关闭字符校验(`gh api --input` 的请求体不受正文口径约束) */
|
|
40
|
+
maxChars?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 解析正文来源。
|
|
44
|
+
*
|
|
45
|
+
* 语义表(与计划文档一致,调用方不再各自二次决策):
|
|
46
|
+
* | 输入 | argv | stdin |
|
|
47
|
+
* | --- | --- | --- |
|
|
48
|
+
* | `file` 有值且文件非空 | `[fileFlag, '-']` | 文件正文 |
|
|
49
|
+
* | `file` 有值但文件为空 | 同「都为空」 | — |
|
|
50
|
+
* | `inline` 非空 | `[flag, inline]` | — |
|
|
51
|
+
* | 都为空 + keepEmptyInline | `[flag, '']` | — |
|
|
52
|
+
* | 都为空 | `[]` | — |
|
|
53
|
+
* | 两者都非空 | 抛错(不做静默优先级) | — |
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveBodySource(input: {
|
|
56
|
+
inline?: unknown;
|
|
57
|
+
file?: unknown;
|
|
58
|
+
}, options?: BodySourceOptions): BodySource;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「正文/长文本来自本地文件」的统一入参解析(纯函数,零 gh 依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:Agent 的典型工作流是**先把报告写成 md 文件**,再提交成 Issue / PR 评论 /
|
|
5
|
+
* 讨论 / 发版说明。改造前这些入参只有字符串 `body`,模型只能把整篇正文搬进参数——
|
|
6
|
+
* 既烧上下文,又会在 Windows 约 32K 的命令行上限处炸掉(`E2BIG/EINVAL`,错误还很难懂)。
|
|
7
|
+
* 于是 Agent 会绕开插件直接用 `gh issue create -F report.md`,插件的确认闸门/错误分类/渲染
|
|
8
|
+
* 全部被跳过。
|
|
9
|
+
*
|
|
10
|
+
* 为什么不让 gh 自己去读文件(`--body-file <path>`):`pr review` 的行级评注走 REST
|
|
11
|
+
* (`POST /pulls/{n}/reviews`),那条通道根本没有 flag,只能插件侧读文件后塞 JSON。
|
|
12
|
+
* 两套通道各写一份,必然漂移成「带 comments 时 body_file 静默失效」。因此统一为:
|
|
13
|
+
* **插件读一次文件** →
|
|
14
|
+
* - CLI 通道:`[fileFlag, '-']` + `stdin`(`--body-file -` 由 gh 读 stdin,无 argv 长度上限);
|
|
15
|
+
* - REST/GraphQL 通道:直接用 `text`。
|
|
16
|
+
*
|
|
17
|
+
* 兼容性纪律:内联来源的 argv 必须与本模块引入前**逐字节一致**(`[flag, text]`),
|
|
18
|
+
* 新通道只在「传了 `*_file`」时才改变 argv。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
/**
|
|
23
|
+
* GitHub 正文上限(Issue/PR/评论/讨论统一口径 65,536 字符)。
|
|
24
|
+
* 抽成常量而不是散落字面量:这是上游约束,将来调整只改一处。
|
|
25
|
+
*/
|
|
26
|
+
export const GITHUB_BODY_MAX_CHARS = 65_536;
|
|
27
|
+
/**
|
|
28
|
+
* 单个正文文件的字节硬上限(4MB)。
|
|
29
|
+
* 字符上限只管正文语义,管不住「模型指了一个 200MB 日志文件」——先按字节拦住,
|
|
30
|
+
* 避免 readFileSync 把整个文件拉进内存再判定。
|
|
31
|
+
*/
|
|
32
|
+
export const BODY_FILE_MAX_BYTES = 4 * 1024 * 1024;
|
|
33
|
+
/** 严格 UTF-8 解码器:GBK/ANSI 存的中文 md 会抛错,而不是静默产出乱码 */
|
|
34
|
+
const strictUtf8 = new TextDecoder('utf-8', { fatal: true });
|
|
35
|
+
function emptyResult(flag, keepEmptyInline) {
|
|
36
|
+
return { argv: keepEmptyInline ? [flag, ''] : [], text: '', chars: 0, fromFile: false };
|
|
37
|
+
}
|
|
38
|
+
function inlineResult(flag, text) {
|
|
39
|
+
return { argv: [flag, text], text, chars: text.length, fromFile: false };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 解析正文来源。
|
|
43
|
+
*
|
|
44
|
+
* 语义表(与计划文档一致,调用方不再各自二次决策):
|
|
45
|
+
* | 输入 | argv | stdin |
|
|
46
|
+
* | --- | --- | --- |
|
|
47
|
+
* | `file` 有值且文件非空 | `[fileFlag, '-']` | 文件正文 |
|
|
48
|
+
* | `file` 有值但文件为空 | 同「都为空」 | — |
|
|
49
|
+
* | `inline` 非空 | `[flag, inline]` | — |
|
|
50
|
+
* | 都为空 + keepEmptyInline | `[flag, '']` | — |
|
|
51
|
+
* | 都为空 | `[]` | — |
|
|
52
|
+
* | 两者都非空 | 抛错(不做静默优先级) | — |
|
|
53
|
+
*/
|
|
54
|
+
export function resolveBodySource(input, options = { cwd: process.cwd() }) {
|
|
55
|
+
const flag = options.flag ?? '--body';
|
|
56
|
+
const fileFlag = options.fileFlag ?? '--body-file';
|
|
57
|
+
const inlineField = options.inlineField ?? 'body';
|
|
58
|
+
const fileField = options.fileField ?? 'body_file';
|
|
59
|
+
const maxChars = options.maxChars ?? GITHUB_BODY_MAX_CHARS;
|
|
60
|
+
const rawInline = input.inline;
|
|
61
|
+
if (rawInline !== undefined && rawInline !== null && typeof rawInline !== 'string') {
|
|
62
|
+
throw new Error(`${inlineField} 必须是字符串。`);
|
|
63
|
+
}
|
|
64
|
+
// 不做 trim:内联正文要原样下发,保持与改造前逐字节一致
|
|
65
|
+
const inline = typeof rawInline === 'string' ? rawInline : '';
|
|
66
|
+
const rawFile = input.file;
|
|
67
|
+
if (rawFile === undefined || rawFile === null) {
|
|
68
|
+
return inline.length > 0 ? inlineResult(flag, inline) : emptyResult(flag, options.keepEmptyInline);
|
|
69
|
+
}
|
|
70
|
+
if (typeof rawFile !== 'string') {
|
|
71
|
+
throw new Error(`${fileField} 必须是本地文件路径字符串(绝对路径或相对当前工作区的路径)。`);
|
|
72
|
+
}
|
|
73
|
+
const fileInput = rawFile.trim();
|
|
74
|
+
// 空串视同未提供:调用方(含契约测试的样例表)用一个空串表达「这条路不走」
|
|
75
|
+
if (!fileInput) {
|
|
76
|
+
return inline.length > 0 ? inlineResult(flag, inline) : emptyResult(flag, options.keepEmptyInline);
|
|
77
|
+
}
|
|
78
|
+
// 二选一而非优先级:模型拿到一次明确失败,远好过「不知道哪份真正生效」
|
|
79
|
+
if (inline.length > 0) {
|
|
80
|
+
throw new Error(`${inlineField} 与 ${fileField} 同时提供:请二选一(长正文用 ${fileField},短文本用 ${inlineField})。`);
|
|
81
|
+
}
|
|
82
|
+
const abs = path.isAbsolute(fileInput) ? fileInput : path.resolve(options.cwd, fileInput);
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = fs.statSync(abs);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`${fileField} 指向的文件不存在: "${abs}"(输入: "${fileInput}")。`);
|
|
89
|
+
}
|
|
90
|
+
if (!stat.isFile()) {
|
|
91
|
+
throw new Error(`${fileField} 必须是普通文件,当前指向的不是文件: "${abs}"。`);
|
|
92
|
+
}
|
|
93
|
+
if (stat.size > BODY_FILE_MAX_BYTES) {
|
|
94
|
+
// 用字节数报错而不是 toFixed(1) 的 MB:4MB+1 字节会被四舍五入显示成「4.0MB 超过 4MB」,
|
|
95
|
+
// 看上去像插件算错了,反而增加排查成本
|
|
96
|
+
throw new Error(`${fileField} 文件过大: ${stat.size} 字节,超过 ${BODY_FILE_MAX_BYTES} 字节(4MB)上限。`);
|
|
97
|
+
}
|
|
98
|
+
const buf = fs.readFileSync(abs);
|
|
99
|
+
// 二进制先判、再解码:PNG 之类在 UTF-8 解码阶段会报「不是合法 UTF-8」并给出 GBK 另存建议,
|
|
100
|
+
// 对二进制文件是误导;NUL 字节是更准确的判据(文本文件不会有)
|
|
101
|
+
if (buf.includes(0)) {
|
|
102
|
+
throw new Error(`${fileField} 内容是二进制(含 NUL 字节),不能作为正文: "${abs}"。`);
|
|
103
|
+
}
|
|
104
|
+
let text;
|
|
105
|
+
try {
|
|
106
|
+
text = strictUtf8.decode(buf);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error(`${fileField} 不是合法 UTF-8 文本: "${abs}"。Windows 上常见 GBK/ANSI 编码,请另存为 UTF-8 后重试。`);
|
|
110
|
+
}
|
|
111
|
+
// 解码头本身会剥 BOM(TextDecoder 的 ignoreBOM 默认 false);这里显式再剥一次是防御:
|
|
112
|
+
// 一旦有人把上面改成 ignoreBOM: true,正文首字符就会变成零宽 BOM 字符
|
|
113
|
+
if (text.charCodeAt(0) === 0xfeff)
|
|
114
|
+
text = text.slice(1);
|
|
115
|
+
const chars = text.length;
|
|
116
|
+
if (maxChars > 0 && chars > maxChars) {
|
|
117
|
+
throw new Error(`${fileField} 正文过长: ${chars} 字符,超过上限 ${maxChars} 字符。请拆分或精简后再提交。`);
|
|
118
|
+
}
|
|
119
|
+
// 空文件不进 stdin 通道:不依赖 gh 对空 stdin 的处理,语义与「没正文」完全一致
|
|
120
|
+
if (chars === 0)
|
|
121
|
+
return emptyResult(flag, options.keepEmptyInline);
|
|
122
|
+
return { argv: [fileFlag, '-'], stdin: text, text, chars, fromFile: true, file: abs };
|
|
123
|
+
}
|
package/lib/tools/cache.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderCacheMarkdown } from '../renderers/markdown.js';
|
|
2
3
|
function normalizeOutput(data) {
|
|
3
4
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
@@ -9,9 +10,9 @@ function normalizeOutput(data) {
|
|
|
9
10
|
return { result: data !== undefined && data !== null ? data : '' };
|
|
10
11
|
}
|
|
11
12
|
export function createCacheTool(executor) {
|
|
12
|
-
return {
|
|
13
|
+
return wrapWithGate({
|
|
13
14
|
name: 'github_cache',
|
|
14
|
-
description: '管理 GitHub Actions 构建缓存 (Caches):查看缓存列表与占用容量、按 ID 或 Key
|
|
15
|
+
description: '管理 GitHub Actions 构建缓存 (Caches):查看缓存列表与占用容量、按 ID 或 Key 删除指定缓存、以及全量清空仓库缓存。删除不可逆,delete 的三种方式(cache_id / key / all)均需 confirm: true。',
|
|
15
16
|
parameters: {
|
|
16
17
|
type: 'object',
|
|
17
18
|
properties: {
|
|
@@ -56,7 +57,7 @@ export function createCacheTool(executor) {
|
|
|
56
57
|
},
|
|
57
58
|
confirm: {
|
|
58
59
|
type: 'boolean',
|
|
59
|
-
description: '高危破坏性操作确认标志(delete
|
|
60
|
+
description: '高危破坏性操作确认标志(delete 的三态 cache_id / key / all 均必需)。未传 true 时输出影响评估报告并拦截。',
|
|
60
61
|
},
|
|
61
62
|
},
|
|
62
63
|
required: ['action'],
|
|
@@ -68,8 +69,7 @@ export function createCacheTool(executor) {
|
|
|
68
69
|
},
|
|
69
70
|
},
|
|
70
71
|
async execute(args, execContext) {
|
|
71
|
-
const cwd
|
|
72
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
72
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
73
73
|
let repoTarget = args.repo;
|
|
74
74
|
if (!repoTarget) {
|
|
75
75
|
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
@@ -120,19 +120,26 @@ export function createCacheTool(executor) {
|
|
|
120
120
|
return normalizeOutput(rawData || []);
|
|
121
121
|
}
|
|
122
122
|
case 'delete': {
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
123
|
+
// ---- 第一段:选择器归一化 + 形态校验(在任何执行之前)----
|
|
124
|
+
const hasAll = args.all === true;
|
|
125
|
+
const cacheIdStr = args.cache_id !== undefined && args.cache_id !== null ? String(args.cache_id).trim() : '';
|
|
126
|
+
const keyStr = args.key !== undefined && args.key !== null ? String(args.key).trim() : '';
|
|
127
|
+
const hasId = cacheIdStr !== '';
|
|
128
|
+
const hasKey = keyStr !== '';
|
|
129
|
+
if (!hasAll && !hasId && !hasKey) {
|
|
130
|
+
throw new Error('删除缓存时必须指定 `cache_id`、`key` 或 `all: true`(三条删除分支均需配合 confirm: true)。');
|
|
131
|
+
}
|
|
132
|
+
// 形态校验必须先于执行:不校验时 `cache_id: "--all"` 会被当作 flag 透传,
|
|
133
|
+
// 等价于清空全仓缓存;而 `gh cache delete --all` 自身没有二次确认,绕过即秒清。
|
|
134
|
+
if (hasId && !/^\d+$/.test(cacheIdStr)) {
|
|
135
|
+
throw new Error(`cache_id 必须是纯数字的缓存 ID(实际收到 "${cacheIdStr}")。按 key 删除请改用 key 参数,全量清空请用 all: true。`);
|
|
136
|
+
}
|
|
137
|
+
if (hasKey && (keyStr.startsWith('-') || keyStr.length > 512)) {
|
|
138
|
+
throw new Error('key 不能以 - 开头(防被当作命令行 flag),且长度不得超过 512 字符。');
|
|
139
|
+
}
|
|
140
|
+
// ---- 执行段(argv 形状与改动前保持一致)----
|
|
141
|
+
// confirm 闸门由 wrapWithGate 在工厂入口统一处理,三条分支共用同一套语义。
|
|
142
|
+
if (hasAll) {
|
|
136
143
|
const delCmd = ['cache', 'delete', '--all', '--succeed-on-no-caches', ...repoArgs];
|
|
137
144
|
if (args.ref) {
|
|
138
145
|
delCmd.push('-r', String(args.ref));
|
|
@@ -147,40 +154,35 @@ export function createCacheTool(executor) {
|
|
|
147
154
|
detail: res.rawOutput || '',
|
|
148
155
|
};
|
|
149
156
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const delCmd = ['cache', 'delete', String(args.cache_id), ...repoArgs];
|
|
157
|
+
if (hasId) {
|
|
158
|
+
const delCmd = ['cache', 'delete', cacheIdStr, ...repoArgs];
|
|
153
159
|
const res = await executor.run(delCmd, runOpts);
|
|
154
160
|
if (!res.ok)
|
|
155
161
|
throw new Error(res.error);
|
|
156
162
|
return {
|
|
157
163
|
success: true,
|
|
158
|
-
cache_id:
|
|
159
|
-
message: `🗑️ 成功删除仓库 ${repoTarget} 中 ID 为 "${
|
|
164
|
+
cache_id: cacheIdStr,
|
|
165
|
+
message: `🗑️ 成功删除仓库 ${repoTarget} 中 ID 为 "${cacheIdStr}" 的 Actions 缓存`,
|
|
160
166
|
detail: res.rawOutput || '',
|
|
161
167
|
};
|
|
162
168
|
}
|
|
163
|
-
|
|
164
|
-
if (args.
|
|
165
|
-
|
|
166
|
-
if (args.ref) {
|
|
167
|
-
delCmd.push('-r', String(args.ref));
|
|
168
|
-
}
|
|
169
|
-
const res = await executor.run(delCmd, runOpts);
|
|
170
|
-
if (!res.ok)
|
|
171
|
-
throw new Error(res.error);
|
|
172
|
-
return {
|
|
173
|
-
success: true,
|
|
174
|
-
key: String(args.key),
|
|
175
|
-
message: `🗑️ 成功删除仓库 ${repoTarget} 中 Key 为 "${args.key}" 的 Actions 缓存` + (args.ref ? ` (Ref: ${args.ref})` : ''),
|
|
176
|
-
detail: res.rawOutput || '',
|
|
177
|
-
};
|
|
169
|
+
const delCmd = ['cache', 'delete', keyStr, ...repoArgs];
|
|
170
|
+
if (args.ref) {
|
|
171
|
+
delCmd.push('-r', String(args.ref));
|
|
178
172
|
}
|
|
179
|
-
|
|
173
|
+
const res = await executor.run(delCmd, runOpts);
|
|
174
|
+
if (!res.ok)
|
|
175
|
+
throw new Error(res.error);
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
key: keyStr,
|
|
179
|
+
message: `🗑️ 成功删除仓库 ${repoTarget} 中 Key 为 "${keyStr}" 的 Actions 缓存` + (args.ref ? ` (Ref: ${args.ref})` : ''),
|
|
180
|
+
detail: res.rawOutput || '',
|
|
181
|
+
};
|
|
180
182
|
}
|
|
181
183
|
default:
|
|
182
184
|
throw new Error(`不支持的 cache 操作: "${args.action}"。可选值: list, delete。`);
|
|
183
185
|
}
|
|
184
186
|
},
|
|
185
|
-
};
|
|
187
|
+
});
|
|
186
188
|
}
|
package/lib/tools/codespace.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderCodespaceMarkdown } from '../renderers/markdown.js';
|
|
2
3
|
import { checkScopeRequired } from './project.js';
|
|
3
4
|
function normalizeOutput(data) {
|
|
@@ -12,7 +13,7 @@ function normalizeOutput(data) {
|
|
|
12
13
|
const CODESPACE_LIST_FIELDS = 'name,displayName,repository,owner,state,gitStatus,lastUsedAt,createdAt,machineName';
|
|
13
14
|
const CODESPACE_VIEW_FIELDS = 'name,displayName,repository,owner,state,gitStatus,lastUsedAt,createdAt,machineName,devcontainerPath,recentFolders,idleTimeoutMinutes';
|
|
14
15
|
export function createCodespaceTool(executor) {
|
|
15
|
-
return {
|
|
16
|
+
return wrapWithGate({
|
|
16
17
|
name: 'github_codespace',
|
|
17
18
|
description: '管理 GitHub Codespaces 云端远程开发环境:查阅实例列表与运行状态、检视实例配置详情、停止运行中的环境防计费、安全销毁实例。未授权时提供平台感知自愈命令。',
|
|
18
19
|
parameters: {
|
|
@@ -53,8 +54,7 @@ export function createCodespaceTool(executor) {
|
|
|
53
54
|
},
|
|
54
55
|
},
|
|
55
56
|
async execute(args, execContext) {
|
|
56
|
-
const cwd
|
|
57
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
57
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
58
58
|
const runOpts = { cwd, sessionId };
|
|
59
59
|
switch (args.action) {
|
|
60
60
|
case 'list': {
|
|
@@ -123,14 +123,6 @@ export function createCodespaceTool(executor) {
|
|
|
123
123
|
throw new Error('delete 操作必须提供 codespace 名称');
|
|
124
124
|
}
|
|
125
125
|
const cleanName = args.codespace.trim();
|
|
126
|
-
if (args.confirm !== true) {
|
|
127
|
-
return {
|
|
128
|
-
status: 'AWAITING_CONFIRMATION',
|
|
129
|
-
warning: `⚠️ 销毁确认拦截:你即将永久删除云端开发环境「${cleanName}」。`,
|
|
130
|
-
impact: '物理销毁 Codespace 将永久释放云端计算资源,环境中任何未提交并推送到远端 Git 的临时修改都将无法找回。',
|
|
131
|
-
requiredAction: '如确认销毁该实例,请核对环境名称并在参数中附加 `confirm: true` 重新调用。',
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
126
|
const deleteCmd = ['codespace', 'delete', '-c', cleanName, '-f'];
|
|
135
127
|
if (args.org)
|
|
136
128
|
deleteCmd.push('-o', String(args.org).trim());
|
|
@@ -150,5 +142,5 @@ export function createCodespaceTool(executor) {
|
|
|
150
142
|
throw new Error(`未知的 Codespace 操作: ${args.action}`);
|
|
151
143
|
}
|
|
152
144
|
},
|
|
153
|
-
};
|
|
145
|
+
});
|
|
154
146
|
}
|
package/lib/tools/discussion.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderDiscussionMarkdown } from '../renderers/markdown.js';
|
|
3
|
+
import { resolveBodySource } from './body-input.js';
|
|
2
4
|
function normalizeOutput(data) {
|
|
3
5
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
4
6
|
return data;
|
|
@@ -11,7 +13,7 @@ function normalizeOutput(data) {
|
|
|
11
13
|
const DISCUSSION_LIST_FIELDS = 'number,title,body,category,author,closed,createdAt,updatedAt,url,labels,answered,stateReason';
|
|
12
14
|
const DISCUSSION_VIEW_FIELDS = 'number,title,body,category,author,state,createdAt,updatedAt,url,labels,answered,comments';
|
|
13
15
|
export function createDiscussionTool(executor) {
|
|
14
|
-
return {
|
|
16
|
+
return wrapWithGate({
|
|
15
17
|
name: 'github_discussion',
|
|
16
18
|
description: '管理 GitHub Discussions 讨论区:浏览社区讨论、检视讨论详情与回复楼层、发起新讨论议题、编辑现有讨论以及追加评论回复。基于 gh CLI preview 特性。',
|
|
17
19
|
parameters: {
|
|
@@ -36,7 +38,11 @@ export function createDiscussionTool(executor) {
|
|
|
36
38
|
},
|
|
37
39
|
body: {
|
|
38
40
|
type: 'string',
|
|
39
|
-
description: '讨论正文或评论正文(create, comment 必需,edit
|
|
41
|
+
description: '讨论正文或评论正文(create, comment 必需,edit 动作修改正文时可选)。与 body_file 二选一。',
|
|
42
|
+
},
|
|
43
|
+
body_file: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: '正文来源:本地 Markdown 文件路径(相对路径按当前工作区解析)。与 body 二选一;长正文用本参数,无需把全文读进上下文。',
|
|
40
46
|
},
|
|
41
47
|
category: {
|
|
42
48
|
type: 'string',
|
|
@@ -74,10 +80,17 @@ export function createDiscussionTool(executor) {
|
|
|
74
80
|
},
|
|
75
81
|
},
|
|
76
82
|
async execute(args, execContext) {
|
|
77
|
-
const cwd
|
|
78
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
83
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
79
84
|
const repoArgs = args.repo ? ['-R', args.repo] : [];
|
|
80
85
|
const runOpts = { cwd, sessionId };
|
|
86
|
+
// 正文来自本地文件时经 stdin 注入(`--body-file -`):绕开 Windows 约 32K 的命令行上限
|
|
87
|
+
const stdinPatch = (body) => (body.stdin === undefined ? {} : { stdin: body.stdin });
|
|
88
|
+
/*
|
|
89
|
+
* 内联正文沿用改造前的 trim 口径(旧代码是 `-b args.body.trim()`):discussion 历史上
|
|
90
|
+
* 就按去空白下发,改成原样会动到既有调用方的 argv。文件来源不 trim——那是用户排好版的
|
|
91
|
+
* Markdown,首尾空行/缩进可能有意义,strip 掉等于替用户改稿。
|
|
92
|
+
*/
|
|
93
|
+
const trimInline = (v) => (typeof v === 'string' ? v.trim() : v);
|
|
81
94
|
switch (args.action) {
|
|
82
95
|
case 'list': {
|
|
83
96
|
const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(args.limit, 100) : 30;
|
|
@@ -126,8 +139,10 @@ export function createDiscussionTool(executor) {
|
|
|
126
139
|
if (!args.title || typeof args.title !== 'string' || !args.title.trim()) {
|
|
127
140
|
throw new Error('create 操作必须提供 title(讨论标题)');
|
|
128
141
|
}
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
const body = resolveBodySource({ inline: trimInline(args.body), file: args.body_file }, { cwd, flag: '-b' });
|
|
143
|
+
// 判空只看解析后的权威正文:文件来源时 args.body 恒为空,拿它判定会误杀
|
|
144
|
+
if (body.chars === 0) {
|
|
145
|
+
throw new Error('create 操作必须提供 body 或 body_file(讨论正文)');
|
|
131
146
|
}
|
|
132
147
|
if (!args.category || typeof args.category !== 'string' || !args.category.trim()) {
|
|
133
148
|
throw new Error('create 操作必须提供 category(讨论分类,例如 "General"、"Q&A"、"Ideas")');
|
|
@@ -135,11 +150,11 @@ export function createDiscussionTool(executor) {
|
|
|
135
150
|
const createCmd = [
|
|
136
151
|
'discussion', 'create',
|
|
137
152
|
'-t', args.title.trim(),
|
|
138
|
-
|
|
153
|
+
...body.argv,
|
|
139
154
|
'-c', args.category.trim(),
|
|
140
155
|
...repoArgs,
|
|
141
156
|
];
|
|
142
|
-
const res = await executor.run(createCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
|
|
157
|
+
const res = await executor.run(createCmd, { ...runOpts, rawText: true, timeoutMs: 60_000, ...stdinPatch(body) });
|
|
143
158
|
if (!res.ok)
|
|
144
159
|
throw new Error(res.error);
|
|
145
160
|
return {
|
|
@@ -151,15 +166,15 @@ export function createDiscussionTool(executor) {
|
|
|
151
166
|
if (!args.discussion_number || typeof args.discussion_number !== 'number') {
|
|
152
167
|
throw new Error('edit 操作必须提供 discussion_number(讨论数字编号)');
|
|
153
168
|
}
|
|
154
|
-
|
|
169
|
+
const body = resolveBodySource({ inline: trimInline(args.body), file: args.body_file }, { cwd, flag: '-b' });
|
|
170
|
+
if (!args.title && body.chars === 0) {
|
|
155
171
|
throw new Error('edit 操作至少需提供 title 或 body 中的一项进行修改');
|
|
156
172
|
}
|
|
157
173
|
const editCmd = ['discussion', 'edit', String(args.discussion_number), ...repoArgs];
|
|
158
174
|
if (args.title)
|
|
159
175
|
editCmd.push('-t', String(args.title).trim());
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const res = await executor.run(editCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
|
|
176
|
+
editCmd.push(...body.argv);
|
|
177
|
+
const res = await executor.run(editCmd, { ...runOpts, rawText: true, timeoutMs: 60_000, ...stdinPatch(body) });
|
|
163
178
|
if (!res.ok)
|
|
164
179
|
throw new Error(res.error);
|
|
165
180
|
return {
|
|
@@ -171,15 +186,16 @@ export function createDiscussionTool(executor) {
|
|
|
171
186
|
if (!args.discussion_number || typeof args.discussion_number !== 'number') {
|
|
172
187
|
throw new Error('comment 操作必须提供 discussion_number(讨论数字编号)');
|
|
173
188
|
}
|
|
174
|
-
|
|
175
|
-
|
|
189
|
+
const body = resolveBodySource({ inline: trimInline(args.body), file: args.body_file }, { cwd, flag: '-b' });
|
|
190
|
+
if (body.chars === 0) {
|
|
191
|
+
throw new Error('comment 操作必须提供 body 或 body_file(评论正文)');
|
|
176
192
|
}
|
|
177
193
|
const commentCmd = [
|
|
178
194
|
'discussion', 'comment', String(args.discussion_number),
|
|
179
|
-
|
|
195
|
+
...body.argv,
|
|
180
196
|
...repoArgs,
|
|
181
197
|
];
|
|
182
|
-
const res = await executor.run(commentCmd, { ...runOpts, rawText: true, timeoutMs: 60_000 });
|
|
198
|
+
const res = await executor.run(commentCmd, { ...runOpts, rawText: true, timeoutMs: 60_000, ...stdinPatch(body) });
|
|
183
199
|
if (!res.ok)
|
|
184
200
|
throw new Error(res.error);
|
|
185
201
|
return {
|
|
@@ -191,5 +207,5 @@ export function createDiscussionTool(executor) {
|
|
|
191
207
|
throw new Error(`未知的 Discussion 操作: ${args.action}`);
|
|
192
208
|
}
|
|
193
209
|
},
|
|
194
|
-
};
|
|
210
|
+
});
|
|
195
211
|
}
|
package/lib/tools/gist.d.ts
CHANGED
|
@@ -71,14 +71,9 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
71
71
|
public?: undefined;
|
|
72
72
|
description?: undefined;
|
|
73
73
|
success?: undefined;
|
|
74
|
+
gist_id?: undefined;
|
|
74
75
|
message?: undefined;
|
|
75
76
|
detail?: undefined;
|
|
76
|
-
status?: undefined;
|
|
77
|
-
action?: undefined;
|
|
78
|
-
gist_id?: undefined;
|
|
79
|
-
warning?: undefined;
|
|
80
|
-
impact?: undefined;
|
|
81
|
-
requiredAction?: undefined;
|
|
82
77
|
} | {
|
|
83
78
|
items?: undefined;
|
|
84
79
|
count?: undefined;
|
|
@@ -92,11 +87,6 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
92
87
|
success?: undefined;
|
|
93
88
|
message?: undefined;
|
|
94
89
|
detail?: undefined;
|
|
95
|
-
status?: undefined;
|
|
96
|
-
action?: undefined;
|
|
97
|
-
warning?: undefined;
|
|
98
|
-
impact?: undefined;
|
|
99
|
-
requiredAction?: undefined;
|
|
100
90
|
} | {
|
|
101
91
|
items?: undefined;
|
|
102
92
|
count?: undefined;
|
|
@@ -110,11 +100,6 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
110
100
|
description: any;
|
|
111
101
|
message: string;
|
|
112
102
|
detail?: undefined;
|
|
113
|
-
status?: undefined;
|
|
114
|
-
action?: undefined;
|
|
115
|
-
warning?: undefined;
|
|
116
|
-
impact?: undefined;
|
|
117
|
-
requiredAction?: undefined;
|
|
118
103
|
} | {
|
|
119
104
|
items?: undefined;
|
|
120
105
|
count?: undefined;
|
|
@@ -128,28 +113,5 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
128
113
|
gist_id: string;
|
|
129
114
|
message: string;
|
|
130
115
|
detail: string;
|
|
131
|
-
status?: undefined;
|
|
132
|
-
action?: undefined;
|
|
133
|
-
warning?: undefined;
|
|
134
|
-
impact?: undefined;
|
|
135
|
-
requiredAction?: undefined;
|
|
136
|
-
} | {
|
|
137
|
-
items?: undefined;
|
|
138
|
-
count?: undefined;
|
|
139
|
-
rawText?: undefined;
|
|
140
|
-
filename?: undefined;
|
|
141
|
-
content?: undefined;
|
|
142
|
-
url?: undefined;
|
|
143
|
-
public?: undefined;
|
|
144
|
-
description?: undefined;
|
|
145
|
-
success?: undefined;
|
|
146
|
-
message?: undefined;
|
|
147
|
-
detail?: undefined;
|
|
148
|
-
status: string;
|
|
149
|
-
action: string;
|
|
150
|
-
gist_id: string;
|
|
151
|
-
warning: string;
|
|
152
|
-
impact: string;
|
|
153
|
-
requiredAction: string;
|
|
154
116
|
}>;
|
|
155
117
|
};
|
package/lib/tools/gist.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderGistMarkdown, parseGistListOutput } from '../renderers/markdown.js';
|
|
2
3
|
export function createGistTool(executor) {
|
|
3
|
-
return {
|
|
4
|
+
return wrapWithGate({
|
|
4
5
|
name: 'github_gist',
|
|
5
6
|
description: '管理 GitHub Gist 代码片段与诊断留痕:查看 Gist 列表、检视代码正文 (view)、通过文本内容或本地文件创建新 Gist (create,默认安全 secret 模式)、编辑以及安全删除。删除操作不可逆,需 confirm: true。',
|
|
6
7
|
parameters: {
|
|
@@ -58,8 +59,7 @@ export function createGistTool(executor) {
|
|
|
58
59
|
},
|
|
59
60
|
},
|
|
60
61
|
async execute(args, execContext) {
|
|
61
|
-
const cwd
|
|
62
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
62
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
63
63
|
const runOpts = { cwd, sessionId, rawText: true };
|
|
64
64
|
switch (args.action) {
|
|
65
65
|
case 'list': {
|
|
@@ -161,16 +161,6 @@ export function createGistTool(executor) {
|
|
|
161
161
|
if (!args.gist_id) {
|
|
162
162
|
throw new Error('删除 Gist 时 `gist_id` 参数为必填项。');
|
|
163
163
|
}
|
|
164
|
-
if (args.confirm !== true) {
|
|
165
|
-
return {
|
|
166
|
-
status: 'AWAITING_CONFIRMATION',
|
|
167
|
-
action: 'delete',
|
|
168
|
-
gist_id: String(args.gist_id),
|
|
169
|
-
warning: `⚠️ 高危破坏性操作:即将永久删除 Gist 代码片段 "${args.gist_id}"`,
|
|
170
|
-
impact: `删除 Gist 为物理删除,无法通过 GitHub API 恢复,所有引用该 Gist URL 的外部链接将失效并返回 404。`,
|
|
171
|
-
requiredAction: `如确认彻底删除,请在参数中附加 \`confirm: true\` 重新调用。示例:\`github_gist(action: 'delete', gist_id: '${args.gist_id}', confirm: true)\``,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
164
|
const delCmd = ['gist', 'delete', String(args.gist_id), '--yes'];
|
|
175
165
|
const res = await executor.run(delCmd, runOpts);
|
|
176
166
|
if (!res.ok)
|
|
@@ -186,5 +176,5 @@ export function createGistTool(executor) {
|
|
|
186
176
|
throw new Error(`不支持的 gist 操作: "${args.action}"。可选值: list, view, create, edit, delete。`);
|
|
187
177
|
}
|
|
188
178
|
},
|
|
189
|
-
};
|
|
179
|
+
});
|
|
190
180
|
}
|