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.
- package/README.en.md +162 -58
- package/README.md +167 -60
- package/docs/UPGRADE-NOTES.md +93 -0
- package/docs/architecture.md +47 -17
- 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 +26 -3
- package/lib/api/route-helpers.d.ts +47 -0
- package/lib/api/route-helpers.js +134 -0
- package/lib/api/routes-actions.d.ts +23 -0
- package/lib/api/routes-actions.js +239 -0
- package/lib/api/routes-lifecycle.d.ts +9 -0
- package/lib/api/routes-lifecycle.js +126 -0
- package/lib/api/routes-overview.d.ts +11 -0
- package/lib/api/routes-overview.js +227 -0
- package/lib/api/routes-proxy.d.ts +18 -0
- package/lib/api/routes-proxy.js +63 -0
- package/lib/api/routes.d.ts +16 -6
- package/lib/api/routes.js +14 -504
- package/lib/client.js +785 -90
- package/lib/commands/gh.js +11 -2
- package/lib/executor.d.ts +17 -0
- package/lib/executor.js +146 -8
- package/lib/main.d.ts +1 -0
- package/lib/main.js +49 -9
- package/lib/proxy.d.ts +87 -0
- package/lib/proxy.js +275 -0
- package/lib/renderers/markdown.d.ts +29 -0
- package/lib/renderers/markdown.js +520 -0
- package/lib/tools/api.d.ts +4 -0
- package/lib/tools/api.js +14 -4
- package/lib/tools/help.d.ts +31 -0
- package/lib/tools/help.js +224 -0
- package/lib/tools/image-utils.d.ts +79 -0
- package/lib/tools/image-utils.js +247 -0
- package/lib/tools/issue.d.ts +40 -4
- package/lib/tools/issue.js +167 -19
- package/lib/tools/pr.d.ts +29 -4
- package/lib/tools/pr.js +144 -18
- package/lib/tools/release.d.ts +89 -0
- package/lib/tools/release.js +219 -0
- package/lib/tools/repo.d.ts +24 -4
- package/lib/tools/repo.js +114 -14
- package/lib/tools/run.d.ts +22 -1
- package/lib/tools/run.js +74 -14
- package/lib/tools/secret.d.ts +60 -0
- package/lib/tools/secret.js +175 -0
- package/lib/tools/skill.d.ts +63 -0
- package/lib/tools/skill.js +146 -0
- package/lib/types.d.ts +24 -1
- package/lib/update-settings.d.ts +18 -0
- package/lib/update-settings.js +60 -0
- package/lib/updater.d.ts +40 -1
- package/lib/updater.js +126 -9
- package/lib/upgrade-manager.d.ts +74 -1
- package/lib/upgrade-manager.js +356 -46
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +3 -2
- package/lib/client/icons.d.ts +0 -13
- package/lib/client/icons.js +0 -41
- package/lib/client/index.d.ts +0 -3
- package/lib/client/index.js +0 -36
- package/lib/client/panel.d.ts +0 -1
- package/lib/client/panel.js +0 -102
- package/lib/client/style.d.ts +0 -1
- package/lib/client/style.js +0 -555
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.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GhExecutionOptions, GhResult, AuthStatus, RepoMetadata, PluginConfig } from './types.js';
|
|
2
|
+
import type { ProxyManager } from './proxy.js';
|
|
2
3
|
/**
|
|
3
4
|
* 按「字符数 + UTF-8 字节数」双口径截断输出。
|
|
4
5
|
* 抽成纯函数是刻意的:截断逻辑是崩溃高发区(切碎多字节字符、误伤 JSON、把二进制流截成垃圾),
|
|
@@ -14,10 +15,26 @@ export declare class GhExecutor {
|
|
|
14
15
|
maxOutputChars: number;
|
|
15
16
|
/** UTF-8 字节口径的硬上限;未配置时按 maxOutputChars × 4 推导(覆盖 CJK 3 字节与 emoji 4 字节) */
|
|
16
17
|
maxOutputBytes: number;
|
|
18
|
+
/**
|
|
19
|
+
* 列表类查询的默认条数上限。
|
|
20
|
+
* 为什么挂在 executor 而不是给每个工具工厂加 config 参数:executor 是全部
|
|
21
|
+
* 工具共享的依赖注入点,配置语义本就属于「执行层默认值」;工厂签名零改动,
|
|
22
|
+
* 且热更新路径 updateConfig 能同步刷新。工具内以 `?? 20` 兜底保证旧配置兼容。
|
|
23
|
+
*/
|
|
24
|
+
defaultListLimit: number;
|
|
25
|
+
/**
|
|
26
|
+
* 代理管理器引用(可选依赖)。
|
|
27
|
+
* 注入后每次 gh 子进程拉起都会合并代理环境变量——这是「工具调用 + Web 页面数据
|
|
28
|
+
* 全部走代理」的唯一咽喉(两条链路共用本 executor)。未注入时零干预,
|
|
29
|
+
* gh 自然继承宿主系统环境变量(系统级代理或直连),即「默认走系统设置」语义。
|
|
30
|
+
*/
|
|
31
|
+
private proxyManager?;
|
|
17
32
|
/** 认证状态缓存。`gh auth status` 实测 5.4 秒,而它位于全局驾驶舱关键路径的最前面 */
|
|
18
33
|
private authCache;
|
|
19
34
|
constructor(config?: PluginConfig);
|
|
20
35
|
updateConfig(config?: PluginConfig): void;
|
|
36
|
+
/** 注入代理管理器(main.ts 装配时调用一次;测试可注入 mock) */
|
|
37
|
+
attachProxyManager(manager: ProxyManager): void;
|
|
21
38
|
/**
|
|
22
39
|
* 智能解析最可靠的工作区物理路径:
|
|
23
40
|
* 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
|
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
|
/**
|
|
@@ -95,6 +173,20 @@ export class GhExecutor {
|
|
|
95
173
|
maxOutputChars;
|
|
96
174
|
/** UTF-8 字节口径的硬上限;未配置时按 maxOutputChars × 4 推导(覆盖 CJK 3 字节与 emoji 4 字节) */
|
|
97
175
|
maxOutputBytes;
|
|
176
|
+
/**
|
|
177
|
+
* 列表类查询的默认条数上限。
|
|
178
|
+
* 为什么挂在 executor 而不是给每个工具工厂加 config 参数:executor 是全部
|
|
179
|
+
* 工具共享的依赖注入点,配置语义本就属于「执行层默认值」;工厂签名零改动,
|
|
180
|
+
* 且热更新路径 updateConfig 能同步刷新。工具内以 `?? 20` 兜底保证旧配置兼容。
|
|
181
|
+
*/
|
|
182
|
+
defaultListLimit;
|
|
183
|
+
/**
|
|
184
|
+
* 代理管理器引用(可选依赖)。
|
|
185
|
+
* 注入后每次 gh 子进程拉起都会合并代理环境变量——这是「工具调用 + Web 页面数据
|
|
186
|
+
* 全部走代理」的唯一咽喉(两条链路共用本 executor)。未注入时零干预,
|
|
187
|
+
* gh 自然继承宿主系统环境变量(系统级代理或直连),即「默认走系统设置」语义。
|
|
188
|
+
*/
|
|
189
|
+
proxyManager;
|
|
98
190
|
/** 认证状态缓存。`gh auth status` 实测 5.4 秒,而它位于全局驾驶舱关键路径的最前面 */
|
|
99
191
|
authCache = null;
|
|
100
192
|
constructor(config = {}) {
|
|
@@ -102,12 +194,15 @@ export class GhExecutor {
|
|
|
102
194
|
this.defaultTimeoutMs = config.defaultTimeoutMs || 30_000;
|
|
103
195
|
this.maxOutputChars = config.maxOutputChars || 24_000;
|
|
104
196
|
this.maxOutputBytes = config.maxOutputBytes || this.maxOutputChars * 4;
|
|
197
|
+
this.defaultListLimit = config.defaultListLimit || 20;
|
|
105
198
|
}
|
|
106
199
|
updateConfig(config = {}) {
|
|
107
200
|
if (config.ghPath)
|
|
108
201
|
this.ghPath = config.ghPath;
|
|
109
202
|
if (config.defaultTimeoutMs)
|
|
110
203
|
this.defaultTimeoutMs = config.defaultTimeoutMs;
|
|
204
|
+
if (config.defaultListLimit)
|
|
205
|
+
this.defaultListLimit = config.defaultListLimit;
|
|
111
206
|
if (config.maxOutputChars) {
|
|
112
207
|
this.maxOutputChars = config.maxOutputChars;
|
|
113
208
|
if (!config.maxOutputBytes)
|
|
@@ -116,6 +211,10 @@ export class GhExecutor {
|
|
|
116
211
|
if (config.maxOutputBytes)
|
|
117
212
|
this.maxOutputBytes = config.maxOutputBytes;
|
|
118
213
|
}
|
|
214
|
+
/** 注入代理管理器(main.ts 装配时调用一次;测试可注入 mock) */
|
|
215
|
+
attachProxyManager(manager) {
|
|
216
|
+
this.proxyManager = manager;
|
|
217
|
+
}
|
|
119
218
|
/**
|
|
120
219
|
* 智能解析最可靠的工作区物理路径:
|
|
121
220
|
* 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
|
|
@@ -164,21 +263,60 @@ export class GhExecutor {
|
|
|
164
263
|
async run(args, options = {}) {
|
|
165
264
|
const cwd = this.resolveWorkspaceCwd(options.cwd, options.sessionId);
|
|
166
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;
|
|
167
270
|
try {
|
|
168
|
-
|
|
271
|
+
// stdin 显式传入时走 spawn 通道:execFile 的 input 选项在本机从不真正写入
|
|
272
|
+
// (详见 spawnWithStdin 注释),gh secret set 等命令会挂到超时。其余调用
|
|
273
|
+
// 保持 execFileAsync 原路径零改动。
|
|
274
|
+
const commonOpts = {
|
|
169
275
|
cwd,
|
|
170
|
-
timeout,
|
|
171
|
-
maxBuffer: 15 * 1024 * 1024, // 15MB 进程缓冲
|
|
172
276
|
env: {
|
|
173
277
|
...process.env,
|
|
174
278
|
GH_NO_UPDATE_NOTIFIER: '1', // 禁用 CLI 版本更新检查,避免污染 stdout
|
|
175
279
|
NO_COLOR: '1', // 禁用 ANSI 颜色转义字符
|
|
280
|
+
// 代理环境变量注入在 options.env 之前:调用方显式传入的 env 仍可覆盖
|
|
281
|
+
// 代理(保持 options.env 的既有语义优先级),正常调用方不传则代理生效。
|
|
282
|
+
...(this.proxyManager?.getEnv('gh') || {}),
|
|
176
283
|
...options.env,
|
|
177
284
|
},
|
|
178
|
-
}
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
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();
|
|
182
320
|
}
|
|
183
321
|
const outcome = truncateOutput(rawOutput, this.maxOutputChars, this.maxOutputBytes);
|
|
184
322
|
rawOutput = outcome.text;
|
package/lib/main.d.ts
CHANGED
package/lib/main.js
CHANGED
|
@@ -4,11 +4,17 @@ import { createIssueTool } from './tools/issue.js';
|
|
|
4
4
|
import { createRunTool } from './tools/run.js';
|
|
5
5
|
import { createRepoTool } from './tools/repo.js';
|
|
6
6
|
import { createApiTool } from './tools/api.js';
|
|
7
|
+
import { createReleaseTool } from './tools/release.js';
|
|
8
|
+
import { createSecretTool } from './tools/secret.js';
|
|
9
|
+
import { createSkillTool } from './tools/skill.js';
|
|
10
|
+
import { createHelpTool } from './tools/help.js';
|
|
7
11
|
import { registerGhCommand } from './commands/gh.js';
|
|
8
12
|
import { registerApiRoutes } from './api/routes.js';
|
|
9
13
|
import { UpdateChecker } from './updater.js';
|
|
10
14
|
import { UpgradeManager } from './upgrade-manager.js';
|
|
15
|
+
import { ProxyManager } from './proxy.js';
|
|
11
16
|
import { NPM_PACKAGE_URL } from './version.js';
|
|
17
|
+
import { loadUpdateSettings } from './update-settings.js';
|
|
12
18
|
// 引用即生效:加载本模块以挂上 declare module '@deepseek-ai/cordis' 的 ctx.github 类型声明,
|
|
13
19
|
// 并随下面 `export * from './github-service.js'` 一起传给消费方。
|
|
14
20
|
import './github-service.js';
|
|
@@ -41,6 +47,7 @@ export const Config = {
|
|
|
41
47
|
checkUpdate: typeof cfg.checkUpdate === 'boolean' ? cfg.checkUpdate : true,
|
|
42
48
|
updateCheckIntervalMs: typeof cfg.updateCheckIntervalMs === 'number' ? cfg.updateCheckIntervalMs : 86_400_000,
|
|
43
49
|
registryUrl: typeof cfg.registryUrl === 'string' ? cfg.registryUrl : undefined,
|
|
50
|
+
proxyUrl: typeof cfg.proxyUrl === 'string' ? cfg.proxyUrl : undefined,
|
|
44
51
|
}
|
|
45
52
|
};
|
|
46
53
|
}
|
|
@@ -48,11 +55,26 @@ export const Config = {
|
|
|
48
55
|
};
|
|
49
56
|
export function apply(ctx, config = {}) {
|
|
50
57
|
const executor = new GhExecutor(config);
|
|
58
|
+
// 代理管理器:优先读取用户在驾驶舱保存的 proxy.json,回落 cordis.patch.yml 的
|
|
59
|
+
// proxyUrl 预置值;两者皆无时保持直连(gh 自然继承宿主系统环境变量)。
|
|
60
|
+
// 装配后 executor/upgradeManager/updater 三条外联链路全部经它路由。
|
|
61
|
+
const proxyManager = new ProxyManager(config.proxyUrl);
|
|
62
|
+
executor.attachProxyManager(proxyManager);
|
|
51
63
|
const updateChecker = new UpdateChecker({
|
|
52
64
|
intervalMs: config.updateCheckIntervalMs,
|
|
53
65
|
customRegistry: config.registryUrl,
|
|
66
|
+
// checkUpdate: false 熔断全部探测入口(路由/命令/启动横幅共用此实例)
|
|
67
|
+
enabled: config.checkUpdate !== false,
|
|
54
68
|
});
|
|
69
|
+
// 更新源设置(UI 持久化 store 优先于 Cordis 静态配置):
|
|
70
|
+
// 启动时读取并应用到 checker;UI 保存时经路由直接调 applySettings 热切换。
|
|
71
|
+
const updateMode = loadUpdateSettings();
|
|
72
|
+
updateChecker.applySettings(updateMode);
|
|
73
|
+
updateChecker.applyProxy(proxyManager);
|
|
55
74
|
const upgradeManager = new UpgradeManager();
|
|
75
|
+
upgradeManager.attachProxyManager(proxyManager);
|
|
76
|
+
// 注入查询函数而非静态值:用户切源后,下一次一键升级的 registry 实时跟随
|
|
77
|
+
upgradeManager.attachUpdateModeQuery(() => updateChecker.getSourceMode());
|
|
56
78
|
// 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
|
|
57
79
|
if (ctx.provide && typeof ctx.provide === 'function') {
|
|
58
80
|
ctx.provide('github', executor);
|
|
@@ -64,35 +86,52 @@ export function apply(ctx, config = {}) {
|
|
|
64
86
|
if (ctx.commands) {
|
|
65
87
|
registerGhCommand(ctx.commands, executor, updateChecker);
|
|
66
88
|
}
|
|
67
|
-
// 3. 注册 Agent
|
|
89
|
+
// 3. 注册 Agent 模型工具集(核心领域工具 + 元指南)
|
|
68
90
|
if (ctx.tools) {
|
|
69
91
|
ctx.tools.register(createPrTool(executor));
|
|
70
92
|
ctx.tools.register(createIssueTool(executor));
|
|
71
93
|
ctx.tools.register(createRunTool(executor));
|
|
72
94
|
ctx.tools.register(createRepoTool(executor));
|
|
73
95
|
ctx.tools.register(createApiTool(executor));
|
|
96
|
+
ctx.tools.register(createReleaseTool(executor));
|
|
97
|
+
ctx.tools.register(createSecretTool(executor));
|
|
98
|
+
ctx.tools.register(createSkillTool(executor));
|
|
99
|
+
ctx.tools.register(createHelpTool(executor));
|
|
74
100
|
}
|
|
75
101
|
// 4. 注册面向 Client 端的 WebServer HTTP 路由(如果当前环境启用了 Web UI 服务)
|
|
76
102
|
if (ctx.webServer) {
|
|
77
103
|
// 传入 ctx 作用域对象,使 routes 能够通过 ctx.effect 绑定生命周期并在插件卸载/重载时自动注销;
|
|
78
104
|
// cacheTtlMs 传给路由层以尊重用户的缓存 TTL 配置。
|
|
79
|
-
|
|
105
|
+
// proxyManager 传入以挂载 /api/github/proxy* 设置路由(驾驶舱代理设置入口)。
|
|
106
|
+
registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000, updateChecker, upgradeManager, proxyManager);
|
|
80
107
|
}
|
|
81
108
|
// 5. 启动后低优先级后台闲时静默探测:若有新版本,仅输出一次友好的控制台提示
|
|
82
109
|
if (config.checkUpdate !== false) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
// 包裹在 ctx.effect 中(Cordis 规范):热重载/卸载发生在 3 秒延迟窗口内时,
|
|
111
|
+
// 取消挂起的定时器,避免旧实例在新实例加载后仍向控制台打印升级横幅。
|
|
112
|
+
// 无头/测试环境缺 ctx.effect 时优雅退化为裸定时器(一次性触发,无泄漏)。
|
|
113
|
+
const scheduleProbe = () => {
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
updateChecker.check(false).then((info) => {
|
|
116
|
+
if (info.hasUpdate) {
|
|
117
|
+
console.log(`
|
|
87
118
|
┌────────────────────────────────────────────────────────────────────────┐
|
|
88
119
|
│ 🚀 dsh-github-flow 插件有新版本可用: ${info.currentVersion} -> ${info.latestVersion} │
|
|
89
120
|
│ 升级命令: ${info.upgradeCommand.padEnd(58, ' ')}│
|
|
90
121
|
│ 更新主页: ${NPM_PACKAGE_URL.padEnd(58, ' ')}│
|
|
91
122
|
└────────────────────────────────────────────────────────────────────────┘
|
|
92
123
|
`);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
}
|
|
125
|
+
}).catch(() => { });
|
|
126
|
+
}, 3_000);
|
|
127
|
+
return () => clearTimeout(timer);
|
|
128
|
+
};
|
|
129
|
+
if (typeof ctx.effect === 'function') {
|
|
130
|
+
ctx.effect(scheduleProbe);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
scheduleProbe();
|
|
134
|
+
}
|
|
96
135
|
}
|
|
97
136
|
}
|
|
98
137
|
export * from './types.js';
|
|
@@ -100,5 +139,6 @@ export * from './version.js';
|
|
|
100
139
|
export * from './updater.js';
|
|
101
140
|
export * from './upgrade-manager.js';
|
|
102
141
|
export * from './executor.js';
|
|
142
|
+
export * from './proxy.js';
|
|
103
143
|
// 让 `ctx.github` 的类型声明随包一起被消费方加载(声明合并在 './github-service.js' 内)
|
|
104
144
|
export * from './github-service.js';
|
package/lib/proxy.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 网络代理管理器:插件全部外联流量的唯一代理状态权威源。
|
|
3
|
+
*
|
|
4
|
+
* 设计依据(docs/plans/proxy-support-plan.md):
|
|
5
|
+
* - gh 子进程链路(9 工具 + Web API 数据)经标准环境变量 HTTPS_PROXY/HTTP_PROXY/NO_PROXY 驱动,
|
|
6
|
+
* 实测 gh CLI 完整遵循 Go net/http 约定;
|
|
7
|
+
* - Node fetch 链路(updater 的 npm registry 探测)无视环境变量,需 undici ProxyAgent
|
|
8
|
+
* dispatcher 驱动——插件自身不依赖 undici,运行时从 DSH 宿主 require 链动态解析;
|
|
9
|
+
* - 不走 Cordis Config 承载:配置变更会触发插件全量 reload(工具注销重注册),
|
|
10
|
+
* 而代理是高频试错型设置。持久化用独立文件,保存即生效、零重载。
|
|
11
|
+
* - 未设置时零干预:不注入任何环境变量、不动 fetch dispatcher,
|
|
12
|
+
* gh 与 fetch 自然回落到系统环境变量(若宿主本身配置了 HTTPS_PROXY)或直连。
|
|
13
|
+
*/
|
|
14
|
+
export interface ProxyConfig {
|
|
15
|
+
/** 代理地址,如 "http://127.0.0.1:7890"。空串 = 未启用(回落系统环境/直连) */
|
|
16
|
+
url: string;
|
|
17
|
+
/** NO_PROXY 排除列表(逗号分隔)。localhost,127.0.0.1 恒被强制前置,防止本机 Web API 回环请求被代理劫持 */
|
|
18
|
+
noProxy: string;
|
|
19
|
+
/** 是否同步应用于 npm registry 探测与一键升级链路(包管理器走环境变量,天然支持) */
|
|
20
|
+
applyToPackageManager: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ProxyProbeResult {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
at: number;
|
|
25
|
+
latencyMs?: number;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ProxyStateSnapshot {
|
|
29
|
+
config: ProxyConfig;
|
|
30
|
+
/** fetch 链路(undici dispatcher)是否可用。不可用时 npm 探测保持直连,GitHub 链路不受影响 */
|
|
31
|
+
fetchProxyAvailable: boolean;
|
|
32
|
+
lastProbe?: ProxyProbeResult;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 校验用户输入的代理地址。
|
|
36
|
+
* 为什么白名单 scheme:地址将注入到全部 gh/npm 子进程环境变量并可能驱动 fetch dispatcher,
|
|
37
|
+
* 放行 file://、gopher:// 等怪异 scheme 属于扩大攻击面;内嵌凭据会随进程 env 暴露,禁止保存。
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateProxyUrl(input: unknown): {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
url?: string;
|
|
42
|
+
error?: string;
|
|
43
|
+
};
|
|
44
|
+
export declare class ProxyManager {
|
|
45
|
+
private config;
|
|
46
|
+
private lastProbe;
|
|
47
|
+
private fetchProxyAvailable;
|
|
48
|
+
private activeDispatcherUrl;
|
|
49
|
+
/** 安装代理 dispatcher 前的进程默认 dispatcher 快照(关闭代理时恢复,防残留) */
|
|
50
|
+
private defaultDispatcher;
|
|
51
|
+
constructor(presetUrl?: string);
|
|
52
|
+
/** 读取持久化文件;不存在时回落 cordis.patch.yml 预置值;再无则保持默认直连 */
|
|
53
|
+
private load;
|
|
54
|
+
/**
|
|
55
|
+
* 原子写持久化文件:先写临时文件再 rename,杜绝并发/崩溃产生半截 JSON。
|
|
56
|
+
* 写盘失败(目录权限/沙箱)不抛出:代理是「尽力持久化」语义——内存态已生效,
|
|
57
|
+
* 仅丢失重启恢复能力;用一次 console.warn 提示用户,UI 仍能正常工作。
|
|
58
|
+
*/
|
|
59
|
+
private persist;
|
|
60
|
+
/**
|
|
61
|
+
* 生成注入子进程的代理环境变量。
|
|
62
|
+
* 未启用时返回 {}——绝不覆盖 process.env 中既有的系统级代理设置(用户不设插件代理
|
|
63
|
+
* 时 gh 自然继承宿主环境变量,即「默认走系统设置」语义)。
|
|
64
|
+
* socks5 对 gh(Go runtime)原生有效;对 npm 同样有效(pacote/agent-base 支持)。
|
|
65
|
+
*/
|
|
66
|
+
getEnv(scope?: 'gh' | 'package-manager'): Record<string, string>;
|
|
67
|
+
/**
|
|
68
|
+
* 应用 fetch dispatcher(npm registry 探测链路)。
|
|
69
|
+
* undici 的 ProxyAgent 不支持 socks5:该场景显式标记不可用并保持直连,
|
|
70
|
+
* UI 需如实提示差异(GitHub 链路正常走 socks5),不做静默降级。
|
|
71
|
+
* dispatcher 按 url 缓存重建,url 未变化时零开销。
|
|
72
|
+
*
|
|
73
|
+
* 关闭代理时必须恢复默认全局 dispatcher:setGlobalDispatcher 是进程级副作用,
|
|
74
|
+
* 若只清空本实例状态而不回滚 dispatcher,此前安装的 ProxyAgent 会永久残留,
|
|
75
|
+
* 「启用→关闭」后 npm 探测仍走死代理直到进程重启。首次安装前先快照原 dispatcher。
|
|
76
|
+
*/
|
|
77
|
+
applyFetchDispatcher(): boolean;
|
|
78
|
+
/** 保存新配置并立即生效(写文件 + 重应用 dispatcher),返回完整状态快照 */
|
|
79
|
+
save(input: Partial<ProxyConfig>): {
|
|
80
|
+
ok: boolean;
|
|
81
|
+
error?: string;
|
|
82
|
+
state: ProxyStateSnapshot;
|
|
83
|
+
};
|
|
84
|
+
/** 经当前代理配置探测 GitHub API 连通性(2.5s 超时)。失败不回滚保存——离线预配置是合法场景 */
|
|
85
|
+
probe(): Promise<ProxyProbeResult>;
|
|
86
|
+
snapshot(): ProxyStateSnapshot;
|
|
87
|
+
}
|