dsh-github-flow 0.2.0 → 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.
- package/README.en.md +31 -12
- package/README.md +33 -14
- package/docs/UPGRADE-NOTES.md +93 -0
- package/docs/architecture.md +33 -18
- package/docs/images/1.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/npm-publish-guide.md +15 -4
- package/lib/api/routes-actions.d.ts +18 -4
- package/lib/api/routes-actions.js +143 -19
- package/lib/api/routes-lifecycle.d.ts +1 -1
- package/lib/api/routes-lifecycle.js +53 -2
- package/lib/client.js +411 -44
- package/lib/commands/gh.js +11 -2
- package/lib/executor.js +122 -12
- package/lib/main.js +9 -0
- package/lib/renderers/markdown.d.ts +6 -1
- package/lib/renderers/markdown.js +41 -14
- package/lib/tools/help.js +12 -0
- package/lib/tools/image-utils.d.ts +79 -0
- package/lib/tools/image-utils.js +247 -0
- package/lib/tools/issue.d.ts +9 -4
- package/lib/tools/issue.js +75 -4
- package/lib/tools/pr.d.ts +8 -4
- package/lib/tools/pr.js +80 -6
- package/lib/tools/repo.d.ts +12 -4
- package/lib/tools/repo.js +71 -3
- package/lib/types.d.ts +12 -1
- package/lib/update-settings.d.ts +18 -0
- package/lib/update-settings.js +60 -0
- package/lib/updater.d.ts +32 -1
- package/lib/updater.js +117 -9
- package/lib/upgrade-manager.d.ts +70 -1
- package/lib/upgrade-manager.js +346 -46
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/commands/gh.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL } from '../version.js';
|
|
2
|
+
import { loadUpdateSettings, describeUpdateMode } from '../update-settings.js';
|
|
2
3
|
export function registerGhCommand(commandsService, executor, updateChecker) {
|
|
3
4
|
if (!commandsService || typeof commandsService.register !== 'function')
|
|
4
5
|
return;
|
|
@@ -53,12 +54,20 @@ export function registerGhCommand(commandsService, executor, updateChecker) {
|
|
|
53
54
|
const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
|
|
54
55
|
let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
|
|
55
56
|
md += `- **当前运行版本**: \`v${curVer}\`\n`;
|
|
57
|
+
// 当前更新源:排查「查不到新版」时(镜像同步滞后)用户需可见自己在用哪个源。
|
|
58
|
+
// 优先读 checker 内存态(store 写盘失败时磁盘是旧值,活实例才是事实源),
|
|
59
|
+
// 无 checker 或未初始化(sourceMode=null 的 legacy 路径)回落磁盘 store。
|
|
60
|
+
const liveMode = updateChecker?.getSourceMode?.() ?? null;
|
|
61
|
+
const effectiveMode = liveMode ?? loadUpdateSettings();
|
|
62
|
+
md += `- **当前更新源**: ${describeUpdateMode(effectiveMode)}\n`;
|
|
56
63
|
md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
|
|
57
64
|
md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
|
|
58
65
|
md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
|
|
59
66
|
md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
// 官方 CLI 语义:--profile 为必填参数(dsh plugin --profile <name> add <pkg>),
|
|
68
|
+
// 任何不带 --profile 的形态(含 pnpm dsh plugin add)都会直接报 required option 错误。
|
|
69
|
+
md += `1. **DSH 桌面版 Profile 环境升级(推荐)**:\n\`\`\`bash\ndsh plugin --profile desktop add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
|
|
70
|
+
md += `2. **DSH Web / CLI 环境**(profile 名按实际环境填写):\n\`\`\`bash\ndsh plugin --profile web add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n`;
|
|
62
71
|
if (isMac) {
|
|
63
72
|
md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
|
|
64
73
|
}
|
package/lib/executor.js
CHANGED
|
@@ -1,9 +1,87 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { findMostRecentWorkspacePath, findWorkspacePathBySession, hasGitDir } from './workspace.js';
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
|
+
/**
|
|
8
|
+
* spawn 形态的执行通道(stdin 可选注入)。
|
|
9
|
+
*
|
|
10
|
+
* 为什么存在:execFile 的 input 选项在本机(node 22.18.0 + Windows)实测从不写入
|
|
11
|
+
* 子进程 stdin——既无数据也无 EOF,管道永久悬空(gh secret set 等需要 stdin 的
|
|
12
|
+
* 命令会一直等到超时)。最小复现:execFile+input 对 `cmd /c more` 与 node 子进程
|
|
13
|
+
* 均挂起;而 spawn + stdin.end(data) 全部正常。此函数即为后者的工程化封装,
|
|
14
|
+
* 仅在调用方显式传入 stdin 时使用,其余路径保持 execFileAsync 零改动。
|
|
15
|
+
*
|
|
16
|
+
* 为什么超时要自己管:spawn 无 timeout 选项,用 kill + SIGTERM 兜底;
|
|
17
|
+
* 输出按 maxBuffer 手工聚合(上限与 execFile 路径一致,15MB)。
|
|
18
|
+
*/
|
|
19
|
+
function spawnWithStdin(ghPath, args, opts) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const child = spawn(ghPath, args, {
|
|
22
|
+
cwd: opts.cwd,
|
|
23
|
+
env: opts.env,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
26
|
+
});
|
|
27
|
+
const outChunks = [];
|
|
28
|
+
const errChunks = [];
|
|
29
|
+
let totalBytes = 0;
|
|
30
|
+
let killed = false;
|
|
31
|
+
let settled = false;
|
|
32
|
+
const timer = setTimeout(() => {
|
|
33
|
+
killed = true;
|
|
34
|
+
child.kill();
|
|
35
|
+
}, opts.timeout);
|
|
36
|
+
const settle = (err) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (err)
|
|
42
|
+
reject(err);
|
|
43
|
+
else
|
|
44
|
+
resolve({ stdout: Buffer.concat(outChunks), stderr: Buffer.concat(errChunks), killed });
|
|
45
|
+
};
|
|
46
|
+
// 聚合上限:超出即杀(与 execFile maxBuffer 语义一致;gh 正常输出远小于此)
|
|
47
|
+
const MAX_BYTES = 15 * 1024 * 1024;
|
|
48
|
+
child.stdout?.on('data', (d) => {
|
|
49
|
+
totalBytes += d.length;
|
|
50
|
+
outChunks.push(d);
|
|
51
|
+
if (totalBytes > MAX_BYTES) {
|
|
52
|
+
killed = true;
|
|
53
|
+
child.kill();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
child.stderr?.on('data', (d) => { errChunks.push(d); });
|
|
57
|
+
child.on('error', (err) => {
|
|
58
|
+
// spawn 失败(如 ENOENT)对齐 execFile 的错误形状:code 保留在 Error 上
|
|
59
|
+
const e = Object.assign(new Error(err.message), { code: err.code });
|
|
60
|
+
settle(e);
|
|
61
|
+
});
|
|
62
|
+
child.on('close', (code, signal) => {
|
|
63
|
+
if (killed) {
|
|
64
|
+
// 超时/超量被杀:对齐 execFile 的 Timeout 错误形状(err.killed + signal)
|
|
65
|
+
const e = Object.assign(new Error(`Command timed out after ${opts.timeout}ms`), { killed: true, signal: signal || 'SIGTERM' });
|
|
66
|
+
settle(e);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (code !== 0) {
|
|
70
|
+
const e = Object.assign(new Error(`Command failed with exit code ${code}`), { code, stderr: Buffer.concat(errChunks).toString('utf8').slice(0, 800) });
|
|
71
|
+
settle(e);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
settle();
|
|
75
|
+
});
|
|
76
|
+
// stdin 注入:end(data) 半关闭(写完即 EOF),子进程流式读取或一次性读取均可
|
|
77
|
+
child.stdin?.end(opts.stdin, (err) => {
|
|
78
|
+
if (err && !settled) {
|
|
79
|
+
const e = Object.assign(new Error(`stdin write failed: ${err.message}`), { code: 'EPIPE' });
|
|
80
|
+
settle(e);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
7
85
|
/** 认证状态缓存时长。用户手动 `gh auth login/logout` 后最多 30 秒内面板才会反映,属可接受代价 */
|
|
8
86
|
const AUTH_CACHE_TTL_MS = 30_000;
|
|
9
87
|
/**
|
|
@@ -185,11 +263,16 @@ export class GhExecutor {
|
|
|
185
263
|
async run(args, options = {}) {
|
|
186
264
|
const cwd = this.resolveWorkspaceCwd(options.cwd, options.sessionId);
|
|
187
265
|
const timeout = options.timeoutMs || this.defaultTimeoutMs;
|
|
266
|
+
// 二进制模式(如 gh api 下载图片字节流):stdout 保持 Buffer,绕过 UTF-8
|
|
267
|
+
// 字符串转换(PNG 等二进制经 utf8 解码必然损坏),也不做截断(截断只面向
|
|
268
|
+
// 文本 Token 预算)。显式开启才生效,既有调用方零感知。
|
|
269
|
+
const binary = options.binary === true;
|
|
188
270
|
try {
|
|
189
|
-
|
|
271
|
+
// stdin 显式传入时走 spawn 通道:execFile 的 input 选项在本机从不真正写入
|
|
272
|
+
// (详见 spawnWithStdin 注释),gh secret set 等命令会挂到超时。其余调用
|
|
273
|
+
// 保持 execFileAsync 原路径零改动。
|
|
274
|
+
const commonOpts = {
|
|
190
275
|
cwd,
|
|
191
|
-
timeout,
|
|
192
|
-
maxBuffer: 15 * 1024 * 1024, // 15MB 进程缓冲
|
|
193
276
|
env: {
|
|
194
277
|
...process.env,
|
|
195
278
|
GH_NO_UPDATE_NOTIFIER: '1', // 禁用 CLI 版本更新检查,避免污染 stdout
|
|
@@ -199,14 +282,41 @@ export class GhExecutor {
|
|
|
199
282
|
...(this.proxyManager?.getEnv('gh') || {}),
|
|
200
283
|
...options.env,
|
|
201
284
|
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
285
|
+
};
|
|
286
|
+
let stdout;
|
|
287
|
+
let stderr;
|
|
288
|
+
if (options.stdin !== undefined) {
|
|
289
|
+
const r = await spawnWithStdin(this.ghPath, args, { ...commonOpts, timeout, stdin: options.stdin });
|
|
290
|
+
stdout = r.stdout;
|
|
291
|
+
stderr = r.stderr;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const r = await execFileAsync(this.ghPath, args, {
|
|
295
|
+
...commonOpts,
|
|
296
|
+
timeout,
|
|
297
|
+
maxBuffer: 15 * 1024 * 1024, // 15MB 进程缓冲
|
|
298
|
+
...(binary ? { encoding: 'buffer' } : {}),
|
|
299
|
+
});
|
|
300
|
+
stdout = r.stdout;
|
|
301
|
+
stderr = r.stderr;
|
|
302
|
+
}
|
|
303
|
+
// 二进制通道:Buffer 原样返回(调用方自行写文件/校验魔数),stderr 仅作
|
|
304
|
+
// 错误线索不做输出回退——把错误提示字节当图片保存毫无意义。
|
|
305
|
+
// TS 无法从 options 展开的 encoding 键收窄 stdout 类型,这里显式归一化。
|
|
306
|
+
if (binary) {
|
|
307
|
+
const buf = Buffer.isBuffer(stdout) ? stdout : Buffer.from(String(stdout ?? ''));
|
|
308
|
+
const errBuf = Buffer.isBuffer(stderr) ? stderr : (stderr ? Buffer.from(String(stderr)) : null);
|
|
309
|
+
if (!buf.length) {
|
|
310
|
+
const errMsg = errBuf ? errBuf.toString('utf8').trim() : '';
|
|
311
|
+
return { ok: false, error: errMsg || '二进制输出为空' };
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, data: buf, binaryBytes: buf.length, truncated: false };
|
|
314
|
+
}
|
|
315
|
+
const outText = Buffer.isBuffer(stdout) ? stdout.toString('utf8') : String(stdout ?? '');
|
|
316
|
+
const errText = Buffer.isBuffer(stderr) ? stderr.toString('utf8') : String(stderr ?? '');
|
|
317
|
+
let rawOutput = outText.trim();
|
|
318
|
+
if (!rawOutput && errText) {
|
|
319
|
+
rawOutput = errText.trim();
|
|
210
320
|
}
|
|
211
321
|
const outcome = truncateOutput(rawOutput, this.maxOutputChars, this.maxOutputBytes);
|
|
212
322
|
rawOutput = outcome.text;
|
package/lib/main.js
CHANGED
|
@@ -14,6 +14,7 @@ import { UpdateChecker } from './updater.js';
|
|
|
14
14
|
import { UpgradeManager } from './upgrade-manager.js';
|
|
15
15
|
import { ProxyManager } from './proxy.js';
|
|
16
16
|
import { NPM_PACKAGE_URL } from './version.js';
|
|
17
|
+
import { loadUpdateSettings } from './update-settings.js';
|
|
17
18
|
// 引用即生效:加载本模块以挂上 declare module '@deepseek-ai/cordis' 的 ctx.github 类型声明,
|
|
18
19
|
// 并随下面 `export * from './github-service.js'` 一起传给消费方。
|
|
19
20
|
import './github-service.js';
|
|
@@ -62,10 +63,18 @@ export function apply(ctx, config = {}) {
|
|
|
62
63
|
const updateChecker = new UpdateChecker({
|
|
63
64
|
intervalMs: config.updateCheckIntervalMs,
|
|
64
65
|
customRegistry: config.registryUrl,
|
|
66
|
+
// checkUpdate: false 熔断全部探测入口(路由/命令/启动横幅共用此实例)
|
|
67
|
+
enabled: config.checkUpdate !== false,
|
|
65
68
|
});
|
|
69
|
+
// 更新源设置(UI 持久化 store 优先于 Cordis 静态配置):
|
|
70
|
+
// 启动时读取并应用到 checker;UI 保存时经路由直接调 applySettings 热切换。
|
|
71
|
+
const updateMode = loadUpdateSettings();
|
|
72
|
+
updateChecker.applySettings(updateMode);
|
|
66
73
|
updateChecker.applyProxy(proxyManager);
|
|
67
74
|
const upgradeManager = new UpgradeManager();
|
|
68
75
|
upgradeManager.attachProxyManager(proxyManager);
|
|
76
|
+
// 注入查询函数而非静态值:用户切源后,下一次一键升级的 registry 实时跟随
|
|
77
|
+
upgradeManager.attachUpdateModeQuery(() => updateChecker.getSourceMode());
|
|
69
78
|
// 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
|
|
70
79
|
if (ctx.provide && typeof ctx.provide === 'function') {
|
|
71
80
|
ctx.provide('github', executor);
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
export declare function sanitizeCell(text: any, maxLen?: number): string;
|
|
12
12
|
/** 格式化 PR / Issue 状态药丸徽章 */
|
|
13
13
|
export declare function formatStateBadge(state?: string, isDraft?: boolean): string;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* 格式化 CI 工作流状态徽章。
|
|
16
|
+
* 第二参数按调用域语义不同:run 域传 conclusion(success/failure/cancelled/skipped),
|
|
17
|
+
* pr checks 域传 bucket(pass/fail/pending/skipping/cancel)——取值族不同但映射到
|
|
18
|
+
* 同一组徽章;state 参数兜底(in_progress/queued 等进行中态)。
|
|
19
|
+
*/
|
|
15
20
|
export declare function formatCiBadge(status?: string, conclusion?: string): string;
|
|
16
21
|
/** 格式化合并冲突状态 */
|
|
17
22
|
export declare function formatConflictBadge(status?: string): string;
|
|
@@ -35,18 +35,25 @@ export function formatStateBadge(state, isDraft) {
|
|
|
35
35
|
return '🔴 CLOSED';
|
|
36
36
|
return s;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* 格式化 CI 工作流状态徽章。
|
|
40
|
+
* 第二参数按调用域语义不同:run 域传 conclusion(success/failure/cancelled/skipped),
|
|
41
|
+
* pr checks 域传 bucket(pass/fail/pending/skipping/cancel)——取值族不同但映射到
|
|
42
|
+
* 同一组徽章;state 参数兜底(in_progress/queued 等进行中态)。
|
|
43
|
+
*/
|
|
39
44
|
export function formatCiBadge(status, conclusion) {
|
|
40
45
|
const conc = (conclusion || '').toLowerCase();
|
|
41
46
|
const st = (status || '').toLowerCase();
|
|
42
|
-
if (conc === 'success')
|
|
43
|
-
return '✅
|
|
44
|
-
if (conc === 'failure')
|
|
45
|
-
return '❌
|
|
46
|
-
if (conc === 'cancelled')
|
|
47
|
-
return '🚫
|
|
48
|
-
if (conc === 'skipped')
|
|
49
|
-
return '⏭️
|
|
47
|
+
if (conc === 'success' || conc === 'pass')
|
|
48
|
+
return '✅ ' + conc;
|
|
49
|
+
if (conc === 'failure' || conc === 'fail')
|
|
50
|
+
return '❌ ' + conc;
|
|
51
|
+
if (conc === 'cancelled' || conc === 'cancel')
|
|
52
|
+
return '🚫 ' + conc;
|
|
53
|
+
if (conc === 'skipped' || conc === 'skipping')
|
|
54
|
+
return '⏭️ ' + conc;
|
|
55
|
+
if (conc === 'pending')
|
|
56
|
+
return '⏳ pending';
|
|
50
57
|
if (st === 'in_progress')
|
|
51
58
|
return '⏳ running';
|
|
52
59
|
if (st === 'queued')
|
|
@@ -112,14 +119,18 @@ export function renderPrMarkdown(args, value) {
|
|
|
112
119
|
return '当前 PR 暂无 CI Checks 运行记录。';
|
|
113
120
|
const rows = list.map((c) => {
|
|
114
121
|
const name = sanitizeCell(c.name, 40);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
// bucket 是 gh pr checks 的分类字段(pass/fail/pending/skipping/cancel),
|
|
123
|
+
// 优先于 state 呈现;state(SUCCESS/FAILURE/...)作为兜底——渲染器同步
|
|
124
|
+
// Issue #3 修复:旧 conclusion/detailsUrl 字段在 gh 中不存在。
|
|
125
|
+
const badge = formatCiBadge(c.state, c.bucket);
|
|
126
|
+
const url = c.link ? `[查看详情](${c.link})` : '-';
|
|
127
|
+
const wf = c.workflow ? sanitizeCell(c.workflow, 24) : '-';
|
|
128
|
+
return `| ${name} | ${badge} | ${wf} | ${url} |`;
|
|
118
129
|
});
|
|
119
130
|
return [
|
|
120
131
|
`**CI 检查结果 (${list.length} 项):**\n`,
|
|
121
|
-
'| 检查项名称 | 结论 / 状态 | 详情链接 |',
|
|
122
|
-
'| :--- | :--- | :--- |',
|
|
132
|
+
'| 检查项名称 | 结论 / 状态 | 工作流 | 详情链接 |',
|
|
133
|
+
'| :--- | :--- | :--- | :--- |',
|
|
123
134
|
...rows,
|
|
124
135
|
].join('\n');
|
|
125
136
|
}
|
|
@@ -228,6 +239,10 @@ export function renderIssueMarkdown(args, value) {
|
|
|
228
239
|
if (value.body) {
|
|
229
240
|
lines.push('\n**议题正文:**\n```markdown\n' + sanitizeCell(value.body, 300) + '\n```');
|
|
230
241
|
}
|
|
242
|
+
// 图片附件清单(view 附带 URL 清单,view_images 按需下载查看)
|
|
243
|
+
if (Array.isArray(value.images) && value.images.length > 0) {
|
|
244
|
+
lines.push(`- **📎 图片附件 (${value.images.length}):** 调用 \`github_issue(action: 'view_images', issue_number: ${value.number})\` 可下载查看`);
|
|
245
|
+
}
|
|
231
246
|
return lines.join('\n');
|
|
232
247
|
}
|
|
233
248
|
// 兜底降级
|
|
@@ -292,6 +307,18 @@ export function renderRepoMarkdown(args, value) {
|
|
|
292
307
|
return '无返回内容';
|
|
293
308
|
if (typeof value.result === 'string')
|
|
294
309
|
return value.result;
|
|
310
|
+
// 4.0 README 预览(文本截断版 + 图片清单)——必须在 message 早退分支之前:
|
|
311
|
+
// readme 返回同时带 message 与 readme 字段,先判 readme 才能渲染预览而非一行摘要
|
|
312
|
+
if (args?.action === 'readme' && typeof value.readme === 'string') {
|
|
313
|
+
const lines = [`### 📖 README 预览`, '', '```markdown', value.readme, '```'];
|
|
314
|
+
if (Array.isArray(value.images) && value.images.length > 0) {
|
|
315
|
+
lines.push('', `**📎 图片清单 (${value.images.length}):** 可用 \`image_urls\` 参数下载指定图片(留空下载全部)`);
|
|
316
|
+
value.images.slice(0, 8).forEach((u, i) => lines.push(`${i + 1}. ${u}`));
|
|
317
|
+
if (value.images.length > 8)
|
|
318
|
+
lines.push(`…共 ${value.images.length} 个`);
|
|
319
|
+
}
|
|
320
|
+
return lines.join('\n');
|
|
321
|
+
}
|
|
295
322
|
if (typeof value.message === 'string' && !value.items) {
|
|
296
323
|
return `**${value.message}**` + (value.output ? `\n\`\`\`\n${value.output}\n\`\`\`` : '');
|
|
297
324
|
}
|
package/lib/tools/help.js
CHANGED
|
@@ -19,6 +19,9 @@ const KNOWLEDGE_BASE = {
|
|
|
19
19
|
pr: `### 🔀 github_pr 详细操作指南
|
|
20
20
|
|
|
21
21
|
- **查看列表**: \`github_pr(action: 'list', state: 'open' | 'closed' | 'merged' | 'all', limit: 20, author: '@me')\`
|
|
22
|
+
- **下载并查看 PR 图片**: \`github_pr(action: 'view_images', pr_number: 123)\`
|
|
23
|
+
- 提取范围覆盖 PR 正文 + 评论,经 gh api 下载(UI 变更对比图、复现截图等)到本地并以图片内容块返回,Agent 审查时可直接看图;魔数校验 + 字节护栏与 Issue 侧一致。
|
|
24
|
+
- **按 URL 精准获取**: 附加 \`image_urls: [...]\` 只下载指定图片;触达目录上限时返回 remaining 清单可分批续传(与 Issue 侧用法完全一致)。
|
|
22
25
|
- **本地检出代码**: \`github_pr(action: 'checkout', pr_number: 123)\`
|
|
23
26
|
- 自动切换到本地分支并拉取最新提交,非常适用于 Agent 在本地沙箱中跑单元测试验证 PR 改动。
|
|
24
27
|
- **差异比对**: \`github_pr(action: 'diff', pr_number: 123, stat_only: true)\`
|
|
@@ -30,6 +33,12 @@ const KNOWLEDGE_BASE = {
|
|
|
30
33
|
issue: `### 🎯 github_issue 详细操作指南
|
|
31
34
|
|
|
32
35
|
- **筛选 Issue**: \`github_issue(action: 'list', state: 'open' | 'closed' | 'all', labels: ['bug'], assignees: ['octocat'], limit: 20)\`
|
|
36
|
+
- **查看 Issue 附带截图**: \`github_issue(action: 'view', issue_number: 42)\`
|
|
37
|
+
- 返回正文中的图片 URL 清单(用户拖拽上传的报障截图等);需要"看图"时执行下方 view_images。
|
|
38
|
+
- **下载并查看 Issue 图片**: \`github_issue(action: 'view_images', issue_number: 42)\`
|
|
39
|
+
- 经 gh api 下载正文+评论图片到本地(~/.dsh/data/dsh-github-flow/images/,内容寻址去重),以图片内容块返回,Agent 可直接查看用户报障截图;魔数校验 + 字节护栏(单图 5MB、目录 15MB)。
|
|
40
|
+
- **按 URL 精准获取**: 附加 \`image_urls: ['<view 返回清单中的 URL>']\` 只下载指定图片(可单张可批量);传入 URL 必须在清单内(防任意下载)。
|
|
41
|
+
- 触达目录上限时返回 \`remaining\` 结构化清单,直接作为下一批 image_urls 续传即可。
|
|
33
42
|
- **关联开发分支**: \`github_issue(action: 'develop', issue_number: 42, branch_name: 'fix-login-bug')\`
|
|
34
43
|
- 自动在远端和本地创建带有官方关联关系的开发分支并切换过去,实现“读需求 -> 开分支写代码”的无缝衔接。
|
|
35
44
|
- **编辑 Issue**: \`github_issue(action: 'edit', issue_number: 42, title: '新标题', add_labels: ['p1'], remove_labels: ['triage'])\`
|
|
@@ -49,6 +58,9 @@ const KNOWLEDGE_BASE = {
|
|
|
49
58
|
|
|
50
59
|
- **查看元数据与标签**: \`github_repo(action: 'view')\`
|
|
51
60
|
- 返回仓库基本信息、默认分支、Star 计数、许可证以及 \`repositoryTopics\`(用于 DSH 插件合规审查)。
|
|
61
|
+
- **读取 README 及截图**: \`github_repo(action: 'readme', repo: 'owner/repo')\`
|
|
62
|
+
- 返回 README 原文与图片 URL 清单(仓库内相对路径自动解析为 raw 链接;默认过滤徽章小图标,\`include_badges: true\` 保留)。
|
|
63
|
+
- **下载图片查看**: 附加 \`image_urls: [...]\` 精准下载指定图(留空下载全部截图);护栏与 Issue/PR 图片一致,触顶返回 remaining 可续传。
|
|
52
64
|
- **一键拉平 Fork 主干 (防止 Stale)**: \`github_repo(action: 'sync', branch: 'main')\`
|
|
53
65
|
- 解决开源项目贡献中因本地 Fork 落后导致上游 CI 门禁失败的顽疾。
|
|
54
66
|
- **代码检索**: \`github_repo(action: 'search_code', query: 'filename:package.json dsh.bundle')\``,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把 README 中的仓库内相对图片路径解析为 raw.githubusercontent 绝对 URL。
|
|
3
|
+
* 强制落在 raw 域名模板内——不存在拼出任意域名的可能(SSRF 白名单天然覆盖)。
|
|
4
|
+
* 路径以 ./ ../ /docs/ 等形态出现;URL 编码空格等字符;无效形态返回 null。
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveRelativeImageUrl(rel: string, owner: string, repo: string, branch: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* 判断 URL 是否为徽章类小图标(shields.io / badge 类 svg)。
|
|
9
|
+
* README 图片场景默认过滤:用户要看的是截图/架构图,版本徽章是排版噪音;
|
|
10
|
+
* include_badges=true 时保留(有人确实要查徽章状态)。
|
|
11
|
+
*/
|
|
12
|
+
export declare function isBadgeImageUrl(url: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 从正文(Issue/PR body 或评论)提取图片 URL。
|
|
15
|
+
* 域名白名单 + https 扩展名双条件:
|
|
16
|
+
* - GitHub 四资产域名直接放行(用户上传/旧版上传/仓库引用/camo 代理);
|
|
17
|
+
* - 外链强制 https 且带图片扩展名——关闭恶意正文诱导访问内网元数据端点
|
|
18
|
+
* (169.254.169.254)等 SSRF 面,file:/// 与 http:// 一律拒绝。
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractImageUrls(body: unknown): string[];
|
|
21
|
+
/**
|
|
22
|
+
* README 专用提取:绝对 URL 走通用提取器;仓库内相对路径解析为 raw URL。
|
|
23
|
+
* 默认过滤徽章小图标(isBadgeImageUrl),include_badges 开关保留。
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractReadmeImageUrls(body: unknown, ctx: {
|
|
26
|
+
owner: string;
|
|
27
|
+
repo: string;
|
|
28
|
+
branch: string;
|
|
29
|
+
includeBadges?: boolean;
|
|
30
|
+
}): {
|
|
31
|
+
urls: string[];
|
|
32
|
+
relativeResolved: number;
|
|
33
|
+
badgeFiltered: number;
|
|
34
|
+
};
|
|
35
|
+
/** 从魔数推断图片 MIME(下载内容才是事实源——URL 扩展名可伪造) */
|
|
36
|
+
export declare function sniffImageMime(buf: Buffer): string | null;
|
|
37
|
+
/** 图片下载护栏:按真实资源消耗计算——单图字节与目录总量,无张数截断 */
|
|
38
|
+
export declare const IMG_MAX_BYTES: number;
|
|
39
|
+
export declare const IMG_DIR_MAX_BYTES: number;
|
|
40
|
+
export interface DownloadedImage {
|
|
41
|
+
file: string;
|
|
42
|
+
mimeType: string;
|
|
43
|
+
url: string;
|
|
44
|
+
bytes: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ImageBatchResult {
|
|
47
|
+
images: DownloadedImage[];
|
|
48
|
+
failures: string[];
|
|
49
|
+
/**
|
|
50
|
+
* 因目录总量护栏触顶而未处理的 URL(结构化续传清单)。
|
|
51
|
+
* Agent 可直接 image_urls: result.remaining 发起下一批——remaining 中的
|
|
52
|
+
* URL 本就来自正文合法清单,交集校验天然通过,分批获取无需清理目录。
|
|
53
|
+
*/
|
|
54
|
+
remaining?: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 交集过滤:把调用方请求的 URL 硬约束在正文实际提取的合法清单内。
|
|
58
|
+
* 为什么必须交集而非直接信任传入:image_urls 来自模型生成参数,存在幻觉或
|
|
59
|
+
* 被诱导传入任意 URL 的可能——交集把下载目标限定在本条 Issue/PR 引用过的
|
|
60
|
+
* 图片范围,SSRF 防线(提取阶段的 https+域名白名单)自动覆盖全部下载目标。
|
|
61
|
+
* 返回 { matched, rejected }:matched 保持请求顺序(Agent 按自己的关注顺序拿结果)。
|
|
62
|
+
*/
|
|
63
|
+
export declare function filterRequestedUrls(requested: unknown, extracted: string[]): {
|
|
64
|
+
matched: string[];
|
|
65
|
+
rejected: string[];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 批量下载图片并落盘(Issue/PR 的 view_images 共用执行体)。
|
|
69
|
+
* 每张经 executor 的二进制通道(gh api),魔数校验 + 三重护栏 + 内容寻址;
|
|
70
|
+
* 权威目录不可写时降级系统临时目录(只损失跨会话去重)。
|
|
71
|
+
* 单张失败不中断批次——failures 记录原因由调用方透传给 Agent。
|
|
72
|
+
*/
|
|
73
|
+
export declare function downloadImages(urls: string[], run: (url: string) => Promise<{
|
|
74
|
+
ok: boolean;
|
|
75
|
+
data?: unknown;
|
|
76
|
+
error?: string;
|
|
77
|
+
}>): Promise<ImageBatchResult>;
|
|
78
|
+
/** render 的 image 内容块构造(text 块由调用方自行拼):仅双合法字段才产出块 */
|
|
79
|
+
export declare function buildImageBlocks(value: any): any[];
|